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