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.
- package/bin/swagmanager-mcp.js +7 -0
- package/dist/cli/app.js +30 -2
- package/dist/cli/chat/ChatApp.d.ts +4 -4
- package/dist/cli/chat/ChatApp.js +114 -44
- package/dist/cli/chat/ChatInput.d.ts +13 -6
- package/dist/cli/chat/ChatInput.js +433 -89
- package/dist/cli/chat/MemoryManager.d.ts +15 -0
- package/dist/cli/chat/MemoryManager.js +61 -0
- package/dist/cli/chat/MessageList.d.ts +8 -0
- package/dist/cli/chat/MessageList.js +1 -1
- package/dist/cli/chat/NodeManager.d.ts +30 -0
- package/dist/cli/chat/NodeManager.js +89 -0
- package/dist/cli/chat/NodeSelector.d.ts +19 -0
- package/dist/cli/chat/NodeSelector.js +37 -0
- package/dist/cli/chat/PlanApproval.d.ts +17 -0
- package/dist/cli/chat/PlanApproval.js +82 -0
- package/dist/cli/chat/SessionManager.d.ts +16 -0
- package/dist/cli/chat/SessionManager.js +43 -0
- package/dist/cli/chat/SlashMenu.d.ts +38 -0
- package/dist/cli/chat/SlashMenu.js +208 -0
- package/dist/cli/chat/StatusBar.d.ts +16 -0
- package/dist/cli/chat/StatusBar.js +22 -0
- package/dist/cli/chat/ThemeSelector.d.ts +14 -0
- package/dist/cli/chat/ThemeSelector.js +29 -0
- package/dist/cli/chat/ToolIndicator.d.ts +8 -0
- package/dist/cli/chat/ToolIndicator.js +33 -9
- package/dist/cli/chat/hooks/useAgentLoop.d.ts +2 -1
- package/dist/cli/chat/hooks/useAgentLoop.js +22 -17
- package/dist/cli/chat/hooks/useSlashCommands.d.ts +19 -0
- package/dist/cli/chat/hooks/useSlashCommands.js +254 -15
- package/dist/cli/commands/config-cmd.js +4 -25
- package/dist/cli/commands/db.d.ts +13 -0
- package/dist/cli/commands/db.js +243 -0
- package/dist/cli/commands/doctor.js +6 -9
- package/dist/cli/commands/mcp.js +1 -20
- package/dist/cli/services/agent-events.d.ts +22 -1
- package/dist/cli/services/agent-events.js +9 -0
- package/dist/cli/services/agent-loop.js +66 -2
- package/dist/cli/services/agent-worker-base.js +21 -6
- package/dist/cli/services/api-retry.d.ts +25 -0
- package/dist/cli/services/api-retry.js +91 -0
- package/dist/cli/services/auth-service.d.ts +1 -1
- package/dist/cli/services/auth-service.js +40 -19
- package/dist/cli/services/background-processes.js +26 -2
- package/dist/cli/services/config-store.d.ts +13 -1
- package/dist/cli/services/config-store.js +116 -13
- package/dist/cli/services/format-server-response.js +12 -6
- package/dist/cli/services/ink-resize-fix.d.ts +18 -0
- package/dist/cli/services/ink-resize-fix.js +66 -0
- package/dist/cli/services/interactive-tools.d.ts +14 -0
- package/dist/cli/services/interactive-tools.js +47 -2
- package/dist/cli/services/keybinding-manager.js +1 -1
- package/dist/cli/services/local-tools.js +35 -2
- package/dist/cli/services/server-tools.js +175 -3
- package/dist/cli/services/subagent.js +15 -3
- package/dist/cli/services/system-prompt.js +5 -3
- package/dist/cli/services/task-decomposer.d.ts +35 -0
- package/dist/cli/services/task-decomposer.js +199 -0
- package/dist/cli/services/team-lead.d.ts +18 -0
- package/dist/cli/services/team-lead.js +80 -0
- package/dist/cli/services/teammate.js +5 -5
- package/dist/cli/services/telemetry.d.ts +8 -2
- package/dist/cli/services/telemetry.js +116 -92
- package/dist/cli/services/tools/agent-tools.d.ts +1 -0
- package/dist/cli/services/tools/agent-tools.js +50 -4
- package/dist/cli/services/tools/file-ops.d.ts +2 -0
- package/dist/cli/services/tools/file-ops.js +71 -19
- package/dist/cli/services/tools/shell-exec.js +22 -12
- package/dist/cli/shared/Theme.d.ts +1 -2
- package/dist/cli/shared/Theme.js +1 -1
- package/dist/cli/shared/WhaleBanner.d.ts +4 -1
- package/dist/cli/shared/WhaleBanner.js +12 -8
- package/dist/cli/shared/markdown.d.ts +5 -4
- package/dist/cli/shared/markdown.js +376 -334
- package/dist/cli/shared/theme-manager.d.ts +27 -0
- package/dist/cli/shared/theme-manager.js +178 -0
- package/dist/cli/shared/theme-presets.d.ts +16 -0
- package/dist/cli/shared/theme-presets.js +265 -0
- package/dist/index.js +0 -51
- package/dist/node/adapters/imessage.d.ts +10 -0
- package/dist/node/adapters/imessage.js +45 -6
- package/dist/node/cli.js +459 -8
- package/dist/node/config.d.ts +17 -0
- package/dist/node/gateway-client.d.ts +55 -0
- package/dist/node/gateway-client.js +201 -0
- package/dist/node/portal/clipboard.d.ts +28 -0
- package/dist/node/portal/clipboard.js +183 -0
- package/dist/node/portal/discovery.d.ts +29 -0
- package/dist/node/portal/discovery.js +61 -0
- package/dist/node/portal/forward.d.ts +30 -0
- package/dist/node/portal/forward.js +90 -0
- package/dist/node/portal/index.d.ts +47 -0
- package/dist/node/portal/index.js +250 -0
- package/dist/node/portal/multiplexer.d.ts +48 -0
- package/dist/node/portal/multiplexer.js +207 -0
- package/dist/node/portal/permissions.d.ts +36 -0
- package/dist/node/portal/permissions.js +131 -0
- package/dist/node/portal/protocol.d.ts +140 -0
- package/dist/node/portal/protocol.js +193 -0
- package/dist/node/portal/screen.d.ts +18 -0
- package/dist/node/portal/screen.js +93 -0
- package/dist/node/portal/session.d.ts +68 -0
- package/dist/node/portal/session.js +127 -0
- package/dist/node/portal/shell.d.ts +26 -0
- package/dist/node/portal/shell.js +142 -0
- package/dist/node/portal/stream.d.ts +43 -0
- package/dist/node/portal/stream.js +90 -0
- package/dist/node/portal/transfer.d.ts +33 -0
- package/dist/node/portal/transfer.js +231 -0
- package/dist/node/portal/ui.d.ts +16 -0
- package/dist/node/portal/ui.js +148 -0
- package/dist/node/remote-desktop/compile-helper.d.ts +13 -0
- package/dist/node/remote-desktop/compile-helper.js +73 -0
- package/dist/node/remote-desktop/index.d.ts +67 -0
- package/dist/node/remote-desktop/index.js +220 -0
- package/dist/node/remote-desktop/protocol.d.ts +96 -0
- package/dist/node/remote-desktop/protocol.js +67 -0
- package/dist/node/runtime.d.ts +8 -1
- package/dist/node/runtime.js +117 -9
- package/dist/server/handlers/__test-utils__/test-db.d.ts +25 -0
- package/dist/server/handlers/__test-utils__/test-db.js +128 -0
- package/dist/server/handlers/api-keys.js +26 -2
- package/dist/server/handlers/browser.d.ts +0 -4
- package/dist/server/handlers/browser.js +0 -46
- package/dist/server/handlers/catalog.js +37 -14
- package/dist/server/handlers/clickhouse.d.ts +10 -0
- package/dist/server/handlers/clickhouse.js +215 -0
- package/dist/server/handlers/comms.d.ts +308 -4
- package/dist/server/handlers/comms.js +444 -11
- package/dist/server/handlers/creations.js +1 -1
- package/dist/server/handlers/crm.d.ts +54 -8
- package/dist/server/handlers/crm.js +353 -68
- package/dist/server/handlers/embeddings.js +3 -3
- package/dist/server/handlers/enrichment.js +39 -55
- package/dist/server/handlers/inventory.js +1 -1
- package/dist/server/handlers/kali.d.ts +9 -1
- package/dist/server/handlers/kali.js +50 -1
- package/dist/server/handlers/media.d.ts +8 -0
- package/dist/server/handlers/media.js +902 -0
- package/dist/server/handlers/meta-ads.js +6 -3
- package/dist/server/handlers/nodes.d.ts +2 -0
- package/dist/server/handlers/nodes.js +331 -40
- package/dist/server/handlers/operations.d.ts +4 -6
- package/dist/server/handlers/operations.js +99 -38
- package/dist/server/handlers/platform.js +224 -107
- package/dist/server/handlers/remove-bg.d.ts +6 -0
- package/dist/server/handlers/remove-bg.js +96 -0
- package/dist/server/handlers/storefront.d.ts +6 -0
- package/dist/server/handlers/storefront.js +477 -0
- package/dist/server/handlers/supply-chain.js +21 -3
- package/dist/server/handlers/workflow-steps.js +87 -31
- package/dist/server/handlers/workflows.js +4 -1
- package/dist/server/index.js +334 -88
- package/dist/server/lib/clickhouse-buffer.d.ts +48 -0
- package/dist/server/lib/clickhouse-buffer.js +175 -0
- package/dist/server/lib/clickhouse-client.d.ts +112 -0
- package/dist/server/lib/clickhouse-client.js +141 -0
- package/dist/server/lib/coa-renderer.d.ts +91 -0
- package/dist/server/lib/coa-renderer.js +411 -0
- package/dist/server/lib/compaction-service.js +45 -1
- package/dist/server/lib/pdf-renderer.d.ts +143 -0
- package/dist/server/lib/pdf-renderer.js +867 -0
- package/dist/server/lib/react-pdf-layout.d.ts +40 -0
- package/dist/server/lib/react-pdf-layout.js +437 -0
- package/dist/server/lib/server-agent-loop.d.ts +2 -0
- package/dist/server/lib/server-agent-loop.js +61 -15
- package/dist/server/lib/server-subagent.d.ts +3 -0
- package/dist/server/lib/server-subagent.js +7 -4
- package/dist/server/lib/supabase-client.js +51 -3
- package/dist/server/lib/template-resolver.js +14 -4
- package/dist/server/lib/utils.js +15 -0
- package/dist/server/local-agent-gateway.d.ts +44 -0
- package/dist/server/local-agent-gateway.js +389 -49
- package/dist/server/providers/anthropic.js +12 -2
- package/dist/server/providers/gemini.js +17 -2
- package/dist/server/proxy-handlers.js +151 -0
- package/dist/server/tool-router.d.ts +2 -2
- package/dist/server/tool-router.js +25 -35
- package/dist/shared/agent-core.d.ts +5 -2
- package/dist/shared/agent-core.js +30 -4
- package/dist/shared/api-client.js +54 -3
- package/dist/shared/sse-parser.d.ts +1 -1
- package/dist/shared/sse-parser.js +5 -2
- package/dist/shared/tool-dispatch.js +1 -1
- package/package.json +16 -10
- package/dist/server/handlers/__test-utils__/mock-supabase.d.ts +0 -11
- 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
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
128
|
-
|
|
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
|
|
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 +
|
|
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
|
-
|
|
56
|
-
|
|
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:
|
|
89
|
+
audit_entries: spans,
|
|
69
90
|
message_count: msgResult.data?.length || 0,
|
|
70
|
-
audit_count:
|
|
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
|
|
122
|
+
// ---- tool_analytics: Per-tool performance metrics via direct ClickHouse ----
|
|
102
123
|
case "tool_analytics": {
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
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
|
|
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
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
262
|
+
// ---- error_patterns: Error correlation + burst detection via ClickHouse ----
|
|
146
263
|
case "error_patterns": {
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
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
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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.` };
|