@orquentra/codebuddy 2.7.0 → 2.9.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-2WH2Q4TA.js} +180 -22
- package/dist/chunk-2WH2Q4TA.js.map +1 -0
- 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-ENKDFLOO.js} +2 -2
- package/dist/{chunk-YJIA2MFL.js → chunk-F4BJ3DXW.js} +15 -13
- package/dist/{chunk-YJIA2MFL.js.map → chunk-F4BJ3DXW.js.map} +1 -1
- 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-QN76UJP7.js} +2 -2
- package/dist/{chunk-JUAFCLRB.js → chunk-SRA4MDVG.js} +2 -2
- package/dist/{chunk-JUAFCLRB.js.map → chunk-SRA4MDVG.js.map} +1 -1
- 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-YN7TEIUH.js} +1 -1
- package/dist/chunk-YN7TEIUH.js.map +1 -0
- 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-YJKSYFPT.js} +1422 -1772
- package/dist/main-YJKSYFPT.js.map +1 -0
- package/dist/{operations-7BTAAUHF.js → operations-OG4ISWIM.js} +3 -3
- package/dist/{pretool-command-3SXQSMAO.js → pretool-command-PRFOIHMW.js} +3 -3
- package/dist/{runtime-E7JQZ2BZ.js → runtime-GCSEIAVD.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-ENKDFLOO.js.map} +0 -0
- /package/dist/{chunk-MSUAVR3R.js.map → chunk-GXOML4UZ.js.map} +0 -0
- /package/dist/{chunk-AK2UAE32.js.map → chunk-QN76UJP7.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-OG4ISWIM.js.map} +0 -0
- /package/dist/{summaries-XV3JGGCC.js.map → pretool-command-PRFOIHMW.js.map} +0 -0
- /package/dist/{zvec-KP4KNB7Y.js.map → runtime-GCSEIAVD.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-ENKDFLOO.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,26 @@ 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
|
+
traceCliCommand,
|
|
69
|
+
watchRepository,
|
|
70
|
+
writeTelemetryState
|
|
71
|
+
} from "./chunk-2WH2Q4TA.js";
|
|
30
72
|
import {
|
|
31
73
|
DEFAULT_ZVEC_MODEL,
|
|
32
74
|
buildZvecIndex,
|
|
33
75
|
zvecStatus
|
|
34
|
-
} from "./chunk-
|
|
76
|
+
} from "./chunk-GXOML4UZ.js";
|
|
35
77
|
import {
|
|
36
78
|
PlanFileStore,
|
|
37
79
|
PlanLifecycle,
|
|
@@ -49,21 +91,21 @@ import {
|
|
|
49
91
|
runResetHook,
|
|
50
92
|
runStageHook,
|
|
51
93
|
runSyncHook
|
|
52
|
-
} from "./chunk-
|
|
53
|
-
import {
|
|
54
|
-
parseTraceLines,
|
|
55
|
-
readTraces,
|
|
56
|
-
tracePath
|
|
57
|
-
} from "./chunk-TUR6YKEE.js";
|
|
58
|
-
import "./chunk-DFV2SK6C.js";
|
|
94
|
+
} from "./chunk-F4BJ3DXW.js";
|
|
59
95
|
import {
|
|
60
96
|
SessionCapsuleLedger,
|
|
61
97
|
detectTestCommand,
|
|
62
98
|
estimateTokens
|
|
63
99
|
} from "./chunk-GKNYMNFM.js";
|
|
100
|
+
import {
|
|
101
|
+
parseTraceLines,
|
|
102
|
+
readTraces,
|
|
103
|
+
tracePath
|
|
104
|
+
} from "./chunk-YN7TEIUH.js";
|
|
64
105
|
import {
|
|
65
106
|
hookRoot
|
|
66
107
|
} from "./chunk-TGFTKWOR.js";
|
|
108
|
+
import "./chunk-DFV2SK6C.js";
|
|
67
109
|
import {
|
|
68
110
|
bootstrapDatabase,
|
|
69
111
|
buildProviderFromConfig,
|
|
@@ -75,11 +117,11 @@ import {
|
|
|
75
117
|
pingDatabase,
|
|
76
118
|
runDoctor,
|
|
77
119
|
runMigrations
|
|
78
|
-
} from "./chunk-
|
|
120
|
+
} from "./chunk-QN76UJP7.js";
|
|
79
121
|
import {
|
|
80
122
|
MemoryFileStore,
|
|
81
123
|
VERSION
|
|
82
|
-
} from "./chunk-
|
|
124
|
+
} from "./chunk-SRA4MDVG.js";
|
|
83
125
|
import {
|
|
84
126
|
computeFactHash,
|
|
85
127
|
generateEntityId
|
|
@@ -92,23 +134,25 @@ import {
|
|
|
92
134
|
import {
|
|
93
135
|
loadLlmSummaries,
|
|
94
136
|
summarizeRepository
|
|
95
|
-
} from "./chunk-
|
|
137
|
+
} from "./chunk-6NZEZJYD.js";
|
|
96
138
|
import {
|
|
97
139
|
chatJson,
|
|
98
140
|
coolingDown,
|
|
99
141
|
usageToday
|
|
100
142
|
} from "./chunk-ESZXCZIN.js";
|
|
101
143
|
import {
|
|
102
|
-
buildArchitectureMap,
|
|
103
144
|
generateGraphifyGraph,
|
|
104
|
-
indexRepository,
|
|
105
|
-
loadFreshIndex,
|
|
106
145
|
loadGraphifyMap,
|
|
107
|
-
neighbours,
|
|
108
|
-
queryMap,
|
|
109
146
|
setupGraphify,
|
|
110
147
|
syncFileVectors
|
|
111
|
-
} from "./chunk-
|
|
148
|
+
} from "./chunk-TLWGZVPC.js";
|
|
149
|
+
import {
|
|
150
|
+
buildArchitectureMap,
|
|
151
|
+
indexRepository,
|
|
152
|
+
loadFreshIndex,
|
|
153
|
+
neighbours,
|
|
154
|
+
queryMap
|
|
155
|
+
} from "./chunk-ITQO632W.js";
|
|
112
156
|
import {
|
|
113
157
|
extractSymbolTable,
|
|
114
158
|
supportsSymbols
|
|
@@ -143,14 +187,12 @@ import {
|
|
|
143
187
|
setZvecEnabled,
|
|
144
188
|
writeConfigFile
|
|
145
189
|
} from "./chunk-KNR2P2UY.js";
|
|
146
|
-
import
|
|
147
|
-
writeAtomic
|
|
148
|
-
} from "./chunk-CG5B3LYG.js";
|
|
190
|
+
import "./chunk-CG5B3LYG.js";
|
|
149
191
|
import "./chunk-PZ5AY32C.js";
|
|
150
192
|
|
|
151
193
|
// src/cli/main.ts
|
|
152
194
|
import { Command } from "commander";
|
|
153
|
-
import
|
|
195
|
+
import pc23 from "picocolors";
|
|
154
196
|
|
|
155
197
|
// src/integrations/github.ts
|
|
156
198
|
import { constants } from "fs";
|
|
@@ -337,7 +379,7 @@ function runTestCommand(command, source, repositoryRoot, timeoutMs) {
|
|
|
337
379
|
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
338
380
|
throw new Error("--timeout must be a positive number of milliseconds.");
|
|
339
381
|
}
|
|
340
|
-
return new Promise((
|
|
382
|
+
return new Promise((resolve6) => {
|
|
341
383
|
const startedAt = Date.now();
|
|
342
384
|
const child = spawn(command, {
|
|
343
385
|
cwd: repositoryRoot,
|
|
@@ -362,7 +404,7 @@ ${output.slice(-MAX_TEST_OUTPUT_CHARS)}`;
|
|
|
362
404
|
}, timeoutMs);
|
|
363
405
|
child.on("close", (exitCode, signal) => {
|
|
364
406
|
clearTimeout(timer);
|
|
365
|
-
|
|
407
|
+
resolve6({
|
|
366
408
|
status: timedOut ? "timed_out" : exitCode === 0 ? "passed" : "failed",
|
|
367
409
|
command,
|
|
368
410
|
source,
|
|
@@ -374,7 +416,7 @@ ${output.slice(-MAX_TEST_OUTPUT_CHARS)}`;
|
|
|
374
416
|
});
|
|
375
417
|
child.on("error", (error) => {
|
|
376
418
|
clearTimeout(timer);
|
|
377
|
-
|
|
419
|
+
resolve6({
|
|
378
420
|
status: "failed",
|
|
379
421
|
command,
|
|
380
422
|
source,
|
|
@@ -585,1016 +627,17 @@ function testBadge(status) {
|
|
|
585
627
|
return pc.yellow("[not run]");
|
|
586
628
|
}
|
|
587
629
|
|
|
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
630
|
// src/cli/commands/clients.ts
|
|
682
631
|
import { relative } from "path";
|
|
683
632
|
import pc2 from "picocolors";
|
|
684
633
|
|
|
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
634
|
// src/clients/cost.ts
|
|
778
|
-
import { mkdtemp, rm
|
|
635
|
+
import { mkdtemp, rm } from "fs/promises";
|
|
779
636
|
import { tmpdir } from "os";
|
|
780
|
-
import { join as
|
|
637
|
+
import { join as join2, resolve as resolve2 } from "path";
|
|
781
638
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
782
639
|
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
783
640
|
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
641
|
async function mcpSchemaTokens(root, namespace) {
|
|
1599
642
|
const server = new McpServer({ name: "codebuddy", version: VERSION });
|
|
1600
643
|
registerCodeBuddyTools(server, {
|
|
@@ -1624,7 +667,7 @@ async function hookBlockTokens(root, namespace) {
|
|
|
1624
667
|
namespace,
|
|
1625
668
|
useGit: true
|
|
1626
669
|
}).catch(() => null);
|
|
1627
|
-
const scratch = await mkdtemp(
|
|
670
|
+
const scratch = await mkdtemp(join2(tmpdir(), "codebuddy-cost-"));
|
|
1628
671
|
try {
|
|
1629
672
|
const ledger = new SessionCapsuleLedger(scratch);
|
|
1630
673
|
const render = () => renderContextBlock(bootstrap, beforeEdit, ledger, "cost", true);
|
|
@@ -1642,11 +685,11 @@ async function hookBlockTokens(root, namespace) {
|
|
|
1642
685
|
lean: { first: estimateTokens(leanFirst.text), later: estimateTokens(leanLater.text) }
|
|
1643
686
|
};
|
|
1644
687
|
} finally {
|
|
1645
|
-
await
|
|
688
|
+
await rm(scratch, { recursive: true, force: true });
|
|
1646
689
|
}
|
|
1647
690
|
}
|
|
1648
691
|
async function measureClientCosts(projectRoot = process.cwd(), namespace) {
|
|
1649
|
-
const root =
|
|
692
|
+
const root = resolve2(projectRoot);
|
|
1650
693
|
const ns = namespace ?? await resolveNamespaceFrom(root);
|
|
1651
694
|
const mcpSchema = await mcpSchemaTokens(root, ns);
|
|
1652
695
|
const block = await hookBlockTokens(root, ns);
|
|
@@ -1676,506 +719,10 @@ async function measureClientCosts(projectRoot = process.cwd(), namespace) {
|
|
|
1676
719
|
});
|
|
1677
720
|
}
|
|
1678
721
|
|
|
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);
|
|
2139
|
-
}
|
|
2140
|
-
return live;
|
|
2141
|
-
}
|
|
2142
|
-
async function setHooks(root, enabled, client = "claude", variant = "full") {
|
|
2143
|
-
const result = await setClientHooks(root, client, enabled, variant);
|
|
2144
|
-
if (!result) return null;
|
|
2145
|
-
const action = result.changed ? enabled ? "installed" : "removed" : "unchanged";
|
|
2146
|
-
return { surface: "hooks", target: result.target, action };
|
|
2147
|
-
}
|
|
2148
|
-
async function detectClient(client, projectRoot = process.cwd(), namespace) {
|
|
2149
|
-
const root = resolve9(projectRoot);
|
|
2150
|
-
const adapter = CLIENT_ADAPTERS[client];
|
|
2151
|
-
const rulesMode = await readManagedMode(join11(root, adapter.rulesFile));
|
|
2152
|
-
const hooks = await clientHooksInstalled(root, client);
|
|
2153
|
-
const name = `codebuddy-${namespace ?? await resolveNamespaceFrom(root)}`;
|
|
2154
|
-
const mcp = "file" in adapter.mcp ? await hasMcpServer(
|
|
2155
|
-
await projectMcpPath(root, adapter.mcp.file, adapter.mcp.format),
|
|
2156
|
-
adapter.mcp.format,
|
|
2157
|
-
name
|
|
2158
|
-
) : await hasGlobalMcp(adapter.mcp.global, name).catch(() => false);
|
|
2159
|
-
const warnings = [];
|
|
2160
|
-
if (hooks && rulesMode === "agent") {
|
|
2161
|
-
warnings.push(
|
|
2162
|
-
`hooks are installed but ${adapter.rulesFile} is agent-driven: retrieval and capture run twice. Run \`codebuddy clients install ${client} --mode hooks\`.`
|
|
2163
|
-
);
|
|
2164
|
-
}
|
|
2165
|
-
if (rulesMode === "hooks" && hooks === false) {
|
|
2166
|
-
warnings.push(
|
|
2167
|
-
`${adapter.rulesFile} is hook-driven but hooks are not installed: turns are not captured.`
|
|
2168
|
-
);
|
|
2169
|
-
}
|
|
2170
|
-
const effectiveRules = hooks === null && rulesMode === "hooks" ? "agent" : rulesMode;
|
|
2171
|
-
const mode = hooks ? await clientHooksVariant(root, client) === "lean" ? "lean" : "hooks" : effectiveRules ?? (mcp ? "mcp-only" : "none");
|
|
2172
|
-
return { client, mode, rulesMode, hooks, mcp, warnings };
|
|
2173
|
-
}
|
|
2174
|
-
|
|
2175
722
|
// src/clients/verify.ts
|
|
2176
723
|
import { execFile } from "child_process";
|
|
2177
|
-
import { readFile
|
|
2178
|
-
import { join as
|
|
724
|
+
import { readFile, realpath } from "fs/promises";
|
|
725
|
+
import { join as join3, resolve as resolve3 } from "path";
|
|
2179
726
|
import { pathToFileURL } from "url";
|
|
2180
727
|
var runHost = (bin, args, cwd) => new Promise((done) => {
|
|
2181
728
|
execFile(
|
|
@@ -2185,9 +732,9 @@ var runHost = (bin, args, cwd) => new Promise((done) => {
|
|
|
2185
732
|
(error, stdout) => done({ ok: !error, stdout: String(stdout ?? "") })
|
|
2186
733
|
);
|
|
2187
734
|
});
|
|
2188
|
-
var pass = (
|
|
2189
|
-
var fail = (
|
|
2190
|
-
var skip = (
|
|
735
|
+
var pass = (name2, detail) => ({ name: name2, status: "pass", detail });
|
|
736
|
+
var fail = (name2, detail) => ({ name: name2, status: "fail", detail });
|
|
737
|
+
var skip = (name2, detail) => ({ name: name2, status: "skip", detail });
|
|
2191
738
|
function json(text3) {
|
|
2192
739
|
try {
|
|
2193
740
|
return JSON.parse(text3);
|
|
@@ -2196,48 +743,48 @@ function json(text3) {
|
|
|
2196
743
|
}
|
|
2197
744
|
}
|
|
2198
745
|
var CONFORMANCE = {
|
|
2199
|
-
claude: async ({ root, name, run }) => {
|
|
746
|
+
claude: async ({ root, name: name2, run }) => {
|
|
2200
747
|
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",
|
|
748
|
+
const mcp = await run("claude", ["mcp", "get", name2], root);
|
|
2202
749
|
return [
|
|
2203
750
|
hooks,
|
|
2204
|
-
mcp.ok ? pass("mcp", `claude mcp get ${
|
|
751
|
+
mcp.ok ? pass("mcp", `claude mcp get ${name2}: ${mcp.stdout.match(/Scope: (.+)/)?.[1] ?? "found"}`) : skip("mcp", `claude mcp get ${name2}: not configured`)
|
|
2205
752
|
];
|
|
2206
753
|
},
|
|
2207
|
-
opencode: async ({ root, name, run }) => {
|
|
754
|
+
opencode: async ({ root, name: name2, run }) => {
|
|
2208
755
|
const result = await run("opencode", ["debug", "config"], root);
|
|
2209
756
|
const config = json(result.stdout);
|
|
2210
757
|
if (!config) return [fail("config", "opencode debug config returned no JSON")];
|
|
2211
758
|
const installed = await clientHooksInstalled(root, "opencode");
|
|
2212
759
|
const href = pathToFileURL(
|
|
2213
|
-
|
|
760
|
+
join3(await realpath(root).catch(() => root), ".opencode", "plugins", "codebuddy.js")
|
|
2214
761
|
).href;
|
|
2215
762
|
const loaded = config.plugin?.includes(href) === true;
|
|
2216
763
|
return [
|
|
2217
764
|
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?.[
|
|
765
|
+
config.mcp?.[name2] ? pass("mcp", `opencode resolves the ${name2} MCP entry`) : skip("mcp", `no ${name2} entry in the resolved config`)
|
|
2219
766
|
];
|
|
2220
767
|
},
|
|
2221
|
-
codex: async ({ root, name, run }) => {
|
|
768
|
+
codex: async ({ root, name: name2, run }) => {
|
|
2222
769
|
const result = await run("codex", ["mcp", "list", "--json"], root);
|
|
2223
|
-
const
|
|
2224
|
-
if (!Array.isArray(
|
|
770
|
+
const servers = json(result.stdout);
|
|
771
|
+
if (!Array.isArray(servers)) return [fail("mcp", "codex mcp list --json returned no JSON")];
|
|
2225
772
|
return [
|
|
2226
|
-
|
|
773
|
+
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
774
|
];
|
|
2228
775
|
},
|
|
2229
|
-
hermes: async ({ root, name, run, env }) => {
|
|
776
|
+
hermes: async ({ root, name: name2, run, env }) => {
|
|
2230
777
|
const checks = [];
|
|
2231
778
|
const listed = await run("hermes", ["mcp", "list"], root);
|
|
2232
779
|
checks.push(
|
|
2233
|
-
listed.ok && listed.stdout.includes(
|
|
780
|
+
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
781
|
);
|
|
2235
782
|
if (!await hermesPluginInstalled(root)) {
|
|
2236
783
|
checks.push(skip("plugin", "not installed in this repo"));
|
|
2237
784
|
return checks;
|
|
2238
785
|
}
|
|
2239
786
|
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
|
|
787
|
+
await readFile(join3(hermesConfigPath(), "..", ".env"), "utf8").catch(() => "")
|
|
2241
788
|
);
|
|
2242
789
|
const plugins = (await readHermesConfig(hermesConfigPath()))?.plugins;
|
|
2243
790
|
const enabled = Array.isArray(plugins?.enabled) && plugins.enabled.includes("codebuddy");
|
|
@@ -2249,7 +796,7 @@ var CONFORMANCE = {
|
|
|
2249
796
|
}
|
|
2250
797
|
};
|
|
2251
798
|
async function verifyClient(client, options = {}) {
|
|
2252
|
-
const root =
|
|
799
|
+
const root = resolve3(options.root ?? process.cwd());
|
|
2253
800
|
const run = options.run ?? runHost;
|
|
2254
801
|
const adapter = CLIENT_ADAPTERS[client];
|
|
2255
802
|
const version = await run(adapter.host.bin, ["--version"], root);
|
|
@@ -2268,9 +815,9 @@ async function verifyClient(client, options = {}) {
|
|
|
2268
815
|
checks: [skip("host", `${adapter.host.bin} not found or printed no version`)]
|
|
2269
816
|
};
|
|
2270
817
|
}
|
|
2271
|
-
const
|
|
818
|
+
const name2 = `codebuddy-${options.namespace ?? await resolveNamespaceFrom(root)}`;
|
|
2272
819
|
const conformance = CONFORMANCE[client];
|
|
2273
|
-
const checks = conformance ? await conformance({ root, name, run, env: options.env ?? process.env }).catch(
|
|
820
|
+
const checks = conformance ? await conformance({ root, name: name2, run, env: options.env ?? process.env }).catch(
|
|
2274
821
|
(error) => [fail("conformance", error.message)]
|
|
2275
822
|
) : [];
|
|
2276
823
|
return {
|
|
@@ -2653,7 +1200,7 @@ th.n { text-align: right; }
|
|
|
2653
1200
|
.chip { display: inline-block; font: 600 10px var(--mono); text-transform: uppercase; letter-spacing: .06em; padding: 2px 6px; border-radius: 4px; }
|
|
2654
1201
|
.chip.recall { background: var(--recall-soft); color: var(--recall); }
|
|
2655
1202
|
.chip.tool { background: var(--tool-soft); color: var(--tool); }
|
|
2656
|
-
.chip.capture, .chip.sync, .chip.guard, .chip.kw { background: var(--line); color: var(--capture); }
|
|
1203
|
+
.chip.capture, .chip.sync, .chip.guard, .chip.cli, .chip.kw { background: var(--line); color: var(--capture); }
|
|
2657
1204
|
.chip.err { background: var(--bad-soft); color: var(--bad); }
|
|
2658
1205
|
.feed { display: grid; gap: 10px; max-height: 520px; overflow: auto; }
|
|
2659
1206
|
.prompt { border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; }
|
|
@@ -3008,12 +1555,12 @@ data: ${data}
|
|
|
3008
1555
|
heartbeat.unref();
|
|
3009
1556
|
return {
|
|
3010
1557
|
url: `http://127.0.0.1:${port}/`,
|
|
3011
|
-
close: () => new Promise((
|
|
1558
|
+
close: () => new Promise((resolve6) => {
|
|
3012
1559
|
clearInterval(heartbeat);
|
|
3013
1560
|
unwatchFile(path);
|
|
3014
1561
|
for (const client of clients) client.end();
|
|
3015
1562
|
server.closeAllConnections();
|
|
3016
|
-
server.close(() =>
|
|
1563
|
+
server.close(() => resolve6());
|
|
3017
1564
|
})
|
|
3018
1565
|
};
|
|
3019
1566
|
}
|
|
@@ -3021,11 +1568,11 @@ async function listen(server, port) {
|
|
|
3021
1568
|
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
3022
1569
|
const candidate = port === 0 ? 0 : port + attempt;
|
|
3023
1570
|
try {
|
|
3024
|
-
await new Promise((
|
|
1571
|
+
await new Promise((resolve6, reject) => {
|
|
3025
1572
|
server.once("error", reject);
|
|
3026
1573
|
server.listen(candidate, "127.0.0.1", () => {
|
|
3027
1574
|
server.off("error", reject);
|
|
3028
|
-
|
|
1575
|
+
resolve6();
|
|
3029
1576
|
});
|
|
3030
1577
|
});
|
|
3031
1578
|
return server.address().port;
|
|
@@ -3063,8 +1610,8 @@ function registerDashboardCommand(program, runSafely2) {
|
|
|
3063
1610
|
);
|
|
3064
1611
|
console.log(pc4.dim(" Ctrl+C to stop."));
|
|
3065
1612
|
if (opts.open) openBrowser(dashboard.url);
|
|
3066
|
-
await new Promise((
|
|
3067
|
-
const stop = () => void dashboard.close().then(
|
|
1613
|
+
await new Promise((resolve6) => {
|
|
1614
|
+
const stop = () => void dashboard.close().then(resolve6);
|
|
3068
1615
|
process.once("SIGINT", stop);
|
|
3069
1616
|
process.once("SIGTERM", stop);
|
|
3070
1617
|
});
|
|
@@ -3076,20 +1623,20 @@ function registerDashboardCommand(program, runSafely2) {
|
|
|
3076
1623
|
import pc5 from "picocolors";
|
|
3077
1624
|
|
|
3078
1625
|
// src/eval/runner.ts
|
|
3079
|
-
import { readFile as
|
|
3080
|
-
import { dirname as
|
|
1626
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
1627
|
+
import { dirname as dirname2, isAbsolute, join as join4, resolve as resolve4 } from "path";
|
|
3081
1628
|
async function runEvaluation(input = {}) {
|
|
3082
|
-
const datasetPath =
|
|
3083
|
-
input.datasetPath ??
|
|
1629
|
+
const datasetPath = resolve4(
|
|
1630
|
+
input.datasetPath ?? join4(process.cwd(), "evals", "change-readiness.json")
|
|
3084
1631
|
);
|
|
3085
|
-
const dataset = JSON.parse(await
|
|
1632
|
+
const dataset = JSON.parse(await readFile2(datasetPath, "utf8"));
|
|
3086
1633
|
if (dataset.version !== 1 || !Array.isArray(dataset.cases)) {
|
|
3087
1634
|
throw new Error(`Unsupported evaluation dataset: ${datasetPath}`);
|
|
3088
1635
|
}
|
|
3089
|
-
const datasetDirectory =
|
|
1636
|
+
const datasetDirectory = dirname2(datasetPath);
|
|
3090
1637
|
const results = await Promise.all(
|
|
3091
1638
|
dataset.cases.map(async (evaluationCase) => {
|
|
3092
|
-
const fixtureRoot = isAbsolute(evaluationCase.fixture) ? evaluationCase.fixture :
|
|
1639
|
+
const fixtureRoot = isAbsolute(evaluationCase.fixture) ? evaluationCase.fixture : resolve4(input.repositoryRoot ?? datasetDirectory, evaluationCase.fixture);
|
|
3093
1640
|
const report = await buildChangeReport({
|
|
3094
1641
|
repositoryRoot: fixtureRoot,
|
|
3095
1642
|
namespace: `eval:${evaluationCase.id}`,
|
|
@@ -3253,6 +1800,7 @@ function registerHooksCommands(program, runSafely2) {
|
|
|
3253
1800
|
);
|
|
3254
1801
|
if (result.saved.length > 0) syncInBackground(hookRoot(input.cwd));
|
|
3255
1802
|
}
|
|
1803
|
+
await flushTelemetry(hookRoot(input.cwd));
|
|
3256
1804
|
});
|
|
3257
1805
|
});
|
|
3258
1806
|
hooks.command("sync").description(
|
|
@@ -3373,7 +1921,7 @@ async function llmStatusReport(root, env = process.env) {
|
|
|
3373
1921
|
};
|
|
3374
1922
|
}
|
|
3375
1923
|
function formatLlmStatus(report) {
|
|
3376
|
-
const features = report.features ? Object.entries(report.features).map(([
|
|
1924
|
+
const features = report.features ? Object.entries(report.features).map(([name2, on]) => `${name2} ${on ? "on" : "off"}`).join(", ") : "-";
|
|
3377
1925
|
return [
|
|
3378
1926
|
`status: ${report.enabled ? "on" : `off (${report.reason})`}`,
|
|
3379
1927
|
`provider: ${report.provider}${report.model ? ` ${report.model}` : ""}`,
|
|
@@ -3579,7 +2127,7 @@ async function runInitWizard() {
|
|
|
3579
2127
|
if (result.code === 0) {
|
|
3580
2128
|
dockerSpinner.stop("Postgres container started.");
|
|
3581
2129
|
for (let attempt = 0; attempt < 10 && !connected; attempt++) {
|
|
3582
|
-
await new Promise((
|
|
2130
|
+
await new Promise((resolve6) => setTimeout(resolve6, 500));
|
|
3583
2131
|
connected = await pingOnce(databaseUrl);
|
|
3584
2132
|
}
|
|
3585
2133
|
if (!connected) {
|
|
@@ -3871,8 +2419,8 @@ function openEditor(path) {
|
|
|
3871
2419
|
}
|
|
3872
2420
|
|
|
3873
2421
|
// src/cli/commands/models.ts
|
|
3874
|
-
import { existsSync
|
|
3875
|
-
import { join as
|
|
2422
|
+
import { existsSync } from "fs";
|
|
2423
|
+
import { join as join5 } from "path";
|
|
3876
2424
|
import { spinner as spinner2 } from "@clack/prompts";
|
|
3877
2425
|
import pc13 from "picocolors";
|
|
3878
2426
|
|
|
@@ -4157,8 +2705,8 @@ function keyPresent(provider, apiKey) {
|
|
|
4157
2705
|
return Boolean(process.env[KEY_ENV2[provider]] || provider === "huggingface" && apiKey);
|
|
4158
2706
|
}
|
|
4159
2707
|
function localModelDownloaded(id, revision) {
|
|
4160
|
-
const dir =
|
|
4161
|
-
return
|
|
2708
|
+
const dir = join5(modelCacheDir(), id, ...revision ? [revision] : []);
|
|
2709
|
+
return existsSync(join5(dir, "onnx", "model_quantized.onnx"));
|
|
4162
2710
|
}
|
|
4163
2711
|
async function vectorCounts(postgresUrl, namespace, space) {
|
|
4164
2712
|
const client = createDatabaseClient({ postgresUrl, maxConnections: 1, connectTimeoutSeconds: 3 });
|
|
@@ -4501,14 +3049,14 @@ function renderPlan(plan) {
|
|
|
4501
3049
|
}
|
|
4502
3050
|
function openEditor2(path) {
|
|
4503
3051
|
const editor = process.env.EDITOR ?? process.env.VISUAL ?? "vi";
|
|
4504
|
-
return new Promise((
|
|
3052
|
+
return new Promise((resolve6, reject) => {
|
|
4505
3053
|
const child = spawn5(editor, [path], { stdio: "inherit" });
|
|
4506
3054
|
child.on("error", reject);
|
|
4507
|
-
child.on("close", () =>
|
|
3055
|
+
child.on("close", () => resolve6());
|
|
4508
3056
|
});
|
|
4509
3057
|
}
|
|
4510
3058
|
function runGit(args) {
|
|
4511
|
-
return new Promise((
|
|
3059
|
+
return new Promise((resolve6, reject) => {
|
|
4512
3060
|
const child = spawn5("git", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
4513
3061
|
let stdout = "";
|
|
4514
3062
|
let stderr = "";
|
|
@@ -4519,7 +3067,7 @@ function runGit(args) {
|
|
|
4519
3067
|
stderr += String(c);
|
|
4520
3068
|
});
|
|
4521
3069
|
child.on("error", reject);
|
|
4522
|
-
child.on("close", (code) =>
|
|
3070
|
+
child.on("close", (code) => resolve6({ code: code ?? 0, stdout, stderr }));
|
|
4523
3071
|
});
|
|
4524
3072
|
}
|
|
4525
3073
|
function registerPlanCommands(program, runSafely2) {
|
|
@@ -4720,147 +3268,1239 @@ async function resolveMode(client, value) {
|
|
|
4720
3268
|
if (!supportedModes(CLIENT_ADAPTERS[client]).includes(mode)) {
|
|
4721
3269
|
throw new Error(`${clientDisplayName(client)} does not support --mode ${mode}.`);
|
|
4722
3270
|
}
|
|
4723
|
-
return mode;
|
|
4724
|
-
}
|
|
4725
|
-
return defaultRulesMode(client);
|
|
4726
|
-
}
|
|
4727
|
-
function registerRulesCommands(program, runSafely2) {
|
|
4728
|
-
const rules = program.command("rules").description("Print or install the client workflow templates (CLAUDE.md / AGENTS.md).");
|
|
4729
|
-
rules.command("show").option("--client <client>", `One of: ${WORKFLOW_CLIENTS.join(", ")}.`, "claude").option("--mode <mode>", "agent or hooks (default: follows installed hooks).").description("Print the workflow template for a client.").action(async (opts) => {
|
|
4730
|
-
await runSafely2(async () => {
|
|
4731
|
-
const client = parseClient(opts.client);
|
|
4732
|
-
const mode = await resolveMode(client, opts.mode);
|
|
4733
|
-
if (mode === "mcp-only") {
|
|
4734
|
-
console.log("MCP-only mode installs no workflow rules.");
|
|
4735
|
-
return;
|
|
4736
|
-
}
|
|
4737
|
-
console.log(renderWorkflowTemplate(client, mode));
|
|
4738
|
-
});
|
|
3271
|
+
return mode;
|
|
3272
|
+
}
|
|
3273
|
+
return defaultRulesMode(client);
|
|
3274
|
+
}
|
|
3275
|
+
function registerRulesCommands(program, runSafely2) {
|
|
3276
|
+
const rules = program.command("rules").description("Print or install the client workflow templates (CLAUDE.md / AGENTS.md).");
|
|
3277
|
+
rules.command("show").option("--client <client>", `One of: ${WORKFLOW_CLIENTS.join(", ")}.`, "claude").option("--mode <mode>", "agent or hooks (default: follows installed hooks).").description("Print the workflow template for a client.").action(async (opts) => {
|
|
3278
|
+
await runSafely2(async () => {
|
|
3279
|
+
const client = parseClient(opts.client);
|
|
3280
|
+
const mode = await resolveMode(client, opts.mode);
|
|
3281
|
+
if (mode === "mcp-only") {
|
|
3282
|
+
console.log("MCP-only mode installs no workflow rules.");
|
|
3283
|
+
return;
|
|
3284
|
+
}
|
|
3285
|
+
console.log(renderWorkflowTemplate(client, mode));
|
|
3286
|
+
});
|
|
3287
|
+
});
|
|
3288
|
+
rules.command("install").option("--client <client>", `One of: ${WORKFLOW_CLIENTS.join(", ")}.`, "claude").option("--path <file>", "Override the target rules file.").option(
|
|
3289
|
+
"--mode <mode>",
|
|
3290
|
+
`One of: ${AUTOMATION_MODES.join(", ")} (default: follows installed hooks; mcp-only removes the block).`
|
|
3291
|
+
).description("Install/update the workflow template in the client's rules file (managed block).").action(async (opts) => {
|
|
3292
|
+
await runSafely2(async () => {
|
|
3293
|
+
const client = parseClient(opts.client);
|
|
3294
|
+
const result = await installWorkflowRules({
|
|
3295
|
+
client,
|
|
3296
|
+
mode: await resolveMode(client, opts.mode),
|
|
3297
|
+
...opts.path ? { targetPath: opts.path } : {}
|
|
3298
|
+
});
|
|
3299
|
+
const where = relative5(process.cwd(), result.path) || result.path;
|
|
3300
|
+
console.log(
|
|
3301
|
+
`${actionLabel(result.action)} ${clientDisplayName(client)} workflow in ${where}`
|
|
3302
|
+
);
|
|
3303
|
+
if (result.action !== "unchanged") {
|
|
3304
|
+
console.log(pc16.dim(" managed block between codebuddy:workflow markers; safe to re-run"));
|
|
3305
|
+
}
|
|
3306
|
+
});
|
|
3307
|
+
});
|
|
3308
|
+
}
|
|
3309
|
+
function actionLabel(action) {
|
|
3310
|
+
switch (action) {
|
|
3311
|
+
case "created":
|
|
3312
|
+
return pc16.green("created");
|
|
3313
|
+
case "added":
|
|
3314
|
+
return pc16.green("added");
|
|
3315
|
+
case "updated":
|
|
3316
|
+
return pc16.cyan("updated");
|
|
3317
|
+
case "removed":
|
|
3318
|
+
return pc16.yellow("removed");
|
|
3319
|
+
default:
|
|
3320
|
+
return pc16.dim("unchanged");
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
// src/cli/commands/suggest.ts
|
|
3325
|
+
import pc17 from "picocolors";
|
|
3326
|
+
function parsePaths4(value) {
|
|
3327
|
+
return value.split(",").map((path) => path.trim()).filter(Boolean);
|
|
3328
|
+
}
|
|
3329
|
+
function registerSuggestCommand(program, runSafely2) {
|
|
3330
|
+
program.command("suggest").option("--paths <paths>", "Comma-separated repo-relative paths to review.").option("--plan <id>", "Review the files listed in a plan.").option("--git", "Review current git changes.").option("--limit <n>", "Maximum suggestions to show.", Number).option("--json", "Print machine-readable JSON.").description("Suggest evidence-backed, read-only code-quality improvements.").action(
|
|
3331
|
+
async (opts) => {
|
|
3332
|
+
await runSafely2(async () => {
|
|
3333
|
+
const namespace = await resolveNamespaceFrom();
|
|
3334
|
+
const result = await generateSuggestions({
|
|
3335
|
+
namespace,
|
|
3336
|
+
...opts.paths ? { paths: parsePaths4(opts.paths) } : {},
|
|
3337
|
+
...opts.plan ? { planId: opts.plan } : {},
|
|
3338
|
+
...opts.git !== void 0 ? { useGit: opts.git } : {},
|
|
3339
|
+
...opts.limit !== void 0 ? { limit: opts.limit } : {}
|
|
3340
|
+
});
|
|
3341
|
+
if (opts.json) {
|
|
3342
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3343
|
+
return;
|
|
3344
|
+
}
|
|
3345
|
+
printSuggestions(result.suggestions);
|
|
3346
|
+
console.log(
|
|
3347
|
+
pc17.dim(
|
|
3348
|
+
`
|
|
3349
|
+
${result.stats.produced} suggestion(s) across ${result.stats.pathsConsidered} path(s)`
|
|
3350
|
+
)
|
|
3351
|
+
);
|
|
3352
|
+
});
|
|
3353
|
+
}
|
|
3354
|
+
);
|
|
3355
|
+
}
|
|
3356
|
+
function printSuggestions(suggestions) {
|
|
3357
|
+
if (suggestions.length === 0) {
|
|
3358
|
+
console.log(pc17.dim("no suggestions \u2014 nothing high-signal to report"));
|
|
3359
|
+
return;
|
|
3360
|
+
}
|
|
3361
|
+
for (const suggestion of suggestions) {
|
|
3362
|
+
console.log(`${severityBadge2(suggestion.severity)} ${pc17.bold(suggestion.title)}`);
|
|
3363
|
+
console.log(` ${suggestion.detail}`);
|
|
3364
|
+
for (const evidence of suggestion.evidence) {
|
|
3365
|
+
console.log(pc17.dim(` \u21B3 ${evidence.kind}: ${evidence.detail}`));
|
|
3366
|
+
}
|
|
3367
|
+
if (suggestion.commandHint) console.log(pc17.dim(` \u2192 ${suggestion.commandHint}`));
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
function severityBadge2(severity) {
|
|
3371
|
+
switch (severity) {
|
|
3372
|
+
case "high":
|
|
3373
|
+
return pc17.red("[high]");
|
|
3374
|
+
case "medium":
|
|
3375
|
+
return pc17.yellow("[med ]");
|
|
3376
|
+
case "low":
|
|
3377
|
+
return pc17.cyan("[low ]");
|
|
3378
|
+
default:
|
|
3379
|
+
return pc17.dim("[info]");
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
3382
|
+
|
|
3383
|
+
// src/cli/commands/symbols.ts
|
|
3384
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
3385
|
+
import { resolve as resolve5 } from "path";
|
|
3386
|
+
import pc18 from "picocolors";
|
|
3387
|
+
function registerSymbolsCommand(program, runSafely2) {
|
|
3388
|
+
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) => {
|
|
3389
|
+
await runSafely2(async () => {
|
|
3390
|
+
if (!supportsSymbols(path)) {
|
|
3391
|
+
console.log(pc18.dim(`no symbol extraction for ${path} (unsupported language)`));
|
|
3392
|
+
return;
|
|
3393
|
+
}
|
|
3394
|
+
const content = await readFile3(resolve5(path), "utf8");
|
|
3395
|
+
const table = extractSymbolTable(content, path);
|
|
3396
|
+
if (opts.json) {
|
|
3397
|
+
console.log(JSON.stringify({ path, symbols: table }, null, 2));
|
|
3398
|
+
return;
|
|
3399
|
+
}
|
|
3400
|
+
if (table.length === 0) {
|
|
3401
|
+
console.log(pc18.dim("no top-level symbols found"));
|
|
3402
|
+
return;
|
|
3403
|
+
}
|
|
3404
|
+
for (const symbol of table) {
|
|
3405
|
+
const badge = symbol.exported ? pc18.green("export") : pc18.dim("local ");
|
|
3406
|
+
console.log(
|
|
3407
|
+
`${badge} ${pc18.cyan(symbol.kind.padEnd(9))} ${pc18.bold(symbol.name)} ${pc18.dim(`L${symbol.startLine}-${symbol.endLine}`)}`
|
|
3408
|
+
);
|
|
3409
|
+
}
|
|
3410
|
+
console.log(pc18.dim(`
|
|
3411
|
+
${table.length} symbol(s)`));
|
|
3412
|
+
});
|
|
3413
|
+
});
|
|
3414
|
+
}
|
|
3415
|
+
|
|
3416
|
+
// src/cli/commands/telemetry.ts
|
|
3417
|
+
import pc19 from "picocolors";
|
|
3418
|
+
|
|
3419
|
+
// src/telemetry/collector.ts
|
|
3420
|
+
import { createHash, timingSafeEqual } from "crypto";
|
|
3421
|
+
import { appendFile, mkdir as mkdir2, readdir, readFile as readFile4 } from "fs/promises";
|
|
3422
|
+
import { createServer as createServer2 } from "http";
|
|
3423
|
+
import { join as join6 } from "path";
|
|
3424
|
+
import { z as z2 } from "zod";
|
|
3425
|
+
|
|
3426
|
+
// src/telemetry/admin-page.ts
|
|
3427
|
+
var ADMIN_HTML = `<!doctype html>
|
|
3428
|
+
<html lang="en">
|
|
3429
|
+
<head>
|
|
3430
|
+
<meta charset="utf-8">
|
|
3431
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
3432
|
+
<title>CodeBuddy Usage</title>
|
|
3433
|
+
<style>
|
|
3434
|
+
:root {
|
|
3435
|
+
--bg: #f4f4f2; --panel: #fcfcfb; --fg: #0b0b0b; --muted: #52514e; --faint: #8a8984; --line: #e4e3de;
|
|
3436
|
+
--bar: #2a78d6; --bar-soft: #e3edfa; --hook: #eb6834; --cli: #1baf7a;
|
|
3437
|
+
--good: #0a7d0a; --bad: #c23030; --bad-soft: #fbeaea;
|
|
3438
|
+
--sans: ui-sans-serif, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
3439
|
+
--mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
|
3440
|
+
}
|
|
3441
|
+
@media (prefers-color-scheme: dark) { :root {
|
|
3442
|
+
--bg: #111110; --panel: #1a1a19; --fg: #f0efec; --muted: #c3c2b7; --faint: #8f8e86; --line: #2e2e2b;
|
|
3443
|
+
--bar: #3987e5; --bar-soft: #1d2b3d; --hook: #d95926; --cli: #199e70;
|
|
3444
|
+
--good: #3fbf3f; --bad: #f07070; --bad-soft: #3a1a1a; color-scheme: dark } }
|
|
3445
|
+
* { box-sizing: border-box; }
|
|
3446
|
+
body { margin: 0; background: var(--bg); color: var(--fg); font: 14px/1.5 var(--sans); }
|
|
3447
|
+
a { color: inherit; }
|
|
3448
|
+
.wrap { max-width: 1240px; margin: 0 auto; padding: 0 16px 56px; display: grid; gap: 16px; }
|
|
3449
|
+
.wrap > * { min-width: 0; }
|
|
3450
|
+
.top { position: sticky; top: 0; z-index: 5; background: color-mix(in srgb, var(--bg) 88%, transparent); backdrop-filter: blur(8px); border-bottom: 1px solid var(--line); }
|
|
3451
|
+
.bar-inner { max-width: 1240px; margin: 0 auto; padding: 12px 16px; display: flex; flex-wrap: wrap; align-items: center; gap: 10px 18px; }
|
|
3452
|
+
.brand { display: flex; align-items: center; gap: 10px; font-weight: 650; font-size: 16px; letter-spacing: -.01em; }
|
|
3453
|
+
.mark { width: 22px; height: 22px; border-radius: 6px; background: var(--bar); display: grid; place-items: center; color: #fff; font: 700 12px var(--mono); }
|
|
3454
|
+
.updated { font: 12px var(--mono); color: var(--faint); }
|
|
3455
|
+
.tabs { display: flex; gap: 4px; }
|
|
3456
|
+
.tabs a { text-decoration: none; color: var(--muted); font-weight: 500; font-size: 13px; padding: 6px 10px; border-radius: 7px; }
|
|
3457
|
+
.tabs a[aria-current="page"] { background: var(--panel); color: var(--fg); box-shadow: inset 0 0 0 1px var(--line); }
|
|
3458
|
+
.tabs a:focus-visible, .seg button:focus-visible, .btn:focus-visible, input:focus-visible, th button:focus-visible, tr.link:focus-visible { outline: 2px solid var(--bar); outline-offset: 1px; }
|
|
3459
|
+
.spacer { flex: 1; }
|
|
3460
|
+
.seg { display: inline-flex; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; background: var(--panel); }
|
|
3461
|
+
.seg button { border: 0; background: none; color: var(--muted); font: 500 12px var(--sans); padding: 6px 11px; cursor: pointer; }
|
|
3462
|
+
.seg button + button { border-left: 1px solid var(--line); }
|
|
3463
|
+
.seg button[aria-pressed="true"] { background: var(--fg); color: var(--bg); }
|
|
3464
|
+
h2 { font-size: 12px; margin: 0; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); font-weight: 600; }
|
|
3465
|
+
.head { display: flex; align-items: baseline; flex-wrap: wrap; gap: 6px 12px; margin-bottom: 12px; }
|
|
3466
|
+
.head .note { font-size: 12px; color: var(--faint); }
|
|
3467
|
+
.hero { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(190px, 100%), 1fr)); gap: 12px; padding-top: 16px; }
|
|
3468
|
+
.kpi { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 14px 16px; }
|
|
3469
|
+
.kpi .label { font-size: 12px; color: var(--muted); }
|
|
3470
|
+
.kpi .value { font: 650 32px/1.15 var(--mono); font-variant-numeric: tabular-nums; margin-top: 6px; letter-spacing: -.02em; }
|
|
3471
|
+
.kpi .sub { font-size: 12px; color: var(--muted); margin-top: 4px; }
|
|
3472
|
+
.up { color: var(--good); font-weight: 600; }
|
|
3473
|
+
.down { color: var(--bad); font-weight: 600; }
|
|
3474
|
+
.panel { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 16px; min-width: 0; }
|
|
3475
|
+
.grid2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr)); gap: 16px; }
|
|
3476
|
+
.grid3 { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr)); gap: 16px; }
|
|
3477
|
+
.chart svg { display: block; width: 100%; height: 190px; overflow: visible; }
|
|
3478
|
+
svg text { fill: var(--faint); font: 11px var(--mono); }
|
|
3479
|
+
.hit { fill: transparent; cursor: crosshair; }
|
|
3480
|
+
.hit:hover { fill: color-mix(in srgb, var(--fg) 6%, transparent); }
|
|
3481
|
+
.tip { position: absolute; pointer-events: none; background: var(--fg); color: var(--bg); font: 12px var(--mono); padding: 5px 8px; border-radius: 6px; white-space: nowrap; display: none; z-index: 10; }
|
|
3482
|
+
.rows { display: grid; gap: 8px; }
|
|
3483
|
+
.row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 3px 12px; font-size: 13px; }
|
|
3484
|
+
.row .track { grid-column: 1 / -1; height: 8px; background: var(--bar-soft); border-radius: 4px; overflow: hidden; }
|
|
3485
|
+
.row .fill { height: 8px; background: var(--bar); border-radius: 4px; }
|
|
3486
|
+
.row .n { font-family: var(--mono); font-variant-numeric: tabular-nums; color: var(--muted); }
|
|
3487
|
+
.funnel .row { font-size: 14px; }
|
|
3488
|
+
.funnel .track { height: 22px; border-radius: 6px; }
|
|
3489
|
+
.funnel .fill { height: 22px; border-radius: 6px; }
|
|
3490
|
+
.scroll { overflow-x: auto; }
|
|
3491
|
+
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
3492
|
+
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; }
|
|
3493
|
+
th button { all: unset; cursor: pointer; }
|
|
3494
|
+
th button::after { content: attr(data-dir); margin-left: 4px; color: var(--faint); }
|
|
3495
|
+
td { padding: 7px 8px; border-bottom: 1px solid var(--line); white-space: nowrap; }
|
|
3496
|
+
tr:last-child td { border-bottom: 0; }
|
|
3497
|
+
td.n, th.n { text-align: right; font-family: var(--mono); font-variant-numeric: tabular-nums; }
|
|
3498
|
+
tr.link { cursor: pointer; }
|
|
3499
|
+
tr.link:hover td { background: var(--bar-soft); }
|
|
3500
|
+
.inline { display: inline-block; height: 6px; background: var(--bar); border-radius: 3px; vertical-align: middle; margin-left: 8px; }
|
|
3501
|
+
.heat td { text-align: center; font: 12px var(--mono); padding: 6px 4px; border: 2px solid var(--panel); border-radius: 6px; min-width: 44px; }
|
|
3502
|
+
.heat td.lab { text-align: left; color: var(--muted); font-family: var(--sans); white-space: nowrap; }
|
|
3503
|
+
.heat td.empty { background: transparent; color: var(--faint); }
|
|
3504
|
+
.bad { color: var(--bad); }
|
|
3505
|
+
.muted { color: var(--muted); }
|
|
3506
|
+
.mono { font-family: var(--mono); }
|
|
3507
|
+
.empty-state { padding: 36px 16px; text-align: center; color: var(--muted); }
|
|
3508
|
+
.toolbar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-bottom: 12px; }
|
|
3509
|
+
input[type="search"] { font: 13px var(--sans); padding: 7px 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--bg); color: var(--fg); min-width: 0; flex: 1 1 200px; }
|
|
3510
|
+
.btn { border: 1px solid var(--line); background: var(--panel); color: var(--fg); font: 500 12px var(--sans); padding: 7px 12px; border-radius: 8px; cursor: pointer; text-decoration: none; }
|
|
3511
|
+
.chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
3512
|
+
.chip { display: inline-flex; align-items: center; gap: 6px; font: 600 11px var(--mono); padding: 2px 8px; border-radius: 999px; background: var(--bar-soft); color: var(--fg); }
|
|
3513
|
+
.sw { width: 10px; height: 10px; border-radius: 3px; display: inline-block; }
|
|
3514
|
+
.sessions { display: grid; gap: 6px; max-height: 520px; overflow: auto; }
|
|
3515
|
+
.session { all: unset; display: grid; gap: 2px; padding: 8px 10px; border: 1px solid var(--line); border-radius: 8px; cursor: pointer; font-size: 12px; }
|
|
3516
|
+
.session[aria-pressed="true"] { border-color: var(--bar); box-shadow: inset 3px 0 0 var(--bar); }
|
|
3517
|
+
.session:focus-visible { outline: 2px solid var(--bar); }
|
|
3518
|
+
.split { display: grid; grid-template-columns: minmax(220px, 300px) minmax(0, 1fr); gap: 16px; }
|
|
3519
|
+
@media (max-width: 820px) { .split { grid-template-columns: 1fr; } .wf .step { grid-template-columns: 120px 52px minmax(0, 1fr) 56px; } }
|
|
3520
|
+
.wf { display: grid; gap: 3px; font-size: 12px; }
|
|
3521
|
+
.wf .step { display: grid; grid-template-columns: 170px 64px minmax(0, 1fr) 70px; gap: 10px; align-items: center; }
|
|
3522
|
+
.wf .lane { position: relative; height: 14px; background: var(--bg); border-radius: 3px; }
|
|
3523
|
+
.wf .seg-bar { position: absolute; top: 2px; height: 10px; min-width: 3px; border-radius: 3px; }
|
|
3524
|
+
.wf .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--mono); }
|
|
3525
|
+
.wf .at { font-family: var(--mono); color: var(--faint); text-align: right; }
|
|
3526
|
+
.wf .t { text-align: right; font-family: var(--mono); color: var(--muted); }
|
|
3527
|
+
.legend { display: flex; gap: 14px; font-size: 12px; color: var(--muted); margin-bottom: 10px; flex-wrap: wrap; }
|
|
3528
|
+
.legend span { display: inline-flex; align-items: center; gap: 6px; }
|
|
3529
|
+
footer { font-size: 12px; color: var(--faint); }
|
|
3530
|
+
[hidden] { display: none !important; }
|
|
3531
|
+
</style>
|
|
3532
|
+
</head>
|
|
3533
|
+
<body>
|
|
3534
|
+
<div class="top"><div class="bar-inner">
|
|
3535
|
+
<div class="brand"><span class="mark">cb</span>CodeBuddy usage</div>
|
|
3536
|
+
<nav class="tabs" aria-label="Views">
|
|
3537
|
+
<a href="#overview" data-view="overview">Overview</a>
|
|
3538
|
+
<a href="#users" data-view="users">Users</a>
|
|
3539
|
+
<a href="#events" data-view="events">Events</a>
|
|
3540
|
+
</nav>
|
|
3541
|
+
<span class="spacer"></span>
|
|
3542
|
+
<span class="updated" id="updated">loading\u2026</span>
|
|
3543
|
+
<div class="seg" role="group" aria-label="Time range" id="range">
|
|
3544
|
+
<button data-days="7">7d</button><button data-days="30" aria-pressed="true">30d</button><button data-days="90">90d</button><button data-days="365">1y</button>
|
|
3545
|
+
</div>
|
|
3546
|
+
</div></div>
|
|
3547
|
+
|
|
3548
|
+
<div class="wrap">
|
|
3549
|
+
<section id="view-overview">
|
|
3550
|
+
<div class="hero" id="hero" aria-label="Headline numbers"></div>
|
|
3551
|
+
<div class="grid2" style="margin-top:16px">
|
|
3552
|
+
<section class="panel"><div class="head"><h2>Total installs</h2><span class="note">all-time, by first-seen day</span></div><div class="chart" id="cumulative"></div></section>
|
|
3553
|
+
<section class="panel"><div class="head"><h2>Daily active installs</h2><span class="note">installs that sent any event</span></div><div class="chart" id="active"></div></section>
|
|
3554
|
+
</div>
|
|
3555
|
+
<div class="grid2" style="margin-top:16px">
|
|
3556
|
+
<section class="panel"><div class="head"><h2>Activation</h2><span class="note">installs active in range reaching each step</span></div><div class="rows funnel" id="funnel"></div></section>
|
|
3557
|
+
<section class="panel"><div class="head"><h2>Retention by weekly cohort</h2><span class="note">share of each week's new installs active in later weeks</span></div><div class="scroll" id="cohorts"></div></section>
|
|
3558
|
+
</div>
|
|
3559
|
+
<section class="panel" style="margin-top:16px"><div class="head"><h2>Feature usage</h2><span class="note">MCP tools, hook steps (recall, capture\u2026) and CLI commands</span></div><div class="scroll" id="features"></div></section>
|
|
3560
|
+
<div class="grid3" style="margin-top:16px">
|
|
3561
|
+
<section class="panel"><div class="head"><h2>Agent clients</h2></div><div class="rows" id="clients"></div></section>
|
|
3562
|
+
<section class="panel"><div class="head"><h2>Versions</h2></div><div class="rows" id="versions"></div></section>
|
|
3563
|
+
<section class="panel"><div class="head"><h2>Platforms</h2></div><div class="rows" id="platforms"></div></section>
|
|
3564
|
+
</div>
|
|
3565
|
+
<section class="panel" style="margin-top:16px"><div class="head"><h2>npm downloads per day</h2><span class="note" id="npmNote"></span></div><div class="chart" id="npm"></div></section>
|
|
3566
|
+
</section>
|
|
3567
|
+
|
|
3568
|
+
<section id="view-users" class="panel" style="margin-top:16px" hidden>
|
|
3569
|
+
<div class="toolbar">
|
|
3570
|
+
<h2>Users</h2><span class="muted" id="userCount" style="font-size:12px"></span><span class="spacer"></span>
|
|
3571
|
+
<input type="search" id="userSearch" placeholder="Filter by id, client, version, platform\u2026" aria-label="Filter users">
|
|
3572
|
+
<button class="btn" id="csv">Download CSV</button>
|
|
3573
|
+
</div>
|
|
3574
|
+
<div class="scroll" id="users"></div>
|
|
3575
|
+
</section>
|
|
3576
|
+
|
|
3577
|
+
<section id="view-install" style="margin-top:16px" hidden></section>
|
|
3578
|
+
|
|
3579
|
+
<section id="view-events" class="panel" style="margin-top:16px" hidden>
|
|
3580
|
+
<div class="toolbar"><h2>Latest events</h2><span class="spacer"></span><div class="chips" id="kindFilter"></div></div>
|
|
3581
|
+
<div class="scroll" id="recent"></div>
|
|
3582
|
+
</section>
|
|
3583
|
+
|
|
3584
|
+
<footer>Anonymous telemetry from installs that did not opt out: random install ids, tool and command names, timings, versions. No prompts, code, paths or repository names are collected. Shares count each install once, from its latest batch.</footer>
|
|
3585
|
+
</div>
|
|
3586
|
+
<div class="tip" id="tip" role="status"></div>
|
|
3587
|
+
<script>
|
|
3588
|
+
(function () {
|
|
3589
|
+
var days = 30;
|
|
3590
|
+
var summary = null, npm = null, install = null;
|
|
3591
|
+
var userSort = { key: "lastSeen", dir: -1 };
|
|
3592
|
+
var kindShown = "all";
|
|
3593
|
+
var selectedSession = null;
|
|
3594
|
+
var path = location.pathname;
|
|
3595
|
+
var base = path.charAt(path.length - 1) === "/" ? path : path + "/";
|
|
3596
|
+
var tip = document.getElementById("tip");
|
|
3597
|
+
var NL = String.fromCharCode(10);
|
|
3598
|
+
var KIND_COLOR = { tool: "var(--bar)", cli: "var(--cli)" };
|
|
3599
|
+
function kindColor(kind) { return KIND_COLOR[kind] || "var(--hook)"; }
|
|
3600
|
+
function kindGroup(kind) { return kind === "tool" ? "MCP tool" : kind === "cli" ? "CLI command" : "hook step"; }
|
|
3601
|
+
|
|
3602
|
+
function $(id) { return document.getElementById(id); }
|
|
3603
|
+
function esc(value) {
|
|
3604
|
+
return String(value == null ? "" : value).replace(/[&<>"']/g, function (c) {
|
|
3605
|
+
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
|
|
3606
|
+
});
|
|
3607
|
+
}
|
|
3608
|
+
function num(n) { return Math.round(n).toLocaleString(); }
|
|
3609
|
+
function compact(n) { return n >= 1e6 ? (n / 1e6).toFixed(1) + "M" : n >= 1e4 ? (n / 1e3).toFixed(1) + "k" : num(n); }
|
|
3610
|
+
function pct(n) { return (n * 100).toFixed(n > 0 && n < 0.1 ? 1 : 0) + "%"; }
|
|
3611
|
+
function plural(n, word) { return num(n) + " " + word + (n === 1 ? "" : "s"); }
|
|
3612
|
+
function offset(n) { var s = Math.round(n / 1000); return "+" + (s >= 60 ? Math.floor(s / 60) + "m " + (s % 60) + "s" : s + "s"); }
|
|
3613
|
+
function ms(n) { return n >= 60000 ? (n / 60000).toFixed(1) + " min" : n >= 1000 ? (n / 1000).toFixed(1) + " s" : Math.round(n) + " ms"; }
|
|
3614
|
+
function date(iso) { return new Date(iso).toLocaleDateString(undefined, { month: "short", day: "numeric" }); }
|
|
3615
|
+
function ago(iso) {
|
|
3616
|
+
var s = Math.max(0, (Date.now() - Date.parse(iso)) / 1000);
|
|
3617
|
+
return s < 60 ? "just now" : s < 3600 ? Math.floor(s / 60) + "m ago" : s < 86400 ? Math.floor(s / 3600) + "h ago" : Math.floor(s / 86400) + "d ago";
|
|
3618
|
+
}
|
|
3619
|
+
function get(url) { return fetch(base + url).then(function (r) { return r.ok ? r.json() : null; }); }
|
|
3620
|
+
|
|
3621
|
+
function kpi(label, value, sub) {
|
|
3622
|
+
return '<div class="kpi"><div class="label">' + esc(label) + '</div><div class="value">' + esc(value) + '</div><div class="sub">' + (sub || "") + "</div></div>";
|
|
3623
|
+
}
|
|
3624
|
+
function delta(now, before) {
|
|
3625
|
+
if (!before) return now ? '<span class="up">\u25B2 new</span> vs previous 7 days' : "no activity yet";
|
|
3626
|
+
var change = (now - before) / before;
|
|
3627
|
+
return '<span class="' + (change >= 0 ? "up" : "down") + '">' + (change >= 0 ? "\u25B2 " : "\u25BC ") + pct(Math.abs(change)) + "</span> vs previous 7 days";
|
|
3628
|
+
}
|
|
3629
|
+
|
|
3630
|
+
/** Bars or a filled line over days, one measure; every point has a hover target. */
|
|
3631
|
+
/** Draw into a container at its own pixel width, so labels never stretch. */
|
|
3632
|
+
function draw(id, points, label, line) {
|
|
3633
|
+
var el = $(id);
|
|
3634
|
+
if (el) el.innerHTML = chart(points, label, line, el.clientWidth);
|
|
3635
|
+
}
|
|
3636
|
+
function chart(points, label, line, width) {
|
|
3637
|
+
if (!points.length) return '<div class="empty-state">No data yet.</div>';
|
|
3638
|
+
var w = Math.max(280, width || 640), h = 190, top = 10, bottom = 22, left = 40, plot = h - top - bottom;
|
|
3639
|
+
var max = Math.max.apply(null, points.map(function (p) { return p.value; }).concat([1]));
|
|
3640
|
+
var step = (w - left) / points.length;
|
|
3641
|
+
var y = function (v) { return top + plot * (1 - v / max); };
|
|
3642
|
+
var out = '<svg viewBox="0 0 ' + w + " " + h + '" preserveAspectRatio="none" role="img" aria-label="' + esc(label) + '">';
|
|
3643
|
+
[0, 0.5, 1].forEach(function (f) {
|
|
3644
|
+
out += '<line x1="' + left + '" x2="' + w + '" y1="' + y(max * f) + '" y2="' + y(max * f) + '" stroke="var(--line)"/>';
|
|
3645
|
+
out += '<text x="' + (left - 6) + '" y="' + (y(max * f) + 4) + '" text-anchor="end">' + esc(compact(max * f)) + "</text>";
|
|
3646
|
+
});
|
|
3647
|
+
if (line) {
|
|
3648
|
+
var pts = points.map(function (p, i) { return (left + i * step + step / 2).toFixed(1) + "," + y(p.value).toFixed(1); });
|
|
3649
|
+
out += '<path d="M' + (left + step / 2) + "," + (top + plot) + " L" + pts.join(" L") + " L" + (left + (points.length - 0.5) * step) + "," + (top + plot) + ' Z" fill="var(--bar-soft)"/>';
|
|
3650
|
+
out += '<polyline points="' + pts.join(" ") + '" fill="none" stroke="var(--bar)" stroke-width="2" vector-effect="non-scaling-stroke" stroke-linejoin="round"/>';
|
|
3651
|
+
}
|
|
3652
|
+
points.forEach(function (p, i) {
|
|
3653
|
+
var x = left + i * step;
|
|
3654
|
+
if (!line) {
|
|
3655
|
+
var bh = plot * (p.value / max), bw = Math.max(1, step - 2);
|
|
3656
|
+
out += '<rect x="' + (x + 1) + '" y="' + (top + plot - bh) + '" width="' + bw + '" height="' + bh + '" rx="' + Math.min(2, bw / 2) + '" fill="var(--bar)"/>';
|
|
3657
|
+
}
|
|
3658
|
+
out += '<rect class="hit" x="' + x + '" y="' + top + '" width="' + step + '" height="' + plot + '" data-tip="' + esc(p.date + " \xB7 " + num(p.value) + " " + label) + '"/>';
|
|
3659
|
+
});
|
|
3660
|
+
out += '<text x="' + left + '" y="' + (h - 5) + '">' + esc(points[0].date) + '</text><text x="' + w + '" y="' + (h - 5) + '" text-anchor="end">' + esc(points[points.length - 1].date) + "</text>";
|
|
3661
|
+
return out + "</svg>";
|
|
3662
|
+
}
|
|
3663
|
+
|
|
3664
|
+
function rows(items, total) {
|
|
3665
|
+
if (!items.length) return '<div class="muted">No data yet.</div>';
|
|
3666
|
+
return items.slice(0, 8).map(function (item) {
|
|
3667
|
+
var share = total ? item.installs / total : 0;
|
|
3668
|
+
return '<div class="row"><span>' + esc(item.label) + '</span><span class="n">' + num(item.installs) + " \xB7 " + pct(share) + '</span><div class="track"><div class="fill" style="width:' + (share * 100).toFixed(1) + '%"></div></div></div>';
|
|
3669
|
+
}).join("");
|
|
3670
|
+
}
|
|
3671
|
+
|
|
3672
|
+
function renderOverview() {
|
|
3673
|
+
var t = summary.totals;
|
|
3674
|
+
var npm30 = npmTotal(30);
|
|
3675
|
+
$("hero").innerHTML =
|
|
3676
|
+
kpi("Weekly active installs", num(t.wau), delta(t.wau, t.wauPrevious)) +
|
|
3677
|
+
kpi("Total installs", num(t.allTimeInstalls), "+" + num(t.newInRange) + " in the last " + summary.days + " days \xB7 " + num(t.newToday) + " today") +
|
|
3678
|
+
kpi("Week-1 retention", t.week1Retention == null ? "\u2013" : pct(t.week1Retention), t.week1Retention == null ? "needs a cohort older than 7 days" : "new installs active again the next week") +
|
|
3679
|
+
kpi("Agent sessions", compact(t.sessions), num(t.dau) + " installs active today \xB7 " + num(t.mau) + " this month") +
|
|
3680
|
+
kpi("npm downloads \xB7 30 days", npm30 == null ? "\u2013" : num(npm30), npm && npm.error ? esc(npm.error) : num(npmTotal(365) || 0) + " in the last year");
|
|
3681
|
+
draw("cumulative", summary.daily.map(function (d) { return { date: d.date, value: d.totalInstalls }; }), "total installs", true);
|
|
3682
|
+
draw("active", summary.daily.map(function (d) { return { date: d.date, value: d.installs }; }), "active installs", false);
|
|
3683
|
+
|
|
3684
|
+
var active = summary.funnel[0] ? summary.funnel[0].installs : 0;
|
|
3685
|
+
$("funnel").innerHTML = active ? summary.funnel.map(function (step) {
|
|
3686
|
+
var share = active ? step.installs / active : 0;
|
|
3687
|
+
return '<div class="row"><span>' + esc(step.label) + '</span><span class="n">' + num(step.installs) + " \xB7 " + pct(share) + '</span><div class="track"><div class="fill" style="width:' + (share * 100).toFixed(1) + '%"></div></div></div>';
|
|
3688
|
+
}).join("") : '<div class="empty-state">No active installs in this range.</div>';
|
|
3689
|
+
|
|
3690
|
+
var cohorts = summary.cohorts.filter(function (c) { return c.size > 0; });
|
|
3691
|
+
var weeks = summary.cohorts.length;
|
|
3692
|
+
$("cohorts").innerHTML = cohorts.length
|
|
3693
|
+
? '<table class="heat"><thead><tr><th>Cohort</th><th class="n">Installs</th>' + Array.from({ length: weeks }, function (_, i) { return '<th class="n">W' + i + "</th>"; }).join("") + "</tr></thead><tbody>" +
|
|
3694
|
+
cohorts.map(function (c) {
|
|
3695
|
+
var cells = "";
|
|
3696
|
+
for (var i = 0; i < weeks; i += 1) {
|
|
3697
|
+
var v = c.weeks[i];
|
|
3698
|
+
if (v == null) { cells += '<td class="empty">\xB7</td>'; continue; }
|
|
3699
|
+
var strength = Math.round(12 + v * 88);
|
|
3700
|
+
cells += '<td style="background:color-mix(in oklab, var(--bar) ' + strength + '%, var(--panel));color:' + (v > 0.5 ? "#fff" : "var(--fg)") + '" title="' + esc(c.start + " cohort, week " + i + ": " + pct(v)) + '">' + pct(v) + "</td>";
|
|
3701
|
+
}
|
|
3702
|
+
return '<tr><td class="lab">' + esc(date(c.start)) + '</td><td class="n">' + num(c.size) + "</td>" + cells + "</tr>";
|
|
3703
|
+
}).join("") + "</tbody></table>"
|
|
3704
|
+
: '<div class="empty-state">Cohorts appear once new installs report in.</div>';
|
|
3705
|
+
|
|
3706
|
+
var maxCalls = Math.max.apply(null, summary.features.map(function (f) { return f.calls; }).concat([1]));
|
|
3707
|
+
$("features").innerHTML = summary.features.length
|
|
3708
|
+
? '<table><thead><tr><th>Feature</th><th class="n">Calls</th><th class="n">Installs</th><th class="n">Median</th><th class="n">p95</th><th class="n">Errors</th></tr></thead><tbody>' +
|
|
3709
|
+
summary.features.map(function (f) {
|
|
3710
|
+
return '<tr><td class="mono">' + esc(f.label) + '<span class="inline" style="width:' + Math.max(2, (f.calls / maxCalls) * 120).toFixed(0) + 'px"></span></td><td class="n">' + num(f.calls) + '</td><td class="n">' + num(f.installs) +
|
|
3711
|
+
'</td><td class="n">' + ms(f.p50ms) + '</td><td class="n">' + ms(f.p95ms) + '</td><td class="n' + (f.errorRate > 0.05 ? " bad" : "") + '">' + pct(f.errorRate) + "</td></tr>";
|
|
3712
|
+
}).join("") + "</tbody></table>"
|
|
3713
|
+
: '<div class="empty-state">No events yet. Installs send a batch at most every 10 minutes.</div>';
|
|
3714
|
+
var installs = summary.versions.reduce(function (sum, v) { return sum + v.installs; }, 0);
|
|
3715
|
+
$("clients").innerHTML = rows(summary.clients, installs);
|
|
3716
|
+
$("versions").innerHTML = rows(summary.versions, installs);
|
|
3717
|
+
$("platforms").innerHTML = rows(summary.platforms, installs);
|
|
3718
|
+
renderNpm();
|
|
3719
|
+
}
|
|
3720
|
+
|
|
3721
|
+
function npmTotal(n) {
|
|
3722
|
+
if (!npm || !npm.downloads) return null;
|
|
3723
|
+
return npm.downloads.slice(-n).reduce(function (sum, d) { return sum + d.downloads; }, 0);
|
|
3724
|
+
}
|
|
3725
|
+
function renderNpm() {
|
|
3726
|
+
if (!npm) return;
|
|
3727
|
+
if (npm.error) { $("npm").innerHTML = '<div class="empty-state">' + esc(npm.error) + "</div>"; return; }
|
|
3728
|
+
$("npmNote").textContent = npm.package + " \xB7 public npm registry";
|
|
3729
|
+
draw("npm", npm.downloads.slice(-Math.min(days, 365)).map(function (d) { return { date: d.day, value: d.downloads }; }), "downloads", false);
|
|
3730
|
+
}
|
|
3731
|
+
|
|
3732
|
+
var USER_COLS = [
|
|
3733
|
+
["install", "Install"], ["firstSeen", "First seen"], ["lastSeen", "Last active"], ["activeDays", "Active days", 1],
|
|
3734
|
+
["sessions", "Sessions", 1], ["events", "Events", 1], ["errors", "Errors", 1], ["client", "Client"], ["version", "Version"], ["platform", "Platform"], ["topFeature", "Top feature"]
|
|
3735
|
+
];
|
|
3736
|
+
function filteredUsers() {
|
|
3737
|
+
var q = $("userSearch").value.trim().toLowerCase();
|
|
3738
|
+
return summary.users.filter(function (u) {
|
|
3739
|
+
return !q || [u.install, u.client, u.version, u.platform, u.topFeature].join(" ").toLowerCase().indexOf(q) !== -1;
|
|
3740
|
+
}).sort(function (a, b) {
|
|
3741
|
+
var x = a[userSort.key], y = b[userSort.key];
|
|
3742
|
+
return (x < y ? -1 : x > y ? 1 : 0) * userSort.dir;
|
|
3743
|
+
});
|
|
3744
|
+
}
|
|
3745
|
+
function renderUsers() {
|
|
3746
|
+
var list = filteredUsers();
|
|
3747
|
+
$("userCount").textContent = num(summary.users.length) + " installs active in the last " + summary.days + " days";
|
|
3748
|
+
$("users").innerHTML = list.length
|
|
3749
|
+
? "<table><thead><tr>" + USER_COLS.map(function (c) {
|
|
3750
|
+
return "<th" + (c[2] ? ' class="n"' : "") + '><button data-sort="' + c[0] + '" data-dir="' + (userSort.key === c[0] ? (userSort.dir > 0 ? "\u2191" : "\u2193") : "") + '">' + c[1] + "</button></th>";
|
|
3751
|
+
}).join("") + "</tr></thead><tbody>" +
|
|
3752
|
+
list.map(function (u) {
|
|
3753
|
+
return '<tr class="link" tabindex="0" data-install="' + esc(u.install) + '"><td class="mono">' + esc(u.install) + "</td><td>" + esc(date(u.firstSeen)) + "</td><td>" + esc(ago(u.lastSeen)) +
|
|
3754
|
+
'</td><td class="n">' + num(u.activeDays) + '</td><td class="n">' + num(u.sessions) + '</td><td class="n">' + num(u.events) + '</td><td class="n' + (u.errors ? " bad" : "") + '">' + num(u.errors) +
|
|
3755
|
+
"</td><td>" + esc(u.client) + '</td><td class="mono">' + esc(u.version) + "</td><td>" + esc(u.platform) + '</td><td class="mono">' + esc(u.topFeature) + "</td></tr>";
|
|
3756
|
+
}).join("") + "</tbody></table>"
|
|
3757
|
+
: '<div class="empty-state">' + (summary.users.length ? "No installs match this filter." : "No users yet.") + "</div>";
|
|
3758
|
+
}
|
|
3759
|
+
|
|
3760
|
+
function renderInstall() {
|
|
3761
|
+
var el = $("view-install");
|
|
3762
|
+
if (!install) { el.innerHTML = '<div class="panel empty-state">Loading\u2026</div>'; return; }
|
|
3763
|
+
if (install.missing) { el.innerHTML = '<div class="panel empty-state">No events for this install in the last 90 days. <a href="#users">Back to users</a></div>'; return; }
|
|
3764
|
+
var errors = install.events.filter(function (e) { return !e.ok; }).length;
|
|
3765
|
+
var perDay = {};
|
|
3766
|
+
install.perDay.forEach(function (d) { perDay[d.date] = d.events; });
|
|
3767
|
+
var points = [];
|
|
3768
|
+
for (var i = 89; i >= 0; i -= 1) {
|
|
3769
|
+
var d = new Date(Date.now() - i * 86400000).toISOString().slice(0, 10);
|
|
3770
|
+
points.push({ date: d, value: perDay[d] || 0 });
|
|
3771
|
+
}
|
|
3772
|
+
if (!selectedSession || !install.sessions.some(function (s) { return s.id === selectedSession; })) selectedSession = install.sessions[0] ? install.sessions[0].id : null;
|
|
3773
|
+
el.innerHTML =
|
|
3774
|
+
'<div class="toolbar"><a class="btn" href="#users">\u2190 Users</a><h2 style="font-size:14px;color:var(--fg);text-transform:none;letter-spacing:0">Install <span class="mono">' + esc(install.install) + "</span></h2>" +
|
|
3775
|
+
'<span class="chips"><span class="chip">' + esc(install.client) + '</span><span class="chip">v' + esc(install.version) + '</span><span class="chip">' + esc(install.platform) + '</span><span class="chip">node ' + esc(install.node) + "</span></span></div>" +
|
|
3776
|
+
'<div class="hero" style="padding-top:0">' + kpi("Events \xB7 90 days", num(install.events.length), errors ? '<span class="bad">' + plural(errors, "error") + "</span>" : "no errors") +
|
|
3777
|
+
kpi("Sessions", num(install.sessions.length), "agent sessions with CodeBuddy") + kpi("Active days", num(install.perDay.length), "in the last 90 days") + kpi("Projects", num(install.projects), "hashed, never named") + "</div>" +
|
|
3778
|
+
'<section class="panel" style="margin-top:16px"><div class="head"><h2>Activity</h2><span class="note">events per day, last 90 days</span></div><div class="chart" id="installActivity"></div></section>' +
|
|
3779
|
+
'<section class="panel" style="margin-top:16px"><div class="head"><h2>Session traces</h2><span class="note">each step ends at its recorded time; bar length is its duration</span></div>' +
|
|
3780
|
+
'<div class="split"><div class="sessions" id="sessionList">' + install.sessions.map(function (s) {
|
|
3781
|
+
return '<button class="session" data-session="' + esc(s.id) + '" aria-pressed="' + (s.id === selectedSession) + '"><strong class="mono">' + esc(s.id === "none" ? "no session id" : s.id.slice(0, 8)) + "</strong>" +
|
|
3782
|
+
'<span class="muted">' + esc(new Date(s.start).toLocaleString()) + " \xB7 " + num(s.events) + " steps" + (s.errors ? ' \xB7 <span class="bad">' + plural(s.errors, "error") + "</span>" : "") + "</span></button>";
|
|
3783
|
+
}).join("") + '</div><div id="waterfall"></div></div></section>';
|
|
3784
|
+
draw("installActivity", points, "events", false);
|
|
3785
|
+
renderWaterfall();
|
|
3786
|
+
}
|
|
3787
|
+
|
|
3788
|
+
function renderWaterfall() {
|
|
3789
|
+
var target = $("waterfall");
|
|
3790
|
+
if (!target || !install) return;
|
|
3791
|
+
var steps = install.events.filter(function (e) { return e.session === selectedSession; })
|
|
3792
|
+
.sort(function (a, b) { return a.ts < b.ts ? -1 : a.ts > b.ts ? 1 : 0; }).slice(-200);
|
|
3793
|
+
if (!steps.length) { target.innerHTML = '<div class="empty-state">Pick a session.</div>'; return; }
|
|
3794
|
+
var start = Math.min.apply(null, steps.map(function (e) { return Date.parse(e.ts) - e.ms; }));
|
|
3795
|
+
var end = Math.max.apply(null, steps.map(function (e) { return Date.parse(e.ts); }));
|
|
3796
|
+
var span = Math.max(1, end - start);
|
|
3797
|
+
target.innerHTML = '<div class="legend"><span><i class="sw" style="background:var(--bar)"></i>MCP tool</span><span><i class="sw" style="background:var(--hook)"></i>hook step</span><span><i class="sw" style="background:var(--cli)"></i>CLI command</span><span class="spacer"></span><span>' + esc(ms(span)) + " total</span></div>" +
|
|
3798
|
+
'<div class="wf">' + steps.map(function (e) {
|
|
3799
|
+
var s = Date.parse(e.ts) - e.ms;
|
|
3800
|
+
var left = ((s - start) / span) * 100, width = Math.max(0.8, (e.ms / span) * 100);
|
|
3801
|
+
return '<div class="step"><span class="name' + (e.ok ? "" : " bad") + '" title="' + esc(kindGroup(e.kind) + ": " + e.name) + '">' + (e.ok ? "" : "\u2715 ") + esc(e.feature) + '</span><span class="at">' + esc(offset(s - start)) + '</span><span class="lane"><span class="seg-bar" style="left:' + left.toFixed(2) + "%;width:" + width.toFixed(2) + "%;background:" + kindColor(e.kind) + '" data-tip="' + esc(e.feature + " \xB7 " + ms(e.ms) + (e.ok ? "" : " \xB7 error") + (e.tokensOut ? " \xB7 " + num(e.tokensOut) + " tokens out" : "")) + '"></span></span><span class="t">' + esc(ms(e.ms)) + "</span></div>";
|
|
3802
|
+
}).join("") + "</div>";
|
|
3803
|
+
}
|
|
3804
|
+
|
|
3805
|
+
function renderEvents() {
|
|
3806
|
+
var kinds = ["all"];
|
|
3807
|
+
summary.recent.forEach(function (e) { if (kinds.indexOf(e.kind) === -1) kinds.push(e.kind); });
|
|
3808
|
+
$("kindFilter").innerHTML = '<div class="seg" role="group" aria-label="Event kind">' + kinds.map(function (k) {
|
|
3809
|
+
return '<button data-kind="' + esc(k) + '" aria-pressed="' + (k === kindShown) + '">' + esc(k) + "</button>";
|
|
3810
|
+
}).join("") + "</div>";
|
|
3811
|
+
var list = summary.recent.filter(function (e) { return kindShown === "all" || e.kind === kindShown; });
|
|
3812
|
+
$("recent").innerHTML = list.length
|
|
3813
|
+
? '<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>' +
|
|
3814
|
+
list.map(function (e) {
|
|
3815
|
+
return '<tr class="link" tabindex="0" data-install="' + esc(e.install) + '"><td>' + esc(new Date(e.receivedAt).toLocaleString()) + '</td><td class="mono">' + esc(e.install) + '</td><td><span class="sw" style="background:' + kindColor(e.kind) + '"></span> ' + esc(e.kind) +
|
|
3816
|
+
'</td><td class="mono">' + esc(e.name) + "</td><td>" + esc(e.client) + '</td><td class="mono">' + esc(e.version) + '</td><td class="n">' + ms(e.ms) + '</td><td class="' + (e.ok ? "" : "bad") + '">' + (e.ok ? "ok" : "\u2715 error") + "</td></tr>";
|
|
3817
|
+
}).join("") + "</tbody></table>"
|
|
3818
|
+
: '<div class="empty-state">No events yet.</div>';
|
|
3819
|
+
}
|
|
3820
|
+
|
|
3821
|
+
function route() {
|
|
3822
|
+
var hash = location.hash.replace("#", "") || "overview";
|
|
3823
|
+
var view = hash.indexOf("install/") === 0 ? "install" : ["overview", "users", "events"].indexOf(hash) !== -1 ? hash : "overview";
|
|
3824
|
+
["overview", "users", "install", "events"].forEach(function (v) { $("view-" + v).hidden = v !== view; });
|
|
3825
|
+
document.querySelectorAll(".tabs a").forEach(function (a) {
|
|
3826
|
+
var current = a.dataset.view === view || (view === "install" && a.dataset.view === "users");
|
|
3827
|
+
if (current) a.setAttribute("aria-current", "page"); else a.removeAttribute("aria-current");
|
|
3828
|
+
});
|
|
3829
|
+
$("range").hidden = view === "install";
|
|
3830
|
+
if (view === "install") {
|
|
3831
|
+
var id = hash.slice(8);
|
|
3832
|
+
if (!install || install.install !== id) {
|
|
3833
|
+
install = null;
|
|
3834
|
+
renderInstall();
|
|
3835
|
+
get("api/install?id=" + encodeURIComponent(id)).then(function (data) { install = data || { install: id, missing: true }; renderInstall(); });
|
|
3836
|
+
} else renderInstall();
|
|
3837
|
+
} else render();
|
|
3838
|
+
}
|
|
3839
|
+
|
|
3840
|
+
function render() {
|
|
3841
|
+
if (!summary) return;
|
|
3842
|
+
$("updated").textContent = "updated " + new Date(summary.generatedAt).toLocaleTimeString();
|
|
3843
|
+
var hash = location.hash.replace("#", "") || "overview";
|
|
3844
|
+
if (hash === "users") renderUsers();
|
|
3845
|
+
else if (hash === "events") renderEvents();
|
|
3846
|
+
else if (hash.indexOf("install/") !== 0) renderOverview();
|
|
3847
|
+
}
|
|
3848
|
+
|
|
3849
|
+
function load() { get("api/summary?days=" + days).then(function (s) { if (s) { summary = s; render(); } }); }
|
|
3850
|
+
get("api/npm").then(function (n) { npm = n; if (summary) render(); });
|
|
3851
|
+
|
|
3852
|
+
$("range").addEventListener("click", function (event) {
|
|
3853
|
+
var button = event.target.closest("button");
|
|
3854
|
+
if (!button) return;
|
|
3855
|
+
days = Number(button.dataset.days);
|
|
3856
|
+
document.querySelectorAll("#range button").forEach(function (b) { b.setAttribute("aria-pressed", String(b === button)); });
|
|
3857
|
+
load();
|
|
3858
|
+
});
|
|
3859
|
+
$("userSearch").addEventListener("input", function () { if (summary) renderUsers(); });
|
|
3860
|
+
document.addEventListener("click", function (event) {
|
|
3861
|
+
var sort = event.target.closest("[data-sort]");
|
|
3862
|
+
if (sort) {
|
|
3863
|
+
var key = sort.dataset.sort;
|
|
3864
|
+
userSort = { key: key, dir: userSort.key === key ? -userSort.dir : -1 };
|
|
3865
|
+
renderUsers();
|
|
3866
|
+
return;
|
|
3867
|
+
}
|
|
3868
|
+
var row = event.target.closest("tr[data-install]");
|
|
3869
|
+
if (row) { location.hash = "install/" + row.dataset.install; return; }
|
|
3870
|
+
var session = event.target.closest("[data-session]");
|
|
3871
|
+
if (session) {
|
|
3872
|
+
selectedSession = session.dataset.session;
|
|
3873
|
+
document.querySelectorAll("#sessionList .session").forEach(function (b) { b.setAttribute("aria-pressed", String(b === session)); });
|
|
3874
|
+
renderWaterfall();
|
|
3875
|
+
return;
|
|
3876
|
+
}
|
|
3877
|
+
var kind = event.target.closest("[data-kind]");
|
|
3878
|
+
if (kind) { kindShown = kind.dataset.kind; renderEvents(); }
|
|
3879
|
+
});
|
|
3880
|
+
document.addEventListener("keydown", function (event) {
|
|
3881
|
+
var row = event.target.closest && event.target.closest("tr[data-install]");
|
|
3882
|
+
if (row && (event.key === "Enter" || event.key === " ")) { event.preventDefault(); location.hash = "install/" + row.dataset.install; }
|
|
3883
|
+
});
|
|
3884
|
+
$("csv").addEventListener("click", function () {
|
|
3885
|
+
if (!summary) return;
|
|
3886
|
+
var cols = USER_COLS.map(function (c) { return c[0]; });
|
|
3887
|
+
var lines = [cols.join(",")].concat(filteredUsers().map(function (u) {
|
|
3888
|
+
return cols.map(function (c) { return '"' + String(u[c]).replace(/"/g, '""') + '"'; }).join(",");
|
|
3889
|
+
}));
|
|
3890
|
+
var link = document.createElement("a");
|
|
3891
|
+
link.href = URL.createObjectURL(new Blob([lines.join(NL)], { type: "text/csv" }));
|
|
3892
|
+
link.download = "codebuddy-users-" + new Date().toISOString().slice(0, 10) + ".csv";
|
|
3893
|
+
link.click();
|
|
3894
|
+
});
|
|
3895
|
+
document.addEventListener("mousemove", function (event) {
|
|
3896
|
+
var text = event.target.getAttribute && event.target.getAttribute("data-tip");
|
|
3897
|
+
if (!text) { tip.style.display = "none"; return; }
|
|
3898
|
+
tip.textContent = text;
|
|
3899
|
+
tip.style.display = "block";
|
|
3900
|
+
tip.style.left = Math.min(event.pageX + 12, document.documentElement.scrollWidth - tip.offsetWidth - 8) + "px";
|
|
3901
|
+
tip.style.top = (event.pageY - 34) + "px";
|
|
3902
|
+
});
|
|
3903
|
+
var resizeTimer = 0;
|
|
3904
|
+
window.addEventListener("resize", function () {
|
|
3905
|
+
clearTimeout(resizeTimer);
|
|
3906
|
+
resizeTimer = setTimeout(function () { if (location.hash.indexOf("#install/") === 0) renderInstall(); else render(); }, 150);
|
|
3907
|
+
});
|
|
3908
|
+
window.addEventListener("hashchange", route);
|
|
3909
|
+
route();
|
|
3910
|
+
load();
|
|
3911
|
+
setInterval(load, 60000);
|
|
3912
|
+
})();
|
|
3913
|
+
</script>
|
|
3914
|
+
</body>
|
|
3915
|
+
</html>
|
|
3916
|
+
`;
|
|
3917
|
+
|
|
3918
|
+
// src/telemetry/collector.ts
|
|
3919
|
+
var MAX_BODY_BYTES = 256 * 1024;
|
|
3920
|
+
var INSTALL_BATCHES_PER_HOUR = 30;
|
|
3921
|
+
var GLOBAL_BATCHES_PER_MINUTE = 600;
|
|
3922
|
+
var NPM_PACKAGE = "@orquentra/codebuddy";
|
|
3923
|
+
var name = z2.string().regex(/^[A-Za-z][A-Za-z0-9_:.-]{0,63}$/);
|
|
3924
|
+
var short = z2.string().max(64);
|
|
3925
|
+
var batchSchema = z2.object({
|
|
3926
|
+
installId: z2.string().uuid(),
|
|
3927
|
+
project: z2.string().regex(/^[0-9a-f]{16}$/),
|
|
3928
|
+
version: short,
|
|
3929
|
+
platform: short,
|
|
3930
|
+
arch: short,
|
|
3931
|
+
node: short,
|
|
3932
|
+
clients: z2.array(name).max(10),
|
|
3933
|
+
events: z2.array(
|
|
3934
|
+
z2.object({
|
|
3935
|
+
ts: z2.string().datetime(),
|
|
3936
|
+
kind: name,
|
|
3937
|
+
name,
|
|
3938
|
+
ms: z2.number().int().min(0).max(864e5),
|
|
3939
|
+
ok: z2.boolean(),
|
|
3940
|
+
tokensIn: z2.number().int().min(0).max(1e7).optional(),
|
|
3941
|
+
tokensOut: z2.number().int().min(0).max(1e7).optional(),
|
|
3942
|
+
session: z2.string().regex(/^[0-9a-f]{16}$/).optional()
|
|
3943
|
+
})
|
|
3944
|
+
).min(1).max(500)
|
|
3945
|
+
});
|
|
3946
|
+
async function startCollector(options) {
|
|
3947
|
+
const { dataDir } = options;
|
|
3948
|
+
await mkdir2(dataDir, { recursive: true });
|
|
3949
|
+
const registry = await loadRegistry(dataDir);
|
|
3950
|
+
let summaryCache;
|
|
3951
|
+
let npmCache;
|
|
3952
|
+
const perInstall = /* @__PURE__ */ new Map();
|
|
3953
|
+
let globalCount = 0;
|
|
3954
|
+
const resetInstalls = setInterval(() => perInstall.clear(), 36e5);
|
|
3955
|
+
const resetGlobal = setInterval(() => {
|
|
3956
|
+
globalCount = 0;
|
|
3957
|
+
}, 6e4);
|
|
3958
|
+
resetInstalls.unref();
|
|
3959
|
+
resetGlobal.unref();
|
|
3960
|
+
const ingest = createServer2(async (request, response) => {
|
|
3961
|
+
const url = new URL(request.url ?? "/", "http://localhost");
|
|
3962
|
+
if (url.pathname !== "/v1/events") {
|
|
3963
|
+
response.writeHead(404).end();
|
|
3964
|
+
return;
|
|
3965
|
+
}
|
|
3966
|
+
if (request.method !== "POST") {
|
|
3967
|
+
response.writeHead(405).end();
|
|
3968
|
+
return;
|
|
3969
|
+
}
|
|
3970
|
+
if (++globalCount > GLOBAL_BATCHES_PER_MINUTE) {
|
|
3971
|
+
response.writeHead(429).end();
|
|
3972
|
+
return;
|
|
3973
|
+
}
|
|
3974
|
+
const body = await readBody(request);
|
|
3975
|
+
if (body === null) {
|
|
3976
|
+
response.writeHead(413, { connection: "close" }).end();
|
|
3977
|
+
return;
|
|
3978
|
+
}
|
|
3979
|
+
let batch;
|
|
3980
|
+
try {
|
|
3981
|
+
batch = batchSchema.parse(JSON.parse(body));
|
|
3982
|
+
} catch {
|
|
3983
|
+
response.writeHead(400).end();
|
|
3984
|
+
return;
|
|
3985
|
+
}
|
|
3986
|
+
const seen = (perInstall.get(batch.installId) ?? 0) + 1;
|
|
3987
|
+
perInstall.set(batch.installId, seen);
|
|
3988
|
+
if (seen > INSTALL_BATCHES_PER_HOUR) {
|
|
3989
|
+
response.writeHead(429).end();
|
|
3990
|
+
return;
|
|
3991
|
+
}
|
|
3992
|
+
const receivedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3993
|
+
if (!registry.has(batch.installId)) {
|
|
3994
|
+
registry.set(batch.installId, receivedAt);
|
|
3995
|
+
await appendFile(
|
|
3996
|
+
join6(dataDir, "installs.jsonl"),
|
|
3997
|
+
`${JSON.stringify({ installId: batch.installId, firstSeen: receivedAt })}
|
|
3998
|
+
`
|
|
3999
|
+
);
|
|
4000
|
+
}
|
|
4001
|
+
const { events, ...meta } = batch;
|
|
4002
|
+
const lines = events.map((event) => JSON.stringify({ ...meta, ...event, receivedAt }));
|
|
4003
|
+
await appendFile(
|
|
4004
|
+
join6(dataDir, `events-${receivedAt.slice(0, 10)}.jsonl`),
|
|
4005
|
+
`${lines.join("\n")}
|
|
4006
|
+
`
|
|
4007
|
+
);
|
|
4008
|
+
response.writeHead(204).end();
|
|
4009
|
+
summaryCache = void 0;
|
|
4739
4010
|
});
|
|
4740
|
-
|
|
4741
|
-
"
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4011
|
+
const admin = createServer2(async (request, response) => {
|
|
4012
|
+
const host = (request.headers.host ?? "").replace(/:\d+$/, "");
|
|
4013
|
+
if (host !== "127.0.0.1" && host !== "localhost" && !host.endsWith(".ts.net")) {
|
|
4014
|
+
response.writeHead(403, { "content-type": "text/plain" }).end("Forbidden host");
|
|
4015
|
+
return;
|
|
4016
|
+
}
|
|
4017
|
+
if (options.adminPassword && !passwordMatches(request, options.adminPassword)) {
|
|
4018
|
+
response.writeHead(401, {
|
|
4019
|
+
"www-authenticate": 'Basic realm="CodeBuddy admin", charset="UTF-8"',
|
|
4020
|
+
"content-type": "text/plain"
|
|
4021
|
+
}).end("Password required");
|
|
4022
|
+
return;
|
|
4023
|
+
}
|
|
4024
|
+
if (request.method !== "GET") {
|
|
4025
|
+
response.writeHead(405).end();
|
|
4026
|
+
return;
|
|
4027
|
+
}
|
|
4028
|
+
const url = new URL(request.url ?? "/", "http://localhost");
|
|
4029
|
+
url.pathname = url.pathname.replace(/^\/admin(?=\/|$)/, "") || "/";
|
|
4030
|
+
const json2 = (value) => {
|
|
4031
|
+
response.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" });
|
|
4032
|
+
response.end(JSON.stringify(value));
|
|
4033
|
+
};
|
|
4034
|
+
if (url.pathname === "/") {
|
|
4035
|
+
response.writeHead(200, {
|
|
4036
|
+
"content-type": "text/html; charset=utf-8",
|
|
4037
|
+
"cache-control": "no-store"
|
|
4750
4038
|
});
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4039
|
+
response.end(ADMIN_HTML);
|
|
4040
|
+
return;
|
|
4041
|
+
}
|
|
4042
|
+
if (url.pathname === "/api/summary") {
|
|
4043
|
+
const days = Math.min(365, Math.max(1, Number(url.searchParams.get("days")) || 30));
|
|
4044
|
+
if (!summaryCache || summaryCache.days !== days || Date.now() - summaryCache.at > 3e4) {
|
|
4045
|
+
summaryCache = {
|
|
4046
|
+
at: Date.now(),
|
|
4047
|
+
days,
|
|
4048
|
+
value: summarize(
|
|
4049
|
+
await readEvents(dataDir, Math.max(days, COHORT_WEEKS * 7)),
|
|
4050
|
+
days,
|
|
4051
|
+
Date.now(),
|
|
4052
|
+
registry
|
|
4053
|
+
)
|
|
4054
|
+
};
|
|
4055
|
+
}
|
|
4056
|
+
json2(summaryCache.value);
|
|
4057
|
+
return;
|
|
4058
|
+
}
|
|
4059
|
+
if (url.pathname === "/api/install") {
|
|
4060
|
+
const trace = installTrace(
|
|
4061
|
+
await readEvents(dataDir, 90),
|
|
4062
|
+
(url.searchParams.get("id") ?? "").toLowerCase()
|
|
4754
4063
|
);
|
|
4755
|
-
if (
|
|
4756
|
-
|
|
4064
|
+
if (!trace) {
|
|
4065
|
+
response.writeHead(404, { "content-type": "application/json" }).end("null");
|
|
4066
|
+
return;
|
|
4757
4067
|
}
|
|
4758
|
-
|
|
4068
|
+
json2(trace);
|
|
4069
|
+
return;
|
|
4070
|
+
}
|
|
4071
|
+
if (url.pathname === "/api/npm") {
|
|
4072
|
+
if (!npmCache || Date.now() - npmCache.at > 36e5) {
|
|
4073
|
+
npmCache = { at: Date.now(), value: await npmDownloads(options.fetchImpl ?? fetch) };
|
|
4074
|
+
}
|
|
4075
|
+
json2(npmCache.value);
|
|
4076
|
+
return;
|
|
4077
|
+
}
|
|
4078
|
+
response.writeHead(404, { "content-type": "text/plain" }).end("Not found");
|
|
4759
4079
|
});
|
|
4080
|
+
const ingestPort = await listen2(ingest, options.ingestPort ?? 8787);
|
|
4081
|
+
const adminPort = await listen2(admin, options.adminPort ?? 8788);
|
|
4082
|
+
const close = (server) => new Promise((resolve6) => {
|
|
4083
|
+
server.closeAllConnections();
|
|
4084
|
+
server.close(() => resolve6());
|
|
4085
|
+
});
|
|
4086
|
+
return {
|
|
4087
|
+
ingestUrl: `http://127.0.0.1:${ingestPort}/v1/events`,
|
|
4088
|
+
adminUrl: `http://127.0.0.1:${adminPort}/`,
|
|
4089
|
+
close: async () => {
|
|
4090
|
+
clearInterval(resetInstalls);
|
|
4091
|
+
clearInterval(resetGlobal);
|
|
4092
|
+
await Promise.all([close(ingest), close(admin)]);
|
|
4093
|
+
}
|
|
4094
|
+
};
|
|
4760
4095
|
}
|
|
4761
|
-
function
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
4773
|
-
|
|
4096
|
+
function passwordMatches(request, password3) {
|
|
4097
|
+
const header = request.headers.authorization ?? "";
|
|
4098
|
+
if (!header.startsWith("Basic ")) return false;
|
|
4099
|
+
const decoded = Buffer.from(header.slice(6), "base64").toString("utf8");
|
|
4100
|
+
const given = decoded.slice(decoded.indexOf(":") + 1);
|
|
4101
|
+
const digest = (value) => createHash("sha256").update(value).digest();
|
|
4102
|
+
return timingSafeEqual(digest(given), digest(password3));
|
|
4103
|
+
}
|
|
4104
|
+
function readBody(request) {
|
|
4105
|
+
return new Promise((resolve6) => {
|
|
4106
|
+
const chunks = [];
|
|
4107
|
+
let size = 0;
|
|
4108
|
+
request.on("data", (chunk) => {
|
|
4109
|
+
size += chunk.length;
|
|
4110
|
+
if (size > MAX_BODY_BYTES) resolve6(null);
|
|
4111
|
+
else chunks.push(chunk);
|
|
4112
|
+
});
|
|
4113
|
+
request.on("end", () => resolve6(Buffer.concat(chunks).toString("utf8")));
|
|
4114
|
+
request.on("error", () => resolve6(null));
|
|
4115
|
+
});
|
|
4774
4116
|
}
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
|
|
4117
|
+
function listen2(server, port) {
|
|
4118
|
+
return new Promise((resolve6, reject) => {
|
|
4119
|
+
server.once("error", reject);
|
|
4120
|
+
server.listen(port, "127.0.0.1", () => resolve6(server.address().port));
|
|
4121
|
+
});
|
|
4780
4122
|
}
|
|
4781
|
-
function
|
|
4782
|
-
|
|
4783
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
|
|
4123
|
+
async function readEvents(dataDir, days, now = Date.now()) {
|
|
4124
|
+
const since = new Date(now - (days - 1) * 864e5).toISOString().slice(0, 10);
|
|
4125
|
+
const files = (await readdir(dataDir).catch(() => [])).filter((file) => /^events-\d{4}-\d{2}-\d{2}\.jsonl$/.test(file) && file.slice(7, 17) >= since).sort();
|
|
4126
|
+
const events = [];
|
|
4127
|
+
for (const file of files) {
|
|
4128
|
+
for (const line of (await readFile4(join6(dataDir, file), "utf8")).split("\n")) {
|
|
4129
|
+
if (!line) continue;
|
|
4130
|
+
try {
|
|
4131
|
+
events.push(JSON.parse(line));
|
|
4132
|
+
} catch {
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
}
|
|
4136
|
+
return events;
|
|
4137
|
+
}
|
|
4138
|
+
var DAY_MS = 864e5;
|
|
4139
|
+
var dayOf = (iso) => iso.slice(0, 10);
|
|
4140
|
+
var dayNumber = (iso) => Math.floor(Date.parse(iso) / DAY_MS);
|
|
4141
|
+
var COHORT_WEEKS = 8;
|
|
4142
|
+
function featureOf(event) {
|
|
4143
|
+
if (event.kind === "tool") return event.name;
|
|
4144
|
+
if (event.kind === "cli") return `cli:${event.name}`;
|
|
4145
|
+
return event.kind;
|
|
4146
|
+
}
|
|
4147
|
+
var GOT_CONTEXT = (e) => e.kind === "recall" || e.kind === "tool" && /^(context_(pack|bootstrap|before_edit)|recall)$/.test(e.name);
|
|
4148
|
+
var SAVED_MEMORY = (e) => e.kind === "capture" || e.kind === "tool" && /^(context_after_turn|remember)$/.test(e.name);
|
|
4149
|
+
function summarize(events, days, now = Date.now(), registry = /* @__PURE__ */ new Map()) {
|
|
4150
|
+
const today = Math.floor(now / DAY_MS);
|
|
4151
|
+
const rangeStart = today - days + 1;
|
|
4152
|
+
const firstSeenOf = (install, fallback) => registry.get(install) ?? fallback;
|
|
4153
|
+
const activeDays = /* @__PURE__ */ new Map();
|
|
4154
|
+
for (const event of events) {
|
|
4155
|
+
const set = activeDays.get(event.installId) ?? /* @__PURE__ */ new Set();
|
|
4156
|
+
set.add(dayNumber(event.receivedAt));
|
|
4157
|
+
activeDays.set(event.installId, set);
|
|
4158
|
+
}
|
|
4159
|
+
const activeBetween = (from, to) => {
|
|
4160
|
+
let count = 0;
|
|
4161
|
+
for (const set of activeDays.values()) {
|
|
4162
|
+
for (const d of set) {
|
|
4163
|
+
if (d >= from && d <= to) {
|
|
4164
|
+
count += 1;
|
|
4165
|
+
break;
|
|
4796
4166
|
}
|
|
4797
|
-
|
|
4798
|
-
console.log(
|
|
4799
|
-
pc17.dim(
|
|
4800
|
-
`
|
|
4801
|
-
${result.stats.produced} suggestion(s) across ${result.stats.pathsConsidered} path(s)`
|
|
4802
|
-
)
|
|
4803
|
-
);
|
|
4804
|
-
});
|
|
4167
|
+
}
|
|
4805
4168
|
}
|
|
4806
|
-
|
|
4169
|
+
return count;
|
|
4170
|
+
};
|
|
4171
|
+
const inRange = events.filter((e) => dayNumber(e.receivedAt) >= rangeStart);
|
|
4172
|
+
const daily = /* @__PURE__ */ new Map();
|
|
4173
|
+
for (let d = rangeStart; d <= today; d += 1) daily.set(d, { installs: /* @__PURE__ */ new Set(), events: 0 });
|
|
4174
|
+
const features = /* @__PURE__ */ new Map();
|
|
4175
|
+
const latest = /* @__PURE__ */ new Map();
|
|
4176
|
+
const users = /* @__PURE__ */ new Map();
|
|
4177
|
+
const funnel = {
|
|
4178
|
+
active: /* @__PURE__ */ new Set(),
|
|
4179
|
+
context: /* @__PURE__ */ new Set(),
|
|
4180
|
+
memory: /* @__PURE__ */ new Set()
|
|
4181
|
+
};
|
|
4182
|
+
let errors = 0;
|
|
4183
|
+
let tokensOut = 0;
|
|
4184
|
+
for (const event of inRange) {
|
|
4185
|
+
const d = dayNumber(event.receivedAt);
|
|
4186
|
+
const bucket = daily.get(d);
|
|
4187
|
+
if (bucket) {
|
|
4188
|
+
bucket.installs.add(event.installId);
|
|
4189
|
+
bucket.events += 1;
|
|
4190
|
+
}
|
|
4191
|
+
latest.set(event.installId, event);
|
|
4192
|
+
const feature = featureOf(event);
|
|
4193
|
+
const user = users.get(event.installId) ?? {
|
|
4194
|
+
firstEvent: event.receivedAt,
|
|
4195
|
+
lastSeen: event.receivedAt,
|
|
4196
|
+
events: 0,
|
|
4197
|
+
errors: 0,
|
|
4198
|
+
sessions: /* @__PURE__ */ new Set(),
|
|
4199
|
+
projects: /* @__PURE__ */ new Set(),
|
|
4200
|
+
features: /* @__PURE__ */ new Map(),
|
|
4201
|
+
days: /* @__PURE__ */ new Set()
|
|
4202
|
+
};
|
|
4203
|
+
if (event.receivedAt > user.lastSeen) user.lastSeen = event.receivedAt;
|
|
4204
|
+
user.events += 1;
|
|
4205
|
+
if (!event.ok) user.errors += 1;
|
|
4206
|
+
if (event.session) user.sessions.add(event.session);
|
|
4207
|
+
user.projects.add(event.project);
|
|
4208
|
+
user.features.set(feature, (user.features.get(feature) ?? 0) + 1);
|
|
4209
|
+
user.days.add(d);
|
|
4210
|
+
users.set(event.installId, user);
|
|
4211
|
+
funnel.active.add(event.installId);
|
|
4212
|
+
if (GOT_CONTEXT(event)) funnel.context.add(event.installId);
|
|
4213
|
+
if (SAVED_MEMORY(event)) funnel.memory.add(event.installId);
|
|
4214
|
+
if (!event.ok) errors += 1;
|
|
4215
|
+
tokensOut += event.tokensOut ?? 0;
|
|
4216
|
+
const stats = features.get(feature) ?? { calls: 0, errors: 0, installs: /* @__PURE__ */ new Set(), ms: [] };
|
|
4217
|
+
stats.calls += 1;
|
|
4218
|
+
if (!event.ok) stats.errors += 1;
|
|
4219
|
+
stats.installs.add(event.installId);
|
|
4220
|
+
stats.ms.push(event.ms);
|
|
4221
|
+
features.set(feature, stats);
|
|
4222
|
+
}
|
|
4223
|
+
const firstSeenDay = /* @__PURE__ */ new Map();
|
|
4224
|
+
for (const [install, seen] of registry) firstSeenDay.set(install, dayNumber(seen));
|
|
4225
|
+
for (const [install, user] of users) {
|
|
4226
|
+
if (!firstSeenDay.has(install)) firstSeenDay.set(install, dayNumber(user.firstEvent));
|
|
4227
|
+
}
|
|
4228
|
+
for (const [install, set] of activeDays) {
|
|
4229
|
+
if (!firstSeenDay.has(install)) firstSeenDay.set(install, Math.min(...set));
|
|
4230
|
+
}
|
|
4231
|
+
const newPerDay = /* @__PURE__ */ new Map();
|
|
4232
|
+
for (const d of firstSeenDay.values()) newPerDay.set(d, (newPerDay.get(d) ?? 0) + 1);
|
|
4233
|
+
let cumulative = [...firstSeenDay.values()].filter((d) => d < rangeStart).length;
|
|
4234
|
+
const cohorts = [];
|
|
4235
|
+
for (let age = COHORT_WEEKS - 1; age >= 0; age -= 1) {
|
|
4236
|
+
const from = today - (age + 1) * 7 + 1;
|
|
4237
|
+
const members = [...firstSeenDay].filter(([, d]) => d >= from && d < from + 7).map(([install]) => install);
|
|
4238
|
+
const weeks = [];
|
|
4239
|
+
for (let week = 0; week <= age; week += 1) {
|
|
4240
|
+
const wFrom = from + week * 7;
|
|
4241
|
+
const active = members.filter((install) => {
|
|
4242
|
+
const set = activeDays.get(install);
|
|
4243
|
+
if (!set) return week === 0;
|
|
4244
|
+
for (const d of set) if (d >= wFrom && d < wFrom + 7) return true;
|
|
4245
|
+
return false;
|
|
4246
|
+
}).length;
|
|
4247
|
+
weeks.push(members.length ? active / members.length : 0);
|
|
4248
|
+
}
|
|
4249
|
+
cohorts.push({
|
|
4250
|
+
start: new Date(from * DAY_MS).toISOString().slice(0, 10),
|
|
4251
|
+
size: members.length,
|
|
4252
|
+
weeks
|
|
4253
|
+
});
|
|
4254
|
+
}
|
|
4255
|
+
const week1 = cohorts.filter((c) => c.size > 0 && c.weeks.length > 1);
|
|
4256
|
+
const week1Retention = week1.length ? week1.reduce((sum, c) => sum + (c.weeks[1] ?? 0) * c.size, 0) / week1.reduce((sum, c) => sum + c.size, 0) : null;
|
|
4257
|
+
const share = (pick) => {
|
|
4258
|
+
const counts = /* @__PURE__ */ new Map();
|
|
4259
|
+
for (const event of latest.values()) {
|
|
4260
|
+
for (const value of pick(event)) counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
4261
|
+
}
|
|
4262
|
+
return [...counts].map(([label, installs]) => ({ label, installs })).sort((a, b) => b.installs - a.installs);
|
|
4263
|
+
};
|
|
4264
|
+
const median = (values) => {
|
|
4265
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
4266
|
+
return sorted[Math.floor(sorted.length / 2)] ?? 0;
|
|
4267
|
+
};
|
|
4268
|
+
const p95 = (values) => {
|
|
4269
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
4270
|
+
return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.95))] ?? 0;
|
|
4271
|
+
};
|
|
4272
|
+
const wau = activeBetween(today - 6, today);
|
|
4273
|
+
const wauPrevious = activeBetween(today - 13, today - 7);
|
|
4274
|
+
const returned = [...users.values()].filter((user) => user.days.size >= 2).length;
|
|
4275
|
+
return {
|
|
4276
|
+
generatedAt: new Date(now).toISOString(),
|
|
4277
|
+
days,
|
|
4278
|
+
totals: {
|
|
4279
|
+
allTimeInstalls: firstSeenDay.size,
|
|
4280
|
+
installs: users.size,
|
|
4281
|
+
newInRange: [...firstSeenDay.values()].filter((d) => d >= rangeStart).length,
|
|
4282
|
+
newToday: newPerDay.get(today) ?? 0,
|
|
4283
|
+
projects: new Set(inRange.map((e) => `${e.installId}:${e.project}`)).size,
|
|
4284
|
+
sessions: new Set(inRange.filter((e) => e.session).map((e) => e.session)).size,
|
|
4285
|
+
events: inRange.length,
|
|
4286
|
+
errorRate: inRange.length ? errors / inRange.length : 0,
|
|
4287
|
+
tokensOut,
|
|
4288
|
+
dau: activeBetween(today, today),
|
|
4289
|
+
wau,
|
|
4290
|
+
wauPrevious,
|
|
4291
|
+
mau: activeBetween(today - 29, today),
|
|
4292
|
+
week1Retention
|
|
4293
|
+
},
|
|
4294
|
+
daily: [...daily].map(([d, bucket]) => {
|
|
4295
|
+
cumulative += newPerDay.get(d) ?? 0;
|
|
4296
|
+
return {
|
|
4297
|
+
date: new Date(d * DAY_MS).toISOString().slice(0, 10),
|
|
4298
|
+
installs: bucket.installs.size,
|
|
4299
|
+
events: bucket.events,
|
|
4300
|
+
newInstalls: newPerDay.get(d) ?? 0,
|
|
4301
|
+
totalInstalls: cumulative
|
|
4302
|
+
};
|
|
4303
|
+
}),
|
|
4304
|
+
/** Installs active in the range that reached each step. */
|
|
4305
|
+
funnel: [
|
|
4306
|
+
{ label: "Active", installs: funnel.active.size },
|
|
4307
|
+
{ label: "Received context", installs: funnel.context.size },
|
|
4308
|
+
{ label: "Saved memory", installs: funnel.memory.size },
|
|
4309
|
+
{ label: "Came back another day", installs: returned }
|
|
4310
|
+
],
|
|
4311
|
+
cohorts,
|
|
4312
|
+
features: [...features].map(([label, stats]) => ({
|
|
4313
|
+
label,
|
|
4314
|
+
calls: stats.calls,
|
|
4315
|
+
installs: stats.installs.size,
|
|
4316
|
+
errorRate: stats.errors / stats.calls,
|
|
4317
|
+
p50ms: median(stats.ms),
|
|
4318
|
+
p95ms: p95(stats.ms)
|
|
4319
|
+
})).sort((a, b) => b.calls - a.calls).slice(0, 25),
|
|
4320
|
+
clients: share((e) => e.clients.length ? e.clients : ["unknown"]),
|
|
4321
|
+
versions: share((e) => [e.version]),
|
|
4322
|
+
platforms: share((e) => [`${e.platform}/${e.arch}`]),
|
|
4323
|
+
nodes: share((e) => [`node ${e.node}`]),
|
|
4324
|
+
/** One row per install active in the range, most recently active first. */
|
|
4325
|
+
users: [...users].map(([installId, user]) => {
|
|
4326
|
+
const last = latest.get(installId);
|
|
4327
|
+
return {
|
|
4328
|
+
install: installId.slice(0, 8),
|
|
4329
|
+
firstSeen: firstSeenOf(installId, user.firstEvent),
|
|
4330
|
+
lastSeen: user.lastSeen,
|
|
4331
|
+
activeDays: user.days.size,
|
|
4332
|
+
events: user.events,
|
|
4333
|
+
errors: user.errors,
|
|
4334
|
+
sessions: user.sessions.size,
|
|
4335
|
+
projects: user.projects.size,
|
|
4336
|
+
client: last?.clients.join(",") || "unknown",
|
|
4337
|
+
version: last?.version ?? "",
|
|
4338
|
+
platform: last ? `${last.platform}/${last.arch}` : "",
|
|
4339
|
+
topFeature: [...user.features].sort((a, b) => b[1] - a[1])[0]?.[0] ?? ""
|
|
4340
|
+
};
|
|
4341
|
+
}).sort((a, b) => b.lastSeen.localeCompare(a.lastSeen)).slice(0, 1e3),
|
|
4342
|
+
recent: inRange.slice(-50).reverse().map((e) => ({
|
|
4343
|
+
receivedAt: e.receivedAt,
|
|
4344
|
+
install: e.installId.slice(0, 8),
|
|
4345
|
+
kind: e.kind,
|
|
4346
|
+
name: e.name,
|
|
4347
|
+
ms: e.ms,
|
|
4348
|
+
ok: e.ok,
|
|
4349
|
+
version: e.version,
|
|
4350
|
+
client: e.clients.join(",") || "unknown"
|
|
4351
|
+
}))
|
|
4352
|
+
};
|
|
4807
4353
|
}
|
|
4808
|
-
function
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4354
|
+
function installTrace(events, prefix) {
|
|
4355
|
+
const own = events.filter((e) => e.installId.startsWith(prefix));
|
|
4356
|
+
const first = own[0];
|
|
4357
|
+
if (!first || !/^[0-9a-f]{8}$/.test(prefix)) return null;
|
|
4358
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
4359
|
+
const perDay = /* @__PURE__ */ new Map();
|
|
4360
|
+
for (const event of own) {
|
|
4361
|
+
perDay.set(dayOf(event.receivedAt), (perDay.get(dayOf(event.receivedAt)) ?? 0) + 1);
|
|
4362
|
+
const key = event.session ?? "none";
|
|
4363
|
+
const session = sessions.get(key) ?? {
|
|
4364
|
+
start: event.ts,
|
|
4365
|
+
end: event.ts,
|
|
4366
|
+
events: 0,
|
|
4367
|
+
errors: 0,
|
|
4368
|
+
ms: 0,
|
|
4369
|
+
features: /* @__PURE__ */ new Set()
|
|
4370
|
+
};
|
|
4371
|
+
if (event.ts < session.start) session.start = event.ts;
|
|
4372
|
+
if (event.ts > session.end) session.end = event.ts;
|
|
4373
|
+
session.events += 1;
|
|
4374
|
+
if (!event.ok) session.errors += 1;
|
|
4375
|
+
session.ms += event.ms;
|
|
4376
|
+
session.features.add(featureOf(event));
|
|
4377
|
+
sessions.set(key, session);
|
|
4812
4378
|
}
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4379
|
+
const last = own[own.length - 1] ?? first;
|
|
4380
|
+
return {
|
|
4381
|
+
install: prefix,
|
|
4382
|
+
client: last.clients.join(",") || "unknown",
|
|
4383
|
+
version: last.version,
|
|
4384
|
+
platform: `${last.platform}/${last.arch}`,
|
|
4385
|
+
node: last.node,
|
|
4386
|
+
projects: new Set(own.map((e) => e.project)).size,
|
|
4387
|
+
perDay: [...perDay].map(([date, count]) => ({ date, events: count })),
|
|
4388
|
+
sessions: [...sessions].map(([id, session]) => ({ id, ...session, features: [...session.features] })).sort((a, b) => b.end.localeCompare(a.end)).slice(0, 50),
|
|
4389
|
+
events: own.slice(-1e3).map((e) => ({
|
|
4390
|
+
ts: e.ts,
|
|
4391
|
+
kind: e.kind,
|
|
4392
|
+
name: e.name,
|
|
4393
|
+
feature: featureOf(e),
|
|
4394
|
+
ms: e.ms,
|
|
4395
|
+
ok: e.ok,
|
|
4396
|
+
session: e.session ?? "none",
|
|
4397
|
+
tokensOut: e.tokensOut ?? 0
|
|
4398
|
+
}))
|
|
4399
|
+
};
|
|
4400
|
+
}
|
|
4401
|
+
async function loadRegistry(dataDir) {
|
|
4402
|
+
const registry = /* @__PURE__ */ new Map();
|
|
4403
|
+
const path = join6(dataDir, "installs.jsonl");
|
|
4404
|
+
const raw = await readFile4(path, "utf8").catch(() => null);
|
|
4405
|
+
if (raw !== null) {
|
|
4406
|
+
for (const line of raw.split("\n")) {
|
|
4407
|
+
try {
|
|
4408
|
+
const entry = JSON.parse(line);
|
|
4409
|
+
if (!registry.has(entry.installId)) registry.set(entry.installId, entry.firstSeen);
|
|
4410
|
+
} catch {
|
|
4411
|
+
}
|
|
4818
4412
|
}
|
|
4819
|
-
|
|
4413
|
+
return registry;
|
|
4820
4414
|
}
|
|
4415
|
+
for (const event of await readEvents(dataDir, 3650)) {
|
|
4416
|
+
if (!registry.has(event.installId)) registry.set(event.installId, event.receivedAt);
|
|
4417
|
+
}
|
|
4418
|
+
const lines = [...registry].map(
|
|
4419
|
+
([installId, firstSeen]) => JSON.stringify({ installId, firstSeen })
|
|
4420
|
+
);
|
|
4421
|
+
await appendFile(path, lines.length ? `${lines.join("\n")}
|
|
4422
|
+
` : "");
|
|
4423
|
+
return registry;
|
|
4821
4424
|
}
|
|
4822
|
-
function
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
|
|
4829
|
-
|
|
4830
|
-
|
|
4831
|
-
|
|
4425
|
+
async function npmDownloads(fetchImpl) {
|
|
4426
|
+
const to = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
4427
|
+
const from = new Date(Date.now() - 364 * 864e5).toISOString().slice(0, 10);
|
|
4428
|
+
try {
|
|
4429
|
+
const response = await fetchImpl(
|
|
4430
|
+
`https://api.npmjs.org/downloads/range/${from}:${to}/${NPM_PACKAGE}`,
|
|
4431
|
+
{ signal: AbortSignal.timeout(5e3) }
|
|
4432
|
+
);
|
|
4433
|
+
if (!response.ok) return { error: `npm API ${response.status}` };
|
|
4434
|
+
const body = await response.json();
|
|
4435
|
+
return { package: NPM_PACKAGE, downloads: body.downloads ?? [] };
|
|
4436
|
+
} catch (error) {
|
|
4437
|
+
return { error: error.message };
|
|
4832
4438
|
}
|
|
4833
4439
|
}
|
|
4834
4440
|
|
|
4835
|
-
// src/cli/commands/
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
4839
|
-
function registerSymbolsCommand(program, runSafely2) {
|
|
4840
|
-
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) => {
|
|
4441
|
+
// src/cli/commands/telemetry.ts
|
|
4442
|
+
function registerTelemetryCommands(program, runSafely2) {
|
|
4443
|
+
const telemetry = program.command("telemetry").description("Show or change anonymous usage telemetry (no prompts, code or paths).");
|
|
4444
|
+
telemetry.command("status").description("Show whether this machine sends anonymous usage data.").action(async () => {
|
|
4841
4445
|
await runSafely2(async () => {
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
if (
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4446
|
+
const state = await readTelemetryState();
|
|
4447
|
+
const blocked = telemetryBlocked(state);
|
|
4448
|
+
console.log(
|
|
4449
|
+
blocked ? `${pc19.yellow("\u25CB")} telemetry off (${blocked})` : `${pc19.green("\u25CF")} telemetry on`
|
|
4450
|
+
);
|
|
4451
|
+
console.log(pc19.dim(` install id ${state.installId} (${telemetryStatePath()})`));
|
|
4452
|
+
if (telemetryUrl()) console.log(pc19.dim(` collector ${telemetryUrl()}`));
|
|
4453
|
+
});
|
|
4454
|
+
});
|
|
4455
|
+
telemetry.command("enable").description("Allow anonymous usage data from this machine.").action(async () => {
|
|
4456
|
+
await runSafely2(async () => {
|
|
4457
|
+
const state = await readTelemetryState();
|
|
4458
|
+
await writeTelemetryState({
|
|
4459
|
+
installId: state.installId,
|
|
4460
|
+
noticeAt: state.noticeAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
4461
|
+
});
|
|
4462
|
+
console.log(TELEMETRY_NOTICE);
|
|
4463
|
+
console.log(pc19.green("Telemetry enabled."));
|
|
4464
|
+
});
|
|
4465
|
+
});
|
|
4466
|
+
telemetry.command("disable").description("Stop sending anonymous usage data from this machine.").action(async () => {
|
|
4467
|
+
await runSafely2(async () => {
|
|
4468
|
+
const state = await readTelemetryState();
|
|
4469
|
+
await writeTelemetryState({ ...state, disabled: true });
|
|
4470
|
+
console.log("Telemetry disabled. Nothing more is sent from this machine.");
|
|
4471
|
+
});
|
|
4472
|
+
});
|
|
4473
|
+
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(
|
|
4474
|
+
"--ingest-port <port>",
|
|
4475
|
+
"127.0.0.1 port for POST /v1/events (expose publicly).",
|
|
4476
|
+
Number,
|
|
4477
|
+
8787
|
|
4478
|
+
).option(
|
|
4479
|
+
"--admin-port <port>",
|
|
4480
|
+
"127.0.0.1 port for the admin panel (keep private).",
|
|
4481
|
+
Number,
|
|
4482
|
+
8788
|
|
4483
|
+
).action(async (opts) => {
|
|
4484
|
+
await runSafely2(async () => {
|
|
4485
|
+
const collector = await startCollector({
|
|
4486
|
+
dataDir: opts.data,
|
|
4487
|
+
ingestPort: opts.ingestPort,
|
|
4488
|
+
adminPort: opts.adminPort,
|
|
4489
|
+
...process.env.CODEBUDDY_ADMIN_PASSWORD ? { adminPassword: process.env.CODEBUDDY_ADMIN_PASSWORD } : {}
|
|
4490
|
+
});
|
|
4491
|
+
console.log(`${pc19.green("\u25CF")} ingest ${pc19.bold(collector.ingestUrl)}`);
|
|
4492
|
+
console.log(`${pc19.green("\u25CF")} admin ${pc19.bold(collector.adminUrl)}`);
|
|
4493
|
+
console.log(
|
|
4494
|
+
pc19.dim(
|
|
4495
|
+
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."
|
|
4496
|
+
)
|
|
4497
|
+
);
|
|
4498
|
+
console.log(pc19.dim(` events in ${opts.data}. Ctrl+C to stop.`));
|
|
4499
|
+
await new Promise((resolve6) => {
|
|
4500
|
+
const stop = () => void collector.close().then(resolve6);
|
|
4501
|
+
process.once("SIGINT", stop);
|
|
4502
|
+
process.once("SIGTERM", stop);
|
|
4503
|
+
});
|
|
4864
4504
|
});
|
|
4865
4505
|
});
|
|
4866
4506
|
}
|
|
@@ -4868,45 +4508,45 @@ ${table.length} symbol(s)`));
|
|
|
4868
4508
|
// src/cli/commands/use-command.ts
|
|
4869
4509
|
import { basename as basename2 } from "path";
|
|
4870
4510
|
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
|
|
4511
|
+
import pc20 from "picocolors";
|
|
4872
4512
|
|
|
4873
4513
|
// src/core/global-config.ts
|
|
4874
|
-
import { chmod, mkdir as
|
|
4875
|
-
import { homedir
|
|
4876
|
-
import { dirname as
|
|
4877
|
-
import { z as
|
|
4514
|
+
import { chmod, mkdir as mkdir3, readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
|
|
4515
|
+
import { homedir } from "os";
|
|
4516
|
+
import { dirname as dirname3, join as join7 } from "path";
|
|
4517
|
+
import { z as z3 } from "zod";
|
|
4878
4518
|
var GLOBAL_DIR = ".codebuddy";
|
|
4879
4519
|
var GLOBAL_FILE = "global.json";
|
|
4880
|
-
var globalConfigSchema =
|
|
4881
|
-
hfToken:
|
|
4882
|
-
databaseUrl:
|
|
4520
|
+
var globalConfigSchema = z3.object({
|
|
4521
|
+
hfToken: z3.string().min(1).optional(),
|
|
4522
|
+
databaseUrl: z3.string().min(1).optional(),
|
|
4883
4523
|
// Where the codebuddy install lives on this machine. Cached so future
|
|
4884
4524
|
// `codebuddy use` calls in unrelated projects can find the CLI entry
|
|
4885
4525
|
// without re-resolving via process.argv every time.
|
|
4886
|
-
installPath:
|
|
4526
|
+
installPath: z3.string().optional()
|
|
4887
4527
|
});
|
|
4888
|
-
function globalConfigPath(home =
|
|
4889
|
-
return
|
|
4528
|
+
function globalConfigPath(home = homedir()) {
|
|
4529
|
+
return join7(home, GLOBAL_DIR, GLOBAL_FILE);
|
|
4890
4530
|
}
|
|
4891
|
-
async function readGlobalConfig(home =
|
|
4531
|
+
async function readGlobalConfig(home = homedir()) {
|
|
4892
4532
|
const path = globalConfigPath(home);
|
|
4893
4533
|
try {
|
|
4894
|
-
const raw = await
|
|
4534
|
+
const raw = await readFile5(path, "utf8");
|
|
4895
4535
|
return globalConfigSchema.parse(JSON.parse(raw));
|
|
4896
4536
|
} catch (error) {
|
|
4897
4537
|
if (error.code === "ENOENT") return {};
|
|
4898
4538
|
throw error;
|
|
4899
4539
|
}
|
|
4900
4540
|
}
|
|
4901
|
-
async function writeGlobalConfig(next, home =
|
|
4541
|
+
async function writeGlobalConfig(next, home = homedir()) {
|
|
4902
4542
|
const path = globalConfigPath(home);
|
|
4903
|
-
await
|
|
4543
|
+
await mkdir3(dirname3(path), { recursive: true, mode: 448 });
|
|
4904
4544
|
await writeFile3(path, `${JSON.stringify(next, null, 2)}
|
|
4905
4545
|
`, { mode: 384 });
|
|
4906
4546
|
await chmod(path, 384);
|
|
4907
4547
|
return { path };
|
|
4908
4548
|
}
|
|
4909
|
-
async function mergeGlobalConfig(patch, home =
|
|
4549
|
+
async function mergeGlobalConfig(patch, home = homedir()) {
|
|
4910
4550
|
const current = await readGlobalConfig(home);
|
|
4911
4551
|
const merged = { ...current, ...patch };
|
|
4912
4552
|
const { path } = await writeGlobalConfig(merged, home);
|
|
@@ -4952,15 +4592,15 @@ async function runUseCommand(options = {}) {
|
|
|
4952
4592
|
})
|
|
4953
4593
|
));
|
|
4954
4594
|
note2(
|
|
4955
|
-
`Project folder: ${
|
|
4956
|
-
Namespace: ${
|
|
4595
|
+
`Project folder: ${pc20.bold(process.cwd())}
|
|
4596
|
+
Namespace: ${pc20.bold(namespace)}`,
|
|
4957
4597
|
"codebuddy use"
|
|
4958
4598
|
);
|
|
4959
4599
|
const stored = await readGlobalConfig();
|
|
4960
4600
|
const projectConfig = await readConfigFile();
|
|
4961
4601
|
let embeddings = projectConfig.provider?.type;
|
|
4962
4602
|
if (embeddings) {
|
|
4963
|
-
log2.info(`Embeddings: ${embeddings} (change with ${
|
|
4603
|
+
log2.info(`Embeddings: ${embeddings} (change with ${pc20.cyan("codebuddy models use")}).`);
|
|
4964
4604
|
} else if (!interactive) {
|
|
4965
4605
|
embeddings = "local";
|
|
4966
4606
|
} else {
|
|
@@ -5001,7 +4641,7 @@ Namespace: ${pc19.bold(namespace)}`,
|
|
|
5001
4641
|
log2.info("Using HF_TOKEN from environment.");
|
|
5002
4642
|
} else if (stored.hfToken) {
|
|
5003
4643
|
hfToken = stored.hfToken;
|
|
5004
|
-
log2.info(`Reusing saved Hugging Face token (${
|
|
4644
|
+
log2.info(`Reusing saved Hugging Face token (${pc20.dim("~/.codebuddy/global.json")}).`);
|
|
5005
4645
|
} else if (!interactive) {
|
|
5006
4646
|
throw new Error(
|
|
5007
4647
|
"Hugging Face embeddings need a token: set HF_TOKEN (no terminal to prompt for it)."
|
|
@@ -5054,7 +4694,7 @@ Namespace: ${pc19.bold(namespace)}`,
|
|
|
5054
4694
|
if (result.code === 0) {
|
|
5055
4695
|
composeSpinner.stop("Postgres container started.");
|
|
5056
4696
|
for (let attempt = 0; attempt < 12 && !connected; attempt++) {
|
|
5057
|
-
await new Promise((
|
|
4697
|
+
await new Promise((resolve6) => setTimeout(resolve6, 500));
|
|
5058
4698
|
connected = await pingOnce2(databaseUrl);
|
|
5059
4699
|
}
|
|
5060
4700
|
if (!connected) {
|
|
@@ -5135,7 +4775,7 @@ Namespace: ${pc19.bold(namespace)}`,
|
|
|
5135
4775
|
databaseUrl
|
|
5136
4776
|
});
|
|
5137
4777
|
log2.success(
|
|
5138
|
-
`Registered ${
|
|
4778
|
+
`Registered ${pc20.bold(install.serverKey)} in Claude Desktop. Restart with \u2318Q to activate.`
|
|
5139
4779
|
);
|
|
5140
4780
|
} catch (error) {
|
|
5141
4781
|
log2.warn(`Claude Desktop registration skipped: ${error.message}`);
|
|
@@ -5150,7 +4790,7 @@ Namespace: ${pc19.bold(namespace)}`,
|
|
|
5150
4790
|
databaseUrl
|
|
5151
4791
|
});
|
|
5152
4792
|
log2.success(
|
|
5153
|
-
`Registered ${
|
|
4793
|
+
`Registered ${pc20.bold(install.serverKey)} in Codex. Restart Codex to activate.`
|
|
5154
4794
|
);
|
|
5155
4795
|
} catch (error) {
|
|
5156
4796
|
log2.warn(`Codex registration skipped: ${error.message}`);
|
|
@@ -5158,24 +4798,24 @@ Namespace: ${pc19.bold(namespace)}`,
|
|
|
5158
4798
|
}
|
|
5159
4799
|
note2(
|
|
5160
4800
|
[
|
|
5161
|
-
`${
|
|
5162
|
-
`${
|
|
5163
|
-
`${
|
|
5164
|
-
`${
|
|
4801
|
+
`${pc20.green("\u2713")} CodeBuddy is ready for ${pc20.bold(folderName)}.`,
|
|
4802
|
+
`${pc20.green("\u2713")} Scaffolded ${pc20.cyan(".codebuddy/policies.yaml")} for project-specific guardrails.`,
|
|
4803
|
+
`${pc20.green("\u2713")} ${graphifyMessage}`,
|
|
4804
|
+
`${pc20.green("\u2713")} Added agent workflow instructions to ${pc20.cyan("CLAUDE.md")} and ${pc20.cyan("AGENTS.md")}.`,
|
|
5165
4805
|
``,
|
|
5166
4806
|
`Next:`,
|
|
5167
4807
|
` 1. Restart Claude Desktop and Codex.`,
|
|
5168
|
-
` 2. Ask Claude or Codex to use the ${
|
|
4808
|
+
` 2. Ask Claude or Codex to use the ${pc20.cyan("context_pack")} tool for each coding task.`,
|
|
5169
4809
|
` 3. Indexing, Graphify, Markdown memory import, and embeddings now update automatically while the MCP server is running.`,
|
|
5170
|
-
` 4. Run ${
|
|
4810
|
+
` 4. Run ${pc20.cyan("codebuddy doctor")} or ${pc20.cyan("codebuddy db doctor")} if you want a full health check.`,
|
|
5171
4811
|
``,
|
|
5172
4812
|
`Manage:`,
|
|
5173
|
-
` codebuddy claude list ${
|
|
5174
|
-
` codebuddy codex list ${
|
|
5175
|
-
` codebuddy db test ${
|
|
4813
|
+
` codebuddy claude list ${pc20.dim("# see every project wired up")}`,
|
|
4814
|
+
` codebuddy codex list ${pc20.dim("# see Codex entries")}`,
|
|
4815
|
+
` codebuddy db test ${pc20.dim("# quick Postgres connectivity check")}`,
|
|
5176
4816
|
` codebuddy claude remove codebuddy-${namespace}`,
|
|
5177
4817
|
` codebuddy codex remove codebuddy-${namespace}`,
|
|
5178
|
-
` codebuddy postgres down ${
|
|
4818
|
+
` codebuddy postgres down ${pc20.dim("# stop the DB (data preserved)")}`
|
|
5179
4819
|
].join("\n"),
|
|
5180
4820
|
"Done"
|
|
5181
4821
|
);
|
|
@@ -5186,7 +4826,7 @@ Namespace: ${pc19.bold(namespace)}`,
|
|
|
5186
4826
|
|
|
5187
4827
|
// src/cli/commands/worklog.ts
|
|
5188
4828
|
import { relative as relative6 } from "path";
|
|
5189
|
-
import
|
|
4829
|
+
import pc21 from "picocolors";
|
|
5190
4830
|
function registerWorklogCommands(program, runSafely2) {
|
|
5191
4831
|
const worklog = program.command("worklog").description("Inspect the cross-chat worklog of completed turns.");
|
|
5192
4832
|
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 +4837,7 @@ function registerWorklogCommands(program, runSafely2) {
|
|
|
5197
4837
|
return;
|
|
5198
4838
|
}
|
|
5199
4839
|
if (entries.length === 0) {
|
|
5200
|
-
console.log(
|
|
4840
|
+
console.log(pc21.dim("no worklog entries yet"));
|
|
5201
4841
|
return;
|
|
5202
4842
|
}
|
|
5203
4843
|
for (const entry of entries) console.log(renderRow2(entry));
|
|
@@ -5211,36 +4851,36 @@ function registerWorklogCommands(program, runSafely2) {
|
|
|
5211
4851
|
worklog.command("latest").description("Show the most recent worklog entry.").action(async () => {
|
|
5212
4852
|
await runSafely2(async () => {
|
|
5213
4853
|
const [entry] = await new WorklogStore().list();
|
|
5214
|
-
console.log(entry ? renderEntry(entry) :
|
|
4854
|
+
console.log(entry ? renderEntry(entry) : pc21.dim("no worklog entries yet"));
|
|
5215
4855
|
});
|
|
5216
4856
|
});
|
|
5217
4857
|
}
|
|
5218
4858
|
function renderRow2(entry) {
|
|
5219
4859
|
const summary = entry.summary.replace(/\s+/g, " ");
|
|
5220
|
-
const
|
|
5221
|
-
return `${
|
|
4860
|
+
const short2 = summary.length <= 70 ? summary : `${summary.slice(0, 69)}\u2026`;
|
|
4861
|
+
return `${pc21.dim(entry.createdAt.slice(0, 16))} ${pc21.dim(entry.id)} ${short2} ${pc21.dim(`(${entry.files.length} file(s))`)}`;
|
|
5222
4862
|
}
|
|
5223
4863
|
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(`${
|
|
4864
|
+
const lines = [`${pc21.bold(entry.id)} ${pc21.dim(entry.createdAt)} ${pc21.dim(entry.namespace)}`];
|
|
4865
|
+
if (entry.sourceSessionId) lines.push(`${pc21.bold("session:")} ${entry.sourceSessionId}`);
|
|
4866
|
+
if (entry.sourcePlanId) lines.push(`${pc21.bold("plan:")} ${entry.sourcePlanId}`);
|
|
4867
|
+
if (entry.commitSha) lines.push(`${pc21.bold("commit:")} ${entry.commitSha}`);
|
|
4868
|
+
if (entry.files.length > 0) lines.push(`${pc21.bold("files:")} ${entry.files.join(", ")}`);
|
|
5229
4869
|
if (entry.verification.length > 0) {
|
|
5230
|
-
lines.push(`${
|
|
4870
|
+
lines.push(`${pc21.bold("verification:")}`, ...entry.verification.map((line) => ` - ${line}`));
|
|
5231
4871
|
}
|
|
5232
4872
|
if (entry.savedMemory.length > 0) {
|
|
5233
|
-
lines.push(`${
|
|
4873
|
+
lines.push(`${pc21.bold("saved memory:")} ${entry.savedMemory.join(", ")}`);
|
|
5234
4874
|
}
|
|
5235
4875
|
if (entry.queuedForReview.length > 0) {
|
|
5236
|
-
lines.push(`${
|
|
4876
|
+
lines.push(`${pc21.bold("queued for review:")} ${entry.queuedForReview.join(", ")}`);
|
|
5237
4877
|
}
|
|
5238
|
-
lines.push("", entry.summary, "",
|
|
4878
|
+
lines.push("", entry.summary, "", pc21.dim(relative6(process.cwd(), entry.path)));
|
|
5239
4879
|
return lines.join("\n");
|
|
5240
4880
|
}
|
|
5241
4881
|
|
|
5242
4882
|
// src/cli/commands/zvec.ts
|
|
5243
|
-
import
|
|
4883
|
+
import pc22 from "picocolors";
|
|
5244
4884
|
function registerZvecCommands(program, runSafely2) {
|
|
5245
4885
|
const zvec = program.command("zvec").description("Optional zvec-grep semantic file discovery for the context tools.");
|
|
5246
4886
|
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 +4890,22 @@ function registerZvecCommands(program, runSafely2) {
|
|
|
5250
4890
|
console.log(JSON.stringify(report, null, 2));
|
|
5251
4891
|
return;
|
|
5252
4892
|
}
|
|
5253
|
-
const mark = (ok2) => ok2 ?
|
|
4893
|
+
const mark = (ok2) => ok2 ? pc22.green("yes") : pc22.yellow("no");
|
|
5254
4894
|
console.log(`enabled: ${mark(report.enabled)}`);
|
|
5255
4895
|
console.log(`binary: ${report.binary}`);
|
|
5256
4896
|
console.log(
|
|
5257
|
-
`version: ${report.version ??
|
|
4897
|
+
`version: ${report.version ?? pc22.yellow("not found")}${report.version && !report.supported ? pc22.yellow(" (unsupported; need 0.2.x)") : ""}`
|
|
5258
4898
|
);
|
|
5259
4899
|
console.log(`index ready: ${mark(report.indexReady)}`);
|
|
5260
4900
|
console.log(
|
|
5261
|
-
`zg server: ${report.server} ${
|
|
4901
|
+
`zg server: ${report.server} ${pc22.dim("(running = faster queries; optional)")}`
|
|
5262
4902
|
);
|
|
5263
4903
|
console.log(`status: ${report.reason}`);
|
|
5264
|
-
if (!report.version) console.log(
|
|
4904
|
+
if (!report.version) console.log(pc22.dim("install: npm install -g @zvec/zvec-grep"));
|
|
5265
4905
|
else if (report.supported && !report.indexReady)
|
|
5266
|
-
console.log(
|
|
4906
|
+
console.log(pc22.dim("next: codebuddy zvec index"));
|
|
5267
4907
|
else if (report.indexReady && !report.enabled)
|
|
5268
|
-
console.log(
|
|
4908
|
+
console.log(pc22.dim("next: codebuddy zvec enable"));
|
|
5269
4909
|
});
|
|
5270
4910
|
});
|
|
5271
4911
|
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 +4915,21 @@ function registerZvecCommands(program, runSafely2) {
|
|
|
5275
4915
|
...opts.rebuild ? { rebuild: true } : {}
|
|
5276
4916
|
});
|
|
5277
4917
|
if (code !== 0) throw new Error(`zg index exited with code ${code}`);
|
|
5278
|
-
console.log(
|
|
4918
|
+
console.log(pc22.dim("next: codebuddy zvec enable (.zvec-grep/ ignores itself in git)"));
|
|
5279
4919
|
});
|
|
5280
4920
|
});
|
|
5281
4921
|
zvec.command("enable").description("Use zvec-grep candidates in context_pack / context_before_edit.").action(async () => {
|
|
5282
4922
|
await runSafely2(async () => {
|
|
5283
4923
|
await setZvecEnabled(true);
|
|
5284
4924
|
console.log(
|
|
5285
|
-
`${
|
|
4925
|
+
`${pc22.green("enabled")} zvec-grep retrieval (falls back to keywords if unavailable)`
|
|
5286
4926
|
);
|
|
5287
4927
|
});
|
|
5288
4928
|
});
|
|
5289
4929
|
zvec.command("disable").description("Stop using zvec-grep; keyword ranking only.").action(async () => {
|
|
5290
4930
|
await runSafely2(async () => {
|
|
5291
4931
|
await setZvecEnabled(false);
|
|
5292
|
-
console.log(`${
|
|
4932
|
+
console.log(`${pc22.yellow("disabled")} zvec-grep retrieval`);
|
|
5293
4933
|
});
|
|
5294
4934
|
});
|
|
5295
4935
|
}
|
|
@@ -5598,6 +5238,13 @@ function createCli() {
|
|
|
5598
5238
|
registerClientsCommands(program, runSafely);
|
|
5599
5239
|
registerGitHubCommands(program, runSafely);
|
|
5600
5240
|
registerSymbolsCommand(program, runSafely);
|
|
5241
|
+
registerTelemetryCommands(program, runSafely);
|
|
5242
|
+
let started = Date.now();
|
|
5243
|
+
program.hook("preAction", () => {
|
|
5244
|
+
started = Date.now();
|
|
5245
|
+
return showTelemetryNoticeOnce();
|
|
5246
|
+
});
|
|
5247
|
+
program.hook("postAction", (_program, command) => traceCliCommand(command, started));
|
|
5601
5248
|
program.action(async () => {
|
|
5602
5249
|
await runSafely(() => runUseCommand());
|
|
5603
5250
|
});
|
|
@@ -5625,17 +5272,20 @@ var HELP_GROUPS = [
|
|
|
5625
5272
|
"prune"
|
|
5626
5273
|
]
|
|
5627
5274
|
],
|
|
5628
|
-
[
|
|
5275
|
+
[
|
|
5276
|
+
"Integrations:",
|
|
5277
|
+
["serve", "claude", "codex", "github", "rules", "learn", "eval", "telemetry", "collector"]
|
|
5278
|
+
]
|
|
5629
5279
|
];
|
|
5630
5280
|
function groupHelp(program) {
|
|
5631
5281
|
const groupOf = new Map(
|
|
5632
|
-
HELP_GROUPS.flatMap(([group, names]) => names.map((
|
|
5282
|
+
HELP_GROUPS.flatMap(([group, names]) => names.map((name2) => [name2, group]))
|
|
5633
5283
|
);
|
|
5634
5284
|
for (const command of program.commands) {
|
|
5635
5285
|
command.helpGroup(groupOf.get(command.name()) ?? "Other:");
|
|
5636
5286
|
}
|
|
5637
5287
|
const position = new Map(
|
|
5638
|
-
HELP_GROUPS.flatMap(([, names]) => names).map((
|
|
5288
|
+
HELP_GROUPS.flatMap(([, names]) => names).map((name2, index) => [name2, index])
|
|
5639
5289
|
);
|
|
5640
5290
|
program.commands.sort(
|
|
5641
5291
|
(a, b) => (position.get(a.name()) ?? 999) - (position.get(b.name()) ?? 999)
|
|
@@ -5665,7 +5315,7 @@ async function runSafely(fn) {
|
|
|
5665
5315
|
try {
|
|
5666
5316
|
await fn();
|
|
5667
5317
|
} catch (error) {
|
|
5668
|
-
console.error(
|
|
5318
|
+
console.error(pc23.red(String(redactSecrets(error.message))));
|
|
5669
5319
|
process.exitCode = 1;
|
|
5670
5320
|
}
|
|
5671
5321
|
}
|
|
@@ -5679,9 +5329,9 @@ function parseDuration(value) {
|
|
|
5679
5329
|
return amount * 60 * 1e3;
|
|
5680
5330
|
}
|
|
5681
5331
|
function printDoctor(report) {
|
|
5682
|
-
const ok2 =
|
|
5683
|
-
const bad =
|
|
5684
|
-
const warn2 =
|
|
5332
|
+
const ok2 = pc23.green("ok");
|
|
5333
|
+
const bad = pc23.red("fail");
|
|
5334
|
+
const warn2 = pc23.yellow("warn");
|
|
5685
5335
|
console.log(`scaffold root: ${report.scaffold.codebuddyDir.ok ? ok2 : bad}`);
|
|
5686
5336
|
if (report.scaffold.codebuddyDir.warning)
|
|
5687
5337
|
console.log(` ${report.scaffold.codebuddyDir.warning}`);
|
|
@@ -5707,7 +5357,7 @@ function printDoctor(report) {
|
|
|
5707
5357
|
);
|
|
5708
5358
|
console.log(`estimated daily cap remaining: ${report.usage.estimatedDailyCapRemaining}`);
|
|
5709
5359
|
if (report.usage.warning80Percent)
|
|
5710
|
-
console.log(
|
|
5360
|
+
console.log(pc23.yellow("usage warning: daily cap is over 80% used"));
|
|
5711
5361
|
console.log(
|
|
5712
5362
|
`config permissions: ${report.config.exists ? report.config.mode : "missing"} ${report.config.ok ? ok2 : warn2}`
|
|
5713
5363
|
);
|
|
@@ -5717,9 +5367,9 @@ function printDoctor(report) {
|
|
|
5717
5367
|
}
|
|
5718
5368
|
}
|
|
5719
5369
|
function printDbDoctor(report) {
|
|
5720
|
-
const ok2 =
|
|
5721
|
-
const bad =
|
|
5722
|
-
const warn2 =
|
|
5370
|
+
const ok2 = pc23.green("ok");
|
|
5371
|
+
const bad = pc23.red("fail");
|
|
5372
|
+
const warn2 = pc23.yellow("warn");
|
|
5723
5373
|
console.log(`DB connectivity: ${report.db.ok ? ok2 : bad}`);
|
|
5724
5374
|
if (report.db.postgresUrlSource) console.log(` url source: ${report.db.postgresUrlSource}`);
|
|
5725
5375
|
if (report.db.error) console.log(` ${report.db.error}`);
|
|
@@ -5735,4 +5385,4 @@ function printDbDoctor(report) {
|
|
|
5735
5385
|
export {
|
|
5736
5386
|
createCli
|
|
5737
5387
|
};
|
|
5738
|
-
//# sourceMappingURL=main-
|
|
5388
|
+
//# sourceMappingURL=main-YJKSYFPT.js.map
|