@keeperhub/wallet 0.1.6 → 0.1.8
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.
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +39 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +49 -21
- package/dist/index.d.ts +49 -21
- package/dist/index.js +38 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/skill/keeperhub-wallet.skill.md +1 -1
package/dist/cli.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts","../src/balance.ts","../src/chains.ts","../src/fund.ts","../src/skill-install.ts","../src/agent-detect.ts","../src/storage.ts","../src/types.ts"],"sourcesContent":["// CLI dispatcher for `npx @keeperhub/wallet <cmd>`. Ships 4 subcommands:\n// add (provision -- NO auth), fund (pure string-build Coinbase Onramp +\n// Tempo address), balance (Base USDC + Tempo USDC.e), info (print subOrgId\n// + walletAddress from ~/.keeperhub/wallet.json).\n//\n// v0.1.4 removed the `link` subcommand. /api/agentic-wallet/link still\n// exists server-side but the UX (copy-paste session cookie) was not fit\n// for real users; the server-approval ask tier that required linking\n// also collapsed into an inline ask in this release. See KEEP-307 and\n// KEEP-308 for the long-term design decisions.\n//\n// @security The HMAC secret written to wallet.json is NEVER printed to stdout\n// or stderr. `add` prints only subOrgId + walletAddress + the config path so\n// users can inspect perms. `info` never references the secret at all. Grep\n// rule: no process.stdout/process.stderr line in this file should include\n// wallet.hmacSecret or data.hmacSecret.\n//\n// Exit codes: 0 on success, 1 on any error (WalletConfigMissingError,\n// HTTP failure, validation error). Uncaught errors are written to stderr.\n\nimport { Command } from \"commander\";\nimport { checkBalance } from \"./balance.js\";\nimport { fund } from \"./fund.js\";\nimport { installSkill } from \"./skill-install.js\";\nimport {\n getWalletConfigPath,\n readWalletConfig,\n writeWalletConfig,\n} from \"./storage.js\";\nimport { WalletConfigMissingError } from \"./types.js\";\n\nconst TRAILING_SLASH = /\\/$/;\nconst WALLET_ADDRESS_PATTERN = /^0x[a-fA-F0-9]{40}$/;\n\nfunction resolveBaseUrl(override: string | undefined): string {\n const candidate =\n override ?? process.env.KEEPERHUB_API_URL ?? \"https://app.keeperhub.com\";\n return candidate.replace(TRAILING_SLASH, \"\");\n}\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === \"string\" && value.length > 0;\n}\n\nfunction provisionInvalidError(\n message: string\n): Error & { code: \"PROVISION_RESPONSE_INVALID\" } {\n const err = new Error(message) as Error & {\n code: \"PROVISION_RESPONSE_INVALID\";\n };\n err.code = \"PROVISION_RESPONSE_INVALID\";\n return err;\n}\n\nfunction validateProvisionResponse(data: unknown): {\n subOrgId: string;\n walletAddress: `0x${string}`;\n hmacSecret: string;\n} {\n if (typeof data !== \"object\" || data === null) {\n throw provisionInvalidError(\"provision response is not an object\");\n }\n const { subOrgId, walletAddress, hmacSecret } = data as Record<\n string,\n unknown\n >;\n if (\n !(\n isNonEmptyString(subOrgId) &&\n isNonEmptyString(walletAddress) &&\n isNonEmptyString(hmacSecret)\n )\n ) {\n throw provisionInvalidError(\n \"provision response missing subOrgId, walletAddress, or hmacSecret\"\n );\n }\n if (!WALLET_ADDRESS_PATTERN.test(walletAddress)) {\n throw provisionInvalidError(\n `provision response walletAddress is not a valid 0x-prefixed 40-hex address: ${walletAddress}`\n );\n }\n return {\n subOrgId,\n walletAddress: walletAddress as `0x${string}`,\n hmacSecret,\n };\n}\n\nasync function cmdAdd(opts: { baseUrl?: string } = {}): Promise<void> {\n const baseUrl = resolveBaseUrl(opts.baseUrl);\n const response = await fetch(`${baseUrl}/api/agentic-wallet/provision`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: \"{}\",\n });\n if (!response.ok) {\n const text = await response.text();\n process.stderr.write(\n `[keeperhub-wallet] provision failed: HTTP ${response.status}: ${text}\\n`\n );\n process.exit(1);\n }\n const raw = (await response.json()) as unknown;\n const data = validateProvisionResponse(raw);\n await writeWalletConfig({\n subOrgId: data.subOrgId,\n walletAddress: data.walletAddress,\n hmacSecret: data.hmacSecret,\n });\n // Intentionally print only public fields. The hmacSecret is written to\n // wallet.json (chmod 0o600) but never printed -- T-34-cli-02 mitigation.\n process.stdout.write(`subOrgId: ${data.subOrgId}\\n`);\n process.stdout.write(`walletAddress: ${data.walletAddress}\\n`);\n process.stdout.write(`config written to ${getWalletConfigPath()}\\n`);\n}\n\nasync function cmdFund(): Promise<void> {\n const wallet = await readWalletConfig();\n const out = fund(wallet.walletAddress);\n process.stdout.write(`${out.coinbaseOnrampUrl}\\n`);\n process.stdout.write(`Tempo address: ${out.tempoAddress}\\n`);\n process.stdout.write(`${out.disclaimer}\\n`);\n}\n\nasync function cmdBalance(): Promise<void> {\n const wallet = await readWalletConfig();\n const snap = await checkBalance(wallet);\n process.stdout.write(`Base USDC: ${snap.base.amount}\\n`);\n process.stdout.write(`Tempo USDC.e: ${snap.tempo.amount}\\n`);\n}\n\nasync function cmdInfo(): Promise<void> {\n const wallet = await readWalletConfig();\n process.stdout.write(`subOrgId: ${wallet.subOrgId}\\n`);\n process.stdout.write(`walletAddress: ${wallet.walletAddress}\\n`);\n}\n\nexport async function runCli(argv: string[] = process.argv): Promise<void> {\n const program = new Command();\n program\n .name(\"keeperhub-wallet\")\n .description(\n \"KeeperHub agentic wallet CLI (auto-pay x402 + MPP 402 responses)\"\n )\n .version(\"0.1.3\");\n\n program\n .command(\"add\")\n .description(\"Provision a new agentic wallet (no account required)\")\n .option(\"--base-url <url>\", \"KeeperHub API base URL\")\n .action(async (opts: { baseUrl?: string }) => {\n await cmdAdd(opts);\n });\n\n program\n .command(\"fund\")\n .description(\n \"Print Coinbase Onramp URL (Base USDC) and Tempo deposit address\"\n )\n .action(async () => {\n await cmdFund();\n });\n\n program\n .command(\"balance\")\n .description(\"Print on-chain balance: Base USDC + Tempo USDC.e\")\n .action(async () => {\n await cmdBalance();\n });\n\n program\n .command(\"info\")\n .description(\"Print subOrgId and walletAddress from local config\")\n .action(async () => {\n await cmdInfo();\n });\n\n program\n .command(\"skill\")\n .description(\n \"Install the KeeperHub skill file into detected agent directories\"\n )\n .addCommand(\n new Command(\"install\")\n .description(\n \"Write skill file + register PreToolUse hook in all detected agents\"\n )\n .action(async () => {\n const result = await installSkill();\n for (const write of result.skillWrites) {\n process.stdout.write(\n `skill: ${write.agent} -> ${write.path} (${write.status})\\n`\n );\n }\n for (const reg of result.hookRegistrations) {\n if (reg.status === \"registered\") {\n process.stdout.write(\n `hook: ${reg.agent} -> PreToolUse registered\\n`\n );\n } else if (reg.status === \"notice\") {\n process.stderr.write(\n `notice: ${reg.agent} -> ${reg.message ?? \"\"}\\n`\n );\n }\n }\n if (result.skillWrites.length === 0) {\n process.stderr.write(\n \"No supported agent skill directories detected under $HOME. Create ~/.claude/, ~/.cursor/, ~/.cline/, ~/.windsurf/, or ~/.config/opencode/ and re-run.\\n\"\n );\n }\n })\n );\n\n try {\n await program.parseAsync(argv);\n } catch (err) {\n if (err instanceof WalletConfigMissingError) {\n process.stderr.write(`[keeperhub-wallet] ${err.message}\\n`);\n process.exit(1);\n }\n process.stderr.write(\n `[keeperhub-wallet] ${(err as Error).message ?? String(err)}\\n`\n );\n process.exit(1);\n }\n}\n","// checkBalance() unified view (PAY-05):\n// - Base USDC balanceOf (viem publicClient on Base)\n// - Tempo USDC.e balanceOf (viem publicClient on Tempo)\n//\n// Both legs are fetched in parallel via Promise.all. The on-chain reads\n// touch only the canonical USDC contract on their respective chains\n// (read-only ERC-20 balanceOf with no state mutation).\n//\n// The /api/agentic-wallet/credit ledger is intentionally NOT read here:\n// the server endpoint exists but no debit path is wired, so surfacing the\n// balance to users implied a capability that has not shipped. Restore the\n// leg here when KEEP-305/306 lands.\n//\n// @security balance.ts does not emit balance data to stdout/stderr via the\n// global console object or util.inspect (T-34-bal-02 mitigation). Any\n// stdout emitter added here is a privacy regression; grep-enforced in\n// acceptance criteria.\nimport {\n createPublicClient,\n erc20Abi,\n formatUnits,\n http,\n type PublicClient,\n} from \"viem\";\nimport { BASE_USDC, base, TEMPO_USDC_E, tempo } from \"./chains.js\";\nimport type { WalletConfig } from \"./types.js\";\n\n// USDC and USDC.e both use 6 decimals on Base + Tempo respectively.\nconst USDC_DECIMALS = 6;\n\nexport type BalanceSnapshot = {\n base: {\n chain: \"base\";\n token: \"USDC\";\n amount: string;\n address: `0x${string}`;\n };\n tempo: {\n chain: \"tempo\";\n token: \"USDC.e\";\n amount: string;\n address: `0x${string}`;\n };\n};\n\nexport type CheckBalanceOptions = {\n /** Injectable viem client for Base (tests mock readContract). */\n baseClient?: PublicClient;\n /** Injectable viem client for Tempo (tests mock readContract). */\n tempoClient?: PublicClient;\n};\n\n/**\n * Read the wallet's on-chain balance across Base + Tempo in parallel. Both\n * legs must resolve; any single failure rejects the Promise.\n *\n * Amounts are formatted as decimal strings (6-decimal USDC precision) so the\n * caller can render them without BigInt math.\n */\nexport async function checkBalance(\n wallet: WalletConfig,\n opts: CheckBalanceOptions = {}\n): Promise<BalanceSnapshot> {\n const baseClient =\n opts.baseClient ??\n (createPublicClient({\n chain: base,\n transport: http(),\n }) as unknown as PublicClient);\n const tempoClient =\n opts.tempoClient ??\n (createPublicClient({\n chain: tempo,\n transport: http(),\n }) as unknown as PublicClient);\n\n // Promise.all fires both reads concurrently. Total elapsed ~= max(leg)\n // rather than sum(leg); SC-3 (<2s) test asserts this.\n const [baseRaw, tempoRaw] = await Promise.all([\n baseClient.readContract({\n address: BASE_USDC,\n abi: erc20Abi,\n functionName: \"balanceOf\",\n args: [wallet.walletAddress],\n }) as Promise<bigint>,\n tempoClient.readContract({\n address: TEMPO_USDC_E,\n abi: erc20Abi,\n functionName: \"balanceOf\",\n args: [wallet.walletAddress],\n }) as Promise<bigint>,\n ]);\n\n return {\n base: {\n chain: \"base\",\n token: \"USDC\",\n amount: formatUnits(baseRaw, USDC_DECIMALS),\n address: wallet.walletAddress,\n },\n tempo: {\n chain: \"tempo\",\n token: \"USDC.e\",\n amount: formatUnits(tempoRaw, USDC_DECIMALS),\n address: wallet.walletAddress,\n },\n };\n}\n","// Sources (truth):\n// - lib/agentic-wallet/sign.ts:56 -- Base USDC at\n// 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 (chainId 8453).\n// - lib/mpp/server.ts:3 -- Tempo USDC.e at\n// 0x20c000000000000000000000b9537d11c60e8b50 (chainId 4217).\n//\n// Tempo is not in viem/chains core as of viem 2.48.1 (the version pinned in\n// this package). Define it inline via defineChain so the only dependency is\n// viem itself. TEMPO_RPC_URL overrides the default RPC for heavy readers who\n// want to point at their own node (T-34-bal-01 mitigation).\nimport { defineChain } from \"viem\";\n\nexport { base } from \"viem/chains\";\n\nexport const tempo = defineChain({\n id: 4217,\n name: \"Tempo\",\n nativeCurrency: { decimals: 18, name: \"Ether\", symbol: \"ETH\" },\n rpcUrls: {\n default: {\n http: [process.env.TEMPO_RPC_URL ?? \"https://rpc.tempo.xyz\"],\n },\n },\n blockExplorers: {\n default: { name: \"Tempo Explorer\", url: \"https://explorer.tempo.xyz\" },\n },\n});\n\n/** Circle-issued USDC on Base mainnet. */\nexport const BASE_USDC = \"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913\" as const;\n\n/** Bridged USDC (USDC.e) on Tempo mainnet. NOT the same contract as BASE_USDC. */\nexport const TEMPO_USDC_E =\n \"0x20c000000000000000000000b9537d11c60e8b50\" as const;\n","// Source: 34-RESEARCH Pattern 5 + Pitfall 5.\n// Coinbase deprecated the query-param pay.coinbase.com flow in favour of\n// sessionToken URLs on 2025-07-31, but the legacy endpoint still returns a\n// working Onramp page (it just may not pre-fill the asset/network/address\n// fields). We print the legacy URL for zero-dependency ergonomics and a\n// follow-up disclaimer so users know to paste manually if prefill is dropped.\n//\n// fund() is a pure string-build: no HTTP, no process spawn, no browser\n// invocation. Callers (the CLI `keeperhub-wallet fund` subcommand, the\n// `check_balance` skill in Phase 35) decide how to display the result.\n//\n// T-34-fund-01 mitigation: the host is hard-coded (pay.coinbase.com) and the\n// only user-supplied input is the wallet address, which is regex-validated\n// against the canonical 0x-prefixed 40-hex-char EVM format before any string\n// interpolation.\n\nexport type FundInstructions = {\n /** Coinbase Onramp deeplink (legacy query-param form). */\n coinbaseOnrampUrl: string;\n /** Tempo deposit address — same as the input wallet (EVM address shared). */\n tempoAddress: `0x${string}`;\n /** Plain-ASCII guidance string; no emojis (CLAUDE.md rule). */\n disclaimer: string;\n};\n\n// 0x followed by exactly 40 hex chars, case-insensitive. Kept at module scope\n// so the regex literal is compiled once (biome/ultracite useTopLevelRegex).\nconst EVM_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;\n\n// Coinbase Onramp legacy deeplink. The host + path pair is the documented\n// entry point for query-param-style Onramp sessions.\nconst COINBASE_HOST = \"pay.coinbase.com\";\nconst COINBASE_PATH = \"/buy/select-asset\";\n\n/**\n * Build Coinbase Onramp URL + Tempo deposit address for the given wallet.\n *\n * No HTTP calls are performed. The caller is expected to either print the\n * resulting URL (CLI) or render it in a chat bubble (skill). The returned\n * `disclaimer` explains the Onramp deprecation + the Tempo external-transfer\n * fallback in plain ASCII so terminal clients with ASCII-only fonts render\n * identically to emoji-capable clients.\n *\n * @throws if `walletAddress` does not match /^0x[0-9a-fA-F]{40}$/.\n */\nexport function fund(walletAddress: string): FundInstructions {\n if (!EVM_ADDRESS_RE.test(walletAddress)) {\n throw new Error(`Invalid EVM wallet address: ${walletAddress}`);\n }\n\n // addresses is a JSON-encoded map {walletAddress: [\"base\"]} per Coinbase\n // Onramp docs. Encoding into URLSearchParams guarantees the colon,\n // brackets, and quotes are percent-escaped correctly.\n const params = new URLSearchParams({\n defaultNetwork: \"base\",\n defaultAsset: \"USDC\",\n addresses: JSON.stringify({ [walletAddress]: [\"base\"] }),\n presetCryptoAmount: \"5\",\n });\n\n const coinbaseOnrampUrl = `https://${COINBASE_HOST}${COINBASE_PATH}?${params.toString()}`;\n\n const disclaimer =\n \"If the Coinbase page does not pre-fill, paste your address manually. \" +\n \"For Tempo USDC.e, transfer from an exchange or another wallet to the \" +\n \"address above -- Onramp does not support Tempo directly. Coinbase \" +\n \"sessionToken URLs are the 2025+ canonical form; legacy query-param \" +\n \"URLs may drop prefill on some accounts.\";\n\n return {\n coinbaseOnrampUrl,\n tempoAddress: walletAddress as `0x${string}`,\n disclaimer,\n };\n}\n","// Idempotent skill installer for @keeperhub/wallet.\n//\n// Two public entry points:\n// - installSkill(options?) -- writes keeperhub-wallet.skill.md into every\n// detected agent's skills directory and, for Claude Code, registers a\n// PreToolUse hook pointing at `keeperhub-wallet-hook` in\n// ~/.claude/settings.json. For non-claude agents, emits a stderr notice.\n// - registerClaudeCodeHook(settingsPath) -- pure settings.json patcher\n// used internally; exported so tests can drive it directly.\n//\n// Idempotency rule: re-running the installer MUST NOT create a duplicate\n// hook entry. We filter any existing array element whose serialised form\n// contains `keeperhub-wallet-hook` before appending a single fresh record.\n//\n// Preservation rule: all top-level keys in settings.json other than\n// hooks.PreToolUse MUST be byte-preserved. We only ever touch\n// hooks.PreToolUse; any foreign hooks.PostToolUse entries survive verbatim.\n\nimport { chmod, copyFile, mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { type AgentTarget, detectAgents } from \"./agent-detect.js\";\n\nconst HOOK_COMMAND = \"keeperhub-wallet-hook\";\n// Match rule for de-dup: any existing PreToolUse entry whose JSON form\n// mentions this string is considered \"ours\" and is removed before append.\nconst KEEPERHUB_HOOK_MARKER = \"keeperhub-wallet-hook\";\n\nexport type InstallResult = {\n skillWrites: Array<{\n agent: string;\n path: string;\n status: \"written\" | \"skipped\";\n }>;\n hookRegistrations: Array<{\n agent: string;\n status: \"registered\" | \"notice\" | \"skipped\";\n message?: string;\n }>;\n};\n\nexport type InstallOptions = {\n homeOverride?: string;\n skillSourcePath?: string;\n onNotice?: (msg: string) => void;\n};\n\ntype ClaudeHookEntry = {\n matcher: string;\n hooks: Array<{ type: string; command: string }>;\n};\n\ntype ClaudeSettings = {\n hooks?: {\n PreToolUse?: unknown[];\n [k: string]: unknown;\n };\n [k: string]: unknown;\n};\n\nfunction buildKeeperhubEntry(): ClaudeHookEntry {\n return {\n matcher: \"*\",\n hooks: [{ type: \"command\", command: HOOK_COMMAND }],\n };\n}\n\nfunction resolveDefaultSkillSource(): string {\n // Resolve the module's own directory in a way that works in both ESM\n // (import.meta.url) and CJS (__dirname shim emitted by tsup). At runtime\n // the module lives inside dist/, so `../skill/` points at the sibling\n // skill/ directory shipped via pkg.files. During vitest tests the module\n // executes from src/, and `../skill/` resolves to packages/wallet/skill/.\n const here = dirname(fileURLToPath(import.meta.url));\n return join(here, \"..\", \"skill\", \"keeperhub-wallet.skill.md\");\n}\n\nfunction defaultNotice(msg: string): void {\n process.stderr.write(`${msg}\\n`);\n}\n\nexport async function registerClaudeCodeHook(\n settingsPath: string\n): Promise<void> {\n let raw: string | null = null;\n try {\n raw = await readFile(settingsPath, \"utf-8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") {\n throw err;\n }\n }\n\n let config: ClaudeSettings = {};\n if (raw !== null) {\n try {\n config = JSON.parse(raw) as ClaudeSettings;\n } catch {\n throw new Error(\n `settings.json at ${settingsPath} is not valid JSON; aborting hook registration`\n );\n }\n }\n\n const hooks: Record<string, unknown> =\n typeof config.hooks === \"object\" && config.hooks !== null\n ? (config.hooks as Record<string, unknown>)\n : {};\n\n const existingPreToolUse = Array.isArray(hooks.PreToolUse)\n ? (hooks.PreToolUse as unknown[])\n : [];\n\n // De-dup: drop any element that references keeperhub-wallet-hook in its\n // serialised form. Covers both exact-shape matches and any legacy\n // representations we may have written in earlier versions.\n const filtered: unknown[] = [];\n for (const entry of existingPreToolUse) {\n const serialised = JSON.stringify(entry);\n if (!serialised.includes(KEEPERHUB_HOOK_MARKER)) {\n filtered.push(entry);\n }\n }\n filtered.push(buildKeeperhubEntry());\n\n hooks.PreToolUse = filtered;\n config.hooks = hooks as ClaudeSettings[\"hooks\"];\n\n await mkdir(dirname(settingsPath), { recursive: true, mode: 0o700 });\n const payload = `${JSON.stringify(config, null, 2)}\\n`;\n await writeFile(settingsPath, payload, { mode: 0o600 });\n // Reassert mode in case the file already existed with looser perms.\n await chmod(settingsPath, 0o600);\n}\n\nasync function writeSkillToAgent(\n agent: AgentTarget,\n skillSource: string\n): Promise<{ agent: string; path: string; status: \"written\" | \"skipped\" }> {\n await mkdir(agent.skillsDir, { recursive: true, mode: 0o755 });\n const target = join(agent.skillsDir, \"keeperhub-wallet.skill.md\");\n await copyFile(skillSource, target);\n await chmod(target, 0o644);\n return { agent: agent.agent, path: target, status: \"written\" };\n}\n\nfunction buildNoticeMessage(agent: AgentTarget): string {\n return `${agent.agent} does not support auto-registered PreToolUse hooks; run \\`${HOOK_COMMAND}\\` on every tool use via ${agent.agent}'s settings file at ${agent.settingsFile}`;\n}\n\nexport async function installSkill(\n options: InstallOptions = {}\n): Promise<InstallResult> {\n const agents = detectAgents(options.homeOverride);\n const skillSource = options.skillSourcePath ?? resolveDefaultSkillSource();\n const onNotice = options.onNotice ?? defaultNotice;\n\n const skillWrites: InstallResult[\"skillWrites\"] = [];\n const hookRegistrations: InstallResult[\"hookRegistrations\"] = [];\n\n for (const agent of agents) {\n const write = await writeSkillToAgent(agent, skillSource);\n skillWrites.push(write);\n\n if (agent.hookSupport === \"claude-code\") {\n await registerClaudeCodeHook(agent.settingsFile);\n hookRegistrations.push({\n agent: agent.agent,\n status: \"registered\",\n });\n } else {\n const message = buildNoticeMessage(agent);\n hookRegistrations.push({\n agent: agent.agent,\n status: \"notice\",\n message,\n });\n onNotice(message);\n }\n }\n\n return { skillWrites, hookRegistrations };\n}\n","// Cross-agent skill/settings directory discovery.\n//\n// Probes canonical paths under $HOME and returns one AgentTarget record per\n// agent whose parent directory exists. The `skills/` leaf may be absent --\n// installSkill() creates it.\n//\n// NOTE: `homedir()` is called per-invocation (via `homeOverride ?? homedir()`)\n// and NEVER hoisted to a module-level constant. Tests override\n// `process.env.HOME` in `beforeEach`; hoisting would freeze the harness's\n// original HOME at import time and detection would run against the real $HOME.\n\nimport { existsSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\n\nexport type AgentTarget = {\n agent: \"claude-code\" | \"cursor\" | \"cline\" | \"windsurf\" | \"opencode\";\n skillsDir: string;\n settingsFile: string;\n hookSupport: \"claude-code\" | \"notice\";\n};\n\ntype AgentSpec = {\n agent: AgentTarget[\"agent\"];\n skillsRel: string[];\n settingsRel: string[];\n hookSupport: AgentTarget[\"hookSupport\"];\n};\n\n// Deterministic order: claude-code first (only agent with hook support),\n// then cursor, cline, windsurf, opencode.\nconst AGENT_SPECS: readonly AgentSpec[] = [\n {\n agent: \"claude-code\",\n skillsRel: [\".claude\", \"skills\"],\n settingsRel: [\".claude\", \"settings.json\"],\n hookSupport: \"claude-code\",\n },\n {\n agent: \"cursor\",\n skillsRel: [\".cursor\", \"skills\"],\n settingsRel: [\".cursor\", \"settings.json\"],\n hookSupport: \"notice\",\n },\n {\n agent: \"cline\",\n skillsRel: [\".cline\", \"skills\"],\n settingsRel: [\".cline\", \"settings.json\"],\n hookSupport: \"notice\",\n },\n {\n agent: \"windsurf\",\n skillsRel: [\".windsurf\", \"skills\"],\n settingsRel: [\".windsurf\", \"settings.json\"],\n hookSupport: \"notice\",\n },\n {\n agent: \"opencode\",\n skillsRel: [\".config\", \"opencode\", \"skills\"],\n settingsRel: [\".config\", \"opencode\", \"settings.json\"],\n hookSupport: \"notice\",\n },\n];\n\nexport function detectAgents(homeOverride?: string): AgentTarget[] {\n const home = homeOverride ?? homedir();\n const results: AgentTarget[] = [];\n for (const spec of AGENT_SPECS) {\n const skillsDir = join(home, ...spec.skillsRel);\n const settingsFile = join(home, ...spec.settingsRel);\n // \"Detected\" iff the parent of skills/ exists (e.g. ~/.claude/).\n // skills/ itself may be absent; installer creates it.\n if (existsSync(dirname(skillsDir))) {\n results.push({\n agent: spec.agent,\n skillsDir,\n settingsFile,\n hookSupport: spec.hookSupport,\n });\n }\n }\n return results;\n}\n","import { chmod, mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { type WalletConfig, WalletConfigMissingError } from \"./types.js\";\n\n// NOTE: Every function calls `join(homedir(), \".keeperhub\", \"wallet.json\")`\n// itself. Do NOT hoist to a module-level `const WALLET_PATH` -- tests\n// override `process.env.HOME` in `beforeEach` and `homedir()` must re-read\n// that on each call. A hoisted constant would freeze the harness's original\n// HOME at import time and every test would write into the real\n// ~/.keeperhub/ directory.\n\nexport async function readWalletConfig(): Promise<WalletConfig> {\n const walletPath = join(homedir(), \".keeperhub\", \"wallet.json\");\n let raw: string;\n try {\n raw = await readFile(walletPath, \"utf-8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n throw new WalletConfigMissingError();\n }\n throw err;\n }\n const parsed = JSON.parse(raw) as Partial<WalletConfig>;\n if (!(parsed.subOrgId && parsed.walletAddress && parsed.hmacSecret)) {\n throw new Error(`Malformed wallet.json at ${walletPath}`);\n }\n return parsed as WalletConfig;\n}\n\nexport async function writeWalletConfig(config: WalletConfig): Promise<void> {\n const walletPath = join(homedir(), \".keeperhub\", \"wallet.json\");\n await mkdir(dirname(walletPath), { recursive: true, mode: 0o700 });\n await writeFile(walletPath, JSON.stringify(config, null, 2), { mode: 0o600 });\n // Reassert mode in case the file already existed with looser perms.\n await chmod(walletPath, 0o600);\n}\n\nexport function getWalletConfigPath(): string {\n return join(homedir(), \".keeperhub\", \"wallet.json\");\n}\n","// Shared types across the package. Phase 34.\nexport type WalletConfig = {\n /** Turnkey sub-org ID returned by POST /api/agentic-wallet/provision */\n subOrgId: string;\n /** EVM-shared wallet address (same for Base chainId 8453 and Tempo chainId 4217) */\n walletAddress: `0x${string}`;\n /** 64-char lowercase hex HMAC secret, minted server-side at provision; never logged */\n hmacSecret: string;\n};\n\nexport type HmacHeaders = {\n \"X-KH-Sub-Org\": string;\n \"X-KH-Timestamp\": string;\n \"X-KH-Signature\": string;\n};\n\nexport type HookDecision = {\n decision: \"allow\" | \"deny\" | \"ask\";\n reason?: string;\n};\n\nexport class KeeperHubError extends Error {\n readonly code: string;\n\n constructor(code: string, message: string) {\n super(message);\n this.name = \"KeeperHubError\";\n this.code = code;\n }\n}\n\nexport class WalletConfigMissingError extends Error {\n constructor() {\n super(\n \"Wallet config not found at ~/.keeperhub/wallet.json. Run `npx @keeperhub/wallet add` to provision.\"\n );\n this.name = \"WalletConfigMissingError\";\n }\n}\n"],"mappings":"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","names":["import_viem","import_node_path","import_promises","import_node_os","import_node_path"]}
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/balance.ts","../src/chains.ts","../src/fund.ts","../src/skill-install.ts","../src/agent-detect.ts","../src/storage.ts","../src/types.ts"],"sourcesContent":["// CLI dispatcher for `npx @keeperhub/wallet <cmd>`. Ships 4 subcommands:\n// add (provision -- NO auth), fund (pure string-build Coinbase Onramp +\n// Tempo address), balance (Base USDC + Tempo USDC.e), info (print subOrgId\n// + walletAddress from ~/.keeperhub/wallet.json).\n//\n// v0.1.4 removed the `link` subcommand. /api/agentic-wallet/link still\n// exists server-side but the UX (copy-paste session cookie) was not fit\n// for real users; the server-approval ask tier that required linking\n// also collapsed into an inline ask in this release. See KEEP-307 and\n// KEEP-308 for the long-term design decisions.\n//\n// @security The HMAC secret written to wallet.json is NEVER printed to stdout\n// or stderr. `add` prints only subOrgId + walletAddress + the config path so\n// users can inspect perms. `info` never references the secret at all. Grep\n// rule: no process.stdout/process.stderr line in this file should include\n// wallet.hmacSecret or data.hmacSecret.\n//\n// Exit codes: 0 on success, 1 on any error (WalletConfigMissingError,\n// HTTP failure, validation error). Uncaught errors are written to stderr.\n\nimport { Command } from \"commander\";\nimport { checkBalance } from \"./balance.js\";\nimport { fund } from \"./fund.js\";\nimport { installSkill } from \"./skill-install.js\";\nimport {\n getWalletConfigPath,\n readWalletConfig,\n writeWalletConfig,\n} from \"./storage.js\";\nimport { WalletConfigMissingError } from \"./types.js\";\n\nconst TRAILING_SLASH = /\\/$/;\nconst WALLET_ADDRESS_PATTERN = /^0x[a-fA-F0-9]{40}$/;\n\nfunction resolveBaseUrl(override: string | undefined): string {\n const candidate =\n override ?? process.env.KEEPERHUB_API_URL ?? \"https://app.keeperhub.com\";\n return candidate.replace(TRAILING_SLASH, \"\");\n}\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === \"string\" && value.length > 0;\n}\n\nfunction provisionInvalidError(\n message: string\n): Error & { code: \"PROVISION_RESPONSE_INVALID\" } {\n const err = new Error(message) as Error & {\n code: \"PROVISION_RESPONSE_INVALID\";\n };\n err.code = \"PROVISION_RESPONSE_INVALID\";\n return err;\n}\n\nfunction validateProvisionResponse(data: unknown): {\n subOrgId: string;\n walletAddress: `0x${string}`;\n hmacSecret: string;\n} {\n if (typeof data !== \"object\" || data === null) {\n throw provisionInvalidError(\"provision response is not an object\");\n }\n const { subOrgId, walletAddress, hmacSecret } = data as Record<\n string,\n unknown\n >;\n if (\n !(\n isNonEmptyString(subOrgId) &&\n isNonEmptyString(walletAddress) &&\n isNonEmptyString(hmacSecret)\n )\n ) {\n throw provisionInvalidError(\n \"provision response missing subOrgId, walletAddress, or hmacSecret\"\n );\n }\n if (!WALLET_ADDRESS_PATTERN.test(walletAddress)) {\n throw provisionInvalidError(\n `provision response walletAddress is not a valid 0x-prefixed 40-hex address: ${walletAddress}`\n );\n }\n return {\n subOrgId,\n walletAddress: walletAddress as `0x${string}`,\n hmacSecret,\n };\n}\n\nasync function cmdAdd(opts: { baseUrl?: string } = {}): Promise<void> {\n const baseUrl = resolveBaseUrl(opts.baseUrl);\n const response = await fetch(`${baseUrl}/api/agentic-wallet/provision`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: \"{}\",\n });\n if (!response.ok) {\n const text = await response.text();\n process.stderr.write(\n `[keeperhub-wallet] provision failed: HTTP ${response.status}: ${text}\\n`\n );\n process.exit(1);\n }\n const raw = (await response.json()) as unknown;\n const data = validateProvisionResponse(raw);\n await writeWalletConfig({\n subOrgId: data.subOrgId,\n walletAddress: data.walletAddress,\n hmacSecret: data.hmacSecret,\n });\n // Intentionally print only public fields. The hmacSecret is written to\n // wallet.json (chmod 0o600) but never printed -- T-34-cli-02 mitigation.\n process.stdout.write(`subOrgId: ${data.subOrgId}\\n`);\n process.stdout.write(`walletAddress: ${data.walletAddress}\\n`);\n process.stdout.write(`config written to ${getWalletConfigPath()}\\n`);\n}\n\nasync function cmdFund(): Promise<void> {\n const wallet = await readWalletConfig();\n const out = fund(wallet.walletAddress);\n process.stdout.write(`${out.coinbaseOnrampUrl}\\n`);\n process.stdout.write(`Tempo address: ${out.tempoAddress}\\n`);\n process.stdout.write(`${out.disclaimer}\\n`);\n}\n\nasync function cmdBalance(): Promise<void> {\n const wallet = await readWalletConfig();\n const snap = await checkBalance(wallet);\n process.stdout.write(`Base USDC: ${snap.base.amount}\\n`);\n process.stdout.write(`Tempo USDC.e: ${snap.tempo.amount}\\n`);\n}\n\nasync function cmdInfo(): Promise<void> {\n const wallet = await readWalletConfig();\n process.stdout.write(`subOrgId: ${wallet.subOrgId}\\n`);\n process.stdout.write(`walletAddress: ${wallet.walletAddress}\\n`);\n}\n\nexport async function runCli(argv: string[] = process.argv): Promise<void> {\n const program = new Command();\n program\n .name(\"keeperhub-wallet\")\n .description(\n \"KeeperHub agentic wallet CLI (auto-pay x402 + MPP 402 responses)\"\n )\n .version(\"0.1.3\");\n\n program\n .command(\"add\")\n .description(\"Provision a new agentic wallet (no account required)\")\n .option(\"--base-url <url>\", \"KeeperHub API base URL\")\n .action(async (opts: { baseUrl?: string }) => {\n await cmdAdd(opts);\n });\n\n program\n .command(\"fund\")\n .description(\n \"Print Coinbase Onramp URL (Base USDC) and Tempo deposit address\"\n )\n .action(async () => {\n await cmdFund();\n });\n\n program\n .command(\"balance\")\n .description(\"Print on-chain balance: Base USDC + Tempo USDC.e\")\n .action(async () => {\n await cmdBalance();\n });\n\n program\n .command(\"info\")\n .description(\"Print subOrgId and walletAddress from local config\")\n .action(async () => {\n await cmdInfo();\n });\n\n program\n .command(\"skill\")\n .description(\n \"Install the KeeperHub skill file into detected agent directories\"\n )\n .addCommand(\n new Command(\"install\")\n .description(\n \"Write skill file + register PreToolUse hook in all detected agents\"\n )\n .action(async () => {\n const result = await installSkill();\n for (const write of result.skillWrites) {\n process.stdout.write(\n `skill: ${write.agent} -> ${write.path} (${write.status})\\n`\n );\n }\n for (const reg of result.hookRegistrations) {\n if (reg.status === \"registered\") {\n process.stdout.write(\n `hook: ${reg.agent} -> PreToolUse registered\\n`\n );\n } else if (reg.status === \"notice\") {\n process.stderr.write(\n `notice: ${reg.agent} -> ${reg.message ?? \"\"}\\n`\n );\n }\n }\n if (result.skillWrites.length === 0) {\n process.stderr.write(\n \"No supported agent skill directories detected under $HOME. Create ~/.claude/, ~/.cursor/, ~/.cline/, ~/.windsurf/, or ~/.config/opencode/ and re-run.\\n\"\n );\n }\n })\n );\n\n try {\n await program.parseAsync(argv);\n } catch (err) {\n if (err instanceof WalletConfigMissingError) {\n process.stderr.write(`[keeperhub-wallet] ${err.message}\\n`);\n process.exit(1);\n }\n process.stderr.write(\n `[keeperhub-wallet] ${(err as Error).message ?? String(err)}\\n`\n );\n process.exit(1);\n }\n}\n","// checkBalance() unified view (PAY-05):\n// - Base USDC balanceOf (viem publicClient on Base)\n// - Tempo USDC.e balanceOf (viem publicClient on Tempo)\n//\n// Both legs are fetched in parallel via Promise.all. The on-chain reads\n// touch only the canonical USDC contract on their respective chains\n// (read-only ERC-20 balanceOf with no state mutation).\n//\n// The /api/agentic-wallet/credit ledger is intentionally NOT read here:\n// the server endpoint exists but no debit path is wired, so surfacing the\n// balance to users implied a capability that has not shipped. Restore the\n// leg here when KEEP-305/306 lands.\n//\n// @security balance.ts does not emit balance data to stdout/stderr via the\n// global console object or util.inspect (T-34-bal-02 mitigation). Any\n// stdout emitter added here is a privacy regression; grep-enforced in\n// acceptance criteria.\nimport {\n createPublicClient,\n erc20Abi,\n formatUnits,\n http,\n type PublicClient,\n} from \"viem\";\nimport { BASE_USDC, base, TEMPO_USDC_E, tempo } from \"./chains.js\";\nimport type { WalletConfig } from \"./types.js\";\n\n// USDC and USDC.e both use 6 decimals on Base + Tempo respectively.\nconst USDC_DECIMALS = 6;\n\nexport type BalanceSnapshot = {\n base: {\n chain: \"base\";\n token: \"USDC\";\n amount: string;\n address: `0x${string}`;\n };\n tempo: {\n chain: \"tempo\";\n token: \"USDC.e\";\n amount: string;\n address: `0x${string}`;\n };\n};\n\nexport type CheckBalanceOptions = {\n /** Injectable viem client for Base (tests mock readContract). */\n baseClient?: PublicClient;\n /** Injectable viem client for Tempo (tests mock readContract). */\n tempoClient?: PublicClient;\n};\n\n/**\n * Read the wallet's on-chain balance across Base + Tempo in parallel. Both\n * legs must resolve; any single failure rejects the Promise.\n *\n * Amounts are formatted as decimal strings (6-decimal USDC precision) so the\n * caller can render them without BigInt math.\n */\nexport async function checkBalance(\n wallet: WalletConfig,\n opts: CheckBalanceOptions = {}\n): Promise<BalanceSnapshot> {\n const baseClient =\n opts.baseClient ??\n (createPublicClient({\n chain: base,\n transport: http(),\n }) as unknown as PublicClient);\n const tempoClient =\n opts.tempoClient ??\n (createPublicClient({\n chain: tempo,\n transport: http(),\n }) as unknown as PublicClient);\n\n // Promise.all fires both reads concurrently. Total elapsed ~= max(leg)\n // rather than sum(leg); SC-3 (<2s) test asserts this.\n const [baseRaw, tempoRaw] = await Promise.all([\n baseClient.readContract({\n address: BASE_USDC,\n abi: erc20Abi,\n functionName: \"balanceOf\",\n args: [wallet.walletAddress],\n }) as Promise<bigint>,\n tempoClient.readContract({\n address: TEMPO_USDC_E,\n abi: erc20Abi,\n functionName: \"balanceOf\",\n args: [wallet.walletAddress],\n }) as Promise<bigint>,\n ]);\n\n return {\n base: {\n chain: \"base\",\n token: \"USDC\",\n amount: formatUnits(baseRaw, USDC_DECIMALS),\n address: wallet.walletAddress,\n },\n tempo: {\n chain: \"tempo\",\n token: \"USDC.e\",\n amount: formatUnits(tempoRaw, USDC_DECIMALS),\n address: wallet.walletAddress,\n },\n };\n}\n","// Sources (truth):\n// - lib/agentic-wallet/sign.ts:56 -- Base USDC at\n// 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 (chainId 8453).\n// - lib/mpp/server.ts:3 -- Tempo USDC.e at\n// 0x20c000000000000000000000b9537d11c60e8b50 (chainId 4217).\n//\n// Tempo is not in viem/chains core as of viem 2.48.1 (the version pinned in\n// this package). Define it inline via defineChain so the only dependency is\n// viem itself. TEMPO_RPC_URL overrides the default RPC for heavy readers who\n// want to point at their own node (T-34-bal-01 mitigation).\nimport { defineChain } from \"viem\";\n\nexport { base } from \"viem/chains\";\n\nexport const tempo = defineChain({\n id: 4217,\n name: \"Tempo\",\n nativeCurrency: { decimals: 18, name: \"Ether\", symbol: \"ETH\" },\n rpcUrls: {\n default: {\n http: [process.env.TEMPO_RPC_URL ?? \"https://rpc.tempo.xyz\"],\n },\n },\n blockExplorers: {\n default: { name: \"Tempo Explorer\", url: \"https://explorer.tempo.xyz\" },\n },\n});\n\n/** Circle-issued USDC on Base mainnet. */\nexport const BASE_USDC = \"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913\" as const;\n\n/** Bridged USDC (USDC.e) on Tempo mainnet. NOT the same contract as BASE_USDC. */\nexport const TEMPO_USDC_E =\n \"0x20c000000000000000000000b9537d11c60e8b50\" as const;\n","// Source: 34-RESEARCH Pattern 5 + Pitfall 5.\n// Coinbase deprecated the query-param pay.coinbase.com flow in favour of\n// sessionToken URLs on 2025-07-31, but the legacy endpoint still returns a\n// working Onramp page (it just may not pre-fill the asset/network/address\n// fields). We print the legacy URL for zero-dependency ergonomics and a\n// follow-up disclaimer so users know to paste manually if prefill is dropped.\n//\n// fund() is a pure string-build: no HTTP, no process spawn, no browser\n// invocation. Callers (the CLI `keeperhub-wallet fund` subcommand, the\n// `check_balance` skill in Phase 35) decide how to display the result.\n//\n// T-34-fund-01 mitigation: the host is hard-coded (pay.coinbase.com) and the\n// only user-supplied input is the wallet address, which is regex-validated\n// against the canonical 0x-prefixed 40-hex-char EVM format before any string\n// interpolation.\n\nexport type FundInstructions = {\n /** Coinbase Onramp deeplink (legacy query-param form). */\n coinbaseOnrampUrl: string;\n /** Tempo deposit address — same as the input wallet (EVM address shared). */\n tempoAddress: `0x${string}`;\n /** Plain-ASCII guidance string; no emojis (CLAUDE.md rule). */\n disclaimer: string;\n};\n\n// 0x followed by exactly 40 hex chars, case-insensitive. Kept at module scope\n// so the regex literal is compiled once (biome/ultracite useTopLevelRegex).\nconst EVM_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;\n\n// Coinbase Onramp legacy deeplink. The host + path pair is the documented\n// entry point for query-param-style Onramp sessions.\nconst COINBASE_HOST = \"pay.coinbase.com\";\nconst COINBASE_PATH = \"/buy/select-asset\";\n\n/**\n * Build Coinbase Onramp URL + Tempo deposit address for the given wallet.\n *\n * No HTTP calls are performed. The caller is expected to either print the\n * resulting URL (CLI) or render it in a chat bubble (skill). The returned\n * `disclaimer` explains the Onramp deprecation + the Tempo external-transfer\n * fallback in plain ASCII so terminal clients with ASCII-only fonts render\n * identically to emoji-capable clients.\n *\n * @throws if `walletAddress` does not match /^0x[0-9a-fA-F]{40}$/.\n */\nexport function fund(walletAddress: string): FundInstructions {\n if (!EVM_ADDRESS_RE.test(walletAddress)) {\n throw new Error(`Invalid EVM wallet address: ${walletAddress}`);\n }\n\n // addresses is a JSON-encoded map {walletAddress: [\"base\"]} per Coinbase\n // Onramp docs. Encoding into URLSearchParams guarantees the colon,\n // brackets, and quotes are percent-escaped correctly.\n const params = new URLSearchParams({\n defaultNetwork: \"base\",\n defaultAsset: \"USDC\",\n addresses: JSON.stringify({ [walletAddress]: [\"base\"] }),\n presetCryptoAmount: \"5\",\n });\n\n const coinbaseOnrampUrl = `https://${COINBASE_HOST}${COINBASE_PATH}?${params.toString()}`;\n\n const disclaimer =\n \"If the Coinbase page does not pre-fill, paste your address manually. \" +\n \"For Tempo USDC.e, transfer from an exchange or another wallet to the \" +\n \"address above -- Onramp does not support Tempo directly. Coinbase \" +\n \"sessionToken URLs are the 2025+ canonical form; legacy query-param \" +\n \"URLs may drop prefill on some accounts.\";\n\n return {\n coinbaseOnrampUrl,\n tempoAddress: walletAddress as `0x${string}`,\n disclaimer,\n };\n}\n","// Idempotent skill installer for @keeperhub/wallet.\n//\n// Two public entry points:\n// - installSkill(options?) -- writes keeperhub-wallet.skill.md into every\n// detected agent's skills directory and, for Claude Code, registers a\n// PreToolUse hook pointing at `keeperhub-wallet-hook` in\n// ~/.claude/settings.json. For non-claude agents, emits a stderr notice.\n// - registerClaudeCodeHook(settingsPath) -- pure settings.json patcher\n// used internally; exported so tests can drive it directly.\n//\n// Idempotency rule: re-running the installer MUST NOT create a duplicate\n// hook entry. We filter any existing array element whose serialised form\n// contains `keeperhub-wallet-hook` before appending a single fresh record.\n//\n// Preservation rule: all top-level keys in settings.json other than\n// hooks.PreToolUse MUST be byte-preserved. We only ever touch\n// hooks.PreToolUse; any foreign hooks.PostToolUse entries survive verbatim.\n\nimport { chmod, copyFile, mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { type AgentTarget, detectAgents } from \"./agent-detect.js\";\n\nconst HOOK_COMMAND = \"keeperhub-wallet-hook\";\n// Match rule for de-dup: any existing PreToolUse entry whose JSON form\n// mentions this string is considered \"ours\" and is removed before append.\nconst KEEPERHUB_HOOK_MARKER = \"keeperhub-wallet-hook\";\n\nexport type InstallResult = {\n skillWrites: Array<{\n agent: string;\n path: string;\n status: \"written\" | \"skipped\";\n }>;\n hookRegistrations: Array<{\n agent: string;\n status: \"registered\" | \"notice\" | \"skipped\";\n message?: string;\n }>;\n};\n\nexport type InstallOptions = {\n homeOverride?: string;\n skillSourcePath?: string;\n onNotice?: (msg: string) => void;\n};\n\ntype ClaudeHookEntry = {\n matcher: string;\n hooks: Array<{ type: string; command: string }>;\n};\n\ntype ClaudeSettings = {\n hooks?: {\n PreToolUse?: unknown[];\n [k: string]: unknown;\n };\n [k: string]: unknown;\n};\n\nfunction buildKeeperhubEntry(): ClaudeHookEntry {\n return {\n matcher: \"*\",\n hooks: [{ type: \"command\", command: HOOK_COMMAND }],\n };\n}\n\nfunction resolveDefaultSkillSource(): string {\n // Resolve the module's own directory in a way that works in both ESM\n // (import.meta.url) and CJS (__dirname shim emitted by tsup). At runtime\n // the module lives inside dist/, so `../skill/` points at the sibling\n // skill/ directory shipped via pkg.files. During vitest tests the module\n // executes from src/, and `../skill/` resolves to packages/wallet/skill/.\n const here = dirname(fileURLToPath(import.meta.url));\n return join(here, \"..\", \"skill\", \"keeperhub-wallet.skill.md\");\n}\n\nfunction defaultNotice(msg: string): void {\n process.stderr.write(`${msg}\\n`);\n}\n\nexport async function registerClaudeCodeHook(\n settingsPath: string\n): Promise<void> {\n let raw: string | null = null;\n try {\n raw = await readFile(settingsPath, \"utf-8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") {\n throw err;\n }\n }\n\n let config: ClaudeSettings = {};\n if (raw !== null) {\n try {\n config = JSON.parse(raw) as ClaudeSettings;\n } catch {\n throw new Error(\n `settings.json at ${settingsPath} is not valid JSON; aborting hook registration`\n );\n }\n }\n\n const hooks: Record<string, unknown> =\n typeof config.hooks === \"object\" && config.hooks !== null\n ? (config.hooks as Record<string, unknown>)\n : {};\n\n const existingPreToolUse = Array.isArray(hooks.PreToolUse)\n ? (hooks.PreToolUse as unknown[])\n : [];\n\n // De-dup: drop any element that references keeperhub-wallet-hook in its\n // serialised form. Covers both exact-shape matches and any legacy\n // representations we may have written in earlier versions.\n const filtered: unknown[] = [];\n for (const entry of existingPreToolUse) {\n const serialised = JSON.stringify(entry);\n if (!serialised.includes(KEEPERHUB_HOOK_MARKER)) {\n filtered.push(entry);\n }\n }\n filtered.push(buildKeeperhubEntry());\n\n hooks.PreToolUse = filtered;\n config.hooks = hooks as ClaudeSettings[\"hooks\"];\n\n await mkdir(dirname(settingsPath), { recursive: true, mode: 0o700 });\n const payload = `${JSON.stringify(config, null, 2)}\\n`;\n await writeFile(settingsPath, payload, { mode: 0o600 });\n // Reassert mode in case the file already existed with looser perms.\n await chmod(settingsPath, 0o600);\n}\n\nasync function writeSkillToAgent(\n agent: AgentTarget,\n skillSource: string\n): Promise<{ agent: string; path: string; status: \"written\" | \"skipped\" }> {\n await mkdir(agent.skillsDir, { recursive: true, mode: 0o755 });\n const target = join(agent.skillsDir, \"keeperhub-wallet.skill.md\");\n await copyFile(skillSource, target);\n await chmod(target, 0o644);\n return { agent: agent.agent, path: target, status: \"written\" };\n}\n\nfunction buildNoticeMessage(agent: AgentTarget): string {\n return `${agent.agent} does not support auto-registered PreToolUse hooks; run \\`${HOOK_COMMAND}\\` on every tool use via ${agent.agent}'s settings file at ${agent.settingsFile}`;\n}\n\nexport async function installSkill(\n options: InstallOptions = {}\n): Promise<InstallResult> {\n const agents = detectAgents(options.homeOverride);\n const skillSource = options.skillSourcePath ?? resolveDefaultSkillSource();\n const onNotice = options.onNotice ?? defaultNotice;\n\n const skillWrites: InstallResult[\"skillWrites\"] = [];\n const hookRegistrations: InstallResult[\"hookRegistrations\"] = [];\n\n for (const agent of agents) {\n const write = await writeSkillToAgent(agent, skillSource);\n skillWrites.push(write);\n\n if (agent.hookSupport === \"claude-code\") {\n await registerClaudeCodeHook(agent.settingsFile);\n hookRegistrations.push({\n agent: agent.agent,\n status: \"registered\",\n });\n } else {\n const message = buildNoticeMessage(agent);\n hookRegistrations.push({\n agent: agent.agent,\n status: \"notice\",\n message,\n });\n onNotice(message);\n }\n }\n\n return { skillWrites, hookRegistrations };\n}\n","// Cross-agent skill/settings directory discovery.\n//\n// Probes canonical paths under $HOME and returns one AgentTarget record per\n// agent whose parent directory exists. The `skills/` leaf may be absent --\n// installSkill() creates it.\n//\n// NOTE: `homedir()` is called per-invocation (via `homeOverride ?? homedir()`)\n// and NEVER hoisted to a module-level constant. Tests override\n// `process.env.HOME` in `beforeEach`; hoisting would freeze the harness's\n// original HOME at import time and detection would run against the real $HOME.\n\nimport { existsSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\n\nexport type AgentTarget = {\n agent: \"claude-code\" | \"cursor\" | \"cline\" | \"windsurf\" | \"opencode\";\n skillsDir: string;\n settingsFile: string;\n hookSupport: \"claude-code\" | \"notice\";\n};\n\ntype AgentSpec = {\n agent: AgentTarget[\"agent\"];\n skillsRel: string[];\n settingsRel: string[];\n hookSupport: AgentTarget[\"hookSupport\"];\n};\n\n// Deterministic order: claude-code first (only agent with hook support),\n// then cursor, cline, windsurf, opencode.\nconst AGENT_SPECS: readonly AgentSpec[] = [\n {\n agent: \"claude-code\",\n skillsRel: [\".claude\", \"skills\"],\n settingsRel: [\".claude\", \"settings.json\"],\n hookSupport: \"claude-code\",\n },\n {\n agent: \"cursor\",\n skillsRel: [\".cursor\", \"skills\"],\n settingsRel: [\".cursor\", \"settings.json\"],\n hookSupport: \"notice\",\n },\n {\n agent: \"cline\",\n skillsRel: [\".cline\", \"skills\"],\n settingsRel: [\".cline\", \"settings.json\"],\n hookSupport: \"notice\",\n },\n {\n agent: \"windsurf\",\n skillsRel: [\".windsurf\", \"skills\"],\n settingsRel: [\".windsurf\", \"settings.json\"],\n hookSupport: \"notice\",\n },\n {\n agent: \"opencode\",\n skillsRel: [\".config\", \"opencode\", \"skills\"],\n settingsRel: [\".config\", \"opencode\", \"settings.json\"],\n hookSupport: \"notice\",\n },\n];\n\nexport function detectAgents(homeOverride?: string): AgentTarget[] {\n const home = homeOverride ?? homedir();\n const results: AgentTarget[] = [];\n for (const spec of AGENT_SPECS) {\n const skillsDir = join(home, ...spec.skillsRel);\n const settingsFile = join(home, ...spec.settingsRel);\n // \"Detected\" iff the parent of skills/ exists (e.g. ~/.claude/).\n // skills/ itself may be absent; installer creates it.\n if (existsSync(dirname(skillsDir))) {\n results.push({\n agent: spec.agent,\n skillsDir,\n settingsFile,\n hookSupport: spec.hookSupport,\n });\n }\n }\n return results;\n}\n","import { chmod, mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { type WalletConfig, WalletConfigMissingError } from \"./types.js\";\n\n// NOTE: Every function calls `join(homedir(), \".keeperhub\", \"wallet.json\")`\n// itself. Do NOT hoist to a module-level `const WALLET_PATH` -- tests\n// override `process.env.HOME` in `beforeEach` and `homedir()` must re-read\n// that on each call. A hoisted constant would freeze the harness's original\n// HOME at import time and every test would write into the real\n// ~/.keeperhub/ directory.\n\nexport async function readWalletConfig(): Promise<WalletConfig> {\n const walletPath = join(homedir(), \".keeperhub\", \"wallet.json\");\n let raw: string;\n try {\n raw = await readFile(walletPath, \"utf-8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n throw new WalletConfigMissingError();\n }\n throw err;\n }\n const parsed = JSON.parse(raw) as Partial<WalletConfig>;\n if (!(parsed.subOrgId && parsed.walletAddress && parsed.hmacSecret)) {\n throw new Error(`Malformed wallet.json at ${walletPath}`);\n }\n return parsed as WalletConfig;\n}\n\nexport async function writeWalletConfig(config: WalletConfig): Promise<void> {\n const walletPath = join(homedir(), \".keeperhub\", \"wallet.json\");\n await mkdir(dirname(walletPath), { recursive: true, mode: 0o700 });\n await writeFile(walletPath, JSON.stringify(config, null, 2), { mode: 0o600 });\n // Reassert mode in case the file already existed with looser perms.\n await chmod(walletPath, 0o600);\n}\n\nexport function getWalletConfigPath(): string {\n return join(homedir(), \".keeperhub\", \"wallet.json\");\n}\n","// Shared types across the package. Phase 34.\nexport type WalletConfig = {\n /** Turnkey sub-org ID returned by POST /api/agentic-wallet/provision */\n subOrgId: string;\n /** EVM-shared wallet address (same for Base chainId 8453 and Tempo chainId 4217) */\n walletAddress: `0x${string}`;\n /** 64-char lowercase hex HMAC secret, minted server-side at provision; never logged */\n hmacSecret: string;\n};\n\nexport type HmacHeaders = {\n \"X-KH-Sub-Org\": string;\n \"X-KH-Timestamp\": string;\n \"X-KH-Signature\": string;\n};\n\nexport type HookDecision = {\n decision: \"allow\" | \"deny\" | \"ask\";\n reason?: string;\n};\n\nexport class KeeperHubError extends Error {\n readonly code: string;\n\n constructor(code: string, message: string) {\n super(message);\n this.name = \"KeeperHubError\";\n this.code = code;\n }\n}\n\n/** Protocol preference for a single pay() or fetch() call. \"auto\" preserves\n * the x402-first default when both challenges are offered. */\nexport type PaymentHint = \"x402\" | \"mpp\" | \"auto\";\n\nexport class WalletConfigMissingError extends Error {\n constructor() {\n super(\n \"Wallet config not found at ~/.keeperhub/wallet.json. Run `npx @keeperhub/wallet add` to provision.\"\n );\n this.name = \"WalletConfigMissingError\";\n }\n}\n"],"mappings":"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","names":["import_viem","import_node_path","import_promises","import_node_os","import_node_path"]}
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cli.ts","../src/balance.ts","../src/chains.ts","../src/fund.ts","../src/skill-install.ts","../src/agent-detect.ts","../src/storage.ts","../src/types.ts"],"sourcesContent":["// CLI dispatcher for `npx @keeperhub/wallet <cmd>`. Ships 4 subcommands:\n// add (provision -- NO auth), fund (pure string-build Coinbase Onramp +\n// Tempo address), balance (Base USDC + Tempo USDC.e), info (print subOrgId\n// + walletAddress from ~/.keeperhub/wallet.json).\n//\n// v0.1.4 removed the `link` subcommand. /api/agentic-wallet/link still\n// exists server-side but the UX (copy-paste session cookie) was not fit\n// for real users; the server-approval ask tier that required linking\n// also collapsed into an inline ask in this release. See KEEP-307 and\n// KEEP-308 for the long-term design decisions.\n//\n// @security The HMAC secret written to wallet.json is NEVER printed to stdout\n// or stderr. `add` prints only subOrgId + walletAddress + the config path so\n// users can inspect perms. `info` never references the secret at all. Grep\n// rule: no process.stdout/process.stderr line in this file should include\n// wallet.hmacSecret or data.hmacSecret.\n//\n// Exit codes: 0 on success, 1 on any error (WalletConfigMissingError,\n// HTTP failure, validation error). Uncaught errors are written to stderr.\n\nimport { Command } from \"commander\";\nimport { checkBalance } from \"./balance.js\";\nimport { fund } from \"./fund.js\";\nimport { installSkill } from \"./skill-install.js\";\nimport {\n getWalletConfigPath,\n readWalletConfig,\n writeWalletConfig,\n} from \"./storage.js\";\nimport { WalletConfigMissingError } from \"./types.js\";\n\nconst TRAILING_SLASH = /\\/$/;\nconst WALLET_ADDRESS_PATTERN = /^0x[a-fA-F0-9]{40}$/;\n\nfunction resolveBaseUrl(override: string | undefined): string {\n const candidate =\n override ?? process.env.KEEPERHUB_API_URL ?? \"https://app.keeperhub.com\";\n return candidate.replace(TRAILING_SLASH, \"\");\n}\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === \"string\" && value.length > 0;\n}\n\nfunction provisionInvalidError(\n message: string\n): Error & { code: \"PROVISION_RESPONSE_INVALID\" } {\n const err = new Error(message) as Error & {\n code: \"PROVISION_RESPONSE_INVALID\";\n };\n err.code = \"PROVISION_RESPONSE_INVALID\";\n return err;\n}\n\nfunction validateProvisionResponse(data: unknown): {\n subOrgId: string;\n walletAddress: `0x${string}`;\n hmacSecret: string;\n} {\n if (typeof data !== \"object\" || data === null) {\n throw provisionInvalidError(\"provision response is not an object\");\n }\n const { subOrgId, walletAddress, hmacSecret } = data as Record<\n string,\n unknown\n >;\n if (\n !(\n isNonEmptyString(subOrgId) &&\n isNonEmptyString(walletAddress) &&\n isNonEmptyString(hmacSecret)\n )\n ) {\n throw provisionInvalidError(\n \"provision response missing subOrgId, walletAddress, or hmacSecret\"\n );\n }\n if (!WALLET_ADDRESS_PATTERN.test(walletAddress)) {\n throw provisionInvalidError(\n `provision response walletAddress is not a valid 0x-prefixed 40-hex address: ${walletAddress}`\n );\n }\n return {\n subOrgId,\n walletAddress: walletAddress as `0x${string}`,\n hmacSecret,\n };\n}\n\nasync function cmdAdd(opts: { baseUrl?: string } = {}): Promise<void> {\n const baseUrl = resolveBaseUrl(opts.baseUrl);\n const response = await fetch(`${baseUrl}/api/agentic-wallet/provision`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: \"{}\",\n });\n if (!response.ok) {\n const text = await response.text();\n process.stderr.write(\n `[keeperhub-wallet] provision failed: HTTP ${response.status}: ${text}\\n`\n );\n process.exit(1);\n }\n const raw = (await response.json()) as unknown;\n const data = validateProvisionResponse(raw);\n await writeWalletConfig({\n subOrgId: data.subOrgId,\n walletAddress: data.walletAddress,\n hmacSecret: data.hmacSecret,\n });\n // Intentionally print only public fields. The hmacSecret is written to\n // wallet.json (chmod 0o600) but never printed -- T-34-cli-02 mitigation.\n process.stdout.write(`subOrgId: ${data.subOrgId}\\n`);\n process.stdout.write(`walletAddress: ${data.walletAddress}\\n`);\n process.stdout.write(`config written to ${getWalletConfigPath()}\\n`);\n}\n\nasync function cmdFund(): Promise<void> {\n const wallet = await readWalletConfig();\n const out = fund(wallet.walletAddress);\n process.stdout.write(`${out.coinbaseOnrampUrl}\\n`);\n process.stdout.write(`Tempo address: ${out.tempoAddress}\\n`);\n process.stdout.write(`${out.disclaimer}\\n`);\n}\n\nasync function cmdBalance(): Promise<void> {\n const wallet = await readWalletConfig();\n const snap = await checkBalance(wallet);\n process.stdout.write(`Base USDC: ${snap.base.amount}\\n`);\n process.stdout.write(`Tempo USDC.e: ${snap.tempo.amount}\\n`);\n}\n\nasync function cmdInfo(): Promise<void> {\n const wallet = await readWalletConfig();\n process.stdout.write(`subOrgId: ${wallet.subOrgId}\\n`);\n process.stdout.write(`walletAddress: ${wallet.walletAddress}\\n`);\n}\n\nexport async function runCli(argv: string[] = process.argv): Promise<void> {\n const program = new Command();\n program\n .name(\"keeperhub-wallet\")\n .description(\n \"KeeperHub agentic wallet CLI (auto-pay x402 + MPP 402 responses)\"\n )\n .version(\"0.1.3\");\n\n program\n .command(\"add\")\n .description(\"Provision a new agentic wallet (no account required)\")\n .option(\"--base-url <url>\", \"KeeperHub API base URL\")\n .action(async (opts: { baseUrl?: string }) => {\n await cmdAdd(opts);\n });\n\n program\n .command(\"fund\")\n .description(\n \"Print Coinbase Onramp URL (Base USDC) and Tempo deposit address\"\n )\n .action(async () => {\n await cmdFund();\n });\n\n program\n .command(\"balance\")\n .description(\"Print on-chain balance: Base USDC + Tempo USDC.e\")\n .action(async () => {\n await cmdBalance();\n });\n\n program\n .command(\"info\")\n .description(\"Print subOrgId and walletAddress from local config\")\n .action(async () => {\n await cmdInfo();\n });\n\n program\n .command(\"skill\")\n .description(\n \"Install the KeeperHub skill file into detected agent directories\"\n )\n .addCommand(\n new Command(\"install\")\n .description(\n \"Write skill file + register PreToolUse hook in all detected agents\"\n )\n .action(async () => {\n const result = await installSkill();\n for (const write of result.skillWrites) {\n process.stdout.write(\n `skill: ${write.agent} -> ${write.path} (${write.status})\\n`\n );\n }\n for (const reg of result.hookRegistrations) {\n if (reg.status === \"registered\") {\n process.stdout.write(\n `hook: ${reg.agent} -> PreToolUse registered\\n`\n );\n } else if (reg.status === \"notice\") {\n process.stderr.write(\n `notice: ${reg.agent} -> ${reg.message ?? \"\"}\\n`\n );\n }\n }\n if (result.skillWrites.length === 0) {\n process.stderr.write(\n \"No supported agent skill directories detected under $HOME. Create ~/.claude/, ~/.cursor/, ~/.cline/, ~/.windsurf/, or ~/.config/opencode/ and re-run.\\n\"\n );\n }\n })\n );\n\n try {\n await program.parseAsync(argv);\n } catch (err) {\n if (err instanceof WalletConfigMissingError) {\n process.stderr.write(`[keeperhub-wallet] ${err.message}\\n`);\n process.exit(1);\n }\n process.stderr.write(\n `[keeperhub-wallet] ${(err as Error).message ?? String(err)}\\n`\n );\n process.exit(1);\n }\n}\n","// checkBalance() unified view (PAY-05):\n// - Base USDC balanceOf (viem publicClient on Base)\n// - Tempo USDC.e balanceOf (viem publicClient on Tempo)\n//\n// Both legs are fetched in parallel via Promise.all. The on-chain reads\n// touch only the canonical USDC contract on their respective chains\n// (read-only ERC-20 balanceOf with no state mutation).\n//\n// The /api/agentic-wallet/credit ledger is intentionally NOT read here:\n// the server endpoint exists but no debit path is wired, so surfacing the\n// balance to users implied a capability that has not shipped. Restore the\n// leg here when KEEP-305/306 lands.\n//\n// @security balance.ts does not emit balance data to stdout/stderr via the\n// global console object or util.inspect (T-34-bal-02 mitigation). Any\n// stdout emitter added here is a privacy regression; grep-enforced in\n// acceptance criteria.\nimport {\n createPublicClient,\n erc20Abi,\n formatUnits,\n http,\n type PublicClient,\n} from \"viem\";\nimport { BASE_USDC, base, TEMPO_USDC_E, tempo } from \"./chains.js\";\nimport type { WalletConfig } from \"./types.js\";\n\n// USDC and USDC.e both use 6 decimals on Base + Tempo respectively.\nconst USDC_DECIMALS = 6;\n\nexport type BalanceSnapshot = {\n base: {\n chain: \"base\";\n token: \"USDC\";\n amount: string;\n address: `0x${string}`;\n };\n tempo: {\n chain: \"tempo\";\n token: \"USDC.e\";\n amount: string;\n address: `0x${string}`;\n };\n};\n\nexport type CheckBalanceOptions = {\n /** Injectable viem client for Base (tests mock readContract). */\n baseClient?: PublicClient;\n /** Injectable viem client for Tempo (tests mock readContract). */\n tempoClient?: PublicClient;\n};\n\n/**\n * Read the wallet's on-chain balance across Base + Tempo in parallel. Both\n * legs must resolve; any single failure rejects the Promise.\n *\n * Amounts are formatted as decimal strings (6-decimal USDC precision) so the\n * caller can render them without BigInt math.\n */\nexport async function checkBalance(\n wallet: WalletConfig,\n opts: CheckBalanceOptions = {}\n): Promise<BalanceSnapshot> {\n const baseClient =\n opts.baseClient ??\n (createPublicClient({\n chain: base,\n transport: http(),\n }) as unknown as PublicClient);\n const tempoClient =\n opts.tempoClient ??\n (createPublicClient({\n chain: tempo,\n transport: http(),\n }) as unknown as PublicClient);\n\n // Promise.all fires both reads concurrently. Total elapsed ~= max(leg)\n // rather than sum(leg); SC-3 (<2s) test asserts this.\n const [baseRaw, tempoRaw] = await Promise.all([\n baseClient.readContract({\n address: BASE_USDC,\n abi: erc20Abi,\n functionName: \"balanceOf\",\n args: [wallet.walletAddress],\n }) as Promise<bigint>,\n tempoClient.readContract({\n address: TEMPO_USDC_E,\n abi: erc20Abi,\n functionName: \"balanceOf\",\n args: [wallet.walletAddress],\n }) as Promise<bigint>,\n ]);\n\n return {\n base: {\n chain: \"base\",\n token: \"USDC\",\n amount: formatUnits(baseRaw, USDC_DECIMALS),\n address: wallet.walletAddress,\n },\n tempo: {\n chain: \"tempo\",\n token: \"USDC.e\",\n amount: formatUnits(tempoRaw, USDC_DECIMALS),\n address: wallet.walletAddress,\n },\n };\n}\n","// Sources (truth):\n// - lib/agentic-wallet/sign.ts:56 -- Base USDC at\n// 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 (chainId 8453).\n// - lib/mpp/server.ts:3 -- Tempo USDC.e at\n// 0x20c000000000000000000000b9537d11c60e8b50 (chainId 4217).\n//\n// Tempo is not in viem/chains core as of viem 2.48.1 (the version pinned in\n// this package). Define it inline via defineChain so the only dependency is\n// viem itself. TEMPO_RPC_URL overrides the default RPC for heavy readers who\n// want to point at their own node (T-34-bal-01 mitigation).\nimport { defineChain } from \"viem\";\n\nexport { base } from \"viem/chains\";\n\nexport const tempo = defineChain({\n id: 4217,\n name: \"Tempo\",\n nativeCurrency: { decimals: 18, name: \"Ether\", symbol: \"ETH\" },\n rpcUrls: {\n default: {\n http: [process.env.TEMPO_RPC_URL ?? \"https://rpc.tempo.xyz\"],\n },\n },\n blockExplorers: {\n default: { name: \"Tempo Explorer\", url: \"https://explorer.tempo.xyz\" },\n },\n});\n\n/** Circle-issued USDC on Base mainnet. */\nexport const BASE_USDC = \"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913\" as const;\n\n/** Bridged USDC (USDC.e) on Tempo mainnet. NOT the same contract as BASE_USDC. */\nexport const TEMPO_USDC_E =\n \"0x20c000000000000000000000b9537d11c60e8b50\" as const;\n","// Source: 34-RESEARCH Pattern 5 + Pitfall 5.\n// Coinbase deprecated the query-param pay.coinbase.com flow in favour of\n// sessionToken URLs on 2025-07-31, but the legacy endpoint still returns a\n// working Onramp page (it just may not pre-fill the asset/network/address\n// fields). We print the legacy URL for zero-dependency ergonomics and a\n// follow-up disclaimer so users know to paste manually if prefill is dropped.\n//\n// fund() is a pure string-build: no HTTP, no process spawn, no browser\n// invocation. Callers (the CLI `keeperhub-wallet fund` subcommand, the\n// `check_balance` skill in Phase 35) decide how to display the result.\n//\n// T-34-fund-01 mitigation: the host is hard-coded (pay.coinbase.com) and the\n// only user-supplied input is the wallet address, which is regex-validated\n// against the canonical 0x-prefixed 40-hex-char EVM format before any string\n// interpolation.\n\nexport type FundInstructions = {\n /** Coinbase Onramp deeplink (legacy query-param form). */\n coinbaseOnrampUrl: string;\n /** Tempo deposit address — same as the input wallet (EVM address shared). */\n tempoAddress: `0x${string}`;\n /** Plain-ASCII guidance string; no emojis (CLAUDE.md rule). */\n disclaimer: string;\n};\n\n// 0x followed by exactly 40 hex chars, case-insensitive. Kept at module scope\n// so the regex literal is compiled once (biome/ultracite useTopLevelRegex).\nconst EVM_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;\n\n// Coinbase Onramp legacy deeplink. The host + path pair is the documented\n// entry point for query-param-style Onramp sessions.\nconst COINBASE_HOST = \"pay.coinbase.com\";\nconst COINBASE_PATH = \"/buy/select-asset\";\n\n/**\n * Build Coinbase Onramp URL + Tempo deposit address for the given wallet.\n *\n * No HTTP calls are performed. The caller is expected to either print the\n * resulting URL (CLI) or render it in a chat bubble (skill). The returned\n * `disclaimer` explains the Onramp deprecation + the Tempo external-transfer\n * fallback in plain ASCII so terminal clients with ASCII-only fonts render\n * identically to emoji-capable clients.\n *\n * @throws if `walletAddress` does not match /^0x[0-9a-fA-F]{40}$/.\n */\nexport function fund(walletAddress: string): FundInstructions {\n if (!EVM_ADDRESS_RE.test(walletAddress)) {\n throw new Error(`Invalid EVM wallet address: ${walletAddress}`);\n }\n\n // addresses is a JSON-encoded map {walletAddress: [\"base\"]} per Coinbase\n // Onramp docs. Encoding into URLSearchParams guarantees the colon,\n // brackets, and quotes are percent-escaped correctly.\n const params = new URLSearchParams({\n defaultNetwork: \"base\",\n defaultAsset: \"USDC\",\n addresses: JSON.stringify({ [walletAddress]: [\"base\"] }),\n presetCryptoAmount: \"5\",\n });\n\n const coinbaseOnrampUrl = `https://${COINBASE_HOST}${COINBASE_PATH}?${params.toString()}`;\n\n const disclaimer =\n \"If the Coinbase page does not pre-fill, paste your address manually. \" +\n \"For Tempo USDC.e, transfer from an exchange or another wallet to the \" +\n \"address above -- Onramp does not support Tempo directly. Coinbase \" +\n \"sessionToken URLs are the 2025+ canonical form; legacy query-param \" +\n \"URLs may drop prefill on some accounts.\";\n\n return {\n coinbaseOnrampUrl,\n tempoAddress: walletAddress as `0x${string}`,\n disclaimer,\n };\n}\n","// Idempotent skill installer for @keeperhub/wallet.\n//\n// Two public entry points:\n// - installSkill(options?) -- writes keeperhub-wallet.skill.md into every\n// detected agent's skills directory and, for Claude Code, registers a\n// PreToolUse hook pointing at `keeperhub-wallet-hook` in\n// ~/.claude/settings.json. For non-claude agents, emits a stderr notice.\n// - registerClaudeCodeHook(settingsPath) -- pure settings.json patcher\n// used internally; exported so tests can drive it directly.\n//\n// Idempotency rule: re-running the installer MUST NOT create a duplicate\n// hook entry. We filter any existing array element whose serialised form\n// contains `keeperhub-wallet-hook` before appending a single fresh record.\n//\n// Preservation rule: all top-level keys in settings.json other than\n// hooks.PreToolUse MUST be byte-preserved. We only ever touch\n// hooks.PreToolUse; any foreign hooks.PostToolUse entries survive verbatim.\n\nimport { chmod, copyFile, mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { type AgentTarget, detectAgents } from \"./agent-detect.js\";\n\nconst HOOK_COMMAND = \"keeperhub-wallet-hook\";\n// Match rule for de-dup: any existing PreToolUse entry whose JSON form\n// mentions this string is considered \"ours\" and is removed before append.\nconst KEEPERHUB_HOOK_MARKER = \"keeperhub-wallet-hook\";\n\nexport type InstallResult = {\n skillWrites: Array<{\n agent: string;\n path: string;\n status: \"written\" | \"skipped\";\n }>;\n hookRegistrations: Array<{\n agent: string;\n status: \"registered\" | \"notice\" | \"skipped\";\n message?: string;\n }>;\n};\n\nexport type InstallOptions = {\n homeOverride?: string;\n skillSourcePath?: string;\n onNotice?: (msg: string) => void;\n};\n\ntype ClaudeHookEntry = {\n matcher: string;\n hooks: Array<{ type: string; command: string }>;\n};\n\ntype ClaudeSettings = {\n hooks?: {\n PreToolUse?: unknown[];\n [k: string]: unknown;\n };\n [k: string]: unknown;\n};\n\nfunction buildKeeperhubEntry(): ClaudeHookEntry {\n return {\n matcher: \"*\",\n hooks: [{ type: \"command\", command: HOOK_COMMAND }],\n };\n}\n\nfunction resolveDefaultSkillSource(): string {\n // Resolve the module's own directory in a way that works in both ESM\n // (import.meta.url) and CJS (__dirname shim emitted by tsup). At runtime\n // the module lives inside dist/, so `../skill/` points at the sibling\n // skill/ directory shipped via pkg.files. During vitest tests the module\n // executes from src/, and `../skill/` resolves to packages/wallet/skill/.\n const here = dirname(fileURLToPath(import.meta.url));\n return join(here, \"..\", \"skill\", \"keeperhub-wallet.skill.md\");\n}\n\nfunction defaultNotice(msg: string): void {\n process.stderr.write(`${msg}\\n`);\n}\n\nexport async function registerClaudeCodeHook(\n settingsPath: string\n): Promise<void> {\n let raw: string | null = null;\n try {\n raw = await readFile(settingsPath, \"utf-8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") {\n throw err;\n }\n }\n\n let config: ClaudeSettings = {};\n if (raw !== null) {\n try {\n config = JSON.parse(raw) as ClaudeSettings;\n } catch {\n throw new Error(\n `settings.json at ${settingsPath} is not valid JSON; aborting hook registration`\n );\n }\n }\n\n const hooks: Record<string, unknown> =\n typeof config.hooks === \"object\" && config.hooks !== null\n ? (config.hooks as Record<string, unknown>)\n : {};\n\n const existingPreToolUse = Array.isArray(hooks.PreToolUse)\n ? (hooks.PreToolUse as unknown[])\n : [];\n\n // De-dup: drop any element that references keeperhub-wallet-hook in its\n // serialised form. Covers both exact-shape matches and any legacy\n // representations we may have written in earlier versions.\n const filtered: unknown[] = [];\n for (const entry of existingPreToolUse) {\n const serialised = JSON.stringify(entry);\n if (!serialised.includes(KEEPERHUB_HOOK_MARKER)) {\n filtered.push(entry);\n }\n }\n filtered.push(buildKeeperhubEntry());\n\n hooks.PreToolUse = filtered;\n config.hooks = hooks as ClaudeSettings[\"hooks\"];\n\n await mkdir(dirname(settingsPath), { recursive: true, mode: 0o700 });\n const payload = `${JSON.stringify(config, null, 2)}\\n`;\n await writeFile(settingsPath, payload, { mode: 0o600 });\n // Reassert mode in case the file already existed with looser perms.\n await chmod(settingsPath, 0o600);\n}\n\nasync function writeSkillToAgent(\n agent: AgentTarget,\n skillSource: string\n): Promise<{ agent: string; path: string; status: \"written\" | \"skipped\" }> {\n await mkdir(agent.skillsDir, { recursive: true, mode: 0o755 });\n const target = join(agent.skillsDir, \"keeperhub-wallet.skill.md\");\n await copyFile(skillSource, target);\n await chmod(target, 0o644);\n return { agent: agent.agent, path: target, status: \"written\" };\n}\n\nfunction buildNoticeMessage(agent: AgentTarget): string {\n return `${agent.agent} does not support auto-registered PreToolUse hooks; run \\`${HOOK_COMMAND}\\` on every tool use via ${agent.agent}'s settings file at ${agent.settingsFile}`;\n}\n\nexport async function installSkill(\n options: InstallOptions = {}\n): Promise<InstallResult> {\n const agents = detectAgents(options.homeOverride);\n const skillSource = options.skillSourcePath ?? resolveDefaultSkillSource();\n const onNotice = options.onNotice ?? defaultNotice;\n\n const skillWrites: InstallResult[\"skillWrites\"] = [];\n const hookRegistrations: InstallResult[\"hookRegistrations\"] = [];\n\n for (const agent of agents) {\n const write = await writeSkillToAgent(agent, skillSource);\n skillWrites.push(write);\n\n if (agent.hookSupport === \"claude-code\") {\n await registerClaudeCodeHook(agent.settingsFile);\n hookRegistrations.push({\n agent: agent.agent,\n status: \"registered\",\n });\n } else {\n const message = buildNoticeMessage(agent);\n hookRegistrations.push({\n agent: agent.agent,\n status: \"notice\",\n message,\n });\n onNotice(message);\n }\n }\n\n return { skillWrites, hookRegistrations };\n}\n","// Cross-agent skill/settings directory discovery.\n//\n// Probes canonical paths under $HOME and returns one AgentTarget record per\n// agent whose parent directory exists. The `skills/` leaf may be absent --\n// installSkill() creates it.\n//\n// NOTE: `homedir()` is called per-invocation (via `homeOverride ?? homedir()`)\n// and NEVER hoisted to a module-level constant. Tests override\n// `process.env.HOME` in `beforeEach`; hoisting would freeze the harness's\n// original HOME at import time and detection would run against the real $HOME.\n\nimport { existsSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\n\nexport type AgentTarget = {\n agent: \"claude-code\" | \"cursor\" | \"cline\" | \"windsurf\" | \"opencode\";\n skillsDir: string;\n settingsFile: string;\n hookSupport: \"claude-code\" | \"notice\";\n};\n\ntype AgentSpec = {\n agent: AgentTarget[\"agent\"];\n skillsRel: string[];\n settingsRel: string[];\n hookSupport: AgentTarget[\"hookSupport\"];\n};\n\n// Deterministic order: claude-code first (only agent with hook support),\n// then cursor, cline, windsurf, opencode.\nconst AGENT_SPECS: readonly AgentSpec[] = [\n {\n agent: \"claude-code\",\n skillsRel: [\".claude\", \"skills\"],\n settingsRel: [\".claude\", \"settings.json\"],\n hookSupport: \"claude-code\",\n },\n {\n agent: \"cursor\",\n skillsRel: [\".cursor\", \"skills\"],\n settingsRel: [\".cursor\", \"settings.json\"],\n hookSupport: \"notice\",\n },\n {\n agent: \"cline\",\n skillsRel: [\".cline\", \"skills\"],\n settingsRel: [\".cline\", \"settings.json\"],\n hookSupport: \"notice\",\n },\n {\n agent: \"windsurf\",\n skillsRel: [\".windsurf\", \"skills\"],\n settingsRel: [\".windsurf\", \"settings.json\"],\n hookSupport: \"notice\",\n },\n {\n agent: \"opencode\",\n skillsRel: [\".config\", \"opencode\", \"skills\"],\n settingsRel: [\".config\", \"opencode\", \"settings.json\"],\n hookSupport: \"notice\",\n },\n];\n\nexport function detectAgents(homeOverride?: string): AgentTarget[] {\n const home = homeOverride ?? homedir();\n const results: AgentTarget[] = [];\n for (const spec of AGENT_SPECS) {\n const skillsDir = join(home, ...spec.skillsRel);\n const settingsFile = join(home, ...spec.settingsRel);\n // \"Detected\" iff the parent of skills/ exists (e.g. ~/.claude/).\n // skills/ itself may be absent; installer creates it.\n if (existsSync(dirname(skillsDir))) {\n results.push({\n agent: spec.agent,\n skillsDir,\n settingsFile,\n hookSupport: spec.hookSupport,\n });\n }\n }\n return results;\n}\n","import { chmod, mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { type WalletConfig, WalletConfigMissingError } from \"./types.js\";\n\n// NOTE: Every function calls `join(homedir(), \".keeperhub\", \"wallet.json\")`\n// itself. Do NOT hoist to a module-level `const WALLET_PATH` -- tests\n// override `process.env.HOME` in `beforeEach` and `homedir()` must re-read\n// that on each call. A hoisted constant would freeze the harness's original\n// HOME at import time and every test would write into the real\n// ~/.keeperhub/ directory.\n\nexport async function readWalletConfig(): Promise<WalletConfig> {\n const walletPath = join(homedir(), \".keeperhub\", \"wallet.json\");\n let raw: string;\n try {\n raw = await readFile(walletPath, \"utf-8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n throw new WalletConfigMissingError();\n }\n throw err;\n }\n const parsed = JSON.parse(raw) as Partial<WalletConfig>;\n if (!(parsed.subOrgId && parsed.walletAddress && parsed.hmacSecret)) {\n throw new Error(`Malformed wallet.json at ${walletPath}`);\n }\n return parsed as WalletConfig;\n}\n\nexport async function writeWalletConfig(config: WalletConfig): Promise<void> {\n const walletPath = join(homedir(), \".keeperhub\", \"wallet.json\");\n await mkdir(dirname(walletPath), { recursive: true, mode: 0o700 });\n await writeFile(walletPath, JSON.stringify(config, null, 2), { mode: 0o600 });\n // Reassert mode in case the file already existed with looser perms.\n await chmod(walletPath, 0o600);\n}\n\nexport function getWalletConfigPath(): string {\n return join(homedir(), \".keeperhub\", \"wallet.json\");\n}\n","// Shared types across the package. Phase 34.\nexport type WalletConfig = {\n /** Turnkey sub-org ID returned by POST /api/agentic-wallet/provision */\n subOrgId: string;\n /** EVM-shared wallet address (same for Base chainId 8453 and Tempo chainId 4217) */\n walletAddress: `0x${string}`;\n /** 64-char lowercase hex HMAC secret, minted server-side at provision; never logged */\n hmacSecret: string;\n};\n\nexport type HmacHeaders = {\n \"X-KH-Sub-Org\": string;\n \"X-KH-Timestamp\": string;\n \"X-KH-Signature\": string;\n};\n\nexport type HookDecision = {\n decision: \"allow\" | \"deny\" | \"ask\";\n reason?: string;\n};\n\nexport class KeeperHubError extends Error {\n readonly code: string;\n\n constructor(code: string, message: string) {\n super(message);\n this.name = \"KeeperHubError\";\n this.code = code;\n }\n}\n\nexport class WalletConfigMissingError extends Error {\n constructor() {\n super(\n \"Wallet config not found at ~/.keeperhub/wallet.json. Run `npx @keeperhub/wallet add` to provision.\"\n );\n this.name = \"WalletConfigMissingError\";\n }\n}\n"],"mappings":"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","names":["dirname","join","dirname","join","chmod","mkdir","readFile","writeFile","homedir","dirname","join","join","homedir","readFile","mkdir","dirname","writeFile","chmod"]}
|
|
1
|
+
{"version":3,"sources":["../src/cli.ts","../src/balance.ts","../src/chains.ts","../src/fund.ts","../src/skill-install.ts","../src/agent-detect.ts","../src/storage.ts","../src/types.ts"],"sourcesContent":["// CLI dispatcher for `npx @keeperhub/wallet <cmd>`. Ships 4 subcommands:\n// add (provision -- NO auth), fund (pure string-build Coinbase Onramp +\n// Tempo address), balance (Base USDC + Tempo USDC.e), info (print subOrgId\n// + walletAddress from ~/.keeperhub/wallet.json).\n//\n// v0.1.4 removed the `link` subcommand. /api/agentic-wallet/link still\n// exists server-side but the UX (copy-paste session cookie) was not fit\n// for real users; the server-approval ask tier that required linking\n// also collapsed into an inline ask in this release. See KEEP-307 and\n// KEEP-308 for the long-term design decisions.\n//\n// @security The HMAC secret written to wallet.json is NEVER printed to stdout\n// or stderr. `add` prints only subOrgId + walletAddress + the config path so\n// users can inspect perms. `info` never references the secret at all. Grep\n// rule: no process.stdout/process.stderr line in this file should include\n// wallet.hmacSecret or data.hmacSecret.\n//\n// Exit codes: 0 on success, 1 on any error (WalletConfigMissingError,\n// HTTP failure, validation error). Uncaught errors are written to stderr.\n\nimport { Command } from \"commander\";\nimport { checkBalance } from \"./balance.js\";\nimport { fund } from \"./fund.js\";\nimport { installSkill } from \"./skill-install.js\";\nimport {\n getWalletConfigPath,\n readWalletConfig,\n writeWalletConfig,\n} from \"./storage.js\";\nimport { WalletConfigMissingError } from \"./types.js\";\n\nconst TRAILING_SLASH = /\\/$/;\nconst WALLET_ADDRESS_PATTERN = /^0x[a-fA-F0-9]{40}$/;\n\nfunction resolveBaseUrl(override: string | undefined): string {\n const candidate =\n override ?? process.env.KEEPERHUB_API_URL ?? \"https://app.keeperhub.com\";\n return candidate.replace(TRAILING_SLASH, \"\");\n}\n\nfunction isNonEmptyString(value: unknown): value is string {\n return typeof value === \"string\" && value.length > 0;\n}\n\nfunction provisionInvalidError(\n message: string\n): Error & { code: \"PROVISION_RESPONSE_INVALID\" } {\n const err = new Error(message) as Error & {\n code: \"PROVISION_RESPONSE_INVALID\";\n };\n err.code = \"PROVISION_RESPONSE_INVALID\";\n return err;\n}\n\nfunction validateProvisionResponse(data: unknown): {\n subOrgId: string;\n walletAddress: `0x${string}`;\n hmacSecret: string;\n} {\n if (typeof data !== \"object\" || data === null) {\n throw provisionInvalidError(\"provision response is not an object\");\n }\n const { subOrgId, walletAddress, hmacSecret } = data as Record<\n string,\n unknown\n >;\n if (\n !(\n isNonEmptyString(subOrgId) &&\n isNonEmptyString(walletAddress) &&\n isNonEmptyString(hmacSecret)\n )\n ) {\n throw provisionInvalidError(\n \"provision response missing subOrgId, walletAddress, or hmacSecret\"\n );\n }\n if (!WALLET_ADDRESS_PATTERN.test(walletAddress)) {\n throw provisionInvalidError(\n `provision response walletAddress is not a valid 0x-prefixed 40-hex address: ${walletAddress}`\n );\n }\n return {\n subOrgId,\n walletAddress: walletAddress as `0x${string}`,\n hmacSecret,\n };\n}\n\nasync function cmdAdd(opts: { baseUrl?: string } = {}): Promise<void> {\n const baseUrl = resolveBaseUrl(opts.baseUrl);\n const response = await fetch(`${baseUrl}/api/agentic-wallet/provision`, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: \"{}\",\n });\n if (!response.ok) {\n const text = await response.text();\n process.stderr.write(\n `[keeperhub-wallet] provision failed: HTTP ${response.status}: ${text}\\n`\n );\n process.exit(1);\n }\n const raw = (await response.json()) as unknown;\n const data = validateProvisionResponse(raw);\n await writeWalletConfig({\n subOrgId: data.subOrgId,\n walletAddress: data.walletAddress,\n hmacSecret: data.hmacSecret,\n });\n // Intentionally print only public fields. The hmacSecret is written to\n // wallet.json (chmod 0o600) but never printed -- T-34-cli-02 mitigation.\n process.stdout.write(`subOrgId: ${data.subOrgId}\\n`);\n process.stdout.write(`walletAddress: ${data.walletAddress}\\n`);\n process.stdout.write(`config written to ${getWalletConfigPath()}\\n`);\n}\n\nasync function cmdFund(): Promise<void> {\n const wallet = await readWalletConfig();\n const out = fund(wallet.walletAddress);\n process.stdout.write(`${out.coinbaseOnrampUrl}\\n`);\n process.stdout.write(`Tempo address: ${out.tempoAddress}\\n`);\n process.stdout.write(`${out.disclaimer}\\n`);\n}\n\nasync function cmdBalance(): Promise<void> {\n const wallet = await readWalletConfig();\n const snap = await checkBalance(wallet);\n process.stdout.write(`Base USDC: ${snap.base.amount}\\n`);\n process.stdout.write(`Tempo USDC.e: ${snap.tempo.amount}\\n`);\n}\n\nasync function cmdInfo(): Promise<void> {\n const wallet = await readWalletConfig();\n process.stdout.write(`subOrgId: ${wallet.subOrgId}\\n`);\n process.stdout.write(`walletAddress: ${wallet.walletAddress}\\n`);\n}\n\nexport async function runCli(argv: string[] = process.argv): Promise<void> {\n const program = new Command();\n program\n .name(\"keeperhub-wallet\")\n .description(\n \"KeeperHub agentic wallet CLI (auto-pay x402 + MPP 402 responses)\"\n )\n .version(\"0.1.3\");\n\n program\n .command(\"add\")\n .description(\"Provision a new agentic wallet (no account required)\")\n .option(\"--base-url <url>\", \"KeeperHub API base URL\")\n .action(async (opts: { baseUrl?: string }) => {\n await cmdAdd(opts);\n });\n\n program\n .command(\"fund\")\n .description(\n \"Print Coinbase Onramp URL (Base USDC) and Tempo deposit address\"\n )\n .action(async () => {\n await cmdFund();\n });\n\n program\n .command(\"balance\")\n .description(\"Print on-chain balance: Base USDC + Tempo USDC.e\")\n .action(async () => {\n await cmdBalance();\n });\n\n program\n .command(\"info\")\n .description(\"Print subOrgId and walletAddress from local config\")\n .action(async () => {\n await cmdInfo();\n });\n\n program\n .command(\"skill\")\n .description(\n \"Install the KeeperHub skill file into detected agent directories\"\n )\n .addCommand(\n new Command(\"install\")\n .description(\n \"Write skill file + register PreToolUse hook in all detected agents\"\n )\n .action(async () => {\n const result = await installSkill();\n for (const write of result.skillWrites) {\n process.stdout.write(\n `skill: ${write.agent} -> ${write.path} (${write.status})\\n`\n );\n }\n for (const reg of result.hookRegistrations) {\n if (reg.status === \"registered\") {\n process.stdout.write(\n `hook: ${reg.agent} -> PreToolUse registered\\n`\n );\n } else if (reg.status === \"notice\") {\n process.stderr.write(\n `notice: ${reg.agent} -> ${reg.message ?? \"\"}\\n`\n );\n }\n }\n if (result.skillWrites.length === 0) {\n process.stderr.write(\n \"No supported agent skill directories detected under $HOME. Create ~/.claude/, ~/.cursor/, ~/.cline/, ~/.windsurf/, or ~/.config/opencode/ and re-run.\\n\"\n );\n }\n })\n );\n\n try {\n await program.parseAsync(argv);\n } catch (err) {\n if (err instanceof WalletConfigMissingError) {\n process.stderr.write(`[keeperhub-wallet] ${err.message}\\n`);\n process.exit(1);\n }\n process.stderr.write(\n `[keeperhub-wallet] ${(err as Error).message ?? String(err)}\\n`\n );\n process.exit(1);\n }\n}\n","// checkBalance() unified view (PAY-05):\n// - Base USDC balanceOf (viem publicClient on Base)\n// - Tempo USDC.e balanceOf (viem publicClient on Tempo)\n//\n// Both legs are fetched in parallel via Promise.all. The on-chain reads\n// touch only the canonical USDC contract on their respective chains\n// (read-only ERC-20 balanceOf with no state mutation).\n//\n// The /api/agentic-wallet/credit ledger is intentionally NOT read here:\n// the server endpoint exists but no debit path is wired, so surfacing the\n// balance to users implied a capability that has not shipped. Restore the\n// leg here when KEEP-305/306 lands.\n//\n// @security balance.ts does not emit balance data to stdout/stderr via the\n// global console object or util.inspect (T-34-bal-02 mitigation). Any\n// stdout emitter added here is a privacy regression; grep-enforced in\n// acceptance criteria.\nimport {\n createPublicClient,\n erc20Abi,\n formatUnits,\n http,\n type PublicClient,\n} from \"viem\";\nimport { BASE_USDC, base, TEMPO_USDC_E, tempo } from \"./chains.js\";\nimport type { WalletConfig } from \"./types.js\";\n\n// USDC and USDC.e both use 6 decimals on Base + Tempo respectively.\nconst USDC_DECIMALS = 6;\n\nexport type BalanceSnapshot = {\n base: {\n chain: \"base\";\n token: \"USDC\";\n amount: string;\n address: `0x${string}`;\n };\n tempo: {\n chain: \"tempo\";\n token: \"USDC.e\";\n amount: string;\n address: `0x${string}`;\n };\n};\n\nexport type CheckBalanceOptions = {\n /** Injectable viem client for Base (tests mock readContract). */\n baseClient?: PublicClient;\n /** Injectable viem client for Tempo (tests mock readContract). */\n tempoClient?: PublicClient;\n};\n\n/**\n * Read the wallet's on-chain balance across Base + Tempo in parallel. Both\n * legs must resolve; any single failure rejects the Promise.\n *\n * Amounts are formatted as decimal strings (6-decimal USDC precision) so the\n * caller can render them without BigInt math.\n */\nexport async function checkBalance(\n wallet: WalletConfig,\n opts: CheckBalanceOptions = {}\n): Promise<BalanceSnapshot> {\n const baseClient =\n opts.baseClient ??\n (createPublicClient({\n chain: base,\n transport: http(),\n }) as unknown as PublicClient);\n const tempoClient =\n opts.tempoClient ??\n (createPublicClient({\n chain: tempo,\n transport: http(),\n }) as unknown as PublicClient);\n\n // Promise.all fires both reads concurrently. Total elapsed ~= max(leg)\n // rather than sum(leg); SC-3 (<2s) test asserts this.\n const [baseRaw, tempoRaw] = await Promise.all([\n baseClient.readContract({\n address: BASE_USDC,\n abi: erc20Abi,\n functionName: \"balanceOf\",\n args: [wallet.walletAddress],\n }) as Promise<bigint>,\n tempoClient.readContract({\n address: TEMPO_USDC_E,\n abi: erc20Abi,\n functionName: \"balanceOf\",\n args: [wallet.walletAddress],\n }) as Promise<bigint>,\n ]);\n\n return {\n base: {\n chain: \"base\",\n token: \"USDC\",\n amount: formatUnits(baseRaw, USDC_DECIMALS),\n address: wallet.walletAddress,\n },\n tempo: {\n chain: \"tempo\",\n token: \"USDC.e\",\n amount: formatUnits(tempoRaw, USDC_DECIMALS),\n address: wallet.walletAddress,\n },\n };\n}\n","// Sources (truth):\n// - lib/agentic-wallet/sign.ts:56 -- Base USDC at\n// 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 (chainId 8453).\n// - lib/mpp/server.ts:3 -- Tempo USDC.e at\n// 0x20c000000000000000000000b9537d11c60e8b50 (chainId 4217).\n//\n// Tempo is not in viem/chains core as of viem 2.48.1 (the version pinned in\n// this package). Define it inline via defineChain so the only dependency is\n// viem itself. TEMPO_RPC_URL overrides the default RPC for heavy readers who\n// want to point at their own node (T-34-bal-01 mitigation).\nimport { defineChain } from \"viem\";\n\nexport { base } from \"viem/chains\";\n\nexport const tempo = defineChain({\n id: 4217,\n name: \"Tempo\",\n nativeCurrency: { decimals: 18, name: \"Ether\", symbol: \"ETH\" },\n rpcUrls: {\n default: {\n http: [process.env.TEMPO_RPC_URL ?? \"https://rpc.tempo.xyz\"],\n },\n },\n blockExplorers: {\n default: { name: \"Tempo Explorer\", url: \"https://explorer.tempo.xyz\" },\n },\n});\n\n/** Circle-issued USDC on Base mainnet. */\nexport const BASE_USDC = \"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913\" as const;\n\n/** Bridged USDC (USDC.e) on Tempo mainnet. NOT the same contract as BASE_USDC. */\nexport const TEMPO_USDC_E =\n \"0x20c000000000000000000000b9537d11c60e8b50\" as const;\n","// Source: 34-RESEARCH Pattern 5 + Pitfall 5.\n// Coinbase deprecated the query-param pay.coinbase.com flow in favour of\n// sessionToken URLs on 2025-07-31, but the legacy endpoint still returns a\n// working Onramp page (it just may not pre-fill the asset/network/address\n// fields). We print the legacy URL for zero-dependency ergonomics and a\n// follow-up disclaimer so users know to paste manually if prefill is dropped.\n//\n// fund() is a pure string-build: no HTTP, no process spawn, no browser\n// invocation. Callers (the CLI `keeperhub-wallet fund` subcommand, the\n// `check_balance` skill in Phase 35) decide how to display the result.\n//\n// T-34-fund-01 mitigation: the host is hard-coded (pay.coinbase.com) and the\n// only user-supplied input is the wallet address, which is regex-validated\n// against the canonical 0x-prefixed 40-hex-char EVM format before any string\n// interpolation.\n\nexport type FundInstructions = {\n /** Coinbase Onramp deeplink (legacy query-param form). */\n coinbaseOnrampUrl: string;\n /** Tempo deposit address — same as the input wallet (EVM address shared). */\n tempoAddress: `0x${string}`;\n /** Plain-ASCII guidance string; no emojis (CLAUDE.md rule). */\n disclaimer: string;\n};\n\n// 0x followed by exactly 40 hex chars, case-insensitive. Kept at module scope\n// so the regex literal is compiled once (biome/ultracite useTopLevelRegex).\nconst EVM_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;\n\n// Coinbase Onramp legacy deeplink. The host + path pair is the documented\n// entry point for query-param-style Onramp sessions.\nconst COINBASE_HOST = \"pay.coinbase.com\";\nconst COINBASE_PATH = \"/buy/select-asset\";\n\n/**\n * Build Coinbase Onramp URL + Tempo deposit address for the given wallet.\n *\n * No HTTP calls are performed. The caller is expected to either print the\n * resulting URL (CLI) or render it in a chat bubble (skill). The returned\n * `disclaimer` explains the Onramp deprecation + the Tempo external-transfer\n * fallback in plain ASCII so terminal clients with ASCII-only fonts render\n * identically to emoji-capable clients.\n *\n * @throws if `walletAddress` does not match /^0x[0-9a-fA-F]{40}$/.\n */\nexport function fund(walletAddress: string): FundInstructions {\n if (!EVM_ADDRESS_RE.test(walletAddress)) {\n throw new Error(`Invalid EVM wallet address: ${walletAddress}`);\n }\n\n // addresses is a JSON-encoded map {walletAddress: [\"base\"]} per Coinbase\n // Onramp docs. Encoding into URLSearchParams guarantees the colon,\n // brackets, and quotes are percent-escaped correctly.\n const params = new URLSearchParams({\n defaultNetwork: \"base\",\n defaultAsset: \"USDC\",\n addresses: JSON.stringify({ [walletAddress]: [\"base\"] }),\n presetCryptoAmount: \"5\",\n });\n\n const coinbaseOnrampUrl = `https://${COINBASE_HOST}${COINBASE_PATH}?${params.toString()}`;\n\n const disclaimer =\n \"If the Coinbase page does not pre-fill, paste your address manually. \" +\n \"For Tempo USDC.e, transfer from an exchange or another wallet to the \" +\n \"address above -- Onramp does not support Tempo directly. Coinbase \" +\n \"sessionToken URLs are the 2025+ canonical form; legacy query-param \" +\n \"URLs may drop prefill on some accounts.\";\n\n return {\n coinbaseOnrampUrl,\n tempoAddress: walletAddress as `0x${string}`,\n disclaimer,\n };\n}\n","// Idempotent skill installer for @keeperhub/wallet.\n//\n// Two public entry points:\n// - installSkill(options?) -- writes keeperhub-wallet.skill.md into every\n// detected agent's skills directory and, for Claude Code, registers a\n// PreToolUse hook pointing at `keeperhub-wallet-hook` in\n// ~/.claude/settings.json. For non-claude agents, emits a stderr notice.\n// - registerClaudeCodeHook(settingsPath) -- pure settings.json patcher\n// used internally; exported so tests can drive it directly.\n//\n// Idempotency rule: re-running the installer MUST NOT create a duplicate\n// hook entry. We filter any existing array element whose serialised form\n// contains `keeperhub-wallet-hook` before appending a single fresh record.\n//\n// Preservation rule: all top-level keys in settings.json other than\n// hooks.PreToolUse MUST be byte-preserved. We only ever touch\n// hooks.PreToolUse; any foreign hooks.PostToolUse entries survive verbatim.\n\nimport { chmod, copyFile, mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { type AgentTarget, detectAgents } from \"./agent-detect.js\";\n\nconst HOOK_COMMAND = \"keeperhub-wallet-hook\";\n// Match rule for de-dup: any existing PreToolUse entry whose JSON form\n// mentions this string is considered \"ours\" and is removed before append.\nconst KEEPERHUB_HOOK_MARKER = \"keeperhub-wallet-hook\";\n\nexport type InstallResult = {\n skillWrites: Array<{\n agent: string;\n path: string;\n status: \"written\" | \"skipped\";\n }>;\n hookRegistrations: Array<{\n agent: string;\n status: \"registered\" | \"notice\" | \"skipped\";\n message?: string;\n }>;\n};\n\nexport type InstallOptions = {\n homeOverride?: string;\n skillSourcePath?: string;\n onNotice?: (msg: string) => void;\n};\n\ntype ClaudeHookEntry = {\n matcher: string;\n hooks: Array<{ type: string; command: string }>;\n};\n\ntype ClaudeSettings = {\n hooks?: {\n PreToolUse?: unknown[];\n [k: string]: unknown;\n };\n [k: string]: unknown;\n};\n\nfunction buildKeeperhubEntry(): ClaudeHookEntry {\n return {\n matcher: \"*\",\n hooks: [{ type: \"command\", command: HOOK_COMMAND }],\n };\n}\n\nfunction resolveDefaultSkillSource(): string {\n // Resolve the module's own directory in a way that works in both ESM\n // (import.meta.url) and CJS (__dirname shim emitted by tsup). At runtime\n // the module lives inside dist/, so `../skill/` points at the sibling\n // skill/ directory shipped via pkg.files. During vitest tests the module\n // executes from src/, and `../skill/` resolves to packages/wallet/skill/.\n const here = dirname(fileURLToPath(import.meta.url));\n return join(here, \"..\", \"skill\", \"keeperhub-wallet.skill.md\");\n}\n\nfunction defaultNotice(msg: string): void {\n process.stderr.write(`${msg}\\n`);\n}\n\nexport async function registerClaudeCodeHook(\n settingsPath: string\n): Promise<void> {\n let raw: string | null = null;\n try {\n raw = await readFile(settingsPath, \"utf-8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") {\n throw err;\n }\n }\n\n let config: ClaudeSettings = {};\n if (raw !== null) {\n try {\n config = JSON.parse(raw) as ClaudeSettings;\n } catch {\n throw new Error(\n `settings.json at ${settingsPath} is not valid JSON; aborting hook registration`\n );\n }\n }\n\n const hooks: Record<string, unknown> =\n typeof config.hooks === \"object\" && config.hooks !== null\n ? (config.hooks as Record<string, unknown>)\n : {};\n\n const existingPreToolUse = Array.isArray(hooks.PreToolUse)\n ? (hooks.PreToolUse as unknown[])\n : [];\n\n // De-dup: drop any element that references keeperhub-wallet-hook in its\n // serialised form. Covers both exact-shape matches and any legacy\n // representations we may have written in earlier versions.\n const filtered: unknown[] = [];\n for (const entry of existingPreToolUse) {\n const serialised = JSON.stringify(entry);\n if (!serialised.includes(KEEPERHUB_HOOK_MARKER)) {\n filtered.push(entry);\n }\n }\n filtered.push(buildKeeperhubEntry());\n\n hooks.PreToolUse = filtered;\n config.hooks = hooks as ClaudeSettings[\"hooks\"];\n\n await mkdir(dirname(settingsPath), { recursive: true, mode: 0o700 });\n const payload = `${JSON.stringify(config, null, 2)}\\n`;\n await writeFile(settingsPath, payload, { mode: 0o600 });\n // Reassert mode in case the file already existed with looser perms.\n await chmod(settingsPath, 0o600);\n}\n\nasync function writeSkillToAgent(\n agent: AgentTarget,\n skillSource: string\n): Promise<{ agent: string; path: string; status: \"written\" | \"skipped\" }> {\n await mkdir(agent.skillsDir, { recursive: true, mode: 0o755 });\n const target = join(agent.skillsDir, \"keeperhub-wallet.skill.md\");\n await copyFile(skillSource, target);\n await chmod(target, 0o644);\n return { agent: agent.agent, path: target, status: \"written\" };\n}\n\nfunction buildNoticeMessage(agent: AgentTarget): string {\n return `${agent.agent} does not support auto-registered PreToolUse hooks; run \\`${HOOK_COMMAND}\\` on every tool use via ${agent.agent}'s settings file at ${agent.settingsFile}`;\n}\n\nexport async function installSkill(\n options: InstallOptions = {}\n): Promise<InstallResult> {\n const agents = detectAgents(options.homeOverride);\n const skillSource = options.skillSourcePath ?? resolveDefaultSkillSource();\n const onNotice = options.onNotice ?? defaultNotice;\n\n const skillWrites: InstallResult[\"skillWrites\"] = [];\n const hookRegistrations: InstallResult[\"hookRegistrations\"] = [];\n\n for (const agent of agents) {\n const write = await writeSkillToAgent(agent, skillSource);\n skillWrites.push(write);\n\n if (agent.hookSupport === \"claude-code\") {\n await registerClaudeCodeHook(agent.settingsFile);\n hookRegistrations.push({\n agent: agent.agent,\n status: \"registered\",\n });\n } else {\n const message = buildNoticeMessage(agent);\n hookRegistrations.push({\n agent: agent.agent,\n status: \"notice\",\n message,\n });\n onNotice(message);\n }\n }\n\n return { skillWrites, hookRegistrations };\n}\n","// Cross-agent skill/settings directory discovery.\n//\n// Probes canonical paths under $HOME and returns one AgentTarget record per\n// agent whose parent directory exists. The `skills/` leaf may be absent --\n// installSkill() creates it.\n//\n// NOTE: `homedir()` is called per-invocation (via `homeOverride ?? homedir()`)\n// and NEVER hoisted to a module-level constant. Tests override\n// `process.env.HOME` in `beforeEach`; hoisting would freeze the harness's\n// original HOME at import time and detection would run against the real $HOME.\n\nimport { existsSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\n\nexport type AgentTarget = {\n agent: \"claude-code\" | \"cursor\" | \"cline\" | \"windsurf\" | \"opencode\";\n skillsDir: string;\n settingsFile: string;\n hookSupport: \"claude-code\" | \"notice\";\n};\n\ntype AgentSpec = {\n agent: AgentTarget[\"agent\"];\n skillsRel: string[];\n settingsRel: string[];\n hookSupport: AgentTarget[\"hookSupport\"];\n};\n\n// Deterministic order: claude-code first (only agent with hook support),\n// then cursor, cline, windsurf, opencode.\nconst AGENT_SPECS: readonly AgentSpec[] = [\n {\n agent: \"claude-code\",\n skillsRel: [\".claude\", \"skills\"],\n settingsRel: [\".claude\", \"settings.json\"],\n hookSupport: \"claude-code\",\n },\n {\n agent: \"cursor\",\n skillsRel: [\".cursor\", \"skills\"],\n settingsRel: [\".cursor\", \"settings.json\"],\n hookSupport: \"notice\",\n },\n {\n agent: \"cline\",\n skillsRel: [\".cline\", \"skills\"],\n settingsRel: [\".cline\", \"settings.json\"],\n hookSupport: \"notice\",\n },\n {\n agent: \"windsurf\",\n skillsRel: [\".windsurf\", \"skills\"],\n settingsRel: [\".windsurf\", \"settings.json\"],\n hookSupport: \"notice\",\n },\n {\n agent: \"opencode\",\n skillsRel: [\".config\", \"opencode\", \"skills\"],\n settingsRel: [\".config\", \"opencode\", \"settings.json\"],\n hookSupport: \"notice\",\n },\n];\n\nexport function detectAgents(homeOverride?: string): AgentTarget[] {\n const home = homeOverride ?? homedir();\n const results: AgentTarget[] = [];\n for (const spec of AGENT_SPECS) {\n const skillsDir = join(home, ...spec.skillsRel);\n const settingsFile = join(home, ...spec.settingsRel);\n // \"Detected\" iff the parent of skills/ exists (e.g. ~/.claude/).\n // skills/ itself may be absent; installer creates it.\n if (existsSync(dirname(skillsDir))) {\n results.push({\n agent: spec.agent,\n skillsDir,\n settingsFile,\n hookSupport: spec.hookSupport,\n });\n }\n }\n return results;\n}\n","import { chmod, mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { type WalletConfig, WalletConfigMissingError } from \"./types.js\";\n\n// NOTE: Every function calls `join(homedir(), \".keeperhub\", \"wallet.json\")`\n// itself. Do NOT hoist to a module-level `const WALLET_PATH` -- tests\n// override `process.env.HOME` in `beforeEach` and `homedir()` must re-read\n// that on each call. A hoisted constant would freeze the harness's original\n// HOME at import time and every test would write into the real\n// ~/.keeperhub/ directory.\n\nexport async function readWalletConfig(): Promise<WalletConfig> {\n const walletPath = join(homedir(), \".keeperhub\", \"wallet.json\");\n let raw: string;\n try {\n raw = await readFile(walletPath, \"utf-8\");\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") {\n throw new WalletConfigMissingError();\n }\n throw err;\n }\n const parsed = JSON.parse(raw) as Partial<WalletConfig>;\n if (!(parsed.subOrgId && parsed.walletAddress && parsed.hmacSecret)) {\n throw new Error(`Malformed wallet.json at ${walletPath}`);\n }\n return parsed as WalletConfig;\n}\n\nexport async function writeWalletConfig(config: WalletConfig): Promise<void> {\n const walletPath = join(homedir(), \".keeperhub\", \"wallet.json\");\n await mkdir(dirname(walletPath), { recursive: true, mode: 0o700 });\n await writeFile(walletPath, JSON.stringify(config, null, 2), { mode: 0o600 });\n // Reassert mode in case the file already existed with looser perms.\n await chmod(walletPath, 0o600);\n}\n\nexport function getWalletConfigPath(): string {\n return join(homedir(), \".keeperhub\", \"wallet.json\");\n}\n","// Shared types across the package. Phase 34.\nexport type WalletConfig = {\n /** Turnkey sub-org ID returned by POST /api/agentic-wallet/provision */\n subOrgId: string;\n /** EVM-shared wallet address (same for Base chainId 8453 and Tempo chainId 4217) */\n walletAddress: `0x${string}`;\n /** 64-char lowercase hex HMAC secret, minted server-side at provision; never logged */\n hmacSecret: string;\n};\n\nexport type HmacHeaders = {\n \"X-KH-Sub-Org\": string;\n \"X-KH-Timestamp\": string;\n \"X-KH-Signature\": string;\n};\n\nexport type HookDecision = {\n decision: \"allow\" | \"deny\" | \"ask\";\n reason?: string;\n};\n\nexport class KeeperHubError extends Error {\n readonly code: string;\n\n constructor(code: string, message: string) {\n super(message);\n this.name = \"KeeperHubError\";\n this.code = code;\n }\n}\n\n/** Protocol preference for a single pay() or fetch() call. \"auto\" preserves\n * the x402-first default when both challenges are offered. */\nexport type PaymentHint = \"x402\" | \"mpp\" | \"auto\";\n\nexport class WalletConfigMissingError extends Error {\n constructor() {\n super(\n \"Wallet config not found at ~/.keeperhub/wallet.json. Run `npx @keeperhub/wallet add` to provision.\"\n );\n this.name = \"WalletConfigMissingError\";\n }\n}\n"],"mappings":";AAoBA,SAAS,eAAe;;;ACHxB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAEK;;;ACbP,SAAS,mBAAmB;AAE5B,SAAS,YAAY;AAEd,IAAM,QAAQ,YAAY;AAAA,EAC/B,IAAI;AAAA,EACJ,MAAM;AAAA,EACN,gBAAgB,EAAE,UAAU,IAAI,MAAM,SAAS,QAAQ,MAAM;AAAA,EAC7D,SAAS;AAAA,IACP,SAAS;AAAA,MACP,MAAM,CAAC,QAAQ,IAAI,iBAAiB,uBAAuB;AAAA,IAC7D;AAAA,EACF;AAAA,EACA,gBAAgB;AAAA,IACd,SAAS,EAAE,MAAM,kBAAkB,KAAK,6BAA6B;AAAA,EACvE;AACF,CAAC;AAGM,IAAM,YAAY;AAGlB,IAAM,eACX;;;ADLF,IAAM,gBAAgB;AA+BtB,eAAsB,aACpB,QACA,OAA4B,CAAC,GACH;AAC1B,QAAM,aACJ,KAAK,cACJ,mBAAmB;AAAA,IAClB,OAAO;AAAA,IACP,WAAW,KAAK;AAAA,EAClB,CAAC;AACH,QAAM,cACJ,KAAK,eACJ,mBAAmB;AAAA,IAClB,OAAO;AAAA,IACP,WAAW,KAAK;AAAA,EAClB,CAAC;AAIH,QAAM,CAAC,SAAS,QAAQ,IAAI,MAAM,QAAQ,IAAI;AAAA,IAC5C,WAAW,aAAa;AAAA,MACtB,SAAS;AAAA,MACT,KAAK;AAAA,MACL,cAAc;AAAA,MACd,MAAM,CAAC,OAAO,aAAa;AAAA,IAC7B,CAAC;AAAA,IACD,YAAY,aAAa;AAAA,MACvB,SAAS;AAAA,MACT,KAAK;AAAA,MACL,cAAc;AAAA,MACd,MAAM,CAAC,OAAO,aAAa;AAAA,IAC7B,CAAC;AAAA,EACH,CAAC;AAED,SAAO;AAAA,IACL,MAAM;AAAA,MACJ,OAAO;AAAA,MACP,OAAO;AAAA,MACP,QAAQ,YAAY,SAAS,aAAa;AAAA,MAC1C,SAAS,OAAO;AAAA,IAClB;AAAA,IACA,OAAO;AAAA,MACL,OAAO;AAAA,MACP,OAAO;AAAA,MACP,QAAQ,YAAY,UAAU,aAAa;AAAA,MAC3C,SAAS,OAAO;AAAA,IAClB;AAAA,EACF;AACF;;;AEhFA,IAAM,iBAAiB;AAIvB,IAAM,gBAAgB;AACtB,IAAM,gBAAgB;AAaf,SAAS,KAAK,eAAyC;AAC5D,MAAI,CAAC,eAAe,KAAK,aAAa,GAAG;AACvC,UAAM,IAAI,MAAM,+BAA+B,aAAa,EAAE;AAAA,EAChE;AAKA,QAAM,SAAS,IAAI,gBAAgB;AAAA,IACjC,gBAAgB;AAAA,IAChB,cAAc;AAAA,IACd,WAAW,KAAK,UAAU,EAAE,CAAC,aAAa,GAAG,CAAC,MAAM,EAAE,CAAC;AAAA,IACvD,oBAAoB;AAAA,EACtB,CAAC;AAED,QAAM,oBAAoB,WAAW,aAAa,GAAG,aAAa,IAAI,OAAO,SAAS,CAAC;AAEvF,QAAM,aACJ;AAMF,SAAO;AAAA,IACL;AAAA,IACA,cAAc;AAAA,IACd;AAAA,EACF;AACF;;;ACxDA,SAAS,OAAO,UAAU,OAAO,UAAU,iBAAiB;AAC5D,SAAS,WAAAA,UAAS,QAAAC,aAAY;AAC9B,SAAS,qBAAqB;;;ACT9B,SAAS,kBAAkB;AAC3B,SAAS,eAAe;AACxB,SAAS,SAAS,YAAY;AAkB9B,IAAM,cAAoC;AAAA,EACxC;AAAA,IACE,OAAO;AAAA,IACP,WAAW,CAAC,WAAW,QAAQ;AAAA,IAC/B,aAAa,CAAC,WAAW,eAAe;AAAA,IACxC,aAAa;AAAA,EACf;AAAA,EACA;AAAA,IACE,OAAO;AAAA,IACP,WAAW,CAAC,WAAW,QAAQ;AAAA,IAC/B,aAAa,CAAC,WAAW,eAAe;AAAA,IACxC,aAAa;AAAA,EACf;AAAA,EACA;AAAA,IACE,OAAO;AAAA,IACP,WAAW,CAAC,UAAU,QAAQ;AAAA,IAC9B,aAAa,CAAC,UAAU,eAAe;AAAA,IACvC,aAAa;AAAA,EACf;AAAA,EACA;AAAA,IACE,OAAO;AAAA,IACP,WAAW,CAAC,aAAa,QAAQ;AAAA,IACjC,aAAa,CAAC,aAAa,eAAe;AAAA,IAC1C,aAAa;AAAA,EACf;AAAA,EACA;AAAA,IACE,OAAO;AAAA,IACP,WAAW,CAAC,WAAW,YAAY,QAAQ;AAAA,IAC3C,aAAa,CAAC,WAAW,YAAY,eAAe;AAAA,IACpD,aAAa;AAAA,EACf;AACF;AAEO,SAAS,aAAa,cAAsC;AACjE,QAAM,OAAO,gBAAgB,QAAQ;AACrC,QAAM,UAAyB,CAAC;AAChC,aAAW,QAAQ,aAAa;AAC9B,UAAM,YAAY,KAAK,MAAM,GAAG,KAAK,SAAS;AAC9C,UAAM,eAAe,KAAK,MAAM,GAAG,KAAK,WAAW;AAGnD,QAAI,WAAW,QAAQ,SAAS,CAAC,GAAG;AAClC,cAAQ,KAAK;AAAA,QACX,OAAO,KAAK;AAAA,QACZ;AAAA,QACA;AAAA,QACA,aAAa,KAAK;AAAA,MACpB,CAAC;AAAA,IACH;AAAA,EACF;AACA,SAAO;AACT;;;AD3DA,IAAM,eAAe;AAGrB,IAAM,wBAAwB;AAkC9B,SAAS,sBAAuC;AAC9C,SAAO;AAAA,IACL,SAAS;AAAA,IACT,OAAO,CAAC,EAAE,MAAM,WAAW,SAAS,aAAa,CAAC;AAAA,EACpD;AACF;AAEA,SAAS,4BAAoC;AAM3C,QAAM,OAAOC,SAAQ,cAAc,YAAY,GAAG,CAAC;AACnD,SAAOC,MAAK,MAAM,MAAM,SAAS,2BAA2B;AAC9D;AAEA,SAAS,cAAc,KAAmB;AACxC,UAAQ,OAAO,MAAM,GAAG,GAAG;AAAA,CAAI;AACjC;AAEA,eAAsB,uBACpB,cACe;AACf,MAAI,MAAqB;AACzB,MAAI;AACF,UAAM,MAAM,SAAS,cAAc,OAAO;AAAA,EAC5C,SAAS,KAAK;AACZ,QAAK,IAA8B,SAAS,UAAU;AACpD,YAAM;AAAA,IACR;AAAA,EACF;AAEA,MAAI,SAAyB,CAAC;AAC9B,MAAI,QAAQ,MAAM;AAChB,QAAI;AACF,eAAS,KAAK,MAAM,GAAG;AAAA,IACzB,QAAQ;AACN,YAAM,IAAI;AAAA,QACR,oBAAoB,YAAY;AAAA,MAClC;AAAA,IACF;AAAA,EACF;AAEA,QAAM,QACJ,OAAO,OAAO,UAAU,YAAY,OAAO,UAAU,OAChD,OAAO,QACR,CAAC;AAEP,QAAM,qBAAqB,MAAM,QAAQ,MAAM,UAAU,IACpD,MAAM,aACP,CAAC;AAKL,QAAM,WAAsB,CAAC;AAC7B,aAAW,SAAS,oBAAoB;AACtC,UAAM,aAAa,KAAK,UAAU,KAAK;AACvC,QAAI,CAAC,WAAW,SAAS,qBAAqB,GAAG;AAC/C,eAAS,KAAK,KAAK;AAAA,IACrB;AAAA,EACF;AACA,WAAS,KAAK,oBAAoB,CAAC;AAEnC,QAAM,aAAa;AACnB,SAAO,QAAQ;AAEf,QAAM,MAAMD,SAAQ,YAAY,GAAG,EAAE,WAAW,MAAM,MAAM,IAAM,CAAC;AACnE,QAAM,UAAU,GAAG,KAAK,UAAU,QAAQ,MAAM,CAAC,CAAC;AAAA;AAClD,QAAM,UAAU,cAAc,SAAS,EAAE,MAAM,IAAM,CAAC;AAEtD,QAAM,MAAM,cAAc,GAAK;AACjC;AAEA,eAAe,kBACb,OACA,aACyE;AACzE,QAAM,MAAM,MAAM,WAAW,EAAE,WAAW,MAAM,MAAM,IAAM,CAAC;AAC7D,QAAM,SAASC,MAAK,MAAM,WAAW,2BAA2B;AAChE,QAAM,SAAS,aAAa,MAAM;AAClC,QAAM,MAAM,QAAQ,GAAK;AACzB,SAAO,EAAE,OAAO,MAAM,OAAO,MAAM,QAAQ,QAAQ,UAAU;AAC/D;AAEA,SAAS,mBAAmB,OAA4B;AACtD,SAAO,GAAG,MAAM,KAAK,6DAA6D,YAAY,4BAA4B,MAAM,KAAK,uBAAuB,MAAM,YAAY;AAChL;AAEA,eAAsB,aACpB,UAA0B,CAAC,GACH;AACxB,QAAM,SAAS,aAAa,QAAQ,YAAY;AAChD,QAAM,cAAc,QAAQ,mBAAmB,0BAA0B;AACzE,QAAM,WAAW,QAAQ,YAAY;AAErC,QAAM,cAA4C,CAAC;AACnD,QAAM,oBAAwD,CAAC;AAE/D,aAAW,SAAS,QAAQ;AAC1B,UAAM,QAAQ,MAAM,kBAAkB,OAAO,WAAW;AACxD,gBAAY,KAAK,KAAK;AAEtB,QAAI,MAAM,gBAAgB,eAAe;AACvC,YAAM,uBAAuB,MAAM,YAAY;AAC/C,wBAAkB,KAAK;AAAA,QACrB,OAAO,MAAM;AAAA,QACb,QAAQ;AAAA,MACV,CAAC;AAAA,IACH,OAAO;AACL,YAAM,UAAU,mBAAmB,KAAK;AACxC,wBAAkB,KAAK;AAAA,QACrB,OAAO,MAAM;AAAA,QACb,QAAQ;AAAA,QACR;AAAA,MACF,CAAC;AACD,eAAS,OAAO;AAAA,IAClB;AAAA,EACF;AAEA,SAAO,EAAE,aAAa,kBAAkB;AAC1C;;;AEtLA,SAAS,SAAAC,QAAO,SAAAC,QAAO,YAAAC,WAAU,aAAAC,kBAAiB;AAClD,SAAS,WAAAC,gBAAe;AACxB,SAAS,WAAAC,UAAS,QAAAC,aAAY;;;ACiCvB,IAAM,2BAAN,cAAuC,MAAM;AAAA,EAClD,cAAc;AACZ;AAAA,MACE;AAAA,IACF;AACA,SAAK,OAAO;AAAA,EACd;AACF;;;AD9BA,eAAsB,mBAA0C;AAC9D,QAAM,aAAaC,MAAKC,SAAQ,GAAG,cAAc,aAAa;AAC9D,MAAI;AACJ,MAAI;AACF,UAAM,MAAMC,UAAS,YAAY,OAAO;AAAA,EAC1C,SAAS,KAAK;AACZ,QAAK,IAA8B,SAAS,UAAU;AACpD,YAAM,IAAI,yBAAyB;AAAA,IACrC;AACA,UAAM;AAAA,EACR;AACA,QAAM,SAAS,KAAK,MAAM,GAAG;AAC7B,MAAI,EAAE,OAAO,YAAY,OAAO,iBAAiB,OAAO,aAAa;AACnE,UAAM,IAAI,MAAM,4BAA4B,UAAU,EAAE;AAAA,EAC1D;AACA,SAAO;AACT;AAEA,eAAsB,kBAAkB,QAAqC;AAC3E,QAAM,aAAaF,MAAKC,SAAQ,GAAG,cAAc,aAAa;AAC9D,QAAME,OAAMC,SAAQ,UAAU,GAAG,EAAE,WAAW,MAAM,MAAM,IAAM,CAAC;AACjE,QAAMC,WAAU,YAAY,KAAK,UAAU,QAAQ,MAAM,CAAC,GAAG,EAAE,MAAM,IAAM,CAAC;AAE5E,QAAMC,OAAM,YAAY,GAAK;AAC/B;AAEO,SAAS,sBAA8B;AAC5C,SAAON,MAAKC,SAAQ,GAAG,cAAc,aAAa;AACpD;;;ANTA,IAAM,iBAAiB;AACvB,IAAM,yBAAyB;AAE/B,SAAS,eAAe,UAAsC;AAC5D,QAAM,YACJ,YAAY,QAAQ,IAAI,qBAAqB;AAC/C,SAAO,UAAU,QAAQ,gBAAgB,EAAE;AAC7C;AAEA,SAAS,iBAAiB,OAAiC;AACzD,SAAO,OAAO,UAAU,YAAY,MAAM,SAAS;AACrD;AAEA,SAAS,sBACP,SACgD;AAChD,QAAM,MAAM,IAAI,MAAM,OAAO;AAG7B,MAAI,OAAO;AACX,SAAO;AACT;AAEA,SAAS,0BAA0B,MAIjC;AACA,MAAI,OAAO,SAAS,YAAY,SAAS,MAAM;AAC7C,UAAM,sBAAsB,qCAAqC;AAAA,EACnE;AACA,QAAM,EAAE,UAAU,eAAe,WAAW,IAAI;AAIhD,MACE,EACE,iBAAiB,QAAQ,KACzB,iBAAiB,aAAa,KAC9B,iBAAiB,UAAU,IAE7B;AACA,UAAM;AAAA,MACJ;AAAA,IACF;AAAA,EACF;AACA,MAAI,CAAC,uBAAuB,KAAK,aAAa,GAAG;AAC/C,UAAM;AAAA,MACJ,+EAA+E,aAAa;AAAA,IAC9F;AAAA,EACF;AACA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAEA,eAAe,OAAO,OAA6B,CAAC,GAAkB;AACpE,QAAM,UAAU,eAAe,KAAK,OAAO;AAC3C,QAAM,WAAW,MAAM,MAAM,GAAG,OAAO,iCAAiC;AAAA,IACtE,QAAQ;AAAA,IACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,IAC9C,MAAM;AAAA,EACR,CAAC;AACD,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,OAAO,MAAM,SAAS,KAAK;AACjC,YAAQ,OAAO;AAAA,MACb,6CAA6C,SAAS,MAAM,KAAK,IAAI;AAAA;AAAA,IACvE;AACA,YAAQ,KAAK,CAAC;AAAA,EAChB;AACA,QAAM,MAAO,MAAM,SAAS,KAAK;AACjC,QAAM,OAAO,0BAA0B,GAAG;AAC1C,QAAM,kBAAkB;AAAA,IACtB,UAAU,KAAK;AAAA,IACf,eAAe,KAAK;AAAA,IACpB,YAAY,KAAK;AAAA,EACnB,CAAC;AAGD,UAAQ,OAAO,MAAM,aAAa,KAAK,QAAQ;AAAA,CAAI;AACnD,UAAQ,OAAO,MAAM,kBAAkB,KAAK,aAAa;AAAA,CAAI;AAC7D,UAAQ,OAAO,MAAM,qBAAqB,oBAAoB,CAAC;AAAA,CAAI;AACrE;AAEA,eAAe,UAAyB;AACtC,QAAM,SAAS,MAAM,iBAAiB;AACtC,QAAM,MAAM,KAAK,OAAO,aAAa;AACrC,UAAQ,OAAO,MAAM,GAAG,IAAI,iBAAiB;AAAA,CAAI;AACjD,UAAQ,OAAO,MAAM,kBAAkB,IAAI,YAAY;AAAA,CAAI;AAC3D,UAAQ,OAAO,MAAM,GAAG,IAAI,UAAU;AAAA,CAAI;AAC5C;AAEA,eAAe,aAA4B;AACzC,QAAM,SAAS,MAAM,iBAAiB;AACtC,QAAM,OAAO,MAAM,aAAa,MAAM;AACtC,UAAQ,OAAO,MAAM,iBAAiB,KAAK,KAAK,MAAM;AAAA,CAAI;AAC1D,UAAQ,OAAO,MAAM,iBAAiB,KAAK,MAAM,MAAM;AAAA,CAAI;AAC7D;AAEA,eAAe,UAAyB;AACtC,QAAM,SAAS,MAAM,iBAAiB;AACtC,UAAQ,OAAO,MAAM,aAAa,OAAO,QAAQ;AAAA,CAAI;AACrD,UAAQ,OAAO,MAAM,kBAAkB,OAAO,aAAa;AAAA,CAAI;AACjE;AAEA,eAAsB,OAAO,OAAiB,QAAQ,MAAqB;AACzE,QAAM,UAAU,IAAI,QAAQ;AAC5B,UACG,KAAK,kBAAkB,EACvB;AAAA,IACC;AAAA,EACF,EACC,QAAQ,OAAO;AAElB,UACG,QAAQ,KAAK,EACb,YAAY,sDAAsD,EAClE,OAAO,oBAAoB,wBAAwB,EACnD,OAAO,OAAO,SAA+B;AAC5C,UAAM,OAAO,IAAI;AAAA,EACnB,CAAC;AAEH,UACG,QAAQ,MAAM,EACd;AAAA,IACC;AAAA,EACF,EACC,OAAO,YAAY;AAClB,UAAM,QAAQ;AAAA,EAChB,CAAC;AAEH,UACG,QAAQ,SAAS,EACjB,YAAY,kDAAkD,EAC9D,OAAO,YAAY;AAClB,UAAM,WAAW;AAAA,EACnB,CAAC;AAEH,UACG,QAAQ,MAAM,EACd,YAAY,oDAAoD,EAChE,OAAO,YAAY;AAClB,UAAM,QAAQ;AAAA,EAChB,CAAC;AAEH,UACG,QAAQ,OAAO,EACf;AAAA,IACC;AAAA,EACF,EACC;AAAA,IACC,IAAI,QAAQ,SAAS,EAClB;AAAA,MACC;AAAA,IACF,EACC,OAAO,YAAY;AAClB,YAAM,SAAS,MAAM,aAAa;AAClC,iBAAW,SAAS,OAAO,aAAa;AACtC,gBAAQ,OAAO;AAAA,UACb,UAAU,MAAM,KAAK,OAAO,MAAM,IAAI,KAAK,MAAM,MAAM;AAAA;AAAA,QACzD;AAAA,MACF;AACA,iBAAW,OAAO,OAAO,mBAAmB;AAC1C,YAAI,IAAI,WAAW,cAAc;AAC/B,kBAAQ,OAAO;AAAA,YACb,SAAS,IAAI,KAAK;AAAA;AAAA,UACpB;AAAA,QACF,WAAW,IAAI,WAAW,UAAU;AAClC,kBAAQ,OAAO;AAAA,YACb,WAAW,IAAI,KAAK,OAAO,IAAI,WAAW,EAAE;AAAA;AAAA,UAC9C;AAAA,QACF;AAAA,MACF;AACA,UAAI,OAAO,YAAY,WAAW,GAAG;AACnC,gBAAQ,OAAO;AAAA,UACb;AAAA,QACF;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACL;AAEF,MAAI;AACF,UAAM,QAAQ,WAAW,IAAI;AAAA,EAC/B,SAAS,KAAK;AACZ,QAAI,eAAe,0BAA0B;AAC3C,cAAQ,OAAO,MAAM,sBAAsB,IAAI,OAAO;AAAA,CAAI;AAC1D,cAAQ,KAAK,CAAC;AAAA,IAChB;AACA,YAAQ,OAAO;AAAA,MACb,sBAAuB,IAAc,WAAW,OAAO,GAAG,CAAC;AAAA;AAAA,IAC7D;AACA,YAAQ,KAAK,CAAC;AAAA,EAChB;AACF;","names":["dirname","join","dirname","join","chmod","mkdir","readFile","writeFile","homedir","dirname","join","join","homedir","readFile","mkdir","dirname","writeFile","chmod"]}
|
package/dist/index.cjs
CHANGED
|
@@ -45,6 +45,7 @@ __export(index_exports, {
|
|
|
45
45
|
registerClaudeCodeHook: () => registerClaudeCodeHook,
|
|
46
46
|
runCli: () => runCli,
|
|
47
47
|
runHookCli: () => runHookCli,
|
|
48
|
+
selectProtocol: () => selectProtocol,
|
|
48
49
|
tempo: () => tempo,
|
|
49
50
|
validateAndMerge: () => validateAndMerge,
|
|
50
51
|
writeWalletConfig: () => writeWalletConfig
|
|
@@ -866,6 +867,30 @@ var NONCE_BYTES = 32;
|
|
|
866
867
|
async function sleep(ms) {
|
|
867
868
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
868
869
|
}
|
|
870
|
+
function selectProtocol(x402, mpp, hint) {
|
|
871
|
+
const h = hint ?? "auto";
|
|
872
|
+
if (h === "x402") {
|
|
873
|
+
if (!x402) {
|
|
874
|
+
throw new KeeperHubError(
|
|
875
|
+
"X402_NOT_OFFERED",
|
|
876
|
+
"x402 is not offered by this endpoint"
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
return "x402";
|
|
880
|
+
}
|
|
881
|
+
if (h === "mpp") {
|
|
882
|
+
if (!mpp) {
|
|
883
|
+
throw new KeeperHubError(
|
|
884
|
+
"MPP_NOT_OFFERED",
|
|
885
|
+
"mpp is not offered by this endpoint"
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
return "mpp";
|
|
889
|
+
}
|
|
890
|
+
if (x402) return "x402";
|
|
891
|
+
if (mpp) return "mpp";
|
|
892
|
+
return null;
|
|
893
|
+
}
|
|
869
894
|
function createPaymentSigner(opts = {}) {
|
|
870
895
|
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
871
896
|
const walletLoader = opts.walletLoader ?? readWalletConfig;
|
|
@@ -972,16 +997,18 @@ function createPaymentSigner(opts = {}) {
|
|
|
972
997
|
}
|
|
973
998
|
});
|
|
974
999
|
const paymentSigPayload = {
|
|
1000
|
+
x402Version: 2,
|
|
1001
|
+
accepted: accept,
|
|
975
1002
|
payload: {
|
|
1003
|
+
signature,
|
|
976
1004
|
authorization: {
|
|
977
1005
|
from: wallet.walletAddress,
|
|
978
1006
|
to: accept.payTo,
|
|
979
1007
|
value: accept.amount,
|
|
980
|
-
validAfter,
|
|
981
|
-
validBefore,
|
|
1008
|
+
validAfter: String(validAfter),
|
|
1009
|
+
validBefore: String(validBefore),
|
|
982
1010
|
nonce
|
|
983
|
-
}
|
|
984
|
-
signature
|
|
1011
|
+
}
|
|
985
1012
|
}
|
|
986
1013
|
};
|
|
987
1014
|
const paymentSigHeader = Buffer.from(
|
|
@@ -1006,12 +1033,13 @@ function createPaymentSigner(opts = {}) {
|
|
|
1006
1033
|
return response;
|
|
1007
1034
|
}
|
|
1008
1035
|
const wallet = await walletLoader();
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
}
|
|
1012
|
-
if (x402) {
|
|
1036
|
+
const protocol = selectProtocol(x402, mpp, options?.paymentHint);
|
|
1037
|
+
if (protocol === "x402") {
|
|
1013
1038
|
return payViaX402(response, x402, wallet, options);
|
|
1014
1039
|
}
|
|
1040
|
+
if (protocol === "mpp") {
|
|
1041
|
+
return payViaMpp(response, mpp, wallet, options);
|
|
1042
|
+
}
|
|
1015
1043
|
return response;
|
|
1016
1044
|
}
|
|
1017
1045
|
return {
|
|
@@ -1024,7 +1052,8 @@ function createPaymentSigner(opts = {}) {
|
|
|
1024
1052
|
return pay(first, {
|
|
1025
1053
|
body: init?.body ?? void 0,
|
|
1026
1054
|
headers: init?.headers,
|
|
1027
|
-
method: init?.method
|
|
1055
|
+
method: init?.method,
|
|
1056
|
+
paymentHint: init?.paymentHint
|
|
1028
1057
|
});
|
|
1029
1058
|
}
|
|
1030
1059
|
};
|
|
@@ -1057,6 +1086,7 @@ var paymentSigner = createPaymentSigner();
|
|
|
1057
1086
|
registerClaudeCodeHook,
|
|
1058
1087
|
runCli,
|
|
1059
1088
|
runHookCli,
|
|
1089
|
+
selectProtocol,
|
|
1060
1090
|
tempo,
|
|
1061
1091
|
validateAndMerge,
|
|
1062
1092
|
writeWalletConfig
|