@thurstonsand/pi-agent-switchboard 0.3.0 → 0.4.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/index.js +79 -15
- package/package.json +2 -1
package/index.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
// packages/pi/src/index.ts
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname as dirname3, join as join2 } from "node:path";
|
|
3
5
|
import { promisify } from "node:util";
|
|
6
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
4
7
|
|
|
5
8
|
// packages/shared/src/index.ts
|
|
6
9
|
import { execFileSync } from "node:child_process";
|
|
7
|
-
import { readFileSync } from "node:fs";
|
|
10
|
+
import { existsSync as existsSync2, readFileSync, realpathSync } from "node:fs";
|
|
11
|
+
import { homedir } from "node:os";
|
|
8
12
|
import { dirname as dirname2, join } from "node:path";
|
|
9
13
|
|
|
10
14
|
// packages/shared/src/db.ts
|
|
@@ -70,7 +74,20 @@ class Db {
|
|
|
70
74
|
}
|
|
71
75
|
|
|
72
76
|
// packages/shared/src/index.ts
|
|
73
|
-
var SCHEMA_VERSION =
|
|
77
|
+
var SCHEMA_VERSION = 3;
|
|
78
|
+
function configDir() {
|
|
79
|
+
return join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "agent-switchboard");
|
|
80
|
+
}
|
|
81
|
+
function operatorDir() {
|
|
82
|
+
if (physicalOperatorDir)
|
|
83
|
+
return physicalOperatorDir;
|
|
84
|
+
const dir = join(configDir(), "operator");
|
|
85
|
+
if (!existsSync2(dir))
|
|
86
|
+
return dir;
|
|
87
|
+
physicalOperatorDir = realpathSync(dir);
|
|
88
|
+
return physicalOperatorDir;
|
|
89
|
+
}
|
|
90
|
+
var physicalOperatorDir;
|
|
74
91
|
function bootId() {
|
|
75
92
|
if (process.platform === "darwin")
|
|
76
93
|
return execFileSync("/usr/sbin/sysctl", ["-n", "kern.bootsessionuuid"], { encoding: "utf8" }).trim();
|
|
@@ -156,6 +173,7 @@ function hostOfTranscript(db, proc, transcript) {
|
|
|
156
173
|
return liveHost(db.get("SELECT r.tmux_session, r.boot_id, r.pid FROM runtimes r JOIN sessions s USING (session_id) WHERE s.transcript = ? AND r.tmux_session != ?", transcript, proc.launch.tmuxSession));
|
|
157
174
|
}
|
|
158
175
|
function src_default(pi) {
|
|
176
|
+
pi.on("project_trust", (event) => ({ trusted: event.cwd === operatorDir() ? "yes" : "undecided" }));
|
|
159
177
|
const claimed = claim();
|
|
160
178
|
if (!claimed)
|
|
161
179
|
return;
|
|
@@ -211,8 +229,24 @@ function src_default(pi) {
|
|
|
211
229
|
ctx.ui.notify(`swb: that session is already open in another pi (${host})`, "error");
|
|
212
230
|
return { cancel: true };
|
|
213
231
|
});
|
|
214
|
-
|
|
232
|
+
const draftPath = (id) => join2(dirname3(proc.launch.db), "drafts", id);
|
|
233
|
+
const restoreDraft = (ctx, id) => {
|
|
234
|
+
let draft;
|
|
235
|
+
try {
|
|
236
|
+
draft = readFileSync2(draftPath(id), "utf8");
|
|
237
|
+
} catch (error) {
|
|
238
|
+
if (error.code === "ENOENT")
|
|
239
|
+
return;
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
if (!ctx.ui.getEditorText())
|
|
243
|
+
ctx.ui.setEditorText(draft);
|
|
244
|
+
rmSync(draftPath(id));
|
|
245
|
+
};
|
|
246
|
+
pi.on("session_start", (event, ctx) => {
|
|
215
247
|
current = ctx.sessionManager.getSessionId();
|
|
248
|
+
if (event.reason !== "reload")
|
|
249
|
+
restoreDraft(ctx, current);
|
|
216
250
|
proc.attention.clear();
|
|
217
251
|
writeFor(ctx, (db, id) => {
|
|
218
252
|
const host = hostOfSession(db, proc, id);
|
|
@@ -287,22 +321,46 @@ function src_default(pi) {
|
|
|
287
321
|
}
|
|
288
322
|
});
|
|
289
323
|
pi.registerTool({
|
|
290
|
-
name: "
|
|
291
|
-
label: "
|
|
292
|
-
description: "Archive
|
|
293
|
-
promptSnippet: "Archive
|
|
324
|
+
name: "swb_update_session",
|
|
325
|
+
label: "Update session",
|
|
326
|
+
description: "Archive or unarchive a session. Omit session to update the current session.",
|
|
327
|
+
promptSnippet: "Archive or unarchive a session",
|
|
294
328
|
promptGuidelines: [
|
|
295
|
-
"
|
|
296
|
-
"
|
|
329
|
+
"Archive only when the user asks, or when you started that session and have driven it to completion.",
|
|
330
|
+
"Archiving the current session will quit it when the turn ends: call it last, then end with a brief final message.",
|
|
331
|
+
"Archiving another session fails while it is mid-turn. Unarchiving doesn't restart it; session_send_message wakes it."
|
|
297
332
|
],
|
|
298
|
-
parameters: Type.Object({
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
333
|
+
parameters: Type.Object({
|
|
334
|
+
session: Type.Optional(Type.String({ description: "Session id; omit for the current session" })),
|
|
335
|
+
archived: Type.Boolean()
|
|
336
|
+
}),
|
|
337
|
+
renderCall(args, theme) {
|
|
338
|
+
const target = !args.session || args.session === current ? "This session" : connect().get("SELECT title FROM sessions WHERE session_id = ?", args.session)?.title ?? args.session;
|
|
339
|
+
return new Text(`${theme.fg("toolTitle", theme.bold(args.archived ? "Archive" : "Unarchive"))} · ${theme.fg("accent", target)}`, 0, 0);
|
|
340
|
+
},
|
|
341
|
+
renderResult(result, _options, theme, context) {
|
|
342
|
+
const output = result.content.find((item) => item.type === "text")?.text ?? "";
|
|
343
|
+
return new Text(context.isError ? theme.fg("error", output) : "", 0, 0);
|
|
344
|
+
},
|
|
345
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
346
|
+
const self = ctx.sessionManager.getSessionId();
|
|
347
|
+
if (!params.session || params.session === self) {
|
|
348
|
+
if (!params.archived)
|
|
349
|
+
throw new Error("this session is already open");
|
|
350
|
+
if (!markArchived(ctx)) {
|
|
351
|
+
throw new Error(proc.failure ? `swb: not recording: ${proc.failure}` : "swb: no session recorded yet");
|
|
352
|
+
}
|
|
353
|
+
ctx.shutdown();
|
|
354
|
+
return { content: [{ type: "text", text: "Archived. pi quits when this turn ends." }], details: undefined };
|
|
355
|
+
}
|
|
356
|
+
try {
|
|
357
|
+
await run(proc.launch.bin, [params.archived ? "archive" : "unarchive", params.session], { timeout: 15000 });
|
|
358
|
+
} catch (error) {
|
|
359
|
+
const { stderr, message } = error;
|
|
360
|
+
throw new Error(stderr?.trim() || message);
|
|
302
361
|
}
|
|
303
|
-
ctx.shutdown();
|
|
304
362
|
return {
|
|
305
|
-
content: [{ type: "text", text: "Archived.
|
|
363
|
+
content: [{ type: "text", text: params.archived ? "Archived." : "Unarchived. It stays dormant until messaged." }],
|
|
306
364
|
details: undefined
|
|
307
365
|
};
|
|
308
366
|
}
|
|
@@ -310,6 +368,12 @@ function src_default(pi) {
|
|
|
310
368
|
pi.on("session_shutdown", (event, ctx) => {
|
|
311
369
|
if (event.reason !== "quit")
|
|
312
370
|
return;
|
|
371
|
+
const draft = ctx.ui.getEditorText();
|
|
372
|
+
if (draft.trim()) {
|
|
373
|
+
const path = draftPath(ctx.sessionManager.getSessionId());
|
|
374
|
+
mkdirSync2(dirname3(path), { recursive: true, mode: 448 });
|
|
375
|
+
writeFileSync(path, draft, { mode: 384 });
|
|
376
|
+
}
|
|
313
377
|
writeFor(ctx, (db) => db.run("DELETE FROM runtimes WHERE tmux_session = ?", proc.launch.tmuxSession));
|
|
314
378
|
});
|
|
315
379
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thurstonsand/pi-agent-switchboard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Records managed pi sessions for Agent Switchboard (swb)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"peerDependencies": {
|
|
20
20
|
"@earendil-works/pi-coding-agent": "*",
|
|
21
|
+
"@earendil-works/pi-tui": "*",
|
|
21
22
|
"typebox": "*"
|
|
22
23
|
}
|
|
23
24
|
}
|