@supatest/cli 0.0.2 → 0.0.3

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 (75) hide show
  1. package/README.md +58 -315
  2. package/dist/agent-runner.js +224 -52
  3. package/dist/commands/login.js +392 -0
  4. package/dist/commands/setup.js +234 -0
  5. package/dist/config.js +29 -0
  6. package/dist/core/agent.js +270 -0
  7. package/dist/index.js +118 -31
  8. package/dist/modes/headless.js +117 -0
  9. package/dist/modes/interactive.js +430 -0
  10. package/dist/presenters/composite.js +32 -0
  11. package/dist/presenters/console.js +163 -0
  12. package/dist/presenters/react.js +220 -0
  13. package/dist/presenters/types.js +1 -0
  14. package/dist/presenters/web.js +78 -0
  15. package/dist/prompts/builder.js +181 -0
  16. package/dist/prompts/fixer.js +148 -0
  17. package/dist/prompts/headless.md +97 -0
  18. package/dist/prompts/index.js +3 -0
  19. package/dist/prompts/interactive.md +43 -0
  20. package/dist/prompts/plan.md +41 -0
  21. package/dist/prompts/planner.js +70 -0
  22. package/dist/prompts/prompts/builder.md +97 -0
  23. package/dist/prompts/prompts/fixer.md +100 -0
  24. package/dist/prompts/prompts/plan.md +41 -0
  25. package/dist/prompts/prompts/planner.md +41 -0
  26. package/dist/services/api-client.js +244 -0
  27. package/dist/services/event-streamer.js +130 -0
  28. package/dist/ui/App.js +322 -0
  29. package/dist/ui/components/AuthBanner.js +20 -0
  30. package/dist/ui/components/AuthDialog.js +32 -0
  31. package/dist/ui/components/Banner.js +12 -0
  32. package/dist/ui/components/ExpandableSection.js +17 -0
  33. package/dist/ui/components/Header.js +49 -0
  34. package/dist/ui/components/HelpMenu.js +89 -0
  35. package/dist/ui/components/InputPrompt.js +292 -0
  36. package/dist/ui/components/MessageList.js +42 -0
  37. package/dist/ui/components/QueuedMessageDisplay.js +31 -0
  38. package/dist/ui/components/Scrollable.js +103 -0
  39. package/dist/ui/components/SessionSelector.js +196 -0
  40. package/dist/ui/components/StatusBar.js +45 -0
  41. package/dist/ui/components/messages/AssistantMessage.js +20 -0
  42. package/dist/ui/components/messages/ErrorMessage.js +26 -0
  43. package/dist/ui/components/messages/LoadingMessage.js +28 -0
  44. package/dist/ui/components/messages/ThinkingMessage.js +17 -0
  45. package/dist/ui/components/messages/TodoMessage.js +44 -0
  46. package/dist/ui/components/messages/ToolMessage.js +218 -0
  47. package/dist/ui/components/messages/UserMessage.js +14 -0
  48. package/dist/ui/contexts/KeypressContext.js +527 -0
  49. package/dist/ui/contexts/MouseContext.js +98 -0
  50. package/dist/ui/contexts/SessionContext.js +131 -0
  51. package/dist/ui/hooks/useAnimatedScrollbar.js +83 -0
  52. package/dist/ui/hooks/useBatchedScroll.js +22 -0
  53. package/dist/ui/hooks/useBracketedPaste.js +31 -0
  54. package/dist/ui/hooks/useFocus.js +50 -0
  55. package/dist/ui/hooks/useKeypress.js +26 -0
  56. package/dist/ui/hooks/useModeToggle.js +25 -0
  57. package/dist/ui/types/auth.js +13 -0
  58. package/dist/ui/utils/file-completion.js +56 -0
  59. package/dist/ui/utils/input.js +50 -0
  60. package/dist/ui/utils/markdown.js +376 -0
  61. package/dist/ui/utils/mouse.js +189 -0
  62. package/dist/ui/utils/theme.js +59 -0
  63. package/dist/utils/banner.js +7 -14
  64. package/dist/utils/encryption.js +71 -0
  65. package/dist/utils/events.js +36 -0
  66. package/dist/utils/keychain-storage.js +120 -0
  67. package/dist/utils/logger.js +103 -1
  68. package/dist/utils/node-version.js +1 -3
  69. package/dist/utils/plan-file.js +75 -0
  70. package/dist/utils/project-instructions.js +23 -0
  71. package/dist/utils/rich-logger.js +1 -1
  72. package/dist/utils/stdio.js +80 -0
  73. package/dist/utils/summary.js +1 -5
  74. package/dist/utils/token-storage.js +242 -0
  75. package/package.json +35 -15
@@ -3,8 +3,11 @@ import { dirname, join } from "node:path";
3
3
  import { query } from "@anthropic-ai/claude-agent-sdk";
4
4
  import chalk from "chalk";
5
5
  import ora from "ora";
6
- import { logger } from "./utils/logger.js";
7
- import { generateSummary } from "./utils/summary.js";
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";
8
11
  const CLI_VERSION = "0.0.1";
9
12
  // Fun spinner messages that rotate randomly
10
13
  const SPINNER_MESSAGES = [
@@ -46,7 +49,7 @@ export async function runAgent(config) {
46
49
  let claudeCodeStderr = "";
47
50
  logger.setVerbose(config.verbose);
48
51
  // Display metadata
49
- console.log("");
52
+ logger.raw("");
50
53
  // Get git branch if available
51
54
  let gitBranch = "";
52
55
  try {
@@ -61,16 +64,16 @@ export async function runAgent(config) {
61
64
  }
62
65
  const metadataParts = [
63
66
  chalk.dim("Supatest AI ") + chalk.cyan(`v${CLI_VERSION}`),
64
- chalk.dim("Model: ") + chalk.cyan(process.env.ANTHROPIC_MODEL_NAME || "claude-sonnet-4-5"),
67
+ chalk.dim("Model: ") + chalk.cyan(envConfig.anthropicModelName),
65
68
  ];
66
69
  if (gitBranch) {
67
70
  metadataParts.push(chalk.dim("Branch: ") + chalk.cyan(gitBranch));
68
71
  }
69
- console.log(metadataParts.join(chalk.dim(" • ")));
70
- console.log(chalk.gray("─".repeat(60)));
72
+ logger.raw(metadataParts.join(chalk.dim(" • ")));
73
+ logger.divider();
71
74
  // Show environment info in verbose mode
72
75
  if (config.verbose) {
73
- console.log("");
76
+ logger.raw("");
74
77
  logger.debug("Environment & System Info:");
75
78
  // Node.js version
76
79
  logger.debug(` Node.js: ${process.version}`);
@@ -103,15 +106,39 @@ export async function runAgent(config) {
103
106
  logger.debug(` Available Disk: unable to determine`);
104
107
  }
105
108
  }
106
- console.log("");
107
- console.log("");
108
- console.log(chalk.white.bold("Task:"), chalk.cyan(config.task));
109
+ logger.raw("");
110
+ logger.raw("");
111
+ logger.raw(chalk.white.bold("Task:") + " " + chalk.cyan(config.task));
109
112
  if (config.logs) {
110
113
  logger.info("Processing provided logs...");
111
114
  }
112
- console.log("");
113
- console.log("");
114
- console.log("");
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("");
115
142
  // Initialize spinner variable (will be created on first agent turn)
116
143
  let spinner = null;
117
144
  // Resolve path to Claude Code executable
@@ -122,11 +149,26 @@ export async function runAgent(config) {
122
149
  if (config.logs) {
123
150
  prompt = `${config.task}\n\nHere are the logs to analyze:\n\`\`\`\n${config.logs}\n\`\`\``;
124
151
  }
125
- // Set API key
126
- process.env.ANTHROPIC_API_KEY = config.apiKey;
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
+ }
127
169
  // Allow override via environment variable for testing/debugging
128
- if (process.env.CLAUDE_CODE_EXECUTABLE_PATH) {
129
- claudeCodePath = process.env.CLAUDE_CODE_EXECUTABLE_PATH;
170
+ if (envConfig.claudeCodeExecutablePath) {
171
+ claudeCodePath = envConfig.claudeCodeExecutablePath;
130
172
  if (config.verbose) {
131
173
  logger.debug(`Using CLAUDE_CODE_EXECUTABLE_PATH: ${claudeCodePath}`);
132
174
  }
@@ -174,22 +216,46 @@ export async function runAgent(config) {
174
216
  logger.debug(` Prompt: ${prompt.substring(0, 100)}${prompt.length > 100 ? '...' : ''}`);
175
217
  logger.debug(` Max turns: ${config.maxIterations}`);
176
218
  logger.debug(` Working directory: ${process.cwd()}`);
177
- logger.debug(` Model: ${process.env.ANTHROPIC_MODEL_NAME || "claude-sonnet-4-5"}`);
219
+ logger.debug(` Model: ${envConfig.anthropicModelName}`);
178
220
  logger.debug(` Claude Code executable: ${claudeCodePath}`);
179
- logger.debug(` API Key: ${config.apiKey ? `${config.apiKey.substring(0, 10)}...` : 'not set'}`);
221
+ logger.debug(` Supatest API Key: ${config.supatestApiKey?.substring(0, 10)}...`);
180
222
  logger.debug(` Environment ANTHROPIC_API_KEY: ${process.env.ANTHROPIC_API_KEY ? 'set' : 'not set'}`);
181
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
+ }
182
237
  // Run the agent using the SDK
238
+ // Note: The proxy will automatically look up the message ID via Redis using the API key
183
239
  const queryOptions = {
184
240
  maxTurns: config.maxIterations,
185
241
  cwd: process.cwd(),
186
- model: process.env.ANTHROPIC_MODEL_NAME || "claude-sonnet-4-5",
242
+ model: envConfig.anthropicModelName,
187
243
  permissionMode: "bypassPermissions",
188
244
  allowDangerouslySkipPermissions: true,
189
245
  pathToClaudeCodeExecutable: claudeCodePath,
246
+ // Enable streaming delta events for real-time updates
247
+ includePartialMessages: true,
190
248
  // Force Node.js runtime even when running from a Bun binary
191
249
  // The claude-code-cli.js is a large minified JS file that Bun can't parse correctly
192
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
+ },
193
259
  stderr: (msg) => {
194
260
  claudeCodeStderr += msg + "\n";
195
261
  if (config.verbose) {
@@ -197,14 +263,24 @@ export async function runAgent(config) {
197
263
  }
198
264
  },
199
265
  };
200
- if (config.verbose) {
201
- logger.debug("\nQuery options:");
202
- logger.debug(JSON.stringify(queryOptions, null, 2));
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();
203
276
  }
204
277
  for await (const msg of query({
205
278
  prompt,
206
279
  options: queryOptions,
207
280
  })) {
281
+ if (config.verbose) {
282
+ logger.debug(`Received SDK message: ${msg.type}`);
283
+ }
208
284
  // Handle different message types
209
285
  if (msg.type === "assistant") {
210
286
  stats.iterations++;
@@ -216,12 +292,41 @@ export async function runAgent(config) {
216
292
  if (Array.isArray(content)) {
217
293
  for (const block of content) {
218
294
  if (block.type === "text") {
219
- console.log(block.text);
295
+ logger.raw(block.text);
220
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
+ }
221
316
  }
222
317
  else if (block.type === "tool_use") {
223
318
  const toolName = block.name;
224
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
+ }
225
330
  // Display tool calls to user
226
331
  if (toolName === "Read") {
227
332
  const filePath = input?.file_path || 'file';
@@ -267,37 +372,60 @@ export async function runAgent(config) {
267
372
  logger.info("📝 Updated todos");
268
373
  }
269
374
  }
270
- else {
271
- logger.debug(`🔧 Using tool: ${toolName}`);
272
- }
273
- if (config.verbose) {
274
- logger.debug(` Input: ${JSON.stringify(input).substring(0, 100)}`);
275
- }
276
375
  }
277
376
  }
278
377
  }
279
- console.log("");
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("");
280
392
  // Stop and clear previous spinner if it exists
281
393
  if (spinner) {
282
394
  spinner.stop();
283
395
  spinner.clear();
284
396
  }
285
- // Create a new spinner instance with a random message
286
- const message = getRandomSpinnerMessage();
287
- spinner = ora({
288
- spinner: {
289
- interval: 80,
290
- frames: createShimmerFrames(message),
291
- }
292
- });
293
- spinner.start();
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
+ }
294
408
  }
295
409
  else if (msg.type === "stream_event") {
296
- // Handle streaming events
297
- if (config.verbose) {
298
- const event = msg.event;
299
- if (event.type === "content_block_start") {
300
- logger.debug("Content block started");
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);
301
429
  }
302
430
  }
303
431
  }
@@ -318,10 +446,6 @@ export async function runAgent(config) {
318
446
  logger.error(error);
319
447
  }
320
448
  }
321
- if (config.verbose) {
322
- logger.debug("Result message details:");
323
- logger.debug(JSON.stringify(msg, null, 2));
324
- }
325
449
  }
326
450
  }
327
451
  }
@@ -329,6 +453,16 @@ export async function runAgent(config) {
329
453
  spinner.stop();
330
454
  }
331
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
+ }
332
466
  // Generate result
333
467
  const result = {
334
468
  success: !hasError && stats.errors.length === 0,
@@ -337,9 +471,31 @@ export async function runAgent(config) {
337
471
  iterations: stats.iterations,
338
472
  error: stats.errors.length > 0 ? stats.errors.join("; ") : undefined,
339
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
+ }
340
488
  // Print summary
341
489
  const summaryText = generateSummary(stats, result, config.verbose);
342
- console.log(summaryText);
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
+ }
343
499
  return result;
344
500
  }
345
501
  catch (error) {
@@ -401,7 +557,7 @@ export async function runAgent(config) {
401
557
  }
402
558
  }
403
559
  if (config.verbose && error instanceof Error && error.stack) {
404
- console.error(error.stack);
560
+ logger.error(error.stack);
405
561
  }
406
562
  const result = {
407
563
  success: false,
@@ -410,8 +566,24 @@ export async function runAgent(config) {
410
566
  iterations: stats.iterations,
411
567
  error: errorMessage,
412
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
+ }
413
577
  const summaryText = generateSummary(stats, result, config.verbose);
414
- console.log(summaryText);
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
+ }
415
587
  return result;
416
588
  }
417
589
  }