@trim21/personal-pi-extensions 0.0.170 → 0.0.172
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/package.json +2 -2
- package/src/spawn-agent.ts +157 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trim21/personal-pi-extensions",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.172",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
|
|
6
6
|
"keywords": [
|
|
@@ -75,5 +75,5 @@
|
|
|
75
75
|
"prettier --write"
|
|
76
76
|
]
|
|
77
77
|
},
|
|
78
|
-
"packageManager": "pnpm@11.
|
|
78
|
+
"packageManager": "pnpm@11.21.0"
|
|
79
79
|
}
|
package/src/spawn-agent.ts
CHANGED
|
@@ -29,7 +29,10 @@ import type { AgentMessage, AgentToolResult } from "@earendil-works/pi-agent-cor
|
|
|
29
29
|
import {
|
|
30
30
|
type AgentSessionEvent,
|
|
31
31
|
type ExtensionAPI,
|
|
32
|
+
type ExtensionUIContext,
|
|
32
33
|
getMarkdownTheme,
|
|
34
|
+
type RpcExtensionUIRequest,
|
|
35
|
+
type RpcExtensionUIResponse,
|
|
33
36
|
truncateTail,
|
|
34
37
|
withFileMutationQueue,
|
|
35
38
|
} from "@earendil-works/pi-coding-agent";
|
|
@@ -176,18 +179,18 @@ async function writePromptToTempFile(agentName: string, prompt: string): Promise
|
|
|
176
179
|
|
|
177
180
|
export function buildSubagentArgs(
|
|
178
181
|
agent: AgentConfig,
|
|
179
|
-
|
|
182
|
+
_task: string,
|
|
180
183
|
systemPromptPath: string | undefined,
|
|
181
184
|
): string[] {
|
|
182
|
-
//
|
|
183
|
-
// --no-session
|
|
185
|
+
// RPC mode emits agent and extension UI events as JSON lines and accepts
|
|
186
|
+
// dialog responses over stdin. --no-session keeps the child ephemeral.
|
|
187
|
+
// --no-extensions disables
|
|
184
188
|
// extension discovery; only the extensions explicitly loaded below (the
|
|
185
189
|
// unconditional guards plus per-tool overrides) run inside the subagent.
|
|
186
|
-
const args: string[] = ["--mode", "
|
|
190
|
+
const args: string[] = ["--mode", "rpc", "--no-session", "--no-extensions"];
|
|
187
191
|
|
|
188
192
|
// Protection layers that must be present in every subagent regardless of
|
|
189
|
-
// its declared toolset: the workspace write guard and the bwrap sandbox
|
|
190
|
-
// (forced read-only for subagents via PI_SUBAGENT_CHILD=1).
|
|
193
|
+
// its declared toolset: the workspace write guard and the bwrap sandbox.
|
|
191
194
|
for (const ext of UNCONDITIONAL_EXTENSIONS) {
|
|
192
195
|
args.push("-e", extensionPath(ext));
|
|
193
196
|
}
|
|
@@ -210,7 +213,6 @@ export function buildSubagentArgs(
|
|
|
210
213
|
}
|
|
211
214
|
args.push("--tools", tools.join(","));
|
|
212
215
|
if (systemPromptPath) args.push("--append-system-prompt", systemPromptPath);
|
|
213
|
-
args.push(`Task: ${task}`);
|
|
214
216
|
return args;
|
|
215
217
|
}
|
|
216
218
|
|
|
@@ -218,12 +220,86 @@ export function buildSubagentArgs(
|
|
|
218
220
|
|
|
219
221
|
type OnUpdateCallback = (partial: AgentToolResult<SubagentDetails>) => void;
|
|
220
222
|
|
|
223
|
+
function dialogOptions(signal: AbortSignal | undefined, timeout: number | undefined) {
|
|
224
|
+
return {
|
|
225
|
+
...(signal && { signal }),
|
|
226
|
+
...(timeout !== undefined && { timeout }),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Forward one RPC extension UI request to the parent session. */
|
|
231
|
+
export async function forwardSubagentUIRequest(
|
|
232
|
+
request: RpcExtensionUIRequest,
|
|
233
|
+
ui: ExtensionUIContext,
|
|
234
|
+
signal?: AbortSignal,
|
|
235
|
+
): Promise<RpcExtensionUIResponse | undefined> {
|
|
236
|
+
switch (request.method) {
|
|
237
|
+
case "select": {
|
|
238
|
+
const value = await ui.select(
|
|
239
|
+
request.title,
|
|
240
|
+
request.options,
|
|
241
|
+
dialogOptions(signal, request.timeout),
|
|
242
|
+
);
|
|
243
|
+
return value === undefined
|
|
244
|
+
? { type: "extension_ui_response", id: request.id, cancelled: true }
|
|
245
|
+
: { type: "extension_ui_response", id: request.id, value };
|
|
246
|
+
}
|
|
247
|
+
case "confirm": {
|
|
248
|
+
const confirmed = await ui.confirm(
|
|
249
|
+
request.title,
|
|
250
|
+
request.message,
|
|
251
|
+
dialogOptions(signal, request.timeout),
|
|
252
|
+
);
|
|
253
|
+
return { type: "extension_ui_response", id: request.id, confirmed };
|
|
254
|
+
}
|
|
255
|
+
case "input": {
|
|
256
|
+
const value = await ui.input(
|
|
257
|
+
request.title,
|
|
258
|
+
request.placeholder,
|
|
259
|
+
dialogOptions(signal, request.timeout),
|
|
260
|
+
);
|
|
261
|
+
return value === undefined
|
|
262
|
+
? { type: "extension_ui_response", id: request.id, cancelled: true }
|
|
263
|
+
: { type: "extension_ui_response", id: request.id, value };
|
|
264
|
+
}
|
|
265
|
+
case "editor": {
|
|
266
|
+
const value = await ui.editor(request.title, request.prefill);
|
|
267
|
+
return value === undefined
|
|
268
|
+
? { type: "extension_ui_response", id: request.id, cancelled: true }
|
|
269
|
+
: { type: "extension_ui_response", id: request.id, value };
|
|
270
|
+
}
|
|
271
|
+
case "notify": {
|
|
272
|
+
ui.notify(request.message, request.notifyType);
|
|
273
|
+
return undefined;
|
|
274
|
+
}
|
|
275
|
+
case "setStatus": {
|
|
276
|
+
ui.setStatus(request.statusKey, request.statusText);
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
case "setWidget": {
|
|
280
|
+
ui.setWidget(request.widgetKey, request.widgetLines, {
|
|
281
|
+
placement: request.widgetPlacement,
|
|
282
|
+
});
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
case "setTitle": {
|
|
286
|
+
ui.setTitle(request.title);
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
case "set_editor_text": {
|
|
290
|
+
ui.setEditorText(request.text);
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
221
296
|
export async function runAgent(
|
|
222
297
|
agent: AgentConfig,
|
|
223
298
|
task: string,
|
|
224
299
|
cwd: string,
|
|
225
300
|
signal: AbortSignal | undefined,
|
|
226
301
|
onUpdate: OnUpdateCallback | undefined,
|
|
302
|
+
parentUI?: ExtensionUIContext,
|
|
227
303
|
): Promise<SubagentDetails> {
|
|
228
304
|
const result: SubagentDetails = {
|
|
229
305
|
agent: agent.name,
|
|
@@ -253,10 +329,8 @@ export async function runAgent(
|
|
|
253
329
|
const proc = spawn(invocation.command, invocation.args, {
|
|
254
330
|
cwd,
|
|
255
331
|
shell: false,
|
|
256
|
-
stdio: ["
|
|
257
|
-
|
|
258
|
-
// bwrap's subagent policy) can recognize and treat it accordingly.
|
|
259
|
-
env: { ...process.env, PI_SUBAGENT_CHILD: "1" },
|
|
332
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
333
|
+
env: { ...process.env },
|
|
260
334
|
});
|
|
261
335
|
|
|
262
336
|
let logLines: string[] = [];
|
|
@@ -281,10 +355,57 @@ export async function runAgent(
|
|
|
281
355
|
|
|
282
356
|
let buffer = "";
|
|
283
357
|
|
|
358
|
+
const sendRpc = (message: object) => {
|
|
359
|
+
proc.stdin.write(`${JSON.stringify(message)}\n`);
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
let requestedShutdown = false;
|
|
363
|
+
const requestShutdown = () => {
|
|
364
|
+
if (requestedShutdown) return;
|
|
365
|
+
requestedShutdown = true;
|
|
366
|
+
proc.stdin.end();
|
|
367
|
+
};
|
|
368
|
+
|
|
284
369
|
const processLine = (line: string) => {
|
|
285
370
|
if (!line.trim()) return;
|
|
286
|
-
const
|
|
287
|
-
if (!
|
|
371
|
+
const record = parseJsonRecord(line);
|
|
372
|
+
if (!record) return;
|
|
373
|
+
|
|
374
|
+
if (record.type === "extension_ui_request") {
|
|
375
|
+
const request = record as RpcExtensionUIRequest;
|
|
376
|
+
if (!parentUI) {
|
|
377
|
+
if (
|
|
378
|
+
request.method === "select" ||
|
|
379
|
+
request.method === "confirm" ||
|
|
380
|
+
request.method === "input" ||
|
|
381
|
+
request.method === "editor"
|
|
382
|
+
) {
|
|
383
|
+
sendRpc({ type: "extension_ui_response", id: request.id, cancelled: true });
|
|
384
|
+
}
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
void forwardSubagentUIRequest(request, parentUI, signal)
|
|
388
|
+
.then((response) => {
|
|
389
|
+
if (response) sendRpc(response);
|
|
390
|
+
return;
|
|
391
|
+
})
|
|
392
|
+
.catch(() => {
|
|
393
|
+
sendRpc({ type: "extension_ui_response", id: request.id, cancelled: true });
|
|
394
|
+
});
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (record.type === "response") {
|
|
399
|
+
if (record.command === "prompt" && record.success === false) {
|
|
400
|
+
result.errorMessage =
|
|
401
|
+
typeof record.error === "string" ? record.error : "Subagent prompt was rejected";
|
|
402
|
+
result.stopReason = "error";
|
|
403
|
+
requestShutdown();
|
|
404
|
+
}
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const event = record as AgentSessionEvent;
|
|
288
409
|
|
|
289
410
|
switch (event.type) {
|
|
290
411
|
case "message_update": {
|
|
@@ -323,6 +444,10 @@ export async function runAgent(
|
|
|
323
444
|
|
|
324
445
|
break;
|
|
325
446
|
}
|
|
447
|
+
case "agent_settled": {
|
|
448
|
+
requestShutdown();
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
326
451
|
// No default
|
|
327
452
|
}
|
|
328
453
|
};
|
|
@@ -338,6 +463,12 @@ export async function runAgent(
|
|
|
338
463
|
result.stderr += data.toString();
|
|
339
464
|
});
|
|
340
465
|
|
|
466
|
+
proc.stdin.on("error", (error) => {
|
|
467
|
+
if (!requestedShutdown) result.stderr += error.message;
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
sendRpc({ type: "prompt", message: `Task: ${task}` });
|
|
471
|
+
|
|
341
472
|
const exitCode = await new Promise<number>((resolve) => {
|
|
342
473
|
proc.on("close", (code) => {
|
|
343
474
|
if (buffer.trim()) processLine(buffer);
|
|
@@ -372,12 +503,10 @@ export async function runAgent(
|
|
|
372
503
|
}
|
|
373
504
|
|
|
374
505
|
/**
|
|
375
|
-
* Parse one line of the subagent's
|
|
376
|
-
*
|
|
377
|
-
* rejected. The cast here is the single trust boundary: downstream branches
|
|
378
|
-
* are fully type-narrowed via the `AgentSessionEvent` discriminated union.
|
|
506
|
+
* Parse one line of the subagent's RPC stream into a JSON object. Non-JSON
|
|
507
|
+
* lines and records without a type discriminator are rejected.
|
|
379
508
|
*/
|
|
380
|
-
function
|
|
509
|
+
function parseJsonRecord(line: string): Record<string, unknown> | null {
|
|
381
510
|
let raw: unknown;
|
|
382
511
|
try {
|
|
383
512
|
raw = JSON.parse(line);
|
|
@@ -386,7 +515,7 @@ function parseJsonEvent(line: string): AgentSessionEvent | null {
|
|
|
386
515
|
}
|
|
387
516
|
if (typeof raw !== "object" || raw === null) return null;
|
|
388
517
|
if (typeof (raw as Record<string, unknown>).type !== "string") return null;
|
|
389
|
-
return raw as
|
|
518
|
+
return raw as Record<string, unknown>;
|
|
390
519
|
}
|
|
391
520
|
|
|
392
521
|
/** Session entry customType used to mark the injected subagent list. */
|
|
@@ -395,7 +524,7 @@ export function formatAgentListSection(agents: AgentConfig[]): string {
|
|
|
395
524
|
return [
|
|
396
525
|
"## Available subagents",
|
|
397
526
|
"",
|
|
398
|
-
"You can delegate tasks to the following subagent types by calling the `
|
|
527
|
+
"You can delegate tasks to the following subagent types by calling the `spawn-agent` tool with their name in the `agent` parameter:",
|
|
399
528
|
"",
|
|
400
529
|
...lines,
|
|
401
530
|
].join("\n");
|
|
@@ -460,7 +589,14 @@ export default function spawnAgent(pi: ExtensionAPI) {
|
|
|
460
589
|
};
|
|
461
590
|
}
|
|
462
591
|
|
|
463
|
-
const result = await runAgent(
|
|
592
|
+
const result = await runAgent(
|
|
593
|
+
agent,
|
|
594
|
+
params.task,
|
|
595
|
+
ctx.cwd,
|
|
596
|
+
signal,
|
|
597
|
+
onUpdate,
|
|
598
|
+
ctx.hasUI ? ctx.ui : undefined,
|
|
599
|
+
);
|
|
464
600
|
|
|
465
601
|
const isError =
|
|
466
602
|
result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
|