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
@@ -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,25 @@ 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 readCache = new Map();
20
+ /** Clear the session-level read cache. Call on session reset. */
21
+ export function clearReadCache() {
22
+ readCache.clear();
23
+ }
24
+ /** Invalidate a specific path from the cache (call on write/edit). */
25
+ function invalidateCache(path) {
26
+ readCache.delete(path);
27
+ }
28
+ /** LRU eviction: remove oldest entry when cache is full. */
29
+ function evictIfNeeded() {
30
+ if (readCache.size >= READ_CACHE_MAX) {
31
+ // Map insertion order = LRU order; delete the first (oldest) entry
32
+ const oldest = readCache.keys().next().value;
33
+ if (oldest)
34
+ readCache.delete(oldest);
35
+ }
36
+ }
18
37
  // ============================================================================
19
38
  // READ FILE
20
39
  // ============================================================================
@@ -30,6 +49,28 @@ const AUDIO_MEDIA_TYPES = {
30
49
  m4a: "audio/mp4",
31
50
  };
32
51
  const AUDIO_MAX_SIZE = 25 * 1024 * 1024; // 25MB
52
+ function formatTextFileResult(lines, input, content) {
53
+ const offset = input.offset || 1; // 1-based
54
+ const limit = input.limit;
55
+ if (offset > 1 || limit) {
56
+ const startIdx = Math.max(0, offset - 1);
57
+ const endIdx = limit ? startIdx + limit : lines.length;
58
+ const slice = lines.slice(startIdx, endIdx);
59
+ const numbered = slice.map((line, i) => {
60
+ const lineNum = startIdx + i + 1;
61
+ return `${String(lineNum).padStart(6)} ${line}`;
62
+ });
63
+ let output = numbered.join("\n");
64
+ if (endIdx < lines.length) {
65
+ output += `\n\n... (showing lines ${startIdx + 1}-${Math.min(endIdx, lines.length)} of ${lines.length})`;
66
+ }
67
+ return { success: true, output };
68
+ }
69
+ if (content.length > 500_000) {
70
+ return { success: true, output: content.slice(0, 500_000) + `\n\n... (safety truncated, ${content.length.toLocaleString()} total chars)` };
71
+ }
72
+ return { success: true, output: content };
73
+ }
33
74
  export async function readFile(input) {
34
75
  const path = resolvePath(input.path);
35
76
  if (!existsSync(path))
@@ -90,29 +131,36 @@ export async function readFile(input) {
90
131
  return { success: false, output: `Failed to parse PDF: ${err}` };
91
132
  }
92
133
  }
93
- // Text files — existing behavior
134
+ // Text files — check cache first via mtime comparison
135
+ try {
136
+ const stat = statSync(path);
137
+ const cached = readCache.get(path);
138
+ if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
139
+ // Cache hit — use previously read content
140
+ debugLog("file-ops", `readFile cache hit: ${path}`);
141
+ const content = cached.content;
142
+ const lines = content.split("\n");
143
+ // Move to end of map for LRU freshness
144
+ readCache.delete(path);
145
+ readCache.set(path, cached);
146
+ return formatTextFileResult(lines, input, content);
147
+ }
148
+ }
149
+ catch {
150
+ // stat failed — fall through to normal read
151
+ }
94
152
  const content = readFileSync(path, "utf-8");
95
153
  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})`;
109
- }
110
- return { success: true, output };
154
+ // Cache the read
155
+ try {
156
+ const stat = statSync(path);
157
+ evictIfNeeded();
158
+ readCache.set(path, { content, mtimeMs: stat.mtimeMs, size: stat.size });
111
159
  }
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)` };
160
+ catch {
161
+ // stat failed skip caching
114
162
  }
115
- return { success: true, output: content };
163
+ return formatTextFileResult(lines, input, content);
116
164
  }
117
165
  function parsePageRange(range, totalPages) {
118
166
  const parts = range.split("-");
@@ -188,6 +236,7 @@ function computeWriteDiff(oldLines, newLines) {
188
236
  export function writeFile(input) {
189
237
  const path = resolvePath(input.path);
190
238
  const content = input.content;
239
+ invalidateCache(path); // Invalidate read cache before write
191
240
  const existed = existsSync(path);
192
241
  const oldContent = existed ? readFileSync(path, "utf-8") : null;
193
242
  backupFile(path); // Save backup before modification
@@ -236,6 +285,7 @@ export function editFile(input) {
236
285
  const replaceAll = input.replace_all ?? false;
237
286
  if (!existsSync(path))
238
287
  return { success: false, output: `File not found: ${path}` };
288
+ invalidateCache(path); // Invalidate read cache before edit
239
289
  backupFile(path); // Save backup before modification
240
290
  let content = readFileSync(path, "utf-8");
241
291
  if (!content.includes(oldString))
@@ -308,6 +358,7 @@ export function multiEdit(input) {
308
358
  return { success: false, output: `File not found: ${path}` };
309
359
  if (!Array.isArray(edits) || edits.length === 0)
310
360
  return { success: false, output: "edits array is required and must not be empty" };
361
+ invalidateCache(path); // Invalidate read cache before edit
311
362
  backupFile(path); // Save backup before modification
312
363
  let content = readFileSync(path, "utf-8");
313
364
  const diffParts = [];
@@ -371,6 +422,7 @@ export function notebookEdit(input) {
371
422
  const cellId = input.cell_id;
372
423
  if (!existsSync(path))
373
424
  return { success: false, output: `Notebook not found: ${path}` };
425
+ invalidateCache(path); // Invalidate read cache before notebook edit
374
426
  let notebook;
375
427
  try {
376
428
  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.