@workscribe/cli 1.9.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;
@@ -123,13 +143,13 @@ function applyMigrations(db) {
123
143
  const applied = new Set(
124
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
  }
@@ -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 {
@@ -490,114 +766,131 @@ function extractPackages(cmd, afterKeyword) {
490
766
  if (idx === -1) return [];
491
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
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", (match) => ({ tool: match[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", (match) => ({ tool: match[1] })),
584
- makeRule(/^bun\s+run\s+build\b/, "build", () => ({ tool: "bun" })),
585
- makeRule(/^(make|gmake)\b/, "build", (match) => ({ tool: match[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", (match) => ({ tool: match[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", (match) => ({ tool: match[1], script: match[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", (match) => ({ shell: match[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", (match) => ({ tool: match[1] })),
657
- makeRule(/^(rg|ag|ack|ripgrep)\b/, "code_search", (match) => ({ tool: match[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", (match) => ({ tool: match[1] })),
661
- makeRule(/^(head|tail)\b/, "code_read", (match) => ({ tool: match[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", (match) => ({ tool: match[1] })),
664
- makeRule(/^(mv)\b/, "file_ops", () => ({ tool: "mv" })),
665
- makeRule(/^(rm|rmdir)\b/, "file_ops", (match) => ({ tool: match[1] })),
666
- makeRule(/^(mkdir|touch|ln)\b/, "file_ops", (match) => ({ tool: match[1] })),
667
- makeRule(/^(chmod|chown|chgrp)\b/, "file_ops", (match) => ({ tool: match[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;
@@ -887,21 +1113,21 @@ function registerHook(program2) {
887
1113
  }
888
1114
 
889
1115
  // src/cli/init.ts
890
- var import_fs10 = __toESM(require("fs"));
891
- var import_path7 = __toESM(require("path"));
1116
+ var import_fs12 = __toESM(require("fs"));
1117
+ var import_path9 = __toESM(require("path"));
892
1118
  var import_os4 = __toESM(require("os"));
893
1119
  var import_prompts = __toESM(require("prompts"));
894
1120
  var import_chalk2 = __toESM(require("chalk"));
895
1121
 
896
1122
  // src/capture/shell-hook.ts
897
- var import_fs8 = __toESM(require("fs"));
898
- var import_path5 = __toESM(require("path"));
1123
+ var import_fs10 = __toESM(require("fs"));
1124
+ var import_path7 = __toESM(require("path"));
899
1125
  var HOOK_START = "# workscribe:hook:start";
900
1126
  var HOOK_END = "# workscribe:hook:end";
901
1127
  var HOOK_PREAMBLE = "# Workscribe \u2014 developer work journal";
902
1128
  function getInstalledHookBlock(rcFile) {
903
- if (!import_fs8.default.existsSync(rcFile)) return null;
904
- 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");
905
1131
  const startIdx = content.indexOf(HOOK_START);
906
1132
  const endIdx = content.indexOf(HOOK_END);
907
1133
  if (startIdx === -1 || endIdx === -1) return null;
@@ -917,38 +1143,38 @@ function isHookOutdated(rcFile, hookContent) {
917
1143
  }
918
1144
  function installHook(rcFile, hookContent) {
919
1145
  const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
920
- import_fs8.default.copyFileSync(rcFile, backup);
921
- log.info(`Backed up ${rcFile} \u2192 ${import_path5.default.basename(backup)}`);
922
- 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, `
923
1149
  ${hookContent}
924
1150
  `);
925
1151
  }
926
1152
  function updateHook(rcFile, hookContent) {
927
1153
  const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
928
- import_fs8.default.copyFileSync(rcFile, backup);
929
- log.info(`Backed up ${rcFile} \u2192 ${import_path5.default.basename(backup)}`);
930
- 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");
931
1157
  const startIdx = content.indexOf(HOOK_START);
932
1158
  const endIdx = content.indexOf(HOOK_END) + HOOK_END.length;
933
- 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));
934
1160
  }
935
1161
  function removeHook(rcFile) {
936
1162
  const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
937
- import_fs8.default.copyFileSync(rcFile, backup);
938
- log.info(`Backed up ${rcFile} \u2192 ${import_path5.default.basename(backup)}`);
939
- 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");
940
1166
  const startIdx = content.indexOf(HOOK_START);
941
1167
  const endIdx = content.indexOf(HOOK_END) + HOOK_END.length;
942
1168
  const preambleIdx = content.lastIndexOf(HOOK_PREAMBLE, startIdx);
943
1169
  const blockStart = preambleIdx !== -1 ? preambleIdx : startIdx;
944
1170
  const before = content.slice(0, blockStart).replace(/\n+$/, "\n");
945
1171
  const after = content.slice(endIdx).replace(/^\n/, "");
946
- import_fs8.default.writeFileSync(rcFile, before + after);
1172
+ import_fs10.default.writeFileSync(rcFile, before + after);
947
1173
  }
948
1174
 
949
1175
  // src/capture/hooks/claude-code.ts
950
- var import_fs9 = __toESM(require("fs"));
951
- var import_path6 = __toESM(require("path"));
1176
+ var import_fs11 = __toESM(require("fs"));
1177
+ var import_path8 = __toESM(require("path"));
952
1178
  var import_os3 = __toESM(require("os"));
953
1179
 
954
1180
  // src/capture/hooks/utils.ts
@@ -963,11 +1189,11 @@ function isCommandInstalled(cmd) {
963
1189
  }
964
1190
 
965
1191
  // src/capture/hooks/claude-code.ts
966
- 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");
967
1193
  function readSettings() {
968
- if (!import_fs9.default.existsSync(CLAUDE_SETTINGS_PATH)) return {};
1194
+ if (!import_fs11.default.existsSync(CLAUDE_SETTINGS_PATH)) return {};
969
1195
  try {
970
- 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"));
971
1197
  } catch {
972
1198
  return {};
973
1199
  }
@@ -1002,8 +1228,8 @@ var claudeCodeIntegration = {
1002
1228
  }
1003
1229
  ]
1004
1230
  };
1005
- import_fs9.default.mkdirSync(import_path6.default.dirname(CLAUDE_SETTINGS_PATH), { recursive: true });
1006
- 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));
1007
1233
  }
1008
1234
  };
1009
1235
 
@@ -1015,22 +1241,25 @@ var AI_HOOK_INTEGRATIONS = {
1015
1241
  // src/cli/init.ts
1016
1242
  function detectShell() {
1017
1243
  const shell = process.env.SHELL ?? "";
1018
- if (shell.includes("zsh")) return { name: "zsh", rcFile: import_path7.default.join(import_os4.default.homedir(), ".zshrc") };
1019
- 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") };
1020
1246
  return null;
1021
1247
  }
1022
1248
  function registerInit(program2) {
1023
1249
  program2.command("init").description("Set up Workscribe and install or update shell hooks").action(async () => {
1024
1250
  log.plain(import_chalk2.default.bold("\n Workscribe \u2014 Developer Work Journal\n"));
1025
- if (!import_fs10.default.existsSync(WORKSCRIBE_DIR)) {
1026
- 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 });
1027
1253
  log.success(`Created ${WORKSCRIBE_DIR}`);
1028
1254
  } else {
1029
1255
  try {
1030
- import_fs10.default.chmodSync(WORKSCRIBE_DIR, 448);
1256
+ import_fs12.default.chmodSync(WORKSCRIBE_DIR, 448);
1031
1257
  } catch {
1032
1258
  }
1033
1259
  }
1260
+ if (!import_fs12.default.existsSync(PROFILES_DIR)) {
1261
+ import_fs12.default.mkdirSync(PROFILES_DIR, { recursive: true, mode: 448 });
1262
+ }
1034
1263
  getDb();
1035
1264
  log.success("Database initialized");
1036
1265
  const shell = detectShell();
@@ -1038,12 +1267,12 @@ function registerInit(program2) {
1038
1267
  log.warn("Could not detect shell. Supported: zsh, bash");
1039
1268
  log.plain(`Manually add the hook from: ${import_chalk2.default.cyan("shell/hook.zsh")} or ${import_chalk2.default.cyan("shell/hook.bash")}`);
1040
1269
  } else {
1041
- const hookFile = import_path7.default.join(__dirname, "..", "shell", `hook.${shell.name}`);
1042
- 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)) {
1043
1272
  log.warn(`Hook template not found at ${hookFile}`);
1044
1273
  } else {
1045
- const hookContent = import_fs10.default.readFileSync(hookFile, "utf-8");
1046
- 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, "");
1047
1276
  if (!isHookInstalled(shell.rcFile)) {
1048
1277
  installHook(shell.rcFile, hookContent);
1049
1278
  log.success(`Shell hook installed in ${shell.rcFile}`);
@@ -1092,6 +1321,33 @@ function registerInit(program2) {
1092
1321
  }
1093
1322
  }
1094
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
+ }
1095
1351
  if (config.ai.provider) {
1096
1352
  log.info(`AI provider already configured: ${import_chalk2.default.cyan(config.ai.provider)}`);
1097
1353
  } else {
@@ -1139,7 +1395,7 @@ function registerInit(program2) {
1139
1395
  log.info("Skipped \u2014 run later: workscribe config set ai.provider <provider>");
1140
1396
  }
1141
1397
  }
1142
- if (!import_fs10.default.existsSync(CONFIG_PATH)) {
1398
+ if (!import_fs12.default.existsSync(CONFIG_PATH)) {
1143
1399
  writeConfig(config);
1144
1400
  log.success("Config file created");
1145
1401
  }
@@ -1158,7 +1414,7 @@ function registerInit(program2) {
1158
1414
  }
1159
1415
 
1160
1416
  // src/cli/status.ts
1161
- var import_fs11 = __toESM(require("fs"));
1417
+ var import_fs13 = __toESM(require("fs"));
1162
1418
  var import_chalk3 = __toESM(require("chalk"));
1163
1419
 
1164
1420
  // src/utils/format.ts
@@ -1216,7 +1472,7 @@ function registerStatus(program2) {
1216
1472
  const total = getEventCount();
1217
1473
  const todayEvents = getEventsForDate(todayDate());
1218
1474
  const last = getLastEvent();
1219
- const isPaused = import_fs11.default.existsSync(PAUSE_FLAG_PATH);
1475
+ const isPaused = import_fs13.default.existsSync(PAUSE_FLAG_PATH);
1220
1476
  log.plain("");
1221
1477
  log.plain(import_chalk3.default.bold(" Workscribe Status"));
1222
1478
  log.plain("");
@@ -1235,6 +1491,25 @@ function registerStatus(program2) {
1235
1491
  ` AI provider ${config.ai.provider ? import_chalk3.default.cyan(config.ai.provider) : import_chalk3.default.yellow("not configured")}`
1236
1492
  );
1237
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
+ }
1238
1513
  log.plain("");
1239
1514
  });
1240
1515
  }
@@ -1242,26 +1517,19 @@ function registerStatus(program2) {
1242
1517
  // src/cli/events.ts
1243
1518
  var import_chalk4 = __toESM(require("chalk"));
1244
1519
  var CATEGORY_COLORS = {
1245
- code_commit: import_chalk4.default.green,
1246
- code_push: import_chalk4.default.green,
1247
- code_pull: import_chalk4.default.blue,
1248
- branch_switch: import_chalk4.default.blue,
1249
- dependency_install: import_chalk4.default.yellow,
1250
- dependency_remove: import_chalk4.default.yellow,
1251
- test_run: import_chalk4.default.magenta,
1252
- build: import_chalk4.default.cyan,
1253
- 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,
1254
1527
  infrastructure: import_chalk4.default.red,
1255
- script_run: import_chalk4.default.white,
1256
- environment: import_chalk4.default.gray,
1257
- git_other: import_chalk4.default.blue,
1258
- code_merge: import_chalk4.default.green,
1259
- 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,
1260
1532
  ai_session: import_chalk4.default.bold.magenta,
1261
- lint_run: import_chalk4.default.yellow,
1262
- code_search: import_chalk4.default.dim.cyan,
1263
- code_read: import_chalk4.default.dim.white,
1264
- file_ops: import_chalk4.default.yellow,
1265
1533
  other: import_chalk4.default.gray
1266
1534
  };
1267
1535
  var AI_TOOL_DISPLAY_NAMES = Object.fromEntries(
@@ -1454,9 +1722,9 @@ function registerSessions(program2) {
1454
1722
 
1455
1723
  // src/cli/summary.ts
1456
1724
  var import_chalk6 = __toESM(require("chalk"));
1457
- var import_fs12 = __toESM(require("fs"));
1725
+ var import_fs14 = __toESM(require("fs"));
1458
1726
  var import_os5 = __toESM(require("os"));
1459
- var import_path8 = __toESM(require("path"));
1727
+ var import_path10 = __toESM(require("path"));
1460
1728
 
1461
1729
  // src/ai/prompt.ts
1462
1730
  var SYSTEM_PROMPT = `You write short developer work summaries. Here is an example:
@@ -1466,27 +1734,34 @@ Project: api-service
1466
1734
  Branch: feature/auth
1467
1735
  Duration: 2h 00m
1468
1736
  Events:
1469
- - branch_switch {"branch":"feature/auth"}
1470
- - 2x dependency_install {"tool":"npm","packages":["bcrypt","jsonwebtoken"]}
1471
- - 2x test_run {"tool":"jest"} [failed]
1472
- - test_run {"tool":"jest"}
1473
- - 2x file_ops {"tool":"mkdir"}
1474
- - code_commit {"message":"fix: auth token validation"}
1475
- - 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"}
1476
1744
 
1477
1745
  OUTPUT:
1478
1746
  - Switched to the authentication feature branch and added authentication dependencies (bcrypt, JWT)
1479
1747
  - Resolved failing auth tests
1480
1748
  - Committed and pushed authentication fixes
1481
1749
 
1482
- 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.
1483
1751
 
1484
- 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).
1485
1753
 
1486
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.
1487
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
+
1488
1758
  Maximum 5 bullets. Output ONLY the bullet points, nothing else.`;
1489
- 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"]);
1490
1765
  function buildUserMessage(payload) {
1491
1766
  const lines = [`Date: ${payload.date}`, ""];
1492
1767
  for (const session of payload.sessions) {
@@ -1613,7 +1888,7 @@ var AnthropicProvider = class {
1613
1888
  body: JSON.stringify({
1614
1889
  model: this.model,
1615
1890
  max_tokens: 1024,
1616
- system: SYSTEM_PROMPT,
1891
+ system: getSystemPrompt(),
1617
1892
  messages: [{ role: "user", content: buildUserMessage(payload) }]
1618
1893
  })
1619
1894
  });
@@ -1668,7 +1943,7 @@ var OpenAIProvider = class {
1668
1943
  body: JSON.stringify({
1669
1944
  model: this.model,
1670
1945
  messages: [
1671
- { role: "system", content: SYSTEM_PROMPT },
1946
+ { role: "system", content: getSystemPrompt() },
1672
1947
  { role: "user", content: buildUserMessage(payload) }
1673
1948
  ],
1674
1949
  max_tokens: 1024
@@ -1722,7 +1997,7 @@ var OllamaProvider = class {
1722
1997
  model: this.model,
1723
1998
  stream: false,
1724
1999
  messages: [
1725
- { role: "system", content: SYSTEM_PROMPT },
2000
+ { role: "system", content: getSystemPrompt() },
1726
2001
  { role: "user", content: buildUserMessage(payload) }
1727
2002
  ],
1728
2003
  options: {
@@ -1904,9 +2179,9 @@ function registerSummary(program2) {
1904
2179
  return;
1905
2180
  }
1906
2181
  const cacheKey = isoWeekKey(today);
1907
- const cacheFile = import_path8.default.join(WEEKLY_CACHE_DIR, `${cacheKey}.md`);
1908
- if (!opts.fresh && import_fs12.default.existsSync(cacheFile)) {
1909
- 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");
1910
2185
  log.plain("");
1911
2186
  log.plain(cached);
1912
2187
  log.plain("");
@@ -1922,8 +2197,8 @@ function registerSummary(program2) {
1922
2197
  log.plain("");
1923
2198
  log.plain(summary);
1924
2199
  log.plain("");
1925
- import_fs12.default.mkdirSync(WEEKLY_CACHE_DIR, { recursive: true });
1926
- 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 });
1927
2202
  await handleWebhook(opts, config, summary);
1928
2203
  } catch (e) {
1929
2204
  log.error(`Weekly summary failed [${config.ai.provider}]: ${e.message}`);
@@ -2010,8 +2285,8 @@ ${bullets.trim()}`;
2010
2285
  }
2011
2286
 
2012
2287
  // src/cli/export.ts
2013
- var import_fs13 = __toESM(require("fs"));
2014
- var import_path9 = __toESM(require("path"));
2288
+ var import_fs15 = __toESM(require("fs"));
2289
+ var import_path11 = __toESM(require("path"));
2015
2290
  function eventRowToPayload2(row) {
2016
2291
  let metadata = {};
2017
2292
  try {
@@ -2067,9 +2342,9 @@ function registerExport(program2) {
2067
2342
  const payload = { date, sessions: sessionPayloads };
2068
2343
  log.info(`Generating summary via ${config.ai.provider}\u2026`);
2069
2344
  const summary = await provider.summarize(payload);
2070
- const outputPath = opts.output ?? import_path9.default.join(EXPORTS_DIR, `workscribe-${date}.md`);
2071
- if (!import_fs13.default.existsSync(EXPORTS_DIR)) import_fs13.default.mkdirSync(EXPORTS_DIR, { recursive: true, mode: 448 });
2072
- 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}
2073
2348
 
2074
2349
  ${summary}
2075
2350
  `, { encoding: "utf-8", mode: 384 });
@@ -2141,22 +2416,22 @@ function printConfig(obj, indent) {
2141
2416
  }
2142
2417
 
2143
2418
  // src/cli/pause.ts
2144
- var import_fs14 = __toESM(require("fs"));
2419
+ var import_fs16 = __toESM(require("fs"));
2145
2420
  function registerPause(program2) {
2146
2421
  program2.command("pause").description("Pause capture temporarily").action(() => {
2147
- if (import_fs14.default.existsSync(PAUSE_FLAG_PATH)) {
2422
+ if (import_fs16.default.existsSync(PAUSE_FLAG_PATH)) {
2148
2423
  log.info("Capture is already paused. Run workscribe resume to restart.");
2149
2424
  return;
2150
2425
  }
2151
- import_fs14.default.writeFileSync(PAUSE_FLAG_PATH, "", { mode: 384 });
2426
+ import_fs16.default.writeFileSync(PAUSE_FLAG_PATH, "", { mode: 384 });
2152
2427
  log.success("Capture paused. Run workscribe resume to restart.");
2153
2428
  });
2154
2429
  program2.command("resume").description("Resume capture after a pause").action(() => {
2155
- if (!import_fs14.default.existsSync(PAUSE_FLAG_PATH)) {
2430
+ if (!import_fs16.default.existsSync(PAUSE_FLAG_PATH)) {
2156
2431
  log.info("Capture is not paused.");
2157
2432
  return;
2158
2433
  }
2159
- import_fs14.default.rmSync(PAUSE_FLAG_PATH);
2434
+ import_fs16.default.rmSync(PAUSE_FLAG_PATH);
2160
2435
  log.success("Capture resumed.");
2161
2436
  });
2162
2437
  }
@@ -2241,18 +2516,18 @@ function registerClear(program2) {
2241
2516
  }
2242
2517
 
2243
2518
  // src/cli/uninstall.ts
2244
- var import_fs15 = __toESM(require("fs"));
2245
- var import_path10 = __toESM(require("path"));
2519
+ var import_fs17 = __toESM(require("fs"));
2520
+ var import_path12 = __toESM(require("path"));
2246
2521
  var import_os6 = __toESM(require("os"));
2247
2522
  var import_prompts3 = __toESM(require("prompts"));
2248
2523
  var import_chalk9 = __toESM(require("chalk"));
2249
2524
  function detectShellRcFiles() {
2250
2525
  const candidates = [
2251
- { name: "zsh", rcFile: import_path10.default.join(import_os6.default.homedir(), ".zshrc") },
2252
- { name: "bash", rcFile: import_path10.default.join(import_os6.default.homedir(), ".bashrc") },
2253
- { 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") }
2254
2529
  ];
2255
- return candidates.filter((candidate) => import_fs15.default.existsSync(candidate.rcFile) && isHookInstalled(candidate.rcFile));
2530
+ return candidates.filter((candidate) => import_fs17.default.existsSync(candidate.rcFile) && isHookInstalled(candidate.rcFile));
2256
2531
  }
2257
2532
  function registerUninstall(program2) {
2258
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) => {
@@ -2271,7 +2546,7 @@ function registerUninstall(program2) {
2271
2546
  }
2272
2547
  }
2273
2548
  if (opts.hooksOnly) return;
2274
- if (import_fs15.default.existsSync(WORKSCRIBE_DIR)) {
2549
+ if (import_fs17.default.existsSync(WORKSCRIBE_DIR)) {
2275
2550
  log.plain("");
2276
2551
  const { deleteData } = await (0, import_prompts3.default)({
2277
2552
  type: "confirm",
@@ -2280,7 +2555,7 @@ function registerUninstall(program2) {
2280
2555
  initial: false
2281
2556
  });
2282
2557
  if (deleteData) {
2283
- import_fs15.default.rmSync(WORKSCRIBE_DIR, { recursive: true, force: true });
2558
+ import_fs17.default.rmSync(WORKSCRIBE_DIR, { recursive: true, force: true });
2284
2559
  log.success(`Deleted ${WORKSCRIBE_DIR}`);
2285
2560
  } else {
2286
2561
  log.info(`Data kept at ${WORKSCRIBE_DIR}`);
@@ -2292,9 +2567,182 @@ function registerUninstall(program2) {
2292
2567
  });
2293
2568
  }
2294
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
+
2295
2743
  // src/index.ts
2296
2744
  var program = new import_commander.Command();
2297
- program.name("workscribe").description("Automatic developer work journal").version("1.9.0");
2745
+ program.name("workscribe").description("Automatic developer work journal").version("1.10.0");
2298
2746
  registerCapture(program);
2299
2747
  registerHook(program);
2300
2748
  registerInit(program);
@@ -2308,5 +2756,6 @@ registerPause(program);
2308
2756
  registerUndo(program);
2309
2757
  registerClear(program);
2310
2758
  registerUninstall(program);
2759
+ registerProfile(program);
2311
2760
  program.parse();
2312
2761
  //# sourceMappingURL=index.js.map