@workscribe/cli 1.3.0 → 1.5.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
@@ -173,6 +173,12 @@ function deleteEventsByIds(ids) {
173
173
  clearSessionsForDate(date);
174
174
  }
175
175
  }
176
+ function deleteEventsForDate(date) {
177
+ const db = getDb();
178
+ clearSessionsForDate(date);
179
+ const result = db.prepare(`DELETE FROM events WHERE date(captured_at, 'localtime') = $date`).run({ date });
180
+ return result.changes;
181
+ }
176
182
  function clearSessionsForDate(date) {
177
183
  const db = getDb();
178
184
  db.prepare(`
@@ -271,52 +277,72 @@ function getGitContext(cwd, nameOverride) {
271
277
  }
272
278
  }
273
279
 
274
- // src/capture/ai-tools.json
280
+ // src/capture/git-snapshot.ts
281
+ var import_child_process2 = require("child_process");
282
+ function getCommitsSince(sha, cwd) {
283
+ if (!sha) return [];
284
+ try {
285
+ const out = (0, import_child_process2.execSync)(`git log ${sha}..HEAD --format=%s`, { cwd, timeout: 3e3 }).toString().trim();
286
+ return out ? out.split("\n").filter(Boolean) : [];
287
+ } catch {
288
+ return [];
289
+ }
290
+ }
291
+
292
+ // src/config/ai-tools.json
275
293
  var ai_tools_default = [
276
294
  {
277
295
  tool: "claude",
296
+ displayName: "Claude Code",
278
297
  pattern: "^claude\\b",
279
298
  hook: "claude-code",
280
299
  contextSource: "hook"
281
300
  },
282
301
  {
283
302
  tool: "aider",
303
+ displayName: "Aider",
284
304
  pattern: "^aider\\b",
285
305
  hook: null,
286
306
  contextSource: "history-file"
287
307
  },
288
308
  {
289
309
  tool: "gemini",
310
+ displayName: "Gemini",
290
311
  pattern: "^gemini\\b",
291
312
  hook: null,
292
313
  contextSource: null
293
314
  },
294
315
  {
295
316
  tool: "gh copilot",
317
+ displayName: "GitHub Copilot",
296
318
  pattern: "^gh\\s+copilot\\b",
297
319
  hook: null,
298
320
  contextSource: null
299
321
  },
300
322
  {
301
323
  tool: "cody",
324
+ displayName: "Cody",
302
325
  pattern: "^cody\\b",
303
326
  hook: null,
304
327
  contextSource: null
305
328
  },
306
329
  {
307
330
  tool: "amazon-q",
331
+ displayName: "Amazon Q",
308
332
  pattern: "^q\\s+(chat|dev)\\b",
309
333
  hook: null,
310
334
  contextSource: null
311
335
  },
312
336
  {
313
337
  tool: "continue",
338
+ displayName: "Continue",
314
339
  pattern: "^continue\\b",
315
340
  hook: null,
316
341
  contextSource: null
317
342
  },
318
343
  {
319
344
  tool: "cursor",
345
+ displayName: "Cursor",
320
346
  pattern: "^cursor\\b",
321
347
  hook: null,
322
348
  contextSource: null
@@ -378,6 +404,10 @@ var RULES = [
378
404
  tool: m[1],
379
405
  packages: extractPackages(cmd, "add")
380
406
  })),
407
+ makeRule(/^bun\s+(install|add)\b/, "dependency_install", (_m, cmd) => ({
408
+ tool: "bun",
409
+ packages: extractPackages(cmd, "add")
410
+ })),
381
411
  makeRule(/^pip\s+(install)\b/, "dependency_install", (_m, cmd) => ({
382
412
  tool: "pip",
383
413
  packages: extractPackages(cmd, "install")
@@ -403,12 +433,19 @@ var RULES = [
403
433
  tool: m[1],
404
434
  packages: extractPackages(cmd, "remove")
405
435
  })),
436
+ makeRule(/^bun\s+remove\b/, "dependency_remove", (_m, cmd) => ({
437
+ tool: "bun",
438
+ packages: extractPackages(cmd, "remove")
439
+ })),
406
440
  makeRule(/^pip\s+uninstall\b/, "dependency_remove", (_m, cmd) => ({
407
441
  tool: "pip",
408
442
  packages: extractPackages(cmd, "uninstall")
409
443
  })),
410
444
  // ─── Tests ────────────────────────────────────────────────────────────────
411
- makeRule(/^(npm\s+(test|run\s+test)|yarn\s+test|pnpm\s+test)\b/, "test_run", () => ({ tool: "npm" })),
445
+ makeRule(/^npm\s+(test|run\s+test)\b/, "test_run", () => ({ tool: "npm" })),
446
+ makeRule(/^yarn\s+test\b/, "test_run", () => ({ tool: "yarn" })),
447
+ makeRule(/^pnpm\s+test\b/, "test_run", () => ({ tool: "pnpm" })),
448
+ makeRule(/^bun\s+test\b/, "test_run", () => ({ tool: "bun" })),
412
449
  makeRule(/^(jest|vitest)\b/, "test_run", (m) => ({ tool: m[1] })),
413
450
  makeRule(/^pytest\b/, "test_run", () => ({ tool: "pytest" })),
414
451
  makeRule(/^go\s+test\b/, "test_run", () => ({ tool: "go" })),
@@ -418,6 +455,7 @@ var RULES = [
418
455
  // ─── Build ────────────────────────────────────────────────────────────────
419
456
  makeRule(/^npm\s+run\s+build\b/, "build", () => ({ tool: "npm" })),
420
457
  makeRule(/^(yarn|pnpm)\s+build\b/, "build", (m) => ({ tool: m[1] })),
458
+ makeRule(/^bun\s+run\s+build\b/, "build", () => ({ tool: "bun" })),
421
459
  makeRule(/^(make|gmake)\b/, "build", (m) => ({ tool: m[1] })),
422
460
  makeRule(/^cargo\s+build\b/, "build", () => ({ tool: "cargo" })),
423
461
  makeRule(/^go\s+build\b/, "build", () => ({ tool: "go" })),
@@ -429,6 +467,10 @@ var RULES = [
429
467
  script: cmd.split(/\s+/)[2]
430
468
  })),
431
469
  makeRule(/^(yarn|pnpm)\s+(dev|start)\b/, "server_run", (m) => ({ tool: m[1], script: m[2] })),
470
+ makeRule(/^bun\s+run\s+(dev|start|serve)\b/, "server_run", (_m, cmd) => ({
471
+ tool: "bun",
472
+ script: cmd.split(/\s+/)[2]
473
+ })),
432
474
  makeRule(/^python\s+.*manage\.py\s+runserver\b/, "server_run", () => ({ tool: "django" })),
433
475
  // ─── Infrastructure ───────────────────────────────────────────────────────
434
476
  makeRule(/^ssh\b/, "infrastructure", (_m, cmd) => {
@@ -461,18 +503,36 @@ var RULES = [
461
503
  return { variable: varName };
462
504
  }),
463
505
  makeRule(/^source\s+/, "environment", () => ({})),
464
- // ─── npm scripts (generic) ────────────────────────────────────────────────
506
+ // ─── generic run scripts ──────────────────────────────────────────────────
465
507
  makeRule(/^npm\s+run\s+\S+/, "script_run", (_m, cmd) => ({
466
508
  tool: "npm",
467
509
  script: cmd.split(/\s+/)[2]
468
510
  })),
511
+ makeRule(/^bun\s+run\s+\S+/, "script_run", (_m, cmd) => ({
512
+ tool: "bun",
513
+ script: cmd.split(/\s+/)[2]
514
+ })),
515
+ // ─── Code search ─────────────────────────────────────────────────────────
516
+ makeRule(/^(grep|egrep|fgrep)\b/, "code_search", (m) => ({ tool: m[1] })),
517
+ makeRule(/^(rg|ag|ack|ripgrep)\b/, "code_search", (m) => ({ tool: m[1] })),
518
+ makeRule(/^find\s/, "code_search", () => ({ tool: "find" })),
519
+ // ─── Code read ────────────────────────────────────────────────────────────
520
+ makeRule(/^(cat|bat|less|more)\s+\S+/, "code_read", (m) => ({ tool: m[1] })),
521
+ makeRule(/^(head|tail)\b/, "code_read", (m) => ({ tool: m[1] })),
522
+ // ─── File operations ──────────────────────────────────────────────────────
523
+ makeRule(/^(cp|rsync)\b/, "file_ops", (m) => ({ tool: m[1] })),
524
+ makeRule(/^(mv)\b/, "file_ops", () => ({ tool: "mv" })),
525
+ makeRule(/^(rm|rmdir)\b/, "file_ops", (m) => ({ tool: m[1] })),
526
+ makeRule(/^(mkdir|touch|ln)\b/, "file_ops", (m) => ({ tool: m[1] })),
527
+ makeRule(/^(chmod|chown|chgrp)\b/, "file_ops", (m) => ({ tool: m[1] })),
469
528
  // ─── AI terminal tools (registry-driven) ─────────────────────────────────
470
529
  ...ai_tools_default.map(
471
530
  (entry) => makeRule(new RegExp(entry.pattern), "ai_session", () => ({ tool: entry.tool }))
472
531
  )
473
532
  ];
474
533
  function normalizeCommand(command) {
475
- const trimmed = command.trim().replace(/\s+/g, " ");
534
+ let trimmed = command.trim().replace(/\s+/g, " ");
535
+ trimmed = trimmed.replace(/^(git)\s+-C\s+\S+\s+/, "$1 ");
476
536
  for (const rule of RULES) {
477
537
  const result = rule.match(trimmed);
478
538
  if (result) return result;
@@ -563,12 +623,14 @@ function deepMerge(defaults, overrides) {
563
623
  function normalizeForFilter(cmd) {
564
624
  return cmd.trim().replace(/\s+/g, " ");
565
625
  }
626
+ var BUILTIN_IGNORE = /* @__PURE__ */ new Set(["workscribe"]);
566
627
  function shouldFilter(command, ignoreList) {
567
628
  const normalized = normalizeForFilter(command);
568
629
  if (!normalized) return true;
630
+ const baseCmd = normalized.split(" ")[0];
631
+ if (BUILTIN_IGNORE.has(baseCmd)) return true;
569
632
  const normalizedIgnore = ignoreList.map(normalizeForFilter);
570
633
  if (normalizedIgnore.includes(normalized)) return true;
571
- const baseCmd = normalized.split(" ")[0];
572
634
  if (normalizedIgnore.includes(baseCmd)) return true;
573
635
  return false;
574
636
  }
@@ -590,6 +652,10 @@ function captureCommand(opts) {
590
652
  const nameOverride = config.projects?.[cwd] ?? null;
591
653
  const { repoName, branch } = getGitContext(cwd, nameOverride);
592
654
  const { category, metadata } = normalizeCommand(safeCommand);
655
+ if (category === "ai_session" && opts.gitBefore) {
656
+ const commits = getCommitsSince(opts.gitBefore, cwd);
657
+ if (commits.length > 0) metadata.commits = commits;
658
+ }
593
659
  insertEvent({
594
660
  captured_at: (/* @__PURE__ */ new Date()).toISOString(),
595
661
  command: safeCommand,
@@ -626,14 +692,15 @@ function debugLog(context, error) {
626
692
 
627
693
  // src/cli/capture.ts
628
694
  function registerCapture(program2) {
629
- program2.command("_capture").description("Internal: capture a shell command event (called by shell hook)").option("--cmd <command>", "command that was executed").option("--cwd <path>", "working directory").option("--exit <code>", "exit code", parseInt).option("--duration <ms>", "duration in milliseconds", parseInt).action((opts) => {
695
+ 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) => {
630
696
  try {
631
697
  if (!opts.cmd || !opts.cwd) return;
632
698
  captureCommand({
633
699
  command: opts.cmd,
634
700
  cwd: opts.cwd,
635
701
  exitCode: opts.exit ?? null,
636
- durationMs: opts.duration ?? null
702
+ durationMs: opts.duration ?? null,
703
+ gitBefore: opts.gitBefore ?? void 0
637
704
  });
638
705
  } catch (err) {
639
706
  debugLog("_capture", err);
@@ -641,22 +708,62 @@ function registerCapture(program2) {
641
708
  });
642
709
  }
643
710
 
711
+ // src/cli/hook.ts
712
+ function parseHookPayload(raw) {
713
+ if (!raw.trim()) return null;
714
+ const payload = JSON.parse(raw);
715
+ const toolInput = payload.tool_input;
716
+ const command = typeof toolInput?.command === "string" ? toolInput.command : null;
717
+ const cwd = typeof payload.cwd === "string" ? payload.cwd : null;
718
+ if (!command || !cwd) return null;
719
+ return { command, cwd };
720
+ }
721
+ function registerHook(program2) {
722
+ 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(() => {
723
+ let raw = "";
724
+ process.stdin.setEncoding("utf-8");
725
+ process.stdin.on("data", (chunk) => {
726
+ raw += chunk;
727
+ });
728
+ process.stdin.on("end", () => {
729
+ try {
730
+ const parsed = parseHookPayload(raw);
731
+ if (!parsed) return;
732
+ captureCommand({ command: parsed.command, cwd: parsed.cwd, exitCode: null, durationMs: null });
733
+ } catch (err) {
734
+ debugLog("_hook", err);
735
+ }
736
+ });
737
+ });
738
+ }
739
+
644
740
  // src/cli/init.ts
645
- var import_fs6 = __toESM(require("fs"));
646
- var import_path3 = __toESM(require("path"));
647
- var import_os2 = __toESM(require("os"));
741
+ var import_fs8 = __toESM(require("fs"));
742
+ var import_path5 = __toESM(require("path"));
743
+ var import_os3 = __toESM(require("os"));
648
744
  var import_prompts = __toESM(require("prompts"));
649
745
  var import_chalk2 = __toESM(require("chalk"));
746
+
747
+ // src/capture/shell-hook.ts
748
+ var import_fs6 = __toESM(require("fs"));
749
+ var import_path3 = __toESM(require("path"));
650
750
  var HOOK_START = "# workscribe:hook:start";
651
- function detectShell() {
652
- const shell = process.env.SHELL ?? "";
653
- if (shell.includes("zsh")) return { name: "zsh", rcFile: import_path3.default.join(import_os2.default.homedir(), ".zshrc") };
654
- if (shell.includes("bash")) return { name: "bash", rcFile: import_path3.default.join(import_os2.default.homedir(), ".bashrc") };
655
- return null;
751
+ var HOOK_END = "# workscribe:hook:end";
752
+ function getInstalledHookBlock(rcFile) {
753
+ if (!import_fs6.default.existsSync(rcFile)) return null;
754
+ const content = import_fs6.default.readFileSync(rcFile, "utf-8");
755
+ const startIdx = content.indexOf(HOOK_START);
756
+ const endIdx = content.indexOf(HOOK_END);
757
+ if (startIdx === -1 || endIdx === -1) return null;
758
+ return content.slice(startIdx, endIdx + HOOK_END.length);
656
759
  }
657
760
  function isHookInstalled(rcFile) {
658
- if (!import_fs6.default.existsSync(rcFile)) return false;
659
- return import_fs6.default.readFileSync(rcFile, "utf-8").includes(HOOK_START);
761
+ return getInstalledHookBlock(rcFile) !== null;
762
+ }
763
+ function isHookOutdated(rcFile, hookContent) {
764
+ const installed = getInstalledHookBlock(rcFile);
765
+ if (!installed) return false;
766
+ return installed.trim() !== hookContent.trim();
660
767
  }
661
768
  function installHook(rcFile, hookContent) {
662
769
  const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
@@ -666,15 +773,98 @@ function installHook(rcFile, hookContent) {
666
773
  ${hookContent}
667
774
  `);
668
775
  }
776
+ function updateHook(rcFile, hookContent) {
777
+ const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
778
+ import_fs6.default.copyFileSync(rcFile, backup);
779
+ log.info(`Backed up ${rcFile} \u2192 ${import_path3.default.basename(backup)}`);
780
+ const content = import_fs6.default.readFileSync(rcFile, "utf-8");
781
+ const startIdx = content.indexOf(HOOK_START);
782
+ const endIdx = content.indexOf(HOOK_END) + HOOK_END.length;
783
+ import_fs6.default.writeFileSync(rcFile, content.slice(0, startIdx) + hookContent + content.slice(endIdx));
784
+ }
785
+
786
+ // src/capture/hooks/claude-code.ts
787
+ var import_fs7 = __toESM(require("fs"));
788
+ var import_path4 = __toESM(require("path"));
789
+ var import_os2 = __toESM(require("os"));
790
+
791
+ // src/capture/hooks/utils.ts
792
+ var import_child_process3 = require("child_process");
793
+ function isCommandInstalled(cmd) {
794
+ try {
795
+ (0, import_child_process3.execSync)(`which ${cmd}`, { stdio: "ignore" });
796
+ return true;
797
+ } catch {
798
+ return false;
799
+ }
800
+ }
801
+
802
+ // src/capture/hooks/claude-code.ts
803
+ var CLAUDE_SETTINGS_PATH = import_path4.default.join(import_os2.default.homedir(), ".claude", "settings.json");
804
+ function readSettings() {
805
+ if (!import_fs7.default.existsSync(CLAUDE_SETTINGS_PATH)) return {};
806
+ try {
807
+ return JSON.parse(import_fs7.default.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8"));
808
+ } catch {
809
+ return {};
810
+ }
811
+ }
812
+ var claudeCodeIntegration = {
813
+ displayName: "Claude Code",
814
+ isInstalled() {
815
+ return isCommandInstalled("claude");
816
+ },
817
+ isHookInstalled() {
818
+ const settings = readSettings();
819
+ const postToolUse = settings.hooks?.PostToolUse ?? [];
820
+ return postToolUse.some((entry) => {
821
+ const hooks = entry.hooks ?? [];
822
+ return hooks.some((h) => {
823
+ const hook = h;
824
+ return typeof hook.command === "string" && hook.command.includes("workscribe _hook");
825
+ });
826
+ });
827
+ },
828
+ install() {
829
+ const settings = readSettings();
830
+ const existingHooks = settings.hooks ?? {};
831
+ const existingPostToolUse = existingHooks.PostToolUse ?? [];
832
+ settings.hooks = {
833
+ ...existingHooks,
834
+ PostToolUse: [
835
+ ...existingPostToolUse,
836
+ {
837
+ matcher: "Bash",
838
+ hooks: [{ type: "command", command: "workscribe _hook --source claude-code", async: true }]
839
+ }
840
+ ]
841
+ };
842
+ import_fs7.default.mkdirSync(import_path4.default.dirname(CLAUDE_SETTINGS_PATH), { recursive: true });
843
+ import_fs7.default.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
844
+ }
845
+ };
846
+
847
+ // src/capture/hooks/index.ts
848
+ var AI_HOOK_INTEGRATIONS = {
849
+ "claude-code": claudeCodeIntegration
850
+ };
851
+
852
+ // src/cli/init.ts
853
+ function detectShell() {
854
+ const shell = process.env.SHELL ?? "";
855
+ if (shell.includes("zsh")) return { name: "zsh", rcFile: import_path5.default.join(import_os3.default.homedir(), ".zshrc") };
856
+ if (shell.includes("bash")) return { name: "bash", rcFile: import_path5.default.join(import_os3.default.homedir(), ".bashrc") };
857
+ return null;
858
+ }
669
859
  function registerInit(program2) {
670
- program2.command("init").description("Install shell hooks and set up Workscribe").action(async () => {
860
+ program2.command("init").description("Set up Workscribe and install or update shell hooks").action(async () => {
671
861
  log.plain(import_chalk2.default.bold("\n Workscribe \u2014 Developer Work Journal\n"));
672
- if (!import_fs6.default.existsSync(WORKSCRIBE_DIR)) {
673
- import_fs6.default.mkdirSync(WORKSCRIBE_DIR, { recursive: true, mode: 448 });
862
+ if (!import_fs8.default.existsSync(WORKSCRIBE_DIR)) {
863
+ import_fs8.default.mkdirSync(WORKSCRIBE_DIR, { recursive: true, mode: 448 });
674
864
  log.success(`Created ${WORKSCRIBE_DIR}`);
675
865
  } else {
676
866
  try {
677
- import_fs6.default.chmodSync(WORKSCRIBE_DIR, 448);
867
+ import_fs8.default.chmodSync(WORKSCRIBE_DIR, 448);
678
868
  } catch {
679
869
  }
680
870
  }
@@ -685,17 +875,48 @@ function registerInit(program2) {
685
875
  log.warn("Could not detect shell. Supported: zsh, bash");
686
876
  log.plain(`Manually add the hook from: ${import_chalk2.default.cyan("shell/hook.zsh")} or ${import_chalk2.default.cyan("shell/hook.bash")}`);
687
877
  } else {
688
- if (isHookInstalled(shell.rcFile)) {
689
- log.info(`Shell hook already installed in ${shell.rcFile}`);
878
+ const hookFile = import_path5.default.join(__dirname, "..", "shell", `hook.${shell.name}`);
879
+ if (!import_fs8.default.existsSync(hookFile)) {
880
+ log.warn(`Hook template not found at ${hookFile}`);
690
881
  } else {
691
- const hookFile = import_path3.default.join(__dirname, "..", "shell", `hook.${shell.name}`);
692
- if (!import_fs6.default.existsSync(hookFile)) {
693
- log.warn(`Hook template not found at ${hookFile}`);
694
- } else {
695
- const hookContent = import_fs6.default.readFileSync(hookFile, "utf-8");
696
- if (!import_fs6.default.existsSync(shell.rcFile)) import_fs6.default.writeFileSync(shell.rcFile, "");
882
+ const hookContent = import_fs8.default.readFileSync(hookFile, "utf-8");
883
+ if (!import_fs8.default.existsSync(shell.rcFile)) import_fs8.default.writeFileSync(shell.rcFile, "");
884
+ if (!isHookInstalled(shell.rcFile)) {
697
885
  installHook(shell.rcFile, hookContent);
698
886
  log.success(`Shell hook installed in ${shell.rcFile}`);
887
+ } else if (isHookOutdated(shell.rcFile, hookContent)) {
888
+ const { update } = await (0, import_prompts.default)({
889
+ type: "confirm",
890
+ name: "update",
891
+ message: `Shell hook in ${shell.rcFile} is outdated. Update?`,
892
+ initial: true
893
+ });
894
+ if (update) {
895
+ updateHook(shell.rcFile, hookContent);
896
+ log.success(`Shell hook updated in ${shell.rcFile}`);
897
+ } else {
898
+ log.info("Shell hook update skipped");
899
+ }
900
+ } else {
901
+ log.info(`Shell hook up to date in ${shell.rcFile}`);
902
+ }
903
+ }
904
+ }
905
+ for (const tool of ai_tools_default.filter((t) => t.hook !== null)) {
906
+ const integration = AI_HOOK_INTEGRATIONS[tool.hook];
907
+ if (!integration?.isInstalled()) continue;
908
+ if (integration.isHookInstalled()) {
909
+ log.info(`${integration.displayName} session hook already installed`);
910
+ } else {
911
+ const { install } = await (0, import_prompts.default)({
912
+ type: "confirm",
913
+ name: "install",
914
+ message: `${integration.displayName} detected. Install session capture hook?`,
915
+ initial: true
916
+ });
917
+ if (install) {
918
+ integration.install();
919
+ log.success(`${integration.displayName} session hook installed`);
699
920
  }
700
921
  }
701
922
  }
@@ -745,7 +966,7 @@ function registerInit(program2) {
745
966
  log.info("Skipped \u2014 run later: workscribe config set ai.provider <provider>");
746
967
  }
747
968
  }
748
- if (!import_fs6.default.existsSync(CONFIG_PATH)) {
969
+ if (!import_fs8.default.existsSync(CONFIG_PATH)) {
749
970
  writeConfig(config);
750
971
  log.success("Config file created");
751
972
  }
@@ -764,7 +985,7 @@ function registerInit(program2) {
764
985
  }
765
986
 
766
987
  // src/cli/status.ts
767
- var import_fs7 = __toESM(require("fs"));
988
+ var import_fs9 = __toESM(require("fs"));
768
989
  var import_chalk3 = __toESM(require("chalk"));
769
990
 
770
991
  // src/utils/format.ts
@@ -822,7 +1043,7 @@ function registerStatus(program2) {
822
1043
  const total = getEventCount();
823
1044
  const todayEvents = getEventsForDate(todayDate());
824
1045
  const last = getLastEvent();
825
- const isPaused = import_fs7.default.existsSync(PAUSE_FLAG_PATH);
1046
+ const isPaused = import_fs9.default.existsSync(PAUSE_FLAG_PATH);
826
1047
  log.plain("");
827
1048
  log.plain(import_chalk3.default.bold(" Workscribe Status"));
828
1049
  log.plain("");
@@ -861,9 +1082,15 @@ var CATEGORY_COLORS = {
861
1082
  git_other: import_chalk4.default.blue,
862
1083
  code_merge: import_chalk4.default.green,
863
1084
  code_stash: import_chalk4.default.blue,
864
- ai_session: import_chalk4.default.magenta,
1085
+ ai_session: import_chalk4.default.bold.magenta,
1086
+ code_search: import_chalk4.default.dim.cyan,
1087
+ code_read: import_chalk4.default.dim.white,
1088
+ file_ops: import_chalk4.default.yellow,
865
1089
  other: import_chalk4.default.gray
866
1090
  };
1091
+ var AI_TOOL_DISPLAY_NAMES = Object.fromEntries(
1092
+ ai_tools_default.map((t) => [t.tool, t.displayName])
1093
+ );
867
1094
  function registerEvents(program2) {
868
1095
  program2.command("events").description("List captured events").option("--date <YYYY-MM-DD>", "date to list events for (default: today)").option("--limit <n>", "max events to show", parseInt).action((opts) => {
869
1096
  let date = todayDate();
@@ -888,10 +1115,22 @@ function registerEvents(program2) {
888
1115
  const colorFn = CATEGORY_COLORS[event.category] ?? import_chalk4.default.gray;
889
1116
  const time = formatDate(event.captured_at);
890
1117
  const category = colorFn(event.category.padEnd(20));
891
- const cmd = import_chalk4.default.dim(event.command.slice(0, 55));
892
1118
  const repo = event.repo_name ? import_chalk4.default.dim(` [${event.repo_name}]`) : "";
893
1119
  const exitBadge = event.exit_code && event.exit_code !== 0 ? import_chalk4.default.red(` \u2717${event.exit_code}`) : "";
894
- log.plain(` ${import_chalk4.default.dim(time)} ${category} ${cmd}${repo}${exitBadge}`);
1120
+ let cmdDisplay;
1121
+ if (event.category === "ai_session") {
1122
+ let meta = {};
1123
+ try {
1124
+ meta = event.metadata ? JSON.parse(event.metadata) : {};
1125
+ } catch {
1126
+ }
1127
+ const toolKey = typeof meta.tool === "string" ? meta.tool : "";
1128
+ const toolName = AI_TOOL_DISPLAY_NAMES[toolKey] ?? toolKey;
1129
+ cmdDisplay = import_chalk4.default.white(toolName.padEnd(20)) + import_chalk4.default.magenta(" \u2726");
1130
+ } else {
1131
+ cmdDisplay = import_chalk4.default.dim(event.command.slice(0, 55));
1132
+ }
1133
+ log.plain(` ${import_chalk4.default.dim(time)} ${category} ${cmdDisplay}${repo}${exitBadge}`);
895
1134
  }
896
1135
  if (limit < events.length) {
897
1136
  log.plain("");
@@ -1033,8 +1272,8 @@ function registerSessions(program2) {
1033
1272
 
1034
1273
  // src/cli/summary.ts
1035
1274
  var import_chalk6 = __toESM(require("chalk"));
1036
- var import_fs8 = __toESM(require("fs"));
1037
- var import_path4 = __toESM(require("path"));
1275
+ var import_fs10 = __toESM(require("fs"));
1276
+ var import_path6 = __toESM(require("path"));
1038
1277
 
1039
1278
  // src/ai/prompt.ts
1040
1279
  var SYSTEM_PROMPT = `You write short developer work summaries. Here is an example:
@@ -1042,23 +1281,27 @@ var SYSTEM_PROMPT = `You write short developer work summaries. Here is an exampl
1042
1281
  INPUT:
1043
1282
  Project: api-service
1044
1283
  Branch: feature/auth
1045
- Duration: 1h 30m
1284
+ Duration: 2h 00m
1046
1285
  Events:
1047
1286
  - branch_switch {"branch":"feature/auth"}
1048
1287
  - 2x dependency_install {"tool":"npm","packages":["bcrypt","jsonwebtoken"]}
1049
1288
  - 2x test_run {"tool":"jest"} [failed]
1050
1289
  - test_run {"tool":"jest"}
1290
+ - 2x file_ops {"tool":"mkdir"}
1051
1291
  - code_commit {"message":"fix: auth token validation"}
1292
+ - code_push {"remote":"origin","branch":"feature/auth"}
1052
1293
 
1053
1294
  OUTPUT:
1054
- - Switched to the authentication feature branch
1055
- - Added JWT and bcrypt dependencies
1295
+ - Switched to the authentication feature branch and installed JWT and bcrypt dependencies
1056
1296
  - Resolved failing auth tests
1057
- - Committed authentication fixes
1297
+ - Committed and pushed authentication fixes
1058
1298
 
1059
- For ai_session events: describe what was built during the AI session using the tool name (e.g. "via Claude Code"). If commits followed the session, group them into the same bullet. Maximum 5 bullets.
1299
+ Events like code_push, build, file_ops, and code_stash provide context \u2014 fold them into the surrounding narrative rather than listing them as separate bullets. Only surface them directly when they are the main activity (e.g. a standalone build fix, or a significant restructure with no accompanying commits).
1060
1300
 
1061
- Now summarise the following. Output ONLY the bullet points, nothing else. Maximum 5 bullets.`;
1301
+ For ai_session events: describe what was built during the AI session using the tool name (e.g. "via Claude Code"). If a commits array is present in metadata, use those commit messages to describe what was built. Group all activity from the session into one narrative bullet.
1302
+
1303
+ Maximum 5 bullets. Output ONLY the bullet points, nothing else.`;
1304
+ var EXCLUDED = /* @__PURE__ */ new Set(["environment", "code_search", "code_read", "server_run", "git_other"]);
1062
1305
  function buildUserMessage(payload) {
1063
1306
  const lines = [`Date: ${payload.date}`, ""];
1064
1307
  for (const session of payload.sessions) {
@@ -1068,6 +1311,7 @@ function buildUserMessage(payload) {
1068
1311
  lines.push("Events:");
1069
1312
  const counts = /* @__PURE__ */ new Map();
1070
1313
  for (const event of session.events) {
1314
+ if (EXCLUDED.has(event.category)) continue;
1071
1315
  const meta = Object.keys(event.metadata).length > 0 ? ` ${JSON.stringify(event.metadata)}` : "";
1072
1316
  const failed = event.exitCode !== null && event.exitCode !== 0 ? " [failed]" : "";
1073
1317
  const key = `${event.category}${meta}${failed}`;
@@ -1124,6 +1368,7 @@ function buildWeeklyMessage(payload) {
1124
1368
  lines.push(` Project: ${project}${branch} \u2014 ${formatDuration(s.durationMinutes)}`);
1125
1369
  const counts = /* @__PURE__ */ new Map();
1126
1370
  for (const e of s.events) {
1371
+ if (EXCLUDED.has(e.category)) continue;
1127
1372
  const meta = Object.keys(e.metadata).length > 0 ? ` ${JSON.stringify(e.metadata)}` : "";
1128
1373
  const failed = e.exitCode !== null && e.exitCode !== 0 ? " [failed]" : "";
1129
1374
  const key = `${e.category}${meta}${failed}`;
@@ -1441,9 +1686,9 @@ function registerSummary(program2) {
1441
1686
  return;
1442
1687
  }
1443
1688
  const cacheKey = isoWeekKey(today);
1444
- const cacheFile = import_path4.default.join(WEEKLY_CACHE_DIR, `${cacheKey}.md`);
1445
- if (!opts.fresh && import_fs8.default.existsSync(cacheFile)) {
1446
- const cached = import_fs8.default.readFileSync(cacheFile, "utf-8");
1689
+ const cacheFile = import_path6.default.join(WEEKLY_CACHE_DIR, `${cacheKey}.md`);
1690
+ if (!opts.fresh && import_fs10.default.existsSync(cacheFile)) {
1691
+ const cached = import_fs10.default.readFileSync(cacheFile, "utf-8");
1447
1692
  log.plain("");
1448
1693
  log.plain(cached);
1449
1694
  log.plain("");
@@ -1459,8 +1704,8 @@ function registerSummary(program2) {
1459
1704
  log.plain("");
1460
1705
  log.plain(summary);
1461
1706
  log.plain("");
1462
- import_fs8.default.mkdirSync(WEEKLY_CACHE_DIR, { recursive: true });
1463
- import_fs8.default.writeFileSync(cacheFile, summary, { encoding: "utf-8", mode: 384 });
1707
+ import_fs10.default.mkdirSync(WEEKLY_CACHE_DIR, { recursive: true });
1708
+ import_fs10.default.writeFileSync(cacheFile, summary, { encoding: "utf-8", mode: 384 });
1464
1709
  await handleWebhook(opts, config, summary);
1465
1710
  } catch (e) {
1466
1711
  log.error(`Weekly summary failed [${config.ai.provider}]: ${e.message}`);
@@ -1523,8 +1768,8 @@ ${bullets.trim()}`;
1523
1768
  }
1524
1769
 
1525
1770
  // src/cli/export.ts
1526
- var import_fs9 = __toESM(require("fs"));
1527
- var import_path5 = __toESM(require("path"));
1771
+ var import_fs11 = __toESM(require("fs"));
1772
+ var import_path7 = __toESM(require("path"));
1528
1773
  function eventRowToPayload2(row) {
1529
1774
  let metadata = {};
1530
1775
  try {
@@ -1569,9 +1814,9 @@ function registerExport(program2) {
1569
1814
  const payload = { date, sessions: sessionPayloads };
1570
1815
  log.info(`Generating summary via ${config.ai.provider}\u2026`);
1571
1816
  const summary = await provider.summarize(payload);
1572
- const outputPath = opts.output ?? import_path5.default.join(EXPORTS_DIR, `workscribe-${date}.md`);
1573
- if (!import_fs9.default.existsSync(EXPORTS_DIR)) import_fs9.default.mkdirSync(EXPORTS_DIR, { recursive: true, mode: 448 });
1574
- import_fs9.default.writeFileSync(outputPath, `# Work Summary \u2014 ${date}
1817
+ const outputPath = opts.output ?? import_path7.default.join(EXPORTS_DIR, `workscribe-${date}.md`);
1818
+ if (!import_fs11.default.existsSync(EXPORTS_DIR)) import_fs11.default.mkdirSync(EXPORTS_DIR, { recursive: true, mode: 448 });
1819
+ import_fs11.default.writeFileSync(outputPath, `# Work Summary \u2014 ${date}
1575
1820
 
1576
1821
  ${summary}
1577
1822
  `, { encoding: "utf-8", mode: 384 });
@@ -1629,22 +1874,22 @@ function printConfig(obj, indent) {
1629
1874
  }
1630
1875
 
1631
1876
  // src/cli/pause.ts
1632
- var import_fs10 = __toESM(require("fs"));
1877
+ var import_fs12 = __toESM(require("fs"));
1633
1878
  function registerPause(program2) {
1634
1879
  program2.command("pause").description("Pause capture temporarily").action(() => {
1635
- if (import_fs10.default.existsSync(PAUSE_FLAG_PATH)) {
1880
+ if (import_fs12.default.existsSync(PAUSE_FLAG_PATH)) {
1636
1881
  log.info("Capture is already paused. Run workscribe resume to restart.");
1637
1882
  return;
1638
1883
  }
1639
- import_fs10.default.writeFileSync(PAUSE_FLAG_PATH, "", { mode: 384 });
1884
+ import_fs12.default.writeFileSync(PAUSE_FLAG_PATH, "", { mode: 384 });
1640
1885
  log.success("Capture paused. Run workscribe resume to restart.");
1641
1886
  });
1642
1887
  program2.command("resume").description("Resume capture after a pause").action(() => {
1643
- if (!import_fs10.default.existsSync(PAUSE_FLAG_PATH)) {
1888
+ if (!import_fs12.default.existsSync(PAUSE_FLAG_PATH)) {
1644
1889
  log.info("Capture is not paused.");
1645
1890
  return;
1646
1891
  }
1647
- import_fs10.default.rmSync(PAUSE_FLAG_PATH);
1892
+ import_fs12.default.rmSync(PAUSE_FLAG_PATH);
1648
1893
  log.success("Capture resumed.");
1649
1894
  });
1650
1895
  }
@@ -1678,10 +1923,61 @@ function registerUndo(program2) {
1678
1923
  });
1679
1924
  }
1680
1925
 
1926
+ // src/cli/clear.ts
1927
+ var import_chalk8 = __toESM(require("chalk"));
1928
+ var import_prompts2 = __toESM(require("prompts"));
1929
+ function registerClear(program2) {
1930
+ program2.command("clear").description("Delete all captured events for a date").option("--date <YYYY-MM-DD>", "date to clear (default: today)").option("--dry-run", "preview what would be deleted without deleting").option("--yes", "skip confirmation prompt").action(async (opts) => {
1931
+ let date = todayDate();
1932
+ if (opts.date) {
1933
+ try {
1934
+ date = parseDate(opts.date);
1935
+ } catch (e) {
1936
+ log.error(e.message);
1937
+ process.exit(1);
1938
+ }
1939
+ }
1940
+ const events = getEventsForDate(date);
1941
+ if (events.length === 0) {
1942
+ log.plain(`No events found for ${date}.`);
1943
+ return;
1944
+ }
1945
+ log.plain("");
1946
+ log.plain(import_chalk8.default.bold(` Events for ${date} (${events.length} total)`));
1947
+ log.plain("");
1948
+ const counts = /* @__PURE__ */ new Map();
1949
+ for (const e of events) counts.set(e.category, (counts.get(e.category) ?? 0) + 1);
1950
+ for (const [cat, n] of counts) {
1951
+ log.plain(` ${import_chalk8.default.dim(String(n).padStart(4))} ${cat}`);
1952
+ }
1953
+ log.plain("");
1954
+ if (opts.dryRun) {
1955
+ log.plain(import_chalk8.default.dim(" Dry run \u2014 nothing deleted."));
1956
+ log.plain("");
1957
+ return;
1958
+ }
1959
+ if (!opts.yes) {
1960
+ const { confirm } = await (0, import_prompts2.default)({
1961
+ type: "confirm",
1962
+ name: "confirm",
1963
+ message: `Delete all ${events.length} events for ${date}?`,
1964
+ initial: false
1965
+ });
1966
+ if (!confirm) {
1967
+ log.plain("Cancelled.");
1968
+ return;
1969
+ }
1970
+ }
1971
+ const deleted = deleteEventsForDate(date);
1972
+ log.success(`Cleared ${deleted} event${deleted !== 1 ? "s" : ""} for ${date}.`);
1973
+ });
1974
+ }
1975
+
1681
1976
  // src/index.ts
1682
1977
  var program = new import_commander.Command();
1683
- program.name("workscribe").description("Automatic developer work journal").version("1.3.0");
1978
+ program.name("workscribe").description("Automatic developer work journal").version("1.5.0");
1684
1979
  registerCapture(program);
1980
+ registerHook(program);
1685
1981
  registerInit(program);
1686
1982
  registerStatus(program);
1687
1983
  registerEvents(program);
@@ -1691,5 +1987,6 @@ registerExport(program);
1691
1987
  registerConfig(program);
1692
1988
  registerPause(program);
1693
1989
  registerUndo(program);
1990
+ registerClear(program);
1694
1991
  program.parse();
1695
1992
  //# sourceMappingURL=index.js.map