@workscribe/cli 1.8.0 → 1.10.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,7 +27,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
27
  var import_commander = require("commander");
28
28
 
29
29
  // src/capture/index.ts
30
- var import_fs7 = __toESM(require("fs"));
30
+ var import_fs9 = __toESM(require("fs"));
31
31
 
32
32
  // src/db/index.ts
33
33
  var import_node_sqlite = require("node:sqlite");
@@ -43,6 +43,7 @@ var DEBUG_LOG_PATH = import_path.default.join(WORKSCRIBE_DIR, "debug.log");
43
43
  var EXPORTS_DIR = import_path.default.join(WORKSCRIBE_DIR, "exports");
44
44
  var PAUSE_FLAG_PATH = import_path.default.join(WORKSCRIBE_DIR, "paused");
45
45
  var WEEKLY_CACHE_DIR = import_path.default.join(WORKSCRIBE_DIR, "weekly-cache");
46
+ var PROFILES_DIR = import_path.default.join(WORKSCRIBE_DIR, "profiles");
46
47
 
47
48
  // src/db/migrations/001_initial_schema.ts
48
49
  var initial_schema_default = {
@@ -85,8 +86,27 @@ var initial_schema_default = {
85
86
  `
86
87
  };
87
88
 
89
+ // src/db/migrations/002_semantic_categories.ts
90
+ var migration = {
91
+ version: 2,
92
+ description: "Rename granular categories to 13 universal semantic categories",
93
+ sql: `
94
+ UPDATE events SET category = 'shipping' WHERE category IN ('code_commit','code_push','code_merge');
95
+ UPDATE events SET category = 'versioning' WHERE category IN ('code_pull','branch_switch','code_stash','git_other');
96
+ UPDATE events SET category = 'building' WHERE category = 'build';
97
+ UPDATE events SET category = 'running' WHERE category IN ('server_run','script_run');
98
+ UPDATE events SET category = 'testing' WHERE category = 'test_run';
99
+ UPDATE events SET category = 'linting' WHERE category = 'lint_run';
100
+ UPDATE events SET category = 'dependencies' WHERE category IN ('dependency_install','dependency_remove');
101
+ UPDATE events SET category = 'infrastructure' WHERE category = 'infrastructure';
102
+ UPDATE events SET category = 'files' WHERE category IN ('file_ops','environment');
103
+ UPDATE events SET category = 'exploring' WHERE category IN ('code_search','code_read');
104
+ `
105
+ };
106
+ var semantic_categories_default = migration;
107
+
88
108
  // src/db/migrations/index.ts
89
- var MIGRATIONS = [initial_schema_default];
109
+ var MIGRATIONS = [initial_schema_default, semantic_categories_default];
90
110
 
91
111
  // src/db/index.ts
92
112
  var _db = null;
@@ -121,15 +141,15 @@ function applyMigrations(db) {
121
141
  });
122
142
  }
123
143
  const applied = new Set(
124
- db.prepare("SELECT version FROM schema_migrations").all().map((r) => r.version)
144
+ db.prepare("SELECT version FROM schema_migrations").all().map((row) => row.version)
125
145
  );
126
- for (const migration of MIGRATIONS) {
127
- if (applied.has(migration.version)) continue;
146
+ for (const migration2 of MIGRATIONS) {
147
+ if (applied.has(migration2.version)) continue;
128
148
  db.exec("BEGIN");
129
149
  try {
130
- db.exec(migration.sql);
150
+ db.exec(migration2.sql);
131
151
  db.prepare("INSERT INTO schema_migrations (version, applied_at) VALUES ($version, datetime('now'))").run({
132
- version: migration.version
152
+ version: migration2.version
133
153
  });
134
154
  db.exec("COMMIT");
135
155
  } catch (err) {
@@ -137,7 +157,7 @@ function applyMigrations(db) {
137
157
  db.exec("ROLLBACK");
138
158
  } catch {
139
159
  }
140
- throw new Error(`Migration ${migration.version} (${migration.description}) failed: ${err.message}`);
160
+ throw new Error(`Migration ${migration2.version} (${migration2.description}) failed: ${err.message}`);
141
161
  }
142
162
  }
143
163
  }
@@ -208,7 +228,7 @@ function updateSessionSummary(sessionId, summary) {
208
228
  function getLastEventIds(n) {
209
229
  const db = getDb();
210
230
  const rows = db.prepare("SELECT id FROM events ORDER BY id DESC LIMIT $n").all({ n });
211
- return rows.map((r) => r.id);
231
+ return rows.map((row) => row.id);
212
232
  }
213
233
  function getEventsByIds(ids) {
214
234
  if (ids.length === 0) return [];
@@ -232,6 +252,28 @@ function deleteEventsForDate(date) {
232
252
  const result = db.prepare(`DELETE FROM events WHERE date(captured_at, 'localtime') = $date`).run({ date });
233
253
  return result.changes;
234
254
  }
255
+ function getOtherEventToolCounts(days) {
256
+ const db = getDb();
257
+ return db.prepare(
258
+ `SELECT json_extract(metadata, '$.tool') as tool, COUNT(*) as count
259
+ FROM events
260
+ WHERE category = 'other'
261
+ AND captured_at >= datetime('now', $offset)
262
+ AND json_extract(metadata, '$.tool') IS NOT NULL
263
+ GROUP BY tool
264
+ ORDER BY count DESC`
265
+ ).all({ offset: `-${days} days` });
266
+ }
267
+ function getRecentOtherRate(days) {
268
+ const db = getDb();
269
+ const row = db.prepare(
270
+ `SELECT COUNT(*) as total,
271
+ SUM(CASE WHEN category = 'other' THEN 1 ELSE 0 END) as otherCount
272
+ FROM events
273
+ WHERE captured_at >= datetime('now', $offset)`
274
+ ).get({ offset: `-${days} days` });
275
+ return row;
276
+ }
235
277
  function clearSessionsForDate(date) {
236
278
  const db = getDb();
237
279
  db.prepare(
@@ -475,6 +517,240 @@ function loadAIToolRegistry() {
475
517
  return [...filteredBuiltins, ...userEntries];
476
518
  }
477
519
 
520
+ // src/profiles/loader.ts
521
+ var import_fs8 = __toESM(require("fs"));
522
+ var import_path6 = __toESM(require("path"));
523
+
524
+ // src/config/index.ts
525
+ var import_fs6 = __toESM(require("fs"));
526
+ var DEFAULTS = {
527
+ ai: {
528
+ provider: "",
529
+ apiKey: "",
530
+ model: "",
531
+ baseUrl: ""
532
+ },
533
+ session: {
534
+ idleTimeout: 30
535
+ },
536
+ capture: {
537
+ ignore: ["cd", "ls", "ll", "la", "ls -la", "ls -l", "ls -a", "pwd", "clear", "exit", "history", "man"],
538
+ aiTools: true,
539
+ includeAiderPrompt: true,
540
+ profiles: []
541
+ },
542
+ redact: {
543
+ extra: []
544
+ },
545
+ summary: {
546
+ format: "markdown"
547
+ }
548
+ };
549
+ function readConfig() {
550
+ if (!import_fs6.default.existsSync(CONFIG_PATH)) return { ...DEFAULTS };
551
+ try {
552
+ const raw = import_fs6.default.readFileSync(CONFIG_PATH, "utf-8");
553
+ const parsed = JSON.parse(raw);
554
+ return deepMerge(DEFAULTS, parsed);
555
+ } catch {
556
+ return { ...DEFAULTS };
557
+ }
558
+ }
559
+ function writeConfig(config) {
560
+ import_fs6.default.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { encoding: "utf-8", mode: 384 });
561
+ try {
562
+ import_fs6.default.chmodSync(CONFIG_PATH, 384);
563
+ } catch {
564
+ }
565
+ }
566
+ function getConfigValue(config, key) {
567
+ const parts = key.split(".");
568
+ let cur = config;
569
+ for (const part of parts) {
570
+ if (cur == null || typeof cur !== "object") return void 0;
571
+ cur = cur[part];
572
+ }
573
+ return cur;
574
+ }
575
+ function setConfigValue(config, key, value) {
576
+ const parts = key.split(".");
577
+ const result = JSON.parse(JSON.stringify(config));
578
+ let cur = result;
579
+ for (let i = 0; i < parts.length - 1; i++) {
580
+ const part = parts[i];
581
+ if (cur[part] == null || typeof cur[part] !== "object") {
582
+ cur[part] = {};
583
+ }
584
+ cur = cur[part];
585
+ }
586
+ const lastKey = parts[parts.length - 1];
587
+ if (value === "true") cur[lastKey] = true;
588
+ else if (value === "false") cur[lastKey] = false;
589
+ else if (!isNaN(Number(value))) cur[lastKey] = Number(value);
590
+ else cur[lastKey] = value;
591
+ return result;
592
+ }
593
+ function deepMerge(defaults, overrides) {
594
+ const result = { ...defaults };
595
+ for (const key of Object.keys(overrides)) {
596
+ const val = overrides[key];
597
+ if (val !== null && typeof val === "object" && !Array.isArray(val) && typeof defaults[key] === "object") {
598
+ result[key] = deepMerge(defaults[key], val);
599
+ } else if (val !== void 0) {
600
+ result[key] = val;
601
+ }
602
+ }
603
+ return result;
604
+ }
605
+
606
+ // src/profiles/schema.ts
607
+ function isNonEmptyString(value) {
608
+ return typeof value === "string" && value.length > 0;
609
+ }
610
+ function isValidRule(rule) {
611
+ if (!rule || typeof rule !== "object") return false;
612
+ const { pattern, category } = rule;
613
+ return isNonEmptyString(pattern) && isNonEmptyString(category);
614
+ }
615
+ function isValidProfile(candidate) {
616
+ if (!candidate || typeof candidate !== "object") return false;
617
+ const { name, displayName, version, rules } = candidate;
618
+ if (!isNonEmptyString(name) || !isNonEmptyString(displayName) || !isNonEmptyString(version)) return false;
619
+ if (!Array.isArray(rules)) return false;
620
+ return rules.every(isValidRule);
621
+ }
622
+ function isValidManifest(m) {
623
+ return Array.isArray(m) && m.every((e) => e && typeof e === "object" && typeof e.name === "string");
624
+ }
625
+
626
+ // src/profiles/fetcher.ts
627
+ var import_fs7 = __toESM(require("fs"));
628
+ var import_path5 = __toESM(require("path"));
629
+ var REGISTRY_BASE = "https://raw.githubusercontent.com/workscribe/profiles/main";
630
+ function manifestCachePath() {
631
+ return import_path5.default.join(PROFILES_DIR, "manifest.json");
632
+ }
633
+ async function fetchManifest() {
634
+ const response = await fetch(`${REGISTRY_BASE}/manifest.json`);
635
+ if (!response.ok) throw new Error(`Registry unreachable (${response.status})`);
636
+ const data = await response.json();
637
+ if (!isValidManifest(data)) throw new Error("Manifest format unrecognised");
638
+ return data;
639
+ }
640
+ async function fetchProfile(name) {
641
+ const response = await fetch(`${REGISTRY_BASE}/${name}.json`);
642
+ if (response.status === 404) throw new Error(`Profile "${name}" not found in registry`);
643
+ if (!response.ok) throw new Error(`Registry unreachable (${response.status})`);
644
+ const data = await response.json();
645
+ if (!isValidProfile(data)) throw new Error(`Profile "${name}" failed schema validation`);
646
+ return data;
647
+ }
648
+ async function installProfile(name) {
649
+ ensureProfilesDir();
650
+ const profile = await fetchProfile(name);
651
+ const dest = import_path5.default.join(PROFILES_DIR, `${name}.json`);
652
+ import_fs7.default.writeFileSync(dest, JSON.stringify(profile, null, 2), { encoding: "utf-8", mode: 384 });
653
+ debugLog("profiles", `Installed profile "${name}" to ${dest}`);
654
+ }
655
+ async function cacheManifest() {
656
+ ensureProfilesDir();
657
+ const manifest = await fetchManifest();
658
+ import_fs7.default.writeFileSync(manifestCachePath(), JSON.stringify(manifest, null, 2), { encoding: "utf-8", mode: 384 });
659
+ return manifest;
660
+ }
661
+ function readCachedManifest() {
662
+ if (!import_fs7.default.existsSync(manifestCachePath())) return null;
663
+ try {
664
+ const data = JSON.parse(import_fs7.default.readFileSync(manifestCachePath(), "utf-8"));
665
+ return isValidManifest(data) ? data : null;
666
+ } catch {
667
+ return null;
668
+ }
669
+ }
670
+ function ensureProfilesDir() {
671
+ if (!import_fs7.default.existsSync(PROFILES_DIR)) {
672
+ import_fs7.default.mkdirSync(PROFILES_DIR, { recursive: true, mode: 448 });
673
+ }
674
+ }
675
+
676
+ // src/profiles/loader.ts
677
+ function compileRule(profileName, rule) {
678
+ let regex;
679
+ try {
680
+ regex = new RegExp(rule.pattern);
681
+ } catch {
682
+ debugLog("profiles", `Profile "${profileName}": invalid pattern "${rule.pattern}" \u2014 skipping rule`);
683
+ return null;
684
+ }
685
+ return {
686
+ match(cmd) {
687
+ const match = cmd.match(regex);
688
+ if (!match) return null;
689
+ const metadata = {};
690
+ for (const [key, value] of Object.entries(rule.metadata ?? {})) {
691
+ metadata[key] = value.replace(/\$(\d+)/g, (_, index) => match[Number(index)] ?? "");
692
+ }
693
+ return { category: rule.category, metadata };
694
+ }
695
+ };
696
+ }
697
+ function loadProfileFromDisk(name) {
698
+ const filePath = import_path6.default.join(PROFILES_DIR, `${name}.json`);
699
+ if (!import_fs8.default.existsSync(filePath)) {
700
+ debugLog("profiles", `Profile "${name}" not found in cache \u2014 run: workscribe profile add ${name}`);
701
+ return null;
702
+ }
703
+ let candidate;
704
+ try {
705
+ candidate = JSON.parse(import_fs8.default.readFileSync(filePath, "utf-8"));
706
+ } catch {
707
+ debugLog("profiles", `Profile "${name}" contains invalid JSON \u2014 skipping`);
708
+ return null;
709
+ }
710
+ if (!isValidProfile(candidate)) {
711
+ debugLog("profiles", `Profile "${name}" failed schema validation \u2014 skipping`);
712
+ return null;
713
+ }
714
+ return candidate;
715
+ }
716
+ function getActiveProfileRules() {
717
+ const activeNames = readConfig().capture.profiles;
718
+ if (activeNames.length === 0) return [];
719
+ return activeNames.flatMap((name) => {
720
+ const profile = loadProfileFromDisk(name);
721
+ if (!profile) return [];
722
+ return profile.rules.flatMap((rule) => {
723
+ const compiled = compileRule(profile.name, rule);
724
+ return compiled ? [compiled] : [];
725
+ });
726
+ });
727
+ }
728
+ function getActiveProfileAiContexts() {
729
+ const activeNames = readConfig().capture.profiles;
730
+ if (activeNames.length === 0) return [];
731
+ return activeNames.flatMap((name) => {
732
+ const profile = loadProfileFromDisk(name);
733
+ return profile?.aiContext ? [profile.aiContext] : [];
734
+ });
735
+ }
736
+ function getCachedProfiles() {
737
+ if (!import_fs8.default.existsSync(PROFILES_DIR)) return [];
738
+ return import_fs8.default.readdirSync(PROFILES_DIR).filter((file) => file.endsWith(".json") && file !== "manifest.json").flatMap((file) => {
739
+ const profile = loadProfileFromDisk(file.replace(/\.json$/, ""));
740
+ return profile ? [profile] : [];
741
+ });
742
+ }
743
+ function detectProfilesForDirectory(dir) {
744
+ const manifest = readCachedManifest();
745
+ if (!manifest) return [];
746
+ const activeNames = new Set(readConfig().capture.profiles);
747
+ return manifest.filter((entry) => {
748
+ if (activeNames.has(entry.name)) return false;
749
+ const indicatorFiles = entry.detect?.files ?? [];
750
+ return indicatorFiles.length > 0 && indicatorFiles.every((file) => import_fs8.default.existsSync(import_path6.default.join(dir, file)));
751
+ });
752
+ }
753
+
478
754
  // src/normalize/patterns.ts
479
755
  function makeRule(pattern, category, extract = () => ({})) {
480
756
  return {
@@ -488,116 +764,133 @@ function makeRule(pattern, category, extract = () => ({})) {
488
764
  function extractPackages(cmd, afterKeyword) {
489
765
  const idx = cmd.indexOf(afterKeyword);
490
766
  if (idx === -1) return [];
491
- return cmd.slice(idx + afterKeyword.length).trim().split(/\s+/).filter((p) => p && !p.startsWith("-"));
767
+ return cmd.slice(idx + afterKeyword.length).trim().split(/\s+/).filter((part) => part && !part.startsWith("-"));
492
768
  }
493
- var RULES = [
769
+ var BASE_RULES = [
494
770
  // ─── Git operations ───────────────────────────────────────────────────────
495
- makeRule(/^git\s+commit\b/, "code_commit", (_m, cmd) => {
771
+ makeRule(/^git\s+commit\b/, "shipping", (_m, cmd) => {
496
772
  const msgMatch = cmd.match(/-m\s+["']?([^"']+)["']?/);
497
773
  return { message: msgMatch ? msgMatch[1].trim() : null };
498
774
  }),
499
- makeRule(/^git\s+push\b/, "code_push", (_m, cmd) => {
775
+ makeRule(/^git\s+push\b/, "shipping", (_m, cmd) => {
500
776
  const parts = cmd.split(/\s+/);
501
777
  return { remote: parts[2] ?? "origin", branch: parts[3] ?? null };
502
778
  }),
503
- makeRule(/^git\s+(pull|fetch)\b/, "code_pull", (_m, cmd) => {
779
+ makeRule(/^git\s+(pull|fetch)\b/, "versioning", (_m, cmd) => {
504
780
  const parts = cmd.split(/\s+/);
505
781
  return { subcommand: parts[1], remote: parts[2] ?? "origin" };
506
782
  }),
507
- makeRule(/^git\s+(checkout|switch)\b/, "branch_switch", (_m, cmd) => {
783
+ makeRule(/^git\s+(checkout|switch)\b/, "versioning", (_m, cmd) => {
508
784
  const parts = cmd.split(/\s+/);
509
- const branch = parts.find((p) => !p.startsWith("-") && p !== "git" && p !== parts[1]);
785
+ const branch = parts.find((part) => !part.startsWith("-") && part !== "git" && part !== parts[1]);
510
786
  return { branch: branch ?? null };
511
787
  }),
512
- makeRule(/^git\s+(merge|rebase)\b/, "code_merge", (_m, cmd) => {
788
+ makeRule(/^git\s+(merge|rebase)\b/, "shipping", (_m, cmd) => {
513
789
  const parts = cmd.split(/\s+/);
514
790
  return { subcommand: parts[1], target: parts[2] ?? null };
515
791
  }),
516
- makeRule(/^git\s+stash\b/, "code_stash", (_m, cmd) => {
792
+ makeRule(/^git\s+stash\b/, "versioning", (_m, cmd) => {
517
793
  const parts = cmd.split(/\s+/);
518
794
  return { subcommand: parts[2] ?? "push" };
519
795
  }),
520
- makeRule(/^git\s+/, "git_other", (_m, cmd) => {
796
+ makeRule(/^git\s+/, "versioning", (_m, cmd) => {
521
797
  const parts = cmd.split(/\s+/);
522
798
  return { subcommand: parts[1] ?? null };
523
799
  }),
524
- // ─── Package managers — install ───────────────────────────────────────────
525
- makeRule(/^npm\s+(install|i|add)\b/, "dependency_install", (_m, cmd) => ({
800
+ // ─── Releasing ────────────────────────────────────────────────────────────
801
+ makeRule(/^npm\s+publish\b/, "releasing", () => ({ tool: "npm" })),
802
+ makeRule(/^cargo\s+publish\b/, "releasing", () => ({ tool: "cargo" })),
803
+ makeRule(/^gem\s+push\b/, "releasing", () => ({ tool: "gem" })),
804
+ makeRule(/^docker\s+push\b/, "releasing", (_m, cmd) => ({ tool: "docker", image: cmd.split(/\s+/)[2] ?? null })),
805
+ makeRule(/^gh\s+release\b/, "releasing", (_m, cmd) => ({ tool: "gh", subcommand: cmd.split(/\s+/)[2] ?? null })),
806
+ makeRule(/^fly\s+(deploy|launch)\b/, "releasing", (match) => ({ tool: "fly", subcommand: match[1] })),
807
+ makeRule(/^vercel\s+(deploy|build|--prod)\b/, "releasing", () => ({ tool: "vercel" })),
808
+ makeRule(/^netlify\s+deploy\b/, "releasing", () => ({ tool: "netlify" })),
809
+ makeRule(/^railway\s+up\b/, "releasing", () => ({ tool: "railway" })),
810
+ makeRule(/^wrangler\s+deploy\b/, "releasing", () => ({ tool: "wrangler" })),
811
+ // ─── Dependencies ─────────────────────────────────────────────────────────
812
+ makeRule(/^npm\s+(install|i|add)\b/, "dependencies", (_m, cmd) => ({
526
813
  tool: "npm",
527
814
  packages: extractPackages(cmd, cmd.includes("install") ? "install" : cmd.includes(" i ") ? " i " : "add")
528
815
  })),
529
- makeRule(/^(yarn|pnpm)\s+(install|add)\b/, "dependency_install", (m, cmd) => ({
816
+ makeRule(/^(yarn|pnpm)\s+(install|add)\b/, "dependencies", (m, cmd) => ({
530
817
  tool: m[1],
531
818
  packages: extractPackages(cmd, "add")
532
819
  })),
533
- makeRule(/^bun\s+(install|add)\b/, "dependency_install", (_m, cmd) => ({
820
+ makeRule(/^bun\s+(install|add)\b/, "dependencies", (_m, cmd) => ({
534
821
  tool: "bun",
535
822
  packages: extractPackages(cmd, "add")
536
823
  })),
537
- makeRule(/^pip\s+(install)\b/, "dependency_install", (_m, cmd) => ({
824
+ makeRule(/^pip\s+(install)\b/, "dependencies", (_m, cmd) => ({
538
825
  tool: "pip",
539
826
  packages: extractPackages(cmd, "install")
540
827
  })),
541
- makeRule(/^cargo\s+add\b/, "dependency_install", (_m, cmd) => ({
828
+ makeRule(/^cargo\s+add\b/, "dependencies", (_m, cmd) => ({
542
829
  tool: "cargo",
543
830
  packages: extractPackages(cmd, "add")
544
831
  })),
545
- makeRule(/^(brew)\s+install\b/, "dependency_install", (_m, cmd) => ({
832
+ makeRule(/^(brew)\s+install\b/, "dependencies", (_m, cmd) => ({
546
833
  tool: "brew",
547
834
  packages: extractPackages(cmd, "install")
548
835
  })),
549
- makeRule(/^go\s+get\b/, "dependency_install", (_m, cmd) => ({
836
+ makeRule(/^go\s+get\b/, "dependencies", (_m, cmd) => ({
550
837
  tool: "go",
551
838
  packages: extractPackages(cmd, "get")
552
839
  })),
553
- // ─── Package managers — remove ────────────────────────────────────────────
554
- makeRule(/^npm\s+(uninstall|remove|rm)\b/, "dependency_remove", (_m, cmd) => ({
840
+ makeRule(/^npm\s+(uninstall|remove|rm)\b/, "dependencies", (_m, cmd) => ({
555
841
  tool: "npm",
556
842
  packages: extractPackages(cmd, cmd.split(/\s+/)[1])
557
843
  })),
558
- makeRule(/^(yarn|pnpm)\s+remove\b/, "dependency_remove", (m, cmd) => ({
844
+ makeRule(/^(yarn|pnpm)\s+remove\b/, "dependencies", (m, cmd) => ({
559
845
  tool: m[1],
560
846
  packages: extractPackages(cmd, "remove")
561
847
  })),
562
- makeRule(/^bun\s+remove\b/, "dependency_remove", (_m, cmd) => ({
848
+ makeRule(/^bun\s+remove\b/, "dependencies", (_m, cmd) => ({
563
849
  tool: "bun",
564
850
  packages: extractPackages(cmd, "remove")
565
851
  })),
566
- makeRule(/^pip\s+uninstall\b/, "dependency_remove", (_m, cmd) => ({
852
+ makeRule(/^pip\s+uninstall\b/, "dependencies", (_m, cmd) => ({
567
853
  tool: "pip",
568
854
  packages: extractPackages(cmd, "uninstall")
569
855
  })),
856
+ // Runtime version managers
857
+ makeRule(/^nvm\b/, "dependencies", (_m, cmd) => ({ tool: "nvm", subcommand: cmd.split(/\s+/)[1] ?? null })),
858
+ makeRule(/^(fnm|volta)\b/, "dependencies", (m, cmd) => ({ tool: m[1], subcommand: cmd.split(/\s+/)[1] ?? null })),
859
+ makeRule(/^(rbenv|pyenv|asdf)\b/, "dependencies", (m, cmd) => ({
860
+ tool: m[1],
861
+ subcommand: cmd.split(/\s+/)[1] ?? null
862
+ })),
570
863
  // ─── Tests ────────────────────────────────────────────────────────────────
571
- makeRule(/^npm\s+(test|run\s+test)\b/, "test_run", () => ({ tool: "npm" })),
572
- makeRule(/^yarn\s+test\b/, "test_run", () => ({ tool: "yarn" })),
573
- makeRule(/^pnpm\s+test\b/, "test_run", () => ({ tool: "pnpm" })),
574
- makeRule(/^bun\s+test\b/, "test_run", () => ({ tool: "bun" })),
575
- makeRule(/^(jest|vitest)\b/, "test_run", (m) => ({ tool: m[1] })),
576
- makeRule(/^pytest\b/, "test_run", () => ({ tool: "pytest" })),
577
- makeRule(/^go\s+test\b/, "test_run", () => ({ tool: "go" })),
578
- makeRule(/^cargo\s+test\b/, "test_run", () => ({ tool: "cargo" })),
579
- makeRule(/^rspec\b/, "test_run", () => ({ tool: "rspec" })),
580
- makeRule(/^(php\s+)?phpunit\b/, "test_run", () => ({ tool: "phpunit" })),
864
+ makeRule(/^npm\s+(test|run\s+test)\b/, "testing", () => ({ tool: "npm" })),
865
+ makeRule(/^yarn\s+test\b/, "testing", () => ({ tool: "yarn" })),
866
+ makeRule(/^pnpm\s+test\b/, "testing", () => ({ tool: "pnpm" })),
867
+ makeRule(/^bun\s+test\b/, "testing", () => ({ tool: "bun" })),
868
+ makeRule(/^(jest|vitest)\b/, "testing", (match) => ({ tool: match[1] })),
869
+ makeRule(/^pytest\b/, "testing", () => ({ tool: "pytest" })),
870
+ makeRule(/^go\s+test\b/, "testing", () => ({ tool: "go" })),
871
+ makeRule(/^cargo\s+test\b/, "testing", () => ({ tool: "cargo" })),
872
+ makeRule(/^rspec\b/, "testing", () => ({ tool: "rspec" })),
873
+ makeRule(/^(php\s+)?phpunit\b/, "testing", () => ({ tool: "phpunit" })),
581
874
  // ─── Build ────────────────────────────────────────────────────────────────
582
- makeRule(/^npm\s+run\s+build\b/, "build", () => ({ tool: "npm" })),
583
- makeRule(/^(yarn|pnpm)\s+build\b/, "build", (m) => ({ tool: m[1] })),
584
- makeRule(/^bun\s+run\s+build\b/, "build", () => ({ tool: "bun" })),
585
- makeRule(/^(make|gmake)\b/, "build", (m) => ({ tool: m[1] })),
586
- makeRule(/^cargo\s+build\b/, "build", () => ({ tool: "cargo" })),
587
- makeRule(/^go\s+build\b/, "build", () => ({ tool: "go" })),
588
- makeRule(/^docker\s+build\b/, "build", () => ({ tool: "docker" })),
589
- makeRule(/^(mvn|gradle)\b/, "build", (m) => ({ tool: m[1] })),
590
- // ─── Dev server ───────────────────────────────────────────────────────────
591
- makeRule(/^npm\s+run\s+(dev|start|serve)\b/, "server_run", (_m, cmd) => ({
875
+ makeRule(/^npm\s+run\s+build\b/, "building", () => ({ tool: "npm" })),
876
+ makeRule(/^(yarn|pnpm)\s+build\b/, "building", (match) => ({ tool: match[1] })),
877
+ makeRule(/^bun\s+run\s+build\b/, "building", () => ({ tool: "bun" })),
878
+ makeRule(/^(make|gmake)\b/, "building", (match) => ({ tool: match[1] })),
879
+ makeRule(/^cargo\s+build\b/, "building", () => ({ tool: "cargo" })),
880
+ makeRule(/^go\s+build\b/, "building", () => ({ tool: "go" })),
881
+ makeRule(/^docker\s+build\b/, "building", () => ({ tool: "docker" })),
882
+ makeRule(/^(mvn|gradle)\b/, "building", (match) => ({ tool: match[1] })),
883
+ // ─── Dev server / running ─────────────────────────────────────────────────
884
+ makeRule(/^npm\s+run\s+(dev|start|serve)\b/, "running", (_m, cmd) => ({
592
885
  tool: "npm",
593
886
  script: cmd.split(/\s+/)[2]
594
887
  })),
595
- makeRule(/^(yarn|pnpm)\s+(dev|start)\b/, "server_run", (m) => ({ tool: m[1], script: m[2] })),
596
- makeRule(/^bun\s+run\s+(dev|start|serve)\b/, "server_run", (_m, cmd) => ({
888
+ makeRule(/^(yarn|pnpm)\s+(dev|start)\b/, "running", (match) => ({ tool: match[1], script: match[2] })),
889
+ makeRule(/^bun\s+run\s+(dev|start|serve)\b/, "running", (_m, cmd) => ({
597
890
  tool: "bun",
598
891
  script: cmd.split(/\s+/)[2]
599
892
  })),
600
- makeRule(/^python\s+.*manage\.py\s+runserver\b/, "server_run", () => ({ tool: "django" })),
893
+ makeRule(/^python\s+.*manage\.py\s+runserver\b/, "running", () => ({ tool: "django" })),
601
894
  // ─── Infrastructure ───────────────────────────────────────────────────────
602
895
  makeRule(/^ssh\b/, "infrastructure", (_m, cmd) => {
603
896
  const parts = cmd.split(/\s+/);
@@ -611,7 +904,7 @@ var RULES = [
611
904
  const parts = cmd.split(/\s+/);
612
905
  return { tool: m[1], subcommand: parts[1] ?? null };
613
906
  }),
614
- makeRule(/^docker\s+(run|compose|ps|stop|start|rm|exec)\b/, "infrastructure", (m, cmd) => {
907
+ makeRule(/^docker\s+(run|compose|ps|stop|start|rm|exec)\b/, "infrastructure", (_m, cmd) => {
615
908
  const parts = cmd.split(/\s+/);
616
909
  return { tool: "docker", subcommand: parts[1] };
617
910
  }),
@@ -619,62 +912,76 @@ var RULES = [
619
912
  const parts = cmd.split(/\s+/);
620
913
  return { tool: m[1], subcommand: parts[1] ?? null };
621
914
  }),
915
+ // ─── Database ─────────────────────────────────────────────────────────────
916
+ makeRule(/^(psql|pgcli)\b/, "database", (m) => ({ tool: m[1] })),
917
+ makeRule(/^(mysql|mycli)\b/, "database", (m) => ({ tool: m[1] })),
918
+ makeRule(/^sqlite3\b/, "database", () => ({ tool: "sqlite3" })),
919
+ makeRule(/^(mongosh|mongo)\b/, "database", (m) => ({ tool: m[1] })),
920
+ makeRule(/^redis-cli\b/, "database", () => ({ tool: "redis-cli" })),
622
921
  // ─── npx ─────────────────────────────────────────────────────────────────
623
- makeRule(/^npx\s+(jest|vitest|mocha|ava|tap)\b/, "test_run", (_m, cmd) => ({
922
+ makeRule(/^npx\s+(jest|vitest|mocha|ava|tap)\b/, "testing", (_m, cmd) => ({
624
923
  tool: cmd.split(/\s+/)[1]
625
924
  })),
626
- makeRule(/^npx\s+tsc\b/, "build", () => ({ tool: "tsc" })),
627
- makeRule(/^npx\s+(eslint|tslint|biome)\b/, "lint_run", (_m, cmd) => ({
925
+ makeRule(/^npx\s+tsc\b/, "building", () => ({ tool: "tsc" })),
926
+ makeRule(/^npx\s+(eslint|tslint|biome)\b/, "linting", (_m, cmd) => ({
628
927
  tool: cmd.split(/\s+/)[1]
629
928
  })),
630
- makeRule(/^npx\s+(prettier|dprint)\b/, "lint_run", (_m, cmd) => ({
929
+ makeRule(/^npx\s+(prettier|dprint)\b/, "linting", (_m, cmd) => ({
631
930
  tool: cmd.split(/\s+/)[1]
632
931
  })),
633
- makeRule(/^npx\s+\S+/, "script_run", (_m, cmd) => ({
932
+ makeRule(/^npx\s+\S+/, "running", (_m, cmd) => ({
634
933
  tool: cmd.split(/\s+/)[1]
635
934
  })),
636
935
  // ─── Scripts ──────────────────────────────────────────────────────────────
637
- makeRule(/^(bash|sh|zsh|fish)\s+\S+/, "script_run", (m) => ({ shell: m[1] })),
638
- makeRule(/^node\s+\S+\.(?:js|mjs|cjs)\b/, "script_run", () => ({ runtime: "node" })),
639
- makeRule(/^python3?\s+\S+\.py\b/, "script_run", () => ({ runtime: "python" })),
640
- // ─── Environment ──────────────────────────────────────────────────────────
641
- makeRule(/^export\s+\w+=/, "environment", (_m, cmd) => {
642
- const varName = cmd.match(/^export\s+(\w+)=/)?.[1] ?? null;
643
- return { variable: varName };
644
- }),
645
- makeRule(/^source\s+/, "environment", () => ({})),
936
+ makeRule(/^(bash|sh|zsh|fish)\s+\S+/, "running", (match) => ({ shell: match[1] })),
937
+ makeRule(/^node\s+\S+\.(?:js|mjs|cjs)\b/, "running", () => ({ runtime: "node" })),
938
+ makeRule(/^python3?\s+\S+\.py\b/, "running", () => ({ runtime: "python" })),
939
+ // ─── Linting / code quality ───────────────────────────────────────────────
940
+ makeRule(/^(eslint|tslint|biome)\b/, "linting", (match) => ({ tool: match[1] })),
941
+ makeRule(/^(prettier|dprint)\b/, "linting", (match) => ({ tool: match[1] })),
646
942
  // ─── generic run scripts ──────────────────────────────────────────────────
647
- makeRule(/^npm\s+run\s+\S+/, "script_run", (_m, cmd) => ({
943
+ makeRule(/^npm\s+run\s+\S+/, "running", (_m, cmd) => ({
648
944
  tool: "npm",
649
945
  script: cmd.split(/\s+/)[2]
650
946
  })),
651
- makeRule(/^bun\s+run\s+\S+/, "script_run", (_m, cmd) => ({
947
+ makeRule(/^bun\s+run\s+\S+/, "running", (_m, cmd) => ({
652
948
  tool: "bun",
653
949
  script: cmd.split(/\s+/)[2]
654
950
  })),
655
951
  // ─── Code search ─────────────────────────────────────────────────────────
656
- makeRule(/^(grep|egrep|fgrep)\b/, "code_search", (m) => ({ tool: m[1] })),
657
- makeRule(/^(rg|ag|ack|ripgrep)\b/, "code_search", (m) => ({ tool: m[1] })),
658
- makeRule(/^find\s/, "code_search", () => ({ tool: "find" })),
952
+ makeRule(/^(grep|egrep|fgrep)\b/, "exploring", (match) => ({ tool: match[1] })),
953
+ makeRule(/^(rg|ag|ack|ripgrep)\b/, "exploring", (match) => ({ tool: match[1] })),
954
+ makeRule(/^find\s/, "exploring", () => ({ tool: "find" })),
659
955
  // ─── Code read ────────────────────────────────────────────────────────────
660
- makeRule(/^(cat|bat|less|more)\s+\S+/, "code_read", (m) => ({ tool: m[1] })),
661
- makeRule(/^(head|tail)\b/, "code_read", (m) => ({ tool: m[1] })),
956
+ makeRule(/^(cat|bat|less|more)\s+\S+/, "exploring", (match) => ({ tool: match[1] })),
957
+ makeRule(/^(head|tail)\b/, "exploring", (match) => ({ tool: match[1] })),
662
958
  // ─── File operations ──────────────────────────────────────────────────────
663
- makeRule(/^(cp|rsync)\b/, "file_ops", (m) => ({ tool: m[1] })),
664
- makeRule(/^(mv)\b/, "file_ops", () => ({ tool: "mv" })),
665
- makeRule(/^(rm|rmdir)\b/, "file_ops", (m) => ({ tool: m[1] })),
666
- makeRule(/^(mkdir|touch|ln)\b/, "file_ops", (m) => ({ tool: m[1] })),
667
- makeRule(/^(chmod|chown|chgrp)\b/, "file_ops", (m) => ({ tool: m[1] })),
668
- // ─── AI terminal tools (registry-driven) ─────────────────────────────────
669
- ...loadAIToolRegistry().flatMap((entry) => {
959
+ makeRule(/^(cp|rsync)\b/, "files", (match) => ({ tool: match[1] })),
960
+ makeRule(/^(mv)\b/, "files", () => ({ tool: "mv" })),
961
+ makeRule(/^(rm|rmdir)\b/, "files", (match) => ({ tool: match[1] })),
962
+ makeRule(/^(mkdir|touch|ln)\b/, "files", (match) => ({ tool: match[1] })),
963
+ makeRule(/^(chmod|chown|chgrp)\b/, "files", (match) => ({ tool: match[1] })),
964
+ // ─── Environment ──────────────────────────────────────────────────────────
965
+ makeRule(/^export\s+\w+=/, "files", (_m, cmd) => {
966
+ const varName = cmd.match(/^export\s+(\w+)=/)?.[1] ?? null;
967
+ return { variable: varName };
968
+ }),
969
+ makeRule(/^source\s+/, "files", () => ({}))
970
+ ];
971
+ function aiToolRules() {
972
+ return loadAIToolRegistry().flatMap((entry) => {
670
973
  try {
671
974
  return [makeRule(new RegExp(entry.pattern), "ai_session", () => ({ tool: entry.tool }))];
672
975
  } catch {
673
976
  debugLog("registry", `Skipping tool "${entry.tool}" \u2014 invalid pattern: ${entry.pattern}`);
674
977
  return [];
675
978
  }
676
- })
677
- ];
979
+ });
980
+ }
981
+ function buildRules(profileRules = []) {
982
+ return [...BASE_RULES, ...profileRules, ...aiToolRules()];
983
+ }
984
+ var RULES = buildRules(getActiveProfileRules());
678
985
  function normalizeCommand(command) {
679
986
  let trimmed = command.trim().replace(/\s+/g, " ");
680
987
  trimmed = trimmed.replace(/^(git)\s+-C\s+\S+\s+/, "$1 ");
@@ -685,87 +992,6 @@ function normalizeCommand(command) {
685
992
  return { category: "other", metadata: {} };
686
993
  }
687
994
 
688
- // src/config/index.ts
689
- var import_fs6 = __toESM(require("fs"));
690
- var DEFAULTS = {
691
- ai: {
692
- provider: "",
693
- apiKey: "",
694
- model: "",
695
- baseUrl: ""
696
- },
697
- session: {
698
- idleTimeout: 30
699
- },
700
- capture: {
701
- ignore: ["cd", "ls", "ll", "la", "ls -la", "ls -l", "ls -a", "pwd", "clear", "exit", "history", "man"],
702
- aiTools: true,
703
- includeAiderPrompt: true
704
- },
705
- redact: {
706
- extra: []
707
- },
708
- summary: {
709
- format: "markdown"
710
- }
711
- };
712
- function readConfig() {
713
- if (!import_fs6.default.existsSync(CONFIG_PATH)) return { ...DEFAULTS };
714
- try {
715
- const raw = import_fs6.default.readFileSync(CONFIG_PATH, "utf-8");
716
- const parsed = JSON.parse(raw);
717
- return deepMerge(DEFAULTS, parsed);
718
- } catch {
719
- return { ...DEFAULTS };
720
- }
721
- }
722
- function writeConfig(config) {
723
- import_fs6.default.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { encoding: "utf-8", mode: 384 });
724
- try {
725
- import_fs6.default.chmodSync(CONFIG_PATH, 384);
726
- } catch {
727
- }
728
- }
729
- function getConfigValue(config, key) {
730
- const parts = key.split(".");
731
- let cur = config;
732
- for (const part of parts) {
733
- if (cur == null || typeof cur !== "object") return void 0;
734
- cur = cur[part];
735
- }
736
- return cur;
737
- }
738
- function setConfigValue(config, key, value) {
739
- const parts = key.split(".");
740
- const result = JSON.parse(JSON.stringify(config));
741
- let cur = result;
742
- for (let i = 0; i < parts.length - 1; i++) {
743
- const part = parts[i];
744
- if (cur[part] == null || typeof cur[part] !== "object") {
745
- cur[part] = {};
746
- }
747
- cur = cur[part];
748
- }
749
- const lastKey = parts[parts.length - 1];
750
- if (value === "true") cur[lastKey] = true;
751
- else if (value === "false") cur[lastKey] = false;
752
- else if (!isNaN(Number(value))) cur[lastKey] = Number(value);
753
- else cur[lastKey] = value;
754
- return result;
755
- }
756
- function deepMerge(defaults, overrides) {
757
- const result = { ...defaults };
758
- for (const key of Object.keys(overrides)) {
759
- const val = overrides[key];
760
- if (val !== null && typeof val === "object" && !Array.isArray(val) && typeof defaults[key] === "object") {
761
- result[key] = deepMerge(defaults[key], val);
762
- } else if (val !== void 0) {
763
- result[key] = val;
764
- }
765
- }
766
- return result;
767
- }
768
-
769
995
  // src/capture/filter.ts
770
996
  function normalizeForFilter(cmd) {
771
997
  return cmd.trim().replace(/\s+/g, " ");
@@ -789,7 +1015,7 @@ function isDuplicate(command, cwd, lastEvent, dedupWindowMs = 5e3, now = Date.no
789
1015
 
790
1016
  // src/capture/index.ts
791
1017
  function captureCommand(opts) {
792
- if (import_fs7.default.existsSync(PAUSE_FLAG_PATH)) return;
1018
+ if (import_fs9.default.existsSync(PAUSE_FLAG_PATH)) return;
793
1019
  const config = readConfig();
794
1020
  const { command, cwd, exitCode, durationMs } = opts;
795
1021
  if (shouldFilter(command, config.capture.ignore)) return;
@@ -800,6 +1026,11 @@ function captureCommand(opts) {
800
1026
  const { repoName, branch } = getGitContext(cwd, nameOverride);
801
1027
  const { category, metadata } = normalizeCommand(safeCommand);
802
1028
  if (category === "ai_session" && config.capture.aiTools === false) return;
1029
+ if (category === "other") {
1030
+ const firstToken = safeCommand.trim().split(/\s+/)[0];
1031
+ if (firstToken) metadata.tool = firstToken;
1032
+ metadata.hint = safeCommand.trim().slice(0, 120);
1033
+ }
803
1034
  if (opts.source) metadata.source = opts.source;
804
1035
  if (category === "ai_session") {
805
1036
  if (opts.gitBefore) {
@@ -882,21 +1113,21 @@ function registerHook(program2) {
882
1113
  }
883
1114
 
884
1115
  // src/cli/init.ts
885
- var import_fs10 = __toESM(require("fs"));
886
- var import_path7 = __toESM(require("path"));
1116
+ var import_fs12 = __toESM(require("fs"));
1117
+ var import_path9 = __toESM(require("path"));
887
1118
  var import_os4 = __toESM(require("os"));
888
1119
  var import_prompts = __toESM(require("prompts"));
889
1120
  var import_chalk2 = __toESM(require("chalk"));
890
1121
 
891
1122
  // src/capture/shell-hook.ts
892
- var import_fs8 = __toESM(require("fs"));
893
- var import_path5 = __toESM(require("path"));
1123
+ var import_fs10 = __toESM(require("fs"));
1124
+ var import_path7 = __toESM(require("path"));
894
1125
  var HOOK_START = "# workscribe:hook:start";
895
1126
  var HOOK_END = "# workscribe:hook:end";
896
1127
  var HOOK_PREAMBLE = "# Workscribe \u2014 developer work journal";
897
1128
  function getInstalledHookBlock(rcFile) {
898
- if (!import_fs8.default.existsSync(rcFile)) return null;
899
- const content = import_fs8.default.readFileSync(rcFile, "utf-8");
1129
+ if (!import_fs10.default.existsSync(rcFile)) return null;
1130
+ const content = import_fs10.default.readFileSync(rcFile, "utf-8");
900
1131
  const startIdx = content.indexOf(HOOK_START);
901
1132
  const endIdx = content.indexOf(HOOK_END);
902
1133
  if (startIdx === -1 || endIdx === -1) return null;
@@ -912,38 +1143,38 @@ function isHookOutdated(rcFile, hookContent) {
912
1143
  }
913
1144
  function installHook(rcFile, hookContent) {
914
1145
  const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
915
- import_fs8.default.copyFileSync(rcFile, backup);
916
- log.info(`Backed up ${rcFile} \u2192 ${import_path5.default.basename(backup)}`);
917
- import_fs8.default.appendFileSync(rcFile, `
1146
+ import_fs10.default.copyFileSync(rcFile, backup);
1147
+ log.info(`Backed up ${rcFile} \u2192 ${import_path7.default.basename(backup)}`);
1148
+ import_fs10.default.appendFileSync(rcFile, `
918
1149
  ${hookContent}
919
1150
  `);
920
1151
  }
921
1152
  function updateHook(rcFile, hookContent) {
922
1153
  const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
923
- import_fs8.default.copyFileSync(rcFile, backup);
924
- log.info(`Backed up ${rcFile} \u2192 ${import_path5.default.basename(backup)}`);
925
- const content = import_fs8.default.readFileSync(rcFile, "utf-8");
1154
+ import_fs10.default.copyFileSync(rcFile, backup);
1155
+ log.info(`Backed up ${rcFile} \u2192 ${import_path7.default.basename(backup)}`);
1156
+ const content = import_fs10.default.readFileSync(rcFile, "utf-8");
926
1157
  const startIdx = content.indexOf(HOOK_START);
927
1158
  const endIdx = content.indexOf(HOOK_END) + HOOK_END.length;
928
- import_fs8.default.writeFileSync(rcFile, content.slice(0, startIdx) + hookContent + content.slice(endIdx));
1159
+ import_fs10.default.writeFileSync(rcFile, content.slice(0, startIdx) + hookContent + content.slice(endIdx));
929
1160
  }
930
1161
  function removeHook(rcFile) {
931
1162
  const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
932
- import_fs8.default.copyFileSync(rcFile, backup);
933
- log.info(`Backed up ${rcFile} \u2192 ${import_path5.default.basename(backup)}`);
934
- const content = import_fs8.default.readFileSync(rcFile, "utf-8");
1163
+ import_fs10.default.copyFileSync(rcFile, backup);
1164
+ log.info(`Backed up ${rcFile} \u2192 ${import_path7.default.basename(backup)}`);
1165
+ const content = import_fs10.default.readFileSync(rcFile, "utf-8");
935
1166
  const startIdx = content.indexOf(HOOK_START);
936
1167
  const endIdx = content.indexOf(HOOK_END) + HOOK_END.length;
937
1168
  const preambleIdx = content.lastIndexOf(HOOK_PREAMBLE, startIdx);
938
1169
  const blockStart = preambleIdx !== -1 ? preambleIdx : startIdx;
939
1170
  const before = content.slice(0, blockStart).replace(/\n+$/, "\n");
940
1171
  const after = content.slice(endIdx).replace(/^\n/, "");
941
- import_fs8.default.writeFileSync(rcFile, before + after);
1172
+ import_fs10.default.writeFileSync(rcFile, before + after);
942
1173
  }
943
1174
 
944
1175
  // src/capture/hooks/claude-code.ts
945
- var import_fs9 = __toESM(require("fs"));
946
- var import_path6 = __toESM(require("path"));
1176
+ var import_fs11 = __toESM(require("fs"));
1177
+ var import_path8 = __toESM(require("path"));
947
1178
  var import_os3 = __toESM(require("os"));
948
1179
 
949
1180
  // src/capture/hooks/utils.ts
@@ -958,11 +1189,11 @@ function isCommandInstalled(cmd) {
958
1189
  }
959
1190
 
960
1191
  // src/capture/hooks/claude-code.ts
961
- var CLAUDE_SETTINGS_PATH = import_path6.default.join(import_os3.default.homedir(), ".claude", "settings.json");
1192
+ var CLAUDE_SETTINGS_PATH = import_path8.default.join(import_os3.default.homedir(), ".claude", "settings.json");
962
1193
  function readSettings() {
963
- if (!import_fs9.default.existsSync(CLAUDE_SETTINGS_PATH)) return {};
1194
+ if (!import_fs11.default.existsSync(CLAUDE_SETTINGS_PATH)) return {};
964
1195
  try {
965
- return JSON.parse(import_fs9.default.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8"));
1196
+ return JSON.parse(import_fs11.default.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8"));
966
1197
  } catch {
967
1198
  return {};
968
1199
  }
@@ -977,8 +1208,8 @@ var claudeCodeIntegration = {
977
1208
  const postToolUse = settings.hooks?.PostToolUse ?? [];
978
1209
  return postToolUse.some((entry) => {
979
1210
  const hooks = entry.hooks ?? [];
980
- return hooks.some((h) => {
981
- const hook = h;
1211
+ return hooks.some((hookItem) => {
1212
+ const hook = hookItem;
982
1213
  return typeof hook.command === "string" && hook.command.includes("workscribe _hook");
983
1214
  });
984
1215
  });
@@ -997,8 +1228,8 @@ var claudeCodeIntegration = {
997
1228
  }
998
1229
  ]
999
1230
  };
1000
- import_fs9.default.mkdirSync(import_path6.default.dirname(CLAUDE_SETTINGS_PATH), { recursive: true });
1001
- import_fs9.default.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
1231
+ import_fs11.default.mkdirSync(import_path8.default.dirname(CLAUDE_SETTINGS_PATH), { recursive: true });
1232
+ import_fs11.default.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
1002
1233
  }
1003
1234
  };
1004
1235
 
@@ -1010,22 +1241,25 @@ var AI_HOOK_INTEGRATIONS = {
1010
1241
  // src/cli/init.ts
1011
1242
  function detectShell() {
1012
1243
  const shell = process.env.SHELL ?? "";
1013
- if (shell.includes("zsh")) return { name: "zsh", rcFile: import_path7.default.join(import_os4.default.homedir(), ".zshrc") };
1014
- if (shell.includes("bash")) return { name: "bash", rcFile: import_path7.default.join(import_os4.default.homedir(), ".bashrc") };
1244
+ if (shell.includes("zsh")) return { name: "zsh", rcFile: import_path9.default.join(import_os4.default.homedir(), ".zshrc") };
1245
+ if (shell.includes("bash")) return { name: "bash", rcFile: import_path9.default.join(import_os4.default.homedir(), ".bashrc") };
1015
1246
  return null;
1016
1247
  }
1017
1248
  function registerInit(program2) {
1018
1249
  program2.command("init").description("Set up Workscribe and install or update shell hooks").action(async () => {
1019
1250
  log.plain(import_chalk2.default.bold("\n Workscribe \u2014 Developer Work Journal\n"));
1020
- if (!import_fs10.default.existsSync(WORKSCRIBE_DIR)) {
1021
- import_fs10.default.mkdirSync(WORKSCRIBE_DIR, { recursive: true, mode: 448 });
1251
+ if (!import_fs12.default.existsSync(WORKSCRIBE_DIR)) {
1252
+ import_fs12.default.mkdirSync(WORKSCRIBE_DIR, { recursive: true, mode: 448 });
1022
1253
  log.success(`Created ${WORKSCRIBE_DIR}`);
1023
1254
  } else {
1024
1255
  try {
1025
- import_fs10.default.chmodSync(WORKSCRIBE_DIR, 448);
1256
+ import_fs12.default.chmodSync(WORKSCRIBE_DIR, 448);
1026
1257
  } catch {
1027
1258
  }
1028
1259
  }
1260
+ if (!import_fs12.default.existsSync(PROFILES_DIR)) {
1261
+ import_fs12.default.mkdirSync(PROFILES_DIR, { recursive: true, mode: 448 });
1262
+ }
1029
1263
  getDb();
1030
1264
  log.success("Database initialized");
1031
1265
  const shell = detectShell();
@@ -1033,12 +1267,12 @@ function registerInit(program2) {
1033
1267
  log.warn("Could not detect shell. Supported: zsh, bash");
1034
1268
  log.plain(`Manually add the hook from: ${import_chalk2.default.cyan("shell/hook.zsh")} or ${import_chalk2.default.cyan("shell/hook.bash")}`);
1035
1269
  } else {
1036
- const hookFile = import_path7.default.join(__dirname, "..", "shell", `hook.${shell.name}`);
1037
- if (!import_fs10.default.existsSync(hookFile)) {
1270
+ const hookFile = import_path9.default.join(__dirname, "..", "shell", `hook.${shell.name}`);
1271
+ if (!import_fs12.default.existsSync(hookFile)) {
1038
1272
  log.warn(`Hook template not found at ${hookFile}`);
1039
1273
  } else {
1040
- const hookContent = import_fs10.default.readFileSync(hookFile, "utf-8");
1041
- if (!import_fs10.default.existsSync(shell.rcFile)) import_fs10.default.writeFileSync(shell.rcFile, "");
1274
+ const hookContent = import_fs12.default.readFileSync(hookFile, "utf-8");
1275
+ if (!import_fs12.default.existsSync(shell.rcFile)) import_fs12.default.writeFileSync(shell.rcFile, "");
1042
1276
  if (!isHookInstalled(shell.rcFile)) {
1043
1277
  installHook(shell.rcFile, hookContent);
1044
1278
  log.success(`Shell hook installed in ${shell.rcFile}`);
@@ -1061,7 +1295,7 @@ function registerInit(program2) {
1061
1295
  }
1062
1296
  }
1063
1297
  const installedTools = loadAIToolRegistry().filter(
1064
- (t) => t.hook !== null && AI_HOOK_INTEGRATIONS[t.hook]?.isInstalled()
1298
+ (entry) => entry.hook !== null && AI_HOOK_INTEGRATIONS[entry.hook]?.isInstalled()
1065
1299
  );
1066
1300
  if (installedTools.length === 0) {
1067
1301
  log.info("No AI coding tools detected \u2014 re-run workscribe init after installing one");
@@ -1087,6 +1321,33 @@ function registerInit(program2) {
1087
1321
  }
1088
1322
  }
1089
1323
  const config = readConfig();
1324
+ try {
1325
+ await cacheManifest();
1326
+ const detected = detectProfilesForDirectory(process.cwd());
1327
+ const alreadyActive = new Set(config.capture.profiles);
1328
+ const toPrompt = detected.filter((entry) => !alreadyActive.has(entry.name));
1329
+ if (toPrompt.length > 0) {
1330
+ log.plain("");
1331
+ for (const entry of toPrompt) {
1332
+ const { install } = await (0, import_prompts.default)({
1333
+ type: "confirm",
1334
+ name: "install",
1335
+ message: `${entry.displayName} project detected. Enable the ${entry.displayName} profile?`,
1336
+ initial: true
1337
+ });
1338
+ if (install) {
1339
+ await installProfile(entry.name);
1340
+ config.capture.profiles = [.../* @__PURE__ */ new Set([...config.capture.profiles, entry.name])];
1341
+ log.success(`${entry.displayName} profile enabled`);
1342
+ }
1343
+ }
1344
+ writeConfig(config);
1345
+ } else if (detected.length === 0) {
1346
+ log.info("No project profiles detected \u2014 run workscribe profile detect from your project directory");
1347
+ }
1348
+ } catch {
1349
+ log.info("Could not reach profile registry \u2014 run workscribe profile detect after connecting");
1350
+ }
1090
1351
  if (config.ai.provider) {
1091
1352
  log.info(`AI provider already configured: ${import_chalk2.default.cyan(config.ai.provider)}`);
1092
1353
  } else {
@@ -1134,7 +1395,7 @@ function registerInit(program2) {
1134
1395
  log.info("Skipped \u2014 run later: workscribe config set ai.provider <provider>");
1135
1396
  }
1136
1397
  }
1137
- if (!import_fs10.default.existsSync(CONFIG_PATH)) {
1398
+ if (!import_fs12.default.existsSync(CONFIG_PATH)) {
1138
1399
  writeConfig(config);
1139
1400
  log.success("Config file created");
1140
1401
  }
@@ -1153,7 +1414,7 @@ function registerInit(program2) {
1153
1414
  }
1154
1415
 
1155
1416
  // src/cli/status.ts
1156
- var import_fs11 = __toESM(require("fs"));
1417
+ var import_fs13 = __toESM(require("fs"));
1157
1418
  var import_chalk3 = __toESM(require("chalk"));
1158
1419
 
1159
1420
  // src/utils/format.ts
@@ -1211,7 +1472,7 @@ function registerStatus(program2) {
1211
1472
  const total = getEventCount();
1212
1473
  const todayEvents = getEventsForDate(todayDate());
1213
1474
  const last = getLastEvent();
1214
- const isPaused = import_fs11.default.existsSync(PAUSE_FLAG_PATH);
1475
+ const isPaused = import_fs13.default.existsSync(PAUSE_FLAG_PATH);
1215
1476
  log.plain("");
1216
1477
  log.plain(import_chalk3.default.bold(" Workscribe Status"));
1217
1478
  log.plain("");
@@ -1230,6 +1491,25 @@ function registerStatus(program2) {
1230
1491
  ` AI provider ${config.ai.provider ? import_chalk3.default.cyan(config.ai.provider) : import_chalk3.default.yellow("not configured")}`
1231
1492
  );
1232
1493
  if (config.ai.model) log.plain(` Model ${import_chalk3.default.dim(config.ai.model)}`);
1494
+ const activeProfiles = config.capture.profiles;
1495
+ log.plain(
1496
+ ` Profiles ${activeProfiles.length > 0 ? import_chalk3.default.cyan(activeProfiles.join(", ")) : import_chalk3.default.dim("none")}`
1497
+ );
1498
+ const { total: weekTotal, otherCount } = getRecentOtherRate(7);
1499
+ if (weekTotal > 0) {
1500
+ const otherPct = Math.round(otherCount / weekTotal * 100);
1501
+ if (otherPct >= 25) {
1502
+ log.plain("");
1503
+ log.plain(
1504
+ ` ${import_chalk3.default.yellow("\u26A0")} ${otherCount} of ${weekTotal} events (${otherPct}%) are uncategorised this week.`
1505
+ );
1506
+ log.plain(
1507
+ import_chalk3.default.dim(
1508
+ " Run workscribe profile detect \u2014 scan current directory for matching profiles\n Run workscribe profile suggest \u2014 analyse your command history"
1509
+ )
1510
+ );
1511
+ }
1512
+ }
1233
1513
  log.plain("");
1234
1514
  });
1235
1515
  }
@@ -1237,30 +1517,23 @@ function registerStatus(program2) {
1237
1517
  // src/cli/events.ts
1238
1518
  var import_chalk4 = __toESM(require("chalk"));
1239
1519
  var CATEGORY_COLORS = {
1240
- code_commit: import_chalk4.default.green,
1241
- code_push: import_chalk4.default.green,
1242
- code_pull: import_chalk4.default.blue,
1243
- branch_switch: import_chalk4.default.blue,
1244
- dependency_install: import_chalk4.default.yellow,
1245
- dependency_remove: import_chalk4.default.yellow,
1246
- test_run: import_chalk4.default.magenta,
1247
- build: import_chalk4.default.cyan,
1248
- server_run: import_chalk4.default.cyan,
1520
+ shipping: import_chalk4.default.green,
1521
+ versioning: import_chalk4.default.blue,
1522
+ building: import_chalk4.default.cyan,
1523
+ running: import_chalk4.default.white,
1524
+ testing: import_chalk4.default.magenta,
1525
+ linting: import_chalk4.default.yellow,
1526
+ dependencies: import_chalk4.default.yellow,
1249
1527
  infrastructure: import_chalk4.default.red,
1250
- script_run: import_chalk4.default.white,
1251
- environment: import_chalk4.default.gray,
1252
- git_other: import_chalk4.default.blue,
1253
- code_merge: import_chalk4.default.green,
1254
- code_stash: import_chalk4.default.blue,
1528
+ releasing: import_chalk4.default.bold.green,
1529
+ database: import_chalk4.default.bold.blue,
1530
+ files: import_chalk4.default.dim.yellow,
1531
+ exploring: import_chalk4.default.dim.cyan,
1255
1532
  ai_session: import_chalk4.default.bold.magenta,
1256
- lint_run: import_chalk4.default.yellow,
1257
- code_search: import_chalk4.default.dim.cyan,
1258
- code_read: import_chalk4.default.dim.white,
1259
- file_ops: import_chalk4.default.yellow,
1260
1533
  other: import_chalk4.default.gray
1261
1534
  };
1262
1535
  var AI_TOOL_DISPLAY_NAMES = Object.fromEntries(
1263
- loadAIToolRegistry().map((t) => [t.tool, t.displayName])
1536
+ loadAIToolRegistry().map((entry) => [entry.tool, entry.displayName])
1264
1537
  );
1265
1538
  function registerEvents(program2) {
1266
1539
  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) => {
@@ -1352,18 +1625,18 @@ function getOrBuildSessions(date) {
1352
1625
  const existing = getSessionsForDate(date);
1353
1626
  const events = getEventsForDate(date);
1354
1627
  if (events.length === 0) return [];
1355
- const cachedEventCount = existing.reduce((sum, s) => sum + s.event_count, 0);
1628
+ const cachedEventCount = existing.reduce((sum, sessionRow) => sum + sessionRow.event_count, 0);
1356
1629
  if (existing.length > 0 && cachedEventCount === events.length) {
1357
- return existing.map((s) => ({
1358
- id: s.id,
1359
- date: s.date,
1360
- startedAt: s.started_at,
1361
- endedAt: s.ended_at,
1362
- repoName: s.repo_name,
1363
- durationMinutes: minutesBetween(s.started_at, s.ended_at),
1364
- eventCount: s.event_count,
1365
- summary: s.summary,
1366
- events: getEventsBySessionId(s.id)
1630
+ return existing.map((sessionRow) => ({
1631
+ id: sessionRow.id,
1632
+ date: sessionRow.date,
1633
+ startedAt: sessionRow.started_at,
1634
+ endedAt: sessionRow.ended_at,
1635
+ repoName: sessionRow.repo_name,
1636
+ durationMinutes: minutesBetween(sessionRow.started_at, sessionRow.ended_at),
1637
+ eventCount: sessionRow.event_count,
1638
+ summary: sessionRow.summary,
1639
+ events: getEventsBySessionId(sessionRow.id)
1367
1640
  }));
1368
1641
  }
1369
1642
  const savedSummaries = /* @__PURE__ */ new Map();
@@ -1449,9 +1722,9 @@ function registerSessions(program2) {
1449
1722
 
1450
1723
  // src/cli/summary.ts
1451
1724
  var import_chalk6 = __toESM(require("chalk"));
1452
- var import_fs12 = __toESM(require("fs"));
1725
+ var import_fs14 = __toESM(require("fs"));
1453
1726
  var import_os5 = __toESM(require("os"));
1454
- var import_path8 = __toESM(require("path"));
1727
+ var import_path10 = __toESM(require("path"));
1455
1728
 
1456
1729
  // src/ai/prompt.ts
1457
1730
  var SYSTEM_PROMPT = `You write short developer work summaries. Here is an example:
@@ -1461,27 +1734,34 @@ Project: api-service
1461
1734
  Branch: feature/auth
1462
1735
  Duration: 2h 00m
1463
1736
  Events:
1464
- - branch_switch {"branch":"feature/auth"}
1465
- - 2x dependency_install {"tool":"npm","packages":["bcrypt","jsonwebtoken"]}
1466
- - 2x test_run {"tool":"jest"} [failed]
1467
- - test_run {"tool":"jest"}
1468
- - 2x file_ops {"tool":"mkdir"}
1469
- - code_commit {"message":"fix: auth token validation"}
1470
- - code_push {"remote":"origin","branch":"feature/auth"}
1737
+ - versioning {"branch":"feature/auth"}
1738
+ - 2x dependencies {"tool":"npm","packages":["bcrypt","jsonwebtoken"]}
1739
+ - 2x testing {"tool":"jest"} [failed]
1740
+ - testing {"tool":"jest"}
1741
+ - 2x files {"tool":"mkdir"}
1742
+ - shipping {"message":"fix: auth token validation"}
1743
+ - shipping {"remote":"origin","branch":"feature/auth"}
1471
1744
 
1472
1745
  OUTPUT:
1473
1746
  - Switched to the authentication feature branch and added authentication dependencies (bcrypt, JWT)
1474
1747
  - Resolved failing auth tests
1475
1748
  - Committed and pushed authentication fixes
1476
1749
 
1477
- For dependency_install events: infer the purpose of packages from their names and describe the domain, not just the packages. Examples: bcrypt/jsonwebtoken \u2192 authentication, prisma/mongoose/pg \u2192 database, jest/vitest/mocha \u2192 testing, axios/got/node-fetch \u2192 HTTP client, stripe/paypal \u2192 payments, socket.io/ws \u2192 real-time. Use your knowledge of common packages. For unfamiliar packages, name them directly without guessing their domain.
1750
+ For dependencies events with a packages field: infer the purpose of packages from their names and describe the domain, not just the packages. Examples: bcrypt/jsonwebtoken \u2192 authentication, prisma/mongoose/pg \u2192 database, jest/vitest/mocha \u2192 testing, axios/got/node-fetch \u2192 HTTP client, stripe/paypal \u2192 payments, socket.io/ws \u2192 real-time. Use your knowledge of common packages. For unfamiliar packages, name them directly without guessing their domain.
1478
1751
 
1479
- 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).
1752
+ Events like shipping (pushes), building, versioning (branch switches, stash), files (file ops, env vars), and infrastructure 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, a deploy, or a significant restructure with no accompanying commits).
1480
1753
 
1481
1754
  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.
1482
1755
 
1756
+ For other events with a hint field: interpret the command snippet and describe the activity in plain language. Do not quote the raw command.
1757
+
1483
1758
  Maximum 5 bullets. Output ONLY the bullet points, nothing else.`;
1484
- var EXCLUDED = /* @__PURE__ */ new Set(["environment", "code_search", "code_read", "server_run", "git_other"]);
1759
+ function getSystemPrompt() {
1760
+ const contexts = getActiveProfileAiContexts();
1761
+ if (contexts.length === 0) return SYSTEM_PROMPT;
1762
+ return SYSTEM_PROMPT + "\n\n" + contexts.join("\n");
1763
+ }
1764
+ var EXCLUDED = /* @__PURE__ */ new Set(["exploring"]);
1485
1765
  function buildUserMessage(payload) {
1486
1766
  const lines = [`Date: ${payload.date}`, ""];
1487
1767
  for (const session of payload.sessions) {
@@ -1492,9 +1772,14 @@ function buildUserMessage(payload) {
1492
1772
  const counts = /* @__PURE__ */ new Map();
1493
1773
  for (const event of session.events) {
1494
1774
  if (EXCLUDED.has(event.category)) continue;
1495
- const meta = Object.keys(event.metadata).length > 0 ? ` ${JSON.stringify(event.metadata)}` : "";
1496
1775
  const failed = event.exitCode !== null && event.exitCode !== 0 ? " [failed]" : "";
1497
- const key = `${event.category}${meta}${failed}`;
1776
+ let key;
1777
+ if (event.hint) {
1778
+ key = `other [${event.hint}]${failed}`;
1779
+ } else {
1780
+ const meta = Object.keys(event.metadata).length > 0 ? ` ${JSON.stringify(event.metadata)}` : "";
1781
+ key = `${event.category}${meta}${failed}`;
1782
+ }
1498
1783
  counts.set(key, (counts.get(key) ?? 0) + 1);
1499
1784
  }
1500
1785
  for (const [key, count] of counts) {
@@ -1549,9 +1834,14 @@ function buildWeeklyMessage(payload) {
1549
1834
  const counts = /* @__PURE__ */ new Map();
1550
1835
  for (const e of s.events) {
1551
1836
  if (EXCLUDED.has(e.category)) continue;
1552
- const meta = Object.keys(e.metadata).length > 0 ? ` ${JSON.stringify(e.metadata)}` : "";
1553
1837
  const failed = e.exitCode !== null && e.exitCode !== 0 ? " [failed]" : "";
1554
- const key = `${e.category}${meta}${failed}`;
1838
+ let key;
1839
+ if (e.hint) {
1840
+ key = `other [${e.hint}]${failed}`;
1841
+ } else {
1842
+ const meta = Object.keys(e.metadata).length > 0 ? ` ${JSON.stringify(e.metadata)}` : "";
1843
+ key = `${e.category}${meta}${failed}`;
1844
+ }
1555
1845
  counts.set(key, (counts.get(key) ?? 0) + 1);
1556
1846
  }
1557
1847
  const eventLine = [...counts.entries()].map(([k, n]) => `${n > 1 ? `${n}x ` : ""}${k}`).join(", ");
@@ -1598,7 +1888,7 @@ var AnthropicProvider = class {
1598
1888
  body: JSON.stringify({
1599
1889
  model: this.model,
1600
1890
  max_tokens: 1024,
1601
- system: SYSTEM_PROMPT,
1891
+ system: getSystemPrompt(),
1602
1892
  messages: [{ role: "user", content: buildUserMessage(payload) }]
1603
1893
  })
1604
1894
  });
@@ -1607,7 +1897,7 @@ var AnthropicProvider = class {
1607
1897
  throw new Error(`Anthropic API error ${response.status}: ${body}`);
1608
1898
  }
1609
1899
  const data = await response.json();
1610
- return data.content.find((c) => c.type === "text")?.text ?? "";
1900
+ return data.content.find((block) => block.type === "text")?.text ?? "";
1611
1901
  }
1612
1902
  async summarizeWeek(payload) {
1613
1903
  const response = await fetchWithTimeout("https://api.anthropic.com/v1/messages", {
@@ -1629,7 +1919,7 @@ var AnthropicProvider = class {
1629
1919
  throw new Error(`Anthropic API error ${response.status}: ${body}`);
1630
1920
  }
1631
1921
  const data = await response.json();
1632
- return data.content.find((c) => c.type === "text")?.text ?? "";
1922
+ return data.content.find((block) => block.type === "text")?.text ?? "";
1633
1923
  }
1634
1924
  };
1635
1925
 
@@ -1653,7 +1943,7 @@ var OpenAIProvider = class {
1653
1943
  body: JSON.stringify({
1654
1944
  model: this.model,
1655
1945
  messages: [
1656
- { role: "system", content: SYSTEM_PROMPT },
1946
+ { role: "system", content: getSystemPrompt() },
1657
1947
  { role: "user", content: buildUserMessage(payload) }
1658
1948
  ],
1659
1949
  max_tokens: 1024
@@ -1707,7 +1997,7 @@ var OllamaProvider = class {
1707
1997
  model: this.model,
1708
1998
  stream: false,
1709
1999
  messages: [
1710
- { role: "system", content: SYSTEM_PROMPT },
2000
+ { role: "system", content: getSystemPrompt() },
1711
2001
  { role: "user", content: buildUserMessage(payload) }
1712
2002
  ],
1713
2003
  options: {
@@ -1815,7 +2105,7 @@ function isSlackWebhookUrl(url) {
1815
2105
 
1816
2106
  // src/cli/summary.ts
1817
2107
  function parseBullets(text) {
1818
- return text.split("\n").map((l) => l.replace(/^[•\-]\s*/, "").trim()).filter(Boolean);
2108
+ return text.split("\n").map((line) => line.replace(/^[•\-]\s*/, "").trim()).filter(Boolean);
1819
2109
  }
1820
2110
  function eventRowToPayload(row) {
1821
2111
  let metadata = {};
@@ -1823,12 +2113,18 @@ function eventRowToPayload(row) {
1823
2113
  if (row.metadata) metadata = JSON.parse(row.metadata);
1824
2114
  } catch {
1825
2115
  }
1826
- return {
2116
+ const payload = {
1827
2117
  category: row.category,
1828
2118
  metadata,
1829
2119
  exitCode: row.exit_code,
1830
2120
  durationMs: row.duration_ms
1831
2121
  };
2122
+ if (row.category === "other" && typeof metadata.hint === "string") {
2123
+ payload.hint = metadata.hint;
2124
+ const { hint: _h, tool: _t, ...rest } = metadata;
2125
+ payload.metadata = rest;
2126
+ }
2127
+ return payload;
1832
2128
  }
1833
2129
  function sessionToPayload(s, date) {
1834
2130
  return {
@@ -1875,7 +2171,7 @@ function registerSummary(program2) {
1875
2171
  for (const day of days) {
1876
2172
  const sessions2 = getOrBuildSessions(day);
1877
2173
  if (sessions2.length > 0) {
1878
- weeklyDays.push({ date: day, sessions: sessions2.map((s) => sessionToPayload(s, day)) });
2174
+ weeklyDays.push({ date: day, sessions: sessions2.map((session) => sessionToPayload(session, day)) });
1879
2175
  }
1880
2176
  }
1881
2177
  if (weeklyDays.length === 0) {
@@ -1883,9 +2179,9 @@ function registerSummary(program2) {
1883
2179
  return;
1884
2180
  }
1885
2181
  const cacheKey = isoWeekKey(today);
1886
- const cacheFile = import_path8.default.join(WEEKLY_CACHE_DIR, `${cacheKey}.md`);
1887
- if (!opts.fresh && import_fs12.default.existsSync(cacheFile)) {
1888
- const cached = import_fs12.default.readFileSync(cacheFile, "utf-8");
2182
+ const cacheFile = import_path10.default.join(WEEKLY_CACHE_DIR, `${cacheKey}.md`);
2183
+ if (!opts.fresh && import_fs14.default.existsSync(cacheFile)) {
2184
+ const cached = import_fs14.default.readFileSync(cacheFile, "utf-8");
1889
2185
  log.plain("");
1890
2186
  log.plain(cached);
1891
2187
  log.plain("");
@@ -1897,12 +2193,12 @@ function registerSummary(program2) {
1897
2193
  log.plain("");
1898
2194
  log.info(`Generating weekly summary via ${import_chalk6.default.cyan(config.ai.provider)}\u2026`);
1899
2195
  try {
1900
- const summary = provider.summarizeWeek ? await provider.summarizeWeek(payload) : await provider.summarize({ date: today, sessions: weeklyDays.flatMap((d) => d.sessions) });
2196
+ const summary = provider.summarizeWeek ? await provider.summarizeWeek(payload) : await provider.summarize({ date: today, sessions: weeklyDays.flatMap((day) => day.sessions) });
1901
2197
  log.plain("");
1902
2198
  log.plain(summary);
1903
2199
  log.plain("");
1904
- import_fs12.default.mkdirSync(WEEKLY_CACHE_DIR, { recursive: true });
1905
- import_fs12.default.writeFileSync(cacheFile, summary, { encoding: "utf-8", mode: 384 });
2200
+ import_fs14.default.mkdirSync(WEEKLY_CACHE_DIR, { recursive: true });
2201
+ import_fs14.default.writeFileSync(cacheFile, summary, { encoding: "utf-8", mode: 384 });
1906
2202
  await handleWebhook(opts, config, summary);
1907
2203
  } catch (e) {
1908
2204
  log.error(`Weekly summary failed [${config.ai.provider}]: ${e.message}`);
@@ -1926,16 +2222,16 @@ function registerSummary(program2) {
1926
2222
  }
1927
2223
  const jsonMode = config.summary?.format === "json";
1928
2224
  if (!opts.fresh) {
1929
- const allCached = sessions.every((s) => s.summary);
2225
+ const allCached = sessions.every((session) => session.summary);
1930
2226
  if (allCached) {
1931
- const fullSummary2 = sessions.map((s) => s.summary).join("\n\n");
2227
+ const fullSummary2 = sessions.map((session) => session.summary).join("\n\n");
1932
2228
  log.plain("");
1933
2229
  if (jsonMode) {
1934
- const jsonOut = sessions.map((s) => ({
1935
- project: s.repoName,
1936
- branch: s.events[0]?.branch ?? null,
1937
- durationMinutes: s.durationMinutes,
1938
- bullets: parseBullets(s.summary.replace(/^## .+\n/, ""))
2230
+ const jsonOut = sessions.map((session) => ({
2231
+ project: session.repoName,
2232
+ branch: session.events[0]?.branch ?? null,
2233
+ durationMinutes: session.durationMinutes,
2234
+ bullets: parseBullets(session.summary.replace(/^## .+\n/, ""))
1939
2235
  }));
1940
2236
  log.plain(JSON.stringify({ date, sessions: jsonOut }, null, 2));
1941
2237
  } else {
@@ -1989,15 +2285,26 @@ ${bullets.trim()}`;
1989
2285
  }
1990
2286
 
1991
2287
  // src/cli/export.ts
1992
- var import_fs13 = __toESM(require("fs"));
1993
- var import_path9 = __toESM(require("path"));
2288
+ var import_fs15 = __toESM(require("fs"));
2289
+ var import_path11 = __toESM(require("path"));
1994
2290
  function eventRowToPayload2(row) {
1995
2291
  let metadata = {};
1996
2292
  try {
1997
2293
  if (row.metadata) metadata = JSON.parse(row.metadata);
1998
2294
  } catch {
1999
2295
  }
2000
- return { category: row.category, metadata, exitCode: row.exit_code, durationMs: row.duration_ms };
2296
+ const payload = {
2297
+ category: row.category,
2298
+ metadata,
2299
+ exitCode: row.exit_code,
2300
+ durationMs: row.duration_ms
2301
+ };
2302
+ if (row.category === "other" && typeof metadata.hint === "string") {
2303
+ payload.hint = metadata.hint;
2304
+ const { hint: _h, tool: _t, ...rest } = metadata;
2305
+ payload.metadata = rest;
2306
+ }
2307
+ return payload;
2001
2308
  }
2002
2309
  function registerExport(program2) {
2003
2310
  program2.command("export").description("Export work summary as a markdown file").option("--date <YYYY-MM-DD>", "date to export (default: today)").option("--output <path>", "output file path (default: ~/.workscribe/exports/)").action(async (opts) => {
@@ -2023,21 +2330,21 @@ function registerExport(program2) {
2023
2330
  log.error(e.message);
2024
2331
  process.exit(1);
2025
2332
  }
2026
- const sessionPayloads = sessions.map((s) => ({
2333
+ const sessionPayloads = sessions.map((session) => ({
2027
2334
  date,
2028
- project: s.repoName,
2029
- branch: s.events[0]?.branch ?? null,
2030
- startTime: formatDate(s.startedAt),
2031
- endTime: formatDate(s.endedAt),
2032
- durationMinutes: s.durationMinutes,
2033
- events: s.events.map(eventRowToPayload2)
2335
+ project: session.repoName,
2336
+ branch: session.events[0]?.branch ?? null,
2337
+ startTime: formatDate(session.startedAt),
2338
+ endTime: formatDate(session.endedAt),
2339
+ durationMinutes: session.durationMinutes,
2340
+ events: session.events.map(eventRowToPayload2)
2034
2341
  }));
2035
2342
  const payload = { date, sessions: sessionPayloads };
2036
2343
  log.info(`Generating summary via ${config.ai.provider}\u2026`);
2037
2344
  const summary = await provider.summarize(payload);
2038
- const outputPath = opts.output ?? import_path9.default.join(EXPORTS_DIR, `workscribe-${date}.md`);
2039
- if (!import_fs13.default.existsSync(EXPORTS_DIR)) import_fs13.default.mkdirSync(EXPORTS_DIR, { recursive: true, mode: 448 });
2040
- import_fs13.default.writeFileSync(outputPath, `# Work Summary \u2014 ${date}
2345
+ const outputPath = opts.output ?? import_path11.default.join(EXPORTS_DIR, `workscribe-${date}.md`);
2346
+ if (!import_fs15.default.existsSync(EXPORTS_DIR)) import_fs15.default.mkdirSync(EXPORTS_DIR, { recursive: true, mode: 448 });
2347
+ import_fs15.default.writeFileSync(outputPath, `# Work Summary \u2014 ${date}
2041
2348
 
2042
2349
  ${summary}
2043
2350
  `, { encoding: "utf-8", mode: 384 });
@@ -2109,22 +2416,22 @@ function printConfig(obj, indent) {
2109
2416
  }
2110
2417
 
2111
2418
  // src/cli/pause.ts
2112
- var import_fs14 = __toESM(require("fs"));
2419
+ var import_fs16 = __toESM(require("fs"));
2113
2420
  function registerPause(program2) {
2114
2421
  program2.command("pause").description("Pause capture temporarily").action(() => {
2115
- if (import_fs14.default.existsSync(PAUSE_FLAG_PATH)) {
2422
+ if (import_fs16.default.existsSync(PAUSE_FLAG_PATH)) {
2116
2423
  log.info("Capture is already paused. Run workscribe resume to restart.");
2117
2424
  return;
2118
2425
  }
2119
- import_fs14.default.writeFileSync(PAUSE_FLAG_PATH, "", { mode: 384 });
2426
+ import_fs16.default.writeFileSync(PAUSE_FLAG_PATH, "", { mode: 384 });
2120
2427
  log.success("Capture paused. Run workscribe resume to restart.");
2121
2428
  });
2122
2429
  program2.command("resume").description("Resume capture after a pause").action(() => {
2123
- if (!import_fs14.default.existsSync(PAUSE_FLAG_PATH)) {
2430
+ if (!import_fs16.default.existsSync(PAUSE_FLAG_PATH)) {
2124
2431
  log.info("Capture is not paused.");
2125
2432
  return;
2126
2433
  }
2127
- import_fs14.default.rmSync(PAUSE_FLAG_PATH);
2434
+ import_fs16.default.rmSync(PAUSE_FLAG_PATH);
2128
2435
  log.success("Capture resumed.");
2129
2436
  });
2130
2437
  }
@@ -2209,18 +2516,18 @@ function registerClear(program2) {
2209
2516
  }
2210
2517
 
2211
2518
  // src/cli/uninstall.ts
2212
- var import_fs15 = __toESM(require("fs"));
2213
- var import_path10 = __toESM(require("path"));
2519
+ var import_fs17 = __toESM(require("fs"));
2520
+ var import_path12 = __toESM(require("path"));
2214
2521
  var import_os6 = __toESM(require("os"));
2215
2522
  var import_prompts3 = __toESM(require("prompts"));
2216
2523
  var import_chalk9 = __toESM(require("chalk"));
2217
2524
  function detectShellRcFiles() {
2218
2525
  const candidates = [
2219
- { name: "zsh", rcFile: import_path10.default.join(import_os6.default.homedir(), ".zshrc") },
2220
- { name: "bash", rcFile: import_path10.default.join(import_os6.default.homedir(), ".bashrc") },
2221
- { name: "bash", rcFile: import_path10.default.join(import_os6.default.homedir(), ".bash_profile") }
2526
+ { name: "zsh", rcFile: import_path12.default.join(import_os6.default.homedir(), ".zshrc") },
2527
+ { name: "bash", rcFile: import_path12.default.join(import_os6.default.homedir(), ".bashrc") },
2528
+ { name: "bash", rcFile: import_path12.default.join(import_os6.default.homedir(), ".bash_profile") }
2222
2529
  ];
2223
- return candidates.filter((c) => import_fs15.default.existsSync(c.rcFile) && isHookInstalled(c.rcFile));
2530
+ return candidates.filter((candidate) => import_fs17.default.existsSync(candidate.rcFile) && isHookInstalled(candidate.rcFile));
2224
2531
  }
2225
2532
  function registerUninstall(program2) {
2226
2533
  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) => {
@@ -2239,7 +2546,7 @@ function registerUninstall(program2) {
2239
2546
  }
2240
2547
  }
2241
2548
  if (opts.hooksOnly) return;
2242
- if (import_fs15.default.existsSync(WORKSCRIBE_DIR)) {
2549
+ if (import_fs17.default.existsSync(WORKSCRIBE_DIR)) {
2243
2550
  log.plain("");
2244
2551
  const { deleteData } = await (0, import_prompts3.default)({
2245
2552
  type: "confirm",
@@ -2248,7 +2555,7 @@ function registerUninstall(program2) {
2248
2555
  initial: false
2249
2556
  });
2250
2557
  if (deleteData) {
2251
- import_fs15.default.rmSync(WORKSCRIBE_DIR, { recursive: true, force: true });
2558
+ import_fs17.default.rmSync(WORKSCRIBE_DIR, { recursive: true, force: true });
2252
2559
  log.success(`Deleted ${WORKSCRIBE_DIR}`);
2253
2560
  } else {
2254
2561
  log.info(`Data kept at ${WORKSCRIBE_DIR}`);
@@ -2260,9 +2567,182 @@ function registerUninstall(program2) {
2260
2567
  });
2261
2568
  }
2262
2569
 
2570
+ // src/cli/profile.ts
2571
+ var import_fs18 = __toESM(require("fs"));
2572
+ var import_path13 = __toESM(require("path"));
2573
+ var import_chalk10 = __toESM(require("chalk"));
2574
+ var MIN_SUGGEST_COUNT = 5;
2575
+ function registerProfile(program2) {
2576
+ const profile = program2.command("profile").description("Manage stack profiles for better command tracking");
2577
+ profile.command("list").description("List available profiles from the registry").action(async () => {
2578
+ const config = readConfig();
2579
+ const activeNames = new Set(config.capture.profiles);
2580
+ let manifest = readCachedManifest();
2581
+ let offline = false;
2582
+ if (!manifest) {
2583
+ try {
2584
+ manifest = await cacheManifest();
2585
+ } catch {
2586
+ offline = true;
2587
+ manifest = getCachedProfiles().map((p) => ({
2588
+ name: p.name,
2589
+ displayName: p.displayName,
2590
+ tags: [],
2591
+ description: "",
2592
+ keywords: [],
2593
+ detect: p.detect
2594
+ }));
2595
+ }
2596
+ } else {
2597
+ cacheManifest().catch(() => void 0);
2598
+ }
2599
+ if (offline) {
2600
+ log.warn("Registry unreachable \u2014 showing installed profiles only");
2601
+ }
2602
+ if (manifest.length === 0) {
2603
+ log.plain("No profiles available. Check your connection and try again.");
2604
+ return;
2605
+ }
2606
+ const grouped = /* @__PURE__ */ new Map();
2607
+ for (const entry of manifest) {
2608
+ const tag = entry.tags[0] ?? "other";
2609
+ if (!grouped.has(tag)) grouped.set(tag, []);
2610
+ grouped.get(tag).push(entry);
2611
+ }
2612
+ log.plain("");
2613
+ for (const [tag, entries] of grouped) {
2614
+ log.plain(import_chalk10.default.bold(` ${tag.charAt(0).toUpperCase() + tag.slice(1)} profiles`));
2615
+ log.plain(import_chalk10.default.dim(" " + "\u2500".repeat(60)));
2616
+ for (const entry of entries) {
2617
+ const badge = activeNames.has(entry.name) ? import_chalk10.default.green(" [active]") : "";
2618
+ const installed = import_fs18.default.existsSync(import_path13.default.join(PROFILES_DIR, `${entry.name}.json`)) ? import_chalk10.default.dim(" installed") : "";
2619
+ log.plain(` ${import_chalk10.default.cyan(entry.name.padEnd(18))} ${entry.displayName.padEnd(28)}${badge}${installed}`);
2620
+ if (entry.description) log.plain(` ${import_chalk10.default.dim(" ".repeat(18) + entry.description)}`);
2621
+ }
2622
+ log.plain("");
2623
+ }
2624
+ });
2625
+ profile.command("add <names...>").description("Install one or more profiles from the registry").action(async (names) => {
2626
+ const config = readConfig();
2627
+ const activeNames = new Set(config.capture.profiles);
2628
+ for (const name of names) {
2629
+ if (activeNames.has(name)) {
2630
+ log.info(`Profile "${name}" is already active`);
2631
+ continue;
2632
+ }
2633
+ try {
2634
+ log.plain(`Fetching ${import_chalk10.default.cyan(name)}\u2026`);
2635
+ await installProfile(name);
2636
+ activeNames.add(name);
2637
+ log.success(`Profile "${name}" installed`);
2638
+ } catch (err) {
2639
+ log.error(`Failed to install "${name}": ${err.message}`);
2640
+ }
2641
+ }
2642
+ config.capture.profiles = [...activeNames];
2643
+ writeConfig(config);
2644
+ });
2645
+ profile.command("remove <names...>").description("Uninstall one or more profiles").action((names) => {
2646
+ const config = readConfig();
2647
+ const activeNames = new Set(config.capture.profiles);
2648
+ for (const name of names) {
2649
+ if (!activeNames.has(name)) {
2650
+ log.warn(`Profile "${name}" is not active`);
2651
+ continue;
2652
+ }
2653
+ activeNames.delete(name);
2654
+ const cachePath = import_path13.default.join(PROFILES_DIR, `${name}.json`);
2655
+ if (import_fs18.default.existsSync(cachePath)) import_fs18.default.unlinkSync(cachePath);
2656
+ log.success(`Profile "${name}" removed`);
2657
+ }
2658
+ config.capture.profiles = [...activeNames];
2659
+ writeConfig(config);
2660
+ });
2661
+ profile.command("update [name]").description("Re-fetch installed profiles from the registry").action(async (name) => {
2662
+ const config = readConfig();
2663
+ const toUpdate = name ? [name] : config.capture.profiles;
2664
+ if (toUpdate.length === 0) {
2665
+ log.plain("No profiles installed. Run workscribe profile add <name> first.");
2666
+ return;
2667
+ }
2668
+ for (const profileName of toUpdate) {
2669
+ try {
2670
+ log.plain(`Updating ${import_chalk10.default.cyan(profileName)}\u2026`);
2671
+ await installProfile(profileName);
2672
+ log.success(`${profileName} updated`);
2673
+ } catch (err) {
2674
+ log.error(`Failed to update "${profileName}": ${err.message}`);
2675
+ }
2676
+ }
2677
+ });
2678
+ profile.command("detect").description("Scan the current directory and suggest matching profiles").action(() => {
2679
+ const cwd = process.cwd();
2680
+ const matches = detectProfilesForDirectory(cwd);
2681
+ if (matches.length === 0) {
2682
+ const cached = readCachedManifest();
2683
+ if (!cached) {
2684
+ log.plain("No manifest cached. Run workscribe profile list to fetch it first.");
2685
+ } else {
2686
+ log.plain(`No profiles detected for ${import_chalk10.default.dim(cwd)}`);
2687
+ log.plain(import_chalk10.default.dim("Run workscribe profile list to browse available profiles."));
2688
+ }
2689
+ return;
2690
+ }
2691
+ log.plain("");
2692
+ log.plain(import_chalk10.default.bold(" Profiles detected for this directory:"));
2693
+ log.plain("");
2694
+ for (const entry of matches) {
2695
+ const files = entry.detect?.files?.join(", ") ?? "";
2696
+ log.plain(` ${import_chalk10.default.cyan(entry.name.padEnd(18))} ${entry.displayName} ${import_chalk10.default.dim("(" + files + ")")}`);
2697
+ }
2698
+ log.plain("");
2699
+ log.plain(` Run: ${import_chalk10.default.cyan("workscribe profile add " + matches.map((e) => e.name).join(" "))}`);
2700
+ log.plain("");
2701
+ });
2702
+ profile.command("suggest").description("Analyse recent unclassified commands and suggest profiles").option("--days <n>", "number of days to analyse", "30").action((opts) => {
2703
+ const days = Math.max(1, parseInt(opts.days, 10) || 30);
2704
+ const toolCounts = getOtherEventToolCounts(days);
2705
+ if (toolCounts.length === 0) {
2706
+ log.plain("No unclassified commands found in the last " + days + " days.");
2707
+ return;
2708
+ }
2709
+ const manifest = readCachedManifest();
2710
+ if (!manifest) {
2711
+ log.plain("No manifest cached. Run workscribe profile list first.");
2712
+ return;
2713
+ }
2714
+ const config = readConfig();
2715
+ const activeNames = new Set(config.capture.profiles);
2716
+ const suggestions = [];
2717
+ for (const entry of manifest) {
2718
+ if (activeNames.has(entry.name)) continue;
2719
+ const matchedCount = toolCounts.filter((tc) => entry.keywords.some((kw) => tc.tool.toLowerCase().includes(kw.toLowerCase()))).reduce((sum, tc) => sum + tc.count, 0);
2720
+ if (matchedCount >= MIN_SUGGEST_COUNT) {
2721
+ suggestions.push({ entry, count: matchedCount });
2722
+ }
2723
+ }
2724
+ suggestions.sort((a, b) => b.count - a.count);
2725
+ if (suggestions.length === 0) {
2726
+ log.plain("No profile matches found. Your commands are well covered.");
2727
+ return;
2728
+ }
2729
+ log.plain("");
2730
+ log.plain(import_chalk10.default.bold(` Analysed last ${days} days of unclassified commands:`));
2731
+ log.plain("");
2732
+ for (const { entry, count } of suggestions) {
2733
+ log.plain(
2734
+ ` ${import_chalk10.default.cyan(entry.name.padEnd(18))} ${String(count).padStart(4)} commands \u2192 ${entry.displayName}`
2735
+ );
2736
+ }
2737
+ log.plain("");
2738
+ log.plain(` Run: ${import_chalk10.default.cyan("workscribe profile add " + suggestions.map((s) => s.entry.name).join(" "))}`);
2739
+ log.plain("");
2740
+ });
2741
+ }
2742
+
2263
2743
  // src/index.ts
2264
2744
  var program = new import_commander.Command();
2265
- program.name("workscribe").description("Automatic developer work journal").version("1.8.0");
2745
+ program.name("workscribe").description("Automatic developer work journal").version("1.10.0");
2266
2746
  registerCapture(program);
2267
2747
  registerHook(program);
2268
2748
  registerInit(program);
@@ -2276,5 +2756,6 @@ registerPause(program);
2276
2756
  registerUndo(program);
2277
2757
  registerClear(program);
2278
2758
  registerUninstall(program);
2759
+ registerProfile(program);
2279
2760
  program.parse();
2280
2761
  //# sourceMappingURL=index.js.map