@franshjy/dbrief 0.1.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/LICENSE +661 -0
- package/README.md +138 -0
- package/dist/index.cjs +802 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +786 -0
- package/package.json +45 -0
- package/skills/dbrief-note/SKILL.md +67 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,802 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/cli/index.ts
|
|
27
|
+
var import_commander = require("commander");
|
|
28
|
+
|
|
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
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/extractor/filter.ts
|
|
194
|
+
function getDayBoundaries(date, timezone) {
|
|
195
|
+
const dateStr = resolveDateStr(date);
|
|
196
|
+
const start = localToUTC(`${dateStr}T00:00:00`, timezone);
|
|
197
|
+
const end = localToUTC(`${dateStr}T23:59:59.999`, timezone);
|
|
198
|
+
return { start, end };
|
|
199
|
+
}
|
|
200
|
+
function resolveDateStr(date) {
|
|
201
|
+
return parseDate(date);
|
|
202
|
+
}
|
|
203
|
+
function localToUTC(localStr, timezone) {
|
|
204
|
+
const naive = /* @__PURE__ */ new Date(localStr + "Z");
|
|
205
|
+
const ms = naive.getMilliseconds();
|
|
206
|
+
const naiveNoMs = new Date(naive.getTime() - ms);
|
|
207
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
208
|
+
timeZone: timezone,
|
|
209
|
+
year: "numeric",
|
|
210
|
+
month: "2-digit",
|
|
211
|
+
day: "2-digit",
|
|
212
|
+
hour: "2-digit",
|
|
213
|
+
minute: "2-digit",
|
|
214
|
+
second: "2-digit",
|
|
215
|
+
hour12: false
|
|
216
|
+
});
|
|
217
|
+
const tzParts = formatter.formatToParts(naiveNoMs);
|
|
218
|
+
const get = (type) => tzParts.find((p) => p.type === type).value;
|
|
219
|
+
let hour = parseInt(get("hour"), 10);
|
|
220
|
+
if (hour === 24) hour = 0;
|
|
221
|
+
const tzAsUTC = new Date(
|
|
222
|
+
Date.UTC(
|
|
223
|
+
parseInt(get("year"), 10),
|
|
224
|
+
parseInt(get("month"), 10) - 1,
|
|
225
|
+
parseInt(get("day"), 10),
|
|
226
|
+
hour,
|
|
227
|
+
parseInt(get("minute"), 10),
|
|
228
|
+
parseInt(get("second"), 10)
|
|
229
|
+
)
|
|
230
|
+
);
|
|
231
|
+
const offsetMs = tzAsUTC.getTime() - naiveNoMs.getTime();
|
|
232
|
+
return new Date(naiveNoMs.getTime() - offsetMs + ms);
|
|
233
|
+
}
|
|
234
|
+
function filterSessionsByActivity(sessions, threadMetadata, start, end) {
|
|
235
|
+
return sessions.filter((s) => {
|
|
236
|
+
if (!threadMetadata.has(s.thread_id)) return false;
|
|
237
|
+
return s.user_activity_timestamps.some((timestamp) => {
|
|
238
|
+
return timestamp >= start.getTime() && timestamp <= end.getTime();
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
function parseDate(dateStr) {
|
|
243
|
+
if (dateStr === "today" || dateStr === "yesterday") {
|
|
244
|
+
const d = /* @__PURE__ */ new Date();
|
|
245
|
+
if (dateStr === "yesterday") {
|
|
246
|
+
d.setDate(d.getDate() - 1);
|
|
247
|
+
}
|
|
248
|
+
return formatDate(d);
|
|
249
|
+
}
|
|
250
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(dateStr)) {
|
|
251
|
+
if (!isValidIsoCalendarDate(dateStr)) {
|
|
252
|
+
throw new Error(
|
|
253
|
+
`Invalid date value: "${dateStr}". Use a real calendar date in YYYY-MM-DD format.`
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
return dateStr;
|
|
257
|
+
}
|
|
258
|
+
throw new Error(
|
|
259
|
+
`Invalid date format: "${dateStr}". Use "today", "yesterday", or "YYYY-MM-DD".`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
function getDateRange(fromDate, toDate) {
|
|
263
|
+
const dates = [];
|
|
264
|
+
const current = /* @__PURE__ */ new Date(fromDate + "T00:00:00");
|
|
265
|
+
const end = /* @__PURE__ */ new Date(toDate + "T00:00:00");
|
|
266
|
+
while (current <= end) {
|
|
267
|
+
dates.push(formatDate(current));
|
|
268
|
+
current.setDate(current.getDate() + 1);
|
|
269
|
+
}
|
|
270
|
+
return dates;
|
|
271
|
+
}
|
|
272
|
+
function timestampToDate(ms, timezone) {
|
|
273
|
+
const d = new Date(ms);
|
|
274
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
275
|
+
timeZone: timezone,
|
|
276
|
+
year: "numeric",
|
|
277
|
+
month: "2-digit",
|
|
278
|
+
day: "2-digit"
|
|
279
|
+
});
|
|
280
|
+
const parts = formatter.formatToParts(d);
|
|
281
|
+
const get = (type) => parts.find((p) => p.type === type).value;
|
|
282
|
+
return `${get("year")}-${get("month")}-${get("day")}`;
|
|
283
|
+
}
|
|
284
|
+
function formatDate(d) {
|
|
285
|
+
const year = d.getFullYear();
|
|
286
|
+
const month = String(d.getMonth() + 1).padStart(2, "0");
|
|
287
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
288
|
+
return `${year}-${month}-${day}`;
|
|
289
|
+
}
|
|
290
|
+
function isValidIsoCalendarDate(dateStr) {
|
|
291
|
+
const [yearStr, monthStr, dayStr] = dateStr.split("-");
|
|
292
|
+
const year = Number.parseInt(yearStr, 10);
|
|
293
|
+
const month = Number.parseInt(monthStr, 10);
|
|
294
|
+
const day = Number.parseInt(dayStr, 10);
|
|
295
|
+
const candidate = new Date(Date.UTC(year, month - 1, day));
|
|
296
|
+
return candidate.getUTCFullYear() === year && candidate.getUTCMonth() === month - 1 && candidate.getUTCDate() === day;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// src/utils/git.ts
|
|
300
|
+
var import_child_process = require("child_process");
|
|
301
|
+
var import_fs2 = require("fs");
|
|
302
|
+
var import_os = require("os");
|
|
303
|
+
var import_path = require("path");
|
|
304
|
+
function resolveGitRoot(cwd) {
|
|
305
|
+
try {
|
|
306
|
+
const gitExecutable = resolveGitExecutable();
|
|
307
|
+
if (!gitExecutable) {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
return runGitCommand(gitExecutable, ["-C", cwd, "rev-parse", "--show-toplevel"]).trim();
|
|
311
|
+
} catch {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
function resolveGitExecutable() {
|
|
316
|
+
const pathValue = process.env.PATH;
|
|
317
|
+
if (!pathValue) {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
const executableNames = process.platform === "win32" ? ["git.exe", "git.cmd", "git.bat"] : ["git"];
|
|
321
|
+
for (const entry of pathValue.split(import_path.delimiter)) {
|
|
322
|
+
const dir = entry.trim().replace(/^"(.*)"$/, "$1");
|
|
323
|
+
if (!dir || dir === ".") {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
for (const executableName of executableNames) {
|
|
327
|
+
const candidate = (0, import_path.join)(dir, executableName);
|
|
328
|
+
if (!(0, import_fs2.existsSync)(candidate)) {
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
if ((0, import_fs2.statSync)(candidate).isFile()) {
|
|
333
|
+
return candidate;
|
|
334
|
+
}
|
|
335
|
+
} catch {
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
function runGitCommand(gitExecutable, args) {
|
|
343
|
+
const options = {
|
|
344
|
+
cwd: resolveSafeWorkingDirectory(),
|
|
345
|
+
encoding: "utf-8",
|
|
346
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
347
|
+
shell: false
|
|
348
|
+
};
|
|
349
|
+
if (process.platform !== "win32") {
|
|
350
|
+
return (0, import_child_process.execFileSync)(gitExecutable, args, options);
|
|
351
|
+
}
|
|
352
|
+
const extension = (0, import_path.extname)(gitExecutable).toLowerCase();
|
|
353
|
+
if (extension !== ".cmd" && extension !== ".bat") {
|
|
354
|
+
return (0, import_child_process.execFileSync)(gitExecutable, args, options);
|
|
355
|
+
}
|
|
356
|
+
const commandProcessor = resolveCommandProcessor();
|
|
357
|
+
if (!commandProcessor) {
|
|
358
|
+
return (0, import_child_process.execFileSync)(gitExecutable, args, options);
|
|
359
|
+
}
|
|
360
|
+
const command = `call ${[gitExecutable, ...args].map(quoteForCmd).join(" ")}`;
|
|
361
|
+
const result = (0, import_child_process.spawnSync)(commandProcessor, ["/d", "/s", "/c", command], {
|
|
362
|
+
...options,
|
|
363
|
+
windowsVerbatimArguments: true
|
|
364
|
+
});
|
|
365
|
+
if (result.error) {
|
|
366
|
+
throw result.error;
|
|
367
|
+
}
|
|
368
|
+
if (result.status !== 0) {
|
|
369
|
+
throw new Error(`Git command failed with status ${result.status ?? "unknown"}`);
|
|
370
|
+
}
|
|
371
|
+
return result.stdout;
|
|
372
|
+
}
|
|
373
|
+
function resolveSafeWorkingDirectory() {
|
|
374
|
+
if (process.platform === "win32") {
|
|
375
|
+
const systemRoot = process.env.SystemRoot ?? process.env.windir;
|
|
376
|
+
if (systemRoot) {
|
|
377
|
+
const candidate = (0, import_path.join)(systemRoot, "System32");
|
|
378
|
+
if ((0, import_fs2.existsSync)(candidate)) {
|
|
379
|
+
try {
|
|
380
|
+
if ((0, import_fs2.statSync)(candidate).isDirectory()) {
|
|
381
|
+
return candidate;
|
|
382
|
+
}
|
|
383
|
+
} catch {
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
const candidates = [(0, import_os.tmpdir)(), process.cwd()];
|
|
389
|
+
for (const candidate of candidates) {
|
|
390
|
+
if (!candidate) continue;
|
|
391
|
+
try {
|
|
392
|
+
if ((0, import_fs2.existsSync)(candidate) && (0, import_fs2.statSync)(candidate).isDirectory()) {
|
|
393
|
+
return candidate;
|
|
394
|
+
}
|
|
395
|
+
} catch {
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return ".";
|
|
400
|
+
}
|
|
401
|
+
function resolveCommandProcessor() {
|
|
402
|
+
const candidates = [
|
|
403
|
+
process.env.ComSpec,
|
|
404
|
+
process.env.COMSPEC,
|
|
405
|
+
process.env.SystemRoot ? (0, import_path.join)(process.env.SystemRoot, "System32", "cmd.exe") : void 0,
|
|
406
|
+
process.env.windir ? (0, import_path.join)(process.env.windir, "System32", "cmd.exe") : void 0
|
|
407
|
+
];
|
|
408
|
+
for (const candidate of candidates) {
|
|
409
|
+
if (!candidate || !(0, import_path.isAbsolute)(candidate)) {
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
if (!(0, import_fs2.existsSync)(candidate)) {
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
try {
|
|
416
|
+
if ((0, import_fs2.statSync)(candidate).isFile()) {
|
|
417
|
+
return candidate;
|
|
418
|
+
}
|
|
419
|
+
} catch {
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return null;
|
|
424
|
+
}
|
|
425
|
+
function quoteForCmd(value) {
|
|
426
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// src/extractor/grouper.ts
|
|
430
|
+
function groupSessionsByProject(sessions, threadMetadata) {
|
|
431
|
+
const grouped = {};
|
|
432
|
+
const gitRootCache = /* @__PURE__ */ new Map();
|
|
433
|
+
for (const session of sessions) {
|
|
434
|
+
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);
|
|
439
|
+
}
|
|
440
|
+
const projectKey = gitRoot ?? cwd;
|
|
441
|
+
const metadata = threadMetadata.get(session.thread_id) ?? null;
|
|
442
|
+
if (!grouped[projectKey]) {
|
|
443
|
+
grouped[projectKey] = {
|
|
444
|
+
sessions: []
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
grouped[projectKey].sessions.push({ session, metadata });
|
|
448
|
+
}
|
|
449
|
+
return grouped;
|
|
450
|
+
}
|
|
451
|
+
function buildProjectStructure(grouped) {
|
|
452
|
+
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,
|
|
456
|
+
context: session.context,
|
|
457
|
+
messages: session.messages
|
|
458
|
+
}));
|
|
459
|
+
return {
|
|
460
|
+
project_key: projectKey,
|
|
461
|
+
threads
|
|
462
|
+
};
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// src/utils/timezone.ts
|
|
467
|
+
function getSystemTimezone() {
|
|
468
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// src/commands/extract.ts
|
|
472
|
+
async function extractCommand(options) {
|
|
473
|
+
try {
|
|
474
|
+
const timezone = getSystemTimezone();
|
|
475
|
+
const codexDir = options.codexDir.replace(/^~/, (0, import_os2.homedir)());
|
|
476
|
+
const dbPath = (0, import_path2.join)(codexDir, "state_5.sqlite");
|
|
477
|
+
const isRangeMode = options.from !== void 0 || options.to !== void 0;
|
|
478
|
+
if (isRangeMode) {
|
|
479
|
+
await extractRange(options, timezone, dbPath);
|
|
480
|
+
} else {
|
|
481
|
+
const dateStr = parseDate(options.date ?? "today");
|
|
482
|
+
const boundaries = getDayBoundaries(dateStr, timezone);
|
|
483
|
+
const threads = readThreadMetadata(dbPath);
|
|
484
|
+
console.log(`Extracting activity for ${dateStr} (${timezone})`);
|
|
485
|
+
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);
|
|
489
|
+
}
|
|
490
|
+
} catch (error) {
|
|
491
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
492
|
+
throw new Error(`Extraction failed: ${message}`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
async function extractRange(options, timezone, dbPath) {
|
|
496
|
+
let fromDate;
|
|
497
|
+
let toDate;
|
|
498
|
+
if (options.from) {
|
|
499
|
+
fromDate = parseDate(options.from);
|
|
500
|
+
} else {
|
|
501
|
+
const earliestMs = getEarliestSessionDate(dbPath);
|
|
502
|
+
fromDate = timestampToDate(earliestMs, timezone);
|
|
503
|
+
}
|
|
504
|
+
if (options.to) {
|
|
505
|
+
toDate = parseDate(options.to);
|
|
506
|
+
} else {
|
|
507
|
+
toDate = parseDate("today");
|
|
508
|
+
}
|
|
509
|
+
if (fromDate > toDate) {
|
|
510
|
+
throw new Error(`Invalid date range: --from ${fromDate} is after --to ${toDate}.`);
|
|
511
|
+
}
|
|
512
|
+
const dates = getDateRange(fromDate, toDate);
|
|
513
|
+
const outDir = options.out;
|
|
514
|
+
if (outDir) {
|
|
515
|
+
(0, import_fs3.mkdirSync)(outDir, { recursive: true });
|
|
516
|
+
}
|
|
517
|
+
console.log(`Extracting ${dates.length} days: ${fromDate} to ${toDate}`);
|
|
518
|
+
console.log(`Output: ${outDir ?? "current directory"}
|
|
519
|
+
`);
|
|
520
|
+
const threads = readThreadMetadata(dbPath);
|
|
521
|
+
const rangeStart = getDayBoundaries(fromDate, timezone).start;
|
|
522
|
+
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);
|
|
527
|
+
for (const dateStr of dates) {
|
|
528
|
+
const boundaries = getDayBoundaries(dateStr, timezone);
|
|
529
|
+
const outPath = outDir ? (0, import_path2.join)(outDir, getDefaultArtifactFilename(dateStr)) : getDefaultArtifactPath(dateStr);
|
|
530
|
+
await extractDay(
|
|
531
|
+
dateStr,
|
|
532
|
+
timezone,
|
|
533
|
+
candidateThreads,
|
|
534
|
+
boundaries,
|
|
535
|
+
outPath,
|
|
536
|
+
sessions,
|
|
537
|
+
warnings,
|
|
538
|
+
threadMetadataMap
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
console.log(`
|
|
542
|
+
Done. Extracted ${dates.length} day${dates.length === 1 ? "" : "s"} to ${outDir ?? "current directory"}`);
|
|
543
|
+
}
|
|
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);
|
|
550
|
+
}
|
|
551
|
+
const activeSessions = filterSessionsByActivity(
|
|
552
|
+
sessions,
|
|
553
|
+
threadMetadataMap,
|
|
554
|
+
boundaries.start,
|
|
555
|
+
boundaries.end
|
|
556
|
+
);
|
|
557
|
+
if (activeSessions.length === 0) {
|
|
558
|
+
console.log(` ${dateStr}: no activity (${threads.length} threads scanned)`);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
const grouped = groupSessionsByProject(activeSessions, threadMetadataMap);
|
|
562
|
+
const projects = buildProjectStructure(grouped);
|
|
563
|
+
const artifact = {
|
|
564
|
+
date: dateStr,
|
|
565
|
+
timezone,
|
|
566
|
+
projects
|
|
567
|
+
};
|
|
568
|
+
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");
|
|
571
|
+
const totalMessages = projects.reduce(
|
|
572
|
+
(sum, p) => sum + p.threads.reduce((tSum, t) => tSum + t.messages.length, 0),
|
|
573
|
+
0
|
|
574
|
+
);
|
|
575
|
+
const totalContext = projects.reduce(
|
|
576
|
+
(sum, p) => sum + p.threads.reduce((tSum, t) => tSum + t.context.length, 0),
|
|
577
|
+
0
|
|
578
|
+
);
|
|
579
|
+
const hybridCount = projects.reduce(
|
|
580
|
+
(sum, p) => sum + p.threads.filter((t) => t.context.length > 0).length,
|
|
581
|
+
0
|
|
582
|
+
);
|
|
583
|
+
const threadCount = projects.reduce((sum, p) => sum + p.threads.length, 0);
|
|
584
|
+
const parts = [`${threadCount} threads`];
|
|
585
|
+
if (hybridCount > 0) parts.push(`${hybridCount} hybrid`);
|
|
586
|
+
parts.push(`${totalMessages} messages`);
|
|
587
|
+
if (totalContext > 0) parts.push(`${totalContext} context`);
|
|
588
|
+
console.log(` ${dateStr}: ${parts.join(", ")}`);
|
|
589
|
+
}
|
|
590
|
+
async function parseThreads(threads, warningStore = []) {
|
|
591
|
+
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);
|
|
597
|
+
}
|
|
598
|
+
return { sessions, warnings: warningStore };
|
|
599
|
+
}
|
|
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();
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
function printWarnings(dateStr, warnings) {
|
|
606
|
+
if (warnings.length === 0) return;
|
|
607
|
+
console.warn(` ${dateStr}: ${warnings.length} session warning${warnings.length === 1 ? "" : "s"}`);
|
|
608
|
+
for (const warning of warnings.slice(0, 5)) {
|
|
609
|
+
console.warn(` - ${formatWarning(warning)}`);
|
|
610
|
+
}
|
|
611
|
+
if (warnings.length > 5) {
|
|
612
|
+
console.warn(` - ... ${warnings.length - 5} more`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
function getDefaultArtifactFilename(dateStr) {
|
|
616
|
+
return `dbrief_${dateStr}.json`;
|
|
617
|
+
}
|
|
618
|
+
function getDefaultArtifactPath(dateStr) {
|
|
619
|
+
return `./${getDefaultArtifactFilename(dateStr)}`;
|
|
620
|
+
}
|
|
621
|
+
function formatWarning(warning) {
|
|
622
|
+
switch (warning.type) {
|
|
623
|
+
case "missing_file":
|
|
624
|
+
return `missing session file for thread ${warning.threadId}: ${warning.filePath}`;
|
|
625
|
+
case "invalid_jsonl":
|
|
626
|
+
return `invalid JSONL at line ${warning.line ?? "?"} in ${warning.filePath}`;
|
|
627
|
+
case "read_error":
|
|
628
|
+
return `${warning.detail} (${warning.filePath})`;
|
|
629
|
+
default:
|
|
630
|
+
return warning.detail;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// src/commands/inspect.ts
|
|
635
|
+
var import_fs4 = require("fs");
|
|
636
|
+
function inspectCommand(options) {
|
|
637
|
+
if (!(0, import_fs4.existsSync)(options.input)) {
|
|
638
|
+
console.error(`File not found: ${options.input}`);
|
|
639
|
+
process.exit(1);
|
|
640
|
+
}
|
|
641
|
+
const raw = (0, import_fs4.readFileSync)(options.input, "utf-8");
|
|
642
|
+
let artifact;
|
|
643
|
+
try {
|
|
644
|
+
artifact = JSON.parse(raw);
|
|
645
|
+
} catch {
|
|
646
|
+
console.error(`Invalid JSON in: ${options.input}`);
|
|
647
|
+
process.exit(1);
|
|
648
|
+
}
|
|
649
|
+
if (!isDailyArtifact(artifact)) {
|
|
650
|
+
console.error("Invalid artifact schema.");
|
|
651
|
+
process.exit(1);
|
|
652
|
+
}
|
|
653
|
+
if (options.format === "summary") {
|
|
654
|
+
printSummary(artifact);
|
|
655
|
+
} else {
|
|
656
|
+
console.log(JSON.stringify(artifact, null, 2));
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
function printSummary(artifact) {
|
|
660
|
+
console.log(`Daily Artifact: ${sanitizeForTerminal(artifact.date)}`);
|
|
661
|
+
console.log(`Timezone: ${sanitizeForTerminal(artifact.timezone)}`);
|
|
662
|
+
console.log(`Projects: ${artifact.projects.length}`);
|
|
663
|
+
console.log();
|
|
664
|
+
for (const project of artifact.projects) {
|
|
665
|
+
const messageCount = project.threads.reduce((s, t) => s + t.messages.length, 0);
|
|
666
|
+
const contextCount = project.threads.reduce((s, t) => s + t.context.length, 0);
|
|
667
|
+
const hybridCount = project.threads.filter((t) => t.context.length > 0).length;
|
|
668
|
+
console.log(` ${sanitizeForTerminal(project.project_key)}`);
|
|
669
|
+
console.log(` threads: ${project.threads.length}${hybridCount > 0 ? ` (${hybridCount} hybrid)` : ""}`);
|
|
670
|
+
console.log(` messages: ${messageCount}`);
|
|
671
|
+
if (contextCount > 0) {
|
|
672
|
+
console.log(` context: ${contextCount}`);
|
|
673
|
+
}
|
|
674
|
+
console.log();
|
|
675
|
+
}
|
|
676
|
+
const totalMessages = artifact.projects.reduce(
|
|
677
|
+
(s, p) => s + p.threads.reduce((tS, t) => tS + t.messages.length, 0),
|
|
678
|
+
0
|
|
679
|
+
);
|
|
680
|
+
const totalContext = artifact.projects.reduce(
|
|
681
|
+
(s, p) => s + p.threads.reduce((tS, t) => tS + t.context.length, 0),
|
|
682
|
+
0
|
|
683
|
+
);
|
|
684
|
+
const totalThreads = artifact.projects.reduce((s, p) => s + p.threads.length, 0);
|
|
685
|
+
const parts = [`${totalThreads} threads`, `${totalMessages} messages`];
|
|
686
|
+
if (totalContext > 0) parts.push(`${totalContext} context`);
|
|
687
|
+
console.log(`Totals: ${parts.join(", ")}`);
|
|
688
|
+
}
|
|
689
|
+
function isDailyArtifact(value) {
|
|
690
|
+
if (!isRecord(value)) {
|
|
691
|
+
return false;
|
|
692
|
+
}
|
|
693
|
+
if (typeof value.date !== "string" || typeof value.timezone !== "string") {
|
|
694
|
+
return false;
|
|
695
|
+
}
|
|
696
|
+
if (!Array.isArray(value.projects)) {
|
|
697
|
+
return false;
|
|
698
|
+
}
|
|
699
|
+
return value.projects.every(isProject);
|
|
700
|
+
}
|
|
701
|
+
function isProject(value) {
|
|
702
|
+
if (!isRecord(value)) {
|
|
703
|
+
return false;
|
|
704
|
+
}
|
|
705
|
+
return typeof value.project_key === "string" && Array.isArray(value.threads) && value.threads.every(isThread);
|
|
706
|
+
}
|
|
707
|
+
function isThread(value) {
|
|
708
|
+
if (!isRecord(value)) {
|
|
709
|
+
return false;
|
|
710
|
+
}
|
|
711
|
+
return typeof value.title === "string" && (typeof value.branch === "string" || value.branch === null) && isMessageTupleArray(value.context) && isMessageTupleArray(value.messages);
|
|
712
|
+
}
|
|
713
|
+
function isMessageTupleArray(value) {
|
|
714
|
+
return Array.isArray(value) && value.every((item) => {
|
|
715
|
+
return Array.isArray(item) && item.length === 2 && (item[0] === "u" || item[0] === "a") && typeof item[1] === "string";
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
function isRecord(value) {
|
|
719
|
+
return typeof value === "object" && value !== null;
|
|
720
|
+
}
|
|
721
|
+
function sanitizeForTerminal(value) {
|
|
722
|
+
return value.replace(
|
|
723
|
+
/[\u0000-\u001f\u007f-\u009f]/g,
|
|
724
|
+
(char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// src/commands/install.ts
|
|
729
|
+
var import_fs5 = require("fs");
|
|
730
|
+
var import_path4 = require("path");
|
|
731
|
+
var import_url = require("url");
|
|
732
|
+
var import_meta = {};
|
|
733
|
+
function findSkillSource() {
|
|
734
|
+
const distDir = (() => {
|
|
735
|
+
try {
|
|
736
|
+
return (0, import_path4.dirname)((0, import_url.fileURLToPath)(import_meta.url));
|
|
737
|
+
} catch {
|
|
738
|
+
return (0, import_path4.dirname)(process.argv[1]);
|
|
739
|
+
}
|
|
740
|
+
})();
|
|
741
|
+
const candidates = [
|
|
742
|
+
(0, import_path4.join)(distDir, "..", "skills", "dbrief-note", "SKILL.md"),
|
|
743
|
+
(0, import_path4.join)(distDir, "..", "..", "skills", "dbrief-note", "SKILL.md")
|
|
744
|
+
];
|
|
745
|
+
for (const p of candidates) {
|
|
746
|
+
if ((0, import_fs5.existsSync)(p)) return p;
|
|
747
|
+
}
|
|
748
|
+
throw new Error(
|
|
749
|
+
"Could not find dbrief-note skill.\nIs the package installed correctly?"
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
function installCommand() {
|
|
753
|
+
const cwd = process.cwd();
|
|
754
|
+
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)(
|
|
759
|
+
targetDir,
|
|
760
|
+
`.SKILL.md.tmp-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`
|
|
761
|
+
);
|
|
762
|
+
try {
|
|
763
|
+
(0, import_fs5.copyFileSync)(skillSource, tempFile);
|
|
764
|
+
ensureSafeInstallDestination(targetFile);
|
|
765
|
+
if ((0, import_fs5.existsSync)(targetFile)) {
|
|
766
|
+
(0, import_fs5.unlinkSync)(targetFile);
|
|
767
|
+
}
|
|
768
|
+
(0, import_fs5.renameSync)(tempFile, targetFile);
|
|
769
|
+
} catch (error) {
|
|
770
|
+
(0, import_fs5.rmSync)(tempFile, { force: true });
|
|
771
|
+
throw error;
|
|
772
|
+
}
|
|
773
|
+
console.log(`Installed dbrief-note skill to ${targetFile}`);
|
|
774
|
+
console.log(`
|
|
775
|
+
Usage:`);
|
|
776
|
+
console.log(` dbrief extract`);
|
|
777
|
+
console.log(` $dbrief_note`);
|
|
778
|
+
console.log(` or invoke the skill in any of your coding agents`);
|
|
779
|
+
}
|
|
780
|
+
function ensureSafeInstallDestination(targetFile) {
|
|
781
|
+
if (!(0, import_fs5.existsSync)(targetFile)) {
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
const stats = (0, import_fs5.lstatSync)(targetFile);
|
|
785
|
+
if (!stats.isFile()) {
|
|
786
|
+
throw new Error(`Refusing to overwrite non-regular file: ${targetFile}`);
|
|
787
|
+
}
|
|
788
|
+
if (stats.isSymbolicLink()) {
|
|
789
|
+
throw new Error(`Refusing to overwrite symbolic link: ${targetFile}`);
|
|
790
|
+
}
|
|
791
|
+
if (stats.nlink > 1) {
|
|
792
|
+
throw new Error(`Refusing to overwrite hard-linked file: ${targetFile}`);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// src/cli/index.ts
|
|
797
|
+
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);
|
|
800
|
+
program.command("install").description("Install the dbrief-note skill into the current project").action(installCommand);
|
|
801
|
+
program.command("inspect").description("Inspect a daily artifact").requiredOption("--input <path>", "Input artifact file").option("--format <format>", "Output format (summary)").action(inspectCommand);
|
|
802
|
+
program.parse();
|