@clawhub-stanestane-85fbf0a896
Audit a game, feature, live-ops system, progression loop, social feature, or monetization surface using a Self-Determination Theory-inspired motivation frame...
--- name: game-design-player-motivation-audit description: Audit a game, feature, live-ops system, progression loop, social feature, or monetization surface using a Self-Determination Theory-inspired motivation framework. Use when evaluating what kind of motivation a design creates, comparing alternative motivational profiles, diagnosing why a system feels sticky, hollow, exhausting, or dead, checking overreliance on rewards and grind, or assessing whether a feature supports short-term activation, medium-term habit, or long-term player identity. --- # Game Design Player Motivation Audit Audit a design by asking not just whether it motivates action, but what kind of motivation it creates, for whom, and at what cost. Use this skill to distinguish between engagement driven by enjoyment, value endorsement, identity, social pressure, reward dependency, or helplessness. Keep the analysis practical and design-facing, but use terminology that stays broadly consistent with Self-Determination Theory. ## Core principle Not all motivation is equal. Two designs may generate similar engagement numbers while creating very different player experiences. One may be driven by genuine enjoyment. Another may be driven by reward compulsion, social pressure, or fear of missing out. This skill helps distinguish those motivational structures. ## Motivation spectrum Audit designs across the following motivation types: 1. **Amotivation** 2. **External Regulation** 3. **Introjected Regulation** 4. **Identified Regulation** 5. **Integrated Regulation** 6. **Intrinsic Motivation** These are not mutually exclusive. Strong game systems often combine several. ## What to produce Generate a motivation audit with these outputs: 1. **Target behavior** - what the design is trying to motivate 2. **Motivation breakdown** - which types of motivation are doing the work 3. **Need satisfaction check** - autonomy, competence, relatedness 4. **Motivational profile** - the overall mix and likely player effect 5. **Diagnosis** - how the experience is likely to feel 6. **Recommendations** - what to strengthen, reduce, or rebalance ## Process ### 1. Define the target behavior Clarify the behavior the design is trying to produce. Examples: - start a session - complete first 3 actions - return later in the day - finish an event track - engage with social systems - optimize city layout - master a mechanic - collect themed content - spend premium currency Write: - **Design being audited** - **Target player behavior** - **Time horizon** - immediate / short-term / medium-term / long-term - **Primary player segment** ### 2. Audit for amotivation Ask where the design may create helplessness, meaninglessness, confusion, or low-agency compliance. Amotivation is the failure state. It appears when actions feel empty, forced, over-scripted, or detached from perceived value. Signals: - player taps because the UI tells them to, not because they care - grind without meaningful context - tutorials or flows that feel like chasing arrows - actions with low perceived agency - delays or chores with no emotional or strategic meaning - story, prompts, or task lists that feel like friction rather than invitation Ask: - Does the player understand why this action matters? - Does the action feel voluntary or merely demanded? - Does the player feel capable of affecting the outcome? - Is any part of the flow just "push button to continue"? - Is the action connected to a player goal, fantasy, or meaningful payoff? Write: - **Amotivation risks** ### 3. Audit external regulation Ask how much of the design relies on rewards, punishments, reminders, deadlines, or pressure external to the activity itself. This is the classic "do X to get Y" layer. It is common in free-to-play systems and often useful for activation, but dangerous when it carries the whole design. Typical patterns: - event track milestones - login rewards - reward ladders - timed offers - battle pass tasks - grind gates with attractive rewards - loss aversion and FOMO structures Ask: - If the reward disappeared, would the action still be attractive? - Is the reward carrying the whole system? - Is the system sustainable, or will it feel hollow once novelty fades? - Is the reward structure clear and fair? - Is there a risk of converting play into chore behavior? Write: - **External regulation drivers** ### 4. Audit introjected regulation Ask where the design taps into ego, validation, self-image, pride, shame avoidance, status, or social comparison. This layer is about internal pressure: proving something, keeping up, avoiding embarrassment, preserving self-image, or feeling recognized. Typical patterns: - leaderboards - ranks - badges and medals - rare visible rewards - progression prestige markers - guild contribution surfaces - public completion proofs - comparative map or avatar placement Ask: - Does this design create bragging rights? - Does it create pressure to keep up with others? - Is social comparison a core driver here? - Does it encourage pride in participation, or anxiety about falling behind? - Who benefits from this layer, and who is demotivated by it? Write: - **Introjected regulation drivers** ### 5. Audit identified regulation Ask where the design helps players consciously endorse the value of the activity, even if the action itself is not always the most fun moment-to-moment. This is where the player believes the activity matters and accepts it as worthwhile. Typical patterns: - helping a club or team - maintaining a city or social ecosystem - contributing to a long-term build - performing support roles - doing tedious actions because they matter to a bigger goal - completing systems because the player values what they represent Ask: - Does the player feel this action matters beyond the immediate reward? - Is there a larger purpose attached to the task? - Does the design create a feeling of contribution? - Are players consciously buying into the value of the action? - Does the action support a long-range project, group, or personally meaningful goal? Write: - **Identified regulation drivers** ### 6. Audit integrated regulation Ask whether the activity can become part of the player's identity, self-concept, or ongoing role. This is a deeper layer of internalization. The player no longer just values the activity; they see it as part of who they are. Typical patterns: - specialized playstyles - creative mastery communities - collector identities - highly expressive builds or loadouts - role ownership within social groups - long-term hobbyist subsystems - deep feature ecosystems with room for expertise Ask: - Can a player say "I am the kind of player who does this"? - Does the system support long-term self-expression? - Does it reward specialization and ownership? - Can players build identity, reputation, or belonging around it? - Would superfans form around this system? Write: - **Integrated regulation drivers** ### 7. Audit intrinsic motivation Ask whether the activity itself is enjoyable enough to sustain engagement without heavy external scaffolding. Intrinsic motivation often depends on satisfaction of three psychological needs: #### A. Autonomy - meaningful choices - self-directed play - feeling like a causal agent - not merely obeying prompts #### B. Competence - clear feedback - felt improvement - controllable outcomes - satisfying mastery arc #### C. Relatedness - social connection - shared play - mutual recognition - meaningful belonging Ask: - Is the activity fun without the reward wrapper? - Does the player feel agency while doing it? - Does the player feel skillful, improving, or effective? - Does the system support connection or belonging? - Would engaged players choose this activity even with reduced extrinsic rewards? Use this format: | Need | Evidence of Satisfaction | Risk of Denial | |---|---|---| | Autonomy | ... | ... | | Competence | ... | ... | | Relatedness | ... | ... | ### 8. Map the motivational profile Summarize the overall mix of motivations the design relies on. Use this format: | Motivation Type | Strength (Low/Med/High) | Evidence | Design Implication | |---|---|---|---| | Amotivation risk | ... | ... | ... | | External regulation | ... | ... | ... | | Introjected regulation | ... | ... | ... | | Identified regulation | ... | ... | ... | | Integrated regulation | ... | ... | ... | | Intrinsic motivation | ... | ... | ... | Interpretation guidance: - healthy long-term designs often combine some external regulation for activation, some introjected energy for prestige or momentum, meaningful identified and integrated layers for depth, and strong intrinsic foundations for sustainability - risky profiles often show high amotivation risk, very high external regulation, weak intrinsic value, and weak deeper internalization ### 9. Diagnose likely player experience Translate the profile into likely felt experience. Example diagnoses: - reward-chasing but hollow - socially energizing but intimidating - compelling for superfans, cold for casuals - good short-term activation, weak long-term meaning - strong hobby potential, weak onboarding - low-agency chore loop - deeply satisfying mastery loop Write: - **Likely player experience** - **Best-fit audience** - **Main retention strength** - **Main motivational weakness** ### 10. Recommend motivational adjustments Recommend changes if the profile is not what the design needs. Adjustment levers: #### To reduce amotivation - improve context and meaning - reduce forced or empty taps - clarify why actions matter - restore agency and readable outcomes #### To reduce overreliance on external regulation - make the action itself more satisfying - add autonomy or mastery depth - reduce pure reward dependency #### To tune introjected regulation - add visible social proof carefully - add prestige markers without overpunishing the broader audience - reduce shame or unhealthy comparison pressure where needed #### To strengthen identified regulation - connect actions to larger goals, contribution, purpose, or personally endorsed value #### To strengthen integrated regulation - support specialization, ownership, self-expression, belonging, and long-term mastery communities #### To strengthen intrinsic motivation - improve core feel, choices, skill expression, feedback, and social connection Write: - **Recommendations** ## Response structure Use this structure unless the user asks for something else: ### Target Behavior - ... ### Amotivation Risks - ... ### Motivation Breakdown - External regulation: ... - Introjected regulation: ... - Identified regulation: ... - Integrated regulation: ... - Intrinsic motivation: ... ### Need Satisfaction Check - Autonomy: ... - Competence: ... - Relatedness: ... ### Motivational Profile - ... ### Recommendations - ... ## Fast mode Use this quick pass when speed matters: - What behavior is this trying to motivate? - Is the player doing it for the activity, the reward, status, purpose, or identity? - Where does the design risk feeling empty or forced? - Which needs does it satisfy: autonomy, competence, relatedness? - Is this best suited for activation, habit, or deep fandom? ## Usage notes This framework is especially useful for auditing: - event tracks - season passes - city journal or task systems - leaderboard and club features - long-term collection systems - retention surfaces - monetization loops - session-opener task flows - social meta structures Examples in game design terms: - **External regulation**: event rewards, pass milestones, claim loops - **Introjected regulation**: rankings, visible rewards, comparative progress - **Identified regulation**: club contribution, city stewardship, completion of meaningful builds - **Integrated regulation**: collector identity, city stylist identity, deep feature ownership - **Intrinsic motivation**: enjoyment of city-building, optimization, planning, and self-expression itself ## Working principle A design is not strong merely because it gets players to act. It is strong when it motivates action in a way that is healthy, satisfying, and sustainable for the intended audience.
Structure game design discussions using GROW to clarify goals, assess reality, explore options, and create actionable recommendations for better decisions an...
--- name: game-design-grow-design description: Evaluate game features, feature pitches, live-ops ideas, UX changes, economy changes, roadmap choices, retention initiatives, monetization initiatives, and ambiguous design problems using the GROW model: Goal, Reality, Options, Will. Use when a team needs structure, clearer decision-making, better option generation, explicit tradeoff analysis, or a concrete recommendation and next-step plan instead of circular discussion. --- # Game Design GROW Design Use the GROW model to turn vague game design conversations into a structured decision flow: **Goal -> Reality -> Options -> Will** Use this skill when a team has ideas, concerns, and opinions, but not enough shared structure to reach a decision. Keep the framework practical, explicit, and usable by teams that need help clarifying what they want, what is true right now, what they could do, and what they should commit to next. ## What to produce Generate a structured decision pass with these outputs: 1. **Goal** - a concise statement of the intended outcome and success criteria 2. **Reality** - the current state, constraints, dependencies, and unknowns 3. **Options** - multiple credible paths, not just one preferred answer 4. **Will** - a recommendation, immediate next steps, and validation needs ## Process ### 1. Goal Clarify what the team is trying to achieve. Ask: - What player problem does this solve? - What player behavior should change? - What business or product goal does this support? - How should this fit existing game loops and systems? - What does success look like in player terms? - What does success look like in KPI terms? - What are the quality constraints? - What is the release window or decision horizon? Use a SMART-style structure: - **Specific** - clearly describe the feature intent - **Measurable** - define KPIs or evaluation signals - **Attainable** - keep the goal feasible within team and technical limits - **Relevant** - connect the idea to game strategy and product priorities - **Time-boxed** - align it to a release, milestone, or decision window Write: - **Goal statement** - **Success criteria** - **Key constraints** Suggested format: **Goal statement** We want to [player/business outcome] by introducing or changing [feature/system], measured by [metrics/signals], within [timeframe]. ### 2. Reality Describe what is true right now. Build a grounded picture of the current situation. Ask: - What is the current player experience? - What already exists that overlaps with this idea? - Which systems would this touch? - What infrastructure, tools, or content pipelines already exist? - What are the open design questions? - What resourcing constraints exist? - What tech, UX, or economy limitations exist? - What assumptions are currently unverified? - Which comparable features already exist in this game or similar games? - What data or prior learnings matter? Useful categories: - **Game state** - existing systems, loops, progression context - **Player state** - motivations, friction, expectations - **Business state** - targets, roadmap role, cannibalization risk - **Production state** - scope, dependencies, tech constraints - **Evidence state** - telemetry, benchmarks, prior experiments, team learnings Write: - **Current state** - **Constraints** - **Unknowns** - **Dependencies** Suggested format: **Reality summary** Current state: ... Constraints: ... Unknowns: ... Dependencies: ... ### 3. Options Generate multiple credible solution paths before recommending anything. General rule: always produce several options, even when one path seems obvious. #### Option-generation methods Choose one or combine several: **A. Five-options approach** Use when several candidate solutions already exist. - list at least 3-5 approaches - define pros and cons for each - compare expected impact and implementation cost - identify the fastest testable version **B. Obstacle approach** Use when the team is blocked by one obvious problem. - name the roadblock clearly - imagine it removed - describe the resulting design space - derive workaround paths **C. Ideal-outcome approach** Use when direction is unclear but the end state is clear. - describe the ideal player experience - work backwards from that state - identify enabling components and milestones **D. Transformative approach** Use when building on existing systems is likely better than inventing from scratch. - identify reusable systems, UI, content pipelines, currencies, or loops - ask what can be tuned, recombined, or extended - prefer leverage over novelty where appropriate **E. Thinking outside the box** Use when the problem is understood but no satisfying approach exists. - brainstorm without commitment - deliberately include non-obvious approaches - separate idea generation from evaluation #### Evaluate each option For each option, score: - **Player value** (1-5) - **Business value** (1-5) - **Implementation cost** (1-5) - **Risk / uncertainty** (1-5) - **Strategic fit** (1-5) Capture the main tradeoffs, not just the scores. Suggested format: | Option | Summary | Player Value | Business Value | Cost | Risk | Notes | |---|---|---:|---:|---:|---:|---| | A | ... | ... | ... | ... | ... | ... | | B | ... | ... | ... | ... | ... | ... | ### 4. Will Decide what should happen now. Turn the discussion into a concrete action plan. Ask: - Which option is recommended and why? - What is the smallest meaningful version? - What should happen now versus later? - What needs validation before full commitment? - What is the roadmap position? - What workstreams are required? - What are the next decisions, owners, or documents needed? Possible outputs: - recommendation - MVP or prototype scope - backlog framing - roadmap sequencing - task breakdown - test plan - KPI follow-up plan Write: - **Recommendation** - **Why this option** - **Immediate next steps** - **Validation needed** - **Risks to monitor** ## Response structure Use this structure unless the user asks for something else: ### Goal - ... ### Reality - ... ### Options 1. ... 2. ... 3. ... ### Will - Recommended path: ... - Near-term actions: ... - Risks / assumptions: ... ## Fast mode Use this condensed version for rapid feature triage: - **Goal**: What outcome are we after? - **Reality**: What constraints and dependencies matter? - **Options**: What are 3 plausible solution paths? - **Will**: Which one should we do now, and what is the first concrete step? ## Usage notes This skill is especially useful for: - new features - feature revisions - UX or economy changes - roadmap choices - retention initiatives - monetization initiatives - live-ops content or event structures - ambiguous design problems that risk circular discussion It is particularly suitable for teams that need extra structure, explicit tradeoff framing, and help converting a loose discussion into a decision path. When relevant, combine this framework with: - source-material lookup from update docs - implementation reality from config files - KPI or economy analysis from spreadsheets - player-facing experience summaries ## Working principle Use this framework to prevent two common failure modes: 1. jumping from vague ambition to implementation detail too early 2. cycling between ideas without committing to a decision path The intent is not bureaucracy for its own sake. The intent is structured design judgment that helps uncertain teams move forward.
Define, refine, and evaluate the emotional identity, feeling, atmosphere, and vibe of a game, feature, event, region, or content theme. Use when shaping a ga...
--- name: game-design-emotional-canvas description: Define, refine, and evaluate the emotional identity, feeling, atmosphere, and vibe of a game, feature, event, region, or content theme. Use when shaping a game's emotional core, building a moodboard brief, clarifying the intended player feeling, checking whether a concept feels emotionally hollow, aligning art/audio/narrative around one emotional promise, or reviewing whether new content reinforces or dilutes the intended mood. --- # Game Design Emotional Canvas Center design conversations on feeling. Use this skill to help a game concept feel like something specific, memorable, and emotionally coherent. Keep the work high-level. Focus on emotional identity, tone, atmosphere, and sensory direction rather than detailed mechanics, balancing, systems design, or production planning. Treat emotion as a first-class creative target. A game can be functional, polished, and content-rich while still feeling emotionally anonymous. This skill exists to prevent that. ## What to produce Generate a compact emotional canvas with these outputs: 1. **Target feeling** - the dominant emotional promise 2. **Emotional field** - adjacent and opposing feelings 3. **Emotional palette** - sensory and aesthetic ingredients that reinforce the feeling 4. **Anti-patterns** - choices that weaken or break the mood 5. **Design heuristics** - short creative principles for future reviews 6. **Moodboard brief** - a handoff-ready summary for adjacent disciplines ## Working stance Stay evocative, clear, and a little poetic when useful. Do not drift into detailed feature design unless the user explicitly asks for it. Keep the conversation centered on vibe, fantasy, atmosphere, emotional texture, and player aftertaste. Prefer one strong feeling over a muddy blend. ## Process ### 1. Name the target feeling Identify the dominant feeling the experience should leave behind. Ask: - What should the player feel most strongly? - What emotional aftertaste should remain after a session? - What emotional promise is this game making? Write: - **Target feeling** - **Short definition** - **Why it matters** Keep this singular and clear. ### 2. Describe the emotional texture Translate the target feeling into language. List: - adjectives - sensations - emotional tensions - states of mind - social or atmospheric qualities This should describe not just what the feeling is, but how it feels in the body and imagination. ### 3. Map the emotional field Clarify what sits near the target feeling and what pulls against it. Include: - **Adjacent feelings** - emotions that support, shade, or enrich the target - **Contrasting feelings** - emotions that weaken, flatten, or contradict it Use this to avoid vagueness. ### 4. Build the emotional palette Translate the feeling into concrete but high-level creative ingredients. Cover only the domains that matter for the current task. Possible domains: - **Visuals** - color, lighting, softness, contrast, density, shape language - **Materials** - textures, surfaces, age, tactility, polish, roughness - **Audio** - ambience, silence, warmth, sharpness, rhythm, tone - **Environment** - weather, openness, shelter, clutter, scale, intimacy - **Objects** - props, motifs, symbolic items, recurring imagery - **Characters** - presence, body language, relationship energy, emotional role - **Narrative tone** - implied stories, voice, restraint, sentiment, mystery Stay at the level of mood and aesthetic direction. Do not turn this into a mechanics checklist. ### 5. Identify emotional anti-patterns List the elements that would break the intended feeling. Look for: - tonal clashes - visual contradictions - emotional overstatement - flattening language - mood-breaking presentation - elements that feel too sterile, too noisy, too cheerful, too harsh, or too explicit for the target feeling Frame each anti-pattern in terms of why it damages the vibe. ### 6. Turn the feeling into heuristics Create 5-8 short creative principles that can guide later reviews. Write them as directional preferences, such as: - softness over harshness - suggestion over explanation - intimacy over spectacle These should feel memorable enough to use in critique, ideation, and alignment conversations. ### 7. Evaluate the concept holistically Review whether the current concept, feature, or theme actually expresses one emotional center. Ask: - What is the strongest emotional signal right now? - What feels emotionally flat or generic? - What feels off-brand even if it is otherwise useful? - Is the intended feeling visible early? - Does the atmosphere remain coherent across the whole concept? Keep this evaluation qualitative. ### 8. Write a moodboard brief End with a brief that another creative discipline could use immediately. Include: - **Target feeling** - **Three defining descriptors** - **Adjacent feelings to lean into** - **Contrasting feelings to avoid** - **Visual references to seek** - **Audio references to seek** - **Narrative tone** - **No-go elements** ## Response structure Use this structure unless the user asks for something else: ### Target Feeling - ... ### Emotional Texture - ... ### Emotional Field - Adjacent: ... - Contrasting: ... ### Emotional Palette - Visuals: ... - Materials: ... - Audio: ... - Environment: ... - Objects: ... - Characters: ... - Narrative tone: ... ### Anti-Patterns - ... ### Design Heuristics 1. ... 2. ... 3. ... ### Evaluation - Strongest emotional signal: ... - Weakest or conflicting signal: ... - Overall coherence: ... ### Moodboard Brief - ... ## Fast mode Use this quick pass when speed matters: - What should the player feel? - What feelings enrich it? - What feelings break it? - What sensory ingredients reinforce it? - What currently feels emotionally generic or off-vibe? ## References Read these only when useful: - `references/examples.md` for sample emotional targets and example emotional fields - `references/prompts.md` for extra workshop-style prompts and facilitation questions ## Working principle Players do not only remember what a game lets them do. They remember the atmosphere it leaves behind. Use this skill when the design feels competent but emotionally vague. FILE:references/examples.md # Examples Use these examples to spark vocabulary and directional thinking. Do not treat them as fixed formulas. ## Example target feelings - coziness - creepiness - wonder - melancholy - serenity - swagger - intimacy - awe - festive abundance - playful chaos - civic pride - nostalgic comfort - quiet mystery - aspirational urban fantasy ## Example emotional texture ### Coziness - safe - warm - soft - sheltered - familiar - abundant - gentle ### Creepiness - eerie - watched - uncanny - uneasy - cold - contaminated - dread-filled ### Wonder - expansive - luminous - curious - delicate - impossible - reverent - enchanted ## Example emotional field ### Coziness **Adjacent** - nostalgia - tenderness - domesticity - romance - childhood comfort **Contrasting** - hostility - stress - exposure - harsh scarcity - danger ### Creepiness **Adjacent** - dread - alienation - helplessness - disgust - existential horror **Contrasting** - comfort - cheerfulness - emotional clarity - safety - playful confidence ### Wonder **Adjacent** - discovery - awe - curiosity - reverence - childlike delight **Contrasting** - cynicism - routine blandness - sterile efficiency - overexplanation - emotional flatness ## Example anti-patterns ### For coziness - aggressive countdowns - noisy urgency - sterile hostility - harsh framing - overly sharp visual language ### For creepiness - comic relief in the wrong place - bright celebratory framing - overexplaining the mystery - excessively gamey optimization signals ### For wonder - excessive exposition - flat utilitarian UI tone - cramped visual framing - overfamiliar imagery without surprise ## Example heuristics ### Coziness - shelter over exposure - softness over harshness - abundance over deprivation - intimacy over spectacle - routine over volatility ### Creepiness - suggestion over explanation - tension over shock - ambiguity over clarity - vulnerability over mastery - contamination over cleanliness ### Wonder - invitation over instruction - discovery over certainty - scale over clutter - reverence over irony - possibility over closure FILE:references/prompts.md # Prompt Bank Use these prompts when the user needs help discovering or articulating the emotional core. ## Core prompts - What should the player feel most strongly? - What should linger after the session ends? - What would a player say this game feels like to a friend? - What emotional promise is the concept making? - If the game had a scent, a temperature, and a weather pattern, what would they be? ## Texture prompts - Is the feeling soft or sharp? - Is it warm or cold? - Is it intimate or grand? - Is it calm, tense, buoyant, haunted, lush, restrained, playful, solemn? - Does it feel handmade, ceremonial, secretive, generous, fragile, or theatrical? ## Contrast prompts - What feeling would completely ruin this vibe? - What would make the concept feel generic? - What kind of tone would flatten the emotional identity? - What kind of visual or narrative choice would feel emotionally false? ## Palette prompts - What colors belong to this feeling? - What kinds of spaces carry it naturally? - What textures, surfaces, and materials support it? - What kinds of sound belong in this atmosphere? - What should remain unsaid? - What recurring objects or motifs belong to this mood? ## Evaluation prompts - Where is the emotional promise strongest? - Where does the concept become emotionally neutral? - What feels accidentally off-vibe? - What part feels competent but soulless? - What should be intensified, softened, removed, or protected? ## Facilitation note When the user is unsure, offer 2-4 candidate emotional directions and explain the difference in vibe between them. Help them choose one strong center rather than blending everything together.
Set up and use Freesound API access from a local Windows OpenClaw workspace with OAuth login, local credential storage, and sound search helpers. Use when th...
---
name: freesound-api
description: Set up and use Freesound API access from a local Windows OpenClaw workspace with OAuth login, local credential storage, and sound search helpers. Use when the user wants to register or use a Freesound API app, save a Freesound client id and client secret locally, complete localhost OAuth login, search Freesound sounds, or fetch Freesound data without exposing secrets in a published skill.
---
# freesound-api
Use this as a local-only skill. Do not publish Freesound client secrets inside the skill.
## Local storage
This skill stores credentials in:
- `%APPDATA%\OpenClaw\freesound-api\credentials.json`
Keep the secret there, not in `SKILL.md`.
## Setup credentials
Save the app credentials locally:
```powershell
python scripts\setup_credentials.py --client-id '<CLIENT_ID>' --client-secret '<CLIENT_SECRET>' --redirect-uri 'http://localhost:8787/callback'
```
Use the same redirect URI here that was registered in Freesound.
## Complete OAuth login
Run:
```powershell
python scripts\oauth_login.py
```
What it does:
1. Starts a temporary localhost callback server on port `8787`
2. Opens the Freesound authorization page in the browser
3. Receives the authorization code at `http://localhost:8787/callback`
4. Exchanges it for an access token
5. Saves the token back into `%APPDATA%\OpenClaw\freesound-api\credentials.json`
If the browser does not open, copy the printed URL manually.
## Search sounds
Run:
```powershell
python scripts\search_sounds.py "rain" --page-size 10
```
Examples with filters:
```powershell
python scripts\search_sounds.py "rain" --license cc0 --duration-min 5 --duration-max 60
python scripts\search_sounds.py "thunder" --tag storm --tag ambience
python scripts\search_sounds.py "wind" --filter "samplerate:[44100 TO *]"
```
The search helper prefers OAuth bearer token auth if available. If there is no OAuth token yet, it falls back to using the saved Freesound secret as the `token` parameter for simple API calls.
## Get sound details
Run:
```powershell
python scripts\sound_details.py 322965
```
Use this to inspect metadata, previews, ratings, tags, format details, and the direct download endpoint for a sound.
## Download a sound
Run:
```powershell
python scripts\download_sound.py 322965 --out-dir downloads
```
Download a preview instead of the original file:
```powershell
python scripts\download_sound.py 322965 --preview hq-mp3 --out-dir previews
```
This saves the original file or selected preview into the chosen output directory using the current OAuth token or saved API key.
## Public-safe publishing
If publishing this skill publicly, publish only the skill folder and scripts. Do not publish `%APPDATA%\OpenClaw\freesound-api\credentials.json` or any client secret.
## Notes
- Keep the redirect URI consistent. A mismatch will break the token exchange.
- Prefer OAuth login for user-level access.
- If a secret was pasted into chat, treat it as exposed and rotate it after testing.
- If `requests` is missing locally, install it in the Python environment before running the scripts.
FILE:scripts/api_utils.py
import requests
from freesound_config import API_BASE, load_config
def get_auth_headers_and_params() -> tuple[dict, dict]:
cfg = load_config()
token_data = cfg.get("token") or {}
access_token = token_data.get("access_token")
api_key = cfg.get("client_secret")
headers = {}
params = {}
if access_token:
headers["Authorization"] = f"Bearer {access_token}"
elif api_key:
params["token"] = api_key
else:
raise SystemExit("Missing token or API key. Run setup_credentials.py and oauth_login.py first.")
return headers, params
def get_json(path: str, params: dict | None = None) -> dict:
headers, auth_params = get_auth_headers_and_params()
merged = dict(auth_params)
if params:
merged.update(params)
resp = requests.get(f"{API_BASE}{path}", params=merged, headers=headers, timeout=30)
resp.raise_for_status()
return resp.json()
FILE:scripts/download_sound.py
import argparse
import mimetypes
from pathlib import Path
from urllib.parse import urlparse
import requests
from api_utils import get_auth_headers_and_params, get_json
def infer_extension(info: dict, response: requests.Response) -> str:
original = info.get("original_filename") or ""
suffix = Path(original).suffix
if suffix:
return suffix
name = info.get("name") or ""
suffix = Path(name).suffix
if suffix:
return suffix
content_type = (response.headers.get("Content-Type") or "").split(";")[0].strip()
guessed = mimetypes.guess_extension(content_type) if content_type else None
if guessed:
return guessed
path_suffix = Path(urlparse(str(response.url)).path).suffix
if path_suffix:
return path_suffix
sound_type = info.get("type")
if sound_type:
return f'.{sound_type}'
return ".bin"
def safe_name(info: dict, response: requests.Response) -> str:
original = info.get("original_filename") or ""
if original and Path(original).suffix:
return original
base = info.get("name") or f"sound-{info.get('id', 'unknown')}"
ext = infer_extension(info, response)
if not Path(base).suffix:
base = f"{base}{ext}"
return ''.join('_' if ch in '<>:"/\\|?*' else ch for ch in base)
def main() -> None:
parser = argparse.ArgumentParser(description="Download a Freesound sound by ID.")
parser.add_argument("sound_id", type=int)
parser.add_argument("--out-dir", default=".")
parser.add_argument("--preview", choices=["hq-mp3", "lq-mp3", "hq-ogg", "lq-ogg"])
args = parser.parse_args()
info = get_json(
f"/sounds/{args.sound_id}/",
{"fields": "id,name,original_filename,type,download,previews"},
)
if args.preview:
key = f"preview-{args.preview}"
download_url = (info.get("previews") or {}).get(key)
if not download_url:
raise SystemExit(f"Preview URL not available for {key}.")
else:
download_url = info.get("download")
if not download_url:
raise SystemExit("No download URL returned for this sound.")
headers, params = get_auth_headers_and_params()
response = requests.get(download_url, headers=headers, params=params, stream=True, timeout=60)
response.raise_for_status()
out_dir = Path(args.out_dir)
out_dir.mkdir(parents=True, exist_ok=True)
filename = safe_name(info, response)
out_path = out_dir / filename
with out_path.open("wb") as f:
for chunk in response.iter_content(chunk_size=1024 * 64):
if chunk:
f.write(chunk)
print(out_path)
if __name__ == "__main__":
main()
FILE:scripts/freesound_config.py
import json
import os
from pathlib import Path
APP_DIR = Path(os.environ.get("APPDATA", Path.home() / ".config")) / "OpenClaw" / "freesound-api"
CONFIG_PATH = APP_DIR / "credentials.json"
DEFAULT_REDIRECT_URI = "http://localhost:8787/callback"
API_BASE = "https://freesound.org/apiv2"
AUTHORIZE_URL = f"{API_BASE}/oauth2/authorize/"
TOKEN_URL = f"{API_BASE}/oauth2/access_token/"
def ensure_app_dir() -> Path:
APP_DIR.mkdir(parents=True, exist_ok=True)
return APP_DIR
def load_config() -> dict:
if CONFIG_PATH.exists():
return json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
return {}
def save_config(data: dict) -> None:
ensure_app_dir()
CONFIG_PATH.write_text(json.dumps(data, indent=2), encoding="utf-8")
FILE:scripts/oauth_login.py
import argparse
import json
import secrets
import threading
import time
import urllib.parse
import webbrowser
from http.server import BaseHTTPRequestHandler, HTTPServer
import requests
from freesound_config import (
AUTHORIZE_URL,
TOKEN_URL,
DEFAULT_REDIRECT_URI,
load_config,
save_config,
)
class CallbackHandler(BaseHTTPRequestHandler):
server_version = "FreesoundOAuth/1.0"
def do_GET(self):
parsed = urllib.parse.urlparse(self.path)
params = urllib.parse.parse_qs(parsed.query)
self.server.oauth_result = {
"code": params.get("code", [None])[0],
"state": params.get("state", [None])[0],
"error": params.get("error", [None])[0],
}
body = b"Freesound login received. You can close this tab and return to OpenClaw."
self.send_response(200)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, format, *args):
return
def main() -> None:
parser = argparse.ArgumentParser(description="Complete Freesound OAuth login.")
parser.add_argument("--redirect-uri", default=None)
parser.add_argument("--timeout", type=int, default=180)
args = parser.parse_args()
cfg = load_config()
client_id = cfg.get("client_id")
client_secret = cfg.get("client_secret")
redirect_uri = args.redirect_uri or cfg.get("redirect_uri") or DEFAULT_REDIRECT_URI
if not client_id or not client_secret:
raise SystemExit("Missing client_id/client_secret. Run setup_credentials.py first.")
parsed_redirect = urllib.parse.urlparse(redirect_uri)
if parsed_redirect.scheme != "http" or parsed_redirect.hostname not in {"localhost", "127.0.0.1"}:
raise SystemExit("This helper expects a localhost redirect URI.")
port = parsed_redirect.port or 80
state = secrets.token_urlsafe(24)
server = HTTPServer((parsed_redirect.hostname, port), CallbackHandler)
server.oauth_result = None
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
query = urllib.parse.urlencode(
{
"client_id": client_id,
"response_type": "code",
"redirect_uri": redirect_uri,
"state": state,
}
)
auth_url = f"{AUTHORIZE_URL}?{query}"
print("Open this URL if the browser does not open automatically:\n")
print(auth_url)
webbrowser.open(auth_url)
deadline = time.time() + args.timeout
try:
while time.time() < deadline:
if server.oauth_result is not None:
break
time.sleep(0.25)
finally:
server.shutdown()
server.server_close()
result = server.oauth_result
if not result:
raise SystemExit("Timed out waiting for Freesound callback.")
if result.get("error"):
raise SystemExit(f"Freesound returned error: {result['error']}")
if result.get("state") != state:
raise SystemExit("State mismatch in callback.")
code = result.get("code")
if not code:
raise SystemExit("No authorization code received.")
token_resp = requests.post(
TOKEN_URL,
data={
"client_id": client_id,
"client_secret": client_secret,
"grant_type": "authorization_code",
"code": code,
"redirect_uri": redirect_uri,
},
timeout=30,
)
token_resp.raise_for_status()
token_data = token_resp.json()
cfg["redirect_uri"] = redirect_uri
cfg["token"] = token_data
save_config(cfg)
print(json.dumps({"saved": True, "token_type": token_data.get("token_type"), "scope": token_data.get("scope")}, indent=2))
if __name__ == "__main__":
main()
FILE:scripts/search_sounds.py
import argparse
import json
from api_utils import get_json
def main() -> None:
parser = argparse.ArgumentParser(description="Search Freesound.")
parser.add_argument("query")
parser.add_argument("--page-size", type=int, default=10)
parser.add_argument("--filter", action="append", default=[], help="Raw Freesound filter expression; may be repeated.")
parser.add_argument("--tag", action="append", default=[], help="Tag to require; may be repeated.")
parser.add_argument("--license", choices=["all", "cc0", "by", "by-nc"], default="all")
parser.add_argument("--duration-min", type=float)
parser.add_argument("--duration-max", type=float)
args = parser.parse_args()
filters = list(args.filter)
for tag in args.tag:
filters.append(f"tag:{tag}")
if args.license == "cc0":
filters.append('license:"Creative Commons 0"')
elif args.license == "by":
filters.append('license:"Attribution"')
elif args.license == "by-nc":
filters.append('license:"Attribution Noncommercial"')
if args.duration_min is not None:
filters.append(f"duration:[{args.duration_min} TO *]")
if args.duration_max is not None:
filters.append(f"duration:[* TO {args.duration_max}]")
params = {
"query": args.query,
"page_size": args.page_size,
"fields": "id,name,username,license,duration,url,previews,tags,type,filesize",
}
if filters:
params["filter"] = " ".join(filters)
data = get_json("/search/text/", params)
slim = []
for item in data.get("results", []):
previews = item.get("previews") or {}
slim.append(
{
"id": item.get("id"),
"name": item.get("name"),
"username": item.get("username"),
"license": item.get("license"),
"duration": item.get("duration"),
"type": item.get("type"),
"filesize": item.get("filesize"),
"url": item.get("url"),
"preview_mp3": previews.get("preview-hq-mp3") or previews.get("preview-lq-mp3"),
"tags": item.get("tags") or [],
}
)
print(json.dumps({"count": data.get("count"), "results": slim}, indent=2))
if __name__ == "__main__":
main()
FILE:scripts/setup_credentials.py
import argparse
from freesound_config import load_config, save_config, DEFAULT_REDIRECT_URI, CONFIG_PATH
def main() -> None:
parser = argparse.ArgumentParser(description="Save Freesound OAuth app credentials locally.")
parser.add_argument("--client-id", required=True)
parser.add_argument("--client-secret", required=True)
parser.add_argument("--redirect-uri", default=DEFAULT_REDIRECT_URI)
args = parser.parse_args()
data = load_config()
data.update(
{
"client_id": args.client_id,
"client_secret": args.client_secret,
"redirect_uri": args.redirect_uri,
}
)
save_config(data)
print(f"Saved credentials to {CONFIG_PATH}")
if __name__ == "__main__":
main()
FILE:scripts/sound_details.py
import argparse
import json
from api_utils import get_json
def main() -> None:
parser = argparse.ArgumentParser(description="Fetch Freesound sound details by ID.")
parser.add_argument("sound_id", type=int)
args = parser.parse_args()
data = get_json(
f"/sounds/{args.sound_id}/",
{
"fields": "id,name,username,license,duration,url,description,tags,type,filesize,bitrate,bitdepth,samplerate,channels,created,download,previews,images,num_downloads,avg_rating,num_ratings"
},
)
print(json.dumps(data, indent=2))
if __name__ == "__main__":
main()
Read itch.io creator stats and publish or update itch.io game builds from Windows using the itch.io server-side API and Butler. Use when the user asks to che...
---
name: itch-io-publisher
description: Read itch.io creator stats and publish or update itch.io game builds from Windows using the itch.io server-side API and Butler. Use when the user asks to check itch.io account or game stats, list their games, validate an itch.io API key, install Butler for itch publishing, or upload or push a local build folder or zip to an itch.io project channel such as username/game:html5.
---
# itch-io-publisher
Keep this skill self-contained. Do not rely on bundled helper scripts being present. Run explicit PowerShell commands directly so the user can inspect exactly what will happen.
## Workflow
1. Validate the API key if needed.
2. Read account and game stats if the user wants visibility first.
3. Install Butler if it is missing.
4. Dry-run a push before a live upload when the target or source path is new.
5. Only do a live push after the user has identified the project target and local build source.
## Key limits
- The public server-side API is suitable for reading account and game data and for purchase or download-key verification.
- Normal page editing is not exposed here; expect metadata edits to require the itch.io web dashboard.
- Build publishing is done with Butler, not the read-oriented JSON API.
## Validate key and read stats
Run this PowerShell directly:
```powershell
$key = '<API_KEY>'
$base = "https://itch.io/api/1/$key"
$me = Invoke-RestMethod -Uri "$base/me" -Method Get -TimeoutSec 30
$games = Invoke-RestMethod -Uri "$base/my-games" -Method Get -TimeoutSec 30
$totalViews = 0
$totalDownloads = 0
$totalPurchases = 0
foreach ($game in $games.games) {
$totalViews += [int]$game.views_count
$totalDownloads += [int]$game.downloads_count
$totalPurchases += [int]$game.purchases_count
}
[pscustomobject]@{
account = $me.user
totals = [pscustomobject]@{
views = $totalViews
downloads = $totalDownloads
purchases = $totalPurchases
games = @($games.games).Count
}
games = $games.games
} | ConvertTo-Json -Depth 8
```
Use this to validate the key, fetch profile info, list games, and summarize views, downloads, and purchases.
## Install Butler
If Butler is not installed yet, run this PowerShell directly:
```powershell
$dest = Join-Path $env:LOCALAPPDATA 'OpenClaw\tools\butler'
New-Item -ItemType Directory -Force -Path $dest | Out-Null
$zipPath = Join-Path ([System.IO.Path]::GetTempPath()) 'butler-windows-amd64.zip'
Invoke-WebRequest -Uri 'https://broth.itch.zone/butler/windows-amd64/LATEST/archive/default' -OutFile $zipPath -TimeoutSec 120
Expand-Archive -Path $zipPath -DestinationPath $dest -Force
& (Join-Path $dest 'butler.exe') version
```
This downloads the latest Windows Butler build from `broth.itch.zone` into `%LOCALAPPDATA%\OpenClaw\tools\butler`.
## Push a build
Dry-run first when possible:
```powershell
$key = '<API_KEY>'
$source = 'D:\path\to\build'
$target = 'username/game:html5'
$butlerDir = Join-Path $env:LOCALAPPDATA 'OpenClaw\tools\butler'
$butler = Join-Path $butlerDir 'butler.exe'
if (!(Test-Path $butler)) {
throw "butler.exe not found. Install Butler first."
}
if (!(Test-Path $source)) {
throw "Source path not found: $source"
}
$tmpDir = Join-Path ([System.IO.Path]::GetTempPath()) 'openclaw-itch'
New-Item -ItemType Directory -Force -Path $tmpDir | Out-Null
$tokenFile = Join-Path $tmpDir 'butler_creds.txt'
Set-Content -Path $tokenFile -Value $key -NoNewline
try {
& $butler -i $tokenFile push $source $target --dry-run
}
finally {
Remove-Item $tokenFile -Force -ErrorAction SilentlyContinue
}
```
Live push:
```powershell
$key = '<API_KEY>'
$source = 'D:\path\to\build'
$target = 'username/game:html5'
$butlerDir = Join-Path $env:LOCALAPPDATA 'OpenClaw\tools\butler'
$butler = Join-Path $butlerDir 'butler.exe'
if (!(Test-Path $butler)) {
throw "butler.exe not found. Install Butler first."
}
if (!(Test-Path $source)) {
throw "Source path not found: $source"
}
$tmpDir = Join-Path ([System.IO.Path]::GetTempPath()) 'openclaw-itch'
New-Item -ItemType Directory -Force -Path $tmpDir | Out-Null
$tokenFile = Join-Path $tmpDir 'butler_creds.txt'
Set-Content -Path $tokenFile -Value $key -NoNewline
try {
& $butler -i $tokenFile push $source $target --if-changed
}
finally {
Remove-Item $tokenFile -Force -ErrorAction SilentlyContinue
}
```
## Target format
Use Butler targets in the form:
- `username/game:channel`
- `game_id:channel`
Common HTML game channel names include `html5` or `web`, but use the channel the user has configured on itch.io.
## Safe defaults
- Do not paste API keys back into chat unless the user explicitly asks.
- Do not live-push to itch.io without a specific source path and target confirmed by the user.
- Prefer dry-run first for a new project or channel.
- If a push fails, inspect Butler's exact error rather than guessing.
Launch, configure, and troubleshoot DOSBox-X first, with fallback to classic DOSBox, for DOS games and software. Use when working with classic DOS programs,...
---
name: dosbox
description: Launch, configure, and troubleshoot DOSBox-X first, with fallback to classic DOSBox, for DOS games and software. Use when working with classic DOS programs, mounting folders or ISO/CD images, generating launch commands, auto-generating reusable .conf files, fixing sound/input/fullscreen issues, editing dosbox config files, or preparing a shareable setup for old games and abandonware.
---
# DOSBox
Use this skill to get a DOS program running with the least drama possible.
Prefer **DOSBox-X** when available. Fall back to classic **DOSBox** only when DOSBox-X is missing or the user explicitly wants stock DOSBox behavior.
## Quick approach
1. Detect available emulators with `scripts/resolve_dosbox.py`.
2. Prefer **DOSBox-X** for:
- ISO/CD-heavy installs
- awkward installers
- hardware and config edge cases
- reusable setups that should be easier to tweak later
3. Identify what the user has:
- a game/app folder
- a floppy/ISO/CD image
- an installer
- an existing config file
4. Build a reproducible launch command or config instead of relying on vague manual steps.
5. If the program fails, troubleshoot in this order:
- wrong mount / wrong drive letter
- wrong startup executable
- graphics/output mode
- sound setup
- CPU cycles / speed sensitivity
- input or fullscreen settings
## Core workflows
### Launch a DOS app from a folder
Prefer mounting the containing folder as `C:`.
Typical command pattern:
```powershell
<dosbox-binary> -c "mount c <path-to-folder>" -c "c:" -c "dir" -c "<program.exe>"
```
Rules:
- Quote Windows paths carefully.
- Mount the parent folder that contains the DOS files.
- If the executable is unknown, inspect with `dir` first.
- If there is a setup utility (`SETUP.EXE`, `INSTALL.EXE`), run that before the main game when sound/video must be configured.
### Launch from CD / ISO media
Prefer DOSBox-X for image handling.
Typical pattern:
```powershell
<dosbox-binary> -c "imgmount d <image-file> -t iso" -c "d:" -c "dir"
```
If the game needs both a writable hard drive and CD:
```powershell
<dosbox-binary> -c "mount c <game-or-install-folder>" -c "imgmount d <image-file> -t iso" -c "c:"
```
### Install a DOS game
Use a dedicated writable game folder.
Recommended flow:
1. Create a clean install directory.
2. Mount it as `C:`.
3. Mount install media as `D:` if needed.
4. Run `INSTALL`, `SETUP`, or vendor-specific installer.
5. After install, create a reusable launch command or config file.
### Generate a reusable config file
Prefer a config file when the user wants a stable, repeatable setup.
Use `scripts/make_dosbox_conf.py` to generate a starter `.conf` file with:
- detected emulator path
- mount commands
- optional ISO mounting
- optional auto-run executable
- sensible defaults for fullscreen, output, cycles, and Sound Blaster
Examples:
```powershell
python scripts/make_dosbox_conf.py --game-path "C:\Games\DOOM" --exe DOOM.EXE --conf "C:\Games\DOOM\doom.conf"
python scripts/make_dosbox_conf.py --game-path "C:\Games\Install" --iso "C:\Images\GAME.iso" --exe INSTALL.EXE --conf "C:\Games\Install\install.conf"
```
Inspect the generated file before claiming it is final; some games need renderer, cycles, or audio tweaks.
### Use an existing config file
If a config already exists, inspect it before changing anything.
Typical launch forms:
```powershell
<dosbox-binary> -conf <config-file>
```
or:
```powershell
<dosbox-binary> -userconf
```
Only edit config values that solve the current problem. Avoid broad random tweaks.
## Troubleshooting checklist
### Program does not start
Check:
- mounted the correct folder
- using the correct drive letter
- executable name is correct
- files are not nested one level deeper than expected
- the program expects to be started from its own directory
Useful in-emulator commands:
```text
mount
c:
dir
cd <subdir>
```
### "This program requires MSCDEX/CD-ROM"
Mount optical media properly:
```powershell
imgmount d <image-file> -t iso
```
If using a host folder as a CD source, prefer DOSBox-X when possible and ensure the game really supports folder-based installation.
### Sound does not work
Run the game's setup program first.
Common working values for many DOS titles:
- Sound Blaster 16
- Port `220`
- IRQ `7`
- DMA `1`
If the game offers autodetect, still verify what it selected.
### Too fast or too slow
Adjust cycles.
Examples:
```text
cycles auto
cycles max
cycles fixed 12000
```
For old timing-sensitive games, prefer a fixed value and iterate.
### Fullscreen / black screen / renderer issues
Try changing output mode in config:
- `output=opengl`
- `output=texture`
- `output=ddraw`
- `output=surface`
Prefer changing one setting at a time.
### Keyboard / mouse problems
Check:
- whether mouse capture is active
- whether the game expects keyboard-only input
- whether key layout issues come from host locale differences
## Command generation rules
When writing commands for the user or a script:
- Prefer a single launch command with chained `-c` directives for quick tests.
- Prefer a config file for repeatable setups.
- Use absolute paths on Windows.
- Do not assume DOSBox is on PATH; detect common executable names or ask for the install path.
- If both DOSBox and DOSBox-X exist, prefer DOSBox-X for ISO/CD-heavy setups and advanced compatibility.
- If the task is shareable or repeatable, generate a `.conf` file and keep commands in `[autoexec]`.
## ClawHub publishing notes
This skill is meant to be portable.
- Do not hardcode one machine's install path as a requirement.
- Treat DOSBox-X as preferred, not mandatory.
- Use helper scripts to detect executables and generate commands/configs.
- Keep claims conservative: the generated config is a good starting point, not a guaranteed universal fix.
## Bundled resources
### scripts/resolve_dosbox.py
Use this helper to detect likely DOSBox executables and emit example commands for folder or ISO launches.
Example:
```powershell
python scripts/resolve_dosbox.py --game-path "C:\Games\DOOM"
python scripts/resolve_dosbox.py --game-path "C:\Games\Install" --iso "C:\Images\GAME.iso"
```
### scripts/make_dosbox_conf.py
Use this helper to generate a reusable `.conf` file for DOSBox-X or DOSBox.
### references/troubleshooting.md
Read this when the task is mainly diagnosis rather than simple launching.
FILE:references/troubleshooting.md
# DOSBox troubleshooting reference
## Choose the emulator
Prefer **DOSBox-X** when:
- working with ISO/CD images
- dealing with awkward installers
- needing stronger hardware/config flexibility
Plain **DOSBox** is usually enough when:
- launching a simple folder-based game
- the user already has a working config
- you only need a minimal command
## Common executable names to inspect
If startup is unclear, look for:
- `GAME.EXE`
- `START.EXE`
- `PLAY.EXE`
- `RUN.EXE`
- `GO.BAT`
- `SETUP.EXE`
- `INSTALL.EXE`
- `CONFIG.EXE`
Check manuals or included text files if the folder is ambiguous.
## Practical config knobs
### Performance
```ini
[cpu]
core=auto
cputype=auto
cycles=auto
cycleup=500
cycledown=500
```
For unstable timing, switch to a fixed cycle count.
### Display
```ini
[render]
aspect=true
scaler=normal2x
[sdl]
fullscreen=false
output=opengl
```
If `opengl` misbehaves, try `texture`, `ddraw`, or `surface`.
### Sound
```ini
[sblaster]
sbtype=sb16
sbbase=220
irq=7
dma=1
hdma=5
mixer=true
```
Not every game needs all values, but these defaults are often a good baseline.
## Installation pattern
Use a structure like:
```text
C:\DOS\GAME\
C:\DOS\MEDIA\GAME.iso
C:\DOS\CONFIG\game.conf
```
This keeps installed files, media, and configs separate.
## Good agent behavior
- Prefer reproducible commands over generic advice.
- Inspect before editing configs.
- Explain only the settings being changed.
- Avoid claiming a renderer/sound tweak will definitely fix the issue.
- If the emulator is not installed, say so plainly and stop short of inventing paths.
FILE:scripts/make_dosbox_conf.py
#!/usr/bin/env python3
"""Generate a starter DOSBox/DOSBox-X config file."""
from __future__ import annotations
import argparse
from pathlib import Path
import sys
from resolve_dosbox import find_binaries
def quote_autoexec_path(path: Path) -> str:
return str(path.resolve()).replace('\\', '/')
def choose_binary() -> str | None:
binaries = find_binaries()
if not binaries:
return None
return next((b for b in binaries if 'dosbox-x' in b.lower()), binaries[0])
def build_conf(game_path: Path, iso_path: Path | None, exe: str | None, fullscreen: bool, output: str, cycles: str, binary: str | None) -> str:
autoexec = [
f"mount c \"{quote_autoexec_path(game_path)}\"",
]
if iso_path:
autoexec.append(f"imgmount d \"{quote_autoexec_path(iso_path)}\" -t iso")
autoexec.append("c:")
autoexec.append("dir")
if exe:
autoexec.append(exe)
full = "true" if fullscreen else "false"
binary_comment = binary or "NOT FOUND"
return f"""# Generated by make_dosbox_conf.py
# Preferred emulator: {binary_comment}
[sdl]
fullscreen={full}
output={output}
[render]
aspect=true
scaler=normal2x
[cpu]
core=auto
cputype=auto
cycles={cycles}
cycleup=500
cycledown=500
[mixer]
rate=44100
blocksize=1024
prebuffer=20
[sblaster]
sbtype=sb16
sbbase=220
irq=7
dma=1
hdma=5
mixer=true
oplmode=auto
oplemu=default
oplrate=44100
[autoexec]
{"\n".join(autoexec)}
"""
def main() -> int:
parser = argparse.ArgumentParser(description="Generate a starter DOSBox config file.")
parser.add_argument("--game-path", required=True, help="Folder to mount as C:")
parser.add_argument("--iso", help="Optional ISO to mount as D:")
parser.add_argument("--exe", help="Optional executable or batch file to auto-run")
parser.add_argument("--conf", required=True, help="Output config path")
parser.add_argument("--fullscreen", action="store_true", help="Start in fullscreen mode")
parser.add_argument("--output", default="opengl", help="Output mode: opengl, texture, ddraw, surface")
parser.add_argument("--cycles", default="auto", help="CPU cycles value")
args = parser.parse_args()
game_path = Path(args.game_path)
if not game_path.exists() or not game_path.is_dir():
print(f"Game path not found or not a directory: {game_path}", file=sys.stderr)
return 1
iso_path = Path(args.iso) if args.iso else None
if iso_path and not iso_path.exists():
print(f"ISO path not found: {iso_path}", file=sys.stderr)
return 1
conf_path = Path(args.conf)
conf_path.parent.mkdir(parents=True, exist_ok=True)
binary = choose_binary()
conf_text = build_conf(
game_path=game_path,
iso_path=iso_path,
exe=args.exe,
fullscreen=args.fullscreen,
output=args.output,
cycles=args.cycles,
binary=binary,
)
conf_path.write_text(conf_text, encoding="utf-8")
print(f"Wrote config: {conf_path.resolve()}")
if binary:
print(f"Preferred emulator: {binary}")
print(f'Launch example: "{binary}" -conf "{conf_path.resolve()}"')
else:
print("Preferred emulator: not found")
return 0
if __name__ == "__main__":
raise SystemExit(main())
FILE:scripts/resolve_dosbox.py
#!/usr/bin/env python3
"""Detect DOSBox/DOSBox-X and print example launch commands."""
from __future__ import annotations
import argparse
import os
import shutil
from pathlib import Path
COMMON_WINDOWS_PATHS = [
r"C:\Program Files\DOSBox-X\dosbox-x.exe",
r"C:\Program Files (x86)\DOSBox-X\dosbox-x.exe",
r"C:\Program Files\DOSBox-0.74-3\DOSBox.exe",
r"C:\Program Files (x86)\DOSBox-0.74-3\DOSBox.exe",
r"C:\Program Files\DOSBox Staging\dosbox.exe",
r"C:\Program Files (x86)\DOSBox Staging\dosbox.exe",
]
def find_binaries() -> list[str]:
found: list[str] = []
for name in ("dosbox-x", "dosbox"):
path = shutil.which(name)
if path and path not in found:
found.append(path)
for path in COMMON_WINDOWS_PATHS:
if os.path.exists(path) and path not in found:
found.append(path)
return found
def quote(value: str) -> str:
return '"' + value.replace('"', '\\"') + '"'
def build_folder_command(binary: str, game_path: Path) -> str:
return (
f"{quote(binary)} "
f"-c \"mount c {game_path}\" "
f"-c \"c:\" "
f"-c \"dir\""
)
def build_iso_command(binary: str, game_path: Path | None, iso_path: Path) -> str:
parts = [quote(binary)]
if game_path:
parts.append(f'-c "mount c {game_path}"')
parts.append(f'-c "imgmount d {iso_path} -t iso"')
if game_path:
parts.append('-c "c:"')
else:
parts.append('-c "d:"')
parts.append('-c "dir"')
return " ".join(parts)
def main() -> int:
parser = argparse.ArgumentParser(description="Detect DOSBox executables and suggest commands.")
parser.add_argument("--game-path", help="Folder to mount as C:")
parser.add_argument("--iso", help="ISO image to mount as D:")
args = parser.parse_args()
binaries = find_binaries()
if not binaries:
print("No DOSBox executable found on PATH or in common Windows install locations.")
return 1
preferred = next((b for b in binaries if "dosbox-x" in b.lower()), binaries[0])
print(f"Preferred binary: {preferred}")
print("Detected binaries:")
for binary in binaries:
print(f"- {binary}")
game_path = Path(args.game_path).resolve() if args.game_path else None
iso_path = Path(args.iso).resolve() if args.iso else None
if game_path:
print("\nFolder launch example:")
print(build_folder_command(preferred, game_path))
if iso_path:
print("\nISO launch example:")
print(build_iso_command(preferred, game_path, iso_path))
if not game_path and not iso_path:
print("\nTip: pass --game-path and/or --iso to generate example commands.")
return 0
if __name__ == "__main__":
raise SystemExit(main())
Use installed TrueCrypt on Windows to mount, dismount, inspect, and automate legacy TrueCrypt containers or encrypted partitions from the command line. Trigg...
---
name: truecrypt-cli
description: Use installed TrueCrypt on Windows to mount, dismount, inspect, and automate legacy TrueCrypt containers or encrypted partitions from the command line. Trigger when a user specifically wants TrueCrypt rather than VeraCrypt, asks for TrueCrypt CLI syntax, wants a batch/PowerShell command for mounting or dismounting, needs to check whether TrueCrypt is installed, or wants help scripting safe non-destructive TrueCrypt operations.
---
# TrueCrypt CLI
Use this skill when the user explicitly wants to work with installed TrueCrypt on Windows, especially version 7.1a, instead of being redirected to VeraCrypt.
## Workflow
1. Confirm that `TrueCrypt.exe` exists before giving machine-specific commands.
2. Prefer the full executable path in examples to avoid PATH issues.
3. Ask only for the minimum needed details:
- volume path or device path
- target drive letter
- whether a keyfile is used
- whether the operation must be non-interactive
4. Prefer non-destructive operations first: detect install path, dismount, or prepare a command without running it.
5. Warn before using `/p` because command-line passwords may be exposed to process listings, logs, or shell history.
6. If built-in help does not print to the console, rely on known command patterns and cautious validation instead of pretending the CLI is self-documenting.
## Quick checks
First, locate the binary. Common paths:
```powershell
C:\Program Files\TrueCrypt\TrueCrypt.exe
C:\Program Files (x86)\TrueCrypt\TrueCrypt.exe
```
PowerShell check:
```powershell
$tc = @(
'C:\Program Files\TrueCrypt\TrueCrypt.exe',
'C:\Program Files (x86)\TrueCrypt\TrueCrypt.exe'
) | Where-Object { Test-Path $_ } | Select-Object -First 1
```
If nothing is found there, fall back to `Get-Command TrueCrypt.exe`.
## Safe command patterns
Use the cookbook in `references/commands.md` for exact examples.
High-confidence operations:
- mount a volume with `/v` and `/l`
- dismount one letter with `/d X`
- dismount all with `/d`
- use `/q` for quiet mode
- use `/k` for keyfiles when needed
- use `/m` for mount options when explicitly required
Prefer examples like:
```bat
"C:\Program Files\TrueCrypt\TrueCrypt.exe" /v "C:\path\container.tc" /l X /q
```
```bat
"C:\Program Files\TrueCrypt\TrueCrypt.exe" /d X /q
```
```bat
"C:\Program Files\TrueCrypt\TrueCrypt.exe" /d /q
```
## Safety and communication rules
- Do not casually recommend migrating to VeraCrypt if the user explicitly asked for TrueCrypt help; answer the TrueCrypt question first.
- Do mention that TrueCrypt is discontinued when security or long-term maintenance is relevant.
- Do not put a real password into saved scripts unless the user explicitly requests that tradeoff.
- For destructive or risky actions, prepare the command and ask before executing it.
- If uncertain about a rare switch, say so plainly and stick to the known-safe command surface.
## Outputs to provide
Depending on the request, provide one of these:
- exact one-line mount or dismount command
- PowerShell or batch wrapper
- install-detection command
- short explanation of each switch used
- a cautious test plan for validating a container without exposing secrets
FILE:references/commands.md
# TrueCrypt command cookbook
Use these examples as templates. Replace paths, letters, and keyfile locations as needed.
## 1. Check whether TrueCrypt is installed
```powershell
$paths = @(
'C:\Program Files\TrueCrypt\TrueCrypt.exe',
'C:\Program Files (x86)\TrueCrypt\TrueCrypt.exe'
) | Where-Object { Test-Path $_ }
if ($paths) {
$paths
} else {
(Get-Command TrueCrypt.exe -ErrorAction SilentlyContinue).Source
}
```
## 2. Mount a file container
```bat
"C:\Program Files\TrueCrypt\TrueCrypt.exe" /v "C:\path\secret.tc" /l X /q
```
## 3. Mount with a keyfile
```bat
"C:\Program Files\TrueCrypt\TrueCrypt.exe" /v "C:\path\secret.tc" /l X /k "C:\path\keyfile.bin" /q
```
## 4. Mount read-only
```bat
"C:\Program Files\TrueCrypt\TrueCrypt.exe" /v "C:\path\secret.tc" /l X /m ro /q
```
## 5. Mount as removable media
```bat
"C:\Program Files\TrueCrypt\TrueCrypt.exe" /v "C:\path\secret.tc" /l X /m rm /q
```
## 6. Dismount one mounted volume
```bat
"C:\Program Files\TrueCrypt\TrueCrypt.exe" /d X /q
```
## 7. Dismount all TrueCrypt volumes
```bat
"C:\Program Files\TrueCrypt\TrueCrypt.exe" /d /q
```
## 8. Password on command line
Possible pattern:
```bat
"C:\Program Files\TrueCrypt\TrueCrypt.exe" /v "C:\path\secret.tc" /l X /p "YOUR_PASSWORD" /q
```
Use this only when the user explicitly accepts the risk. The password may be visible in process lists, logs, or history.
## 9. PowerShell wrapper pattern
```powershell
$tc = 'C:\Program Files\TrueCrypt\TrueCrypt.exe'
$volume = 'C:\path\secret.tc'
$letter = 'X'
& $tc /v $volume /l $letter /q
```
## Common switches
- `/v` - volume path
- `/l` - drive letter
- `/d` - dismount one letter or all volumes
- `/q` - quiet mode
- `/k` - keyfile path
- `/m` - mount options such as `ro` or `rm`
- `/p` - password on command line; avoid unless explicitly requested
## Notes
- Some old TrueCrypt GUI switches do not print helpful console output.
- Prefer giving users exact commands over telling them to discover help interactively.
- If a command must be validated safely, start with install checks or dismount commands before attempting a mount.
Human-friendly Brickset API v3 access for LEGO set lookup and Brickset automation. Use when you need to search LEGO sets, browse themes, years, or subthemes,...
---
name: brickset
description: Human-friendly Brickset API v3 access for LEGO set lookup and Brickset automation. Use when you need to search LEGO sets, browse themes, years, or subthemes, validate a Brickset API key, fetch building instructions or additional images, inspect API usage, or build scripts and agents on top of Brickset web services.
---
# Brickset
Use this skill for real Brickset API v3 operations with either raw JSON output or readable text summaries.
## Requirements
- `BRICKSET_API_KEY` must be set in the environment or workspace `.env`, or passed with `--api-key`
- Python 3.10+
## What works well
- `check-key` — validate the API key
- `usage-stats` — inspect 30-day API usage
- `themes` — list Brickset themes
- `subthemes` — list subthemes for a theme
- `years` — list release years, globally or per theme
- `search` — simple wrapper around `getSets`
- `get-sets` — raw `getSets` access with JSON params
- `instructions2` — fetch instructions by set number
- `additional-images` — fetch extra image URLs by Brickset `setID`
- `raw` — call any Brickset method directly when the built-in subcommands are not enough
## Output modes
- Default: JSON for scripting and automation
- `--format text`: readable summaries for humans
## Commands
```bash
# Validate key
python {{baseDir}}/scripts/brickset.py --format text check-key
# Usage stats
python {{baseDir}}/scripts/brickset.py --format text usage-stats
# Browse catalog metadata
python {{baseDir}}/scripts/brickset.py --format text themes
python {{baseDir}}/scripts/brickset.py --format text subthemes Technic
python {{baseDir}}/scripts/brickset.py --format text years
python {{baseDir}}/scripts/brickset.py --format text years --theme Space
# Search sets
python {{baseDir}}/scripts/brickset.py --format text search "Galaxy Explorer" --page-size 5
python {{baseDir}}/scripts/brickset.py --format text search Blacktron --theme Space --page-size 10 --order-by YearFromDESC
python {{baseDir}}/scripts/brickset.py get-sets --params '{"setNumber":"6990-1","extendedData":1}'
# Instructions and images
python {{baseDir}}/scripts/brickset.py --format text instructions2 10497-1
python {{baseDir}}/scripts/brickset.py --format text additional-images 1700
# Direct/raw API access
python {{baseDir}}/scripts/brickset.py raw getReviews --param setID=1700
python {{baseDir}}/scripts/brickset.py --format text raw getCollection --param userHash=<hash>
```
## Notes
- `getSets` consumes Brickset API quota.
- Brickset's `getSets` endpoint is happier when `userHash` is present, so the CLI sends an empty one automatically for anonymous searches.
- Use `raw` for methods like `login`, `checkUserHash`, `getReviews`, `getCollection`, or collection-management calls that are not wrapped yet.
## Reference
- Read `references/api.md` when you need the compact parameter guide for `getSets` or a reminder of which methods are available.
## Script
- `scripts/brickset.py` — main CLI entrypoint
FILE:references/api.md
# Brickset API v3 quick reference
Base endpoint: `https://brickset.com/api/v3.asmx`
Common methods used by this skill:
- `checkKey` — validate the API key
- `getKeyUsageStats` — inspect 30-day usage
- `getSets` — search sets or retrieve one set by number/setID
- `getThemes` — list themes
- `getSubthemes` — list subthemes for a theme
- `getYears` — list years, optionally filtered by theme
- `getInstructions2` — fetch instructions by set number, no setID lookup needed
- `getAdditionalImages` — fetch extra image URLs by setID
## getSets params JSON
Pass a JSON object string in the `params` field.
Useful fields:
- `query`
- `theme`
- `subtheme`
- `setNumber` like `6876-1`
- `year`
- `tag`
- `owned`
- `wanted`
- `orderBy`
- `pageSize` up to `500`
- `pageNumber`
- `extendedData` = `1`
Examples:
```json
{"theme":"Space","year":"1979,1980","pageSize":25}
```
```json
{"setNumber":"6990-1","extendedData":1}
```
```json
{"query":"Blacktron","orderBy":"YearFromDESC","pageSize":10}
```
## Notes
- All calls require `apiKey`.
- `getSets` counts against daily usage limits.
- `owned`/`wanted` set queries require `userHash`.
- Brickset returns JSON with a top-level `status` field.
FILE:scripts/brickset.py
#!/usr/bin/env python3
"""Brickset API v3 helper CLI.
Reads BRICKSET_API_KEY from:
1. --api-key
2. environment variable BRICKSET_API_KEY
3. workspace .env in current working directory or one of its parents
Default output is JSON. Use --format text for human-readable summaries.
"""
from __future__ import annotations
import argparse
import json
import os
from pathlib import Path
from typing import Any
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode
from urllib.request import Request, urlopen
API_BASE = "https://brickset.com/api/v3.asmx"
def load_dotenv(start: Path | None = None) -> dict[str, str]:
start = (start or Path.cwd()).resolve()
candidates = [start, *start.parents]
for directory in candidates:
env_path = directory / ".env"
if env_path.exists():
values: dict[str, str] = {}
for raw_line in env_path.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
values[key.strip()] = value.strip().strip('"').strip("'")
return values
return {}
def resolve_api_key(explicit: str | None) -> str:
if explicit:
return explicit
if os.getenv("BRICKSET_API_KEY"):
return os.environ["BRICKSET_API_KEY"]
env_values = load_dotenv()
if env_values.get("BRICKSET_API_KEY"):
return env_values["BRICKSET_API_KEY"]
raise SystemExit(
"BRICKSET_API_KEY is not set. Pass --api-key or add BRICKSET_API_KEY=... to the environment or workspace .env"
)
def call_api(method: str, api_key: str, extra_params: dict[str, Any] | None = None, post: bool = False) -> dict[str, Any]:
params: dict[str, Any] = {"apiKey": api_key}
if extra_params:
for key, value in extra_params.items():
if value is None:
continue
params[key] = value
url = f"{API_BASE}/{method}"
headers = {"Accept": "application/json"}
try:
if post:
body = urlencode(params).encode("utf-8")
request = Request(url, data=body, headers=headers, method="POST")
else:
request = Request(f"{url}?{urlencode(params)}", headers=headers, method="GET")
with urlopen(request, timeout=60) as response:
text = response.read().decode("utf-8")
except HTTPError as exc:
text = exc.read().decode("utf-8", errors="replace")
return {"status": "http_error", "code": exc.code, "body": text}
except URLError as exc:
return {"status": "network_error", "message": str(exc)}
try:
return json.loads(text)
except json.JSONDecodeError:
return {"status": "decode_error", "body": text}
def parse_params(raw: str | None) -> str | None:
if raw is None:
return None
parsed = json.loads(raw)
if not isinstance(parsed, dict):
raise SystemExit("--params must decode to a JSON object")
return json.dumps(parsed, separators=(",", ":"))
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="Brickset API v3 CLI")
parser.add_argument("--api-key", help="Brickset API key. Defaults to BRICKSET_API_KEY env/.env")
parser.add_argument("--format", choices=["json", "text"], default="json", help="Output format")
sub = parser.add_subparsers(dest="command", required=True)
sub.add_parser("check-key", help="Validate the API key")
sub.add_parser("usage-stats", help="Fetch 30-day API usage stats")
sub.add_parser("themes", help="List themes")
years = sub.add_parser("years", help="List years, optionally filtered by theme")
years.add_argument("--theme", default="", help="Theme name. Leave blank for all years")
subthemes = sub.add_parser("subthemes", help="List subthemes for a theme")
subthemes.add_argument("theme", help="Theme name")
get_sets = sub.add_parser("get-sets", help="Call getSets with raw JSON params")
get_sets.add_argument("--params", required=True, help='JSON object string, e.g. {"theme":"Space","pageSize":5}')
get_sets.add_argument("--user-hash", help="Optional user hash for owned/wanted queries")
search = sub.add_parser("search", help="Simple set search wrapper")
search.add_argument("query", help="Search term")
search.add_argument("--theme")
search.add_argument("--year")
search.add_argument("--page-size", type=int, default=10)
search.add_argument("--page-number", type=int, default=1)
search.add_argument("--order-by", default="Number")
search.add_argument("--extended-data", action="store_true")
instructions = sub.add_parser("instructions2", help="Get instructions by set number")
instructions.add_argument("set_number", help="Set number like 6876-1")
images = sub.add_parser("additional-images", help="Get additional images by Brickset setID")
images.add_argument("set_id", type=int, help="Brickset setID")
raw = sub.add_parser("raw", help="Call any Brickset method with arbitrary params")
raw.add_argument("method", help="Method name like getReviews or getCollection")
raw.add_argument("--param", action="append", default=[], help="key=value pair; repeatable")
raw.add_argument("--post", action="store_true", help="Use POST instead of GET")
return parser
def safe_get(obj: dict[str, Any], *keys: str, default: Any = None) -> Any:
current: Any = obj
for key in keys:
if not isinstance(current, dict) or key not in current:
return default
current = current[key]
return current
def format_set_line(item: dict[str, Any]) -> str:
number = f"{item.get('number', '?')}-{item.get('numberVariant', '?')}"
name = item.get("name", "(no name)")
year = item.get("year", "?")
theme = item.get("theme", "?")
pieces = item.get("pieces")
minifigs = item.get("minifigs")
extras: list[str] = []
if pieces is not None:
extras.append(f"{pieces} pcs")
if minifigs is not None:
extras.append(f"{minifigs} minifigs")
tail = f" - {', '.join(extras)}" if extras else ""
return f"- {number}: {name} ({year}, {theme}){tail}"
def format_result_text(command: str, args: argparse.Namespace, result: dict[str, Any]) -> str:
status = result.get("status")
if status in {"error", "http_error", "network_error", "decode_error"}:
parts = [f"Brickset API call failed: {status}"]
if "message" in result:
parts.append(str(result["message"]))
if "code" in result:
parts.append(f"HTTP {result['code']}")
if "body" in result:
body = str(result["body"]).strip()
if body:
parts.append(body[:1200])
return "\n".join(parts)
if command == "check-key":
return "Brickset API key is valid."
if command == "usage-stats":
matches = result.get("matches", 0)
lines = [f"Brickset API usage stats: {matches} day entries in the last 30 days."]
usage = result.get("apiKeyUsage", [])[:10]
for entry in usage:
lines.append(f"- {entry.get('dateStamp', '?')}: {entry.get('count', 0)} getSets calls")
if result.get("apiKeyUsage") and len(result["apiKeyUsage"]) > 10:
lines.append(f"- ... {len(result['apiKeyUsage']) - 10} more entries")
return "\n".join(lines)
if command == "themes":
themes = result.get("themes", [])
lines = [f"Brickset themes: {result.get('matches', len(themes))} total."]
for item in themes[:25]:
lines.append(
f"- {item.get('theme', '?')}: {item.get('setCount', 0)} sets, {item.get('subthemeCount', 0)} subthemes, {item.get('yearFrom', '?')}–{item.get('yearTo', '?')}"
)
if len(themes) > 25:
lines.append(f"- ... {len(themes) - 25} more themes")
return "\n".join(lines)
if command == "years":
years = result.get("years", [])
scope = args.theme if getattr(args, "theme", "") else "all themes"
lines = [f"Brickset years for {scope}: {result.get('matches', len(years))} entries."]
for item in years[:25]:
lines.append(f"- {item.get('year', '?')}: {item.get('setCount', 0)} sets")
if len(years) > 25:
lines.append(f"- ... {len(years) - 25} more years")
return "\n".join(lines)
if command == "subthemes":
subthemes = result.get("subthemes", [])
lines = [f"Brickset subthemes for {args.theme}: {result.get('matches', len(subthemes))} entries."]
for item in subthemes[:25]:
lines.append(
f"- {item.get('subtheme', '?')}: {item.get('setCount', 0)} sets, {item.get('yearFrom', '?')}–{item.get('yearTo', '?')}"
)
if len(subthemes) > 25:
lines.append(f"- ... {len(subthemes) - 25} more subthemes")
return "\n".join(lines)
if command in {"search", "get-sets"}:
sets = result.get("sets", [])
lines = [f"Brickset set results: {result.get('matches', len(sets))} matches."]
for item in sets[:20]:
lines.append(format_set_line(item))
if len(sets) > 20:
lines.append(f"- ... {len(sets) - 20} more sets in this page")
return "\n".join(lines)
if command == "instructions2":
instructions = result.get("instructions", [])
lines = [f"Instructions for {args.set_number}: {result.get('matches', len(instructions))} files."]
for item in instructions[:20]:
lines.append(f"- {item.get('description', '(no description)')}: {item.get('URL', '')}")
return "\n".join(lines)
if command == "additional-images":
images = result.get("additionalImages", [])
lines = [f"Additional images for setID {args.set_id}: {result.get('matches', len(images))} files."]
for item in images[:20]:
url = item.get("imageURL") or item.get("URL") or str(item)
lines.append(f"- {url}")
return "\n".join(lines)
if command == "raw":
lines = [f"Brickset raw call succeeded: {args.method}"]
for key in ("matches", "message"):
if key in result:
lines.append(f"- {key}: {result[key]}")
top_keys = ", ".join(sorted(result.keys()))
lines.append(f"- top-level fields: {top_keys}")
return "\n".join(lines)
return json.dumps(result, indent=2, ensure_ascii=False)
def main() -> int:
parser = build_parser()
args = parser.parse_args()
api_key = resolve_api_key(args.api_key)
if args.command == "check-key":
result = call_api("checkKey", api_key)
elif args.command == "usage-stats":
result = call_api("getKeyUsageStats", api_key)
elif args.command == "themes":
result = call_api("getThemes", api_key)
elif args.command == "years":
result = call_api("getYears", api_key, {"theme": args.theme})
elif args.command == "subthemes":
result = call_api("getSubthemes", api_key, {"theme": args.theme})
elif args.command == "get-sets":
result = call_api("getSets", api_key, {"userHash": args.user_hash or "", "params": parse_params(args.params)}, post=True)
elif args.command == "search":
payload = {
"query": args.query,
"pageSize": args.page_size,
"pageNumber": args.page_number,
"orderBy": args.order_by,
}
if args.theme:
payload["theme"] = args.theme
if args.year:
payload["year"] = str(args.year)
if args.extended_data:
payload["extendedData"] = 1
result = call_api("getSets", api_key, {"userHash": "", "params": json.dumps(payload, separators=(",", ":"))}, post=True)
elif args.command == "instructions2":
result = call_api("getInstructions2", api_key, {"setNumber": args.set_number})
elif args.command == "additional-images":
result = call_api("getAdditionalImages", api_key, {"setID": args.set_id})
elif args.command == "raw":
extra: dict[str, str] = {}
for item in args.param:
if "=" not in item:
raise SystemExit(f"Invalid --param '{item}'. Use key=value")
key, value = item.split("=", 1)
extra[key] = value
result = call_api(args.method, api_key, extra, post=args.post)
else:
parser.error("Unknown command")
return 2
if args.format == "text":
print(format_result_text(args.command, args, result))
else:
print(json.dumps(result, indent=2, ensure_ascii=False))
if result.get("status") in {"error", "http_error", "network_error", "decode_error"}:
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())
Query the Civitai public REST API to search models, inspect creators, fetch model or version details, reverse-lookup models by hash, list images or tags, and...
---
name: civitai-api
description: Query the Civitai public REST API to search models, inspect creators, fetch model or version details, reverse-lookup models by hash, list images or tags, and build authenticated download URLs. Use when working with Civitai programmatically, browsing Civitai assets from the terminal, checking model metadata, finding download links, or building automations against https://civitai.com and the developer portal.
---
# Civitai API
Use this skill to work with Civitai from the local workspace without re-deriving endpoints and auth each time.
## Quick start
Store the token in the workspace `.env` file as:
```env
CIVITAI_API_KEY=...
```
Use the bundled script:
```powershell
python .\skills\civitai-api\scripts\civitai.py models --query "flux lora" --limit 5
python .\skills\civitai-api\scripts\civitai.py model 12345
python .\skills\civitai-api\scripts\civitai.py version 67890
python .\skills\civitai-api\scripts\civitai.py by-hash SHA256_OR_AUTOV2_HASH
python .\skills\civitai-api\scripts\civitai.py creators --query "someuser"
python .\skills\civitai-api\scripts\civitai.py tags --query anime
python .\skills\civitai-api\scripts\civitai.py images --model-id 12345 --limit 10
python .\skills\civitai-api\scripts\civitai.py download-url 67890
```
## Workflow
### 1. Find the thing
When the user has a vague name or concept, start with:
```powershell
python .\skills\civitai-api\scripts\civitai.py models --query "search text" --limit 10
```
Useful optional filters include `--types`, `--tag`, `--username`, `--sort`, `--period`, `--cursor`, and `--nsfw true|false`.
### 2. Expand the record
Once you have a model id, inspect the full model payload:
```powershell
python .\skills\civitai-api\scripts\civitai.py model <modelId>
```
Use this to pull:
- version ids
- files and hashes
- tags
- creator info
- images
- download URLs already present in the payload
### 3. Inspect a specific version
When the user already knows the version id, or you need file-level details:
```powershell
python .\skills\civitai-api\scripts\civitai.py version <modelVersionId>
```
### 4. Reverse-lookup by hash
When the user has a local file hash and wants to identify it:
```powershell
python .\skills\civitai-api\scripts\civitai.py by-hash <hash>
```
### 5. Build a direct download URL
When the user wants an authenticated download URL, build it with:
```powershell
python .\skills\civitai-api\scripts\civitai.py download-url <modelVersionId>
```
Optional download selectors:
- `--type`
- `--format`
- `--size`
- `--fp`
Use the generated URL directly in a browser or another download tool. Treat the resulting URL as sensitive because it may include `?token=...`.
## Pagination note
Civitai search endpoints may use cursor-based pagination. When the response includes `metadata.nextCursor`, pass that value back with `--cursor` instead of forcing `--page` on search queries.
## Auth rules
- Prefer `Authorization: Bearer <token>` for JSON API calls.
- Use `?token=<token>` only for direct download URLs.
- Keep tokens in `.env`, not in the skill files.
## References
Read `references/api-notes.md` when you need a compact reminder of endpoints, auth, filters, and workflow hints.
FILE:references/api-notes.md
# Civitai Public REST API notes
## Base URLs
- REST base: `https://civitai.com/api/v1`
- Model downloads: `https://civitai.com/api/download/models/{modelVersionId}`
- Public docs: `https://developer.civitai.com/docs/api/public-rest`
## Auth
Use either:
- `Authorization: Bearer <token>` for normal API requests
- `?token=<token>` on download URLs
Prefer the header for JSON API calls. Use the query token only when constructing a direct download URL.
## Useful endpoints
- `GET /models` — search/list models
- `GET /models/{modelId}` — full model details
- `GET /model-versions/{modelVersionId}` — version details
- `GET /model-versions/by-hash/{hash}` — reverse lookup by file hash
- `GET /creators` — creator listing/search
- `GET /images` — image listing/search
- `GET /tags` — tag listing/search
- `GET /api/download/models/{modelVersionId}` — direct download endpoint
## Common filters
The docs/examples show these as common query inputs depending on endpoint:
- `query`
- `limit`
- `page`
- `sort`
- `period`
- `tag`
- `username`
- `types`
- `nsfw`
- `modelId`
- `modelVersionId`
- `postId`
## Response shape patterns
Expect paginated list endpoints to return objects with items and paging metadata. Model and version detail endpoints return a single object. Large fields often include nested stats, creator info, tags, files, images, and download URLs.
## Workflow hints
1. Start with `/models` when the user knows a name or concept but not an id.
2. Use `/models/{id}` to inspect versions, file hashes, tags, and images.
3. Use `/model-versions/{id}` when the user already has a version id.
4. Use `/model-versions/by-hash/{hash}` to identify a local checkpoint or LoRA file.
5. Use `/api/download/models/{versionId}` when the user wants a real file URL.
## Safety / hygiene
- Keep the token in `.env` as `CIVITAI_API_KEY`; do not hard-code it into the skill.
- Treat download links with embedded `?token=` as sensitive.
- Avoid echoing secrets back to chat unless the user explicitly asks.
FILE:scripts/civitai.py
#!/usr/bin/env python3
import argparse
import json
import os
import sys
import urllib.parse
import urllib.request
from pathlib import Path
BASE_URL = "https://civitai.com/api/v1"
DOWNLOAD_BASE_URL = "https://civitai.com/api/download/models"
def load_env_file(start: Path) -> None:
candidates = [start / ".env", start.parent / ".env"]
for env_path in candidates:
if not env_path.exists():
continue
for raw_line in env_path.read_text(encoding="utf-8").splitlines():
line = raw_line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
os.environ.setdefault(key.strip(), value.strip())
return
def build_url(path: str, params: dict | None = None) -> str:
url = f"{BASE_URL}{path}"
if params:
params = {k: v for k, v in params.items() if v not in (None, "", False)}
if params:
url = f"{url}?{urllib.parse.urlencode(params, doseq=True)}"
return url
def request_json(url: str, token: str | None = None) -> dict:
headers = {"User-Agent": "openclaw-civitai-skill/1.0", "Accept": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
req = urllib.request.Request(url, headers=headers)
with urllib.request.urlopen(req) as resp:
body = resp.read().decode("utf-8")
return json.loads(body)
def print_json(data: dict) -> None:
print(json.dumps(data, indent=2, ensure_ascii=False))
def cmd_models(args, token):
params = {
"query": args.query,
"limit": args.limit,
"page": args.page,
"cursor": args.cursor,
"types": args.types,
"sort": args.sort,
"period": args.period,
"username": args.username,
"tag": args.tag,
"nsfw": str(args.nsfw).lower() if args.nsfw is not None else None,
}
print_json(request_json(build_url("/models", params), token))
def cmd_model(args, token):
print_json(request_json(build_url(f"/models/{args.model_id}"), token))
def cmd_version(args, token):
print_json(request_json(build_url(f"/model-versions/{args.version_id}"), token))
def cmd_hash(args, token):
print_json(request_json(build_url(f"/model-versions/by-hash/{args.hash_value}"), token))
def cmd_creators(args, token):
params = {"query": args.query, "limit": args.limit, "page": args.page, "cursor": args.cursor}
print_json(request_json(build_url("/creators", params), token))
def cmd_tags(args, token):
params = {"query": args.query, "limit": args.limit, "page": args.page, "cursor": args.cursor}
print_json(request_json(build_url("/tags", params), token))
def cmd_images(args, token):
params = {
"postId": args.post_id,
"modelId": args.model_id,
"modelVersionId": args.model_version_id,
"username": args.username,
"limit": args.limit,
"page": args.page,
"cursor": args.cursor,
"sort": args.sort,
"period": args.period,
"nsfw": str(args.nsfw).lower() if args.nsfw is not None else None,
}
print_json(request_json(build_url("/images", params), token))
def cmd_download(args, token):
params = {}
if args.type:
params["type"] = args.type
if args.format:
params["format"] = args.format
if args.size:
params["size"] = args.size
if args.fp:
params["fp"] = args.fp
if token:
params["token"] = token
url = f"{DOWNLOAD_BASE_URL}/{args.version_id}"
if params:
url = f"{url}?{urllib.parse.urlencode(params)}"
print(url)
def main():
script_dir = Path(__file__).resolve().parent
load_env_file(script_dir)
try:
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
except Exception:
pass
parser = argparse.ArgumentParser(description="Query the Civitai public REST API.")
parser.add_argument("--token", default=os.getenv("CIVITAI_API_KEY"), help="Civitai API token. Defaults to CIVITAI_API_KEY from .env or the environment.")
subparsers = parser.add_subparsers(dest="command", required=True)
p = subparsers.add_parser("models", help="Search or list models")
p.add_argument("--query")
p.add_argument("--limit", type=int, default=10)
p.add_argument("--page", type=int)
p.add_argument("--cursor")
p.add_argument("--types")
p.add_argument("--sort")
p.add_argument("--period")
p.add_argument("--username")
p.add_argument("--tag")
p.add_argument("--nsfw", choices=["true", "false"])
p.set_defaults(func=cmd_models)
p = subparsers.add_parser("model", help="Get one model by id")
p.add_argument("model_id", type=int)
p.set_defaults(func=cmd_model)
p = subparsers.add_parser("version", help="Get one model version by id")
p.add_argument("version_id", type=int)
p.set_defaults(func=cmd_version)
p = subparsers.add_parser("by-hash", help="Get one model version by file hash")
p.add_argument("hash_value")
p.set_defaults(func=cmd_hash)
p = subparsers.add_parser("creators", help="Search or list creators")
p.add_argument("--query")
p.add_argument("--limit", type=int, default=10)
p.add_argument("--page", type=int)
p.add_argument("--cursor")
p.set_defaults(func=cmd_creators)
p = subparsers.add_parser("tags", help="Search or list tags")
p.add_argument("--query")
p.add_argument("--limit", type=int, default=10)
p.add_argument("--page", type=int)
p.add_argument("--cursor")
p.set_defaults(func=cmd_tags)
p = subparsers.add_parser("images", help="Search or list images")
p.add_argument("--post-id", type=int)
p.add_argument("--model-id", type=int)
p.add_argument("--model-version-id", type=int)
p.add_argument("--username")
p.add_argument("--limit", type=int, default=10)
p.add_argument("--page", type=int)
p.add_argument("--cursor")
p.add_argument("--sort")
p.add_argument("--period")
p.add_argument("--nsfw", choices=["true", "false"])
p.set_defaults(func=cmd_images)
p = subparsers.add_parser("download-url", help="Build an authenticated model download URL for a model version")
p.add_argument("version_id", type=int)
p.add_argument("--type")
p.add_argument("--format")
p.add_argument("--size")
p.add_argument("--fp")
p.set_defaults(func=cmd_download)
args = parser.parse_args()
try:
args.func(args, args.token)
except urllib.error.HTTPError as exc:
body = exc.read().decode("utf-8", errors="replace")
print(f"HTTP {exc.code}", file=sys.stderr)
if body:
print(body, file=sys.stderr)
sys.exit(1)
except Exception as exc:
print(str(exc), file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
Review, audit, coach, and extract PRISMA 2020 reporting compliance for systematic reviews, meta-analyses, protocols, reviewer comments, and draft manuscript...
--- name: prisma-2020-review-assistant description: Review, audit, coach, and extract PRISMA 2020 reporting compliance for systematic reviews, meta-analyses, protocols, reviewer comments, and draft manuscript sections. Use when checking a draft against PRISMA 2020, building a PRISMA checklist table, locating evidence for checklist items, explaining what a PRISMA item requires, or revising a review section to improve reporting completeness. --- # PRISMA 2020 Review Assistant Assess systematic review reporting against PRISMA 2020 and help authors close reporting gaps. ## Choose a mode Pick the mode that best matches the request: - **Reviewer**: check a draft and flag missing or weak PRISMA items. - **Coach**: help the user draft or revise one section at a time. - **Audit assistant**: produce a structured PRISMA comparison table. - **Extractor**: build an evidence map showing where each item is addressed. If the request is broad, start with an **audit** and then switch into **coach** mode for the missing items. ## Operating rules - Treat PRISMA as a **reporting guideline**, not proof that the methods were good. - Judge only from the text available. Do not assume an item is satisfied unless the manuscript states it. - Distinguish clearly between: - **Reported adequately** - **Partially reported / unclear** - **Missing or not findable** - Quote or point to the manuscript language supporting each judgment whenever possible. - Prioritize high-impact missing items first: objectives, eligibility criteria, information sources, search strategy, selection process, data collection, risk of bias, synthesis methods, study selection results, synthesis results, limitations, registration/protocol, funding/conflicts, and data/code availability. ## Minimum inputs Use whatever is available: - Full manuscript draft, pasted text, or excerpts - Supplementary files if available - Target journal if known - Whether the review includes meta-analysis, narrative synthesis only, or mixed methods - Whether the user wants strict compliance, coaching help, reviewer-style critique, or a checklist table If the user supplies only an abstract or outline, say the assessment is provisional. ## Reference files Load these only as needed: - `references/prisma-2020-map.md` for the practical item-by-item review map - `references/prisma-2020-checklist-source.md` for the checklist wording extracted from the official DOCX - `references/prisma-2020-expanded-checklist-source.md` for text extracted from the official expanded checklist PDF - `assets/source-docs/PRISMA_2020_checklist.docx` for the original checklist source file - `assets/source-docs/PRISMA_2020_expanded_checklist.pdf` for the original expanded checklist source file Use `prisma-2020-map.md` first for normal reviews. Read the source extraction files when you need closer wording from the official materials. ## Reviewer mode Use this mode for requests like: - "Check this draft systematic review for PRISMA gaps" - "Act like a reviewer on PRISMA reporting" - "What am I missing before submission?" ### Procedure 1. Identify which manuscript parts are available. 2. Map visible content to PRISMA sections: Title, Abstract, Introduction, Methods, Results, Discussion, Other Information. 3. Review each relevant PRISMA item and sub-item. 4. Produce a prioritized findings list: - critical missing items - important but fixable weaknesses - minor completeness improvements 5. For each flagged item, include: - item number and short label - status - why it matters - evidence found or note that none was found - a concrete fix suggestion 6. End with a short submission-readiness summary. ### Output pattern - **Overall PRISMA status:** strong / moderate / weak - **Critical gaps:** - **Section-by-section findings:** - **Fastest fixes before submission:** ## Coach mode Use this mode for requests like: - "Help me structure my systematic review" - "Walk me through the methods section" - "What should I include under PRISMA item 13?" ### Procedure 1. Ask which section the user is drafting, unless obvious. 2. Narrow to the relevant PRISMA items. 3. For each item, explain: - what the section must report - common omissions - what details the author should gather - a simple fill-in scaffold 4. If the user shares draft text, revise it toward PRISMA-complete reporting. ### Output pattern For each item, use: - **What to report** - **Questions to answer** - **Common misses** - **Draftable template language** ## Audit assistant mode Use this mode for requests like: - "Compare this manuscript against PRISMA" - "Make an audit table" - "Flag checklist gaps" ### Procedure 1. Build an item-by-item table. 2. Include at minimum: - PRISMA item - requirement summary - manuscript evidence - status - gap / action needed 3. Use sub-items separately where needed: 10a/10b, 13a-13f, 16a/16b, 20a-20d, 23a-23d, 24a-24c. 4. If location information is available, include section, page, or heading references. 5. Sort the action list by importance, not only checklist order. ### Status labels Use one of: - **Met** - **Partly met** - **Not met** - **Not assessable from provided text** - **Not applicable** ## Extractor mode Use this mode for requests like: - "Generate a reporting completeness table" - "Extract where each PRISMA item is addressed" - "Turn this draft into a checklist matrix" ### Procedure 1. Scan the manuscript for explicit evidence tied to each item. 2. Build a matrix with concise evidence snippets. 3. Preserve the author's wording where possible. 4. Mark unresolved items clearly instead of guessing. 5. Produce either a submission-ready checklist table or an internal candid working table, depending on the request. ## Prioritization heuristics If time is short, check these first: 1. **Methods transparency**: items 5-15 2. **Results traceability**: items 16-22 3. **Other information / trust signals**: items 24-27 4. **Framing clarity**: items 1-4 and abstract If the manuscript claims a meta-analysis, pay special attention to items 12, 13d-13f, 20b-20d, 21, and 22. ## Important distinctions - Separate **information sources** (item 6) from **full search strategies** (item 7). - Separate **selection process** (item 8) from **data collection process** (item 9). - Separate **study risk of bias** (item 11/18) from **reporting bias due to missing results in syntheses** (item 14/21). - Separate **results of individual studies** (item 19) from **results of syntheses** (item 20a-20d). - Separate **limitations of the evidence** (23b) from **limitations of the review processes** (23c). - Keep registration, protocol access, and amendments under item 24. ## Default deliverables Unless the user asks for something else, return: 1. A short overall assessment 2. A prioritized list of missing or weak items 3. A section-by-section PRISMA view 4. A small next-step plan for revision If the user explicitly wants a table, provide the audit/extractor matrix first and the narrative summary second. FILE:references/prisma-2020-checklist-source.md # PRISMA 2020 checklist (source extraction) Section and Topic Item # Checklist item Location where item is reported TITLE Title 1 Identify the report as a literature review. Page 1 ABSTRACT Abstract 2 Provide a structured summary including, as applicable: background; objectives; data sources; study eligibility criteria, participants, and interventions; study appraisal and synthesis methods; results; limitations; conclusions and implications of key findings. See the PRISMA 2020 for Abstracts checklist for the complete list. Page 2 INTRODUCTION Rationale 3 Describe the rationale for the review in the context of existing knowledge, i.e., what is already known about your topic. Page 3-4 Objectives 4 Provide an explicit statement of the objective(s) or question(s) the review addresses with reference to participants, interventions, comparisons, outcomes, and study design (PICOS). Page 3-4 METHODS Eligibility criteria 5 Specify the inclusion and exclusion criteria for the review and how studies were grouped for the syntheses with study characteristics (e.g., PICOS, length of follow-up) and report characteristics (e.g., years considered, language, publication status) used as criteria for eligibility, giving rationale. Page 5 Information sources 6 Specify all databases, registers, websites, organisations, reference lists and other sources searched or consulted to identify studies. Specify the date when each source was last searched or consulted. Page 5 Search strategy 7 Present the full search strategies for all databases, registers and websites, including any filters and limits used. Page 5 Selection process 8 State the process for selecting studies (i.e., screening, eligibility). Specify the methods used to decide whether a study met the inclusion criteria of the review, including how many reviewers screened each record and each report retrieved, whether they worked independently, and if applicable, details of automation tools used in the process. Page 5 Study risk of bias assessment 11 Specify the methods used to assess risk of bias in the included studies, including details of the tool(s) used, how many reviewers assessed each study and whether they worked independently, and if applicable, details of automation tools used in the process. Page 6 RESULTS Study selection 16a Describe the results of the search and selection process, from the number of records identified in the search to the number of studies included in the review, ideally using a flow diagram. Page 6-7 16b Cite studies that might appear to meet the inclusion criteria, but which were excluded, and explain why they were excluded. Page 6-7 Study characteristics 17 Cite each included study and present its characteristics (e.g., study size, PICOS, follow-up period). Page 7 Risk of bias in studies 18 Present assessments of risk of bias for each included study. Page 7-8 Results of individual studies 19 For all outcomes, present, for each study: (a) summary statistics for each group (where appropriate) and (b) an effect estimate and its precision (e.g. confidence/credible interval), ideally using structured tables or plots. Page 7-8 DISCUSSION Discussion 23a Provide a general interpretation of the results in the context of other evidence. Page 8-9 23b Discuss any limitations of the evidence included in the review. Page 9-11 23c Discuss any limitations of the review processes used. Page 12 23d Discuss implications of the results for practice, policy, and future research. Page 12-13 OTHER INFORMATION Registration and protocol 24a Provide registration information for the review, including register name and registration number, or state that the review was not registered. Page 4 24b Indicate where the review protocol can be accessed, or state that a protocol was not prepared. Page 4 24c Describe and explain any amendments to information provided at registration or in the protocol. Page 4 Support 25 Describe sources of financial or non-financial support for the review, and the role of the funders or sponsors in the review. Page 13 Competing interests 26 Declare any competing interests of review authors. Page 14 Availability of data, code, and other materials 27 Report which of the following are publicly available and where they can be found: template data collection forms; data extracted from included studies; data used for all analyses; analytic code; any other materials used in the review. Page 4-7 FILE:references/prisma-2020-expanded-checklist-source.md # PRISMA 2020 expanded checklist (source extraction) ## Page 1 PRISMA 2020 expanded checklist Note: This expanded checklist details elements recommended for reporting for each item in the PRISMA 2020 statement. Non-italicized elements are considered ‘essential’ and should be reported in the main report or as supplementary material for all systematic reviews (except for those preceded by “If…”, which should only be reported where applicable). Elements written in italics are ‘additional’, and while not essential, provide supplementary information that may enhance the completeness and usability of systematic review reports. Note that elements presented here are an abridged version of those presented in the explanation and elaboration paper (BMJ 2021;372:n160), with references and some examples removed. Consulting the explanation and elaboration paper is recommended if further clarity or information is required. Section and Topic Item # Items and elements recommended for reporting TITLE TITLE 1 Item: Identify the report as a systematic review. Elements: • Identify the report as a systematic review in the title. • Report an informative title that provides key information about the main objective or question the review addresses (e.g. the population(s) and intervention(s) the review addresses). • Consider providing additional information in the title, such as the method of analysis used, the designs of included studies, or an indication that the review is an update of an existing review, or a continually updated (“living”) systematic review. ABSTRACT ABSTRACT 2 Item: See the PRISMA 2020 for Abstracts checklist. Elements: • Report an abstract addressing each item in the PRISMA 2020 for Abstracts checklist. INTRODUCTION RATIONALE 3 Item: Describe the rationale for the review in the context of existing knowledge. Elements: • Describe the current state of knowledge and its uncertainties. • Articulate why it is important to do the review. • If other systematic reviews addressing the same (or a largely similar) question are available, explain why the current review was considered necessary. If the review is an update or replication of a particular systematic review, indicate this and cite the previous review. • If the review examines the effects of interventions, also briefly describe how the intervention(s) examined might work. • If there is complexity in the intervention or context of its delivery (or both) (e.g. multi-component interventions, equity considerations), consider presenting a logic model to visually display the hypothesised relationship between intervention components and outcomes. OBJECTIVES 4 Item: Provide an explicit statement of the objective(s) or question(s) the review addresses. Elements: • Provide an explicit statement of all objective(s) or question(s) the review addresses, expressed in terms of a relevant question formulation framework. • If the purpose is to evaluate the effects of interventions, use the Population, Intervention, Comparator, Outcome (PICO) framework or one of its variants, to state the comparisons that will be made. METHODS ELIGIBILITY CRITERIA 5 Item: Specify the inclusion and exclusion criteria for the review and how studies were grouped for the syntheses. Elements: • Specify all study characteristics used to decide whether a study was eligible for inclusion in the review, that is, components described in the PICO framework or one of its variants, and other characteristics, such as eligible study design(s) and setting(s), and minimum duration of follow-up. ## Page 2 Section and Topic Item # Items and elements recommended for reporting • Specify eligibility criteria with regard to report characteristics, such as year of dissemination, language, and report status (e.g. whether reports, such as unpublished manuscripts and conference abstracts, were eligible for inclusion). • Clearly indicate if studies were ineligible because the outcomes of interest were not measured, or ineligible because the results for the outcome of interest were not reported. • Specify any groups used in the synthesis (e.g. intervention, outcome and population groups) and link these to the comparisons specified in the objectives (item #4). • Consider providing rationales for any notable restrictions to study eligibility. INFORMATION SOURCES 6 Item: Specify all databases, registers, websites, organisations, reference lists and other sources searched or consulted to identify studies. Specify the date when each source was last searched or consulted. Elements: • Specify the date when each source (e.g. database, register, website, organisation) was last searched or consulted. • If bibliographic databases were searched, specify for each database its name (e.g. MEDLINE, CINAHL), the interface or platform through which the database was searched (e.g. Ovid, EBSCOhost), and the dates of coverage (where this information is provided). • If study registers, regulatory databases and other online repositories were searched, specify the name of each source and any date restrictions that were applied. • If websites, search engines or other online sources were browsed or searched, specify the name and URL of each source. • If organisations or manufacturers were contacted to identify studies, specify the name of each source. • If individuals were contacted to identify studies, specify the types of individuals contacted (e.g. authors of studies included in the review or researchers with expertise in the area). • If reference lists were examined, specify the types of references examined (e.g. references cited in study reports included in the systematic review, or references cited in systematic review reports on the same or similar topic). • If cited or citing reference searches (also called backward and forward citation searching) were conducted, specify the bibliographic details of the reports to which citation searching was applied, the citation index or platform used (e.g. Web of Science), and the date the citation searching was done. • If journals or conference proceedings were consulted, specify of the names of each source, the dates covered and how they were searched (e.g. handsearching or browsing online). SEARCH STRATEGY 7 Item: Present the full search strategies for all databases, registers and websites, including any filters and limits used. Element: • Provide the full line by line search strategy as run in each database with a sophisticated interface (such as Ovid), or the sequence of terms that were used to search simpler interfaces, such as search engines or websites. • Describe any limits applied to the search strategy (e.g. date or language) and justify these by linking back to the review’s eligibility criteria. • If published approaches, including search filters designed to retrieve specific types of records or search strategies from other systematic reviews, were used, cite them. If published approaches were adapted, for example if search filters are amended, note the changes made. • If natural language processing or text frequency analysis tools were used to identify or refine keywords, synonyms or subject indexing terms to use in the search strategy, specify the tool(s) used. • If a tool was used to automatically translate search strings for one database to another, specify the tool used. • If the search strategy was validated, for example by evaluating whether it could identify a set of clearly eligible studies, report the validation process used and specify which studies were included in the validation set. • If the search strategy was peer reviewed, report the peer review process used and specify any tool used such as the Peer Review of Electronic Search Strategies (PRESS) checklist. • If the search strategy structure adopted was not based on a PICO-style approach, describe the final conceptual structure and any explorations that were undertaken to achieve it. ## Page 3 Section and Topic Item # Items and elements recommended for reporting SELECTION PROCESS 8 Item: Specify the methods used to decide whether a study met the inclusion criteria of the review, including how many reviewers screened each record and each report retrieved, whether they worked independently, and if applicable, details of automation tools used in the process. Elements: Recommendations for reporting regardless of the selection processes used: • Report how many reviewers screened each record (title/abstract) and each report retrieved, whether multiple reviewers worked independently at each stage of screening or not, and any processes used to resolve disagreements between screeners. • Report any processes used to obtain or confirm relevant information from study investigators. • If abstracts or articles required translation into another language to determine their eligibility, report how these were translated. Recommendations for reporting in systematic reviews using automation tools in the selection process: • Report how automation tools were integrated within the overall study selection process. • If an externally derived machine learning classifier was applied (e.g. Cochrane RCT Classifier), either to eliminate records or to replace a single screener, include a reference or URL to the version used. If the classifier was used to eliminate records before screening, report the number eliminated in the PRISMA flow diagram as ‘Records marked as ineligible by automation tools’. • If an internally derived machine learning classifier was used to assist with the screening process, identify the software/classifier and version, describe how it was used (e.g. to remove records or replace a single screener) and trained (if relevant), and what internal or external validation was done to understand the risk of missed studies or incorrect classifications. • If machine learning algorithms were used to prioritise screening (whereby unscreened records are continually re-ordered based on screening decisions), state the software used and provide details of any screening rules applied. Recommendations for reporting in systematic reviews using crowdsourcing or previous ‘known’ assessments in the selection process: • If crowdsourcing was used to screen records, provide details of the platform used and specify how it was integrated within the overall study selection process. • If datasets of already-screened records were used to eliminate records retrieved by the search from further consideration, briefly describe the derivation of these datasets. DATA COLLECTION PROCESS 9 Item: Specify the methods used to collect data from reports, including how many reviewers collected data from each report, whether they worked independently, any processes for obtaining or confirming data from study investigators, and if applicable, details of automation tools used in the process. Elements: • Report how many reviewers collected data from each report, whether multiple reviewers worked independently or not, and any processes used to resolve disagreements between data collectors. • Report any processes used to obtain or confirm relevant data from study investigators. • If any automation tools were used to collect data, report how the tool was used, how the tool was trained, and what internal or external validation was done to understand the risk of incorrect extractions. • If articles required translation into another language to enable data collection, report how these articles were translated. • If any software was used to extract data from figures, specify the software used. • If any decision rules were used to select data from multiple reports corresponding to a study, and any steps were taken to resolve inconsistencies across reports, report the rules and steps used. DATA ITEMS (outcomes) 10a Item: List and define all outcomes for which data were sought. Specify whether all results that were compatible with each outcome domain in each study were sought (e.g. for all measures, time points, analyses), and if not, the methods used to decide which results to collect. Elements: • List and define the outcome domains and time frame of measurement for which data were sought. ## Page 4 Section and Topic Item # Items and elements recommended for reporting • Specify whether all results that were compatible with each outcome domain in each study were sought, and if not, what process was used to select results within eligible domains. • If any changes were made to the inclusion or definition of the outcome domains, or to the importance given to them in the review, specify the changes, along with a rationale. • If any changes were made to the processes used to select results within eligible outcome domains, specify the changes, along with a rationale. • Consider specifying which outcome domains were considered the most important for interpreting the review’s conclusions and provide rationale for the labelling (e.g. “a recent core outcome set identified the outcomes labelled ‘critical’ as being the most important to patients”). DATA ITEMS (other variables) 10b Item: List and define all other variables for which data were sought (e.g. participant and intervention characteristics, funding sources). Describe any assumptions made about any missing or unclear information. Elements: • List and define all other variables for which data were sought (e.g. participant and intervention characteristics, funding sources). • Describe any assumptions made about any missing or unclear information from the studies. • If a tool was used to inform which data items to collect, cite the tool used. STUDY RISK OF BIAS ASSESSMENT 11 Item: Specify the methods used to assess risk of bias in the included studies, including details of the tool(s) used, how many reviewers assessed each study and whether they worked independently, and if applicable, details of automation tools used in the process. Elements: • Specify the tool(s) (and version) used to assess risk of bias in the included studies. • Specify the methodological domains/components/items of the risk of bias tool(s) used. • Report whether an overall risk of bias judgement that summarised across domains/components/items was made, and if so, what rules were used to reach an overall judgement. • If any adaptations to an existing tool to assess risk of bias in studies were made, specify the adaptations. • If a new risk of bias tool was developed for use in the review, describe the content of the tool and make it publicly accessible. • Report how many reviewers assessed risk of bias in each study, whether multiple reviewers worked independently, and any processes used to resolve disagreements between assessors. • Report any processes used to obtain or confirm relevant information from study investigators. • If an automation tool was used to assess risk of bias, report how the automation tool was used, how the tool was trained, and details on the tool’s performance and internal validation. EFFECT MEASURES 12 Item: Specify for each outcome the effect measure(s) (e.g. risk ratio, mean difference) used in the synthesis or presentation of results. Elements: • Specify for each outcome (or type of outcome [e.g. binary, continuous]), the effect measure(s) (e.g. risk ratio, mean difference) used in the synthesis or presentation of results. • State any thresholds (or ranges) used to interpret the size of effect (e.g. minimally important difference; ranges for no/trivial, small, moderate and large effects) and the rationale for these thresholds. • If synthesized results were re-expressed to a different effect measure, report the method used to re-express results (e.g. meta-analysing risk ratios and computing an absolute risk reduction based on an assumed comparator risk). • Consider providing justification for the choice of effect measure. SYNTHESIS METHODS (eligibility for synthesis) 13a Item: Describe the processes used to decide which studies were eligible for each synthesis (e.g. tabulating the study intervention characteristics and comparing against the planned groups for each synthesis (item #5)). Element: • Describe the processes used to decide which studies were eligible for each synthesis. ## Page 5 Section and Topic Item # Items and elements recommended for reporting SYNTHESIS METHODS (preparing for synthesis) 13b Item: Describe any methods required to prepare the data for presentation or synthesis, such as handling of missing summary statistics, or data conversions. Element: • Report any methods required to prepare the data collected from studies for presentation or synthesis, such as handling of missing summary statistics, or data conversions. SYNTHESIS METHODS (tabulation and graphical methods) 13c Item: Describe any methods used to tabulate or visually display results of individual studies and syntheses. Elements: • Report chosen tabular structure(s) used to display results of individual studies and syntheses, along with details of the data presented. • Report chosen graphical methods used to visually display results of individual studies and syntheses. • If studies are ordered or grouped within tables or graphs based on study characteristics (e.g. by size of the study effect, year of publication), consider reporting the basis for the chosen ordering/grouping. • If non-standard graphs were used, consider reporting the rationale for selecting the chosen graph. SYNTHESIS METHODS (statistical synthesis methods) 13d Item: Describe any methods used to synthesize results and provide a rationale for the choice(s). If meta-analysis was performed, describe the model(s), method(s) to identify the presence and extent of statistical heterogeneity, and software package(s) used. Elements: • If statistical synthesis methods were used, reference the software, packages and version numbers used to implement synthesis methods. • If it was not possible to conduct a meta-analysis, describe and justify the synthesis methods or summary approach used. • If meta-analysis was done, specify: o the meta-analysis model (fixed-effect, fixed-effects or random-effects) and provide rationale for the selected model. o the method used (e.g. Mantel-Haenszel, inverse-variance). o any methods used to identify or quantify statistical heterogeneity (e.g. visual inspection of results, a formal statistical test for heterogeneity, heterogeneity variance (𝜏2), inconsistency (e.g. I2), and prediction intervals). • If a random-effects meta-analysis model was used: o specify the between-study (heterogeneity) variance estimator used (e.g. DerSimonian and Laird, restricted maximum likelihood (REML)). o specify the method used to calculate the confidence interval for the summary effect (e.g. Wald-type confidence interval, Hartung-Knapp-Sidik- Jonkman). o consider specifying other details about the methods used, such as the method for calculating confidence limits for the heterogeneity variance. • If a Bayesian approach to meta-analysis was used, describe the prior distributions about quantities of interest (e.g. intervention effect being analysed, amount of heterogeneity in results across studies). • If multiple effect estimates from a study were included in a meta-analysis, describe the method(s) used to model or account for the statistical dependency (e.g. multivariate meta-analysis, multilevel models or robust variance estimation). • If a planned synthesis was not considered possible or appropriate, report this and the reason for that decision. SYNTHESIS METHODS (methods to explore heterogeneity) 13e Item: Describe any methods used to explore possible causes of heterogeneity among study results (e.g. subgroup analysis, meta-regression). Elements: • If methods were used to explore possible causes of statistical heterogeneity, specify the method used (e.g. subgroup analysis, meta-regression). • If subgroup analysis or meta-regression was performed, specify for each: o which factors were explored, levels of those factors, and which direction of effect modification was expected and why (where possible). o whether analyses were conducted using study-level variables (i.e. where each study is included in one subgroup only), within-study contrasts (i.e. where data on subsets of participants within a study are available, allowing the study to be included in more than one subgroup), or some combination of the above. ## Page 6 Section and Topic Item # Items and elements recommended for reporting o how subgroup effects were compared (e.g. statistical test for interaction for subgroup analyses). • If other methods were used to explore heterogeneity because data were not amenable to meta-analysis of effect estimates (e.g. structuring tables to examine variation in results across studies based on subpopulation), describe the methods used, along with the factors and levels. • If any analyses used to explore heterogeneity were not pre-specified, identify them as such. SYNTHESIS METHODS (sensitivity analyses) 13f Item: Describe any sensitivity analyses conducted to assess robustness of the synthesized results. Elements: • If sensitivity analyses were performed, provide details of each analysis (e.g. removal of studies at high risk of bias, use of an alternative meta-analysis model). • If any sensitivity analyses were not pre-specified, identify them as such. REPORTING BIAS ASSESSMENT 14 Item: Describe any methods used to assess risk of bias due to missing results in a synthesis (arising from reporting biases). Elements: • Specify the methods (tool, graphical, statistical or other) used to assess the risk of bias due to missing results in a synthesis (arising from reporting biases). • If risk of bias due to missing results was assessed using an existing tool, specify the methodological components/domains/items of the tool, and the process used to reach a judgement of overall risk of bias. • If any adaptations to an existing tool to assess risk of bias due to missing results were made, specify the adaptations. • If a new tool to assess risk of bias due to missing results was developed for use in the review, describe the content of the tool and make it publicly accessible. • Report how many reviewers assessed risk of bias due to missing results in a synthesis, whether multiple reviewers worked independently, and any processes used to resolve disagreements between assessors. • Report any processes used to obtain or confirm relevant information from study investigators. • If an automation tool was used to assess risk of bias due to missing results, report how the automation tool was used, how the tool was trained, and details on the tool’s performance and internal validation. CERTAINTY ASSESSMENT 15 Item: Describe any methods used to assess certainty (or confidence) in the body of evidence for an outcome. Elements: • Specify the tool or system (and version) used to assess certainty (or confidence) in the body of evidence. • Report the factors considered (e.g. precision of the effect estimate, consistency of findings across studies) and the criteria used to assess each factor when assessing certainty in the body of evidence. • Describe the decision rules used to arrive at an overall judgement of the level of certainty, together with the intended interpretation (or definition) of each level of certainty. • If applicable, report any review-specific considerations for assessing certainty, such as thresholds used to assess imprecision and ranges of magnitude of effect that might be considered trivial, moderate or large, and the rationale for these thresholds and ranges (item #12). • If any adaptations to an existing tool or system to assess certainty were made, specify the adaptations. • Report how many reviewers assessed certainty in the body of evidence for an outcome, whether multiple reviewers worked independently, and any processes used to resolve disagreements between assessors. • Report any processes used to obtain or confirm relevant information from investigators. • If an automation tool was used to support the assessment of certainty, report how the automation tool was used, how the tool was trained, and details on the tool’s performance and internal validation. • Describe methods for reporting the results of assessments of certainty, such as the use of Summary of Findings tables. • If standard phrases that incorporate the certainty of evidence were used (e.g. “hip protectors probably reduce the risk of hip fracture slightly”), report the intended interpretation of each phrase and the reference for the source guidance. ## Page 7 Section and Topic Item # Items and elements recommended for reporting RESULTS STUDY SELECTION (flow of studies) 16a Item: Describe the results of the search and selection process, from the number of records identified in the search to the number of studies included in the review, ideally using a flow diagram. Elements: • Report, ideally using a flow diagram, the number of: records identified; records excluded before screening; records screened; records excluded after screening titles or titles and abstracts; reports retrieved for detailed evaluation; potentially eligible reports that were not retrievable; retrieved reports that did not meet inclusion criteria and the primary reasons for exclusion; and the number of studies and reports included in the review. If applicable, also report the number of ongoing studies and associated reports identified. • If the review is an update of a previous review, report results of the search and selection process for the current review and specify the number of studies included in the previous review. • If applicable, indicate in the PRISMA flow diagram how many records were excluded by a human and how many by automation tools. STUDY SELECTION (excluded studies) 16b Item: Cite studies that might appear to meet the inclusion criteria, but which were excluded, and explain why they were excluded. Element: • Cite studies that might appear to meet the inclusion criteria, but which were excluded, and explain why they were excluded. STUDY CHARACTERISTICS 17 Item: Cite each included study and present its characteristics. Elements: • Cite each included study. • Present the key characteristics of each study in a table or figure (considering a format that will facilitate comparison of characteristics across the studies). • If the review examines the effects of interventions, consider presenting an additional table that summarises the intervention details for each study. RISK OF BIAS IN STUDIES 18 Item: Present assessments of risk of bias for each included study. Elements: • Present tables or figures indicating for each study the risk of bias in each domain/component/item assessed (e.g. blinding of outcome assessors, missing outcome data) and overall study-level risk of bias. • Present justification for each risk of bias judgement, for example in the form of relevant quotations from reports of included studies. • If assessments of risk of bias were done for specific outcomes or results in each study, consider displaying risk of bias judgements on a forest plot, next to the study results. RESULTS OF INDIVIDUAL STUDIES 19 Item: For all outcomes, present, for each study: (a) summary statistics for each group (where appropriate) and (b) an effect estimate and its precision (e.g. confidence/credible interval), ideally using structured tables or plots. Elements: • For all outcomes, irrespective of whether statistical synthesis was undertaken, present for each study summary statistics for each group (where appropriate). For dichotomous outcomes, report the number of participants with and without the events for each group; or the number with the event and the total for each group (e.g. 12/45). For continuous outcomes, report the mean, standard deviation and sample size of each group. • For all outcomes, irrespective of whether statistical synthesis was undertaken, present for each study an effect estimate and its precision (e.g. standard error or 95% confidence/credible interval). For example, for time-to-event outcomes, present a hazard ratio and its confidence interval. • If study-level data is presented visually or reported in the text (or both), also present a tabular display of the results. • If results were obtained from multiple data sources (e.g. journal article, study register entry, clinical study report, correspondence with authors), report the source of the data. • If applicable, indicate which results were not reported directly and had to be computed or estimated from other information. RESULTS OF SYNTHESES 20a Item: For each synthesis, briefly summarise the characteristics and risk of bias among contributing studies. Elements: ## Page 8 Section and Topic Item # Items and elements recommended for reporting (characteristics of contributing studies) • Provide a brief summary of the characteristics and risk of bias among studies contributing to each synthesis (meta-analysis or other). The summary should focus only on study characteristics that help in interpreting the results (especially those that suggest the evidence addresses only a restricted part of the review question, or indirectly addresses the question). • Indicate which studies were included in each synthesis (e.g. by listing each study in a forest plot or table or citing studies in the text). RESULTS OF SYNTHESES (results of statistical syntheses) 20b Item: Present results of all statistical syntheses conducted. If meta-analysis was done, present for each the summary estimate and its precision (e.g. confidence/credible interval) and measures of statistical heterogeneity. If comparing groups, describe the direction of the effect. Elements: • Report results of all statistical syntheses described in the protocol and all syntheses conducted that were not pre-specified. • If meta-analysis was conducted, report for each: o the summary estimate and its precision (e.g. standard error or 95% confidence/credible interval) o measures of statistical heterogeneity (e.g. 𝜏2, I2, prediction interval) • If other statistical synthesis methods were used (e.g. summarising effect estimates, combining P values), report the synthesized result and a measure of precision (or equivalent information, for example, the number of studies and total sample size). • If the statistical synthesis method does not yield an estimate of effect (e.g. as is the case when P values are combined), report the relevant statistics (e.g. P value from the statistical test), along with an interpretation of the result that is consistent with the question addressed by the synthesis method. • If comparing groups, describe the direction of effect (e.g. fewer events in the intervention group, or higher pain in the comparator group). • If synthesising mean differences, specify for each synthesis, where applicable, the unit of measurement (e.g. kilograms or pounds for weight), the upper and lower limits of the measurement scale (e.g. anchors range from 0 to 10), direction of benefit (e.g. higher scores denote higher severity of pain), and the minimally important difference, if known. If synthesising standardised mean differences, and the effect estimate is being re-expressed to a particular instrument, details of the instrument, as per the mean difference, should be reported. RESULTS OF SYNTHESES (results of investigations of heterogeneity) 20c Item: Present results of all investigations of possible causes of heterogeneity among study results. Elements: • If investigations of possible causes of heterogeneity were conducted: o present results regardless of the statistical significance, magnitude, or direction of effect modification. o identify the studies contributing to each subgroup. o report results with due consideration to the observational nature of the analysis and risk of confounding due to other factors. • If subgroup analysis was conducted: o report for each analysis the exact P value for a test for interaction, as well as, within each subgroup, the summary estimates, their precision (e.g. standard error or 95% confidence/credible interval) and measures of heterogeneity. o consider presenting the estimate for the difference between subgroups and its precision. • If meta-regression was conducted: o report for each analysis the exact P value for the regression coefficient and its precision. o consider presenting a meta-regression scatterplot with the study effect estimates plotted against the potential effect modifier. • If informal methods (i.e. those that do not involve a formal statistical test) were used to investigate heterogeneity, describe the results observed. RESULTS OF SYNTHESES (results of sensitivity analyses) 20d Item: Present results of all sensitivity analyses conducted to assess the robustness of the synthesized results. Elements: • If any sensitivity analyses were conducted: o report the results for each sensitivity analysis. o comment on how robust the main analysis was given the results of all corresponding sensitivity analyses. ## Page 9 Section and Topic Item # Items and elements recommended for reporting o consider presenting results in tables that indicate: (i) the summary effect estimate, a measure of precision (and potentially other relevant statistics, for example, I2 statistic) and contributing studies for the original meta-analysis; (ii) the same information for the sensitivity analysis; and (iii) details of the original and sensitivity analysis assumptions. o consider presenting results of sensitivity analyses visually using forest plots. REPORTING BIASES 21 Item: Present assessments of risk of bias due to missing results (arising from reporting biases) for each synthesis assessed. Elements: • Present assessments of risk of bias due to missing results (arising from reporting biases) for each synthesis assessed. • If a tool was used to assess risk of bias due to missing results in a synthesis, present responses to questions in the tool, judgements about risk of bias and any information used to support such judgements. • If a funnel plot was generated to evaluate small-study effects (one cause of which is reporting biases), present the plot and specify the effect estimate and measure of precision used in the plot. If a contour-enhanced funnel plot was generated, specify the ‘milestones’ of statistical significance that the plotted contour lines represent (P = 0.01, 0.05, 0.1, etc.) • If a test for funnel plot asymmetry was used, report the exact P value observed for the test, and potentially other relevant statistics, for example the standardised normal deviate, from which the P value is derived. • If any sensitivity analyses seeking to explore the potential impact of missing results on the synthesis were conducted, present results of each analysis (see item #20d), compare them with results of the primary analysis, and report results with due consideration of the limitations of the statistical method. • If studies were assessed for selective non-reporting of results by comparing outcomes and analyses pre-specified in study registers, protocols, and statistical analysis plans with results that were available in study reports, consider presenting a matrix (with rows as studies and columns as syntheses) to present the availability of study results. • If an assessment of selective non-reporting of results reveals that some studies are missing from the synthesis, consider displaying the studies with missing results underneath a forest plot or including a table with the available study results. CERTAINTY OF EVIDENCE 22 Item: Present assessments of certainty (or confidence) in the body of evidence for each outcome assessed. Elements: • Report the overall level of certainty (or confidence) in the body of evidence for each important outcome. • Provide an explanation of reasons for rating down (or rating up) the certainty of evidence (e.g. in footnotes to an evidence summary table). • Communicate certainty in the evidence wherever results are reported (i.e. abstract, evidence summary tables, results, conclusions), using a format appropriate for the section of the review. • Consider including evidence summary tables, such as GRADE Summary of Findings tables. DISCUSSION DISCUSSION (interpretation) 23a Item: Provide a general interpretation of the results in the context of other evidence. Element: • Provide a general interpretation of the results in the context of other evidence. DISCUSSION (limitations of evidence) 23b Item: Discuss any limitations of the evidence included in the review. Element: • Discuss any limitations of the evidence included in the review. DISCUSSION (limitations of review processes) 23c Item: Discuss any limitations of the review processes used. Element: • Discuss any limitations of the review processes used, and comment on the potential impact of each limitation. DISCUSSION (implications) 23d Item: Discuss implications of the results for practice, policy, and future research. Elements: ## Page 10 Section and Topic Item # Items and elements recommended for reporting • Discuss implications of the results for practice and policy. • Make explicit recommendations for future research. OTHER INFORMATION REGISTRATION AND PROTOCOL (registration) 24a Item: Provide registration information for the review, including register name and registration number, or state that the review was not registered. Element: • Provide registration information for the review, including register name and registration number, or state that the review was not registered. REGISTRATION AND PROTOCOL (protocol) 24b Item: Indicate where the review protocol can be accessed, or state that a protocol was not prepared. Element: • Indicate where the review protocol can be accessed (e.g. by providing a citation, DOI or link), or state that a protocol was not prepared. REGISTRATION AND PROTOCOL (amendments) 24c Item: Describe and explain any amendments to information provided at registration or in the protocol. Element: • Report details of any amendments to information provided at registration or in the protocol, noting: (a) the amendment itself; (b) the reason for the amendment; and (c) the stage of the review process at which the amendment was implemented. SUPPORT 25 Item: Describe sources of financial or non-financial support for the review, and the role of the funders or sponsors in the review. Elements: • Describe sources of financial or non-financial support for the review, specifying relevant grant ID numbers for each funder. If no specific financial or non- financial support was received, this should be stated. • Describe the role of the funders or sponsors (or both) in the review. If funders or sponsors had no role in the review, this should be declared. COMPETING INTERESTS 26 Item: Declare any competing interests of review authors. Elements: • Disclose any of the authors’ relationships or activities that readers could consider pertinent or to have influenced the review. • If any authors had competing interests, report how they were managed for particular review processes. AVAILABILITY OF DATA, CODE, AND OTHER MATERIALS 27 Item: Report which of the following are publicly available and where they can be found: template data collection forms; data extracted from included studies; data used for all analyses; analytic code; any other materials used in the review. Elements: • Report which of the following are publicly available: template data collection forms; data extracted from included studies; data used for all analyses; analytic code; any other materials used in the review. • If any of the above materials are publicly available, report where they can be found (e.g. provide a link to files deposited in a public repository). • If data, analytic code, or other materials will be made available upon request, provide the contact details of the author responsible for sharing the materials and describe the circumstances under which such materials will be shared. From: Page MJ, McKenzie JE, Bossuyt PM, Boutron I, Hoffmann TC, Mulrow CD, et al. The PRISMA 2020 statement: an updated guideline for reporting systematic reviews. BMJ 2021;372:n71. doi: 10.1136/bmj.n71. For more information, visit: https://www.prisma-statement.org/ FILE:references/prisma-2020-map.md # PRISMA 2020 map for manuscript review Use this file when you need item-level detail during review, coaching, auditing, or extraction. ## Quick use - For a fast audit, review all items once and expand only those marked Partly met or Not met. - For coaching, focus only on the current manuscript section and its linked items. - For extraction, copy short evidence snippets rather than paraphrasing everything. ## Title and abstract ### Item 1 — Title **Look for:** explicit identification as a systematic review; optionally mention meta-analysis, update, or study design. **Common misses:** title says only "review"; title confuses systematic review with meta-analysis. **Coach cue:** include population/intervention/topic plus "systematic review". ### Item 2 — Abstract **Look for:** PRISMA abstract checklist elements: objective, eligibility criteria, sources and last search date, risk-of-bias methods, synthesis methods, included studies/participants, main results, limitations, interpretation, funding, registration. **Common misses:** no last search date; no limitations; no registration; result reporting too vague. **Extractor cue:** build a 12-item abstract mini-check if the abstract is provided. ## Introduction ### Item 3 — Rationale **Look for:** current knowledge, uncertainty, why this review is needed, and why prior reviews are insufficient if they exist. **Common misses:** generic motivation; no gap in existing evidence; no explanation of why a new review is necessary. ### Item 4 — Objectives **Look for:** explicit review objective or question; PICO or similar framing when relevant. **Common misses:** broad purpose without a precise question; mismatch between question and later synthesis. ## Methods ### Item 5 — Eligibility criteria **Look for:** inclusion/exclusion criteria, study characteristics, report characteristics, and how studies were grouped for syntheses. **Common misses:** missing exclusion criteria; unclear study designs; no grouping logic for synthesis. ### Item 6 — Information sources **Look for:** all databases/registers/websites/other sources plus date last searched or consulted for each. **Common misses:** databases named without platform or dates; grey literature sources omitted; no search date. ### Item 7 — Search strategy **Look for:** full reproducible search strategies for all databases/registers/websites, including filters and limits. **Common misses:** summary only; no exact strings; limits not reported; supplementary appendix mentioned but absent. ### Item 8 — Selection process **Look for:** how records/reports were screened, number of reviewers, independence, conflict resolution, automation tools. **Common misses:** says studies were screened but not by whom or whether independently. ### Item 9 — Data collection process **Look for:** who extracted data, whether independently, use of forms, contact with investigators, automation tools. **Common misses:** no extractor count; no duplicate extraction details; no handling of unclear data. ### Item 10a — Data items: outcomes **Look for:** all outcomes sought; definitions; whether all compatible results were sought across measures/time points/analyses. **Common misses:** outcomes named but not defined; selective result collection process not described. ### Item 10b — Data items: other variables **Look for:** participant/intervention/context/funding and other variables; assumptions for missing or unclear information. **Common misses:** covariates not defined; assumptions left implicit. ### Item 11 — Study risk of bias assessment **Look for:** tool used, number of reviewers, independence, domain-level process, automation tools if any. **Common misses:** tool named but procedure not described; unclear who assessed bias. ### Item 12 — Effect measures **Look for:** effect measures for each outcome or synthesis. **Common misses:** meta-analysis reported without defining RR/OR/MD/SMD or equivalent. ### Item 13a — Synthesis methods: study eligibility for each synthesis **Look for:** how studies were assigned to each synthesis. **Common misses:** pooled analyses shown without explaining why those studies were grouped. ### Item 13b — Synthesis methods: data preparation **Look for:** conversions, handling of missing summary statistics, transformations. **Common misses:** imputation or conversion performed but not reported. ### Item 13c — Synthesis methods: tabulation/visual display **Look for:** methods for tables, forest plots, harvest plots, etc. **Common misses:** displays shown but reporting method absent. ### Item 13d — Synthesis methods: synthesis approach **Look for:** narrative/statistical synthesis method, rationale, model, heterogeneity methods, software. **Common misses:** no rationale for random/fixed effects; software omitted; narrative synthesis process vague. ### Item 13e — Synthesis methods: heterogeneity exploration **Look for:** subgroup analysis, meta-regression, moderator analysis, planned exploration methods. **Common misses:** subgroup results appear with no prior methods description. ### Item 13f — Synthesis methods: sensitivity analysis **Look for:** robustness checks and how they were done. **Common misses:** sensitivity analyses claimed but not pre-described or insufficiently described. ### Item 14 — Reporting bias assessment **Look for:** methods used to assess bias from missing results/reporting biases. **Common misses:** publication bias funnel plot appears with no methods text; omitted entirely. ### Item 15 — Certainty assessment **Look for:** GRADE or other certainty/confidence framework and how applied. **Common misses:** certainty statements made without method. ## Results ### Item 16a — Study selection **Look for:** records identified, screened, excluded, included; ideally a flow diagram. **Common misses:** final included count only; no screening flow. ### Item 16b — Excluded studies that seemed eligible **Look for:** citations and reasons for exclusion of near-miss studies. **Common misses:** no excluded-studies table or rationale. ### Item 17 — Study characteristics **Look for:** citation of each included study and presentation of key characteristics. **Common misses:** study list present but characteristics incomplete. ### Item 18 — Risk of bias in studies **Look for:** risk-of-bias assessments for each included study. **Common misses:** only overall statement; no study-level results. ### Item 19 — Results of individual studies **Look for:** per-study summary statistics and effect estimate with precision for all outcomes, ideally tables/plots. **Common misses:** only pooled result given; no study-level outcome data. ### Item 20a — Results of syntheses: characteristics and bias of contributing studies **Look for:** brief summary of characteristics and risk of bias among studies in each synthesis. **Common misses:** pooled result presented without describing contributing evidence base. ### Item 20b — Results of statistical syntheses **Look for:** summary estimate, precision, heterogeneity, direction of effect. **Common misses:** no confidence interval; no heterogeneity; unclear favored group. ### Item 20c — Results of heterogeneity investigations **Look for:** subgroup/meta-regression/other exploration results. **Common misses:** analyses preplanned in Methods but absent or unexplained in Results. ### Item 20d — Results of sensitivity analyses **Look for:** robustness findings. **Common misses:** methods mention sensitivity analysis but no results reported. ### Item 21 — Reporting biases **Look for:** results of reporting-bias assessment for each synthesis assessed. **Common misses:** methods described, results not shown. ### Item 22 — Certainty of evidence **Look for:** certainty/confidence judgments for each outcome. **Common misses:** vague certainty language with no outcome-level assessment. ## Discussion ### Item 23a — General interpretation **Look for:** interpretation in context of other evidence. **Common misses:** repeats results without contextualization. ### Item 23b — Limitations of the evidence **Look for:** study-level/evidence-body limitations such as bias, inconsistency, imprecision. **Common misses:** only review-process limitations discussed. ### Item 23c — Limitations of the review processes **Look for:** limitations in search, screening, extraction, synthesis decisions, etc. **Common misses:** authors discuss evidence limitations but not their own process limitations. ### Item 23d — Implications **Look for:** implications for practice, policy, and future research. **Common misses:** generic "more research is needed" statement only. ## Other information ### Item 24a — Registration **Look for:** register name and registration number, or explicit statement that not registered. **Common misses:** registry mentioned without number. ### Item 24b — Protocol access **Look for:** where protocol can be accessed, or explicit statement none was prepared. **Common misses:** protocol implied but not locatable. ### Item 24c — Amendments **Look for:** amendments to registered/protocol information and reasons. **Common misses:** deviations apparent but not acknowledged. ### Item 25 — Support **Look for:** financial/non-financial support and role of funders/sponsors. **Common misses:** funding source named but role not stated. ### Item 26 — Competing interests **Look for:** declaration of competing interests. **Common misses:** absent conflict statement. ### Item 27 — Availability of data, code, and other materials **Look for:** availability and location of forms, extracted data, analysis data, code, and other materials. **Common misses:** no data/code statement; vague "available on request" with no specifics. ## Suggested audit table schema Use this structure when the user asks for an audit or extractor output: | Item | Requirement summary | Evidence from manuscript | Status | Action needed | |---|---|---|---|---| | 1 | Identify as systematic review in title | "...systematic review..." | Met | — | Status values: - Met - Partly met - Not met - Not assessable - Not applicable ## High-priority item cluster for rapid triage If you cannot review everything, start here: - 4 Objectives - 5 Eligibility criteria - 6 Information sources - 7 Search strategy - 8 Selection process - 9 Data collection process - 11 Risk of bias assessment - 13d Synthesis methods - 16a Study selection - 20b Results of syntheses - 24a-c Registration and protocol - 25-27 trust and transparency items
Build music recommendations and create Spotify playlists using Last.fm similarity and listening history.
---
name: lastfm-spotify-playlists
description: Build music recommendations and create Spotify playlists using Last.fm similarity and listening history.
---
# Execution Model
This skill runs **directly via local Python scripts**.
Valid entrypoints:
- `python run_pipeline.py ...`
- `python auth.py`
## Absolute rules
- Do NOT use ACP
- Do NOT request or require an agentId
- Do NOT propose manual workflows
- Do NOT suggest alternative scripts or paths
- Do NOT reference any `scripts/` directory
- Do NOT use `python -m` module execution
- Do NOT assume the package must be installed
All commands must be executed from the root of this skill folder.
---
# Critical Behavior Rules
If the user asks for:
- creating a playlist
- adding songs to Spotify
- saving results to Spotify
You MUST execute:
```bash
--output-mode spotify --create-playlist
Primary entrypoints:
- `python run_pipeline.py ...`
- `python auth.py ...`
This skill is intentionally organized as plain scripts plus helper modules:
- `run_pipeline.py`
- `auth.py`
- `lastfm.py`
- `spotify.py`
- `pipeline.py`
- `common.py`
No package installation is required.
# Purpose
This skill:
- uses Last.fm as the discovery engine
- expands seeds through `track.getsimilar`
- ranks merged candidates
- optionally matches results on Spotify
- optionally creates Spotify playlists
# Requirements
## Python
A normal local Python interpreter must be available.
## Last.fm credentials
Supported sources:
- environment variables:
- `LASTFM_API_KEY`
- `LASTFM_SHARED_SECRET`
- `LASTFM_USERNAME`
- credentials file:
- `~/.openclaw/lastfm-credentials.json`
- explicit file path via command flag:
- `--creds <path>`
Example file:
```json
{
"api_key": "YOUR_LASTFM_API_KEY",
"shared_secret": "YOUR_LASTFM_SHARED_SECRET",
"username": "YOUR_LASTFM_USERNAME"
}
```
## Spotify credentials
Needed only for Spotify matching or playlist creation.
Supported sources:
- environment variables:
- `SPOTIFY_CLIENT_ID`
- `SPOTIFY_CLIENT_SECRET`
- `SPOTIFY_REDIRECT_URI`
- credentials file:
- `~/.openclaw/spotify-credentials.json`
- explicit file path via command flag:
- `--spotify-creds <path>`
Saved token location:
- `~/.openclaw/spotify-token.json`
- or explicit path via `--spotify-token <path>`
# Command Selection
## 1. Recommend from recent Last.fm listening
Use when the request is based on a user's recent scrobbles.
```bash
python run_pipeline.py recent-tracks --user "<LASTFM_USER>" --recent-count 10 --similar-per-seed 5 --final-limit 20 --output-mode lastfm-only
```
## 2. Recommend from a seed artist
Use when the request is based on one artist.
```bash
python run_pipeline.py artist-rule-c "<ARTIST_NAME>" --seed-count 5 --similar-per-seed 10 --final-limit 20 --output-mode lastfm-only
```
## 3. Recommend from top artists
Use when the request is based on a user's broader taste profile.
```bash
python run_pipeline.py top-artists-blend --user "<LASTFM_USER>" --period 1month --artist-count 5 --seed-count-per-artist 3 --similar-per-seed 5 --final-limit 20 --output-mode lastfm-only
```
## 4. Match recommendations to Spotify
Use when the user wants playable Spotify results but not necessarily a playlist.
```bash
python run_pipeline.py recent-tracks --user "<LASTFM_USER>" --recent-count 10 --final-limit 20 --output-mode spotify
```
## 5. Create Spotify playlist
Use when the user explicitly wants a playlist created.
```bash
python run_pipeline.py recent-tracks --user "<LASTFM_USER>" --recent-count 10 --final-limit 20 --output-mode spotify --create-playlist --playlist-name "Last.fm Recommendations"
```
## 6. Run Spotify auth
Use when Spotify token setup is required.
```bash
python auth.py
```
Optional explicit paths:
```bash
python auth.py --spotify-creds "<PATH_TO_SPOTIFY_CREDS_JSON>" --spotify-token "<PATH_TO_SPOTIFY_TOKEN_JSON>"
```
# Behavior Rules
- Prefer Last.fm for recommendation discovery
- Use Spotify only for:
- search
- playlist creation
- playlist population
- If the user only wants suggestions, use `--output-mode lastfm-only`
- If the user wants Spotify results, use `--output-mode spotify`
- If the user wants a playlist created, add `--create-playlist`
- Never invent missing credentials
- Never fall back to ACP or agent execution
# Output Expectations
The scripts print JSON to stdout.
Return the JSON result directly or summarize it faithfully.
Typical fields include:
- `mode`
- `user`
- `seed_artist`
- `seed_tracks`
- `suggestions`
- `matched_tracks`
- `unmatched_tracks`
- `playlist`
# Error Handling
If the script exits with an error:
- surface stderr or the raised error message directly
- do not retry through ACP
- do not ask for an agentId
- do not claim the skill is unavailable because it is not a package
Common expected failures:
- missing Last.fm API key
- missing Last.fm username
- missing Spotify credentials
- missing Spotify token
- expired Spotify token without refresh token
# Notes
This skill is intentionally script-based for reliability.
It should work as long as:
- the skill folder is present
- Python is present
- credentials are configured
- commands are executed from the skill folder root
It must not depend on package installation, editable installs, or import path manipulation.
FILE:auth.py
from __future__ import annotations
import argparse
from common import dump_json
from spotify import authorize, get_access_token, request_json
def main() -> None:
parser = argparse.ArgumentParser(description="Run Spotify OAuth and save a reusable token.")
parser.add_argument(
"--scope",
action="append",
dest="scopes",
default=None,
help="Spotify scope. Repeat as needed. If omitted, a sensible default is used.",
)
parser.add_argument("--spotify-creds", help="Path to Spotify credentials JSON file.")
parser.add_argument("--spotify-token", help="Path to Spotify token JSON file.")
parser.add_argument("--no-browser", action="store_true", help="Print auth URL instead of opening browser.")
args = parser.parse_args()
scopes = args.scopes or [
"playlist-modify-private",
"playlist-modify-public",
]
token = authorize(
scopes,
creds_path=args.spotify_creds,
token_path=args.spotify_token,
open_browser=not args.no_browser,
)
access_token = get_access_token(creds_path=args.spotify_creds, token_path=args.spotify_token)
me = request_json("https://api.spotify.com/v1/me", token=access_token)
dump_json({
"saved": True,
"scope": token.get("scope"),
"access_token_present": bool(token.get("access_token")),
"refresh_token_present": bool(token.get("refresh_token")),
"user_id": me.get("id"),
"display_name": me.get("display_name"),
})
if __name__ == "__main__":
main()
FILE:common.py
from __future__ import annotations
import json
import sys
from typing import Any
def dump_json(data: Any) -> None:
json.dump(data, sys.stdout, ensure_ascii=False, indent=2)
sys.stdout.write("\n")
FILE:lastfm.py
from __future__ import annotations
import json
import os
import urllib.parse
import urllib.request
from pathlib import Path
from typing import Any
API_ROOT = "https://ws.audioscrobbler.com/2.0/"
DEFAULT_CREDS_PATH = Path.home() / ".openclaw" / "lastfm-credentials.json"
class LastFMError(RuntimeError):
pass
def ensure_list(value: Any) -> list[Any]:
if value is None:
return []
if isinstance(value, list):
return value
return [value]
class LastFMClient:
def __init__(self, *, explicit_path: str | None = None, api_key: str | None = None) -> None:
self._api_key = api_key or self.load_credentials(explicit_path).get("api_key")
if not self._api_key:
raise LastFMError(
"Missing Last.fm API key. Set LASTFM_API_KEY or create ~/.openclaw/lastfm-credentials.json"
)
@staticmethod
def load_credentials(explicit_path: str | None = None) -> dict[str, str]:
creds: dict[str, str] = {}
env_key = os.getenv("LASTFM_API_KEY")
env_secret = os.getenv("LASTFM_SHARED_SECRET")
env_user = os.getenv("LASTFM_USERNAME")
if env_key:
creds["api_key"] = env_key
if env_secret:
creds["shared_secret"] = env_secret
if env_user:
creds["username"] = env_user
path = Path(explicit_path) if explicit_path else DEFAULT_CREDS_PATH
if path.exists():
file_creds = json.loads(path.read_text(encoding="utf-8"))
for key in ("api_key", "shared_secret", "username"):
if file_creds.get(key) and key not in creds:
creds[key] = str(file_creds[key])
return creds
def get(self, method: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
query: dict[str, Any] = {
"method": method,
"api_key": self._api_key,
"format": "json",
}
if params:
for key, value in params.items():
if value is not None:
query[key] = value
url = API_ROOT + "?" + urllib.parse.urlencode(query)
with urllib.request.urlopen(url) as response:
payload = response.read().decode("utf-8")
data = json.loads(payload)
if "error" in data:
raise LastFMError(f"Last.fm error {data['error']}: {data.get('message', 'Unknown error')}")
return data
def normalize_recent_track(item: dict[str, Any]) -> dict[str, Any]:
return {
"track": item.get("name"),
"artist": item.get("artist", {}).get("#text") if isinstance(item.get("artist"), dict) else item.get("artist"),
"album": item.get("album", {}).get("#text") if isinstance(item.get("album"), dict) else item.get("album"),
"nowplaying": item.get("@attr", {}).get("nowplaying") == "true",
"played_at": item.get("date", {}).get("#text"),
"uts": item.get("date", {}).get("uts"),
"url": item.get("url"),
"loved": item.get("loved") == "1",
}
def normalize_top_artist(item: dict[str, Any]) -> dict[str, Any]:
return {
"artist": item.get("name"),
"playcount": int(item.get("playcount", 0) or 0),
"listeners": int(item.get("listeners", 0) or 0),
"mbid": item.get("mbid") or None,
"url": item.get("url"),
}
def normalize_top_track(item: dict[str, Any]) -> dict[str, Any]:
artist = item.get("artist", {}) if isinstance(item.get("artist"), dict) else {}
return {
"track": item.get("name"),
"artist": artist.get("name") or artist.get("#text"),
"playcount": int(item.get("playcount", 0) or 0),
"listeners": int(item.get("listeners", 0) or 0),
"mbid": item.get("mbid") or None,
"url": item.get("url"),
}
def normalize_similar_artist(item: dict[str, Any]) -> dict[str, Any]:
return {
"artist": item.get("name"),
"match": float(item.get("match", 0.0) or 0.0),
"mbid": item.get("mbid") or None,
"url": item.get("url"),
}
def normalize_similar_track(item: dict[str, Any]) -> dict[str, Any]:
artist = item.get("artist", {}) if isinstance(item.get("artist"), dict) else {}
return {
"track": item.get("name"),
"artist": artist.get("name") or artist.get("#text"),
"match": float(item.get("match", 0.0) or 0.0),
"mbid": item.get("mbid") or None,
"url": item.get("url"),
}
def normalize_seed_track(item: dict[str, Any]) -> dict[str, Any]:
artist = item.get("artist", {}) if isinstance(item.get("artist"), dict) else {}
return {
"track": item.get("name"),
"artist": artist.get("name") or artist.get("#text"),
"playcount": int(item.get("playcount", 0) or 0),
"listeners": int(item.get("listeners", 0) or 0),
"url": item.get("url"),
}
def get_recent_tracks(client: LastFMClient, *, user: str, limit: int, page: int = 1) -> list[dict]:
data = client.get(
"user.getrecenttracks",
{"user": user, "limit": limit, "page": page, "extended": 0},
)
tracks = ensure_list(data.get("recenttracks", {}).get("track"))
return [normalize_recent_track(item) for item in tracks]
def get_top_artists(client: LastFMClient, *, user: str, period: str, limit: int, page: int = 1) -> list[dict]:
data = client.get(
"user.gettopartists",
{"user": user, "period": period, "limit": limit, "page": page},
)
artists = ensure_list(data.get("topartists", {}).get("artist"))
return [normalize_top_artist(item) for item in artists]
def get_top_tracks(client: LastFMClient, *, user: str, period: str, limit: int, page: int = 1) -> list[dict]:
data = client.get(
"user.gettoptracks",
{"user": user, "period": period, "limit": limit, "page": page},
)
tracks = ensure_list(data.get("toptracks", {}).get("track"))
return [normalize_top_track(item) for item in tracks]
def get_artist_top_tracks(client: LastFMClient, *, artist: str, limit: int, autocorrect: int = 1) -> list[dict]:
data = client.get(
"artist.gettoptracks",
{"artist": artist, "limit": limit, "autocorrect": autocorrect},
)
tracks = ensure_list(data.get("toptracks", {}).get("track"))
return [normalize_seed_track(item) for item in tracks]
def get_similar_artists(client: LastFMClient, *, artist: str, limit: int, autocorrect: int = 1) -> list[dict]:
data = client.get(
"artist.getsimilar",
{"artist": artist, "limit": limit, "autocorrect": autocorrect},
)
artists = ensure_list(data.get("similarartists", {}).get("artist"))
return [normalize_similar_artist(item) for item in artists]
def get_similar_tracks(client: LastFMClient, *, artist: str, track: str, limit: int, autocorrect: int = 1) -> list[dict]:
data = client.get(
"track.getsimilar",
{"artist": artist, "track": track, "limit": limit, "autocorrect": autocorrect},
)
tracks = ensure_list(data.get("similartracks", {}).get("track"))
return [normalize_similar_track(item) for item in tracks]
FILE:lastfm_credentials.example.json
{
"api_key": "YOUR_LASTFM_API_KEY",
"shared_secret": "YOUR_LASTFM_SHARED_SECRET",
"username": "YOUR_LASTFM_USERNAME"
}
FILE:pipeline.py
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from typing import Any
from lastfm import LastFMClient, get_artist_top_tracks, get_recent_tracks, get_similar_tracks, get_top_artists
from spotify import add_tracks_to_playlist, create_playlist, get_access_token, search_tracks
def normalized_key(artist: str, track: str) -> tuple[str, str]:
return artist.strip().casefold(), track.strip().casefold()
@dataclass(slots=True)
class TrackSeed:
artist: str
track: str
url: str | None = None
playcount: int = 0
listeners: int = 0
origin_artist: str | None = None
@dataclass(slots=True)
class CandidateSource:
seed: str
match: float
@dataclass(slots=True)
class RankedCandidate:
artist: str
track: str
score: float = 0.0
source_count: int = 0
sources: list[CandidateSource] = field(default_factory=list)
url: str | None = None
def to_dict(self) -> dict[str, Any]:
return {
"artist": self.artist,
"track": self.track,
"score": self.score,
"source_count": self.source_count,
"sources": [{"seed": source.seed, "match": source.match} for source in self.sources],
"url": self.url,
}
def merge_candidate(
merged: dict[tuple[str, str], RankedCandidate],
*,
key: tuple[str, str],
artist: str,
track: str,
match: float,
seed_label: str,
root_artist: str | None = None,
url: str | None = None,
) -> None:
if not artist or not track:
return
if root_artist and artist.casefold() == root_artist.casefold():
return
if key not in merged:
merged[key] = RankedCandidate(artist=artist, track=track, url=url)
candidate = merged[key]
candidate.score += float(match or 0.0)
candidate.source_count += 1
candidate.sources.append(CandidateSource(seed=seed_label, match=float(match or 0.0)))
if not candidate.url and url:
candidate.url = url
def rank_candidates(candidates: dict[tuple[str, str], RankedCandidate]) -> list[RankedCandidate]:
return sorted(candidates.values(), key=lambda item: (item.source_count, item.score), reverse=True)
def artist_top_track_seeds(
client: LastFMClient,
*,
artist: str,
seed_count: int,
autocorrect: int = 1,
) -> list[TrackSeed]:
items = get_artist_top_tracks(client, artist=artist, limit=seed_count, autocorrect=autocorrect)
return [
TrackSeed(
artist=item.get("artist") or artist,
track=item.get("track") or "",
url=item.get("url"),
playcount=item.get("playcount", 0),
listeners=item.get("listeners", 0),
origin_artist=artist,
)
for item in items
if item.get("track")
]
def recent_track_seeds(client: LastFMClient, *, user: str, limit: int) -> list[TrackSeed]:
items = get_recent_tracks(client, user=user, limit=limit, page=1)
seeds: list[TrackSeed] = []
seen: set[tuple[str, str]] = set()
for item in items:
artist = item.get("artist") or ""
track = item.get("track") or ""
if not artist or not track:
continue
key = normalized_key(artist, track)
if key in seen:
continue
seen.add(key)
seeds.append(TrackSeed(artist=artist, track=track, url=item.get("url")))
return seeds
def top_artist_names(client: LastFMClient, *, user: str, period: str, limit: int) -> list[str]:
items = get_top_artists(client, user=user, period=period, limit=limit, page=1)
return [item["artist"] for item in items if item.get("artist")]
def _build_from_seeds(
client: LastFMClient,
*,
seeds: list[TrackSeed],
similar_per_seed: int,
autocorrect: int = 1,
root_artist: str | None = None,
) -> list[RankedCandidate]:
merged: dict[tuple[str, str], RankedCandidate] = {}
for seed in seeds:
similar_tracks = get_similar_tracks(
client,
artist=seed.artist,
track=seed.track,
limit=similar_per_seed,
autocorrect=autocorrect,
)
for item in similar_tracks:
artist = item.get("artist") or ""
track = item.get("track") or ""
merge_candidate(
merged,
key=normalized_key(artist, track),
artist=artist,
track=track,
match=float(item.get("match", 0.0) or 0.0),
seed_label=f"{seed.artist} - {seed.track}",
root_artist=root_artist or seed.artist,
url=item.get("url"),
)
return rank_candidates(merged)
def build_from_artist_top_tracks(
client: LastFMClient,
*,
artist: str,
seed_count: int,
similar_per_seed: int,
autocorrect: int = 1,
) -> tuple[list[TrackSeed], list[RankedCandidate]]:
seeds = artist_top_track_seeds(client, artist=artist, seed_count=seed_count, autocorrect=autocorrect)
ranked = _build_from_seeds(
client,
seeds=seeds,
similar_per_seed=similar_per_seed,
autocorrect=autocorrect,
root_artist=artist,
)
return seeds, ranked
def build_from_recent_tracks(
client: LastFMClient,
*,
user: str,
recent_count: int,
similar_per_seed: int,
autocorrect: int = 1,
) -> tuple[list[TrackSeed], list[RankedCandidate]]:
seeds = recent_track_seeds(client, user=user, limit=recent_count)
ranked = _build_from_seeds(
client,
seeds=seeds,
similar_per_seed=similar_per_seed,
autocorrect=autocorrect,
root_artist=None,
)
return seeds, ranked
def build_from_track_seed(
client: LastFMClient,
*,
artist: str,
track: str,
similar_per_seed: int,
autocorrect: int = 1,
) -> tuple[list[TrackSeed], list[RankedCandidate]]:
seeds = [TrackSeed(artist=artist, track=track, origin_artist=artist)]
ranked = _build_from_seeds(
client,
seeds=seeds,
similar_per_seed=similar_per_seed,
autocorrect=autocorrect,
root_artist=artist,
)
return seeds, ranked
def build_from_top_artists(
client: LastFMClient,
*,
user: str,
period: str,
artist_count: int,
seed_count_per_artist: int,
similar_per_seed: int,
autocorrect: int = 1,
) -> tuple[list[TrackSeed], list[RankedCandidate]]:
artist_names = top_artist_names(client, user=user, period=period, limit=artist_count)
all_seeds: list[TrackSeed] = []
merged: dict[tuple[str, str], RankedCandidate] = {}
for artist in artist_names:
artist_seeds, artist_ranked = build_from_artist_top_tracks(
client,
artist=artist,
seed_count=seed_count_per_artist,
similar_per_seed=similar_per_seed,
autocorrect=autocorrect,
)
all_seeds.extend(artist_seeds)
for candidate in artist_ranked:
key = normalized_key(candidate.artist, candidate.track)
if key not in merged:
merged[key] = RankedCandidate(
artist=candidate.artist,
track=candidate.track,
score=candidate.score,
source_count=candidate.source_count,
sources=list(candidate.sources),
url=candidate.url,
)
continue
existing = merged[key]
existing.score += candidate.score
existing.source_count += candidate.source_count
existing.sources.extend(candidate.sources)
if not existing.url and candidate.url:
existing.url = candidate.url
return all_seeds, rank_candidates(merged)
def match_candidates_on_spotify(
candidates: list[RankedCandidate],
*,
access_token: str,
final_limit: int,
market: str | None = None,
) -> tuple[list[dict], list[str], list[dict]]:
matched: list[dict] = []
unmatched: list[dict] = []
uris: list[str] = []
for candidate in candidates:
if len(uris) >= final_limit:
break
query = f'track:"{candidate.track}" artist:"{candidate.artist}"'
result = search_tracks(query, access_token=access_token, limit=5, market=market)
items = result.get("tracks", {}).get("items", [])
pick = None
for item in items:
item_name = (item.get("name") or "").strip().casefold()
artist_names = [(a.get("name") or "").strip().casefold() for a in item.get("artists", [])]
if item_name == candidate.track.strip().casefold() and candidate.artist.strip().casefold() in artist_names:
pick = item
break
if not pick and items:
pick = items[0]
if not pick:
unmatched.append({
"artist": candidate.artist,
"track": candidate.track,
"score": candidate.score,
})
continue
if pick["uri"] in uris:
continue
uris.append(pick["uri"])
matched.append({
"artist": candidate.artist,
"track": candidate.track,
"score": candidate.score,
"spotify_name": pick.get("name"),
"spotify_artists": ", ".join(a.get("name") for a in pick.get("artists", [])),
"uri": pick.get("uri"),
"url": pick.get("external_urls", {}).get("spotify"),
})
return matched, uris, unmatched
def maybe_create_playlist(
name: str,
description: str,
uris: list[str],
*,
creds_path: str | None = None,
token_path: str | None = None,
public: bool = False,
) -> dict[str, Any]:
token = get_access_token(creds_path=creds_path, token_path=token_path)
playlist = create_playlist(name, access_token=token, public=public, description=description)
add_tracks_to_playlist(playlist["id"], uris, access_token=token)
return {
"id": playlist.get("id"),
"name": playlist.get("name"),
"url": playlist.get("external_urls", {}).get("spotify"),
}
def build_pipeline_output(
*,
mode: str,
lastfm_creds_path: str | None,
spotify_creds_path: str | None,
spotify_token_path: str | None,
user: str | None,
artist: str | None,
track: str | None,
period: str,
final_limit: int,
market: str | None,
autocorrect: int,
output_mode: str,
create_playlist_flag: bool,
playlist_name: str | None,
playlist_description: str | None,
seed_count: int = 5,
similar_per_seed: int = 10,
recent_count: int = 10,
artist_count: int = 5,
seed_count_per_artist: int = 3,
playlist_public: bool = False,
) -> dict[str, Any]:
if output_mode == "lastfm-only" and create_playlist_flag:
raise SystemExit("--create-playlist cannot be used with --output-mode lastfm-only.")
lastfm_client = LastFMClient(explicit_path=lastfm_creds_path)
creds = lastfm_client.load_credentials(lastfm_creds_path)
username = user or creds.get("username")
if mode == "artist-rule-c":
if not artist:
raise SystemExit("artist is required for artist-rule-c")
seeds, ranked = build_from_artist_top_tracks(
lastfm_client,
artist=artist,
seed_count=seed_count,
similar_per_seed=similar_per_seed,
autocorrect=autocorrect,
)
summary = {
"mode": mode,
"seed_artist": artist,
"seed_tracks": [asdict(seed) for seed in seeds],
}
default_name = f"Last.fm Rule C - {artist}"
default_description = f"Built from {artist} top tracks expanded through Last.fm similar tracks."
elif mode == "seed-track":
if not artist or not track:
raise SystemExit("artist and track are required for seed-track")
seeds, ranked = build_from_track_seed(
lastfm_client,
artist=artist,
track=track,
similar_per_seed=similar_per_seed,
autocorrect=autocorrect,
)
summary = {
"mode": mode,
"seed_artist": artist,
"seed_track": track,
"seed_tracks": [asdict(seed) for seed in seeds],
}
default_name = f"Beep Beep - {artist} - {track}"
default_description = f"Built from the Last.fm similar tracks graph for {artist} - {track}."
elif mode == "recent-tracks":
if not username:
raise SystemExit("Missing Last.fm username. Pass --user or set LASTFM_USERNAME / credentials file username.")
seeds, ranked = build_from_recent_tracks(
lastfm_client,
user=username,
recent_count=recent_count,
similar_per_seed=similar_per_seed,
autocorrect=autocorrect,
)
summary = {
"mode": mode,
"user": username,
"seed_tracks": [asdict(seed) for seed in seeds],
}
default_name = f"Last.fm Recent Blend - {username}"
default_description = "Built from recent Last.fm scrobbles expanded through Last.fm similar tracks."
elif mode == "top-artists-blend":
if not username:
raise SystemExit("Missing Last.fm username. Pass --user or set LASTFM_USERNAME / credentials file username.")
seeds, ranked = build_from_top_artists(
lastfm_client,
user=username,
period=period,
artist_count=artist_count,
seed_count_per_artist=seed_count_per_artist,
similar_per_seed=similar_per_seed,
autocorrect=autocorrect,
)
summary = {
"mode": mode,
"user": username,
"period": period,
"seed_tracks": [asdict(seed) for seed in seeds[:50]],
}
default_name = f"Last.fm Top Artists Blend - {username} - {period}"
default_description = f"Built from top Last.fm artists for {period}, expanded through similar tracks."
else:
raise SystemExit(f"Unsupported mode: {mode}")
candidates = ranked[: max(final_limit * 3, final_limit)]
if output_mode == "lastfm-only":
return {
**summary,
"output_mode": output_mode,
"candidate_count_considered": len(candidates),
"suggestion_count": min(len(ranked), final_limit),
"suggestions": [candidate.to_dict() for candidate in ranked[:final_limit]],
}
access_token = get_access_token(creds_path=spotify_creds_path, token_path=spotify_token_path)
matched, uris, unmatched = match_candidates_on_spotify(
candidates,
access_token=access_token,
final_limit=final_limit,
market=market,
)
output: dict[str, Any] = {
**summary,
"output_mode": output_mode,
"candidate_count_considered": len(candidates),
"matched_count": len(matched),
"matched_tracks": matched,
"unmatched_tracks": unmatched,
}
if create_playlist_flag:
output["playlist"] = maybe_create_playlist(
playlist_name or default_name,
playlist_description or default_description,
uris,
creds_path=spotify_creds_path,
token_path=spotify_token_path,
public=playlist_public,
)
return output
FILE:README.md
# lastfm-spotify-playlists (simple script layout)
This version is intentionally blunt and self-contained.
It does **not** require:
- `pip install -e .`
- a Python package layout
- `PYTHONPATH` hacks
- ACP agents
## Main commands
Recommend from recent Last.fm listening:
```bash
python run_pipeline.py recent-tracks --user "YOUR_LASTFM_USERNAME" --output-mode lastfm-only
```
Recommend from a seed artist:
```bash
python run_pipeline.py artist-rule-c "Massive Attack" --output-mode lastfm-only
```
Create a Spotify playlist:
```bash
python run_pipeline.py recent-tracks --user "YOUR_LASTFM_USERNAME" --create-playlist
```
Run Spotify OAuth first:
```bash
python auth.py
```
FILE:requirements.txt
# Standard-library only implementation.
# No third-party packages required.
FILE:run_pipeline.py
from __future__ import annotations
import argparse
from common import dump_json
from pipeline import build_pipeline_output
def add_common_arguments(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--final-limit", type=int, default=20)
parser.add_argument("--market")
parser.add_argument("--autocorrect", type=int, choices=[0, 1], default=1)
parser.add_argument("--output-mode", choices=["spotify", "lastfm-only"], default="spotify")
parser.add_argument("--create-playlist", action="store_true")
parser.add_argument("--playlist-name")
parser.add_argument("--playlist-description")
parser.add_argument("--playlist-public", action="store_true")
parser.add_argument("--creds", help="Path to Last.fm credentials JSON file.")
parser.add_argument("--spotify-creds", help="Path to Spotify credentials JSON file.")
parser.add_argument("--spotify-token", help="Path to Spotify token JSON file.")
def main() -> None:
parser = argparse.ArgumentParser(description="Build Last.fm-driven Spotify playlists in one command.")
subparsers = parser.add_subparsers(dest="mode", required=True)
artist_parser = subparsers.add_parser("artist-rule-c", help="Use one artist's top tracks as Last.fm similar-track seeds.")
artist_parser.add_argument("artist")
artist_parser.add_argument("--seed-count", type=int, default=5)
artist_parser.add_argument("--similar-per-seed", type=int, default=10)
add_common_arguments(artist_parser)
seed_track_parser = subparsers.add_parser("seed-track", help="Use one specific artist/track pair as the Last.fm similar-track seed.")
seed_track_parser.add_argument("--artist", required=True)
seed_track_parser.add_argument("--track", required=True)
seed_track_parser.add_argument("--similar-per-seed", type=int, default=20)
add_common_arguments(seed_track_parser)
recent_parser = subparsers.add_parser("recent-tracks", help="Use recent scrobbles as track-level Last.fm seeds.")
recent_parser.add_argument("--user")
recent_parser.add_argument("--recent-count", type=int, default=10)
recent_parser.add_argument("--similar-per-seed", type=int, default=5)
add_common_arguments(recent_parser)
top_parser = subparsers.add_parser("top-artists-blend", help="Use the user's top artists for a period, then blend Rule C candidates across them.")
top_parser.add_argument("--user")
top_parser.add_argument("--period", default="1month", choices=["7day", "1month", "3month", "6month", "12month", "overall"])
top_parser.add_argument("--artist-count", type=int, default=5)
top_parser.add_argument("--seed-count-per-artist", type=int, default=3)
top_parser.add_argument("--similar-per-seed", type=int, default=5)
add_common_arguments(top_parser)
args = parser.parse_args()
output = build_pipeline_output(
mode=args.mode,
lastfm_creds_path=args.creds,
spotify_creds_path=args.spotify_creds,
spotify_token_path=args.spotify_token,
user=getattr(args, "user", None),
artist=getattr(args, "artist", None),
track=getattr(args, "track", None),
period=getattr(args, "period", "1month"),
final_limit=args.final_limit,
market=args.market,
autocorrect=args.autocorrect,
output_mode=args.output_mode,
create_playlist_flag=args.create_playlist,
playlist_name=args.playlist_name,
playlist_description=args.playlist_description,
seed_count=getattr(args, "seed_count", 5),
similar_per_seed=getattr(args, "similar_per_seed", 10),
recent_count=getattr(args, "recent_count", 10),
artist_count=getattr(args, "artist_count", 5),
seed_count_per_artist=getattr(args, "seed_count_per_artist", 3),
playlist_public=args.playlist_public,
)
dump_json(output)
if __name__ == "__main__":
main()
FILE:spotify.py
from __future__ import annotations
import base64
import json
import os
import time
import urllib.parse
import urllib.request
import webbrowser
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
from typing import Any
API_ROOT = "https://api.spotify.com/v1"
AUTH_URL = "https://accounts.spotify.com/authorize"
TOKEN_URL = "https://accounts.spotify.com/api/token"
DEFAULT_CREDS_PATH = Path.home() / ".openclaw" / "spotify-credentials.json"
DEFAULT_TOKEN_PATH = Path.home() / ".openclaw" / "spotify-token.json"
class SpotifyError(RuntimeError):
pass
def _read_json(path: Path) -> dict[str, Any]:
return json.loads(path.read_text(encoding="utf-8"))
def load_credentials(explicit_path: str | None = None) -> dict[str, str]:
creds: dict[str, str] = {}
if os.getenv("SPOTIFY_CLIENT_ID"):
creds["client_id"] = os.environ["SPOTIFY_CLIENT_ID"]
if os.getenv("SPOTIFY_CLIENT_SECRET"):
creds["client_secret"] = os.environ["SPOTIFY_CLIENT_SECRET"]
if os.getenv("SPOTIFY_REDIRECT_URI"):
creds["redirect_uri"] = os.environ["SPOTIFY_REDIRECT_URI"]
path = Path(explicit_path) if explicit_path else DEFAULT_CREDS_PATH
if path.exists():
file_creds = _read_json(path)
for key in ("client_id", "client_secret", "redirect_uri"):
if key not in creds and file_creds.get(key):
creds[key] = str(file_creds[key])
if "client_id" not in creds or "client_secret" not in creds or "redirect_uri" not in creds:
raise SpotifyError(
"Missing Spotify credentials. Set SPOTIFY_CLIENT_ID / SPOTIFY_CLIENT_SECRET / SPOTIFY_REDIRECT_URI "
"or create ~/.openclaw/spotify-credentials.json"
)
return creds
def _basic_auth_header(client_id: str, client_secret: str) -> str:
raw = f"{client_id}:{client_secret}".encode("utf-8")
return "Basic " + base64.b64encode(raw).decode("ascii")
def _post_form(url: str, data: dict[str, Any], headers: dict[str, str] | None = None) -> dict[str, Any]:
req = urllib.request.Request(
url,
data=urllib.parse.urlencode(data).encode("utf-8"),
headers=headers or {},
method="POST",
)
try:
with urllib.request.urlopen(req) as response:
return json.loads(response.read().decode("utf-8"))
except urllib.error.HTTPError as exc:
body = exc.read().decode("utf-8", errors="replace")
raise SpotifyError(f"Spotify HTTP {exc.code}: {body}") from exc
def save_token(token: dict[str, Any], path: str | None = None) -> Path:
out = Path(path) if path else DEFAULT_TOKEN_PATH
out.parent.mkdir(parents=True, exist_ok=True)
token["saved_at"] = int(time.time())
out.write_text(json.dumps(token, indent=2), encoding="utf-8")
return out
def load_token(path: str | None = None) -> dict[str, Any]:
token_path = Path(path) if path else DEFAULT_TOKEN_PATH
if not token_path.exists():
raise SpotifyError(f"Missing token file: {token_path}")
return _read_json(token_path)
def refresh_token_if_needed(creds: dict[str, str], token: dict[str, Any], *, token_path: str | None = None) -> dict[str, Any]:
expires_in = int(token.get("expires_in", 3600) or 3600)
saved_at = int(token.get("saved_at", 0) or 0)
if token.get("access_token") and time.time() < saved_at + expires_in - 120:
return token
refresh_token = token.get("refresh_token")
if not refresh_token:
raise SpotifyError("Token is expired and no refresh_token is available.")
refreshed = _post_form(
TOKEN_URL,
{"grant_type": "refresh_token", "refresh_token": refresh_token},
headers={"Authorization": _basic_auth_header(creds["client_id"], creds["client_secret"])},
)
refreshed["refresh_token"] = refreshed.get("refresh_token") or refresh_token
save_token(refreshed, token_path)
return refreshed
def get_access_token(creds_path: str | None = None, token_path: str | None = None) -> str:
creds = load_credentials(creds_path)
token = load_token(token_path)
token = refresh_token_if_needed(creds, token, token_path=token_path)
return token["access_token"]
def authorize(
scopes: list[str],
*,
creds_path: str | None = None,
token_path: str | None = None,
open_browser: bool = True,
) -> dict[str, Any]:
creds = load_credentials(creds_path)
parsed = urllib.parse.urlparse(creds["redirect_uri"])
host = parsed.hostname or "127.0.0.1"
port = parsed.port or 8888
redirect_path = parsed.path or "/callback"
result: dict[str, str] = {}
class CallbackHandler(BaseHTTPRequestHandler):
def do_GET(self):
query = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
if urllib.parse.urlparse(self.path).path != redirect_path:
self.send_response(404)
self.end_headers()
return
if "error" in query:
result["error"] = query["error"][0]
elif "code" in query:
result["code"] = query["code"][0]
self.send_response(200)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.end_headers()
self.wfile.write(b"Spotify auth received. You can close this tab.")
def log_message(self, format: str, *args: Any) -> None:
return
server = HTTPServer((host, port), CallbackHandler)
params = {
"client_id": creds["client_id"],
"response_type": "code",
"redirect_uri": creds["redirect_uri"],
"scope": " ".join(scopes),
}
url = AUTH_URL + "?" + urllib.parse.urlencode(params)
if open_browser:
webbrowser.open(url)
else:
print(url)
server.handle_request()
server.server_close()
if result.get("error"):
raise SpotifyError(f"Spotify auth error: {result['error']}")
if not result.get("code"):
raise SpotifyError("No authorization code received.")
token = _post_form(
TOKEN_URL,
{
"grant_type": "authorization_code",
"code": result["code"],
"redirect_uri": creds["redirect_uri"],
},
headers={"Authorization": _basic_auth_header(creds["client_id"], creds["client_secret"])},
)
save_token(token, token_path)
return token
def request_json(
url: str,
*,
method: str = "GET",
token: str | None = None,
body: dict[str, Any] | None = None,
) -> dict[str, Any]:
headers = {"Content-Type": "application/json"}
if token:
headers["Authorization"] = f"Bearer {token}"
data = json.dumps(body).encode("utf-8") if body is not None else None
req = urllib.request.Request(url, data=data, headers=headers, method=method)
try:
with urllib.request.urlopen(req) as response:
payload = response.read().decode("utf-8")
return json.loads(payload) if payload else {}
except urllib.error.HTTPError as exc:
body_text = exc.read().decode("utf-8", errors="replace")
raise SpotifyError(f"Spotify API HTTP {exc.code}: {body_text}") from exc
def search_tracks(query: str, *, access_token: str, limit: int = 10, market: str | None = None) -> dict:
params = {"q": query, "type": "track", "limit": limit}
if market:
params["market"] = market
url = f"{API_ROOT}/search?" + urllib.parse.urlencode(params)
return request_json(url, token=access_token)
def current_user(access_token: str) -> dict:
return request_json(f"{API_ROOT}/me", token=access_token)
def create_playlist(
name: str,
*,
access_token: str,
public: bool = False,
description: str = "",
) -> dict:
return request_json(
f"{API_ROOT}/me/playlists",
method="POST",
token=access_token,
body={"name": name, "public": public, "description": description},
)
def add_tracks_to_playlist(playlist_id: str, track_uris: list[str], *, access_token: str) -> dict:
if not track_uris:
return {"snapshot_id": None}
return request_json(
f"{API_ROOT}/playlists/{playlist_id}/items",
method="POST",
token=access_token,
body={"uris": track_uris},
)
FILE:spotify_credentials.example.json
{
"client_id": "YOUR_SPOTIFY_CLIENT_ID",
"client_secret": "YOUR_SPOTIFY_CLIENT_SECRET",
"redirect_uri": "http://127.0.0.1:8888/callback"
}
Access Flickr with user-supplied local API credentials and OAuth tokens, verify authorization, export recent-upload or album metadata, download recent or alb...
---
name: flickr-claw
description: Access Flickr with user-supplied local API credentials and OAuth tokens, verify authorization, export recent-upload or album metadata, download recent or album images for local visual review, and edit Flickr tags, titles, and descriptions. Use when working with a user's Flickr account, checking auth status, exporting metadata, pulling photos from a specific Flickr album, downloading Flickr images locally, or writing reviewed metadata back to Flickr.
metadata:
version: "1.5.0"
tags:
- flickr
- photo
- tagging
- metadata
- oauth
- albums
---
# Flickr Claw
Use the bundled script for Flickr authentication, export, download, and metadata editing.
## Security & Privacy
- This skill keeps Flickr credentials and OAuth tokens on the local machine.
- The bundled script talks only to official Flickr OAuth and REST endpoints.
- The skill does not send Flickr credentials, tokens, album data, or photo metadata to any third-party service by itself.
- Review the bundled Python script before trusting it in your environment, especially before using write-capable tokens.
## Requirements
Have these available before using the skill:
- A Flickr API key and secret from the user's own Flickr app
- Python
- `requests-oauthlib`
- A local credentials file at `~/.openclaw/flickr-app-credentials.json`
Install the Python dependency with:
```bash
pip install requests-oauthlib
```
Credentials file format:
```json
{
"api_key": "YOUR_FLICKR_API_KEY",
"api_secret": "YOUR_FLICKR_API_SECRET"
}
```
## Quick start
Run from the workspace root:
### Cross-platform form
```bash
python skills/flickr-claw/scripts/flickr_skill.py --check-auth
python skills/flickr-claw/scripts/flickr_skill.py --list-albums
python skills/flickr-claw/scripts/flickr_skill.py --album-photos --album-id ALBUM_ID --out ./flickr_album_photos.csv
```
### Windows PowerShell form
```powershell
python .\skills\flickr-claw\scripts\flickr_skill.py --check-auth
python .\skills\flickr-claw\scripts\flickr_skill.py --list-albums
python .\skills\flickr-claw\scripts\flickr_skill.py --album-photos --album-id ALBUM_ID --out .\flickr_album_photos.csv
```
If `--check-auth` fails because credentials or tokens are missing, use the authorization flow in `references/workflow.md`.
## Workflow
1. Confirm credentials exist at `~/.openclaw/flickr-app-credentials.json`.
2. Run `--check-auth` before larger operations.
3. Use `--list-albums` to find the album/photoset ID you want.
4. Use `--album-photos` or `--download-album` when you want to work from a specific album instead of recent uploads.
5. Use a read token for export-only work.
6. Use a write token for tags, titles, or descriptions.
7. Prefer `--add-tags` over `--set-tags` unless full replacement is intended.
8. Use `--download-latest` or `--download-album` before real image review.
9. Delete local downloaded image copies after tagging/review unless the user explicitly wants to keep them.
10. Expect occasional Flickr UI lag after writes; verify through API or refresh later if needed.
## Metadata guidance
- Preserve useful existing location and event tags unless cleanup is requested.
- Add subject/content tags from real image review when the user wants actual image understanding.
- Keep tags concrete and searchable: subject, scene, material, environment, light, place.
- Avoid speculative tags.
- Prefer short descriptive titles and plain factual descriptions unless the user asks for a different style.
## Commands
### Check auth
```bash
python skills/flickr-claw/scripts/flickr_skill.py --check-auth
```
### List albums
```bash
python skills/flickr-claw/scripts/flickr_skill.py --list-albums
```
### Export photos from one album
```bash
python skills/flickr-claw/scripts/flickr_skill.py --album-photos --album-id ALBUM_ID --out ./flickr_album_photos.csv
```
### Download one album for local review
```bash
python skills/flickr-claw/scripts/flickr_skill.py --download-album --album-id ALBUM_ID --out-dir ./flickr-album-downloads
```
### Start write auth
```bash
python skills/flickr-claw/scripts/flickr_skill.py --start-auth --perms write
```
### Finish auth
```bash
python skills/flickr-claw/scripts/flickr_skill.py --finish-auth --verifier CODE
```
### Audit recent uploads
```bash
python skills/flickr-claw/scripts/flickr_skill.py --audit --days 30 --out ./flickr_recent_uploads_audit.csv
```
### Download latest images for local review
```bash
python skills/flickr-claw/scripts/flickr_skill.py --download-latest --count 10 --days 30 --out-dir ./flickr-latest-downloads
```
### Add tags to a photo
```bash
python skills/flickr-claw/scripts/flickr_skill.py --add-tags --photo-id PHOTO_ID --tags "harbor, waterfront, blue-sky"
```
### Replace all tags on a photo
```bash
python skills/flickr-claw/scripts/flickr_skill.py --set-tags --photo-id PHOTO_ID --tags "harbor waterfront cityscape"
```
### Set title only
```bash
python skills/flickr-claw/scripts/flickr_skill.py --set-title --photo-id PHOTO_ID --title "Urban waterfront scene"
```
### Set description only
```bash
python skills/flickr-claw/scripts/flickr_skill.py --set-description --photo-id PHOTO_ID --description "View across an urban waterfront with clear weather and industrial details."
```
### Set title and description together
```bash
python skills/flickr-claw/scripts/flickr_skill.py --set-meta --photo-id PHOTO_ID --title "Urban waterfront scene" --description "View across an urban waterfront with clear weather and industrial details."
```
## Publication scope
Include:
- auth flow
- auth verification
- album listing/export/download
- recent-upload audit export
- recent-image download
- tag/title/description editing
Exclude:
- real API keys or secrets
- OAuth tokens
- request-token files
- user-specific audit CSVs, manifests, or downloaded images
- account-specific examples when generic ones work
## Resources
- `scripts/flickr_skill.py` — Flickr helper with auth, verification, album/recent export, download, and metadata editing support.
- `references/workflow.md` — setup details and command examples.
FILE:scripts/flickr_skill.py
import argparse
import csv
import json
from datetime import datetime, timedelta, timezone
from pathlib import Path
import sys
from requests_oauthlib import OAuth1Session
REQUEST_TOKEN_URL = 'https://www.flickr.com/services/oauth/request_token'
AUTHORIZE_URL = 'https://www.flickr.com/services/oauth/authorize'
ACCESS_TOKEN_URL = 'https://www.flickr.com/services/oauth/access_token'
REST_URL = 'https://api.flickr.com/services/rest'
CREDS_FILE = Path.home() / '.openclaw' / 'flickr-app-credentials.json'
DEFAULT_REQUEST_FILE = Path.home() / '.openclaw' / 'flickr-request-token-manual.txt'
DEFAULT_ACCESS_FILE = Path.home() / '.openclaw' / 'flickr-access-token-manual.txt'
DEFAULT_AUDIT_OUT = Path.cwd() / 'flickr_recent_uploads_audit.csv'
DEFAULT_DOWNLOAD_DIR = Path.cwd() / 'flickr-latest-downloads'
DEFAULT_ALBUM_OUT = Path.cwd() / 'flickr_album_photos.csv'
def safe_print(message: str):
try:
print(message)
except UnicodeEncodeError:
encoded = message.encode(sys.stdout.encoding or 'utf-8', errors='replace').decode(sys.stdout.encoding or 'utf-8', errors='replace')
print(encoded)
def fail(message: str):
raise SystemExit(message)
def load_creds(creds_file: Path = CREDS_FILE):
if not creds_file.exists():
fail(
f'Missing Flickr credentials file: {creds_file}\n'
'Create ~/.openclaw/flickr-app-credentials.json with api_key and api_secret.'
)
try:
data = json.loads(creds_file.read_text(encoding='utf-8'))
except json.JSONDecodeError as e:
fail(f'Could not parse credentials file as JSON: {creds_file}\n{e}')
api_key = data.get('api_key')
api_secret = data.get('api_secret')
if not api_key or not api_secret:
fail(f'Credentials file must contain api_key and api_secret: {creds_file}')
return api_key, api_secret
def parse_tokens(path: Path):
if not path.exists():
fail(
f'Missing token file: {path}\n'
'Run --start-auth, approve the Flickr auth URL, then run --finish-auth --verifier CODE.'
)
data = path.read_text(encoding='utf-8').strip()
if data.startswith('{') and data.endswith('}'):
items = {}
inner = data[1:-1].strip()
for part in inner.split(','):
if ':' not in part:
continue
k, v = part.split(':', 1)
items[k.strip().strip("'\"")] = v.strip().strip("'\"")
if not items.get('oauth_token') or not items.get('oauth_token_secret'):
fail(f'Token file is missing oauth_token or oauth_token_secret: {path}')
return items
fail(f'Could not parse token file: {path}')
def make_oauth(access_file: Path):
api_key, api_secret = load_creds()
at = parse_tokens(access_file)
return OAuth1Session(
api_key,
client_secret=api_secret,
resource_owner_key=at['oauth_token'],
resource_owner_secret=at['oauth_token_secret'],
)
def start_auth(req_file: Path, perms: str = 'write'):
api_key, api_secret = load_creds()
oauth = OAuth1Session(api_key, client_secret=api_secret, callback_uri='oob')
tokens = oauth.fetch_request_token(REQUEST_TOKEN_URL)
req_file.parent.mkdir(parents=True, exist_ok=True)
req_file.write_text(str(tokens), encoding='utf-8')
auth_url = oauth.authorization_url(AUTHORIZE_URL, perms=perms)
print('AUTH_URL:' + auth_url)
print('REQUEST_TOKEN_FILE:' + str(req_file))
print('REQUEST_PERMS:' + perms)
def finish_auth(req_file: Path, verifier: str, access_file: Path):
api_key, api_secret = load_creds()
rt = parse_tokens(req_file)
oauth = OAuth1Session(
api_key,
client_secret=api_secret,
resource_owner_key=rt['oauth_token'],
resource_owner_secret=rt['oauth_token_secret'],
verifier=verifier,
)
try:
tokens = oauth.fetch_access_token(ACCESS_TOKEN_URL)
except Exception as e:
fail(
'Could not redeem Flickr verifier code.\n'
'Make sure you used the most recent auth URL, approved it, and pasted the verifier exactly.\n'
f'Details: {e}'
)
access_file.parent.mkdir(parents=True, exist_ok=True)
access_file.write_text(str(tokens), encoding='utf-8')
print('ACCESS_TOKEN_FILE:' + str(access_file))
def check_auth(access_file: Path):
oauth = make_oauth(access_file)
info = flickr_get(oauth, 'flickr.test.login')
user = info.get('user', {})
print('AUTH_OK')
print('USER_ID:' + str(user.get('id', '')))
print('USERNAME:' + str((user.get('username') or {}).get('_content', '')))
print('CHECK_AUTH_NOTE: This confirms the token works for authenticated Flickr API calls. Write permission still depends on how the token was minted.')
def flickr_get(oauth, method, **params):
q = {'method': method, 'format': 'json', 'nojsoncallback': '1'}
q.update(params)
r = oauth.get(REST_URL, params=q, timeout=60)
r.raise_for_status()
data = r.json()
if data.get('stat') != 'ok':
raise RuntimeError(f'Flickr API error for {method}: {data}')
return data
def flickr_post(oauth, method, **params):
form = {'method': method, 'format': 'json', 'nojsoncallback': '1'}
form.update(params)
r = oauth.post(REST_URL, data=form, timeout=60)
r.raise_for_status()
data = r.json()
if data.get('stat') != 'ok':
raise RuntimeError(f'Flickr API error for {method}: {data}')
return data
def photo_row_from_api_photo(p):
pid = p['id']
title = p.get('title', '') or ''
desc = (p.get('description') or {}).get('_content', '') if isinstance(p.get('description'), dict) else ''
tags = p.get('tags', '') or ''
geotag = ('latitude' in p and p.get('latitude') not in ('0', '0.0', None, '')) or ('longitude' in p and p.get('longitude') not in ('0', '0.0', None, ''))
taken = p.get('datetaken', '')
uploaded = p.get('dateupload', '')
url = f"https://www.flickr.com/photos/{p.get('pathalias') or 'me'}/{pid}"
return {
'photo_id': pid,
'title': title,
'description_present': 'yes' if desc.strip() else 'no',
'tag_count': len([t for t in tags.split(' ') if t.strip()]) if tags else 0,
'tags': tags,
'uploaded_at': uploaded,
'taken_at': taken,
'has_geo': 'yes' if geotag else 'no',
'photo_url': url,
}
def get_authenticated_user_id(oauth):
info = flickr_get(oauth, 'flickr.test.login')
user = info.get('user', {})
user_id = user.get('id')
if not user_id:
fail('Could not resolve authenticated Flickr user ID.')
return user_id
def rows_from_recent(oauth, days: int):
min_upload_date = int((datetime.now(timezone.utc) - timedelta(days=days)).timestamp())
user_id = get_authenticated_user_id(oauth)
page = 1
rows = []
while True:
data = flickr_get(
oauth,
'flickr.people.getPhotos',
user_id=user_id,
min_upload_date=str(min_upload_date),
extras='date_upload,date_taken,tags,geo,description,path_alias',
per_page='500',
page=str(page),
)
photos = data['photos']
for p in photos['photo']:
rows.append(photo_row_from_api_photo(p))
if page >= int(photos['pages']):
break
page += 1
return rows
def write_csv(rows, out: Path):
out.parent.mkdir(parents=True, exist_ok=True)
with out.open('w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=['photo_id', 'title', 'description_present', 'tag_count', 'tags', 'uploaded_at', 'taken_at', 'has_geo', 'photo_url'])
writer.writeheader()
writer.writerows(rows)
print('WROTE:' + str(out))
print('ROWS:' + str(len(rows)))
def audit(access_file: Path, days: int, out: Path):
oauth = make_oauth(access_file)
rows = rows_from_recent(oauth, days)
write_csv(rows, out)
def list_albums(access_file: Path):
oauth = make_oauth(access_file)
user_id = get_authenticated_user_id(oauth)
page = 1
count = 0
while True:
data = flickr_get(oauth, 'flickr.photosets.getList', user_id=user_id, page=str(page), per_page='500')
photosets = data['photosets']
for s in photosets.get('photoset', []):
count += 1
title = ((s.get('title') or {}).get('_content', '')).strip()
safe_print(f"ALBUM:{s.get('id','')}\t{title}\tPHOTOS:{s.get('photos','')}")
if page >= int(photosets.get('pages', 1)):
break
page += 1
safe_print('ALBUM_COUNT:' + str(count))
def rows_from_album(oauth, album_id: str):
page = 1
rows = []
while True:
data = flickr_get(
oauth,
'flickr.photosets.getPhotos',
photoset_id=album_id,
extras='date_upload,date_taken,tags,geo,description,path_alias',
per_page='500',
page=str(page),
)
photoset = data['photoset']
for p in photoset.get('photo', []):
rows.append(photo_row_from_api_photo(p))
if page >= int(photoset.get('pages', 1)):
break
page += 1
return rows
def album_photos(access_file: Path, album_id: str, out: Path):
oauth = make_oauth(access_file)
rows = rows_from_album(oauth, album_id)
write_csv(rows, out)
def quote_tags(tags_list):
return ' '.join(f'"{t}"' if ' ' in t else t for t in tags_list)
def add_tags(access_file: Path, photo_id: str, tags: str):
oauth = make_oauth(access_file)
info = flickr_get(oauth, 'flickr.photos.getInfo', photo_id=photo_id)
existing_raw = ((info.get('photo') or {}).get('tags') or {}).get('tag', [])
existing = [t.get('raw', '').strip() for t in existing_raw if t.get('raw', '').strip()]
additions = [t.strip() for t in tags.split(',') if t.strip()]
merged = []
seen = set()
for tag in existing + additions:
key = tag.lower()
if key in seen:
continue
seen.add(key)
merged.append(tag)
try:
flickr_post(oauth, 'flickr.photos.setTags', photo_id=photo_id, tags=quote_tags(merged))
except Exception as e:
fail(
'Could not add tags.\n'
'If the token was authorized with read-only access, mint a new write token with --start-auth --perms write.\n'
f'Details: {e}'
)
print('ADD_TAGS_OK:' + photo_id)
print('FINAL:' + ', '.join(merged))
def set_tags(access_file: Path, photo_id: str, tags: str):
oauth = make_oauth(access_file)
try:
flickr_post(oauth, 'flickr.photos.setTags', photo_id=photo_id, tags=tags)
except Exception as e:
fail(
'Could not replace tags.\n'
'If the token was authorized with read-only access, mint a new write token with --start-auth --perms write.\n'
f'Details: {e}'
)
print('SET_TAGS_OK:' + photo_id)
print('TAGS:' + tags)
def set_title(access_file: Path, photo_id: str, title: str):
oauth = make_oauth(access_file)
try:
flickr_post(oauth, 'flickr.photos.setMeta', photo_id=photo_id, title=title)
except Exception as e:
fail(
'Could not set title.\n'
'This operation needs a write-capable token.\n'
f'Details: {e}'
)
print('SET_TITLE_OK:' + photo_id)
print('TITLE:' + title)
def set_description(access_file: Path, photo_id: str, description: str):
oauth = make_oauth(access_file)
info = flickr_get(oauth, 'flickr.photos.getInfo', photo_id=photo_id)
current_title = ((info.get('photo') or {}).get('title') or {}).get('_content', '')
try:
flickr_post(oauth, 'flickr.photos.setMeta', photo_id=photo_id, title=current_title, description=description)
except Exception as e:
fail(
'Could not set description.\n'
'This operation needs a write-capable token.\n'
f'Details: {e}'
)
print('SET_DESCRIPTION_OK:' + photo_id)
def set_meta(access_file: Path, photo_id: str, title: str, description: str):
oauth = make_oauth(access_file)
try:
flickr_post(oauth, 'flickr.photos.setMeta', photo_id=photo_id, title=title, description=description)
except Exception as e:
fail(
'Could not set title and description.\n'
'This operation needs a write-capable token.\n'
f'Details: {e}'
)
print('SET_META_OK:' + photo_id)
def pick_size(sizes):
preferred = ['Large 2048', 'Large 1600', 'Large', 'Medium 1024', 'Medium 800', 'Medium 640', 'Medium']
by_label = {s['label']: s for s in sizes}
for label in preferred:
if label in by_label:
return by_label[label]
return sizes[-1]
def download_photo_rows(oauth, rows, out_dir: Path):
out_dir.mkdir(parents=True, exist_ok=True)
manifest = []
for i, row in enumerate(rows, start=1):
pid = row['photo_id']
sizes = flickr_get(oauth, 'flickr.photos.getSizes', photo_id=pid)['sizes']['size']
chosen = pick_size(sizes)
url = chosen['source']
ext = Path(url).suffix or '.jpg'
out = out_dir / f"{i:02d}_{pid}{ext}"
img = oauth.get(url, timeout=120)
img.raise_for_status()
out.write_bytes(img.content)
row = dict(row)
row['downloaded_file'] = str(out)
row['download_label'] = chosen.get('label')
row['width'] = chosen.get('width')
row['height'] = chosen.get('height')
manifest.append(row)
print('DOWNLOADED:' + str(out))
manifest_path = out_dir / 'manifest.json'
manifest_path.write_text(json.dumps(manifest, indent=2), encoding='utf-8')
print('MANIFEST:' + str(manifest_path))
def download_latest(access_file: Path, count: int, days: int, out_dir: Path):
oauth = make_oauth(access_file)
rows = rows_from_recent(oauth, days)
latest = sorted(rows, key=lambda r: int(r['uploaded_at'] or '0'), reverse=True)[:count]
download_photo_rows(oauth, latest, out_dir)
def download_album(access_file: Path, album_id: str, out_dir: Path):
oauth = make_oauth(access_file)
rows = rows_from_album(oauth, album_id)
download_photo_rows(oauth, rows, out_dir)
def main():
ap = argparse.ArgumentParser(
description='Authenticate to Flickr, export metadata, download recent or album images locally, and edit tags, titles, and descriptions.'
)
ap.add_argument('--request-file', default=str(DEFAULT_REQUEST_FILE), help='Path to the saved Flickr request-token file.')
ap.add_argument('--access-file', default=str(DEFAULT_ACCESS_FILE), help='Path to the saved Flickr access-token file.')
ap.add_argument('--start-auth', action='store_true', help='Start Flickr OAuth and print an authorization URL.')
ap.add_argument('--finish-auth', action='store_true', help='Redeem a Flickr verifier code and save the access token.')
ap.add_argument('--audit', action='store_true', help='Export recent Flickr uploads to CSV.')
ap.add_argument('--check-auth', action='store_true', help='Verify that the current token works for authenticated Flickr API calls.')
ap.add_argument('--list-albums', action='store_true', help='List albums/photosets for the authenticated Flickr account.')
ap.add_argument('--album-photos', action='store_true', help='Export photos from one album/photoset to CSV.')
ap.add_argument('--download-latest', action='store_true', help='Download the latest recent photos for local review.')
ap.add_argument('--download-album', action='store_true', help='Download all photos from one album/photoset for local review.')
ap.add_argument('--set-tags', action='store_true', help='Replace the full tag list on a photo.')
ap.add_argument('--add-tags', action='store_true', help='Merge new tags into the existing tag list on a photo.')
ap.add_argument('--set-title', action='store_true', help='Set the title on a photo.')
ap.add_argument('--set-description', action='store_true', help='Set the description on a photo.')
ap.add_argument('--set-meta', action='store_true', help='Set both title and description on a photo.')
ap.add_argument('--photo-id', help='Flickr photo ID for metadata write operations.')
ap.add_argument('--album-id', help='Flickr album/photoset ID for album export or download operations.')
ap.add_argument('--tags', help='Comma-separated tags for --add-tags, or a full tag string for --set-tags.')
ap.add_argument('--title', help='Photo title for --set-title or --set-meta.')
ap.add_argument('--description', help='Photo description for --set-description or --set-meta.')
ap.add_argument('--verifier', help='Verifier code returned by Flickr after approval.')
ap.add_argument('--perms', choices=['read', 'write', 'delete'], default='write', help='Requested Flickr OAuth permission level.')
ap.add_argument('--days', type=int, default=30, help='How many recent days to inspect for audit or download operations.')
ap.add_argument('--count', type=int, default=10, help='How many recent photos to download with --download-latest.')
ap.add_argument('--out', default=str(DEFAULT_AUDIT_OUT), help='CSV output path for --audit or --album-photos.')
ap.add_argument('--out-dir', default=str(DEFAULT_DOWNLOAD_DIR), help='Output directory for --download-latest or --download-album.')
args = ap.parse_args()
req = Path(args.request_file)
acc = Path(args.access_file)
if args.start_auth:
start_auth(req, args.perms)
elif args.finish_auth:
if not args.verifier:
fail('Missing --verifier')
finish_auth(req, args.verifier, acc)
elif args.audit:
audit(acc, args.days, Path(args.out))
elif args.check_auth:
check_auth(acc)
elif args.list_albums:
list_albums(acc)
elif args.album_photos:
if not args.album_id:
fail('Need --album-id')
album_photos(acc, args.album_id, Path(args.out))
elif args.download_latest:
download_latest(acc, args.count, args.days, Path(args.out_dir))
elif args.download_album:
if not args.album_id:
fail('Need --album-id')
download_album(acc, args.album_id, Path(args.out_dir))
elif args.set_tags:
if not args.photo_id or not args.tags:
fail('Need --photo-id and --tags')
set_tags(acc, args.photo_id, args.tags)
elif args.add_tags:
if not args.photo_id or not args.tags:
fail('Need --photo-id and --tags')
add_tags(acc, args.photo_id, args.tags)
elif args.set_title:
if not args.photo_id or args.title is None:
fail('Need --photo-id and --title')
set_title(acc, args.photo_id, args.title)
elif args.set_description:
if not args.photo_id or args.description is None:
fail('Need --photo-id and --description')
set_description(acc, args.photo_id, args.description)
elif args.set_meta:
if not args.photo_id or args.title is None or args.description is None:
fail('Need --photo-id, --title, and --description')
set_meta(acc, args.photo_id, args.title, args.description)
else:
fail('Use one of --start-auth --finish-auth --audit --check-auth --list-albums --album-photos --download-latest --download-album --set-tags --add-tags --set-title --set-description --set-meta')
if __name__ == '__main__':
main()
FILE:references/workflow.md
# Flickr workflow
## Local state
By default the script stores local state under the current user's home directory in `.openclaw`:
- App credentials: `~/.openclaw/flickr-app-credentials.json`
- Request token: `~/.openclaw/flickr-request-token-manual.txt`
- Access token: `~/.openclaw/flickr-access-token-manual.txt`
Credentials file format:
```json
{
"api_key": "YOUR_FLICKR_API_KEY",
"api_secret": "YOUR_FLICKR_API_SECRET"
}
```
## Dependency
Install the required Python package:
```bash
pip install requests-oauthlib
```
## Common commands
### Cross-platform form
```bash
python skills/flickr-claw/scripts/flickr_skill.py --check-auth
python skills/flickr-claw/scripts/flickr_skill.py --list-albums
python skills/flickr-claw/scripts/flickr_skill.py --album-photos --album-id ALBUM_ID --out ./flickr_album_photos.csv
python skills/flickr-claw/scripts/flickr_skill.py --download-album --album-id ALBUM_ID --out-dir ./flickr-album-downloads
python skills/flickr-claw/scripts/flickr_skill.py --start-auth --perms write
python skills/flickr-claw/scripts/flickr_skill.py --finish-auth --verifier CODE
python skills/flickr-claw/scripts/flickr_skill.py --audit --days 30 --out ./flickr_recent_uploads_audit.csv
python skills/flickr-claw/scripts/flickr_skill.py --download-latest --count 10 --days 30 --out-dir ./flickr-latest-downloads
python skills/flickr-claw/scripts/flickr_skill.py --add-tags --photo-id PHOTO_ID --tags "harbor, waterfront, blue-sky"
python skills/flickr-claw/scripts/flickr_skill.py --set-title --photo-id PHOTO_ID --title "Urban waterfront scene"
python skills/flickr-claw/scripts/flickr_skill.py --set-description --photo-id PHOTO_ID --description "View across an urban waterfront with clear weather and industrial details."
python skills/flickr-claw/scripts/flickr_skill.py --set-meta --photo-id PHOTO_ID --title "Urban waterfront scene" --description "View across an urban waterfront with clear weather and industrial details."
```
### Windows PowerShell form
```powershell
python .\skills\flickr-claw\scripts\flickr_skill.py --check-auth
python .\skills\flickr-claw\scripts\flickr_skill.py --list-albums
python .\skills\flickr-claw\scripts\flickr_skill.py --album-photos --album-id ALBUM_ID --out .\flickr_album_photos.csv
python .\skills\flickr-claw\scripts\flickr_skill.py --download-album --album-id ALBUM_ID --out-dir .\flickr-album-downloads
python .\skills\flickr-claw\scripts\flickr_skill.py --start-auth --perms write
python .\skills\flickr-claw\scripts\flickr_skill.py --finish-auth --verifier CODE
```
## Failure guide
- Missing credentials file: create `~/.openclaw/flickr-app-credentials.json` with `api_key` and `api_secret`.
- Missing token file: run `--start-auth`, approve the Flickr URL, then run `--finish-auth --verifier CODE`.
- `--check-auth` succeeds but write actions fail: the token likely has read access only; mint a new token with `--start-auth --perms write`.
- Album export/download fails: double-check the `--album-id` from `--list-albums`.
- Flickr UI does not show updates immediately: verify through API or refresh later.
## Notes
- `--check-auth` is the safest first command on a fresh install.
- Use `--list-albums` first when working from a specific Flickr album.
- Prefer `--add-tags` over `--set-tags` unless full replacement is intended.
- `--set-tags` overwrites the complete tag list.
- Write operations require a token minted with `--perms write`.
- Use `--download-latest` or `--download-album` before real image review so the agent can inspect actual image content locally.
- Delete downloaded local image copies after review/tagging unless the user explicitly asks to keep them.
- Override defaults with `--request-file`, `--access-file`, `--out`, or `--out-dir` when another location is needed.