@workermill/agent 0.7.8 โ†’ 0.7.9

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 (2) hide show
  1. package/dist/planner.js +92 -21
  2. package/package.json +1 -1
package/dist/planner.js CHANGED
@@ -15,6 +15,7 @@
15
15
  * sees the same planning progress as cloud mode.
16
16
  */
17
17
  import chalk from "chalk";
18
+ import ora from "ora";
18
19
  import { spawn, execSync } from "child_process";
19
20
  import { findClaudePath } from "./config.js";
20
21
  import { api } from "./api.js";
@@ -103,6 +104,23 @@ function runClaudeCli(claudePath, model, prompt, env, taskId, startTime) {
103
104
  let stderrOutput = "";
104
105
  let charsReceived = 0;
105
106
  let toolCallCount = 0;
107
+ // Live spinner โ€” shows elapsed time, phase, and chars generated
108
+ const spinner = ora({
109
+ text: `${taskLabel} Initializing planner...`,
110
+ prefixText: "",
111
+ spinner: "dots",
112
+ }).start();
113
+ function updateSpinner() {
114
+ const elapsed = Math.round((Date.now() - startTime) / 1000);
115
+ const phaseIcon = currentPhase === "reading_repo" ? "๐Ÿ“‚" :
116
+ currentPhase === "analyzing" ? "๐Ÿ”" :
117
+ currentPhase === "generating_plan" ? "๐Ÿ“" :
118
+ currentPhase === "validating" ? "โœ…" : "โณ";
119
+ const stats = chalk.dim(`${formatElapsed(elapsed)} ยท ${charsReceived} chars ยท ${toolCallCount} tools`);
120
+ spinner.text = `${taskLabel} ${phaseIcon} ${phaseLabel(currentPhase, elapsed)} ${stats}`;
121
+ }
122
+ // Update spinner every 500ms for smooth elapsed time display
123
+ const spinnerInterval = setInterval(updateSpinner, 500);
106
124
  // Buffered text streaming โ€” flush complete lines to dashboard every 1s.
107
125
  // LLM deltas are tiny fragments; we accumulate until we see '\n', then
108
126
  // a 1s interval flushes all complete lines as log entries. On exit we
@@ -117,6 +135,11 @@ function runClaudeCli(claudePath, model, prompt, env, taskId, startTime) {
117
135
  for (const line of parts) {
118
136
  if (line.trim()) {
119
137
  postLog(taskId, `${PREFIX} ${line}`, "output");
138
+ // Echo planner thoughts to local terminal
139
+ spinner.stop();
140
+ const truncated = line.trim().length > 160 ? line.trim().substring(0, 160) + "โ€ฆ" : line.trim();
141
+ console.log(`${ts()} ${taskLabel} ${chalk.dim("๐Ÿ’ญ")} ${chalk.dim(truncated)}`);
142
+ spinner.start();
120
143
  }
121
144
  }
122
145
  textBuffer = incomplete;
@@ -133,7 +156,10 @@ function runClaudeCli(claudePath, model, prompt, env, taskId, startTime) {
133
156
  const elapsed = Math.round((Date.now() - startTime) / 1000);
134
157
  const msg = phaseLabel(newPhase, elapsed);
135
158
  postLog(taskId, msg);
159
+ spinner.stop();
136
160
  console.log(`${ts()} ${taskLabel} ${chalk.dim(msg)}`);
161
+ spinner.start();
162
+ updateSpinner();
137
163
  }
138
164
  // Flush buffered LLM text to dashboard every 1s (complete lines only)
139
165
  const textFlushInterval = setInterval(() => flushTextBuffer(), 1_000);
@@ -159,7 +185,9 @@ function runClaudeCli(claudePath, model, prompt, env, taskId, startTime) {
159
185
  lastProgressLogAt = elapsed;
160
186
  const msg = `${PREFIX} Planning in progress โ€” analyzing requirements and decomposing into steps (${formatElapsed(elapsed)} elapsed)`;
161
187
  postLog(taskId, msg);
188
+ spinner.stop();
162
189
  console.log(`${ts()} ${taskLabel} ${chalk.dim(msg)}`);
190
+ spinner.start();
163
191
  }
164
192
  }, 5_000);
165
193
  // Parse streaming JSON lines from Claude CLI
@@ -248,20 +276,22 @@ function runClaudeCli(claudePath, model, prompt, env, taskId, startTime) {
248
276
  proc.stderr.on("data", (chunk) => {
249
277
  stderrOutput += chunk.toString();
250
278
  });
251
- const timeout = setTimeout(() => {
279
+ function cleanupAll() {
252
280
  clearInterval(progressInterval);
253
281
  clearInterval(sseProgressInterval);
254
282
  clearInterval(textFlushInterval);
283
+ clearInterval(spinnerInterval);
255
284
  flushTextBuffer(true);
285
+ spinner.stop();
286
+ }
287
+ const timeout = setTimeout(() => {
288
+ cleanupAll();
256
289
  proc.kill("SIGTERM");
257
290
  reject(new Error("Claude CLI timed out after 20 minutes"));
258
291
  }, 1_200_000);
259
292
  proc.on("exit", (code) => {
260
293
  clearTimeout(timeout);
261
- clearInterval(progressInterval);
262
- clearInterval(sseProgressInterval);
263
- clearInterval(textFlushInterval);
264
- flushTextBuffer(true);
294
+ cleanupAll();
265
295
  // Emit final "validating" phase to dashboard
266
296
  const elapsedAtClose = Math.round((Date.now() - startTime) / 1000);
267
297
  postProgress(taskId, "validating", elapsedAtClose, "Validating plan...", charsReceived, toolCallCount);
@@ -275,10 +305,7 @@ function runClaudeCli(claudePath, model, prompt, env, taskId, startTime) {
275
305
  });
276
306
  proc.on("error", (err) => {
277
307
  clearTimeout(timeout);
278
- clearInterval(progressInterval);
279
- clearInterval(sseProgressInterval);
280
- clearInterval(textFlushInterval);
281
- flushTextBuffer(true);
308
+ cleanupAll();
282
309
  reject(err);
283
310
  });
284
311
  });
@@ -351,10 +378,14 @@ async function cloneTargetRepo(repo, token, scmProvider, taskId) {
351
378
  * Run an analyst agent via Claude CLI with tool access to the cloned repo.
352
379
  * Returns the analyst's report text, or an empty string on failure.
353
380
  */
354
- function runAnalyst(name, claudePath, model, prompt, repoPath, env, timeoutMs = 900_000) {
381
+ function runAnalyst(name, claudePath, model, prompt, repoPath, env, timeoutMs = 900_000, taskId) {
355
382
  const label = chalk.blue(`[${name}]`);
383
+ const modelLabel = chalk.yellow(model);
356
384
  return new Promise((resolve) => {
357
- console.log(`${ts()} ${label} Starting (${chalk.dim(model)})...`);
385
+ console.log(`${ts()} ${label} Starting analyst using ${modelLabel}...`);
386
+ if (taskId) {
387
+ postLog(taskId, `${PREFIX} [${name}] Starting analyst using ${model}...`);
388
+ }
358
389
  const proc = spawn(claudePath, [
359
390
  "--print",
360
391
  "--verbose",
@@ -376,12 +407,23 @@ function runAnalyst(name, claudePath, model, prompt, repoPath, env, timeoutMs =
376
407
  let toolCalls = 0;
377
408
  let timedOut = false;
378
409
  const startMs = Date.now();
410
+ // Live spinner for this analyst
411
+ const analystSpinner = ora({
412
+ text: `${label} Starting (${model})...`,
413
+ spinner: "dots",
414
+ }).start();
415
+ const analystSpinnerInterval = setInterval(() => {
416
+ const elapsed = Math.round((Date.now() - startMs) / 1000);
417
+ analystSpinner.text = `${label} ${chalk.dim(`${formatElapsed(elapsed)} ยท ${toolCalls} tools ยท ${fullText.length} chars`)}`;
418
+ }, 500);
379
419
  proc.stderr.on("data", (chunk) => {
380
420
  const text = chunk.toString();
381
421
  stderrOutput += text;
382
422
  // Show stderr in real-time so we can see what's happening
383
423
  for (const line of text.split("\n").filter((l) => l.trim())) {
424
+ analystSpinner.stop();
384
425
  console.log(`${ts()} ${label} ${chalk.red("stderr:")} ${line.trim()}`);
426
+ analystSpinner.start();
385
427
  }
386
428
  });
387
429
  proc.stdout.on("data", (data) => {
@@ -404,7 +446,11 @@ function runAnalyst(name, claudePath, model, prompt, repoPath, env, timeoutMs =
404
446
  // Log analyst reasoning (first line, truncated)
405
447
  const thought = block.text.trim().split("\n")[0].substring(0, 120);
406
448
  if (thought) {
449
+ analystSpinner.stop();
407
450
  console.log(`${ts()} ${label} ${chalk.dim("๐Ÿ’ญ")} ${chalk.dim(thought)}`);
451
+ if (taskId)
452
+ postLog(taskId, `${PREFIX} [${name}] ๐Ÿ’ญ ${thought}`);
453
+ analystSpinner.start();
408
454
  }
409
455
  }
410
456
  else if (block.type === "tool_use") {
@@ -413,7 +459,11 @@ function runAnalyst(name, claudePath, model, prompt, repoPath, env, timeoutMs =
413
459
  // Show tool name + input preview (file path, pattern, etc.)
414
460
  const inputStr = block.input ? JSON.stringify(block.input) : "";
415
461
  const inputPreview = inputStr.length > 80 ? inputStr.substring(0, 80) + "โ€ฆ" : inputStr;
416
- console.log(`${ts()} ${label} ${chalk.dim(`Tool: ${toolName}`)}${inputPreview ? chalk.dim(` ${inputPreview}`) : ""} (${toolCalls} total)`);
462
+ analystSpinner.stop();
463
+ console.log(`${ts()} ${label} ${chalk.dim(`Tool: ${toolName}`)}${inputPreview ? chalk.dim(` ${inputPreview}`) : ""}`);
464
+ if (taskId)
465
+ postLog(taskId, `${PREFIX} [${name}] Tool: ${toolName} ${inputPreview}`);
466
+ analystSpinner.start();
417
467
  }
418
468
  }
419
469
  }
@@ -428,7 +478,11 @@ function runAnalyst(name, claudePath, model, prompt, repoPath, env, timeoutMs =
428
478
  else if (event.type === "content_block_start" && event.content_block?.type === "tool_use") {
429
479
  toolCalls++;
430
480
  const toolName = event.content_block?.name || "unknown";
431
- console.log(`${ts()} ${label} ${chalk.dim(`Tool: ${toolName}`)} (${toolCalls} total)`);
481
+ analystSpinner.stop();
482
+ console.log(`${ts()} ${label} ${chalk.dim(`Tool: ${toolName}`)}`);
483
+ if (taskId)
484
+ postLog(taskId, `${PREFIX} [${name}] Tool: ${toolName}`);
485
+ analystSpinner.start();
432
486
  }
433
487
  else if (event.type === "result" && event.result) {
434
488
  resultText =
@@ -442,30 +496,44 @@ function runAnalyst(name, claudePath, model, prompt, repoPath, env, timeoutMs =
442
496
  });
443
497
  const timeout = setTimeout(() => {
444
498
  timedOut = true;
499
+ clearInterval(analystSpinnerInterval);
500
+ analystSpinner.stop();
445
501
  proc.kill("SIGTERM");
446
502
  const elapsed = Math.round((Date.now() - startMs) / 1000);
447
- console.log(`${ts()} ${label} ${chalk.yellow("โš  Timed out")} after ${elapsed}s (${toolCalls} tool calls, ${fullText.length} chars)`);
503
+ console.log(`${ts()} ${label} ${chalk.yellow("โš  Timed out")} after ${formatElapsed(elapsed)} (${toolCalls} tool calls, ${fullText.length} chars)`);
504
+ if (taskId)
505
+ postLog(taskId, `${PREFIX} [${name}] โš  Timed out after ${formatElapsed(elapsed)}`);
448
506
  resolve(resultText || fullText || "");
449
507
  }, timeoutMs);
450
508
  proc.on("exit", (code) => {
451
509
  clearTimeout(timeout);
510
+ clearInterval(analystSpinnerInterval);
511
+ analystSpinner.stop();
452
512
  const elapsed = Math.round((Date.now() - startMs) / 1000);
453
513
  if (timedOut)
454
514
  return; // already resolved
455
515
  const output = resultText || fullText || "";
456
516
  if (code === 0 && output.length > 0) {
457
- console.log(`${ts()} ${label} ${chalk.green("โœ“ Done")} in ${elapsed}s (${toolCalls} tool calls, ${output.length} chars)`);
517
+ console.log(`${ts()} ${label} ${chalk.green("โœ“ Done")} in ${formatElapsed(elapsed)} (${toolCalls} tool calls, ${output.length} chars)`);
518
+ if (taskId)
519
+ postLog(taskId, `${PREFIX} [${name}] โœ“ Done in ${formatElapsed(elapsed)} (${toolCalls} tool calls, ${output.length} chars)`);
458
520
  }
459
521
  else if (code !== 0) {
460
- console.log(`${ts()} ${label} ${chalk.red(`โœ— Exited ${code}`)} after ${elapsed}s โ€” ${stderrOutput.substring(0, 150) || "no stderr"}`);
522
+ console.log(`${ts()} ${label} ${chalk.red(`โœ— Exited ${code}`)} after ${formatElapsed(elapsed)} โ€” ${stderrOutput.substring(0, 150) || "no stderr"}`);
523
+ if (taskId)
524
+ postLog(taskId, `${PREFIX} [${name}] โœ— Exited ${code} after ${formatElapsed(elapsed)}`);
461
525
  }
462
526
  else {
463
- console.log(`${ts()} ${label} ${chalk.yellow("โš  Empty output")} after ${elapsed}s (${toolCalls} tool calls)`);
527
+ console.log(`${ts()} ${label} ${chalk.yellow("โš  Empty output")} after ${formatElapsed(elapsed)} (${toolCalls} tool calls)`);
528
+ if (taskId)
529
+ postLog(taskId, `${PREFIX} [${name}] โš  Empty output after ${formatElapsed(elapsed)}`);
464
530
  }
465
531
  resolve(output);
466
532
  });
467
533
  proc.on("error", (err) => {
468
534
  clearTimeout(timeout);
535
+ clearInterval(analystSpinnerInterval);
536
+ analystSpinner.stop();
469
537
  console.log(`${ts()} ${label} ${chalk.red("โœ— Spawn failed:")} ${err.message}`);
470
538
  resolve("");
471
539
  });
@@ -476,10 +544,13 @@ function runAnalyst(name, claudePath, model, prompt, repoPath, env, timeoutMs =
476
544
  * Used for non-Anthropic providers (OpenAI, Google, Ollama) that can't use Claude CLI.
477
545
  * Returns the analyst's report text, or an empty string on failure.
478
546
  */
479
- async function runAnalystWithSdk(name, provider, model, apiKey, prompt, repoPath, timeoutMs = 900_000) {
547
+ async function runAnalystWithSdk(name, provider, model, apiKey, prompt, repoPath, timeoutMs = 900_000, taskId) {
480
548
  const label = chalk.blue(`[${name}]`);
549
+ const modelLabel = chalk.yellow(`${provider}/${model}`);
481
550
  const startMs = Date.now();
482
- console.log(`${ts()} ${label} Starting via AI SDK (${chalk.dim(`${provider}/${model}`)})...`);
551
+ console.log(`${ts()} ${label} Starting analyst using ${modelLabel} via AI SDK...`);
552
+ if (taskId)
553
+ postLog(taskId, `${PREFIX} [${name}] Starting analyst using ${provider}/${model} via AI SDK...`);
483
554
  try {
484
555
  const result = await generateTextWithTools({
485
556
  provider,
@@ -594,13 +665,13 @@ async function runTeamAnalysis(task, basePrompt, claudePath, model, env, repoPat
594
665
  // Helper: dispatch analyst to Claude CLI or AI SDK based on provider
595
666
  const dispatchAnalyst = (name, prompt) => {
596
667
  if (useCliAnalysts) {
597
- return runAnalyst(name, claudePath, analysisModel, prompt, repoPath, env);
668
+ return runAnalyst(name, claudePath, analysisModel, prompt, repoPath, env, 900_000, taskId);
598
669
  }
599
670
  if (!providerApiKey) {
600
671
  console.log(`${ts()} ${taskLabel} ${chalk.yellow("โš ")} No API key for ${provider} analysts, skipping ${name}`);
601
672
  return Promise.resolve("");
602
673
  }
603
- return runAnalystWithSdk(name, provider, analysisModel, providerApiKey, prompt, repoPath);
674
+ return runAnalystWithSdk(name, provider, analysisModel, providerApiKey, prompt, repoPath, 900_000, taskId);
604
675
  };
605
676
  let codebaseReport = "";
606
677
  let requirementsReport = "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workermill/agent",
3
- "version": "0.7.8",
3
+ "version": "0.7.9",
4
4
  "description": "WorkerMill Remote Agent - Run AI workers locally with your Claude Max subscription",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",