whale-code 6.4.0 → 6.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/bin/swagmanager-mcp.js +7 -0
  2. package/dist/cli/app.js +30 -2
  3. package/dist/cli/chat/ChatApp.d.ts +4 -4
  4. package/dist/cli/chat/ChatApp.js +114 -44
  5. package/dist/cli/chat/ChatInput.d.ts +13 -6
  6. package/dist/cli/chat/ChatInput.js +433 -89
  7. package/dist/cli/chat/MemoryManager.d.ts +15 -0
  8. package/dist/cli/chat/MemoryManager.js +61 -0
  9. package/dist/cli/chat/MessageList.d.ts +8 -0
  10. package/dist/cli/chat/MessageList.js +1 -1
  11. package/dist/cli/chat/NodeManager.d.ts +30 -0
  12. package/dist/cli/chat/NodeManager.js +89 -0
  13. package/dist/cli/chat/NodeSelector.d.ts +19 -0
  14. package/dist/cli/chat/NodeSelector.js +37 -0
  15. package/dist/cli/chat/PlanApproval.d.ts +17 -0
  16. package/dist/cli/chat/PlanApproval.js +82 -0
  17. package/dist/cli/chat/SessionManager.d.ts +16 -0
  18. package/dist/cli/chat/SessionManager.js +43 -0
  19. package/dist/cli/chat/SlashMenu.d.ts +38 -0
  20. package/dist/cli/chat/SlashMenu.js +208 -0
  21. package/dist/cli/chat/StatusBar.d.ts +16 -0
  22. package/dist/cli/chat/StatusBar.js +22 -0
  23. package/dist/cli/chat/ThemeSelector.d.ts +14 -0
  24. package/dist/cli/chat/ThemeSelector.js +29 -0
  25. package/dist/cli/chat/ToolIndicator.d.ts +8 -0
  26. package/dist/cli/chat/ToolIndicator.js +33 -9
  27. package/dist/cli/chat/hooks/useAgentLoop.d.ts +2 -1
  28. package/dist/cli/chat/hooks/useAgentLoop.js +22 -17
  29. package/dist/cli/chat/hooks/useSlashCommands.d.ts +19 -0
  30. package/dist/cli/chat/hooks/useSlashCommands.js +254 -15
  31. package/dist/cli/commands/config-cmd.js +4 -25
  32. package/dist/cli/commands/db.d.ts +13 -0
  33. package/dist/cli/commands/db.js +243 -0
  34. package/dist/cli/commands/doctor.js +6 -9
  35. package/dist/cli/commands/mcp.js +1 -20
  36. package/dist/cli/services/agent-events.d.ts +22 -1
  37. package/dist/cli/services/agent-events.js +9 -0
  38. package/dist/cli/services/agent-loop.js +66 -2
  39. package/dist/cli/services/agent-worker-base.js +21 -6
  40. package/dist/cli/services/api-retry.d.ts +25 -0
  41. package/dist/cli/services/api-retry.js +91 -0
  42. package/dist/cli/services/auth-service.d.ts +1 -1
  43. package/dist/cli/services/auth-service.js +40 -19
  44. package/dist/cli/services/background-processes.js +26 -2
  45. package/dist/cli/services/config-store.d.ts +13 -1
  46. package/dist/cli/services/config-store.js +116 -13
  47. package/dist/cli/services/format-server-response.js +12 -6
  48. package/dist/cli/services/ink-resize-fix.d.ts +18 -0
  49. package/dist/cli/services/ink-resize-fix.js +66 -0
  50. package/dist/cli/services/interactive-tools.d.ts +14 -0
  51. package/dist/cli/services/interactive-tools.js +47 -2
  52. package/dist/cli/services/keybinding-manager.js +1 -1
  53. package/dist/cli/services/local-tools.js +35 -2
  54. package/dist/cli/services/server-tools.js +175 -3
  55. package/dist/cli/services/subagent.js +15 -3
  56. package/dist/cli/services/system-prompt.js +5 -3
  57. package/dist/cli/services/task-decomposer.d.ts +35 -0
  58. package/dist/cli/services/task-decomposer.js +199 -0
  59. package/dist/cli/services/team-lead.d.ts +18 -0
  60. package/dist/cli/services/team-lead.js +80 -0
  61. package/dist/cli/services/teammate.js +5 -5
  62. package/dist/cli/services/telemetry.d.ts +8 -2
  63. package/dist/cli/services/telemetry.js +116 -92
  64. package/dist/cli/services/tools/agent-tools.d.ts +1 -0
  65. package/dist/cli/services/tools/agent-tools.js +50 -4
  66. package/dist/cli/services/tools/file-ops.d.ts +2 -0
  67. package/dist/cli/services/tools/file-ops.js +71 -19
  68. package/dist/cli/services/tools/shell-exec.js +22 -12
  69. package/dist/cli/shared/Theme.d.ts +1 -2
  70. package/dist/cli/shared/Theme.js +1 -1
  71. package/dist/cli/shared/WhaleBanner.d.ts +4 -1
  72. package/dist/cli/shared/WhaleBanner.js +12 -8
  73. package/dist/cli/shared/markdown.d.ts +5 -4
  74. package/dist/cli/shared/markdown.js +376 -334
  75. package/dist/cli/shared/theme-manager.d.ts +27 -0
  76. package/dist/cli/shared/theme-manager.js +178 -0
  77. package/dist/cli/shared/theme-presets.d.ts +16 -0
  78. package/dist/cli/shared/theme-presets.js +265 -0
  79. package/dist/index.js +0 -51
  80. package/dist/node/adapters/imessage.d.ts +10 -0
  81. package/dist/node/adapters/imessage.js +45 -6
  82. package/dist/node/cli.js +459 -8
  83. package/dist/node/config.d.ts +17 -0
  84. package/dist/node/gateway-client.d.ts +55 -0
  85. package/dist/node/gateway-client.js +201 -0
  86. package/dist/node/portal/clipboard.d.ts +28 -0
  87. package/dist/node/portal/clipboard.js +183 -0
  88. package/dist/node/portal/discovery.d.ts +29 -0
  89. package/dist/node/portal/discovery.js +61 -0
  90. package/dist/node/portal/forward.d.ts +30 -0
  91. package/dist/node/portal/forward.js +90 -0
  92. package/dist/node/portal/index.d.ts +47 -0
  93. package/dist/node/portal/index.js +250 -0
  94. package/dist/node/portal/multiplexer.d.ts +48 -0
  95. package/dist/node/portal/multiplexer.js +207 -0
  96. package/dist/node/portal/permissions.d.ts +36 -0
  97. package/dist/node/portal/permissions.js +131 -0
  98. package/dist/node/portal/protocol.d.ts +140 -0
  99. package/dist/node/portal/protocol.js +193 -0
  100. package/dist/node/portal/screen.d.ts +18 -0
  101. package/dist/node/portal/screen.js +93 -0
  102. package/dist/node/portal/session.d.ts +68 -0
  103. package/dist/node/portal/session.js +127 -0
  104. package/dist/node/portal/shell.d.ts +26 -0
  105. package/dist/node/portal/shell.js +142 -0
  106. package/dist/node/portal/stream.d.ts +43 -0
  107. package/dist/node/portal/stream.js +90 -0
  108. package/dist/node/portal/transfer.d.ts +33 -0
  109. package/dist/node/portal/transfer.js +231 -0
  110. package/dist/node/portal/ui.d.ts +16 -0
  111. package/dist/node/portal/ui.js +148 -0
  112. package/dist/node/remote-desktop/compile-helper.d.ts +13 -0
  113. package/dist/node/remote-desktop/compile-helper.js +73 -0
  114. package/dist/node/remote-desktop/index.d.ts +67 -0
  115. package/dist/node/remote-desktop/index.js +220 -0
  116. package/dist/node/remote-desktop/protocol.d.ts +96 -0
  117. package/dist/node/remote-desktop/protocol.js +67 -0
  118. package/dist/node/runtime.d.ts +8 -1
  119. package/dist/node/runtime.js +117 -9
  120. package/dist/server/handlers/__test-utils__/test-db.d.ts +25 -0
  121. package/dist/server/handlers/__test-utils__/test-db.js +128 -0
  122. package/dist/server/handlers/api-keys.js +26 -2
  123. package/dist/server/handlers/browser.d.ts +0 -4
  124. package/dist/server/handlers/browser.js +0 -46
  125. package/dist/server/handlers/catalog.js +37 -14
  126. package/dist/server/handlers/clickhouse.d.ts +10 -0
  127. package/dist/server/handlers/clickhouse.js +215 -0
  128. package/dist/server/handlers/comms.d.ts +308 -4
  129. package/dist/server/handlers/comms.js +444 -11
  130. package/dist/server/handlers/creations.js +1 -1
  131. package/dist/server/handlers/crm.d.ts +54 -8
  132. package/dist/server/handlers/crm.js +353 -68
  133. package/dist/server/handlers/embeddings.js +3 -3
  134. package/dist/server/handlers/enrichment.js +39 -55
  135. package/dist/server/handlers/inventory.js +1 -1
  136. package/dist/server/handlers/kali.d.ts +9 -1
  137. package/dist/server/handlers/kali.js +50 -1
  138. package/dist/server/handlers/media.d.ts +8 -0
  139. package/dist/server/handlers/media.js +902 -0
  140. package/dist/server/handlers/meta-ads.js +6 -3
  141. package/dist/server/handlers/nodes.d.ts +2 -0
  142. package/dist/server/handlers/nodes.js +331 -40
  143. package/dist/server/handlers/operations.d.ts +4 -6
  144. package/dist/server/handlers/operations.js +99 -38
  145. package/dist/server/handlers/platform.js +224 -107
  146. package/dist/server/handlers/remove-bg.d.ts +6 -0
  147. package/dist/server/handlers/remove-bg.js +96 -0
  148. package/dist/server/handlers/storefront.d.ts +6 -0
  149. package/dist/server/handlers/storefront.js +477 -0
  150. package/dist/server/handlers/supply-chain.js +21 -3
  151. package/dist/server/handlers/workflow-steps.js +87 -31
  152. package/dist/server/handlers/workflows.js +4 -1
  153. package/dist/server/index.js +334 -88
  154. package/dist/server/lib/clickhouse-buffer.d.ts +48 -0
  155. package/dist/server/lib/clickhouse-buffer.js +175 -0
  156. package/dist/server/lib/clickhouse-client.d.ts +112 -0
  157. package/dist/server/lib/clickhouse-client.js +141 -0
  158. package/dist/server/lib/coa-renderer.d.ts +91 -0
  159. package/dist/server/lib/coa-renderer.js +411 -0
  160. package/dist/server/lib/compaction-service.js +45 -1
  161. package/dist/server/lib/pdf-renderer.d.ts +143 -0
  162. package/dist/server/lib/pdf-renderer.js +867 -0
  163. package/dist/server/lib/react-pdf-layout.d.ts +40 -0
  164. package/dist/server/lib/react-pdf-layout.js +437 -0
  165. package/dist/server/lib/server-agent-loop.d.ts +2 -0
  166. package/dist/server/lib/server-agent-loop.js +61 -15
  167. package/dist/server/lib/server-subagent.d.ts +3 -0
  168. package/dist/server/lib/server-subagent.js +7 -4
  169. package/dist/server/lib/supabase-client.js +51 -3
  170. package/dist/server/lib/template-resolver.js +14 -4
  171. package/dist/server/lib/utils.js +15 -0
  172. package/dist/server/local-agent-gateway.d.ts +44 -0
  173. package/dist/server/local-agent-gateway.js +389 -49
  174. package/dist/server/providers/anthropic.js +12 -2
  175. package/dist/server/providers/gemini.js +17 -2
  176. package/dist/server/proxy-handlers.js +151 -0
  177. package/dist/server/tool-router.d.ts +2 -2
  178. package/dist/server/tool-router.js +25 -35
  179. package/dist/shared/agent-core.d.ts +5 -2
  180. package/dist/shared/agent-core.js +30 -4
  181. package/dist/shared/api-client.js +54 -3
  182. package/dist/shared/sse-parser.d.ts +1 -1
  183. package/dist/shared/sse-parser.js +5 -2
  184. package/dist/shared/tool-dispatch.js +1 -1
  185. package/package.json +16 -10
  186. package/dist/server/handlers/__test-utils__/mock-supabase.d.ts +0 -11
  187. package/dist/server/handlers/__test-utils__/mock-supabase.js +0 -393
@@ -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 { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
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 type AgentEvent = AgentTextEvent | AgentToolStartEvent | AgentToolEndEvent | AgentUsageEvent | AgentDoneEvent | AgentErrorEvent | AgentCompactEvent | ToolOutputEvent | SubagentStartEvent | SubagentProgressEvent | SubagentDoneEvent | SubagentToolStartEvent | SubagentToolEndEvent | TeamStartEvent | TeamProgressEvent | TeamTaskEvent | TeamDoneEvent;
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: opts.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 = 30_000, toolTimeoutMs, } = opts;
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
- // Execute all tool calls in parallel — the model already determined these are independent
191
- const batchResults = await Promise.all(toolBlocks.map(tu => executeSingle(tu)));
192
- toolResults.push(...batchResults);
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, _userId: string): Promise<StoreInfo[]>;
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 filters to only stores the user has access to,
125
- // same as the Swift app does: client.from("stores").select()
126
- export async function getStoresForUser(accessToken, _userId) {
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
- // Let RLS filtersame pattern as the macOS app (SupabaseService.fetchStores)
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 (error) {
134
- // Fallback: try via the users table (auth_user_id column, not id)
135
- const { data: userData } = await client
136
- .from("users")
137
- .select("store_id")
138
- .eq("auth_user_id", _userId)
139
- .single();
140
- if (userData?.store_id) {
141
- const { data: store } = await client
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
- .eq("id", userData.store_id)
145
- .single();
146
- return store ? [{ id: store.id, name: store.store_name, slug: store.slug }] : [];
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 (stores || []).map((s) => ({ id: s.id, name: s.store_name, slug: s.slug }));
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 content = existsSync(agent.outputFile) ? readFileSync(agent.outputFile, "utf-8") : "(no output yet)";
254
- return { status: agent.status, output: content };
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
- * Persistent configuration at ~/.swagmanager/config.json
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;