{"repo": "0xbl33p/goblintown", "files": {"src/cli-help.ts": {"a5f9571f5c1ec2a6e09ead4adb1fba94e06e77b1": {"t": 1785745564.4811444, "n": 14048, "h": "025ec80d379ab89d", "c": "export function buildCliHelp(creatureKinds: readonly string[]): string {\n return `Goblintown — agent management protocol.\n\nUsage:\n goblintown init\n Initialize a Warren in the current directory.\n\n goblintown install [--port ] [--no-serve]\n Single-command agent setup. Creates a Warren, installs the Codex MCP\n config, sidecar skill, and composer plugin, then starts the Tank in\n AI-autopilot mode. MCP rite/plan tools default to harness-token execution;\n pass executionMode=local_provider only for local Tank/provider spend.\n\n goblintown /ask \"\"\n Goblin Mode: Single Goblin, one worker, one answer.\n goblintown /town \"\"\n Goblin Mode: Goblintown planner DAG with multi-agent execution.\n goblintown /tank \"\"\n Goblintown planner DAG intended for the compact live Tank.\n goblintown /history\n Show recent persisted runs.\n goblintown /context ingest [--limit ]\n Import older conversations or project notes as referenceable artifacts.\n goblintown /context search \"\" [--limit ]\n Search imported context and prior run artifacts.\n\n goblintown summon --task \"...\" [--personality

]\n Run a single creature once. Output goes to stdout; loot is stashed.\n Kinds: ${creatureKinds.join(\" \")}\n\n goblintown scavenge --task \"...\" --scan \"\" [--scan \"\"]...\n Run a Raccoon over matched files and stash the distilled facts.\n\n goblintown quest \"\" [--pack ] [--personality

] [--format freeform|markdown|json]\n Goblin pack with Troll arbitration. Default pack=3. Lightweight.\n\n goblintown rite \"\" [--pack ] [--scan ]... [--personality

] [--no-fallback]\n [--budget ] [--max-output ]\n [--cite ]... [--remember]\n [--no-specialist] [--specialist-cap ] [--debate]\n [--format freeform|markdown|json]\n Full ceremony: Raccoon → Goblin pack → [Debate round] → Gremlin chaos →\n Troll review → [Specialist re-rite on failure] →\n Ogre fallback → Scribe.\n --cite : load that rite's Artifact as prior context.\n --remember: auto-load up to 3 most-relevant prior Artifacts.\n --no-specialist: skip the specialist recovery layer (go straight to Ogre).\n --specialist-cap N: max specialist goblins to spawn (default 3).\n --debate: run an inter-agent debate round after the pack proposes.\n --troll-tools: enable verifier tool-use during troll review (json/regex/http.head).\n\n goblintown thesis \"\" [--horizon <30d>] [--context \"...\"] [--scan ]...\n [--solana

] [--signature ] [--remember]\n Build a project-quality thesis memo: team, product, technology,\n ecosystem position, traction, advantages, risks, invalidation triggers,\n and evidence gaps. It is not a buy/sell recommendation. Solana flags\n add read-only onchain diligence context. --scan gives the Raccoon repo\n files to inspect before the pack writes the thesis.\n\n goblintown ancestry \n Print the artifact lineage for a rite (parents → this → children).\n\n goblintown plan \"\" [--max-nodes ] [--max-replan ] [--budget ]\n [--cite ]... [--remember] [--format freeform|markdown|json]\n Use the Planner to decompose the task into a DAG of sub-rites and\n execute them in order. Each sub-rite produces its own artifact;\n dependent sub-rites consume them. On a node failure the planner is\n re-invoked (recursive replan, max depth 2 by default).\n\n goblintown context ingest [--limit ]\n Import local text files from an old conversation or project folder into\n the Hoard as file-backed Artifacts. Generated folders are skipped.\n goblintown context search \"\" [--limit ]\n Search all stored Artifacts, including imported context, by relevance.\n goblintown context scan chats [--source codex|chatgpt|folder] [--path ] [--query ] [--since ] [--limit ] [--json]\n Scan previous chats without importing. Defaults to local Codex sessions.\n goblintown context import chats [--source codex|chatgpt|folder] [--path ] [--all|--ids ] [--query ] [--since ] [--limit ] [--no-vectorize] [--summarize]\n Import selected previous chats as Hoard Artifacts. Default is local parse\n plus pre-vectorized embeddings when an embedding provider is configured.\n AI summaries are opt-in through --summarize.\n goblintown context vectorize [--missing-only] [--limit ]\n Precompute embeddings for stored Artifacts using the embedding route.\n\n goblintown export-trace [--out ]\n Export a run as an LLM-MAS Orchestration Trace (academic schema —\n xxzcc/awesome-llm-mas-rl).\n\n goblintown fold [--threshold ] [--min-overlap ] [--max-cluster ] [--min-age-days ]\n Fold related older artifacts into higher-level summary artifacts\n (Pigeon-Scribe). Defaults: threshold=30, overlap=2, max=6, age=7d.\n\n goblintown reset [--all|--hoard|--artifacts|--runs] [--yes]\n Reset the town. Default scope (--all) clears the entire hoard\n (loot, quests, rites, artifacts, inbox, outbox) and the SSE run\n log; preserves warren.json and reward.mjs. Asks for \"RESET\"\n confirmation unless --yes is passed.\n Narrower scopes:\n --hoard everything in .goblintown/hoard/\n --artifacts only .goblintown/hoard/artifacts/\n --runs only .goblintown/runs/\n\n goblintown reroll [--no-fallback] [--budget ]\n Re-run an existing rite with identical task / pack / personality / scan.\n\n goblintown export [--out ]\n Render a Rite as a self-contained markdown document.\n\n goblintown compare \n Side-by-side comparison of two rites.\n\n goblintown audit \n Walk a Rite's causal graph; report tokens, drift, longest chain, warnings.\n\n goblintown graph \n Render the causal graph as ASCII (rite-shaped if it's a rite id,\n ancestry chain if it's a loot id).\n\n goblintown drift\n Aggregate personality-drift report across all stashed loot.\n\n goblintown hoard [--kind ] [--since ] [--limit ] [--rite ] [--quest ]\n List the contents of the Hoard, optionally filtered.\n\n goblintown send --to --loot [--audience \"...\"]\n Pigeon-compress a Loot and deliver it to another Warren's inbox.\n\n goblintown inbox\n List inbox messages and verify their signatures.\n\n goblintown outbox\n List outbox records.\n\n goblintown route\n List per-creature provider routes.\n goblintown route set --preset [--model ] [--base-url ] [--api-key-env ] [--format freeform|markdown|json]\n Route a specific slot (goblin/ogre/troll/.../embedding) to a provider.\n goblintown route clear |--all\n Remove route overrides.\n\n goblintown country peer add --name --url \n goblintown country peer rm \n goblintown country peer ls\n Manage Goblin-Country peers in warren.json.\n goblintown country show\n Show current country config (backend/mode/code/discoverability/pending requests).\n goblintown country set [--enabled ] [--backend ] [--discoverable ]\n Update country-mode config in warren.json.\n goblintown country discover [--code ] [--server ]\n Query discoverable open countries (3 or fewer members) from a running Goblintown server.\n goblintown country join --country-id --country-code [--target-url ] [--server ]\n Send join request to a discovered country via server API.\n goblintown country requests ls [--server ]\n goblintown country requests approve [--server ]\n goblintown country requests deny [--server ]\n List/resolve pending join requests through server API.\n goblintown country run --task \"...\" [--peer ]... [--all] [--pack ] [--format freeform|markdown|json]\n Dispatch a Rite to peer warrens and wait for completion.\n\n goblintown cloud\n Show the bundled Goblintown Cloud project, first-run Local Only vs Goblintown Cloud choice,\n Settings -> Account controls, and optional Firebase env overrides.\n\n goblintown mcp [--doctor|--install-codex|--config-snippet] [--package ]\n Start the local stdio MCP sidecar for Codex. The sidecar exposes Single\n Goblin, full Rite, Planner DAG, provider snapshot, and doctor tools from\n the current project Warren, or a Codex-local global Warren under\n ~/.codex/goblintown when no project Warren exists. --install-codex writes\n ~/.codex/config.toml. No hosted MCP is required.\n\n goblintown chatgpt install [--port ] [--no-tunnel] [--no-open]\n goblintown chatgpt serve [--port ] [--host ] [--public-base-url ] [--tunnel] [--allowed-host ]...\n Install or start the Goblintown ChatGPT App 1.0 dev adapter. The easy path\n is npx -y goblintown@latest chatgpt install: it starts the adapter,\n opens the walkthrough page, and creates a quick HTTPS tunnel for ChatGPT\n Developer Mode. The adapter exposes a Streamable HTTP MCP endpoint at /mcp\n and a Tank widget resource.\n\n goblintown skill install [--skills-dir ] [--force]\n Install the bundled goblintown-sidecar Codex skill into ~/.codex/skills\n so Codex knows when and how to install packages, register MCP, ask for\n consent, and use the Goblintown MCP tools.\n\n goblintown plugin install [--local-mcp]\n Install Goblintown Codex Plugin 1.0 into ~/plugins/goblintown and add it\n to ~/.agents/plugins/marketplace.json, then run codex plugin add so\n Goblintown is installed and enabled for Codex's composer + menu. Use\n --local-mcp from a built checkout to point the plugin at that checkout's\n dist/cli.js instead of npm latest.\n\n goblintown addon [ls]\n goblintown addon enable solana\n goblintown addon disable solana\n goblintown addon solana
\n goblintown addon solana tx \n Manage optional local add-ons. The Solana add-on contributes read-only\n onchain verifier tools when --troll-tools is enabled and can run direct\n read-only profile/activity/token/transaction lookups.\n\n goblintown sentiment sources\n goblintown sentiment market\n goblintown sentiment project \"\"\n goblintown sentiment key set --value \n goblintown sentiment key clear \n Inspect free sentiment sources, run no-key market/project sentiment\n summaries, and store optional API keys locally in .goblintown/secrets.json.\n Sources: coingecko, dune, neynar, santiment, cryptopanic, lunarcrush.\n\n goblintown serve [--port ] [--chat]\n Start the Goblin Mode GUI. Default port=7777.\n By default runs in AI-autopilot mode: the Tank diorama with config menus, no chat surface.\n The agent drives rites and plans via MCP tools. Use --chat to restore the legacy chat UI.\n Settings also contains Country, Mail, Add-ons, API Provider, and Reset -> Asteroid Mode.\n Bundled sprite sheets and the Goblintown wordmark are loaded from site/assets.\n\nEnvironment:\n OPENAI_API_KEY required (except for init / drift / hoard / inbox / outbox / audit / graph / export / compare / ancestry)\n OPENAI_BASE_URL optional; e.g. https://openrouter.ai/api/v1\n Provider-specific keys OPENROUTER_API_KEY, GROQ_API_KEY, TOGETHER_API_KEY, MISTRAL_API_KEY,\n DEEPSEEK_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY\n FIREBASE_API_KEY optional override for forks; normal users use bundled Goblintown Cloud config\n FIREBASE_AUTH_DOMAIN optional override\n FIREBASE_PROJECT_ID optional override\n FIREBASE_APP_ID optional override\n FIREBASE_STORAGE_BUCKET optional override\n FIREBASE_MESSAGING_SENDER_ID optional override\n FIREBASE_MEASUREMENT_ID optional override\n GOBLINTOWN_MODEL_GOBLIN default: gpt-5-mini\n GOBLINTOWN_MODEL_OGRE default: gpt-5\n GOBLINTOWN_MODEL_TROLL default: gpt-5-mini\n GOBLINTOWN_MODEL_SCRIBE default: gpt-5-mini (Pigeon-as-Scribe artifact distillation)\n GOBLINTOWN_EMBEDDING_MODEL default: text-embedding-3-small (artifact retrieval)\n GOBLINTOWN_TOOLS_HTTP set to 1 to enable http.head verifier tool (default disabled)\n GOBLINTOWN_TOOLS_SOLANA set to 1 to enable the Solana onchain add-on without changing warren.json\n GOBLINTOWN_SOLANA_RPC_URL optional Solana RPC endpoint (default: https://api.mainnet-beta.solana.com)\n GOBLINTOWN_CHATGPT_PORT default port for goblintown chatgpt serve (8787)\n GOBLINTOWN_CHATGPT_HOST bind host for goblintown chatgpt serve (127.0.0.1)\n GOBLINTOWN_CHATGPT_ALLOWED_HOSTS comma-separated Host headers for ChatGPT tunnels/deployments\n COINGECKO_API_KEY optional sentiment key; overrides local .goblintown/secrets.json\n DUNE_API_KEY optional sentiment key; overrides local .goblintown/secrets.json\n NEYNAR_API_KEY optional sentiment key; overrides local .goblintown/secrets.json\n SANTIMENT_API_KEY optional sentiment key; overrides local .goblintown/secrets.json\n CRYPTOPANIC_AUTH_TOKEN optional sentiment token; overrides local .goblintown/secrets.json\n LUNARCRUSH_API_KEY optional sentiment key; overrides local .goblintown/secrets.json\n GOBLINTOWN_MAX_CONCURRENCY default: 5 (in-flight API calls)\n GOBLINTOWN_SERVER_URL default base URL for country discover/join/requests commands\n (also: GREMLIN, RACCOON, PIGEON)\n\n\"OpenAI tried to put the goblins back in the box. We built the box for them.\"\n`;\n}\n"}, "e59e8614bf99587288adcbbef6001c5f33b00447": {"t": 1785754275.473623, "n": 11720, "h": "63b323dcbe2de322", "c": "export function buildCliHelp(creatureKinds: readonly string[]): string {\n return `Goblintown — agent management protocol.\n\nUsage:\n goblintown init\n Initialize a Warren in the current directory.\n\n goblintown /ask \"\"\n Goblin Mode: Single Goblin, one worker, one answer.\n goblintown /town \"\"\n Goblin Mode: Goblintown planner DAG with multi-agent execution.\n goblintown /tank \"\"\n Goblintown planner DAG intended for the compact live Tank.\n goblintown /history\n Show recent persisted runs.\n goblintown /context ingest [--limit ]\n Import older conversations or project notes as referenceable artifacts.\n goblintown /context search \"\" [--limit ]\n Search imported context and prior run artifacts.\n\n goblintown summon --task \"...\" [--personality

]\n Run a single creature once. Output goes to stdout; loot is stashed.\n Kinds: ${creatureKinds.join(\" \")}\n\n goblintown scavenge --task \"...\" --scan \"\" [--scan \"\"]...\n Run a Raccoon over matched files and stash the distilled facts.\n\n goblintown quest \"\" [--pack ] [--personality

] [--format freeform|markdown|json]\n Goblin pack with Troll arbitration. Default pack=3. Lightweight.\n\n goblintown rite \"\" [--pack ] [--scan ]... [--personality

] [--no-fallback]\n [--budget ] [--max-output ]\n [--cite ]... [--remember]\n [--no-specialist] [--specialist-cap ] [--debate]\n [--format freeform|markdown|json]\n Full ceremony: Raccoon → Goblin pack → [Debate round] → Gremlin chaos →\n Troll review → [Specialist re-rite on failure] →\n Ogre fallback → Scribe.\n --cite : load that rite's Artifact as prior context.\n --remember: auto-load up to 3 most-relevant prior Artifacts.\n --no-specialist: skip the specialist recovery layer (go straight to Ogre).\n --specialist-cap N: max specialist goblins to spawn (default 3).\n --debate: run an inter-agent debate round after the pack proposes.\n --troll-tools: enable verifier tool-use during troll review (json/regex/http.head).\n\n goblintown thesis \"\" [--horizon <30d>] [--context \"...\"] [--scan ]...\n [--solana

] [--signature ] [--remember]\n Build a project-quality thesis memo: team, product, technology,\n ecosystem position, traction, advantages, risks, invalidation triggers,\n and evidence gaps. It is not a buy/sell recommendation. Solana flags\n add read-only onchain diligence context. --scan gives the Raccoon repo\n files to inspect before the pack writes the thesis.\n\n goblintown ancestry \n Print the artifact lineage for a rite (parents → this → children).\n\n goblintown plan \"\" [--max-nodes ] [--max-replan ] [--budget ]\n [--cite ]... [--remember] [--format freeform|markdown|json]\n Use the Planner to decompose the task into a DAG of sub-rites and\n execute them in order. Each sub-rite produces its own artifact;\n dependent sub-rites consume them. On a node failure the planner is\n re-invoked (recursive replan, max depth 2 by default).\n\n goblintown context ingest [--limit ]\n Import local text files from an old conversation or project folder into\n the Hoard as file-backed Artifacts. Generated folders are skipped.\n goblintown context search \"\" [--limit ]\n Search all stored Artifacts, including imported context, by relevance.\n goblintown context scan chats [--source codex|chatgpt|folder] [--path ] [--query ] [--since ] [--limit ] [--json]\n Scan previous chats without importing. Defaults to local Codex sessions.\n goblintown context import chats [--source codex|chatgpt|folder] [--path ] [--all|--ids ] [--query ] [--since ] [--limit ] [--no-vectorize] [--summarize]\n Import selected previous chats as Hoard Artifacts. Default is local parse\n plus pre-vectorized embeddings when an embedding provider is configured.\n AI summaries are opt-in through --summarize.\n goblintown context vectorize [--missing-only] [--limit ]\n Precompute embeddings for stored Artifacts using the embedding route.\n\n goblintown export-trace [--out ]\n Export a run as an LLM-MAS Orchestration Trace (academic schema —\n xxzcc/awesome-llm-mas-rl).\n\n goblintown fold [--threshold ] [--min-overlap ] [--max-cluster ] [--min-age-days ]\n Fold related older artifacts into higher-level summary artifacts\n (Pigeon-Scribe). Defaults: threshold=30, overlap=2, max=6, age=7d.\n\n goblintown reset [--all|--hoard|--artifacts|--runs] [--yes]\n Reset the town. Default scope (--all) clears the entire hoard\n (loot, quests, rites, artifacts, inbox, outbox) and the SSE run\n log; preserves warren.json and reward.mjs. Asks for \"RESET\"\n confirmation unless --yes is passed.\n Narrower scopes:\n --hoard everything in .goblintown/hoard/\n --artifacts only .goblintown/hoard/artifacts/\n --runs only .goblintown/runs/\n\n goblintown reroll [--no-fallback] [--budget ]\n Re-run an existing rite with identical task / pack / personality / scan.\n\n goblintown export [--out ]\n Render a Rite as a self-contained markdown document.\n\n goblintown compare \n Side-by-side comparison of two rites.\n\n goblintown audit \n Walk a Rite's causal graph; report tokens, drift, longest chain, warnings.\n\n goblintown graph \n Render the causal graph as ASCII (rite-shaped if it's a rite id,\n ancestry chain if it's a loot id).\n\n goblintown drift\n Aggregate personality-drift report across all stashed loot.\n\n goblintown hoard [--kind ] [--since ] [--limit ] [--rite ] [--quest ]\n List the contents of the Hoard, optionally filtered.\n\n goblintown send --to --loot [--audience \"...\"]\n Pigeon-compress a Loot and deliver it to another Warren's inbox.\n\n goblintown inbox\n List inbox messages and verify their signatures.\n\n goblintown outbox\n List outbox records.\n\n goblintown route\n List per-creature provider routes.\n goblintown route set --preset [--model ] [--base-url ] [--api-key-env ] [--format freeform|markdown|json]\n Route a specific slot (goblin/ogre/troll/.../embedding) to a provider.\n goblintown route clear |--all\n Remove route overrides.\n\n goblintown country peer add --name --url \n goblintown country peer rm \n goblintown country peer ls\n Manage Goblin-Country peers in warren.json.\n goblintown country show\n Show current country config (backend/mode/code/discoverability/pending requests).\n goblintown country set [--enabled ] [--backend ] [--discoverable ]\n Update country-mode config in warren.json.\n goblintown country discover [--code ] [--server ]\n Query discoverable open countries (3 or fewer members) from a running Goblintown server.\n goblintown country join --country-id --country-code [--target-url ] [--server ]\n Send join request to a discovered country via server API.\n goblintown country requests ls [--server ]\n goblintown country requests approve [--server ]\n goblintown country requests deny [--server ]\n List/resolve pending join requests through server API.\n goblintown country run --task \"...\" [--peer ]... [--all] [--pack ] [--format freeform|markdown|json]\n Dispatch a Rite to peer warrens and wait for completion.\n\n goblintown cloud\n Show the bundled Goblintown Cloud project, first-run Local Only vs Goblintown Cloud choice,\n Settings -> Account controls, and optional Firebase env overrides.\n\n goblintown addon [ls]\n goblintown addon enable solana\n goblintown addon disable solana\n goblintown addon solana
\n goblintown addon solana tx \n Manage optional local add-ons. The Solana add-on contributes read-only\n onchain verifier tools when --troll-tools is enabled and can run direct\n read-only profile/activity/token/transaction lookups.\n\n goblintown sentiment sources\n goblintown sentiment market\n goblintown sentiment project \"\"\n goblintown sentiment key set --value \n goblintown sentiment key clear \n Inspect free sentiment sources, run no-key market/project sentiment\n summaries, and store optional API keys locally in .goblintown/secrets.json.\n Sources: coingecko, dune, neynar, santiment, cryptopanic, lunarcrush.\n\n goblintown serve [--port ]\n Start the Goblin Mode GUI. Default port=7777. The legacy Tank UI is at /tank.\n First run asks Local Only vs Goblintown Cloud; later change it in Settings -> Account.\n Settings also contains Country, Mail, Add-ons, API Provider, and Reset -> Asteroid Mode.\n Bundled sprite sheets and the Goblintown wordmark are loaded from site/assets.\n\nEnvironment:\n OPENAI_API_KEY required (except for init / drift / hoard / inbox / outbox / audit / graph / export / compare / ancestry)\n OPENAI_BASE_URL optional; e.g. https://openrouter.ai/api/v1\n Provider-specific keys OPENROUTER_API_KEY, GROQ_API_KEY, TOGETHER_API_KEY, MISTRAL_API_KEY,\n DEEPSEEK_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY\n FIREBASE_API_KEY optional override for forks; normal users use bundled Goblintown Cloud config\n FIREBASE_AUTH_DOMAIN optional override\n FIREBASE_PROJECT_ID optional override\n FIREBASE_APP_ID optional override\n FIREBASE_STORAGE_BUCKET optional override\n FIREBASE_MESSAGING_SENDER_ID optional override\n FIREBASE_MEASUREMENT_ID optional override\n GOBLINTOWN_MODEL_GOBLIN default: gpt-5.4-mini\n GOBLINTOWN_MODEL_OGRE default: gpt-5.5\n GOBLINTOWN_MODEL_TROLL default: gpt-5.4-mini\n GOBLINTOWN_MODEL_SCRIBE default: gpt-5.4-mini (Pigeon-as-Scribe artifact distillation)\n GOBLINTOWN_EMBEDDING_MODEL default: text-embedding-3-small (artifact retrieval)\n GOBLINTOWN_TOOLS_HTTP set to 1 to enable http.head verifier tool (default disabled)\n GOBLINTOWN_TOOLS_SOLANA set to 1 to enable the Solana onchain add-on without changing warren.json\n GOBLINTOWN_SOLANA_RPC_URL optional Solana RPC endpoint (default: https://api.mainnet-beta.solana.com)\n COINGECKO_API_KEY optional sentiment key; overrides local .goblintown/secrets.json\n DUNE_API_KEY optional sentiment key; overrides local .goblintown/secrets.json\n NEYNAR_API_KEY optional sentiment key; overrides local .goblintown/secrets.json\n SANTIMENT_API_KEY optional sentiment key; overrides local .goblintown/secrets.json\n CRYPTOPANIC_AUTH_TOKEN optional sentiment token; overrides local .goblintown/secrets.json\n LUNARCRUSH_API_KEY optional sentiment key; overrides local .goblintown/secrets.json\n GOBLINTOWN_MAX_CONCURRENCY default: 5 (in-flight API calls)\n GOBLINTOWN_SERVER_URL default base URL for country discover/join/requests commands\n (also: GREMLIN, RACCOON, PIGEON)\n\n\"OpenAI tried to put the goblins back in the box. We built the box for them.\"\n`;\n}\n"}, "215ef322654bf0b11bb52ec0ff1b41a71b7ce272": {"t": 1785754277.1877038, "n": 10039, "h": "f2095dbabaf95bab", "c": "export function buildCliHelp(creatureKinds: readonly string[]): string {\n return `Goblintown — agent management protocol.\n\nUsage:\n goblintown init\n Initialize a Warren in the current directory.\n\n goblintown summon --task \"...\" [--personality

]\n Run a single creature once. Output goes to stdout; loot is stashed.\n Kinds: ${creatureKinds.join(\" \")}\n\n goblintown scavenge --task \"...\" --scan \"\" [--scan \"\"]...\n Run a Raccoon over matched files and stash the distilled facts.\n\n goblintown quest \"\" [--pack ] [--personality

] [--format freeform|markdown|json]\n Goblin pack with Troll arbitration. Default pack=3. Lightweight.\n\n goblintown rite \"\" [--pack ] [--scan ]... [--personality

] [--no-fallback]\n [--budget ] [--max-output ]\n [--cite ]... [--remember]\n [--no-specialist] [--specialist-cap ] [--debate]\n [--format freeform|markdown|json]\n Full ceremony: Raccoon → Goblin pack → [Debate round] → Gremlin chaos →\n Troll review → [Specialist re-rite on failure] →\n Ogre fallback → Scribe.\n --cite : load that rite's Artifact as prior context.\n --remember: auto-load up to 3 most-relevant prior Artifacts.\n --no-specialist: skip the specialist recovery layer (go straight to Ogre).\n --specialist-cap N: max specialist goblins to spawn (default 3).\n --debate: run an inter-agent debate round after the pack proposes.\n --troll-tools: enable verifier tool-use during troll review (json/regex/http.head).\n\n goblintown thesis \"\" [--horizon <30d>] [--context \"...\"] [--scan ]...\n [--solana

] [--signature ] [--remember]\n Build a project-quality thesis memo: team, product, technology,\n ecosystem position, traction, advantages, risks, invalidation triggers,\n and evidence gaps. It is not a buy/sell recommendation. Solana flags\n add read-only onchain diligence context. --scan gives the Raccoon repo\n files to inspect before the pack writes the thesis.\n\n goblintown ancestry \n Print the artifact lineage for a rite (parents → this → children).\n\n goblintown plan \"\" [--max-nodes ] [--max-replan ] [--budget ]\n [--cite ]... [--remember] [--format freeform|markdown|json]\n Use the Planner to decompose the task into a DAG of sub-rites and\n execute them in order. Each sub-rite produces its own artifact;\n dependent sub-rites consume them. On a node failure the planner is\n re-invoked (recursive replan, max depth 2 by default).\n\n goblintown export-trace [--out ]\n Export a run as an LLM-MAS Orchestration Trace (academic schema —\n xxzcc/awesome-llm-mas-rl).\n\n goblintown fold [--threshold ] [--min-overlap ] [--max-cluster ] [--min-age-days ]\n Fold related older artifacts into higher-level summary artifacts\n (Pigeon-Scribe). Defaults: threshold=30, overlap=2, max=6, age=7d.\n\n goblintown reset [--all|--hoard|--artifacts|--runs] [--yes]\n Reset the town. Default scope (--all) clears the entire hoard\n (loot, quests, rites, artifacts, inbox, outbox) and the SSE run\n log; preserves warren.json and reward.mjs. Asks for \"RESET\"\n confirmation unless --yes is passed.\n Narrower scopes:\n --hoard everything in .goblintown/hoard/\n --artifacts only .goblintown/hoard/artifacts/\n --runs only .goblintown/runs/\n\n goblintown reroll [--no-fallback] [--budget ]\n Re-run an existing rite with identical task / pack / personality / scan.\n\n goblintown export [--out ]\n Render a Rite as a self-contained markdown document.\n\n goblintown compare \n Side-by-side comparison of two rites.\n\n goblintown audit \n Walk a Rite's causal graph; report tokens, drift, longest chain, warnings.\n\n goblintown graph \n Render the causal graph as ASCII (rite-shaped if it's a rite id,\n ancestry chain if it's a loot id).\n\n goblintown drift\n Aggregate personality-drift report across all stashed loot.\n\n goblintown hoard [--kind ] [--since ] [--limit ] [--rite ] [--quest ]\n List the contents of the Hoard, optionally filtered.\n\n goblintown send --to --loot [--audience \"...\"]\n Pigeon-compress a Loot and deliver it to another Warren's inbox.\n\n goblintown inbox\n List inbox messages and verify their signatures.\n\n goblintown outbox\n List outbox records.\n\n goblintown route\n List per-creature provider routes.\n goblintown route set --preset [--model ] [--base-url ] [--api-key-env ] [--format freeform|markdown|json]\n Route a specific slot (goblin/ogre/troll/.../embedding) to a provider.\n goblintown route clear |--all\n Remove route overrides.\n\n goblintown country peer add --name --url \n goblintown country peer rm \n goblintown country peer ls\n Manage Goblin-Country peers in warren.json.\n goblintown country show\n Show current country config (backend/mode/code/discoverability/pending requests).\n goblintown country set [--enabled ] [--backend ] [--discoverable ]\n Update country-mode config in warren.json.\n goblintown country discover [--code ] [--server ]\n Query discoverable open countries (3 or fewer members) from a running Goblintown server.\n goblintown country join --country-id --country-code [--target-url ] [--server ]\n Send join request to a discovered country via server API.\n goblintown country requests ls [--server ]\n goblintown country requests approve [--server ]\n goblintown country requests deny [--server ]\n List/resolve pending join requests through server API.\n goblintown country run --task \"...\" [--peer ]... [--all] [--pack ] [--format freeform|markdown|json]\n Dispatch a Rite to peer warrens and wait for completion.\n\n goblintown cloud\n Show the bundled Goblintown Cloud project, first-run Local Only vs Goblintown Cloud choice,\n Settings -> Account controls, and optional Firebase env overrides.\n\n goblintown addon [ls]\n goblintown addon enable solana\n goblintown addon disable solana\n goblintown addon solana
\n goblintown addon solana tx \n Manage optional local add-ons. The Solana add-on contributes read-only\n onchain verifier tools when --troll-tools is enabled and can run direct\n read-only profile/activity/token/transaction lookups.\n\n goblintown sentiment sources\n goblintown sentiment market\n goblintown sentiment project \"\"\n goblintown sentiment key set --value \n goblintown sentiment key clear \n Inspect free sentiment sources, run no-key market/project sentiment\n summaries, and store optional API keys locally in .goblintown/secrets.json.\n Sources: coingecko, dune, neynar, santiment, cryptopanic, lunarcrush.\n\n goblintown serve [--port ]\n Start the Tank UI. Default port=7777.\n First run asks Local Only vs Goblintown Cloud; later change it in Settings -> Account.\n Settings also contains Country, Mail, Add-ons, API Provider, and Reset -> Asteroid Mode.\n Bundled sprite sheets and the Goblintown wordmark are loaded from site/assets.\n\nEnvironment:\n OPENAI_API_KEY required (except for init / drift / hoard / inbox / outbox / audit / graph / export / compare / ancestry)\n OPENAI_BASE_URL optional; e.g. https://openrouter.ai/api/v1\n Provider-specific keys OPENROUTER_API_KEY, GROQ_API_KEY, TOGETHER_API_KEY, MISTRAL_API_KEY,\n DEEPSEEK_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY\n FIREBASE_API_KEY optional override for forks; normal users use bundled Goblintown Cloud config\n FIREBASE_AUTH_DOMAIN optional override\n FIREBASE_PROJECT_ID optional override\n FIREBASE_APP_ID optional override\n FIREBASE_STORAGE_BUCKET optional override\n FIREBASE_MESSAGING_SENDER_ID optional override\n FIREBASE_MEASUREMENT_ID optional override\n GOBLINTOWN_MODEL_GOBLIN default: gpt-5.4-mini\n GOBLINTOWN_MODEL_OGRE default: gpt-5.5\n GOBLINTOWN_MODEL_TROLL default: gpt-5.4-mini\n GOBLINTOWN_MODEL_SCRIBE default: gpt-5.4-mini (Pigeon-as-Scribe artifact distillation)\n GOBLINTOWN_EMBEDDING_MODEL default: text-embedding-3-small (artifact retrieval)\n GOBLINTOWN_TOOLS_HTTP set to 1 to enable http.head verifier tool (default disabled)\n GOBLINTOWN_TOOLS_SOLANA set to 1 to enable the Solana onchain add-on without changing warren.json\n GOBLINTOWN_SOLANA_RPC_URL optional Solana RPC endpoint (default: https://api.mainnet-beta.solana.com)\n COINGECKO_API_KEY optional sentiment key; overrides local .goblintown/secrets.json\n DUNE_API_KEY optional sentiment key; overrides local .goblintown/secrets.json\n NEYNAR_API_KEY optional sentiment key; overrides local .goblintown/secrets.json\n SANTIMENT_API_KEY optional sentiment key; overrides local .goblintown/secrets.json\n CRYPTOPANIC_AUTH_TOKEN optional sentiment token; overrides local .goblintown/secrets.json\n LUNARCRUSH_API_KEY optional sentiment key; overrides local .goblintown/secrets.json\n GOBLINTOWN_MAX_CONCURRENCY default: 5 (in-flight API calls)\n GOBLINTOWN_SERVER_URL default base URL for country discover/join/requests commands\n (also: GREMLIN, RACCOON, PIGEON)\n\n\"OpenAI tried to put the goblins back in the box. We built the box for them.\"\n`;\n}\n"}, "e0c8561a07b7e364d9484f3f7b523140daae2d51": {"t": 1785754280.450795, "n": 7908, "h": "91334a47e13c0776", "c": "export function buildCliHelp(creatureKinds: readonly string[]): string {\n return `Goblintown — agent management protocol.\n\nUsage:\n goblintown init\n Initialize a Warren in the current directory.\n\n goblintown summon --task \"...\" [--personality

]\n Run a single creature once. Output goes to stdout; loot is stashed.\n Kinds: ${creatureKinds.join(\" \")}\n\n goblintown scavenge --task \"...\" --scan \"\" [--scan \"\"]...\n Run a Raccoon over matched files and stash the distilled facts.\n\n goblintown quest \"\" [--pack ] [--personality

] [--format freeform|markdown|json]\n Goblin pack with Troll arbitration. Default pack=3. Lightweight.\n\n goblintown rite \"\" [--pack ] [--scan ]... [--personality

] [--no-fallback]\n [--budget ] [--max-output ]\n [--cite ]... [--remember]\n [--no-specialist] [--specialist-cap ] [--debate]\n [--format freeform|markdown|json]\n Full ceremony: Raccoon → Goblin pack → [Debate round] → Gremlin chaos →\n Troll review → [Specialist re-rite on failure] →\n Ogre fallback → Scribe.\n --cite : load that rite's Artifact as prior context.\n --remember: auto-load up to 3 most-relevant prior Artifacts.\n --no-specialist: skip the specialist recovery layer (go straight to Ogre).\n --specialist-cap N: max specialist goblins to spawn (default 3).\n --debate: run an inter-agent debate round after the pack proposes.\n --troll-tools: enable verifier tool-use during troll review (json/regex/http.head).\n\n goblintown ancestry \n Print the artifact lineage for a rite (parents → this → children).\n\n goblintown plan \"\" [--max-nodes ] [--max-replan ] [--budget ]\n [--cite ]... [--remember] [--format freeform|markdown|json]\n Use the Planner to decompose the task into a DAG of sub-rites and\n execute them in order (Phase 3). Each sub-rite produces its own artifact;\n dependent sub-rites consume them. On a node failure the planner is\n re-invoked (recursive replan, max depth 2 by default).\n\n goblintown export-trace [--out ]\n Export a run as an LLM-MAS Orchestration Trace (academic schema —\n xxzcc/awesome-llm-mas-rl).\n\n goblintown fold [--threshold ] [--min-overlap ] [--max-cluster ] [--min-age-days ]\n Phase 6: fold related older artifacts into higher-level summary\n artifacts (Pigeon-Scribe). Defaults: threshold=30, overlap=2, max=6, age=7d.\n\n goblintown reset [--all|--hoard|--artifacts|--runs] [--yes]\n Reset the town. Default scope (--all) clears the entire hoard\n (loot, quests, rites, artifacts, inbox, outbox) and the SSE run\n log; preserves warren.json and reward.mjs. Asks for \"RESET\"\n confirmation unless --yes is passed.\n Narrower scopes:\n --hoard everything in .goblintown/hoard/\n --artifacts only .goblintown/hoard/artifacts/\n --runs only .goblintown/runs/\n\n goblintown reroll [--no-fallback] [--budget ]\n Re-run an existing rite with identical task / pack / personality / scan.\n\n goblintown export [--out ]\n Render a Rite as a self-contained markdown document.\n\n goblintown compare \n Side-by-side comparison of two rites.\n\n goblintown audit \n Walk a Rite's causal graph; report tokens, drift, longest chain, warnings.\n\n goblintown graph \n Render the causal graph as ASCII (rite-shaped if it's a rite id,\n ancestry chain if it's a loot id).\n\n goblintown drift\n Aggregate personality-drift report across all stashed loot.\n\n goblintown hoard [--kind ] [--since ] [--limit ] [--rite ] [--quest ]\n List the contents of the Hoard, optionally filtered.\n\n goblintown send --to --loot [--audience \"...\"]\n Pigeon-compress a Loot and deliver it to another Warren's inbox.\n\n goblintown inbox\n List inbox messages and verify their signatures.\n\n goblintown outbox\n List outbox records.\n\n goblintown route\n List per-creature provider routes.\n goblintown route set --preset [--model ] [--base-url ] [--api-key-env ] [--format freeform|markdown|json]\n Route a specific slot (goblin/ogre/troll/.../embedding) to a provider.\n goblintown route clear |--all\n Remove route overrides.\n\n goblintown country peer add --name --url \n goblintown country peer rm \n goblintown country peer ls\n Manage Goblin-Country peers in warren.json.\n goblintown country show\n Show current country config (backend/mode/code/discoverability/pending requests).\n goblintown country set [--enabled ] [--backend ] [--discoverable ]\n Update country-mode config in warren.json.\n goblintown country discover [--code ] [--server ]\n Query discoverable open countries (3 or fewer members) from a running Goblintown server.\n goblintown country join --country-id --country-code [--target-url ] [--server ]\n Send join request to a discovered country via server API.\n goblintown country requests ls [--server ]\n goblintown country requests approve [--server ]\n goblintown country requests deny [--server ]\n List/resolve pending join requests through server API.\n goblintown country run --task \"...\" [--peer ]... [--all] [--pack ] [--format freeform|markdown|json]\n Dispatch a Rite to peer warrens and wait for completion.\n\n goblintown cloud\n Show the bundled Goblintown Cloud project, first-run Local Only vs Goblintown Cloud choice,\n Settings -> Account controls, and optional Firebase env overrides.\n\n goblintown serve [--port ]\n Start the Tank UI. Default port=7777.\n First run asks Local Only vs Goblintown Cloud; later change it in Settings -> Account.\n Settings also contains Country, Mail, API Provider, and Reset -> Asteroid Mode.\n Optional sprite sheets and logo are loaded from site/assets.\n\nEnvironment:\n OPENAI_API_KEY required (except for init / drift / hoard / inbox / outbox / audit / graph / export / compare / ancestry)\n OPENAI_BASE_URL optional; e.g. https://openrouter.ai/api/v1\n Provider-specific keys OPENROUTER_API_KEY, GROQ_API_KEY, TOGETHER_API_KEY, MISTRAL_API_KEY,\n DEEPSEEK_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY\n FIREBASE_API_KEY optional override for forks; normal users use bundled Goblintown Cloud config\n FIREBASE_AUTH_DOMAIN optional override\n FIREBASE_PROJECT_ID optional override\n FIREBASE_APP_ID optional override\n FIREBASE_STORAGE_BUCKET optional override\n FIREBASE_MESSAGING_SENDER_ID optional override\n FIREBASE_MEASUREMENT_ID optional override\n GOBLINTOWN_MODEL_GOBLIN default: gpt-5.4-mini\n GOBLINTOWN_MODEL_OGRE default: gpt-5.5\n GOBLINTOWN_MODEL_TROLL default: gpt-5.4-mini\n GOBLINTOWN_MODEL_SCRIBE default: gpt-5.4-mini (Pigeon-as-Scribe artifact distillation)\n GOBLINTOWN_EMBEDDING_MODEL default: text-embedding-3-small (artifact retrieval, Phase 6)\n GOBLINTOWN_TOOLS_HTTP set to 1 to enable http.head verifier tool (default disabled)\n GOBLINTOWN_MAX_CONCURRENCY default: 5 (in-flight API calls)\n GOBLINTOWN_SERVER_URL default base URL for country discover/join/requests commands\n (also: GREMLIN, RACCOON, PIGEON)\n\n\"OpenAI tried to put the goblins back in the box. We built the box for them.\"\n`;\n}\n"}, "b1bd07349a916f5d67d961f4621ec7cb28afa5b8": {"t": 1785754281.7886798, "n": 7158, "h": "f29001c1f542ffb2", "c": "export function buildCliHelp(creatureKinds: readonly string[]): string {\n return `Goblintown — agent management protocol.\n\nUsage:\n goblintown init\n Initialize a Warren in the current directory.\n\n goblintown summon --task \"...\" [--personality

]\n Run a single creature once. Output goes to stdout; loot is stashed.\n Kinds: ${creatureKinds.join(\" \")}\n\n goblintown scavenge --task \"...\" --scan \"\" [--scan \"\"]...\n Run a Raccoon over matched files and stash the distilled facts.\n\n goblintown quest \"\" [--pack ] [--personality

] [--format freeform|markdown|json]\n Goblin pack with Troll arbitration. Default pack=3. Lightweight.\n\n goblintown rite \"\" [--pack ] [--scan ]... [--personality

] [--no-fallback]\n [--budget ] [--max-output ]\n [--cite ]... [--remember]\n [--no-specialist] [--specialist-cap ] [--debate]\n [--format freeform|markdown|json]\n Full ceremony: Raccoon → Goblin pack → [Debate round] → Gremlin chaos →\n Troll review → [Specialist re-rite on failure] →\n Ogre fallback → Scribe.\n --cite : load that rite's Artifact as prior context.\n --remember: auto-load up to 3 most-relevant prior Artifacts.\n --no-specialist: skip the specialist recovery layer (go straight to Ogre).\n --specialist-cap N: max specialist goblins to spawn (default 3).\n --debate: run an inter-agent debate round after the pack proposes.\n --troll-tools: enable verifier tool-use during troll review (json/regex/http.head).\n\n goblintown ancestry \n Print the artifact lineage for a rite (parents → this → children).\n\n goblintown plan \"\" [--max-nodes ] [--max-replan ] [--budget ]\n [--cite ]... [--remember] [--format freeform|markdown|json]\n Use the Planner to decompose the task into a DAG of sub-rites and\n execute them in order (Phase 3). Each sub-rite produces its own artifact;\n dependent sub-rites consume them. On a node failure the planner is\n re-invoked (recursive replan, max depth 2 by default).\n\n goblintown export-trace [--out ]\n Export a run as an LLM-MAS Orchestration Trace (academic schema —\n xxzcc/awesome-llm-mas-rl).\n\n goblintown fold [--threshold ] [--min-overlap ] [--max-cluster ] [--min-age-days ]\n Phase 6: fold related older artifacts into higher-level summary\n artifacts (Pigeon-Scribe). Defaults: threshold=30, overlap=2, max=6, age=7d.\n\n goblintown reset [--all|--hoard|--artifacts|--runs] [--yes]\n Reset the town. Default scope (--all) clears the entire hoard\n (loot, quests, rites, artifacts, inbox, outbox) and the SSE run\n log; preserves warren.json and reward.mjs. Asks for \"RESET\"\n confirmation unless --yes is passed.\n Narrower scopes:\n --hoard everything in .goblintown/hoard/\n --artifacts only .goblintown/hoard/artifacts/\n --runs only .goblintown/runs/\n\n goblintown reroll [--no-fallback] [--budget ]\n Re-run an existing rite with identical task / pack / personality / scan.\n\n goblintown export [--out ]\n Render a Rite as a self-contained markdown document.\n\n goblintown compare \n Side-by-side comparison of two rites.\n\n goblintown audit \n Walk a Rite's causal graph; report tokens, drift, longest chain, warnings.\n\n goblintown graph \n Render the causal graph as ASCII (rite-shaped if it's a rite id,\n ancestry chain if it's a loot id).\n\n goblintown drift\n Aggregate personality-drift report across all stashed loot.\n\n goblintown hoard [--kind ] [--since ] [--limit ] [--rite ] [--quest ]\n List the contents of the Hoard, optionally filtered.\n\n goblintown send --to --loot [--audience \"...\"]\n Pigeon-compress a Loot and deliver it to another Warren's inbox.\n\n goblintown inbox\n List inbox messages and verify their signatures.\n\n goblintown outbox\n List outbox records.\n\n goblintown route\n List per-creature provider routes.\n goblintown route set --preset [--model ] [--base-url ] [--api-key-env ] [--format freeform|markdown|json]\n Route a specific slot (goblin/ogre/troll/.../embedding) to a provider.\n goblintown route clear |--all\n Remove route overrides.\n\n goblintown country peer add --name --url \n goblintown country peer rm \n goblintown country peer ls\n Manage Goblin-Country peers in warren.json.\n goblintown country show\n Show current country config (backend/mode/code/discoverability/pending requests).\n goblintown country set [--enabled ] [--backend ] [--discoverable ]\n Update country-mode config in warren.json.\n goblintown country discover [--code ] [--server ]\n Query discoverable open countries (3 or fewer members) from a running Goblintown server.\n goblintown country join --country-id --country-code [--target-url ] [--server ]\n Send join request to a discovered country via server API.\n goblintown country requests ls [--server ]\n goblintown country requests approve [--server ]\n goblintown country requests deny [--server ]\n List/resolve pending join requests through server API.\n goblintown country run --task \"...\" [--peer ]... [--all] [--pack ] [--format freeform|markdown|json]\n Dispatch a Rite to peer warrens and wait for completion.\n\n goblintown serve [--port ]\n Start the Hoard web UI. Default port=7777.\n Optional pigeon sprite sheets are loaded from site/assets (walk + peck).\n\nEnvironment:\n OPENAI_API_KEY required (except for init / drift / hoard / inbox / outbox / audit / graph / export / compare / ancestry)\n OPENAI_BASE_URL optional; e.g. https://openrouter.ai/api/v1\n Provider-specific keys OPENROUTER_API_KEY, GROQ_API_KEY, TOGETHER_API_KEY, MISTRAL_API_KEY,\n DEEPSEEK_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY\n GOBLINTOWN_MODEL_GOBLIN default: gpt-5.4-mini\n GOBLINTOWN_MODEL_OGRE default: gpt-5.5\n GOBLINTOWN_MODEL_TROLL default: gpt-5.4-mini\n GOBLINTOWN_MODEL_SCRIBE default: gpt-5.4-mini (Pigeon-as-Scribe artifact distillation)\n GOBLINTOWN_EMBEDDING_MODEL default: text-embedding-3-small (artifact retrieval, Phase 6)\n GOBLINTOWN_TOOLS_HTTP set to 1 to enable http.head verifier tool (default disabled)\n GOBLINTOWN_MAX_CONCURRENCY default: 5 (in-flight API calls)\n GOBLINTOWN_SERVER_URL default base URL for country discover/join/requests commands\n (also: GREMLIN, RACCOON, PIGEON)\n\n\"OpenAI tried to put the goblins back in the box. We built the box for them.\"\n`;\n}\n"}}, "src/providers.ts": {"a5f9571f5c1ec2a6e09ead4adb1fba94e06e77b1": {"t": 1785745565.1106982, "n": 12507, "h": "3f2a90a5384ee310", "c": "import { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { readProviderSecretsFromCwdSync } from \"./provider-secrets.js\";\nimport type {\n ModelSlot,\n OutputFormat,\n ProviderConfig,\n ProviderRouteConfig,\n ProviderPresetId,\n WarrenManifest,\n} from \"./types.js\";\nimport { normalizeOutputFormat } from \"./formatting.js\";\n\nexport const MODEL_SLOTS: ModelSlot[] = [\n \"goblin\",\n \"gremlin\",\n \"raccoon\",\n \"troll\",\n \"ogre\",\n \"pigeon\",\n \"scribe\",\n \"embedding\",\n];\n\ntype Env = Record;\n\nexport interface ProviderPreset {\n id: ProviderPresetId;\n label: string;\n baseURL?: string;\n apiKeyEnv: string;\n apiKeyEnvAliases?: string[];\n local?: boolean;\n dummyApiKey?: string;\n models: Record;\n note?: string;\n}\n\nexport interface ProviderRuntime {\n id: ProviderPresetId;\n label: string;\n baseURL?: string;\n apiKeyEnv: string;\n apiKey: string;\n apiKeySource: \"env\" | \"stored\" | \"dummy\" | \"none\";\n outputFormat: OutputFormat;\n models: Record;\n missingApiKey?: string;\n defaultHeaders?: Record;\n}\n\nconst OPENAI_MODELS: Record = {\n goblin: \"gpt-5-mini\",\n gremlin: \"gpt-5-mini\",\n raccoon: \"gpt-5-mini\",\n troll: \"gpt-5-mini\",\n ogre: \"gpt-5\",\n pigeon: \"gpt-5-mini\",\n scribe: \"gpt-5-mini\",\n embedding: \"text-embedding-3-small\",\n};\n\nfunction withChatModel(\n chatModel: string,\n embeddingModel: string = OPENAI_MODELS.embedding,\n): Record {\n return {\n goblin: chatModel,\n gremlin: chatModel,\n raccoon: chatModel,\n troll: chatModel,\n ogre: chatModel,\n pigeon: chatModel,\n scribe: chatModel,\n embedding: embeddingModel,\n };\n}\n\nexport const PROVIDER_PRESETS: Record = {\n openai: {\n id: \"openai\",\n label: \"OpenAI\",\n apiKeyEnv: \"OPENAI_API_KEY\",\n models: OPENAI_MODELS,\n },\n openrouter: {\n id: \"openrouter\",\n label: \"OpenRouter\",\n baseURL: \"https://openrouter.ai/api/v1\",\n apiKeyEnv: \"OPENROUTER_API_KEY\",\n models: OPENAI_MODELS,\n },\n ollama: {\n id: \"ollama\",\n label: \"Ollama\",\n baseURL: \"http://localhost:11434/v1\",\n apiKeyEnv: \"OLLAMA_API_KEY\",\n local: true,\n dummyApiKey: \"ollama\",\n models: withChatModel(\"llama3.2\", \"nomic-embed-text\"),\n },\n lmstudio: {\n id: \"lmstudio\",\n label: \"LM Studio\",\n baseURL: \"http://localhost:1234/v1\",\n apiKeyEnv: \"LM_API_TOKEN\",\n apiKeyEnvAliases: [\"LMSTUDIO_API_KEY\"],\n local: true,\n dummyApiKey: \"\",\n models: withChatModel(\"local-model\", \"local-embedding-model\"),\n },\n groq: {\n id: \"groq\",\n label: \"Groq\",\n baseURL: \"https://api.groq.com/openai/v1\",\n apiKeyEnv: \"GROQ_API_KEY\",\n models: withChatModel(\"llama-3.3-70b-versatile\"),\n },\n together: {\n id: \"together\",\n label: \"Together AI\",\n baseURL: \"https://api.together.ai/v1\",\n apiKeyEnv: \"TOGETHER_API_KEY\",\n models: withChatModel(\"meta-llama/Llama-3.3-70B-Instruct-Turbo\"),\n },\n mistral: {\n id: \"mistral\",\n label: \"Mistral\",\n baseURL: \"https://api.mistral.ai/v1\",\n apiKeyEnv: \"MISTRAL_API_KEY\",\n models: {\n goblin: \"mistral-small-latest\",\n gremlin: \"mistral-small-latest\",\n raccoon: \"mistral-small-latest\",\n troll: \"mistral-small-latest\",\n ogre: \"mistral-large-latest\",\n pigeon: \"mistral-small-latest\",\n scribe: \"mistral-small-latest\",\n embedding: \"mistral-embed\",\n },\n },\n deepseek: {\n id: \"deepseek\",\n label: \"DeepSeek\",\n baseURL: \"https://api.deepseek.com\",\n apiKeyEnv: \"DEEPSEEK_API_KEY\",\n models: {\n ...withChatModel(\"deepseek-v4-flash\"),\n ogre: \"deepseek-v4-pro\",\n },\n },\n anthropic: {\n id: \"anthropic\",\n label: \"Anthropic\",\n baseURL: \"https://api.anthropic.com/v1/\",\n apiKeyEnv: \"ANTHROPIC_API_KEY\",\n models: withChatModel(\"claude-opus-4-1-20250805\"),\n note: \"Uses Anthropic's OpenAI SDK compatibility layer.\",\n },\n gemini: {\n id: \"gemini\",\n label: \"Gemini\",\n baseURL: \"https://generativelanguage.googleapis.com/v1beta/openai/\",\n apiKeyEnv: \"GEMINI_API_KEY\",\n models: withChatModel(\"gemini-3-flash-preview\", \"gemini-embedding-001\"),\n },\n custom: {\n id: \"custom\",\n label: \"Custom\",\n apiKeyEnv: \"OPENAI_API_KEY\",\n models: OPENAI_MODELS,\n },\n};\n\nexport function defaultProviderConfig(): ProviderConfig {\n return normalizeProviderConfig({ preset: \"openai\" });\n}\n\nexport function normalizeProviderConfig(value: unknown): ProviderConfig {\n const input =\n value && typeof value === \"object\" ? (value as Record) : {};\n const preset = isProviderPresetId(input.preset) ? input.preset : \"openai\";\n const presetInfo = PROVIDER_PRESETS[preset];\n const baseURL = stringOrUndefined(input.baseURL) ?? presetInfo.baseURL;\n const requestedApiKeyEnv = isEnvName(input.apiKeyEnv)\n ? input.apiKeyEnv\n : presetInfo.apiKeyEnv;\n const apiKeyEnv =\n preset === \"lmstudio\" && requestedApiKeyEnv === \"LMSTUDIO_API_KEY\"\n ? presetInfo.apiKeyEnv\n : requestedApiKeyEnv;\n const outputFormat = normalizeOutputFormat(input.outputFormat);\n const models: Partial> = {};\n const rawModels =\n input.models && typeof input.models === \"object\"\n ? (input.models as Record)\n : {};\n for (const slot of MODEL_SLOTS) {\n const model = stringOrUndefined(rawModels[slot]);\n if (model) models[slot] = model;\n }\n const routes: Partial> = {};\n const rawRoutes =\n input.routes && typeof input.routes === \"object\"\n ? (input.routes as Record)\n : {};\n for (const slot of MODEL_SLOTS) {\n const route = normalizeProviderRoute(rawRoutes[slot]);\n if (route) routes[slot] = route;\n }\n\n return {\n preset,\n ...(baseURL ? { baseURL } : {}),\n apiKeyEnv,\n ...(Object.keys(models).length > 0 ? { models } : {}),\n ...(Object.keys(routes).length > 0 ? { routes } : {}),\n outputFormat,\n };\n}\n\nexport function resolveProviderRuntime(\n config: ProviderConfig | undefined,\n env: Env = process.env,\n storedApiKeys: Record | undefined = undefined,\n): ProviderRuntime {\n const normalized = normalizeProviderConfig(config);\n const preset = PROVIDER_PRESETS[normalized.preset];\n const apiKeyEnv = normalized.apiKeyEnv ?? preset.apiKeyEnv;\n const resolved = resolveApiKey(\n apiKeyEnv,\n preset,\n env,\n storedApiKeys ?? readProviderSecretsFromCwdSync(),\n );\n const apiKey = resolved.apiKey;\n const missingApiKey = apiKey || preset.local ? undefined : apiKeyEnv;\n const baseURL = normalized.baseURL ?? env.OPENAI_BASE_URL;\n const referer = env.OPENROUTER_REFERER;\n const defaultHeaders =\n baseURL && /openrouter\\.ai/i.test(baseURL) && referer\n ? {\n \"HTTP-Referer\": referer,\n \"X-OpenRouter-Title\": env.OPENROUTER_TITLE ?? \"Goblintown\",\n }\n : undefined;\n\n return {\n id: normalized.preset,\n label: preset.label,\n baseURL,\n apiKeyEnv,\n apiKey,\n apiKeySource: resolved.source,\n outputFormat: normalized.outputFormat ?? \"freeform\",\n models: {\n ...preset.models,\n ...(normalized.models ?? {}),\n },\n missingApiKey,\n ...(defaultHeaders ? { defaultHeaders } : {}),\n };\n}\n\nexport function resolveModelForSlot(\n slot: ModelSlot,\n fallbackModel: string,\n config: ProviderConfig | undefined = loadProviderConfigFromCwd(),\n env: Env = process.env,\n): string {\n const envModel = modelEnvValue(slot, env);\n if (envModel) return envModel;\n const runtime = resolveProviderRuntimeForSlot(slot, config, env);\n const model = runtime.models[slot] || fallbackModel;\n return resolveOpenRouterModel(model, runtime.baseURL);\n}\n\nexport function resolveProviderRuntimeForSlot(\n slot: ModelSlot,\n config: ProviderConfig | undefined,\n env: Env = process.env,\n storedApiKeys: Record | undefined = undefined,\n): ProviderRuntime {\n const normalized = normalizeProviderConfig(config);\n const route = normalized.routes?.[slot];\n if (!route) return resolveProviderRuntime(normalized, env, storedApiKeys);\n const models: Partial> = {\n ...(normalized.models ?? {}),\n };\n if (route.model) models[slot] = route.model;\n const routeUsesGlobalPreset = route.preset === normalized.preset;\n return resolveProviderRuntime(\n {\n preset: route.preset,\n ...(route.baseURL\n ? { baseURL: route.baseURL }\n : routeUsesGlobalPreset && normalized.baseURL\n ? { baseURL: normalized.baseURL }\n : {}),\n ...(route.apiKeyEnv\n ? { apiKeyEnv: route.apiKeyEnv }\n : routeUsesGlobalPreset && normalized.apiKeyEnv\n ? { apiKeyEnv: normalized.apiKeyEnv }\n : {}),\n outputFormat: route.outputFormat ?? normalized.outputFormat,\n models,\n },\n env,\n storedApiKeys,\n );\n}\n\nexport function loadProviderConfigFromCwd(cwd = process.cwd()): ProviderConfig {\n const manifestPath = findManifestPath(cwd);\n if (!manifestPath) return defaultProviderConfig();\n try {\n const manifest = JSON.parse(readFileSync(manifestPath, \"utf8\")) as WarrenManifest;\n return normalizeProviderConfig(manifest.provider);\n } catch {\n return defaultProviderConfig();\n }\n}\n\nexport function providerRuntimeSignature(runtime: ProviderRuntime): string {\n return JSON.stringify({\n id: runtime.id,\n baseURL: runtime.baseURL,\n apiKeyEnv: runtime.apiKeyEnv,\n apiKey: runtime.apiKey,\n headers: runtime.defaultHeaders,\n });\n}\n\nfunction resolveOpenRouterModel(model: string, baseURL: string | undefined): string {\n if (model.includes(\"/\")) return model;\n if (!baseURL || !/openrouter\\.ai/i.test(baseURL)) return model;\n return `openai/${model}`;\n}\n\nfunction resolveApiKey(\n apiKeyEnv: string,\n preset: ProviderPreset,\n env: Env,\n storedApiKeys: Record,\n): { apiKey: string; source: \"env\" | \"stored\" | \"dummy\" | \"none\" } {\n const openAiFallback =\n apiKeyEnv === \"OPENAI_API_KEY\" || preset.id === \"openai\" || preset.id === \"custom\"\n ? [\"OPENAI_API_KEY\"]\n : [];\n const candidates = [\n apiKeyEnv,\n preset.apiKeyEnv,\n ...(preset.apiKeyEnvAliases ?? []),\n ...openAiFallback,\n ];\n for (const key of Array.from(new Set(candidates))) {\n const value = stringOrUndefined(env[key]);\n if (value) return { apiKey: value, source: \"env\" };\n }\n for (const key of Array.from(new Set(candidates))) {\n const value = stringOrUndefined(storedApiKeys[key]);\n if (value) return { apiKey: value, source: \"stored\" };\n }\n const dummy = (preset.local ? preset.dummyApiKey : undefined) ?? \"\";\n if (dummy) return { apiKey: dummy, source: \"dummy\" };\n return { apiKey: \"\", source: \"none\" };\n}\n\nfunction normalizeProviderRoute(value: unknown): ProviderRouteConfig | null {\n if (!value || typeof value !== \"object\") return null;\n const input = value as Record;\n if (!isProviderPresetId(input.preset)) return null;\n const baseURL = stringOrUndefined(input.baseURL);\n const apiKeyEnv = isEnvName(input.apiKeyEnv) ? input.apiKeyEnv : undefined;\n const model = stringOrUndefined(input.model);\n const outputFormat = normalizeOutputFormat(input.outputFormat);\n return {\n preset: input.preset,\n ...(baseURL ? { baseURL } : {}),\n ...(apiKeyEnv ? { apiKeyEnv } : {}),\n ...(model ? { model } : {}),\n ...(outputFormat ? { outputFormat } : {}),\n };\n}\n\nfunction modelEnvValue(slot: ModelSlot, env: Env): string | undefined {\n const key =\n slot === \"scribe\"\n ? \"GOBLINTOWN_MODEL_SCRIBE\"\n : slot === \"embedding\"\n ? \"GOBLINTOWN_EMBEDDING_MODEL\"\n : `GOBLINTOWN_MODEL_${slot.toUpperCase()}`;\n return stringOrUndefined(env[key]);\n}\n\nfunction isProviderPresetId(value: unknown): value is ProviderPresetId {\n return typeof value === \"string\" && value in PROVIDER_PRESETS;\n}\n\nfunction isEnvName(value: unknown): value is string {\n return typeof value === \"string\" && /^[A-Z_][A-Z0-9_]*$/.test(value);\n}\n\nfunction stringOrUndefined(value: unknown): string | undefined {\n if (typeof value !== \"string\") return undefined;\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n}\n\nfunction findManifestPath(start: string): string | null {\n let cur = start;\n while (true) {\n const candidate = join(cur, \".goblintown\", \"warren.json\");\n if (existsSync(candidate)) return candidate;\n const parent = dirname(cur);\n if (parent === cur) return null;\n cur = parent;\n }\n}\n"}, "ccab1b4ea03479a6008116d2055904bc42d35271": {"t": 1785754274.5966022, "n": 12068, "h": "8d5e3bb7717f54f2", "c": "import { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { readProviderSecretsFromCwdSync } from \"./provider-secrets.js\";\nimport type {\n ModelSlot,\n OutputFormat,\n ProviderConfig,\n ProviderRouteConfig,\n ProviderPresetId,\n WarrenManifest,\n} from \"./types.js\";\nimport { normalizeOutputFormat } from \"./formatting.js\";\n\nexport const MODEL_SLOTS: ModelSlot[] = [\n \"goblin\",\n \"gremlin\",\n \"raccoon\",\n \"troll\",\n \"ogre\",\n \"pigeon\",\n \"scribe\",\n \"embedding\",\n];\n\ntype Env = Record;\n\nexport interface ProviderPreset {\n id: ProviderPresetId;\n label: string;\n baseURL?: string;\n apiKeyEnv: string;\n apiKeyEnvAliases?: string[];\n local?: boolean;\n dummyApiKey?: string;\n models: Record;\n note?: string;\n}\n\nexport interface ProviderRuntime {\n id: ProviderPresetId;\n label: string;\n baseURL?: string;\n apiKeyEnv: string;\n apiKey: string;\n apiKeySource: \"env\" | \"stored\" | \"dummy\" | \"none\";\n outputFormat: OutputFormat;\n models: Record;\n missingApiKey?: string;\n defaultHeaders?: Record;\n}\n\nconst OPENAI_MODELS: Record = {\n goblin: \"gpt-5.4-mini\",\n gremlin: \"gpt-5.4-mini\",\n raccoon: \"gpt-5.4-mini\",\n troll: \"gpt-5.4-mini\",\n ogre: \"gpt-5.5\",\n pigeon: \"gpt-5.4-mini\",\n scribe: \"gpt-5.4-mini\",\n embedding: \"text-embedding-3-small\",\n};\n\nfunction withChatModel(\n chatModel: string,\n embeddingModel: string = OPENAI_MODELS.embedding,\n): Record {\n return {\n goblin: chatModel,\n gremlin: chatModel,\n raccoon: chatModel,\n troll: chatModel,\n ogre: chatModel,\n pigeon: chatModel,\n scribe: chatModel,\n embedding: embeddingModel,\n };\n}\n\nexport const PROVIDER_PRESETS: Record = {\n openai: {\n id: \"openai\",\n label: \"OpenAI\",\n apiKeyEnv: \"OPENAI_API_KEY\",\n models: OPENAI_MODELS,\n },\n openrouter: {\n id: \"openrouter\",\n label: \"OpenRouter\",\n baseURL: \"https://openrouter.ai/api/v1\",\n apiKeyEnv: \"OPENROUTER_API_KEY\",\n models: OPENAI_MODELS,\n },\n ollama: {\n id: \"ollama\",\n label: \"Ollama\",\n baseURL: \"http://localhost:11434/v1\",\n apiKeyEnv: \"OLLAMA_API_KEY\",\n local: true,\n dummyApiKey: \"ollama\",\n models: withChatModel(\"llama3.2\", \"nomic-embed-text\"),\n },\n lmstudio: {\n id: \"lmstudio\",\n label: \"LM Studio\",\n baseURL: \"http://localhost:1234/v1\",\n apiKeyEnv: \"LM_API_TOKEN\",\n apiKeyEnvAliases: [\"LMSTUDIO_API_KEY\"],\n local: true,\n dummyApiKey: \"\",\n models: withChatModel(\"local-model\", \"local-embedding-model\"),\n },\n groq: {\n id: \"groq\",\n label: \"Groq\",\n baseURL: \"https://api.groq.com/openai/v1\",\n apiKeyEnv: \"GROQ_API_KEY\",\n models: withChatModel(\"llama-3.3-70b-versatile\"),\n },\n together: {\n id: \"together\",\n label: \"Together AI\",\n baseURL: \"https://api.together.ai/v1\",\n apiKeyEnv: \"TOGETHER_API_KEY\",\n models: withChatModel(\"meta-llama/Llama-3.3-70B-Instruct-Turbo\"),\n },\n mistral: {\n id: \"mistral\",\n label: \"Mistral\",\n baseURL: \"https://api.mistral.ai/v1\",\n apiKeyEnv: \"MISTRAL_API_KEY\",\n models: {\n goblin: \"mistral-small-latest\",\n gremlin: \"mistral-small-latest\",\n raccoon: \"mistral-small-latest\",\n troll: \"mistral-small-latest\",\n ogre: \"mistral-large-latest\",\n pigeon: \"mistral-small-latest\",\n scribe: \"mistral-small-latest\",\n embedding: \"mistral-embed\",\n },\n },\n deepseek: {\n id: \"deepseek\",\n label: \"DeepSeek\",\n baseURL: \"https://api.deepseek.com\",\n apiKeyEnv: \"DEEPSEEK_API_KEY\",\n models: {\n ...withChatModel(\"deepseek-v4-flash\"),\n ogre: \"deepseek-v4-pro\",\n },\n },\n anthropic: {\n id: \"anthropic\",\n label: \"Anthropic\",\n baseURL: \"https://api.anthropic.com/v1/\",\n apiKeyEnv: \"ANTHROPIC_API_KEY\",\n models: withChatModel(\"claude-opus-4-1-20250805\"),\n note: \"Uses Anthropic's OpenAI SDK compatibility layer.\",\n },\n gemini: {\n id: \"gemini\",\n label: \"Gemini\",\n baseURL: \"https://generativelanguage.googleapis.com/v1beta/openai/\",\n apiKeyEnv: \"GEMINI_API_KEY\",\n models: withChatModel(\"gemini-3-flash-preview\", \"gemini-embedding-001\"),\n },\n custom: {\n id: \"custom\",\n label: \"Custom\",\n apiKeyEnv: \"OPENAI_API_KEY\",\n models: OPENAI_MODELS,\n },\n};\n\nexport function defaultProviderConfig(): ProviderConfig {\n return normalizeProviderConfig({ preset: \"openai\" });\n}\n\nexport function normalizeProviderConfig(value: unknown): ProviderConfig {\n const input =\n value && typeof value === \"object\" ? (value as Record) : {};\n const preset = isProviderPresetId(input.preset) ? input.preset : \"openai\";\n const presetInfo = PROVIDER_PRESETS[preset];\n const baseURL = stringOrUndefined(input.baseURL) ?? presetInfo.baseURL;\n const requestedApiKeyEnv = isEnvName(input.apiKeyEnv)\n ? input.apiKeyEnv\n : presetInfo.apiKeyEnv;\n const apiKeyEnv =\n preset === \"lmstudio\" && requestedApiKeyEnv === \"LMSTUDIO_API_KEY\"\n ? presetInfo.apiKeyEnv\n : requestedApiKeyEnv;\n const outputFormat = normalizeOutputFormat(input.outputFormat);\n const models: Partial> = {};\n const rawModels =\n input.models && typeof input.models === \"object\"\n ? (input.models as Record)\n : {};\n for (const slot of MODEL_SLOTS) {\n const model = stringOrUndefined(rawModels[slot]);\n if (model) models[slot] = model;\n }\n const routes: Partial> = {};\n const rawRoutes =\n input.routes && typeof input.routes === \"object\"\n ? (input.routes as Record)\n : {};\n for (const slot of MODEL_SLOTS) {\n const route = normalizeProviderRoute(rawRoutes[slot]);\n if (route) routes[slot] = route;\n }\n\n return {\n preset,\n ...(baseURL ? { baseURL } : {}),\n apiKeyEnv,\n ...(Object.keys(models).length > 0 ? { models } : {}),\n ...(Object.keys(routes).length > 0 ? { routes } : {}),\n outputFormat,\n };\n}\n\nexport function resolveProviderRuntime(\n config: ProviderConfig | undefined,\n env: Env = process.env,\n storedApiKeys: Record | undefined = undefined,\n): ProviderRuntime {\n const normalized = normalizeProviderConfig(config);\n const preset = PROVIDER_PRESETS[normalized.preset];\n const apiKeyEnv = normalized.apiKeyEnv ?? preset.apiKeyEnv;\n const resolved = resolveApiKey(\n apiKeyEnv,\n preset,\n env,\n storedApiKeys ?? readProviderSecretsFromCwdSync(),\n );\n const apiKey = resolved.apiKey;\n const missingApiKey = apiKey || preset.local ? undefined : apiKeyEnv;\n const baseURL = normalized.baseURL ?? env.OPENAI_BASE_URL;\n const referer = env.OPENROUTER_REFERER;\n const defaultHeaders =\n baseURL && /openrouter\\.ai/i.test(baseURL) && referer\n ? {\n \"HTTP-Referer\": referer,\n \"X-OpenRouter-Title\": env.OPENROUTER_TITLE ?? \"Goblintown\",\n }\n : undefined;\n\n return {\n id: normalized.preset,\n label: preset.label,\n baseURL,\n apiKeyEnv,\n apiKey,\n apiKeySource: resolved.source,\n outputFormat: normalized.outputFormat ?? \"freeform\",\n models: {\n ...preset.models,\n ...(normalized.models ?? {}),\n },\n missingApiKey,\n ...(defaultHeaders ? { defaultHeaders } : {}),\n };\n}\n\nexport function resolveModelForSlot(\n slot: ModelSlot,\n fallbackModel: string,\n config: ProviderConfig | undefined = loadProviderConfigFromCwd(),\n env: Env = process.env,\n): string {\n const envModel = modelEnvValue(slot, env);\n if (envModel) return envModel;\n const runtime = resolveProviderRuntimeForSlot(slot, config, env);\n const model = runtime.models[slot] || fallbackModel;\n return resolveOpenRouterModel(model, runtime.baseURL);\n}\n\nexport function resolveProviderRuntimeForSlot(\n slot: ModelSlot,\n config: ProviderConfig | undefined,\n env: Env = process.env,\n storedApiKeys: Record | undefined = undefined,\n): ProviderRuntime {\n const normalized = normalizeProviderConfig(config);\n const route = normalized.routes?.[slot];\n if (!route) return resolveProviderRuntime(normalized, env, storedApiKeys);\n const models: Partial> = {\n ...(normalized.models ?? {}),\n };\n if (route.model) models[slot] = route.model;\n return resolveProviderRuntime(\n {\n ...normalized,\n preset: route.preset,\n baseURL: route.baseURL ?? normalized.baseURL,\n apiKeyEnv: route.apiKeyEnv ?? normalized.apiKeyEnv,\n outputFormat: route.outputFormat ?? normalized.outputFormat,\n models,\n },\n env,\n storedApiKeys,\n );\n}\n\nexport function loadProviderConfigFromCwd(cwd = process.cwd()): ProviderConfig {\n const manifestPath = findManifestPath(cwd);\n if (!manifestPath) return defaultProviderConfig();\n try {\n const manifest = JSON.parse(readFileSync(manifestPath, \"utf8\")) as WarrenManifest;\n return normalizeProviderConfig(manifest.provider);\n } catch {\n return defaultProviderConfig();\n }\n}\n\nexport function providerRuntimeSignature(runtime: ProviderRuntime): string {\n return JSON.stringify({\n id: runtime.id,\n baseURL: runtime.baseURL,\n apiKeyEnv: runtime.apiKeyEnv,\n apiKey: runtime.apiKey,\n headers: runtime.defaultHeaders,\n });\n}\n\nfunction resolveOpenRouterModel(model: string, baseURL: string | undefined): string {\n if (model.includes(\"/\")) return model;\n if (!baseURL || !/openrouter\\.ai/i.test(baseURL)) return model;\n return `openai/${model}`;\n}\n\nfunction resolveApiKey(\n apiKeyEnv: string,\n preset: ProviderPreset,\n env: Env,\n storedApiKeys: Record,\n): { apiKey: string; source: \"env\" | \"stored\" | \"dummy\" | \"none\" } {\n const candidates = [\n apiKeyEnv,\n preset.apiKeyEnv,\n ...(preset.apiKeyEnvAliases ?? []),\n \"OPENAI_API_KEY\",\n ];\n for (const key of Array.from(new Set(candidates))) {\n const value = stringOrUndefined(env[key]);\n if (value) return { apiKey: value, source: \"env\" };\n }\n for (const key of Array.from(new Set(candidates))) {\n const value = stringOrUndefined(storedApiKeys[key]);\n if (value) return { apiKey: value, source: \"stored\" };\n }\n const dummy = (preset.local ? preset.dummyApiKey : undefined) ?? \"\";\n if (dummy) return { apiKey: dummy, source: \"dummy\" };\n return { apiKey: \"\", source: \"none\" };\n}\n\nfunction normalizeProviderRoute(value: unknown): ProviderRouteConfig | null {\n if (!value || typeof value !== \"object\") return null;\n const input = value as Record;\n if (!isProviderPresetId(input.preset)) return null;\n const baseURL = stringOrUndefined(input.baseURL);\n const apiKeyEnv = isEnvName(input.apiKeyEnv) ? input.apiKeyEnv : undefined;\n const model = stringOrUndefined(input.model);\n const outputFormat = normalizeOutputFormat(input.outputFormat);\n return {\n preset: input.preset,\n ...(baseURL ? { baseURL } : {}),\n ...(apiKeyEnv ? { apiKeyEnv } : {}),\n ...(model ? { model } : {}),\n ...(outputFormat ? { outputFormat } : {}),\n };\n}\n\nfunction modelEnvValue(slot: ModelSlot, env: Env): string | undefined {\n const key =\n slot === \"scribe\"\n ? \"GOBLINTOWN_MODEL_SCRIBE\"\n : slot === \"embedding\"\n ? \"GOBLINTOWN_EMBEDDING_MODEL\"\n : `GOBLINTOWN_MODEL_${slot.toUpperCase()}`;\n return stringOrUndefined(env[key]);\n}\n\nfunction isProviderPresetId(value: unknown): value is ProviderPresetId {\n return typeof value === \"string\" && value in PROVIDER_PRESETS;\n}\n\nfunction isEnvName(value: unknown): value is string {\n return typeof value === \"string\" && /^[A-Z_][A-Z0-9_]*$/.test(value);\n}\n\nfunction stringOrUndefined(value: unknown): string | undefined {\n if (typeof value !== \"string\") return undefined;\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n}\n\nfunction findManifestPath(start: string): string | null {\n let cur = start;\n while (true) {\n const candidate = join(cur, \".goblintown\", \"warren.json\");\n if (existsSync(candidate)) return candidate;\n const parent = dirname(cur);\n if (parent === cur) return null;\n cur = parent;\n }\n}\n"}, "bd64630875454cc951ddfa6f4bd49c92a8804b09": {"t": 1785754276.0009384, "n": 9357, "h": "8c5e3f27b47ae813", "c": "import { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport type {\n ModelSlot,\n OutputFormat,\n ProviderConfig,\n ProviderPresetId,\n WarrenManifest,\n} from \"./types.js\";\nimport { normalizeOutputFormat } from \"./formatting.js\";\n\nexport const MODEL_SLOTS: ModelSlot[] = [\n \"goblin\",\n \"gremlin\",\n \"raccoon\",\n \"troll\",\n \"ogre\",\n \"pigeon\",\n \"scribe\",\n \"embedding\",\n];\n\ntype Env = Record;\n\nexport interface ProviderPreset {\n id: ProviderPresetId;\n label: string;\n baseURL?: string;\n apiKeyEnv: string;\n apiKeyEnvAliases?: string[];\n local?: boolean;\n dummyApiKey?: string;\n models: Record;\n note?: string;\n}\n\nexport interface ProviderRuntime {\n id: ProviderPresetId;\n label: string;\n baseURL?: string;\n apiKeyEnv: string;\n apiKey: string;\n outputFormat: OutputFormat;\n models: Record;\n missingApiKey?: string;\n defaultHeaders?: Record;\n}\n\nconst OPENAI_MODELS: Record = {\n goblin: \"gpt-5.4-mini\",\n gremlin: \"gpt-5.4-mini\",\n raccoon: \"gpt-5.4-mini\",\n troll: \"gpt-5.4-mini\",\n ogre: \"gpt-5.5\",\n pigeon: \"gpt-5.4-mini\",\n scribe: \"gpt-5.4-mini\",\n embedding: \"text-embedding-3-small\",\n};\n\nfunction withChatModel(\n chatModel: string,\n embeddingModel: string = OPENAI_MODELS.embedding,\n): Record {\n return {\n goblin: chatModel,\n gremlin: chatModel,\n raccoon: chatModel,\n troll: chatModel,\n ogre: chatModel,\n pigeon: chatModel,\n scribe: chatModel,\n embedding: embeddingModel,\n };\n}\n\nexport const PROVIDER_PRESETS: Record = {\n openai: {\n id: \"openai\",\n label: \"OpenAI\",\n apiKeyEnv: \"OPENAI_API_KEY\",\n models: OPENAI_MODELS,\n },\n openrouter: {\n id: \"openrouter\",\n label: \"OpenRouter\",\n baseURL: \"https://openrouter.ai/api/v1\",\n apiKeyEnv: \"OPENROUTER_API_KEY\",\n models: OPENAI_MODELS,\n },\n ollama: {\n id: \"ollama\",\n label: \"Ollama\",\n baseURL: \"http://localhost:11434/v1\",\n apiKeyEnv: \"OLLAMA_API_KEY\",\n local: true,\n dummyApiKey: \"ollama\",\n models: withChatModel(\"llama3.2\", \"nomic-embed-text\"),\n },\n lmstudio: {\n id: \"lmstudio\",\n label: \"LM Studio\",\n baseURL: \"http://localhost:1234/v1\",\n apiKeyEnv: \"LM_API_TOKEN\",\n apiKeyEnvAliases: [\"LMSTUDIO_API_KEY\"],\n local: true,\n dummyApiKey: \"\",\n models: withChatModel(\"local-model\", \"local-embedding-model\"),\n },\n groq: {\n id: \"groq\",\n label: \"Groq\",\n baseURL: \"https://api.groq.com/openai/v1\",\n apiKeyEnv: \"GROQ_API_KEY\",\n models: withChatModel(\"llama-3.3-70b-versatile\"),\n },\n together: {\n id: \"together\",\n label: \"Together AI\",\n baseURL: \"https://api.together.ai/v1\",\n apiKeyEnv: \"TOGETHER_API_KEY\",\n models: withChatModel(\"meta-llama/Llama-3.3-70B-Instruct-Turbo\"),\n },\n mistral: {\n id: \"mistral\",\n label: \"Mistral\",\n baseURL: \"https://api.mistral.ai/v1\",\n apiKeyEnv: \"MISTRAL_API_KEY\",\n models: {\n goblin: \"mistral-small-latest\",\n gremlin: \"mistral-small-latest\",\n raccoon: \"mistral-small-latest\",\n troll: \"mistral-small-latest\",\n ogre: \"mistral-large-latest\",\n pigeon: \"mistral-small-latest\",\n scribe: \"mistral-small-latest\",\n embedding: \"mistral-embed\",\n },\n },\n deepseek: {\n id: \"deepseek\",\n label: \"DeepSeek\",\n baseURL: \"https://api.deepseek.com\",\n apiKeyEnv: \"DEEPSEEK_API_KEY\",\n models: {\n ...withChatModel(\"deepseek-v4-flash\"),\n ogre: \"deepseek-v4-pro\",\n },\n },\n anthropic: {\n id: \"anthropic\",\n label: \"Anthropic\",\n baseURL: \"https://api.anthropic.com/v1/\",\n apiKeyEnv: \"ANTHROPIC_API_KEY\",\n models: withChatModel(\"claude-opus-4-1-20250805\"),\n note: \"Uses Anthropic's OpenAI SDK compatibility layer.\",\n },\n gemini: {\n id: \"gemini\",\n label: \"Gemini\",\n baseURL: \"https://generativelanguage.googleapis.com/v1beta/openai/\",\n apiKeyEnv: \"GEMINI_API_KEY\",\n models: withChatModel(\"gemini-3-flash-preview\", \"gemini-embedding-001\"),\n },\n custom: {\n id: \"custom\",\n label: \"Custom\",\n apiKeyEnv: \"OPENAI_API_KEY\",\n models: OPENAI_MODELS,\n },\n};\n\nexport function defaultProviderConfig(): ProviderConfig {\n return normalizeProviderConfig({ preset: \"openai\" });\n}\n\nexport function normalizeProviderConfig(value: unknown): ProviderConfig {\n const input =\n value && typeof value === \"object\" ? (value as Record) : {};\n const preset = isProviderPresetId(input.preset) ? input.preset : \"openai\";\n const presetInfo = PROVIDER_PRESETS[preset];\n const baseURL = stringOrUndefined(input.baseURL) ?? presetInfo.baseURL;\n const requestedApiKeyEnv = isEnvName(input.apiKeyEnv)\n ? input.apiKeyEnv\n : presetInfo.apiKeyEnv;\n const apiKeyEnv =\n preset === \"lmstudio\" && requestedApiKeyEnv === \"LMSTUDIO_API_KEY\"\n ? presetInfo.apiKeyEnv\n : requestedApiKeyEnv;\n const outputFormat = normalizeOutputFormat(input.outputFormat);\n const models: Partial> = {};\n const rawModels =\n input.models && typeof input.models === \"object\"\n ? (input.models as Record)\n : {};\n for (const slot of MODEL_SLOTS) {\n const model = stringOrUndefined(rawModels[slot]);\n if (model) models[slot] = model;\n }\n\n return {\n preset,\n ...(baseURL ? { baseURL } : {}),\n apiKeyEnv,\n ...(Object.keys(models).length > 0 ? { models } : {}),\n outputFormat,\n };\n}\n\nexport function resolveProviderRuntime(\n config: ProviderConfig | undefined,\n env: Env = process.env,\n): ProviderRuntime {\n const normalized = normalizeProviderConfig(config);\n const preset = PROVIDER_PRESETS[normalized.preset];\n const apiKeyEnv = normalized.apiKeyEnv ?? preset.apiKeyEnv;\n const apiKey = resolveApiKey(apiKeyEnv, preset, env);\n const missingApiKey = apiKey || preset.local ? undefined : apiKeyEnv;\n const baseURL = normalized.baseURL ?? env.OPENAI_BASE_URL;\n const referer = env.OPENROUTER_REFERER;\n const defaultHeaders =\n baseURL && /openrouter\\.ai/i.test(baseURL) && referer\n ? {\n \"HTTP-Referer\": referer,\n \"X-OpenRouter-Title\": env.OPENROUTER_TITLE ?? \"Goblintown\",\n }\n : undefined;\n\n return {\n id: normalized.preset,\n label: preset.label,\n baseURL,\n apiKeyEnv,\n apiKey,\n outputFormat: normalized.outputFormat ?? \"freeform\",\n models: {\n ...preset.models,\n ...(normalized.models ?? {}),\n },\n missingApiKey,\n ...(defaultHeaders ? { defaultHeaders } : {}),\n };\n}\n\nexport function resolveModelForSlot(\n slot: ModelSlot,\n fallbackModel: string,\n config: ProviderConfig | undefined = loadProviderConfigFromCwd(),\n env: Env = process.env,\n): string {\n const envModel = modelEnvValue(slot, env);\n if (envModel) return envModel;\n const runtime = resolveProviderRuntime(config, env);\n const model = runtime.models[slot] || fallbackModel;\n return resolveOpenRouterModel(model, runtime.baseURL);\n}\n\nexport function loadProviderConfigFromCwd(cwd = process.cwd()): ProviderConfig {\n const manifestPath = findManifestPath(cwd);\n if (!manifestPath) return defaultProviderConfig();\n try {\n const manifest = JSON.parse(readFileSync(manifestPath, \"utf8\")) as WarrenManifest;\n return normalizeProviderConfig(manifest.provider);\n } catch {\n return defaultProviderConfig();\n }\n}\n\nexport function providerRuntimeSignature(runtime: ProviderRuntime): string {\n return JSON.stringify({\n id: runtime.id,\n baseURL: runtime.baseURL,\n apiKeyEnv: runtime.apiKeyEnv,\n apiKey: runtime.apiKey,\n headers: runtime.defaultHeaders,\n });\n}\n\nfunction resolveOpenRouterModel(model: string, baseURL: string | undefined): string {\n if (model.includes(\"/\")) return model;\n if (!baseURL || !/openrouter\\.ai/i.test(baseURL)) return model;\n return `openai/${model}`;\n}\n\nfunction resolveApiKey(\n apiKeyEnv: string,\n preset: ProviderPreset,\n env: Env,\n): string {\n const candidates = [\n apiKeyEnv,\n preset.apiKeyEnv,\n ...(preset.apiKeyEnvAliases ?? []),\n \"OPENAI_API_KEY\",\n ];\n for (const key of Array.from(new Set(candidates))) {\n const value = stringOrUndefined(env[key]);\n if (value) return value;\n }\n return (preset.local ? preset.dummyApiKey : undefined) ?? \"\";\n}\n\nfunction modelEnvValue(slot: ModelSlot, env: Env): string | undefined {\n const key =\n slot === \"scribe\"\n ? \"GOBLINTOWN_MODEL_SCRIBE\"\n : slot === \"embedding\"\n ? \"GOBLINTOWN_EMBEDDING_MODEL\"\n : `GOBLINTOWN_MODEL_${slot.toUpperCase()}`;\n return stringOrUndefined(env[key]);\n}\n\nfunction isProviderPresetId(value: unknown): value is ProviderPresetId {\n return typeof value === \"string\" && value in PROVIDER_PRESETS;\n}\n\nfunction isEnvName(value: unknown): value is string {\n return typeof value === \"string\" && /^[A-Z_][A-Z0-9_]*$/.test(value);\n}\n\nfunction stringOrUndefined(value: unknown): string | undefined {\n if (typeof value !== \"string\") return undefined;\n const trimmed = value.trim();\n return trimmed.length > 0 ? trimmed : undefined;\n}\n\nfunction findManifestPath(start: string): string | null {\n let cur = start;\n while (true) {\n const candidate = join(cur, \".goblintown\", \"warren.json\");\n if (existsSync(candidate)) return candidate;\n const parent = dirname(cur);\n if (parent === cur) return null;\n cur = parent;\n }\n}\n"}}}}