@stfade/pi-read-delegator 1.0.11 → 1.0.13

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/dist/index.js ADDED
@@ -0,0 +1,295 @@
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
+ */
13
+ import * as fs from "node:fs";
14
+ import * as os from "node:os";
15
+ import * as path from "node:path";
16
+ import { loadConfig, saveConfig } from "./config";
17
+ import { checkDependencies, isSubagentsInstalled, sessionCache, } from "./reader-manager";
18
+ import { getLanguage, msg } from "./ui";
19
+ import { isReadCommand } from "./bash-filter";
20
+ // ---------------------------------------------------------------------------
21
+ // Reader template path
22
+ // ---------------------------------------------------------------------------
23
+ function readerPath() {
24
+ return path.join(os.homedir(), ".pi", "agent", "agents", "reader.md");
25
+ }
26
+ function syncReaderTemplate(model) {
27
+ const rp = readerPath();
28
+ try {
29
+ const dir = path.dirname(rp);
30
+ fs.mkdirSync(dir, { recursive: true });
31
+ const content = [
32
+ "---",
33
+ "name: reader",
34
+ "description: Compact code-reader — executes tasks, returns results with line numbers",
35
+ "tools: read, grep, find, ls",
36
+ `model: ${model}`,
37
+ "---",
38
+ "",
39
+ "Execute the task. Return only the result, nothing else.",
40
+ "Always include line numbers for grep and read results.",
41
+ "No explanations, summaries, or conversational text.",
42
+ "",
43
+ "### Structure masks (auto-apply to code output)",
44
+ "- Skip import statements unless task explicitly mentions them.",
45
+ "- Collapse long type annotations: `Record<string, string>[]` → `Record<...>[]`.",
46
+ "- Truncate paths: `C:/Users/samet/Documents/Projects/pi-read-delegator/src/` → `src/`.",
47
+ "",
48
+ "### Stats-first for grep / find",
49
+ "- grep: show total match count on line 1, then matches. Large result sets (>20): only count.",
50
+ "- find: show file count first, then list. >50 files: only count.",
51
+ "- ls: show file count first, then list.",
52
+ "",
53
+ "### Smart filtering",
54
+ "- Skip node_modules, .git, dist, .next, coverage, __pycache__ unless task specifies a path inside.",
55
+ "- Skip binary files (images, .exe, .dll, .zip, .db) — return '(binary)'.",
56
+ "- Deduplicate: if same file appears in multiple grep matches, show it once with all line numbers.",
57
+ "",
58
+ "### Output format (no markdown headers)",
59
+ "grep: src/file.ts:42 matched line",
60
+ "read: 42: line content",
61
+ "find: file list, one per line",
62
+ "ls: name size",
63
+ "No matches: (no matches)",
64
+ "Error: Error: <message>",
65
+ ].join("\n");
66
+ fs.writeFileSync(rp, content, "utf8");
67
+ }
68
+ catch {
69
+ // read-only home directory — template sync is best-effort
70
+ }
71
+ }
72
+ // ---------------------------------------------------------------------------
73
+ // Model picker
74
+ // ---------------------------------------------------------------------------
75
+ /**
76
+ * Interactive model picker using Pi's model registry.
77
+ * Shows a select UI with all configured models and updates config + reader.md
78
+ * when the user picks a new model.
79
+ *
80
+ * Returns the selected model string ("provider/model") or undefined if the
81
+ * picker is unavailable or the user cancels.
82
+ */
83
+ async function pickReaderModel(config, ctx) {
84
+ try {
85
+ const models = ctx.modelRegistry?.getAvailable?.() ?? [];
86
+ if (models.length === 0)
87
+ return undefined;
88
+ const options = models.map((m) => `${m.provider ?? "?"}/${m.id}`);
89
+ if (!options.includes(config.reader_model)) {
90
+ options.unshift(config.reader_model);
91
+ }
92
+ const selected = await ctx.ui.select("Choose reader model (ESC to keep current)", options);
93
+ if (selected && selected !== config.reader_model) {
94
+ config.reader_model = selected;
95
+ saveConfig(config, { silent: true });
96
+ syncReaderTemplate(selected);
97
+ ctx.ui.notify("✅ Reader model set to: " + selected, "info");
98
+ return selected;
99
+ }
100
+ }
101
+ catch {
102
+ // modelRegistry or ui.select not available — fallback
103
+ }
104
+ return undefined;
105
+ }
106
+ // ---------------------------------------------------------------------------
107
+ // Tool helpers
108
+ // ---------------------------------------------------------------------------
109
+ /** Determine which tools stay active after blocking read tools.
110
+ * Always keeps "subagent" — the bridge to the reader. */
111
+ function computeActiveTools(pi, blocked) {
112
+ const all = pi.getAllTools();
113
+ const blockedSet = new Set(blocked);
114
+ const forceKeep = new Set(["subagent"]);
115
+ return all
116
+ .map((t) => t.name)
117
+ .filter((name) => forceKeep.has(name) || !blockedSet.has(name));
118
+ }
119
+ // ---------------------------------------------------------------------------
120
+ // Dependency check helpers — extracted to keep session_start handler lean
121
+ // ---------------------------------------------------------------------------
122
+ function createProgressCallback(ctx) {
123
+ return (status) => {
124
+ if (status === "installing") {
125
+ ctx.ui.setStatus("read-delegator", "⏳ Installing pi-subagents…");
126
+ ctx.ui.notify(msg("deps_installing") +
127
+ " This may take up to 60 seconds. Please wait…", "info");
128
+ }
129
+ else if (status === "done") {
130
+ ctx.ui.setStatus("read-delegator", msg("status_active"));
131
+ ctx.ui.notify("✅ pi-subagents installed successfully.", "info");
132
+ }
133
+ else if (status === "failed") {
134
+ ctx.ui.setStatus("read-delegator", msg("status_error"));
135
+ }
136
+ };
137
+ }
138
+ async function performDependencyCheck(ctx, config) {
139
+ const promptFn = async (message) => {
140
+ const ok = await ctx.ui.confirm("pi-subagents required", message);
141
+ return ok ? "y" : "n";
142
+ };
143
+ const ready = await checkDependencies(promptFn, createProgressCallback(ctx));
144
+ if (!ready) {
145
+ config.enabled = false;
146
+ saveConfig(config, { silent: true });
147
+ ctx.ui.setStatus("read-delegator", msg("status_error"));
148
+ ctx.ui.notify(msg("deps_disabled"), "warning");
149
+ return false;
150
+ }
151
+ if (!config.enabled) {
152
+ config.enabled = true;
153
+ saveConfig(config, { silent: true });
154
+ }
155
+ return true;
156
+ }
157
+ // ---------------------------------------------------------------------------
158
+ // Extension factory — registration only; actions go inside events
159
+ // ---------------------------------------------------------------------------
160
+ export default async function (pi) {
161
+ const config = loadConfig();
162
+ // Detect language
163
+ getLanguage(config.language);
164
+ // Quick sync dependency check — interactive prompt is deferred to
165
+ // session_start where we have access to ctx.ui.confirm().
166
+ let depsReady = isSubagentsInstalled();
167
+ let depsChecked = depsReady; // if already ready, no need to check again
168
+ // -----------------------------------------------------------------------
169
+ // 1. session_start: dependency check → tool blocking → status bar
170
+ // -----------------------------------------------------------------------
171
+ pi.on("session_start", async (_event, ctx) => {
172
+ // --- Dependency check ---
173
+ if (!depsChecked) {
174
+ depsReady = await performDependencyCheck(ctx, config);
175
+ depsChecked = true;
176
+ if (!depsReady)
177
+ return;
178
+ // First install: pick reader model from available models
179
+ await pickReaderModel(config, ctx);
180
+ }
181
+ // --- Tool blocking ---
182
+ if (config.enabled) {
183
+ pi.setActiveTools(computeActiveTools(pi, config.blocked_tools));
184
+ }
185
+ ctx.ui.setStatus("read-delegator", config.enabled ? msg("status_active") : msg("status_off"));
186
+ });
187
+ // -----------------------------------------------------------------------
188
+ // 2. before_agent_start: inject orchestrator system prompt
189
+ // -----------------------------------------------------------------------
190
+ pi.on("before_agent_start", (event, _ctx) => {
191
+ if (!config.enabled)
192
+ return;
193
+ return {
194
+ systemPrompt: event.systemPrompt + "\n\n" + config.orchestrator_prompt,
195
+ };
196
+ });
197
+ // -----------------------------------------------------------------------
198
+ // 3. tool_call: intercept bash read commands
199
+ // -----------------------------------------------------------------------
200
+ pi.on("tool_call", (event, _ctx) => {
201
+ if (!config.enabled)
202
+ return;
203
+ if (event.toolName === "bash" || event.toolName === "shell") {
204
+ const command = String(event.input?.command ?? "");
205
+ if (!command)
206
+ return;
207
+ if (isReadCommand(command)) {
208
+ return {
209
+ block: true,
210
+ reason: [
211
+ 'Use subagent(agent: "' +
212
+ config.reader_subagent_name +
213
+ '", task: "Execute and summarize: ' +
214
+ command +
215
+ '")',
216
+ "instead of running file-reading commands directly.",
217
+ ].join(" "),
218
+ };
219
+ }
220
+ }
221
+ });
222
+ // -----------------------------------------------------------------------
223
+ // 4. Commands
224
+ // -----------------------------------------------------------------------
225
+ // Shared status helper
226
+ const showStatus = (ctx) => {
227
+ const s = sessionCache.stats();
228
+ const lines = [
229
+ "Read delegation: " + (config.enabled ? "🟢 enabled" : "🔴 disabled"),
230
+ "Reader subagent: " + config.reader_subagent_name,
231
+ "Reader model: " + config.reader_model,
232
+ "Dependencies: " + (depsReady ? "✅ ready" : "❌ missing"),
233
+ "Blocked tools: " + config.blocked_tools.join(", "),
234
+ "Cache: " + s.files + " files (" + s.sizeKB + " KB)",
235
+ ];
236
+ ctx.ui.notify(lines.join("\n"), "info");
237
+ };
238
+ // Shortcut: enable/disable toggle with subcommand syntax (kept for back compat)
239
+ pi.registerCommand("read-delegator", {
240
+ description: "Show read-delegator status",
241
+ handler: async (_args, ctx) => {
242
+ showStatus(ctx);
243
+ },
244
+ });
245
+ pi.registerCommand("read-delegator-status", {
246
+ description: "Show read-delegator status",
247
+ handler: async (_args, ctx) => {
248
+ showStatus(ctx);
249
+ },
250
+ });
251
+ pi.registerCommand("read-delegator-on", {
252
+ description: "Enable read delegation",
253
+ handler: async (_args, ctx) => {
254
+ if (!depsReady) {
255
+ ctx.ui.notify("pi-subagents not installed. Install it first to enable read delegation.", "warning");
256
+ return;
257
+ }
258
+ config.enabled = true;
259
+ saveConfig(config, { silent: true });
260
+ pi.setActiveTools(computeActiveTools(pi, config.blocked_tools));
261
+ ctx.ui.notify(msg("enabled"), "info");
262
+ ctx.ui.setStatus("read-delegator", msg("status_active"));
263
+ },
264
+ });
265
+ pi.registerCommand("read-delegator-off", {
266
+ description: "Disable read delegation",
267
+ handler: async (_args, ctx) => {
268
+ config.enabled = false;
269
+ saveConfig(config, { silent: true });
270
+ pi.setActiveTools(pi.getAllTools().map((t) => t.name));
271
+ ctx.ui.notify(msg("disabled"), "info");
272
+ ctx.ui.setStatus("read-delegator", msg("status_off"));
273
+ },
274
+ });
275
+ pi.registerCommand("read-delegator-model", {
276
+ description: "Set or view the reader model",
277
+ handler: async (args, ctx) => {
278
+ const modelArg = args?.trim() ?? "";
279
+ if (!modelArg) {
280
+ // Interactive picker via available models
281
+ const picked = await pickReaderModel(config, ctx);
282
+ if (picked === undefined) {
283
+ }
284
+ return;
285
+ }
286
+ config.reader_model = modelArg;
287
+ saveConfig(config, { silent: true });
288
+ syncReaderTemplate(modelArg);
289
+ ctx.ui.notify("✅ Reader model set to: " + modelArg, "info");
290
+ },
291
+ });
292
+ // 5. Background: sync reader.md template from config
293
+ syncReaderTemplate(config.reader_model);
294
+ }
295
+ //# sourceMappingURL=index.js.map
@@ -9,7 +9,32 @@
9
9
  */
10
10
  import type { ReadDelegatorConfig } from "./config";
11
11
  import type { ExtensionAgent } from "./tool-blocker";
12
- /** Progress callback for dependency installation. */
12
+ export declare class SessionFileCache {
13
+ private files;
14
+ private readRanges;
15
+ /** Cache a file's content with a hash for change detection. */
16
+ set(path: string, content: string): void;
17
+ get(path: string): string | undefined;
18
+ has(path: string): boolean;
19
+ getHash(path: string): string | undefined;
20
+ /** Record that a specific line range of a path has been read. */
21
+ markReadRange(path: string, from: number, to: number): void;
22
+ getReadRanges(path: string): Array<[number, number]>;
23
+ /** Compute gaps (unread line ranges) given total line count. */
24
+ getUnreadRanges(path: string, totalLines: number): Array<[number, number]>;
25
+ /** Return cache statistics for the status command. */
26
+ stats(): {
27
+ files: number;
28
+ sizeKB: number;
29
+ ranges: number;
30
+ };
31
+ clear(): void;
32
+ }
33
+ /** Singleton cache instance shared across the extension session. */
34
+ export declare const sessionCache: SessionFileCache;
35
+ /**
36
+ * Progress callback for dependency installation.
37
+ */
13
38
  export type InstallProgress = "installing" | "done" | "failed";
14
39
  export interface AgentWithSubagent extends ExtensionAgent {
15
40
  /** Call a subagent by name with a task string. Returns the subagent's response. */
@@ -1,4 +1,3 @@
1
- "use strict";
2
1
  /**
3
2
  * reader-manager.ts — Reader subagent lifecycle and error handling
4
3
  *
@@ -8,53 +7,86 @@
8
7
  * - Call the Reader subagent with a task
9
8
  * - Handle errors with a [R]etry / [A]llow once / [C]ancel prompt
10
9
  */
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]; } };
10
+ import * as fs from "fs";
11
+ import * as path from "path";
12
+ import * as os from "os";
13
+ import { exec } from "child_process";
14
+ import { rawLog, rawWarn, rawError } from "./ui";
15
+ export class SessionFileCache {
16
+ files = new Map();
17
+ readRanges = new Map();
18
+ /** Cache a file's content with a hash for change detection. */
19
+ set(path, content) {
20
+ this.files.set(path, {
21
+ content,
22
+ hash: simpleHash(content),
23
+ timestamp: Date.now(),
24
+ sizeBytes: Buffer.byteLength(content, "utf-8"),
25
+ });
26
+ }
27
+ get(path) {
28
+ return this.files.get(path)?.content;
29
+ }
30
+ has(path) {
31
+ return this.files.has(path);
32
+ }
33
+ getHash(path) {
34
+ return this.files.get(path)?.hash;
35
+ }
36
+ /** Record that a specific line range of a path has been read. */
37
+ markReadRange(path, from, to) {
38
+ const existing = this.readRanges.get(path) ?? [];
39
+ existing.push([from, to]);
40
+ this.readRanges.set(path, existing);
41
+ }
42
+ getReadRanges(path) {
43
+ return this.readRanges.get(path) ?? [];
44
+ }
45
+ /** Compute gaps (unread line ranges) given total line count. */
46
+ getUnreadRanges(path, totalLines) {
47
+ const read = this.readRanges.get(path);
48
+ if (!read || read.length === 0)
49
+ return [[1, totalLines]];
50
+ const sorted = [...read].sort((a, b) => a[0] - b[0]);
51
+ const gaps = [];
52
+ let pos = 1;
53
+ for (const [from, to] of sorted) {
54
+ if (from > pos)
55
+ gaps.push([pos, from - 1]);
56
+ pos = Math.max(pos, to + 1);
57
+ }
58
+ if (pos <= totalLines)
59
+ gaps.push([pos, totalLines]);
60
+ return gaps;
16
61
  }
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;
62
+ /** Return cache statistics for the status command. */
63
+ stats() {
64
+ let totalSize = 0;
65
+ for (const entry of this.files.values())
66
+ totalSize += entry.sizeBytes;
67
+ return {
68
+ files: this.files.size,
69
+ sizeKB: Math.round(totalSize / 1024),
70
+ ranges: this.readRanges.size,
33
71
  };
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.isSubagentsInstalled = isSubagentsInstalled;
48
- exports.ensureReaderTemplate = ensureReaderTemplate;
49
- exports.callReader = callReader;
50
- exports.handleReaderError = handleReaderError;
51
- const fs = __importStar(require("fs"));
52
- const path = __importStar(require("path"));
53
- const os = __importStar(require("os"));
54
- const child_process_1 = require("child_process");
55
- const ui_1 = require("./ui");
72
+ }
73
+ clear() {
74
+ this.files.clear();
75
+ this.readRanges.clear();
76
+ }
77
+ }
78
+ /** Singleton cache instance shared across the extension session. */
79
+ export const sessionCache = new SessionFileCache();
80
+ /** Simple non-cryptographic hash for cache invalidation. */
81
+ function simpleHash(str) {
82
+ let h = 0;
83
+ for (let i = 0; i < str.length; i++) {
84
+ h = (Math.imul(31, h) + str.charCodeAt(i)) | 0;
85
+ }
86
+ return h.toString(36);
87
+ }
56
88
  /** Error thrown when the Reader subagent fails. */
57
- class ReaderError extends Error {
89
+ export class ReaderError extends Error {
58
90
  originalError;
59
91
  constructor(message, originalError) {
60
92
  super(message);
@@ -62,7 +94,6 @@ class ReaderError extends Error {
62
94
  this.name = "ReaderError";
63
95
  }
64
96
  }
65
- exports.ReaderError = ReaderError;
66
97
  // ---------------------------------------------------------------------------
67
98
  // Paths
68
99
  // ---------------------------------------------------------------------------
@@ -91,43 +122,102 @@ function piSubagentsDir() {
91
122
  function piSubagentsPackageJson() {
92
123
  return path.join(piSubagentsDir(), "package.json");
93
124
  }
94
- async function checkDependencies(prompt, onProgress) {
125
+ function piSettingsPath() {
126
+ return path.join(os.homedir(), ".pi", "agent", "settings.json");
127
+ }
128
+ /**
129
+ * Register pi-subagents in Pi's package list (settings.json).
130
+ *
131
+ * npm install puts the package on disk, but Pi's `pi list` command reads
132
+ * from settings.json's `packages` array. Without this step, the user
133
+ * won't see pi-subagents in their package list.
134
+ */
135
+ function registerPiSubagentsPackage() {
136
+ try {
137
+ const settingsPath = piSettingsPath();
138
+ let settings = {};
139
+ if (fs.existsSync(settingsPath)) {
140
+ const raw = fs.readFileSync(settingsPath, "utf-8");
141
+ settings = JSON.parse(raw);
142
+ }
143
+ const packages = Array.isArray(settings.packages)
144
+ ? [...settings.packages]
145
+ : [];
146
+ const pkgName = "npm:pi-subagents";
147
+ if (!packages.includes(pkgName)) {
148
+ packages.push(pkgName);
149
+ settings.packages = packages;
150
+ // Atomic write: write to temp file then rename to avoid
151
+ // corruption from concurrent writes by other extensions.
152
+ const tmpPath = settingsPath + ".tmp";
153
+ fs.writeFileSync(tmpPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
154
+ fs.renameSync(tmpPath, settingsPath);
155
+ rawLog("✅ Registered pi-subagents in settings.json package list.");
156
+ }
157
+ }
158
+ catch (err) {
159
+ rawWarn("⚠️ Failed to register pi-subagents in settings.json: " + String(err));
160
+ }
161
+ }
162
+ /**
163
+ * Async wrapper around child_process.exec.
164
+ * Returns stdout/stderr and rejects on non-zero exit or timeout.
165
+ */
166
+ function execAsync(command, options = {}) {
167
+ return new Promise((resolve, reject) => {
168
+ const child = exec(command, { cwd: options.cwd, encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
169
+ if (err) {
170
+ reject(Object.assign(err, { stdout, stderr }));
171
+ }
172
+ else {
173
+ resolve({ stdout, stderr });
174
+ }
175
+ });
176
+ if (options.timeout && options.timeout > 0) {
177
+ setTimeout(() => {
178
+ child.kill();
179
+ reject(new Error(`Command timed out after ${options.timeout}ms`));
180
+ }, options.timeout);
181
+ }
182
+ });
183
+ }
184
+ export async function checkDependencies(prompt, onProgress) {
95
185
  // Already installed — nothing to do
96
186
  if (fs.existsSync(piSubagentsPackageJson())) {
187
+ registerPiSubagentsPackage();
97
188
  return true;
98
189
  }
99
- (0, ui_1.rawWarn)("⚠️ pi-subagents is not installed.");
190
+ rawWarn("⚠️ pi-subagents is not installed.");
100
191
  const answer = await prompt("pi-subagents is not installed. Install it now? [Y/n]");
101
192
  const normalized = answer.trim().toLowerCase();
102
193
  if (normalized !== "" && normalized !== "y" && normalized !== "yes") {
103
- (0, ui_1.rawError)("❌ Cannot proceed without pi-subagents. Extension disabled.");
194
+ rawError("❌ Cannot proceed without pi-subagents. Extension disabled.");
104
195
  return false;
105
196
  }
106
197
  // Attempt installation
107
198
  const piNpmDir = path.join(os.homedir(), ".pi", "agent", "npm");
108
- (0, ui_1.rawLog)("📦 Installing pi-subagents to " + piNpmDir + "…");
199
+ rawLog("📦 Installing pi-subagents to " + piNpmDir + "…");
109
200
  onProgress?.("installing");
110
201
  try {
111
- (0, child_process_1.execSync)(`npm install --prefix "${piNpmDir}" pi-subagents`, {
112
- stdio: "pipe",
113
- timeout: 60_000,
114
- encoding: "utf-8",
202
+ await execAsync(`npm install --prefix "${piNpmDir}" pi-subagents`, {
203
+ timeout: 120_000,
115
204
  });
116
- (0, ui_1.rawLog)("✅ pi-subagents installed via npm.");
205
+ rawLog("✅ pi-subagents installed via npm.");
206
+ registerPiSubagentsPackage();
117
207
  onProgress?.("done");
118
208
  }
119
209
  catch (firstErr) {
120
210
  onProgress?.("failed");
121
- (0, ui_1.rawError)("⚠️ npm install failed: " +
211
+ rawError("⚠️ npm install failed: " +
122
212
  (firstErr instanceof Error ? firstErr.message : String(firstErr)));
123
- (0, ui_1.rawError)("❌ Cannot proceed without pi-subagents. Extension disabled.");
213
+ rawError("❌ Cannot proceed without pi-subagents. Extension disabled.");
124
214
  return false;
125
215
  }
126
216
  // Verify installation took effect
127
217
  if (fs.existsSync(piSubagentsPackageJson())) {
128
218
  return true;
129
219
  }
130
- (0, ui_1.rawError)("❌ pi-subagents installed but cannot be found at " +
220
+ rawError("❌ pi-subagents installed but cannot be found at " +
131
221
  piSubagentsDir() +
132
222
  ". Restart Pi and try again.");
133
223
  return false;
@@ -135,7 +225,7 @@ async function checkDependencies(prompt, onProgress) {
135
225
  /**
136
226
  * Quick synchronous check: does pi-subagents exist on disk?
137
227
  */
138
- function isSubagentsInstalled() {
228
+ export function isSubagentsInstalled() {
139
229
  return fs.existsSync(piSubagentsPackageJson());
140
230
  }
141
231
  // ---------------------------------------------------------------------------
@@ -147,26 +237,26 @@ function isSubagentsInstalled() {
147
237
  *
148
238
  * @returns true if the template exists after this call
149
239
  */
150
- function ensureReaderTemplate() {
240
+ export function ensureReaderTemplate() {
151
241
  if (fs.existsSync(READER_TEMPLATE_PATH)) {
152
242
  return true;
153
243
  }
154
244
  // Path to the bundled template (sibling to the compiled JS)
155
245
  const bundledPath = path.join(__dirname, "templates", "reader.md");
156
246
  if (!fs.existsSync(bundledPath)) {
157
- (0, ui_1.rawWarn)("⚠️ Bundled reader template not found at: " + bundledPath);
158
- (0, ui_1.rawWarn)("Please create ~/.pi/agent/agents/reader.md manually.");
247
+ rawWarn("⚠️ Bundled reader template not found at: " + bundledPath);
248
+ rawWarn("Please create ~/.pi/agent/agents/reader.md manually.");
159
249
  return false;
160
250
  }
161
251
  try {
162
252
  const content = fs.readFileSync(bundledPath, "utf-8");
163
253
  ensureDir(path.dirname(READER_TEMPLATE_PATH));
164
254
  fs.writeFileSync(READER_TEMPLATE_PATH, content, "utf-8");
165
- (0, ui_1.rawLog)(`✅ Created reader subagent template: ${READER_TEMPLATE_PATH}`);
255
+ rawLog(`✅ Created reader subagent template: ${READER_TEMPLATE_PATH}`);
166
256
  return true;
167
257
  }
168
258
  catch (err) {
169
- (0, ui_1.rawError)("⚠️ Failed to create reader template: " + String(err));
259
+ rawError("⚠️ Failed to create reader template: " + String(err));
170
260
  return false;
171
261
  }
172
262
  }
@@ -183,7 +273,7 @@ function ensureReaderTemplate() {
183
273
  * @returns The Reader's response text
184
274
  * @throws ReaderError on timeout, failure, or empty response
185
275
  */
186
- async function callReader(agent, config, task, timeoutMs = 30_000) {
276
+ export async function callReader(agent, config, task, timeoutMs = 30_000) {
187
277
  const result = await withTimeout(agent.callSubagent({
188
278
  name: config.reader_subagent_name,
189
279
  task,
@@ -213,21 +303,21 @@ async function callReader(agent, config, task, timeoutMs = 30_000) {
213
303
  * @returns Reader response on Retry/Allow; never returns on Cancel
214
304
  * @throws ReaderError on Cancel or repeated failure
215
305
  */
216
- async function handleReaderError(agent, config, blockedTools, error, task, prompt) {
306
+ export async function handleReaderError(agent, config, blockedTools, error, task, prompt) {
217
307
  const errMsg = error instanceof Error ? error.message : String(error);
218
- (0, ui_1.rawError)(`❌ Reader failed: ${errMsg}`);
308
+ rawError(`❌ Reader failed: ${errMsg}`);
219
309
  const answer = await prompt(`\n❌ Reader subagent failed: ${errMsg}\n` +
220
310
  `[R]etry [A]llow once (let main model do it) [C]ancel\n`);
221
311
  const choice = answer.trim().toLowerCase();
222
312
  if (choice === "r" || choice === "retry") {
223
313
  // Retry the same task
224
- (0, ui_1.rawLog)("🔄 Retrying Reader…");
314
+ rawLog("🔄 Retrying Reader…");
225
315
  return callReader(agent, config, task);
226
316
  }
227
317
  if (choice === "a" || choice === "allow" || choice === "allow once") {
228
318
  // Temporarily unblock tools, let main model execute the task,
229
319
  // then re-block.
230
- (0, ui_1.rawLog)("🔓 Allowing main model to read once…");
320
+ rawLog("🔓 Allowing main model to read once…");
231
321
  // NOTE: For "Allow once", we need the main model to perform the task.
232
322
  // However, we are inside a tool call — the main model can't run code
233
323
  // inline. We return a specially formatted string that instructs the