@supatest/cli 0.0.5 → 0.0.7

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 (69) hide show
  1. package/dist/index.js +9512 -157
  2. package/package.json +9 -6
  3. package/dist/commands/login.js +0 -392
  4. package/dist/commands/setup.js +0 -234
  5. package/dist/config.js +0 -29
  6. package/dist/core/agent.js +0 -259
  7. package/dist/modes/headless.js +0 -117
  8. package/dist/modes/interactive.js +0 -418
  9. package/dist/presenters/composite.js +0 -32
  10. package/dist/presenters/console.js +0 -163
  11. package/dist/presenters/react.js +0 -217
  12. package/dist/presenters/types.js +0 -1
  13. package/dist/presenters/web.js +0 -78
  14. package/dist/prompts/builder.js +0 -181
  15. package/dist/prompts/fixer.js +0 -148
  16. package/dist/prompts/index.js +0 -3
  17. package/dist/prompts/planner.js +0 -70
  18. package/dist/services/api-client.js +0 -244
  19. package/dist/services/event-streamer.js +0 -130
  20. package/dist/types.js +0 -1
  21. package/dist/ui/App.js +0 -322
  22. package/dist/ui/components/AuthBanner.js +0 -24
  23. package/dist/ui/components/AuthDialog.js +0 -32
  24. package/dist/ui/components/Banner.js +0 -12
  25. package/dist/ui/components/ExpandableSection.js +0 -17
  26. package/dist/ui/components/Header.js +0 -51
  27. package/dist/ui/components/HelpMenu.js +0 -89
  28. package/dist/ui/components/InputPrompt.js +0 -286
  29. package/dist/ui/components/MessageList.js +0 -42
  30. package/dist/ui/components/QueuedMessageDisplay.js +0 -31
  31. package/dist/ui/components/Scrollable.js +0 -103
  32. package/dist/ui/components/SessionSelector.js +0 -196
  33. package/dist/ui/components/StatusBar.js +0 -34
  34. package/dist/ui/components/messages/AssistantMessage.js +0 -20
  35. package/dist/ui/components/messages/ErrorMessage.js +0 -26
  36. package/dist/ui/components/messages/LoadingMessage.js +0 -28
  37. package/dist/ui/components/messages/ThinkingMessage.js +0 -17
  38. package/dist/ui/components/messages/TodoMessage.js +0 -44
  39. package/dist/ui/components/messages/ToolMessage.js +0 -218
  40. package/dist/ui/components/messages/UserMessage.js +0 -14
  41. package/dist/ui/contexts/KeypressContext.js +0 -527
  42. package/dist/ui/contexts/MouseContext.js +0 -98
  43. package/dist/ui/contexts/SessionContext.js +0 -129
  44. package/dist/ui/hooks/useAnimatedScrollbar.js +0 -83
  45. package/dist/ui/hooks/useBatchedScroll.js +0 -22
  46. package/dist/ui/hooks/useBracketedPaste.js +0 -31
  47. package/dist/ui/hooks/useFocus.js +0 -50
  48. package/dist/ui/hooks/useKeypress.js +0 -26
  49. package/dist/ui/hooks/useModeToggle.js +0 -25
  50. package/dist/ui/types/auth.js +0 -13
  51. package/dist/ui/utils/file-completion.js +0 -56
  52. package/dist/ui/utils/input.js +0 -50
  53. package/dist/ui/utils/markdown.js +0 -376
  54. package/dist/ui/utils/mouse.js +0 -189
  55. package/dist/ui/utils/theme.js +0 -59
  56. package/dist/utils/banner.js +0 -9
  57. package/dist/utils/encryption.js +0 -71
  58. package/dist/utils/events.js +0 -36
  59. package/dist/utils/keychain-storage.js +0 -120
  60. package/dist/utils/logger.js +0 -209
  61. package/dist/utils/node-version.js +0 -89
  62. package/dist/utils/plan-file.js +0 -75
  63. package/dist/utils/project-instructions.js +0 -23
  64. package/dist/utils/rich-logger.js +0 -208
  65. package/dist/utils/stdin.js +0 -25
  66. package/dist/utils/stdio.js +0 -80
  67. package/dist/utils/summary.js +0 -94
  68. package/dist/utils/token-storage.js +0 -242
  69. package/dist/version.js +0 -6
@@ -1,418 +0,0 @@
1
- /**
2
- * Interactive mode entry point
3
- * Launches the Ink UI and runs the agent with real-time updates
4
- */
5
- import { render } from "ink";
6
- import React, { useEffect, useRef } from "react";
7
- import { config as envConfig } from "../config";
8
- import { CoreAgent } from "../core/agent";
9
- import { ReactPresenter } from "../presenters/react";
10
- import { ApiClient, ApiError } from "../services/api-client";
11
- import { App } from "../ui/App";
12
- import { KeypressProvider } from "../ui/contexts/KeypressContext";
13
- import { MouseProvider } from "../ui/contexts/MouseContext";
14
- import { SessionProvider, useSession } from "../ui/contexts/SessionContext";
15
- import { useBracketedPaste } from "../ui/hooks/useBracketedPaste";
16
- import { disableMouseEvents, enableMouseEvents } from "../ui/utils/mouse";
17
- import { logger } from "../utils/logger";
18
- import { createInkStdio, patchStdio } from "../utils/stdio";
19
- import { CLI_VERSION } from "../version";
20
- /**
21
- * Get human-readable description for tool call (used when resuming sessions)
22
- */
23
- function getToolDescription(toolName, input) {
24
- switch (toolName) {
25
- case "Read":
26
- return input?.file_path || "file";
27
- case "Write":
28
- return input?.file_path || "file";
29
- case "Edit":
30
- return input?.file_path || "file";
31
- case "Bash": {
32
- const cmd = input?.command || "";
33
- return cmd.length > 60 ? `${cmd.substring(0, 60)}...` : cmd;
34
- }
35
- case "Glob":
36
- return `pattern: "${input?.pattern || "files"}"`;
37
- case "Grep": {
38
- const pattern = input?.pattern || "code";
39
- const path = input?.path;
40
- return path ? `"${pattern}" (in ${path})` : `"${pattern}"`;
41
- }
42
- case "Task":
43
- return input?.subagent_type || "task";
44
- case "TodoWrite":
45
- return "Updated todos";
46
- case "BashOutput":
47
- case "Command Output":
48
- return input?.bash_id || "shell output";
49
- default:
50
- return toolName;
51
- }
52
- }
53
- /**
54
- * Agent Runner Component
55
- * Runs the agent via CoreAgent and updates the UI context in real-time
56
- */
57
- const AgentRunner = ({ config, sessionId, apiClient, onComplete }) => {
58
- const { addMessage, updateLastMessage, updateMessageByToolId, setIsAgentRunning, updateStats, setTodos, shouldInterruptAgent, setShouldInterruptAgent, agentMode, planFilePath, } = useSession();
59
- // Keep a ref to the agent so we can call abort()
60
- const agentRef = useRef(null);
61
- // When shouldInterruptAgent changes to true, abort the agent
62
- useEffect(() => {
63
- if (shouldInterruptAgent && agentRef.current) {
64
- agentRef.current.abort();
65
- setShouldInterruptAgent(false);
66
- }
67
- }, [shouldInterruptAgent, setShouldInterruptAgent]);
68
- useEffect(() => {
69
- let isMounted = true;
70
- const runAgent = async () => {
71
- setIsAgentRunning(true);
72
- try {
73
- // Set up environment for Claude Code
74
- const proxyUrl = config.supatestApiUrl || "https://code-api.supatest.ai";
75
- const baseUrl = `${proxyUrl}/v1/sessions/${sessionId}/anthropic`;
76
- process.env.ANTHROPIC_BASE_URL = baseUrl;
77
- process.env.ANTHROPIC_API_KEY = config.supatestApiKey;
78
- // Create presenter with callbacks to React state
79
- const presenter = new ReactPresenter({
80
- addMessage: (msg) => {
81
- if (isMounted)
82
- addMessage(msg);
83
- },
84
- updateLastMessage: (update) => {
85
- if (isMounted)
86
- updateLastMessage(update);
87
- },
88
- updateMessageByToolId: (toolId, update) => {
89
- if (isMounted)
90
- updateMessageByToolId(toolId, update);
91
- },
92
- updateStats: (stats) => {
93
- if (isMounted)
94
- updateStats(stats);
95
- },
96
- setTodos: (todos) => {
97
- if (isMounted)
98
- setTodos(todos);
99
- },
100
- // Note: onComplete is now called after agent.run() returns
101
- // to capture the providerSessionId from the result
102
- onComplete: () => { },
103
- }, apiClient, sessionId, config.verbose);
104
- // Build mode-specific configuration
105
- // Plan mode uses planSystemPrompt and restricts to read-only tools
106
- const runConfig = {
107
- ...config,
108
- mode: agentMode,
109
- planFilePath,
110
- systemPromptAppend: agentMode === 'plan'
111
- ? envConfig.planSystemPrompt
112
- : config.systemPromptAppend,
113
- };
114
- // Run the agent through CoreAgent
115
- // Store ref so we can abort from interrupt effect
116
- const agent = new CoreAgent(presenter);
117
- agentRef.current = agent;
118
- const result = await agent.run(runConfig);
119
- // Pass providerSessionId on completion for storage
120
- if (isMounted) {
121
- onComplete(result.success, result.providerSessionId);
122
- }
123
- }
124
- catch (error) {
125
- if (isMounted) {
126
- const errorMessage = error instanceof Error ? error.message : String(error);
127
- addMessage({
128
- type: "error",
129
- content: errorMessage,
130
- errorType: "error",
131
- });
132
- await apiClient.streamEvent(sessionId, {
133
- type: "session_error",
134
- error: errorMessage,
135
- });
136
- onComplete(false);
137
- }
138
- }
139
- finally {
140
- if (isMounted) {
141
- setIsAgentRunning(false);
142
- }
143
- }
144
- };
145
- runAgent();
146
- return () => {
147
- isMounted = false;
148
- agentRef.current = null;
149
- };
150
- }, []); // eslint-disable-line react-hooks/exhaustive-deps
151
- return null; // This component doesn't render anything
152
- };
153
- /**
154
- * Content component for InteractiveApp that has access to SessionContext
155
- */
156
- const InteractiveAppContent = ({ config, sessionId: initialSessionId, webUrl, apiClient, onExit }) => {
157
- const { addMessage, loadMessages, setSessionId: setContextSessionId, } = useSession();
158
- const [sessionId, setSessionId] = React.useState(initialSessionId);
159
- const [currentTask, setCurrentTask] = React.useState(config.task);
160
- const [taskId, setTaskId] = React.useState(0);
161
- const [shouldRunAgent, setShouldRunAgent] = React.useState(!!config.task);
162
- const [taskQueue, setTaskQueue] = React.useState([]);
163
- // Track provider session ID for resume capability
164
- const [providerSessionId, setProviderSessionId] = React.useState();
165
- const handleSubmitTask = async (task) => {
166
- // Create session on first message if it doesn't exist
167
- if (!sessionId) {
168
- try {
169
- const truncatedTitle = task.length > 50 ? task.slice(0, 50) + "..." : task;
170
- const session = await apiClient.createSession(truncatedTitle, {
171
- cliVersion: CLI_VERSION,
172
- cwd: config.cwd || process.cwd(),
173
- });
174
- setSessionId(session.sessionId);
175
- setContextSessionId(session.sessionId);
176
- }
177
- catch (error) {
178
- const errorMessage = error instanceof ApiError
179
- ? error.message
180
- : `Failed to create session: ${error instanceof Error ? error.message : String(error)}`;
181
- addMessage({
182
- type: "error",
183
- content: errorMessage,
184
- errorType: error instanceof ApiError && error.isAuthError ? "warning" : "error",
185
- });
186
- return;
187
- }
188
- }
189
- if (shouldRunAgent) {
190
- setTaskQueue((prev) => [...prev, task]);
191
- }
192
- else {
193
- setCurrentTask(task);
194
- addMessage({
195
- type: "user",
196
- content: task,
197
- });
198
- setTaskId((prev) => prev + 1);
199
- setShouldRunAgent(true);
200
- }
201
- };
202
- const handleAgentComplete = async (_success, newProviderSessionId) => {
203
- setShouldRunAgent(false);
204
- // Store the provider session ID for future resume capability
205
- if (sessionId && newProviderSessionId && newProviderSessionId !== providerSessionId) {
206
- setProviderSessionId(newProviderSessionId);
207
- try {
208
- await apiClient.updateSession(sessionId, {
209
- providerSessionId: newProviderSessionId,
210
- });
211
- }
212
- catch {
213
- // Non-critical - session will work without resume capability
214
- }
215
- }
216
- };
217
- // Process queued tasks when agent becomes idle
218
- React.useEffect(() => {
219
- if (!shouldRunAgent && taskQueue.length > 0) {
220
- const [nextTask, ...remaining] = taskQueue;
221
- setTaskQueue(remaining);
222
- setCurrentTask(nextTask);
223
- addMessage({
224
- type: "user",
225
- content: nextTask,
226
- });
227
- setTaskId((prev) => prev + 1);
228
- setShouldRunAgent(true);
229
- }
230
- }, [shouldRunAgent, taskQueue, addMessage]);
231
- return (React.createElement(React.Fragment, null,
232
- React.createElement(App, { apiClient: apiClient, config: { ...config, task: currentTask }, onExit: onExit, onResumeSession: async (session) => {
233
- try {
234
- if (!apiClient) {
235
- addMessage({
236
- type: "error",
237
- content: "API client not available. Cannot resume session.",
238
- errorType: "error",
239
- });
240
- return;
241
- }
242
- const response = await apiClient.getSessionMessages(session.id);
243
- const apiMessages = response.messages;
244
- // First pass: collect all tool_results by tool_use_id
245
- const toolResults = new Map();
246
- for (const msg of apiMessages) {
247
- const contentBlocks = msg.content;
248
- if (!contentBlocks)
249
- continue;
250
- for (const block of contentBlocks) {
251
- if (block.type === "tool_result" && block.tool_use_id) {
252
- // Extract text content from tool result
253
- let resultText = "";
254
- if (typeof block.content === "string") {
255
- resultText = block.content;
256
- }
257
- else if (Array.isArray(block.content)) {
258
- resultText = block.content
259
- .map((c) => c.text || "")
260
- .join("\n");
261
- }
262
- toolResults.set(block.tool_use_id, resultText);
263
- }
264
- }
265
- }
266
- // Convert API messages to UI Message format
267
- const uiMessages = apiMessages.flatMap((msg) => {
268
- const messages = [];
269
- const contentBlocks = msg.content;
270
- if (!contentBlocks || contentBlocks.length === 0) {
271
- return [];
272
- }
273
- for (const block of contentBlocks) {
274
- if (block.type === "text") {
275
- messages.push({
276
- id: `${msg.id}-${messages.length}`,
277
- type: msg.role,
278
- content: block.text,
279
- timestamp: new Date(msg.createdAt).getTime(),
280
- });
281
- }
282
- else if (block.type === "thinking") {
283
- messages.push({
284
- id: `${msg.id}-${messages.length}`,
285
- type: "thinking",
286
- content: block.thinking,
287
- timestamp: new Date(msg.createdAt).getTime(),
288
- });
289
- }
290
- else if (block.type === "tool_use") {
291
- // Look up the tool result for this tool_use
292
- const toolResult = toolResults.get(block.id);
293
- messages.push({
294
- id: `${msg.id}-${messages.length}`,
295
- type: "tool",
296
- content: getToolDescription(block.name, block.input),
297
- toolName: block.name,
298
- toolInput: block.input,
299
- toolResult: toolResult,
300
- toolUseId: block.id,
301
- timestamp: new Date(msg.createdAt).getTime(),
302
- });
303
- }
304
- }
305
- return messages;
306
- });
307
- setSessionId(session.id);
308
- setContextSessionId(session.id);
309
- // Set the provider session ID for resume capability
310
- // The session object may have providerSessionId from the API
311
- if (session.providerSessionId) {
312
- setProviderSessionId(session.providerSessionId);
313
- }
314
- uiMessages.push({
315
- id: `resume-info-${Date.now()}`,
316
- type: "error",
317
- content: `Resumed session: ${session.title || "Untitled"} (${apiMessages.length} messages loaded)`,
318
- errorType: "info",
319
- timestamp: Date.now(),
320
- });
321
- loadMessages(uiMessages);
322
- }
323
- catch (error) {
324
- const errorMessage = error instanceof ApiError
325
- ? error.message
326
- : `Failed to resume session: ${error instanceof Error ? error.message : String(error)}`;
327
- addMessage({
328
- type: "error",
329
- content: errorMessage,
330
- errorType: error instanceof ApiError && error.isAuthError
331
- ? "warning"
332
- : "error",
333
- });
334
- }
335
- }, onSubmitTask: handleSubmitTask, queuedTasks: taskQueue, sessionId: sessionId, webUrl: webUrl }),
336
- shouldRunAgent && currentTask && sessionId && (React.createElement(AgentRunner, { apiClient: apiClient, config: { ...config, task: currentTask, providerSessionId }, key: `${taskId}`, onComplete: handleAgentComplete, sessionId: sessionId }))));
337
- };
338
- /**
339
- * Main wrapper component that combines UI and agent
340
- */
341
- const InteractiveApp = (props) => {
342
- useBracketedPaste();
343
- return (React.createElement(KeypressProvider, null,
344
- React.createElement(MouseProvider, { mouseEventsEnabled: true },
345
- React.createElement(SessionProvider, null,
346
- React.createElement(InteractiveAppContent, { ...props })))));
347
- };
348
- /**
349
- * Run the interactive mode with Ink UI
350
- */
351
- export async function runInteractive(config) {
352
- let success = false;
353
- let unmountFn = null;
354
- let isExiting = false;
355
- const cleanupStdio = patchStdio();
356
- const gracefulExit = async (exitCode) => {
357
- if (isExiting)
358
- return;
359
- isExiting = true;
360
- disableMouseEvents();
361
- cleanupStdio();
362
- if (unmountFn) {
363
- unmountFn();
364
- }
365
- process.exit(exitCode);
366
- };
367
- const handleSigInt = async () => {
368
- await gracefulExit(0);
369
- };
370
- process.on("SIGINT", handleSigInt);
371
- process.on("SIGTERM", handleSigInt);
372
- const isDev = process.env.NODE_ENV === "development";
373
- logger.enableFileLogging(isDev);
374
- const apiUrl = config.supatestApiUrl || "https://code-api.supatest.ai";
375
- const apiClient = new ApiClient(apiUrl, config.supatestApiKey);
376
- try {
377
- process.stdout.write("\x1Bc");
378
- console.clear();
379
- enableMouseEvents();
380
- const { stdout: inkStdout, stderr: inkStderr } = createInkStdio();
381
- let sessionId;
382
- let webUrl;
383
- if (config.task) {
384
- const truncatedTitle = config.task.length > 50 ? config.task.slice(0, 50) + "..." : config.task;
385
- const session = await apiClient.createSession(truncatedTitle, {
386
- cliVersion: CLI_VERSION,
387
- cwd: config.cwd || process.cwd(),
388
- });
389
- sessionId = session.sessionId;
390
- webUrl = session.webUrl;
391
- }
392
- const { unmount, waitUntilExit } = render(React.createElement(InteractiveApp, { apiClient: apiClient, config: config, onExit: (exitSuccess) => {
393
- success = exitSuccess;
394
- }, sessionId: sessionId, webUrl: webUrl }), {
395
- stdout: inkStdout,
396
- stderr: inkStderr,
397
- stdin: process.stdin,
398
- alternateBuffer: true,
399
- exitOnCtrlC: false,
400
- });
401
- unmountFn = unmount;
402
- await waitUntilExit();
403
- unmount();
404
- disableMouseEvents();
405
- cleanupStdio();
406
- process.off("SIGINT", handleSigInt);
407
- process.off("SIGTERM", handleSigInt);
408
- process.exit(success ? 0 : 1);
409
- }
410
- catch (error) {
411
- disableMouseEvents();
412
- cleanupStdio();
413
- process.off("SIGINT", handleSigInt);
414
- process.off("SIGTERM", handleSigInt);
415
- console.error("Failed to start interactive mode:", error instanceof Error ? error.message : String(error));
416
- process.exit(1);
417
- }
418
- }
@@ -1,32 +0,0 @@
1
- export class CompositePresenter {
2
- presenters;
3
- constructor(presenters) {
4
- this.presenters = presenters;
5
- }
6
- async onStart(config) {
7
- await Promise.all(this.presenters.map((p) => p.onStart(config)));
8
- }
9
- onLog(message) {
10
- for (const p of this.presenters) {
11
- p.onLog(message);
12
- }
13
- }
14
- async onAssistantText(text) {
15
- await Promise.all(this.presenters.map((p) => p.onAssistantText(text)));
16
- }
17
- async onThinking(text) {
18
- await Promise.all(this.presenters.map((p) => p.onThinking(text)));
19
- }
20
- async onToolUse(tool, input, toolId) {
21
- await Promise.all(this.presenters.map((p) => p.onToolUse(tool, input, toolId)));
22
- }
23
- async onTurnComplete(content) {
24
- await Promise.all(this.presenters.map((p) => p.onTurnComplete(content)));
25
- }
26
- async onError(error) {
27
- await Promise.all(this.presenters.map((p) => p.onError(error)));
28
- }
29
- async onComplete(result) {
30
- await Promise.all(this.presenters.map((p) => p.onComplete(result)));
31
- }
32
- }
@@ -1,163 +0,0 @@
1
- import chalk from "chalk";
2
- import ora from "ora";
3
- import { getToolDisplayName } from "shared";
4
- import { logger } from "../utils/logger";
5
- import { generateSummary } from "../utils/summary";
6
- // Fun spinner messages that rotate randomly
7
- const SPINNER_MESSAGES = [
8
- "Brainstorming...",
9
- "Brewing coffee...",
10
- "Sipping espresso...",
11
- "Testing theories...",
12
- "Making magic...",
13
- "Multiplying matrices...",
14
- ];
15
- function getRandomSpinnerMessage() {
16
- return SPINNER_MESSAGES[Math.floor(Math.random() * SPINNER_MESSAGES.length)];
17
- }
18
- function createShimmerFrames(text) {
19
- const frames = [];
20
- const baseText = text;
21
- const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
22
- for (let i = 0; i <= baseText.length; i++) {
23
- const spinnerIcon = spinnerFrames[i % spinnerFrames.length];
24
- const before = chalk.white(baseText.slice(0, i));
25
- const current = baseText[i] || "";
26
- const after = chalk.white(baseText.slice(i + 1));
27
- const shimmerText = before + chalk.cyan.bold(current) + after;
28
- frames.push(`${chalk.cyan(spinnerIcon)} ${shimmerText}`);
29
- }
30
- return frames;
31
- }
32
- export class ConsolePresenter {
33
- spinner = null;
34
- verbose;
35
- stats;
36
- constructor(options) {
37
- this.verbose = options.verbose;
38
- this.stats = {
39
- startTime: Date.now(),
40
- iterations: 0,
41
- filesModified: new Set(),
42
- commandsRun: [],
43
- errors: [],
44
- };
45
- logger.setVerbose(options.verbose);
46
- }
47
- onStart(config) {
48
- logger.raw("");
49
- logger.raw(chalk.white.bold("Task:") + " " + chalk.cyan(config.task));
50
- if (config.logs) {
51
- logger.info("Processing provided logs...");
52
- }
53
- logger.raw("");
54
- this.startSpinner();
55
- }
56
- onLog(message) {
57
- if (this.verbose) {
58
- this.stopSpinner();
59
- logger.debug(message);
60
- this.startSpinner();
61
- }
62
- }
63
- onAssistantText(text) {
64
- this.stopSpinner();
65
- logger.raw(text);
66
- this.startSpinner();
67
- }
68
- onThinking(text) {
69
- if (this.verbose) {
70
- this.stopSpinner();
71
- logger.debug(`Thinking: ${text}`);
72
- this.startSpinner();
73
- }
74
- }
75
- onToolUse(tool, input, _toolId) {
76
- if (this.spinner) {
77
- const displayName = getToolDisplayName(tool);
78
- this.spinner.text = `Using ${displayName}...`;
79
- }
80
- this.stopSpinner();
81
- if (tool === "Read") {
82
- logger.toolRead(input?.file_path || "file");
83
- }
84
- else if (tool === "Write") {
85
- const filePath = input?.file_path;
86
- if (filePath) {
87
- this.stats.filesModified.add(filePath);
88
- logger.toolWrite(filePath);
89
- }
90
- }
91
- else if (tool === "Edit") {
92
- const filePath = input?.file_path;
93
- if (filePath) {
94
- this.stats.filesModified.add(filePath);
95
- logger.toolEdit(filePath);
96
- }
97
- }
98
- else if (tool === "Bash") {
99
- const command = input?.command;
100
- if (command) {
101
- this.stats.commandsRun.push(command);
102
- const shortCmd = command.length > 60 ? `${command.substring(0, 60)}...` : command;
103
- logger.toolBash(shortCmd);
104
- }
105
- }
106
- else if (tool === "Glob") {
107
- logger.toolSearch("files", input?.pattern || "");
108
- }
109
- else if (tool === "Grep") {
110
- logger.toolSearch("code", input?.pattern || "");
111
- }
112
- else if (tool === "Task") {
113
- logger.toolAgent(input?.subagent_type || "task");
114
- }
115
- else if (tool === "TodoWrite") {
116
- const todos = input?.todos;
117
- if (Array.isArray(todos)) {
118
- logger.todoUpdate(todos);
119
- }
120
- else {
121
- logger.info("📝 Updated todos");
122
- }
123
- }
124
- else {
125
- logger.debug(`🔧 Using tool: ${tool}`);
126
- }
127
- logger.raw("");
128
- this.startSpinner();
129
- }
130
- onTurnComplete(_content) {
131
- // No-op for console - we handle output in individual callbacks
132
- }
133
- onError(error) {
134
- this.stopSpinner();
135
- logger.error(error);
136
- this.stats.errors.push(error);
137
- }
138
- onComplete(result) {
139
- this.stopSpinner();
140
- this.stats.endTime = Date.now();
141
- this.stats.iterations = result.iterations;
142
- const summaryText = generateSummary(this.stats, result, this.verbose);
143
- logger.raw(summaryText);
144
- }
145
- startSpinner() {
146
- if (!this.spinner && !logger.isSilent()) {
147
- const message = getRandomSpinnerMessage();
148
- this.spinner = ora({
149
- spinner: {
150
- interval: 80,
151
- frames: createShimmerFrames(message),
152
- },
153
- });
154
- this.spinner.start();
155
- }
156
- }
157
- stopSpinner() {
158
- if (this.spinner) {
159
- this.spinner.stop();
160
- this.spinner = null;
161
- }
162
- }
163
- }