workermill 0.3.3 → 0.4.1
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.
|
@@ -3162,6 +3162,42 @@ var CostTracker = class {
|
|
|
3162
3162
|
}
|
|
3163
3163
|
};
|
|
3164
3164
|
|
|
3165
|
+
// src/logger.ts
|
|
3166
|
+
init_esm_shims();
|
|
3167
|
+
import fs9 from "fs";
|
|
3168
|
+
import path11 from "path";
|
|
3169
|
+
var LOG_DIR = path11.join(process.cwd(), ".workermill");
|
|
3170
|
+
var LOG_FILE = path11.join(LOG_DIR, "cli.log");
|
|
3171
|
+
var logStream = null;
|
|
3172
|
+
function ensureLogDir() {
|
|
3173
|
+
if (!fs9.existsSync(LOG_DIR)) {
|
|
3174
|
+
fs9.mkdirSync(LOG_DIR, { recursive: true });
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
function getStream() {
|
|
3178
|
+
if (!logStream) {
|
|
3179
|
+
ensureLogDir();
|
|
3180
|
+
logStream = fs9.createWriteStream(LOG_FILE, { flags: "a" });
|
|
3181
|
+
}
|
|
3182
|
+
return logStream;
|
|
3183
|
+
}
|
|
3184
|
+
function timestamp() {
|
|
3185
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
3186
|
+
}
|
|
3187
|
+
function log(level, message, data) {
|
|
3188
|
+
const entry = data ? `[${timestamp()}] ${level}: ${message} ${JSON.stringify(data)}` : `[${timestamp()}] ${level}: ${message}`;
|
|
3189
|
+
getStream().write(entry + "\n");
|
|
3190
|
+
}
|
|
3191
|
+
function info(message, data) {
|
|
3192
|
+
log("INFO", message, data);
|
|
3193
|
+
}
|
|
3194
|
+
function error(message, data) {
|
|
3195
|
+
log("ERROR", message, data);
|
|
3196
|
+
}
|
|
3197
|
+
function debug(message, data) {
|
|
3198
|
+
log("DEBUG", message, data);
|
|
3199
|
+
}
|
|
3200
|
+
|
|
3165
3201
|
export {
|
|
3166
3202
|
__dirname,
|
|
3167
3203
|
init_esm_shims,
|
|
@@ -3172,5 +3208,8 @@ export {
|
|
|
3172
3208
|
createModel,
|
|
3173
3209
|
killActiveProcess,
|
|
3174
3210
|
createToolDefinitions,
|
|
3175
|
-
CostTracker
|
|
3211
|
+
CostTracker,
|
|
3212
|
+
info,
|
|
3213
|
+
error,
|
|
3214
|
+
debug
|
|
3176
3215
|
};
|
package/dist/index.js
CHANGED
|
@@ -4,12 +4,15 @@ import {
|
|
|
4
4
|
buildOllamaOptions,
|
|
5
5
|
createModel,
|
|
6
6
|
createToolDefinitions,
|
|
7
|
+
debug,
|
|
8
|
+
error,
|
|
7
9
|
getProviderForPersona,
|
|
10
|
+
info,
|
|
8
11
|
init_esm_shims,
|
|
9
12
|
killActiveProcess,
|
|
10
13
|
loadConfig,
|
|
11
14
|
saveConfig
|
|
12
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-77CAVW3Y.js";
|
|
13
16
|
|
|
14
17
|
// src/index.ts
|
|
15
18
|
init_esm_shims();
|
|
@@ -24,101 +27,312 @@ import readline from "readline";
|
|
|
24
27
|
import { execSync } from "child_process";
|
|
25
28
|
import chalk from "chalk";
|
|
26
29
|
var PROVIDERS = [
|
|
27
|
-
{
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
{
|
|
31
|
+
name: "ollama",
|
|
32
|
+
display: "Ollama (local, no API key)",
|
|
33
|
+
needsKey: false,
|
|
34
|
+
models: [
|
|
35
|
+
{ id: "qwen3-coder:30b", label: "Qwen 3 Coder 30B (recommended)" },
|
|
36
|
+
{ id: "qwen2.5-coder:32b", label: "Qwen 2.5 Coder 32B" }
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: "anthropic",
|
|
41
|
+
display: "Anthropic (Claude)",
|
|
42
|
+
needsKey: true,
|
|
43
|
+
envVar: "ANTHROPIC_API_KEY",
|
|
44
|
+
models: [
|
|
45
|
+
{ id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6 (recommended)" },
|
|
46
|
+
{ id: "claude-opus-4-6", label: "Claude Opus 4.6 (most powerful)" }
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: "openai",
|
|
51
|
+
display: "OpenAI",
|
|
52
|
+
needsKey: true,
|
|
53
|
+
envVar: "OPENAI_API_KEY",
|
|
54
|
+
models: [
|
|
55
|
+
{ id: "gpt-5.4", label: "GPT-5.4 (latest flagship)" },
|
|
56
|
+
{ id: "gpt-5.3-codex", label: "GPT-5.3 Codex (built for code)" }
|
|
57
|
+
]
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "google",
|
|
61
|
+
display: "Google (Gemini)",
|
|
62
|
+
needsKey: true,
|
|
63
|
+
envVar: "GOOGLE_GENERATIVE_AI_API_KEY",
|
|
64
|
+
models: [
|
|
65
|
+
{ id: "gemini-2.5-pro", label: "Gemini 2.5 Pro (most powerful)" },
|
|
66
|
+
{ id: "gemini-2.5-flash", label: "Gemini 2.5 Flash (fast, good value)" }
|
|
67
|
+
]
|
|
68
|
+
}
|
|
31
69
|
];
|
|
32
70
|
function ask(rl, question) {
|
|
33
71
|
return new Promise((resolve) => rl.question(question, resolve));
|
|
34
72
|
}
|
|
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) => {
|
|
73
|
+
async function pickProvider(rl, label, providers) {
|
|
74
|
+
console.log(` ${chalk.bold(label)}:`);
|
|
75
|
+
providers.forEach((p, i) => {
|
|
44
76
|
console.log(` ${chalk.cyan(`${i + 1}`)}. ${p.display}`);
|
|
45
77
|
});
|
|
46
78
|
console.log();
|
|
47
|
-
const choiceStr = await ask(rl, chalk.dim(
|
|
79
|
+
const choiceStr = await ask(rl, chalk.dim(` Choose (1-${providers.length}): `));
|
|
48
80
|
const choice = parseInt(choiceStr.trim(), 10) - 1;
|
|
49
|
-
const selected =
|
|
81
|
+
const selected = providers[choice] || providers[0];
|
|
82
|
+
console.log(chalk.dim(` \u2192 ${selected.display}`));
|
|
83
|
+
return selected;
|
|
84
|
+
}
|
|
85
|
+
async function pickModel(rl, provider) {
|
|
50
86
|
console.log();
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
87
|
+
provider.models.forEach((m, i) => {
|
|
88
|
+
console.log(` ${chalk.cyan(`${i + 1}`)}. ${m.label}`);
|
|
89
|
+
});
|
|
90
|
+
console.log(` ${chalk.cyan(`${provider.models.length + 1}`)}. Custom model`);
|
|
91
|
+
console.log();
|
|
92
|
+
const choiceStr = await ask(rl, chalk.dim(` Choose (1-${provider.models.length + 1}): `));
|
|
93
|
+
const choice = parseInt(choiceStr.trim(), 10) - 1;
|
|
94
|
+
if (choice >= 0 && choice < provider.models.length) {
|
|
95
|
+
const model2 = provider.models[choice].id;
|
|
96
|
+
console.log(chalk.dim(` \u2192 ${model2}`));
|
|
97
|
+
return model2;
|
|
98
|
+
}
|
|
99
|
+
const custom = await ask(rl, chalk.dim(" Model name: "));
|
|
100
|
+
const model = custom.trim() || provider.models[0].id;
|
|
101
|
+
console.log(chalk.dim(` \u2192 ${model}`));
|
|
102
|
+
return model;
|
|
103
|
+
}
|
|
104
|
+
function maskKey(key) {
|
|
105
|
+
if (key.length <= 12) return "\u2022".repeat(key.length);
|
|
106
|
+
return key.slice(0, 6) + "\u2022".repeat(Math.min(key.length - 10, 30)) + key.slice(-4);
|
|
107
|
+
}
|
|
108
|
+
async function getApiKey(rl, provider, existingKeys) {
|
|
109
|
+
if (!provider.needsKey) return void 0;
|
|
110
|
+
if (existingKeys.has(provider.name)) {
|
|
111
|
+
console.log(chalk.green(` \u2713 Reusing ${provider.display} API key from earlier`));
|
|
112
|
+
return existingKeys.get(provider.name);
|
|
113
|
+
}
|
|
114
|
+
const envValue = provider.envVar ? process.env[provider.envVar] : void 0;
|
|
115
|
+
if (envValue) {
|
|
116
|
+
console.log(chalk.green(` \u2713 Found ${provider.envVar} in environment`));
|
|
117
|
+
const key2 = `{env:${provider.envVar}}`;
|
|
118
|
+
existingKeys.set(provider.name, key2);
|
|
119
|
+
return key2;
|
|
120
|
+
}
|
|
121
|
+
const key = await readKeyMasked(rl, chalk.dim(` ${provider.display} API key: `));
|
|
122
|
+
const trimmed = key.trim();
|
|
123
|
+
existingKeys.set(provider.name, trimmed);
|
|
124
|
+
return trimmed;
|
|
125
|
+
}
|
|
126
|
+
function readKeyMasked(rl, prompt) {
|
|
127
|
+
return new Promise((resolve) => {
|
|
128
|
+
let buffer = "";
|
|
129
|
+
let revealed = false;
|
|
130
|
+
process.stdout.write(prompt);
|
|
131
|
+
const wasRaw = process.stdin.isRaw;
|
|
132
|
+
if (process.stdin.isTTY) {
|
|
133
|
+
process.stdin.setRawMode(true);
|
|
61
134
|
}
|
|
135
|
+
process.stdin.resume();
|
|
136
|
+
const redraw = () => {
|
|
137
|
+
process.stdout.write(`\r\x1B[K${prompt}`);
|
|
138
|
+
if (revealed) {
|
|
139
|
+
process.stdout.write(buffer);
|
|
140
|
+
} else {
|
|
141
|
+
process.stdout.write(maskKey(buffer));
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
const onData = (data) => {
|
|
145
|
+
const str = data.toString();
|
|
146
|
+
for (const ch of str) {
|
|
147
|
+
const code = ch.charCodeAt(0);
|
|
148
|
+
if (code === 13 || code === 10) {
|
|
149
|
+
process.stdin.removeListener("data", onData);
|
|
150
|
+
if (process.stdin.isTTY) {
|
|
151
|
+
process.stdin.setRawMode(wasRaw ?? false);
|
|
152
|
+
}
|
|
153
|
+
process.stdout.write(`\r\x1B[K${prompt}`);
|
|
154
|
+
if (buffer.length > 0) {
|
|
155
|
+
process.stdout.write(chalk.green(maskKey(buffer)));
|
|
156
|
+
}
|
|
157
|
+
process.stdout.write("\n");
|
|
158
|
+
resolve(buffer);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (code === 9) {
|
|
162
|
+
revealed = !revealed;
|
|
163
|
+
redraw();
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (code === 3) {
|
|
167
|
+
process.stdin.removeListener("data", onData);
|
|
168
|
+
if (process.stdin.isTTY) {
|
|
169
|
+
process.stdin.setRawMode(wasRaw ?? false);
|
|
170
|
+
}
|
|
171
|
+
process.stdout.write("\n");
|
|
172
|
+
resolve("");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (code === 127 || code === 8) {
|
|
176
|
+
if (buffer.length > 0) {
|
|
177
|
+
buffer = buffer.slice(0, -1);
|
|
178
|
+
redraw();
|
|
179
|
+
}
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (code === 21) {
|
|
183
|
+
buffer = "";
|
|
184
|
+
redraw();
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (code >= 32) {
|
|
188
|
+
buffer += ch;
|
|
189
|
+
redraw();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
process.stdin.on("data", onData);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
async function configureOllama(providerConfig) {
|
|
197
|
+
const hostsToTry = ["http://localhost:11434"];
|
|
198
|
+
try {
|
|
199
|
+
const gateway = execSync("ip route show default 2>/dev/null | awk '{print $3}'", { encoding: "utf-8" }).trim();
|
|
200
|
+
if (gateway) hostsToTry.push(`http://${gateway}:11434`);
|
|
201
|
+
} catch {
|
|
202
|
+
}
|
|
203
|
+
if (process.env.OLLAMA_HOST) {
|
|
204
|
+
const envHost = process.env.OLLAMA_HOST.startsWith("http") ? process.env.OLLAMA_HOST : `http://${process.env.OLLAMA_HOST}`;
|
|
205
|
+
hostsToTry.unshift(envHost);
|
|
62
206
|
}
|
|
63
|
-
|
|
64
|
-
|
|
207
|
+
providerConfig.contextLength = 65536;
|
|
208
|
+
for (const host of hostsToTry) {
|
|
65
209
|
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;
|
|
210
|
+
const controller = new AbortController();
|
|
211
|
+
const timeout = setTimeout(() => controller.abort(), 3e3);
|
|
212
|
+
const response = await globalThis.fetch(`${host}/api/tags`, { signal: controller.signal });
|
|
213
|
+
clearTimeout(timeout);
|
|
214
|
+
if (response.ok) {
|
|
215
|
+
const data = await response.json();
|
|
216
|
+
providerConfig.host = host;
|
|
217
|
+
console.log(chalk.green(` \u2713 Connected to Ollama at ${host}`));
|
|
218
|
+
const models = data.models || [];
|
|
219
|
+
if (models.length > 0) {
|
|
220
|
+
console.log(chalk.dim(` Available: ${models.map((m) => m.name).join(", ")}`));
|
|
89
221
|
}
|
|
90
|
-
|
|
91
|
-
continue;
|
|
222
|
+
return;
|
|
92
223
|
}
|
|
224
|
+
} catch {
|
|
225
|
+
continue;
|
|
93
226
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
227
|
+
}
|
|
228
|
+
providerConfig.host = "http://localhost:11434";
|
|
229
|
+
console.log(chalk.yellow(" \u26A0 Could not connect to Ollama. Make sure it's running."));
|
|
230
|
+
}
|
|
231
|
+
async function runSetup() {
|
|
232
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
233
|
+
const apiKeys = /* @__PURE__ */ new Map();
|
|
234
|
+
console.log();
|
|
235
|
+
console.log(chalk.bold(" WorkerMill CLI") + chalk.dim(" \u2014 AI coding agent"));
|
|
236
|
+
console.log();
|
|
237
|
+
console.log(chalk.dim(" WorkerMill uses three roles. Each can use a different provider."));
|
|
238
|
+
console.log(chalk.dim(" Workers build code, the planner designs the plan, the reviewer checks quality."));
|
|
239
|
+
console.log();
|
|
240
|
+
console.log(chalk.hex("#D77757").bold(" \u2460 Workers") + chalk.dim(" \u2014 expert personas that write code"));
|
|
241
|
+
console.log();
|
|
242
|
+
const workerProvider = await pickProvider(rl, "Provider for workers", PROVIDERS);
|
|
243
|
+
const workerModel = await pickModel(rl, workerProvider);
|
|
244
|
+
const workerKey = await getApiKey(rl, workerProvider, apiKeys);
|
|
245
|
+
const workerConfig = { model: workerModel };
|
|
246
|
+
if (workerKey) workerConfig.apiKey = workerKey;
|
|
247
|
+
if (workerProvider.name === "ollama") await configureOllama(workerConfig);
|
|
248
|
+
console.log();
|
|
249
|
+
console.log(chalk.hex("#D77757").bold(" \u2461 Planner") + chalk.dim(" \u2014 plans stories and validates the approach"));
|
|
250
|
+
console.log();
|
|
251
|
+
const sameForPlanner = await ask(rl, chalk.dim(` Same as workers (${workerProvider.display} / ${workerModel})? [Y/n] `));
|
|
252
|
+
let plannerProviderName;
|
|
253
|
+
let plannerModel;
|
|
254
|
+
if (sameForPlanner.trim().toLowerCase() === "n") {
|
|
255
|
+
const plannerProvider = await pickProvider(rl, "Provider for planner", PROVIDERS);
|
|
256
|
+
plannerModel = await pickModel(rl, plannerProvider);
|
|
257
|
+
const plannerKey = await getApiKey(rl, plannerProvider, apiKeys);
|
|
258
|
+
plannerProviderName = plannerProvider.name;
|
|
259
|
+
if (plannerKey && plannerProvider.name !== workerProvider.name) {
|
|
106
260
|
}
|
|
261
|
+
} else {
|
|
262
|
+
plannerProviderName = workerProvider.name;
|
|
263
|
+
plannerModel = workerModel;
|
|
264
|
+
console.log(chalk.dim(` \u2192 ${workerProvider.display} / ${workerModel}`));
|
|
107
265
|
}
|
|
108
266
|
console.log();
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
267
|
+
console.log(chalk.hex("#D77757").bold(" \u2462 Reviewer") + chalk.dim(" \u2014 tech lead that reviews code quality"));
|
|
268
|
+
console.log();
|
|
269
|
+
const sameForReviewer = await ask(rl, chalk.dim(` Same as workers (${workerProvider.display} / ${workerModel})? [Y/n] `));
|
|
270
|
+
let reviewerProviderName;
|
|
271
|
+
let reviewerModel;
|
|
272
|
+
if (sameForReviewer.trim().toLowerCase() === "n") {
|
|
273
|
+
const reviewerProvider = await pickProvider(rl, "Provider for reviewer", PROVIDERS);
|
|
274
|
+
reviewerModel = await pickModel(rl, reviewerProvider);
|
|
275
|
+
const reviewerKey = await getApiKey(rl, reviewerProvider, apiKeys);
|
|
276
|
+
reviewerProviderName = reviewerProvider.name;
|
|
277
|
+
if (reviewerKey && reviewerProvider.name !== workerProvider.name) {
|
|
278
|
+
}
|
|
279
|
+
} else {
|
|
280
|
+
reviewerProviderName = workerProvider.name;
|
|
281
|
+
reviewerModel = workerModel;
|
|
282
|
+
console.log(chalk.dim(` \u2192 ${workerProvider.display} / ${workerModel}`));
|
|
112
283
|
}
|
|
113
284
|
rl.close();
|
|
285
|
+
const providers = {
|
|
286
|
+
[workerProvider.name]: workerConfig
|
|
287
|
+
};
|
|
288
|
+
if (plannerProviderName !== workerProvider.name && !providers[plannerProviderName]) {
|
|
289
|
+
const pProvider = PROVIDERS.find((p) => p.name === plannerProviderName);
|
|
290
|
+
const cfg = { model: plannerModel };
|
|
291
|
+
const key = apiKeys.get(plannerProviderName);
|
|
292
|
+
if (key) cfg.apiKey = key;
|
|
293
|
+
if (pProvider.name === "ollama") await configureOllama(cfg);
|
|
294
|
+
providers[plannerProviderName] = cfg;
|
|
295
|
+
}
|
|
296
|
+
if (reviewerProviderName !== workerProvider.name && !providers[reviewerProviderName]) {
|
|
297
|
+
const rProvider = PROVIDERS.find((p) => p.name === reviewerProviderName);
|
|
298
|
+
const cfg = { model: reviewerModel };
|
|
299
|
+
const key = apiKeys.get(reviewerProviderName);
|
|
300
|
+
if (key) cfg.apiKey = key;
|
|
301
|
+
if (rProvider.name === "ollama") await configureOllama(cfg);
|
|
302
|
+
providers[reviewerProviderName] = cfg;
|
|
303
|
+
}
|
|
304
|
+
const routing = {};
|
|
305
|
+
if (plannerProviderName !== workerProvider.name) {
|
|
306
|
+
routing.planner = plannerProviderName;
|
|
307
|
+
routing.critic = plannerProviderName;
|
|
308
|
+
}
|
|
309
|
+
if (reviewerProviderName !== workerProvider.name) {
|
|
310
|
+
routing.tech_lead = reviewerProviderName;
|
|
311
|
+
}
|
|
312
|
+
if (plannerProviderName === workerProvider.name && plannerModel !== workerModel) {
|
|
313
|
+
const altKey = `${plannerProviderName}_planner`;
|
|
314
|
+
providers[altKey] = { ...providers[plannerProviderName], model: plannerModel };
|
|
315
|
+
routing.planner = altKey;
|
|
316
|
+
routing.critic = altKey;
|
|
317
|
+
}
|
|
318
|
+
if (reviewerProviderName === workerProvider.name && reviewerModel !== workerModel) {
|
|
319
|
+
const altKey = `${reviewerProviderName}_reviewer`;
|
|
320
|
+
providers[altKey] = { ...providers[reviewerProviderName], model: reviewerModel };
|
|
321
|
+
routing.tech_lead = altKey;
|
|
322
|
+
}
|
|
114
323
|
const config = {
|
|
115
|
-
providers
|
|
116
|
-
default:
|
|
324
|
+
providers,
|
|
325
|
+
default: workerProvider.name,
|
|
326
|
+
...Object.keys(routing).length > 0 ? { routing } : {}
|
|
117
327
|
};
|
|
118
328
|
saveConfig(config);
|
|
119
329
|
console.log();
|
|
120
330
|
console.log(chalk.green(" \u2713 Config saved to ~/.workermill/cli.json"));
|
|
121
331
|
console.log();
|
|
332
|
+
console.log(chalk.dim(" Workers: ") + `${workerProvider.name}/${workerModel}`);
|
|
333
|
+
console.log(chalk.dim(" Planner: ") + `${plannerProviderName}/${plannerModel}`);
|
|
334
|
+
console.log(chalk.dim(" Reviewer: ") + `${reviewerProviderName}/${reviewerModel}`);
|
|
335
|
+
console.log();
|
|
122
336
|
return config;
|
|
123
337
|
}
|
|
124
338
|
|
|
@@ -127,8 +341,8 @@ init_esm_shims();
|
|
|
127
341
|
import { useState as useState5, useCallback as useCallback3, useRef as useRef3, useEffect as useEffect2 } from "react";
|
|
128
342
|
import { useApp as useApp2 } from "ink";
|
|
129
343
|
import { execSync as execSync2 } from "child_process";
|
|
130
|
-
import
|
|
131
|
-
import
|
|
344
|
+
import fs2 from "fs";
|
|
345
|
+
import path2 from "path";
|
|
132
346
|
import os from "os";
|
|
133
347
|
|
|
134
348
|
// src/ui/useAgent.ts
|
|
@@ -263,42 +477,6 @@ ${result.text}` },
|
|
|
263
477
|
}
|
|
264
478
|
}
|
|
265
479
|
|
|
266
|
-
// src/logger.ts
|
|
267
|
-
init_esm_shims();
|
|
268
|
-
import fs2 from "fs";
|
|
269
|
-
import path2 from "path";
|
|
270
|
-
var LOG_DIR = path2.join(process.cwd(), ".workermill");
|
|
271
|
-
var LOG_FILE = path2.join(LOG_DIR, "cli.log");
|
|
272
|
-
var logStream = null;
|
|
273
|
-
function ensureLogDir() {
|
|
274
|
-
if (!fs2.existsSync(LOG_DIR)) {
|
|
275
|
-
fs2.mkdirSync(LOG_DIR, { recursive: true });
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
function getStream() {
|
|
279
|
-
if (!logStream) {
|
|
280
|
-
ensureLogDir();
|
|
281
|
-
logStream = fs2.createWriteStream(LOG_FILE, { flags: "a" });
|
|
282
|
-
}
|
|
283
|
-
return logStream;
|
|
284
|
-
}
|
|
285
|
-
function timestamp() {
|
|
286
|
-
return (/* @__PURE__ */ new Date()).toISOString();
|
|
287
|
-
}
|
|
288
|
-
function log(level, message, data) {
|
|
289
|
-
const entry = data ? `[${timestamp()}] ${level}: ${message} ${JSON.stringify(data)}` : `[${timestamp()}] ${level}: ${message}`;
|
|
290
|
-
getStream().write(entry + "\n");
|
|
291
|
-
}
|
|
292
|
-
function info(message, data) {
|
|
293
|
-
log("INFO", message, data);
|
|
294
|
-
}
|
|
295
|
-
function error(message, data) {
|
|
296
|
-
log("ERROR", message, data);
|
|
297
|
-
}
|
|
298
|
-
function debug(message, data) {
|
|
299
|
-
log("DEBUG", message, data);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
480
|
// src/ui/useAgent.ts
|
|
303
481
|
var DANGEROUS_PATTERNS = [
|
|
304
482
|
{
|
|
@@ -817,7 +995,7 @@ function useOrchestrator(addMessage2, setCost) {
|
|
|
817
995
|
setRunning(false);
|
|
818
996
|
return;
|
|
819
997
|
}
|
|
820
|
-
const { classifyComplexity, runOrchestration } = await import("./orchestrator-
|
|
998
|
+
const { classifyComplexity, runOrchestration } = await import("./orchestrator-L5LDQYO7.js");
|
|
821
999
|
const output = {
|
|
822
1000
|
log(persona, message) {
|
|
823
1001
|
const emoji = getEmoji(persona);
|
|
@@ -1574,13 +1752,13 @@ function App(props) {
|
|
|
1574
1752
|
|
|
1575
1753
|
// src/ui/Root.tsx
|
|
1576
1754
|
import { jsx as jsx7 } from "react/jsx-runtime";
|
|
1577
|
-
var HISTORY_DIR =
|
|
1578
|
-
var HISTORY_FILE =
|
|
1755
|
+
var HISTORY_DIR = path2.join(os.homedir(), ".workermill");
|
|
1756
|
+
var HISTORY_FILE = path2.join(HISTORY_DIR, "history");
|
|
1579
1757
|
var MAX_HISTORY = 1e3;
|
|
1580
1758
|
function loadHistory() {
|
|
1581
1759
|
try {
|
|
1582
|
-
if (
|
|
1583
|
-
const raw =
|
|
1760
|
+
if (fs2.existsSync(HISTORY_FILE)) {
|
|
1761
|
+
const raw = fs2.readFileSync(HISTORY_FILE, "utf-8").trim();
|
|
1584
1762
|
if (!raw) return [];
|
|
1585
1763
|
return raw.split("\n").slice(-MAX_HISTORY);
|
|
1586
1764
|
}
|
|
@@ -1590,10 +1768,10 @@ function loadHistory() {
|
|
|
1590
1768
|
}
|
|
1591
1769
|
function appendHistory(line) {
|
|
1592
1770
|
try {
|
|
1593
|
-
if (!
|
|
1594
|
-
|
|
1771
|
+
if (!fs2.existsSync(HISTORY_DIR)) {
|
|
1772
|
+
fs2.mkdirSync(HISTORY_DIR, { recursive: true });
|
|
1595
1773
|
}
|
|
1596
|
-
|
|
1774
|
+
fs2.appendFileSync(HISTORY_FILE, line + "\n", "utf-8");
|
|
1597
1775
|
} catch {
|
|
1598
1776
|
}
|
|
1599
1777
|
}
|
|
@@ -1835,15 +2013,15 @@ To change: edit \`~/.workermill/cli.json\` or restart with \`--provider\` / \`--
|
|
|
1835
2013
|
// ---- /editor ----
|
|
1836
2014
|
case "editor": {
|
|
1837
2015
|
const editor = process.env.EDITOR || process.env.VISUAL || "vi";
|
|
1838
|
-
const tmpFile =
|
|
2016
|
+
const tmpFile = path2.join(os.tmpdir(), `workermill-${Date.now()}.md`);
|
|
1839
2017
|
try {
|
|
1840
|
-
|
|
2018
|
+
fs2.writeFileSync(tmpFile, "", "utf-8");
|
|
1841
2019
|
execSync2(`${editor} ${tmpFile}`, {
|
|
1842
2020
|
cwd: props.workingDir,
|
|
1843
2021
|
stdio: "inherit",
|
|
1844
2022
|
timeout: 5 * 60 * 1e3
|
|
1845
2023
|
});
|
|
1846
|
-
const contents =
|
|
2024
|
+
const contents = fs2.readFileSync(tmpFile, "utf-8").trim();
|
|
1847
2025
|
if (contents) {
|
|
1848
2026
|
agent.addUserMessage(contents);
|
|
1849
2027
|
agent.submit(contents);
|
|
@@ -1855,7 +2033,7 @@ To change: edit \`~/.workermill/cli.json\` or restart with \`--provider\` / \`--
|
|
|
1855
2033
|
agent.addSystemMessage(`Failed to open editor (\`${editor}\`): ${errMsg}`);
|
|
1856
2034
|
} finally {
|
|
1857
2035
|
try {
|
|
1858
|
-
|
|
2036
|
+
fs2.unlinkSync(tmpFile);
|
|
1859
2037
|
} catch {
|
|
1860
2038
|
}
|
|
1861
2039
|
}
|
|
@@ -1997,7 +2175,7 @@ function printWelcome(provider, model, workingDir) {
|
|
|
1997
2175
|
console.log(dim(" Type ") + white("/help") + dim(" for all commands."));
|
|
1998
2176
|
console.log();
|
|
1999
2177
|
}
|
|
2000
|
-
var VERSION = "0.
|
|
2178
|
+
var VERSION = "0.4.1";
|
|
2001
2179
|
function addSharedOptions(cmd) {
|
|
2002
2180
|
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
2181
|
}
|
|
@@ -4,9 +4,11 @@ import {
|
|
|
4
4
|
buildOllamaOptions,
|
|
5
5
|
createModel,
|
|
6
6
|
createToolDefinitions,
|
|
7
|
+
error,
|
|
7
8
|
getProviderForPersona,
|
|
9
|
+
info,
|
|
8
10
|
init_esm_shims
|
|
9
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-77CAVW3Y.js";
|
|
10
12
|
|
|
11
13
|
// src/orchestrator.ts
|
|
12
14
|
init_esm_shims();
|
|
@@ -153,9 +155,9 @@ function buildReasoningOptions(provider, modelName) {
|
|
|
153
155
|
return {};
|
|
154
156
|
}
|
|
155
157
|
}
|
|
156
|
-
function isTransientError(
|
|
157
|
-
if (!
|
|
158
|
-
const msg =
|
|
158
|
+
function isTransientError(error2) {
|
|
159
|
+
if (!error2 || typeof error2 !== "object") return false;
|
|
160
|
+
const msg = error2 instanceof Error ? error2.message : String(error2);
|
|
159
161
|
if (/status code (502|503|504)|socket hang up|ECONNRESET|ETIMEDOUT|network error|ECONNREFUSED/i.test(msg)) {
|
|
160
162
|
return true;
|
|
161
163
|
}
|
|
@@ -219,6 +221,7 @@ function formatToolCallDisplay(toolName, toolInput) {
|
|
|
219
221
|
return msg;
|
|
220
222
|
}
|
|
221
223
|
async function classifyComplexity(config, userInput, output) {
|
|
224
|
+
info("Classifying complexity", { input: userInput.slice(0, 200) });
|
|
222
225
|
const resolvedInput = resolveTaskInput(userInput, process.cwd());
|
|
223
226
|
const { provider, model: modelName, apiKey, host, contextLength } = getProviderForPersona(config);
|
|
224
227
|
if (apiKey) {
|
|
@@ -635,6 +638,7 @@ ${plannerStories.map((s) => `- ${s.id}: ${s.title} (${s.persona}) \u2014 ${s.des
|
|
|
635
638
|
output.coordinatorLog(`Task claimed by orchestrator`);
|
|
636
639
|
output.log(story.persona, `Starting ${story.title}`);
|
|
637
640
|
output.log(story.persona, `Executing story with AIClient (model: ${modelName})...`);
|
|
641
|
+
info(`Story ${i + 1}/${sorted.length} started`, { persona: story.persona, title: story.title, provider, model: modelName });
|
|
638
642
|
output.status("");
|
|
639
643
|
const model = createModel(provider, modelName, host, contextLength);
|
|
640
644
|
const allTools = createToolDefinitions(workingDir, model, sandboxed);
|
|
@@ -765,13 +769,16 @@ ${revisionFeedback}` : ""}`;
|
|
|
765
769
|
costTracker.addUsage(persona.name, provider, modelName, inTokens, outTokens);
|
|
766
770
|
output.updateCost?.(costTracker.getTotalCost());
|
|
767
771
|
output.log(story.persona, `${story.title} \u2014 completed! (${i + 1}/${sorted.length})`);
|
|
772
|
+
info(`Story ${i + 1} completed`, { persona: story.persona, inputTokens: inTokens, outputTokens: outTokens });
|
|
768
773
|
output.log("system", "");
|
|
769
774
|
break;
|
|
770
775
|
} catch (err) {
|
|
771
776
|
output.statusDone();
|
|
772
777
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
778
|
+
error(`Story ${i + 1} error`, { persona: story.persona, error: errMsg, revision });
|
|
773
779
|
if (isTransientError(err) && revision < 2) {
|
|
774
780
|
output.log(story.persona, `Transient error: ${errMsg} \u2014 retrying...`);
|
|
781
|
+
info(`Story ${i + 1} retrying (transient)`, { revision });
|
|
775
782
|
continue;
|
|
776
783
|
}
|
|
777
784
|
output.error(`Story ${i + 1} failed: ${errMsg}`);
|
|
@@ -812,8 +819,10 @@ ${revisionFeedback}` : ""}`;
|
|
|
812
819
|
}
|
|
813
820
|
}
|
|
814
821
|
let previousReviewFeedback = "";
|
|
822
|
+
info("Starting review loop", { maxRevisions, approvalThreshold, provider: revProvider, model: revModel });
|
|
815
823
|
for (let reviewRound = 0; reviewRound <= maxRevisions; reviewRound++) {
|
|
816
824
|
const isRevision = reviewRound > 0;
|
|
825
|
+
info(`Review round ${reviewRound}`, { isRevision, maxRevisions });
|
|
817
826
|
output.coordinatorLog(isRevision ? `Starting Tech Lead review (revision ${reviewRound}/${maxRevisions})...` : "Starting Tech Lead review...");
|
|
818
827
|
output.log("tech_lead", `Starting agent execution (model: ${revModel})`);
|
|
819
828
|
output.status(isRevision ? "Reviewer -- Re-checking after revisions" : "Reviewer -- Checking code quality");
|
|
@@ -907,6 +916,7 @@ AFFECTED_REASONS: {"2": "Missing error handling in auth controller", "3": "Front
|
|
|
907
916
|
output.statusDone();
|
|
908
917
|
const score = extractScore(reviewText);
|
|
909
918
|
const approved = score >= approvalThreshold;
|
|
919
|
+
info(`Review round ${reviewRound} result`, { score, approved, threshold: approvalThreshold, reviewTextLength: reviewText.length });
|
|
910
920
|
output.log("tech_lead", `::code_quality_score::${score}`);
|
|
911
921
|
output.log("tech_lead", `::review_decision::${approved ? "approved" : "needs_revision"}`);
|
|
912
922
|
output.coordinatorLog(approved ? `Review approved (score: ${score}/100)` : `Review needs revision (score: ${score}/100)`);
|