@workscribe/cli 1.12.0 → 1.14.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;
@@ -354,15 +436,24 @@ function redact(command, extraPatterns = []) {
354
436
  var import_child_process = require("child_process");
355
437
  var import_path2 = __toESM(require("path"));
356
438
  var import_fs3 = __toESM(require("fs"));
357
- function nameFromRemote(root) {
439
+ function parseOwnerAndRepo(url) {
440
+ const normalized = url.trim().replace(/^[^/]+:(?!\/\/)/, (match) => match.replace(":", "/"));
441
+ const segments = normalized.split("/").filter(Boolean);
442
+ if (segments.length < 2) return null;
443
+ const repo = segments[segments.length - 1].replace(/\.git$/, "");
444
+ const owner = segments[segments.length - 2];
445
+ return owner && repo ? { owner, repo } : null;
446
+ }
447
+ function nameFromRemote(root, qualifyWithOwner) {
358
448
  try {
359
449
  const url = (0, import_child_process.execSync)("git remote get-url origin", {
360
450
  cwd: root,
361
451
  timeout: 2e3,
362
452
  stdio: "pipe"
363
453
  }).toString().trim();
364
- const match = url.match(/\/([^/]+?)(?:\.git)?$/);
365
- return match ? match[1] : null;
454
+ const parsed = parseOwnerAndRepo(url);
455
+ if (!parsed) return null;
456
+ return qualifyWithOwner ? `${parsed.owner}/${parsed.repo}` : parsed.repo;
366
457
  } catch {
367
458
  return null;
368
459
  }
@@ -377,14 +468,14 @@ function nameFromPackageJson(root) {
377
468
  return null;
378
469
  }
379
470
  }
380
- function getGitContext(cwd, nameOverride) {
471
+ function getGitContext(cwd, nameOverride, qualifyRepoName = false) {
381
472
  try {
382
473
  const root = (0, import_child_process.execSync)("git rev-parse --show-toplevel", {
383
474
  cwd,
384
475
  timeout: 2e3,
385
476
  stdio: "pipe"
386
477
  }).toString().trim();
387
- const repoName = nameOverride ?? nameFromRemote(root) ?? nameFromPackageJson(root) ?? import_path2.default.basename(root);
478
+ const repoName = nameOverride ?? nameFromRemote(root, qualifyRepoName) ?? nameFromPackageJson(root) ?? import_path2.default.basename(root);
388
479
  let branch = null;
389
480
  try {
390
481
  branch = (0, import_child_process.execSync)("git branch --show-current", {
@@ -421,7 +512,7 @@ function readLastAiderPrompt(cwd) {
421
512
  try {
422
513
  const histPath = import_path3.default.join(cwd, HISTORY_FILE);
423
514
  const content = import_fs4.default.readFileSync(histPath, "utf-8");
424
- const matches = [...content.matchAll(/^> (.+)$/gm)];
515
+ const matches = [...content.matchAll(/^#### (.+)$/gm)];
425
516
  if (!matches.length) return null;
426
517
  return matches[matches.length - 1][1].trim().slice(0, PROMPT_MAX_LENGTH);
427
518
  } catch {
@@ -441,56 +532,108 @@ var ai_tools_default = [
441
532
  displayName: "Claude Code",
442
533
  pattern: "^claude\\b",
443
534
  hook: "claude-code",
444
- contextSource: "hook"
535
+ contextSource: "hook",
536
+ historyFormat: null,
537
+ securityWarning: null
445
538
  },
446
539
  {
447
540
  tool: "aider",
448
541
  displayName: "Aider",
449
542
  pattern: "^aider\\b",
450
543
  hook: null,
451
- contextSource: "history-file"
544
+ contextSource: "history-file",
545
+ historyFormat: "markdown",
546
+ securityWarning: null
547
+ },
548
+ {
549
+ tool: "codex",
550
+ displayName: "Codex CLI",
551
+ pattern: "^codex\\b",
552
+ hook: null,
553
+ contextSource: "history-file",
554
+ historyFormat: "jsonl",
555
+ securityWarning: null
452
556
  },
453
557
  {
454
558
  tool: "gemini",
455
- displayName: "Gemini",
559
+ displayName: "Gemini CLI",
456
560
  pattern: "^gemini\\b",
457
561
  hook: null,
458
- contextSource: null
562
+ contextSource: "history-file",
563
+ historyFormat: "json",
564
+ securityWarning: null
459
565
  },
460
566
  {
461
567
  tool: "gh copilot",
462
568
  displayName: "GitHub Copilot",
463
569
  pattern: "^gh\\s+copilot\\b",
464
570
  hook: null,
465
- contextSource: null
466
- },
467
- {
468
- tool: "cody",
469
- displayName: "Cody",
470
- pattern: "^cody\\b",
471
- hook: null,
472
- contextSource: null
571
+ contextSource: "history-file",
572
+ historyFormat: "jsonl",
573
+ securityWarning: null
473
574
  },
474
575
  {
475
576
  tool: "amazon-q",
476
577
  displayName: "Amazon Q",
477
578
  pattern: "^q\\s+(chat|dev)\\b",
478
579
  hook: null,
479
- contextSource: null
580
+ contextSource: "history-file",
581
+ historyFormat: "json",
582
+ securityWarning: null
583
+ },
584
+ {
585
+ tool: "opencode",
586
+ displayName: "OpenCode",
587
+ pattern: "^opencode\\b",
588
+ hook: null,
589
+ contextSource: "sqlite",
590
+ historyFormat: "sqlite",
591
+ securityWarning: null
592
+ },
593
+ {
594
+ tool: "kilo",
595
+ displayName: "Kilo Code",
596
+ pattern: "^kilo\\b",
597
+ hook: null,
598
+ contextSource: null,
599
+ historyFormat: null,
600
+ securityWarning: null
601
+ },
602
+ {
603
+ tool: "grok",
604
+ displayName: "Grok Build",
605
+ pattern: "^grok\\b",
606
+ hook: null,
607
+ contextSource: null,
608
+ historyFormat: null,
609
+ 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
610
  },
481
611
  {
482
612
  tool: "continue",
483
613
  displayName: "Continue",
484
614
  pattern: "^continue\\b",
485
615
  hook: null,
486
- contextSource: null
616
+ contextSource: null,
617
+ historyFormat: null,
618
+ securityWarning: null
619
+ },
620
+ {
621
+ tool: "cody",
622
+ displayName: "Cody",
623
+ pattern: "^cody\\b",
624
+ hook: null,
625
+ contextSource: null,
626
+ historyFormat: null,
627
+ securityWarning: null
487
628
  },
488
629
  {
489
630
  tool: "cursor",
490
631
  displayName: "Cursor",
491
632
  pattern: "^cursor\\b",
492
633
  hook: null,
493
- contextSource: null
634
+ contextSource: null,
635
+ historyFormat: null,
636
+ securityWarning: null
494
637
  }
495
638
  ];
496
639
 
@@ -529,7 +672,9 @@ function loadAIToolRegistry() {
529
672
  displayName: entry.displayName ?? entry.tool,
530
673
  pattern: entry.pattern,
531
674
  hook: entry.hook ?? null,
532
- contextSource: entry.contextSource ?? null
675
+ contextSource: entry.contextSource ?? null,
676
+ historyFormat: entry.historyFormat ?? null,
677
+ securityWarning: entry.securityWarning ?? null
533
678
  }));
534
679
  }
535
680
  }
@@ -558,6 +703,9 @@ var DEFAULTS = {
558
703
  session: {
559
704
  idleTimeout: 30
560
705
  },
706
+ git: {
707
+ qualifyRepoName: false
708
+ },
561
709
  capture: {
562
710
  ignore: ["cd", "ls", "ll", "la", "ls -la", "ls -l", "ls -a", "pwd", "clear", "exit", "history", "man"],
563
711
  aiTools: true,
@@ -1074,11 +1222,41 @@ function isDuplicate(command, cwd, lastEvent, dedupWindowMs = 5e3, now = Date.no
1074
1222
  return lastEvent.command === normalizeForFilter(command) && lastEvent.cwd === cwd && now - lastTime < dedupWindowMs;
1075
1223
  }
1076
1224
 
1225
+ // src/capture/ide.ts
1226
+ var IDE_DEDUP_WINDOW_MS = 3e4;
1227
+
1077
1228
  // src/capture/index.ts
1078
1229
  function captureCommand(opts) {
1079
1230
  if (import_fs10.default.existsSync(PAUSE_FLAG_PATH)) return;
1080
1231
  const config = readConfig();
1081
- const { command, cwd, exitCode, durationMs } = opts;
1232
+ const { cwd, exitCode, durationMs } = opts;
1233
+ if (opts.type) {
1234
+ if (opts.type === "code_edit" && opts.file) {
1235
+ const lastForFile = getLastEventForFile(opts.file, cwd);
1236
+ if (lastForFile) {
1237
+ const lastTime = new Date(lastForFile.captured_at).getTime();
1238
+ if (Date.now() - lastTime < IDE_DEDUP_WINDOW_MS) return;
1239
+ }
1240
+ }
1241
+ const command2 = opts.command ?? opts.file ?? opts.type;
1242
+ const nameOverride2 = config.projects?.[cwd] ?? null;
1243
+ const { repoName: repoName2, branch: branch2 } = getGitContext(cwd, nameOverride2, config.git.qualifyRepoName);
1244
+ insertEvent({
1245
+ captured_at: (/* @__PURE__ */ new Date()).toISOString(),
1246
+ command: command2,
1247
+ cwd,
1248
+ exit_code: exitCode,
1249
+ duration_ms: durationMs,
1250
+ repo_name: repoName2,
1251
+ branch: branch2,
1252
+ category: opts.type,
1253
+ metadata: null,
1254
+ source: opts.source ?? null,
1255
+ file_path: opts.file ?? null
1256
+ });
1257
+ return;
1258
+ }
1259
+ const command = opts.command;
1082
1260
  if (shouldFilter(command, config.capture.ignore)) return;
1083
1261
  const last = getLastEvent();
1084
1262
  if (isDuplicate(command, cwd, last ?? void 0)) return;
@@ -1092,7 +1270,6 @@ function captureCommand(opts) {
1092
1270
  if (firstToken) metadata.tool = firstToken;
1093
1271
  metadata.hint = safeCommand.trim().slice(0, 120);
1094
1272
  }
1095
- if (opts.source) metadata.source = opts.source;
1096
1273
  if (category === "ai_session") {
1097
1274
  if (opts.gitBefore) {
1098
1275
  const commits = getCommitsSince(opts.gitBefore, cwd);
@@ -1115,24 +1292,30 @@ function captureCommand(opts) {
1115
1292
  repo_name: repoName,
1116
1293
  branch,
1117
1294
  category,
1118
- metadata: Object.keys(metadata).length > 0 ? JSON.stringify(metadata) : null
1295
+ metadata: Object.keys(metadata).length > 0 ? JSON.stringify(metadata) : null,
1296
+ source: opts.source ?? null,
1297
+ file_path: null
1119
1298
  });
1120
1299
  }
1121
1300
 
1122
1301
  // src/cli/capture.ts
1123
1302
  var CAPTURE_HANG_TIMEOUT_MS = 5e3;
1124
1303
  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) => {
1304
+ 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
1305
  const hangGuard = setTimeout(() => process.exit(0), CAPTURE_HANG_TIMEOUT_MS);
1127
1306
  hangGuard.unref();
1128
1307
  try {
1129
- if (!opts.cmd || !opts.cwd) return;
1308
+ if (!opts.cwd) return;
1309
+ if (!opts.cmd && !opts.type) return;
1130
1310
  captureCommand({
1131
1311
  command: opts.cmd,
1132
1312
  cwd: opts.cwd,
1133
1313
  exitCode: opts.exit ?? null,
1134
1314
  durationMs: opts.duration ?? null,
1135
- gitBefore: opts.gitBefore ?? void 0
1315
+ gitBefore: opts.gitBefore ?? void 0,
1316
+ source: opts.source ?? void 0,
1317
+ type: opts.type ?? void 0,
1318
+ file: opts.file ?? void 0
1136
1319
  });
1137
1320
  } catch (err) {
1138
1321
  debugLog("_capture", err);
@@ -1140,6 +1323,38 @@ function registerCapture(program2) {
1140
1323
  });
1141
1324
  }
1142
1325
 
1326
+ // src/capture/detect-secrets.ts
1327
+ function containsSecrets(text) {
1328
+ return redact(text) !== text;
1329
+ }
1330
+
1331
+ // src/db/alerts.ts
1332
+ var import_fs11 = __toESM(require("fs"));
1333
+ var import_path7 = __toESM(require("path"));
1334
+ function insertAlert(tool, sessionId, context) {
1335
+ getDb().prepare(
1336
+ `INSERT INTO alerts (created_at, tool, session_id, context)
1337
+ VALUES ($created_at, $tool, $session_id, $context)`
1338
+ ).run({ created_at: Date.now(), tool, session_id: sessionId ?? null, context });
1339
+ import_fs11.default.mkdirSync(import_path7.default.dirname(ALERTS_PENDING_PATH), { recursive: true });
1340
+ import_fs11.default.writeFileSync(ALERTS_PENDING_PATH, "", { flag: "w" });
1341
+ }
1342
+ function getPendingAlerts() {
1343
+ return getDb().prepare("SELECT * FROM alerts WHERE cleared = 0 ORDER BY created_at DESC").all();
1344
+ }
1345
+ function hasPendingAlerts() {
1346
+ const row = getDb().prepare("SELECT COUNT(*) as count FROM alerts WHERE cleared = 0").get();
1347
+ return Number(row.count) > 0;
1348
+ }
1349
+ function clearAlerts() {
1350
+ const result = getDb().prepare("UPDATE alerts SET cleared = 1 WHERE cleared = 0").run();
1351
+ try {
1352
+ import_fs11.default.unlinkSync(ALERTS_PENDING_PATH);
1353
+ } catch {
1354
+ }
1355
+ return Number(result.changes);
1356
+ }
1357
+
1143
1358
  // src/cli/hook.ts
1144
1359
  function parseHookPayload(raw) {
1145
1360
  if (!raw.trim()) return null;
@@ -1150,8 +1365,17 @@ function parseHookPayload(raw) {
1150
1365
  if (!command || !cwd) return null;
1151
1366
  return { command, cwd };
1152
1367
  }
1368
+ function parseUserPromptPayload(raw) {
1369
+ if (!raw.trim()) return null;
1370
+ const payload = JSON.parse(raw);
1371
+ const prompt = typeof payload.prompt === "string" ? payload.prompt : null;
1372
+ const sessionId = typeof payload.session_id === "string" ? payload.session_id : "";
1373
+ const cwd = typeof payload.cwd === "string" ? payload.cwd : "";
1374
+ if (!prompt) return null;
1375
+ return { session_id: sessionId, prompt, cwd };
1376
+ }
1153
1377
  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) => {
1378
+ 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
1379
  let raw = "";
1156
1380
  process.stdin.setEncoding("utf-8");
1157
1381
  process.stdin.on("data", (chunk) => {
@@ -1159,6 +1383,23 @@ function registerHook(program2) {
1159
1383
  });
1160
1384
  process.stdin.on("end", () => {
1161
1385
  try {
1386
+ if (opts.event === "UserPromptSubmit") {
1387
+ const parsed2 = parseUserPromptPayload(raw);
1388
+ if (!parsed2) return;
1389
+ if (containsSecrets(parsed2.prompt)) {
1390
+ insertAlert(
1391
+ opts.source ?? "unknown",
1392
+ parsed2.session_id || null,
1393
+ "Possible credential detected in prompt submitted to AI tool"
1394
+ );
1395
+ process.stdout.write(
1396
+ JSON.stringify({
1397
+ systemMessage: "Workscribe detected a possible credential in your prompt. Run `workscribe alerts` for details."
1398
+ })
1399
+ );
1400
+ }
1401
+ return;
1402
+ }
1162
1403
  if (readConfig().capture.aiTools === false) return;
1163
1404
  const parsed = parseHookPayload(raw);
1164
1405
  if (!parsed) return;
@@ -1177,8 +1418,8 @@ function registerHook(program2) {
1177
1418
  }
1178
1419
 
1179
1420
  // src/cli/init.ts
1180
- var import_fs13 = __toESM(require("fs"));
1181
- var import_path9 = __toESM(require("path"));
1421
+ var import_fs14 = __toESM(require("fs"));
1422
+ var import_path10 = __toESM(require("path"));
1182
1423
  var import_os5 = __toESM(require("os"));
1183
1424
  var import_prompts = __toESM(require("prompts"));
1184
1425
  var import_chalk2 = __toESM(require("chalk"));
@@ -1224,6 +1465,17 @@ function eachDayOfRange(start, end) {
1224
1465
  }
1225
1466
  return days;
1226
1467
  }
1468
+ function timeAgo(isoString, now = Date.now()) {
1469
+ const elapsedMs = now - new Date(isoString).getTime();
1470
+ const minutes = Math.floor(elapsedMs / 6e4);
1471
+ if (minutes < 2) return "just now";
1472
+ if (minutes < 60) return `${minutes} minutes ago`;
1473
+ const hours = Math.floor(minutes / 60);
1474
+ if (hours < 24) return hours === 1 ? "1 hour ago" : `${hours} hours ago`;
1475
+ const days = Math.floor(hours / 24);
1476
+ if (days === 1) return "yesterday";
1477
+ return `${days} days ago`;
1478
+ }
1227
1479
  function isoWeekKey(dateStr) {
1228
1480
  const d = /* @__PURE__ */ new Date(dateStr + "T00:00:00");
1229
1481
  const jan4 = new Date(d.getFullYear(), 0, 4);
@@ -1272,6 +1524,8 @@ RULES \u2014 follow these exactly:
1272
1524
 
1273
1525
  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
1526
 
1527
+ 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.
1528
+
1275
1529
  Maximum 5 bullets. Output ONLY the bullet points, nothing else.`;
1276
1530
  function getSystemPrompt() {
1277
1531
  const contexts = getActiveProfileAiContexts();
@@ -1313,7 +1567,9 @@ function buildUserMessage(payload, includeCategories = []) {
1313
1567
  if (excluded.has(event.category)) continue;
1314
1568
  const failed = event.exitCode !== null && event.exitCode !== 0 ? " [failed]" : "";
1315
1569
  let key;
1316
- if (event.hint) {
1570
+ if (event.category === "code_edit") {
1571
+ key = `code_edit${failed}`;
1572
+ } else if (event.hint) {
1317
1573
  key = `other [${event.hint}]${failed}`;
1318
1574
  } else {
1319
1575
  const meta = Object.keys(event.metadata).length > 0 ? ` ${JSON.stringify(event.metadata)}` : "";
@@ -1381,7 +1637,9 @@ function buildWeeklyMessage(payload) {
1381
1637
  if (excluded.has(event.category)) continue;
1382
1638
  const failed = event.exitCode !== null && event.exitCode !== 0 ? " [failed]" : "";
1383
1639
  let key;
1384
- if (event.hint) {
1640
+ if (event.category === "code_edit") {
1641
+ key = `code_edit${failed}`;
1642
+ } else if (event.hint) {
1385
1643
  key = `other [${event.hint}]${failed}`;
1386
1644
  } else {
1387
1645
  const meta = Object.keys(event.metadata).length > 0 ? ` ${JSON.stringify(event.metadata)}` : "";
@@ -1412,7 +1670,7 @@ var GenericProvider = class {
1412
1670
  baseUrl;
1413
1671
  includeCategories;
1414
1672
  async call(system, userMsg, maxTokens) {
1415
- const response = await fetchWithTimeout(this.config.chatUrl(this.baseUrl), {
1673
+ const response = await fetchWithTimeout(this.config.chatUrl(this.baseUrl, this.apiKey, this.model), {
1416
1674
  method: "POST",
1417
1675
  headers: this.config.headers(this.apiKey),
1418
1676
  body: JSON.stringify(this.config.buildChatBody(this.model, system, userMsg, maxTokens))
@@ -1430,7 +1688,7 @@ var GenericProvider = class {
1430
1688
  }
1431
1689
  async validate() {
1432
1690
  const isPost = this.config.validateMethod === "POST";
1433
- const response = await fetchWithTimeout(this.config.validateUrl(this.baseUrl), {
1691
+ const response = await fetchWithTimeout(this.config.validateUrl(this.baseUrl, this.apiKey, this.model), {
1434
1692
  method: this.config.validateMethod,
1435
1693
  headers: isPost ? this.config.headers(this.apiKey) : void 0,
1436
1694
  body: isPost && this.config.buildValidateBody ? JSON.stringify(this.config.buildValidateBody(this.model)) : void 0
@@ -1529,6 +1787,59 @@ var PROVIDER_CONFIGS = {
1529
1787
  },
1530
1788
  chatError: (status, body) => `Ollama error ${status}: ${body}. Is Ollama running? Try: ollama serve`,
1531
1789
  validateError: (_status, _body, baseUrl) => `Ollama is not reachable at ${baseUrl}. Is it running? Try: ollama serve`
1790
+ },
1791
+ gemini: {
1792
+ defaultModel: "gemini-2.0-flash-lite",
1793
+ defaultBaseUrl: "https://generativelanguage.googleapis.com/v1beta",
1794
+ dailyTokens: 1024,
1795
+ weeklyTokens: 1024,
1796
+ // API key and model are embedded in the URL — not in headers
1797
+ chatUrl: (baseUrl, apiKey, model) => `${baseUrl}/models/${model}:generateContent?key=${apiKey}`,
1798
+ validateUrl: (baseUrl, apiKey, model) => `${baseUrl}/models/${model}:generateContent?key=${apiKey}`,
1799
+ validateMethod: "POST",
1800
+ headers: () => ({ "Content-Type": "application/json" }),
1801
+ buildChatBody: (_model, system, userMsg, maxTokens) => ({
1802
+ system_instruction: { parts: [{ text: system }] },
1803
+ contents: [{ role: "user", parts: [{ text: userMsg }] }],
1804
+ generationConfig: { maxOutputTokens: maxTokens, temperature: 0.3 }
1805
+ }),
1806
+ buildValidateBody: () => ({
1807
+ contents: [{ role: "user", parts: [{ text: "." }] }],
1808
+ generationConfig: { maxOutputTokens: 1 }
1809
+ }),
1810
+ parseText: (json) => {
1811
+ const data = json;
1812
+ return data.candidates?.[0]?.content?.parts?.[0]?.text ?? "";
1813
+ },
1814
+ chatError: (status, body) => `Gemini API error ${status}: ${body}`,
1815
+ validateError: (status, body) => `Gemini API error ${status}: ${body}`
1816
+ },
1817
+ groq: {
1818
+ defaultModel: "llama-3.1-8b-instant",
1819
+ defaultBaseUrl: "https://api.groq.com/openai/v1",
1820
+ dailyTokens: 1024,
1821
+ weeklyTokens: 1024,
1822
+ chatUrl: (baseUrl) => `${baseUrl}/chat/completions`,
1823
+ validateUrl: (baseUrl) => `${baseUrl}/models`,
1824
+ validateMethod: "GET",
1825
+ headers: (apiKey) => ({
1826
+ "Content-Type": "application/json",
1827
+ Authorization: `Bearer ${apiKey}`
1828
+ }),
1829
+ buildChatBody: (model, system, userMsg, maxTokens) => ({
1830
+ model,
1831
+ messages: [
1832
+ { role: "system", content: system },
1833
+ { role: "user", content: userMsg }
1834
+ ],
1835
+ max_tokens: maxTokens
1836
+ }),
1837
+ parseText: (json) => {
1838
+ const data = json;
1839
+ return data.choices?.[0]?.message?.content ?? "";
1840
+ },
1841
+ chatError: (status, body) => `Groq API error ${status}: ${body}`,
1842
+ validateError: (status, body) => `Groq API error ${status}: ${body}`
1532
1843
  }
1533
1844
  };
1534
1845
 
@@ -1537,7 +1848,7 @@ function getProvider(config) {
1537
1848
  const { provider, apiKey = "", model, baseUrl } = config.ai;
1538
1849
  if (!provider) {
1539
1850
  throw new Error(
1540
- "No AI provider configured.\nRun: workscribe config set ai.provider <anthropic|openai|ollama|openai-compatible>"
1851
+ "No AI provider configured.\nRun: workscribe config set ai.provider <anthropic|openai|gemini|groq|ollama|openai-compatible>"
1541
1852
  );
1542
1853
  }
1543
1854
  const configKey = provider === "openai-compatible" ? "openai" : provider;
@@ -1545,7 +1856,7 @@ function getProvider(config) {
1545
1856
  if (!providerConfig) {
1546
1857
  throw new Error(
1547
1858
  `Unknown provider: "${provider}"
1548
- Supported providers: anthropic, openai, ollama, openai-compatible`
1859
+ Supported providers: anthropic, openai, gemini, groq, ollama, openai-compatible`
1549
1860
  );
1550
1861
  }
1551
1862
  if (provider !== "ollama" && !apiKey) {
@@ -1565,9 +1876,10 @@ Run: workscribe config set ai.apiKey <your-key>`);
1565
1876
  }
1566
1877
 
1567
1878
  // src/capture/shell-hook.ts
1568
- var import_fs11 = __toESM(require("fs"));
1569
- var import_path7 = __toESM(require("path"));
1879
+ var import_fs12 = __toESM(require("fs"));
1880
+ var import_path8 = __toESM(require("path"));
1570
1881
  var import_os3 = __toESM(require("os"));
1882
+ var import_child_process3 = require("child_process");
1571
1883
 
1572
1884
  // src/utils/completion-scripts.ts
1573
1885
  function bashCompletion(commands) {
@@ -1624,13 +1936,13 @@ function fishCompletion(commands) {
1624
1936
  }
1625
1937
 
1626
1938
  // src/capture/shell-hook.ts
1627
- var FISH_COMPLETIONS_DIR = import_path7.default.join(import_os3.default.homedir(), ".config", "fish", "completions");
1939
+ var FISH_COMPLETIONS_DIR = import_path8.default.join(import_os3.default.homedir(), ".config", "fish", "completions");
1628
1940
  var HOOK_START = "# workscribe:hook:start";
1629
1941
  var HOOK_END = "# workscribe:hook:end";
1630
1942
  var HOOK_PREAMBLE = "# Workscribe \u2014 developer work journal";
1631
1943
  function getInstalledHookBlock(rcFile) {
1632
- if (!import_fs11.default.existsSync(rcFile)) return null;
1633
- const content = import_fs11.default.readFileSync(rcFile, "utf-8");
1944
+ if (!import_fs12.default.existsSync(rcFile)) return null;
1945
+ const content = import_fs12.default.readFileSync(rcFile, "utf-8");
1634
1946
  const startIdx = content.indexOf(HOOK_START);
1635
1947
  const endIdx = content.indexOf(HOOK_END);
1636
1948
  if (startIdx === -1 || endIdx === -1) return null;
@@ -1646,54 +1958,66 @@ function isHookOutdated(rcFile, hookContent) {
1646
1958
  }
1647
1959
  function installHook(rcFile, hookContent) {
1648
1960
  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, `
1961
+ import_fs12.default.copyFileSync(rcFile, backup);
1962
+ log.info(`Backed up ${rcFile} \u2192 ${import_path8.default.basename(backup)}`);
1963
+ import_fs12.default.appendFileSync(rcFile, `
1652
1964
  ${hookContent}
1653
1965
  `);
1654
1966
  }
1655
1967
  function updateHook(rcFile, hookContent) {
1656
1968
  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");
1969
+ import_fs12.default.copyFileSync(rcFile, backup);
1970
+ log.info(`Backed up ${rcFile} \u2192 ${import_path8.default.basename(backup)}`);
1971
+ const content = import_fs12.default.readFileSync(rcFile, "utf-8");
1660
1972
  const startIdx = content.indexOf(HOOK_START);
1661
1973
  const endIdx = content.indexOf(HOOK_END) + HOOK_END.length;
1662
- import_fs11.default.writeFileSync(rcFile, content.slice(0, startIdx) + hookContent + content.slice(endIdx));
1974
+ import_fs12.default.writeFileSync(rcFile, content.slice(0, startIdx) + hookContent + content.slice(endIdx));
1663
1975
  }
1664
1976
  function removeHook(rcFile) {
1665
1977
  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");
1978
+ import_fs12.default.copyFileSync(rcFile, backup);
1979
+ log.info(`Backed up ${rcFile} \u2192 ${import_path8.default.basename(backup)}`);
1980
+ const content = import_fs12.default.readFileSync(rcFile, "utf-8");
1669
1981
  const startIdx = content.indexOf(HOOK_START);
1670
1982
  const endIdx = content.indexOf(HOOK_END) + HOOK_END.length;
1671
1983
  const preambleIdx = content.lastIndexOf(HOOK_PREAMBLE, startIdx);
1672
1984
  const blockStart = preambleIdx !== -1 ? preambleIdx : startIdx;
1673
1985
  const before = content.slice(0, blockStart).replace(/\n+$/, "\n");
1674
1986
  const after = content.slice(endIdx).replace(/^\n/, "");
1675
- import_fs11.default.writeFileSync(rcFile, before + after);
1987
+ import_fs12.default.writeFileSync(rcFile, before + after);
1988
+ }
1989
+ function resolveWorkscribeBinary() {
1990
+ try {
1991
+ const resolved = (0, import_child_process3.execFileSync)("which", ["workscribe"], { encoding: "utf-8" }).trim();
1992
+ return resolved || null;
1993
+ } catch {
1994
+ return null;
1995
+ }
1996
+ }
1997
+ function injectBinaryPath(hookContent, binaryPath) {
1998
+ 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}"
1999
+ or return 0`).replaceAll("command workscribe _capture", `"${binaryPath}" _capture`).replaceAll("command workscribe alerts", `"${binaryPath}" alerts`);
1676
2000
  }
1677
2001
  function isCompletionsInstalled(shell) {
1678
2002
  if (shell.name === "fish") {
1679
- return import_fs11.default.existsSync(import_path7.default.join(FISH_COMPLETIONS_DIR, "workscribe.fish"));
2003
+ return import_fs12.default.existsSync(import_path8.default.join(FISH_COMPLETIONS_DIR, "workscribe.fish"));
1680
2004
  }
1681
2005
  try {
1682
- return import_fs11.default.readFileSync(shell.rcFile, "utf-8").includes("workscribe completions");
2006
+ return import_fs12.default.readFileSync(shell.rcFile, "utf-8").includes("workscribe completions");
1683
2007
  } catch {
1684
2008
  return false;
1685
2009
  }
1686
2010
  }
1687
2011
  function installCompletions(shell, commands) {
1688
2012
  if (shell.name === "fish") {
1689
- if (!import_fs11.default.existsSync(FISH_COMPLETIONS_DIR)) {
1690
- import_fs11.default.mkdirSync(FISH_COMPLETIONS_DIR, { recursive: true });
2013
+ if (!import_fs12.default.existsSync(FISH_COMPLETIONS_DIR)) {
2014
+ import_fs12.default.mkdirSync(FISH_COMPLETIONS_DIR, { recursive: true });
1691
2015
  }
1692
- import_fs11.default.writeFileSync(import_path7.default.join(FISH_COMPLETIONS_DIR, "workscribe.fish"), fishCompletion(commands), { mode: 420 });
2016
+ import_fs12.default.writeFileSync(import_path8.default.join(FISH_COMPLETIONS_DIR, "workscribe.fish"), fishCompletion(commands), { mode: 420 });
1693
2017
  } else {
1694
2018
  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 });
2019
+ const cacheFile = import_path8.default.join(WORKSCRIBE_DIR, `completion.${shell.name}`);
2020
+ import_fs12.default.writeFileSync(cacheFile, script, { mode: 420 });
1697
2021
  const sourceLine = shell.name === "zsh" ? `
1698
2022
  # Workscribe completions
1699
2023
  autoload -Uz compinit && compinit
@@ -1702,20 +2026,20 @@ source "${cacheFile}"
1702
2026
  # Workscribe completions
1703
2027
  source "${cacheFile}"
1704
2028
  `;
1705
- import_fs11.default.appendFileSync(shell.rcFile, sourceLine);
2029
+ import_fs12.default.appendFileSync(shell.rcFile, sourceLine);
1706
2030
  }
1707
2031
  }
1708
2032
 
1709
2033
  // src/capture/hooks/claude-code.ts
1710
- var import_fs12 = __toESM(require("fs"));
1711
- var import_path8 = __toESM(require("path"));
2034
+ var import_fs13 = __toESM(require("fs"));
2035
+ var import_path9 = __toESM(require("path"));
1712
2036
  var import_os4 = __toESM(require("os"));
1713
2037
 
1714
2038
  // src/capture/hooks/utils.ts
1715
- var import_child_process3 = require("child_process");
2039
+ var import_child_process4 = require("child_process");
1716
2040
  function isCommandInstalled(cmd) {
1717
2041
  try {
1718
- (0, import_child_process3.execSync)(`which ${cmd}`, { stdio: "ignore" });
2042
+ (0, import_child_process4.execSync)(`which ${cmd}`, { stdio: "ignore" });
1719
2043
  return true;
1720
2044
  } catch {
1721
2045
  return false;
@@ -1723,11 +2047,11 @@ function isCommandInstalled(cmd) {
1723
2047
  }
1724
2048
 
1725
2049
  // src/capture/hooks/claude-code.ts
1726
- var CLAUDE_SETTINGS_PATH = import_path8.default.join(import_os4.default.homedir(), ".claude", "settings.json");
2050
+ var CLAUDE_SETTINGS_PATH = import_path9.default.join(import_os4.default.homedir(), ".claude", "settings.json");
1727
2051
  function readSettings() {
1728
- if (!import_fs12.default.existsSync(CLAUDE_SETTINGS_PATH)) return {};
2052
+ if (!import_fs13.default.existsSync(CLAUDE_SETTINGS_PATH)) return {};
1729
2053
  try {
1730
- return JSON.parse(import_fs12.default.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8"));
2054
+ return JSON.parse(import_fs13.default.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8"));
1731
2055
  } catch {
1732
2056
  return {};
1733
2057
  }
@@ -1739,19 +2063,30 @@ var claudeCodeIntegration = {
1739
2063
  },
1740
2064
  isHookInstalled() {
1741
2065
  const settings = readSettings();
1742
- const postToolUse = settings.hooks?.PostToolUse ?? [];
1743
- return postToolUse.some((entry) => {
1744
- const hooks = entry.hooks ?? [];
1745
- return hooks.some((hookItem) => {
2066
+ const hooks = settings.hooks ?? {};
2067
+ const postToolUse = hooks.PostToolUse ?? [];
2068
+ const hasPostToolUse = postToolUse.some((entry) => {
2069
+ const entryHooks = entry.hooks ?? [];
2070
+ return entryHooks.some((hookItem) => {
1746
2071
  const hook = hookItem;
1747
2072
  return typeof hook.command === "string" && hook.command.includes("workscribe _hook");
1748
2073
  });
1749
2074
  });
2075
+ const userPromptSubmit = hooks.UserPromptSubmit ?? [];
2076
+ const hasUserPromptSubmit = userPromptSubmit.some((entry) => {
2077
+ const entryHooks = entry.hooks ?? [];
2078
+ return entryHooks.some((hookItem) => {
2079
+ const hook = hookItem;
2080
+ return typeof hook.command === "string" && hook.command.includes("workscribe _hook");
2081
+ });
2082
+ });
2083
+ return hasPostToolUse && hasUserPromptSubmit;
1750
2084
  },
1751
2085
  install() {
1752
2086
  const settings = readSettings();
1753
2087
  const existingHooks = settings.hooks ?? {};
1754
2088
  const existingPostToolUse = existingHooks.PostToolUse ?? [];
2089
+ const existingUserPromptSubmit = existingHooks.UserPromptSubmit ?? [];
1755
2090
  settings.hooks = {
1756
2091
  ...existingHooks,
1757
2092
  PostToolUse: [
@@ -1760,10 +2095,21 @@ var claudeCodeIntegration = {
1760
2095
  matcher: "Bash",
1761
2096
  hooks: [{ type: "command", command: "workscribe _hook --source claude-code", async: true }]
1762
2097
  }
2098
+ ],
2099
+ UserPromptSubmit: [
2100
+ ...existingUserPromptSubmit,
2101
+ {
2102
+ hooks: [
2103
+ {
2104
+ type: "command",
2105
+ command: "workscribe _hook --source claude-code --event UserPromptSubmit"
2106
+ }
2107
+ ]
2108
+ }
1763
2109
  ]
1764
2110
  };
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));
2111
+ import_fs13.default.mkdirSync(import_path9.default.dirname(CLAUDE_SETTINGS_PATH), { recursive: true });
2112
+ import_fs13.default.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
1767
2113
  }
1768
2114
  };
1769
2115
 
@@ -1794,11 +2140,11 @@ function registerCompletions(program2) {
1794
2140
  // src/cli/init.ts
1795
2141
  function detectShell() {
1796
2142
  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") };
2143
+ if (shell.includes("zsh")) return { name: "zsh", rcFile: import_path10.default.join(import_os5.default.homedir(), ".zshrc") };
2144
+ if (shell.includes("bash")) return { name: "bash", rcFile: import_path10.default.join(import_os5.default.homedir(), ".bashrc") };
1799
2145
  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") };
2146
+ const confDir = import_path10.default.join(import_os5.default.homedir(), ".config", "fish", "conf.d");
2147
+ return { name: "fish", rcFile: import_path10.default.join(confDir, "workscribe.fish") };
1802
2148
  }
1803
2149
  return null;
1804
2150
  }
@@ -1812,19 +2158,19 @@ function registerInit(program2) {
1812
2158
  log.plain(import_chalk2.default.bold("\n Workscribe \u2014 Developer Work Journal\n"));
1813
2159
  step(1, TOTAL_STEPS, "Creating data directories");
1814
2160
  for (const dir of REQUIRED_DIRS) {
1815
- if (!import_fs13.default.existsSync(dir)) {
1816
- import_fs13.default.mkdirSync(dir, { recursive: true, mode: 448 });
2161
+ if (!import_fs14.default.existsSync(dir)) {
2162
+ import_fs14.default.mkdirSync(dir, { recursive: true, mode: 448 });
1817
2163
  log.success(`Created ${dir}`);
1818
2164
  } else {
1819
2165
  try {
1820
- import_fs13.default.chmodSync(dir, 448);
2166
+ import_fs14.default.chmodSync(dir, 448);
1821
2167
  } catch {
1822
2168
  }
1823
2169
  log.info(`${dir} already exists`);
1824
2170
  }
1825
2171
  }
1826
- if (!import_fs13.default.existsSync(PROFILES_DIR)) {
1827
- import_fs13.default.mkdirSync(PROFILES_DIR, { recursive: true, mode: 448 });
2172
+ if (!import_fs14.default.existsSync(PROFILES_DIR)) {
2173
+ import_fs14.default.mkdirSync(PROFILES_DIR, { recursive: true, mode: 448 });
1828
2174
  }
1829
2175
  step(2, TOTAL_STEPS, "Initialising database");
1830
2176
  getDb();
@@ -1837,16 +2183,18 @@ function registerInit(program2) {
1837
2183
  ` 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
2184
  );
1839
2185
  } else {
1840
- const hookFile = import_path9.default.join(__dirname, "..", "shell", `hook.${shell.name}`);
1841
- if (!import_fs13.default.existsSync(hookFile)) {
2186
+ const hookFile = import_path10.default.join(__dirname, "..", "shell", `hook.${shell.name}`);
2187
+ if (!import_fs14.default.existsSync(hookFile)) {
1842
2188
  log.warn(`Hook template not found at ${hookFile}`);
1843
2189
  } else {
1844
- const hookContent = import_fs13.default.readFileSync(hookFile, "utf-8");
2190
+ const hookContent = import_fs14.default.readFileSync(hookFile, "utf-8");
2191
+ const binaryPath = resolveWorkscribeBinary();
2192
+ const hookWithPath = binaryPath ? injectBinaryPath(hookContent, binaryPath) : hookContent;
1845
2193
  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 });
2194
+ const confDir = import_path10.default.dirname(shell.rcFile);
2195
+ if (!import_fs14.default.existsSync(confDir)) import_fs14.default.mkdirSync(confDir, { recursive: true });
1848
2196
  }
1849
- if (!import_fs13.default.existsSync(shell.rcFile)) import_fs13.default.writeFileSync(shell.rcFile, "");
2197
+ if (!import_fs14.default.existsSync(shell.rcFile)) import_fs14.default.writeFileSync(shell.rcFile, "");
1850
2198
  if (!isHookInstalled(shell.rcFile)) {
1851
2199
  log.plain("");
1852
2200
  log.plain(import_chalk2.default.dim(" Workscribe adds a lightweight hook to your shell that captures each"));
@@ -1854,9 +2202,9 @@ function registerInit(program2) {
1854
2202
  log.plain(import_chalk2.default.dim(" It runs in the background and adds no latency to your terminal."));
1855
2203
  log.plain(import_chalk2.default.dim(" Credentials and secrets are redacted before anything is stored."));
1856
2204
  log.plain("");
1857
- installHook(shell.rcFile, hookContent);
2205
+ installHook(shell.rcFile, hookWithPath);
1858
2206
  log.success(`Shell hook installed in ${shell.rcFile}`);
1859
- } else if (isHookOutdated(shell.rcFile, hookContent)) {
2207
+ } else if (isHookOutdated(shell.rcFile, hookWithPath)) {
1860
2208
  const { update } = await (0, import_prompts.default)({
1861
2209
  type: "confirm",
1862
2210
  name: "update",
@@ -1864,7 +2212,7 @@ function registerInit(program2) {
1864
2212
  initial: true
1865
2213
  });
1866
2214
  if (update) {
1867
- updateHook(shell.rcFile, hookContent);
2215
+ updateHook(shell.rcFile, hookWithPath);
1868
2216
  log.success(`Shell hook updated in ${shell.rcFile}`);
1869
2217
  } else {
1870
2218
  log.info("Shell hook update skipped");
@@ -2020,7 +2368,7 @@ function registerInit(program2) {
2020
2368
  }
2021
2369
  }
2022
2370
  }
2023
- if (!import_fs13.default.existsSync(CONFIG_PATH)) {
2371
+ if (!import_fs14.default.existsSync(CONFIG_PATH)) {
2024
2372
  writeConfig(config);
2025
2373
  }
2026
2374
  log.plain("");
@@ -2056,7 +2404,7 @@ function registerInit(program2) {
2056
2404
  }
2057
2405
 
2058
2406
  // src/cli/status.ts
2059
- var import_fs14 = __toESM(require("fs"));
2407
+ var import_fs15 = __toESM(require("fs"));
2060
2408
  var import_chalk3 = __toESM(require("chalk"));
2061
2409
  function registerStatus(program2) {
2062
2410
  program2.command("status").description("Show capture status and event counts").action(() => {
@@ -2064,7 +2412,7 @@ function registerStatus(program2) {
2064
2412
  const total = getEventCount();
2065
2413
  const todayEvents = getEventsForDate(todayDate());
2066
2414
  const last = getLastEvent();
2067
- const isPaused = import_fs14.default.existsSync(PAUSE_FLAG_PATH);
2415
+ const isPaused = import_fs15.default.existsSync(PAUSE_FLAG_PATH);
2068
2416
  log.plain("");
2069
2417
  log.plain(import_chalk3.default.bold(" Workscribe Status"));
2070
2418
  log.plain("");
@@ -2087,6 +2435,13 @@ function registerStatus(program2) {
2087
2435
  log.plain(
2088
2436
  ` Profiles ${activeProfiles.length > 0 ? import_chalk3.default.cyan(activeProfiles.join(", ")) : import_chalk3.default.dim("none")}`
2089
2437
  );
2438
+ const pendingAlerts = getPendingAlerts();
2439
+ if (pendingAlerts.length > 0) {
2440
+ log.plain("");
2441
+ log.plain(
2442
+ ` ${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)")
2443
+ );
2444
+ }
2090
2445
  const { total: weekTotal, otherCount } = getRecentOtherRate(7);
2091
2446
  if (weekTotal > 0) {
2092
2447
  const otherPct = Math.round(otherCount / weekTotal * 100);
@@ -2180,13 +2535,16 @@ function registerEvents(program2) {
2180
2535
  var import_chalk5 = __toESM(require("chalk"));
2181
2536
 
2182
2537
  // src/sessions/grouper.ts
2538
+ var LONG_RUNNING_THRESHOLD_MS = 5 * 60 * 1e3;
2183
2539
  function splitByIdleGap(events, idleMs) {
2184
2540
  if (events.length === 0) return [];
2185
2541
  const groups = [[events[0]]];
2186
2542
  for (let i = 1; i < events.length; i++) {
2187
2543
  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) {
2544
+ const currCompleted = new Date(events[i].captured_at).getTime();
2545
+ const currDuration = events[i].duration_ms ?? 0;
2546
+ const currEffective = currDuration >= LONG_RUNNING_THRESHOLD_MS ? currCompleted - currDuration : currCompleted;
2547
+ if (currEffective - prev >= idleMs) {
2190
2548
  groups.push([events[i]]);
2191
2549
  } else {
2192
2550
  groups[groups.length - 1].push(events[i]);
@@ -2194,13 +2552,17 @@ function splitByIdleGap(events, idleMs) {
2194
2552
  }
2195
2553
  return groups;
2196
2554
  }
2197
- function splitByRepo(events) {
2555
+ var REPO_SWITCH_GAP_MS = 10 * 60 * 1e3;
2556
+ function splitByRepo(events, gapMs = 0) {
2198
2557
  if (events.length === 0) return [];
2199
2558
  const groups = [[events[0]]];
2200
2559
  for (let i = 1; i < events.length; i++) {
2201
2560
  const prevRepo = events[i - 1].repo_name;
2202
2561
  const currRepo = events[i].repo_name;
2203
- if (currRepo !== prevRepo) {
2562
+ const prevTime = new Date(events[i - 1].captured_at).getTime();
2563
+ const currTime = new Date(events[i].captured_at).getTime();
2564
+ const gap = currTime - prevTime;
2565
+ if (currRepo !== prevRepo && gap >= gapMs) {
2204
2566
  groups.push([events[i]]);
2205
2567
  } else {
2206
2568
  groups[groups.length - 1].push(events[i]);
@@ -2245,7 +2607,7 @@ function buildSessions(date, events, savedSummaries = /* @__PURE__ */ new Map())
2245
2607
  const timeGroups = splitByIdleGap(events, idleMs);
2246
2608
  for (const group of timeGroups) {
2247
2609
  if (group.length === 0) continue;
2248
- const repoGroups = splitByRepo(group);
2610
+ const repoGroups = splitByRepo(group, REPO_SWITCH_GAP_MS);
2249
2611
  for (const repoGroup of repoGroups) {
2250
2612
  if (repoGroup.length === 0) continue;
2251
2613
  const first = repoGroup[0];
@@ -2324,9 +2686,9 @@ function registerSessions(program2) {
2324
2686
 
2325
2687
  // src/cli/summary.ts
2326
2688
  var import_chalk6 = __toESM(require("chalk"));
2327
- var import_fs15 = __toESM(require("fs"));
2689
+ var import_fs16 = __toESM(require("fs"));
2328
2690
  var import_os6 = __toESM(require("os"));
2329
- var import_path10 = __toESM(require("path"));
2691
+ var import_path11 = __toESM(require("path"));
2330
2692
 
2331
2693
  // src/integrations/webhook.ts
2332
2694
  async function postToWebhook(url, text) {
@@ -2431,9 +2793,9 @@ function registerSummary(program2) {
2431
2793
  return;
2432
2794
  }
2433
2795
  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");
2796
+ const cacheFile = import_path11.default.join(WEEKLY_CACHE_DIR, `${cacheKey}.md`);
2797
+ if (!opts.fresh && import_fs16.default.existsSync(cacheFile)) {
2798
+ const cached = import_fs16.default.readFileSync(cacheFile, "utf-8");
2437
2799
  log.plain("");
2438
2800
  log.plain(cached);
2439
2801
  log.plain("");
@@ -2449,8 +2811,8 @@ function registerSummary(program2) {
2449
2811
  log.plain("");
2450
2812
  log.plain(summary);
2451
2813
  log.plain("");
2452
- import_fs15.default.mkdirSync(WEEKLY_CACHE_DIR, { recursive: true });
2453
- import_fs15.default.writeFileSync(cacheFile, summary, { encoding: "utf-8", mode: 384 });
2814
+ import_fs16.default.mkdirSync(WEEKLY_CACHE_DIR, { recursive: true });
2815
+ import_fs16.default.writeFileSync(cacheFile, summary, { encoding: "utf-8", mode: 384 });
2454
2816
  await handleWebhook(opts, config, summary);
2455
2817
  } catch (error) {
2456
2818
  log.error(`Weekly summary failed [${config.ai.provider}]: ${error.message}`);
@@ -2568,8 +2930,8 @@ ${bullets.trim()}`;
2568
2930
  }
2569
2931
 
2570
2932
  // src/cli/export.ts
2571
- var import_fs16 = __toESM(require("fs"));
2572
- var import_path11 = __toESM(require("path"));
2933
+ var import_fs17 = __toESM(require("fs"));
2934
+ var import_path12 = __toESM(require("path"));
2573
2935
  function eventRowToPayload2(row) {
2574
2936
  let metadata = {};
2575
2937
  try {
@@ -2625,9 +2987,9 @@ function registerExport(program2) {
2625
2987
  const payload = { date, sessions: sessionPayloads };
2626
2988
  log.info(`Generating summary via ${config.ai.provider}\u2026`);
2627
2989
  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}
2990
+ const outputPath = opts.output ?? import_path12.default.join(EXPORTS_DIR, `workscribe-${date}.md`);
2991
+ if (!import_fs17.default.existsSync(EXPORTS_DIR)) import_fs17.default.mkdirSync(EXPORTS_DIR, { recursive: true, mode: 448 });
2992
+ import_fs17.default.writeFileSync(outputPath, `# Work Summary \u2014 ${date}
2631
2993
 
2632
2994
  ${summary}
2633
2995
  `, { encoding: "utf-8", mode: 384 });
@@ -2699,22 +3061,22 @@ function printConfig(obj, indent) {
2699
3061
  }
2700
3062
 
2701
3063
  // src/cli/pause.ts
2702
- var import_fs17 = __toESM(require("fs"));
3064
+ var import_fs18 = __toESM(require("fs"));
2703
3065
  function registerPause(program2) {
2704
3066
  program2.command("pause").description("Pause capture temporarily").action(() => {
2705
- if (import_fs17.default.existsSync(PAUSE_FLAG_PATH)) {
3067
+ if (import_fs18.default.existsSync(PAUSE_FLAG_PATH)) {
2706
3068
  log.info("Capture is already paused. Run workscribe resume to restart.");
2707
3069
  return;
2708
3070
  }
2709
- import_fs17.default.writeFileSync(PAUSE_FLAG_PATH, "", { mode: 384 });
3071
+ import_fs18.default.writeFileSync(PAUSE_FLAG_PATH, "", { mode: 384 });
2710
3072
  log.success("Capture paused. Run workscribe resume to restart.");
2711
3073
  });
2712
3074
  program2.command("resume").description("Resume capture after a pause").action(() => {
2713
- if (!import_fs17.default.existsSync(PAUSE_FLAG_PATH)) {
3075
+ if (!import_fs18.default.existsSync(PAUSE_FLAG_PATH)) {
2714
3076
  log.info("Capture is not paused.");
2715
3077
  return;
2716
3078
  }
2717
- import_fs17.default.rmSync(PAUSE_FLAG_PATH);
3079
+ import_fs18.default.rmSync(PAUSE_FLAG_PATH);
2718
3080
  log.success("Capture resumed.");
2719
3081
  });
2720
3082
  }
@@ -2799,19 +3161,19 @@ function registerClear(program2) {
2799
3161
  }
2800
3162
 
2801
3163
  // src/cli/uninstall.ts
2802
- var import_fs18 = __toESM(require("fs"));
2803
- var import_path12 = __toESM(require("path"));
3164
+ var import_fs19 = __toESM(require("fs"));
3165
+ var import_path13 = __toESM(require("path"));
2804
3166
  var import_os7 = __toESM(require("os"));
2805
3167
  var import_prompts3 = __toESM(require("prompts"));
2806
3168
  var import_chalk9 = __toESM(require("chalk"));
2807
3169
  function detectShellRcFiles() {
2808
3170
  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") }
3171
+ { name: "zsh", rcFile: import_path13.default.join(import_os7.default.homedir(), ".zshrc") },
3172
+ { name: "bash", rcFile: import_path13.default.join(import_os7.default.homedir(), ".bashrc") },
3173
+ { name: "bash", rcFile: import_path13.default.join(import_os7.default.homedir(), ".bash_profile") },
3174
+ { name: "fish", rcFile: import_path13.default.join(import_os7.default.homedir(), ".config", "fish", "conf.d", "workscribe.fish") }
2813
3175
  ];
2814
- return candidates.filter((candidate) => import_fs18.default.existsSync(candidate.rcFile) && isHookInstalled(candidate.rcFile));
3176
+ return candidates.filter((candidate) => import_fs19.default.existsSync(candidate.rcFile) && isHookInstalled(candidate.rcFile));
2815
3177
  }
2816
3178
  function registerUninstall(program2) {
2817
3179
  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 +3184,7 @@ function registerUninstall(program2) {
2822
3184
  } else {
2823
3185
  for (const { name, rcFile } of hooked) {
2824
3186
  if (name === "fish") {
2825
- import_fs18.default.unlinkSync(rcFile);
3187
+ import_fs19.default.unlinkSync(rcFile);
2826
3188
  log.success(`Shell hook removed: ${rcFile}`);
2827
3189
  log.plain(` ${import_chalk9.default.dim("Restart your terminal to take effect")}`);
2828
3190
  } else {
@@ -2836,7 +3198,7 @@ function registerUninstall(program2) {
2836
3198
  }
2837
3199
  }
2838
3200
  if (opts.hooksOnly) return;
2839
- const existingDirs = [...new Set(REQUIRED_DIRS)].filter((dir) => import_fs18.default.existsSync(dir));
3201
+ const existingDirs = [...new Set(REQUIRED_DIRS)].filter((dir) => import_fs19.default.existsSync(dir));
2840
3202
  if (existingDirs.length > 0) {
2841
3203
  log.plain("");
2842
3204
  const { deleteData } = await (0, import_prompts3.default)({
@@ -2847,7 +3209,7 @@ function registerUninstall(program2) {
2847
3209
  });
2848
3210
  if (deleteData) {
2849
3211
  for (const dir of existingDirs) {
2850
- import_fs18.default.rmSync(dir, { recursive: true, force: true });
3212
+ import_fs19.default.rmSync(dir, { recursive: true, force: true });
2851
3213
  log.success(`Deleted ${dir}`);
2852
3214
  }
2853
3215
  } else {
@@ -2861,8 +3223,8 @@ function registerUninstall(program2) {
2861
3223
  }
2862
3224
 
2863
3225
  // src/cli/profile.ts
2864
- var import_fs19 = __toESM(require("fs"));
2865
- var import_path13 = __toESM(require("path"));
3226
+ var import_fs20 = __toESM(require("fs"));
3227
+ var import_path14 = __toESM(require("path"));
2866
3228
  var import_chalk10 = __toESM(require("chalk"));
2867
3229
  var MIN_SUGGEST_COUNT = 5;
2868
3230
  function registerProfile(program2) {
@@ -2908,7 +3270,7 @@ function registerProfile(program2) {
2908
3270
  log.plain(import_chalk10.default.dim(" " + "\u2500".repeat(60)));
2909
3271
  for (const entry of entries) {
2910
3272
  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") : "";
3273
+ const installed = import_fs20.default.existsSync(import_path14.default.join(PROFILES_DIR, `${entry.name}.json`)) ? import_chalk10.default.dim(" installed") : "";
2912
3274
  log.plain(` ${import_chalk10.default.cyan(entry.name.padEnd(18))} ${entry.displayName.padEnd(28)}${badge}${installed}`);
2913
3275
  if (entry.description) log.plain(` ${import_chalk10.default.dim(" ".repeat(18) + entry.description)}`);
2914
3276
  }
@@ -2944,8 +3306,8 @@ function registerProfile(program2) {
2944
3306
  continue;
2945
3307
  }
2946
3308
  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);
3309
+ const cachePath = import_path14.default.join(PROFILES_DIR, `${name}.json`);
3310
+ if (import_fs20.default.existsSync(cachePath)) import_fs20.default.unlinkSync(cachePath);
2949
3311
  log.success(`Profile "${name}" removed`);
2950
3312
  }
2951
3313
  config.capture.profiles = [...activeNames];
@@ -3086,7 +3448,9 @@ function registerNote(program2) {
3086
3448
  repo_name: repoName,
3087
3449
  branch,
3088
3450
  category: "note",
3089
- metadata: JSON.stringify({ text })
3451
+ metadata: JSON.stringify({ text }),
3452
+ source: null,
3453
+ file_path: null
3090
3454
  });
3091
3455
  log.success(`Note saved${repoName ? ` to ${repoName}` : ""}`);
3092
3456
  });
@@ -3094,8 +3458,8 @@ function registerNote(program2) {
3094
3458
 
3095
3459
  // src/cli/week.ts
3096
3460
  var import_chalk11 = __toESM(require("chalk"));
3097
- var import_fs20 = __toESM(require("fs"));
3098
- var import_path14 = __toESM(require("path"));
3461
+ var import_fs21 = __toESM(require("fs"));
3462
+ var import_path15 = __toESM(require("path"));
3099
3463
  var import_os8 = __toESM(require("os"));
3100
3464
  function eventRowToPayload3(row) {
3101
3465
  let metadata = {};
@@ -3168,9 +3532,9 @@ function registerWeek(program2) {
3168
3532
  return;
3169
3533
  }
3170
3534
  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");
3535
+ const cacheFile = import_path15.default.join(WEEKLY_CACHE_DIR, `${cacheKey}.md`);
3536
+ if (!opts.fresh && import_fs21.default.existsSync(cacheFile)) {
3537
+ const cached = import_fs21.default.readFileSync(cacheFile, "utf-8");
3174
3538
  log.plain("");
3175
3539
  log.plain(cached);
3176
3540
  log.plain("");
@@ -3186,8 +3550,8 @@ function registerWeek(program2) {
3186
3550
  log.plain("");
3187
3551
  log.plain(summary);
3188
3552
  log.plain("");
3189
- import_fs20.default.mkdirSync(WEEKLY_CACHE_DIR, { recursive: true });
3190
- import_fs20.default.writeFileSync(cacheFile, summary, { encoding: "utf-8", mode: 384 });
3553
+ import_fs21.default.mkdirSync(WEEKLY_CACHE_DIR, { recursive: true });
3554
+ import_fs21.default.writeFileSync(cacheFile, summary, { encoding: "utf-8", mode: 384 });
3191
3555
  await postSummary(opts, config, summary);
3192
3556
  } catch (error) {
3193
3557
  log.error(`Weekly digest failed [${config.ai.provider}]: ${error.message}`);
@@ -3197,15 +3561,15 @@ function registerWeek(program2) {
3197
3561
  }
3198
3562
 
3199
3563
  // src/cli/bug-report.ts
3200
- var import_fs21 = __toESM(require("fs"));
3564
+ var import_fs22 = __toESM(require("fs"));
3201
3565
  var import_os9 = __toESM(require("os"));
3202
- var import_path15 = __toESM(require("path"));
3566
+ var import_path16 = __toESM(require("path"));
3203
3567
  var import_chalk12 = __toESM(require("chalk"));
3204
3568
  var GITHUB_ISSUES_URL = "https://github.com/workscribe/workscribe/issues/new";
3205
3569
  var DEBUG_LOG_TAIL_LINES = 100;
3206
3570
  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");
3571
+ if (!import_fs22.default.existsSync(filePath)) return "(no debug log found \u2014 set WORKSCRIBE_DEBUG=1 to enable)";
3572
+ const content = import_fs22.default.readFileSync(filePath, "utf-8");
3209
3573
  const allLines = content.split("\n");
3210
3574
  return allLines.slice(-lines).join("\n");
3211
3575
  }
@@ -3223,9 +3587,9 @@ function redactConfig() {
3223
3587
  function registerBugReport(program2) {
3224
3588
  program2.command("bug-report").description("Generate a diagnostic report for filing a bug").action(() => {
3225
3589
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3226
- const reportPath = import_path15.default.join(WORKSCRIBE_DIR, `bug-report-${timestamp}.txt`);
3590
+ const reportPath = import_path16.default.join(WORKSCRIBE_DIR, `bug-report-${timestamp}.txt`);
3227
3591
  const systemInfo = [
3228
- `Workscribe version : ${"1.12.0"}`,
3592
+ `Workscribe version : ${"1.14.0"}`,
3229
3593
  `Node.js version : ${process.version}`,
3230
3594
  `Platform : ${process.platform}`,
3231
3595
  `Architecture : ${process.arch}`,
@@ -3247,17 +3611,17 @@ function registerBugReport(program2) {
3247
3611
  tailFile(DEBUG_LOG_PATH, DEBUG_LOG_TAIL_LINES)
3248
3612
  ].join("\n");
3249
3613
  try {
3250
- if (!import_fs21.default.existsSync(WORKSCRIBE_DIR)) {
3251
- import_fs21.default.mkdirSync(WORKSCRIBE_DIR, { recursive: true, mode: 448 });
3614
+ if (!import_fs22.default.existsSync(WORKSCRIBE_DIR)) {
3615
+ import_fs22.default.mkdirSync(WORKSCRIBE_DIR, { recursive: true, mode: 448 });
3252
3616
  }
3253
- import_fs21.default.writeFileSync(reportPath, report, { mode: 384 });
3617
+ import_fs22.default.writeFileSync(reportPath, report, { mode: 384 });
3254
3618
  } catch (error) {
3255
3619
  log.error(`Could not write report: ${error.message}`);
3256
3620
  process.exit(1);
3257
3621
  }
3258
3622
  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 -->
3623
+ title: `Bug report \u2014 workscribe ${"1.14.0"}`,
3624
+ body: `<!-- Attach ${import_path16.default.basename(reportPath)} or paste the relevant sections below -->
3261
3625
 
3262
3626
  **Describe the issue:**
3263
3627
 
@@ -3275,19 +3639,142 @@ function registerBugReport(program2) {
3275
3639
  });
3276
3640
  }
3277
3641
 
3642
+ // src/cli/alerts.ts
3643
+ var import_chalk13 = __toESM(require("chalk"));
3644
+ function formatTimestamp(ms) {
3645
+ return new Date(ms).toLocaleString([], {
3646
+ month: "short",
3647
+ day: "numeric",
3648
+ hour: "2-digit",
3649
+ minute: "2-digit"
3650
+ });
3651
+ }
3652
+ function registerAlerts(program2) {
3653
+ 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) => {
3654
+ if (opts.check) {
3655
+ if (hasPendingAlerts()) {
3656
+ log.plain(
3657
+ import_chalk13.default.yellow(" [workscribe] Possible credentials detected in an AI session.") + import_chalk13.default.dim(" Run workscribe alerts for details.")
3658
+ );
3659
+ }
3660
+ return;
3661
+ }
3662
+ const alerts = getPendingAlerts();
3663
+ if (alerts.length === 0) {
3664
+ log.plain("");
3665
+ log.plain(" No pending security alerts.");
3666
+ log.plain("");
3667
+ return;
3668
+ }
3669
+ log.plain("");
3670
+ log.plain(import_chalk13.default.bold(" Security Alerts") + import_chalk13.default.dim(` (${alerts.length} pending)`));
3671
+ log.plain("");
3672
+ for (const alert of alerts) {
3673
+ log.plain(` ${import_chalk13.default.yellow("!")} ${import_chalk13.default.dim(formatTimestamp(alert.created_at))} ${import_chalk13.default.cyan(alert.tool)}`);
3674
+ log.plain(` ${alert.context}`);
3675
+ if (alert.session_id) log.plain(` ${import_chalk13.default.dim("Session: " + alert.session_id)}`);
3676
+ log.plain("");
3677
+ }
3678
+ log.plain(import_chalk13.default.dim(" Run workscribe alerts clear to dismiss all alerts"));
3679
+ log.plain("");
3680
+ });
3681
+ alertsCmd.command("clear").description("Dismiss all pending security alerts").action(() => {
3682
+ const count = clearAlerts();
3683
+ if (count === 0) {
3684
+ log.plain(" No pending alerts to clear.");
3685
+ } else {
3686
+ log.plain(` ${count} alert${count === 1 ? "" : "s"} cleared.`);
3687
+ }
3688
+ });
3689
+ }
3690
+
3691
+ // src/cli/context.ts
3692
+ var import_path17 = __toESM(require("path"));
3693
+ var import_chalk14 = __toESM(require("chalk"));
3694
+ function toContextJson(session) {
3695
+ return {
3696
+ date: session.date,
3697
+ startedAt: session.started_at,
3698
+ endedAt: session.ended_at,
3699
+ timeAgo: timeAgo(session.started_at),
3700
+ durationMinutes: (new Date(session.ended_at).getTime() - new Date(session.started_at).getTime()) / 6e4,
3701
+ branch: getSessionBranch(session.id),
3702
+ eventCount: session.event_count,
3703
+ summary: session.summary
3704
+ };
3705
+ }
3706
+ function printSessionContext(session, heading) {
3707
+ const ago = timeAgo(session.started_at);
3708
+ const branch = getSessionBranch(session.id);
3709
+ const durationMinutes = (new Date(session.ended_at).getTime() - new Date(session.started_at).getTime()) / 6e4;
3710
+ log.plain("");
3711
+ log.plain(` ${import_chalk14.default.bold(heading)}: ${import_chalk14.default.cyan(ago)}`);
3712
+ log.plain("");
3713
+ log.plain(` Date ${session.date}`);
3714
+ log.plain(` Duration ${formatDuration(durationMinutes)}`);
3715
+ if (branch) log.plain(` Branch ${import_chalk14.default.dim(branch)}`);
3716
+ log.plain(` Events ${session.event_count}`);
3717
+ if (session.summary) {
3718
+ log.plain("");
3719
+ for (const line of session.summary.split("\n")) {
3720
+ log.plain(` ${line}`);
3721
+ }
3722
+ } else {
3723
+ log.plain("");
3724
+ log.plain(import_chalk14.default.dim(` No summary \u2014 run: workscribe summary ${session.date}`));
3725
+ }
3726
+ log.plain("");
3727
+ }
3728
+ function registerContext(program2) {
3729
+ program2.command("context").description("Show the last session for the current repo").option("--file <path>", "narrow to last session that touched this file").option("--json", "output as JSON instead of formatted text \u2014 null when no session is found").action((opts) => {
3730
+ if (opts.file) {
3731
+ const normalizedFile = import_path17.default.normalize(opts.file);
3732
+ const session = getLastSessionTouchingFile(normalizedFile);
3733
+ if (!session) {
3734
+ if (opts.json) log.plain(JSON.stringify(null));
3735
+ else log.plain(` No sessions found touching ${normalizedFile}`);
3736
+ return;
3737
+ }
3738
+ if (opts.json) {
3739
+ log.plain(JSON.stringify(toContextJson(session)));
3740
+ return;
3741
+ }
3742
+ printSessionContext(session, `Last session touching ${import_path17.default.basename(normalizedFile)}`);
3743
+ return;
3744
+ }
3745
+ const { repoName } = getGitContext(process.cwd(), null, readConfig().git.qualifyRepoName);
3746
+ if (!repoName) {
3747
+ if (opts.json) log.plain(JSON.stringify(null));
3748
+ else log.plain(" Not in a git repository. Run workscribe context from a project directory.");
3749
+ return;
3750
+ }
3751
+ const sessions = getRecentSessionsByRepo(repoName, 1);
3752
+ if (sessions.length === 0) {
3753
+ if (opts.json) log.plain(JSON.stringify(null));
3754
+ else log.plain(` No sessions found for ${repoName}`);
3755
+ return;
3756
+ }
3757
+ if (opts.json) {
3758
+ log.plain(JSON.stringify(toContextJson(sessions[0])));
3759
+ return;
3760
+ }
3761
+ printSessionContext(sessions[0], `Last session on ${repoName}`);
3762
+ });
3763
+ }
3764
+
3278
3765
  // src/utils/update-check.ts
3279
- var import_fs22 = __toESM(require("fs"));
3766
+ var import_fs23 = __toESM(require("fs"));
3280
3767
  var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
3281
3768
  function readCache() {
3282
3769
  try {
3283
- return JSON.parse(import_fs22.default.readFileSync(UPDATE_CHECK_PATH, "utf-8"));
3770
+ return JSON.parse(import_fs23.default.readFileSync(UPDATE_CHECK_PATH, "utf-8"));
3284
3771
  } catch {
3285
3772
  return null;
3286
3773
  }
3287
3774
  }
3288
3775
  function writeCache(cache) {
3289
3776
  try {
3290
- import_fs22.default.writeFileSync(UPDATE_CHECK_PATH, JSON.stringify(cache), { mode: 384 });
3777
+ import_fs23.default.writeFileSync(UPDATE_CHECK_PATH, JSON.stringify(cache), { mode: 384 });
3291
3778
  } catch {
3292
3779
  }
3293
3780
  }
@@ -3300,7 +3787,7 @@ function isNewer(latest, current) {
3300
3787
  return latestPatch > currentPatch;
3301
3788
  }
3302
3789
  function checkForUpdate() {
3303
- const current = "1.12.0";
3790
+ const current = "1.14.0";
3304
3791
  const cache = readCache();
3305
3792
  if (cache && isNewer(cache.latestVersion, current)) {
3306
3793
  process.stderr.write(
@@ -3325,7 +3812,7 @@ function checkForUpdate() {
3325
3812
  assertNodeVersion(process.versions.node);
3326
3813
  checkForUpdate();
3327
3814
  var program = new import_commander.Command();
3328
- program.name("workscribe").description("Automatic developer work journal").version("1.12.0");
3815
+ program.name("workscribe").description("Automatic developer work journal").version("1.14.0");
3329
3816
  registerCapture(program);
3330
3817
  registerHook(program);
3331
3818
  registerInit(program);
@@ -3344,5 +3831,7 @@ registerNote(program);
3344
3831
  registerWeek(program);
3345
3832
  registerBugReport(program);
3346
3833
  registerCompletions(program);
3834
+ registerAlerts(program);
3835
+ registerContext(program);
3347
3836
  program.parse();
3348
3837
  //# sourceMappingURL=index.js.map