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
package/dist/cli/commands/mcp.js
CHANGED
|
@@ -9,26 +9,7 @@
|
|
|
9
9
|
* whale mcp remove <name> Remove server
|
|
10
10
|
* whale mcp get <name> Show server config
|
|
11
11
|
*/
|
|
12
|
-
import {
|
|
13
|
-
import { join } from "path";
|
|
14
|
-
import { homedir } from "os";
|
|
15
|
-
const CONFIG_PATH = join(homedir(), ".swagmanager", "config.json");
|
|
16
|
-
function loadConfig() {
|
|
17
|
-
if (!existsSync(CONFIG_PATH))
|
|
18
|
-
return {};
|
|
19
|
-
try {
|
|
20
|
-
return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
return {};
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
function saveConfig(config) {
|
|
27
|
-
const dir = join(homedir(), ".swagmanager");
|
|
28
|
-
if (!existsSync(dir))
|
|
29
|
-
mkdirSync(dir, { recursive: true });
|
|
30
|
-
writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
31
|
-
}
|
|
12
|
+
import { loadConfig, saveConfig } from "../services/config-store.js";
|
|
32
13
|
function getServers(config) {
|
|
33
14
|
return config.mcpServers || {};
|
|
34
15
|
}
|
|
@@ -54,6 +54,18 @@ export interface ToolOutputEvent {
|
|
|
54
54
|
toolName: string;
|
|
55
55
|
line: string;
|
|
56
56
|
}
|
|
57
|
+
export interface ToolProgressEvent {
|
|
58
|
+
type: "tool_progress";
|
|
59
|
+
toolName: string;
|
|
60
|
+
progress: {
|
|
61
|
+
phase?: string;
|
|
62
|
+
elapsed_s: number;
|
|
63
|
+
stdout_lines: number;
|
|
64
|
+
stderr_lines: number;
|
|
65
|
+
stdout_bytes: number;
|
|
66
|
+
last_line?: string;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
57
69
|
export interface SubagentStartEvent {
|
|
58
70
|
type: "subagent_start";
|
|
59
71
|
id: string;
|
|
@@ -132,12 +144,20 @@ export interface TeamDoneEvent {
|
|
|
132
144
|
};
|
|
133
145
|
durationMs: number;
|
|
134
146
|
}
|
|
135
|
-
export
|
|
147
|
+
export interface AgentThinkingEvent {
|
|
148
|
+
type: "thinking";
|
|
149
|
+
chunkCount: number;
|
|
150
|
+
}
|
|
151
|
+
export type AgentEvent = AgentTextEvent | AgentThinkingEvent | AgentToolStartEvent | AgentToolEndEvent | AgentUsageEvent | AgentDoneEvent | AgentErrorEvent | AgentCompactEvent | ToolOutputEvent | ToolProgressEvent | SubagentStartEvent | SubagentProgressEvent | SubagentDoneEvent | SubagentToolStartEvent | SubagentToolEndEvent | TeamStartEvent | TeamProgressEvent | TeamTaskEvent | TeamDoneEvent;
|
|
136
152
|
export declare class AgentEventEmitter extends EventEmitter {
|
|
137
153
|
/**
|
|
138
154
|
* Emit text immediately — UI-side handles batching via single flush timer
|
|
139
155
|
*/
|
|
140
156
|
emitText(text: string): void;
|
|
157
|
+
/**
|
|
158
|
+
* Emit thinking chunk count — UI renders activity dots
|
|
159
|
+
*/
|
|
160
|
+
emitThinking(chunkCount: number): void;
|
|
141
161
|
/**
|
|
142
162
|
* No-op — kept for interface compat
|
|
143
163
|
*/
|
|
@@ -149,6 +169,7 @@ export declare class AgentEventEmitter extends EventEmitter {
|
|
|
149
169
|
emitError(error: string): void;
|
|
150
170
|
emitCompact(before: number, after: number, tokensSaved: number): void;
|
|
151
171
|
emitToolOutput(toolName: string, line: string): void;
|
|
172
|
+
emitToolProgress(toolName: string, progress: ToolProgressEvent["progress"]): void;
|
|
152
173
|
emitSubagentStart(id: string, agentType: string, model: string, description: string): void;
|
|
153
174
|
emitSubagentProgress(id: string, agentType: string, message: string, turn?: number, toolName?: string): void;
|
|
154
175
|
emitSubagentDone(id: string, agentType: string, success: boolean, output: string, tokens: {
|
|
@@ -19,6 +19,12 @@ export class AgentEventEmitter extends EventEmitter {
|
|
|
19
19
|
accumulated: "", // Set by consumer
|
|
20
20
|
});
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Emit thinking chunk count — UI renders activity dots
|
|
24
|
+
*/
|
|
25
|
+
emitThinking(chunkCount) {
|
|
26
|
+
this.emit("event", { type: "thinking", chunkCount });
|
|
27
|
+
}
|
|
22
28
|
/**
|
|
23
29
|
* No-op — kept for interface compat
|
|
24
30
|
*/
|
|
@@ -55,6 +61,9 @@ export class AgentEventEmitter extends EventEmitter {
|
|
|
55
61
|
emitToolOutput(toolName, line) {
|
|
56
62
|
this.emit("event", { type: "tool_output", toolName, line });
|
|
57
63
|
}
|
|
64
|
+
emitToolProgress(toolName, progress) {
|
|
65
|
+
this.emit("event", { type: "tool_progress", toolName, progress });
|
|
66
|
+
}
|
|
58
67
|
emitSubagentStart(id, agentType, model, description) {
|
|
59
68
|
this.emit("event", { type: "subagent_start", id, agentType, model, description });
|
|
60
69
|
}
|
|
@@ -17,11 +17,11 @@
|
|
|
17
17
|
* - system-prompt.ts (buildSystemPrompt)
|
|
18
18
|
*/
|
|
19
19
|
import { LOCAL_TOOL_DEFINITIONS, executeLocalTool, isLocalTool, } from "./local-tools.js";
|
|
20
|
-
import { INTERACTIVE_TOOL_DEFINITIONS, executeInteractiveTool, } from "./interactive-tools.js";
|
|
20
|
+
import { INTERACTIVE_TOOL_DEFINITIONS, executeInteractiveTool, waitForPlanApproval, } from "./interactive-tools.js";
|
|
21
21
|
import { loadConfig, resolveConfig, getProxyUrl } from "./config-store.js";
|
|
22
22
|
import { getValidToken, refreshSession } from "./auth-service.js";
|
|
23
23
|
import { isServerTool, loadServerToolDefinitions, executeServerTool, getServerStatus, setServerToolContext, } from "./server-tools.js";
|
|
24
|
-
import { nextTurn, createTurnContext, logSpan, generateSpanId, } from "./telemetry.js";
|
|
24
|
+
import { nextTurn, createTurnContext, logSpan, generateSpanId, flushCliSpans, } from "./telemetry.js";
|
|
25
25
|
import { captureError, addBreadcrumb } from "./error-logger.js";
|
|
26
26
|
import { setGlobalEmitter, clearGlobalEmitter, } from "./agent-events.js";
|
|
27
27
|
import { mcpClientManager } from "./mcp-client.js";
|
|
@@ -35,6 +35,7 @@ import { dispatchTools, buildAssistantContent } from "../../shared/tool-dispatch
|
|
|
35
35
|
import { loadMemory, addMemory, removeMemory, listMemories } from "./memory-manager.js";
|
|
36
36
|
import { refreshGitContext, resetGitContext } from "./git-context.js";
|
|
37
37
|
import { loadClaudeMd, reloadClaudeMd, resetClaudeMdCache } from "./claude-md-loader.js";
|
|
38
|
+
import { clearReadCache } from "./tools/file-ops.js";
|
|
38
39
|
import { setPermissionMode, getPermissionMode, isToolAllowedByPermission } from "./permission-modes.js";
|
|
39
40
|
import { setModel, setModelById, getModel, getModelShortName, estimateCostUsd } from "./model-manager.js";
|
|
40
41
|
import { saveSession, loadSession, listSessions, findLatestSessionForCwd } from "./session-persistence.js";
|
|
@@ -86,6 +87,7 @@ export function resetSessionState() {
|
|
|
86
87
|
sessionLoopDetector = null;
|
|
87
88
|
resetGitContext();
|
|
88
89
|
resetClaudeMdCache();
|
|
90
|
+
clearReadCache();
|
|
89
91
|
}
|
|
90
92
|
/** CLI-only: loop detector — persists session error state across turns (reset by resetSessionState) */
|
|
91
93
|
let sessionLoopDetector = null;
|
|
@@ -305,6 +307,7 @@ export async function runAgentLoop(opts) {
|
|
|
305
307
|
});
|
|
306
308
|
let sessionCostUsd = 0;
|
|
307
309
|
let compactionCount = 0;
|
|
310
|
+
const costWarningsEmitted = new Set();
|
|
308
311
|
const activeModel = getModel();
|
|
309
312
|
// Tool executor — routes to interactive, local, server, or MCP tools.
|
|
310
313
|
// Wraps execution with before/after hooks when hooks are loaded.
|
|
@@ -331,6 +334,24 @@ export async function runAgentLoop(opts) {
|
|
|
331
334
|
let result;
|
|
332
335
|
if (INTERACTIVE_TOOL_NAMES.has(name)) {
|
|
333
336
|
result = await executeInteractiveTool(name, effectiveInput);
|
|
337
|
+
// For exit_plan_mode: wait for UI approval, then map decision to result
|
|
338
|
+
if (name === "exit_plan_mode" && result.success) {
|
|
339
|
+
const decision = await waitForPlanApproval();
|
|
340
|
+
switch (decision.action) {
|
|
341
|
+
case "execute":
|
|
342
|
+
result = { success: true, output: `__PLAN_APPROVED_CLEAN__\n${result.output}` };
|
|
343
|
+
break;
|
|
344
|
+
case "edit":
|
|
345
|
+
result = { success: true, output: "Plan returned for revision. Make changes and use ExitPlanMode again when ready." };
|
|
346
|
+
break;
|
|
347
|
+
case "feedback":
|
|
348
|
+
result = { success: true, output: `User feedback on plan:\n\n${decision.feedback || "(no feedback provided)"}\n\nRevise the plan based on this feedback, then use ExitPlanMode again.` };
|
|
349
|
+
break;
|
|
350
|
+
case "cancel":
|
|
351
|
+
result = { success: true, output: "Plan cancelled by user. Do not proceed with implementation." };
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
334
355
|
}
|
|
335
356
|
else if (isLocalTool(name)) {
|
|
336
357
|
result = await executeLocalTool(name, effectiveInput);
|
|
@@ -449,6 +470,7 @@ export async function runAgentLoop(opts) {
|
|
|
449
470
|
},
|
|
450
471
|
});
|
|
451
472
|
// Process stream events with UI callbacks
|
|
473
|
+
let thinkingChunks = 0;
|
|
452
474
|
const result = await processStreamWithCallbacks(parseSSEStream(stream, abortSignal), {
|
|
453
475
|
onText: (text) => {
|
|
454
476
|
if (emitter) {
|
|
@@ -458,6 +480,10 @@ export async function runAgentLoop(opts) {
|
|
|
458
480
|
callbacks.onText(text);
|
|
459
481
|
}
|
|
460
482
|
},
|
|
483
|
+
onThinking: () => {
|
|
484
|
+
thinkingChunks++;
|
|
485
|
+
emitter?.emitThinking(thinkingChunks);
|
|
486
|
+
},
|
|
461
487
|
onToolStart: (name, input) => {
|
|
462
488
|
// NOTE: Do NOT call callbacks.onToolStart here — dispatchTools.onStart
|
|
463
489
|
// fires it once per tool at execution time. Calling it here too would
|
|
@@ -489,6 +515,21 @@ export async function runAgentLoop(opts) {
|
|
|
489
515
|
totalCacheRead += result.usage.cacheReadTokens;
|
|
490
516
|
totalThinking += result.thinkingTokens;
|
|
491
517
|
sessionCostUsd += estimateCostUsd(result.usage.inputTokens, result.usage.outputTokens, currentModel, result.thinkingTokens, result.usage.cacheReadTokens, result.usage.cacheCreationTokens);
|
|
518
|
+
// Graduated cost warnings
|
|
519
|
+
if (opts.maxBudgetUsd) {
|
|
520
|
+
for (const pct of [25, 50, 75]) {
|
|
521
|
+
if (!costWarningsEmitted.has(pct) && sessionCostUsd >= opts.maxBudgetUsd * (pct / 100)) {
|
|
522
|
+
costWarningsEmitted.add(pct);
|
|
523
|
+
const warnMsg = `\n[Cost warning: ${pct}% of budget used ($${sessionCostUsd.toFixed(2)}/$${opts.maxBudgetUsd.toFixed(2)}). ${pct >= 75 ? "Wrap up soon." : ""}]`;
|
|
524
|
+
if (emitter) {
|
|
525
|
+
emitter.emitText(warnMsg);
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
callbacks.onText(warnMsg);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
492
533
|
// Server-side context management notification
|
|
493
534
|
if (result.contextManagementApplied) {
|
|
494
535
|
callbacks.onAutoCompact?.(messages.length, messages.length, 0);
|
|
@@ -616,6 +657,25 @@ export async function runAgentLoop(opts) {
|
|
|
616
657
|
compactionContent: result.compactionContent,
|
|
617
658
|
});
|
|
618
659
|
messages.push({ role: "assistant", content: assistantContent });
|
|
660
|
+
// Check for __PLAN_APPROVED_CLEAN__ marker — clear context and start fresh with just the plan
|
|
661
|
+
const planCleanMarker = "__PLAN_APPROVED_CLEAN__\n";
|
|
662
|
+
const hasCleanPlanApproval = finalToolResults.some((tr) => {
|
|
663
|
+
const content = typeof tr.content === "string" ? tr.content : "";
|
|
664
|
+
return content.startsWith(planCleanMarker);
|
|
665
|
+
});
|
|
666
|
+
if (hasCleanPlanApproval) {
|
|
667
|
+
// Extract plan content from the marker
|
|
668
|
+
const planResult = finalToolResults.find((tr) => {
|
|
669
|
+
const content = typeof tr.content === "string" ? tr.content : "";
|
|
670
|
+
return content.startsWith(planCleanMarker);
|
|
671
|
+
});
|
|
672
|
+
const planText = planResult.content.slice(planCleanMarker.length);
|
|
673
|
+
const beforeCount = messages.length;
|
|
674
|
+
messages.length = 0;
|
|
675
|
+
messages.push({ role: "user", content: [{ type: "text", text: `Implement this plan:\n\n${planText}` }] });
|
|
676
|
+
callbacks.onAutoCompact?.(beforeCount, 1, 0);
|
|
677
|
+
continue;
|
|
678
|
+
}
|
|
619
679
|
messages.push({ role: "user", content: finalToolResults });
|
|
620
680
|
// Non-native compaction for OpenAI/Gemini — fires after tool results appended
|
|
621
681
|
const compactionCfg = getCompactionConfig(currentModel);
|
|
@@ -683,6 +743,8 @@ export async function runAgentLoop(opts) {
|
|
|
683
743
|
});
|
|
684
744
|
const turnCostUsd = estimateCostUsd(totalIn, totalOut, activeModel, totalThinking, totalCacheRead, totalCacheCreation);
|
|
685
745
|
callbacks.onUsage(totalIn, totalOut, totalThinking, activeModel, turnCostUsd, totalCacheRead, totalCacheCreation);
|
|
746
|
+
// Flush telemetry spans to server before session ends
|
|
747
|
+
flushCliSpans();
|
|
686
748
|
// Fire SessionEnd hook (non-blocking)
|
|
687
749
|
if (hooks.length > 0) {
|
|
688
750
|
runSessionHook(hooks, "SessionEnd", { session_id: `turn-${sessionStart}` }).catch(() => { });
|
|
@@ -719,6 +781,8 @@ export async function runAgentLoop(opts) {
|
|
|
719
781
|
tags: { model: activeModel, turn: String(turnNum) },
|
|
720
782
|
});
|
|
721
783
|
}
|
|
784
|
+
// Flush telemetry on error path too
|
|
785
|
+
flushCliSpans();
|
|
722
786
|
emitter?.emitError(errorMsg);
|
|
723
787
|
if (emitter)
|
|
724
788
|
clearGlobalEmitter();
|
|
@@ -13,6 +13,7 @@ import { getProxyUrl, resolveConfig } from "./config-store.js";
|
|
|
13
13
|
import { getValidToken, refreshSession } from "./auth-service.js";
|
|
14
14
|
import { parseSSEStream, collectStreamResult } from "../../shared/sse-parser.js";
|
|
15
15
|
import { callServerProxy, buildAPIRequest, buildSystemBlocks, } from "../../shared/api-client.js";
|
|
16
|
+
import { addPromptCaching } from "../../shared/agent-core.js";
|
|
16
17
|
import { isLocalTool, executeLocalTool, } from "./local-tools.js";
|
|
17
18
|
import { isServerTool, executeServerTool, } from "./server-tools.js";
|
|
18
19
|
// ============================================================================
|
|
@@ -20,7 +21,7 @@ import { isServerTool, executeServerTool, } from "./server-tools.js";
|
|
|
20
21
|
// ============================================================================
|
|
21
22
|
/** Tools that spawn workers/subprocesses and need much longer than 2 min */
|
|
22
23
|
const LONG_RUNNING_TOOL_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
|
|
23
|
-
const LONG_RUNNING_TOOLS = new Set(["team_create", "task", "delegate_task"]);
|
|
24
|
+
const LONG_RUNNING_TOOLS = new Set(["team_create", "task", "delegate_task", "exit_plan_mode", "ask_user_question"]);
|
|
24
25
|
// ============================================================================
|
|
25
26
|
// API CLIENT — unified proxy caller
|
|
26
27
|
// ============================================================================
|
|
@@ -87,13 +88,15 @@ export async function callAgentAPI(opts) {
|
|
|
87
88
|
tools = [...opts.tools];
|
|
88
89
|
}
|
|
89
90
|
const system = buildSystemBlocks(opts.systemPrompt);
|
|
91
|
+
// Apply prompt caching to messages (tools already cached above)
|
|
92
|
+
const { messages: cachedMessages } = addPromptCaching([], opts.messages);
|
|
90
93
|
const { storeId } = resolveConfig();
|
|
91
94
|
const stream = await callServerProxy({
|
|
92
95
|
proxyUrl: getProxyUrl(),
|
|
93
96
|
token,
|
|
94
97
|
model: opts.modelId,
|
|
95
98
|
system,
|
|
96
|
-
messages:
|
|
99
|
+
messages: cachedMessages,
|
|
97
100
|
tools,
|
|
98
101
|
apiConfig,
|
|
99
102
|
storeId: storeId || undefined,
|
|
@@ -119,7 +122,7 @@ export async function callAgentAPI(opts) {
|
|
|
119
122
|
* Returns tool results ready to append to messages, plus metadata.
|
|
120
123
|
*/
|
|
121
124
|
export async function executeToolBlocks(opts) {
|
|
122
|
-
const { toolBlocks, loopDetector, callbacks, customExecutor, maxToolResultChars =
|
|
125
|
+
const { toolBlocks, loopDetector, callbacks, customExecutor, maxToolResultChars = 500_000, toolTimeoutMs, } = opts;
|
|
123
126
|
const toolResults = [];
|
|
124
127
|
const toolsUsed = [];
|
|
125
128
|
const customSignals = {};
|
|
@@ -187,9 +190,21 @@ export async function executeToolBlocks(opts) {
|
|
|
187
190
|
content: contentStr,
|
|
188
191
|
};
|
|
189
192
|
}
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
193
|
+
// Split into file-mutating (sequential) and read-only/server (parallel) batches.
|
|
194
|
+
// File writes/edits must be sequential to prevent race conditions on the same file.
|
|
195
|
+
const FILE_MUTATING_TOOLS = new Set(["write_file", "edit_file", "multi_edit", "notebook_edit", "run_command"]);
|
|
196
|
+
const sequential = toolBlocks.filter(tu => FILE_MUTATING_TOOLS.has(tu.name));
|
|
197
|
+
const parallel = toolBlocks.filter(tu => !FILE_MUTATING_TOOLS.has(tu.name));
|
|
198
|
+
// Run read-only/server tools in parallel
|
|
199
|
+
if (parallel.length > 0) {
|
|
200
|
+
const parallelResults = await Promise.all(parallel.map(tu => executeSingle(tu)));
|
|
201
|
+
toolResults.push(...parallelResults);
|
|
202
|
+
}
|
|
203
|
+
// Run file-mutating tools sequentially
|
|
204
|
+
for (const tu of sequential) {
|
|
205
|
+
const result = await executeSingle(tu);
|
|
206
|
+
toolResults.push(result);
|
|
207
|
+
}
|
|
193
208
|
return { toolResults, toolsUsed, customSignals };
|
|
194
209
|
}
|
|
195
210
|
// ============================================================================
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Retry Utility — shared exponential backoff for Anthropic API calls.
|
|
3
|
+
*
|
|
4
|
+
* Retries on: 429 (rate limit), 5xx (server error), 529 (overloaded),
|
|
5
|
+
* timeout errors, and network errors.
|
|
6
|
+
*
|
|
7
|
+
* Respects `retry-after` header on 429 responses.
|
|
8
|
+
* 429 gets a 4x delay multiplier since the server is actively rate-limiting.
|
|
9
|
+
*/
|
|
10
|
+
export interface RetryOptions {
|
|
11
|
+
maxRetries?: number;
|
|
12
|
+
baseDelayMs?: number;
|
|
13
|
+
maxDelayMs?: number;
|
|
14
|
+
label?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Call an async function with automatic retry and exponential backoff.
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* const response = await callWithRetry(
|
|
21
|
+
* () => client.messages.create({ ... }),
|
|
22
|
+
* { label: "decomposer" }
|
|
23
|
+
* );
|
|
24
|
+
*/
|
|
25
|
+
export declare function callWithRetry<T>(fn: () => Promise<T>, opts?: RetryOptions): Promise<T>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Retry Utility — shared exponential backoff for Anthropic API calls.
|
|
3
|
+
*
|
|
4
|
+
* Retries on: 429 (rate limit), 5xx (server error), 529 (overloaded),
|
|
5
|
+
* timeout errors, and network errors.
|
|
6
|
+
*
|
|
7
|
+
* Respects `retry-after` header on 429 responses.
|
|
8
|
+
* 429 gets a 4x delay multiplier since the server is actively rate-limiting.
|
|
9
|
+
*/
|
|
10
|
+
const RETRIABLE_STATUS_CODES = new Set([429, 500, 502, 503, 504, 529]);
|
|
11
|
+
const RETRIABLE_ERROR_PATTERNS = [
|
|
12
|
+
"ECONNRESET",
|
|
13
|
+
"ECONNREFUSED",
|
|
14
|
+
"ETIMEDOUT",
|
|
15
|
+
"ENOTFOUND",
|
|
16
|
+
"fetch failed",
|
|
17
|
+
"network",
|
|
18
|
+
"socket hang up",
|
|
19
|
+
"aborted",
|
|
20
|
+
];
|
|
21
|
+
function isRetriable(err) {
|
|
22
|
+
if (!err || typeof err !== "object")
|
|
23
|
+
return { retriable: false };
|
|
24
|
+
const e = err;
|
|
25
|
+
// Anthropic SDK errors expose `.status` for HTTP status codes
|
|
26
|
+
const status = e.status ?? e.statusCode;
|
|
27
|
+
if (status && RETRIABLE_STATUS_CODES.has(status)) {
|
|
28
|
+
let retryAfterMs;
|
|
29
|
+
// Check for retry-after header (Anthropic SDK surfaces headers on error)
|
|
30
|
+
const headers = e.headers;
|
|
31
|
+
if (headers?.["retry-after"]) {
|
|
32
|
+
const retryAfterSec = parseFloat(headers["retry-after"]);
|
|
33
|
+
if (!isNaN(retryAfterSec)) {
|
|
34
|
+
retryAfterMs = retryAfterSec * 1000;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return { retriable: true, statusCode: status, retryAfterMs };
|
|
38
|
+
}
|
|
39
|
+
// Check error message for network-level failures
|
|
40
|
+
const message = (e.message ?? "").toLowerCase();
|
|
41
|
+
const code = e.code ?? "";
|
|
42
|
+
if (RETRIABLE_ERROR_PATTERNS.some(p => message.includes(p.toLowerCase()) || code === p)) {
|
|
43
|
+
return { retriable: true };
|
|
44
|
+
}
|
|
45
|
+
return { retriable: false };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Call an async function with automatic retry and exponential backoff.
|
|
49
|
+
*
|
|
50
|
+
* Usage:
|
|
51
|
+
* const response = await callWithRetry(
|
|
52
|
+
* () => client.messages.create({ ... }),
|
|
53
|
+
* { label: "decomposer" }
|
|
54
|
+
* );
|
|
55
|
+
*/
|
|
56
|
+
export async function callWithRetry(fn, opts = {}) {
|
|
57
|
+
const maxRetries = opts.maxRetries ?? 3;
|
|
58
|
+
const baseDelayMs = opts.baseDelayMs ?? 2000;
|
|
59
|
+
const maxDelayMs = opts.maxDelayMs ?? 30000;
|
|
60
|
+
const label = opts.label ?? "api";
|
|
61
|
+
let lastError;
|
|
62
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
63
|
+
try {
|
|
64
|
+
return await fn();
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
lastError = err;
|
|
68
|
+
if (attempt >= maxRetries)
|
|
69
|
+
break;
|
|
70
|
+
const { retriable, statusCode, retryAfterMs } = isRetriable(err);
|
|
71
|
+
if (!retriable)
|
|
72
|
+
throw err;
|
|
73
|
+
// Exponential backoff: 2s → 8s → 30s
|
|
74
|
+
let delayMs = Math.min(baseDelayMs * Math.pow(2, attempt), maxDelayMs);
|
|
75
|
+
// 429 gets 4x multiplier — the server is rate-limiting us
|
|
76
|
+
if (statusCode === 429) {
|
|
77
|
+
delayMs = retryAfterMs
|
|
78
|
+
? Math.min(retryAfterMs, maxDelayMs)
|
|
79
|
+
: Math.min(delayMs * 4, maxDelayMs);
|
|
80
|
+
}
|
|
81
|
+
console.warn(`[${label}] Attempt ${attempt + 1}/${maxRetries + 1} failed` +
|
|
82
|
+
(statusCode ? ` (HTTP ${statusCode})` : "") +
|
|
83
|
+
` — retrying in ${(delayMs / 1000).toFixed(1)}s: ${err.message?.slice(0, 100)}`);
|
|
84
|
+
await sleep(delayMs);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
throw lastError;
|
|
88
|
+
}
|
|
89
|
+
function sleep(ms) {
|
|
90
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
91
|
+
}
|
|
@@ -25,6 +25,6 @@ export declare function signUp(email: string, password: string): Promise<AuthRes
|
|
|
25
25
|
export declare function refreshSession(): Promise<AuthResult>;
|
|
26
26
|
export declare function getValidToken(): Promise<string | null>;
|
|
27
27
|
export declare function signOut(): void;
|
|
28
|
-
export declare function getStoresForUser(accessToken: string,
|
|
28
|
+
export declare function getStoresForUser(accessToken: string, userId: string): Promise<StoreInfo[]>;
|
|
29
29
|
export declare function selectStore(storeId: string, storeName: string): void;
|
|
30
30
|
export declare function isLoggedIn(): boolean;
|
|
@@ -121,33 +121,54 @@ export function signOut() {
|
|
|
121
121
|
clearConfig();
|
|
122
122
|
}
|
|
123
123
|
// Get stores for the authenticated user
|
|
124
|
-
// RLS on the stores table
|
|
125
|
-
// same
|
|
126
|
-
|
|
124
|
+
// With a user JWT, RLS on the stores table handles access control directly —
|
|
125
|
+
// same approach the platform exchange endpoint uses. For service role keys,
|
|
126
|
+
// we fall back to store_members to prevent cross-tenant leakage.
|
|
127
|
+
export async function getStoresForUser(accessToken, userId) {
|
|
128
|
+
if (!userId)
|
|
129
|
+
return [];
|
|
127
130
|
const client = createAuthenticatedClient(accessToken);
|
|
128
|
-
//
|
|
129
|
-
const { data: stores, error } = await client
|
|
131
|
+
// Primary: query stores directly — RLS filters to user's stores
|
|
132
|
+
const { data: stores, error: storeError } = await client
|
|
130
133
|
.from("stores")
|
|
131
134
|
.select("id, store_name, slug")
|
|
132
135
|
.limit(20);
|
|
133
|
-
if (
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
136
|
+
if (!storeError && stores && stores.length > 0) {
|
|
137
|
+
return stores.map((s) => ({ id: s.id, name: s.store_name, slug: s.slug }));
|
|
138
|
+
}
|
|
139
|
+
// Fallback 1: try via users table (auth_user_id → store_id)
|
|
140
|
+
const { data: userData } = await client
|
|
141
|
+
.from("users")
|
|
142
|
+
.select("store_id")
|
|
143
|
+
.eq("auth_user_id", userId);
|
|
144
|
+
if (userData && userData.length > 0) {
|
|
145
|
+
const storeIds = userData.map((u) => u.store_id).filter(Boolean);
|
|
146
|
+
if (storeIds.length > 0) {
|
|
147
|
+
const { data: userStores } = await client
|
|
142
148
|
.from("stores")
|
|
143
149
|
.select("id, store_name, slug")
|
|
144
|
-
.
|
|
145
|
-
|
|
146
|
-
|
|
150
|
+
.in("id", storeIds);
|
|
151
|
+
if (userStores && userStores.length > 0) {
|
|
152
|
+
return userStores.map((s) => ({ id: s.id, name: s.store_name, slug: s.slug }));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Fallback 2: store_members table
|
|
157
|
+
const { data: memberships } = await client
|
|
158
|
+
.from("store_members")
|
|
159
|
+
.select("store_id")
|
|
160
|
+
.eq("user_id", userId);
|
|
161
|
+
if (memberships && memberships.length > 0) {
|
|
162
|
+
const storeIds = memberships.map((m) => m.store_id);
|
|
163
|
+
const { data: memberStores } = await client
|
|
164
|
+
.from("stores")
|
|
165
|
+
.select("id, store_name, slug")
|
|
166
|
+
.in("id", storeIds);
|
|
167
|
+
if (memberStores && memberStores.length > 0) {
|
|
168
|
+
return memberStores.map((s) => ({ id: s.id, name: s.store_name, slug: s.slug }));
|
|
147
169
|
}
|
|
148
|
-
return [];
|
|
149
170
|
}
|
|
150
|
-
return
|
|
171
|
+
return [];
|
|
151
172
|
}
|
|
152
173
|
// Select a store and save to config
|
|
153
174
|
export function selectStore(storeId, storeName) {
|
|
@@ -250,8 +250,32 @@ export function readAgentOutput(id) {
|
|
|
250
250
|
if (!agent)
|
|
251
251
|
return null;
|
|
252
252
|
try {
|
|
253
|
-
const
|
|
254
|
-
|
|
253
|
+
const raw = existsSync(agent.outputFile) ? readFileSync(agent.outputFile, "utf-8") : "";
|
|
254
|
+
if (!raw)
|
|
255
|
+
return { status: agent.status, output: "(no output yet)" };
|
|
256
|
+
// Parse ---DONE--- sentinel: extract .output from the JSON blob after it
|
|
257
|
+
const doneIdx = raw.indexOf("\n---DONE---\n");
|
|
258
|
+
if (doneIdx !== -1) {
|
|
259
|
+
try {
|
|
260
|
+
const json = JSON.parse(raw.slice(doneIdx + "\n---DONE---\n".length).trim());
|
|
261
|
+
const clean = (json.output || json.result || json.message || "").toString().trim();
|
|
262
|
+
return { status: agent.status, output: clean || raw.slice(0, doneIdx).trim() };
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
// JSON parse failed — return content before sentinel
|
|
266
|
+
return { status: agent.status, output: raw.slice(0, doneIdx).trim() };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// Parse ---ERROR--- sentinel
|
|
270
|
+
const errIdx = raw.indexOf("\n---ERROR---\n");
|
|
271
|
+
if (errIdx !== -1) {
|
|
272
|
+
const errMsg = raw.slice(errIdx + "\n---ERROR---\n".length).trim();
|
|
273
|
+
return { status: agent.status, output: errMsg || raw.slice(0, errIdx).trim() };
|
|
274
|
+
}
|
|
275
|
+
// Still running — strip the "Agent X started" header line
|
|
276
|
+
const lines = raw.split("\n");
|
|
277
|
+
const content = lines[0]?.startsWith("Agent ") ? lines.slice(1).join("\n").trim() : raw.trim();
|
|
278
|
+
return { status: agent.status, output: content || "(running...)" };
|
|
255
279
|
}
|
|
256
280
|
catch {
|
|
257
281
|
return { status: agent.status, output: "(failed to read output file)" };
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Config Store
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Unified auth session at ~/.whaletools/session.json
|
|
5
|
+
* User preferences at ~/.whaletools/preferences.json
|
|
5
6
|
*
|
|
6
7
|
* v2.0: Raw Supabase/Anthropic keys (for MCP server env vars)
|
|
7
8
|
* v2.1: Auth tokens from login flow (for CLI chat/status)
|
|
9
|
+
* v4.0: Shared auth with Swift apps via ~/.whaletools/session.json
|
|
8
10
|
*
|
|
9
11
|
* Environment variables always override file-based config for MCP server mode.
|
|
10
12
|
*/
|
|
@@ -26,10 +28,20 @@ export interface SwagManagerConfig {
|
|
|
26
28
|
agent_api_key?: string;
|
|
27
29
|
platform_url?: string;
|
|
28
30
|
}
|
|
31
|
+
/** Preferences that survive sign-out (stored in preferences.json) */
|
|
32
|
+
export interface WhalePreferences {
|
|
33
|
+
default_model?: string;
|
|
34
|
+
thinking_enabled?: boolean;
|
|
35
|
+
permission_mode?: string;
|
|
36
|
+
platform_url?: string;
|
|
37
|
+
theme?: string;
|
|
38
|
+
}
|
|
29
39
|
export declare function loadConfig(): SwagManagerConfig;
|
|
30
40
|
export declare function saveConfig(config: SwagManagerConfig): void;
|
|
31
41
|
export declare function updateConfig(partial: Partial<SwagManagerConfig>): void;
|
|
32
42
|
export declare function clearConfig(): void;
|
|
43
|
+
export declare function loadPreferences(): WhalePreferences;
|
|
44
|
+
export declare function savePreferences(prefs: WhalePreferences): void;
|
|
33
45
|
export interface ResolvedConfig {
|
|
34
46
|
supabaseUrl: string;
|
|
35
47
|
supabaseKey: string;
|