whale-code 6.5.8 → 6.5.9

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.
Files changed (99) hide show
  1. package/dist/cli/services/agent-loop.js +26 -2
  2. package/dist/cli/services/agent-loop.js.map +1 -1
  3. package/dist/cli/services/hooks.js +2 -1
  4. package/dist/cli/services/hooks.js.map +1 -1
  5. package/dist/cli/services/telemetry-spans.js +1 -0
  6. package/dist/cli/services/telemetry-spans.js.map +1 -1
  7. package/dist/cli/services/telemetry.d.ts +23 -0
  8. package/dist/cli/services/telemetry.js +45 -1
  9. package/dist/cli/services/telemetry.js.map +1 -1
  10. package/dist/server/handlers/__test-utils__/test-db.d.ts +17 -3
  11. package/dist/server/handlers/__test-utils__/test-db.js +113 -14
  12. package/dist/server/handlers/__test-utils__/test-db.js.map +1 -1
  13. package/dist/server/handlers/affiliates.d.ts +9 -0
  14. package/dist/server/handlers/affiliates.js +197 -0
  15. package/dist/server/handlers/affiliates.js.map +1 -0
  16. package/dist/server/handlers/api-docs.d.ts +4 -2
  17. package/dist/server/handlers/api-docs.js +204 -1681
  18. package/dist/server/handlers/api-docs.js.map +1 -1
  19. package/dist/server/handlers/campaigns.d.ts +9 -0
  20. package/dist/server/handlers/campaigns.js +237 -0
  21. package/dist/server/handlers/campaigns.js.map +1 -0
  22. package/dist/server/handlers/catalog-schemas.js +9 -9
  23. package/dist/server/handlers/catalog-schemas.js.map +1 -1
  24. package/dist/server/handlers/catalog.js +1 -1
  25. package/dist/server/handlers/catalog.js.map +1 -1
  26. package/dist/server/handlers/comms-documents.js +28 -2
  27. package/dist/server/handlers/comms-documents.js.map +1 -1
  28. package/dist/server/handlers/comms-pdf-generation.js +25 -3
  29. package/dist/server/handlers/comms-pdf-generation.js.map +1 -1
  30. package/dist/server/handlers/comms-pdf-helpers.js +4 -4
  31. package/dist/server/handlers/comms-pdf-helpers.js.map +1 -1
  32. package/dist/server/handlers/comms.d.ts +100 -0
  33. package/dist/server/handlers/comms.js +146 -12
  34. package/dist/server/handlers/comms.js.map +1 -1
  35. package/dist/server/handlers/coupons.d.ts +9 -0
  36. package/dist/server/handlers/coupons.js +220 -0
  37. package/dist/server/handlers/coupons.js.map +1 -0
  38. package/dist/server/handlers/embeddings.js +1 -1
  39. package/dist/server/handlers/embeddings.js.map +1 -1
  40. package/dist/server/handlers/enrichment.js +2 -622
  41. package/dist/server/handlers/enrichment.js.map +1 -1
  42. package/dist/server/handlers/fulfillment.d.ts +9 -0
  43. package/dist/server/handlers/fulfillment.js +209 -0
  44. package/dist/server/handlers/fulfillment.js.map +1 -0
  45. package/dist/server/handlers/google-ads.d.ts +24 -0
  46. package/dist/server/handlers/google-ads.js +2199 -0
  47. package/dist/server/handlers/google-ads.js.map +1 -0
  48. package/dist/server/handlers/invoices.d.ts +9 -0
  49. package/dist/server/handlers/invoices.js +252 -0
  50. package/dist/server/handlers/invoices.js.map +1 -0
  51. package/dist/server/handlers/loyalty.d.ts +9 -0
  52. package/dist/server/handlers/loyalty.js +197 -0
  53. package/dist/server/handlers/loyalty.js.map +1 -0
  54. package/dist/server/handlers/meta-ads-graph-api.js +18 -3
  55. package/dist/server/handlers/meta-ads-graph-api.js.map +1 -1
  56. package/dist/server/handlers/phone.d.ts +9 -0
  57. package/dist/server/handlers/phone.js +197 -0
  58. package/dist/server/handlers/phone.js.map +1 -0
  59. package/dist/server/handlers/pipeline.d.ts +9 -0
  60. package/dist/server/handlers/pipeline.js +277 -0
  61. package/dist/server/handlers/pipeline.js.map +1 -0
  62. package/dist/server/handlers/qr-codes.d.ts +9 -0
  63. package/dist/server/handlers/qr-codes.js +198 -0
  64. package/dist/server/handlers/qr-codes.js.map +1 -0
  65. package/dist/server/handlers/reviews.d.ts +9 -0
  66. package/dist/server/handlers/reviews.js +171 -0
  67. package/dist/server/handlers/reviews.js.map +1 -0
  68. package/dist/server/handlers/segments.d.ts +9 -0
  69. package/dist/server/handlers/segments.js +229 -0
  70. package/dist/server/handlers/segments.js.map +1 -0
  71. package/dist/server/handlers/social.d.ts +9 -0
  72. package/dist/server/handlers/social.js +81 -0
  73. package/dist/server/handlers/social.js.map +1 -0
  74. package/dist/server/handlers/tax.d.ts +9 -0
  75. package/dist/server/handlers/tax.js +182 -0
  76. package/dist/server/handlers/tax.js.map +1 -0
  77. package/dist/server/handlers/wallet.d.ts +9 -0
  78. package/dist/server/handlers/wallet.js +203 -0
  79. package/dist/server/handlers/wallet.js.map +1 -0
  80. package/dist/server/handlers/webhooks-mgmt.d.ts +9 -0
  81. package/dist/server/handlers/webhooks-mgmt.js +181 -0
  82. package/dist/server/handlers/webhooks-mgmt.js.map +1 -0
  83. package/dist/server/handlers/wholesale.d.ts +9 -0
  84. package/dist/server/handlers/wholesale.js +219 -0
  85. package/dist/server/handlers/wholesale.js.map +1 -0
  86. package/dist/server/index.js +20 -9
  87. package/dist/server/index.js.map +1 -1
  88. package/dist/server/lib/clickhouse-buffer.js +1 -0
  89. package/dist/server/lib/clickhouse-buffer.js.map +1 -1
  90. package/dist/server/lib/coa-renderer.d.ts +1 -1
  91. package/dist/server/lib/coa-renderer.js +32 -10
  92. package/dist/server/lib/coa-renderer.js.map +1 -1
  93. package/dist/server/server-worker.d.ts +1 -0
  94. package/dist/server/server-worker.js +464 -3
  95. package/dist/server/server-worker.js.map +1 -1
  96. package/dist/server/tool-router.js +118 -4
  97. package/dist/server/tool-router.js.map +1 -1
  98. package/package.json +26 -3
  99. package/vendor/ink/package.json +0 -2
@@ -1 +1 @@
1
- {"version":3,"file":"hooks.js","names":["existsSync","readFileSync","join","spawn","HOOKS_PATH","DEFAULT_TIMEOUT_MS","matchGlob","pattern","name","regex","replace","RegExp","test","VALID_EVENTS","loadHooks","cwd","hooks","projectPath","projectHooks","loadHooksFromFile","push","userPath","userHooks","filePath","raw","parsed","JSON","parse","Array","isArray","console","error","filter","h","hook","event","command","includes","err","Error","message","executeHook","payload","timeout","Promise","resolve","stdout","stderr","settled","child","stdio","env","process","timer","settle","result","clearTimeout","setTimeout","kill","on","data","toString","code","trim","trimmed","stdin","write","stringify","end","runBeforeToolHook","toolName","input","matching","length","allow","tool_name","tool_input","response","allowModify","modified_input","modifiedInput","runAfterToolHook","output","success","tool_output","tool_success","modified_output","undefined","modifiedOutput","runSessionHook","allSettled","map"],"sources":["../../../src/cli/services/hooks.ts"],"sourcesContent":["/**\n * Hooks System — shell commands that run before/after tool calls and at session lifecycle events.\n *\n * Hooks are configured in:\n * - Project: .whale/hooks.json (array of HookConfig)\n * - User: ~/.whale/hooks.json (array of HookConfig)\n *\n * Both are loaded and merged (project hooks run first).\n *\n * Hook process receives JSON on stdin:\n * { event, tool_name?, tool_input?, tool_output?, tool_success?, session_id? }\n *\n * Hook process may output JSON on stdout:\n * { allow?: boolean, message?: string, modified_input?: object, modified_output?: string }\n *\n * If allow: false, the tool call is blocked and message is returned as the tool result.\n * If hook exits non-zero, it is logged as a warning but does not crash the session.\n */\n\nimport { existsSync, readFileSync } from \"fs\";\nimport { join } from \"path\";\nimport { spawn } from \"child_process\";\nimport { HOOKS_PATH } from \"./paths.js\";\n\n// ============================================================================\n// TYPES\n// ============================================================================\n\nexport type HookEvent =\n | \"BeforeTool\"\n | \"AfterTool\"\n | \"SessionStart\"\n | \"SessionEnd\"\n | \"Notification\";\n\nexport interface HookConfig {\n /** Which event triggers this hook */\n event: HookEvent;\n /** Shell command to run */\n command: string;\n /** Optional: only trigger for matching tool names (glob pattern) */\n pattern?: string;\n /** Max ms to wait for hook process (default 10000) */\n timeout?: number;\n /** Can hook modify the tool input/output? (default false) */\n allowModify?: boolean;\n}\n\n/** JSON sent to hook process on stdin */\ninterface HookPayload {\n event: HookEvent;\n tool_name?: string;\n tool_input?: Record<string, unknown>;\n tool_output?: string;\n tool_success?: boolean;\n session_id?: string;\n}\n\n/** JSON output from hook process on stdout */\ninterface HookResponse {\n allow?: boolean;\n message?: string;\n modified_input?: Record<string, unknown>;\n modified_output?: string;\n}\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\n\n// ============================================================================\n// GLOB MATCHING\n// ============================================================================\n\n/**\n * Match a tool name against a simple glob pattern.\n * Supports * (any chars) and ? (single char).\n */\nexport function matchGlob(pattern: string, name: string): boolean {\n // Escape regex special chars except * and ?\n const regex = pattern\n .replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\")\n .replace(/\\*/g, \".*\")\n .replace(/\\?/g, \".\");\n return new RegExp(`^${regex}$`).test(name);\n}\n\n// ============================================================================\n// LOAD HOOKS\n// ============================================================================\n\nconst VALID_EVENTS: HookEvent[] = [\n \"BeforeTool\",\n \"AfterTool\",\n \"SessionStart\",\n \"SessionEnd\",\n \"Notification\",\n];\n\n/**\n * Load hooks from project (.whale/hooks.json) and user (~/.whale/hooks.json).\n * Project hooks come first in the array, then user hooks.\n * Invalid files are skipped with a warning.\n */\nexport function loadHooks(cwd: string): HookConfig[] {\n const hooks: HookConfig[] = [];\n\n // Project hooks (higher priority, run first)\n const projectPath = join(cwd, \".whale\", \"hooks.json\");\n const projectHooks = loadHooksFromFile(projectPath);\n if (projectHooks) hooks.push(...projectHooks);\n\n // User hooks\n const userPath = HOOKS_PATH;\n const userHooks = loadHooksFromFile(userPath);\n if (userHooks) hooks.push(...userHooks);\n\n return hooks;\n}\n\nfunction loadHooksFromFile(filePath: string): HookConfig[] | null {\n if (!existsSync(filePath)) return null;\n try {\n const raw = readFileSync(filePath, \"utf-8\");\n const parsed = JSON.parse(raw);\n if (!Array.isArray(parsed)) {\n console.error(`[hooks] Warning: ${filePath} should be a JSON array, skipping`);\n return null;\n }\n // Validate each hook config\n return parsed.filter((h: unknown) => {\n if (!h || typeof h !== \"object\") return false;\n const hook = h as Record<string, unknown>;\n if (typeof hook.event !== \"string\" || typeof hook.command !== \"string\") {\n console.error(`[hooks] Warning: Invalid hook in ${filePath}, missing event or command`);\n return false;\n }\n if (!VALID_EVENTS.includes(hook.event as HookEvent)) {\n console.error(`[hooks] Warning: Invalid event \"${hook.event}\" in ${filePath}`);\n return false;\n }\n return true;\n }) as HookConfig[];\n } catch (err) {\n console.error(\n `[hooks] Warning: Failed to parse ${filePath}: ${err instanceof Error ? err.message : err}`,\n );\n return null;\n }\n}\n\n// ============================================================================\n// EXECUTE HOOK\n// ============================================================================\n\n/**\n * Execute a hook command with JSON payload on stdin.\n * Returns parsed JSON response from stdout, or null on error/timeout.\n */\nexport async function executeHook(\n hook: HookConfig,\n payload: HookPayload,\n): Promise<HookResponse | null> {\n const timeout = hook.timeout ?? DEFAULT_TIMEOUT_MS;\n\n return new Promise((resolve) => {\n let stdout = \"\";\n let stderr = \"\";\n let settled = false;\n\n const child = spawn(\"/bin/sh\", [\"-c\", hook.command], {\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n env: { ...process.env },\n });\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n const settle = (result: HookResponse | null) => {\n if (settled) return;\n settled = true;\n if (timer) clearTimeout(timer);\n resolve(result);\n };\n\n // Kill on timeout\n timer = setTimeout(() => {\n if (!settled) {\n console.error(\n `[hooks] Warning: Hook \"${hook.command}\" timed out after ${timeout}ms, killing`,\n );\n try {\n child.kill(\"SIGKILL\");\n } catch {\n /* ignore */\n }\n settle(null);\n }\n }, timeout);\n\n child.stdout?.on(\"data\", (data: Buffer) => {\n stdout += data.toString();\n });\n\n child.stderr?.on(\"data\", (data: Buffer) => {\n stderr += data.toString();\n });\n\n child.on(\"error\", (err) => {\n console.error(\n `[hooks] Warning: Hook \"${hook.command}\" failed to start: ${err.message}`,\n );\n settle(null);\n });\n\n child.on(\"close\", (code) => {\n if (code !== 0) {\n if (stderr) {\n console.error(\n `[hooks] Warning: Hook \"${hook.command}\" exited with code ${code}: ${stderr.trim()}`,\n );\n } else {\n console.error(\n `[hooks] Warning: Hook \"${hook.command}\" exited with code ${code}`,\n );\n }\n settle(null);\n return;\n }\n\n // Parse stdout as JSON\n const trimmed = stdout.trim();\n if (!trimmed) {\n settle(null);\n return;\n }\n try {\n settle(JSON.parse(trimmed) as HookResponse);\n } catch {\n // Non-JSON output is not an error, just no structured response\n settle(null);\n }\n });\n\n // Write payload to stdin\n try {\n child.stdin?.write(JSON.stringify(payload));\n child.stdin?.end();\n } catch {\n // stdin may already be closed — that is fine\n }\n });\n}\n\n// ============================================================================\n// PUBLIC API\n// ============================================================================\n\n/**\n * Run BeforeTool hooks for a given tool call.\n * Returns whether the tool call should proceed and optionally modified input.\n */\nexport async function runBeforeToolHook(\n hooks: HookConfig[],\n toolName: string,\n input: Record<string, unknown>,\n): Promise<{\n allow: boolean;\n message?: string;\n modifiedInput?: Record<string, unknown>;\n}> {\n const matching = hooks.filter(\n (h) =>\n h.event === \"BeforeTool\" &&\n (!h.pattern || matchGlob(h.pattern, toolName)),\n );\n\n if (matching.length === 0) return { allow: true };\n\n const payload: HookPayload = {\n event: \"BeforeTool\",\n tool_name: toolName,\n tool_input: input,\n };\n\n for (const hook of matching) {\n const response = await executeHook(hook, payload);\n if (!response) continue;\n\n // If any hook blocks, stop immediately\n if (response.allow === false) {\n return {\n allow: false,\n message: response.message || `Blocked by hook: ${hook.command}`,\n };\n }\n\n // If hook modifies input and is allowed to\n if (hook.allowModify && response.modified_input) {\n return { allow: true, modifiedInput: response.modified_input };\n }\n }\n\n return { allow: true };\n}\n\n/**\n * Run AfterTool hooks for a given tool result.\n * Returns optionally modified output.\n */\nexport async function runAfterToolHook(\n hooks: HookConfig[],\n toolName: string,\n output: string,\n success: boolean,\n): Promise<{ modifiedOutput?: string }> {\n const matching = hooks.filter(\n (h) =>\n h.event === \"AfterTool\" &&\n (!h.pattern || matchGlob(h.pattern, toolName)),\n );\n\n if (matching.length === 0) return {};\n\n const payload: HookPayload = {\n event: \"AfterTool\",\n tool_name: toolName,\n tool_output: output,\n tool_success: success,\n };\n\n for (const hook of matching) {\n const response = await executeHook(hook, payload);\n if (!response) continue;\n\n // If hook modifies output and is allowed to\n if (hook.allowModify && response.modified_output !== undefined) {\n return { modifiedOutput: response.modified_output };\n }\n }\n\n return {};\n}\n\n/**\n * Run session lifecycle hooks (SessionStart, SessionEnd, Notification).\n * Fire-and-forget — errors are logged but don't affect session.\n */\nexport async function runSessionHook(\n hooks: HookConfig[],\n event: \"SessionStart\" | \"SessionEnd\" | \"Notification\",\n data?: Record<string, unknown>,\n): Promise<void> {\n const matching = hooks.filter((h) => h.event === event);\n\n if (matching.length === 0) return;\n\n const payload: HookPayload = {\n event,\n ...(data || {}),\n };\n\n // Run all session hooks concurrently — don't wait or fail\n await Promise.allSettled(matching.map((hook) => executeHook(hook, payload)));\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"hooks.js","names":["existsSync","readFileSync","join","spawn","HOOKS_PATH","DEFAULT_TIMEOUT_MS","matchGlob","pattern","name","regex","replace","RegExp","test","VALID_EVENTS","loadHooks","cwd","hooks","projectPath","projectHooks","loadHooksFromFile","push","userPath","userHooks","filePath","raw","parsed","JSON","parse","Array","isArray","console","error","filter","h","hook","event","command","includes","err","Error","message","executeHook","payload","timeout","Promise","resolve","stdout","stderr","settled","child","stdio","env","process","timer","settle","result","clearTimeout","setTimeout","kill","on","data","toString","code","trim","trimmed","stdin","write","stringify","end","runBeforeToolHook","toolName","input","matching","length","allow","tool_name","tool_input","response","allowModify","modified_input","modifiedInput","runAfterToolHook","output","success","tool_output","tool_success","modified_output","undefined","modifiedOutput","runSessionHook","allSettled","map"],"sources":["../../../src/cli/services/hooks.ts"],"sourcesContent":["/**\n * Hooks System — shell commands that run before/after tool calls and at session lifecycle events.\n *\n * Hooks are configured in:\n * - Project: .whale/hooks.json (array of HookConfig)\n * - User: ~/.whale/hooks.json (array of HookConfig)\n *\n * Both are loaded and merged (project hooks run first).\n *\n * Hook process receives JSON on stdin:\n * { event, tool_name?, tool_input?, tool_output?, tool_success?, session_id? }\n *\n * Hook process may output JSON on stdout:\n * { allow?: boolean, message?: string, modified_input?: object, modified_output?: string }\n *\n * If allow: false, the tool call is blocked and message is returned as the tool result.\n * If hook exits non-zero, it is logged as a warning but does not crash the session.\n */\n\nimport { existsSync, readFileSync } from \"fs\";\nimport { join } from \"path\";\nimport { spawn } from \"child_process\";\nimport { HOOKS_PATH } from \"./paths.js\";\n\n// ============================================================================\n// TYPES\n// ============================================================================\n\nexport type HookEvent =\n | \"BeforeTool\"\n | \"AfterTool\"\n | \"SessionStart\"\n | \"SessionEnd\"\n | \"Notification\";\n\nexport interface HookConfig {\n /** Which event triggers this hook */\n event: HookEvent;\n /** Shell command to run */\n command: string;\n /** Optional: only trigger for matching tool names (glob pattern) */\n pattern?: string;\n /** Max ms to wait for hook process (default 10000) */\n timeout?: number;\n /** Can hook modify the tool input/output? (default false) */\n allowModify?: boolean;\n}\n\n/** JSON sent to hook process on stdin */\ninterface HookPayload {\n event: HookEvent;\n tool_name?: string;\n tool_input?: Record<string, unknown>;\n tool_output?: string;\n tool_success?: boolean;\n session_id?: string;\n}\n\n/** JSON output from hook process on stdout */\ninterface HookResponse {\n allow?: boolean;\n message?: string;\n modified_input?: Record<string, unknown>;\n modified_output?: string;\n}\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\n\n// ============================================================================\n// GLOB MATCHING\n// ============================================================================\n\n/**\n * Match a tool name against a simple glob pattern.\n * Supports * (any chars) and ? (single char).\n */\nexport function matchGlob(pattern: string, name: string): boolean {\n // Escape regex special chars except * and ?\n const regex = pattern\n .replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\")\n .replace(/\\*/g, \".*\")\n .replace(/\\?/g, \".\");\n return new RegExp(`^${regex}$`).test(name);\n}\n\n// ============================================================================\n// LOAD HOOKS\n// ============================================================================\n\nconst VALID_EVENTS: HookEvent[] = [\n \"BeforeTool\",\n \"AfterTool\",\n \"SessionStart\",\n \"SessionEnd\",\n \"Notification\",\n];\n\n/**\n * Load hooks from project (.whale/hooks.json) and user (~/.whale/hooks.json).\n * Project hooks come first in the array, then user hooks.\n * Invalid files are skipped with a warning.\n */\nexport function loadHooks(cwd: string): HookConfig[] {\n const hooks: HookConfig[] = [];\n\n // Project hooks (higher priority, run first)\n const projectPath = join(cwd, \".whale\", \"hooks.json\");\n const projectHooks = loadHooksFromFile(projectPath);\n if (projectHooks) hooks.push(...projectHooks);\n\n // User hooks\n const userPath = HOOKS_PATH;\n const userHooks = loadHooksFromFile(userPath);\n if (userHooks) hooks.push(...userHooks);\n\n return hooks;\n}\n\nfunction loadHooksFromFile(filePath: string): HookConfig[] | null {\n if (!existsSync(filePath)) return null;\n try {\n const raw = readFileSync(filePath, \"utf-8\");\n const parsed = JSON.parse(raw);\n if (!Array.isArray(parsed)) {\n console.error(`[hooks] Warning: ${filePath} should be a JSON array, skipping`);\n return null;\n }\n // Validate each hook config\n return parsed.filter((h: unknown) => {\n if (!h || typeof h !== \"object\") return false;\n const hook = h as Record<string, unknown>;\n if (typeof hook.event !== \"string\" || typeof hook.command !== \"string\") {\n console.error(`[hooks] Warning: Invalid hook in ${filePath}, missing event or command`);\n return false;\n }\n if (!VALID_EVENTS.includes(hook.event as HookEvent)) {\n console.error(`[hooks] Warning: Invalid event \"${hook.event}\" in ${filePath}`);\n return false;\n }\n return true;\n }) as HookConfig[];\n } catch (err) {\n console.error(\n `[hooks] Warning: Failed to parse ${filePath}: ${err instanceof Error ? err.message : err}`,\n );\n return null;\n }\n}\n\n// ============================================================================\n// EXECUTE HOOK\n// ============================================================================\n\n/**\n * Execute a hook command with JSON payload on stdin.\n * Returns parsed JSON response from stdout, or null on error/timeout.\n */\nexport async function executeHook(\n hook: HookConfig,\n payload: HookPayload,\n): Promise<HookResponse | null> {\n const timeout = hook.timeout ?? DEFAULT_TIMEOUT_MS;\n\n return new Promise((resolve) => {\n let stdout = \"\";\n let stderr = \"\";\n let settled = false;\n\n const child = spawn(\"/bin/sh\", [\"-c\", hook.command], {\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n env: { ...process.env },\n });\n\n let timer: ReturnType<typeof setTimeout> | undefined;\n\n const settle = (result: HookResponse | null) => {\n if (settled) return;\n settled = true;\n if (timer) clearTimeout(timer);\n resolve(result);\n };\n\n // Kill on timeout\n timer = setTimeout(() => {\n if (!settled) {\n console.error(\n `[hooks] Warning: Hook \"${hook.command}\" timed out after ${timeout}ms, killing`,\n );\n try {\n child.kill(\"SIGKILL\");\n } catch {\n /* ignore */\n }\n settle(null);\n }\n }, timeout);\n\n child.stdout?.on(\"data\", (data: Buffer) => {\n stdout += data.toString();\n });\n\n child.stderr?.on(\"data\", (data: Buffer) => {\n stderr += data.toString();\n });\n\n child.on(\"error\", (err) => {\n console.error(\n `[hooks] Warning: Hook \"${hook.command}\" failed to start: ${err.message}`,\n );\n settle(null);\n });\n\n child.on(\"close\", (code) => {\n if (code !== 0) {\n if (stderr) {\n console.error(\n `[hooks] Warning: Hook \"${hook.command}\" exited with code ${code}: ${stderr.trim()}`,\n );\n } else {\n console.error(\n `[hooks] Warning: Hook \"${hook.command}\" exited with code ${code}`,\n );\n }\n settle(null);\n return;\n }\n\n // Parse stdout as JSON\n const trimmed = stdout.trim();\n if (!trimmed) {\n settle(null);\n return;\n }\n try {\n settle(JSON.parse(trimmed) as HookResponse);\n } catch {\n // Non-JSON output is not an error, just no structured response\n settle(null);\n }\n });\n\n // Write payload to stdin — suppress EPIPE if child exits before we finish\n child.stdin?.on('error', () => {});\n try {\n child.stdin?.write(JSON.stringify(payload));\n child.stdin?.end();\n } catch {\n // stdin may already be closed — that is fine\n }\n });\n}\n\n// ============================================================================\n// PUBLIC API\n// ============================================================================\n\n/**\n * Run BeforeTool hooks for a given tool call.\n * Returns whether the tool call should proceed and optionally modified input.\n */\nexport async function runBeforeToolHook(\n hooks: HookConfig[],\n toolName: string,\n input: Record<string, unknown>,\n): Promise<{\n allow: boolean;\n message?: string;\n modifiedInput?: Record<string, unknown>;\n}> {\n const matching = hooks.filter(\n (h) =>\n h.event === \"BeforeTool\" &&\n (!h.pattern || matchGlob(h.pattern, toolName)),\n );\n\n if (matching.length === 0) return { allow: true };\n\n const payload: HookPayload = {\n event: \"BeforeTool\",\n tool_name: toolName,\n tool_input: input,\n };\n\n for (const hook of matching) {\n const response = await executeHook(hook, payload);\n if (!response) continue;\n\n // If any hook blocks, stop immediately\n if (response.allow === false) {\n return {\n allow: false,\n message: response.message || `Blocked by hook: ${hook.command}`,\n };\n }\n\n // If hook modifies input and is allowed to\n if (hook.allowModify && response.modified_input) {\n return { allow: true, modifiedInput: response.modified_input };\n }\n }\n\n return { allow: true };\n}\n\n/**\n * Run AfterTool hooks for a given tool result.\n * Returns optionally modified output.\n */\nexport async function runAfterToolHook(\n hooks: HookConfig[],\n toolName: string,\n output: string,\n success: boolean,\n): Promise<{ modifiedOutput?: string }> {\n const matching = hooks.filter(\n (h) =>\n h.event === \"AfterTool\" &&\n (!h.pattern || matchGlob(h.pattern, toolName)),\n );\n\n if (matching.length === 0) return {};\n\n const payload: HookPayload = {\n event: \"AfterTool\",\n tool_name: toolName,\n tool_output: output,\n tool_success: success,\n };\n\n for (const hook of matching) {\n const response = await executeHook(hook, payload);\n if (!response) continue;\n\n // If hook modifies output and is allowed to\n if (hook.allowModify && response.modified_output !== undefined) {\n return { modifiedOutput: response.modified_output };\n }\n }\n\n return {};\n}\n\n/**\n * Run session lifecycle hooks (SessionStart, SessionEnd, Notification).\n * Fire-and-forget — errors are logged but don't affect session.\n */\nexport async function runSessionHook(\n hooks: HookConfig[],\n event: \"SessionStart\" | \"SessionEnd\" | \"Notification\",\n data?: Record<string, unknown>,\n): Promise<void> {\n const matching = hooks.filter((h) => h.event === event);\n\n if (matching.length === 0) return;\n\n const payload: HookPayload = {\n event,\n ...(data || {}),\n };\n\n // Run all session hooks concurrently — don't wait or fail\n await Promise.allSettled(matching.map((hook) => executeHook(hook, payload)));\n}\n"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA,SAASA,UAAU,EAAEC,YAAY,QAAQ,IAAI;AAC7C,SAASC,IAAI,QAAQ,MAAM;AAC3B,SAASC,KAAK,QAAQ,eAAe;AACrC,SAASC,UAAU,QAAQ,YAAY;;AAEvC;AACA;AACA;;AAsBA;;AAUA;;AAQA,MAAMC,kBAAkB,GAAG,MAAM;;AAEjC;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,OAAO,SAASC,SAASA,CAACC,OAAe,EAAEC,IAAY,EAAW;EAChE;EACA,MAAMC,KAAK,GAAGF,OAAO,CAClBG,OAAO,CAAC,mBAAmB,EAAE,MAAM,CAAC,CACpCA,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CACpBA,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;EACtB,OAAO,IAAIC,MAAM,CAAC,IAAIF,KAAK,GAAG,CAAC,CAACG,IAAI,CAACJ,IAAI,CAAC;AAC5C;;AAEA;AACA;AACA;;AAEA,MAAMK,YAAyB,GAAG,CAChC,YAAY,EACZ,WAAW,EACX,cAAc,EACd,YAAY,EACZ,cAAc,CACf;;AAED;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,SAASA,CAACC,GAAW,EAAgB;EACnD,MAAMC,KAAmB,GAAG,EAAE;;EAE9B;EACA,MAAMC,WAAW,GAAGf,IAAI,CAACa,GAAG,EAAE,QAAQ,EAAE,YAAY,CAAC;EACrD,MAAMG,YAAY,GAAGC,iBAAiB,CAACF,WAAW,CAAC;EACnD,IAAIC,YAAY,EAAEF,KAAK,CAACI,IAAI,CAAC,GAAGF,YAAY,CAAC;;EAE7C;EACA,MAAMG,QAAQ,GAAGjB,UAAU;EAC3B,MAAMkB,SAAS,GAAGH,iBAAiB,CAACE,QAAQ,CAAC;EAC7C,IAAIC,SAAS,EAAEN,KAAK,CAACI,IAAI,CAAC,GAAGE,SAAS,CAAC;EAEvC,OAAON,KAAK;AACd;AAEA,SAASG,iBAAiBA,CAACI,QAAgB,EAAuB;EAChE,IAAI,CAACvB,UAAU,CAACuB,QAAQ,CAAC,EAAE,OAAO,IAAI;EACtC,IAAI;IACF,MAAMC,GAAG,GAAGvB,YAAY,CAACsB,QAAQ,EAAE,OAAO,CAAC;IAC3C,MAAME,MAAM,GAAGC,IAAI,CAACC,KAAK,CAACH,GAAG,CAAC;IAC9B,IAAI,CAACI,KAAK,CAACC,OAAO,CAACJ,MAAM,CAAC,EAAE;MAC1BK,OAAO,CAACC,KAAK,CAAC,oBAAoBR,QAAQ,mCAAmC,CAAC;MAC9E,OAAO,IAAI;IACb;IACA;IACA,OAAOE,MAAM,CAACO,MAAM,CAAEC,CAAU,IAAK;MACnC,IAAI,CAACA,CAAC,IAAI,OAAOA,CAAC,KAAK,QAAQ,EAAE,OAAO,KAAK;MAC7C,MAAMC,IAAI,GAAGD,CAA4B;MACzC,IAAI,OAAOC,IAAI,CAACC,KAAK,KAAK,QAAQ,IAAI,OAAOD,IAAI,CAACE,OAAO,KAAK,QAAQ,EAAE;QACtEN,OAAO,CAACC,KAAK,CAAC,oCAAoCR,QAAQ,4BAA4B,CAAC;QACvF,OAAO,KAAK;MACd;MACA,IAAI,CAACV,YAAY,CAACwB,QAAQ,CAACH,IAAI,CAACC,KAAkB,CAAC,EAAE;QACnDL,OAAO,CAACC,KAAK,CAAC,mCAAmCG,IAAI,CAACC,KAAK,QAAQZ,QAAQ,EAAE,CAAC;QAC9E,OAAO,KAAK;MACd;MACA,OAAO,IAAI;IACb,CAAC,CAAC;EACJ,CAAC,CAAC,OAAOe,GAAG,EAAE;IACZR,OAAO,CAACC,KAAK,CACX,oCAAoCR,QAAQ,KAAKe,GAAG,YAAYC,KAAK,GAAGD,GAAG,CAACE,OAAO,GAAGF,GAAG,EAC3F,CAAC;IACD,OAAO,IAAI;EACb;AACF;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,OAAO,eAAeG,WAAWA,CAC/BP,IAAgB,EAChBQ,OAAoB,EACU;EAC9B,MAAMC,OAAO,GAAGT,IAAI,CAACS,OAAO,IAAItC,kBAAkB;EAElD,OAAO,IAAIuC,OAAO,CAAEC,OAAO,IAAK;IAC9B,IAAIC,MAAM,GAAG,EAAE;IACf,IAAIC,MAAM,GAAG,EAAE;IACf,IAAIC,OAAO,GAAG,KAAK;IAEnB,MAAMC,KAAK,GAAG9C,KAAK,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE+B,IAAI,CAACE,OAAO,CAAC,EAAE;MACnDc,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;MAC/BC,GAAG,EAAE;QAAE,GAAGC,OAAO,CAACD;MAAI;IACxB,CAAC,CAAC;IAEF,IAAIE,KAAgD;IAEpD,MAAMC,MAAM,GAAIC,MAA2B,IAAK;MAC9C,IAAIP,OAAO,EAAE;MACbA,OAAO,GAAG,IAAI;MACd,IAAIK,KAAK,EAAEG,YAAY,CAACH,KAAK,CAAC;MAC9BR,OAAO,CAACU,MAAM,CAAC;IACjB,CAAC;;IAED;IACAF,KAAK,GAAGI,UAAU,CAAC,MAAM;MACvB,IAAI,CAACT,OAAO,EAAE;QACZlB,OAAO,CAACC,KAAK,CACX,0BAA0BG,IAAI,CAACE,OAAO,qBAAqBO,OAAO,aACpE,CAAC;QACD,IAAI;UACFM,KAAK,CAACS,IAAI,CAAC,SAAS,CAAC;QACvB,CAAC,CAAC,MAAM;UACN;QAAA;QAEFJ,MAAM,CAAC,IAAI,CAAC;MACd;IACF,CAAC,EAAEX,OAAO,CAAC;IAEXM,KAAK,CAACH,MAAM,EAAEa,EAAE,CAAC,MAAM,EAAGC,IAAY,IAAK;MACzCd,MAAM,IAAIc,IAAI,CAACC,QAAQ,CAAC,CAAC;IAC3B,CAAC,CAAC;IAEFZ,KAAK,CAACF,MAAM,EAAEY,EAAE,CAAC,MAAM,EAAGC,IAAY,IAAK;MACzCb,MAAM,IAAIa,IAAI,CAACC,QAAQ,CAAC,CAAC;IAC3B,CAAC,CAAC;IAEFZ,KAAK,CAACU,EAAE,CAAC,OAAO,EAAGrB,GAAG,IAAK;MACzBR,OAAO,CAACC,KAAK,CACX,0BAA0BG,IAAI,CAACE,OAAO,sBAAsBE,GAAG,CAACE,OAAO,EACzE,CAAC;MACDc,MAAM,CAAC,IAAI,CAAC;IACd,CAAC,CAAC;IAEFL,KAAK,CAACU,EAAE,CAAC,OAAO,EAAGG,IAAI,IAAK;MAC1B,IAAIA,IAAI,KAAK,CAAC,EAAE;QACd,IAAIf,MAAM,EAAE;UACVjB,OAAO,CAACC,KAAK,CACX,0BAA0BG,IAAI,CAACE,OAAO,sBAAsB0B,IAAI,KAAKf,MAAM,CAACgB,IAAI,CAAC,CAAC,EACpF,CAAC;QACH,CAAC,MAAM;UACLjC,OAAO,CAACC,KAAK,CACX,0BAA0BG,IAAI,CAACE,OAAO,sBAAsB0B,IAAI,EAClE,CAAC;QACH;QACAR,MAAM,CAAC,IAAI,CAAC;QACZ;MACF;;MAEA;MACA,MAAMU,OAAO,GAAGlB,MAAM,CAACiB,IAAI,CAAC,CAAC;MAC7B,IAAI,CAACC,OAAO,EAAE;QACZV,MAAM,CAAC,IAAI,CAAC;QACZ;MACF;MACA,IAAI;QACFA,MAAM,CAAC5B,IAAI,CAACC,KAAK,CAACqC,OAAO,CAAiB,CAAC;MAC7C,CAAC,CAAC,MAAM;QACN;QACAV,MAAM,CAAC,IAAI,CAAC;MACd;IACF,CAAC,CAAC;;IAEF;IACAL,KAAK,CAACgB,KAAK,EAAEN,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;IAClC,IAAI;MACFV,KAAK,CAACgB,KAAK,EAAEC,KAAK,CAACxC,IAAI,CAACyC,SAAS,CAACzB,OAAO,CAAC,CAAC;MAC3CO,KAAK,CAACgB,KAAK,EAAEG,GAAG,CAAC,CAAC;IACpB,CAAC,CAAC,MAAM;MACN;IAAA;EAEJ,CAAC,CAAC;AACJ;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA,OAAO,eAAeC,iBAAiBA,CACrCrD,KAAmB,EACnBsD,QAAgB,EAChBC,KAA8B,EAK7B;EACD,MAAMC,QAAQ,GAAGxD,KAAK,CAACgB,MAAM,CAC1BC,CAAC,IACAA,CAAC,CAACE,KAAK,KAAK,YAAY,KACvB,CAACF,CAAC,CAAC1B,OAAO,IAAID,SAAS,CAAC2B,CAAC,CAAC1B,OAAO,EAAE+D,QAAQ,CAAC,CACjD,CAAC;EAED,IAAIE,QAAQ,CAACC,MAAM,KAAK,CAAC,EAAE,OAAO;IAAEC,KAAK,EAAE;EAAK,CAAC;EAEjD,MAAMhC,OAAoB,GAAG;IAC3BP,KAAK,EAAE,YAAY;IACnBwC,SAAS,EAAEL,QAAQ;IACnBM,UAAU,EAAEL;EACd,CAAC;EAED,KAAK,MAAMrC,IAAI,IAAIsC,QAAQ,EAAE;IAC3B,MAAMK,QAAQ,GAAG,MAAMpC,WAAW,CAACP,IAAI,EAAEQ,OAAO,CAAC;IACjD,IAAI,CAACmC,QAAQ,EAAE;;IAEf;IACA,IAAIA,QAAQ,CAACH,KAAK,KAAK,KAAK,EAAE;MAC5B,OAAO;QACLA,KAAK,EAAE,KAAK;QACZlC,OAAO,EAAEqC,QAAQ,CAACrC,OAAO,IAAI,oBAAoBN,IAAI,CAACE,OAAO;MAC/D,CAAC;IACH;;IAEA;IACA,IAAIF,IAAI,CAAC4C,WAAW,IAAID,QAAQ,CAACE,cAAc,EAAE;MAC/C,OAAO;QAAEL,KAAK,EAAE,IAAI;QAAEM,aAAa,EAAEH,QAAQ,CAACE;MAAe,CAAC;IAChE;EACF;EAEA,OAAO;IAAEL,KAAK,EAAE;EAAK,CAAC;AACxB;;AAEA;AACA;AACA;AACA;AACA,OAAO,eAAeO,gBAAgBA,CACpCjE,KAAmB,EACnBsD,QAAgB,EAChBY,MAAc,EACdC,OAAgB,EACsB;EACtC,MAAMX,QAAQ,GAAGxD,KAAK,CAACgB,MAAM,CAC1BC,CAAC,IACAA,CAAC,CAACE,KAAK,KAAK,WAAW,KACtB,CAACF,CAAC,CAAC1B,OAAO,IAAID,SAAS,CAAC2B,CAAC,CAAC1B,OAAO,EAAE+D,QAAQ,CAAC,CACjD,CAAC;EAED,IAAIE,QAAQ,CAACC,MAAM,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC;EAEpC,MAAM/B,OAAoB,GAAG;IAC3BP,KAAK,EAAE,WAAW;IAClBwC,SAAS,EAAEL,QAAQ;IACnBc,WAAW,EAAEF,MAAM;IACnBG,YAAY,EAAEF;EAChB,CAAC;EAED,KAAK,MAAMjD,IAAI,IAAIsC,QAAQ,EAAE;IAC3B,MAAMK,QAAQ,GAAG,MAAMpC,WAAW,CAACP,IAAI,EAAEQ,OAAO,CAAC;IACjD,IAAI,CAACmC,QAAQ,EAAE;;IAEf;IACA,IAAI3C,IAAI,CAAC4C,WAAW,IAAID,QAAQ,CAACS,eAAe,KAAKC,SAAS,EAAE;MAC9D,OAAO;QAAEC,cAAc,EAAEX,QAAQ,CAACS;MAAgB,CAAC;IACrD;EACF;EAEA,OAAO,CAAC,CAAC;AACX;;AAEA;AACA;AACA;AACA;AACA,OAAO,eAAeG,cAAcA,CAClCzE,KAAmB,EACnBmB,KAAqD,EACrDyB,IAA8B,EACf;EACf,MAAMY,QAAQ,GAAGxD,KAAK,CAACgB,MAAM,CAAEC,CAAC,IAAKA,CAAC,CAACE,KAAK,KAAKA,KAAK,CAAC;EAEvD,IAAIqC,QAAQ,CAACC,MAAM,KAAK,CAAC,EAAE;EAE3B,MAAM/B,OAAoB,GAAG;IAC3BP,KAAK;IACL,IAAIyB,IAAI,IAAI,CAAC,CAAC;EAChB,CAAC;;EAED;EACA,MAAMhB,OAAO,CAAC8C,UAAU,CAAClB,QAAQ,CAACmB,GAAG,CAAEzD,IAAI,IAAKO,WAAW,CAACP,IAAI,EAAEQ,OAAO,CAAC,CAAC,CAAC;AAC9E","ignoreList":[]}
@@ -127,6 +127,7 @@ async function _logSpan(opts) {
127
127
  status_code: opts.error ? "ERROR" : "OK",
128
128
  trace_id: ctx.traceId || null,
129
129
  span_id: ctx.spanId || generateSpanId(),
130
+ parent_span_id: ctx.parentSpanId || null,
130
131
  conversation_id: ctx.conversationId || conversationId,
131
132
  user_id: ctx.userId || null,
132
133
  user_email: ctx.userEmail || null,
@@ -1 +1 @@
1
- {"version":3,"file":"telemetry-spans.js","names":["resolveConfig","loadConfig","getValidToken","captureError","generateSpanId","getConversationId","PKG_VERSION","os","FLUSH_INTERVAL","FLUSH_MAX","spanBuffer","flushTimer","conversationRegistered","queueCliSpan","span","push","length","flushCliSpans","setTimeout","clearTimeout","batch","splice","_sendSpans","catch","err","process","env","DEBUG_TELEMETRY","stderr","write","message","spans","config","serverUrl","authToken","supabaseKey","fileConfig","conversationId","body","mode","conversation_id","store_id","storeId","undefined","userId","user_id","userEmail","email","source","conversation_title","Date","toISOString","split","hostname","version","response","fetch","method","headers","JSON","stringify","ok","text","status","slice","logSpan","opts","_logSpan","action","ctx","context","now","startedAt","getTime","durationMs","endedAt","severity","error","service_name","serviceName","span_kind","status_code","trace_id","traceId","span_id","spanId","user_email","start_time","end_time","duration_ms","error_message","details","input_tokens","inputTokens","output_tokens","outputTokens","cache_read_tokens","cacheReadTokens","cache_creation_tokens","cacheCreationTokens","total_cost","totalCost","model","turn_number","turnNumber","agent_id","agentId","agent_name","agentName","tool_type","toolType","errorType","errorMessage","tags"],"sources":["../../../src/cli/services/telemetry-spans.ts"],"sourcesContent":["// telemetry-spans.ts — Span buffer, flush, and logging for CLI telemetry\n// Extracted from telemetry.ts for modularity.\n\nimport { resolveConfig, loadConfig } from \"./config-store.js\";\nimport { getValidToken } from \"./auth-service.js\";\nimport { captureError } from \"./error-logger.js\";\nimport { generateSpanId, getConversationId, PKG_VERSION, type ExecutionContext } from \"./telemetry.js\";\nimport os from \"node:os\";\n\n// ============================================================================\n// SPAN BUFFER — batches spans for bulk POST to the Fly.io server\n// Same pattern as server's clickhouse-buffer.ts but over HTTP.\n// ============================================================================\n\nconst FLUSH_INTERVAL = 2000; // 2s (slower than server — HTTP has more overhead)\nconst FLUSH_MAX = 50; // max spans before force flush\nconst spanBuffer: Record<string, unknown>[] = [];\nlet flushTimer: ReturnType<typeof setTimeout> | null = null;\nlet conversationRegistered = false;\n\nexport function queueCliSpan(span: Record<string, unknown>): void {\n spanBuffer.push(span);\n\n if (spanBuffer.length >= FLUSH_MAX) {\n flushCliSpans();\n } else if (!flushTimer) {\n flushTimer = setTimeout(() => {\n flushTimer = null;\n flushCliSpans();\n }, FLUSH_INTERVAL);\n }\n}\n\n/**\n * Flush all buffered spans to the Fly.io server.\n * Call this on session end or at shutdown.\n */\nexport function flushCliSpans(): void {\n if (flushTimer) {\n clearTimeout(flushTimer);\n flushTimer = null;\n }\n\n if (spanBuffer.length === 0) return;\n\n const batch = spanBuffer.splice(0, spanBuffer.length);\n // Fire-and-forget — never block the chat\n _sendSpans(batch).catch((err) => {\n if (process.env.DEBUG_TELEMETRY) {\n process.stderr.write(`[telemetry] flush error: ${err.message}\\n`);\n }\n });\n}\n\nasync function _sendSpans(spans: Record<string, unknown>[]): Promise<void> {\n const config = resolveConfig();\n if (!config.serverUrl) return;\n\n let authToken = config.supabaseKey;\n if (!authToken) {\n authToken = await getValidToken() || \"\";\n }\n if (!authToken) return;\n\n const fileConfig = loadConfig();\n const conversationId = getConversationId();\n\n const body: Record<string, unknown> = {\n mode: \"telemetry_ingest\",\n spans,\n conversation_id: conversationId,\n store_id: config.storeId || undefined,\n userId: fileConfig.user_id || undefined,\n userEmail: fileConfig.email || undefined,\n source: \"whale_cli\",\n };\n\n // Include conversation metadata on first flush so server creates the row\n if (!conversationRegistered) {\n conversationRegistered = true;\n body.conversation_title = `CLI Session ${new Date().toISOString().split(\"T\")[0]}`;\n body.hostname = os.hostname();\n body.version = PKG_VERSION;\n }\n\n try {\n const response = await fetch(config.serverUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Authorization\": `Bearer ${authToken}`,\n },\n body: JSON.stringify(body),\n });\n\n if (!response.ok && process.env.DEBUG_TELEMETRY) {\n const text = await response.text().catch(() => \"\");\n process.stderr.write(`[telemetry] ingest failed (${response.status}): ${text.slice(0, 200)}\\n`);\n }\n } catch (err) {\n if (process.env.DEBUG_TELEMETRY) {\n process.stderr.write(`[telemetry] ingest error: ${(err as Error).message}\\n`);\n }\n }\n}\n\n// ============================================================================\n// LOG SPAN (fire-and-forget)\n// ============================================================================\n\nexport interface SpanOptions {\n action: string; // e.g. \"claude_api_request\", \"tool.read_file\"\n severity?: \"info\" | \"warn\" | \"error\";\n durationMs: number;\n context: ExecutionContext;\n storeId?: string;\n error?: string;\n details?: Record<string, unknown>;\n}\n\nexport function logSpan(opts: SpanOptions): void {\n // Fire-and-forget — don't await, log errors in debug mode\n _logSpan(opts).catch((err) => {\n if (process.env.DEBUG_TELEMETRY) {\n process.stderr.write(`[telemetry error] ${opts.action}: ${err.message}\\n`);\n }\n });\n}\n\nasync function _logSpan(opts: SpanOptions): Promise<void> {\n if (process.env.DEBUG_TELEMETRY) {\n process.stderr.write(`[telemetry] _logSpan called for ${opts.action}\\n`);\n }\n\n const ctx = opts.context;\n const conversationId = getConversationId();\n const now = new Date();\n const startedAt = new Date(now.getTime() - opts.durationMs).toISOString();\n const endedAt = now.toISOString();\n\n // Build span in same shape as server's auditRowToSpan input\n const span: Record<string, unknown> = {\n action: opts.action,\n severity: opts.severity || (opts.error ? \"error\" : \"info\"),\n store_id: opts.storeId || resolveConfig().storeId || null,\n source: ctx.source || \"whale_cli\",\n service_name: ctx.serviceName || \"whale-code\",\n span_kind: \"INTERNAL\",\n status_code: opts.error ? \"ERROR\" : \"OK\",\n trace_id: ctx.traceId || null,\n span_id: ctx.spanId || generateSpanId(),\n conversation_id: ctx.conversationId || conversationId,\n user_id: ctx.userId || null,\n user_email: ctx.userEmail || null,\n start_time: startedAt,\n end_time: endedAt,\n duration_ms: opts.durationMs,\n error_message: opts.error || null,\n details: {\n ...opts.details,\n input_tokens: ctx.inputTokens,\n output_tokens: ctx.outputTokens,\n cache_read_tokens: ctx.cacheReadTokens,\n cache_creation_tokens: ctx.cacheCreationTokens,\n total_cost: ctx.totalCost,\n model: ctx.model,\n turn_number: ctx.turnNumber,\n agent_id: ctx.agentId,\n agent_name: ctx.agentName,\n tool_type: ctx.toolType,\n },\n };\n\n // Queue for batch send to server → ClickHouse\n queueCliSpan(span);\n\n // Also bridge errors to the error logging system (Postgres)\n if (opts.error) {\n captureError({\n errorType: opts.action,\n errorMessage: opts.error,\n severity: \"error\",\n traceId: ctx.traceId,\n spanId: ctx.spanId,\n storeId: opts.storeId || resolveConfig().storeId,\n userId: ctx.userId,\n userEmail: ctx.userEmail,\n tags: { action: opts.action },\n });\n }\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"telemetry-spans.js","names":["resolveConfig","loadConfig","getValidToken","captureError","generateSpanId","getConversationId","PKG_VERSION","os","FLUSH_INTERVAL","FLUSH_MAX","spanBuffer","flushTimer","conversationRegistered","queueCliSpan","span","push","length","flushCliSpans","setTimeout","clearTimeout","batch","splice","_sendSpans","catch","err","process","env","DEBUG_TELEMETRY","stderr","write","message","spans","config","serverUrl","authToken","supabaseKey","fileConfig","conversationId","body","mode","conversation_id","store_id","storeId","undefined","userId","user_id","userEmail","email","source","conversation_title","Date","toISOString","split","hostname","version","response","fetch","method","headers","JSON","stringify","ok","text","status","slice","logSpan","opts","_logSpan","action","ctx","context","now","startedAt","getTime","durationMs","endedAt","severity","error","service_name","serviceName","span_kind","status_code","trace_id","traceId","span_id","spanId","parent_span_id","parentSpanId","user_email","start_time","end_time","duration_ms","error_message","details","input_tokens","inputTokens","output_tokens","outputTokens","cache_read_tokens","cacheReadTokens","cache_creation_tokens","cacheCreationTokens","total_cost","totalCost","model","turn_number","turnNumber","agent_id","agentId","agent_name","agentName","tool_type","toolType","errorType","errorMessage","tags"],"sources":["../../../src/cli/services/telemetry-spans.ts"],"sourcesContent":["// telemetry-spans.ts — Span buffer, flush, and logging for CLI telemetry\n// Extracted from telemetry.ts for modularity.\n\nimport { resolveConfig, loadConfig } from \"./config-store.js\";\nimport { getValidToken } from \"./auth-service.js\";\nimport { captureError } from \"./error-logger.js\";\nimport { generateSpanId, getConversationId, PKG_VERSION, type ExecutionContext } from \"./telemetry.js\";\nimport os from \"node:os\";\n\n// ============================================================================\n// SPAN BUFFER — batches spans for bulk POST to the Fly.io server\n// Same pattern as server's clickhouse-buffer.ts but over HTTP.\n// ============================================================================\n\nconst FLUSH_INTERVAL = 2000; // 2s (slower than server — HTTP has more overhead)\nconst FLUSH_MAX = 50; // max spans before force flush\nconst spanBuffer: Record<string, unknown>[] = [];\nlet flushTimer: ReturnType<typeof setTimeout> | null = null;\nlet conversationRegistered = false;\n\nexport function queueCliSpan(span: Record<string, unknown>): void {\n spanBuffer.push(span);\n\n if (spanBuffer.length >= FLUSH_MAX) {\n flushCliSpans();\n } else if (!flushTimer) {\n flushTimer = setTimeout(() => {\n flushTimer = null;\n flushCliSpans();\n }, FLUSH_INTERVAL);\n }\n}\n\n/**\n * Flush all buffered spans to the Fly.io server.\n * Call this on session end or at shutdown.\n */\nexport function flushCliSpans(): void {\n if (flushTimer) {\n clearTimeout(flushTimer);\n flushTimer = null;\n }\n\n if (spanBuffer.length === 0) return;\n\n const batch = spanBuffer.splice(0, spanBuffer.length);\n // Fire-and-forget — never block the chat\n _sendSpans(batch).catch((err) => {\n if (process.env.DEBUG_TELEMETRY) {\n process.stderr.write(`[telemetry] flush error: ${err.message}\\n`);\n }\n });\n}\n\nasync function _sendSpans(spans: Record<string, unknown>[]): Promise<void> {\n const config = resolveConfig();\n if (!config.serverUrl) return;\n\n let authToken = config.supabaseKey;\n if (!authToken) {\n authToken = await getValidToken() || \"\";\n }\n if (!authToken) return;\n\n const fileConfig = loadConfig();\n const conversationId = getConversationId();\n\n const body: Record<string, unknown> = {\n mode: \"telemetry_ingest\",\n spans,\n conversation_id: conversationId,\n store_id: config.storeId || undefined,\n userId: fileConfig.user_id || undefined,\n userEmail: fileConfig.email || undefined,\n source: \"whale_cli\",\n };\n\n // Include conversation metadata on first flush so server creates the row\n if (!conversationRegistered) {\n conversationRegistered = true;\n body.conversation_title = `CLI Session ${new Date().toISOString().split(\"T\")[0]}`;\n body.hostname = os.hostname();\n body.version = PKG_VERSION;\n }\n\n try {\n const response = await fetch(config.serverUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Authorization\": `Bearer ${authToken}`,\n },\n body: JSON.stringify(body),\n });\n\n if (!response.ok && process.env.DEBUG_TELEMETRY) {\n const text = await response.text().catch(() => \"\");\n process.stderr.write(`[telemetry] ingest failed (${response.status}): ${text.slice(0, 200)}\\n`);\n }\n } catch (err) {\n if (process.env.DEBUG_TELEMETRY) {\n process.stderr.write(`[telemetry] ingest error: ${(err as Error).message}\\n`);\n }\n }\n}\n\n// ============================================================================\n// LOG SPAN (fire-and-forget)\n// ============================================================================\n\nexport interface SpanOptions {\n action: string; // e.g. \"claude_api_request\", \"tool.read_file\"\n severity?: \"info\" | \"warn\" | \"error\";\n durationMs: number;\n context: ExecutionContext;\n storeId?: string;\n error?: string;\n details?: Record<string, unknown>;\n}\n\nexport function logSpan(opts: SpanOptions): void {\n // Fire-and-forget — don't await, log errors in debug mode\n _logSpan(opts).catch((err) => {\n if (process.env.DEBUG_TELEMETRY) {\n process.stderr.write(`[telemetry error] ${opts.action}: ${err.message}\\n`);\n }\n });\n}\n\nasync function _logSpan(opts: SpanOptions): Promise<void> {\n if (process.env.DEBUG_TELEMETRY) {\n process.stderr.write(`[telemetry] _logSpan called for ${opts.action}\\n`);\n }\n\n const ctx = opts.context;\n const conversationId = getConversationId();\n const now = new Date();\n const startedAt = new Date(now.getTime() - opts.durationMs).toISOString();\n const endedAt = now.toISOString();\n\n // Build span in same shape as server's auditRowToSpan input\n const span: Record<string, unknown> = {\n action: opts.action,\n severity: opts.severity || (opts.error ? \"error\" : \"info\"),\n store_id: opts.storeId || resolveConfig().storeId || null,\n source: ctx.source || \"whale_cli\",\n service_name: ctx.serviceName || \"whale-code\",\n span_kind: \"INTERNAL\",\n status_code: opts.error ? \"ERROR\" : \"OK\",\n trace_id: ctx.traceId || null,\n span_id: ctx.spanId || generateSpanId(),\n parent_span_id: ctx.parentSpanId || null,\n conversation_id: ctx.conversationId || conversationId,\n user_id: ctx.userId || null,\n user_email: ctx.userEmail || null,\n start_time: startedAt,\n end_time: endedAt,\n duration_ms: opts.durationMs,\n error_message: opts.error || null,\n details: {\n ...opts.details,\n input_tokens: ctx.inputTokens,\n output_tokens: ctx.outputTokens,\n cache_read_tokens: ctx.cacheReadTokens,\n cache_creation_tokens: ctx.cacheCreationTokens,\n total_cost: ctx.totalCost,\n model: ctx.model,\n turn_number: ctx.turnNumber,\n agent_id: ctx.agentId,\n agent_name: ctx.agentName,\n tool_type: ctx.toolType,\n },\n };\n\n // Queue for batch send to server → ClickHouse\n queueCliSpan(span);\n\n // Also bridge errors to the error logging system (Postgres)\n if (opts.error) {\n captureError({\n errorType: opts.action,\n errorMessage: opts.error,\n severity: \"error\",\n traceId: ctx.traceId,\n spanId: ctx.spanId,\n storeId: opts.storeId || resolveConfig().storeId,\n userId: ctx.userId,\n userEmail: ctx.userEmail,\n tags: { action: opts.action },\n });\n }\n}\n"],"mappings":"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","ignoreList":[]}
@@ -43,6 +43,29 @@ export declare function setConversationId(id: string): void;
43
43
  * Get the current conversation ID
44
44
  */
45
45
  export declare function getConversationId(): string;
46
+ /**
47
+ * Get the conversation-level trace ID. All spans in this conversation
48
+ * share this trace_id, forming a single distributed trace.
49
+ */
50
+ export declare function getConversationTraceId(): string;
51
+ /**
52
+ * Get the conversation root span ID. Turn spans use this as their parent_span_id.
53
+ */
54
+ export declare function getConversationRootSpanId(): string;
55
+ /**
56
+ * Conversation-level trace context returned by createConversationContext().
57
+ */
58
+ export interface ConversationContext {
59
+ traceId: string;
60
+ rootSpanId: string;
61
+ conversationId: string;
62
+ }
63
+ /**
64
+ * Create (or reset) the conversation-level trace context.
65
+ * Call once when a new conversation session starts (e.g., app launch, /clear).
66
+ * Returns a root trace_id + root span_id that all turn spans nest under.
67
+ */
68
+ export declare function createConversationContext(): ConversationContext;
46
69
  /**
47
70
  * Initialize the telemetry client with a specific auth token.
48
71
  * Used by worker threads that receive the token from the parent.
@@ -24,6 +24,14 @@ export const PKG_VERSION = require("../../../package.json").version;
24
24
  let conversationId = crypto.randomUUID();
25
25
  let turnNumber = 0;
26
26
 
27
+ /**
28
+ * Conversation-level trace context — one trace per conversation session.
29
+ * All turns and their child spans share this trace_id, forming a single
30
+ * distributed trace per conversation.
31
+ */
32
+ let conversationTraceId = generateTraceId();
33
+ let conversationRootSpanId = generateSpanId();
34
+
27
35
  /**
28
36
  * Set the conversation ID (used by worker threads to share parent's conversation)
29
37
  */
@@ -37,6 +45,40 @@ export function setConversationId(id) {
37
45
  export function getConversationId() {
38
46
  return conversationId;
39
47
  }
48
+
49
+ /**
50
+ * Get the conversation-level trace ID. All spans in this conversation
51
+ * share this trace_id, forming a single distributed trace.
52
+ */
53
+ export function getConversationTraceId() {
54
+ return conversationTraceId;
55
+ }
56
+
57
+ /**
58
+ * Get the conversation root span ID. Turn spans use this as their parent_span_id.
59
+ */
60
+ export function getConversationRootSpanId() {
61
+ return conversationRootSpanId;
62
+ }
63
+
64
+ /**
65
+ * Conversation-level trace context returned by createConversationContext().
66
+ */
67
+
68
+ /**
69
+ * Create (or reset) the conversation-level trace context.
70
+ * Call once when a new conversation session starts (e.g., app launch, /clear).
71
+ * Returns a root trace_id + root span_id that all turn spans nest under.
72
+ */
73
+ export function createConversationContext() {
74
+ conversationTraceId = generateTraceId();
75
+ conversationRootSpanId = generateSpanId();
76
+ return {
77
+ traceId: conversationTraceId,
78
+ rootSpanId: conversationRootSpanId,
79
+ conversationId
80
+ };
81
+ }
40
82
  /**
41
83
  * Initialize the telemetry client with a specific auth token.
42
84
  * Used by worker threads that receive the token from the parent.
@@ -81,8 +123,9 @@ export function createTurnContext(overrides) {
81
123
  serviceVersion: PKG_VERSION,
82
124
  conversationId,
83
125
  turnNumber,
84
- traceId: generateTraceId(),
126
+ traceId: conversationTraceId,
85
127
  spanId: generateSpanId(),
128
+ parentSpanId: conversationRootSpanId,
86
129
  traceFlags: 1,
87
130
  userId: user_id,
88
131
  userEmail: email,
@@ -211,6 +254,7 @@ async function _logSpan(opts) {
211
254
  status_code: opts.error ? "ERROR" : "OK",
212
255
  trace_id: ctx.traceId || null,
213
256
  span_id: ctx.spanId || generateSpanId(),
257
+ parent_span_id: ctx.parentSpanId || null,
214
258
  conversation_id: ctx.conversationId || conversationId,
215
259
  user_id: ctx.userId || null,
216
260
  user_email: ctx.userEmail || null,
@@ -1 +1 @@
1
- {"version":3,"file":"telemetry.js","names":["createRequire","os","resolveConfig","loadConfig","getValidToken","captureError","require","import","meta","url","PKG_VERSION","version","conversationId","crypto","randomUUID","turnNumber","setConversationId","id","getConversationId","initializeTelemetryClient","_authToken","generateTraceId","generateSpanId","bytes","Uint8Array","getRandomValues","Array","from","map","b","toString","padStart","join","nextTurn","createTurnContext","overrides","user_id","email","source","serviceName","serviceVersion","traceId","spanId","traceFlags","userId","userEmail","getTurnNumber","FLUSH_INTERVAL","FLUSH_MAX","spanBuffer","flushTimer","conversationRegistered","queueCliSpan","span","push","length","flushCliSpans","setTimeout","clearTimeout","batch","splice","_sendSpans","catch","err","process","env","DEBUG_TELEMETRY","stderr","write","message","spans","config","serverUrl","authToken","supabaseKey","fileConfig","body","mode","conversation_id","store_id","storeId","undefined","conversation_title","Date","toISOString","split","hostname","response","fetch","method","headers","JSON","stringify","ok","text","status","slice","logSpan","opts","_logSpan","action","ctx","context","now","startedAt","getTime","durationMs","endedAt","severity","error","service_name","span_kind","status_code","trace_id","span_id","user_email","start_time","end_time","duration_ms","error_message","details","input_tokens","inputTokens","output_tokens","outputTokens","cache_read_tokens","cacheReadTokens","cache_creation_tokens","cacheCreationTokens","total_cost","totalCost","model","turn_number","agent_id","agentId","agent_name","agentName","tool_type","toolType","errorType","errorMessage","tags"],"sources":["../../../src/cli/services/telemetry.ts"],"sourcesContent":["/**\n * CLI Telemetry — spans are buffered and flushed to the Fly.io server,\n * which queues them into ClickHouse ai_spans.\n *\n * Session-scoped conversationId + auto-incrementing turnNumber.\n * Uses same column schema as server-side telemetry (trace_id, span_id, etc).\n * Never blocks or crashes the chat.\n */\n\nimport { createRequire } from \"module\";\nimport os from \"node:os\";\nimport { resolveConfig, loadConfig } from \"./config-store.js\";\nimport { getValidToken } from \"./auth-service.js\";\nimport { captureError } from \"./error-logger.js\";\n\nconst require = createRequire(import.meta.url);\nexport const PKG_VERSION: string = require(\"../../../package.json\").version;\n\n// Telemetry context for tracing — formerly in executor.ts, now owned here.\nexport interface ExecutionContext {\n source: string;\n userId?: string;\n userEmail?: string;\n traceId?: string;\n spanId?: string;\n parentSpanId?: string;\n traceFlags?: number;\n requestId?: string;\n parentId?: string; // UUID of parent span's row ID (for parent_id column)\n rowId?: string; // UUID to use as this row's ID (so children can reference it)\n serviceName?: string;\n serviceVersion?: string;\n agentId?: string;\n agentName?: string;\n conversationId?: string;\n turnNumber?: number;\n model?: string;\n inputTokens?: number;\n outputTokens?: number;\n cacheReadTokens?: number;\n cacheCreationTokens?: number;\n totalCost?: number;\n costBefore?: number;\n turnCost?: number;\n iteration?: number;\n toolType?: string;\n}\n\n// ============================================================================\n// SESSION STATE\n// ============================================================================\n\nlet conversationId: string = crypto.randomUUID();\nlet turnNumber = 0;\n\n/**\n * Set the conversation ID (used by worker threads to share parent's conversation)\n */\nexport function setConversationId(id: string): void {\n conversationId = id;\n}\n\n/**\n * Get the current conversation ID\n */\nexport function getConversationId(): string {\n return conversationId;\n}\n/**\n * Initialize the telemetry client with a specific auth token.\n * Used by worker threads that receive the token from the parent.\n * Spans are now sent via HTTP (not Supabase direct), so this is a no-op\n * retained for API compatibility.\n */\nexport function initializeTelemetryClient(_authToken: string): void {\n // Spans flow via _sendSpans() → HTTP → server → ClickHouse.\n // No Supabase client needed.\n}\n\n// ============================================================================\n// W3C TRACE CONTEXT GENERATORS\n// ============================================================================\n\nexport function generateTraceId(): string {\n // Use proper UUID format so it's compatible with Postgres uuid columns\n return crypto.randomUUID();\n}\n\nexport function generateSpanId(): string {\n const bytes = new Uint8Array(8);\n crypto.getRandomValues(bytes);\n return Array.from(bytes).map(b => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\n// ============================================================================\n// TURN CONTEXT\n// ============================================================================\n\nexport function nextTurn(): number {\n return ++turnNumber;\n}\n\nexport function createTurnContext(overrides?: Partial<ExecutionContext>): ExecutionContext {\n // Get user info from config\n const { user_id, email } = loadConfig();\n\n return {\n source: \"whale_cli\",\n serviceName: \"whale-code\",\n serviceVersion: PKG_VERSION,\n conversationId,\n turnNumber,\n traceId: generateTraceId(),\n spanId: generateSpanId(),\n traceFlags: 1,\n userId: user_id,\n userEmail: email,\n ...overrides,\n };\n}\n\nexport function getTurnNumber(): number {\n return turnNumber;\n}\n\n// ============================================================================\n// SPAN BUFFER — batches spans for bulk POST to the Fly.io server\n// Same pattern as server's clickhouse-buffer.ts but over HTTP.\n// ============================================================================\n\nconst FLUSH_INTERVAL = 2000; // 2s (slower than server — HTTP has more overhead)\nconst FLUSH_MAX = 50; // max spans before force flush\nconst spanBuffer: Record<string, unknown>[] = [];\nlet flushTimer: ReturnType<typeof setTimeout> | null = null;\nlet conversationRegistered = false;\n\nfunction queueCliSpan(span: Record<string, unknown>): void {\n spanBuffer.push(span);\n\n if (spanBuffer.length >= FLUSH_MAX) {\n flushCliSpans();\n } else if (!flushTimer) {\n flushTimer = setTimeout(() => {\n flushTimer = null;\n flushCliSpans();\n }, FLUSH_INTERVAL);\n }\n}\n\n/**\n * Flush all buffered spans to the Fly.io server.\n * Call this on session end or at shutdown.\n */\nexport function flushCliSpans(): void {\n if (flushTimer) {\n clearTimeout(flushTimer);\n flushTimer = null;\n }\n\n if (spanBuffer.length === 0) return;\n\n const batch = spanBuffer.splice(0, spanBuffer.length);\n // Fire-and-forget — never block the chat\n _sendSpans(batch).catch((err) => {\n if (process.env.DEBUG_TELEMETRY) {\n process.stderr.write(`[telemetry] flush error: ${err.message}\\n`);\n }\n });\n}\n\nasync function _sendSpans(spans: Record<string, unknown>[]): Promise<void> {\n const config = resolveConfig();\n if (!config.serverUrl) return;\n\n let authToken = config.supabaseKey;\n if (!authToken) {\n authToken = await getValidToken() || \"\";\n }\n if (!authToken) return;\n\n const fileConfig = loadConfig();\n\n const body: Record<string, unknown> = {\n mode: \"telemetry_ingest\",\n spans,\n conversation_id: conversationId,\n store_id: config.storeId || undefined,\n userId: fileConfig.user_id || undefined,\n userEmail: fileConfig.email || undefined,\n source: \"whale_cli\",\n };\n\n // Include conversation metadata on first flush so server creates the row\n if (!conversationRegistered) {\n conversationRegistered = true;\n body.conversation_title = `CLI Session ${new Date().toISOString().split(\"T\")[0]}`;\n body.hostname = os.hostname();\n body.version = PKG_VERSION;\n }\n\n try {\n const response = await fetch(config.serverUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Authorization\": `Bearer ${authToken}`,\n },\n body: JSON.stringify(body),\n });\n\n if (!response.ok && process.env.DEBUG_TELEMETRY) {\n const text = await response.text().catch(() => \"\");\n process.stderr.write(`[telemetry] ingest failed (${response.status}): ${text.slice(0, 200)}\\n`);\n }\n } catch (err) {\n if (process.env.DEBUG_TELEMETRY) {\n process.stderr.write(`[telemetry] ingest error: ${(err as Error).message}\\n`);\n }\n }\n}\n\n// ============================================================================\n// LOG SPAN (fire-and-forget)\n// ============================================================================\n\nexport interface SpanOptions {\n action: string; // e.g. \"claude_api_request\", \"tool.read_file\"\n severity?: \"info\" | \"warn\" | \"error\";\n durationMs: number;\n context: ExecutionContext;\n storeId?: string;\n error?: string;\n details?: Record<string, unknown>;\n}\n\nexport function logSpan(opts: SpanOptions): void {\n // Fire-and-forget — don't await, log errors in debug mode\n _logSpan(opts).catch((err) => {\n if (process.env.DEBUG_TELEMETRY) {\n process.stderr.write(`[telemetry error] ${opts.action}: ${err.message}\\n`);\n }\n });\n}\n\nasync function _logSpan(opts: SpanOptions): Promise<void> {\n if (process.env.DEBUG_TELEMETRY) {\n process.stderr.write(`[telemetry] _logSpan called for ${opts.action}\\n`);\n }\n\n const ctx = opts.context;\n const now = new Date();\n const startedAt = new Date(now.getTime() - opts.durationMs).toISOString();\n const endedAt = now.toISOString();\n\n // Build span in same shape as server's auditRowToSpan input\n const span: Record<string, unknown> = {\n action: opts.action,\n severity: opts.severity || (opts.error ? \"error\" : \"info\"),\n store_id: opts.storeId || resolveConfig().storeId || null,\n source: ctx.source || \"whale_cli\",\n service_name: ctx.serviceName || \"whale-code\",\n span_kind: \"INTERNAL\",\n status_code: opts.error ? \"ERROR\" : \"OK\",\n trace_id: ctx.traceId || null,\n span_id: ctx.spanId || generateSpanId(),\n conversation_id: ctx.conversationId || conversationId,\n user_id: ctx.userId || null,\n user_email: ctx.userEmail || null,\n start_time: startedAt,\n end_time: endedAt,\n duration_ms: opts.durationMs,\n error_message: opts.error || null,\n details: {\n ...opts.details,\n input_tokens: ctx.inputTokens,\n output_tokens: ctx.outputTokens,\n cache_read_tokens: ctx.cacheReadTokens,\n cache_creation_tokens: ctx.cacheCreationTokens,\n total_cost: ctx.totalCost,\n model: ctx.model,\n turn_number: ctx.turnNumber,\n agent_id: ctx.agentId,\n agent_name: ctx.agentName,\n tool_type: ctx.toolType,\n },\n };\n\n // Queue for batch send to server → ClickHouse\n queueCliSpan(span);\n\n // Also bridge errors to the error logging system (Postgres)\n if (opts.error) {\n captureError({\n errorType: opts.action,\n errorMessage: opts.error,\n severity: \"error\",\n traceId: ctx.traceId,\n spanId: ctx.spanId,\n storeId: opts.storeId || resolveConfig().storeId,\n userId: ctx.userId,\n userEmail: ctx.userEmail,\n tags: { action: opts.action },\n });\n }\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"telemetry.js","names":["createRequire","os","resolveConfig","loadConfig","getValidToken","captureError","require","import","meta","url","PKG_VERSION","version","conversationId","crypto","randomUUID","turnNumber","conversationTraceId","generateTraceId","conversationRootSpanId","generateSpanId","setConversationId","id","getConversationId","getConversationTraceId","getConversationRootSpanId","createConversationContext","traceId","rootSpanId","initializeTelemetryClient","_authToken","bytes","Uint8Array","getRandomValues","Array","from","map","b","toString","padStart","join","nextTurn","createTurnContext","overrides","user_id","email","source","serviceName","serviceVersion","spanId","parentSpanId","traceFlags","userId","userEmail","getTurnNumber","FLUSH_INTERVAL","FLUSH_MAX","spanBuffer","flushTimer","conversationRegistered","queueCliSpan","span","push","length","flushCliSpans","setTimeout","clearTimeout","batch","splice","_sendSpans","catch","err","process","env","DEBUG_TELEMETRY","stderr","write","message","spans","config","serverUrl","authToken","supabaseKey","fileConfig","body","mode","conversation_id","store_id","storeId","undefined","conversation_title","Date","toISOString","split","hostname","response","fetch","method","headers","JSON","stringify","ok","text","status","slice","logSpan","opts","_logSpan","action","ctx","context","now","startedAt","getTime","durationMs","endedAt","severity","error","service_name","span_kind","status_code","trace_id","span_id","parent_span_id","user_email","start_time","end_time","duration_ms","error_message","details","input_tokens","inputTokens","output_tokens","outputTokens","cache_read_tokens","cacheReadTokens","cache_creation_tokens","cacheCreationTokens","total_cost","totalCost","model","turn_number","agent_id","agentId","agent_name","agentName","tool_type","toolType","errorType","errorMessage","tags"],"sources":["../../../src/cli/services/telemetry.ts"],"sourcesContent":["/**\n * CLI Telemetry — spans are buffered and flushed to the Fly.io server,\n * which queues them into ClickHouse ai_spans.\n *\n * Session-scoped conversationId + auto-incrementing turnNumber.\n * Uses same column schema as server-side telemetry (trace_id, span_id, etc).\n * Never blocks or crashes the chat.\n */\n\nimport { createRequire } from \"module\";\nimport os from \"node:os\";\nimport { resolveConfig, loadConfig } from \"./config-store.js\";\nimport { getValidToken } from \"./auth-service.js\";\nimport { captureError } from \"./error-logger.js\";\n\nconst require = createRequire(import.meta.url);\nexport const PKG_VERSION: string = require(\"../../../package.json\").version;\n\n// Telemetry context for tracing — formerly in executor.ts, now owned here.\nexport interface ExecutionContext {\n source: string;\n userId?: string;\n userEmail?: string;\n traceId?: string;\n spanId?: string;\n parentSpanId?: string;\n traceFlags?: number;\n requestId?: string;\n parentId?: string; // UUID of parent span's row ID (for parent_id column)\n rowId?: string; // UUID to use as this row's ID (so children can reference it)\n serviceName?: string;\n serviceVersion?: string;\n agentId?: string;\n agentName?: string;\n conversationId?: string;\n turnNumber?: number;\n model?: string;\n inputTokens?: number;\n outputTokens?: number;\n cacheReadTokens?: number;\n cacheCreationTokens?: number;\n totalCost?: number;\n costBefore?: number;\n turnCost?: number;\n iteration?: number;\n toolType?: string;\n}\n\n// ============================================================================\n// SESSION STATE\n// ============================================================================\n\nlet conversationId: string = crypto.randomUUID();\nlet turnNumber = 0;\n\n/**\n * Conversation-level trace context — one trace per conversation session.\n * All turns and their child spans share this trace_id, forming a single\n * distributed trace per conversation.\n */\nlet conversationTraceId: string = generateTraceId();\nlet conversationRootSpanId: string = generateSpanId();\n\n/**\n * Set the conversation ID (used by worker threads to share parent's conversation)\n */\nexport function setConversationId(id: string): void {\n conversationId = id;\n}\n\n/**\n * Get the current conversation ID\n */\nexport function getConversationId(): string {\n return conversationId;\n}\n\n/**\n * Get the conversation-level trace ID. All spans in this conversation\n * share this trace_id, forming a single distributed trace.\n */\nexport function getConversationTraceId(): string {\n return conversationTraceId;\n}\n\n/**\n * Get the conversation root span ID. Turn spans use this as their parent_span_id.\n */\nexport function getConversationRootSpanId(): string {\n return conversationRootSpanId;\n}\n\n/**\n * Conversation-level trace context returned by createConversationContext().\n */\nexport interface ConversationContext {\n traceId: string;\n rootSpanId: string;\n conversationId: string;\n}\n\n/**\n * Create (or reset) the conversation-level trace context.\n * Call once when a new conversation session starts (e.g., app launch, /clear).\n * Returns a root trace_id + root span_id that all turn spans nest under.\n */\nexport function createConversationContext(): ConversationContext {\n conversationTraceId = generateTraceId();\n conversationRootSpanId = generateSpanId();\n return {\n traceId: conversationTraceId,\n rootSpanId: conversationRootSpanId,\n conversationId,\n };\n}\n/**\n * Initialize the telemetry client with a specific auth token.\n * Used by worker threads that receive the token from the parent.\n * Spans are now sent via HTTP (not Supabase direct), so this is a no-op\n * retained for API compatibility.\n */\nexport function initializeTelemetryClient(_authToken: string): void {\n // Spans flow via _sendSpans() → HTTP → server → ClickHouse.\n // No Supabase client needed.\n}\n\n// ============================================================================\n// W3C TRACE CONTEXT GENERATORS\n// ============================================================================\n\nexport function generateTraceId(): string {\n // Use proper UUID format so it's compatible with Postgres uuid columns\n return crypto.randomUUID();\n}\n\nexport function generateSpanId(): string {\n const bytes = new Uint8Array(8);\n crypto.getRandomValues(bytes);\n return Array.from(bytes).map(b => b.toString(16).padStart(2, \"0\")).join(\"\");\n}\n\n// ============================================================================\n// TURN CONTEXT\n// ============================================================================\n\nexport function nextTurn(): number {\n return ++turnNumber;\n}\n\nexport function createTurnContext(overrides?: Partial<ExecutionContext>): ExecutionContext {\n // Get user info from config\n const { user_id, email } = loadConfig();\n\n return {\n source: \"whale_cli\",\n serviceName: \"whale-code\",\n serviceVersion: PKG_VERSION,\n conversationId,\n turnNumber,\n traceId: conversationTraceId,\n spanId: generateSpanId(),\n parentSpanId: conversationRootSpanId,\n traceFlags: 1,\n userId: user_id,\n userEmail: email,\n ...overrides,\n };\n}\n\nexport function getTurnNumber(): number {\n return turnNumber;\n}\n\n// ============================================================================\n// SPAN BUFFER — batches spans for bulk POST to the Fly.io server\n// Same pattern as server's clickhouse-buffer.ts but over HTTP.\n// ============================================================================\n\nconst FLUSH_INTERVAL = 2000; // 2s (slower than server — HTTP has more overhead)\nconst FLUSH_MAX = 50; // max spans before force flush\nconst spanBuffer: Record<string, unknown>[] = [];\nlet flushTimer: ReturnType<typeof setTimeout> | null = null;\nlet conversationRegistered = false;\n\nfunction queueCliSpan(span: Record<string, unknown>): void {\n spanBuffer.push(span);\n\n if (spanBuffer.length >= FLUSH_MAX) {\n flushCliSpans();\n } else if (!flushTimer) {\n flushTimer = setTimeout(() => {\n flushTimer = null;\n flushCliSpans();\n }, FLUSH_INTERVAL);\n }\n}\n\n/**\n * Flush all buffered spans to the Fly.io server.\n * Call this on session end or at shutdown.\n */\nexport function flushCliSpans(): void {\n if (flushTimer) {\n clearTimeout(flushTimer);\n flushTimer = null;\n }\n\n if (spanBuffer.length === 0) return;\n\n const batch = spanBuffer.splice(0, spanBuffer.length);\n // Fire-and-forget — never block the chat\n _sendSpans(batch).catch((err) => {\n if (process.env.DEBUG_TELEMETRY) {\n process.stderr.write(`[telemetry] flush error: ${err.message}\\n`);\n }\n });\n}\n\nasync function _sendSpans(spans: Record<string, unknown>[]): Promise<void> {\n const config = resolveConfig();\n if (!config.serverUrl) return;\n\n let authToken = config.supabaseKey;\n if (!authToken) {\n authToken = await getValidToken() || \"\";\n }\n if (!authToken) return;\n\n const fileConfig = loadConfig();\n\n const body: Record<string, unknown> = {\n mode: \"telemetry_ingest\",\n spans,\n conversation_id: conversationId,\n store_id: config.storeId || undefined,\n userId: fileConfig.user_id || undefined,\n userEmail: fileConfig.email || undefined,\n source: \"whale_cli\",\n };\n\n // Include conversation metadata on first flush so server creates the row\n if (!conversationRegistered) {\n conversationRegistered = true;\n body.conversation_title = `CLI Session ${new Date().toISOString().split(\"T\")[0]}`;\n body.hostname = os.hostname();\n body.version = PKG_VERSION;\n }\n\n try {\n const response = await fetch(config.serverUrl, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"Authorization\": `Bearer ${authToken}`,\n },\n body: JSON.stringify(body),\n });\n\n if (!response.ok && process.env.DEBUG_TELEMETRY) {\n const text = await response.text().catch(() => \"\");\n process.stderr.write(`[telemetry] ingest failed (${response.status}): ${text.slice(0, 200)}\\n`);\n }\n } catch (err) {\n if (process.env.DEBUG_TELEMETRY) {\n process.stderr.write(`[telemetry] ingest error: ${(err as Error).message}\\n`);\n }\n }\n}\n\n// ============================================================================\n// LOG SPAN (fire-and-forget)\n// ============================================================================\n\nexport interface SpanOptions {\n action: string; // e.g. \"claude_api_request\", \"tool.read_file\"\n severity?: \"info\" | \"warn\" | \"error\";\n durationMs: number;\n context: ExecutionContext;\n storeId?: string;\n error?: string;\n details?: Record<string, unknown>;\n}\n\nexport function logSpan(opts: SpanOptions): void {\n // Fire-and-forget — don't await, log errors in debug mode\n _logSpan(opts).catch((err) => {\n if (process.env.DEBUG_TELEMETRY) {\n process.stderr.write(`[telemetry error] ${opts.action}: ${err.message}\\n`);\n }\n });\n}\n\nasync function _logSpan(opts: SpanOptions): Promise<void> {\n if (process.env.DEBUG_TELEMETRY) {\n process.stderr.write(`[telemetry] _logSpan called for ${opts.action}\\n`);\n }\n\n const ctx = opts.context;\n const now = new Date();\n const startedAt = new Date(now.getTime() - opts.durationMs).toISOString();\n const endedAt = now.toISOString();\n\n // Build span in same shape as server's auditRowToSpan input\n const span: Record<string, unknown> = {\n action: opts.action,\n severity: opts.severity || (opts.error ? \"error\" : \"info\"),\n store_id: opts.storeId || resolveConfig().storeId || null,\n source: ctx.source || \"whale_cli\",\n service_name: ctx.serviceName || \"whale-code\",\n span_kind: \"INTERNAL\",\n status_code: opts.error ? \"ERROR\" : \"OK\",\n trace_id: ctx.traceId || null,\n span_id: ctx.spanId || generateSpanId(),\n parent_span_id: ctx.parentSpanId || null,\n conversation_id: ctx.conversationId || conversationId,\n user_id: ctx.userId || null,\n user_email: ctx.userEmail || null,\n start_time: startedAt,\n end_time: endedAt,\n duration_ms: opts.durationMs,\n error_message: opts.error || null,\n details: {\n ...opts.details,\n input_tokens: ctx.inputTokens,\n output_tokens: ctx.outputTokens,\n cache_read_tokens: ctx.cacheReadTokens,\n cache_creation_tokens: ctx.cacheCreationTokens,\n total_cost: ctx.totalCost,\n model: ctx.model,\n turn_number: ctx.turnNumber,\n agent_id: ctx.agentId,\n agent_name: ctx.agentName,\n tool_type: ctx.toolType,\n },\n };\n\n // Queue for batch send to server → ClickHouse\n queueCliSpan(span);\n\n // Also bridge errors to the error logging system (Postgres)\n if (opts.error) {\n captureError({\n errorType: opts.action,\n errorMessage: opts.error,\n severity: \"error\",\n traceId: ctx.traceId,\n spanId: ctx.spanId,\n storeId: opts.storeId || resolveConfig().storeId,\n userId: ctx.userId,\n userEmail: ctx.userEmail,\n tags: { action: opts.action },\n });\n }\n}\n"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA,SAASA,aAAa,QAAQ,QAAQ;AACtC,OAAOC,EAAE,MAAM,SAAS;AACxB,SAASC,aAAa,EAAEC,UAAU,QAAQ,mBAAmB;AAC7D,SAASC,aAAa,QAAQ,mBAAmB;AACjD,SAASC,YAAY,QAAQ,mBAAmB;AAEhD,MAAMC,OAAO,GAAGN,aAAa,CAACO,MAAM,CAACC,IAAI,CAACC,GAAG,CAAC;AAC9C,OAAO,MAAMC,WAAmB,GAAGJ,OAAO,CAAC,uBAAuB,CAAC,CAACK,OAAO;;AAE3E;;AA8BA;AACA;AACA;;AAEA,IAAIC,cAAsB,GAAGC,MAAM,CAACC,UAAU,CAAC,CAAC;AAChD,IAAIC,UAAU,GAAG,CAAC;;AAElB;AACA;AACA;AACA;AACA;AACA,IAAIC,mBAA2B,GAAGC,eAAe,CAAC,CAAC;AACnD,IAAIC,sBAA8B,GAAGC,cAAc,CAAC,CAAC;;AAErD;AACA;AACA;AACA,OAAO,SAASC,iBAAiBA,CAACC,EAAU,EAAQ;EAClDT,cAAc,GAAGS,EAAE;AACrB;;AAEA;AACA;AACA;AACA,OAAO,SAASC,iBAAiBA,CAAA,EAAW;EAC1C,OAAOV,cAAc;AACvB;;AAEA;AACA;AACA;AACA;AACA,OAAO,SAASW,sBAAsBA,CAAA,EAAW;EAC/C,OAAOP,mBAAmB;AAC5B;;AAEA;AACA;AACA;AACA,OAAO,SAASQ,yBAAyBA,CAAA,EAAW;EAClD,OAAON,sBAAsB;AAC/B;;AAEA;AACA;AACA;;AAOA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASO,yBAAyBA,CAAA,EAAwB;EAC/DT,mBAAmB,GAAGC,eAAe,CAAC,CAAC;EACvCC,sBAAsB,GAAGC,cAAc,CAAC,CAAC;EACzC,OAAO;IACLO,OAAO,EAAEV,mBAAmB;IAC5BW,UAAU,EAAET,sBAAsB;IAClCN;EACF,CAAC;AACH;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASgB,yBAAyBA,CAACC,UAAkB,EAAQ;EAClE;EACA;AAAA;;AAGF;AACA;AACA;;AAEA,OAAO,SAASZ,eAAeA,CAAA,EAAW;EACxC;EACA,OAAOJ,MAAM,CAACC,UAAU,CAAC,CAAC;AAC5B;AAEA,OAAO,SAASK,cAAcA,CAAA,EAAW;EACvC,MAAMW,KAAK,GAAG,IAAIC,UAAU,CAAC,CAAC,CAAC;EAC/BlB,MAAM,CAACmB,eAAe,CAACF,KAAK,CAAC;EAC7B,OAAOG,KAAK,CAACC,IAAI,CAACJ,KAAK,CAAC,CAACK,GAAG,CAACC,CAAC,IAAIA,CAAC,CAACC,QAAQ,CAAC,EAAE,CAAC,CAACC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAACC,IAAI,CAAC,EAAE,CAAC;AAC7E;;AAEA;AACA;AACA;;AAEA,OAAO,SAASC,QAAQA,CAAA,EAAW;EACjC,OAAO,EAAEzB,UAAU;AACrB;AAEA,OAAO,SAAS0B,iBAAiBA,CAACC,SAAqC,EAAoB;EACzF;EACA,MAAM;IAAEC,OAAO;IAAEC;EAAM,CAAC,GAAGzC,UAAU,CAAC,CAAC;EAEvC,OAAO;IACL0C,MAAM,EAAE,WAAW;IACnBC,WAAW,EAAE,YAAY;IACzBC,cAAc,EAAErC,WAAW;IAC3BE,cAAc;IACdG,UAAU;IACVW,OAAO,EAAEV,mBAAmB;IAC5BgC,MAAM,EAAE7B,cAAc,CAAC,CAAC;IACxB8B,YAAY,EAAE/B,sBAAsB;IACpCgC,UAAU,EAAE,CAAC;IACbC,MAAM,EAAER,OAAO;IACfS,SAAS,EAAER,KAAK;IAChB,GAAGF;EACL,CAAC;AACH;AAEA,OAAO,SAASW,aAAaA,CAAA,EAAW;EACtC,OAAOtC,UAAU;AACnB;;AAEA;AACA;AACA;AACA;;AAEA,MAAMuC,cAAc,GAAG,IAAI,CAAC,CAAE;AAC9B,MAAMC,SAAS,GAAG,EAAE,CAAC,CAAS;AAC9B,MAAMC,UAAqC,GAAG,EAAE;AAChD,IAAIC,UAAgD,GAAG,IAAI;AAC3D,IAAIC,sBAAsB,GAAG,KAAK;AAElC,SAASC,YAAYA,CAACC,IAA6B,EAAQ;EACzDJ,UAAU,CAACK,IAAI,CAACD,IAAI,CAAC;EAErB,IAAIJ,UAAU,CAACM,MAAM,IAAIP,SAAS,EAAE;IAClCQ,aAAa,CAAC,CAAC;EACjB,CAAC,MAAM,IAAI,CAACN,UAAU,EAAE;IACtBA,UAAU,GAAGO,UAAU,CAAC,MAAM;MAC5BP,UAAU,GAAG,IAAI;MACjBM,aAAa,CAAC,CAAC;IACjB,CAAC,EAAET,cAAc,CAAC;EACpB;AACF;;AAEA;AACA;AACA;AACA;AACA,OAAO,SAASS,aAAaA,CAAA,EAAS;EACpC,IAAIN,UAAU,EAAE;IACdQ,YAAY,CAACR,UAAU,CAAC;IACxBA,UAAU,GAAG,IAAI;EACnB;EAEA,IAAID,UAAU,CAACM,MAAM,KAAK,CAAC,EAAE;EAE7B,MAAMI,KAAK,GAAGV,UAAU,CAACW,MAAM,CAAC,CAAC,EAAEX,UAAU,CAACM,MAAM,CAAC;EACrD;EACAM,UAAU,CAACF,KAAK,CAAC,CAACG,KAAK,CAAEC,GAAG,IAAK;IAC/B,IAAIC,OAAO,CAACC,GAAG,CAACC,eAAe,EAAE;MAC/BF,OAAO,CAACG,MAAM,CAACC,KAAK,CAAC,4BAA4BL,GAAG,CAACM,OAAO,IAAI,CAAC;IACnE;EACF,CAAC,CAAC;AACJ;AAEA,eAAeR,UAAUA,CAACS,KAAgC,EAAiB;EACzE,MAAMC,MAAM,GAAG5E,aAAa,CAAC,CAAC;EAC9B,IAAI,CAAC4E,MAAM,CAACC,SAAS,EAAE;EAEvB,IAAIC,SAAS,GAAGF,MAAM,CAACG,WAAW;EAClC,IAAI,CAACD,SAAS,EAAE;IACdA,SAAS,GAAG,OAAM5E,aAAa,CAAC,CAAC,KAAI,EAAE;EACzC;EACA,IAAI,CAAC4E,SAAS,EAAE;EAEhB,MAAME,UAAU,GAAG/E,UAAU,CAAC,CAAC;EAE/B,MAAMgF,IAA6B,GAAG;IACpCC,IAAI,EAAE,kBAAkB;IACxBP,KAAK;IACLQ,eAAe,EAAEzE,cAAc;IAC/B0E,QAAQ,EAAER,MAAM,CAACS,OAAO,IAAIC,SAAS;IACrCrC,MAAM,EAAE+B,UAAU,CAACvC,OAAO,IAAI6C,SAAS;IACvCpC,SAAS,EAAE8B,UAAU,CAACtC,KAAK,IAAI4C,SAAS;IACxC3C,MAAM,EAAE;EACV,CAAC;;EAED;EACA,IAAI,CAACa,sBAAsB,EAAE;IAC3BA,sBAAsB,GAAG,IAAI;IAC7ByB,IAAI,CAACM,kBAAkB,GAAG,eAAe,IAAIC,IAAI,CAAC,CAAC,CAACC,WAAW,CAAC,CAAC,CAACC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE;IACjFT,IAAI,CAACU,QAAQ,GAAG5F,EAAE,CAAC4F,QAAQ,CAAC,CAAC;IAC7BV,IAAI,CAACxE,OAAO,GAAGD,WAAW;EAC5B;EAEA,IAAI;IACF,MAAMoF,QAAQ,GAAG,MAAMC,KAAK,CAACjB,MAAM,CAACC,SAAS,EAAE;MAC7CiB,MAAM,EAAE,MAAM;MACdC,OAAO,EAAE;QACP,cAAc,EAAE,kBAAkB;QAClC,eAAe,EAAE,UAAUjB,SAAS;MACtC,CAAC;MACDG,IAAI,EAAEe,IAAI,CAACC,SAAS,CAAChB,IAAI;IAC3B,CAAC,CAAC;IAEF,IAAI,CAACW,QAAQ,CAACM,EAAE,IAAI7B,OAAO,CAACC,GAAG,CAACC,eAAe,EAAE;MAC/C,MAAM4B,IAAI,GAAG,MAAMP,QAAQ,CAACO,IAAI,CAAC,CAAC,CAAChC,KAAK,CAAC,MAAM,EAAE,CAAC;MAClDE,OAAO,CAACG,MAAM,CAACC,KAAK,CAAC,8BAA8BmB,QAAQ,CAACQ,MAAM,MAAMD,IAAI,CAACE,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,CAAC;IACjG;EACF,CAAC,CAAC,OAAOjC,GAAG,EAAE;IACZ,IAAIC,OAAO,CAACC,GAAG,CAACC,eAAe,EAAE;MAC/BF,OAAO,CAACG,MAAM,CAACC,KAAK,CAAC,6BAA8BL,GAAG,CAAWM,OAAO,IAAI,CAAC;IAC/E;EACF;AACF;;AAEA;AACA;AACA;;AAYA,OAAO,SAAS4B,OAAOA,CAACC,IAAiB,EAAQ;EAC/C;EACAC,QAAQ,CAACD,IAAI,CAAC,CAACpC,KAAK,CAAEC,GAAG,IAAK;IAC5B,IAAIC,OAAO,CAACC,GAAG,CAACC,eAAe,EAAE;MAC/BF,OAAO,CAACG,MAAM,CAACC,KAAK,CAAC,qBAAqB8B,IAAI,CAACE,MAAM,KAAKrC,GAAG,CAACM,OAAO,IAAI,CAAC;IAC5E;EACF,CAAC,CAAC;AACJ;AAEA,eAAe8B,QAAQA,CAACD,IAAiB,EAAiB;EACxD,IAAIlC,OAAO,CAACC,GAAG,CAACC,eAAe,EAAE;IAC/BF,OAAO,CAACG,MAAM,CAACC,KAAK,CAAC,mCAAmC8B,IAAI,CAACE,MAAM,IAAI,CAAC;EAC1E;EAEA,MAAMC,GAAG,GAAGH,IAAI,CAACI,OAAO;EACxB,MAAMC,GAAG,GAAG,IAAIpB,IAAI,CAAC,CAAC;EACtB,MAAMqB,SAAS,GAAG,IAAIrB,IAAI,CAACoB,GAAG,CAACE,OAAO,CAAC,CAAC,GAAGP,IAAI,CAACQ,UAAU,CAAC,CAACtB,WAAW,CAAC,CAAC;EACzE,MAAMuB,OAAO,GAAGJ,GAAG,CAACnB,WAAW,CAAC,CAAC;;EAEjC;EACA,MAAM/B,IAA6B,GAAG;IACpC+C,MAAM,EAAEF,IAAI,CAACE,MAAM;IACnBQ,QAAQ,EAAEV,IAAI,CAACU,QAAQ,KAAKV,IAAI,CAACW,KAAK,GAAG,OAAO,GAAG,MAAM,CAAC;IAC1D9B,QAAQ,EAAEmB,IAAI,CAAClB,OAAO,IAAIrF,aAAa,CAAC,CAAC,CAACqF,OAAO,IAAI,IAAI;IACzD1C,MAAM,EAAE+D,GAAG,CAAC/D,MAAM,IAAI,WAAW;IACjCwE,YAAY,EAAET,GAAG,CAAC9D,WAAW,IAAI,YAAY;IAC7CwE,SAAS,EAAE,UAAU;IACrBC,WAAW,EAAEd,IAAI,CAACW,KAAK,GAAG,OAAO,GAAG,IAAI;IACxCI,QAAQ,EAAEZ,GAAG,CAAClF,OAAO,IAAI,IAAI;IAC7B+F,OAAO,EAAEb,GAAG,CAAC5D,MAAM,IAAI7B,cAAc,CAAC,CAAC;IACvCuG,cAAc,EAAEd,GAAG,CAAC3D,YAAY,IAAI,IAAI;IACxCoC,eAAe,EAAEuB,GAAG,CAAChG,cAAc,IAAIA,cAAc;IACrD+B,OAAO,EAAEiE,GAAG,CAACzD,MAAM,IAAI,IAAI;IAC3BwE,UAAU,EAAEf,GAAG,CAACxD,SAAS,IAAI,IAAI;IACjCwE,UAAU,EAAEb,SAAS;IACrBc,QAAQ,EAAEX,OAAO;IACjBY,WAAW,EAAErB,IAAI,CAACQ,UAAU;IAC5Bc,aAAa,EAAEtB,IAAI,CAACW,KAAK,IAAI,IAAI;IACjCY,OAAO,EAAE;MACP,GAAGvB,IAAI,CAACuB,OAAO;MACfC,YAAY,EAAErB,GAAG,CAACsB,WAAW;MAC7BC,aAAa,EAAEvB,GAAG,CAACwB,YAAY;MAC/BC,iBAAiB,EAAEzB,GAAG,CAAC0B,eAAe;MACtCC,qBAAqB,EAAE3B,GAAG,CAAC4B,mBAAmB;MAC9CC,UAAU,EAAE7B,GAAG,CAAC8B,SAAS;MACzBC,KAAK,EAAE/B,GAAG,CAAC+B,KAAK;MAChBC,WAAW,EAAEhC,GAAG,CAAC7F,UAAU;MAC3B8H,QAAQ,EAAEjC,GAAG,CAACkC,OAAO;MACrBC,UAAU,EAAEnC,GAAG,CAACoC,SAAS;MACzBC,SAAS,EAAErC,GAAG,CAACsC;IACjB;EACF,CAAC;;EAED;EACAvF,YAAY,CAACC,IAAI,CAAC;;EAElB;EACA,IAAI6C,IAAI,CAACW,KAAK,EAAE;IACd/G,YAAY,CAAC;MACX8I,SAAS,EAAE1C,IAAI,CAACE,MAAM;MACtByC,YAAY,EAAE3C,IAAI,CAACW,KAAK;MACxBD,QAAQ,EAAE,OAAO;MACjBzF,OAAO,EAAEkF,GAAG,CAAClF,OAAO;MACpBsB,MAAM,EAAE4D,GAAG,CAAC5D,MAAM;MAClBuC,OAAO,EAAEkB,IAAI,CAAClB,OAAO,IAAIrF,aAAa,CAAC,CAAC,CAACqF,OAAO;MAChDpC,MAAM,EAAEyD,GAAG,CAACzD,MAAM;MAClBC,SAAS,EAAEwD,GAAG,CAACxD,SAAS;MACxBiG,IAAI,EAAE;QAAE1C,MAAM,EAAEF,IAAI,CAACE;MAAO;IAC9B,CAAC,CAAC;EACJ;AACF","ignoreList":[]}
@@ -1,10 +1,13 @@
1
1
  /**
2
2
  * Real Supabase test helper for handler tests.
3
- * Replaces mock-supabase.ts — all operations hit the real database.
3
+ * All operations hit the real LOCAL database (never production).
4
4
  *
5
5
  * When SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY / TEST_STORE_ID are missing,
6
6
  * exports are stubbed so test files can skip gracefully via:
7
7
  * describe.skipIf(!HAS_TEST_DB)("...", () => { ... })
8
+ *
9
+ * Cleanup uses a direct Postgres connection for speed — one SQL round-trip
10
+ * instead of 44+ individual HTTP DELETE calls through PostgREST.
8
11
  */
9
12
  import { SupabaseClient } from "@supabase/supabase-js";
10
13
  /** true when all required env vars are present — use with describe.skipIf(!HAS_TEST_DB) */
@@ -20,8 +23,19 @@ export declare function seed(table: string, rows: Record<string, unknown>[]): Pr
20
23
  export declare function deleteByIds(table: string, ids: string[]): Promise<void>;
21
24
  /** Delete rows matching column=value */
22
25
  export declare function deleteWhere(table: string, column: string, value: unknown): Promise<void>;
23
- /** Upsert rows (for tables with unique constraints where insert would conflict) */
26
+ /** Upsert rows and track for cleanup */
24
27
  export declare function upsertTracked(table: string, rows: Record<string, unknown>[], onConflict: string): Promise<any[]>;
25
28
  export declare function seedTracked(table: string, rows: Record<string, unknown>[]): Promise<any[]>;
26
- /** Delete all tracked rows in FK-safe order */
29
+ /**
30
+ * Delete all tracked rows via a single Postgres transaction.
31
+ *
32
+ * Instead of 44+ individual HTTP DELETE calls through PostgREST,
33
+ * this sends one SQL statement directly to Postgres. ~10x faster,
34
+ * more reliable, and handles FK ordering via CASCADE-aware deletion.
35
+ */
27
36
  export declare function cleanup(): Promise<void>;
37
+ /**
38
+ * Shut down the Postgres pool. Call in globalTeardown or afterAll
39
+ * of the last test file to prevent open handles.
40
+ */
41
+ export declare function closePool(): Promise<void>;
@@ -1,18 +1,27 @@
1
1
  /**
2
2
  * Real Supabase test helper for handler tests.
3
- * Replaces mock-supabase.ts — all operations hit the real database.
3
+ * All operations hit the real LOCAL database (never production).
4
4
  *
5
5
  * When SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY / TEST_STORE_ID are missing,
6
6
  * exports are stubbed so test files can skip gracefully via:
7
7
  * describe.skipIf(!HAS_TEST_DB)("...", () => { ... })
8
+ *
9
+ * Cleanup uses a direct Postgres connection for speed — one SQL round-trip
10
+ * instead of 44+ individual HTTP DELETE calls through PostgREST.
8
11
  */
9
12
 
10
13
  import { createClient } from "@supabase/supabase-js";
14
+ import pg from "pg";
11
15
  import { randomUUID } from "node:crypto";
12
16
  const url = process.env.SUPABASE_URL;
13
17
  const key = process.env.SUPABASE_SERVICE_ROLE_KEY;
14
18
  const storeId = process.env.TEST_STORE_ID;
15
19
 
20
+ // Guard: refuse to run against production
21
+ if (url && !url.includes("127.0.0.1") && !url.includes("localhost")) {
22
+ throw new Error(`FATAL: SUPABASE_URL points to "${url}" which is NOT local. ` + `Tests must run against local Supabase (127.0.0.1:54321). ` + `Fix your .env.test file.`);
23
+ }
24
+
16
25
  /** true when all required env vars are present — use with describe.skipIf(!HAS_TEST_DB) */
17
26
  export const HAS_TEST_DB = !!(url && key && storeId);
18
27
  const sb = HAS_TEST_DB ? createClient(url, key) : null;
@@ -26,13 +35,36 @@ export function getTestClient() {
26
35
  return sb;
27
36
  }
28
37
 
38
+ // ---------------------------------------------------------------------------
39
+ // Direct Postgres pool for fast cleanup (bypasses PostgREST HTTP overhead)
40
+ // ---------------------------------------------------------------------------
41
+
42
+ const pgPool = HAS_TEST_DB ? new pg.Pool({
43
+ host: "127.0.0.1",
44
+ port: 54322,
45
+ database: "postgres",
46
+ user: "postgres",
47
+ password: "postgres",
48
+ max: 2
49
+ }) : null;
50
+
51
+ /** Timeout wrapper — prevents tests from hanging on unresponsive DB */
52
+ function withTimeout(promise, ms, label) {
53
+ return Promise.race([promise, new Promise((_, reject) => setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms))]);
54
+ }
55
+ const DB_TIMEOUT = 5_000;
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Seed helpers (still go through Supabase client for type consistency)
59
+ // ---------------------------------------------------------------------------
60
+
29
61
  /** Insert rows and return them with DB-generated fields */
30
62
  export async function seed(table, rows) {
31
63
  if (!rows.length) return [];
32
64
  const {
33
65
  data,
34
66
  error
35
- } = await sb.from(table).insert(rows).select();
67
+ } = await withTimeout(Promise.resolve(sb.from(table).insert(rows).select()), DB_TIMEOUT, `seed(${table})`);
36
68
  if (error) throw new Error(`seed(${table}): ${error.message}`);
37
69
  return data;
38
70
  }
@@ -42,7 +74,7 @@ export async function deleteByIds(table, ids) {
42
74
  if (!ids.length) return;
43
75
  const {
44
76
  error
45
- } = await sb.from(table).delete().in("id", ids);
77
+ } = await withTimeout(Promise.resolve(sb.from(table).delete().in("id", ids)), DB_TIMEOUT, `deleteByIds(${table})`);
46
78
  if (error) throw new Error(`deleteByIds(${table}): ${error.message}`);
47
79
  }
48
80
 
@@ -50,23 +82,36 @@ export async function deleteByIds(table, ids) {
50
82
  export async function deleteWhere(table, column, value) {
51
83
  const {
52
84
  error
53
- } = await sb.from(table).delete().eq(column, value);
85
+ } = await withTimeout(Promise.resolve(sb.from(table).delete().eq(column, value)), DB_TIMEOUT, `deleteWhere(${table})`);
54
86
  if (error) throw new Error(`deleteWhere(${table}): ${error.message}`);
55
87
  }
56
88
 
57
- /** Upsert rows (for tables with unique constraints where insert would conflict) */
89
+ /** Upsert rows and track for cleanup */
58
90
  export async function upsertTracked(table, rows, onConflict) {
59
91
  if (!rows.length) return [];
60
92
  const {
61
93
  data,
62
94
  error
63
- } = await sb.from(table).upsert(rows, {
95
+ } = await withTimeout(Promise.resolve(sb.from(table).upsert(rows, {
64
96
  onConflict
65
- }).select();
97
+ }).select()), DB_TIMEOUT, `upsert(${table})`);
66
98
  if (error) throw new Error(`upsert(${table}): ${error.message}`);
99
+ const ids = data.map(r => r.id);
100
+ tracked.set(table, [...(tracked.get(table) || []), ...ids]);
67
101
  return data;
68
102
  }
69
103
 
104
+ // ---------------------------------------------------------------------------
105
+ // Tracking and cleanup
106
+ // ---------------------------------------------------------------------------
107
+
108
+ /**
109
+ * Infrastructure tables that tests must NEVER delete from.
110
+ * Tests can INSERT into these (tracked), but cleanup skips them.
111
+ * This prevents accidental deletion of real API keys, store plans, stores, etc.
112
+ */
113
+ const PROTECTED_TABLES = new Set(["stores", "store_plans", "store_config", "store_prefetch_data"]);
114
+
70
115
  /** Track seeded data for automatic cleanup */
71
116
  const tracked = new Map();
72
117
  export async function seedTracked(table, rows) {
@@ -76,16 +121,70 @@ export async function seedTracked(table, rows) {
76
121
  return data;
77
122
  }
78
123
 
79
- /** Delete all tracked rows in FK-safe order */
124
+ /**
125
+ * Delete all tracked rows via a single Postgres transaction.
126
+ *
127
+ * Instead of 44+ individual HTTP DELETE calls through PostgREST,
128
+ * this sends one SQL statement directly to Postgres. ~10x faster,
129
+ * more reliable, and handles FK ordering via CASCADE-aware deletion.
130
+ */
80
131
  export async function cleanup() {
81
- const fkOrder = ["ai_messages", "ai_conversation_checkpoints", "ai_conversations", "ai_agent_config", "platform_secrets", "business_audit", "workflow_step_runs", "workflow_dlq", "workflow_runs", "workflow_versions", "workflow_steps", "workflows", "error_events", "payment_intents", "order_items", "cart_items", "webhook_events", "inventory_levels", "inventory", "email_sends", "orders", "carts", "products", "categories", "catalogs", "creation_collections", "customers", "store_customer_profiles", "user_creation_relationships", "creations", "locations", "webhook_configs", "webhook_endpoints", "api_keys", "platform_users", "store_media", "purchase_orders", "suppliers", "stock_transfers", "v_daily_sales", "meta_ads", "meta_ad_sets", "meta_campaigns", "channel_messages", "channels", "node_events", "nodes", "store_members", "store_plans", "store_usage", "email_templates", "email_threads", "customer_risk_scores", "customer_exposures", "customer_breach_records", "customer_enrichment_profiles", "stores"];
82
- const orderedTables = [...fkOrder.filter(t => tracked.has(t)), ...[...tracked.keys()].filter(t => !fkOrder.includes(t))];
83
- for (const table of orderedTables) {
84
- const ids = tracked.get(table);
85
- if (ids?.length) {
86
- await deleteByIds(table, ids).catch(() => {});
132
+ if (!tracked.size) return;
133
+
134
+ // Filter out protected tables
135
+ const tablesToClean = [...tracked.entries()].filter(([table]) => !PROTECTED_TABLES.has(table));
136
+ if (!tablesToClean.length) {
137
+ tracked.clear();
138
+ return;
139
+ }
140
+
141
+ // Fast path: single Postgres transaction with all DELETEs
142
+ if (pgPool) {
143
+ const client = await pgPool.connect();
144
+ try {
145
+ // FK-safe order: children before parents
146
+ const fkOrder = ["ai_messages", "ai_conversation_checkpoints", "ai_conversations", "ai_agent_config", "platform_secrets", "business_audit", "workflow_step_runs", "workflow_dlq", "workflow_runs", "workflow_versions", "workflow_steps", "workflows", "error_events", "payment_intents", "order_items", "cart_items", "webhook_events", "inventory_levels", "inventory", "email_sends", "orders", "carts", "products", "categories", "catalogs", "creation_collections", "customers", "store_customer_profiles", "user_creation_relationships", "creations", "locations", "webhook_configs", "webhook_endpoints", "api_keys", "platform_users", "store_media", "purchase_orders", "suppliers", "stock_transfers", "v_daily_sales", "meta_ads", "meta_ad_sets", "meta_campaigns", "channel_messages", "channels", "node_events", "nodes", "store_members", "store_usage", "email_templates", "email_threads", "customer_risk_scores", "customer_exposures", "customer_breach_records", "customer_enrichment_profiles"];
147
+ const ordered = [...fkOrder.filter(t => tablesToClean.some(([table]) => table === t)), ...tablesToClean.filter(([table]) => !fkOrder.includes(table)).map(([t]) => t)];
148
+
149
+ // Build a single transaction with all DELETEs
150
+ const statements = ["BEGIN;"];
151
+ let paramIdx = 1;
152
+ const allParams = [];
153
+ for (const table of ordered) {
154
+ const ids = tracked.get(table);
155
+ if (!ids?.length) continue;
156
+ const placeholders = ids.map(() => `$${paramIdx++}`);
157
+ statements.push(`DELETE FROM "${table}" WHERE id IN (${placeholders.join(",")});`);
158
+ allParams.push(...ids);
159
+ }
160
+ statements.push("COMMIT;");
161
+ await withTimeout(client.query(statements.join("\n"), allParams), DB_TIMEOUT, "cleanup(batch)");
162
+ } catch {
163
+ // Swallow cleanup errors — same as before, tests shouldn't fail on cleanup
164
+ try {
165
+ await client.query("ROLLBACK;");
166
+ } catch {/* ignore */}
167
+ } finally {
168
+ client.release();
169
+ }
170
+ } else {
171
+ // Fallback: individual Supabase calls (slower, for environments without pg)
172
+ for (const [table, ids] of tablesToClean) {
173
+ if (ids?.length) {
174
+ await withTimeout(deleteByIds(table, ids), DB_TIMEOUT, `cleanup(${table})`).catch(() => {});
175
+ }
87
176
  }
88
177
  }
89
178
  tracked.clear();
90
179
  }
180
+
181
+ /**
182
+ * Shut down the Postgres pool. Call in globalTeardown or afterAll
183
+ * of the last test file to prevent open handles.
184
+ */
185
+ export async function closePool() {
186
+ if (pgPool) {
187
+ await pgPool.end();
188
+ }
189
+ }
91
190
  //# sourceMappingURL=test-db.js.map