@blogic-cz/agent-tools 0.14.15 → 0.14.21
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/README.md +33 -0
- package/package.json +1 -1
- package/schemas/agent-tools.schema.json +11 -0
- package/src/config/loader.ts +2 -0
- package/src/config/types.ts +1 -1
- package/src/db-tool/service.ts +7 -1
- package/src/db-tool/types.ts +2 -1
- package/src/gh-tool/pr/commands.ts +3 -1
- package/src/gh-tool/pr/core.ts +7 -3
- package/src/session-tool/codex.ts +237 -0
- package/src/session-tool/config.ts +4 -1
- package/src/session-tool/index.ts +1 -1
- package/src/session-tool/service.ts +100 -62
- package/src/session-tool/types.ts +4 -1
- package/src/shared/path.ts +15 -0
- package/src/shared/prerequisites/config.ts +19 -0
- package/src/shared/prerequisites/runtime.ts +468 -75
- package/src/shared/prerequisites/types.ts +29 -1
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Context, Effect, Layer } from "effect";
|
|
2
2
|
import { readdir } from "node:fs/promises";
|
|
3
3
|
|
|
4
|
-
import type { MessageSummary, SessionInfo } from "./types";
|
|
4
|
+
import type { MessageSummary, SessionInfo, SessionSource } from "./types";
|
|
5
5
|
|
|
6
6
|
import { getClaudeCodeSessions, readClaudeCodeMessages } from "./claude-code";
|
|
7
|
+
import { getCodexSessions, getCodexSessionId, readCodexMessages } from "./codex";
|
|
7
8
|
import { ResolvedPaths } from "./config";
|
|
8
9
|
import { SessionReadError, SessionStorageNotFoundError, type SessionError } from "./errors";
|
|
9
10
|
|
|
@@ -38,7 +39,11 @@ export const truncate = (value: string, maxLen: number): string => {
|
|
|
38
39
|
|
|
39
40
|
type FileEntry = { filePath: string; content: string };
|
|
40
41
|
|
|
41
|
-
type SourceFilter =
|
|
42
|
+
type SourceFilter = ReadonlySet<SessionSource>;
|
|
43
|
+
|
|
44
|
+
const ALL_SOURCES: SourceFilter = new Set<SessionSource>(["opencode", "claude-code", "codex"]);
|
|
45
|
+
const UUID_SOURCES: SourceFilter = new Set<SessionSource>(["claude-code", "codex"]);
|
|
46
|
+
const OPENCODE_ONLY: SourceFilter = new Set<SessionSource>(["opencode"]);
|
|
42
47
|
|
|
43
48
|
const UUID_SESSION_ID_REGEX =
|
|
44
49
|
/^[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}$/u;
|
|
@@ -50,23 +55,23 @@ const getSessionIdFromClaudeFile = (filePath: string): string => {
|
|
|
50
55
|
|
|
51
56
|
const detectSourceFilter = (filterSessions: Set<string> | null): SourceFilter => {
|
|
52
57
|
if (filterSessions === null || filterSessions.size !== 1) {
|
|
53
|
-
return
|
|
58
|
+
return ALL_SOURCES;
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
const sessionId = filterSessions.values().next().value;
|
|
57
62
|
if (typeof sessionId !== "string") {
|
|
58
|
-
return
|
|
63
|
+
return ALL_SOURCES;
|
|
59
64
|
}
|
|
60
65
|
|
|
61
66
|
if (sessionId.startsWith("ses_")) {
|
|
62
|
-
return
|
|
67
|
+
return OPENCODE_ONLY;
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
if (UUID_SESSION_ID_REGEX.test(sessionId)) {
|
|
66
|
-
return
|
|
71
|
+
return UUID_SOURCES;
|
|
67
72
|
}
|
|
68
73
|
|
|
69
|
-
return
|
|
74
|
+
return ALL_SOURCES;
|
|
70
75
|
};
|
|
71
76
|
|
|
72
77
|
/**
|
|
@@ -196,10 +201,26 @@ export class SessionService extends Context.Service<
|
|
|
196
201
|
),
|
|
197
202
|
);
|
|
198
203
|
|
|
204
|
+
const codexSessions =
|
|
205
|
+
paths.codexPath === null
|
|
206
|
+
? new Set<string>()
|
|
207
|
+
: yield* getCodexSessions(paths.codexPath, projectDir).pipe(
|
|
208
|
+
Effect.map(
|
|
209
|
+
(files) =>
|
|
210
|
+
new Set<string>(files.map((filePath) => getCodexSessionId(filePath))),
|
|
211
|
+
),
|
|
212
|
+
Effect.catchTag("SessionStorageNotFoundError", () =>
|
|
213
|
+
Effect.succeed(new Set<string>()),
|
|
214
|
+
),
|
|
215
|
+
);
|
|
216
|
+
|
|
199
217
|
const matchingSessions = new Set<string>(opencodeSessions);
|
|
200
218
|
for (const sessionId of claudeSessions) {
|
|
201
219
|
matchingSessions.add(sessionId);
|
|
202
220
|
}
|
|
221
|
+
for (const sessionId of codexSessions) {
|
|
222
|
+
matchingSessions.add(sessionId);
|
|
223
|
+
}
|
|
203
224
|
|
|
204
225
|
return matchingSessions;
|
|
205
226
|
}),
|
|
@@ -209,65 +230,64 @@ export class SessionService extends Context.Service<
|
|
|
209
230
|
) {
|
|
210
231
|
const sourceFilter = detectSourceFilter(filterSessions);
|
|
211
232
|
|
|
212
|
-
const opencodeSummaries =
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
continue;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
summaries.push({
|
|
255
|
-
sessionID: parsed.sessionID ?? sessionId,
|
|
256
|
-
id: parsed.id ?? filePath.split("/").pop()?.replace(".json", "") ?? "",
|
|
257
|
-
title: parsed.summary.title,
|
|
258
|
-
body: parsed.summary.body ?? "",
|
|
259
|
-
created: parsed.time?.created ?? 0,
|
|
260
|
-
role: parsed.role ?? "unknown",
|
|
261
|
-
source: "opencode",
|
|
262
|
-
});
|
|
233
|
+
const opencodeSummaries = !sourceFilter.has("opencode")
|
|
234
|
+
? []
|
|
235
|
+
: yield* Effect.gen(function* () {
|
|
236
|
+
const sessionDirs = yield* Effect.tryPromise({
|
|
237
|
+
try: async () => {
|
|
238
|
+
const dirs = await readdir(paths.messagesPath);
|
|
239
|
+
return dirs
|
|
240
|
+
.filter((name) => name.startsWith("ses_"))
|
|
241
|
+
.filter((name) => filterSessions === null || filterSessions.has(name));
|
|
242
|
+
},
|
|
243
|
+
catch: () =>
|
|
244
|
+
new SessionStorageNotFoundError({
|
|
245
|
+
message: "Message storage directory not found",
|
|
246
|
+
path: paths.messagesPath,
|
|
247
|
+
}),
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const summaries: MessageSummary[] = [];
|
|
251
|
+
|
|
252
|
+
for (const sessionId of sessionDirs) {
|
|
253
|
+
const sessionPath = `${paths.messagesPath}/${sessionId}`;
|
|
254
|
+
const files = yield* readJsonFilesFlat(sessionPath);
|
|
255
|
+
|
|
256
|
+
for (const { filePath, content } of files) {
|
|
257
|
+
const parsed = parseJson<{
|
|
258
|
+
id?: string;
|
|
259
|
+
role?: string;
|
|
260
|
+
sessionID?: string;
|
|
261
|
+
summary?: {
|
|
262
|
+
body?: string;
|
|
263
|
+
title?: string;
|
|
264
|
+
};
|
|
265
|
+
time?: {
|
|
266
|
+
created?: number;
|
|
267
|
+
};
|
|
268
|
+
}>(content);
|
|
269
|
+
|
|
270
|
+
if (parsed === null || parsed.summary?.title === undefined) {
|
|
271
|
+
continue;
|
|
263
272
|
}
|
|
273
|
+
|
|
274
|
+
summaries.push({
|
|
275
|
+
sessionID: parsed.sessionID ?? sessionId,
|
|
276
|
+
id: parsed.id ?? filePath.split("/").pop()?.replace(".json", "") ?? "",
|
|
277
|
+
title: parsed.summary.title,
|
|
278
|
+
body: parsed.summary.body ?? "",
|
|
279
|
+
created: parsed.time?.created ?? 0,
|
|
280
|
+
role: parsed.role ?? "unknown",
|
|
281
|
+
source: "opencode",
|
|
282
|
+
});
|
|
264
283
|
}
|
|
284
|
+
}
|
|
265
285
|
|
|
266
|
-
|
|
267
|
-
|
|
286
|
+
return summaries;
|
|
287
|
+
}).pipe(Effect.catchTag("SessionStorageNotFoundError", () => Effect.succeed([])));
|
|
268
288
|
|
|
269
289
|
const claudeSummaries =
|
|
270
|
-
sourceFilter
|
|
290
|
+
!sourceFilter.has("claude-code") || paths.claudeCodePath === null
|
|
271
291
|
? []
|
|
272
292
|
: yield* getClaudeCodeSessions(paths.claudeCodePath, null).pipe(
|
|
273
293
|
Effect.map((sessionFiles) =>
|
|
@@ -281,7 +301,25 @@ export class SessionService extends Context.Service<
|
|
|
281
301
|
Effect.catchTag("SessionStorageNotFoundError", () => Effect.succeed([])),
|
|
282
302
|
);
|
|
283
303
|
|
|
284
|
-
const
|
|
304
|
+
const codexSummaries =
|
|
305
|
+
!sourceFilter.has("codex") || paths.codexPath === null
|
|
306
|
+
? []
|
|
307
|
+
: yield* getCodexSessions(paths.codexPath, null).pipe(
|
|
308
|
+
Effect.map((sessionFiles) =>
|
|
309
|
+
filterSessions === null
|
|
310
|
+
? sessionFiles
|
|
311
|
+
: sessionFiles.filter((sessionFile) =>
|
|
312
|
+
filterSessions.has(getCodexSessionId(sessionFile)),
|
|
313
|
+
),
|
|
314
|
+
),
|
|
315
|
+
Effect.flatMap(readCodexMessages),
|
|
316
|
+
Effect.catchTags({
|
|
317
|
+
SessionStorageNotFoundError: () => Effect.succeed([]),
|
|
318
|
+
SessionReadError: () => Effect.succeed([]),
|
|
319
|
+
}),
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
const summaries = [...opencodeSummaries, ...claudeSummaries, ...codexSummaries];
|
|
285
323
|
|
|
286
324
|
return (
|
|
287
325
|
summaries as MessageSummary[] & {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
|
|
1
3
|
import type { OutputFormat } from "#shared";
|
|
2
4
|
|
|
3
5
|
export type { OutputFormat };
|
|
@@ -8,7 +10,8 @@ export type SessionInfo = {
|
|
|
8
10
|
projectID: string;
|
|
9
11
|
};
|
|
10
12
|
|
|
11
|
-
export
|
|
13
|
+
export const SessionSourceLiterals = Schema.Literals(["opencode", "claude-code", "codex"]);
|
|
14
|
+
export type SessionSource = Schema.Schema.Type<typeof SessionSourceLiterals>;
|
|
12
15
|
|
|
13
16
|
export type MessageSummary = {
|
|
14
17
|
sessionID: string;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const trimSlashes = (segment: string) => segment.replace(/^\/+|\/+$/g, "");
|
|
2
|
+
|
|
3
|
+
export const joinPath = (first: string, ...rest: readonly string[]) => {
|
|
4
|
+
const isAbsolute = first.startsWith("/");
|
|
5
|
+
const joined = [first, ...rest]
|
|
6
|
+
.map(trimSlashes)
|
|
7
|
+
.filter((segment) => segment !== "")
|
|
8
|
+
.join("/");
|
|
9
|
+
|
|
10
|
+
if (!isAbsolute) {
|
|
11
|
+
return joined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return joined === "" ? "/" : `/${joined}`;
|
|
15
|
+
};
|
|
@@ -36,3 +36,22 @@ export function resolveProfilePrerequisites(
|
|
|
36
36
|
|
|
37
37
|
return { success: true, prerequisites };
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
const hasOwnPrerequisiteConfig = (profile: ProfilePrerequisites, key: keyof ProfilePrerequisites) =>
|
|
41
|
+
Object.prototype.hasOwnProperty.call(profile, key);
|
|
42
|
+
|
|
43
|
+
export function resolveEnvironmentScopedPrerequisites(
|
|
44
|
+
profile: ProfilePrerequisites,
|
|
45
|
+
environment: ProfilePrerequisites,
|
|
46
|
+
): ProfilePrerequisites {
|
|
47
|
+
const source =
|
|
48
|
+
hasOwnPrerequisiteConfig(environment, "vpn") ||
|
|
49
|
+
hasOwnPrerequisiteConfig(environment, "prerequisites")
|
|
50
|
+
? environment
|
|
51
|
+
: profile;
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
...(source.vpn !== undefined ? { vpn: source.vpn } : {}),
|
|
55
|
+
...(source.prerequisites !== undefined ? { prerequisites: source.prerequisites } : {}),
|
|
56
|
+
};
|
|
57
|
+
}
|