@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
package/README.md
CHANGED
|
@@ -383,6 +383,39 @@ Secrets are **never** stored in the config file. The `db-tool` config references
|
|
|
383
383
|
}
|
|
384
384
|
```
|
|
385
385
|
|
|
386
|
+
Database VPN prerequisites can be set at the database profile or environment level. If an environment declares `vpn` or `prerequisites`, that environment config replaces the profile prerequisites; `prerequisites: []` explicitly disables inherited VPN setup. DB commands try the query directly first and only connect VPN prerequisites if direct access fails.
|
|
387
|
+
|
|
388
|
+
```json5
|
|
389
|
+
{
|
|
390
|
+
vpns: {
|
|
391
|
+
officeVpn: { name: "OfficeVPN" },
|
|
392
|
+
prodVpn: { name: "ProdVPN" },
|
|
393
|
+
},
|
|
394
|
+
database: {
|
|
395
|
+
default: {
|
|
396
|
+
vpn: "officeVpn",
|
|
397
|
+
environments: {
|
|
398
|
+
local: {
|
|
399
|
+
host: "127.0.0.1",
|
|
400
|
+
port: 5432,
|
|
401
|
+
user: "app",
|
|
402
|
+
database: "app",
|
|
403
|
+
prerequisites: [], // no VPN for local/direct access
|
|
404
|
+
},
|
|
405
|
+
prod: {
|
|
406
|
+
host: "db.prod.internal",
|
|
407
|
+
port: 5432,
|
|
408
|
+
user: "readonly",
|
|
409
|
+
database: "app",
|
|
410
|
+
passwordEnvVar: "AGENT_TOOLS_DB_PROD_PASSWORD",
|
|
411
|
+
vpn: "prodVpn", // overrides database.default.vpn
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
386
419
|
Set the values in your shell:
|
|
387
420
|
|
|
388
421
|
```bash
|
package/package.json
CHANGED
|
@@ -185,6 +185,17 @@
|
|
|
185
185
|
"passwordEnvVar": {
|
|
186
186
|
"description": "Name of the environment variable holding the database password.",
|
|
187
187
|
"type": "string"
|
|
188
|
+
},
|
|
189
|
+
"prerequisites": {
|
|
190
|
+
"description": "Ordered prerequisite references for this database environment. Declaring this or vpn overrides profile-level database prerequisites; an empty array explicitly disables inherited prerequisites.",
|
|
191
|
+
"type": "array",
|
|
192
|
+
"items": {
|
|
193
|
+
"$ref": "#/definitions/Prerequisite"
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
"vpn": {
|
|
197
|
+
"type": "string",
|
|
198
|
+
"description": "Convenience sugar for this database environment's VPN prerequisite key. Declaring this or prerequisites overrides profile-level database prerequisites."
|
|
188
199
|
}
|
|
189
200
|
},
|
|
190
201
|
"required": ["host", "port", "user", "database"]
|
package/src/config/loader.ts
CHANGED
|
@@ -91,6 +91,8 @@ const DbEnvConfigSchema = Schema.Struct({
|
|
|
91
91
|
database: Schema.String,
|
|
92
92
|
password: Schema.optionalKey(Schema.String),
|
|
93
93
|
passwordEnvVar: Schema.optionalKey(Schema.String),
|
|
94
|
+
prerequisites: Schema.optionalKey(PrerequisitesSchema),
|
|
95
|
+
vpn: Schema.optionalKey(Schema.String),
|
|
94
96
|
});
|
|
95
97
|
|
|
96
98
|
const DatabaseConfigSchema = Schema.Struct({
|
package/src/config/types.ts
CHANGED
package/src/db-tool/service.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { DbConfig, DbMutationOperation, QueryResult, SchemaMode } from "./t
|
|
|
6
6
|
import { ConfigService } from "#config";
|
|
7
7
|
import { isPrerequisiteRunError } from "#shared/prerequisites/errors";
|
|
8
8
|
import { resolveEnvTemplate } from "#shared/env-template";
|
|
9
|
+
import { resolveEnvironmentScopedPrerequisites } from "#shared/prerequisites/config";
|
|
9
10
|
import { runWithProfilePrerequisites } from "#shared/prerequisites/runtime";
|
|
10
11
|
import { DbConfigService, DbConfigServiceLayer, TUNNEL_CHECK_INTERVAL_MS } from "./config-service";
|
|
11
12
|
import {
|
|
@@ -228,13 +229,15 @@ export class DbService extends Context.Service<
|
|
|
228
229
|
|
|
229
230
|
const runWithVpnPrerequisites = <E>(
|
|
230
231
|
port: number,
|
|
232
|
+
prerequisiteConfig: DbConfig,
|
|
231
233
|
effect: Effect.Effect<QueryResult, E>,
|
|
232
234
|
): Effect.Effect<QueryResult, E | DbTunnelError> =>
|
|
233
235
|
runWithProfilePrerequisites(
|
|
234
236
|
agentToolsConfig ?? {},
|
|
235
|
-
|
|
237
|
+
prerequisiteConfig,
|
|
236
238
|
(command, _label) => executeShellCommand(command),
|
|
237
239
|
effect,
|
|
240
|
+
{ tryWithoutPrerequisites: true },
|
|
238
241
|
).pipe(
|
|
239
242
|
Effect.mapError((error) =>
|
|
240
243
|
isPrerequisiteRunError(error)
|
|
@@ -655,6 +658,7 @@ export class DbService extends Context.Service<
|
|
|
655
658
|
database: envConfig.database,
|
|
656
659
|
password: envConfig.password,
|
|
657
660
|
passwordEnvVar: envConfig.passwordEnvVar,
|
|
661
|
+
...resolveEnvironmentScopedPrerequisites(dbConfig, envConfig),
|
|
658
662
|
port: envConfig.port,
|
|
659
663
|
needsTunnel: accessMode.needsTunnel,
|
|
660
664
|
allowMutations: accessMode.allowMutations,
|
|
@@ -696,6 +700,7 @@ export class DbService extends Context.Service<
|
|
|
696
700
|
|
|
697
701
|
return yield* runWithVpnPrerequisites(
|
|
698
702
|
resolvedConfig.port,
|
|
703
|
+
resolvedConfig,
|
|
699
704
|
runQueryWithOptionalTunnel(resolvedConfig, queryEffect),
|
|
700
705
|
);
|
|
701
706
|
});
|
|
@@ -752,6 +757,7 @@ export class DbService extends Context.Service<
|
|
|
752
757
|
|
|
753
758
|
const result = yield* runWithVpnPrerequisites(
|
|
754
759
|
resolvedConfig.port,
|
|
760
|
+
resolvedConfig,
|
|
755
761
|
runQueryWithOptionalTunnel(resolvedConfig, queryEffect),
|
|
756
762
|
);
|
|
757
763
|
|
package/src/db-tool/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DbMutationOperation } from "#config";
|
|
2
|
+
import type { ProfilePrerequisites } from "#config/types";
|
|
2
3
|
import type { Environment, OutputFormat } from "#shared";
|
|
3
4
|
|
|
4
5
|
export type { DbMutationOperation };
|
|
@@ -6,7 +7,7 @@ export type { Environment, OutputFormat };
|
|
|
6
7
|
|
|
7
8
|
export type SchemaMode = "tables" | "columns" | "full" | "relationships";
|
|
8
9
|
|
|
9
|
-
export type DbConfig = {
|
|
10
|
+
export type DbConfig = ProfilePrerequisites & {
|
|
10
11
|
host: string;
|
|
11
12
|
user: string;
|
|
12
13
|
database: string;
|
|
@@ -119,6 +119,7 @@ export const prCreateCommand = Command.make(
|
|
|
119
119
|
export const prEditCommand = Command.make(
|
|
120
120
|
"edit",
|
|
121
121
|
{
|
|
122
|
+
base: Flag.string("base").pipe(Flag.withDescription("New base branch"), Flag.optional),
|
|
122
123
|
body: Flag.string("body").pipe(Flag.withDescription("New PR body/description"), Flag.optional),
|
|
123
124
|
bodyFile: Flag.string("body-file").pipe(
|
|
124
125
|
Flag.withDescription("Read PR body from a file path or '-' for stdin"),
|
|
@@ -128,7 +129,7 @@ export const prEditCommand = Command.make(
|
|
|
128
129
|
pr: Flag.integer("pr").pipe(Flag.withDescription("PR number to edit")),
|
|
129
130
|
title: Flag.string("title").pipe(Flag.withDescription("New PR title"), Flag.optional),
|
|
130
131
|
},
|
|
131
|
-
({ body, bodyFile, format, pr, title }) =>
|
|
132
|
+
({ base, body, bodyFile, format, pr, title }) =>
|
|
132
133
|
Effect.gen(function* () {
|
|
133
134
|
const resolvedBody = yield* resolveOptionalTextInput(
|
|
134
135
|
"gh-tool pr edit",
|
|
@@ -143,6 +144,7 @@ export const prEditCommand = Command.make(
|
|
|
143
144
|
pr,
|
|
144
145
|
title: Option.getOrNull(title),
|
|
145
146
|
body: resolvedBody,
|
|
147
|
+
base: Option.getOrNull(base),
|
|
146
148
|
});
|
|
147
149
|
yield* logFormatted(info, format);
|
|
148
150
|
}),
|
package/src/gh-tool/pr/core.ts
CHANGED
|
@@ -632,14 +632,15 @@ export const editPR = Effect.fn("pr.editPR")(function* (opts: {
|
|
|
632
632
|
pr: number;
|
|
633
633
|
title: string | null;
|
|
634
634
|
body: string | null;
|
|
635
|
+
base: string | null;
|
|
635
636
|
}) {
|
|
636
|
-
if (!opts.title && !opts.body) {
|
|
637
|
+
if (!opts.title && !opts.body && !opts.base) {
|
|
637
638
|
return yield* Effect.fail(
|
|
638
639
|
new GitHubCommandError({
|
|
639
640
|
command: "pr edit",
|
|
640
641
|
exitCode: 1,
|
|
641
|
-
stderr: "At least one of --title or --
|
|
642
|
-
message: "At least one of --title or --
|
|
642
|
+
stderr: "At least one of --title, --body, or --base must be provided",
|
|
643
|
+
message: "At least one of --title, --body, or --base must be provided",
|
|
643
644
|
}),
|
|
644
645
|
);
|
|
645
646
|
}
|
|
@@ -654,6 +655,9 @@ export const editPR = Effect.fn("pr.editPR")(function* (opts: {
|
|
|
654
655
|
if (opts.body) {
|
|
655
656
|
editArgs.push("--body", opts.body);
|
|
656
657
|
}
|
|
658
|
+
if (opts.base) {
|
|
659
|
+
editArgs.push("--base", opts.base);
|
|
660
|
+
}
|
|
657
661
|
|
|
658
662
|
yield* gh.runGh(editArgs);
|
|
659
663
|
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { Glob } from "bun";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
|
|
4
|
+
import type { MessageSummary } from "./types";
|
|
5
|
+
|
|
6
|
+
import { SessionReadError, SessionStorageNotFoundError, type SessionError } from "./errors";
|
|
7
|
+
|
|
8
|
+
export type CodexContentBlock =
|
|
9
|
+
| { type: "input_text"; text: string }
|
|
10
|
+
| { type: "output_text"; text: string }
|
|
11
|
+
| { type: string; text?: string };
|
|
12
|
+
|
|
13
|
+
export type CodexRecord =
|
|
14
|
+
| {
|
|
15
|
+
type: "session_meta";
|
|
16
|
+
timestamp: string;
|
|
17
|
+
payload: { id: string; cwd?: string; timestamp?: string };
|
|
18
|
+
}
|
|
19
|
+
| {
|
|
20
|
+
type: "event_msg";
|
|
21
|
+
timestamp: string;
|
|
22
|
+
payload: { type: "thread_name_updated"; thread_name: string };
|
|
23
|
+
}
|
|
24
|
+
| {
|
|
25
|
+
type: "response_item";
|
|
26
|
+
timestamp: string;
|
|
27
|
+
payload: {
|
|
28
|
+
type: "message";
|
|
29
|
+
role: "user" | "assistant" | "developer" | string;
|
|
30
|
+
content: ReadonlyArray<CodexContentBlock>;
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
35
|
+
typeof value === "object" && value !== null;
|
|
36
|
+
|
|
37
|
+
export const parseCodexLine = (line: string): CodexRecord | null => {
|
|
38
|
+
let parsed: unknown;
|
|
39
|
+
try {
|
|
40
|
+
parsed = JSON.parse(line);
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (
|
|
46
|
+
!isRecord(parsed) ||
|
|
47
|
+
typeof parsed.type !== "string" ||
|
|
48
|
+
typeof parsed.timestamp !== "string"
|
|
49
|
+
) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const payload = parsed.payload;
|
|
54
|
+
if (!isRecord(payload)) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (parsed.type === "session_meta" && typeof payload.id === "string") {
|
|
59
|
+
return {
|
|
60
|
+
type: "session_meta",
|
|
61
|
+
timestamp: parsed.timestamp,
|
|
62
|
+
payload: {
|
|
63
|
+
id: payload.id,
|
|
64
|
+
cwd: typeof payload.cwd === "string" ? payload.cwd : undefined,
|
|
65
|
+
timestamp: typeof payload.timestamp === "string" ? payload.timestamp : undefined,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (
|
|
71
|
+
parsed.type === "event_msg" &&
|
|
72
|
+
payload.type === "thread_name_updated" &&
|
|
73
|
+
typeof payload.thread_name === "string"
|
|
74
|
+
) {
|
|
75
|
+
return {
|
|
76
|
+
type: "event_msg",
|
|
77
|
+
timestamp: parsed.timestamp,
|
|
78
|
+
payload: { type: "thread_name_updated", thread_name: payload.thread_name },
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (
|
|
83
|
+
parsed.type === "response_item" &&
|
|
84
|
+
payload.type === "message" &&
|
|
85
|
+
typeof payload.role === "string" &&
|
|
86
|
+
Array.isArray(payload.content)
|
|
87
|
+
) {
|
|
88
|
+
return {
|
|
89
|
+
type: "response_item",
|
|
90
|
+
timestamp: parsed.timestamp,
|
|
91
|
+
payload: {
|
|
92
|
+
type: "message",
|
|
93
|
+
role: payload.role,
|
|
94
|
+
content: (payload.content as unknown[]).filter(
|
|
95
|
+
(item): item is CodexContentBlock =>
|
|
96
|
+
isRecord(item) && typeof (item as Record<string, unknown>).type === "string",
|
|
97
|
+
),
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return null;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export const extractCodexText = (content: ReadonlyArray<CodexContentBlock>): string =>
|
|
106
|
+
content
|
|
107
|
+
.filter(
|
|
108
|
+
(block): block is CodexContentBlock & { text: string } =>
|
|
109
|
+
(block.type === "input_text" || block.type === "output_text") &&
|
|
110
|
+
typeof block.text === "string",
|
|
111
|
+
)
|
|
112
|
+
.map((block) => block.text)
|
|
113
|
+
.join("\n");
|
|
114
|
+
|
|
115
|
+
export const extractCodexTitle = (records: ReadonlyArray<CodexRecord>): string => {
|
|
116
|
+
const named = records.find((record) => record.type === "event_msg");
|
|
117
|
+
if (named !== undefined && named.type === "event_msg") {
|
|
118
|
+
return named.payload.thread_name;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const firstUser = records.find(
|
|
122
|
+
(record) => record.type === "response_item" && record.payload.role === "user",
|
|
123
|
+
);
|
|
124
|
+
if (firstUser !== undefined && firstUser.type === "response_item") {
|
|
125
|
+
return extractCodexText(firstUser.payload.content).slice(0, 100);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return "Untitled session";
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const getSessionIdFromFile = (filePath: string): string => {
|
|
132
|
+
const fileName = filePath.split("/").pop() ?? "";
|
|
133
|
+
const match =
|
|
134
|
+
/rollout-.*?-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/u.exec(
|
|
135
|
+
fileName,
|
|
136
|
+
);
|
|
137
|
+
return match?.[1] ?? fileName.replace(/\.jsonl$/u, "");
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export const getCodexSessionId = getSessionIdFromFile;
|
|
141
|
+
|
|
142
|
+
const walkSessionFiles = (basePath: string): Promise<string[]> => {
|
|
143
|
+
const glob = new Glob("*/*/*/rollout-*.jsonl");
|
|
144
|
+
return Array.fromAsync(glob.scan({ cwd: basePath, absolute: true }));
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const readSessionMeta = async (
|
|
148
|
+
sessionFile: string,
|
|
149
|
+
): Promise<{ id: string; cwd?: string } | null> => {
|
|
150
|
+
try {
|
|
151
|
+
const text = await Bun.file(sessionFile).text();
|
|
152
|
+
const firstLine = text.split("\n")[0] ?? "";
|
|
153
|
+
const record = parseCodexLine(firstLine);
|
|
154
|
+
if (record !== null && record.type === "session_meta") {
|
|
155
|
+
return { id: record.payload.id, cwd: record.payload.cwd };
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
} catch {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export const getCodexSessions = (
|
|
164
|
+
basePath: string,
|
|
165
|
+
projectDir: string | null,
|
|
166
|
+
): Effect.Effect<string[], SessionError> =>
|
|
167
|
+
Effect.tryPromise({
|
|
168
|
+
try: async () => {
|
|
169
|
+
const allFiles = await walkSessionFiles(basePath);
|
|
170
|
+
if (projectDir === null) {
|
|
171
|
+
return allFiles;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const metas = await Promise.all(allFiles.map((file) => readSessionMeta(file)));
|
|
175
|
+
return allFiles.filter((_, i) => metas[i] !== null && metas[i]?.cwd === projectDir);
|
|
176
|
+
},
|
|
177
|
+
catch: (error) =>
|
|
178
|
+
new SessionStorageNotFoundError({
|
|
179
|
+
message: error instanceof Error ? error.message : "Codex storage directory not found",
|
|
180
|
+
path: basePath,
|
|
181
|
+
}),
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
export const readCodexMessages = (
|
|
185
|
+
sessionFiles: string[],
|
|
186
|
+
): Effect.Effect<MessageSummary[], SessionError> =>
|
|
187
|
+
Effect.tryPromise({
|
|
188
|
+
try: async () => {
|
|
189
|
+
const summaries: MessageSummary[] = [];
|
|
190
|
+
|
|
191
|
+
for (const sessionFile of sessionFiles) {
|
|
192
|
+
let fileContent: string;
|
|
193
|
+
try {
|
|
194
|
+
// eslint-disable-next-line eslint/no-await-in-loop -- sequential file read keeps memory bounded
|
|
195
|
+
fileContent = await Bun.file(sessionFile).text();
|
|
196
|
+
} catch {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const records = fileContent
|
|
201
|
+
.split(/\r?\n/u)
|
|
202
|
+
.map((line) => line.trim())
|
|
203
|
+
.filter((line) => line.length > 0)
|
|
204
|
+
.map(parseCodexLine)
|
|
205
|
+
.filter((record): record is CodexRecord => record !== null);
|
|
206
|
+
|
|
207
|
+
const title = extractCodexTitle(records);
|
|
208
|
+
const sessionID = getSessionIdFromFile(sessionFile);
|
|
209
|
+
|
|
210
|
+
for (const record of records) {
|
|
211
|
+
if (record.type !== "response_item") continue;
|
|
212
|
+
if (record.payload.role !== "user" && record.payload.role !== "assistant") continue;
|
|
213
|
+
|
|
214
|
+
const body = extractCodexText(record.payload.content);
|
|
215
|
+
if (body.length === 0) continue;
|
|
216
|
+
|
|
217
|
+
const createdTimestamp = new Date(record.timestamp).getTime();
|
|
218
|
+
summaries.push({
|
|
219
|
+
sessionID,
|
|
220
|
+
id: `${sessionID}:${record.timestamp}`,
|
|
221
|
+
title,
|
|
222
|
+
body,
|
|
223
|
+
created: Number.isFinite(createdTimestamp) ? createdTimestamp : 0,
|
|
224
|
+
role: record.payload.role,
|
|
225
|
+
source: "codex",
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return summaries.toSorted((left, right) => right.created - left.created);
|
|
231
|
+
},
|
|
232
|
+
catch: (error) =>
|
|
233
|
+
new SessionReadError({
|
|
234
|
+
message: error instanceof Error ? error.message : "Failed to read Codex sessions",
|
|
235
|
+
source: "codex",
|
|
236
|
+
}),
|
|
237
|
+
});
|
|
@@ -44,6 +44,7 @@ export class ResolvedPaths extends Context.Service<
|
|
|
44
44
|
readonly messagesPath: string;
|
|
45
45
|
readonly sessionsPath: string;
|
|
46
46
|
readonly claudeCodePath: string | null;
|
|
47
|
+
readonly codexPath: string | null;
|
|
47
48
|
}
|
|
48
49
|
>()("@agent-tools/ResolvedPaths") {}
|
|
49
50
|
|
|
@@ -54,6 +55,8 @@ export const ResolvedPathsLayer = Layer.effect(
|
|
|
54
55
|
const sessionsPath = yield* resolveSessionsPath;
|
|
55
56
|
const claudeCodeBasePath = join(homedir(), ".claude/projects");
|
|
56
57
|
const claudeCodePath = existsSync(claudeCodeBasePath) ? claudeCodeBasePath : null;
|
|
57
|
-
|
|
58
|
+
const codexBasePath = join(homedir(), ".codex/sessions");
|
|
59
|
+
const codexPath = existsSync(codexBasePath) ? codexBasePath : null;
|
|
60
|
+
return { messagesPath, sessionsPath, claudeCodePath, codexPath };
|
|
58
61
|
}),
|
|
59
62
|
);
|
|
@@ -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"),
|
|
25
|
+
Flag.withDescription("Filter by source: all, opencode, claude-code, codex"),
|
|
26
26
|
Flag.withDefault("all"),
|
|
27
27
|
);
|
|
28
28
|
|