@supatest/cli 0.0.3 → 0.0.4

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