@workscribe/cli 2.0.0 → 2.0.1

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
@@ -1374,6 +1374,34 @@ var init_migrate = __esm({
1374
1374
  }
1375
1375
  });
1376
1376
 
1377
+ // src/db/session-lookup.ts
1378
+ function findRecentSessionsForRepo(db, lookup, limit) {
1379
+ return db.prepare(RECENT_SESSIONS_FOR_REPO).all({
1380
+ repo_name: lookup.repoName,
1381
+ other_repo_name: lookup.otherRepoName,
1382
+ roots: JSON.stringify(lookup.roots),
1383
+ limit
1384
+ });
1385
+ }
1386
+ var RECENT_SESSIONS_FOR_REPO;
1387
+ var init_session_lookup = __esm({
1388
+ "src/db/session-lookup.ts"() {
1389
+ "use strict";
1390
+ RECENT_SESSIONS_FOR_REPO = `
1391
+ SELECT s.* FROM sessions s
1392
+ WHERE s.repo_name = $repo_name
1393
+ OR (s.repo_name = $other_repo_name AND EXISTS (
1394
+ SELECT 1 FROM session_events se
1395
+ INNER JOIN events e ON e.id = se.event_id
1396
+ INNER JOIN json_each($roots) root
1397
+ WHERE se.session_id = s.id
1398
+ AND (e.cwd = root.value OR substr(e.cwd, 1, length(root.value) + 1) = root.value || '/')
1399
+ ))
1400
+ ORDER BY s.started_at DESC
1401
+ LIMIT $limit`;
1402
+ }
1403
+ });
1404
+
1377
1405
  // src/db/index.ts
1378
1406
  function getDb() {
1379
1407
  if (_db) return _db;
@@ -1404,9 +1432,8 @@ function insertEvent(params) {
1404
1432
  `);
1405
1433
  stmt.run(params);
1406
1434
  }
1407
- function getRecentSessionsByRepo(repoName, limit) {
1408
- const db = getDb();
1409
- return db.prepare("SELECT * FROM sessions WHERE repo_name = $repo_name ORDER BY started_at DESC LIMIT $limit").all({ repo_name: repoName, limit });
1435
+ function getRecentSessionsForRepo(lookup, limit) {
1436
+ return findRecentSessionsForRepo(getDb(), lookup, limit);
1410
1437
  }
1411
1438
  function getLastSessionTouchingFile(filePath) {
1412
1439
  const db = getDb();
@@ -1572,6 +1599,7 @@ var init_db = __esm({
1572
1599
  init_paths();
1573
1600
  init_migrations();
1574
1601
  init_migrate();
1602
+ init_session_lookup();
1575
1603
  _db = null;
1576
1604
  }
1577
1605
  });
@@ -1627,7 +1655,7 @@ var init_redact = __esm({
1627
1655
  }
1628
1656
  });
1629
1657
 
1630
- // src/capture/git.ts
1658
+ // src/capture/repo-name.ts
1631
1659
  function parseOwnerAndRepo(url) {
1632
1660
  const normalized = url.trim().replace(/^[^/]+:(?!\/\/)/, (match) => match.replace(":", "/"));
1633
1661
  const segments = normalized.split("/").filter(Boolean);
@@ -1636,16 +1664,24 @@ function parseOwnerAndRepo(url) {
1636
1664
  const owner = segments[segments.length - 2];
1637
1665
  return owner && repo ? { owner, repo } : null;
1638
1666
  }
1639
- function nameFromRemote(root, qualifyWithOwner) {
1667
+ function formatRepoName(remote, qualify) {
1668
+ return qualify ? `${remote.owner}/${remote.repo}` : remote.repo;
1669
+ }
1670
+ var init_repo_name = __esm({
1671
+ "src/capture/repo-name.ts"() {
1672
+ "use strict";
1673
+ }
1674
+ });
1675
+
1676
+ // src/capture/git.ts
1677
+ function remoteOf(root) {
1640
1678
  try {
1641
1679
  const url = (0, import_child_process.execSync)("git remote get-url origin", {
1642
1680
  cwd: root,
1643
1681
  timeout: 2e3,
1644
1682
  stdio: "pipe"
1645
1683
  }).toString().trim();
1646
- const parsed = parseOwnerAndRepo(url);
1647
- if (!parsed) return null;
1648
- return qualifyWithOwner ? `${parsed.owner}/${parsed.repo}` : parsed.repo;
1684
+ return parseOwnerAndRepo(url);
1649
1685
  } catch {
1650
1686
  return null;
1651
1687
  }
@@ -1660,35 +1696,46 @@ function nameFromPackageJson(root) {
1660
1696
  return null;
1661
1697
  }
1662
1698
  }
1663
- function getGitContext(cwd, nameOverride, qualifyRepoName = false) {
1699
+ function currentBranch(cwd) {
1700
+ try {
1701
+ return (0, import_child_process.execSync)("git branch --show-current", {
1702
+ cwd,
1703
+ timeout: 2e3,
1704
+ stdio: "pipe"
1705
+ }).toString().trim() || null;
1706
+ } catch {
1707
+ return null;
1708
+ }
1709
+ }
1710
+ function nameRepo(root, nameOverride, qualifyRepoName) {
1711
+ if (nameOverride !== null) return { repoName: nameOverride, remote: null };
1712
+ const remote = remoteOf(root);
1713
+ if (remote) return { repoName: formatRepoName(remote, qualifyRepoName), remote };
1714
+ return { repoName: nameFromPackageJson(root) ?? import_path6.default.basename(root), remote: null };
1715
+ }
1716
+ function getRepoContext(cwd, config) {
1717
+ let root;
1664
1718
  try {
1665
- const root = (0, import_child_process.execSync)("git rev-parse --show-toplevel", {
1719
+ root = (0, import_child_process.execSync)("git rev-parse --show-toplevel", {
1666
1720
  cwd,
1667
1721
  timeout: 2e3,
1668
1722
  stdio: "pipe"
1669
1723
  }).toString().trim();
1670
- const repoName = nameOverride ?? nameFromRemote(root, qualifyRepoName) ?? nameFromPackageJson(root) ?? import_path6.default.basename(root);
1671
- let branch = null;
1672
- try {
1673
- branch = (0, import_child_process.execSync)("git branch --show-current", {
1674
- cwd,
1675
- timeout: 2e3,
1676
- stdio: "pipe"
1677
- }).toString().trim() || null;
1678
- } catch {
1679
- }
1680
- return { repoName, branch };
1681
1724
  } catch {
1682
- return { repoName: null, branch: null };
1725
+ return NOT_A_REPO;
1683
1726
  }
1727
+ const naming = nameRepo(root, config.projects?.[cwd] ?? null, config.git.qualifyRepoName);
1728
+ return { ...naming, branch: currentBranch(cwd), root };
1684
1729
  }
1685
- var import_child_process, import_path6, import_fs8;
1730
+ var import_child_process, import_path6, import_fs8, NOT_A_REPO;
1686
1731
  var init_git = __esm({
1687
1732
  "src/capture/git.ts"() {
1688
1733
  "use strict";
1689
1734
  import_child_process = require("child_process");
1690
1735
  import_path6 = __toESM(require("path"));
1691
1736
  import_fs8 = __toESM(require("fs"));
1737
+ init_repo_name();
1738
+ NOT_A_REPO = { repoName: null, branch: null, root: null, remote: null };
1692
1739
  }
1693
1740
  });
1694
1741
 
@@ -1818,8 +1865,7 @@ function captureCommand(opts) {
1818
1865
  }
1819
1866
  }
1820
1867
  const command2 = opts.command ?? opts.file ?? opts.type;
1821
- const nameOverride2 = config.projects?.[cwd] ?? null;
1822
- const { repoName: repoName2, branch: branch2 } = getGitContext(cwd, nameOverride2, config.git.qualifyRepoName);
1868
+ const { repoName: repoName2, branch: branch2 } = getRepoContext(cwd, config);
1823
1869
  insertEvent({
1824
1870
  captured_at: (/* @__PURE__ */ new Date()).toISOString(),
1825
1871
  command: command2,
@@ -1840,8 +1886,7 @@ function captureCommand(opts) {
1840
1886
  const last = getLastEvent();
1841
1887
  if (isDuplicate(command, cwd, last ?? void 0)) return;
1842
1888
  const safeCommand = redact(command, config.redact.extra);
1843
- const nameOverride = config.projects?.[cwd] ?? null;
1844
- const { repoName, branch } = getGitContext(cwd, nameOverride);
1889
+ const { repoName, branch } = getRepoContext(cwd, config);
1845
1890
  const { category, metadata } = normalizeCommand(safeCommand);
1846
1891
  if (category === "ai_session" && config.capture.aiTools === false) return;
1847
1892
  if (category === "other") {
@@ -2105,10 +2150,112 @@ var init_completion_scripts = __esm({
2105
2150
  }
2106
2151
  });
2107
2152
 
2153
+ // src/utils/backup.ts
2154
+ function backUpFile(file) {
2155
+ const base = `${file}.workscribe-backup-${Date.now()}`;
2156
+ for (let attempt = 1; attempt <= MAX_BACKUP_ATTEMPTS; attempt++) {
2157
+ const backup = attempt === 1 ? base : `${base}-${attempt}`;
2158
+ try {
2159
+ import_fs12.default.copyFileSync(file, backup, import_fs12.default.constants.COPYFILE_EXCL);
2160
+ return backup;
2161
+ } catch (error) {
2162
+ if (error.code !== "EEXIST") throw error;
2163
+ }
2164
+ }
2165
+ throw new Error(`Couldn't back up ${file}: too many backups named ${base}`);
2166
+ }
2167
+ var import_fs12, MAX_BACKUP_ATTEMPTS;
2168
+ var init_backup = __esm({
2169
+ "src/utils/backup.ts"() {
2170
+ "use strict";
2171
+ import_fs12 = __toESM(require("fs"));
2172
+ MAX_BACKUP_ATTEMPTS = 100;
2173
+ }
2174
+ });
2175
+
2176
+ // src/capture/completions-block.ts
2177
+ function completionsBlock(shellName, file) {
2178
+ const guardedSource = shellName === "zsh" ? `if [ -f "${file}" ]; then ${ZSH_COMPINIT}; source "${file}"; fi` : `[ -f "${file}" ] && source "${file}"`;
2179
+ return `${COMPLETIONS_MARKER}
2180
+ ${guardedSource}`;
2181
+ }
2182
+ function splitLines(content) {
2183
+ return { lines: content.split("\n"), lineEnding: content.includes("\r\n") ? "\r" : "" };
2184
+ }
2185
+ function bare(line) {
2186
+ return (line ?? "").replace(/\r$/, "");
2187
+ }
2188
+ function blockLengthAfterMarker(lines, markerIndex) {
2189
+ const next = bare(lines[markerIndex + 1]);
2190
+ if (GUARDED_ZSH.test(next) || GUARDED_BASH.test(next) || UNGUARDED_SOURCE.test(next)) return 1;
2191
+ if (next === ZSH_COMPINIT && UNGUARDED_SOURCE.test(bare(lines[markerIndex + 2]))) return 2;
2192
+ return 0;
2193
+ }
2194
+ function findBlocks(lines) {
2195
+ const blocks = [];
2196
+ for (let index = 0; index < lines.length; index++) {
2197
+ if (bare(lines[index]) !== COMPLETIONS_MARKER) continue;
2198
+ const length = blockLengthAfterMarker(lines, index);
2199
+ if (length === 0) continue;
2200
+ blocks.push({ first: index, last: index + length });
2201
+ index += length;
2202
+ }
2203
+ return blocks;
2204
+ }
2205
+ function blockText(lines, block) {
2206
+ return lines.slice(block.first, block.last + 1).map(bare).join("\n");
2207
+ }
2208
+ function hasCompletionsBlock(content) {
2209
+ return findBlocks(splitLines(content).lines).length > 0;
2210
+ }
2211
+ function isCompletionsBlockOutdated(content, currentBlock) {
2212
+ const { lines } = splitLines(content);
2213
+ const blocks = findBlocks(lines);
2214
+ if (blocks.length === 0) return false;
2215
+ return blocks.length > 1 || blockText(lines, blocks[0]) !== currentBlock;
2216
+ }
2217
+ function rebuild(content, replacement) {
2218
+ const { lines, lineEnding } = splitLines(content);
2219
+ const blocks = findBlocks(lines);
2220
+ if (blocks.length === 0) return content;
2221
+ const dropped = /* @__PURE__ */ new Set();
2222
+ blocks.forEach((block, blockIndex) => {
2223
+ const isReplaced = blockIndex === 0 && replacement !== null;
2224
+ for (let index = block.first; index <= block.last; index++) dropped.add(index);
2225
+ if (!isReplaced && block.first > 0 && bare(lines[block.first - 1]) === "") dropped.add(block.first - 1);
2226
+ });
2227
+ const replacementLines = replacement === null ? [] : replacement.split("\n").map((line) => line + lineEnding);
2228
+ return lines.flatMap((line, index) => {
2229
+ if (replacement !== null && index === blocks[0].first) return replacementLines;
2230
+ return dropped.has(index) ? [] : [line];
2231
+ }).join("\n");
2232
+ }
2233
+ function replaceCompletionsBlock(content, newBlock) {
2234
+ return rebuild(content, newBlock);
2235
+ }
2236
+ function removeCompletionsBlock(content) {
2237
+ return rebuild(content, null);
2238
+ }
2239
+ var COMPLETIONS_MARKER, ZSH_COMPINIT, OWN_SCRIPT, GUARDED_ZSH, GUARDED_BASH, UNGUARDED_SOURCE;
2240
+ var init_completions_block = __esm({
2241
+ "src/capture/completions-block.ts"() {
2242
+ "use strict";
2243
+ COMPLETIONS_MARKER = "# Workscribe completions";
2244
+ ZSH_COMPINIT = "autoload -Uz compinit && compinit";
2245
+ OWN_SCRIPT = '"[^"]*completion\\.(?:zsh|bash)"';
2246
+ GUARDED_ZSH = new RegExp(`^if \\[ -f ${OWN_SCRIPT} \\]; then ${ZSH_COMPINIT}; source ${OWN_SCRIPT}; fi$`);
2247
+ GUARDED_BASH = new RegExp(`^\\[ -f ${OWN_SCRIPT} \\] && source ${OWN_SCRIPT}$`);
2248
+ UNGUARDED_SOURCE = new RegExp(`^source ${OWN_SCRIPT}$`);
2249
+ }
2250
+ });
2251
+
2108
2252
  // src/capture/shell-hook.ts
2253
+ function backUp(rcFile) {
2254
+ log.info(`Backed up ${rcFile} \u2192 ${import_path9.default.basename(backUpFile(rcFile))}`);
2255
+ }
2109
2256
  function getInstalledHookBlock(rcFile) {
2110
- if (!import_fs12.default.existsSync(rcFile)) return null;
2111
- const content = import_fs12.default.readFileSync(rcFile, "utf-8");
2257
+ if (!import_fs13.default.existsSync(rcFile)) return null;
2258
+ const content = import_fs13.default.readFileSync(rcFile, "utf-8");
2112
2259
  const startIdx = content.indexOf(HOOK_START);
2113
2260
  const endIdx = content.indexOf(HOOK_END);
2114
2261
  if (startIdx === -1 || endIdx === -1) return null;
@@ -2123,34 +2270,28 @@ function isHookOutdated(rcFile, hookContent) {
2123
2270
  return installed.trim() !== hookContent.trim();
2124
2271
  }
2125
2272
  function installHook(rcFile, hookContent) {
2126
- const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
2127
- import_fs12.default.copyFileSync(rcFile, backup);
2128
- log.info(`Backed up ${rcFile} \u2192 ${import_path9.default.basename(backup)}`);
2129
- import_fs12.default.appendFileSync(rcFile, `
2273
+ backUp(rcFile);
2274
+ import_fs13.default.appendFileSync(rcFile, `
2130
2275
  ${hookContent}
2131
2276
  `);
2132
2277
  }
2133
2278
  function updateHook(rcFile, hookContent) {
2134
- const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
2135
- import_fs12.default.copyFileSync(rcFile, backup);
2136
- log.info(`Backed up ${rcFile} \u2192 ${import_path9.default.basename(backup)}`);
2137
- const content = import_fs12.default.readFileSync(rcFile, "utf-8");
2279
+ backUp(rcFile);
2280
+ const content = import_fs13.default.readFileSync(rcFile, "utf-8");
2138
2281
  const startIdx = content.indexOf(HOOK_START);
2139
2282
  const endIdx = content.indexOf(HOOK_END) + HOOK_END.length;
2140
- import_fs12.default.writeFileSync(rcFile, content.slice(0, startIdx) + hookContent + content.slice(endIdx));
2283
+ import_fs13.default.writeFileSync(rcFile, content.slice(0, startIdx) + hookContent + content.slice(endIdx));
2141
2284
  }
2142
2285
  function removeHook(rcFile) {
2143
- const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
2144
- import_fs12.default.copyFileSync(rcFile, backup);
2145
- log.info(`Backed up ${rcFile} \u2192 ${import_path9.default.basename(backup)}`);
2146
- const content = import_fs12.default.readFileSync(rcFile, "utf-8");
2286
+ backUp(rcFile);
2287
+ const content = import_fs13.default.readFileSync(rcFile, "utf-8");
2147
2288
  const startIdx = content.indexOf(HOOK_START);
2148
2289
  const endIdx = content.indexOf(HOOK_END) + HOOK_END.length;
2149
2290
  const preambleIdx = content.lastIndexOf(HOOK_PREAMBLE, startIdx);
2150
2291
  const blockStart = preambleIdx !== -1 ? preambleIdx : startIdx;
2151
2292
  const before = content.slice(0, blockStart).replace(/\n+$/, "\n");
2152
2293
  const after = content.slice(endIdx).replace(/^\n/, "");
2153
- import_fs12.default.writeFileSync(rcFile, before + after);
2294
+ import_fs13.default.writeFileSync(rcFile, before + after);
2154
2295
  }
2155
2296
  function resolveWorkscribeBinary() {
2156
2297
  try {
@@ -2167,53 +2308,86 @@ function injectBinaryPath(hookContent, binaryPath, nodePath) {
2167
2308
  }
2168
2309
  function isCompletionsInstalled(shell) {
2169
2310
  if (shell.name === "fish") {
2170
- return import_fs12.default.existsSync(import_path9.default.join(FISH_COMPLETIONS_DIR, "workscribe.fish"));
2311
+ return import_fs13.default.existsSync(import_path9.default.join(FISH_COMPLETIONS_DIR, "workscribe.fish"));
2171
2312
  }
2172
2313
  try {
2173
- return import_fs12.default.readFileSync(shell.rcFile, "utf-8").includes(COMPLETIONS_MARKER);
2314
+ return import_fs13.default.readFileSync(shell.rcFile, "utf-8").includes(COMPLETIONS_MARKER);
2174
2315
  } catch {
2175
2316
  return false;
2176
2317
  }
2177
2318
  }
2178
- function installCompletions(shell, commands) {
2319
+ function completionScriptFile(shellName) {
2320
+ return import_path9.default.join(WORKSCRIBE_DIR, `completion.${shellName}`);
2321
+ }
2322
+ function readRcFile(rcFile) {
2323
+ try {
2324
+ return import_fs13.default.readFileSync(rcFile, "utf-8");
2325
+ } catch {
2326
+ return null;
2327
+ }
2328
+ }
2329
+ function writeCompletionScript(shell, commands) {
2179
2330
  if (shell.name === "fish") {
2180
- if (!import_fs12.default.existsSync(FISH_COMPLETIONS_DIR)) {
2181
- import_fs12.default.mkdirSync(FISH_COMPLETIONS_DIR, { recursive: true });
2182
- }
2183
- import_fs12.default.writeFileSync(import_path9.default.join(FISH_COMPLETIONS_DIR, "workscribe.fish"), fishCompletion(commands), { mode: 420 });
2184
- } else {
2185
- const script = shell.name === "zsh" ? zshCompletion(commands) : bashCompletion(commands);
2186
- const cacheFile = import_path9.default.join(WORKSCRIBE_DIR, `completion.${shell.name}`);
2187
- import_fs12.default.writeFileSync(cacheFile, script, { mode: 420 });
2188
- if (!isCompletionsInstalled(shell)) {
2189
- const sourceLine = shell.name === "zsh" ? `
2190
- ${COMPLETIONS_MARKER}
2191
- autoload -Uz compinit && compinit
2192
- source "${cacheFile}"
2193
- ` : `
2194
- ${COMPLETIONS_MARKER}
2195
- source "${cacheFile}"
2196
- `;
2197
- import_fs12.default.appendFileSync(shell.rcFile, sourceLine);
2198
- }
2331
+ import_fs13.default.mkdirSync(FISH_COMPLETIONS_DIR, { recursive: true });
2332
+ import_fs13.default.writeFileSync(FISH_COMPLETIONS_FILE, fishCompletion(commands), { mode: 420 });
2333
+ return;
2199
2334
  }
2335
+ const script = shell.name === "zsh" ? zshCompletion(commands) : bashCompletion(commands);
2336
+ import_fs13.default.mkdirSync(WORKSCRIBE_DIR, { recursive: true });
2337
+ import_fs13.default.writeFileSync(completionScriptFile(shell.name), script, { mode: 420 });
2338
+ }
2339
+ function installCompletions(shell, commands) {
2340
+ writeCompletionScript(shell, commands);
2341
+ if (shell.name !== "fish" && !isCompletionsInstalled(shell)) {
2342
+ import_fs13.default.appendFileSync(shell.rcFile, `
2343
+ ${completionsBlock(shell.name, completionScriptFile(shell.name))}
2344
+ `);
2345
+ }
2346
+ }
2347
+ function isCompletionsOutdated(shell) {
2348
+ if (shell.name === "fish") return false;
2349
+ const content = readRcFile(shell.rcFile);
2350
+ return content !== null && isCompletionsBlockOutdated(content, completionsBlock(shell.name, completionScriptFile(shell.name)));
2351
+ }
2352
+ function updateCompletions(shell) {
2353
+ const content = readRcFile(shell.rcFile);
2354
+ if (content === null || !hasCompletionsBlock(content)) return;
2355
+ backUp(shell.rcFile);
2356
+ import_fs13.default.writeFileSync(
2357
+ shell.rcFile,
2358
+ replaceCompletionsBlock(content, completionsBlock(shell.name, completionScriptFile(shell.name)))
2359
+ );
2360
+ }
2361
+ function removeCompletions(rcFile) {
2362
+ const content = readRcFile(rcFile);
2363
+ if (content === null || !hasCompletionsBlock(content)) return false;
2364
+ backUp(rcFile);
2365
+ import_fs13.default.writeFileSync(rcFile, removeCompletionsBlock(content));
2366
+ return true;
2367
+ }
2368
+ function removeFishCompletions() {
2369
+ if (!import_fs13.default.existsSync(FISH_COMPLETIONS_FILE)) return false;
2370
+ import_fs13.default.rmSync(FISH_COMPLETIONS_FILE, { force: true });
2371
+ return true;
2200
2372
  }
2201
- var import_fs12, import_path9, import_os3, import_child_process3, FISH_COMPLETIONS_DIR, HOOK_START, HOOK_END, HOOK_PREAMBLE, COMPLETIONS_MARKER;
2373
+ var import_fs13, import_path9, import_os3, import_child_process3, FISH_COMPLETIONS_DIR, HOOK_START, HOOK_END, HOOK_PREAMBLE, FISH_COMPLETIONS_FILE;
2202
2374
  var init_shell_hook = __esm({
2203
2375
  "src/capture/shell-hook.ts"() {
2204
2376
  "use strict";
2205
- import_fs12 = __toESM(require("fs"));
2377
+ import_fs13 = __toESM(require("fs"));
2206
2378
  import_path9 = __toESM(require("path"));
2207
2379
  import_os3 = __toESM(require("os"));
2208
2380
  import_child_process3 = require("child_process");
2209
2381
  init_logger();
2210
2382
  init_completion_scripts();
2211
2383
  init_paths();
2384
+ init_backup();
2385
+ init_completions_block();
2212
2386
  FISH_COMPLETIONS_DIR = import_path9.default.join(import_os3.default.homedir(), ".config", "fish", "completions");
2213
2387
  HOOK_START = "# workscribe:hook:start";
2214
2388
  HOOK_END = "# workscribe:hook:end";
2215
2389
  HOOK_PREAMBLE = "# Workscribe \u2014 developer work journal";
2216
- COMPLETIONS_MARKER = "# Workscribe completions";
2390
+ FISH_COMPLETIONS_FILE = import_path9.default.join(FISH_COMPLETIONS_DIR, "workscribe.fish");
2217
2391
  }
2218
2392
  });
2219
2393
 
@@ -2234,11 +2408,43 @@ var init_utils = __esm({
2234
2408
  }
2235
2409
  });
2236
2410
 
2411
+ // src/capture/hooks/claude-code-command.ts
2412
+ function buildHookCommand(options) {
2413
+ if (!options.binaryPath) return `${BARE_GUARD}workscribe ${options.args}${GUARD_SUFFIX}`;
2414
+ const { nodePath, binaryPath, args } = options;
2415
+ return `test -x "${nodePath}" && test -x "${binaryPath}" && "${nodePath}" "${binaryPath}" ${args}${GUARD_SUFFIX}`;
2416
+ }
2417
+ function withoutLeadingGuards(command) {
2418
+ return command.trimStart().replace(LEADING_PATH_GUARDS, "");
2419
+ }
2420
+ function isPinnedCommand(command) {
2421
+ const invocation = withoutLeadingGuards(command);
2422
+ return invocation.startsWith('"') || invocation.startsWith("/");
2423
+ }
2424
+ function isGuardedCommand(command) {
2425
+ const trimmed = command.trim();
2426
+ const hasLeadingGuard = LEADING_PATH_GUARDS.test(trimmed) || trimmed.startsWith(BARE_GUARD);
2427
+ return hasLeadingGuard && trimmed.endsWith(GUARD_SUFFIX);
2428
+ }
2429
+ function pinnedNodePath(command) {
2430
+ return withoutLeadingGuards(command).match(/^"([^"]+)"/)?.[1] ?? null;
2431
+ }
2432
+ var HOOK_MARKER, GUARD_SUFFIX, LEADING_PATH_GUARDS, BARE_GUARD;
2433
+ var init_claude_code_command = __esm({
2434
+ "src/capture/hooks/claude-code-command.ts"() {
2435
+ "use strict";
2436
+ HOOK_MARKER = "_hook --source claude-code";
2437
+ GUARD_SUFFIX = " || true";
2438
+ LEADING_PATH_GUARDS = /^(?:test -x "[^"]*" && )+/;
2439
+ BARE_GUARD = "command -v workscribe >/dev/null 2>&1 && ";
2440
+ }
2441
+ });
2442
+
2237
2443
  // src/capture/hooks/claude-code.ts
2238
2444
  function readSettings() {
2239
- if (!import_fs13.default.existsSync(CLAUDE_SETTINGS_PATH)) return {};
2445
+ if (!import_fs14.default.existsSync(CLAUDE_SETTINGS_PATH)) return {};
2240
2446
  try {
2241
- return JSON.parse(import_fs13.default.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8"));
2447
+ return JSON.parse(import_fs14.default.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8"));
2242
2448
  } catch {
2243
2449
  return {};
2244
2450
  }
@@ -2264,10 +2470,6 @@ function findWorkscribeHookCommands(settings) {
2264
2470
  userPromptSubmit: commandsForEvent(hooks, "UserPromptSubmit")
2265
2471
  };
2266
2472
  }
2267
- function isPinnedCommand(command) {
2268
- const trimmed = command.trimStart();
2269
- return trimmed.startsWith('"') || trimmed.startsWith("/");
2270
- }
2271
2473
  function withoutWorkscribeEntries(entries) {
2272
2474
  return entries.map((entry) => {
2273
2475
  const record = entry;
@@ -2279,11 +2481,29 @@ function withoutWorkscribeEntries(entries) {
2279
2481
  return { ...record, hooks: filteredHooks };
2280
2482
  }).filter((entry) => entry.hooks.length > 0);
2281
2483
  }
2282
- function checkHookHealth() {
2484
+ function isExecutable(filePath) {
2485
+ try {
2486
+ import_fs14.default.accessSync(filePath, import_fs14.default.constants.X_OK);
2487
+ return true;
2488
+ } catch {
2489
+ return false;
2490
+ }
2491
+ }
2492
+ function allWorkscribeCommands() {
2283
2493
  const { postToolUse, userPromptSubmit } = findWorkscribeHookCommands(readSettings());
2284
- const allCommands = [...postToolUse, ...userPromptSubmit];
2285
- if (allCommands.length === 0) return { installed: false, pinned: true, confirmedBroken: null };
2286
- if (allCommands.every(isPinnedCommand)) return { installed: true, pinned: true, confirmedBroken: null };
2494
+ return [...postToolUse, ...userPromptSubmit];
2495
+ }
2496
+ function checkHookHealth() {
2497
+ const allCommands = allWorkscribeCommands();
2498
+ if (allCommands.length === 0) {
2499
+ return { installed: false, pinned: true, guarded: true, nodeMissing: false, confirmedBroken: null };
2500
+ }
2501
+ const guarded = allCommands.every(isGuardedCommand);
2502
+ const nodePaths = allCommands.map(pinnedNodePath).filter((nodePath) => nodePath !== null);
2503
+ const nodeMissing = nodePaths.some((nodePath) => !isExecutable(nodePath));
2504
+ if (allCommands.every(isPinnedCommand)) {
2505
+ return { installed: true, pinned: true, guarded, nodeMissing, confirmedBroken: null };
2506
+ }
2287
2507
  let confirmedBroken;
2288
2508
  try {
2289
2509
  (0, import_child_process5.execFileSync)("workscribe", ["--version"], { stdio: "ignore", timeout: 2e3 });
@@ -2291,48 +2511,76 @@ function checkHookHealth() {
2291
2511
  } catch {
2292
2512
  confirmedBroken = true;
2293
2513
  }
2294
- return { installed: true, pinned: false, confirmedBroken };
2514
+ return { installed: true, pinned: false, guarded, nodeMissing, confirmedBroken };
2515
+ }
2516
+ function readSettingsStrictly() {
2517
+ if (!import_fs14.default.existsSync(CLAUDE_SETTINGS_PATH)) return "missing";
2518
+ try {
2519
+ const parsed = JSON.parse(import_fs14.default.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8"));
2520
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : "unreadable";
2521
+ } catch {
2522
+ return "unreadable";
2523
+ }
2295
2524
  }
2296
- var import_fs13, import_path10, import_os4, import_child_process5, CLAUDE_SETTINGS_PATH, HOOK_MARKER, claudeCodeIntegration;
2525
+ function hooksWithoutWorkscribe(hooks) {
2526
+ let removedAny = false;
2527
+ const remaining = {};
2528
+ for (const [eventName, entries] of Object.entries(hooks)) {
2529
+ if (!Array.isArray(entries)) {
2530
+ remaining[eventName] = entries;
2531
+ continue;
2532
+ }
2533
+ const kept = withoutWorkscribeEntries(entries);
2534
+ if (commandsForEvent(hooks, eventName).length > 0) removedAny = true;
2535
+ if (kept.length > 0) remaining[eventName] = kept;
2536
+ }
2537
+ return removedAny ? remaining : null;
2538
+ }
2539
+ function isHookCurrent() {
2540
+ const { postToolUse, userPromptSubmit } = findWorkscribeHookCommands(readSettings());
2541
+ if (postToolUse.length === 0 || userPromptSubmit.length === 0) return false;
2542
+ return [...postToolUse, ...userPromptSubmit].every((command) => {
2543
+ const nodePath = pinnedNodePath(command);
2544
+ return isPinnedCommand(command) && isGuardedCommand(command) && nodePath !== null && isExecutable(nodePath);
2545
+ });
2546
+ }
2547
+ var import_fs14, import_path10, import_os4, import_child_process5, CLAUDE_SETTINGS_PATH, claudeCodeIntegration;
2297
2548
  var init_claude_code = __esm({
2298
2549
  "src/capture/hooks/claude-code.ts"() {
2299
2550
  "use strict";
2300
- import_fs13 = __toESM(require("fs"));
2551
+ import_fs14 = __toESM(require("fs"));
2301
2552
  import_path10 = __toESM(require("path"));
2302
2553
  import_os4 = __toESM(require("os"));
2303
2554
  import_child_process5 = require("child_process");
2304
2555
  init_utils();
2305
2556
  init_shell_hook();
2557
+ init_backup();
2558
+ init_claude_code_command();
2306
2559
  CLAUDE_SETTINGS_PATH = import_path10.default.join(import_os4.default.homedir(), ".claude", "settings.json");
2307
- HOOK_MARKER = "_hook --source claude-code";
2308
2560
  claudeCodeIntegration = {
2309
2561
  displayName: "Claude Code",
2310
2562
  isInstalled() {
2311
2563
  return isCommandInstalled("claude");
2312
2564
  },
2313
- // "Installed" means a *correctly pinned* hook is present, not just any
2314
- // workscribe hook — this is deliberate: it makes `workscribe init`'s
2315
- // existing prompt-and-install flow naturally offer to repair a stale,
2316
- // unpinned hook from before this fix, with no new UI code needed there.
2565
+ // "Installed" means a *current* hook is present, not just any workscribe hook — so `workscribe init` offers to
2566
+ // update a stale one (unpinned, unguarded, or pinned to a Node that's gone) through its outdated-hook prompt
2317
2567
  isHookInstalled() {
2318
- const { postToolUse, userPromptSubmit } = findWorkscribeHookCommands(readSettings());
2319
- if (postToolUse.length === 0 || userPromptSubmit.length === 0) return false;
2320
- return [...postToolUse, ...userPromptSubmit].every(isPinnedCommand);
2568
+ return isHookCurrent();
2321
2569
  },
2322
2570
  install() {
2323
2571
  const settings = readSettings();
2324
2572
  const existingHooks = settings.hooks ?? {};
2325
2573
  const existingPostToolUse = withoutWorkscribeEntries(existingHooks.PostToolUse ?? []);
2326
2574
  const existingUserPromptSubmit = withoutWorkscribeEntries(existingHooks.UserPromptSubmit ?? []);
2327
- const workscribeBinary = resolveWorkscribeBinary();
2328
- const baseCommand = workscribeBinary ? `"${process.execPath}" "${workscribeBinary}"` : "workscribe";
2575
+ const binaryPath = resolveWorkscribeBinary();
2576
+ const command = (args) => buildHookCommand({ nodePath: process.execPath, binaryPath, args });
2329
2577
  settings.hooks = {
2330
2578
  ...existingHooks,
2331
2579
  PostToolUse: [
2332
2580
  ...existingPostToolUse,
2333
2581
  {
2334
2582
  matcher: "Bash",
2335
- hooks: [{ type: "command", command: `${baseCommand} ${HOOK_MARKER}`, async: true }]
2583
+ hooks: [{ type: "command", command: command(HOOK_MARKER), async: true }]
2336
2584
  }
2337
2585
  ],
2338
2586
  UserPromptSubmit: [
@@ -2341,14 +2589,33 @@ var init_claude_code = __esm({
2341
2589
  hooks: [
2342
2590
  {
2343
2591
  type: "command",
2344
- command: `${baseCommand} ${HOOK_MARKER} --event UserPromptSubmit`
2592
+ command: command(`${HOOK_MARKER} --event UserPromptSubmit`)
2345
2593
  }
2346
2594
  ]
2347
2595
  }
2348
2596
  ]
2349
2597
  };
2350
- import_fs13.default.mkdirSync(import_path10.default.dirname(CLAUDE_SETTINGS_PATH), { recursive: true });
2351
- import_fs13.default.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
2598
+ import_fs14.default.mkdirSync(import_path10.default.dirname(CLAUDE_SETTINGS_PATH), { recursive: true });
2599
+ import_fs14.default.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
2600
+ },
2601
+ // Some workscribe hook is present, but not all of them are current (pinned and guarded) — including a
2602
+ // half-installed one where only PostToolUse or only UserPromptSubmit exists
2603
+ isHookOutdated() {
2604
+ return allWorkscribeCommands().length > 0 && !isHookCurrent();
2605
+ },
2606
+ uninstall() {
2607
+ const settings = readSettingsStrictly();
2608
+ if (settings === "missing") return "not-installed";
2609
+ if (settings === "unreadable") return "unreadable";
2610
+ const hooks = settings.hooks;
2611
+ if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks)) return "not-installed";
2612
+ const remainingHooks = hooksWithoutWorkscribe(hooks);
2613
+ if (!remainingHooks) return "not-installed";
2614
+ const updated = { ...settings, hooks: remainingHooks };
2615
+ if (Object.keys(remainingHooks).length === 0) delete updated.hooks;
2616
+ backUpFile(CLAUDE_SETTINGS_PATH);
2617
+ import_fs14.default.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(updated, null, 2));
2618
+ return "removed";
2352
2619
  }
2353
2620
  };
2354
2621
  }
@@ -2469,10 +2736,10 @@ function defaultDetectionEnvironment() {
2469
2736
  homeDir: import_os5.default.homedir(),
2470
2737
  // `which` spawns a process, so each command is looked up at most once per run
2471
2738
  isCommandInstalled: memoize(isCommandInstalled),
2472
- pathExists: (absolutePath) => import_fs14.default.existsSync(absolutePath),
2739
+ pathExists: (absolutePath) => import_fs15.default.existsSync(absolutePath),
2473
2740
  listDirectory: (absolutePath) => {
2474
2741
  try {
2475
- return import_fs14.default.readdirSync(absolutePath);
2742
+ return import_fs15.default.readdirSync(absolutePath);
2476
2743
  } catch {
2477
2744
  return [];
2478
2745
  }
@@ -2493,11 +2760,11 @@ function isAgentPresent(agent, environment) {
2493
2760
  function detectAgents(environment = defaultDetectionEnvironment()) {
2494
2761
  return SKILL_AGENTS.filter((agent) => isAgentPresent(agent, environment));
2495
2762
  }
2496
- var import_fs14, import_os5, import_path11;
2763
+ var import_fs15, import_os5, import_path11;
2497
2764
  var init_detect = __esm({
2498
2765
  "src/skills/detect.ts"() {
2499
2766
  "use strict";
2500
- import_fs14 = __toESM(require("fs"));
2767
+ import_fs15 = __toESM(require("fs"));
2501
2768
  import_os5 = __toESM(require("os"));
2502
2769
  import_path11 = __toESM(require("path"));
2503
2770
  init_utils();
@@ -2548,7 +2815,7 @@ function isSkillMarker(value) {
2548
2815
  }
2549
2816
  function readMarker(skillDir) {
2550
2817
  try {
2551
- const parsed = JSON.parse(import_fs15.default.readFileSync(import_path12.default.join(skillDir, MARKER_FILE), "utf-8"));
2818
+ const parsed = JSON.parse(import_fs16.default.readFileSync(import_path12.default.join(skillDir, MARKER_FILE), "utf-8"));
2552
2819
  return isSkillMarker(parsed) ? parsed : null;
2553
2820
  } catch {
2554
2821
  return null;
@@ -2557,62 +2824,62 @@ function readMarker(skillDir) {
2557
2824
  function readInstallState(skillsFolder, currentVersion) {
2558
2825
  const skillDir = import_path12.default.join(skillsFolder, SKILL_NAME);
2559
2826
  return describeInstallState({
2560
- folderExists: import_fs15.default.existsSync(skillDir),
2827
+ folderExists: import_fs16.default.existsSync(skillDir),
2561
2828
  marker: readMarker(skillDir),
2562
2829
  currentVersion
2563
2830
  });
2564
2831
  }
2565
2832
  function copyDirectory(from, to) {
2566
- import_fs15.default.mkdirSync(to, { recursive: true });
2567
- for (const entry of import_fs15.default.readdirSync(from, { withFileTypes: true })) {
2833
+ import_fs16.default.mkdirSync(to, { recursive: true });
2834
+ for (const entry of import_fs16.default.readdirSync(from, { withFileTypes: true })) {
2568
2835
  const source = import_path12.default.join(from, entry.name);
2569
2836
  const destination = import_path12.default.join(to, entry.name);
2570
2837
  if (entry.isDirectory()) copyDirectory(source, destination);
2571
- else import_fs15.default.copyFileSync(source, destination);
2838
+ else import_fs16.default.copyFileSync(source, destination);
2572
2839
  }
2573
2840
  }
2574
2841
  function swapInto(skillDir, preparedDir) {
2575
2842
  const previousDir = import_path12.default.join(import_path12.default.dirname(skillDir), `.${SKILL_NAME}.old-${process.pid}`);
2576
- import_fs15.default.rmSync(previousDir, { recursive: true, force: true });
2577
- const hadPrevious = import_fs15.default.existsSync(skillDir);
2578
- if (hadPrevious) import_fs15.default.renameSync(skillDir, previousDir);
2843
+ import_fs16.default.rmSync(previousDir, { recursive: true, force: true });
2844
+ const hadPrevious = import_fs16.default.existsSync(skillDir);
2845
+ if (hadPrevious) import_fs16.default.renameSync(skillDir, previousDir);
2579
2846
  try {
2580
- import_fs15.default.renameSync(preparedDir, skillDir);
2847
+ import_fs16.default.renameSync(preparedDir, skillDir);
2581
2848
  } catch (error) {
2582
- if (hadPrevious) import_fs15.default.renameSync(previousDir, skillDir);
2849
+ if (hadPrevious) import_fs16.default.renameSync(previousDir, skillDir);
2583
2850
  throw error;
2584
2851
  }
2585
- if (hadPrevious) import_fs15.default.rmSync(previousDir, { recursive: true, force: true });
2852
+ if (hadPrevious) import_fs16.default.rmSync(previousDir, { recursive: true, force: true });
2586
2853
  }
2587
2854
  function installSkill(options) {
2588
2855
  const state = readInstallState(options.skillsFolder, options.version);
2589
2856
  if (state.kind === "installed") return "up-to-date";
2590
2857
  if (state.kind === "not-ours" && !options.force) return "skipped-not-ours";
2591
- import_fs15.default.mkdirSync(options.skillsFolder, { recursive: true });
2858
+ import_fs16.default.mkdirSync(options.skillsFolder, { recursive: true });
2592
2859
  const preparedDir = import_path12.default.join(options.skillsFolder, `.${SKILL_NAME}.tmp-${process.pid}`);
2593
- import_fs15.default.rmSync(preparedDir, { recursive: true, force: true });
2860
+ import_fs16.default.rmSync(preparedDir, { recursive: true, force: true });
2594
2861
  try {
2595
2862
  copyDirectory(options.sourceDir, preparedDir);
2596
2863
  const marker = { version: options.version, installedAt: (/* @__PURE__ */ new Date()).toISOString() };
2597
- import_fs15.default.writeFileSync(import_path12.default.join(preparedDir, MARKER_FILE), JSON.stringify(marker, null, 2));
2864
+ import_fs16.default.writeFileSync(import_path12.default.join(preparedDir, MARKER_FILE), JSON.stringify(marker, null, 2));
2598
2865
  swapInto(import_path12.default.join(options.skillsFolder, SKILL_NAME), preparedDir);
2599
2866
  } finally {
2600
- import_fs15.default.rmSync(preparedDir, { recursive: true, force: true });
2867
+ import_fs16.default.rmSync(preparedDir, { recursive: true, force: true });
2601
2868
  }
2602
2869
  return state.kind === "not-installed" ? "installed" : "updated";
2603
2870
  }
2604
2871
  function uninstallSkill(skillsFolder) {
2605
2872
  const skillDir = import_path12.default.join(skillsFolder, SKILL_NAME);
2606
- if (!import_fs15.default.existsSync(skillDir)) return "not-installed";
2873
+ if (!import_fs16.default.existsSync(skillDir)) return "not-installed";
2607
2874
  if (!readMarker(skillDir)) return "skipped-not-ours";
2608
- import_fs15.default.rmSync(skillDir, { recursive: true, force: true });
2875
+ import_fs16.default.rmSync(skillDir, { recursive: true, force: true });
2609
2876
  return "removed";
2610
2877
  }
2611
- var import_fs15, import_path12;
2878
+ var import_fs16, import_path12;
2612
2879
  var init_installer = __esm({
2613
2880
  "src/skills/installer.ts"() {
2614
2881
  "use strict";
2615
- import_fs15 = __toESM(require("fs"));
2882
+ import_fs16 = __toESM(require("fs"));
2616
2883
  import_path12 = __toESM(require("path"));
2617
2884
  init_agents();
2618
2885
  init_state();
@@ -2628,7 +2895,7 @@ function packagedSkillCandidates(fromDirectory) {
2628
2895
  }
2629
2896
  function resolvePackagedSkillDir(fromDirectory = __dirname) {
2630
2897
  const found = packagedSkillCandidates(fromDirectory).find(
2631
- (candidate) => import_fs16.default.existsSync(import_path13.default.join(candidate, SKILL_ENTRY_FILE))
2898
+ (candidate) => import_fs17.default.existsSync(import_path13.default.join(candidate, SKILL_ENTRY_FILE))
2632
2899
  );
2633
2900
  if (!found) {
2634
2901
  throw new Error(
@@ -2637,11 +2904,11 @@ function resolvePackagedSkillDir(fromDirectory = __dirname) {
2637
2904
  }
2638
2905
  return found;
2639
2906
  }
2640
- var import_fs16, import_path13, SKILL_ENTRY_FILE;
2907
+ var import_fs17, import_path13, SKILL_ENTRY_FILE;
2641
2908
  var init_package_path = __esm({
2642
2909
  "src/skills/package-path.ts"() {
2643
2910
  "use strict";
2644
- import_fs16 = __toESM(require("fs"));
2911
+ import_fs17 = __toESM(require("fs"));
2645
2912
  import_path13 = __toESM(require("path"));
2646
2913
  init_agents();
2647
2914
  SKILL_ENTRY_FILE = "SKILL.md";
@@ -2739,10 +3006,10 @@ function installIntoFolder(options) {
2739
3006
  const outcome = installSkill({
2740
3007
  sourceDir: options.sourceDir,
2741
3008
  skillsFolder: target.skillsFolder,
2742
- version: "2.0.0",
3009
+ version: "2.0.1",
2743
3010
  force: options.force
2744
3011
  });
2745
- return describeInstallOutcome({ ...target, outcome, version: "2.0.0" });
3012
+ return describeInstallOutcome({ ...target, outcome, version: "2.0.1" });
2746
3013
  } catch (error) {
2747
3014
  return describeInstallFailure({ ...target, error });
2748
3015
  }
@@ -2761,8 +3028,8 @@ function removeSkills(agents, options) {
2761
3028
  }
2762
3029
  function formatAgentSkillLine(agent, homeDir = import_os6.default.homedir()) {
2763
3030
  const location = displayPath(import_path14.default.join(homeDir, agent.skillsFolder, SKILL_NAME), homeDir);
2764
- const installState = readInstallState(import_path14.default.join(homeDir, agent.skillsFolder), "2.0.0");
2765
- const status = describeSkillStatus(installState, "2.0.0");
3031
+ const installState = readInstallState(import_path14.default.join(homeDir, agent.skillsFolder), "2.0.1");
3032
+ const status = describeSkillStatus(installState, "2.0.1");
2766
3033
  return `${agent.name.padEnd(AGENT_NAME_COLUMN_WIDTH)} ${STATUS_COLOURS[status.level](status.text)} ${import_chalk2.default.dim(location)}`;
2767
3034
  }
2768
3035
  function formatUndetectedAgentLine(agent) {
@@ -2875,19 +3142,19 @@ function step(current, total, label) {
2875
3142
  function runDataDirectoriesStep() {
2876
3143
  step(1, TOTAL_STEPS, "Creating data directories");
2877
3144
  for (const dir of REQUIRED_DIRS) {
2878
- if (!import_fs17.default.existsSync(dir)) {
2879
- import_fs17.default.mkdirSync(dir, { recursive: true, mode: 448 });
3145
+ if (!import_fs18.default.existsSync(dir)) {
3146
+ import_fs18.default.mkdirSync(dir, { recursive: true, mode: 448 });
2880
3147
  log.success(`Created ${dir}`);
2881
3148
  } else {
2882
3149
  try {
2883
- import_fs17.default.chmodSync(dir, 448);
3150
+ import_fs18.default.chmodSync(dir, 448);
2884
3151
  } catch {
2885
3152
  }
2886
3153
  log.info(`${dir} already exists`);
2887
3154
  }
2888
3155
  }
2889
- if (!import_fs17.default.existsSync(PROFILES_DIR)) {
2890
- import_fs17.default.mkdirSync(PROFILES_DIR, { recursive: true, mode: 448 });
3156
+ if (!import_fs18.default.existsSync(PROFILES_DIR)) {
3157
+ import_fs18.default.mkdirSync(PROFILES_DIR, { recursive: true, mode: 448 });
2891
3158
  }
2892
3159
  }
2893
3160
  async function initDatabaseAndConfig() {
@@ -2924,18 +3191,18 @@ async function runShellHookStep(program) {
2924
3191
  return null;
2925
3192
  }
2926
3193
  const hookFile = import_path15.default.join(__dirname, "..", "shell", `hook.${shell.name}`);
2927
- if (!import_fs17.default.existsSync(hookFile)) {
3194
+ if (!import_fs18.default.existsSync(hookFile)) {
2928
3195
  log.warn(`Hook template not found at ${hookFile}`);
2929
3196
  return shell;
2930
3197
  }
2931
- const hookContent = import_fs17.default.readFileSync(hookFile, "utf-8");
3198
+ const hookContent = import_fs18.default.readFileSync(hookFile, "utf-8");
2932
3199
  const binaryPath = resolveWorkscribeBinary();
2933
3200
  const hookWithPath = binaryPath ? injectBinaryPath(hookContent, binaryPath, process.execPath) : hookContent;
2934
3201
  if (shell.name === "fish") {
2935
3202
  const confDir = import_path15.default.dirname(shell.rcFile);
2936
- if (!import_fs17.default.existsSync(confDir)) import_fs17.default.mkdirSync(confDir, { recursive: true });
3203
+ if (!import_fs18.default.existsSync(confDir)) import_fs18.default.mkdirSync(confDir, { recursive: true });
2937
3204
  }
2938
- if (!import_fs17.default.existsSync(shell.rcFile)) import_fs17.default.writeFileSync(shell.rcFile, "");
3205
+ if (!import_fs18.default.existsSync(shell.rcFile)) import_fs18.default.writeFileSync(shell.rcFile, "");
2939
3206
  if (!isHookInstalled(shell.rcFile)) {
2940
3207
  log.plain("");
2941
3208
  log.plain(import_chalk3.default.dim(" Workscribe adds a lightweight hook to your shell that captures each"));
@@ -2962,7 +3229,7 @@ async function runShellHookStep(program) {
2962
3229
  log.info(`Shell hook up to date in ${shell.rcFile}`);
2963
3230
  }
2964
3231
  if (isCompletionsInstalled(shell)) {
2965
- log.info("Shell completions already installed");
3232
+ await refreshCompletions(shell, program);
2966
3233
  } else {
2967
3234
  const { installComps } = await (0, import_prompts2.default)({
2968
3235
  type: "confirm",
@@ -2981,6 +3248,29 @@ async function runShellHookStep(program) {
2981
3248
  }
2982
3249
  return shell;
2983
3250
  }
3251
+ async function refreshCompletions(shell, program) {
3252
+ try {
3253
+ writeCompletionScript(shell, getPublicCommands(program));
3254
+ } catch (error) {
3255
+ log.warn(`Could not refresh shell completions: ${error.message}`);
3256
+ }
3257
+ if (!isCompletionsOutdated(shell)) {
3258
+ log.info("Shell completions already installed");
3259
+ return;
3260
+ }
3261
+ const { updateShellCompletions } = await (0, import_prompts2.default)({
3262
+ type: "confirm",
3263
+ name: "updateShellCompletions",
3264
+ message: `Shell completions in ${shell.rcFile} are outdated (they error in new terminals if Workscribe is removed). Update?`,
3265
+ initial: true
3266
+ });
3267
+ if (!updateShellCompletions) {
3268
+ log.info("Shell completions update skipped");
3269
+ return;
3270
+ }
3271
+ updateCompletions(shell);
3272
+ log.success(`Shell completions updated in ${shell.rcFile}`);
3273
+ }
2984
3274
  async function runAIToolsStep() {
2985
3275
  step(4, TOTAL_STEPS, "Detecting AI coding tools");
2986
3276
  const installedTools = loadAIToolRegistry().filter(
@@ -2994,6 +3284,19 @@ async function runAIToolsStep() {
2994
3284
  const integration = AI_HOOK_INTEGRATIONS[tool.hook];
2995
3285
  if (integration.isHookInstalled()) {
2996
3286
  log.info(`${integration.displayName} session hook already installed`);
3287
+ } else if (integration.isHookOutdated?.()) {
3288
+ const { updateAiHook } = await (0, import_prompts2.default)({
3289
+ type: "confirm",
3290
+ name: "updateAiHook",
3291
+ message: `${integration.displayName} session hook is outdated. Update?`,
3292
+ initial: true
3293
+ });
3294
+ if (updateAiHook) {
3295
+ integration.install();
3296
+ log.success(`${integration.displayName} session hook updated`);
3297
+ } else {
3298
+ log.info(`${integration.displayName} hook update skipped \u2014 re-run workscribe init to update later`);
3299
+ }
2997
3300
  } else {
2998
3301
  const { install } = await (0, import_prompts2.default)({
2999
3302
  type: "confirm",
@@ -3012,10 +3315,10 @@ async function runAIToolsStep() {
3012
3315
  }
3013
3316
  async function offerSkillInstall(group, sourceDir) {
3014
3317
  const target = describeTarget(group);
3015
- const state = readInstallState(target.skillsFolder, "2.0.0");
3318
+ const state = readInstallState(target.skillsFolder, "2.0.1");
3016
3319
  const knownOutcome = outcomeWithoutPrompt(state);
3017
3320
  if (knownOutcome) {
3018
- printMessage(describeInstallOutcome({ ...target, outcome: knownOutcome, version: "2.0.0" }));
3321
+ printMessage(describeInstallOutcome({ ...target, outcome: knownOutcome, version: "2.0.1" }));
3019
3322
  return;
3020
3323
  }
3021
3324
  const { installSkill: shouldInstall } = await (0, import_prompts2.default)({
@@ -3112,17 +3415,17 @@ function registerInit(program) {
3112
3415
  await runAIToolsStep();
3113
3416
  await runAgentSkillStep();
3114
3417
  await runProfileDetectionStep(config);
3115
- if (!import_fs17.default.existsSync(CONFIG_PATH)) {
3418
+ if (!import_fs18.default.existsSync(CONFIG_PATH)) {
3116
3419
  writeConfig(config);
3117
3420
  }
3118
3421
  printNextSteps(shell);
3119
3422
  });
3120
3423
  }
3121
- var import_fs17, import_path15, import_os7, import_prompts2, import_chalk3, TOTAL_STEPS;
3424
+ var import_fs18, import_path15, import_os7, import_prompts2, import_chalk3, TOTAL_STEPS;
3122
3425
  var init_init = __esm({
3123
3426
  "src/cli/init.ts"() {
3124
3427
  "use strict";
3125
- import_fs17 = __toESM(require("fs"));
3428
+ import_fs18 = __toESM(require("fs"));
3126
3429
  import_path15 = __toESM(require("path"));
3127
3430
  import_os7 = __toESM(require("os"));
3128
3431
  import_prompts2 = __toESM(require("prompts"));
@@ -3234,7 +3537,7 @@ function registerStatus(program) {
3234
3537
  const total = getEventCount();
3235
3538
  const todayEvents = getEventsForDate(todayDate());
3236
3539
  const last = getLastEvent();
3237
- const isPaused = import_fs18.default.existsSync(PAUSE_FLAG_PATH);
3540
+ const isPaused = import_fs19.default.existsSync(PAUSE_FLAG_PATH);
3238
3541
  log.plain("");
3239
3542
  log.plain(import_chalk4.default.bold(" Workscribe Status"));
3240
3543
  log.plain("");
@@ -3280,6 +3583,18 @@ function registerStatus(program) {
3280
3583
  );
3281
3584
  }
3282
3585
  log.plain(import_chalk4.default.dim(" Run workscribe init to repair it"));
3586
+ } else if (hookHealth.installed && hookHealth.nodeMissing) {
3587
+ log.plain("");
3588
+ log.plain(
3589
+ ` ${import_chalk4.default.yellow("\u26A0")} The Node.js version the Claude Code hook uses is gone \u2014 Claude Code activity is not being captured.`
3590
+ );
3591
+ log.plain(import_chalk4.default.dim(" Run workscribe init to repair it"));
3592
+ } else if (hookHealth.installed && !hookHealth.guarded) {
3593
+ log.plain("");
3594
+ log.plain(
3595
+ ` ${import_chalk4.default.yellow("\u26A0")} Claude Code hook is from an older version \u2014 it will show errors in Claude Code if Workscribe is ever removed.`
3596
+ );
3597
+ log.plain(import_chalk4.default.dim(" Run workscribe init to update it"));
3283
3598
  }
3284
3599
  const { total: weekTotal, otherCount } = getRecentOtherRate(7);
3285
3600
  if (weekTotal > 0) {
@@ -3299,11 +3614,11 @@ function registerStatus(program) {
3299
3614
  log.plain("");
3300
3615
  });
3301
3616
  }
3302
- var import_fs18, import_chalk4, AGENT_SKILL_LABEL;
3617
+ var import_fs19, import_chalk4, AGENT_SKILL_LABEL;
3303
3618
  var init_status = __esm({
3304
3619
  "src/cli/status.ts"() {
3305
3620
  "use strict";
3306
- import_fs18 = __toESM(require("fs"));
3621
+ import_fs19 = __toESM(require("fs"));
3307
3622
  import_chalk4 = __toESM(require("chalk"));
3308
3623
  init_db();
3309
3624
  init_config();
@@ -3798,13 +4113,13 @@ function isStdinInteractive() {
3798
4113
  return Boolean(process.stdin.isTTY);
3799
4114
  }
3800
4115
  function readAllStdin() {
3801
- return import_fs19.default.readFileSync(STDIN_FILE_DESCRIPTOR, "utf-8");
4116
+ return import_fs20.default.readFileSync(STDIN_FILE_DESCRIPTOR, "utf-8");
3802
4117
  }
3803
- var import_fs19, STDIN_FILE_DESCRIPTOR;
4118
+ var import_fs20, STDIN_FILE_DESCRIPTOR;
3804
4119
  var init_stdin = __esm({
3805
4120
  "src/utils/stdin.ts"() {
3806
4121
  "use strict";
3807
- import_fs19 = __toESM(require("fs"));
4122
+ import_fs20 = __toESM(require("fs"));
3808
4123
  STDIN_FILE_DESCRIPTOR = 0;
3809
4124
  }
3810
4125
  });
@@ -4539,19 +4854,19 @@ function registerExport(program) {
4539
4854
  const payload = buildDayPayload(date, sessions, locationContextFrom(config));
4540
4855
  const summary = await createSummaryWriter(config).summarize(payload);
4541
4856
  const outputPath = options.output ?? import_path17.default.join(EXPORTS_DIR, `workscribe-${date}.md`);
4542
- if (!import_fs20.default.existsSync(EXPORTS_DIR)) import_fs20.default.mkdirSync(EXPORTS_DIR, { recursive: true, mode: 448 });
4543
- import_fs20.default.writeFileSync(outputPath, `# Work Summary \u2014 ${date}
4857
+ if (!import_fs21.default.existsSync(EXPORTS_DIR)) import_fs21.default.mkdirSync(EXPORTS_DIR, { recursive: true, mode: 448 });
4858
+ import_fs21.default.writeFileSync(outputPath, `# Work Summary \u2014 ${date}
4544
4859
 
4545
4860
  ${summary}
4546
4861
  `, { encoding: "utf-8", mode: 384 });
4547
4862
  log.success(`Saved: ${outputPath}`);
4548
4863
  });
4549
4864
  }
4550
- var import_fs20, import_path17;
4865
+ var import_fs21, import_path17;
4551
4866
  var init_export = __esm({
4552
4867
  "src/cli/export.ts"() {
4553
4868
  "use strict";
4554
- import_fs20 = __toESM(require("fs"));
4869
+ import_fs21 = __toESM(require("fs"));
4555
4870
  import_path17 = __toESM(require("path"));
4556
4871
  init_detector();
4557
4872
  init_config();
@@ -4643,27 +4958,27 @@ var init_config2 = __esm({
4643
4958
  // src/cli/pause.ts
4644
4959
  function registerPause(program) {
4645
4960
  program.command("pause").description("Pause capture temporarily").action(() => {
4646
- if (import_fs21.default.existsSync(PAUSE_FLAG_PATH)) {
4961
+ if (import_fs22.default.existsSync(PAUSE_FLAG_PATH)) {
4647
4962
  log.info("Capture is already paused. Run workscribe resume to restart.");
4648
4963
  return;
4649
4964
  }
4650
- import_fs21.default.writeFileSync(PAUSE_FLAG_PATH, "", { mode: 384 });
4965
+ import_fs22.default.writeFileSync(PAUSE_FLAG_PATH, "", { mode: 384 });
4651
4966
  log.success("Capture paused. Run workscribe resume to restart.");
4652
4967
  });
4653
4968
  program.command("resume").description("Resume capture after a pause").action(() => {
4654
- if (!import_fs21.default.existsSync(PAUSE_FLAG_PATH)) {
4969
+ if (!import_fs22.default.existsSync(PAUSE_FLAG_PATH)) {
4655
4970
  log.info("Capture is not paused.");
4656
4971
  return;
4657
4972
  }
4658
- import_fs21.default.rmSync(PAUSE_FLAG_PATH);
4973
+ import_fs22.default.rmSync(PAUSE_FLAG_PATH);
4659
4974
  log.success("Capture resumed.");
4660
4975
  });
4661
4976
  }
4662
- var import_fs21;
4977
+ var import_fs22;
4663
4978
  var init_pause = __esm({
4664
4979
  "src/cli/pause.ts"() {
4665
4980
  "use strict";
4666
- import_fs21 = __toESM(require("fs"));
4981
+ import_fs22 = __toESM(require("fs"));
4667
4982
  init_paths();
4668
4983
  init_logger();
4669
4984
  }
@@ -4765,6 +5080,22 @@ var init_clear = __esm({
4765
5080
  });
4766
5081
 
4767
5082
  // src/cli/uninstall.ts
5083
+ function completionRcFiles() {
5084
+ return [".zshrc", ".bashrc", ".bash_profile"].map((file) => import_path18.default.join(import_os11.default.homedir(), file));
5085
+ }
5086
+ function removeCompletionsAndToolHooks() {
5087
+ for (const rcFile of completionRcFiles()) {
5088
+ if (removeCompletions(rcFile)) log.success(`Shell completions removed from ${rcFile}`);
5089
+ }
5090
+ if (removeFishCompletions()) log.success("Fish completions removed");
5091
+ const outcome = claudeCodeIntegration.uninstall?.();
5092
+ if (outcome === "removed") log.success("Claude Code session hook removed from ~/.claude/settings.json");
5093
+ if (outcome === "unreadable") {
5094
+ log.warn(
5095
+ "Couldn't read ~/.claude/settings.json, so the Claude Code session hook was left in place. Fix the file's JSON, then run: workscribe uninstall --hooks-only"
5096
+ );
5097
+ }
5098
+ }
4768
5099
  function detectShellRcFiles() {
4769
5100
  const candidates = [
4770
5101
  { name: "zsh", rcFile: import_path18.default.join(import_os11.default.homedir(), ".zshrc") },
@@ -4772,10 +5103,15 @@ function detectShellRcFiles() {
4772
5103
  { name: "bash", rcFile: import_path18.default.join(import_os11.default.homedir(), ".bash_profile") },
4773
5104
  { name: "fish", rcFile: import_path18.default.join(import_os11.default.homedir(), ".config", "fish", "conf.d", "workscribe.fish") }
4774
5105
  ];
4775
- return candidates.filter((candidate) => import_fs22.default.existsSync(candidate.rcFile) && isHookInstalled(candidate.rcFile));
5106
+ return candidates.filter((candidate) => import_fs23.default.existsSync(candidate.rcFile) && isHookInstalled(candidate.rcFile));
4776
5107
  }
4777
5108
  function registerUninstall(program) {
4778
- program.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) => {
5109
+ program.command("uninstall").description(
5110
+ "Remove shell hooks, completions, the Claude Code hook and the agent skill; optionally delete all data"
5111
+ ).option(
5112
+ "--hooks-only",
5113
+ "remove shell hooks, completions and the Claude Code hook only \u2014 keep the agent skill and skip the data prompt"
5114
+ ).action(async (options) => {
4779
5115
  log.plain(import_chalk10.default.bold("\n Workscribe \u2014 Uninstall\n"));
4780
5116
  const hooked = detectShellRcFiles();
4781
5117
  if (hooked.length === 0) {
@@ -4783,7 +5119,7 @@ function registerUninstall(program) {
4783
5119
  } else {
4784
5120
  for (const { name, rcFile } of hooked) {
4785
5121
  if (name === "fish") {
4786
- import_fs22.default.unlinkSync(rcFile);
5122
+ import_fs23.default.unlinkSync(rcFile);
4787
5123
  log.success(`Shell hook removed: ${rcFile}`);
4788
5124
  log.plain(` ${import_chalk10.default.dim("Restart your terminal to take effect")}`);
4789
5125
  } else {
@@ -4796,9 +5132,10 @@ function registerUninstall(program) {
4796
5132
  }
4797
5133
  }
4798
5134
  }
4799
- if (opts.hooksOnly) return;
5135
+ removeCompletionsAndToolHooks();
5136
+ if (options.hooksOnly) return;
4800
5137
  removeSkills(SKILL_AGENTS, { reportMissing: false });
4801
- const existingDirs = [...new Set(REQUIRED_DIRS)].filter((dir) => import_fs22.default.existsSync(dir));
5138
+ const existingDirs = [...new Set(REQUIRED_DIRS)].filter((dir) => import_fs23.default.existsSync(dir));
4802
5139
  if (existingDirs.length > 0) {
4803
5140
  log.plain("");
4804
5141
  const { deleteData } = await (0, import_prompts4.default)({
@@ -4809,7 +5146,7 @@ function registerUninstall(program) {
4809
5146
  });
4810
5147
  if (deleteData) {
4811
5148
  for (const dir of existingDirs) {
4812
- import_fs22.default.rmSync(dir, { recursive: true, force: true });
5149
+ import_fs23.default.rmSync(dir, { recursive: true, force: true });
4813
5150
  log.success(`Deleted ${dir}`);
4814
5151
  }
4815
5152
  } else {
@@ -4821,17 +5158,18 @@ function registerUninstall(program) {
4821
5158
  log.plain("");
4822
5159
  });
4823
5160
  }
4824
- var import_fs22, import_path18, import_os11, import_prompts4, import_chalk10;
5161
+ var import_fs23, import_path18, import_os11, import_prompts4, import_chalk10;
4825
5162
  var init_uninstall = __esm({
4826
5163
  "src/cli/uninstall.ts"() {
4827
5164
  "use strict";
4828
- import_fs22 = __toESM(require("fs"));
5165
+ import_fs23 = __toESM(require("fs"));
4829
5166
  import_path18 = __toESM(require("path"));
4830
5167
  import_os11 = __toESM(require("os"));
4831
5168
  import_prompts4 = __toESM(require("prompts"));
4832
5169
  import_chalk10 = __toESM(require("chalk"));
4833
5170
  init_paths();
4834
5171
  init_shell_hook();
5172
+ init_claude_code();
4835
5173
  init_logger();
4836
5174
  init_agents();
4837
5175
  init_actions();
@@ -4882,7 +5220,7 @@ function registerProfile(program) {
4882
5220
  log.plain(import_chalk11.default.dim(" " + "\u2500".repeat(60)));
4883
5221
  for (const entry of entries) {
4884
5222
  const badge = activeNames.has(entry.name) ? import_chalk11.default.green(" [active]") : "";
4885
- const installed = import_fs23.default.existsSync(import_path19.default.join(PROFILES_DIR, `${entry.name}.json`)) ? import_chalk11.default.dim(" installed") : "";
5223
+ const installed = import_fs24.default.existsSync(import_path19.default.join(PROFILES_DIR, `${entry.name}.json`)) ? import_chalk11.default.dim(" installed") : "";
4886
5224
  log.plain(` ${import_chalk11.default.cyan(entry.name.padEnd(18))} ${entry.displayName.padEnd(28)}${badge}${installed}`);
4887
5225
  if (entry.description) log.plain(` ${import_chalk11.default.dim(" ".repeat(18) + entry.description)}`);
4888
5226
  }
@@ -4919,7 +5257,7 @@ function registerProfile(program) {
4919
5257
  }
4920
5258
  activeNames.delete(name);
4921
5259
  const cachePath = import_path19.default.join(PROFILES_DIR, `${name}.json`);
4922
- if (import_fs23.default.existsSync(cachePath)) import_fs23.default.unlinkSync(cachePath);
5260
+ if (import_fs24.default.existsSync(cachePath)) import_fs24.default.unlinkSync(cachePath);
4923
5261
  log.success(`Profile "${name}" removed`);
4924
5262
  }
4925
5263
  config.capture.profiles = [...activeNames];
@@ -5046,11 +5384,11 @@ function registerProfile(program) {
5046
5384
  }
5047
5385
  });
5048
5386
  }
5049
- var import_fs23, import_path19, import_chalk11, MIN_SUGGEST_COUNT;
5387
+ var import_fs24, import_path19, import_chalk11, MIN_SUGGEST_COUNT;
5050
5388
  var init_profile = __esm({
5051
5389
  "src/cli/profile.ts"() {
5052
5390
  "use strict";
5053
- import_fs23 = __toESM(require("fs"));
5391
+ import_fs24 = __toESM(require("fs"));
5054
5392
  import_path19 = __toESM(require("path"));
5055
5393
  import_chalk11 = __toESM(require("chalk"));
5056
5394
  init_config();
@@ -5063,14 +5401,42 @@ var init_profile = __esm({
5063
5401
  }
5064
5402
  });
5065
5403
 
5404
+ // src/utils/working-directory.ts
5405
+ function realPath(target) {
5406
+ try {
5407
+ return import_fs25.default.realpathSync(target);
5408
+ } catch {
5409
+ return null;
5410
+ }
5411
+ }
5412
+ function currentDirectory(where = { pwd: process.env.PWD, cwd: process.cwd() }) {
5413
+ if (where.pwd && realPath(where.pwd) === realPath(where.cwd)) return where.pwd;
5414
+ return where.cwd;
5415
+ }
5416
+ function rootForms(cwd, root) {
5417
+ const resolvedCwd = realPath(cwd);
5418
+ if (!resolvedCwd) return [root];
5419
+ const reached = import_path20.default.resolve(cwd, import_path20.default.relative(resolvedCwd, root));
5420
+ return [.../* @__PURE__ */ new Set([root, reached])];
5421
+ }
5422
+ var import_fs25, import_path20;
5423
+ var init_working_directory = __esm({
5424
+ "src/utils/working-directory.ts"() {
5425
+ "use strict";
5426
+ import_fs25 = __toESM(require("fs"));
5427
+ import_path20 = __toESM(require("path"));
5428
+ }
5429
+ });
5430
+
5066
5431
  // src/cli/note.ts
5067
5432
  function registerNote(program) {
5068
5433
  program.command("note <text>").description("Attach a free-text annotation to the current session").action((text) => {
5069
- const { repoName, branch } = getGitContext(process.cwd());
5434
+ const directory = currentDirectory();
5435
+ const { repoName, branch } = getRepoContext(directory, readConfig());
5070
5436
  insertEvent({
5071
5437
  captured_at: (/* @__PURE__ */ new Date()).toISOString(),
5072
5438
  command: "workscribe note",
5073
- cwd: process.cwd(),
5439
+ cwd: directory,
5074
5440
  exit_code: null,
5075
5441
  duration_ms: null,
5076
5442
  repo_name: repoName,
@@ -5088,6 +5454,8 @@ var init_note = __esm({
5088
5454
  "use strict";
5089
5455
  init_db();
5090
5456
  init_git();
5457
+ init_config();
5458
+ init_working_directory();
5091
5459
  init_logger();
5092
5460
  }
5093
5461
  });
@@ -5118,8 +5486,8 @@ var init_week = __esm({
5118
5486
 
5119
5487
  // src/cli/bug-report.ts
5120
5488
  function tailFile(filePath, lines) {
5121
- if (!import_fs24.default.existsSync(filePath)) return "(no debug log found \u2014 set WORKSCRIBE_DEBUG=1 to enable)";
5122
- const content = import_fs24.default.readFileSync(filePath, "utf-8");
5489
+ if (!import_fs26.default.existsSync(filePath)) return "(no debug log found \u2014 set WORKSCRIBE_DEBUG=1 to enable)";
5490
+ const content = import_fs26.default.readFileSync(filePath, "utf-8");
5123
5491
  const allLines = content.split("\n");
5124
5492
  return allLines.slice(-lines).join("\n");
5125
5493
  }
@@ -5137,9 +5505,9 @@ function redactConfig() {
5137
5505
  function registerBugReport(program) {
5138
5506
  program.command("bug-report").description("Generate a diagnostic report for filing a bug").action(() => {
5139
5507
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
5140
- const reportPath = import_path20.default.join(WORKSCRIBE_DIR, `bug-report-${timestamp}.txt`);
5508
+ const reportPath = import_path21.default.join(WORKSCRIBE_DIR, `bug-report-${timestamp}.txt`);
5141
5509
  const systemInfo = [
5142
- `Workscribe version : ${"2.0.0"}`,
5510
+ `Workscribe version : ${"2.0.1"}`,
5143
5511
  `Node.js version : ${process.version}`,
5144
5512
  `Platform : ${process.platform}`,
5145
5513
  `Architecture : ${process.arch}`,
@@ -5161,17 +5529,17 @@ function registerBugReport(program) {
5161
5529
  tailFile(DEBUG_LOG_PATH, DEBUG_LOG_TAIL_LINES)
5162
5530
  ].join("\n");
5163
5531
  try {
5164
- if (!import_fs24.default.existsSync(WORKSCRIBE_DIR)) {
5165
- import_fs24.default.mkdirSync(WORKSCRIBE_DIR, { recursive: true, mode: 448 });
5532
+ if (!import_fs26.default.existsSync(WORKSCRIBE_DIR)) {
5533
+ import_fs26.default.mkdirSync(WORKSCRIBE_DIR, { recursive: true, mode: 448 });
5166
5534
  }
5167
- import_fs24.default.writeFileSync(reportPath, report, { mode: 384 });
5535
+ import_fs26.default.writeFileSync(reportPath, report, { mode: 384 });
5168
5536
  } catch (error) {
5169
5537
  log.error(`Could not write report: ${error.message}`);
5170
5538
  process.exit(1);
5171
5539
  }
5172
5540
  const params = new URLSearchParams({
5173
- title: `Bug report \u2014 workscribe ${"2.0.0"}`,
5174
- body: `<!-- Attach ${import_path20.default.basename(reportPath)} or paste the relevant sections below -->
5541
+ title: `Bug report \u2014 workscribe ${"2.0.1"}`,
5542
+ body: `<!-- Attach ${import_path21.default.basename(reportPath)} or paste the relevant sections below -->
5175
5543
 
5176
5544
  **Describe the issue:**
5177
5545
 
@@ -5188,13 +5556,13 @@ function registerBugReport(program) {
5188
5556
  log.plain("");
5189
5557
  });
5190
5558
  }
5191
- var import_fs24, import_os12, import_path20, import_chalk12, GITHUB_ISSUES_URL, DEBUG_LOG_TAIL_LINES;
5559
+ var import_fs26, import_os12, import_path21, import_chalk12, GITHUB_ISSUES_URL, DEBUG_LOG_TAIL_LINES;
5192
5560
  var init_bug_report = __esm({
5193
5561
  "src/cli/bug-report.ts"() {
5194
5562
  "use strict";
5195
- import_fs24 = __toESM(require("fs"));
5563
+ import_fs26 = __toESM(require("fs"));
5196
5564
  import_os12 = __toESM(require("os"));
5197
- import_path20 = __toESM(require("path"));
5565
+ import_path21 = __toESM(require("path"));
5198
5566
  import_chalk12 = __toESM(require("chalk"));
5199
5567
  init_paths();
5200
5568
  init_config();
@@ -5320,7 +5688,7 @@ function printSessionContext(session, heading) {
5320
5688
  function registerContext(program) {
5321
5689
  program.command("context").description("Show the last session for the current repo").option("--file <path>", "narrow to last session that touched this file").option("--json", "output as JSON instead of formatted text \u2014 null when no session is found").action((opts) => {
5322
5690
  if (opts.file) {
5323
- const normalizedFile = import_path21.default.normalize(opts.file);
5691
+ const normalizedFile = import_path22.default.normalize(opts.file);
5324
5692
  const session = getLastSessionTouchingFile(normalizedFile);
5325
5693
  if (!session) {
5326
5694
  if (opts.json) log.plain(JSON.stringify(null));
@@ -5331,16 +5699,20 @@ function registerContext(program) {
5331
5699
  log.plain(JSON.stringify(toContextJson(session)));
5332
5700
  return;
5333
5701
  }
5334
- printSessionContext(session, `Last session touching ${import_path21.default.basename(normalizedFile)}`);
5702
+ printSessionContext(session, `Last session touching ${import_path22.default.basename(normalizedFile)}`);
5335
5703
  return;
5336
5704
  }
5337
- const { repoName } = getGitContext(process.cwd(), null, readConfig().git.qualifyRepoName);
5705
+ const config = readConfig();
5706
+ const directory = currentDirectory();
5707
+ const { repoName, root, remote } = getRepoContext(directory, config);
5338
5708
  if (!repoName) {
5339
5709
  if (opts.json) log.plain(JSON.stringify(null));
5340
5710
  else log.plain(" Not in a git repository. Run workscribe context from a project directory.");
5341
5711
  return;
5342
5712
  }
5343
- const sessions = getRecentSessionsByRepo(repoName, 1);
5713
+ const otherRepoName = remote ? formatRepoName(remote, !config.git.qualifyRepoName) : null;
5714
+ const roots = root ? rootForms(directory, root) : [];
5715
+ const sessions = getRecentSessionsForRepo({ repoName, otherRepoName, roots }, 1);
5344
5716
  if (sessions.length === 0) {
5345
5717
  if (opts.json) log.plain(JSON.stringify(null));
5346
5718
  else log.plain(` No sessions found for ${repoName}`);
@@ -5353,14 +5725,16 @@ function registerContext(program) {
5353
5725
  printSessionContext(sessions[0], `Last session on ${repoName}`);
5354
5726
  });
5355
5727
  }
5356
- var import_path21, import_chalk14;
5728
+ var import_path22, import_chalk14;
5357
5729
  var init_context = __esm({
5358
5730
  "src/cli/context.ts"() {
5359
5731
  "use strict";
5360
- import_path21 = __toESM(require("path"));
5732
+ import_path22 = __toESM(require("path"));
5361
5733
  import_chalk14 = __toESM(require("chalk"));
5362
5734
  init_db();
5363
5735
  init_git();
5736
+ init_repo_name();
5737
+ init_working_directory();
5364
5738
  init_config();
5365
5739
  init_format();
5366
5740
  init_logger();
@@ -5448,7 +5822,7 @@ var init_skill = __esm({
5448
5822
  // src/utils/update-check.ts
5449
5823
  function readCache() {
5450
5824
  try {
5451
- return JSON.parse(import_fs25.default.readFileSync(UPDATE_CHECK_PATH, "utf-8"));
5825
+ return JSON.parse(import_fs27.default.readFileSync(UPDATE_CHECK_PATH, "utf-8"));
5452
5826
  } catch {
5453
5827
  return null;
5454
5828
  }
@@ -5456,7 +5830,7 @@ function readCache() {
5456
5830
  function writeCache(updates) {
5457
5831
  try {
5458
5832
  const current = readCache() ?? {};
5459
- import_fs25.default.writeFileSync(UPDATE_CHECK_PATH, JSON.stringify({ ...current, ...updates }), { mode: 384 });
5833
+ import_fs27.default.writeFileSync(UPDATE_CHECK_PATH, JSON.stringify({ ...current, ...updates }), { mode: 384 });
5460
5834
  } catch {
5461
5835
  }
5462
5836
  }
@@ -5469,7 +5843,7 @@ function isNewer(latest, current) {
5469
5843
  return latestPatch > currentPatch;
5470
5844
  }
5471
5845
  function checkForUpdate() {
5472
- const current = "2.0.0";
5846
+ const current = "2.0.1";
5473
5847
  const cache = readCache();
5474
5848
  if (cache && isNewer(cache.latestVersion, current)) {
5475
5849
  process.stderr.write(
@@ -5492,7 +5866,7 @@ function checkForUpdate() {
5492
5866
  function buildTelemetryPayload(config) {
5493
5867
  return {
5494
5868
  installId: config.telemetry.installId,
5495
- cliVersion: "2.0.0",
5869
+ cliVersion: "2.0.1",
5496
5870
  platform: import_os13.default.platform(),
5497
5871
  nodeVersion: process.version
5498
5872
  };
@@ -5517,11 +5891,11 @@ function pingTelemetry() {
5517
5891
  debugLog("pingTelemetry", error);
5518
5892
  });
5519
5893
  }
5520
- var import_fs25, import_os13, CHECK_INTERVAL_MS;
5894
+ var import_fs27, import_os13, CHECK_INTERVAL_MS;
5521
5895
  var init_update_check = __esm({
5522
5896
  "src/utils/update-check.ts"() {
5523
5897
  "use strict";
5524
- import_fs25 = __toESM(require("fs"));
5898
+ import_fs27 = __toESM(require("fs"));
5525
5899
  import_os13 = __toESM(require("os"));
5526
5900
  init_paths();
5527
5901
  init_config();
@@ -5568,7 +5942,7 @@ var require_main = __commonJS({
5568
5942
  pingTelemetry();
5569
5943
  }
5570
5944
  var program = new import_commander.Command();
5571
- program.name("workscribe").description("Automatic developer work journal").version("2.0.0");
5945
+ program.name("workscribe").description("Automatic developer work journal").version("2.0.1");
5572
5946
  registerCapture(program);
5573
5947
  registerHook(program);
5574
5948
  registerInit(program);