@workermill/agent 0.2.1 → 0.3.0
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/cli.js +7 -1
- package/dist/commands/update.d.ts +1 -0
- package/dist/commands/update.js +20 -0
- package/dist/index.js +18 -1
- package/dist/planner.js +26 -3
- package/dist/poller.js +28 -0
- package/dist/updater.d.ts +8 -0
- package/dist/updater.js +40 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +4 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -13,12 +13,14 @@ import { stopCommand } from "./commands/stop.js";
|
|
|
13
13
|
import { statusCommand } from "./commands/status.js";
|
|
14
14
|
import { logsCommand } from "./commands/logs.js";
|
|
15
15
|
import { pullCommand } from "./commands/pull.js";
|
|
16
|
+
import { updateCommand } from "./commands/update.js";
|
|
16
17
|
import { getConfigFile } from "./config.js";
|
|
18
|
+
import { AGENT_VERSION } from "./version.js";
|
|
17
19
|
const program = new Command();
|
|
18
20
|
program
|
|
19
21
|
.name("workermill-agent")
|
|
20
22
|
.description("WorkerMill Remote Agent - Run AI workers locally with your Claude Max subscription")
|
|
21
|
-
.version(
|
|
23
|
+
.version(AGENT_VERSION);
|
|
22
24
|
program
|
|
23
25
|
.command("setup")
|
|
24
26
|
.description("Interactive setup wizard - configure API key, validate prerequisites, pull worker image")
|
|
@@ -45,6 +47,10 @@ program
|
|
|
45
47
|
.command("pull")
|
|
46
48
|
.description("Pull the latest worker Docker image")
|
|
47
49
|
.action(pullCommand);
|
|
50
|
+
program
|
|
51
|
+
.command("update")
|
|
52
|
+
.description("Update the agent to the latest version")
|
|
53
|
+
.action(updateCommand);
|
|
48
54
|
// If no command given, auto-detect: run setup if no config, otherwise start
|
|
49
55
|
if (process.argv.length <= 2) {
|
|
50
56
|
if (existsSync(getConfigFile())) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function updateCommand(): Promise<void>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { AGENT_VERSION } from "../version.js";
|
|
3
|
+
import { selfUpdate } from "../updater.js";
|
|
4
|
+
export async function updateCommand() {
|
|
5
|
+
console.log();
|
|
6
|
+
console.log(chalk.bold.cyan(" WorkerMill Agent Update"));
|
|
7
|
+
console.log(chalk.dim(" ─────────────────────────────────────"));
|
|
8
|
+
console.log(` ${chalk.dim("Current version:")} ${AGENT_VERSION}`);
|
|
9
|
+
console.log();
|
|
10
|
+
const success = await selfUpdate();
|
|
11
|
+
if (success) {
|
|
12
|
+
console.log();
|
|
13
|
+
console.log(chalk.green(" Update complete. If the agent is running, restart it to use the new version."));
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
console.log();
|
|
17
|
+
console.error(chalk.red(" Update failed. Try running manually: npm install -g @workermill/agent@latest"));
|
|
18
|
+
process.exitCode = 1;
|
|
19
|
+
}
|
|
20
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,8 @@ import chalk from "chalk";
|
|
|
8
8
|
import { initApi, api } from "./api.js";
|
|
9
9
|
import { startPolling, startHeartbeat } from "./poller.js";
|
|
10
10
|
import { stopAll } from "./spawner.js";
|
|
11
|
+
import { AGENT_VERSION } from "./version.js";
|
|
12
|
+
import { selfUpdate, restartAgent } from "./updater.js";
|
|
11
13
|
export { loadConfig, loadConfigFromFile, validatePrerequisites, getSystemInfo, findClaudePath } from "./config.js";
|
|
12
14
|
/**
|
|
13
15
|
* Start the remote agent with the given config.
|
|
@@ -47,10 +49,25 @@ export async function startAgent(config) {
|
|
|
47
49
|
}
|
|
48
50
|
// Register agent
|
|
49
51
|
try {
|
|
50
|
-
await api.post("/api/agent/register", {
|
|
52
|
+
const registerResponse = await api.post("/api/agent/register", {
|
|
51
53
|
agentId: config.agentId,
|
|
52
54
|
maxWorkers: config.maxWorkers,
|
|
55
|
+
agentVersion: AGENT_VERSION,
|
|
53
56
|
});
|
|
57
|
+
const { updateAvailable, updateRequired, latestVersion } = registerResponse.data;
|
|
58
|
+
if (updateRequired) {
|
|
59
|
+
console.log(chalk.red(` ⚠ Agent update required (current: ${AGENT_VERSION}, required: ${latestVersion})`));
|
|
60
|
+
const success = await selfUpdate();
|
|
61
|
+
if (success) {
|
|
62
|
+
restartAgent();
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
console.log(chalk.yellow(" Auto-update failed. Run: npm install -g @workermill/agent@latest"));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else if (updateAvailable) {
|
|
69
|
+
console.log(chalk.yellow(` Update available: ${latestVersion} (current: ${AGENT_VERSION}). Run: workermill-agent update`));
|
|
70
|
+
}
|
|
54
71
|
}
|
|
55
72
|
catch {
|
|
56
73
|
// Registration is best-effort, don't fail startup
|
package/dist/planner.js
CHANGED
|
@@ -260,6 +260,9 @@ export async function planTask(task, config) {
|
|
|
260
260
|
let currentPrompt = basePrompt;
|
|
261
261
|
let bestPlan = null;
|
|
262
262
|
let bestScore = 0;
|
|
263
|
+
// Track critic history across iterations for analytics
|
|
264
|
+
const criticHistory = [];
|
|
265
|
+
let totalFileCapTruncations = 0;
|
|
263
266
|
for (let iteration = 1; iteration <= MAX_ITERATIONS; iteration++) {
|
|
264
267
|
const iterLabel = MAX_ITERATIONS > 1 ? ` (attempt ${iteration}/${MAX_ITERATIONS})` : "";
|
|
265
268
|
if (iteration > 1) {
|
|
@@ -299,6 +302,7 @@ export async function planTask(task, config) {
|
|
|
299
302
|
// 2c. Apply file cap (max 5 files per story)
|
|
300
303
|
const { truncatedCount, details } = applyFileCap(plan);
|
|
301
304
|
if (truncatedCount > 0) {
|
|
305
|
+
totalFileCapTruncations += truncatedCount;
|
|
302
306
|
const msg = `${PREFIX} File cap applied: ${truncatedCount} stories truncated to max 5 targetFiles`;
|
|
303
307
|
console.log(`${ts()} ${taskLabel} ${chalk.yellow("⚠")} ${msg}`);
|
|
304
308
|
await postLog(task.id, msg);
|
|
@@ -319,13 +323,25 @@ export async function planTask(task, config) {
|
|
|
319
323
|
// Critic failed entirely — use this plan as fallback
|
|
320
324
|
bestPlan = plan;
|
|
321
325
|
}
|
|
326
|
+
// Record critic history for this iteration
|
|
327
|
+
if (criticResult) {
|
|
328
|
+
criticHistory.push({
|
|
329
|
+
iteration,
|
|
330
|
+
score: criticResult.score,
|
|
331
|
+
approved: criticResult.approved || criticResult.score >= AUTO_APPROVAL_THRESHOLD,
|
|
332
|
+
risks: criticResult.risks,
|
|
333
|
+
suggestions: criticResult.suggestions,
|
|
334
|
+
filesCapApplied: truncatedCount > 0 ? truncatedCount : undefined,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
322
337
|
// 2e. Check critic result
|
|
323
338
|
if (!criticResult) {
|
|
324
339
|
// Critic failed (timeout, parse error, etc.) — post plan without critic gate
|
|
325
340
|
const msg = `${PREFIX} Critic validation failed — posting plan without critic score`;
|
|
326
341
|
console.log(`${ts()} ${taskLabel} ${chalk.yellow("⚠")} ${msg}`);
|
|
327
342
|
await postLog(task.id, msg);
|
|
328
|
-
|
|
343
|
+
const planningDurationMs = Date.now() - startTime;
|
|
344
|
+
return await postValidatedPlan(task.id, plan, config.agentId, taskLabel, elapsed, undefined, undefined, criticHistory, totalFileCapTruncations, planningDurationMs, iteration);
|
|
329
345
|
}
|
|
330
346
|
if (criticResult.approved || criticResult.score >= AUTO_APPROVAL_THRESHOLD) {
|
|
331
347
|
// Approved! Post the file-capped plan
|
|
@@ -337,7 +353,8 @@ export async function planTask(task, config) {
|
|
|
337
353
|
console.log(`${ts()} ${taskLabel} ${chalk.dim(risksMsg)}`);
|
|
338
354
|
await postLog(task.id, risksMsg);
|
|
339
355
|
}
|
|
340
|
-
|
|
356
|
+
const planningDurationMs = Date.now() - startTime;
|
|
357
|
+
return await postValidatedPlan(task.id, plan, config.agentId, taskLabel, elapsed, criticResult.score, criticResult.risks, criticHistory, totalFileCapTruncations, planningDurationMs, iteration);
|
|
341
358
|
}
|
|
342
359
|
// 2f. Rejected — append critic feedback for next iteration
|
|
343
360
|
if (iteration < MAX_ITERATIONS) {
|
|
@@ -382,13 +399,19 @@ export async function planTask(task, config) {
|
|
|
382
399
|
* Re-serializes the plan as a JSON code block since the server-side
|
|
383
400
|
* parseExecutionPlan() expects that format.
|
|
384
401
|
*/
|
|
385
|
-
async function postValidatedPlan(taskId, plan, agentId, taskLabel, elapsed) {
|
|
402
|
+
async function postValidatedPlan(taskId, plan, agentId, taskLabel, elapsed, criticScore, criticRisks, criticHistory, fileCapTruncations, planningDurationMs, criticIterations) {
|
|
386
403
|
const serialized = serializePlan(plan);
|
|
387
404
|
try {
|
|
388
405
|
const result = await api.post("/api/agent/plan-result", {
|
|
389
406
|
taskId,
|
|
390
407
|
rawOutput: serialized,
|
|
391
408
|
agentId,
|
|
409
|
+
criticScore,
|
|
410
|
+
criticRisks,
|
|
411
|
+
criticHistory,
|
|
412
|
+
criticIterations,
|
|
413
|
+
fileCapTruncations,
|
|
414
|
+
planningDurationMs,
|
|
392
415
|
});
|
|
393
416
|
const storyCount = result.data.storyCount;
|
|
394
417
|
console.log(`${ts()} ${taskLabel} ${chalk.green("✓")} Plan validated: ${chalk.bold(storyCount)} stories → ${chalk.green("queued")}`);
|
package/dist/poller.js
CHANGED
|
@@ -8,10 +8,15 @@ import chalk from "chalk";
|
|
|
8
8
|
import { api } from "./api.js";
|
|
9
9
|
import { planTask } from "./planner.js";
|
|
10
10
|
import { spawnWorker, getActiveCount, getActiveTaskIds, stopTask } from "./spawner.js";
|
|
11
|
+
import { AGENT_VERSION } from "./version.js";
|
|
12
|
+
import { selfUpdate, restartAgent } from "./updater.js";
|
|
11
13
|
// Track tasks currently being planned (to avoid double-dispatching)
|
|
12
14
|
const planningInProgress = new Set();
|
|
13
15
|
// Cached org config
|
|
14
16
|
let orgConfig = null;
|
|
17
|
+
// Flags to avoid spamming update notices every heartbeat
|
|
18
|
+
let updateNoticePrinted = false;
|
|
19
|
+
let updateInProgress = false;
|
|
15
20
|
/** Timestamp prefix for log lines */
|
|
16
21
|
function ts() {
|
|
17
22
|
return chalk.dim(new Date().toLocaleTimeString());
|
|
@@ -36,6 +41,9 @@ async function getOrgConfig() {
|
|
|
36
41
|
* Run a single poll iteration.
|
|
37
42
|
*/
|
|
38
43
|
async function pollOnce(config) {
|
|
44
|
+
// Skip polling when a required update is in progress
|
|
45
|
+
if (updateInProgress)
|
|
46
|
+
return;
|
|
39
47
|
try {
|
|
40
48
|
const response = await api.get("/api/agent/poll", {
|
|
41
49
|
params: { agentId: config.agentId },
|
|
@@ -174,6 +182,7 @@ export function startHeartbeat(config) {
|
|
|
174
182
|
const response = await api.post("/api/agent/heartbeat", {
|
|
175
183
|
agentId: config.agentId,
|
|
176
184
|
activeTasks: activeTaskIds,
|
|
185
|
+
agentVersion: AGENT_VERSION,
|
|
177
186
|
});
|
|
178
187
|
// Stop containers for tasks cancelled via the cloud dashboard
|
|
179
188
|
const cancelledTasks = response.data?.cancelledTasks;
|
|
@@ -182,6 +191,25 @@ export function startHeartbeat(config) {
|
|
|
182
191
|
stopTask(taskId);
|
|
183
192
|
}
|
|
184
193
|
}
|
|
194
|
+
// Handle update signals from server
|
|
195
|
+
const { updateAvailable, updateRequired, latestVersion } = response.data ?? {};
|
|
196
|
+
if (updateRequired && !updateInProgress) {
|
|
197
|
+
updateInProgress = true;
|
|
198
|
+
console.log(`${ts()} ${chalk.red("⚠ Agent update required")} (current: ${AGENT_VERSION}, required: ${latestVersion})`);
|
|
199
|
+
console.log(`${ts()} ${chalk.yellow("Refusing new tasks until updated.")}`);
|
|
200
|
+
const success = await selfUpdate();
|
|
201
|
+
if (success) {
|
|
202
|
+
restartAgent();
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
console.log(`${ts()} ${chalk.red("Auto-update failed.")} Run: npm install -g @workermill/agent@latest`);
|
|
206
|
+
updateInProgress = false;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
else if (updateAvailable && !updateNoticePrinted && !updateRequired) {
|
|
210
|
+
updateNoticePrinted = true;
|
|
211
|
+
console.log(`${ts()} ${chalk.yellow(`Update available: ${latestVersion}`)} (current: ${AGENT_VERSION}). Run: workermill-agent update`);
|
|
212
|
+
}
|
|
185
213
|
}
|
|
186
214
|
catch {
|
|
187
215
|
// Heartbeat failures are non-critical
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run `npm install -g @workermill/agent@latest` and return whether it succeeded.
|
|
3
|
+
*/
|
|
4
|
+
export declare function selfUpdate(): Promise<boolean>;
|
|
5
|
+
/**
|
|
6
|
+
* Restart the current process by spawning a new one and exiting.
|
|
7
|
+
*/
|
|
8
|
+
export declare function restartAgent(): never;
|
package/dist/updater.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
/**
|
|
4
|
+
* Run `npm install -g @workermill/agent@latest` and return whether it succeeded.
|
|
5
|
+
*/
|
|
6
|
+
export async function selfUpdate() {
|
|
7
|
+
console.log(chalk.cyan(" Updating @workermill/agent..."));
|
|
8
|
+
return new Promise((resolve) => {
|
|
9
|
+
const child = spawn("npm", ["install", "-g", "@workermill/agent@latest"], {
|
|
10
|
+
stdio: "inherit",
|
|
11
|
+
shell: true,
|
|
12
|
+
});
|
|
13
|
+
child.on("error", (err) => {
|
|
14
|
+
console.error(chalk.red(` Update failed: ${err.message}`));
|
|
15
|
+
resolve(false);
|
|
16
|
+
});
|
|
17
|
+
child.on("close", (code) => {
|
|
18
|
+
if (code === 0) {
|
|
19
|
+
console.log(chalk.green(" Update successful."));
|
|
20
|
+
resolve(true);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
console.error(chalk.red(` Update failed with exit code ${code}`));
|
|
24
|
+
resolve(false);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Restart the current process by spawning a new one and exiting.
|
|
31
|
+
*/
|
|
32
|
+
export function restartAgent() {
|
|
33
|
+
console.log(chalk.cyan(" Restarting agent..."));
|
|
34
|
+
const child = spawn(process.argv[0], process.argv.slice(1), {
|
|
35
|
+
stdio: "inherit",
|
|
36
|
+
detached: true,
|
|
37
|
+
});
|
|
38
|
+
child.unref();
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const AGENT_VERSION: string;
|
package/dist/version.js
ADDED