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.
- package/dist/index.js +281 -70
- 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
|
-
{
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
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(
|
|
76
|
+
const choiceStr = await ask(rl, chalk.dim(` Choose (1-${providers.length}): `));
|
|
48
77
|
const choice = parseInt(choiceStr.trim(), 10) - 1;
|
|
49
|
-
const selected =
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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 (
|
|
64
|
-
const
|
|
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
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
91
|
-
continue;
|
|
219
|
+
return;
|
|
92
220
|
}
|
|
221
|
+
} catch {
|
|
222
|
+
continue;
|
|
93
223
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
116
|
-
default:
|
|
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.
|
|
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
|
}
|