@workermill/agent 0.8.5 → 0.8.8

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.
@@ -311,29 +311,9 @@ export async function setupCommand() {
311
311
  }
312
312
  process.exit(1);
313
313
  }
314
- // SCM token
315
- const tokenPrompts = {
316
- github: "",
317
- bitbucket: "",
318
- gitlab: "",
319
- };
320
- const scmLabel = scmProvider === "bitbucket"
321
- ? "Bitbucket"
322
- : scmProvider === "gitlab"
323
- ? "GitLab"
324
- : "GitHub";
325
- const { scmToken } = await inquirer.prompt([
326
- {
327
- type: "password",
328
- name: "scmToken",
329
- message: `${scmLabel} personal access token (for cloning/pushing to your repos):`,
330
- mask: "*",
331
- validate: (v) => (v.length > 0 ? true : "A token is required for workers to clone and push to your repositories"),
332
- },
333
- ]);
334
- if (scmToken) {
335
- tokenPrompts[scmProvider] = scmToken;
336
- }
314
+ // SCM tokens come from org Settings > Integrations (no local prompt needed)
315
+ console.log(chalk.dim(" SCM tokens are managed via Settings > Integrations on the dashboard."));
316
+ console.log();
337
317
  const { agentId } = await inquirer.prompt([
338
318
  {
339
319
  type: "input",
@@ -342,36 +322,72 @@ export async function setupCommand() {
342
322
  default: `agent-${hostname()}`,
343
323
  },
344
324
  ]);
345
- // ── Step 7: Pull worker image ─────────────────────────────────────────────
325
+ // ── Step 7: AWS CLI + ECR auth + pull worker image ───────────────────────
346
326
  console.log();
347
- const workerImage = "public.ecr.aws/a7k5r0v0/workermill-worker:latest";
348
- console.log(chalk.dim(` Pulling worker image: ${workerImage}`));
349
- console.log(chalk.dim(" This may take a few minutes on first run (~1.1 GB)..."));
350
- console.log();
351
- // Use spawnSync to show progress AND capture errors
352
- const pullResult = spawnSync("docker", ["pull", workerImage], {
353
- stdio: "inherit",
354
- timeout: 600_000, // 10 minutes
355
- });
356
- if (pullResult.status === 0) {
357
- console.log();
358
- console.log(chalk.green(` ✓ Worker image pulled`));
327
+ const PRIVATE_ECR_REGISTRY = "593971626975.dkr.ecr.us-east-1.amazonaws.com";
328
+ const workerImage = `${PRIVATE_ECR_REGISTRY}/workermill-dev/worker:latest`;
329
+ // Check AWS CLI
330
+ const awsSpinner = ora("Checking AWS CLI...").start();
331
+ if (commandExists("aws")) {
332
+ try {
333
+ execSync("aws sts get-caller-identity", {
334
+ stdio: "pipe",
335
+ timeout: 15000,
336
+ });
337
+ awsSpinner.succeed("AWS CLI configured");
338
+ }
339
+ catch {
340
+ awsSpinner.warn("AWS CLI found but credentials not configured");
341
+ console.log();
342
+ console.log(chalk.yellow(" Configure AWS credentials for private ECR image access:"));
343
+ console.log(chalk.cyan(" aws configure"));
344
+ console.log(chalk.dim(" Contact your WorkerMill admin for AWS access key / secret key."));
345
+ console.log(chalk.dim(" Setup will continue — you can configure AWS later before starting."));
346
+ }
359
347
  }
360
348
  else {
349
+ awsSpinner.warn("AWS CLI not found");
361
350
  console.log();
362
- console.log(chalk.red(" Failed to pull worker image."));
351
+ console.log(chalk.yellow(" AWS CLI is required for pulling worker images from private ECR."));
352
+ console.log(chalk.cyan(" Install: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html"));
353
+ console.log(chalk.dim(" Setup will continue — install AWS CLI before starting the agent."));
354
+ }
355
+ // Authenticate with ECR
356
+ console.log();
357
+ console.log(chalk.dim(` Authenticating with private ECR...`));
358
+ let ecrAuthed = false;
359
+ try {
360
+ execSync(`aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin ${PRIVATE_ECR_REGISTRY}`, { stdio: "pipe", timeout: 30_000 });
361
+ console.log(chalk.green(" ✓ ECR authenticated"));
362
+ ecrAuthed = true;
363
+ }
364
+ catch {
365
+ console.log(chalk.yellow(" ⚠ ECR authentication failed (AWS credentials may not be configured yet)"));
366
+ console.log(chalk.dim(" Worker image pull will be skipped — authenticate later with:"));
367
+ console.log(chalk.dim(` aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin ${PRIVATE_ECR_REGISTRY}`));
368
+ }
369
+ // Pull worker image (only if ECR auth succeeded)
370
+ if (ecrAuthed) {
363
371
  console.log();
364
- if (pullResult.error) {
365
- console.log(chalk.yellow(` Error: ${pullResult.error.message}`));
372
+ console.log(chalk.dim(` Pulling worker image: ${workerImage}`));
373
+ console.log(chalk.dim(" This may take a few minutes on first run (~1.1 GB)..."));
374
+ console.log();
375
+ const pullResult = spawnSync("docker", ["pull", workerImage], {
376
+ stdio: "inherit",
377
+ timeout: 600_000,
378
+ });
379
+ if (pullResult.status === 0) {
380
+ console.log();
381
+ console.log(chalk.green(" ✓ Worker image pulled"));
366
382
  }
367
- console.log(chalk.yellow(" Troubleshooting:"));
368
- console.log(chalk.yellow(" 1. Is Docker Desktop running?"));
369
- if (isWindows) {
370
- console.log(chalk.yellow(" 2. Is Docker in Linux containers mode? (Right-click Docker tray icon)"));
383
+ else {
384
+ console.log();
385
+ console.log(chalk.red(" ✗ Failed to pull worker image."));
386
+ if (pullResult.error) {
387
+ console.log(chalk.yellow(` Error: ${pullResult.error.message}`));
388
+ }
389
+ console.log(chalk.dim(" Setup will continue — you can pull the image later before starting."));
371
390
  }
372
- console.log(chalk.yellow(` ${isWindows ? "3" : "2"}. Try manually: ${chalk.cyan(`docker pull ${workerImage}`)}`));
373
- console.log();
374
- console.log(chalk.dim(" Setup will continue — you can pull the image later before starting."));
375
391
  }
376
392
  // ── Step 8: Save config ───────────────────────────────────────────────────
377
393
  const fileConfig = {
@@ -381,7 +397,7 @@ export async function setupCommand() {
381
397
  maxWorkers: 1,
382
398
  pollIntervalMs: 5000,
383
399
  heartbeatIntervalMs: 30000,
384
- tokens: tokenPrompts,
400
+ tokens: { github: "", bitbucket: "", gitlab: "" }, // SCM tokens come from org Settings
385
401
  workerImage,
386
402
  setupCompletedAt: new Date().toISOString(),
387
403
  };
package/dist/config.js CHANGED
@@ -53,10 +53,12 @@ export function loadConfigFromFile() {
53
53
  console.error("Config file is missing required fields (apiUrl, apiKey). Re-run 'workermill-agent setup'.");
54
54
  process.exit(1);
55
55
  }
56
- // Migrate any stale image URLs to current default
57
- const defaultImage = "public.ecr.aws/a7k5r0v0/workermill-worker:latest";
56
+ // Migrate any stale image URLs to current default (private ECR)
57
+ const defaultImage = "593971626975.dkr.ecr.us-east-1.amazonaws.com/workermill-dev/worker:latest";
58
58
  let workerImage = fc.workerImage || defaultImage;
59
- if (workerImage.includes("jarod1/") || !workerImage.includes("workermill-worker")) {
59
+ if (workerImage.includes("jarod1/") ||
60
+ workerImage.includes("public.ecr.aws/") ||
61
+ !workerImage.includes("workermill-worker") && !workerImage.includes("workermill-dev/worker")) {
60
62
  workerImage = defaultImage;
61
63
  fc.workerImage = workerImage;
62
64
  try {
@@ -151,7 +153,7 @@ export function findClaudePath() {
151
153
  */
152
154
  export function checkPrerequisites(workerImage) {
153
155
  const results = [];
154
- const image = workerImage || "public.ecr.aws/a7k5r0v0/workermill-worker:latest";
156
+ const image = workerImage || "593971626975.dkr.ecr.us-east-1.amazonaws.com/workermill-dev/worker:latest";
155
157
  // Docker
156
158
  try {
157
159
  const version = execSync("docker version --format {{.Server.Version}}", {
package/dist/spawner.d.ts CHANGED
@@ -51,6 +51,9 @@ export interface ClaimCredentials {
51
51
  githubReviewerToken?: string;
52
52
  scmBaseUrl?: string;
53
53
  ollamaContextWindow?: number;
54
+ scmToken?: string;
55
+ githubToken?: string;
56
+ bitbucketUsername?: string;
54
57
  anthropicApiKey?: string;
55
58
  openaiApiKey?: string;
56
59
  googleApiKey?: string;
@@ -102,6 +105,9 @@ export interface ManagerCredentials {
102
105
  jiraApiToken?: string;
103
106
  linearApiKey?: string;
104
107
  issueTrackerProvider?: string;
108
+ scmToken?: string;
109
+ githubToken?: string;
110
+ bitbucketUsername?: string;
105
111
  }
106
112
  /**
107
113
  * Spawn a manager Docker container for PR review or log analysis.
package/dist/spawner.js CHANGED
@@ -34,6 +34,21 @@ function detectWSL() {
34
34
  }
35
35
  const isWSL = detectWSL();
36
36
  const isDockerDesktop = isWSL || process.platform === "darwin" || process.platform === "win32";
37
+ /**
38
+ * Get WSL2 host IP for Docker --add-host override.
39
+ * On WSL2, host-gateway resolves to the Docker VM, not WSL2 where the API runs.
40
+ */
41
+ function getWSLHostIP() {
42
+ if (!isWSL)
43
+ return null;
44
+ try {
45
+ const ip = execSync("hostname -I", { encoding: "utf-8" }).trim().split(/\s+/)[0];
46
+ if (ip && /^\d+\.\d+\.\d+\.\d+$/.test(ip))
47
+ return ip;
48
+ }
49
+ catch { /* fall through */ }
50
+ return null;
51
+ }
37
52
  /**
38
53
  * Convert WSL paths to Windows paths for Docker volume mounts.
39
54
  */
@@ -69,14 +84,42 @@ function findClaudeConfigDir() {
69
84
  }
70
85
  return null;
71
86
  }
87
+ /** Private ECR registry for worker images */
88
+ const PRIVATE_ECR_REGISTRY = "593971626975.dkr.ecr.us-east-1.amazonaws.com";
89
+ /** Cache ECR login (token lasts 12h, refresh after 11h) */
90
+ let ecrLoginExpiresAt = 0;
91
+ /**
92
+ * Ensure Docker is logged into private ECR.
93
+ * Uses ambient AWS credentials (aws configure).
94
+ */
95
+ function ensureEcrLogin() {
96
+ if (Date.now() < ecrLoginExpiresAt)
97
+ return true;
98
+ try {
99
+ execSync(`aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin ${PRIVATE_ECR_REGISTRY}`, { stdio: "pipe", timeout: 30_000 });
100
+ ecrLoginExpiresAt = Date.now() + 11 * 60 * 60 * 1000;
101
+ return true;
102
+ }
103
+ catch {
104
+ return false;
105
+ }
106
+ }
72
107
  /**
73
108
  * Get SCM token based on provider.
109
+ * Prefers org credentials from API, falls back to local config.
74
110
  */
75
- function getScmToken(scmProvider, config) {
111
+ function getScmToken(scmProvider, config, credentials) {
112
+ // Org credentials from API are the primary source
113
+ if (credentials?.scmToken)
114
+ return credentials.scmToken;
115
+ // Fall back to local config tokens
76
116
  switch (scmProvider) {
77
- case "bitbucket": return config.bitbucketToken;
78
- case "gitlab": return config.gitlabToken;
79
- default: return config.githubToken;
117
+ case "bitbucket":
118
+ return config.bitbucketToken;
119
+ case "gitlab":
120
+ return config.gitlabToken;
121
+ default:
122
+ return config.githubToken;
80
123
  }
81
124
  }
82
125
  /** Check if a task has the self-review label (works across Jira, GitHub, GitLab, Linear) */
@@ -112,7 +155,9 @@ export async function spawnWorker(task, config, orgConfig, credentials) {
112
155
  }
113
156
  const containerName = `workermill-${task.id.slice(0, 8)}`;
114
157
  // Build Docker run arguments
115
- const dockerArgs = ["run", "--rm", "--pull", "always", "--name", containerName];
158
+ // Only pull from registry for remote images; local images (no '/' in name) are already on disk
159
+ const isLocalImage = !config.workerImage.includes("/");
160
+ const dockerArgs = ["run", "--rm", ...(isLocalImage ? [] : ["--pull", "always"]), "--name", containerName];
116
161
  // Resource limits — 6GB memory with swap for overflow.
117
162
  // NODE_OPTIONS caps V8 heap at 2GB; the extra room is for git, npm, Claude CLI subprocesses.
118
163
  const totalRamGB = Math.round(os.totalmem() / (1024 * 1024 * 1024));
@@ -125,9 +170,10 @@ export async function spawnWorker(task, config, orgConfig, credentials) {
125
170
  else {
126
171
  dockerArgs.push("--memory", "6g", "--memory-swap", "12g", "--cpus", "4");
127
172
  }
128
- // Network mode
173
+ // Network mode — WSL2: host-gateway resolves to Docker VM, not WSL2, so use actual WSL IP
129
174
  if (isDockerDesktop) {
130
- dockerArgs.push("--add-host=host.docker.internal:host-gateway");
175
+ const wslIP = getWSLHostIP();
176
+ dockerArgs.push(`--add-host=host.docker.internal:${wslIP || "host-gateway"}`);
131
177
  }
132
178
  else {
133
179
  dockerArgs.push("--network", "host");
@@ -158,8 +204,18 @@ export async function spawnWorker(task, config, orgConfig, credentials) {
158
204
  console.log(`${ts()} ${taskLabel} ${chalk.dim("Skipping Claude mount (non-Anthropic worker)")}`);
159
205
  }
160
206
  // Build environment variables — KEY DIFFERENCE: API_BASE_URL points to cloud
207
+ // Docker containers can't reach host via "localhost" — translate for Docker networking
208
+ const containerApiUrl = config.apiUrl.replace(/localhost|127\.0\.0\.1/, "host.docker.internal");
161
209
  const scmProvider = (task.scmProvider || "github");
162
- const scmToken = getScmToken(scmProvider, config);
210
+ const scmToken = getScmToken(scmProvider, config, credentials);
211
+ // Derive per-provider tokens: prefer org credentials, fall back to local config
212
+ const githubToken = credentials?.githubToken || config.githubToken;
213
+ const bitbucketToken = scmProvider === "bitbucket"
214
+ ? credentials?.scmToken || config.bitbucketToken
215
+ : config.bitbucketToken;
216
+ const gitlabToken = scmProvider === "gitlab"
217
+ ? credentials?.scmToken || config.gitlabToken
218
+ : config.gitlabToken;
163
219
  const envVars = {
164
220
  // Cap V8 heap to 3GB — forces aggressive GC instead of bloating to fill container.
165
221
  // Each Claude CLI subprocess inherits this, preventing unbounded heap growth.
@@ -178,18 +234,19 @@ export async function spawnWorker(task, config, orgConfig, credentials) {
178
234
  RETRY_NUMBER: String(task.retryCount ?? 0),
179
235
  TICKET_KEY: task.jiraIssueKey || "",
180
236
  // Cloud API — this is what makes remote agent mode work
181
- API_BASE_URL: config.apiUrl,
237
+ // Use containerApiUrl so Docker containers reach the host via host.docker.internal
238
+ API_BASE_URL: containerApiUrl,
182
239
  ORG_API_KEY: config.apiKey,
183
- // SCM configuration
240
+ // SCM configuration — org credentials from API are primary, local config is fallback
184
241
  SCM_PROVIDER: scmProvider,
185
242
  SCM_TOKEN: scmToken,
186
243
  SCM_BASE_URL: credentials?.scmBaseUrl || "",
187
- GITHUB_TOKEN: config.githubToken,
188
- GH_TOKEN: config.githubToken,
244
+ GITHUB_TOKEN: githubToken,
245
+ GH_TOKEN: githubToken,
189
246
  GITHUB_REVIEWER_TOKEN: credentials?.githubReviewerToken || "",
190
- BITBUCKET_TOKEN: config.bitbucketToken,
191
- BITBUCKET_USERNAME: "x-token-auth",
192
- GITLAB_TOKEN: config.gitlabToken,
247
+ BITBUCKET_TOKEN: bitbucketToken,
248
+ BITBUCKET_USERNAME: credentials?.bitbucketUsername || "x-token-auth",
249
+ GITLAB_TOKEN: gitlabToken,
193
250
  // Target repository
194
251
  TARGET_REPO: task.githubRepo || "",
195
252
  GITHUB_REPO: task.githubRepo || "",
@@ -269,8 +326,12 @@ export async function spawnWorker(task, config, orgConfig, credentials) {
269
326
  dockerArgs.push("-e", `${k}=${v}`);
270
327
  }
271
328
  }
272
- // Worker image (configurable: Docker Hub for CLI users, local for bin/remote-agent)
273
- dockerArgs.push(config.workerImage || "public.ecr.aws/a7k5r0v0/workermill-worker:latest");
329
+ // Worker image private ECR requires auth (ensureEcrLogin handles it)
330
+ const workerImage = config.workerImage || `${PRIVATE_ECR_REGISTRY}/workermill-dev/worker:latest`;
331
+ if (workerImage.includes(PRIVATE_ECR_REGISTRY)) {
332
+ ensureEcrLogin();
333
+ }
334
+ dockerArgs.push(workerImage);
274
335
  const reviewEnabled = task.skipManagerReview === false;
275
336
  console.log(`${ts()} ${taskLabel} ${chalk.dim("Starting container")} ${chalk.yellow(containerName)}`);
276
337
  console.log(`${ts()} ${taskLabel} ${chalk.dim(` skipManagerReview=${task.skipManagerReview} → REVIEW_ENABLED=${reviewEnabled}`)}`);
@@ -425,13 +486,29 @@ export async function spawnManagerWorker(task, config, credentials) {
425
486
  "--memory=4g",
426
487
  "--cpus=2",
427
488
  ];
489
+ // Network mode — same as main worker spawn
490
+ if (isDockerDesktop) {
491
+ const wslIP = getWSLHostIP();
492
+ dockerArgs.push(`--add-host=host.docker.internal:${wslIP || "host-gateway"}`);
493
+ }
494
+ else {
495
+ dockerArgs.push("--network", "host");
496
+ }
428
497
  if (claudeConfigDir) {
429
498
  const dockerClaudeDir = toDockerPath(claudeConfigDir);
430
499
  dockerArgs.push("-v", `${dockerClaudeDir}:/home/worker/.claude`);
431
500
  }
432
501
  // Manager-specific env vars (match ECS runManagerTask)
502
+ const containerApiUrl = config.apiUrl.replace(/localhost|127\.0\.0\.1/, "host.docker.internal");
433
503
  const scmProvider = (task.scmProvider || "github");
434
- const scmToken = getScmToken(scmProvider, config);
504
+ const scmToken = getScmToken(scmProvider, config, credentials);
505
+ const githubToken = credentials?.githubToken || config.githubToken;
506
+ const bitbucketToken = scmProvider === "bitbucket"
507
+ ? credentials?.scmToken || config.bitbucketToken
508
+ : config.bitbucketToken;
509
+ const gitlabToken = scmProvider === "gitlab"
510
+ ? credentials?.scmToken || config.gitlabToken
511
+ : config.gitlabToken;
435
512
  const envVars = {
436
513
  NODE_OPTIONS: "--max-old-space-size=3072",
437
514
  TASK_ID: task.id,
@@ -442,17 +519,17 @@ export async function spawnManagerWorker(task, config, credentials) {
442
519
  GITHUB_REPO: task.githubRepo || "",
443
520
  PR_URL: task.githubPrUrl || "",
444
521
  PR_NUMBER: task.githubPrNumber ? String(task.githubPrNumber) : "",
445
- // Cloud API
446
- API_BASE_URL: config.apiUrl,
522
+ // Cloud API — use containerApiUrl for Docker networking
523
+ API_BASE_URL: containerApiUrl,
447
524
  ORG_API_KEY: config.apiKey,
448
- // SCM configuration
525
+ // SCM configuration — org credentials from API are primary, local config is fallback
449
526
  SCM_PROVIDER: scmProvider,
450
527
  SCM_TOKEN: scmToken,
451
- GITHUB_TOKEN: config.githubToken,
452
- GH_TOKEN: config.githubToken,
453
- BITBUCKET_TOKEN: config.bitbucketToken,
454
- BITBUCKET_USERNAME: "x-token-auth",
455
- GITLAB_TOKEN: config.gitlabToken,
528
+ GITHUB_TOKEN: githubToken,
529
+ GH_TOKEN: githubToken,
530
+ BITBUCKET_TOKEN: bitbucketToken,
531
+ BITBUCKET_USERNAME: credentials?.bitbucketUsername || "x-token-auth",
532
+ GITLAB_TOKEN: gitlabToken,
456
533
  // Manager provider and model
457
534
  MANAGER_PROVIDER: credentials?.managerProvider || "anthropic",
458
535
  MANAGER_MODEL: credentials?.managerModelId || "",
@@ -473,8 +550,11 @@ export async function spawnManagerWorker(task, config, credentials) {
473
550
  dockerArgs.push("-e", `${k}=${v}`);
474
551
  }
475
552
  }
476
- // Worker image with manager entrypoint override
477
- const workerImage = config.workerImage || "public.ecr.aws/a7k5r0v0/workermill-worker:latest";
553
+ // Worker image with manager entrypoint override — private ECR requires auth
554
+ const workerImage = config.workerImage || `${PRIVATE_ECR_REGISTRY}/workermill-dev/worker:latest`;
555
+ if (workerImage.includes(PRIVATE_ECR_REGISTRY)) {
556
+ ensureEcrLogin();
557
+ }
478
558
  dockerArgs.push("--entrypoint", "/bin/bash");
479
559
  dockerArgs.push(workerImage);
480
560
  dockerArgs.push("/app/manager-entrypoint.sh");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workermill/agent",
3
- "version": "0.8.5",
3
+ "version": "0.8.8",
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",