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.
Files changed (187) hide show
  1. package/bin/swagmanager-mcp.js +51 -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 +65 -8
  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 +7 -6
  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 +85 -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 +46 -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 +36 -17
  167. package/dist/server/lib/server-subagent.d.ts +3 -0
  168. package/dist/server/lib/server-subagent.js +9 -6
  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 +25 -2
  180. package/dist/shared/agent-core.js +66 -5
  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 +15 -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
@@ -1,12 +1,13 @@
1
1
  /**
2
- * CLI Telemetry — fire-and-forget span logging to audit_logs
2
+ * CLI Telemetry — spans are buffered and flushed to the Fly.io server,
3
+ * which queues them into ClickHouse ai_spans.
3
4
  *
4
5
  * Session-scoped conversationId + auto-incrementing turnNumber.
5
- * Uses same column schema as executor.ts telemetry (trace_id, span_id, etc).
6
+ * Uses same column schema as server-side telemetry (trace_id, span_id, etc).
6
7
  * Never blocks or crashes the chat.
7
8
  */
8
- import { createClient } from "@supabase/supabase-js";
9
9
  import { createRequire } from "module";
10
+ import os from "node:os";
10
11
  import { resolveConfig, loadConfig } from "./config-store.js";
11
12
  import { getValidToken, createAuthenticatedClient } from "./auth-service.js";
12
13
  import { captureError } from "./error-logger.js";
@@ -55,39 +56,6 @@ export function generateSpanId() {
55
56
  return Array.from(bytes).map(b => b.toString(16).padStart(2, "0")).join("");
56
57
  }
57
58
  // ============================================================================
58
- // SUPABASE CLIENT (lazy init)
59
- // ============================================================================
60
- async function getClient() {
61
- if (supabaseClient)
62
- return supabaseClient;
63
- const config = resolveConfig();
64
- // Prefer service role key
65
- if (config.supabaseUrl && config.supabaseKey) {
66
- supabaseClient = createClient(config.supabaseUrl, config.supabaseKey, {
67
- auth: { persistSession: false, autoRefreshToken: false },
68
- });
69
- if (process.env.DEBUG_TELEMETRY) {
70
- process.stderr.write(`[telemetry] using service role key\n`);
71
- }
72
- return supabaseClient;
73
- }
74
- // Fallback: user JWT
75
- const token = await getValidToken();
76
- if (token) {
77
- supabaseClient = createAuthenticatedClient(token);
78
- if (process.env.DEBUG_TELEMETRY) {
79
- process.stderr.write(`[telemetry] using user JWT token\n`);
80
- }
81
- return supabaseClient;
82
- }
83
- if (process.env.DEBUG_TELEMETRY) {
84
- process.stderr.write(`[telemetry] NO CLIENT - no service key and no valid token\n`);
85
- process.stderr.write(`[telemetry] config.supabaseUrl: ${config.supabaseUrl}\n`);
86
- process.stderr.write(`[telemetry] config.supabaseKey: ${config.supabaseKey ? 'set' : 'not set'}\n`);
87
- }
88
- return null;
89
- }
90
- // ============================================================================
91
59
  // TURN CONTEXT
92
60
  // ============================================================================
93
61
  export function nextTurn() {
@@ -113,6 +81,93 @@ export function createTurnContext(overrides) {
113
81
  export function getTurnNumber() {
114
82
  return turnNumber;
115
83
  }
84
+ // ============================================================================
85
+ // SPAN BUFFER — batches spans for bulk POST to the Fly.io server
86
+ // Same pattern as server's clickhouse-buffer.ts but over HTTP.
87
+ // ============================================================================
88
+ const FLUSH_INTERVAL = 2000; // 2s (slower than server — HTTP has more overhead)
89
+ const FLUSH_MAX = 50; // max spans before force flush
90
+ const spanBuffer = [];
91
+ let flushTimer = null;
92
+ let conversationRegistered = false;
93
+ function queueCliSpan(span) {
94
+ spanBuffer.push(span);
95
+ if (spanBuffer.length >= FLUSH_MAX) {
96
+ flushCliSpans();
97
+ }
98
+ else if (!flushTimer) {
99
+ flushTimer = setTimeout(() => {
100
+ flushTimer = null;
101
+ flushCliSpans();
102
+ }, FLUSH_INTERVAL);
103
+ }
104
+ }
105
+ /**
106
+ * Flush all buffered spans to the Fly.io server.
107
+ * Call this on session end or at shutdown.
108
+ */
109
+ export function flushCliSpans() {
110
+ if (flushTimer) {
111
+ clearTimeout(flushTimer);
112
+ flushTimer = null;
113
+ }
114
+ if (spanBuffer.length === 0)
115
+ return;
116
+ const batch = spanBuffer.splice(0, spanBuffer.length);
117
+ // Fire-and-forget — never block the chat
118
+ _sendSpans(batch).catch((err) => {
119
+ if (process.env.DEBUG_TELEMETRY) {
120
+ process.stderr.write(`[telemetry] flush error: ${err.message}\n`);
121
+ }
122
+ });
123
+ }
124
+ async function _sendSpans(spans) {
125
+ const config = resolveConfig();
126
+ if (!config.serverUrl)
127
+ return;
128
+ let authToken = config.supabaseKey;
129
+ if (!authToken) {
130
+ authToken = await getValidToken() || "";
131
+ }
132
+ if (!authToken)
133
+ return;
134
+ const fileConfig = loadConfig();
135
+ const body = {
136
+ mode: "telemetry_ingest",
137
+ spans,
138
+ conversation_id: conversationId,
139
+ store_id: config.storeId || undefined,
140
+ userId: fileConfig.user_id || undefined,
141
+ userEmail: fileConfig.email || undefined,
142
+ source: "whale_cli",
143
+ };
144
+ // Include conversation metadata on first flush so server creates the row
145
+ if (!conversationRegistered) {
146
+ conversationRegistered = true;
147
+ body.conversation_title = `CLI Session ${new Date().toISOString().split("T")[0]}`;
148
+ body.hostname = os.hostname();
149
+ body.version = PKG_VERSION;
150
+ }
151
+ try {
152
+ const response = await fetch(config.serverUrl, {
153
+ method: "POST",
154
+ headers: {
155
+ "Content-Type": "application/json",
156
+ "Authorization": `Bearer ${authToken}`,
157
+ },
158
+ body: JSON.stringify(body),
159
+ });
160
+ if (!response.ok && process.env.DEBUG_TELEMETRY) {
161
+ const text = await response.text().catch(() => "");
162
+ process.stderr.write(`[telemetry] ingest failed (${response.status}): ${text.slice(0, 200)}\n`);
163
+ }
164
+ }
165
+ catch (err) {
166
+ if (process.env.DEBUG_TELEMETRY) {
167
+ process.stderr.write(`[telemetry] ingest error: ${err.message}\n`);
168
+ }
169
+ }
170
+ }
116
171
  export function logSpan(opts) {
117
172
  // Fire-and-forget — don't await, log errors in debug mode
118
173
  _logSpan(opts).catch((err) => {
@@ -125,74 +180,43 @@ async function _logSpan(opts) {
125
180
  if (process.env.DEBUG_TELEMETRY) {
126
181
  process.stderr.write(`[telemetry] _logSpan called for ${opts.action}\n`);
127
182
  }
128
- const client = await getClient();
129
- if (!client) {
130
- if (process.env.DEBUG_TELEMETRY) {
131
- process.stderr.write(`[telemetry] no client for ${opts.action}\n`);
132
- }
133
- return;
134
- }
135
- const now = new Date();
136
- const startTime = new Date(now.getTime() - opts.durationMs);
137
183
  const ctx = opts.context;
138
- // Debug: log team-related spans (only when DEBUG_TELEMETRY is set)
139
- if (process.env.DEBUG_TELEMETRY && (opts.action.startsWith("team.") || opts.details?.parent_conversation_id)) {
140
- process.stderr.write(`[telemetry:team] action=${opts.action}\n`);
141
- process.stderr.write(`[telemetry:team] conversation_id=${ctx.conversationId}\n`);
142
- process.stderr.write(`[telemetry:team] parent_conversation_id=${opts.details?.parent_conversation_id}\n`);
143
- }
144
- const row = {
184
+ const now = new Date();
185
+ const startedAt = new Date(now.getTime() - opts.durationMs).toISOString();
186
+ const endedAt = now.toISOString();
187
+ // Build span in same shape as server's auditRowToSpan input
188
+ const span = {
145
189
  action: opts.action,
146
190
  severity: opts.severity || (opts.error ? "error" : "info"),
147
191
  store_id: opts.storeId || resolveConfig().storeId || null,
192
+ source: ctx.source || "whale_cli",
193
+ service_name: ctx.serviceName || "whale-code",
194
+ span_kind: "INTERNAL",
195
+ status_code: opts.error ? "ERROR" : "OK",
196
+ trace_id: ctx.traceId || null,
197
+ span_id: ctx.spanId || generateSpanId(),
198
+ conversation_id: ctx.conversationId || conversationId,
148
199
  user_id: ctx.userId || null,
149
200
  user_email: ctx.userEmail || null,
150
- resource_type: "cli_span",
151
- resource_id: opts.action,
152
- request_id: ctx.traceId,
153
- parent_id: ctx.parentId || null,
201
+ start_time: startedAt,
202
+ end_time: endedAt,
154
203
  duration_ms: opts.durationMs,
155
204
  error_message: opts.error || null,
156
- // OTEL columns
157
- trace_id: ctx.traceId,
158
- span_id: ctx.spanId,
159
- trace_flags: ctx.traceFlags ?? 1,
160
- span_kind: "INTERNAL",
161
- service_name: ctx.serviceName || "whale-code",
162
- service_version: ctx.serviceVersion || PKG_VERSION,
163
- status_code: opts.error ? "ERROR" : "OK",
164
- start_time: startTime.toISOString(),
165
- end_time: now.toISOString(),
166
- // AI telemetry — use ?? to handle 0 correctly
167
- model: ctx.model || null,
168
- input_tokens: ctx.inputTokens ?? null,
169
- output_tokens: ctx.outputTokens ?? null,
170
- total_cost: ctx.totalCost ?? null,
171
- turn_number: ctx.turnNumber ?? null,
172
- conversation_id: ctx.conversationId || null,
173
205
  details: {
174
- source: ctx.source || "whale_cli",
175
- conversation_id: ctx.conversationId || conversationId,
176
- turn_number: ctx.turnNumber ?? turnNumber,
177
- parent_span_id: ctx.parentSpanId || null,
178
206
  ...opts.details,
207
+ input_tokens: ctx.inputTokens,
208
+ output_tokens: ctx.outputTokens,
209
+ total_cost: ctx.totalCost,
210
+ model: ctx.model,
211
+ turn_number: ctx.turnNumber,
212
+ agent_id: ctx.agentId,
213
+ agent_name: ctx.agentName,
214
+ tool_type: ctx.toolType,
179
215
  },
180
216
  };
181
- // Allow caller to control row ID so children can reference it via parent_id
182
- if (ctx.rowId)
183
- row.id = ctx.rowId;
184
- const { error } = await client.from("audit_logs").insert(row);
185
- if (error) {
186
- if (process.env.DEBUG_TELEMETRY) {
187
- process.stderr.write(`[telemetry db error] ${opts.action}: ${error.message}\n`);
188
- process.stderr.write(`[telemetry db error] code: ${error.code}\n`);
189
- process.stderr.write(`[telemetry db error] hint: ${error.hint}\n`);
190
- }
191
- }
192
- else if (opts.details?.is_teammate && process.env.DEBUG_TELEMETRY) {
193
- process.stderr.write(`[telemetry] teammate span logged: ${opts.action}\n`);
194
- }
195
- // Bridge errors to the error logging system
217
+ // Queue for batch send to server ClickHouse
218
+ queueCliSpan(span);
219
+ // Also bridge errors to the error logging system (Postgres)
196
220
  if (opts.error) {
197
221
  captureError({
198
222
  errorType: opts.action,
@@ -12,3 +12,4 @@ export declare function taskTool(input: Record<string, unknown>): Promise<ToolRe
12
12
  export declare function taskOutput(input: Record<string, unknown>): Promise<ToolResult>;
13
13
  export declare function taskStop(input: Record<string, unknown>): ToolResult;
14
14
  export declare function teamCreateTool(input: Record<string, unknown>): Promise<ToolResult>;
15
+ export declare function teamAutoTool(input: Record<string, unknown>): Promise<ToolResult>;
@@ -8,7 +8,7 @@ import { dirname, join } from "path";
8
8
  import { homedir } from "os";
9
9
  import { runSubagent, runSubagentBackground, } from "../subagent.js";
10
10
  import { createTurnContext, getTurnNumber, } from "../telemetry.js";
11
- import { runAgentTeam, } from "../team-lead.js";
11
+ import { runAgentTeam, runAutoTeam, } from "../team-lead.js";
12
12
  import { readAgentOutput, stopBackgroundAgent } from "../background-processes.js";
13
13
  import { readProcessOutput, killProcess } from "../background-processes.js";
14
14
  import { getGlobalEmitter } from "../agent-events.js";
@@ -244,13 +244,13 @@ export async function taskOutput(input) {
244
244
  await new Promise(r => setTimeout(r, 1000));
245
245
  const updated = readAgentOutput(taskId);
246
246
  if (updated && updated.status !== "running") {
247
- return { success: true, output: `[${updated.status}]\n${updated.output}` };
247
+ return { success: true, output: updated.output };
248
248
  }
249
249
  }
250
250
  const final = readAgentOutput(taskId);
251
- return { success: true, output: `[${final?.status || "running"}timed out waiting]\n${final?.output || ""}` };
251
+ return { success: true, output: `${final?.output || ""}\n(timed out waiting agent may still be running)`.trim() };
252
252
  }
253
- return { success: true, output: `[${agentResult.status}]\n${agentResult.output}` };
253
+ return { success: true, output: agentResult.output };
254
254
  }
255
255
  // Fall back to shell process output (bash_output behavior)
256
256
  const result = readProcessOutput(taskId, {});
@@ -345,3 +345,49 @@ export async function teamCreateTool(input) {
345
345
  };
346
346
  }
347
347
  }
348
+ // ============================================================================
349
+ // TEAM AUTO — auto-decompose + parallel execution + review
350
+ // ============================================================================
351
+ export async function teamAutoTool(input) {
352
+ const task = input.task;
353
+ if (!task)
354
+ return { success: false, output: "task is required" };
355
+ const maxTeammates = input.max_teammates || 4;
356
+ const model = input.model || "sonnet";
357
+ const workingDirectory = input.working_directory || process.cwd();
358
+ const review = input.review !== false;
359
+ try {
360
+ const result = await runAutoTeam(task, {
361
+ maxTeammates,
362
+ model,
363
+ workingDirectory,
364
+ review,
365
+ });
366
+ const lines = [
367
+ `## Auto Team`,
368
+ `Status: ${result.success ? "SUCCESS" : "PARTIAL"}`,
369
+ `Duration: ${(result.durationMs / 1000).toFixed(1)}s`,
370
+ `Tokens: ${result.tokensUsed.input} in, ${result.tokensUsed.output} out`,
371
+ ];
372
+ if (result.warnings?.length) {
373
+ lines.push("", "### Warnings");
374
+ for (const w of result.warnings)
375
+ lines.push(`- ${w}`);
376
+ }
377
+ lines.push("", "### Task Results");
378
+ for (const t of result.taskResults) {
379
+ const icon = t.status === "completed" ? "[done]" : "[fail]";
380
+ lines.push(`${icon} ${t.description.slice(0, 120)}`);
381
+ if (t.result) {
382
+ lines.push(` ${t.result.slice(0, 200)}${t.result.length > 200 ? "..." : ""}`);
383
+ }
384
+ }
385
+ if (result.review) {
386
+ lines.push("", "### Review", result.review);
387
+ }
388
+ return { success: result.success, output: lines.join("\n") };
389
+ }
390
+ catch (err) {
391
+ return { success: false, output: `Auto team failed: ${err.message || err}` };
392
+ }
393
+ }
@@ -5,6 +5,8 @@
5
5
  */
6
6
  import { ToolResult } from "../../../shared/types.js";
7
7
  export declare function resolvePath(p: string): string;
8
+ /** Clear the session-level read cache. Call on session reset. */
9
+ export declare function clearReadCache(): void;
8
10
  export declare function readFile(input: Record<string, unknown>): Promise<ToolResult>;
9
11
  export declare function writeFile(input: Record<string, unknown>): ToolResult;
10
12
  export declare function editFile(input: Record<string, unknown>): ToolResult;
@@ -15,6 +15,36 @@ export function resolvePath(p) {
15
15
  return join(homedir(), p.slice(2));
16
16
  return p;
17
17
  }
18
+ const READ_CACHE_MAX = 100;
19
+ const MAX_ENTRY_SIZE = 100_000; // 100KB — skip caching larger files
20
+ const MAX_CACHE_BYTES = 10_000_000; // 10MB total budget
21
+ let totalCacheBytes = 0;
22
+ const readCache = new Map();
23
+ /** Clear the session-level read cache. Call on session reset. */
24
+ export function clearReadCache() {
25
+ readCache.clear();
26
+ totalCacheBytes = 0;
27
+ }
28
+ /** Invalidate a specific path from the cache (call on write/edit). */
29
+ function invalidateCache(path) {
30
+ const existing = readCache.get(path);
31
+ if (existing) {
32
+ totalCacheBytes -= existing.content.length;
33
+ readCache.delete(path);
34
+ }
35
+ }
36
+ /** LRU eviction: remove oldest entries when cache exceeds count or byte budget. */
37
+ function evictIfNeeded() {
38
+ while (readCache.size >= READ_CACHE_MAX || totalCacheBytes > MAX_CACHE_BYTES) {
39
+ const oldest = readCache.keys().next().value;
40
+ if (!oldest)
41
+ break;
42
+ const entry = readCache.get(oldest);
43
+ if (entry)
44
+ totalCacheBytes -= entry.content.length;
45
+ readCache.delete(oldest);
46
+ }
47
+ }
18
48
  // ============================================================================
19
49
  // READ FILE
20
50
  // ============================================================================
@@ -30,6 +60,28 @@ const AUDIO_MEDIA_TYPES = {
30
60
  m4a: "audio/mp4",
31
61
  };
32
62
  const AUDIO_MAX_SIZE = 25 * 1024 * 1024; // 25MB
63
+ function formatTextFileResult(lines, input, content) {
64
+ const offset = input.offset || 1; // 1-based
65
+ const limit = input.limit;
66
+ if (offset > 1 || limit) {
67
+ const startIdx = Math.max(0, offset - 1);
68
+ const endIdx = limit ? startIdx + limit : lines.length;
69
+ const slice = lines.slice(startIdx, endIdx);
70
+ const numbered = slice.map((line, i) => {
71
+ const lineNum = startIdx + i + 1;
72
+ return `${String(lineNum).padStart(6)} ${line}`;
73
+ });
74
+ let output = numbered.join("\n");
75
+ if (endIdx < lines.length) {
76
+ output += `\n\n... (showing lines ${startIdx + 1}-${Math.min(endIdx, lines.length)} of ${lines.length})`;
77
+ }
78
+ return { success: true, output };
79
+ }
80
+ if (content.length > 500_000) {
81
+ return { success: true, output: content.slice(0, 500_000) + `\n\n... (safety truncated, ${content.length.toLocaleString()} total chars)` };
82
+ }
83
+ return { success: true, output: content };
84
+ }
33
85
  export async function readFile(input) {
34
86
  const path = resolvePath(input.path);
35
87
  if (!existsSync(path))
@@ -90,29 +142,39 @@ export async function readFile(input) {
90
142
  return { success: false, output: `Failed to parse PDF: ${err}` };
91
143
  }
92
144
  }
93
- // Text files — existing behavior
145
+ // Text files — check cache first via mtime comparison
146
+ try {
147
+ const stat = statSync(path);
148
+ const cached = readCache.get(path);
149
+ if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
150
+ // Cache hit — use previously read content
151
+ debugLog("file-ops", `readFile cache hit: ${path}`);
152
+ const content = cached.content;
153
+ const lines = content.split("\n");
154
+ // Move to end of map for LRU freshness
155
+ readCache.delete(path);
156
+ readCache.set(path, cached);
157
+ return formatTextFileResult(lines, input, content);
158
+ }
159
+ }
160
+ catch {
161
+ // stat failed — fall through to normal read
162
+ }
94
163
  const content = readFileSync(path, "utf-8");
95
164
  const lines = content.split("\n");
96
- const offset = input.offset || 1; // 1-based
97
- const limit = input.limit;
98
- if (offset > 1 || limit) {
99
- const startIdx = Math.max(0, offset - 1);
100
- const endIdx = limit ? startIdx + limit : lines.length;
101
- const slice = lines.slice(startIdx, endIdx);
102
- const numbered = slice.map((line, i) => {
103
- const lineNum = startIdx + i + 1;
104
- return `${String(lineNum).padStart(6)} ${line}`;
105
- });
106
- let output = numbered.join("\n");
107
- if (endIdx < lines.length) {
108
- output += `\n\n... (showing lines ${startIdx + 1}-${Math.min(endIdx, lines.length)} of ${lines.length})`;
165
+ // Cache the read skip caching files larger than MAX_ENTRY_SIZE
166
+ if (content.length <= MAX_ENTRY_SIZE) {
167
+ try {
168
+ const stat = statSync(path);
169
+ evictIfNeeded();
170
+ readCache.set(path, { content, mtimeMs: stat.mtimeMs, size: stat.size });
171
+ totalCacheBytes += content.length;
172
+ }
173
+ catch {
174
+ // stat failed — skip caching
109
175
  }
110
- return { success: true, output };
111
- }
112
- if (content.length > 500_000) {
113
- return { success: true, output: content.slice(0, 500_000) + `\n\n... (safety truncated, ${content.length.toLocaleString()} total chars)` };
114
176
  }
115
- return { success: true, output: content };
177
+ return formatTextFileResult(lines, input, content);
116
178
  }
117
179
  function parsePageRange(range, totalPages) {
118
180
  const parts = range.split("-");
@@ -188,6 +250,7 @@ function computeWriteDiff(oldLines, newLines) {
188
250
  export function writeFile(input) {
189
251
  const path = resolvePath(input.path);
190
252
  const content = input.content;
253
+ invalidateCache(path); // Invalidate read cache before write
191
254
  const existed = existsSync(path);
192
255
  const oldContent = existed ? readFileSync(path, "utf-8") : null;
193
256
  backupFile(path); // Save backup before modification
@@ -236,6 +299,7 @@ export function editFile(input) {
236
299
  const replaceAll = input.replace_all ?? false;
237
300
  if (!existsSync(path))
238
301
  return { success: false, output: `File not found: ${path}` };
302
+ invalidateCache(path); // Invalidate read cache before edit
239
303
  backupFile(path); // Save backup before modification
240
304
  let content = readFileSync(path, "utf-8");
241
305
  if (!content.includes(oldString))
@@ -308,6 +372,7 @@ export function multiEdit(input) {
308
372
  return { success: false, output: `File not found: ${path}` };
309
373
  if (!Array.isArray(edits) || edits.length === 0)
310
374
  return { success: false, output: "edits array is required and must not be empty" };
375
+ invalidateCache(path); // Invalidate read cache before edit
311
376
  backupFile(path); // Save backup before modification
312
377
  let content = readFileSync(path, "utf-8");
313
378
  const diffParts = [];
@@ -371,6 +436,7 @@ export function notebookEdit(input) {
371
436
  const cellId = input.cell_id;
372
437
  if (!existsSync(path))
373
438
  return { success: false, output: `Notebook not found: ${path}` };
439
+ invalidateCache(path); // Invalidate read cache before notebook edit
374
440
  let notebook;
375
441
  try {
376
442
  notebook = JSON.parse(readFileSync(path, "utf-8"));
@@ -22,24 +22,34 @@ export async function runCommand(input) {
22
22
  const background = input.run_in_background;
23
23
  const description = input.description;
24
24
  debugLog("tools", `run_command: ${description || command.slice(0, 80)}`, { cwd, timeout, background });
25
- // UX guardrail only — the sandbox (macOS) is the real security boundary.
26
- // This catches obvious destructive commands before they reach the sandbox.
25
+ // ── Safety guardrail ──
26
+ // Minimal filter for catastrophic commands that bypass the macOS sandbox.
27
+ // The sandbox (sandbox-exec) is the real security boundary — it restricts
28
+ // file writes to cwd + /tmp + ~/.swagmanager only.
29
+ //
30
+ // Philosophy: block ONLY what the sandbox cannot prevent.
31
+ // - Hardware/filesystem damage (mkfs, dd to block devices)
32
+ // - Process-level DoS (fork bomb)
33
+ // - rm on literal root / or home ~ (sanity check)
34
+ //
35
+ // Everything else is allowed — chmod, chown, curl|bash, npm, pip, etc.
36
+ // are either harmless within the sandbox or need sudo to do real damage.
27
37
  if (command.length > 10000) {
28
38
  return { success: false, output: "Command too long (max 10000 chars)" };
29
39
  }
30
40
  const DANGEROUS_PATTERNS = [
31
- /\brm\s+(-[a-z]*f[a-z]*\s+)?(-[a-z]*r[a-z]*\s+)?(\/|~)/i,
32
- /\brm\s+(-[a-z]*r[a-z]*\s+)?(-[a-z]*f[a-z]*\s+)?(\/|~)/i,
41
+ // rm on literal root (/) or (/*) — NOT subdirectories like /tmp, /Users/...
42
+ /\brm\b[^|;]*\s\/(\*)?(\s*$|\s*[;|&])/i,
43
+ // rm on literal home (~) or (~/) or (~/*) — NOT subdirs like ~/project
44
+ /\brm\b[^|;]*\s~\/?(\*)?(\s*$|\s*[;|&])/i,
45
+ // Format filesystem — irreversible hardware damage
33
46
  /\bmkfs\b/i,
34
- /\bdd\s+.*\bif=/i,
35
- />\s*\/dev\/sd/,
47
+ // dd writing to raw block devices — irreversible hardware damage
48
+ /\bdd\b.*\bof=\s*\/dev\/(sd|hd|nvme|disk|rdisk|vd)/i,
49
+ // Redirect to raw block devices
50
+ />\s*\/dev\/(sd|hd|nvme|disk|rdisk|vd)/,
51
+ // Fork bomb — process-level DoS, sandbox cannot prevent
36
52
  /:\(\)\s*\{.*\|.*&\s*\}\s*;/,
37
- /\bchmod\s+(-[a-z]*R[a-z]*\s+)?777\s+\//i,
38
- /\bchown\s+(-[a-z]*R[a-z]*\s+).*\//i,
39
- /\bcurl\b.*\|\s*(ba)?sh\b/i,
40
- /\bwget\b.*\|\s*(ba)?sh\b/i,
41
- /\b(python|perl|ruby|node)\s+-e\s+.*\b(system|exec|spawn)\b/i,
42
- /base64\s+(-d|--decode)\s*\|\s*(ba)?sh/i,
43
53
  ];
44
54
  const command_lower = command.toLowerCase().replace(/\s+/g, " ");
45
55
  if (DANGEROUS_PATTERNS.some((p) => p.test(command_lower))) {
@@ -8,7 +8,7 @@
8
8
  * Override any color via ~/.swagmanager/theme.json:
9
9
  * { "brand": "#FF6600", "text": "#E0E0E0" }
10
10
  */
11
- declare const DEFAULT_COLORS: {
11
+ export declare const DEFAULT_COLORS: {
12
12
  brand: string;
13
13
  brandDim: string;
14
14
  success: string;
@@ -71,4 +71,3 @@ export declare const symbols: {
71
71
  export declare function boxLine(width: number): string;
72
72
  export declare function boxTop(width: number): string;
73
73
  export declare function boxBottom(width: number): string;
74
- export {};
@@ -14,7 +14,7 @@ import { homedir } from "os";
14
14
  // ============================================================================
15
15
  // COLORS — macOS system palette (dark appearance)
16
16
  // ============================================================================
17
- const DEFAULT_COLORS = {
17
+ export const DEFAULT_COLORS = {
18
18
  // Accents
19
19
  brand: "#0A84FF", // systemBlue
20
20
  brandDim: "#0071E3", // Apple link blue
@@ -1,5 +1,8 @@
1
1
  /**
2
- * whale code banner — clean terminal style
2
+ * whale code banner — ASCII art header
3
+ *
4
+ * Auto-switches to compact mode when terminal is too narrow for ASCII art.
5
+ * Each line rendered separately with wrap="truncate" to prevent reflow on resize.
3
6
  */
4
7
  interface WhaleBannerProps {
5
8
  subtitle?: string;
@@ -1,12 +1,16 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
- import { colors, symbols, boxTop, boxBottom } from "./Theme.js";
3
+ import { colors } from "./Theme.js";
4
+ const ASCII_LINES = [
5
+ " ╦ ╦╦ ╦╔═╗╦ ╔═╗ ╔═╗╔═╗╔╦╗╔═╗",
6
+ " ║║║╠═╣╠═╣║ ║╣ ║ ║ ║ ║║║╣ ",
7
+ " ╚╩╝╩ ╩╩ ╩╩═╝╚═╝ ╚═╝╚═╝═╩╝╚═╝",
8
+ ];
9
+ const MIN_WIDTH_FOR_ART = 38;
4
10
  export function WhaleBanner({ subtitle, version, compact }) {
5
- if (compact) {
6
- return (_jsxs(Box, { children: [_jsxs(Text, { color: colors.brand, bold: true, children: [symbols.sparkle, " whale code"] }), version && _jsxs(Text, { color: colors.dim, children: [" v", version] }), subtitle && _jsxs(Text, { color: colors.muted, children: [" ", subtitle] })] }));
11
+ const termWidth = process.stdout.columns || 80;
12
+ if (compact || termWidth < MIN_WIDTH_FOR_ART) {
13
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), _jsxs(Box, { children: [_jsx(Text, { color: colors.brand, bold: true, children: " \u25C6 whale code" }), version && _jsxs(Text, { color: colors.dim, children: [" v", version] }), subtitle && _jsxs(Text, { color: colors.muted, children: [" ", subtitle] })] })] }));
7
14
  }
8
- const width = 44;
9
- const titleLine = `${symbols.sparkle} whale code` + (version ? ` v${version}` : "");
10
- const titlePad = Math.max(0, Math.floor((width - 2 - titleLine.length) / 2));
11
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: colors.border, children: boxTop(width) }), _jsxs(Box, { children: [_jsx(Text, { color: colors.border, children: symbols.verticalBar }), _jsx(Text, { children: " ".repeat(width - 2) }), _jsx(Text, { color: colors.border, children: symbols.verticalBar })] }), _jsxs(Box, { children: [_jsx(Text, { color: colors.border, children: symbols.verticalBar }), _jsx(Text, { children: " ".repeat(titlePad) }), _jsxs(Text, { color: colors.brand, bold: true, children: [symbols.sparkle, " whale code"] }), version && _jsxs(Text, { color: colors.dim, children: [" v", version] }), _jsx(Text, { children: " ".repeat(Math.max(0, width - 2 - titlePad - titleLine.length)) }), _jsx(Text, { color: colors.border, children: symbols.verticalBar })] }), subtitle && (_jsxs(Box, { children: [_jsx(Text, { color: colors.border, children: symbols.verticalBar }), _jsx(Text, { children: " ".repeat(titlePad) }), _jsx(Text, { color: colors.muted, children: subtitle }), _jsx(Text, { children: " ".repeat(Math.max(0, width - 2 - titlePad - subtitle.length)) }), _jsx(Text, { color: colors.border, children: symbols.verticalBar })] })), _jsxs(Box, { children: [_jsx(Text, { color: colors.border, children: symbols.verticalBar }), _jsx(Text, { children: " ".repeat(width - 2) }), _jsx(Text, { color: colors.border, children: symbols.verticalBar })] }), _jsx(Text, { color: colors.border, children: boxBottom(width) })] }));
15
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { children: " " }), ASCII_LINES.map((line, i) => (_jsx(Text, { color: colors.info, bold: true, wrap: "truncate", children: line }, i)))] }));
12
16
  }
@@ -1,14 +1,15 @@
1
1
  /**
2
- * Markdown rendering — Apple-polished terminal output
2
+ * Markdown rendering — polished terminal output with shiki syntax highlighting
3
3
  *
4
- * Syntax theme: purples, blues, pinks no yellow.
5
- * Financials: green for gains, red for losses/deductions.
6
- * Uses marked + marked-terminal + cli-highlight.
4
+ * Uses shiki for VS Code-quality syntax highlighting.
5
+ * All chalk instances are rebuilt on theme switch via rebuildMarkdownRenderer().
6
+ * Uses marked + marked-terminal for markdown structure.
7
7
  */
8
8
  /** Width for top-level assistant text (accounts for MessageList marginLeft=2) */
9
9
  export declare function contentWidth(): number;
10
10
  /** Width for content nested inside tool results (MessageList=2 + ToolIndicator=2 + safety=2) */
11
11
  export declare function toolContentWidth(): number;
12
+ export declare function rebuildMarkdownRenderer(): void;
12
13
  /**
13
14
  * Close incomplete markdown fences for safe streaming rendering.
14
15
  * State-tracking approach: handles nested fences, escaped markers, double-backtick spans.