@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/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.