@blogic-cz/agent-tools 0.14.56 → 0.14.57
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/package.json
CHANGED
|
@@ -45,6 +45,7 @@ export class ResolvedPaths extends Context.Service<
|
|
|
45
45
|
readonly sessionsPath: string;
|
|
46
46
|
readonly claudeCodePath: string | null;
|
|
47
47
|
readonly codexPath: string | null;
|
|
48
|
+
readonly piPath: string | null;
|
|
48
49
|
}
|
|
49
50
|
>()("@agent-tools/ResolvedPaths") {}
|
|
50
51
|
|
|
@@ -57,6 +58,8 @@ export const ResolvedPathsLayer = Layer.effect(
|
|
|
57
58
|
const claudeCodePath = existsSync(claudeCodeBasePath) ? claudeCodeBasePath : null;
|
|
58
59
|
const codexBasePath = join(homedir(), ".codex/sessions");
|
|
59
60
|
const codexPath = existsSync(codexBasePath) ? codexBasePath : null;
|
|
60
|
-
|
|
61
|
+
const piBasePath = join(homedir(), ".pi/agent/sessions");
|
|
62
|
+
const piPath = existsSync(piBasePath) ? piBasePath : null;
|
|
63
|
+
return { messagesPath, sessionsPath, claudeCodePath, codexPath, piPath };
|
|
61
64
|
}),
|
|
62
65
|
);
|
|
@@ -22,7 +22,7 @@ import { formatDate, SessionService, SessionServiceLayer, truncate } from "./ser
|
|
|
22
22
|
const AppLayer = SessionServiceLayer.pipe(Layer.provideMerge(ResolvedPathsLayer));
|
|
23
23
|
|
|
24
24
|
const sourceOption = Flag.string("source").pipe(
|
|
25
|
-
Flag.withDescription("Filter by source: all, opencode, claude-code, codex"),
|
|
25
|
+
Flag.withDescription("Filter by source: all, opencode, claude-code, codex, pi"),
|
|
26
26
|
Flag.withDefault("all"),
|
|
27
27
|
);
|
|
28
28
|
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
|
|
3
|
+
import type { MessageSummary } from "./types";
|
|
4
|
+
|
|
5
|
+
import { SessionReadError, SessionStorageNotFoundError, type SessionError } from "./errors";
|
|
6
|
+
|
|
7
|
+
export type PiContentBlock =
|
|
8
|
+
| { type: "text"; text: string }
|
|
9
|
+
| { type: string; [key: string]: unknown };
|
|
10
|
+
|
|
11
|
+
export type PiRecord =
|
|
12
|
+
| { type: "session"; id: string; timestamp?: string; cwd?: string }
|
|
13
|
+
| {
|
|
14
|
+
type: "message";
|
|
15
|
+
timestamp: string;
|
|
16
|
+
message: { role: string; content: string | ReadonlyArray<PiContentBlock> };
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
20
|
+
typeof value === "object" && value !== null;
|
|
21
|
+
|
|
22
|
+
export const parsePiLine = (line: string): PiRecord | null => {
|
|
23
|
+
let parsed: unknown;
|
|
24
|
+
try {
|
|
25
|
+
parsed = JSON.parse(line);
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!isRecord(parsed) || typeof parsed.type !== "string") {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (parsed.type === "session" && typeof parsed.id === "string") {
|
|
35
|
+
return {
|
|
36
|
+
type: "session",
|
|
37
|
+
id: parsed.id,
|
|
38
|
+
timestamp: typeof parsed.timestamp === "string" ? parsed.timestamp : undefined,
|
|
39
|
+
cwd: typeof parsed.cwd === "string" ? parsed.cwd : undefined,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (
|
|
44
|
+
parsed.type === "message" &&
|
|
45
|
+
typeof parsed.timestamp === "string" &&
|
|
46
|
+
isRecord(parsed.message) &&
|
|
47
|
+
typeof parsed.message.role === "string" &&
|
|
48
|
+
(typeof parsed.message.content === "string" || Array.isArray(parsed.message.content))
|
|
49
|
+
) {
|
|
50
|
+
return parsed as Extract<PiRecord, { type: "message" }>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return null;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const extractPiText = (content: string | ReadonlyArray<PiContentBlock>): string => {
|
|
57
|
+
if (typeof content === "string") {
|
|
58
|
+
return content;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return content
|
|
62
|
+
.filter(
|
|
63
|
+
(block): block is { type: "text"; text: string } =>
|
|
64
|
+
block.type === "text" && typeof (block as { text?: unknown }).text === "string",
|
|
65
|
+
)
|
|
66
|
+
.map((block) => block.text)
|
|
67
|
+
.join("\n");
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export const extractPiTitle = (records: ReadonlyArray<PiRecord>): string => {
|
|
71
|
+
const firstUser = records.find(
|
|
72
|
+
(record): record is Extract<PiRecord, { type: "message" }> =>
|
|
73
|
+
record.type === "message" && record.message.role === "user",
|
|
74
|
+
);
|
|
75
|
+
if (firstUser !== undefined) {
|
|
76
|
+
return extractPiText(firstUser.message.content).slice(0, 100);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return "Untitled session";
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const SESSION_ID_REGEX =
|
|
83
|
+
/_([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\.jsonl$/u;
|
|
84
|
+
|
|
85
|
+
export const getPiSessionId = (filePath: string): string => {
|
|
86
|
+
const fileName = filePath.split("/").pop() ?? "";
|
|
87
|
+
const match = SESSION_ID_REGEX.exec(fileName);
|
|
88
|
+
return match?.[1] ?? fileName.replace(/\.jsonl$/u, "");
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const walkSessionFiles = async (basePath: string): Promise<string[]> => {
|
|
92
|
+
const { Glob } = await import("bun");
|
|
93
|
+
const glob = new Glob("*/*.jsonl");
|
|
94
|
+
return Array.fromAsync(glob.scan({ cwd: basePath, absolute: true }));
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const readSessionCwd = async (sessionFile: string): Promise<string | null> => {
|
|
98
|
+
try {
|
|
99
|
+
const text = await Bun.file(sessionFile).text();
|
|
100
|
+
const firstLine = text.split("\n")[0] ?? "";
|
|
101
|
+
const record = parsePiLine(firstLine);
|
|
102
|
+
if (record !== null && record.type === "session") {
|
|
103
|
+
return record.cwd ?? null;
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
} catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const getPiSessions = (
|
|
112
|
+
basePath: string,
|
|
113
|
+
projectDir: string | null,
|
|
114
|
+
): Effect.Effect<string[], SessionError> =>
|
|
115
|
+
Effect.tryPromise({
|
|
116
|
+
try: async () => {
|
|
117
|
+
const allFiles = await walkSessionFiles(basePath);
|
|
118
|
+
if (projectDir === null) {
|
|
119
|
+
return allFiles;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const cwds = await Promise.all(allFiles.map((file) => readSessionCwd(file)));
|
|
123
|
+
return allFiles.filter((_, i) => cwds[i] === projectDir);
|
|
124
|
+
},
|
|
125
|
+
catch: (error) =>
|
|
126
|
+
new SessionStorageNotFoundError({
|
|
127
|
+
message: error instanceof Error ? error.message : "pi storage directory not found",
|
|
128
|
+
path: basePath,
|
|
129
|
+
}),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
export const readPiMessages = (
|
|
133
|
+
sessionFiles: string[],
|
|
134
|
+
): Effect.Effect<MessageSummary[], SessionError> =>
|
|
135
|
+
Effect.tryPromise({
|
|
136
|
+
try: async () => {
|
|
137
|
+
const summaries: MessageSummary[] = [];
|
|
138
|
+
|
|
139
|
+
for (const sessionFile of sessionFiles) {
|
|
140
|
+
let fileContent: string;
|
|
141
|
+
try {
|
|
142
|
+
// eslint-disable-next-line eslint/no-await-in-loop -- sequential file read keeps memory bounded
|
|
143
|
+
fileContent = await Bun.file(sessionFile).text();
|
|
144
|
+
} catch {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const records = fileContent
|
|
149
|
+
.split(/\r?\n/u)
|
|
150
|
+
.map((line) => line.trim())
|
|
151
|
+
.filter((line) => line.length > 0)
|
|
152
|
+
.map(parsePiLine)
|
|
153
|
+
.filter((record): record is PiRecord => record !== null);
|
|
154
|
+
|
|
155
|
+
const title = extractPiTitle(records);
|
|
156
|
+
const sessionID = getPiSessionId(sessionFile);
|
|
157
|
+
|
|
158
|
+
for (const record of records) {
|
|
159
|
+
if (record.type !== "message") continue;
|
|
160
|
+
if (record.message.role !== "user" && record.message.role !== "assistant") continue;
|
|
161
|
+
|
|
162
|
+
const body = extractPiText(record.message.content);
|
|
163
|
+
if (body.length === 0) continue;
|
|
164
|
+
|
|
165
|
+
const createdTimestamp = new Date(record.timestamp).getTime();
|
|
166
|
+
summaries.push({
|
|
167
|
+
sessionID,
|
|
168
|
+
id: `${sessionID}:${record.timestamp}`,
|
|
169
|
+
title,
|
|
170
|
+
body,
|
|
171
|
+
created: Number.isFinite(createdTimestamp) ? createdTimestamp : 0,
|
|
172
|
+
role: record.message.role,
|
|
173
|
+
source: "pi",
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return (
|
|
179
|
+
summaries as MessageSummary[] & {
|
|
180
|
+
toSorted(
|
|
181
|
+
compareFn: (left: MessageSummary, right: MessageSummary) => number,
|
|
182
|
+
): MessageSummary[];
|
|
183
|
+
}
|
|
184
|
+
).toSorted((left, right) => right.created - left.created);
|
|
185
|
+
},
|
|
186
|
+
catch: (error) =>
|
|
187
|
+
new SessionReadError({
|
|
188
|
+
message: error instanceof Error ? error.message : "Failed to read pi sessions",
|
|
189
|
+
source: "pi",
|
|
190
|
+
}),
|
|
191
|
+
});
|
|
@@ -5,6 +5,7 @@ import type { MessageSummary, SessionInfo, SessionSource } from "./types";
|
|
|
5
5
|
|
|
6
6
|
import { getClaudeCodeSessions, readClaudeCodeMessages } from "./claude-code";
|
|
7
7
|
import { getCodexSessions, getCodexSessionId, readCodexMessages } from "./codex";
|
|
8
|
+
import { getPiSessions, getPiSessionId, readPiMessages } from "./pi";
|
|
8
9
|
import { ResolvedPaths } from "./config";
|
|
9
10
|
import { SessionReadError, SessionStorageNotFoundError, type SessionError } from "./errors";
|
|
10
11
|
|
|
@@ -41,8 +42,13 @@ type FileEntry = { filePath: string; content: string };
|
|
|
41
42
|
|
|
42
43
|
type SourceFilter = ReadonlySet<SessionSource>;
|
|
43
44
|
|
|
44
|
-
const ALL_SOURCES: SourceFilter = new Set<SessionSource>([
|
|
45
|
-
|
|
45
|
+
const ALL_SOURCES: SourceFilter = new Set<SessionSource>([
|
|
46
|
+
"opencode",
|
|
47
|
+
"claude-code",
|
|
48
|
+
"codex",
|
|
49
|
+
"pi",
|
|
50
|
+
]);
|
|
51
|
+
const UUID_SOURCES: SourceFilter = new Set<SessionSource>(["claude-code", "codex", "pi"]);
|
|
46
52
|
const OPENCODE_ONLY: SourceFilter = new Set<SessionSource>(["opencode"]);
|
|
47
53
|
|
|
48
54
|
const UUID_SESSION_ID_REGEX =
|
|
@@ -214,6 +220,18 @@ export class SessionService extends Context.Service<
|
|
|
214
220
|
),
|
|
215
221
|
);
|
|
216
222
|
|
|
223
|
+
const piSessions =
|
|
224
|
+
paths.piPath === null
|
|
225
|
+
? new Set<string>()
|
|
226
|
+
: yield* getPiSessions(paths.piPath, projectDir).pipe(
|
|
227
|
+
Effect.map(
|
|
228
|
+
(files) => new Set<string>(files.map((filePath) => getPiSessionId(filePath))),
|
|
229
|
+
),
|
|
230
|
+
Effect.catchTag("SessionStorageNotFoundError", () =>
|
|
231
|
+
Effect.succeed(new Set<string>()),
|
|
232
|
+
),
|
|
233
|
+
);
|
|
234
|
+
|
|
217
235
|
const matchingSessions = new Set<string>(opencodeSessions);
|
|
218
236
|
for (const sessionId of claudeSessions) {
|
|
219
237
|
matchingSessions.add(sessionId);
|
|
@@ -221,6 +239,9 @@ export class SessionService extends Context.Service<
|
|
|
221
239
|
for (const sessionId of codexSessions) {
|
|
222
240
|
matchingSessions.add(sessionId);
|
|
223
241
|
}
|
|
242
|
+
for (const sessionId of piSessions) {
|
|
243
|
+
matchingSessions.add(sessionId);
|
|
244
|
+
}
|
|
224
245
|
|
|
225
246
|
return matchingSessions;
|
|
226
247
|
}),
|
|
@@ -319,7 +340,30 @@ export class SessionService extends Context.Service<
|
|
|
319
340
|
}),
|
|
320
341
|
);
|
|
321
342
|
|
|
322
|
-
const
|
|
343
|
+
const piSummaries =
|
|
344
|
+
!sourceFilter.has("pi") || paths.piPath === null
|
|
345
|
+
? []
|
|
346
|
+
: yield* getPiSessions(paths.piPath, null).pipe(
|
|
347
|
+
Effect.map((sessionFiles) =>
|
|
348
|
+
filterSessions === null
|
|
349
|
+
? sessionFiles
|
|
350
|
+
: sessionFiles.filter((sessionFile) =>
|
|
351
|
+
filterSessions.has(getPiSessionId(sessionFile)),
|
|
352
|
+
),
|
|
353
|
+
),
|
|
354
|
+
Effect.flatMap(readPiMessages),
|
|
355
|
+
Effect.catchTags({
|
|
356
|
+
SessionStorageNotFoundError: () => Effect.succeed([]),
|
|
357
|
+
SessionReadError: () => Effect.succeed([]),
|
|
358
|
+
}),
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
const summaries = [
|
|
362
|
+
...opencodeSummaries,
|
|
363
|
+
...claudeSummaries,
|
|
364
|
+
...codexSummaries,
|
|
365
|
+
...piSummaries,
|
|
366
|
+
];
|
|
323
367
|
|
|
324
368
|
return (
|
|
325
369
|
summaries as MessageSummary[] & {
|
|
@@ -10,7 +10,7 @@ export type SessionInfo = {
|
|
|
10
10
|
projectID: string;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
export const SessionSourceLiterals = Schema.Literals(["opencode", "claude-code", "codex"]);
|
|
13
|
+
export const SessionSourceLiterals = Schema.Literals(["opencode", "claude-code", "codex", "pi"]);
|
|
14
14
|
export type SessionSource = Schema.Schema.Type<typeof SessionSourceLiterals>;
|
|
15
15
|
|
|
16
16
|
export type MessageSummary = {
|