whale-code 6.5.8 → 6.5.10

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 (101) 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/config-store.js +2 -3
  4. package/dist/cli/services/config-store.js.map +1 -1
  5. package/dist/cli/services/hooks.js +2 -1
  6. package/dist/cli/services/hooks.js.map +1 -1
  7. package/dist/cli/services/telemetry-spans.js +1 -0
  8. package/dist/cli/services/telemetry-spans.js.map +1 -1
  9. package/dist/cli/services/telemetry.d.ts +23 -0
  10. package/dist/cli/services/telemetry.js +45 -1
  11. package/dist/cli/services/telemetry.js.map +1 -1
  12. package/dist/server/handlers/__test-utils__/test-db.d.ts +17 -3
  13. package/dist/server/handlers/__test-utils__/test-db.js +113 -14
  14. package/dist/server/handlers/__test-utils__/test-db.js.map +1 -1
  15. package/dist/server/handlers/affiliates.d.ts +9 -0
  16. package/dist/server/handlers/affiliates.js +197 -0
  17. package/dist/server/handlers/affiliates.js.map +1 -0
  18. package/dist/server/handlers/api-docs.d.ts +4 -2
  19. package/dist/server/handlers/api-docs.js +204 -1681
  20. package/dist/server/handlers/api-docs.js.map +1 -1
  21. package/dist/server/handlers/campaigns.d.ts +9 -0
  22. package/dist/server/handlers/campaigns.js +237 -0
  23. package/dist/server/handlers/campaigns.js.map +1 -0
  24. package/dist/server/handlers/catalog-schemas.js +9 -9
  25. package/dist/server/handlers/catalog-schemas.js.map +1 -1
  26. package/dist/server/handlers/catalog.js +1 -1
  27. package/dist/server/handlers/catalog.js.map +1 -1
  28. package/dist/server/handlers/comms-documents.js +28 -2
  29. package/dist/server/handlers/comms-documents.js.map +1 -1
  30. package/dist/server/handlers/comms-pdf-generation.js +25 -3
  31. package/dist/server/handlers/comms-pdf-generation.js.map +1 -1
  32. package/dist/server/handlers/comms-pdf-helpers.js +4 -4
  33. package/dist/server/handlers/comms-pdf-helpers.js.map +1 -1
  34. package/dist/server/handlers/comms.d.ts +100 -0
  35. package/dist/server/handlers/comms.js +146 -12
  36. package/dist/server/handlers/comms.js.map +1 -1
  37. package/dist/server/handlers/coupons.d.ts +9 -0
  38. package/dist/server/handlers/coupons.js +220 -0
  39. package/dist/server/handlers/coupons.js.map +1 -0
  40. package/dist/server/handlers/embeddings.js +1 -1
  41. package/dist/server/handlers/embeddings.js.map +1 -1
  42. package/dist/server/handlers/enrichment.js +2 -622
  43. package/dist/server/handlers/enrichment.js.map +1 -1
  44. package/dist/server/handlers/fulfillment.d.ts +9 -0
  45. package/dist/server/handlers/fulfillment.js +209 -0
  46. package/dist/server/handlers/fulfillment.js.map +1 -0
  47. package/dist/server/handlers/google-ads.d.ts +24 -0
  48. package/dist/server/handlers/google-ads.js +2199 -0
  49. package/dist/server/handlers/google-ads.js.map +1 -0
  50. package/dist/server/handlers/invoices.d.ts +9 -0
  51. package/dist/server/handlers/invoices.js +252 -0
  52. package/dist/server/handlers/invoices.js.map +1 -0
  53. package/dist/server/handlers/loyalty.d.ts +9 -0
  54. package/dist/server/handlers/loyalty.js +197 -0
  55. package/dist/server/handlers/loyalty.js.map +1 -0
  56. package/dist/server/handlers/meta-ads-graph-api.js +18 -3
  57. package/dist/server/handlers/meta-ads-graph-api.js.map +1 -1
  58. package/dist/server/handlers/phone.d.ts +9 -0
  59. package/dist/server/handlers/phone.js +197 -0
  60. package/dist/server/handlers/phone.js.map +1 -0
  61. package/dist/server/handlers/pipeline.d.ts +9 -0
  62. package/dist/server/handlers/pipeline.js +277 -0
  63. package/dist/server/handlers/pipeline.js.map +1 -0
  64. package/dist/server/handlers/qr-codes.d.ts +9 -0
  65. package/dist/server/handlers/qr-codes.js +198 -0
  66. package/dist/server/handlers/qr-codes.js.map +1 -0
  67. package/dist/server/handlers/reviews.d.ts +9 -0
  68. package/dist/server/handlers/reviews.js +171 -0
  69. package/dist/server/handlers/reviews.js.map +1 -0
  70. package/dist/server/handlers/segments.d.ts +9 -0
  71. package/dist/server/handlers/segments.js +229 -0
  72. package/dist/server/handlers/segments.js.map +1 -0
  73. package/dist/server/handlers/social.d.ts +9 -0
  74. package/dist/server/handlers/social.js +81 -0
  75. package/dist/server/handlers/social.js.map +1 -0
  76. package/dist/server/handlers/tax.d.ts +9 -0
  77. package/dist/server/handlers/tax.js +182 -0
  78. package/dist/server/handlers/tax.js.map +1 -0
  79. package/dist/server/handlers/wallet.d.ts +9 -0
  80. package/dist/server/handlers/wallet.js +203 -0
  81. package/dist/server/handlers/wallet.js.map +1 -0
  82. package/dist/server/handlers/webhooks-mgmt.d.ts +9 -0
  83. package/dist/server/handlers/webhooks-mgmt.js +181 -0
  84. package/dist/server/handlers/webhooks-mgmt.js.map +1 -0
  85. package/dist/server/handlers/wholesale.d.ts +9 -0
  86. package/dist/server/handlers/wholesale.js +219 -0
  87. package/dist/server/handlers/wholesale.js.map +1 -0
  88. package/dist/server/index.js +20 -9
  89. package/dist/server/index.js.map +1 -1
  90. package/dist/server/lib/clickhouse-buffer.js +1 -0
  91. package/dist/server/lib/clickhouse-buffer.js.map +1 -1
  92. package/dist/server/lib/coa-renderer.d.ts +1 -1
  93. package/dist/server/lib/coa-renderer.js +32 -10
  94. package/dist/server/lib/coa-renderer.js.map +1 -1
  95. package/dist/server/server-worker.d.ts +1 -0
  96. package/dist/server/server-worker.js +464 -3
  97. package/dist/server/server-worker.js.map +1 -1
  98. package/dist/server/tool-router.js +118 -4
  99. package/dist/server/tool-router.js.map +1 -1
  100. package/package.json +26 -3
  101. package/vendor/ink/package.json +0 -2
@@ -1 +1 @@
1
- {"version":3,"file":"config-store.js","names":["existsSync","mkdirSync","readFileSync","writeFileSync","unlinkSync","homedir","join","CONFIG_DIR","SESSION_PATH","PREFS_PATH","LEGACY_CONFIG_DIR","LEGACY_CONFIG_PATH","migrationChecked","ensureMigration","legacy","JSON","parse","recursive","mode","access_token","refresh_token","session","user_id","email","store_id","store_name","expires_at","stringify","encoding","prefs","default_model","thinking_enabled","undefined","permission_mode","platform_url","Object","keys","length","loadConfig","loadPreferences","err","console","error","Error","message","saveConfig","config","authFields","version","stores","updated_at","Math","floor","Date","now","newPrefs","savePreferences","updateConfig","partial","prefUpdates","existingSession","clearConfig","WHALE_SERVER_URL","DEFAULT_SUPABASE_URL","resolveConfig","file","supabaseUrl","process","env","SUPABASE_URL","supabase_url","supabaseKey","SUPABASE_SERVICE_ROLE_KEY","supabase_key","storeId","STORE_ID","anthropicApiKey","ANTHROPIC_API_KEY","anthropic_api_key","defaultAgentId","default_agent_id","serverUrl","platformUrl","WHALETOOLS_PLATFORM_URL","getConfigPath","getProxyUrl","DEFAULT_SUPABASE_ANON_KEY","resolveDefaultAgentId","_serverUrl","getToken","createClient","token","anonKey","SUPABASE_ANON_KEY","supabase","global","headers","Authorization","data","from","select","eq","order","ascending","limit","single","id"],"sources":["../../../src/cli/services/config-store.ts"],"sourcesContent":["/**\n * Config Store\n *\n * Unified auth session at ~/.whaletools/session.json\n * User preferences at ~/.whaletools/preferences.json\n *\n * v2.0: Raw Supabase/Anthropic keys (for MCP server env vars)\n * v2.1: Auth tokens from login flow (for CLI chat/status)\n * v4.0: Shared auth with Swift apps via ~/.whaletools/session.json\n *\n * Environment variables always override file-based config for MCP server mode.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\n\n// ============================================================================\n// TYPES\n// ============================================================================\n\nexport interface WhaleConfig {\n // v2.0 — MCP server mode (env var override)\n supabase_url?: string;\n supabase_key?: string; // service role key for MCP server\n anthropic_api_key?: string; // only used by MCP server setup\n default_agent_id?: string;\n\n // v2.1 — Auth mode (login flow)\n access_token?: string;\n refresh_token?: string;\n user_id?: string;\n email?: string;\n store_id?: string;\n store_name?: string;\n expires_at?: number; // unix epoch seconds\n\n // v3.0 — User preferences (persisted across sessions)\n default_model?: string; // model alias e.g. \"opus\", \"gemini-3-pro\"\n thinking_enabled?: boolean; // extended thinking toggle\n permission_mode?: string; // \"default\" | \"plan\" | \"yolo\"\n\n // v3.1 — Local agent (auto-spawned from MCP server)\n agent_api_key?: string; // store API key for local agent WebSocket auth\n\n // v3.2 — Browser OAuth\n platform_url?: string; // whaletools.dev override (dev/staging)\n\n // v4.0 — Multi-store support (from login flow)\n stores?: Array<{ id: string; name: string; role?: string }>;\n}\n\n/** Preferences that survive sign-out (stored in preferences.json) */\nexport interface WhalePreferences {\n default_model?: string;\n thinking_enabled?: boolean;\n permission_mode?: string;\n platform_url?: string;\n theme?: string;\n}\n\n// ============================================================================\n// PATHS\n// ============================================================================\n\nconst CONFIG_DIR = join(homedir(), \".whaletools\");\nconst SESSION_PATH = join(CONFIG_DIR, \"session.json\");\nconst PREFS_PATH = join(CONFIG_DIR, \"preferences.json\");\n\n// Legacy paths for migration\nconst LEGACY_CONFIG_DIR = join(homedir(), \".swagmanager\");\nconst LEGACY_CONFIG_PATH = join(LEGACY_CONFIG_DIR, \"config.json\");\n\n// ============================================================================\n// AUTO-MIGRATION from ~/.swagmanager to ~/.whaletools\n// ============================================================================\n\nlet migrationChecked = false;\n\nfunction ensureMigration(): void {\n if (migrationChecked) return;\n migrationChecked = true;\n\n // Skip if new session already exists\n if (existsSync(SESSION_PATH)) return;\n\n // Check for legacy config\n if (!existsSync(LEGACY_CONFIG_PATH)) return;\n\n try {\n const legacy: WhaleConfig = JSON.parse(readFileSync(LEGACY_CONFIG_PATH, \"utf-8\"));\n\n if (!existsSync(CONFIG_DIR)) {\n mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });\n }\n\n // Split auth fields into session.json\n if (legacy.access_token && legacy.refresh_token) {\n const session: Partial<WhaleConfig> = {\n access_token: legacy.access_token,\n refresh_token: legacy.refresh_token,\n user_id: legacy.user_id,\n email: legacy.email,\n store_id: legacy.store_id,\n store_name: legacy.store_name,\n expires_at: legacy.expires_at,\n };\n writeFileSync(SESSION_PATH, JSON.stringify(session, null, 2) + \"\\n\", {\n encoding: \"utf-8\",\n mode: 0o600,\n });\n }\n\n // Split preference fields into preferences.json\n const prefs: WhalePreferences = {};\n if (legacy.default_model) prefs.default_model = legacy.default_model;\n if (legacy.thinking_enabled !== undefined) prefs.thinking_enabled = legacy.thinking_enabled;\n if (legacy.permission_mode) prefs.permission_mode = legacy.permission_mode;\n if (legacy.platform_url) prefs.platform_url = legacy.platform_url;\n\n if (Object.keys(prefs).length > 0) {\n writeFileSync(PREFS_PATH, JSON.stringify(prefs, null, 2) + \"\\n\", {\n encoding: \"utf-8\",\n mode: 0o600,\n });\n }\n } catch {\n // Migration failed — not fatal, user can re-login\n }\n}\n\n// ============================================================================\n// READ / WRITE — session.json (auth tokens + store)\n// ============================================================================\n\nexport function loadConfig(): WhaleConfig {\n ensureMigration();\n try {\n if (existsSync(SESSION_PATH)) {\n const session = JSON.parse(readFileSync(SESSION_PATH, \"utf-8\"));\n // Merge preferences so callers see a unified config\n const prefs = loadPreferences();\n return { ...prefs, ...session };\n }\n } catch (err) {\n console.error(`[config] Warning: Failed to parse ${SESSION_PATH}: ${err instanceof Error ? err.message : err}`);\n console.error(\"[config] Using empty config. You may need to re-login with: whale login\");\n }\n // Even if no session, return preferences\n return { ...loadPreferences() };\n}\n\nexport function saveConfig(config: WhaleConfig): void {\n if (!existsSync(CONFIG_DIR)) {\n mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });\n }\n\n // Split: auth fields go to session.json, prefs go to preferences.json\n const { default_model, thinking_enabled, permission_mode, platform_url, ...authFields } = config;\n // Ensure fields required by Swift WhaleSession are present\n const session = {\n version: 2,\n ...authFields,\n stores: authFields.stores ?? [],\n updated_at: Math.floor(Date.now() / 1000),\n };\n writeFileSync(SESSION_PATH, JSON.stringify(session, null, 2) + \"\\n\", {\n encoding: \"utf-8\",\n mode: 0o600,\n });\n\n // Update preferences if any pref fields are present\n const newPrefs: WhalePreferences = {};\n if (default_model !== undefined) newPrefs.default_model = default_model;\n if (thinking_enabled !== undefined) newPrefs.thinking_enabled = thinking_enabled;\n if (permission_mode !== undefined) newPrefs.permission_mode = permission_mode;\n if (platform_url !== undefined) newPrefs.platform_url = platform_url;\n if (Object.keys(newPrefs).length > 0) {\n savePreferences({ ...loadPreferences(), ...newPrefs });\n }\n}\n\nexport function updateConfig(partial: Partial<WhaleConfig>): void {\n // Only write the fields that are actually being changed.\n // Split into auth fields (session.json) and pref fields (preferences.json)\n // to avoid stale in-memory values overwriting the file.\n const { default_model, thinking_enabled, permission_mode, platform_url, ...authFields } = partial;\n\n // Update preferences.json — merge only the provided pref fields\n const prefUpdates: WhalePreferences = {};\n if (default_model !== undefined) prefUpdates.default_model = default_model;\n if (thinking_enabled !== undefined) prefUpdates.thinking_enabled = thinking_enabled;\n if (permission_mode !== undefined) prefUpdates.permission_mode = permission_mode;\n if (platform_url !== undefined) prefUpdates.platform_url = platform_url;\n if (Object.keys(prefUpdates).length > 0) {\n savePreferences({ ...loadPreferences(), ...prefUpdates });\n }\n\n // Update session.json — merge only the provided auth fields\n if (Object.keys(authFields).length > 0) {\n const existingSession = (() => {\n try {\n if (existsSync(SESSION_PATH)) return JSON.parse(readFileSync(SESSION_PATH, \"utf-8\"));\n } catch { /* ignore */ }\n return {};\n })();\n const session = { version: 2, ...existingSession, ...authFields, updated_at: Math.floor(Date.now() / 1000) };\n if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });\n writeFileSync(SESSION_PATH, JSON.stringify(session, null, 2) + \"\\n\", { encoding: \"utf-8\", mode: 0o600 });\n }\n}\n\nexport function clearConfig(): void {\n try {\n if (existsSync(SESSION_PATH)) unlinkSync(SESSION_PATH);\n } catch { /* ignore */ }\n // Preferences are preserved across sign-out\n}\n\n// ============================================================================\n// READ / WRITE — preferences.json (survives sign-out)\n// ============================================================================\n\nexport function loadPreferences(): WhalePreferences {\n try {\n if (existsSync(PREFS_PATH)) {\n return JSON.parse(readFileSync(PREFS_PATH, \"utf-8\"));\n }\n } catch { /* ignore */ }\n return {};\n}\n\nexport function savePreferences(prefs: WhalePreferences): void {\n if (!existsSync(CONFIG_DIR)) {\n mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });\n }\n writeFileSync(PREFS_PATH, JSON.stringify(prefs, null, 2) + \"\\n\", {\n encoding: \"utf-8\",\n mode: 0o600,\n });\n}\n\n// ============================================================================\n// RESOLVED CONFIG (env vars override file — used by MCP server)\n// ============================================================================\n\nexport interface ResolvedConfig {\n supabaseUrl: string;\n supabaseKey: string;\n storeId: string;\n anthropicApiKey: string;\n defaultAgentId: string;\n serverUrl: string;\n platformUrl: string;\n}\n\n/** Default Fly.io agent server URL */\nexport const WHALE_SERVER_URL = \"https://whale-agent.fly.dev\";\n\n/** Default Supabase URL — fallback when env var and config are both empty */\nconst DEFAULT_SUPABASE_URL = \"https://uaednwpxursknmwdeejn.supabase.co\";\n\nexport function resolveConfig(): ResolvedConfig {\n const file = loadConfig();\n return {\n supabaseUrl: process.env.SUPABASE_URL || file.supabase_url || DEFAULT_SUPABASE_URL,\n // User JWT preferred over legacy service role key from config file;\n // env var SUPABASE_SERVICE_ROLE_KEY still wins (explicit MCP setup)\n supabaseKey: process.env.SUPABASE_SERVICE_ROLE_KEY || file.access_token || file.supabase_key || \"\",\n storeId: process.env.STORE_ID || file.store_id || \"\",\n anthropicApiKey: process.env.ANTHROPIC_API_KEY || file.anthropic_api_key || \"\",\n defaultAgentId: file.default_agent_id || \"\",\n serverUrl: process.env.WHALE_SERVER_URL || WHALE_SERVER_URL,\n platformUrl: process.env.WHALETOOLS_PLATFORM_URL || file.platform_url || \"https://whaletools.dev\",\n };\n}\n\nexport function getConfigPath(): string {\n return SESSION_PATH;\n}\n\n/** Lazy proxy URL — avoids reading config at import time */\nexport function getProxyUrl(): string {\n return resolveConfig().serverUrl;\n}\n\n/** Default Supabase anon key — used for client-side queries */\nconst DEFAULT_SUPABASE_ANON_KEY = \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InVhZWRud3B4dXJza25td2RlZWpuIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjA5OTcyMzMsImV4cCI6MjA3NjU3MzIzM30.N8jPwlyCBB5KJB5I-XaK6m-mq88rSR445AWFJJmwRCg\";\n\n/**\n * Resolve default agent ID — queries ai_agent_config for the store's first\n * active agent if not already configured, then caches the result.\n */\nexport async function resolveDefaultAgentId(\n _serverUrl: string,\n storeId: string,\n getToken: () => Promise<string | null>,\n): Promise<string | null> {\n // Check cached value first\n const config = loadConfig();\n if (config.default_agent_id) return config.default_agent_id;\n\n try {\n const { createClient } = await import(\"@supabase/supabase-js\");\n const token = await getToken();\n if (!token) return null;\n\n const supabaseUrl = process.env.SUPABASE_URL || config.supabase_url || DEFAULT_SUPABASE_URL;\n const anonKey = process.env.SUPABASE_ANON_KEY || DEFAULT_SUPABASE_ANON_KEY;\n const supabase = createClient(supabaseUrl, anonKey, {\n global: { headers: { Authorization: `Bearer ${token}` } },\n });\n\n const { data } = await supabase\n .from(\"ai_agent_config\")\n .select(\"id\")\n .eq(\"store_id\", storeId)\n .eq(\"is_active\", true)\n .order(\"created_at\", { ascending: true })\n .limit(1)\n .single();\n\n if (!data?.id) return null;\n\n // Cache for next time\n updateConfig({ default_agent_id: data.id });\n return data.id;\n } catch {\n return null;\n }\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"file":"config-store.js","names":["existsSync","mkdirSync","readFileSync","writeFileSync","unlinkSync","homedir","join","CONFIG_DIR","SESSION_PATH","PREFS_PATH","LEGACY_CONFIG_DIR","LEGACY_CONFIG_PATH","migrationChecked","ensureMigration","legacy","JSON","parse","recursive","mode","access_token","refresh_token","session","user_id","email","store_id","store_name","expires_at","stringify","encoding","prefs","default_model","thinking_enabled","undefined","permission_mode","platform_url","Object","keys","length","loadConfig","loadPreferences","err","console","error","Error","message","saveConfig","config","authFields","version","stores","updated_at","Math","floor","Date","now","newPrefs","savePreferences","updateConfig","partial","prefUpdates","existingSession","clearConfig","WHALE_SERVER_URL","DEFAULT_SUPABASE_URL","resolveConfig","file","supabaseUrl","process","env","SUPABASE_URL","supabase_url","supabaseKey","SUPABASE_SERVICE_ROLE_KEY","supabase_key","storeId","STORE_ID","anthropicApiKey","ANTHROPIC_API_KEY","anthropic_api_key","defaultAgentId","default_agent_id","serverUrl","platformUrl","WHALETOOLS_PLATFORM_URL","getConfigPath","getProxyUrl","DEFAULT_SUPABASE_ANON_KEY","resolveDefaultAgentId","_serverUrl","getToken","createClient","token","anonKey","SUPABASE_ANON_KEY","supabase","global","headers","Authorization","data","from","select","eq","order","ascending","limit","single","id"],"sources":["../../../src/cli/services/config-store.ts"],"sourcesContent":["/**\n * Config Store\n *\n * Unified auth session at ~/.whaletools/session.json\n * User preferences at ~/.whaletools/preferences.json\n *\n * v2.0: Raw Supabase/Anthropic keys (for MCP server env vars)\n * v2.1: Auth tokens from login flow (for CLI chat/status)\n * v4.0: Shared auth with Swift apps via ~/.whaletools/session.json\n *\n * Environment variables always override file-based config for MCP server mode.\n */\n\nimport { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from \"fs\";\nimport { homedir } from \"os\";\nimport { join } from \"path\";\n\n// ============================================================================\n// TYPES\n// ============================================================================\n\nexport interface WhaleConfig {\n // v2.0 — MCP server mode (env var override)\n supabase_url?: string;\n supabase_key?: string; // service role key for MCP server\n anthropic_api_key?: string; // only used by MCP server setup\n default_agent_id?: string;\n\n // v2.1 — Auth mode (login flow)\n access_token?: string;\n refresh_token?: string;\n user_id?: string;\n email?: string;\n store_id?: string;\n store_name?: string;\n expires_at?: number; // unix epoch seconds\n\n // v3.0 — User preferences (persisted across sessions)\n default_model?: string; // model alias e.g. \"opus\", \"gemini-3-pro\"\n thinking_enabled?: boolean; // extended thinking toggle\n permission_mode?: string; // \"default\" | \"plan\" | \"yolo\"\n\n // v3.1 — Local agent (auto-spawned from MCP server)\n agent_api_key?: string; // store API key for local agent WebSocket auth\n\n // v3.2 — Browser OAuth\n platform_url?: string; // whaletools.dev override (dev/staging)\n\n // v4.0 — Multi-store support (from login flow)\n stores?: Array<{ id: string; name: string; role?: string }>;\n}\n\n/** Preferences that survive sign-out (stored in preferences.json) */\nexport interface WhalePreferences {\n default_model?: string;\n thinking_enabled?: boolean;\n permission_mode?: string;\n platform_url?: string;\n theme?: string;\n}\n\n// ============================================================================\n// PATHS\n// ============================================================================\n\nconst CONFIG_DIR = join(homedir(), \".whaletools\");\nconst SESSION_PATH = join(CONFIG_DIR, \"session.json\");\nconst PREFS_PATH = join(CONFIG_DIR, \"preferences.json\");\n\n// Legacy paths for migration\nconst LEGACY_CONFIG_DIR = join(homedir(), \".swagmanager\");\nconst LEGACY_CONFIG_PATH = join(LEGACY_CONFIG_DIR, \"config.json\");\n\n// ============================================================================\n// AUTO-MIGRATION from ~/.swagmanager to ~/.whaletools\n// ============================================================================\n\nlet migrationChecked = false;\n\nfunction ensureMigration(): void {\n if (migrationChecked) return;\n migrationChecked = true;\n\n // Skip if new session already exists\n if (existsSync(SESSION_PATH)) return;\n\n // Check for legacy config\n if (!existsSync(LEGACY_CONFIG_PATH)) return;\n\n try {\n const legacy: WhaleConfig = JSON.parse(readFileSync(LEGACY_CONFIG_PATH, \"utf-8\"));\n\n if (!existsSync(CONFIG_DIR)) {\n mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });\n }\n\n // Split auth fields into session.json\n if (legacy.access_token && legacy.refresh_token) {\n const session: Partial<WhaleConfig> = {\n access_token: legacy.access_token,\n refresh_token: legacy.refresh_token,\n user_id: legacy.user_id,\n email: legacy.email,\n store_id: legacy.store_id,\n store_name: legacy.store_name,\n expires_at: legacy.expires_at,\n };\n writeFileSync(SESSION_PATH, JSON.stringify(session, null, 2) + \"\\n\", {\n encoding: \"utf-8\",\n mode: 0o600,\n });\n }\n\n // Split preference fields into preferences.json\n const prefs: WhalePreferences = {};\n if (legacy.default_model) prefs.default_model = legacy.default_model;\n if (legacy.thinking_enabled !== undefined) prefs.thinking_enabled = legacy.thinking_enabled;\n if (legacy.permission_mode) prefs.permission_mode = legacy.permission_mode;\n if (legacy.platform_url) prefs.platform_url = legacy.platform_url;\n\n if (Object.keys(prefs).length > 0) {\n writeFileSync(PREFS_PATH, JSON.stringify(prefs, null, 2) + \"\\n\", {\n encoding: \"utf-8\",\n mode: 0o600,\n });\n }\n } catch {\n // Migration failed — not fatal, user can re-login\n }\n}\n\n// ============================================================================\n// READ / WRITE — session.json (auth tokens + store)\n// ============================================================================\n\nexport function loadConfig(): WhaleConfig {\n ensureMigration();\n try {\n if (existsSync(SESSION_PATH)) {\n const session = JSON.parse(readFileSync(SESSION_PATH, \"utf-8\"));\n // Merge preferences so callers see a unified config\n const prefs = loadPreferences();\n return { ...prefs, ...session };\n }\n } catch (err) {\n console.error(`[config] Warning: Failed to parse ${SESSION_PATH}: ${err instanceof Error ? err.message : err}`);\n console.error(\"[config] Using empty config. You may need to re-login with: whale login\");\n }\n // Even if no session, return preferences\n return { ...loadPreferences() };\n}\n\nexport function saveConfig(config: WhaleConfig): void {\n if (!existsSync(CONFIG_DIR)) {\n mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });\n }\n\n // Split: auth fields go to session.json, prefs go to preferences.json\n const { default_model, thinking_enabled, permission_mode, platform_url, ...authFields } = config;\n // Ensure fields required by Swift WhaleSession are present\n const session = {\n version: 2,\n ...authFields,\n stores: authFields.stores ?? [],\n updated_at: Math.floor(Date.now() / 1000),\n };\n writeFileSync(SESSION_PATH, JSON.stringify(session, null, 2) + \"\\n\", {\n encoding: \"utf-8\",\n mode: 0o600,\n });\n\n // Update preferences if any pref fields are present\n const newPrefs: WhalePreferences = {};\n if (default_model !== undefined) newPrefs.default_model = default_model;\n if (thinking_enabled !== undefined) newPrefs.thinking_enabled = thinking_enabled;\n if (permission_mode !== undefined) newPrefs.permission_mode = permission_mode;\n if (platform_url !== undefined) newPrefs.platform_url = platform_url;\n if (Object.keys(newPrefs).length > 0) {\n savePreferences({ ...loadPreferences(), ...newPrefs });\n }\n}\n\nexport function updateConfig(partial: Partial<WhaleConfig>): void {\n // Only write the fields that are actually being changed.\n // Split into auth fields (session.json) and pref fields (preferences.json)\n // to avoid stale in-memory values overwriting the file.\n const { default_model, thinking_enabled, permission_mode, platform_url, ...authFields } = partial;\n\n // Update preferences.json — merge only the provided pref fields\n const prefUpdates: WhalePreferences = {};\n if (default_model !== undefined) prefUpdates.default_model = default_model;\n if (thinking_enabled !== undefined) prefUpdates.thinking_enabled = thinking_enabled;\n if (permission_mode !== undefined) prefUpdates.permission_mode = permission_mode;\n if (platform_url !== undefined) prefUpdates.platform_url = platform_url;\n if (Object.keys(prefUpdates).length > 0) {\n savePreferences({ ...loadPreferences(), ...prefUpdates });\n }\n\n // Update session.json — merge only the provided auth fields\n if (Object.keys(authFields).length > 0) {\n const existingSession = (() => {\n try {\n if (existsSync(SESSION_PATH)) return JSON.parse(readFileSync(SESSION_PATH, \"utf-8\"));\n } catch { /* ignore */ }\n return {};\n })();\n const session = { version: 2, ...existingSession, ...authFields, updated_at: Math.floor(Date.now() / 1000) };\n if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });\n writeFileSync(SESSION_PATH, JSON.stringify(session, null, 2) + \"\\n\", { encoding: \"utf-8\", mode: 0o600 });\n }\n}\n\nexport function clearConfig(): void {\n try {\n if (existsSync(SESSION_PATH)) unlinkSync(SESSION_PATH);\n } catch { /* ignore */ }\n // Preferences are preserved across sign-out\n}\n\n// ============================================================================\n// READ / WRITE — preferences.json (survives sign-out)\n// ============================================================================\n\nexport function loadPreferences(): WhalePreferences {\n try {\n if (existsSync(PREFS_PATH)) {\n return JSON.parse(readFileSync(PREFS_PATH, \"utf-8\"));\n }\n } catch { /* ignore */ }\n return {};\n}\n\nexport function savePreferences(prefs: WhalePreferences): void {\n if (!existsSync(CONFIG_DIR)) {\n mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 });\n }\n writeFileSync(PREFS_PATH, JSON.stringify(prefs, null, 2) + \"\\n\", {\n encoding: \"utf-8\",\n mode: 0o600,\n });\n}\n\n// ============================================================================\n// RESOLVED CONFIG (env vars override file — used by MCP server)\n// ============================================================================\n\nexport interface ResolvedConfig {\n supabaseUrl: string;\n supabaseKey: string;\n storeId: string;\n anthropicApiKey: string;\n defaultAgentId: string;\n serverUrl: string;\n platformUrl: string;\n}\n\n/** Default Fly.io agent server URL */\nexport const WHALE_SERVER_URL = \"https://whale-agent.fly.dev\";\n\n/** Default Supabase URL — fallback when env var and config are both empty */\nconst DEFAULT_SUPABASE_URL = \"https://uaednwpxursknmwdeejn.supabase.co\";\n\nexport function resolveConfig(): ResolvedConfig {\n const file = loadConfig();\n return {\n supabaseUrl: process.env.SUPABASE_URL || file.supabase_url || DEFAULT_SUPABASE_URL,\n // Service role key only — user JWTs go through getValidToken() in auth-service\n supabaseKey: process.env.SUPABASE_SERVICE_ROLE_KEY || file.supabase_key || \"\",\n storeId: process.env.STORE_ID || file.store_id || \"\",\n anthropicApiKey: process.env.ANTHROPIC_API_KEY || file.anthropic_api_key || \"\",\n defaultAgentId: file.default_agent_id || \"\",\n serverUrl: process.env.WHALE_SERVER_URL || WHALE_SERVER_URL,\n platformUrl: process.env.WHALETOOLS_PLATFORM_URL || file.platform_url || \"https://whaletools.dev\",\n };\n}\n\nexport function getConfigPath(): string {\n return SESSION_PATH;\n}\n\n/** Lazy proxy URL — avoids reading config at import time */\nexport function getProxyUrl(): string {\n return resolveConfig().serverUrl;\n}\n\n/** Default Supabase anon key — used for client-side queries */\nconst DEFAULT_SUPABASE_ANON_KEY = \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InVhZWRud3B4dXJza25td2RlZWpuIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjA5OTcyMzMsImV4cCI6MjA3NjU3MzIzM30.N8jPwlyCBB5KJB5I-XaK6m-mq88rSR445AWFJJmwRCg\";\n\n/**\n * Resolve default agent ID — queries ai_agent_config for the store's first\n * active agent if not already configured, then caches the result.\n */\nexport async function resolveDefaultAgentId(\n _serverUrl: string,\n storeId: string,\n getToken: () => Promise<string | null>,\n): Promise<string | null> {\n // Check cached value first\n const config = loadConfig();\n if (config.default_agent_id) return config.default_agent_id;\n\n try {\n const { createClient } = await import(\"@supabase/supabase-js\");\n const token = await getToken();\n if (!token) return null;\n\n const supabaseUrl = process.env.SUPABASE_URL || config.supabase_url || DEFAULT_SUPABASE_URL;\n const anonKey = process.env.SUPABASE_ANON_KEY || DEFAULT_SUPABASE_ANON_KEY;\n const supabase = createClient(supabaseUrl, anonKey, {\n global: { headers: { Authorization: `Bearer ${token}` } },\n });\n\n const { data } = await supabase\n .from(\"ai_agent_config\")\n .select(\"id\")\n .eq(\"store_id\", storeId)\n .eq(\"is_active\", true)\n .order(\"created_at\", { ascending: true })\n .limit(1)\n .single();\n\n if (!data?.id) return null;\n\n // Cache for next time\n updateConfig({ default_agent_id: data.id });\n return data.id;\n } catch {\n return null;\n }\n}\n"],"mappings":"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","ignoreList":[]}
@@ -175,7 +175,8 @@ export async function executeHook(hook, payload) {
175
175
  }
176
176
  });
177
177
 
178
- // Write payload to stdin
178
+ // Write payload to stdin — suppress EPIPE if child exits before we finish
179
+ child.stdin?.on('error', () => {});
179
180
  try {
180
181
  child.stdin?.write(JSON.stringify(payload));
181
182
  child.stdin?.end();
@@ -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":"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","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":"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","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>;