@stfade/pi-read-delegator 1.0.12 → 1.0.13
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/README.md +178 -69
- package/{bash-filter.d.ts → dist/bash-filter.d.ts} +16 -6
- package/dist/bash-filter.js +420 -0
- package/{config.d.ts → dist/config.d.ts} +0 -1
- package/{config.js → dist/config.js} +34 -59
- package/{index.js → dist/index.js} +111 -149
- package/{reader-manager.d.ts → dist/reader-manager.d.ts} +26 -1
- package/{reader-manager.js → dist/reader-manager.js} +127 -72
- package/{tool-blocker.js → dist/tool-blocker.js} +13 -21
- package/{ui.js → dist/ui.js} +15 -62
- package/package.json +20 -17
- package/bash-filter.js +0 -242
- package/templates/reader.md +0 -8
- /package/{index.d.ts → dist/index.d.ts} +0 -0
- /package/{tool-blocker.d.ts → dist/tool-blocker.d.ts} +0 -0
- /package/{ui.d.ts → dist/ui.d.ts} +0 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bash-filter.ts — Bash command classification and Reader forwarding
|
|
3
|
+
*
|
|
4
|
+
* Classifies shell commands as read-only (delegate to Reader subagent),
|
|
5
|
+
* write (execute directly), or ambiguous (prompt user).
|
|
6
|
+
*/
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Command lists
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
/**
|
|
11
|
+
* Commands that ONLY read and should be forwarded to the Reader subagent.
|
|
12
|
+
*
|
|
13
|
+
* Covers three shells:
|
|
14
|
+
* - bash/sh (Linux/macOS/WSL/Git Bash)
|
|
15
|
+
* - PowerShell (Windows)
|
|
16
|
+
* - cmd.exe (Windows)
|
|
17
|
+
*
|
|
18
|
+
* All comparisons are case-insensitive (isReadSegment lowercases the argv).
|
|
19
|
+
*
|
|
20
|
+
* Context-dependent:
|
|
21
|
+
* - sed without -i is read-only (stream editor → stdout)
|
|
22
|
+
* - awk is read-only (pattern scanning / processing language)
|
|
23
|
+
*/
|
|
24
|
+
const READ_COMMANDS = new Set([
|
|
25
|
+
// ── bash / sh (Linux, macOS, WSL, Git Bash) ──
|
|
26
|
+
"cat",
|
|
27
|
+
"grep",
|
|
28
|
+
"find",
|
|
29
|
+
"ls",
|
|
30
|
+
"head",
|
|
31
|
+
"tail",
|
|
32
|
+
"less",
|
|
33
|
+
"wc",
|
|
34
|
+
"nl",
|
|
35
|
+
"more",
|
|
36
|
+
"bat",
|
|
37
|
+
"rg",
|
|
38
|
+
"fd",
|
|
39
|
+
"awk",
|
|
40
|
+
"du",
|
|
41
|
+
"df",
|
|
42
|
+
"stat",
|
|
43
|
+
"file",
|
|
44
|
+
"which",
|
|
45
|
+
"where",
|
|
46
|
+
"type",
|
|
47
|
+
"dir",
|
|
48
|
+
"sort",
|
|
49
|
+
"uniq",
|
|
50
|
+
"cut",
|
|
51
|
+
"tr",
|
|
52
|
+
"diff",
|
|
53
|
+
"cmp",
|
|
54
|
+
"comm",
|
|
55
|
+
"od",
|
|
56
|
+
"hexdump",
|
|
57
|
+
"xxd",
|
|
58
|
+
// ── PowerShell (Windows) ──
|
|
59
|
+
"get-content",
|
|
60
|
+
"select-string",
|
|
61
|
+
"get-childitem",
|
|
62
|
+
"get-itemproperty",
|
|
63
|
+
"get-item",
|
|
64
|
+
"test-path",
|
|
65
|
+
"get-alias",
|
|
66
|
+
"get-command",
|
|
67
|
+
"measure-object",
|
|
68
|
+
"compare-object",
|
|
69
|
+
"where-object",
|
|
70
|
+
"select-object",
|
|
71
|
+
"format-list",
|
|
72
|
+
"format-table",
|
|
73
|
+
"get-service",
|
|
74
|
+
"get-process",
|
|
75
|
+
"get-eventlog",
|
|
76
|
+
"get-history",
|
|
77
|
+
"get-variable",
|
|
78
|
+
"get-psdrive",
|
|
79
|
+
"get-psprovider",
|
|
80
|
+
// ── cmd.exe (Windows) ──
|
|
81
|
+
"findstr",
|
|
82
|
+
"comp",
|
|
83
|
+
"fc",
|
|
84
|
+
"tree",
|
|
85
|
+
]);
|
|
86
|
+
/**
|
|
87
|
+
* Commands that write to the filesystem and should execute directly.
|
|
88
|
+
* sed and tee are context-dependent — handled specially in isWriteCommand.
|
|
89
|
+
*
|
|
90
|
+
* Covers bash/sh, PowerShell, and cmd.exe writable commands.
|
|
91
|
+
*/
|
|
92
|
+
const WRITE_COMMANDS = new Set([
|
|
93
|
+
// ── bash / sh ──
|
|
94
|
+
"mkdir",
|
|
95
|
+
"touch",
|
|
96
|
+
"echo",
|
|
97
|
+
"rm",
|
|
98
|
+
"mv",
|
|
99
|
+
"cp",
|
|
100
|
+
"chmod",
|
|
101
|
+
"chown",
|
|
102
|
+
"ln",
|
|
103
|
+
"rmdir",
|
|
104
|
+
// ── cross-platform build tools ──
|
|
105
|
+
"npm",
|
|
106
|
+
"pnpm",
|
|
107
|
+
"yarn",
|
|
108
|
+
"pip",
|
|
109
|
+
"cargo",
|
|
110
|
+
"go",
|
|
111
|
+
"npx",
|
|
112
|
+
"node",
|
|
113
|
+
"python",
|
|
114
|
+
"python3",
|
|
115
|
+
"git",
|
|
116
|
+
"docker",
|
|
117
|
+
"kubectl",
|
|
118
|
+
"tsc",
|
|
119
|
+
"make",
|
|
120
|
+
"cmake",
|
|
121
|
+
"dotnet",
|
|
122
|
+
"rustc",
|
|
123
|
+
"gcc",
|
|
124
|
+
"g++",
|
|
125
|
+
// ── PowerShell write commands ──
|
|
126
|
+
"set-content",
|
|
127
|
+
"add-content",
|
|
128
|
+
"new-item",
|
|
129
|
+
"remove-item",
|
|
130
|
+
"copy-item",
|
|
131
|
+
"move-item",
|
|
132
|
+
"rename-item",
|
|
133
|
+
"out-file",
|
|
134
|
+
"export-csv",
|
|
135
|
+
"export-clixml",
|
|
136
|
+
"start-process",
|
|
137
|
+
"invoke-expression",
|
|
138
|
+
"invoke-webrequest",
|
|
139
|
+
// ── cmd.exe write commands ──
|
|
140
|
+
"del",
|
|
141
|
+
"erase",
|
|
142
|
+
"rename",
|
|
143
|
+
"copy",
|
|
144
|
+
"xcopy",
|
|
145
|
+
"robocopy",
|
|
146
|
+
"move",
|
|
147
|
+
"md",
|
|
148
|
+
"rd",
|
|
149
|
+
"attrib",
|
|
150
|
+
"icacls",
|
|
151
|
+
"cacls",
|
|
152
|
+
"setx",
|
|
153
|
+
"reg",
|
|
154
|
+
]);
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
// Public API
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
/**
|
|
159
|
+
* Shell operators that separate distinct commands in a pipeline or chain.
|
|
160
|
+
* We check each segment independently — if ANY segment starts with a read
|
|
161
|
+
* command, the whole command is treated as containing a read.
|
|
162
|
+
*/
|
|
163
|
+
const SHELL_SEPARATORS = /(?:&&|\|\||[;|&])(?=(?:[^"']*["'][^"']*["'])*[^"']*$)/;
|
|
164
|
+
/**
|
|
165
|
+
* Output redirect operators. These turn a read command into a write —
|
|
166
|
+
* e.g. `grep pattern file > out.txt` writes output to a file. We still
|
|
167
|
+
* block these because the actual operation (grep/find/cat) is a read.
|
|
168
|
+
*/
|
|
169
|
+
const REDIRECT_WRITE = /\b>>?\b/;
|
|
170
|
+
/**
|
|
171
|
+
* Split a command string by shell separators (&&, ||, ;, |, &) and return
|
|
172
|
+
* the list of segment strings. Respects quoting so separators inside quotes
|
|
173
|
+
* are not treated as actual shell separators.
|
|
174
|
+
*/
|
|
175
|
+
export function splitShellSegments(command) {
|
|
176
|
+
const segments = [];
|
|
177
|
+
let current = "";
|
|
178
|
+
let inSingle = false;
|
|
179
|
+
let inDouble = false;
|
|
180
|
+
for (let i = 0; i < command.length; i++) {
|
|
181
|
+
const ch = command[i];
|
|
182
|
+
if (inSingle) {
|
|
183
|
+
current += ch;
|
|
184
|
+
if (ch === "'")
|
|
185
|
+
inSingle = false;
|
|
186
|
+
}
|
|
187
|
+
else if (inDouble) {
|
|
188
|
+
current += ch;
|
|
189
|
+
if (ch === '"')
|
|
190
|
+
inDouble = false;
|
|
191
|
+
else if (ch === "\\" && i + 1 < command.length) {
|
|
192
|
+
current += command[++i];
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
else if (ch === "'") {
|
|
196
|
+
current += ch;
|
|
197
|
+
inSingle = true;
|
|
198
|
+
}
|
|
199
|
+
else if (ch === '"') {
|
|
200
|
+
current += ch;
|
|
201
|
+
inDouble = true;
|
|
202
|
+
}
|
|
203
|
+
else if (ch === "|") {
|
|
204
|
+
// Peek ahead for ||
|
|
205
|
+
if (i + 1 < command.length && command[i + 1] === "|") {
|
|
206
|
+
// || separator
|
|
207
|
+
segments.push(current.trim());
|
|
208
|
+
current = "";
|
|
209
|
+
i++; // skip second |
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
// Single | pipe
|
|
213
|
+
segments.push(current.trim());
|
|
214
|
+
current = "";
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
else if (ch === "&") {
|
|
218
|
+
if (i + 1 < command.length && command[i + 1] === "&") {
|
|
219
|
+
// && separator
|
|
220
|
+
segments.push(current.trim());
|
|
221
|
+
current = "";
|
|
222
|
+
i++; // skip second &
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
// Single & background
|
|
226
|
+
segments.push(current.trim());
|
|
227
|
+
current = "";
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else if (ch === ";") {
|
|
231
|
+
segments.push(current.trim());
|
|
232
|
+
current = "";
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
current += ch;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// Flush remaining
|
|
239
|
+
const remainder = current.trim();
|
|
240
|
+
if (remainder.length > 0) {
|
|
241
|
+
segments.push(remainder);
|
|
242
|
+
}
|
|
243
|
+
return segments;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Check if a single segment (no pipes/chains) is a read command.
|
|
247
|
+
*/
|
|
248
|
+
function isReadSegment(segment) {
|
|
249
|
+
const argv = parseArgv(segment);
|
|
250
|
+
if (argv.length === 0)
|
|
251
|
+
return false;
|
|
252
|
+
const cmd = argv[0].toLowerCase();
|
|
253
|
+
// sed without -i is read-only
|
|
254
|
+
if (cmd === "sed" || cmd === "sed.exe") {
|
|
255
|
+
return !hasInlineFlag(argv);
|
|
256
|
+
}
|
|
257
|
+
return READ_COMMANDS.has(cmd);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Determine if a bash command is read-only and should be forwarded to Reader.
|
|
261
|
+
*
|
|
262
|
+
* Splits on shell separators (&&, ||, ;, |, &) and checks EACH segment.
|
|
263
|
+
* If ANY segment starts with a read command, the full command is blocked.
|
|
264
|
+
*
|
|
265
|
+
* Examples:
|
|
266
|
+
* isReadCommand("cat file") → true
|
|
267
|
+
* isReadCommand("echo hello && cat file") → true (cat in chain)
|
|
268
|
+
* isReadCommand("npm test") → false
|
|
269
|
+
* isReadCommand("grep x | head -5") → true (pipeline)
|
|
270
|
+
*/
|
|
271
|
+
export function isReadCommand(command) {
|
|
272
|
+
const segments = splitShellSegments(command);
|
|
273
|
+
for (const seg of segments) {
|
|
274
|
+
if (isReadSegment(seg))
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Determine if a bash command modifies the filesystem and should run directly.
|
|
281
|
+
*
|
|
282
|
+
* Rules:
|
|
283
|
+
* - If any segment's first word is in WRITE_COMMANDS → true
|
|
284
|
+
* - sed with -i flag → true (in-place edit)
|
|
285
|
+
* - Command contains > or >> redirect → true (writes to file)
|
|
286
|
+
* - Command contains tee → true (writes to file)
|
|
287
|
+
*/
|
|
288
|
+
export function isWriteCommand(command) {
|
|
289
|
+
const segments = splitShellSegments(command);
|
|
290
|
+
for (const seg of segments) {
|
|
291
|
+
const argv = parseArgv(seg);
|
|
292
|
+
if (argv.length === 0)
|
|
293
|
+
continue;
|
|
294
|
+
const cmd = argv[0].toLowerCase();
|
|
295
|
+
if (cmd === "sed" || cmd === "sed.exe") {
|
|
296
|
+
if (hasInlineFlag(argv))
|
|
297
|
+
return true;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (WRITE_COMMANDS.has(cmd))
|
|
301
|
+
return true;
|
|
302
|
+
// tee always writes
|
|
303
|
+
if (cmd === "tee" || cmd === "tee.exe")
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
// Output redirects anywhere in the full command = write
|
|
307
|
+
if (REDIRECT_WRITE.test(command))
|
|
308
|
+
return true;
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Wrap a shell command into a Reader subagent task.
|
|
313
|
+
*
|
|
314
|
+
* Returns a formatted string instructing the Reader to execute and report
|
|
315
|
+
* minimal results.
|
|
316
|
+
*/
|
|
317
|
+
export function wrapForReader(command) {
|
|
318
|
+
return [
|
|
319
|
+
"Execute this shell command and return ONLY the essential result.",
|
|
320
|
+
"Max 5 lines or a single number. Never dump full file contents.",
|
|
321
|
+
`Command: ${command}`,
|
|
322
|
+
].join("\n");
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Wrap a generic task (non-bash) into a Reader subagent task.
|
|
326
|
+
*/
|
|
327
|
+
export function wrapTaskForReader(task) {
|
|
328
|
+
return [
|
|
329
|
+
"Execute this task and return ONLY the essential result.",
|
|
330
|
+
"Max 5 lines or a single number. Never dump full file contents.",
|
|
331
|
+
`Task: ${task}`,
|
|
332
|
+
].join("\n");
|
|
333
|
+
}
|
|
334
|
+
// ---------------------------------------------------------------------------
|
|
335
|
+
// Internals
|
|
336
|
+
// ---------------------------------------------------------------------------
|
|
337
|
+
/**
|
|
338
|
+
* Parse a command string into argv tokens, respecting single/double quotes.
|
|
339
|
+
*
|
|
340
|
+
* This is a simplified parser — edge cases like escaped quotes inside
|
|
341
|
+
* opposite-quoted strings are handled on a best-effort basis.
|
|
342
|
+
*/
|
|
343
|
+
function parseArgv(command) {
|
|
344
|
+
const tokens = [];
|
|
345
|
+
let current = "";
|
|
346
|
+
let inSingle = false;
|
|
347
|
+
let inDouble = false;
|
|
348
|
+
for (let i = 0; i < command.length; i++) {
|
|
349
|
+
const ch = command[i];
|
|
350
|
+
if (inSingle) {
|
|
351
|
+
if (ch === "'") {
|
|
352
|
+
inSingle = false;
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
current += ch;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
else if (inDouble) {
|
|
359
|
+
if (ch === '"') {
|
|
360
|
+
inDouble = false;
|
|
361
|
+
}
|
|
362
|
+
else if (ch === "\\" && i + 1 < command.length) {
|
|
363
|
+
// Simple escape handling inside double quotes
|
|
364
|
+
const next = command[i + 1];
|
|
365
|
+
if (next === '"' || next === "\\" || next === "$" || next === "`") {
|
|
366
|
+
current += next;
|
|
367
|
+
i++;
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
current += ch;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
current += ch;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
if (ch === "'") {
|
|
379
|
+
inSingle = true;
|
|
380
|
+
}
|
|
381
|
+
else if (ch === '"') {
|
|
382
|
+
inDouble = true;
|
|
383
|
+
}
|
|
384
|
+
else if (ch === " " || ch === "\t") {
|
|
385
|
+
if (current.length > 0) {
|
|
386
|
+
tokens.push(current);
|
|
387
|
+
current = "";
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
current += ch;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
// Flush remaining token
|
|
396
|
+
if (current.length > 0) {
|
|
397
|
+
tokens.push(current);
|
|
398
|
+
}
|
|
399
|
+
return tokens;
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Check whether `sed` has the -i (in-place) flag.
|
|
403
|
+
*/
|
|
404
|
+
function hasInlineFlag(argv) {
|
|
405
|
+
for (let i = 1; i < argv.length; i++) {
|
|
406
|
+
const arg = argv[i];
|
|
407
|
+
// -i, -i.bak, --in-place, --in-place=.bak
|
|
408
|
+
if (arg === "-i" || arg.startsWith("-i.") || arg === "--in-place") {
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
if (arg.startsWith("--in-place=")) {
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
414
|
+
// Stop at the expression (s/.../.../ or -e '...') — flags after that
|
|
415
|
+
// might apply to the expression, not sed itself. In practice, -i always
|
|
416
|
+
// comes before the expression.
|
|
417
|
+
}
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
//# sourceMappingURL=bash-filter.js.map
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* config.ts — Configuration loader for pi-read-delegator
|
|
4
3
|
*
|
|
@@ -6,46 +5,10 @@
|
|
|
6
5
|
* If the config file doesn't exist, it creates one with defaults.
|
|
7
6
|
* If the config file is corrupted, it overwrites with defaults and logs a warning.
|
|
8
7
|
*/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
14
|
-
}
|
|
15
|
-
Object.defineProperty(o, k2, desc);
|
|
16
|
-
}) : (function(o, m, k, k2) {
|
|
17
|
-
if (k2 === undefined) k2 = k;
|
|
18
|
-
o[k2] = m[k];
|
|
19
|
-
}));
|
|
20
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
21
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
22
|
-
}) : function(o, v) {
|
|
23
|
-
o["default"] = v;
|
|
24
|
-
});
|
|
25
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
26
|
-
var ownKeys = function(o) {
|
|
27
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
28
|
-
var ar = [];
|
|
29
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
30
|
-
return ar;
|
|
31
|
-
};
|
|
32
|
-
return ownKeys(o);
|
|
33
|
-
};
|
|
34
|
-
return function (mod) {
|
|
35
|
-
if (mod && mod.__esModule) return mod;
|
|
36
|
-
var result = {};
|
|
37
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
38
|
-
__setModuleDefault(result, mod);
|
|
39
|
-
return result;
|
|
40
|
-
};
|
|
41
|
-
})();
|
|
42
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
-
exports.loadConfig = loadConfig;
|
|
44
|
-
exports.saveConfig = saveConfig;
|
|
45
|
-
const fs = __importStar(require("fs"));
|
|
46
|
-
const os = __importStar(require("os"));
|
|
47
|
-
const path = __importStar(require("path"));
|
|
48
|
-
const ui_1 = require("./ui");
|
|
8
|
+
import * as fs from "fs";
|
|
9
|
+
import * as os from "os";
|
|
10
|
+
import * as path from "path";
|
|
11
|
+
import { rawLog, rawWarn, rawError } from "./ui";
|
|
49
12
|
// ---------------------------------------------------------------------------
|
|
50
13
|
// Defaults
|
|
51
14
|
// ---------------------------------------------------------------------------
|
|
@@ -53,16 +16,31 @@ const DEFAULT_CONFIG = {
|
|
|
53
16
|
enabled: true,
|
|
54
17
|
reader_subagent_name: "reader",
|
|
55
18
|
blocked_tools: ["read", "grep", "find", "ls"],
|
|
56
|
-
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
|
|
65
|
-
|
|
19
|
+
orchestrator_prompt: [
|
|
20
|
+
"## Reader Subagent Protocol",
|
|
21
|
+
"",
|
|
22
|
+
"Your `read`,`grep`,`find`,`ls` tools are BLOCKED. Shell read commands (cat, grep, type, Get-Content, etc.) are also blocked.",
|
|
23
|
+
"",
|
|
24
|
+
"### How to delegate",
|
|
25
|
+
'Use: `subagent(agent="reader", task="<format>")`',
|
|
26
|
+
"Format: Action: {read|grep|find|ls} Target: {file|dir} Detail: {what,be specific}",
|
|
27
|
+
"",
|
|
28
|
+
"### Graduated reading (use this order)",
|
|
29
|
+
"1. **Find first**: `find src/ *.ts` → locate the relevant file.",
|
|
30
|
+
"2. **Grep next**: `grep functionName in src/file.ts` → locate the exact spot.",
|
|
31
|
+
"3. **Read last**: `read src/file.ts lines 42-80` → get only the needed section.",
|
|
32
|
+
"Never read an entire file unless you truly need all of it.",
|
|
33
|
+
"",
|
|
34
|
+
"### Cache awareness",
|
|
35
|
+
"You have previously-read file content in your context window. Before delegating, check if you already have what you need.",
|
|
36
|
+
"Re-reading the same file wastes tokens — reuse cached content.",
|
|
37
|
+
"",
|
|
38
|
+
"### Reader output format (no headers, no fluff)",
|
|
39
|
+
"grep: file:line content | read: N: line | find/ls: bare list",
|
|
40
|
+
"Large grep/find results: reader returns count-line first, then top matches.",
|
|
41
|
+
"The reader auto-skips imports, node_modules, binaries — you get clean data.",
|
|
42
|
+
'If you get "(no matches)" or an error, adjust and retry.',
|
|
43
|
+
].join("\n"),
|
|
66
44
|
reader_model: "lmstudio/nvidia/nemotron-3-nano-4b",
|
|
67
45
|
language: "auto",
|
|
68
46
|
};
|
|
@@ -89,7 +67,7 @@ function configFilePath() {
|
|
|
89
67
|
* - If the file is corrupted, overwrite with defaults, log a warning, return defaults.
|
|
90
68
|
* - Otherwise parse and return the typed config.
|
|
91
69
|
*/
|
|
92
|
-
function loadConfig() {
|
|
70
|
+
export function loadConfig() {
|
|
93
71
|
const filePath = configFilePath();
|
|
94
72
|
try {
|
|
95
73
|
if (!fs.existsSync(filePath)) {
|
|
@@ -106,7 +84,7 @@ function loadConfig() {
|
|
|
106
84
|
}
|
|
107
85
|
catch (err) {
|
|
108
86
|
// File is missing, unreadable, or invalid JSON → overwrite with defaults
|
|
109
|
-
|
|
87
|
+
rawWarn(`Corrupted config file at ${filePath}. Overwriting with defaults. Error: ${err}`);
|
|
110
88
|
try {
|
|
111
89
|
ensureDir(path.dirname(filePath));
|
|
112
90
|
fs.writeFileSync(filePath, JSON.stringify(DEFAULT_CONFIG, null, 2), "utf-8");
|
|
@@ -122,17 +100,17 @@ function loadConfig() {
|
|
|
122
100
|
* @param config The config object to persist
|
|
123
101
|
* @param options.silent If true, suppress console output
|
|
124
102
|
*/
|
|
125
|
-
function saveConfig(config, options) {
|
|
103
|
+
export function saveConfig(config, options) {
|
|
126
104
|
const filePath = configFilePath();
|
|
127
105
|
ensureDir(path.dirname(filePath));
|
|
128
106
|
try {
|
|
129
107
|
fs.writeFileSync(filePath, JSON.stringify(config, null, 2), "utf-8");
|
|
130
108
|
if (!options?.silent) {
|
|
131
|
-
|
|
109
|
+
rawLog(`Config saved to ${filePath}`);
|
|
132
110
|
}
|
|
133
111
|
}
|
|
134
112
|
catch (err) {
|
|
135
|
-
|
|
113
|
+
rawError(`Failed to save config: ${err}`);
|
|
136
114
|
throw err;
|
|
137
115
|
}
|
|
138
116
|
}
|
|
@@ -153,9 +131,6 @@ function mergeDefaults(partial, defaults) {
|
|
|
153
131
|
blocked_tools: Array.isArray(p.blocked_tools)
|
|
154
132
|
? p.blocked_tools
|
|
155
133
|
: defaults.blocked_tools,
|
|
156
|
-
allowed_bash_write_commands: Array.isArray(p.allowed_bash_write_commands)
|
|
157
|
-
? p.allowed_bash_write_commands
|
|
158
|
-
: defaults.allowed_bash_write_commands,
|
|
159
134
|
orchestrator_prompt: typeof p.orchestrator_prompt === "string"
|
|
160
135
|
? p.orchestrator_prompt
|
|
161
136
|
: defaults.orchestrator_prompt,
|