@workscribe/cli 1.11.0 → 1.12.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
@@ -41,23 +41,31 @@ Upgrade at https://nodejs.org or via your version manager:
41
41
  }
42
42
 
43
43
  // src/capture/index.ts
44
- var import_fs9 = __toESM(require("fs"));
44
+ var import_fs10 = __toESM(require("fs"));
45
45
 
46
46
  // src/db/index.ts
47
47
  var import_node_sqlite = require("node:sqlite");
48
- var import_fs = __toESM(require("fs"));
48
+ var import_fs2 = __toESM(require("fs"));
49
49
 
50
50
  // src/utils/paths.ts
51
51
  var import_path = __toESM(require("path"));
52
52
  var import_os = __toESM(require("os"));
53
- var WORKSCRIBE_DIR = import_path.default.join(import_os.default.homedir(), ".workscribe");
54
- var DB_PATH = import_path.default.join(WORKSCRIBE_DIR, "workscribe.db");
55
- var CONFIG_PATH = import_path.default.join(WORKSCRIBE_DIR, "config.json");
56
- var DEBUG_LOG_PATH = import_path.default.join(WORKSCRIBE_DIR, "debug.log");
57
- var EXPORTS_DIR = import_path.default.join(WORKSCRIBE_DIR, "exports");
58
- var PAUSE_FLAG_PATH = import_path.default.join(WORKSCRIBE_DIR, "paused");
59
- var WEEKLY_CACHE_DIR = import_path.default.join(WORKSCRIBE_DIR, "weekly-cache");
60
- var PROFILES_DIR = import_path.default.join(WORKSCRIBE_DIR, "profiles");
53
+ var import_fs = __toESM(require("fs"));
54
+ var LEGACY_DIR = import_path.default.join(import_os.default.homedir(), ".workscribe");
55
+ var USE_XDG = !import_fs.default.existsSync(LEGACY_DIR);
56
+ var xdgDataDir = USE_XDG ? import_path.default.join(process.env.XDG_DATA_HOME ?? import_path.default.join(import_os.default.homedir(), ".local", "share"), "workscribe") : LEGACY_DIR;
57
+ var xdgConfigDir = USE_XDG ? import_path.default.join(process.env.XDG_CONFIG_HOME ?? import_path.default.join(import_os.default.homedir(), ".config"), "workscribe") : LEGACY_DIR;
58
+ var xdgStateDir = USE_XDG ? import_path.default.join(process.env.XDG_STATE_HOME ?? import_path.default.join(import_os.default.homedir(), ".local", "state"), "workscribe") : LEGACY_DIR;
59
+ var REQUIRED_DIRS = [.../* @__PURE__ */ new Set([xdgDataDir, xdgConfigDir, xdgStateDir])];
60
+ var WORKSCRIBE_DIR = xdgDataDir;
61
+ var DB_PATH = import_path.default.join(xdgDataDir, "workscribe.db");
62
+ var CONFIG_PATH = import_path.default.join(xdgConfigDir, "config.json");
63
+ var DEBUG_LOG_PATH = import_path.default.join(xdgStateDir, "debug.log");
64
+ var UPDATE_CHECK_PATH = import_path.default.join(xdgStateDir, "update-check.json");
65
+ var EXPORTS_DIR = import_path.default.join(xdgDataDir, "exports");
66
+ var PAUSE_FLAG_PATH = import_path.default.join(xdgStateDir, "paused");
67
+ var WEEKLY_CACHE_DIR = import_path.default.join(xdgDataDir, "weekly-cache");
68
+ var PROFILES_DIR = import_path.default.join(xdgDataDir, "profiles");
61
69
 
62
70
  // src/db/migrations/001_initial_schema.ts
63
71
  var initial_schema_default = {
@@ -126,12 +134,12 @@ var MIGRATIONS = [initial_schema_default, semantic_categories_default];
126
134
  var _db = null;
127
135
  function getDb() {
128
136
  if (_db) return _db;
129
- if (!import_fs.default.existsSync(WORKSCRIBE_DIR)) {
130
- import_fs.default.mkdirSync(WORKSCRIBE_DIR, { recursive: true });
137
+ if (!import_fs2.default.existsSync(WORKSCRIBE_DIR)) {
138
+ import_fs2.default.mkdirSync(WORKSCRIBE_DIR, { recursive: true });
131
139
  }
132
140
  const db = new import_node_sqlite.DatabaseSync(DB_PATH);
133
141
  try {
134
- import_fs.default.chmodSync(DB_PATH, 384);
142
+ import_fs2.default.chmodSync(DB_PATH, 384);
135
143
  } catch {
136
144
  }
137
145
  db.exec("PRAGMA journal_mode = WAL");
@@ -345,7 +353,7 @@ function redact(command, extraPatterns = []) {
345
353
  // src/capture/git.ts
346
354
  var import_child_process = require("child_process");
347
355
  var import_path2 = __toESM(require("path"));
348
- var import_fs2 = __toESM(require("fs"));
356
+ var import_fs3 = __toESM(require("fs"));
349
357
  function nameFromRemote(root) {
350
358
  try {
351
359
  const url = (0, import_child_process.execSync)("git remote get-url origin", {
@@ -361,7 +369,7 @@ function nameFromRemote(root) {
361
369
  }
362
370
  function nameFromPackageJson(root) {
363
371
  try {
364
- const raw = import_fs2.default.readFileSync(import_path2.default.join(root, "package.json"), "utf-8");
372
+ const raw = import_fs3.default.readFileSync(import_path2.default.join(root, "package.json"), "utf-8");
365
373
  const pkg = JSON.parse(raw);
366
374
  if (!pkg.name) return null;
367
375
  return pkg.name.replace(/^@[^/]+\//, "");
@@ -405,14 +413,14 @@ function getCommitsSince(sha, cwd) {
405
413
  }
406
414
 
407
415
  // src/capture/aider.ts
408
- var import_fs3 = __toESM(require("fs"));
416
+ var import_fs4 = __toESM(require("fs"));
409
417
  var import_path3 = __toESM(require("path"));
410
418
  var HISTORY_FILE = ".aider.chat.history.md";
411
419
  var PROMPT_MAX_LENGTH = 200;
412
420
  function readLastAiderPrompt(cwd) {
413
421
  try {
414
422
  const histPath = import_path3.default.join(cwd, HISTORY_FILE);
415
- const content = import_fs3.default.readFileSync(histPath, "utf-8");
423
+ const content = import_fs4.default.readFileSync(histPath, "utf-8");
416
424
  const matches = [...content.matchAll(/^> (.+)$/gm)];
417
425
  if (!matches.length) return null;
418
426
  return matches[matches.length - 1][1].trim().slice(0, PROMPT_MAX_LENGTH);
@@ -422,7 +430,7 @@ function readLastAiderPrompt(cwd) {
422
430
  }
423
431
 
424
432
  // src/config/registry.ts
425
- var import_fs5 = __toESM(require("fs"));
433
+ var import_fs6 = __toESM(require("fs"));
426
434
  var import_os2 = __toESM(require("os"));
427
435
  var import_path4 = __toESM(require("path"));
428
436
 
@@ -488,7 +496,7 @@ var ai_tools_default = [
488
496
 
489
497
  // src/utils/logger.ts
490
498
  var import_chalk = __toESM(require("chalk"));
491
- var import_fs4 = __toESM(require("fs"));
499
+ var import_fs5 = __toESM(require("fs"));
492
500
  var isDebug = process.env.WORKSCRIBE_DEBUG === "1";
493
501
  var log = {
494
502
  info: (msg) => console.log(import_chalk.default.cyan("\u2192"), msg),
@@ -502,7 +510,7 @@ function debugLog(context, error) {
502
510
  const entry = `[${(/* @__PURE__ */ new Date()).toISOString()}] [${context}] ${error instanceof Error ? error.stack : String(error)}
503
511
  `;
504
512
  try {
505
- import_fs4.default.appendFileSync(DEBUG_LOG_PATH, entry, { mode: 384 });
513
+ import_fs5.default.appendFileSync(DEBUG_LOG_PATH, entry, { mode: 384 });
506
514
  } catch {
507
515
  }
508
516
  }
@@ -512,8 +520,8 @@ var USER_REGISTRY_PATH = import_path4.default.join(import_os2.default.homedir(),
512
520
  function loadAIToolRegistry() {
513
521
  let userEntries = [];
514
522
  try {
515
- if (import_fs5.default.existsSync(USER_REGISTRY_PATH)) {
516
- const raw = import_fs5.default.readFileSync(USER_REGISTRY_PATH, "utf-8");
523
+ if (import_fs6.default.existsSync(USER_REGISTRY_PATH)) {
524
+ const raw = import_fs6.default.readFileSync(USER_REGISTRY_PATH, "utf-8");
517
525
  const parsed = JSON.parse(raw);
518
526
  if (Array.isArray(parsed)) {
519
527
  userEntries = parsed.filter((entry) => typeof entry.tool === "string" && typeof entry.pattern === "string").map((entry) => ({
@@ -535,11 +543,11 @@ function loadAIToolRegistry() {
535
543
  }
536
544
 
537
545
  // src/profiles/loader.ts
538
- var import_fs8 = __toESM(require("fs"));
546
+ var import_fs9 = __toESM(require("fs"));
539
547
  var import_path6 = __toESM(require("path"));
540
548
 
541
549
  // src/config/index.ts
542
- var import_fs6 = __toESM(require("fs"));
550
+ var import_fs7 = __toESM(require("fs"));
543
551
  var DEFAULTS = {
544
552
  ai: {
545
553
  provider: "",
@@ -564,9 +572,9 @@ var DEFAULTS = {
564
572
  }
565
573
  };
566
574
  function readConfig() {
567
- if (!import_fs6.default.existsSync(CONFIG_PATH)) return { ...DEFAULTS };
575
+ if (!import_fs7.default.existsSync(CONFIG_PATH)) return { ...DEFAULTS };
568
576
  try {
569
- const raw = import_fs6.default.readFileSync(CONFIG_PATH, "utf-8");
577
+ const raw = import_fs7.default.readFileSync(CONFIG_PATH, "utf-8");
570
578
  const parsed = JSON.parse(raw);
571
579
  return deepMerge(DEFAULTS, parsed);
572
580
  } catch {
@@ -574,9 +582,9 @@ function readConfig() {
574
582
  }
575
583
  }
576
584
  function writeConfig(config) {
577
- import_fs6.default.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { encoding: "utf-8", mode: 384 });
585
+ import_fs7.default.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { encoding: "utf-8", mode: 384 });
578
586
  try {
579
- import_fs6.default.chmodSync(CONFIG_PATH, 384);
587
+ import_fs7.default.chmodSync(CONFIG_PATH, 384);
580
588
  } catch {
581
589
  }
582
590
  }
@@ -641,21 +649,57 @@ function isValidManifest(m) {
641
649
  }
642
650
 
643
651
  // src/profiles/fetcher.ts
644
- var import_fs7 = __toESM(require("fs"));
652
+ var import_fs8 = __toESM(require("fs"));
645
653
  var import_path5 = __toESM(require("path"));
654
+
655
+ // src/utils/fetch.ts
656
+ var DEFAULT_TIMEOUT_MS = 3e4;
657
+ var NETWORK_ERROR_CODES = /* @__PURE__ */ new Set([
658
+ "ENOTFOUND",
659
+ "ECONNREFUSED",
660
+ "ECONNRESET",
661
+ "ETIMEDOUT",
662
+ "EHOSTUNREACH",
663
+ "ENETUNREACH"
664
+ ]);
665
+ function isNetworkError(err) {
666
+ if (!(err instanceof Error)) return false;
667
+ const cause = err.cause;
668
+ const code = err.code ?? (cause instanceof Error ? cause.code : void 0);
669
+ return NETWORK_ERROR_CODES.has(code ?? "");
670
+ }
671
+ async function fetchWithTimeout(url, init, timeoutMs = DEFAULT_TIMEOUT_MS) {
672
+ const controller = new AbortController();
673
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
674
+ try {
675
+ return await fetch(url, { ...init, signal: controller.signal });
676
+ } catch (err) {
677
+ if (err.name === "AbortError") {
678
+ throw new Error(`Request timed out after ${timeoutMs / 1e3}s`);
679
+ }
680
+ if (isNetworkError(err)) {
681
+ throw new Error("No internet connection \u2014 try again when you're online");
682
+ }
683
+ throw err;
684
+ } finally {
685
+ clearTimeout(timer);
686
+ }
687
+ }
688
+
689
+ // src/profiles/fetcher.ts
646
690
  var REGISTRY_BASE = "https://raw.githubusercontent.com/workscribe/profiles/main";
647
691
  function manifestCachePath() {
648
692
  return import_path5.default.join(PROFILES_DIR, "manifest.json");
649
693
  }
650
694
  async function fetchManifest() {
651
- const response = await fetch(`${REGISTRY_BASE}/manifest.json`);
695
+ const response = await fetchWithTimeout(`${REGISTRY_BASE}/manifest.json`, {});
652
696
  if (!response.ok) throw new Error(`Registry unreachable (${response.status})`);
653
697
  const data = await response.json();
654
698
  if (!isValidManifest(data)) throw new Error("Manifest format unrecognised");
655
699
  return data;
656
700
  }
657
701
  async function fetchProfile(name) {
658
- const response = await fetch(`${REGISTRY_BASE}/${name}.json`);
702
+ const response = await fetchWithTimeout(`${REGISTRY_BASE}/${name}.json`, {});
659
703
  if (response.status === 404) throw new Error(`Profile "${name}" not found in registry`);
660
704
  if (!response.ok) throw new Error(`Registry unreachable (${response.status})`);
661
705
  const data = await response.json();
@@ -666,27 +710,27 @@ async function installProfile(name) {
666
710
  ensureProfilesDir();
667
711
  const profile = await fetchProfile(name);
668
712
  const dest = import_path5.default.join(PROFILES_DIR, `${name}.json`);
669
- import_fs7.default.writeFileSync(dest, JSON.stringify(profile, null, 2), { encoding: "utf-8", mode: 384 });
713
+ import_fs8.default.writeFileSync(dest, JSON.stringify(profile, null, 2), { encoding: "utf-8", mode: 384 });
670
714
  debugLog("profiles", `Installed profile "${name}" to ${dest}`);
671
715
  }
672
716
  async function cacheManifest() {
673
717
  ensureProfilesDir();
674
718
  const manifest = await fetchManifest();
675
- import_fs7.default.writeFileSync(manifestCachePath(), JSON.stringify(manifest, null, 2), { encoding: "utf-8", mode: 384 });
719
+ import_fs8.default.writeFileSync(manifestCachePath(), JSON.stringify(manifest, null, 2), { encoding: "utf-8", mode: 384 });
676
720
  return manifest;
677
721
  }
678
722
  function readCachedManifest() {
679
- if (!import_fs7.default.existsSync(manifestCachePath())) return null;
723
+ if (!import_fs8.default.existsSync(manifestCachePath())) return null;
680
724
  try {
681
- const data = JSON.parse(import_fs7.default.readFileSync(manifestCachePath(), "utf-8"));
725
+ const data = JSON.parse(import_fs8.default.readFileSync(manifestCachePath(), "utf-8"));
682
726
  return isValidManifest(data) ? data : null;
683
727
  } catch {
684
728
  return null;
685
729
  }
686
730
  }
687
731
  function ensureProfilesDir() {
688
- if (!import_fs7.default.existsSync(PROFILES_DIR)) {
689
- import_fs7.default.mkdirSync(PROFILES_DIR, { recursive: true, mode: 448 });
732
+ if (!import_fs8.default.existsSync(PROFILES_DIR)) {
733
+ import_fs8.default.mkdirSync(PROFILES_DIR, { recursive: true, mode: 448 });
690
734
  }
691
735
  }
692
736
 
@@ -713,13 +757,13 @@ function compileRule(profileName, rule) {
713
757
  }
714
758
  function loadProfileFromDisk(name) {
715
759
  const filePath = import_path6.default.join(PROFILES_DIR, `${name}.json`);
716
- if (!import_fs8.default.existsSync(filePath)) {
760
+ if (!import_fs9.default.existsSync(filePath)) {
717
761
  debugLog("profiles", `Profile "${name}" not found in cache \u2014 run: workscribe profile add ${name}`);
718
762
  return null;
719
763
  }
720
764
  let candidate;
721
765
  try {
722
- candidate = JSON.parse(import_fs8.default.readFileSync(filePath, "utf-8"));
766
+ candidate = JSON.parse(import_fs9.default.readFileSync(filePath, "utf-8"));
723
767
  } catch {
724
768
  debugLog("profiles", `Profile "${name}" contains invalid JSON \u2014 skipping`);
725
769
  return null;
@@ -751,8 +795,8 @@ function getActiveProfileAiContexts() {
751
795
  });
752
796
  }
753
797
  function getCachedProfiles() {
754
- if (!import_fs8.default.existsSync(PROFILES_DIR)) return [];
755
- return import_fs8.default.readdirSync(PROFILES_DIR).filter((file) => file.endsWith(".json") && file !== "manifest.json").flatMap((file) => {
798
+ if (!import_fs9.default.existsSync(PROFILES_DIR)) return [];
799
+ return import_fs9.default.readdirSync(PROFILES_DIR).filter((file) => file.endsWith(".json") && file !== "manifest.json").flatMap((file) => {
756
800
  const profile = loadProfileFromDisk(file.replace(/\.json$/, ""));
757
801
  return profile ? [profile] : [];
758
802
  });
@@ -764,7 +808,7 @@ function detectProfilesForDirectory(dir) {
764
808
  return manifest.filter((entry) => {
765
809
  if (activeNames.has(entry.name)) return false;
766
810
  const indicatorFiles = entry.detect?.files ?? [];
767
- return indicatorFiles.length > 0 && indicatorFiles.every((file) => import_fs8.default.existsSync(import_path6.default.join(dir, file)));
811
+ return indicatorFiles.length > 0 && indicatorFiles.every((file) => import_fs9.default.existsSync(import_path6.default.join(dir, file)));
768
812
  });
769
813
  }
770
814
 
@@ -1032,7 +1076,7 @@ function isDuplicate(command, cwd, lastEvent, dedupWindowMs = 5e3, now = Date.no
1032
1076
 
1033
1077
  // src/capture/index.ts
1034
1078
  function captureCommand(opts) {
1035
- if (import_fs9.default.existsSync(PAUSE_FLAG_PATH)) return;
1079
+ if (import_fs10.default.existsSync(PAUSE_FLAG_PATH)) return;
1036
1080
  const config = readConfig();
1037
1081
  const { command, cwd, exitCode, durationMs } = opts;
1038
1082
  if (shouldFilter(command, config.capture.ignore)) return;
@@ -1133,9 +1177,9 @@ function registerHook(program2) {
1133
1177
  }
1134
1178
 
1135
1179
  // src/cli/init.ts
1136
- var import_fs12 = __toESM(require("fs"));
1180
+ var import_fs13 = __toESM(require("fs"));
1137
1181
  var import_path9 = __toESM(require("path"));
1138
- var import_os4 = __toESM(require("os"));
1182
+ var import_os5 = __toESM(require("os"));
1139
1183
  var import_prompts = __toESM(require("prompts"));
1140
1184
  var import_chalk2 = __toESM(require("chalk"));
1141
1185
 
@@ -1353,24 +1397,7 @@ function buildWeeklyMessage(payload) {
1353
1397
  return lines.join("\n");
1354
1398
  }
1355
1399
 
1356
- // src/ai/fetch.ts
1357
- var DEFAULT_TIMEOUT_MS = 3e4;
1358
- async function fetchWithTimeout(url, init, timeoutMs = DEFAULT_TIMEOUT_MS) {
1359
- const controller = new AbortController();
1360
- const timer = setTimeout(() => controller.abort(), timeoutMs);
1361
- try {
1362
- return await fetch(url, { ...init, signal: controller.signal });
1363
- } catch (err) {
1364
- if (err.name === "AbortError") {
1365
- throw new Error(`Request timed out after ${timeoutMs / 1e3}s`);
1366
- }
1367
- throw err;
1368
- } finally {
1369
- clearTimeout(timer);
1370
- }
1371
- }
1372
-
1373
- // src/ai/providers/index.ts
1400
+ // src/ai/generic-provider.ts
1374
1401
  var GenericProvider = class {
1375
1402
  constructor(config, model, apiKey, baseUrl, includeCategories = []) {
1376
1403
  this.config = config;
@@ -1414,7 +1441,7 @@ var GenericProvider = class {
1414
1441
  }
1415
1442
  };
1416
1443
 
1417
- // src/ai/providers/config.ts
1444
+ // src/ai/provider-config.ts
1418
1445
  var PROVIDER_CONFIGS = {
1419
1446
  anthropic: {
1420
1447
  defaultModel: "claude-haiku-4-5-20251001",
@@ -1538,14 +1565,72 @@ Run: workscribe config set ai.apiKey <your-key>`);
1538
1565
  }
1539
1566
 
1540
1567
  // src/capture/shell-hook.ts
1541
- var import_fs10 = __toESM(require("fs"));
1568
+ var import_fs11 = __toESM(require("fs"));
1542
1569
  var import_path7 = __toESM(require("path"));
1570
+ var import_os3 = __toESM(require("os"));
1571
+
1572
+ // src/utils/completion-scripts.ts
1573
+ function bashCompletion(commands) {
1574
+ const names = commands.map((command) => command.name).join(" ");
1575
+ return `_workscribe_completion() {
1576
+ local cur="\${COMP_WORDS[COMP_CWORD]}"
1577
+ local prev="\${COMP_WORDS[COMP_CWORD-1]}"
1578
+ local commands="${names}"
1579
+ case "$prev" in
1580
+ completions) COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") ); return ;;
1581
+ config) COMPREPLY=( $(compgen -W "get set list" -- "$cur") ); return ;;
1582
+ profile) COMPREPLY=( $(compgen -W "list add remove update detect suggest" -- "$cur") ); return ;;
1583
+ esac
1584
+ COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
1585
+ }
1586
+ complete -F _workscribe_completion workscribe`;
1587
+ }
1588
+ function zshCompletion(commands) {
1589
+ const commandList = commands.map((command) => ` '${command.name}:${command.description}'`).join("\n");
1590
+ return `#compdef workscribe
1591
+ _workscribe() {
1592
+ local state
1593
+ _arguments '1: :->command' '*: :->args'
1594
+ case $state in
1595
+ command)
1596
+ local -a commands
1597
+ commands=(
1598
+ ${commandList}
1599
+ )
1600
+ _describe 'command' commands
1601
+ ;;
1602
+ args)
1603
+ case $words[2] in
1604
+ completions) local -a shells; shells=('bash' 'zsh' 'fish'); _describe 'shell' shells ;;
1605
+ config) local -a sub; sub=('get:Get a value' 'set:Set a value' 'list:List all values'); _describe 'subcommand' sub ;;
1606
+ profile) local -a sub; sub=('list' 'add' 'remove' 'update' 'detect' 'suggest'); _describe 'subcommand' sub ;;
1607
+ esac
1608
+ ;;
1609
+ esac
1610
+ }
1611
+ _workscribe`;
1612
+ }
1613
+ function fishCompletion(commands) {
1614
+ const lines = [
1615
+ "complete -c workscribe -f",
1616
+ ...commands.map(
1617
+ (command) => `complete -c workscribe -n "__fish_use_subcommand" -a ${command.name.padEnd(12)} -d "${command.description}"`
1618
+ ),
1619
+ 'complete -c workscribe -n "__fish_seen_subcommand_from completions" -a "bash zsh fish"',
1620
+ 'complete -c workscribe -n "__fish_seen_subcommand_from config" -a "get set list"',
1621
+ 'complete -c workscribe -n "__fish_seen_subcommand_from profile" -a "list add remove update detect suggest"'
1622
+ ];
1623
+ return lines.join("\n");
1624
+ }
1625
+
1626
+ // src/capture/shell-hook.ts
1627
+ var FISH_COMPLETIONS_DIR = import_path7.default.join(import_os3.default.homedir(), ".config", "fish", "completions");
1543
1628
  var HOOK_START = "# workscribe:hook:start";
1544
1629
  var HOOK_END = "# workscribe:hook:end";
1545
1630
  var HOOK_PREAMBLE = "# Workscribe \u2014 developer work journal";
1546
1631
  function getInstalledHookBlock(rcFile) {
1547
- if (!import_fs10.default.existsSync(rcFile)) return null;
1548
- const content = import_fs10.default.readFileSync(rcFile, "utf-8");
1632
+ if (!import_fs11.default.existsSync(rcFile)) return null;
1633
+ const content = import_fs11.default.readFileSync(rcFile, "utf-8");
1549
1634
  const startIdx = content.indexOf(HOOK_START);
1550
1635
  const endIdx = content.indexOf(HOOK_END);
1551
1636
  if (startIdx === -1 || endIdx === -1) return null;
@@ -1561,39 +1646,70 @@ function isHookOutdated(rcFile, hookContent) {
1561
1646
  }
1562
1647
  function installHook(rcFile, hookContent) {
1563
1648
  const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
1564
- import_fs10.default.copyFileSync(rcFile, backup);
1649
+ import_fs11.default.copyFileSync(rcFile, backup);
1565
1650
  log.info(`Backed up ${rcFile} \u2192 ${import_path7.default.basename(backup)}`);
1566
- import_fs10.default.appendFileSync(rcFile, `
1651
+ import_fs11.default.appendFileSync(rcFile, `
1567
1652
  ${hookContent}
1568
1653
  `);
1569
1654
  }
1570
1655
  function updateHook(rcFile, hookContent) {
1571
1656
  const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
1572
- import_fs10.default.copyFileSync(rcFile, backup);
1657
+ import_fs11.default.copyFileSync(rcFile, backup);
1573
1658
  log.info(`Backed up ${rcFile} \u2192 ${import_path7.default.basename(backup)}`);
1574
- const content = import_fs10.default.readFileSync(rcFile, "utf-8");
1659
+ const content = import_fs11.default.readFileSync(rcFile, "utf-8");
1575
1660
  const startIdx = content.indexOf(HOOK_START);
1576
1661
  const endIdx = content.indexOf(HOOK_END) + HOOK_END.length;
1577
- import_fs10.default.writeFileSync(rcFile, content.slice(0, startIdx) + hookContent + content.slice(endIdx));
1662
+ import_fs11.default.writeFileSync(rcFile, content.slice(0, startIdx) + hookContent + content.slice(endIdx));
1578
1663
  }
1579
1664
  function removeHook(rcFile) {
1580
1665
  const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
1581
- import_fs10.default.copyFileSync(rcFile, backup);
1666
+ import_fs11.default.copyFileSync(rcFile, backup);
1582
1667
  log.info(`Backed up ${rcFile} \u2192 ${import_path7.default.basename(backup)}`);
1583
- const content = import_fs10.default.readFileSync(rcFile, "utf-8");
1668
+ const content = import_fs11.default.readFileSync(rcFile, "utf-8");
1584
1669
  const startIdx = content.indexOf(HOOK_START);
1585
1670
  const endIdx = content.indexOf(HOOK_END) + HOOK_END.length;
1586
1671
  const preambleIdx = content.lastIndexOf(HOOK_PREAMBLE, startIdx);
1587
1672
  const blockStart = preambleIdx !== -1 ? preambleIdx : startIdx;
1588
1673
  const before = content.slice(0, blockStart).replace(/\n+$/, "\n");
1589
1674
  const after = content.slice(endIdx).replace(/^\n/, "");
1590
- import_fs10.default.writeFileSync(rcFile, before + after);
1675
+ import_fs11.default.writeFileSync(rcFile, before + after);
1676
+ }
1677
+ function isCompletionsInstalled(shell) {
1678
+ if (shell.name === "fish") {
1679
+ return import_fs11.default.existsSync(import_path7.default.join(FISH_COMPLETIONS_DIR, "workscribe.fish"));
1680
+ }
1681
+ try {
1682
+ return import_fs11.default.readFileSync(shell.rcFile, "utf-8").includes("workscribe completions");
1683
+ } catch {
1684
+ return false;
1685
+ }
1686
+ }
1687
+ function installCompletions(shell, commands) {
1688
+ if (shell.name === "fish") {
1689
+ if (!import_fs11.default.existsSync(FISH_COMPLETIONS_DIR)) {
1690
+ import_fs11.default.mkdirSync(FISH_COMPLETIONS_DIR, { recursive: true });
1691
+ }
1692
+ import_fs11.default.writeFileSync(import_path7.default.join(FISH_COMPLETIONS_DIR, "workscribe.fish"), fishCompletion(commands), { mode: 420 });
1693
+ } else {
1694
+ const script = shell.name === "zsh" ? zshCompletion(commands) : bashCompletion(commands);
1695
+ const cacheFile = import_path7.default.join(WORKSCRIBE_DIR, `completion.${shell.name}`);
1696
+ import_fs11.default.writeFileSync(cacheFile, script, { mode: 420 });
1697
+ const sourceLine = shell.name === "zsh" ? `
1698
+ # Workscribe completions
1699
+ autoload -Uz compinit && compinit
1700
+ source "${cacheFile}"
1701
+ ` : `
1702
+ # Workscribe completions
1703
+ source "${cacheFile}"
1704
+ `;
1705
+ import_fs11.default.appendFileSync(shell.rcFile, sourceLine);
1706
+ }
1591
1707
  }
1592
1708
 
1593
1709
  // src/capture/hooks/claude-code.ts
1594
- var import_fs11 = __toESM(require("fs"));
1710
+ var import_fs12 = __toESM(require("fs"));
1595
1711
  var import_path8 = __toESM(require("path"));
1596
- var import_os3 = __toESM(require("os"));
1712
+ var import_os4 = __toESM(require("os"));
1597
1713
 
1598
1714
  // src/capture/hooks/utils.ts
1599
1715
  var import_child_process3 = require("child_process");
@@ -1607,11 +1723,11 @@ function isCommandInstalled(cmd) {
1607
1723
  }
1608
1724
 
1609
1725
  // src/capture/hooks/claude-code.ts
1610
- var CLAUDE_SETTINGS_PATH = import_path8.default.join(import_os3.default.homedir(), ".claude", "settings.json");
1726
+ var CLAUDE_SETTINGS_PATH = import_path8.default.join(import_os4.default.homedir(), ".claude", "settings.json");
1611
1727
  function readSettings() {
1612
- if (!import_fs11.default.existsSync(CLAUDE_SETTINGS_PATH)) return {};
1728
+ if (!import_fs12.default.existsSync(CLAUDE_SETTINGS_PATH)) return {};
1613
1729
  try {
1614
- return JSON.parse(import_fs11.default.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8"));
1730
+ return JSON.parse(import_fs12.default.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8"));
1615
1731
  } catch {
1616
1732
  return {};
1617
1733
  }
@@ -1646,8 +1762,8 @@ var claudeCodeIntegration = {
1646
1762
  }
1647
1763
  ]
1648
1764
  };
1649
- import_fs11.default.mkdirSync(import_path8.default.dirname(CLAUDE_SETTINGS_PATH), { recursive: true });
1650
- import_fs11.default.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
1765
+ import_fs12.default.mkdirSync(import_path8.default.dirname(CLAUDE_SETTINGS_PATH), { recursive: true });
1766
+ import_fs12.default.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
1651
1767
  }
1652
1768
  };
1653
1769
 
@@ -1656,11 +1772,34 @@ var AI_HOOK_INTEGRATIONS = {
1656
1772
  "claude-code": claudeCodeIntegration
1657
1773
  };
1658
1774
 
1775
+ // src/cli/completions.ts
1776
+ var HIDDEN_COMMANDS = /* @__PURE__ */ new Set(["_capture", "_hook"]);
1777
+ function getPublicCommands(program2) {
1778
+ return program2.commands.filter((command) => !HIDDEN_COMMANDS.has(command.name())).map((command) => ({ name: command.name(), description: command.description() }));
1779
+ }
1780
+ function registerCompletions(program2) {
1781
+ program2.command("completions [shell]").description("Print shell completion script (bash, zsh, fish)").action((shell) => {
1782
+ const detected = shell ?? (process.env.SHELL ?? "").split("/").at(-1);
1783
+ if (!detected || !["bash", "zsh", "fish"].includes(detected)) {
1784
+ log.error(`Unknown shell: "${detected ?? ""}". Supported: bash, zsh, fish`);
1785
+ log.plain(` Usage: workscribe completions <bash|zsh|fish>`);
1786
+ process.exit(1);
1787
+ }
1788
+ const commands = getPublicCommands(program2);
1789
+ const script = detected === "bash" ? bashCompletion(commands) : detected === "zsh" ? zshCompletion(commands) : fishCompletion(commands);
1790
+ process.stdout.write(script + "\n");
1791
+ });
1792
+ }
1793
+
1659
1794
  // src/cli/init.ts
1660
1795
  function detectShell() {
1661
1796
  const shell = process.env.SHELL ?? "";
1662
- if (shell.includes("zsh")) return { name: "zsh", rcFile: import_path9.default.join(import_os4.default.homedir(), ".zshrc") };
1663
- if (shell.includes("bash")) return { name: "bash", rcFile: import_path9.default.join(import_os4.default.homedir(), ".bashrc") };
1797
+ if (shell.includes("zsh")) return { name: "zsh", rcFile: import_path9.default.join(import_os5.default.homedir(), ".zshrc") };
1798
+ if (shell.includes("bash")) return { name: "bash", rcFile: import_path9.default.join(import_os5.default.homedir(), ".bashrc") };
1799
+ if (shell.includes("fish")) {
1800
+ const confDir = import_path9.default.join(import_os5.default.homedir(), ".config", "fish", "conf.d");
1801
+ return { name: "fish", rcFile: import_path9.default.join(confDir, "workscribe.fish") };
1802
+ }
1664
1803
  return null;
1665
1804
  }
1666
1805
  function step(current, total, label) {
@@ -1671,19 +1810,21 @@ function registerInit(program2) {
1671
1810
  program2.command("init").description("Set up Workscribe and install or update shell hooks").action(async () => {
1672
1811
  const TOTAL_STEPS = 5;
1673
1812
  log.plain(import_chalk2.default.bold("\n Workscribe \u2014 Developer Work Journal\n"));
1674
- step(1, TOTAL_STEPS, "Creating data directory");
1675
- if (!import_fs12.default.existsSync(WORKSCRIBE_DIR)) {
1676
- import_fs12.default.mkdirSync(WORKSCRIBE_DIR, { recursive: true, mode: 448 });
1677
- log.success(`Created ${WORKSCRIBE_DIR}`);
1678
- } else {
1679
- try {
1680
- import_fs12.default.chmodSync(WORKSCRIBE_DIR, 448);
1681
- } catch {
1813
+ step(1, TOTAL_STEPS, "Creating data directories");
1814
+ for (const dir of REQUIRED_DIRS) {
1815
+ if (!import_fs13.default.existsSync(dir)) {
1816
+ import_fs13.default.mkdirSync(dir, { recursive: true, mode: 448 });
1817
+ log.success(`Created ${dir}`);
1818
+ } else {
1819
+ try {
1820
+ import_fs13.default.chmodSync(dir, 448);
1821
+ } catch {
1822
+ }
1823
+ log.info(`${dir} already exists`);
1682
1824
  }
1683
- log.info(`${WORKSCRIBE_DIR} already exists`);
1684
1825
  }
1685
- if (!import_fs12.default.existsSync(PROFILES_DIR)) {
1686
- import_fs12.default.mkdirSync(PROFILES_DIR, { recursive: true, mode: 448 });
1826
+ if (!import_fs13.default.existsSync(PROFILES_DIR)) {
1827
+ import_fs13.default.mkdirSync(PROFILES_DIR, { recursive: true, mode: 448 });
1687
1828
  }
1688
1829
  step(2, TOTAL_STEPS, "Initialising database");
1689
1830
  getDb();
@@ -1691,15 +1832,21 @@ function registerInit(program2) {
1691
1832
  step(3, TOTAL_STEPS, "Installing shell hook");
1692
1833
  const shell = detectShell();
1693
1834
  if (!shell) {
1694
- log.warn("Could not detect shell. Supported: zsh, bash");
1695
- log.plain(`Manually add the hook from: ${import_chalk2.default.cyan("shell/hook.zsh")} or ${import_chalk2.default.cyan("shell/hook.bash")}`);
1835
+ log.warn("Could not detect shell. Supported: zsh, bash, fish");
1836
+ log.plain(
1837
+ ` Manually add the hook from: ${import_chalk2.default.cyan("shell/hook.zsh")}, ${import_chalk2.default.cyan("shell/hook.bash")}, or ${import_chalk2.default.cyan("shell/hook.fish")}`
1838
+ );
1696
1839
  } else {
1697
1840
  const hookFile = import_path9.default.join(__dirname, "..", "shell", `hook.${shell.name}`);
1698
- if (!import_fs12.default.existsSync(hookFile)) {
1841
+ if (!import_fs13.default.existsSync(hookFile)) {
1699
1842
  log.warn(`Hook template not found at ${hookFile}`);
1700
1843
  } else {
1701
- const hookContent = import_fs12.default.readFileSync(hookFile, "utf-8");
1702
- if (!import_fs12.default.existsSync(shell.rcFile)) import_fs12.default.writeFileSync(shell.rcFile, "");
1844
+ const hookContent = import_fs13.default.readFileSync(hookFile, "utf-8");
1845
+ if (shell.name === "fish") {
1846
+ const confDir = import_path9.default.dirname(shell.rcFile);
1847
+ if (!import_fs13.default.existsSync(confDir)) import_fs13.default.mkdirSync(confDir, { recursive: true });
1848
+ }
1849
+ if (!import_fs13.default.existsSync(shell.rcFile)) import_fs13.default.writeFileSync(shell.rcFile, "");
1703
1850
  if (!isHookInstalled(shell.rcFile)) {
1704
1851
  log.plain("");
1705
1852
  log.plain(import_chalk2.default.dim(" Workscribe adds a lightweight hook to your shell that captures each"));
@@ -1725,6 +1872,24 @@ function registerInit(program2) {
1725
1872
  } else {
1726
1873
  log.info(`Shell hook up to date in ${shell.rcFile}`);
1727
1874
  }
1875
+ if (isCompletionsInstalled(shell)) {
1876
+ log.info("Shell completions already installed");
1877
+ } else {
1878
+ const { installComps } = await (0, import_prompts.default)({
1879
+ type: "confirm",
1880
+ name: "installComps",
1881
+ message: "Install shell completions (tab-complete workscribe commands)?",
1882
+ initial: true
1883
+ });
1884
+ if (installComps) {
1885
+ try {
1886
+ installCompletions(shell, getPublicCommands(program2));
1887
+ log.success("Shell completions installed");
1888
+ } catch (error) {
1889
+ log.warn(`Completions install failed: ${error.message}`);
1890
+ }
1891
+ }
1892
+ }
1728
1893
  }
1729
1894
  }
1730
1895
  step(4, TOTAL_STEPS, "Detecting AI coding tools");
@@ -1855,7 +2020,7 @@ function registerInit(program2) {
1855
2020
  }
1856
2021
  }
1857
2022
  }
1858
- if (!import_fs12.default.existsSync(CONFIG_PATH)) {
2023
+ if (!import_fs13.default.existsSync(CONFIG_PATH)) {
1859
2024
  writeConfig(config);
1860
2025
  }
1861
2026
  log.plain("");
@@ -1863,8 +2028,13 @@ function registerInit(program2) {
1863
2028
  log.plain("");
1864
2029
  if (shell) {
1865
2030
  log.plain(` Activate capture now:`);
1866
- log.plain(` ${import_chalk2.default.cyan(`source ${shell.rcFile}`)}`);
1867
- log.plain(` Or restart your terminal.`);
2031
+ if (shell.name === "fish") {
2032
+ log.plain(` Restart your terminal`);
2033
+ log.plain(` ${import_chalk2.default.dim(`or run: source ${shell.rcFile}`)}`);
2034
+ } else {
2035
+ log.plain(` ${import_chalk2.default.cyan(`source ${shell.rcFile}`)}`);
2036
+ log.plain(` Or restart your terminal.`);
2037
+ }
1868
2038
  log.plain("");
1869
2039
  }
1870
2040
  if (providerConfigured) {
@@ -1886,7 +2056,7 @@ function registerInit(program2) {
1886
2056
  }
1887
2057
 
1888
2058
  // src/cli/status.ts
1889
- var import_fs13 = __toESM(require("fs"));
2059
+ var import_fs14 = __toESM(require("fs"));
1890
2060
  var import_chalk3 = __toESM(require("chalk"));
1891
2061
  function registerStatus(program2) {
1892
2062
  program2.command("status").description("Show capture status and event counts").action(() => {
@@ -1894,7 +2064,7 @@ function registerStatus(program2) {
1894
2064
  const total = getEventCount();
1895
2065
  const todayEvents = getEventsForDate(todayDate());
1896
2066
  const last = getLastEvent();
1897
- const isPaused = import_fs13.default.existsSync(PAUSE_FLAG_PATH);
2067
+ const isPaused = import_fs14.default.existsSync(PAUSE_FLAG_PATH);
1898
2068
  log.plain("");
1899
2069
  log.plain(import_chalk3.default.bold(" Workscribe Status"));
1900
2070
  log.plain("");
@@ -2154,8 +2324,8 @@ function registerSessions(program2) {
2154
2324
 
2155
2325
  // src/cli/summary.ts
2156
2326
  var import_chalk6 = __toESM(require("chalk"));
2157
- var import_fs14 = __toESM(require("fs"));
2158
- var import_os5 = __toESM(require("os"));
2327
+ var import_fs15 = __toESM(require("fs"));
2328
+ var import_os6 = __toESM(require("os"));
2159
2329
  var import_path10 = __toESM(require("path"));
2160
2330
 
2161
2331
  // src/integrations/webhook.ts
@@ -2224,7 +2394,7 @@ async function handleWebhook(opts, config, summary) {
2224
2394
  if (webhookUrl) {
2225
2395
  try {
2226
2396
  log.info("Posting to webhook\u2026");
2227
- const username = import_os5.default.userInfo().username;
2397
+ const username = import_os6.default.userInfo().username;
2228
2398
  const body = isSlackWebhookUrl(webhookUrl) ? formatForSlack(summary, username) : summary;
2229
2399
  await postToWebhook(webhookUrl, body);
2230
2400
  log.success("Posted.");
@@ -2262,8 +2432,8 @@ function registerSummary(program2) {
2262
2432
  }
2263
2433
  const cacheKey = isoWeekKey(today);
2264
2434
  const cacheFile = import_path10.default.join(WEEKLY_CACHE_DIR, `${cacheKey}.md`);
2265
- if (!opts.fresh && import_fs14.default.existsSync(cacheFile)) {
2266
- const cached = import_fs14.default.readFileSync(cacheFile, "utf-8");
2435
+ if (!opts.fresh && import_fs15.default.existsSync(cacheFile)) {
2436
+ const cached = import_fs15.default.readFileSync(cacheFile, "utf-8");
2267
2437
  log.plain("");
2268
2438
  log.plain(cached);
2269
2439
  log.plain("");
@@ -2279,8 +2449,8 @@ function registerSummary(program2) {
2279
2449
  log.plain("");
2280
2450
  log.plain(summary);
2281
2451
  log.plain("");
2282
- import_fs14.default.mkdirSync(WEEKLY_CACHE_DIR, { recursive: true });
2283
- import_fs14.default.writeFileSync(cacheFile, summary, { encoding: "utf-8", mode: 384 });
2452
+ import_fs15.default.mkdirSync(WEEKLY_CACHE_DIR, { recursive: true });
2453
+ import_fs15.default.writeFileSync(cacheFile, summary, { encoding: "utf-8", mode: 384 });
2284
2454
  await handleWebhook(opts, config, summary);
2285
2455
  } catch (error) {
2286
2456
  log.error(`Weekly summary failed [${config.ai.provider}]: ${error.message}`);
@@ -2398,7 +2568,7 @@ ${bullets.trim()}`;
2398
2568
  }
2399
2569
 
2400
2570
  // src/cli/export.ts
2401
- var import_fs15 = __toESM(require("fs"));
2571
+ var import_fs16 = __toESM(require("fs"));
2402
2572
  var import_path11 = __toESM(require("path"));
2403
2573
  function eventRowToPayload2(row) {
2404
2574
  let metadata = {};
@@ -2456,8 +2626,8 @@ function registerExport(program2) {
2456
2626
  log.info(`Generating summary via ${config.ai.provider}\u2026`);
2457
2627
  const summary = await provider.summarize(payload);
2458
2628
  const outputPath = opts.output ?? import_path11.default.join(EXPORTS_DIR, `workscribe-${date}.md`);
2459
- if (!import_fs15.default.existsSync(EXPORTS_DIR)) import_fs15.default.mkdirSync(EXPORTS_DIR, { recursive: true, mode: 448 });
2460
- import_fs15.default.writeFileSync(outputPath, `# Work Summary \u2014 ${date}
2629
+ if (!import_fs16.default.existsSync(EXPORTS_DIR)) import_fs16.default.mkdirSync(EXPORTS_DIR, { recursive: true, mode: 448 });
2630
+ import_fs16.default.writeFileSync(outputPath, `# Work Summary \u2014 ${date}
2461
2631
 
2462
2632
  ${summary}
2463
2633
  `, { encoding: "utf-8", mode: 384 });
@@ -2529,22 +2699,22 @@ function printConfig(obj, indent) {
2529
2699
  }
2530
2700
 
2531
2701
  // src/cli/pause.ts
2532
- var import_fs16 = __toESM(require("fs"));
2702
+ var import_fs17 = __toESM(require("fs"));
2533
2703
  function registerPause(program2) {
2534
2704
  program2.command("pause").description("Pause capture temporarily").action(() => {
2535
- if (import_fs16.default.existsSync(PAUSE_FLAG_PATH)) {
2705
+ if (import_fs17.default.existsSync(PAUSE_FLAG_PATH)) {
2536
2706
  log.info("Capture is already paused. Run workscribe resume to restart.");
2537
2707
  return;
2538
2708
  }
2539
- import_fs16.default.writeFileSync(PAUSE_FLAG_PATH, "", { mode: 384 });
2709
+ import_fs17.default.writeFileSync(PAUSE_FLAG_PATH, "", { mode: 384 });
2540
2710
  log.success("Capture paused. Run workscribe resume to restart.");
2541
2711
  });
2542
2712
  program2.command("resume").description("Resume capture after a pause").action(() => {
2543
- if (!import_fs16.default.existsSync(PAUSE_FLAG_PATH)) {
2713
+ if (!import_fs17.default.existsSync(PAUSE_FLAG_PATH)) {
2544
2714
  log.info("Capture is not paused.");
2545
2715
  return;
2546
2716
  }
2547
- import_fs16.default.rmSync(PAUSE_FLAG_PATH);
2717
+ import_fs17.default.rmSync(PAUSE_FLAG_PATH);
2548
2718
  log.success("Capture resumed.");
2549
2719
  });
2550
2720
  }
@@ -2629,18 +2799,19 @@ function registerClear(program2) {
2629
2799
  }
2630
2800
 
2631
2801
  // src/cli/uninstall.ts
2632
- var import_fs17 = __toESM(require("fs"));
2802
+ var import_fs18 = __toESM(require("fs"));
2633
2803
  var import_path12 = __toESM(require("path"));
2634
- var import_os6 = __toESM(require("os"));
2804
+ var import_os7 = __toESM(require("os"));
2635
2805
  var import_prompts3 = __toESM(require("prompts"));
2636
2806
  var import_chalk9 = __toESM(require("chalk"));
2637
2807
  function detectShellRcFiles() {
2638
2808
  const candidates = [
2639
- { name: "zsh", rcFile: import_path12.default.join(import_os6.default.homedir(), ".zshrc") },
2640
- { name: "bash", rcFile: import_path12.default.join(import_os6.default.homedir(), ".bashrc") },
2641
- { name: "bash", rcFile: import_path12.default.join(import_os6.default.homedir(), ".bash_profile") }
2809
+ { name: "zsh", rcFile: import_path12.default.join(import_os7.default.homedir(), ".zshrc") },
2810
+ { name: "bash", rcFile: import_path12.default.join(import_os7.default.homedir(), ".bashrc") },
2811
+ { name: "bash", rcFile: import_path12.default.join(import_os7.default.homedir(), ".bash_profile") },
2812
+ { name: "fish", rcFile: import_path12.default.join(import_os7.default.homedir(), ".config", "fish", "conf.d", "workscribe.fish") }
2642
2813
  ];
2643
- return candidates.filter((candidate) => import_fs17.default.existsSync(candidate.rcFile) && isHookInstalled(candidate.rcFile));
2814
+ return candidates.filter((candidate) => import_fs18.default.existsSync(candidate.rcFile) && isHookInstalled(candidate.rcFile));
2644
2815
  }
2645
2816
  function registerUninstall(program2) {
2646
2817
  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) => {
@@ -2650,16 +2821,23 @@ function registerUninstall(program2) {
2650
2821
  log.info("No shell hooks found");
2651
2822
  } else {
2652
2823
  for (const { name, rcFile } of hooked) {
2653
- removeHook(rcFile);
2654
- log.success(`Shell hook removed from ${rcFile}`);
2655
- log.plain(` ${import_chalk9.default.dim(`Run: source ${rcFile}`)} (or restart your terminal to take effect)`);
2656
- log.plain(
2657
- ` ${import_chalk9.default.dim(`${name === "zsh" ? "unfunction" : "unset -f"} _workscribe_preexec _workscribe_precmd`)}`
2658
- );
2824
+ if (name === "fish") {
2825
+ import_fs18.default.unlinkSync(rcFile);
2826
+ log.success(`Shell hook removed: ${rcFile}`);
2827
+ log.plain(` ${import_chalk9.default.dim("Restart your terminal to take effect")}`);
2828
+ } else {
2829
+ removeHook(rcFile);
2830
+ log.success(`Shell hook removed from ${rcFile}`);
2831
+ log.plain(` ${import_chalk9.default.dim(`Run: source ${rcFile}`)} (or restart your terminal to take effect)`);
2832
+ log.plain(
2833
+ ` ${import_chalk9.default.dim(`${name === "zsh" ? "unfunction" : "unset -f"} _workscribe_preexec _workscribe_precmd`)}`
2834
+ );
2835
+ }
2659
2836
  }
2660
2837
  }
2661
2838
  if (opts.hooksOnly) return;
2662
- if (import_fs17.default.existsSync(WORKSCRIBE_DIR)) {
2839
+ const existingDirs = [...new Set(REQUIRED_DIRS)].filter((dir) => import_fs18.default.existsSync(dir));
2840
+ if (existingDirs.length > 0) {
2663
2841
  log.plain("");
2664
2842
  const { deleteData } = await (0, import_prompts3.default)({
2665
2843
  type: "confirm",
@@ -2668,8 +2846,10 @@ function registerUninstall(program2) {
2668
2846
  initial: false
2669
2847
  });
2670
2848
  if (deleteData) {
2671
- import_fs17.default.rmSync(WORKSCRIBE_DIR, { recursive: true, force: true });
2672
- log.success(`Deleted ${WORKSCRIBE_DIR}`);
2849
+ for (const dir of existingDirs) {
2850
+ import_fs18.default.rmSync(dir, { recursive: true, force: true });
2851
+ log.success(`Deleted ${dir}`);
2852
+ }
2673
2853
  } else {
2674
2854
  log.info(`Data kept at ${WORKSCRIBE_DIR}`);
2675
2855
  }
@@ -2681,7 +2861,7 @@ function registerUninstall(program2) {
2681
2861
  }
2682
2862
 
2683
2863
  // src/cli/profile.ts
2684
- var import_fs18 = __toESM(require("fs"));
2864
+ var import_fs19 = __toESM(require("fs"));
2685
2865
  var import_path13 = __toESM(require("path"));
2686
2866
  var import_chalk10 = __toESM(require("chalk"));
2687
2867
  var MIN_SUGGEST_COUNT = 5;
@@ -2728,7 +2908,7 @@ function registerProfile(program2) {
2728
2908
  log.plain(import_chalk10.default.dim(" " + "\u2500".repeat(60)));
2729
2909
  for (const entry of entries) {
2730
2910
  const badge = activeNames.has(entry.name) ? import_chalk10.default.green(" [active]") : "";
2731
- const installed = import_fs18.default.existsSync(import_path13.default.join(PROFILES_DIR, `${entry.name}.json`)) ? import_chalk10.default.dim(" installed") : "";
2911
+ const installed = import_fs19.default.existsSync(import_path13.default.join(PROFILES_DIR, `${entry.name}.json`)) ? import_chalk10.default.dim(" installed") : "";
2732
2912
  log.plain(` ${import_chalk10.default.cyan(entry.name.padEnd(18))} ${entry.displayName.padEnd(28)}${badge}${installed}`);
2733
2913
  if (entry.description) log.plain(` ${import_chalk10.default.dim(" ".repeat(18) + entry.description)}`);
2734
2914
  }
@@ -2765,7 +2945,7 @@ function registerProfile(program2) {
2765
2945
  }
2766
2946
  activeNames.delete(name);
2767
2947
  const cachePath = import_path13.default.join(PROFILES_DIR, `${name}.json`);
2768
- if (import_fs18.default.existsSync(cachePath)) import_fs18.default.unlinkSync(cachePath);
2948
+ if (import_fs19.default.existsSync(cachePath)) import_fs19.default.unlinkSync(cachePath);
2769
2949
  log.success(`Profile "${name}" removed`);
2770
2950
  }
2771
2951
  config.capture.profiles = [...activeNames];
@@ -2829,14 +3009,37 @@ function registerProfile(program2) {
2829
3009
  const suggestions = [];
2830
3010
  for (const entry of manifest) {
2831
3011
  if (activeNames.has(entry.name)) continue;
2832
- const matchedCount = toolCounts.filter((tc) => entry.keywords.some((kw) => tc.tool.toLowerCase().includes(kw.toLowerCase()))).reduce((sum, tc) => sum + tc.count, 0);
3012
+ const matchedCount = toolCounts.filter(
3013
+ (toolCount) => entry.keywords.some((keyword) => toolCount.tool.toLowerCase().includes(keyword.toLowerCase()))
3014
+ ).reduce((total, toolCount) => total + toolCount.count, 0);
2833
3015
  if (matchedCount >= MIN_SUGGEST_COUNT) {
2834
3016
  suggestions.push({ entry, count: matchedCount });
2835
3017
  }
2836
3018
  }
2837
3019
  suggestions.sort((a, b) => b.count - a.count);
2838
3020
  if (suggestions.length === 0) {
2839
- log.plain("No profile matches found. Your commands are well covered.");
3021
+ const activeEntries = manifest.filter((entry) => activeNames.has(entry.name));
3022
+ const isToolCoveredByActiveProfile = (tool) => activeEntries.some(
3023
+ (entry) => entry.keywords.some((keyword) => tool.toLowerCase().includes(keyword.toLowerCase()))
3024
+ );
3025
+ const uncoveredTools = toolCounts.filter((toolCount) => !isToolCoveredByActiveProfile(toolCount.tool));
3026
+ if (uncoveredTools.length === 0) {
3027
+ log.plain("");
3028
+ log.plain("Your active profiles cover all unclassified commands.");
3029
+ log.plain(import_chalk10.default.dim(" Future captures will be classified correctly."));
3030
+ log.plain("");
3031
+ } else {
3032
+ log.plain("");
3033
+ log.plain("No profile matches found for your unclassified commands.");
3034
+ log.plain("");
3035
+ log.plain(import_chalk10.default.dim(" Top unclassified tools:"));
3036
+ for (const { tool, count } of uncoveredTools.slice(0, 5)) {
3037
+ log.plain(import_chalk10.default.dim(` ${tool.padEnd(24)} ${count} event${count === 1 ? "" : "s"}`));
3038
+ }
3039
+ log.plain("");
3040
+ log.plain(import_chalk10.default.dim(" Run workscribe events --category other to review them."));
3041
+ log.plain("");
3042
+ }
2840
3043
  return;
2841
3044
  }
2842
3045
  log.plain("");
@@ -2848,8 +3051,25 @@ function registerProfile(program2) {
2848
3051
  );
2849
3052
  }
2850
3053
  log.plain("");
2851
- log.plain(` Run: ${import_chalk10.default.cyan("workscribe profile add " + suggestions.map((s) => s.entry.name).join(" "))}`);
3054
+ log.plain(
3055
+ ` Run: ${import_chalk10.default.cyan("workscribe profile add " + suggestions.map((suggestion) => suggestion.entry.name).join(" "))}`
3056
+ );
2852
3057
  log.plain("");
3058
+ const allCoveredEntries = [
3059
+ ...manifest.filter((entry) => activeNames.has(entry.name)),
3060
+ ...suggestions.map((suggestion) => suggestion.entry)
3061
+ ];
3062
+ const isToolCoveredAfterInstall = (tool) => allCoveredEntries.some(
3063
+ (entry) => entry.keywords.some((keyword) => tool.toLowerCase().includes(keyword.toLowerCase()))
3064
+ );
3065
+ const stillUncovered = toolCounts.filter((toolCount) => !isToolCoveredAfterInstall(toolCount.tool)).slice(0, 5);
3066
+ if (stillUncovered.length > 0) {
3067
+ log.plain(import_chalk10.default.dim(" Still uncovered after installing:"));
3068
+ for (const { tool, count } of stillUncovered) {
3069
+ log.plain(import_chalk10.default.dim(` ${tool.padEnd(24)} ${count} event${count === 1 ? "" : "s"}`));
3070
+ }
3071
+ log.plain("");
3072
+ }
2853
3073
  });
2854
3074
  }
2855
3075
 
@@ -2874,9 +3094,9 @@ function registerNote(program2) {
2874
3094
 
2875
3095
  // src/cli/week.ts
2876
3096
  var import_chalk11 = __toESM(require("chalk"));
2877
- var import_fs19 = __toESM(require("fs"));
3097
+ var import_fs20 = __toESM(require("fs"));
2878
3098
  var import_path14 = __toESM(require("path"));
2879
- var import_os7 = __toESM(require("os"));
3099
+ var import_os8 = __toESM(require("os"));
2880
3100
  function eventRowToPayload3(row) {
2881
3101
  let metadata = {};
2882
3102
  try {
@@ -2915,7 +3135,7 @@ async function postSummary(opts, config, summary) {
2915
3135
  }
2916
3136
  try {
2917
3137
  log.info("Posting to webhook\u2026");
2918
- const username = import_os7.default.userInfo().username;
3138
+ const username = import_os8.default.userInfo().username;
2919
3139
  const body = isSlackWebhookUrl(webhookUrl) ? formatForSlack(summary, username) : summary;
2920
3140
  await postToWebhook(webhookUrl, body);
2921
3141
  log.success("Posted.");
@@ -2949,8 +3169,8 @@ function registerWeek(program2) {
2949
3169
  }
2950
3170
  const cacheKey = isoWeekKey(today);
2951
3171
  const cacheFile = import_path14.default.join(WEEKLY_CACHE_DIR, `${cacheKey}.md`);
2952
- if (!opts.fresh && import_fs19.default.existsSync(cacheFile)) {
2953
- const cached = import_fs19.default.readFileSync(cacheFile, "utf-8");
3172
+ if (!opts.fresh && import_fs20.default.existsSync(cacheFile)) {
3173
+ const cached = import_fs20.default.readFileSync(cacheFile, "utf-8");
2954
3174
  log.plain("");
2955
3175
  log.plain(cached);
2956
3176
  log.plain("");
@@ -2966,8 +3186,8 @@ function registerWeek(program2) {
2966
3186
  log.plain("");
2967
3187
  log.plain(summary);
2968
3188
  log.plain("");
2969
- import_fs19.default.mkdirSync(WEEKLY_CACHE_DIR, { recursive: true });
2970
- import_fs19.default.writeFileSync(cacheFile, summary, { encoding: "utf-8", mode: 384 });
3189
+ import_fs20.default.mkdirSync(WEEKLY_CACHE_DIR, { recursive: true });
3190
+ import_fs20.default.writeFileSync(cacheFile, summary, { encoding: "utf-8", mode: 384 });
2971
3191
  await postSummary(opts, config, summary);
2972
3192
  } catch (error) {
2973
3193
  log.error(`Weekly digest failed [${config.ai.provider}]: ${error.message}`);
@@ -2976,10 +3196,136 @@ function registerWeek(program2) {
2976
3196
  });
2977
3197
  }
2978
3198
 
3199
+ // src/cli/bug-report.ts
3200
+ var import_fs21 = __toESM(require("fs"));
3201
+ var import_os9 = __toESM(require("os"));
3202
+ var import_path15 = __toESM(require("path"));
3203
+ var import_chalk12 = __toESM(require("chalk"));
3204
+ var GITHUB_ISSUES_URL = "https://github.com/workscribe/workscribe/issues/new";
3205
+ var DEBUG_LOG_TAIL_LINES = 100;
3206
+ function tailFile(filePath, lines) {
3207
+ if (!import_fs21.default.existsSync(filePath)) return "(no debug log found \u2014 set WORKSCRIBE_DEBUG=1 to enable)";
3208
+ const content = import_fs21.default.readFileSync(filePath, "utf-8");
3209
+ const allLines = content.split("\n");
3210
+ return allLines.slice(-lines).join("\n");
3211
+ }
3212
+ function redactConfig() {
3213
+ try {
3214
+ const config = readConfig();
3215
+ if (config.ai.apiKey) config.ai.apiKey = "<redacted>";
3216
+ if (config.integrations?.slackWebhook) config.integrations.slackWebhook = "<redacted>";
3217
+ if (config.integrations?.webhook) config.integrations.webhook = "<redacted>";
3218
+ return JSON.stringify(config, null, 2);
3219
+ } catch {
3220
+ return "(could not read config)";
3221
+ }
3222
+ }
3223
+ function registerBugReport(program2) {
3224
+ program2.command("bug-report").description("Generate a diagnostic report for filing a bug").action(() => {
3225
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3226
+ const reportPath = import_path15.default.join(WORKSCRIBE_DIR, `bug-report-${timestamp}.txt`);
3227
+ const systemInfo = [
3228
+ `Workscribe version : ${"1.12.0"}`,
3229
+ `Node.js version : ${process.version}`,
3230
+ `Platform : ${process.platform}`,
3231
+ `Architecture : ${process.arch}`,
3232
+ `OS release : ${import_os9.default.release()}`,
3233
+ `Config path : ${CONFIG_PATH}`,
3234
+ `Data directory : ${WORKSCRIBE_DIR}`
3235
+ ].join("\n");
3236
+ const report = [
3237
+ "=== Workscribe Bug Report ===",
3238
+ `Generated: ${(/* @__PURE__ */ new Date()).toISOString()}`,
3239
+ "",
3240
+ "--- System ---",
3241
+ systemInfo,
3242
+ "",
3243
+ "--- Config (secrets redacted) ---",
3244
+ redactConfig(),
3245
+ "",
3246
+ `--- Debug log (last ${DEBUG_LOG_TAIL_LINES} lines) ---`,
3247
+ tailFile(DEBUG_LOG_PATH, DEBUG_LOG_TAIL_LINES)
3248
+ ].join("\n");
3249
+ try {
3250
+ if (!import_fs21.default.existsSync(WORKSCRIBE_DIR)) {
3251
+ import_fs21.default.mkdirSync(WORKSCRIBE_DIR, { recursive: true, mode: 448 });
3252
+ }
3253
+ import_fs21.default.writeFileSync(reportPath, report, { mode: 384 });
3254
+ } catch (error) {
3255
+ log.error(`Could not write report: ${error.message}`);
3256
+ process.exit(1);
3257
+ }
3258
+ const params = new URLSearchParams({
3259
+ title: `Bug report \u2014 workscribe ${"1.12.0"}`,
3260
+ body: `<!-- Attach ${import_path15.default.basename(reportPath)} or paste the relevant sections below -->
3261
+
3262
+ **Describe the issue:**
3263
+
3264
+ **Steps to reproduce:**
3265
+
3266
+ **Expected behaviour:**`
3267
+ });
3268
+ log.success(`Report written to: ${import_chalk12.default.cyan(reportPath)}`);
3269
+ log.plain("");
3270
+ log.plain(` Open a GitHub issue:`);
3271
+ log.plain(` ${import_chalk12.default.cyan(`${GITHUB_ISSUES_URL}?${params}`)}`);
3272
+ log.plain("");
3273
+ log.plain(import_chalk12.default.dim(` Attach the report or paste the relevant sections into the issue.`));
3274
+ log.plain("");
3275
+ });
3276
+ }
3277
+
3278
+ // src/utils/update-check.ts
3279
+ var import_fs22 = __toESM(require("fs"));
3280
+ var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
3281
+ function readCache() {
3282
+ try {
3283
+ return JSON.parse(import_fs22.default.readFileSync(UPDATE_CHECK_PATH, "utf-8"));
3284
+ } catch {
3285
+ return null;
3286
+ }
3287
+ }
3288
+ function writeCache(cache) {
3289
+ try {
3290
+ import_fs22.default.writeFileSync(UPDATE_CHECK_PATH, JSON.stringify(cache), { mode: 384 });
3291
+ } catch {
3292
+ }
3293
+ }
3294
+ function isNewer(latest, current) {
3295
+ const parse = (version) => version.replace(/^v/, "").split(".").map(Number);
3296
+ const [latestMajor, latestMinor, latestPatch] = parse(latest);
3297
+ const [currentMajor, currentMinor, currentPatch] = parse(current);
3298
+ if (latestMajor !== currentMajor) return latestMajor > currentMajor;
3299
+ if (latestMinor !== currentMinor) return latestMinor > currentMinor;
3300
+ return latestPatch > currentPatch;
3301
+ }
3302
+ function checkForUpdate() {
3303
+ const current = "1.12.0";
3304
+ const cache = readCache();
3305
+ if (cache && isNewer(cache.latestVersion, current)) {
3306
+ process.stderr.write(
3307
+ `
3308
+ Update available ${current} \u2192 ${cache.latestVersion}
3309
+ Run: npm install -g @workscribe/cli
3310
+
3311
+ `
3312
+ );
3313
+ }
3314
+ const now = Date.now();
3315
+ if (!cache || now - cache.lastChecked > CHECK_INTERVAL_MS) {
3316
+ fetch("https://registry.npmjs.org/@workscribe/cli/latest").then((response) => response.json()).then((data) => {
3317
+ const latest = data.version;
3318
+ if (latest) writeCache({ lastChecked: now, latestVersion: latest });
3319
+ }).catch(() => {
3320
+ });
3321
+ }
3322
+ }
3323
+
2979
3324
  // src/index.ts
2980
3325
  assertNodeVersion(process.versions.node);
3326
+ checkForUpdate();
2981
3327
  var program = new import_commander.Command();
2982
- program.name("workscribe").description("Automatic developer work journal").version("1.11.0");
3328
+ program.name("workscribe").description("Automatic developer work journal").version("1.12.0");
2983
3329
  registerCapture(program);
2984
3330
  registerHook(program);
2985
3331
  registerInit(program);
@@ -2996,5 +3342,7 @@ registerUninstall(program);
2996
3342
  registerProfile(program);
2997
3343
  registerNote(program);
2998
3344
  registerWeek(program);
3345
+ registerBugReport(program);
3346
+ registerCompletions(program);
2999
3347
  program.parse();
3000
3348
  //# sourceMappingURL=index.js.map