@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/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 import_path2 = require("path");
31
- var import_os2 = require("os");
32
- var import_fs3 = require("fs");
33
- var import_path3 = require("path");
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, threadMetadata, start, end) {
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 import_fs2 = require("fs");
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, import_fs2.existsSync)(candidate)) {
169
+ if (!(0, import_fs.existsSync)(candidate)) {
329
170
  continue;
330
171
  }
331
172
  try {
332
- if ((0, import_fs2.statSync)(candidate).isFile()) {
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, import_fs2.existsSync)(candidate)) {
219
+ if ((0, import_fs.existsSync)(candidate)) {
379
220
  try {
380
- if ((0, import_fs2.statSync)(candidate).isDirectory()) {
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, import_fs2.existsSync)(candidate) && (0, import_fs2.statSync)(candidate).isDirectory()) {
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, import_fs2.existsSync)(candidate)) {
253
+ if (!(0, import_fs.existsSync)(candidate)) {
413
254
  continue;
414
255
  }
415
256
  try {
416
- if ((0, import_fs2.statSync)(candidate).isFile()) {
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, threadMetadata) {
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 gitRoot = gitRootCache.get(cwd);
436
- if (gitRoot === void 0) {
437
- gitRoot = resolveGitRoot(cwd);
438
- gitRootCache.set(cwd, gitRoot);
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({ session, metadata });
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(({ session, metadata }) => ({
454
- title: metadata?.title ?? session.thread_id,
455
- branch: metadata?.git_branch ?? null,
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 codexDir = options.codexDir.replace(/^~/, (0, import_os2.homedir)());
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, dbPath);
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 threads = readThreadMetadata(dbPath);
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(`Reading from: ${dbPath}`);
487
- console.log(`Found ${threads.length} active threads`);
488
- await extractDay(dateStr, timezone, threads, boundaries, options.out);
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, dbPath) {
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 = getEarliestSessionDate(dbPath);
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, import_fs3.mkdirSync)(outDir, { recursive: true });
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 candidateThreads = filterThreadsByActivity(threads, rangeStart, rangeEnd);
524
- const { sessions, warnings } = await parseThreads(candidateThreads);
525
- const threadMetadataMap = new Map(candidateThreads.map((t) => [t.id, t]));
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, import_path2.join)(outDir, getDefaultArtifactFilename(dateStr)) : getDefaultArtifactPath(dateStr);
904
+ const outPath = outDir ? (0, import_path4.join)(outDir, getDefaultArtifactFilename(dateStr)) : getDefaultArtifactPath(dateStr);
530
905
  await extractDay(
531
906
  dateStr,
532
907
  timezone,
533
- candidateThreads,
908
+ parsed.candidates,
534
909
  boundaries,
535
910
  outPath,
536
- sessions,
537
- warnings,
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, threads, boundaries, outPathOverride, parsedSessions, sharedWarnings, existingThreadMetadataMap) {
545
- const threadMetadataMap = existingThreadMetadataMap ?? new Map(threads.map((t) => [t.id, t]));
546
- const warnings = sharedWarnings ?? [];
547
- const sessions = parsedSessions ?? (await parseThreads(threads, warnings)).sessions;
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 (${threads.length} threads scanned)`);
925
+ console.log(` ${dateStr}: no activity (${candidates.length} threads scanned)`);
559
926
  return;
560
927
  }
561
- const grouped = groupSessionsByProject(activeSessions, threadMetadataMap);
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, import_fs3.mkdirSync)((0, import_path3.dirname)(outPath), { recursive: true });
570
- (0, import_fs3.writeFileSync)(outPath, JSON.stringify(artifact), "utf-8");
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, p) => sum + p.threads.reduce((tSum, t) => tSum + t.messages.length, 0),
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, p) => sum + p.threads.reduce((tSum, t) => tSum + t.context.length, 0),
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, p) => sum + p.threads.filter((t) => t.context.length > 0).length,
947
+ (sum, project) => sum + project.threads.filter((thread) => thread.context.length > 0).length,
581
948
  0
582
949
  );
583
- const threadCount = projects.reduce((sum, p) => sum + p.threads.length, 0);
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 parseThreads(threads, warningStore = []) {
957
+ async function parseSessions(enabledSources, candidates) {
591
958
  const sessions = [];
592
- for (const thread of threads) {
593
- const session = await parseSessionFile(thread.rollout_path, thread.id, {
594
- onWarning: (warning) => warningStore.push(warning)
595
- });
596
- sessions.push(session);
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 { sessions, warnings: warningStore };
997
+ return Math.min(...timestamps);
599
998
  }
600
- function filterThreadsByActivity(threads, start, end) {
601
- return threads.filter((thread) => {
602
- return thread.created_at_ms <= end.getTime() && thread.updated_at_ms >= start.getTime();
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 `${warning.detail} (${warning.filePath})`;
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 import_fs4 = require("fs");
1095
+ var import_fs6 = require("fs");
636
1096
  function inspectCommand(options) {
637
- if (!(0, import_fs4.existsSync)(options.input)) {
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, import_fs4.readFileSync)(options.input, "utf-8");
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 import_fs5 = require("fs");
730
- var import_path4 = require("path");
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, import_path4.dirname)((0, import_url.fileURLToPath)(import_meta.url));
1196
+ return (0, import_path6.dirname)((0, import_url.fileURLToPath)(import_meta.url));
737
1197
  } catch {
738
- return (0, import_path4.dirname)(process.argv[1]);
1198
+ return (0, import_path6.dirname)(process.argv[1]);
739
1199
  }
740
1200
  })();
741
1201
  const candidates = [
742
- (0, import_path4.join)(distDir, "..", "skills", "dbrief-note", "SKILL.md"),
743
- (0, import_path4.join)(distDir, "..", "..", "skills", "dbrief-note", "SKILL.md")
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, import_fs5.existsSync)(p)) return p;
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, import_path4.join)(cwd, ".codex", "skills", "dbrief-note");
756
- const targetFile = (0, import_path4.join)(targetDir, "SKILL.md");
757
- (0, import_fs5.mkdirSync)(targetDir, { recursive: true });
758
- const tempFile = (0, import_path4.join)(
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, import_fs5.copyFileSync)(skillSource, tempFile);
1223
+ (0, import_fs7.copyFileSync)(skillSource, tempFile);
764
1224
  ensureSafeInstallDestination(targetFile);
765
- if ((0, import_fs5.existsSync)(targetFile)) {
766
- (0, import_fs5.unlinkSync)(targetFile);
1225
+ if ((0, import_fs7.existsSync)(targetFile)) {
1226
+ (0, import_fs7.unlinkSync)(targetFile);
767
1227
  }
768
- (0, import_fs5.renameSync)(tempFile, targetFile);
1228
+ (0, import_fs7.renameSync)(tempFile, targetFile);
769
1229
  } catch (error) {
770
- (0, import_fs5.rmSync)(tempFile, { force: true });
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, import_fs5.existsSync)(targetFile)) {
1241
+ if (!(0, import_fs7.existsSync)(targetFile)) {
782
1242
  return;
783
1243
  }
784
- const stats = (0, import_fs5.lstatSync)(targetFile);
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
- program.name("dbrief").description("Extract Codex session activity into daily artifacts").version("0.1.0");
799
- 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("--codex-dir <path>", "Codex data directory", "~/.codex").action(extractCommand);
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();