@workscribe/cli 1.12.0 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -27,15 +27,33 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
27
  var import_commander = require("commander");
28
28
 
29
29
  // src/utils/node-version.ts
30
+ function detectVersionManager() {
31
+ if (process.env.NVM_DIR) return "nvm";
32
+ if (process.env.VOLTA_HOME) return "volta";
33
+ if (process.env.FNM_DIR) return "fnm";
34
+ if (process.env.MISE_DATA_DIR || process.env.MISE_SHELL) return "mise";
35
+ if (process.env.ASDF_DIR || process.env.ASDF_DATA_DIR) return "asdf";
36
+ if (process.env.NODENV_ROOT) return "nodenv";
37
+ return null;
38
+ }
39
+ var UPGRADE_COMMANDS = {
40
+ nvm: "nvm install 22 && nvm use 22",
41
+ volta: "volta install node@22",
42
+ fnm: "fnm install 22 && fnm use 22",
43
+ mise: "mise use --global node@22",
44
+ asdf: "asdf install nodejs 22 && asdf global nodejs 22",
45
+ nodenv: "nodenv install 22 && nodenv global 22"
46
+ };
30
47
  function assertNodeVersion(nodeVersion) {
31
48
  const major = Number(nodeVersion.split(".")[0]);
32
49
  if (major < 22) {
33
- process.stderr.write(
34
- `workscribe requires Node.js 22 or later (you have v${nodeVersion}).
35
- Upgrade at https://nodejs.org or via your version manager:
36
- nvm install 22 && nvm use 22
37
- `
38
- );
50
+ const manager = detectVersionManager();
51
+ const upgradeHint = manager ? `Upgrade with ${manager}:
52
+ ${UPGRADE_COMMANDS[manager]}` : "Download the latest version from https://nodejs.org";
53
+ process.stderr.write(`workscribe requires Node.js 22 or later (you have v${nodeVersion}).
54
+
55
+ ${upgradeHint}
56
+ `);
39
57
  process.exit(1);
40
58
  }
41
59
  }
@@ -56,7 +74,7 @@ var USE_XDG = !import_fs.default.existsSync(LEGACY_DIR);
56
74
  var xdgDataDir = USE_XDG ? import_path.default.join(process.env.XDG_DATA_HOME ?? import_path.default.join(import_os.default.homedir(), ".local", "share"), "workscribe") : LEGACY_DIR;
57
75
  var xdgConfigDir = USE_XDG ? import_path.default.join(process.env.XDG_CONFIG_HOME ?? import_path.default.join(import_os.default.homedir(), ".config"), "workscribe") : LEGACY_DIR;
58
76
  var xdgStateDir = USE_XDG ? import_path.default.join(process.env.XDG_STATE_HOME ?? import_path.default.join(import_os.default.homedir(), ".local", "state"), "workscribe") : LEGACY_DIR;
59
- var REQUIRED_DIRS = [.../* @__PURE__ */ new Set([xdgDataDir, xdgConfigDir, xdgStateDir])];
77
+ var REQUIRED_DIRS = [.../* @__PURE__ */ new Set([xdgDataDir, xdgConfigDir, xdgStateDir, LEGACY_DIR])];
60
78
  var WORKSCRIBE_DIR = xdgDataDir;
61
79
  var DB_PATH = import_path.default.join(xdgDataDir, "workscribe.db");
62
80
  var CONFIG_PATH = import_path.default.join(xdgConfigDir, "config.json");
@@ -66,6 +84,7 @@ var EXPORTS_DIR = import_path.default.join(xdgDataDir, "exports");
66
84
  var PAUSE_FLAG_PATH = import_path.default.join(xdgStateDir, "paused");
67
85
  var WEEKLY_CACHE_DIR = import_path.default.join(xdgDataDir, "weekly-cache");
68
86
  var PROFILES_DIR = import_path.default.join(xdgDataDir, "profiles");
87
+ var ALERTS_PENDING_PATH = import_path.default.join(LEGACY_DIR, "alerts.pending");
69
88
 
70
89
  // src/db/migrations/001_initial_schema.ts
71
90
  var initial_schema_default = {
@@ -127,8 +146,38 @@ var migration = {
127
146
  };
128
147
  var semantic_categories_default = migration;
129
148
 
149
+ // src/db/migrations/003_alerts.ts
150
+ var migration2 = {
151
+ version: 3,
152
+ description: "Add alerts table for AI session secret detection",
153
+ sql: `
154
+ CREATE TABLE IF NOT EXISTS alerts (
155
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
156
+ created_at INTEGER NOT NULL,
157
+ tool TEXT NOT NULL,
158
+ session_id TEXT,
159
+ context TEXT NOT NULL,
160
+ cleared INTEGER NOT NULL DEFAULT 0
161
+ );
162
+ CREATE INDEX IF NOT EXISTS alerts_cleared ON alerts (cleared);
163
+ `
164
+ };
165
+ var alerts_default = migration2;
166
+
167
+ // src/db/migrations/004_ide_columns.ts
168
+ var migration3 = {
169
+ version: 4,
170
+ description: "Add source and file_path columns to events for IDE integration",
171
+ sql: `
172
+ ALTER TABLE events ADD COLUMN source TEXT;
173
+ ALTER TABLE events ADD COLUMN file_path TEXT;
174
+ CREATE INDEX IF NOT EXISTS idx_events_file_path ON events (file_path, cwd);
175
+ `
176
+ };
177
+ var ide_columns_default = migration3;
178
+
130
179
  // src/db/migrations/index.ts
131
- var MIGRATIONS = [initial_schema_default, semantic_categories_default];
180
+ var MIGRATIONS = [initial_schema_default, semantic_categories_default, alerts_default, ide_columns_default];
132
181
 
133
182
  // src/db/index.ts
134
183
  var _db = null;
@@ -165,13 +214,13 @@ function applyMigrations(db) {
165
214
  const applied = new Set(
166
215
  db.prepare("SELECT version FROM schema_migrations").all().map((row) => row.version)
167
216
  );
168
- for (const migration2 of MIGRATIONS) {
169
- if (applied.has(migration2.version)) continue;
217
+ for (const migration4 of MIGRATIONS) {
218
+ if (applied.has(migration4.version)) continue;
170
219
  db.exec("BEGIN");
171
220
  try {
172
- db.exec(migration2.sql);
221
+ db.exec(migration4.sql);
173
222
  db.prepare("INSERT INTO schema_migrations (version, applied_at) VALUES ($version, datetime('now'))").run({
174
- version: migration2.version
223
+ version: migration4.version
175
224
  });
176
225
  db.exec("COMMIT");
177
226
  } catch (err) {
@@ -179,18 +228,48 @@ function applyMigrations(db) {
179
228
  db.exec("ROLLBACK");
180
229
  } catch {
181
230
  }
182
- throw new Error(`Migration ${migration2.version} (${migration2.description}) failed: ${err.message}`);
231
+ throw new Error(`Migration ${migration4.version} (${migration4.description}) failed: ${err.message}`);
183
232
  }
184
233
  }
185
234
  }
186
235
  function insertEvent(params) {
187
236
  const db = getDb();
188
237
  const stmt = db.prepare(`
189
- INSERT INTO events (captured_at, command, cwd, exit_code, duration_ms, repo_name, branch, category, metadata)
190
- VALUES ($captured_at, $command, $cwd, $exit_code, $duration_ms, $repo_name, $branch, $category, $metadata)
238
+ INSERT INTO events (captured_at, command, cwd, exit_code, duration_ms, repo_name, branch, category, metadata, source, file_path)
239
+ VALUES ($captured_at, $command, $cwd, $exit_code, $duration_ms, $repo_name, $branch, $category, $metadata, $source, $file_path)
191
240
  `);
192
241
  stmt.run(params);
193
242
  }
243
+ function getRecentSessionsByRepo(repoName, limit) {
244
+ const db = getDb();
245
+ return db.prepare("SELECT * FROM sessions WHERE repo_name = $repo_name ORDER BY started_at DESC LIMIT $limit").all({ repo_name: repoName, limit });
246
+ }
247
+ function getLastSessionTouchingFile(filePath) {
248
+ const db = getDb();
249
+ return db.prepare(
250
+ `SELECT s.* FROM sessions s
251
+ INNER JOIN session_events se ON se.session_id = s.id
252
+ INNER JOIN events e ON e.id = se.event_id
253
+ WHERE e.file_path = $file_path
254
+ ORDER BY s.started_at DESC
255
+ LIMIT 1`
256
+ ).get({ file_path: filePath });
257
+ }
258
+ function getSessionBranch(sessionId) {
259
+ const db = getDb();
260
+ const row = db.prepare(
261
+ `SELECT e.branch FROM events e
262
+ INNER JOIN session_events se ON se.event_id = e.id
263
+ WHERE se.session_id = $session_id AND e.branch IS NOT NULL
264
+ ORDER BY e.captured_at DESC
265
+ LIMIT 1`
266
+ ).get({ session_id: sessionId });
267
+ return row?.branch ?? null;
268
+ }
269
+ function getLastEventForFile(filePath, cwd) {
270
+ const db = getDb();
271
+ return db.prepare("SELECT * FROM events WHERE file_path = $file_path AND cwd = $cwd ORDER BY id DESC LIMIT 1").get({ file_path: filePath, cwd });
272
+ }
194
273
  function getLastEvent() {
195
274
  const db = getDb();
196
275
  const stmt = db.prepare("SELECT * FROM events ORDER BY id DESC LIMIT 1");
@@ -332,7 +411,10 @@ var BUILTIN_PATTERNS = [
332
411
  /(?<=export\s+(?:[A-Z_]*(?:TOKEN|SECRET|KEY|PASSWORD|PASS|AUTH|CREDENTIAL)[A-Z_]*)=)\S+/gi,
333
412
  // Inline assignment: VAR_WITH_SENSITIVE_KEYWORD=value cmd (e.g. API_KEY=secret npm start)
334
413
  // Lookbehind matches only the variable-name= prefix, so only the value is replaced
335
- /(?<=[A-Z_][A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|PASS|AUTH|CREDENTIAL)[A-Z0-9_]*=)\S+/gi
414
+ /(?<=[A-Z_][A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|PASS|AUTH|CREDENTIAL)[A-Z0-9_]*=)\S+/gi,
415
+ // Credentials embedded in connection string URLs (e.g. DATABASE_URL=postgres://user:pass@host/db)
416
+ // Matches the password segment between :// user : password @ — preserves scheme and host
417
+ /(?<=[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^:@\s/]+:)[^@\s]+(?=@)/g
336
418
  ];
337
419
  function redact(command, extraPatterns = []) {
338
420
  let result = command;
@@ -421,7 +503,7 @@ function readLastAiderPrompt(cwd) {
421
503
  try {
422
504
  const histPath = import_path3.default.join(cwd, HISTORY_FILE);
423
505
  const content = import_fs4.default.readFileSync(histPath, "utf-8");
424
- const matches = [...content.matchAll(/^> (.+)$/gm)];
506
+ const matches = [...content.matchAll(/^#### (.+)$/gm)];
425
507
  if (!matches.length) return null;
426
508
  return matches[matches.length - 1][1].trim().slice(0, PROMPT_MAX_LENGTH);
427
509
  } catch {
@@ -441,56 +523,108 @@ var ai_tools_default = [
441
523
  displayName: "Claude Code",
442
524
  pattern: "^claude\\b",
443
525
  hook: "claude-code",
444
- contextSource: "hook"
526
+ contextSource: "hook",
527
+ historyFormat: null,
528
+ securityWarning: null
445
529
  },
446
530
  {
447
531
  tool: "aider",
448
532
  displayName: "Aider",
449
533
  pattern: "^aider\\b",
450
534
  hook: null,
451
- contextSource: "history-file"
535
+ contextSource: "history-file",
536
+ historyFormat: "markdown",
537
+ securityWarning: null
538
+ },
539
+ {
540
+ tool: "codex",
541
+ displayName: "Codex CLI",
542
+ pattern: "^codex\\b",
543
+ hook: null,
544
+ contextSource: "history-file",
545
+ historyFormat: "jsonl",
546
+ securityWarning: null
452
547
  },
453
548
  {
454
549
  tool: "gemini",
455
- displayName: "Gemini",
550
+ displayName: "Gemini CLI",
456
551
  pattern: "^gemini\\b",
457
552
  hook: null,
458
- contextSource: null
553
+ contextSource: "history-file",
554
+ historyFormat: "json",
555
+ securityWarning: null
459
556
  },
460
557
  {
461
558
  tool: "gh copilot",
462
559
  displayName: "GitHub Copilot",
463
560
  pattern: "^gh\\s+copilot\\b",
464
561
  hook: null,
465
- contextSource: null
466
- },
467
- {
468
- tool: "cody",
469
- displayName: "Cody",
470
- pattern: "^cody\\b",
471
- hook: null,
472
- contextSource: null
562
+ contextSource: "history-file",
563
+ historyFormat: "jsonl",
564
+ securityWarning: null
473
565
  },
474
566
  {
475
567
  tool: "amazon-q",
476
568
  displayName: "Amazon Q",
477
569
  pattern: "^q\\s+(chat|dev)\\b",
478
570
  hook: null,
479
- contextSource: null
571
+ contextSource: "history-file",
572
+ historyFormat: "json",
573
+ securityWarning: null
574
+ },
575
+ {
576
+ tool: "opencode",
577
+ displayName: "OpenCode",
578
+ pattern: "^opencode\\b",
579
+ hook: null,
580
+ contextSource: "sqlite",
581
+ historyFormat: "sqlite",
582
+ securityWarning: null
583
+ },
584
+ {
585
+ tool: "kilo",
586
+ displayName: "Kilo Code",
587
+ pattern: "^kilo\\b",
588
+ hook: null,
589
+ contextSource: null,
590
+ historyFormat: null,
591
+ securityWarning: null
592
+ },
593
+ {
594
+ tool: "grok",
595
+ displayName: "Grok Build",
596
+ pattern: "^grok\\b",
597
+ hook: null,
598
+ contextSource: null,
599
+ historyFormat: null,
600
+ securityWarning: "Grok Build was found to silently upload entire local git repositories \u2014 including .env files and untracked secrets \u2014 to Google Cloud Storage without user disclosure (July 2026 incident). Workscribe does not read Grok Build session files. Consider whether this tool is appropriate for use in repositories containing sensitive credentials."
480
601
  },
481
602
  {
482
603
  tool: "continue",
483
604
  displayName: "Continue",
484
605
  pattern: "^continue\\b",
485
606
  hook: null,
486
- contextSource: null
607
+ contextSource: null,
608
+ historyFormat: null,
609
+ securityWarning: null
610
+ },
611
+ {
612
+ tool: "cody",
613
+ displayName: "Cody",
614
+ pattern: "^cody\\b",
615
+ hook: null,
616
+ contextSource: null,
617
+ historyFormat: null,
618
+ securityWarning: null
487
619
  },
488
620
  {
489
621
  tool: "cursor",
490
622
  displayName: "Cursor",
491
623
  pattern: "^cursor\\b",
492
624
  hook: null,
493
- contextSource: null
625
+ contextSource: null,
626
+ historyFormat: null,
627
+ securityWarning: null
494
628
  }
495
629
  ];
496
630
 
@@ -529,7 +663,9 @@ function loadAIToolRegistry() {
529
663
  displayName: entry.displayName ?? entry.tool,
530
664
  pattern: entry.pattern,
531
665
  hook: entry.hook ?? null,
532
- contextSource: entry.contextSource ?? null
666
+ contextSource: entry.contextSource ?? null,
667
+ historyFormat: entry.historyFormat ?? null,
668
+ securityWarning: entry.securityWarning ?? null
533
669
  }));
534
670
  }
535
671
  }
@@ -1074,11 +1210,41 @@ function isDuplicate(command, cwd, lastEvent, dedupWindowMs = 5e3, now = Date.no
1074
1210
  return lastEvent.command === normalizeForFilter(command) && lastEvent.cwd === cwd && now - lastTime < dedupWindowMs;
1075
1211
  }
1076
1212
 
1213
+ // src/capture/ide.ts
1214
+ var IDE_DEDUP_WINDOW_MS = 3e4;
1215
+
1077
1216
  // src/capture/index.ts
1078
1217
  function captureCommand(opts) {
1079
1218
  if (import_fs10.default.existsSync(PAUSE_FLAG_PATH)) return;
1080
1219
  const config = readConfig();
1081
- const { command, cwd, exitCode, durationMs } = opts;
1220
+ const { cwd, exitCode, durationMs } = opts;
1221
+ if (opts.type) {
1222
+ if (opts.type === "code_edit" && opts.file) {
1223
+ const lastForFile = getLastEventForFile(opts.file, cwd);
1224
+ if (lastForFile) {
1225
+ const lastTime = new Date(lastForFile.captured_at).getTime();
1226
+ if (Date.now() - lastTime < IDE_DEDUP_WINDOW_MS) return;
1227
+ }
1228
+ }
1229
+ const command2 = opts.command ?? opts.file ?? opts.type;
1230
+ const nameOverride2 = config.projects?.[cwd] ?? null;
1231
+ const { repoName: repoName2, branch: branch2 } = getGitContext(cwd, nameOverride2);
1232
+ insertEvent({
1233
+ captured_at: (/* @__PURE__ */ new Date()).toISOString(),
1234
+ command: command2,
1235
+ cwd,
1236
+ exit_code: exitCode,
1237
+ duration_ms: durationMs,
1238
+ repo_name: repoName2,
1239
+ branch: branch2,
1240
+ category: opts.type,
1241
+ metadata: null,
1242
+ source: opts.source ?? null,
1243
+ file_path: opts.file ?? null
1244
+ });
1245
+ return;
1246
+ }
1247
+ const command = opts.command;
1082
1248
  if (shouldFilter(command, config.capture.ignore)) return;
1083
1249
  const last = getLastEvent();
1084
1250
  if (isDuplicate(command, cwd, last ?? void 0)) return;
@@ -1092,7 +1258,6 @@ function captureCommand(opts) {
1092
1258
  if (firstToken) metadata.tool = firstToken;
1093
1259
  metadata.hint = safeCommand.trim().slice(0, 120);
1094
1260
  }
1095
- if (opts.source) metadata.source = opts.source;
1096
1261
  if (category === "ai_session") {
1097
1262
  if (opts.gitBefore) {
1098
1263
  const commits = getCommitsSince(opts.gitBefore, cwd);
@@ -1115,24 +1280,30 @@ function captureCommand(opts) {
1115
1280
  repo_name: repoName,
1116
1281
  branch,
1117
1282
  category,
1118
- metadata: Object.keys(metadata).length > 0 ? JSON.stringify(metadata) : null
1283
+ metadata: Object.keys(metadata).length > 0 ? JSON.stringify(metadata) : null,
1284
+ source: opts.source ?? null,
1285
+ file_path: null
1119
1286
  });
1120
1287
  }
1121
1288
 
1122
1289
  // src/cli/capture.ts
1123
1290
  var CAPTURE_HANG_TIMEOUT_MS = 5e3;
1124
1291
  function registerCapture(program2) {
1125
- 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).option("--git-before <sha>", "git HEAD SHA before this command ran").action((opts) => {
1292
+ program2.command("_capture").description("Internal: capture a shell command or IDE event (called by shell hook or IDE plugin)").option("--cmd <command>", "command that was executed (shell events)").option("--cwd <path>", "working directory").option("--exit <code>", "exit code", parseInt).option("--duration <ms>", "duration in milliseconds", parseInt).option("--git-before <sha>", "git HEAD SHA before this command ran").option("--type <type>", "event type override \u2014 skips normalization (IDE integrations)").option("--source <source>", "event source identifier (e.g. vscode, neovim, jupyter)").option("--file <path>", "file path for code_edit events").action((opts) => {
1126
1293
  const hangGuard = setTimeout(() => process.exit(0), CAPTURE_HANG_TIMEOUT_MS);
1127
1294
  hangGuard.unref();
1128
1295
  try {
1129
- if (!opts.cmd || !opts.cwd) return;
1296
+ if (!opts.cwd) return;
1297
+ if (!opts.cmd && !opts.type) return;
1130
1298
  captureCommand({
1131
1299
  command: opts.cmd,
1132
1300
  cwd: opts.cwd,
1133
1301
  exitCode: opts.exit ?? null,
1134
1302
  durationMs: opts.duration ?? null,
1135
- gitBefore: opts.gitBefore ?? void 0
1303
+ gitBefore: opts.gitBefore ?? void 0,
1304
+ source: opts.source ?? void 0,
1305
+ type: opts.type ?? void 0,
1306
+ file: opts.file ?? void 0
1136
1307
  });
1137
1308
  } catch (err) {
1138
1309
  debugLog("_capture", err);
@@ -1140,6 +1311,38 @@ function registerCapture(program2) {
1140
1311
  });
1141
1312
  }
1142
1313
 
1314
+ // src/capture/detect-secrets.ts
1315
+ function containsSecrets(text) {
1316
+ return redact(text) !== text;
1317
+ }
1318
+
1319
+ // src/db/alerts.ts
1320
+ var import_fs11 = __toESM(require("fs"));
1321
+ var import_path7 = __toESM(require("path"));
1322
+ function insertAlert(tool, sessionId, context) {
1323
+ getDb().prepare(
1324
+ `INSERT INTO alerts (created_at, tool, session_id, context)
1325
+ VALUES ($created_at, $tool, $session_id, $context)`
1326
+ ).run({ created_at: Date.now(), tool, session_id: sessionId ?? null, context });
1327
+ import_fs11.default.mkdirSync(import_path7.default.dirname(ALERTS_PENDING_PATH), { recursive: true });
1328
+ import_fs11.default.writeFileSync(ALERTS_PENDING_PATH, "", { flag: "w" });
1329
+ }
1330
+ function getPendingAlerts() {
1331
+ return getDb().prepare("SELECT * FROM alerts WHERE cleared = 0 ORDER BY created_at DESC").all();
1332
+ }
1333
+ function hasPendingAlerts() {
1334
+ const row = getDb().prepare("SELECT COUNT(*) as count FROM alerts WHERE cleared = 0").get();
1335
+ return Number(row.count) > 0;
1336
+ }
1337
+ function clearAlerts() {
1338
+ const result = getDb().prepare("UPDATE alerts SET cleared = 1 WHERE cleared = 0").run();
1339
+ try {
1340
+ import_fs11.default.unlinkSync(ALERTS_PENDING_PATH);
1341
+ } catch {
1342
+ }
1343
+ return Number(result.changes);
1344
+ }
1345
+
1143
1346
  // src/cli/hook.ts
1144
1347
  function parseHookPayload(raw) {
1145
1348
  if (!raw.trim()) return null;
@@ -1150,8 +1353,17 @@ function parseHookPayload(raw) {
1150
1353
  if (!command || !cwd) return null;
1151
1354
  return { command, cwd };
1152
1355
  }
1356
+ function parseUserPromptPayload(raw) {
1357
+ if (!raw.trim()) return null;
1358
+ const payload = JSON.parse(raw);
1359
+ const prompt = typeof payload.prompt === "string" ? payload.prompt : null;
1360
+ const sessionId = typeof payload.session_id === "string" ? payload.session_id : "";
1361
+ const cwd = typeof payload.cwd === "string" ? payload.cwd : "";
1362
+ if (!prompt) return null;
1363
+ return { session_id: sessionId, prompt, cwd };
1364
+ }
1153
1365
  function registerHook(program2) {
1154
- program2.command("_hook").description("Internal: capture events from AI tool hooks (stdin JSON payload)").option("--source <source>", "hook source identifier (e.g. claude-code)").action((opts) => {
1366
+ program2.command("_hook").description("Internal: capture events from AI tool hooks (stdin JSON payload)").option("--source <source>", "hook source identifier (e.g. claude-code)").option("--event <event>", "hook event type (PostToolUse or UserPromptSubmit)", "PostToolUse").action((opts) => {
1155
1367
  let raw = "";
1156
1368
  process.stdin.setEncoding("utf-8");
1157
1369
  process.stdin.on("data", (chunk) => {
@@ -1159,6 +1371,23 @@ function registerHook(program2) {
1159
1371
  });
1160
1372
  process.stdin.on("end", () => {
1161
1373
  try {
1374
+ if (opts.event === "UserPromptSubmit") {
1375
+ const parsed2 = parseUserPromptPayload(raw);
1376
+ if (!parsed2) return;
1377
+ if (containsSecrets(parsed2.prompt)) {
1378
+ insertAlert(
1379
+ opts.source ?? "unknown",
1380
+ parsed2.session_id || null,
1381
+ "Possible credential detected in prompt submitted to AI tool"
1382
+ );
1383
+ process.stdout.write(
1384
+ JSON.stringify({
1385
+ systemMessage: "Workscribe detected a possible credential in your prompt. Run `workscribe alerts` for details."
1386
+ })
1387
+ );
1388
+ }
1389
+ return;
1390
+ }
1162
1391
  if (readConfig().capture.aiTools === false) return;
1163
1392
  const parsed = parseHookPayload(raw);
1164
1393
  if (!parsed) return;
@@ -1177,8 +1406,8 @@ function registerHook(program2) {
1177
1406
  }
1178
1407
 
1179
1408
  // src/cli/init.ts
1180
- var import_fs13 = __toESM(require("fs"));
1181
- var import_path9 = __toESM(require("path"));
1409
+ var import_fs14 = __toESM(require("fs"));
1410
+ var import_path10 = __toESM(require("path"));
1182
1411
  var import_os5 = __toESM(require("os"));
1183
1412
  var import_prompts = __toESM(require("prompts"));
1184
1413
  var import_chalk2 = __toESM(require("chalk"));
@@ -1224,6 +1453,17 @@ function eachDayOfRange(start, end) {
1224
1453
  }
1225
1454
  return days;
1226
1455
  }
1456
+ function timeAgo(isoString, now = Date.now()) {
1457
+ const elapsedMs = now - new Date(isoString).getTime();
1458
+ const minutes = Math.floor(elapsedMs / 6e4);
1459
+ if (minutes < 2) return "just now";
1460
+ if (minutes < 60) return `${minutes} minutes ago`;
1461
+ const hours = Math.floor(minutes / 60);
1462
+ if (hours < 24) return hours === 1 ? "1 hour ago" : `${hours} hours ago`;
1463
+ const days = Math.floor(hours / 24);
1464
+ if (days === 1) return "yesterday";
1465
+ return `${days} days ago`;
1466
+ }
1227
1467
  function isoWeekKey(dateStr) {
1228
1468
  const d = /* @__PURE__ */ new Date(dateStr + "T00:00:00");
1229
1469
  const jan4 = new Date(d.getFullYear(), 0, 4);
@@ -1272,6 +1512,8 @@ RULES \u2014 follow these exactly:
1272
1512
 
1273
1513
  7. Stick to what the events show. Do not infer subject matter from the branch name. Do not add details not derivable from the events.
1274
1514
 
1515
+ 8. For code_edit events: these represent file saves from an IDE integration. Describe what was being built based on repository context (branch name, commit messages). Never list individual filenames.
1516
+
1275
1517
  Maximum 5 bullets. Output ONLY the bullet points, nothing else.`;
1276
1518
  function getSystemPrompt() {
1277
1519
  const contexts = getActiveProfileAiContexts();
@@ -1313,7 +1555,9 @@ function buildUserMessage(payload, includeCategories = []) {
1313
1555
  if (excluded.has(event.category)) continue;
1314
1556
  const failed = event.exitCode !== null && event.exitCode !== 0 ? " [failed]" : "";
1315
1557
  let key;
1316
- if (event.hint) {
1558
+ if (event.category === "code_edit") {
1559
+ key = `code_edit${failed}`;
1560
+ } else if (event.hint) {
1317
1561
  key = `other [${event.hint}]${failed}`;
1318
1562
  } else {
1319
1563
  const meta = Object.keys(event.metadata).length > 0 ? ` ${JSON.stringify(event.metadata)}` : "";
@@ -1381,7 +1625,9 @@ function buildWeeklyMessage(payload) {
1381
1625
  if (excluded.has(event.category)) continue;
1382
1626
  const failed = event.exitCode !== null && event.exitCode !== 0 ? " [failed]" : "";
1383
1627
  let key;
1384
- if (event.hint) {
1628
+ if (event.category === "code_edit") {
1629
+ key = `code_edit${failed}`;
1630
+ } else if (event.hint) {
1385
1631
  key = `other [${event.hint}]${failed}`;
1386
1632
  } else {
1387
1633
  const meta = Object.keys(event.metadata).length > 0 ? ` ${JSON.stringify(event.metadata)}` : "";
@@ -1412,7 +1658,7 @@ var GenericProvider = class {
1412
1658
  baseUrl;
1413
1659
  includeCategories;
1414
1660
  async call(system, userMsg, maxTokens) {
1415
- const response = await fetchWithTimeout(this.config.chatUrl(this.baseUrl), {
1661
+ const response = await fetchWithTimeout(this.config.chatUrl(this.baseUrl, this.apiKey, this.model), {
1416
1662
  method: "POST",
1417
1663
  headers: this.config.headers(this.apiKey),
1418
1664
  body: JSON.stringify(this.config.buildChatBody(this.model, system, userMsg, maxTokens))
@@ -1430,7 +1676,7 @@ var GenericProvider = class {
1430
1676
  }
1431
1677
  async validate() {
1432
1678
  const isPost = this.config.validateMethod === "POST";
1433
- const response = await fetchWithTimeout(this.config.validateUrl(this.baseUrl), {
1679
+ const response = await fetchWithTimeout(this.config.validateUrl(this.baseUrl, this.apiKey, this.model), {
1434
1680
  method: this.config.validateMethod,
1435
1681
  headers: isPost ? this.config.headers(this.apiKey) : void 0,
1436
1682
  body: isPost && this.config.buildValidateBody ? JSON.stringify(this.config.buildValidateBody(this.model)) : void 0
@@ -1529,6 +1775,59 @@ var PROVIDER_CONFIGS = {
1529
1775
  },
1530
1776
  chatError: (status, body) => `Ollama error ${status}: ${body}. Is Ollama running? Try: ollama serve`,
1531
1777
  validateError: (_status, _body, baseUrl) => `Ollama is not reachable at ${baseUrl}. Is it running? Try: ollama serve`
1778
+ },
1779
+ gemini: {
1780
+ defaultModel: "gemini-2.0-flash-lite",
1781
+ defaultBaseUrl: "https://generativelanguage.googleapis.com/v1beta",
1782
+ dailyTokens: 1024,
1783
+ weeklyTokens: 1024,
1784
+ // API key and model are embedded in the URL — not in headers
1785
+ chatUrl: (baseUrl, apiKey, model) => `${baseUrl}/models/${model}:generateContent?key=${apiKey}`,
1786
+ validateUrl: (baseUrl, apiKey, model) => `${baseUrl}/models/${model}:generateContent?key=${apiKey}`,
1787
+ validateMethod: "POST",
1788
+ headers: () => ({ "Content-Type": "application/json" }),
1789
+ buildChatBody: (_model, system, userMsg, maxTokens) => ({
1790
+ system_instruction: { parts: [{ text: system }] },
1791
+ contents: [{ role: "user", parts: [{ text: userMsg }] }],
1792
+ generationConfig: { maxOutputTokens: maxTokens, temperature: 0.3 }
1793
+ }),
1794
+ buildValidateBody: () => ({
1795
+ contents: [{ role: "user", parts: [{ text: "." }] }],
1796
+ generationConfig: { maxOutputTokens: 1 }
1797
+ }),
1798
+ parseText: (json) => {
1799
+ const data = json;
1800
+ return data.candidates?.[0]?.content?.parts?.[0]?.text ?? "";
1801
+ },
1802
+ chatError: (status, body) => `Gemini API error ${status}: ${body}`,
1803
+ validateError: (status, body) => `Gemini API error ${status}: ${body}`
1804
+ },
1805
+ groq: {
1806
+ defaultModel: "llama-3.1-8b-instant",
1807
+ defaultBaseUrl: "https://api.groq.com/openai/v1",
1808
+ dailyTokens: 1024,
1809
+ weeklyTokens: 1024,
1810
+ chatUrl: (baseUrl) => `${baseUrl}/chat/completions`,
1811
+ validateUrl: (baseUrl) => `${baseUrl}/models`,
1812
+ validateMethod: "GET",
1813
+ headers: (apiKey) => ({
1814
+ "Content-Type": "application/json",
1815
+ Authorization: `Bearer ${apiKey}`
1816
+ }),
1817
+ buildChatBody: (model, system, userMsg, maxTokens) => ({
1818
+ model,
1819
+ messages: [
1820
+ { role: "system", content: system },
1821
+ { role: "user", content: userMsg }
1822
+ ],
1823
+ max_tokens: maxTokens
1824
+ }),
1825
+ parseText: (json) => {
1826
+ const data = json;
1827
+ return data.choices?.[0]?.message?.content ?? "";
1828
+ },
1829
+ chatError: (status, body) => `Groq API error ${status}: ${body}`,
1830
+ validateError: (status, body) => `Groq API error ${status}: ${body}`
1532
1831
  }
1533
1832
  };
1534
1833
 
@@ -1537,7 +1836,7 @@ function getProvider(config) {
1537
1836
  const { provider, apiKey = "", model, baseUrl } = config.ai;
1538
1837
  if (!provider) {
1539
1838
  throw new Error(
1540
- "No AI provider configured.\nRun: workscribe config set ai.provider <anthropic|openai|ollama|openai-compatible>"
1839
+ "No AI provider configured.\nRun: workscribe config set ai.provider <anthropic|openai|gemini|groq|ollama|openai-compatible>"
1541
1840
  );
1542
1841
  }
1543
1842
  const configKey = provider === "openai-compatible" ? "openai" : provider;
@@ -1545,7 +1844,7 @@ function getProvider(config) {
1545
1844
  if (!providerConfig) {
1546
1845
  throw new Error(
1547
1846
  `Unknown provider: "${provider}"
1548
- Supported providers: anthropic, openai, ollama, openai-compatible`
1847
+ Supported providers: anthropic, openai, gemini, groq, ollama, openai-compatible`
1549
1848
  );
1550
1849
  }
1551
1850
  if (provider !== "ollama" && !apiKey) {
@@ -1565,9 +1864,10 @@ Run: workscribe config set ai.apiKey <your-key>`);
1565
1864
  }
1566
1865
 
1567
1866
  // src/capture/shell-hook.ts
1568
- var import_fs11 = __toESM(require("fs"));
1569
- var import_path7 = __toESM(require("path"));
1867
+ var import_fs12 = __toESM(require("fs"));
1868
+ var import_path8 = __toESM(require("path"));
1570
1869
  var import_os3 = __toESM(require("os"));
1870
+ var import_child_process3 = require("child_process");
1571
1871
 
1572
1872
  // src/utils/completion-scripts.ts
1573
1873
  function bashCompletion(commands) {
@@ -1624,13 +1924,13 @@ function fishCompletion(commands) {
1624
1924
  }
1625
1925
 
1626
1926
  // src/capture/shell-hook.ts
1627
- var FISH_COMPLETIONS_DIR = import_path7.default.join(import_os3.default.homedir(), ".config", "fish", "completions");
1927
+ var FISH_COMPLETIONS_DIR = import_path8.default.join(import_os3.default.homedir(), ".config", "fish", "completions");
1628
1928
  var HOOK_START = "# workscribe:hook:start";
1629
1929
  var HOOK_END = "# workscribe:hook:end";
1630
1930
  var HOOK_PREAMBLE = "# Workscribe \u2014 developer work journal";
1631
1931
  function getInstalledHookBlock(rcFile) {
1632
- if (!import_fs11.default.existsSync(rcFile)) return null;
1633
- const content = import_fs11.default.readFileSync(rcFile, "utf-8");
1932
+ if (!import_fs12.default.existsSync(rcFile)) return null;
1933
+ const content = import_fs12.default.readFileSync(rcFile, "utf-8");
1634
1934
  const startIdx = content.indexOf(HOOK_START);
1635
1935
  const endIdx = content.indexOf(HOOK_END);
1636
1936
  if (startIdx === -1 || endIdx === -1) return null;
@@ -1646,54 +1946,66 @@ function isHookOutdated(rcFile, hookContent) {
1646
1946
  }
1647
1947
  function installHook(rcFile, hookContent) {
1648
1948
  const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
1649
- import_fs11.default.copyFileSync(rcFile, backup);
1650
- log.info(`Backed up ${rcFile} \u2192 ${import_path7.default.basename(backup)}`);
1651
- import_fs11.default.appendFileSync(rcFile, `
1949
+ import_fs12.default.copyFileSync(rcFile, backup);
1950
+ log.info(`Backed up ${rcFile} \u2192 ${import_path8.default.basename(backup)}`);
1951
+ import_fs12.default.appendFileSync(rcFile, `
1652
1952
  ${hookContent}
1653
1953
  `);
1654
1954
  }
1655
1955
  function updateHook(rcFile, hookContent) {
1656
1956
  const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
1657
- import_fs11.default.copyFileSync(rcFile, backup);
1658
- log.info(`Backed up ${rcFile} \u2192 ${import_path7.default.basename(backup)}`);
1659
- const content = import_fs11.default.readFileSync(rcFile, "utf-8");
1957
+ import_fs12.default.copyFileSync(rcFile, backup);
1958
+ log.info(`Backed up ${rcFile} \u2192 ${import_path8.default.basename(backup)}`);
1959
+ const content = import_fs12.default.readFileSync(rcFile, "utf-8");
1660
1960
  const startIdx = content.indexOf(HOOK_START);
1661
1961
  const endIdx = content.indexOf(HOOK_END) + HOOK_END.length;
1662
- import_fs11.default.writeFileSync(rcFile, content.slice(0, startIdx) + hookContent + content.slice(endIdx));
1962
+ import_fs12.default.writeFileSync(rcFile, content.slice(0, startIdx) + hookContent + content.slice(endIdx));
1663
1963
  }
1664
1964
  function removeHook(rcFile) {
1665
1965
  const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
1666
- import_fs11.default.copyFileSync(rcFile, backup);
1667
- log.info(`Backed up ${rcFile} \u2192 ${import_path7.default.basename(backup)}`);
1668
- const content = import_fs11.default.readFileSync(rcFile, "utf-8");
1966
+ import_fs12.default.copyFileSync(rcFile, backup);
1967
+ log.info(`Backed up ${rcFile} \u2192 ${import_path8.default.basename(backup)}`);
1968
+ const content = import_fs12.default.readFileSync(rcFile, "utf-8");
1669
1969
  const startIdx = content.indexOf(HOOK_START);
1670
1970
  const endIdx = content.indexOf(HOOK_END) + HOOK_END.length;
1671
1971
  const preambleIdx = content.lastIndexOf(HOOK_PREAMBLE, startIdx);
1672
1972
  const blockStart = preambleIdx !== -1 ? preambleIdx : startIdx;
1673
1973
  const before = content.slice(0, blockStart).replace(/\n+$/, "\n");
1674
1974
  const after = content.slice(endIdx).replace(/^\n/, "");
1675
- import_fs11.default.writeFileSync(rcFile, before + after);
1975
+ import_fs12.default.writeFileSync(rcFile, before + after);
1976
+ }
1977
+ function resolveWorkscribeBinary() {
1978
+ try {
1979
+ const resolved = (0, import_child_process3.execFileSync)("which", ["workscribe"], { encoding: "utf-8" }).trim();
1980
+ return resolved || null;
1981
+ } catch {
1982
+ return null;
1983
+ }
1984
+ }
1985
+ function injectBinaryPath(hookContent, binaryPath) {
1986
+ return hookContent.replace("command -v workscribe > /dev/null 2>&1 || return 0", `[[ -x "${binaryPath}" ]] || return 0`).replace("command -v workscribe &>/dev/null || return 0", `[[ -x "${binaryPath}" ]] || return 0`).replace("type -q workscribe\n or return 0", `test -x "${binaryPath}"
1987
+ or return 0`).replaceAll("command workscribe _capture", `"${binaryPath}" _capture`).replaceAll("command workscribe alerts", `"${binaryPath}" alerts`);
1676
1988
  }
1677
1989
  function isCompletionsInstalled(shell) {
1678
1990
  if (shell.name === "fish") {
1679
- return import_fs11.default.existsSync(import_path7.default.join(FISH_COMPLETIONS_DIR, "workscribe.fish"));
1991
+ return import_fs12.default.existsSync(import_path8.default.join(FISH_COMPLETIONS_DIR, "workscribe.fish"));
1680
1992
  }
1681
1993
  try {
1682
- return import_fs11.default.readFileSync(shell.rcFile, "utf-8").includes("workscribe completions");
1994
+ return import_fs12.default.readFileSync(shell.rcFile, "utf-8").includes("workscribe completions");
1683
1995
  } catch {
1684
1996
  return false;
1685
1997
  }
1686
1998
  }
1687
1999
  function installCompletions(shell, commands) {
1688
2000
  if (shell.name === "fish") {
1689
- if (!import_fs11.default.existsSync(FISH_COMPLETIONS_DIR)) {
1690
- import_fs11.default.mkdirSync(FISH_COMPLETIONS_DIR, { recursive: true });
2001
+ if (!import_fs12.default.existsSync(FISH_COMPLETIONS_DIR)) {
2002
+ import_fs12.default.mkdirSync(FISH_COMPLETIONS_DIR, { recursive: true });
1691
2003
  }
1692
- import_fs11.default.writeFileSync(import_path7.default.join(FISH_COMPLETIONS_DIR, "workscribe.fish"), fishCompletion(commands), { mode: 420 });
2004
+ import_fs12.default.writeFileSync(import_path8.default.join(FISH_COMPLETIONS_DIR, "workscribe.fish"), fishCompletion(commands), { mode: 420 });
1693
2005
  } else {
1694
2006
  const script = shell.name === "zsh" ? zshCompletion(commands) : bashCompletion(commands);
1695
- const cacheFile = import_path7.default.join(WORKSCRIBE_DIR, `completion.${shell.name}`);
1696
- import_fs11.default.writeFileSync(cacheFile, script, { mode: 420 });
2007
+ const cacheFile = import_path8.default.join(WORKSCRIBE_DIR, `completion.${shell.name}`);
2008
+ import_fs12.default.writeFileSync(cacheFile, script, { mode: 420 });
1697
2009
  const sourceLine = shell.name === "zsh" ? `
1698
2010
  # Workscribe completions
1699
2011
  autoload -Uz compinit && compinit
@@ -1702,20 +2014,20 @@ source "${cacheFile}"
1702
2014
  # Workscribe completions
1703
2015
  source "${cacheFile}"
1704
2016
  `;
1705
- import_fs11.default.appendFileSync(shell.rcFile, sourceLine);
2017
+ import_fs12.default.appendFileSync(shell.rcFile, sourceLine);
1706
2018
  }
1707
2019
  }
1708
2020
 
1709
2021
  // src/capture/hooks/claude-code.ts
1710
- var import_fs12 = __toESM(require("fs"));
1711
- var import_path8 = __toESM(require("path"));
2022
+ var import_fs13 = __toESM(require("fs"));
2023
+ var import_path9 = __toESM(require("path"));
1712
2024
  var import_os4 = __toESM(require("os"));
1713
2025
 
1714
2026
  // src/capture/hooks/utils.ts
1715
- var import_child_process3 = require("child_process");
2027
+ var import_child_process4 = require("child_process");
1716
2028
  function isCommandInstalled(cmd) {
1717
2029
  try {
1718
- (0, import_child_process3.execSync)(`which ${cmd}`, { stdio: "ignore" });
2030
+ (0, import_child_process4.execSync)(`which ${cmd}`, { stdio: "ignore" });
1719
2031
  return true;
1720
2032
  } catch {
1721
2033
  return false;
@@ -1723,11 +2035,11 @@ function isCommandInstalled(cmd) {
1723
2035
  }
1724
2036
 
1725
2037
  // src/capture/hooks/claude-code.ts
1726
- var CLAUDE_SETTINGS_PATH = import_path8.default.join(import_os4.default.homedir(), ".claude", "settings.json");
2038
+ var CLAUDE_SETTINGS_PATH = import_path9.default.join(import_os4.default.homedir(), ".claude", "settings.json");
1727
2039
  function readSettings() {
1728
- if (!import_fs12.default.existsSync(CLAUDE_SETTINGS_PATH)) return {};
2040
+ if (!import_fs13.default.existsSync(CLAUDE_SETTINGS_PATH)) return {};
1729
2041
  try {
1730
- return JSON.parse(import_fs12.default.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8"));
2042
+ return JSON.parse(import_fs13.default.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8"));
1731
2043
  } catch {
1732
2044
  return {};
1733
2045
  }
@@ -1739,19 +2051,30 @@ var claudeCodeIntegration = {
1739
2051
  },
1740
2052
  isHookInstalled() {
1741
2053
  const settings = readSettings();
1742
- const postToolUse = settings.hooks?.PostToolUse ?? [];
1743
- return postToolUse.some((entry) => {
1744
- const hooks = entry.hooks ?? [];
1745
- return hooks.some((hookItem) => {
2054
+ const hooks = settings.hooks ?? {};
2055
+ const postToolUse = hooks.PostToolUse ?? [];
2056
+ const hasPostToolUse = postToolUse.some((entry) => {
2057
+ const entryHooks = entry.hooks ?? [];
2058
+ return entryHooks.some((hookItem) => {
2059
+ const hook = hookItem;
2060
+ return typeof hook.command === "string" && hook.command.includes("workscribe _hook");
2061
+ });
2062
+ });
2063
+ const userPromptSubmit = hooks.UserPromptSubmit ?? [];
2064
+ const hasUserPromptSubmit = userPromptSubmit.some((entry) => {
2065
+ const entryHooks = entry.hooks ?? [];
2066
+ return entryHooks.some((hookItem) => {
1746
2067
  const hook = hookItem;
1747
2068
  return typeof hook.command === "string" && hook.command.includes("workscribe _hook");
1748
2069
  });
1749
2070
  });
2071
+ return hasPostToolUse && hasUserPromptSubmit;
1750
2072
  },
1751
2073
  install() {
1752
2074
  const settings = readSettings();
1753
2075
  const existingHooks = settings.hooks ?? {};
1754
2076
  const existingPostToolUse = existingHooks.PostToolUse ?? [];
2077
+ const existingUserPromptSubmit = existingHooks.UserPromptSubmit ?? [];
1755
2078
  settings.hooks = {
1756
2079
  ...existingHooks,
1757
2080
  PostToolUse: [
@@ -1760,10 +2083,21 @@ var claudeCodeIntegration = {
1760
2083
  matcher: "Bash",
1761
2084
  hooks: [{ type: "command", command: "workscribe _hook --source claude-code", async: true }]
1762
2085
  }
2086
+ ],
2087
+ UserPromptSubmit: [
2088
+ ...existingUserPromptSubmit,
2089
+ {
2090
+ hooks: [
2091
+ {
2092
+ type: "command",
2093
+ command: "workscribe _hook --source claude-code --event UserPromptSubmit"
2094
+ }
2095
+ ]
2096
+ }
1763
2097
  ]
1764
2098
  };
1765
- import_fs12.default.mkdirSync(import_path8.default.dirname(CLAUDE_SETTINGS_PATH), { recursive: true });
1766
- import_fs12.default.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
2099
+ import_fs13.default.mkdirSync(import_path9.default.dirname(CLAUDE_SETTINGS_PATH), { recursive: true });
2100
+ import_fs13.default.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
1767
2101
  }
1768
2102
  };
1769
2103
 
@@ -1794,11 +2128,11 @@ function registerCompletions(program2) {
1794
2128
  // src/cli/init.ts
1795
2129
  function detectShell() {
1796
2130
  const shell = process.env.SHELL ?? "";
1797
- if (shell.includes("zsh")) return { name: "zsh", rcFile: import_path9.default.join(import_os5.default.homedir(), ".zshrc") };
1798
- if (shell.includes("bash")) return { name: "bash", rcFile: import_path9.default.join(import_os5.default.homedir(), ".bashrc") };
2131
+ if (shell.includes("zsh")) return { name: "zsh", rcFile: import_path10.default.join(import_os5.default.homedir(), ".zshrc") };
2132
+ if (shell.includes("bash")) return { name: "bash", rcFile: import_path10.default.join(import_os5.default.homedir(), ".bashrc") };
1799
2133
  if (shell.includes("fish")) {
1800
- const confDir = import_path9.default.join(import_os5.default.homedir(), ".config", "fish", "conf.d");
1801
- return { name: "fish", rcFile: import_path9.default.join(confDir, "workscribe.fish") };
2134
+ const confDir = import_path10.default.join(import_os5.default.homedir(), ".config", "fish", "conf.d");
2135
+ return { name: "fish", rcFile: import_path10.default.join(confDir, "workscribe.fish") };
1802
2136
  }
1803
2137
  return null;
1804
2138
  }
@@ -1812,19 +2146,19 @@ function registerInit(program2) {
1812
2146
  log.plain(import_chalk2.default.bold("\n Workscribe \u2014 Developer Work Journal\n"));
1813
2147
  step(1, TOTAL_STEPS, "Creating data directories");
1814
2148
  for (const dir of REQUIRED_DIRS) {
1815
- if (!import_fs13.default.existsSync(dir)) {
1816
- import_fs13.default.mkdirSync(dir, { recursive: true, mode: 448 });
2149
+ if (!import_fs14.default.existsSync(dir)) {
2150
+ import_fs14.default.mkdirSync(dir, { recursive: true, mode: 448 });
1817
2151
  log.success(`Created ${dir}`);
1818
2152
  } else {
1819
2153
  try {
1820
- import_fs13.default.chmodSync(dir, 448);
2154
+ import_fs14.default.chmodSync(dir, 448);
1821
2155
  } catch {
1822
2156
  }
1823
2157
  log.info(`${dir} already exists`);
1824
2158
  }
1825
2159
  }
1826
- if (!import_fs13.default.existsSync(PROFILES_DIR)) {
1827
- import_fs13.default.mkdirSync(PROFILES_DIR, { recursive: true, mode: 448 });
2160
+ if (!import_fs14.default.existsSync(PROFILES_DIR)) {
2161
+ import_fs14.default.mkdirSync(PROFILES_DIR, { recursive: true, mode: 448 });
1828
2162
  }
1829
2163
  step(2, TOTAL_STEPS, "Initialising database");
1830
2164
  getDb();
@@ -1837,16 +2171,18 @@ function registerInit(program2) {
1837
2171
  ` Manually add the hook from: ${import_chalk2.default.cyan("shell/hook.zsh")}, ${import_chalk2.default.cyan("shell/hook.bash")}, or ${import_chalk2.default.cyan("shell/hook.fish")}`
1838
2172
  );
1839
2173
  } else {
1840
- const hookFile = import_path9.default.join(__dirname, "..", "shell", `hook.${shell.name}`);
1841
- if (!import_fs13.default.existsSync(hookFile)) {
2174
+ const hookFile = import_path10.default.join(__dirname, "..", "shell", `hook.${shell.name}`);
2175
+ if (!import_fs14.default.existsSync(hookFile)) {
1842
2176
  log.warn(`Hook template not found at ${hookFile}`);
1843
2177
  } else {
1844
- const hookContent = import_fs13.default.readFileSync(hookFile, "utf-8");
2178
+ const hookContent = import_fs14.default.readFileSync(hookFile, "utf-8");
2179
+ const binaryPath = resolveWorkscribeBinary();
2180
+ const hookWithPath = binaryPath ? injectBinaryPath(hookContent, binaryPath) : hookContent;
1845
2181
  if (shell.name === "fish") {
1846
- const confDir = import_path9.default.dirname(shell.rcFile);
1847
- if (!import_fs13.default.existsSync(confDir)) import_fs13.default.mkdirSync(confDir, { recursive: true });
2182
+ const confDir = import_path10.default.dirname(shell.rcFile);
2183
+ if (!import_fs14.default.existsSync(confDir)) import_fs14.default.mkdirSync(confDir, { recursive: true });
1848
2184
  }
1849
- if (!import_fs13.default.existsSync(shell.rcFile)) import_fs13.default.writeFileSync(shell.rcFile, "");
2185
+ if (!import_fs14.default.existsSync(shell.rcFile)) import_fs14.default.writeFileSync(shell.rcFile, "");
1850
2186
  if (!isHookInstalled(shell.rcFile)) {
1851
2187
  log.plain("");
1852
2188
  log.plain(import_chalk2.default.dim(" Workscribe adds a lightweight hook to your shell that captures each"));
@@ -1854,9 +2190,9 @@ function registerInit(program2) {
1854
2190
  log.plain(import_chalk2.default.dim(" It runs in the background and adds no latency to your terminal."));
1855
2191
  log.plain(import_chalk2.default.dim(" Credentials and secrets are redacted before anything is stored."));
1856
2192
  log.plain("");
1857
- installHook(shell.rcFile, hookContent);
2193
+ installHook(shell.rcFile, hookWithPath);
1858
2194
  log.success(`Shell hook installed in ${shell.rcFile}`);
1859
- } else if (isHookOutdated(shell.rcFile, hookContent)) {
2195
+ } else if (isHookOutdated(shell.rcFile, hookWithPath)) {
1860
2196
  const { update } = await (0, import_prompts.default)({
1861
2197
  type: "confirm",
1862
2198
  name: "update",
@@ -1864,7 +2200,7 @@ function registerInit(program2) {
1864
2200
  initial: true
1865
2201
  });
1866
2202
  if (update) {
1867
- updateHook(shell.rcFile, hookContent);
2203
+ updateHook(shell.rcFile, hookWithPath);
1868
2204
  log.success(`Shell hook updated in ${shell.rcFile}`);
1869
2205
  } else {
1870
2206
  log.info("Shell hook update skipped");
@@ -2020,7 +2356,7 @@ function registerInit(program2) {
2020
2356
  }
2021
2357
  }
2022
2358
  }
2023
- if (!import_fs13.default.existsSync(CONFIG_PATH)) {
2359
+ if (!import_fs14.default.existsSync(CONFIG_PATH)) {
2024
2360
  writeConfig(config);
2025
2361
  }
2026
2362
  log.plain("");
@@ -2056,7 +2392,7 @@ function registerInit(program2) {
2056
2392
  }
2057
2393
 
2058
2394
  // src/cli/status.ts
2059
- var import_fs14 = __toESM(require("fs"));
2395
+ var import_fs15 = __toESM(require("fs"));
2060
2396
  var import_chalk3 = __toESM(require("chalk"));
2061
2397
  function registerStatus(program2) {
2062
2398
  program2.command("status").description("Show capture status and event counts").action(() => {
@@ -2064,7 +2400,7 @@ function registerStatus(program2) {
2064
2400
  const total = getEventCount();
2065
2401
  const todayEvents = getEventsForDate(todayDate());
2066
2402
  const last = getLastEvent();
2067
- const isPaused = import_fs14.default.existsSync(PAUSE_FLAG_PATH);
2403
+ const isPaused = import_fs15.default.existsSync(PAUSE_FLAG_PATH);
2068
2404
  log.plain("");
2069
2405
  log.plain(import_chalk3.default.bold(" Workscribe Status"));
2070
2406
  log.plain("");
@@ -2087,6 +2423,13 @@ function registerStatus(program2) {
2087
2423
  log.plain(
2088
2424
  ` Profiles ${activeProfiles.length > 0 ? import_chalk3.default.cyan(activeProfiles.join(", ")) : import_chalk3.default.dim("none")}`
2089
2425
  );
2426
+ const pendingAlerts = getPendingAlerts();
2427
+ if (pendingAlerts.length > 0) {
2428
+ log.plain("");
2429
+ log.plain(
2430
+ ` ${import_chalk3.default.yellow("!")} ${import_chalk3.default.yellow(String(pendingAlerts.length))} pending security alert${pendingAlerts.length === 1 ? "" : "s"}` + import_chalk3.default.dim(" (run: workscribe alerts)")
2431
+ );
2432
+ }
2090
2433
  const { total: weekTotal, otherCount } = getRecentOtherRate(7);
2091
2434
  if (weekTotal > 0) {
2092
2435
  const otherPct = Math.round(otherCount / weekTotal * 100);
@@ -2180,13 +2523,16 @@ function registerEvents(program2) {
2180
2523
  var import_chalk5 = __toESM(require("chalk"));
2181
2524
 
2182
2525
  // src/sessions/grouper.ts
2526
+ var LONG_RUNNING_THRESHOLD_MS = 5 * 60 * 1e3;
2183
2527
  function splitByIdleGap(events, idleMs) {
2184
2528
  if (events.length === 0) return [];
2185
2529
  const groups = [[events[0]]];
2186
2530
  for (let i = 1; i < events.length; i++) {
2187
2531
  const prev = new Date(events[i - 1].captured_at).getTime();
2188
- const curr = new Date(events[i].captured_at).getTime();
2189
- if (curr - prev >= idleMs) {
2532
+ const currCompleted = new Date(events[i].captured_at).getTime();
2533
+ const currDuration = events[i].duration_ms ?? 0;
2534
+ const currEffective = currDuration >= LONG_RUNNING_THRESHOLD_MS ? currCompleted - currDuration : currCompleted;
2535
+ if (currEffective - prev >= idleMs) {
2190
2536
  groups.push([events[i]]);
2191
2537
  } else {
2192
2538
  groups[groups.length - 1].push(events[i]);
@@ -2194,13 +2540,17 @@ function splitByIdleGap(events, idleMs) {
2194
2540
  }
2195
2541
  return groups;
2196
2542
  }
2197
- function splitByRepo(events) {
2543
+ var REPO_SWITCH_GAP_MS = 10 * 60 * 1e3;
2544
+ function splitByRepo(events, gapMs = 0) {
2198
2545
  if (events.length === 0) return [];
2199
2546
  const groups = [[events[0]]];
2200
2547
  for (let i = 1; i < events.length; i++) {
2201
2548
  const prevRepo = events[i - 1].repo_name;
2202
2549
  const currRepo = events[i].repo_name;
2203
- if (currRepo !== prevRepo) {
2550
+ const prevTime = new Date(events[i - 1].captured_at).getTime();
2551
+ const currTime = new Date(events[i].captured_at).getTime();
2552
+ const gap = currTime - prevTime;
2553
+ if (currRepo !== prevRepo && gap >= gapMs) {
2204
2554
  groups.push([events[i]]);
2205
2555
  } else {
2206
2556
  groups[groups.length - 1].push(events[i]);
@@ -2245,7 +2595,7 @@ function buildSessions(date, events, savedSummaries = /* @__PURE__ */ new Map())
2245
2595
  const timeGroups = splitByIdleGap(events, idleMs);
2246
2596
  for (const group of timeGroups) {
2247
2597
  if (group.length === 0) continue;
2248
- const repoGroups = splitByRepo(group);
2598
+ const repoGroups = splitByRepo(group, REPO_SWITCH_GAP_MS);
2249
2599
  for (const repoGroup of repoGroups) {
2250
2600
  if (repoGroup.length === 0) continue;
2251
2601
  const first = repoGroup[0];
@@ -2324,9 +2674,9 @@ function registerSessions(program2) {
2324
2674
 
2325
2675
  // src/cli/summary.ts
2326
2676
  var import_chalk6 = __toESM(require("chalk"));
2327
- var import_fs15 = __toESM(require("fs"));
2677
+ var import_fs16 = __toESM(require("fs"));
2328
2678
  var import_os6 = __toESM(require("os"));
2329
- var import_path10 = __toESM(require("path"));
2679
+ var import_path11 = __toESM(require("path"));
2330
2680
 
2331
2681
  // src/integrations/webhook.ts
2332
2682
  async function postToWebhook(url, text) {
@@ -2431,9 +2781,9 @@ function registerSummary(program2) {
2431
2781
  return;
2432
2782
  }
2433
2783
  const cacheKey = isoWeekKey(today);
2434
- const cacheFile = import_path10.default.join(WEEKLY_CACHE_DIR, `${cacheKey}.md`);
2435
- if (!opts.fresh && import_fs15.default.existsSync(cacheFile)) {
2436
- const cached = import_fs15.default.readFileSync(cacheFile, "utf-8");
2784
+ const cacheFile = import_path11.default.join(WEEKLY_CACHE_DIR, `${cacheKey}.md`);
2785
+ if (!opts.fresh && import_fs16.default.existsSync(cacheFile)) {
2786
+ const cached = import_fs16.default.readFileSync(cacheFile, "utf-8");
2437
2787
  log.plain("");
2438
2788
  log.plain(cached);
2439
2789
  log.plain("");
@@ -2449,8 +2799,8 @@ function registerSummary(program2) {
2449
2799
  log.plain("");
2450
2800
  log.plain(summary);
2451
2801
  log.plain("");
2452
- import_fs15.default.mkdirSync(WEEKLY_CACHE_DIR, { recursive: true });
2453
- import_fs15.default.writeFileSync(cacheFile, summary, { encoding: "utf-8", mode: 384 });
2802
+ import_fs16.default.mkdirSync(WEEKLY_CACHE_DIR, { recursive: true });
2803
+ import_fs16.default.writeFileSync(cacheFile, summary, { encoding: "utf-8", mode: 384 });
2454
2804
  await handleWebhook(opts, config, summary);
2455
2805
  } catch (error) {
2456
2806
  log.error(`Weekly summary failed [${config.ai.provider}]: ${error.message}`);
@@ -2568,8 +2918,8 @@ ${bullets.trim()}`;
2568
2918
  }
2569
2919
 
2570
2920
  // src/cli/export.ts
2571
- var import_fs16 = __toESM(require("fs"));
2572
- var import_path11 = __toESM(require("path"));
2921
+ var import_fs17 = __toESM(require("fs"));
2922
+ var import_path12 = __toESM(require("path"));
2573
2923
  function eventRowToPayload2(row) {
2574
2924
  let metadata = {};
2575
2925
  try {
@@ -2625,9 +2975,9 @@ function registerExport(program2) {
2625
2975
  const payload = { date, sessions: sessionPayloads };
2626
2976
  log.info(`Generating summary via ${config.ai.provider}\u2026`);
2627
2977
  const summary = await provider.summarize(payload);
2628
- const outputPath = opts.output ?? import_path11.default.join(EXPORTS_DIR, `workscribe-${date}.md`);
2629
- if (!import_fs16.default.existsSync(EXPORTS_DIR)) import_fs16.default.mkdirSync(EXPORTS_DIR, { recursive: true, mode: 448 });
2630
- import_fs16.default.writeFileSync(outputPath, `# Work Summary \u2014 ${date}
2978
+ const outputPath = opts.output ?? import_path12.default.join(EXPORTS_DIR, `workscribe-${date}.md`);
2979
+ if (!import_fs17.default.existsSync(EXPORTS_DIR)) import_fs17.default.mkdirSync(EXPORTS_DIR, { recursive: true, mode: 448 });
2980
+ import_fs17.default.writeFileSync(outputPath, `# Work Summary \u2014 ${date}
2631
2981
 
2632
2982
  ${summary}
2633
2983
  `, { encoding: "utf-8", mode: 384 });
@@ -2699,22 +3049,22 @@ function printConfig(obj, indent) {
2699
3049
  }
2700
3050
 
2701
3051
  // src/cli/pause.ts
2702
- var import_fs17 = __toESM(require("fs"));
3052
+ var import_fs18 = __toESM(require("fs"));
2703
3053
  function registerPause(program2) {
2704
3054
  program2.command("pause").description("Pause capture temporarily").action(() => {
2705
- if (import_fs17.default.existsSync(PAUSE_FLAG_PATH)) {
3055
+ if (import_fs18.default.existsSync(PAUSE_FLAG_PATH)) {
2706
3056
  log.info("Capture is already paused. Run workscribe resume to restart.");
2707
3057
  return;
2708
3058
  }
2709
- import_fs17.default.writeFileSync(PAUSE_FLAG_PATH, "", { mode: 384 });
3059
+ import_fs18.default.writeFileSync(PAUSE_FLAG_PATH, "", { mode: 384 });
2710
3060
  log.success("Capture paused. Run workscribe resume to restart.");
2711
3061
  });
2712
3062
  program2.command("resume").description("Resume capture after a pause").action(() => {
2713
- if (!import_fs17.default.existsSync(PAUSE_FLAG_PATH)) {
3063
+ if (!import_fs18.default.existsSync(PAUSE_FLAG_PATH)) {
2714
3064
  log.info("Capture is not paused.");
2715
3065
  return;
2716
3066
  }
2717
- import_fs17.default.rmSync(PAUSE_FLAG_PATH);
3067
+ import_fs18.default.rmSync(PAUSE_FLAG_PATH);
2718
3068
  log.success("Capture resumed.");
2719
3069
  });
2720
3070
  }
@@ -2799,19 +3149,19 @@ function registerClear(program2) {
2799
3149
  }
2800
3150
 
2801
3151
  // src/cli/uninstall.ts
2802
- var import_fs18 = __toESM(require("fs"));
2803
- var import_path12 = __toESM(require("path"));
3152
+ var import_fs19 = __toESM(require("fs"));
3153
+ var import_path13 = __toESM(require("path"));
2804
3154
  var import_os7 = __toESM(require("os"));
2805
3155
  var import_prompts3 = __toESM(require("prompts"));
2806
3156
  var import_chalk9 = __toESM(require("chalk"));
2807
3157
  function detectShellRcFiles() {
2808
3158
  const candidates = [
2809
- { name: "zsh", rcFile: import_path12.default.join(import_os7.default.homedir(), ".zshrc") },
2810
- { name: "bash", rcFile: import_path12.default.join(import_os7.default.homedir(), ".bashrc") },
2811
- { name: "bash", rcFile: import_path12.default.join(import_os7.default.homedir(), ".bash_profile") },
2812
- { name: "fish", rcFile: import_path12.default.join(import_os7.default.homedir(), ".config", "fish", "conf.d", "workscribe.fish") }
3159
+ { name: "zsh", rcFile: import_path13.default.join(import_os7.default.homedir(), ".zshrc") },
3160
+ { name: "bash", rcFile: import_path13.default.join(import_os7.default.homedir(), ".bashrc") },
3161
+ { name: "bash", rcFile: import_path13.default.join(import_os7.default.homedir(), ".bash_profile") },
3162
+ { name: "fish", rcFile: import_path13.default.join(import_os7.default.homedir(), ".config", "fish", "conf.d", "workscribe.fish") }
2813
3163
  ];
2814
- return candidates.filter((candidate) => import_fs18.default.existsSync(candidate.rcFile) && isHookInstalled(candidate.rcFile));
3164
+ return candidates.filter((candidate) => import_fs19.default.existsSync(candidate.rcFile) && isHookInstalled(candidate.rcFile));
2815
3165
  }
2816
3166
  function registerUninstall(program2) {
2817
3167
  program2.command("uninstall").description("Remove shell hooks and optionally delete all Workscribe data").option("--hooks-only", "remove shell hooks only, skip data deletion prompt (used by npm preuninstall)").action(async (opts) => {
@@ -2822,7 +3172,7 @@ function registerUninstall(program2) {
2822
3172
  } else {
2823
3173
  for (const { name, rcFile } of hooked) {
2824
3174
  if (name === "fish") {
2825
- import_fs18.default.unlinkSync(rcFile);
3175
+ import_fs19.default.unlinkSync(rcFile);
2826
3176
  log.success(`Shell hook removed: ${rcFile}`);
2827
3177
  log.plain(` ${import_chalk9.default.dim("Restart your terminal to take effect")}`);
2828
3178
  } else {
@@ -2836,7 +3186,7 @@ function registerUninstall(program2) {
2836
3186
  }
2837
3187
  }
2838
3188
  if (opts.hooksOnly) return;
2839
- const existingDirs = [...new Set(REQUIRED_DIRS)].filter((dir) => import_fs18.default.existsSync(dir));
3189
+ const existingDirs = [...new Set(REQUIRED_DIRS)].filter((dir) => import_fs19.default.existsSync(dir));
2840
3190
  if (existingDirs.length > 0) {
2841
3191
  log.plain("");
2842
3192
  const { deleteData } = await (0, import_prompts3.default)({
@@ -2847,7 +3197,7 @@ function registerUninstall(program2) {
2847
3197
  });
2848
3198
  if (deleteData) {
2849
3199
  for (const dir of existingDirs) {
2850
- import_fs18.default.rmSync(dir, { recursive: true, force: true });
3200
+ import_fs19.default.rmSync(dir, { recursive: true, force: true });
2851
3201
  log.success(`Deleted ${dir}`);
2852
3202
  }
2853
3203
  } else {
@@ -2861,8 +3211,8 @@ function registerUninstall(program2) {
2861
3211
  }
2862
3212
 
2863
3213
  // src/cli/profile.ts
2864
- var import_fs19 = __toESM(require("fs"));
2865
- var import_path13 = __toESM(require("path"));
3214
+ var import_fs20 = __toESM(require("fs"));
3215
+ var import_path14 = __toESM(require("path"));
2866
3216
  var import_chalk10 = __toESM(require("chalk"));
2867
3217
  var MIN_SUGGEST_COUNT = 5;
2868
3218
  function registerProfile(program2) {
@@ -2908,7 +3258,7 @@ function registerProfile(program2) {
2908
3258
  log.plain(import_chalk10.default.dim(" " + "\u2500".repeat(60)));
2909
3259
  for (const entry of entries) {
2910
3260
  const badge = activeNames.has(entry.name) ? import_chalk10.default.green(" [active]") : "";
2911
- const installed = import_fs19.default.existsSync(import_path13.default.join(PROFILES_DIR, `${entry.name}.json`)) ? import_chalk10.default.dim(" installed") : "";
3261
+ const installed = import_fs20.default.existsSync(import_path14.default.join(PROFILES_DIR, `${entry.name}.json`)) ? import_chalk10.default.dim(" installed") : "";
2912
3262
  log.plain(` ${import_chalk10.default.cyan(entry.name.padEnd(18))} ${entry.displayName.padEnd(28)}${badge}${installed}`);
2913
3263
  if (entry.description) log.plain(` ${import_chalk10.default.dim(" ".repeat(18) + entry.description)}`);
2914
3264
  }
@@ -2944,8 +3294,8 @@ function registerProfile(program2) {
2944
3294
  continue;
2945
3295
  }
2946
3296
  activeNames.delete(name);
2947
- const cachePath = import_path13.default.join(PROFILES_DIR, `${name}.json`);
2948
- if (import_fs19.default.existsSync(cachePath)) import_fs19.default.unlinkSync(cachePath);
3297
+ const cachePath = import_path14.default.join(PROFILES_DIR, `${name}.json`);
3298
+ if (import_fs20.default.existsSync(cachePath)) import_fs20.default.unlinkSync(cachePath);
2949
3299
  log.success(`Profile "${name}" removed`);
2950
3300
  }
2951
3301
  config.capture.profiles = [...activeNames];
@@ -3086,7 +3436,9 @@ function registerNote(program2) {
3086
3436
  repo_name: repoName,
3087
3437
  branch,
3088
3438
  category: "note",
3089
- metadata: JSON.stringify({ text })
3439
+ metadata: JSON.stringify({ text }),
3440
+ source: null,
3441
+ file_path: null
3090
3442
  });
3091
3443
  log.success(`Note saved${repoName ? ` to ${repoName}` : ""}`);
3092
3444
  });
@@ -3094,8 +3446,8 @@ function registerNote(program2) {
3094
3446
 
3095
3447
  // src/cli/week.ts
3096
3448
  var import_chalk11 = __toESM(require("chalk"));
3097
- var import_fs20 = __toESM(require("fs"));
3098
- var import_path14 = __toESM(require("path"));
3449
+ var import_fs21 = __toESM(require("fs"));
3450
+ var import_path15 = __toESM(require("path"));
3099
3451
  var import_os8 = __toESM(require("os"));
3100
3452
  function eventRowToPayload3(row) {
3101
3453
  let metadata = {};
@@ -3168,9 +3520,9 @@ function registerWeek(program2) {
3168
3520
  return;
3169
3521
  }
3170
3522
  const cacheKey = isoWeekKey(today);
3171
- const cacheFile = import_path14.default.join(WEEKLY_CACHE_DIR, `${cacheKey}.md`);
3172
- if (!opts.fresh && import_fs20.default.existsSync(cacheFile)) {
3173
- const cached = import_fs20.default.readFileSync(cacheFile, "utf-8");
3523
+ const cacheFile = import_path15.default.join(WEEKLY_CACHE_DIR, `${cacheKey}.md`);
3524
+ if (!opts.fresh && import_fs21.default.existsSync(cacheFile)) {
3525
+ const cached = import_fs21.default.readFileSync(cacheFile, "utf-8");
3174
3526
  log.plain("");
3175
3527
  log.plain(cached);
3176
3528
  log.plain("");
@@ -3186,8 +3538,8 @@ function registerWeek(program2) {
3186
3538
  log.plain("");
3187
3539
  log.plain(summary);
3188
3540
  log.plain("");
3189
- import_fs20.default.mkdirSync(WEEKLY_CACHE_DIR, { recursive: true });
3190
- import_fs20.default.writeFileSync(cacheFile, summary, { encoding: "utf-8", mode: 384 });
3541
+ import_fs21.default.mkdirSync(WEEKLY_CACHE_DIR, { recursive: true });
3542
+ import_fs21.default.writeFileSync(cacheFile, summary, { encoding: "utf-8", mode: 384 });
3191
3543
  await postSummary(opts, config, summary);
3192
3544
  } catch (error) {
3193
3545
  log.error(`Weekly digest failed [${config.ai.provider}]: ${error.message}`);
@@ -3197,15 +3549,15 @@ function registerWeek(program2) {
3197
3549
  }
3198
3550
 
3199
3551
  // src/cli/bug-report.ts
3200
- var import_fs21 = __toESM(require("fs"));
3552
+ var import_fs22 = __toESM(require("fs"));
3201
3553
  var import_os9 = __toESM(require("os"));
3202
- var import_path15 = __toESM(require("path"));
3554
+ var import_path16 = __toESM(require("path"));
3203
3555
  var import_chalk12 = __toESM(require("chalk"));
3204
3556
  var GITHUB_ISSUES_URL = "https://github.com/workscribe/workscribe/issues/new";
3205
3557
  var DEBUG_LOG_TAIL_LINES = 100;
3206
3558
  function tailFile(filePath, lines) {
3207
- if (!import_fs21.default.existsSync(filePath)) return "(no debug log found \u2014 set WORKSCRIBE_DEBUG=1 to enable)";
3208
- const content = import_fs21.default.readFileSync(filePath, "utf-8");
3559
+ if (!import_fs22.default.existsSync(filePath)) return "(no debug log found \u2014 set WORKSCRIBE_DEBUG=1 to enable)";
3560
+ const content = import_fs22.default.readFileSync(filePath, "utf-8");
3209
3561
  const allLines = content.split("\n");
3210
3562
  return allLines.slice(-lines).join("\n");
3211
3563
  }
@@ -3223,9 +3575,9 @@ function redactConfig() {
3223
3575
  function registerBugReport(program2) {
3224
3576
  program2.command("bug-report").description("Generate a diagnostic report for filing a bug").action(() => {
3225
3577
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3226
- const reportPath = import_path15.default.join(WORKSCRIBE_DIR, `bug-report-${timestamp}.txt`);
3578
+ const reportPath = import_path16.default.join(WORKSCRIBE_DIR, `bug-report-${timestamp}.txt`);
3227
3579
  const systemInfo = [
3228
- `Workscribe version : ${"1.12.0"}`,
3580
+ `Workscribe version : ${"1.13.0"}`,
3229
3581
  `Node.js version : ${process.version}`,
3230
3582
  `Platform : ${process.platform}`,
3231
3583
  `Architecture : ${process.arch}`,
@@ -3247,17 +3599,17 @@ function registerBugReport(program2) {
3247
3599
  tailFile(DEBUG_LOG_PATH, DEBUG_LOG_TAIL_LINES)
3248
3600
  ].join("\n");
3249
3601
  try {
3250
- if (!import_fs21.default.existsSync(WORKSCRIBE_DIR)) {
3251
- import_fs21.default.mkdirSync(WORKSCRIBE_DIR, { recursive: true, mode: 448 });
3602
+ if (!import_fs22.default.existsSync(WORKSCRIBE_DIR)) {
3603
+ import_fs22.default.mkdirSync(WORKSCRIBE_DIR, { recursive: true, mode: 448 });
3252
3604
  }
3253
- import_fs21.default.writeFileSync(reportPath, report, { mode: 384 });
3605
+ import_fs22.default.writeFileSync(reportPath, report, { mode: 384 });
3254
3606
  } catch (error) {
3255
3607
  log.error(`Could not write report: ${error.message}`);
3256
3608
  process.exit(1);
3257
3609
  }
3258
3610
  const params = new URLSearchParams({
3259
- title: `Bug report \u2014 workscribe ${"1.12.0"}`,
3260
- body: `<!-- Attach ${import_path15.default.basename(reportPath)} or paste the relevant sections below -->
3611
+ title: `Bug report \u2014 workscribe ${"1.13.0"}`,
3612
+ body: `<!-- Attach ${import_path16.default.basename(reportPath)} or paste the relevant sections below -->
3261
3613
 
3262
3614
  **Describe the issue:**
3263
3615
 
@@ -3275,19 +3627,119 @@ function registerBugReport(program2) {
3275
3627
  });
3276
3628
  }
3277
3629
 
3630
+ // src/cli/alerts.ts
3631
+ var import_chalk13 = __toESM(require("chalk"));
3632
+ function formatTimestamp(ms) {
3633
+ return new Date(ms).toLocaleString([], {
3634
+ month: "short",
3635
+ day: "numeric",
3636
+ hour: "2-digit",
3637
+ minute: "2-digit"
3638
+ });
3639
+ }
3640
+ function registerAlerts(program2) {
3641
+ const alertsCmd = program2.command("alerts").description("Show and manage security alerts from AI session monitoring").option("--check", "Print warning if alerts are pending (used by shell hooks)").action((opts) => {
3642
+ if (opts.check) {
3643
+ if (hasPendingAlerts()) {
3644
+ log.plain(
3645
+ import_chalk13.default.yellow(" [workscribe] Possible credentials detected in an AI session.") + import_chalk13.default.dim(" Run workscribe alerts for details.")
3646
+ );
3647
+ }
3648
+ return;
3649
+ }
3650
+ const alerts = getPendingAlerts();
3651
+ if (alerts.length === 0) {
3652
+ log.plain("");
3653
+ log.plain(" No pending security alerts.");
3654
+ log.plain("");
3655
+ return;
3656
+ }
3657
+ log.plain("");
3658
+ log.plain(import_chalk13.default.bold(" Security Alerts") + import_chalk13.default.dim(` (${alerts.length} pending)`));
3659
+ log.plain("");
3660
+ for (const alert of alerts) {
3661
+ log.plain(` ${import_chalk13.default.yellow("!")} ${import_chalk13.default.dim(formatTimestamp(alert.created_at))} ${import_chalk13.default.cyan(alert.tool)}`);
3662
+ log.plain(` ${alert.context}`);
3663
+ if (alert.session_id) log.plain(` ${import_chalk13.default.dim("Session: " + alert.session_id)}`);
3664
+ log.plain("");
3665
+ }
3666
+ log.plain(import_chalk13.default.dim(" Run workscribe alerts clear to dismiss all alerts"));
3667
+ log.plain("");
3668
+ });
3669
+ alertsCmd.command("clear").description("Dismiss all pending security alerts").action(() => {
3670
+ const count = clearAlerts();
3671
+ if (count === 0) {
3672
+ log.plain(" No pending alerts to clear.");
3673
+ } else {
3674
+ log.plain(` ${count} alert${count === 1 ? "" : "s"} cleared.`);
3675
+ }
3676
+ });
3677
+ }
3678
+
3679
+ // src/cli/context.ts
3680
+ var import_path17 = __toESM(require("path"));
3681
+ var import_chalk14 = __toESM(require("chalk"));
3682
+ function printSessionContext(session, heading) {
3683
+ const ago = timeAgo(session.started_at);
3684
+ const branch = getSessionBranch(session.id);
3685
+ const durationMinutes = (new Date(session.ended_at).getTime() - new Date(session.started_at).getTime()) / 6e4;
3686
+ log.plain("");
3687
+ log.plain(` ${import_chalk14.default.bold(heading)}: ${import_chalk14.default.cyan(ago)}`);
3688
+ log.plain("");
3689
+ log.plain(` Date ${session.date}`);
3690
+ log.plain(` Duration ${formatDuration(durationMinutes)}`);
3691
+ if (branch) log.plain(` Branch ${import_chalk14.default.dim(branch)}`);
3692
+ log.plain(` Events ${session.event_count}`);
3693
+ if (session.summary) {
3694
+ log.plain("");
3695
+ for (const line of session.summary.split("\n")) {
3696
+ log.plain(` ${line}`);
3697
+ }
3698
+ } else {
3699
+ log.plain("");
3700
+ log.plain(import_chalk14.default.dim(` No summary \u2014 run: workscribe summary ${session.date}`));
3701
+ }
3702
+ log.plain("");
3703
+ }
3704
+ function registerContext(program2) {
3705
+ program2.command("context").description("Show the last session for the current repo").option("--file <path>", "narrow to last session that touched this file").action((opts) => {
3706
+ if (opts.file) {
3707
+ const normalizedFile = import_path17.default.normalize(opts.file);
3708
+ const session = getLastSessionTouchingFile(normalizedFile);
3709
+ if (!session) {
3710
+ log.plain(` No sessions found touching ${normalizedFile}`);
3711
+ return;
3712
+ }
3713
+ printSessionContext(session, `Last session touching ${import_path17.default.basename(normalizedFile)}`);
3714
+ return;
3715
+ }
3716
+ const { repoName } = getGitContext(process.cwd());
3717
+ if (!repoName) {
3718
+ log.plain(" Not in a git repository. Run workscribe context from a project directory.");
3719
+ return;
3720
+ }
3721
+ const sessions = getRecentSessionsByRepo(repoName, 1);
3722
+ if (sessions.length === 0) {
3723
+ log.plain(` No sessions found for ${repoName}`);
3724
+ return;
3725
+ }
3726
+ printSessionContext(sessions[0], `Last session on ${repoName}`);
3727
+ });
3728
+ }
3729
+
3278
3730
  // src/utils/update-check.ts
3279
- var import_fs22 = __toESM(require("fs"));
3731
+ var import_fs23 = __toESM(require("fs"));
3280
3732
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
3281
3733
  function readCache() {
3282
3734
  try {
3283
- return JSON.parse(import_fs22.default.readFileSync(UPDATE_CHECK_PATH, "utf-8"));
3735
+ return JSON.parse(import_fs23.default.readFileSync(UPDATE_CHECK_PATH, "utf-8"));
3284
3736
  } catch {
3285
3737
  return null;
3286
3738
  }
3287
3739
  }
3288
3740
  function writeCache(cache) {
3289
3741
  try {
3290
- import_fs22.default.writeFileSync(UPDATE_CHECK_PATH, JSON.stringify(cache), { mode: 384 });
3742
+ import_fs23.default.writeFileSync(UPDATE_CHECK_PATH, JSON.stringify(cache), { mode: 384 });
3291
3743
  } catch {
3292
3744
  }
3293
3745
  }
@@ -3300,7 +3752,7 @@ function isNewer(latest, current) {
3300
3752
  return latestPatch > currentPatch;
3301
3753
  }
3302
3754
  function checkForUpdate() {
3303
- const current = "1.12.0";
3755
+ const current = "1.13.0";
3304
3756
  const cache = readCache();
3305
3757
  if (cache && isNewer(cache.latestVersion, current)) {
3306
3758
  process.stderr.write(
@@ -3325,7 +3777,7 @@ function checkForUpdate() {
3325
3777
  assertNodeVersion(process.versions.node);
3326
3778
  checkForUpdate();
3327
3779
  var program = new import_commander.Command();
3328
- program.name("workscribe").description("Automatic developer work journal").version("1.12.0");
3780
+ program.name("workscribe").description("Automatic developer work journal").version("1.13.0");
3329
3781
  registerCapture(program);
3330
3782
  registerHook(program);
3331
3783
  registerInit(program);
@@ -3344,5 +3796,7 @@ registerNote(program);
3344
3796
  registerWeek(program);
3345
3797
  registerBugReport(program);
3346
3798
  registerCompletions(program);
3799
+ registerAlerts(program);
3800
+ registerContext(program);
3347
3801
  program.parse();
3348
3802
  //# sourceMappingURL=index.js.map