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