@franshjy/dbrief 0.1.0 → 0.2.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/README.md +30 -15
- package/dist/index.cjs +720 -259
- package/dist/index.js +703 -242
- package/package.json +9 -9
- package/skills/dbrief-note/SKILL.md +24 -24
package/dist/index.js
CHANGED
|
@@ -4,169 +4,11 @@
|
|
|
4
4
|
import { Command } from "commander";
|
|
5
5
|
|
|
6
6
|
// src/commands/extract.ts
|
|
7
|
-
import { join as
|
|
8
|
-
import { homedir } from "os";
|
|
7
|
+
import { join as join4 } from "path";
|
|
8
|
+
import { homedir as homedir3 } from "os";
|
|
9
9
|
import { writeFileSync, mkdirSync } from "fs";
|
|
10
10
|
import { dirname } from "path";
|
|
11
11
|
|
|
12
|
-
// src/extractor/parser.ts
|
|
13
|
-
import Database from "better-sqlite3";
|
|
14
|
-
import { createReadStream, existsSync } from "fs";
|
|
15
|
-
import { createInterface } from "readline";
|
|
16
|
-
function getEarliestSessionDate(dbPath) {
|
|
17
|
-
if (!existsSync(dbPath)) {
|
|
18
|
-
throw new Error(`Database not found: ${dbPath}`);
|
|
19
|
-
}
|
|
20
|
-
let db = null;
|
|
21
|
-
try {
|
|
22
|
-
db = new Database(dbPath, { readonly: true });
|
|
23
|
-
const row = db.prepare(`SELECT MIN(created_at_ms) as earliest FROM threads WHERE archived = 0`).get();
|
|
24
|
-
if (row.earliest === null) {
|
|
25
|
-
throw new Error("No threads found in database");
|
|
26
|
-
}
|
|
27
|
-
return row.earliest;
|
|
28
|
-
} catch (error) {
|
|
29
|
-
throw new Error(`Failed to read earliest session date from ${dbPath}: ${getErrorMessage(error)}`);
|
|
30
|
-
} finally {
|
|
31
|
-
db?.close();
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
function readThreadMetadata(dbPath) {
|
|
35
|
-
if (!existsSync(dbPath)) {
|
|
36
|
-
throw new Error(`Database not found: ${dbPath}`);
|
|
37
|
-
}
|
|
38
|
-
let db = null;
|
|
39
|
-
try {
|
|
40
|
-
db = new Database(dbPath, { readonly: true });
|
|
41
|
-
const rows = db.prepare(
|
|
42
|
-
`SELECT id, rollout_path, cwd, title, first_user_message,
|
|
43
|
-
created_at_ms, updated_at_ms, git_branch, git_sha,
|
|
44
|
-
git_origin_url, source, model, archived
|
|
45
|
-
FROM threads
|
|
46
|
-
WHERE archived = 0`
|
|
47
|
-
).all();
|
|
48
|
-
return rows;
|
|
49
|
-
} catch (error) {
|
|
50
|
-
throw new Error(`Failed to read thread metadata from ${dbPath}: ${getErrorMessage(error)}`);
|
|
51
|
-
} finally {
|
|
52
|
-
db?.close();
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
async function parseSessionFile(filePath, threadId, options = {}) {
|
|
56
|
-
const session = {
|
|
57
|
-
thread_id: threadId,
|
|
58
|
-
source_file: filePath,
|
|
59
|
-
cwd: null,
|
|
60
|
-
timezone: null,
|
|
61
|
-
context: [],
|
|
62
|
-
messages: [],
|
|
63
|
-
user_activity_timestamps: []
|
|
64
|
-
};
|
|
65
|
-
if (!existsSync(filePath)) {
|
|
66
|
-
options.onWarning?.({
|
|
67
|
-
type: "missing_file",
|
|
68
|
-
filePath,
|
|
69
|
-
threadId,
|
|
70
|
-
detail: "Session file does not exist"
|
|
71
|
-
});
|
|
72
|
-
return session;
|
|
73
|
-
}
|
|
74
|
-
const rl = createInterface({
|
|
75
|
-
input: createReadStream(filePath, { encoding: "utf-8" }),
|
|
76
|
-
crlfDelay: Infinity
|
|
77
|
-
});
|
|
78
|
-
try {
|
|
79
|
-
let lineNumber = 0;
|
|
80
|
-
for await (const line of rl) {
|
|
81
|
-
lineNumber += 1;
|
|
82
|
-
if (!line.trim()) continue;
|
|
83
|
-
let raw;
|
|
84
|
-
try {
|
|
85
|
-
raw = JSON.parse(line);
|
|
86
|
-
} catch {
|
|
87
|
-
options.onWarning?.({
|
|
88
|
-
type: "invalid_jsonl",
|
|
89
|
-
filePath,
|
|
90
|
-
threadId,
|
|
91
|
-
line: lineNumber,
|
|
92
|
-
detail: "Skipped invalid JSONL line"
|
|
93
|
-
});
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
if (raw.type === "turn_context" && raw.payload) {
|
|
97
|
-
session.cwd = raw.payload.cwd ?? session.cwd;
|
|
98
|
-
session.timezone = raw.payload.timezone ?? session.timezone;
|
|
99
|
-
}
|
|
100
|
-
if (raw.type === "compacted" && raw.payload) {
|
|
101
|
-
const replacementHistory = raw.payload.replacement_history;
|
|
102
|
-
if (replacementHistory && replacementHistory.length > 0) {
|
|
103
|
-
session.context = extractMessagesFromHistory(replacementHistory);
|
|
104
|
-
}
|
|
105
|
-
session.messages = [];
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
if (raw.type === "event_msg" && raw.payload) {
|
|
109
|
-
const eventType = raw.payload.type;
|
|
110
|
-
if (eventType === "user_message") {
|
|
111
|
-
const content = raw.payload.message ?? "";
|
|
112
|
-
const timestampMs = Date.parse(raw.timestamp);
|
|
113
|
-
if (!Number.isNaN(timestampMs)) {
|
|
114
|
-
session.user_activity_timestamps.push(timestampMs);
|
|
115
|
-
}
|
|
116
|
-
if (content) {
|
|
117
|
-
session.messages.push(["u", content]);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
if (raw.type === "response_item" && raw.payload) {
|
|
122
|
-
const payloadType = raw.payload.type;
|
|
123
|
-
if (payloadType === "message" && raw.payload.role === "assistant") {
|
|
124
|
-
const content = raw.payload.content;
|
|
125
|
-
if (Array.isArray(content)) {
|
|
126
|
-
const textParts = content.filter((c) => c.type === "output_text" && c.text).map((c) => c.text).join("\n");
|
|
127
|
-
if (textParts) {
|
|
128
|
-
session.messages.push(["a", textParts]);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
} catch (error) {
|
|
135
|
-
options.onWarning?.({
|
|
136
|
-
type: "read_error",
|
|
137
|
-
filePath,
|
|
138
|
-
threadId,
|
|
139
|
-
detail: `Failed to read session file: ${getErrorMessage(error)}`
|
|
140
|
-
});
|
|
141
|
-
} finally {
|
|
142
|
-
rl.close();
|
|
143
|
-
}
|
|
144
|
-
return session;
|
|
145
|
-
}
|
|
146
|
-
function extractMessagesFromHistory(items) {
|
|
147
|
-
const messages = [];
|
|
148
|
-
for (const item of items) {
|
|
149
|
-
const role = item.role;
|
|
150
|
-
const content = item.content;
|
|
151
|
-
if (role === "user" && typeof content === "string" && content.trim()) {
|
|
152
|
-
messages.push(["u", content.trim()]);
|
|
153
|
-
} else if (role === "assistant") {
|
|
154
|
-
if (typeof content === "string" && content.trim()) {
|
|
155
|
-
messages.push(["a", content.trim()]);
|
|
156
|
-
} else if (Array.isArray(content)) {
|
|
157
|
-
const textParts = content.filter((c) => c.type === "output_text" && c.text).map((c) => c.text).join("\n");
|
|
158
|
-
if (textParts.trim()) {
|
|
159
|
-
messages.push(["a", textParts.trim()]);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
return messages;
|
|
165
|
-
}
|
|
166
|
-
function getErrorMessage(error) {
|
|
167
|
-
return error instanceof Error ? error.message : String(error);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
12
|
// src/extractor/filter.ts
|
|
171
13
|
function getDayBoundaries(date, timezone) {
|
|
172
14
|
const dateStr = resolveDateStr(date);
|
|
@@ -208,9 +50,8 @@ function localToUTC(localStr, timezone) {
|
|
|
208
50
|
const offsetMs = tzAsUTC.getTime() - naiveNoMs.getTime();
|
|
209
51
|
return new Date(naiveNoMs.getTime() - offsetMs + ms);
|
|
210
52
|
}
|
|
211
|
-
function filterSessionsByActivity(sessions,
|
|
53
|
+
function filterSessionsByActivity(sessions, start, end) {
|
|
212
54
|
return sessions.filter((s) => {
|
|
213
|
-
if (!threadMetadata.has(s.thread_id)) return false;
|
|
214
55
|
return s.user_activity_timestamps.some((timestamp) => {
|
|
215
56
|
return timestamp >= start.getTime() && timestamp <= end.getTime();
|
|
216
57
|
});
|
|
@@ -275,7 +116,7 @@ function isValidIsoCalendarDate(dateStr) {
|
|
|
275
116
|
|
|
276
117
|
// src/utils/git.ts
|
|
277
118
|
import { execFileSync, spawnSync } from "child_process";
|
|
278
|
-
import { existsSync
|
|
119
|
+
import { existsSync, statSync } from "fs";
|
|
279
120
|
import { tmpdir } from "os";
|
|
280
121
|
import { delimiter, extname, isAbsolute, join } from "path";
|
|
281
122
|
function resolveGitRoot(cwd) {
|
|
@@ -302,7 +143,7 @@ function resolveGitExecutable() {
|
|
|
302
143
|
}
|
|
303
144
|
for (const executableName of executableNames) {
|
|
304
145
|
const candidate = join(dir, executableName);
|
|
305
|
-
if (!
|
|
146
|
+
if (!existsSync(candidate)) {
|
|
306
147
|
continue;
|
|
307
148
|
}
|
|
308
149
|
try {
|
|
@@ -352,7 +193,7 @@ function resolveSafeWorkingDirectory() {
|
|
|
352
193
|
const systemRoot = process.env.SystemRoot ?? process.env.windir;
|
|
353
194
|
if (systemRoot) {
|
|
354
195
|
const candidate = join(systemRoot, "System32");
|
|
355
|
-
if (
|
|
196
|
+
if (existsSync(candidate)) {
|
|
356
197
|
try {
|
|
357
198
|
if (statSync(candidate).isDirectory()) {
|
|
358
199
|
return candidate;
|
|
@@ -366,7 +207,7 @@ function resolveSafeWorkingDirectory() {
|
|
|
366
207
|
for (const candidate of candidates) {
|
|
367
208
|
if (!candidate) continue;
|
|
368
209
|
try {
|
|
369
|
-
if (
|
|
210
|
+
if (existsSync(candidate) && statSync(candidate).isDirectory()) {
|
|
370
211
|
return candidate;
|
|
371
212
|
}
|
|
372
213
|
} catch {
|
|
@@ -386,7 +227,7 @@ function resolveCommandProcessor() {
|
|
|
386
227
|
if (!candidate || !isAbsolute(candidate)) {
|
|
387
228
|
continue;
|
|
388
229
|
}
|
|
389
|
-
if (!
|
|
230
|
+
if (!existsSync(candidate)) {
|
|
390
231
|
continue;
|
|
391
232
|
}
|
|
392
233
|
try {
|
|
@@ -404,32 +245,34 @@ function quoteForCmd(value) {
|
|
|
404
245
|
}
|
|
405
246
|
|
|
406
247
|
// src/extractor/grouper.ts
|
|
407
|
-
function groupSessionsByProject(sessions
|
|
248
|
+
function groupSessionsByProject(sessions) {
|
|
408
249
|
const grouped = {};
|
|
409
250
|
const gitRootCache = /* @__PURE__ */ new Map();
|
|
410
251
|
for (const session of sessions) {
|
|
411
252
|
const cwd = session.cwd ?? "unknown";
|
|
412
|
-
let
|
|
413
|
-
if (
|
|
414
|
-
gitRoot =
|
|
415
|
-
|
|
253
|
+
let projectKey = session.project_root ?? null;
|
|
254
|
+
if (!projectKey) {
|
|
255
|
+
let gitRoot = gitRootCache.get(cwd);
|
|
256
|
+
if (gitRoot === void 0) {
|
|
257
|
+
gitRoot = resolveGitRoot(cwd);
|
|
258
|
+
gitRootCache.set(cwd, gitRoot);
|
|
259
|
+
}
|
|
260
|
+
projectKey = gitRoot ?? cwd;
|
|
416
261
|
}
|
|
417
|
-
const projectKey = gitRoot ?? cwd;
|
|
418
|
-
const metadata = threadMetadata.get(session.thread_id) ?? null;
|
|
419
262
|
if (!grouped[projectKey]) {
|
|
420
263
|
grouped[projectKey] = {
|
|
421
264
|
sessions: []
|
|
422
265
|
};
|
|
423
266
|
}
|
|
424
|
-
grouped[projectKey].sessions.push(
|
|
267
|
+
grouped[projectKey].sessions.push(session);
|
|
425
268
|
}
|
|
426
269
|
return grouped;
|
|
427
270
|
}
|
|
428
271
|
function buildProjectStructure(grouped) {
|
|
429
272
|
return Object.entries(grouped).map(([projectKey, data]) => {
|
|
430
|
-
const threads = data.sessions.map((
|
|
431
|
-
title:
|
|
432
|
-
branch:
|
|
273
|
+
const threads = data.sessions.map((session) => ({
|
|
274
|
+
title: session.title ?? session.thread_id,
|
|
275
|
+
branch: session.branch ?? null,
|
|
433
276
|
context: session.context,
|
|
434
277
|
messages: session.messages
|
|
435
278
|
}));
|
|
@@ -445,37 +288,570 @@ function getSystemTimezone() {
|
|
|
445
288
|
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
446
289
|
}
|
|
447
290
|
|
|
291
|
+
// src/sources/codex.ts
|
|
292
|
+
import { existsSync as existsSync3 } from "fs";
|
|
293
|
+
import { join as join2 } from "path";
|
|
294
|
+
import { homedir } from "os";
|
|
295
|
+
|
|
296
|
+
// src/extractor/parser.ts
|
|
297
|
+
import Database from "better-sqlite3";
|
|
298
|
+
import { createReadStream, existsSync as existsSync2 } from "fs";
|
|
299
|
+
import { createInterface } from "readline";
|
|
300
|
+
|
|
301
|
+
// src/sources/types.ts
|
|
302
|
+
function createEmptyParsedSession(candidate) {
|
|
303
|
+
return {
|
|
304
|
+
...candidate,
|
|
305
|
+
timezone: null,
|
|
306
|
+
context: [],
|
|
307
|
+
messages: [],
|
|
308
|
+
user_activity_timestamps: []
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// src/extractor/parser.ts
|
|
313
|
+
function getEarliestSessionDate(dbPath) {
|
|
314
|
+
if (!existsSync2(dbPath)) {
|
|
315
|
+
throw new Error(`Database not found: ${dbPath}`);
|
|
316
|
+
}
|
|
317
|
+
let db = null;
|
|
318
|
+
try {
|
|
319
|
+
db = new Database(dbPath, { readonly: true });
|
|
320
|
+
const row = db.prepare(`SELECT MIN(created_at_ms) as earliest FROM threads WHERE archived = 0`).get();
|
|
321
|
+
if (row.earliest === null) {
|
|
322
|
+
throw new Error("No threads found in database");
|
|
323
|
+
}
|
|
324
|
+
return row.earliest;
|
|
325
|
+
} catch (error) {
|
|
326
|
+
throw new Error(`Failed to read earliest session date from ${dbPath}: ${getErrorMessage(error)}`);
|
|
327
|
+
} finally {
|
|
328
|
+
db?.close();
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
function readThreadMetadata(dbPath) {
|
|
332
|
+
if (!existsSync2(dbPath)) {
|
|
333
|
+
throw new Error(`Database not found: ${dbPath}`);
|
|
334
|
+
}
|
|
335
|
+
let db = null;
|
|
336
|
+
try {
|
|
337
|
+
db = new Database(dbPath, { readonly: true });
|
|
338
|
+
const rows = db.prepare(
|
|
339
|
+
`SELECT id, rollout_path, cwd, title, first_user_message,
|
|
340
|
+
created_at_ms, updated_at_ms, git_branch, git_sha,
|
|
341
|
+
git_origin_url, source, model, archived
|
|
342
|
+
FROM threads
|
|
343
|
+
WHERE archived = 0`
|
|
344
|
+
).all();
|
|
345
|
+
return rows;
|
|
346
|
+
} catch (error) {
|
|
347
|
+
throw new Error(`Failed to read thread metadata from ${dbPath}: ${getErrorMessage(error)}`);
|
|
348
|
+
} finally {
|
|
349
|
+
db?.close();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
async function parseSessionFile(filePath, thread, options = {}) {
|
|
353
|
+
const candidate = typeof thread === "string" ? {
|
|
354
|
+
thread_id: thread,
|
|
355
|
+
source: "codex",
|
|
356
|
+
source_file: filePath,
|
|
357
|
+
cwd: null,
|
|
358
|
+
project_root: null,
|
|
359
|
+
title: null,
|
|
360
|
+
branch: null,
|
|
361
|
+
created_at_ms: 0,
|
|
362
|
+
updated_at_ms: 0,
|
|
363
|
+
archived: false
|
|
364
|
+
} : thread;
|
|
365
|
+
const session = createEmptyParsedSession({
|
|
366
|
+
...candidate,
|
|
367
|
+
source_file: filePath
|
|
368
|
+
});
|
|
369
|
+
if (!existsSync2(filePath)) {
|
|
370
|
+
options.onWarning?.({
|
|
371
|
+
source: session.source,
|
|
372
|
+
type: "missing_file",
|
|
373
|
+
filePath,
|
|
374
|
+
threadId: session.thread_id,
|
|
375
|
+
detail: "Session file does not exist"
|
|
376
|
+
});
|
|
377
|
+
return session;
|
|
378
|
+
}
|
|
379
|
+
const rl = createInterface({
|
|
380
|
+
input: createReadStream(filePath, { encoding: "utf-8" }),
|
|
381
|
+
crlfDelay: Infinity
|
|
382
|
+
});
|
|
383
|
+
try {
|
|
384
|
+
let lineNumber = 0;
|
|
385
|
+
for await (const line of rl) {
|
|
386
|
+
lineNumber += 1;
|
|
387
|
+
if (!line.trim()) continue;
|
|
388
|
+
let raw;
|
|
389
|
+
try {
|
|
390
|
+
raw = JSON.parse(line);
|
|
391
|
+
} catch {
|
|
392
|
+
options.onWarning?.({
|
|
393
|
+
source: session.source,
|
|
394
|
+
type: "invalid_jsonl",
|
|
395
|
+
filePath,
|
|
396
|
+
threadId: session.thread_id,
|
|
397
|
+
line: lineNumber,
|
|
398
|
+
detail: "Skipped invalid JSONL line"
|
|
399
|
+
});
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (raw.type === "turn_context" && raw.payload) {
|
|
403
|
+
session.cwd = raw.payload.cwd ?? session.cwd;
|
|
404
|
+
session.timezone = raw.payload.timezone ?? session.timezone;
|
|
405
|
+
}
|
|
406
|
+
if (raw.type === "compacted" && raw.payload) {
|
|
407
|
+
const replacementHistory = raw.payload.replacement_history;
|
|
408
|
+
if (replacementHistory && replacementHistory.length > 0) {
|
|
409
|
+
session.context = extractMessagesFromHistory(replacementHistory);
|
|
410
|
+
}
|
|
411
|
+
session.messages = [];
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (raw.type === "event_msg" && raw.payload) {
|
|
415
|
+
const eventType = raw.payload.type;
|
|
416
|
+
if (eventType === "user_message") {
|
|
417
|
+
const content = raw.payload.message ?? "";
|
|
418
|
+
const timestampMs = Date.parse(raw.timestamp);
|
|
419
|
+
if (!Number.isNaN(timestampMs)) {
|
|
420
|
+
session.user_activity_timestamps.push(timestampMs);
|
|
421
|
+
}
|
|
422
|
+
if (content) {
|
|
423
|
+
session.messages.push(["u", content]);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (raw.type === "response_item" && raw.payload) {
|
|
428
|
+
const payloadType = raw.payload.type;
|
|
429
|
+
if (payloadType === "message" && raw.payload.role === "assistant") {
|
|
430
|
+
const content = raw.payload.content;
|
|
431
|
+
if (Array.isArray(content)) {
|
|
432
|
+
const textParts = content.filter((c) => c.type === "output_text" && c.text).map((c) => c.text).join("\n");
|
|
433
|
+
if (textParts) {
|
|
434
|
+
session.messages.push(["a", textParts]);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
} catch (error) {
|
|
441
|
+
options.onWarning?.({
|
|
442
|
+
source: session.source,
|
|
443
|
+
type: "read_error",
|
|
444
|
+
filePath,
|
|
445
|
+
threadId: session.thread_id,
|
|
446
|
+
detail: `Failed to read session file: ${getErrorMessage(error)}`
|
|
447
|
+
});
|
|
448
|
+
} finally {
|
|
449
|
+
rl.close();
|
|
450
|
+
}
|
|
451
|
+
return session;
|
|
452
|
+
}
|
|
453
|
+
function extractMessagesFromHistory(items) {
|
|
454
|
+
const messages = [];
|
|
455
|
+
for (const item of items) {
|
|
456
|
+
const role = item.role;
|
|
457
|
+
const content = item.content;
|
|
458
|
+
if (role === "user" && typeof content === "string" && content.trim()) {
|
|
459
|
+
messages.push(["u", content.trim()]);
|
|
460
|
+
} else if (role === "assistant") {
|
|
461
|
+
if (typeof content === "string" && content.trim()) {
|
|
462
|
+
messages.push(["a", content.trim()]);
|
|
463
|
+
} else if (Array.isArray(content)) {
|
|
464
|
+
const textParts = content.filter((c) => c.type === "output_text" && c.text).map((c) => c.text).join("\n");
|
|
465
|
+
if (textParts.trim()) {
|
|
466
|
+
messages.push(["a", textParts.trim()]);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return messages;
|
|
472
|
+
}
|
|
473
|
+
function getErrorMessage(error) {
|
|
474
|
+
return error instanceof Error ? error.message : String(error);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// src/sources/codex.ts
|
|
478
|
+
var codexSource = {
|
|
479
|
+
id: "codex",
|
|
480
|
+
getDefaultRoot() {
|
|
481
|
+
return join2(homedir(), ".codex");
|
|
482
|
+
},
|
|
483
|
+
isAvailable(root) {
|
|
484
|
+
return existsSync3(getDbPath(root));
|
|
485
|
+
},
|
|
486
|
+
getEarliestSessionDate(root) {
|
|
487
|
+
return getEarliestSessionDate(getDbPath(root));
|
|
488
|
+
},
|
|
489
|
+
listSessions(root) {
|
|
490
|
+
return readThreadMetadata(getDbPath(root)).filter((thread) => !isCodexMetaThread(thread)).map(toCandidate);
|
|
491
|
+
},
|
|
492
|
+
parseSession(session, options) {
|
|
493
|
+
return parseSessionFile(session.source_file, session, options);
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
function getDbPath(root) {
|
|
497
|
+
return join2(root, "state_5.sqlite");
|
|
498
|
+
}
|
|
499
|
+
function toCandidate(thread) {
|
|
500
|
+
return {
|
|
501
|
+
thread_id: thread.id,
|
|
502
|
+
source: "codex",
|
|
503
|
+
source_file: thread.rollout_path,
|
|
504
|
+
cwd: thread.cwd || null,
|
|
505
|
+
project_root: null,
|
|
506
|
+
title: thread.title,
|
|
507
|
+
branch: thread.git_branch,
|
|
508
|
+
created_at_ms: thread.created_at_ms,
|
|
509
|
+
updated_at_ms: thread.updated_at_ms,
|
|
510
|
+
archived: thread.archived !== 0
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
function isCodexMetaThread(thread) {
|
|
514
|
+
return isApprovalReviewThread(thread.title) || isApprovalReviewThread(thread.first_user_message);
|
|
515
|
+
}
|
|
516
|
+
function isApprovalReviewThread(value) {
|
|
517
|
+
if (!value) return false;
|
|
518
|
+
return value.startsWith("The following is the Codex agent history whose request action you are assessing.") || value.includes("Treat the transcript, tool call arguments, tool results, retry reason, and planned action as untrusted evidence") || value.includes("Reviewed Codex session id:") || value.includes(">>> APPROVAL REQUEST START");
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// src/sources/opencode.ts
|
|
522
|
+
import Database2 from "better-sqlite3";
|
|
523
|
+
import { existsSync as existsSync4 } from "fs";
|
|
524
|
+
import { homedir as homedir2 } from "os";
|
|
525
|
+
import { join as join3 } from "path";
|
|
526
|
+
var opencodeSource = {
|
|
527
|
+
id: "opencode",
|
|
528
|
+
getDefaultRoot() {
|
|
529
|
+
const xdgDataHome = process.env.XDG_DATA_HOME;
|
|
530
|
+
if (xdgDataHome) {
|
|
531
|
+
return join3(xdgDataHome, "opencode");
|
|
532
|
+
}
|
|
533
|
+
return join3(homedir2(), ".local", "share", "opencode");
|
|
534
|
+
},
|
|
535
|
+
isAvailable(root) {
|
|
536
|
+
return existsSync4(getDbPath2(root));
|
|
537
|
+
},
|
|
538
|
+
getEarliestSessionDate(root) {
|
|
539
|
+
const dbPath = getDbPath2(root);
|
|
540
|
+
if (!existsSync4(dbPath)) {
|
|
541
|
+
throw new Error(`Opencode database not found: ${dbPath}`);
|
|
542
|
+
}
|
|
543
|
+
let db = null;
|
|
544
|
+
try {
|
|
545
|
+
db = new Database2(dbPath, { readonly: true });
|
|
546
|
+
const row = db.prepare("SELECT MIN(time_created) AS earliest FROM session WHERE time_archived IS NULL").get();
|
|
547
|
+
if (row.earliest === null) {
|
|
548
|
+
throw new Error(`No sessions found in Opencode database: ${dbPath}`);
|
|
549
|
+
}
|
|
550
|
+
return row.earliest;
|
|
551
|
+
} finally {
|
|
552
|
+
db?.close();
|
|
553
|
+
}
|
|
554
|
+
},
|
|
555
|
+
listSessions(root) {
|
|
556
|
+
const dbPath = getDbPath2(root);
|
|
557
|
+
if (!existsSync4(dbPath)) {
|
|
558
|
+
throw new Error(`Opencode database not found: ${dbPath}`);
|
|
559
|
+
}
|
|
560
|
+
let db = null;
|
|
561
|
+
try {
|
|
562
|
+
db = new Database2(dbPath, { readonly: true });
|
|
563
|
+
const rows = db.prepare(`
|
|
564
|
+
SELECT
|
|
565
|
+
s.id,
|
|
566
|
+
s.title,
|
|
567
|
+
s.directory,
|
|
568
|
+
s.time_created,
|
|
569
|
+
s.time_updated,
|
|
570
|
+
s.time_archived,
|
|
571
|
+
s.metadata,
|
|
572
|
+
p.worktree,
|
|
573
|
+
(
|
|
574
|
+
SELECT w.branch
|
|
575
|
+
FROM workspace w
|
|
576
|
+
WHERE w.project_id = s.project_id
|
|
577
|
+
AND w.branch IS NOT NULL
|
|
578
|
+
AND w.branch != ''
|
|
579
|
+
ORDER BY w.time_used DESC
|
|
580
|
+
LIMIT 1
|
|
581
|
+
) AS workspace_branch
|
|
582
|
+
FROM session s
|
|
583
|
+
JOIN project p ON p.id = s.project_id
|
|
584
|
+
WHERE s.time_archived IS NULL
|
|
585
|
+
`).all();
|
|
586
|
+
return rows.map((row) => toCandidate2(dbPath, row));
|
|
587
|
+
} finally {
|
|
588
|
+
db?.close();
|
|
589
|
+
}
|
|
590
|
+
},
|
|
591
|
+
async parseSession(session, options = {}) {
|
|
592
|
+
const parsed = createEmptyParsedSession(session);
|
|
593
|
+
const dbPath = session.source_file;
|
|
594
|
+
if (!existsSync4(dbPath)) {
|
|
595
|
+
options.onWarning?.({
|
|
596
|
+
source: "opencode",
|
|
597
|
+
type: "missing_file",
|
|
598
|
+
filePath: dbPath,
|
|
599
|
+
threadId: session.thread_id,
|
|
600
|
+
detail: "Opencode database does not exist"
|
|
601
|
+
});
|
|
602
|
+
return parsed;
|
|
603
|
+
}
|
|
604
|
+
let db = null;
|
|
605
|
+
try {
|
|
606
|
+
db = new Database2(dbPath, { readonly: true });
|
|
607
|
+
parsed.context = readCompactedContext(db, session.thread_id, dbPath, options);
|
|
608
|
+
const rows = db.prepare(`
|
|
609
|
+
SELECT
|
|
610
|
+
m.id AS message_id,
|
|
611
|
+
m.time_created AS message_time_created,
|
|
612
|
+
m.data AS message_data,
|
|
613
|
+
p.id AS part_id,
|
|
614
|
+
p.time_created AS part_time_created,
|
|
615
|
+
p.data AS part_data
|
|
616
|
+
FROM message m
|
|
617
|
+
LEFT JOIN part p ON p.message_id = m.id
|
|
618
|
+
WHERE m.session_id = ?
|
|
619
|
+
ORDER BY m.time_created ASC, p.time_created ASC, p.id ASC
|
|
620
|
+
`).all(session.thread_id);
|
|
621
|
+
for (const message of groupMessageRows(rows, dbPath, session.thread_id, options)) {
|
|
622
|
+
const role = getString(message.messageData?.role);
|
|
623
|
+
const createdAt = toFiniteNumber(asRecord(message.messageData?.time)?.created) ?? message.timeCreated;
|
|
624
|
+
const visibleText = message.parts.map((part) => extractVisibleText(part.data)).filter((value) => Boolean(value));
|
|
625
|
+
if (role === "user") {
|
|
626
|
+
if (visibleText.length > 0) {
|
|
627
|
+
parsed.messages.push(["u", visibleText.join("\n")]);
|
|
628
|
+
parsed.user_activity_timestamps.push(createdAt);
|
|
629
|
+
}
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
if (role === "assistant" && visibleText.length > 0) {
|
|
633
|
+
parsed.messages.push(["a", visibleText.join("\n")]);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return parsed;
|
|
637
|
+
} catch (error) {
|
|
638
|
+
options.onWarning?.({
|
|
639
|
+
source: "opencode",
|
|
640
|
+
type: "read_error",
|
|
641
|
+
filePath: dbPath,
|
|
642
|
+
threadId: session.thread_id,
|
|
643
|
+
detail: getErrorMessage2(error)
|
|
644
|
+
});
|
|
645
|
+
return parsed;
|
|
646
|
+
} finally {
|
|
647
|
+
db?.close();
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
};
|
|
651
|
+
function getDbPath2(root) {
|
|
652
|
+
return join3(root, "opencode.db");
|
|
653
|
+
}
|
|
654
|
+
function toCandidate2(dbPath, row) {
|
|
655
|
+
const metadata = parseJsonRecord(row.metadata);
|
|
656
|
+
const projectRoot = normalizeProjectRoot(row.worktree, row.directory);
|
|
657
|
+
return {
|
|
658
|
+
thread_id: row.id,
|
|
659
|
+
source: "opencode",
|
|
660
|
+
source_file: dbPath,
|
|
661
|
+
cwd: row.directory || null,
|
|
662
|
+
project_root: projectRoot,
|
|
663
|
+
title: row.title || row.id,
|
|
664
|
+
branch: row.workspace_branch ?? getString(metadata?.branch) ?? getString(metadata?.gitBranch) ?? null,
|
|
665
|
+
created_at_ms: row.time_created,
|
|
666
|
+
updated_at_ms: row.time_updated,
|
|
667
|
+
archived: row.time_archived !== null
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
function normalizeProjectRoot(worktree, directory) {
|
|
671
|
+
if (worktree && worktree !== "/") {
|
|
672
|
+
return worktree;
|
|
673
|
+
}
|
|
674
|
+
return directory || null;
|
|
675
|
+
}
|
|
676
|
+
function readCompactedContext(db, sessionId, dbPath, options) {
|
|
677
|
+
const row = db.prepare(`
|
|
678
|
+
SELECT baseline, snapshot
|
|
679
|
+
FROM session_context_epoch
|
|
680
|
+
WHERE session_id = ?
|
|
681
|
+
`).get(sessionId);
|
|
682
|
+
if (!row) {
|
|
683
|
+
return [];
|
|
684
|
+
}
|
|
685
|
+
const tuples = [];
|
|
686
|
+
for (const field of [row.baseline, row.snapshot]) {
|
|
687
|
+
const parsed = parseJsonRecord(field);
|
|
688
|
+
if (!parsed) continue;
|
|
689
|
+
tuples.push(...extractContextMessages(parsed));
|
|
690
|
+
}
|
|
691
|
+
if (tuples.length === 0 && (row.baseline.trim() || row.snapshot.trim())) {
|
|
692
|
+
options.onWarning?.({
|
|
693
|
+
source: "opencode",
|
|
694
|
+
type: "invalid_record",
|
|
695
|
+
filePath: dbPath,
|
|
696
|
+
threadId: sessionId,
|
|
697
|
+
detail: "Session compaction exists but no recoverable summary text was found"
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
return dedupeContextMessages(tuples);
|
|
701
|
+
}
|
|
702
|
+
function groupMessageRows(rows, dbPath, threadId, options) {
|
|
703
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
704
|
+
for (const row of rows) {
|
|
705
|
+
const messageData = parseJsonRecord(row.message_data);
|
|
706
|
+
if (row.message_data && !messageData) {
|
|
707
|
+
options.onWarning?.({
|
|
708
|
+
source: "opencode",
|
|
709
|
+
type: "invalid_record",
|
|
710
|
+
filePath: dbPath,
|
|
711
|
+
threadId,
|
|
712
|
+
detail: `Invalid message JSON for ${row.message_id}`
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
let message = grouped.get(row.message_id);
|
|
716
|
+
if (!message) {
|
|
717
|
+
message = {
|
|
718
|
+
id: row.message_id,
|
|
719
|
+
timeCreated: row.message_time_created,
|
|
720
|
+
messageData,
|
|
721
|
+
parts: []
|
|
722
|
+
};
|
|
723
|
+
grouped.set(row.message_id, message);
|
|
724
|
+
}
|
|
725
|
+
if (!row.part_id || row.part_time_created === null || row.part_data === null) {
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
const partData = parseJsonRecord(row.part_data);
|
|
729
|
+
if (!partData) {
|
|
730
|
+
options.onWarning?.({
|
|
731
|
+
source: "opencode",
|
|
732
|
+
type: "invalid_record",
|
|
733
|
+
filePath: dbPath,
|
|
734
|
+
threadId,
|
|
735
|
+
detail: `Invalid part JSON for ${row.part_id}`
|
|
736
|
+
});
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
message.parts.push({
|
|
740
|
+
id: row.part_id,
|
|
741
|
+
timeCreated: row.part_time_created,
|
|
742
|
+
data: partData
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
return Array.from(grouped.values());
|
|
746
|
+
}
|
|
747
|
+
function extractVisibleText(part) {
|
|
748
|
+
if (!part) return null;
|
|
749
|
+
if (getString(part.type) !== "text") return null;
|
|
750
|
+
if (part.synthetic === true) return null;
|
|
751
|
+
const text = getString(part.text);
|
|
752
|
+
if (!text || !text.trim()) return null;
|
|
753
|
+
return text.trim();
|
|
754
|
+
}
|
|
755
|
+
function extractContextMessages(value) {
|
|
756
|
+
if (Array.isArray(value)) {
|
|
757
|
+
return value.flatMap((entry) => extractContextMessages(entry));
|
|
758
|
+
}
|
|
759
|
+
const record = asRecord(value);
|
|
760
|
+
if (!record) {
|
|
761
|
+
return [];
|
|
762
|
+
}
|
|
763
|
+
const role = normalizeRole(getString(record.role));
|
|
764
|
+
const directText = getString(record.text) ?? getString(record.content);
|
|
765
|
+
if (role && directText && directText.trim()) {
|
|
766
|
+
return [[role, directText.trim()]];
|
|
767
|
+
}
|
|
768
|
+
const nestedContent = record.content;
|
|
769
|
+
if (role && Array.isArray(nestedContent)) {
|
|
770
|
+
const joined = nestedContent.map((entry) => asRecord(entry)).map((entry) => getString(entry?.text)).filter((entry) => Boolean(entry && entry.trim())).map((entry) => entry.trim()).join("\n");
|
|
771
|
+
if (joined) {
|
|
772
|
+
return [[role, joined]];
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return [];
|
|
776
|
+
}
|
|
777
|
+
function dedupeContextMessages(messages) {
|
|
778
|
+
const result = [];
|
|
779
|
+
const seen = /* @__PURE__ */ new Set();
|
|
780
|
+
for (const message of messages) {
|
|
781
|
+
const key = `${message[0]}:${message[1]}`;
|
|
782
|
+
if (seen.has(key)) continue;
|
|
783
|
+
seen.add(key);
|
|
784
|
+
result.push(message);
|
|
785
|
+
}
|
|
786
|
+
return result;
|
|
787
|
+
}
|
|
788
|
+
function normalizeRole(role) {
|
|
789
|
+
if (role === "user") return "u";
|
|
790
|
+
if (role === "assistant") return "a";
|
|
791
|
+
return null;
|
|
792
|
+
}
|
|
793
|
+
function parseJsonRecord(value) {
|
|
794
|
+
if (!value) return null;
|
|
795
|
+
try {
|
|
796
|
+
const parsed = JSON.parse(value);
|
|
797
|
+
return asRecord(parsed);
|
|
798
|
+
} catch {
|
|
799
|
+
return null;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
function asRecord(value) {
|
|
803
|
+
return typeof value === "object" && value !== null ? value : null;
|
|
804
|
+
}
|
|
805
|
+
function getString(value) {
|
|
806
|
+
return typeof value === "string" ? value : null;
|
|
807
|
+
}
|
|
808
|
+
function toFiniteNumber(value) {
|
|
809
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
810
|
+
}
|
|
811
|
+
function getErrorMessage2(error) {
|
|
812
|
+
return error instanceof Error ? error.message : String(error);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
// src/sources/index.ts
|
|
816
|
+
var implementedSources = {
|
|
817
|
+
codex: codexSource,
|
|
818
|
+
opencode: opencodeSource
|
|
819
|
+
};
|
|
820
|
+
var knownSourceIds = ["codex", "opencode", "claude"];
|
|
821
|
+
function isKnownSourceId(value) {
|
|
822
|
+
return knownSourceIds.includes(value);
|
|
823
|
+
}
|
|
824
|
+
|
|
448
825
|
// src/commands/extract.ts
|
|
449
826
|
async function extractCommand(options) {
|
|
450
827
|
try {
|
|
451
828
|
const timezone = getSystemTimezone();
|
|
452
|
-
const
|
|
453
|
-
const dbPath = join2(codexDir, "state_5.sqlite");
|
|
829
|
+
const enabledSources = resolveEnabledSources(options);
|
|
454
830
|
const isRangeMode = options.from !== void 0 || options.to !== void 0;
|
|
455
831
|
if (isRangeMode) {
|
|
456
|
-
await extractRange(options, timezone,
|
|
832
|
+
await extractRange(options, timezone, enabledSources);
|
|
457
833
|
} else {
|
|
458
834
|
const dateStr = parseDate(options.date ?? "today");
|
|
459
835
|
const boundaries = getDayBoundaries(dateStr, timezone);
|
|
460
|
-
const
|
|
836
|
+
const candidates = listCandidateSessions(enabledSources);
|
|
461
837
|
console.log(`Extracting activity for ${dateStr} (${timezone})`);
|
|
462
838
|
console.log(`Day boundaries: ${boundaries.start.toISOString()} - ${boundaries.end.toISOString()}`);
|
|
463
|
-
console.log(`
|
|
464
|
-
console.log(`Found ${
|
|
465
|
-
await extractDay(dateStr, timezone,
|
|
839
|
+
console.log(`Sources: ${enabledSources.map((entry) => `${entry.source.id}=${entry.root}`).join(", ")}`);
|
|
840
|
+
console.log(`Found ${candidates.length} candidate threads`);
|
|
841
|
+
await extractDay(dateStr, timezone, candidates, boundaries, options.out, enabledSources);
|
|
466
842
|
}
|
|
467
843
|
} catch (error) {
|
|
468
844
|
const message = error instanceof Error ? error.message : String(error);
|
|
469
845
|
throw new Error(`Extraction failed: ${message}`);
|
|
470
846
|
}
|
|
471
847
|
}
|
|
472
|
-
async function extractRange(options, timezone,
|
|
848
|
+
async function extractRange(options, timezone, enabledSources) {
|
|
473
849
|
let fromDate;
|
|
474
850
|
let toDate;
|
|
475
851
|
if (options.from) {
|
|
476
852
|
fromDate = parseDate(options.from);
|
|
477
853
|
} else {
|
|
478
|
-
const earliestMs =
|
|
854
|
+
const earliestMs = getEarliestSessionDate2(enabledSources);
|
|
479
855
|
fromDate = timestampToDate(earliestMs, timezone);
|
|
480
856
|
}
|
|
481
857
|
if (options.to) {
|
|
@@ -492,50 +868,41 @@ async function extractRange(options, timezone, dbPath) {
|
|
|
492
868
|
mkdirSync(outDir, { recursive: true });
|
|
493
869
|
}
|
|
494
870
|
console.log(`Extracting ${dates.length} days: ${fromDate} to ${toDate}`);
|
|
871
|
+
console.log(`Sources: ${enabledSources.map((entry) => `${entry.source.id}=${entry.root}`).join(", ")}`);
|
|
495
872
|
console.log(`Output: ${outDir ?? "current directory"}
|
|
496
873
|
`);
|
|
497
|
-
const threads = readThreadMetadata(dbPath);
|
|
498
874
|
const rangeStart = getDayBoundaries(fromDate, timezone).start;
|
|
499
875
|
const rangeEnd = getDayBoundaries(toDate, timezone).end;
|
|
500
|
-
const
|
|
501
|
-
const
|
|
502
|
-
|
|
503
|
-
printWarnings(`${fromDate}..${toDate}`, warnings);
|
|
876
|
+
const candidates = listCandidateSessions(enabledSources, rangeStart, rangeEnd);
|
|
877
|
+
const parsed = await parseSessions(enabledSources, candidates);
|
|
878
|
+
printWarnings(`${fromDate}..${toDate}`, parsed.warnings);
|
|
504
879
|
for (const dateStr of dates) {
|
|
505
880
|
const boundaries = getDayBoundaries(dateStr, timezone);
|
|
506
|
-
const outPath = outDir ?
|
|
881
|
+
const outPath = outDir ? join4(outDir, getDefaultArtifactFilename(dateStr)) : getDefaultArtifactPath(dateStr);
|
|
507
882
|
await extractDay(
|
|
508
883
|
dateStr,
|
|
509
884
|
timezone,
|
|
510
|
-
|
|
885
|
+
parsed.candidates,
|
|
511
886
|
boundaries,
|
|
512
887
|
outPath,
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
threadMetadataMap
|
|
888
|
+
enabledSources,
|
|
889
|
+
parsed
|
|
516
890
|
);
|
|
517
891
|
}
|
|
518
892
|
console.log(`
|
|
519
893
|
Done. Extracted ${dates.length} day${dates.length === 1 ? "" : "s"} to ${outDir ?? "current directory"}`);
|
|
520
894
|
}
|
|
521
|
-
async function extractDay(dateStr, timezone,
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
if (!parsedSessions) {
|
|
526
|
-
printWarnings(dateStr, warnings);
|
|
895
|
+
async function extractDay(dateStr, timezone, candidates, boundaries, outPathOverride, enabledSources, parsedSourceSessions) {
|
|
896
|
+
const parsed = parsedSourceSessions ?? await parseSessions(enabledSources, candidates);
|
|
897
|
+
if (!parsedSourceSessions) {
|
|
898
|
+
printWarnings(dateStr, parsed.warnings);
|
|
527
899
|
}
|
|
528
|
-
const activeSessions = filterSessionsByActivity(
|
|
529
|
-
sessions,
|
|
530
|
-
threadMetadataMap,
|
|
531
|
-
boundaries.start,
|
|
532
|
-
boundaries.end
|
|
533
|
-
);
|
|
900
|
+
const activeSessions = filterSessionsByActivity(parsed.sessions, boundaries.start, boundaries.end);
|
|
534
901
|
if (activeSessions.length === 0) {
|
|
535
|
-
console.log(` ${dateStr}: no activity (${
|
|
902
|
+
console.log(` ${dateStr}: no activity (${candidates.length} threads scanned)`);
|
|
536
903
|
return;
|
|
537
904
|
}
|
|
538
|
-
const grouped = groupSessionsByProject(activeSessions
|
|
905
|
+
const grouped = groupSessionsByProject(activeSessions);
|
|
539
906
|
const projects = buildProjectStructure(grouped);
|
|
540
907
|
const artifact = {
|
|
541
908
|
date: dateStr,
|
|
@@ -546,38 +913,129 @@ async function extractDay(dateStr, timezone, threads, boundaries, outPathOverrid
|
|
|
546
913
|
mkdirSync(dirname(outPath), { recursive: true });
|
|
547
914
|
writeFileSync(outPath, JSON.stringify(artifact), "utf-8");
|
|
548
915
|
const totalMessages = projects.reduce(
|
|
549
|
-
(sum,
|
|
916
|
+
(sum, project) => sum + project.threads.reduce((threadSum, thread) => threadSum + thread.messages.length, 0),
|
|
550
917
|
0
|
|
551
918
|
);
|
|
552
919
|
const totalContext = projects.reduce(
|
|
553
|
-
(sum,
|
|
920
|
+
(sum, project) => sum + project.threads.reduce((threadSum, thread) => threadSum + thread.context.length, 0),
|
|
554
921
|
0
|
|
555
922
|
);
|
|
556
923
|
const hybridCount = projects.reduce(
|
|
557
|
-
(sum,
|
|
924
|
+
(sum, project) => sum + project.threads.filter((thread) => thread.context.length > 0).length,
|
|
558
925
|
0
|
|
559
926
|
);
|
|
560
|
-
const threadCount = projects.reduce((sum,
|
|
927
|
+
const threadCount = projects.reduce((sum, project) => sum + project.threads.length, 0);
|
|
561
928
|
const parts = [`${threadCount} threads`];
|
|
562
929
|
if (hybridCount > 0) parts.push(`${hybridCount} hybrid`);
|
|
563
930
|
parts.push(`${totalMessages} messages`);
|
|
564
931
|
if (totalContext > 0) parts.push(`${totalContext} context`);
|
|
565
932
|
console.log(` ${dateStr}: ${parts.join(", ")}`);
|
|
566
933
|
}
|
|
567
|
-
async function
|
|
934
|
+
async function parseSessions(enabledSources, candidates) {
|
|
568
935
|
const sessions = [];
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
936
|
+
const warnings = [];
|
|
937
|
+
const sourceMap = new Map(
|
|
938
|
+
enabledSources.map((entry) => [entry.source.id, entry.source])
|
|
939
|
+
);
|
|
940
|
+
for (const candidate of candidates) {
|
|
941
|
+
const source = candidate.source === "claude" ? void 0 : sourceMap.get(candidate.source);
|
|
942
|
+
if (!source) continue;
|
|
943
|
+
sessions.push(await source.parseSession(candidate, {
|
|
944
|
+
onWarning: (warning) => warnings.push(warning)
|
|
945
|
+
}));
|
|
574
946
|
}
|
|
575
|
-
return {
|
|
947
|
+
return {
|
|
948
|
+
sessions,
|
|
949
|
+
warnings,
|
|
950
|
+
candidates
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
function listCandidateSessions(enabledSources, start, end) {
|
|
954
|
+
return enabledSources.flatMap(({ source, root }) => source.listSessions(root)).filter((session) => {
|
|
955
|
+
if (!start || !end) {
|
|
956
|
+
return !session.archived;
|
|
957
|
+
}
|
|
958
|
+
return session.created_at_ms <= end.getTime() && session.updated_at_ms >= start.getTime() && !session.archived;
|
|
959
|
+
}).sort((left, right) => {
|
|
960
|
+
if (left.created_at_ms !== right.created_at_ms) {
|
|
961
|
+
return left.created_at_ms - right.created_at_ms;
|
|
962
|
+
}
|
|
963
|
+
if (left.source !== right.source) {
|
|
964
|
+
return left.source.localeCompare(right.source);
|
|
965
|
+
}
|
|
966
|
+
return left.thread_id.localeCompare(right.thread_id);
|
|
967
|
+
});
|
|
576
968
|
}
|
|
577
|
-
function
|
|
578
|
-
|
|
579
|
-
|
|
969
|
+
function getEarliestSessionDate2(enabledSources) {
|
|
970
|
+
const timestamps = enabledSources.map(({ source, root }) => source.getEarliestSessionDate(root));
|
|
971
|
+
if (timestamps.length === 0) {
|
|
972
|
+
throw new Error("No enabled sources available to determine the earliest session date.");
|
|
973
|
+
}
|
|
974
|
+
return Math.min(...timestamps);
|
|
975
|
+
}
|
|
976
|
+
function resolveEnabledSources(options) {
|
|
977
|
+
const explicitSources = normalizeSourceSelection(options.source);
|
|
978
|
+
if (explicitSources.length > 0) {
|
|
979
|
+
return explicitSources.map((sourceId) => {
|
|
980
|
+
if (sourceId === "claude") {
|
|
981
|
+
throw new Error("Claude source is not implemented yet.");
|
|
982
|
+
}
|
|
983
|
+
const source = implementedSources[sourceId];
|
|
984
|
+
return {
|
|
985
|
+
source,
|
|
986
|
+
root: getSourceRoot(sourceId, options)
|
|
987
|
+
};
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
const discovered = Object.values(implementedSources).map((source) => ({ source, root: getSourceRoot(source.id, options) })).filter(({ source, root }) => {
|
|
991
|
+
return hasExplicitRoot(source.id, options) || source.isAvailable(root);
|
|
580
992
|
});
|
|
993
|
+
if (discovered.length === 0) {
|
|
994
|
+
throw new Error(
|
|
995
|
+
"No supported session sources found. Checked " + Object.values(implementedSources).map((source) => `${source.id} at ${getSourceRoot(source.id, options)}`).join(", ")
|
|
996
|
+
);
|
|
997
|
+
}
|
|
998
|
+
return discovered;
|
|
999
|
+
}
|
|
1000
|
+
function normalizeSourceSelection(values) {
|
|
1001
|
+
if (!values || values.length === 0) return [];
|
|
1002
|
+
const result = [];
|
|
1003
|
+
for (const rawValue of values) {
|
|
1004
|
+
for (const item of rawValue.split(",")) {
|
|
1005
|
+
const trimmed = item.trim().toLowerCase();
|
|
1006
|
+
if (!trimmed) continue;
|
|
1007
|
+
if (!isKnownSourceId(trimmed)) {
|
|
1008
|
+
throw new Error(`Unknown source: ${trimmed}. Use codex, opencode, or claude.`);
|
|
1009
|
+
}
|
|
1010
|
+
if (!result.includes(trimmed)) {
|
|
1011
|
+
result.push(trimmed);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
return result;
|
|
1016
|
+
}
|
|
1017
|
+
function getSourceRoot(sourceId, options) {
|
|
1018
|
+
switch (sourceId) {
|
|
1019
|
+
case "codex":
|
|
1020
|
+
return expandHome(options.codexDir ?? implementedSources.codex.getDefaultRoot());
|
|
1021
|
+
case "opencode":
|
|
1022
|
+
return expandHome(options.opencodeDir ?? implementedSources.opencode.getDefaultRoot());
|
|
1023
|
+
case "claude":
|
|
1024
|
+
return expandHome(options.claudeDir ?? join4(homedir3(), ".claude"));
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
function hasExplicitRoot(sourceId, options) {
|
|
1028
|
+
switch (sourceId) {
|
|
1029
|
+
case "codex":
|
|
1030
|
+
return options.codexDir !== void 0;
|
|
1031
|
+
case "opencode":
|
|
1032
|
+
return options.opencodeDir !== void 0;
|
|
1033
|
+
case "claude":
|
|
1034
|
+
return options.claudeDir !== void 0;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
function expandHome(value) {
|
|
1038
|
+
return value.replace(/^~/, homedir3());
|
|
581
1039
|
}
|
|
582
1040
|
function printWarnings(dateStr, warnings) {
|
|
583
1041
|
if (warnings.length === 0) return;
|
|
@@ -598,20 +1056,22 @@ function getDefaultArtifactPath(dateStr) {
|
|
|
598
1056
|
function formatWarning(warning) {
|
|
599
1057
|
switch (warning.type) {
|
|
600
1058
|
case "missing_file":
|
|
601
|
-
return `missing session file for thread ${warning.threadId}: ${warning.filePath}`;
|
|
1059
|
+
return `[${warning.source}] missing session file for thread ${warning.threadId}: ${warning.filePath}`;
|
|
602
1060
|
case "invalid_jsonl":
|
|
603
|
-
return `invalid JSONL at line ${warning.line ?? "?"} in ${warning.filePath}`;
|
|
1061
|
+
return `[${warning.source}] invalid JSONL at line ${warning.line ?? "?"} in ${warning.filePath}`;
|
|
1062
|
+
case "invalid_record":
|
|
1063
|
+
return `[${warning.source}] invalid record in ${warning.filePath}: ${warning.detail}`;
|
|
604
1064
|
case "read_error":
|
|
605
|
-
return
|
|
1065
|
+
return `[${warning.source}] ${warning.detail} (${warning.filePath})`;
|
|
606
1066
|
default:
|
|
607
1067
|
return warning.detail;
|
|
608
1068
|
}
|
|
609
1069
|
}
|
|
610
1070
|
|
|
611
1071
|
// src/commands/inspect.ts
|
|
612
|
-
import { readFileSync, existsSync as
|
|
1072
|
+
import { readFileSync, existsSync as existsSync5 } from "fs";
|
|
613
1073
|
function inspectCommand(options) {
|
|
614
|
-
if (!
|
|
1074
|
+
if (!existsSync5(options.input)) {
|
|
615
1075
|
console.error(`File not found: ${options.input}`);
|
|
616
1076
|
process.exit(1);
|
|
617
1077
|
}
|
|
@@ -704,7 +1164,7 @@ function sanitizeForTerminal(value) {
|
|
|
704
1164
|
|
|
705
1165
|
// src/commands/install.ts
|
|
706
1166
|
import {
|
|
707
|
-
existsSync as
|
|
1167
|
+
existsSync as existsSync6,
|
|
708
1168
|
mkdirSync as mkdirSync2,
|
|
709
1169
|
copyFileSync,
|
|
710
1170
|
lstatSync,
|
|
@@ -712,7 +1172,7 @@ import {
|
|
|
712
1172
|
rmSync,
|
|
713
1173
|
unlinkSync
|
|
714
1174
|
} from "fs";
|
|
715
|
-
import { join as
|
|
1175
|
+
import { join as join5, dirname as dirname2 } from "path";
|
|
716
1176
|
import { fileURLToPath } from "url";
|
|
717
1177
|
function findSkillSource() {
|
|
718
1178
|
const distDir = (() => {
|
|
@@ -723,11 +1183,11 @@ function findSkillSource() {
|
|
|
723
1183
|
}
|
|
724
1184
|
})();
|
|
725
1185
|
const candidates = [
|
|
726
|
-
|
|
727
|
-
|
|
1186
|
+
join5(distDir, "..", "skills", "dbrief-note", "SKILL.md"),
|
|
1187
|
+
join5(distDir, "..", "..", "skills", "dbrief-note", "SKILL.md")
|
|
728
1188
|
];
|
|
729
1189
|
for (const p of candidates) {
|
|
730
|
-
if (
|
|
1190
|
+
if (existsSync6(p)) return p;
|
|
731
1191
|
}
|
|
732
1192
|
throw new Error(
|
|
733
1193
|
"Could not find dbrief-note skill.\nIs the package installed correctly?"
|
|
@@ -736,17 +1196,17 @@ function findSkillSource() {
|
|
|
736
1196
|
function installCommand() {
|
|
737
1197
|
const cwd = process.cwd();
|
|
738
1198
|
const skillSource = findSkillSource();
|
|
739
|
-
const targetDir =
|
|
740
|
-
const targetFile =
|
|
1199
|
+
const targetDir = join5(cwd, ".codex", "skills", "dbrief-note");
|
|
1200
|
+
const targetFile = join5(targetDir, "SKILL.md");
|
|
741
1201
|
mkdirSync2(targetDir, { recursive: true });
|
|
742
|
-
const tempFile =
|
|
1202
|
+
const tempFile = join5(
|
|
743
1203
|
targetDir,
|
|
744
1204
|
`.SKILL.md.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`
|
|
745
1205
|
);
|
|
746
1206
|
try {
|
|
747
1207
|
copyFileSync(skillSource, tempFile);
|
|
748
1208
|
ensureSafeInstallDestination(targetFile);
|
|
749
|
-
if (
|
|
1209
|
+
if (existsSync6(targetFile)) {
|
|
750
1210
|
unlinkSync(targetFile);
|
|
751
1211
|
}
|
|
752
1212
|
renameSync(tempFile, targetFile);
|
|
@@ -762,7 +1222,7 @@ Usage:`);
|
|
|
762
1222
|
console.log(` or invoke the skill in any of your coding agents`);
|
|
763
1223
|
}
|
|
764
1224
|
function ensureSafeInstallDestination(targetFile) {
|
|
765
|
-
if (!
|
|
1225
|
+
if (!existsSync6(targetFile)) {
|
|
766
1226
|
return;
|
|
767
1227
|
}
|
|
768
1228
|
const stats = lstatSync(targetFile);
|
|
@@ -779,8 +1239,9 @@ function ensureSafeInstallDestination(targetFile) {
|
|
|
779
1239
|
|
|
780
1240
|
// src/cli/index.ts
|
|
781
1241
|
var program = new Command();
|
|
782
|
-
|
|
783
|
-
program.
|
|
1242
|
+
var collectOption = (value, previous) => previous.concat(value);
|
|
1243
|
+
program.name("dbrief").description("Extract coding agent session activity into daily artifacts").version("0.1.0");
|
|
1244
|
+
program.command("extract").description("Extract session data for a target date or date range").option("--date <date>", "Target date (today, yesterday, or YYYY-MM-DD)").option("--from <date>", "Start date for range extraction (YYYY-MM-DD)").option("--to <date>", "End date for range extraction (YYYY-MM-DD)").option("--out <path>", "Output file path (single day) or directory (range)").option("--source <source>", "Enable session source(s): codex, opencode, claude", collectOption, []).option("--codex-dir <path>", "Codex data directory").option("--opencode-dir <path>", "Opencode data directory").option("--claude-dir <path>", "Claude Code data directory").action(extractCommand);
|
|
784
1245
|
program.command("install").description("Install the dbrief-note skill into the current project").action(installCommand);
|
|
785
1246
|
program.command("inspect").description("Inspect a daily artifact").requiredOption("--input <path>", "Input artifact file").option("--format <format>", "Output format (summary)").action(inspectCommand);
|
|
786
1247
|
program.parse();
|