@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 +837 -260
- package/dist/index.js.map +1 -1
- package/package.json +1 -2
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(
|
|
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
|
|
1408
|
-
|
|
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/
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 (!
|
|
2111
|
-
const content =
|
|
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
|
-
|
|
2127
|
-
|
|
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
|
-
|
|
2135
|
-
|
|
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
|
-
|
|
2340
|
+
import_fs13.default.writeFileSync(rcFile, content.slice(0, startIdx) + hookContent + content.slice(endIdx));
|
|
2141
2341
|
}
|
|
2142
2342
|
function removeHook(rcFile) {
|
|
2143
|
-
|
|
2144
|
-
|
|
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
|
-
|
|
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
|
|
2368
|
+
return import_fs13.default.existsSync(import_path9.default.join(FISH_COMPLETIONS_DIR, "workscribe.fish"));
|
|
2171
2369
|
}
|
|
2172
2370
|
try {
|
|
2173
|
-
return
|
|
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
|
|
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
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
2502
|
+
if (!import_fs14.default.existsSync(CLAUDE_SETTINGS_PATH)) return {};
|
|
2240
2503
|
try {
|
|
2241
|
-
return JSON.parse(
|
|
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
|
|
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
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
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
|
|
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
|
-
|
|
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 *
|
|
2314
|
-
//
|
|
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
|
-
|
|
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
|
|
2328
|
-
const
|
|
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:
|
|
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: `${
|
|
2649
|
+
command: command(`${HOOK_MARKER} --event UserPromptSubmit`)
|
|
2345
2650
|
}
|
|
2346
2651
|
]
|
|
2347
2652
|
}
|
|
2348
2653
|
]
|
|
2349
2654
|
};
|
|
2350
|
-
|
|
2351
|
-
|
|
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) =>
|
|
2796
|
+
pathExists: (absolutePath) => import_fs15.default.existsSync(absolutePath),
|
|
2473
2797
|
listDirectory: (absolutePath) => {
|
|
2474
2798
|
try {
|
|
2475
|
-
return
|
|
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
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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
|
-
|
|
2567
|
-
for (const entry of
|
|
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
|
|
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
|
-
|
|
2577
|
-
const hadPrevious =
|
|
2578
|
-
if (hadPrevious)
|
|
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
|
-
|
|
2904
|
+
import_fs16.default.renameSync(preparedDir, skillDir);
|
|
2581
2905
|
} catch (error) {
|
|
2582
|
-
if (hadPrevious)
|
|
2906
|
+
if (hadPrevious) import_fs16.default.renameSync(previousDir, skillDir);
|
|
2583
2907
|
throw error;
|
|
2584
2908
|
}
|
|
2585
|
-
if (hadPrevious)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
2930
|
+
if (!import_fs16.default.existsSync(skillDir)) return "not-installed";
|
|
2607
2931
|
if (!readMarker(skillDir)) return "skipped-not-ours";
|
|
2608
|
-
|
|
2932
|
+
import_fs16.default.rmSync(skillDir, { recursive: true, force: true });
|
|
2609
2933
|
return "removed";
|
|
2610
2934
|
}
|
|
2611
|
-
var
|
|
2935
|
+
var import_fs16, import_path12;
|
|
2612
2936
|
var init_installer = __esm({
|
|
2613
2937
|
"src/skills/installer.ts"() {
|
|
2614
2938
|
"use strict";
|
|
2615
|
-
|
|
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) =>
|
|
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
|
|
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
|
-
|
|
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.
|
|
3066
|
+
version: "2.1.0",
|
|
2743
3067
|
force: options.force
|
|
2744
3068
|
});
|
|
2745
|
-
return describeInstallOutcome({ ...target, outcome, version: "2.
|
|
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.
|
|
2765
|
-
const status = describeSkillStatus(installState, "2.
|
|
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 (!
|
|
2879
|
-
|
|
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
|
-
|
|
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 (!
|
|
2890
|
-
|
|
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 (!
|
|
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 =
|
|
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 (!
|
|
3260
|
+
if (!import_fs18.default.existsSync(confDir)) import_fs18.default.mkdirSync(confDir, { recursive: true });
|
|
2937
3261
|
}
|
|
2938
|
-
if (!
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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 (!
|
|
3475
|
+
if (!import_fs18.default.existsSync(CONFIG_PATH)) {
|
|
3116
3476
|
writeConfig(config);
|
|
3117
3477
|
}
|
|
3118
3478
|
printNextSteps(shell);
|
|
3119
3479
|
});
|
|
3120
3480
|
}
|
|
3121
|
-
var
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
3687
|
-
const
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
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
|
|
3706
|
-
if (
|
|
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
|
-
|
|
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
|
|
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
|
|
4181
|
+
return import_fs20.default.readFileSync(STDIN_FILE_DESCRIPTOR, "utf-8");
|
|
3802
4182
|
}
|
|
3803
|
-
var
|
|
4183
|
+
var import_fs20, STDIN_FILE_DESCRIPTOR;
|
|
3804
4184
|
var init_stdin = __esm({
|
|
3805
4185
|
"src/utils/stdin.ts"() {
|
|
3806
4186
|
"use strict";
|
|
3807
|
-
|
|
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:
|
|
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(
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
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
|
-
|
|
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 ??
|
|
4542
|
-
if (!
|
|
4543
|
-
|
|
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
|
|
4962
|
+
var import_fs21, import_path18;
|
|
4551
4963
|
var init_export = __esm({
|
|
4552
4964
|
"src/cli/export.ts"() {
|
|
4553
4965
|
"use strict";
|
|
4554
|
-
|
|
4555
|
-
|
|
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 (
|
|
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
|
-
|
|
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 (!
|
|
5066
|
+
if (!import_fs22.default.existsSync(PAUSE_FLAG_PATH)) {
|
|
4655
5067
|
log.info("Capture is not paused.");
|
|
4656
5068
|
return;
|
|
4657
5069
|
}
|
|
4658
|
-
|
|
5070
|
+
import_fs22.default.rmSync(PAUSE_FLAG_PATH);
|
|
4659
5071
|
log.success("Capture resumed.");
|
|
4660
5072
|
});
|
|
4661
5073
|
}
|
|
4662
|
-
var
|
|
5074
|
+
var import_fs22;
|
|
4663
5075
|
var init_pause = __esm({
|
|
4664
5076
|
"src/cli/pause.ts"() {
|
|
4665
5077
|
"use strict";
|
|
4666
|
-
|
|
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:
|
|
4771
|
-
{ name: "bash", rcFile:
|
|
4772
|
-
{ name: "bash", rcFile:
|
|
4773
|
-
{ name: "fish", rcFile:
|
|
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) =>
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
5232
|
+
removeCompletionsAndToolHooks();
|
|
5233
|
+
if (options.hooksOnly) return;
|
|
4800
5234
|
removeSkills(SKILL_AGENTS, { reportMissing: false });
|
|
4801
|
-
const existingDirs = [...new Set(REQUIRED_DIRS)].filter((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
|
-
|
|
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
|
|
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
|
-
|
|
4829
|
-
|
|
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 =
|
|
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 =
|
|
4922
|
-
if (
|
|
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
|
|
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
|
-
|
|
5054
|
-
|
|
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
|
|
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:
|
|
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 (!
|
|
5122
|
-
const content =
|
|
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 =
|
|
5608
|
+
const reportPath = import_path22.default.join(WORKSCRIBE_DIR, `bug-report-${timestamp}.txt`);
|
|
5141
5609
|
const systemInfo = [
|
|
5142
|
-
`Workscribe version : ${"2.
|
|
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 (!
|
|
5165
|
-
|
|
5632
|
+
if (!import_fs26.default.existsSync(WORKSCRIBE_DIR)) {
|
|
5633
|
+
import_fs26.default.mkdirSync(WORKSCRIBE_DIR, { recursive: true, mode: 448 });
|
|
5166
5634
|
}
|
|
5167
|
-
|
|
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.
|
|
5174
|
-
body: `<!-- Attach ${
|
|
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
|
|
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
|
-
|
|
5663
|
+
import_fs26 = __toESM(require("fs"));
|
|
5196
5664
|
import_os12 = __toESM(require("os"));
|
|
5197
|
-
|
|
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
|
|
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(
|
|
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 =
|
|
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(
|
|
5901
|
+
log.plain(JSON.stringify(toFileContextJson(session, normalizedFile)));
|
|
5332
5902
|
return;
|
|
5333
5903
|
}
|
|
5334
|
-
printSessionContext(session, `Last session touching ${
|
|
5904
|
+
printSessionContext(session, `Last session touching ${import_path24.default.basename(normalizedFile)}`);
|
|
5335
5905
|
return;
|
|
5336
5906
|
}
|
|
5337
|
-
const
|
|
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
|
|
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
|
|
5930
|
+
var import_path24, import_chalk14;
|
|
5357
5931
|
var init_context = __esm({
|
|
5358
5932
|
"src/cli/context.ts"() {
|
|
5359
5933
|
"use strict";
|
|
5360
|
-
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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.
|
|
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);
|