@workermill/agent 0.1.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.
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Remote Agent Configuration
3
+ *
4
+ * Supports two modes:
5
+ * 1. File-based config (~/.workermill/config.json) — for npm-installed CLI
6
+ * 2. Environment variable config (.env.remote) — for bin/remote-agent backward compat
7
+ */
8
+ export interface AgentConfig {
9
+ apiUrl: string;
10
+ apiKey: string;
11
+ agentId: string;
12
+ maxWorkers: number;
13
+ pollIntervalMs: number;
14
+ heartbeatIntervalMs: number;
15
+ githubToken: string;
16
+ bitbucketToken: string;
17
+ gitlabToken: string;
18
+ workerImage: string;
19
+ }
20
+ export interface FileConfig {
21
+ apiUrl: string;
22
+ apiKey: string;
23
+ agentId: string;
24
+ maxWorkers: number;
25
+ pollIntervalMs: number;
26
+ heartbeatIntervalMs: number;
27
+ tokens: {
28
+ github: string;
29
+ bitbucket: string;
30
+ gitlab: string;
31
+ };
32
+ workerImage: string;
33
+ setupCompletedAt: string;
34
+ }
35
+ export declare function getConfigDir(): string;
36
+ export declare function getConfigFile(): string;
37
+ export declare function getPidFile(): string;
38
+ export declare function getLogFile(): string;
39
+ /**
40
+ * Load config from ~/.workermill/config.json (CLI mode).
41
+ */
42
+ export declare function loadConfigFromFile(): AgentConfig;
43
+ /**
44
+ * Save config to ~/.workermill/config.json with restricted permissions.
45
+ */
46
+ export declare function saveConfigToFile(fc: FileConfig): void;
47
+ /**
48
+ * Load config from environment variables (backward compat with bin/remote-agent).
49
+ */
50
+ export declare function loadConfig(): AgentConfig;
51
+ /**
52
+ * Find claude binary. Checks PATH, then known install locations.
53
+ */
54
+ export declare function findClaudePath(): string | null;
55
+ export interface PrerequisiteResult {
56
+ name: string;
57
+ ok: boolean;
58
+ detail?: string;
59
+ }
60
+ /**
61
+ * Check all prerequisites and return results (non-exiting version for setup wizard).
62
+ */
63
+ export declare function checkPrerequisites(workerImage?: string): PrerequisiteResult[];
64
+ /**
65
+ * Validate prerequisites (exits on failure — backward compat).
66
+ */
67
+ export declare function validatePrerequisites(): void;
68
+ /**
69
+ * Get system info for agent registration.
70
+ */
71
+ export declare function getSystemInfo(): {
72
+ hostname: string;
73
+ platform: string;
74
+ nodeVersion: string;
75
+ dockerVersion: string;
76
+ claudeVersion: string;
77
+ };
package/dist/config.js ADDED
@@ -0,0 +1,284 @@
1
+ /**
2
+ * Remote Agent Configuration
3
+ *
4
+ * Supports two modes:
5
+ * 1. File-based config (~/.workermill/config.json) — for npm-installed CLI
6
+ * 2. Environment variable config (.env.remote) — for bin/remote-agent backward compat
7
+ */
8
+ import { existsSync, readFileSync, mkdirSync, writeFileSync, chmodSync } from "fs";
9
+ import { execSync } from "child_process";
10
+ import { hostname, homedir } from "os";
11
+ import { join } from "path";
12
+ const CONFIG_DIR = join(homedir(), ".workermill");
13
+ const CONFIG_FILE = join(CONFIG_DIR, "config.json");
14
+ const PID_FILE = join(CONFIG_DIR, "agent.pid");
15
+ const LOG_FILE = join(CONFIG_DIR, "agent.log");
16
+ export function getConfigDir() {
17
+ return CONFIG_DIR;
18
+ }
19
+ export function getConfigFile() {
20
+ return CONFIG_FILE;
21
+ }
22
+ export function getPidFile() {
23
+ return PID_FILE;
24
+ }
25
+ export function getLogFile() {
26
+ return LOG_FILE;
27
+ }
28
+ /**
29
+ * Load config from ~/.workermill/config.json (CLI mode).
30
+ */
31
+ export function loadConfigFromFile() {
32
+ if (!existsSync(CONFIG_FILE)) {
33
+ console.error("No config found. Run 'workermill-agent setup' first.");
34
+ process.exit(1);
35
+ }
36
+ let raw;
37
+ try {
38
+ raw = readFileSync(CONFIG_FILE, "utf-8");
39
+ }
40
+ catch {
41
+ console.error("Failed to read config file:", CONFIG_FILE);
42
+ process.exit(1);
43
+ }
44
+ let fc;
45
+ try {
46
+ fc = JSON.parse(raw);
47
+ }
48
+ catch {
49
+ console.error("Config file is corrupted. Re-run 'workermill-agent setup'.");
50
+ process.exit(1);
51
+ }
52
+ if (!fc.apiUrl || !fc.apiKey) {
53
+ console.error("Config file is missing required fields (apiUrl, apiKey). Re-run 'workermill-agent setup'.");
54
+ process.exit(1);
55
+ }
56
+ // Migrate any stale image URLs to current default
57
+ const defaultImage = "public.ecr.aws/a7k5r0v0/workermill-worker:latest";
58
+ let workerImage = fc.workerImage || defaultImage;
59
+ if (workerImage.includes("jarod1/") || !workerImage.includes("workermill-worker")) {
60
+ workerImage = defaultImage;
61
+ fc.workerImage = workerImage;
62
+ try {
63
+ writeFileSync(CONFIG_FILE, JSON.stringify(fc, null, 2), "utf-8");
64
+ }
65
+ catch { /* best effort */ }
66
+ }
67
+ return {
68
+ apiUrl: fc.apiUrl,
69
+ apiKey: fc.apiKey,
70
+ agentId: fc.agentId,
71
+ maxWorkers: fc.maxWorkers || 4,
72
+ pollIntervalMs: fc.pollIntervalMs || 5000,
73
+ heartbeatIntervalMs: fc.heartbeatIntervalMs || 30000,
74
+ githubToken: fc.tokens?.github || "",
75
+ bitbucketToken: fc.tokens?.bitbucket || "",
76
+ gitlabToken: fc.tokens?.gitlab || "",
77
+ workerImage,
78
+ };
79
+ }
80
+ /**
81
+ * Save config to ~/.workermill/config.json with restricted permissions.
82
+ */
83
+ export function saveConfigToFile(fc) {
84
+ if (!existsSync(CONFIG_DIR)) {
85
+ mkdirSync(CONFIG_DIR, { recursive: true });
86
+ }
87
+ writeFileSync(CONFIG_FILE, JSON.stringify(fc, null, 2), "utf-8");
88
+ // Restrict permissions (owner-only read/write)
89
+ try {
90
+ chmodSync(CONFIG_FILE, 0o600);
91
+ }
92
+ catch {
93
+ // chmod may not work on Windows, that's OK
94
+ }
95
+ }
96
+ /**
97
+ * Load config from environment variables (backward compat with bin/remote-agent).
98
+ */
99
+ export function loadConfig() {
100
+ const apiUrl = process.env.WORKERMILL_API_URL;
101
+ const apiKey = process.env.WORKERMILL_API_KEY;
102
+ if (!apiUrl) {
103
+ console.error("WORKERMILL_API_URL is required in .env.remote");
104
+ process.exit(1);
105
+ }
106
+ if (!apiKey) {
107
+ console.error("WORKERMILL_API_KEY is required in .env.remote");
108
+ console.error("Get your API key from Settings > Integrations on the WorkerMill dashboard.");
109
+ process.exit(1);
110
+ }
111
+ return {
112
+ apiUrl: apiUrl.replace(/\/$/, ""), // Strip trailing slash
113
+ apiKey,
114
+ agentId: process.env.AGENT_ID || `agent-${hostname()}`,
115
+ maxWorkers: parseInt(process.env.MAX_WORKERS || "4", 10),
116
+ pollIntervalMs: parseInt(process.env.POLL_INTERVAL_MS || "5000", 10),
117
+ heartbeatIntervalMs: parseInt(process.env.HEARTBEAT_INTERVAL_MS || "30000", 10),
118
+ githubToken: process.env.GITHUB_TOKEN || "",
119
+ bitbucketToken: process.env.BITBUCKET_TOKEN || "",
120
+ gitlabToken: process.env.GITLAB_TOKEN || "",
121
+ workerImage: process.env.WORKER_IMAGE || "workermill-worker:local",
122
+ };
123
+ }
124
+ /**
125
+ * Find claude binary. Checks PATH, then known install locations.
126
+ */
127
+ export function findClaudePath() {
128
+ const isWin = process.platform === "win32";
129
+ const which = isWin ? "where" : "which";
130
+ // Check PATH first
131
+ try {
132
+ execSync(`${which} claude`, { stdio: "ignore", timeout: 10000 });
133
+ return "claude";
134
+ }
135
+ catch { /* not in PATH */ }
136
+ const candidates = [];
137
+ if (isWin) {
138
+ candidates.push(join(process.env.ProgramFiles || "C:\\Program Files", "ClaudeCode", "claude.exe"), join(process.env.LOCALAPPDATA || "", "Programs", "ClaudeCode", "claude.exe"), join(homedir(), "AppData", "Local", "Programs", "ClaudeCode", "claude.exe"), join(homedir(), ".local", "bin", "claude.exe"));
139
+ }
140
+ else {
141
+ candidates.push(join(homedir(), ".local", "bin", "claude"), "/opt/homebrew/bin/claude", "/usr/local/bin/claude");
142
+ }
143
+ for (const candidate of candidates) {
144
+ if (candidate && existsSync(candidate))
145
+ return candidate;
146
+ }
147
+ return null;
148
+ }
149
+ /**
150
+ * Check all prerequisites and return results (non-exiting version for setup wizard).
151
+ */
152
+ export function checkPrerequisites(workerImage) {
153
+ const results = [];
154
+ const image = workerImage || "public.ecr.aws/a7k5r0v0/workermill-worker:latest";
155
+ // Docker
156
+ try {
157
+ const version = execSync("docker version --format {{.Server.Version}}", {
158
+ encoding: "utf-8",
159
+ timeout: 10000,
160
+ }).trim();
161
+ results.push({ name: "Docker", ok: true, detail: version });
162
+ }
163
+ catch {
164
+ results.push({ name: "Docker", ok: false, detail: "Not running or not installed" });
165
+ }
166
+ // Claude CLI (search known install locations, not just PATH)
167
+ const claudePath = findClaudePath();
168
+ if (claudePath) {
169
+ try {
170
+ const version = execSync(`"${claudePath}" --version`, { encoding: "utf-8", timeout: 10000 }).trim();
171
+ results.push({ name: "Claude CLI", ok: true, detail: version });
172
+ }
173
+ catch {
174
+ results.push({ name: "Claude CLI", ok: true, detail: claudePath });
175
+ }
176
+ }
177
+ else {
178
+ results.push({ name: "Claude CLI", ok: false, detail: "Not installed" });
179
+ }
180
+ // Claude credentials
181
+ const home = homedir();
182
+ const credsPath = join(home, ".claude", ".credentials.json");
183
+ if (existsSync(credsPath)) {
184
+ results.push({ name: "Claude auth", ok: true, detail: "Credentials found" });
185
+ }
186
+ else {
187
+ results.push({ name: "Claude auth", ok: false, detail: "Run 'claude' and complete sign-in" });
188
+ }
189
+ // Node.js version
190
+ const nodeVersion = process.version;
191
+ const major = parseInt(nodeVersion.slice(1).split(".")[0], 10);
192
+ if (major >= 20) {
193
+ results.push({ name: "Node.js", ok: true, detail: nodeVersion });
194
+ }
195
+ else {
196
+ results.push({ name: "Node.js", ok: false, detail: `${nodeVersion} (need >= 20)` });
197
+ }
198
+ // Worker image
199
+ try {
200
+ execSync(`docker image inspect ${image}`, { stdio: "ignore", timeout: 10000 });
201
+ results.push({ name: "Worker image", ok: true, detail: image });
202
+ }
203
+ catch {
204
+ results.push({ name: "Worker image", ok: false, detail: `'${image}' not found` });
205
+ }
206
+ return results;
207
+ }
208
+ /**
209
+ * Validate prerequisites (exits on failure — backward compat).
210
+ */
211
+ export function validatePrerequisites() {
212
+ // Check Docker
213
+ try {
214
+ execSync("docker version", { stdio: "ignore" });
215
+ }
216
+ catch {
217
+ console.error("Docker is not available. Please install Docker and ensure it's running.");
218
+ process.exit(1);
219
+ }
220
+ // Check worker image (use env-configured image or default)
221
+ const image = process.env.WORKER_IMAGE || "workermill-worker:local";
222
+ try {
223
+ execSync(`docker image inspect ${image}`, { stdio: "ignore" });
224
+ }
225
+ catch {
226
+ console.error(`Worker image '${image}' not found.`);
227
+ if (image === "workermill-worker:local") {
228
+ console.error("Build it with: ./bin/local-workermill build-worker");
229
+ }
230
+ else {
231
+ console.error(`Pull it with: docker pull ${image}`);
232
+ }
233
+ process.exit(1);
234
+ }
235
+ // Check Claude CLI
236
+ if (!findClaudePath()) {
237
+ console.error("Claude CLI is not installed.");
238
+ console.error("Install it: curl -fsSL https://claude.ai/install.sh | bash");
239
+ process.exit(1);
240
+ }
241
+ // Check Claude credentials
242
+ const home = homedir();
243
+ const credsPath = join(home, ".claude", ".credentials.json");
244
+ if (!existsSync(credsPath)) {
245
+ console.error("Claude credentials not found.");
246
+ console.error("Run 'claude' and complete the sign-in flow to authenticate.");
247
+ process.exit(1);
248
+ }
249
+ }
250
+ /**
251
+ * Get system info for agent registration.
252
+ */
253
+ export function getSystemInfo() {
254
+ let dockerVersion = "unknown";
255
+ try {
256
+ dockerVersion = execSync("docker version --format {{.Server.Version}}", {
257
+ encoding: "utf-8",
258
+ timeout: 10000,
259
+ }).trim();
260
+ }
261
+ catch {
262
+ /* ignore */
263
+ }
264
+ let claudeVersion = "unknown";
265
+ const claudeBin = findClaudePath();
266
+ if (claudeBin) {
267
+ try {
268
+ claudeVersion = execSync(`"${claudeBin}" --version`, {
269
+ encoding: "utf-8",
270
+ timeout: 10000,
271
+ }).trim();
272
+ }
273
+ catch {
274
+ /* ignore */
275
+ }
276
+ }
277
+ return {
278
+ hostname: hostname(),
279
+ platform: process.platform,
280
+ nodeVersion: process.version,
281
+ dockerVersion,
282
+ claudeVersion,
283
+ };
284
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * WorkerMill Remote Agent
3
+ *
4
+ * Importable module for starting the agent programmatically.
5
+ * Can also be run directly via `bin/remote-agent` (backward compat with dotenv).
6
+ */
7
+ import type { AgentConfig } from "./config.js";
8
+ export { loadConfig, loadConfigFromFile, validatePrerequisites, getSystemInfo, findClaudePath } from "./config.js";
9
+ export type { AgentConfig } from "./config.js";
10
+ /**
11
+ * Start the remote agent with the given config.
12
+ * Returns a cleanup function to stop the agent.
13
+ */
14
+ export declare function startAgent(config: AgentConfig): Promise<() => Promise<void>>;
package/dist/index.js ADDED
@@ -0,0 +1,104 @@
1
+ /**
2
+ * WorkerMill Remote Agent
3
+ *
4
+ * Importable module for starting the agent programmatically.
5
+ * Can also be run directly via `bin/remote-agent` (backward compat with dotenv).
6
+ */
7
+ import chalk from "chalk";
8
+ import { initApi, api } from "./api.js";
9
+ import { startPolling, startHeartbeat } from "./poller.js";
10
+ import { stopAll } from "./spawner.js";
11
+ export { loadConfig, loadConfigFromFile, validatePrerequisites, getSystemInfo, findClaudePath } from "./config.js";
12
+ /**
13
+ * Start the remote agent with the given config.
14
+ * Returns a cleanup function to stop the agent.
15
+ */
16
+ export async function startAgent(config) {
17
+ console.log();
18
+ console.log(chalk.bold.cyan(" WorkerMill Remote Agent"));
19
+ console.log(chalk.dim(" ─────────────────────────────────────"));
20
+ console.log();
21
+ // Initialize API client
22
+ initApi(config.apiUrl, config.apiKey);
23
+ // Verify connectivity
24
+ try {
25
+ const configResponse = await api.get("/api/agent/config");
26
+ // Override maxWorkers from cloud settings if available
27
+ const cloudMaxWorkers = configResponse.data.maxConcurrentWorkers;
28
+ if (cloudMaxWorkers && typeof cloudMaxWorkers === "number") {
29
+ config.maxWorkers = cloudMaxWorkers;
30
+ }
31
+ console.log(` ${chalk.green("●")} Connected to ${chalk.cyan(config.apiUrl)}`);
32
+ console.log(` ${chalk.dim("Agent:")} ${config.agentId}`);
33
+ console.log(` ${chalk.dim("Workers:")} ${chalk.yellow(String(config.maxWorkers))} parallel`);
34
+ console.log(` ${chalk.dim("Image:")} ${config.workerImage}`);
35
+ console.log(` ${chalk.dim("SCM:")} ${configResponse.data.scmProvider}`);
36
+ console.log(` ${chalk.dim("Model:")} ${chalk.yellow(configResponse.data.defaultWorkerModel)}`);
37
+ console.log();
38
+ }
39
+ catch (error) {
40
+ const err = error;
41
+ if (err.response?.status === 401) {
42
+ throw new Error("Authentication failed. Check your API key.");
43
+ }
44
+ else {
45
+ throw new Error(`Failed to connect to WorkerMill API: ${err.message || String(error)}`);
46
+ }
47
+ }
48
+ // Register agent
49
+ try {
50
+ await api.post("/api/agent/register", {
51
+ agentId: config.agentId,
52
+ maxWorkers: config.maxWorkers,
53
+ });
54
+ }
55
+ catch {
56
+ // Registration is best-effort, don't fail startup
57
+ }
58
+ // Start polling and heartbeat loops
59
+ startPolling(config);
60
+ startHeartbeat(config);
61
+ console.log(chalk.dim(" ─────────────────────────────────────"));
62
+ console.log(` ${chalk.green("●")} Agent is running. ${chalk.dim("Press Ctrl+C to stop.")}`);
63
+ console.log();
64
+ // Return cleanup function
65
+ return async () => {
66
+ console.log();
67
+ console.log(chalk.dim(" Shutting down..."));
68
+ try {
69
+ await api.post("/api/agent/deregister", { agentId: config.agentId });
70
+ }
71
+ catch {
72
+ // Best-effort deregister
73
+ }
74
+ await stopAll();
75
+ console.log(` ${chalk.red("●")} Agent stopped.`);
76
+ };
77
+ }
78
+ // Direct execution support (for bin/remote-agent backward compat)
79
+ // Only runs when this file is the main module
80
+ const isDirectRun = typeof process !== "undefined" &&
81
+ process.argv[1] &&
82
+ (process.argv[1].endsWith("/index.ts") || process.argv[1].endsWith("/index.js"));
83
+ if (isDirectRun) {
84
+ // Dynamic import dotenv for backward compat (not a dependency in published package)
85
+ try {
86
+ await import("dotenv/config");
87
+ }
88
+ catch {
89
+ // dotenv not available in published package — that's fine
90
+ }
91
+ const { loadConfig, validatePrerequisites: validate } = await import("./config.js");
92
+ const config = loadConfig();
93
+ validate();
94
+ console.log(chalk.dim(" Prerequisites validated."));
95
+ const cleanup = await startAgent(config);
96
+ process.on("SIGINT", async () => {
97
+ await cleanup();
98
+ process.exit(0);
99
+ });
100
+ process.on("SIGTERM", async () => {
101
+ await cleanup();
102
+ process.exit(0);
103
+ });
104
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Remote Agent Planner
3
+ *
4
+ * Fetches the planning prompt from the cloud API, runs it through
5
+ * Claude CLI locally (using the customer's Claude Max subscription),
6
+ * and posts the raw output back for server-side validation.
7
+ *
8
+ * Logs are streamed to the cloud dashboard in real-time so the user
9
+ * sees the same planning progress as cloud mode.
10
+ */
11
+ import { type AgentConfig } from "./config.js";
12
+ export interface PlanningTask {
13
+ id: string;
14
+ summary: string;
15
+ }
16
+ /**
17
+ * Run planning for a task: fetch prompt, execute Claude CLI, post result.
18
+ */
19
+ export declare function planTask(task: PlanningTask, config: AgentConfig): Promise<boolean>;