workermill 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.
- package/LICENSE +202 -0
- package/README.md +124 -0
- package/dist/chunk-2NTK7H4W.js +10 -0
- package/dist/chunk-3KIFXIBC.js +1906 -0
- package/dist/chunk-LVCJZJJH.js +29 -0
- package/dist/index.js +971 -0
- package/dist/orchestrator-NMTZUS23.js +875 -0
- package/dist/terminal-ILMO7Z3P.js +17 -0
- package/package.json +61 -0
- package/personas/architect.md +22 -0
- package/personas/backend_developer.md +22 -0
- package/personas/critic.md +27 -0
- package/personas/data_engineer.md +22 -0
- package/personas/database_engineer.md +22 -0
- package/personas/devops_engineer.md +22 -0
- package/personas/frontend_developer.md +22 -0
- package/personas/fullstack_developer.md +22 -0
- package/personas/ml_engineer.md +22 -0
- package/personas/mobile_developer.md +22 -0
- package/personas/planner.md +25 -0
- package/personas/qa_engineer.md +22 -0
- package/personas/reviewer.md +22 -0
- package/personas/security_engineer.md +22 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,971 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
exitTerminal,
|
|
4
|
+
initTerminal,
|
|
5
|
+
setStatusBar,
|
|
6
|
+
showStatusBar
|
|
7
|
+
} from "./chunk-LVCJZJJH.js";
|
|
8
|
+
import {
|
|
9
|
+
CostTracker,
|
|
10
|
+
PermissionManager,
|
|
11
|
+
createModel,
|
|
12
|
+
createToolDefinitions,
|
|
13
|
+
getProviderForPersona,
|
|
14
|
+
killActiveProcess,
|
|
15
|
+
loadConfig,
|
|
16
|
+
printError,
|
|
17
|
+
printHeader,
|
|
18
|
+
printStatusBar,
|
|
19
|
+
printToolCall,
|
|
20
|
+
printToolResult,
|
|
21
|
+
saveConfig
|
|
22
|
+
} from "./chunk-3KIFXIBC.js";
|
|
23
|
+
import "./chunk-2NTK7H4W.js";
|
|
24
|
+
|
|
25
|
+
// src/index.ts
|
|
26
|
+
import { Command } from "commander";
|
|
27
|
+
|
|
28
|
+
// src/setup.js
|
|
29
|
+
import readline from "readline";
|
|
30
|
+
import { execSync } from "child_process";
|
|
31
|
+
import chalk from "chalk";
|
|
32
|
+
var PROVIDERS = [
|
|
33
|
+
{ name: "ollama", display: "Ollama (local, no API key needed)", needsKey: false, defaultModel: "qwen3-coder:30b" },
|
|
34
|
+
{ name: "anthropic", display: "Anthropic (Claude)", needsKey: true, defaultModel: "claude-sonnet-4-6", envVar: "ANTHROPIC_API_KEY" },
|
|
35
|
+
{ name: "openai", display: "OpenAI (GPT)", needsKey: true, defaultModel: "gpt-4o", envVar: "OPENAI_API_KEY" },
|
|
36
|
+
{ name: "google", display: "Google (Gemini)", needsKey: true, defaultModel: "gemini-2.5-pro", envVar: "GOOGLE_API_KEY" }
|
|
37
|
+
];
|
|
38
|
+
function ask(rl, question) {
|
|
39
|
+
return new Promise((resolve) => rl.question(question, resolve));
|
|
40
|
+
}
|
|
41
|
+
async function runSetup() {
|
|
42
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
43
|
+
console.log();
|
|
44
|
+
console.log(chalk.bold(" WorkerMill CLI") + chalk.dim(" \u2014 AI coding agent"));
|
|
45
|
+
console.log();
|
|
46
|
+
console.log(chalk.dim(" No provider configured. Let's set one up."));
|
|
47
|
+
console.log();
|
|
48
|
+
console.log(" Provider:");
|
|
49
|
+
PROVIDERS.forEach((p, i) => {
|
|
50
|
+
console.log(` ${chalk.cyan(`${i + 1}`)}. ${p.display}`);
|
|
51
|
+
});
|
|
52
|
+
console.log();
|
|
53
|
+
const choiceStr = await ask(rl, chalk.dim(" Choose (1-4): "));
|
|
54
|
+
const choice = parseInt(choiceStr.trim(), 10) - 1;
|
|
55
|
+
const selected = PROVIDERS[choice] || PROVIDERS[0];
|
|
56
|
+
console.log();
|
|
57
|
+
console.log(chalk.dim(` Selected: ${selected.display}`));
|
|
58
|
+
const providerConfig = { model: selected.defaultModel };
|
|
59
|
+
if (selected.needsKey) {
|
|
60
|
+
const envValue = selected.envVar ? process.env[selected.envVar] : void 0;
|
|
61
|
+
if (envValue) {
|
|
62
|
+
console.log(chalk.green(` \u2713 Found ${selected.envVar} in environment`));
|
|
63
|
+
providerConfig.apiKey = `{env:${selected.envVar}}`;
|
|
64
|
+
} else {
|
|
65
|
+
const key = await ask(rl, chalk.dim(` API key: `));
|
|
66
|
+
providerConfig.apiKey = key.trim();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (selected.name === "ollama") {
|
|
70
|
+
const hostsToTry = ["http://localhost:11434"];
|
|
71
|
+
try {
|
|
72
|
+
const gateway = execSync("ip route show default 2>/dev/null | awk '{print $3}'", { encoding: "utf-8" }).trim();
|
|
73
|
+
if (gateway) {
|
|
74
|
+
hostsToTry.push(`http://${gateway}:11434`);
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
}
|
|
78
|
+
if (process.env.OLLAMA_HOST) {
|
|
79
|
+
const envHost = process.env.OLLAMA_HOST.startsWith("http") ? process.env.OLLAMA_HOST : `http://${process.env.OLLAMA_HOST}`;
|
|
80
|
+
hostsToTry.unshift(envHost);
|
|
81
|
+
}
|
|
82
|
+
let connectedHost = null;
|
|
83
|
+
let models = [];
|
|
84
|
+
for (const host of hostsToTry) {
|
|
85
|
+
try {
|
|
86
|
+
const controller = new AbortController();
|
|
87
|
+
const timeout = setTimeout(() => controller.abort(), 3e3);
|
|
88
|
+
const response = await globalThis.fetch(`${host}/api/tags`, { signal: controller.signal });
|
|
89
|
+
clearTimeout(timeout);
|
|
90
|
+
if (response.ok) {
|
|
91
|
+
const data = await response.json();
|
|
92
|
+
connectedHost = host;
|
|
93
|
+
models = data.models || [];
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (connectedHost) {
|
|
101
|
+
providerConfig.host = connectedHost;
|
|
102
|
+
console.log(chalk.green(` \u2713 Connected to Ollama at ${connectedHost}`));
|
|
103
|
+
if (models.length > 0) {
|
|
104
|
+
console.log(chalk.dim(` Available models: ${models.map((m) => m.name).join(", ")}`));
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
providerConfig.host = "http://localhost:11434";
|
|
108
|
+
console.log(chalk.yellow(" \u26A0 Could not connect to Ollama. Make sure it's running."));
|
|
109
|
+
console.log(chalk.dim(" Tried: " + hostsToTry.join(", ")));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
console.log();
|
|
113
|
+
const modelOverride = await ask(rl, chalk.dim(` Model (press Enter for ${selected.defaultModel}): `));
|
|
114
|
+
if (modelOverride.trim()) {
|
|
115
|
+
providerConfig.model = modelOverride.trim();
|
|
116
|
+
}
|
|
117
|
+
rl.close();
|
|
118
|
+
const config = {
|
|
119
|
+
providers: { [selected.name]: providerConfig },
|
|
120
|
+
default: selected.name
|
|
121
|
+
};
|
|
122
|
+
saveConfig(config);
|
|
123
|
+
console.log();
|
|
124
|
+
console.log(chalk.green(" \u2713 Config saved to ~/.workermill/cli.json"));
|
|
125
|
+
console.log();
|
|
126
|
+
return config;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// src/agent.js
|
|
130
|
+
import readline2 from "readline";
|
|
131
|
+
import fs4 from "fs";
|
|
132
|
+
import pathModule from "path";
|
|
133
|
+
import chalk3 from "chalk";
|
|
134
|
+
import ora2 from "ora";
|
|
135
|
+
import { execSync as execSync3 } from "child_process";
|
|
136
|
+
import { streamText, stepCountIs } from "ai";
|
|
137
|
+
|
|
138
|
+
// src/commands.js
|
|
139
|
+
import chalk2 from "chalk";
|
|
140
|
+
import ora from "ora";
|
|
141
|
+
import fs2 from "fs";
|
|
142
|
+
import os from "os";
|
|
143
|
+
import path2 from "path";
|
|
144
|
+
import { execSync as execSync2 } from "child_process";
|
|
145
|
+
|
|
146
|
+
// src/compaction.js
|
|
147
|
+
import { generateText } from "ai";
|
|
148
|
+
var CONTEXT_LIMITS = {
|
|
149
|
+
// Anthropic
|
|
150
|
+
"claude-opus": 2e5,
|
|
151
|
+
"claude-sonnet": 2e5,
|
|
152
|
+
"claude-haiku": 2e5,
|
|
153
|
+
// OpenAI
|
|
154
|
+
"gpt-4o": 128e3,
|
|
155
|
+
"gpt-4o-mini": 128e3,
|
|
156
|
+
"o3": 2e5,
|
|
157
|
+
"o3-mini": 128e3,
|
|
158
|
+
// Google
|
|
159
|
+
"gemini-2.5-pro": 1e6,
|
|
160
|
+
"gemini-2.5-flash": 1e6,
|
|
161
|
+
// Ollama (conservative default)
|
|
162
|
+
"default": 32e3
|
|
163
|
+
};
|
|
164
|
+
function getContextLimit(model) {
|
|
165
|
+
for (const [prefix, limit] of Object.entries(CONTEXT_LIMITS)) {
|
|
166
|
+
if (model.includes(prefix))
|
|
167
|
+
return limit;
|
|
168
|
+
}
|
|
169
|
+
return CONTEXT_LIMITS["default"];
|
|
170
|
+
}
|
|
171
|
+
function shouldCompact(totalTokens, model) {
|
|
172
|
+
const limit = getContextLimit(model);
|
|
173
|
+
if (totalTokens >= limit * 0.95)
|
|
174
|
+
return "hard";
|
|
175
|
+
if (totalTokens >= limit * 0.8)
|
|
176
|
+
return "soft";
|
|
177
|
+
return "none";
|
|
178
|
+
}
|
|
179
|
+
async function compactMessages(model, messages, mode) {
|
|
180
|
+
if (messages.length <= 4)
|
|
181
|
+
return messages;
|
|
182
|
+
const keepCount = mode === "hard" ? 2 : 4;
|
|
183
|
+
const toCompact = messages.slice(0, -keepCount);
|
|
184
|
+
const toKeep = messages.slice(-keepCount);
|
|
185
|
+
if (toCompact.length === 0)
|
|
186
|
+
return messages;
|
|
187
|
+
const summaryText = toCompact.map((m) => `${m.role}: ${m.content.slice(0, 500)}`).join("\n\n");
|
|
188
|
+
try {
|
|
189
|
+
const result = await generateText({
|
|
190
|
+
model,
|
|
191
|
+
system: "Summarize this conversation history concisely. Focus on: what was discussed, what decisions were made, what files were modified, and what the current state of work is. Be brief but preserve all important context.",
|
|
192
|
+
prompt: summaryText
|
|
193
|
+
});
|
|
194
|
+
return [
|
|
195
|
+
{ role: "assistant", content: `[Conversation summary]
|
|
196
|
+
${result.text}` },
|
|
197
|
+
...toKeep
|
|
198
|
+
];
|
|
199
|
+
} catch {
|
|
200
|
+
return toKeep;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// src/session.js
|
|
205
|
+
import fs from "fs";
|
|
206
|
+
import path from "path";
|
|
207
|
+
import crypto from "crypto";
|
|
208
|
+
var SESSIONS_DIR = path.join(process.cwd(), ".workermill", "sessions");
|
|
209
|
+
function ensureSessionsDir() {
|
|
210
|
+
if (!fs.existsSync(SESSIONS_DIR)) {
|
|
211
|
+
fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function createSession(provider, model) {
|
|
215
|
+
return {
|
|
216
|
+
id: crypto.randomUUID(),
|
|
217
|
+
messages: [],
|
|
218
|
+
provider,
|
|
219
|
+
model,
|
|
220
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
221
|
+
totalTokens: 0
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
function saveSession(session) {
|
|
225
|
+
ensureSessionsDir();
|
|
226
|
+
const filePath = path.join(SESSIONS_DIR, `${session.id}.json`);
|
|
227
|
+
fs.writeFileSync(filePath, JSON.stringify(session, null, 2), "utf-8");
|
|
228
|
+
}
|
|
229
|
+
function loadLatestSession() {
|
|
230
|
+
ensureSessionsDir();
|
|
231
|
+
try {
|
|
232
|
+
const files = fs.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith(".json")).map((f) => ({
|
|
233
|
+
name: f,
|
|
234
|
+
mtime: fs.statSync(path.join(SESSIONS_DIR, f)).mtimeMs
|
|
235
|
+
})).sort((a, b) => b.mtime - a.mtime);
|
|
236
|
+
if (files.length === 0)
|
|
237
|
+
return null;
|
|
238
|
+
const content = fs.readFileSync(path.join(SESSIONS_DIR, files[0].name), "utf-8");
|
|
239
|
+
return JSON.parse(content);
|
|
240
|
+
} catch {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function addMessage(session, role, content) {
|
|
245
|
+
session.messages.push({
|
|
246
|
+
role,
|
|
247
|
+
content,
|
|
248
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
function listSessions(max = 20) {
|
|
252
|
+
ensureSessionsDir();
|
|
253
|
+
try {
|
|
254
|
+
const files = fs.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith(".json")).map((f) => ({
|
|
255
|
+
name: f,
|
|
256
|
+
mtime: fs.statSync(path.join(SESSIONS_DIR, f)).mtimeMs
|
|
257
|
+
})).sort((a, b) => b.mtime - a.mtime).slice(0, max);
|
|
258
|
+
return files.map((f) => {
|
|
259
|
+
const content = JSON.parse(fs.readFileSync(path.join(SESSIONS_DIR, f.name), "utf-8"));
|
|
260
|
+
const firstUserMsg = content.messages.find((m) => m.role === "user");
|
|
261
|
+
return {
|
|
262
|
+
id: content.id,
|
|
263
|
+
name: content.name,
|
|
264
|
+
startedAt: content.startedAt,
|
|
265
|
+
messageCount: content.messages.length,
|
|
266
|
+
totalTokens: content.totalTokens,
|
|
267
|
+
preview: firstUserMsg ? firstUserMsg.content.slice(0, 50) : "(empty)"
|
|
268
|
+
};
|
|
269
|
+
});
|
|
270
|
+
} catch {
|
|
271
|
+
return [];
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function loadSessionById(id) {
|
|
275
|
+
ensureSessionsDir();
|
|
276
|
+
try {
|
|
277
|
+
const filePath = path.join(SESSIONS_DIR, `${id}.json`);
|
|
278
|
+
if (fs.existsSync(filePath)) {
|
|
279
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
280
|
+
}
|
|
281
|
+
const files = fs.readdirSync(SESSIONS_DIR).filter((f) => f.startsWith(id));
|
|
282
|
+
if (files.length === 1) {
|
|
283
|
+
return JSON.parse(fs.readFileSync(path.join(SESSIONS_DIR, files[0]), "utf-8"));
|
|
284
|
+
}
|
|
285
|
+
} catch {
|
|
286
|
+
}
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
function deleteSession(id) {
|
|
290
|
+
ensureSessionsDir();
|
|
291
|
+
try {
|
|
292
|
+
const filePath = path.join(SESSIONS_DIR, `${id}.json`);
|
|
293
|
+
if (fs.existsSync(filePath)) {
|
|
294
|
+
fs.unlinkSync(filePath);
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
} catch {
|
|
298
|
+
}
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// src/commands.js
|
|
303
|
+
async function handleCommand(cmd, ctx) {
|
|
304
|
+
const parts = cmd.slice(1).split(" ");
|
|
305
|
+
const command = parts[0];
|
|
306
|
+
switch (command) {
|
|
307
|
+
case "help":
|
|
308
|
+
console.log();
|
|
309
|
+
console.log(chalk2.bold(" Commands:"));
|
|
310
|
+
console.log(chalk2.dim(" /help ") + "Show this help");
|
|
311
|
+
console.log(chalk2.dim(" /clear ") + "Clear conversation history");
|
|
312
|
+
console.log(chalk2.dim(" /compact ") + "Manually compact conversation");
|
|
313
|
+
console.log(chalk2.dim(" /status ") + "Show session status");
|
|
314
|
+
console.log(chalk2.dim(" /model ") + "Show current model");
|
|
315
|
+
console.log(chalk2.dim(" /cost ") + "Show token cost breakdown");
|
|
316
|
+
console.log(chalk2.dim(" /git ") + "Show git status");
|
|
317
|
+
console.log(chalk2.dim(" /sessions ") + "List/switch sessions");
|
|
318
|
+
console.log(chalk2.dim(" /editor ") + "Open $EDITOR for input");
|
|
319
|
+
console.log(chalk2.dim(" /plan ") + "Toggle plan mode (read-only)");
|
|
320
|
+
console.log(chalk2.dim(" /quit ") + "Exit");
|
|
321
|
+
console.log();
|
|
322
|
+
console.log(chalk2.dim(" Prefix ! for bash: ") + chalk2.white("!git status"));
|
|
323
|
+
console.log(chalk2.dim(" Multiline: ``` to start/end, or \\ at end of line"));
|
|
324
|
+
console.log();
|
|
325
|
+
break;
|
|
326
|
+
case "clear":
|
|
327
|
+
ctx.session.messages = [];
|
|
328
|
+
saveSession(ctx.session);
|
|
329
|
+
console.log(chalk2.green("\n Conversation cleared\n"));
|
|
330
|
+
break;
|
|
331
|
+
case "compact": {
|
|
332
|
+
const { provider, model: modelName, host } = getProviderForPersona(ctx.config);
|
|
333
|
+
const model = createModel(provider, modelName, host);
|
|
334
|
+
const plainMessages = ctx.session.messages.map((m) => ({ role: m.role, content: m.content }));
|
|
335
|
+
if (plainMessages.length <= 4) {
|
|
336
|
+
console.log(chalk2.dim("\n Not enough messages to compact.\n"));
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
const spinner = ora({ stream: process.stdout, text: "Compacting conversation...", prefixText: " " }).start();
|
|
340
|
+
const compacted = await compactMessages(model, plainMessages, "soft");
|
|
341
|
+
ctx.session.messages = compacted.map((m) => ({
|
|
342
|
+
role: m.role,
|
|
343
|
+
content: m.content,
|
|
344
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
345
|
+
}));
|
|
346
|
+
saveSession(ctx.session);
|
|
347
|
+
spinner.succeed(`Compacted to ${ctx.session.messages.length} messages`);
|
|
348
|
+
console.log();
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
case "status":
|
|
352
|
+
console.log();
|
|
353
|
+
console.log(chalk2.bold(" Session Status:"));
|
|
354
|
+
console.log(chalk2.dim(" Messages: ") + ctx.session.messages.length);
|
|
355
|
+
console.log(chalk2.dim(" Tokens: ") + ctx.session.totalTokens.toLocaleString());
|
|
356
|
+
console.log(chalk2.dim(" Cost: ") + `$${ctx.costTracker.getTotalCost().toFixed(4)}`);
|
|
357
|
+
console.log(chalk2.dim(" Mode: ") + (ctx.planMode ? chalk2.cyan("PLAN (read-only)") : "normal"));
|
|
358
|
+
console.log();
|
|
359
|
+
break;
|
|
360
|
+
case "model": {
|
|
361
|
+
const { provider, model } = getProviderForPersona(ctx.config);
|
|
362
|
+
console.log(chalk2.dim(`
|
|
363
|
+
${provider}/${model}
|
|
364
|
+
`));
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
case "cost":
|
|
368
|
+
console.log(chalk2.dim(`
|
|
369
|
+
${ctx.costTracker.getSummary()}
|
|
370
|
+
`));
|
|
371
|
+
break;
|
|
372
|
+
case "git": {
|
|
373
|
+
try {
|
|
374
|
+
const status = execSync2("git status --short", { cwd: ctx.workingDir, encoding: "utf-8" });
|
|
375
|
+
const branch = execSync2("git branch --show-current", { cwd: ctx.workingDir, encoding: "utf-8" }).trim();
|
|
376
|
+
console.log(chalk2.dim(`
|
|
377
|
+
Branch: `) + chalk2.white(branch));
|
|
378
|
+
if (status.trim()) {
|
|
379
|
+
console.log(chalk2.dim(" Changes:"));
|
|
380
|
+
for (const line of status.trim().split("\n").slice(0, 15)) {
|
|
381
|
+
console.log(chalk2.dim(` ${line}`));
|
|
382
|
+
}
|
|
383
|
+
} else {
|
|
384
|
+
console.log(chalk2.dim(" Working tree clean"));
|
|
385
|
+
}
|
|
386
|
+
console.log();
|
|
387
|
+
} catch {
|
|
388
|
+
console.log(chalk2.dim("\n Not a git repository.\n"));
|
|
389
|
+
}
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
case "sessions": {
|
|
393
|
+
const args = parts.slice(1).join(" ").trim();
|
|
394
|
+
if (args.startsWith("delete ")) {
|
|
395
|
+
const deleteId = args.slice(7).trim();
|
|
396
|
+
if (deleteSession(deleteId)) {
|
|
397
|
+
console.log(chalk2.green(`
|
|
398
|
+
Session ${deleteId.slice(0, 8)} deleted.
|
|
399
|
+
`));
|
|
400
|
+
} else {
|
|
401
|
+
console.log(chalk2.red(`
|
|
402
|
+
Session not found: ${deleteId}
|
|
403
|
+
`));
|
|
404
|
+
}
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
if (args) {
|
|
408
|
+
const targetId = args;
|
|
409
|
+
let loaded = loadSessionById(targetId);
|
|
410
|
+
if (!loaded) {
|
|
411
|
+
const sessions2 = listSessions();
|
|
412
|
+
const idx = parseInt(targetId, 10) - 1;
|
|
413
|
+
if (idx >= 0 && idx < sessions2.length) {
|
|
414
|
+
loaded = loadSessionById(sessions2[idx].id);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (loaded) {
|
|
418
|
+
Object.assign(ctx.session, loaded);
|
|
419
|
+
console.log(chalk2.green(`
|
|
420
|
+
Switched to session ${loaded.id.slice(0, 8)} (${loaded.messages.length} messages)
|
|
421
|
+
`));
|
|
422
|
+
} else {
|
|
423
|
+
console.log(chalk2.red(`
|
|
424
|
+
Session not found: ${targetId}
|
|
425
|
+
`));
|
|
426
|
+
}
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
const sessions = listSessions();
|
|
430
|
+
if (sessions.length === 0) {
|
|
431
|
+
console.log(chalk2.dim("\n No saved sessions.\n"));
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
console.log(chalk2.bold("\n Recent Sessions:"));
|
|
435
|
+
sessions.forEach((s, i) => {
|
|
436
|
+
const date = new Date(s.startedAt).toLocaleDateString();
|
|
437
|
+
const current = s.id === ctx.session.id ? chalk2.green(" <- current") : "";
|
|
438
|
+
console.log(chalk2.dim(` ${i + 1}. `) + chalk2.white(`${s.name || s.preview}`) + chalk2.dim(` (${s.messageCount} msgs, ${date})`) + current);
|
|
439
|
+
});
|
|
440
|
+
console.log(chalk2.dim(`
|
|
441
|
+
Use /sessions <n> to switch, /sessions delete <id> to delete.
|
|
442
|
+
`));
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
case "editor": {
|
|
446
|
+
const editor = process.env.EDITOR || process.env.VISUAL || "vi";
|
|
447
|
+
const tmpFile = path2.join(os.tmpdir(), `workermill-${Date.now()}.md`);
|
|
448
|
+
fs2.writeFileSync(tmpFile, "", "utf-8");
|
|
449
|
+
try {
|
|
450
|
+
execSync2(`${editor} ${tmpFile}`, { stdio: "inherit" });
|
|
451
|
+
const content = fs2.readFileSync(tmpFile, "utf-8").trim();
|
|
452
|
+
fs2.unlinkSync(tmpFile);
|
|
453
|
+
if (content) {
|
|
454
|
+
ctx.processInput(content);
|
|
455
|
+
} else {
|
|
456
|
+
console.log(chalk2.dim("\n Empty input, cancelled.\n"));
|
|
457
|
+
}
|
|
458
|
+
} catch {
|
|
459
|
+
console.log(chalk2.red("\n Editor failed.\n"));
|
|
460
|
+
}
|
|
461
|
+
break;
|
|
462
|
+
}
|
|
463
|
+
case "plan": {
|
|
464
|
+
const arg = parts[1];
|
|
465
|
+
if (arg === "off" || ctx.planMode) {
|
|
466
|
+
ctx.setPlanMode(false);
|
|
467
|
+
console.log(chalk2.green("\n Plan mode off. Full tools available.\n"));
|
|
468
|
+
} else {
|
|
469
|
+
ctx.setPlanMode(true);
|
|
470
|
+
console.log(chalk2.cyan("\n [PLAN MODE] Read-only tools only. Use /plan off to exit.\n"));
|
|
471
|
+
}
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
474
|
+
case "quit":
|
|
475
|
+
case "exit": {
|
|
476
|
+
const { exitTerminal: exitTerminal2 } = await import("./terminal-ILMO7Z3P.js");
|
|
477
|
+
exitTerminal2();
|
|
478
|
+
console.log(chalk2.dim(" Goodbye!"));
|
|
479
|
+
process.exit(0);
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
default:
|
|
483
|
+
console.log(chalk2.yellow(`
|
|
484
|
+
Unknown command: /${command}. Type /help for available commands.
|
|
485
|
+
`));
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// src/logger.js
|
|
490
|
+
import fs3 from "fs";
|
|
491
|
+
import path3 from "path";
|
|
492
|
+
var LOG_DIR = path3.join(process.cwd(), ".workermill");
|
|
493
|
+
var LOG_FILE = path3.join(LOG_DIR, "cli.log");
|
|
494
|
+
var logStream = null;
|
|
495
|
+
function ensureLogDir() {
|
|
496
|
+
if (!fs3.existsSync(LOG_DIR)) {
|
|
497
|
+
fs3.mkdirSync(LOG_DIR, { recursive: true });
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
function getStream() {
|
|
501
|
+
if (!logStream) {
|
|
502
|
+
ensureLogDir();
|
|
503
|
+
logStream = fs3.createWriteStream(LOG_FILE, { flags: "a" });
|
|
504
|
+
}
|
|
505
|
+
return logStream;
|
|
506
|
+
}
|
|
507
|
+
function timestamp() {
|
|
508
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
509
|
+
}
|
|
510
|
+
function log(level, message, data) {
|
|
511
|
+
const entry = data ? `[${timestamp()}] ${level}: ${message} ${JSON.stringify(data)}` : `[${timestamp()}] ${level}: ${message}`;
|
|
512
|
+
getStream().write(entry + "\n");
|
|
513
|
+
}
|
|
514
|
+
function info(message, data) {
|
|
515
|
+
log("INFO", message, data);
|
|
516
|
+
}
|
|
517
|
+
function error(message, data) {
|
|
518
|
+
log("ERROR", message, data);
|
|
519
|
+
}
|
|
520
|
+
function debug(message, data) {
|
|
521
|
+
log("DEBUG", message, data);
|
|
522
|
+
}
|
|
523
|
+
function flush() {
|
|
524
|
+
if (logStream) {
|
|
525
|
+
logStream.end();
|
|
526
|
+
logStream = null;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// src/agent.js
|
|
531
|
+
var HISTORY_FILE = pathModule.join(process.env.HOME || "~", ".workermill", "history");
|
|
532
|
+
var MAX_HISTORY = 1e3;
|
|
533
|
+
function loadHistory() {
|
|
534
|
+
try {
|
|
535
|
+
if (fs4.existsSync(HISTORY_FILE)) {
|
|
536
|
+
const lines = fs4.readFileSync(HISTORY_FILE, "utf-8").split("\n").filter(Boolean);
|
|
537
|
+
return lines.slice(-MAX_HISTORY);
|
|
538
|
+
}
|
|
539
|
+
} catch {
|
|
540
|
+
}
|
|
541
|
+
return [];
|
|
542
|
+
}
|
|
543
|
+
function appendHistory(line) {
|
|
544
|
+
try {
|
|
545
|
+
const dir = pathModule.dirname(HISTORY_FILE);
|
|
546
|
+
if (!fs4.existsSync(dir))
|
|
547
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
548
|
+
fs4.appendFileSync(HISTORY_FILE, line + "\n", "utf-8");
|
|
549
|
+
} catch {
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
var SLASH_COMMANDS = [
|
|
553
|
+
"/help",
|
|
554
|
+
"/clear",
|
|
555
|
+
"/compact",
|
|
556
|
+
"/status",
|
|
557
|
+
"/model",
|
|
558
|
+
"/quit",
|
|
559
|
+
"/cost",
|
|
560
|
+
"/git",
|
|
561
|
+
"/editor",
|
|
562
|
+
"/plan",
|
|
563
|
+
"/sessions",
|
|
564
|
+
"/exit"
|
|
565
|
+
];
|
|
566
|
+
function completer(line) {
|
|
567
|
+
if (line.startsWith("/")) {
|
|
568
|
+
const hits = SLASH_COMMANDS.filter((c) => c.startsWith(line));
|
|
569
|
+
return [hits.length ? hits : SLASH_COMMANDS, line];
|
|
570
|
+
}
|
|
571
|
+
return [[], line];
|
|
572
|
+
}
|
|
573
|
+
async function runAgent(config, trustAll, resume, startInPlanMode, fullDisk) {
|
|
574
|
+
const { provider, model: modelName, apiKey, host } = getProviderForPersona(config);
|
|
575
|
+
if (apiKey) {
|
|
576
|
+
const envMap = {
|
|
577
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
578
|
+
openai: "OPENAI_API_KEY",
|
|
579
|
+
google: "GOOGLE_API_KEY"
|
|
580
|
+
};
|
|
581
|
+
const envVar = envMap[provider];
|
|
582
|
+
if (envVar && !process.env[envVar]) {
|
|
583
|
+
process.env[envVar] = apiKey;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
const aiProvider = provider;
|
|
587
|
+
const model = createModel(aiProvider, modelName, host);
|
|
588
|
+
const workingDir = process.cwd();
|
|
589
|
+
const sandboxed = !fullDisk;
|
|
590
|
+
const tools = createToolDefinitions(workingDir, model, sandboxed);
|
|
591
|
+
const permissions = new PermissionManager(trustAll);
|
|
592
|
+
let session;
|
|
593
|
+
if (resume) {
|
|
594
|
+
const loaded = loadLatestSession();
|
|
595
|
+
if (loaded) {
|
|
596
|
+
session = loaded;
|
|
597
|
+
console.log(chalk3.green(` Resumed session ${session.id.slice(0, 8)}... (${session.messages.length} messages)
|
|
598
|
+
`));
|
|
599
|
+
} else {
|
|
600
|
+
session = createSession(provider, modelName);
|
|
601
|
+
console.log(chalk3.dim(" No previous session found, starting fresh.\n"));
|
|
602
|
+
}
|
|
603
|
+
} else {
|
|
604
|
+
session = createSession(provider, modelName);
|
|
605
|
+
}
|
|
606
|
+
let agentState = "idle";
|
|
607
|
+
let currentAbortController = null;
|
|
608
|
+
let lastCtrlCTime = 0;
|
|
609
|
+
let planMode = startInPlanMode || false;
|
|
610
|
+
const costTracker = new CostTracker();
|
|
611
|
+
const READ_ONLY_TOOLS = /* @__PURE__ */ new Set(["read_file", "glob", "grep", "ls", "sub_agent"]);
|
|
612
|
+
function getActiveTools() {
|
|
613
|
+
if (!planMode)
|
|
614
|
+
return permissionedTools;
|
|
615
|
+
const filtered = {};
|
|
616
|
+
for (const [name, def] of Object.entries(permissionedTools)) {
|
|
617
|
+
if (READ_ONLY_TOOLS.has(name)) {
|
|
618
|
+
filtered[name] = def;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return filtered;
|
|
622
|
+
}
|
|
623
|
+
const permissionedTools = {};
|
|
624
|
+
for (const [name, toolDef] of Object.entries(tools)) {
|
|
625
|
+
const td = toolDef;
|
|
626
|
+
permissionedTools[name] = {
|
|
627
|
+
...td,
|
|
628
|
+
execute: async (input) => {
|
|
629
|
+
const prevState = agentState;
|
|
630
|
+
agentState = "permission_waiting";
|
|
631
|
+
const allowed = await permissions.checkPermission(name, input);
|
|
632
|
+
if (!allowed) {
|
|
633
|
+
agentState = prevState;
|
|
634
|
+
debug("Tool denied by user", { tool: name });
|
|
635
|
+
return "Tool execution denied by user.";
|
|
636
|
+
}
|
|
637
|
+
info("Tool call", { tool: name, input: JSON.stringify(input).slice(0, 200) });
|
|
638
|
+
printToolCall(name, input);
|
|
639
|
+
agentState = "tool_executing";
|
|
640
|
+
const result = await td.execute(input);
|
|
641
|
+
agentState = "streaming";
|
|
642
|
+
const resultStr = typeof result === "string" ? result : JSON.stringify(result);
|
|
643
|
+
debug("Tool result", { tool: name, result: resultStr.slice(0, 200) });
|
|
644
|
+
printToolResult(name, resultStr);
|
|
645
|
+
return result;
|
|
646
|
+
}
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
info("Session started", { provider, model: modelName, workingDir, trustAll });
|
|
650
|
+
initTerminal();
|
|
651
|
+
printHeader("0.1.0", provider, modelName, workingDir);
|
|
652
|
+
const statusText = printStatusBar(provider, modelName, 0, planMode ? "PLAN" : trustAll ? "trust all" : "ask", 0);
|
|
653
|
+
setStatusBar(statusText);
|
|
654
|
+
const rl = readline2.createInterface({
|
|
655
|
+
input: process.stdin,
|
|
656
|
+
output: process.stdout,
|
|
657
|
+
prompt: chalk3.cyan("\u276F "),
|
|
658
|
+
completer
|
|
659
|
+
});
|
|
660
|
+
function promptWithStatus() {
|
|
661
|
+
showStatusBar();
|
|
662
|
+
rl.prompt();
|
|
663
|
+
}
|
|
664
|
+
const history = loadHistory();
|
|
665
|
+
rl.history = history.reverse();
|
|
666
|
+
permissions.setReadline(rl);
|
|
667
|
+
process.on("SIGINT", () => {
|
|
668
|
+
if (agentState === "streaming") {
|
|
669
|
+
if (currentAbortController)
|
|
670
|
+
currentAbortController.abort();
|
|
671
|
+
console.log(chalk3.yellow("\n [cancelled]"));
|
|
672
|
+
agentState = "idle";
|
|
673
|
+
currentAbortController = null;
|
|
674
|
+
processing = false;
|
|
675
|
+
rl.resume();
|
|
676
|
+
promptWithStatus();
|
|
677
|
+
} else if (agentState === "tool_executing") {
|
|
678
|
+
console.log(chalk3.yellow("\n [cancelling...]"));
|
|
679
|
+
killActiveProcess();
|
|
680
|
+
if (currentAbortController)
|
|
681
|
+
currentAbortController.abort();
|
|
682
|
+
agentState = "idle";
|
|
683
|
+
currentAbortController = null;
|
|
684
|
+
processing = false;
|
|
685
|
+
rl.resume();
|
|
686
|
+
promptWithStatus();
|
|
687
|
+
} else if (agentState === "permission_waiting") {
|
|
688
|
+
permissions.cancelPrompt();
|
|
689
|
+
console.log(chalk3.yellow("\n [cancelled]"));
|
|
690
|
+
agentState = "idle";
|
|
691
|
+
currentAbortController = null;
|
|
692
|
+
processing = false;
|
|
693
|
+
rl.resume();
|
|
694
|
+
promptWithStatus();
|
|
695
|
+
} else {
|
|
696
|
+
const now = Date.now();
|
|
697
|
+
if (now - lastCtrlCTime < 500) {
|
|
698
|
+
saveSession(session);
|
|
699
|
+
exitTerminal();
|
|
700
|
+
console.log(chalk3.dim(" Goodbye!"));
|
|
701
|
+
process.exit(0);
|
|
702
|
+
}
|
|
703
|
+
lastCtrlCTime = now;
|
|
704
|
+
console.log(chalk3.dim("\n Press Ctrl+C again to exit."));
|
|
705
|
+
promptWithStatus();
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
const systemPrompt = `You are WorkerMill, an AI coding agent running in the user's terminal.
|
|
709
|
+
You have access to tools for reading, writing, and editing files, running bash commands, searching code, and fetching web content.
|
|
710
|
+
|
|
711
|
+
Working directory: ${workingDir}
|
|
712
|
+
|
|
713
|
+
Guidelines:
|
|
714
|
+
- Be concise and direct in your responses
|
|
715
|
+
- Use tools proactively to explore the codebase before making changes
|
|
716
|
+
- When editing files, read them first to understand context
|
|
717
|
+
- Prefer editing existing files over creating new ones
|
|
718
|
+
- Run tests after making changes when test infrastructure exists
|
|
719
|
+
- Use glob and grep to find relevant files before reading them
|
|
720
|
+
|
|
721
|
+
Focus on writing clean, production-ready code.`;
|
|
722
|
+
promptWithStatus();
|
|
723
|
+
let processing = false;
|
|
724
|
+
let multilineBacktick = false;
|
|
725
|
+
let multilineBackslash = false;
|
|
726
|
+
let multilineBuffer = [];
|
|
727
|
+
const cmdCtx = {
|
|
728
|
+
config,
|
|
729
|
+
session,
|
|
730
|
+
costTracker,
|
|
731
|
+
workingDir,
|
|
732
|
+
planMode,
|
|
733
|
+
setPlanMode: (mode) => {
|
|
734
|
+
planMode = mode;
|
|
735
|
+
cmdCtx.planMode = mode;
|
|
736
|
+
},
|
|
737
|
+
processInput
|
|
738
|
+
};
|
|
739
|
+
function processInput(input) {
|
|
740
|
+
processing = true;
|
|
741
|
+
rl.pause();
|
|
742
|
+
(async () => {
|
|
743
|
+
try {
|
|
744
|
+
addMessage(session, "user", input);
|
|
745
|
+
appendHistory(input);
|
|
746
|
+
if (!session.name) {
|
|
747
|
+
session.name = input.slice(0, 50).replace(/\n/g, " ");
|
|
748
|
+
}
|
|
749
|
+
info("User message", { length: input.length, preview: input.slice(0, 100) });
|
|
750
|
+
if (session.messages.length <= 1) {
|
|
751
|
+
const { classifyComplexity, runOrchestration } = await import("./orchestrator-NMTZUS23.js");
|
|
752
|
+
console.log(chalk3.dim("\n Analyzing task complexity..."));
|
|
753
|
+
info("Running complexity classifier");
|
|
754
|
+
const classification = await classifyComplexity(config, input);
|
|
755
|
+
info("Classification result", { isMulti: classification.isMulti, reason: classification.reason });
|
|
756
|
+
if (classification.isMulti) {
|
|
757
|
+
console.log(chalk3.dim(` \u2192 multi-expert (${classification.reason})
|
|
758
|
+
`));
|
|
759
|
+
await runOrchestration(config, input, trustAll, sandboxed, rl);
|
|
760
|
+
processing = false;
|
|
761
|
+
rl.resume();
|
|
762
|
+
promptWithStatus();
|
|
763
|
+
return;
|
|
764
|
+
} else {
|
|
765
|
+
console.log(chalk3.dim(` \u2192 single agent (${classification.reason})
|
|
766
|
+
`));
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
info("Starting streamText", { model: modelName, messageCount: session.messages.length });
|
|
770
|
+
const thinkingSpinner = ora2({ stream: process.stdout, text: chalk3.dim("Thinking..."), prefixText: " " }).start();
|
|
771
|
+
currentAbortController = new AbortController();
|
|
772
|
+
const timeoutId = setTimeout(() => currentAbortController?.abort(), 10 * 60 * 1e3);
|
|
773
|
+
agentState = "streaming";
|
|
774
|
+
const stream = streamText({
|
|
775
|
+
model,
|
|
776
|
+
system: systemPrompt,
|
|
777
|
+
messages: session.messages.map((m) => ({
|
|
778
|
+
role: m.role,
|
|
779
|
+
content: m.content
|
|
780
|
+
})),
|
|
781
|
+
tools: getActiveTools(),
|
|
782
|
+
stopWhen: stepCountIs(100),
|
|
783
|
+
abortSignal: currentAbortController.signal
|
|
784
|
+
});
|
|
785
|
+
let fullText = "";
|
|
786
|
+
let spinnerStopped = false;
|
|
787
|
+
for await (const chunk of stream.textStream) {
|
|
788
|
+
if (!spinnerStopped) {
|
|
789
|
+
thinkingSpinner.stop();
|
|
790
|
+
spinnerStopped = true;
|
|
791
|
+
process.stdout.write("\n");
|
|
792
|
+
}
|
|
793
|
+
process.stdout.write(chalk3.white(chunk));
|
|
794
|
+
fullText += chunk;
|
|
795
|
+
}
|
|
796
|
+
clearTimeout(timeoutId);
|
|
797
|
+
agentState = "idle";
|
|
798
|
+
currentAbortController = null;
|
|
799
|
+
if (!spinnerStopped) {
|
|
800
|
+
thinkingSpinner.stop();
|
|
801
|
+
process.stdout.write("\n");
|
|
802
|
+
}
|
|
803
|
+
if (fullText.trim()) {
|
|
804
|
+
process.stdout.write("\n");
|
|
805
|
+
}
|
|
806
|
+
const usage = await stream.totalUsage;
|
|
807
|
+
const tokens = (usage?.inputTokens || 0) + (usage?.outputTokens || 0);
|
|
808
|
+
session.totalTokens += tokens;
|
|
809
|
+
costTracker.addUsage("agent", provider, modelName, usage?.inputTokens || 0, usage?.outputTokens || 0);
|
|
810
|
+
const finalText = await stream.text;
|
|
811
|
+
info("Response complete", { tokens, textLength: finalText.length });
|
|
812
|
+
addMessage(session, "assistant", finalText);
|
|
813
|
+
saveSession(session);
|
|
814
|
+
const compactionLevel = shouldCompact(session.totalTokens, modelName);
|
|
815
|
+
if (compactionLevel !== "none") {
|
|
816
|
+
const spinner = ora2({ stream: process.stdout, text: `Compacting conversation (${compactionLevel})...`, prefixText: " " }).start();
|
|
817
|
+
const plainMessages = session.messages.map((m) => ({ role: m.role, content: m.content }));
|
|
818
|
+
const compacted = await compactMessages(model, plainMessages, compactionLevel);
|
|
819
|
+
session.messages = compacted.map((m) => ({
|
|
820
|
+
role: m.role,
|
|
821
|
+
content: m.content,
|
|
822
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
823
|
+
}));
|
|
824
|
+
saveSession(session);
|
|
825
|
+
spinner.succeed("Conversation compacted");
|
|
826
|
+
}
|
|
827
|
+
const bar = printStatusBar(provider, modelName, session.totalTokens, planMode ? "PLAN" : trustAll ? "trust all" : "ask", costTracker.getTotalCost());
|
|
828
|
+
setStatusBar(bar);
|
|
829
|
+
} catch (err) {
|
|
830
|
+
agentState = "idle";
|
|
831
|
+
currentAbortController = null;
|
|
832
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
833
|
+
} else {
|
|
834
|
+
error("Agent error", { error: err instanceof Error ? err.message : String(err) });
|
|
835
|
+
printError(err instanceof Error ? err.message : String(err));
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
processing = false;
|
|
839
|
+
rl.resume();
|
|
840
|
+
promptWithStatus();
|
|
841
|
+
})();
|
|
842
|
+
}
|
|
843
|
+
rl.on("line", (input) => {
|
|
844
|
+
if (permissions.questionActive)
|
|
845
|
+
return;
|
|
846
|
+
if (input.trim() === "```" && !multilineBacktick && !multilineBackslash) {
|
|
847
|
+
multilineBacktick = true;
|
|
848
|
+
multilineBuffer = [];
|
|
849
|
+
process.stdout.write(chalk3.dim(" ... "));
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
if (input.trim() === "```" && multilineBacktick) {
|
|
853
|
+
multilineBacktick = false;
|
|
854
|
+
const fullInput = multilineBuffer.join("\n");
|
|
855
|
+
multilineBuffer = [];
|
|
856
|
+
if (!fullInput.trim() || processing) {
|
|
857
|
+
rl.prompt();
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
processInput(fullInput);
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
if (multilineBacktick) {
|
|
864
|
+
multilineBuffer.push(input);
|
|
865
|
+
process.stdout.write(chalk3.dim(" ... "));
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
if (input.endsWith("\\") && !input.endsWith("\\\\")) {
|
|
869
|
+
if (!multilineBackslash) {
|
|
870
|
+
multilineBackslash = true;
|
|
871
|
+
multilineBuffer = [input.slice(0, -1)];
|
|
872
|
+
} else {
|
|
873
|
+
multilineBuffer.push(input.slice(0, -1));
|
|
874
|
+
}
|
|
875
|
+
process.stdout.write(chalk3.dim(" ... "));
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
if (multilineBackslash) {
|
|
879
|
+
multilineBackslash = false;
|
|
880
|
+
multilineBuffer.push(input);
|
|
881
|
+
const fullInput = multilineBuffer.join("\n");
|
|
882
|
+
multilineBuffer = [];
|
|
883
|
+
if (!fullInput.trim() || processing) {
|
|
884
|
+
rl.prompt();
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
processInput(fullInput);
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
const trimmed = input.trim();
|
|
891
|
+
if (!trimmed || processing) {
|
|
892
|
+
if (!processing)
|
|
893
|
+
rl.prompt();
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
if (trimmed.startsWith("!")) {
|
|
897
|
+
const cmd = trimmed.slice(1).trim();
|
|
898
|
+
if (cmd) {
|
|
899
|
+
try {
|
|
900
|
+
const output = execSync3(cmd, { cwd: workingDir, encoding: "utf-8", timeout: 3e4 });
|
|
901
|
+
if (output.trim())
|
|
902
|
+
console.log(output);
|
|
903
|
+
} catch (err) {
|
|
904
|
+
console.log(chalk3.red(err.stderr || err.message));
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
rl.prompt();
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
if (trimmed.startsWith("/")) {
|
|
911
|
+
processing = true;
|
|
912
|
+
rl.pause();
|
|
913
|
+
handleCommand(trimmed, cmdCtx).then(() => {
|
|
914
|
+
processing = false;
|
|
915
|
+
rl.resume();
|
|
916
|
+
promptWithStatus();
|
|
917
|
+
});
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
processInput(trimmed);
|
|
921
|
+
});
|
|
922
|
+
rl.on("close", () => {
|
|
923
|
+
info("Session ended", { totalTokens: session.totalTokens, messages: session.messages.length });
|
|
924
|
+
flush();
|
|
925
|
+
const cleanup = () => {
|
|
926
|
+
saveSession(session);
|
|
927
|
+
exitTerminal();
|
|
928
|
+
console.log(chalk3.dim(" Goodbye!"));
|
|
929
|
+
process.exit(0);
|
|
930
|
+
};
|
|
931
|
+
if (processing) {
|
|
932
|
+
const checkDone = setInterval(() => {
|
|
933
|
+
if (!processing) {
|
|
934
|
+
clearInterval(checkDone);
|
|
935
|
+
cleanup();
|
|
936
|
+
}
|
|
937
|
+
}, 500);
|
|
938
|
+
} else {
|
|
939
|
+
cleanup();
|
|
940
|
+
}
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// src/index.ts
|
|
945
|
+
var VERSION = "0.1.0";
|
|
946
|
+
var program = new Command().name("workermill").description("AI coding agent with multi-expert orchestration").version(VERSION).option("--provider <provider>", "Override default provider").option("--model <model>", "Override model").option("--trust", "Skip all tool permission prompts").option("--resume", "Resume the last conversation").option("--plan", "Start in plan mode (read-only tools)").option("--auto-revise", "Auto-revise on failed reviews without prompting").option("--max-revisions <n>", "Max review\u2192revise cycles (default: 2)", parseInt).option("--critic", "Run separate critic pass on plan before execution").option("--full-disk", "Allow tools to access files outside working directory (default: restricted to cwd)").action(async (options) => {
|
|
947
|
+
let config = loadConfig();
|
|
948
|
+
if (!config) {
|
|
949
|
+
config = await runSetup();
|
|
950
|
+
}
|
|
951
|
+
if (options.provider) {
|
|
952
|
+
config.default = options.provider;
|
|
953
|
+
}
|
|
954
|
+
if (options.model) {
|
|
955
|
+
const providerConfig = config.providers[config.default];
|
|
956
|
+
if (providerConfig) {
|
|
957
|
+
providerConfig.model = options.model;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
if (options.autoRevise || options.maxRevisions || options.critic) {
|
|
961
|
+
config.review = {
|
|
962
|
+
...config.review,
|
|
963
|
+
...options.autoRevise ? { autoRevise: true } : {},
|
|
964
|
+
...options.maxRevisions ? { maxRevisions: options.maxRevisions } : {},
|
|
965
|
+
...options.critic ? { useCritic: true } : {}
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
const fullDisk = options.fullDisk || false;
|
|
969
|
+
await runAgent(config, options.trust || false, options.resume || false, options.plan || false, fullDisk);
|
|
970
|
+
});
|
|
971
|
+
program.parse();
|