@stfade/pi-read-delegator 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -0
- package/bash-filter.d.ts +37 -0
- package/bash-filter.js +242 -0
- package/config.d.ts +31 -0
- package/config.js +169 -0
- package/index.d.ts +61 -0
- package/index.js +260 -0
- package/package.json +46 -0
- package/reader-manager.d.ts +71 -0
- package/reader-manager.js +274 -0
- package/templates/reader.md +8 -0
- package/tool-blocker.d.ts +59 -0
- package/tool-blocker.js +140 -0
- package/ui.d.ts +59 -0
- package/ui.js +273 -0
package/README.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# pi-read-delegator
|
|
2
|
+
|
|
3
|
+
Pi extension that removes read tools (`read`, `grep`, `find`, `ls`) from the main model and delegates all read operations to a local **Reader** subagent.
|
|
4
|
+
|
|
5
|
+
## How It Works
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Main Model Reader Subagent
|
|
9
|
+
┌──────────┐ ┌──────────┐
|
|
10
|
+
│ write │ │ read │
|
|
11
|
+
│ edit │ ──task──> │ grep │
|
|
12
|
+
│ bash │ <──result── │ find │
|
|
13
|
+
│ (write) │ │ ls │
|
|
14
|
+
└──────────┘ └──────────┘
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- **Write tools** (`write`, `edit`) remain with the main model.
|
|
18
|
+
- **Read tools** (`read`, `grep`, `find`, `ls`) are blocked on the main model and routed to the Reader subagent.
|
|
19
|
+
- **Bash commands** are filtered: read commands go to Reader, write commands execute directly, ambiguous commands prompt the user.
|
|
20
|
+
|
|
21
|
+
## Requirements
|
|
22
|
+
|
|
23
|
+
- [pi-subagents](https://github.com/earendil-works/pi-subagents) installed
|
|
24
|
+
- A local LLM for the Reader subagent (default: `lmstudio/nemotron-mini`)
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pi install pi-read-delegator
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or manually:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install -g pi-read-delegator
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Then edit `~/.pi/agent/agents/reader.md` to set your preferred Reader model.
|
|
39
|
+
|
|
40
|
+
## Commands
|
|
41
|
+
|
|
42
|
+
| Command | Description |
|
|
43
|
+
|---|---|
|
|
44
|
+
| `/read-delegator on` | Enable read delegation |
|
|
45
|
+
| `/read-delegator off` | Disable read delegation |
|
|
46
|
+
| `/read-delegator status` | Show current status |
|
|
47
|
+
|
|
48
|
+
## Configuration
|
|
49
|
+
|
|
50
|
+
Edit `~/.pi/agent/read-delegator.json`:
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"enabled": true,
|
|
55
|
+
"reader_subagent_name": "reader",
|
|
56
|
+
"blocked_tools": ["read", "grep", "find", "ls"],
|
|
57
|
+
"allowed_bash_write_commands": ["mkdir", "echo", "touch", "sed", "rm", "mv", "cp"],
|
|
58
|
+
"orchestrator_prompt": "You are an orchestrator. For any file reading... use the subagent tool...",
|
|
59
|
+
"language": "auto"
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
| Field | Description |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `enabled` | Enable/disable the extension |
|
|
66
|
+
| `reader_subagent_name` | Name of the Reader subagent (must match `reader.md`) |
|
|
67
|
+
| `blocked_tools` | Tools to block from the main model |
|
|
68
|
+
| `language` | `"auto"`, `"tr"`, or `"en"` |
|
|
69
|
+
|
|
70
|
+
## Reader Subagent
|
|
71
|
+
|
|
72
|
+
The Reader template is created at `~/.pi/agent/agents/reader.md` on first run. Edit the `model:` line to use your preferred provider:
|
|
73
|
+
|
|
74
|
+
```yaml
|
|
75
|
+
model: lmstudio/nemotron-mini # LM Studio
|
|
76
|
+
# model: ollama/phi3 # Ollama
|
|
77
|
+
# model: openai/gpt-4o-mini # OpenAI
|
|
78
|
+
# model: anthropic/claude-haiku # Anthropic
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Error Handling
|
|
82
|
+
|
|
83
|
+
When the Reader fails:
|
|
84
|
+
|
|
85
|
+
- **[R]etry** — resend the same task to Reader
|
|
86
|
+
- **[A]llow once** — temporarily unblock tools for one operation
|
|
87
|
+
- **[C]ancel** — report the failure
|
|
88
|
+
|
|
89
|
+
## Bash Filter
|
|
90
|
+
|
|
91
|
+
| Read commands → Reader | Write commands → Direct |
|
|
92
|
+
|---|---|
|
|
93
|
+
| `cat`, `grep`, `find`, `ls` | `mkdir`, `touch`, `rm`, `mv` |
|
|
94
|
+
| `head`, `tail`, `less`, `wc` | `cp`, `chmod`, `chown` |
|
|
95
|
+
| `bat`, `rg`, `fd`, `awk` | `npm`, `git`, `docker` |
|
|
96
|
+
| `sed` (without `-i`) | `sed -i` (in-place edit) |
|
|
97
|
+
|
|
98
|
+
## Supported Languages
|
|
99
|
+
|
|
100
|
+
- **English** (`en`)
|
|
101
|
+
- **Turkish** (`tr`)
|
|
102
|
+
|
|
103
|
+
Language is auto-detected from Pi's settings or the OS locale.
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
MIT
|
package/bash-filter.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
* Determine if a bash command is read-only and should be forwarded to Reader.
|
|
9
|
+
*
|
|
10
|
+
* Rules:
|
|
11
|
+
* - If the first word is in READ_COMMANDS → true
|
|
12
|
+
* - sed without -i flag → true (read-only stream edit)
|
|
13
|
+
* - sed with -i → false (in-place edit = write)
|
|
14
|
+
*/
|
|
15
|
+
export declare function isReadCommand(command: string): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Determine if a bash command modifies the filesystem and should run directly.
|
|
18
|
+
*
|
|
19
|
+
* Rules:
|
|
20
|
+
* - If the first word is in WRITE_COMMANDS → true
|
|
21
|
+
* - sed with -i flag → true (in-place edit)
|
|
22
|
+
* - Command contains > or >> redirect → true (writes to file)
|
|
23
|
+
* - Command contains tee without -a flag → true (writes to file)
|
|
24
|
+
*/
|
|
25
|
+
export declare function isWriteCommand(command: string): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Wrap a shell command into a Reader subagent task.
|
|
28
|
+
*
|
|
29
|
+
* Returns a formatted string instructing the Reader to execute and report
|
|
30
|
+
* minimal results.
|
|
31
|
+
*/
|
|
32
|
+
export declare function wrapForReader(command: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* Wrap a generic task (non-bash) into a Reader subagent task.
|
|
35
|
+
*/
|
|
36
|
+
export declare function wrapTaskForReader(task: string): string;
|
|
37
|
+
//# sourceMappingURL=bash-filter.d.ts.map
|
package/bash-filter.js
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* bash-filter.ts — Bash command classification and Reader forwarding
|
|
4
|
+
*
|
|
5
|
+
* Classifies shell commands as read-only (delegate to Reader subagent),
|
|
6
|
+
* write (execute directly), or ambiguous (prompt user).
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.isReadCommand = isReadCommand;
|
|
10
|
+
exports.isWriteCommand = isWriteCommand;
|
|
11
|
+
exports.wrapForReader = wrapForReader;
|
|
12
|
+
exports.wrapTaskForReader = wrapTaskForReader;
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Command lists
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
/**
|
|
17
|
+
* Commands that ONLY read and should be forwarded to the Reader subagent.
|
|
18
|
+
* - sed without -i is read-only (stream editor writing to stdout).
|
|
19
|
+
* - awk is read-only (pattern scanning and processing language).
|
|
20
|
+
*/
|
|
21
|
+
const READ_COMMANDS = new Set([
|
|
22
|
+
"cat",
|
|
23
|
+
"grep",
|
|
24
|
+
"find",
|
|
25
|
+
"ls",
|
|
26
|
+
"head",
|
|
27
|
+
"tail",
|
|
28
|
+
"less",
|
|
29
|
+
"wc",
|
|
30
|
+
"nl",
|
|
31
|
+
"more",
|
|
32
|
+
"bat",
|
|
33
|
+
"rg",
|
|
34
|
+
"fd",
|
|
35
|
+
"awk",
|
|
36
|
+
"du",
|
|
37
|
+
"df",
|
|
38
|
+
"stat",
|
|
39
|
+
"file",
|
|
40
|
+
"which",
|
|
41
|
+
"where",
|
|
42
|
+
"type",
|
|
43
|
+
"dir",
|
|
44
|
+
]);
|
|
45
|
+
/**
|
|
46
|
+
* Commands that write to the filesystem and should execute directly.
|
|
47
|
+
* sed and tee are context-dependent — handled specially in isWriteCommand.
|
|
48
|
+
*/
|
|
49
|
+
const WRITE_COMMANDS = new Set([
|
|
50
|
+
"mkdir",
|
|
51
|
+
"touch",
|
|
52
|
+
"echo",
|
|
53
|
+
"rm",
|
|
54
|
+
"mv",
|
|
55
|
+
"cp",
|
|
56
|
+
"chmod",
|
|
57
|
+
"chown",
|
|
58
|
+
"ln",
|
|
59
|
+
"rmdir",
|
|
60
|
+
"npm",
|
|
61
|
+
"pnpm",
|
|
62
|
+
"yarn",
|
|
63
|
+
"pip",
|
|
64
|
+
"cargo",
|
|
65
|
+
"go",
|
|
66
|
+
"npx",
|
|
67
|
+
"node",
|
|
68
|
+
"python",
|
|
69
|
+
"python3",
|
|
70
|
+
"git",
|
|
71
|
+
"docker",
|
|
72
|
+
"kubectl",
|
|
73
|
+
"tsc",
|
|
74
|
+
"make",
|
|
75
|
+
"cmake",
|
|
76
|
+
"dotnet",
|
|
77
|
+
"rustc",
|
|
78
|
+
"gcc",
|
|
79
|
+
"g++",
|
|
80
|
+
]);
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Public API
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
/**
|
|
85
|
+
* Determine if a bash command is read-only and should be forwarded to Reader.
|
|
86
|
+
*
|
|
87
|
+
* Rules:
|
|
88
|
+
* - If the first word is in READ_COMMANDS → true
|
|
89
|
+
* - sed without -i flag → true (read-only stream edit)
|
|
90
|
+
* - sed with -i → false (in-place edit = write)
|
|
91
|
+
*/
|
|
92
|
+
function isReadCommand(command) {
|
|
93
|
+
const argv = parseArgv(command);
|
|
94
|
+
if (argv.length === 0)
|
|
95
|
+
return false;
|
|
96
|
+
const cmd = argv[0].toLowerCase();
|
|
97
|
+
// sed is special: if -i is present, it's a write; otherwise read-only
|
|
98
|
+
if (cmd === "sed" || cmd === "sed.exe") {
|
|
99
|
+
return !hasInlineFlag(argv);
|
|
100
|
+
}
|
|
101
|
+
return READ_COMMANDS.has(cmd);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Determine if a bash command modifies the filesystem and should run directly.
|
|
105
|
+
*
|
|
106
|
+
* Rules:
|
|
107
|
+
* - If the first word is in WRITE_COMMANDS → true
|
|
108
|
+
* - sed with -i flag → true (in-place edit)
|
|
109
|
+
* - Command contains > or >> redirect → true (writes to file)
|
|
110
|
+
* - Command contains tee without -a flag → true (writes to file)
|
|
111
|
+
*/
|
|
112
|
+
function isWriteCommand(command) {
|
|
113
|
+
const argv = parseArgv(command);
|
|
114
|
+
if (argv.length === 0)
|
|
115
|
+
return false;
|
|
116
|
+
const cmd = argv[0].toLowerCase();
|
|
117
|
+
// sed with -i = write
|
|
118
|
+
if (cmd === "sed" || cmd === "sed.exe") {
|
|
119
|
+
return hasInlineFlag(argv);
|
|
120
|
+
}
|
|
121
|
+
if (WRITE_COMMANDS.has(cmd))
|
|
122
|
+
return true;
|
|
123
|
+
// Check for output redirection markers (> or >>)
|
|
124
|
+
// We do a simple string match outside the parsed argv because parseArgv
|
|
125
|
+
// might stop at the redirect operator.
|
|
126
|
+
if (/\b>>?\b/.test(command))
|
|
127
|
+
return true;
|
|
128
|
+
// tee is ambiguous: if -a (append) it's write, otherwise also write
|
|
129
|
+
if (cmd === "tee" || cmd === "tee.exe")
|
|
130
|
+
return true;
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Wrap a shell command into a Reader subagent task.
|
|
135
|
+
*
|
|
136
|
+
* Returns a formatted string instructing the Reader to execute and report
|
|
137
|
+
* minimal results.
|
|
138
|
+
*/
|
|
139
|
+
function wrapForReader(command) {
|
|
140
|
+
return [
|
|
141
|
+
"Execute this shell command and return ONLY the essential result.",
|
|
142
|
+
"Max 5 lines or a single number. Never dump full file contents.",
|
|
143
|
+
`Command: ${command}`,
|
|
144
|
+
].join("\n");
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Wrap a generic task (non-bash) into a Reader subagent task.
|
|
148
|
+
*/
|
|
149
|
+
function wrapTaskForReader(task) {
|
|
150
|
+
return [
|
|
151
|
+
"Execute this task and return ONLY the essential result.",
|
|
152
|
+
"Max 5 lines or a single number. Never dump full file contents.",
|
|
153
|
+
`Task: ${task}`,
|
|
154
|
+
].join("\n");
|
|
155
|
+
}
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// Internals
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
/**
|
|
160
|
+
* Parse a command string into argv tokens, respecting single/double quotes.
|
|
161
|
+
*
|
|
162
|
+
* This is a simplified parser — edge cases like escaped quotes inside
|
|
163
|
+
* opposite-quoted strings are handled on a best-effort basis.
|
|
164
|
+
*/
|
|
165
|
+
function parseArgv(command) {
|
|
166
|
+
const tokens = [];
|
|
167
|
+
let current = "";
|
|
168
|
+
let inSingle = false;
|
|
169
|
+
let inDouble = false;
|
|
170
|
+
for (let i = 0; i < command.length; i++) {
|
|
171
|
+
const ch = command[i];
|
|
172
|
+
if (inSingle) {
|
|
173
|
+
if (ch === "'") {
|
|
174
|
+
inSingle = false;
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
current += ch;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else if (inDouble) {
|
|
181
|
+
if (ch === '"') {
|
|
182
|
+
inDouble = false;
|
|
183
|
+
}
|
|
184
|
+
else if (ch === "\\" && i + 1 < command.length) {
|
|
185
|
+
// Simple escape handling inside double quotes
|
|
186
|
+
const next = command[i + 1];
|
|
187
|
+
if (next === '"' || next === "\\" || next === "$" || next === "`") {
|
|
188
|
+
current += next;
|
|
189
|
+
i++;
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
current += ch;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
current += ch;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
if (ch === "'") {
|
|
201
|
+
inSingle = true;
|
|
202
|
+
}
|
|
203
|
+
else if (ch === '"') {
|
|
204
|
+
inDouble = true;
|
|
205
|
+
}
|
|
206
|
+
else if (ch === " " || ch === "\t") {
|
|
207
|
+
if (current.length > 0) {
|
|
208
|
+
tokens.push(current);
|
|
209
|
+
current = "";
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
current += ch;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// Flush remaining token
|
|
218
|
+
if (current.length > 0) {
|
|
219
|
+
tokens.push(current);
|
|
220
|
+
}
|
|
221
|
+
return tokens;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Check whether `sed` has the -i (in-place) flag.
|
|
225
|
+
*/
|
|
226
|
+
function hasInlineFlag(argv) {
|
|
227
|
+
for (let i = 1; i < argv.length; i++) {
|
|
228
|
+
const arg = argv[i];
|
|
229
|
+
// -i, -i.bak, --in-place, --in-place=.bak
|
|
230
|
+
if (arg === "-i" || arg.startsWith("-i.") || arg === "--in-place") {
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
if (arg.startsWith("--in-place=")) {
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
// Stop at the expression (s/.../.../ or -e '...') — flags after that
|
|
237
|
+
// might apply to the expression, not sed itself. In practice, -i always
|
|
238
|
+
// comes before the expression.
|
|
239
|
+
}
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
//# sourceMappingURL=bash-filter.js.map
|
package/config.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* config.ts — Configuration loader for pi-read-delegator
|
|
3
|
+
*
|
|
4
|
+
* Reads/writes ~/.pi/agent/read-delegator.json with sensible defaults.
|
|
5
|
+
* If the config file doesn't exist, it creates one with defaults.
|
|
6
|
+
* If the config file is corrupted, it overwrites with defaults and logs a warning.
|
|
7
|
+
*/
|
|
8
|
+
export interface ReadDelegatorConfig {
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
reader_subagent_name: string;
|
|
11
|
+
blocked_tools: string[];
|
|
12
|
+
allowed_bash_write_commands: string[];
|
|
13
|
+
orchestrator_prompt: string;
|
|
14
|
+
language: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Load configuration from disk.
|
|
18
|
+
* - If the file doesn't exist, create it with defaults and return them.
|
|
19
|
+
* - If the file is corrupted, overwrite with defaults, log a warning, return defaults.
|
|
20
|
+
* - Otherwise parse and return the typed config.
|
|
21
|
+
*/
|
|
22
|
+
export declare function loadConfig(): ReadDelegatorConfig;
|
|
23
|
+
/**
|
|
24
|
+
* Save configuration to disk.
|
|
25
|
+
* @param config The config object to persist
|
|
26
|
+
* @param options.silent If true, suppress console output
|
|
27
|
+
*/
|
|
28
|
+
export declare function saveConfig(config: ReadDelegatorConfig, options?: {
|
|
29
|
+
silent?: boolean;
|
|
30
|
+
}): void;
|
|
31
|
+
//# sourceMappingURL=config.d.ts.map
|
package/config.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* config.ts — Configuration loader for pi-read-delegator
|
|
4
|
+
*
|
|
5
|
+
* Reads/writes ~/.pi/agent/read-delegator.json with sensible defaults.
|
|
6
|
+
* If the config file doesn't exist, it creates one with defaults.
|
|
7
|
+
* If the config file is corrupted, it overwrites with defaults and logs a warning.
|
|
8
|
+
*/
|
|
9
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
12
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
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
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Defaults
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
const DEFAULT_CONFIG = {
|
|
52
|
+
enabled: true,
|
|
53
|
+
reader_subagent_name: "reader",
|
|
54
|
+
blocked_tools: ["read", "grep", "find", "ls"],
|
|
55
|
+
allowed_bash_write_commands: [
|
|
56
|
+
"mkdir",
|
|
57
|
+
"echo",
|
|
58
|
+
"touch",
|
|
59
|
+
"sed",
|
|
60
|
+
"rm",
|
|
61
|
+
"mv",
|
|
62
|
+
"cp",
|
|
63
|
+
],
|
|
64
|
+
orchestrator_prompt: "You are an orchestrator. For any file reading, searching, or listing operation, you MUST use the subagent tool with subagent='reader'. Do not use read/grep/find/ls yourself. If you need to run a shell command that only reads (like cat, grep, find, ls), also delegate it to the reader subagent.",
|
|
65
|
+
language: "auto",
|
|
66
|
+
};
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Path helpers
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
/** Expand ~ to the user's home directory. */
|
|
71
|
+
function expandTilde(filePath) {
|
|
72
|
+
if (filePath.startsWith("~")) {
|
|
73
|
+
return path.join(os.homedir(), filePath.slice(1));
|
|
74
|
+
}
|
|
75
|
+
return filePath;
|
|
76
|
+
}
|
|
77
|
+
/** Full path to the config file. */
|
|
78
|
+
function configFilePath() {
|
|
79
|
+
return expandTilde("~/.pi/agent/read-delegator.json");
|
|
80
|
+
}
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Public API
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
/**
|
|
85
|
+
* Load configuration from disk.
|
|
86
|
+
* - If the file doesn't exist, create it with defaults and return them.
|
|
87
|
+
* - If the file is corrupted, overwrite with defaults, log a warning, return defaults.
|
|
88
|
+
* - Otherwise parse and return the typed config.
|
|
89
|
+
*/
|
|
90
|
+
function loadConfig() {
|
|
91
|
+
const filePath = configFilePath();
|
|
92
|
+
try {
|
|
93
|
+
if (!fs.existsSync(filePath)) {
|
|
94
|
+
// First run: create the config directory and write defaults
|
|
95
|
+
ensureDir(path.dirname(filePath));
|
|
96
|
+
saveConfig(DEFAULT_CONFIG, { silent: true });
|
|
97
|
+
return { ...DEFAULT_CONFIG };
|
|
98
|
+
}
|
|
99
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
100
|
+
const parsed = JSON.parse(raw);
|
|
101
|
+
// Merge with defaults so missing keys get their default values
|
|
102
|
+
const config = mergeDefaults(parsed, DEFAULT_CONFIG);
|
|
103
|
+
return config;
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
// File is missing, unreadable, or invalid JSON → overwrite with defaults
|
|
107
|
+
console.warn(`[pi-read-delegator] Corrupted config file at ${filePath}. Overwriting with defaults. Error: ${err}`);
|
|
108
|
+
try {
|
|
109
|
+
ensureDir(path.dirname(filePath));
|
|
110
|
+
fs.writeFileSync(filePath, JSON.stringify(DEFAULT_CONFIG, null, 2), "utf-8");
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// Silently fail — we tried our best
|
|
114
|
+
}
|
|
115
|
+
return { ...DEFAULT_CONFIG };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Save configuration to disk.
|
|
120
|
+
* @param config The config object to persist
|
|
121
|
+
* @param options.silent If true, suppress console output
|
|
122
|
+
*/
|
|
123
|
+
function saveConfig(config, options) {
|
|
124
|
+
const filePath = configFilePath();
|
|
125
|
+
ensureDir(path.dirname(filePath));
|
|
126
|
+
try {
|
|
127
|
+
fs.writeFileSync(filePath, JSON.stringify(config, null, 2), "utf-8");
|
|
128
|
+
if (!options?.silent) {
|
|
129
|
+
console.log(`[pi-read-delegator] Config saved to ${filePath}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
console.error(`[pi-read-delegator] Failed to save config: ${err}`);
|
|
134
|
+
throw err;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Helpers
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
/** Recursively merge a partial user config on top of the defaults. */
|
|
141
|
+
function mergeDefaults(partial, defaults) {
|
|
142
|
+
if (typeof partial !== "object" || partial === null) {
|
|
143
|
+
return { ...defaults };
|
|
144
|
+
}
|
|
145
|
+
const p = partial;
|
|
146
|
+
return {
|
|
147
|
+
enabled: typeof p.enabled === "boolean" ? p.enabled : defaults.enabled,
|
|
148
|
+
reader_subagent_name: typeof p.reader_subagent_name === "string"
|
|
149
|
+
? p.reader_subagent_name
|
|
150
|
+
: defaults.reader_subagent_name,
|
|
151
|
+
blocked_tools: Array.isArray(p.blocked_tools)
|
|
152
|
+
? p.blocked_tools
|
|
153
|
+
: defaults.blocked_tools,
|
|
154
|
+
allowed_bash_write_commands: Array.isArray(p.allowed_bash_write_commands)
|
|
155
|
+
? p.allowed_bash_write_commands
|
|
156
|
+
: defaults.allowed_bash_write_commands,
|
|
157
|
+
orchestrator_prompt: typeof p.orchestrator_prompt === "string"
|
|
158
|
+
? p.orchestrator_prompt
|
|
159
|
+
: defaults.orchestrator_prompt,
|
|
160
|
+
language: typeof p.language === "string" ? p.language : defaults.language,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
/** Recursively ensure a directory exists. */
|
|
164
|
+
function ensureDir(dir) {
|
|
165
|
+
if (!fs.existsSync(dir)) {
|
|
166
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=config.js.map
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* index.ts — pi-read-delegator extension entry point
|
|
3
|
+
*
|
|
4
|
+
* Lifecycle:
|
|
5
|
+
* init(agent) → load config, check deps, ensure template, enable/disable
|
|
6
|
+
* enable(agent) → block tools, add system prompt, attach bash filter
|
|
7
|
+
* disable(agent) → restore tools, remove prompt, detach bash filter
|
|
8
|
+
*
|
|
9
|
+
* Commands:
|
|
10
|
+
* /read-delegator on → enable the delegator
|
|
11
|
+
* /read-delegator off → disable the delegator
|
|
12
|
+
* /read-delegator status → show current status
|
|
13
|
+
*/
|
|
14
|
+
import { type AgentWithSubagent } from "./reader-manager";
|
|
15
|
+
/**
|
|
16
|
+
* The Pi agent interface as consumed by pi-read-delegator.
|
|
17
|
+
* Extends the building-block types from sub-modules.
|
|
18
|
+
*/
|
|
19
|
+
export interface PiAgent extends AgentWithSubagent {
|
|
20
|
+
/** Return current tool definitions. */
|
|
21
|
+
getTools(): Array<{
|
|
22
|
+
name: string;
|
|
23
|
+
}>;
|
|
24
|
+
/** Remove a tool by name. */
|
|
25
|
+
removeTool(name: string): void;
|
|
26
|
+
/** Add/re-add a tool definition. */
|
|
27
|
+
addTool(definition: {
|
|
28
|
+
name: string;
|
|
29
|
+
[key: string]: unknown;
|
|
30
|
+
}): void;
|
|
31
|
+
/** Append a persistent system message to the conversation. */
|
|
32
|
+
addSystemMessage(text: string): void;
|
|
33
|
+
/** Remove a previously-added system message by its exact text. */
|
|
34
|
+
removeSystemMessage(text: string): void;
|
|
35
|
+
/** Register a hook that fires BEFORE a tool with the given name is called. */
|
|
36
|
+
onBeforeToolCall(toolName: string, callback: (params: unknown) => Promise<unknown> | unknown): void;
|
|
37
|
+
/** Register a Pi command (like /read-delegator on). */
|
|
38
|
+
registerCommand(name: string, handler: (args: string[]) => Promise<string> | string): void;
|
|
39
|
+
/** Execute a raw shell command directly on the system. */
|
|
40
|
+
executeShellCommand(command: string): Promise<{
|
|
41
|
+
stdout: string;
|
|
42
|
+
stderr: string;
|
|
43
|
+
}>;
|
|
44
|
+
/** Prompt the user for input. */
|
|
45
|
+
promptUser(message: string): Promise<string>;
|
|
46
|
+
/** Display a message to the user. */
|
|
47
|
+
displayMessage(message: string): void;
|
|
48
|
+
/** Set status bar text. */
|
|
49
|
+
setStatusBarText(text: string): void;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Initialize the extension.
|
|
53
|
+
*
|
|
54
|
+
* This is the function Pi calls when loading the extension.
|
|
55
|
+
* It returns a lifecycle object with enable() and disable().
|
|
56
|
+
*/
|
|
57
|
+
export declare function init(agent: PiAgent): {
|
|
58
|
+
enable: () => void;
|
|
59
|
+
disable: () => void;
|
|
60
|
+
};
|
|
61
|
+
//# sourceMappingURL=index.d.ts.map
|