@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/index.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* index.ts — pi-read-delegator extension entry point
|
|
4
|
+
*
|
|
5
|
+
* Lifecycle:
|
|
6
|
+
* init(agent) → load config, check deps, ensure template, enable/disable
|
|
7
|
+
* enable(agent) → block tools, add system prompt, attach bash filter
|
|
8
|
+
* disable(agent) → restore tools, remove prompt, detach bash filter
|
|
9
|
+
*
|
|
10
|
+
* Commands:
|
|
11
|
+
* /read-delegator on → enable the delegator
|
|
12
|
+
* /read-delegator off → disable the delegator
|
|
13
|
+
* /read-delegator status → show current status
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.init = init;
|
|
17
|
+
const config_1 = require("./config");
|
|
18
|
+
const tool_blocker_1 = require("./tool-blocker");
|
|
19
|
+
const bash_filter_1 = require("./bash-filter");
|
|
20
|
+
const reader_manager_1 = require("./reader-manager");
|
|
21
|
+
const ui_1 = require("./ui");
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Module state
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
let enabled = false;
|
|
26
|
+
let config = null;
|
|
27
|
+
let currentSystemMessage = null;
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Lifecycle: init
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
/**
|
|
32
|
+
* Initialize the extension.
|
|
33
|
+
*
|
|
34
|
+
* This is the function Pi calls when loading the extension.
|
|
35
|
+
* It returns a lifecycle object with enable() and disable().
|
|
36
|
+
*/
|
|
37
|
+
function init(agent) {
|
|
38
|
+
// 1. Load configuration
|
|
39
|
+
config = (0, config_1.loadConfig)();
|
|
40
|
+
// 2. Detect language
|
|
41
|
+
(0, ui_1.getLanguage)(config.language);
|
|
42
|
+
// 3. Initialize status bar
|
|
43
|
+
(0, ui_1.initStatusBar)(agent);
|
|
44
|
+
// 4. Register commands
|
|
45
|
+
registerCommands(agent);
|
|
46
|
+
// 5. Run async init tasks (dependency check, template) in background.
|
|
47
|
+
// We do NOT block init — if deps are missing the user will be prompted.
|
|
48
|
+
initAsync(agent);
|
|
49
|
+
// Build lifecycle interface
|
|
50
|
+
const enable = () => doEnable(agent);
|
|
51
|
+
const disable = () => doDisable(agent);
|
|
52
|
+
// If config says enabled, auto-enable (synchronous part first)
|
|
53
|
+
if (config?.enabled) {
|
|
54
|
+
doEnable(agent);
|
|
55
|
+
}
|
|
56
|
+
return { enable, disable };
|
|
57
|
+
}
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Async initialization (runs in background)
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
async function initAsync(agent) {
|
|
62
|
+
try {
|
|
63
|
+
// Check pi-subagents dependency
|
|
64
|
+
await (0, reader_manager_1.checkDependencies)(agent.promptUser);
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
(0, ui_1.logError)("deps_failed");
|
|
68
|
+
(0, ui_1.logError)("reader_failed", String(err));
|
|
69
|
+
// Disable the extension if dependencies can't be satisfied
|
|
70
|
+
doDisable(agent);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
// Ensure reader.md template exists
|
|
74
|
+
const templateOk = (0, reader_manager_1.ensureReaderTemplate)();
|
|
75
|
+
if (!templateOk) {
|
|
76
|
+
(0, ui_1.logWarn)("reader_failed", "Reader template could not be created. Create ~/.pi/agent/agents/reader.md manually.");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Enable / Disable
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
function doEnable(agent) {
|
|
83
|
+
if (enabled) {
|
|
84
|
+
agent.displayMessage((0, ui_1.msg)("already_blocked"));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (!config) {
|
|
88
|
+
(0, ui_1.logError)("reader_failed", "No configuration loaded.");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// Block read tools
|
|
92
|
+
(0, tool_blocker_1.blockTools)(agent, config.blocked_tools);
|
|
93
|
+
// Add system message
|
|
94
|
+
currentSystemMessage = config.orchestrator_prompt;
|
|
95
|
+
agent.addSystemMessage(config.orchestrator_prompt);
|
|
96
|
+
// Attach bash filter hook
|
|
97
|
+
attachBashFilter(agent);
|
|
98
|
+
// Update status
|
|
99
|
+
enabled = true;
|
|
100
|
+
(0, ui_1.updateStatusBar)("active");
|
|
101
|
+
(0, ui_1.log)("enabled");
|
|
102
|
+
agent.displayMessage((0, ui_1.msg)("enabled"));
|
|
103
|
+
}
|
|
104
|
+
function doDisable(agent) {
|
|
105
|
+
if (!enabled) {
|
|
106
|
+
agent.displayMessage((0, ui_1.msg)("already_enabled"));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
// Restore read tools
|
|
110
|
+
(0, tool_blocker_1.restoreTools)(agent);
|
|
111
|
+
// Remove system message
|
|
112
|
+
if (currentSystemMessage) {
|
|
113
|
+
try {
|
|
114
|
+
agent.removeSystemMessage(currentSystemMessage);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Best effort — the message text may have been mutated
|
|
118
|
+
}
|
|
119
|
+
currentSystemMessage = null;
|
|
120
|
+
}
|
|
121
|
+
// Detach bash filter (we can't undo onBeforeToolCall, but we set a flag)
|
|
122
|
+
enabled = false;
|
|
123
|
+
(0, ui_1.updateStatusBar)("idle");
|
|
124
|
+
(0, ui_1.log)("disabled");
|
|
125
|
+
agent.displayMessage((0, ui_1.msg)("disabled"));
|
|
126
|
+
}
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
// Bash filter hook
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
/**
|
|
131
|
+
* Attach a before-tool-call hook on the "bash" (and "shell") tools.
|
|
132
|
+
*
|
|
133
|
+
* When the main model tries to execute a bash command:
|
|
134
|
+
* - Read commands → forwarded to Reader subagent
|
|
135
|
+
* - Write commands → executed directly
|
|
136
|
+
* - Ambiguous → user is prompted
|
|
137
|
+
*/
|
|
138
|
+
function attachBashFilter(agent) {
|
|
139
|
+
// Hook both "bash" and "shell" tools, since Pi may expose either.
|
|
140
|
+
const bashToolNames = ["bash", "shell"];
|
|
141
|
+
for (const toolName of bashToolNames) {
|
|
142
|
+
try {
|
|
143
|
+
agent.onBeforeToolCall(toolName, async (params) => {
|
|
144
|
+
// Only intercept if the extension is enabled
|
|
145
|
+
if (!enabled || !config)
|
|
146
|
+
return undefined; // undefined = proceed normally
|
|
147
|
+
const p = params;
|
|
148
|
+
const command = typeof p.command === "string" ? p.command : "";
|
|
149
|
+
if (!command)
|
|
150
|
+
return undefined; // Let the tool handle the error
|
|
151
|
+
// Classify the command
|
|
152
|
+
if ((0, bash_filter_1.isWriteCommand)(command)) {
|
|
153
|
+
// Let the raw bash/shell tool execute this directly
|
|
154
|
+
return undefined; // undefined → Pi runs the original tool
|
|
155
|
+
}
|
|
156
|
+
if ((0, bash_filter_1.isReadCommand)(command)) {
|
|
157
|
+
// Forward to Reader subagent
|
|
158
|
+
(0, ui_1.log)("reader_calling", command);
|
|
159
|
+
try {
|
|
160
|
+
const result = await (0, reader_manager_1.callReader)(agent, config, (0, bash_filter_1.wrapForReader)(command));
|
|
161
|
+
(0, ui_1.log)("reader_done");
|
|
162
|
+
// Return the result directly — Pi will use this as the tool output
|
|
163
|
+
// instead of running the original bash command.
|
|
164
|
+
return { result, subagent_used: true };
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
(0, ui_1.logError)("reader_failed", String(err));
|
|
168
|
+
// Offer the [R/A/C] dialog
|
|
169
|
+
try {
|
|
170
|
+
const handled = await (0, reader_manager_1.handleReaderError)(agent, config, config.blocked_tools, err, (0, bash_filter_1.wrapForReader)(command), agent.promptUser);
|
|
171
|
+
// If "Allow once" was selected, return a special marker
|
|
172
|
+
if (handled.startsWith("[ALLOW_ONCE]")) {
|
|
173
|
+
return { result: handled, allow_once: true };
|
|
174
|
+
}
|
|
175
|
+
// Retry succeeded — return the result
|
|
176
|
+
return { result: handled, subagent_used: true };
|
|
177
|
+
}
|
|
178
|
+
catch (finalErr) {
|
|
179
|
+
(0, ui_1.updateStatusBar)("error");
|
|
180
|
+
return {
|
|
181
|
+
error: true,
|
|
182
|
+
message: finalErr instanceof Error
|
|
183
|
+
? finalErr.message
|
|
184
|
+
: "Reader failed",
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// Ambiguous command → ask user
|
|
190
|
+
const answer = await agent.promptUser(`The command "${command}" may read files. Run via Reader? [Y/n]`);
|
|
191
|
+
if (answer.trim().toLowerCase() === "n" ||
|
|
192
|
+
answer.trim().toLowerCase() === "no") {
|
|
193
|
+
// Let the original tool run
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
// Forward to Reader
|
|
197
|
+
(0, ui_1.log)("reader_calling", command);
|
|
198
|
+
try {
|
|
199
|
+
const result = await (0, reader_manager_1.callReader)(agent, config, (0, bash_filter_1.wrapForReader)(command));
|
|
200
|
+
(0, ui_1.log)("reader_done");
|
|
201
|
+
return { result, subagent_used: true };
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
(0, ui_1.logError)("reader_failed", String(err));
|
|
205
|
+
return {
|
|
206
|
+
error: true,
|
|
207
|
+
message: err instanceof Error ? err.message : "Reader failed",
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
// onBeforeToolCall not supported for this tool — no-op
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
// Pi commands
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
function registerCommands(agent) {
|
|
221
|
+
agent.registerCommand("read-delegator", async (args) => {
|
|
222
|
+
const sub = args[0]?.toLowerCase();
|
|
223
|
+
switch (sub) {
|
|
224
|
+
case "on":
|
|
225
|
+
case "enable": {
|
|
226
|
+
if (!config) {
|
|
227
|
+
config = (0, config_1.loadConfig)();
|
|
228
|
+
}
|
|
229
|
+
config.enabled = true;
|
|
230
|
+
(0, config_1.saveConfig)(config, { silent: true });
|
|
231
|
+
doEnable(agent);
|
|
232
|
+
return (0, ui_1.msg)("enabled");
|
|
233
|
+
}
|
|
234
|
+
case "off":
|
|
235
|
+
case "disable": {
|
|
236
|
+
if (config) {
|
|
237
|
+
config.enabled = false;
|
|
238
|
+
(0, config_1.saveConfig)(config, { silent: true });
|
|
239
|
+
}
|
|
240
|
+
doDisable(agent);
|
|
241
|
+
return (0, ui_1.msg)("disabled");
|
|
242
|
+
}
|
|
243
|
+
case "status": {
|
|
244
|
+
const status = (0, ui_1.getStatus)();
|
|
245
|
+
const blocked = (0, tool_blocker_1.getBlockedTools)();
|
|
246
|
+
return (`pi-read-delegator is ${status}\n` +
|
|
247
|
+
`Enabled: ${enabled ? "yes" : "no"}\n` +
|
|
248
|
+
`Blocked tools: ${blocked.join(", ") || "(none)"}\n` +
|
|
249
|
+
`Reader subagent: ${config?.reader_subagent_name ?? "reader"}\n` +
|
|
250
|
+
`Language: ${config?.language ?? "auto"}`);
|
|
251
|
+
}
|
|
252
|
+
default:
|
|
253
|
+
return ("Usage:\n" +
|
|
254
|
+
" /read-delegator on — enable read delegation\n" +
|
|
255
|
+
" /read-delegator off — disable read delegation\n" +
|
|
256
|
+
" /read-delegator status — show current status");
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
//# sourceMappingURL=index.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stfade/pi-read-delegator",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Pi extension that delegates all read operations to a Reader subagent, blocking read tools from the main model",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"pi-extension": true,
|
|
8
|
+
"files": [
|
|
9
|
+
"index.js",
|
|
10
|
+
"index.d.ts",
|
|
11
|
+
"config.js",
|
|
12
|
+
"config.d.ts",
|
|
13
|
+
"tool-blocker.js",
|
|
14
|
+
"tool-blocker.d.ts",
|
|
15
|
+
"bash-filter.js",
|
|
16
|
+
"bash-filter.d.ts",
|
|
17
|
+
"reader-manager.js",
|
|
18
|
+
"reader-manager.d.ts",
|
|
19
|
+
"ui.js",
|
|
20
|
+
"ui.d.ts",
|
|
21
|
+
"templates/reader.md"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc",
|
|
25
|
+
"prepublishOnly": "npm run build"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"pi",
|
|
29
|
+
"pi-extension",
|
|
30
|
+
"read-delegator",
|
|
31
|
+
"subagent",
|
|
32
|
+
"reader"
|
|
33
|
+
],
|
|
34
|
+
"author": "",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^20.0.0",
|
|
38
|
+
"typescript": "^5.4.0"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"pi-subagents": "*"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reader-manager.ts — Reader subagent lifecycle and error handling
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities:
|
|
5
|
+
* - Check that pi-subagents is installed (prompt user if not)
|
|
6
|
+
* - Ensure the reader.md subagent template exists
|
|
7
|
+
* - Call the Reader subagent with a task
|
|
8
|
+
* - Handle errors with a [R]etry / [A]llow once / [C]ancel prompt
|
|
9
|
+
*/
|
|
10
|
+
import type { ReadDelegatorConfig } from "./config";
|
|
11
|
+
import type { ExtensionAgent } from "./tool-blocker";
|
|
12
|
+
export interface AgentWithSubagent extends ExtensionAgent {
|
|
13
|
+
/** Call a subagent by name with a task string. Returns the subagent's response. */
|
|
14
|
+
callSubagent(params: {
|
|
15
|
+
name: string;
|
|
16
|
+
task: string;
|
|
17
|
+
}): Promise<string>;
|
|
18
|
+
}
|
|
19
|
+
/** Error thrown when the Reader subagent fails. */
|
|
20
|
+
export declare class ReaderError extends Error {
|
|
21
|
+
readonly originalError?: unknown | undefined;
|
|
22
|
+
constructor(message: string, originalError?: unknown | undefined);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Verify that pi-subagents is installed as a Pi extension.
|
|
26
|
+
*
|
|
27
|
+
* Strategy: check whether the `pi-subagents` npm package is findable.
|
|
28
|
+
* If not, prompt the user to install it. If they agree, install via
|
|
29
|
+
* `pi install pi-subagents` (fallback: `npm install -g pi-subagents`).
|
|
30
|
+
*
|
|
31
|
+
* @returns true if installed or successfully installed
|
|
32
|
+
* @throws if the user declines or installation fails
|
|
33
|
+
*/
|
|
34
|
+
export declare function checkDependencies(prompt: (message: string) => Promise<string>): Promise<boolean>;
|
|
35
|
+
/**
|
|
36
|
+
* Ensure the reader.md subagent template exists.
|
|
37
|
+
* If not, copy the bundled template from `templates/reader.md`.
|
|
38
|
+
*
|
|
39
|
+
* @returns true if the template exists after this call
|
|
40
|
+
*/
|
|
41
|
+
export declare function ensureReaderTemplate(): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Send a task to the Reader subagent and return its response.
|
|
44
|
+
*
|
|
45
|
+
* @param agent The Pi agent with subagent-calling capability
|
|
46
|
+
* @param config Current extension configuration
|
|
47
|
+
* @param task The task string to send
|
|
48
|
+
* @param timeoutMs Timeout in milliseconds (default 30s)
|
|
49
|
+
* @returns The Reader's response text
|
|
50
|
+
* @throws ReaderError on timeout, failure, or empty response
|
|
51
|
+
*/
|
|
52
|
+
export declare function callReader(agent: AgentWithSubagent, config: ReadDelegatorConfig, task: string, timeoutMs?: number): Promise<string>;
|
|
53
|
+
/**
|
|
54
|
+
* Handle a Reader failure by prompting the user.
|
|
55
|
+
*
|
|
56
|
+
* Options:
|
|
57
|
+
* - [R]etry → re-send the same task to Reader
|
|
58
|
+
* - [A]llow once → temporarily unblock tools, let main model do it, re-block
|
|
59
|
+
* - [C]ancel → throw the error upstream
|
|
60
|
+
*
|
|
61
|
+
* @param agent The Pi agent
|
|
62
|
+
* @param config Extension config
|
|
63
|
+
* @param blockedTools Tool names currently blocked
|
|
64
|
+
* @param error The error that occurred
|
|
65
|
+
* @param task The original task string
|
|
66
|
+
* @param prompt Async prompt function (should collect user input)
|
|
67
|
+
* @returns Reader response on Retry/Allow; never returns on Cancel
|
|
68
|
+
* @throws ReaderError on Cancel or repeated failure
|
|
69
|
+
*/
|
|
70
|
+
export declare function handleReaderError(agent: AgentWithSubagent, config: ReadDelegatorConfig, blockedTools: string[], error: unknown, task: string, prompt: (message: string) => Promise<string>): Promise<string>;
|
|
71
|
+
//# sourceMappingURL=reader-manager.d.ts.map
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* reader-manager.ts — Reader subagent lifecycle and error handling
|
|
4
|
+
*
|
|
5
|
+
* Responsibilities:
|
|
6
|
+
* - Check that pi-subagents is installed (prompt user if not)
|
|
7
|
+
* - Ensure the reader.md subagent template exists
|
|
8
|
+
* - Call the Reader subagent with a task
|
|
9
|
+
* - Handle errors with a [R]etry / [A]llow once / [C]ancel prompt
|
|
10
|
+
*/
|
|
11
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
14
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
15
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
16
|
+
}
|
|
17
|
+
Object.defineProperty(o, k2, desc);
|
|
18
|
+
}) : (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
o[k2] = m[k];
|
|
21
|
+
}));
|
|
22
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
23
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
24
|
+
}) : function(o, v) {
|
|
25
|
+
o["default"] = v;
|
|
26
|
+
});
|
|
27
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
28
|
+
var ownKeys = function(o) {
|
|
29
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
30
|
+
var ar = [];
|
|
31
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
32
|
+
return ar;
|
|
33
|
+
};
|
|
34
|
+
return ownKeys(o);
|
|
35
|
+
};
|
|
36
|
+
return function (mod) {
|
|
37
|
+
if (mod && mod.__esModule) return mod;
|
|
38
|
+
var result = {};
|
|
39
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
40
|
+
__setModuleDefault(result, mod);
|
|
41
|
+
return result;
|
|
42
|
+
};
|
|
43
|
+
})();
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.ReaderError = void 0;
|
|
46
|
+
exports.checkDependencies = checkDependencies;
|
|
47
|
+
exports.ensureReaderTemplate = ensureReaderTemplate;
|
|
48
|
+
exports.callReader = callReader;
|
|
49
|
+
exports.handleReaderError = handleReaderError;
|
|
50
|
+
const fs = __importStar(require("fs"));
|
|
51
|
+
const path = __importStar(require("path"));
|
|
52
|
+
const os = __importStar(require("os"));
|
|
53
|
+
const child_process_1 = require("child_process");
|
|
54
|
+
/** Error thrown when the Reader subagent fails. */
|
|
55
|
+
class ReaderError extends Error {
|
|
56
|
+
originalError;
|
|
57
|
+
constructor(message, originalError) {
|
|
58
|
+
super(message);
|
|
59
|
+
this.originalError = originalError;
|
|
60
|
+
this.name = "ReaderError";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
exports.ReaderError = ReaderError;
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Paths
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
function expandTilde(p) {
|
|
68
|
+
if (p.startsWith("~"))
|
|
69
|
+
return path.join(os.homedir(), p.slice(1));
|
|
70
|
+
return p;
|
|
71
|
+
}
|
|
72
|
+
const READER_TEMPLATE_PATH = expandTilde("~/.pi/agent/agents/reader.md");
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// 1. Dependency check
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
/**
|
|
77
|
+
* Verify that pi-subagents is installed as a Pi extension.
|
|
78
|
+
*
|
|
79
|
+
* Strategy: check whether the `pi-subagents` npm package is findable.
|
|
80
|
+
* If not, prompt the user to install it. If they agree, install via
|
|
81
|
+
* `pi install pi-subagents` (fallback: `npm install -g pi-subagents`).
|
|
82
|
+
*
|
|
83
|
+
* @returns true if installed or successfully installed
|
|
84
|
+
* @throws if the user declines or installation fails
|
|
85
|
+
*/
|
|
86
|
+
async function checkDependencies(prompt) {
|
|
87
|
+
// Try to resolve pi-subagents to verify it's installed
|
|
88
|
+
if (isSubagentsInstalled()) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
console.warn("[pi-read-delegator] ⚠️ pi-subagents is not installed.");
|
|
92
|
+
const answer = await prompt("⚠️ pi-subagents is not installed. Install it now? [Y/n]");
|
|
93
|
+
const normalized = answer.trim().toLowerCase();
|
|
94
|
+
if (normalized !== "" && normalized !== "y" && normalized !== "yes") {
|
|
95
|
+
throw new Error("pi-subagents is required. Please install it manually: pi install pi-subagents");
|
|
96
|
+
}
|
|
97
|
+
// Attempt installation
|
|
98
|
+
console.log("[pi-read-delegator] 📦 Installing pi-subagents…");
|
|
99
|
+
try {
|
|
100
|
+
// Try `pi install pi-subagents` first (the Pi package manager)
|
|
101
|
+
(0, child_process_1.execSync)("pi install pi-subagents", {
|
|
102
|
+
stdio: "pipe",
|
|
103
|
+
timeout: 60_000,
|
|
104
|
+
encoding: "utf-8",
|
|
105
|
+
});
|
|
106
|
+
console.log("[pi-read-delegator] ✅ pi-subagents installed via pi.");
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
// Fallback to global npm install
|
|
110
|
+
try {
|
|
111
|
+
(0, child_process_1.execSync)("npm install -g pi-subagents", {
|
|
112
|
+
stdio: "pipe",
|
|
113
|
+
timeout: 60_000,
|
|
114
|
+
encoding: "utf-8",
|
|
115
|
+
});
|
|
116
|
+
console.log("[pi-read-delegator] ✅ pi-subagents installed via npm.");
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
throw new Error("❌ Installation failed. Please install manually: npm install -g pi-subagents");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Verify installation took effect
|
|
123
|
+
if (!isSubagentsInstalled()) {
|
|
124
|
+
throw new Error("❌ pi-subagents installed but cannot be found. Restart Pi and try again.");
|
|
125
|
+
}
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Simple check: can we require/import pi-subagents?
|
|
130
|
+
*/
|
|
131
|
+
function isSubagentsInstalled() {
|
|
132
|
+
try {
|
|
133
|
+
// Dynamic require that works even if TypeScript doesn't know the module
|
|
134
|
+
const mod = require("pi-subagents");
|
|
135
|
+
return mod !== undefined;
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
// 2. Reader template
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
/**
|
|
145
|
+
* Ensure the reader.md subagent template exists.
|
|
146
|
+
* If not, copy the bundled template from `templates/reader.md`.
|
|
147
|
+
*
|
|
148
|
+
* @returns true if the template exists after this call
|
|
149
|
+
*/
|
|
150
|
+
function ensureReaderTemplate() {
|
|
151
|
+
if (fs.existsSync(READER_TEMPLATE_PATH)) {
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
// Path to the bundled template (sibling to the compiled JS)
|
|
155
|
+
const bundledPath = path.join(__dirname, "templates", "reader.md");
|
|
156
|
+
if (!fs.existsSync(bundledPath)) {
|
|
157
|
+
console.warn("[pi-read-delegator] ⚠️ Bundled reader template not found at:", bundledPath);
|
|
158
|
+
console.warn("[pi-read-delegator] Please create ~/.pi/agent/agents/reader.md manually.");
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
const content = fs.readFileSync(bundledPath, "utf-8");
|
|
163
|
+
ensureDir(path.dirname(READER_TEMPLATE_PATH));
|
|
164
|
+
fs.writeFileSync(READER_TEMPLATE_PATH, content, "utf-8");
|
|
165
|
+
console.log(`[pi-read-delegator] ✅ Created reader subagent template: ${READER_TEMPLATE_PATH}`);
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
console.error("[pi-read-delegator] ⚠️ Failed to create reader template:", err);
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// 3. Call Reader
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
/**
|
|
177
|
+
* Send a task to the Reader subagent and return its response.
|
|
178
|
+
*
|
|
179
|
+
* @param agent The Pi agent with subagent-calling capability
|
|
180
|
+
* @param config Current extension configuration
|
|
181
|
+
* @param task The task string to send
|
|
182
|
+
* @param timeoutMs Timeout in milliseconds (default 30s)
|
|
183
|
+
* @returns The Reader's response text
|
|
184
|
+
* @throws ReaderError on timeout, failure, or empty response
|
|
185
|
+
*/
|
|
186
|
+
async function callReader(agent, config, task, timeoutMs = 30_000) {
|
|
187
|
+
const result = await withTimeout(agent.callSubagent({
|
|
188
|
+
name: config.reader_subagent_name,
|
|
189
|
+
task,
|
|
190
|
+
}), timeoutMs, `Reader subagent timed out after ${timeoutMs / 1000}s`);
|
|
191
|
+
if (!result || result.trim().length === 0) {
|
|
192
|
+
throw new ReaderError("Reader subagent returned an empty response.");
|
|
193
|
+
}
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
// 4. Error handling: [R]etry / [A]llow once / [C]ancel
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
/**
|
|
200
|
+
* Handle a Reader failure by prompting the user.
|
|
201
|
+
*
|
|
202
|
+
* Options:
|
|
203
|
+
* - [R]etry → re-send the same task to Reader
|
|
204
|
+
* - [A]llow once → temporarily unblock tools, let main model do it, re-block
|
|
205
|
+
* - [C]ancel → throw the error upstream
|
|
206
|
+
*
|
|
207
|
+
* @param agent The Pi agent
|
|
208
|
+
* @param config Extension config
|
|
209
|
+
* @param blockedTools Tool names currently blocked
|
|
210
|
+
* @param error The error that occurred
|
|
211
|
+
* @param task The original task string
|
|
212
|
+
* @param prompt Async prompt function (should collect user input)
|
|
213
|
+
* @returns Reader response on Retry/Allow; never returns on Cancel
|
|
214
|
+
* @throws ReaderError on Cancel or repeated failure
|
|
215
|
+
*/
|
|
216
|
+
async function handleReaderError(agent, config, blockedTools, error, task, prompt) {
|
|
217
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
218
|
+
console.error(`[pi-read-delegator] ❌ Reader failed: ${errMsg}`);
|
|
219
|
+
const answer = await prompt(`\n❌ Reader subagent failed: ${errMsg}\n` +
|
|
220
|
+
`[R]etry [A]llow once (let main model do it) [C]ancel\n`);
|
|
221
|
+
const choice = answer.trim().toLowerCase();
|
|
222
|
+
if (choice === "r" || choice === "retry") {
|
|
223
|
+
// Retry the same task
|
|
224
|
+
console.log("[pi-read-delegator] 🔄 Retrying Reader…");
|
|
225
|
+
return callReader(agent, config, task);
|
|
226
|
+
}
|
|
227
|
+
if (choice === "a" || choice === "allow" || choice === "allow once") {
|
|
228
|
+
// Temporarily unblock tools, let main model execute the task,
|
|
229
|
+
// then re-block.
|
|
230
|
+
console.log("[pi-read-delegator] 🔓 Allowing main model to read once…");
|
|
231
|
+
// NOTE: For "Allow once", we need the main model to perform the task.
|
|
232
|
+
// However, we are inside a tool call — the main model can't run code
|
|
233
|
+
// inline. We return a specially formatted string that instructs the
|
|
234
|
+
// main model: "blocked tools are now available for one operation;
|
|
235
|
+
// please perform the following task and then they will be re-locked."
|
|
236
|
+
//
|
|
237
|
+
// The tempAllowOnce wrapper re-blocks after this handler returns.
|
|
238
|
+
// Actually, the flow for "Allow once" is:
|
|
239
|
+
// 1. We restore tools (they're currently blocked)
|
|
240
|
+
// 2. We return a message telling the main model "do this task yourself"
|
|
241
|
+
// 3. After the main model does ONE operation, tools are re-blocked
|
|
242
|
+
//
|
|
243
|
+
// But since we can't intercept the main model's turn boundary
|
|
244
|
+
// precisely, we return a message and rely on the next hook point
|
|
245
|
+
// to re-block. The caller should call tempAllowOnce around this.
|
|
246
|
+
return `[ALLOW_ONCE] The blocked tools (${blockedTools.join(", ")}) are now temporarily available. Please perform the following task yourself, then tools will be re-blocked:\n\n${task}`;
|
|
247
|
+
}
|
|
248
|
+
// Cancel
|
|
249
|
+
throw new ReaderError(`Reader failed and user cancelled: ${errMsg}`, error);
|
|
250
|
+
}
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
// Helpers
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
/** Race a promise against a timeout. */
|
|
255
|
+
async function withTimeout(promise, ms, timeoutMessage) {
|
|
256
|
+
let timer;
|
|
257
|
+
const timeout = new Promise((_, reject) => {
|
|
258
|
+
timer = setTimeout(() => reject(new ReaderError(timeoutMessage)), ms);
|
|
259
|
+
});
|
|
260
|
+
try {
|
|
261
|
+
const result = await Promise.race([promise, timeout]);
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
264
|
+
finally {
|
|
265
|
+
clearTimeout(timer);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/** Ensure a directory exists, creating it recursively. */
|
|
269
|
+
function ensureDir(dir) {
|
|
270
|
+
if (!fs.existsSync(dir)) {
|
|
271
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
//# sourceMappingURL=reader-manager.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: reader
|
|
3
|
+
description: Read-only file agent that returns minimal results
|
|
4
|
+
tools: read, grep, find, ls
|
|
5
|
+
model: lmstudio/nemotron-mini
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
You are a token-efficient assistant. Execute read/search tasks and return ONLY the essential result. Max 5 lines or a single number. Never dump files. If running a shell command, return only the output, nothing else.
|