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
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { WebSocketServer, WebSocket } from "ws";
|
|
14
14
|
import { randomUUID, createHash } from "node:crypto";
|
|
15
15
|
import { createClient } from "@supabase/supabase-js";
|
|
16
|
+
import { queueSpan, auditRowToSpan } from "./lib/clickhouse-buffer.js";
|
|
16
17
|
// ============================================================================
|
|
17
18
|
// CONNECTION POOL
|
|
18
19
|
// ============================================================================
|
|
@@ -20,7 +21,12 @@ import { createClient } from "@supabase/supabase-js";
|
|
|
20
21
|
const agentPool = new Map();
|
|
21
22
|
/** Map<requestId, pending request> */
|
|
22
23
|
const pendingRequests = new Map();
|
|
23
|
-
|
|
24
|
+
/** Map<sessionId, RemoteDesktopSession> — active remote desktop relay sessions */
|
|
25
|
+
const rdSessions = new Map();
|
|
26
|
+
/** Map<sessionId, PortalRelaySession> — active portal sessions */
|
|
27
|
+
const portalSessions = new Map();
|
|
28
|
+
const PORTAL_MARKER = 0x01;
|
|
29
|
+
const MAX_AGENTS_PER_STORE = 25;
|
|
24
30
|
const HEARTBEAT_INTERVAL_MS = 30_000;
|
|
25
31
|
const PONG_TIMEOUT_MS = 45_000;
|
|
26
32
|
const MAX_OUTPUT_CHARS = 500 * 1024; // 500KB safety cap — context_management handles limits
|
|
@@ -72,6 +78,7 @@ export function initLocalAgentGateway(server) {
|
|
|
72
78
|
function handleConnection(ws, _req) {
|
|
73
79
|
let authenticated = false;
|
|
74
80
|
let agent = null;
|
|
81
|
+
let viewerSession = null;
|
|
75
82
|
// Auth timeout — must authenticate within 10 seconds
|
|
76
83
|
const authTimer = setTimeout(() => {
|
|
77
84
|
if (!authenticated) {
|
|
@@ -80,6 +87,45 @@ function handleConnection(ws, _req) {
|
|
|
80
87
|
}
|
|
81
88
|
}, 10_000);
|
|
82
89
|
ws.on("message", async (raw) => {
|
|
90
|
+
// Handle binary messages — forward to paired viewer/agent in RD/portal sessions
|
|
91
|
+
if (typeof raw !== "string" && !isJsonString(raw)) {
|
|
92
|
+
const buf = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
|
|
93
|
+
// Portal binary frames (marker 0x01) — route by session UUID
|
|
94
|
+
if (buf.length > 17 && buf[0] === PORTAL_MARKER) {
|
|
95
|
+
const sessionUuid = buf.subarray(1, 17).toString("hex");
|
|
96
|
+
const sessionId = `${sessionUuid.slice(0, 8)}-${sessionUuid.slice(8, 12)}-${sessionUuid.slice(12, 16)}-${sessionUuid.slice(16, 20)}-${sessionUuid.slice(20)}`;
|
|
97
|
+
const portalSession = portalSessions.get(sessionId);
|
|
98
|
+
if (portalSession && agent) {
|
|
99
|
+
// Forward to the other side
|
|
100
|
+
const targetAgentId = agent.agentId === portalSession.initiatorAgentId
|
|
101
|
+
? portalSession.targetAgentId
|
|
102
|
+
: portalSession.initiatorAgentId;
|
|
103
|
+
const target = pickAgentById(targetAgentId);
|
|
104
|
+
if (target?.ws.readyState === WebSocket.OPEN) {
|
|
105
|
+
target.ws.send(buf);
|
|
106
|
+
portalSession.bytesRelayed += buf.length;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
// Remote desktop binary frames (no marker or 0x00) — existing behavior
|
|
112
|
+
if (agent) {
|
|
113
|
+
for (const [, session] of rdSessions) {
|
|
114
|
+
if (session.agentId === agent.agentId && session.viewerWs.readyState === WebSocket.OPEN) {
|
|
115
|
+
session.viewerWs.send(buf);
|
|
116
|
+
session.framesRelayed++;
|
|
117
|
+
session.bytesRelayed += buf.length;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (viewerSession) {
|
|
122
|
+
const target = pickAgentById(viewerSession.agentId);
|
|
123
|
+
if (target?.ws.readyState === WebSocket.OPEN) {
|
|
124
|
+
target.ws.send(buf);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
83
129
|
let msg;
|
|
84
130
|
try {
|
|
85
131
|
msg = JSON.parse(raw.toString());
|
|
@@ -88,6 +134,54 @@ function handleConnection(ws, _req) {
|
|
|
88
134
|
send(ws, { type: "error", error: "Invalid JSON" });
|
|
89
135
|
return;
|
|
90
136
|
}
|
|
137
|
+
// Handle viewer auth (remote desktop relay)
|
|
138
|
+
if (msg.type === "auth" && msg.role === "viewer") {
|
|
139
|
+
clearTimeout(authTimer);
|
|
140
|
+
const viewerAuth = msg;
|
|
141
|
+
const authResult = await authenticateViewer(viewerAuth);
|
|
142
|
+
if (!authResult) {
|
|
143
|
+
send(ws, { type: "error", error: "Invalid token or unauthorized" });
|
|
144
|
+
ws.close(4003, "Authentication failed");
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
authenticated = true;
|
|
148
|
+
// Find target agent with remote_desktop capability
|
|
149
|
+
const targetAgent = pickAgent(authResult.storeId, undefined, viewerAuth.target_node_id);
|
|
150
|
+
if (!targetAgent || !targetAgent.capabilities.includes("remote_desktop")) {
|
|
151
|
+
send(ws, { type: "remote_desktop_response", success: false, error: "No remote desktop capable node online" });
|
|
152
|
+
ws.close(4004, "No target node");
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
// Create relay session
|
|
156
|
+
const sessionId = randomUUID();
|
|
157
|
+
viewerSession = {
|
|
158
|
+
sessionId,
|
|
159
|
+
viewerWs: ws,
|
|
160
|
+
agentId: targetAgent.agentId,
|
|
161
|
+
storeId: authResult.storeId,
|
|
162
|
+
userId: authResult.userId,
|
|
163
|
+
startedAt: new Date(),
|
|
164
|
+
framesRelayed: 0,
|
|
165
|
+
bytesRelayed: 0,
|
|
166
|
+
};
|
|
167
|
+
rdSessions.set(sessionId, viewerSession);
|
|
168
|
+
// Tell the agent to start a remote desktop session
|
|
169
|
+
send(targetAgent.ws, {
|
|
170
|
+
type: "remote_desktop",
|
|
171
|
+
request_id: randomUUID(),
|
|
172
|
+
type_inner: "remote_desktop_request",
|
|
173
|
+
session_id: sessionId,
|
|
174
|
+
viewer_user_id: authResult.userId,
|
|
175
|
+
});
|
|
176
|
+
send(ws, {
|
|
177
|
+
type: "remote_desktop_response",
|
|
178
|
+
success: true,
|
|
179
|
+
session_id: sessionId,
|
|
180
|
+
agent_hostname: targetAgent.hostname,
|
|
181
|
+
});
|
|
182
|
+
console.log(`[remote-desktop] Relay session ${sessionId}: viewer=${authResult.userId} → agent=${targetAgent.agentId}`);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
91
185
|
// Handle auth
|
|
92
186
|
if (msg.type === "auth") {
|
|
93
187
|
clearTimeout(authTimer);
|
|
@@ -101,6 +195,7 @@ function handleConnection(ws, _req) {
|
|
|
101
195
|
ws,
|
|
102
196
|
storeId: authResult.storeId.toLowerCase(),
|
|
103
197
|
userId: authResult.userId,
|
|
198
|
+
nodeId: authResult.nodeId,
|
|
104
199
|
capabilities: msg.capabilities || [],
|
|
105
200
|
connectedAt: new Date(),
|
|
106
201
|
lastPong: Date.now(),
|
|
@@ -125,25 +220,34 @@ function handleConnection(ws, _req) {
|
|
|
125
220
|
message: `Connected to SwagManager. ${agent.capabilities.length} local tools registered.`,
|
|
126
221
|
});
|
|
127
222
|
console.log(`[local-agent] Agent connected: store=${agent.storeId} platform=${agent.platform} hostname=${agent.hostname} tools=${agent.capabilities.length}`);
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
223
|
+
// Telemetry → ClickHouse
|
|
224
|
+
queueSpan(auditRowToSpan({
|
|
225
|
+
action: "local_agent.connected",
|
|
226
|
+
severity: "info",
|
|
227
|
+
store_id: agent.storeId,
|
|
228
|
+
resource_type: "local_agent",
|
|
229
|
+
resource_id: agent.agentId,
|
|
230
|
+
source: "local_agent",
|
|
231
|
+
service_name: "agent-server",
|
|
232
|
+
span_kind: "INTERNAL",
|
|
233
|
+
status_code: "OK",
|
|
234
|
+
start_time: new Date().toISOString(),
|
|
235
|
+
end_time: new Date().toISOString(),
|
|
236
|
+
details: {
|
|
237
|
+
platform: agent.platform,
|
|
238
|
+
hostname: agent.hostname,
|
|
239
|
+
capabilities: agent.capabilities,
|
|
240
|
+
},
|
|
241
|
+
user_id: agent.userId,
|
|
242
|
+
}));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
// Handle viewer input forwarding (after auth)
|
|
246
|
+
if (authenticated && viewerSession && !agent) {
|
|
247
|
+
// Forward input events from viewer to the target agent
|
|
248
|
+
const target = pickAgentById(viewerSession.agentId);
|
|
249
|
+
if (target?.ws.readyState === WebSocket.OPEN) {
|
|
250
|
+
send(target.ws, { type: "remote_desktop", ...msg });
|
|
147
251
|
}
|
|
148
252
|
return;
|
|
149
253
|
}
|
|
@@ -156,6 +260,72 @@ function handleConnection(ws, _req) {
|
|
|
156
260
|
agent.lastPong = Date.now();
|
|
157
261
|
return;
|
|
158
262
|
}
|
|
263
|
+
// Handle portal messages
|
|
264
|
+
if (msg.type === "portal_request" && agent) {
|
|
265
|
+
// Initiator wants to connect to a target node
|
|
266
|
+
const targetAgent = pickAgent(agent.storeId, undefined, msg.target_node_id);
|
|
267
|
+
if (!targetAgent) {
|
|
268
|
+
send(ws, { type: "portal_reject", session_id: msg.session_id, reason: "Target node not online" });
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
// Forward request to target agent
|
|
272
|
+
send(targetAgent.ws, {
|
|
273
|
+
type: "portal_request",
|
|
274
|
+
session_id: msg.session_id,
|
|
275
|
+
capabilities: msg.capabilities,
|
|
276
|
+
requester_node_id: agent.nodeId,
|
|
277
|
+
requester_hostname: agent.hostname,
|
|
278
|
+
requester_role: msg.requester_role || "admin",
|
|
279
|
+
});
|
|
280
|
+
// Pre-register session so binary routing works once accepted
|
|
281
|
+
portalSessions.set(msg.session_id, {
|
|
282
|
+
sessionId: msg.session_id,
|
|
283
|
+
initiatorAgentId: agent.agentId,
|
|
284
|
+
targetAgentId: targetAgent.agentId,
|
|
285
|
+
storeId: agent.storeId,
|
|
286
|
+
capabilities: msg.capabilities || [],
|
|
287
|
+
startedAt: new Date(),
|
|
288
|
+
bytesRelayed: 0,
|
|
289
|
+
});
|
|
290
|
+
console.log(`[portal] Session ${msg.session_id}: ${agent.hostname} → ${targetAgent.hostname}`);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (msg.type === "portal_accept" && agent) {
|
|
294
|
+
const session = portalSessions.get(msg.session_id);
|
|
295
|
+
if (session) {
|
|
296
|
+
const initiator = pickAgentById(session.initiatorAgentId);
|
|
297
|
+
if (initiator) {
|
|
298
|
+
send(initiator.ws, { type: "portal_accept", session_id: msg.session_id, hostname: agent.hostname });
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
if (msg.type === "portal_reject" && agent) {
|
|
304
|
+
const session = portalSessions.get(msg.session_id);
|
|
305
|
+
if (session) {
|
|
306
|
+
const initiator = pickAgentById(session.initiatorAgentId);
|
|
307
|
+
if (initiator) {
|
|
308
|
+
send(initiator.ws, { type: "portal_reject", session_id: msg.session_id, reason: msg.reason });
|
|
309
|
+
}
|
|
310
|
+
portalSessions.delete(msg.session_id);
|
|
311
|
+
}
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (msg.type === "portal_close" && agent) {
|
|
315
|
+
const session = portalSessions.get(msg.session_id);
|
|
316
|
+
if (session) {
|
|
317
|
+
// Notify the other side
|
|
318
|
+
const otherAgentId = agent.agentId === session.initiatorAgentId
|
|
319
|
+
? session.targetAgentId : session.initiatorAgentId;
|
|
320
|
+
const other = pickAgentById(otherAgentId);
|
|
321
|
+
if (other) {
|
|
322
|
+
send(other.ws, { type: "portal_close", session_id: msg.session_id });
|
|
323
|
+
}
|
|
324
|
+
portalSessions.delete(msg.session_id);
|
|
325
|
+
console.log(`[portal] Session ${msg.session_id} closed (${session.bytesRelayed} bytes relayed)`);
|
|
326
|
+
}
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
159
329
|
// Handle tool discovery response
|
|
160
330
|
if (msg.type === "tools") {
|
|
161
331
|
agent.capabilities = msg.tools;
|
|
@@ -196,7 +366,36 @@ function handleConnection(ws, _req) {
|
|
|
196
366
|
agentPool.delete(agent.storeId);
|
|
197
367
|
}
|
|
198
368
|
console.log(`[local-agent] Agent disconnected: ${agent.agentId} (store=${agent.storeId})`);
|
|
199
|
-
//
|
|
369
|
+
// Immediately fail all pending requests for this agent
|
|
370
|
+
failPendingRequestsForAgent(agent.agentId, `Agent disconnected (${agent.hostname})`);
|
|
371
|
+
// End any portal sessions through this agent
|
|
372
|
+
for (const [sessionId, session] of portalSessions) {
|
|
373
|
+
if (session.initiatorAgentId === agent.agentId || session.targetAgentId === agent.agentId) {
|
|
374
|
+
const otherAgentId = agent.agentId === session.initiatorAgentId
|
|
375
|
+
? session.targetAgentId : session.initiatorAgentId;
|
|
376
|
+
const other = pickAgentById(otherAgentId);
|
|
377
|
+
if (other) {
|
|
378
|
+
send(other.ws, { type: "portal_close", session_id: sessionId });
|
|
379
|
+
}
|
|
380
|
+
portalSessions.delete(sessionId);
|
|
381
|
+
console.log(`[portal] Session ${sessionId} ended (agent disconnected)`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// End any remote desktop sessions through this agent
|
|
385
|
+
for (const [sessionId, session] of rdSessions) {
|
|
386
|
+
if (session.agentId === agent.agentId) {
|
|
387
|
+
send(session.viewerWs, { type: "session_end", reason: "agent_disconnected" });
|
|
388
|
+
session.viewerWs.close(1001, "Agent disconnected");
|
|
389
|
+
rdSessions.delete(sessionId);
|
|
390
|
+
console.log(`[remote-desktop] Session ${sessionId} ended (agent disconnected)`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
// Clean up viewer sessions
|
|
395
|
+
if (viewerSession) {
|
|
396
|
+
rdSessions.delete(viewerSession.sessionId);
|
|
397
|
+
console.log(`[remote-desktop] Session ${viewerSession.sessionId} ended (viewer disconnected)`);
|
|
398
|
+
viewerSession = null;
|
|
200
399
|
}
|
|
201
400
|
});
|
|
202
401
|
ws.on("error", (err) => {
|
|
@@ -210,21 +409,62 @@ async function authenticateAgent(msg) {
|
|
|
210
409
|
if (!supabaseClient || !msg.api_key)
|
|
211
410
|
return null;
|
|
212
411
|
const keyHash = createHash("sha256").update(msg.api_key).digest("hex");
|
|
213
|
-
//
|
|
412
|
+
// 1. Try api_keys table first (standard API keys)
|
|
214
413
|
const { data, error } = await supabaseClient
|
|
215
414
|
.from("api_keys")
|
|
216
415
|
.select("id, store_id, owner_user_id")
|
|
217
416
|
.eq("key_hash", keyHash)
|
|
218
417
|
.eq("is_active", true)
|
|
219
418
|
.single();
|
|
220
|
-
if (
|
|
419
|
+
if (data && !error) {
|
|
420
|
+
// Update last_used_at
|
|
421
|
+
supabaseClient.from("api_keys")
|
|
422
|
+
.update({ last_used_at: new Date().toISOString() })
|
|
423
|
+
.eq("id", data.id)
|
|
424
|
+
.then(() => { });
|
|
425
|
+
return { storeId: data.store_id, userId: data.owner_user_id || null, nodeId: null };
|
|
426
|
+
}
|
|
427
|
+
// 2. Fall back to nodes table (node daemon API keys)
|
|
428
|
+
const { data: nodeData, error: nodeErr } = await supabaseClient
|
|
429
|
+
.from("nodes")
|
|
430
|
+
.select("id, store_id")
|
|
431
|
+
.eq("api_key_hash", keyHash)
|
|
432
|
+
.single();
|
|
433
|
+
if (nodeData && !nodeErr) {
|
|
434
|
+
// Update node last_heartbeat on gateway connect
|
|
435
|
+
supabaseClient.from("nodes")
|
|
436
|
+
.update({ status: "online", last_heartbeat: new Date().toISOString() })
|
|
437
|
+
.eq("id", nodeData.id)
|
|
438
|
+
.then(() => { });
|
|
439
|
+
return { storeId: nodeData.store_id, userId: null, nodeId: nodeData.id };
|
|
440
|
+
}
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Authenticate a viewer connection using Supabase JWT.
|
|
445
|
+
*/
|
|
446
|
+
async function authenticateViewer(msg) {
|
|
447
|
+
if (!supabaseClient || !msg.token)
|
|
221
448
|
return null;
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
449
|
+
try {
|
|
450
|
+
// Verify JWT with Supabase auth
|
|
451
|
+
const { data: { user }, error } = await supabaseClient.auth.getUser(msg.token);
|
|
452
|
+
if (error || !user)
|
|
453
|
+
return null;
|
|
454
|
+
// Verify user has access to the requested store
|
|
455
|
+
const { data: storeAccess } = await supabaseClient
|
|
456
|
+
.from("store_users")
|
|
457
|
+
.select("store_id")
|
|
458
|
+
.eq("user_id", user.id)
|
|
459
|
+
.eq("store_id", msg.store_id)
|
|
460
|
+
.single();
|
|
461
|
+
if (!storeAccess)
|
|
462
|
+
return null;
|
|
463
|
+
return { storeId: msg.store_id, userId: user.id };
|
|
464
|
+
}
|
|
465
|
+
catch {
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
228
468
|
}
|
|
229
469
|
// ============================================================================
|
|
230
470
|
// PUBLIC API — used by local-agent handler
|
|
@@ -250,6 +490,7 @@ export function getAgentInfo(storeId) {
|
|
|
250
490
|
capabilities: a.capabilities,
|
|
251
491
|
connected_at: a.connectedAt.toISOString(),
|
|
252
492
|
uptime_seconds: Math.floor((Date.now() - a.connectedAt.getTime()) / 1000),
|
|
493
|
+
node_id: a.nodeId,
|
|
253
494
|
}));
|
|
254
495
|
}
|
|
255
496
|
/**
|
|
@@ -265,6 +506,16 @@ export async function executeOnLocalAgent(storeId, command, options = {}) {
|
|
|
265
506
|
}
|
|
266
507
|
const requestId = randomUUID();
|
|
267
508
|
const timeout = Math.min(options.timeout || 30000, 600000);
|
|
509
|
+
const msg = {
|
|
510
|
+
type: "exec",
|
|
511
|
+
request_id: requestId,
|
|
512
|
+
command,
|
|
513
|
+
session_id: options.session_id,
|
|
514
|
+
timeout,
|
|
515
|
+
};
|
|
516
|
+
const sent = send(agent.ws, msg);
|
|
517
|
+
if (!sent)
|
|
518
|
+
return { success: false, error: "Agent connection closed" };
|
|
268
519
|
return new Promise((resolve, reject) => {
|
|
269
520
|
const timer = setTimeout(() => {
|
|
270
521
|
pendingRequests.delete(requestId);
|
|
@@ -273,15 +524,7 @@ export async function executeOnLocalAgent(storeId, command, options = {}) {
|
|
|
273
524
|
error: `Local agent command timed out after ${timeout}ms`,
|
|
274
525
|
});
|
|
275
526
|
}, timeout + 5000); // 5s buffer over command timeout
|
|
276
|
-
pendingRequests.set(requestId, { resolve, reject, timer });
|
|
277
|
-
const msg = {
|
|
278
|
-
type: "exec",
|
|
279
|
-
request_id: requestId,
|
|
280
|
-
command,
|
|
281
|
-
session_id: options.session_id,
|
|
282
|
-
timeout,
|
|
283
|
-
};
|
|
284
|
-
send(agent.ws, msg);
|
|
527
|
+
pendingRequests.set(requestId, { resolve, reject, timer, agentId: agent.agentId });
|
|
285
528
|
});
|
|
286
529
|
}
|
|
287
530
|
/**
|
|
@@ -304,6 +547,16 @@ export async function executeToolOnLocalAgent(storeId, tool, args, options = {})
|
|
|
304
547
|
}
|
|
305
548
|
const requestId = randomUUID();
|
|
306
549
|
const timeout = Math.min(options.timeout || 30000, 600000);
|
|
550
|
+
const msg = {
|
|
551
|
+
type: "tool_exec",
|
|
552
|
+
request_id: requestId,
|
|
553
|
+
tool,
|
|
554
|
+
args,
|
|
555
|
+
timeout,
|
|
556
|
+
};
|
|
557
|
+
const sent = send(agent.ws, msg);
|
|
558
|
+
if (!sent)
|
|
559
|
+
return { success: false, error: "Agent connection closed" };
|
|
307
560
|
return new Promise((resolve, reject) => {
|
|
308
561
|
const timer = setTimeout(() => {
|
|
309
562
|
pendingRequests.delete(requestId);
|
|
@@ -312,15 +565,7 @@ export async function executeToolOnLocalAgent(storeId, tool, args, options = {})
|
|
|
312
565
|
error: `Local tool "${tool}" timed out after ${timeout}ms`,
|
|
313
566
|
});
|
|
314
567
|
}, timeout + 5000);
|
|
315
|
-
pendingRequests.set(requestId, { resolve, reject, timer });
|
|
316
|
-
const msg = {
|
|
317
|
-
type: "tool_exec",
|
|
318
|
-
request_id: requestId,
|
|
319
|
-
tool,
|
|
320
|
-
args,
|
|
321
|
-
timeout,
|
|
322
|
-
};
|
|
323
|
-
send(agent.ws, msg);
|
|
568
|
+
pendingRequests.set(requestId, { resolve, reject, timer, agentId: agent.agentId });
|
|
324
569
|
});
|
|
325
570
|
}
|
|
326
571
|
/**
|
|
@@ -335,6 +580,9 @@ export async function discoverLocalTools(storeId) {
|
|
|
335
580
|
};
|
|
336
581
|
}
|
|
337
582
|
const requestId = randomUUID();
|
|
583
|
+
const sent = send(agent.ws, { type: "discover", request_id: requestId });
|
|
584
|
+
if (!sent)
|
|
585
|
+
return { success: false, error: "Agent connection closed" };
|
|
338
586
|
return new Promise((resolve) => {
|
|
339
587
|
const timer = setTimeout(() => {
|
|
340
588
|
pendingRequests.delete(requestId);
|
|
@@ -344,8 +592,8 @@ export async function discoverLocalTools(storeId) {
|
|
|
344
592
|
resolve,
|
|
345
593
|
reject: () => resolve({ success: false, error: "Discovery failed" }),
|
|
346
594
|
timer,
|
|
595
|
+
agentId: agent.agentId,
|
|
347
596
|
});
|
|
348
|
-
send(agent.ws, { type: "discover", request_id: requestId });
|
|
349
597
|
});
|
|
350
598
|
}
|
|
351
599
|
/**
|
|
@@ -368,10 +616,38 @@ export function getGatewayStats() {
|
|
|
368
616
|
agents_by_store: agentsByStore,
|
|
369
617
|
};
|
|
370
618
|
}
|
|
619
|
+
/**
|
|
620
|
+
* Get active remote desktop sessions.
|
|
621
|
+
*/
|
|
622
|
+
export function getRemoteDesktopSessions() {
|
|
623
|
+
return Array.from(rdSessions.values()).map(s => ({
|
|
624
|
+
session_id: s.sessionId,
|
|
625
|
+
store_id: s.storeId,
|
|
626
|
+
user_id: s.userId,
|
|
627
|
+
agent_id: s.agentId,
|
|
628
|
+
started_at: s.startedAt.toISOString(),
|
|
629
|
+
frames_relayed: s.framesRelayed,
|
|
630
|
+
bytes_relayed: s.bytesRelayed,
|
|
631
|
+
}));
|
|
632
|
+
}
|
|
633
|
+
/**
|
|
634
|
+
* Get active portal sessions.
|
|
635
|
+
*/
|
|
636
|
+
export function getPortalSessions() {
|
|
637
|
+
return Array.from(portalSessions.values()).map(s => ({
|
|
638
|
+
session_id: s.sessionId,
|
|
639
|
+
store_id: s.storeId,
|
|
640
|
+
initiator_agent_id: s.initiatorAgentId,
|
|
641
|
+
target_agent_id: s.targetAgentId,
|
|
642
|
+
capabilities: s.capabilities,
|
|
643
|
+
started_at: s.startedAt.toISOString(),
|
|
644
|
+
bytes_relayed: s.bytesRelayed,
|
|
645
|
+
}));
|
|
646
|
+
}
|
|
371
647
|
// ============================================================================
|
|
372
648
|
// INTERNALS
|
|
373
649
|
// ============================================================================
|
|
374
|
-
function pickAgent(storeId, agentId) {
|
|
650
|
+
function pickAgent(storeId, agentId, nodeId) {
|
|
375
651
|
const agents = agentPool.get(storeId.toLowerCase());
|
|
376
652
|
if (!agents?.length)
|
|
377
653
|
return null;
|
|
@@ -383,12 +659,44 @@ function pickAgent(storeId, agentId) {
|
|
|
383
659
|
if (agentId) {
|
|
384
660
|
return open.find(a => a.agentId === agentId) || null;
|
|
385
661
|
}
|
|
662
|
+
// Target specific node if requested
|
|
663
|
+
if (nodeId) {
|
|
664
|
+
return open.find(a => a.nodeId === nodeId) || null;
|
|
665
|
+
}
|
|
386
666
|
// Pick agent with most recent pong (most responsive)
|
|
387
667
|
return open.reduce((best, a) => a.lastPong > best.lastPong ? a : best);
|
|
388
668
|
}
|
|
669
|
+
function pickAgentById(agentId) {
|
|
670
|
+
for (const [, agents] of agentPool) {
|
|
671
|
+
const found = agents.find(a => a.agentId === agentId && a.ws.readyState === WebSocket.OPEN);
|
|
672
|
+
if (found)
|
|
673
|
+
return found;
|
|
674
|
+
}
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
function isJsonString(data) {
|
|
678
|
+
if (typeof data === "string")
|
|
679
|
+
return true;
|
|
680
|
+
if (Buffer.isBuffer(data)) {
|
|
681
|
+
// Quick check: JSON starts with { or [
|
|
682
|
+
return data.length > 0 && (data[0] === 0x7b || data[0] === 0x5b);
|
|
683
|
+
}
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
389
686
|
function send(ws, data) {
|
|
390
687
|
if (ws.readyState === WebSocket.OPEN) {
|
|
391
688
|
ws.send(JSON.stringify(data));
|
|
689
|
+
return true;
|
|
690
|
+
}
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
693
|
+
function failPendingRequestsForAgent(agentId, reason) {
|
|
694
|
+
for (const [requestId, pending] of pendingRequests) {
|
|
695
|
+
if (pending.agentId === agentId) {
|
|
696
|
+
clearTimeout(pending.timer);
|
|
697
|
+
pendingRequests.delete(requestId);
|
|
698
|
+
pending.resolve({ success: false, error: reason });
|
|
699
|
+
}
|
|
392
700
|
}
|
|
393
701
|
}
|
|
394
702
|
function truncate(text) {
|
|
@@ -396,6 +704,38 @@ function truncate(text) {
|
|
|
396
704
|
return text;
|
|
397
705
|
return text.substring(0, MAX_OUTPUT_CHARS) + `\n...[truncated, ${text.length} total chars]`;
|
|
398
706
|
}
|
|
707
|
+
/**
|
|
708
|
+
* Execute a cluster command on the connected whale-node.
|
|
709
|
+
* Sends via WebSocket and waits for the result.
|
|
710
|
+
*/
|
|
711
|
+
export async function executeClusterCommand(storeId, args, options = {}) {
|
|
712
|
+
const agent = pickAgent(storeId, options.agent_id, options.node_id);
|
|
713
|
+
if (!agent) {
|
|
714
|
+
return {
|
|
715
|
+
success: false,
|
|
716
|
+
error: "No local agent connected for cluster commands.",
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
const requestId = randomUUID();
|
|
720
|
+
const timeout = Math.min(options.timeout || 60000, 600000);
|
|
721
|
+
const sent = send(agent.ws, {
|
|
722
|
+
type: "cluster_command",
|
|
723
|
+
request_id: requestId,
|
|
724
|
+
args,
|
|
725
|
+
});
|
|
726
|
+
if (!sent)
|
|
727
|
+
return { success: false, error: "Agent connection closed" };
|
|
728
|
+
return new Promise((resolve, reject) => {
|
|
729
|
+
const timer = setTimeout(() => {
|
|
730
|
+
pendingRequests.delete(requestId);
|
|
731
|
+
resolve({
|
|
732
|
+
success: false,
|
|
733
|
+
error: `Cluster command timed out after ${timeout}ms`,
|
|
734
|
+
});
|
|
735
|
+
}, timeout + 5000);
|
|
736
|
+
pendingRequests.set(requestId, { resolve, reject, timer, agentId: agent.agentId });
|
|
737
|
+
});
|
|
738
|
+
}
|
|
399
739
|
// ============================================================================
|
|
400
740
|
// SHUTDOWN
|
|
401
741
|
// ============================================================================
|
|
@@ -67,8 +67,18 @@ export class AnthropicAdapter {
|
|
|
67
67
|
}
|
|
68
68
|
if (effectiveBetas.length)
|
|
69
69
|
apiParams.betas = effectiveBetas;
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
// Safety: strip clear_thinking_20251015 edits when thinking is not enabled.
|
|
71
|
+
// Anthropic returns 400 if this strategy is present without thinking=enabled/adaptive.
|
|
72
|
+
let safeContextMgmt = context_management;
|
|
73
|
+
if (safeContextMgmt?.edits?.length && !thinking) {
|
|
74
|
+
const filtered = safeContextMgmt.edits.filter((e) => e.type !== "clear_thinking_20251015");
|
|
75
|
+
if (filtered.length !== safeContextMgmt.edits.length) {
|
|
76
|
+
console.log(`[proxy] Stripped ${safeContextMgmt.edits.length - filtered.length} clear_thinking edit(s) — thinking not enabled`);
|
|
77
|
+
safeContextMgmt = { ...safeContextMgmt, edits: filtered };
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (safeContextMgmt?.edits?.length)
|
|
81
|
+
apiParams.context_management = safeContextMgmt;
|
|
72
82
|
if (thinking)
|
|
73
83
|
apiParams.thinking = thinking;
|
|
74
84
|
// Map tool_choice to Anthropic format
|
|
@@ -34,7 +34,7 @@ const MODEL_MAX_OUTPUT_TOKENS = {
|
|
|
34
34
|
// ============================================================================
|
|
35
35
|
/** Convert Anthropic messages to Gemini Content format */
|
|
36
36
|
function anthropicToGeminiMessages(messages) {
|
|
37
|
-
const
|
|
37
|
+
const raw = [];
|
|
38
38
|
// Track tool_use_id → name for tool_result conversion
|
|
39
39
|
const toolIdMap = new Map();
|
|
40
40
|
for (const msg of messages) {
|
|
@@ -86,7 +86,22 @@ function anthropicToGeminiMessages(messages) {
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
if (parts.length > 0) {
|
|
89
|
-
|
|
89
|
+
raw.push({ role, parts });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Gemini requires strictly alternating user/model turns.
|
|
93
|
+
// After context compaction or multi-step tool use, consecutive same-role turns
|
|
94
|
+
// can appear — Gemini rejects these with INVALID_ARGUMENT.
|
|
95
|
+
// Fix: merge consecutive same-role turns by concatenating their parts.
|
|
96
|
+
const contents = [];
|
|
97
|
+
for (const turn of raw) {
|
|
98
|
+
const prev = contents[contents.length - 1];
|
|
99
|
+
if (prev && prev.role === turn.role) {
|
|
100
|
+
// Merge into the previous turn
|
|
101
|
+
prev.parts = [...(prev.parts || []), ...(turn.parts || [])];
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
contents.push({ role: turn.role, parts: turn.parts });
|
|
90
105
|
}
|
|
91
106
|
}
|
|
92
107
|
return contents;
|