@dougbots/avenor-pi 0.1.0

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 ADDED
@@ -0,0 +1,159 @@
1
+ # @dougbots/avenor-pi
2
+
3
+ Pi extension that registers avenor agent runs as tools in your Pi session. When you need a sub-agent to handle a well-defined task — write code, run tests, search a codebase — avenor operates in the background while you keep your session focused.
4
+
5
+ ## Prerequisites
6
+
7
+ You need the `avenor` binary available. It is installed to `~/.botfiles/bin/avenor` by default.
8
+
9
+ ```bash
10
+ # Check it's on PATH
11
+ which avenor
12
+
13
+ # Or set explicitly
14
+ export AVENOR_BIN=~/.botfiles/bin/avenor
15
+ ```
16
+
17
+ ## Installation
18
+
19
+ ### From npm (once published)
20
+
21
+ ```bash
22
+ pi install npm:@dougbots/avenor-pi
23
+ ```
24
+
25
+ This writes to your global settings (`~/.pi/agent/settings.json`). Use `-l` for project-local installation (`.pi/settings.json`).
26
+
27
+ ### From a git repo
28
+
29
+ ```bash
30
+ pi install git:github.com/sdougbrown/avenor@v0.1.0
31
+ ```
32
+
33
+ Pi will clone the repo and load the extension from `packages/pi`.
34
+
35
+ ### Local development
36
+
37
+ **Quick test** — run pi with the extension directly:
38
+
39
+ ```bash
40
+ cd packages/pi
41
+ pi -e ./src/index.ts
42
+ ```
43
+
44
+ **Symlink for auto-discovery** — link into your global extensions directory:
45
+
46
+ ```bash
47
+ mkdir -p ~/.pi/agent/extensions/avenor
48
+ ln -sf $(pwd)/packages/pi/src/index.ts ~/.pi/agent/extensions/avenor/index.ts
49
+ ```
50
+
51
+ Pi auto-discovers `~/.pi/agent/extensions/*/index.ts`. Changes are hot-reloadable with `/reload`.
52
+
53
+ **Local path in settings** — add the built dist directly:
54
+
55
+ ```bash
56
+ # Build first
57
+ cd packages/pi && bun run build
58
+
59
+ # Add to settings
60
+ pi install /path/to/avenor/packages/pi
61
+ ```
62
+
63
+ ### Manual settings.json
64
+
65
+ If you prefer to edit settings directly:
66
+
67
+ ```json
68
+ // ~/.pi/agent/settings.json
69
+ {
70
+ "packages": ["npm:@dougbots/avenor-pi"]
71
+ }
72
+ ```
73
+
74
+ Or for a local path:
75
+
76
+ ```json
77
+ {
78
+ "packages": ["/path/to/avenor/packages/pi"]
79
+ }
80
+ ```
81
+
82
+ ## Package Structure
83
+
84
+ ```
85
+ packages/pi/
86
+ ├── package.json
87
+ ├── tsdown.config.ts
88
+ ├── src/
89
+ │ ├── index.ts # main extension (tools, commands, hooks, rendering)
90
+ │ ├── types.ts # shared types, status emoji mapping
91
+ │ └── watch.ts # EventFeedOverlay TUI component
92
+ └── dist/
93
+ └── index.js # built output (loaded by pi)
94
+ ```
95
+
96
+ The `pi` key in `package.json` declares the extension entry point:
97
+
98
+ ```json
99
+ {
100
+ "pi": {
101
+ "extensions": ["./dist/index.js"]
102
+ }
103
+ }
104
+ ```
105
+
106
+ ## Features
107
+
108
+ ### Tools
109
+
110
+ Six tools for the LLM to dispatch and manage sub-agents:
111
+
112
+ | Tool | Description |
113
+ |---|---|
114
+ | `avenor_spawn` | Dispatch an agent run (blocking or fire-and-forget) |
115
+ | `avenor_status` | Get status of a run or all runs |
116
+ | `avenor_answer_permission` | Answer a pending permission request |
117
+ | `avenor_follow_up` | Resume a completed run with a follow-up message |
118
+ | `avenor_events` | Read events from a run |
119
+ | `avenor_shutdown` | Shut down the avenor supervisor |
120
+
121
+ ### Pi-specific features
122
+
123
+ Beyond the tools, the extension integrates with Pi's TUI and event system:
124
+
125
+ - **Status widget** — persistent widget showing all active runs with status, phase, and permission state
126
+ - **Footer status** — active runs shown in the Pi footer bar
127
+ - **Live progress** — blocking `avenor_spawn` calls stream progress updates via `onUpdate`
128
+ - **Context enrichment** — active sub-agents are automatically surfaced in the system prompt via `before_agent_start`
129
+ - **Custom rendering** — tool calls and results are rendered with status emojis and color-coded output
130
+ - **Commands** — interactive commands for run management:
131
+ - `/avenor-status` — show status of all runs
132
+ - `/avenor-watch <run_id>` — open a live event feed overlay
133
+ - `/avenor-cancel <run_id>` — cancel a running sub-agent
134
+
135
+ ## Typical workflows
136
+
137
+ **Blocking (default):**
138
+ ```
139
+ 1. avenor_spawn → tool call shows live progress, blocks until done
140
+ 2. tool call returns → structured result with status + session_id
141
+ 3. avenor_events → inspect detailed output
142
+ 4. avenor_follow_up → optionally iterate
143
+ 5. avenor_shutdown → clean up when finished
144
+ ```
145
+
146
+ **Parallel / fire-and-forget (`wait=false`):**
147
+ ```
148
+ 1. avenor_spawn × N → each returns run_id immediately
149
+ 2. (status widget updates) → persistent widget shows all active runs
150
+ 3. /avenor-watch <id> → open live event feed for a specific run
151
+ 4. avenor_events → inspect output when done
152
+ ```
153
+
154
+ ## Dependencies
155
+
156
+ - **Peer:** `@earendil-works/pi-coding-agent` (Pi runtime, provided by pi)
157
+ - **Peer:** `@earendil-works/pi-tui` (TUI components, provided by pi)
158
+ - **Dependency:** `@dougbots/avenor-core` (supervisor, client, tool primitives)
159
+ - **Binary:** `avenor` must be available on `PATH` or at `AVENOR_BIN`
@@ -0,0 +1,6 @@
1
+ import { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+
3
+ //#region src/index.d.ts
4
+ declare function export_default(pi: ExtensionAPI): Promise<void>;
5
+ //#endregion
6
+ export { export_default as default };
package/dist/index.js ADDED
@@ -0,0 +1,618 @@
1
+ import { Text } from "@earendil-works/pi-tui";
2
+ import { Type } from "typebox";
3
+ import { answerPermissionTool, eventsTool, followUpTool, shutdownTool, spawnTool, statusTool } from "@dougbots/avenor-core";
4
+ //#region src/watch.ts
5
+ var EventFeedOverlay = class {
6
+ _events = [];
7
+ _runId;
8
+ _onClose;
9
+ _maxLines = 50;
10
+ constructor(runId, onClose) {
11
+ this._runId = runId;
12
+ this._onClose = onClose ?? (() => {});
13
+ }
14
+ setOnClose(fn) {
15
+ this._onClose = fn;
16
+ }
17
+ setEvents(events) {
18
+ this._events = events.slice(-this._maxLines);
19
+ }
20
+ appendEvent(event) {
21
+ this._events.push(event);
22
+ if (this._events.length > this._maxLines) this._events = this._events.slice(-this._maxLines);
23
+ }
24
+ render(width) {
25
+ const lines = [];
26
+ const header = `📡 avenor watch: ${this._runId.slice(0, 8)}… (Esc to close)`;
27
+ lines.push(header);
28
+ lines.push("─".repeat(Math.min(width, header.length)));
29
+ if (this._events.length === 0) lines.push(" (waiting for events…)");
30
+ else for (const evt of this._events) {
31
+ const ts = new Date(evt.ts).toISOString().slice(11, 19);
32
+ const type = evt.type ?? "unknown";
33
+ const extra = this.summarizeEvent(evt);
34
+ lines.push(` ${ts} ${type}${extra ? ` — ${extra}` : ""}`);
35
+ }
36
+ return lines;
37
+ }
38
+ handleInput(data) {
39
+ if (data === "\x1B") this._onClose();
40
+ }
41
+ summarizeEvent(evt) {
42
+ if (evt.type === "tool.call" || evt.type === "tool.use") return `called ${String(evt.tool ?? evt.name ?? "")}`;
43
+ if (evt.type === "agent.message_chunk") return "thinking…";
44
+ if (evt.type?.startsWith("permission.")) return "permission requested";
45
+ if (evt.type === "tool.result") {
46
+ const result = evt.result;
47
+ if (typeof result === "string" && result.length < 80) return result;
48
+ return "completed";
49
+ }
50
+ return "";
51
+ }
52
+ invalidate() {}
53
+ };
54
+ //#endregion
55
+ //#region src/types.ts
56
+ const TERMINAL_STATUSES = new Set([
57
+ "done",
58
+ "failed",
59
+ "timeout",
60
+ "killed"
61
+ ]);
62
+ const STATUS_EMOJI = {
63
+ running: "🟢",
64
+ idle: "🟡",
65
+ done: "✅",
66
+ failed: "❌",
67
+ timeout: "⏱️",
68
+ killed: "💀",
69
+ permission: "🔒"
70
+ };
71
+ function statusEmoji(status) {
72
+ return STATUS_EMOJI[status] ?? "⚪";
73
+ }
74
+ function formatRunLine(entry) {
75
+ const emoji = statusEmoji(entry.status);
76
+ const perm = entry.pendingPermission ? ` — blocked: ${entry.permissionDescription ?? "permission"}` : "";
77
+ const phase = entry.phaseLabel ? ` (${entry.phaseLabel})` : "";
78
+ return `${emoji} ${entry.label} (${entry.agent}) — ${entry.status}${phase}${perm}`;
79
+ }
80
+ //#endregion
81
+ //#region src/index.ts
82
+ const POLL_INTERVAL_MS = 3e3;
83
+ function sleep(ms) {
84
+ return new Promise((resolve) => setTimeout(resolve, ms));
85
+ }
86
+ function buildCompletionText(run, result) {
87
+ const lines = [`Sub-agent "${run.label}" finished with status **${result.status}**.`];
88
+ if (result.stop_reason && result.stop_reason.toUpperCase() !== result.status.toUpperCase()) lines.push(`Stop reason: ${result.stop_reason}`);
89
+ if (result.session_id) lines.push(`Session: \`${result.session_id}\``);
90
+ lines.push(`\nCall \`avenor_events\` with \`run_id: "${run.runId}"\` to review the output, or \`avenor_follow_up\` to iterate.`);
91
+ return lines.join("\n");
92
+ }
93
+ function summarizeEvent(evt) {
94
+ const type = String(evt.type ?? evt.event ?? "");
95
+ if (type === "tool.call" || type === "tool.use") return `called ${String(evt.tool ?? evt.name ?? type)}`;
96
+ if (type === "agent.message_chunk") return "thinking…";
97
+ if (type.startsWith("permission.")) return `permission requested`;
98
+ return type;
99
+ }
100
+ async function src_default(pi) {
101
+ const trackedRuns = /* @__PURE__ */ new Map();
102
+ let pollingActive = false;
103
+ let sessionCtx = null;
104
+ async function pollRuns() {
105
+ let raw;
106
+ try {
107
+ raw = await statusTool({});
108
+ } catch (err) {
109
+ console.error("avenor pollRuns: statusTool failed", err);
110
+ return [];
111
+ }
112
+ const results = Array.isArray(raw) ? raw : [raw];
113
+ const entries = [];
114
+ for (const r of results) {
115
+ const run = trackedRuns.get(r.run_id);
116
+ if (run) run.lastStatus = r;
117
+ entries.push({
118
+ runId: r.run_id,
119
+ label: r.label,
120
+ status: r.status,
121
+ phase: r.phase,
122
+ phaseLabel: r.phase_label,
123
+ agent: trackedRuns.get(r.run_id)?.agent ?? "unknown",
124
+ pendingPermission: !!r.pending_permission,
125
+ permissionDescription: r.pending_permission?.description
126
+ });
127
+ }
128
+ return entries;
129
+ }
130
+ function renderStatusLines(entries) {
131
+ const active = entries.filter((e) => !TERMINAL_STATUSES.has(e.status));
132
+ if (active.length === 0) return [];
133
+ return active.map((e) => {
134
+ const emoji = statusEmoji(e.status);
135
+ const perm = e.pendingPermission ? ` 🔒` : "";
136
+ const phase = e.phaseLabel ? ` [${e.phaseLabel}]` : "";
137
+ return `${emoji} ${e.label} — ${e.status}${phase}${perm}`;
138
+ });
139
+ }
140
+ function updateWidget(entries) {
141
+ if (!sessionCtx) return;
142
+ const lines = renderStatusLines(entries);
143
+ if (lines.length > 0) {
144
+ sessionCtx.ui.setWidget("avenor-status", lines);
145
+ const parts = lines.map((l) => l.slice(0, 60));
146
+ sessionCtx.ui.setStatus("avenor", parts.join(" "));
147
+ } else {
148
+ sessionCtx.ui.setWidget("avenor-status", void 0);
149
+ sessionCtx.ui.setStatus("avenor", "");
150
+ }
151
+ }
152
+ function startPolling() {
153
+ if (pollingActive) return;
154
+ pollingActive = true;
155
+ const tick = async () => {
156
+ const prevStatuses = /* @__PURE__ */ new Map();
157
+ for (const [id, run] of trackedRuns) prevStatuses.set(id, run.lastStatus?.status);
158
+ const entries = await pollRuns();
159
+ updateWidget(entries);
160
+ for (const entry of entries) if (TERMINAL_STATUSES.has(entry.status)) {
161
+ if (trackedRuns.get(entry.runId)) {
162
+ const prevStatus = prevStatuses.get(entry.runId);
163
+ if (prevStatus && !TERMINAL_STATUSES.has(prevStatus)) sessionCtx?.ui.notify(`Sub-agent "${entry.label}" finished: ${entry.status}`, entry.status === "done" ? "info" : "warning");
164
+ trackedRuns.delete(entry.runId);
165
+ }
166
+ }
167
+ if (entries.some((e) => !TERMINAL_STATUSES.has(e.status))) setTimeout(tick, POLL_INTERVAL_MS);
168
+ else {
169
+ pollingActive = false;
170
+ sessionCtx?.ui.setStatus("avenor", "");
171
+ sessionCtx?.ui.setWidget("avenor-status", void 0);
172
+ }
173
+ };
174
+ tick();
175
+ }
176
+ pi.on("session_start", async (_event, ctx) => {
177
+ sessionCtx = ctx;
178
+ });
179
+ pi.on("session_shutdown", async (_event, _ctx) => {
180
+ pollingActive = false;
181
+ sessionCtx = null;
182
+ });
183
+ pi.on("before_agent_start", async (event, _ctx) => {
184
+ if (trackedRuns.size === 0) return;
185
+ const active = (await pollRuns()).filter((e) => !TERMINAL_STATUSES.has(e.status));
186
+ if (active.length === 0) return;
187
+ return { message: {
188
+ customType: "avenor-active-runs",
189
+ content: `Active sub-agents:\n${active.map((e) => {
190
+ const perm = e.pendingPermission ? ` [blocked: ${e.permissionDescription ?? "permission"}]` : "";
191
+ const phase = e.phaseLabel ? ` (${e.phaseLabel})` : "";
192
+ return ` • ${e.label} (${e.agent}): ${e.status}${phase}${perm}`;
193
+ }).join("\n")}`,
194
+ display: true
195
+ } };
196
+ });
197
+ pi.registerMessageRenderer("avenor-active-runs", (message, options, theme) => {
198
+ const lines = message.content.split("\n");
199
+ let text = "";
200
+ for (let i = 0; i < lines.length; i++) {
201
+ if (i === 0) text += theme.fg("info", theme.bold(lines[i]));
202
+ else text += theme.fg("muted", lines[i]);
203
+ if (i < lines.length - 1) text += "\n";
204
+ }
205
+ return new Text(text, 0, 0);
206
+ });
207
+ pi.registerTool({
208
+ name: "avenor_spawn",
209
+ label: "Avenor Spawn",
210
+ description: "Dispatch an agent run via avenor. Blocks by default, showing live progress. Set wait=false for fire-and-forget.",
211
+ promptSnippet: "Dispatch sub-agent runs via avenor (spawn, status, events, follow-up, shutdown)",
212
+ promptGuidelines: [
213
+ "Use avenor_spawn to delegate well-defined tasks to sub-agents.",
214
+ "Use avenor_status to check progress of fire-and-forget runs.",
215
+ "Use avenor_events to inspect detailed output from a run.",
216
+ "Use avenor_follow_up to iterate on a completed run.",
217
+ "Use avenor_shutdown when done delegating work."
218
+ ],
219
+ parameters: Type.Object({
220
+ agent: Type.String({ description: "Agent name (required, no default)" }),
221
+ prompt: Type.Optional(Type.String({ description: "Prompt text" })),
222
+ prompt_file: Type.Optional(Type.String({ description: "Path to prompt file" })),
223
+ dir: Type.Optional(Type.String({ description: "Working directory (defaults to session directory)" })),
224
+ label: Type.Optional(Type.String({ description: "Human-readable label for the run" })),
225
+ timeout: Type.Optional(Type.String({ description: "Timeout duration (e.g. 3600s)" })),
226
+ model: Type.Optional(Type.String({ description: "Model override" })),
227
+ backend: Type.Optional(Type.String({ description: "Backend override" })),
228
+ server_url: Type.Optional(Type.String({ description: "Backend server URL" })),
229
+ supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" })),
230
+ wait: Type.Optional(Type.Boolean({ description: "Block until complete (default true)" }))
231
+ }),
232
+ async execute(_toolCallId, params, signal, onUpdate, ctx) {
233
+ if (signal?.aborted) return { content: [{
234
+ type: "text",
235
+ text: "Cancelled"
236
+ }] };
237
+ const wait = params.wait ?? true;
238
+ const dir = params.dir ?? ctx.cwd;
239
+ const label = params.label ?? `${params.agent}-${Date.now()}`;
240
+ const result = await spawnTool({
241
+ agent: params.agent,
242
+ prompt: params.prompt,
243
+ promptFile: params.prompt_file,
244
+ label,
245
+ dir,
246
+ timeout: params.timeout,
247
+ model: params.model,
248
+ backend: params.backend,
249
+ serverUrl: params.server_url,
250
+ supervisorId: params.supervisor_id
251
+ });
252
+ trackedRuns.set(result.run_id, {
253
+ runId: result.run_id,
254
+ agent: params.agent,
255
+ label,
256
+ supervisorId: result.supervisor_id,
257
+ startTime: Date.now()
258
+ });
259
+ startPolling();
260
+ if (!wait) return {
261
+ content: [{
262
+ type: "text",
263
+ text: `Dispatched "${label}" (run_id: ${result.run_id}). Call avenor_status to check progress.`
264
+ }],
265
+ details: {
266
+ run_id: result.run_id,
267
+ label
268
+ }
269
+ };
270
+ onUpdate?.({
271
+ content: [{
272
+ type: "text",
273
+ text: `Starting ${label}…`
274
+ }],
275
+ details: {
276
+ status: "running",
277
+ run_id: result.run_id
278
+ }
279
+ });
280
+ let firstPoll = true;
281
+ while (!signal?.aborted) {
282
+ if (!firstPoll) await sleep(POLL_INTERVAL_MS);
283
+ firstPoll = false;
284
+ let raw;
285
+ try {
286
+ raw = await statusTool({
287
+ runId: result.run_id,
288
+ supervisorId: result.supervisor_id
289
+ });
290
+ } catch (err) {
291
+ console.error("avenor spawn poll: statusTool failed", err);
292
+ continue;
293
+ }
294
+ const status = Array.isArray(raw) ? raw[0] : raw;
295
+ if (!status) continue;
296
+ if (status.pending_permission) onUpdate?.({
297
+ content: [{
298
+ type: "text",
299
+ text: `[blocked] ${label} — permission: ${status.pending_permission.description}`
300
+ }],
301
+ details: {
302
+ status: "permission",
303
+ run_id: result.run_id
304
+ }
305
+ });
306
+ else {
307
+ let lastAction = "";
308
+ if (!result.supervisor_id) try {
309
+ const eventsResult = await eventsTool({
310
+ runId: result.run_id,
311
+ limit: 3
312
+ });
313
+ const last = eventsResult.events[eventsResult.events.length - 1];
314
+ if (last) lastAction = summarizeEvent(last);
315
+ } catch (err) {
316
+ console.error("avenor spawn poll: eventsTool failed", err);
317
+ }
318
+ onUpdate?.({
319
+ content: [{
320
+ type: "text",
321
+ text: `${label}${status.phase_label ? ` (${status.phase_label})` : ""}${lastAction ? ` — ${lastAction}` : ""}`
322
+ }],
323
+ details: {
324
+ status: status.status,
325
+ run_id: result.run_id,
326
+ lastAction
327
+ }
328
+ });
329
+ }
330
+ if (TERMINAL_STATUSES.has(status.status)) {
331
+ trackedRuns.delete(result.run_id);
332
+ return {
333
+ content: [{
334
+ type: "text",
335
+ text: buildCompletionText({
336
+ runId: result.run_id,
337
+ label
338
+ }, status)
339
+ }],
340
+ details: {
341
+ status: status.status,
342
+ run_id: result.run_id,
343
+ session_id: status.session_id
344
+ }
345
+ };
346
+ }
347
+ }
348
+ trackedRuns.delete(result.run_id);
349
+ return {
350
+ content: [{
351
+ type: "text",
352
+ text: `Monitoring of "${label}" (run_id: ${result.run_id}) was interrupted. Use avenor_status to check.`
353
+ }],
354
+ details: { run_id: result.run_id }
355
+ };
356
+ },
357
+ renderCall(args, theme) {
358
+ const agent = theme.fg("toolTitle", theme.bold("avenor_spawn "));
359
+ const agentName = theme.fg("accent", args.agent);
360
+ const label = args.label ? theme.fg("dim", ` "${args.label}"`) : "";
361
+ const wait = args.wait === false ? theme.fg("warning", " [async]") : "";
362
+ return new Text(agent + agentName + label + wait, 0, 0);
363
+ },
364
+ renderResult(result, { isPartial }, theme) {
365
+ if (isPartial) {
366
+ const emoji = statusEmoji(result.details?.status ?? "running");
367
+ return new Text(theme.fg("info", `${emoji} ${result.content?.[0]?.text ?? "Running…"}`), 0, 0);
368
+ }
369
+ const status = result.details?.status;
370
+ if (status && TERMINAL_STATUSES.has(status)) {
371
+ const emoji = statusEmoji(status);
372
+ const header = theme.fg(status === "done" ? "success" : "error", `${emoji} ${status}`);
373
+ const lines = (result.content?.[0]?.text ?? "").split("\n");
374
+ return new Text(header + "\n" + lines.map((l) => theme.fg("muted", ` ${l}`)).join("\n"), 0, 0);
375
+ }
376
+ return new Text((result.content?.[0]?.text ?? "").split("\n").map((l) => theme.fg("muted", l)).join("\n"), 0, 0);
377
+ }
378
+ });
379
+ pi.registerTool({
380
+ name: "avenor_status",
381
+ label: "Avenor Status",
382
+ description: "Get status of a run or all runs. Surfaces pending permission requests.",
383
+ parameters: Type.Object({
384
+ run_id: Type.Optional(Type.String({ description: "Specific run ID to query" })),
385
+ supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
386
+ }),
387
+ async execute(_toolCallId, params) {
388
+ const result = await statusTool({
389
+ runId: params.run_id,
390
+ supervisorId: params.supervisor_id
391
+ });
392
+ return {
393
+ content: [{
394
+ type: "text",
395
+ text: JSON.stringify(result, null, 2)
396
+ }],
397
+ details: result
398
+ };
399
+ }
400
+ });
401
+ pi.registerTool({
402
+ name: "avenor_answer_permission",
403
+ label: "Avenor Answer Permission",
404
+ description: "Answer a pending permission request for a sub-agent. Pass option_id from avenor_status pending_permission.options.",
405
+ parameters: Type.Object({
406
+ run_id: Type.String({ description: "Run ID with the pending permission" }),
407
+ option_id: Type.String({ description: "allow_once | allow_always | deny" }),
408
+ request_id: Type.Optional(Type.String({ description: "Permission request ID (auto-discovered if omitted)" })),
409
+ supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
410
+ }),
411
+ async execute(_toolCallId, params) {
412
+ const result = await answerPermissionTool({
413
+ runId: params.run_id,
414
+ optionId: params.option_id,
415
+ requestId: params.request_id,
416
+ supervisorId: params.supervisor_id
417
+ });
418
+ return {
419
+ content: [{
420
+ type: "text",
421
+ text: JSON.stringify(result, null, 2)
422
+ }],
423
+ details: result
424
+ };
425
+ }
426
+ });
427
+ pi.registerTool({
428
+ name: "avenor_follow_up",
429
+ label: "Avenor Follow Up",
430
+ description: "Resume a completed run with a follow-up message.",
431
+ parameters: Type.Object({
432
+ run_id: Type.String({ description: "Completed run ID to resume" }),
433
+ message: Type.String({ description: "Follow-up message text" }),
434
+ label: Type.Optional(Type.String({ description: "Override label (defaults to <original>-followup)" })),
435
+ supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
436
+ }),
437
+ async execute(_toolCallId, params) {
438
+ const result = await followUpTool({
439
+ runId: params.run_id,
440
+ message: params.message,
441
+ label: params.label,
442
+ supervisorId: params.supervisor_id
443
+ });
444
+ return {
445
+ content: [{
446
+ type: "text",
447
+ text: JSON.stringify(result, null, 2)
448
+ }],
449
+ details: result
450
+ };
451
+ }
452
+ });
453
+ pi.registerTool({
454
+ name: "avenor_events",
455
+ label: "Avenor Events",
456
+ description: "Read events from a run. Filter by type. Returns last N events.",
457
+ parameters: Type.Object({
458
+ run_id: Type.String({ description: "Run ID to read events from" }),
459
+ types: Type.Optional(Type.Array(Type.String(), { description: "Filter by event types" })),
460
+ limit: Type.Optional(Type.Number({ description: "Max events to return (default 50)" })),
461
+ supervisor_id: Type.Optional(Type.String({ description: "Reuse an existing supervisor by socket path" }))
462
+ }),
463
+ async execute(_toolCallId, params) {
464
+ const result = await eventsTool({
465
+ runId: params.run_id,
466
+ types: params.types,
467
+ limit: params.limit,
468
+ supervisorId: params.supervisor_id
469
+ });
470
+ return {
471
+ content: [{
472
+ type: "text",
473
+ text: JSON.stringify(result, null, 2)
474
+ }],
475
+ details: result
476
+ };
477
+ }
478
+ });
479
+ pi.registerTool({
480
+ name: "avenor_shutdown",
481
+ label: "Avenor Shutdown",
482
+ description: "Shut down the avenor supervisor and clean up temp files.",
483
+ parameters: Type.Object({
484
+ supervisor_id: Type.Optional(Type.String({ description: "Supervisor to shut down (defaults to singleton)" })),
485
+ force: Type.Optional(Type.Boolean({ description: "Force shutdown instead of graceful" }))
486
+ }),
487
+ async execute(_toolCallId, params) {
488
+ const result = await shutdownTool({
489
+ supervisorId: params.supervisor_id,
490
+ force: params.force
491
+ });
492
+ return {
493
+ content: [{
494
+ type: "text",
495
+ text: JSON.stringify(result, null, 2)
496
+ }],
497
+ details: result
498
+ };
499
+ }
500
+ });
501
+ pi.registerCommand("avenor-status", {
502
+ description: "Show status of all avenor runs",
503
+ handler: async (_args, ctx) => {
504
+ const entries = await pollRuns();
505
+ if (entries.length === 0) {
506
+ ctx.ui.notify("No active avenor runs", "info");
507
+ return;
508
+ }
509
+ const lines = entries.map((e) => formatRunLine(e));
510
+ ctx.ui.setWidget("avenor-status-detail", lines);
511
+ }
512
+ });
513
+ pi.registerCommand("avenor-watch", {
514
+ description: "Open a live event feed for a run",
515
+ getArgumentCompletions: (prefix) => {
516
+ const items = Array.from(trackedRuns.values()).filter((r) => r.label.startsWith(prefix) || r.runId.startsWith(prefix)).map((r) => ({
517
+ value: r.runId,
518
+ label: `${r.label} (${r.runId.slice(0, 8)})`
519
+ }));
520
+ return items.length > 0 ? items : null;
521
+ },
522
+ handler: async (args, ctx) => {
523
+ if (!ctx.hasUI) {
524
+ ctx.ui.notify("avenor-watch requires TUI mode", "warning");
525
+ return;
526
+ }
527
+ let runId = args?.trim();
528
+ if (!runId) {
529
+ const runList = Array.from(trackedRuns.values());
530
+ if (runList.length === 0) {
531
+ ctx.ui.notify("No tracked runs", "warning");
532
+ return;
533
+ }
534
+ const choices = runList.map((r) => r.runId);
535
+ const labels = runList.map((r) => `${r.label} (${r.runId.slice(0, 8)})`);
536
+ const idx = await ctx.ui.select("Watch run:", labels);
537
+ if (idx === null) return;
538
+ runId = choices[idx];
539
+ }
540
+ let watchActive = true;
541
+ const overlay = new EventFeedOverlay(runId);
542
+ try {
543
+ const { events } = await eventsTool({
544
+ runId,
545
+ limit: 50
546
+ });
547
+ overlay.setEvents(events);
548
+ } catch (err) {
549
+ console.error("avenor watch: failed to load initial events", err);
550
+ overlay.setEvents([]);
551
+ }
552
+ const handle = ctx.ui.custom((_tui, _theme, _kb, done) => {
553
+ overlay.setOnClose(() => {
554
+ watchActive = false;
555
+ done();
556
+ });
557
+ return overlay;
558
+ }, {
559
+ overlay: true,
560
+ overlayOptions: {
561
+ anchor: "right-center",
562
+ width: "45%",
563
+ minWidth: 60,
564
+ maxHeight: "70%"
565
+ },
566
+ onHandle: (h) => {
567
+ handle._inner = h;
568
+ }
569
+ });
570
+ const watchTick = async () => {
571
+ while (watchActive) {
572
+ await sleep(POLL_INTERVAL_MS);
573
+ try {
574
+ const { events } = await eventsTool({
575
+ runId,
576
+ limit: 50
577
+ });
578
+ overlay.setEvents(events);
579
+ handle.requestRender?.();
580
+ } catch (err) {
581
+ console.error("avenor watch poll: eventsTool failed", err);
582
+ watchActive = false;
583
+ }
584
+ }
585
+ };
586
+ watchTick();
587
+ }
588
+ });
589
+ pi.registerCommand("avenor-cancel", {
590
+ description: "Cancel a running avenor sub-agent",
591
+ getArgumentCompletions: (prefix) => {
592
+ const items = Array.from(trackedRuns.values()).filter((r) => r.label.startsWith(prefix) || r.runId.startsWith(prefix)).map((r) => ({
593
+ value: r.runId,
594
+ label: `${r.label} (${r.runId.slice(0, 8)})`
595
+ }));
596
+ return items.length > 0 ? items : null;
597
+ },
598
+ handler: async (args, ctx) => {
599
+ if (!args?.trim()) {
600
+ ctx.ui.notify("Usage: /avenor-cancel <run_id>", "warning");
601
+ return;
602
+ }
603
+ const runId = args.trim();
604
+ const run = trackedRuns.get(runId);
605
+ const label = run?.label ?? runId;
606
+ if (!await ctx.ui.confirm("Cancel run", `Cancel "${label}" (${runId.slice(0, 8)})?`)) return;
607
+ try {
608
+ const { Supervisor } = await import("@dougbots/avenor-core");
609
+ await (await Supervisor.get()).getClient().cancel(run?.lastStatus?.runtime_id ?? runId);
610
+ ctx.ui.notify(`Cancelled "${label}"`, "info");
611
+ } catch (err) {
612
+ ctx.ui.notify(`Failed to cancel: ${err instanceof Error ? err.message : String(err)}`, "error");
613
+ }
614
+ }
615
+ });
616
+ }
617
+ //#endregion
618
+ export { src_default as default };
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@dougbots/avenor-pi",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "keywords": [
6
+ "pi-package"
7
+ ],
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/sdougbrown/avenor",
11
+ "directory": "packages/pi"
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "main": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "default": "./dist/index.js"
22
+ }
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "pi": {
28
+ "extensions": [
29
+ "./dist/index.js"
30
+ ]
31
+ },
32
+ "scripts": {
33
+ "build": "tsdown",
34
+ "test": "bun test",
35
+ "dev": "tsdown --watch"
36
+ },
37
+ "dependencies": {
38
+ "@dougbots/avenor-core": "^0.3.2"
39
+ },
40
+ "peerDependencies": {
41
+ "@earendil-works/pi-coding-agent": "*",
42
+ "@earendil-works/pi-tui": "*"
43
+ },
44
+ "devDependencies": {
45
+ "@earendil-works/pi-coding-agent": "*",
46
+ "@earendil-works/pi-tui": "*",
47
+ "@types/bun": "^1.3.12",
48
+ "@types/node": "^25.8.0",
49
+ "tsdown": "^0.22.0",
50
+ "typebox": "^1.2.17",
51
+ "typescript": "^6.0.3"
52
+ }
53
+ }