@streichsbaer/pi-mesh 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/LICENSE +21 -0
- package/README.md +113 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +796 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/live-socket.d.ts +17 -0
- package/dist/live-socket.js +116 -0
- package/dist/lock.d.ts +7 -0
- package/dist/lock.js +82 -0
- package/dist/mesh.d.ts +3 -0
- package/dist/mesh.js +22 -0
- package/dist/model-list.d.ts +48 -0
- package/dist/model-list.js +208 -0
- package/dist/model-selection.d.ts +47 -0
- package/dist/model-selection.js +171 -0
- package/dist/pi-runner.d.ts +39 -0
- package/dist/pi-runner.js +182 -0
- package/dist/pi-session-parser.d.ts +57 -0
- package/dist/pi-session-parser.js +459 -0
- package/dist/registry.d.ts +24 -0
- package/dist/registry.js +139 -0
- package/dist/types.d.ts +142 -0
- package/dist/types.js +1 -0
- package/dist/utils.d.ts +17 -0
- package/dist/utils.js +157 -0
- package/package.json +72 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,796 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { exists } from "./utils.js";
|
|
5
|
+
import { formatTimestamp, truncate, compactWhitespace } from "./utils.js";
|
|
6
|
+
import { loadSessionData, loadSessionDataForSession, renderToolLine, resolveSessionSpec, searchSessions, tail } from "./pi-session-parser.js";
|
|
7
|
+
import { createMeshId, filterManagedSessions, findManagedSessions, listManagedSessions, lockPathFor, normalizeLabels, socketPathFor, upsertManagedSession } from "./registry.js";
|
|
8
|
+
import { resolveMesh } from "./mesh.js";
|
|
9
|
+
import { withDirectoryLock } from "./lock.js";
|
|
10
|
+
import { mergeModelSelection } from "./model-selection.js";
|
|
11
|
+
import { THINKING_LEVELS } from "./types.js";
|
|
12
|
+
function parseArgs(argv) {
|
|
13
|
+
const positionals = [];
|
|
14
|
+
const options = new Map();
|
|
15
|
+
const setOption = (name, value) => {
|
|
16
|
+
const existing = options.get(name);
|
|
17
|
+
if (existing === undefined)
|
|
18
|
+
options.set(name, value);
|
|
19
|
+
else if (Array.isArray(existing))
|
|
20
|
+
existing.push(value);
|
|
21
|
+
else
|
|
22
|
+
options.set(name, [existing, value]);
|
|
23
|
+
};
|
|
24
|
+
for (let i = 0; i < argv.length; i++) {
|
|
25
|
+
const arg = argv[i];
|
|
26
|
+
if (arg === "--") {
|
|
27
|
+
positionals.push(...argv.slice(i + 1));
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
if (!arg.startsWith("--")) {
|
|
31
|
+
positionals.push(arg);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const eq = arg.indexOf("=");
|
|
35
|
+
if (eq >= 0) {
|
|
36
|
+
setOption(arg.slice(2, eq), arg.slice(eq + 1));
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const name = arg.slice(2);
|
|
40
|
+
const next = argv[i + 1];
|
|
41
|
+
if (next && !next.startsWith("--")) {
|
|
42
|
+
setOption(name, next);
|
|
43
|
+
i += 1;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
setOption(name, true);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return { positionals, options };
|
|
50
|
+
}
|
|
51
|
+
function getString(args, name, fallback) {
|
|
52
|
+
const value = args.options.get(name);
|
|
53
|
+
if (typeof value === "string")
|
|
54
|
+
return value;
|
|
55
|
+
if (Array.isArray(value))
|
|
56
|
+
return [...value].reverse().find((item) => typeof item === "string") ?? fallback;
|
|
57
|
+
return fallback;
|
|
58
|
+
}
|
|
59
|
+
function getStrings(args, name) {
|
|
60
|
+
const value = args.options.get(name);
|
|
61
|
+
const values = Array.isArray(value) ? value : value === undefined ? [] : [value];
|
|
62
|
+
return values.filter((item) => typeof item === "string");
|
|
63
|
+
}
|
|
64
|
+
function getRequiredString(args, name) {
|
|
65
|
+
const value = args.options.get(name);
|
|
66
|
+
if (value === true || (Array.isArray(value) && value.some((item) => item === true)))
|
|
67
|
+
throw new Error(`--${name} requires a value.`);
|
|
68
|
+
if (typeof value === "string")
|
|
69
|
+
return value;
|
|
70
|
+
if (Array.isArray(value))
|
|
71
|
+
return [...value].reverse().find((item) => typeof item === "string");
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
function getBool(args, name) {
|
|
75
|
+
const value = args.options.get(name);
|
|
76
|
+
return value === true || (Array.isArray(value) && value.includes(true));
|
|
77
|
+
}
|
|
78
|
+
function getNumber(args, name, fallback) {
|
|
79
|
+
const value = Number(getString(args, name, String(fallback)));
|
|
80
|
+
return Number.isFinite(value) ? value : fallback;
|
|
81
|
+
}
|
|
82
|
+
function getDelivery(args) {
|
|
83
|
+
const value = getString(args, "delivery", "auto");
|
|
84
|
+
if (["auto", "prompt", "steer", "follow-up"].includes(value))
|
|
85
|
+
return value;
|
|
86
|
+
throw new Error(`Invalid --delivery ${JSON.stringify(value)}. Expected auto, prompt, steer, or follow-up.`);
|
|
87
|
+
}
|
|
88
|
+
function getThinkingLevel(args) {
|
|
89
|
+
const value = getRequiredString(args, "thinking");
|
|
90
|
+
if (value === undefined)
|
|
91
|
+
return undefined;
|
|
92
|
+
if (THINKING_LEVELS.includes(value))
|
|
93
|
+
return value;
|
|
94
|
+
throw new Error(`Invalid --thinking ${JSON.stringify(value)}. Expected: ${THINKING_LEVELS.join(", ")}.`);
|
|
95
|
+
}
|
|
96
|
+
function getModelSelection(args) {
|
|
97
|
+
const provider = getRequiredString(args, "provider")?.trim();
|
|
98
|
+
const model = getRequiredString(args, "model")?.trim();
|
|
99
|
+
const thinkingLevel = getThinkingLevel(args);
|
|
100
|
+
if (provider === "")
|
|
101
|
+
throw new Error("--provider requires a non-empty value.");
|
|
102
|
+
if (model === "")
|
|
103
|
+
throw new Error("--model requires a non-empty value.");
|
|
104
|
+
if (provider && !model)
|
|
105
|
+
throw new Error("--provider requires --model.");
|
|
106
|
+
if (!provider && !model && !thinkingLevel)
|
|
107
|
+
return undefined;
|
|
108
|
+
return { provider, model, thinkingLevel };
|
|
109
|
+
}
|
|
110
|
+
async function validateCliModelSelection(folder, modelSelection) {
|
|
111
|
+
if (!modelSelection)
|
|
112
|
+
return undefined;
|
|
113
|
+
const { validateModelSelection } = await import("./pi-runner.js");
|
|
114
|
+
return validateModelSelection({ cwd: folder, modelSelection });
|
|
115
|
+
}
|
|
116
|
+
async function resolveFolder(value) {
|
|
117
|
+
const folder = path.resolve(value || process.cwd());
|
|
118
|
+
return fs.realpath(folder).catch(() => folder);
|
|
119
|
+
}
|
|
120
|
+
function getLabels(args) {
|
|
121
|
+
for (const name of ["label", "labels"]) {
|
|
122
|
+
const value = args.options.get(name);
|
|
123
|
+
if (value === true || (Array.isArray(value) && value.some((item) => item === true)))
|
|
124
|
+
throw new Error(`--${name} requires a value.`);
|
|
125
|
+
}
|
|
126
|
+
return normalizeLabels([...getStrings(args, "label"), ...getStrings(args, "labels")]);
|
|
127
|
+
}
|
|
128
|
+
async function getSessionSelector(args, spec) {
|
|
129
|
+
const folderOption = getString(args, "folder");
|
|
130
|
+
return {
|
|
131
|
+
spec,
|
|
132
|
+
folder: folderOption ? await resolveFolder(folderOption) : undefined,
|
|
133
|
+
name: getString(args, "name"),
|
|
134
|
+
labels: getLabels(args),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function isLiveStatus(status) {
|
|
138
|
+
return status === "running" || status === "idle" || status === "busy" || status === "starting";
|
|
139
|
+
}
|
|
140
|
+
function formatMatches(records) {
|
|
141
|
+
return records.map((record) => `- ${record.meshId}${record.name ? ` name=${JSON.stringify(record.name)}` : ""} folder=${record.folder} status=${record.status}`).join("\n");
|
|
142
|
+
}
|
|
143
|
+
function isProcessAlive(pid) {
|
|
144
|
+
if (!pid || pid <= 0)
|
|
145
|
+
return false;
|
|
146
|
+
try {
|
|
147
|
+
process.kill(pid, 0);
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function isStaleSocketError(error) {
|
|
155
|
+
const code = error?.code;
|
|
156
|
+
return code === "ECONNREFUSED" || code === "ENOENT" || code === "EPIPE" || code === "ECONNRESET";
|
|
157
|
+
}
|
|
158
|
+
async function refreshStaleManagedSessions(mesh, records) {
|
|
159
|
+
const refreshed = [];
|
|
160
|
+
for (const record of records) {
|
|
161
|
+
const stale = isLiveStatus(record.status) && (!record.pid || !isProcessAlive(record.pid));
|
|
162
|
+
if (!stale) {
|
|
163
|
+
refreshed.push(record);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (record.socketPath)
|
|
167
|
+
await fs.rm(record.socketPath, { force: true }).catch(() => undefined);
|
|
168
|
+
refreshed.push(await upsertManagedSession(mesh, { ...record, status: "offline", socketPath: undefined, pid: undefined }));
|
|
169
|
+
}
|
|
170
|
+
return refreshed;
|
|
171
|
+
}
|
|
172
|
+
async function getMesh() {
|
|
173
|
+
return resolveMesh();
|
|
174
|
+
}
|
|
175
|
+
function printHelp() {
|
|
176
|
+
console.log(`pi-mesh
|
|
177
|
+
|
|
178
|
+
Usage:
|
|
179
|
+
pi-mesh sessions list [--folder <dir>] [--name <name>] [--label <label>] [--limit 25] [--json] [--include-pi|--all]
|
|
180
|
+
pi-mesh sessions find <query> [--folder <dir>] [--name <name>] [--label <label>] [--limit 25] [--json] [--include-pi|--all]
|
|
181
|
+
pi-mesh transcript <session> [--folder <dir>] [--name <name>] [--label <label>] [--last 3] [--json] [--show-tools]
|
|
182
|
+
pi-mesh state <session> [--folder <dir>] [--name <name>] [--label <label>] [--json]
|
|
183
|
+
pi-mesh models list [search] [--folder <dir>] [--json] [--all] [--scoped]
|
|
184
|
+
|
|
185
|
+
pi-mesh spawn --name <name> [--folder <dir>] [--label <label>] [--prompt <text>] [--attach] [--provider <name>] [--model <ref>] [--thinking <level>]
|
|
186
|
+
pi-mesh run --name <name> [--folder <dir>] [--label <label>] [--new] [--prompt <text>] [--provider <name>] [--model <ref>] [--thinking <level>]
|
|
187
|
+
pi-mesh attach <session|session-file> [--name <name>] [--folder <dir>] [--label <label>] [--provider <name>] [--model <ref>] [--thinking <level>]
|
|
188
|
+
pi-mesh send [<session>] <message> [--folder <dir>] [--name <name>] [--label <label>] [--all] [--delivery auto|prompt|steer|follow-up] [--stream] [--provider <name>] [--model <ref>] [--thinking <level>]
|
|
189
|
+
|
|
190
|
+
Notes:
|
|
191
|
+
- spawn defaults to sleeping/headless. Use --attach or pi-mesh run for vanilla Pi TUI.
|
|
192
|
+
- send wakes sleeping managed sessions, or uses a live socket for pi-mesh run sessions.
|
|
193
|
+
- sessions are tracked in one machine-local registry; --folder, --name, and --label filter it.
|
|
194
|
+
- names and labels are not unique; use --all to intentionally broadcast to multiple matches.
|
|
195
|
+
- pass --model provider/model or --model model:thinking to choose a session model.
|
|
196
|
+
- use models list to inspect Pi-configured models; --folder selects the target session/settings scope, --all includes unauthenticated models, and --scoped filters Pi enabledModels.
|
|
197
|
+
- unmanaged already-running Pi sessions are readable; close and attach them to make them managed.
|
|
198
|
+
`);
|
|
199
|
+
}
|
|
200
|
+
function printManaged(record) {
|
|
201
|
+
const name = record.name ? `${record.name} ` : "";
|
|
202
|
+
console.log(`${record.meshId} · ${name}${record.kind} · ${record.status}`);
|
|
203
|
+
console.log(` folder: ${record.folder}`);
|
|
204
|
+
if (record.labels?.length)
|
|
205
|
+
console.log(` labels: ${record.labels.join(",")}`);
|
|
206
|
+
console.log(` sessionFile: ${record.sessionFile}`);
|
|
207
|
+
if (record.socketPath)
|
|
208
|
+
console.log(` socket: ${record.socketPath}`);
|
|
209
|
+
if (record.pendingModelSelection) {
|
|
210
|
+
const model = record.pendingModelSelection.model
|
|
211
|
+
? `${record.pendingModelSelection.provider ? `${record.pendingModelSelection.provider}/` : ""}${record.pendingModelSelection.model}`
|
|
212
|
+
: undefined;
|
|
213
|
+
console.log(` pendingModel: ${[model, record.pendingModelSelection.thinkingLevel && `thinking=${record.pendingModelSelection.thinkingLevel}`].filter(Boolean).join(" ")}`);
|
|
214
|
+
}
|
|
215
|
+
console.log(` updatedAt: ${record.updatedAt}`);
|
|
216
|
+
if (record.lastError)
|
|
217
|
+
console.log(` error: ${record.lastError}`);
|
|
218
|
+
console.log("");
|
|
219
|
+
}
|
|
220
|
+
async function cmdSessions(parsed) {
|
|
221
|
+
const sub = parsed.positionals[1] || "list";
|
|
222
|
+
const limit = getNumber(parsed, "limit", 25);
|
|
223
|
+
const asJson = getBool(parsed, "json");
|
|
224
|
+
const mesh = await getMesh();
|
|
225
|
+
const selector = await getSessionSelector(parsed);
|
|
226
|
+
const managed = filterManagedSessions(await refreshStaleManagedSessions(mesh, await listManagedSessions(mesh)), selector);
|
|
227
|
+
if (sub === "list") {
|
|
228
|
+
const includePi = getBool(parsed, "include-pi") || getBool(parsed, "all");
|
|
229
|
+
const piSessions = includePi ? await searchSessions({ limit }) : [];
|
|
230
|
+
if (asJson) {
|
|
231
|
+
console.log(JSON.stringify({ ok: true, mesh, filters: selector, managed, piSessions }, null, 2));
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
console.log("# pi-mesh sessions");
|
|
235
|
+
console.log("");
|
|
236
|
+
console.log("## managed sessions");
|
|
237
|
+
if (managed.length)
|
|
238
|
+
for (const record of managed)
|
|
239
|
+
printManaged(record);
|
|
240
|
+
else
|
|
241
|
+
console.log("[none]\n");
|
|
242
|
+
if (includePi) {
|
|
243
|
+
console.log("## recent Pi sessions");
|
|
244
|
+
if (piSessions.length) {
|
|
245
|
+
for (const item of piSessions) {
|
|
246
|
+
console.log(`${item.rawSessionId} · ${item.sessionName || item.repoName}`);
|
|
247
|
+
console.log(` updatedAt: ${formatTimestamp(item.updatedAt)}`);
|
|
248
|
+
console.log(` path: ${item.path}`);
|
|
249
|
+
console.log(` lastUser: ${truncate(compactWhitespace(item.lastUser || item.firstUser || ""), 160)}`);
|
|
250
|
+
console.log("");
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
console.log("[none]\n");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (sub === "find") {
|
|
260
|
+
const query = parsed.positionals.slice(2).join(" ").trim();
|
|
261
|
+
if (!query)
|
|
262
|
+
throw new Error("Usage: pi-mesh sessions find <query>");
|
|
263
|
+
const queryLower = query.toLowerCase();
|
|
264
|
+
const managedMatches = managed.filter((record) => [record.meshId, record.name, record.folder, record.sessionFile, record.rawSessionId, ...(record.labels ?? [])]
|
|
265
|
+
.filter(Boolean)
|
|
266
|
+
.some((value) => String(value).toLowerCase().includes(queryLower)));
|
|
267
|
+
const exactManaged = managedMatches.some((record) => record.meshId === query || record.name === query || record.rawSessionId === query || record.sessionFile === query);
|
|
268
|
+
const includePi = getBool(parsed, "include-pi") || getBool(parsed, "all");
|
|
269
|
+
const piSessions = exactManaged && !includePi ? [] : await searchSessions({ query, limit });
|
|
270
|
+
if (asJson) {
|
|
271
|
+
console.log(JSON.stringify({ ok: true, mesh, filters: selector, managed: managedMatches, piSessions, skippedPiSearch: exactManaged && !includePi }, null, 2));
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
console.log("## managed matches");
|
|
275
|
+
if (managedMatches.length)
|
|
276
|
+
for (const record of managedMatches)
|
|
277
|
+
printManaged(record);
|
|
278
|
+
else
|
|
279
|
+
console.log("[none]\n");
|
|
280
|
+
if (exactManaged && !includePi) {
|
|
281
|
+
console.log("## Pi session matches");
|
|
282
|
+
console.log("[skipped exact managed match; pass --include-pi to search unmanaged Pi sessions]\n");
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
console.log("## Pi session matches");
|
|
286
|
+
for (const item of piSessions) {
|
|
287
|
+
console.log(`${item.rawSessionId} · ${item.sessionName || item.repoName}`);
|
|
288
|
+
console.log(` path: ${item.path}`);
|
|
289
|
+
console.log(` lastUser: ${truncate(compactWhitespace(item.lastUser || item.firstUser || ""), 160)}`);
|
|
290
|
+
console.log("");
|
|
291
|
+
}
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
throw new Error(`Unknown sessions subcommand: ${sub}`);
|
|
295
|
+
}
|
|
296
|
+
async function resolveSessionFile(mesh, spec, selector = {}) {
|
|
297
|
+
const matches = await findManagedSessions(mesh, { ...selector, spec });
|
|
298
|
+
if (matches.length === 1)
|
|
299
|
+
return { sessionFile: matches[0].sessionFile, folder: matches[0].folder, managed: matches[0] };
|
|
300
|
+
if (matches.length > 1)
|
|
301
|
+
throw new Error(`Multiple managed sessions match ${JSON.stringify(spec)}; refine with --folder, --name, or --label:\n${formatMatches(matches)}`);
|
|
302
|
+
const session = await resolveSessionSpec(spec);
|
|
303
|
+
return { sessionFile: session.path, folder: await resolveFolder(session.cwd || process.cwd()), summary: session };
|
|
304
|
+
}
|
|
305
|
+
async function cmdModels(parsed) {
|
|
306
|
+
const sub = parsed.positionals[1] || "list";
|
|
307
|
+
if (sub !== "list")
|
|
308
|
+
throw new Error(`Unknown models subcommand: ${sub}`);
|
|
309
|
+
const folder = await resolveFolder(getString(parsed, "folder") || process.cwd());
|
|
310
|
+
const search = parsed.positionals.slice(2).join(" ").trim() || undefined;
|
|
311
|
+
const { listModels, printModelList } = await import("./model-list.js");
|
|
312
|
+
const result = await listModels({
|
|
313
|
+
cwd: folder,
|
|
314
|
+
search,
|
|
315
|
+
includeAll: getBool(parsed, "all"),
|
|
316
|
+
scopedOnly: getBool(parsed, "scoped"),
|
|
317
|
+
});
|
|
318
|
+
if (getBool(parsed, "json"))
|
|
319
|
+
console.log(JSON.stringify(result, null, 2));
|
|
320
|
+
else
|
|
321
|
+
printModelList(result);
|
|
322
|
+
}
|
|
323
|
+
async function cmdTranscript(parsed) {
|
|
324
|
+
const spec = parsed.positionals[1];
|
|
325
|
+
if (!spec)
|
|
326
|
+
throw new Error("Usage: pi-mesh transcript <session>");
|
|
327
|
+
const mesh = await getMesh();
|
|
328
|
+
const resolved = await resolveSessionFile(mesh, spec, await getSessionSelector(parsed));
|
|
329
|
+
if (!(await exists(resolved.sessionFile))) {
|
|
330
|
+
const payload = { ok: true, managed: resolved.managed, sessionFile: resolved.sessionFile, transcriptAvailable: false, turns: [] };
|
|
331
|
+
if (getBool(parsed, "json"))
|
|
332
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
333
|
+
else {
|
|
334
|
+
console.log(`# ${resolved.sessionFile}`);
|
|
335
|
+
console.log("[transcript not available yet]");
|
|
336
|
+
if (resolved.managed)
|
|
337
|
+
console.log(`managed: ${resolved.managed.meshId} · ${resolved.managed.kind} · ${resolved.managed.status}`);
|
|
338
|
+
}
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const data = resolved.summary ? await loadSessionDataForSession(resolved.summary) : await loadSessionData(resolved.sessionFile);
|
|
342
|
+
const last = Math.max(1, getNumber(parsed, "last", 3));
|
|
343
|
+
const asJson = getBool(parsed, "json");
|
|
344
|
+
const showTools = getBool(parsed, "show-tools");
|
|
345
|
+
const turns = tail(data.turns, last);
|
|
346
|
+
if (asJson) {
|
|
347
|
+
console.log(JSON.stringify({ ok: true, managed: resolved.managed, session: data.session, turns }, null, 2));
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
console.log(`# ${data.session.path}`);
|
|
351
|
+
console.log(`rawSessionId: ${data.session.rawSessionId}`);
|
|
352
|
+
console.log("");
|
|
353
|
+
for (const turn of turns) {
|
|
354
|
+
console.log(`## Turn ${turn.index} · ${formatTimestamp(turn.startedAt)}`);
|
|
355
|
+
console.log(`user: ${truncate(compactWhitespace(turn.user.text || ""), 260)}`);
|
|
356
|
+
console.log(`assistantMessages: ${turn.assistantMessages.length} · toolCalls: ${turn.toolInvocations.length} · failures: ${turn.failures.length}`);
|
|
357
|
+
console.log("");
|
|
358
|
+
if (showTools) {
|
|
359
|
+
for (const tool of turn.toolInvocations) {
|
|
360
|
+
console.log(renderToolLine(tool));
|
|
361
|
+
console.log("");
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
console.log(turn.finalAssistant?.text || "[no assistant text]");
|
|
366
|
+
console.log("");
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
async function cmdState(parsed) {
|
|
371
|
+
const spec = parsed.positionals[1];
|
|
372
|
+
if (!spec)
|
|
373
|
+
throw new Error("Usage: pi-mesh state <session>");
|
|
374
|
+
const mesh = await getMesh();
|
|
375
|
+
const resolved = await resolveSessionFile(mesh, spec, await getSessionSelector(parsed));
|
|
376
|
+
if (!(await exists(resolved.sessionFile))) {
|
|
377
|
+
const payload = {
|
|
378
|
+
ok: true,
|
|
379
|
+
managed: resolved.managed,
|
|
380
|
+
sessionFile: resolved.sessionFile,
|
|
381
|
+
transcriptAvailable: false,
|
|
382
|
+
counts: { entries: 0, events: 0, turns: 0, tools: 0, failures: 0 },
|
|
383
|
+
lastTurn: null,
|
|
384
|
+
};
|
|
385
|
+
if (getBool(parsed, "json"))
|
|
386
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
387
|
+
else {
|
|
388
|
+
console.log(`${resolved.managed?.rawSessionId || resolved.managed?.meshId || spec} · ${resolved.managed?.name || "managed session"}`);
|
|
389
|
+
console.log(`path: ${resolved.sessionFile}`);
|
|
390
|
+
console.log(`folder: ${resolved.folder}`);
|
|
391
|
+
if (resolved.managed)
|
|
392
|
+
console.log(`managed: ${resolved.managed.meshId} · ${resolved.managed.kind} · ${resolved.managed.status}`);
|
|
393
|
+
console.log("transcript: not available yet");
|
|
394
|
+
}
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const data = resolved.summary ? await loadSessionDataForSession(resolved.summary) : await loadSessionData(resolved.sessionFile);
|
|
398
|
+
const lastTurn = data.turns[data.turns.length - 1];
|
|
399
|
+
const payload = {
|
|
400
|
+
ok: true,
|
|
401
|
+
managed: resolved.managed,
|
|
402
|
+
session: data.session,
|
|
403
|
+
counts: {
|
|
404
|
+
entries: data.transcript.entries.length,
|
|
405
|
+
events: data.events.length,
|
|
406
|
+
turns: data.turns.length,
|
|
407
|
+
tools: data.toolInvocations.length,
|
|
408
|
+
failures: data.toolInvocations.filter((tool) => tool.failed).length,
|
|
409
|
+
},
|
|
410
|
+
lastTurn: lastTurn
|
|
411
|
+
? {
|
|
412
|
+
index: lastTurn.index,
|
|
413
|
+
startedAt: lastTurn.startedAt,
|
|
414
|
+
user: lastTurn.user.text,
|
|
415
|
+
assistant: lastTurn.finalAssistant?.text || null,
|
|
416
|
+
}
|
|
417
|
+
: null,
|
|
418
|
+
};
|
|
419
|
+
if (getBool(parsed, "json")) {
|
|
420
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
console.log(`${data.session.rawSessionId} · ${data.session.sessionName || data.session.repoName}`);
|
|
424
|
+
console.log(`path: ${data.session.path}`);
|
|
425
|
+
console.log(`folder: ${data.session.cwd}`);
|
|
426
|
+
if (resolved.managed)
|
|
427
|
+
console.log(`managed: ${resolved.managed.meshId} · ${resolved.managed.kind} · ${resolved.managed.status}`);
|
|
428
|
+
console.log(`turns: ${payload.counts.turns} · tools: ${payload.counts.tools} · failures: ${payload.counts.failures}`);
|
|
429
|
+
if (payload.lastTurn) {
|
|
430
|
+
console.log(`lastUser: ${truncate(compactWhitespace(payload.lastTurn.user || ""), 180)}`);
|
|
431
|
+
console.log(`lastAssistant: ${truncate(compactWhitespace(payload.lastTurn.assistant || ""), 180)}`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
async function registerSession(mesh, input) {
|
|
435
|
+
const now = new Date().toISOString();
|
|
436
|
+
const meshId = input.meshId || createMeshId({ folder: input.folder, sessionFile: input.sessionFile, rawSessionId: input.rawSessionId });
|
|
437
|
+
return upsertManagedSession(mesh, {
|
|
438
|
+
meshId,
|
|
439
|
+
name: input.name,
|
|
440
|
+
labels: normalizeLabels(input.labels ?? []),
|
|
441
|
+
kind: input.kind,
|
|
442
|
+
status: input.status,
|
|
443
|
+
folder: input.folder,
|
|
444
|
+
sessionFile: input.sessionFile,
|
|
445
|
+
rawSessionId: input.rawSessionId,
|
|
446
|
+
pid: process.pid,
|
|
447
|
+
socketPath: input.socketPath,
|
|
448
|
+
createdAt: now,
|
|
449
|
+
updatedAt: now,
|
|
450
|
+
lastError: input.lastError,
|
|
451
|
+
pendingModelSelection: input.pendingModelSelection,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
async function upsertManagedStatus(mesh, record, patch) {
|
|
455
|
+
const pendingModelSelection = record.pendingModelSelection && (await exists(record.sessionFile)) ? undefined : record.pendingModelSelection;
|
|
456
|
+
return upsertManagedSession(mesh, { ...record, pendingModelSelection, ...patch });
|
|
457
|
+
}
|
|
458
|
+
function assertNotAlreadyLive(record, action) {
|
|
459
|
+
if (!record || !isLiveStatus(record.status) || !isProcessAlive(record.pid))
|
|
460
|
+
return;
|
|
461
|
+
throw new Error(`Session ${record.meshId} is already live (${record.status}); use pi-mesh send or stop that TUI before ${action}.`);
|
|
462
|
+
}
|
|
463
|
+
async function cmdSpawn(parsed) {
|
|
464
|
+
const folder = await resolveFolder(getString(parsed, "folder") || process.cwd());
|
|
465
|
+
const name = getString(parsed, "name");
|
|
466
|
+
const labels = getLabels(parsed);
|
|
467
|
+
const prompt = getString(parsed, "prompt") || parsed.positionals.slice(1).join(" ").trim();
|
|
468
|
+
const attach = getBool(parsed, "attach");
|
|
469
|
+
const modelSelection = await validateCliModelSelection(folder, getModelSelection(parsed));
|
|
470
|
+
const mesh = await getMesh();
|
|
471
|
+
const { createPersistentSession, runHeadlessTurn, runInteractive } = await import("./pi-runner.js");
|
|
472
|
+
const created = await createPersistentSession({ cwd: folder, name, modelSelection });
|
|
473
|
+
if (!created.sessionFile)
|
|
474
|
+
throw new Error("Pi did not create a persistent session file.");
|
|
475
|
+
let record = await registerSession(mesh, {
|
|
476
|
+
name,
|
|
477
|
+
labels,
|
|
478
|
+
folder,
|
|
479
|
+
sessionFile: created.sessionFile,
|
|
480
|
+
rawSessionId: created.rawSessionId,
|
|
481
|
+
kind: attach ? "interactive" : "sleeping",
|
|
482
|
+
status: attach ? "starting" : "offline",
|
|
483
|
+
pendingModelSelection: modelSelection && !(await exists(created.sessionFile)) ? modelSelection : undefined,
|
|
484
|
+
});
|
|
485
|
+
if (attach) {
|
|
486
|
+
const socketPath = await socketPathFor(mesh, record.meshId);
|
|
487
|
+
record = await upsertManagedSession(mesh, { ...record, socketPath, status: "starting", kind: "interactive" });
|
|
488
|
+
await runInteractive({
|
|
489
|
+
cwd: folder,
|
|
490
|
+
sessionFile: record.sessionFile,
|
|
491
|
+
name,
|
|
492
|
+
initialMessage: prompt || undefined,
|
|
493
|
+
socketPath,
|
|
494
|
+
modelSelection,
|
|
495
|
+
onSessionFile: async (sessionFile, rawSessionId) => {
|
|
496
|
+
if (!sessionFile)
|
|
497
|
+
return;
|
|
498
|
+
record = await upsertManagedSession(mesh, {
|
|
499
|
+
...record,
|
|
500
|
+
sessionFile,
|
|
501
|
+
rawSessionId,
|
|
502
|
+
status: "running",
|
|
503
|
+
pid: process.pid,
|
|
504
|
+
socketPath,
|
|
505
|
+
pendingModelSelection: modelSelection && !(await exists(sessionFile)) ? modelSelection : undefined,
|
|
506
|
+
});
|
|
507
|
+
},
|
|
508
|
+
onMaterialized: async () => {
|
|
509
|
+
if (!record.pendingModelSelection)
|
|
510
|
+
return;
|
|
511
|
+
record = await upsertManagedStatus(mesh, record, {});
|
|
512
|
+
},
|
|
513
|
+
onStatus: async (status, error) => {
|
|
514
|
+
record = await upsertManagedStatus(mesh, record, { status, lastError: error, pid: process.pid, socketPath });
|
|
515
|
+
},
|
|
516
|
+
});
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (prompt) {
|
|
520
|
+
await upsertManagedSession(mesh, { ...record, status: "busy" });
|
|
521
|
+
const result = await runHeadlessTurn({ cwd: folder, sessionFile: record.sessionFile, message: prompt, delivery: "prompt", stream: getBool(parsed, "stream"), modelSelection });
|
|
522
|
+
record = await upsertManagedSession(mesh, {
|
|
523
|
+
...record,
|
|
524
|
+
sessionFile: result.sessionFile || record.sessionFile,
|
|
525
|
+
rawSessionId: result.rawSessionId,
|
|
526
|
+
status: "offline",
|
|
527
|
+
pendingModelSelection: undefined,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
if (getBool(parsed, "json"))
|
|
531
|
+
console.log(JSON.stringify({ ok: true, mesh, session: record }, null, 2));
|
|
532
|
+
else {
|
|
533
|
+
console.log(`Spawned sleeping session ${record.meshId}`);
|
|
534
|
+
console.log(`sessionFile: ${record.sessionFile}`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
async function cmdRun(parsed) {
|
|
538
|
+
const folder = await resolveFolder(getString(parsed, "folder") || process.cwd());
|
|
539
|
+
const name = getString(parsed, "name");
|
|
540
|
+
const labels = getLabels(parsed);
|
|
541
|
+
const prompt = getString(parsed, "prompt") || parsed.positionals.slice(1).join(" ").trim();
|
|
542
|
+
const rawModelSelection = getModelSelection(parsed);
|
|
543
|
+
const mesh = await getMesh();
|
|
544
|
+
await refreshStaleManagedSessions(mesh, await listManagedSessions(mesh));
|
|
545
|
+
let existing;
|
|
546
|
+
if (name && !getBool(parsed, "new")) {
|
|
547
|
+
const matches = await findManagedSessions(mesh, { folder, name, labels });
|
|
548
|
+
if (matches.length > 1)
|
|
549
|
+
throw new Error(`Multiple sessions named ${JSON.stringify(name)} match this folder/label selection; use a session id or --new:\n${formatMatches(matches)}`);
|
|
550
|
+
existing = matches[0];
|
|
551
|
+
}
|
|
552
|
+
assertNotAlreadyLive(existing, "opening another live TUI");
|
|
553
|
+
const sessionFile = existing?.sessionFile;
|
|
554
|
+
const pendingModelSelection = existing && !(await exists(existing.sessionFile)) ? existing.pendingModelSelection : undefined;
|
|
555
|
+
const seedModelSelection = mergeModelSelection(pendingModelSelection, rawModelSelection);
|
|
556
|
+
const runFolder = existing?.folder || folder;
|
|
557
|
+
const modelSelection = await validateCliModelSelection(runFolder, seedModelSelection);
|
|
558
|
+
const meshId = existing?.meshId || createMeshId({ folder: runFolder, sessionFile });
|
|
559
|
+
const socketPath = await socketPathFor(mesh, meshId);
|
|
560
|
+
let record = existing;
|
|
561
|
+
const { runInteractive } = await import("./pi-runner.js");
|
|
562
|
+
await runInteractive({
|
|
563
|
+
cwd: runFolder,
|
|
564
|
+
sessionFile,
|
|
565
|
+
name,
|
|
566
|
+
initialMessage: prompt || undefined,
|
|
567
|
+
socketPath,
|
|
568
|
+
modelSelection,
|
|
569
|
+
onSessionFile: async (newSessionFile, rawSessionId) => {
|
|
570
|
+
if (!newSessionFile)
|
|
571
|
+
return;
|
|
572
|
+
record = await registerSession(mesh, {
|
|
573
|
+
meshId,
|
|
574
|
+
name,
|
|
575
|
+
labels: labels.length ? labels : existing?.labels,
|
|
576
|
+
folder: runFolder,
|
|
577
|
+
sessionFile: newSessionFile,
|
|
578
|
+
rawSessionId,
|
|
579
|
+
kind: "interactive",
|
|
580
|
+
status: "running",
|
|
581
|
+
socketPath,
|
|
582
|
+
pendingModelSelection: modelSelection && !(await exists(newSessionFile)) ? modelSelection : undefined,
|
|
583
|
+
});
|
|
584
|
+
},
|
|
585
|
+
onMaterialized: async () => {
|
|
586
|
+
if (!record?.pendingModelSelection)
|
|
587
|
+
return;
|
|
588
|
+
record = await upsertManagedStatus(mesh, record, {});
|
|
589
|
+
},
|
|
590
|
+
onStatus: async (status, error) => {
|
|
591
|
+
if (!record)
|
|
592
|
+
return;
|
|
593
|
+
record = await upsertManagedStatus(mesh, record, { status, lastError: error, pid: process.pid, socketPath });
|
|
594
|
+
},
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
async function cmdAttach(parsed) {
|
|
598
|
+
const spec = parsed.positionals[1];
|
|
599
|
+
if (!spec)
|
|
600
|
+
throw new Error("Usage: pi-mesh attach <session|session-file>");
|
|
601
|
+
const rawModelSelection = getModelSelection(parsed);
|
|
602
|
+
const mesh = await getMesh();
|
|
603
|
+
await refreshStaleManagedSessions(mesh, await listManagedSessions(mesh));
|
|
604
|
+
const resolved = await resolveSessionFile(mesh, spec);
|
|
605
|
+
assertNotAlreadyLive(resolved.managed, "attaching it again");
|
|
606
|
+
const folder = getString(parsed, "folder") ? await resolveFolder(getString(parsed, "folder")) : resolved.folder;
|
|
607
|
+
const pendingModelSelection = resolved.managed && !(await exists(resolved.managed.sessionFile)) ? resolved.managed.pendingModelSelection : undefined;
|
|
608
|
+
const seedModelSelection = mergeModelSelection(pendingModelSelection, rawModelSelection);
|
|
609
|
+
const modelSelection = await validateCliModelSelection(folder, seedModelSelection);
|
|
610
|
+
const name = getString(parsed, "name", resolved.managed?.name);
|
|
611
|
+
const labels = getLabels(parsed);
|
|
612
|
+
const meshId = resolved.managed?.meshId || createMeshId({ folder, sessionFile: resolved.sessionFile });
|
|
613
|
+
const socketPath = await socketPathFor(mesh, meshId);
|
|
614
|
+
let record = resolved.managed;
|
|
615
|
+
if (!resolved.managed) {
|
|
616
|
+
console.error("Note: attaching an unmanaged Pi session. Close any other Pi process using the same JSONL file first.");
|
|
617
|
+
}
|
|
618
|
+
const { runInteractive } = await import("./pi-runner.js");
|
|
619
|
+
await runInteractive({
|
|
620
|
+
cwd: folder,
|
|
621
|
+
sessionFile: resolved.sessionFile,
|
|
622
|
+
name,
|
|
623
|
+
socketPath,
|
|
624
|
+
modelSelection,
|
|
625
|
+
onSessionFile: async (sessionFile, rawSessionId) => {
|
|
626
|
+
if (!sessionFile)
|
|
627
|
+
return;
|
|
628
|
+
record = await registerSession(mesh, {
|
|
629
|
+
meshId,
|
|
630
|
+
name,
|
|
631
|
+
labels: labels.length ? labels : resolved.managed?.labels,
|
|
632
|
+
folder,
|
|
633
|
+
sessionFile,
|
|
634
|
+
rawSessionId,
|
|
635
|
+
kind: "attached",
|
|
636
|
+
status: "running",
|
|
637
|
+
socketPath,
|
|
638
|
+
pendingModelSelection: modelSelection && !(await exists(sessionFile)) ? modelSelection : undefined,
|
|
639
|
+
});
|
|
640
|
+
},
|
|
641
|
+
onMaterialized: async () => {
|
|
642
|
+
if (!record?.pendingModelSelection)
|
|
643
|
+
return;
|
|
644
|
+
record = await upsertManagedStatus(mesh, record, {});
|
|
645
|
+
},
|
|
646
|
+
onStatus: async (status, error) => {
|
|
647
|
+
if (!record)
|
|
648
|
+
return;
|
|
649
|
+
record = await upsertManagedStatus(mesh, record, { status, lastError: error, pid: process.pid, socketPath });
|
|
650
|
+
},
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
async function deliverToManagedSession(mesh, managed, message, delivery, rawModelSelection, stream) {
|
|
654
|
+
const sessionFileExists = await exists(managed.sessionFile);
|
|
655
|
+
const pendingModelSelection = !sessionFileExists ? managed.pendingModelSelection : undefined;
|
|
656
|
+
const usingPendingModelSelection = Boolean(pendingModelSelection);
|
|
657
|
+
const modelSelection = await validateCliModelSelection(managed.folder, mergeModelSelection(pendingModelSelection, rawModelSelection));
|
|
658
|
+
const socket = managed.socketPath;
|
|
659
|
+
if (socket && (await exists(socket))) {
|
|
660
|
+
const { sendToLiveSocket } = await import("./pi-runner.js");
|
|
661
|
+
try {
|
|
662
|
+
await sendToLiveSocket(socket, message, delivery, modelSelection);
|
|
663
|
+
if (usingPendingModelSelection)
|
|
664
|
+
managed = await upsertManagedSession(mesh, { ...managed, pendingModelSelection: undefined });
|
|
665
|
+
return { delivery: "live", session: managed };
|
|
666
|
+
}
|
|
667
|
+
catch (error) {
|
|
668
|
+
if (!isStaleSocketError(error))
|
|
669
|
+
throw error;
|
|
670
|
+
await fs.rm(socket, { force: true }).catch(() => undefined);
|
|
671
|
+
managed = await upsertManagedSession(mesh, {
|
|
672
|
+
...managed,
|
|
673
|
+
status: "offline",
|
|
674
|
+
socketPath: undefined,
|
|
675
|
+
pid: undefined,
|
|
676
|
+
lastError: undefined,
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
const lockPath = lockPathFor(mesh, managed.meshId);
|
|
681
|
+
let response;
|
|
682
|
+
await withDirectoryLock(lockPath, async () => {
|
|
683
|
+
const { runHeadlessTurn } = await import("./pi-runner.js");
|
|
684
|
+
await upsertManagedSession(mesh, { ...managed, status: "busy", lastError: undefined });
|
|
685
|
+
try {
|
|
686
|
+
const result = await runHeadlessTurn({
|
|
687
|
+
cwd: managed.folder,
|
|
688
|
+
sessionFile: managed.sessionFile,
|
|
689
|
+
message,
|
|
690
|
+
delivery: "prompt",
|
|
691
|
+
stream,
|
|
692
|
+
modelSelection,
|
|
693
|
+
});
|
|
694
|
+
const next = await upsertManagedSession(mesh, {
|
|
695
|
+
...managed,
|
|
696
|
+
sessionFile: result.sessionFile || managed.sessionFile,
|
|
697
|
+
rawSessionId: result.rawSessionId,
|
|
698
|
+
status: "offline",
|
|
699
|
+
lastError: undefined,
|
|
700
|
+
pendingModelSelection: undefined,
|
|
701
|
+
});
|
|
702
|
+
response = { delivery: "wake", session: next };
|
|
703
|
+
}
|
|
704
|
+
catch (error) {
|
|
705
|
+
await upsertManagedSession(mesh, { ...managed, status: "error", lastError: error.message });
|
|
706
|
+
throw error;
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
return response;
|
|
710
|
+
}
|
|
711
|
+
async function cmdSend(parsed) {
|
|
712
|
+
const positional = parsed.positionals.slice(1);
|
|
713
|
+
const hasSelectorOptions = Boolean(getString(parsed, "folder") || getString(parsed, "name") || getLabels(parsed).length);
|
|
714
|
+
let spec;
|
|
715
|
+
let message;
|
|
716
|
+
const messageOption = getString(parsed, "message", "");
|
|
717
|
+
if (positional.length >= 2) {
|
|
718
|
+
spec = positional[0];
|
|
719
|
+
message = positional.slice(1).join(" ").trim();
|
|
720
|
+
}
|
|
721
|
+
else if (positional.length === 1 && messageOption) {
|
|
722
|
+
spec = positional[0];
|
|
723
|
+
message = messageOption;
|
|
724
|
+
}
|
|
725
|
+
else {
|
|
726
|
+
message = positional[0] || messageOption;
|
|
727
|
+
}
|
|
728
|
+
if (!message)
|
|
729
|
+
throw new Error("Usage: pi-mesh send [<session>] <message>");
|
|
730
|
+
if (!spec && !hasSelectorOptions)
|
|
731
|
+
throw new Error("Usage: pi-mesh send [<session>] <message> [--folder <dir>] [--name <name>] [--label <label>]");
|
|
732
|
+
const rawModelSelection = getModelSelection(parsed);
|
|
733
|
+
const mesh = await getMesh();
|
|
734
|
+
await refreshStaleManagedSessions(mesh, await listManagedSessions(mesh));
|
|
735
|
+
const matches = await findManagedSessions(mesh, await getSessionSelector(parsed, spec));
|
|
736
|
+
if (!matches.length) {
|
|
737
|
+
if (spec) {
|
|
738
|
+
const session = await resolveSessionSpec(spec).catch(() => null);
|
|
739
|
+
if (session) {
|
|
740
|
+
throw new Error(`Session is readable but not managed: ${session.path}\nClose the original Pi TUI, then run: pi-mesh attach ${JSON.stringify(session.path)}`);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
throw new Error(`No managed session found for ${JSON.stringify(spec || "selector")}`);
|
|
744
|
+
}
|
|
745
|
+
if (matches.length > 1 && !getBool(parsed, "all")) {
|
|
746
|
+
throw new Error(`Multiple managed sessions match; refine with --folder, --name, --label, use a session id, or pass --all:\n${formatMatches(matches)}`);
|
|
747
|
+
}
|
|
748
|
+
const results = [];
|
|
749
|
+
for (const managed of matches) {
|
|
750
|
+
results.push(await deliverToManagedSession(mesh, managed, message, getDelivery(parsed), rawModelSelection, getBool(parsed, "stream")));
|
|
751
|
+
}
|
|
752
|
+
if (getBool(parsed, "json")) {
|
|
753
|
+
console.log(JSON.stringify({ ok: true, results, sessions: results.map((result) => result.session) }, null, 2));
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
for (const result of results) {
|
|
757
|
+
if (result.delivery === "live")
|
|
758
|
+
console.log(`Sent to live session ${result.session.meshId}`);
|
|
759
|
+
else
|
|
760
|
+
console.log(`Woke session ${result.session.meshId}, delivered message, and shut it down.`);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
async function main() {
|
|
764
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
765
|
+
const command = parsed.positionals[0];
|
|
766
|
+
if (!command || command === "help" || getBool(parsed, "help")) {
|
|
767
|
+
printHelp();
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
770
|
+
if (command === "sessions")
|
|
771
|
+
return cmdSessions(parsed);
|
|
772
|
+
if (command === "models")
|
|
773
|
+
return cmdModels(parsed);
|
|
774
|
+
if (command === "transcript")
|
|
775
|
+
return cmdTranscript(parsed);
|
|
776
|
+
if (command === "state")
|
|
777
|
+
return cmdState(parsed);
|
|
778
|
+
if (command === "spawn")
|
|
779
|
+
return cmdSpawn(parsed);
|
|
780
|
+
if (command === "run")
|
|
781
|
+
return cmdRun(parsed);
|
|
782
|
+
if (command === "attach")
|
|
783
|
+
return cmdAttach(parsed);
|
|
784
|
+
if (command === "send")
|
|
785
|
+
return cmdSend(parsed);
|
|
786
|
+
throw new Error(`Unknown command: ${command}`);
|
|
787
|
+
}
|
|
788
|
+
main().catch((error) => {
|
|
789
|
+
if (error.matches) {
|
|
790
|
+
console.error(error.message);
|
|
791
|
+
console.error(JSON.stringify({ matches: error.matches }, null, 2));
|
|
792
|
+
process.exit(2);
|
|
793
|
+
}
|
|
794
|
+
console.error(error.message || String(error));
|
|
795
|
+
process.exit(1);
|
|
796
|
+
});
|