whale-code 6.4.0 → 6.5.0

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 (187) hide show
  1. package/bin/swagmanager-mcp.js +7 -0
  2. package/dist/cli/app.js +30 -2
  3. package/dist/cli/chat/ChatApp.d.ts +4 -4
  4. package/dist/cli/chat/ChatApp.js +114 -44
  5. package/dist/cli/chat/ChatInput.d.ts +13 -6
  6. package/dist/cli/chat/ChatInput.js +433 -89
  7. package/dist/cli/chat/MemoryManager.d.ts +15 -0
  8. package/dist/cli/chat/MemoryManager.js +61 -0
  9. package/dist/cli/chat/MessageList.d.ts +8 -0
  10. package/dist/cli/chat/MessageList.js +1 -1
  11. package/dist/cli/chat/NodeManager.d.ts +30 -0
  12. package/dist/cli/chat/NodeManager.js +89 -0
  13. package/dist/cli/chat/NodeSelector.d.ts +19 -0
  14. package/dist/cli/chat/NodeSelector.js +37 -0
  15. package/dist/cli/chat/PlanApproval.d.ts +17 -0
  16. package/dist/cli/chat/PlanApproval.js +82 -0
  17. package/dist/cli/chat/SessionManager.d.ts +16 -0
  18. package/dist/cli/chat/SessionManager.js +43 -0
  19. package/dist/cli/chat/SlashMenu.d.ts +38 -0
  20. package/dist/cli/chat/SlashMenu.js +208 -0
  21. package/dist/cli/chat/StatusBar.d.ts +16 -0
  22. package/dist/cli/chat/StatusBar.js +22 -0
  23. package/dist/cli/chat/ThemeSelector.d.ts +14 -0
  24. package/dist/cli/chat/ThemeSelector.js +29 -0
  25. package/dist/cli/chat/ToolIndicator.d.ts +8 -0
  26. package/dist/cli/chat/ToolIndicator.js +33 -9
  27. package/dist/cli/chat/hooks/useAgentLoop.d.ts +2 -1
  28. package/dist/cli/chat/hooks/useAgentLoop.js +22 -17
  29. package/dist/cli/chat/hooks/useSlashCommands.d.ts +19 -0
  30. package/dist/cli/chat/hooks/useSlashCommands.js +254 -15
  31. package/dist/cli/commands/config-cmd.js +4 -25
  32. package/dist/cli/commands/db.d.ts +13 -0
  33. package/dist/cli/commands/db.js +243 -0
  34. package/dist/cli/commands/doctor.js +6 -9
  35. package/dist/cli/commands/mcp.js +1 -20
  36. package/dist/cli/services/agent-events.d.ts +22 -1
  37. package/dist/cli/services/agent-events.js +9 -0
  38. package/dist/cli/services/agent-loop.js +66 -2
  39. package/dist/cli/services/agent-worker-base.js +21 -6
  40. package/dist/cli/services/api-retry.d.ts +25 -0
  41. package/dist/cli/services/api-retry.js +91 -0
  42. package/dist/cli/services/auth-service.d.ts +1 -1
  43. package/dist/cli/services/auth-service.js +40 -19
  44. package/dist/cli/services/background-processes.js +26 -2
  45. package/dist/cli/services/config-store.d.ts +13 -1
  46. package/dist/cli/services/config-store.js +116 -13
  47. package/dist/cli/services/format-server-response.js +12 -6
  48. package/dist/cli/services/ink-resize-fix.d.ts +18 -0
  49. package/dist/cli/services/ink-resize-fix.js +66 -0
  50. package/dist/cli/services/interactive-tools.d.ts +14 -0
  51. package/dist/cli/services/interactive-tools.js +47 -2
  52. package/dist/cli/services/keybinding-manager.js +1 -1
  53. package/dist/cli/services/local-tools.js +35 -2
  54. package/dist/cli/services/server-tools.js +175 -3
  55. package/dist/cli/services/subagent.js +15 -3
  56. package/dist/cli/services/system-prompt.js +5 -3
  57. package/dist/cli/services/task-decomposer.d.ts +35 -0
  58. package/dist/cli/services/task-decomposer.js +199 -0
  59. package/dist/cli/services/team-lead.d.ts +18 -0
  60. package/dist/cli/services/team-lead.js +80 -0
  61. package/dist/cli/services/teammate.js +5 -5
  62. package/dist/cli/services/telemetry.d.ts +8 -2
  63. package/dist/cli/services/telemetry.js +116 -92
  64. package/dist/cli/services/tools/agent-tools.d.ts +1 -0
  65. package/dist/cli/services/tools/agent-tools.js +50 -4
  66. package/dist/cli/services/tools/file-ops.d.ts +2 -0
  67. package/dist/cli/services/tools/file-ops.js +71 -19
  68. package/dist/cli/services/tools/shell-exec.js +22 -12
  69. package/dist/cli/shared/Theme.d.ts +1 -2
  70. package/dist/cli/shared/Theme.js +1 -1
  71. package/dist/cli/shared/WhaleBanner.d.ts +4 -1
  72. package/dist/cli/shared/WhaleBanner.js +12 -8
  73. package/dist/cli/shared/markdown.d.ts +5 -4
  74. package/dist/cli/shared/markdown.js +376 -334
  75. package/dist/cli/shared/theme-manager.d.ts +27 -0
  76. package/dist/cli/shared/theme-manager.js +178 -0
  77. package/dist/cli/shared/theme-presets.d.ts +16 -0
  78. package/dist/cli/shared/theme-presets.js +265 -0
  79. package/dist/index.js +0 -51
  80. package/dist/node/adapters/imessage.d.ts +10 -0
  81. package/dist/node/adapters/imessage.js +45 -6
  82. package/dist/node/cli.js +459 -8
  83. package/dist/node/config.d.ts +17 -0
  84. package/dist/node/gateway-client.d.ts +55 -0
  85. package/dist/node/gateway-client.js +201 -0
  86. package/dist/node/portal/clipboard.d.ts +28 -0
  87. package/dist/node/portal/clipboard.js +183 -0
  88. package/dist/node/portal/discovery.d.ts +29 -0
  89. package/dist/node/portal/discovery.js +61 -0
  90. package/dist/node/portal/forward.d.ts +30 -0
  91. package/dist/node/portal/forward.js +90 -0
  92. package/dist/node/portal/index.d.ts +47 -0
  93. package/dist/node/portal/index.js +250 -0
  94. package/dist/node/portal/multiplexer.d.ts +48 -0
  95. package/dist/node/portal/multiplexer.js +207 -0
  96. package/dist/node/portal/permissions.d.ts +36 -0
  97. package/dist/node/portal/permissions.js +131 -0
  98. package/dist/node/portal/protocol.d.ts +140 -0
  99. package/dist/node/portal/protocol.js +193 -0
  100. package/dist/node/portal/screen.d.ts +18 -0
  101. package/dist/node/portal/screen.js +93 -0
  102. package/dist/node/portal/session.d.ts +68 -0
  103. package/dist/node/portal/session.js +127 -0
  104. package/dist/node/portal/shell.d.ts +26 -0
  105. package/dist/node/portal/shell.js +142 -0
  106. package/dist/node/portal/stream.d.ts +43 -0
  107. package/dist/node/portal/stream.js +90 -0
  108. package/dist/node/portal/transfer.d.ts +33 -0
  109. package/dist/node/portal/transfer.js +231 -0
  110. package/dist/node/portal/ui.d.ts +16 -0
  111. package/dist/node/portal/ui.js +148 -0
  112. package/dist/node/remote-desktop/compile-helper.d.ts +13 -0
  113. package/dist/node/remote-desktop/compile-helper.js +73 -0
  114. package/dist/node/remote-desktop/index.d.ts +67 -0
  115. package/dist/node/remote-desktop/index.js +220 -0
  116. package/dist/node/remote-desktop/protocol.d.ts +96 -0
  117. package/dist/node/remote-desktop/protocol.js +67 -0
  118. package/dist/node/runtime.d.ts +8 -1
  119. package/dist/node/runtime.js +117 -9
  120. package/dist/server/handlers/__test-utils__/test-db.d.ts +25 -0
  121. package/dist/server/handlers/__test-utils__/test-db.js +128 -0
  122. package/dist/server/handlers/api-keys.js +26 -2
  123. package/dist/server/handlers/browser.d.ts +0 -4
  124. package/dist/server/handlers/browser.js +0 -46
  125. package/dist/server/handlers/catalog.js +37 -14
  126. package/dist/server/handlers/clickhouse.d.ts +10 -0
  127. package/dist/server/handlers/clickhouse.js +215 -0
  128. package/dist/server/handlers/comms.d.ts +308 -4
  129. package/dist/server/handlers/comms.js +444 -11
  130. package/dist/server/handlers/creations.js +1 -1
  131. package/dist/server/handlers/crm.d.ts +54 -8
  132. package/dist/server/handlers/crm.js +353 -68
  133. package/dist/server/handlers/embeddings.js +3 -3
  134. package/dist/server/handlers/enrichment.js +39 -55
  135. package/dist/server/handlers/inventory.js +1 -1
  136. package/dist/server/handlers/kali.d.ts +9 -1
  137. package/dist/server/handlers/kali.js +50 -1
  138. package/dist/server/handlers/media.d.ts +8 -0
  139. package/dist/server/handlers/media.js +902 -0
  140. package/dist/server/handlers/meta-ads.js +6 -3
  141. package/dist/server/handlers/nodes.d.ts +2 -0
  142. package/dist/server/handlers/nodes.js +331 -40
  143. package/dist/server/handlers/operations.d.ts +4 -6
  144. package/dist/server/handlers/operations.js +99 -38
  145. package/dist/server/handlers/platform.js +224 -107
  146. package/dist/server/handlers/remove-bg.d.ts +6 -0
  147. package/dist/server/handlers/remove-bg.js +96 -0
  148. package/dist/server/handlers/storefront.d.ts +6 -0
  149. package/dist/server/handlers/storefront.js +477 -0
  150. package/dist/server/handlers/supply-chain.js +21 -3
  151. package/dist/server/handlers/workflow-steps.js +87 -31
  152. package/dist/server/handlers/workflows.js +4 -1
  153. package/dist/server/index.js +334 -88
  154. package/dist/server/lib/clickhouse-buffer.d.ts +48 -0
  155. package/dist/server/lib/clickhouse-buffer.js +175 -0
  156. package/dist/server/lib/clickhouse-client.d.ts +112 -0
  157. package/dist/server/lib/clickhouse-client.js +141 -0
  158. package/dist/server/lib/coa-renderer.d.ts +91 -0
  159. package/dist/server/lib/coa-renderer.js +411 -0
  160. package/dist/server/lib/compaction-service.js +45 -1
  161. package/dist/server/lib/pdf-renderer.d.ts +143 -0
  162. package/dist/server/lib/pdf-renderer.js +867 -0
  163. package/dist/server/lib/react-pdf-layout.d.ts +40 -0
  164. package/dist/server/lib/react-pdf-layout.js +437 -0
  165. package/dist/server/lib/server-agent-loop.d.ts +2 -0
  166. package/dist/server/lib/server-agent-loop.js +61 -15
  167. package/dist/server/lib/server-subagent.d.ts +3 -0
  168. package/dist/server/lib/server-subagent.js +7 -4
  169. package/dist/server/lib/supabase-client.js +51 -3
  170. package/dist/server/lib/template-resolver.js +14 -4
  171. package/dist/server/lib/utils.js +15 -0
  172. package/dist/server/local-agent-gateway.d.ts +44 -0
  173. package/dist/server/local-agent-gateway.js +389 -49
  174. package/dist/server/providers/anthropic.js +12 -2
  175. package/dist/server/providers/gemini.js +17 -2
  176. package/dist/server/proxy-handlers.js +151 -0
  177. package/dist/server/tool-router.d.ts +2 -2
  178. package/dist/server/tool-router.js +25 -35
  179. package/dist/shared/agent-core.d.ts +5 -2
  180. package/dist/shared/agent-core.js +30 -4
  181. package/dist/shared/api-client.js +54 -3
  182. package/dist/shared/sse-parser.d.ts +1 -1
  183. package/dist/shared/sse-parser.js +5 -2
  184. package/dist/shared/tool-dispatch.js +1 -1
  185. package/package.json +16 -10
  186. package/dist/server/handlers/__test-utils__/mock-supabase.d.ts +0 -11
  187. package/dist/server/handlers/__test-utils__/mock-supabase.js +0 -393
@@ -1,5 +1,10 @@
1
1
  // server/handlers/operations.ts — Locations, Suppliers, Alerts, Audit Trail handlers
2
2
  import { sanitizeFilterValue } from "../lib/utils.js";
3
+ import { getClickHouseClient } from "../lib/clickhouse-client.js";
4
+ /** Escape single quotes for ClickHouse SQL */
5
+ function escQ(s) {
6
+ return s.replace(/'/g, "\\'");
7
+ }
3
8
  export async function handleLocations(sb, args, storeId) {
4
9
  const sid = storeId;
5
10
  let q = sb.from("locations").select("id, name, address_line1, city, state, is_active, type").eq("store_id", sid);
@@ -82,50 +87,106 @@ export async function handleAuditTrail(sb, args, storeId) {
82
87
  const sid = storeId;
83
88
  const days = args.days || 1;
84
89
  const cutoff = new Date(Date.now() - days * 86400_000).toISOString();
85
- // "search" action: full-text search across logs
90
+ const ch = getClickHouseClient();
91
+ const rowLimit = Math.min(args.limit || 100, 500);
92
+ // Business audit actions in Postgres business_audit table
93
+ // All action types that live in business_audit (queried by handleAuditTrail)
94
+ const BUSINESS_ACTIONS = [
95
+ "inventory.po_receive", "inventory.transfer_receive", "inventory.transfer_cancel_restore",
96
+ "inventory.po_create", "inventory.transfer_create", "inventory.adjustment_create",
97
+ "terminal_credentials_access", "payment.initiated", "payment.failed",
98
+ "trigger.schedule.fired", "trigger.condition.fired",
99
+ "trigger.condition.blocked", "trigger.condition.error",
100
+ "system.connection_warning",
101
+ ];
102
+ // "search" action: dual-read ClickHouse + Postgres
86
103
  if (args.action === "search") {
87
104
  const query = args.query;
88
105
  if (!query)
89
106
  return { success: false, error: "query parameter is required for search action" };
90
- let sq = sb.from("audit_logs")
91
- .select("id, action, severity, source, resource_type, resource_id, user_email, details, created_at")
92
- .eq("store_id", sid)
93
- .gte("created_at", cutoff)
94
- .or(`action.ilike.%${sanitizeFilterValue(query)}%,error_message.ilike.%${sanitizeFilterValue(query)}%,user_email.ilike.%${sanitizeFilterValue(query)}%,resource_id.ilike.%${sanitizeFilterValue(query)}%`)
95
- .order("created_at", { ascending: false });
96
- if (args.limit)
97
- sq = sq.limit(args.limit);
98
- const { data, error } = await sq;
99
- if (error)
100
- return { success: false, error: error.message };
101
- return { success: true, data: { query, entries: data, count: data?.length || 0, days } };
102
- }
103
- // Default action: filtered list
104
- let q = sb.from("audit_logs")
105
- .select("id, action, severity, source, resource_type, resource_id, details, user_email, created_at")
106
- .eq("store_id", sid)
107
- .gte("created_at", cutoff)
108
- .order("created_at", { ascending: false });
109
- if (args.limit)
110
- q = q.limit(args.limit);
111
- // Optional filters
112
- if (args.action_filter) {
113
- const sf = sanitizeFilterValue(args.action_filter);
114
- q = q.ilike("action", `%${sf}%`);
107
+ const sq = sanitizeFilterValue(query);
108
+ // Parallel: ClickHouse telemetry + Postgres business audit
109
+ const [chSpans, pgResult] = await Promise.all([
110
+ ch.isEnabled
111
+ ? ch.query(`
112
+ SELECT
113
+ span_id AS id, operation_name AS action, severity, source,
114
+ '' AS resource_type, tool_name AS resource_id,
115
+ user_id AS user_email, attributes AS details, started_at AS created_at
116
+ FROM ai_spans
117
+ WHERE store_id = '${escQ(sid)}'
118
+ AND started_at >= '${cutoff}'
119
+ AND (operation_name ILIKE '%${escQ(sq)}%' OR error_message ILIKE '%${escQ(sq)}%' OR tool_name ILIKE '%${escQ(sq)}%')
120
+ ORDER BY started_at DESC
121
+ LIMIT ${rowLimit}
122
+ `)
123
+ : Promise.resolve([]),
124
+ sb.from("business_audit")
125
+ .select("id, action, severity, source, resource_type, resource_id, details, created_at")
126
+ .eq("store_id", sid)
127
+ .gte("created_at", cutoff)
128
+ .in("action", BUSINESS_ACTIONS)
129
+ .or(`action.ilike.%${sq}%,resource_id.ilike.%${sq}%`)
130
+ .order("created_at", { ascending: false })
131
+ .limit(rowLimit),
132
+ ]);
133
+ const entries = [...chSpans, ...(pgResult.data || [])]
134
+ .sort((a, b) => String(b.created_at).localeCompare(String(a.created_at)))
135
+ .slice(0, rowLimit);
136
+ return { success: true, data: { query, entries, count: entries.length, days } };
115
137
  }
116
- if (args.resource_type)
117
- q = q.eq("resource_type", args.resource_type);
118
- if (args.severity)
119
- q = q.eq("severity", args.severity);
120
- if (args.source)
121
- q = q.eq("source", args.source);
122
- const { data, error } = await q;
123
- if (error)
124
- return { success: false, error: error.message };
138
+ // Default action: dual-read filtered list
139
+ const actionFilter = args.action_filter ? sanitizeFilterValue(args.action_filter) : null;
140
+ const severityFilter = args.severity;
141
+ const sourceFilter = args.source;
142
+ // Build ClickHouse filters
143
+ const chFilters = [`store_id = '${escQ(sid)}'`, `started_at >= '${cutoff}'`];
144
+ if (actionFilter)
145
+ chFilters.push(`operation_name ILIKE '%${escQ(actionFilter)}%'`);
146
+ if (severityFilter)
147
+ chFilters.push(`severity = '${escQ(severityFilter)}'`);
148
+ if (sourceFilter)
149
+ chFilters.push(`source = '${escQ(sourceFilter)}'`);
150
+ const [chSpans, pgResult] = await Promise.all([
151
+ ch.isEnabled
152
+ ? ch.query(`
153
+ SELECT
154
+ span_id AS id, operation_name AS action, severity, source,
155
+ '' AS resource_type, tool_name AS resource_id,
156
+ attributes AS details, user_id AS user_email, started_at AS created_at
157
+ FROM ai_spans
158
+ WHERE ${chFilters.join(" AND ")}
159
+ ORDER BY started_at DESC
160
+ LIMIT ${rowLimit}
161
+ `)
162
+ : Promise.resolve([]),
163
+ (() => {
164
+ let q = sb.from("business_audit")
165
+ .select("id, action, severity, source, resource_type, resource_id, details, created_at")
166
+ .eq("store_id", sid)
167
+ .gte("created_at", cutoff)
168
+ .in("action", BUSINESS_ACTIONS)
169
+ .order("created_at", { ascending: false })
170
+ .limit(rowLimit);
171
+ if (actionFilter)
172
+ q = q.ilike("action", `%${actionFilter}%`);
173
+ if (args.resource_type)
174
+ q = q.eq("resource_type", args.resource_type);
175
+ if (severityFilter)
176
+ q = q.eq("severity", severityFilter);
177
+ if (sourceFilter)
178
+ q = q.eq("source", sourceFilter);
179
+ return q;
180
+ })(),
181
+ ]);
182
+ const entries = [...chSpans, ...(pgResult.data || [])]
183
+ .sort((a, b) => String(b.created_at).localeCompare(String(a.created_at)))
184
+ .slice(0, rowLimit);
125
185
  // Summarize by action for quick overview
126
186
  const byAction = {};
127
- for (const entry of data || []) {
128
- byAction[entry.action] = (byAction[entry.action] || 0) + 1;
187
+ for (const entry of entries) {
188
+ const act = entry.action;
189
+ byAction[act] = (byAction[act] || 0) + 1;
129
190
  }
130
- return { success: true, data: { entries: data, summary: byAction, days, count: data?.length || 0 } };
191
+ return { success: true, data: { entries, summary: byAction, days, count: entries.length } };
131
192
  }
@@ -1,15 +1,21 @@
1
+ import { getClickHouseClient } from "../lib/clickhouse-client.js";
2
+ /** Escape single quotes for ClickHouse SQL */
3
+ function esc(s) {
4
+ return s.replace(/'/g, "\\'");
5
+ }
1
6
  export async function handleWebSearch(sb, args, _storeId) {
2
7
  const query = args.query;
3
8
  const numResults = args.num_results || 5;
9
+ // Validate input first (cheap, no DB call)
10
+ if (!query) {
11
+ return { success: false, error: "Query parameter is required" };
12
+ }
4
13
  // Read from platform_secrets table first, fall back to env var
5
14
  const { data: secret } = await sb.from("platform_secrets").select("value").eq("key", "exa_api_key").single();
6
15
  const exaApiKey = secret?.value || process.env["EXA_API_KEY"];
7
16
  if (!exaApiKey) {
8
17
  return { success: false, error: "Exa API key not configured. Add 'exa_api_key' to platform_secrets table." };
9
18
  }
10
- if (!query) {
11
- return { success: false, error: "Query parameter is required" };
12
- }
13
19
  try {
14
20
  const response = await fetch("https://api.exa.ai/search", {
15
21
  method: "POST",
@@ -47,28 +53,43 @@ export async function handleTelemetry(sb, args, storeId) {
47
53
  const hoursBack = args.hours_back || 24;
48
54
  const limit = Math.min(args.limit || 50, 200);
49
55
  switch (args.action) {
50
- // ---- conversation_detail: Full conversation with messages + audit entries ----
56
+ // ---- conversation_detail: Full conversation with messages + ClickHouse spans ----
51
57
  case "conversation_detail": {
52
58
  const convId = args.conversation_id;
53
59
  if (!convId)
54
60
  return { success: false, error: "conversation_id is required" };
55
- const [convResult, msgResult, auditResult] = await Promise.all([
56
- sb.from("ai_conversations").select("*").eq("id", convId).eq("store_id", sid).single(),
57
- sb.from("ai_messages").select("*").eq("conversation_id", convId).order("created_at", { ascending: true }),
58
- sb.from("audit_logs").select("id, action, severity, duration_ms, status_code, error_message, resource_id, input_tokens, output_tokens, model, created_at")
59
- .eq("conversation_id", convId).order("created_at", { ascending: true }).limit(200)
60
- ]);
61
+ // Verify conversation belongs to this store first
62
+ const convResult = await sb.from("ai_conversations").select("*").eq("id", convId).eq("store_id", sid).single();
61
63
  if (convResult.error)
62
64
  return { success: false, error: convResult.error.message };
65
+ // Fetch messages from Postgres + spans from ClickHouse in parallel
66
+ const ch = getClickHouseClient();
67
+ const [msgResult, spans] = await Promise.all([
68
+ sb.from("ai_messages").select("*").eq("conversation_id", convId).order("created_at", { ascending: true }),
69
+ ch.isEnabled
70
+ ? ch.query(`
71
+ SELECT
72
+ span_id AS id, operation_name AS action, severity,
73
+ duration_ms, status_code, error_message,
74
+ tool_name AS resource_id,
75
+ prompt_tokens AS input_tokens, completion_tokens AS output_tokens,
76
+ model_name AS model, started_at AS created_at
77
+ FROM ai_spans
78
+ WHERE conversation_id = '${esc(convId)}'
79
+ ORDER BY started_at ASC
80
+ LIMIT 200
81
+ `)
82
+ : Promise.resolve([]),
83
+ ]);
63
84
  return {
64
85
  success: true,
65
86
  data: {
66
87
  conversation: convResult.data,
67
88
  messages: msgResult.data || [],
68
- audit_entries: auditResult.data || [],
89
+ audit_entries: spans,
69
90
  message_count: msgResult.data?.length || 0,
70
- audit_count: auditResult.data?.length || 0
71
- }
91
+ audit_count: spans.length,
92
+ },
72
93
  };
73
94
  }
74
95
  // ---- conversations: List recent conversations ----
@@ -98,128 +119,224 @@ export async function handleTelemetry(sb, args, storeId) {
98
119
  const { data, error } = await sb.rpc("get_agent_analytics", { p_agent_id: agentId, p_days: days });
99
120
  return error ? { success: false, error: error.message } : { success: true, data };
100
121
  }
101
- // ---- tool_analytics: Per-tool performance metrics via RPC ----
122
+ // ---- tool_analytics: Per-tool performance metrics via direct ClickHouse ----
102
123
  case "tool_analytics": {
103
- const { data, error } = await sb.rpc("get_tool_analytics", {
104
- p_store_id: sid,
105
- p_hours_back: hoursBack,
106
- p_tool_name: args.tool_name || null
107
- });
108
- return error ? { success: false, error: error.message } : { success: true, data };
124
+ const ch = getClickHouseClient();
125
+ if (!ch.isEnabled)
126
+ return { success: false, error: "ClickHouse not configured" };
127
+ const toolFilter = args.tool_name ? `AND tool_name = '${esc(args.tool_name)}'` : "";
128
+ const storeF = sid ? `AND store_id = '${esc(sid)}'` : "";
129
+ const tools = await ch.query(`
130
+ SELECT
131
+ tool_name,
132
+ count() AS total_calls,
133
+ countIf(status_code = 'OK') AS success_count,
134
+ countIf(status_code = 'ERROR') AS error_count,
135
+ countIf(duration_ms > 30000) AS timeout_count,
136
+ round(avg(duration_ms), 1) AS avg_ms,
137
+ round(quantile(0.50)(duration_ms), 1) AS p50_ms,
138
+ round(quantile(0.90)(duration_ms), 1) AS p90_ms,
139
+ round(quantile(0.95)(duration_ms), 1) AS p95_ms,
140
+ round(quantile(0.99)(duration_ms), 1) AS p99_ms,
141
+ min(duration_ms) AS min_ms,
142
+ max(duration_ms) AS max_ms,
143
+ round(100.0 * countIf(status_code = 'ERROR') / count(), 2) AS error_rate,
144
+ round(sum(token_cost_usd), 6) AS total_marginal_cost,
145
+ min(started_at) AS first_call,
146
+ max(started_at) AS last_call,
147
+ round(count() / greatest(dateDiff('minute', min(started_at), max(started_at)), 1), 2) AS calls_per_minute,
148
+ round(greatest(0, 100.0 - 100.0 * countIf(status_code = 'ERROR') / count() - 100.0 * countIf(duration_ms > 30000) / count()), 1) AS reliability_score
149
+ FROM ai_spans
150
+ WHERE started_at >= now() - INTERVAL ${hoursBack} HOUR
151
+ AND tool_name IS NOT NULL AND tool_name != ''
152
+ ${storeF} ${toolFilter}
153
+ GROUP BY tool_name
154
+ ORDER BY total_calls DESC
155
+ `);
156
+ const totalCalls = tools.reduce((s, t) => s + (t.total_calls || 0), 0);
157
+ const totalErrors = tools.reduce((s, t) => s + (t.error_count || 0), 0);
158
+ const totalTimeouts = tools.reduce((s, t) => s + (t.timeout_count || 0), 0);
159
+ const totalCost = tools.reduce((s, t) => s + (t.total_marginal_cost || 0), 0);
160
+ return { success: true, data: {
161
+ tools,
162
+ summary: {
163
+ total_calls: totalCalls, total_errors: totalErrors, total_timeouts: totalTimeouts,
164
+ overall_error_rate: totalCalls > 0 ? Math.round(10000 * totalErrors / totalCalls) / 100 : 0,
165
+ unique_tools: tools.length, total_marginal_cost: Math.round(totalCost * 1e6) / 1e6,
166
+ hours_analyzed: hoursBack,
167
+ },
168
+ } };
109
169
  }
110
- // ---- tool_timeline: Time-bucketed tool metrics via RPC ----
170
+ // ---- tool_timeline: Time-bucketed tool metrics via direct ClickHouse ----
111
171
  case "tool_timeline": {
172
+ const ch = getClickHouseClient();
173
+ if (!ch.isEnabled)
174
+ return { success: false, error: "ClickHouse not configured" };
112
175
  const bucketMinutes = args.bucket_minutes || 15;
113
- const { data, error } = await sb.rpc("get_tool_timeline", {
114
- p_store_id: sid,
115
- p_hours_back: hoursBack,
116
- p_bucket_minutes: bucketMinutes,
117
- p_tool_name: args.tool_name || null
118
- });
119
- return error ? { success: false, error: error.message } : { success: true, data };
176
+ const toolFilter = args.tool_name ? `AND tool_name = '${esc(args.tool_name)}'` : "";
177
+ const storeF = sid ? `AND store_id = '${esc(sid)}'` : "";
178
+ const buckets = await ch.query(`
179
+ SELECT
180
+ toStartOfInterval(started_at, INTERVAL ${bucketMinutes} MINUTE) AS time,
181
+ tool_name AS tool,
182
+ count() AS calls,
183
+ countIf(status_code = 'ERROR') AS errors,
184
+ countIf(duration_ms > 30000) AS timeouts,
185
+ round(avg(duration_ms), 1) AS avg_ms,
186
+ round(quantile(0.95)(duration_ms), 1) AS p95_ms,
187
+ max(duration_ms) AS max_ms
188
+ FROM ai_spans
189
+ WHERE started_at >= now() - INTERVAL ${hoursBack} HOUR
190
+ AND tool_name IS NOT NULL AND tool_name != ''
191
+ ${storeF} ${toolFilter}
192
+ GROUP BY time, tool
193
+ ORDER BY time ASC, tool
194
+ `);
195
+ return { success: true, data: { bucket_minutes: bucketMinutes, hours_back: hoursBack, buckets } };
120
196
  }
121
- // ---- trace: Full trace reconstruction via RPC ----
197
+ // ---- trace: Full trace reconstruction via direct ClickHouse ----
122
198
  case "trace": {
123
199
  const traceId = args.trace_id;
124
200
  if (!traceId)
125
201
  return { success: false, error: "trace_id is required" };
126
- // Verify trace belongs to this store
127
- const { data: traceLog } = await sb.from("audit_logs").select("id").eq("trace_id", traceId).eq("store_id", sid).limit(1);
128
- if (!traceLog?.length)
129
- return { success: false, error: "Trace not found for this store" };
130
- const { data, error } = await sb.rpc("get_trace", { p_trace_id: traceId });
131
- return error ? { success: false, error: error.message } : { success: true, data };
202
+ const ch = getClickHouseClient();
203
+ if (!ch.isEnabled)
204
+ return { success: false, error: "ClickHouse not configured" };
205
+ const spans = await ch.query(`
206
+ SELECT
207
+ span_id, parent_span_id, trace_id, operation_name,
208
+ service_name, duration_ms AS duration,
209
+ started_at, ended_at,
210
+ if(status_code = 'ERROR', 'error', 'ok') AS status,
211
+ attributes, events
212
+ FROM ai_spans
213
+ WHERE trace_id = '${esc(traceId)}'
214
+ ORDER BY started_at ASC
215
+ `);
216
+ return { success: true, data: { trace_id: traceId, spans } };
132
217
  }
133
- // ---- span_detail: Individual span deep-dive via RPC ----
218
+ // ---- span_detail: Individual span deep-dive via direct ClickHouse ----
134
219
  case "span_detail": {
135
220
  const spanId = args.span_id;
136
221
  if (!spanId)
137
222
  return { success: false, error: "span_id is required" };
138
- // Verify span belongs to this store via audit_logs
139
- const { data: spanLog } = await sb.from("audit_logs").select("id").eq("id", spanId).eq("store_id", sid).limit(1);
140
- if (!spanLog?.length)
141
- return { success: false, error: "Span not found for this store" };
142
- const { data, error } = await sb.rpc("get_tool_trace_detail", { p_span_id: spanId });
143
- return error ? { success: false, error: error.message } : { success: true, data };
223
+ const ch = getClickHouseClient();
224
+ if (!ch.isEnabled)
225
+ return { success: false, error: "ClickHouse not configured" };
226
+ const spans = await ch.query(`
227
+ SELECT
228
+ span_id, operation_name, tool_name, severity, duration_ms,
229
+ status_code, error_message, started_at, ended_at,
230
+ trace_id, span_kind, service_name, model_name,
231
+ conversation_id, attributes
232
+ FROM ai_spans
233
+ WHERE span_id = '${esc(spanId)}'
234
+ LIMIT 1
235
+ `);
236
+ if (spans.length === 0)
237
+ return { success: false, error: "Span not found" };
238
+ const span = spans[0];
239
+ // Comparison metrics for this tool (24h lookback)
240
+ const toolName = span.tool_name;
241
+ let comparison = {};
242
+ if (toolName) {
243
+ const comp = await ch.query(`
244
+ SELECT
245
+ round(avg(duration_ms), 1) AS avg_ms,
246
+ round(quantile(0.95)(duration_ms), 1) AS p95_ms,
247
+ round(100.0 * countIf(status_code = 'ERROR') / count(), 2) AS error_rate,
248
+ count() AS total_calls_24h
249
+ FROM ai_spans
250
+ WHERE tool_name = '${esc(toolName)}'
251
+ AND started_at >= now() - INTERVAL 24 HOUR
252
+ `);
253
+ if (comp.length > 0) {
254
+ comparison = {
255
+ ...comp[0],
256
+ is_slow: span.duration_ms > (comp[0].p95_ms || 999999),
257
+ };
258
+ }
259
+ }
260
+ return { success: true, data: { span, comparison } };
144
261
  }
145
- // ---- error_patterns: Error correlation + burst detection via RPC ----
262
+ // ---- error_patterns: Error correlation + burst detection via ClickHouse ----
146
263
  case "error_patterns": {
147
- const { data, error } = await sb.rpc("get_tool_error_patterns", {
148
- p_store_id: sid,
149
- p_hours_back: hoursBack
150
- });
151
- return error ? { success: false, error: error.message } : { success: true, data };
264
+ const ch = getClickHouseClient();
265
+ if (!ch.isEnabled)
266
+ return { success: false, error: "ClickHouse not configured" };
267
+ const patterns = await ch.query(`
268
+ SELECT
269
+ fingerprint,
270
+ any(error_type) AS error_type,
271
+ substring(any(error_message), 1, 300) AS message,
272
+ any(severity) AS severity,
273
+ any(service_name) AS service,
274
+ count() AS occurrence_count,
275
+ min(occurred_at) AS first_seen,
276
+ max(occurred_at) AS last_seen,
277
+ uniqExact(trace_id) AS affected_traces,
278
+ groupUniqArrayIf(service_name, service_name != '')[1] AS primary_service
279
+ FROM error_events
280
+ WHERE occurred_at >= now() - INTERVAL ${hoursBack} HOUR
281
+ ${sid ? `AND store_id = '${esc(sid)}'` : ""}
282
+ GROUP BY fingerprint
283
+ ORDER BY occurrence_count DESC
284
+ LIMIT 50
285
+ `);
286
+ return { success: true, data: patterns };
152
287
  }
153
- // ---- token_usage: Token consumption by model/day ----
288
+ // ---- token_usage: Token consumption by model/day via ClickHouse ----
154
289
  case "token_usage": {
155
- const cutoff = new Date(Date.now() - hoursBack * 3600_000).toISOString();
156
- // Build base query — if agent_id is provided, get conversation IDs first
157
- let conversationFilter = null;
290
+ const ch = getClickHouseClient();
291
+ if (!ch.isEnabled)
292
+ return { success: false, error: "ClickHouse not configured" };
293
+ const storeF = sid ? `AND store_id = '${esc(sid)}'` : "";
294
+ let agentFilter = "";
158
295
  if (args.agent_id) {
159
- const { data: convs } = await sb.from("ai_conversations")
160
- .select("id").eq("agent_id", args.agent_id).eq("store_id", sid);
161
- conversationFilter = convs?.map(c => c.id) || [];
162
- }
163
- let q = sb.from("audit_logs")
164
- .select("model, input_tokens, output_tokens, total_cost, created_at")
165
- .eq("store_id", sid)
166
- .gte("created_at", cutoff)
167
- .not("input_tokens", "is", null);
168
- if (conversationFilter !== null) {
169
- if (conversationFilter.length === 0)
170
- return { success: true, data: { rows: [], summary: { total_input: 0, total_output: 0, total_cost: 0 } } };
171
- q = q.in("conversation_id", conversationFilter);
296
+ agentFilter = `AND agent_id = '${esc(args.agent_id)}'`;
172
297
  }
173
- const { data, error } = await q.order("created_at", { ascending: false }).limit(1000);
174
- if (error)
175
- return { success: false, error: error.message };
176
- // Aggregate in-memory by model + day
177
- const buckets = {};
178
- for (const row of data || []) {
179
- const day = row.created_at.substring(0, 10);
180
- const model = row.model || "unknown";
181
- const key = `${model}|${day}`;
182
- if (!buckets[key])
183
- buckets[key] = { model, day, requests: 0, input_tokens: 0, output_tokens: 0, total_cost: 0 };
184
- buckets[key].requests++;
185
- buckets[key].input_tokens += row.input_tokens || 0;
186
- buckets[key].output_tokens += row.output_tokens || 0;
187
- buckets[key].total_cost += parseFloat(row.total_cost || "0");
188
- }
189
- const rows = Object.values(buckets).sort((a, b) => b.day.localeCompare(a.day) || b.total_cost - a.total_cost);
298
+ const rows = await ch.query(`
299
+ SELECT
300
+ model_name AS model,
301
+ toDate(hour) AS day,
302
+ sum(request_count) AS requests,
303
+ sum(prompt_tokens) AS input_tokens,
304
+ sum(completion_tokens) AS output_tokens,
305
+ round(sum(total_cost_usd), 6) AS total_cost
306
+ FROM token_usage_hourly
307
+ WHERE hour >= now() - INTERVAL ${hoursBack} HOUR
308
+ ${storeF} ${agentFilter}
309
+ GROUP BY model_name, toDate(hour)
310
+ ORDER BY day DESC, total_cost DESC
311
+ `);
190
312
  const summary = rows.reduce((acc, r) => ({
191
313
  total_input: acc.total_input + r.input_tokens,
192
314
  total_output: acc.total_output + r.output_tokens,
193
315
  total_cost: acc.total_cost + r.total_cost,
194
- total_requests: acc.total_requests + r.requests
316
+ total_requests: acc.total_requests + r.requests,
195
317
  }), { total_input: 0, total_output: 0, total_cost: 0, total_requests: 0 });
196
318
  return { success: true, data: { rows, summary, hours_back: hoursBack } };
197
319
  }
198
- // ---- sources: List all telemetry sources with counts ----
320
+ // ---- sources: List all telemetry sources with counts via ClickHouse ----
199
321
  case "sources": {
200
- const cutoff = new Date(Date.now() - hoursBack * 3600_000).toISOString();
201
- const { data, error } = await sb.from("audit_logs")
202
- .select("source, severity, created_at")
203
- .eq("store_id", sid)
204
- .gte("created_at", cutoff)
205
- .not("source", "is", null)
206
- .limit(1000);
207
- if (error)
208
- return { success: false, error: error.message };
209
- // Aggregate by source
210
- const sourceMap = {};
211
- for (const row of data || []) {
212
- const src = row.source;
213
- if (!sourceMap[src])
214
- sourceMap[src] = { source: src, count: 0, errors: 0, last_seen: row.created_at };
215
- sourceMap[src].count++;
216
- if (row.severity === "error")
217
- sourceMap[src].errors++;
218
- if (row.created_at > sourceMap[src].last_seen)
219
- sourceMap[src].last_seen = row.created_at;
220
- }
221
- const sources = Object.values(sourceMap).sort((a, b) => b.count - a.count);
222
- return { success: true, data: { sources, total_entries: data?.length || 0, hours_back: hoursBack } };
322
+ const ch = getClickHouseClient();
323
+ if (!ch.isEnabled)
324
+ return { success: false, error: "ClickHouse not configured" };
325
+ const storeF = sid ? `AND store_id = '${esc(sid)}'` : "";
326
+ const sources = await ch.query(`
327
+ SELECT
328
+ source,
329
+ count() AS count,
330
+ countIf(severity = 'error') AS errors,
331
+ max(started_at) AS last_seen
332
+ FROM ai_spans
333
+ WHERE started_at >= now() - INTERVAL ${hoursBack} HOUR
334
+ AND source IS NOT NULL AND source != ''
335
+ ${storeF}
336
+ GROUP BY source
337
+ ORDER BY count DESC
338
+ `);
339
+ return { success: true, data: { sources, total_entries: sources.reduce((s, r) => s + r.count, 0), hours_back: hoursBack } };
223
340
  }
224
341
  default:
225
342
  return { success: false, error: `Unknown telemetry action: ${args.action}. Available: conversation_detail, conversations, agent_performance, tool_analytics, tool_timeline, trace, span_detail, error_patterns, token_usage, sources. For activity logs and inventory changes, use the audit_trail tool instead.` };
@@ -0,0 +1,6 @@
1
+ import type { SupabaseClient } from "@supabase/supabase-js";
2
+ export declare function handleRemoveBg(sb: SupabaseClient, args: Record<string, unknown>, storeId?: string): Promise<{
3
+ success: boolean;
4
+ data?: unknown;
5
+ error?: string;
6
+ }>;