@rely-ai/caliber 1.24.1-dev.1773872765 → 1.25.0-dev.1773915517
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/bin.js +1754 -888
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -20,6 +20,41 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
20
20
|
};
|
|
21
21
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
22
22
|
|
|
23
|
+
// src/constants.ts
|
|
24
|
+
var constants_exports = {};
|
|
25
|
+
__export(constants_exports, {
|
|
26
|
+
AUTH_DIR: () => AUTH_DIR,
|
|
27
|
+
BACKUPS_DIR: () => BACKUPS_DIR,
|
|
28
|
+
CALIBER_DIR: () => CALIBER_DIR,
|
|
29
|
+
LEARNING_DIR: () => LEARNING_DIR,
|
|
30
|
+
LEARNING_MAX_EVENTS: () => LEARNING_MAX_EVENTS,
|
|
31
|
+
LEARNING_ROI_FILE: () => LEARNING_ROI_FILE,
|
|
32
|
+
LEARNING_SESSION_FILE: () => LEARNING_SESSION_FILE,
|
|
33
|
+
LEARNING_STATE_FILE: () => LEARNING_STATE_FILE,
|
|
34
|
+
MANIFEST_FILE: () => MANIFEST_FILE,
|
|
35
|
+
MIN_SESSIONS_FOR_COMPARISON: () => MIN_SESSIONS_FOR_COMPARISON,
|
|
36
|
+
PERSONAL_LEARNINGS_FILE: () => PERSONAL_LEARNINGS_FILE
|
|
37
|
+
});
|
|
38
|
+
import path2 from "path";
|
|
39
|
+
import os from "os";
|
|
40
|
+
var AUTH_DIR, CALIBER_DIR, MANIFEST_FILE, BACKUPS_DIR, LEARNING_DIR, LEARNING_SESSION_FILE, LEARNING_STATE_FILE, LEARNING_MAX_EVENTS, LEARNING_ROI_FILE, PERSONAL_LEARNINGS_FILE, MIN_SESSIONS_FOR_COMPARISON;
|
|
41
|
+
var init_constants = __esm({
|
|
42
|
+
"src/constants.ts"() {
|
|
43
|
+
"use strict";
|
|
44
|
+
AUTH_DIR = path2.join(os.homedir(), ".caliber");
|
|
45
|
+
CALIBER_DIR = ".caliber";
|
|
46
|
+
MANIFEST_FILE = path2.join(CALIBER_DIR, "manifest.json");
|
|
47
|
+
BACKUPS_DIR = path2.join(CALIBER_DIR, "backups");
|
|
48
|
+
LEARNING_DIR = path2.join(CALIBER_DIR, "learning");
|
|
49
|
+
LEARNING_SESSION_FILE = "current-session.jsonl";
|
|
50
|
+
LEARNING_STATE_FILE = "state.json";
|
|
51
|
+
LEARNING_MAX_EVENTS = 500;
|
|
52
|
+
LEARNING_ROI_FILE = "roi-stats.json";
|
|
53
|
+
PERSONAL_LEARNINGS_FILE = path2.join(AUTH_DIR, "personal-learnings.md");
|
|
54
|
+
MIN_SESSIONS_FOR_COMPARISON = 3;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
23
58
|
// src/llm/config.ts
|
|
24
59
|
var config_exports = {};
|
|
25
60
|
__export(config_exports, {
|
|
@@ -36,8 +71,8 @@ __export(config_exports, {
|
|
|
36
71
|
writeConfigFile: () => writeConfigFile
|
|
37
72
|
});
|
|
38
73
|
import fs4 from "fs";
|
|
39
|
-
import
|
|
40
|
-
import
|
|
74
|
+
import path5 from "path";
|
|
75
|
+
import os2 from "os";
|
|
41
76
|
function getMaxPromptTokens() {
|
|
42
77
|
const config = loadConfig();
|
|
43
78
|
const model = process.env.CALIBER_MODEL || config?.model;
|
|
@@ -136,8 +171,8 @@ var CONFIG_DIR, CONFIG_FILE, DEFAULT_MODELS, MODEL_CONTEXT_WINDOWS, DEFAULT_CONT
|
|
|
136
171
|
var init_config = __esm({
|
|
137
172
|
"src/llm/config.ts"() {
|
|
138
173
|
"use strict";
|
|
139
|
-
CONFIG_DIR =
|
|
140
|
-
CONFIG_FILE =
|
|
174
|
+
CONFIG_DIR = path5.join(os2.homedir(), ".caliber");
|
|
175
|
+
CONFIG_FILE = path5.join(CONFIG_DIR, "config.json");
|
|
141
176
|
DEFAULT_MODELS = {
|
|
142
177
|
anthropic: "claude-sonnet-4-6",
|
|
143
178
|
vertex: "claude-sonnet-4-6",
|
|
@@ -185,44 +220,13 @@ var init_types = __esm({
|
|
|
185
220
|
}
|
|
186
221
|
});
|
|
187
222
|
|
|
188
|
-
// src/constants.ts
|
|
189
|
-
var constants_exports = {};
|
|
190
|
-
__export(constants_exports, {
|
|
191
|
-
AUTH_DIR: () => AUTH_DIR,
|
|
192
|
-
BACKUPS_DIR: () => BACKUPS_DIR,
|
|
193
|
-
CALIBER_DIR: () => CALIBER_DIR,
|
|
194
|
-
LEARNING_DIR: () => LEARNING_DIR,
|
|
195
|
-
LEARNING_MAX_EVENTS: () => LEARNING_MAX_EVENTS,
|
|
196
|
-
LEARNING_ROI_FILE: () => LEARNING_ROI_FILE,
|
|
197
|
-
LEARNING_SESSION_FILE: () => LEARNING_SESSION_FILE,
|
|
198
|
-
LEARNING_STATE_FILE: () => LEARNING_STATE_FILE,
|
|
199
|
-
MANIFEST_FILE: () => MANIFEST_FILE
|
|
200
|
-
});
|
|
201
|
-
import path10 from "path";
|
|
202
|
-
import os3 from "os";
|
|
203
|
-
var AUTH_DIR, CALIBER_DIR, MANIFEST_FILE, BACKUPS_DIR, LEARNING_DIR, LEARNING_SESSION_FILE, LEARNING_STATE_FILE, LEARNING_MAX_EVENTS, LEARNING_ROI_FILE;
|
|
204
|
-
var init_constants = __esm({
|
|
205
|
-
"src/constants.ts"() {
|
|
206
|
-
"use strict";
|
|
207
|
-
AUTH_DIR = path10.join(os3.homedir(), ".caliber");
|
|
208
|
-
CALIBER_DIR = ".caliber";
|
|
209
|
-
MANIFEST_FILE = path10.join(CALIBER_DIR, "manifest.json");
|
|
210
|
-
BACKUPS_DIR = path10.join(CALIBER_DIR, "backups");
|
|
211
|
-
LEARNING_DIR = path10.join(CALIBER_DIR, "learning");
|
|
212
|
-
LEARNING_SESSION_FILE = "current-session.jsonl";
|
|
213
|
-
LEARNING_STATE_FILE = "state.json";
|
|
214
|
-
LEARNING_MAX_EVENTS = 500;
|
|
215
|
-
LEARNING_ROI_FILE = "roi-stats.json";
|
|
216
|
-
}
|
|
217
|
-
});
|
|
218
|
-
|
|
219
223
|
// src/lib/resolve-caliber.ts
|
|
220
224
|
var resolve_caliber_exports = {};
|
|
221
225
|
__export(resolve_caliber_exports, {
|
|
222
226
|
isCaliberCommand: () => isCaliberCommand,
|
|
223
227
|
resolveCaliber: () => resolveCaliber
|
|
224
228
|
});
|
|
225
|
-
import
|
|
229
|
+
import fs18 from "fs";
|
|
226
230
|
import { execSync as execSync6 } from "child_process";
|
|
227
231
|
function resolveCaliber() {
|
|
228
232
|
if (_resolved) return _resolved;
|
|
@@ -244,7 +248,7 @@ function resolveCaliber() {
|
|
|
244
248
|
} catch {
|
|
245
249
|
}
|
|
246
250
|
const binPath = process.argv[1];
|
|
247
|
-
if (binPath &&
|
|
251
|
+
if (binPath && fs18.existsSync(binPath)) {
|
|
248
252
|
_resolved = binPath;
|
|
249
253
|
return _resolved;
|
|
250
254
|
}
|
|
@@ -268,13 +272,13 @@ var init_resolve_caliber = __esm({
|
|
|
268
272
|
|
|
269
273
|
// src/utils/editor.ts
|
|
270
274
|
import { execSync as execSync13, spawn as spawn3 } from "child_process";
|
|
271
|
-
import
|
|
272
|
-
import
|
|
275
|
+
import fs26 from "fs";
|
|
276
|
+
import path22 from "path";
|
|
273
277
|
import os5 from "os";
|
|
274
278
|
function getEmptyFilePath(proposedPath) {
|
|
275
|
-
|
|
276
|
-
const tempPath =
|
|
277
|
-
|
|
279
|
+
fs26.mkdirSync(DIFF_TEMP_DIR, { recursive: true });
|
|
280
|
+
const tempPath = path22.join(DIFF_TEMP_DIR, path22.basename(proposedPath));
|
|
281
|
+
fs26.writeFileSync(tempPath, "");
|
|
278
282
|
return tempPath;
|
|
279
283
|
}
|
|
280
284
|
function commandExists(cmd) {
|
|
@@ -314,7 +318,7 @@ var init_editor = __esm({
|
|
|
314
318
|
"src/utils/editor.ts"() {
|
|
315
319
|
"use strict";
|
|
316
320
|
IS_WINDOWS3 = process.platform === "win32";
|
|
317
|
-
DIFF_TEMP_DIR =
|
|
321
|
+
DIFF_TEMP_DIR = path22.join(os5.tmpdir(), "caliber-diff");
|
|
318
322
|
}
|
|
319
323
|
});
|
|
320
324
|
|
|
@@ -326,7 +330,7 @@ __export(review_exports, {
|
|
|
326
330
|
promptWantsReview: () => promptWantsReview
|
|
327
331
|
});
|
|
328
332
|
import chalk10 from "chalk";
|
|
329
|
-
import
|
|
333
|
+
import fs27 from "fs";
|
|
330
334
|
import select4 from "@inquirer/select";
|
|
331
335
|
import { createTwoFilesPatch } from "diff";
|
|
332
336
|
async function promptWantsReview() {
|
|
@@ -363,8 +367,8 @@ async function openReview(method, stagedFiles) {
|
|
|
363
367
|
return;
|
|
364
368
|
}
|
|
365
369
|
const fileInfos = stagedFiles.map((file) => {
|
|
366
|
-
const proposed =
|
|
367
|
-
const current = file.currentPath ?
|
|
370
|
+
const proposed = fs27.readFileSync(file.proposedPath, "utf-8");
|
|
371
|
+
const current = file.currentPath ? fs27.readFileSync(file.currentPath, "utf-8") : "";
|
|
368
372
|
const patch = createTwoFilesPatch(
|
|
369
373
|
file.isNew ? "/dev/null" : file.relativePath,
|
|
370
374
|
file.relativePath,
|
|
@@ -539,13 +543,13 @@ __export(lock_exports, {
|
|
|
539
543
|
isCaliberRunning: () => isCaliberRunning,
|
|
540
544
|
releaseLock: () => releaseLock
|
|
541
545
|
});
|
|
542
|
-
import
|
|
543
|
-
import
|
|
546
|
+
import fs36 from "fs";
|
|
547
|
+
import path28 from "path";
|
|
544
548
|
import os7 from "os";
|
|
545
549
|
function isCaliberRunning() {
|
|
546
550
|
try {
|
|
547
|
-
if (!
|
|
548
|
-
const raw =
|
|
551
|
+
if (!fs36.existsSync(LOCK_FILE)) return false;
|
|
552
|
+
const raw = fs36.readFileSync(LOCK_FILE, "utf-8").trim();
|
|
549
553
|
const { pid, ts } = JSON.parse(raw);
|
|
550
554
|
if (Date.now() - ts > STALE_MS) return false;
|
|
551
555
|
try {
|
|
@@ -560,13 +564,13 @@ function isCaliberRunning() {
|
|
|
560
564
|
}
|
|
561
565
|
function acquireLock() {
|
|
562
566
|
try {
|
|
563
|
-
|
|
567
|
+
fs36.writeFileSync(LOCK_FILE, JSON.stringify({ pid: process.pid, ts: Date.now() }));
|
|
564
568
|
} catch {
|
|
565
569
|
}
|
|
566
570
|
}
|
|
567
571
|
function releaseLock() {
|
|
568
572
|
try {
|
|
569
|
-
if (
|
|
573
|
+
if (fs36.existsSync(LOCK_FILE)) fs36.unlinkSync(LOCK_FILE);
|
|
570
574
|
} catch {
|
|
571
575
|
}
|
|
572
576
|
}
|
|
@@ -574,25 +578,25 @@ var LOCK_FILE, STALE_MS;
|
|
|
574
578
|
var init_lock = __esm({
|
|
575
579
|
"src/lib/lock.ts"() {
|
|
576
580
|
"use strict";
|
|
577
|
-
LOCK_FILE =
|
|
581
|
+
LOCK_FILE = path28.join(os7.tmpdir(), ".caliber.lock");
|
|
578
582
|
STALE_MS = 10 * 60 * 1e3;
|
|
579
583
|
}
|
|
580
584
|
});
|
|
581
585
|
|
|
582
586
|
// src/cli.ts
|
|
583
587
|
import { Command } from "commander";
|
|
584
|
-
import
|
|
585
|
-
import
|
|
588
|
+
import fs45 from "fs";
|
|
589
|
+
import path35 from "path";
|
|
586
590
|
import { fileURLToPath } from "url";
|
|
587
591
|
|
|
588
592
|
// src/commands/init.ts
|
|
589
|
-
import
|
|
593
|
+
import path24 from "path";
|
|
590
594
|
import chalk14 from "chalk";
|
|
591
|
-
import
|
|
595
|
+
import fs31 from "fs";
|
|
592
596
|
|
|
593
597
|
// src/fingerprint/index.ts
|
|
594
598
|
import fs7 from "fs";
|
|
595
|
-
import
|
|
599
|
+
import path7 from "path";
|
|
596
600
|
|
|
597
601
|
// src/fingerprint/git.ts
|
|
598
602
|
import { execSync } from "child_process";
|
|
@@ -607,10 +611,14 @@ function isInsideGitRepo() {
|
|
|
607
611
|
dir = parent;
|
|
608
612
|
}
|
|
609
613
|
}
|
|
610
|
-
function getGitRemoteUrl() {
|
|
611
|
-
if (!isInsideGitRepo()) return void 0;
|
|
614
|
+
function getGitRemoteUrl(cwd) {
|
|
615
|
+
if (!cwd && !isInsideGitRepo()) return void 0;
|
|
612
616
|
try {
|
|
613
|
-
return execSync("git remote get-url origin", {
|
|
617
|
+
return execSync("git remote get-url origin", {
|
|
618
|
+
encoding: "utf-8",
|
|
619
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
620
|
+
...cwd ? { cwd, timeout: 3e3 } : {}
|
|
621
|
+
}).trim();
|
|
614
622
|
} catch {
|
|
615
623
|
return void 0;
|
|
616
624
|
}
|
|
@@ -696,37 +704,38 @@ function scan(base, rel, depth, maxDepth, result) {
|
|
|
696
704
|
}
|
|
697
705
|
|
|
698
706
|
// src/fingerprint/existing-config.ts
|
|
707
|
+
init_constants();
|
|
699
708
|
import fs2 from "fs";
|
|
700
|
-
import
|
|
709
|
+
import path3 from "path";
|
|
701
710
|
function readExistingConfigs(dir) {
|
|
702
711
|
const configs = {};
|
|
703
|
-
const readmeMdPath =
|
|
712
|
+
const readmeMdPath = path3.join(dir, "README.md");
|
|
704
713
|
if (fs2.existsSync(readmeMdPath)) {
|
|
705
714
|
configs.readmeMd = fs2.readFileSync(readmeMdPath, "utf-8");
|
|
706
715
|
}
|
|
707
|
-
const agentsMdPath =
|
|
716
|
+
const agentsMdPath = path3.join(dir, "AGENTS.md");
|
|
708
717
|
if (fs2.existsSync(agentsMdPath)) {
|
|
709
718
|
configs.agentsMd = fs2.readFileSync(agentsMdPath, "utf-8");
|
|
710
719
|
}
|
|
711
|
-
const claudeMdPath =
|
|
720
|
+
const claudeMdPath = path3.join(dir, "CLAUDE.md");
|
|
712
721
|
if (fs2.existsSync(claudeMdPath)) {
|
|
713
722
|
configs.claudeMd = fs2.readFileSync(claudeMdPath, "utf-8");
|
|
714
723
|
}
|
|
715
|
-
const claudeSettingsPath =
|
|
724
|
+
const claudeSettingsPath = path3.join(dir, ".claude", "settings.json");
|
|
716
725
|
if (fs2.existsSync(claudeSettingsPath)) {
|
|
717
726
|
try {
|
|
718
727
|
configs.claudeSettings = JSON.parse(fs2.readFileSync(claudeSettingsPath, "utf-8"));
|
|
719
728
|
} catch {
|
|
720
729
|
}
|
|
721
730
|
}
|
|
722
|
-
const skillsDir =
|
|
731
|
+
const skillsDir = path3.join(dir, ".claude", "skills");
|
|
723
732
|
if (fs2.existsSync(skillsDir)) {
|
|
724
733
|
try {
|
|
725
734
|
const entries = fs2.readdirSync(skillsDir);
|
|
726
735
|
const skills = [];
|
|
727
736
|
for (const entry of entries) {
|
|
728
|
-
const entryPath =
|
|
729
|
-
const skillMdPath =
|
|
737
|
+
const entryPath = path3.join(skillsDir, entry);
|
|
738
|
+
const skillMdPath = path3.join(entryPath, "SKILL.md");
|
|
730
739
|
if (fs2.statSync(entryPath).isDirectory() && fs2.existsSync(skillMdPath)) {
|
|
731
740
|
skills.push({ filename: `${entry}/SKILL.md`, content: fs2.readFileSync(skillMdPath, "utf-8") });
|
|
732
741
|
} else if (entry.endsWith(".md")) {
|
|
@@ -737,36 +746,36 @@ function readExistingConfigs(dir) {
|
|
|
737
746
|
} catch {
|
|
738
747
|
}
|
|
739
748
|
}
|
|
740
|
-
const cursorrulesPath =
|
|
749
|
+
const cursorrulesPath = path3.join(dir, ".cursorrules");
|
|
741
750
|
if (fs2.existsSync(cursorrulesPath)) {
|
|
742
751
|
configs.cursorrules = fs2.readFileSync(cursorrulesPath, "utf-8");
|
|
743
752
|
}
|
|
744
|
-
const cursorRulesDir =
|
|
753
|
+
const cursorRulesDir = path3.join(dir, ".cursor", "rules");
|
|
745
754
|
if (fs2.existsSync(cursorRulesDir)) {
|
|
746
755
|
try {
|
|
747
756
|
const files = fs2.readdirSync(cursorRulesDir).filter((f) => f.endsWith(".mdc"));
|
|
748
757
|
configs.cursorRules = files.map((f) => ({
|
|
749
758
|
filename: f,
|
|
750
|
-
content: fs2.readFileSync(
|
|
759
|
+
content: fs2.readFileSync(path3.join(cursorRulesDir, f), "utf-8")
|
|
751
760
|
}));
|
|
752
761
|
} catch {
|
|
753
762
|
}
|
|
754
763
|
}
|
|
755
|
-
const cursorSkillsDir =
|
|
764
|
+
const cursorSkillsDir = path3.join(dir, ".cursor", "skills");
|
|
756
765
|
if (fs2.existsSync(cursorSkillsDir)) {
|
|
757
766
|
try {
|
|
758
767
|
const slugs = fs2.readdirSync(cursorSkillsDir).filter((f) => {
|
|
759
|
-
return fs2.statSync(
|
|
768
|
+
return fs2.statSync(path3.join(cursorSkillsDir, f)).isDirectory();
|
|
760
769
|
});
|
|
761
|
-
configs.cursorSkills = slugs.filter((slug) => fs2.existsSync(
|
|
770
|
+
configs.cursorSkills = slugs.filter((slug) => fs2.existsSync(path3.join(cursorSkillsDir, slug, "SKILL.md"))).map((name) => ({
|
|
762
771
|
name,
|
|
763
772
|
filename: "SKILL.md",
|
|
764
|
-
content: fs2.readFileSync(
|
|
773
|
+
content: fs2.readFileSync(path3.join(cursorSkillsDir, name, "SKILL.md"), "utf-8")
|
|
765
774
|
}));
|
|
766
775
|
} catch {
|
|
767
776
|
}
|
|
768
777
|
}
|
|
769
|
-
const mcpJsonPath =
|
|
778
|
+
const mcpJsonPath = path3.join(dir, ".mcp.json");
|
|
770
779
|
if (fs2.existsSync(mcpJsonPath)) {
|
|
771
780
|
try {
|
|
772
781
|
const mcpJson = JSON.parse(fs2.readFileSync(mcpJsonPath, "utf-8"));
|
|
@@ -776,7 +785,7 @@ function readExistingConfigs(dir) {
|
|
|
776
785
|
} catch {
|
|
777
786
|
}
|
|
778
787
|
}
|
|
779
|
-
const cursorMcpPath =
|
|
788
|
+
const cursorMcpPath = path3.join(dir, ".cursor", "mcp.json");
|
|
780
789
|
if (fs2.existsSync(cursorMcpPath)) {
|
|
781
790
|
try {
|
|
782
791
|
const cursorMcpJson = JSON.parse(fs2.readFileSync(cursorMcpPath, "utf-8"));
|
|
@@ -786,12 +795,20 @@ function readExistingConfigs(dir) {
|
|
|
786
795
|
} catch {
|
|
787
796
|
}
|
|
788
797
|
}
|
|
798
|
+
if (fs2.existsSync(PERSONAL_LEARNINGS_FILE)) {
|
|
799
|
+
try {
|
|
800
|
+
const content = fs2.readFileSync(PERSONAL_LEARNINGS_FILE, "utf-8");
|
|
801
|
+
const bullets = content.split("\n").filter((l) => l.startsWith("- ")).join("\n");
|
|
802
|
+
if (bullets) configs.personalLearnings = bullets;
|
|
803
|
+
} catch {
|
|
804
|
+
}
|
|
805
|
+
}
|
|
789
806
|
return configs;
|
|
790
807
|
}
|
|
791
808
|
|
|
792
809
|
// src/fingerprint/code-analysis.ts
|
|
793
810
|
import fs3 from "fs";
|
|
794
|
-
import
|
|
811
|
+
import path4 from "path";
|
|
795
812
|
import { execSync as execSync2 } from "child_process";
|
|
796
813
|
var IGNORE_DIRS2 = /* @__PURE__ */ new Set([
|
|
797
814
|
"node_modules",
|
|
@@ -1096,18 +1113,18 @@ function extractSkeletonIndentBased(lines, ext) {
|
|
|
1096
1113
|
}
|
|
1097
1114
|
function extractImports(content, filePath) {
|
|
1098
1115
|
const imports = [];
|
|
1099
|
-
const dir =
|
|
1116
|
+
const dir = path4.dirname(filePath);
|
|
1100
1117
|
for (const line of content.split("\n")) {
|
|
1101
1118
|
const trimmed = line.trim();
|
|
1102
1119
|
const jsMatch = trimmed.match(/(?:from|require\()\s*['"]([^'"]+)['"]/);
|
|
1103
1120
|
if (jsMatch && jsMatch[1].startsWith(".")) {
|
|
1104
|
-
imports.push(
|
|
1121
|
+
imports.push(path4.normalize(path4.join(dir, jsMatch[1])));
|
|
1105
1122
|
continue;
|
|
1106
1123
|
}
|
|
1107
1124
|
const pyMatch = trimmed.match(/^from\s+(\.[.\w]*)\s+import/);
|
|
1108
1125
|
if (pyMatch) {
|
|
1109
1126
|
const modulePath = pyMatch[1].replace(/\./g, "/");
|
|
1110
|
-
imports.push(
|
|
1127
|
+
imports.push(path4.normalize(path4.join(dir, modulePath)));
|
|
1111
1128
|
continue;
|
|
1112
1129
|
}
|
|
1113
1130
|
const goMatch = trimmed.match(/^\s*"([^"]+)"/);
|
|
@@ -1120,7 +1137,7 @@ function extractImports(content, filePath) {
|
|
|
1120
1137
|
function buildImportCounts(files) {
|
|
1121
1138
|
const counts = /* @__PURE__ */ new Map();
|
|
1122
1139
|
for (const [filePath, content] of files) {
|
|
1123
|
-
const ext =
|
|
1140
|
+
const ext = path4.extname(filePath).toLowerCase();
|
|
1124
1141
|
if (!SOURCE_EXTENSIONS.has(ext)) continue;
|
|
1125
1142
|
const imports = extractImports(content, filePath);
|
|
1126
1143
|
for (const imp of imports) {
|
|
@@ -1154,7 +1171,7 @@ function getGitFrequency(dir) {
|
|
|
1154
1171
|
function groupByDirectory(files) {
|
|
1155
1172
|
const groups = /* @__PURE__ */ new Map();
|
|
1156
1173
|
for (const f of files) {
|
|
1157
|
-
const dir =
|
|
1174
|
+
const dir = path4.dirname(f.path);
|
|
1158
1175
|
const group = groups.get(dir) || [];
|
|
1159
1176
|
group.push(f);
|
|
1160
1177
|
groups.set(dir, group);
|
|
@@ -1175,14 +1192,14 @@ function analyzeCode(dir) {
|
|
|
1175
1192
|
let totalChars = 0;
|
|
1176
1193
|
for (const relPath of allPaths) {
|
|
1177
1194
|
try {
|
|
1178
|
-
totalChars += fs3.statSync(
|
|
1195
|
+
totalChars += fs3.statSync(path4.join(dir, relPath)).size;
|
|
1179
1196
|
} catch {
|
|
1180
1197
|
}
|
|
1181
1198
|
}
|
|
1182
1199
|
const fileContents = /* @__PURE__ */ new Map();
|
|
1183
1200
|
for (const relPath of allPaths) {
|
|
1184
1201
|
try {
|
|
1185
|
-
const content = fs3.readFileSync(
|
|
1202
|
+
const content = fs3.readFileSync(path4.join(dir, relPath), "utf-8");
|
|
1186
1203
|
if (content.split("\n").length <= 500) fileContents.set(relPath, content);
|
|
1187
1204
|
} catch {
|
|
1188
1205
|
}
|
|
@@ -1192,7 +1209,7 @@ function analyzeCode(dir) {
|
|
|
1192
1209
|
const scored = [];
|
|
1193
1210
|
let compressedChars = 0;
|
|
1194
1211
|
for (const [relPath, rawContent] of fileContents) {
|
|
1195
|
-
const ext =
|
|
1212
|
+
const ext = path4.extname(relPath).toLowerCase();
|
|
1196
1213
|
const compressed = compressContent(rawContent, ext);
|
|
1197
1214
|
const skeleton = extractSkeleton(compressed, ext);
|
|
1198
1215
|
compressedChars += compressed.length;
|
|
@@ -1221,7 +1238,7 @@ function analyzeCode(dir) {
|
|
|
1221
1238
|
}
|
|
1222
1239
|
if (similar.length > 0) {
|
|
1223
1240
|
dupGroups++;
|
|
1224
|
-
const names = similar.map((f) =>
|
|
1241
|
+
const names = similar.map((f) => path4.basename(f.path));
|
|
1225
1242
|
const summary = `(${similar.length} similar file${similar.length === 1 ? "" : "s"} in ${dirPath}/: ${names.join(", ")})`;
|
|
1226
1243
|
const summarySize = summary.length + 30;
|
|
1227
1244
|
if (includedChars + summarySize <= CHAR_BUDGET) {
|
|
@@ -1258,7 +1275,7 @@ function analyzeCode(dir) {
|
|
|
1258
1275
|
}
|
|
1259
1276
|
function walkDir(base, rel, depth, maxDepth, files) {
|
|
1260
1277
|
if (depth > maxDepth) return;
|
|
1261
|
-
const fullPath =
|
|
1278
|
+
const fullPath = path4.join(base, rel);
|
|
1262
1279
|
let entries;
|
|
1263
1280
|
try {
|
|
1264
1281
|
entries = fs3.readdirSync(fullPath, { withFileTypes: true });
|
|
@@ -1274,7 +1291,7 @@ function walkDir(base, rel, depth, maxDepth, files) {
|
|
|
1274
1291
|
} else if (entry.isFile()) {
|
|
1275
1292
|
if (SKIP_FILES.has(entry.name)) continue;
|
|
1276
1293
|
if (SKIP_PATTERNS.some((p) => p.test(entry.name))) continue;
|
|
1277
|
-
const ext =
|
|
1294
|
+
const ext = path4.extname(entry.name).toLowerCase();
|
|
1278
1295
|
if (TEXT_EXTENSIONS.has(ext) || depth === 0 && !ext && !entry.name.startsWith(".")) {
|
|
1279
1296
|
files.push(relPath);
|
|
1280
1297
|
}
|
|
@@ -1282,7 +1299,7 @@ function walkDir(base, rel, depth, maxDepth, files) {
|
|
|
1282
1299
|
}
|
|
1283
1300
|
}
|
|
1284
1301
|
function filePriority(filePath) {
|
|
1285
|
-
const base =
|
|
1302
|
+
const base = path4.basename(filePath);
|
|
1286
1303
|
const entryPoints = /* @__PURE__ */ new Set([
|
|
1287
1304
|
"index.ts",
|
|
1288
1305
|
"index.js",
|
|
@@ -1589,7 +1606,7 @@ var OpenAICompatProvider = class {
|
|
|
1589
1606
|
|
|
1590
1607
|
// src/llm/cursor-acp.ts
|
|
1591
1608
|
import { spawn, execSync as execSync3 } from "child_process";
|
|
1592
|
-
import
|
|
1609
|
+
import os3 from "os";
|
|
1593
1610
|
|
|
1594
1611
|
// src/llm/seat-based-errors.ts
|
|
1595
1612
|
var ERROR_PATTERNS = [
|
|
@@ -1663,7 +1680,7 @@ var CursorAcpProvider = class {
|
|
|
1663
1680
|
});
|
|
1664
1681
|
}
|
|
1665
1682
|
buildArgs(model, streaming) {
|
|
1666
|
-
const args = ["--print", "--trust", "--workspace",
|
|
1683
|
+
const args = ["--print", "--trust", "--workspace", os3.tmpdir()];
|
|
1667
1684
|
if (model && model !== "auto" && model !== "default") {
|
|
1668
1685
|
args.push("--model", model);
|
|
1669
1686
|
}
|
|
@@ -2302,7 +2319,7 @@ async function validateModel(options) {
|
|
|
2302
2319
|
init_config();
|
|
2303
2320
|
|
|
2304
2321
|
// src/ai/prompts.ts
|
|
2305
|
-
var ROLE_AND_CONTEXT = `You are an expert auditor for coding agent configurations (Claude Code, Cursor, and
|
|
2322
|
+
var ROLE_AND_CONTEXT = `You are an expert auditor for coding agent configurations (Claude Code, Cursor, Codex, and GitHub Copilot).
|
|
2306
2323
|
|
|
2307
2324
|
Your job depends on context:
|
|
2308
2325
|
- If no existing configs exist \u2192 generate an initial setup from scratch.
|
|
@@ -2314,7 +2331,9 @@ var CONFIG_FILE_TYPES = `You understand these config files:
|
|
|
2314
2331
|
- .agents/skills/{name}/SKILL.md: Same OpenSkills format for Codex skills (Codex scans .agents/skills/ for skills).
|
|
2315
2332
|
- .cursor/skills/{name}/SKILL.md: Same OpenSkills format for Cursor skills.
|
|
2316
2333
|
- .cursorrules: Coding rules for Cursor (deprecated legacy format \u2014 do NOT generate this).
|
|
2317
|
-
- .cursor/rules/*.mdc: Modern Cursor rules with frontmatter (description, globs, alwaysApply)
|
|
2334
|
+
- .cursor/rules/*.mdc: Modern Cursor rules with frontmatter (description, globs, alwaysApply).
|
|
2335
|
+
- .github/copilot-instructions.md: Always-on repository-wide instructions for GitHub Copilot \u2014 same purpose as CLAUDE.md but for Copilot. Plain markdown, no frontmatter.
|
|
2336
|
+
- .github/instructions/*.instructions.md: Path-specific instruction files for GitHub Copilot with YAML frontmatter containing an \`applyTo\` glob pattern (e.g. \`applyTo: "**/*.ts,**/*.tsx"\`). Only loaded when Copilot is working on matching files.`;
|
|
2318
2337
|
var EXCLUSIONS = `Do NOT generate .claude/settings.json, .claude/settings.local.json, or mcpServers \u2014 those are managed separately.`;
|
|
2319
2338
|
var OUTPUT_FORMAT = `Your output MUST follow this exact format (no markdown fences):
|
|
2320
2339
|
|
|
@@ -2356,6 +2375,7 @@ var SCORING_CRITERIA = `SCORING CRITERIA \u2014 your output is scored determinis
|
|
|
2356
2375
|
Existence (25 pts):
|
|
2357
2376
|
- CLAUDE.md exists (6 pts) \u2014 always generate for claude targets
|
|
2358
2377
|
- AGENTS.md exists (6 pts) \u2014 always generate for codex target
|
|
2378
|
+
- copilot-instructions.md exists (6 pts) \u2014 always generate for github-copilot target
|
|
2359
2379
|
- Skills configured (8 pts) \u2014 generate 3+ skills for full points
|
|
2360
2380
|
- MCP servers referenced (3 pts) \u2014 mention detected MCP integrations in your config text
|
|
2361
2381
|
- When cursor is targeted: Cursor rules exist (3+3 pts), cross-platform parity (2 pts)
|
|
@@ -2407,7 +2427,7 @@ ${OUTPUT_FORMAT}
|
|
|
2407
2427
|
|
|
2408
2428
|
AgentSetup schema:
|
|
2409
2429
|
{
|
|
2410
|
-
"targetAgent": ["claude", "cursor", "codex"] (array of selected agents),
|
|
2430
|
+
"targetAgent": ["claude", "cursor", "codex", "github-copilot"] (array of selected agents),
|
|
2411
2431
|
"fileDescriptions": {
|
|
2412
2432
|
"<file-path>": "reason for this change (max 80 chars)"
|
|
2413
2433
|
},
|
|
@@ -2425,6 +2445,10 @@ AgentSetup schema:
|
|
|
2425
2445
|
"cursor": {
|
|
2426
2446
|
"skills": [{ "name": "string (kebab-case, matches directory name)", "description": "string (what this skill does and when to use it)", "content": "string (markdown body \u2014 NO frontmatter, it will be generated from name+description)" }],
|
|
2427
2447
|
"rules": [{ "filename": "string.mdc", "content": "string (with frontmatter)" }]
|
|
2448
|
+
},
|
|
2449
|
+
"copilot": {
|
|
2450
|
+
"instructions": "string (markdown content for .github/copilot-instructions.md \u2014 same quality/structure as CLAUDE.md)",
|
|
2451
|
+
"instructionFiles": [{ "filename": "string.instructions.md", "content": "string (with applyTo YAML frontmatter, e.g. ---\\napplyTo: \\"**/*.ts,**/*.tsx\\"\\n---\\n\\nInstructions here)" }]
|
|
2428
2452
|
}
|
|
2429
2453
|
}
|
|
2430
2454
|
|
|
@@ -2446,7 +2470,7 @@ ${OUTPUT_FORMAT}
|
|
|
2446
2470
|
|
|
2447
2471
|
CoreSetup schema:
|
|
2448
2472
|
{
|
|
2449
|
-
"targetAgent": ["claude", "cursor", "codex"] (array of selected agents),
|
|
2473
|
+
"targetAgent": ["claude", "cursor", "codex", "github-copilot"] (array of selected agents),
|
|
2450
2474
|
"fileDescriptions": {
|
|
2451
2475
|
"<file-path>": "reason for this change (max 80 chars)"
|
|
2452
2476
|
},
|
|
@@ -2464,6 +2488,10 @@ CoreSetup schema:
|
|
|
2464
2488
|
"cursor": {
|
|
2465
2489
|
"skillTopics": [{ "name": "string (kebab-case)", "description": "string" }],
|
|
2466
2490
|
"rules": [{ "filename": "string.mdc", "content": "string (with frontmatter)" }]
|
|
2491
|
+
},
|
|
2492
|
+
"copilot": {
|
|
2493
|
+
"instructions": "string (markdown content for .github/copilot-instructions.md \u2014 same quality/structure as CLAUDE.md)",
|
|
2494
|
+
"instructionFiles": [{ "filename": "string.instructions.md", "content": "string (with applyTo YAML frontmatter)" }]
|
|
2467
2495
|
}
|
|
2468
2496
|
}
|
|
2469
2497
|
|
|
@@ -2515,7 +2543,7 @@ Description field formula: [What it does] + [When to use it with trigger phrases
|
|
|
2515
2543
|
|
|
2516
2544
|
Return ONLY a JSON object:
|
|
2517
2545
|
{"name": "string (kebab-case)", "description": "string (what + when + capabilities + negative triggers)", "content": "string (markdown body)"}`;
|
|
2518
|
-
var REFINE_SYSTEM_PROMPT = `You are an expert at modifying coding agent configurations (Claude Code, Cursor, and
|
|
2546
|
+
var REFINE_SYSTEM_PROMPT = `You are an expert at modifying coding agent configurations (Claude Code, Cursor, Codex, and GitHub Copilot).
|
|
2519
2547
|
|
|
2520
2548
|
You will receive the current AgentSetup JSON and a user request describing what to change.
|
|
2521
2549
|
|
|
@@ -2523,7 +2551,7 @@ Apply the requested changes to the setup and return the complete updated AgentSe
|
|
|
2523
2551
|
|
|
2524
2552
|
AgentSetup schema:
|
|
2525
2553
|
{
|
|
2526
|
-
"targetAgent": ["claude", "cursor", "codex"] (array of selected agents),
|
|
2554
|
+
"targetAgent": ["claude", "cursor", "codex", "github-copilot"] (array of selected agents),
|
|
2527
2555
|
"fileDescriptions": {
|
|
2528
2556
|
"<file-path>": "reason for this change (max 80 chars)"
|
|
2529
2557
|
},
|
|
@@ -2541,6 +2569,10 @@ AgentSetup schema:
|
|
|
2541
2569
|
"cursor": {
|
|
2542
2570
|
"skills": [{ "name": "string (kebab-case)", "description": "string", "content": "string (markdown body, no frontmatter)" }],
|
|
2543
2571
|
"rules": [{ "filename": "string.mdc", "content": "string (with frontmatter)" }]
|
|
2572
|
+
},
|
|
2573
|
+
"copilot": {
|
|
2574
|
+
"instructions": "string (markdown content for .github/copilot-instructions.md)",
|
|
2575
|
+
"instructionFiles": [{ "filename": "string.instructions.md", "content": "string (with applyTo YAML frontmatter)" }]
|
|
2544
2576
|
}
|
|
2545
2577
|
}
|
|
2546
2578
|
|
|
@@ -2586,7 +2618,9 @@ Return a JSON object with this exact shape:
|
|
|
2586
2618
|
"readmeMd": "<updated content or null>",
|
|
2587
2619
|
"cursorrules": "<updated content or null>",
|
|
2588
2620
|
"cursorRules": [{"filename": "name.mdc", "content": "..."}] or null,
|
|
2589
|
-
"claudeSkills": [{"filename": "name.md", "content": "..."}] or null
|
|
2621
|
+
"claudeSkills": [{"filename": "name.md", "content": "..."}] or null,
|
|
2622
|
+
"copilotInstructions": "<updated content or null>",
|
|
2623
|
+
"copilotInstructionFiles": [{"filename": "name.instructions.md", "content": "..."}] or null
|
|
2590
2624
|
},
|
|
2591
2625
|
"changesSummary": "<1-2 sentence summary of what was updated and why>",
|
|
2592
2626
|
"docsUpdated": ["CLAUDE.md", "README.md"]
|
|
@@ -2628,6 +2662,12 @@ Each bullet MUST be prefixed with an observation type in bold brackets. Valid ty
|
|
|
2628
2662
|
- **[env]** \u2014 an environment or configuration requirement
|
|
2629
2663
|
- **[convention]** \u2014 a project-specific rule or naming convention
|
|
2630
2664
|
|
|
2665
|
+
Each type can optionally include a scope suffix to indicate whether the learning is project-specific or personal to the developer:
|
|
2666
|
+
- **[type:project]** \u2014 specific to this codebase (DEFAULT \u2014 use when in doubt)
|
|
2667
|
+
- **[type:personal]** \u2014 specific to the developer's workflow or personal preferences
|
|
2668
|
+
|
|
2669
|
+
Use :personal ONLY for user corrections that are clearly personal preferences rather than project requirements. For example, "use bun not npm" is :personal if the project's lockfile doesn't enforce it. "Always run tsc before tsup" is :project because it's about the project's build process. When in doubt, default to :project (or omit the scope entirely, which means :project).
|
|
2670
|
+
|
|
2631
2671
|
Good examples:
|
|
2632
2672
|
- "**[correction]** Files in \`src/generated/\` are auto-generated \u2014 never edit them directly"
|
|
2633
2673
|
- "**[correction]** Use \`pnpm\` not \`npm\` \u2014 the lockfile is pnpm-lock.yaml and npm creates conflicts"
|
|
@@ -2675,6 +2715,7 @@ Analyze the provided file tree and file extension distribution. Return a JSON ob
|
|
|
2675
2715
|
- "languages": array of programming languages used, ordered by prominence in the project (most files first)
|
|
2676
2716
|
- "frameworks": array of frameworks and key libraries detected, ordered by prominence
|
|
2677
2717
|
- "tools": array of external tools, services, and platforms the project integrates with, ordered by prominence
|
|
2718
|
+
- "workspaces": array of relative paths to sub-projects, workspaces, or modules detected from workspace config files (e.g. pnpm-workspace.yaml, Cargo.toml [workspace], go.work, settings.gradle, BUILD files). Empty array if not a multi-project repo.
|
|
2678
2719
|
|
|
2679
2720
|
Use the file extension distribution to determine the ordering \u2014 technologies with more files should appear first.
|
|
2680
2721
|
|
|
@@ -2711,7 +2752,8 @@ async function detectProjectStack(fileTree, suffixCounts) {
|
|
|
2711
2752
|
return {
|
|
2712
2753
|
languages: Array.isArray(result.languages) ? result.languages : [],
|
|
2713
2754
|
frameworks: Array.isArray(result.frameworks) ? result.frameworks : [],
|
|
2714
|
-
tools: Array.isArray(result.tools) ? result.tools : []
|
|
2755
|
+
tools: Array.isArray(result.tools) ? result.tools : [],
|
|
2756
|
+
workspaces: Array.isArray(result.workspaces) ? result.workspaces : []
|
|
2715
2757
|
};
|
|
2716
2758
|
}
|
|
2717
2759
|
|
|
@@ -2720,14 +2762,14 @@ init_config();
|
|
|
2720
2762
|
|
|
2721
2763
|
// src/fingerprint/cache.ts
|
|
2722
2764
|
import fs6 from "fs";
|
|
2723
|
-
import
|
|
2765
|
+
import path6 from "path";
|
|
2724
2766
|
import crypto from "crypto";
|
|
2725
2767
|
import { execSync as execSync5 } from "child_process";
|
|
2726
2768
|
var CACHE_VERSION = 1;
|
|
2727
2769
|
var CACHE_DIR = ".caliber/cache";
|
|
2728
2770
|
var CACHE_FILE = "fingerprint.json";
|
|
2729
2771
|
function getCachePath(dir) {
|
|
2730
|
-
return
|
|
2772
|
+
return path6.join(dir, CACHE_DIR, CACHE_FILE);
|
|
2731
2773
|
}
|
|
2732
2774
|
function getGitHead(dir) {
|
|
2733
2775
|
try {
|
|
@@ -2776,16 +2818,17 @@ function loadFingerprintCache(dir, fileTree) {
|
|
|
2776
2818
|
codeAnalysis: cache.codeAnalysis,
|
|
2777
2819
|
languages: cache.languages,
|
|
2778
2820
|
frameworks: cache.frameworks,
|
|
2779
|
-
tools: cache.tools
|
|
2821
|
+
tools: cache.tools,
|
|
2822
|
+
workspaces: cache.workspaces
|
|
2780
2823
|
};
|
|
2781
2824
|
} catch {
|
|
2782
2825
|
return null;
|
|
2783
2826
|
}
|
|
2784
2827
|
}
|
|
2785
|
-
function saveFingerprintCache(dir, fileTree, codeAnalysis, languages, frameworks, tools) {
|
|
2828
|
+
function saveFingerprintCache(dir, fileTree, codeAnalysis, languages, frameworks, tools, workspaces) {
|
|
2786
2829
|
const cachePath = getCachePath(dir);
|
|
2787
2830
|
try {
|
|
2788
|
-
const cacheDir =
|
|
2831
|
+
const cacheDir = path6.dirname(cachePath);
|
|
2789
2832
|
if (!fs6.existsSync(cacheDir)) {
|
|
2790
2833
|
fs6.mkdirSync(cacheDir, { recursive: true });
|
|
2791
2834
|
}
|
|
@@ -2796,12 +2839,24 @@ function saveFingerprintCache(dir, fileTree, codeAnalysis, languages, frameworks
|
|
|
2796
2839
|
codeAnalysis,
|
|
2797
2840
|
languages,
|
|
2798
2841
|
frameworks,
|
|
2799
|
-
tools
|
|
2842
|
+
tools,
|
|
2843
|
+
workspaces
|
|
2800
2844
|
};
|
|
2801
2845
|
fs6.writeFileSync(cachePath, JSON.stringify(cache), "utf-8");
|
|
2802
2846
|
} catch {
|
|
2803
2847
|
}
|
|
2804
2848
|
}
|
|
2849
|
+
function getDetectedWorkspaces(dir) {
|
|
2850
|
+
const cachePath = getCachePath(dir);
|
|
2851
|
+
try {
|
|
2852
|
+
if (!fs6.existsSync(cachePath)) return [];
|
|
2853
|
+
const raw = fs6.readFileSync(cachePath, "utf-8");
|
|
2854
|
+
const cache = JSON.parse(raw);
|
|
2855
|
+
return cache.workspaces ?? [];
|
|
2856
|
+
} catch {
|
|
2857
|
+
return [];
|
|
2858
|
+
}
|
|
2859
|
+
}
|
|
2805
2860
|
|
|
2806
2861
|
// src/fingerprint/index.ts
|
|
2807
2862
|
async function collectFingerprint(dir) {
|
|
@@ -2833,20 +2888,21 @@ async function collectFingerprint(dir) {
|
|
|
2833
2888
|
existingConfigs,
|
|
2834
2889
|
codeAnalysis
|
|
2835
2890
|
};
|
|
2836
|
-
await enrichWithLLM(fingerprint);
|
|
2891
|
+
const workspaces = await enrichWithLLM(fingerprint);
|
|
2837
2892
|
saveFingerprintCache(
|
|
2838
2893
|
dir,
|
|
2839
2894
|
fileTree,
|
|
2840
2895
|
codeAnalysis,
|
|
2841
2896
|
fingerprint.languages,
|
|
2842
2897
|
fingerprint.frameworks,
|
|
2843
|
-
fingerprint.tools
|
|
2898
|
+
fingerprint.tools,
|
|
2899
|
+
workspaces
|
|
2844
2900
|
);
|
|
2845
2901
|
return fingerprint;
|
|
2846
2902
|
}
|
|
2847
2903
|
function readPackageName(dir) {
|
|
2848
2904
|
try {
|
|
2849
|
-
const pkgPath =
|
|
2905
|
+
const pkgPath = path7.join(dir, "package.json");
|
|
2850
2906
|
if (!fs7.existsSync(pkgPath)) return void 0;
|
|
2851
2907
|
const pkg3 = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
|
|
2852
2908
|
return pkg3.name;
|
|
@@ -2857,12 +2913,12 @@ function readPackageName(dir) {
|
|
|
2857
2913
|
async function enrichWithLLM(fingerprint) {
|
|
2858
2914
|
try {
|
|
2859
2915
|
const config = loadConfig();
|
|
2860
|
-
if (!config) return;
|
|
2861
|
-
if (fingerprint.fileTree.length === 0) return;
|
|
2916
|
+
if (!config) return [];
|
|
2917
|
+
if (fingerprint.fileTree.length === 0) return [];
|
|
2862
2918
|
const suffixCounts = {};
|
|
2863
2919
|
for (const entry of fingerprint.fileTree) {
|
|
2864
2920
|
if (entry.endsWith("/")) continue;
|
|
2865
|
-
const ext =
|
|
2921
|
+
const ext = path7.extname(entry).toLowerCase();
|
|
2866
2922
|
if (ext) {
|
|
2867
2923
|
suffixCounts[ext] = (suffixCounts[ext] || 0) + 1;
|
|
2868
2924
|
}
|
|
@@ -2871,99 +2927,574 @@ async function enrichWithLLM(fingerprint) {
|
|
|
2871
2927
|
if (result.languages?.length) fingerprint.languages = result.languages;
|
|
2872
2928
|
if (result.frameworks?.length) fingerprint.frameworks = result.frameworks;
|
|
2873
2929
|
if (result.tools?.length) fingerprint.tools = result.tools;
|
|
2930
|
+
return result.workspaces ?? [];
|
|
2874
2931
|
} catch {
|
|
2932
|
+
return [];
|
|
2875
2933
|
}
|
|
2876
2934
|
}
|
|
2877
2935
|
|
|
2878
|
-
// src/
|
|
2879
|
-
|
|
2936
|
+
// src/fingerprint/sources.ts
|
|
2937
|
+
import fs8 from "fs";
|
|
2938
|
+
import path8 from "path";
|
|
2880
2939
|
|
|
2881
|
-
// src/utils
|
|
2882
|
-
import { readFileSync } from "fs";
|
|
2883
|
-
import { join } from "path";
|
|
2884
|
-
function readFileOrNull(
|
|
2940
|
+
// src/scoring/utils.ts
|
|
2941
|
+
import { existsSync as existsSync2, readFileSync, readdirSync } from "fs";
|
|
2942
|
+
import { join, relative } from "path";
|
|
2943
|
+
function readFileOrNull(filePath) {
|
|
2885
2944
|
try {
|
|
2886
|
-
return readFileSync(
|
|
2945
|
+
return readFileSync(filePath, "utf-8");
|
|
2887
2946
|
} catch {
|
|
2888
2947
|
return null;
|
|
2889
2948
|
}
|
|
2890
2949
|
}
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2950
|
+
var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
2951
|
+
"node_modules",
|
|
2952
|
+
".git",
|
|
2953
|
+
"dist",
|
|
2954
|
+
"build",
|
|
2955
|
+
"out",
|
|
2956
|
+
".next",
|
|
2957
|
+
".nuxt",
|
|
2958
|
+
"__pycache__",
|
|
2959
|
+
".venv",
|
|
2960
|
+
"venv",
|
|
2961
|
+
"env",
|
|
2962
|
+
".env",
|
|
2963
|
+
"target",
|
|
2964
|
+
"vendor",
|
|
2965
|
+
".cache",
|
|
2966
|
+
".parcel-cache",
|
|
2967
|
+
"coverage",
|
|
2968
|
+
".nyc_output",
|
|
2969
|
+
".turbo",
|
|
2970
|
+
".caliber",
|
|
2971
|
+
".claude",
|
|
2972
|
+
".cursor",
|
|
2973
|
+
".agents",
|
|
2974
|
+
".codex"
|
|
2975
|
+
]);
|
|
2976
|
+
var IGNORED_FILES = /* @__PURE__ */ new Set([
|
|
2977
|
+
".DS_Store",
|
|
2978
|
+
"Thumbs.db",
|
|
2979
|
+
".gitignore",
|
|
2980
|
+
".editorconfig",
|
|
2981
|
+
".prettierrc",
|
|
2982
|
+
".prettierignore",
|
|
2983
|
+
".eslintignore",
|
|
2984
|
+
"package-lock.json",
|
|
2985
|
+
"yarn.lock",
|
|
2986
|
+
"pnpm-lock.yaml",
|
|
2987
|
+
"bun.lockb"
|
|
2988
|
+
]);
|
|
2989
|
+
function collectProjectStructure(dir, maxDepth = 2) {
|
|
2990
|
+
const dirs = [];
|
|
2991
|
+
const files = [];
|
|
2992
|
+
function walk(currentDir, depth) {
|
|
2993
|
+
if (depth > maxDepth) return;
|
|
2994
|
+
try {
|
|
2995
|
+
const entries = readdirSync(currentDir, { withFileTypes: true });
|
|
2996
|
+
for (const entry of entries) {
|
|
2997
|
+
const name = entry.name;
|
|
2998
|
+
if (name.startsWith(".") && IGNORED_DIRS.has(name)) continue;
|
|
2999
|
+
if (IGNORED_FILES.has(name)) continue;
|
|
3000
|
+
const rel = relative(dir, join(currentDir, name));
|
|
3001
|
+
if (entry.isDirectory()) {
|
|
3002
|
+
if (IGNORED_DIRS.has(name)) continue;
|
|
3003
|
+
dirs.push(rel);
|
|
3004
|
+
walk(join(currentDir, name), depth + 1);
|
|
3005
|
+
} else if (entry.isFile()) {
|
|
3006
|
+
files.push(rel);
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
} catch {
|
|
3010
|
+
}
|
|
2898
3011
|
}
|
|
3012
|
+
walk(dir, 0);
|
|
3013
|
+
return { dirs, files };
|
|
2899
3014
|
}
|
|
2900
|
-
function
|
|
2901
|
-
const
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
...pkg3.devDependencies
|
|
2906
|
-
};
|
|
2907
|
-
const trivial = /* @__PURE__ */ new Set([
|
|
2908
|
-
"typescript",
|
|
2909
|
-
"@types/node",
|
|
2910
|
-
"tslib",
|
|
2911
|
-
"ts-node",
|
|
2912
|
-
"tsx",
|
|
2913
|
-
"prettier",
|
|
2914
|
-
"eslint",
|
|
2915
|
-
"@eslint/js",
|
|
2916
|
-
"rimraf",
|
|
2917
|
-
"cross-env",
|
|
2918
|
-
"dotenv",
|
|
2919
|
-
"nodemon",
|
|
2920
|
-
"husky",
|
|
2921
|
-
"lint-staged",
|
|
2922
|
-
"commitlint",
|
|
2923
|
-
"@commitlint/cli",
|
|
2924
|
-
"@commitlint/config-conventional"
|
|
2925
|
-
]);
|
|
2926
|
-
const trivialPatterns = [
|
|
2927
|
-
/^@rely-ai\//,
|
|
2928
|
-
/^@caliber-ai\//,
|
|
2929
|
-
/^eslint-/,
|
|
2930
|
-
/^@eslint\//,
|
|
2931
|
-
/^prettier-/,
|
|
2932
|
-
/^@typescript-eslint\//
|
|
2933
|
-
];
|
|
2934
|
-
return Object.keys(deps).filter((d) => !trivial.has(d) && !d.startsWith("@types/") && !trivialPatterns.some((p) => p.test(d))).slice(0, 30);
|
|
2935
|
-
}
|
|
2936
|
-
function extractPythonDeps(dir) {
|
|
2937
|
-
const reqTxt = readFileOrNull(join(dir, "requirements.txt"));
|
|
2938
|
-
if (reqTxt) {
|
|
2939
|
-
return reqTxt.split("\n").map((l) => l.trim().split(/[=<>!~\[]/)[0].trim()).filter((l) => l && !l.startsWith("#")).slice(0, 30);
|
|
3015
|
+
function collectPrimaryConfigContent(dir) {
|
|
3016
|
+
const parts = [];
|
|
3017
|
+
for (const file of ["CLAUDE.md", ".cursorrules", "AGENTS.md"]) {
|
|
3018
|
+
const content = readFileOrNull(join(dir, file));
|
|
3019
|
+
if (content) parts.push(content);
|
|
2940
3020
|
}
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
const
|
|
2944
|
-
|
|
2945
|
-
|
|
3021
|
+
try {
|
|
3022
|
+
const rulesDir = join(dir, ".cursor", "rules");
|
|
3023
|
+
const mdcFiles = readdirSync(rulesDir).filter((f) => f.endsWith(".mdc"));
|
|
3024
|
+
for (const f of mdcFiles) {
|
|
3025
|
+
const content = readFileOrNull(join(rulesDir, f));
|
|
3026
|
+
if (content) parts.push(content);
|
|
2946
3027
|
}
|
|
3028
|
+
} catch {
|
|
2947
3029
|
}
|
|
2948
|
-
return
|
|
3030
|
+
return parts.join("\n");
|
|
2949
3031
|
}
|
|
2950
|
-
function
|
|
2951
|
-
const
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
3032
|
+
function collectAllConfigContent(dir) {
|
|
3033
|
+
const parts = [collectPrimaryConfigContent(dir)];
|
|
3034
|
+
for (const skillsDir of [join(dir, ".claude", "skills"), join(dir, ".agents", "skills")]) {
|
|
3035
|
+
try {
|
|
3036
|
+
const entries = readdirSync(skillsDir, { withFileTypes: true });
|
|
3037
|
+
for (const entry of entries) {
|
|
3038
|
+
if (entry.isDirectory()) {
|
|
3039
|
+
const skill = readFileOrNull(join(skillsDir, entry.name, "SKILL.md"));
|
|
3040
|
+
if (skill) parts.push(skill);
|
|
3041
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
3042
|
+
const content = readFileOrNull(join(skillsDir, entry.name));
|
|
3043
|
+
if (content) parts.push(content);
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
} catch {
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
return parts.join("\n");
|
|
2956
3050
|
}
|
|
2957
|
-
function
|
|
2958
|
-
|
|
2959
|
-
if (!cargo) return [];
|
|
2960
|
-
const depSection = cargo.match(/\[dependencies\]([\s\S]*?)(?:\[|$)/);
|
|
2961
|
-
if (!depSection) return [];
|
|
2962
|
-
return depSection[1].split("\n").map((l) => l.trim().split(/\s*=/)[0].trim()).filter((l) => l.length > 0 && !l.startsWith("#")).slice(0, 30);
|
|
3051
|
+
function estimateTokens2(text) {
|
|
3052
|
+
return Math.ceil(text.length / 4);
|
|
2963
3053
|
}
|
|
2964
|
-
function
|
|
2965
|
-
|
|
2966
|
-
|
|
3054
|
+
function analyzeMarkdownStructure(content) {
|
|
3055
|
+
const lines = content.split("\n");
|
|
3056
|
+
let headingCount = 0;
|
|
3057
|
+
let h2Count = 0;
|
|
3058
|
+
let h3Count = 0;
|
|
3059
|
+
let codeBlockCount = 0;
|
|
3060
|
+
let codeBlockLines = 0;
|
|
3061
|
+
let listItemCount = 0;
|
|
3062
|
+
let inlineCodeCount = 0;
|
|
3063
|
+
let inCodeBlock = false;
|
|
3064
|
+
for (const line of lines) {
|
|
3065
|
+
const trimmed = line.trim();
|
|
3066
|
+
if (trimmed.startsWith("```")) {
|
|
3067
|
+
if (!inCodeBlock) codeBlockCount++;
|
|
3068
|
+
inCodeBlock = !inCodeBlock;
|
|
3069
|
+
continue;
|
|
3070
|
+
}
|
|
3071
|
+
if (inCodeBlock) {
|
|
3072
|
+
codeBlockLines++;
|
|
3073
|
+
continue;
|
|
3074
|
+
}
|
|
3075
|
+
if (trimmed.startsWith("## ") && !trimmed.startsWith("### ")) h2Count++;
|
|
3076
|
+
if (trimmed.startsWith("### ")) h3Count++;
|
|
3077
|
+
if (trimmed.startsWith("#")) headingCount++;
|
|
3078
|
+
if (/^[-*+]\s/.test(trimmed) || /^\d+\.\s/.test(trimmed)) listItemCount++;
|
|
3079
|
+
const inlineMatches = trimmed.match(/`[^`]+`/g);
|
|
3080
|
+
if (inlineMatches) inlineCodeCount += inlineMatches.length;
|
|
3081
|
+
}
|
|
3082
|
+
return {
|
|
3083
|
+
headingCount,
|
|
3084
|
+
h2Count,
|
|
3085
|
+
h3Count,
|
|
3086
|
+
codeBlockCount,
|
|
3087
|
+
codeBlockLines,
|
|
3088
|
+
listItemCount,
|
|
3089
|
+
inlineCodeCount,
|
|
3090
|
+
totalLines: lines.length,
|
|
3091
|
+
nonEmptyLines: lines.filter((l) => l.trim().length > 0).length
|
|
3092
|
+
};
|
|
3093
|
+
}
|
|
3094
|
+
function extractReferences(content) {
|
|
3095
|
+
const refs = /* @__PURE__ */ new Set();
|
|
3096
|
+
const backtickPattern = /`([^`]+)`/g;
|
|
3097
|
+
let match;
|
|
3098
|
+
while ((match = backtickPattern.exec(content)) !== null) {
|
|
3099
|
+
const term = match[1].trim();
|
|
3100
|
+
if ((term.includes("/") || /\.\w{1,5}$/.test(term)) && !term.startsWith("-") && term.length < 200) {
|
|
3101
|
+
if (term.startsWith("@") && (term.match(/\//g) || []).length === 1) continue;
|
|
3102
|
+
if (term.includes(" ")) continue;
|
|
3103
|
+
if (/^\d+\.\d+/.test(term)) continue;
|
|
3104
|
+
if (term.includes("/") && !/\.\w{1,5}$/.test(term)) {
|
|
3105
|
+
if (term !== term.toLowerCase() && !/^[a-z]/.test(term)) continue;
|
|
3106
|
+
const segments = term.split("/");
|
|
3107
|
+
if (segments.every((s) => s.length <= 3)) continue;
|
|
3108
|
+
}
|
|
3109
|
+
const cleaned = term.replace(/[,;:!?)]+$/, "");
|
|
3110
|
+
if (cleaned.length > 1) refs.add(cleaned);
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
const pathPattern = /(?:^|\s)((?:[a-zA-Z0-9_@.-]+\/)+[a-zA-Z0-9_.*-]+\.[a-zA-Z]{1,5})/gm;
|
|
3114
|
+
while ((match = pathPattern.exec(content)) !== null) {
|
|
3115
|
+
const term = match[1].trim();
|
|
3116
|
+
if (term.length > 2 && term.length < 200) {
|
|
3117
|
+
if (term.startsWith("@") && (term.match(/\//g) || []).length === 1) continue;
|
|
3118
|
+
const cleaned = term.replace(/[,;:!?)]+$/, "");
|
|
3119
|
+
if (cleaned.length > 1) refs.add(cleaned);
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
return Array.from(refs);
|
|
3123
|
+
}
|
|
3124
|
+
function validateFileReferences(content, dir, checkExists = existsSync2) {
|
|
3125
|
+
const refs = extractReferences(content);
|
|
3126
|
+
const valid = [];
|
|
3127
|
+
const invalid = [];
|
|
3128
|
+
for (const ref of refs) {
|
|
3129
|
+
if (/^https?:\/\//.test(ref)) continue;
|
|
3130
|
+
if (/^\d+\.\d+/.test(ref)) continue;
|
|
3131
|
+
if (ref.startsWith("#") || ref.startsWith("@")) continue;
|
|
3132
|
+
if (ref.includes("*") || ref.includes("..")) continue;
|
|
3133
|
+
if (!ref.includes("/") && !ref.includes(".")) continue;
|
|
3134
|
+
const fullPath = join(dir, ref);
|
|
3135
|
+
if (checkExists(fullPath)) {
|
|
3136
|
+
valid.push(ref);
|
|
3137
|
+
} else {
|
|
3138
|
+
const withoutTrailing = ref.replace(/\/+$/, "");
|
|
3139
|
+
if (withoutTrailing !== ref && checkExists(join(dir, withoutTrailing))) {
|
|
3140
|
+
valid.push(ref);
|
|
3141
|
+
} else {
|
|
3142
|
+
invalid.push(ref);
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
return { valid, invalid, total: valid.length + invalid.length };
|
|
3147
|
+
}
|
|
3148
|
+
function countConcreteness(content) {
|
|
3149
|
+
let concrete = 0;
|
|
3150
|
+
let abstract = 0;
|
|
3151
|
+
let inCodeBlock = false;
|
|
3152
|
+
for (const line of content.split("\n")) {
|
|
3153
|
+
if (line.trim().startsWith("```")) {
|
|
3154
|
+
inCodeBlock = !inCodeBlock;
|
|
3155
|
+
continue;
|
|
3156
|
+
}
|
|
3157
|
+
const classification = classifyLine(line, inCodeBlock);
|
|
3158
|
+
if (classification === "concrete") concrete++;
|
|
3159
|
+
else if (classification === "abstract") abstract++;
|
|
3160
|
+
}
|
|
3161
|
+
return { concrete, abstract };
|
|
3162
|
+
}
|
|
3163
|
+
function countTreeLines(content) {
|
|
3164
|
+
const treeLinePattern = /[├└│─┬]/;
|
|
3165
|
+
let count = 0;
|
|
3166
|
+
let inCodeBlock = false;
|
|
3167
|
+
for (const line of content.split("\n")) {
|
|
3168
|
+
if (line.trim().startsWith("```")) {
|
|
3169
|
+
inCodeBlock = !inCodeBlock;
|
|
3170
|
+
continue;
|
|
3171
|
+
}
|
|
3172
|
+
if (inCodeBlock && treeLinePattern.test(line)) count++;
|
|
3173
|
+
}
|
|
3174
|
+
return count;
|
|
3175
|
+
}
|
|
3176
|
+
function calculateDuplicatePercent(content1, content2) {
|
|
3177
|
+
const lines1 = new Set(content1.split("\n").map((l) => l.trim()).filter((l) => l.length > 10));
|
|
3178
|
+
const lines2 = content2.split("\n").map((l) => l.trim()).filter((l) => l.length > 10);
|
|
3179
|
+
const overlapping = lines2.filter((l) => lines1.has(l)).length;
|
|
3180
|
+
return lines2.length > 0 ? Math.round(overlapping / lines2.length * 100) : 0;
|
|
3181
|
+
}
|
|
3182
|
+
function calculateDensityPoints(density, maxPoints) {
|
|
3183
|
+
if (density >= 40) return maxPoints;
|
|
3184
|
+
if (density >= 25) return Math.round(maxPoints * 0.75);
|
|
3185
|
+
if (density >= 15) return Math.round(maxPoints * 0.5);
|
|
3186
|
+
if (density >= 5) return Math.round(maxPoints * 0.25);
|
|
3187
|
+
return 0;
|
|
3188
|
+
}
|
|
3189
|
+
function isEntryMentioned(entry, contentLower) {
|
|
3190
|
+
const entryLower = entry.toLowerCase();
|
|
3191
|
+
const variants = [entryLower, entryLower.replace(/\\/g, "/")];
|
|
3192
|
+
const lastSegment = entry.split("/").pop()?.toLowerCase();
|
|
3193
|
+
if (lastSegment && lastSegment.length > 3) variants.push(lastSegment);
|
|
3194
|
+
return variants.some((v) => {
|
|
3195
|
+
const escaped = v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3196
|
+
return new RegExp(`(?:^|[\\s\`/"'\\.,(])${escaped}(?:[\\s\`/"'.,;:!?)\\\\]|$)`, "i").test(contentLower);
|
|
3197
|
+
});
|
|
3198
|
+
}
|
|
3199
|
+
function classifyLine(line, inCodeBlock) {
|
|
3200
|
+
if (inCodeBlock) return "concrete";
|
|
3201
|
+
const trimmed = line.trim();
|
|
3202
|
+
if (trimmed.length === 0) return "neutral";
|
|
3203
|
+
if (trimmed.startsWith("#")) return "neutral";
|
|
3204
|
+
if (/`[^`]+`/.test(trimmed)) return "concrete";
|
|
3205
|
+
if (/[a-zA-Z0-9_-]+\/[a-zA-Z0-9_.-]+\.[a-zA-Z]{1,5}/.test(trimmed)) return "concrete";
|
|
3206
|
+
if (/[a-zA-Z0-9_]{4,}\/[a-zA-Z0-9_.-]/.test(trimmed)) return "concrete";
|
|
3207
|
+
if (/\b[a-zA-Z0-9_-]+\.[a-zA-Z]{1,5}\b/.test(trimmed) && !/\b(e\.g|i\.e|vs|etc)\b/i.test(trimmed)) return "concrete";
|
|
3208
|
+
return "abstract";
|
|
3209
|
+
}
|
|
3210
|
+
|
|
3211
|
+
// src/fingerprint/sources.ts
|
|
3212
|
+
var MAX_SOURCES_IN_PROMPT = 5;
|
|
3213
|
+
var SOURCE_CONTENT_LIMIT = 2e3;
|
|
3214
|
+
var README_CONTENT_LIMIT = 1e3;
|
|
3215
|
+
var ORIGIN_PRIORITY = { cli: 0, config: 1, workspace: 2 };
|
|
3216
|
+
function loadSourcesConfig(dir) {
|
|
3217
|
+
const configPath = path8.join(dir, ".caliber", "sources.json");
|
|
3218
|
+
const content = readFileOrNull(configPath);
|
|
3219
|
+
if (!content) return [];
|
|
3220
|
+
try {
|
|
3221
|
+
const parsed = JSON.parse(content);
|
|
3222
|
+
if (!Array.isArray(parsed.sources)) {
|
|
3223
|
+
console.warn("Warning: .caliber/sources.json is malformed (missing sources array), skipping sources");
|
|
3224
|
+
return [];
|
|
3225
|
+
}
|
|
3226
|
+
return parsed.sources.filter(
|
|
3227
|
+
(s) => s.type && (s.path || s.url)
|
|
3228
|
+
);
|
|
3229
|
+
} catch {
|
|
3230
|
+
console.warn("Warning: .caliber/sources.json is malformed, skipping sources");
|
|
3231
|
+
return [];
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
function writeSourcesConfig(dir, sources2) {
|
|
3235
|
+
const configDir = path8.join(dir, ".caliber");
|
|
3236
|
+
if (!fs8.existsSync(configDir)) {
|
|
3237
|
+
fs8.mkdirSync(configDir, { recursive: true });
|
|
3238
|
+
}
|
|
3239
|
+
const configPath = path8.join(configDir, "sources.json");
|
|
3240
|
+
fs8.writeFileSync(configPath, JSON.stringify({ sources: sources2 }, null, 2) + "\n", "utf-8");
|
|
3241
|
+
}
|
|
3242
|
+
function detectSourceType(absPath) {
|
|
3243
|
+
try {
|
|
3244
|
+
return fs8.statSync(absPath).isDirectory() ? "repo" : "file";
|
|
3245
|
+
} catch {
|
|
3246
|
+
return "file";
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
function isInsideDir(childPath, parentDir) {
|
|
3250
|
+
const relative2 = path8.relative(parentDir, childPath);
|
|
3251
|
+
return !relative2.startsWith("..") && !path8.isAbsolute(relative2);
|
|
3252
|
+
}
|
|
3253
|
+
function resolveAllSources(dir, cliSources, workspaces) {
|
|
3254
|
+
const seen = /* @__PURE__ */ new Map();
|
|
3255
|
+
const projectRoot = path8.resolve(dir);
|
|
3256
|
+
for (const src of cliSources) {
|
|
3257
|
+
const absPath = path8.resolve(dir, src);
|
|
3258
|
+
if (seen.has(absPath)) continue;
|
|
3259
|
+
const type = detectSourceType(absPath);
|
|
3260
|
+
seen.set(absPath, {
|
|
3261
|
+
absPath,
|
|
3262
|
+
config: { type, path: src },
|
|
3263
|
+
origin: "cli"
|
|
3264
|
+
});
|
|
3265
|
+
}
|
|
3266
|
+
const configSources = loadSourcesConfig(dir);
|
|
3267
|
+
for (const cfg of configSources) {
|
|
3268
|
+
if (cfg.type === "url") continue;
|
|
3269
|
+
if (!cfg.path) continue;
|
|
3270
|
+
const absPath = path8.resolve(dir, cfg.path);
|
|
3271
|
+
if (seen.has(absPath)) continue;
|
|
3272
|
+
seen.set(absPath, { absPath, config: cfg, origin: "config" });
|
|
3273
|
+
}
|
|
3274
|
+
for (const ws of workspaces) {
|
|
3275
|
+
const absPath = path8.resolve(dir, ws);
|
|
3276
|
+
if (seen.has(absPath)) continue;
|
|
3277
|
+
if (!isInsideDir(absPath, projectRoot)) continue;
|
|
3278
|
+
seen.set(absPath, {
|
|
3279
|
+
absPath,
|
|
3280
|
+
config: { type: "repo", path: ws, role: "workspace-sibling" },
|
|
3281
|
+
origin: "workspace"
|
|
3282
|
+
});
|
|
3283
|
+
}
|
|
3284
|
+
const valid = [];
|
|
3285
|
+
for (const [absPath, resolved] of seen) {
|
|
3286
|
+
let stat;
|
|
3287
|
+
try {
|
|
3288
|
+
stat = fs8.statSync(absPath);
|
|
3289
|
+
} catch {
|
|
3290
|
+
console.warn(`Source ${resolved.config.path || absPath} not found, skipping`);
|
|
3291
|
+
continue;
|
|
3292
|
+
}
|
|
3293
|
+
if (isInsideDir(absPath, projectRoot) && resolved.origin !== "workspace") {
|
|
3294
|
+
if (absPath !== projectRoot) {
|
|
3295
|
+
console.warn(`Skipping ${resolved.config.path || absPath}: inside current project`);
|
|
3296
|
+
}
|
|
3297
|
+
continue;
|
|
3298
|
+
}
|
|
3299
|
+
if (resolved.config.type === "file" && !stat.isFile()) continue;
|
|
3300
|
+
if (resolved.config.type === "repo" && !stat.isDirectory()) continue;
|
|
3301
|
+
valid.push(resolved);
|
|
3302
|
+
}
|
|
3303
|
+
valid.sort((a, b) => {
|
|
3304
|
+
const pA = ORIGIN_PRIORITY[a.origin] ?? 2;
|
|
3305
|
+
const pB = ORIGIN_PRIORITY[b.origin] ?? 2;
|
|
3306
|
+
if (pA !== pB) return pA - pB;
|
|
3307
|
+
return a.absPath.length - b.absPath.length;
|
|
3308
|
+
});
|
|
3309
|
+
const capped = valid.slice(0, MAX_SOURCES_IN_PROMPT);
|
|
3310
|
+
return capped.map((r) => collectSourceSummary(r, dir));
|
|
3311
|
+
}
|
|
3312
|
+
function collectSourceSummary(resolved, projectDir) {
|
|
3313
|
+
const { config, origin, absPath } = resolved;
|
|
3314
|
+
if (config.type === "file") {
|
|
3315
|
+
return collectFileSummary(resolved, projectDir);
|
|
3316
|
+
}
|
|
3317
|
+
const summaryPath = path8.join(absPath, ".caliber", "summary.json");
|
|
3318
|
+
const summaryContent = readFileOrNull(summaryPath);
|
|
3319
|
+
if (summaryContent) {
|
|
3320
|
+
try {
|
|
3321
|
+
const published = JSON.parse(summaryContent);
|
|
3322
|
+
return {
|
|
3323
|
+
name: published.name || path8.basename(absPath),
|
|
3324
|
+
type: "repo",
|
|
3325
|
+
role: config.role || published.role || "related-repo",
|
|
3326
|
+
description: config.description || published.description || "",
|
|
3327
|
+
origin,
|
|
3328
|
+
topLevelDirs: Array.isArray(published.topLevelDirs) ? published.topLevelDirs : void 0,
|
|
3329
|
+
existingClaudeMd: typeof published.conventions === "string" ? published.conventions : void 0,
|
|
3330
|
+
packageName: typeof published.name === "string" ? published.name : void 0
|
|
3331
|
+
};
|
|
3332
|
+
} catch {
|
|
3333
|
+
}
|
|
3334
|
+
}
|
|
3335
|
+
return collectRepoSummary(resolved, projectDir);
|
|
3336
|
+
}
|
|
3337
|
+
function collectRepoSummary(resolved, projectDir) {
|
|
3338
|
+
const { config, origin, absPath } = resolved;
|
|
3339
|
+
const packageName = readPackageName(absPath);
|
|
3340
|
+
let topLevelDirs;
|
|
3341
|
+
let keyFiles;
|
|
3342
|
+
try {
|
|
3343
|
+
const entries = fs8.readdirSync(absPath, { withFileTypes: true });
|
|
3344
|
+
topLevelDirs = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules").map((e) => e.name).slice(0, 20);
|
|
3345
|
+
keyFiles = entries.filter((e) => e.isFile() && !e.name.startsWith(".")).map((e) => e.name).slice(0, 15);
|
|
3346
|
+
} catch {
|
|
3347
|
+
}
|
|
3348
|
+
const claudeMdContent = readFileOrNull(path8.join(absPath, "CLAUDE.md"));
|
|
3349
|
+
const existingClaudeMd = claudeMdContent ? claudeMdContent.slice(0, SOURCE_CONTENT_LIMIT) : void 0;
|
|
3350
|
+
const readmeContent = readFileOrNull(path8.join(absPath, "README.md"));
|
|
3351
|
+
const readmeExcerpt = readmeContent ? readmeContent.slice(0, README_CONTENT_LIMIT) : void 0;
|
|
3352
|
+
const gitRemoteUrl = getGitRemoteUrl(absPath);
|
|
3353
|
+
return {
|
|
3354
|
+
name: packageName || path8.basename(absPath),
|
|
3355
|
+
type: "repo",
|
|
3356
|
+
role: config.role || "related-repo",
|
|
3357
|
+
description: config.description || "",
|
|
3358
|
+
origin,
|
|
3359
|
+
gitRemoteUrl,
|
|
3360
|
+
topLevelDirs,
|
|
3361
|
+
keyFiles,
|
|
3362
|
+
existingClaudeMd,
|
|
3363
|
+
readmeExcerpt,
|
|
3364
|
+
packageName
|
|
3365
|
+
};
|
|
3366
|
+
}
|
|
3367
|
+
function collectFileSummary(resolved, projectDir) {
|
|
3368
|
+
const { config, origin, absPath } = resolved;
|
|
3369
|
+
const content = readFileOrNull(absPath);
|
|
3370
|
+
return {
|
|
3371
|
+
name: path8.basename(absPath),
|
|
3372
|
+
type: "file",
|
|
3373
|
+
role: config.role || "reference-doc",
|
|
3374
|
+
description: config.description || content?.slice(0, 100).split("\n")[0] || "",
|
|
3375
|
+
origin,
|
|
3376
|
+
existingClaudeMd: content ? content.slice(0, SOURCE_CONTENT_LIMIT) : void 0
|
|
3377
|
+
};
|
|
3378
|
+
}
|
|
3379
|
+
function formatSourcesForPrompt(sources2) {
|
|
3380
|
+
if (sources2.length === 0) return "";
|
|
3381
|
+
const parts = [
|
|
3382
|
+
"\n--- Related Sources ---",
|
|
3383
|
+
"This project works with these related sources. Reference them in the generated",
|
|
3384
|
+
"config where relevant \u2014 mention shared conventions, cross-repo workflows, and",
|
|
3385
|
+
"integration points.\n"
|
|
3386
|
+
];
|
|
3387
|
+
for (const source of sources2) {
|
|
3388
|
+
parts.push(`[${source.name}] (${source.role})`);
|
|
3389
|
+
if (source.description) parts.push(source.description);
|
|
3390
|
+
if (source.topLevelDirs?.length) {
|
|
3391
|
+
parts.push(`Key dirs: ${source.topLevelDirs.join(", ")}`);
|
|
3392
|
+
}
|
|
3393
|
+
if (source.keyFiles?.length) {
|
|
3394
|
+
parts.push(`Key files: ${source.keyFiles.join(", ")}`);
|
|
3395
|
+
}
|
|
3396
|
+
if (source.existingClaudeMd) {
|
|
3397
|
+
parts.push(`Their CLAUDE.md:
|
|
3398
|
+
${source.existingClaudeMd}`);
|
|
3399
|
+
} else if (source.readmeExcerpt) {
|
|
3400
|
+
parts.push(`Their README:
|
|
3401
|
+
${source.readmeExcerpt}`);
|
|
3402
|
+
}
|
|
3403
|
+
parts.push("");
|
|
3404
|
+
}
|
|
3405
|
+
parts.push("--- End Related Sources ---");
|
|
3406
|
+
return parts.join("\n");
|
|
3407
|
+
}
|
|
3408
|
+
|
|
3409
|
+
// src/ai/generate.ts
|
|
3410
|
+
init_config();
|
|
3411
|
+
|
|
3412
|
+
// src/utils/dependencies.ts
|
|
3413
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
3414
|
+
import { join as join2 } from "path";
|
|
3415
|
+
function readFileOrNull2(path37) {
|
|
3416
|
+
try {
|
|
3417
|
+
return readFileSync2(path37, "utf-8");
|
|
3418
|
+
} catch {
|
|
3419
|
+
return null;
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
function readJsonOrNull(path37) {
|
|
3423
|
+
const content = readFileOrNull2(path37);
|
|
3424
|
+
if (!content) return null;
|
|
3425
|
+
try {
|
|
3426
|
+
return JSON.parse(content);
|
|
3427
|
+
} catch {
|
|
3428
|
+
return null;
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
function extractNpmDeps(dir) {
|
|
3432
|
+
const pkg3 = readJsonOrNull(join2(dir, "package.json"));
|
|
3433
|
+
if (!pkg3) return [];
|
|
3434
|
+
const deps = {
|
|
3435
|
+
...pkg3.dependencies,
|
|
3436
|
+
...pkg3.devDependencies
|
|
3437
|
+
};
|
|
3438
|
+
const trivial = /* @__PURE__ */ new Set([
|
|
3439
|
+
"typescript",
|
|
3440
|
+
"@types/node",
|
|
3441
|
+
"tslib",
|
|
3442
|
+
"ts-node",
|
|
3443
|
+
"tsx",
|
|
3444
|
+
"prettier",
|
|
3445
|
+
"eslint",
|
|
3446
|
+
"@eslint/js",
|
|
3447
|
+
"rimraf",
|
|
3448
|
+
"cross-env",
|
|
3449
|
+
"dotenv",
|
|
3450
|
+
"nodemon",
|
|
3451
|
+
"husky",
|
|
3452
|
+
"lint-staged",
|
|
3453
|
+
"commitlint",
|
|
3454
|
+
"@commitlint/cli",
|
|
3455
|
+
"@commitlint/config-conventional"
|
|
3456
|
+
]);
|
|
3457
|
+
const trivialPatterns = [
|
|
3458
|
+
/^@rely-ai\//,
|
|
3459
|
+
/^@caliber-ai\//,
|
|
3460
|
+
/^eslint-/,
|
|
3461
|
+
/^@eslint\//,
|
|
3462
|
+
/^prettier-/,
|
|
3463
|
+
/^@typescript-eslint\//
|
|
3464
|
+
];
|
|
3465
|
+
return Object.keys(deps).filter((d) => !trivial.has(d) && !d.startsWith("@types/") && !trivialPatterns.some((p) => p.test(d))).slice(0, 30);
|
|
3466
|
+
}
|
|
3467
|
+
function extractPythonDeps(dir) {
|
|
3468
|
+
const reqTxt = readFileOrNull2(join2(dir, "requirements.txt"));
|
|
3469
|
+
if (reqTxt) {
|
|
3470
|
+
return reqTxt.split("\n").map((l) => l.trim().split(/[=<>!~\[]/)[0].trim()).filter((l) => l && !l.startsWith("#")).slice(0, 30);
|
|
3471
|
+
}
|
|
3472
|
+
const pyproject = readFileOrNull2(join2(dir, "pyproject.toml"));
|
|
3473
|
+
if (pyproject) {
|
|
3474
|
+
const depMatch = pyproject.match(/dependencies\s*=\s*\[([\s\S]*?)\]/);
|
|
3475
|
+
if (depMatch) {
|
|
3476
|
+
return depMatch[1].split("\n").map((l) => l.trim().replace(/["',]/g, "").split(/[=<>!~\[]/)[0].trim()).filter((l) => l.length > 0).slice(0, 30);
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
return [];
|
|
3480
|
+
}
|
|
3481
|
+
function extractGoDeps(dir) {
|
|
3482
|
+
const goMod = readFileOrNull2(join2(dir, "go.mod"));
|
|
3483
|
+
if (!goMod) return [];
|
|
3484
|
+
const requireBlock = goMod.match(/require\s*\(([\s\S]*?)\)/);
|
|
3485
|
+
if (!requireBlock) return [];
|
|
3486
|
+
return requireBlock[1].split("\n").map((l) => l.trim().split(/\s/)[0]).filter((l) => l && !l.startsWith("//")).map((l) => l.split("/").pop() || l).slice(0, 30);
|
|
3487
|
+
}
|
|
3488
|
+
function extractRustDeps(dir) {
|
|
3489
|
+
const cargo = readFileOrNull2(join2(dir, "Cargo.toml"));
|
|
3490
|
+
if (!cargo) return [];
|
|
3491
|
+
const depSection = cargo.match(/\[dependencies\]([\s\S]*?)(?:\[|$)/);
|
|
3492
|
+
if (!depSection) return [];
|
|
3493
|
+
return depSection[1].split("\n").map((l) => l.trim().split(/\s*=/)[0].trim()).filter((l) => l.length > 0 && !l.startsWith("#")).slice(0, 30);
|
|
3494
|
+
}
|
|
3495
|
+
function extractAllDeps(dir) {
|
|
3496
|
+
return [
|
|
3497
|
+
...extractNpmDeps(dir),
|
|
2967
3498
|
...extractPythonDeps(dir),
|
|
2968
3499
|
...extractGoDeps(dir),
|
|
2969
3500
|
...extractRustDeps(dir)
|
|
@@ -3094,6 +3625,12 @@ ${fingerprint.fileTree.slice(0, 50).join("\n")}`);
|
|
|
3094
3625
|
parts.push(`
|
|
3095
3626
|
Dependencies: ${allDeps.join(", ")}`);
|
|
3096
3627
|
}
|
|
3628
|
+
if (fingerprint.sources?.length) {
|
|
3629
|
+
parts.push("\nRelated Sources:");
|
|
3630
|
+
for (const s of fingerprint.sources) {
|
|
3631
|
+
parts.push(`- ${s.name} (${s.role}): ${s.description || "related project"}`);
|
|
3632
|
+
}
|
|
3633
|
+
}
|
|
3097
3634
|
return parts.join("\n");
|
|
3098
3635
|
}
|
|
3099
3636
|
async function generateSkill(context, topic, model) {
|
|
@@ -3409,6 +3946,11 @@ ${truncate(skill.content, LIMITS.SKILL_CHARS)}`);
|
|
|
3409
3946
|
(${existing.cursorSkills.length - LIMITS.SKILLS_MAX} more skills omitted)`);
|
|
3410
3947
|
}
|
|
3411
3948
|
}
|
|
3949
|
+
if (existing.personalLearnings) {
|
|
3950
|
+
parts.push(`
|
|
3951
|
+
--- Personal Learnings (developer-specific, include in generated configs) ---
|
|
3952
|
+
${existing.personalLearnings}`);
|
|
3953
|
+
}
|
|
3412
3954
|
const allDeps = extractAllDeps(process.cwd());
|
|
3413
3955
|
if (allDeps.length > 0) {
|
|
3414
3956
|
parts.push(`
|
|
@@ -3458,24 +4000,27 @@ ${f.content}
|
|
|
3458
4000
|
parts.push(header);
|
|
3459
4001
|
parts.push(codeLines.join("\n"));
|
|
3460
4002
|
}
|
|
4003
|
+
if (fingerprint.sources?.length) {
|
|
4004
|
+
parts.push(formatSourcesForPrompt(fingerprint.sources));
|
|
4005
|
+
}
|
|
3461
4006
|
return parts.join("\n");
|
|
3462
4007
|
}
|
|
3463
4008
|
|
|
3464
4009
|
// src/writers/index.ts
|
|
3465
|
-
import
|
|
4010
|
+
import fs15 from "fs";
|
|
3466
4011
|
|
|
3467
4012
|
// src/writers/claude/index.ts
|
|
3468
|
-
import
|
|
3469
|
-
import
|
|
4013
|
+
import fs9 from "fs";
|
|
4014
|
+
import path9 from "path";
|
|
3470
4015
|
function writeClaudeConfig(config) {
|
|
3471
4016
|
const written = [];
|
|
3472
|
-
|
|
4017
|
+
fs9.writeFileSync("CLAUDE.md", config.claudeMd);
|
|
3473
4018
|
written.push("CLAUDE.md");
|
|
3474
4019
|
if (config.skills?.length) {
|
|
3475
4020
|
for (const skill of config.skills) {
|
|
3476
|
-
const skillDir =
|
|
3477
|
-
if (!
|
|
3478
|
-
const skillPath =
|
|
4021
|
+
const skillDir = path9.join(".claude", "skills", skill.name);
|
|
4022
|
+
if (!fs9.existsSync(skillDir)) fs9.mkdirSync(skillDir, { recursive: true });
|
|
4023
|
+
const skillPath = path9.join(skillDir, "SKILL.md");
|
|
3479
4024
|
const frontmatter = [
|
|
3480
4025
|
"---",
|
|
3481
4026
|
`name: ${skill.name}`,
|
|
@@ -3483,49 +4028,49 @@ function writeClaudeConfig(config) {
|
|
|
3483
4028
|
"---",
|
|
3484
4029
|
""
|
|
3485
4030
|
].join("\n");
|
|
3486
|
-
|
|
4031
|
+
fs9.writeFileSync(skillPath, frontmatter + skill.content);
|
|
3487
4032
|
written.push(skillPath);
|
|
3488
4033
|
}
|
|
3489
4034
|
}
|
|
3490
4035
|
if (config.mcpServers && Object.keys(config.mcpServers).length > 0) {
|
|
3491
4036
|
let existingServers = {};
|
|
3492
4037
|
try {
|
|
3493
|
-
if (
|
|
3494
|
-
const existing = JSON.parse(
|
|
4038
|
+
if (fs9.existsSync(".mcp.json")) {
|
|
4039
|
+
const existing = JSON.parse(fs9.readFileSync(".mcp.json", "utf-8"));
|
|
3495
4040
|
if (existing.mcpServers) existingServers = existing.mcpServers;
|
|
3496
4041
|
}
|
|
3497
4042
|
} catch {
|
|
3498
4043
|
}
|
|
3499
4044
|
const mergedServers = { ...existingServers, ...config.mcpServers };
|
|
3500
|
-
|
|
4045
|
+
fs9.writeFileSync(".mcp.json", JSON.stringify({ mcpServers: mergedServers }, null, 2));
|
|
3501
4046
|
written.push(".mcp.json");
|
|
3502
4047
|
}
|
|
3503
4048
|
return written;
|
|
3504
4049
|
}
|
|
3505
4050
|
|
|
3506
4051
|
// src/writers/cursor/index.ts
|
|
3507
|
-
import
|
|
3508
|
-
import
|
|
4052
|
+
import fs10 from "fs";
|
|
4053
|
+
import path10 from "path";
|
|
3509
4054
|
function writeCursorConfig(config) {
|
|
3510
4055
|
const written = [];
|
|
3511
4056
|
if (config.cursorrules) {
|
|
3512
|
-
|
|
4057
|
+
fs10.writeFileSync(".cursorrules", config.cursorrules);
|
|
3513
4058
|
written.push(".cursorrules");
|
|
3514
4059
|
}
|
|
3515
4060
|
if (config.rules?.length) {
|
|
3516
|
-
const rulesDir =
|
|
3517
|
-
if (!
|
|
4061
|
+
const rulesDir = path10.join(".cursor", "rules");
|
|
4062
|
+
if (!fs10.existsSync(rulesDir)) fs10.mkdirSync(rulesDir, { recursive: true });
|
|
3518
4063
|
for (const rule of config.rules) {
|
|
3519
|
-
const rulePath =
|
|
3520
|
-
|
|
4064
|
+
const rulePath = path10.join(rulesDir, rule.filename);
|
|
4065
|
+
fs10.writeFileSync(rulePath, rule.content);
|
|
3521
4066
|
written.push(rulePath);
|
|
3522
4067
|
}
|
|
3523
4068
|
}
|
|
3524
4069
|
if (config.skills?.length) {
|
|
3525
4070
|
for (const skill of config.skills) {
|
|
3526
|
-
const skillDir =
|
|
3527
|
-
if (!
|
|
3528
|
-
const skillPath =
|
|
4071
|
+
const skillDir = path10.join(".cursor", "skills", skill.name);
|
|
4072
|
+
if (!fs10.existsSync(skillDir)) fs10.mkdirSync(skillDir, { recursive: true });
|
|
4073
|
+
const skillPath = path10.join(skillDir, "SKILL.md");
|
|
3529
4074
|
const frontmatter = [
|
|
3530
4075
|
"---",
|
|
3531
4076
|
`name: ${skill.name}`,
|
|
@@ -3533,41 +4078,41 @@ function writeCursorConfig(config) {
|
|
|
3533
4078
|
"---",
|
|
3534
4079
|
""
|
|
3535
4080
|
].join("\n");
|
|
3536
|
-
|
|
4081
|
+
fs10.writeFileSync(skillPath, frontmatter + skill.content);
|
|
3537
4082
|
written.push(skillPath);
|
|
3538
4083
|
}
|
|
3539
4084
|
}
|
|
3540
4085
|
if (config.mcpServers && Object.keys(config.mcpServers).length > 0) {
|
|
3541
4086
|
const cursorDir = ".cursor";
|
|
3542
|
-
if (!
|
|
3543
|
-
const mcpPath =
|
|
4087
|
+
if (!fs10.existsSync(cursorDir)) fs10.mkdirSync(cursorDir, { recursive: true });
|
|
4088
|
+
const mcpPath = path10.join(cursorDir, "mcp.json");
|
|
3544
4089
|
let existingServers = {};
|
|
3545
4090
|
try {
|
|
3546
|
-
if (
|
|
3547
|
-
const existing = JSON.parse(
|
|
4091
|
+
if (fs10.existsSync(mcpPath)) {
|
|
4092
|
+
const existing = JSON.parse(fs10.readFileSync(mcpPath, "utf-8"));
|
|
3548
4093
|
if (existing.mcpServers) existingServers = existing.mcpServers;
|
|
3549
4094
|
}
|
|
3550
4095
|
} catch {
|
|
3551
4096
|
}
|
|
3552
4097
|
const mergedServers = { ...existingServers, ...config.mcpServers };
|
|
3553
|
-
|
|
4098
|
+
fs10.writeFileSync(mcpPath, JSON.stringify({ mcpServers: mergedServers }, null, 2));
|
|
3554
4099
|
written.push(mcpPath);
|
|
3555
4100
|
}
|
|
3556
4101
|
return written;
|
|
3557
4102
|
}
|
|
3558
4103
|
|
|
3559
4104
|
// src/writers/codex/index.ts
|
|
3560
|
-
import
|
|
3561
|
-
import
|
|
4105
|
+
import fs11 from "fs";
|
|
4106
|
+
import path11 from "path";
|
|
3562
4107
|
function writeCodexConfig(config) {
|
|
3563
4108
|
const written = [];
|
|
3564
|
-
|
|
4109
|
+
fs11.writeFileSync("AGENTS.md", config.agentsMd);
|
|
3565
4110
|
written.push("AGENTS.md");
|
|
3566
4111
|
if (config.skills?.length) {
|
|
3567
4112
|
for (const skill of config.skills) {
|
|
3568
|
-
const skillDir =
|
|
3569
|
-
if (!
|
|
3570
|
-
const skillPath =
|
|
4113
|
+
const skillDir = path11.join(".agents", "skills", skill.name);
|
|
4114
|
+
if (!fs11.existsSync(skillDir)) fs11.mkdirSync(skillDir, { recursive: true });
|
|
4115
|
+
const skillPath = path11.join(skillDir, "SKILL.md");
|
|
3571
4116
|
const frontmatter = [
|
|
3572
4117
|
"---",
|
|
3573
4118
|
`name: ${skill.name}`,
|
|
@@ -3575,71 +4120,92 @@ function writeCodexConfig(config) {
|
|
|
3575
4120
|
"---",
|
|
3576
4121
|
""
|
|
3577
4122
|
].join("\n");
|
|
3578
|
-
|
|
4123
|
+
fs11.writeFileSync(skillPath, frontmatter + skill.content);
|
|
3579
4124
|
written.push(skillPath);
|
|
3580
4125
|
}
|
|
3581
4126
|
}
|
|
3582
4127
|
return written;
|
|
3583
4128
|
}
|
|
3584
4129
|
|
|
4130
|
+
// src/writers/github-copilot/index.ts
|
|
4131
|
+
import fs12 from "fs";
|
|
4132
|
+
import path12 from "path";
|
|
4133
|
+
function writeGithubCopilotConfig(config) {
|
|
4134
|
+
const written = [];
|
|
4135
|
+
if (config.instructions) {
|
|
4136
|
+
fs12.mkdirSync(".github", { recursive: true });
|
|
4137
|
+
fs12.writeFileSync(path12.join(".github", "copilot-instructions.md"), config.instructions);
|
|
4138
|
+
written.push(".github/copilot-instructions.md");
|
|
4139
|
+
}
|
|
4140
|
+
if (config.instructionFiles?.length) {
|
|
4141
|
+
const instructionsDir = path12.join(".github", "instructions");
|
|
4142
|
+
fs12.mkdirSync(instructionsDir, { recursive: true });
|
|
4143
|
+
for (const file of config.instructionFiles) {
|
|
4144
|
+
fs12.writeFileSync(path12.join(instructionsDir, file.filename), file.content);
|
|
4145
|
+
written.push(`.github/instructions/${file.filename}`);
|
|
4146
|
+
}
|
|
4147
|
+
}
|
|
4148
|
+
return written;
|
|
4149
|
+
}
|
|
4150
|
+
|
|
3585
4151
|
// src/writers/backup.ts
|
|
3586
4152
|
init_constants();
|
|
3587
|
-
import
|
|
3588
|
-
import
|
|
4153
|
+
import fs13 from "fs";
|
|
4154
|
+
import path13 from "path";
|
|
3589
4155
|
function createBackup(files) {
|
|
3590
4156
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
3591
|
-
const backupDir =
|
|
4157
|
+
const backupDir = path13.join(BACKUPS_DIR, timestamp);
|
|
3592
4158
|
for (const file of files) {
|
|
3593
|
-
if (!
|
|
3594
|
-
const dest =
|
|
3595
|
-
const destDir =
|
|
3596
|
-
if (!
|
|
3597
|
-
|
|
4159
|
+
if (!fs13.existsSync(file)) continue;
|
|
4160
|
+
const dest = path13.join(backupDir, file);
|
|
4161
|
+
const destDir = path13.dirname(dest);
|
|
4162
|
+
if (!fs13.existsSync(destDir)) {
|
|
4163
|
+
fs13.mkdirSync(destDir, { recursive: true });
|
|
3598
4164
|
}
|
|
3599
|
-
|
|
4165
|
+
fs13.copyFileSync(file, dest);
|
|
3600
4166
|
}
|
|
3601
4167
|
return backupDir;
|
|
3602
4168
|
}
|
|
3603
4169
|
function restoreBackup(backupDir, file) {
|
|
3604
|
-
const backupFile =
|
|
3605
|
-
if (!
|
|
3606
|
-
const destDir =
|
|
3607
|
-
if (!
|
|
3608
|
-
|
|
4170
|
+
const backupFile = path13.join(backupDir, file);
|
|
4171
|
+
if (!fs13.existsSync(backupFile)) return false;
|
|
4172
|
+
const destDir = path13.dirname(file);
|
|
4173
|
+
if (!fs13.existsSync(destDir)) {
|
|
4174
|
+
fs13.mkdirSync(destDir, { recursive: true });
|
|
3609
4175
|
}
|
|
3610
|
-
|
|
4176
|
+
fs13.copyFileSync(backupFile, file);
|
|
3611
4177
|
return true;
|
|
3612
4178
|
}
|
|
3613
4179
|
|
|
3614
4180
|
// src/writers/manifest.ts
|
|
3615
4181
|
init_constants();
|
|
3616
|
-
import
|
|
4182
|
+
import fs14 from "fs";
|
|
3617
4183
|
import crypto2 from "crypto";
|
|
3618
4184
|
function readManifest() {
|
|
3619
4185
|
try {
|
|
3620
|
-
if (!
|
|
3621
|
-
return JSON.parse(
|
|
4186
|
+
if (!fs14.existsSync(MANIFEST_FILE)) return null;
|
|
4187
|
+
return JSON.parse(fs14.readFileSync(MANIFEST_FILE, "utf-8"));
|
|
3622
4188
|
} catch {
|
|
3623
4189
|
return null;
|
|
3624
4190
|
}
|
|
3625
4191
|
}
|
|
3626
4192
|
function writeManifest(manifest) {
|
|
3627
|
-
if (!
|
|
3628
|
-
|
|
4193
|
+
if (!fs14.existsSync(CALIBER_DIR)) {
|
|
4194
|
+
fs14.mkdirSync(CALIBER_DIR, { recursive: true });
|
|
3629
4195
|
}
|
|
3630
|
-
|
|
4196
|
+
fs14.writeFileSync(MANIFEST_FILE, JSON.stringify(manifest, null, 2));
|
|
3631
4197
|
}
|
|
3632
4198
|
function fileChecksum(filePath) {
|
|
3633
|
-
const content =
|
|
4199
|
+
const content = fs14.readFileSync(filePath);
|
|
3634
4200
|
return crypto2.createHash("sha256").update(content).digest("hex");
|
|
3635
4201
|
}
|
|
3636
4202
|
|
|
3637
4203
|
// src/writers/index.ts
|
|
3638
4204
|
function writeSetup(setup) {
|
|
3639
4205
|
const filesToWrite = getFilesToWrite(setup);
|
|
3640
|
-
const filesToDelete = (setup.deletions || []).map((d) => d.filePath).filter((f) =>
|
|
4206
|
+
const filesToDelete = (setup.deletions || []).map((d) => d.filePath).filter((f) => fs15.existsSync(f));
|
|
3641
4207
|
const existingFiles = [
|
|
3642
|
-
...filesToWrite.filter((f) =>
|
|
4208
|
+
...filesToWrite.filter((f) => fs15.existsSync(f)),
|
|
3643
4209
|
...filesToDelete
|
|
3644
4210
|
];
|
|
3645
4211
|
const backupDir = existingFiles.length > 0 ? createBackup(existingFiles) : void 0;
|
|
@@ -3653,9 +4219,12 @@ function writeSetup(setup) {
|
|
|
3653
4219
|
if (setup.targetAgent.includes("codex") && setup.codex) {
|
|
3654
4220
|
written.push(...writeCodexConfig(setup.codex));
|
|
3655
4221
|
}
|
|
4222
|
+
if (setup.targetAgent.includes("github-copilot") && setup.copilot) {
|
|
4223
|
+
written.push(...writeGithubCopilotConfig(setup.copilot));
|
|
4224
|
+
}
|
|
3656
4225
|
const deleted = [];
|
|
3657
4226
|
for (const filePath of filesToDelete) {
|
|
3658
|
-
|
|
4227
|
+
fs15.unlinkSync(filePath);
|
|
3659
4228
|
deleted.push(filePath);
|
|
3660
4229
|
}
|
|
3661
4230
|
ensureGitignore();
|
|
@@ -3685,8 +4254,8 @@ function undoSetup() {
|
|
|
3685
4254
|
const removed = [];
|
|
3686
4255
|
for (const entry of manifest.entries) {
|
|
3687
4256
|
if (entry.action === "created") {
|
|
3688
|
-
if (
|
|
3689
|
-
|
|
4257
|
+
if (fs15.existsSync(entry.path)) {
|
|
4258
|
+
fs15.unlinkSync(entry.path);
|
|
3690
4259
|
removed.push(entry.path);
|
|
3691
4260
|
}
|
|
3692
4261
|
} else if ((entry.action === "modified" || entry.action === "deleted") && manifest.backupDir) {
|
|
@@ -3696,8 +4265,8 @@ function undoSetup() {
|
|
|
3696
4265
|
}
|
|
3697
4266
|
}
|
|
3698
4267
|
const { MANIFEST_FILE: MANIFEST_FILE2 } = (init_constants(), __toCommonJS(constants_exports));
|
|
3699
|
-
if (
|
|
3700
|
-
|
|
4268
|
+
if (fs15.existsSync(MANIFEST_FILE2)) {
|
|
4269
|
+
fs15.unlinkSync(MANIFEST_FILE2);
|
|
3701
4270
|
}
|
|
3702
4271
|
return { restored, removed };
|
|
3703
4272
|
}
|
|
@@ -3728,27 +4297,33 @@ function getFilesToWrite(setup) {
|
|
|
3728
4297
|
for (const s of setup.codex.skills) files.push(`.agents/skills/${s.name}/SKILL.md`);
|
|
3729
4298
|
}
|
|
3730
4299
|
}
|
|
4300
|
+
if (setup.targetAgent.includes("github-copilot") && setup.copilot) {
|
|
4301
|
+
if (setup.copilot.instructions) files.push(".github/copilot-instructions.md");
|
|
4302
|
+
if (setup.copilot.instructionFiles) {
|
|
4303
|
+
for (const f of setup.copilot.instructionFiles) files.push(`.github/instructions/${f.filename}`);
|
|
4304
|
+
}
|
|
4305
|
+
}
|
|
3731
4306
|
return files;
|
|
3732
4307
|
}
|
|
3733
4308
|
function ensureGitignore() {
|
|
3734
4309
|
const gitignorePath = ".gitignore";
|
|
3735
|
-
if (
|
|
3736
|
-
const content =
|
|
4310
|
+
if (fs15.existsSync(gitignorePath)) {
|
|
4311
|
+
const content = fs15.readFileSync(gitignorePath, "utf-8");
|
|
3737
4312
|
if (!content.includes(".caliber/")) {
|
|
3738
|
-
|
|
4313
|
+
fs15.appendFileSync(gitignorePath, "\n# Caliber local state\n.caliber/\n");
|
|
3739
4314
|
}
|
|
3740
4315
|
} else {
|
|
3741
|
-
|
|
4316
|
+
fs15.writeFileSync(gitignorePath, "# Caliber local state\n.caliber/\n");
|
|
3742
4317
|
}
|
|
3743
4318
|
}
|
|
3744
4319
|
|
|
3745
4320
|
// src/writers/staging.ts
|
|
3746
4321
|
init_constants();
|
|
3747
|
-
import
|
|
3748
|
-
import
|
|
3749
|
-
var STAGED_DIR =
|
|
3750
|
-
var PROPOSED_DIR =
|
|
3751
|
-
var CURRENT_DIR =
|
|
4322
|
+
import fs16 from "fs";
|
|
4323
|
+
import path14 from "path";
|
|
4324
|
+
var STAGED_DIR = path14.join(CALIBER_DIR, "staged");
|
|
4325
|
+
var PROPOSED_DIR = path14.join(STAGED_DIR, "proposed");
|
|
4326
|
+
var CURRENT_DIR = path14.join(STAGED_DIR, "current");
|
|
3752
4327
|
function normalizeContent(content) {
|
|
3753
4328
|
return content.split("\n").map((line) => line.trimEnd()).join("\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
3754
4329
|
}
|
|
@@ -3758,20 +4333,20 @@ function stageFiles(files, projectDir) {
|
|
|
3758
4333
|
let modifiedFiles = 0;
|
|
3759
4334
|
const stagedFiles = [];
|
|
3760
4335
|
for (const file of files) {
|
|
3761
|
-
const originalPath =
|
|
3762
|
-
if (
|
|
3763
|
-
const existing =
|
|
4336
|
+
const originalPath = path14.join(projectDir, file.path);
|
|
4337
|
+
if (fs16.existsSync(originalPath)) {
|
|
4338
|
+
const existing = fs16.readFileSync(originalPath, "utf-8");
|
|
3764
4339
|
if (normalizeContent(existing) === normalizeContent(file.content)) {
|
|
3765
4340
|
continue;
|
|
3766
4341
|
}
|
|
3767
4342
|
}
|
|
3768
|
-
const proposedPath =
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
if (
|
|
3772
|
-
const currentPath =
|
|
3773
|
-
|
|
3774
|
-
|
|
4343
|
+
const proposedPath = path14.join(PROPOSED_DIR, file.path);
|
|
4344
|
+
fs16.mkdirSync(path14.dirname(proposedPath), { recursive: true });
|
|
4345
|
+
fs16.writeFileSync(proposedPath, file.content);
|
|
4346
|
+
if (fs16.existsSync(originalPath)) {
|
|
4347
|
+
const currentPath = path14.join(CURRENT_DIR, file.path);
|
|
4348
|
+
fs16.mkdirSync(path14.dirname(currentPath), { recursive: true });
|
|
4349
|
+
fs16.copyFileSync(originalPath, currentPath);
|
|
3775
4350
|
modifiedFiles++;
|
|
3776
4351
|
stagedFiles.push({ relativePath: file.path, proposedPath, currentPath, originalPath, isNew: false });
|
|
3777
4352
|
} else {
|
|
@@ -3782,13 +4357,13 @@ function stageFiles(files, projectDir) {
|
|
|
3782
4357
|
return { newFiles, modifiedFiles, stagedFiles };
|
|
3783
4358
|
}
|
|
3784
4359
|
function cleanupStaging() {
|
|
3785
|
-
if (
|
|
3786
|
-
|
|
4360
|
+
if (fs16.existsSync(STAGED_DIR)) {
|
|
4361
|
+
fs16.rmSync(STAGED_DIR, { recursive: true, force: true });
|
|
3787
4362
|
}
|
|
3788
4363
|
}
|
|
3789
4364
|
|
|
3790
4365
|
// src/commands/setup-files.ts
|
|
3791
|
-
import
|
|
4366
|
+
import fs17 from "fs";
|
|
3792
4367
|
function buildSkillContent(skill) {
|
|
3793
4368
|
const frontmatter = `---
|
|
3794
4369
|
name: ${skill.name}
|
|
@@ -3836,8 +4411,18 @@ function collectSetupFiles(setup, targetAgent) {
|
|
|
3836
4411
|
}
|
|
3837
4412
|
}
|
|
3838
4413
|
}
|
|
4414
|
+
const copilot = setup.copilot;
|
|
4415
|
+
if (copilot) {
|
|
4416
|
+
if (copilot.instructions) files.push({ path: ".github/copilot-instructions.md", content: copilot.instructions });
|
|
4417
|
+
const instructionFiles = copilot.instructionFiles;
|
|
4418
|
+
if (Array.isArray(instructionFiles)) {
|
|
4419
|
+
for (const file of instructionFiles) {
|
|
4420
|
+
files.push({ path: `.github/instructions/${file.filename}`, content: file.content });
|
|
4421
|
+
}
|
|
4422
|
+
}
|
|
4423
|
+
}
|
|
3839
4424
|
const codexTargeted = targetAgent ? targetAgent.includes("codex") : false;
|
|
3840
|
-
if (codexTargeted && !
|
|
4425
|
+
if (codexTargeted && !fs17.existsSync("AGENTS.md") && !(codex && codex.agentsMd)) {
|
|
3841
4426
|
const agentRefs = [];
|
|
3842
4427
|
if (claude) agentRefs.push("See `CLAUDE.md` for Claude Code configuration.");
|
|
3843
4428
|
if (cursor) agentRefs.push("See `.cursor/rules/` for Cursor rules.");
|
|
@@ -3855,27 +4440,27 @@ ${agentRefs.join(" ")}
|
|
|
3855
4440
|
|
|
3856
4441
|
// src/lib/hooks.ts
|
|
3857
4442
|
init_resolve_caliber();
|
|
3858
|
-
import
|
|
3859
|
-
import
|
|
4443
|
+
import fs19 from "fs";
|
|
4444
|
+
import path15 from "path";
|
|
3860
4445
|
import { execSync as execSync7 } from "child_process";
|
|
3861
|
-
var SETTINGS_PATH =
|
|
4446
|
+
var SETTINGS_PATH = path15.join(".claude", "settings.json");
|
|
3862
4447
|
var REFRESH_TAIL = "refresh --quiet";
|
|
3863
4448
|
var HOOK_DESCRIPTION = "Caliber: auto-refreshing docs based on code changes";
|
|
3864
4449
|
function getHookCommand() {
|
|
3865
4450
|
return `${resolveCaliber()} ${REFRESH_TAIL}`;
|
|
3866
4451
|
}
|
|
3867
4452
|
function readSettings() {
|
|
3868
|
-
if (!
|
|
4453
|
+
if (!fs19.existsSync(SETTINGS_PATH)) return {};
|
|
3869
4454
|
try {
|
|
3870
|
-
return JSON.parse(
|
|
4455
|
+
return JSON.parse(fs19.readFileSync(SETTINGS_PATH, "utf-8"));
|
|
3871
4456
|
} catch {
|
|
3872
4457
|
return {};
|
|
3873
4458
|
}
|
|
3874
4459
|
}
|
|
3875
4460
|
function writeSettings(settings) {
|
|
3876
|
-
const dir =
|
|
3877
|
-
if (!
|
|
3878
|
-
|
|
4461
|
+
const dir = path15.dirname(SETTINGS_PATH);
|
|
4462
|
+
if (!fs19.existsSync(dir)) fs19.mkdirSync(dir, { recursive: true });
|
|
4463
|
+
fs19.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
3879
4464
|
}
|
|
3880
4465
|
function findHookIndex(sessionEnd) {
|
|
3881
4466
|
return sessionEnd.findIndex(
|
|
@@ -3938,19 +4523,19 @@ ${PRECOMMIT_END}`;
|
|
|
3938
4523
|
function getGitHooksDir() {
|
|
3939
4524
|
try {
|
|
3940
4525
|
const gitDir = execSync7("git rev-parse --git-dir", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
3941
|
-
return
|
|
4526
|
+
return path15.join(gitDir, "hooks");
|
|
3942
4527
|
} catch {
|
|
3943
4528
|
return null;
|
|
3944
4529
|
}
|
|
3945
4530
|
}
|
|
3946
4531
|
function getPreCommitPath() {
|
|
3947
4532
|
const hooksDir = getGitHooksDir();
|
|
3948
|
-
return hooksDir ?
|
|
4533
|
+
return hooksDir ? path15.join(hooksDir, "pre-commit") : null;
|
|
3949
4534
|
}
|
|
3950
4535
|
function isPreCommitHookInstalled() {
|
|
3951
4536
|
const hookPath = getPreCommitPath();
|
|
3952
|
-
if (!hookPath || !
|
|
3953
|
-
const content =
|
|
4537
|
+
if (!hookPath || !fs19.existsSync(hookPath)) return false;
|
|
4538
|
+
const content = fs19.readFileSync(hookPath, "utf-8");
|
|
3954
4539
|
return content.includes(PRECOMMIT_START);
|
|
3955
4540
|
}
|
|
3956
4541
|
function installPreCommitHook() {
|
|
@@ -3959,44 +4544,44 @@ function installPreCommitHook() {
|
|
|
3959
4544
|
}
|
|
3960
4545
|
const hookPath = getPreCommitPath();
|
|
3961
4546
|
if (!hookPath) return { installed: false, alreadyInstalled: false };
|
|
3962
|
-
const hooksDir =
|
|
3963
|
-
if (!
|
|
4547
|
+
const hooksDir = path15.dirname(hookPath);
|
|
4548
|
+
if (!fs19.existsSync(hooksDir)) fs19.mkdirSync(hooksDir, { recursive: true });
|
|
3964
4549
|
let content = "";
|
|
3965
|
-
if (
|
|
3966
|
-
content =
|
|
4550
|
+
if (fs19.existsSync(hookPath)) {
|
|
4551
|
+
content = fs19.readFileSync(hookPath, "utf-8");
|
|
3967
4552
|
if (!content.endsWith("\n")) content += "\n";
|
|
3968
4553
|
content += "\n" + getPrecommitBlock() + "\n";
|
|
3969
4554
|
} else {
|
|
3970
4555
|
content = "#!/bin/sh\n\n" + getPrecommitBlock() + "\n";
|
|
3971
4556
|
}
|
|
3972
|
-
|
|
3973
|
-
|
|
4557
|
+
fs19.writeFileSync(hookPath, content);
|
|
4558
|
+
fs19.chmodSync(hookPath, 493);
|
|
3974
4559
|
return { installed: true, alreadyInstalled: false };
|
|
3975
4560
|
}
|
|
3976
4561
|
function removePreCommitHook() {
|
|
3977
4562
|
const hookPath = getPreCommitPath();
|
|
3978
|
-
if (!hookPath || !
|
|
4563
|
+
if (!hookPath || !fs19.existsSync(hookPath)) {
|
|
3979
4564
|
return { removed: false, notFound: true };
|
|
3980
4565
|
}
|
|
3981
|
-
let content =
|
|
4566
|
+
let content = fs19.readFileSync(hookPath, "utf-8");
|
|
3982
4567
|
if (!content.includes(PRECOMMIT_START)) {
|
|
3983
4568
|
return { removed: false, notFound: true };
|
|
3984
4569
|
}
|
|
3985
4570
|
const regex = new RegExp(`\\n?${PRECOMMIT_START}[\\s\\S]*?${PRECOMMIT_END}\\n?`);
|
|
3986
4571
|
content = content.replace(regex, "\n");
|
|
3987
4572
|
if (content.trim() === "#!/bin/sh" || content.trim() === "") {
|
|
3988
|
-
|
|
4573
|
+
fs19.unlinkSync(hookPath);
|
|
3989
4574
|
} else {
|
|
3990
|
-
|
|
4575
|
+
fs19.writeFileSync(hookPath, content);
|
|
3991
4576
|
}
|
|
3992
4577
|
return { removed: true, notFound: false };
|
|
3993
4578
|
}
|
|
3994
4579
|
|
|
3995
4580
|
// src/lib/learning-hooks.ts
|
|
3996
4581
|
init_resolve_caliber();
|
|
3997
|
-
import
|
|
3998
|
-
import
|
|
3999
|
-
var SETTINGS_PATH2 =
|
|
4582
|
+
import fs20 from "fs";
|
|
4583
|
+
import path16 from "path";
|
|
4584
|
+
var SETTINGS_PATH2 = path16.join(".claude", "settings.json");
|
|
4000
4585
|
var HOOK_TAILS = [
|
|
4001
4586
|
{ event: "PostToolUse", tail: "learn observe", description: "Caliber: recording tool usage for session learning" },
|
|
4002
4587
|
{ event: "PostToolUseFailure", tail: "learn observe --failure", description: "Caliber: recording tool failure for session learning" },
|
|
@@ -4013,17 +4598,17 @@ function getHookConfigs() {
|
|
|
4013
4598
|
}));
|
|
4014
4599
|
}
|
|
4015
4600
|
function readSettings2() {
|
|
4016
|
-
if (!
|
|
4601
|
+
if (!fs20.existsSync(SETTINGS_PATH2)) return {};
|
|
4017
4602
|
try {
|
|
4018
|
-
return JSON.parse(
|
|
4603
|
+
return JSON.parse(fs20.readFileSync(SETTINGS_PATH2, "utf-8"));
|
|
4019
4604
|
} catch {
|
|
4020
4605
|
return {};
|
|
4021
4606
|
}
|
|
4022
4607
|
}
|
|
4023
4608
|
function writeSettings2(settings) {
|
|
4024
|
-
const dir =
|
|
4025
|
-
if (!
|
|
4026
|
-
|
|
4609
|
+
const dir = path16.dirname(SETTINGS_PATH2);
|
|
4610
|
+
if (!fs20.existsSync(dir)) fs20.mkdirSync(dir, { recursive: true });
|
|
4611
|
+
fs20.writeFileSync(SETTINGS_PATH2, JSON.stringify(settings, null, 2));
|
|
4027
4612
|
}
|
|
4028
4613
|
function hasLearningHook(matchers, tail) {
|
|
4029
4614
|
return matchers.some((entry) => entry.hooks?.some((h) => isCaliberCommand(h.command, tail)));
|
|
@@ -4057,7 +4642,7 @@ function installLearningHooks() {
|
|
|
4057
4642
|
writeSettings2(settings);
|
|
4058
4643
|
return { installed: true, alreadyInstalled: false };
|
|
4059
4644
|
}
|
|
4060
|
-
var CURSOR_HOOKS_PATH =
|
|
4645
|
+
var CURSOR_HOOKS_PATH = path16.join(".cursor", "hooks.json");
|
|
4061
4646
|
var CURSOR_HOOK_EVENTS = [
|
|
4062
4647
|
{ event: "postToolUse", tail: "learn observe" },
|
|
4063
4648
|
{ event: "postToolUseFailure", tail: "learn observe --failure" },
|
|
@@ -4065,17 +4650,17 @@ var CURSOR_HOOK_EVENTS = [
|
|
|
4065
4650
|
{ event: "sessionEnd", tail: "learn finalize --auto" }
|
|
4066
4651
|
];
|
|
4067
4652
|
function readCursorHooks() {
|
|
4068
|
-
if (!
|
|
4653
|
+
if (!fs20.existsSync(CURSOR_HOOKS_PATH)) return { version: 1, hooks: {} };
|
|
4069
4654
|
try {
|
|
4070
|
-
return JSON.parse(
|
|
4655
|
+
return JSON.parse(fs20.readFileSync(CURSOR_HOOKS_PATH, "utf-8"));
|
|
4071
4656
|
} catch {
|
|
4072
4657
|
return { version: 1, hooks: {} };
|
|
4073
4658
|
}
|
|
4074
4659
|
}
|
|
4075
4660
|
function writeCursorHooks(config) {
|
|
4076
|
-
const dir =
|
|
4077
|
-
if (!
|
|
4078
|
-
|
|
4661
|
+
const dir = path16.dirname(CURSOR_HOOKS_PATH);
|
|
4662
|
+
if (!fs20.existsSync(dir)) fs20.mkdirSync(dir, { recursive: true });
|
|
4663
|
+
fs20.writeFileSync(CURSOR_HOOKS_PATH, JSON.stringify(config, null, 2));
|
|
4079
4664
|
}
|
|
4080
4665
|
function hasCursorHook(entries, tail) {
|
|
4081
4666
|
return entries.some((e) => isCaliberCommand(e.command, tail));
|
|
@@ -4145,22 +4730,22 @@ function removeLearningHooks() {
|
|
|
4145
4730
|
|
|
4146
4731
|
// src/lib/state.ts
|
|
4147
4732
|
init_constants();
|
|
4148
|
-
import
|
|
4149
|
-
import
|
|
4733
|
+
import fs21 from "fs";
|
|
4734
|
+
import path17 from "path";
|
|
4150
4735
|
import { execSync as execSync8 } from "child_process";
|
|
4151
|
-
var STATE_FILE =
|
|
4736
|
+
var STATE_FILE = path17.join(CALIBER_DIR, ".caliber-state.json");
|
|
4152
4737
|
function normalizeTargetAgent(value) {
|
|
4153
4738
|
if (Array.isArray(value)) return value;
|
|
4154
4739
|
if (typeof value === "string") {
|
|
4155
4740
|
if (value === "both") return ["claude", "cursor"];
|
|
4156
|
-
if (["claude", "cursor", "codex"].includes(value)) return [value];
|
|
4741
|
+
if (["claude", "cursor", "codex", "github-copilot"].includes(value)) return [value];
|
|
4157
4742
|
}
|
|
4158
4743
|
return void 0;
|
|
4159
4744
|
}
|
|
4160
4745
|
function readState() {
|
|
4161
4746
|
try {
|
|
4162
|
-
if (!
|
|
4163
|
-
const raw = JSON.parse(
|
|
4747
|
+
if (!fs21.existsSync(STATE_FILE)) return null;
|
|
4748
|
+
const raw = JSON.parse(fs21.readFileSync(STATE_FILE, "utf-8"));
|
|
4164
4749
|
if (raw.targetAgent) raw.targetAgent = normalizeTargetAgent(raw.targetAgent);
|
|
4165
4750
|
return raw;
|
|
4166
4751
|
} catch {
|
|
@@ -4168,10 +4753,10 @@ function readState() {
|
|
|
4168
4753
|
}
|
|
4169
4754
|
}
|
|
4170
4755
|
function writeState(state) {
|
|
4171
|
-
if (!
|
|
4172
|
-
|
|
4756
|
+
if (!fs21.existsSync(CALIBER_DIR)) {
|
|
4757
|
+
fs21.mkdirSync(CALIBER_DIR, { recursive: true });
|
|
4173
4758
|
}
|
|
4174
|
-
|
|
4759
|
+
fs21.writeFileSync(STATE_FILE, JSON.stringify(state, null, 2));
|
|
4175
4760
|
}
|
|
4176
4761
|
function getCurrentHeadSha() {
|
|
4177
4762
|
try {
|
|
@@ -4287,11 +4872,11 @@ async function runInteractiveProviderSetup(options) {
|
|
|
4287
4872
|
|
|
4288
4873
|
// src/scoring/index.ts
|
|
4289
4874
|
import { existsSync as existsSync7 } from "fs";
|
|
4290
|
-
import { join as
|
|
4875
|
+
import { join as join9 } from "path";
|
|
4291
4876
|
|
|
4292
4877
|
// src/scoring/checks/existence.ts
|
|
4293
|
-
import { existsSync as
|
|
4294
|
-
import { join as
|
|
4878
|
+
import { existsSync as existsSync3, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
|
|
4879
|
+
import { join as join3 } from "path";
|
|
4295
4880
|
|
|
4296
4881
|
// src/scoring/constants.ts
|
|
4297
4882
|
var POINTS_CLAUDE_MD_EXISTS = 6;
|
|
@@ -4319,6 +4904,8 @@ var POINTS_HOOKS = 2;
|
|
|
4319
4904
|
var POINTS_AGENTS_MD = 1;
|
|
4320
4905
|
var POINTS_OPEN_SKILLS_FORMAT = 2;
|
|
4321
4906
|
var POINTS_LEARNED_CONTENT = 2;
|
|
4907
|
+
var POINTS_SOURCES_CONFIGURED = 3;
|
|
4908
|
+
var POINTS_SOURCES_REFERENCED = 3;
|
|
4322
4909
|
var TOKEN_BUDGET_THRESHOLDS = [
|
|
4323
4910
|
{ maxTokens: 2e3, points: 6 },
|
|
4324
4911
|
{ maxTokens: 3500, points: 5 },
|
|
@@ -4382,6 +4969,9 @@ var BOTH_ONLY_CHECKS = /* @__PURE__ */ new Set([
|
|
|
4382
4969
|
var CODEX_ONLY_CHECKS = /* @__PURE__ */ new Set([
|
|
4383
4970
|
"codex_agents_md_exists"
|
|
4384
4971
|
]);
|
|
4972
|
+
var COPILOT_ONLY_CHECKS = /* @__PURE__ */ new Set([
|
|
4973
|
+
"copilot_instructions_exists"
|
|
4974
|
+
]);
|
|
4385
4975
|
var NON_CODEX_CHECKS = /* @__PURE__ */ new Set([
|
|
4386
4976
|
"agents_md_exists"
|
|
4387
4977
|
]);
|
|
@@ -4402,13 +4992,13 @@ function computeGrade(score) {
|
|
|
4402
4992
|
// src/scoring/checks/existence.ts
|
|
4403
4993
|
function countFiles(dir, pattern) {
|
|
4404
4994
|
try {
|
|
4405
|
-
return
|
|
4995
|
+
return readdirSync2(dir, { recursive: true }).map(String).filter((f) => pattern.test(f));
|
|
4406
4996
|
} catch {
|
|
4407
4997
|
return [];
|
|
4408
4998
|
}
|
|
4409
4999
|
}
|
|
4410
5000
|
function hasMcpServers(dir) {
|
|
4411
|
-
const
|
|
5001
|
+
const sources2 = [];
|
|
4412
5002
|
let count = 0;
|
|
4413
5003
|
const mcpFiles = [
|
|
4414
5004
|
".mcp.json",
|
|
@@ -4418,21 +5008,21 @@ function hasMcpServers(dir) {
|
|
|
4418
5008
|
];
|
|
4419
5009
|
for (const rel of mcpFiles) {
|
|
4420
5010
|
try {
|
|
4421
|
-
const content =
|
|
5011
|
+
const content = readFileSync3(join3(dir, rel), "utf-8");
|
|
4422
5012
|
const parsed = JSON.parse(content);
|
|
4423
5013
|
const servers = parsed.mcpServers;
|
|
4424
5014
|
if (servers && Object.keys(servers).length > 0) {
|
|
4425
5015
|
count += Object.keys(servers).length;
|
|
4426
|
-
|
|
5016
|
+
sources2.push(rel);
|
|
4427
5017
|
}
|
|
4428
5018
|
} catch {
|
|
4429
5019
|
}
|
|
4430
5020
|
}
|
|
4431
|
-
return { count, sources };
|
|
5021
|
+
return { count, sources: sources2 };
|
|
4432
5022
|
}
|
|
4433
5023
|
function checkExistence(dir) {
|
|
4434
5024
|
const checks = [];
|
|
4435
|
-
const claudeMdExists =
|
|
5025
|
+
const claudeMdExists = existsSync3(join3(dir, "CLAUDE.md"));
|
|
4436
5026
|
checks.push({
|
|
4437
5027
|
id: "claude_md_exists",
|
|
4438
5028
|
name: "CLAUDE.md exists",
|
|
@@ -4448,8 +5038,8 @@ function checkExistence(dir) {
|
|
|
4448
5038
|
instruction: "Create CLAUDE.md with project context, commands, architecture, and conventions."
|
|
4449
5039
|
}
|
|
4450
5040
|
});
|
|
4451
|
-
const hasCursorrules =
|
|
4452
|
-
const cursorRulesDir =
|
|
5041
|
+
const hasCursorrules = existsSync3(join3(dir, ".cursorrules"));
|
|
5042
|
+
const cursorRulesDir = existsSync3(join3(dir, ".cursor", "rules"));
|
|
4453
5043
|
const cursorRulesExist = hasCursorrules || cursorRulesDir;
|
|
4454
5044
|
checks.push({
|
|
4455
5045
|
id: "cursor_rules_exist",
|
|
@@ -4466,7 +5056,7 @@ function checkExistence(dir) {
|
|
|
4466
5056
|
instruction: "Create .cursor/rules/ with project-specific Cursor rules."
|
|
4467
5057
|
}
|
|
4468
5058
|
});
|
|
4469
|
-
const agentsMdExists =
|
|
5059
|
+
const agentsMdExists = existsSync3(join3(dir, "AGENTS.md"));
|
|
4470
5060
|
checks.push({
|
|
4471
5061
|
id: "codex_agents_md_exists",
|
|
4472
5062
|
name: "AGENTS.md exists",
|
|
@@ -4478,12 +5068,28 @@ function checkExistence(dir) {
|
|
|
4478
5068
|
suggestion: agentsMdExists ? void 0 : "Create AGENTS.md with project context for Codex",
|
|
4479
5069
|
fix: agentsMdExists ? void 0 : {
|
|
4480
5070
|
action: "create_file",
|
|
4481
|
-
data: { file: "AGENTS.md" },
|
|
4482
|
-
instruction: "Create AGENTS.md with project context for Codex."
|
|
5071
|
+
data: { file: "AGENTS.md" },
|
|
5072
|
+
instruction: "Create AGENTS.md with project context for Codex."
|
|
5073
|
+
}
|
|
5074
|
+
});
|
|
5075
|
+
const copilotInstructionsExists = existsSync3(join3(dir, ".github", "copilot-instructions.md"));
|
|
5076
|
+
checks.push({
|
|
5077
|
+
id: "copilot_instructions_exists",
|
|
5078
|
+
name: "Copilot instructions exist",
|
|
5079
|
+
category: "existence",
|
|
5080
|
+
maxPoints: POINTS_CLAUDE_MD_EXISTS,
|
|
5081
|
+
earnedPoints: copilotInstructionsExists ? POINTS_CLAUDE_MD_EXISTS : 0,
|
|
5082
|
+
passed: copilotInstructionsExists,
|
|
5083
|
+
detail: copilotInstructionsExists ? "Found at .github/copilot-instructions.md" : "Not found",
|
|
5084
|
+
suggestion: copilotInstructionsExists ? void 0 : "Create .github/copilot-instructions.md with project context for GitHub Copilot",
|
|
5085
|
+
fix: copilotInstructionsExists ? void 0 : {
|
|
5086
|
+
action: "create_file",
|
|
5087
|
+
data: { file: ".github/copilot-instructions.md" },
|
|
5088
|
+
instruction: "Create .github/copilot-instructions.md with project context for GitHub Copilot."
|
|
4483
5089
|
}
|
|
4484
5090
|
});
|
|
4485
|
-
const claudeSkills = countFiles(
|
|
4486
|
-
const codexSkills = countFiles(
|
|
5091
|
+
const claudeSkills = countFiles(join3(dir, ".claude", "skills"), /\.(md|SKILL\.md)$/);
|
|
5092
|
+
const codexSkills = countFiles(join3(dir, ".agents", "skills"), /SKILL\.md$/);
|
|
4487
5093
|
const skillCount = claudeSkills.length + codexSkills.length;
|
|
4488
5094
|
const skillBase = skillCount >= 1 ? POINTS_SKILLS_EXIST : 0;
|
|
4489
5095
|
const skillBonus = Math.min((skillCount - 1) * POINTS_SKILLS_BONUS_PER_EXTRA, POINTS_SKILLS_BONUS_CAP);
|
|
@@ -4504,7 +5110,7 @@ function checkExistence(dir) {
|
|
|
4504
5110
|
instruction: "Create .claude/skills/ with 2-3 project-specific workflow skills."
|
|
4505
5111
|
} : void 0
|
|
4506
5112
|
});
|
|
4507
|
-
const mdcFiles = countFiles(
|
|
5113
|
+
const mdcFiles = countFiles(join3(dir, ".cursor", "rules"), /\.mdc$/);
|
|
4508
5114
|
const mdcCount = mdcFiles.length;
|
|
4509
5115
|
checks.push({
|
|
4510
5116
|
id: "cursor_mdc_rules",
|
|
@@ -4547,297 +5153,24 @@ function checkExistence(dir) {
|
|
|
4547
5153
|
maxPoints: POINTS_CROSS_PLATFORM_PARITY,
|
|
4548
5154
|
earnedPoints: hasParity ? POINTS_CROSS_PLATFORM_PARITY : 0,
|
|
4549
5155
|
passed: hasParity,
|
|
4550
|
-
detail: hasParity ? "Both Claude Code and Cursor configured" : hasClaudeConfigs ? "Only Claude Code \u2014 no Cursor configs" : hasCursorConfigs ? "Only Cursor \u2014 no Claude Code configs" : "Neither platform configured",
|
|
4551
|
-
suggestion: hasParity ? void 0 : "Add configs for both platforms so all teammates get context",
|
|
4552
|
-
fix: hasParity ? void 0 : {
|
|
4553
|
-
action: "add_platform",
|
|
4554
|
-
data: { hasClaude: hasClaudeConfigs, hasCursor: hasCursorConfigs },
|
|
4555
|
-
instruction: hasClaudeConfigs ? "Add Cursor rules (.cursor/rules/) for cross-platform support." : "Add CLAUDE.md for cross-platform support."
|
|
4556
|
-
}
|
|
4557
|
-
});
|
|
4558
|
-
return checks;
|
|
4559
|
-
}
|
|
4560
|
-
|
|
4561
|
-
// src/scoring/checks/quality.ts
|
|
4562
|
-
import { join as join4 } from "path";
|
|
4563
|
-
|
|
4564
|
-
// src/scoring/utils.ts
|
|
4565
|
-
import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync as readdirSync2 } from "fs";
|
|
4566
|
-
import { join as join3, relative } from "path";
|
|
4567
|
-
function readFileOrNull2(filePath) {
|
|
4568
|
-
try {
|
|
4569
|
-
return readFileSync3(filePath, "utf-8");
|
|
4570
|
-
} catch {
|
|
4571
|
-
return null;
|
|
4572
|
-
}
|
|
4573
|
-
}
|
|
4574
|
-
var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
4575
|
-
"node_modules",
|
|
4576
|
-
".git",
|
|
4577
|
-
"dist",
|
|
4578
|
-
"build",
|
|
4579
|
-
"out",
|
|
4580
|
-
".next",
|
|
4581
|
-
".nuxt",
|
|
4582
|
-
"__pycache__",
|
|
4583
|
-
".venv",
|
|
4584
|
-
"venv",
|
|
4585
|
-
"env",
|
|
4586
|
-
".env",
|
|
4587
|
-
"target",
|
|
4588
|
-
"vendor",
|
|
4589
|
-
".cache",
|
|
4590
|
-
".parcel-cache",
|
|
4591
|
-
"coverage",
|
|
4592
|
-
".nyc_output",
|
|
4593
|
-
".turbo",
|
|
4594
|
-
".caliber",
|
|
4595
|
-
".claude",
|
|
4596
|
-
".cursor",
|
|
4597
|
-
".agents",
|
|
4598
|
-
".codex"
|
|
4599
|
-
]);
|
|
4600
|
-
var IGNORED_FILES = /* @__PURE__ */ new Set([
|
|
4601
|
-
".DS_Store",
|
|
4602
|
-
"Thumbs.db",
|
|
4603
|
-
".gitignore",
|
|
4604
|
-
".editorconfig",
|
|
4605
|
-
".prettierrc",
|
|
4606
|
-
".prettierignore",
|
|
4607
|
-
".eslintignore",
|
|
4608
|
-
"package-lock.json",
|
|
4609
|
-
"yarn.lock",
|
|
4610
|
-
"pnpm-lock.yaml",
|
|
4611
|
-
"bun.lockb"
|
|
4612
|
-
]);
|
|
4613
|
-
function collectProjectStructure(dir, maxDepth = 2) {
|
|
4614
|
-
const dirs = [];
|
|
4615
|
-
const files = [];
|
|
4616
|
-
function walk(currentDir, depth) {
|
|
4617
|
-
if (depth > maxDepth) return;
|
|
4618
|
-
try {
|
|
4619
|
-
const entries = readdirSync2(currentDir, { withFileTypes: true });
|
|
4620
|
-
for (const entry of entries) {
|
|
4621
|
-
const name = entry.name;
|
|
4622
|
-
if (name.startsWith(".") && IGNORED_DIRS.has(name)) continue;
|
|
4623
|
-
if (IGNORED_FILES.has(name)) continue;
|
|
4624
|
-
const rel = relative(dir, join3(currentDir, name));
|
|
4625
|
-
if (entry.isDirectory()) {
|
|
4626
|
-
if (IGNORED_DIRS.has(name)) continue;
|
|
4627
|
-
dirs.push(rel);
|
|
4628
|
-
walk(join3(currentDir, name), depth + 1);
|
|
4629
|
-
} else if (entry.isFile()) {
|
|
4630
|
-
files.push(rel);
|
|
4631
|
-
}
|
|
4632
|
-
}
|
|
4633
|
-
} catch {
|
|
4634
|
-
}
|
|
4635
|
-
}
|
|
4636
|
-
walk(dir, 0);
|
|
4637
|
-
return { dirs, files };
|
|
4638
|
-
}
|
|
4639
|
-
function collectPrimaryConfigContent(dir) {
|
|
4640
|
-
const parts = [];
|
|
4641
|
-
for (const file of ["CLAUDE.md", ".cursorrules", "AGENTS.md"]) {
|
|
4642
|
-
const content = readFileOrNull2(join3(dir, file));
|
|
4643
|
-
if (content) parts.push(content);
|
|
4644
|
-
}
|
|
4645
|
-
try {
|
|
4646
|
-
const rulesDir = join3(dir, ".cursor", "rules");
|
|
4647
|
-
const mdcFiles = readdirSync2(rulesDir).filter((f) => f.endsWith(".mdc"));
|
|
4648
|
-
for (const f of mdcFiles) {
|
|
4649
|
-
const content = readFileOrNull2(join3(rulesDir, f));
|
|
4650
|
-
if (content) parts.push(content);
|
|
4651
|
-
}
|
|
4652
|
-
} catch {
|
|
4653
|
-
}
|
|
4654
|
-
return parts.join("\n");
|
|
4655
|
-
}
|
|
4656
|
-
function collectAllConfigContent(dir) {
|
|
4657
|
-
const parts = [collectPrimaryConfigContent(dir)];
|
|
4658
|
-
for (const skillsDir of [join3(dir, ".claude", "skills"), join3(dir, ".agents", "skills")]) {
|
|
4659
|
-
try {
|
|
4660
|
-
const entries = readdirSync2(skillsDir, { withFileTypes: true });
|
|
4661
|
-
for (const entry of entries) {
|
|
4662
|
-
if (entry.isDirectory()) {
|
|
4663
|
-
const skill = readFileOrNull2(join3(skillsDir, entry.name, "SKILL.md"));
|
|
4664
|
-
if (skill) parts.push(skill);
|
|
4665
|
-
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
4666
|
-
const content = readFileOrNull2(join3(skillsDir, entry.name));
|
|
4667
|
-
if (content) parts.push(content);
|
|
4668
|
-
}
|
|
4669
|
-
}
|
|
4670
|
-
} catch {
|
|
4671
|
-
}
|
|
4672
|
-
}
|
|
4673
|
-
return parts.join("\n");
|
|
4674
|
-
}
|
|
4675
|
-
function estimateTokens2(text) {
|
|
4676
|
-
return Math.ceil(text.length / 4);
|
|
4677
|
-
}
|
|
4678
|
-
function analyzeMarkdownStructure(content) {
|
|
4679
|
-
const lines = content.split("\n");
|
|
4680
|
-
let headingCount = 0;
|
|
4681
|
-
let h2Count = 0;
|
|
4682
|
-
let h3Count = 0;
|
|
4683
|
-
let codeBlockCount = 0;
|
|
4684
|
-
let codeBlockLines = 0;
|
|
4685
|
-
let listItemCount = 0;
|
|
4686
|
-
let inlineCodeCount = 0;
|
|
4687
|
-
let inCodeBlock = false;
|
|
4688
|
-
for (const line of lines) {
|
|
4689
|
-
const trimmed = line.trim();
|
|
4690
|
-
if (trimmed.startsWith("```")) {
|
|
4691
|
-
if (!inCodeBlock) codeBlockCount++;
|
|
4692
|
-
inCodeBlock = !inCodeBlock;
|
|
4693
|
-
continue;
|
|
4694
|
-
}
|
|
4695
|
-
if (inCodeBlock) {
|
|
4696
|
-
codeBlockLines++;
|
|
4697
|
-
continue;
|
|
4698
|
-
}
|
|
4699
|
-
if (trimmed.startsWith("## ") && !trimmed.startsWith("### ")) h2Count++;
|
|
4700
|
-
if (trimmed.startsWith("### ")) h3Count++;
|
|
4701
|
-
if (trimmed.startsWith("#")) headingCount++;
|
|
4702
|
-
if (/^[-*+]\s/.test(trimmed) || /^\d+\.\s/.test(trimmed)) listItemCount++;
|
|
4703
|
-
const inlineMatches = trimmed.match(/`[^`]+`/g);
|
|
4704
|
-
if (inlineMatches) inlineCodeCount += inlineMatches.length;
|
|
4705
|
-
}
|
|
4706
|
-
return {
|
|
4707
|
-
headingCount,
|
|
4708
|
-
h2Count,
|
|
4709
|
-
h3Count,
|
|
4710
|
-
codeBlockCount,
|
|
4711
|
-
codeBlockLines,
|
|
4712
|
-
listItemCount,
|
|
4713
|
-
inlineCodeCount,
|
|
4714
|
-
totalLines: lines.length,
|
|
4715
|
-
nonEmptyLines: lines.filter((l) => l.trim().length > 0).length
|
|
4716
|
-
};
|
|
4717
|
-
}
|
|
4718
|
-
function extractReferences(content) {
|
|
4719
|
-
const refs = /* @__PURE__ */ new Set();
|
|
4720
|
-
const backtickPattern = /`([^`]+)`/g;
|
|
4721
|
-
let match;
|
|
4722
|
-
while ((match = backtickPattern.exec(content)) !== null) {
|
|
4723
|
-
const term = match[1].trim();
|
|
4724
|
-
if ((term.includes("/") || /\.\w{1,5}$/.test(term)) && !term.startsWith("-") && term.length < 200) {
|
|
4725
|
-
if (term.startsWith("@") && (term.match(/\//g) || []).length === 1) continue;
|
|
4726
|
-
if (term.includes(" ")) continue;
|
|
4727
|
-
if (/^\d+\.\d+/.test(term)) continue;
|
|
4728
|
-
if (term.includes("/") && !/\.\w{1,5}$/.test(term)) {
|
|
4729
|
-
if (term !== term.toLowerCase() && !/^[a-z]/.test(term)) continue;
|
|
4730
|
-
const segments = term.split("/");
|
|
4731
|
-
if (segments.every((s) => s.length <= 3)) continue;
|
|
4732
|
-
}
|
|
4733
|
-
const cleaned = term.replace(/[,;:!?)]+$/, "");
|
|
4734
|
-
if (cleaned.length > 1) refs.add(cleaned);
|
|
4735
|
-
}
|
|
4736
|
-
}
|
|
4737
|
-
const pathPattern = /(?:^|\s)((?:[a-zA-Z0-9_@.-]+\/)+[a-zA-Z0-9_.*-]+\.[a-zA-Z]{1,5})/gm;
|
|
4738
|
-
while ((match = pathPattern.exec(content)) !== null) {
|
|
4739
|
-
const term = match[1].trim();
|
|
4740
|
-
if (term.length > 2 && term.length < 200) {
|
|
4741
|
-
if (term.startsWith("@") && (term.match(/\//g) || []).length === 1) continue;
|
|
4742
|
-
const cleaned = term.replace(/[,;:!?)]+$/, "");
|
|
4743
|
-
if (cleaned.length > 1) refs.add(cleaned);
|
|
4744
|
-
}
|
|
4745
|
-
}
|
|
4746
|
-
return Array.from(refs);
|
|
4747
|
-
}
|
|
4748
|
-
function validateFileReferences(content, dir, checkExists = existsSync3) {
|
|
4749
|
-
const refs = extractReferences(content);
|
|
4750
|
-
const valid = [];
|
|
4751
|
-
const invalid = [];
|
|
4752
|
-
for (const ref of refs) {
|
|
4753
|
-
if (/^https?:\/\//.test(ref)) continue;
|
|
4754
|
-
if (/^\d+\.\d+/.test(ref)) continue;
|
|
4755
|
-
if (ref.startsWith("#") || ref.startsWith("@")) continue;
|
|
4756
|
-
if (ref.includes("*") || ref.includes("..")) continue;
|
|
4757
|
-
if (!ref.includes("/") && !ref.includes(".")) continue;
|
|
4758
|
-
const fullPath = join3(dir, ref);
|
|
4759
|
-
if (checkExists(fullPath)) {
|
|
4760
|
-
valid.push(ref);
|
|
4761
|
-
} else {
|
|
4762
|
-
const withoutTrailing = ref.replace(/\/+$/, "");
|
|
4763
|
-
if (withoutTrailing !== ref && checkExists(join3(dir, withoutTrailing))) {
|
|
4764
|
-
valid.push(ref);
|
|
4765
|
-
} else {
|
|
4766
|
-
invalid.push(ref);
|
|
4767
|
-
}
|
|
4768
|
-
}
|
|
4769
|
-
}
|
|
4770
|
-
return { valid, invalid, total: valid.length + invalid.length };
|
|
4771
|
-
}
|
|
4772
|
-
function countConcreteness(content) {
|
|
4773
|
-
let concrete = 0;
|
|
4774
|
-
let abstract = 0;
|
|
4775
|
-
let inCodeBlock = false;
|
|
4776
|
-
for (const line of content.split("\n")) {
|
|
4777
|
-
if (line.trim().startsWith("```")) {
|
|
4778
|
-
inCodeBlock = !inCodeBlock;
|
|
4779
|
-
continue;
|
|
4780
|
-
}
|
|
4781
|
-
const classification = classifyLine(line, inCodeBlock);
|
|
4782
|
-
if (classification === "concrete") concrete++;
|
|
4783
|
-
else if (classification === "abstract") abstract++;
|
|
4784
|
-
}
|
|
4785
|
-
return { concrete, abstract };
|
|
4786
|
-
}
|
|
4787
|
-
function countTreeLines(content) {
|
|
4788
|
-
const treeLinePattern = /[├└│─┬]/;
|
|
4789
|
-
let count = 0;
|
|
4790
|
-
let inCodeBlock = false;
|
|
4791
|
-
for (const line of content.split("\n")) {
|
|
4792
|
-
if (line.trim().startsWith("```")) {
|
|
4793
|
-
inCodeBlock = !inCodeBlock;
|
|
4794
|
-
continue;
|
|
4795
|
-
}
|
|
4796
|
-
if (inCodeBlock && treeLinePattern.test(line)) count++;
|
|
4797
|
-
}
|
|
4798
|
-
return count;
|
|
4799
|
-
}
|
|
4800
|
-
function calculateDuplicatePercent(content1, content2) {
|
|
4801
|
-
const lines1 = new Set(content1.split("\n").map((l) => l.trim()).filter((l) => l.length > 10));
|
|
4802
|
-
const lines2 = content2.split("\n").map((l) => l.trim()).filter((l) => l.length > 10);
|
|
4803
|
-
const overlapping = lines2.filter((l) => lines1.has(l)).length;
|
|
4804
|
-
return lines2.length > 0 ? Math.round(overlapping / lines2.length * 100) : 0;
|
|
4805
|
-
}
|
|
4806
|
-
function calculateDensityPoints(density, maxPoints) {
|
|
4807
|
-
if (density >= 40) return maxPoints;
|
|
4808
|
-
if (density >= 25) return Math.round(maxPoints * 0.75);
|
|
4809
|
-
if (density >= 15) return Math.round(maxPoints * 0.5);
|
|
4810
|
-
if (density >= 5) return Math.round(maxPoints * 0.25);
|
|
4811
|
-
return 0;
|
|
4812
|
-
}
|
|
4813
|
-
function isEntryMentioned(entry, contentLower) {
|
|
4814
|
-
const entryLower = entry.toLowerCase();
|
|
4815
|
-
const variants = [entryLower, entryLower.replace(/\\/g, "/")];
|
|
4816
|
-
const lastSegment = entry.split("/").pop()?.toLowerCase();
|
|
4817
|
-
if (lastSegment && lastSegment.length > 3) variants.push(lastSegment);
|
|
4818
|
-
return variants.some((v) => {
|
|
4819
|
-
const escaped = v.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4820
|
-
return new RegExp(`(?:^|[\\s\`/"'\\.,(])${escaped}(?:[\\s\`/"'.,;:!?)\\\\]|$)`, "i").test(contentLower);
|
|
5156
|
+
detail: hasParity ? "Both Claude Code and Cursor configured" : hasClaudeConfigs ? "Only Claude Code \u2014 no Cursor configs" : hasCursorConfigs ? "Only Cursor \u2014 no Claude Code configs" : "Neither platform configured",
|
|
5157
|
+
suggestion: hasParity ? void 0 : "Add configs for both platforms so all teammates get context",
|
|
5158
|
+
fix: hasParity ? void 0 : {
|
|
5159
|
+
action: "add_platform",
|
|
5160
|
+
data: { hasClaude: hasClaudeConfigs, hasCursor: hasCursorConfigs },
|
|
5161
|
+
instruction: hasClaudeConfigs ? "Add Cursor rules (.cursor/rules/) for cross-platform support." : "Add CLAUDE.md for cross-platform support."
|
|
5162
|
+
}
|
|
4821
5163
|
});
|
|
4822
|
-
|
|
4823
|
-
function classifyLine(line, inCodeBlock) {
|
|
4824
|
-
if (inCodeBlock) return "concrete";
|
|
4825
|
-
const trimmed = line.trim();
|
|
4826
|
-
if (trimmed.length === 0) return "neutral";
|
|
4827
|
-
if (trimmed.startsWith("#")) return "neutral";
|
|
4828
|
-
if (/`[^`]+`/.test(trimmed)) return "concrete";
|
|
4829
|
-
if (/[a-zA-Z0-9_-]+\/[a-zA-Z0-9_.-]+\.[a-zA-Z]{1,5}/.test(trimmed)) return "concrete";
|
|
4830
|
-
if (/[a-zA-Z0-9_]{4,}\/[a-zA-Z0-9_.-]/.test(trimmed)) return "concrete";
|
|
4831
|
-
if (/\b[a-zA-Z0-9_-]+\.[a-zA-Z]{1,5}\b/.test(trimmed) && !/\b(e\.g|i\.e|vs|etc)\b/i.test(trimmed)) return "concrete";
|
|
4832
|
-
return "abstract";
|
|
5164
|
+
return checks;
|
|
4833
5165
|
}
|
|
4834
5166
|
|
|
4835
5167
|
// src/scoring/checks/quality.ts
|
|
5168
|
+
import { join as join4 } from "path";
|
|
4836
5169
|
function checkQuality(dir) {
|
|
4837
5170
|
const checks = [];
|
|
4838
|
-
const claudeMd =
|
|
4839
|
-
const cursorrules =
|
|
4840
|
-
const agentsMd =
|
|
5171
|
+
const claudeMd = readFileOrNull(join4(dir, "CLAUDE.md"));
|
|
5172
|
+
const cursorrules = readFileOrNull(join4(dir, ".cursorrules"));
|
|
5173
|
+
const agentsMd = readFileOrNull(join4(dir, "AGENTS.md"));
|
|
4841
5174
|
const allContent = [claudeMd, cursorrules, agentsMd].filter(Boolean);
|
|
4842
5175
|
const combinedContent = allContent.join("\n");
|
|
4843
5176
|
const primaryInstructions = claudeMd ?? agentsMd ?? cursorrules;
|
|
@@ -5253,7 +5586,7 @@ function checkFreshness(dir) {
|
|
|
5253
5586
|
];
|
|
5254
5587
|
const secretFindings = [];
|
|
5255
5588
|
for (const rel of filesToScan) {
|
|
5256
|
-
const content =
|
|
5589
|
+
const content = readFileOrNull(join6(dir, rel));
|
|
5257
5590
|
if (!content) continue;
|
|
5258
5591
|
const lines = content.split("\n");
|
|
5259
5592
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -5287,7 +5620,7 @@ function checkFreshness(dir) {
|
|
|
5287
5620
|
const settingsPath = join6(dir, ".claude", "settings.json");
|
|
5288
5621
|
let hasPermissions = false;
|
|
5289
5622
|
let permissionDetail = "";
|
|
5290
|
-
const settingsContent =
|
|
5623
|
+
const settingsContent = readFileOrNull(settingsPath);
|
|
5291
5624
|
if (settingsContent) {
|
|
5292
5625
|
try {
|
|
5293
5626
|
const settings = JSON.parse(settingsContent);
|
|
@@ -5327,7 +5660,7 @@ function hasPreCommitHook(dir) {
|
|
|
5327
5660
|
try {
|
|
5328
5661
|
const gitDir = execSync11("git rev-parse --git-dir", { cwd: dir, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
5329
5662
|
const hookPath = join7(gitDir, "hooks", "pre-commit");
|
|
5330
|
-
const content =
|
|
5663
|
+
const content = readFileOrNull(hookPath);
|
|
5331
5664
|
return content ? content.includes("caliber") : false;
|
|
5332
5665
|
} catch {
|
|
5333
5666
|
return false;
|
|
@@ -5338,7 +5671,7 @@ function checkBonus(dir) {
|
|
|
5338
5671
|
let hasClaudeHooks = false;
|
|
5339
5672
|
let hasPrecommit = false;
|
|
5340
5673
|
const hookSources = [];
|
|
5341
|
-
const settingsContent =
|
|
5674
|
+
const settingsContent = readFileOrNull(join7(dir, ".claude", "settings.json"));
|
|
5342
5675
|
if (settingsContent) {
|
|
5343
5676
|
try {
|
|
5344
5677
|
const settings = JSON.parse(settingsContent);
|
|
@@ -5393,7 +5726,7 @@ function checkBonus(dir) {
|
|
|
5393
5726
|
const entries = readdirSync3(skillsDir, { withFileTypes: true });
|
|
5394
5727
|
for (const entry of entries) {
|
|
5395
5728
|
if (entry.isDirectory()) {
|
|
5396
|
-
const skillMd =
|
|
5729
|
+
const skillMd = readFileOrNull(join7(skillsDir, entry.name, "SKILL.md"));
|
|
5397
5730
|
if (skillMd) {
|
|
5398
5731
|
totalSkillFiles++;
|
|
5399
5732
|
if (skillMd.trimStart().startsWith("---")) {
|
|
@@ -5422,7 +5755,7 @@ function checkBonus(dir) {
|
|
|
5422
5755
|
instruction: "Migrate flat skill files to .claude/skills/{name}/SKILL.md with YAML frontmatter."
|
|
5423
5756
|
} : void 0
|
|
5424
5757
|
});
|
|
5425
|
-
const learningsContent =
|
|
5758
|
+
const learningsContent = readFileOrNull(join7(dir, "CALIBER_LEARNINGS.md"));
|
|
5426
5759
|
const hasLearned = learningsContent ? learningsContent.split("\n").filter((l) => l.startsWith("- ")).length > 0 : false;
|
|
5427
5760
|
checks.push({
|
|
5428
5761
|
id: "learned_content",
|
|
@@ -5437,24 +5770,62 @@ function checkBonus(dir) {
|
|
|
5437
5770
|
return checks;
|
|
5438
5771
|
}
|
|
5439
5772
|
|
|
5773
|
+
// src/scoring/checks/sources.ts
|
|
5774
|
+
import { join as join8 } from "path";
|
|
5775
|
+
function checkSources(dir) {
|
|
5776
|
+
const checks = [];
|
|
5777
|
+
const configSources = loadSourcesConfig(dir);
|
|
5778
|
+
const hasSources = configSources.length > 0;
|
|
5779
|
+
checks.push({
|
|
5780
|
+
id: "sources_configured",
|
|
5781
|
+
name: "External sources configured",
|
|
5782
|
+
category: "bonus",
|
|
5783
|
+
maxPoints: hasSources ? POINTS_SOURCES_CONFIGURED : 0,
|
|
5784
|
+
earnedPoints: hasSources ? POINTS_SOURCES_CONFIGURED : 0,
|
|
5785
|
+
passed: hasSources,
|
|
5786
|
+
detail: hasSources ? `${configSources.length} source${configSources.length === 1 ? "" : "s"} configured` : "No external sources configured",
|
|
5787
|
+
suggestion: hasSources ? void 0 : "Run `caliber sources add <path>` to add related repos or docs"
|
|
5788
|
+
});
|
|
5789
|
+
if (hasSources) {
|
|
5790
|
+
const claudeMd = readFileOrNull(join8(dir, "CLAUDE.md"));
|
|
5791
|
+
const content = claudeMd?.toLowerCase() || "";
|
|
5792
|
+
const referenced = configSources.some((source) => {
|
|
5793
|
+
if (!source.path) return false;
|
|
5794
|
+
const name = source.path.split("/").pop()?.toLowerCase() || "";
|
|
5795
|
+
return name.length > 2 && content.includes(name);
|
|
5796
|
+
});
|
|
5797
|
+
checks.push({
|
|
5798
|
+
id: "sources_referenced",
|
|
5799
|
+
name: "Sources referenced in config",
|
|
5800
|
+
category: "grounding",
|
|
5801
|
+
maxPoints: POINTS_SOURCES_REFERENCED,
|
|
5802
|
+
earnedPoints: referenced ? POINTS_SOURCES_REFERENCED : 0,
|
|
5803
|
+
passed: referenced,
|
|
5804
|
+
detail: referenced ? "At least one source is referenced in CLAUDE.md" : "No configured sources are mentioned in CLAUDE.md",
|
|
5805
|
+
suggestion: referenced ? void 0 : "Regenerate with `caliber init` to include source context in your config"
|
|
5806
|
+
});
|
|
5807
|
+
}
|
|
5808
|
+
return checks;
|
|
5809
|
+
}
|
|
5810
|
+
|
|
5440
5811
|
// src/scoring/dismissed.ts
|
|
5441
5812
|
init_constants();
|
|
5442
|
-
import
|
|
5443
|
-
import
|
|
5444
|
-
var DISMISSED_FILE =
|
|
5813
|
+
import fs22 from "fs";
|
|
5814
|
+
import path18 from "path";
|
|
5815
|
+
var DISMISSED_FILE = path18.join(CALIBER_DIR, "dismissed-checks.json");
|
|
5445
5816
|
function readDismissedChecks() {
|
|
5446
5817
|
try {
|
|
5447
|
-
if (!
|
|
5448
|
-
return JSON.parse(
|
|
5818
|
+
if (!fs22.existsSync(DISMISSED_FILE)) return [];
|
|
5819
|
+
return JSON.parse(fs22.readFileSync(DISMISSED_FILE, "utf-8"));
|
|
5449
5820
|
} catch {
|
|
5450
5821
|
return [];
|
|
5451
5822
|
}
|
|
5452
5823
|
}
|
|
5453
5824
|
function writeDismissedChecks(checks) {
|
|
5454
|
-
if (!
|
|
5455
|
-
|
|
5825
|
+
if (!fs22.existsSync(CALIBER_DIR)) {
|
|
5826
|
+
fs22.mkdirSync(CALIBER_DIR, { recursive: true });
|
|
5456
5827
|
}
|
|
5457
|
-
|
|
5828
|
+
fs22.writeFileSync(DISMISSED_FILE, JSON.stringify(checks, null, 2) + "\n");
|
|
5458
5829
|
}
|
|
5459
5830
|
function getDismissedIds() {
|
|
5460
5831
|
return new Set(readDismissedChecks().map((c) => c.id));
|
|
@@ -5473,6 +5844,7 @@ function filterChecksForTarget(checks, target) {
|
|
|
5473
5844
|
if (CLAUDE_ONLY_CHECKS.has(c.id)) return target.includes("claude");
|
|
5474
5845
|
if (CURSOR_ONLY_CHECKS.has(c.id)) return target.includes("cursor");
|
|
5475
5846
|
if (CODEX_ONLY_CHECKS.has(c.id)) return target.includes("codex");
|
|
5847
|
+
if (COPILOT_ONLY_CHECKS.has(c.id)) return target.includes("github-copilot");
|
|
5476
5848
|
if (BOTH_ONLY_CHECKS.has(c.id)) return target.includes("claude") && target.includes("cursor");
|
|
5477
5849
|
if (NON_CODEX_CHECKS.has(c.id)) return !target.includes("codex");
|
|
5478
5850
|
return true;
|
|
@@ -5480,9 +5852,10 @@ function filterChecksForTarget(checks, target) {
|
|
|
5480
5852
|
}
|
|
5481
5853
|
function detectTargetAgent(dir) {
|
|
5482
5854
|
const agents = [];
|
|
5483
|
-
if (existsSync7(
|
|
5484
|
-
if (existsSync7(
|
|
5485
|
-
if (existsSync7(
|
|
5855
|
+
if (existsSync7(join9(dir, "CLAUDE.md")) || existsSync7(join9(dir, ".claude", "skills"))) agents.push("claude");
|
|
5856
|
+
if (existsSync7(join9(dir, ".cursorrules")) || existsSync7(join9(dir, ".cursor", "rules"))) agents.push("cursor");
|
|
5857
|
+
if (existsSync7(join9(dir, ".codex")) || existsSync7(join9(dir, ".agents", "skills"))) agents.push("codex");
|
|
5858
|
+
if (existsSync7(join9(dir, ".github", "copilot-instructions.md"))) agents.push("github-copilot");
|
|
5486
5859
|
return agents.length > 0 ? agents : ["claude"];
|
|
5487
5860
|
}
|
|
5488
5861
|
function computeLocalScore(dir, targetAgent) {
|
|
@@ -5493,7 +5866,8 @@ function computeLocalScore(dir, targetAgent) {
|
|
|
5493
5866
|
...checkGrounding(dir),
|
|
5494
5867
|
...checkAccuracy(dir),
|
|
5495
5868
|
...checkFreshness(dir),
|
|
5496
|
-
...checkBonus(dir)
|
|
5869
|
+
...checkBonus(dir),
|
|
5870
|
+
...checkSources(dir)
|
|
5497
5871
|
];
|
|
5498
5872
|
const dismissed = getDismissedIds();
|
|
5499
5873
|
const checks = filterChecksForTarget(allChecks, target).filter((c) => !dismissed.has(c.id));
|
|
@@ -5696,16 +6070,16 @@ import chalk6 from "chalk";
|
|
|
5696
6070
|
import ora from "ora";
|
|
5697
6071
|
import select3 from "@inquirer/select";
|
|
5698
6072
|
import { mkdirSync, readFileSync as readFileSync4, readdirSync as readdirSync4, existsSync as existsSync8, writeFileSync } from "fs";
|
|
5699
|
-
import { join as
|
|
6073
|
+
import { join as join10, dirname as dirname2 } from "path";
|
|
5700
6074
|
|
|
5701
6075
|
// src/scanner/index.ts
|
|
5702
|
-
import
|
|
5703
|
-
import
|
|
6076
|
+
import fs23 from "fs";
|
|
6077
|
+
import path19 from "path";
|
|
5704
6078
|
import crypto3 from "crypto";
|
|
5705
6079
|
function scanLocalState(dir) {
|
|
5706
6080
|
const items = [];
|
|
5707
|
-
const claudeMdPath =
|
|
5708
|
-
if (
|
|
6081
|
+
const claudeMdPath = path19.join(dir, "CLAUDE.md");
|
|
6082
|
+
if (fs23.existsSync(claudeMdPath)) {
|
|
5709
6083
|
items.push({
|
|
5710
6084
|
type: "rule",
|
|
5711
6085
|
platform: "claude",
|
|
@@ -5714,10 +6088,10 @@ function scanLocalState(dir) {
|
|
|
5714
6088
|
path: claudeMdPath
|
|
5715
6089
|
});
|
|
5716
6090
|
}
|
|
5717
|
-
const skillsDir =
|
|
5718
|
-
if (
|
|
5719
|
-
for (const file of
|
|
5720
|
-
const filePath =
|
|
6091
|
+
const skillsDir = path19.join(dir, ".claude", "skills");
|
|
6092
|
+
if (fs23.existsSync(skillsDir)) {
|
|
6093
|
+
for (const file of fs23.readdirSync(skillsDir).filter((f) => f.endsWith(".md"))) {
|
|
6094
|
+
const filePath = path19.join(skillsDir, file);
|
|
5721
6095
|
items.push({
|
|
5722
6096
|
type: "skill",
|
|
5723
6097
|
platform: "claude",
|
|
@@ -5727,10 +6101,10 @@ function scanLocalState(dir) {
|
|
|
5727
6101
|
});
|
|
5728
6102
|
}
|
|
5729
6103
|
}
|
|
5730
|
-
const mcpJsonPath =
|
|
5731
|
-
if (
|
|
6104
|
+
const mcpJsonPath = path19.join(dir, ".mcp.json");
|
|
6105
|
+
if (fs23.existsSync(mcpJsonPath)) {
|
|
5732
6106
|
try {
|
|
5733
|
-
const mcpJson = JSON.parse(
|
|
6107
|
+
const mcpJson = JSON.parse(fs23.readFileSync(mcpJsonPath, "utf-8"));
|
|
5734
6108
|
if (mcpJson.mcpServers) {
|
|
5735
6109
|
for (const name of Object.keys(mcpJson.mcpServers)) {
|
|
5736
6110
|
items.push({
|
|
@@ -5745,8 +6119,8 @@ function scanLocalState(dir) {
|
|
|
5745
6119
|
} catch {
|
|
5746
6120
|
}
|
|
5747
6121
|
}
|
|
5748
|
-
const agentsMdPath =
|
|
5749
|
-
if (
|
|
6122
|
+
const agentsMdPath = path19.join(dir, "AGENTS.md");
|
|
6123
|
+
if (fs23.existsSync(agentsMdPath)) {
|
|
5750
6124
|
items.push({
|
|
5751
6125
|
type: "rule",
|
|
5752
6126
|
platform: "codex",
|
|
@@ -5755,12 +6129,12 @@ function scanLocalState(dir) {
|
|
|
5755
6129
|
path: agentsMdPath
|
|
5756
6130
|
});
|
|
5757
6131
|
}
|
|
5758
|
-
const codexSkillsDir =
|
|
5759
|
-
if (
|
|
6132
|
+
const codexSkillsDir = path19.join(dir, ".agents", "skills");
|
|
6133
|
+
if (fs23.existsSync(codexSkillsDir)) {
|
|
5760
6134
|
try {
|
|
5761
|
-
for (const name of
|
|
5762
|
-
const skillFile =
|
|
5763
|
-
if (
|
|
6135
|
+
for (const name of fs23.readdirSync(codexSkillsDir)) {
|
|
6136
|
+
const skillFile = path19.join(codexSkillsDir, name, "SKILL.md");
|
|
6137
|
+
if (fs23.existsSync(skillFile)) {
|
|
5764
6138
|
items.push({
|
|
5765
6139
|
type: "skill",
|
|
5766
6140
|
platform: "codex",
|
|
@@ -5773,8 +6147,8 @@ function scanLocalState(dir) {
|
|
|
5773
6147
|
} catch {
|
|
5774
6148
|
}
|
|
5775
6149
|
}
|
|
5776
|
-
const cursorrulesPath =
|
|
5777
|
-
if (
|
|
6150
|
+
const cursorrulesPath = path19.join(dir, ".cursorrules");
|
|
6151
|
+
if (fs23.existsSync(cursorrulesPath)) {
|
|
5778
6152
|
items.push({
|
|
5779
6153
|
type: "rule",
|
|
5780
6154
|
platform: "cursor",
|
|
@@ -5783,10 +6157,10 @@ function scanLocalState(dir) {
|
|
|
5783
6157
|
path: cursorrulesPath
|
|
5784
6158
|
});
|
|
5785
6159
|
}
|
|
5786
|
-
const cursorRulesDir =
|
|
5787
|
-
if (
|
|
5788
|
-
for (const file of
|
|
5789
|
-
const filePath =
|
|
6160
|
+
const cursorRulesDir = path19.join(dir, ".cursor", "rules");
|
|
6161
|
+
if (fs23.existsSync(cursorRulesDir)) {
|
|
6162
|
+
for (const file of fs23.readdirSync(cursorRulesDir).filter((f) => f.endsWith(".mdc"))) {
|
|
6163
|
+
const filePath = path19.join(cursorRulesDir, file);
|
|
5790
6164
|
items.push({
|
|
5791
6165
|
type: "rule",
|
|
5792
6166
|
platform: "cursor",
|
|
@@ -5796,12 +6170,12 @@ function scanLocalState(dir) {
|
|
|
5796
6170
|
});
|
|
5797
6171
|
}
|
|
5798
6172
|
}
|
|
5799
|
-
const cursorSkillsDir =
|
|
5800
|
-
if (
|
|
6173
|
+
const cursorSkillsDir = path19.join(dir, ".cursor", "skills");
|
|
6174
|
+
if (fs23.existsSync(cursorSkillsDir)) {
|
|
5801
6175
|
try {
|
|
5802
|
-
for (const name of
|
|
5803
|
-
const skillFile =
|
|
5804
|
-
if (
|
|
6176
|
+
for (const name of fs23.readdirSync(cursorSkillsDir)) {
|
|
6177
|
+
const skillFile = path19.join(cursorSkillsDir, name, "SKILL.md");
|
|
6178
|
+
if (fs23.existsSync(skillFile)) {
|
|
5805
6179
|
items.push({
|
|
5806
6180
|
type: "skill",
|
|
5807
6181
|
platform: "cursor",
|
|
@@ -5814,10 +6188,10 @@ function scanLocalState(dir) {
|
|
|
5814
6188
|
} catch {
|
|
5815
6189
|
}
|
|
5816
6190
|
}
|
|
5817
|
-
const cursorMcpPath =
|
|
5818
|
-
if (
|
|
6191
|
+
const cursorMcpPath = path19.join(dir, ".cursor", "mcp.json");
|
|
6192
|
+
if (fs23.existsSync(cursorMcpPath)) {
|
|
5819
6193
|
try {
|
|
5820
|
-
const mcpJson = JSON.parse(
|
|
6194
|
+
const mcpJson = JSON.parse(fs23.readFileSync(cursorMcpPath, "utf-8"));
|
|
5821
6195
|
if (mcpJson.mcpServers) {
|
|
5822
6196
|
for (const name of Object.keys(mcpJson.mcpServers)) {
|
|
5823
6197
|
items.push({
|
|
@@ -5835,7 +6209,7 @@ function scanLocalState(dir) {
|
|
|
5835
6209
|
return items;
|
|
5836
6210
|
}
|
|
5837
6211
|
function hashFile(filePath) {
|
|
5838
|
-
const text =
|
|
6212
|
+
const text = fs23.readFileSync(filePath, "utf-8");
|
|
5839
6213
|
return crypto3.createHash("sha256").update(JSON.stringify({ text })).digest("hex");
|
|
5840
6214
|
}
|
|
5841
6215
|
function hashJson(obj) {
|
|
@@ -5850,27 +6224,27 @@ import { PostHog } from "posthog-node";
|
|
|
5850
6224
|
import chalk5 from "chalk";
|
|
5851
6225
|
|
|
5852
6226
|
// src/telemetry/config.ts
|
|
5853
|
-
import
|
|
5854
|
-
import
|
|
6227
|
+
import fs24 from "fs";
|
|
6228
|
+
import path20 from "path";
|
|
5855
6229
|
import os4 from "os";
|
|
5856
6230
|
import crypto4 from "crypto";
|
|
5857
6231
|
import { execSync as execSync12 } from "child_process";
|
|
5858
|
-
var CONFIG_DIR2 =
|
|
5859
|
-
var CONFIG_FILE2 =
|
|
6232
|
+
var CONFIG_DIR2 = path20.join(os4.homedir(), ".caliber");
|
|
6233
|
+
var CONFIG_FILE2 = path20.join(CONFIG_DIR2, "config.json");
|
|
5860
6234
|
var runtimeDisabled = false;
|
|
5861
6235
|
function readConfig() {
|
|
5862
6236
|
try {
|
|
5863
|
-
if (!
|
|
5864
|
-
return JSON.parse(
|
|
6237
|
+
if (!fs24.existsSync(CONFIG_FILE2)) return {};
|
|
6238
|
+
return JSON.parse(fs24.readFileSync(CONFIG_FILE2, "utf-8"));
|
|
5865
6239
|
} catch {
|
|
5866
6240
|
return {};
|
|
5867
6241
|
}
|
|
5868
6242
|
}
|
|
5869
6243
|
function writeConfig(config) {
|
|
5870
|
-
if (!
|
|
5871
|
-
|
|
6244
|
+
if (!fs24.existsSync(CONFIG_DIR2)) {
|
|
6245
|
+
fs24.mkdirSync(CONFIG_DIR2, { recursive: true });
|
|
5872
6246
|
}
|
|
5873
|
-
|
|
6247
|
+
fs24.writeFileSync(CONFIG_FILE2, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
|
|
5874
6248
|
}
|
|
5875
6249
|
function getMachineId() {
|
|
5876
6250
|
const config = readConfig();
|
|
@@ -6054,17 +6428,17 @@ function detectLocalPlatforms() {
|
|
|
6054
6428
|
}
|
|
6055
6429
|
function getSkillPath(platform, slug) {
|
|
6056
6430
|
if (platform === "cursor") {
|
|
6057
|
-
return
|
|
6431
|
+
return join10(".cursor", "skills", slug, "SKILL.md");
|
|
6058
6432
|
}
|
|
6059
6433
|
if (platform === "codex") {
|
|
6060
|
-
return
|
|
6434
|
+
return join10(".agents", "skills", slug, "SKILL.md");
|
|
6061
6435
|
}
|
|
6062
|
-
return
|
|
6436
|
+
return join10(".claude", "skills", slug, "SKILL.md");
|
|
6063
6437
|
}
|
|
6064
6438
|
function getSkillDir(platform) {
|
|
6065
|
-
if (platform === "cursor") return
|
|
6066
|
-
if (platform === "codex") return
|
|
6067
|
-
return
|
|
6439
|
+
if (platform === "cursor") return join10(process.cwd(), ".cursor", "skills");
|
|
6440
|
+
if (platform === "codex") return join10(process.cwd(), ".agents", "skills");
|
|
6441
|
+
return join10(process.cwd(), ".claude", "skills");
|
|
6068
6442
|
}
|
|
6069
6443
|
function getInstalledSkills(platforms) {
|
|
6070
6444
|
const installed = /* @__PURE__ */ new Set();
|
|
@@ -6235,7 +6609,7 @@ Already installed skills: ${Array.from(installed).join(", ")}`);
|
|
|
6235
6609
|
return parts.join("\n");
|
|
6236
6610
|
}
|
|
6237
6611
|
function extractTopDeps() {
|
|
6238
|
-
const pkgPath =
|
|
6612
|
+
const pkgPath = join10(process.cwd(), "package.json");
|
|
6239
6613
|
if (!existsSync8(pkgPath)) return [];
|
|
6240
6614
|
try {
|
|
6241
6615
|
const pkg3 = JSON.parse(readFileSync4(pkgPath, "utf-8"));
|
|
@@ -6574,7 +6948,7 @@ async function installSkills(recs, platforms, contentMap) {
|
|
|
6574
6948
|
if (!content) continue;
|
|
6575
6949
|
for (const platform of platforms) {
|
|
6576
6950
|
const skillPath = getSkillPath(platform, rec.slug);
|
|
6577
|
-
const fullPath =
|
|
6951
|
+
const fullPath = join10(process.cwd(), skillPath);
|
|
6578
6952
|
mkdirSync(dirname2(fullPath), { recursive: true });
|
|
6579
6953
|
writeFileSync(fullPath, content, "utf-8");
|
|
6580
6954
|
installed.push(`[${platform}] ${skillPath}`);
|
|
@@ -6829,11 +7203,11 @@ function countIssuePoints(issues) {
|
|
|
6829
7203
|
}
|
|
6830
7204
|
async function scoreAndRefine(setup, dir, sessionHistory, callbacks) {
|
|
6831
7205
|
const existsCache = /* @__PURE__ */ new Map();
|
|
6832
|
-
const cachedExists = (
|
|
6833
|
-
const cached = existsCache.get(
|
|
7206
|
+
const cachedExists = (path37) => {
|
|
7207
|
+
const cached = existsCache.get(path37);
|
|
6834
7208
|
if (cached !== void 0) return cached;
|
|
6835
|
-
const result = existsSync9(
|
|
6836
|
-
existsCache.set(
|
|
7209
|
+
const result = existsSync9(path37);
|
|
7210
|
+
existsCache.set(path37, result);
|
|
6837
7211
|
return result;
|
|
6838
7212
|
};
|
|
6839
7213
|
const projectStructure = collectProjectStructure(dir);
|
|
@@ -6972,8 +7346,8 @@ async function runScoreRefineWithSpinner(setup, dir, sessionHistory) {
|
|
|
6972
7346
|
}
|
|
6973
7347
|
|
|
6974
7348
|
// src/lib/debug-report.ts
|
|
6975
|
-
import
|
|
6976
|
-
import
|
|
7349
|
+
import fs25 from "fs";
|
|
7350
|
+
import path21 from "path";
|
|
6977
7351
|
var DebugReport = class {
|
|
6978
7352
|
sections = [];
|
|
6979
7353
|
startTime;
|
|
@@ -7042,11 +7416,11 @@ var DebugReport = class {
|
|
|
7042
7416
|
lines.push(`| **Total** | **${formatMs(totalMs)}** |`);
|
|
7043
7417
|
lines.push("");
|
|
7044
7418
|
}
|
|
7045
|
-
const dir =
|
|
7046
|
-
if (!
|
|
7047
|
-
|
|
7419
|
+
const dir = path21.dirname(outputPath);
|
|
7420
|
+
if (!fs25.existsSync(dir)) {
|
|
7421
|
+
fs25.mkdirSync(dir, { recursive: true });
|
|
7048
7422
|
}
|
|
7049
|
-
|
|
7423
|
+
fs25.writeFileSync(outputPath, lines.join("\n"));
|
|
7050
7424
|
}
|
|
7051
7425
|
};
|
|
7052
7426
|
function formatMs(ms) {
|
|
@@ -7447,7 +7821,7 @@ import chalk11 from "chalk";
|
|
|
7447
7821
|
import ora3 from "ora";
|
|
7448
7822
|
import select5 from "@inquirer/select";
|
|
7449
7823
|
import checkbox from "@inquirer/checkbox";
|
|
7450
|
-
import
|
|
7824
|
+
import fs28 from "fs";
|
|
7451
7825
|
|
|
7452
7826
|
// src/ai/refine.ts
|
|
7453
7827
|
async function refineSetup(currentSetup, message, conversationHistory, callbacks) {
|
|
@@ -7588,16 +7962,18 @@ init_config();
|
|
|
7588
7962
|
init_review();
|
|
7589
7963
|
function detectAgents(dir) {
|
|
7590
7964
|
const agents = [];
|
|
7591
|
-
if (
|
|
7592
|
-
if (
|
|
7593
|
-
if (
|
|
7965
|
+
if (fs28.existsSync(`${dir}/.claude`)) agents.push("claude");
|
|
7966
|
+
if (fs28.existsSync(`${dir}/.cursor`)) agents.push("cursor");
|
|
7967
|
+
if (fs28.existsSync(`${dir}/.agents`) || fs28.existsSync(`${dir}/AGENTS.md`)) agents.push("codex");
|
|
7968
|
+
if (fs28.existsSync(`${dir}/.github/copilot-instructions.md`)) agents.push("github-copilot");
|
|
7594
7969
|
return agents;
|
|
7595
7970
|
}
|
|
7596
7971
|
async function promptAgent(detected) {
|
|
7597
7972
|
const choices = [
|
|
7598
7973
|
{ name: "Claude Code", value: "claude", checked: detected?.includes("claude") ?? false },
|
|
7599
7974
|
{ name: "Cursor", value: "cursor", checked: detected?.includes("cursor") ?? false },
|
|
7600
|
-
{ name: "Codex (OpenAI)", value: "codex", checked: detected?.includes("codex") ?? false }
|
|
7975
|
+
{ name: "Codex (OpenAI)", value: "codex", checked: detected?.includes("codex") ?? false },
|
|
7976
|
+
{ name: "GitHub Copilot", value: "github-copilot", checked: detected?.includes("github-copilot") ?? false }
|
|
7601
7977
|
];
|
|
7602
7978
|
const hasDefaults = detected && detected.length > 0;
|
|
7603
7979
|
const message = hasDefaults ? "Detected agents (press Enter to confirm, or toggle with space)" : "Which coding agents do you use? (toggle with space)";
|
|
@@ -7671,7 +8047,7 @@ async function classifyRefineIntent(message) {
|
|
|
7671
8047
|
const fastModel = getFastModel();
|
|
7672
8048
|
try {
|
|
7673
8049
|
const result = await llmJsonCall({
|
|
7674
|
-
system: `You classify whether a user message is a valid request to modify AI agent config files (CLAUDE.md, .cursorrules, skills).
|
|
8050
|
+
system: `You classify whether a user message is a valid request to modify AI agent config files (CLAUDE.md, .cursorrules, copilot-instructions.md, skills).
|
|
7675
8051
|
Valid: requests to add, remove, change, or restructure config content. Examples: "add testing commands", "remove the terraform section", "make CLAUDE.md shorter".
|
|
7676
8052
|
Invalid: questions, requests to show/display something, general chat, or anything that isn't a concrete config change.
|
|
7677
8053
|
Return {"valid": true} or {"valid": false}. Nothing else.`,
|
|
@@ -7725,18 +8101,18 @@ async function refineLoop(currentSetup, sessionHistory, summarizeSetup2, printSu
|
|
|
7725
8101
|
|
|
7726
8102
|
// src/commands/init-display.ts
|
|
7727
8103
|
import chalk12 from "chalk";
|
|
7728
|
-
import
|
|
8104
|
+
import fs29 from "fs";
|
|
7729
8105
|
function formatWhatChanged(setup) {
|
|
7730
8106
|
const lines = [];
|
|
7731
8107
|
const claude = setup.claude;
|
|
7732
8108
|
const codex = setup.codex;
|
|
7733
8109
|
const cursor = setup.cursor;
|
|
7734
8110
|
if (claude?.claudeMd) {
|
|
7735
|
-
const action =
|
|
8111
|
+
const action = fs29.existsSync("CLAUDE.md") ? "Updated" : "Created";
|
|
7736
8112
|
lines.push(`${action} CLAUDE.md`);
|
|
7737
8113
|
}
|
|
7738
8114
|
if (codex?.agentsMd) {
|
|
7739
|
-
const action =
|
|
8115
|
+
const action = fs29.existsSync("AGENTS.md") ? "Updated" : "Created";
|
|
7740
8116
|
lines.push(`${action} AGENTS.md`);
|
|
7741
8117
|
}
|
|
7742
8118
|
const allSkills = [];
|
|
@@ -7773,7 +8149,7 @@ function printSetupSummary(setup) {
|
|
|
7773
8149
|
};
|
|
7774
8150
|
if (claude) {
|
|
7775
8151
|
if (claude.claudeMd) {
|
|
7776
|
-
const icon =
|
|
8152
|
+
const icon = fs29.existsSync("CLAUDE.md") ? chalk12.yellow("~") : chalk12.green("+");
|
|
7777
8153
|
const desc = getDescription("CLAUDE.md");
|
|
7778
8154
|
console.log(` ${icon} ${chalk12.bold("CLAUDE.md")}`);
|
|
7779
8155
|
if (desc) console.log(chalk12.dim(` ${desc}`));
|
|
@@ -7783,7 +8159,7 @@ function printSetupSummary(setup) {
|
|
|
7783
8159
|
if (Array.isArray(skills) && skills.length > 0) {
|
|
7784
8160
|
for (const skill of skills) {
|
|
7785
8161
|
const skillPath = `.claude/skills/${skill.name}/SKILL.md`;
|
|
7786
|
-
const icon =
|
|
8162
|
+
const icon = fs29.existsSync(skillPath) ? chalk12.yellow("~") : chalk12.green("+");
|
|
7787
8163
|
const desc = getDescription(skillPath);
|
|
7788
8164
|
console.log(` ${icon} ${chalk12.bold(skillPath)}`);
|
|
7789
8165
|
console.log(chalk12.dim(` ${desc || skill.description || skill.name}`));
|
|
@@ -7794,7 +8170,7 @@ function printSetupSummary(setup) {
|
|
|
7794
8170
|
const codex = setup.codex;
|
|
7795
8171
|
if (codex) {
|
|
7796
8172
|
if (codex.agentsMd) {
|
|
7797
|
-
const icon =
|
|
8173
|
+
const icon = fs29.existsSync("AGENTS.md") ? chalk12.yellow("~") : chalk12.green("+");
|
|
7798
8174
|
const desc = getDescription("AGENTS.md");
|
|
7799
8175
|
console.log(` ${icon} ${chalk12.bold("AGENTS.md")}`);
|
|
7800
8176
|
if (desc) console.log(chalk12.dim(` ${desc}`));
|
|
@@ -7804,7 +8180,7 @@ function printSetupSummary(setup) {
|
|
|
7804
8180
|
if (Array.isArray(codexSkills) && codexSkills.length > 0) {
|
|
7805
8181
|
for (const skill of codexSkills) {
|
|
7806
8182
|
const skillPath = `.agents/skills/${skill.name}/SKILL.md`;
|
|
7807
|
-
const icon =
|
|
8183
|
+
const icon = fs29.existsSync(skillPath) ? chalk12.yellow("~") : chalk12.green("+");
|
|
7808
8184
|
const desc = getDescription(skillPath);
|
|
7809
8185
|
console.log(` ${icon} ${chalk12.bold(skillPath)}`);
|
|
7810
8186
|
console.log(chalk12.dim(` ${desc || skill.description || skill.name}`));
|
|
@@ -7814,7 +8190,7 @@ function printSetupSummary(setup) {
|
|
|
7814
8190
|
}
|
|
7815
8191
|
if (cursor) {
|
|
7816
8192
|
if (cursor.cursorrules) {
|
|
7817
|
-
const icon =
|
|
8193
|
+
const icon = fs29.existsSync(".cursorrules") ? chalk12.yellow("~") : chalk12.green("+");
|
|
7818
8194
|
const desc = getDescription(".cursorrules");
|
|
7819
8195
|
console.log(` ${icon} ${chalk12.bold(".cursorrules")}`);
|
|
7820
8196
|
if (desc) console.log(chalk12.dim(` ${desc}`));
|
|
@@ -7824,7 +8200,7 @@ function printSetupSummary(setup) {
|
|
|
7824
8200
|
if (Array.isArray(cursorSkills) && cursorSkills.length > 0) {
|
|
7825
8201
|
for (const skill of cursorSkills) {
|
|
7826
8202
|
const skillPath = `.cursor/skills/${skill.name}/SKILL.md`;
|
|
7827
|
-
const icon =
|
|
8203
|
+
const icon = fs29.existsSync(skillPath) ? chalk12.yellow("~") : chalk12.green("+");
|
|
7828
8204
|
const desc = getDescription(skillPath);
|
|
7829
8205
|
console.log(` ${icon} ${chalk12.bold(skillPath)}`);
|
|
7830
8206
|
console.log(chalk12.dim(` ${desc || skill.description || skill.name}`));
|
|
@@ -7835,7 +8211,7 @@ function printSetupSummary(setup) {
|
|
|
7835
8211
|
if (Array.isArray(rulesArr) && rulesArr.length > 0) {
|
|
7836
8212
|
for (const rule of rulesArr) {
|
|
7837
8213
|
const rulePath = `.cursor/rules/${rule.filename}`;
|
|
7838
|
-
const icon =
|
|
8214
|
+
const icon = fs29.existsSync(rulePath) ? chalk12.yellow("~") : chalk12.green("+");
|
|
7839
8215
|
const desc = getDescription(rulePath);
|
|
7840
8216
|
console.log(` ${icon} ${chalk12.bold(rulePath)}`);
|
|
7841
8217
|
if (desc) {
|
|
@@ -7882,12 +8258,12 @@ function displayTokenUsage() {
|
|
|
7882
8258
|
// src/commands/init-helpers.ts
|
|
7883
8259
|
init_config();
|
|
7884
8260
|
import chalk13 from "chalk";
|
|
7885
|
-
import
|
|
7886
|
-
import
|
|
8261
|
+
import fs30 from "fs";
|
|
8262
|
+
import path23 from "path";
|
|
7887
8263
|
function isFirstRun(dir) {
|
|
7888
|
-
const caliberDir =
|
|
8264
|
+
const caliberDir = path23.join(dir, ".caliber");
|
|
7889
8265
|
try {
|
|
7890
|
-
const stat =
|
|
8266
|
+
const stat = fs30.statSync(caliberDir);
|
|
7891
8267
|
return !stat.isDirectory();
|
|
7892
8268
|
} catch {
|
|
7893
8269
|
return true;
|
|
@@ -7940,8 +8316,8 @@ function ensurePermissions(fingerprint) {
|
|
|
7940
8316
|
const settingsPath = ".claude/settings.json";
|
|
7941
8317
|
let settings = {};
|
|
7942
8318
|
try {
|
|
7943
|
-
if (
|
|
7944
|
-
settings = JSON.parse(
|
|
8319
|
+
if (fs30.existsSync(settingsPath)) {
|
|
8320
|
+
settings = JSON.parse(fs30.readFileSync(settingsPath, "utf-8"));
|
|
7945
8321
|
}
|
|
7946
8322
|
} catch {
|
|
7947
8323
|
}
|
|
@@ -7950,12 +8326,12 @@ function ensurePermissions(fingerprint) {
|
|
|
7950
8326
|
if (Array.isArray(allow) && allow.length > 0) return;
|
|
7951
8327
|
permissions.allow = derivePermissions(fingerprint);
|
|
7952
8328
|
settings.permissions = permissions;
|
|
7953
|
-
if (!
|
|
7954
|
-
|
|
8329
|
+
if (!fs30.existsSync(".claude")) fs30.mkdirSync(".claude", { recursive: true });
|
|
8330
|
+
fs30.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
7955
8331
|
}
|
|
7956
8332
|
function writeErrorLog(config, rawOutput, error, stopReason) {
|
|
7957
8333
|
try {
|
|
7958
|
-
const logPath =
|
|
8334
|
+
const logPath = path23.join(process.cwd(), ".caliber", "error-log.md");
|
|
7959
8335
|
const lines = [
|
|
7960
8336
|
`# Generation Error \u2014 ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
7961
8337
|
"",
|
|
@@ -7968,8 +8344,8 @@ function writeErrorLog(config, rawOutput, error, stopReason) {
|
|
|
7968
8344
|
lines.push("## Error", "```", error, "```", "");
|
|
7969
8345
|
}
|
|
7970
8346
|
lines.push("## Raw LLM Output", "```", rawOutput || "(empty)", "```");
|
|
7971
|
-
|
|
7972
|
-
|
|
8347
|
+
fs30.mkdirSync(path23.join(process.cwd(), ".caliber"), { recursive: true });
|
|
8348
|
+
fs30.writeFileSync(logPath, lines.join("\n"));
|
|
7973
8349
|
console.log(chalk13.dim(`
|
|
7974
8350
|
Error log written to .caliber/error-log.md`));
|
|
7975
8351
|
} catch {
|
|
@@ -8036,7 +8412,7 @@ async function initCommand(options) {
|
|
|
8036
8412
|
\u255A\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D
|
|
8037
8413
|
`));
|
|
8038
8414
|
console.log(chalk14.dim(" Scan your project and generate tailored config files for"));
|
|
8039
|
-
console.log(chalk14.dim(" Claude Code, Cursor, and
|
|
8415
|
+
console.log(chalk14.dim(" Claude Code, Cursor, Codex, and GitHub Copilot.\n"));
|
|
8040
8416
|
console.log(title.bold(" How it works:\n"));
|
|
8041
8417
|
console.log(chalk14.dim(" 1. Setup Connect your LLM provider and select your agents"));
|
|
8042
8418
|
console.log(chalk14.dim(" 2. Engine Detect stack, generate configs & skills in parallel"));
|
|
@@ -8165,6 +8541,13 @@ async function initCommand(options) {
|
|
|
8165
8541
|
display.update(TASK_STACK, "done", stackSummary + largeRepoNote);
|
|
8166
8542
|
trackInitProjectDiscovered(fingerprint.languages.length, fingerprint.frameworks.length, fingerprint.fileTree.length);
|
|
8167
8543
|
log(options.verbose, `Fingerprint: ${fingerprint.languages.length} languages, ${fingerprint.frameworks.length} frameworks, ${fingerprint.fileTree.length} files`);
|
|
8544
|
+
const cliSources = options.source || [];
|
|
8545
|
+
const workspaces = getDetectedWorkspaces(process.cwd());
|
|
8546
|
+
const sources2 = resolveAllSources(process.cwd(), cliSources, workspaces);
|
|
8547
|
+
if (sources2.length > 0) {
|
|
8548
|
+
fingerprint.sources = sources2;
|
|
8549
|
+
log(options.verbose, `Sources: ${sources2.length} resolved (${sources2.map((s) => s.name).join(", ")})`);
|
|
8550
|
+
}
|
|
8168
8551
|
if (report) {
|
|
8169
8552
|
report.addJson("Fingerprint: Git", { remote: fingerprint.gitRemoteUrl, packageName: fingerprint.packageName });
|
|
8170
8553
|
report.addCodeBlock("Fingerprint: File Tree", fingerprint.fileTree.join("\n"));
|
|
@@ -8397,10 +8780,10 @@ async function initCommand(options) {
|
|
|
8397
8780
|
console.log(JSON.stringify(generatedSetup, null, 2));
|
|
8398
8781
|
return;
|
|
8399
8782
|
}
|
|
8400
|
-
const { default:
|
|
8401
|
-
const writeSpinner =
|
|
8783
|
+
const { default: ora9 } = await import("ora");
|
|
8784
|
+
const writeSpinner = ora9("Writing config files...").start();
|
|
8402
8785
|
try {
|
|
8403
|
-
if (targetAgent.includes("codex") && !
|
|
8786
|
+
if (targetAgent.includes("codex") && !fs31.existsSync("AGENTS.md") && !generatedSetup.codex) {
|
|
8404
8787
|
const claude = generatedSetup.claude;
|
|
8405
8788
|
const cursor = generatedSetup.cursor;
|
|
8406
8789
|
const agentRefs = [];
|
|
@@ -8613,9 +8996,9 @@ ${agentRefs.join(" ")}
|
|
|
8613
8996
|
}
|
|
8614
8997
|
if (report) {
|
|
8615
8998
|
report.markStep("Finished");
|
|
8616
|
-
const reportPath =
|
|
8999
|
+
const reportPath = path24.join(process.cwd(), ".caliber", "debug-report.md");
|
|
8617
9000
|
report.write(reportPath);
|
|
8618
|
-
console.log(chalk14.dim(` Debug report written to ${
|
|
9001
|
+
console.log(chalk14.dim(` Debug report written to ${path24.relative(process.cwd(), reportPath)}
|
|
8619
9002
|
`));
|
|
8620
9003
|
}
|
|
8621
9004
|
}
|
|
@@ -8654,7 +9037,7 @@ function undoCommand() {
|
|
|
8654
9037
|
|
|
8655
9038
|
// src/commands/status.ts
|
|
8656
9039
|
import chalk16 from "chalk";
|
|
8657
|
-
import
|
|
9040
|
+
import fs32 from "fs";
|
|
8658
9041
|
init_config();
|
|
8659
9042
|
async function statusCommand(options) {
|
|
8660
9043
|
const config = loadConfig();
|
|
@@ -8681,7 +9064,7 @@ async function statusCommand(options) {
|
|
|
8681
9064
|
}
|
|
8682
9065
|
console.log(` Files managed: ${chalk16.cyan(manifest.entries.length.toString())}`);
|
|
8683
9066
|
for (const entry of manifest.entries) {
|
|
8684
|
-
const exists =
|
|
9067
|
+
const exists = fs32.existsSync(entry.path);
|
|
8685
9068
|
const icon = exists ? chalk16.green("\u2713") : chalk16.red("\u2717");
|
|
8686
9069
|
console.log(` ${icon} ${entry.path} (${entry.action})`);
|
|
8687
9070
|
}
|
|
@@ -8836,21 +9219,21 @@ async function regenerateCommand(options) {
|
|
|
8836
9219
|
}
|
|
8837
9220
|
|
|
8838
9221
|
// src/commands/score.ts
|
|
8839
|
-
import
|
|
9222
|
+
import fs33 from "fs";
|
|
8840
9223
|
import os6 from "os";
|
|
8841
|
-
import
|
|
9224
|
+
import path25 from "path";
|
|
8842
9225
|
import { execFileSync } from "child_process";
|
|
8843
9226
|
import chalk18 from "chalk";
|
|
8844
9227
|
var CONFIG_FILES = ["CLAUDE.md", "AGENTS.md", ".cursorrules", "CALIBER_LEARNINGS.md"];
|
|
8845
9228
|
var CONFIG_DIRS = [".claude", ".cursor"];
|
|
8846
9229
|
function scoreBaseRef(ref, target) {
|
|
8847
9230
|
if (!/^[\w.\-\/~^@{}]+$/.test(ref)) return null;
|
|
8848
|
-
const tmpDir =
|
|
9231
|
+
const tmpDir = fs33.mkdtempSync(path25.join(os6.tmpdir(), "caliber-compare-"));
|
|
8849
9232
|
try {
|
|
8850
9233
|
for (const file of CONFIG_FILES) {
|
|
8851
9234
|
try {
|
|
8852
9235
|
const content = execFileSync("git", ["show", `${ref}:${file}`], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
8853
|
-
|
|
9236
|
+
fs33.writeFileSync(path25.join(tmpDir, file), content);
|
|
8854
9237
|
} catch {
|
|
8855
9238
|
}
|
|
8856
9239
|
}
|
|
@@ -8858,10 +9241,10 @@ function scoreBaseRef(ref, target) {
|
|
|
8858
9241
|
try {
|
|
8859
9242
|
const files = execFileSync("git", ["ls-tree", "-r", "--name-only", ref, `${dir}/`], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim().split("\n").filter(Boolean);
|
|
8860
9243
|
for (const file of files) {
|
|
8861
|
-
const filePath =
|
|
8862
|
-
|
|
9244
|
+
const filePath = path25.join(tmpDir, file);
|
|
9245
|
+
fs33.mkdirSync(path25.dirname(filePath), { recursive: true });
|
|
8863
9246
|
const content = execFileSync("git", ["show", `${ref}:${file}`], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
|
|
8864
|
-
|
|
9247
|
+
fs33.writeFileSync(filePath, content);
|
|
8865
9248
|
}
|
|
8866
9249
|
} catch {
|
|
8867
9250
|
}
|
|
@@ -8871,7 +9254,7 @@ function scoreBaseRef(ref, target) {
|
|
|
8871
9254
|
} catch {
|
|
8872
9255
|
return null;
|
|
8873
9256
|
} finally {
|
|
8874
|
-
|
|
9257
|
+
fs33.rmSync(tmpDir, { recursive: true, force: true });
|
|
8875
9258
|
}
|
|
8876
9259
|
}
|
|
8877
9260
|
async function scoreCommand(options) {
|
|
@@ -8931,8 +9314,8 @@ async function scoreCommand(options) {
|
|
|
8931
9314
|
}
|
|
8932
9315
|
|
|
8933
9316
|
// src/commands/refresh.ts
|
|
8934
|
-
import
|
|
8935
|
-
import
|
|
9317
|
+
import fs37 from "fs";
|
|
9318
|
+
import path29 from "path";
|
|
8936
9319
|
import chalk19 from "chalk";
|
|
8937
9320
|
import ora6 from "ora";
|
|
8938
9321
|
|
|
@@ -9010,38 +9393,49 @@ function collectDiff(lastSha) {
|
|
|
9010
9393
|
}
|
|
9011
9394
|
|
|
9012
9395
|
// src/writers/refresh.ts
|
|
9013
|
-
import
|
|
9014
|
-
import
|
|
9396
|
+
import fs34 from "fs";
|
|
9397
|
+
import path26 from "path";
|
|
9015
9398
|
function writeRefreshDocs(docs) {
|
|
9016
9399
|
const written = [];
|
|
9017
9400
|
if (docs.claudeMd) {
|
|
9018
|
-
|
|
9401
|
+
fs34.writeFileSync("CLAUDE.md", docs.claudeMd);
|
|
9019
9402
|
written.push("CLAUDE.md");
|
|
9020
9403
|
}
|
|
9021
9404
|
if (docs.readmeMd) {
|
|
9022
|
-
|
|
9405
|
+
fs34.writeFileSync("README.md", docs.readmeMd);
|
|
9023
9406
|
written.push("README.md");
|
|
9024
9407
|
}
|
|
9025
9408
|
if (docs.cursorrules) {
|
|
9026
|
-
|
|
9409
|
+
fs34.writeFileSync(".cursorrules", docs.cursorrules);
|
|
9027
9410
|
written.push(".cursorrules");
|
|
9028
9411
|
}
|
|
9029
9412
|
if (docs.cursorRules) {
|
|
9030
|
-
const rulesDir =
|
|
9031
|
-
if (!
|
|
9413
|
+
const rulesDir = path26.join(".cursor", "rules");
|
|
9414
|
+
if (!fs34.existsSync(rulesDir)) fs34.mkdirSync(rulesDir, { recursive: true });
|
|
9032
9415
|
for (const rule of docs.cursorRules) {
|
|
9033
|
-
|
|
9034
|
-
|
|
9035
|
-
written.push(filePath);
|
|
9416
|
+
fs34.writeFileSync(path26.join(rulesDir, rule.filename), rule.content);
|
|
9417
|
+
written.push(`.cursor/rules/${rule.filename}`);
|
|
9036
9418
|
}
|
|
9037
9419
|
}
|
|
9038
9420
|
if (docs.claudeSkills) {
|
|
9039
|
-
const skillsDir =
|
|
9040
|
-
if (!
|
|
9421
|
+
const skillsDir = path26.join(".claude", "skills");
|
|
9422
|
+
if (!fs34.existsSync(skillsDir)) fs34.mkdirSync(skillsDir, { recursive: true });
|
|
9041
9423
|
for (const skill of docs.claudeSkills) {
|
|
9042
|
-
|
|
9043
|
-
|
|
9044
|
-
|
|
9424
|
+
fs34.writeFileSync(path26.join(skillsDir, skill.filename), skill.content);
|
|
9425
|
+
written.push(`.claude/skills/${skill.filename}`);
|
|
9426
|
+
}
|
|
9427
|
+
}
|
|
9428
|
+
if (docs.copilotInstructions) {
|
|
9429
|
+
fs34.mkdirSync(".github", { recursive: true });
|
|
9430
|
+
fs34.writeFileSync(path26.join(".github", "copilot-instructions.md"), docs.copilotInstructions);
|
|
9431
|
+
written.push(".github/copilot-instructions.md");
|
|
9432
|
+
}
|
|
9433
|
+
if (docs.copilotInstructionFiles) {
|
|
9434
|
+
const instructionsDir = path26.join(".github", "instructions");
|
|
9435
|
+
fs34.mkdirSync(instructionsDir, { recursive: true });
|
|
9436
|
+
for (const file of docs.copilotInstructionFiles) {
|
|
9437
|
+
fs34.writeFileSync(path26.join(instructionsDir, file.filename), file.content);
|
|
9438
|
+
written.push(`.github/instructions/${file.filename}`);
|
|
9045
9439
|
}
|
|
9046
9440
|
}
|
|
9047
9441
|
return written;
|
|
@@ -9049,8 +9443,8 @@ function writeRefreshDocs(docs) {
|
|
|
9049
9443
|
|
|
9050
9444
|
// src/ai/refresh.ts
|
|
9051
9445
|
init_config();
|
|
9052
|
-
async function refreshDocs(diff, existingDocs, projectContext, learnedSection) {
|
|
9053
|
-
const prompt = buildRefreshPrompt(diff, existingDocs, projectContext, learnedSection);
|
|
9446
|
+
async function refreshDocs(diff, existingDocs, projectContext, learnedSection, sources2) {
|
|
9447
|
+
const prompt = buildRefreshPrompt(diff, existingDocs, projectContext, learnedSection, sources2);
|
|
9054
9448
|
const fastModel = getFastModel();
|
|
9055
9449
|
const raw = await llmCall({
|
|
9056
9450
|
system: REFRESH_SYSTEM_PROMPT,
|
|
@@ -9060,7 +9454,7 @@ async function refreshDocs(diff, existingDocs, projectContext, learnedSection) {
|
|
|
9060
9454
|
});
|
|
9061
9455
|
return parseJsonResponse(raw);
|
|
9062
9456
|
}
|
|
9063
|
-
function buildRefreshPrompt(diff, existingDocs, projectContext, learnedSection) {
|
|
9457
|
+
function buildRefreshPrompt(diff, existingDocs, projectContext, learnedSection, sources2) {
|
|
9064
9458
|
const parts = [];
|
|
9065
9459
|
parts.push("Update documentation based on the following code changes.\n");
|
|
9066
9460
|
if (projectContext.packageName) parts.push(`Project: ${projectContext.packageName}`);
|
|
@@ -9113,12 +9507,15 @@ Changed files: ${diff.changedFiles.join(", ")}`);
|
|
|
9113
9507
|
parts.push("Consider these accumulated learnings when deciding what to update:");
|
|
9114
9508
|
parts.push(learnedSection);
|
|
9115
9509
|
}
|
|
9510
|
+
if (sources2?.length) {
|
|
9511
|
+
parts.push(formatSourcesForPrompt(sources2));
|
|
9512
|
+
}
|
|
9116
9513
|
return parts.join("\n");
|
|
9117
9514
|
}
|
|
9118
9515
|
|
|
9119
9516
|
// src/learner/writer.ts
|
|
9120
|
-
import
|
|
9121
|
-
import
|
|
9517
|
+
import fs35 from "fs";
|
|
9518
|
+
import path27 from "path";
|
|
9122
9519
|
|
|
9123
9520
|
// src/learner/utils.ts
|
|
9124
9521
|
var TYPE_PREFIX_RE = /^\*\*\[[^\]]+\]\*\*\s*/;
|
|
@@ -9128,6 +9525,13 @@ function normalizeBullet(bullet) {
|
|
|
9128
9525
|
function hasTypePrefix(bullet) {
|
|
9129
9526
|
return TYPE_PREFIX_RE.test(bullet.replace(/^- /, ""));
|
|
9130
9527
|
}
|
|
9528
|
+
var SCOPE_RE = /^\*\*\[[^\]:]+:([^\]]+)\]\*\*/;
|
|
9529
|
+
function extractScope(bullet) {
|
|
9530
|
+
const clean = bullet.replace(/^- /, "");
|
|
9531
|
+
const match = clean.match(SCOPE_RE);
|
|
9532
|
+
if (match && match[1] === "personal") return "personal";
|
|
9533
|
+
return "project";
|
|
9534
|
+
}
|
|
9131
9535
|
var SIMILARITY_THRESHOLD = 0.7;
|
|
9132
9536
|
function isSimilarLearning(a, b) {
|
|
9133
9537
|
const normA = normalizeBullet(a);
|
|
@@ -9140,12 +9544,19 @@ function isSimilarLearning(a, b) {
|
|
|
9140
9544
|
}
|
|
9141
9545
|
|
|
9142
9546
|
// src/learner/writer.ts
|
|
9547
|
+
init_constants();
|
|
9143
9548
|
var LEARNINGS_FILE = "CALIBER_LEARNINGS.md";
|
|
9144
9549
|
var LEARNINGS_HEADER = `# Caliber Learnings
|
|
9145
9550
|
|
|
9146
9551
|
Accumulated patterns and anti-patterns from development sessions.
|
|
9147
9552
|
Auto-managed by [caliber](https://github.com/rely-ai-org/caliber) \u2014 do not edit manually.
|
|
9148
9553
|
|
|
9554
|
+
`;
|
|
9555
|
+
var PERSONAL_LEARNINGS_HEADER = `# Personal Learnings
|
|
9556
|
+
|
|
9557
|
+
Developer-specific patterns and preferences.
|
|
9558
|
+
Auto-managed by [caliber](https://github.com/rely-ai-org/caliber) \u2014 do not edit manually.
|
|
9559
|
+
|
|
9149
9560
|
`;
|
|
9150
9561
|
var LEARNED_START = "<!-- caliber:learned -->";
|
|
9151
9562
|
var LEARNED_END = "<!-- /caliber:learned -->";
|
|
@@ -9154,11 +9565,24 @@ function writeLearnedContent(update) {
|
|
|
9154
9565
|
const written = [];
|
|
9155
9566
|
let newItemCount = 0;
|
|
9156
9567
|
let newItems = [];
|
|
9568
|
+
let personalItemCount = 0;
|
|
9569
|
+
let personalItems = [];
|
|
9157
9570
|
if (update.claudeMdLearnedSection) {
|
|
9158
|
-
const
|
|
9159
|
-
|
|
9160
|
-
|
|
9161
|
-
|
|
9571
|
+
const bullets = parseBullets(update.claudeMdLearnedSection);
|
|
9572
|
+
const projectBullets = bullets.filter((b) => extractScope(b) === "project");
|
|
9573
|
+
const personalBullets = bullets.filter((b) => extractScope(b) === "personal");
|
|
9574
|
+
if (projectBullets.length > 0) {
|
|
9575
|
+
const result = writeLearnedSection(projectBullets.join("\n"));
|
|
9576
|
+
newItemCount = result.newCount;
|
|
9577
|
+
newItems = result.newItems;
|
|
9578
|
+
written.push(LEARNINGS_FILE);
|
|
9579
|
+
}
|
|
9580
|
+
if (personalBullets.length > 0) {
|
|
9581
|
+
const result = writePersonalLearnedSection(personalBullets.join("\n"));
|
|
9582
|
+
personalItemCount = result.newCount;
|
|
9583
|
+
personalItems = result.newItems;
|
|
9584
|
+
written.push(PERSONAL_LEARNINGS_FILE);
|
|
9585
|
+
}
|
|
9162
9586
|
}
|
|
9163
9587
|
if (update.skills?.length) {
|
|
9164
9588
|
for (const skill of update.skills) {
|
|
@@ -9166,7 +9590,7 @@ function writeLearnedContent(update) {
|
|
|
9166
9590
|
written.push(skillPath);
|
|
9167
9591
|
}
|
|
9168
9592
|
}
|
|
9169
|
-
return { written, newItemCount, newItems };
|
|
9593
|
+
return { written, newItemCount, newItems, personalItemCount, personalItems };
|
|
9170
9594
|
}
|
|
9171
9595
|
function parseBullets(content) {
|
|
9172
9596
|
const lines = content.split("\n");
|
|
@@ -9207,19 +9631,22 @@ function deduplicateLearnedItems(existing, incoming) {
|
|
|
9207
9631
|
const capped = merged.length > MAX_LEARNED_ITEMS ? merged.slice(-MAX_LEARNED_ITEMS) : merged;
|
|
9208
9632
|
return { merged: capped.join("\n"), newCount: newItems.length, newItems };
|
|
9209
9633
|
}
|
|
9210
|
-
function
|
|
9211
|
-
const
|
|
9212
|
-
|
|
9213
|
-
|
|
9634
|
+
function writeLearnedSectionTo(filePath, header, existing, incoming, mode) {
|
|
9635
|
+
const { merged, newCount, newItems } = deduplicateLearnedItems(existing, incoming);
|
|
9636
|
+
fs35.writeFileSync(filePath, header + merged + "\n");
|
|
9637
|
+
if (mode) fs35.chmodSync(filePath, mode);
|
|
9214
9638
|
return { newCount, newItems };
|
|
9215
9639
|
}
|
|
9640
|
+
function writeLearnedSection(content) {
|
|
9641
|
+
return writeLearnedSectionTo(LEARNINGS_FILE, LEARNINGS_HEADER, readLearnedSection(), content);
|
|
9642
|
+
}
|
|
9216
9643
|
function writeLearnedSkill(skill) {
|
|
9217
|
-
const skillDir =
|
|
9218
|
-
if (!
|
|
9219
|
-
const skillPath =
|
|
9220
|
-
if (!skill.isNew &&
|
|
9221
|
-
const existing =
|
|
9222
|
-
|
|
9644
|
+
const skillDir = path27.join(".claude", "skills", skill.name);
|
|
9645
|
+
if (!fs35.existsSync(skillDir)) fs35.mkdirSync(skillDir, { recursive: true });
|
|
9646
|
+
const skillPath = path27.join(skillDir, "SKILL.md");
|
|
9647
|
+
if (!skill.isNew && fs35.existsSync(skillPath)) {
|
|
9648
|
+
const existing = fs35.readFileSync(skillPath, "utf-8");
|
|
9649
|
+
fs35.writeFileSync(skillPath, existing.trimEnd() + "\n\n" + skill.content);
|
|
9223
9650
|
} else {
|
|
9224
9651
|
const frontmatter = [
|
|
9225
9652
|
"---",
|
|
@@ -9228,37 +9655,47 @@ function writeLearnedSkill(skill) {
|
|
|
9228
9655
|
"---",
|
|
9229
9656
|
""
|
|
9230
9657
|
].join("\n");
|
|
9231
|
-
|
|
9658
|
+
fs35.writeFileSync(skillPath, frontmatter + skill.content);
|
|
9232
9659
|
}
|
|
9233
9660
|
return skillPath;
|
|
9234
9661
|
}
|
|
9662
|
+
function writePersonalLearnedSection(content) {
|
|
9663
|
+
if (!fs35.existsSync(AUTH_DIR)) fs35.mkdirSync(AUTH_DIR, { recursive: true });
|
|
9664
|
+
return writeLearnedSectionTo(PERSONAL_LEARNINGS_FILE, PERSONAL_LEARNINGS_HEADER, readPersonalLearnings(), content, 384);
|
|
9665
|
+
}
|
|
9666
|
+
function readPersonalLearnings() {
|
|
9667
|
+
if (!fs35.existsSync(PERSONAL_LEARNINGS_FILE)) return null;
|
|
9668
|
+
const content = fs35.readFileSync(PERSONAL_LEARNINGS_FILE, "utf-8");
|
|
9669
|
+
const bullets = content.split("\n").filter((l) => l.startsWith("- ")).join("\n");
|
|
9670
|
+
return bullets || null;
|
|
9671
|
+
}
|
|
9235
9672
|
function readLearnedSection() {
|
|
9236
|
-
if (
|
|
9237
|
-
const content2 =
|
|
9673
|
+
if (fs35.existsSync(LEARNINGS_FILE)) {
|
|
9674
|
+
const content2 = fs35.readFileSync(LEARNINGS_FILE, "utf-8");
|
|
9238
9675
|
const bullets = content2.split("\n").filter((l) => l.startsWith("- ")).join("\n");
|
|
9239
9676
|
return bullets || null;
|
|
9240
9677
|
}
|
|
9241
9678
|
const claudeMdPath = "CLAUDE.md";
|
|
9242
|
-
if (!
|
|
9243
|
-
const content =
|
|
9679
|
+
if (!fs35.existsSync(claudeMdPath)) return null;
|
|
9680
|
+
const content = fs35.readFileSync(claudeMdPath, "utf-8");
|
|
9244
9681
|
const startIdx = content.indexOf(LEARNED_START);
|
|
9245
9682
|
const endIdx = content.indexOf(LEARNED_END);
|
|
9246
9683
|
if (startIdx === -1 || endIdx === -1) return null;
|
|
9247
9684
|
return content.slice(startIdx + LEARNED_START.length, endIdx).trim() || null;
|
|
9248
9685
|
}
|
|
9249
9686
|
function migrateInlineLearnings() {
|
|
9250
|
-
if (
|
|
9687
|
+
if (fs35.existsSync(LEARNINGS_FILE)) return false;
|
|
9251
9688
|
const claudeMdPath = "CLAUDE.md";
|
|
9252
|
-
if (!
|
|
9253
|
-
const content =
|
|
9689
|
+
if (!fs35.existsSync(claudeMdPath)) return false;
|
|
9690
|
+
const content = fs35.readFileSync(claudeMdPath, "utf-8");
|
|
9254
9691
|
const startIdx = content.indexOf(LEARNED_START);
|
|
9255
9692
|
const endIdx = content.indexOf(LEARNED_END);
|
|
9256
9693
|
if (startIdx === -1 || endIdx === -1) return false;
|
|
9257
9694
|
const section = content.slice(startIdx + LEARNED_START.length, endIdx).trim();
|
|
9258
9695
|
if (!section) return false;
|
|
9259
|
-
|
|
9696
|
+
fs35.writeFileSync(LEARNINGS_FILE, LEARNINGS_HEADER + section + "\n");
|
|
9260
9697
|
const cleaned = content.slice(0, startIdx) + content.slice(endIdx + LEARNED_END.length);
|
|
9261
|
-
|
|
9698
|
+
fs35.writeFileSync(claudeMdPath, cleaned.replace(/\n{3,}/g, "\n\n").trim() + "\n");
|
|
9262
9699
|
return true;
|
|
9263
9700
|
}
|
|
9264
9701
|
|
|
@@ -9270,11 +9707,11 @@ function log2(quiet, ...args) {
|
|
|
9270
9707
|
function discoverGitRepos(parentDir) {
|
|
9271
9708
|
const repos = [];
|
|
9272
9709
|
try {
|
|
9273
|
-
const entries =
|
|
9710
|
+
const entries = fs37.readdirSync(parentDir, { withFileTypes: true });
|
|
9274
9711
|
for (const entry of entries) {
|
|
9275
9712
|
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
9276
|
-
const childPath =
|
|
9277
|
-
if (
|
|
9713
|
+
const childPath = path29.join(parentDir, entry.name);
|
|
9714
|
+
if (fs37.existsSync(path29.join(childPath, ".git"))) {
|
|
9278
9715
|
repos.push(childPath);
|
|
9279
9716
|
}
|
|
9280
9717
|
}
|
|
@@ -9305,6 +9742,8 @@ async function refreshSingleRepo(repoDir, options) {
|
|
|
9305
9742
|
frameworks: fingerprint.frameworks,
|
|
9306
9743
|
packageName: fingerprint.packageName
|
|
9307
9744
|
};
|
|
9745
|
+
const workspaces = getDetectedWorkspaces(repoDir);
|
|
9746
|
+
const sources2 = resolveAllSources(repoDir, [], workspaces);
|
|
9308
9747
|
const response = await refreshDocs(
|
|
9309
9748
|
{
|
|
9310
9749
|
committed: diff.committedDiff,
|
|
@@ -9315,7 +9754,8 @@ async function refreshSingleRepo(repoDir, options) {
|
|
|
9315
9754
|
},
|
|
9316
9755
|
existingDocs,
|
|
9317
9756
|
projectContext,
|
|
9318
|
-
learnedSection
|
|
9757
|
+
learnedSection,
|
|
9758
|
+
sources2.length > 0 ? sources2 : void 0
|
|
9319
9759
|
);
|
|
9320
9760
|
if (!response.docsUpdated || response.docsUpdated.length === 0) {
|
|
9321
9761
|
spinner?.succeed(`${prefix}No doc updates needed`);
|
|
@@ -9377,7 +9817,7 @@ async function refreshCommand(options) {
|
|
|
9377
9817
|
`));
|
|
9378
9818
|
const originalDir = process.cwd();
|
|
9379
9819
|
for (const repo of repos) {
|
|
9380
|
-
const repoName =
|
|
9820
|
+
const repoName = path29.basename(repo);
|
|
9381
9821
|
try {
|
|
9382
9822
|
process.chdir(repo);
|
|
9383
9823
|
await refreshSingleRepo(repo, { ...options, label: repoName });
|
|
@@ -9398,7 +9838,7 @@ async function refreshCommand(options) {
|
|
|
9398
9838
|
|
|
9399
9839
|
// src/commands/hooks.ts
|
|
9400
9840
|
import chalk20 from "chalk";
|
|
9401
|
-
import
|
|
9841
|
+
import fs38 from "fs";
|
|
9402
9842
|
var HOOKS = [
|
|
9403
9843
|
{
|
|
9404
9844
|
id: "session-end",
|
|
@@ -9438,11 +9878,11 @@ async function hooksCommand(options) {
|
|
|
9438
9878
|
console.log(chalk20.green(" \u2713") + ` ${hook.label} enabled`);
|
|
9439
9879
|
}
|
|
9440
9880
|
}
|
|
9441
|
-
if (
|
|
9881
|
+
if (fs38.existsSync(".claude")) {
|
|
9442
9882
|
const r = installLearningHooks();
|
|
9443
9883
|
if (r.installed) console.log(chalk20.green(" \u2713") + " Claude Code learning hooks enabled");
|
|
9444
9884
|
}
|
|
9445
|
-
if (
|
|
9885
|
+
if (fs38.existsSync(".cursor")) {
|
|
9446
9886
|
const r = installCursorLearningHooks();
|
|
9447
9887
|
if (r.installed) console.log(chalk20.green(" \u2713") + " Cursor learning hooks enabled");
|
|
9448
9888
|
}
|
|
@@ -9610,7 +10050,7 @@ async function configCommand() {
|
|
|
9610
10050
|
}
|
|
9611
10051
|
|
|
9612
10052
|
// src/commands/learn.ts
|
|
9613
|
-
import
|
|
10053
|
+
import fs42 from "fs";
|
|
9614
10054
|
import chalk23 from "chalk";
|
|
9615
10055
|
|
|
9616
10056
|
// src/learner/stdin.ts
|
|
@@ -9642,8 +10082,8 @@ function readStdin() {
|
|
|
9642
10082
|
|
|
9643
10083
|
// src/learner/storage.ts
|
|
9644
10084
|
init_constants();
|
|
9645
|
-
import
|
|
9646
|
-
import
|
|
10085
|
+
import fs39 from "fs";
|
|
10086
|
+
import path30 from "path";
|
|
9647
10087
|
var MAX_RESPONSE_LENGTH = 2e3;
|
|
9648
10088
|
var DEFAULT_STATE = {
|
|
9649
10089
|
sessionId: null,
|
|
@@ -9652,48 +10092,48 @@ var DEFAULT_STATE = {
|
|
|
9652
10092
|
lastAnalysisEventCount: 0
|
|
9653
10093
|
};
|
|
9654
10094
|
function ensureLearningDir() {
|
|
9655
|
-
if (!
|
|
9656
|
-
|
|
10095
|
+
if (!fs39.existsSync(LEARNING_DIR)) {
|
|
10096
|
+
fs39.mkdirSync(LEARNING_DIR, { recursive: true });
|
|
9657
10097
|
}
|
|
9658
10098
|
}
|
|
9659
10099
|
function sessionFilePath() {
|
|
9660
|
-
return
|
|
10100
|
+
return path30.join(LEARNING_DIR, LEARNING_SESSION_FILE);
|
|
9661
10101
|
}
|
|
9662
10102
|
function stateFilePath() {
|
|
9663
|
-
return
|
|
10103
|
+
return path30.join(LEARNING_DIR, LEARNING_STATE_FILE);
|
|
9664
10104
|
}
|
|
9665
10105
|
function truncateResponse(response) {
|
|
9666
10106
|
const str = JSON.stringify(response);
|
|
9667
10107
|
if (str.length <= MAX_RESPONSE_LENGTH) return response;
|
|
9668
10108
|
return { _truncated: str.slice(0, MAX_RESPONSE_LENGTH) };
|
|
9669
10109
|
}
|
|
10110
|
+
function trimSessionFileIfNeeded(filePath) {
|
|
10111
|
+
const state = readState2();
|
|
10112
|
+
if (state.eventCount + 1 > LEARNING_MAX_EVENTS) {
|
|
10113
|
+
const lines = fs39.readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
|
|
10114
|
+
if (lines.length > LEARNING_MAX_EVENTS) {
|
|
10115
|
+
const kept = lines.slice(lines.length - LEARNING_MAX_EVENTS);
|
|
10116
|
+
fs39.writeFileSync(filePath, kept.join("\n") + "\n");
|
|
10117
|
+
}
|
|
10118
|
+
}
|
|
10119
|
+
}
|
|
9670
10120
|
function appendEvent(event) {
|
|
9671
10121
|
ensureLearningDir();
|
|
9672
10122
|
const truncated = { ...event, tool_response: truncateResponse(event.tool_response) };
|
|
9673
10123
|
const filePath = sessionFilePath();
|
|
9674
|
-
|
|
9675
|
-
|
|
9676
|
-
if (count > LEARNING_MAX_EVENTS) {
|
|
9677
|
-
const lines = fs37.readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
|
|
9678
|
-
const kept = lines.slice(lines.length - LEARNING_MAX_EVENTS);
|
|
9679
|
-
fs37.writeFileSync(filePath, kept.join("\n") + "\n");
|
|
9680
|
-
}
|
|
10124
|
+
fs39.appendFileSync(filePath, JSON.stringify(truncated) + "\n");
|
|
10125
|
+
trimSessionFileIfNeeded(filePath);
|
|
9681
10126
|
}
|
|
9682
10127
|
function appendPromptEvent(event) {
|
|
9683
10128
|
ensureLearningDir();
|
|
9684
10129
|
const filePath = sessionFilePath();
|
|
9685
|
-
|
|
9686
|
-
|
|
9687
|
-
if (count > LEARNING_MAX_EVENTS) {
|
|
9688
|
-
const lines = fs37.readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
|
|
9689
|
-
const kept = lines.slice(lines.length - LEARNING_MAX_EVENTS);
|
|
9690
|
-
fs37.writeFileSync(filePath, kept.join("\n") + "\n");
|
|
9691
|
-
}
|
|
10130
|
+
fs39.appendFileSync(filePath, JSON.stringify(event) + "\n");
|
|
10131
|
+
trimSessionFileIfNeeded(filePath);
|
|
9692
10132
|
}
|
|
9693
10133
|
function readAllEvents() {
|
|
9694
10134
|
const filePath = sessionFilePath();
|
|
9695
|
-
if (!
|
|
9696
|
-
const lines =
|
|
10135
|
+
if (!fs39.existsSync(filePath)) return [];
|
|
10136
|
+
const lines = fs39.readFileSync(filePath, "utf-8").split("\n").filter(Boolean);
|
|
9697
10137
|
const events = [];
|
|
9698
10138
|
for (const line of lines) {
|
|
9699
10139
|
try {
|
|
@@ -9705,26 +10145,26 @@ function readAllEvents() {
|
|
|
9705
10145
|
}
|
|
9706
10146
|
function getEventCount() {
|
|
9707
10147
|
const filePath = sessionFilePath();
|
|
9708
|
-
if (!
|
|
9709
|
-
const content =
|
|
10148
|
+
if (!fs39.existsSync(filePath)) return 0;
|
|
10149
|
+
const content = fs39.readFileSync(filePath, "utf-8");
|
|
9710
10150
|
return content.split("\n").filter(Boolean).length;
|
|
9711
10151
|
}
|
|
9712
10152
|
function clearSession() {
|
|
9713
10153
|
const filePath = sessionFilePath();
|
|
9714
|
-
if (
|
|
10154
|
+
if (fs39.existsSync(filePath)) fs39.unlinkSync(filePath);
|
|
9715
10155
|
}
|
|
9716
10156
|
function readState2() {
|
|
9717
10157
|
const filePath = stateFilePath();
|
|
9718
|
-
if (!
|
|
10158
|
+
if (!fs39.existsSync(filePath)) return { ...DEFAULT_STATE };
|
|
9719
10159
|
try {
|
|
9720
|
-
return JSON.parse(
|
|
10160
|
+
return JSON.parse(fs39.readFileSync(filePath, "utf-8"));
|
|
9721
10161
|
} catch {
|
|
9722
10162
|
return { ...DEFAULT_STATE };
|
|
9723
10163
|
}
|
|
9724
10164
|
}
|
|
9725
10165
|
function writeState2(state) {
|
|
9726
10166
|
ensureLearningDir();
|
|
9727
|
-
|
|
10167
|
+
fs39.writeFileSync(stateFilePath(), JSON.stringify(state, null, 2));
|
|
9728
10168
|
}
|
|
9729
10169
|
function resetState() {
|
|
9730
10170
|
writeState2({ ...DEFAULT_STATE });
|
|
@@ -9732,14 +10172,14 @@ function resetState() {
|
|
|
9732
10172
|
var LOCK_FILE2 = "finalize.lock";
|
|
9733
10173
|
var LOCK_STALE_MS = 5 * 60 * 1e3;
|
|
9734
10174
|
function lockFilePath() {
|
|
9735
|
-
return
|
|
10175
|
+
return path30.join(LEARNING_DIR, LOCK_FILE2);
|
|
9736
10176
|
}
|
|
9737
10177
|
function acquireFinalizeLock() {
|
|
9738
10178
|
ensureLearningDir();
|
|
9739
10179
|
const lockPath = lockFilePath();
|
|
9740
|
-
if (
|
|
10180
|
+
if (fs39.existsSync(lockPath)) {
|
|
9741
10181
|
try {
|
|
9742
|
-
const stat =
|
|
10182
|
+
const stat = fs39.statSync(lockPath);
|
|
9743
10183
|
if (Date.now() - stat.mtimeMs < LOCK_STALE_MS) {
|
|
9744
10184
|
return false;
|
|
9745
10185
|
}
|
|
@@ -9747,7 +10187,7 @@ function acquireFinalizeLock() {
|
|
|
9747
10187
|
}
|
|
9748
10188
|
}
|
|
9749
10189
|
try {
|
|
9750
|
-
|
|
10190
|
+
fs39.writeFileSync(lockPath, String(process.pid), { flag: "wx" });
|
|
9751
10191
|
return true;
|
|
9752
10192
|
} catch {
|
|
9753
10193
|
return false;
|
|
@@ -9756,7 +10196,7 @@ function acquireFinalizeLock() {
|
|
|
9756
10196
|
function releaseFinalizeLock() {
|
|
9757
10197
|
const lockPath = lockFilePath();
|
|
9758
10198
|
try {
|
|
9759
|
-
if (
|
|
10199
|
+
if (fs39.existsSync(lockPath)) fs39.unlinkSync(lockPath);
|
|
9760
10200
|
} catch {
|
|
9761
10201
|
}
|
|
9762
10202
|
}
|
|
@@ -9802,22 +10242,22 @@ function sanitizeSecrets(text) {
|
|
|
9802
10242
|
|
|
9803
10243
|
// src/lib/notifications.ts
|
|
9804
10244
|
init_constants();
|
|
9805
|
-
import
|
|
9806
|
-
import
|
|
10245
|
+
import fs40 from "fs";
|
|
10246
|
+
import path31 from "path";
|
|
9807
10247
|
import chalk22 from "chalk";
|
|
9808
|
-
var NOTIFICATION_FILE =
|
|
10248
|
+
var NOTIFICATION_FILE = path31.join(LEARNING_DIR, "last-finalize-summary.json");
|
|
9809
10249
|
function writeFinalizeSummary(summary) {
|
|
9810
10250
|
try {
|
|
9811
10251
|
ensureLearningDir();
|
|
9812
|
-
|
|
10252
|
+
fs40.writeFileSync(NOTIFICATION_FILE, JSON.stringify(summary, null, 2));
|
|
9813
10253
|
} catch {
|
|
9814
10254
|
}
|
|
9815
10255
|
}
|
|
9816
10256
|
function checkPendingNotifications() {
|
|
9817
10257
|
try {
|
|
9818
|
-
if (!
|
|
9819
|
-
const raw =
|
|
9820
|
-
|
|
10258
|
+
if (!fs40.existsSync(NOTIFICATION_FILE)) return;
|
|
10259
|
+
const raw = fs40.readFileSync(NOTIFICATION_FILE, "utf-8");
|
|
10260
|
+
fs40.unlinkSync(NOTIFICATION_FILE);
|
|
9821
10261
|
const summary = JSON.parse(raw);
|
|
9822
10262
|
if (!summary.newItemCount || summary.newItemCount === 0) return;
|
|
9823
10263
|
const wasteLabel = summary.wasteTokens > 0 ? ` (~${summary.wasteTokens.toLocaleString()} wasted tokens captured)` : "";
|
|
@@ -9833,7 +10273,7 @@ function checkPendingNotifications() {
|
|
|
9833
10273
|
console.log("");
|
|
9834
10274
|
} catch {
|
|
9835
10275
|
try {
|
|
9836
|
-
|
|
10276
|
+
fs40.unlinkSync(NOTIFICATION_FILE);
|
|
9837
10277
|
} catch {
|
|
9838
10278
|
}
|
|
9839
10279
|
}
|
|
@@ -9888,7 +10328,7 @@ function parseAnalysisResponse(raw) {
|
|
|
9888
10328
|
return { claudeMdLearnedSection: null, skills: null, explanations: ["LLM response contained invalid JSON."] };
|
|
9889
10329
|
}
|
|
9890
10330
|
}
|
|
9891
|
-
async function analyzeEvents(events, existingClaudeMd, existingLearnedSection, existingSkills) {
|
|
10331
|
+
async function analyzeEvents(events, existingClaudeMd, existingLearnedSection, existingSkills, personalLearnings) {
|
|
9892
10332
|
const fittedEvents = trimEventsToFit(events, MAX_PROMPT_TOKENS - 1e4);
|
|
9893
10333
|
const eventsText = formatEventsForPrompt(fittedEvents);
|
|
9894
10334
|
const contextParts = [];
|
|
@@ -9907,6 +10347,11 @@ ${existingLearnedSection}`);
|
|
|
9907
10347
|
contextParts.push(`## Existing Skills
|
|
9908
10348
|
|
|
9909
10349
|
${skillsSummary}`);
|
|
10350
|
+
}
|
|
10351
|
+
if (personalLearnings) {
|
|
10352
|
+
contextParts.push(`## Personal Learnings (developer-specific, do NOT duplicate these)
|
|
10353
|
+
|
|
10354
|
+
${personalLearnings}`);
|
|
9910
10355
|
}
|
|
9911
10356
|
contextParts.push(`## Task Segmentation & Attribution Instructions
|
|
9912
10357
|
|
|
@@ -9966,8 +10411,8 @@ init_config();
|
|
|
9966
10411
|
|
|
9967
10412
|
// src/learner/roi.ts
|
|
9968
10413
|
init_constants();
|
|
9969
|
-
import
|
|
9970
|
-
import
|
|
10414
|
+
import fs41 from "fs";
|
|
10415
|
+
import path32 from "path";
|
|
9971
10416
|
var DEFAULT_TOTALS = {
|
|
9972
10417
|
totalWasteTokens: 0,
|
|
9973
10418
|
totalWasteSeconds: 0,
|
|
@@ -9981,22 +10426,28 @@ var DEFAULT_TOTALS = {
|
|
|
9981
10426
|
lastSessionTimestamp: ""
|
|
9982
10427
|
};
|
|
9983
10428
|
function roiFilePath() {
|
|
9984
|
-
return
|
|
10429
|
+
return path32.join(LEARNING_DIR, LEARNING_ROI_FILE);
|
|
9985
10430
|
}
|
|
9986
10431
|
function readROIStats() {
|
|
9987
10432
|
const filePath = roiFilePath();
|
|
9988
|
-
if (!
|
|
10433
|
+
if (!fs41.existsSync(filePath)) {
|
|
9989
10434
|
return { learnings: [], sessions: [], totals: { ...DEFAULT_TOTALS } };
|
|
9990
10435
|
}
|
|
9991
10436
|
try {
|
|
9992
|
-
return JSON.parse(
|
|
10437
|
+
return JSON.parse(fs41.readFileSync(filePath, "utf-8"));
|
|
9993
10438
|
} catch {
|
|
10439
|
+
try {
|
|
10440
|
+
const corruptPath = filePath + ".corrupt";
|
|
10441
|
+
fs41.renameSync(filePath, corruptPath);
|
|
10442
|
+
console.error(`caliber: roi-stats.json was corrupt \u2014 renamed to ${corruptPath}`);
|
|
10443
|
+
} catch {
|
|
10444
|
+
}
|
|
9994
10445
|
return { learnings: [], sessions: [], totals: { ...DEFAULT_TOTALS } };
|
|
9995
10446
|
}
|
|
9996
10447
|
}
|
|
9997
10448
|
function writeROIStats(stats) {
|
|
9998
10449
|
ensureLearningDir();
|
|
9999
|
-
|
|
10450
|
+
fs41.writeFileSync(roiFilePath(), JSON.stringify(stats, null, 2));
|
|
10000
10451
|
}
|
|
10001
10452
|
function recalculateTotals(stats) {
|
|
10002
10453
|
const totals = stats.totals;
|
|
@@ -10016,8 +10467,24 @@ function recalculateTotals(stats) {
|
|
|
10016
10467
|
totals.totalFailuresWithoutLearnings += s.failureCount;
|
|
10017
10468
|
}
|
|
10018
10469
|
}
|
|
10019
|
-
totals.
|
|
10020
|
-
|
|
10470
|
+
if (totals.totalSessionsWithLearnings >= MIN_SESSIONS_FOR_COMPARISON && totals.totalSessionsWithoutLearnings >= MIN_SESSIONS_FOR_COMPARISON) {
|
|
10471
|
+
const rateWithout = totals.totalFailuresWithoutLearnings / totals.totalSessionsWithoutLearnings;
|
|
10472
|
+
const rateWith = totals.totalFailuresWithLearnings / totals.totalSessionsWithLearnings;
|
|
10473
|
+
if (rateWithout > 0 && rateWith < rateWithout) {
|
|
10474
|
+
const reduction = (rateWithout - rateWith) / rateWithout;
|
|
10475
|
+
const totalSessions = totals.totalSessionsWithLearnings + totals.totalSessionsWithoutLearnings;
|
|
10476
|
+
const avgWastePerSession = totalSessions > 0 ? totals.totalWasteTokens / totalSessions : 0;
|
|
10477
|
+
totals.estimatedSavingsTokens = Math.round(reduction * avgWastePerSession * totals.totalSessionsWithLearnings);
|
|
10478
|
+
const avgWasteSecondsPerSession = totalSessions > 0 ? totals.totalWasteSeconds / totalSessions : 0;
|
|
10479
|
+
totals.estimatedSavingsSeconds = Math.round(reduction * avgWasteSecondsPerSession * totals.totalSessionsWithLearnings);
|
|
10480
|
+
} else {
|
|
10481
|
+
totals.estimatedSavingsTokens = 0;
|
|
10482
|
+
totals.estimatedSavingsSeconds = 0;
|
|
10483
|
+
}
|
|
10484
|
+
} else {
|
|
10485
|
+
totals.estimatedSavingsTokens = 0;
|
|
10486
|
+
totals.estimatedSavingsSeconds = 0;
|
|
10487
|
+
}
|
|
10021
10488
|
if (stats.sessions.length > 0) {
|
|
10022
10489
|
totals.firstSessionTimestamp = stats.sessions[0].timestamp;
|
|
10023
10490
|
totals.lastSessionTimestamp = stats.sessions[stats.sessions.length - 1].timestamp;
|
|
@@ -10085,7 +10552,102 @@ function formatROISummary(stats) {
|
|
|
10085
10552
|
return lines.join("\n");
|
|
10086
10553
|
}
|
|
10087
10554
|
|
|
10555
|
+
// src/learner/attribution.ts
|
|
10556
|
+
init_config();
|
|
10557
|
+
function normalizeText(text) {
|
|
10558
|
+
return text.replace(/[^a-zA-Z0-9\s]/g, "").replace(/\s+/g, " ").toLowerCase().trim();
|
|
10559
|
+
}
|
|
10560
|
+
function preNormalizeLearning(summary) {
|
|
10561
|
+
const norm = normalizeText(summary);
|
|
10562
|
+
return { words: norm.split(" ").filter((w) => w.length > 3) };
|
|
10563
|
+
}
|
|
10564
|
+
function matchesNormalized(learning, normError) {
|
|
10565
|
+
if (learning.words.length === 0 || !normError) return false;
|
|
10566
|
+
const matchCount = learning.words.filter((w) => normError.includes(w)).length;
|
|
10567
|
+
return matchCount / learning.words.length >= 0.6;
|
|
10568
|
+
}
|
|
10569
|
+
function matchLearningsToFailures(learnings, failureEvents) {
|
|
10570
|
+
if (learnings.length === 0 || failureEvents.length === 0) {
|
|
10571
|
+
return { matchedIndices: [], unmatchedFailures: failureEvents.length };
|
|
10572
|
+
}
|
|
10573
|
+
const normalized = learnings.map((l) => preNormalizeLearning(l.summary));
|
|
10574
|
+
const matchedIndices = /* @__PURE__ */ new Set();
|
|
10575
|
+
let unmatchedFailures = 0;
|
|
10576
|
+
for (const event of failureEvents) {
|
|
10577
|
+
const errorText = typeof event.tool_response === "object" && "_truncated" in event.tool_response ? String(event.tool_response._truncated) : JSON.stringify(event.tool_response);
|
|
10578
|
+
const normError = normalizeText(errorText);
|
|
10579
|
+
let matched = false;
|
|
10580
|
+
for (let i = 0; i < learnings.length; i++) {
|
|
10581
|
+
if (matchesNormalized(normalized[i], normError)) {
|
|
10582
|
+
matchedIndices.add(i);
|
|
10583
|
+
matched = true;
|
|
10584
|
+
break;
|
|
10585
|
+
}
|
|
10586
|
+
}
|
|
10587
|
+
if (!matched) unmatchedFailures++;
|
|
10588
|
+
}
|
|
10589
|
+
return { matchedIndices: [...matchedIndices], unmatchedFailures };
|
|
10590
|
+
}
|
|
10591
|
+
async function semanticMatchFallback(learnings, failureEvents) {
|
|
10592
|
+
if (learnings.length === 0 || failureEvents.length === 0) {
|
|
10593
|
+
return { matchedIndices: [], unmatchedFailures: failureEvents.length };
|
|
10594
|
+
}
|
|
10595
|
+
const learningSummaries = learnings.map((l, i) => `${i}: ${l.summary}`).join("\n");
|
|
10596
|
+
const failureSummaries = failureEvents.slice(0, 10).map((e) => {
|
|
10597
|
+
const errorText = typeof e.tool_response === "object" && "_truncated" in e.tool_response ? String(e.tool_response._truncated).slice(0, 200) : JSON.stringify(e.tool_response).slice(0, 200);
|
|
10598
|
+
return `[${e.tool_name}] ${errorText}`;
|
|
10599
|
+
}).join("\n");
|
|
10600
|
+
const prompt = `Given these existing learnings (numbered):
|
|
10601
|
+
${learningSummaries}
|
|
10602
|
+
|
|
10603
|
+
And these failure events from a session:
|
|
10604
|
+
${failureSummaries}
|
|
10605
|
+
|
|
10606
|
+
Which learnings (by index number) are related to or could have prevented these failures?
|
|
10607
|
+
Return a JSON object: {"matchedIndices": [0, 2]} or {"matchedIndices": []} if none match.`;
|
|
10608
|
+
try {
|
|
10609
|
+
const fastModel = getFastModel();
|
|
10610
|
+
const raw = await llmCall({
|
|
10611
|
+
system: "You match failure patterns to existing learnings. Return only valid JSON.",
|
|
10612
|
+
prompt,
|
|
10613
|
+
maxTokens: 256,
|
|
10614
|
+
...fastModel ? { model: fastModel } : {}
|
|
10615
|
+
});
|
|
10616
|
+
const json = extractJson(raw);
|
|
10617
|
+
if (json) {
|
|
10618
|
+
const parsed = JSON.parse(json);
|
|
10619
|
+
const indices = (parsed.matchedIndices || []).filter((i) => typeof i === "number" && i >= 0 && i < learnings.length);
|
|
10620
|
+
return { matchedIndices: indices, unmatchedFailures: failureEvents.length - indices.length };
|
|
10621
|
+
}
|
|
10622
|
+
} catch {
|
|
10623
|
+
}
|
|
10624
|
+
return { matchedIndices: [], unmatchedFailures: failureEvents.length };
|
|
10625
|
+
}
|
|
10626
|
+
function updateActivations(stats, matchedIndices) {
|
|
10627
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10628
|
+
for (const idx of matchedIndices) {
|
|
10629
|
+
if (idx < stats.learnings.length) {
|
|
10630
|
+
stats.learnings[idx].activationCount = (stats.learnings[idx].activationCount ?? 0) + 1;
|
|
10631
|
+
stats.learnings[idx].lastActivationTimestamp = now;
|
|
10632
|
+
}
|
|
10633
|
+
}
|
|
10634
|
+
}
|
|
10635
|
+
var DEFAULT_MIN_SESSIONS = 10;
|
|
10636
|
+
function findStaleLearnings(stats, minSessions = DEFAULT_MIN_SESSIONS) {
|
|
10637
|
+
if (stats.sessions.length < minSessions) return [];
|
|
10638
|
+
return stats.learnings.filter((l) => {
|
|
10639
|
+
const activations = l.activationCount ?? 0;
|
|
10640
|
+
if (activations > 0) return false;
|
|
10641
|
+
const createdAt = new Date(l.timestamp).getTime();
|
|
10642
|
+
const sessionsAfterCreation = stats.sessions.filter(
|
|
10643
|
+
(s) => new Date(s.timestamp).getTime() > createdAt
|
|
10644
|
+
).length;
|
|
10645
|
+
return sessionsAfterCreation >= minSessions;
|
|
10646
|
+
});
|
|
10647
|
+
}
|
|
10648
|
+
|
|
10088
10649
|
// src/commands/learn.ts
|
|
10650
|
+
init_constants();
|
|
10089
10651
|
var MIN_EVENTS_FOR_ANALYSIS = 25;
|
|
10090
10652
|
var MIN_EVENTS_AUTO = 10;
|
|
10091
10653
|
var AUTO_SETTLE_MS = 200;
|
|
@@ -10169,25 +10731,34 @@ async function learnFinalizeCommand(options) {
|
|
|
10169
10731
|
resetState();
|
|
10170
10732
|
return;
|
|
10171
10733
|
}
|
|
10172
|
-
const
|
|
10734
|
+
const allEvents = readAllEvents();
|
|
10173
10735
|
const threshold = isAuto ? MIN_EVENTS_AUTO : MIN_EVENTS_FOR_ANALYSIS;
|
|
10174
|
-
if (
|
|
10175
|
-
if (!isAuto) console.log(chalk23.dim(`caliber: ${
|
|
10736
|
+
if (allEvents.length < threshold) {
|
|
10737
|
+
if (!isAuto) console.log(chalk23.dim(`caliber: ${allEvents.length}/${threshold} events recorded \u2014 need more before analysis`));
|
|
10176
10738
|
return;
|
|
10177
10739
|
}
|
|
10178
10740
|
await validateModel({ fast: true });
|
|
10179
10741
|
migrateInlineLearnings();
|
|
10742
|
+
const state = readState2();
|
|
10743
|
+
const analysisOffset = isIncremental ? state.lastAnalysisEventCount || 0 : 0;
|
|
10744
|
+
const events = analysisOffset > 0 ? allEvents.slice(analysisOffset) : allEvents;
|
|
10745
|
+
if (events.length < threshold) {
|
|
10746
|
+
if (!isAuto) console.log(chalk23.dim(`caliber: ${events.length}/${threshold} new events since last analysis \u2014 need more`));
|
|
10747
|
+
return;
|
|
10748
|
+
}
|
|
10180
10749
|
const existingConfigs = readExistingConfigs(process.cwd());
|
|
10181
10750
|
const existingLearnedSection = readLearnedSection();
|
|
10751
|
+
const existingPersonalLearnings = readPersonalLearnings();
|
|
10182
10752
|
const existingSkills = existingConfigs.claudeSkills || [];
|
|
10183
10753
|
const response = await analyzeEvents(
|
|
10184
10754
|
events,
|
|
10185
10755
|
existingConfigs.claudeMd || "",
|
|
10186
10756
|
existingLearnedSection,
|
|
10187
|
-
existingSkills
|
|
10757
|
+
existingSkills,
|
|
10758
|
+
existingPersonalLearnings
|
|
10188
10759
|
);
|
|
10189
10760
|
analyzed = true;
|
|
10190
|
-
const waste = calculateSessionWaste(
|
|
10761
|
+
const waste = calculateSessionWaste(allEvents);
|
|
10191
10762
|
const existingLearnedItems = existingLearnedSection ? existingLearnedSection.split("\n").filter((l) => l.startsWith("- ")).length : 0;
|
|
10192
10763
|
const hadLearnings = existingLearnedItems > 0;
|
|
10193
10764
|
let newLearningsProduced = 0;
|
|
@@ -10215,7 +10786,8 @@ async function learnFinalizeCommand(options) {
|
|
|
10215
10786
|
}
|
|
10216
10787
|
const wastePerLearning = Math.round(waste.totalWasteTokens / result.newItemCount);
|
|
10217
10788
|
const TYPE_RE = /^\*\*\[([^\]]+)\]\*\*/;
|
|
10218
|
-
const
|
|
10789
|
+
const explanations = response.explanations || [];
|
|
10790
|
+
const learningEntries = result.newItems.map((item, idx) => {
|
|
10219
10791
|
const clean = item.replace(/^- /, "");
|
|
10220
10792
|
const typeMatch = clean.match(TYPE_RE);
|
|
10221
10793
|
return {
|
|
@@ -10223,7 +10795,8 @@ async function learnFinalizeCommand(options) {
|
|
|
10223
10795
|
observationType: typeMatch ? typeMatch[1] : "unknown",
|
|
10224
10796
|
summary: clean.replace(TYPE_RE, "").trim().slice(0, 80),
|
|
10225
10797
|
wasteTokens: wastePerLearning,
|
|
10226
|
-
sourceEventCount: events.length
|
|
10798
|
+
sourceEventCount: events.length,
|
|
10799
|
+
explanation: explanations[idx] || null
|
|
10227
10800
|
};
|
|
10228
10801
|
});
|
|
10229
10802
|
for (const entry of learningEntries) {
|
|
@@ -10247,8 +10820,8 @@ async function learnFinalizeCommand(options) {
|
|
|
10247
10820
|
}
|
|
10248
10821
|
const sessionSummary = {
|
|
10249
10822
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10250
|
-
sessionId:
|
|
10251
|
-
eventCount:
|
|
10823
|
+
sessionId: state.sessionId || "unknown",
|
|
10824
|
+
eventCount: allEvents.length,
|
|
10252
10825
|
failureCount: waste.failureCount,
|
|
10253
10826
|
promptCount: waste.promptCount,
|
|
10254
10827
|
wasteSeconds: Math.round(waste.totalWasteSeconds),
|
|
@@ -10261,8 +10834,21 @@ async function learnFinalizeCommand(options) {
|
|
|
10261
10834
|
taskFailureCount: tasks.length > 0 ? taskFailureCount : void 0
|
|
10262
10835
|
};
|
|
10263
10836
|
const roiStats = recordSession(sessionSummary, roiLearningEntries);
|
|
10837
|
+
if (roiStats.learnings.length > 0 && waste.failureCount > 0) {
|
|
10838
|
+
const failureEvents = allEvents.filter(
|
|
10839
|
+
(e) => e.hook_event_name === "PostToolUseFailure"
|
|
10840
|
+
);
|
|
10841
|
+
let attribution = matchLearningsToFailures(roiStats.learnings, failureEvents);
|
|
10842
|
+
if (attribution.matchedIndices.length === 0 && failureEvents.length > 0) {
|
|
10843
|
+
attribution = await semanticMatchFallback(roiStats.learnings, failureEvents);
|
|
10844
|
+
}
|
|
10845
|
+
if (attribution.matchedIndices.length > 0) {
|
|
10846
|
+
updateActivations(roiStats, attribution.matchedIndices);
|
|
10847
|
+
writeROIStats(roiStats);
|
|
10848
|
+
}
|
|
10849
|
+
}
|
|
10264
10850
|
trackLearnSessionAnalyzed({
|
|
10265
|
-
eventCount:
|
|
10851
|
+
eventCount: allEvents.length,
|
|
10266
10852
|
failureCount: waste.failureCount,
|
|
10267
10853
|
correctionCount: waste.promptCount,
|
|
10268
10854
|
hadLearningsAvailable: hadLearnings,
|
|
@@ -10285,6 +10871,12 @@ async function learnFinalizeCommand(options) {
|
|
|
10285
10871
|
estimatedSavingsSeconds: t.estimatedSavingsSeconds,
|
|
10286
10872
|
learningCount: roiStats.learnings.length
|
|
10287
10873
|
});
|
|
10874
|
+
if (!isIncremental) {
|
|
10875
|
+
const staleLearnings = findStaleLearnings(roiStats);
|
|
10876
|
+
if (staleLearnings.length > 0 && !isAuto) {
|
|
10877
|
+
console.log(chalk23.yellow(`caliber: ${staleLearnings.length} learning${staleLearnings.length === 1 ? "" : "s"} never activated \u2014 run \`caliber learn list --verbose\` to review`));
|
|
10878
|
+
}
|
|
10879
|
+
}
|
|
10288
10880
|
if (!isAuto && t.estimatedSavingsTokens > 0) {
|
|
10289
10881
|
const totalLearnings = existingLearnedItems + newLearningsProduced;
|
|
10290
10882
|
console.log(chalk23.dim(`caliber: ${totalLearnings} learnings active \u2014 est. ~${t.estimatedSavingsTokens.toLocaleString()} tokens saved across ${t.totalSessionsWithLearnings} sessions`));
|
|
@@ -10310,7 +10902,7 @@ async function learnFinalizeCommand(options) {
|
|
|
10310
10902
|
}
|
|
10311
10903
|
async function learnInstallCommand() {
|
|
10312
10904
|
let anyInstalled = false;
|
|
10313
|
-
if (
|
|
10905
|
+
if (fs42.existsSync(".claude")) {
|
|
10314
10906
|
const r = installLearningHooks();
|
|
10315
10907
|
if (r.installed) {
|
|
10316
10908
|
console.log(chalk23.green("\u2713") + " Claude Code learning hooks installed");
|
|
@@ -10319,7 +10911,7 @@ async function learnInstallCommand() {
|
|
|
10319
10911
|
console.log(chalk23.dim(" Claude Code hooks already installed"));
|
|
10320
10912
|
}
|
|
10321
10913
|
}
|
|
10322
|
-
if (
|
|
10914
|
+
if (fs42.existsSync(".cursor")) {
|
|
10323
10915
|
const r = installCursorLearningHooks();
|
|
10324
10916
|
if (r.installed) {
|
|
10325
10917
|
console.log(chalk23.green("\u2713") + " Cursor learning hooks installed");
|
|
@@ -10328,7 +10920,7 @@ async function learnInstallCommand() {
|
|
|
10328
10920
|
console.log(chalk23.dim(" Cursor hooks already installed"));
|
|
10329
10921
|
}
|
|
10330
10922
|
}
|
|
10331
|
-
if (!
|
|
10923
|
+
if (!fs42.existsSync(".claude") && !fs42.existsSync(".cursor")) {
|
|
10332
10924
|
console.log(chalk23.yellow("No .claude/ or .cursor/ directory found."));
|
|
10333
10925
|
console.log(chalk23.dim(" Run `caliber init` first, or create the directory manually."));
|
|
10334
10926
|
return;
|
|
@@ -10398,16 +10990,116 @@ Learned items in CALIBER_LEARNINGS.md: ${chalk23.cyan(String(lineCount))}`);
|
|
|
10398
10990
|
}
|
|
10399
10991
|
}
|
|
10400
10992
|
}
|
|
10993
|
+
function getAllLearnings() {
|
|
10994
|
+
const items = [];
|
|
10995
|
+
let idx = 0;
|
|
10996
|
+
const projectSection = readLearnedSection();
|
|
10997
|
+
if (projectSection) {
|
|
10998
|
+
for (const line of projectSection.split("\n").filter((l) => l.startsWith("- "))) {
|
|
10999
|
+
items.push({ text: line, source: "project", index: idx++ });
|
|
11000
|
+
}
|
|
11001
|
+
}
|
|
11002
|
+
const personalSection = readPersonalLearnings();
|
|
11003
|
+
if (personalSection) {
|
|
11004
|
+
for (const line of personalSection.split("\n").filter((l) => l.startsWith("- "))) {
|
|
11005
|
+
items.push({ text: line, source: "personal", index: idx++ });
|
|
11006
|
+
}
|
|
11007
|
+
}
|
|
11008
|
+
return items;
|
|
11009
|
+
}
|
|
11010
|
+
async function learnListCommand(options) {
|
|
11011
|
+
const items = getAllLearnings();
|
|
11012
|
+
if (items.length === 0) {
|
|
11013
|
+
console.log(chalk23.dim("No learnings yet. Run `caliber learn install` to start."));
|
|
11014
|
+
return;
|
|
11015
|
+
}
|
|
11016
|
+
const roiStats = options?.verbose ? readROIStats() : null;
|
|
11017
|
+
console.log(chalk23.bold(`
|
|
11018
|
+
Learnings (${items.length})
|
|
11019
|
+
`));
|
|
11020
|
+
for (const item of items) {
|
|
11021
|
+
const tag = item.source === "personal" ? chalk23.magenta("[personal]") : chalk23.blue("[project]");
|
|
11022
|
+
const display = item.text.replace(/^- /, "").slice(0, 100);
|
|
11023
|
+
console.log(` ${chalk23.dim(String(item.index + 1).padStart(2, " "))}. ${tag} ${display}`);
|
|
11024
|
+
if (options?.verbose && roiStats) {
|
|
11025
|
+
const match = roiStats.learnings.find((l) => display.includes(l.summary.slice(0, 40)));
|
|
11026
|
+
if (match) {
|
|
11027
|
+
const activations = match.activationCount ?? 0;
|
|
11028
|
+
const stale = activations === 0 && roiStats.sessions.length >= 10;
|
|
11029
|
+
const activationLabel = stale ? chalk23.yellow(`${activations} activations [stale]`) : chalk23.dim(`${activations} activation${activations === 1 ? "" : "s"}`);
|
|
11030
|
+
console.log(` ${activationLabel}`);
|
|
11031
|
+
if (match.explanation) {
|
|
11032
|
+
console.log(` ${chalk23.dim("Why: " + match.explanation.slice(0, 80))}`);
|
|
11033
|
+
}
|
|
11034
|
+
}
|
|
11035
|
+
}
|
|
11036
|
+
}
|
|
11037
|
+
console.log("");
|
|
11038
|
+
}
|
|
11039
|
+
async function learnDeleteCommand(indexStr) {
|
|
11040
|
+
const index = parseInt(indexStr, 10);
|
|
11041
|
+
if (isNaN(index) || index < 1) {
|
|
11042
|
+
console.log(chalk23.red(`Invalid index: "${indexStr}". Use a number from \`caliber learn list\`.`));
|
|
11043
|
+
return;
|
|
11044
|
+
}
|
|
11045
|
+
const items = getAllLearnings();
|
|
11046
|
+
const targetIdx = index - 1;
|
|
11047
|
+
if (targetIdx >= items.length) {
|
|
11048
|
+
console.log(chalk23.red(`Index ${index} is out of range. You have ${items.length} learnings.`));
|
|
11049
|
+
return;
|
|
11050
|
+
}
|
|
11051
|
+
const item = items[targetIdx];
|
|
11052
|
+
const filePath = item.source === "personal" ? PERSONAL_LEARNINGS_FILE : "CALIBER_LEARNINGS.md";
|
|
11053
|
+
if (!fs42.existsSync(filePath)) {
|
|
11054
|
+
console.log(chalk23.red("Learnings file not found."));
|
|
11055
|
+
return;
|
|
11056
|
+
}
|
|
11057
|
+
const content = fs42.readFileSync(filePath, "utf-8");
|
|
11058
|
+
const lines = content.split("\n");
|
|
11059
|
+
const bulletsOfSource = items.filter((i) => i.source === item.source);
|
|
11060
|
+
const posInFile = bulletsOfSource.indexOf(item);
|
|
11061
|
+
let bulletsSeen = 0;
|
|
11062
|
+
let lineToRemove = -1;
|
|
11063
|
+
for (let i = 0; i < lines.length; i++) {
|
|
11064
|
+
if (lines[i].startsWith("- ")) {
|
|
11065
|
+
if (bulletsSeen === posInFile) {
|
|
11066
|
+
lineToRemove = i;
|
|
11067
|
+
break;
|
|
11068
|
+
}
|
|
11069
|
+
bulletsSeen++;
|
|
11070
|
+
}
|
|
11071
|
+
}
|
|
11072
|
+
if (lineToRemove === -1) {
|
|
11073
|
+
console.log(chalk23.red("Could not locate learning in file."));
|
|
11074
|
+
return;
|
|
11075
|
+
}
|
|
11076
|
+
const bulletToRemove = lines[lineToRemove];
|
|
11077
|
+
const newLines = lines.filter((_, i) => i !== lineToRemove);
|
|
11078
|
+
fs42.writeFileSync(filePath, newLines.join("\n"));
|
|
11079
|
+
if (item.source === "personal") {
|
|
11080
|
+
fs42.chmodSync(filePath, 384);
|
|
11081
|
+
}
|
|
11082
|
+
const roiStats = readROIStats();
|
|
11083
|
+
const cleanText = bulletToRemove.replace(/^- /, "").replace(/^\*\*\[[^\]]+\]\*\*\s*/, "").trim();
|
|
11084
|
+
const roiIdx = roiStats.learnings.findIndex((l) => cleanText.includes(l.summary.slice(0, 30)));
|
|
11085
|
+
if (roiIdx !== -1) {
|
|
11086
|
+
roiStats.learnings.splice(roiIdx, 1);
|
|
11087
|
+
writeROIStats(roiStats);
|
|
11088
|
+
}
|
|
11089
|
+
console.log(chalk23.green("\u2713") + ` Removed: ${bulletToRemove.replace(/^- /, "").slice(0, 80)}`);
|
|
11090
|
+
}
|
|
10401
11091
|
|
|
10402
11092
|
// src/commands/insights.ts
|
|
10403
11093
|
import chalk24 from "chalk";
|
|
11094
|
+
init_constants();
|
|
10404
11095
|
var MIN_SESSIONS_FULL = 20;
|
|
10405
11096
|
function buildInsightsData(stats) {
|
|
10406
11097
|
const t = stats.totals;
|
|
10407
11098
|
const totalSessions = t.totalSessionsWithLearnings + t.totalSessionsWithoutLearnings;
|
|
10408
11099
|
const failureRateWith = t.totalSessionsWithLearnings > 0 ? t.totalFailuresWithLearnings / t.totalSessionsWithLearnings : null;
|
|
10409
11100
|
const failureRateWithout = t.totalSessionsWithoutLearnings > 0 ? t.totalFailuresWithoutLearnings / t.totalSessionsWithoutLearnings : null;
|
|
10410
|
-
const
|
|
11101
|
+
const hasSufficientCohorts = t.totalSessionsWithLearnings >= MIN_SESSIONS_FOR_COMPARISON && t.totalSessionsWithoutLearnings >= MIN_SESSIONS_FOR_COMPARISON;
|
|
11102
|
+
const failureRateImprovement = hasSufficientCohorts && failureRateWith !== null && failureRateWithout !== null && failureRateWithout > 0 ? Math.round((1 - failureRateWith / failureRateWithout) * 100) : null;
|
|
10411
11103
|
let taskCount = 0;
|
|
10412
11104
|
let taskSuccessCount = 0;
|
|
10413
11105
|
let taskCorrectionCount = 0;
|
|
@@ -10462,6 +11154,8 @@ function displayEarlyData(data, score) {
|
|
|
10462
11154
|
}
|
|
10463
11155
|
if (data.failureRateImprovement !== null && data.failureRateImprovement > 0) {
|
|
10464
11156
|
console.log(` Failure rate trend: ${chalk24.green(`${data.failureRateImprovement}% fewer`)} failures with learnings ${chalk24.dim("(early signal)")}`);
|
|
11157
|
+
} else if (data.totalSessions > 0 && data.failureRateImprovement === null) {
|
|
11158
|
+
console.log(` Failure rate trend: ${chalk24.dim("collecting data (need 3+ sessions in each group)")}`);
|
|
10465
11159
|
}
|
|
10466
11160
|
if (data.taskSuccessRate !== null) {
|
|
10467
11161
|
console.log(` Task success rate: ${chalk24.cyan(`${data.taskSuccessRate}%`)} ${chalk24.dim(`(${data.taskCount} tasks)`)}`);
|
|
@@ -10486,6 +11180,8 @@ function displayFullInsights(data, score) {
|
|
|
10486
11180
|
console.log(` Failure rate: ${chalk24.red(data.failureRateWithout.toFixed(1))}/session ${chalk24.dim("\u2192")} ${chalk24.green(data.failureRateWith.toFixed(1))}/session with learnings`);
|
|
10487
11181
|
if (data.failureRateImprovement !== null && data.failureRateImprovement > 0) {
|
|
10488
11182
|
console.log(` Improvement: ${chalk24.green(`${data.failureRateImprovement}%`)} fewer failures`);
|
|
11183
|
+
} else if (data.failureRateImprovement === null) {
|
|
11184
|
+
console.log(` Improvement: ${chalk24.dim("collecting data (need 3+ sessions in each group)")}`);
|
|
10489
11185
|
}
|
|
10490
11186
|
}
|
|
10491
11187
|
if (data.totalWasteTokens > 0 || data.estimatedSavingsTokens > 0) {
|
|
@@ -10527,10 +11223,173 @@ async function insightsCommand(options) {
|
|
|
10527
11223
|
}
|
|
10528
11224
|
}
|
|
10529
11225
|
|
|
11226
|
+
// src/commands/sources.ts
|
|
11227
|
+
import fs43 from "fs";
|
|
11228
|
+
import path33 from "path";
|
|
11229
|
+
import chalk25 from "chalk";
|
|
11230
|
+
async function sourcesListCommand() {
|
|
11231
|
+
const dir = process.cwd();
|
|
11232
|
+
const configSources = loadSourcesConfig(dir);
|
|
11233
|
+
const workspaces = getDetectedWorkspaces(dir);
|
|
11234
|
+
if (configSources.length === 0 && workspaces.length === 0) {
|
|
11235
|
+
console.log(chalk25.dim("\n No sources configured.\n"));
|
|
11236
|
+
console.log(chalk25.dim(" Add a source: ") + chalk25.hex("#83D1EB")("caliber sources add <path>"));
|
|
11237
|
+
console.log(chalk25.dim(" Or add to .caliber/sources.json manually.\n"));
|
|
11238
|
+
return;
|
|
11239
|
+
}
|
|
11240
|
+
console.log(chalk25.bold("\n External Sources\n"));
|
|
11241
|
+
if (configSources.length > 0) {
|
|
11242
|
+
for (const source of configSources) {
|
|
11243
|
+
const sourcePath = source.path || source.url || "";
|
|
11244
|
+
const exists = source.path ? fs43.existsSync(path33.resolve(dir, source.path)) : false;
|
|
11245
|
+
const status = exists ? chalk25.green("reachable") : chalk25.red("not found");
|
|
11246
|
+
const hasSummary = source.path && fs43.existsSync(path33.join(path33.resolve(dir, source.path), ".caliber", "summary.json"));
|
|
11247
|
+
console.log(` ${chalk25.bold(source.role || source.type)} ${chalk25.dim(sourcePath)}`);
|
|
11248
|
+
console.log(` Type: ${source.type} Status: ${status}${hasSummary ? " " + chalk25.cyan("has summary.json") : ""}`);
|
|
11249
|
+
if (source.description) console.log(` ${chalk25.dim(source.description)}`);
|
|
11250
|
+
console.log("");
|
|
11251
|
+
}
|
|
11252
|
+
}
|
|
11253
|
+
if (workspaces.length > 0) {
|
|
11254
|
+
console.log(chalk25.dim(" Auto-detected workspaces:"));
|
|
11255
|
+
for (const ws of workspaces) {
|
|
11256
|
+
const exists = fs43.existsSync(path33.resolve(dir, ws));
|
|
11257
|
+
console.log(` ${exists ? chalk25.green("\u25CF") : chalk25.red("\u25CF")} ${ws}`);
|
|
11258
|
+
}
|
|
11259
|
+
console.log("");
|
|
11260
|
+
}
|
|
11261
|
+
}
|
|
11262
|
+
async function sourcesAddCommand(sourcePath) {
|
|
11263
|
+
const dir = process.cwd();
|
|
11264
|
+
const absPath = path33.resolve(dir, sourcePath);
|
|
11265
|
+
if (!fs43.existsSync(absPath)) {
|
|
11266
|
+
console.log(chalk25.red(`
|
|
11267
|
+
Path not found: ${sourcePath}
|
|
11268
|
+
`));
|
|
11269
|
+
throw new Error("__exit__");
|
|
11270
|
+
}
|
|
11271
|
+
const type = detectSourceType(absPath);
|
|
11272
|
+
if (isInsideDir(absPath, dir)) {
|
|
11273
|
+
console.log(chalk25.red(`
|
|
11274
|
+
Cannot add a path inside the current project as a source.
|
|
11275
|
+
`));
|
|
11276
|
+
throw new Error("__exit__");
|
|
11277
|
+
}
|
|
11278
|
+
const existing = loadSourcesConfig(dir);
|
|
11279
|
+
const alreadyConfigured = existing.some(
|
|
11280
|
+
(s) => s.path && path33.resolve(dir, s.path) === absPath
|
|
11281
|
+
);
|
|
11282
|
+
if (alreadyConfigured) {
|
|
11283
|
+
console.log(chalk25.yellow(`
|
|
11284
|
+
Already configured: ${sourcePath}
|
|
11285
|
+
`));
|
|
11286
|
+
return;
|
|
11287
|
+
}
|
|
11288
|
+
const defaultRole = type === "repo" ? "related-repo" : "reference-doc";
|
|
11289
|
+
const roleInput = await promptInput(`Role (e.g., shared-library, deployment) [${defaultRole}]:`);
|
|
11290
|
+
const role = roleInput || defaultRole;
|
|
11291
|
+
const description = await promptInput("Brief description (optional):");
|
|
11292
|
+
const newSource = {
|
|
11293
|
+
type,
|
|
11294
|
+
path: sourcePath,
|
|
11295
|
+
role,
|
|
11296
|
+
...description ? { description } : {}
|
|
11297
|
+
};
|
|
11298
|
+
existing.push(newSource);
|
|
11299
|
+
writeSourcesConfig(dir, existing);
|
|
11300
|
+
console.log(chalk25.green(`
|
|
11301
|
+
\u2713 Added ${sourcePath} as ${type} source (${role})
|
|
11302
|
+
`));
|
|
11303
|
+
}
|
|
11304
|
+
async function sourcesRemoveCommand(name) {
|
|
11305
|
+
const dir = process.cwd();
|
|
11306
|
+
const existing = loadSourcesConfig(dir);
|
|
11307
|
+
const idx = existing.findIndex(
|
|
11308
|
+
(s) => s.path?.includes(name) || s.role === name
|
|
11309
|
+
);
|
|
11310
|
+
if (idx === -1) {
|
|
11311
|
+
console.log(chalk25.red(`
|
|
11312
|
+
Source not found: ${name}
|
|
11313
|
+
`));
|
|
11314
|
+
console.log(chalk25.dim(" Available sources:"));
|
|
11315
|
+
for (const s of existing) {
|
|
11316
|
+
console.log(chalk25.dim(` ${s.path || s.url} (${s.role || s.type})`));
|
|
11317
|
+
}
|
|
11318
|
+
throw new Error("__exit__");
|
|
11319
|
+
}
|
|
11320
|
+
const removed = existing.splice(idx, 1)[0];
|
|
11321
|
+
writeSourcesConfig(dir, existing);
|
|
11322
|
+
console.log(chalk25.green(`
|
|
11323
|
+
\u2713 Removed ${removed.path || removed.url} (${removed.role || removed.type})
|
|
11324
|
+
`));
|
|
11325
|
+
}
|
|
11326
|
+
|
|
11327
|
+
// src/commands/publish.ts
|
|
11328
|
+
import fs44 from "fs";
|
|
11329
|
+
import path34 from "path";
|
|
11330
|
+
import chalk26 from "chalk";
|
|
11331
|
+
import ora7 from "ora";
|
|
11332
|
+
init_config();
|
|
11333
|
+
async function publishCommand() {
|
|
11334
|
+
const dir = process.cwd();
|
|
11335
|
+
const config = loadConfig();
|
|
11336
|
+
if (!config) {
|
|
11337
|
+
console.log(chalk26.red("No LLM provider configured. Run ") + chalk26.hex("#83D1EB")("caliber config") + chalk26.red(" first."));
|
|
11338
|
+
throw new Error("__exit__");
|
|
11339
|
+
}
|
|
11340
|
+
const spinner = ora7("Generating project summary...").start();
|
|
11341
|
+
try {
|
|
11342
|
+
const fingerprint = await collectFingerprint(dir);
|
|
11343
|
+
const claudeMd = readFileOrNull(path34.join(dir, "CLAUDE.md"));
|
|
11344
|
+
const topLevelDirs = fingerprint.fileTree.filter((f) => f.endsWith("/") && !f.includes("/")).map((f) => f.replace(/\/$/, ""));
|
|
11345
|
+
const summary = {
|
|
11346
|
+
name: fingerprint.packageName || path34.basename(dir),
|
|
11347
|
+
version: "1.0.0",
|
|
11348
|
+
description: fingerprint.description || "",
|
|
11349
|
+
languages: fingerprint.languages,
|
|
11350
|
+
frameworks: fingerprint.frameworks,
|
|
11351
|
+
tools: fingerprint.tools,
|
|
11352
|
+
topLevelDirs
|
|
11353
|
+
};
|
|
11354
|
+
if (claudeMd) {
|
|
11355
|
+
summary.conventions = claudeMd.slice(0, 2e3);
|
|
11356
|
+
}
|
|
11357
|
+
try {
|
|
11358
|
+
const pkgContent = readFileOrNull(path34.join(dir, "package.json"));
|
|
11359
|
+
if (pkgContent) {
|
|
11360
|
+
const pkg3 = JSON.parse(pkgContent);
|
|
11361
|
+
if (pkg3.scripts) {
|
|
11362
|
+
const commands = {};
|
|
11363
|
+
for (const key of ["test", "build", "dev", "lint", "start"]) {
|
|
11364
|
+
if (pkg3.scripts[key]) commands[key] = `npm run ${key}`;
|
|
11365
|
+
}
|
|
11366
|
+
if (Object.keys(commands).length > 0) summary.commands = commands;
|
|
11367
|
+
}
|
|
11368
|
+
}
|
|
11369
|
+
} catch {
|
|
11370
|
+
}
|
|
11371
|
+
const outputDir = path34.join(dir, ".caliber");
|
|
11372
|
+
if (!fs44.existsSync(outputDir)) {
|
|
11373
|
+
fs44.mkdirSync(outputDir, { recursive: true });
|
|
11374
|
+
}
|
|
11375
|
+
const outputPath = path34.join(outputDir, "summary.json");
|
|
11376
|
+
fs44.writeFileSync(outputPath, JSON.stringify(summary, null, 2) + "\n", "utf-8");
|
|
11377
|
+
spinner.succeed("Project summary published");
|
|
11378
|
+
console.log(` ${chalk26.green("\u2713")} ${path34.relative(dir, outputPath)}`);
|
|
11379
|
+
console.log(chalk26.dim("\n Other projects can now reference this repo as a source."));
|
|
11380
|
+
console.log(chalk26.dim(" When they run `caliber init`, they'll read this summary automatically.\n"));
|
|
11381
|
+
} catch (err) {
|
|
11382
|
+
spinner.fail("Failed to generate summary");
|
|
11383
|
+
if (err instanceof Error && err.message === "__exit__") throw err;
|
|
11384
|
+
console.error(chalk26.red(err instanceof Error ? err.message : "Unknown error"));
|
|
11385
|
+
throw new Error("__exit__");
|
|
11386
|
+
}
|
|
11387
|
+
}
|
|
11388
|
+
|
|
10530
11389
|
// src/cli.ts
|
|
10531
|
-
var __dirname =
|
|
11390
|
+
var __dirname = path35.dirname(fileURLToPath(import.meta.url));
|
|
10532
11391
|
var pkg = JSON.parse(
|
|
10533
|
-
|
|
11392
|
+
fs45.readFileSync(path35.resolve(__dirname, "..", "package.json"), "utf-8")
|
|
10534
11393
|
);
|
|
10535
11394
|
var program = new Command();
|
|
10536
11395
|
var displayVersion = process.env.CALIBER_LOCAL ? `${pkg.version}-local` : pkg.version;
|
|
@@ -10582,43 +11441,50 @@ program.hook("preAction", (thisCommand) => {
|
|
|
10582
11441
|
});
|
|
10583
11442
|
function parseAgentOption(value) {
|
|
10584
11443
|
if (value === "both") return ["claude", "cursor"];
|
|
10585
|
-
if (value === "all") return ["claude", "cursor", "codex"];
|
|
10586
|
-
const valid = ["claude", "cursor", "codex"];
|
|
11444
|
+
if (value === "all") return ["claude", "cursor", "codex", "github-copilot"];
|
|
11445
|
+
const valid = ["claude", "cursor", "codex", "github-copilot"];
|
|
10587
11446
|
const agents = [...new Set(value.split(",").map((s) => s.trim().toLowerCase()).filter((a) => valid.includes(a)))];
|
|
10588
11447
|
if (agents.length === 0) {
|
|
10589
|
-
console.error(`Invalid agent "${value}". Choose from: claude, cursor, codex (comma-separated for multiple)`);
|
|
11448
|
+
console.error(`Invalid agent "${value}". Choose from: claude, cursor, codex, github-copilot (comma-separated for multiple)`);
|
|
10590
11449
|
process.exit(1);
|
|
10591
11450
|
}
|
|
10592
11451
|
return agents;
|
|
10593
11452
|
}
|
|
10594
|
-
program.command("init").description("Initialize your project for AI-assisted development").option("--agent <type>", "Target agents (comma-separated): claude, cursor, codex", parseAgentOption).option("--dry-run", "Preview changes without writing files").option("--force", "Overwrite existing setup without prompting").option("--debug-report", void 0, false).option("--show-tokens", "Show token usage summary at the end").option("--auto-approve", "Run without interactive prompts (auto-accept all)").option("--verbose", "Show detailed logs of each step").action(tracked("init", initCommand));
|
|
11453
|
+
program.command("init").description("Initialize your project for AI-assisted development").option("--agent <type>", "Target agents (comma-separated): claude, cursor, codex, github-copilot", parseAgentOption).option("--source <paths...>", "Related source paths to include as context").option("--dry-run", "Preview changes without writing files").option("--force", "Overwrite existing setup without prompting").option("--debug-report", void 0, false).option("--show-tokens", "Show token usage summary at the end").option("--auto-approve", "Run without interactive prompts (auto-accept all)").option("--verbose", "Show detailed logs of each step").action(tracked("init", initCommand));
|
|
10595
11454
|
program.command("undo").description("Revert all config changes made by Caliber").action(tracked("undo", undoCommand));
|
|
10596
11455
|
program.command("status").description("Show current Caliber setup status").option("--json", "Output as JSON").action(tracked("status", statusCommand));
|
|
10597
11456
|
program.command("regenerate").alias("regen").alias("re").description("Re-analyze project and regenerate setup").option("--dry-run", "Preview changes without writing files").action(tracked("regenerate", regenerateCommand));
|
|
10598
11457
|
program.command("config").description("Configure LLM provider, API key, and model").action(tracked("config", configCommand));
|
|
10599
11458
|
program.command("skills").description("Discover and install community skills for your project").action(tracked("skills", recommendCommand));
|
|
10600
|
-
program.command("score").description("Score your current agent config setup (deterministic, no network)").option("--json", "Output as JSON").option("--quiet", "One-line output for scripts/hooks").option("--agent <type>", "Target agents (comma-separated): claude, cursor, codex", parseAgentOption).option("--compare <ref>", "Compare score against a git ref (branch, tag, or SHA)").action(tracked("score", scoreCommand));
|
|
11459
|
+
program.command("score").description("Score your current agent config setup (deterministic, no network)").option("--json", "Output as JSON").option("--quiet", "One-line output for scripts/hooks").option("--agent <type>", "Target agents (comma-separated): claude, cursor, codex, github-copilot", parseAgentOption).option("--compare <ref>", "Compare score against a git ref (branch, tag, or SHA)").action(tracked("score", scoreCommand));
|
|
10601
11460
|
program.command("refresh").description("Update docs based on recent code changes").option("--quiet", "Suppress output (for use in hooks)").option("--dry-run", "Preview changes without writing files").action(tracked("refresh", refreshCommand));
|
|
10602
11461
|
program.command("hooks").description("Manage auto-refresh hooks (toggle interactively)").option("--install", "Enable all hooks non-interactively").option("--remove", "Disable all hooks non-interactively").action(tracked("hooks", hooksCommand));
|
|
10603
11462
|
program.command("insights").description("Show agent performance insights and learning impact").option("--json", "Output as JSON").action(tracked("insights", insightsCommand));
|
|
11463
|
+
var sources = program.command("sources").description("Manage external context sources (related repos, docs)");
|
|
11464
|
+
sources.command("list").description("Show configured and auto-detected sources").action(tracked("sources:list", sourcesListCommand));
|
|
11465
|
+
sources.command("add").description("Add an external source").argument("<path>", "Path to repo directory or file").action(tracked("sources:add", sourcesAddCommand));
|
|
11466
|
+
sources.command("remove").description("Remove a configured source").argument("<name>", "Source path or role to remove").action(tracked("sources:remove", sourcesRemoveCommand));
|
|
11467
|
+
program.command("publish").description("Generate a machine-readable summary for other repos to consume").action(tracked("publish", publishCommand));
|
|
10604
11468
|
var learn = program.command("learn", { hidden: true }).description("[dev] Session learning \u2014 observe tool usage and extract reusable instructions");
|
|
10605
11469
|
learn.command("observe").description("Record a tool event from stdin (called by hooks)").option("--failure", "Mark event as a tool failure").option("--prompt", "Record a user prompt event").action(tracked("learn:observe", learnObserveCommand));
|
|
10606
11470
|
learn.command("finalize").description("Analyze session events and update CALIBER_LEARNINGS.md (called on SessionEnd)").option("--force", "Skip the running-process check (for manual invocation)").option("--auto", "Silent mode for hooks (lower threshold, no interactive output)").option("--incremental", "Extract learnings mid-session without clearing events").action(tracked("learn:finalize", (opts) => learnFinalizeCommand(opts)));
|
|
10607
11471
|
learn.command("install").description("Install learning hooks into .claude/settings.json").action(tracked("learn:install", learnInstallCommand));
|
|
10608
11472
|
learn.command("remove").description("Remove learning hooks from .claude/settings.json").action(tracked("learn:remove", learnRemoveCommand));
|
|
10609
11473
|
learn.command("status").description("Show learning system status").action(tracked("learn:status", learnStatusCommand));
|
|
11474
|
+
learn.command("list").description("List all learnings with their source and activation data").option("--verbose", "Show explanations and activation counts").action(tracked("learn:list", (opts) => learnListCommand(opts)));
|
|
11475
|
+
learn.command("delete <index>").description("Delete a learning by its index number (from `caliber learn list`)").action(tracked("learn:delete", (index) => learnDeleteCommand(index)));
|
|
10610
11476
|
|
|
10611
11477
|
// src/utils/version-check.ts
|
|
10612
|
-
import
|
|
10613
|
-
import
|
|
11478
|
+
import fs46 from "fs";
|
|
11479
|
+
import path36 from "path";
|
|
10614
11480
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
10615
11481
|
import { execSync as execSync15 } from "child_process";
|
|
10616
|
-
import
|
|
10617
|
-
import
|
|
11482
|
+
import chalk27 from "chalk";
|
|
11483
|
+
import ora8 from "ora";
|
|
10618
11484
|
import confirm2 from "@inquirer/confirm";
|
|
10619
|
-
var __dirname_vc =
|
|
11485
|
+
var __dirname_vc = path36.dirname(fileURLToPath2(import.meta.url));
|
|
10620
11486
|
var pkg2 = JSON.parse(
|
|
10621
|
-
|
|
11487
|
+
fs46.readFileSync(path36.resolve(__dirname_vc, "..", "package.json"), "utf-8")
|
|
10622
11488
|
);
|
|
10623
11489
|
function getChannel(version) {
|
|
10624
11490
|
const match = version.match(/-(dev|next)\./);
|
|
@@ -10643,8 +11509,8 @@ function isNewer(registry, current) {
|
|
|
10643
11509
|
function getInstalledVersion() {
|
|
10644
11510
|
try {
|
|
10645
11511
|
const globalRoot = execSync15("npm root -g", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
10646
|
-
const pkgPath =
|
|
10647
|
-
return JSON.parse(
|
|
11512
|
+
const pkgPath = path36.join(globalRoot, "@rely-ai", "caliber", "package.json");
|
|
11513
|
+
return JSON.parse(fs46.readFileSync(pkgPath, "utf-8")).version;
|
|
10648
11514
|
} catch {
|
|
10649
11515
|
return null;
|
|
10650
11516
|
}
|
|
@@ -10669,17 +11535,17 @@ async function checkForUpdates() {
|
|
|
10669
11535
|
if (!isInteractive) {
|
|
10670
11536
|
const installTag = channel === "latest" ? "" : `@${channel}`;
|
|
10671
11537
|
console.log(
|
|
10672
|
-
|
|
11538
|
+
chalk27.yellow(
|
|
10673
11539
|
`
|
|
10674
11540
|
Update available: ${current} -> ${latest}
|
|
10675
|
-
Run ${
|
|
11541
|
+
Run ${chalk27.bold(`npm install -g @rely-ai/caliber${installTag}`)} to upgrade.
|
|
10676
11542
|
`
|
|
10677
11543
|
)
|
|
10678
11544
|
);
|
|
10679
11545
|
return;
|
|
10680
11546
|
}
|
|
10681
11547
|
console.log(
|
|
10682
|
-
|
|
11548
|
+
chalk27.yellow(`
|
|
10683
11549
|
Update available: ${current} -> ${latest}`)
|
|
10684
11550
|
);
|
|
10685
11551
|
const shouldUpdate = await confirm2({ message: "Would you like to update now? (Y/n)", default: true });
|
|
@@ -10688,7 +11554,7 @@ Update available: ${current} -> ${latest}`)
|
|
|
10688
11554
|
return;
|
|
10689
11555
|
}
|
|
10690
11556
|
const tag = channel === "latest" ? latest : channel;
|
|
10691
|
-
const spinner =
|
|
11557
|
+
const spinner = ora8("Updating caliber...").start();
|
|
10692
11558
|
try {
|
|
10693
11559
|
execSync15(`npm install -g @rely-ai/caliber@${tag}`, {
|
|
10694
11560
|
stdio: "pipe",
|
|
@@ -10698,13 +11564,13 @@ Update available: ${current} -> ${latest}`)
|
|
|
10698
11564
|
const installed = getInstalledVersion();
|
|
10699
11565
|
if (installed !== latest) {
|
|
10700
11566
|
spinner.fail(`Update incomplete \u2014 got ${installed ?? "unknown"}, expected ${latest}`);
|
|
10701
|
-
console.log(
|
|
11567
|
+
console.log(chalk27.yellow(`Run ${chalk27.bold(`npm install -g @rely-ai/caliber@${tag}`)} manually.
|
|
10702
11568
|
`));
|
|
10703
11569
|
return;
|
|
10704
11570
|
}
|
|
10705
|
-
spinner.succeed(
|
|
11571
|
+
spinner.succeed(chalk27.green(`Updated to ${latest}`));
|
|
10706
11572
|
const args = process.argv.slice(2);
|
|
10707
|
-
console.log(
|
|
11573
|
+
console.log(chalk27.dim(`
|
|
10708
11574
|
Restarting: caliber ${args.join(" ")}
|
|
10709
11575
|
`));
|
|
10710
11576
|
execSync15(`caliber ${args.map((a) => JSON.stringify(a)).join(" ")}`, {
|
|
@@ -10717,11 +11583,11 @@ Restarting: caliber ${args.join(" ")}
|
|
|
10717
11583
|
if (err instanceof Error) {
|
|
10718
11584
|
const stderr = err.stderr;
|
|
10719
11585
|
const errMsg = stderr ? String(stderr).trim().split("\n").pop() : err.message.split("\n")[0];
|
|
10720
|
-
if (errMsg && !errMsg.includes("SIGTERM")) console.log(
|
|
11586
|
+
if (errMsg && !errMsg.includes("SIGTERM")) console.log(chalk27.dim(` ${errMsg}`));
|
|
10721
11587
|
}
|
|
10722
11588
|
console.log(
|
|
10723
|
-
|
|
10724
|
-
`Run ${
|
|
11589
|
+
chalk27.yellow(
|
|
11590
|
+
`Run ${chalk27.bold(`npm install -g @rely-ai/caliber@${tag}`)} manually to upgrade.
|
|
10725
11591
|
`
|
|
10726
11592
|
)
|
|
10727
11593
|
);
|