@workermill/agent 0.7.16 → 0.7.18

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.
package/dist/planner.js CHANGED
@@ -20,6 +20,50 @@ import { findClaudePath } from "./config.js";
20
20
  import { api } from "./api.js";
21
21
  import { parseExecutionPlan, applyFileCap, applyStoryCap, resolveFileOverlaps, serializePlan, runCriticValidation, formatCriticFeedback, AUTO_APPROVAL_THRESHOLD, } from "./plan-validator.js";
22
22
  import { generateTextWithTools } from "./ai-sdk-generate.js";
23
+ /**
24
+ * Extract token usage from a stream-json event.
25
+ * Claude reports cumulative tokens, so we use Math.max to track the highest values.
26
+ */
27
+ function extractTokenUsage(event, usage) {
28
+ const paths = [
29
+ event.usage,
30
+ event.message?.usage,
31
+ event.result?.usage,
32
+ ];
33
+ for (const u of paths) {
34
+ if (u && typeof u === "object") {
35
+ const d = u;
36
+ if (typeof d.input_tokens === "number")
37
+ usage.inputTokens = Math.max(usage.inputTokens, d.input_tokens);
38
+ if (typeof d.output_tokens === "number")
39
+ usage.outputTokens = Math.max(usage.outputTokens, d.output_tokens);
40
+ if (typeof d.cache_creation_input_tokens === "number")
41
+ usage.cacheCreationTokens = Math.max(usage.cacheCreationTokens, d.cache_creation_input_tokens);
42
+ if (typeof d.cache_read_input_tokens === "number")
43
+ usage.cacheReadTokens = Math.max(usage.cacheReadTokens, d.cache_read_input_tokens);
44
+ }
45
+ }
46
+ }
47
+ /**
48
+ * Report partial token usage to the cloud API.
49
+ */
50
+ async function reportPlanningUsage(taskId, usage, model, mode) {
51
+ if (usage.inputTokens === 0 && usage.outputTokens === 0)
52
+ return;
53
+ try {
54
+ await api.post(`/api/tasks/${taskId}/usage/partial`, {
55
+ inputTokens: usage.inputTokens,
56
+ outputTokens: usage.outputTokens,
57
+ cacheCreationTokens: usage.cacheCreationTokens,
58
+ cacheReadTokens: usage.cacheReadTokens,
59
+ model,
60
+ mode,
61
+ });
62
+ }
63
+ catch {
64
+ // Fire and forget
65
+ }
66
+ }
23
67
  /** Max Planner-Critic iterations before giving up */
24
68
  const MAX_ITERATIONS = 3;
25
69
  /** Timestamp prefix */
@@ -83,16 +127,22 @@ function phaseLabel(phase, elapsed) {
83
127
  * Run Claude CLI with stream-json output, posting real-time phase milestones
84
128
  * to the cloud dashboard — identical terminal experience to cloud planning.
85
129
  */
86
- function runClaudeCli(claudePath, model, prompt, env, taskId, startTime) {
130
+ function runClaudeCli(claudePath, model, prompt, env, taskId, startTime, disableTools = false) {
87
131
  const taskLabel = chalk.cyan(taskId.slice(0, 8));
88
132
  return new Promise((resolve, reject) => {
89
- const proc = spawn(claudePath, [
133
+ const cliArgs = [
90
134
  "--print",
91
135
  "--verbose",
92
136
  "--output-format", "stream-json",
93
137
  "--model", model,
94
138
  "--permission-mode", "bypassPermissions",
95
- ], {
139
+ ];
140
+ // When analysts already explored the repo, strip tools so the planner
141
+ // doesn't waste turns re-exploring — it has all context in the prompt.
142
+ if (disableTools) {
143
+ cliArgs.push("--allowedTools", "");
144
+ }
145
+ const proc = spawn(claudePath, cliArgs, {
96
146
  env,
97
147
  stdio: ["pipe", "pipe", "pipe"],
98
148
  });
@@ -103,6 +153,9 @@ function runClaudeCli(claudePath, model, prompt, env, taskId, startTime) {
103
153
  let stderrOutput = "";
104
154
  let charsReceived = 0;
105
155
  let toolCallCount = 0;
156
+ // Token usage accumulator — extract from stream events using Math.max
157
+ const tokenUsage = { inputTokens: 0, outputTokens: 0, cacheCreationTokens: 0, cacheReadTokens: 0 };
158
+ let resultModel = model;
106
159
  // Buffered text streaming — flush complete lines to dashboard every 1s.
107
160
  // LLM deltas are tiny fragments; we accumulate until we see '\n', then
108
161
  // a 1s interval flushes all complete lines as log entries. On exit we
@@ -240,6 +293,16 @@ function runClaudeCli(claudePath, model, prompt, env, taskId, startTime) {
240
293
  else if (event.type === "result" && event.result) {
241
294
  resultText = typeof event.result === "string" ? event.result : "";
242
295
  }
296
+ // Extract token usage from any event that carries it
297
+ extractTokenUsage(event, tokenUsage);
298
+ if (event.type === "result" && event.total_cost_usd !== undefined) {
299
+ // Result event also carries model info
300
+ if (event.modelUsage && typeof event.modelUsage === "object") {
301
+ const models = Object.keys(event.modelUsage);
302
+ if (models.length > 0)
303
+ resultModel = models[0];
304
+ }
305
+ }
243
306
  }
244
307
  catch {
245
308
  // Not valid JSON — raw text, accumulate
@@ -251,10 +314,17 @@ function runClaudeCli(claudePath, model, prompt, env, taskId, startTime) {
251
314
  proc.stderr.on("data", (chunk) => {
252
315
  stderrOutput += chunk.toString();
253
316
  });
317
+ // Report partial token usage every 30s during planning
318
+ const usageReportInterval = setInterval(() => {
319
+ if (tokenUsage.inputTokens > 0 || tokenUsage.outputTokens > 0) {
320
+ reportPlanningUsage(taskId, tokenUsage, resultModel, "greatest").catch(() => { });
321
+ }
322
+ }, 30_000);
254
323
  function cleanupAll() {
255
324
  clearInterval(progressInterval);
256
325
  clearInterval(sseProgressInterval);
257
326
  clearInterval(textFlushInterval);
327
+ clearInterval(usageReportInterval);
258
328
  flushTextBuffer(true);
259
329
  }
260
330
  const timeout = setTimeout(() => {
@@ -268,6 +338,8 @@ function runClaudeCli(claudePath, model, prompt, env, taskId, startTime) {
268
338
  // Emit final "validating" phase to dashboard
269
339
  const elapsedAtClose = Math.round((Date.now() - startTime) / 1000);
270
340
  postProgress(taskId, "validating", elapsedAtClose, "Validating plan...", charsReceived, toolCallCount);
341
+ // Final usage report
342
+ reportPlanningUsage(taskId, tokenUsage, resultModel, "greatest").catch(() => { });
271
343
  if (code !== 0) {
272
344
  reject(new Error(`Claude CLI failed (exit ${code}): ${stderrOutput.substring(0, 300)}`));
273
345
  }
@@ -766,7 +838,9 @@ export async function planTask(task, config, credentials) {
766
838
  let rawOutput;
767
839
  try {
768
840
  if (isAnthropicPlanning) {
769
- rawOutput = await runClaudeCli(claudePath, cliModel, currentPrompt, cleanEnv, task.id, startTime);
841
+ // Disable tools when analysts already provided repo context
842
+ const hasAnalystContext = enhancedBasePrompt !== basePrompt;
843
+ rawOutput = await runClaudeCli(claudePath, cliModel, currentPrompt, cleanEnv, task.id, startTime, hasAnalystContext);
770
844
  }
771
845
  else {
772
846
  if (!providerApiKey) {
package/dist/spawner.js CHANGED
@@ -140,6 +140,17 @@ export async function spawnWorker(task, config, orgConfig, credentials) {
140
140
  return;
141
141
  }
142
142
  if (claudeConfigDir) {
143
+ // Ensure credentials file is readable AND writable inside container.
144
+ // Claude CLI creates .credentials.json with 600 permissions, but the container
145
+ // runs as UID 1001 (worker) while the host user is UID 1000. Without this chmod,
146
+ // the mounted file is unreadable inside the container → "Invalid API key" errors.
147
+ const credFile = path.join(claudeConfigDir, ".credentials.json");
148
+ try {
149
+ fs.chmodSync(credFile, 0o666);
150
+ }
151
+ catch {
152
+ // Ignore - file may not exist yet
153
+ }
143
154
  const dockerClaudeDir = toDockerPath(claudeConfigDir);
144
155
  dockerArgs.push("-v", `${dockerClaudeDir}:/home/worker/.claude`);
145
156
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workermill/agent",
3
- "version": "0.7.16",
3
+ "version": "0.7.18",
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",