@stfade/pi-read-delegator 1.0.4 → 1.0.5

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.
Files changed (3) hide show
  1. package/index.d.ts +2 -60
  2. package/index.js +208 -232
  3. package/package.json +1 -1
package/index.d.ts CHANGED
@@ -1,61 +1,3 @@
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
- };
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ export default function (pi: ExtensionAPI): Promise<void>;
61
3
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -1,260 +1,236 @@
1
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);
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
55
7
  }
56
- return { enable, disable };
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.default = default_1;
37
+ const fs = __importStar(require("node:fs"));
38
+ const os = __importStar(require("node:os"));
39
+ const path = __importStar(require("node:path"));
40
+ const DEFAULT_CONFIG = {
41
+ enabled: true,
42
+ reader_subagent_name: "reader",
43
+ blocked_tools: ["read", "grep", "find", "ls"],
44
+ orchestrator_prompt: [
45
+ "You are an orchestrator. You do NOT have direct file-reading tools.",
46
+ "For any file reading, searching, or directory listing, use the",
47
+ "'subagent' tool with agent='reader'.",
48
+ 'Example: subagent(agent: "reader", task: "Find all TS files that import \'lodash\'")',
49
+ "Never try to use read, grep, find, or ls yourself. Always delegate.",
50
+ ].join("\n"),
51
+ language: "auto",
52
+ };
53
+ const READ_BASH_COMMANDS = new Set([
54
+ "cat",
55
+ "grep",
56
+ "find",
57
+ "ls",
58
+ "head",
59
+ "tail",
60
+ "less",
61
+ "wc",
62
+ "nl",
63
+ "more",
64
+ "bat",
65
+ "rg",
66
+ "fd",
67
+ "awk",
68
+ "du",
69
+ "df",
70
+ "stat",
71
+ "file",
72
+ "which",
73
+ "where",
74
+ "type",
75
+ "dir",
76
+ ]);
77
+ function configPath() {
78
+ return path.join(os.homedir(), ".pi", "agent", "read-delegator.json");
57
79
  }
58
- // ---------------------------------------------------------------------------
59
- // Async initialization (runs in background)
60
- // ---------------------------------------------------------------------------
61
- async function initAsync(agent) {
80
+ function readerPath() {
81
+ return path.join(os.homedir(), ".pi", "agent", "agents", "reader.md");
82
+ }
83
+ function loadConfig() {
84
+ const cp = configPath();
62
85
  try {
63
- // Check pi-subagents dependency
64
- await (0, reader_manager_1.checkDependencies)(agent.promptUser);
86
+ if (fs.existsSync(cp)) {
87
+ const raw = fs.readFileSync(cp, "utf8");
88
+ const parsed = JSON.parse(raw);
89
+ return { ...DEFAULT_CONFIG, ...parsed };
90
+ }
65
91
  }
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;
92
+ catch {
93
+ // corrupt file --- fall back to defaults
72
94
  }
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.");
95
+ try {
96
+ const dir = path.dirname(cp);
97
+ fs.mkdirSync(dir, { recursive: true });
98
+ fs.writeFileSync(cp, JSON.stringify(DEFAULT_CONFIG, null, 2), "utf8");
99
+ }
100
+ catch {
101
+ // read-only home directory --- ignore
77
102
  }
103
+ return { ...DEFAULT_CONFIG };
78
104
  }
79
- // ---------------------------------------------------------------------------
80
- // Enable / Disable
81
- // ---------------------------------------------------------------------------
82
- function doEnable(agent) {
83
- if (enabled) {
84
- agent.displayMessage((0, ui_1.msg)("already_blocked"));
85
- return;
105
+ function saveConfig(config) {
106
+ const cp = configPath();
107
+ try {
108
+ const dir = path.dirname(cp);
109
+ fs.mkdirSync(dir, { recursive: true });
110
+ fs.writeFileSync(cp, JSON.stringify(config, null, 2), "utf8");
86
111
  }
87
- if (!config) {
88
- (0, ui_1.logError)("reader_failed", "No configuration loaded.");
89
- return;
112
+ catch {
113
+ // read-only home directory --- ignore
90
114
  }
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
115
  }
104
- function doDisable(agent) {
105
- if (!enabled) {
106
- agent.displayMessage((0, ui_1.msg)("already_enabled"));
116
+ async function ensureReaderTemplate() {
117
+ const rp = readerPath();
118
+ if (fs.existsSync(rp))
107
119
  return;
120
+ const content = [
121
+ "---",
122
+ "name: reader",
123
+ "description: Token-efficient code reader that returns minimal results.",
124
+ "tools: read, grep, find, ls",
125
+ "model: lmstudio/nvidia/nemotron-3-nano-4b",
126
+ "---",
127
+ "",
128
+ "You are a token-efficient analyst. Execute read/search/list tasks and return",
129
+ "ONLY the essential result. Maximum 10 lines. Use bullet summaries.",
130
+ "Never dump entire files. Focus only on what was asked.",
131
+ ].join("\n");
132
+ try {
133
+ const dir = path.dirname(rp);
134
+ fs.mkdirSync(dir, { recursive: true });
135
+ await fs.promises.writeFile(rp, content, "utf8");
108
136
  }
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;
137
+ catch {
138
+ // read-only home directory --- template creation is best-effort
120
139
  }
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
140
  }
127
- // ---------------------------------------------------------------------------
128
- // Bash filter hook
129
- // ---------------------------------------------------------------------------
130
141
  /**
131
- * Attach a before-tool-call hook on the "bash" (and "shell") tools.
142
+ * Determine which tools should stay active after blocking read tools.
132
143
  *
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
144
+ * We MUST keep the 'subagent' tool (registered by pi-subagents) active;
145
+ * otherwise the orchestrator cannot call the reader at all.
137
146
  */
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
- }
147
+ function computeActiveTools(pi, blocked) {
148
+ const all = pi.getAllTools();
149
+ const blockedSet = new Set(blocked);
150
+ // Always keep "subagent" --- it is the bridge to the reader.
151
+ const forceKeep = new Set(["subagent"]);
152
+ return all
153
+ .map((t) => t.name)
154
+ .filter((name) => forceKeep.has(name) || !blockedSet.has(name));
216
155
  }
217
156
  // ---------------------------------------------------------------------------
218
- // Pi commands
157
+ // Extension entry
219
158
  // ---------------------------------------------------------------------------
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");
159
+ async function default_1(pi) {
160
+ const config = loadConfig();
161
+ if (!config.enabled)
162
+ return;
163
+ // --- 1. Block read tools ------------------------------------------------
164
+ const active = computeActiveTools(pi, config.blocked_tools);
165
+ pi.setActiveTools(active);
166
+ // --- 2. Inject orchestrator system prompt -------------------------------
167
+ pi.on("before_agent_start", async (event, _ctx) => {
168
+ return {
169
+ systemPrompt: `${event.systemPrompt}\n\n${config.orchestrator_prompt}`,
170
+ };
171
+ });
172
+ // --- 3. Intercept bash read commands ------------------------------------
173
+ //
174
+ // When the LLM tries `cat some-file` or similar, we block the call and
175
+ // tell it to route through the reader subagent instead.
176
+ pi.on("tool_call", async (event, _ctx) => {
177
+ if (event.toolName === "bash" || event.toolName === "shell") {
178
+ const command = String(event.input?.command ?? "");
179
+ const firstWord = command.trim().split(/\s+/)[0]?.toLowerCase() ?? "";
180
+ if (READ_BASH_COMMANDS.has(firstWord)) {
181
+ return {
182
+ block: true,
183
+ reason: [
184
+ `Use subagent(agent: "reader", task: "Execute and summarize: ${command}")`,
185
+ "instead of running file-reading commands directly.",
186
+ ].join(" "),
187
+ };
233
188
  }
234
- case "off":
235
- case "disable": {
236
- if (config) {
189
+ }
190
+ });
191
+ // --- 4. Register /read-delegator command --------------------------------
192
+ pi.registerCommand("read-delegator", {
193
+ description: "Manage read delegation (on|off|status)",
194
+ handler: async (args, ctx) => {
195
+ const sub = args?.trim().toLowerCase() ?? "status";
196
+ switch (sub) {
197
+ case "on":
198
+ case "enable": {
199
+ config.enabled = true;
200
+ saveConfig(config);
201
+ const active2 = computeActiveTools(pi, config.blocked_tools);
202
+ pi.setActiveTools(active2);
203
+ ctx.ui.notify("🟢 Read delegation enabled", "info");
204
+ return;
205
+ }
206
+ case "off":
207
+ case "disable": {
237
208
  config.enabled = false;
238
- (0, config_1.saveConfig)(config, { silent: true });
209
+ saveConfig(config);
210
+ // Restore all tools
211
+ pi.setActiveTools(pi.getAllTools().map((t) => t.name));
212
+ ctx.ui.notify("🔴 Read delegation disabled", "info");
213
+ ctx.ui.setStatus("read-delegator", undefined);
214
+ return;
215
+ }
216
+ case "status":
217
+ default: {
218
+ const lines = [
219
+ `Read delegation: ${config.enabled ? "🟢 enabled" : "🔴 disabled"}`,
220
+ `Blocked tools: ${config.blocked_tools.join(", ")}`,
221
+ `Reader subagent: ${config.reader_subagent_name}`,
222
+ ];
223
+ ctx.ui.notify(lines.join("\n"), "info");
224
+ return;
239
225
  }
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
226
  }
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
- }
227
+ },
228
+ });
229
+ // --- 5. Ensure reader.md template ---------------------------------------
230
+ await ensureReaderTemplate();
231
+ // --- 6. Status bar ------------------------------------------------------
232
+ pi.on("session_start", async (_event, ctx) => {
233
+ ctx.ui.setStatus("read-delegator", `● reader: ${config.reader_subagent_name}`);
258
234
  });
259
235
  }
260
236
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stfade/pi-read-delegator",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
4
4
  "description": "Pi extension that delegates all read operations to a Reader subagent, blocking read tools from the main model",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",