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.
- package/dist/cli/services/agent-loop.js +26 -2
- package/dist/cli/services/agent-loop.js.map +1 -1
- package/dist/cli/services/hooks.js +2 -1
- package/dist/cli/services/hooks.js.map +1 -1
- package/dist/cli/services/telemetry-spans.js +1 -0
- package/dist/cli/services/telemetry-spans.js.map +1 -1
- package/dist/cli/services/telemetry.d.ts +23 -0
- package/dist/cli/services/telemetry.js +45 -1
- package/dist/cli/services/telemetry.js.map +1 -1
- package/dist/server/handlers/__test-utils__/test-db.d.ts +17 -3
- package/dist/server/handlers/__test-utils__/test-db.js +113 -14
- package/dist/server/handlers/__test-utils__/test-db.js.map +1 -1
- package/dist/server/handlers/affiliates.d.ts +9 -0
- package/dist/server/handlers/affiliates.js +197 -0
- package/dist/server/handlers/affiliates.js.map +1 -0
- package/dist/server/handlers/api-docs.d.ts +4 -2
- package/dist/server/handlers/api-docs.js +204 -1681
- package/dist/server/handlers/api-docs.js.map +1 -1
- package/dist/server/handlers/campaigns.d.ts +9 -0
- package/dist/server/handlers/campaigns.js +237 -0
- package/dist/server/handlers/campaigns.js.map +1 -0
- package/dist/server/handlers/catalog-schemas.js +9 -9
- package/dist/server/handlers/catalog-schemas.js.map +1 -1
- package/dist/server/handlers/catalog.js +1 -1
- package/dist/server/handlers/catalog.js.map +1 -1
- package/dist/server/handlers/comms-documents.js +28 -2
- package/dist/server/handlers/comms-documents.js.map +1 -1
- package/dist/server/handlers/comms-pdf-generation.js +25 -3
- package/dist/server/handlers/comms-pdf-generation.js.map +1 -1
- package/dist/server/handlers/comms-pdf-helpers.js +4 -4
- package/dist/server/handlers/comms-pdf-helpers.js.map +1 -1
- package/dist/server/handlers/comms.d.ts +100 -0
- package/dist/server/handlers/comms.js +146 -12
- package/dist/server/handlers/comms.js.map +1 -1
- package/dist/server/handlers/coupons.d.ts +9 -0
- package/dist/server/handlers/coupons.js +220 -0
- package/dist/server/handlers/coupons.js.map +1 -0
- package/dist/server/handlers/embeddings.js +1 -1
- package/dist/server/handlers/embeddings.js.map +1 -1
- package/dist/server/handlers/enrichment.js +2 -622
- package/dist/server/handlers/enrichment.js.map +1 -1
- package/dist/server/handlers/fulfillment.d.ts +9 -0
- package/dist/server/handlers/fulfillment.js +209 -0
- package/dist/server/handlers/fulfillment.js.map +1 -0
- package/dist/server/handlers/google-ads.d.ts +24 -0
- package/dist/server/handlers/google-ads.js +2199 -0
- package/dist/server/handlers/google-ads.js.map +1 -0
- package/dist/server/handlers/invoices.d.ts +9 -0
- package/dist/server/handlers/invoices.js +252 -0
- package/dist/server/handlers/invoices.js.map +1 -0
- package/dist/server/handlers/loyalty.d.ts +9 -0
- package/dist/server/handlers/loyalty.js +197 -0
- package/dist/server/handlers/loyalty.js.map +1 -0
- package/dist/server/handlers/meta-ads-graph-api.js +18 -3
- package/dist/server/handlers/meta-ads-graph-api.js.map +1 -1
- package/dist/server/handlers/phone.d.ts +9 -0
- package/dist/server/handlers/phone.js +197 -0
- package/dist/server/handlers/phone.js.map +1 -0
- package/dist/server/handlers/pipeline.d.ts +9 -0
- package/dist/server/handlers/pipeline.js +277 -0
- package/dist/server/handlers/pipeline.js.map +1 -0
- package/dist/server/handlers/qr-codes.d.ts +9 -0
- package/dist/server/handlers/qr-codes.js +198 -0
- package/dist/server/handlers/qr-codes.js.map +1 -0
- package/dist/server/handlers/reviews.d.ts +9 -0
- package/dist/server/handlers/reviews.js +171 -0
- package/dist/server/handlers/reviews.js.map +1 -0
- package/dist/server/handlers/segments.d.ts +9 -0
- package/dist/server/handlers/segments.js +229 -0
- package/dist/server/handlers/segments.js.map +1 -0
- package/dist/server/handlers/social.d.ts +9 -0
- package/dist/server/handlers/social.js +81 -0
- package/dist/server/handlers/social.js.map +1 -0
- package/dist/server/handlers/tax.d.ts +9 -0
- package/dist/server/handlers/tax.js +182 -0
- package/dist/server/handlers/tax.js.map +1 -0
- package/dist/server/handlers/wallet.d.ts +9 -0
- package/dist/server/handlers/wallet.js +203 -0
- package/dist/server/handlers/wallet.js.map +1 -0
- package/dist/server/handlers/webhooks-mgmt.d.ts +9 -0
- package/dist/server/handlers/webhooks-mgmt.js +181 -0
- package/dist/server/handlers/webhooks-mgmt.js.map +1 -0
- package/dist/server/handlers/wholesale.d.ts +9 -0
- package/dist/server/handlers/wholesale.js +219 -0
- package/dist/server/handlers/wholesale.js.map +1 -0
- package/dist/server/index.js +20 -9
- package/dist/server/index.js.map +1 -1
- package/dist/server/lib/clickhouse-buffer.js +1 -0
- package/dist/server/lib/clickhouse-buffer.js.map +1 -1
- package/dist/server/lib/coa-renderer.d.ts +1 -1
- package/dist/server/lib/coa-renderer.js +32 -10
- package/dist/server/lib/coa-renderer.js.map +1 -1
- package/dist/server/server-worker.d.ts +1 -0
- package/dist/server/server-worker.js +464 -3
- package/dist/server/server-worker.js.map +1 -1
- package/dist/server/tool-router.js +118 -4
- package/dist/server/tool-router.js.map +1 -1
- package/package.json +26 -3
- 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:
|
|
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":"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","ignoreList":[]}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Real Supabase test helper for handler tests.
|
|
3
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|