@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/README.md +178 -69
- package/{bash-filter.d.ts → dist/bash-filter.d.ts} +16 -6
- package/dist/bash-filter.js +420 -0
- package/{config.d.ts → dist/config.d.ts} +1 -1
- package/{config.js → dist/config.js} +38 -58
- package/dist/index.js +295 -0
- package/{reader-manager.d.ts → dist/reader-manager.d.ts} +26 -1
- package/{reader-manager.js → dist/reader-manager.js} +159 -69
- package/{tool-blocker.js → dist/tool-blocker.js} +13 -20
- package/{ui.js → dist/ui.js} +15 -62
- package/package.json +20 -17
- package/bash-filter.js +0 -242
- package/index.js +0 -278
- package/templates/reader.md +0 -8
- /package/{index.d.ts → dist/index.d.ts} +0 -0
- /package/{tool-blocker.d.ts → dist/tool-blocker.d.ts} +0 -0
- /package/{ui.d.ts → dist/ui.d.ts} +0 -0
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
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
199
|
+
rawLog("📦 Installing pi-subagents to " + piNpmDir + "…");
|
|
109
200
|
onProgress?.("installing");
|
|
110
201
|
try {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
timeout: 60_000,
|
|
114
|
-
encoding: "utf-8",
|
|
202
|
+
await execAsync(`npm install --prefix "${piNpmDir}" pi-subagents`, {
|
|
203
|
+
timeout: 120_000,
|
|
115
204
|
});
|
|
116
|
-
|
|
205
|
+
rawLog("✅ pi-subagents installed via npm.");
|
|
206
|
+
registerPiSubagentsPackage();
|
|
117
207
|
onProgress?.("done");
|
|
118
208
|
}
|
|
119
209
|
catch (firstErr) {
|
|
120
210
|
onProgress?.("failed");
|
|
121
|
-
|
|
211
|
+
rawError("⚠️ npm install failed: " +
|
|
122
212
|
(firstErr instanceof Error ? firstErr.message : String(firstErr)));
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
158
|
-
|
|
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
|
-
|
|
255
|
+
rawLog(`✅ Created reader subagent template: ${READER_TEMPLATE_PATH}`);
|
|
166
256
|
return true;
|
|
167
257
|
}
|
|
168
258
|
catch (err) {
|
|
169
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|