@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supatest/cli",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "Supatest CLI - AI-powered task automation for CI/CD",
5
5
  "type": "module",
6
6
  "bin": {
@@ -63,12 +63,13 @@
63
63
  "string-width": "^8.1.0",
64
64
  "strip-ansi": "^7.1.2",
65
65
  "wrap-ansi": "^9.0.2",
66
- "shared": "0.0.1"
66
+ "zod": "^3.25.76"
67
67
  },
68
68
  "devDependencies": {
69
69
  "@types/node": "^20.12.12",
70
70
  "@types/react": "^19.0.0",
71
71
  "nodemon": "^3.1.11",
72
+ "tsup": "^8.5.1",
72
73
  "tsx": "^4.10.0",
73
74
  "typescript": "^5.4.5"
74
75
  },
@@ -82,7 +83,7 @@
82
83
  "dev": "NODE_ENV=development tsx src/index.ts",
83
84
  "dev:watch": "nodemon",
84
85
  "dev:bun": "bun run src/index.ts",
85
- "build": "tsc && node scripts/make-executable.js",
86
+ "build": "tsup",
86
87
  "build:bun": "bun build src/index.ts --compile --external=@anthropic-ai/claude-agent-sdk --outfile dist/supatest-ai && node scripts/copy-claude-cli.js",
87
88
  "type-check": "tsc --noEmit",
88
89
  "clean:bundle": "rimraf dist",
@@ -1,589 +0,0 @@
1
- import { createRequire } from "node:module";
2
- import { dirname, join } from "node:path";
3
- import { query } from "@anthropic-ai/claude-agent-sdk";
4
- import chalk from "chalk";
5
- import ora from "ora";
6
- import { config as envConfig } from "./config";
7
- import { ApiClient } from "./services/api-client";
8
- import { EventStreamer } from "./services/event-streamer";
9
- import { logger } from "./utils/logger";
10
- import { generateSummary } from "./utils/summary";
11
- const CLI_VERSION = "0.0.1";
12
- // Fun spinner messages that rotate randomly
13
- const SPINNER_MESSAGES = [
14
- "Brainstorming...",
15
- "Brewing coffee...",
16
- "Sipping espresso...",
17
- "Testing theories...",
18
- "Making magic...",
19
- "Multiplying matrices...",
20
- ];
21
- function getRandomSpinnerMessage() {
22
- return SPINNER_MESSAGES[Math.floor(Math.random() * SPINNER_MESSAGES.length)];
23
- }
24
- // Create shimmer effect frames that include the spinner icon
25
- function createShimmerFrames(text) {
26
- const frames = [];
27
- const baseText = text;
28
- // Ora's default dots spinner frames
29
- const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
30
- // Create frames with moving highlight across the text
31
- for (let i = 0; i <= baseText.length; i++) {
32
- const spinnerIcon = spinnerFrames[i % spinnerFrames.length];
33
- const before = chalk.white(baseText.slice(0, i));
34
- const current = baseText[i] || '';
35
- const after = chalk.white(baseText.slice(i + 1));
36
- const shimmerText = before + chalk.cyan.bold(current) + after;
37
- frames.push(`${chalk.cyan(spinnerIcon)} ${shimmerText}`);
38
- }
39
- return frames;
40
- }
41
- export async function runAgent(config) {
42
- const stats = {
43
- startTime: Date.now(),
44
- iterations: 0,
45
- filesModified: new Set(),
46
- commandsRun: [],
47
- errors: [],
48
- };
49
- let claudeCodeStderr = "";
50
- logger.setVerbose(config.verbose);
51
- // Display metadata
52
- logger.raw("");
53
- // Get git branch if available
54
- let gitBranch = "";
55
- try {
56
- const { execSync } = await import("node:child_process");
57
- gitBranch = execSync("git rev-parse --abbrev-ref HEAD", {
58
- encoding: "utf8",
59
- stdio: ["pipe", "pipe", "ignore"]
60
- }).trim();
61
- }
62
- catch {
63
- // Not in a git repo or git not available
64
- }
65
- const metadataParts = [
66
- chalk.dim("Supatest AI ") + chalk.cyan(`v${CLI_VERSION}`),
67
- chalk.dim("Model: ") + chalk.cyan(envConfig.anthropicModelName),
68
- ];
69
- if (gitBranch) {
70
- metadataParts.push(chalk.dim("Branch: ") + chalk.cyan(gitBranch));
71
- }
72
- logger.raw(metadataParts.join(chalk.dim(" • ")));
73
- logger.divider();
74
- // Show environment info in verbose mode
75
- if (config.verbose) {
76
- logger.raw("");
77
- logger.debug("Environment & System Info:");
78
- // Node.js version
79
- logger.debug(` Node.js: ${process.version}`);
80
- // OS & Platform
81
- const os = await import("node:os");
82
- logger.debug(` Platform: ${os.platform()} ${os.arch()} (${os.type()} ${os.release()})`);
83
- // Working directory
84
- logger.debug(` Working Dir: ${process.cwd()}`);
85
- // Git status
86
- try {
87
- const { execSync } = await import("node:child_process");
88
- const gitStatus = execSync("git status --porcelain", {
89
- encoding: "utf8",
90
- stdio: ["pipe", "pipe", "ignore"]
91
- }).trim();
92
- const statusText = gitStatus ? "dirty (uncommitted changes)" : "clean";
93
- logger.debug(` Git Status: ${statusText}`);
94
- }
95
- catch {
96
- logger.debug(` Git Status: not a git repository`);
97
- }
98
- // Available disk space
99
- try {
100
- const fs = await import("node:fs");
101
- const stats = fs.statfsSync(process.cwd());
102
- const availableGB = ((stats.bavail * stats.bsize) / (1024 ** 3)).toFixed(2);
103
- logger.debug(` Available Disk: ${availableGB} GB`);
104
- }
105
- catch {
106
- logger.debug(` Available Disk: unable to determine`);
107
- }
108
- }
109
- logger.raw("");
110
- logger.raw("");
111
- logger.raw(chalk.white.bold("Task:") + " " + chalk.cyan(config.task));
112
- if (config.logs) {
113
- logger.info("Processing provided logs...");
114
- }
115
- logger.raw("");
116
- // Create session on backend and initialize event streaming
117
- const apiUrl = config.supatestApiUrl || "https://api.supatest.ai";
118
- const apiClient = new ApiClient(apiUrl, config.supatestApiKey);
119
- let sessionId;
120
- let webUrl;
121
- let eventStreamer;
122
- try {
123
- const session = await apiClient.createSession(config.task, {
124
- cliVersion: CLI_VERSION,
125
- cwd: process.cwd(),
126
- });
127
- sessionId = session.sessionId;
128
- webUrl = session.webUrl;
129
- eventStreamer = new EventStreamer(apiClient, sessionId);
130
- logger.raw("");
131
- logger.divider();
132
- logger.raw(chalk.white.bold("View session live: ") +
133
- chalk.cyan.underline(webUrl));
134
- logger.divider();
135
- logger.raw("");
136
- }
137
- catch (error) {
138
- logger.warn(`Failed to create session on backend: ${error.message}`);
139
- logger.warn("Continuing without web streaming...");
140
- }
141
- logger.raw("");
142
- // Initialize spinner variable (will be created on first agent turn)
143
- let spinner = null;
144
- // Resolve path to Claude Code executable
145
- let claudeCodePath;
146
- try {
147
- // Build the prompt
148
- let prompt = config.task;
149
- if (config.logs) {
150
- prompt = `${config.task}\n\nHere are the logs to analyze:\n\`\`\`\n${config.logs}\n\`\`\``;
151
- }
152
- const proxyUrl = config.supatestApiUrl || "https://api.supatest.ai";
153
- // Build base URL with session ID embedded in the path for message tracking
154
- // The proxy will extract the session ID from the path and lookup the message ID in Redis
155
- // Format: {proxyUrl}/v1/sessions/{sessionId}/anthropic
156
- let baseUrl = `${proxyUrl}/public`;
157
- if (sessionId) {
158
- baseUrl = `${proxyUrl}/v1/sessions/${sessionId}/anthropic`;
159
- if (config.verbose) {
160
- logger.debug(`Using session-based proxy URL: ${baseUrl}`);
161
- }
162
- }
163
- process.env.ANTHROPIC_BASE_URL = baseUrl;
164
- process.env.ANTHROPIC_API_KEY = config.supatestApiKey;
165
- if (config.verbose) {
166
- logger.debug(`Using Supatest proxy: ${proxyUrl}/public`);
167
- logger.debug(`Supatest API key: ${config.supatestApiKey?.substring(0, 15)}...`);
168
- }
169
- // Allow override via environment variable for testing/debugging
170
- if (envConfig.claudeCodeExecutablePath) {
171
- claudeCodePath = envConfig.claudeCodeExecutablePath;
172
- if (config.verbose) {
173
- logger.debug(`Using CLAUDE_CODE_EXECUTABLE_PATH: ${claudeCodePath}`);
174
- }
175
- }
176
- else {
177
- // Determine binary directory
178
- // For compiled binaries: same directory as the executable
179
- // For development: use the SDK's bundled cli.js from node_modules
180
- const isCompiledBinary = process.execPath && !process.execPath.includes("node");
181
- if (isCompiledBinary) {
182
- // Production: claude-code-cli.js should be next to the binary
183
- claudeCodePath = join(dirname(process.execPath), "claude-code-cli.js");
184
- if (config.verbose) {
185
- logger.debug(`Production mode: ${claudeCodePath}`);
186
- }
187
- }
188
- else {
189
- // Development: use SDK's cli.js from node_modules
190
- const require = createRequire(import.meta.url);
191
- const sdkPath = require.resolve("@anthropic-ai/claude-agent-sdk/sdk.mjs");
192
- claudeCodePath = join(dirname(sdkPath), "cli.js");
193
- if (config.verbose) {
194
- logger.debug(`Development mode: ${claudeCodePath}`);
195
- }
196
- }
197
- // Verify the file exists
198
- const fs = await import("node:fs/promises");
199
- try {
200
- await fs.access(claudeCodePath);
201
- if (config.verbose) {
202
- logger.debug(`✓ Claude Code CLI found: ${claudeCodePath}`);
203
- }
204
- }
205
- catch {
206
- throw new Error(`Claude Code executable not found at: ${claudeCodePath}\n` +
207
- "For compiled binaries, ensure claude-code-cli.js is in the same directory as the binary.\n" +
208
- "Set CLAUDE_CODE_EXECUTABLE_PATH environment variable to override.");
209
- }
210
- }
211
- let resultText = "";
212
- let hasError = false;
213
- // Log SDK configuration for debugging
214
- if (config.verbose) {
215
- logger.debug(`\nSDK Configuration:`);
216
- logger.debug(` Prompt: ${prompt.substring(0, 100)}${prompt.length > 100 ? '...' : ''}`);
217
- logger.debug(` Max turns: ${config.maxIterations}`);
218
- logger.debug(` Working directory: ${process.cwd()}`);
219
- logger.debug(` Model: ${envConfig.anthropicModelName}`);
220
- logger.debug(` Claude Code executable: ${claudeCodePath}`);
221
- logger.debug(` Supatest API Key: ${config.supatestApiKey?.substring(0, 10)}...`);
222
- logger.debug(` Environment ANTHROPIC_API_KEY: ${process.env.ANTHROPIC_API_KEY ? 'set' : 'not set'}`);
223
- }
224
- // Stream initial user message and capture assistant message ID for usage tracking
225
- let assistantMessageId;
226
- if (eventStreamer && sessionId) {
227
- const userMessageEvent = {
228
- type: "user_message",
229
- content: [{ type: "text", text: prompt }],
230
- };
231
- const eventResponse = await apiClient.streamEvent(sessionId, userMessageEvent);
232
- assistantMessageId = eventResponse.assistantMessageId;
233
- if (assistantMessageId && config.verbose) {
234
- logger.debug(`Assistant message ID for tracking: ${assistantMessageId}`);
235
- }
236
- }
237
- // Run the agent using the SDK
238
- // Note: The proxy will automatically look up the message ID via Redis using the API key
239
- const queryOptions = {
240
- maxTurns: config.maxIterations,
241
- cwd: process.cwd(),
242
- model: envConfig.anthropicModelName,
243
- permissionMode: "bypassPermissions",
244
- allowDangerouslySkipPermissions: true,
245
- pathToClaudeCodeExecutable: claudeCodePath,
246
- // Enable streaming delta events for real-time updates
247
- includePartialMessages: true,
248
- // Force Node.js runtime even when running from a Bun binary
249
- // The claude-code-cli.js is a large minified JS file that Bun can't parse correctly
250
- executable: 'node',
251
- // Explicitly pass environment variables to the subprocess
252
- env: {
253
- ...process.env,
254
- ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || '',
255
- ANTHROPIC_BASE_URL: process.env.ANTHROPIC_BASE_URL || '',
256
- ANTHROPIC_AUTH_TOKEN: '', // Clear stored OAuth token
257
- CLAUDE_CODE_AUTH_TOKEN: '', // Clear any other auth tokens
258
- },
259
- stderr: (msg) => {
260
- claudeCodeStderr += msg + "\n";
261
- if (config.verbose) {
262
- logger.debug(`[Claude Code stderr] ${msg}`);
263
- }
264
- },
265
- };
266
- // Start initial spinner while waiting for first assistant message (skip in silent mode)
267
- if (!logger.isSilent()) {
268
- const message = getRandomSpinnerMessage();
269
- spinner = ora({
270
- spinner: {
271
- interval: 80,
272
- frames: createShimmerFrames(message),
273
- }
274
- });
275
- spinner.start();
276
- }
277
- for await (const msg of query({
278
- prompt,
279
- options: queryOptions,
280
- })) {
281
- if (config.verbose) {
282
- logger.debug(`Received SDK message: ${msg.type}`);
283
- }
284
- // Handle different message types
285
- if (msg.type === "assistant") {
286
- stats.iterations++;
287
- if (spinner) {
288
- spinner.stop();
289
- }
290
- // Extract text content and tool uses
291
- const content = msg.message.content;
292
- if (Array.isArray(content)) {
293
- for (const block of content) {
294
- if (block.type === "text") {
295
- logger.raw(block.text);
296
- resultText += block.text + "\n";
297
- // WORKAROUND: Since SDK doesn't emit stream_event with includePartialMessages,
298
- // send the complete text as an assistant_text event immediately
299
- if (eventStreamer && block.text) {
300
- const textEvent = {
301
- type: "assistant_text",
302
- delta: block.text,
303
- };
304
- await eventStreamer.queueEvent(textEvent);
305
- }
306
- }
307
- else if (block.type === "thinking") {
308
- // Send thinking blocks as well
309
- if (eventStreamer && block.thinking) {
310
- const thinkingEvent = {
311
- type: "assistant_thinking",
312
- delta: block.thinking,
313
- };
314
- await eventStreamer.queueEvent(thinkingEvent);
315
- }
316
- }
317
- else if (block.type === "tool_use") {
318
- const toolName = block.name;
319
- const input = block.input;
320
- // Stream tool use event
321
- if (eventStreamer) {
322
- const toolUseEvent = {
323
- type: "tool_use",
324
- id: block.id,
325
- name: toolName,
326
- input: input || {},
327
- };
328
- await eventStreamer.queueEvent(toolUseEvent);
329
- }
330
- // Display tool calls to user
331
- if (toolName === "Read") {
332
- const filePath = input?.file_path || 'file';
333
- logger.toolRead(filePath);
334
- }
335
- else if (toolName === "Write") {
336
- const filePath = input?.file_path;
337
- if (filePath) {
338
- stats.filesModified.add(filePath);
339
- logger.toolWrite(filePath);
340
- }
341
- }
342
- else if (toolName === "Edit") {
343
- const filePath = input?.file_path;
344
- if (filePath) {
345
- stats.filesModified.add(filePath);
346
- logger.toolEdit(filePath);
347
- }
348
- }
349
- else if (toolName === "Bash") {
350
- const command = input?.command;
351
- if (command) {
352
- stats.commandsRun.push(command);
353
- const shortCmd = command.length > 60 ? `${command.substring(0, 60)}...` : command;
354
- logger.toolBash(shortCmd);
355
- }
356
- }
357
- else if (toolName === "Glob") {
358
- logger.toolSearch("files", input?.pattern || '');
359
- }
360
- else if (toolName === "Grep") {
361
- logger.toolSearch("code", input?.pattern || '');
362
- }
363
- else if (toolName === "Task") {
364
- logger.toolAgent(input?.subagent_type || 'task');
365
- }
366
- else if (toolName === "TodoWrite") {
367
- const todos = input?.todos;
368
- if (Array.isArray(todos)) {
369
- logger.todoUpdate(todos);
370
- }
371
- else {
372
- logger.info("📝 Updated todos");
373
- }
374
- }
375
- }
376
- }
377
- }
378
- // Stream message_complete event with full content
379
- if (eventStreamer) {
380
- // Flush any pending delta events first to ensure they arrive before message_complete
381
- await eventStreamer.flush();
382
- const messageCompleteEvent = {
383
- type: "message_complete",
384
- message: {
385
- role: "assistant",
386
- content: content, // Cast to avoid type mismatch between SDK and shared types
387
- },
388
- };
389
- await eventStreamer.queueEvent(messageCompleteEvent);
390
- }
391
- logger.raw("");
392
- // Stop and clear previous spinner if it exists
393
- if (spinner) {
394
- spinner.stop();
395
- spinner.clear();
396
- }
397
- // Create a new spinner instance with a random message (skip in silent mode)
398
- if (!logger.isSilent()) {
399
- const message = getRandomSpinnerMessage();
400
- spinner = ora({
401
- spinner: {
402
- interval: 80,
403
- frames: createShimmerFrames(message),
404
- }
405
- });
406
- spinner.start();
407
- }
408
- }
409
- else if (msg.type === "stream_event") {
410
- // NOTE: This code path is currently not triggered due to an SDK issue with includePartialMessages
411
- // We've implemented a workaround above to send text immediately when assistant messages arrive
412
- // Keeping this code in case future SDK versions fix the streaming support
413
- const event = msg.event;
414
- if (event.type === "content_block_delta") {
415
- const delta = event.delta;
416
- if (delta.type === "text_delta" && eventStreamer) {
417
- const textDeltaEvent = {
418
- type: "assistant_text",
419
- delta: delta.text,
420
- };
421
- await eventStreamer.queueEvent(textDeltaEvent);
422
- }
423
- else if (delta.type === "thinking_delta" && eventStreamer) {
424
- const thinkingDeltaEvent = {
425
- type: "assistant_thinking",
426
- delta: delta.thinking,
427
- };
428
- await eventStreamer.queueEvent(thinkingDeltaEvent);
429
- }
430
- }
431
- }
432
- else if (msg.type === "tool_progress") {
433
- spinner.text = `Using ${msg.tool_name}... (${msg.elapsed_time_seconds.toFixed(1)}s)`;
434
- }
435
- else if (msg.type === "result") {
436
- spinner.stop();
437
- stats.iterations = msg.num_turns;
438
- if (msg.subtype === "success") {
439
- resultText = msg.result || resultText;
440
- }
441
- else {
442
- hasError = true;
443
- if ("errors" in msg && Array.isArray(msg.errors)) {
444
- stats.errors.push(...msg.errors);
445
- for (const error of msg.errors) {
446
- logger.error(error);
447
- }
448
- }
449
- }
450
- }
451
- }
452
- if (spinner) {
453
- spinner.stop();
454
- }
455
- stats.endTime = Date.now();
456
- // Complete usage tracking for this message turn
457
- if (assistantMessageId && apiClient) {
458
- try {
459
- await apiClient.completeUsage(assistantMessageId);
460
- }
461
- catch (error) {
462
- logger.warn(`Failed to complete usage tracking: ${error.message}`);
463
- // Don't fail the task if usage tracking fails
464
- }
465
- }
466
- // Generate result
467
- const result = {
468
- success: !hasError && stats.errors.length === 0,
469
- summary: resultText || "Task completed",
470
- filesModified: Array.from(stats.filesModified),
471
- iterations: stats.iterations,
472
- error: stats.errors.length > 0 ? stats.errors.join("; ") : undefined,
473
- };
474
- // Stream session completion or error
475
- if (eventStreamer) {
476
- if (result.success) {
477
- await eventStreamer.queueEvent({ type: "session_complete" });
478
- }
479
- else {
480
- await eventStreamer.queueEvent({
481
- type: "session_error",
482
- error: result.error || "Unknown error",
483
- });
484
- }
485
- // Flush and shutdown event streamer
486
- await eventStreamer.shutdown();
487
- }
488
- // Print summary
489
- const summaryText = generateSummary(stats, result, config.verbose);
490
- logger.raw(summaryText);
491
- // Display web URL again at the end if available
492
- if (webUrl) {
493
- logger.raw("");
494
- logger.divider();
495
- logger.raw(chalk.white.bold("Continue on web: ") +
496
- chalk.cyan.underline(webUrl));
497
- logger.divider();
498
- }
499
- return result;
500
- }
501
- catch (error) {
502
- if (spinner) {
503
- spinner.stop();
504
- }
505
- stats.endTime = Date.now();
506
- const errorMessage = error instanceof Error ? error.message : String(error);
507
- // Check if this is a Claude Code process error and extract details
508
- const isProcessError = errorMessage.includes("Claude Code process exited");
509
- let exitCode;
510
- let stderr;
511
- let stdout;
512
- // Try to extract exit code from error message (e.g., "exited with code 1")
513
- const exitCodeMatch = errorMessage.match(/exited with code (\d+)/i);
514
- if (exitCodeMatch) {
515
- exitCode = Number.parseInt(exitCodeMatch[1], 10);
516
- }
517
- if (error && typeof error === "object") {
518
- // Try to extract exit code and process output from error object
519
- if ("exitCode" in error && typeof error.exitCode === "number") {
520
- exitCode = error.exitCode;
521
- }
522
- if ("code" in error && typeof error.code === "number") {
523
- exitCode = error.code;
524
- }
525
- if ("stderr" in error) {
526
- stderr = String(error.stderr);
527
- }
528
- if ("stdout" in error) {
529
- stdout = String(error.stdout);
530
- }
531
- }
532
- logger.error(`Fatal error: ${errorMessage}`);
533
- // Show captured stderr if available
534
- if (claudeCodeStderr && claudeCodeStderr.trim()) {
535
- logger.error("\nClaude Code stderr output:");
536
- logger.error(claudeCodeStderr);
537
- }
538
- if (isProcessError) {
539
- if (exitCode !== undefined) {
540
- logger.error(`Claude Code process exited with code ${exitCode}`);
541
- }
542
- else {
543
- logger.error("Claude Code process exited with an error");
544
- }
545
- // Provide helpful guidance for process errors
546
- // Show guidance for exit code 1 or if exit code is unknown
547
- if (exitCode === 1 || exitCode === undefined) {
548
- logger.warn("\nPossible causes:");
549
- logger.warn(" • Task may not be actionable (Claude Code works with code-related tasks)");
550
- logger.warn(" • Task description may be too vague or not code-focused");
551
- logger.warn(" • API authentication or rate limiting issue");
552
- logger.warn("\nTry tasks like:");
553
- logger.warn(" • 'Fix the failing tests in calculator.test.js'");
554
- logger.warn(" • 'Add error handling to the divide function'");
555
- logger.warn(" • 'Create a new file hello.js with a greeting function'");
556
- logger.warn(" • 'Update imports to use the new package structure'");
557
- }
558
- }
559
- if (config.verbose && error instanceof Error && error.stack) {
560
- logger.error(error.stack);
561
- }
562
- const result = {
563
- success: false,
564
- summary: `Failed: ${errorMessage}`,
565
- filesModified: Array.from(stats.filesModified),
566
- iterations: stats.iterations,
567
- error: errorMessage,
568
- };
569
- // Stream session error and shutdown event streamer
570
- if (eventStreamer) {
571
- await eventStreamer.queueEvent({
572
- type: "session_error",
573
- error: errorMessage,
574
- });
575
- await eventStreamer.shutdown();
576
- }
577
- const summaryText = generateSummary(stats, result, config.verbose);
578
- logger.raw(summaryText);
579
- // Display web URL at the end if available
580
- if (webUrl) {
581
- logger.raw("");
582
- logger.divider();
583
- logger.raw(chalk.white.bold("View session: ") +
584
- chalk.cyan.underline(webUrl));
585
- logger.divider();
586
- }
587
- return result;
588
- }
589
- }