@workscribe/cli 1.3.0 → 1.5.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/README.md +7 -2
- package/dist/index.js +356 -59
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/shell/hook.bash +3 -0
- package/shell/hook.zsh +3 -0
package/dist/index.js
CHANGED
|
@@ -173,6 +173,12 @@ function deleteEventsByIds(ids) {
|
|
|
173
173
|
clearSessionsForDate(date);
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
|
+
function deleteEventsForDate(date) {
|
|
177
|
+
const db = getDb();
|
|
178
|
+
clearSessionsForDate(date);
|
|
179
|
+
const result = db.prepare(`DELETE FROM events WHERE date(captured_at, 'localtime') = $date`).run({ date });
|
|
180
|
+
return result.changes;
|
|
181
|
+
}
|
|
176
182
|
function clearSessionsForDate(date) {
|
|
177
183
|
const db = getDb();
|
|
178
184
|
db.prepare(`
|
|
@@ -271,52 +277,72 @@ function getGitContext(cwd, nameOverride) {
|
|
|
271
277
|
}
|
|
272
278
|
}
|
|
273
279
|
|
|
274
|
-
// src/capture/
|
|
280
|
+
// src/capture/git-snapshot.ts
|
|
281
|
+
var import_child_process2 = require("child_process");
|
|
282
|
+
function getCommitsSince(sha, cwd) {
|
|
283
|
+
if (!sha) return [];
|
|
284
|
+
try {
|
|
285
|
+
const out = (0, import_child_process2.execSync)(`git log ${sha}..HEAD --format=%s`, { cwd, timeout: 3e3 }).toString().trim();
|
|
286
|
+
return out ? out.split("\n").filter(Boolean) : [];
|
|
287
|
+
} catch {
|
|
288
|
+
return [];
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// src/config/ai-tools.json
|
|
275
293
|
var ai_tools_default = [
|
|
276
294
|
{
|
|
277
295
|
tool: "claude",
|
|
296
|
+
displayName: "Claude Code",
|
|
278
297
|
pattern: "^claude\\b",
|
|
279
298
|
hook: "claude-code",
|
|
280
299
|
contextSource: "hook"
|
|
281
300
|
},
|
|
282
301
|
{
|
|
283
302
|
tool: "aider",
|
|
303
|
+
displayName: "Aider",
|
|
284
304
|
pattern: "^aider\\b",
|
|
285
305
|
hook: null,
|
|
286
306
|
contextSource: "history-file"
|
|
287
307
|
},
|
|
288
308
|
{
|
|
289
309
|
tool: "gemini",
|
|
310
|
+
displayName: "Gemini",
|
|
290
311
|
pattern: "^gemini\\b",
|
|
291
312
|
hook: null,
|
|
292
313
|
contextSource: null
|
|
293
314
|
},
|
|
294
315
|
{
|
|
295
316
|
tool: "gh copilot",
|
|
317
|
+
displayName: "GitHub Copilot",
|
|
296
318
|
pattern: "^gh\\s+copilot\\b",
|
|
297
319
|
hook: null,
|
|
298
320
|
contextSource: null
|
|
299
321
|
},
|
|
300
322
|
{
|
|
301
323
|
tool: "cody",
|
|
324
|
+
displayName: "Cody",
|
|
302
325
|
pattern: "^cody\\b",
|
|
303
326
|
hook: null,
|
|
304
327
|
contextSource: null
|
|
305
328
|
},
|
|
306
329
|
{
|
|
307
330
|
tool: "amazon-q",
|
|
331
|
+
displayName: "Amazon Q",
|
|
308
332
|
pattern: "^q\\s+(chat|dev)\\b",
|
|
309
333
|
hook: null,
|
|
310
334
|
contextSource: null
|
|
311
335
|
},
|
|
312
336
|
{
|
|
313
337
|
tool: "continue",
|
|
338
|
+
displayName: "Continue",
|
|
314
339
|
pattern: "^continue\\b",
|
|
315
340
|
hook: null,
|
|
316
341
|
contextSource: null
|
|
317
342
|
},
|
|
318
343
|
{
|
|
319
344
|
tool: "cursor",
|
|
345
|
+
displayName: "Cursor",
|
|
320
346
|
pattern: "^cursor\\b",
|
|
321
347
|
hook: null,
|
|
322
348
|
contextSource: null
|
|
@@ -378,6 +404,10 @@ var RULES = [
|
|
|
378
404
|
tool: m[1],
|
|
379
405
|
packages: extractPackages(cmd, "add")
|
|
380
406
|
})),
|
|
407
|
+
makeRule(/^bun\s+(install|add)\b/, "dependency_install", (_m, cmd) => ({
|
|
408
|
+
tool: "bun",
|
|
409
|
+
packages: extractPackages(cmd, "add")
|
|
410
|
+
})),
|
|
381
411
|
makeRule(/^pip\s+(install)\b/, "dependency_install", (_m, cmd) => ({
|
|
382
412
|
tool: "pip",
|
|
383
413
|
packages: extractPackages(cmd, "install")
|
|
@@ -403,12 +433,19 @@ var RULES = [
|
|
|
403
433
|
tool: m[1],
|
|
404
434
|
packages: extractPackages(cmd, "remove")
|
|
405
435
|
})),
|
|
436
|
+
makeRule(/^bun\s+remove\b/, "dependency_remove", (_m, cmd) => ({
|
|
437
|
+
tool: "bun",
|
|
438
|
+
packages: extractPackages(cmd, "remove")
|
|
439
|
+
})),
|
|
406
440
|
makeRule(/^pip\s+uninstall\b/, "dependency_remove", (_m, cmd) => ({
|
|
407
441
|
tool: "pip",
|
|
408
442
|
packages: extractPackages(cmd, "uninstall")
|
|
409
443
|
})),
|
|
410
444
|
// ─── Tests ────────────────────────────────────────────────────────────────
|
|
411
|
-
makeRule(/^
|
|
445
|
+
makeRule(/^npm\s+(test|run\s+test)\b/, "test_run", () => ({ tool: "npm" })),
|
|
446
|
+
makeRule(/^yarn\s+test\b/, "test_run", () => ({ tool: "yarn" })),
|
|
447
|
+
makeRule(/^pnpm\s+test\b/, "test_run", () => ({ tool: "pnpm" })),
|
|
448
|
+
makeRule(/^bun\s+test\b/, "test_run", () => ({ tool: "bun" })),
|
|
412
449
|
makeRule(/^(jest|vitest)\b/, "test_run", (m) => ({ tool: m[1] })),
|
|
413
450
|
makeRule(/^pytest\b/, "test_run", () => ({ tool: "pytest" })),
|
|
414
451
|
makeRule(/^go\s+test\b/, "test_run", () => ({ tool: "go" })),
|
|
@@ -418,6 +455,7 @@ var RULES = [
|
|
|
418
455
|
// ─── Build ────────────────────────────────────────────────────────────────
|
|
419
456
|
makeRule(/^npm\s+run\s+build\b/, "build", () => ({ tool: "npm" })),
|
|
420
457
|
makeRule(/^(yarn|pnpm)\s+build\b/, "build", (m) => ({ tool: m[1] })),
|
|
458
|
+
makeRule(/^bun\s+run\s+build\b/, "build", () => ({ tool: "bun" })),
|
|
421
459
|
makeRule(/^(make|gmake)\b/, "build", (m) => ({ tool: m[1] })),
|
|
422
460
|
makeRule(/^cargo\s+build\b/, "build", () => ({ tool: "cargo" })),
|
|
423
461
|
makeRule(/^go\s+build\b/, "build", () => ({ tool: "go" })),
|
|
@@ -429,6 +467,10 @@ var RULES = [
|
|
|
429
467
|
script: cmd.split(/\s+/)[2]
|
|
430
468
|
})),
|
|
431
469
|
makeRule(/^(yarn|pnpm)\s+(dev|start)\b/, "server_run", (m) => ({ tool: m[1], script: m[2] })),
|
|
470
|
+
makeRule(/^bun\s+run\s+(dev|start|serve)\b/, "server_run", (_m, cmd) => ({
|
|
471
|
+
tool: "bun",
|
|
472
|
+
script: cmd.split(/\s+/)[2]
|
|
473
|
+
})),
|
|
432
474
|
makeRule(/^python\s+.*manage\.py\s+runserver\b/, "server_run", () => ({ tool: "django" })),
|
|
433
475
|
// ─── Infrastructure ───────────────────────────────────────────────────────
|
|
434
476
|
makeRule(/^ssh\b/, "infrastructure", (_m, cmd) => {
|
|
@@ -461,18 +503,36 @@ var RULES = [
|
|
|
461
503
|
return { variable: varName };
|
|
462
504
|
}),
|
|
463
505
|
makeRule(/^source\s+/, "environment", () => ({})),
|
|
464
|
-
// ───
|
|
506
|
+
// ─── generic run scripts ──────────────────────────────────────────────────
|
|
465
507
|
makeRule(/^npm\s+run\s+\S+/, "script_run", (_m, cmd) => ({
|
|
466
508
|
tool: "npm",
|
|
467
509
|
script: cmd.split(/\s+/)[2]
|
|
468
510
|
})),
|
|
511
|
+
makeRule(/^bun\s+run\s+\S+/, "script_run", (_m, cmd) => ({
|
|
512
|
+
tool: "bun",
|
|
513
|
+
script: cmd.split(/\s+/)[2]
|
|
514
|
+
})),
|
|
515
|
+
// ─── Code search ─────────────────────────────────────────────────────────
|
|
516
|
+
makeRule(/^(grep|egrep|fgrep)\b/, "code_search", (m) => ({ tool: m[1] })),
|
|
517
|
+
makeRule(/^(rg|ag|ack|ripgrep)\b/, "code_search", (m) => ({ tool: m[1] })),
|
|
518
|
+
makeRule(/^find\s/, "code_search", () => ({ tool: "find" })),
|
|
519
|
+
// ─── Code read ────────────────────────────────────────────────────────────
|
|
520
|
+
makeRule(/^(cat|bat|less|more)\s+\S+/, "code_read", (m) => ({ tool: m[1] })),
|
|
521
|
+
makeRule(/^(head|tail)\b/, "code_read", (m) => ({ tool: m[1] })),
|
|
522
|
+
// ─── File operations ──────────────────────────────────────────────────────
|
|
523
|
+
makeRule(/^(cp|rsync)\b/, "file_ops", (m) => ({ tool: m[1] })),
|
|
524
|
+
makeRule(/^(mv)\b/, "file_ops", () => ({ tool: "mv" })),
|
|
525
|
+
makeRule(/^(rm|rmdir)\b/, "file_ops", (m) => ({ tool: m[1] })),
|
|
526
|
+
makeRule(/^(mkdir|touch|ln)\b/, "file_ops", (m) => ({ tool: m[1] })),
|
|
527
|
+
makeRule(/^(chmod|chown|chgrp)\b/, "file_ops", (m) => ({ tool: m[1] })),
|
|
469
528
|
// ─── AI terminal tools (registry-driven) ─────────────────────────────────
|
|
470
529
|
...ai_tools_default.map(
|
|
471
530
|
(entry) => makeRule(new RegExp(entry.pattern), "ai_session", () => ({ tool: entry.tool }))
|
|
472
531
|
)
|
|
473
532
|
];
|
|
474
533
|
function normalizeCommand(command) {
|
|
475
|
-
|
|
534
|
+
let trimmed = command.trim().replace(/\s+/g, " ");
|
|
535
|
+
trimmed = trimmed.replace(/^(git)\s+-C\s+\S+\s+/, "$1 ");
|
|
476
536
|
for (const rule of RULES) {
|
|
477
537
|
const result = rule.match(trimmed);
|
|
478
538
|
if (result) return result;
|
|
@@ -563,12 +623,14 @@ function deepMerge(defaults, overrides) {
|
|
|
563
623
|
function normalizeForFilter(cmd) {
|
|
564
624
|
return cmd.trim().replace(/\s+/g, " ");
|
|
565
625
|
}
|
|
626
|
+
var BUILTIN_IGNORE = /* @__PURE__ */ new Set(["workscribe"]);
|
|
566
627
|
function shouldFilter(command, ignoreList) {
|
|
567
628
|
const normalized = normalizeForFilter(command);
|
|
568
629
|
if (!normalized) return true;
|
|
630
|
+
const baseCmd = normalized.split(" ")[0];
|
|
631
|
+
if (BUILTIN_IGNORE.has(baseCmd)) return true;
|
|
569
632
|
const normalizedIgnore = ignoreList.map(normalizeForFilter);
|
|
570
633
|
if (normalizedIgnore.includes(normalized)) return true;
|
|
571
|
-
const baseCmd = normalized.split(" ")[0];
|
|
572
634
|
if (normalizedIgnore.includes(baseCmd)) return true;
|
|
573
635
|
return false;
|
|
574
636
|
}
|
|
@@ -590,6 +652,10 @@ function captureCommand(opts) {
|
|
|
590
652
|
const nameOverride = config.projects?.[cwd] ?? null;
|
|
591
653
|
const { repoName, branch } = getGitContext(cwd, nameOverride);
|
|
592
654
|
const { category, metadata } = normalizeCommand(safeCommand);
|
|
655
|
+
if (category === "ai_session" && opts.gitBefore) {
|
|
656
|
+
const commits = getCommitsSince(opts.gitBefore, cwd);
|
|
657
|
+
if (commits.length > 0) metadata.commits = commits;
|
|
658
|
+
}
|
|
593
659
|
insertEvent({
|
|
594
660
|
captured_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
595
661
|
command: safeCommand,
|
|
@@ -626,14 +692,15 @@ function debugLog(context, error) {
|
|
|
626
692
|
|
|
627
693
|
// src/cli/capture.ts
|
|
628
694
|
function registerCapture(program2) {
|
|
629
|
-
program2.command("_capture").description("Internal: capture a shell command event (called by shell hook)").option("--cmd <command>", "command that was executed").option("--cwd <path>", "working directory").option("--exit <code>", "exit code", parseInt).option("--duration <ms>", "duration in milliseconds", parseInt).action((opts) => {
|
|
695
|
+
program2.command("_capture").description("Internal: capture a shell command event (called by shell hook)").option("--cmd <command>", "command that was executed").option("--cwd <path>", "working directory").option("--exit <code>", "exit code", parseInt).option("--duration <ms>", "duration in milliseconds", parseInt).option("--git-before <sha>", "git HEAD SHA before this command ran").action((opts) => {
|
|
630
696
|
try {
|
|
631
697
|
if (!opts.cmd || !opts.cwd) return;
|
|
632
698
|
captureCommand({
|
|
633
699
|
command: opts.cmd,
|
|
634
700
|
cwd: opts.cwd,
|
|
635
701
|
exitCode: opts.exit ?? null,
|
|
636
|
-
durationMs: opts.duration ?? null
|
|
702
|
+
durationMs: opts.duration ?? null,
|
|
703
|
+
gitBefore: opts.gitBefore ?? void 0
|
|
637
704
|
});
|
|
638
705
|
} catch (err) {
|
|
639
706
|
debugLog("_capture", err);
|
|
@@ -641,22 +708,62 @@ function registerCapture(program2) {
|
|
|
641
708
|
});
|
|
642
709
|
}
|
|
643
710
|
|
|
711
|
+
// src/cli/hook.ts
|
|
712
|
+
function parseHookPayload(raw) {
|
|
713
|
+
if (!raw.trim()) return null;
|
|
714
|
+
const payload = JSON.parse(raw);
|
|
715
|
+
const toolInput = payload.tool_input;
|
|
716
|
+
const command = typeof toolInput?.command === "string" ? toolInput.command : null;
|
|
717
|
+
const cwd = typeof payload.cwd === "string" ? payload.cwd : null;
|
|
718
|
+
if (!command || !cwd) return null;
|
|
719
|
+
return { command, cwd };
|
|
720
|
+
}
|
|
721
|
+
function registerHook(program2) {
|
|
722
|
+
program2.command("_hook").description("Internal: capture events from AI tool hooks (stdin JSON payload)").option("--source <source>", "hook source identifier (e.g. claude-code)").action(() => {
|
|
723
|
+
let raw = "";
|
|
724
|
+
process.stdin.setEncoding("utf-8");
|
|
725
|
+
process.stdin.on("data", (chunk) => {
|
|
726
|
+
raw += chunk;
|
|
727
|
+
});
|
|
728
|
+
process.stdin.on("end", () => {
|
|
729
|
+
try {
|
|
730
|
+
const parsed = parseHookPayload(raw);
|
|
731
|
+
if (!parsed) return;
|
|
732
|
+
captureCommand({ command: parsed.command, cwd: parsed.cwd, exitCode: null, durationMs: null });
|
|
733
|
+
} catch (err) {
|
|
734
|
+
debugLog("_hook", err);
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
|
|
644
740
|
// src/cli/init.ts
|
|
645
|
-
var
|
|
646
|
-
var
|
|
647
|
-
var
|
|
741
|
+
var import_fs8 = __toESM(require("fs"));
|
|
742
|
+
var import_path5 = __toESM(require("path"));
|
|
743
|
+
var import_os3 = __toESM(require("os"));
|
|
648
744
|
var import_prompts = __toESM(require("prompts"));
|
|
649
745
|
var import_chalk2 = __toESM(require("chalk"));
|
|
746
|
+
|
|
747
|
+
// src/capture/shell-hook.ts
|
|
748
|
+
var import_fs6 = __toESM(require("fs"));
|
|
749
|
+
var import_path3 = __toESM(require("path"));
|
|
650
750
|
var HOOK_START = "# workscribe:hook:start";
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
if (
|
|
654
|
-
|
|
655
|
-
|
|
751
|
+
var HOOK_END = "# workscribe:hook:end";
|
|
752
|
+
function getInstalledHookBlock(rcFile) {
|
|
753
|
+
if (!import_fs6.default.existsSync(rcFile)) return null;
|
|
754
|
+
const content = import_fs6.default.readFileSync(rcFile, "utf-8");
|
|
755
|
+
const startIdx = content.indexOf(HOOK_START);
|
|
756
|
+
const endIdx = content.indexOf(HOOK_END);
|
|
757
|
+
if (startIdx === -1 || endIdx === -1) return null;
|
|
758
|
+
return content.slice(startIdx, endIdx + HOOK_END.length);
|
|
656
759
|
}
|
|
657
760
|
function isHookInstalled(rcFile) {
|
|
658
|
-
|
|
659
|
-
|
|
761
|
+
return getInstalledHookBlock(rcFile) !== null;
|
|
762
|
+
}
|
|
763
|
+
function isHookOutdated(rcFile, hookContent) {
|
|
764
|
+
const installed = getInstalledHookBlock(rcFile);
|
|
765
|
+
if (!installed) return false;
|
|
766
|
+
return installed.trim() !== hookContent.trim();
|
|
660
767
|
}
|
|
661
768
|
function installHook(rcFile, hookContent) {
|
|
662
769
|
const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
|
|
@@ -666,15 +773,98 @@ function installHook(rcFile, hookContent) {
|
|
|
666
773
|
${hookContent}
|
|
667
774
|
`);
|
|
668
775
|
}
|
|
776
|
+
function updateHook(rcFile, hookContent) {
|
|
777
|
+
const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
|
|
778
|
+
import_fs6.default.copyFileSync(rcFile, backup);
|
|
779
|
+
log.info(`Backed up ${rcFile} \u2192 ${import_path3.default.basename(backup)}`);
|
|
780
|
+
const content = import_fs6.default.readFileSync(rcFile, "utf-8");
|
|
781
|
+
const startIdx = content.indexOf(HOOK_START);
|
|
782
|
+
const endIdx = content.indexOf(HOOK_END) + HOOK_END.length;
|
|
783
|
+
import_fs6.default.writeFileSync(rcFile, content.slice(0, startIdx) + hookContent + content.slice(endIdx));
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// src/capture/hooks/claude-code.ts
|
|
787
|
+
var import_fs7 = __toESM(require("fs"));
|
|
788
|
+
var import_path4 = __toESM(require("path"));
|
|
789
|
+
var import_os2 = __toESM(require("os"));
|
|
790
|
+
|
|
791
|
+
// src/capture/hooks/utils.ts
|
|
792
|
+
var import_child_process3 = require("child_process");
|
|
793
|
+
function isCommandInstalled(cmd) {
|
|
794
|
+
try {
|
|
795
|
+
(0, import_child_process3.execSync)(`which ${cmd}`, { stdio: "ignore" });
|
|
796
|
+
return true;
|
|
797
|
+
} catch {
|
|
798
|
+
return false;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// src/capture/hooks/claude-code.ts
|
|
803
|
+
var CLAUDE_SETTINGS_PATH = import_path4.default.join(import_os2.default.homedir(), ".claude", "settings.json");
|
|
804
|
+
function readSettings() {
|
|
805
|
+
if (!import_fs7.default.existsSync(CLAUDE_SETTINGS_PATH)) return {};
|
|
806
|
+
try {
|
|
807
|
+
return JSON.parse(import_fs7.default.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8"));
|
|
808
|
+
} catch {
|
|
809
|
+
return {};
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
var claudeCodeIntegration = {
|
|
813
|
+
displayName: "Claude Code",
|
|
814
|
+
isInstalled() {
|
|
815
|
+
return isCommandInstalled("claude");
|
|
816
|
+
},
|
|
817
|
+
isHookInstalled() {
|
|
818
|
+
const settings = readSettings();
|
|
819
|
+
const postToolUse = settings.hooks?.PostToolUse ?? [];
|
|
820
|
+
return postToolUse.some((entry) => {
|
|
821
|
+
const hooks = entry.hooks ?? [];
|
|
822
|
+
return hooks.some((h) => {
|
|
823
|
+
const hook = h;
|
|
824
|
+
return typeof hook.command === "string" && hook.command.includes("workscribe _hook");
|
|
825
|
+
});
|
|
826
|
+
});
|
|
827
|
+
},
|
|
828
|
+
install() {
|
|
829
|
+
const settings = readSettings();
|
|
830
|
+
const existingHooks = settings.hooks ?? {};
|
|
831
|
+
const existingPostToolUse = existingHooks.PostToolUse ?? [];
|
|
832
|
+
settings.hooks = {
|
|
833
|
+
...existingHooks,
|
|
834
|
+
PostToolUse: [
|
|
835
|
+
...existingPostToolUse,
|
|
836
|
+
{
|
|
837
|
+
matcher: "Bash",
|
|
838
|
+
hooks: [{ type: "command", command: "workscribe _hook --source claude-code", async: true }]
|
|
839
|
+
}
|
|
840
|
+
]
|
|
841
|
+
};
|
|
842
|
+
import_fs7.default.mkdirSync(import_path4.default.dirname(CLAUDE_SETTINGS_PATH), { recursive: true });
|
|
843
|
+
import_fs7.default.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
844
|
+
}
|
|
845
|
+
};
|
|
846
|
+
|
|
847
|
+
// src/capture/hooks/index.ts
|
|
848
|
+
var AI_HOOK_INTEGRATIONS = {
|
|
849
|
+
"claude-code": claudeCodeIntegration
|
|
850
|
+
};
|
|
851
|
+
|
|
852
|
+
// src/cli/init.ts
|
|
853
|
+
function detectShell() {
|
|
854
|
+
const shell = process.env.SHELL ?? "";
|
|
855
|
+
if (shell.includes("zsh")) return { name: "zsh", rcFile: import_path5.default.join(import_os3.default.homedir(), ".zshrc") };
|
|
856
|
+
if (shell.includes("bash")) return { name: "bash", rcFile: import_path5.default.join(import_os3.default.homedir(), ".bashrc") };
|
|
857
|
+
return null;
|
|
858
|
+
}
|
|
669
859
|
function registerInit(program2) {
|
|
670
|
-
program2.command("init").description("
|
|
860
|
+
program2.command("init").description("Set up Workscribe and install or update shell hooks").action(async () => {
|
|
671
861
|
log.plain(import_chalk2.default.bold("\n Workscribe \u2014 Developer Work Journal\n"));
|
|
672
|
-
if (!
|
|
673
|
-
|
|
862
|
+
if (!import_fs8.default.existsSync(WORKSCRIBE_DIR)) {
|
|
863
|
+
import_fs8.default.mkdirSync(WORKSCRIBE_DIR, { recursive: true, mode: 448 });
|
|
674
864
|
log.success(`Created ${WORKSCRIBE_DIR}`);
|
|
675
865
|
} else {
|
|
676
866
|
try {
|
|
677
|
-
|
|
867
|
+
import_fs8.default.chmodSync(WORKSCRIBE_DIR, 448);
|
|
678
868
|
} catch {
|
|
679
869
|
}
|
|
680
870
|
}
|
|
@@ -685,17 +875,48 @@ function registerInit(program2) {
|
|
|
685
875
|
log.warn("Could not detect shell. Supported: zsh, bash");
|
|
686
876
|
log.plain(`Manually add the hook from: ${import_chalk2.default.cyan("shell/hook.zsh")} or ${import_chalk2.default.cyan("shell/hook.bash")}`);
|
|
687
877
|
} else {
|
|
688
|
-
|
|
689
|
-
|
|
878
|
+
const hookFile = import_path5.default.join(__dirname, "..", "shell", `hook.${shell.name}`);
|
|
879
|
+
if (!import_fs8.default.existsSync(hookFile)) {
|
|
880
|
+
log.warn(`Hook template not found at ${hookFile}`);
|
|
690
881
|
} else {
|
|
691
|
-
const
|
|
692
|
-
if (!
|
|
693
|
-
|
|
694
|
-
} else {
|
|
695
|
-
const hookContent = import_fs6.default.readFileSync(hookFile, "utf-8");
|
|
696
|
-
if (!import_fs6.default.existsSync(shell.rcFile)) import_fs6.default.writeFileSync(shell.rcFile, "");
|
|
882
|
+
const hookContent = import_fs8.default.readFileSync(hookFile, "utf-8");
|
|
883
|
+
if (!import_fs8.default.existsSync(shell.rcFile)) import_fs8.default.writeFileSync(shell.rcFile, "");
|
|
884
|
+
if (!isHookInstalled(shell.rcFile)) {
|
|
697
885
|
installHook(shell.rcFile, hookContent);
|
|
698
886
|
log.success(`Shell hook installed in ${shell.rcFile}`);
|
|
887
|
+
} else if (isHookOutdated(shell.rcFile, hookContent)) {
|
|
888
|
+
const { update } = await (0, import_prompts.default)({
|
|
889
|
+
type: "confirm",
|
|
890
|
+
name: "update",
|
|
891
|
+
message: `Shell hook in ${shell.rcFile} is outdated. Update?`,
|
|
892
|
+
initial: true
|
|
893
|
+
});
|
|
894
|
+
if (update) {
|
|
895
|
+
updateHook(shell.rcFile, hookContent);
|
|
896
|
+
log.success(`Shell hook updated in ${shell.rcFile}`);
|
|
897
|
+
} else {
|
|
898
|
+
log.info("Shell hook update skipped");
|
|
899
|
+
}
|
|
900
|
+
} else {
|
|
901
|
+
log.info(`Shell hook up to date in ${shell.rcFile}`);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
for (const tool of ai_tools_default.filter((t) => t.hook !== null)) {
|
|
906
|
+
const integration = AI_HOOK_INTEGRATIONS[tool.hook];
|
|
907
|
+
if (!integration?.isInstalled()) continue;
|
|
908
|
+
if (integration.isHookInstalled()) {
|
|
909
|
+
log.info(`${integration.displayName} session hook already installed`);
|
|
910
|
+
} else {
|
|
911
|
+
const { install } = await (0, import_prompts.default)({
|
|
912
|
+
type: "confirm",
|
|
913
|
+
name: "install",
|
|
914
|
+
message: `${integration.displayName} detected. Install session capture hook?`,
|
|
915
|
+
initial: true
|
|
916
|
+
});
|
|
917
|
+
if (install) {
|
|
918
|
+
integration.install();
|
|
919
|
+
log.success(`${integration.displayName} session hook installed`);
|
|
699
920
|
}
|
|
700
921
|
}
|
|
701
922
|
}
|
|
@@ -745,7 +966,7 @@ function registerInit(program2) {
|
|
|
745
966
|
log.info("Skipped \u2014 run later: workscribe config set ai.provider <provider>");
|
|
746
967
|
}
|
|
747
968
|
}
|
|
748
|
-
if (!
|
|
969
|
+
if (!import_fs8.default.existsSync(CONFIG_PATH)) {
|
|
749
970
|
writeConfig(config);
|
|
750
971
|
log.success("Config file created");
|
|
751
972
|
}
|
|
@@ -764,7 +985,7 @@ function registerInit(program2) {
|
|
|
764
985
|
}
|
|
765
986
|
|
|
766
987
|
// src/cli/status.ts
|
|
767
|
-
var
|
|
988
|
+
var import_fs9 = __toESM(require("fs"));
|
|
768
989
|
var import_chalk3 = __toESM(require("chalk"));
|
|
769
990
|
|
|
770
991
|
// src/utils/format.ts
|
|
@@ -822,7 +1043,7 @@ function registerStatus(program2) {
|
|
|
822
1043
|
const total = getEventCount();
|
|
823
1044
|
const todayEvents = getEventsForDate(todayDate());
|
|
824
1045
|
const last = getLastEvent();
|
|
825
|
-
const isPaused =
|
|
1046
|
+
const isPaused = import_fs9.default.existsSync(PAUSE_FLAG_PATH);
|
|
826
1047
|
log.plain("");
|
|
827
1048
|
log.plain(import_chalk3.default.bold(" Workscribe Status"));
|
|
828
1049
|
log.plain("");
|
|
@@ -861,9 +1082,15 @@ var CATEGORY_COLORS = {
|
|
|
861
1082
|
git_other: import_chalk4.default.blue,
|
|
862
1083
|
code_merge: import_chalk4.default.green,
|
|
863
1084
|
code_stash: import_chalk4.default.blue,
|
|
864
|
-
ai_session: import_chalk4.default.magenta,
|
|
1085
|
+
ai_session: import_chalk4.default.bold.magenta,
|
|
1086
|
+
code_search: import_chalk4.default.dim.cyan,
|
|
1087
|
+
code_read: import_chalk4.default.dim.white,
|
|
1088
|
+
file_ops: import_chalk4.default.yellow,
|
|
865
1089
|
other: import_chalk4.default.gray
|
|
866
1090
|
};
|
|
1091
|
+
var AI_TOOL_DISPLAY_NAMES = Object.fromEntries(
|
|
1092
|
+
ai_tools_default.map((t) => [t.tool, t.displayName])
|
|
1093
|
+
);
|
|
867
1094
|
function registerEvents(program2) {
|
|
868
1095
|
program2.command("events").description("List captured events").option("--date <YYYY-MM-DD>", "date to list events for (default: today)").option("--limit <n>", "max events to show", parseInt).action((opts) => {
|
|
869
1096
|
let date = todayDate();
|
|
@@ -888,10 +1115,22 @@ function registerEvents(program2) {
|
|
|
888
1115
|
const colorFn = CATEGORY_COLORS[event.category] ?? import_chalk4.default.gray;
|
|
889
1116
|
const time = formatDate(event.captured_at);
|
|
890
1117
|
const category = colorFn(event.category.padEnd(20));
|
|
891
|
-
const cmd = import_chalk4.default.dim(event.command.slice(0, 55));
|
|
892
1118
|
const repo = event.repo_name ? import_chalk4.default.dim(` [${event.repo_name}]`) : "";
|
|
893
1119
|
const exitBadge = event.exit_code && event.exit_code !== 0 ? import_chalk4.default.red(` \u2717${event.exit_code}`) : "";
|
|
894
|
-
|
|
1120
|
+
let cmdDisplay;
|
|
1121
|
+
if (event.category === "ai_session") {
|
|
1122
|
+
let meta = {};
|
|
1123
|
+
try {
|
|
1124
|
+
meta = event.metadata ? JSON.parse(event.metadata) : {};
|
|
1125
|
+
} catch {
|
|
1126
|
+
}
|
|
1127
|
+
const toolKey = typeof meta.tool === "string" ? meta.tool : "";
|
|
1128
|
+
const toolName = AI_TOOL_DISPLAY_NAMES[toolKey] ?? toolKey;
|
|
1129
|
+
cmdDisplay = import_chalk4.default.white(toolName.padEnd(20)) + import_chalk4.default.magenta(" \u2726");
|
|
1130
|
+
} else {
|
|
1131
|
+
cmdDisplay = import_chalk4.default.dim(event.command.slice(0, 55));
|
|
1132
|
+
}
|
|
1133
|
+
log.plain(` ${import_chalk4.default.dim(time)} ${category} ${cmdDisplay}${repo}${exitBadge}`);
|
|
895
1134
|
}
|
|
896
1135
|
if (limit < events.length) {
|
|
897
1136
|
log.plain("");
|
|
@@ -1033,8 +1272,8 @@ function registerSessions(program2) {
|
|
|
1033
1272
|
|
|
1034
1273
|
// src/cli/summary.ts
|
|
1035
1274
|
var import_chalk6 = __toESM(require("chalk"));
|
|
1036
|
-
var
|
|
1037
|
-
var
|
|
1275
|
+
var import_fs10 = __toESM(require("fs"));
|
|
1276
|
+
var import_path6 = __toESM(require("path"));
|
|
1038
1277
|
|
|
1039
1278
|
// src/ai/prompt.ts
|
|
1040
1279
|
var SYSTEM_PROMPT = `You write short developer work summaries. Here is an example:
|
|
@@ -1042,23 +1281,27 @@ var SYSTEM_PROMPT = `You write short developer work summaries. Here is an exampl
|
|
|
1042
1281
|
INPUT:
|
|
1043
1282
|
Project: api-service
|
|
1044
1283
|
Branch: feature/auth
|
|
1045
|
-
Duration:
|
|
1284
|
+
Duration: 2h 00m
|
|
1046
1285
|
Events:
|
|
1047
1286
|
- branch_switch {"branch":"feature/auth"}
|
|
1048
1287
|
- 2x dependency_install {"tool":"npm","packages":["bcrypt","jsonwebtoken"]}
|
|
1049
1288
|
- 2x test_run {"tool":"jest"} [failed]
|
|
1050
1289
|
- test_run {"tool":"jest"}
|
|
1290
|
+
- 2x file_ops {"tool":"mkdir"}
|
|
1051
1291
|
- code_commit {"message":"fix: auth token validation"}
|
|
1292
|
+
- code_push {"remote":"origin","branch":"feature/auth"}
|
|
1052
1293
|
|
|
1053
1294
|
OUTPUT:
|
|
1054
|
-
- Switched to the authentication feature branch
|
|
1055
|
-
- Added JWT and bcrypt dependencies
|
|
1295
|
+
- Switched to the authentication feature branch and installed JWT and bcrypt dependencies
|
|
1056
1296
|
- Resolved failing auth tests
|
|
1057
|
-
- Committed authentication fixes
|
|
1297
|
+
- Committed and pushed authentication fixes
|
|
1058
1298
|
|
|
1059
|
-
|
|
1299
|
+
Events like code_push, build, file_ops, and code_stash provide context \u2014 fold them into the surrounding narrative rather than listing them as separate bullets. Only surface them directly when they are the main activity (e.g. a standalone build fix, or a significant restructure with no accompanying commits).
|
|
1060
1300
|
|
|
1061
|
-
|
|
1301
|
+
For ai_session events: describe what was built during the AI session using the tool name (e.g. "via Claude Code"). If a commits array is present in metadata, use those commit messages to describe what was built. Group all activity from the session into one narrative bullet.
|
|
1302
|
+
|
|
1303
|
+
Maximum 5 bullets. Output ONLY the bullet points, nothing else.`;
|
|
1304
|
+
var EXCLUDED = /* @__PURE__ */ new Set(["environment", "code_search", "code_read", "server_run", "git_other"]);
|
|
1062
1305
|
function buildUserMessage(payload) {
|
|
1063
1306
|
const lines = [`Date: ${payload.date}`, ""];
|
|
1064
1307
|
for (const session of payload.sessions) {
|
|
@@ -1068,6 +1311,7 @@ function buildUserMessage(payload) {
|
|
|
1068
1311
|
lines.push("Events:");
|
|
1069
1312
|
const counts = /* @__PURE__ */ new Map();
|
|
1070
1313
|
for (const event of session.events) {
|
|
1314
|
+
if (EXCLUDED.has(event.category)) continue;
|
|
1071
1315
|
const meta = Object.keys(event.metadata).length > 0 ? ` ${JSON.stringify(event.metadata)}` : "";
|
|
1072
1316
|
const failed = event.exitCode !== null && event.exitCode !== 0 ? " [failed]" : "";
|
|
1073
1317
|
const key = `${event.category}${meta}${failed}`;
|
|
@@ -1124,6 +1368,7 @@ function buildWeeklyMessage(payload) {
|
|
|
1124
1368
|
lines.push(` Project: ${project}${branch} \u2014 ${formatDuration(s.durationMinutes)}`);
|
|
1125
1369
|
const counts = /* @__PURE__ */ new Map();
|
|
1126
1370
|
for (const e of s.events) {
|
|
1371
|
+
if (EXCLUDED.has(e.category)) continue;
|
|
1127
1372
|
const meta = Object.keys(e.metadata).length > 0 ? ` ${JSON.stringify(e.metadata)}` : "";
|
|
1128
1373
|
const failed = e.exitCode !== null && e.exitCode !== 0 ? " [failed]" : "";
|
|
1129
1374
|
const key = `${e.category}${meta}${failed}`;
|
|
@@ -1441,9 +1686,9 @@ function registerSummary(program2) {
|
|
|
1441
1686
|
return;
|
|
1442
1687
|
}
|
|
1443
1688
|
const cacheKey = isoWeekKey(today);
|
|
1444
|
-
const cacheFile =
|
|
1445
|
-
if (!opts.fresh &&
|
|
1446
|
-
const cached =
|
|
1689
|
+
const cacheFile = import_path6.default.join(WEEKLY_CACHE_DIR, `${cacheKey}.md`);
|
|
1690
|
+
if (!opts.fresh && import_fs10.default.existsSync(cacheFile)) {
|
|
1691
|
+
const cached = import_fs10.default.readFileSync(cacheFile, "utf-8");
|
|
1447
1692
|
log.plain("");
|
|
1448
1693
|
log.plain(cached);
|
|
1449
1694
|
log.plain("");
|
|
@@ -1459,8 +1704,8 @@ function registerSummary(program2) {
|
|
|
1459
1704
|
log.plain("");
|
|
1460
1705
|
log.plain(summary);
|
|
1461
1706
|
log.plain("");
|
|
1462
|
-
|
|
1463
|
-
|
|
1707
|
+
import_fs10.default.mkdirSync(WEEKLY_CACHE_DIR, { recursive: true });
|
|
1708
|
+
import_fs10.default.writeFileSync(cacheFile, summary, { encoding: "utf-8", mode: 384 });
|
|
1464
1709
|
await handleWebhook(opts, config, summary);
|
|
1465
1710
|
} catch (e) {
|
|
1466
1711
|
log.error(`Weekly summary failed [${config.ai.provider}]: ${e.message}`);
|
|
@@ -1523,8 +1768,8 @@ ${bullets.trim()}`;
|
|
|
1523
1768
|
}
|
|
1524
1769
|
|
|
1525
1770
|
// src/cli/export.ts
|
|
1526
|
-
var
|
|
1527
|
-
var
|
|
1771
|
+
var import_fs11 = __toESM(require("fs"));
|
|
1772
|
+
var import_path7 = __toESM(require("path"));
|
|
1528
1773
|
function eventRowToPayload2(row) {
|
|
1529
1774
|
let metadata = {};
|
|
1530
1775
|
try {
|
|
@@ -1569,9 +1814,9 @@ function registerExport(program2) {
|
|
|
1569
1814
|
const payload = { date, sessions: sessionPayloads };
|
|
1570
1815
|
log.info(`Generating summary via ${config.ai.provider}\u2026`);
|
|
1571
1816
|
const summary = await provider.summarize(payload);
|
|
1572
|
-
const outputPath = opts.output ??
|
|
1573
|
-
if (!
|
|
1574
|
-
|
|
1817
|
+
const outputPath = opts.output ?? import_path7.default.join(EXPORTS_DIR, `workscribe-${date}.md`);
|
|
1818
|
+
if (!import_fs11.default.existsSync(EXPORTS_DIR)) import_fs11.default.mkdirSync(EXPORTS_DIR, { recursive: true, mode: 448 });
|
|
1819
|
+
import_fs11.default.writeFileSync(outputPath, `# Work Summary \u2014 ${date}
|
|
1575
1820
|
|
|
1576
1821
|
${summary}
|
|
1577
1822
|
`, { encoding: "utf-8", mode: 384 });
|
|
@@ -1629,22 +1874,22 @@ function printConfig(obj, indent) {
|
|
|
1629
1874
|
}
|
|
1630
1875
|
|
|
1631
1876
|
// src/cli/pause.ts
|
|
1632
|
-
var
|
|
1877
|
+
var import_fs12 = __toESM(require("fs"));
|
|
1633
1878
|
function registerPause(program2) {
|
|
1634
1879
|
program2.command("pause").description("Pause capture temporarily").action(() => {
|
|
1635
|
-
if (
|
|
1880
|
+
if (import_fs12.default.existsSync(PAUSE_FLAG_PATH)) {
|
|
1636
1881
|
log.info("Capture is already paused. Run workscribe resume to restart.");
|
|
1637
1882
|
return;
|
|
1638
1883
|
}
|
|
1639
|
-
|
|
1884
|
+
import_fs12.default.writeFileSync(PAUSE_FLAG_PATH, "", { mode: 384 });
|
|
1640
1885
|
log.success("Capture paused. Run workscribe resume to restart.");
|
|
1641
1886
|
});
|
|
1642
1887
|
program2.command("resume").description("Resume capture after a pause").action(() => {
|
|
1643
|
-
if (!
|
|
1888
|
+
if (!import_fs12.default.existsSync(PAUSE_FLAG_PATH)) {
|
|
1644
1889
|
log.info("Capture is not paused.");
|
|
1645
1890
|
return;
|
|
1646
1891
|
}
|
|
1647
|
-
|
|
1892
|
+
import_fs12.default.rmSync(PAUSE_FLAG_PATH);
|
|
1648
1893
|
log.success("Capture resumed.");
|
|
1649
1894
|
});
|
|
1650
1895
|
}
|
|
@@ -1678,10 +1923,61 @@ function registerUndo(program2) {
|
|
|
1678
1923
|
});
|
|
1679
1924
|
}
|
|
1680
1925
|
|
|
1926
|
+
// src/cli/clear.ts
|
|
1927
|
+
var import_chalk8 = __toESM(require("chalk"));
|
|
1928
|
+
var import_prompts2 = __toESM(require("prompts"));
|
|
1929
|
+
function registerClear(program2) {
|
|
1930
|
+
program2.command("clear").description("Delete all captured events for a date").option("--date <YYYY-MM-DD>", "date to clear (default: today)").option("--dry-run", "preview what would be deleted without deleting").option("--yes", "skip confirmation prompt").action(async (opts) => {
|
|
1931
|
+
let date = todayDate();
|
|
1932
|
+
if (opts.date) {
|
|
1933
|
+
try {
|
|
1934
|
+
date = parseDate(opts.date);
|
|
1935
|
+
} catch (e) {
|
|
1936
|
+
log.error(e.message);
|
|
1937
|
+
process.exit(1);
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
const events = getEventsForDate(date);
|
|
1941
|
+
if (events.length === 0) {
|
|
1942
|
+
log.plain(`No events found for ${date}.`);
|
|
1943
|
+
return;
|
|
1944
|
+
}
|
|
1945
|
+
log.plain("");
|
|
1946
|
+
log.plain(import_chalk8.default.bold(` Events for ${date} (${events.length} total)`));
|
|
1947
|
+
log.plain("");
|
|
1948
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1949
|
+
for (const e of events) counts.set(e.category, (counts.get(e.category) ?? 0) + 1);
|
|
1950
|
+
for (const [cat, n] of counts) {
|
|
1951
|
+
log.plain(` ${import_chalk8.default.dim(String(n).padStart(4))} ${cat}`);
|
|
1952
|
+
}
|
|
1953
|
+
log.plain("");
|
|
1954
|
+
if (opts.dryRun) {
|
|
1955
|
+
log.plain(import_chalk8.default.dim(" Dry run \u2014 nothing deleted."));
|
|
1956
|
+
log.plain("");
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
if (!opts.yes) {
|
|
1960
|
+
const { confirm } = await (0, import_prompts2.default)({
|
|
1961
|
+
type: "confirm",
|
|
1962
|
+
name: "confirm",
|
|
1963
|
+
message: `Delete all ${events.length} events for ${date}?`,
|
|
1964
|
+
initial: false
|
|
1965
|
+
});
|
|
1966
|
+
if (!confirm) {
|
|
1967
|
+
log.plain("Cancelled.");
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
const deleted = deleteEventsForDate(date);
|
|
1972
|
+
log.success(`Cleared ${deleted} event${deleted !== 1 ? "s" : ""} for ${date}.`);
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1681
1976
|
// src/index.ts
|
|
1682
1977
|
var program = new import_commander.Command();
|
|
1683
|
-
program.name("workscribe").description("Automatic developer work journal").version("1.
|
|
1978
|
+
program.name("workscribe").description("Automatic developer work journal").version("1.5.0");
|
|
1684
1979
|
registerCapture(program);
|
|
1980
|
+
registerHook(program);
|
|
1685
1981
|
registerInit(program);
|
|
1686
1982
|
registerStatus(program);
|
|
1687
1983
|
registerEvents(program);
|
|
@@ -1691,5 +1987,6 @@ registerExport(program);
|
|
|
1691
1987
|
registerConfig(program);
|
|
1692
1988
|
registerPause(program);
|
|
1693
1989
|
registerUndo(program);
|
|
1990
|
+
registerClear(program);
|
|
1694
1991
|
program.parse();
|
|
1695
1992
|
//# sourceMappingURL=index.js.map
|