@nordbyte/nordrelay 0.3.1 → 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/.env.example +45 -2
- package/README.md +204 -30
- package/dist/agent-activity.js +300 -0
- package/dist/agent-adapter.js +17 -30
- package/dist/agent-factory.js +27 -0
- package/dist/agent.js +123 -9
- package/dist/artifacts.js +1 -1
- package/dist/audit-log.js +1 -1
- package/dist/bot-ui.js +1 -1
- package/dist/bot.js +328 -159
- package/dist/claude-code-auth.js +121 -0
- package/dist/claude-code-cli.js +19 -0
- package/dist/claude-code-launch.js +73 -0
- package/dist/claude-code-session.js +660 -0
- package/dist/claude-code-state.js +590 -0
- package/dist/codex-session.js +12 -1
- package/dist/config.js +113 -9
- package/dist/hermes-api.js +150 -0
- package/dist/hermes-auth.js +96 -0
- package/dist/hermes-cli.js +19 -0
- package/dist/hermes-launch.js +57 -0
- package/dist/hermes-session.js +477 -0
- package/dist/hermes-state.js +609 -0
- package/dist/index.js +51 -8
- package/dist/openclaw-auth.js +27 -0
- package/dist/openclaw-cli.js +19 -0
- package/dist/openclaw-gateway.js +285 -0
- package/dist/openclaw-launch.js +65 -0
- package/dist/openclaw-session.js +549 -0
- package/dist/openclaw-state.js +409 -0
- package/dist/operations.js +83 -2
- package/dist/pi-auth.js +59 -0
- package/dist/pi-launch.js +61 -0
- package/dist/pi-rpc.js +18 -0
- package/dist/pi-session.js +103 -15
- package/dist/pi-state.js +253 -0
- package/dist/relay-runtime.js +673 -51
- package/dist/session-format.js +28 -18
- package/dist/session-registry.js +40 -15
- package/dist/settings-service.js +35 -4
- package/dist/web-dashboard-ui.js +18 -0
- package/dist/web-dashboard.js +329 -47
- package/package.json +8 -3
- package/plugins/nordrelay/.codex-plugin/plugin.json +7 -4
- package/plugins/nordrelay/commands/remote.md +2 -2
- package/plugins/nordrelay/scripts/nordrelay.mjs +131 -3
- package/plugins/nordrelay/skills/telegram-remote/SKILL.md +2 -2
- package/CHANGELOG.md +0 -26
package/dist/pi-session.js
CHANGED
|
@@ -2,7 +2,7 @@ import { spawnSync } from "node:child_process";
|
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { PI_AGENT_CAPABILITIES, PI_THINKING_LEVELS, } from "./agent.js";
|
|
5
|
-
import {
|
|
5
|
+
import { findPiLaunchProfile, listPiLaunchProfiles, piProfileAsLaunchProfile } from "./pi-launch.js";
|
|
6
6
|
import { resolvePiCli } from "./pi-cli.js";
|
|
7
7
|
import { PiRpcClient } from "./pi-rpc.js";
|
|
8
8
|
import { getPiSession, listPiSessions, listPiWorkspaces, readPiSessionRecord, resolvePiSessionDir, } from "./pi-state.js";
|
|
@@ -10,7 +10,7 @@ export class PiSessionService {
|
|
|
10
10
|
config;
|
|
11
11
|
sessionDir;
|
|
12
12
|
cliPath;
|
|
13
|
-
|
|
13
|
+
currentLaunchProfile;
|
|
14
14
|
rpc = null;
|
|
15
15
|
currentWorkspace;
|
|
16
16
|
currentThreadId = null;
|
|
@@ -31,13 +31,14 @@ export class PiSessionService {
|
|
|
31
31
|
this.currentWorkspace = config.workspace;
|
|
32
32
|
this.currentModel = config.piDefaultModel;
|
|
33
33
|
this.currentThinking = config.piDefaultThinking;
|
|
34
|
-
this.
|
|
34
|
+
this.currentLaunchProfile = findPiLaunchProfile(config.piDefaultLaunchProfileId);
|
|
35
35
|
}
|
|
36
36
|
static async create(config, options) {
|
|
37
37
|
const service = new PiSessionService(config);
|
|
38
38
|
service.currentWorkspace = options?.workspace ?? config.workspace;
|
|
39
39
|
service.currentModel = options?.model ?? config.piDefaultModel;
|
|
40
40
|
service.currentThinking = options?.reasoningEffort ?? config.piDefaultThinking;
|
|
41
|
+
service.currentLaunchProfile = findPiLaunchProfile(options?.launchProfileId ?? config.piDefaultLaunchProfileId);
|
|
41
42
|
if (options?.sessionPath) {
|
|
42
43
|
await service.switchSession(options.sessionPath);
|
|
43
44
|
return service;
|
|
@@ -59,9 +60,9 @@ export class PiSessionService {
|
|
|
59
60
|
workspace: this.currentWorkspace,
|
|
60
61
|
model: this.currentModel,
|
|
61
62
|
reasoningEffort: this.currentThinking,
|
|
62
|
-
launchProfileId:
|
|
63
|
-
launchProfileLabel:
|
|
64
|
-
launchProfileBehavior:
|
|
63
|
+
launchProfileId: this.currentLaunchProfile.id,
|
|
64
|
+
launchProfileLabel: this.currentLaunchProfile.label,
|
|
65
|
+
launchProfileBehavior: this.currentLaunchProfile.behavior,
|
|
65
66
|
sandboxMode: "host",
|
|
66
67
|
approvalPolicy: "never",
|
|
67
68
|
fastMode: false,
|
|
@@ -220,6 +221,9 @@ export class PiSessionService {
|
|
|
220
221
|
workspaces.add(this.config.workspace);
|
|
221
222
|
return [...workspaces].sort((left, right) => left.localeCompare(right));
|
|
222
223
|
}
|
|
224
|
+
async refreshModels() {
|
|
225
|
+
// Pi models are read from the CLI on each listModels() call.
|
|
226
|
+
}
|
|
223
227
|
listModels() {
|
|
224
228
|
const result = spawnSync(this.cliPath, ["--list-models"], {
|
|
225
229
|
cwd: this.currentWorkspace,
|
|
@@ -243,11 +247,17 @@ export class PiSessionService {
|
|
|
243
247
|
continue;
|
|
244
248
|
}
|
|
245
249
|
const slug = `${provider}/${model}`;
|
|
246
|
-
const
|
|
250
|
+
const contextWindow = parseCompactTokenCount(parts[2]);
|
|
251
|
+
const maxOutputTokens = parseCompactTokenCount(parts[3]);
|
|
252
|
+
const supportsThinking = parseYesNo(parts[4]);
|
|
253
|
+
const supportsImages = parseYesNo(parts[5]);
|
|
247
254
|
records.push({
|
|
248
255
|
slug,
|
|
249
256
|
displayName: slug,
|
|
250
|
-
...(
|
|
257
|
+
...(contextWindow !== undefined ? { maxInputTokens: contextWindow, contextWindow } : {}),
|
|
258
|
+
...(maxOutputTokens !== undefined ? { maxOutputTokens } : {}),
|
|
259
|
+
...(supportsThinking !== undefined ? { supportsThinking } : {}),
|
|
260
|
+
...(supportsImages !== undefined ? { supportsImages } : {}),
|
|
251
261
|
});
|
|
252
262
|
}
|
|
253
263
|
if (this.currentModel && !records.some((record) => record.slug === this.currentModel)) {
|
|
@@ -255,6 +265,9 @@ export class PiSessionService {
|
|
|
255
265
|
}
|
|
256
266
|
return records;
|
|
257
267
|
}
|
|
268
|
+
listLaunchProfiles() {
|
|
269
|
+
return listPiLaunchProfiles();
|
|
270
|
+
}
|
|
258
271
|
getSessionRecord(threadId) {
|
|
259
272
|
return getPiSession(threadId, { sessionDir: this.sessionDir });
|
|
260
273
|
}
|
|
@@ -297,21 +310,68 @@ export class PiSessionService {
|
|
|
297
310
|
}
|
|
298
311
|
return { value: level, appliedToActiveThread };
|
|
299
312
|
}
|
|
300
|
-
setLaunchProfile() {
|
|
301
|
-
|
|
313
|
+
setLaunchProfile(profileId) {
|
|
314
|
+
this.ensureIdle("change Pi profile");
|
|
315
|
+
this.currentLaunchProfile = findPiLaunchProfile(profileId);
|
|
316
|
+
this.restartRpcIfIdle();
|
|
317
|
+
return piProfileAsLaunchProfile(this.currentLaunchProfile);
|
|
302
318
|
}
|
|
303
319
|
setFastMode() {
|
|
304
320
|
throw new Error("Fast mode is only supported by Codex sessions");
|
|
305
321
|
}
|
|
306
322
|
getSelectedLaunchProfile() {
|
|
307
|
-
return this.
|
|
323
|
+
return piProfileAsLaunchProfile(this.currentLaunchProfile);
|
|
308
324
|
}
|
|
309
|
-
|
|
325
|
+
syncFromAgentState(options = {}) {
|
|
326
|
+
const before = {
|
|
327
|
+
threadId: this.currentThreadId,
|
|
328
|
+
workspace: this.currentWorkspace,
|
|
329
|
+
model: this.currentModel,
|
|
330
|
+
thinking: this.currentThinking,
|
|
331
|
+
sessionPath: this.currentSessionPath,
|
|
332
|
+
};
|
|
333
|
+
const changedFields = new Set();
|
|
334
|
+
const record = this.currentSessionPath
|
|
335
|
+
? readPiSessionRecord(this.currentSessionPath, path.basename(path.dirname(this.currentSessionPath)))
|
|
336
|
+
: this.currentThreadId
|
|
337
|
+
? getPiSession(this.currentThreadId, { sessionDir: this.sessionDir })
|
|
338
|
+
: null;
|
|
339
|
+
if (record) {
|
|
340
|
+
if (record.id !== this.currentThreadId)
|
|
341
|
+
changedFields.add("thread");
|
|
342
|
+
if (record.cwd && record.cwd !== this.currentWorkspace)
|
|
343
|
+
changedFields.add("workspace");
|
|
344
|
+
if (record.model && record.model !== this.currentModel)
|
|
345
|
+
changedFields.add("model");
|
|
346
|
+
if (record.reasoningEffort && record.reasoningEffort !== this.currentThinking)
|
|
347
|
+
changedFields.add("reasoning");
|
|
348
|
+
if (record.sessionPath !== this.currentSessionPath)
|
|
349
|
+
changedFields.add("sessionPath");
|
|
350
|
+
this.currentThreadId = record.id;
|
|
351
|
+
this.currentWorkspace = record.cwd || this.currentWorkspace;
|
|
352
|
+
this.currentSessionPath = record.sessionPath;
|
|
353
|
+
this.currentModel = record.model ?? this.currentModel;
|
|
354
|
+
this.currentThinking = record.reasoningEffort ?? this.currentThinking;
|
|
355
|
+
if (changedFields.has("thread") || changedFields.has("sessionPath")) {
|
|
356
|
+
this.cachedStats = {};
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const changed = changedFields.size > 0 ||
|
|
360
|
+
before.threadId !== this.currentThreadId ||
|
|
361
|
+
before.workspace !== this.currentWorkspace ||
|
|
362
|
+
before.model !== this.currentModel ||
|
|
363
|
+
before.thinking !== this.currentThinking ||
|
|
364
|
+
before.sessionPath !== this.currentSessionPath;
|
|
365
|
+
let reattached = false;
|
|
366
|
+
if (changed && options.reattach && this.rpc && !this.processing) {
|
|
367
|
+
this.restartRpc();
|
|
368
|
+
reattached = true;
|
|
369
|
+
}
|
|
310
370
|
return {
|
|
311
371
|
threadId: this.currentThreadId,
|
|
312
|
-
changed
|
|
313
|
-
reattached
|
|
314
|
-
changedFields: [],
|
|
372
|
+
changed,
|
|
373
|
+
reattached,
|
|
374
|
+
changedFields: [...changedFields],
|
|
315
375
|
info: this.getInfo(),
|
|
316
376
|
};
|
|
317
377
|
}
|
|
@@ -354,6 +414,7 @@ export class PiSessionService {
|
|
|
354
414
|
sessionPath: this.currentSessionPath,
|
|
355
415
|
model: this.currentModel,
|
|
356
416
|
thinking: this.currentThinking,
|
|
417
|
+
...this.currentLaunchProfile.cli,
|
|
357
418
|
env: { PI_CODING_AGENT_SESSION_DIR: this.sessionDir },
|
|
358
419
|
});
|
|
359
420
|
}
|
|
@@ -433,11 +494,25 @@ export class PiSessionService {
|
|
|
433
494
|
data: (await readFile(imagePath)).toString("base64"),
|
|
434
495
|
mimeType: mimeTypeForImage(imagePath),
|
|
435
496
|
})));
|
|
497
|
+
if (images.length > 0) {
|
|
498
|
+
const imageSupport = this.currentModelSupportsImages();
|
|
499
|
+
if (imageSupport === false) {
|
|
500
|
+
throw new Error(`Current Pi model does not support image input: ${this.currentModel}`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
436
503
|
return {
|
|
437
504
|
message: textParts.join("\n\n") || "Please inspect the attached file(s).",
|
|
438
505
|
...(images.length > 0 ? { images } : {}),
|
|
439
506
|
};
|
|
440
507
|
}
|
|
508
|
+
currentModelSupportsImages() {
|
|
509
|
+
const model = this.currentModel;
|
|
510
|
+
if (!model) {
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
const record = this.listModels().find((candidate) => candidate.slug === model || candidate.slug.endsWith(`/${model}`));
|
|
514
|
+
return record?.supportsImages ?? null;
|
|
515
|
+
}
|
|
441
516
|
refreshFromTurnEnd(event, callbacks) {
|
|
442
517
|
const message = objectValue(event.message);
|
|
443
518
|
const usage = objectValue(message?.usage);
|
|
@@ -556,6 +631,19 @@ function parseCompactTokenCount(value) {
|
|
|
556
631
|
const multiplier = unit === "M" ? 1_000_000 : unit === "K" ? 1_000 : unit === "B" ? 1_000_000_000 : 1;
|
|
557
632
|
return Math.round(number * multiplier);
|
|
558
633
|
}
|
|
634
|
+
function parseYesNo(value) {
|
|
635
|
+
if (!value) {
|
|
636
|
+
return undefined;
|
|
637
|
+
}
|
|
638
|
+
const normalized = value.toLowerCase();
|
|
639
|
+
if (["yes", "true", "1"].includes(normalized)) {
|
|
640
|
+
return true;
|
|
641
|
+
}
|
|
642
|
+
if (["no", "false", "0"].includes(normalized)) {
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
return undefined;
|
|
646
|
+
}
|
|
559
647
|
function mimeTypeForImage(filePath) {
|
|
560
648
|
switch (path.extname(filePath).toLowerCase()) {
|
|
561
649
|
case ".png":
|
package/dist/pi-state.js
CHANGED
|
@@ -48,6 +48,58 @@ export function getPiSession(idOrPath, options = {}) {
|
|
|
48
48
|
path.basename(record.sessionPath, ".jsonl").endsWith(`_${normalized}`));
|
|
49
49
|
return matches[0] ?? null;
|
|
50
50
|
}
|
|
51
|
+
export function getPiSessionActivity(idOrPath, options = {}) {
|
|
52
|
+
return getPiSessionSnapshot(idOrPath, { ...options, maxEvents: 0 })?.activity ?? null;
|
|
53
|
+
}
|
|
54
|
+
export function getPiSessionActivityLog(idOrPath, limit = 50, options = {}) {
|
|
55
|
+
const snapshot = getPiSessionSnapshot(idOrPath, { ...options, maxEvents: Math.max(1, limit) });
|
|
56
|
+
return snapshot?.events.slice(-Math.max(1, limit)) ?? [];
|
|
57
|
+
}
|
|
58
|
+
export function getPiSessionSnapshot(idOrPath, options = {}) {
|
|
59
|
+
const record = getPiSession(idOrPath, options);
|
|
60
|
+
if (!record) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return readPiSessionSnapshot(record, options);
|
|
64
|
+
}
|
|
65
|
+
export function getPiSessionDiagnostics(idOrPath, options = {}) {
|
|
66
|
+
const sessionDir = resolvePiSessionDir(options);
|
|
67
|
+
if (!idOrPath) {
|
|
68
|
+
return {
|
|
69
|
+
sessionDir,
|
|
70
|
+
sessionPath: null,
|
|
71
|
+
lineCount: 0,
|
|
72
|
+
status: "unavailable",
|
|
73
|
+
reason: "no active Pi session",
|
|
74
|
+
updatedAt: null,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const snapshot = getPiSessionSnapshot(idOrPath, { ...options, maxEvents: 0 });
|
|
78
|
+
if (!snapshot) {
|
|
79
|
+
return {
|
|
80
|
+
sessionDir,
|
|
81
|
+
sessionPath: null,
|
|
82
|
+
lineCount: 0,
|
|
83
|
+
status: "unavailable",
|
|
84
|
+
reason: "session file not found or unreadable",
|
|
85
|
+
updatedAt: null,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const status = snapshot.activity.active ? "active" : snapshot.activity.stale ? "stale" : "idle";
|
|
89
|
+
const reason = snapshot.activity.active
|
|
90
|
+
? "latest Pi user turn has no assistant response yet"
|
|
91
|
+
: snapshot.activity.stale
|
|
92
|
+
? "open Pi turn exceeded stale timeout"
|
|
93
|
+
: "latest Pi turn has a terminal response";
|
|
94
|
+
return {
|
|
95
|
+
sessionDir,
|
|
96
|
+
sessionPath: snapshot.sourcePath,
|
|
97
|
+
lineCount: snapshot.lineCount,
|
|
98
|
+
status,
|
|
99
|
+
reason,
|
|
100
|
+
updatedAt: snapshot.activity.updatedAt,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
51
103
|
export function listPiWorkspaces(options = {}) {
|
|
52
104
|
const workspaces = new Set();
|
|
53
105
|
for (const record of listPiSessions(500, options)) {
|
|
@@ -131,6 +183,178 @@ export function readPiSessionRecord(sessionPath, workspaceSlug) {
|
|
|
131
183
|
return null;
|
|
132
184
|
}
|
|
133
185
|
}
|
|
186
|
+
function readPiSessionSnapshot(record, options = {}) {
|
|
187
|
+
try {
|
|
188
|
+
const fileStat = statSync(record.sessionPath);
|
|
189
|
+
const lines = readFileSync(record.sessionPath, "utf8")
|
|
190
|
+
.split(/\r?\n/)
|
|
191
|
+
.filter(Boolean);
|
|
192
|
+
const parsed = parsePiActivityEvents(lines, record.id, 0);
|
|
193
|
+
const staleAfterMs = options.staleAfterMs ?? 5 * 60 * 1000;
|
|
194
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
195
|
+
const latestUser = [...parsed.events].reverse().find((event) => event.kind === "user");
|
|
196
|
+
const latestAgent = [...parsed.events].reverse().find((event) => event.kind === "agent");
|
|
197
|
+
const latestTerminal = [...parsed.events].reverse().find((event) => event.kind === "task" && event.status && event.status !== "started");
|
|
198
|
+
const latestTool = [...parsed.events].reverse().find((event) => event.kind === "tool" && event.toolName);
|
|
199
|
+
const latestTimestamp = parsed.latestTimestamp ?? fileStat.mtime;
|
|
200
|
+
const activeStartedAt = latestUser?.timestamp ?? null;
|
|
201
|
+
const hasAssistantAfterUser = Boolean(latestUser && latestAgent && latestAgent.lineNumber > latestUser.lineNumber);
|
|
202
|
+
const terminalAfterUser = Boolean(latestUser && latestTerminal && latestTerminal.lineNumber > latestUser.lineNumber);
|
|
203
|
+
const openTurn = Boolean(latestUser && !hasAssistantAfterUser && !terminalAfterUser);
|
|
204
|
+
const stale = openTurn && nowMs - latestTimestamp.getTime() > staleAfterMs;
|
|
205
|
+
const active = openTurn && !stale;
|
|
206
|
+
const turnId = latestUser?.turnId ?? latestTerminal?.turnId ?? null;
|
|
207
|
+
const maxEvents = options.maxEvents ?? 50;
|
|
208
|
+
const afterLine = options.afterLine ?? 0;
|
|
209
|
+
const events = maxEvents <= 0 ? [] : parsed.events.filter((event) => event.lineNumber > afterLine).slice(-maxEvents);
|
|
210
|
+
return {
|
|
211
|
+
agentId: "pi",
|
|
212
|
+
agentLabel: "Pi",
|
|
213
|
+
threadId: record.id,
|
|
214
|
+
sourcePath: record.sessionPath,
|
|
215
|
+
sourceLabel: "Pi session",
|
|
216
|
+
lineCount: lines.length,
|
|
217
|
+
activity: {
|
|
218
|
+
agentId: "pi",
|
|
219
|
+
agentLabel: "Pi",
|
|
220
|
+
threadId: record.id,
|
|
221
|
+
sourcePath: record.sessionPath,
|
|
222
|
+
sourceLabel: "Pi session",
|
|
223
|
+
active,
|
|
224
|
+
stale,
|
|
225
|
+
turnId,
|
|
226
|
+
startedAt: activeStartedAt,
|
|
227
|
+
updatedAt: latestTimestamp,
|
|
228
|
+
},
|
|
229
|
+
events,
|
|
230
|
+
latestAgentMessage: latestAgent?.text ?? null,
|
|
231
|
+
latestUserMessage: latestUser?.text ?? null,
|
|
232
|
+
latestToolName: latestTool?.toolName ?? null,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function parsePiActivityEvents(lines, sessionId, afterLine) {
|
|
240
|
+
const events = [];
|
|
241
|
+
let latestTimestamp = null;
|
|
242
|
+
let currentTurnId = null;
|
|
243
|
+
for (const [index, line] of lines.entries()) {
|
|
244
|
+
const lineNumber = index + 1;
|
|
245
|
+
const entry = safeJsonParse(line);
|
|
246
|
+
if (!entry) {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const timestamp = dateValue(entry.timestamp) ?? dateValue(objectValue(entry.message)?.timestamp);
|
|
250
|
+
if (timestamp && (!latestTimestamp || timestamp > latestTimestamp)) {
|
|
251
|
+
latestTimestamp = timestamp;
|
|
252
|
+
}
|
|
253
|
+
const type = stringValue(entry.type) ?? "entry";
|
|
254
|
+
if (type === "message") {
|
|
255
|
+
const message = objectValue(entry.message);
|
|
256
|
+
const role = stringValue(message?.role);
|
|
257
|
+
const text = message ? extractMessageText(message) : null;
|
|
258
|
+
if (role === "user") {
|
|
259
|
+
currentTurnId = `pi-${sessionId}-${lineNumber}`;
|
|
260
|
+
pushEvent(events, afterLine, {
|
|
261
|
+
lineNumber,
|
|
262
|
+
kind: "task",
|
|
263
|
+
timestamp,
|
|
264
|
+
type: "turn",
|
|
265
|
+
turnId: currentTurnId,
|
|
266
|
+
status: "started",
|
|
267
|
+
text,
|
|
268
|
+
toolName: null,
|
|
269
|
+
phase: null,
|
|
270
|
+
});
|
|
271
|
+
pushEvent(events, afterLine, {
|
|
272
|
+
lineNumber,
|
|
273
|
+
kind: "user",
|
|
274
|
+
timestamp,
|
|
275
|
+
type,
|
|
276
|
+
turnId: currentTurnId,
|
|
277
|
+
status: null,
|
|
278
|
+
text,
|
|
279
|
+
toolName: null,
|
|
280
|
+
phase: null,
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
else if (role === "assistant") {
|
|
284
|
+
pushEvent(events, afterLine, {
|
|
285
|
+
lineNumber,
|
|
286
|
+
kind: "agent",
|
|
287
|
+
timestamp,
|
|
288
|
+
type,
|
|
289
|
+
turnId: currentTurnId,
|
|
290
|
+
status: "completed",
|
|
291
|
+
text,
|
|
292
|
+
toolName: null,
|
|
293
|
+
phase: null,
|
|
294
|
+
});
|
|
295
|
+
pushEvent(events, afterLine, {
|
|
296
|
+
lineNumber,
|
|
297
|
+
kind: "task",
|
|
298
|
+
timestamp,
|
|
299
|
+
type: "turn",
|
|
300
|
+
turnId: currentTurnId,
|
|
301
|
+
status: "completed",
|
|
302
|
+
text: null,
|
|
303
|
+
toolName: null,
|
|
304
|
+
phase: null,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
else if (role === "tool") {
|
|
308
|
+
pushEvent(events, afterLine, {
|
|
309
|
+
lineNumber,
|
|
310
|
+
kind: "tool",
|
|
311
|
+
timestamp,
|
|
312
|
+
type,
|
|
313
|
+
turnId: currentTurnId,
|
|
314
|
+
status: "finished",
|
|
315
|
+
text,
|
|
316
|
+
toolName: stringValue(message?.name) ?? extractToolName(message) ?? "tool",
|
|
317
|
+
phase: null,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
if (/tool/i.test(type)) {
|
|
323
|
+
const status = /start/i.test(type) ? "started" : /error|fail/i.test(type) ? "failed" : /end|finish|complete/i.test(type) ? "finished" : null;
|
|
324
|
+
pushEvent(events, afterLine, {
|
|
325
|
+
lineNumber,
|
|
326
|
+
kind: "tool",
|
|
327
|
+
timestamp,
|
|
328
|
+
type,
|
|
329
|
+
turnId: currentTurnId,
|
|
330
|
+
status,
|
|
331
|
+
text: extractContentText(entry),
|
|
332
|
+
toolName: stringValue(entry.toolName) ?? stringValue(entry.name) ?? "tool",
|
|
333
|
+
phase: null,
|
|
334
|
+
});
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (/error|fail/i.test(type)) {
|
|
338
|
+
pushEvent(events, afterLine, {
|
|
339
|
+
lineNumber,
|
|
340
|
+
kind: "task",
|
|
341
|
+
timestamp,
|
|
342
|
+
type,
|
|
343
|
+
turnId: currentTurnId,
|
|
344
|
+
status: "failed",
|
|
345
|
+
text: stringValue(entry.error) ?? stringValue(entry.message),
|
|
346
|
+
toolName: null,
|
|
347
|
+
phase: null,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return { events, latestTimestamp };
|
|
352
|
+
}
|
|
353
|
+
function pushEvent(events, afterLine, event) {
|
|
354
|
+
if (event.lineNumber > afterLine) {
|
|
355
|
+
events.push(event);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
134
358
|
function safeReadDir(directory) {
|
|
135
359
|
try {
|
|
136
360
|
return readdirSync(directory);
|
|
@@ -217,6 +441,35 @@ function extractMessageText(message) {
|
|
|
217
441
|
.filter(Boolean);
|
|
218
442
|
return parts.join("\n").trim() || null;
|
|
219
443
|
}
|
|
444
|
+
function extractContentText(container) {
|
|
445
|
+
const direct = stringValue(container.text) ?? stringValue(container.error);
|
|
446
|
+
if (direct) {
|
|
447
|
+
return direct;
|
|
448
|
+
}
|
|
449
|
+
const content = container.content;
|
|
450
|
+
if (typeof content === "string") {
|
|
451
|
+
return content.trim() || null;
|
|
452
|
+
}
|
|
453
|
+
if (!Array.isArray(content)) {
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
const text = content
|
|
457
|
+
.map((entry) => {
|
|
458
|
+
const block = objectValue(entry);
|
|
459
|
+
return block ? stringValue(block.text) ?? stringValue(block.content) ?? "" : "";
|
|
460
|
+
})
|
|
461
|
+
.filter(Boolean)
|
|
462
|
+
.join("\n")
|
|
463
|
+
.trim();
|
|
464
|
+
return text || null;
|
|
465
|
+
}
|
|
466
|
+
function extractToolName(message) {
|
|
467
|
+
if (!message) {
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
const toolCall = objectValue(message.toolCall) ?? objectValue(message.tool_call);
|
|
471
|
+
return stringValue(toolCall?.name) ?? stringValue(toolCall?.toolName) ?? stringValue(message.toolName);
|
|
472
|
+
}
|
|
220
473
|
function summarizeTitle(text) {
|
|
221
474
|
if (!text) {
|
|
222
475
|
return null;
|