@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
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { compactWhitespace, exists, formatTimestamp, piAgentDir, readJson, safeJson, truncate, walkFiles, } from "./utils.js";
|
|
4
|
+
const DEFAULT_SESSION_ROOT = path.join(piAgentDir(), "sessions");
|
|
5
|
+
const LEGACY_INTERFACE_ROOT = path.join(piAgentDir(), "pi-agent-interface");
|
|
6
|
+
function defaultRegistryRoots() {
|
|
7
|
+
return [
|
|
8
|
+
path.join(LEGACY_INTERFACE_ROOT, "registry", "main-sessions"),
|
|
9
|
+
path.join(LEGACY_INTERFACE_ROOT, "registry", "subagents"),
|
|
10
|
+
path.join(LEGACY_INTERFACE_ROOT, "sessions"),
|
|
11
|
+
];
|
|
12
|
+
}
|
|
13
|
+
export function contentText(part, options = {}) {
|
|
14
|
+
if (!part || typeof part !== "object")
|
|
15
|
+
return "";
|
|
16
|
+
if (part.type === "text") {
|
|
17
|
+
const text = part.text;
|
|
18
|
+
return typeof text === "string" ? text : "";
|
|
19
|
+
}
|
|
20
|
+
if (part.type === "thinking") {
|
|
21
|
+
const thinking = part.thinking;
|
|
22
|
+
return options.includeThinking && typeof thinking === "string" ? thinking : "";
|
|
23
|
+
}
|
|
24
|
+
return "";
|
|
25
|
+
}
|
|
26
|
+
export function messageText(message, options = {}) {
|
|
27
|
+
if (!message)
|
|
28
|
+
return "";
|
|
29
|
+
if (typeof message.content === "string")
|
|
30
|
+
return message.content;
|
|
31
|
+
if (!Array.isArray(message.content))
|
|
32
|
+
return "";
|
|
33
|
+
return message.content
|
|
34
|
+
.map((part) => contentText(part, options))
|
|
35
|
+
.filter(Boolean)
|
|
36
|
+
.join("\n\n")
|
|
37
|
+
.trim();
|
|
38
|
+
}
|
|
39
|
+
function messageToolCalls(message) {
|
|
40
|
+
if (!Array.isArray(message?.content))
|
|
41
|
+
return [];
|
|
42
|
+
return message.content.filter((part) => part?.type === "toolCall");
|
|
43
|
+
}
|
|
44
|
+
function contentStat(message, type) {
|
|
45
|
+
if (!Array.isArray(message?.content))
|
|
46
|
+
return 0;
|
|
47
|
+
return message.content.filter((part) => part?.type === type).length;
|
|
48
|
+
}
|
|
49
|
+
async function loadRegistryHints(registryRoots = defaultRegistryRoots()) {
|
|
50
|
+
const bySessionId = new Map();
|
|
51
|
+
const bySessionFile = new Map();
|
|
52
|
+
for (const root of registryRoots) {
|
|
53
|
+
const files = await walkFiles(root, (file) => file.endsWith(".json"));
|
|
54
|
+
for (const file of files) {
|
|
55
|
+
const item = await readJson(file, null);
|
|
56
|
+
if (!item?.sessionId)
|
|
57
|
+
continue;
|
|
58
|
+
const hint = { ...item, _recordPath: file };
|
|
59
|
+
bySessionId.set(item.sessionId, hint);
|
|
60
|
+
if (item.sessionFile)
|
|
61
|
+
bySessionFile.set(item.sessionFile, hint);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return { bySessionId, bySessionFile };
|
|
65
|
+
}
|
|
66
|
+
export async function parseSessionSummary(filePath, hints) {
|
|
67
|
+
const stat = await fs.stat(filePath).catch(() => null);
|
|
68
|
+
if (!stat?.isFile())
|
|
69
|
+
return null;
|
|
70
|
+
const text = await fs.readFile(filePath, "utf8").catch(() => "");
|
|
71
|
+
if (!text)
|
|
72
|
+
return null;
|
|
73
|
+
const lines = text.split("\n").filter(Boolean);
|
|
74
|
+
let header = null;
|
|
75
|
+
let firstUser = "";
|
|
76
|
+
let lastUser = "";
|
|
77
|
+
let lastAssistant = "";
|
|
78
|
+
for (const line of lines) {
|
|
79
|
+
const item = safeJson(line, null);
|
|
80
|
+
if (!item)
|
|
81
|
+
continue;
|
|
82
|
+
if (!header && item.type === "session")
|
|
83
|
+
header = item;
|
|
84
|
+
if (item.type === "message" && item.message?.role === "user") {
|
|
85
|
+
const value = compactWhitespace(messageText(item.message));
|
|
86
|
+
if (!firstUser && value)
|
|
87
|
+
firstUser = value;
|
|
88
|
+
if (value)
|
|
89
|
+
lastUser = value;
|
|
90
|
+
}
|
|
91
|
+
if (item.type === "message" && item.message?.role === "assistant") {
|
|
92
|
+
const value = compactWhitespace(messageText(item.message));
|
|
93
|
+
if (value)
|
|
94
|
+
lastAssistant = value;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (!header?.id)
|
|
98
|
+
return null;
|
|
99
|
+
const hint = hints?.bySessionFile.get(filePath) || null;
|
|
100
|
+
const cwd = typeof header.cwd === "string" ? header.cwd : "";
|
|
101
|
+
return {
|
|
102
|
+
path: filePath,
|
|
103
|
+
rawSessionId: header.id,
|
|
104
|
+
cwd,
|
|
105
|
+
repoName: path.basename(cwd || path.dirname(filePath)),
|
|
106
|
+
timestamp: typeof header.timestamp === "string" ? header.timestamp : null,
|
|
107
|
+
updatedAt: stat.mtimeMs,
|
|
108
|
+
sessionName: hint?.sessionName || "",
|
|
109
|
+
dashboardSessionId: hint?.sessionId || "",
|
|
110
|
+
workflowId: hint?.workflowId || "",
|
|
111
|
+
providerId: hint?.providerId || "",
|
|
112
|
+
laneId: hint?.laneId || "",
|
|
113
|
+
firstUser,
|
|
114
|
+
lastUser,
|
|
115
|
+
lastAssistant,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
export function matchesSessionSummary(item, query = "") {
|
|
119
|
+
const needle = query.trim().toLowerCase();
|
|
120
|
+
if (!needle)
|
|
121
|
+
return true;
|
|
122
|
+
return [
|
|
123
|
+
item.path,
|
|
124
|
+
item.rawSessionId,
|
|
125
|
+
item.cwd,
|
|
126
|
+
item.repoName,
|
|
127
|
+
item.sessionName,
|
|
128
|
+
item.dashboardSessionId,
|
|
129
|
+
item.workflowId,
|
|
130
|
+
item.providerId,
|
|
131
|
+
item.laneId,
|
|
132
|
+
item.firstUser,
|
|
133
|
+
item.lastUser,
|
|
134
|
+
item.lastAssistant,
|
|
135
|
+
]
|
|
136
|
+
.filter(Boolean)
|
|
137
|
+
.some((value) => String(value).toLowerCase().includes(needle));
|
|
138
|
+
}
|
|
139
|
+
function looksLikeRawSessionId(value) {
|
|
140
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
141
|
+
}
|
|
142
|
+
async function findSessionFilesByRawId(rawSessionId, sessionRoot = DEFAULT_SESSION_ROOT) {
|
|
143
|
+
const suffix = `_${rawSessionId}.jsonl`;
|
|
144
|
+
const files = await walkFiles(sessionRoot, (file) => path.basename(file).endsWith(suffix));
|
|
145
|
+
const ranked = await Promise.all(files.map(async (file) => ({ file, stat: await fs.stat(file).catch(() => null) })));
|
|
146
|
+
return ranked
|
|
147
|
+
.filter((item) => item.stat?.isFile())
|
|
148
|
+
.sort((a, b) => (b.stat?.mtimeMs || 0) - (a.stat?.mtimeMs || 0))
|
|
149
|
+
.map((item) => item.file);
|
|
150
|
+
}
|
|
151
|
+
export async function searchSessions(options = {}) {
|
|
152
|
+
const query = options.query || "";
|
|
153
|
+
const limit = Math.max(1, Math.min(500, options.limit ?? 25));
|
|
154
|
+
const hints = await loadRegistryHints(options.registryRoots);
|
|
155
|
+
const files = await walkFiles(options.sessionRoot || DEFAULT_SESSION_ROOT, (file) => file.endsWith(".jsonl"));
|
|
156
|
+
const ranked = await Promise.all(files.map(async (file) => ({ file, stat: await fs.stat(file).catch(() => null) })));
|
|
157
|
+
const sorted = ranked
|
|
158
|
+
.filter((item) => item.stat?.isFile())
|
|
159
|
+
.sort((a, b) => (b.stat?.mtimeMs || 0) - (a.stat?.mtimeMs || 0));
|
|
160
|
+
const out = [];
|
|
161
|
+
for (const item of sorted) {
|
|
162
|
+
const parsed = await parseSessionSummary(item.file, hints);
|
|
163
|
+
if (!parsed || !matchesSessionSummary(parsed, query))
|
|
164
|
+
continue;
|
|
165
|
+
out.push(parsed);
|
|
166
|
+
if (out.length >= limit)
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
export async function resolveSessionSpec(spec, options = {}) {
|
|
172
|
+
const query = spec.trim();
|
|
173
|
+
if (!query)
|
|
174
|
+
throw new Error("session spec is required");
|
|
175
|
+
const directPath = path.resolve(query);
|
|
176
|
+
if ((query.endsWith(".jsonl") || query.includes(path.sep)) && (await exists(directPath))) {
|
|
177
|
+
const hints = await loadRegistryHints(options.registryRoots);
|
|
178
|
+
const parsed = await parseSessionSummary(directPath, hints);
|
|
179
|
+
if (!parsed)
|
|
180
|
+
throw new Error(`Could not parse session file ${directPath}`);
|
|
181
|
+
return parsed;
|
|
182
|
+
}
|
|
183
|
+
const hints = await loadRegistryHints(options.registryRoots);
|
|
184
|
+
if (hints.bySessionId.has(query)) {
|
|
185
|
+
const hint = hints.bySessionId.get(query);
|
|
186
|
+
if (hint?.sessionFile && (await exists(hint.sessionFile))) {
|
|
187
|
+
const parsed = await parseSessionSummary(hint.sessionFile, hints);
|
|
188
|
+
if (parsed)
|
|
189
|
+
return parsed;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (looksLikeRawSessionId(query)) {
|
|
193
|
+
const files = await findSessionFilesByRawId(query, options.sessionRoot || DEFAULT_SESSION_ROOT);
|
|
194
|
+
for (const file of files) {
|
|
195
|
+
const parsed = await parseSessionSummary(file, hints);
|
|
196
|
+
if (parsed?.rawSessionId === query)
|
|
197
|
+
return parsed;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const matches = await searchSessions({ ...options, query, limit: 20 });
|
|
201
|
+
const exactRaw = matches.find((item) => item.rawSessionId === query);
|
|
202
|
+
if (exactRaw)
|
|
203
|
+
return exactRaw;
|
|
204
|
+
const exactDashboard = matches.find((item) => item.dashboardSessionId === query);
|
|
205
|
+
if (exactDashboard)
|
|
206
|
+
return exactDashboard;
|
|
207
|
+
if (!matches.length)
|
|
208
|
+
throw new Error(`No Pi session found for ${JSON.stringify(query)}`);
|
|
209
|
+
if (matches.length > 1) {
|
|
210
|
+
const shortlist = matches.slice(0, 10).map((item) => ({
|
|
211
|
+
path: item.path,
|
|
212
|
+
rawSessionId: item.rawSessionId,
|
|
213
|
+
dashboardSessionId: item.dashboardSessionId,
|
|
214
|
+
sessionName: item.sessionName,
|
|
215
|
+
providerId: item.providerId,
|
|
216
|
+
workflowId: item.workflowId,
|
|
217
|
+
updatedAt: item.updatedAt,
|
|
218
|
+
lastUser: truncate(compactWhitespace(item.lastUser || item.firstUser || ""), 140),
|
|
219
|
+
}));
|
|
220
|
+
const error = new Error(`Multiple Pi sessions match ${JSON.stringify(query)}`);
|
|
221
|
+
error.matches = shortlist;
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
return matches[0];
|
|
225
|
+
}
|
|
226
|
+
export async function loadActiveBranch(sessionFile) {
|
|
227
|
+
const text = await fs.readFile(sessionFile, "utf8").catch(() => "");
|
|
228
|
+
if (!text)
|
|
229
|
+
throw new Error(`Empty session file: ${sessionFile}`);
|
|
230
|
+
const rows = [];
|
|
231
|
+
let header = null;
|
|
232
|
+
for (const line of text.split("\n").filter(Boolean)) {
|
|
233
|
+
const item = safeJson(line, null);
|
|
234
|
+
if (!item)
|
|
235
|
+
continue;
|
|
236
|
+
if (!header && item.type === "session") {
|
|
237
|
+
header = item;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
rows.push(item);
|
|
241
|
+
}
|
|
242
|
+
if (!header?.id)
|
|
243
|
+
throw new Error(`Invalid Pi session file: ${sessionFile}`);
|
|
244
|
+
const byId = new Map(rows.filter((item) => item.id).map((item) => [item.id, item]));
|
|
245
|
+
let activeLeaf = null;
|
|
246
|
+
for (let i = rows.length - 1; i >= 0; i -= 1) {
|
|
247
|
+
if (rows[i]?.id) {
|
|
248
|
+
activeLeaf = rows[i];
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const activeIds = new Set();
|
|
253
|
+
let cursor = activeLeaf;
|
|
254
|
+
while (cursor?.id && !activeIds.has(cursor.id)) {
|
|
255
|
+
activeIds.add(cursor.id);
|
|
256
|
+
cursor = cursor.parentId ? byId.get(cursor.parentId) || null : null;
|
|
257
|
+
}
|
|
258
|
+
const branchEntries = activeIds.size ? rows.filter((item) => !item.id || activeIds.has(item.id)) : rows;
|
|
259
|
+
return {
|
|
260
|
+
header: {
|
|
261
|
+
id: header.id,
|
|
262
|
+
version: typeof header.version === "number" ? header.version : undefined,
|
|
263
|
+
cwd: typeof header.cwd === "string" ? header.cwd : undefined,
|
|
264
|
+
timestamp: typeof header.timestamp === "string" ? header.timestamp : undefined,
|
|
265
|
+
},
|
|
266
|
+
entries: branchEntries,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
export function normalizeEvents(entries) {
|
|
270
|
+
const events = [];
|
|
271
|
+
for (const entry of entries) {
|
|
272
|
+
if (entry.type !== "message" || !entry.message?.role)
|
|
273
|
+
continue;
|
|
274
|
+
const role = entry.message.role;
|
|
275
|
+
const timestampMs = Date.parse(entry.timestamp || "") || null;
|
|
276
|
+
if (role === "user" || role === "assistant") {
|
|
277
|
+
const text = messageText(entry.message);
|
|
278
|
+
if (text) {
|
|
279
|
+
events.push({
|
|
280
|
+
kind: "message",
|
|
281
|
+
role,
|
|
282
|
+
id: entry.id || null,
|
|
283
|
+
parentId: entry.parentId || null,
|
|
284
|
+
timestamp: entry.timestamp || null,
|
|
285
|
+
timestampMs,
|
|
286
|
+
text,
|
|
287
|
+
thinkingParts: contentStat(entry.message, "thinking"),
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
for (const toolCall of messageToolCalls(entry.message)) {
|
|
291
|
+
events.push({
|
|
292
|
+
kind: "tool_call",
|
|
293
|
+
role: "assistant",
|
|
294
|
+
id: toolCall.id || null,
|
|
295
|
+
parentMessageId: entry.id || null,
|
|
296
|
+
timestamp: entry.timestamp || null,
|
|
297
|
+
timestampMs,
|
|
298
|
+
toolName: toolCall.name || "",
|
|
299
|
+
toolCallId: toolCall.id || null,
|
|
300
|
+
args: toolCall.arguments || {},
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (role === "toolResult") {
|
|
306
|
+
const text = messageText(entry.message, { includeThinking: false });
|
|
307
|
+
events.push({
|
|
308
|
+
kind: "tool_result",
|
|
309
|
+
role,
|
|
310
|
+
id: entry.id || null,
|
|
311
|
+
parentId: entry.parentId || null,
|
|
312
|
+
timestamp: entry.timestamp || null,
|
|
313
|
+
timestampMs,
|
|
314
|
+
toolName: entry.message.toolName || "",
|
|
315
|
+
toolCallId: entry.message.toolCallId || null,
|
|
316
|
+
text,
|
|
317
|
+
isError: Boolean(entry.message.isError),
|
|
318
|
+
details: entry.message.details,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
return events;
|
|
323
|
+
}
|
|
324
|
+
function extractExitCode(text) {
|
|
325
|
+
const match = String(text || "").match(/Command exited with code (\d+)/);
|
|
326
|
+
return match ? Number(match[1]) : null;
|
|
327
|
+
}
|
|
328
|
+
export function summarizeArgs(toolName, args) {
|
|
329
|
+
if (!args || typeof args !== "object")
|
|
330
|
+
return "";
|
|
331
|
+
if (toolName === "bash")
|
|
332
|
+
return String(args.command || "");
|
|
333
|
+
if (toolName === "read" || toolName === "write" || toolName === "edit")
|
|
334
|
+
return String(args.path || args.file_path || "");
|
|
335
|
+
return JSON.stringify(args);
|
|
336
|
+
}
|
|
337
|
+
export function buildToolInvocations(events) {
|
|
338
|
+
const out = [];
|
|
339
|
+
const byToolCallId = new Map();
|
|
340
|
+
for (const event of events) {
|
|
341
|
+
if (event.kind === "tool_call") {
|
|
342
|
+
const invocation = {
|
|
343
|
+
toolCallId: event.toolCallId || null,
|
|
344
|
+
toolName: event.toolName || "",
|
|
345
|
+
startedAt: event.timestamp,
|
|
346
|
+
startedAtMs: event.timestampMs,
|
|
347
|
+
args: event.args || {},
|
|
348
|
+
argsSummary: summarizeArgs(event.toolName, event.args),
|
|
349
|
+
callEvent: event,
|
|
350
|
+
resultEvent: null,
|
|
351
|
+
status: "pending",
|
|
352
|
+
failed: false,
|
|
353
|
+
exitCode: null,
|
|
354
|
+
};
|
|
355
|
+
out.push(invocation);
|
|
356
|
+
if (event.toolCallId)
|
|
357
|
+
byToolCallId.set(event.toolCallId, invocation);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (event.kind === "tool_result") {
|
|
361
|
+
let invocation = event.toolCallId ? byToolCallId.get(event.toolCallId) || null : null;
|
|
362
|
+
if (!invocation) {
|
|
363
|
+
invocation = {
|
|
364
|
+
toolCallId: event.toolCallId || null,
|
|
365
|
+
toolName: event.toolName || "",
|
|
366
|
+
startedAt: event.timestamp,
|
|
367
|
+
startedAtMs: event.timestampMs,
|
|
368
|
+
args: {},
|
|
369
|
+
argsSummary: "",
|
|
370
|
+
callEvent: null,
|
|
371
|
+
resultEvent: null,
|
|
372
|
+
status: "pending",
|
|
373
|
+
failed: false,
|
|
374
|
+
exitCode: null,
|
|
375
|
+
};
|
|
376
|
+
out.push(invocation);
|
|
377
|
+
if (event.toolCallId)
|
|
378
|
+
byToolCallId.set(event.toolCallId, invocation);
|
|
379
|
+
}
|
|
380
|
+
invocation.resultEvent = event;
|
|
381
|
+
invocation.endedAt = event.timestamp;
|
|
382
|
+
invocation.endedAtMs = event.timestampMs;
|
|
383
|
+
invocation.exitCode = extractExitCode(event.text);
|
|
384
|
+
invocation.failed = Boolean(event.isError);
|
|
385
|
+
invocation.status = invocation.failed ? "error" : "ok";
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return out;
|
|
389
|
+
}
|
|
390
|
+
export function buildTurns(events) {
|
|
391
|
+
const turns = [];
|
|
392
|
+
let current = null;
|
|
393
|
+
for (const event of events) {
|
|
394
|
+
if (event.kind === "message" && event.role === "user") {
|
|
395
|
+
if (current)
|
|
396
|
+
turns.push(finalizeTurn(current));
|
|
397
|
+
current = {
|
|
398
|
+
index: turns.length + 1,
|
|
399
|
+
startedAt: event.timestamp,
|
|
400
|
+
startedAtMs: event.timestampMs,
|
|
401
|
+
events: [event],
|
|
402
|
+
user: event,
|
|
403
|
+
};
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
if (!current)
|
|
407
|
+
continue;
|
|
408
|
+
current.events.push(event);
|
|
409
|
+
}
|
|
410
|
+
if (current)
|
|
411
|
+
turns.push(finalizeTurn(current));
|
|
412
|
+
return turns;
|
|
413
|
+
}
|
|
414
|
+
function finalizeTurn(turn) {
|
|
415
|
+
const toolInvocations = buildToolInvocations(turn.events);
|
|
416
|
+
const assistantMessages = turn.events.filter((event) => event.kind === "message" && event.role === "assistant");
|
|
417
|
+
const failures = toolInvocations.filter((item) => item.failed);
|
|
418
|
+
const lastEvent = turn.events[turn.events.length - 1] || turn.user;
|
|
419
|
+
return {
|
|
420
|
+
...turn,
|
|
421
|
+
endedAt: lastEvent.timestamp || turn.startedAt,
|
|
422
|
+
endedAtMs: lastEvent.timestampMs || turn.startedAtMs,
|
|
423
|
+
assistantMessages,
|
|
424
|
+
toolInvocations,
|
|
425
|
+
failures,
|
|
426
|
+
finalAssistant: assistantMessages[assistantMessages.length - 1] || null,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
export function inWindow(timestampMs, window = {}) {
|
|
430
|
+
if (!timestampMs)
|
|
431
|
+
return false;
|
|
432
|
+
if (window.since !== null && window.since !== undefined && timestampMs < window.since)
|
|
433
|
+
return false;
|
|
434
|
+
if (window.until !== null && window.until !== undefined && timestampMs > window.until)
|
|
435
|
+
return false;
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
export function tail(items, count) {
|
|
439
|
+
if (!count || count <= 0)
|
|
440
|
+
return items;
|
|
441
|
+
return items.slice(-count);
|
|
442
|
+
}
|
|
443
|
+
export async function loadSessionDataForSession(session) {
|
|
444
|
+
const transcript = await loadActiveBranch(session.path);
|
|
445
|
+
const events = normalizeEvents(transcript.entries);
|
|
446
|
+
const toolInvocations = buildToolInvocations(events);
|
|
447
|
+
const turns = buildTurns(events);
|
|
448
|
+
return { session, transcript, events, toolInvocations, turns };
|
|
449
|
+
}
|
|
450
|
+
export async function loadSessionData(spec, options = {}) {
|
|
451
|
+
const session = await resolveSessionSpec(spec, options);
|
|
452
|
+
return loadSessionDataForSession(session);
|
|
453
|
+
}
|
|
454
|
+
export function renderToolLine(item) {
|
|
455
|
+
const status = item.failed ? "ERROR" : item.status.toUpperCase();
|
|
456
|
+
const command = item.argsSummary ? `\n ${item.argsSummary}` : "";
|
|
457
|
+
const result = item.resultEvent?.text ? `\n result: ${truncate(compactWhitespace(item.resultEvent.text), 220)}` : "";
|
|
458
|
+
return `${formatTimestamp(item.startedAt)} · ${status} · ${item.toolName}${command}${result}`;
|
|
459
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ManagedSessionRecord, MeshPaths } from "./types.js";
|
|
2
|
+
export interface SessionSelector {
|
|
3
|
+
spec?: string;
|
|
4
|
+
folder?: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
labels?: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function normalizeMeshId(nameOrId: string): string;
|
|
9
|
+
export declare function normalizeLabel(label: string): string;
|
|
10
|
+
export declare function normalizeLabels(labels: Array<string | undefined>): string[];
|
|
11
|
+
export declare function createMeshId(input: {
|
|
12
|
+
folder: string;
|
|
13
|
+
sessionFile?: string;
|
|
14
|
+
rawSessionId?: string;
|
|
15
|
+
}): string;
|
|
16
|
+
export declare function listManagedSessions(mesh: MeshPaths): Promise<ManagedSessionRecord[]>;
|
|
17
|
+
export declare function sessionMatchesSelector(record: ManagedSessionRecord, selector: SessionSelector): boolean;
|
|
18
|
+
export declare function filterManagedSessions(records: ManagedSessionRecord[], selector: SessionSelector): ManagedSessionRecord[];
|
|
19
|
+
export declare function findManagedSessions(mesh: MeshPaths, selector: SessionSelector): Promise<ManagedSessionRecord[]>;
|
|
20
|
+
export declare function findManagedSession(mesh: MeshPaths, spec: string): Promise<ManagedSessionRecord | undefined>;
|
|
21
|
+
export declare function upsertManagedSession(mesh: MeshPaths, record: ManagedSessionRecord): Promise<ManagedSessionRecord>;
|
|
22
|
+
export declare function markManagedSession(mesh: MeshPaths, meshId: string, patch: Partial<ManagedSessionRecord>): Promise<ManagedSessionRecord | undefined>;
|
|
23
|
+
export declare function socketPathFor(mesh: MeshPaths, meshId: string): Promise<string>;
|
|
24
|
+
export declare function lockPathFor(mesh: MeshPaths, meshId: string): string;
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { compactWhitespace, ensureDir, ensurePrivateDir, safeJson, socketRuntimePrefix, stableId } from "./utils.js";
|
|
4
|
+
export function normalizeMeshId(nameOrId) {
|
|
5
|
+
return compactWhitespace(nameOrId)
|
|
6
|
+
.toLowerCase()
|
|
7
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
8
|
+
.replace(/^-+|-+$/g, "")
|
|
9
|
+
.slice(0, 80);
|
|
10
|
+
}
|
|
11
|
+
export function normalizeLabel(label) {
|
|
12
|
+
return normalizeMeshId(label);
|
|
13
|
+
}
|
|
14
|
+
export function normalizeLabels(labels) {
|
|
15
|
+
return [...new Set(labels.flatMap((label) => String(label || "").split(",")).map(normalizeLabel).filter(Boolean))].sort();
|
|
16
|
+
}
|
|
17
|
+
export function createMeshId(input) {
|
|
18
|
+
const stableSource = input.rawSessionId || input.sessionFile;
|
|
19
|
+
if (stableSource)
|
|
20
|
+
return `session-${stableId(stableSource, 12)}`;
|
|
21
|
+
return `session-${stableId(`${input.folder}:${Date.now()}:${Math.random()}`, 12)}`;
|
|
22
|
+
}
|
|
23
|
+
async function appendRegistryEvent(mesh, event) {
|
|
24
|
+
await fs.mkdir(path.dirname(mesh.registryFile), { recursive: true });
|
|
25
|
+
await fs.appendFile(mesh.registryFile, `${JSON.stringify(event)}\n`, "utf8");
|
|
26
|
+
}
|
|
27
|
+
function coerceRecord(record) {
|
|
28
|
+
const folder = record.folder || record.cwd || process.cwd();
|
|
29
|
+
const labels = normalizeLabels(record.labels ?? []);
|
|
30
|
+
const next = { ...record, folder, labels };
|
|
31
|
+
delete next.cwd;
|
|
32
|
+
return next;
|
|
33
|
+
}
|
|
34
|
+
export async function listManagedSessions(mesh) {
|
|
35
|
+
const text = await fs.readFile(mesh.registryFile, "utf8").catch(() => "");
|
|
36
|
+
const byId = new Map();
|
|
37
|
+
for (const line of text.split("\n")) {
|
|
38
|
+
if (!line.trim())
|
|
39
|
+
continue;
|
|
40
|
+
const event = safeJson(line, null);
|
|
41
|
+
if (!event)
|
|
42
|
+
continue;
|
|
43
|
+
if (event.type === "delete" && event.meshId)
|
|
44
|
+
byId.delete(event.meshId);
|
|
45
|
+
if (event.type === "upsert" && event.record?.meshId)
|
|
46
|
+
byId.set(event.record.meshId, coerceRecord(event.record));
|
|
47
|
+
}
|
|
48
|
+
return [...byId.values()].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
49
|
+
}
|
|
50
|
+
function recordMatchesSpec(record, spec) {
|
|
51
|
+
const normalized = normalizeMeshId(spec);
|
|
52
|
+
return record.meshId === spec ||
|
|
53
|
+
record.meshId === normalized ||
|
|
54
|
+
record.name === spec ||
|
|
55
|
+
(record.name ? normalizeMeshId(record.name) === normalized : false) ||
|
|
56
|
+
record.sessionFile === spec ||
|
|
57
|
+
record.rawSessionId === spec;
|
|
58
|
+
}
|
|
59
|
+
export function sessionMatchesSelector(record, selector) {
|
|
60
|
+
if (selector.spec && !recordMatchesSpec(record, selector.spec))
|
|
61
|
+
return false;
|
|
62
|
+
if (selector.folder && record.folder !== selector.folder)
|
|
63
|
+
return false;
|
|
64
|
+
if (selector.name && record.name !== selector.name && normalizeMeshId(record.name || "") !== normalizeMeshId(selector.name))
|
|
65
|
+
return false;
|
|
66
|
+
const labels = normalizeLabels(selector.labels ?? []);
|
|
67
|
+
if (labels.length) {
|
|
68
|
+
const existing = new Set(normalizeLabels(record.labels ?? []));
|
|
69
|
+
if (!labels.every((label) => existing.has(label)))
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
export function filterManagedSessions(records, selector) {
|
|
75
|
+
return records.filter((record) => sessionMatchesSelector(record, selector));
|
|
76
|
+
}
|
|
77
|
+
export async function findManagedSessions(mesh, selector) {
|
|
78
|
+
return filterManagedSessions(await listManagedSessions(mesh), selector);
|
|
79
|
+
}
|
|
80
|
+
export async function findManagedSession(mesh, spec) {
|
|
81
|
+
return (await findManagedSessions(mesh, { spec }))[0];
|
|
82
|
+
}
|
|
83
|
+
function sameUnderlyingSession(a, b) {
|
|
84
|
+
return Boolean((a.sessionFile && b.sessionFile && a.sessionFile === b.sessionFile) ||
|
|
85
|
+
(a.rawSessionId && b.rawSessionId && a.rawSessionId === b.rawSessionId));
|
|
86
|
+
}
|
|
87
|
+
export async function upsertManagedSession(mesh, record) {
|
|
88
|
+
const now = new Date().toISOString();
|
|
89
|
+
const normalized = coerceRecord(record);
|
|
90
|
+
const existing = (await listManagedSessions(mesh)).find((item) => item.meshId === normalized.meshId || sameUnderlyingSession(item, normalized));
|
|
91
|
+
const next = {
|
|
92
|
+
...normalized,
|
|
93
|
+
meshId: existing?.meshId || normalized.meshId,
|
|
94
|
+
createdAt: existing?.createdAt || normalized.createdAt || now,
|
|
95
|
+
updatedAt: now,
|
|
96
|
+
};
|
|
97
|
+
await appendRegistryEvent(mesh, { type: "upsert", record: next, timestamp: now });
|
|
98
|
+
return next;
|
|
99
|
+
}
|
|
100
|
+
export async function markManagedSession(mesh, meshId, patch) {
|
|
101
|
+
const current = await findManagedSession(mesh, meshId);
|
|
102
|
+
if (!current)
|
|
103
|
+
return undefined;
|
|
104
|
+
return upsertManagedSession(mesh, { ...current, ...patch });
|
|
105
|
+
}
|
|
106
|
+
async function createRuntimeSocketDir(mesh) {
|
|
107
|
+
await ensureDir(mesh.baseDir);
|
|
108
|
+
const root = await fs.mkdtemp(socketRuntimePrefix());
|
|
109
|
+
await ensurePrivateDir(root);
|
|
110
|
+
await fs.writeFile(mesh.socketDirFile, `${root}\n`, { mode: 0o600 });
|
|
111
|
+
return root;
|
|
112
|
+
}
|
|
113
|
+
async function ensureStoredRuntimeSocketDir(dir) {
|
|
114
|
+
try {
|
|
115
|
+
await ensurePrivateDir(dir);
|
|
116
|
+
return dir;
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (error.code === "ENOENT")
|
|
120
|
+
return undefined;
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function runtimeSocketDirFor(mesh) {
|
|
125
|
+
const stored = (await fs.readFile(mesh.socketDirFile, "utf8").catch(() => "")).trim();
|
|
126
|
+
if (stored) {
|
|
127
|
+
const dir = await ensureStoredRuntimeSocketDir(stored);
|
|
128
|
+
if (dir)
|
|
129
|
+
return dir;
|
|
130
|
+
}
|
|
131
|
+
return createRuntimeSocketDir(mesh);
|
|
132
|
+
}
|
|
133
|
+
export async function socketPathFor(mesh, meshId) {
|
|
134
|
+
const dir = await runtimeSocketDirFor(mesh);
|
|
135
|
+
return path.join(dir, `${stableId(meshId, 20)}.sock`);
|
|
136
|
+
}
|
|
137
|
+
export function lockPathFor(mesh, meshId) {
|
|
138
|
+
return path.join(mesh.locksDir, `${normalizeMeshId(meshId)}.lock`);
|
|
139
|
+
}
|