From 1d1daf0151bc4cfe8d8484da6865d0edddf90f5e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Jul 2026 02:14:17 +0000 Subject: [PATCH 1/4] Prepare gbrain keyless-mode fix for garrytan/gbrain#94 git am-ready patch adding 'gbrain init --mode claude-code': zero-API-key operation for Claude Code subscribers (PGLite + keyword/graph search, chat via the claude-cli OAuth recipe), plus two related upstream bug fixes. See work/gbrain-no-api-keys/README.md for scope and verification. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01WQ7za53ZWxWqL2XAfSgTux --- .../0001-claude-code-keyless-mode.patch | 583 ++++++++++++++++++ work/gbrain-no-api-keys/README.md | 87 +++ 2 files changed, 670 insertions(+) create mode 100644 work/gbrain-no-api-keys/0001-claude-code-keyless-mode.patch create mode 100644 work/gbrain-no-api-keys/README.md diff --git a/work/gbrain-no-api-keys/0001-claude-code-keyless-mode.patch b/work/gbrain-no-api-keys/0001-claude-code-keyless-mode.patch new file mode 100644 index 0000000..f5d44ba --- /dev/null +++ b/work/gbrain-no-api-keys/0001-claude-code-keyless-mode.patch @@ -0,0 +1,583 @@ +From 4e3c2a8071e536a4959b6f974a7f328b6a56c010 Mon Sep 17 00:00:00 2001 +From: Claude +Date: Tue, 28 Jul 2026 02:13:39 +0000 +Subject: [PATCH] =?UTF-8?q?feat(init):=20--mode=20claude-code=20=E2=80=94?= + =?UTF-8?q?=20keyless=20mode=20for=20Claude=20Code=20subscribers=20(#94)?= +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +A Claude Code subscriber gets a working brain with zero provider API +keys: PGLite engine, keyword + graph + title search (hybrid search's +existing no-embedding-provider path), chat via the claude-cli recipe's +OAuth-session subprocess. + +- init: new --mode claude-code flag. Implies --pglite, writes + claude_code_mode: true alongside embedding_disabled: true, defaults + chat_model to claude-cli:claude-sonnet-4-6, seeds + search.mode=conservative (the one bundle with no reranker / LLM + expansion spend), and best-effort-checks the claude binary on PATH. +- import: a claude-code brain imports without vectors (implicit + --no-embed) instead of refusing with the D9 deferred-setup error. +- embed: refusal message tailored to keyless mode with the vector + upgrade recipe instead of the deferred-setup hint. +- advisor: embedding_disabled is not a setup smell when the brain is + keyless by design. +- fix: explicit --embedding-model/--model now clears a persisted + deferred/keyless sentinel — the documented upgrade path + (gbrain init --force --embedding-model ...) previously lost to the + sentinel and was silently ignored; a resolved model also removes + stale sentinels from config.json. +- fix: the post-init subagent-Anthropic caveat no longer fires for + claude-cli:* chat models (the recipe drives the subagent loop through + the OAuth session; no ANTHROPIC_API_KEY involved). + +Verified end-to-end with all provider keys unset: init -> import -> +search (keyword arm) -> embed refusal -> re-init upgrade path. + +Co-Authored-By: Claude Fable 5 +Claude-Session: https://claude.ai/code/session_01WQ7za53ZWxWqL2XAfSgTux +--- + CHANGELOG.md | 11 ++ + README.md | 9 ++ + src/commands/import.ts | 13 ++- + src/commands/init.ts | 124 +++++++++++++++++++++-- + src/core/advisor/collect-setup-smells.ts | 6 +- + src/core/config.ts | 15 +++ + src/core/embedding-dim-check.ts | 27 ++++- + test/init-claude-code-mode.test.ts | 111 ++++++++++++++++++++ + 8 files changed, 303 insertions(+), 13 deletions(-) + create mode 100644 test/init-claude-code-mode.test.ts + +diff --git a/CHANGELOG.md b/CHANGELOG.md +index 0a8784a..3ce7967 100644 +--- a/CHANGELOG.md ++++ b/CHANGELOG.md +@@ -2,6 +2,17 @@ + + All notable changes to GBrain will be documented in this file. + ++## [Unreleased] ++ ++### Added ++ ++- `gbrain init --mode claude-code` (#94): Claude Code-native keyless mode. No provider API keys required — PGLite engine, keyword + graph + title search via hybrid search's existing no-embedding-provider path, chat through the `claude-cli` recipe's OAuth session. Writes a `claude_code_mode: true` sentinel alongside `embedding_disabled: true`; `gbrain import` on such a brain proceeds without vectors (implicit `--no-embed`) instead of refusing, `search.mode` defaults to `conservative` (the one bundle with no reranker or LLM-expansion spend), and doctor/advisor treat the missing embedding provider as intentional rather than an unfinished setup. ++ ++### Fixed ++ ++- `gbrain init --force --embedding-model …` on a `--no-embedding` (deferred-setup) brain now actually configures the embedding provider — previously the persisted sentinel silently won over the explicit flag, leaving the brain deferred despite the documented upgrade path. Explicit embedding flags now clear the sentinel, and a resolved model removes stale `embedding_disabled` / `claude_code_mode` from config.json. ++- The post-init "subagent features require ANTHROPIC_API_KEY" caveat no longer fires for `claude-cli:*` chat models — that recipe drives the subagent loop through the Claude Code OAuth session and needs no key. ++ + ## [0.42.66.1] - 2026-07-27 + + ### Fixed +diff --git a/README.md b/README.md +index 2cd2657..dcab2b4 100644 +--- a/README.md ++++ b/README.md +@@ -96,6 +96,15 @@ gbrain init --pglite # 2-second local brain (no Docker) + claude mcp add gbrain -- gbrain serve # or: codex mcp add gbrain -- gbrain serve + ``` + ++**No API keys at all?** Claude Code subscribers can run fully keyless (#94) — no `OPENAI_API_KEY`, no `ANTHROPIC_API_KEY`, nothing: ++ ++```bash ++gbrain init --mode claude-code # keyless: keyword + graph search ++claude mcp add gbrain -- gbrain serve ++``` ++ ++Search runs on Postgres FTS + the knowledge graph + title matching (no vector embeddings); chat commands (`gbrain think`) route through your existing `claude` CLI login via the `claude-cli` recipe — billed to your subscription, not per token. Upgrade to vector search any time: `gbrain init --force --pglite --embedding-model :`, then `gbrain embed --stale`. ++ + **Already have a brain on a remote host** (OpenClaw, Hermes, or any `gbrain serve --http`)? Point your laptop agents at it with one command each — `--install` wires it up and smoke-tests the token before handoff: + + ```bash +diff --git a/src/commands/import.ts b/src/commands/import.ts +index 8d50341..e12394b 100644 +--- a/src/commands/import.ts ++++ b/src/commands/import.ts +@@ -73,11 +73,22 @@ export async function runImport( + slugRoot?: string; + } = {}, + ): Promise { +- const noEmbed = args.includes('--no-embed'); ++ let noEmbed = args.includes('--no-embed'); + const fresh = args.includes('--fresh'); + const jsonOutput = args.includes('--json'); + const includeGitignored = args.includes('--include-gitignored') || opts.includeGitignored === true; + ++ // #94 — Claude Code keyless mode: embedding is off BY DESIGN, so import ++ // proceeds without vectors (implicit --no-embed) instead of refusing. ++ // Chunks land keyword-searchable; the graph + FTS arms serve queries. ++ if (!noEmbed) { ++ const { isKeylessBrain } = await import('../core/embedding-dim-check.ts'); ++ const { loadConfig } = await import('../core/config.ts'); ++ if (isKeylessBrain(loadConfig())) { ++ noEmbed = true; ++ } ++ } ++ + // T7 (D9): refuse cleanly when init persisted the deferred-setup sentinel, + // unless the user is explicitly skipping embedding via `--no-embed` (in + // which case the chunks land without vectors and the user can backfill +diff --git a/src/commands/init.ts b/src/commands/init.ts +index ca01c61..91f3ed3 100644 +--- a/src/commands/init.ts ++++ b/src/commands/init.ts +@@ -51,6 +51,23 @@ export async function runInit(args: string[]) { + ? args[schemaPackIdx + 1] + : 'gbrain-base-v2'; + ++ // #94 — `--mode claude-code`: Claude Code-native keyless init. No provider ++ // API keys required: PGLite engine, no embedding provider (keyword/FTS + ++ // graph + title search via hybrid's existing no-embedding-provider path), ++ // chat through the `claude-cli` recipe (Claude Code OAuth session). ++ const modeIdx = args.indexOf('--mode'); ++ const initMode = modeIdx !== -1 ? args[modeIdx + 1] : null; ++ if (initMode !== null && initMode !== 'claude-code') { ++ failInitFlag(`gbrain init: unknown --mode "${initMode}" (supported: claude-code)`, jsonOutput); ++ } ++ const isClaudeCode = initMode === 'claude-code'; ++ if (isClaudeCode && (isSupabase || manualUrl || isMcpOnly)) { ++ failInitFlag( ++ 'gbrain init: --mode claude-code implies a local PGLite brain; drop --supabase/--url/--mcp-only', ++ jsonOutput, ++ ); ++ } ++ + // Multi-topology v1: thin-client init. Skips local engine entirely; writes + // remote_mcp config that the CLI dispatch guard reads to refuse DB-bound ops. + if (isMcpOnly) { +@@ -108,13 +125,14 @@ export async function runInit(args: string[]) { + expansion: expModelIdx !== -1 ? args[expModelIdx + 1] : null, + chat: chatModelIdx !== -1 ? args[chatModelIdx + 1] : null, + noEmbedding, ++ claudeCode: isClaudeCode, + nonInteractive: isNonInteractive, + }); + +- // Explicit PGLite mode +- if (isPGLite || (!isSupabase && !manualUrl && !isNonInteractive)) { ++ // Explicit PGLite mode (--mode claude-code implies it) ++ if (isPGLite || isClaudeCode || (!isSupabase && !manualUrl && !isNonInteractive)) { + // Smart detection: scan for .md files unless --pglite flag forces it +- if (!isPGLite && !isSupabase) { ++ if (!isPGLite && !isClaudeCode && !isSupabase) { + const fileCount = countMarkdownFiles(process.cwd()); + if (fileCount >= 1000) { + console.log(`Found ~${fileCount} .md files. For a brain this size, Supabase gives faster`); +@@ -169,6 +187,7 @@ const INIT_VALUE_FLAGS = new Set([ + '--url', + '--key', + '--path', ++ '--mode', + '--schema-pack', + '--embedding-model', + '--model', +@@ -219,6 +238,7 @@ interface ResolveAIOptionsArgs { + expansion: string | null; // --expansion-model + chat: string | null; // --chat-model + noEmbedding: boolean; // --no-embedding (D9) ++ claudeCode: boolean; // --mode claude-code (#94) + nonInteractive: boolean; // --non-interactive (forces D3 fail-loud, no picker) + } + +@@ -229,8 +249,17 @@ export interface ResolvedAIOptions { + chat_model?: string; + /** v0.37 (D9): user opted into deferred embedding setup. */ + noEmbedding?: boolean; ++ /** #94: Claude Code-native keyless mode (`--mode claude-code`). */ ++ claudeCode?: boolean; + } + ++/** ++ * #94 — default chat model for `--mode claude-code`. Sonnet-tier via the ++ * `claude-cli` recipe (Claude Code OAuth session, no ANTHROPIC_API_KEY). ++ * The recipe's aliases keep `claude-cli:sonnet` etc. working for overrides. ++ */ ++export const CLAUDE_CODE_DEFAULT_CHAT_MODEL = 'claude-cli:claude-sonnet-4-6'; ++ + /** + * Seed init's AI options from persisted config, falling back to the raw env + * vars when loadConfig() returned null (#1058). On a cold install (no +@@ -257,6 +286,9 @@ export function seedAIOptionsFromConfig( + const out: ResolvedAIOptions = {}; + if (seed.embedding_disabled) { + out.noEmbedding = true; ++ // #94: a claude-code brain re-inits back into claude-code mode without ++ // needing --mode claude-code on every invocation. ++ if ((seed as { claude_code_mode?: boolean }).claude_code_mode) out.claudeCode = true; + } else if (seed.embedding_model) { + out.embedding_model = seed.embedding_model; + if (seed.embedding_dimensions) out.embedding_dimensions = seed.embedding_dimensions; +@@ -283,7 +315,7 @@ export function seedAIOptionsFromConfig( + * persists nulls; embed callsites refuse with a config-set hint. + */ + async function resolveAIOptions(opts: ResolveAIOptionsArgs): Promise { +- const { verbose, shorthand, dimsArg, expansion, chat, noEmbedding, nonInteractive } = opts; ++ const { verbose, shorthand, dimsArg, expansion, chat, noEmbedding, claudeCode, nonInteractive } = opts; + const out: ResolvedAIOptions = {}; + + // --- D5: persisted config wins on re-init ----------------------------------- +@@ -312,6 +344,13 @@ async function resolveAIOptions(opts: ResolveAIOptionsArgs): Promise 0) { +@@ -386,6 +428,19 @@ async function resolveAIOptions(opts: ResolveAIOptionsArgs): Promise`)'); ++ console.error(''); ++ console.error('Or run keyless with Claude Code (no API keys at all):'); ++ console.error(' gbrain init --mode claude-code'); ++ console.error(' (keyword + graph search; chat via your Claude Code subscription)'); + // D13: surface near-miss env vars (e.g. OPENAPI_API_KEY → OPENAI_API_KEY). + if (typos.length > 0) { + console.error(''); +@@ -901,7 +960,23 @@ async function initPGLite(opts: { + // gateway's resolved dim by construction — preflight validates that. + let resolvedDim: number | undefined; + let resolvedModel: string | undefined; +- if (opts.aiOpts?.noEmbedding) { ++ if (opts.aiOpts?.claudeCode) { ++ // #94 claude-code keyless mode: embedding is off by design, not deferred. ++ console.log(` Claude Code mode: no API keys — keyword + graph search, chat via the \`claude\` CLI`); ++ // Best-effort binary check (same pattern as the supabase CLI probe): ++ // warn loudly now instead of failing at first `gbrain think`. ++ const claudeBin = process.env.GBRAIN_CLAUDE_CLI_BIN ?? 'claude'; ++ try { ++ execSync(`${claudeBin} --version`, { stdio: 'pipe' }); ++ } catch { ++ console.warn(''); ++ console.warn(` Heads up: \`${claudeBin}\` CLI not found on PATH.`); ++ console.warn(' Import and search work without it, but chat commands (gbrain think)'); ++ console.warn(' need Claude Code installed and logged in:'); ++ console.warn(' npm install -g @anthropic-ai/claude-code && claude'); ++ console.warn(' Or point at the binary: export GBRAIN_CLAUDE_CLI_BIN=/path/to/claude'); ++ } ++ } else if (opts.aiOpts?.noEmbedding) { + // D9 deferred-setup mode: skip preflight, no model/dim resolved. + console.log(` --no-embedding: deferred setup — configure with \`gbrain config set embedding_model \` before import`); + } else if (opts.aiOpts?.embedding_model) { +@@ -1031,7 +1106,7 @@ async function initPGLite(opts: { + database_path: dbPath, + ...(opts.apiKey ? { openai_api_key: opts.apiKey } : {}), + ...(opts.aiOpts?.noEmbedding +- ? { embedding_disabled: true } ++ ? { embedding_disabled: true, ...(opts.aiOpts?.claudeCode ? { claude_code_mode: true } : {}) } + : (resolvedModel && resolvedDim) + ? { embedding_model: resolvedModel, embedding_dimensions: resolvedDim } + : {}), +@@ -1042,6 +1117,15 @@ async function initPGLite(opts: { + // unless explicitly overridden by --schema-pack on re-init. + ...(opts.schemaPack ? { schema_pack: opts.schemaPack } : {}), + }; ++ // #94: the sentinels and a configured embedding model are mutually ++ // exclusive ("init writes one or the other, never both"). When this ++ // invocation resolves a real model — the documented upgrade path out of ++ // deferred/keyless mode — clear stale sentinels the existingFile spread ++ // carried forward. ++ if (config.embedding_model) { ++ delete config.embedding_disabled; ++ delete config.claude_code_mode; ++ } + // PR1: new installs publish their skill catalog over MCP by default + // (existing config wins on re-init, so a prior opt-out is preserved). + config.mcp = { publish_skills: true, ...(config.mcp ?? {}) }; +@@ -1058,12 +1142,30 @@ async function initPGLite(opts: { + + // T6 (D7): post-init subagent-Anthropic caveat. Fires for both auto-pick + // and picker paths so users see the implication of running on a chat +- // provider that can't drive the subagent loop. +- if (opts.aiOpts?.chat_model && !opts.aiOpts.chat_model.startsWith('anthropic:') && !process.env.ANTHROPIC_API_KEY) { ++ // provider that can't drive the subagent loop. claude-cli is exempt: ++ // its recipe declares supports_subagent_loop and drives Minions through ++ // the Claude Code OAuth session — the caveat would be flat wrong (#94). ++ if (opts.aiOpts?.chat_model && !opts.aiOpts.chat_model.startsWith('anthropic:') && !opts.aiOpts.chat_model.startsWith('claude-cli:') && !process.env.ANTHROPIC_API_KEY) { + const { printSubagentAnthropicCaveat } = await import('./init-provider-picker.ts'); + printSubagentAnthropicCaveat((s) => process.stderr.write(s)); + } + ++ // #94: claude-code mode pre-seeds `search.mode = conservative` — the one ++ // bundle with no reranker (needs ZEROENTROPY_API_KEY) and no LLM ++ // expansion, i.e. the only bundle whose every knob works keylessly. ++ // Seeding before the picker makes the picker skip (idempotence contract); ++ // an existing operator-set mode is never overwritten. ++ if (opts.aiOpts?.claudeCode) { ++ try { ++ const { SEARCH_MODE_KEY } = await import('../core/search/mode.ts'); ++ const existingMode = await engine.getConfig(SEARCH_MODE_KEY); ++ if (!existingMode) { ++ await engine.setConfig(SEARCH_MODE_KEY, 'conservative'); ++ console.log(' Search mode: conservative (keyless — no reranker / LLM expansion)'); ++ } ++ } catch { /* best-effort; picker below still runs */ } ++ } ++ + // v0.32.3 search-lite install-time mode picker. Runs AFTER initSchema so + // DB config writes are valid. Idempotent: skipped on re-init if already set. + // Non-TTY auto-selects; --json emits a structured event. +@@ -1602,11 +1704,15 @@ OPTIONS + --chat-model + Default subagent driver (v0.27+) + --no-embedding Defer embedding setup (skips the embedding-key check) ++ --mode claude-code Keyless mode for Claude Code subscribers (#94): PGLite, ++ keyword + graph search (no embedding provider), chat via ++ the \`claude\` CLI OAuth session. No API keys required. + --skip-embed-check Skip the init-time embedding-key validation (config + + live test-embed). Also via GBRAIN_INIT_SKIP_EMBED_CHECK=1 + + EXAMPLES +- gbrain init --pglite # Local-only, no API keys ++ gbrain init --pglite # Local-only Postgres, embedding key needed ++ gbrain init --mode claude-code # Fully keyless (Claude Code subscription) + gbrain init --supabase # Interactive Supabase setup + gbrain init --url postgresql://... # Use a custom Postgres + gbrain init --mcp-only --url https://... # Thin-client mode +diff --git a/src/core/advisor/collect-setup-smells.ts b/src/core/advisor/collect-setup-smells.ts +index de7884e..c5fdfee 100644 +--- a/src/core/advisor/collect-setup-smells.ts ++++ b/src/core/advisor/collect-setup-smells.ts +@@ -25,8 +25,10 @@ export const collectSetupSmells: AdvisorCollector = { + const findings: AdvisorFinding[] = []; + const cfg = ctx.config ?? ({} as typeof ctx.config); + +- // Embeddings disabled — deferred setup never completed. +- if (cfg.embedding_disabled === true) { ++ // Embeddings disabled — deferred setup never completed. Exempt ++ // claude-code keyless brains (#94): there, no embedding provider is the ++ // designed steady state, not an unfinished setup. ++ if (cfg.embedding_disabled === true && cfg.claude_code_mode !== true) { + findings.push({ + id: 'embeddings_disabled', + severity: 'warn', +diff --git a/src/core/config.ts b/src/core/config.ts +index e92b62a..4b938cb 100644 +--- a/src/core/config.ts ++++ b/src/core/config.ts +@@ -81,6 +81,20 @@ export interface GBrainConfig { + * or the other, never both. + */ + embedding_disabled?: boolean; ++ /** ++ * Claude Code-native keyless mode (#94): the brain was initialized with ++ * `gbrain init --mode claude-code` and is DESIGNED to run without any ++ * provider API keys. Search is keyword/FTS + graph + title (the existing ++ * no-embedding-provider path in hybrid search); chat routes through the ++ * `claude-cli` recipe (Claude Code OAuth session). Distinct from bare ++ * `embedding_disabled` (deferred setup, "configure a key before import"): ++ * with this flag set, `gbrain import` proceeds without vectors instead of ++ * refusing, and doctor/advisor treat the missing embedding provider as ++ * intentional, not a smell. Always written alongside ++ * `embedding_disabled: true` so every existing skip-embed callsite ++ * (sync, cycle, backfill) keeps working unchanged. ++ */ ++ claude_code_mode?: boolean; + expansion_model?: string; + /** + * Default chat model for `gateway.chat()` callers (v0.27+). +@@ -925,6 +939,7 @@ export const KNOWN_CONFIG_KEYS: readonly string[] = [ + 'embedding_model', + 'embedding_dimensions', + 'embedding_disabled', ++ 'claude_code_mode', + 'expansion_model', + 'chat_model', + 'chat_fallback_chain', +diff --git a/src/core/embedding-dim-check.ts b/src/core/embedding-dim-check.ts +index bdeee01..b40af16 100644 +--- a/src/core/embedding-dim-check.ts ++++ b/src/core/embedding-dim-check.ts +@@ -70,7 +70,32 @@ export class EmbeddingDisabledError extends Error { + } + } + +-export function assertEmbeddingEnabled(cfg: { embedding_disabled?: boolean } | null): void { ++/** ++ * #94 — Claude Code-native keyless mode predicate. True when the brain was ++ * initialized with `gbrain init --mode claude-code`: embedding is off BY ++ * DESIGN (keyword/FTS + graph search), not deferred. Callsites that refuse ++ * on the deferred-setup sentinel (`gbrain import`) use this to proceed ++ * without vectors instead; callsites that only make sense with vectors ++ * (`gbrain embed`) still refuse, with an upgrade hint. ++ */ ++export function isKeylessBrain( ++ cfg: { embedding_disabled?: boolean; claude_code_mode?: boolean } | null, ++): boolean { ++ return cfg?.claude_code_mode === true; ++} ++ ++export function assertEmbeddingEnabled( ++ cfg: { embedding_disabled?: boolean; claude_code_mode?: boolean } | null, ++): void { ++ if (isKeylessBrain(cfg)) { ++ throw new EmbeddingDisabledError( ++ 'This brain runs in Claude Code mode (keyless): search is keyword + graph,\n' + ++ 'no embedding provider is configured. Embedding commands are not available.\n' + ++ 'To upgrade to vector search, pick an embedding provider and re-init:\n' + ++ ' gbrain init --force --pglite --embedding-model :\n' + ++ 'then backfill with `gbrain embed --stale`.\n', ++ ); ++ } + if (cfg?.embedding_disabled) { + throw new EmbeddingDisabledError( + 'This brain was initialized with `--no-embedding` (deferred setup).\n' + +diff --git a/test/init-claude-code-mode.test.ts b/test/init-claude-code-mode.test.ts +new file mode 100644 +index 0000000..93e61e3 +--- /dev/null ++++ b/test/init-claude-code-mode.test.ts +@@ -0,0 +1,111 @@ ++/** ++ * #94 — `gbrain init --mode claude-code`: Claude Code-native keyless mode. ++ * ++ * The mode's contract: a Claude Code subscriber gets a working brain with ++ * ZERO provider API keys. PGLite engine, no embedding provider (hybrid ++ * search's no-embedding-provider path serves keyword + graph + title), ++ * chat through the `claude-cli` recipe's OAuth-session subprocess. ++ * ++ * Pure-function tests; no DB, no gateway state, no process.env mutation ++ * (helpers take cfg/env as arguments per CLAUDE.md test isolation rules). ++ */ ++import { describe, test, expect } from 'bun:test'; ++import { seedAIOptionsFromConfig, CLAUDE_CODE_DEFAULT_CHAT_MODEL } from '../src/commands/init.ts'; ++import { ++ assertEmbeddingEnabled, ++ isKeylessBrain, ++ EmbeddingDisabledError, ++} from '../src/core/embedding-dim-check.ts'; ++import { resolveRecipe } from '../src/core/ai/model-resolver.ts'; ++import type { GBrainConfig } from '../src/core/config.ts'; ++ ++describe('isKeylessBrain', () => { ++ test('claude_code_mode: true → keyless', () => { ++ expect(isKeylessBrain({ embedding_disabled: true, claude_code_mode: true })).toBe(true); ++ }); ++ ++ test('deferred setup alone (embedding_disabled without claude_code_mode) is NOT keyless', () => { ++ expect(isKeylessBrain({ embedding_disabled: true })).toBe(false); ++ }); ++ ++ test('null / empty config is NOT keyless', () => { ++ expect(isKeylessBrain(null)).toBe(false); ++ expect(isKeylessBrain({})).toBe(false); ++ }); ++ ++ test('sentinel must be strictly true — junk config values do not enable keyless mode', () => { ++ expect(isKeylessBrain({ claude_code_mode: 'yes' as unknown as boolean })).toBe(false); ++ expect(isKeylessBrain({ claude_code_mode: 1 as unknown as boolean })).toBe(false); ++ }); ++}); ++ ++describe('assertEmbeddingEnabled — keyless vs deferred messaging', () => { ++ test('claude-code brain refuses embed with the keyless upgrade hint, not the deferred-setup hint', () => { ++ let err: unknown; ++ try { ++ assertEmbeddingEnabled({ embedding_disabled: true, claude_code_mode: true }); ++ } catch (e) { ++ err = e; ++ } ++ expect(err).toBeInstanceOf(EmbeddingDisabledError); ++ const msg = (err as Error).message; ++ expect(msg).toContain('Claude Code mode'); ++ expect(msg).not.toContain('--no-embedding'); ++ }); ++ ++ test('deferred-setup brain keeps the original D9 message', () => { ++ let err: unknown; ++ try { ++ assertEmbeddingEnabled({ embedding_disabled: true }); ++ } catch (e) { ++ err = e; ++ } ++ expect(err).toBeInstanceOf(EmbeddingDisabledError); ++ expect((err as Error).message).toContain('--no-embedding'); ++ }); ++ ++ test('embedding-enabled brain passes', () => { ++ expect(() => assertEmbeddingEnabled({ embedding_disabled: false })).not.toThrow(); ++ expect(() => assertEmbeddingEnabled(null)).not.toThrow(); ++ }); ++}); ++ ++describe('seedAIOptionsFromConfig — claude-code re-init stability', () => { ++ test('claude-code brain re-seeds both noEmbedding and claudeCode', () => { ++ const cfg = { ++ engine: 'pglite', ++ embedding_disabled: true, ++ claude_code_mode: true, ++ chat_model: CLAUDE_CODE_DEFAULT_CHAT_MODEL, ++ } as GBrainConfig; ++ const out = seedAIOptionsFromConfig(cfg, {}); ++ expect(out.noEmbedding).toBe(true); ++ expect(out.claudeCode).toBe(true); ++ expect(out.chat_model).toBe(CLAUDE_CODE_DEFAULT_CHAT_MODEL); ++ expect(out.embedding_model).toBeUndefined(); ++ }); ++ ++ test('plain deferred-setup brain seeds noEmbedding only', () => { ++ const out = seedAIOptionsFromConfig({ embedding_disabled: true } as GBrainConfig, {}); ++ expect(out.noEmbedding).toBe(true); ++ expect(out.claudeCode).toBeUndefined(); ++ }); ++}); ++ ++describe('claude-cli default chat model — keyless contract', () => { ++ test('CLAUDE_CODE_DEFAULT_CHAT_MODEL resolves to the claude-cli recipe', () => { ++ const { recipe, parsed } = resolveRecipe(CLAUDE_CODE_DEFAULT_CHAT_MODEL); ++ expect(recipe.id).toBe('claude-cli'); ++ expect(recipe.touchpoints.chat?.models).toContain(parsed.modelId); ++ }); ++ ++ test('claude-cli recipe requires no env vars (the CLI owns auth)', () => { ++ const { recipe } = resolveRecipe(CLAUDE_CODE_DEFAULT_CHAT_MODEL); ++ expect(recipe.auth_env?.required ?? []).toEqual([]); ++ }); ++ ++ test('claude-cli recipe supports the subagent loop (D7 caveat exemption is sound)', () => { ++ const { recipe } = resolveRecipe(CLAUDE_CODE_DEFAULT_CHAT_MODEL); ++ expect(recipe.touchpoints.chat?.supports_subagent_loop).toBe(true); ++ }); ++}); +-- +2.43.0 + diff --git a/work/gbrain-no-api-keys/README.md b/work/gbrain-no-api-keys/README.md new file mode 100644 index 0000000..5d1de5a --- /dev/null +++ b/work/gbrain-no-api-keys/README.md @@ -0,0 +1,87 @@ +# gbrain: Claude Code-native keyless mode (fix for garrytan/gbrain#94) + +Prepared fix for [garrytan/gbrain#94](https://github.com/garrytan/gbrain/issues/94) — +"Plans for a Claude Code-native mode (no API keys, like gstack)?" + +The patch adds `gbrain init --mode claude-code`: a Claude Code subscriber gets a +fully working brain with **zero provider API keys** (no `OPENAI_API_KEY`, no +`ANTHROPIC_API_KEY`). + +## Applying + +```bash +git clone https://github.com/garrytan/gbrain && cd gbrain +git checkout ddd66e1 # base commit the patch was built against (master, 2026-07-27) +git am path/to/0001-claude-code-keyless-mode.patch +``` + +## What was already there vs. what was missing + +gbrain already had most of the building blocks; the blockers were at the seams: + +| Piece | Status before the patch | +|---|---| +| `claude-cli` chat recipe (OAuth session, no key, subagent loop) | ✅ existed (their #334) | +| Hybrid search degrades to keyword + graph + title when no embedding provider | ✅ existed (Codex C3 / D1 paths) | +| `gbrain sync` skips embedding on the `embedding_disabled` sentinel | ✅ existed | +| `gbrain init` without keys | ❌ exits 1 unless you know `--no-embedding` | +| `gbrain import` on a no-embedding brain | ❌ refuses outright (deferred-setup error) | +| Chat/expansion defaults | ❌ point at `anthropic:*`, which needs a key | +| Upgrade path `init --force --embedding-model …` out of deferred mode | ❌ latent bug: persisted sentinel silently beat the explicit flag | + +## What the patch does + +- **`gbrain init --mode claude-code`** — implies `--pglite`; writes + `claude_code_mode: true` alongside `embedding_disabled: true` (so every + existing skip-embed callsite keeps working); defaults `chat_model` to + `claude-cli:claude-sonnet-4-6`; seeds `search.mode = conservative` (the only + mode bundle with no reranker / LLM-expansion spend); warns if the `claude` + binary isn't on PATH. +- **`gbrain import`** treats a claude-code brain as implicit `--no-embed` — + chunks land keyword-searchable instead of the command exiting 1. +- **`gbrain embed`** refuses with a keyless-specific message including the + vector upgrade recipe. +- **advisor** no longer flags the missing embedding provider as a setup smell + on keyless brains. +- **Bug fix**: explicit `--embedding-model` / `--model` now clears a persisted + deferred/keyless sentinel, making the documented upgrade path work; a + resolved model also removes stale sentinels from `config.json`. +- **Bug fix**: the post-init "subagent features require ANTHROPIC_API_KEY" + caveat is suppressed for `claude-cli:*` chat models (that recipe drives the + subagent loop through the Claude Code OAuth session). +- Docs: README quick-start section, CHANGELOG entry, `init --help` text, and + the fail-loud no-provider hint now advertises the keyless path. +- Tests: `test/init-claude-code-mode.test.ts` (12 tests) covering the keyless + predicate, tailored refusal messages, re-init seeding, and the claude-cli + recipe's keyless contract (no required env vars, subagent loop supported). + +## Verification performed + +- `bun test` on the new suite plus all touched-area suites + (init-env-detection, init-embed-check, init-mode-picker, init-provider-picker, + sync-no-embed-sentinel, advisor-core, import-file, embed-preflight, + embedding-dim-check, config): **all green**. +- `bun run typecheck` (tsc --noEmit): clean. +- End-to-end smoke with **all provider keys unset**: + `init --mode claude-code` → config sentinels + conservative search mode + written → `import` of markdown succeeds without vectors → `search` returns + keyword-arm results → `embed --stale` refuses with the keyless message → + `init --force --pglite --embedding-model zeroentropyai:zembed-1 + --embedding-dimensions 1280` clears the sentinels and configures the model. + +## Not in scope (deliberate) + +- Query expansion / reranking via Claude Code skills (the issue's stretch + ideas): expansion is already gracefully gated behind + `isAvailable('expansion')` and off in the conservative bundle; the reranker + fails open. Nothing breaks keylessly; wiring them through `claude-cli` can be + a follow-up. +- Postgres/Supabase keyless installs: `--mode claude-code` targets the local + PGLite path per the issue; the runtime behavior (import/search) is + engine-agnostic, so extending the flag later is trivial. + +## Submitting upstream + +This session couldn't open a PR against `garrytan/gbrain` (repo outside the +session's GitHub scope), so the change ships here as a `git am`-ready patch. +Apply on a fork branch and open the PR referencing issue #94. From b7c4883521a8f44faf1aba25ac4caec93e4fab6c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Jul 2026 02:16:49 +0000 Subject: [PATCH 2/4] Add ready-to-paste upstream PR description for gbrain#94 fix Includes the node_modules-symlink heads-up for the upstream maintainer. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01WQ7za53ZWxWqL2XAfSgTux --- work/gbrain-no-api-keys/PR-DESCRIPTION.md | 101 ++++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 work/gbrain-no-api-keys/PR-DESCRIPTION.md diff --git a/work/gbrain-no-api-keys/PR-DESCRIPTION.md b/work/gbrain-no-api-keys/PR-DESCRIPTION.md new file mode 100644 index 0000000..6f19795 --- /dev/null +++ b/work/gbrain-no-api-keys/PR-DESCRIPTION.md @@ -0,0 +1,101 @@ +# Upstream PR description for garrytan/gbrain + +Ready to paste when opening the PR from a fork (apply +`0001-claude-code-keyless-mode.patch` on a branch off `master`). + +--- + +**Title:** feat(init): `--mode claude-code` — keyless mode for Claude Code subscribers (#94) + +**Body:** + +Closes #94. + +## What + +`gbrain init --mode claude-code` gives a Claude Code subscriber a fully working +brain with **zero provider API keys** — no `OPENAI_API_KEY`, no +`ANTHROPIC_API_KEY`: + +```bash +gbrain init --mode claude-code # keyless: PGLite, keyword + graph search +gbrain import ~/notes/ +gbrain search "who works at Acme" +``` + +Search runs on the existing keyword + graph + title arms; chat commands route +through the existing `claude-cli` recipe (Claude Code OAuth session, billed to +the subscription). + +## Why this is a small diff + +Most of the machinery already existed — this PR wires the seams: + +- the `claude-cli` chat recipe (#334) already runs keyless and supports the + subagent loop; +- hybrid search already degrades gracefully to keyword + graph + title when no + embedding provider is available (Codex C3 / D1 paths); +- `gbrain sync` already honors the `embedding_disabled` sentinel. + +What was missing: `init` fail-louded without keys, `import` refused outright on +a no-embedding brain, and chat defaulted to `anthropic:*`. + +## Changes + +- **init**: new `--mode claude-code` flag. Implies `--pglite`; writes + `claude_code_mode: true` alongside `embedding_disabled: true` (every existing + skip-embed callsite keeps working); defaults `chat_model` to + `claude-cli:claude-sonnet-4-6`; seeds `search.mode = conservative` (the one + bundle with no reranker / LLM-expansion spend); best-effort check that the + `claude` binary is on PATH. Re-inits stay keyless without re-passing the flag. +- **import**: a claude-code brain imports without vectors (implicit + `--no-embed`) instead of exiting 1 with the deferred-setup error. +- **embed**: refusal message tailored to keyless mode, with the vector upgrade + recipe. +- **advisor**: `embedding_disabled` is not a setup smell when keyless by design. +- **Bug fix**: explicit `--embedding-model` / `--model` now clears a persisted + deferred/keyless sentinel. Previously the documented upgrade path + (`gbrain init --force --embedding-model …` — the exact command + `assertEmbeddingEnabled` prints) was silently ignored because the seeded + `noEmbedding` won in `initPGLite`. A resolved model also removes stale + sentinels from `config.json` (the "one or the other, never both" invariant). +- **Bug fix**: the post-init "subagent features require ANTHROPIC_API_KEY" + caveat no longer fires for `claude-cli:*` chat models — that recipe declares + `supports_subagent_loop` and drives Minions through the OAuth session. +- Docs: README quick-start subsection, CHANGELOG, `init --help`, and the + fail-loud no-provider hint now advertises the keyless path. + +## Tests + +- New `test/init-claude-code-mode.test.ts` (12 tests): keyless predicate, + tailored refusal messages, re-init seeding, and the claude-cli recipe's + keyless contract (no required env vars, subagent loop supported). +- All touched-area suites green (init-env-detection, init-embed-check, + init-mode-picker, init-provider-picker, sync-no-embed-sentinel, advisor-core, + import-file, embed-preflight, embedding-dim-check, config); + `bun run typecheck` clean. +- End-to-end smoke with all provider keys unset: + init → import → keyword search returns the right page → `embed --stale` + refuses with the keyless message → `init --force --pglite --embedding-model + zeroentropyai:zembed-1 --embedding-dimensions 1280` clears the sentinels and + configures the model. + +## Out of scope (deliberate) + +- Query expansion / reranking via Claude Code skills (the issue's stretch + ideas): expansion is already gated behind `isAvailable('expansion')` and off + in the conservative bundle; the reranker fails open. Nothing breaks keylessly; + wiring them through `claude-cli` can be a follow-up. +- Postgres/Supabase keyless installs: the flag targets the local PGLite path + per the issue; the runtime behavior (import/search) is engine-agnostic, so + extending later is trivial. + +## Unrelated heads-up found while working on this + +`master` (at least since faf5cdb) has an accidentally **committed +`node_modules` symlink** pointing at `/tmp/fleet/repo/node_modules` — a path +from someone's build environment. It's tracked despite `node_modules/` being in +`.gitignore` (gitignore doesn't apply to already-tracked paths), so every fresh +clone gets a dangling symlink and `bun install` behaves oddly until you +`rm node_modules && bun install`. Worth a one-line cleanup commit: +`git rm --cached node_modules`. Not fixed in this PR to keep it scoped to #94. From 449728f4ed623671b2d18c5288f7f57bf84d7bdd Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Jul 2026 02:21:44 +0000 Subject: [PATCH 3/4] Add one-command script to open the upstream gbrain#94 PR The prep session's proxy blocks all GitHub API calls to garrytan/gbrain (cross-owner add_repo is a v1 limitation), so the final PR-open call is packaged as a curl script + JSON payload runnable with any token that can see the upstream repo. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01WQ7za53ZWxWqL2XAfSgTux --- work/gbrain-no-api-keys/open-upstream-pr.sh | 23 +++++++++++++++++++++ work/gbrain-no-api-keys/pr-payload.json | 7 +++++++ 2 files changed, 30 insertions(+) create mode 100755 work/gbrain-no-api-keys/open-upstream-pr.sh create mode 100644 work/gbrain-no-api-keys/pr-payload.json diff --git a/work/gbrain-no-api-keys/open-upstream-pr.sh b/work/gbrain-no-api-keys/open-upstream-pr.sh new file mode 100755 index 0000000..375ecd9 --- /dev/null +++ b/work/gbrain-no-api-keys/open-upstream-pr.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Opens the upstream PR garrytan/gbrain <- mnemonik-dev:claude-code-keyless-mode. +# +# Run from any machine with a GitHub token that can see garrytan/gbrain +# (a classic PAT with `public_repo`, or a fine-grained token; the branch +# already sits on the mnemonik-dev/gbrain fork): +# +# GITHUB_TOKEN=ghp_... ./open-upstream-pr.sh +# +# Why this script exists: the Claude Code session that prepared the fix is +# repo-scoped to mnemonik-dev/* — its outbound proxy 403s every GitHub API +# call to garrytan/gbrain, and cross-owner add_repo is a v1 limitation. +set -euo pipefail +cd "$(dirname "$0")" + +: "${GITHUB_TOKEN:?set GITHUB_TOKEN to a token that can access garrytan/gbrain}" + +curl -sS -X POST \ + -H "Authorization: Bearer ${GITHUB_TOKEN}" \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + https://api.github.com/repos/garrytan/gbrain/pulls \ + -d @pr-payload.json | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("html_url") or json.dumps(d, indent=2))' diff --git a/work/gbrain-no-api-keys/pr-payload.json b/work/gbrain-no-api-keys/pr-payload.json new file mode 100644 index 0000000..916590f --- /dev/null +++ b/work/gbrain-no-api-keys/pr-payload.json @@ -0,0 +1,7 @@ +{ + "title": "feat(init): --mode claude-code — keyless mode for Claude Code subscribers (#94)", + "head": "mnemonik-dev:claude-code-keyless-mode", + "base": "master", + "maintainer_can_modify": true, + "body": "Closes #94.\n\n## What\n\n`gbrain init --mode claude-code` gives a Claude Code subscriber a fully working\nbrain with **zero provider API keys** — no `OPENAI_API_KEY`, no\n`ANTHROPIC_API_KEY`:\n\n```bash\ngbrain init --mode claude-code # keyless: PGLite, keyword + graph search\ngbrain import ~/notes/\ngbrain search \"who works at Acme\"\n```\n\nSearch runs on the existing keyword + graph + title arms; chat commands route\nthrough the existing `claude-cli` recipe (Claude Code OAuth session, billed to\nthe subscription).\n\n## Why this is a small diff\n\nMost of the machinery already existed — this PR wires the seams:\n\n- the `claude-cli` chat recipe (#334) already runs keyless and supports the\n subagent loop;\n- hybrid search already degrades gracefully to keyword + graph + title when no\n embedding provider is available (Codex C3 / D1 paths);\n- `gbrain sync` already honors the `embedding_disabled` sentinel.\n\nWhat was missing: `init` fail-louded without keys, `import` refused outright on\na no-embedding brain, and chat defaulted to `anthropic:*`.\n\n## Changes\n\n- **init**: new `--mode claude-code` flag. Implies `--pglite`; writes\n `claude_code_mode: true` alongside `embedding_disabled: true` (every existing\n skip-embed callsite keeps working); defaults `chat_model` to\n `claude-cli:claude-sonnet-4-6`; seeds `search.mode = conservative` (the one\n bundle with no reranker / LLM-expansion spend); best-effort check that the\n `claude` binary is on PATH. Re-inits stay keyless without re-passing the flag.\n- **import**: a claude-code brain imports without vectors (implicit\n `--no-embed`) instead of exiting 1 with the deferred-setup error.\n- **embed**: refusal message tailored to keyless mode, with the vector upgrade\n recipe.\n- **advisor**: `embedding_disabled` is not a setup smell when keyless by design.\n- **Bug fix**: explicit `--embedding-model` / `--model` now clears a persisted\n deferred/keyless sentinel. Previously the documented upgrade path\n (`gbrain init --force --embedding-model …` — the exact command\n `assertEmbeddingEnabled` prints) was silently ignored because the seeded\n `noEmbedding` won in `initPGLite`. A resolved model also removes stale\n sentinels from `config.json` (the \"one or the other, never both\" invariant).\n- **Bug fix**: the post-init \"subagent features require ANTHROPIC_API_KEY\"\n caveat no longer fires for `claude-cli:*` chat models — that recipe declares\n `supports_subagent_loop` and drives Minions through the OAuth session.\n- Docs: README quick-start subsection, CHANGELOG, `init --help`, and the\n fail-loud no-provider hint now advertises the keyless path.\n\n## Tests\n\n- New `test/init-claude-code-mode.test.ts` (12 tests): keyless predicate,\n tailored refusal messages, re-init seeding, and the claude-cli recipe's\n keyless contract (no required env vars, subagent loop supported).\n- All touched-area suites green (init-env-detection, init-embed-check,\n init-mode-picker, init-provider-picker, sync-no-embed-sentinel, advisor-core,\n import-file, embed-preflight, embedding-dim-check, config);\n `bun run typecheck` clean.\n- End-to-end smoke with all provider keys unset:\n init → import → keyword search returns the right page → `embed --stale`\n refuses with the keyless message → `init --force --pglite --embedding-model\n zeroentropyai:zembed-1 --embedding-dimensions 1280` clears the sentinels and\n configures the model.\n\n## Out of scope (deliberate)\n\n- Query expansion / reranking via Claude Code skills (the issue's stretch\n ideas): expansion is already gated behind `isAvailable('expansion')` and off\n in the conservative bundle; the reranker fails open. Nothing breaks keylessly;\n wiring them through `claude-cli` can be a follow-up.\n- Postgres/Supabase keyless installs: the flag targets the local PGLite path\n per the issue; the runtime behavior (import/search) is engine-agnostic, so\n extending later is trivial.\n\n## Unrelated heads-up found while working on this\n\n`master` (at least since faf5cdb) has an accidentally **committed\n`node_modules` symlink** pointing at `/tmp/fleet/repo/node_modules` — a path\nfrom someone's build environment. It's tracked despite `node_modules/` being in\n`.gitignore` (gitignore doesn't apply to already-tracked paths), so every fresh\nclone gets a dangling symlink and `bun install` behaves oddly until you\n`rm node_modules && bun install`. Worth a one-line cleanup commit:\n`git rm --cached node_modules`. Not fixed in this PR to keep it scoped to #94.\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)\n\nhttps://claude.ai/code/session_01WQ7za53ZWxWqL2XAfSgTux" +} \ No newline at end of file From 9b719a894757e1eab74f1c80600e5eda3fcb3d35 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 28 Jul 2026 03:12:28 +0000 Subject: [PATCH 4/4] Add ready-to-paste codex/kimi keyless follow-up issue for gbrain Two one-shot sessions failed to post it; manual posting fallback. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01WQ7za53ZWxWqL2XAfSgTux --- work/gbrain-no-api-keys/ISSUE-codex-kimi.md | 36 +++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 work/gbrain-no-api-keys/ISSUE-codex-kimi.md diff --git a/work/gbrain-no-api-keys/ISSUE-codex-kimi.md b/work/gbrain-no-api-keys/ISSUE-codex-kimi.md new file mode 100644 index 0000000..8e90ab4 --- /dev/null +++ b/work/gbrain-no-api-keys/ISSUE-codex-kimi.md @@ -0,0 +1,36 @@ +# Ready-to-paste issue for https://github.com/garrytan/gbrain/issues/new + +**Title:** + +codex-cli and kimi-cli recipes: subscription-native keyless dispatch for Codex and Kimi users (follow-up to #94) + +**Body:** + +## Context + +#94 asked for a Claude Code-native mode with no API keys; PR #3464 implements it: `gbrain init --mode claude-code` routes chat, query expansion, and the subagent loop through the `claude-cli` recipe (Claude Code OAuth session, subscription-billed), and picks up a local Ollama daemon for keyless embeddings when one is running. + +The same friction exists for users of other agent CLIs with subscription auth. This issue proposes extending the pattern so **any** agent subscription can power gbrain keylessly: + +## Proposal + +**1. `codex-cli` recipe** — OpenAI Codex CLI users sign in with their ChatGPT subscription; `codex exec` provides a non-interactive mode suitable for subprocess dispatch. A `CodexCliLanguageModel` modeled on `src/core/ai/providers/claude-cli-language-model.ts` plus a pure-data recipe (`implementation: 'codex-cli'`, `auth_env.required: []`) would give Codex subscribers chat + expansion + subagent dispatch with zero API keys — the exact #334/#94 story for the other big agent population. + +**2. `kimi-cli` recipe** — Moonshot's Kimi CLI is the same shape: subscription-authenticated agent CLI with a non-interactive mode. Same provider pattern, same recipe contract. (gbrain already ships a hosted `moonshot` recipe; this would be its keyless sibling, the way `claude-cli` sits alongside `anthropic`.) + +**3. Possible generalization** — the two providers above will be ~90% identical to the claude-cli one (spawn binary, isolate cwd, parse JSON/text output, map tool calls). It may be worth extracting a shared `agent-cli` subprocess base where each recipe supplies binary name/env var, argv template, and an output parser — so the third agent CLI is a recipe, not a provider implementation. + +**4. `--mode claude-code` naming** — with more than one agent CLI supported, init could grow `--mode codex` / `--mode kimi` (or a generic `--mode agent-cli --agent `), all sharing the keyless machinery PR #3464 introduced (`claude_code_mode` sentinel, implicit `--no-embed` import, conservative search-mode seeding, Ollama embedding probe). + +## Why not in PR #3464 + +Shipping subprocess providers untested would be worse than proposing them: verifying the `codex exec` / Kimi CLI output protocols needs machines with those binaries logged in. The claude-cli path in #3464 was verified end-to-end; these two deserve the same bar. + +## Questions for the maintainer + +1. Would PRs for `codex-cli` / `kimi-cli` recipes be welcome, following the claude-cli provider pattern? +2. Preference between per-provider implementations vs. the shared `agent-cli` base (option 3)? +3. Any constraints on adding init modes per agent CLI vs. a single generic flag? + +--- +_Generated by [Claude Code](https://claude.ai/code)_