@workscribe/cli 1.8.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +782 -301
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -27,7 +27,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
27
|
var import_commander = require("commander");
|
|
28
28
|
|
|
29
29
|
// src/capture/index.ts
|
|
30
|
-
var
|
|
30
|
+
var import_fs9 = __toESM(require("fs"));
|
|
31
31
|
|
|
32
32
|
// src/db/index.ts
|
|
33
33
|
var import_node_sqlite = require("node:sqlite");
|
|
@@ -43,6 +43,7 @@ var DEBUG_LOG_PATH = import_path.default.join(WORKSCRIBE_DIR, "debug.log");
|
|
|
43
43
|
var EXPORTS_DIR = import_path.default.join(WORKSCRIBE_DIR, "exports");
|
|
44
44
|
var PAUSE_FLAG_PATH = import_path.default.join(WORKSCRIBE_DIR, "paused");
|
|
45
45
|
var WEEKLY_CACHE_DIR = import_path.default.join(WORKSCRIBE_DIR, "weekly-cache");
|
|
46
|
+
var PROFILES_DIR = import_path.default.join(WORKSCRIBE_DIR, "profiles");
|
|
46
47
|
|
|
47
48
|
// src/db/migrations/001_initial_schema.ts
|
|
48
49
|
var initial_schema_default = {
|
|
@@ -85,8 +86,27 @@ var initial_schema_default = {
|
|
|
85
86
|
`
|
|
86
87
|
};
|
|
87
88
|
|
|
89
|
+
// src/db/migrations/002_semantic_categories.ts
|
|
90
|
+
var migration = {
|
|
91
|
+
version: 2,
|
|
92
|
+
description: "Rename granular categories to 13 universal semantic categories",
|
|
93
|
+
sql: `
|
|
94
|
+
UPDATE events SET category = 'shipping' WHERE category IN ('code_commit','code_push','code_merge');
|
|
95
|
+
UPDATE events SET category = 'versioning' WHERE category IN ('code_pull','branch_switch','code_stash','git_other');
|
|
96
|
+
UPDATE events SET category = 'building' WHERE category = 'build';
|
|
97
|
+
UPDATE events SET category = 'running' WHERE category IN ('server_run','script_run');
|
|
98
|
+
UPDATE events SET category = 'testing' WHERE category = 'test_run';
|
|
99
|
+
UPDATE events SET category = 'linting' WHERE category = 'lint_run';
|
|
100
|
+
UPDATE events SET category = 'dependencies' WHERE category IN ('dependency_install','dependency_remove');
|
|
101
|
+
UPDATE events SET category = 'infrastructure' WHERE category = 'infrastructure';
|
|
102
|
+
UPDATE events SET category = 'files' WHERE category IN ('file_ops','environment');
|
|
103
|
+
UPDATE events SET category = 'exploring' WHERE category IN ('code_search','code_read');
|
|
104
|
+
`
|
|
105
|
+
};
|
|
106
|
+
var semantic_categories_default = migration;
|
|
107
|
+
|
|
88
108
|
// src/db/migrations/index.ts
|
|
89
|
-
var MIGRATIONS = [initial_schema_default];
|
|
109
|
+
var MIGRATIONS = [initial_schema_default, semantic_categories_default];
|
|
90
110
|
|
|
91
111
|
// src/db/index.ts
|
|
92
112
|
var _db = null;
|
|
@@ -121,15 +141,15 @@ function applyMigrations(db) {
|
|
|
121
141
|
});
|
|
122
142
|
}
|
|
123
143
|
const applied = new Set(
|
|
124
|
-
db.prepare("SELECT version FROM schema_migrations").all().map((
|
|
144
|
+
db.prepare("SELECT version FROM schema_migrations").all().map((row) => row.version)
|
|
125
145
|
);
|
|
126
|
-
for (const
|
|
127
|
-
if (applied.has(
|
|
146
|
+
for (const migration2 of MIGRATIONS) {
|
|
147
|
+
if (applied.has(migration2.version)) continue;
|
|
128
148
|
db.exec("BEGIN");
|
|
129
149
|
try {
|
|
130
|
-
db.exec(
|
|
150
|
+
db.exec(migration2.sql);
|
|
131
151
|
db.prepare("INSERT INTO schema_migrations (version, applied_at) VALUES ($version, datetime('now'))").run({
|
|
132
|
-
version:
|
|
152
|
+
version: migration2.version
|
|
133
153
|
});
|
|
134
154
|
db.exec("COMMIT");
|
|
135
155
|
} catch (err) {
|
|
@@ -137,7 +157,7 @@ function applyMigrations(db) {
|
|
|
137
157
|
db.exec("ROLLBACK");
|
|
138
158
|
} catch {
|
|
139
159
|
}
|
|
140
|
-
throw new Error(`Migration ${
|
|
160
|
+
throw new Error(`Migration ${migration2.version} (${migration2.description}) failed: ${err.message}`);
|
|
141
161
|
}
|
|
142
162
|
}
|
|
143
163
|
}
|
|
@@ -208,7 +228,7 @@ function updateSessionSummary(sessionId, summary) {
|
|
|
208
228
|
function getLastEventIds(n) {
|
|
209
229
|
const db = getDb();
|
|
210
230
|
const rows = db.prepare("SELECT id FROM events ORDER BY id DESC LIMIT $n").all({ n });
|
|
211
|
-
return rows.map((
|
|
231
|
+
return rows.map((row) => row.id);
|
|
212
232
|
}
|
|
213
233
|
function getEventsByIds(ids) {
|
|
214
234
|
if (ids.length === 0) return [];
|
|
@@ -232,6 +252,28 @@ function deleteEventsForDate(date) {
|
|
|
232
252
|
const result = db.prepare(`DELETE FROM events WHERE date(captured_at, 'localtime') = $date`).run({ date });
|
|
233
253
|
return result.changes;
|
|
234
254
|
}
|
|
255
|
+
function getOtherEventToolCounts(days) {
|
|
256
|
+
const db = getDb();
|
|
257
|
+
return db.prepare(
|
|
258
|
+
`SELECT json_extract(metadata, '$.tool') as tool, COUNT(*) as count
|
|
259
|
+
FROM events
|
|
260
|
+
WHERE category = 'other'
|
|
261
|
+
AND captured_at >= datetime('now', $offset)
|
|
262
|
+
AND json_extract(metadata, '$.tool') IS NOT NULL
|
|
263
|
+
GROUP BY tool
|
|
264
|
+
ORDER BY count DESC`
|
|
265
|
+
).all({ offset: `-${days} days` });
|
|
266
|
+
}
|
|
267
|
+
function getRecentOtherRate(days) {
|
|
268
|
+
const db = getDb();
|
|
269
|
+
const row = db.prepare(
|
|
270
|
+
`SELECT COUNT(*) as total,
|
|
271
|
+
SUM(CASE WHEN category = 'other' THEN 1 ELSE 0 END) as otherCount
|
|
272
|
+
FROM events
|
|
273
|
+
WHERE captured_at >= datetime('now', $offset)`
|
|
274
|
+
).get({ offset: `-${days} days` });
|
|
275
|
+
return row;
|
|
276
|
+
}
|
|
235
277
|
function clearSessionsForDate(date) {
|
|
236
278
|
const db = getDb();
|
|
237
279
|
db.prepare(
|
|
@@ -475,6 +517,240 @@ function loadAIToolRegistry() {
|
|
|
475
517
|
return [...filteredBuiltins, ...userEntries];
|
|
476
518
|
}
|
|
477
519
|
|
|
520
|
+
// src/profiles/loader.ts
|
|
521
|
+
var import_fs8 = __toESM(require("fs"));
|
|
522
|
+
var import_path6 = __toESM(require("path"));
|
|
523
|
+
|
|
524
|
+
// src/config/index.ts
|
|
525
|
+
var import_fs6 = __toESM(require("fs"));
|
|
526
|
+
var DEFAULTS = {
|
|
527
|
+
ai: {
|
|
528
|
+
provider: "",
|
|
529
|
+
apiKey: "",
|
|
530
|
+
model: "",
|
|
531
|
+
baseUrl: ""
|
|
532
|
+
},
|
|
533
|
+
session: {
|
|
534
|
+
idleTimeout: 30
|
|
535
|
+
},
|
|
536
|
+
capture: {
|
|
537
|
+
ignore: ["cd", "ls", "ll", "la", "ls -la", "ls -l", "ls -a", "pwd", "clear", "exit", "history", "man"],
|
|
538
|
+
aiTools: true,
|
|
539
|
+
includeAiderPrompt: true,
|
|
540
|
+
profiles: []
|
|
541
|
+
},
|
|
542
|
+
redact: {
|
|
543
|
+
extra: []
|
|
544
|
+
},
|
|
545
|
+
summary: {
|
|
546
|
+
format: "markdown"
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
function readConfig() {
|
|
550
|
+
if (!import_fs6.default.existsSync(CONFIG_PATH)) return { ...DEFAULTS };
|
|
551
|
+
try {
|
|
552
|
+
const raw = import_fs6.default.readFileSync(CONFIG_PATH, "utf-8");
|
|
553
|
+
const parsed = JSON.parse(raw);
|
|
554
|
+
return deepMerge(DEFAULTS, parsed);
|
|
555
|
+
} catch {
|
|
556
|
+
return { ...DEFAULTS };
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
function writeConfig(config) {
|
|
560
|
+
import_fs6.default.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { encoding: "utf-8", mode: 384 });
|
|
561
|
+
try {
|
|
562
|
+
import_fs6.default.chmodSync(CONFIG_PATH, 384);
|
|
563
|
+
} catch {
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
function getConfigValue(config, key) {
|
|
567
|
+
const parts = key.split(".");
|
|
568
|
+
let cur = config;
|
|
569
|
+
for (const part of parts) {
|
|
570
|
+
if (cur == null || typeof cur !== "object") return void 0;
|
|
571
|
+
cur = cur[part];
|
|
572
|
+
}
|
|
573
|
+
return cur;
|
|
574
|
+
}
|
|
575
|
+
function setConfigValue(config, key, value) {
|
|
576
|
+
const parts = key.split(".");
|
|
577
|
+
const result = JSON.parse(JSON.stringify(config));
|
|
578
|
+
let cur = result;
|
|
579
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
580
|
+
const part = parts[i];
|
|
581
|
+
if (cur[part] == null || typeof cur[part] !== "object") {
|
|
582
|
+
cur[part] = {};
|
|
583
|
+
}
|
|
584
|
+
cur = cur[part];
|
|
585
|
+
}
|
|
586
|
+
const lastKey = parts[parts.length - 1];
|
|
587
|
+
if (value === "true") cur[lastKey] = true;
|
|
588
|
+
else if (value === "false") cur[lastKey] = false;
|
|
589
|
+
else if (!isNaN(Number(value))) cur[lastKey] = Number(value);
|
|
590
|
+
else cur[lastKey] = value;
|
|
591
|
+
return result;
|
|
592
|
+
}
|
|
593
|
+
function deepMerge(defaults, overrides) {
|
|
594
|
+
const result = { ...defaults };
|
|
595
|
+
for (const key of Object.keys(overrides)) {
|
|
596
|
+
const val = overrides[key];
|
|
597
|
+
if (val !== null && typeof val === "object" && !Array.isArray(val) && typeof defaults[key] === "object") {
|
|
598
|
+
result[key] = deepMerge(defaults[key], val);
|
|
599
|
+
} else if (val !== void 0) {
|
|
600
|
+
result[key] = val;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return result;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// src/profiles/schema.ts
|
|
607
|
+
function isNonEmptyString(value) {
|
|
608
|
+
return typeof value === "string" && value.length > 0;
|
|
609
|
+
}
|
|
610
|
+
function isValidRule(rule) {
|
|
611
|
+
if (!rule || typeof rule !== "object") return false;
|
|
612
|
+
const { pattern, category } = rule;
|
|
613
|
+
return isNonEmptyString(pattern) && isNonEmptyString(category);
|
|
614
|
+
}
|
|
615
|
+
function isValidProfile(candidate) {
|
|
616
|
+
if (!candidate || typeof candidate !== "object") return false;
|
|
617
|
+
const { name, displayName, version, rules } = candidate;
|
|
618
|
+
if (!isNonEmptyString(name) || !isNonEmptyString(displayName) || !isNonEmptyString(version)) return false;
|
|
619
|
+
if (!Array.isArray(rules)) return false;
|
|
620
|
+
return rules.every(isValidRule);
|
|
621
|
+
}
|
|
622
|
+
function isValidManifest(m) {
|
|
623
|
+
return Array.isArray(m) && m.every((e) => e && typeof e === "object" && typeof e.name === "string");
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// src/profiles/fetcher.ts
|
|
627
|
+
var import_fs7 = __toESM(require("fs"));
|
|
628
|
+
var import_path5 = __toESM(require("path"));
|
|
629
|
+
var REGISTRY_BASE = "https://raw.githubusercontent.com/workscribe/profiles/main";
|
|
630
|
+
function manifestCachePath() {
|
|
631
|
+
return import_path5.default.join(PROFILES_DIR, "manifest.json");
|
|
632
|
+
}
|
|
633
|
+
async function fetchManifest() {
|
|
634
|
+
const response = await fetch(`${REGISTRY_BASE}/manifest.json`);
|
|
635
|
+
if (!response.ok) throw new Error(`Registry unreachable (${response.status})`);
|
|
636
|
+
const data = await response.json();
|
|
637
|
+
if (!isValidManifest(data)) throw new Error("Manifest format unrecognised");
|
|
638
|
+
return data;
|
|
639
|
+
}
|
|
640
|
+
async function fetchProfile(name) {
|
|
641
|
+
const response = await fetch(`${REGISTRY_BASE}/${name}.json`);
|
|
642
|
+
if (response.status === 404) throw new Error(`Profile "${name}" not found in registry`);
|
|
643
|
+
if (!response.ok) throw new Error(`Registry unreachable (${response.status})`);
|
|
644
|
+
const data = await response.json();
|
|
645
|
+
if (!isValidProfile(data)) throw new Error(`Profile "${name}" failed schema validation`);
|
|
646
|
+
return data;
|
|
647
|
+
}
|
|
648
|
+
async function installProfile(name) {
|
|
649
|
+
ensureProfilesDir();
|
|
650
|
+
const profile = await fetchProfile(name);
|
|
651
|
+
const dest = import_path5.default.join(PROFILES_DIR, `${name}.json`);
|
|
652
|
+
import_fs7.default.writeFileSync(dest, JSON.stringify(profile, null, 2), { encoding: "utf-8", mode: 384 });
|
|
653
|
+
debugLog("profiles", `Installed profile "${name}" to ${dest}`);
|
|
654
|
+
}
|
|
655
|
+
async function cacheManifest() {
|
|
656
|
+
ensureProfilesDir();
|
|
657
|
+
const manifest = await fetchManifest();
|
|
658
|
+
import_fs7.default.writeFileSync(manifestCachePath(), JSON.stringify(manifest, null, 2), { encoding: "utf-8", mode: 384 });
|
|
659
|
+
return manifest;
|
|
660
|
+
}
|
|
661
|
+
function readCachedManifest() {
|
|
662
|
+
if (!import_fs7.default.existsSync(manifestCachePath())) return null;
|
|
663
|
+
try {
|
|
664
|
+
const data = JSON.parse(import_fs7.default.readFileSync(manifestCachePath(), "utf-8"));
|
|
665
|
+
return isValidManifest(data) ? data : null;
|
|
666
|
+
} catch {
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
function ensureProfilesDir() {
|
|
671
|
+
if (!import_fs7.default.existsSync(PROFILES_DIR)) {
|
|
672
|
+
import_fs7.default.mkdirSync(PROFILES_DIR, { recursive: true, mode: 448 });
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// src/profiles/loader.ts
|
|
677
|
+
function compileRule(profileName, rule) {
|
|
678
|
+
let regex;
|
|
679
|
+
try {
|
|
680
|
+
regex = new RegExp(rule.pattern);
|
|
681
|
+
} catch {
|
|
682
|
+
debugLog("profiles", `Profile "${profileName}": invalid pattern "${rule.pattern}" \u2014 skipping rule`);
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
685
|
+
return {
|
|
686
|
+
match(cmd) {
|
|
687
|
+
const match = cmd.match(regex);
|
|
688
|
+
if (!match) return null;
|
|
689
|
+
const metadata = {};
|
|
690
|
+
for (const [key, value] of Object.entries(rule.metadata ?? {})) {
|
|
691
|
+
metadata[key] = value.replace(/\$(\d+)/g, (_, index) => match[Number(index)] ?? "");
|
|
692
|
+
}
|
|
693
|
+
return { category: rule.category, metadata };
|
|
694
|
+
}
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
function loadProfileFromDisk(name) {
|
|
698
|
+
const filePath = import_path6.default.join(PROFILES_DIR, `${name}.json`);
|
|
699
|
+
if (!import_fs8.default.existsSync(filePath)) {
|
|
700
|
+
debugLog("profiles", `Profile "${name}" not found in cache \u2014 run: workscribe profile add ${name}`);
|
|
701
|
+
return null;
|
|
702
|
+
}
|
|
703
|
+
let candidate;
|
|
704
|
+
try {
|
|
705
|
+
candidate = JSON.parse(import_fs8.default.readFileSync(filePath, "utf-8"));
|
|
706
|
+
} catch {
|
|
707
|
+
debugLog("profiles", `Profile "${name}" contains invalid JSON \u2014 skipping`);
|
|
708
|
+
return null;
|
|
709
|
+
}
|
|
710
|
+
if (!isValidProfile(candidate)) {
|
|
711
|
+
debugLog("profiles", `Profile "${name}" failed schema validation \u2014 skipping`);
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
return candidate;
|
|
715
|
+
}
|
|
716
|
+
function getActiveProfileRules() {
|
|
717
|
+
const activeNames = readConfig().capture.profiles;
|
|
718
|
+
if (activeNames.length === 0) return [];
|
|
719
|
+
return activeNames.flatMap((name) => {
|
|
720
|
+
const profile = loadProfileFromDisk(name);
|
|
721
|
+
if (!profile) return [];
|
|
722
|
+
return profile.rules.flatMap((rule) => {
|
|
723
|
+
const compiled = compileRule(profile.name, rule);
|
|
724
|
+
return compiled ? [compiled] : [];
|
|
725
|
+
});
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
function getActiveProfileAiContexts() {
|
|
729
|
+
const activeNames = readConfig().capture.profiles;
|
|
730
|
+
if (activeNames.length === 0) return [];
|
|
731
|
+
return activeNames.flatMap((name) => {
|
|
732
|
+
const profile = loadProfileFromDisk(name);
|
|
733
|
+
return profile?.aiContext ? [profile.aiContext] : [];
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
function getCachedProfiles() {
|
|
737
|
+
if (!import_fs8.default.existsSync(PROFILES_DIR)) return [];
|
|
738
|
+
return import_fs8.default.readdirSync(PROFILES_DIR).filter((file) => file.endsWith(".json") && file !== "manifest.json").flatMap((file) => {
|
|
739
|
+
const profile = loadProfileFromDisk(file.replace(/\.json$/, ""));
|
|
740
|
+
return profile ? [profile] : [];
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
function detectProfilesForDirectory(dir) {
|
|
744
|
+
const manifest = readCachedManifest();
|
|
745
|
+
if (!manifest) return [];
|
|
746
|
+
const activeNames = new Set(readConfig().capture.profiles);
|
|
747
|
+
return manifest.filter((entry) => {
|
|
748
|
+
if (activeNames.has(entry.name)) return false;
|
|
749
|
+
const indicatorFiles = entry.detect?.files ?? [];
|
|
750
|
+
return indicatorFiles.length > 0 && indicatorFiles.every((file) => import_fs8.default.existsSync(import_path6.default.join(dir, file)));
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
|
|
478
754
|
// src/normalize/patterns.ts
|
|
479
755
|
function makeRule(pattern, category, extract = () => ({})) {
|
|
480
756
|
return {
|
|
@@ -488,116 +764,133 @@ function makeRule(pattern, category, extract = () => ({})) {
|
|
|
488
764
|
function extractPackages(cmd, afterKeyword) {
|
|
489
765
|
const idx = cmd.indexOf(afterKeyword);
|
|
490
766
|
if (idx === -1) return [];
|
|
491
|
-
return cmd.slice(idx + afterKeyword.length).trim().split(/\s+/).filter((
|
|
767
|
+
return cmd.slice(idx + afterKeyword.length).trim().split(/\s+/).filter((part) => part && !part.startsWith("-"));
|
|
492
768
|
}
|
|
493
|
-
var
|
|
769
|
+
var BASE_RULES = [
|
|
494
770
|
// ─── Git operations ───────────────────────────────────────────────────────
|
|
495
|
-
makeRule(/^git\s+commit\b/, "
|
|
771
|
+
makeRule(/^git\s+commit\b/, "shipping", (_m, cmd) => {
|
|
496
772
|
const msgMatch = cmd.match(/-m\s+["']?([^"']+)["']?/);
|
|
497
773
|
return { message: msgMatch ? msgMatch[1].trim() : null };
|
|
498
774
|
}),
|
|
499
|
-
makeRule(/^git\s+push\b/, "
|
|
775
|
+
makeRule(/^git\s+push\b/, "shipping", (_m, cmd) => {
|
|
500
776
|
const parts = cmd.split(/\s+/);
|
|
501
777
|
return { remote: parts[2] ?? "origin", branch: parts[3] ?? null };
|
|
502
778
|
}),
|
|
503
|
-
makeRule(/^git\s+(pull|fetch)\b/, "
|
|
779
|
+
makeRule(/^git\s+(pull|fetch)\b/, "versioning", (_m, cmd) => {
|
|
504
780
|
const parts = cmd.split(/\s+/);
|
|
505
781
|
return { subcommand: parts[1], remote: parts[2] ?? "origin" };
|
|
506
782
|
}),
|
|
507
|
-
makeRule(/^git\s+(checkout|switch)\b/, "
|
|
783
|
+
makeRule(/^git\s+(checkout|switch)\b/, "versioning", (_m, cmd) => {
|
|
508
784
|
const parts = cmd.split(/\s+/);
|
|
509
|
-
const branch = parts.find((
|
|
785
|
+
const branch = parts.find((part) => !part.startsWith("-") && part !== "git" && part !== parts[1]);
|
|
510
786
|
return { branch: branch ?? null };
|
|
511
787
|
}),
|
|
512
|
-
makeRule(/^git\s+(merge|rebase)\b/, "
|
|
788
|
+
makeRule(/^git\s+(merge|rebase)\b/, "shipping", (_m, cmd) => {
|
|
513
789
|
const parts = cmd.split(/\s+/);
|
|
514
790
|
return { subcommand: parts[1], target: parts[2] ?? null };
|
|
515
791
|
}),
|
|
516
|
-
makeRule(/^git\s+stash\b/, "
|
|
792
|
+
makeRule(/^git\s+stash\b/, "versioning", (_m, cmd) => {
|
|
517
793
|
const parts = cmd.split(/\s+/);
|
|
518
794
|
return { subcommand: parts[2] ?? "push" };
|
|
519
795
|
}),
|
|
520
|
-
makeRule(/^git\s+/, "
|
|
796
|
+
makeRule(/^git\s+/, "versioning", (_m, cmd) => {
|
|
521
797
|
const parts = cmd.split(/\s+/);
|
|
522
798
|
return { subcommand: parts[1] ?? null };
|
|
523
799
|
}),
|
|
524
|
-
// ───
|
|
525
|
-
makeRule(/^npm\s+
|
|
800
|
+
// ─── Releasing ────────────────────────────────────────────────────────────
|
|
801
|
+
makeRule(/^npm\s+publish\b/, "releasing", () => ({ tool: "npm" })),
|
|
802
|
+
makeRule(/^cargo\s+publish\b/, "releasing", () => ({ tool: "cargo" })),
|
|
803
|
+
makeRule(/^gem\s+push\b/, "releasing", () => ({ tool: "gem" })),
|
|
804
|
+
makeRule(/^docker\s+push\b/, "releasing", (_m, cmd) => ({ tool: "docker", image: cmd.split(/\s+/)[2] ?? null })),
|
|
805
|
+
makeRule(/^gh\s+release\b/, "releasing", (_m, cmd) => ({ tool: "gh", subcommand: cmd.split(/\s+/)[2] ?? null })),
|
|
806
|
+
makeRule(/^fly\s+(deploy|launch)\b/, "releasing", (match) => ({ tool: "fly", subcommand: match[1] })),
|
|
807
|
+
makeRule(/^vercel\s+(deploy|build|--prod)\b/, "releasing", () => ({ tool: "vercel" })),
|
|
808
|
+
makeRule(/^netlify\s+deploy\b/, "releasing", () => ({ tool: "netlify" })),
|
|
809
|
+
makeRule(/^railway\s+up\b/, "releasing", () => ({ tool: "railway" })),
|
|
810
|
+
makeRule(/^wrangler\s+deploy\b/, "releasing", () => ({ tool: "wrangler" })),
|
|
811
|
+
// ─── Dependencies ─────────────────────────────────────────────────────────
|
|
812
|
+
makeRule(/^npm\s+(install|i|add)\b/, "dependencies", (_m, cmd) => ({
|
|
526
813
|
tool: "npm",
|
|
527
814
|
packages: extractPackages(cmd, cmd.includes("install") ? "install" : cmd.includes(" i ") ? " i " : "add")
|
|
528
815
|
})),
|
|
529
|
-
makeRule(/^(yarn|pnpm)\s+(install|add)\b/, "
|
|
816
|
+
makeRule(/^(yarn|pnpm)\s+(install|add)\b/, "dependencies", (m, cmd) => ({
|
|
530
817
|
tool: m[1],
|
|
531
818
|
packages: extractPackages(cmd, "add")
|
|
532
819
|
})),
|
|
533
|
-
makeRule(/^bun\s+(install|add)\b/, "
|
|
820
|
+
makeRule(/^bun\s+(install|add)\b/, "dependencies", (_m, cmd) => ({
|
|
534
821
|
tool: "bun",
|
|
535
822
|
packages: extractPackages(cmd, "add")
|
|
536
823
|
})),
|
|
537
|
-
makeRule(/^pip\s+(install)\b/, "
|
|
824
|
+
makeRule(/^pip\s+(install)\b/, "dependencies", (_m, cmd) => ({
|
|
538
825
|
tool: "pip",
|
|
539
826
|
packages: extractPackages(cmd, "install")
|
|
540
827
|
})),
|
|
541
|
-
makeRule(/^cargo\s+add\b/, "
|
|
828
|
+
makeRule(/^cargo\s+add\b/, "dependencies", (_m, cmd) => ({
|
|
542
829
|
tool: "cargo",
|
|
543
830
|
packages: extractPackages(cmd, "add")
|
|
544
831
|
})),
|
|
545
|
-
makeRule(/^(brew)\s+install\b/, "
|
|
832
|
+
makeRule(/^(brew)\s+install\b/, "dependencies", (_m, cmd) => ({
|
|
546
833
|
tool: "brew",
|
|
547
834
|
packages: extractPackages(cmd, "install")
|
|
548
835
|
})),
|
|
549
|
-
makeRule(/^go\s+get\b/, "
|
|
836
|
+
makeRule(/^go\s+get\b/, "dependencies", (_m, cmd) => ({
|
|
550
837
|
tool: "go",
|
|
551
838
|
packages: extractPackages(cmd, "get")
|
|
552
839
|
})),
|
|
553
|
-
|
|
554
|
-
makeRule(/^npm\s+(uninstall|remove|rm)\b/, "dependency_remove", (_m, cmd) => ({
|
|
840
|
+
makeRule(/^npm\s+(uninstall|remove|rm)\b/, "dependencies", (_m, cmd) => ({
|
|
555
841
|
tool: "npm",
|
|
556
842
|
packages: extractPackages(cmd, cmd.split(/\s+/)[1])
|
|
557
843
|
})),
|
|
558
|
-
makeRule(/^(yarn|pnpm)\s+remove\b/, "
|
|
844
|
+
makeRule(/^(yarn|pnpm)\s+remove\b/, "dependencies", (m, cmd) => ({
|
|
559
845
|
tool: m[1],
|
|
560
846
|
packages: extractPackages(cmd, "remove")
|
|
561
847
|
})),
|
|
562
|
-
makeRule(/^bun\s+remove\b/, "
|
|
848
|
+
makeRule(/^bun\s+remove\b/, "dependencies", (_m, cmd) => ({
|
|
563
849
|
tool: "bun",
|
|
564
850
|
packages: extractPackages(cmd, "remove")
|
|
565
851
|
})),
|
|
566
|
-
makeRule(/^pip\s+uninstall\b/, "
|
|
852
|
+
makeRule(/^pip\s+uninstall\b/, "dependencies", (_m, cmd) => ({
|
|
567
853
|
tool: "pip",
|
|
568
854
|
packages: extractPackages(cmd, "uninstall")
|
|
569
855
|
})),
|
|
856
|
+
// Runtime version managers
|
|
857
|
+
makeRule(/^nvm\b/, "dependencies", (_m, cmd) => ({ tool: "nvm", subcommand: cmd.split(/\s+/)[1] ?? null })),
|
|
858
|
+
makeRule(/^(fnm|volta)\b/, "dependencies", (m, cmd) => ({ tool: m[1], subcommand: cmd.split(/\s+/)[1] ?? null })),
|
|
859
|
+
makeRule(/^(rbenv|pyenv|asdf)\b/, "dependencies", (m, cmd) => ({
|
|
860
|
+
tool: m[1],
|
|
861
|
+
subcommand: cmd.split(/\s+/)[1] ?? null
|
|
862
|
+
})),
|
|
570
863
|
// ─── 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/, "
|
|
864
|
+
makeRule(/^npm\s+(test|run\s+test)\b/, "testing", () => ({ tool: "npm" })),
|
|
865
|
+
makeRule(/^yarn\s+test\b/, "testing", () => ({ tool: "yarn" })),
|
|
866
|
+
makeRule(/^pnpm\s+test\b/, "testing", () => ({ tool: "pnpm" })),
|
|
867
|
+
makeRule(/^bun\s+test\b/, "testing", () => ({ tool: "bun" })),
|
|
868
|
+
makeRule(/^(jest|vitest)\b/, "testing", (match) => ({ tool: match[1] })),
|
|
869
|
+
makeRule(/^pytest\b/, "testing", () => ({ tool: "pytest" })),
|
|
870
|
+
makeRule(/^go\s+test\b/, "testing", () => ({ tool: "go" })),
|
|
871
|
+
makeRule(/^cargo\s+test\b/, "testing", () => ({ tool: "cargo" })),
|
|
872
|
+
makeRule(/^rspec\b/, "testing", () => ({ tool: "rspec" })),
|
|
873
|
+
makeRule(/^(php\s+)?phpunit\b/, "testing", () => ({ tool: "phpunit" })),
|
|
581
874
|
// ─── 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/, "
|
|
875
|
+
makeRule(/^npm\s+run\s+build\b/, "building", () => ({ tool: "npm" })),
|
|
876
|
+
makeRule(/^(yarn|pnpm)\s+build\b/, "building", (match) => ({ tool: match[1] })),
|
|
877
|
+
makeRule(/^bun\s+run\s+build\b/, "building", () => ({ tool: "bun" })),
|
|
878
|
+
makeRule(/^(make|gmake)\b/, "building", (match) => ({ tool: match[1] })),
|
|
879
|
+
makeRule(/^cargo\s+build\b/, "building", () => ({ tool: "cargo" })),
|
|
880
|
+
makeRule(/^go\s+build\b/, "building", () => ({ tool: "go" })),
|
|
881
|
+
makeRule(/^docker\s+build\b/, "building", () => ({ tool: "docker" })),
|
|
882
|
+
makeRule(/^(mvn|gradle)\b/, "building", (match) => ({ tool: match[1] })),
|
|
883
|
+
// ─── Dev server / running ─────────────────────────────────────────────────
|
|
884
|
+
makeRule(/^npm\s+run\s+(dev|start|serve)\b/, "running", (_m, cmd) => ({
|
|
592
885
|
tool: "npm",
|
|
593
886
|
script: cmd.split(/\s+/)[2]
|
|
594
887
|
})),
|
|
595
|
-
makeRule(/^(yarn|pnpm)\s+(dev|start)\b/, "
|
|
596
|
-
makeRule(/^bun\s+run\s+(dev|start|serve)\b/, "
|
|
888
|
+
makeRule(/^(yarn|pnpm)\s+(dev|start)\b/, "running", (match) => ({ tool: match[1], script: match[2] })),
|
|
889
|
+
makeRule(/^bun\s+run\s+(dev|start|serve)\b/, "running", (_m, cmd) => ({
|
|
597
890
|
tool: "bun",
|
|
598
891
|
script: cmd.split(/\s+/)[2]
|
|
599
892
|
})),
|
|
600
|
-
makeRule(/^python\s+.*manage\.py\s+runserver\b/, "
|
|
893
|
+
makeRule(/^python\s+.*manage\.py\s+runserver\b/, "running", () => ({ tool: "django" })),
|
|
601
894
|
// ─── Infrastructure ───────────────────────────────────────────────────────
|
|
602
895
|
makeRule(/^ssh\b/, "infrastructure", (_m, cmd) => {
|
|
603
896
|
const parts = cmd.split(/\s+/);
|
|
@@ -611,7 +904,7 @@ var RULES = [
|
|
|
611
904
|
const parts = cmd.split(/\s+/);
|
|
612
905
|
return { tool: m[1], subcommand: parts[1] ?? null };
|
|
613
906
|
}),
|
|
614
|
-
makeRule(/^docker\s+(run|compose|ps|stop|start|rm|exec)\b/, "infrastructure", (
|
|
907
|
+
makeRule(/^docker\s+(run|compose|ps|stop|start|rm|exec)\b/, "infrastructure", (_m, cmd) => {
|
|
615
908
|
const parts = cmd.split(/\s+/);
|
|
616
909
|
return { tool: "docker", subcommand: parts[1] };
|
|
617
910
|
}),
|
|
@@ -619,62 +912,76 @@ var RULES = [
|
|
|
619
912
|
const parts = cmd.split(/\s+/);
|
|
620
913
|
return { tool: m[1], subcommand: parts[1] ?? null };
|
|
621
914
|
}),
|
|
915
|
+
// ─── Database ─────────────────────────────────────────────────────────────
|
|
916
|
+
makeRule(/^(psql|pgcli)\b/, "database", (m) => ({ tool: m[1] })),
|
|
917
|
+
makeRule(/^(mysql|mycli)\b/, "database", (m) => ({ tool: m[1] })),
|
|
918
|
+
makeRule(/^sqlite3\b/, "database", () => ({ tool: "sqlite3" })),
|
|
919
|
+
makeRule(/^(mongosh|mongo)\b/, "database", (m) => ({ tool: m[1] })),
|
|
920
|
+
makeRule(/^redis-cli\b/, "database", () => ({ tool: "redis-cli" })),
|
|
622
921
|
// ─── npx ─────────────────────────────────────────────────────────────────
|
|
623
|
-
makeRule(/^npx\s+(jest|vitest|mocha|ava|tap)\b/, "
|
|
922
|
+
makeRule(/^npx\s+(jest|vitest|mocha|ava|tap)\b/, "testing", (_m, cmd) => ({
|
|
624
923
|
tool: cmd.split(/\s+/)[1]
|
|
625
924
|
})),
|
|
626
|
-
makeRule(/^npx\s+tsc\b/, "
|
|
627
|
-
makeRule(/^npx\s+(eslint|tslint|biome)\b/, "
|
|
925
|
+
makeRule(/^npx\s+tsc\b/, "building", () => ({ tool: "tsc" })),
|
|
926
|
+
makeRule(/^npx\s+(eslint|tslint|biome)\b/, "linting", (_m, cmd) => ({
|
|
628
927
|
tool: cmd.split(/\s+/)[1]
|
|
629
928
|
})),
|
|
630
|
-
makeRule(/^npx\s+(prettier|dprint)\b/, "
|
|
929
|
+
makeRule(/^npx\s+(prettier|dprint)\b/, "linting", (_m, cmd) => ({
|
|
631
930
|
tool: cmd.split(/\s+/)[1]
|
|
632
931
|
})),
|
|
633
|
-
makeRule(/^npx\s+\S+/, "
|
|
932
|
+
makeRule(/^npx\s+\S+/, "running", (_m, cmd) => ({
|
|
634
933
|
tool: cmd.split(/\s+/)[1]
|
|
635
934
|
})),
|
|
636
935
|
// ─── 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", () => ({})),
|
|
936
|
+
makeRule(/^(bash|sh|zsh|fish)\s+\S+/, "running", (match) => ({ shell: match[1] })),
|
|
937
|
+
makeRule(/^node\s+\S+\.(?:js|mjs|cjs)\b/, "running", () => ({ runtime: "node" })),
|
|
938
|
+
makeRule(/^python3?\s+\S+\.py\b/, "running", () => ({ runtime: "python" })),
|
|
939
|
+
// ─── Linting / code quality ───────────────────────────────────────────────
|
|
940
|
+
makeRule(/^(eslint|tslint|biome)\b/, "linting", (match) => ({ tool: match[1] })),
|
|
941
|
+
makeRule(/^(prettier|dprint)\b/, "linting", (match) => ({ tool: match[1] })),
|
|
646
942
|
// ─── generic run scripts ──────────────────────────────────────────────────
|
|
647
|
-
makeRule(/^npm\s+run\s+\S+/, "
|
|
943
|
+
makeRule(/^npm\s+run\s+\S+/, "running", (_m, cmd) => ({
|
|
648
944
|
tool: "npm",
|
|
649
945
|
script: cmd.split(/\s+/)[2]
|
|
650
946
|
})),
|
|
651
|
-
makeRule(/^bun\s+run\s+\S+/, "
|
|
947
|
+
makeRule(/^bun\s+run\s+\S+/, "running", (_m, cmd) => ({
|
|
652
948
|
tool: "bun",
|
|
653
949
|
script: cmd.split(/\s+/)[2]
|
|
654
950
|
})),
|
|
655
951
|
// ─── Code search ─────────────────────────────────────────────────────────
|
|
656
|
-
makeRule(/^(grep|egrep|fgrep)\b/, "
|
|
657
|
-
makeRule(/^(rg|ag|ack|ripgrep)\b/, "
|
|
658
|
-
makeRule(/^find\s/, "
|
|
952
|
+
makeRule(/^(grep|egrep|fgrep)\b/, "exploring", (match) => ({ tool: match[1] })),
|
|
953
|
+
makeRule(/^(rg|ag|ack|ripgrep)\b/, "exploring", (match) => ({ tool: match[1] })),
|
|
954
|
+
makeRule(/^find\s/, "exploring", () => ({ tool: "find" })),
|
|
659
955
|
// ─── Code read ────────────────────────────────────────────────────────────
|
|
660
|
-
makeRule(/^(cat|bat|less|more)\s+\S+/, "
|
|
661
|
-
makeRule(/^(head|tail)\b/, "
|
|
956
|
+
makeRule(/^(cat|bat|less|more)\s+\S+/, "exploring", (match) => ({ tool: match[1] })),
|
|
957
|
+
makeRule(/^(head|tail)\b/, "exploring", (match) => ({ tool: match[1] })),
|
|
662
958
|
// ─── 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
|
-
|
|
959
|
+
makeRule(/^(cp|rsync)\b/, "files", (match) => ({ tool: match[1] })),
|
|
960
|
+
makeRule(/^(mv)\b/, "files", () => ({ tool: "mv" })),
|
|
961
|
+
makeRule(/^(rm|rmdir)\b/, "files", (match) => ({ tool: match[1] })),
|
|
962
|
+
makeRule(/^(mkdir|touch|ln)\b/, "files", (match) => ({ tool: match[1] })),
|
|
963
|
+
makeRule(/^(chmod|chown|chgrp)\b/, "files", (match) => ({ tool: match[1] })),
|
|
964
|
+
// ─── Environment ──────────────────────────────────────────────────────────
|
|
965
|
+
makeRule(/^export\s+\w+=/, "files", (_m, cmd) => {
|
|
966
|
+
const varName = cmd.match(/^export\s+(\w+)=/)?.[1] ?? null;
|
|
967
|
+
return { variable: varName };
|
|
968
|
+
}),
|
|
969
|
+
makeRule(/^source\s+/, "files", () => ({}))
|
|
970
|
+
];
|
|
971
|
+
function aiToolRules() {
|
|
972
|
+
return loadAIToolRegistry().flatMap((entry) => {
|
|
670
973
|
try {
|
|
671
974
|
return [makeRule(new RegExp(entry.pattern), "ai_session", () => ({ tool: entry.tool }))];
|
|
672
975
|
} catch {
|
|
673
976
|
debugLog("registry", `Skipping tool "${entry.tool}" \u2014 invalid pattern: ${entry.pattern}`);
|
|
674
977
|
return [];
|
|
675
978
|
}
|
|
676
|
-
})
|
|
677
|
-
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
function buildRules(profileRules = []) {
|
|
982
|
+
return [...BASE_RULES, ...profileRules, ...aiToolRules()];
|
|
983
|
+
}
|
|
984
|
+
var RULES = buildRules(getActiveProfileRules());
|
|
678
985
|
function normalizeCommand(command) {
|
|
679
986
|
let trimmed = command.trim().replace(/\s+/g, " ");
|
|
680
987
|
trimmed = trimmed.replace(/^(git)\s+-C\s+\S+\s+/, "$1 ");
|
|
@@ -685,87 +992,6 @@ function normalizeCommand(command) {
|
|
|
685
992
|
return { category: "other", metadata: {} };
|
|
686
993
|
}
|
|
687
994
|
|
|
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
995
|
// src/capture/filter.ts
|
|
770
996
|
function normalizeForFilter(cmd) {
|
|
771
997
|
return cmd.trim().replace(/\s+/g, " ");
|
|
@@ -789,7 +1015,7 @@ function isDuplicate(command, cwd, lastEvent, dedupWindowMs = 5e3, now = Date.no
|
|
|
789
1015
|
|
|
790
1016
|
// src/capture/index.ts
|
|
791
1017
|
function captureCommand(opts) {
|
|
792
|
-
if (
|
|
1018
|
+
if (import_fs9.default.existsSync(PAUSE_FLAG_PATH)) return;
|
|
793
1019
|
const config = readConfig();
|
|
794
1020
|
const { command, cwd, exitCode, durationMs } = opts;
|
|
795
1021
|
if (shouldFilter(command, config.capture.ignore)) return;
|
|
@@ -800,6 +1026,11 @@ function captureCommand(opts) {
|
|
|
800
1026
|
const { repoName, branch } = getGitContext(cwd, nameOverride);
|
|
801
1027
|
const { category, metadata } = normalizeCommand(safeCommand);
|
|
802
1028
|
if (category === "ai_session" && config.capture.aiTools === false) return;
|
|
1029
|
+
if (category === "other") {
|
|
1030
|
+
const firstToken = safeCommand.trim().split(/\s+/)[0];
|
|
1031
|
+
if (firstToken) metadata.tool = firstToken;
|
|
1032
|
+
metadata.hint = safeCommand.trim().slice(0, 120);
|
|
1033
|
+
}
|
|
803
1034
|
if (opts.source) metadata.source = opts.source;
|
|
804
1035
|
if (category === "ai_session") {
|
|
805
1036
|
if (opts.gitBefore) {
|
|
@@ -882,21 +1113,21 @@ function registerHook(program2) {
|
|
|
882
1113
|
}
|
|
883
1114
|
|
|
884
1115
|
// src/cli/init.ts
|
|
885
|
-
var
|
|
886
|
-
var
|
|
1116
|
+
var import_fs12 = __toESM(require("fs"));
|
|
1117
|
+
var import_path9 = __toESM(require("path"));
|
|
887
1118
|
var import_os4 = __toESM(require("os"));
|
|
888
1119
|
var import_prompts = __toESM(require("prompts"));
|
|
889
1120
|
var import_chalk2 = __toESM(require("chalk"));
|
|
890
1121
|
|
|
891
1122
|
// src/capture/shell-hook.ts
|
|
892
|
-
var
|
|
893
|
-
var
|
|
1123
|
+
var import_fs10 = __toESM(require("fs"));
|
|
1124
|
+
var import_path7 = __toESM(require("path"));
|
|
894
1125
|
var HOOK_START = "# workscribe:hook:start";
|
|
895
1126
|
var HOOK_END = "# workscribe:hook:end";
|
|
896
1127
|
var HOOK_PREAMBLE = "# Workscribe \u2014 developer work journal";
|
|
897
1128
|
function getInstalledHookBlock(rcFile) {
|
|
898
|
-
if (!
|
|
899
|
-
const content =
|
|
1129
|
+
if (!import_fs10.default.existsSync(rcFile)) return null;
|
|
1130
|
+
const content = import_fs10.default.readFileSync(rcFile, "utf-8");
|
|
900
1131
|
const startIdx = content.indexOf(HOOK_START);
|
|
901
1132
|
const endIdx = content.indexOf(HOOK_END);
|
|
902
1133
|
if (startIdx === -1 || endIdx === -1) return null;
|
|
@@ -912,38 +1143,38 @@ function isHookOutdated(rcFile, hookContent) {
|
|
|
912
1143
|
}
|
|
913
1144
|
function installHook(rcFile, hookContent) {
|
|
914
1145
|
const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
|
|
915
|
-
|
|
916
|
-
log.info(`Backed up ${rcFile} \u2192 ${
|
|
917
|
-
|
|
1146
|
+
import_fs10.default.copyFileSync(rcFile, backup);
|
|
1147
|
+
log.info(`Backed up ${rcFile} \u2192 ${import_path7.default.basename(backup)}`);
|
|
1148
|
+
import_fs10.default.appendFileSync(rcFile, `
|
|
918
1149
|
${hookContent}
|
|
919
1150
|
`);
|
|
920
1151
|
}
|
|
921
1152
|
function updateHook(rcFile, hookContent) {
|
|
922
1153
|
const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
|
|
923
|
-
|
|
924
|
-
log.info(`Backed up ${rcFile} \u2192 ${
|
|
925
|
-
const content =
|
|
1154
|
+
import_fs10.default.copyFileSync(rcFile, backup);
|
|
1155
|
+
log.info(`Backed up ${rcFile} \u2192 ${import_path7.default.basename(backup)}`);
|
|
1156
|
+
const content = import_fs10.default.readFileSync(rcFile, "utf-8");
|
|
926
1157
|
const startIdx = content.indexOf(HOOK_START);
|
|
927
1158
|
const endIdx = content.indexOf(HOOK_END) + HOOK_END.length;
|
|
928
|
-
|
|
1159
|
+
import_fs10.default.writeFileSync(rcFile, content.slice(0, startIdx) + hookContent + content.slice(endIdx));
|
|
929
1160
|
}
|
|
930
1161
|
function removeHook(rcFile) {
|
|
931
1162
|
const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
|
|
932
|
-
|
|
933
|
-
log.info(`Backed up ${rcFile} \u2192 ${
|
|
934
|
-
const content =
|
|
1163
|
+
import_fs10.default.copyFileSync(rcFile, backup);
|
|
1164
|
+
log.info(`Backed up ${rcFile} \u2192 ${import_path7.default.basename(backup)}`);
|
|
1165
|
+
const content = import_fs10.default.readFileSync(rcFile, "utf-8");
|
|
935
1166
|
const startIdx = content.indexOf(HOOK_START);
|
|
936
1167
|
const endIdx = content.indexOf(HOOK_END) + HOOK_END.length;
|
|
937
1168
|
const preambleIdx = content.lastIndexOf(HOOK_PREAMBLE, startIdx);
|
|
938
1169
|
const blockStart = preambleIdx !== -1 ? preambleIdx : startIdx;
|
|
939
1170
|
const before = content.slice(0, blockStart).replace(/\n+$/, "\n");
|
|
940
1171
|
const after = content.slice(endIdx).replace(/^\n/, "");
|
|
941
|
-
|
|
1172
|
+
import_fs10.default.writeFileSync(rcFile, before + after);
|
|
942
1173
|
}
|
|
943
1174
|
|
|
944
1175
|
// src/capture/hooks/claude-code.ts
|
|
945
|
-
var
|
|
946
|
-
var
|
|
1176
|
+
var import_fs11 = __toESM(require("fs"));
|
|
1177
|
+
var import_path8 = __toESM(require("path"));
|
|
947
1178
|
var import_os3 = __toESM(require("os"));
|
|
948
1179
|
|
|
949
1180
|
// src/capture/hooks/utils.ts
|
|
@@ -958,11 +1189,11 @@ function isCommandInstalled(cmd) {
|
|
|
958
1189
|
}
|
|
959
1190
|
|
|
960
1191
|
// src/capture/hooks/claude-code.ts
|
|
961
|
-
var CLAUDE_SETTINGS_PATH =
|
|
1192
|
+
var CLAUDE_SETTINGS_PATH = import_path8.default.join(import_os3.default.homedir(), ".claude", "settings.json");
|
|
962
1193
|
function readSettings() {
|
|
963
|
-
if (!
|
|
1194
|
+
if (!import_fs11.default.existsSync(CLAUDE_SETTINGS_PATH)) return {};
|
|
964
1195
|
try {
|
|
965
|
-
return JSON.parse(
|
|
1196
|
+
return JSON.parse(import_fs11.default.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8"));
|
|
966
1197
|
} catch {
|
|
967
1198
|
return {};
|
|
968
1199
|
}
|
|
@@ -977,8 +1208,8 @@ var claudeCodeIntegration = {
|
|
|
977
1208
|
const postToolUse = settings.hooks?.PostToolUse ?? [];
|
|
978
1209
|
return postToolUse.some((entry) => {
|
|
979
1210
|
const hooks = entry.hooks ?? [];
|
|
980
|
-
return hooks.some((
|
|
981
|
-
const hook =
|
|
1211
|
+
return hooks.some((hookItem) => {
|
|
1212
|
+
const hook = hookItem;
|
|
982
1213
|
return typeof hook.command === "string" && hook.command.includes("workscribe _hook");
|
|
983
1214
|
});
|
|
984
1215
|
});
|
|
@@ -997,8 +1228,8 @@ var claudeCodeIntegration = {
|
|
|
997
1228
|
}
|
|
998
1229
|
]
|
|
999
1230
|
};
|
|
1000
|
-
|
|
1001
|
-
|
|
1231
|
+
import_fs11.default.mkdirSync(import_path8.default.dirname(CLAUDE_SETTINGS_PATH), { recursive: true });
|
|
1232
|
+
import_fs11.default.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
1002
1233
|
}
|
|
1003
1234
|
};
|
|
1004
1235
|
|
|
@@ -1010,22 +1241,25 @@ var AI_HOOK_INTEGRATIONS = {
|
|
|
1010
1241
|
// src/cli/init.ts
|
|
1011
1242
|
function detectShell() {
|
|
1012
1243
|
const shell = process.env.SHELL ?? "";
|
|
1013
|
-
if (shell.includes("zsh")) return { name: "zsh", rcFile:
|
|
1014
|
-
if (shell.includes("bash")) return { name: "bash", rcFile:
|
|
1244
|
+
if (shell.includes("zsh")) return { name: "zsh", rcFile: import_path9.default.join(import_os4.default.homedir(), ".zshrc") };
|
|
1245
|
+
if (shell.includes("bash")) return { name: "bash", rcFile: import_path9.default.join(import_os4.default.homedir(), ".bashrc") };
|
|
1015
1246
|
return null;
|
|
1016
1247
|
}
|
|
1017
1248
|
function registerInit(program2) {
|
|
1018
1249
|
program2.command("init").description("Set up Workscribe and install or update shell hooks").action(async () => {
|
|
1019
1250
|
log.plain(import_chalk2.default.bold("\n Workscribe \u2014 Developer Work Journal\n"));
|
|
1020
|
-
if (!
|
|
1021
|
-
|
|
1251
|
+
if (!import_fs12.default.existsSync(WORKSCRIBE_DIR)) {
|
|
1252
|
+
import_fs12.default.mkdirSync(WORKSCRIBE_DIR, { recursive: true, mode: 448 });
|
|
1022
1253
|
log.success(`Created ${WORKSCRIBE_DIR}`);
|
|
1023
1254
|
} else {
|
|
1024
1255
|
try {
|
|
1025
|
-
|
|
1256
|
+
import_fs12.default.chmodSync(WORKSCRIBE_DIR, 448);
|
|
1026
1257
|
} catch {
|
|
1027
1258
|
}
|
|
1028
1259
|
}
|
|
1260
|
+
if (!import_fs12.default.existsSync(PROFILES_DIR)) {
|
|
1261
|
+
import_fs12.default.mkdirSync(PROFILES_DIR, { recursive: true, mode: 448 });
|
|
1262
|
+
}
|
|
1029
1263
|
getDb();
|
|
1030
1264
|
log.success("Database initialized");
|
|
1031
1265
|
const shell = detectShell();
|
|
@@ -1033,12 +1267,12 @@ function registerInit(program2) {
|
|
|
1033
1267
|
log.warn("Could not detect shell. Supported: zsh, bash");
|
|
1034
1268
|
log.plain(`Manually add the hook from: ${import_chalk2.default.cyan("shell/hook.zsh")} or ${import_chalk2.default.cyan("shell/hook.bash")}`);
|
|
1035
1269
|
} else {
|
|
1036
|
-
const hookFile =
|
|
1037
|
-
if (!
|
|
1270
|
+
const hookFile = import_path9.default.join(__dirname, "..", "shell", `hook.${shell.name}`);
|
|
1271
|
+
if (!import_fs12.default.existsSync(hookFile)) {
|
|
1038
1272
|
log.warn(`Hook template not found at ${hookFile}`);
|
|
1039
1273
|
} else {
|
|
1040
|
-
const hookContent =
|
|
1041
|
-
if (!
|
|
1274
|
+
const hookContent = import_fs12.default.readFileSync(hookFile, "utf-8");
|
|
1275
|
+
if (!import_fs12.default.existsSync(shell.rcFile)) import_fs12.default.writeFileSync(shell.rcFile, "");
|
|
1042
1276
|
if (!isHookInstalled(shell.rcFile)) {
|
|
1043
1277
|
installHook(shell.rcFile, hookContent);
|
|
1044
1278
|
log.success(`Shell hook installed in ${shell.rcFile}`);
|
|
@@ -1061,7 +1295,7 @@ function registerInit(program2) {
|
|
|
1061
1295
|
}
|
|
1062
1296
|
}
|
|
1063
1297
|
const installedTools = loadAIToolRegistry().filter(
|
|
1064
|
-
(
|
|
1298
|
+
(entry) => entry.hook !== null && AI_HOOK_INTEGRATIONS[entry.hook]?.isInstalled()
|
|
1065
1299
|
);
|
|
1066
1300
|
if (installedTools.length === 0) {
|
|
1067
1301
|
log.info("No AI coding tools detected \u2014 re-run workscribe init after installing one");
|
|
@@ -1087,6 +1321,33 @@ function registerInit(program2) {
|
|
|
1087
1321
|
}
|
|
1088
1322
|
}
|
|
1089
1323
|
const config = readConfig();
|
|
1324
|
+
try {
|
|
1325
|
+
await cacheManifest();
|
|
1326
|
+
const detected = detectProfilesForDirectory(process.cwd());
|
|
1327
|
+
const alreadyActive = new Set(config.capture.profiles);
|
|
1328
|
+
const toPrompt = detected.filter((entry) => !alreadyActive.has(entry.name));
|
|
1329
|
+
if (toPrompt.length > 0) {
|
|
1330
|
+
log.plain("");
|
|
1331
|
+
for (const entry of toPrompt) {
|
|
1332
|
+
const { install } = await (0, import_prompts.default)({
|
|
1333
|
+
type: "confirm",
|
|
1334
|
+
name: "install",
|
|
1335
|
+
message: `${entry.displayName} project detected. Enable the ${entry.displayName} profile?`,
|
|
1336
|
+
initial: true
|
|
1337
|
+
});
|
|
1338
|
+
if (install) {
|
|
1339
|
+
await installProfile(entry.name);
|
|
1340
|
+
config.capture.profiles = [.../* @__PURE__ */ new Set([...config.capture.profiles, entry.name])];
|
|
1341
|
+
log.success(`${entry.displayName} profile enabled`);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
writeConfig(config);
|
|
1345
|
+
} else if (detected.length === 0) {
|
|
1346
|
+
log.info("No project profiles detected \u2014 run workscribe profile detect from your project directory");
|
|
1347
|
+
}
|
|
1348
|
+
} catch {
|
|
1349
|
+
log.info("Could not reach profile registry \u2014 run workscribe profile detect after connecting");
|
|
1350
|
+
}
|
|
1090
1351
|
if (config.ai.provider) {
|
|
1091
1352
|
log.info(`AI provider already configured: ${import_chalk2.default.cyan(config.ai.provider)}`);
|
|
1092
1353
|
} else {
|
|
@@ -1134,7 +1395,7 @@ function registerInit(program2) {
|
|
|
1134
1395
|
log.info("Skipped \u2014 run later: workscribe config set ai.provider <provider>");
|
|
1135
1396
|
}
|
|
1136
1397
|
}
|
|
1137
|
-
if (!
|
|
1398
|
+
if (!import_fs12.default.existsSync(CONFIG_PATH)) {
|
|
1138
1399
|
writeConfig(config);
|
|
1139
1400
|
log.success("Config file created");
|
|
1140
1401
|
}
|
|
@@ -1153,7 +1414,7 @@ function registerInit(program2) {
|
|
|
1153
1414
|
}
|
|
1154
1415
|
|
|
1155
1416
|
// src/cli/status.ts
|
|
1156
|
-
var
|
|
1417
|
+
var import_fs13 = __toESM(require("fs"));
|
|
1157
1418
|
var import_chalk3 = __toESM(require("chalk"));
|
|
1158
1419
|
|
|
1159
1420
|
// src/utils/format.ts
|
|
@@ -1211,7 +1472,7 @@ function registerStatus(program2) {
|
|
|
1211
1472
|
const total = getEventCount();
|
|
1212
1473
|
const todayEvents = getEventsForDate(todayDate());
|
|
1213
1474
|
const last = getLastEvent();
|
|
1214
|
-
const isPaused =
|
|
1475
|
+
const isPaused = import_fs13.default.existsSync(PAUSE_FLAG_PATH);
|
|
1215
1476
|
log.plain("");
|
|
1216
1477
|
log.plain(import_chalk3.default.bold(" Workscribe Status"));
|
|
1217
1478
|
log.plain("");
|
|
@@ -1230,6 +1491,25 @@ function registerStatus(program2) {
|
|
|
1230
1491
|
` AI provider ${config.ai.provider ? import_chalk3.default.cyan(config.ai.provider) : import_chalk3.default.yellow("not configured")}`
|
|
1231
1492
|
);
|
|
1232
1493
|
if (config.ai.model) log.plain(` Model ${import_chalk3.default.dim(config.ai.model)}`);
|
|
1494
|
+
const activeProfiles = config.capture.profiles;
|
|
1495
|
+
log.plain(
|
|
1496
|
+
` Profiles ${activeProfiles.length > 0 ? import_chalk3.default.cyan(activeProfiles.join(", ")) : import_chalk3.default.dim("none")}`
|
|
1497
|
+
);
|
|
1498
|
+
const { total: weekTotal, otherCount } = getRecentOtherRate(7);
|
|
1499
|
+
if (weekTotal > 0) {
|
|
1500
|
+
const otherPct = Math.round(otherCount / weekTotal * 100);
|
|
1501
|
+
if (otherPct >= 25) {
|
|
1502
|
+
log.plain("");
|
|
1503
|
+
log.plain(
|
|
1504
|
+
` ${import_chalk3.default.yellow("\u26A0")} ${otherCount} of ${weekTotal} events (${otherPct}%) are uncategorised this week.`
|
|
1505
|
+
);
|
|
1506
|
+
log.plain(
|
|
1507
|
+
import_chalk3.default.dim(
|
|
1508
|
+
" Run workscribe profile detect \u2014 scan current directory for matching profiles\n Run workscribe profile suggest \u2014 analyse your command history"
|
|
1509
|
+
)
|
|
1510
|
+
);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1233
1513
|
log.plain("");
|
|
1234
1514
|
});
|
|
1235
1515
|
}
|
|
@@ -1237,30 +1517,23 @@ function registerStatus(program2) {
|
|
|
1237
1517
|
// src/cli/events.ts
|
|
1238
1518
|
var import_chalk4 = __toESM(require("chalk"));
|
|
1239
1519
|
var CATEGORY_COLORS = {
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
build: import_chalk4.default.cyan,
|
|
1248
|
-
server_run: import_chalk4.default.cyan,
|
|
1520
|
+
shipping: import_chalk4.default.green,
|
|
1521
|
+
versioning: import_chalk4.default.blue,
|
|
1522
|
+
building: import_chalk4.default.cyan,
|
|
1523
|
+
running: import_chalk4.default.white,
|
|
1524
|
+
testing: import_chalk4.default.magenta,
|
|
1525
|
+
linting: import_chalk4.default.yellow,
|
|
1526
|
+
dependencies: import_chalk4.default.yellow,
|
|
1249
1527
|
infrastructure: import_chalk4.default.red,
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
code_stash: import_chalk4.default.blue,
|
|
1528
|
+
releasing: import_chalk4.default.bold.green,
|
|
1529
|
+
database: import_chalk4.default.bold.blue,
|
|
1530
|
+
files: import_chalk4.default.dim.yellow,
|
|
1531
|
+
exploring: import_chalk4.default.dim.cyan,
|
|
1255
1532
|
ai_session: import_chalk4.default.bold.magenta,
|
|
1256
|
-
lint_run: import_chalk4.default.yellow,
|
|
1257
|
-
code_search: import_chalk4.default.dim.cyan,
|
|
1258
|
-
code_read: import_chalk4.default.dim.white,
|
|
1259
|
-
file_ops: import_chalk4.default.yellow,
|
|
1260
1533
|
other: import_chalk4.default.gray
|
|
1261
1534
|
};
|
|
1262
1535
|
var AI_TOOL_DISPLAY_NAMES = Object.fromEntries(
|
|
1263
|
-
loadAIToolRegistry().map((
|
|
1536
|
+
loadAIToolRegistry().map((entry) => [entry.tool, entry.displayName])
|
|
1264
1537
|
);
|
|
1265
1538
|
function registerEvents(program2) {
|
|
1266
1539
|
program2.command("events").description("List captured events").option("--date <YYYY-MM-DD>", "date to list events for (default: today)").option("--limit <n>", "max events to show", parseInt).action((opts) => {
|
|
@@ -1352,18 +1625,18 @@ function getOrBuildSessions(date) {
|
|
|
1352
1625
|
const existing = getSessionsForDate(date);
|
|
1353
1626
|
const events = getEventsForDate(date);
|
|
1354
1627
|
if (events.length === 0) return [];
|
|
1355
|
-
const cachedEventCount = existing.reduce((sum,
|
|
1628
|
+
const cachedEventCount = existing.reduce((sum, sessionRow) => sum + sessionRow.event_count, 0);
|
|
1356
1629
|
if (existing.length > 0 && cachedEventCount === events.length) {
|
|
1357
|
-
return existing.map((
|
|
1358
|
-
id:
|
|
1359
|
-
date:
|
|
1360
|
-
startedAt:
|
|
1361
|
-
endedAt:
|
|
1362
|
-
repoName:
|
|
1363
|
-
durationMinutes: minutesBetween(
|
|
1364
|
-
eventCount:
|
|
1365
|
-
summary:
|
|
1366
|
-
events: getEventsBySessionId(
|
|
1630
|
+
return existing.map((sessionRow) => ({
|
|
1631
|
+
id: sessionRow.id,
|
|
1632
|
+
date: sessionRow.date,
|
|
1633
|
+
startedAt: sessionRow.started_at,
|
|
1634
|
+
endedAt: sessionRow.ended_at,
|
|
1635
|
+
repoName: sessionRow.repo_name,
|
|
1636
|
+
durationMinutes: minutesBetween(sessionRow.started_at, sessionRow.ended_at),
|
|
1637
|
+
eventCount: sessionRow.event_count,
|
|
1638
|
+
summary: sessionRow.summary,
|
|
1639
|
+
events: getEventsBySessionId(sessionRow.id)
|
|
1367
1640
|
}));
|
|
1368
1641
|
}
|
|
1369
1642
|
const savedSummaries = /* @__PURE__ */ new Map();
|
|
@@ -1449,9 +1722,9 @@ function registerSessions(program2) {
|
|
|
1449
1722
|
|
|
1450
1723
|
// src/cli/summary.ts
|
|
1451
1724
|
var import_chalk6 = __toESM(require("chalk"));
|
|
1452
|
-
var
|
|
1725
|
+
var import_fs14 = __toESM(require("fs"));
|
|
1453
1726
|
var import_os5 = __toESM(require("os"));
|
|
1454
|
-
var
|
|
1727
|
+
var import_path10 = __toESM(require("path"));
|
|
1455
1728
|
|
|
1456
1729
|
// src/ai/prompt.ts
|
|
1457
1730
|
var SYSTEM_PROMPT = `You write short developer work summaries. Here is an example:
|
|
@@ -1461,27 +1734,34 @@ Project: api-service
|
|
|
1461
1734
|
Branch: feature/auth
|
|
1462
1735
|
Duration: 2h 00m
|
|
1463
1736
|
Events:
|
|
1464
|
-
-
|
|
1465
|
-
- 2x
|
|
1466
|
-
- 2x
|
|
1467
|
-
-
|
|
1468
|
-
- 2x
|
|
1469
|
-
-
|
|
1470
|
-
-
|
|
1737
|
+
- versioning {"branch":"feature/auth"}
|
|
1738
|
+
- 2x dependencies {"tool":"npm","packages":["bcrypt","jsonwebtoken"]}
|
|
1739
|
+
- 2x testing {"tool":"jest"} [failed]
|
|
1740
|
+
- testing {"tool":"jest"}
|
|
1741
|
+
- 2x files {"tool":"mkdir"}
|
|
1742
|
+
- shipping {"message":"fix: auth token validation"}
|
|
1743
|
+
- shipping {"remote":"origin","branch":"feature/auth"}
|
|
1471
1744
|
|
|
1472
1745
|
OUTPUT:
|
|
1473
1746
|
- Switched to the authentication feature branch and added authentication dependencies (bcrypt, JWT)
|
|
1474
1747
|
- Resolved failing auth tests
|
|
1475
1748
|
- Committed and pushed authentication fixes
|
|
1476
1749
|
|
|
1477
|
-
For
|
|
1750
|
+
For dependencies events with a packages field: infer the purpose of packages from their names and describe the domain, not just the packages. Examples: bcrypt/jsonwebtoken \u2192 authentication, prisma/mongoose/pg \u2192 database, jest/vitest/mocha \u2192 testing, axios/got/node-fetch \u2192 HTTP client, stripe/paypal \u2192 payments, socket.io/ws \u2192 real-time. Use your knowledge of common packages. For unfamiliar packages, name them directly without guessing their domain.
|
|
1478
1751
|
|
|
1479
|
-
Events like
|
|
1752
|
+
Events like shipping (pushes), building, versioning (branch switches, stash), files (file ops, env vars), and infrastructure provide context \u2014 fold them into the surrounding narrative rather than listing them as separate bullets. Only surface them directly when they are the main activity (e.g. a standalone build fix, a deploy, or a significant restructure with no accompanying commits).
|
|
1480
1753
|
|
|
1481
1754
|
For ai_session events: describe what was built during the AI session using the tool name (e.g. "via Claude Code"). If a commits array is present in metadata, use those commit messages to describe what was built. Group all activity from the session into one narrative bullet.
|
|
1482
1755
|
|
|
1756
|
+
For other events with a hint field: interpret the command snippet and describe the activity in plain language. Do not quote the raw command.
|
|
1757
|
+
|
|
1483
1758
|
Maximum 5 bullets. Output ONLY the bullet points, nothing else.`;
|
|
1484
|
-
|
|
1759
|
+
function getSystemPrompt() {
|
|
1760
|
+
const contexts = getActiveProfileAiContexts();
|
|
1761
|
+
if (contexts.length === 0) return SYSTEM_PROMPT;
|
|
1762
|
+
return SYSTEM_PROMPT + "\n\n" + contexts.join("\n");
|
|
1763
|
+
}
|
|
1764
|
+
var EXCLUDED = /* @__PURE__ */ new Set(["exploring"]);
|
|
1485
1765
|
function buildUserMessage(payload) {
|
|
1486
1766
|
const lines = [`Date: ${payload.date}`, ""];
|
|
1487
1767
|
for (const session of payload.sessions) {
|
|
@@ -1492,9 +1772,14 @@ function buildUserMessage(payload) {
|
|
|
1492
1772
|
const counts = /* @__PURE__ */ new Map();
|
|
1493
1773
|
for (const event of session.events) {
|
|
1494
1774
|
if (EXCLUDED.has(event.category)) continue;
|
|
1495
|
-
const meta = Object.keys(event.metadata).length > 0 ? ` ${JSON.stringify(event.metadata)}` : "";
|
|
1496
1775
|
const failed = event.exitCode !== null && event.exitCode !== 0 ? " [failed]" : "";
|
|
1497
|
-
|
|
1776
|
+
let key;
|
|
1777
|
+
if (event.hint) {
|
|
1778
|
+
key = `other [${event.hint}]${failed}`;
|
|
1779
|
+
} else {
|
|
1780
|
+
const meta = Object.keys(event.metadata).length > 0 ? ` ${JSON.stringify(event.metadata)}` : "";
|
|
1781
|
+
key = `${event.category}${meta}${failed}`;
|
|
1782
|
+
}
|
|
1498
1783
|
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
1499
1784
|
}
|
|
1500
1785
|
for (const [key, count] of counts) {
|
|
@@ -1549,9 +1834,14 @@ function buildWeeklyMessage(payload) {
|
|
|
1549
1834
|
const counts = /* @__PURE__ */ new Map();
|
|
1550
1835
|
for (const e of s.events) {
|
|
1551
1836
|
if (EXCLUDED.has(e.category)) continue;
|
|
1552
|
-
const meta = Object.keys(e.metadata).length > 0 ? ` ${JSON.stringify(e.metadata)}` : "";
|
|
1553
1837
|
const failed = e.exitCode !== null && e.exitCode !== 0 ? " [failed]" : "";
|
|
1554
|
-
|
|
1838
|
+
let key;
|
|
1839
|
+
if (e.hint) {
|
|
1840
|
+
key = `other [${e.hint}]${failed}`;
|
|
1841
|
+
} else {
|
|
1842
|
+
const meta = Object.keys(e.metadata).length > 0 ? ` ${JSON.stringify(e.metadata)}` : "";
|
|
1843
|
+
key = `${e.category}${meta}${failed}`;
|
|
1844
|
+
}
|
|
1555
1845
|
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
1556
1846
|
}
|
|
1557
1847
|
const eventLine = [...counts.entries()].map(([k, n]) => `${n > 1 ? `${n}x ` : ""}${k}`).join(", ");
|
|
@@ -1598,7 +1888,7 @@ var AnthropicProvider = class {
|
|
|
1598
1888
|
body: JSON.stringify({
|
|
1599
1889
|
model: this.model,
|
|
1600
1890
|
max_tokens: 1024,
|
|
1601
|
-
system:
|
|
1891
|
+
system: getSystemPrompt(),
|
|
1602
1892
|
messages: [{ role: "user", content: buildUserMessage(payload) }]
|
|
1603
1893
|
})
|
|
1604
1894
|
});
|
|
@@ -1607,7 +1897,7 @@ var AnthropicProvider = class {
|
|
|
1607
1897
|
throw new Error(`Anthropic API error ${response.status}: ${body}`);
|
|
1608
1898
|
}
|
|
1609
1899
|
const data = await response.json();
|
|
1610
|
-
return data.content.find((
|
|
1900
|
+
return data.content.find((block) => block.type === "text")?.text ?? "";
|
|
1611
1901
|
}
|
|
1612
1902
|
async summarizeWeek(payload) {
|
|
1613
1903
|
const response = await fetchWithTimeout("https://api.anthropic.com/v1/messages", {
|
|
@@ -1629,7 +1919,7 @@ var AnthropicProvider = class {
|
|
|
1629
1919
|
throw new Error(`Anthropic API error ${response.status}: ${body}`);
|
|
1630
1920
|
}
|
|
1631
1921
|
const data = await response.json();
|
|
1632
|
-
return data.content.find((
|
|
1922
|
+
return data.content.find((block) => block.type === "text")?.text ?? "";
|
|
1633
1923
|
}
|
|
1634
1924
|
};
|
|
1635
1925
|
|
|
@@ -1653,7 +1943,7 @@ var OpenAIProvider = class {
|
|
|
1653
1943
|
body: JSON.stringify({
|
|
1654
1944
|
model: this.model,
|
|
1655
1945
|
messages: [
|
|
1656
|
-
{ role: "system", content:
|
|
1946
|
+
{ role: "system", content: getSystemPrompt() },
|
|
1657
1947
|
{ role: "user", content: buildUserMessage(payload) }
|
|
1658
1948
|
],
|
|
1659
1949
|
max_tokens: 1024
|
|
@@ -1707,7 +1997,7 @@ var OllamaProvider = class {
|
|
|
1707
1997
|
model: this.model,
|
|
1708
1998
|
stream: false,
|
|
1709
1999
|
messages: [
|
|
1710
|
-
{ role: "system", content:
|
|
2000
|
+
{ role: "system", content: getSystemPrompt() },
|
|
1711
2001
|
{ role: "user", content: buildUserMessage(payload) }
|
|
1712
2002
|
],
|
|
1713
2003
|
options: {
|
|
@@ -1815,7 +2105,7 @@ function isSlackWebhookUrl(url) {
|
|
|
1815
2105
|
|
|
1816
2106
|
// src/cli/summary.ts
|
|
1817
2107
|
function parseBullets(text) {
|
|
1818
|
-
return text.split("\n").map((
|
|
2108
|
+
return text.split("\n").map((line) => line.replace(/^[•\-]\s*/, "").trim()).filter(Boolean);
|
|
1819
2109
|
}
|
|
1820
2110
|
function eventRowToPayload(row) {
|
|
1821
2111
|
let metadata = {};
|
|
@@ -1823,12 +2113,18 @@ function eventRowToPayload(row) {
|
|
|
1823
2113
|
if (row.metadata) metadata = JSON.parse(row.metadata);
|
|
1824
2114
|
} catch {
|
|
1825
2115
|
}
|
|
1826
|
-
|
|
2116
|
+
const payload = {
|
|
1827
2117
|
category: row.category,
|
|
1828
2118
|
metadata,
|
|
1829
2119
|
exitCode: row.exit_code,
|
|
1830
2120
|
durationMs: row.duration_ms
|
|
1831
2121
|
};
|
|
2122
|
+
if (row.category === "other" && typeof metadata.hint === "string") {
|
|
2123
|
+
payload.hint = metadata.hint;
|
|
2124
|
+
const { hint: _h, tool: _t, ...rest } = metadata;
|
|
2125
|
+
payload.metadata = rest;
|
|
2126
|
+
}
|
|
2127
|
+
return payload;
|
|
1832
2128
|
}
|
|
1833
2129
|
function sessionToPayload(s, date) {
|
|
1834
2130
|
return {
|
|
@@ -1875,7 +2171,7 @@ function registerSummary(program2) {
|
|
|
1875
2171
|
for (const day of days) {
|
|
1876
2172
|
const sessions2 = getOrBuildSessions(day);
|
|
1877
2173
|
if (sessions2.length > 0) {
|
|
1878
|
-
weeklyDays.push({ date: day, sessions: sessions2.map((
|
|
2174
|
+
weeklyDays.push({ date: day, sessions: sessions2.map((session) => sessionToPayload(session, day)) });
|
|
1879
2175
|
}
|
|
1880
2176
|
}
|
|
1881
2177
|
if (weeklyDays.length === 0) {
|
|
@@ -1883,9 +2179,9 @@ function registerSummary(program2) {
|
|
|
1883
2179
|
return;
|
|
1884
2180
|
}
|
|
1885
2181
|
const cacheKey = isoWeekKey(today);
|
|
1886
|
-
const cacheFile =
|
|
1887
|
-
if (!opts.fresh &&
|
|
1888
|
-
const cached =
|
|
2182
|
+
const cacheFile = import_path10.default.join(WEEKLY_CACHE_DIR, `${cacheKey}.md`);
|
|
2183
|
+
if (!opts.fresh && import_fs14.default.existsSync(cacheFile)) {
|
|
2184
|
+
const cached = import_fs14.default.readFileSync(cacheFile, "utf-8");
|
|
1889
2185
|
log.plain("");
|
|
1890
2186
|
log.plain(cached);
|
|
1891
2187
|
log.plain("");
|
|
@@ -1897,12 +2193,12 @@ function registerSummary(program2) {
|
|
|
1897
2193
|
log.plain("");
|
|
1898
2194
|
log.info(`Generating weekly summary via ${import_chalk6.default.cyan(config.ai.provider)}\u2026`);
|
|
1899
2195
|
try {
|
|
1900
|
-
const summary = provider.summarizeWeek ? await provider.summarizeWeek(payload) : await provider.summarize({ date: today, sessions: weeklyDays.flatMap((
|
|
2196
|
+
const summary = provider.summarizeWeek ? await provider.summarizeWeek(payload) : await provider.summarize({ date: today, sessions: weeklyDays.flatMap((day) => day.sessions) });
|
|
1901
2197
|
log.plain("");
|
|
1902
2198
|
log.plain(summary);
|
|
1903
2199
|
log.plain("");
|
|
1904
|
-
|
|
1905
|
-
|
|
2200
|
+
import_fs14.default.mkdirSync(WEEKLY_CACHE_DIR, { recursive: true });
|
|
2201
|
+
import_fs14.default.writeFileSync(cacheFile, summary, { encoding: "utf-8", mode: 384 });
|
|
1906
2202
|
await handleWebhook(opts, config, summary);
|
|
1907
2203
|
} catch (e) {
|
|
1908
2204
|
log.error(`Weekly summary failed [${config.ai.provider}]: ${e.message}`);
|
|
@@ -1926,16 +2222,16 @@ function registerSummary(program2) {
|
|
|
1926
2222
|
}
|
|
1927
2223
|
const jsonMode = config.summary?.format === "json";
|
|
1928
2224
|
if (!opts.fresh) {
|
|
1929
|
-
const allCached = sessions.every((
|
|
2225
|
+
const allCached = sessions.every((session) => session.summary);
|
|
1930
2226
|
if (allCached) {
|
|
1931
|
-
const fullSummary2 = sessions.map((
|
|
2227
|
+
const fullSummary2 = sessions.map((session) => session.summary).join("\n\n");
|
|
1932
2228
|
log.plain("");
|
|
1933
2229
|
if (jsonMode) {
|
|
1934
|
-
const jsonOut = sessions.map((
|
|
1935
|
-
project:
|
|
1936
|
-
branch:
|
|
1937
|
-
durationMinutes:
|
|
1938
|
-
bullets: parseBullets(
|
|
2230
|
+
const jsonOut = sessions.map((session) => ({
|
|
2231
|
+
project: session.repoName,
|
|
2232
|
+
branch: session.events[0]?.branch ?? null,
|
|
2233
|
+
durationMinutes: session.durationMinutes,
|
|
2234
|
+
bullets: parseBullets(session.summary.replace(/^## .+\n/, ""))
|
|
1939
2235
|
}));
|
|
1940
2236
|
log.plain(JSON.stringify({ date, sessions: jsonOut }, null, 2));
|
|
1941
2237
|
} else {
|
|
@@ -1989,15 +2285,26 @@ ${bullets.trim()}`;
|
|
|
1989
2285
|
}
|
|
1990
2286
|
|
|
1991
2287
|
// src/cli/export.ts
|
|
1992
|
-
var
|
|
1993
|
-
var
|
|
2288
|
+
var import_fs15 = __toESM(require("fs"));
|
|
2289
|
+
var import_path11 = __toESM(require("path"));
|
|
1994
2290
|
function eventRowToPayload2(row) {
|
|
1995
2291
|
let metadata = {};
|
|
1996
2292
|
try {
|
|
1997
2293
|
if (row.metadata) metadata = JSON.parse(row.metadata);
|
|
1998
2294
|
} catch {
|
|
1999
2295
|
}
|
|
2000
|
-
|
|
2296
|
+
const payload = {
|
|
2297
|
+
category: row.category,
|
|
2298
|
+
metadata,
|
|
2299
|
+
exitCode: row.exit_code,
|
|
2300
|
+
durationMs: row.duration_ms
|
|
2301
|
+
};
|
|
2302
|
+
if (row.category === "other" && typeof metadata.hint === "string") {
|
|
2303
|
+
payload.hint = metadata.hint;
|
|
2304
|
+
const { hint: _h, tool: _t, ...rest } = metadata;
|
|
2305
|
+
payload.metadata = rest;
|
|
2306
|
+
}
|
|
2307
|
+
return payload;
|
|
2001
2308
|
}
|
|
2002
2309
|
function registerExport(program2) {
|
|
2003
2310
|
program2.command("export").description("Export work summary as a markdown file").option("--date <YYYY-MM-DD>", "date to export (default: today)").option("--output <path>", "output file path (default: ~/.workscribe/exports/)").action(async (opts) => {
|
|
@@ -2023,21 +2330,21 @@ function registerExport(program2) {
|
|
|
2023
2330
|
log.error(e.message);
|
|
2024
2331
|
process.exit(1);
|
|
2025
2332
|
}
|
|
2026
|
-
const sessionPayloads = sessions.map((
|
|
2333
|
+
const sessionPayloads = sessions.map((session) => ({
|
|
2027
2334
|
date,
|
|
2028
|
-
project:
|
|
2029
|
-
branch:
|
|
2030
|
-
startTime: formatDate(
|
|
2031
|
-
endTime: formatDate(
|
|
2032
|
-
durationMinutes:
|
|
2033
|
-
events:
|
|
2335
|
+
project: session.repoName,
|
|
2336
|
+
branch: session.events[0]?.branch ?? null,
|
|
2337
|
+
startTime: formatDate(session.startedAt),
|
|
2338
|
+
endTime: formatDate(session.endedAt),
|
|
2339
|
+
durationMinutes: session.durationMinutes,
|
|
2340
|
+
events: session.events.map(eventRowToPayload2)
|
|
2034
2341
|
}));
|
|
2035
2342
|
const payload = { date, sessions: sessionPayloads };
|
|
2036
2343
|
log.info(`Generating summary via ${config.ai.provider}\u2026`);
|
|
2037
2344
|
const summary = await provider.summarize(payload);
|
|
2038
|
-
const outputPath = opts.output ??
|
|
2039
|
-
if (!
|
|
2040
|
-
|
|
2345
|
+
const outputPath = opts.output ?? import_path11.default.join(EXPORTS_DIR, `workscribe-${date}.md`);
|
|
2346
|
+
if (!import_fs15.default.existsSync(EXPORTS_DIR)) import_fs15.default.mkdirSync(EXPORTS_DIR, { recursive: true, mode: 448 });
|
|
2347
|
+
import_fs15.default.writeFileSync(outputPath, `# Work Summary \u2014 ${date}
|
|
2041
2348
|
|
|
2042
2349
|
${summary}
|
|
2043
2350
|
`, { encoding: "utf-8", mode: 384 });
|
|
@@ -2109,22 +2416,22 @@ function printConfig(obj, indent) {
|
|
|
2109
2416
|
}
|
|
2110
2417
|
|
|
2111
2418
|
// src/cli/pause.ts
|
|
2112
|
-
var
|
|
2419
|
+
var import_fs16 = __toESM(require("fs"));
|
|
2113
2420
|
function registerPause(program2) {
|
|
2114
2421
|
program2.command("pause").description("Pause capture temporarily").action(() => {
|
|
2115
|
-
if (
|
|
2422
|
+
if (import_fs16.default.existsSync(PAUSE_FLAG_PATH)) {
|
|
2116
2423
|
log.info("Capture is already paused. Run workscribe resume to restart.");
|
|
2117
2424
|
return;
|
|
2118
2425
|
}
|
|
2119
|
-
|
|
2426
|
+
import_fs16.default.writeFileSync(PAUSE_FLAG_PATH, "", { mode: 384 });
|
|
2120
2427
|
log.success("Capture paused. Run workscribe resume to restart.");
|
|
2121
2428
|
});
|
|
2122
2429
|
program2.command("resume").description("Resume capture after a pause").action(() => {
|
|
2123
|
-
if (!
|
|
2430
|
+
if (!import_fs16.default.existsSync(PAUSE_FLAG_PATH)) {
|
|
2124
2431
|
log.info("Capture is not paused.");
|
|
2125
2432
|
return;
|
|
2126
2433
|
}
|
|
2127
|
-
|
|
2434
|
+
import_fs16.default.rmSync(PAUSE_FLAG_PATH);
|
|
2128
2435
|
log.success("Capture resumed.");
|
|
2129
2436
|
});
|
|
2130
2437
|
}
|
|
@@ -2209,18 +2516,18 @@ function registerClear(program2) {
|
|
|
2209
2516
|
}
|
|
2210
2517
|
|
|
2211
2518
|
// src/cli/uninstall.ts
|
|
2212
|
-
var
|
|
2213
|
-
var
|
|
2519
|
+
var import_fs17 = __toESM(require("fs"));
|
|
2520
|
+
var import_path12 = __toESM(require("path"));
|
|
2214
2521
|
var import_os6 = __toESM(require("os"));
|
|
2215
2522
|
var import_prompts3 = __toESM(require("prompts"));
|
|
2216
2523
|
var import_chalk9 = __toESM(require("chalk"));
|
|
2217
2524
|
function detectShellRcFiles() {
|
|
2218
2525
|
const candidates = [
|
|
2219
|
-
{ name: "zsh", rcFile:
|
|
2220
|
-
{ name: "bash", rcFile:
|
|
2221
|
-
{ name: "bash", rcFile:
|
|
2526
|
+
{ name: "zsh", rcFile: import_path12.default.join(import_os6.default.homedir(), ".zshrc") },
|
|
2527
|
+
{ name: "bash", rcFile: import_path12.default.join(import_os6.default.homedir(), ".bashrc") },
|
|
2528
|
+
{ name: "bash", rcFile: import_path12.default.join(import_os6.default.homedir(), ".bash_profile") }
|
|
2222
2529
|
];
|
|
2223
|
-
return candidates.filter((
|
|
2530
|
+
return candidates.filter((candidate) => import_fs17.default.existsSync(candidate.rcFile) && isHookInstalled(candidate.rcFile));
|
|
2224
2531
|
}
|
|
2225
2532
|
function registerUninstall(program2) {
|
|
2226
2533
|
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) => {
|
|
@@ -2239,7 +2546,7 @@ function registerUninstall(program2) {
|
|
|
2239
2546
|
}
|
|
2240
2547
|
}
|
|
2241
2548
|
if (opts.hooksOnly) return;
|
|
2242
|
-
if (
|
|
2549
|
+
if (import_fs17.default.existsSync(WORKSCRIBE_DIR)) {
|
|
2243
2550
|
log.plain("");
|
|
2244
2551
|
const { deleteData } = await (0, import_prompts3.default)({
|
|
2245
2552
|
type: "confirm",
|
|
@@ -2248,7 +2555,7 @@ function registerUninstall(program2) {
|
|
|
2248
2555
|
initial: false
|
|
2249
2556
|
});
|
|
2250
2557
|
if (deleteData) {
|
|
2251
|
-
|
|
2558
|
+
import_fs17.default.rmSync(WORKSCRIBE_DIR, { recursive: true, force: true });
|
|
2252
2559
|
log.success(`Deleted ${WORKSCRIBE_DIR}`);
|
|
2253
2560
|
} else {
|
|
2254
2561
|
log.info(`Data kept at ${WORKSCRIBE_DIR}`);
|
|
@@ -2260,9 +2567,182 @@ function registerUninstall(program2) {
|
|
|
2260
2567
|
});
|
|
2261
2568
|
}
|
|
2262
2569
|
|
|
2570
|
+
// src/cli/profile.ts
|
|
2571
|
+
var import_fs18 = __toESM(require("fs"));
|
|
2572
|
+
var import_path13 = __toESM(require("path"));
|
|
2573
|
+
var import_chalk10 = __toESM(require("chalk"));
|
|
2574
|
+
var MIN_SUGGEST_COUNT = 5;
|
|
2575
|
+
function registerProfile(program2) {
|
|
2576
|
+
const profile = program2.command("profile").description("Manage stack profiles for better command tracking");
|
|
2577
|
+
profile.command("list").description("List available profiles from the registry").action(async () => {
|
|
2578
|
+
const config = readConfig();
|
|
2579
|
+
const activeNames = new Set(config.capture.profiles);
|
|
2580
|
+
let manifest = readCachedManifest();
|
|
2581
|
+
let offline = false;
|
|
2582
|
+
if (!manifest) {
|
|
2583
|
+
try {
|
|
2584
|
+
manifest = await cacheManifest();
|
|
2585
|
+
} catch {
|
|
2586
|
+
offline = true;
|
|
2587
|
+
manifest = getCachedProfiles().map((p) => ({
|
|
2588
|
+
name: p.name,
|
|
2589
|
+
displayName: p.displayName,
|
|
2590
|
+
tags: [],
|
|
2591
|
+
description: "",
|
|
2592
|
+
keywords: [],
|
|
2593
|
+
detect: p.detect
|
|
2594
|
+
}));
|
|
2595
|
+
}
|
|
2596
|
+
} else {
|
|
2597
|
+
cacheManifest().catch(() => void 0);
|
|
2598
|
+
}
|
|
2599
|
+
if (offline) {
|
|
2600
|
+
log.warn("Registry unreachable \u2014 showing installed profiles only");
|
|
2601
|
+
}
|
|
2602
|
+
if (manifest.length === 0) {
|
|
2603
|
+
log.plain("No profiles available. Check your connection and try again.");
|
|
2604
|
+
return;
|
|
2605
|
+
}
|
|
2606
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
2607
|
+
for (const entry of manifest) {
|
|
2608
|
+
const tag = entry.tags[0] ?? "other";
|
|
2609
|
+
if (!grouped.has(tag)) grouped.set(tag, []);
|
|
2610
|
+
grouped.get(tag).push(entry);
|
|
2611
|
+
}
|
|
2612
|
+
log.plain("");
|
|
2613
|
+
for (const [tag, entries] of grouped) {
|
|
2614
|
+
log.plain(import_chalk10.default.bold(` ${tag.charAt(0).toUpperCase() + tag.slice(1)} profiles`));
|
|
2615
|
+
log.plain(import_chalk10.default.dim(" " + "\u2500".repeat(60)));
|
|
2616
|
+
for (const entry of entries) {
|
|
2617
|
+
const badge = activeNames.has(entry.name) ? import_chalk10.default.green(" [active]") : "";
|
|
2618
|
+
const installed = import_fs18.default.existsSync(import_path13.default.join(PROFILES_DIR, `${entry.name}.json`)) ? import_chalk10.default.dim(" installed") : "";
|
|
2619
|
+
log.plain(` ${import_chalk10.default.cyan(entry.name.padEnd(18))} ${entry.displayName.padEnd(28)}${badge}${installed}`);
|
|
2620
|
+
if (entry.description) log.plain(` ${import_chalk10.default.dim(" ".repeat(18) + entry.description)}`);
|
|
2621
|
+
}
|
|
2622
|
+
log.plain("");
|
|
2623
|
+
}
|
|
2624
|
+
});
|
|
2625
|
+
profile.command("add <names...>").description("Install one or more profiles from the registry").action(async (names) => {
|
|
2626
|
+
const config = readConfig();
|
|
2627
|
+
const activeNames = new Set(config.capture.profiles);
|
|
2628
|
+
for (const name of names) {
|
|
2629
|
+
if (activeNames.has(name)) {
|
|
2630
|
+
log.info(`Profile "${name}" is already active`);
|
|
2631
|
+
continue;
|
|
2632
|
+
}
|
|
2633
|
+
try {
|
|
2634
|
+
log.plain(`Fetching ${import_chalk10.default.cyan(name)}\u2026`);
|
|
2635
|
+
await installProfile(name);
|
|
2636
|
+
activeNames.add(name);
|
|
2637
|
+
log.success(`Profile "${name}" installed`);
|
|
2638
|
+
} catch (err) {
|
|
2639
|
+
log.error(`Failed to install "${name}": ${err.message}`);
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
config.capture.profiles = [...activeNames];
|
|
2643
|
+
writeConfig(config);
|
|
2644
|
+
});
|
|
2645
|
+
profile.command("remove <names...>").description("Uninstall one or more profiles").action((names) => {
|
|
2646
|
+
const config = readConfig();
|
|
2647
|
+
const activeNames = new Set(config.capture.profiles);
|
|
2648
|
+
for (const name of names) {
|
|
2649
|
+
if (!activeNames.has(name)) {
|
|
2650
|
+
log.warn(`Profile "${name}" is not active`);
|
|
2651
|
+
continue;
|
|
2652
|
+
}
|
|
2653
|
+
activeNames.delete(name);
|
|
2654
|
+
const cachePath = import_path13.default.join(PROFILES_DIR, `${name}.json`);
|
|
2655
|
+
if (import_fs18.default.existsSync(cachePath)) import_fs18.default.unlinkSync(cachePath);
|
|
2656
|
+
log.success(`Profile "${name}" removed`);
|
|
2657
|
+
}
|
|
2658
|
+
config.capture.profiles = [...activeNames];
|
|
2659
|
+
writeConfig(config);
|
|
2660
|
+
});
|
|
2661
|
+
profile.command("update [name]").description("Re-fetch installed profiles from the registry").action(async (name) => {
|
|
2662
|
+
const config = readConfig();
|
|
2663
|
+
const toUpdate = name ? [name] : config.capture.profiles;
|
|
2664
|
+
if (toUpdate.length === 0) {
|
|
2665
|
+
log.plain("No profiles installed. Run workscribe profile add <name> first.");
|
|
2666
|
+
return;
|
|
2667
|
+
}
|
|
2668
|
+
for (const profileName of toUpdate) {
|
|
2669
|
+
try {
|
|
2670
|
+
log.plain(`Updating ${import_chalk10.default.cyan(profileName)}\u2026`);
|
|
2671
|
+
await installProfile(profileName);
|
|
2672
|
+
log.success(`${profileName} updated`);
|
|
2673
|
+
} catch (err) {
|
|
2674
|
+
log.error(`Failed to update "${profileName}": ${err.message}`);
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
});
|
|
2678
|
+
profile.command("detect").description("Scan the current directory and suggest matching profiles").action(() => {
|
|
2679
|
+
const cwd = process.cwd();
|
|
2680
|
+
const matches = detectProfilesForDirectory(cwd);
|
|
2681
|
+
if (matches.length === 0) {
|
|
2682
|
+
const cached = readCachedManifest();
|
|
2683
|
+
if (!cached) {
|
|
2684
|
+
log.plain("No manifest cached. Run workscribe profile list to fetch it first.");
|
|
2685
|
+
} else {
|
|
2686
|
+
log.plain(`No profiles detected for ${import_chalk10.default.dim(cwd)}`);
|
|
2687
|
+
log.plain(import_chalk10.default.dim("Run workscribe profile list to browse available profiles."));
|
|
2688
|
+
}
|
|
2689
|
+
return;
|
|
2690
|
+
}
|
|
2691
|
+
log.plain("");
|
|
2692
|
+
log.plain(import_chalk10.default.bold(" Profiles detected for this directory:"));
|
|
2693
|
+
log.plain("");
|
|
2694
|
+
for (const entry of matches) {
|
|
2695
|
+
const files = entry.detect?.files?.join(", ") ?? "";
|
|
2696
|
+
log.plain(` ${import_chalk10.default.cyan(entry.name.padEnd(18))} ${entry.displayName} ${import_chalk10.default.dim("(" + files + ")")}`);
|
|
2697
|
+
}
|
|
2698
|
+
log.plain("");
|
|
2699
|
+
log.plain(` Run: ${import_chalk10.default.cyan("workscribe profile add " + matches.map((e) => e.name).join(" "))}`);
|
|
2700
|
+
log.plain("");
|
|
2701
|
+
});
|
|
2702
|
+
profile.command("suggest").description("Analyse recent unclassified commands and suggest profiles").option("--days <n>", "number of days to analyse", "30").action((opts) => {
|
|
2703
|
+
const days = Math.max(1, parseInt(opts.days, 10) || 30);
|
|
2704
|
+
const toolCounts = getOtherEventToolCounts(days);
|
|
2705
|
+
if (toolCounts.length === 0) {
|
|
2706
|
+
log.plain("No unclassified commands found in the last " + days + " days.");
|
|
2707
|
+
return;
|
|
2708
|
+
}
|
|
2709
|
+
const manifest = readCachedManifest();
|
|
2710
|
+
if (!manifest) {
|
|
2711
|
+
log.plain("No manifest cached. Run workscribe profile list first.");
|
|
2712
|
+
return;
|
|
2713
|
+
}
|
|
2714
|
+
const config = readConfig();
|
|
2715
|
+
const activeNames = new Set(config.capture.profiles);
|
|
2716
|
+
const suggestions = [];
|
|
2717
|
+
for (const entry of manifest) {
|
|
2718
|
+
if (activeNames.has(entry.name)) continue;
|
|
2719
|
+
const matchedCount = toolCounts.filter((tc) => entry.keywords.some((kw) => tc.tool.toLowerCase().includes(kw.toLowerCase()))).reduce((sum, tc) => sum + tc.count, 0);
|
|
2720
|
+
if (matchedCount >= MIN_SUGGEST_COUNT) {
|
|
2721
|
+
suggestions.push({ entry, count: matchedCount });
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
suggestions.sort((a, b) => b.count - a.count);
|
|
2725
|
+
if (suggestions.length === 0) {
|
|
2726
|
+
log.plain("No profile matches found. Your commands are well covered.");
|
|
2727
|
+
return;
|
|
2728
|
+
}
|
|
2729
|
+
log.plain("");
|
|
2730
|
+
log.plain(import_chalk10.default.bold(` Analysed last ${days} days of unclassified commands:`));
|
|
2731
|
+
log.plain("");
|
|
2732
|
+
for (const { entry, count } of suggestions) {
|
|
2733
|
+
log.plain(
|
|
2734
|
+
` ${import_chalk10.default.cyan(entry.name.padEnd(18))} ${String(count).padStart(4)} commands \u2192 ${entry.displayName}`
|
|
2735
|
+
);
|
|
2736
|
+
}
|
|
2737
|
+
log.plain("");
|
|
2738
|
+
log.plain(` Run: ${import_chalk10.default.cyan("workscribe profile add " + suggestions.map((s) => s.entry.name).join(" "))}`);
|
|
2739
|
+
log.plain("");
|
|
2740
|
+
});
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2263
2743
|
// src/index.ts
|
|
2264
2744
|
var program = new import_commander.Command();
|
|
2265
|
-
program.name("workscribe").description("Automatic developer work journal").version("1.
|
|
2745
|
+
program.name("workscribe").description("Automatic developer work journal").version("1.10.0");
|
|
2266
2746
|
registerCapture(program);
|
|
2267
2747
|
registerHook(program);
|
|
2268
2748
|
registerInit(program);
|
|
@@ -2276,5 +2756,6 @@ registerPause(program);
|
|
|
2276
2756
|
registerUndo(program);
|
|
2277
2757
|
registerClear(program);
|
|
2278
2758
|
registerUninstall(program);
|
|
2759
|
+
registerProfile(program);
|
|
2279
2760
|
program.parse();
|
|
2280
2761
|
//# sourceMappingURL=index.js.map
|