workermill 0.3.3 → 0.4.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.
Files changed (2) hide show
  1. package/dist/index.js +281 -70
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -24,101 +24,312 @@ import readline from "readline";
24
24
  import { execSync } from "child_process";
25
25
  import chalk from "chalk";
26
26
  var PROVIDERS = [
27
- { name: "ollama", display: "Ollama (local, no API key needed)", needsKey: false, defaultModel: "qwen3-coder:30b" },
28
- { name: "anthropic", display: "Anthropic (Claude)", needsKey: true, defaultModel: "claude-sonnet-4-6", envVar: "ANTHROPIC_API_KEY" },
29
- { name: "openai", display: "OpenAI (GPT)", needsKey: true, defaultModel: "gpt-5.4", envVar: "OPENAI_API_KEY" },
30
- { name: "google", display: "Google (Gemini)", needsKey: true, defaultModel: "gemini-3.1-pro", envVar: "GOOGLE_GENERATIVE_AI_API_KEY" }
27
+ {
28
+ name: "ollama",
29
+ display: "Ollama (local, no API key)",
30
+ needsKey: false,
31
+ models: [
32
+ { id: "qwen3-coder:30b", label: "Qwen 3 Coder 30B (recommended)" },
33
+ { id: "qwen2.5-coder:32b", label: "Qwen 2.5 Coder 32B" }
34
+ ]
35
+ },
36
+ {
37
+ name: "anthropic",
38
+ display: "Anthropic (Claude)",
39
+ needsKey: true,
40
+ envVar: "ANTHROPIC_API_KEY",
41
+ models: [
42
+ { id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6 (recommended)" },
43
+ { id: "claude-opus-4-6", label: "Claude Opus 4.6 (most powerful)" }
44
+ ]
45
+ },
46
+ {
47
+ name: "openai",
48
+ display: "OpenAI",
49
+ needsKey: true,
50
+ envVar: "OPENAI_API_KEY",
51
+ models: [
52
+ { id: "gpt-5.2-codex", label: "GPT-5.2 Codex (built for code)" },
53
+ { id: "gpt-5", label: "GPT-5 (general flagship)" }
54
+ ]
55
+ },
56
+ {
57
+ name: "google",
58
+ display: "Google (Gemini)",
59
+ needsKey: true,
60
+ envVar: "GOOGLE_GENERATIVE_AI_API_KEY",
61
+ models: [
62
+ { id: "gemini-2.5-pro", label: "Gemini 2.5 Pro (most powerful)" },
63
+ { id: "gemini-2.5-flash", label: "Gemini 2.5 Flash (fast, good value)" }
64
+ ]
65
+ }
31
66
  ];
32
67
  function ask(rl, question) {
33
68
  return new Promise((resolve) => rl.question(question, resolve));
34
69
  }
35
- async function runSetup() {
36
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
37
- console.log();
38
- console.log(chalk.bold(" WorkerMill CLI") + chalk.dim(" \u2014 AI coding agent"));
39
- console.log();
40
- console.log(chalk.dim(" No provider configured. Let's set one up."));
41
- console.log();
42
- console.log(" Provider:");
43
- PROVIDERS.forEach((p, i) => {
70
+ async function pickProvider(rl, label, providers) {
71
+ console.log(` ${chalk.bold(label)}:`);
72
+ providers.forEach((p, i) => {
44
73
  console.log(` ${chalk.cyan(`${i + 1}`)}. ${p.display}`);
45
74
  });
46
75
  console.log();
47
- const choiceStr = await ask(rl, chalk.dim(" Choose (1-4): "));
76
+ const choiceStr = await ask(rl, chalk.dim(` Choose (1-${providers.length}): `));
48
77
  const choice = parseInt(choiceStr.trim(), 10) - 1;
49
- const selected = PROVIDERS[choice] || PROVIDERS[0];
78
+ const selected = providers[choice] || providers[0];
79
+ console.log(chalk.dim(` \u2192 ${selected.display}`));
80
+ return selected;
81
+ }
82
+ async function pickModel(rl, provider) {
50
83
  console.log();
51
- console.log(chalk.dim(` Selected: ${selected.display}`));
52
- const providerConfig = { model: selected.defaultModel };
53
- if (selected.needsKey) {
54
- const envValue = selected.envVar ? process.env[selected.envVar] : void 0;
55
- if (envValue) {
56
- console.log(chalk.green(` \u2713 Found ${selected.envVar} in environment`));
57
- providerConfig.apiKey = `{env:${selected.envVar}}`;
58
- } else {
59
- const key = await ask(rl, chalk.dim(` API key: `));
60
- providerConfig.apiKey = key.trim();
84
+ provider.models.forEach((m, i) => {
85
+ console.log(` ${chalk.cyan(`${i + 1}`)}. ${m.label}`);
86
+ });
87
+ console.log(` ${chalk.cyan(`${provider.models.length + 1}`)}. Custom model`);
88
+ console.log();
89
+ const choiceStr = await ask(rl, chalk.dim(` Choose (1-${provider.models.length + 1}): `));
90
+ const choice = parseInt(choiceStr.trim(), 10) - 1;
91
+ if (choice >= 0 && choice < provider.models.length) {
92
+ const model2 = provider.models[choice].id;
93
+ console.log(chalk.dim(` \u2192 ${model2}`));
94
+ return model2;
95
+ }
96
+ const custom = await ask(rl, chalk.dim(" Model name: "));
97
+ const model = custom.trim() || provider.models[0].id;
98
+ console.log(chalk.dim(` \u2192 ${model}`));
99
+ return model;
100
+ }
101
+ function maskKey(key) {
102
+ if (key.length <= 12) return "\u2022".repeat(key.length);
103
+ return key.slice(0, 6) + "\u2022".repeat(Math.min(key.length - 10, 30)) + key.slice(-4);
104
+ }
105
+ async function getApiKey(rl, provider, existingKeys) {
106
+ if (!provider.needsKey) return void 0;
107
+ if (existingKeys.has(provider.name)) {
108
+ console.log(chalk.green(` \u2713 Reusing ${provider.display} API key from earlier`));
109
+ return existingKeys.get(provider.name);
110
+ }
111
+ const envValue = provider.envVar ? process.env[provider.envVar] : void 0;
112
+ if (envValue) {
113
+ console.log(chalk.green(` \u2713 Found ${provider.envVar} in environment`));
114
+ const key2 = `{env:${provider.envVar}}`;
115
+ existingKeys.set(provider.name, key2);
116
+ return key2;
117
+ }
118
+ const key = await readKeyMasked(rl, chalk.dim(` ${provider.display} API key: `));
119
+ const trimmed = key.trim();
120
+ existingKeys.set(provider.name, trimmed);
121
+ return trimmed;
122
+ }
123
+ function readKeyMasked(rl, prompt) {
124
+ return new Promise((resolve) => {
125
+ let buffer = "";
126
+ let revealed = false;
127
+ process.stdout.write(prompt);
128
+ const wasRaw = process.stdin.isRaw;
129
+ if (process.stdin.isTTY) {
130
+ process.stdin.setRawMode(true);
61
131
  }
132
+ process.stdin.resume();
133
+ const redraw = () => {
134
+ process.stdout.write(`\r\x1B[K${prompt}`);
135
+ if (revealed) {
136
+ process.stdout.write(buffer);
137
+ } else {
138
+ process.stdout.write(maskKey(buffer));
139
+ }
140
+ };
141
+ const onData = (data) => {
142
+ const str = data.toString();
143
+ for (const ch of str) {
144
+ const code = ch.charCodeAt(0);
145
+ if (code === 13 || code === 10) {
146
+ process.stdin.removeListener("data", onData);
147
+ if (process.stdin.isTTY) {
148
+ process.stdin.setRawMode(wasRaw ?? false);
149
+ }
150
+ process.stdout.write(`\r\x1B[K${prompt}`);
151
+ if (buffer.length > 0) {
152
+ process.stdout.write(chalk.green(maskKey(buffer)));
153
+ }
154
+ process.stdout.write("\n");
155
+ resolve(buffer);
156
+ return;
157
+ }
158
+ if (code === 9) {
159
+ revealed = !revealed;
160
+ redraw();
161
+ continue;
162
+ }
163
+ if (code === 3) {
164
+ process.stdin.removeListener("data", onData);
165
+ if (process.stdin.isTTY) {
166
+ process.stdin.setRawMode(wasRaw ?? false);
167
+ }
168
+ process.stdout.write("\n");
169
+ resolve("");
170
+ return;
171
+ }
172
+ if (code === 127 || code === 8) {
173
+ if (buffer.length > 0) {
174
+ buffer = buffer.slice(0, -1);
175
+ redraw();
176
+ }
177
+ continue;
178
+ }
179
+ if (code === 21) {
180
+ buffer = "";
181
+ redraw();
182
+ continue;
183
+ }
184
+ if (code >= 32) {
185
+ buffer += ch;
186
+ redraw();
187
+ }
188
+ }
189
+ };
190
+ process.stdin.on("data", onData);
191
+ });
192
+ }
193
+ async function configureOllama(providerConfig) {
194
+ const hostsToTry = ["http://localhost:11434"];
195
+ try {
196
+ const gateway = execSync("ip route show default 2>/dev/null | awk '{print $3}'", { encoding: "utf-8" }).trim();
197
+ if (gateway) hostsToTry.push(`http://${gateway}:11434`);
198
+ } catch {
62
199
  }
63
- if (selected.name === "ollama") {
64
- const hostsToTry = ["http://localhost:11434"];
200
+ if (process.env.OLLAMA_HOST) {
201
+ const envHost = process.env.OLLAMA_HOST.startsWith("http") ? process.env.OLLAMA_HOST : `http://${process.env.OLLAMA_HOST}`;
202
+ hostsToTry.unshift(envHost);
203
+ }
204
+ providerConfig.contextLength = 65536;
205
+ for (const host of hostsToTry) {
65
206
  try {
66
- const gateway = execSync("ip route show default 2>/dev/null | awk '{print $3}'", { encoding: "utf-8" }).trim();
67
- if (gateway) {
68
- hostsToTry.push(`http://${gateway}:11434`);
69
- }
70
- } catch {
71
- }
72
- if (process.env.OLLAMA_HOST) {
73
- const envHost = process.env.OLLAMA_HOST.startsWith("http") ? process.env.OLLAMA_HOST : `http://${process.env.OLLAMA_HOST}`;
74
- hostsToTry.unshift(envHost);
75
- }
76
- let connectedHost = null;
77
- let models = [];
78
- for (const host of hostsToTry) {
79
- try {
80
- const controller = new AbortController();
81
- const timeout = setTimeout(() => controller.abort(), 3e3);
82
- const response = await globalThis.fetch(`${host}/api/tags`, { signal: controller.signal });
83
- clearTimeout(timeout);
84
- if (response.ok) {
85
- const data = await response.json();
86
- connectedHost = host;
87
- models = data.models || [];
88
- break;
207
+ const controller = new AbortController();
208
+ const timeout = setTimeout(() => controller.abort(), 3e3);
209
+ const response = await globalThis.fetch(`${host}/api/tags`, { signal: controller.signal });
210
+ clearTimeout(timeout);
211
+ if (response.ok) {
212
+ const data = await response.json();
213
+ providerConfig.host = host;
214
+ console.log(chalk.green(` \u2713 Connected to Ollama at ${host}`));
215
+ const models = data.models || [];
216
+ if (models.length > 0) {
217
+ console.log(chalk.dim(` Available: ${models.map((m) => m.name).join(", ")}`));
89
218
  }
90
- } catch {
91
- continue;
219
+ return;
92
220
  }
221
+ } catch {
222
+ continue;
93
223
  }
94
- providerConfig.contextLength = 65536;
95
- if (connectedHost) {
96
- providerConfig.host = connectedHost;
97
- console.log(chalk.green(` \u2713 Connected to Ollama at ${connectedHost}`));
98
- console.log(chalk.dim(` Context window: ${providerConfig.contextLength.toLocaleString()} tokens`));
99
- if (models.length > 0) {
100
- console.log(chalk.dim(` Available models: ${models.map((m) => m.name).join(", ")}`));
101
- }
102
- } else {
103
- providerConfig.host = "http://localhost:11434";
104
- console.log(chalk.yellow(" \u26A0 Could not connect to Ollama. Make sure it's running."));
105
- console.log(chalk.dim(" Tried: " + hostsToTry.join(", ")));
224
+ }
225
+ providerConfig.host = "http://localhost:11434";
226
+ console.log(chalk.yellow(" \u26A0 Could not connect to Ollama. Make sure it's running."));
227
+ }
228
+ async function runSetup() {
229
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
230
+ const apiKeys = /* @__PURE__ */ new Map();
231
+ console.log();
232
+ console.log(chalk.bold(" WorkerMill CLI") + chalk.dim(" \u2014 AI coding agent"));
233
+ console.log();
234
+ console.log(chalk.dim(" WorkerMill uses three roles. Each can use a different provider."));
235
+ console.log(chalk.dim(" Workers build code, the planner designs the plan, the reviewer checks quality."));
236
+ console.log();
237
+ console.log(chalk.hex("#D77757").bold(" \u2460 Workers") + chalk.dim(" \u2014 expert personas that write code"));
238
+ console.log();
239
+ const workerProvider = await pickProvider(rl, "Provider for workers", PROVIDERS);
240
+ const workerModel = await pickModel(rl, workerProvider);
241
+ const workerKey = await getApiKey(rl, workerProvider, apiKeys);
242
+ const workerConfig = { model: workerModel };
243
+ if (workerKey) workerConfig.apiKey = workerKey;
244
+ if (workerProvider.name === "ollama") await configureOllama(workerConfig);
245
+ console.log();
246
+ console.log(chalk.hex("#D77757").bold(" \u2461 Planner") + chalk.dim(" \u2014 plans stories and validates the approach"));
247
+ console.log();
248
+ const sameForPlanner = await ask(rl, chalk.dim(` Same as workers (${workerProvider.display} / ${workerModel})? [Y/n] `));
249
+ let plannerProviderName;
250
+ let plannerModel;
251
+ if (sameForPlanner.trim().toLowerCase() === "n") {
252
+ const plannerProvider = await pickProvider(rl, "Provider for planner", PROVIDERS);
253
+ plannerModel = await pickModel(rl, plannerProvider);
254
+ const plannerKey = await getApiKey(rl, plannerProvider, apiKeys);
255
+ plannerProviderName = plannerProvider.name;
256
+ if (plannerKey && plannerProvider.name !== workerProvider.name) {
106
257
  }
258
+ } else {
259
+ plannerProviderName = workerProvider.name;
260
+ plannerModel = workerModel;
261
+ console.log(chalk.dim(` \u2192 ${workerProvider.display} / ${workerModel}`));
107
262
  }
108
263
  console.log();
109
- const modelOverride = await ask(rl, chalk.dim(` Model (press Enter for ${selected.defaultModel}): `));
110
- if (modelOverride.trim()) {
111
- providerConfig.model = modelOverride.trim();
264
+ console.log(chalk.hex("#D77757").bold(" \u2462 Reviewer") + chalk.dim(" \u2014 tech lead that reviews code quality"));
265
+ console.log();
266
+ const sameForReviewer = await ask(rl, chalk.dim(` Same as workers (${workerProvider.display} / ${workerModel})? [Y/n] `));
267
+ let reviewerProviderName;
268
+ let reviewerModel;
269
+ if (sameForReviewer.trim().toLowerCase() === "n") {
270
+ const reviewerProvider = await pickProvider(rl, "Provider for reviewer", PROVIDERS);
271
+ reviewerModel = await pickModel(rl, reviewerProvider);
272
+ const reviewerKey = await getApiKey(rl, reviewerProvider, apiKeys);
273
+ reviewerProviderName = reviewerProvider.name;
274
+ if (reviewerKey && reviewerProvider.name !== workerProvider.name) {
275
+ }
276
+ } else {
277
+ reviewerProviderName = workerProvider.name;
278
+ reviewerModel = workerModel;
279
+ console.log(chalk.dim(` \u2192 ${workerProvider.display} / ${workerModel}`));
112
280
  }
113
281
  rl.close();
282
+ const providers = {
283
+ [workerProvider.name]: workerConfig
284
+ };
285
+ if (plannerProviderName !== workerProvider.name && !providers[plannerProviderName]) {
286
+ const pProvider = PROVIDERS.find((p) => p.name === plannerProviderName);
287
+ const cfg = { model: plannerModel };
288
+ const key = apiKeys.get(plannerProviderName);
289
+ if (key) cfg.apiKey = key;
290
+ if (pProvider.name === "ollama") await configureOllama(cfg);
291
+ providers[plannerProviderName] = cfg;
292
+ }
293
+ if (reviewerProviderName !== workerProvider.name && !providers[reviewerProviderName]) {
294
+ const rProvider = PROVIDERS.find((p) => p.name === reviewerProviderName);
295
+ const cfg = { model: reviewerModel };
296
+ const key = apiKeys.get(reviewerProviderName);
297
+ if (key) cfg.apiKey = key;
298
+ if (rProvider.name === "ollama") await configureOllama(cfg);
299
+ providers[reviewerProviderName] = cfg;
300
+ }
301
+ const routing = {};
302
+ if (plannerProviderName !== workerProvider.name) {
303
+ routing.planner = plannerProviderName;
304
+ routing.critic = plannerProviderName;
305
+ }
306
+ if (reviewerProviderName !== workerProvider.name) {
307
+ routing.tech_lead = reviewerProviderName;
308
+ }
309
+ if (plannerProviderName === workerProvider.name && plannerModel !== workerModel) {
310
+ const altKey = `${plannerProviderName}_planner`;
311
+ providers[altKey] = { ...providers[plannerProviderName], model: plannerModel };
312
+ routing.planner = altKey;
313
+ routing.critic = altKey;
314
+ }
315
+ if (reviewerProviderName === workerProvider.name && reviewerModel !== workerModel) {
316
+ const altKey = `${reviewerProviderName}_reviewer`;
317
+ providers[altKey] = { ...providers[reviewerProviderName], model: reviewerModel };
318
+ routing.tech_lead = altKey;
319
+ }
114
320
  const config = {
115
- providers: { [selected.name]: providerConfig },
116
- default: selected.name
321
+ providers,
322
+ default: workerProvider.name,
323
+ ...Object.keys(routing).length > 0 ? { routing } : {}
117
324
  };
118
325
  saveConfig(config);
119
326
  console.log();
120
327
  console.log(chalk.green(" \u2713 Config saved to ~/.workermill/cli.json"));
121
328
  console.log();
329
+ console.log(chalk.dim(" Workers: ") + `${workerProvider.name}/${workerModel}`);
330
+ console.log(chalk.dim(" Planner: ") + `${plannerProviderName}/${plannerModel}`);
331
+ console.log(chalk.dim(" Reviewer: ") + `${reviewerProviderName}/${reviewerModel}`);
332
+ console.log();
122
333
  return config;
123
334
  }
124
335
 
@@ -1997,7 +2208,7 @@ function printWelcome(provider, model, workingDir) {
1997
2208
  console.log(dim(" Type ") + white("/help") + dim(" for all commands."));
1998
2209
  console.log();
1999
2210
  }
2000
- var VERSION = "0.3.3";
2211
+ var VERSION = "0.4.0";
2001
2212
  function addSharedOptions(cmd) {
2002
2213
  return cmd.option("--provider <provider>", "Override default provider").option("--model <model>", "Override model").option("--trust", "Skip all tool permission prompts").option("--full-disk", "Allow tools to access files outside working directory");
2003
2214
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "workermill",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
4
4
  "description": "AI coding agent with multi-expert orchestration. Works with any LLM provider.",
5
5
  "type": "module",
6
6
  "bin": {