@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.
@@ -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 = "both" | "opencode" | "claude-code";
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 "both";
58
+ return ALL_SOURCES;
54
59
  }
55
60
 
56
61
  const sessionId = filterSessions.values().next().value;
57
62
  if (typeof sessionId !== "string") {
58
- return "both";
63
+ return ALL_SOURCES;
59
64
  }
60
65
 
61
66
  if (sessionId.startsWith("ses_")) {
62
- return "opencode";
67
+ return OPENCODE_ONLY;
63
68
  }
64
69
 
65
70
  if (UUID_SESSION_ID_REGEX.test(sessionId)) {
66
- return "claude-code";
71
+ return UUID_SOURCES;
67
72
  }
68
73
 
69
- return "both";
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
- sourceFilter === "claude-code"
214
- ? []
215
- : yield* Effect.gen(function* () {
216
- const sessionDirs = yield* Effect.tryPromise({
217
- try: async () => {
218
- const dirs = await readdir(paths.messagesPath);
219
- return dirs
220
- .filter((name) => name.startsWith("ses_"))
221
- .filter((name) => filterSessions === null || filterSessions.has(name));
222
- },
223
- catch: () =>
224
- new SessionStorageNotFoundError({
225
- message: "Message storage directory not found",
226
- path: paths.messagesPath,
227
- }),
228
- });
229
-
230
- const summaries: MessageSummary[] = [];
231
-
232
- for (const sessionId of sessionDirs) {
233
- const sessionPath = `${paths.messagesPath}/${sessionId}`;
234
- const files = yield* readJsonFilesFlat(sessionPath);
235
-
236
- for (const { filePath, content } of files) {
237
- const parsed = parseJson<{
238
- id?: string;
239
- role?: string;
240
- sessionID?: string;
241
- summary?: {
242
- body?: string;
243
- title?: string;
244
- };
245
- time?: {
246
- created?: number;
247
- };
248
- }>(content);
249
-
250
- if (parsed === null || parsed.summary?.title === undefined) {
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
- return summaries;
267
- }).pipe(Effect.catchTag("SessionStorageNotFoundError", () => Effect.succeed([])));
286
+ return summaries;
287
+ }).pipe(Effect.catchTag("SessionStorageNotFoundError", () => Effect.succeed([])));
268
288
 
269
289
  const claudeSummaries =
270
- sourceFilter === "opencode" || paths.claudeCodePath === null
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 summaries = [...opencodeSummaries, ...claudeSummaries];
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 type SessionSource = "opencode" | "claude-code";
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
+ }