whale-code 6.4.0 → 6.5.1
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 +51 -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 +65 -8
- 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 +7 -6
- 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 +85 -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 +46 -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 +36 -17
- package/dist/server/lib/server-subagent.d.ts +3 -0
- package/dist/server/lib/server-subagent.js +9 -6
- 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 +25 -2
- package/dist/shared/agent-core.js +66 -5
- 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 +15 -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,16 +17,16 @@
|
|
|
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";
|
|
28
28
|
import { loadHooks, runBeforeToolHook, runAfterToolHook, runSessionHook } from "./hooks.js";
|
|
29
|
-
import { LoopDetector, COMPACTION_TRIGGER_TOKENS, COMPACTION_TOTAL_BUDGET, getCompactionConfig } from "../../shared/agent-core.js";
|
|
29
|
+
import { LoopDetector, COMPACTION_TRIGGER_TOKENS, COMPACTION_TOTAL_BUDGET, getCompactionConfig, DEFAULT_SESSION_COST_BUDGET_USD, emitCostWarningIfNeeded } from "../../shared/agent-core.js";
|
|
30
30
|
import { parseSSEStream, processStreamWithCallbacks, collectStreamResult } from "../../shared/sse-parser.js";
|
|
31
31
|
import { callServerProxy, callTranscribe, buildAPIRequest, buildSystemBlocks, prepareWithCaching, trimGeminiContext, trimOpenAIContext, requestProviderCompaction } from "../../shared/api-client.js";
|
|
32
32
|
import { getProvider, MODELS } from "../../shared/constants.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,8 @@ export async function runAgentLoop(opts) {
|
|
|
305
307
|
});
|
|
306
308
|
let sessionCostUsd = 0;
|
|
307
309
|
let compactionCount = 0;
|
|
310
|
+
const costWarningsEmitted = new Set();
|
|
311
|
+
const effectiveBudget = opts.maxBudgetUsd ?? DEFAULT_SESSION_COST_BUDGET_USD;
|
|
308
312
|
const activeModel = getModel();
|
|
309
313
|
// Tool executor — routes to interactive, local, server, or MCP tools.
|
|
310
314
|
// Wraps execution with before/after hooks when hooks are loaded.
|
|
@@ -331,6 +335,24 @@ export async function runAgentLoop(opts) {
|
|
|
331
335
|
let result;
|
|
332
336
|
if (INTERACTIVE_TOOL_NAMES.has(name)) {
|
|
333
337
|
result = await executeInteractiveTool(name, effectiveInput);
|
|
338
|
+
// For exit_plan_mode: wait for UI approval, then map decision to result
|
|
339
|
+
if (name === "exit_plan_mode" && result.success) {
|
|
340
|
+
const decision = await waitForPlanApproval();
|
|
341
|
+
switch (decision.action) {
|
|
342
|
+
case "execute":
|
|
343
|
+
result = { success: true, output: `__PLAN_APPROVED_CLEAN__\n${result.output}` };
|
|
344
|
+
break;
|
|
345
|
+
case "edit":
|
|
346
|
+
result = { success: true, output: "Plan returned for revision. Make changes and use ExitPlanMode again when ready." };
|
|
347
|
+
break;
|
|
348
|
+
case "feedback":
|
|
349
|
+
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.` };
|
|
350
|
+
break;
|
|
351
|
+
case "cancel":
|
|
352
|
+
result = { success: true, output: "Plan cancelled by user. Do not proceed with implementation." };
|
|
353
|
+
break;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
334
356
|
}
|
|
335
357
|
else if (isLocalTool(name)) {
|
|
336
358
|
result = await executeLocalTool(name, effectiveInput);
|
|
@@ -363,16 +385,16 @@ export async function runAgentLoop(opts) {
|
|
|
363
385
|
callbacks.onError("Cancelled", messages);
|
|
364
386
|
return;
|
|
365
387
|
}
|
|
366
|
-
// Budget enforcement
|
|
367
|
-
if (
|
|
368
|
-
logSpan({ action: "chat.budget_exceeded", durationMs: Date.now() - sessionStart, context: turnCtx, storeId: storeId || undefined, severity: "warn", details: { session_cost_usd: sessionCostUsd, max_budget_usd:
|
|
369
|
-
callbacks.onError(`Budget exceeded: $${sessionCostUsd.toFixed(4)} >= $${
|
|
388
|
+
// Budget enforcement — always enforced (defaults to DEFAULT_SESSION_COST_BUDGET_USD)
|
|
389
|
+
if (sessionCostUsd >= effectiveBudget) {
|
|
390
|
+
logSpan({ action: "chat.budget_exceeded", durationMs: Date.now() - sessionStart, context: turnCtx, storeId: storeId || undefined, severity: "warn", details: { session_cost_usd: sessionCostUsd, max_budget_usd: effectiveBudget, iteration } });
|
|
391
|
+
callbacks.onError(`Budget exceeded: $${sessionCostUsd.toFixed(4)} >= $${effectiveBudget}`, messages);
|
|
370
392
|
return;
|
|
371
393
|
}
|
|
372
394
|
const apiStart = Date.now();
|
|
373
395
|
const apiSpanId = generateSpanId();
|
|
374
396
|
const apiRowId = crypto.randomUUID(); // UUID for this span's row — children reference via parent_id
|
|
375
|
-
const costContext = `Session cost: $${sessionCostUsd.toFixed(2)}
|
|
397
|
+
const costContext = `Session cost: $${sessionCostUsd.toFixed(2)} | Budget remaining: $${(effectiveBudget - sessionCostUsd).toFixed(2)}`;
|
|
376
398
|
// Build API request config
|
|
377
399
|
const currentModel = getModel();
|
|
378
400
|
const apiConfig = buildAPIRequest({
|
|
@@ -449,6 +471,7 @@ export async function runAgentLoop(opts) {
|
|
|
449
471
|
},
|
|
450
472
|
});
|
|
451
473
|
// Process stream events with UI callbacks
|
|
474
|
+
let thinkingChunks = 0;
|
|
452
475
|
const result = await processStreamWithCallbacks(parseSSEStream(stream, abortSignal), {
|
|
453
476
|
onText: (text) => {
|
|
454
477
|
if (emitter) {
|
|
@@ -458,6 +481,10 @@ export async function runAgentLoop(opts) {
|
|
|
458
481
|
callbacks.onText(text);
|
|
459
482
|
}
|
|
460
483
|
},
|
|
484
|
+
onThinking: () => {
|
|
485
|
+
thinkingChunks++;
|
|
486
|
+
emitter?.emitThinking(thinkingChunks);
|
|
487
|
+
},
|
|
461
488
|
onToolStart: (name, input) => {
|
|
462
489
|
// NOTE: Do NOT call callbacks.onToolStart here — dispatchTools.onStart
|
|
463
490
|
// fires it once per tool at execution time. Calling it here too would
|
|
@@ -489,6 +516,13 @@ export async function runAgentLoop(opts) {
|
|
|
489
516
|
totalCacheRead += result.usage.cacheReadTokens;
|
|
490
517
|
totalThinking += result.thinkingTokens;
|
|
491
518
|
sessionCostUsd += estimateCostUsd(result.usage.inputTokens, result.usage.outputTokens, currentModel, result.thinkingTokens, result.usage.cacheReadTokens, result.usage.cacheCreationTokens);
|
|
519
|
+
// Graduated cost warnings
|
|
520
|
+
emitCostWarningIfNeeded(sessionCostUsd, effectiveBudget, costWarningsEmitted, (text) => { if (emitter) {
|
|
521
|
+
emitter.emitText(text);
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
callbacks.onText(text);
|
|
525
|
+
} });
|
|
492
526
|
// Server-side context management notification
|
|
493
527
|
if (result.contextManagementApplied) {
|
|
494
528
|
callbacks.onAutoCompact?.(messages.length, messages.length, 0);
|
|
@@ -616,6 +650,25 @@ export async function runAgentLoop(opts) {
|
|
|
616
650
|
compactionContent: result.compactionContent,
|
|
617
651
|
});
|
|
618
652
|
messages.push({ role: "assistant", content: assistantContent });
|
|
653
|
+
// Check for __PLAN_APPROVED_CLEAN__ marker — clear context and start fresh with just the plan
|
|
654
|
+
const planCleanMarker = "__PLAN_APPROVED_CLEAN__\n";
|
|
655
|
+
const hasCleanPlanApproval = finalToolResults.some((tr) => {
|
|
656
|
+
const content = typeof tr.content === "string" ? tr.content : "";
|
|
657
|
+
return content.startsWith(planCleanMarker);
|
|
658
|
+
});
|
|
659
|
+
if (hasCleanPlanApproval) {
|
|
660
|
+
// Extract plan content from the marker
|
|
661
|
+
const planResult = finalToolResults.find((tr) => {
|
|
662
|
+
const content = typeof tr.content === "string" ? tr.content : "";
|
|
663
|
+
return content.startsWith(planCleanMarker);
|
|
664
|
+
});
|
|
665
|
+
const planText = planResult.content.slice(planCleanMarker.length);
|
|
666
|
+
const beforeCount = messages.length;
|
|
667
|
+
messages.length = 0;
|
|
668
|
+
messages.push({ role: "user", content: [{ type: "text", text: `Implement this plan:\n\n${planText}` }] });
|
|
669
|
+
callbacks.onAutoCompact?.(beforeCount, 1, 0);
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
619
672
|
messages.push({ role: "user", content: finalToolResults });
|
|
620
673
|
// Non-native compaction for OpenAI/Gemini — fires after tool results appended
|
|
621
674
|
const compactionCfg = getCompactionConfig(currentModel);
|
|
@@ -683,6 +736,8 @@ export async function runAgentLoop(opts) {
|
|
|
683
736
|
});
|
|
684
737
|
const turnCostUsd = estimateCostUsd(totalIn, totalOut, activeModel, totalThinking, totalCacheRead, totalCacheCreation);
|
|
685
738
|
callbacks.onUsage(totalIn, totalOut, totalThinking, activeModel, turnCostUsd, totalCacheRead, totalCacheCreation);
|
|
739
|
+
// Flush telemetry spans to server before session ends
|
|
740
|
+
flushCliSpans();
|
|
686
741
|
// Fire SessionEnd hook (non-blocking)
|
|
687
742
|
if (hooks.length > 0) {
|
|
688
743
|
runSessionHook(hooks, "SessionEnd", { session_id: `turn-${sessionStart}` }).catch(() => { });
|
|
@@ -719,6 +774,8 @@ export async function runAgentLoop(opts) {
|
|
|
719
774
|
tags: { model: activeModel, turn: String(turnNum) },
|
|
720
775
|
});
|
|
721
776
|
}
|
|
777
|
+
// Flush telemetry on error path too
|
|
778
|
+
flushCliSpans();
|
|
722
779
|
emitter?.emitError(errorMsg);
|
|
723
780
|
if (emitter)
|
|
724
781
|
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;
|