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.
@@ -0,0 +1,1906 @@
1
+ // src/config.js
2
+ import fs from "fs";
3
+ import path from "path";
4
+ import os from "os";
5
+ var CONFIG_DIR = path.join(os.homedir(), ".workermill");
6
+ var CONFIG_FILE = path.join(CONFIG_DIR, "cli.json");
7
+ function loadConfig() {
8
+ try {
9
+ if (!fs.existsSync(CONFIG_FILE))
10
+ return null;
11
+ const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
12
+ return JSON.parse(raw);
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+ function saveConfig(config) {
18
+ if (!fs.existsSync(CONFIG_DIR)) {
19
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
20
+ }
21
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", "utf-8");
22
+ }
23
+ function getProviderForPersona(config, persona) {
24
+ const providerName = persona && config.routing?.[persona] || config.default;
25
+ const providerConfig = config.providers[providerName];
26
+ if (!providerConfig) {
27
+ throw new Error(`Provider "${providerName}" not found in configuration.`);
28
+ }
29
+ return {
30
+ provider: providerName,
31
+ model: providerConfig.model,
32
+ apiKey: providerConfig.apiKey?.startsWith("{env:") ? process.env[providerConfig.apiKey.slice(5, -1)] || void 0 : providerConfig.apiKey,
33
+ host: providerConfig.host
34
+ };
35
+ }
36
+
37
+ // ../packages/engine/src/model-factory.js
38
+ import { anthropic } from "@ai-sdk/anthropic";
39
+ import { openai } from "@ai-sdk/openai";
40
+ import { google } from "@ai-sdk/google";
41
+ import { createOllama } from "ollama-ai-provider-v2";
42
+ function createModel(provider, modelName, ollamaHost) {
43
+ switch (provider) {
44
+ case "anthropic":
45
+ return anthropic(modelName);
46
+ case "openai":
47
+ return openai(modelName);
48
+ case "google":
49
+ case "gemini":
50
+ return google(modelName);
51
+ case "ollama": {
52
+ const host = ollamaHost || "http://localhost:11434";
53
+ const ollamaProvider = createOllama({ baseURL: `${host}/api` });
54
+ return ollamaProvider(modelName);
55
+ }
56
+ default:
57
+ throw new Error(`Unsupported provider: ${provider}`);
58
+ }
59
+ }
60
+
61
+ // ../packages/engine/src/tools/index.js
62
+ import { tool } from "ai";
63
+ import { z } from "zod";
64
+ import path9 from "path";
65
+
66
+ // ../packages/engine/src/tools/bash.js
67
+ import { spawn } from "child_process";
68
+ var activeChild = null;
69
+ function killActiveProcess() {
70
+ if (activeChild?.pid) {
71
+ try {
72
+ process.kill(-activeChild.pid, "SIGTERM");
73
+ setTimeout(() => {
74
+ try {
75
+ if (activeChild?.pid)
76
+ process.kill(-activeChild.pid, "SIGKILL");
77
+ } catch {
78
+ }
79
+ }, 3e3);
80
+ } catch {
81
+ }
82
+ }
83
+ }
84
+ var description = "Execute a bash command and return the output. Use for running shell commands, git operations, npm commands, etc.";
85
+ var LONG_RUNNING_PATTERNS = [
86
+ /\bnpm\s+(?:run\s+)?(?:dev|start|serve)\b/,
87
+ /\bnpx\s+(?:next|vite|webpack-dev-server|react-scripts\s+start)\b/,
88
+ /\bnodemon\b/,
89
+ /\btsc\s+--watch\b/,
90
+ /\bwebpack\s+serve\b/,
91
+ /\byarn\s+(?:dev|start|serve)\b/,
92
+ /\bpnpm\s+(?:dev|start|serve)\b/,
93
+ /\bpython\s+-m\s+(?:http\.server|flask\s+run|uvicorn|gunicorn)\b/,
94
+ /\brails\s+server\b/,
95
+ /\bphp\s+-S\b/,
96
+ /\bdocker\s+compose\s+up(?!\s+--build\b.*--exit)/
97
+ ];
98
+ function isLongRunning(command) {
99
+ for (const pattern of LONG_RUNNING_PATTERNS) {
100
+ if (pattern.test(command)) {
101
+ const match = command.match(pattern);
102
+ return match ? match[0] : "long-running process";
103
+ }
104
+ }
105
+ return null;
106
+ }
107
+ async function execute({ command, cwd, timeout = 12e4 }) {
108
+ const longRunning = isLongRunning(command);
109
+ if (longRunning) {
110
+ return {
111
+ success: false,
112
+ exitCode: -1,
113
+ stdout: "",
114
+ stderr: "",
115
+ error: `Blocked: "${longRunning}" is a long-running process that would block execution. Use a one-shot command instead (e.g., "npx tsc --noEmit" to check compilation, "npm test" to run tests).`,
116
+ duration: 0
117
+ };
118
+ }
119
+ return new Promise((resolve) => {
120
+ const startTime = Date.now();
121
+ let stdout = "";
122
+ let stderr = "";
123
+ let killed = false;
124
+ const child = spawn("/bin/bash", ["-c", command], {
125
+ cwd: cwd || process.cwd(),
126
+ env: {
127
+ ...process.env,
128
+ PATH: "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
129
+ },
130
+ stdio: ["pipe", "pipe", "pipe"],
131
+ detached: true
132
+ });
133
+ activeChild = child;
134
+ const timeoutId = setTimeout(() => {
135
+ killed = true;
136
+ try {
137
+ process.kill(-child.pid, "SIGTERM");
138
+ } catch {
139
+ }
140
+ setTimeout(() => {
141
+ try {
142
+ process.kill(-child.pid, "SIGKILL");
143
+ } catch {
144
+ }
145
+ }, 5e3);
146
+ }, timeout);
147
+ child.stdout.on("data", (data) => {
148
+ stdout += data.toString();
149
+ if (stdout.length > 1024 * 1024) {
150
+ stdout = stdout.slice(0, 1024 * 1024) + "\n... [output truncated]";
151
+ }
152
+ });
153
+ child.stderr.on("data", (data) => {
154
+ stderr += data.toString();
155
+ if (stderr.length > 1024 * 1024) {
156
+ stderr = stderr.slice(0, 1024 * 1024) + "\n... [output truncated]";
157
+ }
158
+ });
159
+ child.on("close", (code) => {
160
+ clearTimeout(timeoutId);
161
+ activeChild = null;
162
+ const duration = Date.now() - startTime;
163
+ if (killed) {
164
+ resolve({
165
+ success: false,
166
+ exitCode: code,
167
+ stdout: stdout.trim(),
168
+ stderr: stderr.trim(),
169
+ error: `Command timed out after ${timeout}ms`,
170
+ duration
171
+ });
172
+ } else if (code === 0) {
173
+ resolve({
174
+ success: true,
175
+ exitCode: 0,
176
+ stdout: stdout.trim(),
177
+ stderr: stderr.trim(),
178
+ duration
179
+ });
180
+ } else {
181
+ resolve({
182
+ success: false,
183
+ exitCode: code,
184
+ stdout: stdout.trim(),
185
+ stderr: stderr.trim(),
186
+ error: `Command exited with code ${code}`,
187
+ duration
188
+ });
189
+ }
190
+ });
191
+ child.on("error", (err) => {
192
+ clearTimeout(timeoutId);
193
+ activeChild = null;
194
+ const duration = Date.now() - startTime;
195
+ resolve({
196
+ success: false,
197
+ exitCode: -1,
198
+ stdout: stdout.trim(),
199
+ stderr: stderr.trim(),
200
+ error: `Failed to execute command: ${err.message}`,
201
+ duration
202
+ });
203
+ });
204
+ });
205
+ }
206
+
207
+ // ../packages/engine/src/tools/read-file.js
208
+ import fs2 from "fs";
209
+ import path2 from "path";
210
+ var description2 = "Read the contents of a file. Returns the file content as a string. Supports text files of any type.";
211
+ async function execute2({ path: filePath, encoding = "utf8", maxLines, startLine }) {
212
+ try {
213
+ const absolutePath = path2.isAbsolute(filePath) ? filePath : path2.resolve(process.cwd(), filePath);
214
+ if (!fs2.existsSync(absolutePath)) {
215
+ return {
216
+ success: false,
217
+ error: `File not found: ${absolutePath}`
218
+ };
219
+ }
220
+ const stats = fs2.statSync(absolutePath);
221
+ if (stats.isDirectory()) {
222
+ return {
223
+ success: false,
224
+ error: `Path is a directory, not a file: ${absolutePath}`
225
+ };
226
+ }
227
+ const maxSize = 10 * 1024 * 1024;
228
+ if (stats.size > maxSize) {
229
+ return {
230
+ success: false,
231
+ error: `File is too large (${(stats.size / 1024 / 1024).toFixed(2)}MB). Max size: 10MB. Use maxLines parameter to read partial content.`
232
+ };
233
+ }
234
+ let content = fs2.readFileSync(absolutePath, encoding);
235
+ if (startLine !== void 0 || maxLines !== void 0) {
236
+ const lines = content.split("\n");
237
+ const start = (startLine || 1) - 1;
238
+ const end = maxLines !== void 0 ? start + maxLines : lines.length;
239
+ content = lines.slice(start, end).join("\n");
240
+ return {
241
+ success: true,
242
+ content,
243
+ path: absolutePath,
244
+ totalLines: lines.length,
245
+ linesReturned: Math.min(end - start, lines.length - start),
246
+ startLine: start + 1,
247
+ endLine: Math.min(end, lines.length)
248
+ };
249
+ }
250
+ return {
251
+ success: true,
252
+ content,
253
+ path: absolutePath,
254
+ size: stats.size
255
+ };
256
+ } catch (err) {
257
+ return {
258
+ success: false,
259
+ error: `Failed to read file: ${err.message}`
260
+ };
261
+ }
262
+ }
263
+
264
+ // ../packages/engine/src/tools/write-file.js
265
+ import fs3 from "fs";
266
+ import path3 from "path";
267
+ var description3 = "Write content to a file. Creates the file if it does not exist, and creates any necessary parent directories. Overwrites existing content.";
268
+ async function execute3({ path: filePath, content, encoding = "utf8", append = false }) {
269
+ try {
270
+ const absolutePath = path3.isAbsolute(filePath) ? filePath : path3.resolve(process.cwd(), filePath);
271
+ const dirPath = path3.dirname(absolutePath);
272
+ if (!fs3.existsSync(dirPath)) {
273
+ fs3.mkdirSync(dirPath, { recursive: true });
274
+ }
275
+ if (fs3.existsSync(absolutePath)) {
276
+ const stats2 = fs3.statSync(absolutePath);
277
+ if (stats2.isDirectory()) {
278
+ return {
279
+ success: false,
280
+ error: `Path is a directory, cannot write as file: ${absolutePath}`
281
+ };
282
+ }
283
+ }
284
+ if (append) {
285
+ fs3.appendFileSync(absolutePath, content, encoding);
286
+ } else {
287
+ fs3.writeFileSync(absolutePath, content, encoding);
288
+ }
289
+ const stats = fs3.statSync(absolutePath);
290
+ return {
291
+ success: true,
292
+ path: absolutePath,
293
+ size: stats.size,
294
+ action: append ? "appended" : "written",
295
+ linesWritten: content.split("\n").length
296
+ };
297
+ } catch (err) {
298
+ return {
299
+ success: false,
300
+ error: `Failed to write file: ${err.message}`
301
+ };
302
+ }
303
+ }
304
+
305
+ // ../packages/engine/src/tools/edit-file.js
306
+ import fs4 from "fs";
307
+ import path4 from "path";
308
+ var description4 = "Edit a file by finding and replacing text. The old_string must be unique in the file (or use replaceAll for multiple occurrences). Use this instead of write_file when making targeted changes to existing files.";
309
+ async function execute4({ path: filePath, old_string, new_string, replaceAll = false }) {
310
+ try {
311
+ const absolutePath = path4.isAbsolute(filePath) ? filePath : path4.resolve(process.cwd(), filePath);
312
+ if (!fs4.existsSync(absolutePath)) {
313
+ return {
314
+ success: false,
315
+ error: `File not found: ${absolutePath}`
316
+ };
317
+ }
318
+ const stats = fs4.statSync(absolutePath);
319
+ if (stats.isDirectory()) {
320
+ return {
321
+ success: false,
322
+ error: `Path is a directory, not a file: ${absolutePath}`
323
+ };
324
+ }
325
+ const content = fs4.readFileSync(absolutePath, "utf8");
326
+ if (!content.includes(old_string)) {
327
+ const lines = content.split("\n");
328
+ const preview = lines.slice(0, 20).map((l, i) => `${i + 1}: ${l}`).join("\n");
329
+ return {
330
+ success: false,
331
+ error: `old_string not found in file. Make sure it matches exactly including whitespace and indentation.`,
332
+ hint: "Use read_file first to see the exact content.",
333
+ filePreview: preview.length < 2e3 ? preview : preview.slice(0, 2e3) + "\n... [truncated]"
334
+ };
335
+ }
336
+ const occurrences = content.split(old_string).length - 1;
337
+ if (!replaceAll && occurrences > 1) {
338
+ return {
339
+ success: false,
340
+ error: `old_string found ${occurrences} times in file. Either provide more context to make it unique, or set replaceAll: true.`,
341
+ occurrences
342
+ };
343
+ }
344
+ let newContent;
345
+ if (replaceAll) {
346
+ newContent = content.split(old_string).join(new_string);
347
+ } else {
348
+ newContent = content.replace(old_string, new_string);
349
+ }
350
+ if (content === newContent) {
351
+ return {
352
+ success: false,
353
+ error: "No changes made. old_string and new_string may be identical."
354
+ };
355
+ }
356
+ fs4.writeFileSync(absolutePath, newContent, "utf8");
357
+ const oldLines = content.split("\n").length;
358
+ const newLines = newContent.split("\n").length;
359
+ const linesDiff = newLines - oldLines;
360
+ return {
361
+ success: true,
362
+ path: absolutePath,
363
+ replacements: replaceAll ? occurrences : 1,
364
+ linesBefore: oldLines,
365
+ linesAfter: newLines,
366
+ linesDiff: linesDiff > 0 ? `+${linesDiff}` : linesDiff.toString()
367
+ };
368
+ } catch (err) {
369
+ return {
370
+ success: false,
371
+ error: `Failed to edit file: ${err.message}`
372
+ };
373
+ }
374
+ }
375
+
376
+ // ../packages/engine/src/tools/glob.js
377
+ import fs5 from "fs";
378
+ import path5 from "path";
379
+ function globToRegex(pattern) {
380
+ let regex = "";
381
+ let i = 0;
382
+ const len = pattern.length;
383
+ while (i < len) {
384
+ const c = pattern[i];
385
+ if (c === "*") {
386
+ if (pattern[i + 1] === "*") {
387
+ if (pattern[i + 2] === "/" || pattern[i + 2] === void 0) {
388
+ regex += "(?:[^/]*(?:/[^/]*)*)";
389
+ i += pattern[i + 2] === "/" ? 3 : 2;
390
+ continue;
391
+ }
392
+ }
393
+ regex += "[^/]*";
394
+ i++;
395
+ } else if (c === "?") {
396
+ regex += "[^/]";
397
+ i++;
398
+ } else if (c === "[") {
399
+ let j = i + 1;
400
+ let classContent = "";
401
+ if (pattern[j] === "!") {
402
+ classContent += "^";
403
+ j++;
404
+ }
405
+ while (j < len && pattern[j] !== "]") {
406
+ classContent += pattern[j];
407
+ j++;
408
+ }
409
+ regex += `[${classContent}]`;
410
+ i = j + 1;
411
+ } else if (c === "{") {
412
+ let j = i + 1;
413
+ const options = [];
414
+ let current = "";
415
+ while (j < len && pattern[j] !== "}") {
416
+ if (pattern[j] === ",") {
417
+ options.push(current);
418
+ current = "";
419
+ } else {
420
+ current += pattern[j];
421
+ }
422
+ j++;
423
+ }
424
+ options.push(current);
425
+ regex += `(?:${options.map((o) => o.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})`;
426
+ i = j + 1;
427
+ } else if (c === "/" || c === "\\") {
428
+ regex += "[/\\\\]";
429
+ i++;
430
+ } else if (".+^${}()|[]\\".includes(c)) {
431
+ regex += "\\" + c;
432
+ i++;
433
+ } else {
434
+ regex += c;
435
+ i++;
436
+ }
437
+ }
438
+ return new RegExp(`^${regex}$`);
439
+ }
440
+ function walkDir(dir, files = [], maxDepth = 20, currentDepth = 0) {
441
+ if (currentDepth > maxDepth)
442
+ return files;
443
+ try {
444
+ const entries = fs5.readdirSync(dir, { withFileTypes: true });
445
+ for (const entry of entries) {
446
+ if (entry.name.startsWith(".") && entry.name !== ".")
447
+ continue;
448
+ if (entry.name === "node_modules")
449
+ continue;
450
+ if (entry.name === "__pycache__")
451
+ continue;
452
+ if (entry.name === ".git")
453
+ continue;
454
+ const fullPath = path5.join(dir, entry.name);
455
+ if (entry.isDirectory()) {
456
+ walkDir(fullPath, files, maxDepth, currentDepth + 1);
457
+ } else if (entry.isFile()) {
458
+ files.push(fullPath);
459
+ }
460
+ }
461
+ } catch (_err) {
462
+ }
463
+ return files;
464
+ }
465
+ var description5 = 'Find files matching a glob pattern. Supports patterns like "**/*.ts", "src/**/*.js", "*.{ts,tsx}". Excludes node_modules and hidden files by default.';
466
+ async function execute5({ pattern, cwd, maxResults = 1e3, includeHidden = false }) {
467
+ try {
468
+ const searchDir = cwd ? path5.isAbsolute(cwd) ? cwd : path5.resolve(process.cwd(), cwd) : process.cwd();
469
+ if (!fs5.existsSync(searchDir)) {
470
+ return {
471
+ success: false,
472
+ error: `Directory not found: ${searchDir}`
473
+ };
474
+ }
475
+ const allFiles = walkDir(searchDir);
476
+ const regex = globToRegex(pattern);
477
+ const matches = [];
478
+ for (const file of allFiles) {
479
+ const relativePath = path5.relative(searchDir, file).replace(/\\/g, "/");
480
+ if (!includeHidden && relativePath.split("/").some((part) => part.startsWith("."))) {
481
+ continue;
482
+ }
483
+ if (regex.test(relativePath)) {
484
+ matches.push({
485
+ path: file,
486
+ relativePath
487
+ });
488
+ if (matches.length >= maxResults) {
489
+ break;
490
+ }
491
+ }
492
+ }
493
+ matches.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
494
+ return {
495
+ success: true,
496
+ pattern,
497
+ cwd: searchDir,
498
+ matches: matches.map((m) => m.relativePath),
499
+ absolutePaths: matches.map((m) => m.path),
500
+ count: matches.length,
501
+ truncated: matches.length >= maxResults
502
+ };
503
+ } catch (err) {
504
+ return {
505
+ success: false,
506
+ error: `Glob search failed: ${err.message}`
507
+ };
508
+ }
509
+ }
510
+
511
+ // ../packages/engine/src/tools/grep.js
512
+ import fs6 from "fs";
513
+ import path6 from "path";
514
+ function walkDir2(dir, files = [], maxDepth = 20, currentDepth = 0) {
515
+ if (currentDepth > maxDepth)
516
+ return files;
517
+ try {
518
+ const entries = fs6.readdirSync(dir, { withFileTypes: true });
519
+ for (const entry of entries) {
520
+ if (entry.name === "node_modules")
521
+ continue;
522
+ if (entry.name === "__pycache__")
523
+ continue;
524
+ if (entry.name === ".git")
525
+ continue;
526
+ if (entry.name === "dist")
527
+ continue;
528
+ if (entry.name === "build")
529
+ continue;
530
+ if (entry.name === ".next")
531
+ continue;
532
+ const fullPath = path6.join(dir, entry.name);
533
+ if (entry.isDirectory()) {
534
+ if (!entry.name.startsWith(".")) {
535
+ walkDir2(fullPath, files, maxDepth, currentDepth + 1);
536
+ }
537
+ } else if (entry.isFile()) {
538
+ const ext = path6.extname(entry.name).toLowerCase();
539
+ const binaryExts = [
540
+ ".png",
541
+ ".jpg",
542
+ ".jpeg",
543
+ ".gif",
544
+ ".ico",
545
+ ".pdf",
546
+ ".zip",
547
+ ".tar",
548
+ ".gz",
549
+ ".exe",
550
+ ".dll",
551
+ ".so",
552
+ ".dylib",
553
+ ".woff",
554
+ ".woff2",
555
+ ".ttf",
556
+ ".eot"
557
+ ];
558
+ if (!binaryExts.includes(ext)) {
559
+ files.push(fullPath);
560
+ }
561
+ }
562
+ }
563
+ } catch (_err) {
564
+ }
565
+ return files;
566
+ }
567
+ function searchFile(filePath, regex, contextLines = 0) {
568
+ try {
569
+ const content = fs6.readFileSync(filePath, "utf8");
570
+ const lines = content.split("\n");
571
+ const matches = [];
572
+ for (let i = 0; i < lines.length; i++) {
573
+ const line = lines[i];
574
+ if (regex.test(line)) {
575
+ const match = {
576
+ line: i + 1,
577
+ content: line.trim(),
578
+ fullLine: line
579
+ };
580
+ if (contextLines > 0) {
581
+ const beforeStart = Math.max(0, i - contextLines);
582
+ const afterEnd = Math.min(lines.length - 1, i + contextLines);
583
+ match.before = lines.slice(beforeStart, i).map((l, idx) => ({
584
+ line: beforeStart + idx + 1,
585
+ content: l
586
+ }));
587
+ match.after = lines.slice(i + 1, afterEnd + 1).map((l, idx) => ({
588
+ line: i + 2 + idx,
589
+ content: l
590
+ }));
591
+ }
592
+ matches.push(match);
593
+ }
594
+ }
595
+ return matches;
596
+ } catch (_err) {
597
+ return [];
598
+ }
599
+ }
600
+ var description6 = "Search for a pattern in files. Uses regex pattern matching. Returns matching lines with file paths and line numbers.";
601
+ async function execute6({ pattern, path: searchPath, filePattern, ignoreCase = false, contextLines = 0, maxResults = 100 }) {
602
+ try {
603
+ let regex;
604
+ try {
605
+ regex = new RegExp(pattern, ignoreCase ? "gi" : "g");
606
+ } catch (err) {
607
+ return {
608
+ success: false,
609
+ error: `Invalid regex pattern: ${err.message}`
610
+ };
611
+ }
612
+ const targetPath = searchPath ? path6.isAbsolute(searchPath) ? searchPath : path6.resolve(process.cwd(), searchPath) : process.cwd();
613
+ if (!fs6.existsSync(targetPath)) {
614
+ return {
615
+ success: false,
616
+ error: `Path not found: ${targetPath}`
617
+ };
618
+ }
619
+ const stats = fs6.statSync(targetPath);
620
+ let filesToSearch = [];
621
+ if (stats.isFile()) {
622
+ filesToSearch = [targetPath];
623
+ } else if (stats.isDirectory()) {
624
+ filesToSearch = walkDir2(targetPath);
625
+ }
626
+ if (filePattern) {
627
+ const ext = filePattern.replace("*", "");
628
+ filesToSearch = filesToSearch.filter((f) => f.endsWith(ext));
629
+ }
630
+ const results = [];
631
+ let totalMatches = 0;
632
+ for (const file of filesToSearch) {
633
+ if (totalMatches >= maxResults)
634
+ break;
635
+ const matches = searchFile(file, regex, contextLines);
636
+ if (matches.length > 0) {
637
+ const relativePath = path6.relative(targetPath, file).replace(/\\/g, "/") || path6.basename(file);
638
+ for (const match of matches) {
639
+ if (totalMatches >= maxResults)
640
+ break;
641
+ results.push({
642
+ file: relativePath,
643
+ absolutePath: file,
644
+ line: match.line,
645
+ content: match.content,
646
+ ...contextLines > 0 ? { before: match.before, after: match.after } : {}
647
+ });
648
+ totalMatches++;
649
+ }
650
+ }
651
+ }
652
+ const byFile = {};
653
+ for (const result of results) {
654
+ if (!byFile[result.file]) {
655
+ byFile[result.file] = [];
656
+ }
657
+ byFile[result.file].push({
658
+ line: result.line,
659
+ content: result.content,
660
+ ...result.before ? { before: result.before } : {},
661
+ ...result.after ? { after: result.after } : {}
662
+ });
663
+ }
664
+ return {
665
+ success: true,
666
+ pattern,
667
+ searchPath: targetPath,
668
+ matchCount: totalMatches,
669
+ fileCount: Object.keys(byFile).length,
670
+ truncated: totalMatches >= maxResults,
671
+ results: byFile
672
+ };
673
+ } catch (err) {
674
+ return {
675
+ success: false,
676
+ error: `Grep search failed: ${err.message}`
677
+ };
678
+ }
679
+ }
680
+
681
+ // ../packages/engine/src/tools/ls.js
682
+ import fs7 from "fs";
683
+ import path7 from "path";
684
+ var description7 = "List directory contents in a tree format. Shows file names, types, and sizes. More efficient than bash ls \u2014 no subprocess needed, returns structured output.";
685
+ var DEFAULT_IGNORE = ["node_modules", ".git", "__pycache__", ".next", ".nuxt"];
686
+ function shouldIgnore(name, ignorePatterns) {
687
+ return ignorePatterns.some((pattern) => {
688
+ if (pattern.startsWith("*.")) {
689
+ return name.endsWith(pattern.slice(1));
690
+ }
691
+ return name === pattern;
692
+ });
693
+ }
694
+ function buildTree(dirPath, prefix, depth, maxDepth, ignorePatterns, state) {
695
+ if (depth > maxDepth || state.files + state.dirs >= state.maxFiles) {
696
+ if (depth > maxDepth) {
697
+ state.lines.push(`${prefix}... (max depth reached)`);
698
+ }
699
+ return;
700
+ }
701
+ let entries;
702
+ try {
703
+ entries = fs7.readdirSync(dirPath, { withFileTypes: true });
704
+ } catch {
705
+ state.lines.push(`${prefix}[permission denied]`);
706
+ return;
707
+ }
708
+ entries.sort((a, b) => {
709
+ if (a.isDirectory() && !b.isDirectory())
710
+ return -1;
711
+ if (!a.isDirectory() && b.isDirectory())
712
+ return 1;
713
+ return a.name.localeCompare(b.name);
714
+ });
715
+ entries = entries.filter((e) => !shouldIgnore(e.name, ignorePatterns));
716
+ for (let i = 0; i < entries.length; i++) {
717
+ if (state.files + state.dirs >= state.maxFiles) {
718
+ state.lines.push(`${prefix}... (${state.maxFiles} entries limit reached)`);
719
+ return;
720
+ }
721
+ const entry = entries[i];
722
+ const isLast = i === entries.length - 1;
723
+ const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
724
+ const childPrefix = isLast ? " " : "\u2502 ";
725
+ const fullPath = path7.join(dirPath, entry.name);
726
+ if (entry.isDirectory()) {
727
+ state.dirs++;
728
+ state.lines.push(`${prefix}${connector}${entry.name}/`);
729
+ buildTree(fullPath, prefix + childPrefix, depth + 1, maxDepth, ignorePatterns, state);
730
+ } else {
731
+ state.files++;
732
+ let size = "";
733
+ try {
734
+ const stat = fs7.statSync(fullPath);
735
+ if (stat.size < 1024)
736
+ size = ` (${stat.size}B)`;
737
+ else if (stat.size < 1024 * 1024)
738
+ size = ` (${(stat.size / 1024).toFixed(1)}KB)`;
739
+ else
740
+ size = ` (${(stat.size / (1024 * 1024)).toFixed(1)}MB)`;
741
+ } catch {
742
+ }
743
+ state.lines.push(`${prefix}${connector}${entry.name}${size}`);
744
+ }
745
+ }
746
+ }
747
+ async function execute7({ path: dirPath, ignore, maxDepth = 3, maxFiles = 1e3 }) {
748
+ try {
749
+ if (!fs7.existsSync(dirPath)) {
750
+ return { success: false, tree: "", totalFiles: 0, totalDirs: 0, truncated: false, error: `Directory not found: ${dirPath}` };
751
+ }
752
+ const stat = fs7.statSync(dirPath);
753
+ if (!stat.isDirectory()) {
754
+ return { success: false, tree: "", totalFiles: 0, totalDirs: 0, truncated: false, error: `Not a directory: ${dirPath}` };
755
+ }
756
+ const ignorePatterns = ignore ? [...DEFAULT_IGNORE, ...ignore] : DEFAULT_IGNORE;
757
+ const state = { files: 0, dirs: 0, lines: [], maxFiles };
758
+ state.lines.push(dirPath);
759
+ buildTree(dirPath, "", 0, maxDepth, ignorePatterns, state);
760
+ const truncated = state.files + state.dirs >= maxFiles;
761
+ return { success: true, tree: state.lines.join("\n"), totalFiles: state.files, totalDirs: state.dirs, truncated };
762
+ } catch (err) {
763
+ return { success: false, tree: "", totalFiles: 0, totalDirs: 0, truncated: false, error: `Failed to list directory: ${err instanceof Error ? err.message : String(err)}` };
764
+ }
765
+ }
766
+
767
+ // ../packages/engine/src/tools/fetch.js
768
+ var description8 = "Fetch content from a URL and return it as text or markdown. Useful for reading documentation, API references, error pages, and other web content.";
769
+ function htmlToText(html) {
770
+ return html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<\/?(p|div|br|h[1-6]|li|tr|blockquote|pre|hr)[^>]*>/gi, "\n").replace(/<[^>]+>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&nbsp;/g, " ").replace(/\n{3,}/g, "\n\n").trim();
771
+ }
772
+ function htmlToMarkdown(html) {
773
+ return html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, "").replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "").replace(/<h1[^>]*>([\s\S]*?)<\/h1>/gi, "# $1\n\n").replace(/<h2[^>]*>([\s\S]*?)<\/h2>/gi, "## $1\n\n").replace(/<h3[^>]*>([\s\S]*?)<\/h3>/gi, "### $1\n\n").replace(/<h4[^>]*>([\s\S]*?)<\/h4>/gi, "#### $1\n\n").replace(/<h5[^>]*>([\s\S]*?)<\/h5>/gi, "##### $1\n\n").replace(/<h6[^>]*>([\s\S]*?)<\/h6>/gi, "###### $1\n\n").replace(/<(strong|b)[^>]*>([\s\S]*?)<\/(strong|b)>/gi, "**$2**").replace(/<(em|i)[^>]*>([\s\S]*?)<\/(em|i)>/gi, "*$2*").replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, "`$1`").replace(/<pre[^>]*>([\s\S]*?)<\/pre>/gi, "```\n$1\n```\n\n").replace(/<a[^>]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/gi, "[$2]($1)").replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, "- $1\n").replace(/<\/p>/gi, "\n\n").replace(/<br\s*\/?>/gi, "\n").replace(/<hr\s*\/?>/gi, "\n---\n\n").replace(/<[^>]+>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&nbsp;/g, " ").replace(/\n{3,}/g, "\n\n").trim();
774
+ }
775
+ var MAX_CONTENT_SIZE = 512 * 1024;
776
+ async function execute8({ url, format = "markdown", timeout = 3e4 }) {
777
+ try {
778
+ let parsedUrl;
779
+ try {
780
+ parsedUrl = new URL(url);
781
+ } catch {
782
+ return { success: false, content: "", url, error: `Invalid URL: ${url}` };
783
+ }
784
+ if (!["http:", "https:"].includes(parsedUrl.protocol)) {
785
+ return { success: false, content: "", url, error: `Unsupported protocol: ${parsedUrl.protocol}. Only http and https are allowed.` };
786
+ }
787
+ const clampedTimeout = Math.min(timeout, 12e4);
788
+ const controller = new AbortController();
789
+ const timeoutId = setTimeout(() => controller.abort(), clampedTimeout);
790
+ try {
791
+ const response = await globalThis.fetch(url, {
792
+ signal: controller.signal,
793
+ headers: {
794
+ "User-Agent": "WorkerMill/1.0",
795
+ Accept: "text/html,application/xhtml+xml,text/plain,*/*"
796
+ }
797
+ });
798
+ clearTimeout(timeoutId);
799
+ if (!response.ok) {
800
+ return { success: false, content: "", url, statusCode: response.status, error: `HTTP ${response.status}: ${response.statusText}` };
801
+ }
802
+ const contentType = response.headers.get("content-type") || "";
803
+ let body = await response.text();
804
+ if (body.length > MAX_CONTENT_SIZE) {
805
+ body = body.slice(0, MAX_CONTENT_SIZE) + "\n\n... [content truncated at 512KB]";
806
+ }
807
+ let content;
808
+ if (contentType.includes("text/html") || contentType.includes("xhtml")) {
809
+ if (format === "text") {
810
+ content = htmlToText(body);
811
+ } else if (format === "markdown") {
812
+ content = htmlToMarkdown(body);
813
+ } else {
814
+ content = body;
815
+ }
816
+ } else {
817
+ content = body;
818
+ }
819
+ return { success: true, content, url, statusCode: response.status, contentType };
820
+ } catch (err) {
821
+ clearTimeout(timeoutId);
822
+ if (err instanceof Error && err.name === "AbortError") {
823
+ return { success: false, content: "", url, error: `Request timed out after ${clampedTimeout}ms` };
824
+ }
825
+ throw err;
826
+ }
827
+ } catch (err) {
828
+ return { success: false, content: "", url, error: `Fetch failed: ${err instanceof Error ? err.message : String(err)}` };
829
+ }
830
+ }
831
+
832
+ // ../packages/engine/src/tools/patch.js
833
+ import fs8 from "fs";
834
+ import path8 from "path";
835
+ var description9 = "Apply a unified diff patch to one or more files atomically. All hunks are validated before any changes are written. If any hunk fails to apply, no files are modified. Use standard unified diff format (output of `git diff` or `diff -u`).";
836
+ function parsePatch(patchText) {
837
+ const patches = [];
838
+ const lines = patchText.split("\n");
839
+ let i = 0;
840
+ while (i < lines.length) {
841
+ if (!lines[i].startsWith("--- ")) {
842
+ i++;
843
+ continue;
844
+ }
845
+ const oldFile = lines[i].replace(/^--- (a\/)?/, "").replace(/\t.*$/, "");
846
+ i++;
847
+ if (i >= lines.length || !lines[i].startsWith("+++ ")) {
848
+ continue;
849
+ }
850
+ const newFile = lines[i].replace(/^\+\+\+ (b\/)?/, "").replace(/\t.*$/, "");
851
+ i++;
852
+ const isNew = oldFile === "/dev/null";
853
+ const isDelete = newFile === "/dev/null";
854
+ const filePatch = {
855
+ oldFile: isNew ? newFile : oldFile,
856
+ newFile: isDelete ? oldFile : newFile,
857
+ hunks: [],
858
+ isNew,
859
+ isDelete
860
+ };
861
+ while (i < lines.length && lines[i].startsWith("@@")) {
862
+ const match = lines[i].match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/);
863
+ if (!match) {
864
+ i++;
865
+ continue;
866
+ }
867
+ const hunk = {
868
+ oldStart: parseInt(match[1], 10),
869
+ oldCount: match[2] !== void 0 ? parseInt(match[2], 10) : 1,
870
+ newStart: parseInt(match[3], 10),
871
+ newCount: match[4] !== void 0 ? parseInt(match[4], 10) : 1,
872
+ lines: []
873
+ };
874
+ i++;
875
+ while (i < lines.length) {
876
+ const line = lines[i];
877
+ if (line.startsWith("@@") || line.startsWith("--- ") || line.startsWith("diff ")) {
878
+ break;
879
+ }
880
+ if (line.startsWith("+")) {
881
+ hunk.lines.push({ type: "add", content: line.slice(1) });
882
+ } else if (line.startsWith("-")) {
883
+ hunk.lines.push({ type: "remove", content: line.slice(1) });
884
+ } else if (line.startsWith(" ") || line === "") {
885
+ hunk.lines.push({
886
+ type: "context",
887
+ content: line.startsWith(" ") ? line.slice(1) : line
888
+ });
889
+ }
890
+ i++;
891
+ }
892
+ filePatch.hunks.push(hunk);
893
+ }
894
+ if (filePatch.hunks.length > 0) {
895
+ patches.push(filePatch);
896
+ }
897
+ }
898
+ return patches;
899
+ }
900
+ function applyHunks(originalContent, hunks) {
901
+ const originalLines = originalContent.split("\n");
902
+ if (originalLines[originalLines.length - 1] === "" && originalContent.endsWith("\n")) {
903
+ originalLines.pop();
904
+ }
905
+ let offset = 0;
906
+ const resultLines = [...originalLines];
907
+ for (const hunk of hunks) {
908
+ const startLine = hunk.oldStart - 1 + offset;
909
+ let lineIdx = startLine;
910
+ for (const hunkLine of hunk.lines) {
911
+ if (hunkLine.type === "context" || hunkLine.type === "remove") {
912
+ if (lineIdx >= resultLines.length || resultLines[lineIdx] !== hunkLine.content) {
913
+ return null;
914
+ }
915
+ lineIdx++;
916
+ }
917
+ }
918
+ const newLines = [];
919
+ for (const hunkLine of hunk.lines) {
920
+ if (hunkLine.type === "context") {
921
+ newLines.push(hunkLine.content);
922
+ } else if (hunkLine.type === "add") {
923
+ newLines.push(hunkLine.content);
924
+ }
925
+ }
926
+ resultLines.splice(startLine, hunk.oldCount, ...newLines);
927
+ offset += newLines.length - hunk.oldCount;
928
+ }
929
+ return resultLines.join("\n") + "\n";
930
+ }
931
+ async function execute9({ patch_text }) {
932
+ try {
933
+ const patches = parsePatch(patch_text);
934
+ if (patches.length === 0) {
935
+ return {
936
+ success: false,
937
+ filesModified: [],
938
+ filesCreated: [],
939
+ filesDeleted: [],
940
+ error: "No valid patches found in input",
941
+ hint: "Ensure the patch uses unified diff format with --- and +++ headers and @@ hunk markers."
942
+ };
943
+ }
944
+ const pendingWrites = /* @__PURE__ */ new Map();
945
+ const pendingDeletes = [];
946
+ const errors = [];
947
+ for (const filePatch of patches) {
948
+ const targetFile = filePatch.newFile;
949
+ if (filePatch.isNew) {
950
+ const content = filePatch.hunks.flatMap((h) => h.lines.filter((l) => l.type === "add").map((l) => l.content)).join("\n") + "\n";
951
+ pendingWrites.set(targetFile, content);
952
+ continue;
953
+ }
954
+ if (filePatch.isDelete) {
955
+ if (!fs8.existsSync(filePatch.oldFile)) {
956
+ errors.push(`Cannot delete ${filePatch.oldFile}: file not found`);
957
+ continue;
958
+ }
959
+ pendingDeletes.push(filePatch.oldFile);
960
+ continue;
961
+ }
962
+ if (!fs8.existsSync(targetFile)) {
963
+ errors.push(`Cannot patch ${targetFile}: file not found`);
964
+ continue;
965
+ }
966
+ const originalContent = fs8.readFileSync(targetFile, "utf-8");
967
+ const result = applyHunks(originalContent, filePatch.hunks);
968
+ if (result === null) {
969
+ errors.push(`Patch for ${targetFile} failed: context lines do not match. The file may have been modified since the diff was generated.`);
970
+ continue;
971
+ }
972
+ pendingWrites.set(targetFile, result);
973
+ }
974
+ if (errors.length > 0) {
975
+ return {
976
+ success: false,
977
+ filesModified: [],
978
+ filesCreated: [],
979
+ filesDeleted: [],
980
+ error: `Patch validation failed:
981
+ ${errors.join("\n")}`,
982
+ hint: "All hunks must match the current file contents exactly. Regenerate the diff against the current file state."
983
+ };
984
+ }
985
+ const filesModified = [];
986
+ const filesCreated = [];
987
+ const filesDeleted = [];
988
+ for (const [filePath, content] of pendingWrites) {
989
+ const dir = path8.dirname(filePath);
990
+ if (!fs8.existsSync(dir)) {
991
+ fs8.mkdirSync(dir, { recursive: true });
992
+ }
993
+ const isNew = !fs8.existsSync(filePath);
994
+ fs8.writeFileSync(filePath, content, "utf-8");
995
+ if (isNew) {
996
+ filesCreated.push(filePath);
997
+ } else {
998
+ filesModified.push(filePath);
999
+ }
1000
+ }
1001
+ for (const filePath of pendingDeletes) {
1002
+ fs8.unlinkSync(filePath);
1003
+ filesDeleted.push(filePath);
1004
+ }
1005
+ return { success: true, filesModified, filesCreated, filesDeleted };
1006
+ } catch (err) {
1007
+ return {
1008
+ success: false,
1009
+ filesModified: [],
1010
+ filesCreated: [],
1011
+ filesDeleted: [],
1012
+ error: `Patch failed: ${err instanceof Error ? err.message : String(err)}`
1013
+ };
1014
+ }
1015
+ }
1016
+
1017
+ // ../packages/engine/src/tools/sub-agent.js
1018
+ import { streamText, stepCountIs } from "ai";
1019
+ var description10 = "Spawn a read-only sub-agent to explore the codebase. The sub-agent can read files, search with glob/grep, and list directories, but cannot write files, edit, or run commands. Use this for parallel codebase exploration \u2014 understanding architecture, finding patterns, or researching how something works before making changes.";
1020
+ function createSubAgentExecutor(model, workingDir, readOnlyTools) {
1021
+ return async function execute11({ prompt, maxTurns = 20 }) {
1022
+ try {
1023
+ let turnsUsed = 0;
1024
+ const stream = streamText({
1025
+ model,
1026
+ system: "You are a codebase exploration agent. You can read files, search for patterns, and list directories to understand the codebase. You CANNOT modify any files. Provide a thorough, detailed answer to the task you are given. Include specific file paths, line numbers, and code snippets in your findings.",
1027
+ prompt,
1028
+ tools: readOnlyTools,
1029
+ stopWhen: stepCountIs(maxTurns),
1030
+ abortSignal: AbortSignal.timeout(5 * 60 * 1e3),
1031
+ onStepFinish() {
1032
+ turnsUsed++;
1033
+ }
1034
+ });
1035
+ for await (const _chunk of stream.textStream) {
1036
+ }
1037
+ const text = await stream.text;
1038
+ return {
1039
+ success: true,
1040
+ content: text,
1041
+ turnsUsed
1042
+ };
1043
+ } catch (err) {
1044
+ return {
1045
+ success: false,
1046
+ content: "",
1047
+ turnsUsed: 0,
1048
+ error: `Sub-agent failed: ${err instanceof Error ? err.message : String(err)}`
1049
+ };
1050
+ }
1051
+ };
1052
+ }
1053
+
1054
+ // ../packages/engine/src/tools/git.js
1055
+ import { execSync } from "child_process";
1056
+ var description11 = "Execute git operations. Supports: status, diff, log, add, commit, branch, checkout, stash. Blocks destructive operations like force-push or reset --hard.";
1057
+ var BLOCKED_PATTERNS = [
1058
+ /--force/,
1059
+ /--hard/,
1060
+ /push.*-f\b/,
1061
+ /reset.*--hard/,
1062
+ /clean\s+-[a-z]*f/,
1063
+ /branch\s+-D\b/
1064
+ ];
1065
+ async function execute10({ action, args, cwd }) {
1066
+ const workDir = cwd || process.cwd();
1067
+ const fullCmd = `${action} ${args || ""}`;
1068
+ for (const pattern of BLOCKED_PATTERNS) {
1069
+ if (pattern.test(fullCmd)) {
1070
+ return { success: false, output: "", error: `Blocked: destructive git operation "${fullCmd}"` };
1071
+ }
1072
+ }
1073
+ const cmdMap = {
1074
+ status: `git status${args ? " " + args : " --short"}`,
1075
+ diff: `git diff${args ? " " + args : ""}`,
1076
+ log: `git log${args ? " " + args : " --oneline -20"}`,
1077
+ add: `git add ${args || "."}`,
1078
+ commit: `git commit -m "${(args || "").replace(/"/g, '\\"')}"`,
1079
+ branch: `git branch${args ? " " + args : ""}`,
1080
+ checkout: `git checkout ${args || ""}`,
1081
+ stash: `git stash${args ? " " + args : ""}`
1082
+ };
1083
+ const cmd = cmdMap[action];
1084
+ if (!cmd) {
1085
+ return { success: false, output: "", error: `Unknown git action: ${action}` };
1086
+ }
1087
+ try {
1088
+ const output = execSync(cmd, {
1089
+ cwd: workDir,
1090
+ encoding: "utf-8",
1091
+ timeout: 3e4
1092
+ });
1093
+ return { success: true, output: output.trim() };
1094
+ } catch (err) {
1095
+ return {
1096
+ success: false,
1097
+ output: err.stdout?.trim() || "",
1098
+ error: err.stderr?.trim() || err.message
1099
+ };
1100
+ }
1101
+ }
1102
+
1103
+ // ../packages/engine/src/tools/index.js
1104
+ function assertPathInBounds(resolvedPath, workingDir, sandboxed) {
1105
+ if (!sandboxed)
1106
+ return resolvedPath;
1107
+ const normalized = path9.resolve(resolvedPath);
1108
+ const normalizedWorkDir = path9.resolve(workingDir);
1109
+ if (!normalized.startsWith(normalizedWorkDir + path9.sep) && normalized !== normalizedWorkDir) {
1110
+ throw new Error(`Path "${resolvedPath}" is outside the working directory. Use --full-disk to allow access outside ${workingDir}`);
1111
+ }
1112
+ return normalized;
1113
+ }
1114
+ function createToolDefinitions(workingDir, model, sandboxed = true) {
1115
+ return {
1116
+ bash: tool({
1117
+ description,
1118
+ inputSchema: z.object({
1119
+ command: z.string().describe("The bash command to execute"),
1120
+ cwd: z.string().optional().describe("Working directory for the command (optional)"),
1121
+ timeout: z.number().optional().describe("Timeout in milliseconds (default: 120000 = 2 minutes)")
1122
+ }),
1123
+ execute: async ({ command, cwd, timeout }) => {
1124
+ const resolvedCwd = cwd ? path9.isAbsolute(cwd) ? cwd : path9.resolve(workingDir, cwd) : workingDir;
1125
+ assertPathInBounds(resolvedCwd, workingDir, sandboxed);
1126
+ const result = await execute({
1127
+ command,
1128
+ cwd: resolvedCwd,
1129
+ timeout
1130
+ });
1131
+ if (result.success) {
1132
+ return result.stdout || "(no output)";
1133
+ }
1134
+ return `Error: ${result.error || result.stderr}
1135
+ ${result.stdout || ""}`.trim();
1136
+ }
1137
+ }),
1138
+ read_file: tool({
1139
+ description: description2,
1140
+ inputSchema: z.object({
1141
+ path: z.string().describe("Path to the file to read (absolute or relative to cwd)"),
1142
+ encoding: z.string().optional().describe("File encoding (default: utf8)"),
1143
+ maxLines: z.number().optional().describe("Maximum number of lines to read (optional, reads entire file if not specified)"),
1144
+ startLine: z.number().optional().describe("Line number to start reading from (1-indexed, optional)")
1145
+ }),
1146
+ execute: async ({ path: filePath, encoding, maxLines, startLine }) => {
1147
+ const resolvedPath = path9.isAbsolute(filePath) ? filePath : path9.resolve(workingDir, filePath);
1148
+ assertPathInBounds(resolvedPath, workingDir, sandboxed);
1149
+ const result = await execute2({
1150
+ path: resolvedPath,
1151
+ encoding,
1152
+ maxLines,
1153
+ startLine
1154
+ });
1155
+ if (result.success) {
1156
+ return result.content || "";
1157
+ }
1158
+ return `Error: ${result.error}`;
1159
+ }
1160
+ }),
1161
+ write_file: tool({
1162
+ description: description3,
1163
+ inputSchema: z.object({
1164
+ path: z.string().describe("Path to the file to write (absolute or relative to cwd)"),
1165
+ content: z.string().describe("Content to write to the file"),
1166
+ encoding: z.string().optional().describe("File encoding (default: utf8)"),
1167
+ append: z.boolean().optional().describe("Append to file instead of overwriting (default: false)")
1168
+ }),
1169
+ execute: async ({ path: filePath, content, encoding, append }) => {
1170
+ const resolvedPath = path9.isAbsolute(filePath) ? filePath : path9.resolve(workingDir, filePath);
1171
+ assertPathInBounds(resolvedPath, workingDir, sandboxed);
1172
+ const result = await execute3({
1173
+ path: resolvedPath,
1174
+ content,
1175
+ encoding,
1176
+ append
1177
+ });
1178
+ if (result.success) {
1179
+ return `File ${result.action} successfully: ${result.path} (${result.linesWritten} lines, ${result.size} bytes)`;
1180
+ }
1181
+ return `Error: ${result.error}`;
1182
+ }
1183
+ }),
1184
+ edit_file: tool({
1185
+ description: description4,
1186
+ inputSchema: z.object({
1187
+ path: z.string().describe("Path to the file to edit (absolute or relative to cwd)"),
1188
+ old_string: z.string().describe("The exact text to find and replace. Must match exactly including whitespace and indentation."),
1189
+ new_string: z.string().describe("The text to replace old_string with. Can be empty string to delete."),
1190
+ replaceAll: z.boolean().optional().describe("Replace all occurrences instead of requiring unique match (default: false)")
1191
+ }),
1192
+ execute: async ({ path: filePath, old_string, new_string, replaceAll }) => {
1193
+ const resolvedPath = path9.isAbsolute(filePath) ? filePath : path9.resolve(workingDir, filePath);
1194
+ assertPathInBounds(resolvedPath, workingDir, sandboxed);
1195
+ const result = await execute4({
1196
+ path: resolvedPath,
1197
+ old_string,
1198
+ new_string,
1199
+ replaceAll
1200
+ });
1201
+ if (result.success) {
1202
+ return `File edited: ${result.path} (${result.replacements} replacement(s), ${result.linesDiff} lines)`;
1203
+ }
1204
+ return `Error: ${result.error}${result.hint ? `
1205
+ Hint: ${result.hint}` : ""}`;
1206
+ }
1207
+ }),
1208
+ glob: tool({
1209
+ description: description5,
1210
+ inputSchema: z.object({
1211
+ pattern: z.string().describe('Glob pattern to match files (e.g., "**/*.ts", "src/**/*.js")'),
1212
+ cwd: z.string().optional().describe("Directory to search in (default: current working directory)"),
1213
+ maxResults: z.number().optional().describe("Maximum number of results to return (default: 1000)"),
1214
+ includeHidden: z.boolean().optional().describe("Include hidden files (starting with .) (default: false)")
1215
+ }),
1216
+ execute: async ({ pattern, cwd, maxResults, includeHidden }) => {
1217
+ const resolvedCwd = cwd ? path9.isAbsolute(cwd) ? cwd : path9.resolve(workingDir, cwd) : workingDir;
1218
+ assertPathInBounds(resolvedCwd, workingDir, sandboxed);
1219
+ const result = await execute5({
1220
+ pattern,
1221
+ cwd: resolvedCwd,
1222
+ maxResults,
1223
+ includeHidden
1224
+ });
1225
+ if (result.success) {
1226
+ if (result.count === 0) {
1227
+ return `No files found matching pattern: ${pattern}`;
1228
+ }
1229
+ return `Found ${result.count} file(s)${result.truncated ? " (truncated)" : ""}:
1230
+ ${result.matches.join("\n")}`;
1231
+ }
1232
+ return `Error: ${result.error}`;
1233
+ }
1234
+ }),
1235
+ grep: tool({
1236
+ description: description6,
1237
+ inputSchema: z.object({
1238
+ pattern: z.string().describe("Regex pattern to search for"),
1239
+ path: z.string().optional().describe("File or directory to search in (default: current directory)"),
1240
+ filePattern: z.string().optional().describe('Glob pattern to filter files (e.g., "*.ts", "*.js")'),
1241
+ ignoreCase: z.boolean().optional().describe("Case-insensitive search (default: false)"),
1242
+ contextLines: z.number().optional().describe("Number of context lines before and after match (default: 0)"),
1243
+ maxResults: z.number().optional().describe("Maximum number of total matches to return (default: 100)")
1244
+ }),
1245
+ execute: async ({ pattern, path: searchPath, filePattern, ignoreCase, contextLines, maxResults }) => {
1246
+ const resolvedPath = searchPath ? path9.isAbsolute(searchPath) ? searchPath : path9.resolve(workingDir, searchPath) : workingDir;
1247
+ assertPathInBounds(resolvedPath, workingDir, sandboxed);
1248
+ const result = await execute6({
1249
+ pattern,
1250
+ path: resolvedPath,
1251
+ filePattern,
1252
+ ignoreCase,
1253
+ contextLines,
1254
+ maxResults
1255
+ });
1256
+ if (result.success) {
1257
+ if (result.matchCount === 0) {
1258
+ return `No matches found for pattern: ${pattern}`;
1259
+ }
1260
+ const lines = [
1261
+ `Found ${result.matchCount} match(es) in ${result.fileCount} file(s)${result.truncated ? " (truncated)" : ""}:`
1262
+ ];
1263
+ for (const [file, matches] of Object.entries(result.results)) {
1264
+ for (const match of matches) {
1265
+ lines.push(`${file}:${match.line}: ${match.content}`);
1266
+ }
1267
+ }
1268
+ return lines.join("\n");
1269
+ }
1270
+ return `Error: ${result.error}`;
1271
+ }
1272
+ }),
1273
+ ls: tool({
1274
+ description: description7,
1275
+ inputSchema: z.object({
1276
+ path: z.string().describe("Directory path to list (absolute or relative to cwd)"),
1277
+ ignore: z.array(z.string()).optional().describe('Glob patterns to exclude (e.g., ["node_modules", "dist"])'),
1278
+ maxDepth: z.number().optional().describe("Maximum directory depth to traverse (default: 3)"),
1279
+ maxFiles: z.number().optional().describe("Maximum number of entries to return (default: 1000)")
1280
+ }),
1281
+ execute: async ({ path: dirPath, ignore, maxDepth, maxFiles }) => {
1282
+ const resolvedPath = path9.isAbsolute(dirPath) ? dirPath : path9.resolve(workingDir, dirPath);
1283
+ assertPathInBounds(resolvedPath, workingDir, sandboxed);
1284
+ const result = await execute7({ path: resolvedPath, ignore, maxDepth, maxFiles });
1285
+ if (result.success) {
1286
+ return `${result.tree}
1287
+
1288
+ ${result.totalFiles} files, ${result.totalDirs} directories${result.truncated ? " (truncated)" : ""}`;
1289
+ }
1290
+ return `Error: ${result.error}`;
1291
+ }
1292
+ }),
1293
+ fetch: tool({
1294
+ description: description8,
1295
+ inputSchema: z.object({
1296
+ url: z.string().describe("The URL to fetch"),
1297
+ format: z.enum(["text", "markdown", "html"]).optional().describe("Output format (default: markdown)"),
1298
+ timeout: z.number().optional().describe("Timeout in milliseconds (default: 30000, max: 120000)")
1299
+ }),
1300
+ execute: async ({ url, format, timeout }) => {
1301
+ const result = await execute8({ url, format, timeout });
1302
+ if (result.success) {
1303
+ return `Content from ${result.url} (${result.contentType || "unknown"}):
1304
+
1305
+ ${result.content}`;
1306
+ }
1307
+ return `Error: ${result.error}`;
1308
+ }
1309
+ }),
1310
+ patch: tool({
1311
+ description: description9,
1312
+ inputSchema: z.object({
1313
+ patch_text: z.string().describe("Unified diff patch text with --- and +++ headers and @@ hunk markers")
1314
+ }),
1315
+ execute: async ({ patch_text }) => {
1316
+ const result = await execute9({ patch_text });
1317
+ if (result.success) {
1318
+ const parts = ["Patch applied successfully:"];
1319
+ if (result.filesCreated.length > 0)
1320
+ parts.push(` Created: ${result.filesCreated.join(", ")}`);
1321
+ if (result.filesModified.length > 0)
1322
+ parts.push(` Modified: ${result.filesModified.join(", ")}`);
1323
+ if (result.filesDeleted.length > 0)
1324
+ parts.push(` Deleted: ${result.filesDeleted.join(", ")}`);
1325
+ return parts.join("\n");
1326
+ }
1327
+ return `Error: ${result.error}${result.hint ? `
1328
+ Hint: ${result.hint}` : ""}`;
1329
+ }
1330
+ }),
1331
+ git: tool({
1332
+ description: description11,
1333
+ inputSchema: z.object({
1334
+ action: z.enum(["status", "diff", "log", "add", "commit", "branch", "checkout", "stash"]).describe("The git action to perform"),
1335
+ args: z.string().optional().describe("Additional arguments (file paths, branch name, commit message)")
1336
+ }),
1337
+ execute: async ({ action, args }) => {
1338
+ const result = await execute10({ action, args, cwd: workingDir });
1339
+ if (result.success) {
1340
+ return result.output || "(no output)";
1341
+ }
1342
+ return `Error: ${result.error}`;
1343
+ }
1344
+ }),
1345
+ ...model ? {
1346
+ sub_agent: tool({
1347
+ description: description10,
1348
+ inputSchema: z.object({
1349
+ prompt: z.string().describe("Detailed task description for the sub-agent. Be specific about what to look for."),
1350
+ maxTurns: z.number().optional().describe("Maximum tool-use turns (default: 20)")
1351
+ }),
1352
+ execute: async ({ prompt, maxTurns }) => {
1353
+ const readOnlyTools = {
1354
+ read_file: tool({
1355
+ description: description2,
1356
+ inputSchema: z.object({
1357
+ path: z.string().describe("Path to the file to read"),
1358
+ maxLines: z.number().optional().describe("Max lines to read"),
1359
+ startLine: z.number().optional().describe("Start line (1-indexed)")
1360
+ }),
1361
+ execute: async ({ path: filePath, maxLines, startLine }) => {
1362
+ const resolvedPath = path9.isAbsolute(filePath) ? filePath : path9.resolve(workingDir, filePath);
1363
+ assertPathInBounds(resolvedPath, workingDir, sandboxed);
1364
+ const result2 = await execute2({ path: resolvedPath, maxLines, startLine });
1365
+ return result2.success ? result2.content || "" : `Error: ${result2.error}`;
1366
+ }
1367
+ }),
1368
+ glob: tool({
1369
+ description: description5,
1370
+ inputSchema: z.object({
1371
+ pattern: z.string().describe("Glob pattern to match files"),
1372
+ cwd: z.string().optional().describe("Directory to search in")
1373
+ }),
1374
+ execute: async ({ pattern, cwd }) => {
1375
+ const resolvedCwd = cwd ? path9.isAbsolute(cwd) ? cwd : path9.resolve(workingDir, cwd) : workingDir;
1376
+ assertPathInBounds(resolvedCwd, workingDir, sandboxed);
1377
+ const result2 = await execute5({ pattern, cwd: resolvedCwd });
1378
+ return result2.success ? result2.count === 0 ? `No files found matching: ${pattern}` : `Found ${result2.count} file(s):
1379
+ ${result2.matches.join("\n")}` : `Error: ${result2.error}`;
1380
+ }
1381
+ }),
1382
+ grep: tool({
1383
+ description: description6,
1384
+ inputSchema: z.object({
1385
+ pattern: z.string().describe("Regex pattern to search for"),
1386
+ path: z.string().optional().describe("File or directory to search in"),
1387
+ filePattern: z.string().optional().describe("Glob to filter files")
1388
+ }),
1389
+ execute: async ({ pattern, path: searchPath, filePattern }) => {
1390
+ const resolvedPath = searchPath ? path9.isAbsolute(searchPath) ? searchPath : path9.resolve(workingDir, searchPath) : workingDir;
1391
+ assertPathInBounds(resolvedPath, workingDir, sandboxed);
1392
+ const result2 = await execute6({ pattern, path: resolvedPath, filePattern });
1393
+ if (!result2.success)
1394
+ return `Error: ${result2.error}`;
1395
+ if (result2.matchCount === 0)
1396
+ return `No matches for: ${pattern}`;
1397
+ const lines = [`Found ${result2.matchCount} match(es):`];
1398
+ for (const [file, matches] of Object.entries(result2.results)) {
1399
+ for (const match of matches) {
1400
+ lines.push(`${file}:${match.line}: ${match.content}`);
1401
+ }
1402
+ }
1403
+ return lines.join("\n");
1404
+ }
1405
+ }),
1406
+ ls: tool({
1407
+ description: description7,
1408
+ inputSchema: z.object({
1409
+ path: z.string().describe("Directory path to list"),
1410
+ maxDepth: z.number().optional().describe("Max depth (default: 3)")
1411
+ }),
1412
+ execute: async ({ path: dirPath, maxDepth }) => {
1413
+ const resolvedPath = path9.isAbsolute(dirPath) ? dirPath : path9.resolve(workingDir, dirPath);
1414
+ assertPathInBounds(resolvedPath, workingDir, sandboxed);
1415
+ const result2 = await execute7({ path: resolvedPath, maxDepth });
1416
+ return result2.success ? result2.tree : `Error: ${result2.error}`;
1417
+ }
1418
+ })
1419
+ };
1420
+ const executor = createSubAgentExecutor(model, workingDir, readOnlyTools);
1421
+ const result = await executor({ prompt, maxTurns });
1422
+ if (result.success) {
1423
+ return `Sub-agent findings (${result.turnsUsed} turns):
1424
+
1425
+ ${result.content}`;
1426
+ }
1427
+ return `Error: ${result.error}`;
1428
+ }
1429
+ })
1430
+ } : {}
1431
+ };
1432
+ }
1433
+
1434
+ // src/permissions.js
1435
+ import readline from "readline";
1436
+ import chalk from "chalk";
1437
+ var READ_TOOLS = /* @__PURE__ */ new Set(["read_file", "glob", "grep", "ls", "sub_agent"]);
1438
+ var DANGEROUS_PATTERNS = [
1439
+ { pattern: /rm\s+(-[a-z]*f|-[a-z]*r|--force|--recursive)/i, label: "recursive/forced delete" },
1440
+ { pattern: /git\s+reset\s+--hard/i, label: "hard reset" },
1441
+ { pattern: /git\s+push\s+.*--force/i, label: "force push" },
1442
+ { pattern: /git\s+clean\s+-[a-z]*f/i, label: "git clean" },
1443
+ { pattern: /drop\s+table/i, label: "drop table" },
1444
+ { pattern: /truncate\s+/i, label: "truncate" },
1445
+ { pattern: /DELETE\s+FROM\s+\w+\s*;/i, label: "DELETE without WHERE" },
1446
+ { pattern: /chmod\s+777/i, label: "chmod 777" },
1447
+ { pattern: />(\/dev\/sda|\/dev\/disk)/i, label: "write to disk device" }
1448
+ ];
1449
+ function isDangerous(command) {
1450
+ for (const { pattern, label } of DANGEROUS_PATTERNS) {
1451
+ if (pattern.test(command))
1452
+ return label;
1453
+ }
1454
+ return null;
1455
+ }
1456
+ var PermissionManager = class {
1457
+ sessionAllow = /* @__PURE__ */ new Set();
1458
+ trustAll;
1459
+ configTrust;
1460
+ rl = null;
1461
+ cancelCurrentPrompt = null;
1462
+ /** True while rl.question() is active — external line handlers must ignore input */
1463
+ questionActive = false;
1464
+ constructor(trustAll = false, configTrust = []) {
1465
+ this.trustAll = trustAll;
1466
+ this.configTrust = new Set(configTrust);
1467
+ }
1468
+ /** Bind to the agent's readline instance so we reuse it for prompts */
1469
+ setReadline(rl) {
1470
+ this.rl = rl;
1471
+ }
1472
+ cancelPrompt() {
1473
+ if (this.cancelCurrentPrompt) {
1474
+ this.cancelCurrentPrompt();
1475
+ this.cancelCurrentPrompt = null;
1476
+ }
1477
+ }
1478
+ async checkPermission(toolName, toolInput) {
1479
+ if (toolName === "bash") {
1480
+ const cmd = String(toolInput.command || "");
1481
+ const danger = isDangerous(cmd);
1482
+ if (danger) {
1483
+ console.log();
1484
+ console.log(chalk.red.bold(` \u26A0 DANGEROUS: ${danger}`));
1485
+ console.log(chalk.red(` Command: ${cmd}`));
1486
+ if (this.trustAll) {
1487
+ console.log(chalk.yellow(" (allowed by --trust mode)"));
1488
+ return true;
1489
+ }
1490
+ const answer = await this.askUser(chalk.red(" Are you sure? (yes to confirm): "));
1491
+ if (answer.trim().toLowerCase() !== "yes")
1492
+ return false;
1493
+ return true;
1494
+ }
1495
+ }
1496
+ if (this.trustAll)
1497
+ return true;
1498
+ if (READ_TOOLS.has(toolName))
1499
+ return true;
1500
+ if (this.sessionAllow.has(toolName))
1501
+ return true;
1502
+ if (this.configTrust.has(toolName))
1503
+ return true;
1504
+ return this.promptUser(toolName, toolInput);
1505
+ }
1506
+ async promptUser(toolName, toolInput) {
1507
+ const display = this.formatToolCall(toolName, toolInput);
1508
+ console.log();
1509
+ console.log(chalk.cyan(` \u250C\u2500 ${toolName} ${"\u2500".repeat(Math.max(0, 40 - toolName.length))}\u2510`));
1510
+ for (const line of display.split("\n")) {
1511
+ console.log(chalk.cyan(" \u2502 ") + chalk.white(line));
1512
+ }
1513
+ console.log(chalk.cyan(` \u2514${"\u2500".repeat(43)}\u2518`));
1514
+ const answer = await this.askUser(chalk.dim(" Allow? ") + chalk.white("(y)es / (n)o / (a)lways this tool / (t)rust all: "));
1515
+ const choice = answer.trim().toLowerCase();
1516
+ if (choice === "t" || choice === "trust") {
1517
+ this.trustAll = true;
1518
+ return true;
1519
+ }
1520
+ if (choice === "a" || choice === "always") {
1521
+ this.sessionAllow.add(toolName);
1522
+ return true;
1523
+ }
1524
+ return choice === "y" || choice === "yes";
1525
+ }
1526
+ /**
1527
+ * Prompt the user with a question. Sets questionActive flag so the
1528
+ * agent's line handler knows to ignore this input.
1529
+ */
1530
+ askUser(prompt) {
1531
+ return new Promise((resolve, reject) => {
1532
+ this.cancelCurrentPrompt = () => {
1533
+ this.questionActive = false;
1534
+ reject(new Error("cancelled"));
1535
+ };
1536
+ if (this.rl) {
1537
+ this.questionActive = true;
1538
+ this.rl.resume();
1539
+ this.rl.question(prompt, (answer) => {
1540
+ this.questionActive = false;
1541
+ this.cancelCurrentPrompt = null;
1542
+ this.rl.pause();
1543
+ resolve(answer);
1544
+ });
1545
+ } else {
1546
+ const questionRl = readline.createInterface({
1547
+ input: process.stdin,
1548
+ output: process.stdout
1549
+ });
1550
+ this.questionActive = true;
1551
+ questionRl.question(prompt, (answer) => {
1552
+ this.questionActive = false;
1553
+ this.cancelCurrentPrompt = null;
1554
+ questionRl.close();
1555
+ resolve(answer);
1556
+ });
1557
+ }
1558
+ });
1559
+ }
1560
+ formatToolCall(toolName, input) {
1561
+ switch (toolName) {
1562
+ case "bash":
1563
+ return String(input.command || "");
1564
+ case "write_file":
1565
+ case "edit_file":
1566
+ return `${input.path || ""}`;
1567
+ case "patch":
1568
+ return String(input.patch_text || "").slice(0, 200) + "...";
1569
+ case "fetch":
1570
+ return String(input.url || "");
1571
+ default:
1572
+ return JSON.stringify(input, null, 2).slice(0, 200);
1573
+ }
1574
+ }
1575
+ };
1576
+
1577
+ // src/tui.js
1578
+ import chalk2 from "chalk";
1579
+ import { execSync as execSync2 } from "child_process";
1580
+ var toolCounts = {};
1581
+ var cachedGitBranch = "";
1582
+ var lastBranchCheck = 0;
1583
+ function getGitBranch() {
1584
+ const now = Date.now();
1585
+ if (now - lastBranchCheck > 1e4) {
1586
+ lastBranchCheck = now;
1587
+ try {
1588
+ cachedGitBranch = execSync2("git rev-parse --abbrev-ref HEAD 2>/dev/null", { encoding: "utf-8", timeout: 2e3 }).trim();
1589
+ } catch {
1590
+ cachedGitBranch = "";
1591
+ }
1592
+ }
1593
+ return cachedGitBranch;
1594
+ }
1595
+ function incrementToolCount(toolName) {
1596
+ toolCounts[toolName] = (toolCounts[toolName] || 0) + 1;
1597
+ }
1598
+ function printHeader(version, provider, model, cwd) {
1599
+ console.log();
1600
+ console.log(chalk2.bold.white(` WorkerMill`) + chalk2.dim(` v${version}`));
1601
+ if (provider && model) {
1602
+ console.log(chalk2.dim(` ${provider}/`) + chalk2.white(model));
1603
+ }
1604
+ if (cwd) {
1605
+ console.log(chalk2.dim(` cwd: `) + chalk2.white(cwd));
1606
+ }
1607
+ console.log(chalk2.dim(` /help`) + chalk2.dim(` for commands, `) + chalk2.dim(`Ctrl+C`) + chalk2.dim(` to cancel`));
1608
+ console.log();
1609
+ }
1610
+ function printToolCall(toolName, toolInput) {
1611
+ incrementToolCount(toolName);
1612
+ const arrow = chalk2.dim(" \u2193 ");
1613
+ const label = chalk2.cyan;
1614
+ switch (toolName) {
1615
+ case "bash": {
1616
+ let cmd = String(toolInput.command || "");
1617
+ const heredocIdx = cmd.indexOf("<<");
1618
+ if (heredocIdx > 0)
1619
+ cmd = cmd.slice(0, heredocIdx).trim() + " << ...";
1620
+ if (cmd.length > 120)
1621
+ cmd = cmd.slice(0, 117) + "...";
1622
+ console.log(arrow + label("Bash ") + chalk2.yellow(cmd));
1623
+ break;
1624
+ }
1625
+ case "read_file":
1626
+ console.log(arrow + label("Read ") + chalk2.white(String(toolInput.path || "")));
1627
+ break;
1628
+ case "write_file":
1629
+ console.log(arrow + label("Write ") + chalk2.white(String(toolInput.path || "")));
1630
+ break;
1631
+ case "edit_file":
1632
+ console.log(arrow + label("Edit ") + chalk2.white(String(toolInput.path || "")));
1633
+ break;
1634
+ case "patch":
1635
+ console.log(arrow + label("Patch ") + chalk2.white("(multi-file)"));
1636
+ break;
1637
+ case "glob":
1638
+ console.log(arrow + label("Glob ") + chalk2.white(String(toolInput.pattern || "")));
1639
+ break;
1640
+ case "grep":
1641
+ console.log(arrow + label("Grep ") + chalk2.white(String(toolInput.pattern || "")));
1642
+ break;
1643
+ case "ls":
1644
+ console.log(arrow + label("List ") + chalk2.white(String(toolInput.path || ".")));
1645
+ break;
1646
+ case "fetch":
1647
+ console.log(arrow + label("Fetch ") + chalk2.white(String(toolInput.url || "")));
1648
+ break;
1649
+ case "sub_agent":
1650
+ console.log(arrow + label("Agent ") + chalk2.white(String(toolInput.prompt || "").slice(0, 80)));
1651
+ break;
1652
+ case "git":
1653
+ console.log(arrow + label("Git ") + chalk2.white(`${toolInput.action}${toolInput.args ? " " + toolInput.args : ""}`));
1654
+ break;
1655
+ default:
1656
+ console.log(arrow + label(toolName));
1657
+ }
1658
+ }
1659
+ var PERSONA_EMOJIS = {
1660
+ frontend_developer: "\u{1F3A8}",
1661
+ // 🎨
1662
+ backend_developer: "\u{1F4BB}",
1663
+ // 💻
1664
+ fullstack_developer: "\u{1F4BB}",
1665
+ // 💻 (same as backend)
1666
+ devops_engineer: "\u{1F527}",
1667
+ // 🔧
1668
+ security_engineer: "\u{1F512}",
1669
+ // 🔐
1670
+ qa_engineer: "\u{1F9EA}",
1671
+ // 🧪
1672
+ tech_writer: "\u{1F4DD}",
1673
+ // 📝
1674
+ project_manager: "\u{1F4CB}",
1675
+ // 📋
1676
+ architect: "\u{1F3D7}\uFE0F",
1677
+ // 🏗️
1678
+ database_engineer: "\u{1F4CA}",
1679
+ // 📊
1680
+ data_engineer: "\u{1F4CA}",
1681
+ // 📊
1682
+ data_ml_engineer: "\u{1F4CA}",
1683
+ // 📊
1684
+ ml_engineer: "\u{1F4CA}",
1685
+ // 📊
1686
+ mobile_developer: "\u{1F4F1}",
1687
+ // 📱
1688
+ tech_lead: "\u{1F451}",
1689
+ // 👑
1690
+ manager: "\u{1F454}",
1691
+ // 👔
1692
+ support_agent: "\u{1F4AC}",
1693
+ // 💬
1694
+ planner: "\u{1F4A1}",
1695
+ // 💡 (planning_agent)
1696
+ coordinator: "\u{1F3AF}",
1697
+ // 🎯
1698
+ critic: "\u{1F50D}",
1699
+ // 🔍
1700
+ reviewer: "\u{1F50D}"
1701
+ // 🔍
1702
+ };
1703
+ function getPersonaEmoji(persona) {
1704
+ return PERSONA_EMOJIS[persona] || "\u{1F916}";
1705
+ }
1706
+ function printToolResult(toolName, result) {
1707
+ const isError = result.startsWith("Error:") || result.startsWith("error:");
1708
+ const lines = result.split("\n").filter((l) => l.trim());
1709
+ if (isError) {
1710
+ const errLines = lines.slice(0, 5);
1711
+ for (const line of errLines) {
1712
+ console.log(chalk2.red(` ${line}`));
1713
+ }
1714
+ if (lines.length > 5)
1715
+ console.log(chalk2.red(` ... ${lines.length - 5} more lines`));
1716
+ return;
1717
+ }
1718
+ switch (toolName) {
1719
+ case "read_file":
1720
+ console.log(chalk2.dim(` (${lines.length} lines)`));
1721
+ break;
1722
+ case "write_file":
1723
+ case "edit_file":
1724
+ case "patch":
1725
+ if (lines.length === 1) {
1726
+ console.log(chalk2.dim(` ${lines[0]}`));
1727
+ } else {
1728
+ console.log(chalk2.dim(` (${lines.length} lines changed)`));
1729
+ }
1730
+ break;
1731
+ case "bash": {
1732
+ if (lines.length === 0)
1733
+ break;
1734
+ const shown = lines.slice(0, 5);
1735
+ for (const line of shown) {
1736
+ console.log(chalk2.dim(` ${line}`));
1737
+ }
1738
+ if (lines.length > 5)
1739
+ console.log(chalk2.dim(` ... ${lines.length - 5} more lines`));
1740
+ break;
1741
+ }
1742
+ case "glob":
1743
+ case "grep":
1744
+ case "ls":
1745
+ const searchShown = lines.slice(0, 8);
1746
+ for (const line of searchShown) {
1747
+ console.log(chalk2.dim(` ${line}`));
1748
+ }
1749
+ if (lines.length > 8)
1750
+ console.log(chalk2.dim(` ... ${lines.length - 8} more`));
1751
+ break;
1752
+ default:
1753
+ if (lines.length <= 3) {
1754
+ for (const line of lines)
1755
+ console.log(chalk2.dim(` ${line}`));
1756
+ } else {
1757
+ console.log(chalk2.dim(` (${lines.length} lines)`));
1758
+ }
1759
+ }
1760
+ }
1761
+ function wmLog(persona, message) {
1762
+ const emoji = getPersonaEmoji(persona);
1763
+ console.log(chalk2.cyan(`[${emoji} ${persona} \u{1F3E0}] `) + chalk2.white(message));
1764
+ }
1765
+ function wmLogPrefix(persona) {
1766
+ const emoji = getPersonaEmoji(persona);
1767
+ return chalk2.cyan(`[${emoji} ${persona} \u{1F3E0}] `);
1768
+ }
1769
+ function wmCoordinatorLog(message) {
1770
+ console.log(chalk2.cyan("[coordinator] ") + chalk2.white(message));
1771
+ }
1772
+ function printError(message) {
1773
+ console.log(chalk2.red(`
1774
+ \u2717 ${message}
1775
+ `));
1776
+ }
1777
+ var sessionStartTime = Date.now();
1778
+ function formatTokens(n) {
1779
+ if (n >= 1e6)
1780
+ return `${(n / 1e6).toFixed(1)}M`;
1781
+ if (n >= 1e3)
1782
+ return `${(n / 1e3).toFixed(n >= 1e4 ? 0 : 1)}k`;
1783
+ return String(n);
1784
+ }
1785
+ function formatElapsed() {
1786
+ const mins = Math.floor((Date.now() - sessionStartTime) / 6e4);
1787
+ if (mins < 1)
1788
+ return "<1m";
1789
+ return `>${mins}m`;
1790
+ }
1791
+ function contextBar(tokens, maxContext) {
1792
+ const barLen = 8;
1793
+ const usage = Math.min(1, tokens / maxContext);
1794
+ const filled = Math.round(usage * barLen);
1795
+ const empty = barLen - filled;
1796
+ const color = usage < 0.5 ? chalk2.green : usage < 0.8 ? chalk2.yellow : chalk2.red;
1797
+ return color("\u2588".repeat(filled)) + chalk2.dim("\u2591".repeat(empty));
1798
+ }
1799
+ function printStatusBar(provider, model, tokens, permissionMode, cost) {
1800
+ const width = process.stdout.columns || 80;
1801
+ const bg = chalk2.bgRgb(30, 30, 30);
1802
+ const modelDisplay = ` ${model} `;
1803
+ const maxCtx = 128e3;
1804
+ const bar = contextBar(tokens, maxCtx);
1805
+ const tokStr = formatTokens(tokens);
1806
+ const shortNames = {
1807
+ bash: "Bash",
1808
+ read_file: "Read",
1809
+ write_file: "Write",
1810
+ edit_file: "Edit",
1811
+ glob: "Glob",
1812
+ grep: "Grep",
1813
+ ls: "List",
1814
+ fetch: "Fetch",
1815
+ patch: "Patch",
1816
+ sub_agent: "Agent",
1817
+ git: "Git"
1818
+ };
1819
+ const countParts = Object.entries(toolCounts).filter(([_, count]) => count > 0).map(([name, count]) => `${shortNames[name] || name} x${count}`);
1820
+ const toolStr = countParts.length > 0 ? countParts.join(" | ") : "";
1821
+ const gitBranch = getGitBranch();
1822
+ const cwd = process.cwd().split("/").pop() || "";
1823
+ const costStr = cost !== void 0 && cost > 0 ? `$${cost.toFixed(4)} | ` : "";
1824
+ const elapsed = formatElapsed();
1825
+ const modeStr = permissionMode || "ask";
1826
+ const left = bg.white(modelDisplay) + " " + bar + " " + bg.white(tokStr);
1827
+ const middle = gitBranch ? bg.dim(" | ") + bg.white(cwd) + bg.dim(" git:(") + bg.green(gitBranch) + bg.dim(")") : bg.dim(" | ") + bg.white(cwd);
1828
+ const tools = toolStr ? bg.dim(" | ") + bg.dim(toolStr) : "";
1829
+ const right = bg.dim(" | ") + bg.dim(costStr) + bg.white(elapsed) + bg.dim(" ") + bg.green(modeStr) + " ";
1830
+ const stripAnsi = (s) => s.replace(/\x1b\[[0-9;]*m/g, "");
1831
+ const contentLen = stripAnsi(left).length + stripAnsi(middle).length + stripAnsi(tools).length + stripAnsi(right).length;
1832
+ const pad = Math.max(0, width - contentLen);
1833
+ return left + middle + tools + bg(" ".repeat(pad)) + right;
1834
+ }
1835
+
1836
+ // src/cost-tracker.js
1837
+ var PRICING = {
1838
+ // Anthropic
1839
+ "claude-opus-4-6": { input: 15, output: 75 },
1840
+ "claude-sonnet-4-6": { input: 3, output: 15 },
1841
+ "claude-haiku-4-5": { input: 0.8, output: 4 },
1842
+ // OpenAI
1843
+ "gpt-4o": { input: 2.5, output: 10 },
1844
+ "gpt-4o-mini": { input: 0.15, output: 0.6 },
1845
+ "o3": { input: 10, output: 40 },
1846
+ "o3-mini": { input: 1.1, output: 4.4 },
1847
+ // Google
1848
+ "gemini-2.5-pro": { input: 1.25, output: 10 },
1849
+ "gemini-2.5-flash": { input: 0.15, output: 0.6 }
1850
+ };
1851
+ var CostTracker = class {
1852
+ entries = [];
1853
+ addUsage(persona, provider, model, inputTokens, outputTokens) {
1854
+ const pricing = PRICING[model] || { input: 0, output: 0 };
1855
+ const cost = inputTokens / 1e6 * pricing.input + outputTokens / 1e6 * pricing.output;
1856
+ this.entries.push({
1857
+ persona,
1858
+ provider,
1859
+ model,
1860
+ inputTokens,
1861
+ outputTokens,
1862
+ cost
1863
+ });
1864
+ }
1865
+ getTotalCost() {
1866
+ return this.entries.reduce((sum, e) => sum + e.cost, 0);
1867
+ }
1868
+ getTotalTokens() {
1869
+ return this.entries.reduce((sum, e) => sum + e.inputTokens + e.outputTokens, 0);
1870
+ }
1871
+ getBreakdown() {
1872
+ return [...this.entries];
1873
+ }
1874
+ getSummary() {
1875
+ const total = this.getTotalCost();
1876
+ const totalIn = this.entries.reduce((s, e) => s + e.inputTokens, 0);
1877
+ const totalOut = this.entries.reduce((s, e) => s + e.outputTokens, 0);
1878
+ const lines = [
1879
+ `Session cost: $${total.toFixed(4)} (${totalIn.toLocaleString()} in / ${totalOut.toLocaleString()} out)`
1880
+ ];
1881
+ for (const entry of this.entries) {
1882
+ lines.push(` * ${entry.persona}: $${entry.cost.toFixed(4)} (${entry.provider}/${entry.model})`);
1883
+ }
1884
+ return lines.join("\n");
1885
+ }
1886
+ };
1887
+
1888
+ export {
1889
+ loadConfig,
1890
+ saveConfig,
1891
+ getProviderForPersona,
1892
+ createModel,
1893
+ killActiveProcess,
1894
+ createToolDefinitions,
1895
+ PermissionManager,
1896
+ printHeader,
1897
+ printToolCall,
1898
+ getPersonaEmoji,
1899
+ printToolResult,
1900
+ wmLog,
1901
+ wmLogPrefix,
1902
+ wmCoordinatorLog,
1903
+ printError,
1904
+ printStatusBar,
1905
+ CostTracker
1906
+ };