@workscribe/cli 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -0
- package/dist/index.js +1234 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
- package/shell/hook.bash +29 -0
- package/shell/hook.zsh +26 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1234 @@
|
|
|
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/index.ts
|
|
27
|
+
var import_commander = require("commander");
|
|
28
|
+
|
|
29
|
+
// src/db/index.ts
|
|
30
|
+
var import_node_sqlite = require("node:sqlite");
|
|
31
|
+
var import_fs = __toESM(require("fs"));
|
|
32
|
+
|
|
33
|
+
// src/utils/paths.ts
|
|
34
|
+
var import_path = __toESM(require("path"));
|
|
35
|
+
var import_os = __toESM(require("os"));
|
|
36
|
+
var CHRONICLE_DIR = import_path.default.join(import_os.default.homedir(), ".chronicle");
|
|
37
|
+
var DB_PATH = import_path.default.join(CHRONICLE_DIR, "chronicle.db");
|
|
38
|
+
var CONFIG_PATH = import_path.default.join(CHRONICLE_DIR, "config.json");
|
|
39
|
+
var DEBUG_LOG_PATH = import_path.default.join(CHRONICLE_DIR, "debug.log");
|
|
40
|
+
var EXPORTS_DIR = import_path.default.join(CHRONICLE_DIR, "exports");
|
|
41
|
+
|
|
42
|
+
// src/db/index.ts
|
|
43
|
+
var _db = null;
|
|
44
|
+
function getDb() {
|
|
45
|
+
if (_db) return _db;
|
|
46
|
+
if (!import_fs.default.existsSync(CHRONICLE_DIR)) {
|
|
47
|
+
import_fs.default.mkdirSync(CHRONICLE_DIR, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
_db = new import_node_sqlite.DatabaseSync(DB_PATH);
|
|
50
|
+
_db.exec("PRAGMA journal_mode = WAL");
|
|
51
|
+
_db.exec("PRAGMA foreign_keys = ON");
|
|
52
|
+
runMigrations(_db);
|
|
53
|
+
return _db;
|
|
54
|
+
}
|
|
55
|
+
function runMigrations(db) {
|
|
56
|
+
db.exec(`
|
|
57
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
58
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
59
|
+
captured_at TEXT NOT NULL,
|
|
60
|
+
command TEXT NOT NULL,
|
|
61
|
+
cwd TEXT NOT NULL,
|
|
62
|
+
exit_code INTEGER,
|
|
63
|
+
duration_ms INTEGER,
|
|
64
|
+
repo_name TEXT,
|
|
65
|
+
branch TEXT,
|
|
66
|
+
category TEXT NOT NULL DEFAULT 'other',
|
|
67
|
+
metadata TEXT
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_events_captured_at ON events(captured_at);
|
|
71
|
+
CREATE INDEX IF NOT EXISTS idx_events_repo_name ON events(repo_name);
|
|
72
|
+
|
|
73
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
74
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
75
|
+
date TEXT NOT NULL,
|
|
76
|
+
started_at TEXT NOT NULL,
|
|
77
|
+
ended_at TEXT NOT NULL,
|
|
78
|
+
repo_name TEXT,
|
|
79
|
+
event_count INTEGER NOT NULL DEFAULT 0,
|
|
80
|
+
summary TEXT
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_date ON sessions(date);
|
|
84
|
+
|
|
85
|
+
CREATE TABLE IF NOT EXISTS session_events (
|
|
86
|
+
session_id INTEGER NOT NULL REFERENCES sessions(id),
|
|
87
|
+
event_id INTEGER NOT NULL REFERENCES events(id),
|
|
88
|
+
PRIMARY KEY (session_id, event_id)
|
|
89
|
+
);
|
|
90
|
+
`);
|
|
91
|
+
}
|
|
92
|
+
function insertEvent(params) {
|
|
93
|
+
const db = getDb();
|
|
94
|
+
const stmt = db.prepare(`
|
|
95
|
+
INSERT INTO events (captured_at, command, cwd, exit_code, duration_ms, repo_name, branch, category, metadata)
|
|
96
|
+
VALUES ($captured_at, $command, $cwd, $exit_code, $duration_ms, $repo_name, $branch, $category, $metadata)
|
|
97
|
+
`);
|
|
98
|
+
stmt.run(params);
|
|
99
|
+
}
|
|
100
|
+
function getLastEvent() {
|
|
101
|
+
const db = getDb();
|
|
102
|
+
const stmt = db.prepare("SELECT * FROM events ORDER BY id DESC LIMIT 1");
|
|
103
|
+
return stmt.get();
|
|
104
|
+
}
|
|
105
|
+
function getEventsForDate(date) {
|
|
106
|
+
const db = getDb();
|
|
107
|
+
const stmt = db.prepare(`
|
|
108
|
+
SELECT * FROM events
|
|
109
|
+
WHERE date(captured_at) = $date
|
|
110
|
+
ORDER BY captured_at ASC
|
|
111
|
+
`);
|
|
112
|
+
return stmt.all({ date });
|
|
113
|
+
}
|
|
114
|
+
function getEventCount() {
|
|
115
|
+
const db = getDb();
|
|
116
|
+
const stmt = db.prepare("SELECT COUNT(*) as count FROM events");
|
|
117
|
+
const row = stmt.get();
|
|
118
|
+
return row.count;
|
|
119
|
+
}
|
|
120
|
+
function insertSession(params) {
|
|
121
|
+
const db = getDb();
|
|
122
|
+
const stmt = db.prepare(`
|
|
123
|
+
INSERT INTO sessions (date, started_at, ended_at, repo_name, event_count, summary)
|
|
124
|
+
VALUES ($date, $started_at, $ended_at, $repo_name, $event_count, $summary)
|
|
125
|
+
`);
|
|
126
|
+
const result = stmt.run(params);
|
|
127
|
+
return Number(result.lastInsertRowid);
|
|
128
|
+
}
|
|
129
|
+
function insertSessionEvent(sessionId, eventId) {
|
|
130
|
+
const db = getDb();
|
|
131
|
+
const stmt = db.prepare(`
|
|
132
|
+
INSERT OR IGNORE INTO session_events (session_id, event_id) VALUES ($session_id, $event_id)
|
|
133
|
+
`);
|
|
134
|
+
stmt.run({ session_id: sessionId, event_id: eventId });
|
|
135
|
+
}
|
|
136
|
+
function getSessionsForDate(date) {
|
|
137
|
+
const db = getDb();
|
|
138
|
+
const stmt = db.prepare("SELECT * FROM sessions WHERE date = $date ORDER BY started_at ASC");
|
|
139
|
+
return stmt.all({ date });
|
|
140
|
+
}
|
|
141
|
+
function updateSessionSummary(sessionId, summary) {
|
|
142
|
+
const db = getDb();
|
|
143
|
+
const stmt = db.prepare("UPDATE sessions SET summary = $summary WHERE id = $id");
|
|
144
|
+
stmt.run({ summary, id: sessionId });
|
|
145
|
+
}
|
|
146
|
+
function clearSessionsForDate(date) {
|
|
147
|
+
const db = getDb();
|
|
148
|
+
db.exec(`
|
|
149
|
+
DELETE FROM session_events WHERE session_id IN (
|
|
150
|
+
SELECT id FROM sessions WHERE date = '${date.replace(/'/g, "''")}'
|
|
151
|
+
)
|
|
152
|
+
`);
|
|
153
|
+
db.exec(`DELETE FROM sessions WHERE date = '${date.replace(/'/g, "''")}'`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// src/capture/redact.ts
|
|
157
|
+
var BUILTIN_PATTERNS = [
|
|
158
|
+
// AWS access keys
|
|
159
|
+
/AKIA[0-9A-Z]{16}/g,
|
|
160
|
+
// AWS secret access keys (40 char base64-ish after common env vars)
|
|
161
|
+
/(?<=AWS_SECRET_ACCESS_KEY[=\s]+)\S+/gi,
|
|
162
|
+
// GitHub tokens
|
|
163
|
+
/ghp_[a-zA-Z0-9]{36}/g,
|
|
164
|
+
/github_pat_[a-zA-Z0-9_]{82}/g,
|
|
165
|
+
// GitLab tokens
|
|
166
|
+
/glpat-[a-zA-Z0-9\-]{20}/g,
|
|
167
|
+
// Generic JWTs (three base64url segments)
|
|
168
|
+
/eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+/g,
|
|
169
|
+
// Slack tokens
|
|
170
|
+
/xox[baprs]-[a-zA-Z0-9\-]+/g,
|
|
171
|
+
// Stripe keys
|
|
172
|
+
/sk_(live|test)_[a-zA-Z0-9]{24,}/g,
|
|
173
|
+
// Values after long-form secret flags (single-char flags like -t/-p/-k are too ambiguous)
|
|
174
|
+
/(?<=--(password|passwd|secret|token|key|api[-_]?key|auth[-_]?token)\s+)\S+/gi,
|
|
175
|
+
// export VAR=VALUE where VAR looks sensitive
|
|
176
|
+
/(?<=export\s+(?:[A-Z_]*(?:TOKEN|SECRET|KEY|PASSWORD|PASS|AUTH|CREDENTIAL)[A-Z_]*)=)\S+/gi
|
|
177
|
+
];
|
|
178
|
+
function redact(command, extraPatterns = []) {
|
|
179
|
+
let result = command;
|
|
180
|
+
const allPatterns = [...BUILTIN_PATTERNS];
|
|
181
|
+
for (const pattern of extraPatterns) {
|
|
182
|
+
try {
|
|
183
|
+
allPatterns.push(new RegExp(pattern, "g"));
|
|
184
|
+
} catch {
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
for (const pattern of allPatterns) {
|
|
188
|
+
pattern.lastIndex = 0;
|
|
189
|
+
result = result.replace(pattern, "[REDACTED]");
|
|
190
|
+
}
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// src/capture/git.ts
|
|
195
|
+
var import_child_process = require("child_process");
|
|
196
|
+
var import_path2 = __toESM(require("path"));
|
|
197
|
+
function getGitContext(cwd) {
|
|
198
|
+
try {
|
|
199
|
+
const root = (0, import_child_process.execSync)("git rev-parse --show-toplevel", {
|
|
200
|
+
cwd,
|
|
201
|
+
timeout: 2e3,
|
|
202
|
+
stdio: "pipe"
|
|
203
|
+
}).toString().trim();
|
|
204
|
+
const repoName = import_path2.default.basename(root);
|
|
205
|
+
let branch = null;
|
|
206
|
+
try {
|
|
207
|
+
branch = (0, import_child_process.execSync)("git branch --show-current", {
|
|
208
|
+
cwd,
|
|
209
|
+
timeout: 2e3,
|
|
210
|
+
stdio: "pipe"
|
|
211
|
+
}).toString().trim() || null;
|
|
212
|
+
} catch {
|
|
213
|
+
}
|
|
214
|
+
return { repoName, branch };
|
|
215
|
+
} catch {
|
|
216
|
+
return { repoName: null, branch: null };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// src/normalize/patterns.ts
|
|
221
|
+
function makeRule(pattern, category, extract = () => ({})) {
|
|
222
|
+
return {
|
|
223
|
+
match(cmd) {
|
|
224
|
+
const m = cmd.match(pattern);
|
|
225
|
+
if (!m) return null;
|
|
226
|
+
return { category, metadata: extract(m, cmd) };
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function extractPackages(cmd, afterKeyword) {
|
|
231
|
+
const idx = cmd.indexOf(afterKeyword);
|
|
232
|
+
if (idx === -1) return [];
|
|
233
|
+
return cmd.slice(idx + afterKeyword.length).trim().split(/\s+/).filter((p) => p && !p.startsWith("-"));
|
|
234
|
+
}
|
|
235
|
+
var RULES = [
|
|
236
|
+
// ─── Git operations ───────────────────────────────────────────────────────
|
|
237
|
+
makeRule(/^git\s+commit\b/, "code_commit", (_m, cmd) => {
|
|
238
|
+
const msgMatch = cmd.match(/-m\s+["']?([^"']+)["']?/);
|
|
239
|
+
return { message: msgMatch ? msgMatch[1].trim() : null };
|
|
240
|
+
}),
|
|
241
|
+
makeRule(/^git\s+push\b/, "code_push", (_m, cmd) => {
|
|
242
|
+
const parts = cmd.split(/\s+/);
|
|
243
|
+
return { remote: parts[2] ?? "origin", branch: parts[3] ?? null };
|
|
244
|
+
}),
|
|
245
|
+
makeRule(/^git\s+(pull|fetch)\b/, "code_pull", (_m, cmd) => {
|
|
246
|
+
const parts = cmd.split(/\s+/);
|
|
247
|
+
return { subcommand: parts[1], remote: parts[2] ?? "origin" };
|
|
248
|
+
}),
|
|
249
|
+
makeRule(/^git\s+(checkout|switch)\b/, "branch_switch", (_m, cmd) => {
|
|
250
|
+
const parts = cmd.split(/\s+/);
|
|
251
|
+
const branch = parts.find((p) => !p.startsWith("-") && p !== "git" && p !== parts[1]);
|
|
252
|
+
return { branch: branch ?? null };
|
|
253
|
+
}),
|
|
254
|
+
makeRule(/^git\s+(merge|rebase)\b/, "code_merge", (_m, cmd) => {
|
|
255
|
+
const parts = cmd.split(/\s+/);
|
|
256
|
+
return { subcommand: parts[1], target: parts[2] ?? null };
|
|
257
|
+
}),
|
|
258
|
+
makeRule(/^git\s+stash\b/, "code_stash", (_m, cmd) => {
|
|
259
|
+
const parts = cmd.split(/\s+/);
|
|
260
|
+
return { subcommand: parts[2] ?? "push" };
|
|
261
|
+
}),
|
|
262
|
+
makeRule(/^git\s+/, "git_other", (_m, cmd) => {
|
|
263
|
+
const parts = cmd.split(/\s+/);
|
|
264
|
+
return { subcommand: parts[1] ?? null };
|
|
265
|
+
}),
|
|
266
|
+
// ─── Package managers — install ───────────────────────────────────────────
|
|
267
|
+
makeRule(/^npm\s+(install|i|add)\b/, "dependency_install", (_m, cmd) => ({
|
|
268
|
+
tool: "npm",
|
|
269
|
+
packages: extractPackages(cmd, cmd.includes("install") ? "install" : cmd.includes(" i ") ? " i " : "add")
|
|
270
|
+
})),
|
|
271
|
+
makeRule(/^(yarn|pnpm)\s+(install|add)\b/, "dependency_install", (m, cmd) => ({
|
|
272
|
+
tool: m[1],
|
|
273
|
+
packages: extractPackages(cmd, "add")
|
|
274
|
+
})),
|
|
275
|
+
makeRule(/^pip\s+(install)\b/, "dependency_install", (_m, cmd) => ({
|
|
276
|
+
tool: "pip",
|
|
277
|
+
packages: extractPackages(cmd, "install")
|
|
278
|
+
})),
|
|
279
|
+
makeRule(/^cargo\s+add\b/, "dependency_install", (_m, cmd) => ({
|
|
280
|
+
tool: "cargo",
|
|
281
|
+
packages: extractPackages(cmd, "add")
|
|
282
|
+
})),
|
|
283
|
+
makeRule(/^(brew)\s+install\b/, "dependency_install", (_m, cmd) => ({
|
|
284
|
+
tool: "brew",
|
|
285
|
+
packages: extractPackages(cmd, "install")
|
|
286
|
+
})),
|
|
287
|
+
makeRule(/^go\s+get\b/, "dependency_install", (_m, cmd) => ({
|
|
288
|
+
tool: "go",
|
|
289
|
+
packages: extractPackages(cmd, "get")
|
|
290
|
+
})),
|
|
291
|
+
// ─── Package managers — remove ────────────────────────────────────────────
|
|
292
|
+
makeRule(/^npm\s+(uninstall|remove|rm)\b/, "dependency_remove", (_m, cmd) => ({
|
|
293
|
+
tool: "npm",
|
|
294
|
+
packages: extractPackages(cmd, cmd.split(/\s+/)[1])
|
|
295
|
+
})),
|
|
296
|
+
makeRule(/^(yarn|pnpm)\s+remove\b/, "dependency_remove", (m, cmd) => ({
|
|
297
|
+
tool: m[1],
|
|
298
|
+
packages: extractPackages(cmd, "remove")
|
|
299
|
+
})),
|
|
300
|
+
makeRule(/^pip\s+uninstall\b/, "dependency_remove", (_m, cmd) => ({
|
|
301
|
+
tool: "pip",
|
|
302
|
+
packages: extractPackages(cmd, "uninstall")
|
|
303
|
+
})),
|
|
304
|
+
// ─── Tests ────────────────────────────────────────────────────────────────
|
|
305
|
+
makeRule(/^(npm\s+(test|run\s+test)|yarn\s+test|pnpm\s+test)\b/, "test_run", () => ({ tool: "npm" })),
|
|
306
|
+
makeRule(/^(jest|vitest)\b/, "test_run", (m) => ({ tool: m[1] })),
|
|
307
|
+
makeRule(/^pytest\b/, "test_run", () => ({ tool: "pytest" })),
|
|
308
|
+
makeRule(/^go\s+test\b/, "test_run", () => ({ tool: "go" })),
|
|
309
|
+
makeRule(/^cargo\s+test\b/, "test_run", () => ({ tool: "cargo" })),
|
|
310
|
+
makeRule(/^rspec\b/, "test_run", () => ({ tool: "rspec" })),
|
|
311
|
+
makeRule(/^(php\s+)?phpunit\b/, "test_run", () => ({ tool: "phpunit" })),
|
|
312
|
+
// ─── Build ────────────────────────────────────────────────────────────────
|
|
313
|
+
makeRule(/^npm\s+run\s+build\b/, "build", () => ({ tool: "npm" })),
|
|
314
|
+
makeRule(/^(yarn|pnpm)\s+build\b/, "build", (m) => ({ tool: m[1] })),
|
|
315
|
+
makeRule(/^(make|gmake)\b/, "build", (m) => ({ tool: m[1] })),
|
|
316
|
+
makeRule(/^cargo\s+build\b/, "build", () => ({ tool: "cargo" })),
|
|
317
|
+
makeRule(/^go\s+build\b/, "build", () => ({ tool: "go" })),
|
|
318
|
+
makeRule(/^docker\s+build\b/, "build", () => ({ tool: "docker" })),
|
|
319
|
+
makeRule(/^(mvn|gradle)\b/, "build", (m) => ({ tool: m[1] })),
|
|
320
|
+
// ─── Dev server ───────────────────────────────────────────────────────────
|
|
321
|
+
makeRule(/^npm\s+run\s+(dev|start|serve)\b/, "server_run", (_m, cmd) => ({
|
|
322
|
+
tool: "npm",
|
|
323
|
+
script: cmd.split(/\s+/)[2]
|
|
324
|
+
})),
|
|
325
|
+
makeRule(/^(yarn|pnpm)\s+(dev|start)\b/, "server_run", (m) => ({ tool: m[1], script: m[2] })),
|
|
326
|
+
makeRule(/^python\s+.*manage\.py\s+runserver\b/, "server_run", () => ({ tool: "django" })),
|
|
327
|
+
// ─── Infrastructure ───────────────────────────────────────────────────────
|
|
328
|
+
makeRule(/^ssh\b/, "infrastructure", (_m, cmd) => {
|
|
329
|
+
const parts = cmd.split(/\s+/);
|
|
330
|
+
return { tool: "ssh", target: parts[parts.length - 1] };
|
|
331
|
+
}),
|
|
332
|
+
makeRule(/^kubectl\b/, "infrastructure", (_m, cmd) => {
|
|
333
|
+
const parts = cmd.split(/\s+/);
|
|
334
|
+
return { tool: "kubectl", subcommand: parts[1] ?? null };
|
|
335
|
+
}),
|
|
336
|
+
makeRule(/^(helm|terraform|pulumi)\b/, "infrastructure", (m, cmd) => {
|
|
337
|
+
const parts = cmd.split(/\s+/);
|
|
338
|
+
return { tool: m[1], subcommand: parts[1] ?? null };
|
|
339
|
+
}),
|
|
340
|
+
makeRule(/^docker\s+(run|compose|ps|stop|start|rm|exec)\b/, "infrastructure", (m, cmd) => {
|
|
341
|
+
const parts = cmd.split(/\s+/);
|
|
342
|
+
return { tool: "docker", subcommand: parts[1] };
|
|
343
|
+
}),
|
|
344
|
+
makeRule(/^(aws|gcloud|az)\b/, "infrastructure", (m, cmd) => {
|
|
345
|
+
const parts = cmd.split(/\s+/);
|
|
346
|
+
return { tool: m[1], subcommand: parts[1] ?? null };
|
|
347
|
+
}),
|
|
348
|
+
// ─── Scripts ──────────────────────────────────────────────────────────────
|
|
349
|
+
makeRule(/^(bash|sh|zsh|fish)\s+\S+/, "script_run", (m) => ({ shell: m[1] })),
|
|
350
|
+
makeRule(/^node\s+\S+\.(?:js|mjs|cjs)\b/, "script_run", () => ({ runtime: "node" })),
|
|
351
|
+
makeRule(/^python3?\s+\S+\.py\b/, "script_run", () => ({ runtime: "python" })),
|
|
352
|
+
// ─── Environment ──────────────────────────────────────────────────────────
|
|
353
|
+
makeRule(/^export\s+\w+=/, "environment", (_m, cmd) => {
|
|
354
|
+
const varName = cmd.match(/^export\s+(\w+)=/)?.[1] ?? null;
|
|
355
|
+
return { variable: varName };
|
|
356
|
+
}),
|
|
357
|
+
makeRule(/^source\s+/, "environment", () => ({})),
|
|
358
|
+
// ─── npm scripts (generic) ────────────────────────────────────────────────
|
|
359
|
+
makeRule(/^npm\s+run\s+\S+/, "script_run", (_m, cmd) => ({
|
|
360
|
+
tool: "npm",
|
|
361
|
+
script: cmd.split(/\s+/)[2]
|
|
362
|
+
}))
|
|
363
|
+
];
|
|
364
|
+
function normalizeCommand(command) {
|
|
365
|
+
const trimmed = command.trim().replace(/\s+/g, " ");
|
|
366
|
+
for (const rule of RULES) {
|
|
367
|
+
const result = rule.match(trimmed);
|
|
368
|
+
if (result) return result;
|
|
369
|
+
}
|
|
370
|
+
return { category: "other", metadata: {} };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/config/index.ts
|
|
374
|
+
var import_fs2 = __toESM(require("fs"));
|
|
375
|
+
var DEFAULTS = {
|
|
376
|
+
ai: {
|
|
377
|
+
provider: "",
|
|
378
|
+
apiKey: "",
|
|
379
|
+
model: "",
|
|
380
|
+
baseUrl: ""
|
|
381
|
+
},
|
|
382
|
+
session: {
|
|
383
|
+
idleTimeout: 30
|
|
384
|
+
},
|
|
385
|
+
capture: {
|
|
386
|
+
ignore: ["cd", "ls", "ll", "la", "ls -la", "ls -l", "ls -a", "pwd", "clear", "exit", "history", "man"]
|
|
387
|
+
},
|
|
388
|
+
redact: {
|
|
389
|
+
extra: []
|
|
390
|
+
},
|
|
391
|
+
summary: {
|
|
392
|
+
format: "markdown"
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
function readConfig() {
|
|
396
|
+
if (!import_fs2.default.existsSync(CONFIG_PATH)) return { ...DEFAULTS };
|
|
397
|
+
try {
|
|
398
|
+
const raw = import_fs2.default.readFileSync(CONFIG_PATH, "utf-8");
|
|
399
|
+
const parsed = JSON.parse(raw);
|
|
400
|
+
return deepMerge(DEFAULTS, parsed);
|
|
401
|
+
} catch {
|
|
402
|
+
return { ...DEFAULTS };
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
function writeConfig(config) {
|
|
406
|
+
import_fs2.default.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
|
|
407
|
+
}
|
|
408
|
+
function getConfigValue(config, key) {
|
|
409
|
+
const parts = key.split(".");
|
|
410
|
+
let cur = config;
|
|
411
|
+
for (const part of parts) {
|
|
412
|
+
if (cur == null || typeof cur !== "object") return void 0;
|
|
413
|
+
cur = cur[part];
|
|
414
|
+
}
|
|
415
|
+
return cur;
|
|
416
|
+
}
|
|
417
|
+
function setConfigValue(config, key, value) {
|
|
418
|
+
const parts = key.split(".");
|
|
419
|
+
const result = JSON.parse(JSON.stringify(config));
|
|
420
|
+
let cur = result;
|
|
421
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
422
|
+
const part = parts[i];
|
|
423
|
+
if (cur[part] == null || typeof cur[part] !== "object") {
|
|
424
|
+
cur[part] = {};
|
|
425
|
+
}
|
|
426
|
+
cur = cur[part];
|
|
427
|
+
}
|
|
428
|
+
const lastKey = parts[parts.length - 1];
|
|
429
|
+
if (value === "true") cur[lastKey] = true;
|
|
430
|
+
else if (value === "false") cur[lastKey] = false;
|
|
431
|
+
else if (!isNaN(Number(value))) cur[lastKey] = Number(value);
|
|
432
|
+
else cur[lastKey] = value;
|
|
433
|
+
return result;
|
|
434
|
+
}
|
|
435
|
+
function deepMerge(defaults, overrides) {
|
|
436
|
+
const result = { ...defaults };
|
|
437
|
+
for (const key of Object.keys(overrides)) {
|
|
438
|
+
const val = overrides[key];
|
|
439
|
+
if (val !== null && typeof val === "object" && !Array.isArray(val) && typeof defaults[key] === "object") {
|
|
440
|
+
result[key] = deepMerge(defaults[key], val);
|
|
441
|
+
} else if (val !== void 0) {
|
|
442
|
+
result[key] = val;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return result;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// src/capture/filter.ts
|
|
449
|
+
function normalizeForFilter(cmd) {
|
|
450
|
+
return cmd.trim().replace(/\s+/g, " ");
|
|
451
|
+
}
|
|
452
|
+
function shouldFilter(command, ignoreList) {
|
|
453
|
+
const normalized2 = normalizeForFilter(command);
|
|
454
|
+
if (!normalized2) return true;
|
|
455
|
+
const normalizedIgnore = ignoreList.map(normalizeForFilter);
|
|
456
|
+
if (normalizedIgnore.includes(normalized2)) return true;
|
|
457
|
+
const baseCmd = normalized2.split(" ")[0];
|
|
458
|
+
if (normalizedIgnore.includes(baseCmd)) return true;
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
function isDuplicate(command, cwd, lastEvent, dedupWindowMs = 5e3, now = Date.now()) {
|
|
462
|
+
if (!lastEvent) return false;
|
|
463
|
+
const lastTime = new Date(lastEvent.captured_at).getTime();
|
|
464
|
+
return lastEvent.command === normalizeForFilter(command) && lastEvent.cwd === cwd && now - lastTime < dedupWindowMs;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// src/capture/index.ts
|
|
468
|
+
function captureCommand(opts) {
|
|
469
|
+
const config = readConfig();
|
|
470
|
+
const { command, cwd, exitCode, durationMs } = opts;
|
|
471
|
+
if (shouldFilter(command, config.capture.ignore)) return;
|
|
472
|
+
const last = getLastEvent();
|
|
473
|
+
if (isDuplicate(command, cwd, last ?? void 0)) return;
|
|
474
|
+
const safeCommand = redact(normalized, config.redact.extra);
|
|
475
|
+
const { repoName, branch } = getGitContext(cwd);
|
|
476
|
+
const { category, metadata } = normalizeCommand(safeCommand);
|
|
477
|
+
insertEvent({
|
|
478
|
+
captured_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
479
|
+
command: safeCommand,
|
|
480
|
+
cwd,
|
|
481
|
+
exit_code: exitCode,
|
|
482
|
+
duration_ms: durationMs,
|
|
483
|
+
repo_name: repoName,
|
|
484
|
+
branch,
|
|
485
|
+
category,
|
|
486
|
+
metadata: Object.keys(metadata).length > 0 ? JSON.stringify(metadata) : null
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// src/utils/logger.ts
|
|
491
|
+
var import_chalk = __toESM(require("chalk"));
|
|
492
|
+
var import_fs3 = __toESM(require("fs"));
|
|
493
|
+
var isDebug = process.env.CHRONICLE_DEBUG === "1";
|
|
494
|
+
var log = {
|
|
495
|
+
info: (msg) => console.log(import_chalk.default.cyan("\u2192"), msg),
|
|
496
|
+
success: (msg) => console.log(import_chalk.default.green("\u2713"), msg),
|
|
497
|
+
warn: (msg) => console.log(import_chalk.default.yellow("\u26A0"), msg),
|
|
498
|
+
error: (msg) => console.error(import_chalk.default.red("\u2717"), msg),
|
|
499
|
+
plain: (msg) => console.log(msg)
|
|
500
|
+
};
|
|
501
|
+
function debugLog(context, error) {
|
|
502
|
+
if (!isDebug) return;
|
|
503
|
+
const entry = `[${(/* @__PURE__ */ new Date()).toISOString()}] [${context}] ${error instanceof Error ? error.stack : String(error)}
|
|
504
|
+
`;
|
|
505
|
+
try {
|
|
506
|
+
import_fs3.default.appendFileSync(DEBUG_LOG_PATH, entry);
|
|
507
|
+
} catch {
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// src/cli/capture.ts
|
|
512
|
+
function registerCapture(program2) {
|
|
513
|
+
program2.command("_capture").description("Internal: capture a shell command event (called by shell hook)").option("--cmd <command>", "command that was executed").option("--cwd <path>", "working directory").option("--exit <code>", "exit code", parseInt).option("--duration <ms>", "duration in milliseconds", parseInt).action((opts) => {
|
|
514
|
+
try {
|
|
515
|
+
if (!opts.cmd || !opts.cwd) return;
|
|
516
|
+
captureCommand({
|
|
517
|
+
command: opts.cmd,
|
|
518
|
+
cwd: opts.cwd,
|
|
519
|
+
exitCode: opts.exit ?? null,
|
|
520
|
+
durationMs: opts.duration ?? null
|
|
521
|
+
});
|
|
522
|
+
} catch (err) {
|
|
523
|
+
debugLog("_capture", err);
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// src/cli/init.ts
|
|
529
|
+
var import_fs4 = __toESM(require("fs"));
|
|
530
|
+
var import_path3 = __toESM(require("path"));
|
|
531
|
+
var import_os2 = __toESM(require("os"));
|
|
532
|
+
var import_prompts = __toESM(require("prompts"));
|
|
533
|
+
var import_chalk2 = __toESM(require("chalk"));
|
|
534
|
+
var HOOK_START = "# chronicle:hook:start";
|
|
535
|
+
function detectShell() {
|
|
536
|
+
const shell = process.env.SHELL ?? "";
|
|
537
|
+
if (shell.includes("zsh")) return { name: "zsh", rcFile: import_path3.default.join(import_os2.default.homedir(), ".zshrc") };
|
|
538
|
+
if (shell.includes("bash")) return { name: "bash", rcFile: import_path3.default.join(import_os2.default.homedir(), ".bashrc") };
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
function isHookInstalled(rcFile) {
|
|
542
|
+
if (!import_fs4.default.existsSync(rcFile)) return false;
|
|
543
|
+
return import_fs4.default.readFileSync(rcFile, "utf-8").includes(HOOK_START);
|
|
544
|
+
}
|
|
545
|
+
function installHook(rcFile, hookContent) {
|
|
546
|
+
const backup = `${rcFile}.chronicle-backup-${Date.now()}`;
|
|
547
|
+
import_fs4.default.copyFileSync(rcFile, backup);
|
|
548
|
+
log.info(`Backed up ${rcFile} \u2192 ${import_path3.default.basename(backup)}`);
|
|
549
|
+
import_fs4.default.appendFileSync(rcFile, `
|
|
550
|
+
${hookContent}
|
|
551
|
+
`);
|
|
552
|
+
}
|
|
553
|
+
function registerInit(program2) {
|
|
554
|
+
program2.command("init").description("Install shell hooks and set up Chronicle").action(async () => {
|
|
555
|
+
log.plain(import_chalk2.default.bold("\n Chronicle \u2014 Developer Work Journal\n"));
|
|
556
|
+
if (!import_fs4.default.existsSync(CHRONICLE_DIR)) {
|
|
557
|
+
import_fs4.default.mkdirSync(CHRONICLE_DIR, { recursive: true });
|
|
558
|
+
log.success(`Created ${CHRONICLE_DIR}`);
|
|
559
|
+
}
|
|
560
|
+
getDb();
|
|
561
|
+
log.success("Database initialized");
|
|
562
|
+
const shell = detectShell();
|
|
563
|
+
if (!shell) {
|
|
564
|
+
log.warn("Could not detect shell. Supported: zsh, bash");
|
|
565
|
+
log.plain(`Manually add the hook from: ${import_chalk2.default.cyan("shell/hook.zsh")} or ${import_chalk2.default.cyan("shell/hook.bash")}`);
|
|
566
|
+
} else {
|
|
567
|
+
if (isHookInstalled(shell.rcFile)) {
|
|
568
|
+
log.info(`Shell hook already installed in ${shell.rcFile}`);
|
|
569
|
+
} else {
|
|
570
|
+
const hookFile = import_path3.default.join(__dirname, "..", "shell", `hook.${shell.name}`);
|
|
571
|
+
if (!import_fs4.default.existsSync(hookFile)) {
|
|
572
|
+
log.warn(`Hook template not found at ${hookFile}`);
|
|
573
|
+
} else {
|
|
574
|
+
const hookContent = import_fs4.default.readFileSync(hookFile, "utf-8");
|
|
575
|
+
if (!import_fs4.default.existsSync(shell.rcFile)) import_fs4.default.writeFileSync(shell.rcFile, "");
|
|
576
|
+
installHook(shell.rcFile, hookContent);
|
|
577
|
+
log.success(`Shell hook installed in ${shell.rcFile}`);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
const config = readConfig();
|
|
582
|
+
if (config.ai.provider) {
|
|
583
|
+
log.info(`AI provider already configured: ${import_chalk2.default.cyan(config.ai.provider)}`);
|
|
584
|
+
} else {
|
|
585
|
+
log.plain("");
|
|
586
|
+
const { provider } = await (0, import_prompts.default)({
|
|
587
|
+
type: "select",
|
|
588
|
+
name: "provider",
|
|
589
|
+
message: "Which AI provider do you want to use?",
|
|
590
|
+
choices: [
|
|
591
|
+
{ title: "Anthropic (Claude) \u2014 requires API key", value: "anthropic" },
|
|
592
|
+
{ title: "OpenAI \u2014 requires API key", value: "openai" },
|
|
593
|
+
{ title: "Ollama (local) \u2014 free, runs on your machine", value: "ollama" },
|
|
594
|
+
{ title: "Other (OpenAI-compatible) \u2014 Groq, Together, Mistral\u2026", value: "openai-compatible" },
|
|
595
|
+
{ title: "Skip \u2014 configure later", value: "skip" }
|
|
596
|
+
]
|
|
597
|
+
});
|
|
598
|
+
if (provider && provider !== "skip") {
|
|
599
|
+
config.ai.provider = provider;
|
|
600
|
+
if (provider === "ollama") {
|
|
601
|
+
log.plain(`
|
|
602
|
+
${import_chalk2.default.dim("Make sure Ollama is running: ollama serve")}`);
|
|
603
|
+
log.plain(` ${import_chalk2.default.dim("Default model: llama3 (change with: chronicle config set ai.model <model>)")}
|
|
604
|
+
`);
|
|
605
|
+
} else {
|
|
606
|
+
const { apiKey } = await (0, import_prompts.default)({
|
|
607
|
+
type: "password",
|
|
608
|
+
name: "apiKey",
|
|
609
|
+
message: `Enter your ${provider === "openai-compatible" ? "" : provider + " "}API key:`
|
|
610
|
+
});
|
|
611
|
+
if (apiKey) config.ai.apiKey = apiKey;
|
|
612
|
+
if (provider === "openai-compatible") {
|
|
613
|
+
const { baseUrl } = await (0, import_prompts.default)({
|
|
614
|
+
type: "text",
|
|
615
|
+
name: "baseUrl",
|
|
616
|
+
message: "Enter the base URL (e.g. https://api.groq.com/openai/v1):"
|
|
617
|
+
});
|
|
618
|
+
if (baseUrl) config.ai.baseUrl = baseUrl;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
writeConfig(config);
|
|
622
|
+
log.success(`AI provider set to ${import_chalk2.default.cyan(provider)}`);
|
|
623
|
+
} else if (provider === "skip" || !provider) {
|
|
624
|
+
log.info("Skipped \u2014 run later: chronicle config set ai.provider <provider>");
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
if (!import_fs4.default.existsSync(CONFIG_PATH)) {
|
|
628
|
+
writeConfig(config);
|
|
629
|
+
log.success("Config file created");
|
|
630
|
+
}
|
|
631
|
+
log.plain("");
|
|
632
|
+
log.plain(import_chalk2.default.bold(" Setup complete!"));
|
|
633
|
+
if (shell) {
|
|
634
|
+
log.plain("");
|
|
635
|
+
log.plain(` Activate capture now: ${import_chalk2.default.cyan(`source ${shell.rcFile}`)}`);
|
|
636
|
+
log.plain(` Or restart your terminal.`);
|
|
637
|
+
}
|
|
638
|
+
log.plain("");
|
|
639
|
+
log.plain(` ${import_chalk2.default.dim("chronicle summary")} \u2014 generate today's work summary`);
|
|
640
|
+
log.plain(` ${import_chalk2.default.dim("chronicle status")} \u2014 check capture is working`);
|
|
641
|
+
log.plain("");
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// src/cli/status.ts
|
|
646
|
+
var import_chalk3 = __toESM(require("chalk"));
|
|
647
|
+
|
|
648
|
+
// src/utils/format.ts
|
|
649
|
+
function formatDuration(minutes) {
|
|
650
|
+
if (minutes < 60) return `${Math.round(minutes)}m`;
|
|
651
|
+
const h = Math.floor(minutes / 60);
|
|
652
|
+
const m = Math.round(minutes % 60);
|
|
653
|
+
return m > 0 ? `${h}h ${m}m` : `${h}h`;
|
|
654
|
+
}
|
|
655
|
+
function formatDate(isoString) {
|
|
656
|
+
return new Date(isoString).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
657
|
+
}
|
|
658
|
+
function todayDate() {
|
|
659
|
+
return (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
660
|
+
}
|
|
661
|
+
function parseDate(input) {
|
|
662
|
+
const d = /* @__PURE__ */ new Date(input + "T00:00:00");
|
|
663
|
+
if (isNaN(d.getTime())) throw new Error(`Invalid date: ${input}`);
|
|
664
|
+
return input;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// src/cli/status.ts
|
|
668
|
+
function registerStatus(program2) {
|
|
669
|
+
program2.command("status").description("Show capture status and event counts").action(() => {
|
|
670
|
+
const config = readConfig();
|
|
671
|
+
const total = getEventCount();
|
|
672
|
+
const todayEvents = getEventsForDate(todayDate());
|
|
673
|
+
const last = getLastEvent();
|
|
674
|
+
log.plain("");
|
|
675
|
+
log.plain(import_chalk3.default.bold(" Chronicle Status"));
|
|
676
|
+
log.plain("");
|
|
677
|
+
log.plain(` Capture ${import_chalk3.default.green("active")}`);
|
|
678
|
+
log.plain(` Events today ${import_chalk3.default.cyan(String(todayEvents.length))}`);
|
|
679
|
+
log.plain(` Events total ${import_chalk3.default.cyan(String(total))}`);
|
|
680
|
+
if (last) {
|
|
681
|
+
log.plain(` Last event ${formatDate(last.captured_at)} ${import_chalk3.default.dim(last.command.slice(0, 60))}`);
|
|
682
|
+
}
|
|
683
|
+
log.plain("");
|
|
684
|
+
log.plain(` AI provider ${config.ai.provider ? import_chalk3.default.cyan(config.ai.provider) : import_chalk3.default.yellow("not configured")}`);
|
|
685
|
+
if (config.ai.model) log.plain(` Model ${import_chalk3.default.dim(config.ai.model)}`);
|
|
686
|
+
log.plain("");
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// src/cli/events.ts
|
|
691
|
+
var import_chalk4 = __toESM(require("chalk"));
|
|
692
|
+
var CATEGORY_COLORS = {
|
|
693
|
+
code_commit: import_chalk4.default.green,
|
|
694
|
+
code_push: import_chalk4.default.green,
|
|
695
|
+
code_pull: import_chalk4.default.blue,
|
|
696
|
+
branch_switch: import_chalk4.default.blue,
|
|
697
|
+
dependency_install: import_chalk4.default.yellow,
|
|
698
|
+
dependency_remove: import_chalk4.default.yellow,
|
|
699
|
+
test_run: import_chalk4.default.magenta,
|
|
700
|
+
build: import_chalk4.default.cyan,
|
|
701
|
+
server_run: import_chalk4.default.cyan,
|
|
702
|
+
infrastructure: import_chalk4.default.red,
|
|
703
|
+
script_run: import_chalk4.default.white,
|
|
704
|
+
environment: import_chalk4.default.gray,
|
|
705
|
+
git_other: import_chalk4.default.blue,
|
|
706
|
+
code_merge: import_chalk4.default.green,
|
|
707
|
+
code_stash: import_chalk4.default.blue,
|
|
708
|
+
other: import_chalk4.default.gray
|
|
709
|
+
};
|
|
710
|
+
function registerEvents(program2) {
|
|
711
|
+
program2.command("events").description("List captured events").option("--date <YYYY-MM-DD>", "date to list events for (default: today)").option("--limit <n>", "max events to show", parseInt).action((opts) => {
|
|
712
|
+
let date = todayDate();
|
|
713
|
+
if (opts.date) {
|
|
714
|
+
try {
|
|
715
|
+
date = parseDate(opts.date);
|
|
716
|
+
} catch (e) {
|
|
717
|
+
log.error(e.message);
|
|
718
|
+
process.exit(1);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
const events = getEventsForDate(date);
|
|
722
|
+
const limit = opts.limit ?? events.length;
|
|
723
|
+
if (events.length === 0) {
|
|
724
|
+
log.plain(`No events captured for ${date}`);
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
log.plain("");
|
|
728
|
+
log.plain(import_chalk4.default.bold(` Events for ${date} (${events.length} total)`));
|
|
729
|
+
log.plain("");
|
|
730
|
+
for (const event of events.slice(0, limit)) {
|
|
731
|
+
const colorFn = CATEGORY_COLORS[event.category] ?? import_chalk4.default.gray;
|
|
732
|
+
const time = formatDate(event.captured_at);
|
|
733
|
+
const category = colorFn(event.category.padEnd(20));
|
|
734
|
+
const cmd = import_chalk4.default.dim(event.command.slice(0, 55));
|
|
735
|
+
const repo = event.repo_name ? import_chalk4.default.dim(` [${event.repo_name}]`) : "";
|
|
736
|
+
const exitBadge = event.exit_code && event.exit_code !== 0 ? import_chalk4.default.red(` \u2717${event.exit_code}`) : "";
|
|
737
|
+
log.plain(` ${import_chalk4.default.dim(time)} ${category} ${cmd}${repo}${exitBadge}`);
|
|
738
|
+
}
|
|
739
|
+
if (limit < events.length) {
|
|
740
|
+
log.plain("");
|
|
741
|
+
log.plain(import_chalk4.default.dim(` \u2026 and ${events.length - limit} more (remove --limit to see all)`));
|
|
742
|
+
}
|
|
743
|
+
log.plain("");
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// src/cli/sessions.ts
|
|
748
|
+
var import_chalk5 = __toESM(require("chalk"));
|
|
749
|
+
|
|
750
|
+
// src/sessions/grouper.ts
|
|
751
|
+
function splitByIdleGap(events, idleMs) {
|
|
752
|
+
if (events.length === 0) return [];
|
|
753
|
+
const groups = [[events[0]]];
|
|
754
|
+
for (let i = 1; i < events.length; i++) {
|
|
755
|
+
const prev = new Date(events[i - 1].captured_at).getTime();
|
|
756
|
+
const curr = new Date(events[i].captured_at).getTime();
|
|
757
|
+
if (curr - prev >= idleMs) {
|
|
758
|
+
groups.push([events[i]]);
|
|
759
|
+
} else {
|
|
760
|
+
groups[groups.length - 1].push(events[i]);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
return groups;
|
|
764
|
+
}
|
|
765
|
+
function splitByRepo(events) {
|
|
766
|
+
if (events.length === 0) return [];
|
|
767
|
+
const groups = [[events[0]]];
|
|
768
|
+
for (let i = 1; i < events.length; i++) {
|
|
769
|
+
const prevRepo = events[i - 1].repo_name;
|
|
770
|
+
const currRepo = events[i].repo_name;
|
|
771
|
+
if (currRepo !== prevRepo) {
|
|
772
|
+
groups.push([events[i]]);
|
|
773
|
+
} else {
|
|
774
|
+
groups[groups.length - 1].push(events[i]);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
return groups;
|
|
778
|
+
}
|
|
779
|
+
function minutesBetween(start, end) {
|
|
780
|
+
return (new Date(end).getTime() - new Date(start).getTime()) / 6e4;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// src/sessions/detector.ts
|
|
784
|
+
function getOrBuildSessions(date) {
|
|
785
|
+
const existing = getSessionsForDate(date);
|
|
786
|
+
const events = getEventsForDate(date);
|
|
787
|
+
if (events.length === 0) return [];
|
|
788
|
+
const cachedEventCount = existing.reduce((sum, s) => sum + s.event_count, 0);
|
|
789
|
+
if (existing.length > 0 && cachedEventCount === events.length) {
|
|
790
|
+
return existing.map((s) => ({
|
|
791
|
+
id: s.id,
|
|
792
|
+
date: s.date,
|
|
793
|
+
startedAt: s.started_at,
|
|
794
|
+
endedAt: s.ended_at,
|
|
795
|
+
repoName: s.repo_name,
|
|
796
|
+
durationMinutes: minutesBetween(s.started_at, s.ended_at),
|
|
797
|
+
eventCount: s.event_count,
|
|
798
|
+
summary: s.summary,
|
|
799
|
+
events: []
|
|
800
|
+
// not needed when using cached
|
|
801
|
+
}));
|
|
802
|
+
}
|
|
803
|
+
clearSessionsForDate(date);
|
|
804
|
+
return buildSessions(date, events);
|
|
805
|
+
}
|
|
806
|
+
function buildSessions(date, events) {
|
|
807
|
+
const config = readConfig();
|
|
808
|
+
const idleMs = config.session.idleTimeout * 60 * 1e3;
|
|
809
|
+
const sessions = [];
|
|
810
|
+
const timeGroups = splitByIdleGap(events, idleMs);
|
|
811
|
+
for (const group of timeGroups) {
|
|
812
|
+
if (group.length === 0) continue;
|
|
813
|
+
const repoGroups = splitByRepo(group);
|
|
814
|
+
for (const repoGroup of repoGroups) {
|
|
815
|
+
if (repoGroup.length === 0) continue;
|
|
816
|
+
const first = repoGroup[0];
|
|
817
|
+
const last = repoGroup[repoGroup.length - 1];
|
|
818
|
+
const repoName = last.repo_name ?? first.repo_name ?? null;
|
|
819
|
+
const sessionId = insertSession({
|
|
820
|
+
date,
|
|
821
|
+
started_at: first.captured_at,
|
|
822
|
+
ended_at: last.captured_at,
|
|
823
|
+
repo_name: repoName,
|
|
824
|
+
event_count: repoGroup.length,
|
|
825
|
+
summary: null
|
|
826
|
+
});
|
|
827
|
+
for (const event of repoGroup) {
|
|
828
|
+
insertSessionEvent(sessionId, event.id);
|
|
829
|
+
}
|
|
830
|
+
sessions.push({
|
|
831
|
+
id: sessionId,
|
|
832
|
+
date,
|
|
833
|
+
startedAt: first.captured_at,
|
|
834
|
+
endedAt: last.captured_at,
|
|
835
|
+
repoName,
|
|
836
|
+
durationMinutes: minutesBetween(first.captured_at, last.captured_at),
|
|
837
|
+
eventCount: repoGroup.length,
|
|
838
|
+
summary: null,
|
|
839
|
+
events: repoGroup
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return sessions;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
// src/cli/sessions.ts
|
|
847
|
+
function registerSessions(program2) {
|
|
848
|
+
program2.command("sessions").description("List detected work sessions").option("--date <YYYY-MM-DD>", "date to show sessions for (default: today)").action((opts) => {
|
|
849
|
+
let date = todayDate();
|
|
850
|
+
if (opts.date) {
|
|
851
|
+
try {
|
|
852
|
+
date = parseDate(opts.date);
|
|
853
|
+
} catch (e) {
|
|
854
|
+
log.error(e.message);
|
|
855
|
+
process.exit(1);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
const sessions = getOrBuildSessions(date);
|
|
859
|
+
if (sessions.length === 0) {
|
|
860
|
+
log.plain(`No sessions detected for ${date}`);
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
log.plain("");
|
|
864
|
+
log.plain(import_chalk5.default.bold(` Sessions for ${date} (${sessions.length} session${sessions.length > 1 ? "s" : ""})`));
|
|
865
|
+
log.plain("");
|
|
866
|
+
for (const session of sessions) {
|
|
867
|
+
const start = formatDate(session.startedAt);
|
|
868
|
+
const end = formatDate(session.endedAt);
|
|
869
|
+
const duration = formatDuration(session.durationMinutes);
|
|
870
|
+
const repo = session.repoName ? import_chalk5.default.cyan(session.repoName) : import_chalk5.default.gray("no repo");
|
|
871
|
+
log.plain(` ${import_chalk5.default.dim(start)} \u2013 ${import_chalk5.default.dim(end)} ${repo} ${import_chalk5.default.dim(duration)} ${import_chalk5.default.dim(session.eventCount + " events")}`);
|
|
872
|
+
}
|
|
873
|
+
log.plain("");
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// src/cli/summary.ts
|
|
878
|
+
var import_chalk6 = __toESM(require("chalk"));
|
|
879
|
+
|
|
880
|
+
// src/ai/prompt.ts
|
|
881
|
+
var SYSTEM_PROMPT = `You are a technical writer generating a work summary for a software developer.
|
|
882
|
+
|
|
883
|
+
Rules:
|
|
884
|
+
- Write in past tense, professional tone
|
|
885
|
+
- Describe what was ACCOMPLISHED, not what commands were run
|
|
886
|
+
- Never write literal command names (e.g. do not write "ran npm install" \u2014 write "added HTTP client dependency")
|
|
887
|
+
- Never list commands literally
|
|
888
|
+
- Each bullet point should describe a meaningful unit of work
|
|
889
|
+
- Group related events into single bullet points
|
|
890
|
+
- If tests failed then passed, describe it as "resolved failing tests" not two separate events
|
|
891
|
+
- Return 3-6 bullet points per project section
|
|
892
|
+
- Do not pad or add filler sentences
|
|
893
|
+
- Output only the bullet points grouped under project headers using this exact format:
|
|
894
|
+
|
|
895
|
+
## <project-name> (<branch>) \u2014 <duration>
|
|
896
|
+
|
|
897
|
+
- bullet point one
|
|
898
|
+
- bullet point two
|
|
899
|
+
|
|
900
|
+
If there is no branch, omit the branch. If there are multiple projects, repeat the section for each.`;
|
|
901
|
+
function buildUserMessage(payload) {
|
|
902
|
+
const lines = [`Date: ${payload.date}`, ""];
|
|
903
|
+
for (const session of payload.sessions) {
|
|
904
|
+
lines.push(`Project: ${session.project ?? "unknown"}`);
|
|
905
|
+
if (session.branch) lines.push(`Branch: ${session.branch}`);
|
|
906
|
+
lines.push(`Time: ${session.startTime} \u2013 ${session.endTime} (${formatDuration(session.durationMinutes)})`);
|
|
907
|
+
lines.push("Events:");
|
|
908
|
+
for (const event of session.events) {
|
|
909
|
+
const meta = Object.keys(event.metadata).length > 0 ? ` ${JSON.stringify(event.metadata)}` : "";
|
|
910
|
+
const exitNote = event.exitCode !== null && event.exitCode !== 0 ? ` [exit ${event.exitCode}]` : "";
|
|
911
|
+
lines.push(` - ${event.category}${meta}${exitNote}`);
|
|
912
|
+
}
|
|
913
|
+
lines.push("");
|
|
914
|
+
}
|
|
915
|
+
return lines.join("\n");
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// src/ai/providers/anthropic.ts
|
|
919
|
+
var AnthropicProvider = class {
|
|
920
|
+
apiKey;
|
|
921
|
+
model;
|
|
922
|
+
constructor(apiKey, model) {
|
|
923
|
+
this.apiKey = apiKey;
|
|
924
|
+
this.model = model || "claude-haiku-4-5-20251001";
|
|
925
|
+
}
|
|
926
|
+
async summarize(payload) {
|
|
927
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
928
|
+
method: "POST",
|
|
929
|
+
headers: {
|
|
930
|
+
"Content-Type": "application/json",
|
|
931
|
+
"x-api-key": this.apiKey,
|
|
932
|
+
"anthropic-version": "2023-06-01"
|
|
933
|
+
},
|
|
934
|
+
body: JSON.stringify({
|
|
935
|
+
model: this.model,
|
|
936
|
+
max_tokens: 1024,
|
|
937
|
+
system: SYSTEM_PROMPT,
|
|
938
|
+
messages: [{ role: "user", content: buildUserMessage(payload) }]
|
|
939
|
+
})
|
|
940
|
+
});
|
|
941
|
+
if (!response.ok) {
|
|
942
|
+
const body = await response.text();
|
|
943
|
+
throw new Error(`Anthropic API error ${response.status}: ${body}`);
|
|
944
|
+
}
|
|
945
|
+
const data = await response.json();
|
|
946
|
+
return data.content.find((c) => c.type === "text")?.text ?? "";
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
// src/ai/providers/openai.ts
|
|
951
|
+
var OpenAIProvider = class {
|
|
952
|
+
apiKey;
|
|
953
|
+
model;
|
|
954
|
+
baseUrl;
|
|
955
|
+
constructor(apiKey, model, baseUrl) {
|
|
956
|
+
this.apiKey = apiKey;
|
|
957
|
+
this.model = model || "gpt-4o-mini";
|
|
958
|
+
this.baseUrl = baseUrl || "https://api.openai.com/v1";
|
|
959
|
+
}
|
|
960
|
+
async summarize(payload) {
|
|
961
|
+
const response = await fetch(`${this.baseUrl}/chat/completions`, {
|
|
962
|
+
method: "POST",
|
|
963
|
+
headers: {
|
|
964
|
+
"Content-Type": "application/json",
|
|
965
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
966
|
+
},
|
|
967
|
+
body: JSON.stringify({
|
|
968
|
+
model: this.model,
|
|
969
|
+
messages: [
|
|
970
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
971
|
+
{ role: "user", content: buildUserMessage(payload) }
|
|
972
|
+
],
|
|
973
|
+
max_tokens: 1024
|
|
974
|
+
})
|
|
975
|
+
});
|
|
976
|
+
if (!response.ok) {
|
|
977
|
+
const body = await response.text();
|
|
978
|
+
throw new Error(`OpenAI API error ${response.status}: ${body}`);
|
|
979
|
+
}
|
|
980
|
+
const data = await response.json();
|
|
981
|
+
return data.choices[0]?.message?.content ?? "";
|
|
982
|
+
}
|
|
983
|
+
};
|
|
984
|
+
|
|
985
|
+
// src/ai/providers/ollama.ts
|
|
986
|
+
var OllamaProvider = class {
|
|
987
|
+
model;
|
|
988
|
+
baseUrl;
|
|
989
|
+
constructor(model, baseUrl) {
|
|
990
|
+
this.model = model || "llama3";
|
|
991
|
+
this.baseUrl = baseUrl || "http://localhost:11434";
|
|
992
|
+
}
|
|
993
|
+
async summarize(payload) {
|
|
994
|
+
const response = await fetch(`${this.baseUrl}/api/chat`, {
|
|
995
|
+
method: "POST",
|
|
996
|
+
headers: { "Content-Type": "application/json" },
|
|
997
|
+
body: JSON.stringify({
|
|
998
|
+
model: this.model,
|
|
999
|
+
stream: false,
|
|
1000
|
+
messages: [
|
|
1001
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
1002
|
+
{ role: "user", content: buildUserMessage(payload) }
|
|
1003
|
+
]
|
|
1004
|
+
})
|
|
1005
|
+
});
|
|
1006
|
+
if (!response.ok) {
|
|
1007
|
+
const body = await response.text();
|
|
1008
|
+
throw new Error(`Ollama error ${response.status}: ${body}. Is Ollama running? Try: ollama serve`);
|
|
1009
|
+
}
|
|
1010
|
+
const data = await response.json();
|
|
1011
|
+
return data.message?.content ?? "";
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
|
|
1015
|
+
// src/ai/router.ts
|
|
1016
|
+
function getProvider(config) {
|
|
1017
|
+
const { provider, apiKey, model, baseUrl } = config.ai;
|
|
1018
|
+
if (!provider) {
|
|
1019
|
+
throw new Error(
|
|
1020
|
+
"No AI provider configured.\nRun: chronicle config set ai.provider <anthropic|openai|ollama|openai-compatible>"
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
switch (provider) {
|
|
1024
|
+
case "anthropic": {
|
|
1025
|
+
if (!apiKey) throw new Error("Anthropic requires an API key.\nRun: chronicle config set ai.apiKey <your-key>");
|
|
1026
|
+
return new AnthropicProvider(apiKey, model || void 0);
|
|
1027
|
+
}
|
|
1028
|
+
case "openai": {
|
|
1029
|
+
if (!apiKey) throw new Error("OpenAI requires an API key.\nRun: chronicle config set ai.apiKey <your-key>");
|
|
1030
|
+
return new OpenAIProvider(apiKey, model || void 0);
|
|
1031
|
+
}
|
|
1032
|
+
case "openai-compatible": {
|
|
1033
|
+
if (!apiKey) throw new Error("This provider requires an API key.\nRun: chronicle config set ai.apiKey <your-key>");
|
|
1034
|
+
if (!baseUrl) throw new Error("openai-compatible requires a base URL.\nRun: chronicle config set ai.baseUrl <url>");
|
|
1035
|
+
return new OpenAIProvider(apiKey, model || void 0, baseUrl);
|
|
1036
|
+
}
|
|
1037
|
+
case "ollama": {
|
|
1038
|
+
return new OllamaProvider(model || void 0, baseUrl || void 0);
|
|
1039
|
+
}
|
|
1040
|
+
default:
|
|
1041
|
+
throw new Error(
|
|
1042
|
+
`Unknown provider: "${provider}"
|
|
1043
|
+
Supported providers: anthropic, openai, ollama, openai-compatible`
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
// src/cli/summary.ts
|
|
1049
|
+
function eventRowToPayload(row) {
|
|
1050
|
+
let metadata = {};
|
|
1051
|
+
try {
|
|
1052
|
+
if (row.metadata) metadata = JSON.parse(row.metadata);
|
|
1053
|
+
} catch {
|
|
1054
|
+
}
|
|
1055
|
+
return {
|
|
1056
|
+
category: row.category,
|
|
1057
|
+
metadata,
|
|
1058
|
+
exitCode: row.exit_code,
|
|
1059
|
+
durationMs: row.duration_ms
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
function registerSummary(program2) {
|
|
1063
|
+
program2.command("summary").description("Generate an AI work summary").option("--date <YYYY-MM-DD>", "date to summarize (default: today)").option("--week", "summarize the current week").option("--fresh", "bypass cache and regenerate").action(async (opts) => {
|
|
1064
|
+
let date = todayDate();
|
|
1065
|
+
if (opts.date) {
|
|
1066
|
+
try {
|
|
1067
|
+
date = parseDate(opts.date);
|
|
1068
|
+
} catch (e) {
|
|
1069
|
+
log.error(e.message);
|
|
1070
|
+
process.exit(1);
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
const sessions = getOrBuildSessions(date);
|
|
1074
|
+
if (sessions.length === 0) {
|
|
1075
|
+
log.plain(`No activity captured for ${date}`);
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
if (!opts.fresh) {
|
|
1079
|
+
const allCached = sessions.every((s) => s.summary);
|
|
1080
|
+
if (allCached) {
|
|
1081
|
+
log.plain("");
|
|
1082
|
+
for (const s of sessions) {
|
|
1083
|
+
log.plain(s.summary);
|
|
1084
|
+
}
|
|
1085
|
+
log.plain("");
|
|
1086
|
+
log.plain(import_chalk6.default.dim(" (cached \u2014 use --fresh to regenerate)"));
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
const config = readConfig();
|
|
1091
|
+
let provider;
|
|
1092
|
+
try {
|
|
1093
|
+
provider = getProvider(config);
|
|
1094
|
+
} catch (e) {
|
|
1095
|
+
log.error(e.message);
|
|
1096
|
+
process.exit(1);
|
|
1097
|
+
}
|
|
1098
|
+
const sessionPayloads = sessions.map((s) => ({
|
|
1099
|
+
date,
|
|
1100
|
+
project: s.repoName,
|
|
1101
|
+
branch: s.events[0]?.branch ?? null,
|
|
1102
|
+
startTime: formatDate(s.startedAt),
|
|
1103
|
+
endTime: formatDate(s.endedAt),
|
|
1104
|
+
durationMinutes: s.durationMinutes,
|
|
1105
|
+
events: s.events.map(eventRowToPayload)
|
|
1106
|
+
}));
|
|
1107
|
+
const payload = { date, sessions: sessionPayloads };
|
|
1108
|
+
log.plain("");
|
|
1109
|
+
log.info(`Generating summary via ${import_chalk6.default.cyan(config.ai.provider)}\u2026`);
|
|
1110
|
+
try {
|
|
1111
|
+
const summary = await provider.summarize(payload);
|
|
1112
|
+
log.plain("");
|
|
1113
|
+
log.plain(summary);
|
|
1114
|
+
log.plain("");
|
|
1115
|
+
if (sessions[0]) {
|
|
1116
|
+
updateSessionSummary(sessions[0].id, summary);
|
|
1117
|
+
}
|
|
1118
|
+
} catch (e) {
|
|
1119
|
+
log.error(`Summary failed [${config.ai.provider}]: ${e.message}`);
|
|
1120
|
+
process.exit(1);
|
|
1121
|
+
}
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// src/cli/export.ts
|
|
1126
|
+
var import_fs5 = __toESM(require("fs"));
|
|
1127
|
+
var import_path4 = __toESM(require("path"));
|
|
1128
|
+
function eventRowToPayload2(row) {
|
|
1129
|
+
let metadata = {};
|
|
1130
|
+
try {
|
|
1131
|
+
if (row.metadata) metadata = JSON.parse(row.metadata);
|
|
1132
|
+
} catch {
|
|
1133
|
+
}
|
|
1134
|
+
return { category: row.category, metadata, exitCode: row.exit_code, durationMs: row.duration_ms };
|
|
1135
|
+
}
|
|
1136
|
+
function registerExport(program2) {
|
|
1137
|
+
program2.command("export").description("Export work summary as a markdown file").option("--date <YYYY-MM-DD>", "date to export (default: today)").option("--output <path>", "output file path (default: ~/.chronicle/exports/)").action(async (opts) => {
|
|
1138
|
+
let date = todayDate();
|
|
1139
|
+
if (opts.date) {
|
|
1140
|
+
try {
|
|
1141
|
+
date = parseDate(opts.date);
|
|
1142
|
+
} catch (e) {
|
|
1143
|
+
log.error(e.message);
|
|
1144
|
+
process.exit(1);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
const sessions = getOrBuildSessions(date);
|
|
1148
|
+
if (sessions.length === 0) {
|
|
1149
|
+
log.plain(`No activity captured for ${date}`);
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
const config = readConfig();
|
|
1153
|
+
let provider;
|
|
1154
|
+
try {
|
|
1155
|
+
provider = getProvider(config);
|
|
1156
|
+
} catch (e) {
|
|
1157
|
+
log.error(e.message);
|
|
1158
|
+
process.exit(1);
|
|
1159
|
+
}
|
|
1160
|
+
const sessionPayloads = sessions.map((s) => ({
|
|
1161
|
+
date,
|
|
1162
|
+
project: s.repoName,
|
|
1163
|
+
branch: s.events[0]?.branch ?? null,
|
|
1164
|
+
startTime: formatDate(s.startedAt),
|
|
1165
|
+
endTime: formatDate(s.endedAt),
|
|
1166
|
+
durationMinutes: s.durationMinutes,
|
|
1167
|
+
events: s.events.map(eventRowToPayload2)
|
|
1168
|
+
}));
|
|
1169
|
+
const payload = { date, sessions: sessionPayloads };
|
|
1170
|
+
log.info(`Generating summary via ${config.ai.provider}\u2026`);
|
|
1171
|
+
const summary = await provider.summarize(payload);
|
|
1172
|
+
const outputPath = opts.output ?? import_path4.default.join(EXPORTS_DIR, `chronicle-${date}.md`);
|
|
1173
|
+
if (!import_fs5.default.existsSync(EXPORTS_DIR)) import_fs5.default.mkdirSync(EXPORTS_DIR, { recursive: true });
|
|
1174
|
+
import_fs5.default.writeFileSync(outputPath, `# Work Summary \u2014 ${date}
|
|
1175
|
+
|
|
1176
|
+
${summary}
|
|
1177
|
+
`, "utf-8");
|
|
1178
|
+
log.success(`Saved: ${outputPath}`);
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// src/cli/config.ts
|
|
1183
|
+
var import_chalk7 = __toESM(require("chalk"));
|
|
1184
|
+
function registerConfig(program2) {
|
|
1185
|
+
const configCmd = program2.command("config").description("View or update Chronicle configuration").action(() => {
|
|
1186
|
+
const config = readConfig();
|
|
1187
|
+
log.plain("");
|
|
1188
|
+
log.plain(import_chalk7.default.bold(" Chronicle Config"));
|
|
1189
|
+
log.plain("");
|
|
1190
|
+
printConfig(config, " ");
|
|
1191
|
+
log.plain("");
|
|
1192
|
+
});
|
|
1193
|
+
configCmd.command("set <key> <value>").description("Set a config value (e.g. chronicle config set ai.provider anthropic)").action((key, value) => {
|
|
1194
|
+
const config = readConfig();
|
|
1195
|
+
const updated = setConfigValue(config, key, value);
|
|
1196
|
+
writeConfig(updated);
|
|
1197
|
+
log.success(`Set ${import_chalk7.default.cyan(key)} = ${import_chalk7.default.yellow(value)}`);
|
|
1198
|
+
});
|
|
1199
|
+
configCmd.command("get <key>").description("Get a config value").action((key) => {
|
|
1200
|
+
const config = readConfig();
|
|
1201
|
+
const value = getConfigValue(config, key);
|
|
1202
|
+
if (value === void 0) {
|
|
1203
|
+
log.warn(`Key not found: ${key}`);
|
|
1204
|
+
} else {
|
|
1205
|
+
log.plain(String(value));
|
|
1206
|
+
}
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
function printConfig(obj, indent) {
|
|
1210
|
+
if (typeof obj !== "object" || obj === null) return;
|
|
1211
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
1212
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
|
1213
|
+
log.plain(`${indent}${import_chalk7.default.bold(key)}:`);
|
|
1214
|
+
printConfig(value, indent + " ");
|
|
1215
|
+
} else {
|
|
1216
|
+
const display = Array.isArray(value) ? `[${value.join(", ")}]` : String(value) || import_chalk7.default.dim("(not set)");
|
|
1217
|
+
log.plain(`${indent}${import_chalk7.default.dim(key)}: ${import_chalk7.default.yellow(display)}`);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// src/index.ts
|
|
1223
|
+
var program = new import_commander.Command();
|
|
1224
|
+
program.name("chronicle").description("Automatic developer work journal").version("0.1.0");
|
|
1225
|
+
registerCapture(program);
|
|
1226
|
+
registerInit(program);
|
|
1227
|
+
registerStatus(program);
|
|
1228
|
+
registerEvents(program);
|
|
1229
|
+
registerSessions(program);
|
|
1230
|
+
registerSummary(program);
|
|
1231
|
+
registerExport(program);
|
|
1232
|
+
registerConfig(program);
|
|
1233
|
+
program.parse();
|
|
1234
|
+
//# sourceMappingURL=index.js.map
|