@workscribe/cli 1.9.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 +706 -257
- 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;
|
|
@@ -123,13 +143,13 @@ function applyMigrations(db) {
|
|
|
123
143
|
const applied = new Set(
|
|
124
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
|
}
|
|
@@ -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 {
|
|
@@ -490,114 +766,131 @@ function extractPackages(cmd, afterKeyword) {
|
|
|
490
766
|
if (idx === -1) return [];
|
|
491
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
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;
|
|
@@ -887,21 +1113,21 @@ function registerHook(program2) {
|
|
|
887
1113
|
}
|
|
888
1114
|
|
|
889
1115
|
// src/cli/init.ts
|
|
890
|
-
var
|
|
891
|
-
var
|
|
1116
|
+
var import_fs12 = __toESM(require("fs"));
|
|
1117
|
+
var import_path9 = __toESM(require("path"));
|
|
892
1118
|
var import_os4 = __toESM(require("os"));
|
|
893
1119
|
var import_prompts = __toESM(require("prompts"));
|
|
894
1120
|
var import_chalk2 = __toESM(require("chalk"));
|
|
895
1121
|
|
|
896
1122
|
// src/capture/shell-hook.ts
|
|
897
|
-
var
|
|
898
|
-
var
|
|
1123
|
+
var import_fs10 = __toESM(require("fs"));
|
|
1124
|
+
var import_path7 = __toESM(require("path"));
|
|
899
1125
|
var HOOK_START = "# workscribe:hook:start";
|
|
900
1126
|
var HOOK_END = "# workscribe:hook:end";
|
|
901
1127
|
var HOOK_PREAMBLE = "# Workscribe \u2014 developer work journal";
|
|
902
1128
|
function getInstalledHookBlock(rcFile) {
|
|
903
|
-
if (!
|
|
904
|
-
const content =
|
|
1129
|
+
if (!import_fs10.default.existsSync(rcFile)) return null;
|
|
1130
|
+
const content = import_fs10.default.readFileSync(rcFile, "utf-8");
|
|
905
1131
|
const startIdx = content.indexOf(HOOK_START);
|
|
906
1132
|
const endIdx = content.indexOf(HOOK_END);
|
|
907
1133
|
if (startIdx === -1 || endIdx === -1) return null;
|
|
@@ -917,38 +1143,38 @@ function isHookOutdated(rcFile, hookContent) {
|
|
|
917
1143
|
}
|
|
918
1144
|
function installHook(rcFile, hookContent) {
|
|
919
1145
|
const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
|
|
920
|
-
|
|
921
|
-
log.info(`Backed up ${rcFile} \u2192 ${
|
|
922
|
-
|
|
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, `
|
|
923
1149
|
${hookContent}
|
|
924
1150
|
`);
|
|
925
1151
|
}
|
|
926
1152
|
function updateHook(rcFile, hookContent) {
|
|
927
1153
|
const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
|
|
928
|
-
|
|
929
|
-
log.info(`Backed up ${rcFile} \u2192 ${
|
|
930
|
-
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");
|
|
931
1157
|
const startIdx = content.indexOf(HOOK_START);
|
|
932
1158
|
const endIdx = content.indexOf(HOOK_END) + HOOK_END.length;
|
|
933
|
-
|
|
1159
|
+
import_fs10.default.writeFileSync(rcFile, content.slice(0, startIdx) + hookContent + content.slice(endIdx));
|
|
934
1160
|
}
|
|
935
1161
|
function removeHook(rcFile) {
|
|
936
1162
|
const backup = `${rcFile}.workscribe-backup-${Date.now()}`;
|
|
937
|
-
|
|
938
|
-
log.info(`Backed up ${rcFile} \u2192 ${
|
|
939
|
-
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");
|
|
940
1166
|
const startIdx = content.indexOf(HOOK_START);
|
|
941
1167
|
const endIdx = content.indexOf(HOOK_END) + HOOK_END.length;
|
|
942
1168
|
const preambleIdx = content.lastIndexOf(HOOK_PREAMBLE, startIdx);
|
|
943
1169
|
const blockStart = preambleIdx !== -1 ? preambleIdx : startIdx;
|
|
944
1170
|
const before = content.slice(0, blockStart).replace(/\n+$/, "\n");
|
|
945
1171
|
const after = content.slice(endIdx).replace(/^\n/, "");
|
|
946
|
-
|
|
1172
|
+
import_fs10.default.writeFileSync(rcFile, before + after);
|
|
947
1173
|
}
|
|
948
1174
|
|
|
949
1175
|
// src/capture/hooks/claude-code.ts
|
|
950
|
-
var
|
|
951
|
-
var
|
|
1176
|
+
var import_fs11 = __toESM(require("fs"));
|
|
1177
|
+
var import_path8 = __toESM(require("path"));
|
|
952
1178
|
var import_os3 = __toESM(require("os"));
|
|
953
1179
|
|
|
954
1180
|
// src/capture/hooks/utils.ts
|
|
@@ -963,11 +1189,11 @@ function isCommandInstalled(cmd) {
|
|
|
963
1189
|
}
|
|
964
1190
|
|
|
965
1191
|
// src/capture/hooks/claude-code.ts
|
|
966
|
-
var CLAUDE_SETTINGS_PATH =
|
|
1192
|
+
var CLAUDE_SETTINGS_PATH = import_path8.default.join(import_os3.default.homedir(), ".claude", "settings.json");
|
|
967
1193
|
function readSettings() {
|
|
968
|
-
if (!
|
|
1194
|
+
if (!import_fs11.default.existsSync(CLAUDE_SETTINGS_PATH)) return {};
|
|
969
1195
|
try {
|
|
970
|
-
return JSON.parse(
|
|
1196
|
+
return JSON.parse(import_fs11.default.readFileSync(CLAUDE_SETTINGS_PATH, "utf-8"));
|
|
971
1197
|
} catch {
|
|
972
1198
|
return {};
|
|
973
1199
|
}
|
|
@@ -1002,8 +1228,8 @@ var claudeCodeIntegration = {
|
|
|
1002
1228
|
}
|
|
1003
1229
|
]
|
|
1004
1230
|
};
|
|
1005
|
-
|
|
1006
|
-
|
|
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));
|
|
1007
1233
|
}
|
|
1008
1234
|
};
|
|
1009
1235
|
|
|
@@ -1015,22 +1241,25 @@ var AI_HOOK_INTEGRATIONS = {
|
|
|
1015
1241
|
// src/cli/init.ts
|
|
1016
1242
|
function detectShell() {
|
|
1017
1243
|
const shell = process.env.SHELL ?? "";
|
|
1018
|
-
if (shell.includes("zsh")) return { name: "zsh", rcFile:
|
|
1019
|
-
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") };
|
|
1020
1246
|
return null;
|
|
1021
1247
|
}
|
|
1022
1248
|
function registerInit(program2) {
|
|
1023
1249
|
program2.command("init").description("Set up Workscribe and install or update shell hooks").action(async () => {
|
|
1024
1250
|
log.plain(import_chalk2.default.bold("\n Workscribe \u2014 Developer Work Journal\n"));
|
|
1025
|
-
if (!
|
|
1026
|
-
|
|
1251
|
+
if (!import_fs12.default.existsSync(WORKSCRIBE_DIR)) {
|
|
1252
|
+
import_fs12.default.mkdirSync(WORKSCRIBE_DIR, { recursive: true, mode: 448 });
|
|
1027
1253
|
log.success(`Created ${WORKSCRIBE_DIR}`);
|
|
1028
1254
|
} else {
|
|
1029
1255
|
try {
|
|
1030
|
-
|
|
1256
|
+
import_fs12.default.chmodSync(WORKSCRIBE_DIR, 448);
|
|
1031
1257
|
} catch {
|
|
1032
1258
|
}
|
|
1033
1259
|
}
|
|
1260
|
+
if (!import_fs12.default.existsSync(PROFILES_DIR)) {
|
|
1261
|
+
import_fs12.default.mkdirSync(PROFILES_DIR, { recursive: true, mode: 448 });
|
|
1262
|
+
}
|
|
1034
1263
|
getDb();
|
|
1035
1264
|
log.success("Database initialized");
|
|
1036
1265
|
const shell = detectShell();
|
|
@@ -1038,12 +1267,12 @@ function registerInit(program2) {
|
|
|
1038
1267
|
log.warn("Could not detect shell. Supported: zsh, bash");
|
|
1039
1268
|
log.plain(`Manually add the hook from: ${import_chalk2.default.cyan("shell/hook.zsh")} or ${import_chalk2.default.cyan("shell/hook.bash")}`);
|
|
1040
1269
|
} else {
|
|
1041
|
-
const hookFile =
|
|
1042
|
-
if (!
|
|
1270
|
+
const hookFile = import_path9.default.join(__dirname, "..", "shell", `hook.${shell.name}`);
|
|
1271
|
+
if (!import_fs12.default.existsSync(hookFile)) {
|
|
1043
1272
|
log.warn(`Hook template not found at ${hookFile}`);
|
|
1044
1273
|
} else {
|
|
1045
|
-
const hookContent =
|
|
1046
|
-
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, "");
|
|
1047
1276
|
if (!isHookInstalled(shell.rcFile)) {
|
|
1048
1277
|
installHook(shell.rcFile, hookContent);
|
|
1049
1278
|
log.success(`Shell hook installed in ${shell.rcFile}`);
|
|
@@ -1092,6 +1321,33 @@ function registerInit(program2) {
|
|
|
1092
1321
|
}
|
|
1093
1322
|
}
|
|
1094
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
|
+
}
|
|
1095
1351
|
if (config.ai.provider) {
|
|
1096
1352
|
log.info(`AI provider already configured: ${import_chalk2.default.cyan(config.ai.provider)}`);
|
|
1097
1353
|
} else {
|
|
@@ -1139,7 +1395,7 @@ function registerInit(program2) {
|
|
|
1139
1395
|
log.info("Skipped \u2014 run later: workscribe config set ai.provider <provider>");
|
|
1140
1396
|
}
|
|
1141
1397
|
}
|
|
1142
|
-
if (!
|
|
1398
|
+
if (!import_fs12.default.existsSync(CONFIG_PATH)) {
|
|
1143
1399
|
writeConfig(config);
|
|
1144
1400
|
log.success("Config file created");
|
|
1145
1401
|
}
|
|
@@ -1158,7 +1414,7 @@ function registerInit(program2) {
|
|
|
1158
1414
|
}
|
|
1159
1415
|
|
|
1160
1416
|
// src/cli/status.ts
|
|
1161
|
-
var
|
|
1417
|
+
var import_fs13 = __toESM(require("fs"));
|
|
1162
1418
|
var import_chalk3 = __toESM(require("chalk"));
|
|
1163
1419
|
|
|
1164
1420
|
// src/utils/format.ts
|
|
@@ -1216,7 +1472,7 @@ function registerStatus(program2) {
|
|
|
1216
1472
|
const total = getEventCount();
|
|
1217
1473
|
const todayEvents = getEventsForDate(todayDate());
|
|
1218
1474
|
const last = getLastEvent();
|
|
1219
|
-
const isPaused =
|
|
1475
|
+
const isPaused = import_fs13.default.existsSync(PAUSE_FLAG_PATH);
|
|
1220
1476
|
log.plain("");
|
|
1221
1477
|
log.plain(import_chalk3.default.bold(" Workscribe Status"));
|
|
1222
1478
|
log.plain("");
|
|
@@ -1235,6 +1491,25 @@ function registerStatus(program2) {
|
|
|
1235
1491
|
` AI provider ${config.ai.provider ? import_chalk3.default.cyan(config.ai.provider) : import_chalk3.default.yellow("not configured")}`
|
|
1236
1492
|
);
|
|
1237
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
|
+
}
|
|
1238
1513
|
log.plain("");
|
|
1239
1514
|
});
|
|
1240
1515
|
}
|
|
@@ -1242,26 +1517,19 @@ function registerStatus(program2) {
|
|
|
1242
1517
|
// src/cli/events.ts
|
|
1243
1518
|
var import_chalk4 = __toESM(require("chalk"));
|
|
1244
1519
|
var CATEGORY_COLORS = {
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
build: import_chalk4.default.cyan,
|
|
1253
|
-
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,
|
|
1254
1527
|
infrastructure: import_chalk4.default.red,
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
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,
|
|
1260
1532
|
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
1533
|
other: import_chalk4.default.gray
|
|
1266
1534
|
};
|
|
1267
1535
|
var AI_TOOL_DISPLAY_NAMES = Object.fromEntries(
|
|
@@ -1454,9 +1722,9 @@ function registerSessions(program2) {
|
|
|
1454
1722
|
|
|
1455
1723
|
// src/cli/summary.ts
|
|
1456
1724
|
var import_chalk6 = __toESM(require("chalk"));
|
|
1457
|
-
var
|
|
1725
|
+
var import_fs14 = __toESM(require("fs"));
|
|
1458
1726
|
var import_os5 = __toESM(require("os"));
|
|
1459
|
-
var
|
|
1727
|
+
var import_path10 = __toESM(require("path"));
|
|
1460
1728
|
|
|
1461
1729
|
// src/ai/prompt.ts
|
|
1462
1730
|
var SYSTEM_PROMPT = `You write short developer work summaries. Here is an example:
|
|
@@ -1466,27 +1734,34 @@ Project: api-service
|
|
|
1466
1734
|
Branch: feature/auth
|
|
1467
1735
|
Duration: 2h 00m
|
|
1468
1736
|
Events:
|
|
1469
|
-
-
|
|
1470
|
-
- 2x
|
|
1471
|
-
- 2x
|
|
1472
|
-
-
|
|
1473
|
-
- 2x
|
|
1474
|
-
-
|
|
1475
|
-
-
|
|
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"}
|
|
1476
1744
|
|
|
1477
1745
|
OUTPUT:
|
|
1478
1746
|
- Switched to the authentication feature branch and added authentication dependencies (bcrypt, JWT)
|
|
1479
1747
|
- Resolved failing auth tests
|
|
1480
1748
|
- Committed and pushed authentication fixes
|
|
1481
1749
|
|
|
1482
|
-
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.
|
|
1483
1751
|
|
|
1484
|
-
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).
|
|
1485
1753
|
|
|
1486
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.
|
|
1487
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
|
+
|
|
1488
1758
|
Maximum 5 bullets. Output ONLY the bullet points, nothing else.`;
|
|
1489
|
-
|
|
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"]);
|
|
1490
1765
|
function buildUserMessage(payload) {
|
|
1491
1766
|
const lines = [`Date: ${payload.date}`, ""];
|
|
1492
1767
|
for (const session of payload.sessions) {
|
|
@@ -1613,7 +1888,7 @@ var AnthropicProvider = class {
|
|
|
1613
1888
|
body: JSON.stringify({
|
|
1614
1889
|
model: this.model,
|
|
1615
1890
|
max_tokens: 1024,
|
|
1616
|
-
system:
|
|
1891
|
+
system: getSystemPrompt(),
|
|
1617
1892
|
messages: [{ role: "user", content: buildUserMessage(payload) }]
|
|
1618
1893
|
})
|
|
1619
1894
|
});
|
|
@@ -1668,7 +1943,7 @@ var OpenAIProvider = class {
|
|
|
1668
1943
|
body: JSON.stringify({
|
|
1669
1944
|
model: this.model,
|
|
1670
1945
|
messages: [
|
|
1671
|
-
{ role: "system", content:
|
|
1946
|
+
{ role: "system", content: getSystemPrompt() },
|
|
1672
1947
|
{ role: "user", content: buildUserMessage(payload) }
|
|
1673
1948
|
],
|
|
1674
1949
|
max_tokens: 1024
|
|
@@ -1722,7 +1997,7 @@ var OllamaProvider = class {
|
|
|
1722
1997
|
model: this.model,
|
|
1723
1998
|
stream: false,
|
|
1724
1999
|
messages: [
|
|
1725
|
-
{ role: "system", content:
|
|
2000
|
+
{ role: "system", content: getSystemPrompt() },
|
|
1726
2001
|
{ role: "user", content: buildUserMessage(payload) }
|
|
1727
2002
|
],
|
|
1728
2003
|
options: {
|
|
@@ -1904,9 +2179,9 @@ function registerSummary(program2) {
|
|
|
1904
2179
|
return;
|
|
1905
2180
|
}
|
|
1906
2181
|
const cacheKey = isoWeekKey(today);
|
|
1907
|
-
const cacheFile =
|
|
1908
|
-
if (!opts.fresh &&
|
|
1909
|
-
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");
|
|
1910
2185
|
log.plain("");
|
|
1911
2186
|
log.plain(cached);
|
|
1912
2187
|
log.plain("");
|
|
@@ -1922,8 +2197,8 @@ function registerSummary(program2) {
|
|
|
1922
2197
|
log.plain("");
|
|
1923
2198
|
log.plain(summary);
|
|
1924
2199
|
log.plain("");
|
|
1925
|
-
|
|
1926
|
-
|
|
2200
|
+
import_fs14.default.mkdirSync(WEEKLY_CACHE_DIR, { recursive: true });
|
|
2201
|
+
import_fs14.default.writeFileSync(cacheFile, summary, { encoding: "utf-8", mode: 384 });
|
|
1927
2202
|
await handleWebhook(opts, config, summary);
|
|
1928
2203
|
} catch (e) {
|
|
1929
2204
|
log.error(`Weekly summary failed [${config.ai.provider}]: ${e.message}`);
|
|
@@ -2010,8 +2285,8 @@ ${bullets.trim()}`;
|
|
|
2010
2285
|
}
|
|
2011
2286
|
|
|
2012
2287
|
// src/cli/export.ts
|
|
2013
|
-
var
|
|
2014
|
-
var
|
|
2288
|
+
var import_fs15 = __toESM(require("fs"));
|
|
2289
|
+
var import_path11 = __toESM(require("path"));
|
|
2015
2290
|
function eventRowToPayload2(row) {
|
|
2016
2291
|
let metadata = {};
|
|
2017
2292
|
try {
|
|
@@ -2067,9 +2342,9 @@ function registerExport(program2) {
|
|
|
2067
2342
|
const payload = { date, sessions: sessionPayloads };
|
|
2068
2343
|
log.info(`Generating summary via ${config.ai.provider}\u2026`);
|
|
2069
2344
|
const summary = await provider.summarize(payload);
|
|
2070
|
-
const outputPath = opts.output ??
|
|
2071
|
-
if (!
|
|
2072
|
-
|
|
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}
|
|
2073
2348
|
|
|
2074
2349
|
${summary}
|
|
2075
2350
|
`, { encoding: "utf-8", mode: 384 });
|
|
@@ -2141,22 +2416,22 @@ function printConfig(obj, indent) {
|
|
|
2141
2416
|
}
|
|
2142
2417
|
|
|
2143
2418
|
// src/cli/pause.ts
|
|
2144
|
-
var
|
|
2419
|
+
var import_fs16 = __toESM(require("fs"));
|
|
2145
2420
|
function registerPause(program2) {
|
|
2146
2421
|
program2.command("pause").description("Pause capture temporarily").action(() => {
|
|
2147
|
-
if (
|
|
2422
|
+
if (import_fs16.default.existsSync(PAUSE_FLAG_PATH)) {
|
|
2148
2423
|
log.info("Capture is already paused. Run workscribe resume to restart.");
|
|
2149
2424
|
return;
|
|
2150
2425
|
}
|
|
2151
|
-
|
|
2426
|
+
import_fs16.default.writeFileSync(PAUSE_FLAG_PATH, "", { mode: 384 });
|
|
2152
2427
|
log.success("Capture paused. Run workscribe resume to restart.");
|
|
2153
2428
|
});
|
|
2154
2429
|
program2.command("resume").description("Resume capture after a pause").action(() => {
|
|
2155
|
-
if (!
|
|
2430
|
+
if (!import_fs16.default.existsSync(PAUSE_FLAG_PATH)) {
|
|
2156
2431
|
log.info("Capture is not paused.");
|
|
2157
2432
|
return;
|
|
2158
2433
|
}
|
|
2159
|
-
|
|
2434
|
+
import_fs16.default.rmSync(PAUSE_FLAG_PATH);
|
|
2160
2435
|
log.success("Capture resumed.");
|
|
2161
2436
|
});
|
|
2162
2437
|
}
|
|
@@ -2241,18 +2516,18 @@ function registerClear(program2) {
|
|
|
2241
2516
|
}
|
|
2242
2517
|
|
|
2243
2518
|
// src/cli/uninstall.ts
|
|
2244
|
-
var
|
|
2245
|
-
var
|
|
2519
|
+
var import_fs17 = __toESM(require("fs"));
|
|
2520
|
+
var import_path12 = __toESM(require("path"));
|
|
2246
2521
|
var import_os6 = __toESM(require("os"));
|
|
2247
2522
|
var import_prompts3 = __toESM(require("prompts"));
|
|
2248
2523
|
var import_chalk9 = __toESM(require("chalk"));
|
|
2249
2524
|
function detectShellRcFiles() {
|
|
2250
2525
|
const candidates = [
|
|
2251
|
-
{ name: "zsh", rcFile:
|
|
2252
|
-
{ name: "bash", rcFile:
|
|
2253
|
-
{ 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") }
|
|
2254
2529
|
];
|
|
2255
|
-
return candidates.filter((candidate) =>
|
|
2530
|
+
return candidates.filter((candidate) => import_fs17.default.existsSync(candidate.rcFile) && isHookInstalled(candidate.rcFile));
|
|
2256
2531
|
}
|
|
2257
2532
|
function registerUninstall(program2) {
|
|
2258
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) => {
|
|
@@ -2271,7 +2546,7 @@ function registerUninstall(program2) {
|
|
|
2271
2546
|
}
|
|
2272
2547
|
}
|
|
2273
2548
|
if (opts.hooksOnly) return;
|
|
2274
|
-
if (
|
|
2549
|
+
if (import_fs17.default.existsSync(WORKSCRIBE_DIR)) {
|
|
2275
2550
|
log.plain("");
|
|
2276
2551
|
const { deleteData } = await (0, import_prompts3.default)({
|
|
2277
2552
|
type: "confirm",
|
|
@@ -2280,7 +2555,7 @@ function registerUninstall(program2) {
|
|
|
2280
2555
|
initial: false
|
|
2281
2556
|
});
|
|
2282
2557
|
if (deleteData) {
|
|
2283
|
-
|
|
2558
|
+
import_fs17.default.rmSync(WORKSCRIBE_DIR, { recursive: true, force: true });
|
|
2284
2559
|
log.success(`Deleted ${WORKSCRIBE_DIR}`);
|
|
2285
2560
|
} else {
|
|
2286
2561
|
log.info(`Data kept at ${WORKSCRIBE_DIR}`);
|
|
@@ -2292,9 +2567,182 @@ function registerUninstall(program2) {
|
|
|
2292
2567
|
});
|
|
2293
2568
|
}
|
|
2294
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
|
+
|
|
2295
2743
|
// src/index.ts
|
|
2296
2744
|
var program = new import_commander.Command();
|
|
2297
|
-
program.name("workscribe").description("Automatic developer work journal").version("1.
|
|
2745
|
+
program.name("workscribe").description("Automatic developer work journal").version("1.10.0");
|
|
2298
2746
|
registerCapture(program);
|
|
2299
2747
|
registerHook(program);
|
|
2300
2748
|
registerInit(program);
|
|
@@ -2308,5 +2756,6 @@ registerPause(program);
|
|
|
2308
2756
|
registerUndo(program);
|
|
2309
2757
|
registerClear(program);
|
|
2310
2758
|
registerUninstall(program);
|
|
2759
|
+
registerProfile(program);
|
|
2311
2760
|
program.parse();
|
|
2312
2761
|
//# sourceMappingURL=index.js.map
|