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