@algosuite/vo-mcp 0.2.0-beta.2 → 0.2.0-beta.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../scripts/virtual-office/code-runner/agent-key-store.mjs", "../src/set-key-args.mjs", "../src/set-key-cli.mjs"],
4
- "sourcesContent": ["/**\n * agent-key-store \u2014 BYO Phase-B multi-agent. A provider-keyed generalization of\n * [anthropic-key-store] so a friend can store the credential for whichever CLI\n * agent they run (Claude / Codex / Cursor) in the OPERATING-SYSTEM keychain\n * (@napi-rs/keyring). The key lives ONLY on the friend's machine and never\n * reaches Algosuite; the runner reads it at spawn time to authenticate the\n * headless agent.\n *\n * Relationship to anthropic-key-store: that module is the Claude-specific M4\n * path and is left UNTOUCHED. This store uses the SAME service ('algosuite-vo')\n * and the SAME account convention ('<provider>-api-key'), so a key written by\n * `vo-mcp set-key` (account 'anthropic-api-key') is readable here as provider\n * 'anthropic' \u2014 the two are interoperable, not competing stores.\n *\n * Design rules (identical posture to anthropic-key-store):\n * - PURELY graceful: if @napi-rs/keyring is absent, EVERY op no-ops\n * (null/false) so the runner falls back to the ambient env / CLI login.\n * - An explicit credential already in the environment ALWAYS wins over the\n * keychain (a manual override is never silently replaced).\n * - The key travels via process env to the spawned agent, never via argv.\n */\nimport { createRequire } from 'node:module';\n\nconst require = createRequire(import.meta.url);\n\nexport const KEY_SERVICE = 'algosuite-vo';\n\n/**\n * Provider \u2192 the env var(s) its CLI reads for its API credential. Filling more\n * than one is harmless (the CLI ignores vars it doesn't use); 'openai' covers\n * both names the Codex CLI accepts across versions.\n */\nexport const PROVIDER_ENV = {\n anthropic: ['ANTHROPIC_API_KEY'],\n openai: ['OPENAI_API_KEY', 'CODEX_API_KEY'],\n cursor: ['CURSOR_API_KEY'],\n};\n\n/** Canonical provider for a runner's logical name (claude\u2192anthropic, codex\u2192openai). */\nconst PROVIDER_ALIAS = {\n claude: 'anthropic',\n anthropic: 'anthropic',\n codex: 'openai',\n openai: 'openai',\n cursor: 'cursor',\n};\n\n/** Normalize a runner/provider label to its credential provider. Returns null if unknown. */\nexport function resolveProvider(name) {\n const key = String(name || '').trim().toLowerCase();\n return PROVIDER_ALIAS[key] || null;\n}\n\n/** Keychain account name for a provider (matches anthropic-key-store's 'anthropic-api-key'). */\nexport function accountFor(provider) {\n return `${provider}-api-key`;\n}\n\nlet _entryCtor;\nlet _loadTried = false;\n\n/** Lazily load @napi-rs/keyring's `Entry`; null when unavailable (\u2192 no-op ops). */\nfunction defaultEntryCtor() {\n if (_loadTried) return _entryCtor;\n _loadTried = true;\n try {\n _entryCtor = require('@napi-rs/keyring').Entry;\n } catch {\n _entryCtor = null; // not installed / unsupported platform \u2192 graceful fallback\n }\n return _entryCtor;\n}\n\n/** Store `key` for `provider` in the OS keychain. Returns true on success. */\nexport function setAgentKey(provider, key, { EntryCtor = defaultEntryCtor() } = {}) {\n const p = resolveProvider(provider);\n if (!p || !key || !EntryCtor) return false;\n try {\n new EntryCtor(KEY_SERVICE, accountFor(p)).setPassword(String(key));\n return true;\n } catch {\n return false;\n }\n}\n\n/** Read `provider`'s key from the OS keychain. Returns the key, or null. */\nexport function getAgentKey(provider, { EntryCtor = defaultEntryCtor() } = {}) {\n const p = resolveProvider(provider);\n if (!p || !EntryCtor) return null;\n try {\n // getPassword throws (keyring-rs NoEntry) when nothing is stored.\n return new EntryCtor(KEY_SERVICE, accountFor(p)).getPassword() || null;\n } catch {\n return null;\n }\n}\n\n/** Delete `provider`'s stored key. Returns true if a key was removed. */\nexport function clearAgentKey(provider, { EntryCtor = defaultEntryCtor() } = {}) {\n const p = resolveProvider(provider);\n if (!p || !EntryCtor) return false;\n try {\n new EntryCtor(KEY_SERVICE, accountFor(p)).deletePassword();\n return true;\n } catch {\n return false;\n }\n}\n\n/** True if a key is currently stored for `provider` (and the keychain is available). */\nexport function hasAgentKey(provider, opts = {}) {\n return getAgentKey(provider, opts) !== null;\n}\n\n/**\n * Return an env for the spawned agent with `provider`'s credential env var(s)\n * filled from the keychain \u2014 but ONLY for vars not already set (explicit env\n * wins) and only when a key is actually stored. Always returns a fresh object;\n * never mutates `baseEnv`. Unknown provider or no stored key \u2192 shallow copy.\n */\nexport function withAgentKey(provider, baseEnv = {}, { getKey = getAgentKey } = {}) {\n const p = resolveProvider(provider);\n const vars = (p && PROVIDER_ENV[p]) || [];\n const out = { ...baseEnv };\n if (!p || vars.length === 0) return out;\n // If any target var is already set, treat the credential as operator-provided.\n if (vars.some((v) => out[v])) return out;\n const key = getKey(p);\n if (!key) return out;\n for (const v of vars) out[v] = key;\n return out;\n}\n", "/**\n * set-key-args \u2014 the PURE arg-parsing + provider table for `vo-mcp set-key`.\n * Split out of set-key-cli.mjs so it can be unit-tested without importing the\n * CLI (whose module body reads stdin and runs on import). esbuild inlines it at\n * bundle time, same as the other runner-tree .mjs imports.\n */\nimport { resolveProvider } from '../../../scripts/virtual-office/code-runner/agent-key-store.mjs';\n\n/**\n * Per-provider metadata: display `label`, the agent `cli` binary, the\n * `selector` keyword the user types after `--provider` (NOT the binary \u2014 e.g.\n * cursor's binary is `cursor-agent`), the `envVar` fallback, and a typical\n * key-prefix `hint` for a gentle (never-blocking) format check.\n */\nexport const PROVIDERS = {\n anthropic: { label: 'Anthropic', cli: 'claude', selector: 'claude', envVar: 'ANTHROPIC_API_KEY', hint: 'sk-' },\n openai: { label: 'OpenAI / Codex', cli: 'codex', selector: 'codex', envVar: 'OPENAI_API_KEY', hint: 'sk-' },\n cursor: { label: 'Cursor', cli: 'cursor-agent', selector: 'cursor', envVar: 'CURSOR_API_KEY', hint: '' },\n};\n\n/**\n * Parse `vo-mcp set-key` argv (the FULL process.argv; the subcommand is argv[2]).\n * Supports `--provider <p>` / `--provider=p` (default anthropic), `--status`,\n * `--clear`. Returns `{ provider, providerArg, mode }` where `provider` is the\n * normalized credential provider (null if the value is unknown) and `mode` is\n * 'set' | 'status' | 'clear'.\n */\nexport function parseSetKeyArgs(argv = []) {\n const rest = argv.slice(3);\n let providerArg = 'anthropic';\n let mode = 'set';\n for (let i = 0; i < rest.length; i += 1) {\n const a = rest[i];\n if (a === '--status') mode = 'status';\n else if (a === '--clear') mode = 'clear';\n else if (a === '--provider') {\n providerArg = rest[i + 1] || '';\n i += 1; // consume the value token\n } else if (a.startsWith('--provider=')) {\n providerArg = a.slice('--provider='.length);\n }\n }\n return { provider: resolveProvider(providerArg), providerArg, mode };\n}\n", "#!/usr/bin/env node\r\n/**\r\n * `vo-mcp set-key` \u2014 BYO Phase-B M4 + multi-agent. Stores the friend's AI API key\r\n * in the OS keychain (Windows Credential Manager / macOS Keychain / libsecret) so\r\n * the runner can authenticate its CLI agent without an env var. The key lives ONLY\r\n * on the friend's machine \u2014 it never travels to Algosuite.\r\n *\r\n * vo-mcp set-key store the Anthropic (Claude) key [default]\r\n * vo-mcp set-key --provider codex store the OpenAI/Codex key\r\n * vo-mcp set-key --provider cursor store the Cursor key\r\n * vo-mcp set-key --status report whether the provider's key is stored\r\n * vo-mcp set-key --clear remove the provider's stored key\r\n *\r\n * The key may be piped (`echo $KEY | vo-mcp set-key --provider codex`) or typed at\r\n * the hidden prompt. .mjs (like runner-cli.mjs) because it imports the runner-tree\r\n * key store, outside tsconfig rootDir \u2014 tsc skips it; esbuild inlines it at bundle.\r\n */\r\nimport {\r\n setAgentKey,\r\n hasAgentKey,\r\n clearAgentKey,\r\n} from '../../../scripts/virtual-office/code-runner/agent-key-store.mjs';\r\nimport { PROVIDERS, parseSetKeyArgs } from './set-key-args.mjs';\r\n\r\nconst log = (m) => console.error(m);\r\nconst mask = (k) => (k && k.length > 8 ? `\u2026${k.slice(-4)}` : '****');\r\n\r\n/** Read a secret from a TTY without echoing; falls back to piped stdin. */\r\nfunction readSecret(promptText) {\r\n return new Promise((resolve) => {\r\n if (!process.stdin.isTTY) {\r\n // Piped: `echo $KEY | vo-mcp set-key` \u2014 read everything, take the first line.\r\n let buf = '';\r\n process.stdin.setEncoding('utf8');\r\n process.stdin.on('data', (d) => (buf += d));\r\n process.stdin.on('end', () => resolve(buf.split(/\\r?\\n/)[0].trim()));\r\n return;\r\n }\r\n // Interactive: print the prompt, then mute the echoed characters.\r\n process.stdout.write(promptText);\r\n const stdin = process.stdin;\r\n stdin.setRawMode?.(true);\r\n stdin.resume();\r\n stdin.setEncoding('utf8');\r\n let val = '';\r\n const onData = (ch) => {\r\n if (ch === '\\r' || ch === '\\n' || ch === '\u0004') {\r\n stdin.setRawMode?.(false);\r\n stdin.pause();\r\n stdin.removeListener('data', onData);\r\n process.stdout.write('\\n');\r\n resolve(val.trim());\r\n } else if (ch === '\u0003') {\r\n // Ctrl-C\r\n process.stdout.write('\\n');\r\n process.exit(130);\r\n } else if (ch === '\u007F' || ch === '\\b') {\r\n val = val.slice(0, -1);\r\n } else {\r\n val += ch;\r\n }\r\n };\r\n stdin.on('data', onData);\r\n });\r\n}\r\n\r\nasync function main() {\r\n const { provider, providerArg, mode } = parseSetKeyArgs(process.argv);\r\n if (!provider) {\r\n log(`Unknown --provider \"${providerArg}\". Use one of: claude, codex, cursor.`);\r\n process.exitCode = 1;\r\n return;\r\n }\r\n const info = PROVIDERS[provider];\r\n\r\n if (mode === 'status') {\r\n log(\r\n hasAgentKey(provider)\r\n ? `\u2713 A ${info.label} API key is stored in your OS keychain.`\r\n : `No ${info.label} API key stored. Run \\`vo-mcp set-key${provider === 'anthropic' ? '' : ` --provider ${info.selector}`}\\` to add one.`,\r\n );\r\n return;\r\n }\r\n\r\n if (mode === 'clear') {\r\n log(\r\n clearAgentKey(provider)\r\n ? `\u2713 Removed the stored ${info.label} API key.`\r\n : `No stored ${info.label} key to remove (or keychain unavailable).`,\r\n );\r\n return;\r\n }\r\n\r\n log(`Paste your ${info.label} API key (for the \\`${info.cli}\\` agent). It is stored ONLY in`);\r\n log(\"your computer's keychain \u2014 it never leaves this machine and Algosuite never sees it.\");\r\n const key = await readSecret(`${info.label} API key: `);\r\n if (!key) {\r\n log('No key entered \u2014 nothing stored.');\r\n process.exitCode = 1;\r\n return;\r\n }\r\n // A gentle format hint only \u2014 key formats change and Cursor's isn't `sk-`, so\r\n // never hard-reject a non-matching key (that would block a valid credential).\r\n if (info.hint && !key.startsWith(info.hint)) {\r\n log(`Heads up: ${info.label} keys usually start with \"${info.hint}\". Storing it anyway.`);\r\n }\r\n if (setAgentKey(provider, key)) {\r\n log(`\u2713 Stored your ${info.label} API key (${mask(key)}) in the OS keychain. The runner will use it automatically.`);\r\n } else {\r\n log(`Could not access the OS keychain on this machine. As a fallback, set ${info.envVar} in your environment before running \\`vo-mcp runner\\`.`);\r\n process.exitCode = 1;\r\n }\r\n}\r\n\r\nmain().catch((e) => {\r\n log(`set-key failed: ${e && e.message ? e.message : e}`);\r\n process.exitCode = 1;\r\n});\r\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["/**\n * agent-key-store \u2014 BYO Phase-B multi-agent. A provider-keyed generalization of\n * [anthropic-key-store] so a friend can store the credential for whichever CLI\n * agent they run (Claude / Codex / Cursor) in the OPERATING-SYSTEM keychain\n * (@napi-rs/keyring). The key lives ONLY on the friend's machine and never\n * reaches Algosuite; the runner reads it at spawn time to authenticate the\n * headless agent.\n *\n * Relationship to anthropic-key-store: that module is the Claude-specific M4\n * path and is left UNTOUCHED. This store uses the SAME service ('algosuite-vo')\n * and the SAME account convention ('<provider>-api-key'), so a key written by\n * `vo-mcp set-key` (account 'anthropic-api-key') is readable here as provider\n * 'anthropic' \u2014 the two are interoperable, not competing stores.\n *\n * Design rules (identical posture to anthropic-key-store):\n * - PURELY graceful: if @napi-rs/keyring is absent, EVERY op no-ops\n * (null/false) so the runner falls back to the ambient env / CLI login.\n * - An explicit credential already in the environment ALWAYS wins over the\n * keychain (a manual override is never silently replaced).\n * - The key travels via process env to the spawned agent, never via argv.\n */\nimport { createRequire } from 'node:module';\n\nconst require = createRequire(import.meta.url);\n\nexport const KEY_SERVICE = 'algosuite-vo';\n\n/**\n * Provider \u2192 the env var(s) its CLI reads for its API credential. Filling more\n * than one is harmless (the CLI ignores vars it doesn't use); 'openai' covers\n * both names the Codex CLI accepts across versions.\n */\nexport const PROVIDER_ENV = {\n anthropic: ['ANTHROPIC_API_KEY'],\n openai: ['OPENAI_API_KEY', 'CODEX_API_KEY'],\n cursor: ['CURSOR_API_KEY'],\n meta: ['MODEL_API_KEY'],\n // Generic OpenAI-compatible runner (bring-your-own model + endpoint): its key\n // is a dedicated var so it never collides with a real OpenAI/Codex key.\n 'oai-compat': ['VO_CODE_RUNNER_OAI_API_KEY'],\n // Sovereign local inference (Ollama / LM Studio). The key is OPTIONAL \u2014 most\n // local servers need none \u2014 and exists for locally secured endpoints only.\n local: ['VO_CODE_RUNNER_LOCAL_API_KEY'],\n};\n\n/** Canonical provider for a runner's logical name (claude\u2192anthropic, codex\u2192openai). */\nconst PROVIDER_ALIAS = {\n claude: 'anthropic',\n anthropic: 'anthropic',\n codex: 'openai',\n openai: 'openai',\n cursor: 'cursor',\n meta: 'meta',\n muse: 'meta',\n spark: 'meta',\n 'muse-spark': 'meta',\n oai: 'oai-compat',\n 'oai-compat': 'oai-compat',\n local: 'local',\n ollama: 'local',\n lmstudio: 'local',\n};\n\n/** Normalize a runner/provider label to its credential provider. Returns null if unknown. */\nexport function resolveProvider(name) {\n const key = String(name || '').trim().toLowerCase();\n return PROVIDER_ALIAS[key] || null;\n}\n\n/** Recognize retired provider aliases only for explicit legacy-key cleanup. */\nexport function resolveRetiredProvider(name) {\n const key = String(name || '').trim().toLowerCase();\n return key === 'grok' || key === 'xai' ? 'xai' : null;\n}\n\n/** Keychain account name for a provider (matches anthropic-key-store's 'anthropic-api-key'). */\nexport function accountFor(provider) {\n return `${provider}-api-key`;\n}\n\nlet _entryCtor;\nlet _loadTried = false;\n\n/** Lazily load @napi-rs/keyring's `Entry`; null when unavailable (\u2192 no-op ops). */\nfunction defaultEntryCtor() {\n if (_loadTried) return _entryCtor;\n _loadTried = true;\n try {\n _entryCtor = require('@napi-rs/keyring').Entry;\n } catch {\n _entryCtor = null; // not installed / unsupported platform \u2192 graceful fallback\n }\n return _entryCtor;\n}\n\n/** Store `key` for `provider` in the OS keychain. Returns true on success. */\nexport function setAgentKey(provider, key, { EntryCtor = defaultEntryCtor() } = {}) {\n const p = resolveProvider(provider);\n if (!p || !key || !EntryCtor) return false;\n try {\n new EntryCtor(KEY_SERVICE, accountFor(p)).setPassword(String(key));\n return true;\n } catch {\n return false;\n }\n}\n\n/** Read `provider`'s key from the OS keychain. Returns the key, or null. */\nexport function getAgentKey(provider, { EntryCtor = defaultEntryCtor() } = {}) {\n const p = resolveProvider(provider);\n if (!p || !EntryCtor) return null;\n try {\n // getPassword throws (keyring-rs NoEntry) when nothing is stored.\n return new EntryCtor(KEY_SERVICE, accountFor(p)).getPassword() || null;\n } catch {\n return null;\n }\n}\n\n/** Delete `provider`'s stored key. Returns true if a key was removed. */\nexport function clearAgentKey(provider, { EntryCtor = defaultEntryCtor() } = {}) {\n const p = resolveProvider(provider);\n if (!p || !EntryCtor) return false;\n try {\n new EntryCtor(KEY_SERVICE, accountFor(p)).deletePassword();\n return true;\n } catch {\n return false;\n }\n}\n\n/** True if a key is currently stored for `provider` (and the keychain is available). */\nexport function hasAgentKey(provider, opts = {}) {\n return getAgentKey(provider, opts) !== null;\n}\n\n/** True only when the legacy xAI keychain account still contains a key. */\nexport function hasRetiredAgentKey(provider, { EntryCtor = defaultEntryCtor() } = {}) {\n if (!resolveRetiredProvider(provider) || !EntryCtor) return false;\n try {\n return Boolean(new EntryCtor(KEY_SERVICE, accountFor('xai')).getPassword());\n } catch {\n return false;\n }\n}\n\n/** Delete the legacy xAI key without restoring any active provider mapping. */\nexport function clearRetiredAgentKey(provider, { EntryCtor = defaultEntryCtor() } = {}) {\n if (!resolveRetiredProvider(provider) || !EntryCtor) return false;\n try {\n new EntryCtor(KEY_SERVICE, accountFor('xai')).deletePassword();\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Return an env for the spawned agent with `provider`'s credential env var(s)\n * filled from the keychain \u2014 but ONLY for vars not already set (explicit env\n * wins) and only when a key is actually stored. Always returns a fresh object;\n * never mutates `baseEnv`. Unknown provider or no stored key \u2192 shallow copy.\n */\nexport function withAgentKey(provider, baseEnv = {}, { getKey = getAgentKey } = {}) {\n const p = resolveProvider(provider);\n const vars = (p && PROVIDER_ENV[p]) || [];\n const out = { ...baseEnv };\n if (!p || vars.length === 0) return out;\n // If any target var is already set, treat the credential as operator-provided.\n if (vars.some((v) => out[v])) return out;\n const key = getKey(p);\n if (!key) return out;\n for (const v of vars) out[v] = key;\n return out;\n}\n", "/**\n * set-key-args \u2014 the PURE arg-parsing + provider table for `vo-mcp set-key`.\n * Split out of set-key-cli.mjs so it can be unit-tested without importing the\n * CLI (whose module body reads stdin and runs on import). esbuild inlines it at\n * bundle time, same as the other runner-tree .mjs imports.\n */\nimport {\n resolveProvider,\n resolveRetiredProvider,\n} from '../../../scripts/virtual-office/code-runner/agent-key-store.mjs';\n\n/**\n * Per-provider metadata: display `label`, the agent `cli` binary, the\n * `selector` keyword the user types after `--provider` (NOT the binary \u2014 e.g.\n * cursor's binary is `cursor-agent`), the `envVar` fallback, and a typical\n * key-prefix `hint` for a gentle (never-blocking) format check.\n */\nexport const PROVIDERS = {\n anthropic: { label: 'Anthropic', cli: 'claude', selector: 'claude', envVar: 'ANTHROPIC_API_KEY', hint: 'sk-' },\n openai: { label: 'OpenAI / Codex', cli: 'codex', selector: 'codex', envVar: 'OPENAI_API_KEY', hint: 'sk-' },\n cursor: { label: 'Cursor', cli: 'cursor-agent', selector: 'cursor', envVar: 'CURSOR_API_KEY', hint: '' },\n // Optional \u2014 most local servers need no key; exists for locally secured\n // Ollama / LM Studio endpoints only. Never leaves the machine (like all keys).\n local: { label: 'Local models (Ollama / LM Studio)', cli: 'codex', selector: 'local', envVar: 'VO_CODE_RUNNER_LOCAL_API_KEY', hint: '' },\n meta: { label: 'Meta Model API / Muse Spark', cli: 'codex', selector: 'meta', envVar: 'MODEL_API_KEY', hint: '' },\n};\n\n/**\n * Parse `vo-mcp set-key` argv (the FULL process.argv; the subcommand is argv[2]).\n * Supports `--provider <p>` / `--provider=p` (default anthropic), `--status`,\n * `--clear`. Returns `{ provider, providerArg, mode }` where `provider` is the\n * normalized credential provider (null if the value is unknown) and `mode` is\n * 'set' | 'status' | 'clear'.\n */\nexport function parseSetKeyArgs(argv = []) {\n const rest = argv.slice(3);\n let providerArg = 'anthropic';\n let mode = 'set';\n for (let i = 0; i < rest.length; i += 1) {\n const a = rest[i];\n if (a === '--status') mode = 'status';\n else if (a === '--clear') mode = 'clear';\n else if (a === '--provider') {\n providerArg = rest[i + 1] || '';\n i += 1; // consume the value token\n } else if (a.startsWith('--provider=')) {\n providerArg = a.slice('--provider='.length);\n }\n }\n const retiredProvider = resolveRetiredProvider(providerArg);\n if (retiredProvider) return { provider: null, providerArg, mode, retiredProvider };\n return { provider: resolveProvider(providerArg), providerArg, mode };\n}\n", "#!/usr/bin/env node\n/**\n * `vo-mcp set-key` \u2014 BYO Phase-B M4 + multi-agent. Stores the friend's AI API key\n * in the OS keychain (Windows Credential Manager / macOS Keychain / libsecret) so\n * the runner can authenticate its CLI agent without an env var. The key lives ONLY\n * on the friend's machine \u2014 it never travels to Algosuite.\n *\n * vo-mcp set-key store the Anthropic (Claude) key [default]\n * vo-mcp set-key --provider codex store the OpenAI/Codex key\n * vo-mcp set-key --provider cursor store the Cursor key\n * vo-mcp set-key --provider grok --clear remove a legacy retired xAI key\n * vo-mcp set-key --provider meta store the Meta Model API key\n * vo-mcp set-key --provider local store an optional local-endpoint key (Ollama/LM Studio)\n * vo-mcp set-key --status report whether the provider's key is stored\n * vo-mcp set-key --clear remove the provider's stored key\n *\n * The key may be piped (`echo $KEY | vo-mcp set-key --provider codex`) or typed at\n * the hidden prompt. .mjs (like runner-cli.mjs) because it imports the runner-tree\n * key store, outside tsconfig rootDir \u2014 tsc skips it; esbuild inlines it at bundle.\n */\nimport {\n setAgentKey,\n hasAgentKey,\n clearAgentKey,\n clearRetiredAgentKey,\n} from '../../../scripts/virtual-office/code-runner/agent-key-store.mjs';\nimport { PROVIDERS, parseSetKeyArgs } from './set-key-args.mjs';\n\nconst log = (m) => console.error(m);\nconst mask = (k) => (k && k.length > 8 ? `\u2026${k.slice(-4)}` : '****');\n\n/** Read a secret from a TTY without echoing; falls back to piped stdin. */\nfunction readSecret(promptText) {\n return new Promise((resolve) => {\n if (!process.stdin.isTTY) {\n // Piped: `echo $KEY | vo-mcp set-key` \u2014 read everything, take the first line.\n let buf = '';\n process.stdin.setEncoding('utf8');\n process.stdin.on('data', (d) => (buf += d));\n process.stdin.on('end', () => resolve(buf.split(/\\r?\\n/)[0].trim()));\n return;\n }\n // Interactive: print the prompt, then mute the echoed characters.\n process.stdout.write(promptText);\n const stdin = process.stdin;\n stdin.setRawMode?.(true);\n stdin.resume();\n stdin.setEncoding('utf8');\n let val = '';\n const onData = (ch) => {\n if (ch === '\\r' || ch === '\\n' || ch === '\u0004') {\n stdin.setRawMode?.(false);\n stdin.pause();\n stdin.removeListener('data', onData);\n process.stdout.write('\\n');\n resolve(val.trim());\n } else if (ch === '\u0003') {\n // Ctrl-C\n process.stdout.write('\\n');\n process.exit(130);\n } else if (ch === '\u007F' || ch === '\\b') {\n val = val.slice(0, -1);\n } else {\n val += ch;\n }\n };\n stdin.on('data', onData);\n });\n}\n\nasync function main() {\n const { provider, providerArg, mode, retiredProvider } = parseSetKeyArgs(process.argv);\n if (retiredProvider) {\n if (mode === 'clear') {\n log(\n clearRetiredAgentKey(providerArg)\n ? '\u2713 Removed the legacy xAI/Grok key from your OS keychain.'\n : 'No legacy xAI/Grok key to remove (or keychain unavailable).',\n );\n return;\n }\n log('Grok/xAI support is retired. This key will not be stored or used.');\n log('To remove an old local key, run `vo-mcp set-key --provider grok --clear`.');\n process.exitCode = 1;\n return;\n }\n if (!provider) {\n log(`Unknown --provider \"${providerArg}\". Use one of: claude, codex, cursor, local, meta.`);\n process.exitCode = 1;\n return;\n }\n const info = PROVIDERS[provider];\n\n if (mode === 'status') {\n log(\n hasAgentKey(provider)\n ? `\u2713 A ${info.label} API key is stored in your OS keychain.`\n : `No ${info.label} API key stored. Run \\`vo-mcp set-key${provider === 'anthropic' ? '' : ` --provider ${info.selector}`}\\` to add one.`,\n );\n return;\n }\n\n if (mode === 'clear') {\n log(\n clearAgentKey(provider)\n ? `\u2713 Removed the stored ${info.label} API key.`\n : `No stored ${info.label} key to remove (or keychain unavailable).`,\n );\n return;\n }\n\n log(`Paste your ${info.label} API key (for the \\`${info.cli}\\` agent). It is stored ONLY in`);\n log(\"your computer's keychain \u2014 it never leaves this machine and Algosuite never sees it.\");\n const key = await readSecret(`${info.label} API key: `);\n if (!key) {\n log('No key entered \u2014 nothing stored.');\n process.exitCode = 1;\n return;\n }\n // A gentle format hint only \u2014 key formats change and Cursor's isn't `sk-`, so\n // never hard-reject a non-matching key (that would block a valid credential).\n if (info.hint && !key.startsWith(info.hint)) {\n log(`Heads up: ${info.label} keys usually start with \"${info.hint}\". Storing it anyway.`);\n }\n if (setAgentKey(provider, key)) {\n log(`\u2713 Stored your ${info.label} API key (${mask(key)}) in the OS keychain. The runner will use it automatically.`);\n } else {\n log(`Could not access the OS keychain on this machine. As a fallback, set ${info.envVar} in your environment before running \\`vo-mcp runner\\`.`);\n process.exitCode = 1;\n }\n}\n\nmain().catch((e) => {\n log(`set-key failed: ${e && e.message ? e.message : e}`);\n process.exitCode = 1;\n});\n"],
5
+ "mappings": "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",
6
6
  "names": ["require", "require"]
7
7
  }
@@ -0,0 +1,233 @@
1
+ import { createRequire as __cr } from 'module'; const require = __cr(import.meta.url);
2
+
3
+ // src/cloud/credential-store.ts
4
+ import { homedir } from "node:os";
5
+ import { join, dirname } from "node:path";
6
+ import {
7
+ existsSync,
8
+ mkdirSync,
9
+ readFileSync,
10
+ writeFileSync,
11
+ chmodSync,
12
+ rmSync
13
+ } from "node:fs";
14
+
15
+ // src/cloud/keychain.ts
16
+ import { createRequire } from "node:module";
17
+ var SERVICE = "vo-mcp";
18
+ var ACCOUNT = "refresh-credential";
19
+ var cached;
20
+ function loadKeyring() {
21
+ if (cached !== void 0) return cached;
22
+ try {
23
+ const req = createRequire(import.meta.url);
24
+ const mod = req("@napi-rs/keyring");
25
+ cached = mod && typeof mod.Entry === "function" ? mod : null;
26
+ } catch {
27
+ cached = null;
28
+ }
29
+ return cached;
30
+ }
31
+ function keychainAvailable() {
32
+ return loadKeyring() !== null;
33
+ }
34
+ function keychainGet() {
35
+ const k = loadKeyring();
36
+ if (!k) return null;
37
+ try {
38
+ return new k.Entry(SERVICE, ACCOUNT).getPassword();
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+ function keychainSet(secret) {
44
+ const k = loadKeyring();
45
+ if (!k) return false;
46
+ try {
47
+ new k.Entry(SERVICE, ACCOUNT).setPassword(secret);
48
+ return true;
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+ function keychainDelete() {
54
+ const k = loadKeyring();
55
+ if (!k) return false;
56
+ try {
57
+ return new k.Entry(SERVICE, ACCOUNT).deletePassword();
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
62
+
63
+ // src/cloud/credential-store.ts
64
+ var realKeychain = {
65
+ available: keychainAvailable,
66
+ get: keychainGet,
67
+ set: keychainSet,
68
+ delete: keychainDelete
69
+ };
70
+ function credentialPath(env = process.env) {
71
+ const override = env["VO_MCP_CREDENTIALS_PATH"]?.trim();
72
+ if (override) return override;
73
+ return join(homedir(), ".config", "vo-mcp", "credentials.json");
74
+ }
75
+ function keychainEnabled(env, keychain) {
76
+ const disabled = (env["VO_MCP_DISABLE_KEYCHAIN"] ?? "").trim().toLowerCase();
77
+ if (disabled === "1" || disabled === "true" || disabled === "yes") return false;
78
+ return keychain.available();
79
+ }
80
+ function deserialize(raw) {
81
+ try {
82
+ const parsed = JSON.parse(raw);
83
+ const refresh = typeof parsed.refresh_token === "string" ? parsed.refresh_token.trim() : "";
84
+ const apiKey = typeof parsed.api_key === "string" ? parsed.api_key.trim() : "";
85
+ const voCred = typeof parsed.vo_credential === "string" ? parsed.vo_credential.trim() : "";
86
+ if (!voCred && (!refresh || !apiKey)) return null;
87
+ return {
88
+ ...refresh ? { refresh_token: refresh } : {},
89
+ ...apiKey ? { api_key: apiKey } : {},
90
+ ...voCred ? { vo_credential: voCred } : {},
91
+ ...typeof parsed.vo_credential_expires_at === "string" ? { vo_credential_expires_at: parsed.vo_credential_expires_at } : {},
92
+ ...typeof parsed.email === "string" ? { email: parsed.email } : {},
93
+ ...typeof parsed.stored_at === "string" ? { stored_at: parsed.stored_at } : {}
94
+ };
95
+ } catch {
96
+ return null;
97
+ }
98
+ }
99
+ function readFromFile(env) {
100
+ try {
101
+ const p = credentialPath(env);
102
+ if (!existsSync(p)) return null;
103
+ return deserialize(readFileSync(p, "utf8"));
104
+ } catch {
105
+ return null;
106
+ }
107
+ }
108
+ function readStoredCredential(env = process.env, keychain = realKeychain) {
109
+ if (keychainEnabled(env, keychain)) {
110
+ const raw = keychain.get();
111
+ const fromKeychain = raw ? deserialize(raw) : null;
112
+ if (fromKeychain) return fromKeychain;
113
+ }
114
+ return readFromFile(env);
115
+ }
116
+ function readStoredCredentialKeychainOnly(env = process.env, keychain = realKeychain) {
117
+ if (!keychainEnabled(env, keychain)) return null;
118
+ const raw = keychain.get();
119
+ return raw ? deserialize(raw) : null;
120
+ }
121
+
122
+ // src/cloud/auth-token-source.ts
123
+ var FIREBASE_SECURETOKEN_URL = "https://securetoken.googleapis.com/v1/token";
124
+ var FIREBASE_TOKEN_REFERER = "https://algosuite.ai/";
125
+ var REFRESH_SKEW_MS = 6e4;
126
+ function createStaticTokenSource(token, kind = "admin-token") {
127
+ const value = token.trim();
128
+ return { kind, getToken: async () => value.length > 0 ? value : null };
129
+ }
130
+ function createFirebaseRefreshTokenSource(opts) {
131
+ const refreshToken = opts.refreshToken.trim();
132
+ const apiKey = opts.apiKey.trim();
133
+ const now = opts.now ?? (() => Date.now());
134
+ const fetchFn = opts.fetchFn ?? globalThis.fetch;
135
+ let cachedToken = null;
136
+ let expiresAtMs = 0;
137
+ let inFlight = null;
138
+ async function refresh() {
139
+ try {
140
+ const res = await fetchFn(`${FIREBASE_SECURETOKEN_URL}?key=${encodeURIComponent(apiKey)}`, {
141
+ method: "POST",
142
+ headers: {
143
+ "content-type": "application/x-www-form-urlencoded",
144
+ referer: FIREBASE_TOKEN_REFERER
145
+ },
146
+ body: `grant_type=refresh_token&refresh_token=${encodeURIComponent(refreshToken)}`
147
+ });
148
+ const text = await res.text();
149
+ if (res.status < 200 || res.status >= 300) {
150
+ cachedToken = null;
151
+ return null;
152
+ }
153
+ const parsed = JSON.parse(text);
154
+ const idToken = typeof parsed.id_token === "string" ? parsed.id_token : "";
155
+ if (!idToken) {
156
+ cachedToken = null;
157
+ return null;
158
+ }
159
+ const expiresInSec = Number(parsed.expires_in);
160
+ const ttlMs = Number.isFinite(expiresInSec) && expiresInSec > 0 ? expiresInSec * 1e3 : 36e5;
161
+ cachedToken = idToken;
162
+ expiresAtMs = now() + ttlMs;
163
+ return idToken;
164
+ } catch {
165
+ cachedToken = null;
166
+ return null;
167
+ }
168
+ }
169
+ return {
170
+ kind: "firebase-refresh",
171
+ async getToken() {
172
+ if (cachedToken && now() < expiresAtMs - REFRESH_SKEW_MS) return cachedToken;
173
+ if (!inFlight) {
174
+ inFlight = refresh().finally(() => {
175
+ inFlight = null;
176
+ });
177
+ }
178
+ return inFlight;
179
+ }
180
+ };
181
+ }
182
+ function createAuthTokenSourceFromEnv(env = process.env, fetchFn, readStoredCred = () => null) {
183
+ const refreshToken = env["VO_USER_REFRESH_TOKEN"]?.trim();
184
+ const apiKey = env["VO_FIREBASE_API_KEY"]?.trim();
185
+ const idToken = env["VO_USER_ID_TOKEN"]?.trim();
186
+ const adminToken = env["VO_CONTROL_PLANE_ADMIN_TOKEN"]?.trim();
187
+ if (refreshToken || apiKey) {
188
+ if (!refreshToken || !apiKey) {
189
+ throw new Error(
190
+ "Per-user refresh auth requires BOTH VO_USER_REFRESH_TOKEN and VO_FIREBASE_API_KEY"
191
+ );
192
+ }
193
+ return createFirebaseRefreshTokenSource({
194
+ refreshToken,
195
+ apiKey,
196
+ ...fetchFn ? { fetchFn } : {}
197
+ });
198
+ }
199
+ if (idToken) return createStaticTokenSource(idToken, "firebase-id-token");
200
+ const stored = readStoredCred();
201
+ if (stored?.vo_credential && stored.vo_credential.trim()) {
202
+ return createStaticTokenSource(stored.vo_credential.trim(), "vo-credential");
203
+ }
204
+ if (stored && stored.refresh_token?.trim() && stored.api_key?.trim()) {
205
+ return createFirebaseRefreshTokenSource({
206
+ refreshToken: stored.refresh_token.trim(),
207
+ apiKey: stored.api_key.trim(),
208
+ ...fetchFn ? { fetchFn } : {}
209
+ });
210
+ }
211
+ if (adminToken) return createStaticTokenSource(adminToken, "admin-token");
212
+ return null;
213
+ }
214
+
215
+ // src/supervisor-credential-helper.mjs
216
+ var keychainOnly = process.argv.includes("--control-plane-auth-keychain-only");
217
+ var credential = keychainOnly ? readStoredCredentialKeychainOnly() : readStoredCredential();
218
+ if (process.argv.includes("--control-plane-auth") || keychainOnly) {
219
+ const source = createAuthTokenSourceFromEnv(process.env, void 0, () => credential);
220
+ const token = await source?.getToken();
221
+ if (!token) {
222
+ process.stderr.write("control-plane credential was not found\n");
223
+ process.exitCode = 2;
224
+ } else {
225
+ process.stdout.write(JSON.stringify({ token }));
226
+ }
227
+ } else if (!credential) {
228
+ process.stderr.write("paired runner credential was not found\n");
229
+ process.exitCode = 2;
230
+ } else {
231
+ process.stdout.write(JSON.stringify(credential));
232
+ }
233
+ //# sourceMappingURL=supervisor-credential-helper.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/cloud/credential-store.ts", "../src/cloud/keychain.ts", "../src/cloud/auth-token-source.ts", "../src/supervisor-credential-helper.mjs"],
4
+ "sourcesContent": ["/**\n * Local credential store for the thin-client `vo-mcp login` flow (Increment 3b,\n * Option A \u2014 `docs/vo/vo-command-center-inc3b-login-design-2026-06-05.md`).\n *\n * Persists the per-user Firebase refresh token (the user's OWN credential, never\n * the god-token, never model keys) captured by `login`, so the auto-refreshing\n * token source (Inc 3a) can mint fresh ID tokens across MCP restarts.\n *\n * Storage precedence (Inc 3b.3):\n * 1. **OS keychain** (Windows Credential Manager / macOS Keychain / libsecret)\n * via the optional `@napi-rs/keyring` backend (`keychain.ts`). The DEFAULT\n * when available \u2014 the secret never lands in plaintext on disk.\n * 2. **0600 file** at `$VO_MCP_CREDENTIALS_PATH` or `~/.config/vo-mcp/credentials.json`.\n * The fallback when the keychain is unavailable or disabled\n * (`VO_MCP_DISABLE_KEYCHAIN`). `VO_MCP_CREDENTIALS_PATH` only sets the file\n * LOCATION; force file storage with `VO_MCP_DISABLE_KEYCHAIN`.\n *\n * `env`-supplied tokens (`VO_USER_REFRESH_TOKEN`, etc.) still win over BOTH\n * stores \u2014 that precedence lives upstream in `auth-token-source.ts`.\n *\n * **Single source of truth.** The credential lives in EITHER the keychain OR the\n * file, never both: a write to one store CLEARS the other, so a stale entry can\n * never shadow the current credential on read, and the secret never lingers in\n * plaintext after a migration to the keychain.\n *\n * **Keychain durability.** A keychain-stored credential is only readable while\n * the `@napi-rs/keyring` native module loads. If the module later becomes\n * unavailable (an ABI break across a Node upgrade, a corrupted install), the\n * credential can't be read and the user re-runs `vo-mcp login` \u2014 the same\n * behaviour as `gh` / `gcloud` / `firebase` keychain storage. We deliberately do\n * NOT mirror the secret to a plaintext file as a fallback: that would defeat the\n * entire point of keychain storage (keeping the secret off plaintext disk).\n */\nimport { homedir } from 'node:os';\nimport { join, dirname } from 'node:path';\nimport {\n existsSync,\n mkdirSync,\n readFileSync,\n writeFileSync,\n chmodSync,\n rmSync,\n} from 'node:fs';\n\nimport { keychainAvailable, keychainGet, keychainSet, keychainDelete } from './keychain.js';\n\nexport interface StoredCredential {\n /**\n * Firebase refresh token (long-lived; exchanged for short-lived ID tokens).\n * OPTIONAL since Inc 3b.4b: once a scoped `vo_credential` is minted, the raw\n * refresh token is dropped, so a stored credential may carry ONLY the vocred_.\n */\n readonly refresh_token?: string;\n /** Firebase Web API key (PUBLIC) needed for the securetoken refresh exchange. */\n readonly api_key?: string;\n /**\n * Scoped, revocable VO credential (`vocred_`) minted by the control-plane\n * (Inc 3b.4b). Preferred over the raw refresh token; lets the client present a\n * revocable, server-side credential instead of the Firebase refresh token.\n */\n readonly vo_credential?: string;\n /** ISO-8601 expiry of `vo_credential` (the client re-logs-in past this). */\n readonly vo_credential_expires_at?: string;\n /** The signed-in operator email (diagnostics only). */\n readonly email?: string;\n /** ISO timestamp the credential was stored. */\n readonly stored_at?: string;\n}\n\n/**\n * Pluggable OS-keychain backend. Defaults to the real `@napi-rs/keyring` wrapper;\n * tests inject a deterministic fake so they never touch the host keychain.\n */\nexport interface KeychainBackend {\n available(): boolean;\n get(): string | null;\n set(secret: string): boolean;\n delete(): boolean;\n}\n\nconst realKeychain: KeychainBackend = {\n available: keychainAvailable,\n get: keychainGet,\n set: keychainSet,\n delete: keychainDelete,\n};\n\n/** Human-readable \"location\" returned when the credential was stored in the OS keychain. */\nexport const KEYCHAIN_LOCATION = 'OS keychain (service \"vo-mcp\")';\n\n/** Resolve the credentials file path (env override \u2192 XDG-ish default under home). */\nexport function credentialPath(env: Readonly<Record<string, string | undefined>> = process.env): string {\n const override = env['VO_MCP_CREDENTIALS_PATH']?.trim();\n if (override) return override;\n return join(homedir(), '.config', 'vo-mcp', 'credentials.json');\n}\n\n/**\n * Whether the keychain should be consulted at all (read OR write). False when the\n * native backend is unavailable or `VO_MCP_DISABLE_KEYCHAIN` is set (CI/headless).\n */\nfunction keychainEnabled(\n env: Readonly<Record<string, string | undefined>>,\n keychain: KeychainBackend,\n): boolean {\n const disabled = (env['VO_MCP_DISABLE_KEYCHAIN'] ?? '').trim().toLowerCase();\n if (disabled === '1' || disabled === 'true' || disabled === 'yes') return false;\n return keychain.available();\n}\n\n/** Parse + validate a stored credential blob. Returns null on any problem (never throws). */\nfunction deserialize(raw: string): StoredCredential | null {\n try {\n const parsed = JSON.parse(raw) as Partial<StoredCredential>;\n const refresh = typeof parsed.refresh_token === 'string' ? parsed.refresh_token.trim() : '';\n const apiKey = typeof parsed.api_key === 'string' ? parsed.api_key.trim() : '';\n const voCred = typeof parsed.vo_credential === 'string' ? parsed.vo_credential.trim() : '';\n // Valid if it carries a scoped vocred_ OR a full Firebase refresh pair.\n if (!voCred && (!refresh || !apiKey)) return null;\n return {\n ...(refresh ? { refresh_token: refresh } : {}),\n ...(apiKey ? { api_key: apiKey } : {}),\n ...(voCred ? { vo_credential: voCred } : {}),\n ...(typeof parsed.vo_credential_expires_at === 'string' ? { vo_credential_expires_at: parsed.vo_credential_expires_at } : {}),\n ...(typeof parsed.email === 'string' ? { email: parsed.email } : {}),\n ...(typeof parsed.stored_at === 'string' ? { stored_at: parsed.stored_at } : {}),\n };\n } catch {\n return null;\n }\n}\n\nfunction readFromFile(env: Readonly<Record<string, string | undefined>>): StoredCredential | null {\n try {\n const p = credentialPath(env);\n if (!existsSync(p)) return null;\n return deserialize(readFileSync(p, 'utf8'));\n } catch {\n return null;\n }\n}\n\n/**\n * Read the stored credential, or `null` if absent/unreadable/invalid (never\n * throws). Consults an ENABLED keychain first (regardless of the write-target\n * flags, so a credential written to the keychain is found even if\n * `VO_MCP_CREDENTIALS_PATH` is later set), then the 0600 file.\n */\nexport function readStoredCredential(\n env: Readonly<Record<string, string | undefined>> = process.env,\n keychain: KeychainBackend = realKeychain,\n): StoredCredential | null {\n if (keychainEnabled(env, keychain)) {\n const raw = keychain.get();\n const fromKeychain = raw ? deserialize(raw) : null;\n if (fromKeychain) return fromKeychain;\n }\n return readFromFile(env);\n}\n\n/** Read only the OS keychain; never fall back to env-selected plaintext files. */\nexport function readStoredCredentialKeychainOnly(\n env: Readonly<Record<string, string | undefined>> = process.env,\n keychain: KeychainBackend = realKeychain,\n): StoredCredential | null {\n if (!keychainEnabled(env, keychain)) return null;\n const raw = keychain.get();\n return raw ? deserialize(raw) : null;\n}\n\nfunction deleteFile(env: Readonly<Record<string, string | undefined>>): void {\n try {\n rmSync(credentialPath(env), { force: true });\n } catch {\n /* best-effort */\n }\n}\n\nfunction writeToFile(\n payload: StoredCredential,\n env: Readonly<Record<string, string | undefined>>,\n): string {\n const p = credentialPath(env);\n mkdirSync(dirname(p), { recursive: true });\n writeFileSync(p, `${JSON.stringify(payload, null, 2)}\\n`, { mode: 0o600 });\n // Best-effort tighten (no-op / throws on some Windows filesystems \u2014 ignore).\n try {\n chmodSync(p, 0o600);\n } catch {\n /* best-effort */\n }\n return p;\n}\n\n/**\n * Persist the credential. Prefers the OS keychain (secret never hits plaintext\n * disk); otherwise writes the 0600 file. Writing to one store CLEARS the other\n * (single source of truth \u2014 no stale shadow, no lingering plaintext). Returns the\n * location it was stored (`KEYCHAIN_LOCATION` or the file path).\n */\nexport function writeStoredCredential(\n cred: StoredCredential,\n storedAt: string,\n env: Readonly<Record<string, string | undefined>> = process.env,\n keychain: KeychainBackend = realKeychain,\n): string {\n const payload: StoredCredential = {\n ...(cred.refresh_token ? { refresh_token: cred.refresh_token } : {}),\n ...(cred.api_key ? { api_key: cred.api_key } : {}),\n ...(cred.vo_credential ? { vo_credential: cred.vo_credential } : {}),\n ...(cred.vo_credential_expires_at ? { vo_credential_expires_at: cred.vo_credential_expires_at } : {}),\n ...(cred.email ? { email: cred.email } : {}),\n stored_at: cred.stored_at ?? storedAt,\n };\n if (keychainEnabled(env, keychain) && keychain.set(JSON.stringify(payload))) {\n // Stored in the keychain \u2192 clear any stale plaintext file so the secret\n // doesn't linger on disk and can't shadow the keychain on read.\n deleteFile(env);\n return KEYCHAIN_LOCATION;\n }\n const p = writeToFile(payload, env);\n // Stored in the file \u2192 clear any stale keychain entry so it can't shadow the\n // newer file credential on read.\n if (keychainEnabled(env, keychain)) keychain.delete();\n return p;\n}\n", "/**\n * Optional OS-keychain backend for the thin-client credential store (Increment\n * 3b.3 \u2014 `docs/vo/vo-command-center-inc3b-login-design-2026-06-05.md` \u00A75/\u00A76).\n *\n * Loads `@napi-rs/keyring` at runtime via `createRequire`, so it is a TRUE\n * optional dependency: if the native module is absent or fails to load\n * (unsupported platform, prebuilt binary missing, headless CI), every function\n * degrades to a no-op and the caller (`credential-store.ts`) falls back to the\n * 0600 file store. `@napi-rs/keyring`'s `Entry` API is SYNCHRONOUS, so the\n * credential store stays synchronous \u2014 no async ripple into the Inc-3a token\n * source that reads it.\n *\n * Why `createRequire` and not a static/dynamic `import`: a static import would\n * make the native module a HARD dependency (a missing prebuilt would crash the\n * MCP at startup); a dynamic `import()` is async (would force the whole read\n * path async). `createRequire(...)` inside a try/catch loads it lazily and\n * synchronously, and a load failure is just \"keychain unavailable\".\n */\nimport { createRequire } from 'node:module';\n\n/** Keychain service + account the single refresh credential is stored under. */\nconst SERVICE = 'vo-mcp';\nconst ACCOUNT = 'refresh-credential';\n\ninterface KeyringEntry {\n getPassword(): string | null;\n setPassword(password: string): void;\n deletePassword(): boolean;\n}\ninterface KeyringModule {\n Entry: new (service: string, account: string) => KeyringEntry;\n}\n\n// undefined = not yet attempted; null = attempted and unavailable.\nlet cached: KeyringModule | null | undefined;\n\nfunction loadKeyring(): KeyringModule | null {\n if (cached !== undefined) return cached;\n try {\n const req = createRequire(import.meta.url);\n const mod = req('@napi-rs/keyring') as Partial<KeyringModule>;\n cached = mod && typeof mod.Entry === 'function' ? (mod as KeyringModule) : null;\n } catch {\n cached = null;\n }\n return cached;\n}\n\n/** True when the OS keychain backend is usable in this runtime. */\nexport function keychainAvailable(): boolean {\n return loadKeyring() !== null;\n}\n\n/** Read the raw stored secret string from the OS keychain, or null. Never throws. */\nexport function keychainGet(): string | null {\n const k = loadKeyring();\n if (!k) return null;\n try {\n return new k.Entry(SERVICE, ACCOUNT).getPassword();\n } catch {\n return null;\n }\n}\n\n/** Store the raw secret string in the OS keychain. Returns true on success. Never throws. */\nexport function keychainSet(secret: string): boolean {\n const k = loadKeyring();\n if (!k) return false;\n try {\n new k.Entry(SERVICE, ACCOUNT).setPassword(secret);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Delete the stored secret from the OS keychain. Returns true if an entry was\n * removed. Never throws \u2014 a no-op (and `false`) when the backend is unavailable\n * or the entry is absent. Used to keep ONE source of truth: when the credential\n * is (re)written to the file, any stale keychain entry is cleared so it can't\n * shadow the newer file credential on read (and vice-versa).\n */\nexport function keychainDelete(): boolean {\n const k = loadKeyring();\n if (!k) return false;\n try {\n return new k.Entry(SERVICE, ACCOUNT).deletePassword();\n } catch {\n return false;\n }\n}\n\n/** Test-only seam to reset the memoised module load. */\nexport function __resetKeychainCache(): void {\n cached = undefined;\n}\n", "/**\n * Auth token sources for the cloud admin client \u2014 Increment 3 of the VO Command\n * Center unification (`docs/vo/vo-command-center-unification-spec-2026-06-05.md`\n * \u00A76): retire the shared god admin-token from the LOCAL install. Instead of a\n * single static `VO_CONTROL_PLANE_ADMIN_TOKEN`, the client can authenticate as\n * the USER with a short-lived Firebase ID token (the same credential the\n * dashboard sends; the control-plane already accepts it for an allow-listed\n * operator \u2014 `cloud-run/vo-control-plane/src/firebase-auth.ts`).\n *\n * Three sources, selected by env (per-user preferred):\n * 1. `VO_USER_REFRESH_TOKEN` (+ `VO_FIREBASE_API_KEY`) \u2192 auto-refreshing Firebase\n * user token (durable; exchanges the refresh token for fresh ID tokens via\n * the PUBLIC securetoken endpoint \u2014 no backend, no god-token, no model keys).\n * 2. `VO_USER_ID_TOKEN` \u2192 a raw Firebase ID token (simplest interim; expires ~1h).\n * 3. `VO_CONTROL_PLANE_ADMIN_TOKEN` \u2192 the legacy static god-token (back-compat).\n *\n * FAIL-CLOSED: a source that cannot produce a token returns `null`; the caller\n * (admin client) then refuses the request rather than sending an empty Bearer.\n *\n * NOTE: this slice removes the god-token from the *client config* (the user's\n * own credential is sent instead). Acquiring the initial refresh/ID token via a\n * browser/device-flow `login` command, and OS-keychain storage, are follow-up\n * slices (spec \u00A77); for now the token is supplied via env.\n */\n\n/** A source of a fresh bearer token for the control-plane. */\nexport interface AuthTokenSource {\n /** Stable label for diagnostics/logs \u2014 NEVER the token value. */\n readonly kind: 'admin-token' | 'firebase-id-token' | 'firebase-refresh' | 'vo-credential';\n /** Returns a fresh bearer token, or `null` when unavailable (fail-closed). */\n getToken(): Promise<string | null>;\n}\n\ntype FetchLike = (\n url: string,\n init?: { method?: string; headers?: Record<string, string>; body?: string },\n) => Promise<{ status: number; text: () => Promise<string> }>;\n\n/** Public Google endpoint that exchanges a Firebase refresh token for a fresh ID token. */\nexport const FIREBASE_SECURETOKEN_URL = 'https://securetoken.googleapis.com/v1/token';\n\n/**\n * The Algosuite Firebase Web API key is HTTP-referrer-restricted, and Google's\n * Identity Toolkit / securetoken endpoints reject key'd requests that arrive with\n * an EMPTY Referer (HTTP 403 \"Requests from referer <empty> are blocked\"). Browser\n * callers send one automatically; Node `fetch` sends none \u2014 so every server-side\n * exchange must pin an origin the key's restriction allows. Live-diagnosed\n * 2026-06-09: without this header the whole Inc 3a refresh flow (and the Inc 3b.4b\n * vocred_ exchange that builds on it) fail-opens silently.\n */\nexport const FIREBASE_TOKEN_REFERER = 'https://algosuite.ai/';\n\n/** Refresh this many ms BEFORE the ID token actually expires (clock-skew margin). */\nconst REFRESH_SKEW_MS = 60_000;\n\n/** A fixed, already-available token (the god-token or a pasted ID token). */\nexport function createStaticTokenSource(\n token: string,\n kind: AuthTokenSource['kind'] = 'admin-token',\n): AuthTokenSource {\n const value = token.trim();\n return { kind, getToken: async () => (value.length > 0 ? value : null) };\n}\n\nexport interface FirebaseRefreshTokenSourceOptions {\n readonly refreshToken: string;\n /** The Firebase Web API key (PUBLIC, not a secret) for the Algosuite project. */\n readonly apiKey: string;\n /** Injectable clock (ms) for tests; defaults to `Date.now`. */\n readonly now?: () => number;\n /** Injectable fetch for tests; defaults to the global `fetch`. */\n readonly fetchFn?: FetchLike;\n}\n\n/**\n * Auto-refreshing Firebase user token. Exchanges the long-lived refresh token\n * for short-lived ID tokens via the public securetoken endpoint and caches the\n * ID token until shortly before it expires. Fail-closed: any error \u21D2 `null`.\n */\nexport function createFirebaseRefreshTokenSource(\n opts: FirebaseRefreshTokenSourceOptions,\n): AuthTokenSource {\n const refreshToken = opts.refreshToken.trim();\n const apiKey = opts.apiKey.trim();\n const now = opts.now ?? (() => Date.now());\n const fetchFn: FetchLike = opts.fetchFn ?? (globalThis.fetch as unknown as FetchLike);\n\n let cachedToken: string | null = null;\n let expiresAtMs = 0;\n // Collapse concurrent refreshes so N parallel tool calls trigger ONE exchange.\n let inFlight: Promise<string | null> | null = null;\n\n async function refresh(): Promise<string | null> {\n try {\n const res = await fetchFn(`${FIREBASE_SECURETOKEN_URL}?key=${encodeURIComponent(apiKey)}`, {\n method: 'POST',\n headers: {\n 'content-type': 'application/x-www-form-urlencoded',\n referer: FIREBASE_TOKEN_REFERER,\n },\n body: `grant_type=refresh_token&refresh_token=${encodeURIComponent(refreshToken)}`,\n });\n const text = await res.text();\n if (res.status < 200 || res.status >= 300) {\n cachedToken = null;\n return null;\n }\n const parsed = JSON.parse(text) as { id_token?: unknown; expires_in?: unknown };\n const idToken = typeof parsed.id_token === 'string' ? parsed.id_token : '';\n if (!idToken) {\n cachedToken = null;\n return null;\n }\n const expiresInSec = Number(parsed.expires_in);\n const ttlMs = Number.isFinite(expiresInSec) && expiresInSec > 0 ? expiresInSec * 1000 : 3_600_000;\n cachedToken = idToken;\n expiresAtMs = now() + ttlMs;\n return idToken;\n } catch {\n cachedToken = null;\n return null;\n }\n }\n\n return {\n kind: 'firebase-refresh',\n async getToken(): Promise<string | null> {\n if (cachedToken && now() < expiresAtMs - REFRESH_SKEW_MS) return cachedToken;\n if (!inFlight) {\n inFlight = refresh().finally(() => {\n inFlight = null;\n });\n }\n return inFlight;\n },\n };\n}\n\n/**\n * Select an auth token source from env. Per-user credentials win over the legacy\n * god-token, so a thin install configured with the user's token needs NO\n * `VO_CONTROL_PLANE_ADMIN_TOKEN`. Returns `null` when nothing is configured.\n * Throws on a half-configured refresh source (refresh token without API key).\n */\n/** Minimal stored-credential shape this selector consumes (from `vo-mcp login`). */\nexport interface StoredRefreshCredential {\n /** Firebase refresh token (optional once a scoped vocred_ is minted \u2014 Inc 3b.4b). */\n readonly refresh_token?: string;\n readonly api_key?: string;\n /** Scoped, revocable VO credential (Inc 3b.4b) \u2014 preferred over the raw refresh. */\n readonly vo_credential?: string;\n}\n\nexport function createAuthTokenSourceFromEnv(\n env: Readonly<Record<string, string | undefined>> = process.env,\n fetchFn?: FetchLike,\n /**\n * Inc 3b: a reader for a stored login credential (`vo-mcp login`). Injected so\n * the env-only callers (and tests) stay filesystem-free; production passes\n * `readStoredCredential`. A stored login credential is preferred over the legacy\n * god-token (so logging in retires it) but explicit env user-tokens still win.\n */\n readStoredCred: () => StoredRefreshCredential | null = () => null,\n): AuthTokenSource | null {\n const refreshToken = env['VO_USER_REFRESH_TOKEN']?.trim();\n const apiKey = env['VO_FIREBASE_API_KEY']?.trim();\n const idToken = env['VO_USER_ID_TOKEN']?.trim();\n const adminToken = env['VO_CONTROL_PLANE_ADMIN_TOKEN']?.trim();\n\n // 1. explicit env per-user refresh (CI / override).\n if (refreshToken || apiKey) {\n if (!refreshToken || !apiKey) {\n throw new Error(\n 'Per-user refresh auth requires BOTH VO_USER_REFRESH_TOKEN and VO_FIREBASE_API_KEY',\n );\n }\n return createFirebaseRefreshTokenSource({\n refreshToken,\n apiKey,\n ...(fetchFn ? { fetchFn } : {}),\n });\n }\n // 2. explicit env ID token.\n if (idToken) return createStaticTokenSource(idToken, 'firebase-id-token');\n // 3. stored login credential (`vo-mcp login`) \u2014 preferred over the god-token.\n const stored = readStoredCred();\n // 3a. a scoped vocred_ (Inc 3b.4b) wins \u2014 revocable, server-validated, and it\n // means no raw Firebase refresh token sits on disk. Sent as a static bearer;\n // the control-plane validates expiry/revocation (a stale one \u21D2 401 \u21D2 re-login).\n if (stored?.vo_credential && stored.vo_credential.trim()) {\n return createStaticTokenSource(stored.vo_credential.trim(), 'vo-credential');\n }\n // 3b. else the Firebase refresh credential.\n if (stored && stored.refresh_token?.trim() && stored.api_key?.trim()) {\n return createFirebaseRefreshTokenSource({\n refreshToken: stored.refresh_token.trim(),\n apiKey: stored.api_key.trim(),\n ...(fetchFn ? { fetchFn } : {}),\n });\n }\n // 4. legacy static god-token (back-compat).\n if (adminToken) return createStaticTokenSource(adminToken, 'admin-token');\n return null;\n}\n", "/**\n * Read the paired runner credential in a disposable process.\n *\n * On Windows, @napi-rs/keyring loads a native DLL. Keeping that DLL loaded in\n * the long-lived supervisor prevents npm from replacing the globally installed\n * package during a self-update. This helper exits before maintenance starts, so\n * Windows releases the native module while the supervisor retains only the\n * parsed credential in memory.\n */\nimport { readStoredCredential, readStoredCredentialKeychainOnly } from './cloud/credential-store.js';\nimport { createAuthTokenSourceFromEnv } from './cloud/auth-token-source.js';\n\nconst keychainOnly = process.argv.includes('--control-plane-auth-keychain-only');\nconst credential = keychainOnly ? readStoredCredentialKeychainOnly() : readStoredCredential();\nif (process.argv.includes('--control-plane-auth') || keychainOnly) {\n const source = createAuthTokenSourceFromEnv(process.env, undefined, () => credential);\n const token = await source?.getToken();\n if (!token) {\n process.stderr.write('control-plane credential was not found\\n');\n process.exitCode = 2;\n } else {\n process.stdout.write(JSON.stringify({ token }));\n }\n} else if (!credential) {\n process.stderr.write('paired runner credential was not found\\n');\n process.exitCode = 2;\n} else {\n process.stdout.write(JSON.stringify(credential));\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire as __cr } from 'module'; const require = __cr(import.meta.url);
3
+
4
+ // src/update.ts
5
+ import { spawnSync as spawnSync2 } from "node:child_process";
6
+ import { createRequire } from "node:module";
7
+
8
+ // ../../scripts/virtual-office/code-runner/runner-host-maintenance.mjs
9
+ import { spawnSync } from "node:child_process";
10
+ import { existsSync } from "node:fs";
11
+ import { win32 } from "node:path";
12
+ var DEFAULT_RUNNER_PACKAGE = "@algosuite/vo-mcp@beta";
13
+ var PACKAGE_SPEC_RE = /^@algosuite\/vo-mcp@(beta|latest|\d+(?:\.\d+){0,2}(?:-[\w.-]+)?)$/u;
14
+ var NPM_CLI_SUFFIX = `\\${win32.join("node_modules", "npm", "bin", "npm-cli.js").toLowerCase()}`;
15
+ function isNpmCliPath(value) {
16
+ return typeof value === "string" && win32.isAbsolute(value) && win32.normalize(value).toLowerCase().endsWith(NPM_CLI_SUFFIX);
17
+ }
18
+ function resolveNpmCli({
19
+ env = process.env,
20
+ execPath = process.execPath,
21
+ fileExists = existsSync
22
+ } = {}) {
23
+ const candidates = [];
24
+ if (isNpmCliPath(env.npm_execpath)) candidates.push(env.npm_execpath);
25
+ candidates.push(win32.join(win32.dirname(execPath), "node_modules", "npm", "bin", "npm-cli.js"));
26
+ const pathValue = env.PATH ?? env.Path ?? env.path ?? "";
27
+ for (const entry of pathValue.split(";")) {
28
+ const trimmed = entry.trim();
29
+ if (!win32.isAbsolute(trimmed)) continue;
30
+ candidates.push(win32.join(trimmed, "node_modules", "npm", "bin", "npm-cli.js"));
31
+ }
32
+ const seen = /* @__PURE__ */ new Set();
33
+ for (const candidate of candidates) {
34
+ const normalized = win32.normalize(candidate);
35
+ const key = normalized.toLowerCase();
36
+ if (seen.has(key) || !isNpmCliPath(normalized)) continue;
37
+ seen.add(key);
38
+ if (fileExists(normalized)) return normalized;
39
+ }
40
+ return null;
41
+ }
42
+ function buildMaintenanceCommand(kind, {
43
+ platform = process.platform,
44
+ packageSpec: packageSpec2 = DEFAULT_RUNNER_PACKAGE,
45
+ env = process.env,
46
+ execPath = process.execPath,
47
+ fileExists = existsSync
48
+ } = {}) {
49
+ if (!["update", "reinstall"].includes(kind)) return null;
50
+ if (!PACKAGE_SPEC_RE.test(packageSpec2)) throw new Error("unsafe runner package spec");
51
+ const npmCli = platform === "win32" ? resolveNpmCli({ env, execPath, fileExists }) : null;
52
+ if (platform === "win32" && !npmCli) throw new Error("trusted npm CLI not found");
53
+ const command = platform === "win32" ? execPath : "npm";
54
+ const args = [...npmCli ? [npmCli] : [], "install", "-g", packageSpec2];
55
+ if (kind === "reinstall") args.push("--force");
56
+ return { command, args };
57
+ }
58
+
59
+ // src/update.ts
60
+ var DEFAULT_UPDATE_SPEC = "@algosuite/vo-mcp@beta";
61
+ function packageVersion() {
62
+ try {
63
+ return createRequire(import.meta.url)("../package.json").version || "unknown";
64
+ } catch {
65
+ return "unknown";
66
+ }
67
+ }
68
+ function buildUpdateCommand(packageSpec2 = DEFAULT_UPDATE_SPEC, platform = process.platform, runtime = {}) {
69
+ const command = buildMaintenanceCommand("update", {
70
+ packageSpec: packageSpec2,
71
+ platform,
72
+ env: runtime.env,
73
+ execPath: runtime.execPath,
74
+ fileExists: runtime.fileExists
75
+ });
76
+ if (!command) throw new Error("update command unavailable");
77
+ return command;
78
+ }
79
+ function updateVoMcp(opts = {}) {
80
+ const log = opts.log ?? ((message) => console.error(message));
81
+ const platform = opts.platform ?? process.platform;
82
+ const packageSpec2 = opts.packageSpec?.trim() || DEFAULT_UPDATE_SPEC;
83
+ let command;
84
+ let args;
85
+ try {
86
+ ({ command, args } = buildUpdateCommand(packageSpec2, platform, opts));
87
+ } catch (error) {
88
+ log(`Update unavailable: ${error instanceof Error ? error.message : String(error)}`);
89
+ return { ok: false, status: 2, command: "", args: [], skipped: false };
90
+ }
91
+ log("=== vo-mcp updater ===");
92
+ log(`Current installed package version: ${packageVersion()}`);
93
+ log(`Updating MCP + runner with: ${command} ${args.join(" ")}`);
94
+ if (opts.dryRun) {
95
+ log("Dry run only - no package changes made.");
96
+ return { ok: true, status: 0, command, args, skipped: true };
97
+ }
98
+ const run = opts.spawn ?? spawnSync2;
99
+ const result2 = run(command, args, {
100
+ stdio: "inherit",
101
+ shell: false,
102
+ windowsHide: true,
103
+ env: opts.env ?? process.env
104
+ });
105
+ const status = typeof result2.status === "number" ? result2.status : 1;
106
+ if (status !== 0) {
107
+ log(`Update failed with exit code ${status}.`);
108
+ return { ok: false, status, command, args, skipped: false };
109
+ }
110
+ log("\nUpdate complete.");
111
+ log("Restart any running `vo-mcp runner` process so it loads the new bundle.");
112
+ log("If Claude Desktop / Claude Code was open, restart it to reload the MCP server.");
113
+ return { ok: true, status: 0, command, args, skipped: false };
114
+ }
115
+
116
+ // src/update-cli.ts
117
+ function argValue(name) {
118
+ const index = process.argv.indexOf(name);
119
+ return index >= 0 ? process.argv[index + 1] : void 0;
120
+ }
121
+ var packageSpec = argValue("--package") || DEFAULT_UPDATE_SPEC;
122
+ var dryRun = process.argv.includes("--dry-run");
123
+ var result = updateVoMcp({ packageSpec, dryRun });
124
+ process.exit(result.ok ? 0 : result.status || 1);
125
+ //# sourceMappingURL=update-cli.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/update.ts", "../../../scripts/virtual-office/code-runner/runner-host-maintenance.mjs", "../src/update-cli.ts"],
4
+ "sourcesContent": ["/**\n * `vo-mcp update` - product-level updater for the MCP + runner package.\n *\n * Users should not have to uninstall/reinstall to pick up a new bundled runner.\n * The package is installed globally, so updating the single npm package updates\n * both `vo-mcp` and `vo-mcp runner` together.\n */\nimport { spawnSync } from 'node:child_process';\nimport { createRequire } from 'node:module';\nimport { buildMaintenanceCommand } from '../../../scripts/virtual-office/code-runner/runner-host-maintenance.mjs';\n\nexport const DEFAULT_UPDATE_SPEC = '@algosuite/vo-mcp@beta';\n\nexport interface UpdateOptions {\n readonly packageSpec?: string;\n readonly dryRun?: boolean;\n readonly log?: (message: string) => void;\n readonly spawn?: typeof spawnSync;\n readonly platform?: NodeJS.Platform;\n readonly env?: NodeJS.ProcessEnv;\n readonly execPath?: string;\n readonly fileExists?: (path: string) => boolean;\n}\n\nexport interface UpdateResult {\n readonly ok: boolean;\n readonly status: number;\n readonly command: string;\n readonly args: readonly string[];\n readonly skipped: boolean;\n}\n\nfunction packageVersion(): string {\n try {\n return createRequire(import.meta.url)('../package.json').version || 'unknown';\n } catch {\n return 'unknown';\n }\n}\n\nexport function buildUpdateCommand(\n packageSpec = DEFAULT_UPDATE_SPEC,\n platform: NodeJS.Platform = process.platform,\n runtime: Pick<UpdateOptions, 'env' | 'execPath' | 'fileExists'> = {},\n): { command: string; args: string[] } {\n const command = buildMaintenanceCommand('update', {\n packageSpec,\n platform,\n env: runtime.env,\n execPath: runtime.execPath,\n fileExists: runtime.fileExists,\n });\n if (!command) throw new Error('update command unavailable');\n return command;\n}\n\nexport function updateVoMcp(opts: UpdateOptions = {}): UpdateResult {\n const log = opts.log ?? ((message: string) => console.error(message));\n const platform = opts.platform ?? process.platform;\n const packageSpec = opts.packageSpec?.trim() || DEFAULT_UPDATE_SPEC;\n let command: string;\n let args: string[];\n try {\n ({ command, args } = buildUpdateCommand(packageSpec, platform, opts));\n } catch (error) {\n log(`Update unavailable: ${error instanceof Error ? error.message : String(error)}`);\n return { ok: false, status: 2, command: '', args: [], skipped: false };\n }\n\n log('=== vo-mcp updater ===');\n log(`Current installed package version: ${packageVersion()}`);\n log(`Updating MCP + runner with: ${command} ${args.join(' ')}`);\n\n if (opts.dryRun) {\n log('Dry run only - no package changes made.');\n return { ok: true, status: 0, command, args, skipped: true };\n }\n\n const run = opts.spawn ?? spawnSync;\n const result = run(command, args, {\n stdio: 'inherit',\n shell: false,\n windowsHide: true,\n env: opts.env ?? process.env,\n });\n const status = typeof result.status === 'number' ? result.status : 1;\n if (status !== 0) {\n log(`Update failed with exit code ${status}.`);\n return { ok: false, status, command, args, skipped: false };\n }\n\n log('\\nUpdate complete.');\n log('Restart any running `vo-mcp runner` process so it loads the new bundle.');\n log('If Claude Desktop / Claude Code was open, restart it to reload the MCP server.');\n return { ok: true, status: 0, command, args, skipped: false };\n}\n", "/** Allow-listed host maintenance operations used by the runner supervisor. */\nimport { spawnSync } from 'node:child_process';\nimport { existsSync } from 'node:fs';\nimport { win32 } from 'node:path';\n\nexport const DEFAULT_RUNNER_PACKAGE = '@algosuite/vo-mcp@beta';\nconst PACKAGE_SPEC_RE = /^@algosuite\\/vo-mcp@(beta|latest|\\d+(?:\\.\\d+){0,2}(?:-[\\w.-]+)?)$/u;\nconst MAX_DIAGNOSTIC_CHARS = 800;\nconst NPM_CLI_SUFFIX = `\\\\${win32.join('node_modules', 'npm', 'bin', 'npm-cli.js').toLowerCase()}`;\n\nfunction escapeRegExp(value) {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/gu, '\\\\$&');\n}\n\n/** Keep action detail useful without ever returning credentials or tokens. */\nexport function sanitizeMaintenanceDiagnostic(raw, env = {}) {\n let value = String(raw || '')\n .replace(/\\b(?:vocred|npm|gh[oprsu])_[A-Za-z0-9._-]+\\b/gu, '[REDACTED]')\n .replace(/\\bBearer\\s+\\S+/giu, 'Bearer [REDACTED]')\n .replace(/\\b(_?authToken|token|password|secret|credential)(\\s*[=:]\\s*)\\S+/giu, '$1$2[REDACTED]');\n for (const [name, secret] of Object.entries(env)) {\n if (!/(?:TOKEN|KEY|SECRET|PASSWORD|CREDENTIAL)/iu.test(name)) continue;\n const text = String(secret || '');\n if (text.length < 4) continue;\n value = value.replace(new RegExp(escapeRegExp(text), 'gu'), '[REDACTED]');\n }\n value = value.replace(/\\s+/gu, ' ').trim();\n if (value.length <= MAX_DIAGNOSTIC_CHARS) return value;\n return `${value.slice(0, MAX_DIAGNOSTIC_CHARS - 1)}\u2026`;\n}\n\nfunction isNpmCliPath(value) {\n return typeof value === 'string'\n && win32.isAbsolute(value)\n && win32.normalize(value).toLowerCase().endsWith(NPM_CLI_SUFFIX);\n}\n\n/**\n * Resolve npm's JavaScript entrypoint without executing a Windows `.cmd` shim.\n *\n * The desktop app can run from a bundled node.exe that does not carry npm, so\n * resolution also checks the exact npm layout below absolute PATH entries. No\n * candidate controls the executable or package argv: process.execPath remains\n * the executable and the package spec remains allow-listed below.\n */\nexport function resolveNpmCli({\n env = process.env,\n execPath = process.execPath,\n fileExists = existsSync,\n} = {}) {\n const candidates = [];\n if (isNpmCliPath(env.npm_execpath)) candidates.push(env.npm_execpath);\n candidates.push(win32.join(win32.dirname(execPath), 'node_modules', 'npm', 'bin', 'npm-cli.js'));\n\n const pathValue = env.PATH ?? env.Path ?? env.path ?? '';\n for (const entry of pathValue.split(';')) {\n const trimmed = entry.trim();\n if (!win32.isAbsolute(trimmed)) continue;\n candidates.push(win32.join(trimmed, 'node_modules', 'npm', 'bin', 'npm-cli.js'));\n }\n\n const seen = new Set();\n for (const candidate of candidates) {\n const normalized = win32.normalize(candidate);\n const key = normalized.toLowerCase();\n if (seen.has(key) || !isNpmCliPath(normalized)) continue;\n seen.add(key);\n if (fileExists(normalized)) return normalized;\n }\n return null;\n}\n\nexport function buildMaintenanceCommand(kind, {\n platform = process.platform,\n packageSpec = DEFAULT_RUNNER_PACKAGE,\n env = process.env,\n execPath = process.execPath,\n fileExists = existsSync,\n} = {}) {\n if (!['update', 'reinstall'].includes(kind)) return null;\n if (!PACKAGE_SPEC_RE.test(packageSpec)) throw new Error('unsafe runner package spec');\n const npmCli = platform === 'win32' ? resolveNpmCli({ env, execPath, fileExists }) : null;\n if (platform === 'win32' && !npmCli) throw new Error('trusted npm CLI not found');\n const command = platform === 'win32' ? execPath : 'npm';\n const args = [...(npmCli ? [npmCli] : []), 'install', '-g', packageSpec];\n if (kind === 'reinstall') args.push('--force');\n return { command, args };\n}\n\nexport function runHostMaintenance(kind, {\n platform = process.platform,\n packageSpec = DEFAULT_RUNNER_PACKAGE,\n env = process.env,\n execPath = process.execPath,\n fileExists = existsSync,\n spawn = spawnSync,\n log = () => {},\n} = {}) {\n if (kind === 'reconnect') return { ok: true, status: 0, command: null, args: [] };\n let command;\n try {\n command = buildMaintenanceCommand(kind, { platform, packageSpec, env, execPath, fileExists });\n } catch (error) {\n return {\n ok: false,\n status: 2,\n command: null,\n args: [],\n detail: sanitizeMaintenanceDiagnostic(error instanceof Error ? error.message : String(error), env),\n };\n }\n if (!command) return { ok: false, status: 2, command: null, args: [] };\n log(`runner maintenance: ${command.command} ${command.args.join(' ')}`);\n const result = spawn(command.command, command.args, {\n stdio: ['ignore', 'pipe', 'pipe'],\n encoding: 'utf8',\n env,\n shell: false,\n windowsHide: true,\n });\n const status = typeof result.status === 'number' ? result.status : 1;\n const detail = sanitizeMaintenanceDiagnostic(\n [result.stderr, result.stdout, result.error?.message].filter(Boolean).join('\\n'),\n env,\n );\n if (detail) log(`runner maintenance result: ${detail}`);\n return { ok: status === 0, status, command: command.command, args: command.args, detail };\n}\n", "#!/usr/bin/env node\n/**\n * `vo-mcp update` CLI entry point.\n */\nimport { updateVoMcp, DEFAULT_UPDATE_SPEC } from './update.js';\n\nfunction argValue(name: string): string | undefined {\n const index = process.argv.indexOf(name);\n return index >= 0 ? process.argv[index + 1] : undefined;\n}\n\nconst packageSpec = argValue('--package') || DEFAULT_UPDATE_SPEC;\nconst dryRun = process.argv.includes('--dry-run');\nconst result = updateVoMcp({ packageSpec, dryRun });\n\nprocess.exit(result.ok ? 0 : result.status || 1);\n"],
5
+ "mappings": "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",
6
+ "names": ["spawnSync", "packageSpec", "packageSpec", "spawnSync", "result"]
7
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@algosuite/vo-mcp",
3
- "version": "0.2.0-beta.2",
4
- "description": "Virtual Office MCP server — open protocol surface for the VO consensus / ratchet tool family. Stdio transport, cross-vendor MCP client compatible (Claude Code, Claude Desktop, Cursor, Continue).",
3
+ "version": "0.2.0-beta.21",
4
+ "description": "AlgoHQ MCP server — open protocol surface for the HQ consensus and ratchet tool family. Stdio transport, cross-vendor MCP client compatible (Claude Code, Claude Desktop, Codex, Cursor, Continue).",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
@@ -34,11 +34,13 @@
34
34
  "keywords": [
35
35
  "mcp",
36
36
  "model-context-protocol",
37
+ "algohq",
37
38
  "virtual-office",
38
39
  "consensus",
39
40
  "ratchet",
40
41
  "claude",
41
42
  "claude-desktop",
43
+ "codex",
42
44
  "cursor",
43
45
  "continue"
44
46
  ],