@stfade/pi-read-delegator 1.0.5 → 1.0.7

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.d.ts CHANGED
@@ -1,3 +1,15 @@
1
+ /**
2
+ * index.ts — pi-read-delegator entry point
3
+ *
4
+ * Blocks read tools from the orchestrator and tells it to delegate every
5
+ * file-read / search task to the 'reader' subagent.
6
+ *
7
+ * Architecture:
8
+ * - Factory body: registration only (pi.on, pi.registerCommand, ensureReaderTemplate)
9
+ * - session_start: dependency check → tool blocking → status bar
10
+ * - before_agent_start: inject orchestrator system prompt
11
+ * - tool_call: intercept bash read commands → redirect to reader
12
+ */
1
13
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
14
  export default function (pi: ExtensionAPI): Promise<void>;
3
15
  //# sourceMappingURL=index.d.ts.map
package/index.js CHANGED
@@ -1,4 +1,16 @@
1
1
  "use strict";
2
+ /**
3
+ * index.ts — pi-read-delegator entry point
4
+ *
5
+ * Blocks read tools from the orchestrator and tells it to delegate every
6
+ * file-read / search task to the 'reader' subagent.
7
+ *
8
+ * Architecture:
9
+ * - Factory body: registration only (pi.on, pi.registerCommand, ensureReaderTemplate)
10
+ * - session_start: dependency check → tool blocking → status bar
11
+ * - before_agent_start: inject orchestrator system prompt
12
+ * - tool_call: intercept bash read commands → redirect to reader
13
+ */
2
14
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
15
  if (k2 === undefined) k2 = k;
4
16
  var desc = Object.getOwnPropertyDescriptor(m, k);
@@ -37,19 +49,12 @@ exports.default = default_1;
37
49
  const fs = __importStar(require("node:fs"));
38
50
  const os = __importStar(require("node:os"));
39
51
  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
- };
52
+ const config_1 = require("./config");
53
+ const reader_manager_1 = require("./reader-manager");
54
+ const ui_1 = require("./ui");
55
+ // ---------------------------------------------------------------------------
56
+ // Bash read commands — these are intercepted and redirected to reader
57
+ // ---------------------------------------------------------------------------
53
58
  const READ_BASH_COMMANDS = new Set([
54
59
  "cat",
55
60
  "grep",
@@ -74,45 +79,12 @@ const READ_BASH_COMMANDS = new Set([
74
79
  "type",
75
80
  "dir",
76
81
  ]);
77
- function configPath() {
78
- return path.join(os.homedir(), ".pi", "agent", "read-delegator.json");
79
- }
82
+ // ---------------------------------------------------------------------------
83
+ // Reader template path
84
+ // ---------------------------------------------------------------------------
80
85
  function readerPath() {
81
86
  return path.join(os.homedir(), ".pi", "agent", "agents", "reader.md");
82
87
  }
83
- function loadConfig() {
84
- const cp = configPath();
85
- try {
86
- if (fs.existsSync(cp)) {
87
- const raw = fs.readFileSync(cp, "utf8");
88
- const parsed = JSON.parse(raw);
89
- return { ...DEFAULT_CONFIG, ...parsed };
90
- }
91
- }
92
- catch {
93
- // corrupt file --- fall back to defaults
94
- }
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
102
- }
103
- return { ...DEFAULT_CONFIG };
104
- }
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");
111
- }
112
- catch {
113
- // read-only home directory --- ignore
114
- }
115
- }
116
88
  async function ensureReaderTemplate() {
117
89
  const rp = readerPath();
118
90
  if (fs.existsSync(rp))
@@ -135,60 +107,106 @@ async function ensureReaderTemplate() {
135
107
  await fs.promises.writeFile(rp, content, "utf8");
136
108
  }
137
109
  catch {
138
- // read-only home directory --- template creation is best-effort
110
+ // read-only home directory template creation is best-effort
139
111
  }
140
112
  }
141
- /**
142
- * Determine which tools should stay active after blocking read tools.
143
- *
144
- * We MUST keep the 'subagent' tool (registered by pi-subagents) active;
145
- * otherwise the orchestrator cannot call the reader at all.
146
- */
113
+ // ---------------------------------------------------------------------------
114
+ // Tool helpers
115
+ // ---------------------------------------------------------------------------
116
+ /** Determine which tools stay active after blocking read tools.
117
+ * Always keeps "subagent" — the bridge to the reader. */
147
118
  function computeActiveTools(pi, blocked) {
148
119
  const all = pi.getAllTools();
149
120
  const blockedSet = new Set(blocked);
150
- // Always keep "subagent" --- it is the bridge to the reader.
151
121
  const forceKeep = new Set(["subagent"]);
152
122
  return all
153
123
  .map((t) => t.name)
154
124
  .filter((name) => forceKeep.has(name) || !blockedSet.has(name));
155
125
  }
156
126
  // ---------------------------------------------------------------------------
157
- // Extension entry
127
+ // Extension factory — registration only; actions go inside events
158
128
  // ---------------------------------------------------------------------------
159
129
  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 -------------------------------
130
+ const config = (0, config_1.loadConfig)();
131
+ // Detect language
132
+ (0, ui_1.getLanguage)(config.language);
133
+ // Quick sync dependency check interactive prompt is deferred to
134
+ // session_start where we have access to ctx.ui.confirm().
135
+ let depsReady = (0, reader_manager_1.isSubagentsInstalled)();
136
+ let depsChecked = depsReady; // if already ready, no need to check again
137
+ // -----------------------------------------------------------------------
138
+ // 1. session_start: dependency check → tool blocking → status bar
139
+ // -----------------------------------------------------------------------
140
+ pi.on("session_start", async (_event, ctx) => {
141
+ // --- Dependency check ---
142
+ if (!depsChecked) {
143
+ // Build a prompt function backed by ctx.ui.confirm()
144
+ const promptFn = async (message) => {
145
+ const ok = await ctx.ui.confirm("pi-subagents required", message);
146
+ return ok ? "y" : "n";
147
+ };
148
+ depsReady = await (0, reader_manager_1.checkDependencies)(promptFn);
149
+ depsChecked = true;
150
+ if (!depsReady) {
151
+ // Dependency missing and user declined / install failed
152
+ config.enabled = false;
153
+ (0, config_1.saveConfig)(config, { silent: true });
154
+ ctx.ui.setStatus("read-delegator", (0, ui_1.msg)("status_error"));
155
+ ctx.ui.notify((0, ui_1.msg)("deps_disabled"), "warning");
156
+ return;
157
+ }
158
+ // If deps are now ready, re-enable config in case user previously
159
+ // declined but now installed manually before session start.
160
+ if (!config.enabled) {
161
+ config.enabled = true;
162
+ (0, config_1.saveConfig)(config, { silent: true });
163
+ }
164
+ }
165
+ // --- Tool blocking ---
166
+ if (config.enabled) {
167
+ pi.setActiveTools(computeActiveTools(pi, config.blocked_tools));
168
+ }
169
+ ctx.ui.setStatus("read-delegator", config.enabled ? (0, ui_1.msg)("status_active") : (0, ui_1.msg)("status_off"));
170
+ });
171
+ // -----------------------------------------------------------------------
172
+ // 2. before_agent_start: inject orchestrator system prompt
173
+ // -----------------------------------------------------------------------
167
174
  pi.on("before_agent_start", async (event, _ctx) => {
175
+ if (!config.enabled)
176
+ return;
168
177
  return {
169
- systemPrompt: `${event.systemPrompt}\n\n${config.orchestrator_prompt}`,
178
+ systemPrompt: event.systemPrompt + "\n\n" + config.orchestrator_prompt,
170
179
  };
171
180
  });
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.
181
+ // -----------------------------------------------------------------------
182
+ // 3. tool_call: intercept bash read commands
183
+ // -----------------------------------------------------------------------
176
184
  pi.on("tool_call", async (event, _ctx) => {
185
+ if (!config.enabled)
186
+ return;
177
187
  if (event.toolName === "bash" || event.toolName === "shell") {
178
188
  const command = String(event.input?.command ?? "");
189
+ if (!command)
190
+ return;
179
191
  const firstWord = command.trim().split(/\s+/)[0]?.toLowerCase() ?? "";
180
192
  if (READ_BASH_COMMANDS.has(firstWord)) {
181
193
  return {
182
194
  block: true,
183
195
  reason: [
184
- `Use subagent(agent: "reader", task: "Execute and summarize: ${command}")`,
196
+ 'Use subagent(agent: "' +
197
+ config.reader_subagent_name +
198
+ '", task: "Execute and summarize: ' +
199
+ command +
200
+ '")',
185
201
  "instead of running file-reading commands directly.",
186
202
  ].join(" "),
187
203
  };
188
204
  }
189
205
  }
190
206
  });
191
- // --- 4. Register /read-delegator command --------------------------------
207
+ // -----------------------------------------------------------------------
208
+ // 4. /read-delegator command
209
+ // -----------------------------------------------------------------------
192
210
  pi.registerCommand("read-delegator", {
193
211
  description: "Manage read delegation (on|off|status)",
194
212
  handler: async (args, ctx) => {
@@ -196,29 +214,34 @@ async function default_1(pi) {
196
214
  switch (sub) {
197
215
  case "on":
198
216
  case "enable": {
217
+ if (!depsReady) {
218
+ ctx.ui.notify("pi-subagents not installed. Install it first to enable read delegation.", "warning");
219
+ return;
220
+ }
199
221
  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");
222
+ (0, config_1.saveConfig)(config, { silent: true });
223
+ pi.setActiveTools(computeActiveTools(pi, config.blocked_tools));
224
+ ctx.ui.notify((0, ui_1.msg)("enabled"), "info");
225
+ ctx.ui.setStatus("read-delegator", (0, ui_1.msg)("status_active"));
204
226
  return;
205
227
  }
206
228
  case "off":
207
229
  case "disable": {
208
230
  config.enabled = false;
209
- saveConfig(config);
210
- // Restore all tools
231
+ (0, config_1.saveConfig)(config, { silent: true });
211
232
  pi.setActiveTools(pi.getAllTools().map((t) => t.name));
212
- ctx.ui.notify("🔴 Read delegation disabled", "info");
213
- ctx.ui.setStatus("read-delegator", undefined);
233
+ ctx.ui.notify((0, ui_1.msg)("disabled"), "info");
234
+ ctx.ui.setStatus("read-delegator", (0, ui_1.msg)("status_off"));
214
235
  return;
215
236
  }
216
237
  case "status":
217
238
  default: {
218
239
  const lines = [
219
- `Read delegation: ${config.enabled ? "🟢 enabled" : "🔴 disabled"}`,
220
- `Blocked tools: ${config.blocked_tools.join(", ")}`,
221
- `Reader subagent: ${config.reader_subagent_name}`,
240
+ "Read delegation: " +
241
+ (config.enabled ? "🟢 enabled" : "🔴 disabled"),
242
+ "Reader subagent: " + config.reader_subagent_name,
243
+ "Dependencies: " + (depsReady ? "✅ ready" : "❌ missing"),
244
+ "Blocked tools: " + config.blocked_tools.join(", "),
222
245
  ];
223
246
  ctx.ui.notify(lines.join("\n"), "info");
224
247
  return;
@@ -226,11 +249,9 @@ async function default_1(pi) {
226
249
  }
227
250
  },
228
251
  });
229
- // --- 5. Ensure reader.md template ---------------------------------------
252
+ // -----------------------------------------------------------------------
253
+ // 5. Background: ensure reader.md template
254
+ // -----------------------------------------------------------------------
230
255
  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}`);
234
- });
235
256
  }
236
257
  //# 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.5",
3
+ "version": "1.0.7",
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",
@@ -24,14 +24,17 @@ export declare class ReaderError extends Error {
24
24
  /**
25
25
  * Verify that pi-subagents is installed as a Pi extension.
26
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`).
27
+ * - Uses `require.resolve('pi-subagents')` to check installation.
28
+ * - If not installed, prompts the user to install via `pi install pi-subagents`.
29
+ * - If the user declines or installation fails, returns false (caller disables the extension).
30
30
  *
31
- * @returns true if installed or successfully installed
32
- * @throws if the user declines or installation fails
31
+ * @returns true if installed or successfully installed; false otherwise
33
32
  */
34
33
  export declare function checkDependencies(prompt: (message: string) => Promise<string>): Promise<boolean>;
34
+ /**
35
+ * Quick synchronous check: can we resolve pi-subagents?
36
+ */
37
+ export declare function isSubagentsInstalled(): boolean;
35
38
  /**
36
39
  * Ensure the reader.md subagent template exists.
37
40
  * If not, copy the bundled template from `templates/reader.md`.
package/reader-manager.js CHANGED
@@ -44,6 +44,7 @@ var __importStar = (this && this.__importStar) || (function () {
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.ReaderError = void 0;
46
46
  exports.checkDependencies = checkDependencies;
47
+ exports.isSubagentsInstalled = isSubagentsInstalled;
47
48
  exports.ensureReaderTemplate = ensureReaderTemplate;
48
49
  exports.callReader = callReader;
49
50
  exports.handleReaderError = handleReaderError;
@@ -76,28 +77,31 @@ const READER_TEMPLATE_PATH = expandTilde("~/.pi/agent/agents/reader.md");
76
77
  /**
77
78
  * Verify that pi-subagents is installed as a Pi extension.
78
79
  *
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`).
80
+ * - Uses `require.resolve('pi-subagents')` to check installation.
81
+ * - If not installed, prompts the user to install via `pi install pi-subagents`.
82
+ * - If the user declines or installation fails, returns false (caller disables the extension).
82
83
  *
83
- * @returns true if installed or successfully installed
84
- * @throws if the user declines or installation fails
84
+ * @returns true if installed or successfully installed; false otherwise
85
85
  */
86
86
  async function checkDependencies(prompt) {
87
- // Try to resolve pi-subagents to verify it's installed
88
- if (isSubagentsInstalled()) {
87
+ // Already installed nothing to do
88
+ try {
89
+ require.resolve("pi-subagents");
89
90
  return true;
90
91
  }
92
+ catch {
93
+ // MODULE_NOT_FOUND — prompt the user
94
+ }
91
95
  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]");
96
+ const answer = await prompt("pi-subagents is not installed. Install it now? [Y/n]");
93
97
  const normalized = answer.trim().toLowerCase();
94
98
  if (normalized !== "" && normalized !== "y" && normalized !== "yes") {
95
- throw new Error("pi-subagents is required. Please install it manually: pi install pi-subagents");
99
+ console.error("[pi-read-delegator] Cannot proceed without pi-subagents. Extension disabled.");
100
+ return false;
96
101
  }
97
102
  // Attempt installation
98
103
  console.log("[pi-read-delegator] 📦 Installing pi-subagents…");
99
104
  try {
100
- // Try `pi install pi-subagents` first (the Pi package manager)
101
105
  (0, child_process_1.execSync)("pi install pi-subagents", {
102
106
  stdio: "pipe",
103
107
  timeout: 60_000,
@@ -105,7 +109,7 @@ async function checkDependencies(prompt) {
105
109
  });
106
110
  console.log("[pi-read-delegator] ✅ pi-subagents installed via pi.");
107
111
  }
108
- catch {
112
+ catch (firstErr) {
109
113
  // Fallback to global npm install
110
114
  try {
111
115
  (0, child_process_1.execSync)("npm install -g pi-subagents", {
@@ -115,24 +119,28 @@ async function checkDependencies(prompt) {
115
119
  });
116
120
  console.log("[pi-read-delegator] ✅ pi-subagents installed via npm.");
117
121
  }
118
- catch (err) {
119
- throw new Error("❌ Installation failed. Please install manually: npm install -g pi-subagents");
122
+ catch (secondErr) {
123
+ console.error("[pi-read-delegator] Cannot proceed without pi-subagents. Extension disabled.");
124
+ return false;
120
125
  }
121
126
  }
122
127
  // Verify installation took effect
123
- if (!isSubagentsInstalled()) {
124
- throw new Error("pi-subagents installed but cannot be found. Restart Pi and try again.");
128
+ try {
129
+ require.resolve("pi-subagents");
130
+ return true;
131
+ }
132
+ catch {
133
+ console.error("[pi-read-delegator] ❌ pi-subagents installed but cannot be found. Restart Pi and try again.");
134
+ return false;
125
135
  }
126
- return true;
127
136
  }
128
137
  /**
129
- * Simple check: can we require/import pi-subagents?
138
+ * Quick synchronous check: can we resolve pi-subagents?
130
139
  */
131
140
  function isSubagentsInstalled() {
132
141
  try {
133
- // Dynamic require that works even if TypeScript doesn't know the module
134
- const mod = require("pi-subagents");
135
- return mod !== undefined;
142
+ require.resolve("pi-subagents");
143
+ return true;
136
144
  }
137
145
  catch {
138
146
  return false;
package/ui.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * Detects the user's language preference (config → Pi settings → OS → "en")
5
5
  * and provides localized messages. Manages a status bar indicator.
6
6
  */
7
- export type Status = "active" | "idle" | "error";
7
+ export type Status = "active" | "idle" | "error" | "off";
8
8
  /** Minimal Pi agent interface for the status bar API. */
9
9
  export interface AgentWithStatusBar {
10
10
  /** Set text to display in the Pi status bar. */
package/ui.js CHANGED
@@ -84,17 +84,25 @@ const messages = {
84
84
  tr: "❌ Kurulum başarısız. Elle yükleyin.",
85
85
  en: "❌ Installation failed. Install manually.",
86
86
  },
87
+ deps_disabled: {
88
+ tr: "pi-subagents olmadan devam edilemez. Eklenti devre dışı.",
89
+ en: "Cannot proceed without pi-subagents. Extension disabled.",
90
+ },
87
91
  status_active: {
88
- tr: " aktif",
89
- en: " active",
92
+ tr: "🟢 reader",
93
+ en: "🟢 reader",
90
94
  },
91
95
  status_idle: {
92
- tr: "○ boşta",
93
- en: "○ idle",
96
+ tr: "○ reader",
97
+ en: "○ reader",
94
98
  },
95
99
  status_error: {
96
- tr: "⚠ hata",
97
- en: "⚠ error",
100
+ tr: "⚠ reader",
101
+ en: "⚠ reader",
102
+ },
103
+ status_off: {
104
+ tr: "🔴 reader",
105
+ en: "🔴 reader",
98
106
  },
99
107
  blocked: {
100
108
  tr: "🚫 Engellendi: ",
@@ -217,12 +225,15 @@ function updateStatusBar(status) {
217
225
  case "error":
218
226
  text = msg("status_error");
219
227
  break;
228
+ case "off":
229
+ text = msg("status_off");
230
+ break;
220
231
  default:
221
232
  text = msg("status_idle");
222
233
  break;
223
234
  }
224
235
  try {
225
- statusBarAgent.setStatusBarText(`pi-read-delegator: ${text}`);
236
+ statusBarAgent.setStatusBarText(text);
226
237
  }
227
238
  catch {
228
239
  // Graceful fallback — not all Pi versions support status bar