@workscribe/cli 2.0.0 → 2.1.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
@@ -1374,6 +1374,70 @@ 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
+
1405
+ // src/db/transaction.ts
1406
+ function isDatabaseLocked(error) {
1407
+ return error?.errcode === SQLITE_BUSY;
1408
+ }
1409
+ function rollbackQuietly(db) {
1410
+ try {
1411
+ db.exec("ROLLBACK");
1412
+ } catch {
1413
+ }
1414
+ }
1415
+ function withImmediateTransaction(db, work, options = {}) {
1416
+ const { busyTimeoutMs } = options;
1417
+ if (busyTimeoutMs !== void 0) db.exec(`PRAGMA busy_timeout = ${Math.max(0, Math.floor(busyTimeoutMs))}`);
1418
+ try {
1419
+ db.exec("BEGIN IMMEDIATE");
1420
+ try {
1421
+ const result = work();
1422
+ db.exec("COMMIT");
1423
+ return result;
1424
+ } catch (error) {
1425
+ rollbackQuietly(db);
1426
+ throw error;
1427
+ }
1428
+ } finally {
1429
+ if (busyTimeoutMs !== void 0) db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`);
1430
+ }
1431
+ }
1432
+ var BUSY_TIMEOUT_MS, SQLITE_BUSY;
1433
+ var init_transaction = __esm({
1434
+ "src/db/transaction.ts"() {
1435
+ "use strict";
1436
+ BUSY_TIMEOUT_MS = 5e3;
1437
+ SQLITE_BUSY = 5;
1438
+ }
1439
+ });
1440
+
1377
1441
  // src/db/index.ts
1378
1442
  function getDb() {
1379
1443
  if (_db) return _db;
@@ -1391,7 +1455,7 @@ function getDb() {
1391
1455
  }
1392
1456
  db.exec("PRAGMA journal_mode = WAL");
1393
1457
  db.exec("PRAGMA foreign_keys = ON");
1394
- db.exec("PRAGMA busy_timeout = 5000");
1458
+ db.exec(`PRAGMA busy_timeout = ${BUSY_TIMEOUT_MS}`);
1395
1459
  applyMigrations(db, MIGRATIONS);
1396
1460
  _db = db;
1397
1461
  return _db;
@@ -1404,9 +1468,8 @@ function insertEvent(params) {
1404
1468
  `);
1405
1469
  stmt.run(params);
1406
1470
  }
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 });
1471
+ function getRecentSessionsForRepo(lookup, limit) {
1472
+ return findRecentSessionsForRepo(getDb(), lookup, limit);
1410
1473
  }
1411
1474
  function getLastSessionTouchingFile(filePath) {
1412
1475
  const db = getDb();
@@ -1448,10 +1511,30 @@ function getEventsForDate(date) {
1448
1511
  const stmt = db.prepare(`
1449
1512
  SELECT * FROM events
1450
1513
  WHERE date(captured_at, 'localtime') = $date
1451
- ORDER BY captured_at ASC
1514
+ ORDER BY captured_at ASC, id ASC
1452
1515
  `);
1453
1516
  return stmt.all({ date });
1454
1517
  }
1518
+ function countEventsBetween(start, end) {
1519
+ const row = getDb().prepare("SELECT COUNT(*) AS count FROM events WHERE captured_at >= $start AND captured_at < $end").get({ start, end });
1520
+ return row.count;
1521
+ }
1522
+ function sumSessionEventCounts(date) {
1523
+ const row = getDb().prepare("SELECT COALESCE(SUM(event_count), 0) AS total FROM sessions WHERE date = $date").get({ date });
1524
+ return row.total;
1525
+ }
1526
+ function getLatestFileSaveAt(filePath) {
1527
+ const row = getDb().prepare(
1528
+ `SELECT captured_at FROM events
1529
+ WHERE file_path = $file_path AND category = 'code_edit'
1530
+ ORDER BY captured_at DESC
1531
+ LIMIT 1`
1532
+ ).get({ file_path: filePath });
1533
+ return row?.captured_at ?? null;
1534
+ }
1535
+ function runInImmediateTransaction(work, options) {
1536
+ return withImmediateTransaction(getDb(), work, options);
1537
+ }
1455
1538
  function getEventCount() {
1456
1539
  const db = getDb();
1457
1540
  const stmt = db.prepare("SELECT COUNT(*) as count FROM events");
@@ -1480,7 +1563,7 @@ function getEventsBySessionId(sessionId) {
1480
1563
  SELECT e.* FROM events e
1481
1564
  INNER JOIN session_events se ON se.event_id = e.id
1482
1565
  WHERE se.session_id = $session_id
1483
- ORDER BY e.captured_at ASC
1566
+ ORDER BY e.captured_at ASC, e.id ASC
1484
1567
  `);
1485
1568
  return stmt.all({ session_id: sessionId });
1486
1569
  }
@@ -1572,6 +1655,8 @@ var init_db = __esm({
1572
1655
  init_paths();
1573
1656
  init_migrations();
1574
1657
  init_migrate();
1658
+ init_session_lookup();
1659
+ init_transaction();
1575
1660
  _db = null;
1576
1661
  }
1577
1662
  });
@@ -1627,7 +1712,7 @@ var init_redact = __esm({
1627
1712
  }
1628
1713
  });
1629
1714
 
1630
- // src/capture/git.ts
1715
+ // src/capture/repo-name.ts
1631
1716
  function parseOwnerAndRepo(url) {
1632
1717
  const normalized = url.trim().replace(/^[^/]+:(?!\/\/)/, (match) => match.replace(":", "/"));
1633
1718
  const segments = normalized.split("/").filter(Boolean);
@@ -1636,16 +1721,24 @@ function parseOwnerAndRepo(url) {
1636
1721
  const owner = segments[segments.length - 2];
1637
1722
  return owner && repo ? { owner, repo } : null;
1638
1723
  }
1639
- function nameFromRemote(root, qualifyWithOwner) {
1724
+ function formatRepoName(remote, qualify) {
1725
+ return qualify ? `${remote.owner}/${remote.repo}` : remote.repo;
1726
+ }
1727
+ var init_repo_name = __esm({
1728
+ "src/capture/repo-name.ts"() {
1729
+ "use strict";
1730
+ }
1731
+ });
1732
+
1733
+ // src/capture/git.ts
1734
+ function remoteOf(root) {
1640
1735
  try {
1641
1736
  const url = (0, import_child_process.execSync)("git remote get-url origin", {
1642
1737
  cwd: root,
1643
1738
  timeout: 2e3,
1644
1739
  stdio: "pipe"
1645
1740
  }).toString().trim();
1646
- const parsed = parseOwnerAndRepo(url);
1647
- if (!parsed) return null;
1648
- return qualifyWithOwner ? `${parsed.owner}/${parsed.repo}` : parsed.repo;
1741
+ return parseOwnerAndRepo(url);
1649
1742
  } catch {
1650
1743
  return null;
1651
1744
  }
@@ -1660,35 +1753,46 @@ function nameFromPackageJson(root) {
1660
1753
  return null;
1661
1754
  }
1662
1755
  }
1663
- function getGitContext(cwd, nameOverride, qualifyRepoName = false) {
1756
+ function currentBranch(cwd) {
1757
+ try {
1758
+ return (0, import_child_process.execSync)("git branch --show-current", {
1759
+ cwd,
1760
+ timeout: 2e3,
1761
+ stdio: "pipe"
1762
+ }).toString().trim() || null;
1763
+ } catch {
1764
+ return null;
1765
+ }
1766
+ }
1767
+ function nameRepo(root, nameOverride, qualifyRepoName) {
1768
+ if (nameOverride !== null) return { repoName: nameOverride, remote: null };
1769
+ const remote = remoteOf(root);
1770
+ if (remote) return { repoName: formatRepoName(remote, qualifyRepoName), remote };
1771
+ return { repoName: nameFromPackageJson(root) ?? import_path6.default.basename(root), remote: null };
1772
+ }
1773
+ function getRepoContext(cwd, config) {
1774
+ let root;
1664
1775
  try {
1665
- const root = (0, import_child_process.execSync)("git rev-parse --show-toplevel", {
1776
+ root = (0, import_child_process.execSync)("git rev-parse --show-toplevel", {
1666
1777
  cwd,
1667
1778
  timeout: 2e3,
1668
1779
  stdio: "pipe"
1669
1780
  }).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
1781
  } catch {
1682
- return { repoName: null, branch: null };
1782
+ return NOT_A_REPO;
1683
1783
  }
1784
+ const naming = nameRepo(root, config.projects?.[cwd] ?? null, config.git.qualifyRepoName);
1785
+ return { ...naming, branch: currentBranch(cwd), root };
1684
1786
  }
1685
- var import_child_process, import_path6, import_fs8;
1787
+ var import_child_process, import_path6, import_fs8, NOT_A_REPO;
1686
1788
  var init_git = __esm({
1687
1789
  "src/capture/git.ts"() {
1688
1790
  "use strict";
1689
1791
  import_child_process = require("child_process");
1690
1792
  import_path6 = __toESM(require("path"));
1691
1793
  import_fs8 = __toESM(require("fs"));
1794
+ init_repo_name();
1795
+ NOT_A_REPO = { repoName: null, branch: null, root: null, remote: null };
1692
1796
  }
1693
1797
  });
1694
1798
 
@@ -1818,8 +1922,7 @@ function captureCommand(opts) {
1818
1922
  }
1819
1923
  }
1820
1924
  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);
1925
+ const { repoName: repoName2, branch: branch2 } = getRepoContext(cwd, config);
1823
1926
  insertEvent({
1824
1927
  captured_at: (/* @__PURE__ */ new Date()).toISOString(),
1825
1928
  command: command2,
@@ -1840,8 +1943,7 @@ function captureCommand(opts) {
1840
1943
  const last = getLastEvent();
1841
1944
  if (isDuplicate(command, cwd, last ?? void 0)) return;
1842
1945
  const safeCommand = redact(command, config.redact.extra);
1843
- const nameOverride = config.projects?.[cwd] ?? null;
1844
- const { repoName, branch } = getGitContext(cwd, nameOverride);
1946
+ const { repoName, branch } = getRepoContext(cwd, config);
1845
1947
  const { category, metadata } = normalizeCommand(safeCommand);
1846
1948
  if (category === "ai_session" && config.capture.aiTools === false) return;
1847
1949
  if (category === "other") {
@@ -2105,10 +2207,112 @@ var init_completion_scripts = __esm({
2105
2207
  }
2106
2208
  });
2107
2209
 
2210
+ // src/utils/backup.ts
2211
+ function backUpFile(file) {
2212
+ const base = `${file}.workscribe-backup-${Date.now()}`;
2213
+ for (let attempt = 1; attempt <= MAX_BACKUP_ATTEMPTS; attempt++) {
2214
+ const backup = attempt === 1 ? base : `${base}-${attempt}`;
2215
+ try {
2216
+ import_fs12.default.copyFileSync(file, backup, import_fs12.default.constants.COPYFILE_EXCL);
2217
+ return backup;
2218
+ } catch (error) {
2219
+ if (error.code !== "EEXIST") throw error;
2220
+ }
2221
+ }
2222
+ throw new Error(`Couldn't back up ${file}: too many backups named ${base}`);
2223
+ }
2224
+ var import_fs12, MAX_BACKUP_ATTEMPTS;
2225
+ var init_backup = __esm({
2226
+ "src/utils/backup.ts"() {
2227
+ "use strict";
2228
+ import_fs12 = __toESM(require("fs"));
2229
+ MAX_BACKUP_ATTEMPTS = 100;
2230
+ }
2231
+ });
2232
+
2233
+ // src/capture/completions-block.ts
2234
+ function completionsBlock(shellName, file) {
2235
+ const guardedSource = shellName === "zsh" ? `if [ -f "${file}" ]; then ${ZSH_COMPINIT}; source "${file}"; fi` : `[ -f "${file}" ] && source "${file}"`;
2236
+ return `${COMPLETIONS_MARKER}
2237
+ ${guardedSource}`;
2238
+ }
2239
+ function splitLines(content) {
2240
+ return { lines: content.split("\n"), lineEnding: content.includes("\r\n") ? "\r" : "" };
2241
+ }
2242
+ function bare(line) {
2243
+ return (line ?? "").replace(/\r$/, "");
2244
+ }
2245
+ function blockLengthAfterMarker(lines, markerIndex) {
2246
+ const next = bare(lines[markerIndex + 1]);
2247
+ if (GUARDED_ZSH.test(next) || GUARDED_BASH.test(next) || UNGUARDED_SOURCE.test(next)) return 1;
2248
+ if (next === ZSH_COMPINIT && UNGUARDED_SOURCE.test(bare(lines[markerIndex + 2]))) return 2;
2249
+ return 0;
2250
+ }
2251
+ function findBlocks(lines) {
2252
+ const blocks = [];
2253
+ for (let index = 0; index < lines.length; index++) {
2254
+ if (bare(lines[index]) !== COMPLETIONS_MARKER) continue;
2255
+ const length = blockLengthAfterMarker(lines, index);
2256
+ if (length === 0) continue;
2257
+ blocks.push({ first: index, last: index + length });
2258
+ index += length;
2259
+ }
2260
+ return blocks;
2261
+ }
2262
+ function blockText(lines, block) {
2263
+ return lines.slice(block.first, block.last + 1).map(bare).join("\n");
2264
+ }
2265
+ function hasCompletionsBlock(content) {
2266
+ return findBlocks(splitLines(content).lines).length > 0;
2267
+ }
2268
+ function isCompletionsBlockOutdated(content, currentBlock) {
2269
+ const { lines } = splitLines(content);
2270
+ const blocks = findBlocks(lines);
2271
+ if (blocks.length === 0) return false;
2272
+ return blocks.length > 1 || blockText(lines, blocks[0]) !== currentBlock;
2273
+ }
2274
+ function rebuild(content, replacement) {
2275
+ const { lines, lineEnding } = splitLines(content);
2276
+ const blocks = findBlocks(lines);
2277
+ if (blocks.length === 0) return content;
2278
+ const dropped = /* @__PURE__ */ new Set();
2279
+ blocks.forEach((block, blockIndex) => {
2280
+ const isReplaced = blockIndex === 0 && replacement !== null;
2281
+ for (let index = block.first; index <= block.last; index++) dropped.add(index);
2282
+ if (!isReplaced && block.first > 0 && bare(lines[block.first - 1]) === "") dropped.add(block.first - 1);
2283
+ });
2284
+ const replacementLines = replacement === null ? [] : replacement.split("\n").map((line) => line + lineEnding);
2285
+ return lines.flatMap((line, index) => {
2286
+ if (replacement !== null && index === blocks[0].first) return replacementLines;
2287
+ return dropped.has(index) ? [] : [line];
2288
+ }).join("\n");
2289
+ }
2290
+ function replaceCompletionsBlock(content, newBlock) {
2291
+ return rebuild(content, newBlock);
2292
+ }
2293
+ function removeCompletionsBlock(content) {
2294
+ return rebuild(content, null);
2295
+ }
2296
+ var COMPLETIONS_MARKER, ZSH_COMPINIT, OWN_SCRIPT, GUARDED_ZSH, GUARDED_BASH, UNGUARDED_SOURCE;
2297
+ var init_completions_block = __esm({
2298
+ "src/capture/completions-block.ts"() {
2299
+ "use strict";
2300
+ COMPLETIONS_MARKER = "# Workscribe completions";
2301
+ ZSH_COMPINIT = "autoload -Uz compinit && compinit";
2302
+ OWN_SCRIPT = '"[^"]*completion\\.(?:zsh|bash)"';
2303
+ GUARDED_ZSH = new RegExp(`^if \\[ -f ${OWN_SCRIPT} \\]; then ${ZSH_COMPINIT}; source ${OWN_SCRIPT}; fi$`);
2304
+ GUARDED_BASH = new RegExp(`^\\[ -f ${OWN_SCRIPT} \\] && source ${OWN_SCRIPT}$`);
2305
+ UNGUARDED_SOURCE = new RegExp(`^source ${OWN_SCRIPT}$`);
2306
+ }
2307
+ });
2308
+
2108
2309
  // src/capture/shell-hook.ts
2310
+ function backUp(rcFile) {
2311
+ log.info(`Backed up ${rcFile} \u2192 ${import_path9.default.basename(backUpFile(rcFile))}`);
2312
+ }
2109
2313
  function getInstalledHookBlock(rcFile) {
2110
- if (!import_fs12.default.existsSync(rcFile)) return null;
2111
- const content = import_fs12.default.readFileSync(rcFile, "utf-8");
2314
+ if (!import_fs13.default.existsSync(rcFile)) return null;
2315
+ const content = import_fs13.default.readFileSync(rcFile, "utf-8");
2112
2316
  const startIdx = content.indexOf(HOOK_START);
2113
2317
  const endIdx = content.indexOf(HOOK_END);
2114
2318
  if (startIdx === -1 || endIdx === -1) return null;
@@ -2123,34 +2327,28 @@ function isHookOutdated(rcFile, hookContent) {
2123
2327
  return installed.trim() !== hookContent.trim();
2124
2328
  }
2125
2329
  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, `
2330
+ backUp(rcFile);
2331
+ import_fs13.default.appendFileSync(rcFile, `
2130
2332
  ${hookContent}
2131
2333
  `);
2132
2334
  }
2133
2335
  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");
2336
+ backUp(rcFile);
2337
+ const content = import_fs13.default.readFileSync(rcFile, "utf-8");
2138
2338
  const startIdx = content.indexOf(HOOK_START);
2139
2339
  const endIdx = content.indexOf(HOOK_END) + HOOK_END.length;
2140
- import_fs12.default.writeFileSync(rcFile, content.slice(0, startIdx) + hookContent + content.slice(endIdx));
2340
+ import_fs13.default.writeFileSync(rcFile, content.slice(0, startIdx) + hookContent + content.slice(endIdx));
2141
2341
  }
2142
2342
  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");
2343
+ backUp(rcFile);
2344
+ const content = import_fs13.default.readFileSync(rcFile, "utf-8");
2147
2345
  const startIdx = content.indexOf(HOOK_START);
2148
2346
  const endIdx = content.indexOf(HOOK_END) + HOOK_END.length;
2149
2347
  const preambleIdx = content.lastIndexOf(HOOK_PREAMBLE, startIdx);
2150
2348
  const blockStart = preambleIdx !== -1 ? preambleIdx : startIdx;
2151
2349
  const before = content.slice(0, blockStart).replace(/\n+$/, "\n");
2152
2350
  const after = content.slice(endIdx).replace(/^\n/, "");
2153
- import_fs12.default.writeFileSync(rcFile, before + after);
2351
+ import_fs13.default.writeFileSync(rcFile, before + after);
2154
2352
  }
2155
2353
  function resolveWorkscribeBinary() {
2156
2354
  try {
@@ -2167,53 +2365,86 @@ function injectBinaryPath(hookContent, binaryPath, nodePath) {
2167
2365
  }
2168
2366
  function isCompletionsInstalled(shell) {
2169
2367
  if (shell.name === "fish") {
2170
- return import_fs12.default.existsSync(import_path9.default.join(FISH_COMPLETIONS_DIR, "workscribe.fish"));
2368
+ return import_fs13.default.existsSync(import_path9.default.join(FISH_COMPLETIONS_DIR, "workscribe.fish"));
2171
2369
  }
2172
2370
  try {
2173
- return import_fs12.default.readFileSync(shell.rcFile, "utf-8").includes(COMPLETIONS_MARKER);
2371
+ return import_fs13.default.readFileSync(shell.rcFile, "utf-8").includes(COMPLETIONS_MARKER);
2174
2372
  } catch {
2175
2373
  return false;
2176
2374
  }
2177
2375
  }
2178
- function installCompletions(shell, commands) {
2376
+ function completionScriptFile(shellName) {
2377
+ return import_path9.default.join(WORKSCRIBE_DIR, `completion.${shellName}`);
2378
+ }
2379
+ function readRcFile(rcFile) {
2380
+ try {
2381
+ return import_fs13.default.readFileSync(rcFile, "utf-8");
2382
+ } catch {
2383
+ return null;
2384
+ }
2385
+ }
2386
+ function writeCompletionScript(shell, commands) {
2179
2387
  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
- }
2388
+ import_fs13.default.mkdirSync(FISH_COMPLETIONS_DIR, { recursive: true });
2389
+ import_fs13.default.writeFileSync(FISH_COMPLETIONS_FILE, fishCompletion(commands), { mode: 420 });
2390
+ return;
2391
+ }
2392
+ const script = shell.name === "zsh" ? zshCompletion(commands) : bashCompletion(commands);
2393
+ import_fs13.default.mkdirSync(WORKSCRIBE_DIR, { recursive: true });
2394
+ import_fs13.default.writeFileSync(completionScriptFile(shell.name), script, { mode: 420 });
2395
+ }
2396
+ function installCompletions(shell, commands) {
2397
+ writeCompletionScript(shell, commands);
2398
+ if (shell.name !== "fish" && !isCompletionsInstalled(shell)) {
2399
+ import_fs13.default.appendFileSync(shell.rcFile, `
2400
+ ${completionsBlock(shell.name, completionScriptFile(shell.name))}
2401
+ `);
2199
2402
  }
2200
2403
  }
2201
- var import_fs12, import_path9, import_os3, import_child_process3, FISH_COMPLETIONS_DIR, HOOK_START, HOOK_END, HOOK_PREAMBLE, COMPLETIONS_MARKER;
2404
+ function isCompletionsOutdated(shell) {
2405
+ if (shell.name === "fish") return false;
2406
+ const content = readRcFile(shell.rcFile);
2407
+ return content !== null && isCompletionsBlockOutdated(content, completionsBlock(shell.name, completionScriptFile(shell.name)));
2408
+ }
2409
+ function updateCompletions(shell) {
2410
+ const content = readRcFile(shell.rcFile);
2411
+ if (content === null || !hasCompletionsBlock(content)) return;
2412
+ backUp(shell.rcFile);
2413
+ import_fs13.default.writeFileSync(
2414
+ shell.rcFile,
2415
+ replaceCompletionsBlock(content, completionsBlock(shell.name, completionScriptFile(shell.name)))
2416
+ );
2417
+ }
2418
+ function removeCompletions(rcFile) {
2419
+ const content = readRcFile(rcFile);
2420
+ if (content === null || !hasCompletionsBlock(content)) return false;
2421
+ backUp(rcFile);
2422
+ import_fs13.default.writeFileSync(rcFile, removeCompletionsBlock(content));
2423
+ return true;
2424
+ }
2425
+ function removeFishCompletions() {
2426
+ if (!import_fs13.default.existsSync(FISH_COMPLETIONS_FILE)) return false;
2427
+ import_fs13.default.rmSync(FISH_COMPLETIONS_FILE, { force: true });
2428
+ return true;
2429
+ }
2430
+ var import_fs13, import_path9, import_os3, import_child_process3, FISH_COMPLETIONS_DIR, HOOK_START, HOOK_END, HOOK_PREAMBLE, FISH_COMPLETIONS_FILE;
2202
2431
  var init_shell_hook = __esm({
2203
2432
  "src/capture/shell-hook.ts"() {
2204
2433
  "use strict";
2205
- import_fs12 = __toESM(require("fs"));
2434
+ import_fs13 = __toESM(require("fs"));
2206
2435
  import_path9 = __toESM(require("path"));
2207
2436
  import_os3 = __toESM(require("os"));
2208
2437
  import_child_process3 = require("child_process");
2209
2438
  init_logger();
2210
2439
  init_completion_scripts();
2211
2440
  init_paths();
2441
+ init_backup();
2442
+ init_completions_block();
2212
2443
  FISH_COMPLETIONS_DIR = import_path9.default.join(import_os3.default.homedir(), ".config", "fish", "completions");
2213
2444
  HOOK_START = "# workscribe:hook:start";
2214
2445
  HOOK_END = "# workscribe:hook:end";
2215
2446
  HOOK_PREAMBLE = "# Workscribe \u2014 developer work journal";
2216
- COMPLETIONS_MARKER = "# Workscribe completions";
2447
+ FISH_COMPLETIONS_FILE = import_path9.default.join(FISH_COMPLETIONS_DIR, "workscribe.fish");
2217
2448
  }
2218
2449
  });
2219
2450
 
@@ -2234,11 +2465,43 @@ var init_utils = __esm({
2234
2465
  }
2235
2466
  });
2236
2467
 
2468
+ // src/capture/hooks/claude-code-command.ts
2469
+ function buildHookCommand(options) {
2470
+ if (!options.binaryPath) return `${BARE_GUARD}workscribe ${options.args}${GUARD_SUFFIX}`;
2471
+ const { nodePath, binaryPath, args } = options;
2472
+ return `test -x "${nodePath}" && test -x "${binaryPath}" && "${nodePath}" "${binaryPath}" ${args}${GUARD_SUFFIX}`;
2473
+ }
2474
+ function withoutLeadingGuards(command) {
2475
+ return command.trimStart().replace(LEADING_PATH_GUARDS, "");
2476
+ }
2477
+ function isPinnedCommand(command) {
2478
+ const invocation = withoutLeadingGuards(command);
2479
+ return invocation.startsWith('"') || invocation.startsWith("/");
2480
+ }
2481
+ function isGuardedCommand(command) {
2482
+ const trimmed = command.trim();
2483
+ const hasLeadingGuard = LEADING_PATH_GUARDS.test(trimmed) || trimmed.startsWith(BARE_GUARD);
2484
+ return hasLeadingGuard && trimmed.endsWith(GUARD_SUFFIX);
2485
+ }
2486
+ function pinnedNodePath(command) {
2487
+ return withoutLeadingGuards(command).match(/^"([^"]+)"/)?.[1] ?? null;
2488
+ }
2489
+ var HOOK_MARKER, GUARD_SUFFIX, LEADING_PATH_GUARDS, BARE_GUARD;
2490
+ var init_claude_code_command = __esm({
2491
+ "src/capture/hooks/claude-code-command.ts"() {
2492
+ "use strict";
2493
+ HOOK_MARKER = "_hook --source claude-code";
2494
+ GUARD_SUFFIX = " || true";
2495
+ LEADING_PATH_GUARDS = /^(?:test -x "[^"]*" && )+/;
2496
+ BARE_GUARD = "command -v workscribe >/dev/null 2>&1 && ";
2497
+ }
2498
+ });
2499
+
2237
2500
  // src/capture/hooks/claude-code.ts
2238
2501
  function readSettings() {
2239
- if (!import_fs13.default.existsSync(CLAUDE_SETTINGS_PATH)) return {};
2502
+ if (!import_fs14.default.existsSync(CLAUDE_SETTINGS_PATH)) return {};
2240
2503
  try {
2241
- return JSON.parse(import_fs13.default.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8"));
2504
+ return JSON.parse(import_fs14.default.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8"));
2242
2505
  } catch {
2243
2506
  return {};
2244
2507
  }
@@ -2264,10 +2527,6 @@ function findWorkscribeHookCommands(settings) {
2264
2527
  userPromptSubmit: commandsForEvent(hooks, "UserPromptSubmit")
2265
2528
  };
2266
2529
  }
2267
- function isPinnedCommand(command) {
2268
- const trimmed = command.trimStart();
2269
- return trimmed.startsWith('"') || trimmed.startsWith("/");
2270
- }
2271
2530
  function withoutWorkscribeEntries(entries) {
2272
2531
  return entries.map((entry) => {
2273
2532
  const record = entry;
@@ -2279,11 +2538,29 @@ function withoutWorkscribeEntries(entries) {
2279
2538
  return { ...record, hooks: filteredHooks };
2280
2539
  }).filter((entry) => entry.hooks.length > 0);
2281
2540
  }
2282
- function checkHookHealth() {
2541
+ function isExecutable(filePath) {
2542
+ try {
2543
+ import_fs14.default.accessSync(filePath, import_fs14.default.constants.X_OK);
2544
+ return true;
2545
+ } catch {
2546
+ return false;
2547
+ }
2548
+ }
2549
+ function allWorkscribeCommands() {
2283
2550
  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 };
2551
+ return [...postToolUse, ...userPromptSubmit];
2552
+ }
2553
+ function checkHookHealth() {
2554
+ const allCommands = allWorkscribeCommands();
2555
+ if (allCommands.length === 0) {
2556
+ return { installed: false, pinned: true, guarded: true, nodeMissing: false, confirmedBroken: null };
2557
+ }
2558
+ const guarded = allCommands.every(isGuardedCommand);
2559
+ const nodePaths = allCommands.map(pinnedNodePath).filter((nodePath) => nodePath !== null);
2560
+ const nodeMissing = nodePaths.some((nodePath) => !isExecutable(nodePath));
2561
+ if (allCommands.every(isPinnedCommand)) {
2562
+ return { installed: true, pinned: true, guarded, nodeMissing, confirmedBroken: null };
2563
+ }
2287
2564
  let confirmedBroken;
2288
2565
  try {
2289
2566
  (0, import_child_process5.execFileSync)("workscribe", ["--version"], { stdio: "ignore", timeout: 2e3 });
@@ -2291,48 +2568,76 @@ function checkHookHealth() {
2291
2568
  } catch {
2292
2569
  confirmedBroken = true;
2293
2570
  }
2294
- return { installed: true, pinned: false, confirmedBroken };
2571
+ return { installed: true, pinned: false, guarded, nodeMissing, confirmedBroken };
2572
+ }
2573
+ function readSettingsStrictly() {
2574
+ if (!import_fs14.default.existsSync(CLAUDE_SETTINGS_PATH)) return "missing";
2575
+ try {
2576
+ const parsed = JSON.parse(import_fs14.default.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8"));
2577
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : "unreadable";
2578
+ } catch {
2579
+ return "unreadable";
2580
+ }
2581
+ }
2582
+ function hooksWithoutWorkscribe(hooks) {
2583
+ let removedAny = false;
2584
+ const remaining = {};
2585
+ for (const [eventName, entries] of Object.entries(hooks)) {
2586
+ if (!Array.isArray(entries)) {
2587
+ remaining[eventName] = entries;
2588
+ continue;
2589
+ }
2590
+ const kept = withoutWorkscribeEntries(entries);
2591
+ if (commandsForEvent(hooks, eventName).length > 0) removedAny = true;
2592
+ if (kept.length > 0) remaining[eventName] = kept;
2593
+ }
2594
+ return removedAny ? remaining : null;
2595
+ }
2596
+ function isHookCurrent() {
2597
+ const { postToolUse, userPromptSubmit } = findWorkscribeHookCommands(readSettings());
2598
+ if (postToolUse.length === 0 || userPromptSubmit.length === 0) return false;
2599
+ return [...postToolUse, ...userPromptSubmit].every((command) => {
2600
+ const nodePath = pinnedNodePath(command);
2601
+ return isPinnedCommand(command) && isGuardedCommand(command) && nodePath !== null && isExecutable(nodePath);
2602
+ });
2295
2603
  }
2296
- var import_fs13, import_path10, import_os4, import_child_process5, CLAUDE_SETTINGS_PATH, HOOK_MARKER, claudeCodeIntegration;
2604
+ var import_fs14, import_path10, import_os4, import_child_process5, CLAUDE_SETTINGS_PATH, claudeCodeIntegration;
2297
2605
  var init_claude_code = __esm({
2298
2606
  "src/capture/hooks/claude-code.ts"() {
2299
2607
  "use strict";
2300
- import_fs13 = __toESM(require("fs"));
2608
+ import_fs14 = __toESM(require("fs"));
2301
2609
  import_path10 = __toESM(require("path"));
2302
2610
  import_os4 = __toESM(require("os"));
2303
2611
  import_child_process5 = require("child_process");
2304
2612
  init_utils();
2305
2613
  init_shell_hook();
2614
+ init_backup();
2615
+ init_claude_code_command();
2306
2616
  CLAUDE_SETTINGS_PATH = import_path10.default.join(import_os4.default.homedir(), ".claude", "settings.json");
2307
- HOOK_MARKER = "_hook --source claude-code";
2308
2617
  claudeCodeIntegration = {
2309
2618
  displayName: "Claude Code",
2310
2619
  isInstalled() {
2311
2620
  return isCommandInstalled("claude");
2312
2621
  },
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.
2622
+ // "Installed" means a *current* hook is present, not just any workscribe hook — so `workscribe init` offers to
2623
+ // update a stale one (unpinned, unguarded, or pinned to a Node that's gone) through its outdated-hook prompt
2317
2624
  isHookInstalled() {
2318
- const { postToolUse, userPromptSubmit } = findWorkscribeHookCommands(readSettings());
2319
- if (postToolUse.length === 0 || userPromptSubmit.length === 0) return false;
2320
- return [...postToolUse, ...userPromptSubmit].every(isPinnedCommand);
2625
+ return isHookCurrent();
2321
2626
  },
2322
2627
  install() {
2323
2628
  const settings = readSettings();
2324
2629
  const existingHooks = settings.hooks ?? {};
2325
2630
  const existingPostToolUse = withoutWorkscribeEntries(existingHooks.PostToolUse ?? []);
2326
2631
  const existingUserPromptSubmit = withoutWorkscribeEntries(existingHooks.UserPromptSubmit ?? []);
2327
- const workscribeBinary = resolveWorkscribeBinary();
2328
- const baseCommand = workscribeBinary ? `"${process.execPath}" "${workscribeBinary}"` : "workscribe";
2632
+ const binaryPath = resolveWorkscribeBinary();
2633
+ const command = (args) => buildHookCommand({ nodePath: process.execPath, binaryPath, args });
2329
2634
  settings.hooks = {
2330
2635
  ...existingHooks,
2331
2636
  PostToolUse: [
2332
2637
  ...existingPostToolUse,
2333
2638
  {
2334
2639
  matcher: "Bash",
2335
- hooks: [{ type: "command", command: `${baseCommand} ${HOOK_MARKER}`, async: true }]
2640
+ hooks: [{ type: "command", command: command(HOOK_MARKER), async: true }]
2336
2641
  }
2337
2642
  ],
2338
2643
  UserPromptSubmit: [
@@ -2341,14 +2646,33 @@ var init_claude_code = __esm({
2341
2646
  hooks: [
2342
2647
  {
2343
2648
  type: "command",
2344
- command: `${baseCommand} ${HOOK_MARKER} --event UserPromptSubmit`
2649
+ command: command(`${HOOK_MARKER} --event UserPromptSubmit`)
2345
2650
  }
2346
2651
  ]
2347
2652
  }
2348
2653
  ]
2349
2654
  };
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));
2655
+ import_fs14.default.mkdirSync(import_path10.default.dirname(CLAUDE_SETTINGS_PATH), { recursive: true });
2656
+ import_fs14.default.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
2657
+ },
2658
+ // Some workscribe hook is present, but not all of them are current (pinned and guarded) — including a
2659
+ // half-installed one where only PostToolUse or only UserPromptSubmit exists
2660
+ isHookOutdated() {
2661
+ return allWorkscribeCommands().length > 0 && !isHookCurrent();
2662
+ },
2663
+ uninstall() {
2664
+ const settings = readSettingsStrictly();
2665
+ if (settings === "missing") return "not-installed";
2666
+ if (settings === "unreadable") return "unreadable";
2667
+ const hooks = settings.hooks;
2668
+ if (hooks === null || typeof hooks !== "object" || Array.isArray(hooks)) return "not-installed";
2669
+ const remainingHooks = hooksWithoutWorkscribe(hooks);
2670
+ if (!remainingHooks) return "not-installed";
2671
+ const updated = { ...settings, hooks: remainingHooks };
2672
+ if (Object.keys(remainingHooks).length === 0) delete updated.hooks;
2673
+ backUpFile(CLAUDE_SETTINGS_PATH);
2674
+ import_fs14.default.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(updated, null, 2));
2675
+ return "removed";
2352
2676
  }
2353
2677
  };
2354
2678
  }
@@ -2469,10 +2793,10 @@ function defaultDetectionEnvironment() {
2469
2793
  homeDir: import_os5.default.homedir(),
2470
2794
  // `which` spawns a process, so each command is looked up at most once per run
2471
2795
  isCommandInstalled: memoize(isCommandInstalled),
2472
- pathExists: (absolutePath) => import_fs14.default.existsSync(absolutePath),
2796
+ pathExists: (absolutePath) => import_fs15.default.existsSync(absolutePath),
2473
2797
  listDirectory: (absolutePath) => {
2474
2798
  try {
2475
- return import_fs14.default.readdirSync(absolutePath);
2799
+ return import_fs15.default.readdirSync(absolutePath);
2476
2800
  } catch {
2477
2801
  return [];
2478
2802
  }
@@ -2493,11 +2817,11 @@ function isAgentPresent(agent, environment) {
2493
2817
  function detectAgents(environment = defaultDetectionEnvironment()) {
2494
2818
  return SKILL_AGENTS.filter((agent) => isAgentPresent(agent, environment));
2495
2819
  }
2496
- var import_fs14, import_os5, import_path11;
2820
+ var import_fs15, import_os5, import_path11;
2497
2821
  var init_detect = __esm({
2498
2822
  "src/skills/detect.ts"() {
2499
2823
  "use strict";
2500
- import_fs14 = __toESM(require("fs"));
2824
+ import_fs15 = __toESM(require("fs"));
2501
2825
  import_os5 = __toESM(require("os"));
2502
2826
  import_path11 = __toESM(require("path"));
2503
2827
  init_utils();
@@ -2548,7 +2872,7 @@ function isSkillMarker(value) {
2548
2872
  }
2549
2873
  function readMarker(skillDir) {
2550
2874
  try {
2551
- const parsed = JSON.parse(import_fs15.default.readFileSync(import_path12.default.join(skillDir, MARKER_FILE), "utf-8"));
2875
+ const parsed = JSON.parse(import_fs16.default.readFileSync(import_path12.default.join(skillDir, MARKER_FILE), "utf-8"));
2552
2876
  return isSkillMarker(parsed) ? parsed : null;
2553
2877
  } catch {
2554
2878
  return null;
@@ -2557,62 +2881,62 @@ function readMarker(skillDir) {
2557
2881
  function readInstallState(skillsFolder, currentVersion) {
2558
2882
  const skillDir = import_path12.default.join(skillsFolder, SKILL_NAME);
2559
2883
  return describeInstallState({
2560
- folderExists: import_fs15.default.existsSync(skillDir),
2884
+ folderExists: import_fs16.default.existsSync(skillDir),
2561
2885
  marker: readMarker(skillDir),
2562
2886
  currentVersion
2563
2887
  });
2564
2888
  }
2565
2889
  function copyDirectory(from, to) {
2566
- import_fs15.default.mkdirSync(to, { recursive: true });
2567
- for (const entry of import_fs15.default.readdirSync(from, { withFileTypes: true })) {
2890
+ import_fs16.default.mkdirSync(to, { recursive: true });
2891
+ for (const entry of import_fs16.default.readdirSync(from, { withFileTypes: true })) {
2568
2892
  const source = import_path12.default.join(from, entry.name);
2569
2893
  const destination = import_path12.default.join(to, entry.name);
2570
2894
  if (entry.isDirectory()) copyDirectory(source, destination);
2571
- else import_fs15.default.copyFileSync(source, destination);
2895
+ else import_fs16.default.copyFileSync(source, destination);
2572
2896
  }
2573
2897
  }
2574
2898
  function swapInto(skillDir, preparedDir) {
2575
2899
  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);
2900
+ import_fs16.default.rmSync(previousDir, { recursive: true, force: true });
2901
+ const hadPrevious = import_fs16.default.existsSync(skillDir);
2902
+ if (hadPrevious) import_fs16.default.renameSync(skillDir, previousDir);
2579
2903
  try {
2580
- import_fs15.default.renameSync(preparedDir, skillDir);
2904
+ import_fs16.default.renameSync(preparedDir, skillDir);
2581
2905
  } catch (error) {
2582
- if (hadPrevious) import_fs15.default.renameSync(previousDir, skillDir);
2906
+ if (hadPrevious) import_fs16.default.renameSync(previousDir, skillDir);
2583
2907
  throw error;
2584
2908
  }
2585
- if (hadPrevious) import_fs15.default.rmSync(previousDir, { recursive: true, force: true });
2909
+ if (hadPrevious) import_fs16.default.rmSync(previousDir, { recursive: true, force: true });
2586
2910
  }
2587
2911
  function installSkill(options) {
2588
2912
  const state = readInstallState(options.skillsFolder, options.version);
2589
2913
  if (state.kind === "installed") return "up-to-date";
2590
2914
  if (state.kind === "not-ours" && !options.force) return "skipped-not-ours";
2591
- import_fs15.default.mkdirSync(options.skillsFolder, { recursive: true });
2915
+ import_fs16.default.mkdirSync(options.skillsFolder, { recursive: true });
2592
2916
  const preparedDir = import_path12.default.join(options.skillsFolder, `.${SKILL_NAME}.tmp-${process.pid}`);
2593
- import_fs15.default.rmSync(preparedDir, { recursive: true, force: true });
2917
+ import_fs16.default.rmSync(preparedDir, { recursive: true, force: true });
2594
2918
  try {
2595
2919
  copyDirectory(options.sourceDir, preparedDir);
2596
2920
  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));
2921
+ import_fs16.default.writeFileSync(import_path12.default.join(preparedDir, MARKER_FILE), JSON.stringify(marker, null, 2));
2598
2922
  swapInto(import_path12.default.join(options.skillsFolder, SKILL_NAME), preparedDir);
2599
2923
  } finally {
2600
- import_fs15.default.rmSync(preparedDir, { recursive: true, force: true });
2924
+ import_fs16.default.rmSync(preparedDir, { recursive: true, force: true });
2601
2925
  }
2602
2926
  return state.kind === "not-installed" ? "installed" : "updated";
2603
2927
  }
2604
2928
  function uninstallSkill(skillsFolder) {
2605
2929
  const skillDir = import_path12.default.join(skillsFolder, SKILL_NAME);
2606
- if (!import_fs15.default.existsSync(skillDir)) return "not-installed";
2930
+ if (!import_fs16.default.existsSync(skillDir)) return "not-installed";
2607
2931
  if (!readMarker(skillDir)) return "skipped-not-ours";
2608
- import_fs15.default.rmSync(skillDir, { recursive: true, force: true });
2932
+ import_fs16.default.rmSync(skillDir, { recursive: true, force: true });
2609
2933
  return "removed";
2610
2934
  }
2611
- var import_fs15, import_path12;
2935
+ var import_fs16, import_path12;
2612
2936
  var init_installer = __esm({
2613
2937
  "src/skills/installer.ts"() {
2614
2938
  "use strict";
2615
- import_fs15 = __toESM(require("fs"));
2939
+ import_fs16 = __toESM(require("fs"));
2616
2940
  import_path12 = __toESM(require("path"));
2617
2941
  init_agents();
2618
2942
  init_state();
@@ -2628,7 +2952,7 @@ function packagedSkillCandidates(fromDirectory) {
2628
2952
  }
2629
2953
  function resolvePackagedSkillDir(fromDirectory = __dirname) {
2630
2954
  const found = packagedSkillCandidates(fromDirectory).find(
2631
- (candidate) => import_fs16.default.existsSync(import_path13.default.join(candidate, SKILL_ENTRY_FILE))
2955
+ (candidate) => import_fs17.default.existsSync(import_path13.default.join(candidate, SKILL_ENTRY_FILE))
2632
2956
  );
2633
2957
  if (!found) {
2634
2958
  throw new Error(
@@ -2637,11 +2961,11 @@ function resolvePackagedSkillDir(fromDirectory = __dirname) {
2637
2961
  }
2638
2962
  return found;
2639
2963
  }
2640
- var import_fs16, import_path13, SKILL_ENTRY_FILE;
2964
+ var import_fs17, import_path13, SKILL_ENTRY_FILE;
2641
2965
  var init_package_path = __esm({
2642
2966
  "src/skills/package-path.ts"() {
2643
2967
  "use strict";
2644
- import_fs16 = __toESM(require("fs"));
2968
+ import_fs17 = __toESM(require("fs"));
2645
2969
  import_path13 = __toESM(require("path"));
2646
2970
  init_agents();
2647
2971
  SKILL_ENTRY_FILE = "SKILL.md";
@@ -2739,10 +3063,10 @@ function installIntoFolder(options) {
2739
3063
  const outcome = installSkill({
2740
3064
  sourceDir: options.sourceDir,
2741
3065
  skillsFolder: target.skillsFolder,
2742
- version: "2.0.0",
3066
+ version: "2.1.0",
2743
3067
  force: options.force
2744
3068
  });
2745
- return describeInstallOutcome({ ...target, outcome, version: "2.0.0" });
3069
+ return describeInstallOutcome({ ...target, outcome, version: "2.1.0" });
2746
3070
  } catch (error) {
2747
3071
  return describeInstallFailure({ ...target, error });
2748
3072
  }
@@ -2761,8 +3085,8 @@ function removeSkills(agents, options) {
2761
3085
  }
2762
3086
  function formatAgentSkillLine(agent, homeDir = import_os6.default.homedir()) {
2763
3087
  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");
3088
+ const installState = readInstallState(import_path14.default.join(homeDir, agent.skillsFolder), "2.1.0");
3089
+ const status = describeSkillStatus(installState, "2.1.0");
2766
3090
  return `${agent.name.padEnd(AGENT_NAME_COLUMN_WIDTH)} ${STATUS_COLOURS[status.level](status.text)} ${import_chalk2.default.dim(location)}`;
2767
3091
  }
2768
3092
  function formatUndetectedAgentLine(agent) {
@@ -2875,19 +3199,19 @@ function step(current, total, label) {
2875
3199
  function runDataDirectoriesStep() {
2876
3200
  step(1, TOTAL_STEPS, "Creating data directories");
2877
3201
  for (const dir of REQUIRED_DIRS) {
2878
- if (!import_fs17.default.existsSync(dir)) {
2879
- import_fs17.default.mkdirSync(dir, { recursive: true, mode: 448 });
3202
+ if (!import_fs18.default.existsSync(dir)) {
3203
+ import_fs18.default.mkdirSync(dir, { recursive: true, mode: 448 });
2880
3204
  log.success(`Created ${dir}`);
2881
3205
  } else {
2882
3206
  try {
2883
- import_fs17.default.chmodSync(dir, 448);
3207
+ import_fs18.default.chmodSync(dir, 448);
2884
3208
  } catch {
2885
3209
  }
2886
3210
  log.info(`${dir} already exists`);
2887
3211
  }
2888
3212
  }
2889
- if (!import_fs17.default.existsSync(PROFILES_DIR)) {
2890
- import_fs17.default.mkdirSync(PROFILES_DIR, { recursive: true, mode: 448 });
3213
+ if (!import_fs18.default.existsSync(PROFILES_DIR)) {
3214
+ import_fs18.default.mkdirSync(PROFILES_DIR, { recursive: true, mode: 448 });
2891
3215
  }
2892
3216
  }
2893
3217
  async function initDatabaseAndConfig() {
@@ -2924,18 +3248,18 @@ async function runShellHookStep(program) {
2924
3248
  return null;
2925
3249
  }
2926
3250
  const hookFile = import_path15.default.join(__dirname, "..", "shell", `hook.${shell.name}`);
2927
- if (!import_fs17.default.existsSync(hookFile)) {
3251
+ if (!import_fs18.default.existsSync(hookFile)) {
2928
3252
  log.warn(`Hook template not found at ${hookFile}`);
2929
3253
  return shell;
2930
3254
  }
2931
- const hookContent = import_fs17.default.readFileSync(hookFile, "utf-8");
3255
+ const hookContent = import_fs18.default.readFileSync(hookFile, "utf-8");
2932
3256
  const binaryPath = resolveWorkscribeBinary();
2933
3257
  const hookWithPath = binaryPath ? injectBinaryPath(hookContent, binaryPath, process.execPath) : hookContent;
2934
3258
  if (shell.name === "fish") {
2935
3259
  const confDir = import_path15.default.dirname(shell.rcFile);
2936
- if (!import_fs17.default.existsSync(confDir)) import_fs17.default.mkdirSync(confDir, { recursive: true });
3260
+ if (!import_fs18.default.existsSync(confDir)) import_fs18.default.mkdirSync(confDir, { recursive: true });
2937
3261
  }
2938
- if (!import_fs17.default.existsSync(shell.rcFile)) import_fs17.default.writeFileSync(shell.rcFile, "");
3262
+ if (!import_fs18.default.existsSync(shell.rcFile)) import_fs18.default.writeFileSync(shell.rcFile, "");
2939
3263
  if (!isHookInstalled(shell.rcFile)) {
2940
3264
  log.plain("");
2941
3265
  log.plain(import_chalk3.default.dim(" Workscribe adds a lightweight hook to your shell that captures each"));
@@ -2962,7 +3286,7 @@ async function runShellHookStep(program) {
2962
3286
  log.info(`Shell hook up to date in ${shell.rcFile}`);
2963
3287
  }
2964
3288
  if (isCompletionsInstalled(shell)) {
2965
- log.info("Shell completions already installed");
3289
+ await refreshCompletions(shell, program);
2966
3290
  } else {
2967
3291
  const { installComps } = await (0, import_prompts2.default)({
2968
3292
  type: "confirm",
@@ -2981,6 +3305,29 @@ async function runShellHookStep(program) {
2981
3305
  }
2982
3306
  return shell;
2983
3307
  }
3308
+ async function refreshCompletions(shell, program) {
3309
+ try {
3310
+ writeCompletionScript(shell, getPublicCommands(program));
3311
+ } catch (error) {
3312
+ log.warn(`Could not refresh shell completions: ${error.message}`);
3313
+ }
3314
+ if (!isCompletionsOutdated(shell)) {
3315
+ log.info("Shell completions already installed");
3316
+ return;
3317
+ }
3318
+ const { updateShellCompletions } = await (0, import_prompts2.default)({
3319
+ type: "confirm",
3320
+ name: "updateShellCompletions",
3321
+ message: `Shell completions in ${shell.rcFile} are outdated (they error in new terminals if Workscribe is removed). Update?`,
3322
+ initial: true
3323
+ });
3324
+ if (!updateShellCompletions) {
3325
+ log.info("Shell completions update skipped");
3326
+ return;
3327
+ }
3328
+ updateCompletions(shell);
3329
+ log.success(`Shell completions updated in ${shell.rcFile}`);
3330
+ }
2984
3331
  async function runAIToolsStep() {
2985
3332
  step(4, TOTAL_STEPS, "Detecting AI coding tools");
2986
3333
  const installedTools = loadAIToolRegistry().filter(
@@ -2994,6 +3341,19 @@ async function runAIToolsStep() {
2994
3341
  const integration = AI_HOOK_INTEGRATIONS[tool.hook];
2995
3342
  if (integration.isHookInstalled()) {
2996
3343
  log.info(`${integration.displayName} session hook already installed`);
3344
+ } else if (integration.isHookOutdated?.()) {
3345
+ const { updateAiHook } = await (0, import_prompts2.default)({
3346
+ type: "confirm",
3347
+ name: "updateAiHook",
3348
+ message: `${integration.displayName} session hook is outdated. Update?`,
3349
+ initial: true
3350
+ });
3351
+ if (updateAiHook) {
3352
+ integration.install();
3353
+ log.success(`${integration.displayName} session hook updated`);
3354
+ } else {
3355
+ log.info(`${integration.displayName} hook update skipped \u2014 re-run workscribe init to update later`);
3356
+ }
2997
3357
  } else {
2998
3358
  const { install } = await (0, import_prompts2.default)({
2999
3359
  type: "confirm",
@@ -3012,10 +3372,10 @@ async function runAIToolsStep() {
3012
3372
  }
3013
3373
  async function offerSkillInstall(group, sourceDir) {
3014
3374
  const target = describeTarget(group);
3015
- const state = readInstallState(target.skillsFolder, "2.0.0");
3375
+ const state = readInstallState(target.skillsFolder, "2.1.0");
3016
3376
  const knownOutcome = outcomeWithoutPrompt(state);
3017
3377
  if (knownOutcome) {
3018
- printMessage(describeInstallOutcome({ ...target, outcome: knownOutcome, version: "2.0.0" }));
3378
+ printMessage(describeInstallOutcome({ ...target, outcome: knownOutcome, version: "2.1.0" }));
3019
3379
  return;
3020
3380
  }
3021
3381
  const { installSkill: shouldInstall } = await (0, import_prompts2.default)({
@@ -3112,17 +3472,17 @@ function registerInit(program) {
3112
3472
  await runAIToolsStep();
3113
3473
  await runAgentSkillStep();
3114
3474
  await runProfileDetectionStep(config);
3115
- if (!import_fs17.default.existsSync(CONFIG_PATH)) {
3475
+ if (!import_fs18.default.existsSync(CONFIG_PATH)) {
3116
3476
  writeConfig(config);
3117
3477
  }
3118
3478
  printNextSteps(shell);
3119
3479
  });
3120
3480
  }
3121
- var import_fs17, import_path15, import_os7, import_prompts2, import_chalk3, TOTAL_STEPS;
3481
+ var import_fs18, import_path15, import_os7, import_prompts2, import_chalk3, TOTAL_STEPS;
3122
3482
  var init_init = __esm({
3123
3483
  "src/cli/init.ts"() {
3124
3484
  "use strict";
3125
- import_fs17 = __toESM(require("fs"));
3485
+ import_fs18 = __toESM(require("fs"));
3126
3486
  import_path15 = __toESM(require("path"));
3127
3487
  import_os7 = __toESM(require("os"));
3128
3488
  import_prompts2 = __toESM(require("prompts"));
@@ -3175,6 +3535,12 @@ function localDateStr(d) {
3175
3535
  function localDateOf(isoString) {
3176
3536
  return localDateStr(new Date(isoString));
3177
3537
  }
3538
+ function localDayBounds(date) {
3539
+ const start = /* @__PURE__ */ new Date(`${date}T00:00:00`);
3540
+ const end = new Date(start);
3541
+ end.setDate(end.getDate() + 1);
3542
+ return { start: start.toISOString(), end: end.toISOString() };
3543
+ }
3178
3544
  function startOfWeek(dateStr) {
3179
3545
  const d = /* @__PURE__ */ new Date(dateStr + "T00:00:00");
3180
3546
  const day = d.getDay();
@@ -3234,7 +3600,7 @@ function registerStatus(program) {
3234
3600
  const total = getEventCount();
3235
3601
  const todayEvents = getEventsForDate(todayDate());
3236
3602
  const last = getLastEvent();
3237
- const isPaused = import_fs18.default.existsSync(PAUSE_FLAG_PATH);
3603
+ const isPaused = import_fs19.default.existsSync(PAUSE_FLAG_PATH);
3238
3604
  log.plain("");
3239
3605
  log.plain(import_chalk4.default.bold(" Workscribe Status"));
3240
3606
  log.plain("");
@@ -3280,6 +3646,18 @@ function registerStatus(program) {
3280
3646
  );
3281
3647
  }
3282
3648
  log.plain(import_chalk4.default.dim(" Run workscribe init to repair it"));
3649
+ } else if (hookHealth.installed && hookHealth.nodeMissing) {
3650
+ log.plain("");
3651
+ log.plain(
3652
+ ` ${import_chalk4.default.yellow("\u26A0")} The Node.js version the Claude Code hook uses is gone \u2014 Claude Code activity is not being captured.`
3653
+ );
3654
+ log.plain(import_chalk4.default.dim(" Run workscribe init to repair it"));
3655
+ } else if (hookHealth.installed && !hookHealth.guarded) {
3656
+ log.plain("");
3657
+ log.plain(
3658
+ ` ${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.`
3659
+ );
3660
+ log.plain(import_chalk4.default.dim(" Run workscribe init to update it"));
3283
3661
  }
3284
3662
  const { total: weekTotal, otherCount } = getRecentOtherRate(7);
3285
3663
  if (weekTotal > 0) {
@@ -3299,11 +3677,11 @@ function registerStatus(program) {
3299
3677
  log.plain("");
3300
3678
  });
3301
3679
  }
3302
- var import_fs18, import_chalk4, AGENT_SKILL_LABEL;
3680
+ var import_fs19, import_chalk4, AGENT_SKILL_LABEL;
3303
3681
  var init_status = __esm({
3304
3682
  "src/cli/status.ts"() {
3305
3683
  "use strict";
3306
- import_fs18 = __toESM(require("fs"));
3684
+ import_fs19 = __toESM(require("fs"));
3307
3685
  import_chalk4 = __toESM(require("chalk"));
3308
3686
  init_db();
3309
3687
  init_config();
@@ -3683,35 +4061,48 @@ var init_grouper = __esm({
3683
4061
  });
3684
4062
 
3685
4063
  // src/sessions/detector.ts
3686
- function getOrBuildSessions(date) {
3687
- const existing = getSessionsForDate(date);
3688
- const events = getEventsForDate(date);
3689
- if (events.length === 0) return [];
3690
- const cachedEventCount = existing.reduce((sum, sessionRow) => sum + sessionRow.event_count, 0);
3691
- if (existing.length > 0 && cachedEventCount === events.length) {
3692
- return existing.map((sessionRow) => ({
3693
- id: sessionRow.id,
3694
- date: sessionRow.date,
3695
- startedAt: sessionRow.started_at,
3696
- endedAt: sessionRow.ended_at,
3697
- repoName: sessionRow.repo_name,
3698
- durationMinutes: minutesBetween(sessionRow.started_at, sessionRow.ended_at),
3699
- eventCount: sessionRow.event_count,
3700
- summary: sessionRow.summary,
3701
- events: getEventsBySessionId(sessionRow.id)
3702
- }));
3703
- }
4064
+ function isDayGrouped(date) {
4065
+ const { start, end } = localDayBounds(date);
4066
+ return countEventsBetween(start, end) === sumSessionEventCounts(date);
4067
+ }
4068
+ function regroupDay(date) {
3704
4069
  const savedSummaries = /* @__PURE__ */ new Map();
3705
- for (const s of existing) {
3706
- if (s.summary) savedSummaries.set(`${s.repo_name}|${s.started_at}`, s.summary);
4070
+ for (const sessionRow of getSessionsForDate(date)) {
4071
+ if (sessionRow.summary) savedSummaries.set(`${sessionRow.repo_name}|${sessionRow.started_at}`, sessionRow.summary);
3707
4072
  }
3708
4073
  clearSessionsForDate(date);
3709
- return buildSessions(date, events, savedSummaries);
4074
+ buildSessions(date, getEventsForDate(date), savedSummaries);
4075
+ }
4076
+ function ensureDayGrouped(date, options = {}) {
4077
+ if (isDayGrouped(date)) return;
4078
+ runInImmediateTransaction(
4079
+ () => {
4080
+ if (isDayGrouped(date)) return;
4081
+ regroupDay(date);
4082
+ },
4083
+ { busyTimeoutMs: options.lockWaitMs }
4084
+ );
3710
4085
  }
3711
- function buildSessions(date, events, savedSummaries = /* @__PURE__ */ new Map()) {
4086
+ function toSession(sessionRow) {
4087
+ return {
4088
+ id: sessionRow.id,
4089
+ date: sessionRow.date,
4090
+ startedAt: sessionRow.started_at,
4091
+ endedAt: sessionRow.ended_at,
4092
+ repoName: sessionRow.repo_name,
4093
+ durationMinutes: minutesBetween(sessionRow.started_at, sessionRow.ended_at),
4094
+ eventCount: sessionRow.event_count,
4095
+ summary: sessionRow.summary,
4096
+ events: getEventsBySessionId(sessionRow.id)
4097
+ };
4098
+ }
4099
+ function getOrBuildSessions(date) {
4100
+ ensureDayGrouped(date);
4101
+ return getSessionsForDate(date).map(toSession);
4102
+ }
4103
+ function buildSessions(date, events, savedSummaries) {
3712
4104
  const config = readConfig();
3713
4105
  const idleMs = config.session.idleTimeout * 60 * 1e3;
3714
- const sessions = [];
3715
4106
  const timeGroups = splitByIdleGap(events, idleMs);
3716
4107
  for (const group of timeGroups) {
3717
4108
  if (group.length === 0) continue;
@@ -3733,26 +4124,15 @@ function buildSessions(date, events, savedSummaries = /* @__PURE__ */ new Map())
3733
4124
  for (const event of repoGroup) {
3734
4125
  insertSessionEvent(sessionId, event.id);
3735
4126
  }
3736
- sessions.push({
3737
- id: sessionId,
3738
- date,
3739
- startedAt: first.captured_at,
3740
- endedAt: last.captured_at,
3741
- repoName,
3742
- durationMinutes: minutesBetween(first.captured_at, last.captured_at),
3743
- eventCount: repoGroup.length,
3744
- summary,
3745
- events: repoGroup
3746
- });
3747
4127
  }
3748
4128
  }
3749
- return sessions;
3750
4129
  }
3751
4130
  var init_detector = __esm({
3752
4131
  "src/sessions/detector.ts"() {
3753
4132
  "use strict";
3754
4133
  init_db();
3755
4134
  init_config();
4135
+ init_format();
3756
4136
  init_grouper();
3757
4137
  }
3758
4138
  });
@@ -3798,13 +4178,13 @@ function isStdinInteractive() {
3798
4178
  return Boolean(process.stdin.isTTY);
3799
4179
  }
3800
4180
  function readAllStdin() {
3801
- return import_fs19.default.readFileSync(STDIN_FILE_DESCRIPTOR, "utf-8");
4181
+ return import_fs20.default.readFileSync(STDIN_FILE_DESCRIPTOR, "utf-8");
3802
4182
  }
3803
- var import_fs19, STDIN_FILE_DESCRIPTOR;
4183
+ var import_fs20, STDIN_FILE_DESCRIPTOR;
3804
4184
  var init_stdin = __esm({
3805
4185
  "src/utils/stdin.ts"() {
3806
4186
  "use strict";
3807
- import_fs19 = __toESM(require("fs"));
4187
+ import_fs20 = __toESM(require("fs"));
3808
4188
  STDIN_FILE_DESCRIPTOR = 0;
3809
4189
  }
3810
4190
  });
@@ -3867,18 +4247,18 @@ var init_session_summary = __esm({
3867
4247
  });
3868
4248
 
3869
4249
  // src/summary/git-commits.ts
3870
- function runGit(cwd, args) {
4250
+ function runGit(cwd, args, timeoutMs = GIT_TIMEOUT_MS) {
3871
4251
  try {
3872
- return (0, import_child_process6.execFileSync)("git", args, { cwd, timeout: GIT_TIMEOUT_MS, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
4252
+ return (0, import_child_process6.execFileSync)("git", args, { cwd, timeout: timeoutMs, stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
3873
4253
  } catch {
3874
4254
  return null;
3875
4255
  }
3876
4256
  }
3877
- function getRepoRoot(cwd) {
3878
- return runGit(cwd, ["rev-parse", "--show-toplevel"]) || null;
4257
+ function getRepoRoot(cwd, timeoutMs) {
4258
+ return runGit(cwd, ["rev-parse", "--show-toplevel"], timeoutMs) || null;
3879
4259
  }
3880
- function getUserEmail(repoRoot) {
3881
- return runGit(repoRoot, ["config", "user.email"]) || null;
4260
+ function getUserEmail(repoRoot, timeoutMs) {
4261
+ return runGit(repoRoot, ["config", "user.email"], timeoutMs) || null;
3882
4262
  }
3883
4263
  function getOthersSubjects(repoRoot, author) {
3884
4264
  if (!author) return [];
@@ -3896,15 +4276,20 @@ function getOthersSubjects(repoRoot, author) {
3896
4276
  }
3897
4277
  return [...theirs].filter((subject) => !mine.has(subject));
3898
4278
  }
3899
- function getCommitsInWindow({ repoRoot, since, until, author }) {
4279
+ function getCommitsInWindow({ repoRoot, since, until, author, filePath, timeoutMs }) {
3900
4280
  const shiftedBy = (iso, milliseconds) => new Date(new Date(iso).getTime() + milliseconds).toISOString();
3901
- const output = runGit(repoRoot, [
3902
- "log",
3903
- `--since=${shiftedBy(since, -WINDOW_LEAD_MS)}`,
3904
- `--until=${shiftedBy(until, WINDOW_TRAIL_MS)}`,
3905
- "--format=%h%x09%ce%x09%s",
3906
- "--reverse"
3907
- ]);
4281
+ const output = runGit(
4282
+ repoRoot,
4283
+ [
4284
+ "log",
4285
+ `--since=${shiftedBy(since, -WINDOW_LEAD_MS)}`,
4286
+ `--until=${shiftedBy(until, WINDOW_TRAIL_MS)}`,
4287
+ "--format=%h%x09%ce%x09%s",
4288
+ "--reverse",
4289
+ ...filePath ? ["--", filePath] : []
4290
+ ],
4291
+ timeoutMs
4292
+ );
3908
4293
  if (!output) return [];
3909
4294
  const wantedAuthor = author?.toLowerCase() ?? null;
3910
4295
  return output.split("\n").flatMap((line) => {
@@ -3915,11 +4300,38 @@ function getCommitsInWindow({ repoRoot, since, until, author }) {
3915
4300
  return [{ hash, subject }];
3916
4301
  });
3917
4302
  }
3918
- var import_child_process6, GIT_TIMEOUT_MS, WINDOW_LEAD_MS, WINDOW_TRAIL_MS, AUTHORSHIP_HISTORY_DEPTH;
4303
+ function getFileCommits(options) {
4304
+ const now = options.now ?? Date.now;
4305
+ const deadline = now() + options.budgetMs;
4306
+ const timeLeft = () => {
4307
+ const milliseconds = deadline - now();
4308
+ return milliseconds >= 1 ? milliseconds : null;
4309
+ };
4310
+ const rootTimeout = timeLeft();
4311
+ if (rootTimeout === null) return { repoRoot: null, commits: [] };
4312
+ const repoRoot = getRepoRoot(import_path17.default.dirname(options.filePath), rootTimeout);
4313
+ if (!repoRoot) return { repoRoot: null, commits: [] };
4314
+ const emailTimeout = timeLeft();
4315
+ if (emailTimeout === null) return { repoRoot, commits: [] };
4316
+ const author = getUserEmail(repoRoot, emailTimeout);
4317
+ const logTimeout = timeLeft();
4318
+ if (logTimeout === null) return { repoRoot, commits: [] };
4319
+ const commits = getCommitsInWindow({
4320
+ repoRoot,
4321
+ since: options.since,
4322
+ until: options.until,
4323
+ author,
4324
+ filePath: options.filePath,
4325
+ timeoutMs: logTimeout
4326
+ });
4327
+ return { repoRoot, commits };
4328
+ }
4329
+ var import_child_process6, import_path17, GIT_TIMEOUT_MS, WINDOW_LEAD_MS, WINDOW_TRAIL_MS, AUTHORSHIP_HISTORY_DEPTH;
3919
4330
  var init_git_commits = __esm({
3920
4331
  "src/summary/git-commits.ts"() {
3921
4332
  "use strict";
3922
4333
  import_child_process6 = require("child_process");
4334
+ import_path17 = __toESM(require("path"));
3923
4335
  GIT_TIMEOUT_MS = 3e3;
3924
4336
  WINDOW_LEAD_MS = 12e4;
3925
4337
  WINDOW_TRAIL_MS = 6e4;
@@ -4538,21 +4950,21 @@ function registerExport(program) {
4538
4950
  }
4539
4951
  const payload = buildDayPayload(date, sessions, locationContextFrom(config));
4540
4952
  const summary = await createSummaryWriter(config).summarize(payload);
4541
- 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}
4953
+ const outputPath = options.output ?? import_path18.default.join(EXPORTS_DIR, `workscribe-${date}.md`);
4954
+ if (!import_fs21.default.existsSync(EXPORTS_DIR)) import_fs21.default.mkdirSync(EXPORTS_DIR, { recursive: true, mode: 448 });
4955
+ import_fs21.default.writeFileSync(outputPath, `# Work Summary \u2014 ${date}
4544
4956
 
4545
4957
  ${summary}
4546
4958
  `, { encoding: "utf-8", mode: 384 });
4547
4959
  log.success(`Saved: ${outputPath}`);
4548
4960
  });
4549
4961
  }
4550
- var import_fs20, import_path17;
4962
+ var import_fs21, import_path18;
4551
4963
  var init_export = __esm({
4552
4964
  "src/cli/export.ts"() {
4553
4965
  "use strict";
4554
- import_fs20 = __toESM(require("fs"));
4555
- import_path17 = __toESM(require("path"));
4966
+ import_fs21 = __toESM(require("fs"));
4967
+ import_path18 = __toESM(require("path"));
4556
4968
  init_detector();
4557
4969
  init_config();
4558
4970
  init_logger();
@@ -4643,27 +5055,27 @@ var init_config2 = __esm({
4643
5055
  // src/cli/pause.ts
4644
5056
  function registerPause(program) {
4645
5057
  program.command("pause").description("Pause capture temporarily").action(() => {
4646
- if (import_fs21.default.existsSync(PAUSE_FLAG_PATH)) {
5058
+ if (import_fs22.default.existsSync(PAUSE_FLAG_PATH)) {
4647
5059
  log.info("Capture is already paused. Run workscribe resume to restart.");
4648
5060
  return;
4649
5061
  }
4650
- import_fs21.default.writeFileSync(PAUSE_FLAG_PATH, "", { mode: 384 });
5062
+ import_fs22.default.writeFileSync(PAUSE_FLAG_PATH, "", { mode: 384 });
4651
5063
  log.success("Capture paused. Run workscribe resume to restart.");
4652
5064
  });
4653
5065
  program.command("resume").description("Resume capture after a pause").action(() => {
4654
- if (!import_fs21.default.existsSync(PAUSE_FLAG_PATH)) {
5066
+ if (!import_fs22.default.existsSync(PAUSE_FLAG_PATH)) {
4655
5067
  log.info("Capture is not paused.");
4656
5068
  return;
4657
5069
  }
4658
- import_fs21.default.rmSync(PAUSE_FLAG_PATH);
5070
+ import_fs22.default.rmSync(PAUSE_FLAG_PATH);
4659
5071
  log.success("Capture resumed.");
4660
5072
  });
4661
5073
  }
4662
- var import_fs21;
5074
+ var import_fs22;
4663
5075
  var init_pause = __esm({
4664
5076
  "src/cli/pause.ts"() {
4665
5077
  "use strict";
4666
- import_fs21 = __toESM(require("fs"));
5078
+ import_fs22 = __toESM(require("fs"));
4667
5079
  init_paths();
4668
5080
  init_logger();
4669
5081
  }
@@ -4765,17 +5177,38 @@ var init_clear = __esm({
4765
5177
  });
4766
5178
 
4767
5179
  // src/cli/uninstall.ts
5180
+ function completionRcFiles() {
5181
+ return [".zshrc", ".bashrc", ".bash_profile"].map((file) => import_path19.default.join(import_os11.default.homedir(), file));
5182
+ }
5183
+ function removeCompletionsAndToolHooks() {
5184
+ for (const rcFile of completionRcFiles()) {
5185
+ if (removeCompletions(rcFile)) log.success(`Shell completions removed from ${rcFile}`);
5186
+ }
5187
+ if (removeFishCompletions()) log.success("Fish completions removed");
5188
+ const outcome = claudeCodeIntegration.uninstall?.();
5189
+ if (outcome === "removed") log.success("Claude Code session hook removed from ~/.claude/settings.json");
5190
+ if (outcome === "unreadable") {
5191
+ log.warn(
5192
+ "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"
5193
+ );
5194
+ }
5195
+ }
4768
5196
  function detectShellRcFiles() {
4769
5197
  const candidates = [
4770
- { name: "zsh", rcFile: import_path18.default.join(import_os11.default.homedir(), ".zshrc") },
4771
- { name: "bash", rcFile: import_path18.default.join(import_os11.default.homedir(), ".bashrc") },
4772
- { name: "bash", rcFile: import_path18.default.join(import_os11.default.homedir(), ".bash_profile") },
4773
- { name: "fish", rcFile: import_path18.default.join(import_os11.default.homedir(), ".config", "fish", "conf.d", "workscribe.fish") }
5198
+ { name: "zsh", rcFile: import_path19.default.join(import_os11.default.homedir(), ".zshrc") },
5199
+ { name: "bash", rcFile: import_path19.default.join(import_os11.default.homedir(), ".bashrc") },
5200
+ { name: "bash", rcFile: import_path19.default.join(import_os11.default.homedir(), ".bash_profile") },
5201
+ { name: "fish", rcFile: import_path19.default.join(import_os11.default.homedir(), ".config", "fish", "conf.d", "workscribe.fish") }
4774
5202
  ];
4775
- return candidates.filter((candidate) => import_fs22.default.existsSync(candidate.rcFile) && isHookInstalled(candidate.rcFile));
5203
+ return candidates.filter((candidate) => import_fs23.default.existsSync(candidate.rcFile) && isHookInstalled(candidate.rcFile));
4776
5204
  }
4777
5205
  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) => {
5206
+ program.command("uninstall").description(
5207
+ "Remove shell hooks, completions, the Claude Code hook and the agent skill; optionally delete all data"
5208
+ ).option(
5209
+ "--hooks-only",
5210
+ "remove shell hooks, completions and the Claude Code hook only \u2014 keep the agent skill and skip the data prompt"
5211
+ ).action(async (options) => {
4779
5212
  log.plain(import_chalk10.default.bold("\n Workscribe \u2014 Uninstall\n"));
4780
5213
  const hooked = detectShellRcFiles();
4781
5214
  if (hooked.length === 0) {
@@ -4783,7 +5216,7 @@ function registerUninstall(program) {
4783
5216
  } else {
4784
5217
  for (const { name, rcFile } of hooked) {
4785
5218
  if (name === "fish") {
4786
- import_fs22.default.unlinkSync(rcFile);
5219
+ import_fs23.default.unlinkSync(rcFile);
4787
5220
  log.success(`Shell hook removed: ${rcFile}`);
4788
5221
  log.plain(` ${import_chalk10.default.dim("Restart your terminal to take effect")}`);
4789
5222
  } else {
@@ -4796,9 +5229,10 @@ function registerUninstall(program) {
4796
5229
  }
4797
5230
  }
4798
5231
  }
4799
- if (opts.hooksOnly) return;
5232
+ removeCompletionsAndToolHooks();
5233
+ if (options.hooksOnly) return;
4800
5234
  removeSkills(SKILL_AGENTS, { reportMissing: false });
4801
- const existingDirs = [...new Set(REQUIRED_DIRS)].filter((dir) => import_fs22.default.existsSync(dir));
5235
+ const existingDirs = [...new Set(REQUIRED_DIRS)].filter((dir) => import_fs23.default.existsSync(dir));
4802
5236
  if (existingDirs.length > 0) {
4803
5237
  log.plain("");
4804
5238
  const { deleteData } = await (0, import_prompts4.default)({
@@ -4809,7 +5243,7 @@ function registerUninstall(program) {
4809
5243
  });
4810
5244
  if (deleteData) {
4811
5245
  for (const dir of existingDirs) {
4812
- import_fs22.default.rmSync(dir, { recursive: true, force: true });
5246
+ import_fs23.default.rmSync(dir, { recursive: true, force: true });
4813
5247
  log.success(`Deleted ${dir}`);
4814
5248
  }
4815
5249
  } else {
@@ -4821,17 +5255,18 @@ function registerUninstall(program) {
4821
5255
  log.plain("");
4822
5256
  });
4823
5257
  }
4824
- var import_fs22, import_path18, import_os11, import_prompts4, import_chalk10;
5258
+ var import_fs23, import_path19, import_os11, import_prompts4, import_chalk10;
4825
5259
  var init_uninstall = __esm({
4826
5260
  "src/cli/uninstall.ts"() {
4827
5261
  "use strict";
4828
- import_fs22 = __toESM(require("fs"));
4829
- import_path18 = __toESM(require("path"));
5262
+ import_fs23 = __toESM(require("fs"));
5263
+ import_path19 = __toESM(require("path"));
4830
5264
  import_os11 = __toESM(require("os"));
4831
5265
  import_prompts4 = __toESM(require("prompts"));
4832
5266
  import_chalk10 = __toESM(require("chalk"));
4833
5267
  init_paths();
4834
5268
  init_shell_hook();
5269
+ init_claude_code();
4835
5270
  init_logger();
4836
5271
  init_agents();
4837
5272
  init_actions();
@@ -4882,7 +5317,7 @@ function registerProfile(program) {
4882
5317
  log.plain(import_chalk11.default.dim(" " + "\u2500".repeat(60)));
4883
5318
  for (const entry of entries) {
4884
5319
  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") : "";
5320
+ const installed = import_fs24.default.existsSync(import_path20.default.join(PROFILES_DIR, `${entry.name}.json`)) ? import_chalk11.default.dim(" installed") : "";
4886
5321
  log.plain(` ${import_chalk11.default.cyan(entry.name.padEnd(18))} ${entry.displayName.padEnd(28)}${badge}${installed}`);
4887
5322
  if (entry.description) log.plain(` ${import_chalk11.default.dim(" ".repeat(18) + entry.description)}`);
4888
5323
  }
@@ -4918,8 +5353,8 @@ function registerProfile(program) {
4918
5353
  continue;
4919
5354
  }
4920
5355
  activeNames.delete(name);
4921
- const cachePath = import_path19.default.join(PROFILES_DIR, `${name}.json`);
4922
- if (import_fs23.default.existsSync(cachePath)) import_fs23.default.unlinkSync(cachePath);
5356
+ const cachePath = import_path20.default.join(PROFILES_DIR, `${name}.json`);
5357
+ if (import_fs24.default.existsSync(cachePath)) import_fs24.default.unlinkSync(cachePath);
4923
5358
  log.success(`Profile "${name}" removed`);
4924
5359
  }
4925
5360
  config.capture.profiles = [...activeNames];
@@ -5046,12 +5481,12 @@ function registerProfile(program) {
5046
5481
  }
5047
5482
  });
5048
5483
  }
5049
- var import_fs23, import_path19, import_chalk11, MIN_SUGGEST_COUNT;
5484
+ var import_fs24, import_path20, import_chalk11, MIN_SUGGEST_COUNT;
5050
5485
  var init_profile = __esm({
5051
5486
  "src/cli/profile.ts"() {
5052
5487
  "use strict";
5053
- import_fs23 = __toESM(require("fs"));
5054
- import_path19 = __toESM(require("path"));
5488
+ import_fs24 = __toESM(require("fs"));
5489
+ import_path20 = __toESM(require("path"));
5055
5490
  import_chalk11 = __toESM(require("chalk"));
5056
5491
  init_config();
5057
5492
  init_paths();
@@ -5063,14 +5498,45 @@ var init_profile = __esm({
5063
5498
  }
5064
5499
  });
5065
5500
 
5501
+ // src/utils/working-directory.ts
5502
+ function realPath(target) {
5503
+ try {
5504
+ return import_fs25.default.realpathSync(target);
5505
+ } catch {
5506
+ return null;
5507
+ }
5508
+ }
5509
+ function resolvedPath(target) {
5510
+ return realPath(target) ?? target;
5511
+ }
5512
+ function currentDirectory(where = { pwd: process.env.PWD, cwd: process.cwd() }) {
5513
+ if (where.pwd && realPath(where.pwd) === realPath(where.cwd)) return where.pwd;
5514
+ return where.cwd;
5515
+ }
5516
+ function rootForms(cwd, root) {
5517
+ const resolvedCwd = realPath(cwd);
5518
+ if (!resolvedCwd) return [root];
5519
+ const reached = import_path21.default.resolve(cwd, import_path21.default.relative(resolvedCwd, root));
5520
+ return [.../* @__PURE__ */ new Set([root, reached])];
5521
+ }
5522
+ var import_fs25, import_path21;
5523
+ var init_working_directory = __esm({
5524
+ "src/utils/working-directory.ts"() {
5525
+ "use strict";
5526
+ import_fs25 = __toESM(require("fs"));
5527
+ import_path21 = __toESM(require("path"));
5528
+ }
5529
+ });
5530
+
5066
5531
  // src/cli/note.ts
5067
5532
  function registerNote(program) {
5068
5533
  program.command("note <text>").description("Attach a free-text annotation to the current session").action((text) => {
5069
- const { repoName, branch } = getGitContext(process.cwd());
5534
+ const directory = currentDirectory();
5535
+ const { repoName, branch } = getRepoContext(directory, readConfig());
5070
5536
  insertEvent({
5071
5537
  captured_at: (/* @__PURE__ */ new Date()).toISOString(),
5072
5538
  command: "workscribe note",
5073
- cwd: process.cwd(),
5539
+ cwd: directory,
5074
5540
  exit_code: null,
5075
5541
  duration_ms: null,
5076
5542
  repo_name: repoName,
@@ -5088,6 +5554,8 @@ var init_note = __esm({
5088
5554
  "use strict";
5089
5555
  init_db();
5090
5556
  init_git();
5557
+ init_config();
5558
+ init_working_directory();
5091
5559
  init_logger();
5092
5560
  }
5093
5561
  });
@@ -5118,8 +5586,8 @@ var init_week = __esm({
5118
5586
 
5119
5587
  // src/cli/bug-report.ts
5120
5588
  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");
5589
+ if (!import_fs26.default.existsSync(filePath)) return "(no debug log found \u2014 set WORKSCRIBE_DEBUG=1 to enable)";
5590
+ const content = import_fs26.default.readFileSync(filePath, "utf-8");
5123
5591
  const allLines = content.split("\n");
5124
5592
  return allLines.slice(-lines).join("\n");
5125
5593
  }
@@ -5137,9 +5605,9 @@ function redactConfig() {
5137
5605
  function registerBugReport(program) {
5138
5606
  program.command("bug-report").description("Generate a diagnostic report for filing a bug").action(() => {
5139
5607
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
5140
- const reportPath = import_path20.default.join(WORKSCRIBE_DIR, `bug-report-${timestamp}.txt`);
5608
+ const reportPath = import_path22.default.join(WORKSCRIBE_DIR, `bug-report-${timestamp}.txt`);
5141
5609
  const systemInfo = [
5142
- `Workscribe version : ${"2.0.0"}`,
5610
+ `Workscribe version : ${"2.1.0"}`,
5143
5611
  `Node.js version : ${process.version}`,
5144
5612
  `Platform : ${process.platform}`,
5145
5613
  `Architecture : ${process.arch}`,
@@ -5161,17 +5629,17 @@ function registerBugReport(program) {
5161
5629
  tailFile(DEBUG_LOG_PATH, DEBUG_LOG_TAIL_LINES)
5162
5630
  ].join("\n");
5163
5631
  try {
5164
- if (!import_fs24.default.existsSync(WORKSCRIBE_DIR)) {
5165
- import_fs24.default.mkdirSync(WORKSCRIBE_DIR, { recursive: true, mode: 448 });
5632
+ if (!import_fs26.default.existsSync(WORKSCRIBE_DIR)) {
5633
+ import_fs26.default.mkdirSync(WORKSCRIBE_DIR, { recursive: true, mode: 448 });
5166
5634
  }
5167
- import_fs24.default.writeFileSync(reportPath, report, { mode: 384 });
5635
+ import_fs26.default.writeFileSync(reportPath, report, { mode: 384 });
5168
5636
  } catch (error) {
5169
5637
  log.error(`Could not write report: ${error.message}`);
5170
5638
  process.exit(1);
5171
5639
  }
5172
5640
  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 -->
5641
+ title: `Bug report \u2014 workscribe ${"2.1.0"}`,
5642
+ body: `<!-- Attach ${import_path22.default.basename(reportPath)} or paste the relevant sections below -->
5175
5643
 
5176
5644
  **Describe the issue:**
5177
5645
 
@@ -5188,13 +5656,13 @@ function registerBugReport(program) {
5188
5656
  log.plain("");
5189
5657
  });
5190
5658
  }
5191
- var import_fs24, import_os12, import_path20, import_chalk12, GITHUB_ISSUES_URL, DEBUG_LOG_TAIL_LINES;
5659
+ var import_fs26, import_os12, import_path22, import_chalk12, GITHUB_ISSUES_URL, DEBUG_LOG_TAIL_LINES;
5192
5660
  var init_bug_report = __esm({
5193
5661
  "src/cli/bug-report.ts"() {
5194
5662
  "use strict";
5195
- import_fs24 = __toESM(require("fs"));
5663
+ import_fs26 = __toESM(require("fs"));
5196
5664
  import_os12 = __toESM(require("os"));
5197
- import_path20 = __toESM(require("path"));
5665
+ import_path22 = __toESM(require("path"));
5198
5666
  import_chalk12 = __toESM(require("chalk"));
5199
5667
  init_paths();
5200
5668
  init_config();
@@ -5261,11 +5729,108 @@ var init_alerts3 = __esm({
5261
5729
  }
5262
5730
  });
5263
5731
 
5732
+ // src/summary/file-details.ts
5733
+ function displayPath2(absoluteFilePath, repoRoot) {
5734
+ if (repoRoot) {
5735
+ const relative = import_path23.default.relative(repoRoot, absoluteFilePath);
5736
+ if (relative && !relative.startsWith("..") && !import_path23.default.isAbsolute(relative)) return relative;
5737
+ }
5738
+ return import_path23.default.basename(absoluteFilePath);
5739
+ }
5740
+ function otherFilesBySaves(sessionEvents, filePath) {
5741
+ const saves = /* @__PURE__ */ new Map();
5742
+ for (const event of sessionEvents) {
5743
+ if (event.category !== CODE_EDIT || !event.file_path || event.file_path === filePath) continue;
5744
+ const seen = saves.get(event.file_path);
5745
+ saves.set(event.file_path, {
5746
+ count: (seen?.count ?? 0) + 1,
5747
+ lastSavedAt: seen && seen.lastSavedAt > event.captured_at ? seen.lastSavedAt : event.captured_at
5748
+ });
5749
+ }
5750
+ return [...saves.entries()].sort(([, first], [, second]) => second.count - first.count || second.lastSavedAt.localeCompare(first.lastSavedAt)).map(([otherPath]) => otherPath);
5751
+ }
5752
+ function savesOf(sessionEvents, filePath) {
5753
+ return sessionEvents.filter((event) => event.category === CODE_EDIT && event.file_path === filePath);
5754
+ }
5755
+ function buildFileDetails(options) {
5756
+ const { filePath, sessionEvents, displayPathOf } = options;
5757
+ const saves = savesOf(sessionEvents, filePath);
5758
+ if (saves.length === 0) return null;
5759
+ const savedTimes = saves.map((save) => save.captured_at).sort();
5760
+ const latestBranch = [...saves].reverse().find((save) => save.branch)?.branch ?? null;
5761
+ const otherFiles = otherFilesBySaves(sessionEvents, filePath);
5762
+ return {
5763
+ path: filePath,
5764
+ relativePath: displayPathOf(filePath),
5765
+ firstSavedAt: savedTimes[0],
5766
+ lastSavedAt: savedTimes.at(-1) ?? savedTimes[0],
5767
+ saveCount: saves.length,
5768
+ branch: latestBranch ?? options.sessionBranch,
5769
+ commits: options.commits,
5770
+ editedAlongside: {
5771
+ files: otherFiles.slice(0, EDITED_ALONGSIDE_LIMIT).map(displayPathOf),
5772
+ total: otherFiles.length
5773
+ }
5774
+ };
5775
+ }
5776
+ var import_path23, EDITED_ALONGSIDE_LIMIT, CODE_EDIT;
5777
+ var init_file_details = __esm({
5778
+ "src/summary/file-details.ts"() {
5779
+ "use strict";
5780
+ import_path23 = __toESM(require("path"));
5781
+ EDITED_ALONGSIDE_LIMIT = 5;
5782
+ CODE_EDIT = "code_edit";
5783
+ }
5784
+ });
5785
+
5786
+ // src/cli/context-file.ts
5787
+ function groupDayOfLatestSave(filePath) {
5788
+ const latestSave = getLatestFileSaveAt(filePath);
5789
+ if (!latestSave) return;
5790
+ try {
5791
+ ensureDayGrouped(localDateOf(latestSave), { lockWaitMs: LOCK_WAIT_MS });
5792
+ } catch (error) {
5793
+ if (!isDatabaseLocked(error)) throw error;
5794
+ }
5795
+ }
5796
+ function getFileDetails(filePath, session) {
5797
+ const sessionEvents = getEventsBySessionId(session.id);
5798
+ if (savesOf(sessionEvents, filePath).length === 0) return null;
5799
+ const { repoRoot, commits } = getFileCommits({
5800
+ filePath: resolvedPath(filePath),
5801
+ since: session.started_at,
5802
+ until: session.ended_at,
5803
+ budgetMs: GIT_BUDGET_MS
5804
+ });
5805
+ return buildFileDetails({
5806
+ filePath,
5807
+ sessionEvents,
5808
+ sessionBranch: getSessionBranch(session.id),
5809
+ commits,
5810
+ displayPathOf: (storedPath) => displayPath2(resolvedPath(storedPath), repoRoot)
5811
+ });
5812
+ }
5813
+ var LOCK_WAIT_MS, GIT_BUDGET_MS;
5814
+ var init_context_file = __esm({
5815
+ "src/cli/context-file.ts"() {
5816
+ "use strict";
5817
+ init_db();
5818
+ init_transaction();
5819
+ init_detector();
5820
+ init_file_details();
5821
+ init_git_commits();
5822
+ init_format();
5823
+ init_working_directory();
5824
+ LOCK_WAIT_MS = 1e3;
5825
+ GIT_BUDGET_MS = 2e3;
5826
+ }
5827
+ });
5828
+
5264
5829
  // src/cli/context.ts
5265
5830
  function sessionMinutes(session) {
5266
5831
  return (new Date(session.ended_at).getTime() - new Date(session.started_at).getTime()) / 6e4;
5267
5832
  }
5268
- function toSession(row) {
5833
+ function toSession2(row) {
5269
5834
  return {
5270
5835
  id: row.id,
5271
5836
  date: row.date,
@@ -5280,7 +5845,7 @@ function toSession(row) {
5280
5845
  }
5281
5846
  function renderTemplateSummary(row) {
5282
5847
  const config = readConfig();
5283
- const payload = sessionToPayload(toSession(row), row.date, locationContextFrom(config));
5848
+ const payload = sessionToPayload(toSession2(row), row.date, locationContextFrom(config));
5284
5849
  const bullets = describeSession(payload, config.summary?.includeCategories ?? []);
5285
5850
  return `${formatSessionHeading(payload)}
5286
5851
  ${toBulletList(bullets)}`;
@@ -5290,6 +5855,7 @@ function resolveContextSummary(row) {
5290
5855
  }
5291
5856
  function toContextJson(session) {
5292
5857
  return {
5858
+ repoName: session.repo_name,
5293
5859
  date: session.date,
5294
5860
  startedAt: session.started_at,
5295
5861
  endedAt: session.ended_at,
@@ -5300,6 +5866,9 @@ function toContextJson(session) {
5300
5866
  summary: resolveContextSummary(session)
5301
5867
  };
5302
5868
  }
5869
+ function toFileContextJson(session, filePath) {
5870
+ return { ...toContextJson(session), file: getFileDetails(filePath, session) };
5871
+ }
5303
5872
  function printSessionContext(session, heading) {
5304
5873
  const ago = timeAgo(session.started_at);
5305
5874
  const branch = getSessionBranch(session.id);
@@ -5320,7 +5889,8 @@ function printSessionContext(session, heading) {
5320
5889
  function registerContext(program) {
5321
5890
  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
5891
  if (opts.file) {
5323
- const normalizedFile = import_path21.default.normalize(opts.file);
5892
+ const normalizedFile = import_path24.default.normalize(opts.file);
5893
+ groupDayOfLatestSave(normalizedFile);
5324
5894
  const session = getLastSessionTouchingFile(normalizedFile);
5325
5895
  if (!session) {
5326
5896
  if (opts.json) log.plain(JSON.stringify(null));
@@ -5328,19 +5898,23 @@ function registerContext(program) {
5328
5898
  return;
5329
5899
  }
5330
5900
  if (opts.json) {
5331
- log.plain(JSON.stringify(toContextJson(session)));
5901
+ log.plain(JSON.stringify(toFileContextJson(session, normalizedFile)));
5332
5902
  return;
5333
5903
  }
5334
- printSessionContext(session, `Last session touching ${import_path21.default.basename(normalizedFile)}`);
5904
+ printSessionContext(session, `Last session touching ${import_path24.default.basename(normalizedFile)}`);
5335
5905
  return;
5336
5906
  }
5337
- const { repoName } = getGitContext(process.cwd(), null, readConfig().git.qualifyRepoName);
5907
+ const config = readConfig();
5908
+ const directory = currentDirectory();
5909
+ const { repoName, root, remote } = getRepoContext(directory, config);
5338
5910
  if (!repoName) {
5339
5911
  if (opts.json) log.plain(JSON.stringify(null));
5340
5912
  else log.plain(" Not in a git repository. Run workscribe context from a project directory.");
5341
5913
  return;
5342
5914
  }
5343
- const sessions = getRecentSessionsByRepo(repoName, 1);
5915
+ const otherRepoName = remote ? formatRepoName(remote, !config.git.qualifyRepoName) : null;
5916
+ const roots = root ? rootForms(directory, root) : [];
5917
+ const sessions = getRecentSessionsForRepo({ repoName, otherRepoName, roots }, 1);
5344
5918
  if (sessions.length === 0) {
5345
5919
  if (opts.json) log.plain(JSON.stringify(null));
5346
5920
  else log.plain(` No sessions found for ${repoName}`);
@@ -5353,17 +5927,20 @@ function registerContext(program) {
5353
5927
  printSessionContext(sessions[0], `Last session on ${repoName}`);
5354
5928
  });
5355
5929
  }
5356
- var import_path21, import_chalk14;
5930
+ var import_path24, import_chalk14;
5357
5931
  var init_context = __esm({
5358
5932
  "src/cli/context.ts"() {
5359
5933
  "use strict";
5360
- import_path21 = __toESM(require("path"));
5934
+ import_path24 = __toESM(require("path"));
5361
5935
  import_chalk14 = __toESM(require("chalk"));
5362
5936
  init_db();
5363
5937
  init_git();
5938
+ init_repo_name();
5939
+ init_working_directory();
5364
5940
  init_config();
5365
5941
  init_format();
5366
5942
  init_logger();
5943
+ init_context_file();
5367
5944
  init_describe();
5368
5945
  init_utils3();
5369
5946
  init_payload();
@@ -5448,7 +6025,7 @@ var init_skill = __esm({
5448
6025
  // src/utils/update-check.ts
5449
6026
  function readCache() {
5450
6027
  try {
5451
- return JSON.parse(import_fs25.default.readFileSync(UPDATE_CHECK_PATH, "utf-8"));
6028
+ return JSON.parse(import_fs27.default.readFileSync(UPDATE_CHECK_PATH, "utf-8"));
5452
6029
  } catch {
5453
6030
  return null;
5454
6031
  }
@@ -5456,7 +6033,7 @@ function readCache() {
5456
6033
  function writeCache(updates) {
5457
6034
  try {
5458
6035
  const current = readCache() ?? {};
5459
- import_fs25.default.writeFileSync(UPDATE_CHECK_PATH, JSON.stringify({ ...current, ...updates }), { mode: 384 });
6036
+ import_fs27.default.writeFileSync(UPDATE_CHECK_PATH, JSON.stringify({ ...current, ...updates }), { mode: 384 });
5460
6037
  } catch {
5461
6038
  }
5462
6039
  }
@@ -5469,7 +6046,7 @@ function isNewer(latest, current) {
5469
6046
  return latestPatch > currentPatch;
5470
6047
  }
5471
6048
  function checkForUpdate() {
5472
- const current = "2.0.0";
6049
+ const current = "2.1.0";
5473
6050
  const cache = readCache();
5474
6051
  if (cache && isNewer(cache.latestVersion, current)) {
5475
6052
  process.stderr.write(
@@ -5492,7 +6069,7 @@ function checkForUpdate() {
5492
6069
  function buildTelemetryPayload(config) {
5493
6070
  return {
5494
6071
  installId: config.telemetry.installId,
5495
- cliVersion: "2.0.0",
6072
+ cliVersion: "2.1.0",
5496
6073
  platform: import_os13.default.platform(),
5497
6074
  nodeVersion: process.version
5498
6075
  };
@@ -5517,11 +6094,11 @@ function pingTelemetry() {
5517
6094
  debugLog("pingTelemetry", error);
5518
6095
  });
5519
6096
  }
5520
- var import_fs25, import_os13, CHECK_INTERVAL_MS;
6097
+ var import_fs27, import_os13, CHECK_INTERVAL_MS;
5521
6098
  var init_update_check = __esm({
5522
6099
  "src/utils/update-check.ts"() {
5523
6100
  "use strict";
5524
- import_fs25 = __toESM(require("fs"));
6101
+ import_fs27 = __toESM(require("fs"));
5525
6102
  import_os13 = __toESM(require("os"));
5526
6103
  init_paths();
5527
6104
  init_config();
@@ -5568,7 +6145,7 @@ var require_main = __commonJS({
5568
6145
  pingTelemetry();
5569
6146
  }
5570
6147
  var program = new import_commander.Command();
5571
- program.name("workscribe").description("Automatic developer work journal").version("2.0.0");
6148
+ program.name("workscribe").description("Automatic developer work journal").version("2.1.0");
5572
6149
  registerCapture(program);
5573
6150
  registerHook(program);
5574
6151
  registerInit(program);