@workscribe/cli 1.9.0 → 1.11.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 +1464 -776
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/shell/hook.bash +1 -1
- 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,8 +26,22 @@ 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_fs9 = __toESM(require("fs"));
|
|
31
45
|
|
|
32
46
|
// src/db/index.ts
|
|
33
47
|
var import_node_sqlite = require("node:sqlite");
|
|
@@ -43,6 +57,7 @@ var DEBUG_LOG_PATH = import_path.default.join(WORKSCRIBE_DIR, "debug.log");
|
|
|
43
57
|
var EXPORTS_DIR = import_path.default.join(WORKSCRIBE_DIR, "exports");
|
|
44
58
|
var PAUSE_FLAG_PATH = import_path.default.join(WORKSCRIBE_DIR, "paused");
|
|
45
59
|
var WEEKLY_CACHE_DIR = import_path.default.join(WORKSCRIBE_DIR, "weekly-cache");
|
|
60
|
+
var PROFILES_DIR = import_path.default.join(WORKSCRIBE_DIR, "profiles");
|
|
46
61
|
|
|
47
62
|
// src/db/migrations/001_initial_schema.ts
|
|
48
63
|
var initial_schema_default = {
|
|
@@ -85,8 +100,27 @@ var initial_schema_default = {
|
|
|
85
100
|
`
|
|
86
101
|
};
|
|
87
102
|
|
|
103
|
+
// src/db/migrations/002_semantic_categories.ts
|
|
104
|
+
var migration = {
|
|
105
|
+
version: 2,
|
|
106
|
+
description: "Rename granular categories to 13 universal semantic categories",
|
|
107
|
+
sql: `
|
|
108
|
+
UPDATE events SET category = 'shipping' WHERE category IN ('code_commit','code_push','code_merge');
|
|
109
|
+
UPDATE events SET category = 'versioning' WHERE category IN ('code_pull','branch_switch','code_stash','git_other');
|
|
110
|
+
UPDATE events SET category = 'building' WHERE category = 'build';
|
|
111
|
+
UPDATE events SET category = 'running' WHERE category IN ('server_run','script_run');
|
|
112
|
+
UPDATE events SET category = 'testing' WHERE category = 'test_run';
|
|
113
|
+
UPDATE events SET category = 'linting' WHERE category = 'lint_run';
|
|
114
|
+
UPDATE events SET category = 'dependencies' WHERE category IN ('dependency_install','dependency_remove');
|
|
115
|
+
UPDATE events SET category = 'infrastructure' WHERE category = 'infrastructure';
|
|
116
|
+
UPDATE events SET category = 'files' WHERE category IN ('file_ops','environment');
|
|
117
|
+
UPDATE events SET category = 'exploring' WHERE category IN ('code_search','code_read');
|
|
118
|
+
`
|
|
119
|
+
};
|
|
120
|
+
var semantic_categories_default = migration;
|
|
121
|
+
|
|
88
122
|
// src/db/migrations/index.ts
|
|
89
|
-
var MIGRATIONS = [initial_schema_default];
|
|
123
|
+
var MIGRATIONS = [initial_schema_default, semantic_categories_default];
|
|
90
124
|
|
|
91
125
|
// src/db/index.ts
|
|
92
126
|
var _db = null;
|
|
@@ -123,13 +157,13 @@ function applyMigrations(db) {
|
|
|
123
157
|
const applied = new Set(
|
|
124
158
|
db.prepare("SELECT version FROM schema_migrations").all().map((row) => row.version)
|
|
125
159
|
);
|
|
126
|
-
for (const
|
|
127
|
-
if (applied.has(
|
|
160
|
+
for (const migration2 of MIGRATIONS) {
|
|
161
|
+
if (applied.has(migration2.version)) continue;
|
|
128
162
|
db.exec("BEGIN");
|
|
129
163
|
try {
|
|
130
|
-
db.exec(
|
|
164
|
+
db.exec(migration2.sql);
|
|
131
165
|
db.prepare("INSERT INTO schema_migrations (version, applied_at) VALUES ($version, datetime('now'))").run({
|
|
132
|
-
version:
|
|
166
|
+
version: migration2.version
|
|
133
167
|
});
|
|
134
168
|
db.exec("COMMIT");
|
|
135
169
|
} catch (err) {
|
|
@@ -137,7 +171,7 @@ function applyMigrations(db) {
|
|
|
137
171
|
db.exec("ROLLBACK");
|
|
138
172
|
} catch {
|
|
139
173
|
}
|
|
140
|
-
throw new Error(`Migration ${
|
|
174
|
+
throw new Error(`Migration ${migration2.version} (${migration2.description}) failed: ${err.message}`);
|
|
141
175
|
}
|
|
142
176
|
}
|
|
143
177
|
}
|
|
@@ -232,6 +266,28 @@ function deleteEventsForDate(date) {
|
|
|
232
266
|
const result = db.prepare(`DELETE FROM events WHERE date(captured_at, 'localtime') = $date`).run({ date });
|
|
233
267
|
return result.changes;
|
|
234
268
|
}
|
|
269
|
+
function getOtherEventToolCounts(days) {
|
|
270
|
+
const db = getDb();
|
|
271
|
+
return db.prepare(
|
|
272
|
+
`SELECT json_extract(metadata, '$.tool') as tool, COUNT(*) as count
|
|
273
|
+
FROM events
|
|
274
|
+
WHERE category = 'other'
|
|
275
|
+
AND captured_at >= datetime('now', $offset)
|
|
276
|
+
AND json_extract(metadata, '$.tool') IS NOT NULL
|
|
277
|
+
GROUP BY tool
|
|
278
|
+
ORDER BY count DESC`
|
|
279
|
+
).all({ offset: `-${days} days` });
|
|
280
|
+
}
|
|
281
|
+
function getRecentOtherRate(days) {
|
|
282
|
+
const db = getDb();
|
|
283
|
+
const row = db.prepare(
|
|
284
|
+
`SELECT COUNT(*) as total,
|
|
285
|
+
SUM(CASE WHEN category = 'other' THEN 1 ELSE 0 END) as otherCount
|
|
286
|
+
FROM events
|
|
287
|
+
WHERE captured_at >= datetime('now', $offset)`
|
|
288
|
+
).get({ offset: `-${days} days` });
|
|
289
|
+
return row;
|
|
290
|
+
}
|
|
235
291
|
function clearSessionsForDate(date) {
|
|
236
292
|
const db = getDb();
|
|
237
293
|
db.prepare(
|
|
@@ -265,7 +321,10 @@ var BUILTIN_PATTERNS = [
|
|
|
265
321
|
// Values after long-form secret flags (single-char flags like -t/-p/-k are too ambiguous)
|
|
266
322
|
/(?<=--(password|passwd|secret|token|key|api[-_]?key|auth[-_]?token)\s+)\S+/gi,
|
|
267
323
|
// export VAR=VALUE where VAR looks sensitive
|
|
268
|
-
/(?<=export\s+(?:[A-Z_]*(?:TOKEN|SECRET|KEY|PASSWORD|PASS|AUTH|CREDENTIAL)[A-Z_]*)=)\S+/gi
|
|
324
|
+
/(?<=export\s+(?:[A-Z_]*(?:TOKEN|SECRET|KEY|PASSWORD|PASS|AUTH|CREDENTIAL)[A-Z_]*)=)\S+/gi,
|
|
325
|
+
// Inline assignment: VAR_WITH_SENSITIVE_KEYWORD=value cmd (e.g. API_KEY=secret npm start)
|
|
326
|
+
// Lookbehind matches only the variable-name= prefix, so only the value is replaced
|
|
327
|
+
/(?<=[A-Z_][A-Z0-9_]*(?:TOKEN|SECRET|KEY|PASSWORD|PASS|AUTH|CREDENTIAL)[A-Z0-9_]*=)\S+/gi
|
|
269
328
|
];
|
|
270
329
|
function redact(command, extraPatterns = []) {
|
|
271
330
|
let result = command;
|
|
@@ -475,6 +534,240 @@ function loadAIToolRegistry() {
|
|
|
475
534
|
return [...filteredBuiltins, ...userEntries];
|
|
476
535
|
}
|
|
477
536
|
|
|
537
|
+
// src/profiles/loader.ts
|
|
538
|
+
var import_fs8 = __toESM(require("fs"));
|
|
539
|
+
var import_path6 = __toESM(require("path"));
|
|
540
|
+
|
|
541
|
+
// src/config/index.ts
|
|
542
|
+
var import_fs6 = __toESM(require("fs"));
|
|
543
|
+
var DEFAULTS = {
|
|
544
|
+
ai: {
|
|
545
|
+
provider: "",
|
|
546
|
+
apiKey: "",
|
|
547
|
+
model: "",
|
|
548
|
+
baseUrl: ""
|
|
549
|
+
},
|
|
550
|
+
session: {
|
|
551
|
+
idleTimeout: 30
|
|
552
|
+
},
|
|
553
|
+
capture: {
|
|
554
|
+
ignore: ["cd", "ls", "ll", "la", "ls -la", "ls -l", "ls -a", "pwd", "clear", "exit", "history", "man"],
|
|
555
|
+
aiTools: true,
|
|
556
|
+
includeAiderPrompt: true,
|
|
557
|
+
profiles: []
|
|
558
|
+
},
|
|
559
|
+
redact: {
|
|
560
|
+
extra: []
|
|
561
|
+
},
|
|
562
|
+
summary: {
|
|
563
|
+
format: "markdown"
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
function readConfig() {
|
|
567
|
+
if (!import_fs6.default.existsSync(CONFIG_PATH)) return { ...DEFAULTS };
|
|
568
|
+
try {
|
|
569
|
+
const raw = import_fs6.default.readFileSync(CONFIG_PATH, "utf-8");
|
|
570
|
+
const parsed = JSON.parse(raw);
|
|
571
|
+
return deepMerge(DEFAULTS, parsed);
|
|
572
|
+
} catch {
|
|
573
|
+
return { ...DEFAULTS };
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
function writeConfig(config) {
|
|
577
|
+
import_fs6.default.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { encoding: "utf-8", mode: 384 });
|
|
578
|
+
try {
|
|
579
|
+
import_fs6.default.chmodSync(CONFIG_PATH, 384);
|
|
580
|
+
} catch {
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
function getConfigValue(config, key) {
|
|
584
|
+
const parts = key.split(".");
|
|
585
|
+
let cur = config;
|
|
586
|
+
for (const part of parts) {
|
|
587
|
+
if (cur == null || typeof cur !== "object") return void 0;
|
|
588
|
+
cur = cur[part];
|
|
589
|
+
}
|
|
590
|
+
return cur;
|
|
591
|
+
}
|
|
592
|
+
function setConfigValue(config, key, value) {
|
|
593
|
+
const parts = key.split(".");
|
|
594
|
+
const result = JSON.parse(JSON.stringify(config));
|
|
595
|
+
let cur = result;
|
|
596
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
597
|
+
const part = parts[i];
|
|
598
|
+
if (cur[part] == null || typeof cur[part] !== "object") {
|
|
599
|
+
cur[part] = {};
|
|
600
|
+
}
|
|
601
|
+
cur = cur[part];
|
|
602
|
+
}
|
|
603
|
+
const lastKey = parts[parts.length - 1];
|
|
604
|
+
if (value === "true") cur[lastKey] = true;
|
|
605
|
+
else if (value === "false") cur[lastKey] = false;
|
|
606
|
+
else if (!isNaN(Number(value))) cur[lastKey] = Number(value);
|
|
607
|
+
else cur[lastKey] = value;
|
|
608
|
+
return result;
|
|
609
|
+
}
|
|
610
|
+
function deepMerge(defaults, overrides) {
|
|
611
|
+
const result = { ...defaults };
|
|
612
|
+
for (const key of Object.keys(overrides)) {
|
|
613
|
+
const val = overrides[key];
|
|
614
|
+
if (val !== null && typeof val === "object" && !Array.isArray(val) && typeof defaults[key] === "object") {
|
|
615
|
+
result[key] = deepMerge(defaults[key], val);
|
|
616
|
+
} else if (val !== void 0) {
|
|
617
|
+
result[key] = val;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return result;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// src/profiles/schema.ts
|
|
624
|
+
function isNonEmptyString(value) {
|
|
625
|
+
return typeof value === "string" && value.length > 0;
|
|
626
|
+
}
|
|
627
|
+
function isValidRule(rule) {
|
|
628
|
+
if (!rule || typeof rule !== "object") return false;
|
|
629
|
+
const { pattern, category } = rule;
|
|
630
|
+
return isNonEmptyString(pattern) && isNonEmptyString(category);
|
|
631
|
+
}
|
|
632
|
+
function isValidProfile(candidate) {
|
|
633
|
+
if (!candidate || typeof candidate !== "object") return false;
|
|
634
|
+
const { name, displayName, version, rules } = candidate;
|
|
635
|
+
if (!isNonEmptyString(name) || !isNonEmptyString(displayName) || !isNonEmptyString(version)) return false;
|
|
636
|
+
if (!Array.isArray(rules)) return false;
|
|
637
|
+
return rules.every(isValidRule);
|
|
638
|
+
}
|
|
639
|
+
function isValidManifest(m) {
|
|
640
|
+
return Array.isArray(m) && m.every((e) => e && typeof e === "object" && typeof e.name === "string");
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// src/profiles/fetcher.ts
|
|
644
|
+
var import_fs7 = __toESM(require("fs"));
|
|
645
|
+
var import_path5 = __toESM(require("path"));
|
|
646
|
+
var REGISTRY_BASE = "https://raw.githubusercontent.com/workscribe/profiles/main";
|
|
647
|
+
function manifestCachePath() {
|
|
648
|
+
return import_path5.default.join(PROFILES_DIR, "manifest.json");
|
|
649
|
+
}
|
|
650
|
+
async function fetchManifest() {
|
|
651
|
+
const response = await fetch(`${REGISTRY_BASE}/manifest.json`);
|
|
652
|
+
if (!response.ok) throw new Error(`Registry unreachable (${response.status})`);
|
|
653
|
+
const data = await response.json();
|
|
654
|
+
if (!isValidManifest(data)) throw new Error("Manifest format unrecognised");
|
|
655
|
+
return data;
|
|
656
|
+
}
|
|
657
|
+
async function fetchProfile(name) {
|
|
658
|
+
const response = await fetch(`${REGISTRY_BASE}/${name}.json`);
|
|
659
|
+
if (response.status === 404) throw new Error(`Profile "${name}" not found in registry`);
|
|
660
|
+
if (!response.ok) throw new Error(`Registry unreachable (${response.status})`);
|
|
661
|
+
const data = await response.json();
|
|
662
|
+
if (!isValidProfile(data)) throw new Error(`Profile "${name}" failed schema validation`);
|
|
663
|
+
return data;
|
|
664
|
+
}
|
|
665
|
+
async function installProfile(name) {
|
|
666
|
+
ensureProfilesDir();
|
|
667
|
+
const profile = await fetchProfile(name);
|
|
668
|
+
const dest = import_path5.default.join(PROFILES_DIR, `${name}.json`);
|
|
669
|
+
import_fs7.default.writeFileSync(dest, JSON.stringify(profile, null, 2), { encoding: "utf-8", mode: 384 });
|
|
670
|
+
debugLog("profiles", `Installed profile "${name}" to ${dest}`);
|
|
671
|
+
}
|
|
672
|
+
async function cacheManifest() {
|
|
673
|
+
ensureProfilesDir();
|
|
674
|
+
const manifest = await fetchManifest();
|
|
675
|
+
import_fs7.default.writeFileSync(manifestCachePath(), JSON.stringify(manifest, null, 2), { encoding: "utf-8", mode: 384 });
|
|
676
|
+
return manifest;
|
|
677
|
+
}
|
|
678
|
+
function readCachedManifest() {
|
|
679
|
+
if (!import_fs7.default.existsSync(manifestCachePath())) return null;
|
|
680
|
+
try {
|
|
681
|
+
const data = JSON.parse(import_fs7.default.readFileSync(manifestCachePath(), "utf-8"));
|
|
682
|
+
return isValidManifest(data) ? data : null;
|
|
683
|
+
} catch {
|
|
684
|
+
return null;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
function ensureProfilesDir() {
|
|
688
|
+
if (!import_fs7.default.existsSync(PROFILES_DIR)) {
|
|
689
|
+
import_fs7.default.mkdirSync(PROFILES_DIR, { recursive: true, mode: 448 });
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// src/profiles/loader.ts
|
|
694
|
+
function compileRule(profileName, rule) {
|
|
695
|
+
let regex;
|
|
696
|
+
try {
|
|
697
|
+
regex = new RegExp(rule.pattern);
|
|
698
|
+
} catch {
|
|
699
|
+
debugLog("profiles", `Profile "${profileName}": invalid pattern "${rule.pattern}" \u2014 skipping rule`);
|
|
700
|
+
return null;
|
|
701
|
+
}
|
|
702
|
+
return {
|
|
703
|
+
match(cmd) {
|
|
704
|
+
const match = cmd.match(regex);
|
|
705
|
+
if (!match) return null;
|
|
706
|
+
const metadata = {};
|
|
707
|
+
for (const [key, value] of Object.entries(rule.metadata ?? {})) {
|
|
708
|
+
metadata[key] = value.replace(/\$(\d+)/g, (_, index) => match[Number(index)] ?? "");
|
|
709
|
+
}
|
|
710
|
+
return { category: rule.category, metadata };
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
function loadProfileFromDisk(name) {
|
|
715
|
+
const filePath = import_path6.default.join(PROFILES_DIR, `${name}.json`);
|
|
716
|
+
if (!import_fs8.default.existsSync(filePath)) {
|
|
717
|
+
debugLog("profiles", `Profile "${name}" not found in cache \u2014 run: workscribe profile add ${name}`);
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
let candidate;
|
|
721
|
+
try {
|
|
722
|
+
candidate = JSON.parse(import_fs8.default.readFileSync(filePath, "utf-8"));
|
|
723
|
+
} catch {
|
|
724
|
+
debugLog("profiles", `Profile "${name}" contains invalid JSON \u2014 skipping`);
|
|
725
|
+
return null;
|
|
726
|
+
}
|
|
727
|
+
if (!isValidProfile(candidate)) {
|
|
728
|
+
debugLog("profiles", `Profile "${name}" failed schema validation \u2014 skipping`);
|
|
729
|
+
return null;
|
|
730
|
+
}
|
|
731
|
+
return candidate;
|
|
732
|
+
}
|
|
733
|
+
function getActiveProfileRules() {
|
|
734
|
+
const activeNames = readConfig().capture.profiles;
|
|
735
|
+
if (activeNames.length === 0) return [];
|
|
736
|
+
return activeNames.flatMap((name) => {
|
|
737
|
+
const profile = loadProfileFromDisk(name);
|
|
738
|
+
if (!profile) return [];
|
|
739
|
+
return profile.rules.flatMap((rule) => {
|
|
740
|
+
const compiled = compileRule(profile.name, rule);
|
|
741
|
+
return compiled ? [compiled] : [];
|
|
742
|
+
});
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
function getActiveProfileAiContexts() {
|
|
746
|
+
const activeNames = readConfig().capture.profiles;
|
|
747
|
+
if (activeNames.length === 0) return [];
|
|
748
|
+
return activeNames.flatMap((name) => {
|
|
749
|
+
const profile = loadProfileFromDisk(name);
|
|
750
|
+
return profile?.aiContext ? [profile.aiContext] : [];
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
function getCachedProfiles() {
|
|
754
|
+
if (!import_fs8.default.existsSync(PROFILES_DIR)) return [];
|
|
755
|
+
return import_fs8.default.readdirSync(PROFILES_DIR).filter((file) => file.endsWith(".json") && file !== "manifest.json").flatMap((file) => {
|
|
756
|
+
const profile = loadProfileFromDisk(file.replace(/\.json$/, ""));
|
|
757
|
+
return profile ? [profile] : [];
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
function detectProfilesForDirectory(dir) {
|
|
761
|
+
const manifest = readCachedManifest();
|
|
762
|
+
if (!manifest) return [];
|
|
763
|
+
const activeNames = new Set(readConfig().capture.profiles);
|
|
764
|
+
return manifest.filter((entry) => {
|
|
765
|
+
if (activeNames.has(entry.name)) return false;
|
|
766
|
+
const indicatorFiles = entry.detect?.files ?? [];
|
|
767
|
+
return indicatorFiles.length > 0 && indicatorFiles.every((file) => import_fs8.default.existsSync(import_path6.default.join(dir, file)));
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
|
|
478
771
|
// src/normalize/patterns.ts
|
|
479
772
|
function makeRule(pattern, category, extract = () => ({})) {
|
|
480
773
|
return {
|
|
@@ -490,114 +783,131 @@ function extractPackages(cmd, afterKeyword) {
|
|
|
490
783
|
if (idx === -1) return [];
|
|
491
784
|
return cmd.slice(idx + afterKeyword.length).trim().split(/\s+/).filter((part) => part && !part.startsWith("-"));
|
|
492
785
|
}
|
|
493
|
-
var
|
|
786
|
+
var BASE_RULES = [
|
|
494
787
|
// ─── Git operations ───────────────────────────────────────────────────────
|
|
495
|
-
makeRule(/^git\s+commit\b/, "
|
|
788
|
+
makeRule(/^git\s+commit\b/, "shipping", (_m, cmd) => {
|
|
496
789
|
const msgMatch = cmd.match(/-m\s+["']?([^"']+)["']?/);
|
|
497
790
|
return { message: msgMatch ? msgMatch[1].trim() : null };
|
|
498
791
|
}),
|
|
499
|
-
makeRule(/^git\s+push\b/, "
|
|
792
|
+
makeRule(/^git\s+push\b/, "shipping", (_m, cmd) => {
|
|
500
793
|
const parts = cmd.split(/\s+/);
|
|
501
794
|
return { remote: parts[2] ?? "origin", branch: parts[3] ?? null };
|
|
502
795
|
}),
|
|
503
|
-
makeRule(/^git\s+(pull|fetch)\b/, "
|
|
796
|
+
makeRule(/^git\s+(pull|fetch)\b/, "versioning", (_m, cmd) => {
|
|
504
797
|
const parts = cmd.split(/\s+/);
|
|
505
798
|
return { subcommand: parts[1], remote: parts[2] ?? "origin" };
|
|
506
799
|
}),
|
|
507
|
-
makeRule(/^git\s+(checkout|switch)\b/, "
|
|
800
|
+
makeRule(/^git\s+(checkout|switch)\b/, "versioning", (_m, cmd) => {
|
|
508
801
|
const parts = cmd.split(/\s+/);
|
|
509
802
|
const branch = parts.find((part) => !part.startsWith("-") && part !== "git" && part !== parts[1]);
|
|
510
803
|
return { branch: branch ?? null };
|
|
511
804
|
}),
|
|
512
|
-
makeRule(/^git\s+(merge|rebase)\b/, "
|
|
805
|
+
makeRule(/^git\s+(merge|rebase)\b/, "shipping", (_m, cmd) => {
|
|
513
806
|
const parts = cmd.split(/\s+/);
|
|
514
807
|
return { subcommand: parts[1], target: parts[2] ?? null };
|
|
515
808
|
}),
|
|
516
|
-
makeRule(/^git\s+stash\b/, "
|
|
809
|
+
makeRule(/^git\s+stash\b/, "versioning", (_m, cmd) => {
|
|
517
810
|
const parts = cmd.split(/\s+/);
|
|
518
811
|
return { subcommand: parts[2] ?? "push" };
|
|
519
812
|
}),
|
|
520
|
-
makeRule(/^git\s+/, "
|
|
813
|
+
makeRule(/^git\s+/, "versioning", (_m, cmd) => {
|
|
521
814
|
const parts = cmd.split(/\s+/);
|
|
522
815
|
return { subcommand: parts[1] ?? null };
|
|
523
816
|
}),
|
|
524
|
-
// ───
|
|
525
|
-
makeRule(/^npm\s+
|
|
817
|
+
// ─── Releasing ────────────────────────────────────────────────────────────
|
|
818
|
+
makeRule(/^npm\s+publish\b/, "releasing", () => ({ tool: "npm" })),
|
|
819
|
+
makeRule(/^cargo\s+publish\b/, "releasing", () => ({ tool: "cargo" })),
|
|
820
|
+
makeRule(/^gem\s+push\b/, "releasing", () => ({ tool: "gem" })),
|
|
821
|
+
makeRule(/^docker\s+push\b/, "releasing", (_m, cmd) => ({ tool: "docker", image: cmd.split(/\s+/)[2] ?? null })),
|
|
822
|
+
makeRule(/^gh\s+release\b/, "releasing", (_m, cmd) => ({ tool: "gh", subcommand: cmd.split(/\s+/)[2] ?? null })),
|
|
823
|
+
makeRule(/^fly\s+(deploy|launch)\b/, "releasing", (match) => ({ tool: "fly", subcommand: match[1] })),
|
|
824
|
+
makeRule(/^vercel\s+(deploy|build|--prod)\b/, "releasing", () => ({ tool: "vercel" })),
|
|
825
|
+
makeRule(/^netlify\s+deploy\b/, "releasing", () => ({ tool: "netlify" })),
|
|
826
|
+
makeRule(/^railway\s+up\b/, "releasing", () => ({ tool: "railway" })),
|
|
827
|
+
makeRule(/^wrangler\s+deploy\b/, "releasing", () => ({ tool: "wrangler" })),
|
|
828
|
+
// ─── Dependencies ─────────────────────────────────────────────────────────
|
|
829
|
+
makeRule(/^npm\s+(install|i|add)\b/, "dependencies", (_m, cmd) => ({
|
|
526
830
|
tool: "npm",
|
|
527
831
|
packages: extractPackages(cmd, cmd.includes("install") ? "install" : cmd.includes(" i ") ? " i " : "add")
|
|
528
832
|
})),
|
|
529
|
-
makeRule(/^(yarn|pnpm)\s+(install|add)\b/, "
|
|
833
|
+
makeRule(/^(yarn|pnpm)\s+(install|add)\b/, "dependencies", (m, cmd) => ({
|
|
530
834
|
tool: m[1],
|
|
531
835
|
packages: extractPackages(cmd, "add")
|
|
532
836
|
})),
|
|
533
|
-
makeRule(/^bun\s+(install|add)\b/, "
|
|
837
|
+
makeRule(/^bun\s+(install|add)\b/, "dependencies", (_m, cmd) => ({
|
|
534
838
|
tool: "bun",
|
|
535
839
|
packages: extractPackages(cmd, "add")
|
|
536
840
|
})),
|
|
537
|
-
makeRule(/^pip\s+(install)\b/, "
|
|
841
|
+
makeRule(/^pip\s+(install)\b/, "dependencies", (_m, cmd) => ({
|
|
538
842
|
tool: "pip",
|
|
539
843
|
packages: extractPackages(cmd, "install")
|
|
540
844
|
})),
|
|
541
|
-
makeRule(/^cargo\s+add\b/, "
|
|
845
|
+
makeRule(/^cargo\s+add\b/, "dependencies", (_m, cmd) => ({
|
|
542
846
|
tool: "cargo",
|
|
543
847
|
packages: extractPackages(cmd, "add")
|
|
544
848
|
})),
|
|
545
|
-
makeRule(/^(brew)\s+install\b/, "
|
|
849
|
+
makeRule(/^(brew)\s+install\b/, "dependencies", (_m, cmd) => ({
|
|
546
850
|
tool: "brew",
|
|
547
851
|
packages: extractPackages(cmd, "install")
|
|
548
852
|
})),
|
|
549
|
-
makeRule(/^go\s+get\b/, "
|
|
853
|
+
makeRule(/^go\s+get\b/, "dependencies", (_m, cmd) => ({
|
|
550
854
|
tool: "go",
|
|
551
855
|
packages: extractPackages(cmd, "get")
|
|
552
856
|
})),
|
|
553
|
-
|
|
554
|
-
makeRule(/^npm\s+(uninstall|remove|rm)\b/, "dependency_remove", (_m, cmd) => ({
|
|
857
|
+
makeRule(/^npm\s+(uninstall|remove|rm)\b/, "dependencies", (_m, cmd) => ({
|
|
555
858
|
tool: "npm",
|
|
556
859
|
packages: extractPackages(cmd, cmd.split(/\s+/)[1])
|
|
557
860
|
})),
|
|
558
|
-
makeRule(/^(yarn|pnpm)\s+remove\b/, "
|
|
861
|
+
makeRule(/^(yarn|pnpm)\s+remove\b/, "dependencies", (m, cmd) => ({
|
|
559
862
|
tool: m[1],
|
|
560
863
|
packages: extractPackages(cmd, "remove")
|
|
561
864
|
})),
|
|
562
|
-
makeRule(/^bun\s+remove\b/, "
|
|
865
|
+
makeRule(/^bun\s+remove\b/, "dependencies", (_m, cmd) => ({
|
|
563
866
|
tool: "bun",
|
|
564
867
|
packages: extractPackages(cmd, "remove")
|
|
565
868
|
})),
|
|
566
|
-
makeRule(/^pip\s+uninstall\b/, "
|
|
869
|
+
makeRule(/^pip\s+uninstall\b/, "dependencies", (_m, cmd) => ({
|
|
567
870
|
tool: "pip",
|
|
568
871
|
packages: extractPackages(cmd, "uninstall")
|
|
569
872
|
})),
|
|
873
|
+
// Runtime version managers
|
|
874
|
+
makeRule(/^nvm\b/, "dependencies", (_m, cmd) => ({ tool: "nvm", subcommand: cmd.split(/\s+/)[1] ?? null })),
|
|
875
|
+
makeRule(/^(fnm|volta)\b/, "dependencies", (m, cmd) => ({ tool: m[1], subcommand: cmd.split(/\s+/)[1] ?? null })),
|
|
876
|
+
makeRule(/^(rbenv|pyenv|asdf)\b/, "dependencies", (m, cmd) => ({
|
|
877
|
+
tool: m[1],
|
|
878
|
+
subcommand: cmd.split(/\s+/)[1] ?? null
|
|
879
|
+
})),
|
|
570
880
|
// ─── Tests ────────────────────────────────────────────────────────────────
|
|
571
|
-
makeRule(/^npm\s+(test|run\s+test)\b/, "
|
|
572
|
-
makeRule(/^yarn\s+test\b/, "
|
|
573
|
-
makeRule(/^pnpm\s+test\b/, "
|
|
574
|
-
makeRule(/^bun\s+test\b/, "
|
|
575
|
-
makeRule(/^(jest|vitest)\b/, "
|
|
576
|
-
makeRule(/^pytest\b/, "
|
|
577
|
-
makeRule(/^go\s+test\b/, "
|
|
578
|
-
makeRule(/^cargo\s+test\b/, "
|
|
579
|
-
makeRule(/^rspec\b/, "
|
|
580
|
-
makeRule(/^(php\s+)?phpunit\b/, "
|
|
881
|
+
makeRule(/^npm\s+(test|run\s+test)\b/, "testing", () => ({ tool: "npm" })),
|
|
882
|
+
makeRule(/^yarn\s+test\b/, "testing", () => ({ tool: "yarn" })),
|
|
883
|
+
makeRule(/^pnpm\s+test\b/, "testing", () => ({ tool: "pnpm" })),
|
|
884
|
+
makeRule(/^bun\s+test\b/, "testing", () => ({ tool: "bun" })),
|
|
885
|
+
makeRule(/^(jest|vitest)\b/, "testing", (match) => ({ tool: match[1] })),
|
|
886
|
+
makeRule(/^pytest\b/, "testing", () => ({ tool: "pytest" })),
|
|
887
|
+
makeRule(/^go\s+test\b/, "testing", () => ({ tool: "go" })),
|
|
888
|
+
makeRule(/^cargo\s+test\b/, "testing", () => ({ tool: "cargo" })),
|
|
889
|
+
makeRule(/^rspec\b/, "testing", () => ({ tool: "rspec" })),
|
|
890
|
+
makeRule(/^(php\s+)?phpunit\b/, "testing", () => ({ tool: "phpunit" })),
|
|
581
891
|
// ─── Build ────────────────────────────────────────────────────────────────
|
|
582
|
-
makeRule(/^npm\s+run\s+build\b/, "
|
|
583
|
-
makeRule(/^(yarn|pnpm)\s+build\b/, "
|
|
584
|
-
makeRule(/^bun\s+run\s+build\b/, "
|
|
585
|
-
makeRule(/^(make|gmake)\b/, "
|
|
586
|
-
makeRule(/^cargo\s+build\b/, "
|
|
587
|
-
makeRule(/^go\s+build\b/, "
|
|
588
|
-
makeRule(/^docker\s+build\b/, "
|
|
589
|
-
makeRule(/^(mvn|gradle)\b/, "
|
|
590
|
-
// ─── Dev server
|
|
591
|
-
makeRule(/^npm\s+run\s+(dev|start|serve)\b/, "
|
|
892
|
+
makeRule(/^npm\s+run\s+build\b/, "building", () => ({ tool: "npm" })),
|
|
893
|
+
makeRule(/^(yarn|pnpm)\s+build\b/, "building", (match) => ({ tool: match[1] })),
|
|
894
|
+
makeRule(/^bun\s+run\s+build\b/, "building", () => ({ tool: "bun" })),
|
|
895
|
+
makeRule(/^(make|gmake)\b/, "building", (match) => ({ tool: match[1] })),
|
|
896
|
+
makeRule(/^cargo\s+build\b/, "building", () => ({ tool: "cargo" })),
|
|
897
|
+
makeRule(/^go\s+build\b/, "building", () => ({ tool: "go" })),
|
|
898
|
+
makeRule(/^docker\s+build\b/, "building", () => ({ tool: "docker" })),
|
|
899
|
+
makeRule(/^(mvn|gradle)\b/, "building", (match) => ({ tool: match[1] })),
|
|
900
|
+
// ─── Dev server / running ─────────────────────────────────────────────────
|
|
901
|
+
makeRule(/^npm\s+run\s+(dev|start|serve)\b/, "running", (_m, cmd) => ({
|
|
592
902
|
tool: "npm",
|
|
593
903
|
script: cmd.split(/\s+/)[2]
|
|
594
904
|
})),
|
|
595
|
-
makeRule(/^(yarn|pnpm)\s+(dev|start)\b/, "
|
|
596
|
-
makeRule(/^bun\s+run\s+(dev|start|serve)\b/, "
|
|
905
|
+
makeRule(/^(yarn|pnpm)\s+(dev|start)\b/, "running", (match) => ({ tool: match[1], script: match[2] })),
|
|
906
|
+
makeRule(/^bun\s+run\s+(dev|start|serve)\b/, "running", (_m, cmd) => ({
|
|
597
907
|
tool: "bun",
|
|
598
908
|
script: cmd.split(/\s+/)[2]
|
|
599
909
|
})),
|
|
600
|
-
makeRule(/^python\s+.*manage\.py\s+runserver\b/, "
|
|
910
|
+
makeRule(/^python\s+.*manage\.py\s+runserver\b/, "running", () => ({ tool: "django" })),
|
|
601
911
|
// ─── Infrastructure ───────────────────────────────────────────────────────
|
|
602
912
|
makeRule(/^ssh\b/, "infrastructure", (_m, cmd) => {
|
|
603
913
|
const parts = cmd.split(/\s+/);
|
|
@@ -611,7 +921,7 @@ var RULES = [
|
|
|
611
921
|
const parts = cmd.split(/\s+/);
|
|
612
922
|
return { tool: m[1], subcommand: parts[1] ?? null };
|
|
613
923
|
}),
|
|
614
|
-
makeRule(/^docker\s+(run|compose|ps|stop|start|rm|exec)\b/, "infrastructure", (
|
|
924
|
+
makeRule(/^docker\s+(run|compose|ps|stop|start|rm|exec)\b/, "infrastructure", (_m, cmd) => {
|
|
615
925
|
const parts = cmd.split(/\s+/);
|
|
616
926
|
return { tool: "docker", subcommand: parts[1] };
|
|
617
927
|
}),
|
|
@@ -619,62 +929,76 @@ var RULES = [
|
|
|
619
929
|
const parts = cmd.split(/\s+/);
|
|
620
930
|
return { tool: m[1], subcommand: parts[1] ?? null };
|
|
621
931
|
}),
|
|
932
|
+
// ─── Database ─────────────────────────────────────────────────────────────
|
|
933
|
+
makeRule(/^(psql|pgcli)\b/, "database", (m) => ({ tool: m[1] })),
|
|
934
|
+
makeRule(/^(mysql|mycli)\b/, "database", (m) => ({ tool: m[1] })),
|
|
935
|
+
makeRule(/^sqlite3\b/, "database", () => ({ tool: "sqlite3" })),
|
|
936
|
+
makeRule(/^(mongosh|mongo)\b/, "database", (m) => ({ tool: m[1] })),
|
|
937
|
+
makeRule(/^redis-cli\b/, "database", () => ({ tool: "redis-cli" })),
|
|
622
938
|
// ─── npx ─────────────────────────────────────────────────────────────────
|
|
623
|
-
makeRule(/^npx\s+(jest|vitest|mocha|ava|tap)\b/, "
|
|
939
|
+
makeRule(/^npx\s+(jest|vitest|mocha|ava|tap)\b/, "testing", (_m, cmd) => ({
|
|
624
940
|
tool: cmd.split(/\s+/)[1]
|
|
625
941
|
})),
|
|
626
|
-
makeRule(/^npx\s+tsc\b/, "
|
|
627
|
-
makeRule(/^npx\s+(eslint|tslint|biome)\b/, "
|
|
942
|
+
makeRule(/^npx\s+tsc\b/, "building", () => ({ tool: "tsc" })),
|
|
943
|
+
makeRule(/^npx\s+(eslint|tslint|biome)\b/, "linting", (_m, cmd) => ({
|
|
628
944
|
tool: cmd.split(/\s+/)[1]
|
|
629
945
|
})),
|
|
630
|
-
makeRule(/^npx\s+(prettier|dprint)\b/, "
|
|
946
|
+
makeRule(/^npx\s+(prettier|dprint)\b/, "linting", (_m, cmd) => ({
|
|
631
947
|
tool: cmd.split(/\s+/)[1]
|
|
632
948
|
})),
|
|
633
|
-
makeRule(/^npx\s+\S+/, "
|
|
949
|
+
makeRule(/^npx\s+\S+/, "running", (_m, cmd) => ({
|
|
634
950
|
tool: cmd.split(/\s+/)[1]
|
|
635
951
|
})),
|
|
636
952
|
// ─── Scripts ──────────────────────────────────────────────────────────────
|
|
637
|
-
makeRule(/^(bash|sh|zsh|fish)\s+\S+/, "
|
|
638
|
-
makeRule(/^node\s+\S+\.(?:js|mjs|cjs)\b/, "
|
|
639
|
-
makeRule(/^python3?\s+\S+\.py\b/, "
|
|
640
|
-
// ───
|
|
641
|
-
makeRule(/^
|
|
642
|
-
|
|
643
|
-
return { variable: varName };
|
|
644
|
-
}),
|
|
645
|
-
makeRule(/^source\s+/, "environment", () => ({})),
|
|
953
|
+
makeRule(/^(bash|sh|zsh|fish)\s+\S+/, "running", (match) => ({ shell: match[1] })),
|
|
954
|
+
makeRule(/^node\s+\S+\.(?:js|mjs|cjs)\b/, "running", () => ({ runtime: "node" })),
|
|
955
|
+
makeRule(/^python3?\s+\S+\.py\b/, "running", () => ({ runtime: "python" })),
|
|
956
|
+
// ─── Linting / code quality ───────────────────────────────────────────────
|
|
957
|
+
makeRule(/^(eslint|tslint|biome)\b/, "linting", (match) => ({ tool: match[1] })),
|
|
958
|
+
makeRule(/^(prettier|dprint)\b/, "linting", (match) => ({ tool: match[1] })),
|
|
646
959
|
// ─── generic run scripts ──────────────────────────────────────────────────
|
|
647
|
-
makeRule(/^npm\s+run\s+\S+/, "
|
|
960
|
+
makeRule(/^npm\s+run\s+\S+/, "running", (_m, cmd) => ({
|
|
648
961
|
tool: "npm",
|
|
649
962
|
script: cmd.split(/\s+/)[2]
|
|
650
963
|
})),
|
|
651
|
-
makeRule(/^bun\s+run\s+\S+/, "
|
|
964
|
+
makeRule(/^bun\s+run\s+\S+/, "running", (_m, cmd) => ({
|
|
652
965
|
tool: "bun",
|
|
653
966
|
script: cmd.split(/\s+/)[2]
|
|
654
967
|
})),
|
|
655
968
|
// ─── Code search ─────────────────────────────────────────────────────────
|
|
656
|
-
makeRule(/^(grep|egrep|fgrep)\b/, "
|
|
657
|
-
makeRule(/^(rg|ag|ack|ripgrep)\b/, "
|
|
658
|
-
makeRule(/^find\s/, "
|
|
969
|
+
makeRule(/^(grep|egrep|fgrep)\b/, "exploring", (match) => ({ tool: match[1] })),
|
|
970
|
+
makeRule(/^(rg|ag|ack|ripgrep)\b/, "exploring", (match) => ({ tool: match[1] })),
|
|
971
|
+
makeRule(/^find\s/, "exploring", () => ({ tool: "find" })),
|
|
659
972
|
// ─── Code read ────────────────────────────────────────────────────────────
|
|
660
|
-
makeRule(/^(cat|bat|less|more)\s+\S+/, "
|
|
661
|
-
makeRule(/^(head|tail)\b/, "
|
|
973
|
+
makeRule(/^(cat|bat|less|more)\s+\S+/, "exploring", (match) => ({ tool: match[1] })),
|
|
974
|
+
makeRule(/^(head|tail)\b/, "exploring", (match) => ({ tool: match[1] })),
|
|
662
975
|
// ─── File operations ──────────────────────────────────────────────────────
|
|
663
|
-
makeRule(/^(cp|rsync)\b/, "
|
|
664
|
-
makeRule(/^(mv)\b/, "
|
|
665
|
-
makeRule(/^(rm|rmdir)\b/, "
|
|
666
|
-
makeRule(/^(mkdir|touch|ln)\b/, "
|
|
667
|
-
makeRule(/^(chmod|chown|chgrp)\b/, "
|
|
668
|
-
// ───
|
|
669
|
-
|
|
976
|
+
makeRule(/^(cp|rsync)\b/, "files", (match) => ({ tool: match[1] })),
|
|
977
|
+
makeRule(/^(mv)\b/, "files", () => ({ tool: "mv" })),
|
|
978
|
+
makeRule(/^(rm|rmdir)\b/, "files", (match) => ({ tool: match[1] })),
|
|
979
|
+
makeRule(/^(mkdir|touch|ln)\b/, "files", (match) => ({ tool: match[1] })),
|
|
980
|
+
makeRule(/^(chmod|chown|chgrp)\b/, "files", (match) => ({ tool: match[1] })),
|
|
981
|
+
// ─── Environment ──────────────────────────────────────────────────────────
|
|
982
|
+
makeRule(/^export\s+\w+=/, "files", (_m, cmd) => {
|
|
983
|
+
const varName = cmd.match(/^export\s+(\w+)=/)?.[1] ?? null;
|
|
984
|
+
return { variable: varName };
|
|
985
|
+
}),
|
|
986
|
+
makeRule(/^source\s+/, "files", () => ({}))
|
|
987
|
+
];
|
|
988
|
+
function aiToolRules() {
|
|
989
|
+
return loadAIToolRegistry().flatMap((entry) => {
|
|
670
990
|
try {
|
|
671
991
|
return [makeRule(new RegExp(entry.pattern), "ai_session", () => ({ tool: entry.tool }))];
|
|
672
992
|
} catch {
|
|
673
993
|
debugLog("registry", `Skipping tool "${entry.tool}" \u2014 invalid pattern: ${entry.pattern}`);
|
|
674
994
|
return [];
|
|
675
995
|
}
|
|
676
|
-
})
|
|
677
|
-
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
function buildRules(profileRules = []) {
|
|
999
|
+
return [...BASE_RULES, ...profileRules, ...aiToolRules()];
|
|
1000
|
+
}
|
|
1001
|
+
var RULES = buildRules(getActiveProfileRules());
|
|
678
1002
|
function normalizeCommand(command) {
|
|
679
1003
|
let trimmed = command.trim().replace(/\s+/g, " ");
|
|
680
1004
|
trimmed = trimmed.replace(/^(git)\s+-C\s+\S+\s+/, "$1 ");
|
|
@@ -685,87 +1009,6 @@ function normalizeCommand(command) {
|
|
|
685
1009
|
return { category: "other", metadata: {} };
|
|
686
1010
|
}
|
|
687
1011
|
|
|
688
|
-
// src/config/index.ts
|
|
689
|
-
var import_fs6 = __toESM(require("fs"));
|
|
690
|
-
var DEFAULTS = {
|
|
691
|
-
ai: {
|
|
692
|
-
provider: "",
|
|
693
|
-
apiKey: "",
|
|
694
|
-
model: "",
|
|
695
|
-
baseUrl: ""
|
|
696
|
-
},
|
|
697
|
-
session: {
|
|
698
|
-
idleTimeout: 30
|
|
699
|
-
},
|
|
700
|
-
capture: {
|
|
701
|
-
ignore: ["cd", "ls", "ll", "la", "ls -la", "ls -l", "ls -a", "pwd", "clear", "exit", "history", "man"],
|
|
702
|
-
aiTools: true,
|
|
703
|
-
includeAiderPrompt: true
|
|
704
|
-
},
|
|
705
|
-
redact: {
|
|
706
|
-
extra: []
|
|
707
|
-
},
|
|
708
|
-
summary: {
|
|
709
|
-
format: "markdown"
|
|
710
|
-
}
|
|
711
|
-
};
|
|
712
|
-
function readConfig() {
|
|
713
|
-
if (!import_fs6.default.existsSync(CONFIG_PATH)) return { ...DEFAULTS };
|
|
714
|
-
try {
|
|
715
|
-
const raw = import_fs6.default.readFileSync(CONFIG_PATH, "utf-8");
|
|
716
|
-
const parsed = JSON.parse(raw);
|
|
717
|
-
return deepMerge(DEFAULTS, parsed);
|
|
718
|
-
} catch {
|
|
719
|
-
return { ...DEFAULTS };
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
function writeConfig(config) {
|
|
723
|
-
import_fs6.default.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { encoding: "utf-8", mode: 384 });
|
|
724
|
-
try {
|
|
725
|
-
import_fs6.default.chmodSync(CONFIG_PATH, 384);
|
|
726
|
-
} catch {
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
function getConfigValue(config, key) {
|
|
730
|
-
const parts = key.split(".");
|
|
731
|
-
let cur = config;
|
|
732
|
-
for (const part of parts) {
|
|
733
|
-
if (cur == null || typeof cur !== "object") return void 0;
|
|
734
|
-
cur = cur[part];
|
|
735
|
-
}
|
|
736
|
-
return cur;
|
|
737
|
-
}
|
|
738
|
-
function setConfigValue(config, key, value) {
|
|
739
|
-
const parts = key.split(".");
|
|
740
|
-
const result = JSON.parse(JSON.stringify(config));
|
|
741
|
-
let cur = result;
|
|
742
|
-
for (let i = 0; i < parts.length - 1; i++) {
|
|
743
|
-
const part = parts[i];
|
|
744
|
-
if (cur[part] == null || typeof cur[part] !== "object") {
|
|
745
|
-
cur[part] = {};
|
|
746
|
-
}
|
|
747
|
-
cur = cur[part];
|
|
748
|
-
}
|
|
749
|
-
const lastKey = parts[parts.length - 1];
|
|
750
|
-
if (value === "true") cur[lastKey] = true;
|
|
751
|
-
else if (value === "false") cur[lastKey] = false;
|
|
752
|
-
else if (!isNaN(Number(value))) cur[lastKey] = Number(value);
|
|
753
|
-
else cur[lastKey] = value;
|
|
754
|
-
return result;
|
|
755
|
-
}
|
|
756
|
-
function deepMerge(defaults, overrides) {
|
|
757
|
-
const result = { ...defaults };
|
|
758
|
-
for (const key of Object.keys(overrides)) {
|
|
759
|
-
const val = overrides[key];
|
|
760
|
-
if (val !== null && typeof val === "object" && !Array.isArray(val) && typeof defaults[key] === "object") {
|
|
761
|
-
result[key] = deepMerge(defaults[key], val);
|
|
762
|
-
} else if (val !== void 0) {
|
|
763
|
-
result[key] = val;
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
return result;
|
|
767
|
-
}
|
|
768
|
-
|
|
769
1012
|
// src/capture/filter.ts
|
|
770
1013
|
function normalizeForFilter(cmd) {
|
|
771
1014
|
return cmd.trim().replace(/\s+/g, " ");
|
|
@@ -789,7 +1032,7 @@ function isDuplicate(command, cwd, lastEvent, dedupWindowMs = 5e3, now = Date.no
|
|
|
789
1032
|
|
|
790
1033
|
// src/capture/index.ts
|
|
791
1034
|
function captureCommand(opts) {
|
|
792
|
-
if (
|
|
1035
|
+
if (import_fs9.default.existsSync(PAUSE_FLAG_PATH)) return;
|
|
793
1036
|
const config = readConfig();
|
|
794
1037
|
const { command, cwd, exitCode, durationMs } = opts;
|
|
795
1038
|
if (shouldFilter(command, config.capture.ignore)) return;
|
|
@@ -833,8 +1076,11 @@ function captureCommand(opts) {
|
|
|
833
1076
|
}
|
|
834
1077
|
|
|
835
1078
|
// src/cli/capture.ts
|
|
1079
|
+
var CAPTURE_HANG_TIMEOUT_MS = 5e3;
|
|
836
1080
|
function registerCapture(program2) {
|
|
837
1081
|
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) => {
|
|
1082
|
+
const hangGuard = setTimeout(() => process.exit(0), CAPTURE_HANG_TIMEOUT_MS);
|
|
1083
|
+
hangGuard.unref();
|
|
838
1084
|
try {
|
|
839
1085
|
if (!opts.cmd || !opts.cwd) return;
|
|
840
1086
|
captureCommand({
|
|
@@ -887,25 +1133,423 @@ function registerHook(program2) {
|
|
|
887
1133
|
}
|
|
888
1134
|
|
|
889
1135
|
// src/cli/init.ts
|
|
890
|
-
var
|
|
891
|
-
var
|
|
1136
|
+
var import_fs12 = __toESM(require("fs"));
|
|
1137
|
+
var import_path9 = __toESM(require("path"));
|
|
892
1138
|
var import_os4 = __toESM(require("os"));
|
|
893
1139
|
var import_prompts = __toESM(require("prompts"));
|
|
894
1140
|
var import_chalk2 = __toESM(require("chalk"));
|
|
895
1141
|
|
|
896
|
-
// src/
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1142
|
+
// src/utils/format.ts
|
|
1143
|
+
function formatDuration(minutes) {
|
|
1144
|
+
if (minutes < 60) return `${Math.round(minutes)}m`;
|
|
1145
|
+
const h = Math.floor(minutes / 60);
|
|
1146
|
+
const m = Math.round(minutes % 60);
|
|
1147
|
+
return m > 0 ? `${h}h ${m}m` : `${h}h`;
|
|
1148
|
+
}
|
|
1149
|
+
function formatDate(isoString) {
|
|
1150
|
+
return new Date(isoString).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
1151
|
+
}
|
|
1152
|
+
function todayDate() {
|
|
1153
|
+
return localDateStr(/* @__PURE__ */ new Date());
|
|
1154
|
+
}
|
|
1155
|
+
function parseDate(input) {
|
|
1156
|
+
const d = /* @__PURE__ */ new Date(input + "T00:00:00");
|
|
1157
|
+
if (isNaN(d.getTime())) throw new Error(`Invalid date: ${input}`);
|
|
1158
|
+
return input;
|
|
1159
|
+
}
|
|
1160
|
+
function localDateStr(d) {
|
|
1161
|
+
const y = d.getFullYear();
|
|
1162
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
1163
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
1164
|
+
return `${y}-${m}-${day}`;
|
|
1165
|
+
}
|
|
1166
|
+
function startOfWeek(dateStr) {
|
|
1167
|
+
const d = /* @__PURE__ */ new Date(dateStr + "T00:00:00");
|
|
1168
|
+
const day = d.getDay();
|
|
1169
|
+
const diff = day === 0 ? -6 : 1 - day;
|
|
1170
|
+
d.setDate(d.getDate() + diff);
|
|
1171
|
+
return localDateStr(d);
|
|
1172
|
+
}
|
|
1173
|
+
function eachDayOfRange(start, end) {
|
|
1174
|
+
const days = [];
|
|
1175
|
+
const cur = /* @__PURE__ */ new Date(start + "T00:00:00");
|
|
1176
|
+
const last = /* @__PURE__ */ new Date(end + "T00:00:00");
|
|
1177
|
+
while (cur <= last) {
|
|
1178
|
+
days.push(localDateStr(cur));
|
|
1179
|
+
cur.setDate(cur.getDate() + 1);
|
|
1180
|
+
}
|
|
1181
|
+
return days;
|
|
1182
|
+
}
|
|
1183
|
+
function isoWeekKey(dateStr) {
|
|
1184
|
+
const d = /* @__PURE__ */ new Date(dateStr + "T00:00:00");
|
|
1185
|
+
const jan4 = new Date(d.getFullYear(), 0, 4);
|
|
1186
|
+
const weekNum = Math.ceil(((d.getTime() - jan4.getTime()) / 864e5 + jan4.getDay() + 1) / 7);
|
|
1187
|
+
return `${d.getFullYear()}-W${String(weekNum).padStart(2, "0")}`;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// src/ai/prompt.ts
|
|
1191
|
+
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.
|
|
1192
|
+
|
|
1193
|
+
Here is an example:
|
|
1194
|
+
|
|
1195
|
+
INPUT:
|
|
1196
|
+
Project: api-service
|
|
1197
|
+
Branch: feature/auth
|
|
1198
|
+
Duration: 2h 00m
|
|
1199
|
+
Events:
|
|
1200
|
+
- versioning {"branch":"feature/auth"}
|
|
1201
|
+
- 2x dependencies {"tool":"npm","packages":["bcrypt","jsonwebtoken"]}
|
|
1202
|
+
- 8x testing [failed]
|
|
1203
|
+
- testing
|
|
1204
|
+
- 2x files {"tool":"mkdir"}
|
|
1205
|
+
- shipping {"message":"fix: auth token validation"}
|
|
1206
|
+
- shipping {"remote":"origin","branch":"feature/auth"}
|
|
1207
|
+
|
|
1208
|
+
OUTPUT:
|
|
1209
|
+
- Added authentication dependencies (bcrypt, JWT)
|
|
1210
|
+
- Debugged and resolved failing auth tests
|
|
1211
|
+
- Committed and pushed the token validation fix
|
|
1212
|
+
|
|
1213
|
+
RULES \u2014 follow these exactly:
|
|
1214
|
+
|
|
1215
|
+
1. Interpret repeated events as patterns, not individual steps.
|
|
1216
|
+
- Many testing [failed] followed by a passing test = debugging a failure. Say "resolved failing X tests", not "ran tests multiple times".
|
|
1217
|
+
- Many versioning events = branch housekeeping. Fold into context, do not list each git command.
|
|
1218
|
+
|
|
1219
|
+
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.
|
|
1220
|
+
|
|
1221
|
+
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.
|
|
1222
|
+
|
|
1223
|
+
4. For other events with a hint field: describe the developer's intent, not the raw command.
|
|
1224
|
+
|
|
1225
|
+
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.
|
|
1226
|
+
|
|
1227
|
+
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).
|
|
1228
|
+
|
|
1229
|
+
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.
|
|
1230
|
+
|
|
1231
|
+
Maximum 5 bullets. Output ONLY the bullet points, nothing else.`;
|
|
1232
|
+
function getSystemPrompt() {
|
|
1233
|
+
const contexts = getActiveProfileAiContexts();
|
|
1234
|
+
if (contexts.length === 0) return SYSTEM_PROMPT;
|
|
1235
|
+
return SYSTEM_PROMPT + "\n\n" + contexts.join("\n");
|
|
1236
|
+
}
|
|
1237
|
+
var DEFAULT_EXCLUDED_CATEGORIES = /* @__PURE__ */ new Set(["exploring", "note", "linting"]);
|
|
1238
|
+
function buildExcludedSet(includeCategories = []) {
|
|
1239
|
+
const excluded = new Set(DEFAULT_EXCLUDED_CATEGORIES);
|
|
1240
|
+
for (const cat of includeCategories) excluded.delete(cat);
|
|
1241
|
+
return excluded;
|
|
1242
|
+
}
|
|
1243
|
+
function countExcludedEvents(sessions, excluded) {
|
|
1244
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1245
|
+
for (const session of sessions) {
|
|
1246
|
+
for (const event of session.events) {
|
|
1247
|
+
if (excluded.has(event.category)) {
|
|
1248
|
+
counts.set(event.category, (counts.get(event.category) ?? 0) + 1);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
return counts;
|
|
1253
|
+
}
|
|
1254
|
+
function buildUserMessage(payload, includeCategories = []) {
|
|
1255
|
+
const excluded = buildExcludedSet(includeCategories);
|
|
1256
|
+
const lines = [`Date: ${payload.date}`, ""];
|
|
1257
|
+
for (const session of payload.sessions) {
|
|
1258
|
+
lines.push(`Project: ${session.project ?? "unknown"}`);
|
|
1259
|
+
if (session.branch) lines.push(`Branch: ${session.branch}`);
|
|
1260
|
+
lines.push(`Duration: ${formatDuration(session.durationMinutes)}`);
|
|
1261
|
+
for (const event of session.events) {
|
|
1262
|
+
if (event.category === "note") {
|
|
1263
|
+
lines.push(`NOTE: ${event.metadata.text}`);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
lines.push("Events:");
|
|
1267
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1268
|
+
for (const event of session.events) {
|
|
1269
|
+
if (excluded.has(event.category)) continue;
|
|
1270
|
+
const failed = event.exitCode !== null && event.exitCode !== 0 ? " [failed]" : "";
|
|
1271
|
+
let key;
|
|
1272
|
+
if (event.hint) {
|
|
1273
|
+
key = `other [${event.hint}]${failed}`;
|
|
1274
|
+
} else {
|
|
1275
|
+
const meta = Object.keys(event.metadata).length > 0 ? ` ${JSON.stringify(event.metadata)}` : "";
|
|
1276
|
+
key = `${event.category}${meta}${failed}`;
|
|
1277
|
+
}
|
|
1278
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
1279
|
+
}
|
|
1280
|
+
for (const [key, count] of counts) {
|
|
1281
|
+
lines.push(` - ${count > 1 ? `${count}x ` : ""}${key}`);
|
|
1282
|
+
}
|
|
1283
|
+
lines.push("");
|
|
1284
|
+
}
|
|
1285
|
+
return lines.join("\n");
|
|
1286
|
+
}
|
|
1287
|
+
var WEEKLY_SYSTEM_PROMPT = `You write short weekly developer summaries. Here is an example:
|
|
1288
|
+
|
|
1289
|
+
INPUT:
|
|
1290
|
+
Week: 2026-05-18 to 2026-05-22
|
|
1291
|
+
|
|
1292
|
+
Monday 2026-05-18:
|
|
1293
|
+
Project: api-service (feature/auth) \u2014 2h 10m
|
|
1294
|
+
Events: 3x code_commit, 2x test_run, dependency_install
|
|
1295
|
+
|
|
1296
|
+
Tuesday 2026-05-19:
|
|
1297
|
+
Project: api-service (feature/auth) \u2014 1h 30m
|
|
1298
|
+
Events: 2x code_commit, test_run, build
|
|
1299
|
+
|
|
1300
|
+
Wednesday 2026-05-20:
|
|
1301
|
+
Project: dashboard (main) \u2014 3h 00m
|
|
1302
|
+
Events: 4x code_commit, 2x build, test_run
|
|
1303
|
+
|
|
1304
|
+
Thursday 2026-05-21:
|
|
1305
|
+
Project: api-service (feature/auth) \u2014 1h 00m
|
|
1306
|
+
Events: code_commit {"message":"feat: merge auth"}, branch_switch
|
|
1307
|
+
|
|
1308
|
+
Friday 2026-05-22:
|
|
1309
|
+
Project: dashboard (main) \u2014 2h 00m
|
|
1310
|
+
Events: 3x code_commit, build
|
|
1311
|
+
|
|
1312
|
+
OUTPUT:
|
|
1313
|
+
## Week of 18 May \u2014 9h 40m across 2 projects
|
|
1314
|
+
|
|
1315
|
+
**api-service** (Mon\u2013Thu): Built and shipped the auth feature \u2014 JWT setup, token validation fixes, and final merge.
|
|
1316
|
+
**dashboard** (Wed, Fri): Active development across 7 commits with builds passing.
|
|
1317
|
+
|
|
1318
|
+
Now summarise the following week. Output ONLY the summary \u2014 a one-line header then one bullet per active project. Maximum 4 bullets.`;
|
|
1319
|
+
var DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
1320
|
+
function buildWeeklyMessage(payload) {
|
|
1321
|
+
const lines = [`Week: ${payload.weekStart} to ${payload.weekEnd}`, ""];
|
|
1322
|
+
for (const { date, sessions } of payload.days) {
|
|
1323
|
+
const dayName = DAY_NAMES[(/* @__PURE__ */ new Date(date + "T00:00:00")).getDay()];
|
|
1324
|
+
lines.push(`${dayName} ${date}:`);
|
|
1325
|
+
for (const session of sessions) {
|
|
1326
|
+
const project = session.project ?? "unknown";
|
|
1327
|
+
const branch = session.branch ? ` (${session.branch})` : "";
|
|
1328
|
+
lines.push(` Project: ${project}${branch} \u2014 ${formatDuration(session.durationMinutes)}`);
|
|
1329
|
+
for (const event of session.events) {
|
|
1330
|
+
if (event.category === "note") {
|
|
1331
|
+
lines.push(` NOTE: ${event.metadata.text}`);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
const excluded = buildExcludedSet();
|
|
1335
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1336
|
+
for (const event of session.events) {
|
|
1337
|
+
if (excluded.has(event.category)) continue;
|
|
1338
|
+
const failed = event.exitCode !== null && event.exitCode !== 0 ? " [failed]" : "";
|
|
1339
|
+
let key;
|
|
1340
|
+
if (event.hint) {
|
|
1341
|
+
key = `other [${event.hint}]${failed}`;
|
|
1342
|
+
} else {
|
|
1343
|
+
const meta = Object.keys(event.metadata).length > 0 ? ` ${JSON.stringify(event.metadata)}` : "";
|
|
1344
|
+
key = `${event.category}${meta}${failed}`;
|
|
1345
|
+
}
|
|
1346
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
1347
|
+
}
|
|
1348
|
+
const eventLine = [...counts.entries()].map(([key, count]) => `${count > 1 ? `${count}x ` : ""}${key}`).join(", ");
|
|
1349
|
+
lines.push(` Events: ${eventLine}`);
|
|
1350
|
+
}
|
|
1351
|
+
lines.push("");
|
|
1352
|
+
}
|
|
1353
|
+
return lines.join("\n");
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// src/ai/fetch.ts
|
|
1357
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
1358
|
+
async function fetchWithTimeout(url, init, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
1359
|
+
const controller = new AbortController();
|
|
1360
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1361
|
+
try {
|
|
1362
|
+
return await fetch(url, { ...init, signal: controller.signal });
|
|
1363
|
+
} catch (err) {
|
|
1364
|
+
if (err.name === "AbortError") {
|
|
1365
|
+
throw new Error(`Request timed out after ${timeoutMs / 1e3}s`);
|
|
1366
|
+
}
|
|
1367
|
+
throw err;
|
|
1368
|
+
} finally {
|
|
1369
|
+
clearTimeout(timer);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
// src/ai/providers/index.ts
|
|
1374
|
+
var GenericProvider = class {
|
|
1375
|
+
constructor(config, model, apiKey, baseUrl, includeCategories = []) {
|
|
1376
|
+
this.config = config;
|
|
1377
|
+
this.model = model;
|
|
1378
|
+
this.apiKey = apiKey;
|
|
1379
|
+
this.baseUrl = baseUrl;
|
|
1380
|
+
this.includeCategories = includeCategories;
|
|
1381
|
+
}
|
|
1382
|
+
config;
|
|
1383
|
+
model;
|
|
1384
|
+
apiKey;
|
|
1385
|
+
baseUrl;
|
|
1386
|
+
includeCategories;
|
|
1387
|
+
async call(system, userMsg, maxTokens) {
|
|
1388
|
+
const response = await fetchWithTimeout(this.config.chatUrl(this.baseUrl), {
|
|
1389
|
+
method: "POST",
|
|
1390
|
+
headers: this.config.headers(this.apiKey),
|
|
1391
|
+
body: JSON.stringify(this.config.buildChatBody(this.model, system, userMsg, maxTokens))
|
|
1392
|
+
});
|
|
1393
|
+
if (!response.ok) {
|
|
1394
|
+
throw new Error(this.config.chatError(response.status, await response.text()));
|
|
1395
|
+
}
|
|
1396
|
+
return this.config.parseText(await response.json());
|
|
1397
|
+
}
|
|
1398
|
+
async summarize(payload) {
|
|
1399
|
+
return this.call(getSystemPrompt(), buildUserMessage(payload, this.includeCategories), this.config.dailyTokens);
|
|
1400
|
+
}
|
|
1401
|
+
async summarizeWeek(payload) {
|
|
1402
|
+
return this.call(WEEKLY_SYSTEM_PROMPT, buildWeeklyMessage(payload), this.config.weeklyTokens);
|
|
1403
|
+
}
|
|
1404
|
+
async validate() {
|
|
1405
|
+
const isPost = this.config.validateMethod === "POST";
|
|
1406
|
+
const response = await fetchWithTimeout(this.config.validateUrl(this.baseUrl), {
|
|
1407
|
+
method: this.config.validateMethod,
|
|
1408
|
+
headers: isPost ? this.config.headers(this.apiKey) : void 0,
|
|
1409
|
+
body: isPost && this.config.buildValidateBody ? JSON.stringify(this.config.buildValidateBody(this.model)) : void 0
|
|
1410
|
+
});
|
|
1411
|
+
if (!response.ok) {
|
|
1412
|
+
throw new Error(this.config.validateError(response.status, await response.text(), this.baseUrl));
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
};
|
|
1416
|
+
|
|
1417
|
+
// src/ai/providers/config.ts
|
|
1418
|
+
var PROVIDER_CONFIGS = {
|
|
1419
|
+
anthropic: {
|
|
1420
|
+
defaultModel: "claude-haiku-4-5-20251001",
|
|
1421
|
+
defaultBaseUrl: "https://api.anthropic.com/v1",
|
|
1422
|
+
dailyTokens: 1024,
|
|
1423
|
+
weeklyTokens: 1024,
|
|
1424
|
+
chatUrl: (baseUrl) => `${baseUrl}/messages`,
|
|
1425
|
+
validateUrl: (baseUrl) => `${baseUrl}/messages`,
|
|
1426
|
+
validateMethod: "POST",
|
|
1427
|
+
headers: (apiKey) => ({
|
|
1428
|
+
"Content-Type": "application/json",
|
|
1429
|
+
"x-api-key": apiKey,
|
|
1430
|
+
"anthropic-version": "2023-06-01"
|
|
1431
|
+
}),
|
|
1432
|
+
buildChatBody: (model, system, userMsg, maxTokens) => ({
|
|
1433
|
+
model,
|
|
1434
|
+
max_tokens: maxTokens,
|
|
1435
|
+
system,
|
|
1436
|
+
messages: [{ role: "user", content: userMsg }]
|
|
1437
|
+
}),
|
|
1438
|
+
buildValidateBody: (model) => ({
|
|
1439
|
+
model,
|
|
1440
|
+
max_tokens: 1,
|
|
1441
|
+
messages: [{ role: "user", content: "." }]
|
|
1442
|
+
}),
|
|
1443
|
+
parseText: (json) => {
|
|
1444
|
+
const data = json;
|
|
1445
|
+
return data.content.find((b) => b.type === "text")?.text ?? "";
|
|
1446
|
+
},
|
|
1447
|
+
chatError: (status, body) => `Anthropic API error ${status}: ${body}`,
|
|
1448
|
+
validateError: (status, body) => `Anthropic API error ${status}: ${body}`
|
|
1449
|
+
},
|
|
1450
|
+
openai: {
|
|
1451
|
+
defaultModel: "gpt-4o-mini",
|
|
1452
|
+
defaultBaseUrl: "https://api.openai.com/v1",
|
|
1453
|
+
dailyTokens: 1024,
|
|
1454
|
+
weeklyTokens: 1024,
|
|
1455
|
+
chatUrl: (baseUrl) => `${baseUrl}/chat/completions`,
|
|
1456
|
+
validateUrl: (baseUrl) => `${baseUrl}/chat/completions`,
|
|
1457
|
+
validateMethod: "POST",
|
|
1458
|
+
headers: (apiKey) => ({
|
|
1459
|
+
"Content-Type": "application/json",
|
|
1460
|
+
Authorization: `Bearer ${apiKey}`
|
|
1461
|
+
}),
|
|
1462
|
+
buildChatBody: (model, system, userMsg, maxTokens) => ({
|
|
1463
|
+
model,
|
|
1464
|
+
messages: [
|
|
1465
|
+
{ role: "system", content: system },
|
|
1466
|
+
{ role: "user", content: userMsg }
|
|
1467
|
+
],
|
|
1468
|
+
max_tokens: maxTokens
|
|
1469
|
+
}),
|
|
1470
|
+
buildValidateBody: (model) => ({
|
|
1471
|
+
model,
|
|
1472
|
+
messages: [{ role: "user", content: "." }],
|
|
1473
|
+
max_tokens: 1
|
|
1474
|
+
}),
|
|
1475
|
+
parseText: (json) => {
|
|
1476
|
+
const data = json;
|
|
1477
|
+
return data.choices[0]?.message?.content ?? "";
|
|
1478
|
+
},
|
|
1479
|
+
chatError: (status, body) => `OpenAI API error ${status}: ${body}`,
|
|
1480
|
+
validateError: (status, body) => `OpenAI API error ${status}: ${body}`
|
|
1481
|
+
},
|
|
1482
|
+
ollama: {
|
|
1483
|
+
defaultModel: "llama3",
|
|
1484
|
+
defaultBaseUrl: "http://localhost:11434",
|
|
1485
|
+
dailyTokens: 300,
|
|
1486
|
+
weeklyTokens: 400,
|
|
1487
|
+
chatUrl: (baseUrl) => `${baseUrl}/api/chat`,
|
|
1488
|
+
validateUrl: (baseUrl) => `${baseUrl}/api/tags`,
|
|
1489
|
+
validateMethod: "GET",
|
|
1490
|
+
headers: () => ({ "Content-Type": "application/json" }),
|
|
1491
|
+
buildChatBody: (model, system, userMsg, maxTokens) => ({
|
|
1492
|
+
model,
|
|
1493
|
+
stream: false,
|
|
1494
|
+
messages: [
|
|
1495
|
+
{ role: "system", content: system },
|
|
1496
|
+
{ role: "user", content: userMsg }
|
|
1497
|
+
],
|
|
1498
|
+
options: { num_predict: maxTokens, temperature: 0.3 }
|
|
1499
|
+
}),
|
|
1500
|
+
parseText: (json) => {
|
|
1501
|
+
return json.message?.content ?? "";
|
|
1502
|
+
},
|
|
1503
|
+
chatError: (status, body) => `Ollama error ${status}: ${body}. Is Ollama running? Try: ollama serve`,
|
|
1504
|
+
validateError: (_status, _body, baseUrl) => `Ollama is not reachable at ${baseUrl}. Is it running? Try: ollama serve`
|
|
1505
|
+
}
|
|
1506
|
+
};
|
|
1507
|
+
|
|
1508
|
+
// src/ai/router.ts
|
|
1509
|
+
function getProvider(config) {
|
|
1510
|
+
const { provider, apiKey = "", model, baseUrl } = config.ai;
|
|
1511
|
+
if (!provider) {
|
|
1512
|
+
throw new Error(
|
|
1513
|
+
"No AI provider configured.\nRun: workscribe config set ai.provider <anthropic|openai|ollama|openai-compatible>"
|
|
1514
|
+
);
|
|
1515
|
+
}
|
|
1516
|
+
const configKey = provider === "openai-compatible" ? "openai" : provider;
|
|
1517
|
+
const providerConfig = PROVIDER_CONFIGS[configKey];
|
|
1518
|
+
if (!providerConfig) {
|
|
1519
|
+
throw new Error(
|
|
1520
|
+
`Unknown provider: "${provider}"
|
|
1521
|
+
Supported providers: anthropic, openai, ollama, openai-compatible`
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
if (provider !== "ollama" && !apiKey) {
|
|
1525
|
+
throw new Error(`${provider} requires an API key.
|
|
1526
|
+
Run: workscribe config set ai.apiKey <your-key>`);
|
|
1527
|
+
}
|
|
1528
|
+
if (provider === "openai-compatible" && !baseUrl) {
|
|
1529
|
+
throw new Error("openai-compatible requires a base URL.\nRun: workscribe config set ai.baseUrl <url>");
|
|
1530
|
+
}
|
|
1531
|
+
return new GenericProvider(
|
|
1532
|
+
providerConfig,
|
|
1533
|
+
model || providerConfig.defaultModel,
|
|
1534
|
+
apiKey,
|
|
1535
|
+
baseUrl || providerConfig.defaultBaseUrl,
|
|
1536
|
+
config.summary?.includeCategories ?? []
|
|
1537
|
+
);
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
// src/capture/shell-hook.ts
|
|
1541
|
+
var import_fs10 = __toESM(require("fs"));
|
|
1542
|
+
var import_path7 = __toESM(require("path"));
|
|
1543
|
+
var HOOK_START = "# workscribe:hook:start";
|
|
1544
|
+
var HOOK_END = "# workscribe:hook:end";
|
|
1545
|
+
var HOOK_PREAMBLE = "# Workscribe \u2014 developer work journal";
|
|
1546
|
+
function getInstalledHookBlock(rcFile) {
|
|
1547
|
+
if (!import_fs10.default.existsSync(rcFile)) return null;
|
|
1548
|
+
const content = import_fs10.default.readFileSync(rcFile, "utf-8");
|
|
1549
|
+
const startIdx = content.indexOf(HOOK_START);
|
|
1550
|
+
const endIdx = content.indexOf(HOOK_END);
|
|
1551
|
+
if (startIdx === -1 || endIdx === -1) return null;
|
|
1552
|
+
return content.slice(startIdx, endIdx + HOOK_END.length);
|
|
909
1553
|
}
|
|
910
1554
|
function isHookInstalled(rcFile) {
|
|
911
1555
|
return getInstalledHookBlock(rcFile) !== null;
|
|
@@ -917,38 +1561,38 @@ function isHookOutdated(rcFile, hookContent) {
|
|
|
917
1561
|
}
|
|
918
1562
|
function installHook(rcFile, hookContent) {
|
|
919
1563
|
const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
|
|
920
|
-
|
|
921
|
-
log.info(`Backed up ${rcFile} \u2192 ${
|
|
922
|
-
|
|
1564
|
+
import_fs10.default.copyFileSync(rcFile, backup);
|
|
1565
|
+
log.info(`Backed up ${rcFile} \u2192 ${import_path7.default.basename(backup)}`);
|
|
1566
|
+
import_fs10.default.appendFileSync(rcFile, `
|
|
923
1567
|
${hookContent}
|
|
924
1568
|
`);
|
|
925
1569
|
}
|
|
926
1570
|
function updateHook(rcFile, hookContent) {
|
|
927
1571
|
const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
|
|
928
|
-
|
|
929
|
-
log.info(`Backed up ${rcFile} \u2192 ${
|
|
930
|
-
const content =
|
|
1572
|
+
import_fs10.default.copyFileSync(rcFile, backup);
|
|
1573
|
+
log.info(`Backed up ${rcFile} \u2192 ${import_path7.default.basename(backup)}`);
|
|
1574
|
+
const content = import_fs10.default.readFileSync(rcFile, "utf-8");
|
|
931
1575
|
const startIdx = content.indexOf(HOOK_START);
|
|
932
1576
|
const endIdx = content.indexOf(HOOK_END) + HOOK_END.length;
|
|
933
|
-
|
|
1577
|
+
import_fs10.default.writeFileSync(rcFile, content.slice(0, startIdx) + hookContent + content.slice(endIdx));
|
|
934
1578
|
}
|
|
935
1579
|
function removeHook(rcFile) {
|
|
936
1580
|
const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
|
|
937
|
-
|
|
938
|
-
log.info(`Backed up ${rcFile} \u2192 ${
|
|
939
|
-
const content =
|
|
1581
|
+
import_fs10.default.copyFileSync(rcFile, backup);
|
|
1582
|
+
log.info(`Backed up ${rcFile} \u2192 ${import_path7.default.basename(backup)}`);
|
|
1583
|
+
const content = import_fs10.default.readFileSync(rcFile, "utf-8");
|
|
940
1584
|
const startIdx = content.indexOf(HOOK_START);
|
|
941
1585
|
const endIdx = content.indexOf(HOOK_END) + HOOK_END.length;
|
|
942
1586
|
const preambleIdx = content.lastIndexOf(HOOK_PREAMBLE, startIdx);
|
|
943
1587
|
const blockStart = preambleIdx !== -1 ? preambleIdx : startIdx;
|
|
944
1588
|
const before = content.slice(0, blockStart).replace(/\n+$/, "\n");
|
|
945
1589
|
const after = content.slice(endIdx).replace(/^\n/, "");
|
|
946
|
-
|
|
1590
|
+
import_fs10.default.writeFileSync(rcFile, before + after);
|
|
947
1591
|
}
|
|
948
1592
|
|
|
949
1593
|
// src/capture/hooks/claude-code.ts
|
|
950
|
-
var
|
|
951
|
-
var
|
|
1594
|
+
var import_fs11 = __toESM(require("fs"));
|
|
1595
|
+
var import_path8 = __toESM(require("path"));
|
|
952
1596
|
var import_os3 = __toESM(require("os"));
|
|
953
1597
|
|
|
954
1598
|
// src/capture/hooks/utils.ts
|
|
@@ -963,11 +1607,11 @@ function isCommandInstalled(cmd) {
|
|
|
963
1607
|
}
|
|
964
1608
|
|
|
965
1609
|
// src/capture/hooks/claude-code.ts
|
|
966
|
-
var CLAUDE_SETTINGS_PATH =
|
|
1610
|
+
var CLAUDE_SETTINGS_PATH = import_path8.default.join(import_os3.default.homedir(), ".claude", "settings.json");
|
|
967
1611
|
function readSettings() {
|
|
968
|
-
if (!
|
|
1612
|
+
if (!import_fs11.default.existsSync(CLAUDE_SETTINGS_PATH)) return {};
|
|
969
1613
|
try {
|
|
970
|
-
return JSON.parse(
|
|
1614
|
+
return JSON.parse(import_fs11.default.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8"));
|
|
971
1615
|
} catch {
|
|
972
1616
|
return {};
|
|
973
1617
|
}
|
|
@@ -1002,8 +1646,8 @@ var claudeCodeIntegration = {
|
|
|
1002
1646
|
}
|
|
1003
1647
|
]
|
|
1004
1648
|
};
|
|
1005
|
-
|
|
1006
|
-
|
|
1649
|
+
import_fs11.default.mkdirSync(import_path8.default.dirname(CLAUDE_SETTINGS_PATH), { recursive: true });
|
|
1650
|
+
import_fs11.default.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
1007
1651
|
}
|
|
1008
1652
|
};
|
|
1009
1653
|
|
|
@@ -1015,36 +1659,54 @@ var AI_HOOK_INTEGRATIONS = {
|
|
|
1015
1659
|
// src/cli/init.ts
|
|
1016
1660
|
function detectShell() {
|
|
1017
1661
|
const shell = process.env.SHELL ?? "";
|
|
1018
|
-
if (shell.includes("zsh")) return { name: "zsh", rcFile:
|
|
1019
|
-
if (shell.includes("bash")) return { name: "bash", rcFile:
|
|
1662
|
+
if (shell.includes("zsh")) return { name: "zsh", rcFile: import_path9.default.join(import_os4.default.homedir(), ".zshrc") };
|
|
1663
|
+
if (shell.includes("bash")) return { name: "bash", rcFile: import_path9.default.join(import_os4.default.homedir(), ".bashrc") };
|
|
1020
1664
|
return null;
|
|
1021
1665
|
}
|
|
1666
|
+
function step(current, total, label) {
|
|
1667
|
+
log.plain(import_chalk2.default.bold(`
|
|
1668
|
+
[${current}/${total}] ${label}`));
|
|
1669
|
+
}
|
|
1022
1670
|
function registerInit(program2) {
|
|
1023
1671
|
program2.command("init").description("Set up Workscribe and install or update shell hooks").action(async () => {
|
|
1672
|
+
const TOTAL_STEPS = 5;
|
|
1024
1673
|
log.plain(import_chalk2.default.bold("\n Workscribe \u2014 Developer Work Journal\n"));
|
|
1025
|
-
|
|
1026
|
-
|
|
1674
|
+
step(1, TOTAL_STEPS, "Creating data directory");
|
|
1675
|
+
if (!import_fs12.default.existsSync(WORKSCRIBE_DIR)) {
|
|
1676
|
+
import_fs12.default.mkdirSync(WORKSCRIBE_DIR, { recursive: true, mode: 448 });
|
|
1027
1677
|
log.success(`Created ${WORKSCRIBE_DIR}`);
|
|
1028
1678
|
} else {
|
|
1029
1679
|
try {
|
|
1030
|
-
|
|
1680
|
+
import_fs12.default.chmodSync(WORKSCRIBE_DIR, 448);
|
|
1031
1681
|
} catch {
|
|
1032
1682
|
}
|
|
1683
|
+
log.info(`${WORKSCRIBE_DIR} already exists`);
|
|
1033
1684
|
}
|
|
1685
|
+
if (!import_fs12.default.existsSync(PROFILES_DIR)) {
|
|
1686
|
+
import_fs12.default.mkdirSync(PROFILES_DIR, { recursive: true, mode: 448 });
|
|
1687
|
+
}
|
|
1688
|
+
step(2, TOTAL_STEPS, "Initialising database");
|
|
1034
1689
|
getDb();
|
|
1035
|
-
log.success("Database
|
|
1690
|
+
log.success("Database ready");
|
|
1691
|
+
step(3, TOTAL_STEPS, "Installing shell hook");
|
|
1036
1692
|
const shell = detectShell();
|
|
1037
1693
|
if (!shell) {
|
|
1038
1694
|
log.warn("Could not detect shell. Supported: zsh, bash");
|
|
1039
1695
|
log.plain(`Manually add the hook from: ${import_chalk2.default.cyan("shell/hook.zsh")} or ${import_chalk2.default.cyan("shell/hook.bash")}`);
|
|
1040
1696
|
} else {
|
|
1041
|
-
const hookFile =
|
|
1042
|
-
if (!
|
|
1697
|
+
const hookFile = import_path9.default.join(__dirname, "..", "shell", `hook.${shell.name}`);
|
|
1698
|
+
if (!import_fs12.default.existsSync(hookFile)) {
|
|
1043
1699
|
log.warn(`Hook template not found at ${hookFile}`);
|
|
1044
1700
|
} else {
|
|
1045
|
-
const hookContent =
|
|
1046
|
-
if (!
|
|
1701
|
+
const hookContent = import_fs12.default.readFileSync(hookFile, "utf-8");
|
|
1702
|
+
if (!import_fs12.default.existsSync(shell.rcFile)) import_fs12.default.writeFileSync(shell.rcFile, "");
|
|
1047
1703
|
if (!isHookInstalled(shell.rcFile)) {
|
|
1704
|
+
log.plain("");
|
|
1705
|
+
log.plain(import_chalk2.default.dim(" Workscribe adds a lightweight hook to your shell that captures each"));
|
|
1706
|
+
log.plain(import_chalk2.default.dim(" command you run \u2014 the text, exit code, duration, and working directory."));
|
|
1707
|
+
log.plain(import_chalk2.default.dim(" It runs in the background and adds no latency to your terminal."));
|
|
1708
|
+
log.plain(import_chalk2.default.dim(" Credentials and secrets are redacted before anything is stored."));
|
|
1709
|
+
log.plain("");
|
|
1048
1710
|
installHook(shell.rcFile, hookContent);
|
|
1049
1711
|
log.success(`Shell hook installed in ${shell.rcFile}`);
|
|
1050
1712
|
} else if (isHookOutdated(shell.rcFile, hookContent)) {
|
|
@@ -1065,6 +1727,7 @@ function registerInit(program2) {
|
|
|
1065
1727
|
}
|
|
1066
1728
|
}
|
|
1067
1729
|
}
|
|
1730
|
+
step(4, TOTAL_STEPS, "Detecting AI coding tools");
|
|
1068
1731
|
const installedTools = loadAIToolRegistry().filter(
|
|
1069
1732
|
(entry) => entry.hook !== null && AI_HOOK_INTEGRATIONS[entry.hook]?.isInstalled()
|
|
1070
1733
|
);
|
|
@@ -1092,6 +1755,35 @@ function registerInit(program2) {
|
|
|
1092
1755
|
}
|
|
1093
1756
|
}
|
|
1094
1757
|
const config = readConfig();
|
|
1758
|
+
try {
|
|
1759
|
+
await cacheManifest();
|
|
1760
|
+
const detected = detectProfilesForDirectory(process.cwd());
|
|
1761
|
+
const alreadyActive = new Set(config.capture.profiles);
|
|
1762
|
+
const toPrompt = detected.filter((entry) => !alreadyActive.has(entry.name));
|
|
1763
|
+
if (toPrompt.length > 0) {
|
|
1764
|
+
log.plain("");
|
|
1765
|
+
for (const entry of toPrompt) {
|
|
1766
|
+
const { install } = await (0, import_prompts.default)({
|
|
1767
|
+
type: "confirm",
|
|
1768
|
+
name: "install",
|
|
1769
|
+
message: `${entry.displayName} project detected. Enable the ${entry.displayName} profile?`,
|
|
1770
|
+
initial: true
|
|
1771
|
+
});
|
|
1772
|
+
if (install) {
|
|
1773
|
+
await installProfile(entry.name);
|
|
1774
|
+
config.capture.profiles = [.../* @__PURE__ */ new Set([...config.capture.profiles, entry.name])];
|
|
1775
|
+
log.success(`${entry.displayName} profile enabled`);
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
writeConfig(config);
|
|
1779
|
+
} else if (detected.length === 0) {
|
|
1780
|
+
log.info("No project profiles detected \u2014 run workscribe profile detect from your project directory");
|
|
1781
|
+
}
|
|
1782
|
+
} catch {
|
|
1783
|
+
log.info("Could not reach profile registry \u2014 run workscribe profile detect after connecting");
|
|
1784
|
+
}
|
|
1785
|
+
step(5, TOTAL_STEPS, "Configuring AI provider");
|
|
1786
|
+
let providerConfigured = !!config.ai.provider;
|
|
1095
1787
|
if (config.ai.provider) {
|
|
1096
1788
|
log.info(`AI provider already configured: ${import_chalk2.default.cyan(config.ai.provider)}`);
|
|
1097
1789
|
} else {
|
|
@@ -1108,7 +1800,10 @@ function registerInit(program2) {
|
|
|
1108
1800
|
{ title: "Skip \u2014 configure later", value: "skip" }
|
|
1109
1801
|
]
|
|
1110
1802
|
});
|
|
1111
|
-
if (provider
|
|
1803
|
+
if (!provider || provider === "skip") {
|
|
1804
|
+
providerConfigured = false;
|
|
1805
|
+
log.info("Skipped \u2014 run later: workscribe config set ai.provider <provider>");
|
|
1806
|
+
} else {
|
|
1112
1807
|
config.ai.provider = provider;
|
|
1113
1808
|
if (provider === "ollama") {
|
|
1114
1809
|
log.plain(`
|
|
@@ -1133,90 +1828,73 @@ function registerInit(program2) {
|
|
|
1133
1828
|
if (baseUrl) config.ai.baseUrl = baseUrl;
|
|
1134
1829
|
}
|
|
1135
1830
|
}
|
|
1136
|
-
|
|
1137
|
-
log.
|
|
1138
|
-
|
|
1139
|
-
|
|
1831
|
+
log.plain("");
|
|
1832
|
+
log.info(`Validating ${import_chalk2.default.cyan(provider)} connection\u2026`);
|
|
1833
|
+
try {
|
|
1834
|
+
const testProvider = getProvider(config);
|
|
1835
|
+
await testProvider.validate();
|
|
1836
|
+
log.success("API key verified");
|
|
1837
|
+
writeConfig(config);
|
|
1838
|
+
providerConfigured = true;
|
|
1839
|
+
} catch (error) {
|
|
1840
|
+
log.warn(`Validation failed: ${error.message}`);
|
|
1841
|
+
const { retry } = await (0, import_prompts.default)({
|
|
1842
|
+
type: "confirm",
|
|
1843
|
+
name: "retry",
|
|
1844
|
+
message: "Save anyway and configure later?",
|
|
1845
|
+
initial: false
|
|
1846
|
+
});
|
|
1847
|
+
if (retry) {
|
|
1848
|
+
writeConfig(config);
|
|
1849
|
+
providerConfigured = false;
|
|
1850
|
+
log.info("Config saved \u2014 run workscribe config set ai.apiKey <key> to update");
|
|
1851
|
+
} else {
|
|
1852
|
+
log.info("API key not saved \u2014 run workscribe init again to configure");
|
|
1853
|
+
providerConfigured = false;
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1140
1856
|
}
|
|
1141
1857
|
}
|
|
1142
|
-
if (!
|
|
1858
|
+
if (!import_fs12.default.existsSync(CONFIG_PATH)) {
|
|
1143
1859
|
writeConfig(config);
|
|
1144
|
-
log.success("Config file created");
|
|
1145
1860
|
}
|
|
1146
1861
|
log.plain("");
|
|
1147
1862
|
log.plain(import_chalk2.default.bold(" Setup complete!"));
|
|
1863
|
+
log.plain("");
|
|
1148
1864
|
if (shell) {
|
|
1149
|
-
log.plain(
|
|
1150
|
-
log.plain(`
|
|
1865
|
+
log.plain(` Activate capture now:`);
|
|
1866
|
+
log.plain(` ${import_chalk2.default.cyan(`source ${shell.rcFile}`)}`);
|
|
1151
1867
|
log.plain(` Or restart your terminal.`);
|
|
1868
|
+
log.plain("");
|
|
1869
|
+
}
|
|
1870
|
+
if (providerConfigured) {
|
|
1871
|
+
log.plain(` Then run a few commands and generate your first summary:`);
|
|
1872
|
+
log.plain(` ${import_chalk2.default.cyan("workscribe summary")}`);
|
|
1873
|
+
log.plain("");
|
|
1874
|
+
log.plain(` Check capture is working:`);
|
|
1875
|
+
log.plain(` ${import_chalk2.default.cyan("workscribe status")}`);
|
|
1876
|
+
} else {
|
|
1877
|
+
log.plain(` Before generating summaries, configure an AI provider:`);
|
|
1878
|
+
log.plain(` ${import_chalk2.default.cyan("workscribe config set ai.provider anthropic")}`);
|
|
1879
|
+
log.plain(` ${import_chalk2.default.cyan("workscribe config set ai.apiKey <your-key>")}`);
|
|
1880
|
+
log.plain("");
|
|
1881
|
+
log.plain(` Then run:`);
|
|
1882
|
+
log.plain(` ${import_chalk2.default.cyan("workscribe summary")}`);
|
|
1152
1883
|
}
|
|
1153
|
-
log.plain("");
|
|
1154
|
-
log.plain(` ${import_chalk2.default.dim("workscribe summary")} \u2014 generate today's work summary`);
|
|
1155
|
-
log.plain(` ${import_chalk2.default.dim("workscribe status")} \u2014 check capture is working`);
|
|
1156
1884
|
log.plain("");
|
|
1157
1885
|
});
|
|
1158
1886
|
}
|
|
1159
1887
|
|
|
1160
1888
|
// src/cli/status.ts
|
|
1161
|
-
var
|
|
1889
|
+
var import_fs13 = __toESM(require("fs"));
|
|
1162
1890
|
var import_chalk3 = __toESM(require("chalk"));
|
|
1163
|
-
|
|
1164
|
-
// src/utils/format.ts
|
|
1165
|
-
function formatDuration(minutes) {
|
|
1166
|
-
if (minutes < 60) return `${Math.round(minutes)}m`;
|
|
1167
|
-
const h = Math.floor(minutes / 60);
|
|
1168
|
-
const m = Math.round(minutes % 60);
|
|
1169
|
-
return m > 0 ? `${h}h ${m}m` : `${h}h`;
|
|
1170
|
-
}
|
|
1171
|
-
function formatDate(isoString) {
|
|
1172
|
-
return new Date(isoString).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
1173
|
-
}
|
|
1174
|
-
function todayDate() {
|
|
1175
|
-
return localDateStr(/* @__PURE__ */ new Date());
|
|
1176
|
-
}
|
|
1177
|
-
function parseDate(input) {
|
|
1178
|
-
const d = /* @__PURE__ */ new Date(input + "T00:00:00");
|
|
1179
|
-
if (isNaN(d.getTime())) throw new Error(`Invalid date: ${input}`);
|
|
1180
|
-
return input;
|
|
1181
|
-
}
|
|
1182
|
-
function localDateStr(d) {
|
|
1183
|
-
const y = d.getFullYear();
|
|
1184
|
-
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
1185
|
-
const day = String(d.getDate()).padStart(2, "0");
|
|
1186
|
-
return `${y}-${m}-${day}`;
|
|
1187
|
-
}
|
|
1188
|
-
function startOfWeek(dateStr) {
|
|
1189
|
-
const d = /* @__PURE__ */ new Date(dateStr + "T00:00:00");
|
|
1190
|
-
const day = d.getDay();
|
|
1191
|
-
const diff = day === 0 ? -6 : 1 - day;
|
|
1192
|
-
d.setDate(d.getDate() + diff);
|
|
1193
|
-
return localDateStr(d);
|
|
1194
|
-
}
|
|
1195
|
-
function eachDayOfRange(start, end) {
|
|
1196
|
-
const days = [];
|
|
1197
|
-
const cur = /* @__PURE__ */ new Date(start + "T00:00:00");
|
|
1198
|
-
const last = /* @__PURE__ */ new Date(end + "T00:00:00");
|
|
1199
|
-
while (cur <= last) {
|
|
1200
|
-
days.push(localDateStr(cur));
|
|
1201
|
-
cur.setDate(cur.getDate() + 1);
|
|
1202
|
-
}
|
|
1203
|
-
return days;
|
|
1204
|
-
}
|
|
1205
|
-
function isoWeekKey(dateStr) {
|
|
1206
|
-
const d = /* @__PURE__ */ new Date(dateStr + "T00:00:00");
|
|
1207
|
-
const jan4 = new Date(d.getFullYear(), 0, 4);
|
|
1208
|
-
const weekNum = Math.ceil(((d.getTime() - jan4.getTime()) / 864e5 + jan4.getDay() + 1) / 7);
|
|
1209
|
-
return `${d.getFullYear()}-W${String(weekNum).padStart(2, "0")}`;
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
// src/cli/status.ts
|
|
1213
1891
|
function registerStatus(program2) {
|
|
1214
1892
|
program2.command("status").description("Show capture status and event counts").action(() => {
|
|
1215
1893
|
const config = readConfig();
|
|
1216
1894
|
const total = getEventCount();
|
|
1217
1895
|
const todayEvents = getEventsForDate(todayDate());
|
|
1218
1896
|
const last = getLastEvent();
|
|
1219
|
-
const isPaused =
|
|
1897
|
+
const isPaused = import_fs13.default.existsSync(PAUSE_FLAG_PATH);
|
|
1220
1898
|
log.plain("");
|
|
1221
1899
|
log.plain(import_chalk3.default.bold(" Workscribe Status"));
|
|
1222
1900
|
log.plain("");
|
|
@@ -1235,6 +1913,25 @@ function registerStatus(program2) {
|
|
|
1235
1913
|
` AI provider ${config.ai.provider ? import_chalk3.default.cyan(config.ai.provider) : import_chalk3.default.yellow("not configured")}`
|
|
1236
1914
|
);
|
|
1237
1915
|
if (config.ai.model) log.plain(` Model ${import_chalk3.default.dim(config.ai.model)}`);
|
|
1916
|
+
const activeProfiles = config.capture.profiles;
|
|
1917
|
+
log.plain(
|
|
1918
|
+
` Profiles ${activeProfiles.length > 0 ? import_chalk3.default.cyan(activeProfiles.join(", ")) : import_chalk3.default.dim("none")}`
|
|
1919
|
+
);
|
|
1920
|
+
const { total: weekTotal, otherCount } = getRecentOtherRate(7);
|
|
1921
|
+
if (weekTotal > 0) {
|
|
1922
|
+
const otherPct = Math.round(otherCount / weekTotal * 100);
|
|
1923
|
+
if (otherPct >= 25) {
|
|
1924
|
+
log.plain("");
|
|
1925
|
+
log.plain(
|
|
1926
|
+
` ${import_chalk3.default.yellow("\u26A0")} ${otherCount} of ${weekTotal} events (${otherPct}%) are uncategorised this week.`
|
|
1927
|
+
);
|
|
1928
|
+
log.plain(
|
|
1929
|
+
import_chalk3.default.dim(
|
|
1930
|
+
" Run workscribe profile detect \u2014 scan current directory for matching profiles\n Run workscribe profile suggest \u2014 analyse your command history"
|
|
1931
|
+
)
|
|
1932
|
+
);
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1238
1935
|
log.plain("");
|
|
1239
1936
|
});
|
|
1240
1937
|
}
|
|
@@ -1242,26 +1939,19 @@ function registerStatus(program2) {
|
|
|
1242
1939
|
// src/cli/events.ts
|
|
1243
1940
|
var import_chalk4 = __toESM(require("chalk"));
|
|
1244
1941
|
var CATEGORY_COLORS = {
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
build: import_chalk4.default.cyan,
|
|
1253
|
-
server_run: import_chalk4.default.cyan,
|
|
1942
|
+
shipping: import_chalk4.default.green,
|
|
1943
|
+
versioning: import_chalk4.default.blue,
|
|
1944
|
+
building: import_chalk4.default.cyan,
|
|
1945
|
+
running: import_chalk4.default.white,
|
|
1946
|
+
testing: import_chalk4.default.magenta,
|
|
1947
|
+
linting: import_chalk4.default.yellow,
|
|
1948
|
+
dependencies: import_chalk4.default.yellow,
|
|
1254
1949
|
infrastructure: import_chalk4.default.red,
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
code_stash: import_chalk4.default.blue,
|
|
1950
|
+
releasing: import_chalk4.default.bold.green,
|
|
1951
|
+
database: import_chalk4.default.bold.blue,
|
|
1952
|
+
files: import_chalk4.default.dim.yellow,
|
|
1953
|
+
exploring: import_chalk4.default.dim.cyan,
|
|
1260
1954
|
ai_session: import_chalk4.default.bold.magenta,
|
|
1261
|
-
lint_run: import_chalk4.default.yellow,
|
|
1262
|
-
code_search: import_chalk4.default.dim.cyan,
|
|
1263
|
-
code_read: import_chalk4.default.dim.white,
|
|
1264
|
-
file_ops: import_chalk4.default.yellow,
|
|
1265
1955
|
other: import_chalk4.default.gray
|
|
1266
1956
|
};
|
|
1267
1957
|
var AI_TOOL_DISPLAY_NAMES = Object.fromEntries(
|
|
@@ -1346,461 +2036,128 @@ function splitByRepo(events) {
|
|
|
1346
2036
|
groups[groups.length - 1].push(events[i]);
|
|
1347
2037
|
}
|
|
1348
2038
|
}
|
|
1349
|
-
return groups;
|
|
1350
|
-
}
|
|
1351
|
-
function minutesBetween(start, end) {
|
|
1352
|
-
return (new Date(end).getTime() - new Date(start).getTime()) / 6e4;
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
// src/sessions/detector.ts
|
|
1356
|
-
function getOrBuildSessions(date) {
|
|
1357
|
-
const existing = getSessionsForDate(date);
|
|
1358
|
-
const events = getEventsForDate(date);
|
|
1359
|
-
if (events.length === 0) return [];
|
|
1360
|
-
const cachedEventCount = existing.reduce((sum, sessionRow) => sum + sessionRow.event_count, 0);
|
|
1361
|
-
if (existing.length > 0 && cachedEventCount === events.length) {
|
|
1362
|
-
return existing.map((sessionRow) => ({
|
|
1363
|
-
id: sessionRow.id,
|
|
1364
|
-
date: sessionRow.date,
|
|
1365
|
-
startedAt: sessionRow.started_at,
|
|
1366
|
-
endedAt: sessionRow.ended_at,
|
|
1367
|
-
repoName: sessionRow.repo_name,
|
|
1368
|
-
durationMinutes: minutesBetween(sessionRow.started_at, sessionRow.ended_at),
|
|
1369
|
-
eventCount: sessionRow.event_count,
|
|
1370
|
-
summary: sessionRow.summary,
|
|
1371
|
-
events: getEventsBySessionId(sessionRow.id)
|
|
1372
|
-
}));
|
|
1373
|
-
}
|
|
1374
|
-
const savedSummaries = /* @__PURE__ */ new Map();
|
|
1375
|
-
for (const s of existing) {
|
|
1376
|
-
if (s.summary) savedSummaries.set(`${s.repo_name}|${s.started_at}`, s.summary);
|
|
1377
|
-
}
|
|
1378
|
-
clearSessionsForDate(date);
|
|
1379
|
-
return buildSessions(date, events, savedSummaries);
|
|
1380
|
-
}
|
|
1381
|
-
function buildSessions(date, events, savedSummaries = /* @__PURE__ */ new Map()) {
|
|
1382
|
-
const config = readConfig();
|
|
1383
|
-
const idleMs = config.session.idleTimeout * 60 * 1e3;
|
|
1384
|
-
const sessions = [];
|
|
1385
|
-
const timeGroups = splitByIdleGap(events, idleMs);
|
|
1386
|
-
for (const group of timeGroups) {
|
|
1387
|
-
if (group.length === 0) continue;
|
|
1388
|
-
const repoGroups = splitByRepo(group);
|
|
1389
|
-
for (const repoGroup of repoGroups) {
|
|
1390
|
-
if (repoGroup.length === 0) continue;
|
|
1391
|
-
const first = repoGroup[0];
|
|
1392
|
-
const last = repoGroup[repoGroup.length - 1];
|
|
1393
|
-
const repoName = last.repo_name ?? first.repo_name ?? null;
|
|
1394
|
-
const summary = savedSummaries.get(`${repoName}|${first.captured_at}`) ?? null;
|
|
1395
|
-
const sessionId = insertSession({
|
|
1396
|
-
date,
|
|
1397
|
-
started_at: first.captured_at,
|
|
1398
|
-
ended_at: last.captured_at,
|
|
1399
|
-
repo_name: repoName,
|
|
1400
|
-
event_count: repoGroup.length,
|
|
1401
|
-
summary
|
|
1402
|
-
});
|
|
1403
|
-
for (const event of repoGroup) {
|
|
1404
|
-
insertSessionEvent(sessionId, event.id);
|
|
1405
|
-
}
|
|
1406
|
-
sessions.push({
|
|
1407
|
-
id: sessionId,
|
|
1408
|
-
date,
|
|
1409
|
-
startedAt: first.captured_at,
|
|
1410
|
-
endedAt: last.captured_at,
|
|
1411
|
-
repoName,
|
|
1412
|
-
durationMinutes: minutesBetween(first.captured_at, last.captured_at),
|
|
1413
|
-
eventCount: repoGroup.length,
|
|
1414
|
-
summary,
|
|
1415
|
-
events: repoGroup
|
|
1416
|
-
});
|
|
1417
|
-
}
|
|
1418
|
-
}
|
|
1419
|
-
return sessions;
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
// src/cli/sessions.ts
|
|
1423
|
-
function registerSessions(program2) {
|
|
1424
|
-
program2.command("sessions").description("List detected work sessions").option("--date <YYYY-MM-DD>", "date to show sessions for (default: today)").action((opts) => {
|
|
1425
|
-
let date = todayDate();
|
|
1426
|
-
if (opts.date) {
|
|
1427
|
-
try {
|
|
1428
|
-
date = parseDate(opts.date);
|
|
1429
|
-
} catch (e) {
|
|
1430
|
-
log.error(e.message);
|
|
1431
|
-
process.exit(1);
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
const sessions = getOrBuildSessions(date);
|
|
1435
|
-
if (sessions.length === 0) {
|
|
1436
|
-
log.plain(`No sessions detected for ${date}`);
|
|
1437
|
-
return;
|
|
1438
|
-
}
|
|
1439
|
-
log.plain("");
|
|
1440
|
-
log.plain(import_chalk5.default.bold(` Sessions for ${date} (${sessions.length} session${sessions.length > 1 ? "s" : ""})`));
|
|
1441
|
-
log.plain("");
|
|
1442
|
-
for (const session of sessions) {
|
|
1443
|
-
const start = formatDate(session.startedAt);
|
|
1444
|
-
const end = formatDate(session.endedAt);
|
|
1445
|
-
const duration = formatDuration(session.durationMinutes);
|
|
1446
|
-
const repo = session.repoName ? import_chalk5.default.cyan(session.repoName) : import_chalk5.default.gray("no repo");
|
|
1447
|
-
log.plain(
|
|
1448
|
-
` ${import_chalk5.default.dim(start)} \u2013 ${import_chalk5.default.dim(end)} ${repo} ${import_chalk5.default.dim(duration)} ${import_chalk5.default.dim(session.eventCount + " events")}`
|
|
1449
|
-
);
|
|
1450
|
-
}
|
|
1451
|
-
log.plain("");
|
|
1452
|
-
});
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
// src/cli/summary.ts
|
|
1456
|
-
var import_chalk6 = __toESM(require("chalk"));
|
|
1457
|
-
var import_fs12 = __toESM(require("fs"));
|
|
1458
|
-
var import_os5 = __toESM(require("os"));
|
|
1459
|
-
var import_path8 = __toESM(require("path"));
|
|
1460
|
-
|
|
1461
|
-
// src/ai/prompt.ts
|
|
1462
|
-
var SYSTEM_PROMPT = `You write short developer work summaries. Here is an example:
|
|
1463
|
-
|
|
1464
|
-
INPUT:
|
|
1465
|
-
Project: api-service
|
|
1466
|
-
Branch: feature/auth
|
|
1467
|
-
Duration: 2h 00m
|
|
1468
|
-
Events:
|
|
1469
|
-
- branch_switch {"branch":"feature/auth"}
|
|
1470
|
-
- 2x dependency_install {"tool":"npm","packages":["bcrypt","jsonwebtoken"]}
|
|
1471
|
-
- 2x test_run {"tool":"jest"} [failed]
|
|
1472
|
-
- test_run {"tool":"jest"}
|
|
1473
|
-
- 2x file_ops {"tool":"mkdir"}
|
|
1474
|
-
- code_commit {"message":"fix: auth token validation"}
|
|
1475
|
-
- code_push {"remote":"origin","branch":"feature/auth"}
|
|
1476
|
-
|
|
1477
|
-
OUTPUT:
|
|
1478
|
-
- Switched to the authentication feature branch and added authentication dependencies (bcrypt, JWT)
|
|
1479
|
-
- Resolved failing auth tests
|
|
1480
|
-
- Committed and pushed authentication fixes
|
|
1481
|
-
|
|
1482
|
-
For dependency_install events: 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.
|
|
1483
|
-
|
|
1484
|
-
Events like code_push, build, file_ops, and code_stash provide context \u2014 fold them into the surrounding narrative rather than listing them as separate bullets. Only surface them directly when they are the main activity (e.g. a standalone build fix, or a significant restructure with no accompanying commits).
|
|
1485
|
-
|
|
1486
|
-
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.
|
|
1487
|
-
|
|
1488
|
-
Maximum 5 bullets. Output ONLY the bullet points, nothing else.`;
|
|
1489
|
-
var EXCLUDED = /* @__PURE__ */ new Set(["environment", "code_search", "code_read", "server_run", "git_other"]);
|
|
1490
|
-
function buildUserMessage(payload) {
|
|
1491
|
-
const lines = [`Date: ${payload.date}`, ""];
|
|
1492
|
-
for (const session of payload.sessions) {
|
|
1493
|
-
lines.push(`Project: ${session.project ?? "unknown"}`);
|
|
1494
|
-
if (session.branch) lines.push(`Branch: ${session.branch}`);
|
|
1495
|
-
lines.push(`Duration: ${formatDuration(session.durationMinutes)}`);
|
|
1496
|
-
lines.push("Events:");
|
|
1497
|
-
const counts = /* @__PURE__ */ new Map();
|
|
1498
|
-
for (const event of session.events) {
|
|
1499
|
-
if (EXCLUDED.has(event.category)) continue;
|
|
1500
|
-
const failed = event.exitCode !== null && event.exitCode !== 0 ? " [failed]" : "";
|
|
1501
|
-
let key;
|
|
1502
|
-
if (event.hint) {
|
|
1503
|
-
key = `other [${event.hint}]${failed}`;
|
|
1504
|
-
} else {
|
|
1505
|
-
const meta = Object.keys(event.metadata).length > 0 ? ` ${JSON.stringify(event.metadata)}` : "";
|
|
1506
|
-
key = `${event.category}${meta}${failed}`;
|
|
1507
|
-
}
|
|
1508
|
-
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
1509
|
-
}
|
|
1510
|
-
for (const [key, count] of counts) {
|
|
1511
|
-
lines.push(` - ${count > 1 ? `${count}x ` : ""}${key}`);
|
|
1512
|
-
}
|
|
1513
|
-
lines.push("");
|
|
1514
|
-
}
|
|
1515
|
-
return lines.join("\n");
|
|
1516
|
-
}
|
|
1517
|
-
var WEEKLY_SYSTEM_PROMPT = `You write short weekly developer summaries. Here is an example:
|
|
1518
|
-
|
|
1519
|
-
INPUT:
|
|
1520
|
-
Week: 2026-05-18 to 2026-05-22
|
|
1521
|
-
|
|
1522
|
-
Monday 2026-05-18:
|
|
1523
|
-
Project: api-service (feature/auth) \u2014 2h 10m
|
|
1524
|
-
Events: 3x code_commit, 2x test_run, dependency_install
|
|
1525
|
-
|
|
1526
|
-
Tuesday 2026-05-19:
|
|
1527
|
-
Project: api-service (feature/auth) \u2014 1h 30m
|
|
1528
|
-
Events: 2x code_commit, test_run, build
|
|
1529
|
-
|
|
1530
|
-
Wednesday 2026-05-20:
|
|
1531
|
-
Project: dashboard (main) \u2014 3h 00m
|
|
1532
|
-
Events: 4x code_commit, 2x build, test_run
|
|
1533
|
-
|
|
1534
|
-
Thursday 2026-05-21:
|
|
1535
|
-
Project: api-service (feature/auth) \u2014 1h 00m
|
|
1536
|
-
Events: code_commit {"message":"feat: merge auth"}, branch_switch
|
|
1537
|
-
|
|
1538
|
-
Friday 2026-05-22:
|
|
1539
|
-
Project: dashboard (main) \u2014 2h 00m
|
|
1540
|
-
Events: 3x code_commit, build
|
|
1541
|
-
|
|
1542
|
-
OUTPUT:
|
|
1543
|
-
## Week of 18 May \u2014 9h 40m across 2 projects
|
|
1544
|
-
|
|
1545
|
-
**api-service** (Mon\u2013Thu): Built and shipped the auth feature \u2014 JWT setup, token validation fixes, and final merge.
|
|
1546
|
-
**dashboard** (Wed, Fri): Active development across 7 commits with builds passing.
|
|
1547
|
-
|
|
1548
|
-
Now summarise the following week. Output ONLY the summary \u2014 a one-line header then one bullet per active project. Maximum 4 bullets.`;
|
|
1549
|
-
var DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
1550
|
-
function buildWeeklyMessage(payload) {
|
|
1551
|
-
const lines = [`Week: ${payload.weekStart} to ${payload.weekEnd}`, ""];
|
|
1552
|
-
for (const { date, sessions } of payload.days) {
|
|
1553
|
-
const dayName = DAY_NAMES[(/* @__PURE__ */ new Date(date + "T00:00:00")).getDay()];
|
|
1554
|
-
lines.push(`${dayName} ${date}:`);
|
|
1555
|
-
for (const s of sessions) {
|
|
1556
|
-
const project = s.project ?? "unknown";
|
|
1557
|
-
const branch = s.branch ? ` (${s.branch})` : "";
|
|
1558
|
-
lines.push(` Project: ${project}${branch} \u2014 ${formatDuration(s.durationMinutes)}`);
|
|
1559
|
-
const counts = /* @__PURE__ */ new Map();
|
|
1560
|
-
for (const e of s.events) {
|
|
1561
|
-
if (EXCLUDED.has(e.category)) continue;
|
|
1562
|
-
const failed = e.exitCode !== null && e.exitCode !== 0 ? " [failed]" : "";
|
|
1563
|
-
let key;
|
|
1564
|
-
if (e.hint) {
|
|
1565
|
-
key = `other [${e.hint}]${failed}`;
|
|
1566
|
-
} else {
|
|
1567
|
-
const meta = Object.keys(e.metadata).length > 0 ? ` ${JSON.stringify(e.metadata)}` : "";
|
|
1568
|
-
key = `${e.category}${meta}${failed}`;
|
|
1569
|
-
}
|
|
1570
|
-
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
1571
|
-
}
|
|
1572
|
-
const eventLine = [...counts.entries()].map(([k, n]) => `${n > 1 ? `${n}x ` : ""}${k}`).join(", ");
|
|
1573
|
-
lines.push(` Events: ${eventLine}`);
|
|
1574
|
-
}
|
|
1575
|
-
lines.push("");
|
|
1576
|
-
}
|
|
1577
|
-
return lines.join("\n");
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
// src/ai/fetch.ts
|
|
1581
|
-
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
1582
|
-
async function fetchWithTimeout(url, init, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
1583
|
-
const controller = new AbortController();
|
|
1584
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1585
|
-
try {
|
|
1586
|
-
return await fetch(url, { ...init, signal: controller.signal });
|
|
1587
|
-
} catch (err) {
|
|
1588
|
-
if (err.name === "AbortError") {
|
|
1589
|
-
throw new Error(`Request timed out after ${timeoutMs / 1e3}s`);
|
|
1590
|
-
}
|
|
1591
|
-
throw err;
|
|
1592
|
-
} finally {
|
|
1593
|
-
clearTimeout(timer);
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
|
-
|
|
1597
|
-
// src/ai/providers/anthropic.ts
|
|
1598
|
-
var AnthropicProvider = class {
|
|
1599
|
-
apiKey;
|
|
1600
|
-
model;
|
|
1601
|
-
constructor(apiKey, model) {
|
|
1602
|
-
this.apiKey = apiKey;
|
|
1603
|
-
this.model = model || "claude-haiku-4-5-20251001";
|
|
1604
|
-
}
|
|
1605
|
-
async summarize(payload) {
|
|
1606
|
-
const response = await fetchWithTimeout("https://api.anthropic.com/v1/messages", {
|
|
1607
|
-
method: "POST",
|
|
1608
|
-
headers: {
|
|
1609
|
-
"Content-Type": "application/json",
|
|
1610
|
-
"x-api-key": this.apiKey,
|
|
1611
|
-
"anthropic-version": "2023-06-01"
|
|
1612
|
-
},
|
|
1613
|
-
body: JSON.stringify({
|
|
1614
|
-
model: this.model,
|
|
1615
|
-
max_tokens: 1024,
|
|
1616
|
-
system: SYSTEM_PROMPT,
|
|
1617
|
-
messages: [{ role: "user", content: buildUserMessage(payload) }]
|
|
1618
|
-
})
|
|
1619
|
-
});
|
|
1620
|
-
if (!response.ok) {
|
|
1621
|
-
const body = await response.text();
|
|
1622
|
-
throw new Error(`Anthropic API error ${response.status}: ${body}`);
|
|
1623
|
-
}
|
|
1624
|
-
const data = await response.json();
|
|
1625
|
-
return data.content.find((block) => block.type === "text")?.text ?? "";
|
|
1626
|
-
}
|
|
1627
|
-
async summarizeWeek(payload) {
|
|
1628
|
-
const response = await fetchWithTimeout("https://api.anthropic.com/v1/messages", {
|
|
1629
|
-
method: "POST",
|
|
1630
|
-
headers: {
|
|
1631
|
-
"Content-Type": "application/json",
|
|
1632
|
-
"x-api-key": this.apiKey,
|
|
1633
|
-
"anthropic-version": "2023-06-01"
|
|
1634
|
-
},
|
|
1635
|
-
body: JSON.stringify({
|
|
1636
|
-
model: this.model,
|
|
1637
|
-
max_tokens: 1024,
|
|
1638
|
-
system: WEEKLY_SYSTEM_PROMPT,
|
|
1639
|
-
messages: [{ role: "user", content: buildWeeklyMessage(payload) }]
|
|
1640
|
-
})
|
|
1641
|
-
});
|
|
1642
|
-
if (!response.ok) {
|
|
1643
|
-
const body = await response.text();
|
|
1644
|
-
throw new Error(`Anthropic API error ${response.status}: ${body}`);
|
|
1645
|
-
}
|
|
1646
|
-
const data = await response.json();
|
|
1647
|
-
return data.content.find((block) => block.type === "text")?.text ?? "";
|
|
1648
|
-
}
|
|
1649
|
-
};
|
|
1650
|
-
|
|
1651
|
-
// src/ai/providers/openai.ts
|
|
1652
|
-
var OpenAIProvider = class {
|
|
1653
|
-
apiKey;
|
|
1654
|
-
model;
|
|
1655
|
-
baseUrl;
|
|
1656
|
-
constructor(apiKey, model, baseUrl) {
|
|
1657
|
-
this.apiKey = apiKey;
|
|
1658
|
-
this.model = model || "gpt-4o-mini";
|
|
1659
|
-
this.baseUrl = baseUrl || "https://api.openai.com/v1";
|
|
1660
|
-
}
|
|
1661
|
-
async summarize(payload) {
|
|
1662
|
-
const response = await fetchWithTimeout(`${this.baseUrl}/chat/completions`, {
|
|
1663
|
-
method: "POST",
|
|
1664
|
-
headers: {
|
|
1665
|
-
"Content-Type": "application/json",
|
|
1666
|
-
Authorization: `Bearer ${this.apiKey}`
|
|
1667
|
-
},
|
|
1668
|
-
body: JSON.stringify({
|
|
1669
|
-
model: this.model,
|
|
1670
|
-
messages: [
|
|
1671
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
1672
|
-
{ role: "user", content: buildUserMessage(payload) }
|
|
1673
|
-
],
|
|
1674
|
-
max_tokens: 1024
|
|
1675
|
-
})
|
|
1676
|
-
});
|
|
1677
|
-
if (!response.ok) {
|
|
1678
|
-
const body = await response.text();
|
|
1679
|
-
throw new Error(`OpenAI API error ${response.status}: ${body}`);
|
|
1680
|
-
}
|
|
1681
|
-
const data = await response.json();
|
|
1682
|
-
return data.choices[0]?.message?.content ?? "";
|
|
1683
|
-
}
|
|
1684
|
-
async summarizeWeek(payload) {
|
|
1685
|
-
const response = await fetchWithTimeout(`${this.baseUrl}/chat/completions`, {
|
|
1686
|
-
method: "POST",
|
|
1687
|
-
headers: {
|
|
1688
|
-
"Content-Type": "application/json",
|
|
1689
|
-
Authorization: `Bearer ${this.apiKey}`
|
|
1690
|
-
},
|
|
1691
|
-
body: JSON.stringify({
|
|
1692
|
-
model: this.model,
|
|
1693
|
-
messages: [
|
|
1694
|
-
{ role: "system", content: WEEKLY_SYSTEM_PROMPT },
|
|
1695
|
-
{ role: "user", content: buildWeeklyMessage(payload) }
|
|
1696
|
-
],
|
|
1697
|
-
max_tokens: 1024
|
|
1698
|
-
})
|
|
1699
|
-
});
|
|
1700
|
-
if (!response.ok) {
|
|
1701
|
-
const body = await response.text();
|
|
1702
|
-
throw new Error(`OpenAI API error ${response.status}: ${body}`);
|
|
1703
|
-
}
|
|
1704
|
-
const data = await response.json();
|
|
1705
|
-
return data.choices[0]?.message?.content ?? "";
|
|
1706
|
-
}
|
|
1707
|
-
};
|
|
1708
|
-
|
|
1709
|
-
// src/ai/providers/ollama.ts
|
|
1710
|
-
var OllamaProvider = class {
|
|
1711
|
-
model;
|
|
1712
|
-
baseUrl;
|
|
1713
|
-
constructor(model, baseUrl) {
|
|
1714
|
-
this.model = model || "llama3";
|
|
1715
|
-
this.baseUrl = baseUrl || "http://localhost:11434";
|
|
1716
|
-
}
|
|
1717
|
-
async summarize(payload) {
|
|
1718
|
-
const response = await fetchWithTimeout(`${this.baseUrl}/api/chat`, {
|
|
1719
|
-
method: "POST",
|
|
1720
|
-
headers: { "Content-Type": "application/json" },
|
|
1721
|
-
body: JSON.stringify({
|
|
1722
|
-
model: this.model,
|
|
1723
|
-
stream: false,
|
|
1724
|
-
messages: [
|
|
1725
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
1726
|
-
{ role: "user", content: buildUserMessage(payload) }
|
|
1727
|
-
],
|
|
1728
|
-
options: {
|
|
1729
|
-
num_predict: 300,
|
|
1730
|
-
// hard cap on output tokens — forces conciseness
|
|
1731
|
-
temperature: 0.3
|
|
1732
|
-
// lower temperature = more focused, less wandering
|
|
1733
|
-
}
|
|
1734
|
-
})
|
|
1735
|
-
});
|
|
1736
|
-
if (!response.ok) {
|
|
1737
|
-
const body = await response.text();
|
|
1738
|
-
throw new Error(`Ollama error ${response.status}: ${body}. Is Ollama running? Try: ollama serve`);
|
|
1739
|
-
}
|
|
1740
|
-
const data = await response.json();
|
|
1741
|
-
return data.message?.content ?? "";
|
|
1742
|
-
}
|
|
1743
|
-
async summarizeWeek(payload) {
|
|
1744
|
-
const response = await fetchWithTimeout(`${this.baseUrl}/api/chat`, {
|
|
1745
|
-
method: "POST",
|
|
1746
|
-
headers: { "Content-Type": "application/json" },
|
|
1747
|
-
body: JSON.stringify({
|
|
1748
|
-
model: this.model,
|
|
1749
|
-
stream: false,
|
|
1750
|
-
messages: [
|
|
1751
|
-
{ role: "system", content: WEEKLY_SYSTEM_PROMPT },
|
|
1752
|
-
{ role: "user", content: buildWeeklyMessage(payload) }
|
|
1753
|
-
],
|
|
1754
|
-
options: {
|
|
1755
|
-
num_predict: 400,
|
|
1756
|
-
temperature: 0.3
|
|
1757
|
-
}
|
|
1758
|
-
})
|
|
1759
|
-
});
|
|
1760
|
-
if (!response.ok) {
|
|
1761
|
-
const body = await response.text();
|
|
1762
|
-
throw new Error(`Ollama error ${response.status}: ${body}. Is Ollama running? Try: ollama serve`);
|
|
1763
|
-
}
|
|
1764
|
-
const data = await response.json();
|
|
1765
|
-
return data.message?.content ?? "";
|
|
1766
|
-
}
|
|
1767
|
-
};
|
|
2039
|
+
return groups;
|
|
2040
|
+
}
|
|
2041
|
+
function minutesBetween(start, end) {
|
|
2042
|
+
return (new Date(end).getTime() - new Date(start).getTime()) / 6e4;
|
|
2043
|
+
}
|
|
1768
2044
|
|
|
1769
|
-
// src/
|
|
1770
|
-
function
|
|
1771
|
-
const
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
2045
|
+
// src/sessions/detector.ts
|
|
2046
|
+
function getOrBuildSessions(date) {
|
|
2047
|
+
const existing = getSessionsForDate(date);
|
|
2048
|
+
const events = getEventsForDate(date);
|
|
2049
|
+
if (events.length === 0) return [];
|
|
2050
|
+
const cachedEventCount = existing.reduce((sum, sessionRow) => sum + sessionRow.event_count, 0);
|
|
2051
|
+
if (existing.length > 0 && cachedEventCount === events.length) {
|
|
2052
|
+
return existing.map((sessionRow) => ({
|
|
2053
|
+
id: sessionRow.id,
|
|
2054
|
+
date: sessionRow.date,
|
|
2055
|
+
startedAt: sessionRow.started_at,
|
|
2056
|
+
endedAt: sessionRow.ended_at,
|
|
2057
|
+
repoName: sessionRow.repo_name,
|
|
2058
|
+
durationMinutes: minutesBetween(sessionRow.started_at, sessionRow.ended_at),
|
|
2059
|
+
eventCount: sessionRow.event_count,
|
|
2060
|
+
summary: sessionRow.summary,
|
|
2061
|
+
events: getEventsBySessionId(sessionRow.id)
|
|
2062
|
+
}));
|
|
1776
2063
|
}
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
2064
|
+
const savedSummaries = /* @__PURE__ */ new Map();
|
|
2065
|
+
for (const s of existing) {
|
|
2066
|
+
if (s.summary) savedSummaries.set(`${s.repo_name}|${s.started_at}`, s.summary);
|
|
2067
|
+
}
|
|
2068
|
+
clearSessionsForDate(date);
|
|
2069
|
+
return buildSessions(date, events, savedSummaries);
|
|
2070
|
+
}
|
|
2071
|
+
function buildSessions(date, events, savedSummaries = /* @__PURE__ */ new Map()) {
|
|
2072
|
+
const config = readConfig();
|
|
2073
|
+
const idleMs = config.session.idleTimeout * 60 * 1e3;
|
|
2074
|
+
const sessions = [];
|
|
2075
|
+
const timeGroups = splitByIdleGap(events, idleMs);
|
|
2076
|
+
for (const group of timeGroups) {
|
|
2077
|
+
if (group.length === 0) continue;
|
|
2078
|
+
const repoGroups = splitByRepo(group);
|
|
2079
|
+
for (const repoGroup of repoGroups) {
|
|
2080
|
+
if (repoGroup.length === 0) continue;
|
|
2081
|
+
const first = repoGroup[0];
|
|
2082
|
+
const last = repoGroup[repoGroup.length - 1];
|
|
2083
|
+
const repoName = last.repo_name ?? first.repo_name ?? null;
|
|
2084
|
+
const summary = savedSummaries.get(`${repoName}|${first.captured_at}`) ?? null;
|
|
2085
|
+
const sessionId = insertSession({
|
|
2086
|
+
date,
|
|
2087
|
+
started_at: first.captured_at,
|
|
2088
|
+
ended_at: last.captured_at,
|
|
2089
|
+
repo_name: repoName,
|
|
2090
|
+
event_count: repoGroup.length,
|
|
2091
|
+
summary
|
|
2092
|
+
});
|
|
2093
|
+
for (const event of repoGroup) {
|
|
2094
|
+
insertSessionEvent(sessionId, event.id);
|
|
2095
|
+
}
|
|
2096
|
+
sessions.push({
|
|
2097
|
+
id: sessionId,
|
|
2098
|
+
date,
|
|
2099
|
+
startedAt: first.captured_at,
|
|
2100
|
+
endedAt: last.captured_at,
|
|
2101
|
+
repoName,
|
|
2102
|
+
durationMinutes: minutesBetween(first.captured_at, last.captured_at),
|
|
2103
|
+
eventCount: repoGroup.length,
|
|
2104
|
+
summary,
|
|
2105
|
+
events: repoGroup
|
|
2106
|
+
});
|
|
1785
2107
|
}
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
2108
|
+
}
|
|
2109
|
+
return sessions;
|
|
2110
|
+
}
|
|
2111
|
+
|
|
2112
|
+
// src/cli/sessions.ts
|
|
2113
|
+
function registerSessions(program2) {
|
|
2114
|
+
program2.command("sessions").description("List detected work sessions").option("--date <YYYY-MM-DD>", "date to show sessions for (default: today)").action((opts) => {
|
|
2115
|
+
let date = todayDate();
|
|
2116
|
+
if (opts.date) {
|
|
2117
|
+
try {
|
|
2118
|
+
date = parseDate(opts.date);
|
|
2119
|
+
} catch (error) {
|
|
2120
|
+
log.error(error.message);
|
|
2121
|
+
process.exit(1);
|
|
2122
|
+
}
|
|
1792
2123
|
}
|
|
1793
|
-
|
|
1794
|
-
|
|
2124
|
+
const sessions = getOrBuildSessions(date);
|
|
2125
|
+
if (sessions.length === 0) {
|
|
2126
|
+
log.plain(`No sessions detected for ${date}`);
|
|
2127
|
+
return;
|
|
1795
2128
|
}
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
2129
|
+
log.plain("");
|
|
2130
|
+
log.plain(import_chalk5.default.bold(` Sessions for ${date} (${sessions.length} session${sessions.length > 1 ? "s" : ""})`));
|
|
2131
|
+
log.plain("");
|
|
2132
|
+
for (const session of sessions) {
|
|
2133
|
+
const start = formatDate(session.startedAt);
|
|
2134
|
+
const end = formatDate(session.endedAt);
|
|
2135
|
+
const duration = formatDuration(session.durationMinutes);
|
|
2136
|
+
const repo = session.repoName ? import_chalk5.default.cyan(session.repoName) : import_chalk5.default.gray("no repo");
|
|
2137
|
+
log.plain(
|
|
2138
|
+
` ${import_chalk5.default.dim(start)} \u2013 ${import_chalk5.default.dim(end)} ${repo} ${import_chalk5.default.dim(duration)} ${import_chalk5.default.dim(session.eventCount + " events")}`
|
|
1800
2139
|
);
|
|
1801
|
-
|
|
2140
|
+
for (const event of session.events) {
|
|
2141
|
+
if (event.category === "note") {
|
|
2142
|
+
let text = "";
|
|
2143
|
+
try {
|
|
2144
|
+
text = JSON.parse(event.metadata ?? "{}").text ?? "";
|
|
2145
|
+
} catch {
|
|
2146
|
+
}
|
|
2147
|
+
if (text) log.plain(` ${import_chalk5.default.yellow("note")} ${import_chalk5.default.dim(text)}`);
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
log.plain("");
|
|
2152
|
+
});
|
|
1802
2153
|
}
|
|
1803
2154
|
|
|
2155
|
+
// src/cli/summary.ts
|
|
2156
|
+
var import_chalk6 = __toESM(require("chalk"));
|
|
2157
|
+
var import_fs14 = __toESM(require("fs"));
|
|
2158
|
+
var import_os5 = __toESM(require("os"));
|
|
2159
|
+
var import_path10 = __toESM(require("path"));
|
|
2160
|
+
|
|
1804
2161
|
// src/integrations/webhook.ts
|
|
1805
2162
|
async function postToWebhook(url, text) {
|
|
1806
2163
|
const response = await fetchWithTimeout(
|
|
@@ -1846,7 +2203,7 @@ function eventRowToPayload(row) {
|
|
|
1846
2203
|
};
|
|
1847
2204
|
if (row.category === "other" && typeof metadata.hint === "string") {
|
|
1848
2205
|
payload.hint = metadata.hint;
|
|
1849
|
-
const { hint:
|
|
2206
|
+
const { hint: _hint, tool: _tool, ...rest } = metadata;
|
|
1850
2207
|
payload.metadata = rest;
|
|
1851
2208
|
}
|
|
1852
2209
|
return payload;
|
|
@@ -1871,21 +2228,21 @@ async function handleWebhook(opts, config, summary) {
|
|
|
1871
2228
|
const body = isSlackWebhookUrl(webhookUrl) ? formatForSlack(summary, username) : summary;
|
|
1872
2229
|
await postToWebhook(webhookUrl, body);
|
|
1873
2230
|
log.success("Posted.");
|
|
1874
|
-
} catch (
|
|
1875
|
-
log.error(`Webhook failed: ${
|
|
2231
|
+
} catch (error) {
|
|
2232
|
+
log.error(`Webhook failed: ${error.message}`);
|
|
1876
2233
|
}
|
|
1877
2234
|
} else if (opts.slack && !config.integrations?.slackWebhook) {
|
|
1878
2235
|
log.warn("--slack requires integrations.slackWebhook to be set in config");
|
|
1879
2236
|
}
|
|
1880
2237
|
}
|
|
1881
2238
|
function registerSummary(program2) {
|
|
1882
|
-
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) => {
|
|
2239
|
+
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) => {
|
|
1883
2240
|
const config = readConfig();
|
|
1884
2241
|
let provider;
|
|
1885
2242
|
try {
|
|
1886
2243
|
provider = getProvider(config);
|
|
1887
|
-
} catch (
|
|
1888
|
-
log.error(
|
|
2244
|
+
} catch (error) {
|
|
2245
|
+
log.error(error.message);
|
|
1889
2246
|
process.exit(1);
|
|
1890
2247
|
}
|
|
1891
2248
|
if (opts.week) {
|
|
@@ -1904,9 +2261,9 @@ function registerSummary(program2) {
|
|
|
1904
2261
|
return;
|
|
1905
2262
|
}
|
|
1906
2263
|
const cacheKey = isoWeekKey(today);
|
|
1907
|
-
const cacheFile =
|
|
1908
|
-
if (!opts.fresh &&
|
|
1909
|
-
const cached =
|
|
2264
|
+
const cacheFile = import_path10.default.join(WEEKLY_CACHE_DIR, `${cacheKey}.md`);
|
|
2265
|
+
if (!opts.fresh && import_fs14.default.existsSync(cacheFile)) {
|
|
2266
|
+
const cached = import_fs14.default.readFileSync(cacheFile, "utf-8");
|
|
1910
2267
|
log.plain("");
|
|
1911
2268
|
log.plain(cached);
|
|
1912
2269
|
log.plain("");
|
|
@@ -1922,11 +2279,11 @@ function registerSummary(program2) {
|
|
|
1922
2279
|
log.plain("");
|
|
1923
2280
|
log.plain(summary);
|
|
1924
2281
|
log.plain("");
|
|
1925
|
-
|
|
1926
|
-
|
|
2282
|
+
import_fs14.default.mkdirSync(WEEKLY_CACHE_DIR, { recursive: true });
|
|
2283
|
+
import_fs14.default.writeFileSync(cacheFile, summary, { encoding: "utf-8", mode: 384 });
|
|
1927
2284
|
await handleWebhook(opts, config, summary);
|
|
1928
|
-
} catch (
|
|
1929
|
-
log.error(`Weekly summary failed [${config.ai.provider}]: ${
|
|
2285
|
+
} catch (error) {
|
|
2286
|
+
log.error(`Weekly summary failed [${config.ai.provider}]: ${error.message}`);
|
|
1930
2287
|
process.exit(1);
|
|
1931
2288
|
}
|
|
1932
2289
|
return;
|
|
@@ -1935,8 +2292,8 @@ function registerSummary(program2) {
|
|
|
1935
2292
|
if (opts.date) {
|
|
1936
2293
|
try {
|
|
1937
2294
|
date = parseDate(opts.date);
|
|
1938
|
-
} catch (
|
|
1939
|
-
log.error(
|
|
2295
|
+
} catch (error) {
|
|
2296
|
+
log.error(error.message);
|
|
1940
2297
|
process.exit(1);
|
|
1941
2298
|
}
|
|
1942
2299
|
}
|
|
@@ -1968,12 +2325,35 @@ function registerSummary(program2) {
|
|
|
1968
2325
|
return;
|
|
1969
2326
|
}
|
|
1970
2327
|
}
|
|
2328
|
+
const excluded = buildExcludedSet(config.summary?.includeCategories ?? []);
|
|
2329
|
+
const allSessionPayloads = sessions.map((session) => sessionToPayload(session, date));
|
|
2330
|
+
if (opts.verbose) {
|
|
2331
|
+
log.plain("");
|
|
2332
|
+
log.plain(import_chalk6.default.bold(" Captured events (--verbose):"));
|
|
2333
|
+
for (const sessionPayload of allSessionPayloads) {
|
|
2334
|
+
const branch = sessionPayload.branch ? ` (${sessionPayload.branch})` : "";
|
|
2335
|
+
log.plain(
|
|
2336
|
+
import_chalk6.default.dim(
|
|
2337
|
+
`
|
|
2338
|
+
${sessionPayload.project ?? "unknown"}${branch} \u2014 ${formatDuration(sessionPayload.durationMinutes)}`
|
|
2339
|
+
)
|
|
2340
|
+
);
|
|
2341
|
+
for (const event of sessionPayload.events) {
|
|
2342
|
+
const tag = excluded.has(event.category) ? import_chalk6.default.dim(` [excluded] ${event.category}`) : ` ${event.category}`;
|
|
2343
|
+
const hint = event.hint ? import_chalk6.default.dim(` [${event.hint}]`) : "";
|
|
2344
|
+
const meta = Object.keys(event.metadata).length > 0 ? import_chalk6.default.dim(` ${JSON.stringify(event.metadata)}`) : "";
|
|
2345
|
+
log.plain(` ${tag}${hint}${meta}`);
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
log.plain("");
|
|
2349
|
+
}
|
|
1971
2350
|
log.plain("");
|
|
1972
2351
|
log.info(`Generating summary via ${import_chalk6.default.cyan(config.ai.provider)}\u2026`);
|
|
1973
2352
|
const allSummaries = [];
|
|
1974
2353
|
const jsonSessions = [];
|
|
1975
|
-
for (
|
|
1976
|
-
const
|
|
2354
|
+
for (let index = 0; index < sessions.length; index++) {
|
|
2355
|
+
const session = sessions[index];
|
|
2356
|
+
const sessionPayload = { date, sessions: [allSessionPayloads[index]] };
|
|
1977
2357
|
try {
|
|
1978
2358
|
const bullets = await provider.summarize(sessionPayload);
|
|
1979
2359
|
const project = session.repoName ?? "unknown";
|
|
@@ -1992,8 +2372,8 @@ ${bullets.trim()}`;
|
|
|
1992
2372
|
bullets: parseBullets(bullets)
|
|
1993
2373
|
});
|
|
1994
2374
|
}
|
|
1995
|
-
} catch (
|
|
1996
|
-
log.error(`Summary failed [${config.ai.provider}]: ${
|
|
2375
|
+
} catch (error) {
|
|
2376
|
+
log.error(`Summary failed [${config.ai.provider}]: ${error.message}`);
|
|
1997
2377
|
process.exit(1);
|
|
1998
2378
|
}
|
|
1999
2379
|
}
|
|
@@ -2005,13 +2385,21 @@ ${bullets.trim()}`;
|
|
|
2005
2385
|
log.plain(fullSummary);
|
|
2006
2386
|
}
|
|
2007
2387
|
log.plain("");
|
|
2388
|
+
const excludedCounts = countExcludedEvents(allSessionPayloads, excluded);
|
|
2389
|
+
if (excludedCounts.size > 0) {
|
|
2390
|
+
const total = [...excludedCounts.values()].reduce((sum, count) => sum + count, 0);
|
|
2391
|
+
const breakdown = [...excludedCounts.entries()].map(([category, count]) => `${category} \xD7${count}`).join(", ");
|
|
2392
|
+
log.plain(import_chalk6.default.dim(` \u2139 ${total} event${total === 1 ? "" : "s"} not included in summary (${breakdown})`));
|
|
2393
|
+
log.plain(import_chalk6.default.dim(" Use --verbose to inspect \xB7 workscribe config set summary.includeCategories to opt in"));
|
|
2394
|
+
log.plain("");
|
|
2395
|
+
}
|
|
2008
2396
|
await handleWebhook(opts, config, fullSummary);
|
|
2009
2397
|
});
|
|
2010
2398
|
}
|
|
2011
2399
|
|
|
2012
2400
|
// src/cli/export.ts
|
|
2013
|
-
var
|
|
2014
|
-
var
|
|
2401
|
+
var import_fs15 = __toESM(require("fs"));
|
|
2402
|
+
var import_path11 = __toESM(require("path"));
|
|
2015
2403
|
function eventRowToPayload2(row) {
|
|
2016
2404
|
let metadata = {};
|
|
2017
2405
|
try {
|
|
@@ -2067,9 +2455,9 @@ function registerExport(program2) {
|
|
|
2067
2455
|
const payload = { date, sessions: sessionPayloads };
|
|
2068
2456
|
log.info(`Generating summary via ${config.ai.provider}\u2026`);
|
|
2069
2457
|
const summary = await provider.summarize(payload);
|
|
2070
|
-
const outputPath = opts.output ??
|
|
2071
|
-
if (!
|
|
2072
|
-
|
|
2458
|
+
const outputPath = opts.output ?? import_path11.default.join(EXPORTS_DIR, `workscribe-${date}.md`);
|
|
2459
|
+
if (!import_fs15.default.existsSync(EXPORTS_DIR)) import_fs15.default.mkdirSync(EXPORTS_DIR, { recursive: true, mode: 448 });
|
|
2460
|
+
import_fs15.default.writeFileSync(outputPath, `# Work Summary \u2014 ${date}
|
|
2073
2461
|
|
|
2074
2462
|
${summary}
|
|
2075
2463
|
`, { encoding: "utf-8", mode: 384 });
|
|
@@ -2141,22 +2529,22 @@ function printConfig(obj, indent) {
|
|
|
2141
2529
|
}
|
|
2142
2530
|
|
|
2143
2531
|
// src/cli/pause.ts
|
|
2144
|
-
var
|
|
2532
|
+
var import_fs16 = __toESM(require("fs"));
|
|
2145
2533
|
function registerPause(program2) {
|
|
2146
2534
|
program2.command("pause").description("Pause capture temporarily").action(() => {
|
|
2147
|
-
if (
|
|
2535
|
+
if (import_fs16.default.existsSync(PAUSE_FLAG_PATH)) {
|
|
2148
2536
|
log.info("Capture is already paused. Run workscribe resume to restart.");
|
|
2149
2537
|
return;
|
|
2150
2538
|
}
|
|
2151
|
-
|
|
2539
|
+
import_fs16.default.writeFileSync(PAUSE_FLAG_PATH, "", { mode: 384 });
|
|
2152
2540
|
log.success("Capture paused. Run workscribe resume to restart.");
|
|
2153
2541
|
});
|
|
2154
2542
|
program2.command("resume").description("Resume capture after a pause").action(() => {
|
|
2155
|
-
if (!
|
|
2543
|
+
if (!import_fs16.default.existsSync(PAUSE_FLAG_PATH)) {
|
|
2156
2544
|
log.info("Capture is not paused.");
|
|
2157
2545
|
return;
|
|
2158
2546
|
}
|
|
2159
|
-
|
|
2547
|
+
import_fs16.default.rmSync(PAUSE_FLAG_PATH);
|
|
2160
2548
|
log.success("Capture resumed.");
|
|
2161
2549
|
});
|
|
2162
2550
|
}
|
|
@@ -2241,18 +2629,18 @@ function registerClear(program2) {
|
|
|
2241
2629
|
}
|
|
2242
2630
|
|
|
2243
2631
|
// src/cli/uninstall.ts
|
|
2244
|
-
var
|
|
2245
|
-
var
|
|
2632
|
+
var import_fs17 = __toESM(require("fs"));
|
|
2633
|
+
var import_path12 = __toESM(require("path"));
|
|
2246
2634
|
var import_os6 = __toESM(require("os"));
|
|
2247
2635
|
var import_prompts3 = __toESM(require("prompts"));
|
|
2248
2636
|
var import_chalk9 = __toESM(require("chalk"));
|
|
2249
2637
|
function detectShellRcFiles() {
|
|
2250
2638
|
const candidates = [
|
|
2251
|
-
{ name: "zsh", rcFile:
|
|
2252
|
-
{ name: "bash", rcFile:
|
|
2253
|
-
{ name: "bash", rcFile:
|
|
2639
|
+
{ name: "zsh", rcFile: import_path12.default.join(import_os6.default.homedir(), ".zshrc") },
|
|
2640
|
+
{ name: "bash", rcFile: import_path12.default.join(import_os6.default.homedir(), ".bashrc") },
|
|
2641
|
+
{ name: "bash", rcFile: import_path12.default.join(import_os6.default.homedir(), ".bash_profile") }
|
|
2254
2642
|
];
|
|
2255
|
-
return candidates.filter((candidate) =>
|
|
2643
|
+
return candidates.filter((candidate) => import_fs17.default.existsSync(candidate.rcFile) && isHookInstalled(candidate.rcFile));
|
|
2256
2644
|
}
|
|
2257
2645
|
function registerUninstall(program2) {
|
|
2258
2646
|
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) => {
|
|
@@ -2271,7 +2659,7 @@ function registerUninstall(program2) {
|
|
|
2271
2659
|
}
|
|
2272
2660
|
}
|
|
2273
2661
|
if (opts.hooksOnly) return;
|
|
2274
|
-
if (
|
|
2662
|
+
if (import_fs17.default.existsSync(WORKSCRIBE_DIR)) {
|
|
2275
2663
|
log.plain("");
|
|
2276
2664
|
const { deleteData } = await (0, import_prompts3.default)({
|
|
2277
2665
|
type: "confirm",
|
|
@@ -2280,7 +2668,7 @@ function registerUninstall(program2) {
|
|
|
2280
2668
|
initial: false
|
|
2281
2669
|
});
|
|
2282
2670
|
if (deleteData) {
|
|
2283
|
-
|
|
2671
|
+
import_fs17.default.rmSync(WORKSCRIBE_DIR, { recursive: true, force: true });
|
|
2284
2672
|
log.success(`Deleted ${WORKSCRIBE_DIR}`);
|
|
2285
2673
|
} else {
|
|
2286
2674
|
log.info(`Data kept at ${WORKSCRIBE_DIR}`);
|
|
@@ -2292,9 +2680,306 @@ function registerUninstall(program2) {
|
|
|
2292
2680
|
});
|
|
2293
2681
|
}
|
|
2294
2682
|
|
|
2683
|
+
// src/cli/profile.ts
|
|
2684
|
+
var import_fs18 = __toESM(require("fs"));
|
|
2685
|
+
var import_path13 = __toESM(require("path"));
|
|
2686
|
+
var import_chalk10 = __toESM(require("chalk"));
|
|
2687
|
+
var MIN_SUGGEST_COUNT = 5;
|
|
2688
|
+
function registerProfile(program2) {
|
|
2689
|
+
const profile = program2.command("profile").description("Manage stack profiles for better command tracking");
|
|
2690
|
+
profile.command("list").description("List available profiles from the registry").action(async () => {
|
|
2691
|
+
const config = readConfig();
|
|
2692
|
+
const activeNames = new Set(config.capture.profiles);
|
|
2693
|
+
let manifest = readCachedManifest();
|
|
2694
|
+
let offline = false;
|
|
2695
|
+
if (!manifest) {
|
|
2696
|
+
try {
|
|
2697
|
+
manifest = await cacheManifest();
|
|
2698
|
+
} catch {
|
|
2699
|
+
offline = true;
|
|
2700
|
+
manifest = getCachedProfiles().map((p) => ({
|
|
2701
|
+
name: p.name,
|
|
2702
|
+
displayName: p.displayName,
|
|
2703
|
+
tags: [],
|
|
2704
|
+
description: "",
|
|
2705
|
+
keywords: [],
|
|
2706
|
+
detect: p.detect
|
|
2707
|
+
}));
|
|
2708
|
+
}
|
|
2709
|
+
} else {
|
|
2710
|
+
cacheManifest().catch(() => void 0);
|
|
2711
|
+
}
|
|
2712
|
+
if (offline) {
|
|
2713
|
+
log.warn("Registry unreachable \u2014 showing installed profiles only");
|
|
2714
|
+
}
|
|
2715
|
+
if (manifest.length === 0) {
|
|
2716
|
+
log.plain("No profiles available. Check your connection and try again.");
|
|
2717
|
+
return;
|
|
2718
|
+
}
|
|
2719
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
2720
|
+
for (const entry of manifest) {
|
|
2721
|
+
const tag = entry.tags[0] ?? "other";
|
|
2722
|
+
if (!grouped.has(tag)) grouped.set(tag, []);
|
|
2723
|
+
grouped.get(tag).push(entry);
|
|
2724
|
+
}
|
|
2725
|
+
log.plain("");
|
|
2726
|
+
for (const [tag, entries] of grouped) {
|
|
2727
|
+
log.plain(import_chalk10.default.bold(` ${tag.charAt(0).toUpperCase() + tag.slice(1)} profiles`));
|
|
2728
|
+
log.plain(import_chalk10.default.dim(" " + "\u2500".repeat(60)));
|
|
2729
|
+
for (const entry of entries) {
|
|
2730
|
+
const badge = activeNames.has(entry.name) ? import_chalk10.default.green(" [active]") : "";
|
|
2731
|
+
const installed = import_fs18.default.existsSync(import_path13.default.join(PROFILES_DIR, `${entry.name}.json`)) ? import_chalk10.default.dim(" installed") : "";
|
|
2732
|
+
log.plain(` ${import_chalk10.default.cyan(entry.name.padEnd(18))} ${entry.displayName.padEnd(28)}${badge}${installed}`);
|
|
2733
|
+
if (entry.description) log.plain(` ${import_chalk10.default.dim(" ".repeat(18) + entry.description)}`);
|
|
2734
|
+
}
|
|
2735
|
+
log.plain("");
|
|
2736
|
+
}
|
|
2737
|
+
});
|
|
2738
|
+
profile.command("add <names...>").description("Install one or more profiles from the registry").action(async (names) => {
|
|
2739
|
+
const config = readConfig();
|
|
2740
|
+
const activeNames = new Set(config.capture.profiles);
|
|
2741
|
+
for (const name of names) {
|
|
2742
|
+
if (activeNames.has(name)) {
|
|
2743
|
+
log.info(`Profile "${name}" is already active`);
|
|
2744
|
+
continue;
|
|
2745
|
+
}
|
|
2746
|
+
try {
|
|
2747
|
+
log.plain(`Fetching ${import_chalk10.default.cyan(name)}\u2026`);
|
|
2748
|
+
await installProfile(name);
|
|
2749
|
+
activeNames.add(name);
|
|
2750
|
+
log.success(`Profile "${name}" installed`);
|
|
2751
|
+
} catch (err) {
|
|
2752
|
+
log.error(`Failed to install "${name}": ${err.message}`);
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
config.capture.profiles = [...activeNames];
|
|
2756
|
+
writeConfig(config);
|
|
2757
|
+
});
|
|
2758
|
+
profile.command("remove <names...>").description("Uninstall one or more profiles").action((names) => {
|
|
2759
|
+
const config = readConfig();
|
|
2760
|
+
const activeNames = new Set(config.capture.profiles);
|
|
2761
|
+
for (const name of names) {
|
|
2762
|
+
if (!activeNames.has(name)) {
|
|
2763
|
+
log.warn(`Profile "${name}" is not active`);
|
|
2764
|
+
continue;
|
|
2765
|
+
}
|
|
2766
|
+
activeNames.delete(name);
|
|
2767
|
+
const cachePath = import_path13.default.join(PROFILES_DIR, `${name}.json`);
|
|
2768
|
+
if (import_fs18.default.existsSync(cachePath)) import_fs18.default.unlinkSync(cachePath);
|
|
2769
|
+
log.success(`Profile "${name}" removed`);
|
|
2770
|
+
}
|
|
2771
|
+
config.capture.profiles = [...activeNames];
|
|
2772
|
+
writeConfig(config);
|
|
2773
|
+
});
|
|
2774
|
+
profile.command("update [name]").description("Re-fetch installed profiles from the registry").action(async (name) => {
|
|
2775
|
+
const config = readConfig();
|
|
2776
|
+
const toUpdate = name ? [name] : config.capture.profiles;
|
|
2777
|
+
if (toUpdate.length === 0) {
|
|
2778
|
+
log.plain("No profiles installed. Run workscribe profile add <name> first.");
|
|
2779
|
+
return;
|
|
2780
|
+
}
|
|
2781
|
+
for (const profileName of toUpdate) {
|
|
2782
|
+
try {
|
|
2783
|
+
log.plain(`Updating ${import_chalk10.default.cyan(profileName)}\u2026`);
|
|
2784
|
+
await installProfile(profileName);
|
|
2785
|
+
log.success(`${profileName} updated`);
|
|
2786
|
+
} catch (err) {
|
|
2787
|
+
log.error(`Failed to update "${profileName}": ${err.message}`);
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
});
|
|
2791
|
+
profile.command("detect").description("Scan the current directory and suggest matching profiles").action(() => {
|
|
2792
|
+
const cwd = process.cwd();
|
|
2793
|
+
const matches = detectProfilesForDirectory(cwd);
|
|
2794
|
+
if (matches.length === 0) {
|
|
2795
|
+
const cached = readCachedManifest();
|
|
2796
|
+
if (!cached) {
|
|
2797
|
+
log.plain("No manifest cached. Run workscribe profile list to fetch it first.");
|
|
2798
|
+
} else {
|
|
2799
|
+
log.plain(`No profiles detected for ${import_chalk10.default.dim(cwd)}`);
|
|
2800
|
+
log.plain(import_chalk10.default.dim("Run workscribe profile list to browse available profiles."));
|
|
2801
|
+
}
|
|
2802
|
+
return;
|
|
2803
|
+
}
|
|
2804
|
+
log.plain("");
|
|
2805
|
+
log.plain(import_chalk10.default.bold(" Profiles detected for this directory:"));
|
|
2806
|
+
log.plain("");
|
|
2807
|
+
for (const entry of matches) {
|
|
2808
|
+
const files = entry.detect?.files?.join(", ") ?? "";
|
|
2809
|
+
log.plain(` ${import_chalk10.default.cyan(entry.name.padEnd(18))} ${entry.displayName} ${import_chalk10.default.dim("(" + files + ")")}`);
|
|
2810
|
+
}
|
|
2811
|
+
log.plain("");
|
|
2812
|
+
log.plain(` Run: ${import_chalk10.default.cyan("workscribe profile add " + matches.map((e) => e.name).join(" "))}`);
|
|
2813
|
+
log.plain("");
|
|
2814
|
+
});
|
|
2815
|
+
profile.command("suggest").description("Analyse recent unclassified commands and suggest profiles").option("--days <n>", "number of days to analyse", "30").action((opts) => {
|
|
2816
|
+
const days = Math.max(1, parseInt(opts.days, 10) || 30);
|
|
2817
|
+
const toolCounts = getOtherEventToolCounts(days);
|
|
2818
|
+
if (toolCounts.length === 0) {
|
|
2819
|
+
log.plain("No unclassified commands found in the last " + days + " days.");
|
|
2820
|
+
return;
|
|
2821
|
+
}
|
|
2822
|
+
const manifest = readCachedManifest();
|
|
2823
|
+
if (!manifest) {
|
|
2824
|
+
log.plain("No manifest cached. Run workscribe profile list first.");
|
|
2825
|
+
return;
|
|
2826
|
+
}
|
|
2827
|
+
const config = readConfig();
|
|
2828
|
+
const activeNames = new Set(config.capture.profiles);
|
|
2829
|
+
const suggestions = [];
|
|
2830
|
+
for (const entry of manifest) {
|
|
2831
|
+
if (activeNames.has(entry.name)) continue;
|
|
2832
|
+
const matchedCount = toolCounts.filter((tc) => entry.keywords.some((kw) => tc.tool.toLowerCase().includes(kw.toLowerCase()))).reduce((sum, tc) => sum + tc.count, 0);
|
|
2833
|
+
if (matchedCount >= MIN_SUGGEST_COUNT) {
|
|
2834
|
+
suggestions.push({ entry, count: matchedCount });
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
suggestions.sort((a, b) => b.count - a.count);
|
|
2838
|
+
if (suggestions.length === 0) {
|
|
2839
|
+
log.plain("No profile matches found. Your commands are well covered.");
|
|
2840
|
+
return;
|
|
2841
|
+
}
|
|
2842
|
+
log.plain("");
|
|
2843
|
+
log.plain(import_chalk10.default.bold(` Analysed last ${days} days of unclassified commands:`));
|
|
2844
|
+
log.plain("");
|
|
2845
|
+
for (const { entry, count } of suggestions) {
|
|
2846
|
+
log.plain(
|
|
2847
|
+
` ${import_chalk10.default.cyan(entry.name.padEnd(18))} ${String(count).padStart(4)} commands \u2192 ${entry.displayName}`
|
|
2848
|
+
);
|
|
2849
|
+
}
|
|
2850
|
+
log.plain("");
|
|
2851
|
+
log.plain(` Run: ${import_chalk10.default.cyan("workscribe profile add " + suggestions.map((s) => s.entry.name).join(" "))}`);
|
|
2852
|
+
log.plain("");
|
|
2853
|
+
});
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2856
|
+
// src/cli/note.ts
|
|
2857
|
+
function registerNote(program2) {
|
|
2858
|
+
program2.command("note <text>").description("Attach a free-text annotation to the current session").action((text) => {
|
|
2859
|
+
const { repoName, branch } = getGitContext(process.cwd());
|
|
2860
|
+
insertEvent({
|
|
2861
|
+
captured_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2862
|
+
command: "workscribe note",
|
|
2863
|
+
cwd: process.cwd(),
|
|
2864
|
+
exit_code: null,
|
|
2865
|
+
duration_ms: null,
|
|
2866
|
+
repo_name: repoName,
|
|
2867
|
+
branch,
|
|
2868
|
+
category: "note",
|
|
2869
|
+
metadata: JSON.stringify({ text })
|
|
2870
|
+
});
|
|
2871
|
+
log.success(`Note saved${repoName ? ` to ${repoName}` : ""}`);
|
|
2872
|
+
});
|
|
2873
|
+
}
|
|
2874
|
+
|
|
2875
|
+
// src/cli/week.ts
|
|
2876
|
+
var import_chalk11 = __toESM(require("chalk"));
|
|
2877
|
+
var import_fs19 = __toESM(require("fs"));
|
|
2878
|
+
var import_path14 = __toESM(require("path"));
|
|
2879
|
+
var import_os7 = __toESM(require("os"));
|
|
2880
|
+
function eventRowToPayload3(row) {
|
|
2881
|
+
let metadata = {};
|
|
2882
|
+
try {
|
|
2883
|
+
if (row.metadata) metadata = JSON.parse(row.metadata);
|
|
2884
|
+
} catch {
|
|
2885
|
+
}
|
|
2886
|
+
const payload = {
|
|
2887
|
+
category: row.category,
|
|
2888
|
+
metadata,
|
|
2889
|
+
exitCode: row.exit_code,
|
|
2890
|
+
durationMs: row.duration_ms
|
|
2891
|
+
};
|
|
2892
|
+
if (row.category === "other" && typeof metadata.hint === "string") {
|
|
2893
|
+
payload.hint = metadata.hint;
|
|
2894
|
+
const { hint: _hint, tool: _tool, ...rest } = metadata;
|
|
2895
|
+
payload.metadata = rest;
|
|
2896
|
+
}
|
|
2897
|
+
return payload;
|
|
2898
|
+
}
|
|
2899
|
+
function sessionToPayload2(session, date) {
|
|
2900
|
+
return {
|
|
2901
|
+
date,
|
|
2902
|
+
project: session.repoName,
|
|
2903
|
+
branch: session.events[0]?.branch ?? null,
|
|
2904
|
+
startTime: formatDate(session.startedAt),
|
|
2905
|
+
endTime: formatDate(session.endedAt),
|
|
2906
|
+
durationMinutes: session.durationMinutes,
|
|
2907
|
+
events: session.events.map(eventRowToPayload3)
|
|
2908
|
+
};
|
|
2909
|
+
}
|
|
2910
|
+
async function postSummary(opts, config, summary) {
|
|
2911
|
+
const webhookUrl = opts.webhook ?? (opts.slack ? config.integrations?.slackWebhook : void 0);
|
|
2912
|
+
if (!webhookUrl) {
|
|
2913
|
+
if (opts.slack) log.warn("--slack requires integrations.slackWebhook to be set in config");
|
|
2914
|
+
return;
|
|
2915
|
+
}
|
|
2916
|
+
try {
|
|
2917
|
+
log.info("Posting to webhook\u2026");
|
|
2918
|
+
const username = import_os7.default.userInfo().username;
|
|
2919
|
+
const body = isSlackWebhookUrl(webhookUrl) ? formatForSlack(summary, username) : summary;
|
|
2920
|
+
await postToWebhook(webhookUrl, body);
|
|
2921
|
+
log.success("Posted.");
|
|
2922
|
+
} catch (error) {
|
|
2923
|
+
log.error(`Webhook failed: ${error.message}`);
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
function registerWeek(program2) {
|
|
2927
|
+
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) => {
|
|
2928
|
+
const config = readConfig();
|
|
2929
|
+
let provider;
|
|
2930
|
+
try {
|
|
2931
|
+
provider = getProvider(config);
|
|
2932
|
+
} catch (error) {
|
|
2933
|
+
log.error(error.message);
|
|
2934
|
+
process.exit(1);
|
|
2935
|
+
}
|
|
2936
|
+
const today = todayDate();
|
|
2937
|
+
const weekStart = startOfWeek(today);
|
|
2938
|
+
const days = eachDayOfRange(weekStart, today);
|
|
2939
|
+
const weeklyDays = [];
|
|
2940
|
+
for (const day of days) {
|
|
2941
|
+
const sessions = getOrBuildSessions(day);
|
|
2942
|
+
if (sessions.length > 0) {
|
|
2943
|
+
weeklyDays.push({ date: day, sessions: sessions.map((session) => sessionToPayload2(session, day)) });
|
|
2944
|
+
}
|
|
2945
|
+
}
|
|
2946
|
+
if (weeklyDays.length === 0) {
|
|
2947
|
+
log.plain("No activity captured this week.");
|
|
2948
|
+
return;
|
|
2949
|
+
}
|
|
2950
|
+
const cacheKey = isoWeekKey(today);
|
|
2951
|
+
const cacheFile = import_path14.default.join(WEEKLY_CACHE_DIR, `${cacheKey}.md`);
|
|
2952
|
+
if (!opts.fresh && import_fs19.default.existsSync(cacheFile)) {
|
|
2953
|
+
const cached = import_fs19.default.readFileSync(cacheFile, "utf-8");
|
|
2954
|
+
log.plain("");
|
|
2955
|
+
log.plain(cached);
|
|
2956
|
+
log.plain("");
|
|
2957
|
+
log.plain(import_chalk11.default.dim(" (cached \u2014 use --fresh to regenerate)"));
|
|
2958
|
+
await postSummary(opts, config, cached);
|
|
2959
|
+
return;
|
|
2960
|
+
}
|
|
2961
|
+
const payload = { weekStart, weekEnd: today, days: weeklyDays };
|
|
2962
|
+
log.plain("");
|
|
2963
|
+
log.info(`Generating weekly digest via ${import_chalk11.default.cyan(config.ai.provider)}\u2026`);
|
|
2964
|
+
try {
|
|
2965
|
+
const summary = provider.summarizeWeek ? await provider.summarizeWeek(payload) : await provider.summarize({ date: today, sessions: weeklyDays.flatMap((day) => day.sessions) });
|
|
2966
|
+
log.plain("");
|
|
2967
|
+
log.plain(summary);
|
|
2968
|
+
log.plain("");
|
|
2969
|
+
import_fs19.default.mkdirSync(WEEKLY_CACHE_DIR, { recursive: true });
|
|
2970
|
+
import_fs19.default.writeFileSync(cacheFile, summary, { encoding: "utf-8", mode: 384 });
|
|
2971
|
+
await postSummary(opts, config, summary);
|
|
2972
|
+
} catch (error) {
|
|
2973
|
+
log.error(`Weekly digest failed [${config.ai.provider}]: ${error.message}`);
|
|
2974
|
+
process.exit(1);
|
|
2975
|
+
}
|
|
2976
|
+
});
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2295
2979
|
// src/index.ts
|
|
2980
|
+
assertNodeVersion(process.versions.node);
|
|
2296
2981
|
var program = new import_commander.Command();
|
|
2297
|
-
program.name("workscribe").description("Automatic developer work journal").version("1.
|
|
2982
|
+
program.name("workscribe").description("Automatic developer work journal").version("1.11.0");
|
|
2298
2983
|
registerCapture(program);
|
|
2299
2984
|
registerHook(program);
|
|
2300
2985
|
registerInit(program);
|
|
@@ -2308,5 +2993,8 @@ registerPause(program);
|
|
|
2308
2993
|
registerUndo(program);
|
|
2309
2994
|
registerClear(program);
|
|
2310
2995
|
registerUninstall(program);
|
|
2996
|
+
registerProfile(program);
|
|
2997
|
+
registerNote(program);
|
|
2998
|
+
registerWeek(program);
|
|
2311
2999
|
program.parse();
|
|
2312
3000
|
//# sourceMappingURL=index.js.map
|