@orquentra/codebuddy 2.7.0 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -0
- package/dist/adapters-S4HHTWJW.js +24 -0
- package/dist/{chunk-EEUFU7KV.js → chunk-6DONJW3W.js} +163 -22
- package/dist/chunk-6DONJW3W.js.map +1 -0
- package/dist/{chunk-JUAFCLRB.js → chunk-6KGYAFG7.js} +2 -2
- package/dist/{chunk-JUAFCLRB.js.map → chunk-6KGYAFG7.js.map} +1 -1
- package/dist/{chunk-BQM7FJ7D.js → chunk-6NZEZJYD.js} +5 -3
- package/dist/{chunk-BQM7FJ7D.js.map → chunk-6NZEZJYD.js.map} +1 -1
- package/dist/{chunk-AQHPEJR6.js → chunk-EJQVGFH2.js} +2 -2
- package/dist/{chunk-MSUAVR3R.js → chunk-GXOML4UZ.js} +2 -2
- package/dist/chunk-ITQO632W.js +673 -0
- package/dist/chunk-ITQO632W.js.map +1 -0
- package/dist/chunk-J4T6HHI4.js +1444 -0
- package/dist/chunk-J4T6HHI4.js.map +1 -0
- package/dist/{chunk-AK2UAE32.js → chunk-NY2YXP3R.js} +2 -2
- package/dist/{chunk-5EDGAZQN.js → chunk-TLWGZVPC.js} +64 -725
- package/dist/chunk-TLWGZVPC.js.map +1 -0
- package/dist/{chunk-LCSBL5TT.js → chunk-UK6XTS2Y.js} +5 -3
- package/dist/{chunk-LCSBL5TT.js.map → chunk-UK6XTS2Y.js.map} +1 -1
- package/dist/chunk-W7GAXL6C.js +104 -0
- package/dist/chunk-W7GAXL6C.js.map +1 -0
- package/dist/{chunk-TUR6YKEE.js → chunk-XL2HLACE.js} +1 -1
- package/dist/chunk-XL2HLACE.js.map +1 -0
- package/dist/{chunk-YJIA2MFL.js → chunk-ZNLSCV3J.js} +15 -13
- package/dist/{chunk-YJIA2MFL.js.map → chunk-ZNLSCV3J.js.map} +1 -1
- package/dist/cli/index.js +4 -4
- package/dist/index.js +9 -8
- package/dist/install-BXM56QCD.js +19 -0
- package/dist/{lean-command-CWJAVQ5G.js → lean-command-7DFW32MA.js} +4 -3
- package/dist/{main-AHNGHC4L.js → main-QUASSFP7.js} +902 -1686
- package/dist/main-QUASSFP7.js.map +1 -0
- package/dist/{operations-7BTAAUHF.js → operations-WJR7AZZF.js} +3 -3
- package/dist/{pretool-command-3SXQSMAO.js → pretool-command-JTKX6G3M.js} +3 -3
- package/dist/{runtime-E7JQZ2BZ.js → runtime-OQQ22VJF.js} +7 -6
- package/dist/{summaries-XV3JGGCC.js → summaries-ML53R7EL.js} +4 -3
- package/dist/summaries-ML53R7EL.js.map +1 -0
- package/dist/{zvec-KP4KNB7Y.js → zvec-XDZNZ2OL.js} +4 -3
- package/dist/zvec-XDZNZ2OL.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-5EDGAZQN.js.map +0 -1
- package/dist/chunk-EEUFU7KV.js.map +0 -1
- package/dist/chunk-TUR6YKEE.js.map +0 -1
- package/dist/main-AHNGHC4L.js.map +0 -1
- /package/dist/{lean-command-CWJAVQ5G.js.map → adapters-S4HHTWJW.js.map} +0 -0
- /package/dist/{chunk-AQHPEJR6.js.map → chunk-EJQVGFH2.js.map} +0 -0
- /package/dist/{chunk-MSUAVR3R.js.map → chunk-GXOML4UZ.js.map} +0 -0
- /package/dist/{chunk-AK2UAE32.js.map → chunk-NY2YXP3R.js.map} +0 -0
- /package/dist/{operations-7BTAAUHF.js.map → install-BXM56QCD.js.map} +0 -0
- /package/dist/{pretool-command-3SXQSMAO.js.map → lean-command-7DFW32MA.js.map} +0 -0
- /package/dist/{runtime-E7JQZ2BZ.js.map → operations-WJR7AZZF.js.map} +0 -0
- /package/dist/{summaries-XV3JGGCC.js.map → pretool-command-JTKX6G3M.js.map} +0 -0
- /package/dist/{zvec-KP4KNB7Y.js.map → runtime-OQQ22VJF.js.map} +0 -0
|
@@ -1,6 +1,48 @@
|
|
|
1
|
+
import {
|
|
2
|
+
renderLeanBlock,
|
|
3
|
+
runLeanHookCommand
|
|
4
|
+
} from "./chunk-UK6XTS2Y.js";
|
|
1
5
|
import {
|
|
2
6
|
runPreToolHookCommand
|
|
3
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-EJQVGFH2.js";
|
|
8
|
+
import {
|
|
9
|
+
failSoft,
|
|
10
|
+
readHookInput,
|
|
11
|
+
writeStdout
|
|
12
|
+
} from "./chunk-FJOTP632.js";
|
|
13
|
+
import {
|
|
14
|
+
WORKFLOW_CLIENTS,
|
|
15
|
+
clientDisplayName,
|
|
16
|
+
clientHooksInstalled,
|
|
17
|
+
defaultRulesMode,
|
|
18
|
+
detectClient,
|
|
19
|
+
hermesConfigPath,
|
|
20
|
+
hermesPluginInstalled,
|
|
21
|
+
installClaudeEntry,
|
|
22
|
+
installClient,
|
|
23
|
+
installCodexEntry,
|
|
24
|
+
installWorkflowRules,
|
|
25
|
+
listClaudeEntries,
|
|
26
|
+
listCodexEntries,
|
|
27
|
+
readHermesConfig,
|
|
28
|
+
removeClaudeEntry,
|
|
29
|
+
removeCodexEntry,
|
|
30
|
+
renderManagedBlock,
|
|
31
|
+
renderWorkflowTemplate,
|
|
32
|
+
setHooks,
|
|
33
|
+
uninstallClient,
|
|
34
|
+
writeProjectMcpConfig
|
|
35
|
+
} from "./chunk-J4T6HHI4.js";
|
|
36
|
+
import {
|
|
37
|
+
AUTOMATION_MODES,
|
|
38
|
+
CLIENT_ADAPTERS,
|
|
39
|
+
CLIENT_IDS,
|
|
40
|
+
LIFECYCLE_EVENTS,
|
|
41
|
+
MODE_LABELS,
|
|
42
|
+
parseClient,
|
|
43
|
+
parseMode,
|
|
44
|
+
supportedModes
|
|
45
|
+
} from "./chunk-W7GAXL6C.js";
|
|
4
46
|
import {
|
|
5
47
|
isDockerDaemonRunning,
|
|
6
48
|
tryStartDockerDesktop,
|
|
@@ -12,26 +54,25 @@ import {
|
|
|
12
54
|
postgresUp
|
|
13
55
|
} from "./chunk-CSRD5OQA.js";
|
|
14
56
|
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
} from "./chunk-LCSBL5TT.js";
|
|
18
|
-
import {
|
|
19
|
-
failSoft,
|
|
20
|
-
readHookInput,
|
|
21
|
-
writeStdout
|
|
22
|
-
} from "./chunk-FJOTP632.js";
|
|
23
|
-
import {
|
|
57
|
+
TELEMETRY_NOTICE,
|
|
58
|
+
flushTelemetry,
|
|
24
59
|
generateSuggestions,
|
|
25
60
|
migrateMemoryToFiles,
|
|
61
|
+
readTelemetryState,
|
|
26
62
|
registerCodeBuddyTools,
|
|
63
|
+
showTelemetryNoticeOnce,
|
|
27
64
|
startMcpServer,
|
|
28
|
-
|
|
29
|
-
|
|
65
|
+
telemetryBlocked,
|
|
66
|
+
telemetryStatePath,
|
|
67
|
+
telemetryUrl,
|
|
68
|
+
watchRepository,
|
|
69
|
+
writeTelemetryState
|
|
70
|
+
} from "./chunk-6DONJW3W.js";
|
|
30
71
|
import {
|
|
31
72
|
DEFAULT_ZVEC_MODEL,
|
|
32
73
|
buildZvecIndex,
|
|
33
74
|
zvecStatus
|
|
34
|
-
} from "./chunk-
|
|
75
|
+
} from "./chunk-GXOML4UZ.js";
|
|
35
76
|
import {
|
|
36
77
|
PlanFileStore,
|
|
37
78
|
PlanLifecycle,
|
|
@@ -49,21 +90,21 @@ import {
|
|
|
49
90
|
runResetHook,
|
|
50
91
|
runStageHook,
|
|
51
92
|
runSyncHook
|
|
52
|
-
} from "./chunk-
|
|
53
|
-
import {
|
|
54
|
-
parseTraceLines,
|
|
55
|
-
readTraces,
|
|
56
|
-
tracePath
|
|
57
|
-
} from "./chunk-TUR6YKEE.js";
|
|
58
|
-
import "./chunk-DFV2SK6C.js";
|
|
93
|
+
} from "./chunk-ZNLSCV3J.js";
|
|
59
94
|
import {
|
|
60
95
|
SessionCapsuleLedger,
|
|
61
96
|
detectTestCommand,
|
|
62
97
|
estimateTokens
|
|
63
98
|
} from "./chunk-GKNYMNFM.js";
|
|
99
|
+
import {
|
|
100
|
+
parseTraceLines,
|
|
101
|
+
readTraces,
|
|
102
|
+
tracePath
|
|
103
|
+
} from "./chunk-XL2HLACE.js";
|
|
64
104
|
import {
|
|
65
105
|
hookRoot
|
|
66
106
|
} from "./chunk-TGFTKWOR.js";
|
|
107
|
+
import "./chunk-DFV2SK6C.js";
|
|
67
108
|
import {
|
|
68
109
|
bootstrapDatabase,
|
|
69
110
|
buildProviderFromConfig,
|
|
@@ -75,11 +116,11 @@ import {
|
|
|
75
116
|
pingDatabase,
|
|
76
117
|
runDoctor,
|
|
77
118
|
runMigrations
|
|
78
|
-
} from "./chunk-
|
|
119
|
+
} from "./chunk-NY2YXP3R.js";
|
|
79
120
|
import {
|
|
80
121
|
MemoryFileStore,
|
|
81
122
|
VERSION
|
|
82
|
-
} from "./chunk-
|
|
123
|
+
} from "./chunk-6KGYAFG7.js";
|
|
83
124
|
import {
|
|
84
125
|
computeFactHash,
|
|
85
126
|
generateEntityId
|
|
@@ -92,23 +133,25 @@ import {
|
|
|
92
133
|
import {
|
|
93
134
|
loadLlmSummaries,
|
|
94
135
|
summarizeRepository
|
|
95
|
-
} from "./chunk-
|
|
136
|
+
} from "./chunk-6NZEZJYD.js";
|
|
96
137
|
import {
|
|
97
138
|
chatJson,
|
|
98
139
|
coolingDown,
|
|
99
140
|
usageToday
|
|
100
141
|
} from "./chunk-ESZXCZIN.js";
|
|
101
142
|
import {
|
|
102
|
-
buildArchitectureMap,
|
|
103
143
|
generateGraphifyGraph,
|
|
104
|
-
indexRepository,
|
|
105
|
-
loadFreshIndex,
|
|
106
144
|
loadGraphifyMap,
|
|
107
|
-
neighbours,
|
|
108
|
-
queryMap,
|
|
109
145
|
setupGraphify,
|
|
110
146
|
syncFileVectors
|
|
111
|
-
} from "./chunk-
|
|
147
|
+
} from "./chunk-TLWGZVPC.js";
|
|
148
|
+
import {
|
|
149
|
+
buildArchitectureMap,
|
|
150
|
+
indexRepository,
|
|
151
|
+
loadFreshIndex,
|
|
152
|
+
neighbours,
|
|
153
|
+
queryMap
|
|
154
|
+
} from "./chunk-ITQO632W.js";
|
|
112
155
|
import {
|
|
113
156
|
extractSymbolTable,
|
|
114
157
|
supportsSymbols
|
|
@@ -143,14 +186,12 @@ import {
|
|
|
143
186
|
setZvecEnabled,
|
|
144
187
|
writeConfigFile
|
|
145
188
|
} from "./chunk-KNR2P2UY.js";
|
|
146
|
-
import
|
|
147
|
-
writeAtomic
|
|
148
|
-
} from "./chunk-CG5B3LYG.js";
|
|
189
|
+
import "./chunk-CG5B3LYG.js";
|
|
149
190
|
import "./chunk-PZ5AY32C.js";
|
|
150
191
|
|
|
151
192
|
// src/cli/main.ts
|
|
152
193
|
import { Command } from "commander";
|
|
153
|
-
import
|
|
194
|
+
import pc23 from "picocolors";
|
|
154
195
|
|
|
155
196
|
// src/integrations/github.ts
|
|
156
197
|
import { constants } from "fs";
|
|
@@ -337,7 +378,7 @@ function runTestCommand(command, source, repositoryRoot, timeoutMs) {
|
|
|
337
378
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
338
379
|
throw new Error("--timeout must be a positive number of milliseconds.");
|
|
339
380
|
}
|
|
340
|
-
return new Promise((
|
|
381
|
+
return new Promise((resolve6) => {
|
|
341
382
|
const startedAt = Date.now();
|
|
342
383
|
const child = spawn(command, {
|
|
343
384
|
cwd: repositoryRoot,
|
|
@@ -362,7 +403,7 @@ ${output.slice(-MAX_TEST_OUTPUT_CHARS)}`;
|
|
|
362
403
|
}, timeoutMs);
|
|
363
404
|
child.on("close", (exitCode, signal) => {
|
|
364
405
|
clearTimeout(timer);
|
|
365
|
-
|
|
406
|
+
resolve6({
|
|
366
407
|
status: timedOut ? "timed_out" : exitCode === 0 ? "passed" : "failed",
|
|
367
408
|
command,
|
|
368
409
|
source,
|
|
@@ -374,7 +415,7 @@ ${output.slice(-MAX_TEST_OUTPUT_CHARS)}`;
|
|
|
374
415
|
});
|
|
375
416
|
child.on("error", (error) => {
|
|
376
417
|
clearTimeout(timer);
|
|
377
|
-
|
|
418
|
+
resolve6({
|
|
378
419
|
status: "failed",
|
|
379
420
|
command,
|
|
380
421
|
source,
|
|
@@ -585,1016 +626,17 @@ function testBadge(status) {
|
|
|
585
626
|
return pc.yellow("[not run]");
|
|
586
627
|
}
|
|
587
628
|
|
|
588
|
-
// src/cli/commands/claude-desktop.ts
|
|
589
|
-
import { existsSync } from "fs";
|
|
590
|
-
import { mkdir as mkdir2, readFile } from "fs/promises";
|
|
591
|
-
import { dirname as dirname2, join as join2, resolve as resolve2 } from "path";
|
|
592
|
-
import { fileURLToPath } from "url";
|
|
593
|
-
function claudeDesktopConfigPath(home = process.env.HOME ?? "") {
|
|
594
|
-
if (process.platform === "darwin") {
|
|
595
|
-
return join2(home, "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
596
|
-
}
|
|
597
|
-
if (process.platform === "win32") {
|
|
598
|
-
return join2(process.env.APPDATA ?? home, "Claude", "claude_desktop_config.json");
|
|
599
|
-
}
|
|
600
|
-
return join2(home, ".config", "Claude", "claude_desktop_config.json");
|
|
601
|
-
}
|
|
602
|
-
async function readClaudeConfig(path) {
|
|
603
|
-
if (!existsSync(path)) return {};
|
|
604
|
-
try {
|
|
605
|
-
return JSON.parse(await readFile(path, "utf8"));
|
|
606
|
-
} catch (error) {
|
|
607
|
-
throw new Error(
|
|
608
|
-
`Failed to parse Claude Desktop config at ${path}: ${error.message}`
|
|
609
|
-
);
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
async function writeClaudeConfig(path, config) {
|
|
613
|
-
await mkdir2(dirname2(path), { recursive: true });
|
|
614
|
-
await writeAtomic(path, `${JSON.stringify(config, null, 2)}
|
|
615
|
-
`, { mode: 384 });
|
|
616
|
-
}
|
|
617
|
-
function resolveCliEntrypoint() {
|
|
618
|
-
const fromArgv = process.argv[1];
|
|
619
|
-
if (fromArgv && existsSync(fromArgv)) return fromArgv;
|
|
620
|
-
const here = dirname2(fileURLToPath(import.meta.url));
|
|
621
|
-
const candidates = [
|
|
622
|
-
resolve2(here, "index.js"),
|
|
623
|
-
// bundled: here = dist/cli/
|
|
624
|
-
resolve2(here, "..", "cli", "index.js"),
|
|
625
|
-
// source: here = src/cli/commands/
|
|
626
|
-
resolve2(here, "..", "index.js")
|
|
627
|
-
];
|
|
628
|
-
for (const candidate of candidates) {
|
|
629
|
-
if (existsSync(candidate)) return candidate;
|
|
630
|
-
}
|
|
631
|
-
return fromArgv ?? candidates[0] ?? "";
|
|
632
|
-
}
|
|
633
|
-
function buildEntry(options) {
|
|
634
|
-
const env = {
|
|
635
|
-
CODEBUDDY_NAMESPACE: options.namespace,
|
|
636
|
-
CODEBUDDY_PROJECT_ROOT: resolve2(options.projectRoot ?? process.cwd())
|
|
637
|
-
};
|
|
638
|
-
if (options.hfToken) env.HF_TOKEN = options.hfToken;
|
|
639
|
-
if (options.groqApiKey) env.GROQ_API_KEY = options.groqApiKey;
|
|
640
|
-
if (options.databaseUrl) env.DATABASE_URL = options.databaseUrl;
|
|
641
|
-
return {
|
|
642
|
-
// Absolute: Claude Desktop is a GUI app and does not inherit the shell's
|
|
643
|
-
// PATH (nvm, Homebrew), so a bare "node" may not resolve or may be another
|
|
644
|
-
// version. Re-run `codebuddy claude install` after switching Node.
|
|
645
|
-
command: process.execPath,
|
|
646
|
-
args: [resolveCliEntrypoint(), "serve"],
|
|
647
|
-
env
|
|
648
|
-
};
|
|
649
|
-
}
|
|
650
|
-
async function installClaudeEntry(options) {
|
|
651
|
-
const path = claudeDesktopConfigPath();
|
|
652
|
-
const config = await readClaudeConfig(path);
|
|
653
|
-
config.mcpServers ??= {};
|
|
654
|
-
const serverKey = options.serverName ?? `codebuddy-${options.namespace}`;
|
|
655
|
-
const existed = config.mcpServers[serverKey] !== void 0;
|
|
656
|
-
config.mcpServers[serverKey] = buildEntry(options);
|
|
657
|
-
await writeClaudeConfig(path, config);
|
|
658
|
-
return { path, serverKey, created: !existed };
|
|
659
|
-
}
|
|
660
|
-
async function removeClaudeEntry(serverKey) {
|
|
661
|
-
const path = claudeDesktopConfigPath();
|
|
662
|
-
const config = await readClaudeConfig(path);
|
|
663
|
-
if (!config.mcpServers || !(serverKey in config.mcpServers)) {
|
|
664
|
-
return { removed: false, path };
|
|
665
|
-
}
|
|
666
|
-
delete config.mcpServers[serverKey];
|
|
667
|
-
await writeClaudeConfig(path, config);
|
|
668
|
-
return { removed: true, path };
|
|
669
|
-
}
|
|
670
|
-
async function listClaudeEntries() {
|
|
671
|
-
const path = claudeDesktopConfigPath();
|
|
672
|
-
const config = await readClaudeConfig(path);
|
|
673
|
-
const entries = Object.entries(config.mcpServers ?? {}).filter(([key]) => key === "codebuddy" || key.startsWith("codebuddy-")).map(([key, entry]) => ({
|
|
674
|
-
key,
|
|
675
|
-
namespace: entry.env?.CODEBUDDY_NAMESPACE ?? null,
|
|
676
|
-
databaseUrl: entry.env?.DATABASE_URL ?? null
|
|
677
|
-
}));
|
|
678
|
-
return { path, entries };
|
|
679
|
-
}
|
|
680
|
-
|
|
681
629
|
// src/cli/commands/clients.ts
|
|
682
630
|
import { relative } from "path";
|
|
683
631
|
import pc2 from "picocolors";
|
|
684
632
|
|
|
685
|
-
// src/clients/adapters.ts
|
|
686
|
-
var AUTOMATION_MODES = ["agent", "hooks", "mcp-only", "lean"];
|
|
687
|
-
var MODE_LABELS = {
|
|
688
|
-
agent: "agent-driven",
|
|
689
|
-
hooks: "hook-driven",
|
|
690
|
-
"mcp-only": "MCP-only",
|
|
691
|
-
lean: "Lean (token reduction)"
|
|
692
|
-
};
|
|
693
|
-
var LIFECYCLE_EVENTS = ["prompt", "edit", "turn-end", "compaction"];
|
|
694
|
-
var CLIENT_IDS = ["claude", "codex", "opencode", "freebuff", "hermes"];
|
|
695
|
-
var CLIENT_ADAPTERS = {
|
|
696
|
-
claude: {
|
|
697
|
-
name: "Claude Code",
|
|
698
|
-
host: { bin: "claude", verified: "2.1.287" },
|
|
699
|
-
rulesFile: "CLAUDE.md",
|
|
700
|
-
mcp: { file: ".mcp.json", format: "mcpServers" },
|
|
701
|
-
hookEvents: LIFECYCLE_EVENTS,
|
|
702
|
-
hooks: "claude-settings",
|
|
703
|
-
framing: "UserPromptSubmit stdout is added to the prompt as hook context (wrapper is host-defined); unchanged lines are not resent; MCP schemas may be deferred by tool search",
|
|
704
|
-
defersMcpSchemas: true
|
|
705
|
-
},
|
|
706
|
-
codex: {
|
|
707
|
-
name: "Codex",
|
|
708
|
-
host: { bin: "codex", verified: "0.163.0" },
|
|
709
|
-
rulesFile: "AGENTS.md",
|
|
710
|
-
mcp: { global: "codex", manual: "codebuddy codex install (global ~/.codex/config.toml)" },
|
|
711
|
-
hookEvents: [],
|
|
712
|
-
gap: "no CodeBuddy hook adapter for Codex yet",
|
|
713
|
-
framing: "agent mode only; AGENTS.md is part of the instructions every request"
|
|
714
|
-
},
|
|
715
|
-
opencode: {
|
|
716
|
-
name: "OpenCode",
|
|
717
|
-
host: { bin: "opencode", verified: "1.18.32" },
|
|
718
|
-
rulesFile: "AGENTS.md",
|
|
719
|
-
// opencode.jsonc is used instead when it is the only one present.
|
|
720
|
-
mcp: { file: "opencode.json", format: "opencode" },
|
|
721
|
-
// Plugin bridge: chat.message (prompt), tool.execute.after (edit),
|
|
722
|
-
// session.idle (turn-end), session.compacted.
|
|
723
|
-
hookEvents: LIFECYCLE_EVENTS,
|
|
724
|
-
hooks: "opencode-plugin",
|
|
725
|
-
framing: "block appended verbatim as a synthetic text part that stays in history; unchanged lines are not resent"
|
|
726
|
-
},
|
|
727
|
-
freebuff: {
|
|
728
|
-
name: "Freebuff",
|
|
729
|
-
// Not installed; checked against source CodebuffAI/freebuff@f9ae64dd.
|
|
730
|
-
host: { bin: "freebuff", verified: "source f9ae64dd" },
|
|
731
|
-
rulesFile: "AGENTS.md",
|
|
732
|
-
mcp: { file: ".agents/mcp.json", format: "mcpServers" },
|
|
733
|
-
hookEvents: [],
|
|
734
|
-
gap: "no lifecycle surface at CodebuffAI/freebuff@f9ae64dd: no user hook config, and the SDK run_file_change_hooks tool is a no-op (sdk/src/tools/run-file-change-hooks.ts)",
|
|
735
|
-
note: "reads AGENTS.md, and CLAUDE.md only in a directory without one (common/src/constants/knowledge.ts)",
|
|
736
|
-
framing: "agent mode only; AGENTS.md is sent as a fenced block under a fixed project-instructions header"
|
|
737
|
-
},
|
|
738
|
-
hermes: {
|
|
739
|
-
name: "Hermes Agent",
|
|
740
|
-
host: { bin: "hermes", verified: "0.17.0" },
|
|
741
|
-
rulesFile: "AGENTS.md",
|
|
742
|
-
mcp: {
|
|
743
|
-
global: "hermes",
|
|
744
|
-
manual: "add a codebuddy entry under mcp_servers in ~/.hermes/config.yaml"
|
|
745
|
-
},
|
|
746
|
-
// Project Python plugin: pre_llm_call (prompt), post_tool_call (edit),
|
|
747
|
-
// on_session_end (turn-end). Compaction has no plugin hook; the context is
|
|
748
|
-
// resent in full every turn and the session ledger reset with it.
|
|
749
|
-
hookEvents: LIFECYCLE_EVENTS,
|
|
750
|
-
hooks: "hermes-plugin",
|
|
751
|
-
ephemeralContext: true,
|
|
752
|
-
framing: "block appended to this turn's user message only and never persisted, so it is resent in full every turn",
|
|
753
|
-
note: "hooks load only with HERMES_ENABLE_PROJECT_PLUGINS=1 and codebuddy in plugins.enabled of ~/.hermes/config.yaml; .hermes.md takes precedence over AGENTS.md"
|
|
754
|
-
}
|
|
755
|
-
};
|
|
756
|
-
function parseClient(value) {
|
|
757
|
-
const client = value ?? "claude";
|
|
758
|
-
if (!CLIENT_IDS.includes(client)) {
|
|
759
|
-
throw new Error(`Unknown client "${client}". Use one of: ${CLIENT_IDS.join(", ")}.`);
|
|
760
|
-
}
|
|
761
|
-
return client;
|
|
762
|
-
}
|
|
763
|
-
function parseMode(value) {
|
|
764
|
-
if (!AUTOMATION_MODES.includes(value)) {
|
|
765
|
-
throw new Error(`Unknown mode "${value}". Use one of: ${AUTOMATION_MODES.join(", ")}.`);
|
|
766
|
-
}
|
|
767
|
-
return value;
|
|
768
|
-
}
|
|
769
|
-
function supportedModes(adapter) {
|
|
770
|
-
return adapter.hookEvents.length === LIFECYCLE_EVENTS.length ? [...AUTOMATION_MODES] : ["agent", "mcp-only"];
|
|
771
|
-
}
|
|
772
|
-
function sharedRulesReaders(client) {
|
|
773
|
-
const file = CLIENT_ADAPTERS[client].rulesFile;
|
|
774
|
-
return CLIENT_IDS.filter((id) => id !== client && CLIENT_ADAPTERS[id].rulesFile === file);
|
|
775
|
-
}
|
|
776
|
-
|
|
777
633
|
// src/clients/cost.ts
|
|
778
|
-
import { mkdtemp, rm
|
|
634
|
+
import { mkdtemp, rm } from "fs/promises";
|
|
779
635
|
import { tmpdir } from "os";
|
|
780
|
-
import { join as
|
|
636
|
+
import { join as join2, resolve as resolve2 } from "path";
|
|
781
637
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
782
638
|
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
783
639
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
784
|
-
|
|
785
|
-
// src/templates/install.ts
|
|
786
|
-
import { readFile as readFile6, rm as rm2 } from "fs/promises";
|
|
787
|
-
import { join as join6, resolve as resolve6 } from "path";
|
|
788
|
-
|
|
789
|
-
// src/hooks/hermes-plugin.ts
|
|
790
|
-
import { readFile as readFile3 } from "fs/promises";
|
|
791
|
-
import { dirname as dirname4, join as join3, resolve as resolve3 } from "path";
|
|
792
|
-
|
|
793
|
-
// src/hooks/managed-plugin.ts
|
|
794
|
-
import { mkdir as mkdir3, readFile as readFile2, rm, rmdir } from "fs/promises";
|
|
795
|
-
import { dirname as dirname3 } from "path";
|
|
796
|
-
async function read(path) {
|
|
797
|
-
try {
|
|
798
|
-
return await readFile2(path, "utf8");
|
|
799
|
-
} catch (error) {
|
|
800
|
-
if (error.code === "ENOENT") return null;
|
|
801
|
-
throw error;
|
|
802
|
-
}
|
|
803
|
-
}
|
|
804
|
-
async function managedPluginInstalled(plugin3) {
|
|
805
|
-
for (const file of plugin3.files) {
|
|
806
|
-
if (!(await read(file.path))?.startsWith(plugin3.marker)) return false;
|
|
807
|
-
}
|
|
808
|
-
return true;
|
|
809
|
-
}
|
|
810
|
-
async function setManagedPlugin(plugin3, enabled) {
|
|
811
|
-
const current = await Promise.all(plugin3.files.map((file) => read(file.path)));
|
|
812
|
-
const foreign = plugin3.files.find((_, i) => {
|
|
813
|
-
const text3 = current[i];
|
|
814
|
-
return text3 != null && !text3.startsWith(plugin3.marker);
|
|
815
|
-
});
|
|
816
|
-
if (foreign) {
|
|
817
|
-
if (!enabled) return false;
|
|
818
|
-
throw new Error(
|
|
819
|
-
`${foreign.path} exists and is not managed by CodeBuddy; move it aside and re-run.`
|
|
820
|
-
);
|
|
821
|
-
}
|
|
822
|
-
let changed = false;
|
|
823
|
-
for (const [i, file] of plugin3.files.entries()) {
|
|
824
|
-
if (enabled && current[i] !== file.content) {
|
|
825
|
-
await mkdir3(dirname3(file.path), { recursive: true });
|
|
826
|
-
await writeAtomic(file.path, file.content, { mode: 420 });
|
|
827
|
-
changed = true;
|
|
828
|
-
} else if (!enabled && current[i] != null) {
|
|
829
|
-
await rm(file.path, { force: true });
|
|
830
|
-
changed = true;
|
|
831
|
-
}
|
|
832
|
-
}
|
|
833
|
-
if (!enabled && changed) {
|
|
834
|
-
for (const dir of plugin3.dirs) await rmdir(dir).catch(() => {
|
|
835
|
-
});
|
|
836
|
-
}
|
|
837
|
-
return changed;
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
// src/hooks/hermes-plugin.ts
|
|
841
|
-
var HERMES_PLUGIN_MARKER = "# codebuddy-managed: hermes-plugin";
|
|
842
|
-
var HERMES_PLUGIN_MANIFEST = `${HERMES_PLUGIN_MARKER} v1
|
|
843
|
-
name: codebuddy
|
|
844
|
-
version: 1.0.0
|
|
845
|
-
kind: standalone
|
|
846
|
-
description: CodeBuddy recall and capture bridge (codebuddy clients install hermes --mode hooks).
|
|
847
|
-
provides_hooks:
|
|
848
|
-
- pre_llm_call
|
|
849
|
-
- post_tool_call
|
|
850
|
-
- post_llm_call
|
|
851
|
-
- on_session_end
|
|
852
|
-
`;
|
|
853
|
-
var HERMES_LEAN_MANIFEST = `${HERMES_PLUGIN_MARKER} v1 lean
|
|
854
|
-
name: codebuddy
|
|
855
|
-
version: 1.0.0
|
|
856
|
-
kind: standalone
|
|
857
|
-
description: CodeBuddy lean bridge (codebuddy clients install hermes --mode lean).
|
|
858
|
-
provides_hooks:
|
|
859
|
-
- pre_llm_call
|
|
860
|
-
`;
|
|
861
|
-
var HERMES_HOOK_FN = `def _hook(command, payload):
|
|
862
|
-
"""Run one fail-soft CodeBuddy hook; its stdout, or "" on any failure."""
|
|
863
|
-
try:
|
|
864
|
-
done = subprocess.run(
|
|
865
|
-
["codebuddy", "hooks", command],
|
|
866
|
-
input=json.dumps({"cwd": ROOT, **payload}),
|
|
867
|
-
capture_output=True,
|
|
868
|
-
text=True,
|
|
869
|
-
timeout=TIMEOUT_S[command],
|
|
870
|
-
cwd=ROOT,
|
|
871
|
-
)
|
|
872
|
-
except Exception:
|
|
873
|
-
return ""
|
|
874
|
-
return done.stdout if done.returncode == 0 else ""
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
`;
|
|
878
|
-
function renderHermesPlugin(variant) {
|
|
879
|
-
return variant === "lean" ? `${HERMES_PLUGIN_MARKER} v1 lean
|
|
880
|
-
# Generated by "codebuddy clients install hermes --mode lean"; removed by
|
|
881
|
-
# "codebuddy clients uninstall hermes". Local edits are overwritten.
|
|
882
|
-
"""CodeBuddy lean bridge: forwards each prompt to "codebuddy hooks lean"."""
|
|
883
|
-
|
|
884
|
-
import json
|
|
885
|
-
import subprocess
|
|
886
|
-
from pathlib import Path
|
|
887
|
-
|
|
888
|
-
# <root>/.hermes/plugins/codebuddy/__init__.py
|
|
889
|
-
ROOT = str(Path(__file__).absolute().parents[3])
|
|
890
|
-
TIMEOUT_S = {"lean": 2}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
${HERMES_HOOK_FN}def _pre_llm_call(session_id="", user_message="", **_):
|
|
894
|
-
if not isinstance(user_message, str) or not user_message.strip():
|
|
895
|
-
return None
|
|
896
|
-
payload = {
|
|
897
|
-
"session_id": session_id,
|
|
898
|
-
"hook_event_name": "pre_llm_call",
|
|
899
|
-
"ephemeral_context": True,
|
|
900
|
-
"prompt": user_message,
|
|
901
|
-
}
|
|
902
|
-
block = _hook("lean", payload).strip()
|
|
903
|
-
return {"context": block} if block else None
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
def register(ctx):
|
|
907
|
-
ctx.register_hook("pre_llm_call", _pre_llm_call)
|
|
908
|
-
` : `${HERMES_PLUGIN_MARKER} v1
|
|
909
|
-
# Generated by "codebuddy clients install hermes --mode hooks"; removed by
|
|
910
|
-
# "codebuddy clients uninstall hermes". Local edits are overwritten.
|
|
911
|
-
"""CodeBuddy lifecycle bridge: forwards Hermes hooks to "codebuddy hooks <command>"."""
|
|
912
|
-
|
|
913
|
-
import json
|
|
914
|
-
import subprocess
|
|
915
|
-
import threading
|
|
916
|
-
from pathlib import Path
|
|
917
|
-
|
|
918
|
-
# <root>/.hermes/plugins/codebuddy/__init__.py
|
|
919
|
-
ROOT = str(Path(__file__).absolute().parents[3])
|
|
920
|
-
EDIT_TOOLS = {"write_file", "patch"}
|
|
921
|
-
TIMEOUT_S = {"context": 20, "stage": 10, "capture": 60}
|
|
922
|
-
_lock = threading.Lock()
|
|
923
|
-
_turns = {}
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
${HERMES_HOOK_FN}def _turn(turn_id, session_id):
|
|
927
|
-
# Compression can rotate the session id mid-turn; a turn keeps its first id.
|
|
928
|
-
with _lock:
|
|
929
|
-
return _turns.setdefault(
|
|
930
|
-
turn_id or session_id, {"session": session_id, "text": "", "commands": []}
|
|
931
|
-
)
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
def _pre_llm_call(session_id="", turn_id="", user_message="", **_):
|
|
935
|
-
_turn(turn_id, session_id)
|
|
936
|
-
payload = {"session_id": session_id, "hook_event_name": "pre_llm_call", "ephemeral_context": True}
|
|
937
|
-
if isinstance(user_message, str) and user_message.strip():
|
|
938
|
-
payload["prompt"] = user_message
|
|
939
|
-
block = _hook("context", payload).strip()
|
|
940
|
-
return {"context": block} if block else None
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
def _post_tool_call(tool_name="", args=None, result=None, session_id="", turn_id="", **_):
|
|
944
|
-
turn = _turn(turn_id, session_id)
|
|
945
|
-
args = args if isinstance(args, dict) else {}
|
|
946
|
-
if tool_name in EDIT_TOOLS:
|
|
947
|
-
# Paths only: write_file content never needs to cross the pipe.
|
|
948
|
-
tool_input = {k: args[k] for k in ("path", "mode", "patch") if k in args}
|
|
949
|
-
_hook("stage", {
|
|
950
|
-
"session_id": turn["session"],
|
|
951
|
-
"hook_event_name": "post_tool_call",
|
|
952
|
-
"tool_name": tool_name,
|
|
953
|
-
"tool_input": tool_input,
|
|
954
|
-
})
|
|
955
|
-
elif tool_name == "terminal" and isinstance(args.get("command"), str):
|
|
956
|
-
exit_code = None
|
|
957
|
-
if not args.get("background"): # a background process has not finished
|
|
958
|
-
try:
|
|
959
|
-
exit_code = json.loads(result).get("exit_code")
|
|
960
|
-
except Exception:
|
|
961
|
-
pass
|
|
962
|
-
turn["commands"].append({
|
|
963
|
-
"command": args["command"],
|
|
964
|
-
"exit_code": exit_code if type(exit_code) is int else None,
|
|
965
|
-
})
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
def _post_llm_call(session_id="", turn_id="", assistant_response="", **_):
|
|
969
|
-
if isinstance(assistant_response, str) and assistant_response.strip():
|
|
970
|
-
_turn(turn_id, session_id)["text"] = assistant_response
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
def _on_session_end(session_id="", turn_id="", **_):
|
|
974
|
-
with _lock:
|
|
975
|
-
turn = _turns.pop(turn_id or session_id, None)
|
|
976
|
-
turn = turn or {"session": session_id, "text": "", "commands": []}
|
|
977
|
-
payload = {"session_id": turn["session"], "hook_event_name": "on_session_end"}
|
|
978
|
-
if turn["text"]:
|
|
979
|
-
payload["last_assistant_message"] = turn["text"]
|
|
980
|
-
if turn["commands"]:
|
|
981
|
-
payload["shell_commands"] = turn["commands"][-20:]
|
|
982
|
-
_hook("capture", payload)
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
def register(ctx):
|
|
986
|
-
ctx.register_hook("pre_llm_call", _pre_llm_call)
|
|
987
|
-
ctx.register_hook("post_tool_call", _post_tool_call)
|
|
988
|
-
ctx.register_hook("post_llm_call", _post_llm_call)
|
|
989
|
-
ctx.register_hook("on_session_end", _on_session_end)
|
|
990
|
-
`;
|
|
991
|
-
}
|
|
992
|
-
var HERMES_PLUGIN_SOURCE = renderHermesPlugin("full");
|
|
993
|
-
function hermesPluginDir(repositoryRoot) {
|
|
994
|
-
return join3(resolve3(repositoryRoot), ".hermes", "plugins", "codebuddy");
|
|
995
|
-
}
|
|
996
|
-
function plugin(repositoryRoot, variant = "full") {
|
|
997
|
-
const dir = hermesPluginDir(repositoryRoot);
|
|
998
|
-
return {
|
|
999
|
-
marker: HERMES_PLUGIN_MARKER,
|
|
1000
|
-
files: [
|
|
1001
|
-
{
|
|
1002
|
-
path: join3(dir, "plugin.yaml"),
|
|
1003
|
-
content: variant === "lean" ? HERMES_LEAN_MANIFEST : HERMES_PLUGIN_MANIFEST
|
|
1004
|
-
},
|
|
1005
|
-
{ path: join3(dir, "__init__.py"), content: renderHermesPlugin(variant) }
|
|
1006
|
-
],
|
|
1007
|
-
dirs: [dir, dirname4(dir), dirname4(dirname4(dir))]
|
|
1008
|
-
};
|
|
1009
|
-
}
|
|
1010
|
-
function hermesPluginInstalled(repositoryRoot) {
|
|
1011
|
-
return managedPluginInstalled(plugin(repositoryRoot));
|
|
1012
|
-
}
|
|
1013
|
-
function setHermesPlugin(repositoryRoot, enabled, variant = "full") {
|
|
1014
|
-
return setManagedPlugin(plugin(repositoryRoot, variant), enabled);
|
|
1015
|
-
}
|
|
1016
|
-
async function hermesPluginVariant(repositoryRoot) {
|
|
1017
|
-
if (!await hermesPluginInstalled(repositoryRoot)) return null;
|
|
1018
|
-
const source = await readFile3(join3(hermesPluginDir(repositoryRoot), "__init__.py"), "utf8");
|
|
1019
|
-
return source.startsWith(`${HERMES_PLUGIN_MARKER} v1 lean`) ? "lean" : "full";
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
// src/hooks/opencode-plugin.ts
|
|
1023
|
-
import { readFile as readFile4 } from "fs/promises";
|
|
1024
|
-
import { dirname as dirname5, join as join4, resolve as resolve4 } from "path";
|
|
1025
|
-
var OPENCODE_PLUGIN_MARKER = "// codebuddy-managed: opencode-plugin";
|
|
1026
|
-
var OPENCODE_RUNTIME = `const ID_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
1027
|
-
let lastMs = 0;
|
|
1028
|
-
let counter = 0;
|
|
1029
|
-
|
|
1030
|
-
/** Run one fail-soft CodeBuddy hook; resolves to its stdout, "" on any failure. */
|
|
1031
|
-
function hook(command, payload) {
|
|
1032
|
-
return new Promise((done) => {
|
|
1033
|
-
let out = "";
|
|
1034
|
-
let child;
|
|
1035
|
-
try {
|
|
1036
|
-
child = spawn("codebuddy", ["hooks", command], {
|
|
1037
|
-
cwd: payload.cwd,
|
|
1038
|
-
stdio: ["pipe", "pipe", "ignore"],
|
|
1039
|
-
// npm installs a codebuddy.cmd shim on Windows, which spawn only runs through a shell.
|
|
1040
|
-
shell: process.platform === "win32",
|
|
1041
|
-
});
|
|
1042
|
-
} catch {
|
|
1043
|
-
done("");
|
|
1044
|
-
return;
|
|
1045
|
-
}
|
|
1046
|
-
const timer = setTimeout(() => child.kill(), TIMEOUT_MS[command]);
|
|
1047
|
-
child.stdout.setEncoding("utf8");
|
|
1048
|
-
child.stdout.on("data", (chunk) => {
|
|
1049
|
-
out += chunk;
|
|
1050
|
-
});
|
|
1051
|
-
child.on("error", () => {
|
|
1052
|
-
clearTimeout(timer);
|
|
1053
|
-
done("");
|
|
1054
|
-
});
|
|
1055
|
-
child.on("close", (code) => {
|
|
1056
|
-
clearTimeout(timer);
|
|
1057
|
-
done(code === 0 ? out : "");
|
|
1058
|
-
});
|
|
1059
|
-
child.stdin.on("error", () => {});
|
|
1060
|
-
child.stdin.end(JSON.stringify(payload));
|
|
1061
|
-
});
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
/** An ascending OpenCode part id (prt_ + 48-bit time/counter hex + 14 base62). */
|
|
1065
|
-
function partId() {
|
|
1066
|
-
const now = Date.now();
|
|
1067
|
-
if (now !== lastMs) {
|
|
1068
|
-
lastMs = now;
|
|
1069
|
-
counter = 0;
|
|
1070
|
-
}
|
|
1071
|
-
counter += 1;
|
|
1072
|
-
const time = (BigInt(now) * 4096n + BigInt(counter)) & 0xffffffffffffn;
|
|
1073
|
-
let random = "";
|
|
1074
|
-
for (const byte of crypto.getRandomValues(new Uint8Array(14))) random += ID_CHARS[byte % 62];
|
|
1075
|
-
return "prt_" + time.toString(16).padStart(12, "0") + random;
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
`;
|
|
1079
|
-
function renderOpencodePlugin(variant) {
|
|
1080
|
-
return variant === "lean" ? `${OPENCODE_PLUGIN_MARKER} v1 lean
|
|
1081
|
-
// Generated by \`codebuddy clients install opencode --mode lean\`; removed by
|
|
1082
|
-
// \`codebuddy clients uninstall opencode\`. Local edits are overwritten.
|
|
1083
|
-
import { spawn } from "node:child_process";
|
|
1084
|
-
|
|
1085
|
-
const TIMEOUT_MS = { lean: 2000, reset: 10000 };
|
|
1086
|
-
${OPENCODE_RUNTIME}/** @type {import("@opencode-ai/plugin").Plugin} */
|
|
1087
|
-
export const CodeBuddyPlugin = async ({ directory }) => {
|
|
1088
|
-
const base = (sessionID, event) => ({ session_id: sessionID, cwd: directory, hook_event_name: event });
|
|
1089
|
-
|
|
1090
|
-
return {
|
|
1091
|
-
"chat.message": async (input, output) => {
|
|
1092
|
-
const prompt = output.parts
|
|
1093
|
-
.flatMap((part) => (part.type === "text" && !part.synthetic ? [part.text] : []))
|
|
1094
|
-
.join("\\n")
|
|
1095
|
-
.trim();
|
|
1096
|
-
if (!prompt) return;
|
|
1097
|
-
const block = (await hook("lean", { ...base(input.sessionID, "chat.message"), prompt })).trim();
|
|
1098
|
-
if (!block) return;
|
|
1099
|
-
output.parts.push({
|
|
1100
|
-
id: partId(),
|
|
1101
|
-
sessionID: input.sessionID,
|
|
1102
|
-
messageID: output.message.id,
|
|
1103
|
-
type: "text",
|
|
1104
|
-
text: block,
|
|
1105
|
-
synthetic: true,
|
|
1106
|
-
});
|
|
1107
|
-
},
|
|
1108
|
-
event: async ({ event }) => {
|
|
1109
|
-
if (event.type === "session.compacted") {
|
|
1110
|
-
await hook("reset", { ...base(event.properties.sessionID, "session.compacted"), source: "compact" });
|
|
1111
|
-
}
|
|
1112
|
-
},
|
|
1113
|
-
};
|
|
1114
|
-
};
|
|
1115
|
-
` : `${OPENCODE_PLUGIN_MARKER} v1
|
|
1116
|
-
// Generated by \`codebuddy clients install opencode --mode hooks\`; removed by
|
|
1117
|
-
// \`codebuddy clients uninstall opencode\`. Local edits are overwritten.
|
|
1118
|
-
import { spawn } from "node:child_process";
|
|
1119
|
-
|
|
1120
|
-
const EDIT_TOOLS = new Set(["edit", "write", "patch", "apply_patch"]);
|
|
1121
|
-
const TIMEOUT_MS = { context: 20000, stage: 10000, capture: 60000, reset: 10000 };
|
|
1122
|
-
${OPENCODE_RUNTIME}/** @type {import("@opencode-ai/plugin").Plugin} */
|
|
1123
|
-
export const CodeBuddyPlugin = async ({ directory }) => {
|
|
1124
|
-
/** @type {Map<string, { text: string, commands: Array<{ command: string, exit_code: number | null }> }>} */
|
|
1125
|
-
const turns = new Map();
|
|
1126
|
-
const turn = (sessionID) => {
|
|
1127
|
-
if (!turns.has(sessionID)) turns.set(sessionID, { text: "", commands: [] });
|
|
1128
|
-
return turns.get(sessionID);
|
|
1129
|
-
};
|
|
1130
|
-
const base = (sessionID, event) => ({ session_id: sessionID, cwd: directory, hook_event_name: event });
|
|
1131
|
-
|
|
1132
|
-
return {
|
|
1133
|
-
"chat.message": async (input, output) => {
|
|
1134
|
-
turns.delete(input.sessionID);
|
|
1135
|
-
const prompt = output.parts
|
|
1136
|
-
.flatMap((part) => (part.type === "text" && !part.synthetic ? [part.text] : []))
|
|
1137
|
-
.join("\\n")
|
|
1138
|
-
.trim();
|
|
1139
|
-
const block = (
|
|
1140
|
-
await hook("context", { ...base(input.sessionID, "chat.message"), ...(prompt ? { prompt } : {}) })
|
|
1141
|
-
).trim();
|
|
1142
|
-
if (!block) return;
|
|
1143
|
-
output.parts.push({
|
|
1144
|
-
id: partId(),
|
|
1145
|
-
sessionID: input.sessionID,
|
|
1146
|
-
messageID: output.message.id,
|
|
1147
|
-
type: "text",
|
|
1148
|
-
text: block,
|
|
1149
|
-
synthetic: true,
|
|
1150
|
-
});
|
|
1151
|
-
},
|
|
1152
|
-
"tool.execute.after": async (input, output) => {
|
|
1153
|
-
if (EDIT_TOOLS.has(input.tool)) {
|
|
1154
|
-
await hook("stage", {
|
|
1155
|
-
...base(input.sessionID, "tool.execute.after"),
|
|
1156
|
-
tool_name: input.tool,
|
|
1157
|
-
tool_input: input.args,
|
|
1158
|
-
});
|
|
1159
|
-
} else if (input.tool === "bash" && typeof input.args?.command === "string") {
|
|
1160
|
-
const exit = output.metadata?.exit;
|
|
1161
|
-
turn(input.sessionID).commands.push({
|
|
1162
|
-
command: input.args.command,
|
|
1163
|
-
exit_code: typeof exit === "number" ? exit : null,
|
|
1164
|
-
});
|
|
1165
|
-
}
|
|
1166
|
-
},
|
|
1167
|
-
"experimental.text.complete": async (input, output) => {
|
|
1168
|
-
if (output.text.trim()) turn(input.sessionID).text = output.text;
|
|
1169
|
-
},
|
|
1170
|
-
event: async ({ event }) => {
|
|
1171
|
-
if (event.type === "session.idle") {
|
|
1172
|
-
const sessionID = event.properties.sessionID;
|
|
1173
|
-
const current = turns.get(sessionID);
|
|
1174
|
-
turns.delete(sessionID);
|
|
1175
|
-
await hook("capture", {
|
|
1176
|
-
...base(sessionID, "session.idle"),
|
|
1177
|
-
...(current?.text ? { last_assistant_message: current.text } : {}),
|
|
1178
|
-
...(current?.commands.length ? { shell_commands: current.commands.slice(-20) } : {}),
|
|
1179
|
-
});
|
|
1180
|
-
} else if (event.type === "session.compacted") {
|
|
1181
|
-
await hook("reset", { ...base(event.properties.sessionID, "session.compacted"), source: "compact" });
|
|
1182
|
-
}
|
|
1183
|
-
},
|
|
1184
|
-
};
|
|
1185
|
-
};
|
|
1186
|
-
`;
|
|
1187
|
-
}
|
|
1188
|
-
var OPENCODE_PLUGIN_SOURCE = renderOpencodePlugin("full");
|
|
1189
|
-
function opencodePluginPath(repositoryRoot) {
|
|
1190
|
-
return join4(resolve4(repositoryRoot), ".opencode", "plugins", "codebuddy.js");
|
|
1191
|
-
}
|
|
1192
|
-
function plugin2(repositoryRoot, variant = "full") {
|
|
1193
|
-
const path = opencodePluginPath(repositoryRoot);
|
|
1194
|
-
return {
|
|
1195
|
-
marker: OPENCODE_PLUGIN_MARKER,
|
|
1196
|
-
files: [{ path, content: renderOpencodePlugin(variant) }],
|
|
1197
|
-
dirs: [dirname5(path), dirname5(dirname5(path))]
|
|
1198
|
-
};
|
|
1199
|
-
}
|
|
1200
|
-
function opencodePluginInstalled(repositoryRoot) {
|
|
1201
|
-
return managedPluginInstalled(plugin2(repositoryRoot));
|
|
1202
|
-
}
|
|
1203
|
-
function setOpencodePlugin(repositoryRoot, enabled, variant = "full") {
|
|
1204
|
-
return setManagedPlugin(plugin2(repositoryRoot, variant), enabled);
|
|
1205
|
-
}
|
|
1206
|
-
async function opencodePluginVariant(repositoryRoot) {
|
|
1207
|
-
if (!await opencodePluginInstalled(repositoryRoot)) return null;
|
|
1208
|
-
const source = await readFile4(opencodePluginPath(repositoryRoot), "utf8");
|
|
1209
|
-
return source.startsWith(`${OPENCODE_PLUGIN_MARKER} v1 lean`) ? "lean" : "full";
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
// src/hooks/settings.ts
|
|
1213
|
-
import { mkdir as mkdir4, readFile as readFile5 } from "fs/promises";
|
|
1214
|
-
import { dirname as dirname6, join as join5, resolve as resolve5 } from "path";
|
|
1215
|
-
var HOOK_COMMAND_PREFIX = "codebuddy hooks";
|
|
1216
|
-
var TOOL_GUARD_DEF = {
|
|
1217
|
-
event: "PreToolUse",
|
|
1218
|
-
matcher: "Read|Grep|Bash",
|
|
1219
|
-
command: `${HOOK_COMMAND_PREFIX} pretool`
|
|
1220
|
-
};
|
|
1221
|
-
var HOOK_DEFS = [
|
|
1222
|
-
{ event: "UserPromptSubmit", command: `${HOOK_COMMAND_PREFIX} context` },
|
|
1223
|
-
{
|
|
1224
|
-
event: "PostToolUse",
|
|
1225
|
-
matcher: "Edit|Write|MultiEdit",
|
|
1226
|
-
command: `${HOOK_COMMAND_PREFIX} stage`
|
|
1227
|
-
},
|
|
1228
|
-
{ event: "Stop", command: `${HOOK_COMMAND_PREFIX} capture` },
|
|
1229
|
-
// After compaction or /clear the model no longer holds earlier context, so
|
|
1230
|
-
// capsules the session ledger marked as sent must be sent again.
|
|
1231
|
-
{ event: "SessionStart", matcher: "compact|clear", command: `${HOOK_COMMAND_PREFIX} reset` },
|
|
1232
|
-
TOOL_GUARD_DEF
|
|
1233
|
-
];
|
|
1234
|
-
var LEAN_HOOK_DEFS = [
|
|
1235
|
-
{ event: "UserPromptSubmit", command: `${HOOK_COMMAND_PREFIX} lean` },
|
|
1236
|
-
{ event: "SessionStart", matcher: "compact|clear", command: `${HOOK_COMMAND_PREFIX} reset` },
|
|
1237
|
-
TOOL_GUARD_DEF
|
|
1238
|
-
];
|
|
1239
|
-
function claudeSettingsPath(repositoryRoot = process.cwd()) {
|
|
1240
|
-
return join5(resolve5(repositoryRoot), ".claude", "settings.json");
|
|
1241
|
-
}
|
|
1242
|
-
function upsertHooks(settings, variant = "full") {
|
|
1243
|
-
const defs = variant === "lean" ? LEAN_HOOK_DEFS : HOOK_DEFS;
|
|
1244
|
-
const wanted = new Set(defs.map((def) => def.command));
|
|
1245
|
-
const next = { ...settings, hooks: { ...settings.hooks ?? {} } };
|
|
1246
|
-
const hooks = next.hooks;
|
|
1247
|
-
let changed = false;
|
|
1248
|
-
for (const event of Object.keys(hooks)) {
|
|
1249
|
-
const groups = [];
|
|
1250
|
-
for (const group of hooks[event] ?? []) {
|
|
1251
|
-
const kept = (group.hooks ?? []).filter(
|
|
1252
|
-
(hook) => !hook.command?.startsWith(HOOK_COMMAND_PREFIX) || wanted.has(hook.command)
|
|
1253
|
-
);
|
|
1254
|
-
if (kept.length !== (group.hooks ?? []).length) changed = true;
|
|
1255
|
-
if (kept.length > 0) groups.push({ ...group, hooks: kept });
|
|
1256
|
-
}
|
|
1257
|
-
if (groups.length > 0) hooks[event] = groups;
|
|
1258
|
-
else delete hooks[event];
|
|
1259
|
-
}
|
|
1260
|
-
for (const def of defs) {
|
|
1261
|
-
const groups = [...hooks[def.event] ?? []];
|
|
1262
|
-
const existingIndex = groups.findIndex(
|
|
1263
|
-
(group) => group.hooks?.some((hook) => hook.command === def.command)
|
|
1264
|
-
);
|
|
1265
|
-
if (existingIndex === -1) {
|
|
1266
|
-
groups.push({
|
|
1267
|
-
...def.matcher ? { matcher: def.matcher } : {},
|
|
1268
|
-
hooks: [{ type: "command", command: def.command }]
|
|
1269
|
-
});
|
|
1270
|
-
changed = true;
|
|
1271
|
-
} else {
|
|
1272
|
-
const existing = groups[existingIndex];
|
|
1273
|
-
if (existing && (def.matcher ?? void 0) !== (existing.matcher ?? void 0)) {
|
|
1274
|
-
const { matcher: _oldMatcher, ...rest } = existing;
|
|
1275
|
-
groups[existingIndex] = def.matcher ? { ...rest, matcher: def.matcher } : rest;
|
|
1276
|
-
changed = true;
|
|
1277
|
-
}
|
|
1278
|
-
}
|
|
1279
|
-
hooks[def.event] = groups;
|
|
1280
|
-
}
|
|
1281
|
-
return { settings: next, changed };
|
|
1282
|
-
}
|
|
1283
|
-
function removeHooks(settings) {
|
|
1284
|
-
if (!settings.hooks) return { settings, changed: false };
|
|
1285
|
-
const next = { ...settings, hooks: { ...settings.hooks } };
|
|
1286
|
-
const hooks = next.hooks;
|
|
1287
|
-
let changed = false;
|
|
1288
|
-
for (const event of Object.keys(hooks)) {
|
|
1289
|
-
const keptGroups = [];
|
|
1290
|
-
for (const group of hooks[event] ?? []) {
|
|
1291
|
-
const keptHooks = (group.hooks ?? []).filter(
|
|
1292
|
-
(hook) => !hook.command?.startsWith(HOOK_COMMAND_PREFIX)
|
|
1293
|
-
);
|
|
1294
|
-
if (keptHooks.length !== (group.hooks ?? []).length) changed = true;
|
|
1295
|
-
if (keptHooks.length === 0) continue;
|
|
1296
|
-
keptGroups.push({ ...group, hooks: keptHooks });
|
|
1297
|
-
}
|
|
1298
|
-
if (keptGroups.length === 0) delete hooks[event];
|
|
1299
|
-
else hooks[event] = keptGroups;
|
|
1300
|
-
}
|
|
1301
|
-
return { settings: next, changed };
|
|
1302
|
-
}
|
|
1303
|
-
function hooksInstalled(settings) {
|
|
1304
|
-
const hooks = settings.hooks ?? {};
|
|
1305
|
-
return Object.values(hooks).some(
|
|
1306
|
-
(groups) => groups.some((group) => group.hooks?.some((h) => h.command?.startsWith(HOOK_COMMAND_PREFIX)))
|
|
1307
|
-
);
|
|
1308
|
-
}
|
|
1309
|
-
function hooksVariant(settings) {
|
|
1310
|
-
if (!hooksInstalled(settings)) return null;
|
|
1311
|
-
const leanCommand = `${HOOK_COMMAND_PREFIX} lean`;
|
|
1312
|
-
const lean = Object.values(settings.hooks ?? {}).some(
|
|
1313
|
-
(groups) => groups.some((group) => group.hooks?.some((hook) => hook.command === leanCommand))
|
|
1314
|
-
);
|
|
1315
|
-
return lean ? "lean" : "full";
|
|
1316
|
-
}
|
|
1317
|
-
async function readSettings(path) {
|
|
1318
|
-
try {
|
|
1319
|
-
return JSON.parse(await readFile5(path, "utf8"));
|
|
1320
|
-
} catch (error) {
|
|
1321
|
-
if (error.code === "ENOENT") return {};
|
|
1322
|
-
throw error;
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
async function writeSettings(path, settings) {
|
|
1326
|
-
await mkdir4(dirname6(path), { recursive: true });
|
|
1327
|
-
await writeAtomic(path, `${JSON.stringify(settings, null, 2)}
|
|
1328
|
-
`, { mode: 420 });
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
// src/hooks/install.ts
|
|
1332
|
-
var SURFACES = {
|
|
1333
|
-
"claude-settings": {
|
|
1334
|
-
path: claudeSettingsPath,
|
|
1335
|
-
installed: async (root) => hooksInstalled(await readSettings(claudeSettingsPath(root))),
|
|
1336
|
-
variant: async (root) => hooksVariant(await readSettings(claudeSettingsPath(root))),
|
|
1337
|
-
set: async (root, enabled, variant) => {
|
|
1338
|
-
const path = claudeSettingsPath(root);
|
|
1339
|
-
const current = await readSettings(path);
|
|
1340
|
-
const { settings, changed } = enabled ? upsertHooks(current, variant) : removeHooks(current);
|
|
1341
|
-
if (changed) await writeSettings(path, settings);
|
|
1342
|
-
return changed;
|
|
1343
|
-
}
|
|
1344
|
-
},
|
|
1345
|
-
"opencode-plugin": {
|
|
1346
|
-
path: opencodePluginPath,
|
|
1347
|
-
installed: opencodePluginInstalled,
|
|
1348
|
-
variant: opencodePluginVariant,
|
|
1349
|
-
set: setOpencodePlugin
|
|
1350
|
-
},
|
|
1351
|
-
"hermes-plugin": {
|
|
1352
|
-
path: hermesPluginDir,
|
|
1353
|
-
installed: hermesPluginInstalled,
|
|
1354
|
-
variant: hermesPluginVariant,
|
|
1355
|
-
set: setHermesPlugin
|
|
1356
|
-
}
|
|
1357
|
-
};
|
|
1358
|
-
async function setClientHooks(root, client, enabled, variant = "full") {
|
|
1359
|
-
const surface = CLIENT_ADAPTERS[client].hooks;
|
|
1360
|
-
if (!surface) return null;
|
|
1361
|
-
const { path, set } = SURFACES[surface];
|
|
1362
|
-
return { target: path(root), changed: await set(root, enabled, variant) };
|
|
1363
|
-
}
|
|
1364
|
-
async function clientHooksVariant(root, client) {
|
|
1365
|
-
const surface = CLIENT_ADAPTERS[client].hooks;
|
|
1366
|
-
if (!surface) return void 0;
|
|
1367
|
-
return SURFACES[surface].variant(root);
|
|
1368
|
-
}
|
|
1369
|
-
async function clientHooksInstalled(root, client) {
|
|
1370
|
-
const surface = CLIENT_ADAPTERS[client].hooks;
|
|
1371
|
-
return surface ? SURFACES[surface].installed(root) : null;
|
|
1372
|
-
}
|
|
1373
|
-
async function hookedRulesSiblings(root, client) {
|
|
1374
|
-
const file = CLIENT_ADAPTERS[client].rulesFile;
|
|
1375
|
-
const hooked = [];
|
|
1376
|
-
for (const id of CLIENT_IDS) {
|
|
1377
|
-
if (id !== client && CLIENT_ADAPTERS[id].rulesFile === file && await clientHooksInstalled(root, id))
|
|
1378
|
-
hooked.push(id);
|
|
1379
|
-
}
|
|
1380
|
-
return hooked;
|
|
1381
|
-
}
|
|
1382
|
-
async function rulesFileHooked(root, client) {
|
|
1383
|
-
return await clientHooksInstalled(root, client) === true || (await hookedRulesSiblings(root, client)).length > 0;
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
// src/templates/workflow.ts
|
|
1387
|
-
var WORKFLOW_CLIENTS = CLIENT_IDS;
|
|
1388
|
-
var INTRO = [
|
|
1389
|
-
"You have CodeBuddy available as a local, MCP-backed project-context engine.",
|
|
1390
|
-
"Use it to work like a careful senior engineer, not to dump broad context.",
|
|
1391
|
-
""
|
|
1392
|
-
];
|
|
1393
|
-
var START_AGENT = [
|
|
1394
|
-
"## At the start of a substantive coding turn",
|
|
1395
|
-
"",
|
|
1396
|
-
"1. When project context is needed, call `context_pack` with the user's task and a token budget (start",
|
|
1397
|
-
" with 4000). It automatically finds relevant files and returns bounded symbols,",
|
|
1398
|
-
" plan, policy, incidents, risks, architecture neighbours, and team facts.",
|
|
1399
|
-
" Reuse evidence already present. For a clearly identified small edit, read the",
|
|
1400
|
-
" needed source directly; fetch context for unknown dependencies or project constraints.",
|
|
1401
|
-
" Read missing function bodies before editing. If safety evidence was omitted,",
|
|
1402
|
-
" narrow the targets or increase the budget before proceeding.",
|
|
1403
|
-
"2. Use `context_bootstrap` only when you need broad project orientation beyond",
|
|
1404
|
-
" the task pack. Use `context_before_edit` when you already have exact paths",
|
|
1405
|
-
" or a plan and need a narrower follow-up.",
|
|
1406
|
-
"3. Prior work from earlier chats arrives as `handoffs` with freshness",
|
|
1407
|
-
" (`changedSince`, `missing`); re-check those files before relying on it.",
|
|
1408
|
-
"4. On repeated context calls in one conversation, pass a stable `sessionId` and",
|
|
1409
|
-
" the previous response's `delivery.deliveryId` as `ack`. Unchanged sections",
|
|
1410
|
-
" are then omitted and listed in `delivery.unchanged`. If earlier output is no",
|
|
1411
|
-
" longer in your context (e.g. after compaction), pass `full: true`.",
|
|
1412
|
-
""
|
|
1413
|
-
];
|
|
1414
|
-
var START_HOOKS = [
|
|
1415
|
-
"## At the start of a substantive coding turn",
|
|
1416
|
-
"",
|
|
1417
|
-
"CodeBuddy hooks inject a `CodeBuddy project context (auto-recalled)` block into",
|
|
1418
|
-
"each prompt: project, plan, policies, facts, incidents, handoffs, current",
|
|
1419
|
-
"changes, and risk. Agents without CodeBuddy hooks (no such block in the prompt)",
|
|
1420
|
-
"follow the agent-driven flow: `context_pack` first, `context_after_turn` last.",
|
|
1421
|
-
"",
|
|
1422
|
-
"1. Start from the injected block; do not call `context_bootstrap` for orientation",
|
|
1423
|
-
" it already covers. Its `task files` are CodeBuddy's best match; when it names",
|
|
1424
|
-
" none, call `context_pack` with the task before searching the repository yourself.",
|
|
1425
|
-
"2. Read ranges, not whole files: `Read` with offset/limit, or `sed -n 'A,Bp'`.",
|
|
1426
|
-
" Large files read whole (`Read` without a range, `cat`, `less`) are denied with an",
|
|
1427
|
-
" outline of their symbols and line ranges; read the range you need from it.",
|
|
1428
|
-
"3. Call `context_pack` (or `context_before_edit` with exact paths) when the",
|
|
1429
|
-
" task needs symbols, neighbours, or file-level evidence the block lacks. Pass the",
|
|
1430
|
-
" block's `session` value as `sessionId` and the previous `delivery.deliveryId`",
|
|
1431
|
-
" as `ack` so unchanged sections are not resent.",
|
|
1432
|
-
"4. Prior work from earlier chats arrives as handoffs with freshness; re-check",
|
|
1433
|
-
" those files before relying on it.",
|
|
1434
|
-
""
|
|
1435
|
-
];
|
|
1436
|
-
var MIDDLE = [
|
|
1437
|
-
"## While implementing",
|
|
1438
|
-
"",
|
|
1439
|
-
"- Prefer the smallest sufficient set of edits; preserve existing behavior.",
|
|
1440
|
-
"- Honor risk warnings and policy rules for the files you touch; call out risk",
|
|
1441
|
-
" before editing sensitive areas.",
|
|
1442
|
-
"- Do not re-request broad context if the task pack already suffices.",
|
|
1443
|
-
"",
|
|
1444
|
-
"## Before finalizing",
|
|
1445
|
-
"",
|
|
1446
|
-
"- Consider calling `code_suggestions` (with `paths`, `planId`, or `useGit`) for",
|
|
1447
|
-
" read-only, evidence-backed quality findings; resolve high-severity ones.",
|
|
1448
|
-
"- Run the smallest relevant verification (tests, lint, typecheck) and fix",
|
|
1449
|
-
" failures before presenting completion.",
|
|
1450
|
-
""
|
|
1451
|
-
];
|
|
1452
|
-
var AFTER_AGENT = [
|
|
1453
|
-
"## After a meaningful turn",
|
|
1454
|
-
"",
|
|
1455
|
-
"- Call `context_after_turn` with a concise `summary`, the `changedFiles`, and a",
|
|
1456
|
-
" `planId` if one is active, plus `verification` (commands run and results)",
|
|
1457
|
-
" so the turn's worklog handoff records it. CodeBuddy auto-saves confident facts, decisions,",
|
|
1458
|
-
" and incidents, queues uncertain or sensitive items for review, and drops",
|
|
1459
|
-
" chatter. Never paste secrets, tokens, or credentials into the summary.",
|
|
1460
|
-
""
|
|
1461
|
-
];
|
|
1462
|
-
var AFTER_HOOKS = [
|
|
1463
|
-
"## After a meaningful turn",
|
|
1464
|
-
"",
|
|
1465
|
-
"- Do not call `context_after_turn`: the turn-end hook captures the turn once, from",
|
|
1466
|
-
" the real change set and the verification commands you ran. A second capture",
|
|
1467
|
-
" would record a divergent worklog entry.",
|
|
1468
|
-
"- Never paste secrets, tokens, or credentials into your final message; it is the",
|
|
1469
|
-
" capture summary.",
|
|
1470
|
-
""
|
|
1471
|
-
];
|
|
1472
|
-
var TAIL = [
|
|
1473
|
-
"## If a tool or service is unavailable (degraded mode)",
|
|
1474
|
-
"",
|
|
1475
|
-
"- A CodeBuddy MCP tool errors or is missing: proceed using the repository",
|
|
1476
|
-
" itself as the source of truth; do not block on it.",
|
|
1477
|
-
"- Postgres is unreachable: the file-based tools (context, plan, risk, map,",
|
|
1478
|
-
" suggest) still work; recall and embeddings may be degraded \u2014 say so instead",
|
|
1479
|
-
" of inventing remembered facts.",
|
|
1480
|
-
"- Retrieval returns no context: read the relevant files directly rather than",
|
|
1481
|
-
" guessing; the working tree is authoritative.",
|
|
1482
|
-
"",
|
|
1483
|
-
"## Never",
|
|
1484
|
-
"",
|
|
1485
|
-
"- Invent behavior the codebase does not support.",
|
|
1486
|
-
"- Ignore failing tests or lint.",
|
|
1487
|
-
"- Make speculative, wide changes without evidence.",
|
|
1488
|
-
"- Save low-confidence project memory as a durable fact."
|
|
1489
|
-
];
|
|
1490
|
-
function renderBody(mode) {
|
|
1491
|
-
return [
|
|
1492
|
-
...INTRO,
|
|
1493
|
-
...mode === "hooks" ? START_HOOKS : START_AGENT,
|
|
1494
|
-
...MIDDLE,
|
|
1495
|
-
...mode === "hooks" ? AFTER_HOOKS : AFTER_AGENT,
|
|
1496
|
-
...TAIL
|
|
1497
|
-
].join("\n");
|
|
1498
|
-
}
|
|
1499
|
-
function rulesReaders(file) {
|
|
1500
|
-
return CLIENT_IDS.filter((id) => CLIENT_ADAPTERS[id].rulesFile === file).map((id) => CLIENT_ADAPTERS[id].name).join(", ");
|
|
1501
|
-
}
|
|
1502
|
-
function renderWorkflowTemplate(client, mode = "agent") {
|
|
1503
|
-
const file = CLIENT_ADAPTERS[client].rulesFile;
|
|
1504
|
-
return [
|
|
1505
|
-
`# CodeBuddy workflow (${rulesReaders(file)})`,
|
|
1506
|
-
"",
|
|
1507
|
-
"This section is managed by CodeBuddy (`codebuddy clients install <client> --mode <mode>`)",
|
|
1508
|
-
`and lives in \`${file}\`. Edit outside the managed markers to customize.`,
|
|
1509
|
-
`Automation mode: \`${mode}\` (${MODE_LABELS[mode]}).`,
|
|
1510
|
-
"",
|
|
1511
|
-
renderBody(mode)
|
|
1512
|
-
].join("\n");
|
|
1513
|
-
}
|
|
1514
|
-
function parseWorkflowMode(block) {
|
|
1515
|
-
return block.match(/Automation mode: `(hooks|agent)`/)?.[1] === "hooks" ? "hooks" : "agent";
|
|
1516
|
-
}
|
|
1517
|
-
function defaultRulesFile(client) {
|
|
1518
|
-
return CLIENT_ADAPTERS[client].rulesFile;
|
|
1519
|
-
}
|
|
1520
|
-
function clientDisplayName(client) {
|
|
1521
|
-
return CLIENT_ADAPTERS[client].name;
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
// src/templates/install.ts
|
|
1525
|
-
var START_MARKER = "<!-- codebuddy:workflow:start -->";
|
|
1526
|
-
var END_MARKER = "<!-- codebuddy:workflow:end -->";
|
|
1527
|
-
function renderManagedBlock(client, mode = "agent") {
|
|
1528
|
-
return `${START_MARKER}
|
|
1529
|
-
${renderWorkflowTemplate(client, mode)}
|
|
1530
|
-
${END_MARKER}
|
|
1531
|
-
`;
|
|
1532
|
-
}
|
|
1533
|
-
async function installWorkflowRules(options) {
|
|
1534
|
-
const projectRoot = resolve6(options.projectRoot ?? process.cwd());
|
|
1535
|
-
const path = options.targetPath ?? join6(projectRoot, defaultRulesFile(options.client));
|
|
1536
|
-
const existing = await readIfPresent(path);
|
|
1537
|
-
const mode = options.mode ?? await defaultRulesMode(options.client, projectRoot);
|
|
1538
|
-
if (options.onlyIfManaged && (existing === null || !hasManagedBlock(existing))) {
|
|
1539
|
-
return { path, client: options.client, action: "unchanged" };
|
|
1540
|
-
}
|
|
1541
|
-
if (mode === "mcp-only") {
|
|
1542
|
-
const content2 = existing === null ? null : removeBlock(existing);
|
|
1543
|
-
if (content2 === null || content2 === existing) {
|
|
1544
|
-
return { path, client: options.client, action: "unchanged" };
|
|
1545
|
-
}
|
|
1546
|
-
if (content2.trim() === "") await rm2(path, { force: true });
|
|
1547
|
-
else await writeAtomic(path, content2, { mode: 420 });
|
|
1548
|
-
return { path, client: options.client, action: "removed" };
|
|
1549
|
-
}
|
|
1550
|
-
const { content, action } = upsertBlock(existing, renderManagedBlock(options.client, mode));
|
|
1551
|
-
if (action === "unchanged") return { path, client: options.client, action };
|
|
1552
|
-
await writeAtomic(path, content, { mode: 420 });
|
|
1553
|
-
return { path, client: options.client, action };
|
|
1554
|
-
}
|
|
1555
|
-
function upsertBlock(existing, block) {
|
|
1556
|
-
if (existing === null) return { content: block, action: "created" };
|
|
1557
|
-
const start = existing.indexOf(START_MARKER);
|
|
1558
|
-
const end = existing.indexOf(END_MARKER);
|
|
1559
|
-
if (start !== -1 && end !== -1 && end > start) {
|
|
1560
|
-
const before = existing.slice(0, start);
|
|
1561
|
-
const after = existing.slice(end + END_MARKER.length).replace(/^\n/, "");
|
|
1562
|
-
const replaced = `${before}${block}${after}`;
|
|
1563
|
-
return { content: replaced, action: replaced === existing ? "unchanged" : "updated" };
|
|
1564
|
-
}
|
|
1565
|
-
const separator = existing.length === 0 || existing.endsWith("\n\n") ? "" : existing.endsWith("\n") ? "\n" : "\n\n";
|
|
1566
|
-
return { content: `${existing}${separator}${block}`, action: "added" };
|
|
1567
|
-
}
|
|
1568
|
-
async function defaultRulesMode(client, projectRoot = process.cwd()) {
|
|
1569
|
-
return await rulesFileHooked(projectRoot, client) ? "hooks" : "agent";
|
|
1570
|
-
}
|
|
1571
|
-
async function readManagedMode(path) {
|
|
1572
|
-
const existing = await readIfPresent(path);
|
|
1573
|
-
if (existing === null || !hasManagedBlock(existing)) return null;
|
|
1574
|
-
return parseWorkflowMode(existing.slice(existing.indexOf(START_MARKER)));
|
|
1575
|
-
}
|
|
1576
|
-
function hasManagedBlock(content) {
|
|
1577
|
-
const start = content.indexOf(START_MARKER);
|
|
1578
|
-
return start !== -1 && content.indexOf(END_MARKER) > start;
|
|
1579
|
-
}
|
|
1580
|
-
function removeBlock(existing) {
|
|
1581
|
-
if (!hasManagedBlock(existing)) return existing;
|
|
1582
|
-
const start = existing.indexOf(START_MARKER);
|
|
1583
|
-
const end = existing.indexOf(END_MARKER) + END_MARKER.length;
|
|
1584
|
-
const before = existing.slice(0, start).replace(/\n\n$/, "\n");
|
|
1585
|
-
const after = existing.slice(end).replace(/^\n/, "");
|
|
1586
|
-
return `${before}${after}`;
|
|
1587
|
-
}
|
|
1588
|
-
async function readIfPresent(path) {
|
|
1589
|
-
try {
|
|
1590
|
-
return await readFile6(path, "utf8");
|
|
1591
|
-
} catch (error) {
|
|
1592
|
-
if (error.code === "ENOENT") return null;
|
|
1593
|
-
throw error;
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
|
-
|
|
1597
|
-
// src/clients/cost.ts
|
|
1598
640
|
async function mcpSchemaTokens(root, namespace) {
|
|
1599
641
|
const server = new McpServer({ name: "codebuddy", version: VERSION });
|
|
1600
642
|
registerCodeBuddyTools(server, {
|
|
@@ -1624,7 +666,7 @@ async function hookBlockTokens(root, namespace) {
|
|
|
1624
666
|
namespace,
|
|
1625
667
|
useGit: true
|
|
1626
668
|
}).catch(() => null);
|
|
1627
|
-
const scratch = await mkdtemp(
|
|
669
|
+
const scratch = await mkdtemp(join2(tmpdir(), "codebuddy-cost-"));
|
|
1628
670
|
try {
|
|
1629
671
|
const ledger = new SessionCapsuleLedger(scratch);
|
|
1630
672
|
const render = () => renderContextBlock(bootstrap, beforeEdit, ledger, "cost", true);
|
|
@@ -1636,546 +678,50 @@ async function hookBlockTokens(root, namespace) {
|
|
|
1636
678
|
const leanFirst = await renderLean();
|
|
1637
679
|
await ledger.markSent("lean", leanFirst.emitted);
|
|
1638
680
|
const leanLater = await renderLean();
|
|
1639
|
-
return {
|
|
1640
|
-
first: estimateTokens(first.text),
|
|
1641
|
-
later: estimateTokens(later.text),
|
|
1642
|
-
lean: { first: estimateTokens(leanFirst.text), later: estimateTokens(leanLater.text) }
|
|
1643
|
-
};
|
|
1644
|
-
} finally {
|
|
1645
|
-
await
|
|
1646
|
-
}
|
|
1647
|
-
}
|
|
1648
|
-
async function measureClientCosts(projectRoot = process.cwd(), namespace) {
|
|
1649
|
-
const root = resolve7(projectRoot);
|
|
1650
|
-
const ns = namespace ?? await resolveNamespaceFrom(root);
|
|
1651
|
-
const mcpSchema = await mcpSchemaTokens(root, ns);
|
|
1652
|
-
const block = await hookBlockTokens(root, ns);
|
|
1653
|
-
return CLIENT_IDS.flatMap((client) => {
|
|
1654
|
-
const adapter = CLIENT_ADAPTERS[client];
|
|
1655
|
-
return supportedModes(adapter).flatMap((mode) => {
|
|
1656
|
-
if (mode === "mcp-only") return [];
|
|
1657
|
-
if (mode === "lean") {
|
|
1658
|
-
const later2 = adapter.ephemeralContext ? block.lean.first : block.lean.later;
|
|
1659
|
-
return [
|
|
1660
|
-
{ client, mode, rules: 0, mcpSchema: 0, firstBlock: block.lean.first, laterBlock: later2 }
|
|
1661
|
-
];
|
|
1662
|
-
}
|
|
1663
|
-
const hooks = mode === "hooks";
|
|
1664
|
-
const later = adapter.ephemeralContext ? block.first : block.later;
|
|
1665
|
-
return [
|
|
1666
|
-
{
|
|
1667
|
-
client,
|
|
1668
|
-
mode,
|
|
1669
|
-
rules: estimateTokens(renderManagedBlock(client, mode)),
|
|
1670
|
-
mcpSchema: adapter.defersMcpSchemas ? mcpSchema.listing : mcpSchema.full,
|
|
1671
|
-
firstBlock: hooks ? block.first : 0,
|
|
1672
|
-
laterBlock: hooks ? later : 0
|
|
1673
|
-
}
|
|
1674
|
-
];
|
|
1675
|
-
});
|
|
1676
|
-
});
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
|
-
// src/clients/install.ts
|
|
1680
|
-
import { access as access3 } from "fs/promises";
|
|
1681
|
-
import { join as join11, resolve as resolve9 } from "path";
|
|
1682
|
-
|
|
1683
|
-
// src/integrations/project-mcp.ts
|
|
1684
|
-
import { access as access2, mkdir as mkdir5, readFile as readFile7, rm as rm4 } from "fs/promises";
|
|
1685
|
-
import { dirname as dirname7, join as join8, resolve as resolve8 } from "path";
|
|
1686
|
-
async function writeProjectMcpConfig(options) {
|
|
1687
|
-
const path = join8(options.repositoryRoot, ".mcp.json");
|
|
1688
|
-
const current = await readProjectMcpConfig(path);
|
|
1689
|
-
current.mcpServers ??= {};
|
|
1690
|
-
current.mcpServers[`codebuddy-${options.namespace}`] = codebuddyMcpServer(
|
|
1691
|
-
options.repositoryRoot,
|
|
1692
|
-
options.namespace
|
|
1693
|
-
);
|
|
1694
|
-
if (options.graphify) {
|
|
1695
|
-
current.mcpServers.graphify = {
|
|
1696
|
-
command: "python",
|
|
1697
|
-
args: [
|
|
1698
|
-
"-m",
|
|
1699
|
-
"graphify.serve",
|
|
1700
|
-
resolve8(options.repositoryRoot, options.graphPath ?? "graphify-out/graph.json")
|
|
1701
|
-
],
|
|
1702
|
-
env: { CODEBUDDY_PROJECT_ROOT: options.repositoryRoot }
|
|
1703
|
-
};
|
|
1704
|
-
} else if (current.mcpServers.graphify) {
|
|
1705
|
-
delete current.mcpServers.graphify;
|
|
1706
|
-
}
|
|
1707
|
-
await writeAtomic(path, `${JSON.stringify(current, null, 2)}
|
|
1708
|
-
`, { mode: 420 });
|
|
1709
|
-
return { path, servers: Object.keys(current.mcpServers).sort() };
|
|
1710
|
-
}
|
|
1711
|
-
var OPENCODE_SCHEMA = "https://opencode.ai/config.json";
|
|
1712
|
-
function codebuddyMcpServer(repositoryRoot, namespace) {
|
|
1713
|
-
return {
|
|
1714
|
-
command: "codebuddy",
|
|
1715
|
-
args: ["serve"],
|
|
1716
|
-
env: {
|
|
1717
|
-
CODEBUDDY_PROJECT_ROOT: repositoryRoot,
|
|
1718
|
-
CODEBUDDY_NAMESPACE: namespace,
|
|
1719
|
-
CODEBUDDY_TOOL_PROFILE: "core"
|
|
1720
|
-
}
|
|
1721
|
-
};
|
|
1722
|
-
}
|
|
1723
|
-
async function projectMcpPath(root, file, format) {
|
|
1724
|
-
const path = join8(root, file);
|
|
1725
|
-
if (format !== "opencode" || await exists(path)) return path;
|
|
1726
|
-
return await exists(`${path}c`) ? `${path}c` : path;
|
|
1727
|
-
}
|
|
1728
|
-
async function exists(path) {
|
|
1729
|
-
return access2(path).then(
|
|
1730
|
-
() => true,
|
|
1731
|
-
() => false
|
|
1732
|
-
);
|
|
1733
|
-
}
|
|
1734
|
-
function formatEntry(format, server) {
|
|
1735
|
-
if (format === "mcpServers") return server;
|
|
1736
|
-
return {
|
|
1737
|
-
type: "local",
|
|
1738
|
-
command: [server.command, ...server.args],
|
|
1739
|
-
...server.env ? { environment: server.env } : {},
|
|
1740
|
-
enabled: true
|
|
1741
|
-
};
|
|
1742
|
-
}
|
|
1743
|
-
function serversKey(format) {
|
|
1744
|
-
return format === "opencode" ? "mcp" : "mcpServers";
|
|
1745
|
-
}
|
|
1746
|
-
async function upsertMcpServer(path, format, name, server) {
|
|
1747
|
-
const existed = await exists(path);
|
|
1748
|
-
const { config, comments } = await readConfig(path);
|
|
1749
|
-
const key = serversKey(format);
|
|
1750
|
-
const servers2 = { ...config[key] ?? {} };
|
|
1751
|
-
const entry = formatEntry(format, server);
|
|
1752
|
-
if (JSON.stringify(servers2[name]) === JSON.stringify(entry)) return "unchanged";
|
|
1753
|
-
if (comments) return "manual";
|
|
1754
|
-
servers2[name] = entry;
|
|
1755
|
-
const next = format === "opencode" && !existed ? { $schema: OPENCODE_SCHEMA, ...config, [key]: servers2 } : { ...config, [key]: servers2 };
|
|
1756
|
-
await mkdir5(dirname7(path), { recursive: true });
|
|
1757
|
-
await writeAtomic(path, `${JSON.stringify(next, null, 2)}
|
|
1758
|
-
`, { mode: 420 });
|
|
1759
|
-
return existed ? "updated" : "created";
|
|
1760
|
-
}
|
|
1761
|
-
async function removeMcpServer(path, format, name) {
|
|
1762
|
-
const { config, comments } = await readConfig(path);
|
|
1763
|
-
const key = serversKey(format);
|
|
1764
|
-
const servers2 = { ...config[key] ?? {} };
|
|
1765
|
-
if (!(name in servers2)) return "unchanged";
|
|
1766
|
-
if (comments) return "manual";
|
|
1767
|
-
delete servers2[name];
|
|
1768
|
-
const next = { ...config, [key]: servers2 };
|
|
1769
|
-
const leftover = Object.keys(next).filter((k) => k !== key && k !== "$schema");
|
|
1770
|
-
if (Object.keys(servers2).length === 0 && leftover.length === 0) await rm4(path, { force: true });
|
|
1771
|
-
else await writeAtomic(path, `${JSON.stringify(next, null, 2)}
|
|
1772
|
-
`, { mode: 420 });
|
|
1773
|
-
return "removed";
|
|
1774
|
-
}
|
|
1775
|
-
async function hasMcpServer(path, format, name) {
|
|
1776
|
-
const { config } = await readConfig(path);
|
|
1777
|
-
return name in (config[serversKey(format)] ?? {});
|
|
1778
|
-
}
|
|
1779
|
-
function parseJsonc(raw) {
|
|
1780
|
-
let comments = false;
|
|
1781
|
-
const json2 = raw.replace(/"(?:[^"\\]|\\.)*"|\/\/[^\n]*|\/\*[\s\S]*?\*\//g, (match) => {
|
|
1782
|
-
if (match.startsWith('"')) return match;
|
|
1783
|
-
comments = true;
|
|
1784
|
-
return " ";
|
|
1785
|
-
}).replace(/"(?:[^"\\]|\\.)*"|,(\s*[}\]])/g, (match, tail) => tail ?? match);
|
|
1786
|
-
return { value: JSON.parse(json2), comments };
|
|
1787
|
-
}
|
|
1788
|
-
async function readConfig(path) {
|
|
1789
|
-
try {
|
|
1790
|
-
const { value, comments } = parseJsonc(await readFile7(path, "utf8"));
|
|
1791
|
-
const config = value && typeof value === "object" ? value : {};
|
|
1792
|
-
return { config, comments };
|
|
1793
|
-
} catch (error) {
|
|
1794
|
-
if (error.code === "ENOENT") return { config: {}, comments: false };
|
|
1795
|
-
throw new Error(`Could not read ${path}: ${error.message}`);
|
|
1796
|
-
}
|
|
1797
|
-
}
|
|
1798
|
-
async function readProjectMcpConfig(path) {
|
|
1799
|
-
return (await readConfig(path)).config;
|
|
1800
|
-
}
|
|
1801
|
-
|
|
1802
|
-
// src/cli/commands/codex.ts
|
|
1803
|
-
import { existsSync as existsSync2 } from "fs";
|
|
1804
|
-
import { mkdir as mkdir6, readFile as readFile8 } from "fs/promises";
|
|
1805
|
-
import { homedir } from "os";
|
|
1806
|
-
import { dirname as dirname8, join as join9 } from "path";
|
|
1807
|
-
function codexConfigPath(home = homedir()) {
|
|
1808
|
-
return join9(home, ".codex", "config.toml");
|
|
1809
|
-
}
|
|
1810
|
-
async function readCodexConfig(path) {
|
|
1811
|
-
if (!existsSync2(path)) return "";
|
|
1812
|
-
return readFile8(path, "utf8");
|
|
1813
|
-
}
|
|
1814
|
-
async function writeCodexConfig(path, content) {
|
|
1815
|
-
await mkdir6(dirname8(path), { recursive: true });
|
|
1816
|
-
await writeAtomic(path, content, { mode: 384 });
|
|
1817
|
-
}
|
|
1818
|
-
async function installCodexEntry(options) {
|
|
1819
|
-
const path = codexConfigPath();
|
|
1820
|
-
const serverKey = options.serverName ?? `codebuddy-${options.namespace}`;
|
|
1821
|
-
const current = await readCodexConfig(path);
|
|
1822
|
-
const existed = hasServerBlock(current, serverKey);
|
|
1823
|
-
const entry = buildEntry(options);
|
|
1824
|
-
if (options.toolProfile)
|
|
1825
|
-
entry.env = { ...entry.env, CODEBUDDY_TOOL_PROFILE: options.toolProfile };
|
|
1826
|
-
const next = upsertServerBlock(current, serverKey, entry);
|
|
1827
|
-
if (next !== current) await writeCodexConfig(path, next);
|
|
1828
|
-
return { path, serverKey, created: !existed, changed: next !== current };
|
|
1829
|
-
}
|
|
1830
|
-
async function removeCodexEntry(serverKey) {
|
|
1831
|
-
const path = codexConfigPath();
|
|
1832
|
-
const current = await readCodexConfig(path);
|
|
1833
|
-
const next = removeServerBlock(current, serverKey);
|
|
1834
|
-
const removed = next !== current;
|
|
1835
|
-
const trimmed = next.trimEnd();
|
|
1836
|
-
if (removed) await writeCodexConfig(path, trimmed ? `${trimmed}
|
|
1837
|
-
` : "");
|
|
1838
|
-
return { removed, path };
|
|
1839
|
-
}
|
|
1840
|
-
async function listCodexEntries() {
|
|
1841
|
-
const path = codexConfigPath();
|
|
1842
|
-
const config = await readCodexConfig(path);
|
|
1843
|
-
const entries = Array.from(config.matchAll(/^\[mcp_servers\."([^"]+)"\]\s*$/gm)).flatMap((match) => match[1] ? [match[1]] : []).filter((key) => key === "codebuddy" || key.startsWith("codebuddy-")).map((key) => {
|
|
1844
|
-
const block = getServerBlock(config, key);
|
|
1845
|
-
return {
|
|
1846
|
-
key,
|
|
1847
|
-
namespace: readEnvValue(block, "CODEBUDDY_NAMESPACE"),
|
|
1848
|
-
databaseUrl: readEnvValue(block, "DATABASE_URL")
|
|
1849
|
-
};
|
|
1850
|
-
});
|
|
1851
|
-
return { path, entries };
|
|
1852
|
-
}
|
|
1853
|
-
function upsertServerBlock(config, serverKey, entry) {
|
|
1854
|
-
const trimmed = removeServerBlock(config, serverKey).trimEnd();
|
|
1855
|
-
const block = formatServerBlock(serverKey, entry);
|
|
1856
|
-
return `${trimmed}${trimmed ? "\n\n" : ""}${block}
|
|
1857
|
-
`;
|
|
1858
|
-
}
|
|
1859
|
-
function removeServerBlock(config, serverKey) {
|
|
1860
|
-
const escaped = escapeRegExp(serverKey);
|
|
1861
|
-
const end = String.raw`(?![\s\S])`;
|
|
1862
|
-
const sectionPattern = String.raw`^\[mcp_servers\."${escaped}"\][\s\S]*?(?=^\[[^\]]+\]\s*$|${end})`;
|
|
1863
|
-
const envPattern = String.raw`^\[mcp_servers\."${escaped}"\.env\][\s\S]*?(?=^\[[^\]]+\]\s*$|${end})`;
|
|
1864
|
-
return config.replace(new RegExp(sectionPattern, "gm"), "").replace(new RegExp(envPattern, "gm"), "");
|
|
1865
|
-
}
|
|
1866
|
-
function getServerBlock(config, serverKey) {
|
|
1867
|
-
const escaped = escapeRegExp(serverKey);
|
|
1868
|
-
const end = String.raw`(?![\s\S])`;
|
|
1869
|
-
const pattern = String.raw`^\[mcp_servers\."${escaped}"\][\s\S]*?(?=^\[mcp_servers\."${escaped}"\.env\][\s\S]*?(?=^\[[^\]]+\]\s*$|${end})|^\[[^\]]+\]\s*$|${end})`;
|
|
1870
|
-
const server = config.match(new RegExp(pattern, "m"))?.[0] ?? "";
|
|
1871
|
-
const env = config.match(
|
|
1872
|
-
new RegExp(
|
|
1873
|
-
String.raw`^\[mcp_servers\."${escaped}"\.env\][\s\S]*?(?=^\[[^\]]+\]\s*$|${end})`,
|
|
1874
|
-
"m"
|
|
1875
|
-
)
|
|
1876
|
-
)?.[0] ?? "";
|
|
1877
|
-
return `${server}
|
|
1878
|
-
${env}`;
|
|
1879
|
-
}
|
|
1880
|
-
function hasServerBlock(config, serverKey) {
|
|
1881
|
-
return new RegExp(String.raw`^\[mcp_servers\."${escapeRegExp(serverKey)}"\]\s*$`, "m").test(
|
|
1882
|
-
config
|
|
1883
|
-
);
|
|
1884
|
-
}
|
|
1885
|
-
function formatServerBlock(serverKey, entry) {
|
|
1886
|
-
const lines = [
|
|
1887
|
-
`[mcp_servers.${tomlKey(serverKey)}]`,
|
|
1888
|
-
`command = ${tomlString(entry.command)}`,
|
|
1889
|
-
`args = ${tomlArray(entry.args)}`
|
|
1890
|
-
];
|
|
1891
|
-
if (entry.env && Object.keys(entry.env).length > 0) {
|
|
1892
|
-
lines.push("", `[mcp_servers.${tomlKey(serverKey)}.env]`);
|
|
1893
|
-
for (const [key, value] of Object.entries(entry.env).sort(
|
|
1894
|
-
([left], [right]) => left.localeCompare(right)
|
|
1895
|
-
)) {
|
|
1896
|
-
lines.push(`${key} = ${tomlString(value)}`);
|
|
1897
|
-
}
|
|
1898
|
-
}
|
|
1899
|
-
return lines.join("\n");
|
|
1900
|
-
}
|
|
1901
|
-
function readEnvValue(block, key) {
|
|
1902
|
-
return block.match(new RegExp(`^${escapeRegExp(key)}\\s*=\\s*"([^"]*)"`, "m"))?.[1] ?? null;
|
|
1903
|
-
}
|
|
1904
|
-
function tomlKey(key) {
|
|
1905
|
-
return `"${key.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
1906
|
-
}
|
|
1907
|
-
function tomlString(value) {
|
|
1908
|
-
return JSON.stringify(value);
|
|
1909
|
-
}
|
|
1910
|
-
function tomlArray(values) {
|
|
1911
|
-
return `[${values.map(tomlString).join(", ")}]`;
|
|
1912
|
-
}
|
|
1913
|
-
function escapeRegExp(value) {
|
|
1914
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1915
|
-
}
|
|
1916
|
-
|
|
1917
|
-
// src/integrations/hermes-config.ts
|
|
1918
|
-
import { mkdir as mkdir7, readFile as readFile9 } from "fs/promises";
|
|
1919
|
-
import { homedir as homedir2 } from "os";
|
|
1920
|
-
import { dirname as dirname9, join as join10 } from "path";
|
|
1921
|
-
import { isDeepStrictEqual } from "util";
|
|
1922
|
-
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
1923
|
-
function hermesConfigPath(home = homedir2()) {
|
|
1924
|
-
return join10(process.env.HERMES_HOME ?? join10(home, ".hermes"), "config.yaml");
|
|
1925
|
-
}
|
|
1926
|
-
async function readRaw(path) {
|
|
1927
|
-
try {
|
|
1928
|
-
return await readFile9(path, "utf8");
|
|
1929
|
-
} catch (error) {
|
|
1930
|
-
if (error.code === "ENOENT") return "";
|
|
1931
|
-
throw error;
|
|
1932
|
-
}
|
|
1933
|
-
}
|
|
1934
|
-
function parseConfig(raw) {
|
|
1935
|
-
try {
|
|
1936
|
-
const value = parseYaml(raw) ?? {};
|
|
1937
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
1938
|
-
} catch {
|
|
1939
|
-
return null;
|
|
1940
|
-
}
|
|
1941
|
-
}
|
|
1942
|
-
function servers(config) {
|
|
1943
|
-
const value = config.mcp_servers;
|
|
1944
|
-
return value && typeof value === "object" ? value : {};
|
|
1945
|
-
}
|
|
1946
|
-
var isItem = (line) => line.trim() !== "" && !line.trim().startsWith("#");
|
|
1947
|
-
var indentOf = (line) => line.length - line.trimStart().length;
|
|
1948
|
-
function editMcpServers(raw, name, entry) {
|
|
1949
|
-
const lines = raw.split("\n");
|
|
1950
|
-
const header = lines.findIndex((line) => /^mcp_servers:\s*(#.*)?$/.test(line));
|
|
1951
|
-
const render = (indent2) => stringifyYaml({ [name]: entry }).trimEnd().split("\n").map((line) => `${" ".repeat(indent2)}${line}`);
|
|
1952
|
-
if (header === -1) {
|
|
1953
|
-
if (entry === null) return raw;
|
|
1954
|
-
const body = raw === "" || raw.endsWith("\n") ? raw : `${raw}
|
|
1955
|
-
`;
|
|
1956
|
-
return `${body}mcp_servers:
|
|
1957
|
-
${render(2).join("\n")}
|
|
1958
|
-
`;
|
|
1959
|
-
}
|
|
1960
|
-
let end = header + 1;
|
|
1961
|
-
while (end < lines.length && !/^\S/.test(lines[end] ?? "")) end++;
|
|
1962
|
-
const block = Array.from({ length: end - header - 1 }, (_, i) => header + 1 + i);
|
|
1963
|
-
const items = block.filter((i) => isItem(lines[i] ?? ""));
|
|
1964
|
-
const indent = items.length > 0 ? indentOf(lines[items[0]] ?? "") : 2;
|
|
1965
|
-
const quoted = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1966
|
-
const keyLine = new RegExp(`^ {${indent}}(?:${quoted}|"${quoted}"|'${quoted}'):\\s*(#.*)?$`);
|
|
1967
|
-
const start = items.find((i) => keyLine.test(lines[i] ?? ""));
|
|
1968
|
-
if (start === void 0) {
|
|
1969
|
-
if (entry === null) return raw;
|
|
1970
|
-
const at = (items.at(-1) ?? header) + 1;
|
|
1971
|
-
lines.splice(at, 0, ...render(indent));
|
|
1972
|
-
return lines.join("\n");
|
|
1973
|
-
}
|
|
1974
|
-
const next = block.find(
|
|
1975
|
-
(i) => i > start && (lines[i] ?? "").trim() !== "" && indentOf(lines[i] ?? "") <= indent
|
|
1976
|
-
);
|
|
1977
|
-
let stop = next ?? end;
|
|
1978
|
-
while (stop > start + 1 && (lines[stop - 1] ?? "").trim() === "") stop--;
|
|
1979
|
-
lines.splice(start, stop - start, ...entry === null ? [] : render(indent));
|
|
1980
|
-
if (entry === null && items.length > 0 && items.every((i) => i >= start && i < stop)) {
|
|
1981
|
-
lines.splice(header, 1);
|
|
1982
|
-
}
|
|
1983
|
-
return lines.join("\n");
|
|
1984
|
-
}
|
|
1985
|
-
async function setHermesMcpServer(path, name, entry) {
|
|
1986
|
-
const raw = await readRaw(path);
|
|
1987
|
-
const before = parseConfig(raw);
|
|
1988
|
-
if (!before) return "manual";
|
|
1989
|
-
const current = servers(before)[name];
|
|
1990
|
-
if (entry === null ? current === void 0 : isDeepStrictEqual(current, entry)) {
|
|
1991
|
-
return "unchanged";
|
|
1992
|
-
}
|
|
1993
|
-
const nextServers = { ...servers(before) };
|
|
1994
|
-
if (entry === null) delete nextServers[name];
|
|
1995
|
-
else nextServers[name] = entry;
|
|
1996
|
-
const { mcp_servers: _, ...rest } = before;
|
|
1997
|
-
const expected = Object.keys(nextServers).length > 0 ? { ...rest, mcp_servers: nextServers } : rest;
|
|
1998
|
-
const next = editMcpServers(raw, name, entry);
|
|
1999
|
-
if (!isDeepStrictEqual(parseConfig(next), expected)) return "manual";
|
|
2000
|
-
await mkdir7(dirname9(path), { recursive: true });
|
|
2001
|
-
await writeAtomic(path, next, { mode: 384 });
|
|
2002
|
-
return entry === null ? "removed" : current === void 0 ? "created" : "updated";
|
|
2003
|
-
}
|
|
2004
|
-
async function readHermesConfig(path) {
|
|
2005
|
-
return parseConfig(await readRaw(path));
|
|
2006
|
-
}
|
|
2007
|
-
async function hasHermesMcpServer(path, name) {
|
|
2008
|
-
const config = await readHermesConfig(path);
|
|
2009
|
-
return config !== null && name in servers(config);
|
|
2010
|
-
}
|
|
2011
|
-
|
|
2012
|
-
// src/clients/global-mcp.ts
|
|
2013
|
-
function globalMcpPath(target) {
|
|
2014
|
-
return target === "codex" ? codexConfigPath() : hermesConfigPath();
|
|
2015
|
-
}
|
|
2016
|
-
async function setGlobalMcp(target, options) {
|
|
2017
|
-
const { name, root, namespace, enabled } = options;
|
|
2018
|
-
if (target === "hermes") {
|
|
2019
|
-
return setHermesMcpServer(
|
|
2020
|
-
hermesConfigPath(),
|
|
2021
|
-
name,
|
|
2022
|
-
enabled ? codebuddyMcpServer(root, namespace) : null
|
|
2023
|
-
);
|
|
2024
|
-
}
|
|
2025
|
-
if (!enabled) return (await removeCodexEntry(name)).removed ? "removed" : "unchanged";
|
|
2026
|
-
const result = await installCodexEntry({
|
|
2027
|
-
namespace,
|
|
2028
|
-
projectRoot: root,
|
|
2029
|
-
serverName: name,
|
|
2030
|
-
toolProfile: "core"
|
|
2031
|
-
});
|
|
2032
|
-
return result.created ? "created" : result.changed ? "updated" : "unchanged";
|
|
2033
|
-
}
|
|
2034
|
-
async function hasGlobalMcp(target, name) {
|
|
2035
|
-
if (target === "hermes") return hasHermesMcpServer(hermesConfigPath(), name);
|
|
2036
|
-
return (await listCodexEntries()).entries.some((entry) => entry.key === name);
|
|
2037
|
-
}
|
|
2038
|
-
|
|
2039
|
-
// src/clients/install.ts
|
|
2040
|
-
async function installClient(options) {
|
|
2041
|
-
const adapter = CLIENT_ADAPTERS[options.client];
|
|
2042
|
-
if (!supportedModes(adapter).includes(options.mode)) {
|
|
2043
|
-
throw new Error(
|
|
2044
|
-
`${adapter.name} does not support ${MODE_LABELS[options.mode]} mode: ${adapter.gap ?? "no lifecycle hooks"}.`
|
|
2045
|
-
);
|
|
2046
|
-
}
|
|
2047
|
-
return apply(options, options.mode, options.mode !== "lean");
|
|
2048
|
-
}
|
|
2049
|
-
async function ensureIndex(root) {
|
|
2050
|
-
const target = join11(root, ".codebuddy", "cache", "index.json");
|
|
2051
|
-
const existed = await access3(target).then(
|
|
2052
|
-
() => true,
|
|
2053
|
-
() => false
|
|
2054
|
-
);
|
|
2055
|
-
if (!existed) await indexRepository({ repositoryRoot: root });
|
|
2056
|
-
return { surface: "index", target, action: existed ? "unchanged" : "built" };
|
|
2057
|
-
}
|
|
2058
|
-
async function uninstallClient(options) {
|
|
2059
|
-
return apply(options, "mcp-only", false);
|
|
2060
|
-
}
|
|
2061
|
-
async function apply(options, mode, keepMcp) {
|
|
2062
|
-
const root = resolve9(options.projectRoot ?? process.cwd());
|
|
2063
|
-
const adapter = CLIENT_ADAPTERS[options.client];
|
|
2064
|
-
const steps = [];
|
|
2065
|
-
const notes = [];
|
|
2066
|
-
const namespace = options.namespace ?? await resolveNamespaceFrom(root);
|
|
2067
|
-
const name = `codebuddy-${namespace}`;
|
|
2068
|
-
const shared = sharedRulesReaders(options.client);
|
|
2069
|
-
const liveSiblings = mode === "mcp-only" || mode === "lean" ? await installedWithMcp(root, shared, namespace) : [];
|
|
2070
|
-
if (liveSiblings.length > 0) {
|
|
2071
|
-
const path = join11(root, adapter.rulesFile);
|
|
2072
|
-
steps.push({ surface: "rules", target: path, action: "kept" });
|
|
2073
|
-
notes.push(
|
|
2074
|
-
`Kept the CodeBuddy block in ${adapter.rulesFile}: still used by ${liveSiblings.map((id) => CLIENT_ADAPTERS[id].name).join(", ")}.`
|
|
2075
|
-
);
|
|
2076
|
-
} else {
|
|
2077
|
-
const hooked = mode === "agent" ? await hookedRulesSiblings(root, options.client) : [];
|
|
2078
|
-
const rules = await installWorkflowRules({
|
|
2079
|
-
client: options.client,
|
|
2080
|
-
projectRoot: root,
|
|
2081
|
-
// Lean sends no rules: the block would cost tokens every turn and shadow AGENTS.md.
|
|
2082
|
-
mode: mode === "lean" ? "mcp-only" : hooked.length > 0 ? "hooks" : mode
|
|
2083
|
-
});
|
|
2084
|
-
steps.push({ surface: "rules", target: rules.path, action: rules.action });
|
|
2085
|
-
if (hooked.length > 0) {
|
|
2086
|
-
notes.push(
|
|
2087
|
-
`Kept ${adapter.rulesFile} hook-driven: ${hooked.map((id) => CLIENT_ADAPTERS[id].name).join(", ")} hooks are installed; agents without hooks follow its agent-driven fallback.`
|
|
2088
|
-
);
|
|
2089
|
-
}
|
|
2090
|
-
}
|
|
2091
|
-
if (shared.length > 0) {
|
|
2092
|
-
notes.push(
|
|
2093
|
-
`${adapter.rulesFile} is also read by ${shared.map((id) => CLIENT_ADAPTERS[id].name).join(", ")}.`
|
|
2094
|
-
);
|
|
2095
|
-
}
|
|
2096
|
-
const hooks = await setHooks(
|
|
2097
|
-
root,
|
|
2098
|
-
mode === "hooks" || mode === "lean",
|
|
2099
|
-
options.client,
|
|
2100
|
-
mode === "lean" ? "lean" : "full"
|
|
2101
|
-
);
|
|
2102
|
-
if (hooks) steps.push(hooks);
|
|
2103
|
-
if (mode === "lean") steps.push(await ensureIndex(root));
|
|
2104
|
-
if ("file" in adapter.mcp) {
|
|
2105
|
-
const path = await projectMcpPath(root, adapter.mcp.file, adapter.mcp.format);
|
|
2106
|
-
const action = keepMcp ? await upsertMcpServer(path, adapter.mcp.format, name, codebuddyMcpServer(root, namespace)) : await removeMcpServer(path, adapter.mcp.format, name);
|
|
2107
|
-
steps.push({ surface: "mcp", target: path, action });
|
|
2108
|
-
if (action === "manual") {
|
|
2109
|
-
notes.push(
|
|
2110
|
-
`${path} has comments CodeBuddy will not rewrite; ${keepMcp ? "add" : "remove"} the "${name}" entry under "mcp" by hand${keepMcp ? `: ${JSON.stringify(formatEntry(adapter.mcp.format, codebuddyMcpServer(root, namespace)))}` : ""}.`
|
|
2111
|
-
);
|
|
2112
|
-
}
|
|
2113
|
-
} else if (options.global) {
|
|
2114
|
-
const target = globalMcpPath(adapter.mcp.global);
|
|
2115
|
-
const action = await setGlobalMcp(adapter.mcp.global, {
|
|
2116
|
-
name,
|
|
2117
|
-
root,
|
|
2118
|
-
namespace,
|
|
2119
|
-
enabled: keepMcp
|
|
2120
|
-
});
|
|
2121
|
-
steps.push({ surface: "mcp", target, action });
|
|
2122
|
-
if (action === "manual") {
|
|
2123
|
-
notes.push(
|
|
2124
|
-
`${target} is not a YAML map CodeBuddy can edit safely; ${keepMcp ? "add" : "remove"} the "${name}" entry under mcp_servers by hand.`
|
|
2125
|
-
);
|
|
2126
|
-
}
|
|
2127
|
-
} else {
|
|
2128
|
-
notes.push(
|
|
2129
|
-
`MCP config is global; ${keepMcp ? "add" : "remove"} it with --global, or: ${adapter.mcp.manual}.`
|
|
2130
|
-
);
|
|
2131
|
-
}
|
|
2132
|
-
if (adapter.note && mode !== "mcp-only") notes.push(`${adapter.name}: ${adapter.note}.`);
|
|
2133
|
-
return { client: options.client, steps, notes };
|
|
2134
|
-
}
|
|
2135
|
-
async function installedWithMcp(root, clients, namespace) {
|
|
2136
|
-
const live = [];
|
|
2137
|
-
for (const id of clients) {
|
|
2138
|
-
if ((await detectClient(id, root, namespace)).mcp) live.push(id);
|
|
681
|
+
return {
|
|
682
|
+
first: estimateTokens(first.text),
|
|
683
|
+
later: estimateTokens(later.text),
|
|
684
|
+
lean: { first: estimateTokens(leanFirst.text), later: estimateTokens(leanLater.text) }
|
|
685
|
+
};
|
|
686
|
+
} finally {
|
|
687
|
+
await rm(scratch, { recursive: true, force: true });
|
|
2139
688
|
}
|
|
2140
|
-
return live;
|
|
2141
689
|
}
|
|
2142
|
-
async function
|
|
2143
|
-
const
|
|
2144
|
-
|
|
2145
|
-
const
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
const mode = hooks ? await clientHooksVariant(root, client) === "lean" ? "lean" : "hooks" : effectiveRules ?? (mcp ? "mcp-only" : "none");
|
|
2172
|
-
return { client, mode, rulesMode, hooks, mcp, warnings };
|
|
690
|
+
async function measureClientCosts(projectRoot = process.cwd(), namespace) {
|
|
691
|
+
const root = resolve2(projectRoot);
|
|
692
|
+
const ns = namespace ?? await resolveNamespaceFrom(root);
|
|
693
|
+
const mcpSchema = await mcpSchemaTokens(root, ns);
|
|
694
|
+
const block = await hookBlockTokens(root, ns);
|
|
695
|
+
return CLIENT_IDS.flatMap((client) => {
|
|
696
|
+
const adapter = CLIENT_ADAPTERS[client];
|
|
697
|
+
return supportedModes(adapter).flatMap((mode) => {
|
|
698
|
+
if (mode === "mcp-only") return [];
|
|
699
|
+
if (mode === "lean") {
|
|
700
|
+
const later2 = adapter.ephemeralContext ? block.lean.first : block.lean.later;
|
|
701
|
+
return [
|
|
702
|
+
{ client, mode, rules: 0, mcpSchema: 0, firstBlock: block.lean.first, laterBlock: later2 }
|
|
703
|
+
];
|
|
704
|
+
}
|
|
705
|
+
const hooks = mode === "hooks";
|
|
706
|
+
const later = adapter.ephemeralContext ? block.first : block.later;
|
|
707
|
+
return [
|
|
708
|
+
{
|
|
709
|
+
client,
|
|
710
|
+
mode,
|
|
711
|
+
rules: estimateTokens(renderManagedBlock(client, mode)),
|
|
712
|
+
mcpSchema: adapter.defersMcpSchemas ? mcpSchema.listing : mcpSchema.full,
|
|
713
|
+
firstBlock: hooks ? block.first : 0,
|
|
714
|
+
laterBlock: hooks ? later : 0
|
|
715
|
+
}
|
|
716
|
+
];
|
|
717
|
+
});
|
|
718
|
+
});
|
|
2173
719
|
}
|
|
2174
720
|
|
|
2175
721
|
// src/clients/verify.ts
|
|
2176
722
|
import { execFile } from "child_process";
|
|
2177
|
-
import { readFile
|
|
2178
|
-
import { join as
|
|
723
|
+
import { readFile, realpath } from "fs/promises";
|
|
724
|
+
import { join as join3, resolve as resolve3 } from "path";
|
|
2179
725
|
import { pathToFileURL } from "url";
|
|
2180
726
|
var runHost = (bin, args, cwd) => new Promise((done) => {
|
|
2181
727
|
execFile(
|
|
@@ -2185,9 +731,9 @@ var runHost = (bin, args, cwd) => new Promise((done) => {
|
|
|
2185
731
|
(error, stdout) => done({ ok: !error, stdout: String(stdout ?? "") })
|
|
2186
732
|
);
|
|
2187
733
|
});
|
|
2188
|
-
var pass = (
|
|
2189
|
-
var fail = (
|
|
2190
|
-
var skip = (
|
|
734
|
+
var pass = (name2, detail) => ({ name: name2, status: "pass", detail });
|
|
735
|
+
var fail = (name2, detail) => ({ name: name2, status: "fail", detail });
|
|
736
|
+
var skip = (name2, detail) => ({ name: name2, status: "skip", detail });
|
|
2191
737
|
function json(text3) {
|
|
2192
738
|
try {
|
|
2193
739
|
return JSON.parse(text3);
|
|
@@ -2196,48 +742,48 @@ function json(text3) {
|
|
|
2196
742
|
}
|
|
2197
743
|
}
|
|
2198
744
|
var CONFORMANCE = {
|
|
2199
|
-
claude: async ({ root, name, run }) => {
|
|
745
|
+
claude: async ({ root, name: name2, run }) => {
|
|
2200
746
|
const hooks = await clientHooksInstalled(root, "claude") ? pass("hooks", ".claude/settings.json has the CodeBuddy hooks") : skip("hooks", "not installed in this repo");
|
|
2201
|
-
const mcp = await run("claude", ["mcp", "get",
|
|
747
|
+
const mcp = await run("claude", ["mcp", "get", name2], root);
|
|
2202
748
|
return [
|
|
2203
749
|
hooks,
|
|
2204
|
-
mcp.ok ? pass("mcp", `claude mcp get ${
|
|
750
|
+
mcp.ok ? pass("mcp", `claude mcp get ${name2}: ${mcp.stdout.match(/Scope: (.+)/)?.[1] ?? "found"}`) : skip("mcp", `claude mcp get ${name2}: not configured`)
|
|
2205
751
|
];
|
|
2206
752
|
},
|
|
2207
|
-
opencode: async ({ root, name, run }) => {
|
|
753
|
+
opencode: async ({ root, name: name2, run }) => {
|
|
2208
754
|
const result = await run("opencode", ["debug", "config"], root);
|
|
2209
755
|
const config = json(result.stdout);
|
|
2210
756
|
if (!config) return [fail("config", "opencode debug config returned no JSON")];
|
|
2211
757
|
const installed = await clientHooksInstalled(root, "opencode");
|
|
2212
758
|
const href = pathToFileURL(
|
|
2213
|
-
|
|
759
|
+
join3(await realpath(root).catch(() => root), ".opencode", "plugins", "codebuddy.js")
|
|
2214
760
|
).href;
|
|
2215
761
|
const loaded = config.plugin?.includes(href) === true;
|
|
2216
762
|
return [
|
|
2217
763
|
installed ? loaded ? pass("plugin", "opencode debug config loads .opencode/plugins/codebuddy.js") : fail("plugin", "plugin file is installed but opencode does not load it") : skip("plugin", "not installed in this repo"),
|
|
2218
|
-
config.mcp?.[
|
|
764
|
+
config.mcp?.[name2] ? pass("mcp", `opencode resolves the ${name2} MCP entry`) : skip("mcp", `no ${name2} entry in the resolved config`)
|
|
2219
765
|
];
|
|
2220
766
|
},
|
|
2221
|
-
codex: async ({ root, name, run }) => {
|
|
767
|
+
codex: async ({ root, name: name2, run }) => {
|
|
2222
768
|
const result = await run("codex", ["mcp", "list", "--json"], root);
|
|
2223
|
-
const
|
|
2224
|
-
if (!Array.isArray(
|
|
769
|
+
const servers = json(result.stdout);
|
|
770
|
+
if (!Array.isArray(servers)) return [fail("mcp", "codex mcp list --json returned no JSON")];
|
|
2225
771
|
return [
|
|
2226
|
-
|
|
772
|
+
servers.some((server) => server.name === name2) ? pass("mcp", `codex mcp list has ${name2}`) : skip("mcp", `no ${name2} entry (clients install codex --global adds it)`)
|
|
2227
773
|
];
|
|
2228
774
|
},
|
|
2229
|
-
hermes: async ({ root, name, run, env }) => {
|
|
775
|
+
hermes: async ({ root, name: name2, run, env }) => {
|
|
2230
776
|
const checks = [];
|
|
2231
777
|
const listed = await run("hermes", ["mcp", "list"], root);
|
|
2232
778
|
checks.push(
|
|
2233
|
-
listed.ok && listed.stdout.includes(
|
|
779
|
+
listed.ok && listed.stdout.includes(name2) ? pass("mcp", `hermes mcp list has ${name2}`) : skip("mcp", `no ${name2} entry (clients install hermes --global adds it)`)
|
|
2234
780
|
);
|
|
2235
781
|
if (!await hermesPluginInstalled(root)) {
|
|
2236
782
|
checks.push(skip("plugin", "not installed in this repo"));
|
|
2237
783
|
return checks;
|
|
2238
784
|
}
|
|
2239
785
|
const projectPlugins = /^(1|true|yes|on)$/i.test(env.HERMES_ENABLE_PROJECT_PLUGINS ?? "") || /^\s*HERMES_ENABLE_PROJECT_PLUGINS\s*=\s*["']?(1|true|yes|on)/im.test(
|
|
2240
|
-
await
|
|
786
|
+
await readFile(join3(hermesConfigPath(), "..", ".env"), "utf8").catch(() => "")
|
|
2241
787
|
);
|
|
2242
788
|
const plugins = (await readHermesConfig(hermesConfigPath()))?.plugins;
|
|
2243
789
|
const enabled = Array.isArray(plugins?.enabled) && plugins.enabled.includes("codebuddy");
|
|
@@ -2249,7 +795,7 @@ var CONFORMANCE = {
|
|
|
2249
795
|
}
|
|
2250
796
|
};
|
|
2251
797
|
async function verifyClient(client, options = {}) {
|
|
2252
|
-
const root =
|
|
798
|
+
const root = resolve3(options.root ?? process.cwd());
|
|
2253
799
|
const run = options.run ?? runHost;
|
|
2254
800
|
const adapter = CLIENT_ADAPTERS[client];
|
|
2255
801
|
const version = await run(adapter.host.bin, ["--version"], root);
|
|
@@ -2268,9 +814,9 @@ async function verifyClient(client, options = {}) {
|
|
|
2268
814
|
checks: [skip("host", `${adapter.host.bin} not found or printed no version`)]
|
|
2269
815
|
};
|
|
2270
816
|
}
|
|
2271
|
-
const
|
|
817
|
+
const name2 = `codebuddy-${options.namespace ?? await resolveNamespaceFrom(root)}`;
|
|
2272
818
|
const conformance = CONFORMANCE[client];
|
|
2273
|
-
const checks = conformance ? await conformance({ root, name, run, env: options.env ?? process.env }).catch(
|
|
819
|
+
const checks = conformance ? await conformance({ root, name: name2, run, env: options.env ?? process.env }).catch(
|
|
2274
820
|
(error) => [fail("conformance", error.message)]
|
|
2275
821
|
) : [];
|
|
2276
822
|
return {
|
|
@@ -3008,12 +1554,12 @@ data: ${data}
|
|
|
3008
1554
|
heartbeat.unref();
|
|
3009
1555
|
return {
|
|
3010
1556
|
url: `http://127.0.0.1:${port}/`,
|
|
3011
|
-
close: () => new Promise((
|
|
1557
|
+
close: () => new Promise((resolve6) => {
|
|
3012
1558
|
clearInterval(heartbeat);
|
|
3013
1559
|
unwatchFile(path);
|
|
3014
1560
|
for (const client of clients) client.end();
|
|
3015
1561
|
server.closeAllConnections();
|
|
3016
|
-
server.close(() =>
|
|
1562
|
+
server.close(() => resolve6());
|
|
3017
1563
|
})
|
|
3018
1564
|
};
|
|
3019
1565
|
}
|
|
@@ -3021,11 +1567,11 @@ async function listen(server, port) {
|
|
|
3021
1567
|
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
3022
1568
|
const candidate = port === 0 ? 0 : port + attempt;
|
|
3023
1569
|
try {
|
|
3024
|
-
await new Promise((
|
|
1570
|
+
await new Promise((resolve6, reject) => {
|
|
3025
1571
|
server.once("error", reject);
|
|
3026
1572
|
server.listen(candidate, "127.0.0.1", () => {
|
|
3027
1573
|
server.off("error", reject);
|
|
3028
|
-
|
|
1574
|
+
resolve6();
|
|
3029
1575
|
});
|
|
3030
1576
|
});
|
|
3031
1577
|
return server.address().port;
|
|
@@ -3063,8 +1609,8 @@ function registerDashboardCommand(program, runSafely2) {
|
|
|
3063
1609
|
);
|
|
3064
1610
|
console.log(pc4.dim(" Ctrl+C to stop."));
|
|
3065
1611
|
if (opts.open) openBrowser(dashboard.url);
|
|
3066
|
-
await new Promise((
|
|
3067
|
-
const stop = () => void dashboard.close().then(
|
|
1612
|
+
await new Promise((resolve6) => {
|
|
1613
|
+
const stop = () => void dashboard.close().then(resolve6);
|
|
3068
1614
|
process.once("SIGINT", stop);
|
|
3069
1615
|
process.once("SIGTERM", stop);
|
|
3070
1616
|
});
|
|
@@ -3076,20 +1622,20 @@ function registerDashboardCommand(program, runSafely2) {
|
|
|
3076
1622
|
import pc5 from "picocolors";
|
|
3077
1623
|
|
|
3078
1624
|
// src/eval/runner.ts
|
|
3079
|
-
import { readFile as
|
|
3080
|
-
import { dirname as
|
|
1625
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
1626
|
+
import { dirname as dirname2, isAbsolute, join as join4, resolve as resolve4 } from "path";
|
|
3081
1627
|
async function runEvaluation(input = {}) {
|
|
3082
|
-
const datasetPath =
|
|
3083
|
-
input.datasetPath ??
|
|
1628
|
+
const datasetPath = resolve4(
|
|
1629
|
+
input.datasetPath ?? join4(process.cwd(), "evals", "change-readiness.json")
|
|
3084
1630
|
);
|
|
3085
|
-
const dataset = JSON.parse(await
|
|
1631
|
+
const dataset = JSON.parse(await readFile2(datasetPath, "utf8"));
|
|
3086
1632
|
if (dataset.version !== 1 || !Array.isArray(dataset.cases)) {
|
|
3087
1633
|
throw new Error(`Unsupported evaluation dataset: ${datasetPath}`);
|
|
3088
1634
|
}
|
|
3089
|
-
const datasetDirectory =
|
|
1635
|
+
const datasetDirectory = dirname2(datasetPath);
|
|
3090
1636
|
const results = await Promise.all(
|
|
3091
1637
|
dataset.cases.map(async (evaluationCase) => {
|
|
3092
|
-
const fixtureRoot = isAbsolute(evaluationCase.fixture) ? evaluationCase.fixture :
|
|
1638
|
+
const fixtureRoot = isAbsolute(evaluationCase.fixture) ? evaluationCase.fixture : resolve4(input.repositoryRoot ?? datasetDirectory, evaluationCase.fixture);
|
|
3093
1639
|
const report = await buildChangeReport({
|
|
3094
1640
|
repositoryRoot: fixtureRoot,
|
|
3095
1641
|
namespace: `eval:${evaluationCase.id}`,
|
|
@@ -3253,6 +1799,7 @@ function registerHooksCommands(program, runSafely2) {
|
|
|
3253
1799
|
);
|
|
3254
1800
|
if (result.saved.length > 0) syncInBackground(hookRoot(input.cwd));
|
|
3255
1801
|
}
|
|
1802
|
+
await flushTelemetry(hookRoot(input.cwd));
|
|
3256
1803
|
});
|
|
3257
1804
|
});
|
|
3258
1805
|
hooks.command("sync").description(
|
|
@@ -3373,7 +1920,7 @@ async function llmStatusReport(root, env = process.env) {
|
|
|
3373
1920
|
};
|
|
3374
1921
|
}
|
|
3375
1922
|
function formatLlmStatus(report) {
|
|
3376
|
-
const features = report.features ? Object.entries(report.features).map(([
|
|
1923
|
+
const features = report.features ? Object.entries(report.features).map(([name2, on]) => `${name2} ${on ? "on" : "off"}`).join(", ") : "-";
|
|
3377
1924
|
return [
|
|
3378
1925
|
`status: ${report.enabled ? "on" : `off (${report.reason})`}`,
|
|
3379
1926
|
`provider: ${report.provider}${report.model ? ` ${report.model}` : ""}`,
|
|
@@ -3579,7 +2126,7 @@ async function runInitWizard() {
|
|
|
3579
2126
|
if (result.code === 0) {
|
|
3580
2127
|
dockerSpinner.stop("Postgres container started.");
|
|
3581
2128
|
for (let attempt = 0; attempt < 10 && !connected; attempt++) {
|
|
3582
|
-
await new Promise((
|
|
2129
|
+
await new Promise((resolve6) => setTimeout(resolve6, 500));
|
|
3583
2130
|
connected = await pingOnce(databaseUrl);
|
|
3584
2131
|
}
|
|
3585
2132
|
if (!connected) {
|
|
@@ -3871,8 +2418,8 @@ function openEditor(path) {
|
|
|
3871
2418
|
}
|
|
3872
2419
|
|
|
3873
2420
|
// src/cli/commands/models.ts
|
|
3874
|
-
import { existsSync
|
|
3875
|
-
import { join as
|
|
2421
|
+
import { existsSync } from "fs";
|
|
2422
|
+
import { join as join5 } from "path";
|
|
3876
2423
|
import { spinner as spinner2 } from "@clack/prompts";
|
|
3877
2424
|
import pc13 from "picocolors";
|
|
3878
2425
|
|
|
@@ -4157,8 +2704,8 @@ function keyPresent(provider, apiKey) {
|
|
|
4157
2704
|
return Boolean(process.env[KEY_ENV2[provider]] || provider === "huggingface" && apiKey);
|
|
4158
2705
|
}
|
|
4159
2706
|
function localModelDownloaded(id, revision) {
|
|
4160
|
-
const dir =
|
|
4161
|
-
return
|
|
2707
|
+
const dir = join5(modelCacheDir(), id, ...revision ? [revision] : []);
|
|
2708
|
+
return existsSync(join5(dir, "onnx", "model_quantized.onnx"));
|
|
4162
2709
|
}
|
|
4163
2710
|
async function vectorCounts(postgresUrl, namespace, space) {
|
|
4164
2711
|
const client = createDatabaseClient({ postgresUrl, maxConnections: 1, connectTimeoutSeconds: 3 });
|
|
@@ -4501,14 +3048,14 @@ function renderPlan(plan) {
|
|
|
4501
3048
|
}
|
|
4502
3049
|
function openEditor2(path) {
|
|
4503
3050
|
const editor = process.env.EDITOR ?? process.env.VISUAL ?? "vi";
|
|
4504
|
-
return new Promise((
|
|
3051
|
+
return new Promise((resolve6, reject) => {
|
|
4505
3052
|
const child = spawn5(editor, [path], { stdio: "inherit" });
|
|
4506
3053
|
child.on("error", reject);
|
|
4507
|
-
child.on("close", () =>
|
|
3054
|
+
child.on("close", () => resolve6());
|
|
4508
3055
|
});
|
|
4509
3056
|
}
|
|
4510
3057
|
function runGit(args) {
|
|
4511
|
-
return new Promise((
|
|
3058
|
+
return new Promise((resolve6, reject) => {
|
|
4512
3059
|
const child = spawn5("git", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
4513
3060
|
let stdout = "";
|
|
4514
3061
|
let stderr = "";
|
|
@@ -4519,7 +3066,7 @@ function runGit(args) {
|
|
|
4519
3066
|
stderr += String(c);
|
|
4520
3067
|
});
|
|
4521
3068
|
child.on("error", reject);
|
|
4522
|
-
child.on("close", (code) =>
|
|
3069
|
+
child.on("close", (code) => resolve6({ code: code ?? 0, stdout, stderr }));
|
|
4523
3070
|
});
|
|
4524
3071
|
}
|
|
4525
3072
|
function registerPlanCommands(program, runSafely2) {
|
|
@@ -4833,8 +3380,8 @@ function severityBadge2(severity) {
|
|
|
4833
3380
|
}
|
|
4834
3381
|
|
|
4835
3382
|
// src/cli/commands/symbols.ts
|
|
4836
|
-
import { readFile as
|
|
4837
|
-
import { resolve as
|
|
3383
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
3384
|
+
import { resolve as resolve5 } from "path";
|
|
4838
3385
|
import pc18 from "picocolors";
|
|
4839
3386
|
function registerSymbolsCommand(program, runSafely2) {
|
|
4840
3387
|
program.command("symbols").argument("<path>", "Repo-relative or absolute path to a code file.").option("--json", "Print machine-readable JSON.").description("List a file's top-level symbols (functions, classes, types, exports).").action(async (path, opts) => {
|
|
@@ -4843,7 +3390,7 @@ function registerSymbolsCommand(program, runSafely2) {
|
|
|
4843
3390
|
console.log(pc18.dim(`no symbol extraction for ${path} (unsupported language)`));
|
|
4844
3391
|
return;
|
|
4845
3392
|
}
|
|
4846
|
-
const content = await
|
|
3393
|
+
const content = await readFile3(resolve5(path), "utf8");
|
|
4847
3394
|
const table = extractSymbolTable(content, path);
|
|
4848
3395
|
if (opts.json) {
|
|
4849
3396
|
console.log(JSON.stringify({ path, symbols: table }, null, 2));
|
|
@@ -4865,48 +3412,712 @@ ${table.length} symbol(s)`));
|
|
|
4865
3412
|
});
|
|
4866
3413
|
}
|
|
4867
3414
|
|
|
3415
|
+
// src/cli/commands/telemetry.ts
|
|
3416
|
+
import pc19 from "picocolors";
|
|
3417
|
+
|
|
3418
|
+
// src/telemetry/collector.ts
|
|
3419
|
+
import { createHash, timingSafeEqual } from "crypto";
|
|
3420
|
+
import { appendFile, mkdir as mkdir2, readdir, readFile as readFile4 } from "fs/promises";
|
|
3421
|
+
import { createServer as createServer2 } from "http";
|
|
3422
|
+
import { join as join6 } from "path";
|
|
3423
|
+
import { z as z2 } from "zod";
|
|
3424
|
+
|
|
3425
|
+
// src/telemetry/admin-page.ts
|
|
3426
|
+
var ADMIN_HTML = `<!doctype html>
|
|
3427
|
+
<html lang="en">
|
|
3428
|
+
<head>
|
|
3429
|
+
<meta charset="utf-8">
|
|
3430
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
3431
|
+
<title>CodeBuddy Usage</title>
|
|
3432
|
+
<style>
|
|
3433
|
+
:root {
|
|
3434
|
+
--bg: #f4f4f2; --panel: #fcfcfb; --fg: #0b0b0b; --muted: #52514e; --line: #e2e1dc;
|
|
3435
|
+
--bar: #2a78d6; --bar-soft: #e3edfa; --bad: #b42318;
|
|
3436
|
+
--sans: ui-sans-serif, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
3437
|
+
--mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
|
3438
|
+
}
|
|
3439
|
+
@media (prefers-color-scheme: dark) { :root {
|
|
3440
|
+
--bg: #121211; --panel: #1a1a19; --fg: #f0efec; --muted: #c3c2b7; --line: #2e2e2b;
|
|
3441
|
+
--bar: #3987e5; --bar-soft: #1d2b3d; --bad: #f97066; color-scheme: dark } }
|
|
3442
|
+
* { box-sizing: border-box; }
|
|
3443
|
+
body { margin: 0; background: var(--bg); color: var(--fg); font: 14px/1.5 var(--sans); }
|
|
3444
|
+
.wrap { max-width: 1200px; margin: 0 auto; padding: 20px 16px 48px; display: grid; gap: 16px; }
|
|
3445
|
+
.wrap > * { min-width: 0; }
|
|
3446
|
+
header { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 20px; }
|
|
3447
|
+
h1 { font-size: 20px; margin: 0; letter-spacing: -.01em; }
|
|
3448
|
+
h1 small { font: 500 13px var(--mono); color: var(--muted); margin-left: 8px; }
|
|
3449
|
+
h2 { font-size: 13px; margin: 0 0 10px; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); font-weight: 600; }
|
|
3450
|
+
.spacer { flex: 1; }
|
|
3451
|
+
.seg { display: inline-flex; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; background: var(--panel); }
|
|
3452
|
+
.seg button { border: 0; background: none; color: var(--muted); font: 500 12px var(--sans); padding: 6px 12px; cursor: pointer; }
|
|
3453
|
+
.seg button + button { border-left: 1px solid var(--line); }
|
|
3454
|
+
.seg button[aria-pressed="true"] { background: var(--fg); color: var(--bg); }
|
|
3455
|
+
.seg button:focus-visible { outline: 2px solid var(--bar); outline-offset: -2px; }
|
|
3456
|
+
.kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; }
|
|
3457
|
+
.kpi { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 12px 14px; }
|
|
3458
|
+
.kpi .label { font-size: 12px; color: var(--muted); }
|
|
3459
|
+
.kpi .value { font: 600 28px/1.2 var(--mono); font-variant-numeric: tabular-nums; margin-top: 4px; }
|
|
3460
|
+
.kpi .sub { font-size: 12px; color: var(--muted); margin-top: 2px; }
|
|
3461
|
+
.panel { background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 14px 16px; min-width: 0; position: relative; }
|
|
3462
|
+
.cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 16px; }
|
|
3463
|
+
.chart svg { display: block; width: 100%; height: 180px; }
|
|
3464
|
+
.chart rect.hit { fill: transparent; }
|
|
3465
|
+
.chart rect.hit:hover + rect.mark, .chart rect.mark:hover { opacity: .75; }
|
|
3466
|
+
svg text { fill: var(--muted); font: 11px var(--mono); }
|
|
3467
|
+
.tip { position: absolute; pointer-events: none; background: var(--fg); color: var(--bg); font: 12px var(--mono); padding: 4px 8px; border-radius: 6px; white-space: nowrap; display: none; z-index: 2; }
|
|
3468
|
+
.rows { display: grid; gap: 6px; }
|
|
3469
|
+
.row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 2px 12px; font-size: 13px; }
|
|
3470
|
+
.row .track { grid-column: 1 / -1; height: 6px; background: var(--bar-soft); border-radius: 3px; }
|
|
3471
|
+
.row .fill { height: 6px; background: var(--bar); border-radius: 3px; }
|
|
3472
|
+
.row .n { font-family: var(--mono); font-variant-numeric: tabular-nums; color: var(--muted); }
|
|
3473
|
+
.scroll { overflow-x: auto; }
|
|
3474
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
3475
|
+
th { text-align: left; font: 600 11px var(--sans); text-transform: uppercase; letter-spacing: .06em; color: var(--muted); padding: 6px 8px; border-bottom: 1px solid var(--line); white-space: nowrap; }
|
|
3476
|
+
td { padding: 6px 8px; border-bottom: 1px solid var(--line); white-space: nowrap; }
|
|
3477
|
+
tr:last-child td { border-bottom: 0; }
|
|
3478
|
+
td.n, th.n { text-align: right; font-family: var(--mono); font-variant-numeric: tabular-nums; }
|
|
3479
|
+
.bad { color: var(--bad); }
|
|
3480
|
+
.muted { color: var(--muted); }
|
|
3481
|
+
.mono { font-family: var(--mono); }
|
|
3482
|
+
.btn { border: 1px solid var(--line); background: var(--panel); color: var(--fg); font: 500 12px var(--sans); padding: 6px 12px; border-radius: 8px; cursor: pointer; }
|
|
3483
|
+
.btn:focus-visible { outline: 2px solid var(--bar); outline-offset: 2px; }
|
|
3484
|
+
.users { max-height: 560px; overflow: auto; }
|
|
3485
|
+
.users thead th { position: sticky; top: 0; background: var(--panel); }
|
|
3486
|
+
.empty { padding: 32px 16px; text-align: center; color: var(--muted); }
|
|
3487
|
+
</style>
|
|
3488
|
+
</head>
|
|
3489
|
+
<body>
|
|
3490
|
+
<div class="wrap">
|
|
3491
|
+
<header>
|
|
3492
|
+
<h1>CodeBuddy usage<small id="updated"></small></h1>
|
|
3493
|
+
<span class="spacer"></span>
|
|
3494
|
+
<div class="seg" role="group" aria-label="Time range" id="range">
|
|
3495
|
+
<button data-days="7">7 days</button>
|
|
3496
|
+
<button data-days="30" aria-pressed="true">30 days</button>
|
|
3497
|
+
<button data-days="90">90 days</button>
|
|
3498
|
+
<button data-days="365">1 year</button>
|
|
3499
|
+
</div>
|
|
3500
|
+
</header>
|
|
3501
|
+
<section class="kpis" id="kpis" aria-label="Summary"></section>
|
|
3502
|
+
<div class="cols">
|
|
3503
|
+
<section class="panel"><h2>Daily active installs</h2><div class="chart" id="active"></div></section>
|
|
3504
|
+
<section class="panel"><h2>npm downloads per day</h2><div class="chart" id="npm"></div></section>
|
|
3505
|
+
</div>
|
|
3506
|
+
<section class="panel"><h2>Feature usage</h2><div class="scroll" id="features"></div></section>
|
|
3507
|
+
<div class="cols">
|
|
3508
|
+
<section class="panel"><h2>Agent clients</h2><div class="rows" id="clients"></div></section>
|
|
3509
|
+
<section class="panel"><h2>Versions</h2><div class="rows" id="versions"></div></section>
|
|
3510
|
+
<section class="panel"><h2>Platforms</h2><div class="rows" id="platforms"></div></section>
|
|
3511
|
+
</div>
|
|
3512
|
+
<section class="panel">
|
|
3513
|
+
<div style="display:flex;align-items:center;gap:12px;margin-bottom:10px"><h2 style="margin:0">Users</h2><span class="muted" id="userCount" style="font-size:12px"></span><span class="spacer"></span><button class="btn" id="csv">Download CSV</button></div>
|
|
3514
|
+
<div class="scroll users" id="users"></div>
|
|
3515
|
+
</section>
|
|
3516
|
+
<section class="panel"><h2>Latest events</h2><div class="scroll" id="recent"></div></section>
|
|
3517
|
+
<p class="muted" style="margin:0;font-size:12px">Anonymous telemetry from installs that did not opt out: random install ids, tool names, timings and versions. No prompts, code, paths or repository names are collected. Shares count each install once, from its latest batch.</p>
|
|
3518
|
+
</div>
|
|
3519
|
+
<div class="tip" id="tip"></div>
|
|
3520
|
+
<script>
|
|
3521
|
+
(function () {
|
|
3522
|
+
var days = 30;
|
|
3523
|
+
// Works at "/" and under a proxy mount such as "/admin" (with or without a trailing slash).
|
|
3524
|
+
var base = location.pathname.replace(/\\/?$/, "/");
|
|
3525
|
+
var summary = null;
|
|
3526
|
+
var npm = null;
|
|
3527
|
+
var tip = document.getElementById("tip");
|
|
3528
|
+
|
|
3529
|
+
function esc(value) {
|
|
3530
|
+
return String(value == null ? "" : value).replace(/[&<>"']/g, function (c) {
|
|
3531
|
+
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
|
|
3532
|
+
});
|
|
3533
|
+
}
|
|
3534
|
+
function num(n) { return Math.round(n).toLocaleString(); }
|
|
3535
|
+
function compact(n) { return n >= 1e6 ? (n / 1e6).toFixed(1) + "M" : n >= 1e4 ? (n / 1e3).toFixed(1) + "k" : num(n); }
|
|
3536
|
+
function pct(n) { return (n * 100).toFixed(n < 0.1 ? 1 : 0) + "%"; }
|
|
3537
|
+
function ms(n) { return n >= 1000 ? (n / 1000).toFixed(1) + " s" : Math.round(n) + " ms"; }
|
|
3538
|
+
|
|
3539
|
+
function kpi(label, value, sub) {
|
|
3540
|
+
return '<div class="kpi"><div class="label">' + esc(label) + '</div><div class="value">' + esc(value) +
|
|
3541
|
+
'</div><div class="sub">' + esc(sub || "") + "</div></div>";
|
|
3542
|
+
}
|
|
3543
|
+
|
|
3544
|
+
/** Bars of one measure over days; hover shows the exact value. */
|
|
3545
|
+
function bars(points, label) {
|
|
3546
|
+
if (!points.length) return '<div class="empty">No data yet.</div>';
|
|
3547
|
+
var w = 600, h = 180, top = 10, bottom = 22, left = 36;
|
|
3548
|
+
var max = Math.max.apply(null, points.map(function (p) { return p.value; }).concat([1]));
|
|
3549
|
+
var step = (w - left) / points.length;
|
|
3550
|
+
var bw = Math.max(1, step - 2);
|
|
3551
|
+
var out = '<svg viewBox="0 0 ' + w + " " + h + '" preserveAspectRatio="none" role="img" aria-label="' + esc(label) + '">';
|
|
3552
|
+
[0, 0.5, 1].forEach(function (f) {
|
|
3553
|
+
var y = top + (h - top - bottom) * (1 - f);
|
|
3554
|
+
out += '<line x1="' + left + '" x2="' + w + '" y1="' + y + '" y2="' + y + '" stroke="var(--line)" stroke-width="1"/>';
|
|
3555
|
+
out += '<text x="' + (left - 6) + '" y="' + (y + 4) + '" text-anchor="end">' + esc(compact(max * f)) + "</text>";
|
|
3556
|
+
});
|
|
3557
|
+
points.forEach(function (p, i) {
|
|
3558
|
+
var bh = (h - top - bottom) * (p.value / max);
|
|
3559
|
+
var x = left + i * step + 1;
|
|
3560
|
+
var tipText = esc(p.date + " \xB7 " + num(p.value) + " " + label);
|
|
3561
|
+
out += '<rect class="hit" x="' + (left + i * step) + '" y="' + top + '" width="' + step + '" height="' + (h - top - bottom) + '" data-tip="' + tipText + '"/>';
|
|
3562
|
+
out += '<rect class="mark" x="' + x + '" y="' + (h - bottom - bh) + '" width="' + bw + '" height="' + bh + '" rx="' + Math.min(2, bw / 2) + '" fill="var(--bar)" data-tip="' + tipText + '"/>';
|
|
3563
|
+
});
|
|
3564
|
+
var first = points[0].date, last = points[points.length - 1].date;
|
|
3565
|
+
out += '<text x="' + left + '" y="' + (h - 6) + '">' + esc(first) + '</text><text x="' + w + '" y="' + (h - 6) + '" text-anchor="end">' + esc(last) + "</text>";
|
|
3566
|
+
return out + "</svg>";
|
|
3567
|
+
}
|
|
3568
|
+
|
|
3569
|
+
function rows(items, total) {
|
|
3570
|
+
if (!items.length) return '<div class="muted">No data yet.</div>';
|
|
3571
|
+
return items.slice(0, 8).map(function (item) {
|
|
3572
|
+
var share = total ? item.installs / total : 0;
|
|
3573
|
+
return '<div class="row"><span>' + esc(item.label) + '</span><span class="n">' + num(item.installs) + " \xB7 " + pct(share) +
|
|
3574
|
+
'</span><div class="track"><div class="fill" style="width:' + (share * 100).toFixed(1) + '%"></div></div></div>';
|
|
3575
|
+
}).join("");
|
|
3576
|
+
}
|
|
3577
|
+
|
|
3578
|
+
function npmPoints() {
|
|
3579
|
+
if (!npm || !npm.downloads) return [];
|
|
3580
|
+
return npm.downloads.slice(-Math.min(days, 365)).map(function (d) { return { date: d.day, value: d.downloads }; });
|
|
3581
|
+
}
|
|
3582
|
+
function npmTotal(n) {
|
|
3583
|
+
if (!npm || !npm.downloads) return null;
|
|
3584
|
+
return npm.downloads.slice(-n).reduce(function (sum, d) { return sum + d.downloads; }, 0);
|
|
3585
|
+
}
|
|
3586
|
+
|
|
3587
|
+
function render() {
|
|
3588
|
+
if (!summary) return;
|
|
3589
|
+
var t = summary.totals;
|
|
3590
|
+
var npm30 = npmTotal(30), npm365 = npmTotal(365);
|
|
3591
|
+
document.getElementById("updated").textContent = "updated " + new Date(summary.generatedAt).toLocaleTimeString();
|
|
3592
|
+
document.getElementById("kpis").innerHTML =
|
|
3593
|
+
kpi("Active installs \xB7 7 days", num(t.active7d), num(t.active1d) + " today") +
|
|
3594
|
+
kpi("Active installs \xB7 30 days", num(t.active30d), num(t.newToday) + " new today") +
|
|
3595
|
+
kpi("Installs in range", num(t.installs), num(t.projects) + " projects") +
|
|
3596
|
+
kpi("Agent sessions", num(t.sessions), "in " + summary.days + " days") +
|
|
3597
|
+
kpi("Events", compact(t.events), pct(1 - t.errorRate) + " succeeded") +
|
|
3598
|
+
kpi("Tokens delivered", compact(t.tokensOut), "context sent to agents") +
|
|
3599
|
+
kpi("npm downloads \xB7 30 days", npm30 == null ? "\u2013" : num(npm30), npm365 == null ? (npm && npm.error ? npm.error : "") : num(npm365) + " in the last year");
|
|
3600
|
+
document.getElementById("active").innerHTML = bars(summary.daily.map(function (d) { return { date: d.date, value: d.installs }; }), "active installs");
|
|
3601
|
+
document.getElementById("npm").innerHTML = npm && npm.error ? '<div class="empty">' + esc(npm.error) + "</div>" : bars(npmPoints(), "downloads");
|
|
3602
|
+
document.getElementById("features").innerHTML = summary.features.length
|
|
3603
|
+
? "<table><thead><tr><th>Feature</th><th class=\\"n\\">Calls</th><th class=\\"n\\">Installs</th><th class=\\"n\\">Median</th><th class=\\"n\\">Errors</th></tr></thead><tbody>" +
|
|
3604
|
+
summary.features.map(function (f) {
|
|
3605
|
+
return "<tr><td class=\\"mono\\">" + esc(f.label) + '</td><td class="n">' + num(f.calls) + '</td><td class="n">' + num(f.installs) +
|
|
3606
|
+
'</td><td class="n">' + ms(f.p50ms) + '</td><td class="n' + (f.errorRate > 0.05 ? " bad" : "") + '">' + pct(f.errorRate) + "</td></tr>";
|
|
3607
|
+
}).join("") + "</tbody></table>"
|
|
3608
|
+
: '<div class="empty">No events yet. Installs send a batch at most every 10 minutes.</div>';
|
|
3609
|
+
var installs = summary.versions.reduce(function (sum, v) { return sum + v.installs; }, 0);
|
|
3610
|
+
document.getElementById("clients").innerHTML = rows(summary.clients, installs);
|
|
3611
|
+
document.getElementById("versions").innerHTML = rows(summary.versions, installs);
|
|
3612
|
+
document.getElementById("platforms").innerHTML = rows(summary.platforms, installs);
|
|
3613
|
+
document.getElementById("userCount").textContent = num(summary.users.length) + " installs seen in " + summary.days + " days";
|
|
3614
|
+
document.getElementById("users").innerHTML = summary.users.length
|
|
3615
|
+
? "<table><thead><tr><th>Install</th><th>First seen</th><th>Last active</th><th class=\\"n\\">Events</th><th class=\\"n\\">Sessions</th><th class=\\"n\\">Projects</th><th>Client</th><th>Version</th><th>Platform</th><th>Top feature</th></tr></thead><tbody>" +
|
|
3616
|
+
summary.users.map(function (u) {
|
|
3617
|
+
return '<tr><td class="mono">' + esc(u.install) + "</td><td>" + esc(new Date(u.firstSeen).toLocaleDateString()) + "</td><td>" + esc(new Date(u.lastSeen).toLocaleString()) +
|
|
3618
|
+
'</td><td class="n">' + num(u.events) + '</td><td class="n">' + num(u.sessions) + '</td><td class="n">' + num(u.projects) + "</td><td>" + esc(u.client) +
|
|
3619
|
+
'</td><td class="mono">' + esc(u.version) + "</td><td>" + esc(u.platform) + '</td><td class="mono">' + esc(u.topFeature) + "</td></tr>";
|
|
3620
|
+
}).join("") + "</tbody></table>"
|
|
3621
|
+
: '<div class="empty">No users yet.</div>';
|
|
3622
|
+
document.getElementById("recent").innerHTML = summary.recent.length
|
|
3623
|
+
? "<table><thead><tr><th>Received</th><th>Install</th><th>Kind</th><th>Name</th><th>Client</th><th>Version</th><th class=\\"n\\">Time</th><th>Result</th></tr></thead><tbody>" +
|
|
3624
|
+
summary.recent.map(function (e) {
|
|
3625
|
+
return "<tr><td>" + esc(new Date(e.receivedAt).toLocaleString()) + '</td><td class="mono">' + esc(e.install) + "</td><td>" + esc(e.kind) +
|
|
3626
|
+
'</td><td class="mono">' + esc(e.name) + "</td><td>" + esc(e.client) + '</td><td class="mono">' + esc(e.version) +
|
|
3627
|
+
'</td><td class="n">' + ms(e.ms) + '</td><td class="' + (e.ok ? "" : "bad") + '">' + (e.ok ? "ok" : "error") + "</td></tr>";
|
|
3628
|
+
}).join("") + "</tbody></table>"
|
|
3629
|
+
: '<div class="empty">No events yet.</div>';
|
|
3630
|
+
}
|
|
3631
|
+
|
|
3632
|
+
function load() {
|
|
3633
|
+
fetch(base + "api/summary?days=" + days).then(function (r) { return r.json(); }).then(function (s) { summary = s; render(); });
|
|
3634
|
+
}
|
|
3635
|
+
fetch(base + "api/npm").then(function (r) { return r.json(); }).then(function (n) { npm = n; render(); });
|
|
3636
|
+
|
|
3637
|
+
document.getElementById("range").addEventListener("click", function (event) {
|
|
3638
|
+
var button = event.target.closest("button");
|
|
3639
|
+
if (!button) return;
|
|
3640
|
+
days = Number(button.dataset.days);
|
|
3641
|
+
document.querySelectorAll("#range button").forEach(function (b) { b.setAttribute("aria-pressed", String(b === button)); });
|
|
3642
|
+
load();
|
|
3643
|
+
});
|
|
3644
|
+
document.getElementById("csv").addEventListener("click", function () {
|
|
3645
|
+
if (!summary) return;
|
|
3646
|
+
var cols = ["install", "firstSeen", "lastSeen", "events", "sessions", "projects", "client", "version", "platform", "topFeature"];
|
|
3647
|
+
var lines = [cols.join(",")].concat(summary.users.map(function (u) {
|
|
3648
|
+
return cols.map(function (c) { return '"' + String(u[c]).replace(/"/g, '""') + '"'; }).join(",");
|
|
3649
|
+
}));
|
|
3650
|
+
var link = document.createElement("a");
|
|
3651
|
+
link.href = URL.createObjectURL(new Blob([lines.join("\\n")], { type: "text/csv" }));
|
|
3652
|
+
link.download = "codebuddy-users-" + new Date().toISOString().slice(0, 10) + ".csv";
|
|
3653
|
+
link.click();
|
|
3654
|
+
});
|
|
3655
|
+
document.addEventListener("mousemove", function (event) {
|
|
3656
|
+
var text = event.target.getAttribute && event.target.getAttribute("data-tip");
|
|
3657
|
+
if (!text) { tip.style.display = "none"; return; }
|
|
3658
|
+
tip.textContent = text;
|
|
3659
|
+
tip.style.display = "block";
|
|
3660
|
+
var x = Math.min(event.pageX + 12, document.documentElement.scrollWidth - tip.offsetWidth - 8);
|
|
3661
|
+
tip.style.left = x + "px";
|
|
3662
|
+
tip.style.top = (event.pageY - 32) + "px";
|
|
3663
|
+
});
|
|
3664
|
+
load();
|
|
3665
|
+
setInterval(load, 60000);
|
|
3666
|
+
})();
|
|
3667
|
+
</script>
|
|
3668
|
+
</body>
|
|
3669
|
+
</html>
|
|
3670
|
+
`;
|
|
3671
|
+
|
|
3672
|
+
// src/telemetry/collector.ts
|
|
3673
|
+
var MAX_BODY_BYTES = 256 * 1024;
|
|
3674
|
+
var INSTALL_BATCHES_PER_HOUR = 30;
|
|
3675
|
+
var GLOBAL_BATCHES_PER_MINUTE = 600;
|
|
3676
|
+
var NPM_PACKAGE = "@orquentra/codebuddy";
|
|
3677
|
+
var name = z2.string().regex(/^[A-Za-z][A-Za-z0-9_:.-]{0,63}$/);
|
|
3678
|
+
var short = z2.string().max(64);
|
|
3679
|
+
var batchSchema = z2.object({
|
|
3680
|
+
installId: z2.string().uuid(),
|
|
3681
|
+
project: z2.string().regex(/^[0-9a-f]{16}$/),
|
|
3682
|
+
version: short,
|
|
3683
|
+
platform: short,
|
|
3684
|
+
arch: short,
|
|
3685
|
+
node: short,
|
|
3686
|
+
clients: z2.array(name).max(10),
|
|
3687
|
+
events: z2.array(
|
|
3688
|
+
z2.object({
|
|
3689
|
+
ts: z2.string().datetime(),
|
|
3690
|
+
kind: name,
|
|
3691
|
+
name,
|
|
3692
|
+
ms: z2.number().int().min(0).max(864e5),
|
|
3693
|
+
ok: z2.boolean(),
|
|
3694
|
+
tokensIn: z2.number().int().min(0).max(1e7).optional(),
|
|
3695
|
+
tokensOut: z2.number().int().min(0).max(1e7).optional(),
|
|
3696
|
+
session: z2.string().regex(/^[0-9a-f]{16}$/).optional()
|
|
3697
|
+
})
|
|
3698
|
+
).min(1).max(500)
|
|
3699
|
+
});
|
|
3700
|
+
async function startCollector(options) {
|
|
3701
|
+
const { dataDir } = options;
|
|
3702
|
+
await mkdir2(dataDir, { recursive: true });
|
|
3703
|
+
let summaryCache;
|
|
3704
|
+
let npmCache;
|
|
3705
|
+
const perInstall = /* @__PURE__ */ new Map();
|
|
3706
|
+
let globalCount = 0;
|
|
3707
|
+
const resetInstalls = setInterval(() => perInstall.clear(), 36e5);
|
|
3708
|
+
const resetGlobal = setInterval(() => {
|
|
3709
|
+
globalCount = 0;
|
|
3710
|
+
}, 6e4);
|
|
3711
|
+
resetInstalls.unref();
|
|
3712
|
+
resetGlobal.unref();
|
|
3713
|
+
const ingest = createServer2(async (request, response) => {
|
|
3714
|
+
const url = new URL(request.url ?? "/", "http://localhost");
|
|
3715
|
+
if (url.pathname !== "/v1/events") {
|
|
3716
|
+
response.writeHead(404).end();
|
|
3717
|
+
return;
|
|
3718
|
+
}
|
|
3719
|
+
if (request.method !== "POST") {
|
|
3720
|
+
response.writeHead(405).end();
|
|
3721
|
+
return;
|
|
3722
|
+
}
|
|
3723
|
+
if (++globalCount > GLOBAL_BATCHES_PER_MINUTE) {
|
|
3724
|
+
response.writeHead(429).end();
|
|
3725
|
+
return;
|
|
3726
|
+
}
|
|
3727
|
+
const body = await readBody(request);
|
|
3728
|
+
if (body === null) {
|
|
3729
|
+
response.writeHead(413, { connection: "close" }).end();
|
|
3730
|
+
return;
|
|
3731
|
+
}
|
|
3732
|
+
let batch;
|
|
3733
|
+
try {
|
|
3734
|
+
batch = batchSchema.parse(JSON.parse(body));
|
|
3735
|
+
} catch {
|
|
3736
|
+
response.writeHead(400).end();
|
|
3737
|
+
return;
|
|
3738
|
+
}
|
|
3739
|
+
const seen = (perInstall.get(batch.installId) ?? 0) + 1;
|
|
3740
|
+
perInstall.set(batch.installId, seen);
|
|
3741
|
+
if (seen > INSTALL_BATCHES_PER_HOUR) {
|
|
3742
|
+
response.writeHead(429).end();
|
|
3743
|
+
return;
|
|
3744
|
+
}
|
|
3745
|
+
const receivedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3746
|
+
const { events, ...meta } = batch;
|
|
3747
|
+
const lines = events.map((event) => JSON.stringify({ ...meta, ...event, receivedAt }));
|
|
3748
|
+
await appendFile(
|
|
3749
|
+
join6(dataDir, `events-${receivedAt.slice(0, 10)}.jsonl`),
|
|
3750
|
+
`${lines.join("\n")}
|
|
3751
|
+
`
|
|
3752
|
+
);
|
|
3753
|
+
response.writeHead(204).end();
|
|
3754
|
+
summaryCache = void 0;
|
|
3755
|
+
});
|
|
3756
|
+
const admin = createServer2(async (request, response) => {
|
|
3757
|
+
const host = (request.headers.host ?? "").replace(/:\d+$/, "");
|
|
3758
|
+
if (host !== "127.0.0.1" && host !== "localhost" && !host.endsWith(".ts.net")) {
|
|
3759
|
+
response.writeHead(403, { "content-type": "text/plain" }).end("Forbidden host");
|
|
3760
|
+
return;
|
|
3761
|
+
}
|
|
3762
|
+
if (options.adminPassword && !passwordMatches(request, options.adminPassword)) {
|
|
3763
|
+
response.writeHead(401, {
|
|
3764
|
+
"www-authenticate": 'Basic realm="CodeBuddy admin", charset="UTF-8"',
|
|
3765
|
+
"content-type": "text/plain"
|
|
3766
|
+
}).end("Password required");
|
|
3767
|
+
return;
|
|
3768
|
+
}
|
|
3769
|
+
if (request.method !== "GET") {
|
|
3770
|
+
response.writeHead(405).end();
|
|
3771
|
+
return;
|
|
3772
|
+
}
|
|
3773
|
+
const url = new URL(request.url ?? "/", "http://localhost");
|
|
3774
|
+
url.pathname = url.pathname.replace(/^\/admin(?=\/|$)/, "") || "/";
|
|
3775
|
+
const json2 = (value) => {
|
|
3776
|
+
response.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" });
|
|
3777
|
+
response.end(JSON.stringify(value));
|
|
3778
|
+
};
|
|
3779
|
+
if (url.pathname === "/") {
|
|
3780
|
+
response.writeHead(200, {
|
|
3781
|
+
"content-type": "text/html; charset=utf-8",
|
|
3782
|
+
"cache-control": "no-store"
|
|
3783
|
+
});
|
|
3784
|
+
response.end(ADMIN_HTML);
|
|
3785
|
+
return;
|
|
3786
|
+
}
|
|
3787
|
+
if (url.pathname === "/api/summary") {
|
|
3788
|
+
const days = Math.min(365, Math.max(1, Number(url.searchParams.get("days")) || 30));
|
|
3789
|
+
if (!summaryCache || summaryCache.days !== days || Date.now() - summaryCache.at > 3e4) {
|
|
3790
|
+
summaryCache = {
|
|
3791
|
+
at: Date.now(),
|
|
3792
|
+
days,
|
|
3793
|
+
value: summarize(await readEvents(dataDir, days), days)
|
|
3794
|
+
};
|
|
3795
|
+
}
|
|
3796
|
+
json2(summaryCache.value);
|
|
3797
|
+
return;
|
|
3798
|
+
}
|
|
3799
|
+
if (url.pathname === "/api/npm") {
|
|
3800
|
+
if (!npmCache || Date.now() - npmCache.at > 36e5) {
|
|
3801
|
+
npmCache = { at: Date.now(), value: await npmDownloads(options.fetchImpl ?? fetch) };
|
|
3802
|
+
}
|
|
3803
|
+
json2(npmCache.value);
|
|
3804
|
+
return;
|
|
3805
|
+
}
|
|
3806
|
+
response.writeHead(404, { "content-type": "text/plain" }).end("Not found");
|
|
3807
|
+
});
|
|
3808
|
+
const ingestPort = await listen2(ingest, options.ingestPort ?? 8787);
|
|
3809
|
+
const adminPort = await listen2(admin, options.adminPort ?? 8788);
|
|
3810
|
+
const close = (server) => new Promise((resolve6) => {
|
|
3811
|
+
server.closeAllConnections();
|
|
3812
|
+
server.close(() => resolve6());
|
|
3813
|
+
});
|
|
3814
|
+
return {
|
|
3815
|
+
ingestUrl: `http://127.0.0.1:${ingestPort}/v1/events`,
|
|
3816
|
+
adminUrl: `http://127.0.0.1:${adminPort}/`,
|
|
3817
|
+
close: async () => {
|
|
3818
|
+
clearInterval(resetInstalls);
|
|
3819
|
+
clearInterval(resetGlobal);
|
|
3820
|
+
await Promise.all([close(ingest), close(admin)]);
|
|
3821
|
+
}
|
|
3822
|
+
};
|
|
3823
|
+
}
|
|
3824
|
+
function passwordMatches(request, password3) {
|
|
3825
|
+
const header = request.headers.authorization ?? "";
|
|
3826
|
+
if (!header.startsWith("Basic ")) return false;
|
|
3827
|
+
const decoded = Buffer.from(header.slice(6), "base64").toString("utf8");
|
|
3828
|
+
const given = decoded.slice(decoded.indexOf(":") + 1);
|
|
3829
|
+
const digest = (value) => createHash("sha256").update(value).digest();
|
|
3830
|
+
return timingSafeEqual(digest(given), digest(password3));
|
|
3831
|
+
}
|
|
3832
|
+
function readBody(request) {
|
|
3833
|
+
return new Promise((resolve6) => {
|
|
3834
|
+
const chunks = [];
|
|
3835
|
+
let size = 0;
|
|
3836
|
+
request.on("data", (chunk) => {
|
|
3837
|
+
size += chunk.length;
|
|
3838
|
+
if (size > MAX_BODY_BYTES) resolve6(null);
|
|
3839
|
+
else chunks.push(chunk);
|
|
3840
|
+
});
|
|
3841
|
+
request.on("end", () => resolve6(Buffer.concat(chunks).toString("utf8")));
|
|
3842
|
+
request.on("error", () => resolve6(null));
|
|
3843
|
+
});
|
|
3844
|
+
}
|
|
3845
|
+
function listen2(server, port) {
|
|
3846
|
+
return new Promise((resolve6, reject) => {
|
|
3847
|
+
server.once("error", reject);
|
|
3848
|
+
server.listen(port, "127.0.0.1", () => resolve6(server.address().port));
|
|
3849
|
+
});
|
|
3850
|
+
}
|
|
3851
|
+
async function readEvents(dataDir, days, now = Date.now()) {
|
|
3852
|
+
const since = new Date(now - (days - 1) * 864e5).toISOString().slice(0, 10);
|
|
3853
|
+
const files = (await readdir(dataDir).catch(() => [])).filter((file) => /^events-\d{4}-\d{2}-\d{2}\.jsonl$/.test(file) && file.slice(7, 17) >= since).sort();
|
|
3854
|
+
const events = [];
|
|
3855
|
+
for (const file of files) {
|
|
3856
|
+
for (const line of (await readFile4(join6(dataDir, file), "utf8")).split("\n")) {
|
|
3857
|
+
if (!line) continue;
|
|
3858
|
+
try {
|
|
3859
|
+
events.push(JSON.parse(line));
|
|
3860
|
+
} catch {
|
|
3861
|
+
}
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3864
|
+
return events;
|
|
3865
|
+
}
|
|
3866
|
+
function summarize(events, days, now = Date.now()) {
|
|
3867
|
+
const day = (iso) => iso.slice(0, 10);
|
|
3868
|
+
const today = day(new Date(now).toISOString());
|
|
3869
|
+
const activeWithin = (window) => {
|
|
3870
|
+
const since = day(new Date(now - (window - 1) * 864e5).toISOString());
|
|
3871
|
+
return new Set(events.filter((e) => day(e.receivedAt) >= since).map((e) => e.installId)).size;
|
|
3872
|
+
};
|
|
3873
|
+
const daily = /* @__PURE__ */ new Map();
|
|
3874
|
+
for (let index = days - 1; index >= 0; index -= 1) {
|
|
3875
|
+
daily.set(day(new Date(now - index * 864e5).toISOString()), {
|
|
3876
|
+
installs: /* @__PURE__ */ new Set(),
|
|
3877
|
+
events: 0
|
|
3878
|
+
});
|
|
3879
|
+
}
|
|
3880
|
+
const firstSeen = /* @__PURE__ */ new Map();
|
|
3881
|
+
const tools = /* @__PURE__ */ new Map();
|
|
3882
|
+
const kinds = /* @__PURE__ */ new Map();
|
|
3883
|
+
const latest = /* @__PURE__ */ new Map();
|
|
3884
|
+
const users = /* @__PURE__ */ new Map();
|
|
3885
|
+
let errors = 0;
|
|
3886
|
+
let tokensOut = 0;
|
|
3887
|
+
for (const event of events) {
|
|
3888
|
+
const bucket = daily.get(day(event.receivedAt));
|
|
3889
|
+
if (bucket) {
|
|
3890
|
+
bucket.installs.add(event.installId);
|
|
3891
|
+
bucket.events += 1;
|
|
3892
|
+
}
|
|
3893
|
+
const seen = firstSeen.get(event.installId);
|
|
3894
|
+
if (!seen || event.receivedAt < seen) firstSeen.set(event.installId, event.receivedAt);
|
|
3895
|
+
latest.set(event.installId, event);
|
|
3896
|
+
const user = users.get(event.installId) ?? {
|
|
3897
|
+
lastSeen: event.receivedAt,
|
|
3898
|
+
events: 0,
|
|
3899
|
+
sessions: /* @__PURE__ */ new Set(),
|
|
3900
|
+
projects: /* @__PURE__ */ new Set(),
|
|
3901
|
+
features: /* @__PURE__ */ new Map()
|
|
3902
|
+
};
|
|
3903
|
+
if (event.receivedAt > user.lastSeen) user.lastSeen = event.receivedAt;
|
|
3904
|
+
user.events += 1;
|
|
3905
|
+
if (event.session) user.sessions.add(event.session);
|
|
3906
|
+
user.projects.add(event.project);
|
|
3907
|
+
const feature = event.kind === "tool" ? event.name : event.kind;
|
|
3908
|
+
user.features.set(feature, (user.features.get(feature) ?? 0) + 1);
|
|
3909
|
+
users.set(event.installId, user);
|
|
3910
|
+
kinds.set(event.kind, (kinds.get(event.kind) ?? 0) + 1);
|
|
3911
|
+
if (!event.ok) errors += 1;
|
|
3912
|
+
tokensOut += event.tokensOut ?? 0;
|
|
3913
|
+
const key = event.kind === "tool" ? event.name : `${event.kind}`;
|
|
3914
|
+
const tool = tools.get(key) ?? { calls: 0, errors: 0, installs: /* @__PURE__ */ new Set(), ms: [] };
|
|
3915
|
+
tool.calls += 1;
|
|
3916
|
+
if (!event.ok) tool.errors += 1;
|
|
3917
|
+
tool.installs.add(event.installId);
|
|
3918
|
+
tool.ms.push(event.ms);
|
|
3919
|
+
tools.set(key, tool);
|
|
3920
|
+
}
|
|
3921
|
+
const newPerDay = /* @__PURE__ */ new Map();
|
|
3922
|
+
for (const seen of firstSeen.values())
|
|
3923
|
+
newPerDay.set(day(seen), (newPerDay.get(day(seen)) ?? 0) + 1);
|
|
3924
|
+
const share = (pick) => {
|
|
3925
|
+
const counts = /* @__PURE__ */ new Map();
|
|
3926
|
+
for (const event of latest.values()) {
|
|
3927
|
+
for (const value of pick(event)) counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
3928
|
+
}
|
|
3929
|
+
return [...counts].map(([label, installs]) => ({ label, installs })).sort((a, b) => b.installs - a.installs);
|
|
3930
|
+
};
|
|
3931
|
+
const median = (values) => {
|
|
3932
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
3933
|
+
return sorted[Math.floor(sorted.length / 2)] ?? 0;
|
|
3934
|
+
};
|
|
3935
|
+
return {
|
|
3936
|
+
generatedAt: new Date(now).toISOString(),
|
|
3937
|
+
days,
|
|
3938
|
+
totals: {
|
|
3939
|
+
installs: firstSeen.size,
|
|
3940
|
+
projects: new Set(events.map((e) => `${e.installId}:${e.project}`)).size,
|
|
3941
|
+
sessions: new Set(events.filter((e) => e.session).map((e) => e.session)).size,
|
|
3942
|
+
events: events.length,
|
|
3943
|
+
errorRate: events.length ? errors / events.length : 0,
|
|
3944
|
+
tokensOut,
|
|
3945
|
+
active1d: activeWithin(1),
|
|
3946
|
+
active7d: activeWithin(7),
|
|
3947
|
+
active30d: activeWithin(30),
|
|
3948
|
+
newToday: newPerDay.get(today) ?? 0
|
|
3949
|
+
},
|
|
3950
|
+
daily: [...daily].map(([date, bucket]) => ({
|
|
3951
|
+
date,
|
|
3952
|
+
installs: bucket.installs.size,
|
|
3953
|
+
events: bucket.events,
|
|
3954
|
+
newInstalls: newPerDay.get(date) ?? 0
|
|
3955
|
+
})),
|
|
3956
|
+
features: [...tools].map(([label, tool]) => ({
|
|
3957
|
+
label,
|
|
3958
|
+
calls: tool.calls,
|
|
3959
|
+
installs: tool.installs.size,
|
|
3960
|
+
errorRate: tool.errors / tool.calls,
|
|
3961
|
+
p50ms: median(tool.ms)
|
|
3962
|
+
})).sort((a, b) => b.calls - a.calls).slice(0, 20),
|
|
3963
|
+
kinds: [...kinds].map(([label, count]) => ({ label, count })).sort((a, b) => b.count - a.count),
|
|
3964
|
+
clients: share((e) => e.clients.length ? e.clients : ["unknown"]),
|
|
3965
|
+
versions: share((e) => [e.version]),
|
|
3966
|
+
platforms: share((e) => [`${e.platform}/${e.arch}`]),
|
|
3967
|
+
nodes: share((e) => [`node ${e.node}`]),
|
|
3968
|
+
/** One row per install, most recently active first. */
|
|
3969
|
+
users: [...users].map(([installId, user]) => {
|
|
3970
|
+
const last = latest.get(installId);
|
|
3971
|
+
return {
|
|
3972
|
+
install: installId.slice(0, 8),
|
|
3973
|
+
firstSeen: firstSeen.get(installId) ?? user.lastSeen,
|
|
3974
|
+
lastSeen: user.lastSeen,
|
|
3975
|
+
events: user.events,
|
|
3976
|
+
sessions: user.sessions.size,
|
|
3977
|
+
projects: user.projects.size,
|
|
3978
|
+
client: last?.clients.join(",") || "unknown",
|
|
3979
|
+
version: last?.version ?? "",
|
|
3980
|
+
platform: last ? `${last.platform}/${last.arch}` : "",
|
|
3981
|
+
topFeature: [...user.features].sort((a, b) => b[1] - a[1])[0]?.[0] ?? ""
|
|
3982
|
+
};
|
|
3983
|
+
}).sort((a, b) => b.lastSeen.localeCompare(a.lastSeen)).slice(0, 1e3),
|
|
3984
|
+
recent: events.slice(-50).reverse().map((e) => ({
|
|
3985
|
+
receivedAt: e.receivedAt,
|
|
3986
|
+
install: e.installId.slice(0, 8),
|
|
3987
|
+
kind: e.kind,
|
|
3988
|
+
name: e.name,
|
|
3989
|
+
ms: e.ms,
|
|
3990
|
+
ok: e.ok,
|
|
3991
|
+
version: e.version,
|
|
3992
|
+
client: e.clients.join(",") || "unknown"
|
|
3993
|
+
}))
|
|
3994
|
+
};
|
|
3995
|
+
}
|
|
3996
|
+
async function npmDownloads(fetchImpl) {
|
|
3997
|
+
const to = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3998
|
+
const from = new Date(Date.now() - 364 * 864e5).toISOString().slice(0, 10);
|
|
3999
|
+
try {
|
|
4000
|
+
const response = await fetchImpl(
|
|
4001
|
+
`https://api.npmjs.org/downloads/range/${from}:${to}/${NPM_PACKAGE}`,
|
|
4002
|
+
{ signal: AbortSignal.timeout(5e3) }
|
|
4003
|
+
);
|
|
4004
|
+
if (!response.ok) return { error: `npm API ${response.status}` };
|
|
4005
|
+
const body = await response.json();
|
|
4006
|
+
return { package: NPM_PACKAGE, downloads: body.downloads ?? [] };
|
|
4007
|
+
} catch (error) {
|
|
4008
|
+
return { error: error.message };
|
|
4009
|
+
}
|
|
4010
|
+
}
|
|
4011
|
+
|
|
4012
|
+
// src/cli/commands/telemetry.ts
|
|
4013
|
+
function registerTelemetryCommands(program, runSafely2) {
|
|
4014
|
+
const telemetry = program.command("telemetry").description("Show or change anonymous usage telemetry (no prompts, code or paths).");
|
|
4015
|
+
telemetry.command("status").description("Show whether this machine sends anonymous usage data.").action(async () => {
|
|
4016
|
+
await runSafely2(async () => {
|
|
4017
|
+
const state = await readTelemetryState();
|
|
4018
|
+
const blocked = telemetryBlocked(state);
|
|
4019
|
+
console.log(
|
|
4020
|
+
blocked ? `${pc19.yellow("\u25CB")} telemetry off (${blocked})` : `${pc19.green("\u25CF")} telemetry on`
|
|
4021
|
+
);
|
|
4022
|
+
console.log(pc19.dim(` install id ${state.installId} (${telemetryStatePath()})`));
|
|
4023
|
+
if (telemetryUrl()) console.log(pc19.dim(` collector ${telemetryUrl()}`));
|
|
4024
|
+
});
|
|
4025
|
+
});
|
|
4026
|
+
telemetry.command("enable").description("Allow anonymous usage data from this machine.").action(async () => {
|
|
4027
|
+
await runSafely2(async () => {
|
|
4028
|
+
const state = await readTelemetryState();
|
|
4029
|
+
await writeTelemetryState({
|
|
4030
|
+
installId: state.installId,
|
|
4031
|
+
noticeAt: state.noticeAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
4032
|
+
});
|
|
4033
|
+
console.log(TELEMETRY_NOTICE);
|
|
4034
|
+
console.log(pc19.green("Telemetry enabled."));
|
|
4035
|
+
});
|
|
4036
|
+
});
|
|
4037
|
+
telemetry.command("disable").description("Stop sending anonymous usage data from this machine.").action(async () => {
|
|
4038
|
+
await runSafely2(async () => {
|
|
4039
|
+
const state = await readTelemetryState();
|
|
4040
|
+
await writeTelemetryState({ ...state, disabled: true });
|
|
4041
|
+
console.log("Telemetry disabled. Nothing more is sent from this machine.");
|
|
4042
|
+
});
|
|
4043
|
+
});
|
|
4044
|
+
program.command("collector").description("[admin] Receive anonymous telemetry and serve the usage admin panel.").option("--data <dir>", "Where events are stored (one JSONL file per day).", "./telemetry-data").option(
|
|
4045
|
+
"--ingest-port <port>",
|
|
4046
|
+
"127.0.0.1 port for POST /v1/events (expose publicly).",
|
|
4047
|
+
Number,
|
|
4048
|
+
8787
|
|
4049
|
+
).option(
|
|
4050
|
+
"--admin-port <port>",
|
|
4051
|
+
"127.0.0.1 port for the admin panel (keep private).",
|
|
4052
|
+
Number,
|
|
4053
|
+
8788
|
|
4054
|
+
).action(async (opts) => {
|
|
4055
|
+
await runSafely2(async () => {
|
|
4056
|
+
const collector = await startCollector({
|
|
4057
|
+
dataDir: opts.data,
|
|
4058
|
+
ingestPort: opts.ingestPort,
|
|
4059
|
+
adminPort: opts.adminPort,
|
|
4060
|
+
...process.env.CODEBUDDY_ADMIN_PASSWORD ? { adminPassword: process.env.CODEBUDDY_ADMIN_PASSWORD } : {}
|
|
4061
|
+
});
|
|
4062
|
+
console.log(`${pc19.green("\u25CF")} ingest ${pc19.bold(collector.ingestUrl)}`);
|
|
4063
|
+
console.log(`${pc19.green("\u25CF")} admin ${pc19.bold(collector.adminUrl)}`);
|
|
4064
|
+
console.log(
|
|
4065
|
+
pc19.dim(
|
|
4066
|
+
process.env.CODEBUDDY_ADMIN_PASSWORD ? " admin panel is password-protected (CODEBUDDY_ADMIN_PASSWORD)." : " admin panel has no password: keep it private, or set CODEBUDDY_ADMIN_PASSWORD."
|
|
4067
|
+
)
|
|
4068
|
+
);
|
|
4069
|
+
console.log(pc19.dim(` events in ${opts.data}. Ctrl+C to stop.`));
|
|
4070
|
+
await new Promise((resolve6) => {
|
|
4071
|
+
const stop = () => void collector.close().then(resolve6);
|
|
4072
|
+
process.once("SIGINT", stop);
|
|
4073
|
+
process.once("SIGTERM", stop);
|
|
4074
|
+
});
|
|
4075
|
+
});
|
|
4076
|
+
});
|
|
4077
|
+
}
|
|
4078
|
+
|
|
4868
4079
|
// src/cli/commands/use-command.ts
|
|
4869
4080
|
import { basename as basename2 } from "path";
|
|
4870
4081
|
import { confirm as confirm2, isCancel as isCancel2, log as log2, note as note2, password as password2, select, spinner as spinner3, text as text2 } from "@clack/prompts";
|
|
4871
|
-
import
|
|
4082
|
+
import pc20 from "picocolors";
|
|
4872
4083
|
|
|
4873
4084
|
// src/core/global-config.ts
|
|
4874
|
-
import { chmod, mkdir as
|
|
4875
|
-
import { homedir
|
|
4876
|
-
import { dirname as
|
|
4877
|
-
import { z as
|
|
4085
|
+
import { chmod, mkdir as mkdir3, readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
|
|
4086
|
+
import { homedir } from "os";
|
|
4087
|
+
import { dirname as dirname3, join as join7 } from "path";
|
|
4088
|
+
import { z as z3 } from "zod";
|
|
4878
4089
|
var GLOBAL_DIR = ".codebuddy";
|
|
4879
4090
|
var GLOBAL_FILE = "global.json";
|
|
4880
|
-
var globalConfigSchema =
|
|
4881
|
-
hfToken:
|
|
4882
|
-
databaseUrl:
|
|
4091
|
+
var globalConfigSchema = z3.object({
|
|
4092
|
+
hfToken: z3.string().min(1).optional(),
|
|
4093
|
+
databaseUrl: z3.string().min(1).optional(),
|
|
4883
4094
|
// Where the codebuddy install lives on this machine. Cached so future
|
|
4884
4095
|
// `codebuddy use` calls in unrelated projects can find the CLI entry
|
|
4885
4096
|
// without re-resolving via process.argv every time.
|
|
4886
|
-
installPath:
|
|
4097
|
+
installPath: z3.string().optional()
|
|
4887
4098
|
});
|
|
4888
|
-
function globalConfigPath(home =
|
|
4889
|
-
return
|
|
4099
|
+
function globalConfigPath(home = homedir()) {
|
|
4100
|
+
return join7(home, GLOBAL_DIR, GLOBAL_FILE);
|
|
4890
4101
|
}
|
|
4891
|
-
async function readGlobalConfig(home =
|
|
4102
|
+
async function readGlobalConfig(home = homedir()) {
|
|
4892
4103
|
const path = globalConfigPath(home);
|
|
4893
4104
|
try {
|
|
4894
|
-
const raw = await
|
|
4105
|
+
const raw = await readFile5(path, "utf8");
|
|
4895
4106
|
return globalConfigSchema.parse(JSON.parse(raw));
|
|
4896
4107
|
} catch (error) {
|
|
4897
4108
|
if (error.code === "ENOENT") return {};
|
|
4898
4109
|
throw error;
|
|
4899
4110
|
}
|
|
4900
4111
|
}
|
|
4901
|
-
async function writeGlobalConfig(next, home =
|
|
4112
|
+
async function writeGlobalConfig(next, home = homedir()) {
|
|
4902
4113
|
const path = globalConfigPath(home);
|
|
4903
|
-
await
|
|
4114
|
+
await mkdir3(dirname3(path), { recursive: true, mode: 448 });
|
|
4904
4115
|
await writeFile3(path, `${JSON.stringify(next, null, 2)}
|
|
4905
4116
|
`, { mode: 384 });
|
|
4906
4117
|
await chmod(path, 384);
|
|
4907
4118
|
return { path };
|
|
4908
4119
|
}
|
|
4909
|
-
async function mergeGlobalConfig(patch, home =
|
|
4120
|
+
async function mergeGlobalConfig(patch, home = homedir()) {
|
|
4910
4121
|
const current = await readGlobalConfig(home);
|
|
4911
4122
|
const merged = { ...current, ...patch };
|
|
4912
4123
|
const { path } = await writeGlobalConfig(merged, home);
|
|
@@ -4952,15 +4163,15 @@ async function runUseCommand(options = {}) {
|
|
|
4952
4163
|
})
|
|
4953
4164
|
));
|
|
4954
4165
|
note2(
|
|
4955
|
-
`Project folder: ${
|
|
4956
|
-
Namespace: ${
|
|
4166
|
+
`Project folder: ${pc20.bold(process.cwd())}
|
|
4167
|
+
Namespace: ${pc20.bold(namespace)}`,
|
|
4957
4168
|
"codebuddy use"
|
|
4958
4169
|
);
|
|
4959
4170
|
const stored = await readGlobalConfig();
|
|
4960
4171
|
const projectConfig = await readConfigFile();
|
|
4961
4172
|
let embeddings = projectConfig.provider?.type;
|
|
4962
4173
|
if (embeddings) {
|
|
4963
|
-
log2.info(`Embeddings: ${embeddings} (change with ${
|
|
4174
|
+
log2.info(`Embeddings: ${embeddings} (change with ${pc20.cyan("codebuddy models use")}).`);
|
|
4964
4175
|
} else if (!interactive) {
|
|
4965
4176
|
embeddings = "local";
|
|
4966
4177
|
} else {
|
|
@@ -5001,7 +4212,7 @@ Namespace: ${pc19.bold(namespace)}`,
|
|
|
5001
4212
|
log2.info("Using HF_TOKEN from environment.");
|
|
5002
4213
|
} else if (stored.hfToken) {
|
|
5003
4214
|
hfToken = stored.hfToken;
|
|
5004
|
-
log2.info(`Reusing saved Hugging Face token (${
|
|
4215
|
+
log2.info(`Reusing saved Hugging Face token (${pc20.dim("~/.codebuddy/global.json")}).`);
|
|
5005
4216
|
} else if (!interactive) {
|
|
5006
4217
|
throw new Error(
|
|
5007
4218
|
"Hugging Face embeddings need a token: set HF_TOKEN (no terminal to prompt for it)."
|
|
@@ -5054,7 +4265,7 @@ Namespace: ${pc19.bold(namespace)}`,
|
|
|
5054
4265
|
if (result.code === 0) {
|
|
5055
4266
|
composeSpinner.stop("Postgres container started.");
|
|
5056
4267
|
for (let attempt = 0; attempt < 12 && !connected; attempt++) {
|
|
5057
|
-
await new Promise((
|
|
4268
|
+
await new Promise((resolve6) => setTimeout(resolve6, 500));
|
|
5058
4269
|
connected = await pingOnce2(databaseUrl);
|
|
5059
4270
|
}
|
|
5060
4271
|
if (!connected) {
|
|
@@ -5135,7 +4346,7 @@ Namespace: ${pc19.bold(namespace)}`,
|
|
|
5135
4346
|
databaseUrl
|
|
5136
4347
|
});
|
|
5137
4348
|
log2.success(
|
|
5138
|
-
`Registered ${
|
|
4349
|
+
`Registered ${pc20.bold(install.serverKey)} in Claude Desktop. Restart with \u2318Q to activate.`
|
|
5139
4350
|
);
|
|
5140
4351
|
} catch (error) {
|
|
5141
4352
|
log2.warn(`Claude Desktop registration skipped: ${error.message}`);
|
|
@@ -5150,7 +4361,7 @@ Namespace: ${pc19.bold(namespace)}`,
|
|
|
5150
4361
|
databaseUrl
|
|
5151
4362
|
});
|
|
5152
4363
|
log2.success(
|
|
5153
|
-
`Registered ${
|
|
4364
|
+
`Registered ${pc20.bold(install.serverKey)} in Codex. Restart Codex to activate.`
|
|
5154
4365
|
);
|
|
5155
4366
|
} catch (error) {
|
|
5156
4367
|
log2.warn(`Codex registration skipped: ${error.message}`);
|
|
@@ -5158,24 +4369,24 @@ Namespace: ${pc19.bold(namespace)}`,
|
|
|
5158
4369
|
}
|
|
5159
4370
|
note2(
|
|
5160
4371
|
[
|
|
5161
|
-
`${
|
|
5162
|
-
`${
|
|
5163
|
-
`${
|
|
5164
|
-
`${
|
|
4372
|
+
`${pc20.green("\u2713")} CodeBuddy is ready for ${pc20.bold(folderName)}.`,
|
|
4373
|
+
`${pc20.green("\u2713")} Scaffolded ${pc20.cyan(".codebuddy/policies.yaml")} for project-specific guardrails.`,
|
|
4374
|
+
`${pc20.green("\u2713")} ${graphifyMessage}`,
|
|
4375
|
+
`${pc20.green("\u2713")} Added agent workflow instructions to ${pc20.cyan("CLAUDE.md")} and ${pc20.cyan("AGENTS.md")}.`,
|
|
5165
4376
|
``,
|
|
5166
4377
|
`Next:`,
|
|
5167
4378
|
` 1. Restart Claude Desktop and Codex.`,
|
|
5168
|
-
` 2. Ask Claude or Codex to use the ${
|
|
4379
|
+
` 2. Ask Claude or Codex to use the ${pc20.cyan("context_pack")} tool for each coding task.`,
|
|
5169
4380
|
` 3. Indexing, Graphify, Markdown memory import, and embeddings now update automatically while the MCP server is running.`,
|
|
5170
|
-
` 4. Run ${
|
|
4381
|
+
` 4. Run ${pc20.cyan("codebuddy doctor")} or ${pc20.cyan("codebuddy db doctor")} if you want a full health check.`,
|
|
5171
4382
|
``,
|
|
5172
4383
|
`Manage:`,
|
|
5173
|
-
` codebuddy claude list ${
|
|
5174
|
-
` codebuddy codex list ${
|
|
5175
|
-
` codebuddy db test ${
|
|
4384
|
+
` codebuddy claude list ${pc20.dim("# see every project wired up")}`,
|
|
4385
|
+
` codebuddy codex list ${pc20.dim("# see Codex entries")}`,
|
|
4386
|
+
` codebuddy db test ${pc20.dim("# quick Postgres connectivity check")}`,
|
|
5176
4387
|
` codebuddy claude remove codebuddy-${namespace}`,
|
|
5177
4388
|
` codebuddy codex remove codebuddy-${namespace}`,
|
|
5178
|
-
` codebuddy postgres down ${
|
|
4389
|
+
` codebuddy postgres down ${pc20.dim("# stop the DB (data preserved)")}`
|
|
5179
4390
|
].join("\n"),
|
|
5180
4391
|
"Done"
|
|
5181
4392
|
);
|
|
@@ -5186,7 +4397,7 @@ Namespace: ${pc19.bold(namespace)}`,
|
|
|
5186
4397
|
|
|
5187
4398
|
// src/cli/commands/worklog.ts
|
|
5188
4399
|
import { relative as relative6 } from "path";
|
|
5189
|
-
import
|
|
4400
|
+
import pc21 from "picocolors";
|
|
5190
4401
|
function registerWorklogCommands(program, runSafely2) {
|
|
5191
4402
|
const worklog = program.command("worklog").description("Inspect the cross-chat worklog of completed turns.");
|
|
5192
4403
|
worklog.command("list", { isDefault: true }).option("--limit <n>", "Maximum entries to show.", "20").option("--json", "Print machine-readable JSON.").description("List worklog entries, newest first.").action(async (opts) => {
|
|
@@ -5197,7 +4408,7 @@ function registerWorklogCommands(program, runSafely2) {
|
|
|
5197
4408
|
return;
|
|
5198
4409
|
}
|
|
5199
4410
|
if (entries.length === 0) {
|
|
5200
|
-
console.log(
|
|
4411
|
+
console.log(pc21.dim("no worklog entries yet"));
|
|
5201
4412
|
return;
|
|
5202
4413
|
}
|
|
5203
4414
|
for (const entry of entries) console.log(renderRow2(entry));
|
|
@@ -5211,36 +4422,36 @@ function registerWorklogCommands(program, runSafely2) {
|
|
|
5211
4422
|
worklog.command("latest").description("Show the most recent worklog entry.").action(async () => {
|
|
5212
4423
|
await runSafely2(async () => {
|
|
5213
4424
|
const [entry] = await new WorklogStore().list();
|
|
5214
|
-
console.log(entry ? renderEntry(entry) :
|
|
4425
|
+
console.log(entry ? renderEntry(entry) : pc21.dim("no worklog entries yet"));
|
|
5215
4426
|
});
|
|
5216
4427
|
});
|
|
5217
4428
|
}
|
|
5218
4429
|
function renderRow2(entry) {
|
|
5219
4430
|
const summary = entry.summary.replace(/\s+/g, " ");
|
|
5220
|
-
const
|
|
5221
|
-
return `${
|
|
4431
|
+
const short2 = summary.length <= 70 ? summary : `${summary.slice(0, 69)}\u2026`;
|
|
4432
|
+
return `${pc21.dim(entry.createdAt.slice(0, 16))} ${pc21.dim(entry.id)} ${short2} ${pc21.dim(`(${entry.files.length} file(s))`)}`;
|
|
5222
4433
|
}
|
|
5223
4434
|
function renderEntry(entry) {
|
|
5224
|
-
const lines = [`${
|
|
5225
|
-
if (entry.sourceSessionId) lines.push(`${
|
|
5226
|
-
if (entry.sourcePlanId) lines.push(`${
|
|
5227
|
-
if (entry.commitSha) lines.push(`${
|
|
5228
|
-
if (entry.files.length > 0) lines.push(`${
|
|
4435
|
+
const lines = [`${pc21.bold(entry.id)} ${pc21.dim(entry.createdAt)} ${pc21.dim(entry.namespace)}`];
|
|
4436
|
+
if (entry.sourceSessionId) lines.push(`${pc21.bold("session:")} ${entry.sourceSessionId}`);
|
|
4437
|
+
if (entry.sourcePlanId) lines.push(`${pc21.bold("plan:")} ${entry.sourcePlanId}`);
|
|
4438
|
+
if (entry.commitSha) lines.push(`${pc21.bold("commit:")} ${entry.commitSha}`);
|
|
4439
|
+
if (entry.files.length > 0) lines.push(`${pc21.bold("files:")} ${entry.files.join(", ")}`);
|
|
5229
4440
|
if (entry.verification.length > 0) {
|
|
5230
|
-
lines.push(`${
|
|
4441
|
+
lines.push(`${pc21.bold("verification:")}`, ...entry.verification.map((line) => ` - ${line}`));
|
|
5231
4442
|
}
|
|
5232
4443
|
if (entry.savedMemory.length > 0) {
|
|
5233
|
-
lines.push(`${
|
|
4444
|
+
lines.push(`${pc21.bold("saved memory:")} ${entry.savedMemory.join(", ")}`);
|
|
5234
4445
|
}
|
|
5235
4446
|
if (entry.queuedForReview.length > 0) {
|
|
5236
|
-
lines.push(`${
|
|
4447
|
+
lines.push(`${pc21.bold("queued for review:")} ${entry.queuedForReview.join(", ")}`);
|
|
5237
4448
|
}
|
|
5238
|
-
lines.push("", entry.summary, "",
|
|
4449
|
+
lines.push("", entry.summary, "", pc21.dim(relative6(process.cwd(), entry.path)));
|
|
5239
4450
|
return lines.join("\n");
|
|
5240
4451
|
}
|
|
5241
4452
|
|
|
5242
4453
|
// src/cli/commands/zvec.ts
|
|
5243
|
-
import
|
|
4454
|
+
import pc22 from "picocolors";
|
|
5244
4455
|
function registerZvecCommands(program, runSafely2) {
|
|
5245
4456
|
const zvec = program.command("zvec").description("Optional zvec-grep semantic file discovery for the context tools.");
|
|
5246
4457
|
zvec.command("status", { isDefault: true }).option("--json", "Print machine-readable JSON.").description("Show whether zvec-grep is enabled, installed, supported and indexed.").action(async (opts) => {
|
|
@@ -5250,22 +4461,22 @@ function registerZvecCommands(program, runSafely2) {
|
|
|
5250
4461
|
console.log(JSON.stringify(report, null, 2));
|
|
5251
4462
|
return;
|
|
5252
4463
|
}
|
|
5253
|
-
const mark = (ok2) => ok2 ?
|
|
4464
|
+
const mark = (ok2) => ok2 ? pc22.green("yes") : pc22.yellow("no");
|
|
5254
4465
|
console.log(`enabled: ${mark(report.enabled)}`);
|
|
5255
4466
|
console.log(`binary: ${report.binary}`);
|
|
5256
4467
|
console.log(
|
|
5257
|
-
`version: ${report.version ??
|
|
4468
|
+
`version: ${report.version ?? pc22.yellow("not found")}${report.version && !report.supported ? pc22.yellow(" (unsupported; need 0.2.x)") : ""}`
|
|
5258
4469
|
);
|
|
5259
4470
|
console.log(`index ready: ${mark(report.indexReady)}`);
|
|
5260
4471
|
console.log(
|
|
5261
|
-
`zg server: ${report.server} ${
|
|
4472
|
+
`zg server: ${report.server} ${pc22.dim("(running = faster queries; optional)")}`
|
|
5262
4473
|
);
|
|
5263
4474
|
console.log(`status: ${report.reason}`);
|
|
5264
|
-
if (!report.version) console.log(
|
|
4475
|
+
if (!report.version) console.log(pc22.dim("install: npm install -g @zvec/zvec-grep"));
|
|
5265
4476
|
else if (report.supported && !report.indexReady)
|
|
5266
|
-
console.log(
|
|
4477
|
+
console.log(pc22.dim("next: codebuddy zvec index"));
|
|
5267
4478
|
else if (report.indexReady && !report.enabled)
|
|
5268
|
-
console.log(
|
|
4479
|
+
console.log(pc22.dim("next: codebuddy zvec enable"));
|
|
5269
4480
|
});
|
|
5270
4481
|
});
|
|
5271
4482
|
zvec.command("index").option("--model <model>", "Embedding model for a new index.", DEFAULT_ZVEC_MODEL).option("--rebuild", "Rebuild the index from scratch (e.g. after changing model).").description("Build or refresh the zg index for this repository (explicit; never automatic).").action(async (opts) => {
|
|
@@ -5275,21 +4486,21 @@ function registerZvecCommands(program, runSafely2) {
|
|
|
5275
4486
|
...opts.rebuild ? { rebuild: true } : {}
|
|
5276
4487
|
});
|
|
5277
4488
|
if (code !== 0) throw new Error(`zg index exited with code ${code}`);
|
|
5278
|
-
console.log(
|
|
4489
|
+
console.log(pc22.dim("next: codebuddy zvec enable (.zvec-grep/ ignores itself in git)"));
|
|
5279
4490
|
});
|
|
5280
4491
|
});
|
|
5281
4492
|
zvec.command("enable").description("Use zvec-grep candidates in context_pack / context_before_edit.").action(async () => {
|
|
5282
4493
|
await runSafely2(async () => {
|
|
5283
4494
|
await setZvecEnabled(true);
|
|
5284
4495
|
console.log(
|
|
5285
|
-
`${
|
|
4496
|
+
`${pc22.green("enabled")} zvec-grep retrieval (falls back to keywords if unavailable)`
|
|
5286
4497
|
);
|
|
5287
4498
|
});
|
|
5288
4499
|
});
|
|
5289
4500
|
zvec.command("disable").description("Stop using zvec-grep; keyword ranking only.").action(async () => {
|
|
5290
4501
|
await runSafely2(async () => {
|
|
5291
4502
|
await setZvecEnabled(false);
|
|
5292
|
-
console.log(`${
|
|
4503
|
+
console.log(`${pc22.yellow("disabled")} zvec-grep retrieval`);
|
|
5293
4504
|
});
|
|
5294
4505
|
});
|
|
5295
4506
|
}
|
|
@@ -5598,6 +4809,8 @@ function createCli() {
|
|
|
5598
4809
|
registerClientsCommands(program, runSafely);
|
|
5599
4810
|
registerGitHubCommands(program, runSafely);
|
|
5600
4811
|
registerSymbolsCommand(program, runSafely);
|
|
4812
|
+
registerTelemetryCommands(program, runSafely);
|
|
4813
|
+
program.hook("preAction", () => showTelemetryNoticeOnce());
|
|
5601
4814
|
program.action(async () => {
|
|
5602
4815
|
await runSafely(() => runUseCommand());
|
|
5603
4816
|
});
|
|
@@ -5625,17 +4838,20 @@ var HELP_GROUPS = [
|
|
|
5625
4838
|
"prune"
|
|
5626
4839
|
]
|
|
5627
4840
|
],
|
|
5628
|
-
[
|
|
4841
|
+
[
|
|
4842
|
+
"Integrations:",
|
|
4843
|
+
["serve", "claude", "codex", "github", "rules", "learn", "eval", "telemetry", "collector"]
|
|
4844
|
+
]
|
|
5629
4845
|
];
|
|
5630
4846
|
function groupHelp(program) {
|
|
5631
4847
|
const groupOf = new Map(
|
|
5632
|
-
HELP_GROUPS.flatMap(([group, names]) => names.map((
|
|
4848
|
+
HELP_GROUPS.flatMap(([group, names]) => names.map((name2) => [name2, group]))
|
|
5633
4849
|
);
|
|
5634
4850
|
for (const command of program.commands) {
|
|
5635
4851
|
command.helpGroup(groupOf.get(command.name()) ?? "Other:");
|
|
5636
4852
|
}
|
|
5637
4853
|
const position = new Map(
|
|
5638
|
-
HELP_GROUPS.flatMap(([, names]) => names).map((
|
|
4854
|
+
HELP_GROUPS.flatMap(([, names]) => names).map((name2, index) => [name2, index])
|
|
5639
4855
|
);
|
|
5640
4856
|
program.commands.sort(
|
|
5641
4857
|
(a, b) => (position.get(a.name()) ?? 999) - (position.get(b.name()) ?? 999)
|
|
@@ -5665,7 +4881,7 @@ async function runSafely(fn) {
|
|
|
5665
4881
|
try {
|
|
5666
4882
|
await fn();
|
|
5667
4883
|
} catch (error) {
|
|
5668
|
-
console.error(
|
|
4884
|
+
console.error(pc23.red(String(redactSecrets(error.message))));
|
|
5669
4885
|
process.exitCode = 1;
|
|
5670
4886
|
}
|
|
5671
4887
|
}
|
|
@@ -5679,9 +4895,9 @@ function parseDuration(value) {
|
|
|
5679
4895
|
return amount * 60 * 1e3;
|
|
5680
4896
|
}
|
|
5681
4897
|
function printDoctor(report) {
|
|
5682
|
-
const ok2 =
|
|
5683
|
-
const bad =
|
|
5684
|
-
const warn2 =
|
|
4898
|
+
const ok2 = pc23.green("ok");
|
|
4899
|
+
const bad = pc23.red("fail");
|
|
4900
|
+
const warn2 = pc23.yellow("warn");
|
|
5685
4901
|
console.log(`scaffold root: ${report.scaffold.codebuddyDir.ok ? ok2 : bad}`);
|
|
5686
4902
|
if (report.scaffold.codebuddyDir.warning)
|
|
5687
4903
|
console.log(` ${report.scaffold.codebuddyDir.warning}`);
|
|
@@ -5707,7 +4923,7 @@ function printDoctor(report) {
|
|
|
5707
4923
|
);
|
|
5708
4924
|
console.log(`estimated daily cap remaining: ${report.usage.estimatedDailyCapRemaining}`);
|
|
5709
4925
|
if (report.usage.warning80Percent)
|
|
5710
|
-
console.log(
|
|
4926
|
+
console.log(pc23.yellow("usage warning: daily cap is over 80% used"));
|
|
5711
4927
|
console.log(
|
|
5712
4928
|
`config permissions: ${report.config.exists ? report.config.mode : "missing"} ${report.config.ok ? ok2 : warn2}`
|
|
5713
4929
|
);
|
|
@@ -5717,9 +4933,9 @@ function printDoctor(report) {
|
|
|
5717
4933
|
}
|
|
5718
4934
|
}
|
|
5719
4935
|
function printDbDoctor(report) {
|
|
5720
|
-
const ok2 =
|
|
5721
|
-
const bad =
|
|
5722
|
-
const warn2 =
|
|
4936
|
+
const ok2 = pc23.green("ok");
|
|
4937
|
+
const bad = pc23.red("fail");
|
|
4938
|
+
const warn2 = pc23.yellow("warn");
|
|
5723
4939
|
console.log(`DB connectivity: ${report.db.ok ? ok2 : bad}`);
|
|
5724
4940
|
if (report.db.postgresUrlSource) console.log(` url source: ${report.db.postgresUrlSource}`);
|
|
5725
4941
|
if (report.db.error) console.log(` ${report.db.error}`);
|
|
@@ -5735,4 +4951,4 @@ function printDbDoctor(report) {
|
|
|
5735
4951
|
export {
|
|
5736
4952
|
createCli
|
|
5737
4953
|
};
|
|
5738
|
-
//# sourceMappingURL=main-
|
|
4954
|
+
//# sourceMappingURL=main-QUASSFP7.js.map
|