@workscribe/cli 1.10.0 → 1.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/index.js +1435 -848
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/shell/hook.bash +1 -1
- package/shell/hook.fish +34 -0
- package/shell/hook.zsh +1 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
1
|
+
#!/usr/bin/env -S node --disable-warning=ExperimentalWarning
|
|
2
2
|
"use strict";
|
|
3
3
|
var __create = Object.create;
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -26,24 +26,46 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
26
26
|
// src/index.ts
|
|
27
27
|
var import_commander = require("commander");
|
|
28
28
|
|
|
29
|
+
// src/utils/node-version.ts
|
|
30
|
+
function assertNodeVersion(nodeVersion) {
|
|
31
|
+
const major = Number(nodeVersion.split(".")[0]);
|
|
32
|
+
if (major < 22) {
|
|
33
|
+
process.stderr.write(
|
|
34
|
+
`workscribe requires Node.js 22 or later (you have v${nodeVersion}).
|
|
35
|
+
Upgrade at https://nodejs.org or via your version manager:
|
|
36
|
+
nvm install 22 && nvm use 22
|
|
37
|
+
`
|
|
38
|
+
);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
29
43
|
// src/capture/index.ts
|
|
30
|
-
var
|
|
44
|
+
var import_fs10 = __toESM(require("fs"));
|
|
31
45
|
|
|
32
46
|
// src/db/index.ts
|
|
33
47
|
var import_node_sqlite = require("node:sqlite");
|
|
34
|
-
var
|
|
48
|
+
var import_fs2 = __toESM(require("fs"));
|
|
35
49
|
|
|
36
50
|
// src/utils/paths.ts
|
|
37
51
|
var import_path = __toESM(require("path"));
|
|
38
52
|
var import_os = __toESM(require("os"));
|
|
39
|
-
var
|
|
40
|
-
var
|
|
41
|
-
var
|
|
42
|
-
var
|
|
43
|
-
var
|
|
44
|
-
var
|
|
45
|
-
var
|
|
46
|
-
var
|
|
53
|
+
var import_fs = __toESM(require("fs"));
|
|
54
|
+
var LEGACY_DIR = import_path.default.join(import_os.default.homedir(), ".workscribe");
|
|
55
|
+
var USE_XDG = !import_fs.default.existsSync(LEGACY_DIR);
|
|
56
|
+
var xdgDataDir = USE_XDG ? import_path.default.join(process.env.XDG_DATA_HOME ?? import_path.default.join(import_os.default.homedir(), ".local", "share"), "workscribe") : LEGACY_DIR;
|
|
57
|
+
var xdgConfigDir = USE_XDG ? import_path.default.join(process.env.XDG_CONFIG_HOME ?? import_path.default.join(import_os.default.homedir(), ".config"), "workscribe") : LEGACY_DIR;
|
|
58
|
+
var xdgStateDir = USE_XDG ? import_path.default.join(process.env.XDG_STATE_HOME ?? import_path.default.join(import_os.default.homedir(), ".local", "state"), "workscribe") : LEGACY_DIR;
|
|
59
|
+
var REQUIRED_DIRS = [.../* @__PURE__ */ new Set([xdgDataDir, xdgConfigDir, xdgStateDir])];
|
|
60
|
+
var WORKSCRIBE_DIR = xdgDataDir;
|
|
61
|
+
var DB_PATH = import_path.default.join(xdgDataDir, "workscribe.db");
|
|
62
|
+
var CONFIG_PATH = import_path.default.join(xdgConfigDir, "config.json");
|
|
63
|
+
var DEBUG_LOG_PATH = import_path.default.join(xdgStateDir, "debug.log");
|
|
64
|
+
var UPDATE_CHECK_PATH = import_path.default.join(xdgStateDir, "update-check.json");
|
|
65
|
+
var EXPORTS_DIR = import_path.default.join(xdgDataDir, "exports");
|
|
66
|
+
var PAUSE_FLAG_PATH = import_path.default.join(xdgStateDir, "paused");
|
|
67
|
+
var WEEKLY_CACHE_DIR = import_path.default.join(xdgDataDir, "weekly-cache");
|
|
68
|
+
var PROFILES_DIR = import_path.default.join(xdgDataDir, "profiles");
|
|
47
69
|
|
|
48
70
|
// src/db/migrations/001_initial_schema.ts
|
|
49
71
|
var initial_schema_default = {
|
|
@@ -112,12 +134,12 @@ var MIGRATIONS = [initial_schema_default, semantic_categories_default];
|
|
|
112
134
|
var _db = null;
|
|
113
135
|
function getDb() {
|
|
114
136
|
if (_db) return _db;
|
|
115
|
-
if (!
|
|
116
|
-
|
|
137
|
+
if (!import_fs2.default.existsSync(WORKSCRIBE_DIR)) {
|
|
138
|
+
import_fs2.default.mkdirSync(WORKSCRIBE_DIR, { recursive: true });
|
|
117
139
|
}
|
|
118
140
|
const db = new import_node_sqlite.DatabaseSync(DB_PATH);
|
|
119
141
|
try {
|
|
120
|
-
|
|
142
|
+
import_fs2.default.chmodSync(DB_PATH, 384);
|
|
121
143
|
} catch {
|
|
122
144
|
}
|
|
123
145
|
db.exec("PRAGMA journal_mode = WAL");
|
|
@@ -307,7 +329,10 @@ var BUILTIN_PATTERNS = [
|
|
|
307
329
|
// Values after long-form secret flags (single-char flags like -t/-p/-k are too ambiguous)
|
|
308
330
|
/(?<=--(password|passwd|secret|token|key|api[-_]?key|auth[-_]?token)\s+)\S+/gi,
|
|
309
331
|
// export VAR=VALUE where VAR looks sensitive
|
|
310
|
-
/(?<=export\s+(?:[A-Z_]*(?:TOKEN|SECRET|KEY|PASSWORD|PASS|AUTH|CREDENTIAL)[A-Z_]*)=)\S+/gi
|
|
332
|
+
/(?<=export\s+(?:[A-Z_]*(?:TOKEN|SECRET|KEY|PASSWORD|PASS|AUTH|CREDENTIAL)[A-Z_]*)=)\S+/gi,
|
|
333
|
+
// Inline assignment: VAR_WITH_SENSITIVE_KEYWORD=value cmd (e.g. API_KEY=secret npm start)
|
|
334
|
+
// Lookbehind matches only the variable-name= prefix, so only the value is replaced
|
|
335
|
+
/(?<=[A-Z_][A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|PASS|AUTH|CREDENTIAL)[A-Z0-9_]*=)\S+/gi
|
|
311
336
|
];
|
|
312
337
|
function redact(command, extraPatterns = []) {
|
|
313
338
|
let result = command;
|
|
@@ -328,7 +353,7 @@ function redact(command, extraPatterns = []) {
|
|
|
328
353
|
// src/capture/git.ts
|
|
329
354
|
var import_child_process = require("child_process");
|
|
330
355
|
var import_path2 = __toESM(require("path"));
|
|
331
|
-
var
|
|
356
|
+
var import_fs3 = __toESM(require("fs"));
|
|
332
357
|
function nameFromRemote(root) {
|
|
333
358
|
try {
|
|
334
359
|
const url = (0, import_child_process.execSync)("git remote get-url origin", {
|
|
@@ -344,7 +369,7 @@ function nameFromRemote(root) {
|
|
|
344
369
|
}
|
|
345
370
|
function nameFromPackageJson(root) {
|
|
346
371
|
try {
|
|
347
|
-
const raw =
|
|
372
|
+
const raw = import_fs3.default.readFileSync(import_path2.default.join(root, "package.json"), "utf-8");
|
|
348
373
|
const pkg = JSON.parse(raw);
|
|
349
374
|
if (!pkg.name) return null;
|
|
350
375
|
return pkg.name.replace(/^@[^/]+\//, "");
|
|
@@ -388,14 +413,14 @@ function getCommitsSince(sha, cwd) {
|
|
|
388
413
|
}
|
|
389
414
|
|
|
390
415
|
// src/capture/aider.ts
|
|
391
|
-
var
|
|
416
|
+
var import_fs4 = __toESM(require("fs"));
|
|
392
417
|
var import_path3 = __toESM(require("path"));
|
|
393
418
|
var HISTORY_FILE = ".aider.chat.history.md";
|
|
394
419
|
var PROMPT_MAX_LENGTH = 200;
|
|
395
420
|
function readLastAiderPrompt(cwd) {
|
|
396
421
|
try {
|
|
397
422
|
const histPath = import_path3.default.join(cwd, HISTORY_FILE);
|
|
398
|
-
const content =
|
|
423
|
+
const content = import_fs4.default.readFileSync(histPath, "utf-8");
|
|
399
424
|
const matches = [...content.matchAll(/^> (.+)$/gm)];
|
|
400
425
|
if (!matches.length) return null;
|
|
401
426
|
return matches[matches.length - 1][1].trim().slice(0, PROMPT_MAX_LENGTH);
|
|
@@ -405,7 +430,7 @@ function readLastAiderPrompt(cwd) {
|
|
|
405
430
|
}
|
|
406
431
|
|
|
407
432
|
// src/config/registry.ts
|
|
408
|
-
var
|
|
433
|
+
var import_fs6 = __toESM(require("fs"));
|
|
409
434
|
var import_os2 = __toESM(require("os"));
|
|
410
435
|
var import_path4 = __toESM(require("path"));
|
|
411
436
|
|
|
@@ -471,7 +496,7 @@ var ai_tools_default = [
|
|
|
471
496
|
|
|
472
497
|
// src/utils/logger.ts
|
|
473
498
|
var import_chalk = __toESM(require("chalk"));
|
|
474
|
-
var
|
|
499
|
+
var import_fs5 = __toESM(require("fs"));
|
|
475
500
|
var isDebug = process.env.WORKSCRIBE_DEBUG === "1";
|
|
476
501
|
var log = {
|
|
477
502
|
info: (msg) => console.log(import_chalk.default.cyan("\u2192"), msg),
|
|
@@ -485,7 +510,7 @@ function debugLog(context, error) {
|
|
|
485
510
|
const entry = `[${(/* @__PURE__ */ new Date()).toISOString()}] [${context}] ${error instanceof Error ? error.stack : String(error)}
|
|
486
511
|
`;
|
|
487
512
|
try {
|
|
488
|
-
|
|
513
|
+
import_fs5.default.appendFileSync(DEBUG_LOG_PATH, entry, { mode: 384 });
|
|
489
514
|
} catch {
|
|
490
515
|
}
|
|
491
516
|
}
|
|
@@ -495,8 +520,8 @@ var USER_REGISTRY_PATH = import_path4.default.join(import_os2.default.homedir(),
|
|
|
495
520
|
function loadAIToolRegistry() {
|
|
496
521
|
let userEntries = [];
|
|
497
522
|
try {
|
|
498
|
-
if (
|
|
499
|
-
const raw =
|
|
523
|
+
if (import_fs6.default.existsSync(USER_REGISTRY_PATH)) {
|
|
524
|
+
const raw = import_fs6.default.readFileSync(USER_REGISTRY_PATH, "utf-8");
|
|
500
525
|
const parsed = JSON.parse(raw);
|
|
501
526
|
if (Array.isArray(parsed)) {
|
|
502
527
|
userEntries = parsed.filter((entry) => typeof entry.tool === "string" && typeof entry.pattern === "string").map((entry) => ({
|
|
@@ -518,11 +543,11 @@ function loadAIToolRegistry() {
|
|
|
518
543
|
}
|
|
519
544
|
|
|
520
545
|
// src/profiles/loader.ts
|
|
521
|
-
var
|
|
546
|
+
var import_fs9 = __toESM(require("fs"));
|
|
522
547
|
var import_path6 = __toESM(require("path"));
|
|
523
548
|
|
|
524
549
|
// src/config/index.ts
|
|
525
|
-
var
|
|
550
|
+
var import_fs7 = __toESM(require("fs"));
|
|
526
551
|
var DEFAULTS = {
|
|
527
552
|
ai: {
|
|
528
553
|
provider: "",
|
|
@@ -547,9 +572,9 @@ var DEFAULTS = {
|
|
|
547
572
|
}
|
|
548
573
|
};
|
|
549
574
|
function readConfig() {
|
|
550
|
-
if (!
|
|
575
|
+
if (!import_fs7.default.existsSync(CONFIG_PATH)) return { ...DEFAULTS };
|
|
551
576
|
try {
|
|
552
|
-
const raw =
|
|
577
|
+
const raw = import_fs7.default.readFileSync(CONFIG_PATH, "utf-8");
|
|
553
578
|
const parsed = JSON.parse(raw);
|
|
554
579
|
return deepMerge(DEFAULTS, parsed);
|
|
555
580
|
} catch {
|
|
@@ -557,9 +582,9 @@ function readConfig() {
|
|
|
557
582
|
}
|
|
558
583
|
}
|
|
559
584
|
function writeConfig(config) {
|
|
560
|
-
|
|
585
|
+
import_fs7.default.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { encoding: "utf-8", mode: 384 });
|
|
561
586
|
try {
|
|
562
|
-
|
|
587
|
+
import_fs7.default.chmodSync(CONFIG_PATH, 384);
|
|
563
588
|
} catch {
|
|
564
589
|
}
|
|
565
590
|
}
|
|
@@ -624,21 +649,57 @@ function isValidManifest(m) {
|
|
|
624
649
|
}
|
|
625
650
|
|
|
626
651
|
// src/profiles/fetcher.ts
|
|
627
|
-
var
|
|
652
|
+
var import_fs8 = __toESM(require("fs"));
|
|
628
653
|
var import_path5 = __toESM(require("path"));
|
|
654
|
+
|
|
655
|
+
// src/utils/fetch.ts
|
|
656
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
657
|
+
var NETWORK_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
658
|
+
"ENOTFOUND",
|
|
659
|
+
"ECONNREFUSED",
|
|
660
|
+
"ECONNRESET",
|
|
661
|
+
"ETIMEDOUT",
|
|
662
|
+
"EHOSTUNREACH",
|
|
663
|
+
"ENETUNREACH"
|
|
664
|
+
]);
|
|
665
|
+
function isNetworkError(err) {
|
|
666
|
+
if (!(err instanceof Error)) return false;
|
|
667
|
+
const cause = err.cause;
|
|
668
|
+
const code = err.code ?? (cause instanceof Error ? cause.code : void 0);
|
|
669
|
+
return NETWORK_ERROR_CODES.has(code ?? "");
|
|
670
|
+
}
|
|
671
|
+
async function fetchWithTimeout(url, init, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
672
|
+
const controller = new AbortController();
|
|
673
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
674
|
+
try {
|
|
675
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
676
|
+
} catch (err) {
|
|
677
|
+
if (err.name === "AbortError") {
|
|
678
|
+
throw new Error(`Request timed out after ${timeoutMs / 1e3}s`);
|
|
679
|
+
}
|
|
680
|
+
if (isNetworkError(err)) {
|
|
681
|
+
throw new Error("No internet connection \u2014 try again when you're online");
|
|
682
|
+
}
|
|
683
|
+
throw err;
|
|
684
|
+
} finally {
|
|
685
|
+
clearTimeout(timer);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// src/profiles/fetcher.ts
|
|
629
690
|
var REGISTRY_BASE = "https://raw.githubusercontent.com/workscribe/profiles/main";
|
|
630
691
|
function manifestCachePath() {
|
|
631
692
|
return import_path5.default.join(PROFILES_DIR, "manifest.json");
|
|
632
693
|
}
|
|
633
694
|
async function fetchManifest() {
|
|
634
|
-
const response = await
|
|
695
|
+
const response = await fetchWithTimeout(`${REGISTRY_BASE}/manifest.json`, {});
|
|
635
696
|
if (!response.ok) throw new Error(`Registry unreachable (${response.status})`);
|
|
636
697
|
const data = await response.json();
|
|
637
698
|
if (!isValidManifest(data)) throw new Error("Manifest format unrecognised");
|
|
638
699
|
return data;
|
|
639
700
|
}
|
|
640
701
|
async function fetchProfile(name) {
|
|
641
|
-
const response = await
|
|
702
|
+
const response = await fetchWithTimeout(`${REGISTRY_BASE}/${name}.json`, {});
|
|
642
703
|
if (response.status === 404) throw new Error(`Profile "${name}" not found in registry`);
|
|
643
704
|
if (!response.ok) throw new Error(`Registry unreachable (${response.status})`);
|
|
644
705
|
const data = await response.json();
|
|
@@ -649,27 +710,27 @@ async function installProfile(name) {
|
|
|
649
710
|
ensureProfilesDir();
|
|
650
711
|
const profile = await fetchProfile(name);
|
|
651
712
|
const dest = import_path5.default.join(PROFILES_DIR, `${name}.json`);
|
|
652
|
-
|
|
713
|
+
import_fs8.default.writeFileSync(dest, JSON.stringify(profile, null, 2), { encoding: "utf-8", mode: 384 });
|
|
653
714
|
debugLog("profiles", `Installed profile "${name}" to ${dest}`);
|
|
654
715
|
}
|
|
655
716
|
async function cacheManifest() {
|
|
656
717
|
ensureProfilesDir();
|
|
657
718
|
const manifest = await fetchManifest();
|
|
658
|
-
|
|
719
|
+
import_fs8.default.writeFileSync(manifestCachePath(), JSON.stringify(manifest, null, 2), { encoding: "utf-8", mode: 384 });
|
|
659
720
|
return manifest;
|
|
660
721
|
}
|
|
661
722
|
function readCachedManifest() {
|
|
662
|
-
if (!
|
|
723
|
+
if (!import_fs8.default.existsSync(manifestCachePath())) return null;
|
|
663
724
|
try {
|
|
664
|
-
const data = JSON.parse(
|
|
725
|
+
const data = JSON.parse(import_fs8.default.readFileSync(manifestCachePath(), "utf-8"));
|
|
665
726
|
return isValidManifest(data) ? data : null;
|
|
666
727
|
} catch {
|
|
667
728
|
return null;
|
|
668
729
|
}
|
|
669
730
|
}
|
|
670
731
|
function ensureProfilesDir() {
|
|
671
|
-
if (!
|
|
672
|
-
|
|
732
|
+
if (!import_fs8.default.existsSync(PROFILES_DIR)) {
|
|
733
|
+
import_fs8.default.mkdirSync(PROFILES_DIR, { recursive: true, mode: 448 });
|
|
673
734
|
}
|
|
674
735
|
}
|
|
675
736
|
|
|
@@ -696,13 +757,13 @@ function compileRule(profileName, rule) {
|
|
|
696
757
|
}
|
|
697
758
|
function loadProfileFromDisk(name) {
|
|
698
759
|
const filePath = import_path6.default.join(PROFILES_DIR, `${name}.json`);
|
|
699
|
-
if (!
|
|
760
|
+
if (!import_fs9.default.existsSync(filePath)) {
|
|
700
761
|
debugLog("profiles", `Profile "${name}" not found in cache \u2014 run: workscribe profile add ${name}`);
|
|
701
762
|
return null;
|
|
702
763
|
}
|
|
703
764
|
let candidate;
|
|
704
765
|
try {
|
|
705
|
-
candidate = JSON.parse(
|
|
766
|
+
candidate = JSON.parse(import_fs9.default.readFileSync(filePath, "utf-8"));
|
|
706
767
|
} catch {
|
|
707
768
|
debugLog("profiles", `Profile "${name}" contains invalid JSON \u2014 skipping`);
|
|
708
769
|
return null;
|
|
@@ -734,8 +795,8 @@ function getActiveProfileAiContexts() {
|
|
|
734
795
|
});
|
|
735
796
|
}
|
|
736
797
|
function getCachedProfiles() {
|
|
737
|
-
if (!
|
|
738
|
-
return
|
|
798
|
+
if (!import_fs9.default.existsSync(PROFILES_DIR)) return [];
|
|
799
|
+
return import_fs9.default.readdirSync(PROFILES_DIR).filter((file) => file.endsWith(".json") && file !== "manifest.json").flatMap((file) => {
|
|
739
800
|
const profile = loadProfileFromDisk(file.replace(/\.json$/, ""));
|
|
740
801
|
return profile ? [profile] : [];
|
|
741
802
|
});
|
|
@@ -747,7 +808,7 @@ function detectProfilesForDirectory(dir) {
|
|
|
747
808
|
return manifest.filter((entry) => {
|
|
748
809
|
if (activeNames.has(entry.name)) return false;
|
|
749
810
|
const indicatorFiles = entry.detect?.files ?? [];
|
|
750
|
-
return indicatorFiles.length > 0 && indicatorFiles.every((file) =>
|
|
811
|
+
return indicatorFiles.length > 0 && indicatorFiles.every((file) => import_fs9.default.existsSync(import_path6.default.join(dir, file)));
|
|
751
812
|
});
|
|
752
813
|
}
|
|
753
814
|
|
|
@@ -1015,7 +1076,7 @@ function isDuplicate(command, cwd, lastEvent, dedupWindowMs = 5e3, now = Date.no
|
|
|
1015
1076
|
|
|
1016
1077
|
// src/capture/index.ts
|
|
1017
1078
|
function captureCommand(opts) {
|
|
1018
|
-
if (
|
|
1079
|
+
if (import_fs10.default.existsSync(PAUSE_FLAG_PATH)) return;
|
|
1019
1080
|
const config = readConfig();
|
|
1020
1081
|
const { command, cwd, exitCode, durationMs } = opts;
|
|
1021
1082
|
if (shouldFilter(command, config.capture.ignore)) return;
|
|
@@ -1059,8 +1120,11 @@ function captureCommand(opts) {
|
|
|
1059
1120
|
}
|
|
1060
1121
|
|
|
1061
1122
|
// src/cli/capture.ts
|
|
1123
|
+
var CAPTURE_HANG_TIMEOUT_MS = 5e3;
|
|
1062
1124
|
function registerCapture(program2) {
|
|
1063
1125
|
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) => {
|
|
1126
|
+
const hangGuard = setTimeout(() => process.exit(0), CAPTURE_HANG_TIMEOUT_MS);
|
|
1127
|
+
hangGuard.unref();
|
|
1064
1128
|
try {
|
|
1065
1129
|
if (!opts.cmd || !opts.cwd) return;
|
|
1066
1130
|
captureCommand({
|
|
@@ -1113,188 +1177,723 @@ function registerHook(program2) {
|
|
|
1113
1177
|
}
|
|
1114
1178
|
|
|
1115
1179
|
// src/cli/init.ts
|
|
1116
|
-
var
|
|
1180
|
+
var import_fs13 = __toESM(require("fs"));
|
|
1117
1181
|
var import_path9 = __toESM(require("path"));
|
|
1118
|
-
var
|
|
1182
|
+
var import_os5 = __toESM(require("os"));
|
|
1119
1183
|
var import_prompts = __toESM(require("prompts"));
|
|
1120
1184
|
var import_chalk2 = __toESM(require("chalk"));
|
|
1121
1185
|
|
|
1122
|
-
// src/
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
function getInstalledHookBlock(rcFile) {
|
|
1129
|
-
if (!import_fs10.default.existsSync(rcFile)) return null;
|
|
1130
|
-
const content = import_fs10.default.readFileSync(rcFile, "utf-8");
|
|
1131
|
-
const startIdx = content.indexOf(HOOK_START);
|
|
1132
|
-
const endIdx = content.indexOf(HOOK_END);
|
|
1133
|
-
if (startIdx === -1 || endIdx === -1) return null;
|
|
1134
|
-
return content.slice(startIdx, endIdx + HOOK_END.length);
|
|
1186
|
+
// src/utils/format.ts
|
|
1187
|
+
function formatDuration(minutes) {
|
|
1188
|
+
if (minutes < 60) return `${Math.round(minutes)}m`;
|
|
1189
|
+
const h = Math.floor(minutes / 60);
|
|
1190
|
+
const m = Math.round(minutes % 60);
|
|
1191
|
+
return m > 0 ? `${h}h ${m}m` : `${h}h`;
|
|
1135
1192
|
}
|
|
1136
|
-
function
|
|
1137
|
-
return
|
|
1193
|
+
function formatDate(isoString) {
|
|
1194
|
+
return new Date(isoString).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
1138
1195
|
}
|
|
1139
|
-
function
|
|
1140
|
-
|
|
1141
|
-
if (!installed) return false;
|
|
1142
|
-
return installed.trim() !== hookContent.trim();
|
|
1196
|
+
function todayDate() {
|
|
1197
|
+
return localDateStr(/* @__PURE__ */ new Date());
|
|
1143
1198
|
}
|
|
1144
|
-
function
|
|
1145
|
-
const
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
import_fs10.default.appendFileSync(rcFile, `
|
|
1149
|
-
${hookContent}
|
|
1150
|
-
`);
|
|
1199
|
+
function parseDate(input) {
|
|
1200
|
+
const d = /* @__PURE__ */ new Date(input + "T00:00:00");
|
|
1201
|
+
if (isNaN(d.getTime())) throw new Error(`Invalid date: ${input}`);
|
|
1202
|
+
return input;
|
|
1151
1203
|
}
|
|
1152
|
-
function
|
|
1153
|
-
const
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
const startIdx = content.indexOf(HOOK_START);
|
|
1158
|
-
const endIdx = content.indexOf(HOOK_END) + HOOK_END.length;
|
|
1159
|
-
import_fs10.default.writeFileSync(rcFile, content.slice(0, startIdx) + hookContent + content.slice(endIdx));
|
|
1204
|
+
function localDateStr(d) {
|
|
1205
|
+
const y = d.getFullYear();
|
|
1206
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
1207
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
1208
|
+
return `${y}-${m}-${day}`;
|
|
1160
1209
|
}
|
|
1161
|
-
function
|
|
1162
|
-
const
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
const endIdx = content.indexOf(HOOK_END) + HOOK_END.length;
|
|
1168
|
-
const preambleIdx = content.lastIndexOf(HOOK_PREAMBLE, startIdx);
|
|
1169
|
-
const blockStart = preambleIdx !== -1 ? preambleIdx : startIdx;
|
|
1170
|
-
const before = content.slice(0, blockStart).replace(/\n+$/, "\n");
|
|
1171
|
-
const after = content.slice(endIdx).replace(/^\n/, "");
|
|
1172
|
-
import_fs10.default.writeFileSync(rcFile, before + after);
|
|
1210
|
+
function startOfWeek(dateStr) {
|
|
1211
|
+
const d = /* @__PURE__ */ new Date(dateStr + "T00:00:00");
|
|
1212
|
+
const day = d.getDay();
|
|
1213
|
+
const diff = day === 0 ? -6 : 1 - day;
|
|
1214
|
+
d.setDate(d.getDate() + diff);
|
|
1215
|
+
return localDateStr(d);
|
|
1173
1216
|
}
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
var import_child_process3 = require("child_process");
|
|
1182
|
-
function isCommandInstalled(cmd) {
|
|
1183
|
-
try {
|
|
1184
|
-
(0, import_child_process3.execSync)(`which ${cmd}`, { stdio: "ignore" });
|
|
1185
|
-
return true;
|
|
1186
|
-
} catch {
|
|
1187
|
-
return false;
|
|
1217
|
+
function eachDayOfRange(start, end) {
|
|
1218
|
+
const days = [];
|
|
1219
|
+
const cur = /* @__PURE__ */ new Date(start + "T00:00:00");
|
|
1220
|
+
const last = /* @__PURE__ */ new Date(end + "T00:00:00");
|
|
1221
|
+
while (cur <= last) {
|
|
1222
|
+
days.push(localDateStr(cur));
|
|
1223
|
+
cur.setDate(cur.getDate() + 1);
|
|
1188
1224
|
}
|
|
1225
|
+
return days;
|
|
1189
1226
|
}
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
try {
|
|
1196
|
-
return JSON.parse(import_fs11.default.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8"));
|
|
1197
|
-
} catch {
|
|
1198
|
-
return {};
|
|
1199
|
-
}
|
|
1227
|
+
function isoWeekKey(dateStr) {
|
|
1228
|
+
const d = /* @__PURE__ */ new Date(dateStr + "T00:00:00");
|
|
1229
|
+
const jan4 = new Date(d.getFullYear(), 0, 4);
|
|
1230
|
+
const weekNum = Math.ceil(((d.getTime() - jan4.getTime()) / 864e5 + jan4.getDay() + 1) / 7);
|
|
1231
|
+
return `${d.getFullYear()}-W${String(weekNum).padStart(2, "0")}`;
|
|
1200
1232
|
}
|
|
1201
|
-
var claudeCodeIntegration = {
|
|
1202
|
-
displayName: "Claude Code",
|
|
1203
|
-
isInstalled() {
|
|
1204
|
-
return isCommandInstalled("claude");
|
|
1205
|
-
},
|
|
1206
|
-
isHookInstalled() {
|
|
1207
|
-
const settings = readSettings();
|
|
1208
|
-
const postToolUse = settings.hooks?.PostToolUse ?? [];
|
|
1209
|
-
return postToolUse.some((entry) => {
|
|
1210
|
-
const hooks = entry.hooks ?? [];
|
|
1211
|
-
return hooks.some((hookItem) => {
|
|
1212
|
-
const hook = hookItem;
|
|
1213
|
-
return typeof hook.command === "string" && hook.command.includes("workscribe _hook");
|
|
1214
|
-
});
|
|
1215
|
-
});
|
|
1216
|
-
},
|
|
1217
|
-
install() {
|
|
1218
|
-
const settings = readSettings();
|
|
1219
|
-
const existingHooks = settings.hooks ?? {};
|
|
1220
|
-
const existingPostToolUse = existingHooks.PostToolUse ?? [];
|
|
1221
|
-
settings.hooks = {
|
|
1222
|
-
...existingHooks,
|
|
1223
|
-
PostToolUse: [
|
|
1224
|
-
...existingPostToolUse,
|
|
1225
|
-
{
|
|
1226
|
-
matcher: "Bash",
|
|
1227
|
-
hooks: [{ type: "command", command: "workscribe _hook --source claude-code", async: true }]
|
|
1228
|
-
}
|
|
1229
|
-
]
|
|
1230
|
-
};
|
|
1231
|
-
import_fs11.default.mkdirSync(import_path8.default.dirname(CLAUDE_SETTINGS_PATH), { recursive: true });
|
|
1232
|
-
import_fs11.default.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
1233
|
-
}
|
|
1234
|
-
};
|
|
1235
1233
|
|
|
1236
|
-
// src/
|
|
1237
|
-
var
|
|
1238
|
-
"claude-code": claudeCodeIntegration
|
|
1239
|
-
};
|
|
1234
|
+
// src/ai/prompt.ts
|
|
1235
|
+
var SYSTEM_PROMPT = `You write short developer work summaries. Focus on what the developer accomplished and what changed \u2014 not which tools or commands were used.
|
|
1240
1236
|
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1237
|
+
Here is an example:
|
|
1238
|
+
|
|
1239
|
+
INPUT:
|
|
1240
|
+
Project: api-service
|
|
1241
|
+
Branch: feature/auth
|
|
1242
|
+
Duration: 2h 00m
|
|
1243
|
+
Events:
|
|
1244
|
+
- versioning {"branch":"feature/auth"}
|
|
1245
|
+
- 2x dependencies {"tool":"npm","packages":["bcrypt","jsonwebtoken"]}
|
|
1246
|
+
- 8x testing [failed]
|
|
1247
|
+
- testing
|
|
1248
|
+
- 2x files {"tool":"mkdir"}
|
|
1249
|
+
- shipping {"message":"fix: auth token validation"}
|
|
1250
|
+
- shipping {"remote":"origin","branch":"feature/auth"}
|
|
1251
|
+
|
|
1252
|
+
OUTPUT:
|
|
1253
|
+
- Added authentication dependencies (bcrypt, JWT)
|
|
1254
|
+
- Debugged and resolved failing auth tests
|
|
1255
|
+
- Committed and pushed the token validation fix
|
|
1256
|
+
|
|
1257
|
+
RULES \u2014 follow these exactly:
|
|
1258
|
+
|
|
1259
|
+
1. Interpret repeated events as patterns, not individual steps.
|
|
1260
|
+
- Many testing [failed] followed by a passing test = debugging a failure. Say "resolved failing X tests", not "ran tests multiple times".
|
|
1261
|
+
- Many versioning events = branch housekeeping. Fold into context, do not list each git command.
|
|
1262
|
+
|
|
1263
|
+
2. Omit utility commands entirely. File size checks, file reads, grep searches, and cat commands are research steps \u2014 they are not outcomes and must not appear in the summary.
|
|
1264
|
+
|
|
1265
|
+
3. Do not name the AI tool. For ai_session events: focus on what was built or committed. If a commits array is present in metadata, use those commit messages. Never write "via Claude Code", "using Claude Code", or similar \u2014 omit the tool name entirely.
|
|
1266
|
+
|
|
1267
|
+
4. For other events with a hint field: describe the developer's intent, not the raw command.
|
|
1268
|
+
|
|
1269
|
+
5. For dependencies events: infer the purpose from package names and describe the domain (bcrypt/jsonwebtoken \u2192 authentication, prisma/pg \u2192 database, jest/vitest \u2192 testing). For unfamiliar packages, name them directly.
|
|
1270
|
+
|
|
1271
|
+
6. Fold supporting events into the narrative. Building, shipping, versioning, and file operations are context \u2014 only make them a bullet when they are the primary activity (e.g. a standalone build fix or deploy with no accompanying commits).
|
|
1272
|
+
|
|
1273
|
+
7. Stick to what the events show. Do not infer subject matter from the branch name. Do not add details not derivable from the events.
|
|
1274
|
+
|
|
1275
|
+
Maximum 5 bullets. Output ONLY the bullet points, nothing else.`;
|
|
1276
|
+
function getSystemPrompt() {
|
|
1277
|
+
const contexts = getActiveProfileAiContexts();
|
|
1278
|
+
if (contexts.length === 0) return SYSTEM_PROMPT;
|
|
1279
|
+
return SYSTEM_PROMPT + "\n\n" + contexts.join("\n");
|
|
1247
1280
|
}
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1281
|
+
var DEFAULT_EXCLUDED_CATEGORIES = /* @__PURE__ */ new Set(["exploring", "note", "linting"]);
|
|
1282
|
+
function buildExcludedSet(includeCategories = []) {
|
|
1283
|
+
const excluded = new Set(DEFAULT_EXCLUDED_CATEGORIES);
|
|
1284
|
+
for (const cat of includeCategories) excluded.delete(cat);
|
|
1285
|
+
return excluded;
|
|
1286
|
+
}
|
|
1287
|
+
function countExcludedEvents(sessions, excluded) {
|
|
1288
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1289
|
+
for (const session of sessions) {
|
|
1290
|
+
for (const event of session.events) {
|
|
1291
|
+
if (excluded.has(event.category)) {
|
|
1292
|
+
counts.set(event.category, (counts.get(event.category) ?? 0) + 1);
|
|
1258
1293
|
}
|
|
1259
1294
|
}
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
if (
|
|
1272
|
-
|
|
1273
|
-
} else {
|
|
1274
|
-
const hookContent = import_fs12.default.readFileSync(hookFile, "utf-8");
|
|
1275
|
-
if (!import_fs12.default.existsSync(shell.rcFile)) import_fs12.default.writeFileSync(shell.rcFile, "");
|
|
1276
|
-
if (!isHookInstalled(shell.rcFile)) {
|
|
1277
|
-
installHook(shell.rcFile, hookContent);
|
|
1278
|
-
log.success(`Shell hook installed in ${shell.rcFile}`);
|
|
1279
|
-
} else if (isHookOutdated(shell.rcFile, hookContent)) {
|
|
1280
|
-
const { update } = await (0, import_prompts.default)({
|
|
1281
|
-
type: "confirm",
|
|
1282
|
-
name: "update",
|
|
1283
|
-
message: `Shell hook in ${shell.rcFile} is outdated. Update?`,
|
|
1284
|
-
initial: true
|
|
1285
|
-
});
|
|
1286
|
-
if (update) {
|
|
1287
|
-
updateHook(shell.rcFile, hookContent);
|
|
1288
|
-
log.success(`Shell hook updated in ${shell.rcFile}`);
|
|
1289
|
-
} else {
|
|
1290
|
-
log.info("Shell hook update skipped");
|
|
1291
|
-
}
|
|
1292
|
-
} else {
|
|
1293
|
-
log.info(`Shell hook up to date in ${shell.rcFile}`);
|
|
1294
|
-
}
|
|
1295
|
+
}
|
|
1296
|
+
return counts;
|
|
1297
|
+
}
|
|
1298
|
+
function buildUserMessage(payload, includeCategories = []) {
|
|
1299
|
+
const excluded = buildExcludedSet(includeCategories);
|
|
1300
|
+
const lines = [`Date: ${payload.date}`, ""];
|
|
1301
|
+
for (const session of payload.sessions) {
|
|
1302
|
+
lines.push(`Project: ${session.project ?? "unknown"}`);
|
|
1303
|
+
if (session.branch) lines.push(`Branch: ${session.branch}`);
|
|
1304
|
+
lines.push(`Duration: ${formatDuration(session.durationMinutes)}`);
|
|
1305
|
+
for (const event of session.events) {
|
|
1306
|
+
if (event.category === "note") {
|
|
1307
|
+
lines.push(`NOTE: ${event.metadata.text}`);
|
|
1295
1308
|
}
|
|
1296
1309
|
}
|
|
1297
|
-
|
|
1310
|
+
lines.push("Events:");
|
|
1311
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1312
|
+
for (const event of session.events) {
|
|
1313
|
+
if (excluded.has(event.category)) continue;
|
|
1314
|
+
const failed = event.exitCode !== null && event.exitCode !== 0 ? " [failed]" : "";
|
|
1315
|
+
let key;
|
|
1316
|
+
if (event.hint) {
|
|
1317
|
+
key = `other [${event.hint}]${failed}`;
|
|
1318
|
+
} else {
|
|
1319
|
+
const meta = Object.keys(event.metadata).length > 0 ? ` ${JSON.stringify(event.metadata)}` : "";
|
|
1320
|
+
key = `${event.category}${meta}${failed}`;
|
|
1321
|
+
}
|
|
1322
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
1323
|
+
}
|
|
1324
|
+
for (const [key, count] of counts) {
|
|
1325
|
+
lines.push(` - ${count > 1 ? `${count}x ` : ""}${key}`);
|
|
1326
|
+
}
|
|
1327
|
+
lines.push("");
|
|
1328
|
+
}
|
|
1329
|
+
return lines.join("\n");
|
|
1330
|
+
}
|
|
1331
|
+
var WEEKLY_SYSTEM_PROMPT = `You write short weekly developer summaries. Here is an example:
|
|
1332
|
+
|
|
1333
|
+
INPUT:
|
|
1334
|
+
Week: 2026-05-18 to 2026-05-22
|
|
1335
|
+
|
|
1336
|
+
Monday 2026-05-18:
|
|
1337
|
+
Project: api-service (feature/auth) \u2014 2h 10m
|
|
1338
|
+
Events: 3x code_commit, 2x test_run, dependency_install
|
|
1339
|
+
|
|
1340
|
+
Tuesday 2026-05-19:
|
|
1341
|
+
Project: api-service (feature/auth) \u2014 1h 30m
|
|
1342
|
+
Events: 2x code_commit, test_run, build
|
|
1343
|
+
|
|
1344
|
+
Wednesday 2026-05-20:
|
|
1345
|
+
Project: dashboard (main) \u2014 3h 00m
|
|
1346
|
+
Events: 4x code_commit, 2x build, test_run
|
|
1347
|
+
|
|
1348
|
+
Thursday 2026-05-21:
|
|
1349
|
+
Project: api-service (feature/auth) \u2014 1h 00m
|
|
1350
|
+
Events: code_commit {"message":"feat: merge auth"}, branch_switch
|
|
1351
|
+
|
|
1352
|
+
Friday 2026-05-22:
|
|
1353
|
+
Project: dashboard (main) \u2014 2h 00m
|
|
1354
|
+
Events: 3x code_commit, build
|
|
1355
|
+
|
|
1356
|
+
OUTPUT:
|
|
1357
|
+
## Week of 18 May \u2014 9h 40m across 2 projects
|
|
1358
|
+
|
|
1359
|
+
**api-service** (Mon\u2013Thu): Built and shipped the auth feature \u2014 JWT setup, token validation fixes, and final merge.
|
|
1360
|
+
**dashboard** (Wed, Fri): Active development across 7 commits with builds passing.
|
|
1361
|
+
|
|
1362
|
+
Now summarise the following week. Output ONLY the summary \u2014 a one-line header then one bullet per active project. Maximum 4 bullets.`;
|
|
1363
|
+
var DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
1364
|
+
function buildWeeklyMessage(payload) {
|
|
1365
|
+
const lines = [`Week: ${payload.weekStart} to ${payload.weekEnd}`, ""];
|
|
1366
|
+
for (const { date, sessions } of payload.days) {
|
|
1367
|
+
const dayName = DAY_NAMES[(/* @__PURE__ */ new Date(date + "T00:00:00")).getDay()];
|
|
1368
|
+
lines.push(`${dayName} ${date}:`);
|
|
1369
|
+
for (const session of sessions) {
|
|
1370
|
+
const project = session.project ?? "unknown";
|
|
1371
|
+
const branch = session.branch ? ` (${session.branch})` : "";
|
|
1372
|
+
lines.push(` Project: ${project}${branch} \u2014 ${formatDuration(session.durationMinutes)}`);
|
|
1373
|
+
for (const event of session.events) {
|
|
1374
|
+
if (event.category === "note") {
|
|
1375
|
+
lines.push(` NOTE: ${event.metadata.text}`);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
const excluded = buildExcludedSet();
|
|
1379
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1380
|
+
for (const event of session.events) {
|
|
1381
|
+
if (excluded.has(event.category)) continue;
|
|
1382
|
+
const failed = event.exitCode !== null && event.exitCode !== 0 ? " [failed]" : "";
|
|
1383
|
+
let key;
|
|
1384
|
+
if (event.hint) {
|
|
1385
|
+
key = `other [${event.hint}]${failed}`;
|
|
1386
|
+
} else {
|
|
1387
|
+
const meta = Object.keys(event.metadata).length > 0 ? ` ${JSON.stringify(event.metadata)}` : "";
|
|
1388
|
+
key = `${event.category}${meta}${failed}`;
|
|
1389
|
+
}
|
|
1390
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
1391
|
+
}
|
|
1392
|
+
const eventLine = [...counts.entries()].map(([key, count]) => `${count > 1 ? `${count}x ` : ""}${key}`).join(", ");
|
|
1393
|
+
lines.push(` Events: ${eventLine}`);
|
|
1394
|
+
}
|
|
1395
|
+
lines.push("");
|
|
1396
|
+
}
|
|
1397
|
+
return lines.join("\n");
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
// src/ai/generic-provider.ts
|
|
1401
|
+
var GenericProvider = class {
|
|
1402
|
+
constructor(config, model, apiKey, baseUrl, includeCategories = []) {
|
|
1403
|
+
this.config = config;
|
|
1404
|
+
this.model = model;
|
|
1405
|
+
this.apiKey = apiKey;
|
|
1406
|
+
this.baseUrl = baseUrl;
|
|
1407
|
+
this.includeCategories = includeCategories;
|
|
1408
|
+
}
|
|
1409
|
+
config;
|
|
1410
|
+
model;
|
|
1411
|
+
apiKey;
|
|
1412
|
+
baseUrl;
|
|
1413
|
+
includeCategories;
|
|
1414
|
+
async call(system, userMsg, maxTokens) {
|
|
1415
|
+
const response = await fetchWithTimeout(this.config.chatUrl(this.baseUrl), {
|
|
1416
|
+
method: "POST",
|
|
1417
|
+
headers: this.config.headers(this.apiKey),
|
|
1418
|
+
body: JSON.stringify(this.config.buildChatBody(this.model, system, userMsg, maxTokens))
|
|
1419
|
+
});
|
|
1420
|
+
if (!response.ok) {
|
|
1421
|
+
throw new Error(this.config.chatError(response.status, await response.text()));
|
|
1422
|
+
}
|
|
1423
|
+
return this.config.parseText(await response.json());
|
|
1424
|
+
}
|
|
1425
|
+
async summarize(payload) {
|
|
1426
|
+
return this.call(getSystemPrompt(), buildUserMessage(payload, this.includeCategories), this.config.dailyTokens);
|
|
1427
|
+
}
|
|
1428
|
+
async summarizeWeek(payload) {
|
|
1429
|
+
return this.call(WEEKLY_SYSTEM_PROMPT, buildWeeklyMessage(payload), this.config.weeklyTokens);
|
|
1430
|
+
}
|
|
1431
|
+
async validate() {
|
|
1432
|
+
const isPost = this.config.validateMethod === "POST";
|
|
1433
|
+
const response = await fetchWithTimeout(this.config.validateUrl(this.baseUrl), {
|
|
1434
|
+
method: this.config.validateMethod,
|
|
1435
|
+
headers: isPost ? this.config.headers(this.apiKey) : void 0,
|
|
1436
|
+
body: isPost && this.config.buildValidateBody ? JSON.stringify(this.config.buildValidateBody(this.model)) : void 0
|
|
1437
|
+
});
|
|
1438
|
+
if (!response.ok) {
|
|
1439
|
+
throw new Error(this.config.validateError(response.status, await response.text(), this.baseUrl));
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
};
|
|
1443
|
+
|
|
1444
|
+
// src/ai/provider-config.ts
|
|
1445
|
+
var PROVIDER_CONFIGS = {
|
|
1446
|
+
anthropic: {
|
|
1447
|
+
defaultModel: "claude-haiku-4-5-20251001",
|
|
1448
|
+
defaultBaseUrl: "https://api.anthropic.com/v1",
|
|
1449
|
+
dailyTokens: 1024,
|
|
1450
|
+
weeklyTokens: 1024,
|
|
1451
|
+
chatUrl: (baseUrl) => `${baseUrl}/messages`,
|
|
1452
|
+
validateUrl: (baseUrl) => `${baseUrl}/messages`,
|
|
1453
|
+
validateMethod: "POST",
|
|
1454
|
+
headers: (apiKey) => ({
|
|
1455
|
+
"Content-Type": "application/json",
|
|
1456
|
+
"x-api-key": apiKey,
|
|
1457
|
+
"anthropic-version": "2023-06-01"
|
|
1458
|
+
}),
|
|
1459
|
+
buildChatBody: (model, system, userMsg, maxTokens) => ({
|
|
1460
|
+
model,
|
|
1461
|
+
max_tokens: maxTokens,
|
|
1462
|
+
system,
|
|
1463
|
+
messages: [{ role: "user", content: userMsg }]
|
|
1464
|
+
}),
|
|
1465
|
+
buildValidateBody: (model) => ({
|
|
1466
|
+
model,
|
|
1467
|
+
max_tokens: 1,
|
|
1468
|
+
messages: [{ role: "user", content: "." }]
|
|
1469
|
+
}),
|
|
1470
|
+
parseText: (json) => {
|
|
1471
|
+
const data = json;
|
|
1472
|
+
return data.content.find((b) => b.type === "text")?.text ?? "";
|
|
1473
|
+
},
|
|
1474
|
+
chatError: (status, body) => `Anthropic API error ${status}: ${body}`,
|
|
1475
|
+
validateError: (status, body) => `Anthropic API error ${status}: ${body}`
|
|
1476
|
+
},
|
|
1477
|
+
openai: {
|
|
1478
|
+
defaultModel: "gpt-4o-mini",
|
|
1479
|
+
defaultBaseUrl: "https://api.openai.com/v1",
|
|
1480
|
+
dailyTokens: 1024,
|
|
1481
|
+
weeklyTokens: 1024,
|
|
1482
|
+
chatUrl: (baseUrl) => `${baseUrl}/chat/completions`,
|
|
1483
|
+
validateUrl: (baseUrl) => `${baseUrl}/chat/completions`,
|
|
1484
|
+
validateMethod: "POST",
|
|
1485
|
+
headers: (apiKey) => ({
|
|
1486
|
+
"Content-Type": "application/json",
|
|
1487
|
+
Authorization: `Bearer ${apiKey}`
|
|
1488
|
+
}),
|
|
1489
|
+
buildChatBody: (model, system, userMsg, maxTokens) => ({
|
|
1490
|
+
model,
|
|
1491
|
+
messages: [
|
|
1492
|
+
{ role: "system", content: system },
|
|
1493
|
+
{ role: "user", content: userMsg }
|
|
1494
|
+
],
|
|
1495
|
+
max_tokens: maxTokens
|
|
1496
|
+
}),
|
|
1497
|
+
buildValidateBody: (model) => ({
|
|
1498
|
+
model,
|
|
1499
|
+
messages: [{ role: "user", content: "." }],
|
|
1500
|
+
max_tokens: 1
|
|
1501
|
+
}),
|
|
1502
|
+
parseText: (json) => {
|
|
1503
|
+
const data = json;
|
|
1504
|
+
return data.choices[0]?.message?.content ?? "";
|
|
1505
|
+
},
|
|
1506
|
+
chatError: (status, body) => `OpenAI API error ${status}: ${body}`,
|
|
1507
|
+
validateError: (status, body) => `OpenAI API error ${status}: ${body}`
|
|
1508
|
+
},
|
|
1509
|
+
ollama: {
|
|
1510
|
+
defaultModel: "llama3",
|
|
1511
|
+
defaultBaseUrl: "http://localhost:11434",
|
|
1512
|
+
dailyTokens: 300,
|
|
1513
|
+
weeklyTokens: 400,
|
|
1514
|
+
chatUrl: (baseUrl) => `${baseUrl}/api/chat`,
|
|
1515
|
+
validateUrl: (baseUrl) => `${baseUrl}/api/tags`,
|
|
1516
|
+
validateMethod: "GET",
|
|
1517
|
+
headers: () => ({ "Content-Type": "application/json" }),
|
|
1518
|
+
buildChatBody: (model, system, userMsg, maxTokens) => ({
|
|
1519
|
+
model,
|
|
1520
|
+
stream: false,
|
|
1521
|
+
messages: [
|
|
1522
|
+
{ role: "system", content: system },
|
|
1523
|
+
{ role: "user", content: userMsg }
|
|
1524
|
+
],
|
|
1525
|
+
options: { num_predict: maxTokens, temperature: 0.3 }
|
|
1526
|
+
}),
|
|
1527
|
+
parseText: (json) => {
|
|
1528
|
+
return json.message?.content ?? "";
|
|
1529
|
+
},
|
|
1530
|
+
chatError: (status, body) => `Ollama error ${status}: ${body}. Is Ollama running? Try: ollama serve`,
|
|
1531
|
+
validateError: (_status, _body, baseUrl) => `Ollama is not reachable at ${baseUrl}. Is it running? Try: ollama serve`
|
|
1532
|
+
}
|
|
1533
|
+
};
|
|
1534
|
+
|
|
1535
|
+
// src/ai/router.ts
|
|
1536
|
+
function getProvider(config) {
|
|
1537
|
+
const { provider, apiKey = "", model, baseUrl } = config.ai;
|
|
1538
|
+
if (!provider) {
|
|
1539
|
+
throw new Error(
|
|
1540
|
+
"No AI provider configured.\nRun: workscribe config set ai.provider <anthropic|openai|ollama|openai-compatible>"
|
|
1541
|
+
);
|
|
1542
|
+
}
|
|
1543
|
+
const configKey = provider === "openai-compatible" ? "openai" : provider;
|
|
1544
|
+
const providerConfig = PROVIDER_CONFIGS[configKey];
|
|
1545
|
+
if (!providerConfig) {
|
|
1546
|
+
throw new Error(
|
|
1547
|
+
`Unknown provider: "${provider}"
|
|
1548
|
+
Supported providers: anthropic, openai, ollama, openai-compatible`
|
|
1549
|
+
);
|
|
1550
|
+
}
|
|
1551
|
+
if (provider !== "ollama" && !apiKey) {
|
|
1552
|
+
throw new Error(`${provider} requires an API key.
|
|
1553
|
+
Run: workscribe config set ai.apiKey <your-key>`);
|
|
1554
|
+
}
|
|
1555
|
+
if (provider === "openai-compatible" && !baseUrl) {
|
|
1556
|
+
throw new Error("openai-compatible requires a base URL.\nRun: workscribe config set ai.baseUrl <url>");
|
|
1557
|
+
}
|
|
1558
|
+
return new GenericProvider(
|
|
1559
|
+
providerConfig,
|
|
1560
|
+
model || providerConfig.defaultModel,
|
|
1561
|
+
apiKey,
|
|
1562
|
+
baseUrl || providerConfig.defaultBaseUrl,
|
|
1563
|
+
config.summary?.includeCategories ?? []
|
|
1564
|
+
);
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
// src/capture/shell-hook.ts
|
|
1568
|
+
var import_fs11 = __toESM(require("fs"));
|
|
1569
|
+
var import_path7 = __toESM(require("path"));
|
|
1570
|
+
var import_os3 = __toESM(require("os"));
|
|
1571
|
+
|
|
1572
|
+
// src/utils/completion-scripts.ts
|
|
1573
|
+
function bashCompletion(commands) {
|
|
1574
|
+
const names = commands.map((command) => command.name).join(" ");
|
|
1575
|
+
return `_workscribe_completion() {
|
|
1576
|
+
local cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
1577
|
+
local prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
1578
|
+
local commands="${names}"
|
|
1579
|
+
case "$prev" in
|
|
1580
|
+
completions) COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") ); return ;;
|
|
1581
|
+
config) COMPREPLY=( $(compgen -W "get set list" -- "$cur") ); return ;;
|
|
1582
|
+
profile) COMPREPLY=( $(compgen -W "list add remove update detect suggest" -- "$cur") ); return ;;
|
|
1583
|
+
esac
|
|
1584
|
+
COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
|
|
1585
|
+
}
|
|
1586
|
+
complete -F _workscribe_completion workscribe`;
|
|
1587
|
+
}
|
|
1588
|
+
function zshCompletion(commands) {
|
|
1589
|
+
const commandList = commands.map((command) => ` '${command.name}:${command.description}'`).join("\n");
|
|
1590
|
+
return `#compdef workscribe
|
|
1591
|
+
_workscribe() {
|
|
1592
|
+
local state
|
|
1593
|
+
_arguments '1: :->command' '*: :->args'
|
|
1594
|
+
case $state in
|
|
1595
|
+
command)
|
|
1596
|
+
local -a commands
|
|
1597
|
+
commands=(
|
|
1598
|
+
${commandList}
|
|
1599
|
+
)
|
|
1600
|
+
_describe 'command' commands
|
|
1601
|
+
;;
|
|
1602
|
+
args)
|
|
1603
|
+
case $words[2] in
|
|
1604
|
+
completions) local -a shells; shells=('bash' 'zsh' 'fish'); _describe 'shell' shells ;;
|
|
1605
|
+
config) local -a sub; sub=('get:Get a value' 'set:Set a value' 'list:List all values'); _describe 'subcommand' sub ;;
|
|
1606
|
+
profile) local -a sub; sub=('list' 'add' 'remove' 'update' 'detect' 'suggest'); _describe 'subcommand' sub ;;
|
|
1607
|
+
esac
|
|
1608
|
+
;;
|
|
1609
|
+
esac
|
|
1610
|
+
}
|
|
1611
|
+
_workscribe`;
|
|
1612
|
+
}
|
|
1613
|
+
function fishCompletion(commands) {
|
|
1614
|
+
const lines = [
|
|
1615
|
+
"complete -c workscribe -f",
|
|
1616
|
+
...commands.map(
|
|
1617
|
+
(command) => `complete -c workscribe -n "__fish_use_subcommand" -a ${command.name.padEnd(12)} -d "${command.description}"`
|
|
1618
|
+
),
|
|
1619
|
+
'complete -c workscribe -n "__fish_seen_subcommand_from completions" -a "bash zsh fish"',
|
|
1620
|
+
'complete -c workscribe -n "__fish_seen_subcommand_from config" -a "get set list"',
|
|
1621
|
+
'complete -c workscribe -n "__fish_seen_subcommand_from profile" -a "list add remove update detect suggest"'
|
|
1622
|
+
];
|
|
1623
|
+
return lines.join("\n");
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
// src/capture/shell-hook.ts
|
|
1627
|
+
var FISH_COMPLETIONS_DIR = import_path7.default.join(import_os3.default.homedir(), ".config", "fish", "completions");
|
|
1628
|
+
var HOOK_START = "# workscribe:hook:start";
|
|
1629
|
+
var HOOK_END = "# workscribe:hook:end";
|
|
1630
|
+
var HOOK_PREAMBLE = "# Workscribe \u2014 developer work journal";
|
|
1631
|
+
function getInstalledHookBlock(rcFile) {
|
|
1632
|
+
if (!import_fs11.default.existsSync(rcFile)) return null;
|
|
1633
|
+
const content = import_fs11.default.readFileSync(rcFile, "utf-8");
|
|
1634
|
+
const startIdx = content.indexOf(HOOK_START);
|
|
1635
|
+
const endIdx = content.indexOf(HOOK_END);
|
|
1636
|
+
if (startIdx === -1 || endIdx === -1) return null;
|
|
1637
|
+
return content.slice(startIdx, endIdx + HOOK_END.length);
|
|
1638
|
+
}
|
|
1639
|
+
function isHookInstalled(rcFile) {
|
|
1640
|
+
return getInstalledHookBlock(rcFile) !== null;
|
|
1641
|
+
}
|
|
1642
|
+
function isHookOutdated(rcFile, hookContent) {
|
|
1643
|
+
const installed = getInstalledHookBlock(rcFile);
|
|
1644
|
+
if (!installed) return false;
|
|
1645
|
+
return installed.trim() !== hookContent.trim();
|
|
1646
|
+
}
|
|
1647
|
+
function installHook(rcFile, hookContent) {
|
|
1648
|
+
const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
|
|
1649
|
+
import_fs11.default.copyFileSync(rcFile, backup);
|
|
1650
|
+
log.info(`Backed up ${rcFile} \u2192 ${import_path7.default.basename(backup)}`);
|
|
1651
|
+
import_fs11.default.appendFileSync(rcFile, `
|
|
1652
|
+
${hookContent}
|
|
1653
|
+
`);
|
|
1654
|
+
}
|
|
1655
|
+
function updateHook(rcFile, hookContent) {
|
|
1656
|
+
const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
|
|
1657
|
+
import_fs11.default.copyFileSync(rcFile, backup);
|
|
1658
|
+
log.info(`Backed up ${rcFile} \u2192 ${import_path7.default.basename(backup)}`);
|
|
1659
|
+
const content = import_fs11.default.readFileSync(rcFile, "utf-8");
|
|
1660
|
+
const startIdx = content.indexOf(HOOK_START);
|
|
1661
|
+
const endIdx = content.indexOf(HOOK_END) + HOOK_END.length;
|
|
1662
|
+
import_fs11.default.writeFileSync(rcFile, content.slice(0, startIdx) + hookContent + content.slice(endIdx));
|
|
1663
|
+
}
|
|
1664
|
+
function removeHook(rcFile) {
|
|
1665
|
+
const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
|
|
1666
|
+
import_fs11.default.copyFileSync(rcFile, backup);
|
|
1667
|
+
log.info(`Backed up ${rcFile} \u2192 ${import_path7.default.basename(backup)}`);
|
|
1668
|
+
const content = import_fs11.default.readFileSync(rcFile, "utf-8");
|
|
1669
|
+
const startIdx = content.indexOf(HOOK_START);
|
|
1670
|
+
const endIdx = content.indexOf(HOOK_END) + HOOK_END.length;
|
|
1671
|
+
const preambleIdx = content.lastIndexOf(HOOK_PREAMBLE, startIdx);
|
|
1672
|
+
const blockStart = preambleIdx !== -1 ? preambleIdx : startIdx;
|
|
1673
|
+
const before = content.slice(0, blockStart).replace(/\n+$/, "\n");
|
|
1674
|
+
const after = content.slice(endIdx).replace(/^\n/, "");
|
|
1675
|
+
import_fs11.default.writeFileSync(rcFile, before + after);
|
|
1676
|
+
}
|
|
1677
|
+
function isCompletionsInstalled(shell) {
|
|
1678
|
+
if (shell.name === "fish") {
|
|
1679
|
+
return import_fs11.default.existsSync(import_path7.default.join(FISH_COMPLETIONS_DIR, "workscribe.fish"));
|
|
1680
|
+
}
|
|
1681
|
+
try {
|
|
1682
|
+
return import_fs11.default.readFileSync(shell.rcFile, "utf-8").includes("workscribe completions");
|
|
1683
|
+
} catch {
|
|
1684
|
+
return false;
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
function installCompletions(shell, commands) {
|
|
1688
|
+
if (shell.name === "fish") {
|
|
1689
|
+
if (!import_fs11.default.existsSync(FISH_COMPLETIONS_DIR)) {
|
|
1690
|
+
import_fs11.default.mkdirSync(FISH_COMPLETIONS_DIR, { recursive: true });
|
|
1691
|
+
}
|
|
1692
|
+
import_fs11.default.writeFileSync(import_path7.default.join(FISH_COMPLETIONS_DIR, "workscribe.fish"), fishCompletion(commands), { mode: 420 });
|
|
1693
|
+
} else {
|
|
1694
|
+
const script = shell.name === "zsh" ? zshCompletion(commands) : bashCompletion(commands);
|
|
1695
|
+
const cacheFile = import_path7.default.join(WORKSCRIBE_DIR, `completion.${shell.name}`);
|
|
1696
|
+
import_fs11.default.writeFileSync(cacheFile, script, { mode: 420 });
|
|
1697
|
+
const sourceLine = shell.name === "zsh" ? `
|
|
1698
|
+
# Workscribe completions
|
|
1699
|
+
autoload -Uz compinit && compinit
|
|
1700
|
+
source "${cacheFile}"
|
|
1701
|
+
` : `
|
|
1702
|
+
# Workscribe completions
|
|
1703
|
+
source "${cacheFile}"
|
|
1704
|
+
`;
|
|
1705
|
+
import_fs11.default.appendFileSync(shell.rcFile, sourceLine);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
// src/capture/hooks/claude-code.ts
|
|
1710
|
+
var import_fs12 = __toESM(require("fs"));
|
|
1711
|
+
var import_path8 = __toESM(require("path"));
|
|
1712
|
+
var import_os4 = __toESM(require("os"));
|
|
1713
|
+
|
|
1714
|
+
// src/capture/hooks/utils.ts
|
|
1715
|
+
var import_child_process3 = require("child_process");
|
|
1716
|
+
function isCommandInstalled(cmd) {
|
|
1717
|
+
try {
|
|
1718
|
+
(0, import_child_process3.execSync)(`which ${cmd}`, { stdio: "ignore" });
|
|
1719
|
+
return true;
|
|
1720
|
+
} catch {
|
|
1721
|
+
return false;
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
// src/capture/hooks/claude-code.ts
|
|
1726
|
+
var CLAUDE_SETTINGS_PATH = import_path8.default.join(import_os4.default.homedir(), ".claude", "settings.json");
|
|
1727
|
+
function readSettings() {
|
|
1728
|
+
if (!import_fs12.default.existsSync(CLAUDE_SETTINGS_PATH)) return {};
|
|
1729
|
+
try {
|
|
1730
|
+
return JSON.parse(import_fs12.default.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8"));
|
|
1731
|
+
} catch {
|
|
1732
|
+
return {};
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
var claudeCodeIntegration = {
|
|
1736
|
+
displayName: "Claude Code",
|
|
1737
|
+
isInstalled() {
|
|
1738
|
+
return isCommandInstalled("claude");
|
|
1739
|
+
},
|
|
1740
|
+
isHookInstalled() {
|
|
1741
|
+
const settings = readSettings();
|
|
1742
|
+
const postToolUse = settings.hooks?.PostToolUse ?? [];
|
|
1743
|
+
return postToolUse.some((entry) => {
|
|
1744
|
+
const hooks = entry.hooks ?? [];
|
|
1745
|
+
return hooks.some((hookItem) => {
|
|
1746
|
+
const hook = hookItem;
|
|
1747
|
+
return typeof hook.command === "string" && hook.command.includes("workscribe _hook");
|
|
1748
|
+
});
|
|
1749
|
+
});
|
|
1750
|
+
},
|
|
1751
|
+
install() {
|
|
1752
|
+
const settings = readSettings();
|
|
1753
|
+
const existingHooks = settings.hooks ?? {};
|
|
1754
|
+
const existingPostToolUse = existingHooks.PostToolUse ?? [];
|
|
1755
|
+
settings.hooks = {
|
|
1756
|
+
...existingHooks,
|
|
1757
|
+
PostToolUse: [
|
|
1758
|
+
...existingPostToolUse,
|
|
1759
|
+
{
|
|
1760
|
+
matcher: "Bash",
|
|
1761
|
+
hooks: [{ type: "command", command: "workscribe _hook --source claude-code", async: true }]
|
|
1762
|
+
}
|
|
1763
|
+
]
|
|
1764
|
+
};
|
|
1765
|
+
import_fs12.default.mkdirSync(import_path8.default.dirname(CLAUDE_SETTINGS_PATH), { recursive: true });
|
|
1766
|
+
import_fs12.default.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
1767
|
+
}
|
|
1768
|
+
};
|
|
1769
|
+
|
|
1770
|
+
// src/capture/hooks/index.ts
|
|
1771
|
+
var AI_HOOK_INTEGRATIONS = {
|
|
1772
|
+
"claude-code": claudeCodeIntegration
|
|
1773
|
+
};
|
|
1774
|
+
|
|
1775
|
+
// src/cli/completions.ts
|
|
1776
|
+
var HIDDEN_COMMANDS = /* @__PURE__ */ new Set(["_capture", "_hook"]);
|
|
1777
|
+
function getPublicCommands(program2) {
|
|
1778
|
+
return program2.commands.filter((command) => !HIDDEN_COMMANDS.has(command.name())).map((command) => ({ name: command.name(), description: command.description() }));
|
|
1779
|
+
}
|
|
1780
|
+
function registerCompletions(program2) {
|
|
1781
|
+
program2.command("completions [shell]").description("Print shell completion script (bash, zsh, fish)").action((shell) => {
|
|
1782
|
+
const detected = shell ?? (process.env.SHELL ?? "").split("/").at(-1);
|
|
1783
|
+
if (!detected || !["bash", "zsh", "fish"].includes(detected)) {
|
|
1784
|
+
log.error(`Unknown shell: "${detected ?? ""}". Supported: bash, zsh, fish`);
|
|
1785
|
+
log.plain(` Usage: workscribe completions <bash|zsh|fish>`);
|
|
1786
|
+
process.exit(1);
|
|
1787
|
+
}
|
|
1788
|
+
const commands = getPublicCommands(program2);
|
|
1789
|
+
const script = detected === "bash" ? bashCompletion(commands) : detected === "zsh" ? zshCompletion(commands) : fishCompletion(commands);
|
|
1790
|
+
process.stdout.write(script + "\n");
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
// src/cli/init.ts
|
|
1795
|
+
function detectShell() {
|
|
1796
|
+
const shell = process.env.SHELL ?? "";
|
|
1797
|
+
if (shell.includes("zsh")) return { name: "zsh", rcFile: import_path9.default.join(import_os5.default.homedir(), ".zshrc") };
|
|
1798
|
+
if (shell.includes("bash")) return { name: "bash", rcFile: import_path9.default.join(import_os5.default.homedir(), ".bashrc") };
|
|
1799
|
+
if (shell.includes("fish")) {
|
|
1800
|
+
const confDir = import_path9.default.join(import_os5.default.homedir(), ".config", "fish", "conf.d");
|
|
1801
|
+
return { name: "fish", rcFile: import_path9.default.join(confDir, "workscribe.fish") };
|
|
1802
|
+
}
|
|
1803
|
+
return null;
|
|
1804
|
+
}
|
|
1805
|
+
function step(current, total, label) {
|
|
1806
|
+
log.plain(import_chalk2.default.bold(`
|
|
1807
|
+
[${current}/${total}] ${label}`));
|
|
1808
|
+
}
|
|
1809
|
+
function registerInit(program2) {
|
|
1810
|
+
program2.command("init").description("Set up Workscribe and install or update shell hooks").action(async () => {
|
|
1811
|
+
const TOTAL_STEPS = 5;
|
|
1812
|
+
log.plain(import_chalk2.default.bold("\n Workscribe \u2014 Developer Work Journal\n"));
|
|
1813
|
+
step(1, TOTAL_STEPS, "Creating data directories");
|
|
1814
|
+
for (const dir of REQUIRED_DIRS) {
|
|
1815
|
+
if (!import_fs13.default.existsSync(dir)) {
|
|
1816
|
+
import_fs13.default.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
1817
|
+
log.success(`Created ${dir}`);
|
|
1818
|
+
} else {
|
|
1819
|
+
try {
|
|
1820
|
+
import_fs13.default.chmodSync(dir, 448);
|
|
1821
|
+
} catch {
|
|
1822
|
+
}
|
|
1823
|
+
log.info(`${dir} already exists`);
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
if (!import_fs13.default.existsSync(PROFILES_DIR)) {
|
|
1827
|
+
import_fs13.default.mkdirSync(PROFILES_DIR, { recursive: true, mode: 448 });
|
|
1828
|
+
}
|
|
1829
|
+
step(2, TOTAL_STEPS, "Initialising database");
|
|
1830
|
+
getDb();
|
|
1831
|
+
log.success("Database ready");
|
|
1832
|
+
step(3, TOTAL_STEPS, "Installing shell hook");
|
|
1833
|
+
const shell = detectShell();
|
|
1834
|
+
if (!shell) {
|
|
1835
|
+
log.warn("Could not detect shell. Supported: zsh, bash, fish");
|
|
1836
|
+
log.plain(
|
|
1837
|
+
` Manually add the hook from: ${import_chalk2.default.cyan("shell/hook.zsh")}, ${import_chalk2.default.cyan("shell/hook.bash")}, or ${import_chalk2.default.cyan("shell/hook.fish")}`
|
|
1838
|
+
);
|
|
1839
|
+
} else {
|
|
1840
|
+
const hookFile = import_path9.default.join(__dirname, "..", "shell", `hook.${shell.name}`);
|
|
1841
|
+
if (!import_fs13.default.existsSync(hookFile)) {
|
|
1842
|
+
log.warn(`Hook template not found at ${hookFile}`);
|
|
1843
|
+
} else {
|
|
1844
|
+
const hookContent = import_fs13.default.readFileSync(hookFile, "utf-8");
|
|
1845
|
+
if (shell.name === "fish") {
|
|
1846
|
+
const confDir = import_path9.default.dirname(shell.rcFile);
|
|
1847
|
+
if (!import_fs13.default.existsSync(confDir)) import_fs13.default.mkdirSync(confDir, { recursive: true });
|
|
1848
|
+
}
|
|
1849
|
+
if (!import_fs13.default.existsSync(shell.rcFile)) import_fs13.default.writeFileSync(shell.rcFile, "");
|
|
1850
|
+
if (!isHookInstalled(shell.rcFile)) {
|
|
1851
|
+
log.plain("");
|
|
1852
|
+
log.plain(import_chalk2.default.dim(" Workscribe adds a lightweight hook to your shell that captures each"));
|
|
1853
|
+
log.plain(import_chalk2.default.dim(" command you run \u2014 the text, exit code, duration, and working directory."));
|
|
1854
|
+
log.plain(import_chalk2.default.dim(" It runs in the background and adds no latency to your terminal."));
|
|
1855
|
+
log.plain(import_chalk2.default.dim(" Credentials and secrets are redacted before anything is stored."));
|
|
1856
|
+
log.plain("");
|
|
1857
|
+
installHook(shell.rcFile, hookContent);
|
|
1858
|
+
log.success(`Shell hook installed in ${shell.rcFile}`);
|
|
1859
|
+
} else if (isHookOutdated(shell.rcFile, hookContent)) {
|
|
1860
|
+
const { update } = await (0, import_prompts.default)({
|
|
1861
|
+
type: "confirm",
|
|
1862
|
+
name: "update",
|
|
1863
|
+
message: `Shell hook in ${shell.rcFile} is outdated. Update?`,
|
|
1864
|
+
initial: true
|
|
1865
|
+
});
|
|
1866
|
+
if (update) {
|
|
1867
|
+
updateHook(shell.rcFile, hookContent);
|
|
1868
|
+
log.success(`Shell hook updated in ${shell.rcFile}`);
|
|
1869
|
+
} else {
|
|
1870
|
+
log.info("Shell hook update skipped");
|
|
1871
|
+
}
|
|
1872
|
+
} else {
|
|
1873
|
+
log.info(`Shell hook up to date in ${shell.rcFile}`);
|
|
1874
|
+
}
|
|
1875
|
+
if (isCompletionsInstalled(shell)) {
|
|
1876
|
+
log.info("Shell completions already installed");
|
|
1877
|
+
} else {
|
|
1878
|
+
const { installComps } = await (0, import_prompts.default)({
|
|
1879
|
+
type: "confirm",
|
|
1880
|
+
name: "installComps",
|
|
1881
|
+
message: "Install shell completions (tab-complete workscribe commands)?",
|
|
1882
|
+
initial: true
|
|
1883
|
+
});
|
|
1884
|
+
if (installComps) {
|
|
1885
|
+
try {
|
|
1886
|
+
installCompletions(shell, getPublicCommands(program2));
|
|
1887
|
+
log.success("Shell completions installed");
|
|
1888
|
+
} catch (error) {
|
|
1889
|
+
log.warn(`Completions install failed: ${error.message}`);
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
step(4, TOTAL_STEPS, "Detecting AI coding tools");
|
|
1896
|
+
const installedTools = loadAIToolRegistry().filter(
|
|
1298
1897
|
(entry) => entry.hook !== null && AI_HOOK_INTEGRATIONS[entry.hook]?.isInstalled()
|
|
1299
1898
|
);
|
|
1300
1899
|
if (installedTools.length === 0) {
|
|
@@ -1348,6 +1947,8 @@ function registerInit(program2) {
|
|
|
1348
1947
|
} catch {
|
|
1349
1948
|
log.info("Could not reach profile registry \u2014 run workscribe profile detect after connecting");
|
|
1350
1949
|
}
|
|
1950
|
+
step(5, TOTAL_STEPS, "Configuring AI provider");
|
|
1951
|
+
let providerConfigured = !!config.ai.provider;
|
|
1351
1952
|
if (config.ai.provider) {
|
|
1352
1953
|
log.info(`AI provider already configured: ${import_chalk2.default.cyan(config.ai.provider)}`);
|
|
1353
1954
|
} else {
|
|
@@ -1364,7 +1965,10 @@ function registerInit(program2) {
|
|
|
1364
1965
|
{ title: "Skip \u2014 configure later", value: "skip" }
|
|
1365
1966
|
]
|
|
1366
1967
|
});
|
|
1367
|
-
if (provider
|
|
1968
|
+
if (!provider || provider === "skip") {
|
|
1969
|
+
providerConfigured = false;
|
|
1970
|
+
log.info("Skipped \u2014 run later: workscribe config set ai.provider <provider>");
|
|
1971
|
+
} else {
|
|
1368
1972
|
config.ai.provider = provider;
|
|
1369
1973
|
if (provider === "ollama") {
|
|
1370
1974
|
log.plain(`
|
|
@@ -1389,90 +1993,78 @@ function registerInit(program2) {
|
|
|
1389
1993
|
if (baseUrl) config.ai.baseUrl = baseUrl;
|
|
1390
1994
|
}
|
|
1391
1995
|
}
|
|
1392
|
-
|
|
1393
|
-
log.
|
|
1394
|
-
|
|
1395
|
-
|
|
1996
|
+
log.plain("");
|
|
1997
|
+
log.info(`Validating ${import_chalk2.default.cyan(provider)} connection\u2026`);
|
|
1998
|
+
try {
|
|
1999
|
+
const testProvider = getProvider(config);
|
|
2000
|
+
await testProvider.validate();
|
|
2001
|
+
log.success("API key verified");
|
|
2002
|
+
writeConfig(config);
|
|
2003
|
+
providerConfigured = true;
|
|
2004
|
+
} catch (error) {
|
|
2005
|
+
log.warn(`Validation failed: ${error.message}`);
|
|
2006
|
+
const { retry } = await (0, import_prompts.default)({
|
|
2007
|
+
type: "confirm",
|
|
2008
|
+
name: "retry",
|
|
2009
|
+
message: "Save anyway and configure later?",
|
|
2010
|
+
initial: false
|
|
2011
|
+
});
|
|
2012
|
+
if (retry) {
|
|
2013
|
+
writeConfig(config);
|
|
2014
|
+
providerConfigured = false;
|
|
2015
|
+
log.info("Config saved \u2014 run workscribe config set ai.apiKey <key> to update");
|
|
2016
|
+
} else {
|
|
2017
|
+
log.info("API key not saved \u2014 run workscribe init again to configure");
|
|
2018
|
+
providerConfigured = false;
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
1396
2021
|
}
|
|
1397
2022
|
}
|
|
1398
|
-
if (!
|
|
2023
|
+
if (!import_fs13.default.existsSync(CONFIG_PATH)) {
|
|
1399
2024
|
writeConfig(config);
|
|
1400
|
-
log.success("Config file created");
|
|
1401
2025
|
}
|
|
1402
2026
|
log.plain("");
|
|
1403
2027
|
log.plain(import_chalk2.default.bold(" Setup complete!"));
|
|
2028
|
+
log.plain("");
|
|
1404
2029
|
if (shell) {
|
|
2030
|
+
log.plain(` Activate capture now:`);
|
|
2031
|
+
if (shell.name === "fish") {
|
|
2032
|
+
log.plain(` Restart your terminal`);
|
|
2033
|
+
log.plain(` ${import_chalk2.default.dim(`or run: source ${shell.rcFile}`)}`);
|
|
2034
|
+
} else {
|
|
2035
|
+
log.plain(` ${import_chalk2.default.cyan(`source ${shell.rcFile}`)}`);
|
|
2036
|
+
log.plain(` Or restart your terminal.`);
|
|
2037
|
+
}
|
|
1405
2038
|
log.plain("");
|
|
1406
|
-
log.plain(` Activate capture now: ${import_chalk2.default.cyan(`source ${shell.rcFile}`)}`);
|
|
1407
|
-
log.plain(` Or restart your terminal.`);
|
|
1408
2039
|
}
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
2040
|
+
if (providerConfigured) {
|
|
2041
|
+
log.plain(` Then run a few commands and generate your first summary:`);
|
|
2042
|
+
log.plain(` ${import_chalk2.default.cyan("workscribe summary")}`);
|
|
2043
|
+
log.plain("");
|
|
2044
|
+
log.plain(` Check capture is working:`);
|
|
2045
|
+
log.plain(` ${import_chalk2.default.cyan("workscribe status")}`);
|
|
2046
|
+
} else {
|
|
2047
|
+
log.plain(` Before generating summaries, configure an AI provider:`);
|
|
2048
|
+
log.plain(` ${import_chalk2.default.cyan("workscribe config set ai.provider anthropic")}`);
|
|
2049
|
+
log.plain(` ${import_chalk2.default.cyan("workscribe config set ai.apiKey <your-key>")}`);
|
|
2050
|
+
log.plain("");
|
|
2051
|
+
log.plain(` Then run:`);
|
|
2052
|
+
log.plain(` ${import_chalk2.default.cyan("workscribe summary")}`);
|
|
2053
|
+
}
|
|
1412
2054
|
log.plain("");
|
|
1413
2055
|
});
|
|
1414
2056
|
}
|
|
1415
2057
|
|
|
1416
2058
|
// src/cli/status.ts
|
|
1417
|
-
var
|
|
2059
|
+
var import_fs14 = __toESM(require("fs"));
|
|
1418
2060
|
var import_chalk3 = __toESM(require("chalk"));
|
|
1419
|
-
|
|
1420
|
-
// src/utils/format.ts
|
|
1421
|
-
function formatDuration(minutes) {
|
|
1422
|
-
if (minutes < 60) return `${Math.round(minutes)}m`;
|
|
1423
|
-
const h = Math.floor(minutes / 60);
|
|
1424
|
-
const m = Math.round(minutes % 60);
|
|
1425
|
-
return m > 0 ? `${h}h ${m}m` : `${h}h`;
|
|
1426
|
-
}
|
|
1427
|
-
function formatDate(isoString) {
|
|
1428
|
-
return new Date(isoString).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
1429
|
-
}
|
|
1430
|
-
function todayDate() {
|
|
1431
|
-
return localDateStr(/* @__PURE__ */ new Date());
|
|
1432
|
-
}
|
|
1433
|
-
function parseDate(input) {
|
|
1434
|
-
const d = /* @__PURE__ */ new Date(input + "T00:00:00");
|
|
1435
|
-
if (isNaN(d.getTime())) throw new Error(`Invalid date: ${input}`);
|
|
1436
|
-
return input;
|
|
1437
|
-
}
|
|
1438
|
-
function localDateStr(d) {
|
|
1439
|
-
const y = d.getFullYear();
|
|
1440
|
-
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
1441
|
-
const day = String(d.getDate()).padStart(2, "0");
|
|
1442
|
-
return `${y}-${m}-${day}`;
|
|
1443
|
-
}
|
|
1444
|
-
function startOfWeek(dateStr) {
|
|
1445
|
-
const d = /* @__PURE__ */ new Date(dateStr + "T00:00:00");
|
|
1446
|
-
const day = d.getDay();
|
|
1447
|
-
const diff = day === 0 ? -6 : 1 - day;
|
|
1448
|
-
d.setDate(d.getDate() + diff);
|
|
1449
|
-
return localDateStr(d);
|
|
1450
|
-
}
|
|
1451
|
-
function eachDayOfRange(start, end) {
|
|
1452
|
-
const days = [];
|
|
1453
|
-
const cur = /* @__PURE__ */ new Date(start + "T00:00:00");
|
|
1454
|
-
const last = /* @__PURE__ */ new Date(end + "T00:00:00");
|
|
1455
|
-
while (cur <= last) {
|
|
1456
|
-
days.push(localDateStr(cur));
|
|
1457
|
-
cur.setDate(cur.getDate() + 1);
|
|
1458
|
-
}
|
|
1459
|
-
return days;
|
|
1460
|
-
}
|
|
1461
|
-
function isoWeekKey(dateStr) {
|
|
1462
|
-
const d = /* @__PURE__ */ new Date(dateStr + "T00:00:00");
|
|
1463
|
-
const jan4 = new Date(d.getFullYear(), 0, 4);
|
|
1464
|
-
const weekNum = Math.ceil(((d.getTime() - jan4.getTime()) / 864e5 + jan4.getDay() + 1) / 7);
|
|
1465
|
-
return `${d.getFullYear()}-W${String(weekNum).padStart(2, "0")}`;
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
// src/cli/status.ts
|
|
1469
2061
|
function registerStatus(program2) {
|
|
1470
2062
|
program2.command("status").description("Show capture status and event counts").action(() => {
|
|
1471
2063
|
const config = readConfig();
|
|
1472
2064
|
const total = getEventCount();
|
|
1473
2065
|
const todayEvents = getEventsForDate(todayDate());
|
|
1474
2066
|
const last = getLastEvent();
|
|
1475
|
-
const isPaused =
|
|
2067
|
+
const isPaused = import_fs14.default.existsSync(PAUSE_FLAG_PATH);
|
|
1476
2068
|
log.plain("");
|
|
1477
2069
|
log.plain(import_chalk3.default.bold(" Workscribe Status"));
|
|
1478
2070
|
log.plain("");
|
|
@@ -1515,181 +2107,28 @@ function registerStatus(program2) {
|
|
|
1515
2107
|
}
|
|
1516
2108
|
|
|
1517
2109
|
// src/cli/events.ts
|
|
1518
|
-
var import_chalk4 = __toESM(require("chalk"));
|
|
1519
|
-
var CATEGORY_COLORS = {
|
|
1520
|
-
shipping: import_chalk4.default.green,
|
|
1521
|
-
versioning: import_chalk4.default.blue,
|
|
1522
|
-
building: import_chalk4.default.cyan,
|
|
1523
|
-
running: import_chalk4.default.white,
|
|
1524
|
-
testing: import_chalk4.default.magenta,
|
|
1525
|
-
linting: import_chalk4.default.yellow,
|
|
1526
|
-
dependencies: import_chalk4.default.yellow,
|
|
1527
|
-
infrastructure: import_chalk4.default.red,
|
|
1528
|
-
releasing: import_chalk4.default.bold.green,
|
|
1529
|
-
database: import_chalk4.default.bold.blue,
|
|
1530
|
-
files: import_chalk4.default.dim.yellow,
|
|
1531
|
-
exploring: import_chalk4.default.dim.cyan,
|
|
1532
|
-
ai_session: import_chalk4.default.bold.magenta,
|
|
1533
|
-
other: import_chalk4.default.gray
|
|
1534
|
-
};
|
|
1535
|
-
var AI_TOOL_DISPLAY_NAMES = Object.fromEntries(
|
|
1536
|
-
loadAIToolRegistry().map((entry) => [entry.tool, entry.displayName])
|
|
1537
|
-
);
|
|
1538
|
-
function registerEvents(program2) {
|
|
1539
|
-
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) => {
|
|
1540
|
-
let date = todayDate();
|
|
1541
|
-
if (opts.date) {
|
|
1542
|
-
try {
|
|
1543
|
-
date = parseDate(opts.date);
|
|
1544
|
-
} catch (e) {
|
|
1545
|
-
log.error(e.message);
|
|
1546
|
-
process.exit(1);
|
|
1547
|
-
}
|
|
1548
|
-
}
|
|
1549
|
-
const events = getEventsForDate(date);
|
|
1550
|
-
const limit = opts.limit ?? events.length;
|
|
1551
|
-
if (events.length === 0) {
|
|
1552
|
-
log.plain(`No events captured for ${date}`);
|
|
1553
|
-
return;
|
|
1554
|
-
}
|
|
1555
|
-
log.plain("");
|
|
1556
|
-
log.plain(import_chalk4.default.bold(` Events for ${date} (${events.length} total)`));
|
|
1557
|
-
log.plain("");
|
|
1558
|
-
for (const event of events.slice(0, limit)) {
|
|
1559
|
-
const colorFn = CATEGORY_COLORS[event.category] ?? import_chalk4.default.gray;
|
|
1560
|
-
const time = formatDate(event.captured_at);
|
|
1561
|
-
const category = colorFn(event.category.padEnd(20));
|
|
1562
|
-
const repo = event.repo_name ? import_chalk4.default.dim(` [${event.repo_name}]`) : "";
|
|
1563
|
-
const exitBadge = event.exit_code && event.exit_code !== 0 ? import_chalk4.default.red(` \u2717${event.exit_code}`) : "";
|
|
1564
|
-
let cmdDisplay;
|
|
1565
|
-
if (event.category === "ai_session") {
|
|
1566
|
-
let meta = {};
|
|
1567
|
-
try {
|
|
1568
|
-
meta = event.metadata ? JSON.parse(event.metadata) : {};
|
|
1569
|
-
} catch {
|
|
1570
|
-
}
|
|
1571
|
-
const toolKey = typeof meta.tool === "string" ? meta.tool : "";
|
|
1572
|
-
const toolName = AI_TOOL_DISPLAY_NAMES[toolKey] ?? toolKey;
|
|
1573
|
-
cmdDisplay = import_chalk4.default.white(toolName.padEnd(20)) + import_chalk4.default.magenta(" \u2726");
|
|
1574
|
-
} else {
|
|
1575
|
-
cmdDisplay = import_chalk4.default.dim(event.command.slice(0, 55));
|
|
1576
|
-
}
|
|
1577
|
-
log.plain(` ${import_chalk4.default.dim(time)} ${category} ${cmdDisplay}${repo}${exitBadge}`);
|
|
1578
|
-
}
|
|
1579
|
-
if (limit < events.length) {
|
|
1580
|
-
log.plain("");
|
|
1581
|
-
log.plain(import_chalk4.default.dim(` \u2026 and ${events.length - limit} more (remove --limit to see all)`));
|
|
1582
|
-
}
|
|
1583
|
-
log.plain("");
|
|
1584
|
-
});
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
// src/cli/sessions.ts
|
|
1588
|
-
var import_chalk5 = __toESM(require("chalk"));
|
|
1589
|
-
|
|
1590
|
-
// src/sessions/grouper.ts
|
|
1591
|
-
function splitByIdleGap(events, idleMs) {
|
|
1592
|
-
if (events.length === 0) return [];
|
|
1593
|
-
const groups = [[events[0]]];
|
|
1594
|
-
for (let i = 1; i < events.length; i++) {
|
|
1595
|
-
const prev = new Date(events[i - 1].captured_at).getTime();
|
|
1596
|
-
const curr = new Date(events[i].captured_at).getTime();
|
|
1597
|
-
if (curr - prev >= idleMs) {
|
|
1598
|
-
groups.push([events[i]]);
|
|
1599
|
-
} else {
|
|
1600
|
-
groups[groups.length - 1].push(events[i]);
|
|
1601
|
-
}
|
|
1602
|
-
}
|
|
1603
|
-
return groups;
|
|
1604
|
-
}
|
|
1605
|
-
function splitByRepo(events) {
|
|
1606
|
-
if (events.length === 0) return [];
|
|
1607
|
-
const groups = [[events[0]]];
|
|
1608
|
-
for (let i = 1; i < events.length; i++) {
|
|
1609
|
-
const prevRepo = events[i - 1].repo_name;
|
|
1610
|
-
const currRepo = events[i].repo_name;
|
|
1611
|
-
if (currRepo !== prevRepo) {
|
|
1612
|
-
groups.push([events[i]]);
|
|
1613
|
-
} else {
|
|
1614
|
-
groups[groups.length - 1].push(events[i]);
|
|
1615
|
-
}
|
|
1616
|
-
}
|
|
1617
|
-
return groups;
|
|
1618
|
-
}
|
|
1619
|
-
function minutesBetween(start, end) {
|
|
1620
|
-
return (new Date(end).getTime() - new Date(start).getTime()) / 6e4;
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
// src/sessions/detector.ts
|
|
1624
|
-
function getOrBuildSessions(date) {
|
|
1625
|
-
const existing = getSessionsForDate(date);
|
|
1626
|
-
const events = getEventsForDate(date);
|
|
1627
|
-
if (events.length === 0) return [];
|
|
1628
|
-
const cachedEventCount = existing.reduce((sum, sessionRow) => sum + sessionRow.event_count, 0);
|
|
1629
|
-
if (existing.length > 0 && cachedEventCount === events.length) {
|
|
1630
|
-
return existing.map((sessionRow) => ({
|
|
1631
|
-
id: sessionRow.id,
|
|
1632
|
-
date: sessionRow.date,
|
|
1633
|
-
startedAt: sessionRow.started_at,
|
|
1634
|
-
endedAt: sessionRow.ended_at,
|
|
1635
|
-
repoName: sessionRow.repo_name,
|
|
1636
|
-
durationMinutes: minutesBetween(sessionRow.started_at, sessionRow.ended_at),
|
|
1637
|
-
eventCount: sessionRow.event_count,
|
|
1638
|
-
summary: sessionRow.summary,
|
|
1639
|
-
events: getEventsBySessionId(sessionRow.id)
|
|
1640
|
-
}));
|
|
1641
|
-
}
|
|
1642
|
-
const savedSummaries = /* @__PURE__ */ new Map();
|
|
1643
|
-
for (const s of existing) {
|
|
1644
|
-
if (s.summary) savedSummaries.set(`${s.repo_name}|${s.started_at}`, s.summary);
|
|
1645
|
-
}
|
|
1646
|
-
clearSessionsForDate(date);
|
|
1647
|
-
return buildSessions(date, events, savedSummaries);
|
|
1648
|
-
}
|
|
1649
|
-
function buildSessions(date, events, savedSummaries = /* @__PURE__ */ new Map()) {
|
|
1650
|
-
const config = readConfig();
|
|
1651
|
-
const idleMs = config.session.idleTimeout * 60 * 1e3;
|
|
1652
|
-
const sessions = [];
|
|
1653
|
-
const timeGroups = splitByIdleGap(events, idleMs);
|
|
1654
|
-
for (const group of timeGroups) {
|
|
1655
|
-
if (group.length === 0) continue;
|
|
1656
|
-
const repoGroups = splitByRepo(group);
|
|
1657
|
-
for (const repoGroup of repoGroups) {
|
|
1658
|
-
if (repoGroup.length === 0) continue;
|
|
1659
|
-
const first = repoGroup[0];
|
|
1660
|
-
const last = repoGroup[repoGroup.length - 1];
|
|
1661
|
-
const repoName = last.repo_name ?? first.repo_name ?? null;
|
|
1662
|
-
const summary = savedSummaries.get(`${repoName}|${first.captured_at}`) ?? null;
|
|
1663
|
-
const sessionId = insertSession({
|
|
1664
|
-
date,
|
|
1665
|
-
started_at: first.captured_at,
|
|
1666
|
-
ended_at: last.captured_at,
|
|
1667
|
-
repo_name: repoName,
|
|
1668
|
-
event_count: repoGroup.length,
|
|
1669
|
-
summary
|
|
1670
|
-
});
|
|
1671
|
-
for (const event of repoGroup) {
|
|
1672
|
-
insertSessionEvent(sessionId, event.id);
|
|
1673
|
-
}
|
|
1674
|
-
sessions.push({
|
|
1675
|
-
id: sessionId,
|
|
1676
|
-
date,
|
|
1677
|
-
startedAt: first.captured_at,
|
|
1678
|
-
endedAt: last.captured_at,
|
|
1679
|
-
repoName,
|
|
1680
|
-
durationMinutes: minutesBetween(first.captured_at, last.captured_at),
|
|
1681
|
-
eventCount: repoGroup.length,
|
|
1682
|
-
summary,
|
|
1683
|
-
events: repoGroup
|
|
1684
|
-
});
|
|
1685
|
-
}
|
|
1686
|
-
}
|
|
1687
|
-
return sessions;
|
|
1688
|
-
}
|
|
1689
|
-
|
|
1690
|
-
// src/cli/sessions.ts
|
|
1691
|
-
function registerSessions(program2) {
|
|
1692
|
-
program2.command("sessions").description("List detected work sessions").option("--date <YYYY-MM-DD>", "date to show sessions for (default: today)").action((opts) => {
|
|
2110
|
+
var import_chalk4 = __toESM(require("chalk"));
|
|
2111
|
+
var CATEGORY_COLORS = {
|
|
2112
|
+
shipping: import_chalk4.default.green,
|
|
2113
|
+
versioning: import_chalk4.default.blue,
|
|
2114
|
+
building: import_chalk4.default.cyan,
|
|
2115
|
+
running: import_chalk4.default.white,
|
|
2116
|
+
testing: import_chalk4.default.magenta,
|
|
2117
|
+
linting: import_chalk4.default.yellow,
|
|
2118
|
+
dependencies: import_chalk4.default.yellow,
|
|
2119
|
+
infrastructure: import_chalk4.default.red,
|
|
2120
|
+
releasing: import_chalk4.default.bold.green,
|
|
2121
|
+
database: import_chalk4.default.bold.blue,
|
|
2122
|
+
files: import_chalk4.default.dim.yellow,
|
|
2123
|
+
exploring: import_chalk4.default.dim.cyan,
|
|
2124
|
+
ai_session: import_chalk4.default.bold.magenta,
|
|
2125
|
+
other: import_chalk4.default.gray
|
|
2126
|
+
};
|
|
2127
|
+
var AI_TOOL_DISPLAY_NAMES = Object.fromEntries(
|
|
2128
|
+
loadAIToolRegistry().map((entry) => [entry.tool, entry.displayName])
|
|
2129
|
+
);
|
|
2130
|
+
function registerEvents(program2) {
|
|
2131
|
+
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) => {
|
|
1693
2132
|
let date = todayDate();
|
|
1694
2133
|
if (opts.date) {
|
|
1695
2134
|
try {
|
|
@@ -1699,383 +2138,196 @@ function registerSessions(program2) {
|
|
|
1699
2138
|
process.exit(1);
|
|
1700
2139
|
}
|
|
1701
2140
|
}
|
|
1702
|
-
const
|
|
1703
|
-
|
|
1704
|
-
|
|
2141
|
+
const events = getEventsForDate(date);
|
|
2142
|
+
const limit = opts.limit ?? events.length;
|
|
2143
|
+
if (events.length === 0) {
|
|
2144
|
+
log.plain(`No events captured for ${date}`);
|
|
1705
2145
|
return;
|
|
1706
2146
|
}
|
|
1707
2147
|
log.plain("");
|
|
1708
|
-
log.plain(
|
|
1709
|
-
log.plain("");
|
|
1710
|
-
for (const session of sessions) {
|
|
1711
|
-
const start = formatDate(session.startedAt);
|
|
1712
|
-
const end = formatDate(session.endedAt);
|
|
1713
|
-
const duration = formatDuration(session.durationMinutes);
|
|
1714
|
-
const repo = session.repoName ? import_chalk5.default.cyan(session.repoName) : import_chalk5.default.gray("no repo");
|
|
1715
|
-
log.plain(
|
|
1716
|
-
` ${import_chalk5.default.dim(start)} \u2013 ${import_chalk5.default.dim(end)} ${repo} ${import_chalk5.default.dim(duration)} ${import_chalk5.default.dim(session.eventCount + " events")}`
|
|
1717
|
-
);
|
|
1718
|
-
}
|
|
2148
|
+
log.plain(import_chalk4.default.bold(` Events for ${date} (${events.length} total)`));
|
|
1719
2149
|
log.plain("");
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
Events:
|
|
1737
|
-
- versioning {"branch":"feature/auth"}
|
|
1738
|
-
- 2x dependencies {"tool":"npm","packages":["bcrypt","jsonwebtoken"]}
|
|
1739
|
-
- 2x testing {"tool":"jest"} [failed]
|
|
1740
|
-
- testing {"tool":"jest"}
|
|
1741
|
-
- 2x files {"tool":"mkdir"}
|
|
1742
|
-
- shipping {"message":"fix: auth token validation"}
|
|
1743
|
-
- shipping {"remote":"origin","branch":"feature/auth"}
|
|
1744
|
-
|
|
1745
|
-
OUTPUT:
|
|
1746
|
-
- Switched to the authentication feature branch and added authentication dependencies (bcrypt, JWT)
|
|
1747
|
-
- Resolved failing auth tests
|
|
1748
|
-
- Committed and pushed authentication fixes
|
|
1749
|
-
|
|
1750
|
-
For dependencies events with a packages field: infer the purpose of packages from their names and describe the domain, not just the packages. Examples: bcrypt/jsonwebtoken \u2192 authentication, prisma/mongoose/pg \u2192 database, jest/vitest/mocha \u2192 testing, axios/got/node-fetch \u2192 HTTP client, stripe/paypal \u2192 payments, socket.io/ws \u2192 real-time. Use your knowledge of common packages. For unfamiliar packages, name them directly without guessing their domain.
|
|
1751
|
-
|
|
1752
|
-
Events like shipping (pushes), building, versioning (branch switches, stash), files (file ops, env vars), and infrastructure 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, a deploy, or a significant restructure with no accompanying commits).
|
|
1753
|
-
|
|
1754
|
-
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.
|
|
1755
|
-
|
|
1756
|
-
For other events with a hint field: interpret the command snippet and describe the activity in plain language. Do not quote the raw command.
|
|
1757
|
-
|
|
1758
|
-
Maximum 5 bullets. Output ONLY the bullet points, nothing else.`;
|
|
1759
|
-
function getSystemPrompt() {
|
|
1760
|
-
const contexts = getActiveProfileAiContexts();
|
|
1761
|
-
if (contexts.length === 0) return SYSTEM_PROMPT;
|
|
1762
|
-
return SYSTEM_PROMPT + "\n\n" + contexts.join("\n");
|
|
1763
|
-
}
|
|
1764
|
-
var EXCLUDED = /* @__PURE__ */ new Set(["exploring"]);
|
|
1765
|
-
function buildUserMessage(payload) {
|
|
1766
|
-
const lines = [`Date: ${payload.date}`, ""];
|
|
1767
|
-
for (const session of payload.sessions) {
|
|
1768
|
-
lines.push(`Project: ${session.project ?? "unknown"}`);
|
|
1769
|
-
if (session.branch) lines.push(`Branch: ${session.branch}`);
|
|
1770
|
-
lines.push(`Duration: ${formatDuration(session.durationMinutes)}`);
|
|
1771
|
-
lines.push("Events:");
|
|
1772
|
-
const counts = /* @__PURE__ */ new Map();
|
|
1773
|
-
for (const event of session.events) {
|
|
1774
|
-
if (EXCLUDED.has(event.category)) continue;
|
|
1775
|
-
const failed = event.exitCode !== null && event.exitCode !== 0 ? " [failed]" : "";
|
|
1776
|
-
let key;
|
|
1777
|
-
if (event.hint) {
|
|
1778
|
-
key = `other [${event.hint}]${failed}`;
|
|
2150
|
+
for (const event of events.slice(0, limit)) {
|
|
2151
|
+
const colorFn = CATEGORY_COLORS[event.category] ?? import_chalk4.default.gray;
|
|
2152
|
+
const time = formatDate(event.captured_at);
|
|
2153
|
+
const category = colorFn(event.category.padEnd(20));
|
|
2154
|
+
const repo = event.repo_name ? import_chalk4.default.dim(` [${event.repo_name}]`) : "";
|
|
2155
|
+
const exitBadge = event.exit_code && event.exit_code !== 0 ? import_chalk4.default.red(` \u2717${event.exit_code}`) : "";
|
|
2156
|
+
let cmdDisplay;
|
|
2157
|
+
if (event.category === "ai_session") {
|
|
2158
|
+
let meta = {};
|
|
2159
|
+
try {
|
|
2160
|
+
meta = event.metadata ? JSON.parse(event.metadata) : {};
|
|
2161
|
+
} catch {
|
|
2162
|
+
}
|
|
2163
|
+
const toolKey = typeof meta.tool === "string" ? meta.tool : "";
|
|
2164
|
+
const toolName = AI_TOOL_DISPLAY_NAMES[toolKey] ?? toolKey;
|
|
2165
|
+
cmdDisplay = import_chalk4.default.white(toolName.padEnd(20)) + import_chalk4.default.magenta(" \u2726");
|
|
1779
2166
|
} else {
|
|
1780
|
-
|
|
1781
|
-
key = `${event.category}${meta}${failed}`;
|
|
2167
|
+
cmdDisplay = import_chalk4.default.dim(event.command.slice(0, 55));
|
|
1782
2168
|
}
|
|
1783
|
-
|
|
2169
|
+
log.plain(` ${import_chalk4.default.dim(time)} ${category} ${cmdDisplay}${repo}${exitBadge}`);
|
|
1784
2170
|
}
|
|
1785
|
-
|
|
1786
|
-
|
|
2171
|
+
if (limit < events.length) {
|
|
2172
|
+
log.plain("");
|
|
2173
|
+
log.plain(import_chalk4.default.dim(` \u2026 and ${events.length - limit} more (remove --limit to see all)`));
|
|
1787
2174
|
}
|
|
1788
|
-
|
|
1789
|
-
}
|
|
1790
|
-
return lines.join("\n");
|
|
2175
|
+
log.plain("");
|
|
2176
|
+
});
|
|
1791
2177
|
}
|
|
1792
|
-
var WEEKLY_SYSTEM_PROMPT = `You write short weekly developer summaries. Here is an example:
|
|
1793
|
-
|
|
1794
|
-
INPUT:
|
|
1795
|
-
Week: 2026-05-18 to 2026-05-22
|
|
1796
|
-
|
|
1797
|
-
Monday 2026-05-18:
|
|
1798
|
-
Project: api-service (feature/auth) \u2014 2h 10m
|
|
1799
|
-
Events: 3x code_commit, 2x test_run, dependency_install
|
|
1800
|
-
|
|
1801
|
-
Tuesday 2026-05-19:
|
|
1802
|
-
Project: api-service (feature/auth) \u2014 1h 30m
|
|
1803
|
-
Events: 2x code_commit, test_run, build
|
|
1804
|
-
|
|
1805
|
-
Wednesday 2026-05-20:
|
|
1806
|
-
Project: dashboard (main) \u2014 3h 00m
|
|
1807
|
-
Events: 4x code_commit, 2x build, test_run
|
|
1808
|
-
|
|
1809
|
-
Thursday 2026-05-21:
|
|
1810
|
-
Project: api-service (feature/auth) \u2014 1h 00m
|
|
1811
|
-
Events: code_commit {"message":"feat: merge auth"}, branch_switch
|
|
1812
|
-
|
|
1813
|
-
Friday 2026-05-22:
|
|
1814
|
-
Project: dashboard (main) \u2014 2h 00m
|
|
1815
|
-
Events: 3x code_commit, build
|
|
1816
|
-
|
|
1817
|
-
OUTPUT:
|
|
1818
|
-
## Week of 18 May \u2014 9h 40m across 2 projects
|
|
1819
|
-
|
|
1820
|
-
**api-service** (Mon\u2013Thu): Built and shipped the auth feature \u2014 JWT setup, token validation fixes, and final merge.
|
|
1821
|
-
**dashboard** (Wed, Fri): Active development across 7 commits with builds passing.
|
|
1822
2178
|
|
|
1823
|
-
|
|
1824
|
-
var
|
|
1825
|
-
function buildWeeklyMessage(payload) {
|
|
1826
|
-
const lines = [`Week: ${payload.weekStart} to ${payload.weekEnd}`, ""];
|
|
1827
|
-
for (const { date, sessions } of payload.days) {
|
|
1828
|
-
const dayName = DAY_NAMES[(/* @__PURE__ */ new Date(date + "T00:00:00")).getDay()];
|
|
1829
|
-
lines.push(`${dayName} ${date}:`);
|
|
1830
|
-
for (const s of sessions) {
|
|
1831
|
-
const project = s.project ?? "unknown";
|
|
1832
|
-
const branch = s.branch ? ` (${s.branch})` : "";
|
|
1833
|
-
lines.push(` Project: ${project}${branch} \u2014 ${formatDuration(s.durationMinutes)}`);
|
|
1834
|
-
const counts = /* @__PURE__ */ new Map();
|
|
1835
|
-
for (const e of s.events) {
|
|
1836
|
-
if (EXCLUDED.has(e.category)) continue;
|
|
1837
|
-
const failed = e.exitCode !== null && e.exitCode !== 0 ? " [failed]" : "";
|
|
1838
|
-
let key;
|
|
1839
|
-
if (e.hint) {
|
|
1840
|
-
key = `other [${e.hint}]${failed}`;
|
|
1841
|
-
} else {
|
|
1842
|
-
const meta = Object.keys(e.metadata).length > 0 ? ` ${JSON.stringify(e.metadata)}` : "";
|
|
1843
|
-
key = `${e.category}${meta}${failed}`;
|
|
1844
|
-
}
|
|
1845
|
-
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
1846
|
-
}
|
|
1847
|
-
const eventLine = [...counts.entries()].map(([k, n]) => `${n > 1 ? `${n}x ` : ""}${k}`).join(", ");
|
|
1848
|
-
lines.push(` Events: ${eventLine}`);
|
|
1849
|
-
}
|
|
1850
|
-
lines.push("");
|
|
1851
|
-
}
|
|
1852
|
-
return lines.join("\n");
|
|
1853
|
-
}
|
|
2179
|
+
// src/cli/sessions.ts
|
|
2180
|
+
var import_chalk5 = __toESM(require("chalk"));
|
|
1854
2181
|
|
|
1855
|
-
// src/
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
const
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
2182
|
+
// src/sessions/grouper.ts
|
|
2183
|
+
function splitByIdleGap(events, idleMs) {
|
|
2184
|
+
if (events.length === 0) return [];
|
|
2185
|
+
const groups = [[events[0]]];
|
|
2186
|
+
for (let i = 1; i < events.length; i++) {
|
|
2187
|
+
const prev = new Date(events[i - 1].captured_at).getTime();
|
|
2188
|
+
const curr = new Date(events[i].captured_at).getTime();
|
|
2189
|
+
if (curr - prev >= idleMs) {
|
|
2190
|
+
groups.push([events[i]]);
|
|
2191
|
+
} else {
|
|
2192
|
+
groups[groups.length - 1].push(events[i]);
|
|
1865
2193
|
}
|
|
1866
|
-
throw err;
|
|
1867
|
-
} finally {
|
|
1868
|
-
clearTimeout(timer);
|
|
1869
2194
|
}
|
|
2195
|
+
return groups;
|
|
1870
2196
|
}
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
const response = await fetchWithTimeout("https://api.anthropic.com/v1/messages", {
|
|
1882
|
-
method: "POST",
|
|
1883
|
-
headers: {
|
|
1884
|
-
"Content-Type": "application/json",
|
|
1885
|
-
"x-api-key": this.apiKey,
|
|
1886
|
-
"anthropic-version": "2023-06-01"
|
|
1887
|
-
},
|
|
1888
|
-
body: JSON.stringify({
|
|
1889
|
-
model: this.model,
|
|
1890
|
-
max_tokens: 1024,
|
|
1891
|
-
system: getSystemPrompt(),
|
|
1892
|
-
messages: [{ role: "user", content: buildUserMessage(payload) }]
|
|
1893
|
-
})
|
|
1894
|
-
});
|
|
1895
|
-
if (!response.ok) {
|
|
1896
|
-
const body = await response.text();
|
|
1897
|
-
throw new Error(`Anthropic API error ${response.status}: ${body}`);
|
|
1898
|
-
}
|
|
1899
|
-
const data = await response.json();
|
|
1900
|
-
return data.content.find((block) => block.type === "text")?.text ?? "";
|
|
1901
|
-
}
|
|
1902
|
-
async summarizeWeek(payload) {
|
|
1903
|
-
const response = await fetchWithTimeout("https://api.anthropic.com/v1/messages", {
|
|
1904
|
-
method: "POST",
|
|
1905
|
-
headers: {
|
|
1906
|
-
"Content-Type": "application/json",
|
|
1907
|
-
"x-api-key": this.apiKey,
|
|
1908
|
-
"anthropic-version": "2023-06-01"
|
|
1909
|
-
},
|
|
1910
|
-
body: JSON.stringify({
|
|
1911
|
-
model: this.model,
|
|
1912
|
-
max_tokens: 1024,
|
|
1913
|
-
system: WEEKLY_SYSTEM_PROMPT,
|
|
1914
|
-
messages: [{ role: "user", content: buildWeeklyMessage(payload) }]
|
|
1915
|
-
})
|
|
1916
|
-
});
|
|
1917
|
-
if (!response.ok) {
|
|
1918
|
-
const body = await response.text();
|
|
1919
|
-
throw new Error(`Anthropic API error ${response.status}: ${body}`);
|
|
1920
|
-
}
|
|
1921
|
-
const data = await response.json();
|
|
1922
|
-
return data.content.find((block) => block.type === "text")?.text ?? "";
|
|
1923
|
-
}
|
|
1924
|
-
};
|
|
1925
|
-
|
|
1926
|
-
// src/ai/providers/openai.ts
|
|
1927
|
-
var OpenAIProvider = class {
|
|
1928
|
-
apiKey;
|
|
1929
|
-
model;
|
|
1930
|
-
baseUrl;
|
|
1931
|
-
constructor(apiKey, model, baseUrl) {
|
|
1932
|
-
this.apiKey = apiKey;
|
|
1933
|
-
this.model = model || "gpt-4o-mini";
|
|
1934
|
-
this.baseUrl = baseUrl || "https://api.openai.com/v1";
|
|
1935
|
-
}
|
|
1936
|
-
async summarize(payload) {
|
|
1937
|
-
const response = await fetchWithTimeout(`${this.baseUrl}/chat/completions`, {
|
|
1938
|
-
method: "POST",
|
|
1939
|
-
headers: {
|
|
1940
|
-
"Content-Type": "application/json",
|
|
1941
|
-
Authorization: `Bearer ${this.apiKey}`
|
|
1942
|
-
},
|
|
1943
|
-
body: JSON.stringify({
|
|
1944
|
-
model: this.model,
|
|
1945
|
-
messages: [
|
|
1946
|
-
{ role: "system", content: getSystemPrompt() },
|
|
1947
|
-
{ role: "user", content: buildUserMessage(payload) }
|
|
1948
|
-
],
|
|
1949
|
-
max_tokens: 1024
|
|
1950
|
-
})
|
|
1951
|
-
});
|
|
1952
|
-
if (!response.ok) {
|
|
1953
|
-
const body = await response.text();
|
|
1954
|
-
throw new Error(`OpenAI API error ${response.status}: ${body}`);
|
|
1955
|
-
}
|
|
1956
|
-
const data = await response.json();
|
|
1957
|
-
return data.choices[0]?.message?.content ?? "";
|
|
1958
|
-
}
|
|
1959
|
-
async summarizeWeek(payload) {
|
|
1960
|
-
const response = await fetchWithTimeout(`${this.baseUrl}/chat/completions`, {
|
|
1961
|
-
method: "POST",
|
|
1962
|
-
headers: {
|
|
1963
|
-
"Content-Type": "application/json",
|
|
1964
|
-
Authorization: `Bearer ${this.apiKey}`
|
|
1965
|
-
},
|
|
1966
|
-
body: JSON.stringify({
|
|
1967
|
-
model: this.model,
|
|
1968
|
-
messages: [
|
|
1969
|
-
{ role: "system", content: WEEKLY_SYSTEM_PROMPT },
|
|
1970
|
-
{ role: "user", content: buildWeeklyMessage(payload) }
|
|
1971
|
-
],
|
|
1972
|
-
max_tokens: 1024
|
|
1973
|
-
})
|
|
1974
|
-
});
|
|
1975
|
-
if (!response.ok) {
|
|
1976
|
-
const body = await response.text();
|
|
1977
|
-
throw new Error(`OpenAI API error ${response.status}: ${body}`);
|
|
1978
|
-
}
|
|
1979
|
-
const data = await response.json();
|
|
1980
|
-
return data.choices[0]?.message?.content ?? "";
|
|
1981
|
-
}
|
|
1982
|
-
};
|
|
1983
|
-
|
|
1984
|
-
// src/ai/providers/ollama.ts
|
|
1985
|
-
var OllamaProvider = class {
|
|
1986
|
-
model;
|
|
1987
|
-
baseUrl;
|
|
1988
|
-
constructor(model, baseUrl) {
|
|
1989
|
-
this.model = model || "llama3";
|
|
1990
|
-
this.baseUrl = baseUrl || "http://localhost:11434";
|
|
1991
|
-
}
|
|
1992
|
-
async summarize(payload) {
|
|
1993
|
-
const response = await fetchWithTimeout(`${this.baseUrl}/api/chat`, {
|
|
1994
|
-
method: "POST",
|
|
1995
|
-
headers: { "Content-Type": "application/json" },
|
|
1996
|
-
body: JSON.stringify({
|
|
1997
|
-
model: this.model,
|
|
1998
|
-
stream: false,
|
|
1999
|
-
messages: [
|
|
2000
|
-
{ role: "system", content: getSystemPrompt() },
|
|
2001
|
-
{ role: "user", content: buildUserMessage(payload) }
|
|
2002
|
-
],
|
|
2003
|
-
options: {
|
|
2004
|
-
num_predict: 300,
|
|
2005
|
-
// hard cap on output tokens — forces conciseness
|
|
2006
|
-
temperature: 0.3
|
|
2007
|
-
// lower temperature = more focused, less wandering
|
|
2008
|
-
}
|
|
2009
|
-
})
|
|
2010
|
-
});
|
|
2011
|
-
if (!response.ok) {
|
|
2012
|
-
const body = await response.text();
|
|
2013
|
-
throw new Error(`Ollama error ${response.status}: ${body}. Is Ollama running? Try: ollama serve`);
|
|
2014
|
-
}
|
|
2015
|
-
const data = await response.json();
|
|
2016
|
-
return data.message?.content ?? "";
|
|
2017
|
-
}
|
|
2018
|
-
async summarizeWeek(payload) {
|
|
2019
|
-
const response = await fetchWithTimeout(`${this.baseUrl}/api/chat`, {
|
|
2020
|
-
method: "POST",
|
|
2021
|
-
headers: { "Content-Type": "application/json" },
|
|
2022
|
-
body: JSON.stringify({
|
|
2023
|
-
model: this.model,
|
|
2024
|
-
stream: false,
|
|
2025
|
-
messages: [
|
|
2026
|
-
{ role: "system", content: WEEKLY_SYSTEM_PROMPT },
|
|
2027
|
-
{ role: "user", content: buildWeeklyMessage(payload) }
|
|
2028
|
-
],
|
|
2029
|
-
options: {
|
|
2030
|
-
num_predict: 400,
|
|
2031
|
-
temperature: 0.3
|
|
2032
|
-
}
|
|
2033
|
-
})
|
|
2034
|
-
});
|
|
2035
|
-
if (!response.ok) {
|
|
2036
|
-
const body = await response.text();
|
|
2037
|
-
throw new Error(`Ollama error ${response.status}: ${body}. Is Ollama running? Try: ollama serve`);
|
|
2197
|
+
function splitByRepo(events) {
|
|
2198
|
+
if (events.length === 0) return [];
|
|
2199
|
+
const groups = [[events[0]]];
|
|
2200
|
+
for (let i = 1; i < events.length; i++) {
|
|
2201
|
+
const prevRepo = events[i - 1].repo_name;
|
|
2202
|
+
const currRepo = events[i].repo_name;
|
|
2203
|
+
if (currRepo !== prevRepo) {
|
|
2204
|
+
groups.push([events[i]]);
|
|
2205
|
+
} else {
|
|
2206
|
+
groups[groups.length - 1].push(events[i]);
|
|
2038
2207
|
}
|
|
2039
|
-
const data = await response.json();
|
|
2040
|
-
return data.message?.content ?? "";
|
|
2041
2208
|
}
|
|
2042
|
-
|
|
2209
|
+
return groups;
|
|
2210
|
+
}
|
|
2211
|
+
function minutesBetween(start, end) {
|
|
2212
|
+
return (new Date(end).getTime() - new Date(start).getTime()) / 6e4;
|
|
2213
|
+
}
|
|
2043
2214
|
|
|
2044
|
-
// src/
|
|
2045
|
-
function
|
|
2046
|
-
const
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2215
|
+
// src/sessions/detector.ts
|
|
2216
|
+
function getOrBuildSessions(date) {
|
|
2217
|
+
const existing = getSessionsForDate(date);
|
|
2218
|
+
const events = getEventsForDate(date);
|
|
2219
|
+
if (events.length === 0) return [];
|
|
2220
|
+
const cachedEventCount = existing.reduce((sum, sessionRow) => sum + sessionRow.event_count, 0);
|
|
2221
|
+
if (existing.length > 0 && cachedEventCount === events.length) {
|
|
2222
|
+
return existing.map((sessionRow) => ({
|
|
2223
|
+
id: sessionRow.id,
|
|
2224
|
+
date: sessionRow.date,
|
|
2225
|
+
startedAt: sessionRow.started_at,
|
|
2226
|
+
endedAt: sessionRow.ended_at,
|
|
2227
|
+
repoName: sessionRow.repo_name,
|
|
2228
|
+
durationMinutes: minutesBetween(sessionRow.started_at, sessionRow.ended_at),
|
|
2229
|
+
eventCount: sessionRow.event_count,
|
|
2230
|
+
summary: sessionRow.summary,
|
|
2231
|
+
events: getEventsBySessionId(sessionRow.id)
|
|
2232
|
+
}));
|
|
2051
2233
|
}
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2234
|
+
const savedSummaries = /* @__PURE__ */ new Map();
|
|
2235
|
+
for (const s of existing) {
|
|
2236
|
+
if (s.summary) savedSummaries.set(`${s.repo_name}|${s.started_at}`, s.summary);
|
|
2237
|
+
}
|
|
2238
|
+
clearSessionsForDate(date);
|
|
2239
|
+
return buildSessions(date, events, savedSummaries);
|
|
2240
|
+
}
|
|
2241
|
+
function buildSessions(date, events, savedSummaries = /* @__PURE__ */ new Map()) {
|
|
2242
|
+
const config = readConfig();
|
|
2243
|
+
const idleMs = config.session.idleTimeout * 60 * 1e3;
|
|
2244
|
+
const sessions = [];
|
|
2245
|
+
const timeGroups = splitByIdleGap(events, idleMs);
|
|
2246
|
+
for (const group of timeGroups) {
|
|
2247
|
+
if (group.length === 0) continue;
|
|
2248
|
+
const repoGroups = splitByRepo(group);
|
|
2249
|
+
for (const repoGroup of repoGroups) {
|
|
2250
|
+
if (repoGroup.length === 0) continue;
|
|
2251
|
+
const first = repoGroup[0];
|
|
2252
|
+
const last = repoGroup[repoGroup.length - 1];
|
|
2253
|
+
const repoName = last.repo_name ?? first.repo_name ?? null;
|
|
2254
|
+
const summary = savedSummaries.get(`${repoName}|${first.captured_at}`) ?? null;
|
|
2255
|
+
const sessionId = insertSession({
|
|
2256
|
+
date,
|
|
2257
|
+
started_at: first.captured_at,
|
|
2258
|
+
ended_at: last.captured_at,
|
|
2259
|
+
repo_name: repoName,
|
|
2260
|
+
event_count: repoGroup.length,
|
|
2261
|
+
summary
|
|
2262
|
+
});
|
|
2263
|
+
for (const event of repoGroup) {
|
|
2264
|
+
insertSessionEvent(sessionId, event.id);
|
|
2265
|
+
}
|
|
2266
|
+
sessions.push({
|
|
2267
|
+
id: sessionId,
|
|
2268
|
+
date,
|
|
2269
|
+
startedAt: first.captured_at,
|
|
2270
|
+
endedAt: last.captured_at,
|
|
2271
|
+
repoName,
|
|
2272
|
+
durationMinutes: minutesBetween(first.captured_at, last.captured_at),
|
|
2273
|
+
eventCount: repoGroup.length,
|
|
2274
|
+
summary,
|
|
2275
|
+
events: repoGroup
|
|
2276
|
+
});
|
|
2060
2277
|
}
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2278
|
+
}
|
|
2279
|
+
return sessions;
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
// src/cli/sessions.ts
|
|
2283
|
+
function registerSessions(program2) {
|
|
2284
|
+
program2.command("sessions").description("List detected work sessions").option("--date <YYYY-MM-DD>", "date to show sessions for (default: today)").action((opts) => {
|
|
2285
|
+
let date = todayDate();
|
|
2286
|
+
if (opts.date) {
|
|
2287
|
+
try {
|
|
2288
|
+
date = parseDate(opts.date);
|
|
2289
|
+
} catch (error) {
|
|
2290
|
+
log.error(error.message);
|
|
2291
|
+
process.exit(1);
|
|
2292
|
+
}
|
|
2067
2293
|
}
|
|
2068
|
-
|
|
2069
|
-
|
|
2294
|
+
const sessions = getOrBuildSessions(date);
|
|
2295
|
+
if (sessions.length === 0) {
|
|
2296
|
+
log.plain(`No sessions detected for ${date}`);
|
|
2297
|
+
return;
|
|
2070
2298
|
}
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2299
|
+
log.plain("");
|
|
2300
|
+
log.plain(import_chalk5.default.bold(` Sessions for ${date} (${sessions.length} session${sessions.length > 1 ? "s" : ""})`));
|
|
2301
|
+
log.plain("");
|
|
2302
|
+
for (const session of sessions) {
|
|
2303
|
+
const start = formatDate(session.startedAt);
|
|
2304
|
+
const end = formatDate(session.endedAt);
|
|
2305
|
+
const duration = formatDuration(session.durationMinutes);
|
|
2306
|
+
const repo = session.repoName ? import_chalk5.default.cyan(session.repoName) : import_chalk5.default.gray("no repo");
|
|
2307
|
+
log.plain(
|
|
2308
|
+
` ${import_chalk5.default.dim(start)} \u2013 ${import_chalk5.default.dim(end)} ${repo} ${import_chalk5.default.dim(duration)} ${import_chalk5.default.dim(session.eventCount + " events")}`
|
|
2075
2309
|
);
|
|
2076
|
-
|
|
2310
|
+
for (const event of session.events) {
|
|
2311
|
+
if (event.category === "note") {
|
|
2312
|
+
let text = "";
|
|
2313
|
+
try {
|
|
2314
|
+
text = JSON.parse(event.metadata ?? "{}").text ?? "";
|
|
2315
|
+
} catch {
|
|
2316
|
+
}
|
|
2317
|
+
if (text) log.plain(` ${import_chalk5.default.yellow("note")} ${import_chalk5.default.dim(text)}`);
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
log.plain("");
|
|
2322
|
+
});
|
|
2077
2323
|
}
|
|
2078
2324
|
|
|
2325
|
+
// src/cli/summary.ts
|
|
2326
|
+
var import_chalk6 = __toESM(require("chalk"));
|
|
2327
|
+
var import_fs15 = __toESM(require("fs"));
|
|
2328
|
+
var import_os6 = __toESM(require("os"));
|
|
2329
|
+
var import_path10 = __toESM(require("path"));
|
|
2330
|
+
|
|
2079
2331
|
// src/integrations/webhook.ts
|
|
2080
2332
|
async function postToWebhook(url, text) {
|
|
2081
2333
|
const response = await fetchWithTimeout(
|
|
@@ -2121,7 +2373,7 @@ function eventRowToPayload(row) {
|
|
|
2121
2373
|
};
|
|
2122
2374
|
if (row.category === "other" && typeof metadata.hint === "string") {
|
|
2123
2375
|
payload.hint = metadata.hint;
|
|
2124
|
-
const { hint:
|
|
2376
|
+
const { hint: _hint, tool: _tool, ...rest } = metadata;
|
|
2125
2377
|
payload.metadata = rest;
|
|
2126
2378
|
}
|
|
2127
2379
|
return payload;
|
|
@@ -2142,25 +2394,25 @@ async function handleWebhook(opts, config, summary) {
|
|
|
2142
2394
|
if (webhookUrl) {
|
|
2143
2395
|
try {
|
|
2144
2396
|
log.info("Posting to webhook\u2026");
|
|
2145
|
-
const username =
|
|
2397
|
+
const username = import_os6.default.userInfo().username;
|
|
2146
2398
|
const body = isSlackWebhookUrl(webhookUrl) ? formatForSlack(summary, username) : summary;
|
|
2147
2399
|
await postToWebhook(webhookUrl, body);
|
|
2148
2400
|
log.success("Posted.");
|
|
2149
|
-
} catch (
|
|
2150
|
-
log.error(`Webhook failed: ${
|
|
2401
|
+
} catch (error) {
|
|
2402
|
+
log.error(`Webhook failed: ${error.message}`);
|
|
2151
2403
|
}
|
|
2152
2404
|
} else if (opts.slack && !config.integrations?.slackWebhook) {
|
|
2153
2405
|
log.warn("--slack requires integrations.slackWebhook to be set in config");
|
|
2154
2406
|
}
|
|
2155
2407
|
}
|
|
2156
2408
|
function registerSummary(program2) {
|
|
2157
|
-
program2.command("summary").description("Generate an AI work summary").option("--date <YYYY-MM-DD>", "date to summarize (default: today)").option("--week", "summarize the current week").option("--fresh", "bypass cache and regenerate").option("--slack", "post summary to Slack webhook from config").option("--webhook <url>", "post summary to a custom webhook URL").action(async (opts) => {
|
|
2409
|
+
program2.command("summary").description("Generate an AI work summary").option("--date <YYYY-MM-DD>", "date to summarize (default: today)").option("--week", "summarize the current week").option("--fresh", "bypass cache and regenerate").option("--verbose", "show all captured events including those not sent to AI").option("--slack", "post summary to Slack webhook from config").option("--webhook <url>", "post summary to a custom webhook URL").action(async (opts) => {
|
|
2158
2410
|
const config = readConfig();
|
|
2159
2411
|
let provider;
|
|
2160
2412
|
try {
|
|
2161
2413
|
provider = getProvider(config);
|
|
2162
|
-
} catch (
|
|
2163
|
-
log.error(
|
|
2414
|
+
} catch (error) {
|
|
2415
|
+
log.error(error.message);
|
|
2164
2416
|
process.exit(1);
|
|
2165
2417
|
}
|
|
2166
2418
|
if (opts.week) {
|
|
@@ -2180,8 +2432,8 @@ function registerSummary(program2) {
|
|
|
2180
2432
|
}
|
|
2181
2433
|
const cacheKey = isoWeekKey(today);
|
|
2182
2434
|
const cacheFile = import_path10.default.join(WEEKLY_CACHE_DIR, `${cacheKey}.md`);
|
|
2183
|
-
if (!opts.fresh &&
|
|
2184
|
-
const cached =
|
|
2435
|
+
if (!opts.fresh && import_fs15.default.existsSync(cacheFile)) {
|
|
2436
|
+
const cached = import_fs15.default.readFileSync(cacheFile, "utf-8");
|
|
2185
2437
|
log.plain("");
|
|
2186
2438
|
log.plain(cached);
|
|
2187
2439
|
log.plain("");
|
|
@@ -2197,11 +2449,11 @@ function registerSummary(program2) {
|
|
|
2197
2449
|
log.plain("");
|
|
2198
2450
|
log.plain(summary);
|
|
2199
2451
|
log.plain("");
|
|
2200
|
-
|
|
2201
|
-
|
|
2452
|
+
import_fs15.default.mkdirSync(WEEKLY_CACHE_DIR, { recursive: true });
|
|
2453
|
+
import_fs15.default.writeFileSync(cacheFile, summary, { encoding: "utf-8", mode: 384 });
|
|
2202
2454
|
await handleWebhook(opts, config, summary);
|
|
2203
|
-
} catch (
|
|
2204
|
-
log.error(`Weekly summary failed [${config.ai.provider}]: ${
|
|
2455
|
+
} catch (error) {
|
|
2456
|
+
log.error(`Weekly summary failed [${config.ai.provider}]: ${error.message}`);
|
|
2205
2457
|
process.exit(1);
|
|
2206
2458
|
}
|
|
2207
2459
|
return;
|
|
@@ -2210,8 +2462,8 @@ function registerSummary(program2) {
|
|
|
2210
2462
|
if (opts.date) {
|
|
2211
2463
|
try {
|
|
2212
2464
|
date = parseDate(opts.date);
|
|
2213
|
-
} catch (
|
|
2214
|
-
log.error(
|
|
2465
|
+
} catch (error) {
|
|
2466
|
+
log.error(error.message);
|
|
2215
2467
|
process.exit(1);
|
|
2216
2468
|
}
|
|
2217
2469
|
}
|
|
@@ -2243,12 +2495,35 @@ function registerSummary(program2) {
|
|
|
2243
2495
|
return;
|
|
2244
2496
|
}
|
|
2245
2497
|
}
|
|
2498
|
+
const excluded = buildExcludedSet(config.summary?.includeCategories ?? []);
|
|
2499
|
+
const allSessionPayloads = sessions.map((session) => sessionToPayload(session, date));
|
|
2500
|
+
if (opts.verbose) {
|
|
2501
|
+
log.plain("");
|
|
2502
|
+
log.plain(import_chalk6.default.bold(" Captured events (--verbose):"));
|
|
2503
|
+
for (const sessionPayload of allSessionPayloads) {
|
|
2504
|
+
const branch = sessionPayload.branch ? ` (${sessionPayload.branch})` : "";
|
|
2505
|
+
log.plain(
|
|
2506
|
+
import_chalk6.default.dim(
|
|
2507
|
+
`
|
|
2508
|
+
${sessionPayload.project ?? "unknown"}${branch} \u2014 ${formatDuration(sessionPayload.durationMinutes)}`
|
|
2509
|
+
)
|
|
2510
|
+
);
|
|
2511
|
+
for (const event of sessionPayload.events) {
|
|
2512
|
+
const tag = excluded.has(event.category) ? import_chalk6.default.dim(` [excluded] ${event.category}`) : ` ${event.category}`;
|
|
2513
|
+
const hint = event.hint ? import_chalk6.default.dim(` [${event.hint}]`) : "";
|
|
2514
|
+
const meta = Object.keys(event.metadata).length > 0 ? import_chalk6.default.dim(` ${JSON.stringify(event.metadata)}`) : "";
|
|
2515
|
+
log.plain(` ${tag}${hint}${meta}`);
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
log.plain("");
|
|
2519
|
+
}
|
|
2246
2520
|
log.plain("");
|
|
2247
2521
|
log.info(`Generating summary via ${import_chalk6.default.cyan(config.ai.provider)}\u2026`);
|
|
2248
2522
|
const allSummaries = [];
|
|
2249
2523
|
const jsonSessions = [];
|
|
2250
|
-
for (
|
|
2251
|
-
const
|
|
2524
|
+
for (let index = 0; index < sessions.length; index++) {
|
|
2525
|
+
const session = sessions[index];
|
|
2526
|
+
const sessionPayload = { date, sessions: [allSessionPayloads[index]] };
|
|
2252
2527
|
try {
|
|
2253
2528
|
const bullets = await provider.summarize(sessionPayload);
|
|
2254
2529
|
const project = session.repoName ?? "unknown";
|
|
@@ -2267,8 +2542,8 @@ ${bullets.trim()}`;
|
|
|
2267
2542
|
bullets: parseBullets(bullets)
|
|
2268
2543
|
});
|
|
2269
2544
|
}
|
|
2270
|
-
} catch (
|
|
2271
|
-
log.error(`Summary failed [${config.ai.provider}]: ${
|
|
2545
|
+
} catch (error) {
|
|
2546
|
+
log.error(`Summary failed [${config.ai.provider}]: ${error.message}`);
|
|
2272
2547
|
process.exit(1);
|
|
2273
2548
|
}
|
|
2274
2549
|
}
|
|
@@ -2280,12 +2555,20 @@ ${bullets.trim()}`;
|
|
|
2280
2555
|
log.plain(fullSummary);
|
|
2281
2556
|
}
|
|
2282
2557
|
log.plain("");
|
|
2558
|
+
const excludedCounts = countExcludedEvents(allSessionPayloads, excluded);
|
|
2559
|
+
if (excludedCounts.size > 0) {
|
|
2560
|
+
const total = [...excludedCounts.values()].reduce((sum, count) => sum + count, 0);
|
|
2561
|
+
const breakdown = [...excludedCounts.entries()].map(([category, count]) => `${category} \xD7${count}`).join(", ");
|
|
2562
|
+
log.plain(import_chalk6.default.dim(` \u2139 ${total} event${total === 1 ? "" : "s"} not included in summary (${breakdown})`));
|
|
2563
|
+
log.plain(import_chalk6.default.dim(" Use --verbose to inspect \xB7 workscribe config set summary.includeCategories to opt in"));
|
|
2564
|
+
log.plain("");
|
|
2565
|
+
}
|
|
2283
2566
|
await handleWebhook(opts, config, fullSummary);
|
|
2284
2567
|
});
|
|
2285
2568
|
}
|
|
2286
2569
|
|
|
2287
2570
|
// src/cli/export.ts
|
|
2288
|
-
var
|
|
2571
|
+
var import_fs16 = __toESM(require("fs"));
|
|
2289
2572
|
var import_path11 = __toESM(require("path"));
|
|
2290
2573
|
function eventRowToPayload2(row) {
|
|
2291
2574
|
let metadata = {};
|
|
@@ -2343,8 +2626,8 @@ function registerExport(program2) {
|
|
|
2343
2626
|
log.info(`Generating summary via ${config.ai.provider}\u2026`);
|
|
2344
2627
|
const summary = await provider.summarize(payload);
|
|
2345
2628
|
const outputPath = opts.output ?? import_path11.default.join(EXPORTS_DIR, `workscribe-${date}.md`);
|
|
2346
|
-
if (!
|
|
2347
|
-
|
|
2629
|
+
if (!import_fs16.default.existsSync(EXPORTS_DIR)) import_fs16.default.mkdirSync(EXPORTS_DIR, { recursive: true, mode: 448 });
|
|
2630
|
+
import_fs16.default.writeFileSync(outputPath, `# Work Summary \u2014 ${date}
|
|
2348
2631
|
|
|
2349
2632
|
${summary}
|
|
2350
2633
|
`, { encoding: "utf-8", mode: 384 });
|
|
@@ -2416,22 +2699,22 @@ function printConfig(obj, indent) {
|
|
|
2416
2699
|
}
|
|
2417
2700
|
|
|
2418
2701
|
// src/cli/pause.ts
|
|
2419
|
-
var
|
|
2702
|
+
var import_fs17 = __toESM(require("fs"));
|
|
2420
2703
|
function registerPause(program2) {
|
|
2421
2704
|
program2.command("pause").description("Pause capture temporarily").action(() => {
|
|
2422
|
-
if (
|
|
2705
|
+
if (import_fs17.default.existsSync(PAUSE_FLAG_PATH)) {
|
|
2423
2706
|
log.info("Capture is already paused. Run workscribe resume to restart.");
|
|
2424
2707
|
return;
|
|
2425
2708
|
}
|
|
2426
|
-
|
|
2709
|
+
import_fs17.default.writeFileSync(PAUSE_FLAG_PATH, "", { mode: 384 });
|
|
2427
2710
|
log.success("Capture paused. Run workscribe resume to restart.");
|
|
2428
2711
|
});
|
|
2429
2712
|
program2.command("resume").description("Resume capture after a pause").action(() => {
|
|
2430
|
-
if (!
|
|
2713
|
+
if (!import_fs17.default.existsSync(PAUSE_FLAG_PATH)) {
|
|
2431
2714
|
log.info("Capture is not paused.");
|
|
2432
2715
|
return;
|
|
2433
2716
|
}
|
|
2434
|
-
|
|
2717
|
+
import_fs17.default.rmSync(PAUSE_FLAG_PATH);
|
|
2435
2718
|
log.success("Capture resumed.");
|
|
2436
2719
|
});
|
|
2437
2720
|
}
|
|
@@ -2516,18 +2799,19 @@ function registerClear(program2) {
|
|
|
2516
2799
|
}
|
|
2517
2800
|
|
|
2518
2801
|
// src/cli/uninstall.ts
|
|
2519
|
-
var
|
|
2802
|
+
var import_fs18 = __toESM(require("fs"));
|
|
2520
2803
|
var import_path12 = __toESM(require("path"));
|
|
2521
|
-
var
|
|
2804
|
+
var import_os7 = __toESM(require("os"));
|
|
2522
2805
|
var import_prompts3 = __toESM(require("prompts"));
|
|
2523
2806
|
var import_chalk9 = __toESM(require("chalk"));
|
|
2524
2807
|
function detectShellRcFiles() {
|
|
2525
2808
|
const candidates = [
|
|
2526
|
-
{ name: "zsh", rcFile: import_path12.default.join(
|
|
2527
|
-
{ name: "bash", rcFile: import_path12.default.join(
|
|
2528
|
-
{ name: "bash", rcFile: import_path12.default.join(
|
|
2809
|
+
{ name: "zsh", rcFile: import_path12.default.join(import_os7.default.homedir(), ".zshrc") },
|
|
2810
|
+
{ name: "bash", rcFile: import_path12.default.join(import_os7.default.homedir(), ".bashrc") },
|
|
2811
|
+
{ name: "bash", rcFile: import_path12.default.join(import_os7.default.homedir(), ".bash_profile") },
|
|
2812
|
+
{ name: "fish", rcFile: import_path12.default.join(import_os7.default.homedir(), ".config", "fish", "conf.d", "workscribe.fish") }
|
|
2529
2813
|
];
|
|
2530
|
-
return candidates.filter((candidate) =>
|
|
2814
|
+
return candidates.filter((candidate) => import_fs18.default.existsSync(candidate.rcFile) && isHookInstalled(candidate.rcFile));
|
|
2531
2815
|
}
|
|
2532
2816
|
function registerUninstall(program2) {
|
|
2533
2817
|
program2.command("uninstall").description("Remove shell hooks and optionally delete all Workscribe data").option("--hooks-only", "remove shell hooks only, skip data deletion prompt (used by npm preuninstall)").action(async (opts) => {
|
|
@@ -2537,16 +2821,23 @@ function registerUninstall(program2) {
|
|
|
2537
2821
|
log.info("No shell hooks found");
|
|
2538
2822
|
} else {
|
|
2539
2823
|
for (const { name, rcFile } of hooked) {
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2824
|
+
if (name === "fish") {
|
|
2825
|
+
import_fs18.default.unlinkSync(rcFile);
|
|
2826
|
+
log.success(`Shell hook removed: ${rcFile}`);
|
|
2827
|
+
log.plain(` ${import_chalk9.default.dim("Restart your terminal to take effect")}`);
|
|
2828
|
+
} else {
|
|
2829
|
+
removeHook(rcFile);
|
|
2830
|
+
log.success(`Shell hook removed from ${rcFile}`);
|
|
2831
|
+
log.plain(` ${import_chalk9.default.dim(`Run: source ${rcFile}`)} (or restart your terminal to take effect)`);
|
|
2832
|
+
log.plain(
|
|
2833
|
+
` ${import_chalk9.default.dim(`${name === "zsh" ? "unfunction" : "unset -f"} _workscribe_preexec _workscribe_precmd`)}`
|
|
2834
|
+
);
|
|
2835
|
+
}
|
|
2546
2836
|
}
|
|
2547
2837
|
}
|
|
2548
2838
|
if (opts.hooksOnly) return;
|
|
2549
|
-
|
|
2839
|
+
const existingDirs = [...new Set(REQUIRED_DIRS)].filter((dir) => import_fs18.default.existsSync(dir));
|
|
2840
|
+
if (existingDirs.length > 0) {
|
|
2550
2841
|
log.plain("");
|
|
2551
2842
|
const { deleteData } = await (0, import_prompts3.default)({
|
|
2552
2843
|
type: "confirm",
|
|
@@ -2555,8 +2846,10 @@ function registerUninstall(program2) {
|
|
|
2555
2846
|
initial: false
|
|
2556
2847
|
});
|
|
2557
2848
|
if (deleteData) {
|
|
2558
|
-
|
|
2559
|
-
|
|
2849
|
+
for (const dir of existingDirs) {
|
|
2850
|
+
import_fs18.default.rmSync(dir, { recursive: true, force: true });
|
|
2851
|
+
log.success(`Deleted ${dir}`);
|
|
2852
|
+
}
|
|
2560
2853
|
} else {
|
|
2561
2854
|
log.info(`Data kept at ${WORKSCRIBE_DIR}`);
|
|
2562
2855
|
}
|
|
@@ -2568,7 +2861,7 @@ function registerUninstall(program2) {
|
|
|
2568
2861
|
}
|
|
2569
2862
|
|
|
2570
2863
|
// src/cli/profile.ts
|
|
2571
|
-
var
|
|
2864
|
+
var import_fs19 = __toESM(require("fs"));
|
|
2572
2865
|
var import_path13 = __toESM(require("path"));
|
|
2573
2866
|
var import_chalk10 = __toESM(require("chalk"));
|
|
2574
2867
|
var MIN_SUGGEST_COUNT = 5;
|
|
@@ -2615,7 +2908,7 @@ function registerProfile(program2) {
|
|
|
2615
2908
|
log.plain(import_chalk10.default.dim(" " + "\u2500".repeat(60)));
|
|
2616
2909
|
for (const entry of entries) {
|
|
2617
2910
|
const badge = activeNames.has(entry.name) ? import_chalk10.default.green(" [active]") : "";
|
|
2618
|
-
const installed =
|
|
2911
|
+
const installed = import_fs19.default.existsSync(import_path13.default.join(PROFILES_DIR, `${entry.name}.json`)) ? import_chalk10.default.dim(" installed") : "";
|
|
2619
2912
|
log.plain(` ${import_chalk10.default.cyan(entry.name.padEnd(18))} ${entry.displayName.padEnd(28)}${badge}${installed}`);
|
|
2620
2913
|
if (entry.description) log.plain(` ${import_chalk10.default.dim(" ".repeat(18) + entry.description)}`);
|
|
2621
2914
|
}
|
|
@@ -2652,7 +2945,7 @@ function registerProfile(program2) {
|
|
|
2652
2945
|
}
|
|
2653
2946
|
activeNames.delete(name);
|
|
2654
2947
|
const cachePath = import_path13.default.join(PROFILES_DIR, `${name}.json`);
|
|
2655
|
-
if (
|
|
2948
|
+
if (import_fs19.default.existsSync(cachePath)) import_fs19.default.unlinkSync(cachePath);
|
|
2656
2949
|
log.success(`Profile "${name}" removed`);
|
|
2657
2950
|
}
|
|
2658
2951
|
config.capture.profiles = [...activeNames];
|
|
@@ -2716,14 +3009,37 @@ function registerProfile(program2) {
|
|
|
2716
3009
|
const suggestions = [];
|
|
2717
3010
|
for (const entry of manifest) {
|
|
2718
3011
|
if (activeNames.has(entry.name)) continue;
|
|
2719
|
-
const matchedCount = toolCounts.filter(
|
|
3012
|
+
const matchedCount = toolCounts.filter(
|
|
3013
|
+
(toolCount) => entry.keywords.some((keyword) => toolCount.tool.toLowerCase().includes(keyword.toLowerCase()))
|
|
3014
|
+
).reduce((total, toolCount) => total + toolCount.count, 0);
|
|
2720
3015
|
if (matchedCount >= MIN_SUGGEST_COUNT) {
|
|
2721
3016
|
suggestions.push({ entry, count: matchedCount });
|
|
2722
3017
|
}
|
|
2723
3018
|
}
|
|
2724
3019
|
suggestions.sort((a, b) => b.count - a.count);
|
|
2725
3020
|
if (suggestions.length === 0) {
|
|
2726
|
-
|
|
3021
|
+
const activeEntries = manifest.filter((entry) => activeNames.has(entry.name));
|
|
3022
|
+
const isToolCoveredByActiveProfile = (tool) => activeEntries.some(
|
|
3023
|
+
(entry) => entry.keywords.some((keyword) => tool.toLowerCase().includes(keyword.toLowerCase()))
|
|
3024
|
+
);
|
|
3025
|
+
const uncoveredTools = toolCounts.filter((toolCount) => !isToolCoveredByActiveProfile(toolCount.tool));
|
|
3026
|
+
if (uncoveredTools.length === 0) {
|
|
3027
|
+
log.plain("");
|
|
3028
|
+
log.plain("Your active profiles cover all unclassified commands.");
|
|
3029
|
+
log.plain(import_chalk10.default.dim(" Future captures will be classified correctly."));
|
|
3030
|
+
log.plain("");
|
|
3031
|
+
} else {
|
|
3032
|
+
log.plain("");
|
|
3033
|
+
log.plain("No profile matches found for your unclassified commands.");
|
|
3034
|
+
log.plain("");
|
|
3035
|
+
log.plain(import_chalk10.default.dim(" Top unclassified tools:"));
|
|
3036
|
+
for (const { tool, count } of uncoveredTools.slice(0, 5)) {
|
|
3037
|
+
log.plain(import_chalk10.default.dim(` ${tool.padEnd(24)} ${count} event${count === 1 ? "" : "s"}`));
|
|
3038
|
+
}
|
|
3039
|
+
log.plain("");
|
|
3040
|
+
log.plain(import_chalk10.default.dim(" Run workscribe events --category other to review them."));
|
|
3041
|
+
log.plain("");
|
|
3042
|
+
}
|
|
2727
3043
|
return;
|
|
2728
3044
|
}
|
|
2729
3045
|
log.plain("");
|
|
@@ -2735,14 +3051,281 @@ function registerProfile(program2) {
|
|
|
2735
3051
|
);
|
|
2736
3052
|
}
|
|
2737
3053
|
log.plain("");
|
|
2738
|
-
log.plain(
|
|
3054
|
+
log.plain(
|
|
3055
|
+
` Run: ${import_chalk10.default.cyan("workscribe profile add " + suggestions.map((suggestion) => suggestion.entry.name).join(" "))}`
|
|
3056
|
+
);
|
|
3057
|
+
log.plain("");
|
|
3058
|
+
const allCoveredEntries = [
|
|
3059
|
+
...manifest.filter((entry) => activeNames.has(entry.name)),
|
|
3060
|
+
...suggestions.map((suggestion) => suggestion.entry)
|
|
3061
|
+
];
|
|
3062
|
+
const isToolCoveredAfterInstall = (tool) => allCoveredEntries.some(
|
|
3063
|
+
(entry) => entry.keywords.some((keyword) => tool.toLowerCase().includes(keyword.toLowerCase()))
|
|
3064
|
+
);
|
|
3065
|
+
const stillUncovered = toolCounts.filter((toolCount) => !isToolCoveredAfterInstall(toolCount.tool)).slice(0, 5);
|
|
3066
|
+
if (stillUncovered.length > 0) {
|
|
3067
|
+
log.plain(import_chalk10.default.dim(" Still uncovered after installing:"));
|
|
3068
|
+
for (const { tool, count } of stillUncovered) {
|
|
3069
|
+
log.plain(import_chalk10.default.dim(` ${tool.padEnd(24)} ${count} event${count === 1 ? "" : "s"}`));
|
|
3070
|
+
}
|
|
3071
|
+
log.plain("");
|
|
3072
|
+
}
|
|
3073
|
+
});
|
|
3074
|
+
}
|
|
3075
|
+
|
|
3076
|
+
// src/cli/note.ts
|
|
3077
|
+
function registerNote(program2) {
|
|
3078
|
+
program2.command("note <text>").description("Attach a free-text annotation to the current session").action((text) => {
|
|
3079
|
+
const { repoName, branch } = getGitContext(process.cwd());
|
|
3080
|
+
insertEvent({
|
|
3081
|
+
captured_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3082
|
+
command: "workscribe note",
|
|
3083
|
+
cwd: process.cwd(),
|
|
3084
|
+
exit_code: null,
|
|
3085
|
+
duration_ms: null,
|
|
3086
|
+
repo_name: repoName,
|
|
3087
|
+
branch,
|
|
3088
|
+
category: "note",
|
|
3089
|
+
metadata: JSON.stringify({ text })
|
|
3090
|
+
});
|
|
3091
|
+
log.success(`Note saved${repoName ? ` to ${repoName}` : ""}`);
|
|
3092
|
+
});
|
|
3093
|
+
}
|
|
3094
|
+
|
|
3095
|
+
// src/cli/week.ts
|
|
3096
|
+
var import_chalk11 = __toESM(require("chalk"));
|
|
3097
|
+
var import_fs20 = __toESM(require("fs"));
|
|
3098
|
+
var import_path14 = __toESM(require("path"));
|
|
3099
|
+
var import_os8 = __toESM(require("os"));
|
|
3100
|
+
function eventRowToPayload3(row) {
|
|
3101
|
+
let metadata = {};
|
|
3102
|
+
try {
|
|
3103
|
+
if (row.metadata) metadata = JSON.parse(row.metadata);
|
|
3104
|
+
} catch {
|
|
3105
|
+
}
|
|
3106
|
+
const payload = {
|
|
3107
|
+
category: row.category,
|
|
3108
|
+
metadata,
|
|
3109
|
+
exitCode: row.exit_code,
|
|
3110
|
+
durationMs: row.duration_ms
|
|
3111
|
+
};
|
|
3112
|
+
if (row.category === "other" && typeof metadata.hint === "string") {
|
|
3113
|
+
payload.hint = metadata.hint;
|
|
3114
|
+
const { hint: _hint, tool: _tool, ...rest } = metadata;
|
|
3115
|
+
payload.metadata = rest;
|
|
3116
|
+
}
|
|
3117
|
+
return payload;
|
|
3118
|
+
}
|
|
3119
|
+
function sessionToPayload2(session, date) {
|
|
3120
|
+
return {
|
|
3121
|
+
date,
|
|
3122
|
+
project: session.repoName,
|
|
3123
|
+
branch: session.events[0]?.branch ?? null,
|
|
3124
|
+
startTime: formatDate(session.startedAt),
|
|
3125
|
+
endTime: formatDate(session.endedAt),
|
|
3126
|
+
durationMinutes: session.durationMinutes,
|
|
3127
|
+
events: session.events.map(eventRowToPayload3)
|
|
3128
|
+
};
|
|
3129
|
+
}
|
|
3130
|
+
async function postSummary(opts, config, summary) {
|
|
3131
|
+
const webhookUrl = opts.webhook ?? (opts.slack ? config.integrations?.slackWebhook : void 0);
|
|
3132
|
+
if (!webhookUrl) {
|
|
3133
|
+
if (opts.slack) log.warn("--slack requires integrations.slackWebhook to be set in config");
|
|
3134
|
+
return;
|
|
3135
|
+
}
|
|
3136
|
+
try {
|
|
3137
|
+
log.info("Posting to webhook\u2026");
|
|
3138
|
+
const username = import_os8.default.userInfo().username;
|
|
3139
|
+
const body = isSlackWebhookUrl(webhookUrl) ? formatForSlack(summary, username) : summary;
|
|
3140
|
+
await postToWebhook(webhookUrl, body);
|
|
3141
|
+
log.success("Posted.");
|
|
3142
|
+
} catch (error) {
|
|
3143
|
+
log.error(`Webhook failed: ${error.message}`);
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
function registerWeek(program2) {
|
|
3147
|
+
program2.command("week").description("Generate an AI weekly digest (Mon\u2013today)").option("--fresh", "bypass cache and regenerate").option("--slack", "post to Slack webhook from config").option("--webhook <url>", "post to a custom webhook URL").action(async (opts) => {
|
|
3148
|
+
const config = readConfig();
|
|
3149
|
+
let provider;
|
|
3150
|
+
try {
|
|
3151
|
+
provider = getProvider(config);
|
|
3152
|
+
} catch (error) {
|
|
3153
|
+
log.error(error.message);
|
|
3154
|
+
process.exit(1);
|
|
3155
|
+
}
|
|
3156
|
+
const today = todayDate();
|
|
3157
|
+
const weekStart = startOfWeek(today);
|
|
3158
|
+
const days = eachDayOfRange(weekStart, today);
|
|
3159
|
+
const weeklyDays = [];
|
|
3160
|
+
for (const day of days) {
|
|
3161
|
+
const sessions = getOrBuildSessions(day);
|
|
3162
|
+
if (sessions.length > 0) {
|
|
3163
|
+
weeklyDays.push({ date: day, sessions: sessions.map((session) => sessionToPayload2(session, day)) });
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
if (weeklyDays.length === 0) {
|
|
3167
|
+
log.plain("No activity captured this week.");
|
|
3168
|
+
return;
|
|
3169
|
+
}
|
|
3170
|
+
const cacheKey = isoWeekKey(today);
|
|
3171
|
+
const cacheFile = import_path14.default.join(WEEKLY_CACHE_DIR, `${cacheKey}.md`);
|
|
3172
|
+
if (!opts.fresh && import_fs20.default.existsSync(cacheFile)) {
|
|
3173
|
+
const cached = import_fs20.default.readFileSync(cacheFile, "utf-8");
|
|
3174
|
+
log.plain("");
|
|
3175
|
+
log.plain(cached);
|
|
3176
|
+
log.plain("");
|
|
3177
|
+
log.plain(import_chalk11.default.dim(" (cached \u2014 use --fresh to regenerate)"));
|
|
3178
|
+
await postSummary(opts, config, cached);
|
|
3179
|
+
return;
|
|
3180
|
+
}
|
|
3181
|
+
const payload = { weekStart, weekEnd: today, days: weeklyDays };
|
|
3182
|
+
log.plain("");
|
|
3183
|
+
log.info(`Generating weekly digest via ${import_chalk11.default.cyan(config.ai.provider)}\u2026`);
|
|
3184
|
+
try {
|
|
3185
|
+
const summary = provider.summarizeWeek ? await provider.summarizeWeek(payload) : await provider.summarize({ date: today, sessions: weeklyDays.flatMap((day) => day.sessions) });
|
|
3186
|
+
log.plain("");
|
|
3187
|
+
log.plain(summary);
|
|
3188
|
+
log.plain("");
|
|
3189
|
+
import_fs20.default.mkdirSync(WEEKLY_CACHE_DIR, { recursive: true });
|
|
3190
|
+
import_fs20.default.writeFileSync(cacheFile, summary, { encoding: "utf-8", mode: 384 });
|
|
3191
|
+
await postSummary(opts, config, summary);
|
|
3192
|
+
} catch (error) {
|
|
3193
|
+
log.error(`Weekly digest failed [${config.ai.provider}]: ${error.message}`);
|
|
3194
|
+
process.exit(1);
|
|
3195
|
+
}
|
|
3196
|
+
});
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
// src/cli/bug-report.ts
|
|
3200
|
+
var import_fs21 = __toESM(require("fs"));
|
|
3201
|
+
var import_os9 = __toESM(require("os"));
|
|
3202
|
+
var import_path15 = __toESM(require("path"));
|
|
3203
|
+
var import_chalk12 = __toESM(require("chalk"));
|
|
3204
|
+
var GITHUB_ISSUES_URL = "https://github.com/workscribe/workscribe/issues/new";
|
|
3205
|
+
var DEBUG_LOG_TAIL_LINES = 100;
|
|
3206
|
+
function tailFile(filePath, lines) {
|
|
3207
|
+
if (!import_fs21.default.existsSync(filePath)) return "(no debug log found \u2014 set WORKSCRIBE_DEBUG=1 to enable)";
|
|
3208
|
+
const content = import_fs21.default.readFileSync(filePath, "utf-8");
|
|
3209
|
+
const allLines = content.split("\n");
|
|
3210
|
+
return allLines.slice(-lines).join("\n");
|
|
3211
|
+
}
|
|
3212
|
+
function redactConfig() {
|
|
3213
|
+
try {
|
|
3214
|
+
const config = readConfig();
|
|
3215
|
+
if (config.ai.apiKey) config.ai.apiKey = "<redacted>";
|
|
3216
|
+
if (config.integrations?.slackWebhook) config.integrations.slackWebhook = "<redacted>";
|
|
3217
|
+
if (config.integrations?.webhook) config.integrations.webhook = "<redacted>";
|
|
3218
|
+
return JSON.stringify(config, null, 2);
|
|
3219
|
+
} catch {
|
|
3220
|
+
return "(could not read config)";
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
function registerBugReport(program2) {
|
|
3224
|
+
program2.command("bug-report").description("Generate a diagnostic report for filing a bug").action(() => {
|
|
3225
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
3226
|
+
const reportPath = import_path15.default.join(WORKSCRIBE_DIR, `bug-report-${timestamp}.txt`);
|
|
3227
|
+
const systemInfo = [
|
|
3228
|
+
`Workscribe version : ${"1.12.0"}`,
|
|
3229
|
+
`Node.js version : ${process.version}`,
|
|
3230
|
+
`Platform : ${process.platform}`,
|
|
3231
|
+
`Architecture : ${process.arch}`,
|
|
3232
|
+
`OS release : ${import_os9.default.release()}`,
|
|
3233
|
+
`Config path : ${CONFIG_PATH}`,
|
|
3234
|
+
`Data directory : ${WORKSCRIBE_DIR}`
|
|
3235
|
+
].join("\n");
|
|
3236
|
+
const report = [
|
|
3237
|
+
"=== Workscribe Bug Report ===",
|
|
3238
|
+
`Generated: ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
3239
|
+
"",
|
|
3240
|
+
"--- System ---",
|
|
3241
|
+
systemInfo,
|
|
3242
|
+
"",
|
|
3243
|
+
"--- Config (secrets redacted) ---",
|
|
3244
|
+
redactConfig(),
|
|
3245
|
+
"",
|
|
3246
|
+
`--- Debug log (last ${DEBUG_LOG_TAIL_LINES} lines) ---`,
|
|
3247
|
+
tailFile(DEBUG_LOG_PATH, DEBUG_LOG_TAIL_LINES)
|
|
3248
|
+
].join("\n");
|
|
3249
|
+
try {
|
|
3250
|
+
if (!import_fs21.default.existsSync(WORKSCRIBE_DIR)) {
|
|
3251
|
+
import_fs21.default.mkdirSync(WORKSCRIBE_DIR, { recursive: true, mode: 448 });
|
|
3252
|
+
}
|
|
3253
|
+
import_fs21.default.writeFileSync(reportPath, report, { mode: 384 });
|
|
3254
|
+
} catch (error) {
|
|
3255
|
+
log.error(`Could not write report: ${error.message}`);
|
|
3256
|
+
process.exit(1);
|
|
3257
|
+
}
|
|
3258
|
+
const params = new URLSearchParams({
|
|
3259
|
+
title: `Bug report \u2014 workscribe ${"1.12.0"}`,
|
|
3260
|
+
body: `<!-- Attach ${import_path15.default.basename(reportPath)} or paste the relevant sections below -->
|
|
3261
|
+
|
|
3262
|
+
**Describe the issue:**
|
|
3263
|
+
|
|
3264
|
+
**Steps to reproduce:**
|
|
3265
|
+
|
|
3266
|
+
**Expected behaviour:**`
|
|
3267
|
+
});
|
|
3268
|
+
log.success(`Report written to: ${import_chalk12.default.cyan(reportPath)}`);
|
|
3269
|
+
log.plain("");
|
|
3270
|
+
log.plain(` Open a GitHub issue:`);
|
|
3271
|
+
log.plain(` ${import_chalk12.default.cyan(`${GITHUB_ISSUES_URL}?${params}`)}`);
|
|
3272
|
+
log.plain("");
|
|
3273
|
+
log.plain(import_chalk12.default.dim(` Attach the report or paste the relevant sections into the issue.`));
|
|
2739
3274
|
log.plain("");
|
|
2740
3275
|
});
|
|
2741
3276
|
}
|
|
2742
3277
|
|
|
3278
|
+
// src/utils/update-check.ts
|
|
3279
|
+
var import_fs22 = __toESM(require("fs"));
|
|
3280
|
+
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
3281
|
+
function readCache() {
|
|
3282
|
+
try {
|
|
3283
|
+
return JSON.parse(import_fs22.default.readFileSync(UPDATE_CHECK_PATH, "utf-8"));
|
|
3284
|
+
} catch {
|
|
3285
|
+
return null;
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
function writeCache(cache) {
|
|
3289
|
+
try {
|
|
3290
|
+
import_fs22.default.writeFileSync(UPDATE_CHECK_PATH, JSON.stringify(cache), { mode: 384 });
|
|
3291
|
+
} catch {
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
3294
|
+
function isNewer(latest, current) {
|
|
3295
|
+
const parse = (version) => version.replace(/^v/, "").split(".").map(Number);
|
|
3296
|
+
const [latestMajor, latestMinor, latestPatch] = parse(latest);
|
|
3297
|
+
const [currentMajor, currentMinor, currentPatch] = parse(current);
|
|
3298
|
+
if (latestMajor !== currentMajor) return latestMajor > currentMajor;
|
|
3299
|
+
if (latestMinor !== currentMinor) return latestMinor > currentMinor;
|
|
3300
|
+
return latestPatch > currentPatch;
|
|
3301
|
+
}
|
|
3302
|
+
function checkForUpdate() {
|
|
3303
|
+
const current = "1.12.0";
|
|
3304
|
+
const cache = readCache();
|
|
3305
|
+
if (cache && isNewer(cache.latestVersion, current)) {
|
|
3306
|
+
process.stderr.write(
|
|
3307
|
+
`
|
|
3308
|
+
Update available ${current} \u2192 ${cache.latestVersion}
|
|
3309
|
+
Run: npm install -g @workscribe/cli
|
|
3310
|
+
|
|
3311
|
+
`
|
|
3312
|
+
);
|
|
3313
|
+
}
|
|
3314
|
+
const now = Date.now();
|
|
3315
|
+
if (!cache || now - cache.lastChecked > CHECK_INTERVAL_MS) {
|
|
3316
|
+
fetch("https://registry.npmjs.org/@workscribe/cli/latest").then((response) => response.json()).then((data) => {
|
|
3317
|
+
const latest = data.version;
|
|
3318
|
+
if (latest) writeCache({ lastChecked: now, latestVersion: latest });
|
|
3319
|
+
}).catch(() => {
|
|
3320
|
+
});
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
|
|
2743
3324
|
// src/index.ts
|
|
3325
|
+
assertNodeVersion(process.versions.node);
|
|
3326
|
+
checkForUpdate();
|
|
2744
3327
|
var program = new import_commander.Command();
|
|
2745
|
-
program.name("workscribe").description("Automatic developer work journal").version("1.
|
|
3328
|
+
program.name("workscribe").description("Automatic developer work journal").version("1.12.0");
|
|
2746
3329
|
registerCapture(program);
|
|
2747
3330
|
registerHook(program);
|
|
2748
3331
|
registerInit(program);
|
|
@@ -2757,5 +3340,9 @@ registerUndo(program);
|
|
|
2757
3340
|
registerClear(program);
|
|
2758
3341
|
registerUninstall(program);
|
|
2759
3342
|
registerProfile(program);
|
|
3343
|
+
registerNote(program);
|
|
3344
|
+
registerWeek(program);
|
|
3345
|
+
registerBugReport(program);
|
|
3346
|
+
registerCompletions(program);
|
|
2760
3347
|
program.parse();
|
|
2761
3348
|
//# sourceMappingURL=index.js.map
|