@orquentra/codebuddy 2.5.0 → 2.6.2
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/CLAUDE_CODEX.md +6 -4
- package/README.md +8 -0
- package/WORKFLOW.md +5 -5
- package/dist/{chunk-JDZ3VK26.js → chunk-2XDPKFUW.js} +114 -823
- package/dist/chunk-2XDPKFUW.js.map +1 -0
- package/dist/{chunk-QDBDYCM2.js → chunk-5FW3PMNP.js} +2 -2
- package/dist/{chunk-776EQXDV.js → chunk-5KZY6OTG.js} +82 -5
- package/dist/chunk-5KZY6OTG.js.map +1 -0
- package/dist/{chunk-UFZD3JAA.js → chunk-5MQOE4J6.js} +3 -3
- package/dist/chunk-5UH3BEQR.js +32 -0
- package/dist/chunk-5UH3BEQR.js.map +1 -0
- package/dist/{chunk-ROQTT5ZO.js → chunk-B6CO63SQ.js} +8 -7
- package/dist/chunk-B6CO63SQ.js.map +1 -0
- package/dist/chunk-CSRD5OQA.js +79 -0
- package/dist/chunk-CSRD5OQA.js.map +1 -0
- package/dist/{chunk-PYLH4MH5.js → chunk-FJOTP632.js} +1 -1
- package/dist/{chunk-3XYQGFPB.js → chunk-IBLJOHJL.js} +16 -3
- package/dist/chunk-IBLJOHJL.js.map +1 -0
- package/dist/chunk-IMFUYEZA.js +1607 -0
- package/dist/chunk-IMFUYEZA.js.map +1 -0
- package/dist/{chunk-CQPCHMBS.js → chunk-J2EAZ4MA.js} +21 -8
- package/dist/chunk-J2EAZ4MA.js.map +1 -0
- package/dist/{chunk-KYK6RPAJ.js → chunk-JFDRBPYF.js} +2 -2
- package/dist/{chunk-JMWJ7V6Y.js → chunk-JPHJBCZB.js} +2 -2
- package/dist/{chunk-JMWJ7V6Y.js.map → chunk-JPHJBCZB.js.map} +1 -1
- package/dist/{chunk-L5AYZC3F.js → chunk-NERKZSSE.js} +7 -7
- package/dist/{chunk-PCE4STAY.js → chunk-OWWCDRXW.js} +5 -5
- package/dist/{chunk-WHZSVY2L.js → chunk-QBDJXGG5.js} +2 -2
- package/dist/{chunk-Z5QB367W.js → chunk-R7CD6EXS.js} +367 -1798
- package/dist/chunk-R7CD6EXS.js.map +1 -0
- package/dist/{chunk-6MKEIDX5.js → chunk-RAJJSCIP.js} +2 -2
- package/dist/chunk-S6VEG4S2.js +991 -0
- package/dist/chunk-S6VEG4S2.js.map +1 -0
- package/dist/{chunk-4BYGTEJT.js → chunk-XBJQKCLU.js} +2 -2
- package/dist/cli/index.js +14 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/{cloud-GP6G3EAK.js → cloud-7WM3YLI4.js} +3 -3
- package/dist/{codebuddy-DM-i0Fu5.d.ts → codebuddy-B_pCaQrV.d.ts} +22 -2
- package/dist/{config-KFEHLV6W.js → config-DEVHUDIU.js} +3 -3
- package/dist/docker-autostart-KDME7TVC.js +12 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +22 -18
- package/dist/langgraph/index.d.ts +1 -1
- package/dist/lean-command-HTQCPH76.js +15 -0
- package/dist/local-5MZURLCZ.js +17 -0
- package/dist/{main-BDVQAGAS.js → main-MTBQZGJV.js} +333 -219
- package/dist/main-MTBQZGJV.js.map +1 -0
- package/dist/{memory-4RGVLPKG.js → memory-KIC5FJVW.js} +4 -4
- package/dist/{operations-EQGOFCBD.js → operations-POCHY3WR.js} +8 -7
- package/dist/postgres-docker-QQZQ2ABK.js +16 -0
- package/dist/{pretool-command-MQQ2UNQM.js → pretool-command-AX6JXP25.js} +3 -3
- package/dist/{rerank-3P3C2UHN.js → rerank-5QZ3PNUW.js} +4 -4
- package/dist/runtime-CUBOOPZO.js +43 -0
- package/dist/runtime-CUBOOPZO.js.map +1 -0
- package/dist/stdio-HOV4P3DQ.js +12 -0
- package/dist/stdio-HOV4P3DQ.js.map +1 -0
- package/dist/{summaries-ZBV5IN7Y.js → summaries-SB5L2TSW.js} +6 -6
- package/dist/summaries-SB5L2TSW.js.map +1 -0
- package/dist/{zvec-MAPSJQSR.js → zvec-MSRKT7IV.js} +3 -3
- package/dist/zvec-MSRKT7IV.js.map +1 -0
- package/package.json +7 -10
- package/dist/chunk-3XYQGFPB.js.map +0 -1
- package/dist/chunk-776EQXDV.js.map +0 -1
- package/dist/chunk-CQPCHMBS.js.map +0 -1
- package/dist/chunk-JDZ3VK26.js.map +0 -1
- package/dist/chunk-ROQTT5ZO.js.map +0 -1
- package/dist/chunk-Z5QB367W.js.map +0 -1
- package/dist/lean-command-DA4ZNKG3.js +0 -15
- package/dist/local-3NGNESNB.js +0 -13
- package/dist/main-BDVQAGAS.js.map +0 -1
- /package/dist/{chunk-QDBDYCM2.js.map → chunk-5FW3PMNP.js.map} +0 -0
- /package/dist/{chunk-UFZD3JAA.js.map → chunk-5MQOE4J6.js.map} +0 -0
- /package/dist/{chunk-PYLH4MH5.js.map → chunk-FJOTP632.js.map} +0 -0
- /package/dist/{chunk-KYK6RPAJ.js.map → chunk-JFDRBPYF.js.map} +0 -0
- /package/dist/{chunk-L5AYZC3F.js.map → chunk-NERKZSSE.js.map} +0 -0
- /package/dist/{chunk-PCE4STAY.js.map → chunk-OWWCDRXW.js.map} +0 -0
- /package/dist/{chunk-WHZSVY2L.js.map → chunk-QBDJXGG5.js.map} +0 -0
- /package/dist/{chunk-6MKEIDX5.js.map → chunk-RAJJSCIP.js.map} +0 -0
- /package/dist/{chunk-4BYGTEJT.js.map → chunk-XBJQKCLU.js.map} +0 -0
- /package/dist/{cloud-GP6G3EAK.js.map → cloud-7WM3YLI4.js.map} +0 -0
- /package/dist/{config-KFEHLV6W.js.map → config-DEVHUDIU.js.map} +0 -0
- /package/dist/{lean-command-DA4ZNKG3.js.map → docker-autostart-KDME7TVC.js.map} +0 -0
- /package/dist/{local-3NGNESNB.js.map → lean-command-HTQCPH76.js.map} +0 -0
- /package/dist/{operations-EQGOFCBD.js.map → local-5MZURLCZ.js.map} +0 -0
- /package/dist/{memory-4RGVLPKG.js.map → memory-KIC5FJVW.js.map} +0 -0
- /package/dist/{pretool-command-MQQ2UNQM.js.map → operations-POCHY3WR.js.map} +0 -0
- /package/dist/{summaries-ZBV5IN7Y.js.map → postgres-docker-QQZQ2ABK.js.map} +0 -0
- /package/dist/{zvec-MAPSJQSR.js.map → pretool-command-AX6JXP25.js.map} +0 -0
- /package/dist/{rerank-3P3C2UHN.js.map → rerank-5QZ3PNUW.js.map} +0 -0
|
@@ -0,0 +1,1607 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PlanFileStore,
|
|
3
|
+
PlanLifecycle,
|
|
4
|
+
assessRisk,
|
|
5
|
+
buildBeforeEditContext,
|
|
6
|
+
buildBootstrapContext,
|
|
7
|
+
captureAfterTurn,
|
|
8
|
+
configuredReranker,
|
|
9
|
+
enforceContextBudget,
|
|
10
|
+
llmMemoryOptions,
|
|
11
|
+
readPolicyFile,
|
|
12
|
+
recordTrace,
|
|
13
|
+
resolveRiskPaths
|
|
14
|
+
} from "./chunk-R7CD6EXS.js";
|
|
15
|
+
import {
|
|
16
|
+
SessionCapsuleLedger,
|
|
17
|
+
estimateTokens
|
|
18
|
+
} from "./chunk-J2EAZ4MA.js";
|
|
19
|
+
import {
|
|
20
|
+
buildArchitectureMap,
|
|
21
|
+
generateGraphifyGraph,
|
|
22
|
+
indexRepository,
|
|
23
|
+
loadArchitectureMap,
|
|
24
|
+
loadFreshIndex,
|
|
25
|
+
neighbours,
|
|
26
|
+
queryMap
|
|
27
|
+
} from "./chunk-NERKZSSE.js";
|
|
28
|
+
import {
|
|
29
|
+
codeBuddyConfigSchema,
|
|
30
|
+
createRuntime,
|
|
31
|
+
listFactsPage
|
|
32
|
+
} from "./chunk-2XDPKFUW.js";
|
|
33
|
+
import {
|
|
34
|
+
MemoryFileStore,
|
|
35
|
+
VERSION
|
|
36
|
+
} from "./chunk-S6VEG4S2.js";
|
|
37
|
+
import {
|
|
38
|
+
redactPersisted
|
|
39
|
+
} from "./chunk-GDJH7XJC.js";
|
|
40
|
+
import {
|
|
41
|
+
refreshZvecIndex
|
|
42
|
+
} from "./chunk-5FW3PMNP.js";
|
|
43
|
+
import {
|
|
44
|
+
configPath,
|
|
45
|
+
loadPlanPolicy,
|
|
46
|
+
loadRuntimeConfig,
|
|
47
|
+
redactSecrets
|
|
48
|
+
} from "./chunk-5KZY6OTG.js";
|
|
49
|
+
|
|
50
|
+
// src/core/migrate-to-files.ts
|
|
51
|
+
import { createHash } from "crypto";
|
|
52
|
+
async function migrateMemoryToFiles(input) {
|
|
53
|
+
const namespace = await input.repository.getNamespaceByName(input.namespace);
|
|
54
|
+
if (!namespace) {
|
|
55
|
+
return {
|
|
56
|
+
namespace: input.namespace,
|
|
57
|
+
dryRun: !input.write,
|
|
58
|
+
facts: { found: 0, existing: 0, toWrite: 0, conflicts: 0, written: 0 },
|
|
59
|
+
summaries: { found: 0, existing: 0, toWrite: 0, conflicts: 0, written: 0 },
|
|
60
|
+
conflicts: [],
|
|
61
|
+
failures: []
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const [facts, summaries, existingFacts, existingSummaries] = await Promise.all([
|
|
65
|
+
input.repository.listFacts({ namespaceId: namespace.id, limit: 1e5 }),
|
|
66
|
+
input.repository.listSummaries({ namespaceId: namespace.id, limit: 1e5 }),
|
|
67
|
+
input.store.listFacts(),
|
|
68
|
+
input.store.listSummaries()
|
|
69
|
+
]);
|
|
70
|
+
const warnings = facts.length >= 1e5 || summaries.length >= 1e5 ? [
|
|
71
|
+
"namespace is at or above the 100,000-row read cap; the migration may be partial \u2014 prune or export in batches"
|
|
72
|
+
] : [];
|
|
73
|
+
const existingFactsById = new Map(existingFacts.map((fact) => [fact.id, fact]));
|
|
74
|
+
const existingSummariesById = new Map(existingSummaries.map((summary) => [summary.id, summary]));
|
|
75
|
+
const conflicts = [];
|
|
76
|
+
const failures = [];
|
|
77
|
+
const factsToWrite = facts.filter((fact) => {
|
|
78
|
+
const existing = existingFactsById.get(fact.id);
|
|
79
|
+
if (!existing) return true;
|
|
80
|
+
const databaseChecksum = checksum(redactPersisted(fact).content);
|
|
81
|
+
const fileChecksum = checksum(existing.content);
|
|
82
|
+
if (databaseChecksum !== fileChecksum) {
|
|
83
|
+
conflicts.push({ kind: "fact", id: fact.id, databaseChecksum, fileChecksum });
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
});
|
|
87
|
+
const summariesToWrite = summaries.filter((summary) => {
|
|
88
|
+
const existing = existingSummariesById.get(summary.id);
|
|
89
|
+
if (!existing) return true;
|
|
90
|
+
const databaseChecksum = checksum(summary.content);
|
|
91
|
+
const fileChecksum = checksum(existing.content);
|
|
92
|
+
if (databaseChecksum !== fileChecksum) {
|
|
93
|
+
conflicts.push({ kind: "summary", id: summary.id, databaseChecksum, fileChecksum });
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
});
|
|
97
|
+
let factsWritten = 0;
|
|
98
|
+
let summariesWritten = 0;
|
|
99
|
+
if (input.write) {
|
|
100
|
+
for (const fact of factsToWrite) {
|
|
101
|
+
try {
|
|
102
|
+
await input.store.writeFact({
|
|
103
|
+
id: fact.id,
|
|
104
|
+
namespace: input.namespace,
|
|
105
|
+
subject: fact.subject,
|
|
106
|
+
predicate: fact.predicate,
|
|
107
|
+
object: fact.object,
|
|
108
|
+
confidence: fact.confidence,
|
|
109
|
+
createdAt: fact.createdAt.toISOString(),
|
|
110
|
+
createdByAgent: fact.createdByAgent,
|
|
111
|
+
sourceInteractionId: fact.sourceInteractionId,
|
|
112
|
+
sourceDeleted: fact.sourceDeleted,
|
|
113
|
+
supersededBy: fact.supersededBy,
|
|
114
|
+
content: fact.content
|
|
115
|
+
});
|
|
116
|
+
factsWritten += 1;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
failures.push({ kind: "fact", id: fact.id, error: error.message });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
for (const summary of summariesToWrite) {
|
|
122
|
+
try {
|
|
123
|
+
await input.store.writeSummary({
|
|
124
|
+
id: summary.id,
|
|
125
|
+
namespace: input.namespace,
|
|
126
|
+
sessionId: summary.sessionId,
|
|
127
|
+
version: summary.version,
|
|
128
|
+
tokenCount: summary.tokenCount,
|
|
129
|
+
createdAt: summary.createdAt.toISOString(),
|
|
130
|
+
createdByAgent: summary.createdByAgent,
|
|
131
|
+
content: summary.content
|
|
132
|
+
});
|
|
133
|
+
summariesWritten += 1;
|
|
134
|
+
} catch (error) {
|
|
135
|
+
failures.push({ kind: "summary", id: summary.id, error: error.message });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const factConflicts = conflicts.filter((conflict) => conflict.kind === "fact").length;
|
|
140
|
+
const summaryConflicts = conflicts.length - factConflicts;
|
|
141
|
+
return {
|
|
142
|
+
namespace: input.namespace,
|
|
143
|
+
dryRun: !input.write,
|
|
144
|
+
...warnings.length > 0 ? { warnings } : {},
|
|
145
|
+
facts: {
|
|
146
|
+
found: facts.length,
|
|
147
|
+
existing: facts.length - factsToWrite.length - factConflicts,
|
|
148
|
+
toWrite: factsToWrite.length,
|
|
149
|
+
conflicts: factConflicts,
|
|
150
|
+
written: factsWritten
|
|
151
|
+
},
|
|
152
|
+
summaries: {
|
|
153
|
+
found: summaries.length,
|
|
154
|
+
existing: summaries.length - summariesToWrite.length - summaryConflicts,
|
|
155
|
+
toWrite: summariesToWrite.length,
|
|
156
|
+
conflicts: summaryConflicts,
|
|
157
|
+
written: summariesWritten
|
|
158
|
+
},
|
|
159
|
+
conflicts,
|
|
160
|
+
failures
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
function checksum(content) {
|
|
164
|
+
return createHash("sha256").update(content).digest("hex");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// src/mcp/tools/index.ts
|
|
168
|
+
import { z } from "zod";
|
|
169
|
+
|
|
170
|
+
// src/context/delivery.ts
|
|
171
|
+
import { createHash as createHash2 } from "crypto";
|
|
172
|
+
var DELIVERY_VERSION = 1;
|
|
173
|
+
var OMITTABLE_SECTIONS = [
|
|
174
|
+
"plan",
|
|
175
|
+
"policyRules",
|
|
176
|
+
"incidents",
|
|
177
|
+
"facts",
|
|
178
|
+
"rules",
|
|
179
|
+
"handoffs",
|
|
180
|
+
"architecture",
|
|
181
|
+
"symbols",
|
|
182
|
+
"neighbours"
|
|
183
|
+
];
|
|
184
|
+
var SESSION_TTL_MS = 30 * 60 * 1e3;
|
|
185
|
+
async function applySessionDelivery(repositoryRoot, payload, options) {
|
|
186
|
+
let output = { ...payload };
|
|
187
|
+
if (!options.sessionId) return output;
|
|
188
|
+
const sections = OMITTABLE_SECTIONS.filter((key) => output[key] !== void 0).map((key) => ({
|
|
189
|
+
key: `mcp:${key}`,
|
|
190
|
+
hash: createHash2("sha256").update(JSON.stringify(output[key])).digest("hex")
|
|
191
|
+
}));
|
|
192
|
+
try {
|
|
193
|
+
await new SessionCapsuleLedger(repositoryRoot).exchange(options.sessionId, sections, {
|
|
194
|
+
...options.ack !== void 0 ? { ack: options.ack } : {},
|
|
195
|
+
ttlMs: SESSION_TTL_MS,
|
|
196
|
+
...options.full ? { full: true } : {},
|
|
197
|
+
prepare: (exchange) => {
|
|
198
|
+
const unchanged = exchange.unchanged.map((key) => key.slice("mcp:".length));
|
|
199
|
+
let avoidedTokens = 0;
|
|
200
|
+
for (const key of unchanged) {
|
|
201
|
+
avoidedTokens += estimateTokens(output[key]);
|
|
202
|
+
delete output[key];
|
|
203
|
+
}
|
|
204
|
+
const delivery = {
|
|
205
|
+
version: DELIVERY_VERSION,
|
|
206
|
+
sessionId: options.sessionId,
|
|
207
|
+
deliveryId: exchange.deliveryId,
|
|
208
|
+
ack: exchange.ack,
|
|
209
|
+
unchanged,
|
|
210
|
+
avoidedTokens,
|
|
211
|
+
note: "Pass deliveryId as ack. Use full=true after losing context."
|
|
212
|
+
};
|
|
213
|
+
output.delivery = delivery;
|
|
214
|
+
output = enforceContextBudget(output);
|
|
215
|
+
return OMITTABLE_SECTIONS.filter((key) => output[key] !== void 0).map((key) => ({
|
|
216
|
+
key: `mcp:${key}`,
|
|
217
|
+
hash: createHash2("sha256").update(JSON.stringify(output[key])).digest("hex")
|
|
218
|
+
}));
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
return output;
|
|
222
|
+
} catch {
|
|
223
|
+
return enforceContextBudget({ ...payload });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// src/suggest/engine.ts
|
|
228
|
+
import { constants } from "fs";
|
|
229
|
+
import { access } from "fs/promises";
|
|
230
|
+
import { extname, join, resolve } from "path";
|
|
231
|
+
|
|
232
|
+
// src/suggest/types.ts
|
|
233
|
+
var SEVERITY_RANK = {
|
|
234
|
+
high: 3,
|
|
235
|
+
medium: 2,
|
|
236
|
+
low: 1,
|
|
237
|
+
info: 0
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
// src/suggest/engine.ts
|
|
241
|
+
var RISK_SUGGEST_THRESHOLD = 50;
|
|
242
|
+
var BLAST_RADIUS_MEDIUM = 5;
|
|
243
|
+
var BLAST_RADIUS_HIGH = 10;
|
|
244
|
+
var CODE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
245
|
+
async function generateSuggestions(input) {
|
|
246
|
+
const repositoryRoot = resolve(input.repositoryRoot ?? process.cwd());
|
|
247
|
+
const namespace = input.namespace;
|
|
248
|
+
const targetPaths = await resolveRiskPaths({
|
|
249
|
+
repositoryRoot,
|
|
250
|
+
...input.paths !== void 0 ? { paths: input.paths } : {},
|
|
251
|
+
...input.planId !== void 0 ? { planId: input.planId } : {},
|
|
252
|
+
...input.useGit !== void 0 ? { useGit: input.useGit } : {}
|
|
253
|
+
});
|
|
254
|
+
const planStore = new PlanFileStore(repositoryRoot);
|
|
255
|
+
const memoryStore = new MemoryFileStore(repositoryRoot);
|
|
256
|
+
const [risks, plan, incidents, map, policyFile] = await Promise.all([
|
|
257
|
+
assessRisk({
|
|
258
|
+
repositoryRoot,
|
|
259
|
+
namespace,
|
|
260
|
+
...input.paths !== void 0 ? { paths: input.paths } : {},
|
|
261
|
+
...input.planId !== void 0 ? { planId: input.planId } : {},
|
|
262
|
+
...input.useGit !== void 0 ? { useGit: input.useGit } : {}
|
|
263
|
+
}),
|
|
264
|
+
resolveRelevantPlan(planStore, namespace, input.planId),
|
|
265
|
+
memoryStore.findIncidentFactsForPaths({ namespace, paths: targetPaths }),
|
|
266
|
+
buildArchitectureMap(repositoryRoot),
|
|
267
|
+
readPolicyFile(join(repositoryRoot, ".codebuddy", "policies.yaml"))
|
|
268
|
+
]);
|
|
269
|
+
const suggestions = [];
|
|
270
|
+
for (const item of risks.items) {
|
|
271
|
+
if (item.score < RISK_SUGGEST_THRESHOLD) continue;
|
|
272
|
+
suggestions.push({
|
|
273
|
+
id: `risk:${item.path}`,
|
|
274
|
+
category: "risk",
|
|
275
|
+
severity: item.score >= 75 ? "high" : "medium",
|
|
276
|
+
title: `Review high-risk change to ${item.path}`,
|
|
277
|
+
detail: item.reason,
|
|
278
|
+
path: item.path,
|
|
279
|
+
evidence: [
|
|
280
|
+
{ kind: "risk_score", detail: `score ${item.score} (${item.category})` },
|
|
281
|
+
...item.evidence.length > 0 ? [{ kind: "risk_evidence", detail: `${item.evidence.length} evidence item(s)` }] : []
|
|
282
|
+
],
|
|
283
|
+
commandHint: `codebuddy risk assess --paths ${item.path} --explain`
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
for (const incident of incidents) {
|
|
287
|
+
const path = incident.paths[0] ?? "(unknown)";
|
|
288
|
+
suggestions.push({
|
|
289
|
+
id: `incident:${path}`,
|
|
290
|
+
category: "incident",
|
|
291
|
+
severity: "high",
|
|
292
|
+
title: `Recurring incident hotspot: ${path}`,
|
|
293
|
+
detail: `${incident.subject} \u2014 proceed carefully; this area has an unresolved incident.`,
|
|
294
|
+
path,
|
|
295
|
+
evidence: [{ kind: "incident", detail: `${incident.id} severity=${incident.severity}` }]
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
const inbound = inboundDegrees(map);
|
|
299
|
+
for (const path of targetPaths) {
|
|
300
|
+
const dependents = inbound.get(path) ?? 0;
|
|
301
|
+
if (dependents < BLAST_RADIUS_MEDIUM) continue;
|
|
302
|
+
suggestions.push({
|
|
303
|
+
id: `architecture:${path}`,
|
|
304
|
+
category: "architecture",
|
|
305
|
+
severity: dependents >= BLAST_RADIUS_HIGH ? "high" : "medium",
|
|
306
|
+
title: `High blast radius: ${path}`,
|
|
307
|
+
detail: `${dependents} module(s) import ${path}; a change here ripples widely.`,
|
|
308
|
+
path,
|
|
309
|
+
evidence: [{ kind: "dependents", detail: `${dependents} inbound import(s)` }],
|
|
310
|
+
commandHint: `codebuddy map dependents ${path}`
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
if (plan) {
|
|
314
|
+
const planned = new Set(plan.filesToTouch.map((file) => normalize(file.path)));
|
|
315
|
+
const strays = targetPaths.filter((path) => !planned.has(normalize(path)));
|
|
316
|
+
if (planned.size > 0 && strays.length > 0) {
|
|
317
|
+
suggestions.push({
|
|
318
|
+
id: `plan_divergence:${plan.id}`,
|
|
319
|
+
category: "plan_divergence",
|
|
320
|
+
severity: "medium",
|
|
321
|
+
title: `Change set diverges from plan ${plan.id}`,
|
|
322
|
+
detail: `${strays.length} target file(s) are outside the active plan's scope: ${strays.slice(0, 5).join(", ")}.`,
|
|
323
|
+
evidence: [
|
|
324
|
+
{ kind: "plan", detail: `plan ${plan.id} (${plan.status})` },
|
|
325
|
+
{ kind: "out_of_scope", detail: strays.slice(0, 5).join(", ") }
|
|
326
|
+
],
|
|
327
|
+
commandHint: `codebuddy plan show ${plan.id}`
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
for (const path of targetPaths) {
|
|
332
|
+
if (!isTestableCode(path)) continue;
|
|
333
|
+
if (await hasSiblingTest(repositoryRoot, path)) continue;
|
|
334
|
+
suggestions.push({
|
|
335
|
+
id: `missing_test:${path}`,
|
|
336
|
+
category: "missing_test",
|
|
337
|
+
severity: "low",
|
|
338
|
+
title: `No test found for ${path}`,
|
|
339
|
+
detail: "Consider adding a test alongside this change.",
|
|
340
|
+
path,
|
|
341
|
+
evidence: [{ kind: "no_test", detail: "no *.test.* / *.spec.* sibling found" }]
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
for (const rule of policyFile.rules) {
|
|
345
|
+
const prefix = literalPrefix(rule.pattern);
|
|
346
|
+
if (!prefix) continue;
|
|
347
|
+
if (await pathExists(join(repositoryRoot, prefix))) continue;
|
|
348
|
+
suggestions.push({
|
|
349
|
+
id: `stale_policy:${rule.id}`,
|
|
350
|
+
category: "stale_policy",
|
|
351
|
+
severity: "low",
|
|
352
|
+
title: `Stale policy rule "${rule.id}"`,
|
|
353
|
+
detail: `Pattern "${rule.pattern}" targets "${prefix}", which does not exist.`,
|
|
354
|
+
evidence: [{ kind: "policy_rule", detail: `${rule.id} \u2192 ${rule.pattern}` }]
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
const deduped = dedupe(suggestions).sort(compareSuggestions);
|
|
358
|
+
const limited = input.limit ? deduped.slice(0, input.limit) : deduped;
|
|
359
|
+
return {
|
|
360
|
+
suggestions: limited,
|
|
361
|
+
stats: buildStats(targetPaths.length, limited)
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
async function resolveRelevantPlan(store, namespace, planId) {
|
|
365
|
+
if (planId) {
|
|
366
|
+
try {
|
|
367
|
+
return await store.readPlan(planId);
|
|
368
|
+
} catch (error) {
|
|
369
|
+
if (error.code === "ENOENT") return null;
|
|
370
|
+
throw error;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return new PlanLifecycle(store).current(namespace);
|
|
374
|
+
}
|
|
375
|
+
function inboundDegrees(map) {
|
|
376
|
+
const inbound = /* @__PURE__ */ new Map();
|
|
377
|
+
for (const edge of map.edges) inbound.set(edge.to, (inbound.get(edge.to) ?? 0) + 1);
|
|
378
|
+
return inbound;
|
|
379
|
+
}
|
|
380
|
+
function isTestableCode(path) {
|
|
381
|
+
if (/\.(test|spec)\.[cm]?[jt]sx?$/.test(path)) return false;
|
|
382
|
+
if (path.endsWith(".d.ts")) return false;
|
|
383
|
+
if (path.includes("__tests__/")) return false;
|
|
384
|
+
return CODE_EXTENSIONS.has(extname(path).toLowerCase());
|
|
385
|
+
}
|
|
386
|
+
async function hasSiblingTest(repositoryRoot, path) {
|
|
387
|
+
const ext = extname(path);
|
|
388
|
+
const base = path.slice(0, path.length - ext.length);
|
|
389
|
+
const dir = base.includes("/") ? base.slice(0, base.lastIndexOf("/")) : "";
|
|
390
|
+
const name = base.includes("/") ? base.slice(base.lastIndexOf("/") + 1) : base;
|
|
391
|
+
const candidates = [
|
|
392
|
+
`${base}.test${ext}`,
|
|
393
|
+
`${base}.spec${ext}`,
|
|
394
|
+
`${dir ? `${dir}/` : ""}__tests__/${name}${ext}`,
|
|
395
|
+
`${dir ? `${dir}/` : ""}__tests__/${name}.test${ext}`
|
|
396
|
+
];
|
|
397
|
+
for (const candidate of candidates) {
|
|
398
|
+
if (await pathExists(join(repositoryRoot, candidate))) return true;
|
|
399
|
+
}
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
function literalPrefix(pattern) {
|
|
403
|
+
const normalized = normalize(pattern);
|
|
404
|
+
const wildcard = normalized.search(/[*?[]/);
|
|
405
|
+
if (wildcard === -1) return normalized.replace(/\/+$/, "");
|
|
406
|
+
const beforeWildcard = normalized.slice(0, wildcard);
|
|
407
|
+
const lastSlash = beforeWildcard.lastIndexOf("/");
|
|
408
|
+
return lastSlash === -1 ? "" : beforeWildcard.slice(0, lastSlash);
|
|
409
|
+
}
|
|
410
|
+
function dedupe(suggestions) {
|
|
411
|
+
const byId = /* @__PURE__ */ new Map();
|
|
412
|
+
for (const suggestion of suggestions) {
|
|
413
|
+
const existing = byId.get(suggestion.id);
|
|
414
|
+
if (!existing || SEVERITY_RANK[suggestion.severity] > SEVERITY_RANK[existing.severity]) {
|
|
415
|
+
byId.set(suggestion.id, suggestion);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return [...byId.values()];
|
|
419
|
+
}
|
|
420
|
+
function compareSuggestions(left, right) {
|
|
421
|
+
const severity = SEVERITY_RANK[right.severity] - SEVERITY_RANK[left.severity];
|
|
422
|
+
if (severity !== 0) return severity;
|
|
423
|
+
if (left.category !== right.category) return left.category.localeCompare(right.category);
|
|
424
|
+
return (left.path ?? "").localeCompare(right.path ?? "");
|
|
425
|
+
}
|
|
426
|
+
function buildStats(pathsConsidered, suggestions) {
|
|
427
|
+
const byCategory = {};
|
|
428
|
+
const bySeverity = {};
|
|
429
|
+
for (const suggestion of suggestions) {
|
|
430
|
+
byCategory[suggestion.category] = (byCategory[suggestion.category] ?? 0) + 1;
|
|
431
|
+
bySeverity[suggestion.severity] = (bySeverity[suggestion.severity] ?? 0) + 1;
|
|
432
|
+
}
|
|
433
|
+
return { pathsConsidered, produced: suggestions.length, byCategory, bySeverity };
|
|
434
|
+
}
|
|
435
|
+
async function pathExists(path) {
|
|
436
|
+
try {
|
|
437
|
+
await access(path, constants.F_OK);
|
|
438
|
+
return true;
|
|
439
|
+
} catch {
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function normalize(path) {
|
|
444
|
+
return path.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// src/mcp/tool-result.ts
|
|
448
|
+
function serializeToolResult(value, format = process.env.CODEBUDDY_RESPONSE_FORMAT ?? "both") {
|
|
449
|
+
if (format !== "text" && format !== "both") {
|
|
450
|
+
throw new Error('CODEBUDDY_RESPONSE_FORMAT must be "text" or "both"');
|
|
451
|
+
}
|
|
452
|
+
return {
|
|
453
|
+
content: [{ type: "text", text: JSON.stringify(value) }],
|
|
454
|
+
...format === "both" && value !== null && typeof value === "object" && !Array.isArray(value) ? { structuredContent: value } : {}
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// src/mcp/tools/index.ts
|
|
459
|
+
var INPUT_LIMITS = {
|
|
460
|
+
contentChars: 32e3,
|
|
461
|
+
batchItems: 100,
|
|
462
|
+
summaryChars: 32e3,
|
|
463
|
+
planBodyChars: 64e3,
|
|
464
|
+
planBriefChars: 4e3,
|
|
465
|
+
titleChars: 300,
|
|
466
|
+
arrayItems: 100,
|
|
467
|
+
pathChars: 1024,
|
|
468
|
+
taskChars: 2e3,
|
|
469
|
+
agentIdChars: 200
|
|
470
|
+
};
|
|
471
|
+
var deliverySchema = {
|
|
472
|
+
sessionId: z.string().min(1).max(200).optional(),
|
|
473
|
+
ack: z.string().regex(/^dlv_[a-z0-9]+$/).optional(),
|
|
474
|
+
full: z.boolean().optional()
|
|
475
|
+
};
|
|
476
|
+
var rememberSchema = {
|
|
477
|
+
sessionId: z.string().min(1).max(200).optional(),
|
|
478
|
+
content: z.string().min(1).max(INPUT_LIMITS.contentChars),
|
|
479
|
+
type: z.enum(["interaction", "fact", "summary"]).optional(),
|
|
480
|
+
idempotencyKey: z.string().min(1).max(200).optional(),
|
|
481
|
+
agentId: z.string().min(1).max(INPUT_LIMITS.agentIdChars).optional()
|
|
482
|
+
};
|
|
483
|
+
var recallSchema = {
|
|
484
|
+
sessionId: z.string().min(1).max(200),
|
|
485
|
+
query: z.string().min(1).max(INPUT_LIMITS.taskChars),
|
|
486
|
+
budget: z.number().int().positive().optional(),
|
|
487
|
+
callerModel: z.string().min(1).max(200).optional(),
|
|
488
|
+
conflictMode: z.enum(["all", "latest", "highest_confidence"]).optional()
|
|
489
|
+
};
|
|
490
|
+
var shareSchema = {
|
|
491
|
+
from: z.string().min(1).max(INPUT_LIMITS.agentIdChars).optional(),
|
|
492
|
+
to_namespace: z.string().min(1).max(INPUT_LIMITS.agentIdChars),
|
|
493
|
+
factIds: z.array(z.string().min(1).max(100)).min(1).max(INPUT_LIMITS.arrayItems),
|
|
494
|
+
mode: z.enum(["reference", "snapshot"]).optional(),
|
|
495
|
+
/** Snapshot mode: the target project's root; its Markdown store receives the copy. */
|
|
496
|
+
to_project_root: z.string().min(1).max(INPUT_LIMITS.pathChars).optional(),
|
|
497
|
+
agentId: z.string().min(1).max(INPUT_LIMITS.agentIdChars).optional()
|
|
498
|
+
};
|
|
499
|
+
var unshareSchema = {
|
|
500
|
+
to_namespace: z.string().min(1).max(INPUT_LIMITS.agentIdChars),
|
|
501
|
+
factIds: z.array(z.string().min(1).max(100)).min(1).max(INPUT_LIMITS.arrayItems),
|
|
502
|
+
agentId: z.string().min(1).max(INPUT_LIMITS.agentIdChars).optional()
|
|
503
|
+
};
|
|
504
|
+
var plannedFileSchema = z.object({
|
|
505
|
+
path: z.string().min(1).max(INPUT_LIMITS.pathChars),
|
|
506
|
+
action: z.enum(["create", "edit", "delete"]),
|
|
507
|
+
notes: z.string().max(INPUT_LIMITS.taskChars).optional()
|
|
508
|
+
});
|
|
509
|
+
var plannedTestSchema = z.object({
|
|
510
|
+
path: z.string().min(1).max(INPUT_LIMITS.pathChars),
|
|
511
|
+
description: z.string().max(INPUT_LIMITS.taskChars).optional()
|
|
512
|
+
});
|
|
513
|
+
var planPatchSchema = {
|
|
514
|
+
title: z.string().min(1).max(INPUT_LIMITS.titleChars).optional(),
|
|
515
|
+
brief: z.string().max(INPUT_LIMITS.planBriefChars).optional(),
|
|
516
|
+
body: z.string().max(INPUT_LIMITS.planBodyChars).optional(),
|
|
517
|
+
filesToTouch: z.array(plannedFileSchema).max(INPUT_LIMITS.arrayItems).optional(),
|
|
518
|
+
testsToAdd: z.array(plannedTestSchema).max(INPUT_LIMITS.arrayItems).optional(),
|
|
519
|
+
outOfScope: z.array(z.string().max(INPUT_LIMITS.pathChars)).max(INPUT_LIMITS.arrayItems).optional(),
|
|
520
|
+
risks: z.array(z.string().max(INPUT_LIMITS.taskChars)).max(INPUT_LIMITS.arrayItems).optional()
|
|
521
|
+
};
|
|
522
|
+
var mcpToolInputSchemas = {
|
|
523
|
+
remember: z.object(rememberSchema),
|
|
524
|
+
remember_batch: z.object({ items: z.array(z.object(rememberSchema)).min(1) }),
|
|
525
|
+
recall: z.object(recallSchema),
|
|
526
|
+
list_facts: z.object({
|
|
527
|
+
subject: z.string().optional(),
|
|
528
|
+
limit: z.number().int().positive().max(100).default(50),
|
|
529
|
+
cursor: z.string().optional()
|
|
530
|
+
}),
|
|
531
|
+
list_namespaces: z.object({}),
|
|
532
|
+
forget: z.object({ id: z.string().min(1) }),
|
|
533
|
+
share: z.object(shareSchema),
|
|
534
|
+
unshare: z.object(unshareSchema),
|
|
535
|
+
plan_current: z.object({}),
|
|
536
|
+
plan_create: z.object({
|
|
537
|
+
title: z.string().min(1).max(INPUT_LIMITS.titleChars),
|
|
538
|
+
brief: z.string().min(1).max(INPUT_LIMITS.planBriefChars),
|
|
539
|
+
body: z.string().max(INPUT_LIMITS.planBodyChars).optional(),
|
|
540
|
+
filesToTouch: z.array(plannedFileSchema).max(INPUT_LIMITS.arrayItems).optional(),
|
|
541
|
+
testsToAdd: z.array(plannedTestSchema).max(INPUT_LIMITS.arrayItems).optional(),
|
|
542
|
+
outOfScope: z.array(z.string().max(INPUT_LIMITS.pathChars)).max(INPUT_LIMITS.arrayItems).optional(),
|
|
543
|
+
risks: z.array(z.string().max(INPUT_LIMITS.taskChars)).max(INPUT_LIMITS.arrayItems).optional(),
|
|
544
|
+
agentId: z.string().min(1).max(INPUT_LIMITS.agentIdChars).optional()
|
|
545
|
+
}),
|
|
546
|
+
plan_amend: z.object({ id: z.string().min(1), patch: z.object(planPatchSchema) }),
|
|
547
|
+
plan_status: z.object({
|
|
548
|
+
id: z.string().min(1),
|
|
549
|
+
status: z.enum(["approved", "executing", "complete", "abandoned"]),
|
|
550
|
+
commitSha: z.string().optional(),
|
|
551
|
+
notes: z.string().optional(),
|
|
552
|
+
reason: z.string().optional()
|
|
553
|
+
}),
|
|
554
|
+
risk_assess: z.object({
|
|
555
|
+
planId: z.string().min(1).max(100).optional(),
|
|
556
|
+
paths: z.array(z.string().min(1).max(INPUT_LIMITS.pathChars)).max(INPUT_LIMITS.arrayItems).optional(),
|
|
557
|
+
evidence: z.enum(["overview", "source"]).optional(),
|
|
558
|
+
useGit: z.boolean().optional(),
|
|
559
|
+
limit: z.number().int().positive().max(100).optional()
|
|
560
|
+
}),
|
|
561
|
+
map_query: z.object({
|
|
562
|
+
from: z.string().min(1).max(INPUT_LIMITS.pathChars).optional(),
|
|
563
|
+
to: z.string().min(1).max(INPUT_LIMITS.pathChars).optional(),
|
|
564
|
+
limit: z.number().int().positive().max(100).optional()
|
|
565
|
+
}),
|
|
566
|
+
map_neighbours: z.object({
|
|
567
|
+
module: z.string().min(1).max(INPUT_LIMITS.pathChars),
|
|
568
|
+
direction: z.enum(["in", "out", "both"]).default("both")
|
|
569
|
+
}),
|
|
570
|
+
context_bootstrap: z.object({ ...deliverySchema }),
|
|
571
|
+
context_before_edit: z.object({
|
|
572
|
+
task: z.string().min(1).max(INPUT_LIMITS.taskChars).optional(),
|
|
573
|
+
paths: z.array(z.string().min(1).max(INPUT_LIMITS.pathChars)).max(INPUT_LIMITS.arrayItems).optional(),
|
|
574
|
+
planId: z.string().min(1).max(100).optional(),
|
|
575
|
+
evidence: z.enum(["overview", "source"]).optional(),
|
|
576
|
+
useGit: z.boolean().optional(),
|
|
577
|
+
tokenBudget: z.number().int().positive().max(1e5).optional(),
|
|
578
|
+
...deliverySchema
|
|
579
|
+
}),
|
|
580
|
+
context_pack: z.object({
|
|
581
|
+
task: z.string().min(1).max(INPUT_LIMITS.taskChars),
|
|
582
|
+
paths: z.array(z.string().min(1).max(INPUT_LIMITS.pathChars)).max(INPUT_LIMITS.arrayItems).optional(),
|
|
583
|
+
planId: z.string().min(1).max(100).optional(),
|
|
584
|
+
evidence: z.enum(["overview", "source"]).optional(),
|
|
585
|
+
useGit: z.boolean().optional(),
|
|
586
|
+
tokenBudget: z.number().int().positive().max(1e5).optional(),
|
|
587
|
+
...deliverySchema
|
|
588
|
+
}),
|
|
589
|
+
context_after_turn: z.object({
|
|
590
|
+
summary: z.string().min(1).max(INPUT_LIMITS.summaryChars),
|
|
591
|
+
changedFiles: z.array(z.string().min(1).max(INPUT_LIMITS.pathChars)).max(INPUT_LIMITS.arrayItems).optional(),
|
|
592
|
+
planId: z.string().min(1).max(100).optional(),
|
|
593
|
+
taskType: z.string().min(1).max(100).optional(),
|
|
594
|
+
agentId: z.string().min(1).max(INPUT_LIMITS.agentIdChars).optional(),
|
|
595
|
+
sessionId: z.string().min(1).max(200).optional(),
|
|
596
|
+
verification: z.array(z.string().min(1).max(500)).max(20).optional(),
|
|
597
|
+
commitSha: z.string().min(7).max(64).optional()
|
|
598
|
+
}),
|
|
599
|
+
code_suggestions: z.object({
|
|
600
|
+
paths: z.array(z.string().min(1).max(INPUT_LIMITS.pathChars)).max(INPUT_LIMITS.arrayItems).optional(),
|
|
601
|
+
planId: z.string().min(1).max(100).optional(),
|
|
602
|
+
evidence: z.enum(["overview", "source"]).optional(),
|
|
603
|
+
useGit: z.boolean().optional(),
|
|
604
|
+
limit: z.number().int().positive().max(100).optional()
|
|
605
|
+
}),
|
|
606
|
+
health: z.object({})
|
|
607
|
+
};
|
|
608
|
+
var DATABASE_TOOLS = [
|
|
609
|
+
"remember",
|
|
610
|
+
"remember_batch",
|
|
611
|
+
"recall",
|
|
612
|
+
"list_facts",
|
|
613
|
+
"list_namespaces",
|
|
614
|
+
"forget",
|
|
615
|
+
"share",
|
|
616
|
+
"unshare"
|
|
617
|
+
];
|
|
618
|
+
var FILE_TOOLS = Object.keys(mcpToolInputSchemas).filter(
|
|
619
|
+
(name) => !DATABASE_TOOLS.includes(name)
|
|
620
|
+
);
|
|
621
|
+
var DEGRADED_FALLBACK = "PostgreSQL is unavailable, so this call did nothing. File-based tools still work; read the repository directly and say recall is unavailable instead of guessing. Run `codebuddy db doctor` to diagnose; the next call after retryInMs reconnects.";
|
|
622
|
+
var CORE_TOOLS = [
|
|
623
|
+
"context_pack",
|
|
624
|
+
"context_bootstrap",
|
|
625
|
+
"context_before_edit",
|
|
626
|
+
"context_after_turn",
|
|
627
|
+
"code_suggestions",
|
|
628
|
+
"health"
|
|
629
|
+
];
|
|
630
|
+
function resolveToolProfile(value = process.env.CODEBUDDY_TOOL_PROFILE) {
|
|
631
|
+
if (value === void 0 || value === "" || value === "full") return "full";
|
|
632
|
+
if (value === "core") return "core";
|
|
633
|
+
throw new Error(`CODEBUDDY_TOOL_PROFILE must be "core" or "full", got "${value}"`);
|
|
634
|
+
}
|
|
635
|
+
var PLAN_BODY_LIMIT = 8e3;
|
|
636
|
+
function serializePlan(plan) {
|
|
637
|
+
if (!plan) return { plan: null };
|
|
638
|
+
const body = plan.body.length > PLAN_BODY_LIMIT ? `${plan.body.slice(0, PLAN_BODY_LIMIT)}
|
|
639
|
+
|
|
640
|
+
\u2026[truncated]` : plan.body;
|
|
641
|
+
return { plan: { ...plan, body } };
|
|
642
|
+
}
|
|
643
|
+
function registerCodeBuddyTools(server, options, profile = "full") {
|
|
644
|
+
const exposed = (name) => profile === "full" || CORE_TOOLS.includes(name);
|
|
645
|
+
const config = "database" in options ? options.config : options.memory.config;
|
|
646
|
+
const traceRoot = config.projectRoot ?? process.cwd();
|
|
647
|
+
const register = (name, toolConfig, callback) => {
|
|
648
|
+
const traced = (async (...args) => {
|
|
649
|
+
const started = Date.now();
|
|
650
|
+
const input = args.length > 1 ? args[0] : void 0;
|
|
651
|
+
const trace = (ok, output) => void recordTrace(traceRoot, {
|
|
652
|
+
kind: "tool",
|
|
653
|
+
name,
|
|
654
|
+
ms: Date.now() - started,
|
|
655
|
+
ok,
|
|
656
|
+
tokensIn: estimateTokens(input),
|
|
657
|
+
tokensOut: estimateTokens(output),
|
|
658
|
+
...typeof input?.sessionId === "string" ? { session: input.sessionId } : {}
|
|
659
|
+
});
|
|
660
|
+
try {
|
|
661
|
+
const result = await callback(...args);
|
|
662
|
+
trace(result?.isError !== true, result?.content?.map((part) => part.text ?? "").join(""));
|
|
663
|
+
return result;
|
|
664
|
+
} catch (error) {
|
|
665
|
+
trace(false);
|
|
666
|
+
throw error;
|
|
667
|
+
}
|
|
668
|
+
});
|
|
669
|
+
const tool = server.registerTool(name, toolConfig, traced);
|
|
670
|
+
if (!exposed(name)) tool.remove();
|
|
671
|
+
return tool;
|
|
672
|
+
};
|
|
673
|
+
const json = serializeToolResult;
|
|
674
|
+
const database = "database" in options ? options.database : { get: async () => options, status: () => ({ available: true, state: "ready" }) };
|
|
675
|
+
const viaDatabase = async (tool, run) => {
|
|
676
|
+
let db;
|
|
677
|
+
try {
|
|
678
|
+
db = await database.get();
|
|
679
|
+
} catch {
|
|
680
|
+
return {
|
|
681
|
+
...json({ degraded: true, tool, database: database.status(), fallback: DEGRADED_FALLBACK }),
|
|
682
|
+
isError: true
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
return json(await run(db));
|
|
686
|
+
};
|
|
687
|
+
const namespace = config.namespace;
|
|
688
|
+
const projectRoot = config.projectRoot ?? process.cwd();
|
|
689
|
+
const rankContextFacts = async (task, candidates) => {
|
|
690
|
+
if (database.status().state !== "ready") return {};
|
|
691
|
+
try {
|
|
692
|
+
const { memory } = await database.get();
|
|
693
|
+
return await memory.rankContextFacts(
|
|
694
|
+
task,
|
|
695
|
+
candidates.map(({ id }) => id)
|
|
696
|
+
);
|
|
697
|
+
} catch {
|
|
698
|
+
return {};
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
const reranker = configuredReranker(projectRoot).catch(() => void 0);
|
|
702
|
+
const rerankerOption = async () => {
|
|
703
|
+
const configured = await reranker;
|
|
704
|
+
return configured ? { reranker: configured } : {};
|
|
705
|
+
};
|
|
706
|
+
const planStore = new PlanFileStore(projectRoot);
|
|
707
|
+
const planLifecycle = new PlanLifecycle(planStore);
|
|
708
|
+
const toFiles = (items) => items?.map((item) => ({ path: item.path, action: item.action, notes: item.notes ?? "" }));
|
|
709
|
+
const toTests = (items) => items?.map((item) => ({ path: item.path, description: item.description ?? "" }));
|
|
710
|
+
register(
|
|
711
|
+
"remember",
|
|
712
|
+
{
|
|
713
|
+
title: "Remember",
|
|
714
|
+
description: "Store one CodeBuddy memory item.",
|
|
715
|
+
inputSchema: mcpToolInputSchemas.remember.shape
|
|
716
|
+
},
|
|
717
|
+
async (input) => viaDatabase("remember", (db) => db.memory.remember(toRememberInput(input)))
|
|
718
|
+
);
|
|
719
|
+
register(
|
|
720
|
+
"remember_batch",
|
|
721
|
+
{
|
|
722
|
+
title: "Remember batch",
|
|
723
|
+
description: "Store multiple CodeBuddy memory items.",
|
|
724
|
+
inputSchema: mcpToolInputSchemas.remember_batch.shape
|
|
725
|
+
},
|
|
726
|
+
async (input) => viaDatabase("remember_batch", async (db) => ({
|
|
727
|
+
items: await db.memory.rememberBatch(input.items.map(toRememberInput))
|
|
728
|
+
}))
|
|
729
|
+
);
|
|
730
|
+
register(
|
|
731
|
+
"recall",
|
|
732
|
+
{
|
|
733
|
+
title: "Recall",
|
|
734
|
+
description: "Recall prompt-ready CodeBuddy context.",
|
|
735
|
+
inputSchema: mcpToolInputSchemas.recall.shape
|
|
736
|
+
},
|
|
737
|
+
async (input) => viaDatabase("recall", (db) => db.memory.recall(toRecallInput(input)))
|
|
738
|
+
);
|
|
739
|
+
register(
|
|
740
|
+
"list_facts",
|
|
741
|
+
{
|
|
742
|
+
title: "List facts",
|
|
743
|
+
description: "List facts in the configured namespace.",
|
|
744
|
+
inputSchema: mcpToolInputSchemas.list_facts.shape
|
|
745
|
+
},
|
|
746
|
+
async (input) => viaDatabase(
|
|
747
|
+
"list_facts",
|
|
748
|
+
(db) => listFactsPage(
|
|
749
|
+
db.memory,
|
|
750
|
+
db.repository,
|
|
751
|
+
cleanOptional(input)
|
|
752
|
+
)
|
|
753
|
+
)
|
|
754
|
+
);
|
|
755
|
+
register(
|
|
756
|
+
"list_namespaces",
|
|
757
|
+
{
|
|
758
|
+
title: "List namespaces",
|
|
759
|
+
description: "List CodeBuddy namespaces.",
|
|
760
|
+
inputSchema: mcpToolInputSchemas.list_namespaces.shape
|
|
761
|
+
},
|
|
762
|
+
async () => viaDatabase("list_namespaces", async (db) => ({
|
|
763
|
+
namespaces: await db.repository.listNamespaces()
|
|
764
|
+
}))
|
|
765
|
+
);
|
|
766
|
+
register(
|
|
767
|
+
"forget",
|
|
768
|
+
{
|
|
769
|
+
title: "Forget",
|
|
770
|
+
description: "Delete an interaction or fact by id.",
|
|
771
|
+
inputSchema: mcpToolInputSchemas.forget.shape
|
|
772
|
+
},
|
|
773
|
+
async (input) => viaDatabase("forget", (db) => db.memory.forget(input.id))
|
|
774
|
+
);
|
|
775
|
+
register(
|
|
776
|
+
"share",
|
|
777
|
+
{
|
|
778
|
+
title: "Share",
|
|
779
|
+
description: "Share facts from the configured namespace to another namespace. Reference (default) is a live, revocable grant; snapshot copies the fact into the target project's Markdown store (requires to_project_root).",
|
|
780
|
+
inputSchema: mcpToolInputSchemas.share.shape
|
|
781
|
+
},
|
|
782
|
+
async (input) => viaDatabase(
|
|
783
|
+
"share",
|
|
784
|
+
(db) => db.memory.share({
|
|
785
|
+
from: input.from ?? namespace,
|
|
786
|
+
to: input.to_namespace,
|
|
787
|
+
factIds: input.factIds,
|
|
788
|
+
...input.mode ? { mode: input.mode } : {},
|
|
789
|
+
...input.to_project_root ? { targetProjectRoot: input.to_project_root } : {},
|
|
790
|
+
...input.agentId ? { agentId: input.agentId } : {}
|
|
791
|
+
})
|
|
792
|
+
)
|
|
793
|
+
);
|
|
794
|
+
register(
|
|
795
|
+
"unshare",
|
|
796
|
+
{
|
|
797
|
+
title: "Unshare",
|
|
798
|
+
description: "Revoke live reference shares of configured-namespace facts to another namespace.",
|
|
799
|
+
inputSchema: mcpToolInputSchemas.unshare.shape
|
|
800
|
+
},
|
|
801
|
+
async (input) => viaDatabase(
|
|
802
|
+
"unshare",
|
|
803
|
+
(db) => db.memory.unshare({
|
|
804
|
+
to: input.to_namespace,
|
|
805
|
+
factIds: input.factIds,
|
|
806
|
+
...input.agentId ? { agentId: input.agentId } : {}
|
|
807
|
+
})
|
|
808
|
+
)
|
|
809
|
+
);
|
|
810
|
+
register(
|
|
811
|
+
"plan_current",
|
|
812
|
+
{
|
|
813
|
+
title: "Current plan",
|
|
814
|
+
description: "Return the active (approved or executing) plan for this namespace, or null.",
|
|
815
|
+
inputSchema: mcpToolInputSchemas.plan_current.shape
|
|
816
|
+
},
|
|
817
|
+
async () => {
|
|
818
|
+
const [current, policy] = await Promise.all([
|
|
819
|
+
planLifecycle.current(namespace),
|
|
820
|
+
loadPlanPolicy(projectRoot)
|
|
821
|
+
]);
|
|
822
|
+
return json({ ...serializePlan(current), policy });
|
|
823
|
+
}
|
|
824
|
+
);
|
|
825
|
+
register(
|
|
826
|
+
"plan_create",
|
|
827
|
+
{
|
|
828
|
+
title: "Create plan",
|
|
829
|
+
description: "Create a draft plan in this namespace.",
|
|
830
|
+
inputSchema: mcpToolInputSchemas.plan_create.shape
|
|
831
|
+
},
|
|
832
|
+
async (input) => {
|
|
833
|
+
const files = toFiles(input.filesToTouch);
|
|
834
|
+
const tests = toTests(input.testsToAdd);
|
|
835
|
+
const created = await planLifecycle.create({
|
|
836
|
+
namespace,
|
|
837
|
+
title: input.title,
|
|
838
|
+
brief: input.brief,
|
|
839
|
+
...input.body !== void 0 ? { body: input.body } : {},
|
|
840
|
+
...files ? { filesToTouch: files } : {},
|
|
841
|
+
...tests ? { testsToAdd: tests } : {},
|
|
842
|
+
...input.outOfScope !== void 0 ? { outOfScope: input.outOfScope } : {},
|
|
843
|
+
...input.risks !== void 0 ? { risks: input.risks } : {},
|
|
844
|
+
...input.agentId !== void 0 ? { createdByAgent: input.agentId } : {}
|
|
845
|
+
});
|
|
846
|
+
return json(serializePlan(created));
|
|
847
|
+
}
|
|
848
|
+
);
|
|
849
|
+
register(
|
|
850
|
+
"plan_amend",
|
|
851
|
+
{
|
|
852
|
+
title: "Amend plan",
|
|
853
|
+
description: "Amend a draft or executing plan's content fields.",
|
|
854
|
+
inputSchema: mcpToolInputSchemas.plan_amend.shape
|
|
855
|
+
},
|
|
856
|
+
async (input) => {
|
|
857
|
+
const patch = input.patch;
|
|
858
|
+
const files = toFiles(patch.filesToTouch);
|
|
859
|
+
const tests = toTests(patch.testsToAdd);
|
|
860
|
+
const amended = await planLifecycle.amend(input.id, {
|
|
861
|
+
...patch.title !== void 0 ? { title: patch.title } : {},
|
|
862
|
+
...patch.brief !== void 0 ? { brief: patch.brief } : {},
|
|
863
|
+
...patch.body !== void 0 ? { body: patch.body } : {},
|
|
864
|
+
...files ? { filesToTouch: files } : {},
|
|
865
|
+
...tests ? { testsToAdd: tests } : {},
|
|
866
|
+
...patch.outOfScope !== void 0 ? { outOfScope: patch.outOfScope } : {},
|
|
867
|
+
...patch.risks !== void 0 ? { risks: patch.risks } : {}
|
|
868
|
+
});
|
|
869
|
+
return json(serializePlan(amended));
|
|
870
|
+
}
|
|
871
|
+
);
|
|
872
|
+
register(
|
|
873
|
+
"plan_status",
|
|
874
|
+
{
|
|
875
|
+
title: "Transition plan",
|
|
876
|
+
description: "Move a plan through its lifecycle: approved, executing, complete, abandoned.",
|
|
877
|
+
inputSchema: mcpToolInputSchemas.plan_status.shape
|
|
878
|
+
},
|
|
879
|
+
async (input) => {
|
|
880
|
+
let next;
|
|
881
|
+
if (input.status === "approved") next = await planLifecycle.approve(input.id);
|
|
882
|
+
else if (input.status === "executing") next = await planLifecycle.start(input.id);
|
|
883
|
+
else if (input.status === "complete")
|
|
884
|
+
next = await planLifecycle.complete(input.id, {
|
|
885
|
+
...input.commitSha !== void 0 ? { commitSha: input.commitSha } : {},
|
|
886
|
+
...input.notes !== void 0 ? { notes: input.notes } : {}
|
|
887
|
+
});
|
|
888
|
+
else
|
|
889
|
+
next = await planLifecycle.abandon(
|
|
890
|
+
input.id,
|
|
891
|
+
input.reason !== void 0 ? { reason: input.reason } : {}
|
|
892
|
+
);
|
|
893
|
+
return json(serializePlan(next));
|
|
894
|
+
}
|
|
895
|
+
);
|
|
896
|
+
register(
|
|
897
|
+
"risk_assess",
|
|
898
|
+
{
|
|
899
|
+
title: "Assess risk",
|
|
900
|
+
description: "Assess evidence-backed risk for a plan, explicit paths, or git changes.",
|
|
901
|
+
inputSchema: mcpToolInputSchemas.risk_assess.shape
|
|
902
|
+
},
|
|
903
|
+
async (input) => json(
|
|
904
|
+
await assessRisk({
|
|
905
|
+
repositoryRoot: projectRoot,
|
|
906
|
+
namespace,
|
|
907
|
+
...input.planId !== void 0 ? { planId: input.planId } : {},
|
|
908
|
+
...input.paths !== void 0 ? { paths: input.paths } : {},
|
|
909
|
+
...input.useGit !== void 0 ? { useGit: input.useGit } : {},
|
|
910
|
+
...input.limit !== void 0 ? { limit: input.limit } : {}
|
|
911
|
+
})
|
|
912
|
+
)
|
|
913
|
+
);
|
|
914
|
+
register(
|
|
915
|
+
"map_query",
|
|
916
|
+
{
|
|
917
|
+
title: "Query architecture map",
|
|
918
|
+
description: "Query lightweight module import edges in the current repository.",
|
|
919
|
+
inputSchema: mcpToolInputSchemas.map_query.shape
|
|
920
|
+
},
|
|
921
|
+
async (input) => {
|
|
922
|
+
const graph = await cachedMap();
|
|
923
|
+
return json(
|
|
924
|
+
queryMap(graph, {
|
|
925
|
+
...input.from !== void 0 ? { from: input.from } : {},
|
|
926
|
+
...input.to !== void 0 ? { to: input.to } : {},
|
|
927
|
+
...input.limit !== void 0 ? { limit: input.limit } : {}
|
|
928
|
+
})
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
);
|
|
932
|
+
register(
|
|
933
|
+
"map_neighbours",
|
|
934
|
+
{
|
|
935
|
+
title: "Module neighbours",
|
|
936
|
+
description: "Return inbound/outbound neighbours for a repository module.",
|
|
937
|
+
inputSchema: mcpToolInputSchemas.map_neighbours.shape
|
|
938
|
+
},
|
|
939
|
+
async (input) => json(neighbours(await cachedMap(), input.module, input.direction))
|
|
940
|
+
);
|
|
941
|
+
const tokenBudget = config.tokenBudget;
|
|
942
|
+
const cachedMap = async () => {
|
|
943
|
+
const { manifest } = await loadFreshIndex(projectRoot);
|
|
944
|
+
const cached = await loadArchitectureMap(projectRoot, manifest);
|
|
945
|
+
if (cached.map.modules.length > 0) return cached.map;
|
|
946
|
+
return buildArchitectureMap(projectRoot);
|
|
947
|
+
};
|
|
948
|
+
register(
|
|
949
|
+
"context_bootstrap",
|
|
950
|
+
{
|
|
951
|
+
title: "Bootstrap project context",
|
|
952
|
+
description: "Return compact project awareness for the start of a turn: identity, active plan and policy, top policy rules, incident hotspots, and an architecture summary. Prefer this over a manual recall at session start. Optional sessionId enables session-aware delivery: pass the previous response's delivery.deliveryId as ack and unchanged sections are omitted (listed in delivery.unchanged); pass full=true to get everything again.",
|
|
953
|
+
inputSchema: mcpToolInputSchemas.context_bootstrap.shape
|
|
954
|
+
},
|
|
955
|
+
async (input) => json(
|
|
956
|
+
await applySessionDelivery(
|
|
957
|
+
projectRoot,
|
|
958
|
+
await buildBootstrapContext({
|
|
959
|
+
repositoryRoot: projectRoot,
|
|
960
|
+
namespace,
|
|
961
|
+
...tokenBudget !== void 0 ? { tokenBudget } : {}
|
|
962
|
+
}),
|
|
963
|
+
deliveryOptions(input)
|
|
964
|
+
)
|
|
965
|
+
)
|
|
966
|
+
);
|
|
967
|
+
register(
|
|
968
|
+
"context_before_edit",
|
|
969
|
+
{
|
|
970
|
+
title: "Context before edit",
|
|
971
|
+
description: "Assemble relevant context before changing code: resolves target files from paths, a plan, or git, then returns the relevant plan, matching policy rules, incident memory, an evidence-backed risk assessment, and import neighbours. Optional sessionId enables session-aware delivery: pass the previous response's delivery.deliveryId as ack and unchanged sections are omitted (listed in delivery.unchanged); pass full=true to get everything again.",
|
|
972
|
+
inputSchema: mcpToolInputSchemas.context_before_edit.shape
|
|
973
|
+
},
|
|
974
|
+
async (input) => json(
|
|
975
|
+
await applySessionDelivery(
|
|
976
|
+
projectRoot,
|
|
977
|
+
await buildBeforeEditContext({
|
|
978
|
+
repositoryRoot: projectRoot,
|
|
979
|
+
namespace,
|
|
980
|
+
semanticRanker: rankContextFacts,
|
|
981
|
+
...await rerankerOption(),
|
|
982
|
+
...input.evidence ? { evidence: input.evidence } : {},
|
|
983
|
+
...input.task !== void 0 ? { task: input.task } : {},
|
|
984
|
+
...input.paths !== void 0 ? { paths: input.paths } : {},
|
|
985
|
+
...input.planId !== void 0 ? { planId: input.planId } : {},
|
|
986
|
+
...input.useGit !== void 0 ? { useGit: input.useGit } : {},
|
|
987
|
+
...input.tokenBudget !== void 0 ? { tokenBudget: input.tokenBudget } : {},
|
|
988
|
+
...tokenBudget !== void 0 && input.tokenBudget === void 0 ? { tokenBudget } : {}
|
|
989
|
+
}),
|
|
990
|
+
deliveryOptions(input)
|
|
991
|
+
)
|
|
992
|
+
)
|
|
993
|
+
);
|
|
994
|
+
register(
|
|
995
|
+
"context_pack",
|
|
996
|
+
{
|
|
997
|
+
title: "Build task context pack",
|
|
998
|
+
description: "Build the smallest useful context pack for a coding task. Automatically finds relevant files from the local index when paths are omitted, then includes only bounded symbols, policies, risks, incidents, architecture neighbours, and relevant team facts within the requested token budget. Use this once at the start of a task instead of manually chaining context, risk, map, and memory tools. Request evidence=source with exact paths when short excerpts omit needed behavior; source excerpts remain bounded by the token budget. Optional sessionId enables session-aware delivery: pass the previous response's delivery.deliveryId as ack and unchanged sections are omitted (listed in delivery.unchanged); pass full=true to get everything again.",
|
|
999
|
+
inputSchema: mcpToolInputSchemas.context_pack.shape
|
|
1000
|
+
},
|
|
1001
|
+
async (input) => json(
|
|
1002
|
+
await applySessionDelivery(
|
|
1003
|
+
projectRoot,
|
|
1004
|
+
await buildBeforeEditContext({
|
|
1005
|
+
repositoryRoot: projectRoot,
|
|
1006
|
+
namespace,
|
|
1007
|
+
task: input.task,
|
|
1008
|
+
semanticRanker: rankContextFacts,
|
|
1009
|
+
...await rerankerOption(),
|
|
1010
|
+
...input.evidence ? { evidence: input.evidence } : {},
|
|
1011
|
+
...input.paths !== void 0 ? { paths: input.paths } : {},
|
|
1012
|
+
...input.planId !== void 0 ? { planId: input.planId } : {},
|
|
1013
|
+
...input.useGit !== void 0 ? { useGit: input.useGit } : {},
|
|
1014
|
+
tokenBudget: input.tokenBudget ?? tokenBudget ?? 4e3
|
|
1015
|
+
}),
|
|
1016
|
+
deliveryOptions(input)
|
|
1017
|
+
)
|
|
1018
|
+
)
|
|
1019
|
+
);
|
|
1020
|
+
register(
|
|
1021
|
+
"context_after_turn",
|
|
1022
|
+
{
|
|
1023
|
+
title: "Capture context after a turn",
|
|
1024
|
+
description: "Extract durable knowledge from a turn summary. Confident facts/decisions/incidents are auto-saved as Markdown; temporary notes, low-confidence, and sensitive items are queued for review under .codebuddy/memory/review/; chatter is ignored. Never saves sensitive content automatically. Turns that changed files or report verification also record a bounded worklog handoff under .codebuddy/worklog/ so later chats can recover the work.",
|
|
1025
|
+
inputSchema: mcpToolInputSchemas.context_after_turn.shape
|
|
1026
|
+
},
|
|
1027
|
+
async (input) => {
|
|
1028
|
+
const result = await captureAfterTurn(
|
|
1029
|
+
{
|
|
1030
|
+
summary: input.summary,
|
|
1031
|
+
namespace,
|
|
1032
|
+
...input.changedFiles !== void 0 ? { changedFiles: input.changedFiles } : {},
|
|
1033
|
+
...input.planId !== void 0 ? { planId: input.planId } : {},
|
|
1034
|
+
...input.taskType !== void 0 ? { taskType: input.taskType } : {},
|
|
1035
|
+
...input.agentId !== void 0 ? { agentId: input.agentId } : {},
|
|
1036
|
+
...input.sessionId !== void 0 ? { sessionId: input.sessionId } : {},
|
|
1037
|
+
...input.verification !== void 0 ? { verification: input.verification } : {},
|
|
1038
|
+
...input.commitSha !== void 0 ? { commitSha: input.commitSha } : {}
|
|
1039
|
+
},
|
|
1040
|
+
{
|
|
1041
|
+
repositoryRoot: projectRoot,
|
|
1042
|
+
...(input.changedFiles?.length ?? 0) > 0 ? await llmMemoryOptions(projectRoot) : {}
|
|
1043
|
+
}
|
|
1044
|
+
);
|
|
1045
|
+
const db = await database.get().catch(() => void 0);
|
|
1046
|
+
await db?.memory.reindexMemory({ respace: false });
|
|
1047
|
+
return json({
|
|
1048
|
+
saved: result.saved.map((d) => ({
|
|
1049
|
+
id: d.ref,
|
|
1050
|
+
class: d.candidate.class,
|
|
1051
|
+
subject: d.candidate.subject
|
|
1052
|
+
})),
|
|
1053
|
+
queuedForReview: result.queuedForReview.map((d) => ({
|
|
1054
|
+
id: d.ref,
|
|
1055
|
+
class: d.candidate.class,
|
|
1056
|
+
reason: d.reason,
|
|
1057
|
+
sensitive: d.sensitive
|
|
1058
|
+
})),
|
|
1059
|
+
ignored: result.ignored.length,
|
|
1060
|
+
reasons: result.reasons,
|
|
1061
|
+
extractor: result.extractor,
|
|
1062
|
+
worklogId: result.worklogId,
|
|
1063
|
+
...db ? {} : { degraded: true, database: database.status() }
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
);
|
|
1067
|
+
register(
|
|
1068
|
+
"code_suggestions",
|
|
1069
|
+
{
|
|
1070
|
+
title: "Code suggestions",
|
|
1071
|
+
description: "Return read-only, evidence-backed code-quality suggestions for a change set (paths, a plan, or git). Composes risk, plan divergence, architecture blast radius, incident history, missing tests, and stale policies. Never edits code.",
|
|
1072
|
+
inputSchema: mcpToolInputSchemas.code_suggestions.shape
|
|
1073
|
+
},
|
|
1074
|
+
async (input) => json(
|
|
1075
|
+
await generateSuggestions({
|
|
1076
|
+
repositoryRoot: projectRoot,
|
|
1077
|
+
namespace,
|
|
1078
|
+
...input.paths !== void 0 ? { paths: input.paths } : {},
|
|
1079
|
+
...input.planId !== void 0 ? { planId: input.planId } : {},
|
|
1080
|
+
...input.useGit !== void 0 ? { useGit: input.useGit } : {},
|
|
1081
|
+
...input.limit !== void 0 ? { limit: input.limit } : {}
|
|
1082
|
+
})
|
|
1083
|
+
)
|
|
1084
|
+
);
|
|
1085
|
+
register(
|
|
1086
|
+
"health",
|
|
1087
|
+
{
|
|
1088
|
+
title: "Health",
|
|
1089
|
+
description: "Report which CodeBuddy tools are available right now: file-based tools always work; database tools (recall, remember, facts) need PostgreSQL. Does not wait on a connection attempt.",
|
|
1090
|
+
inputSchema: mcpToolInputSchemas.health.shape
|
|
1091
|
+
},
|
|
1092
|
+
async () => json({
|
|
1093
|
+
database: database.status(),
|
|
1094
|
+
tools: {
|
|
1095
|
+
profile,
|
|
1096
|
+
file: FILE_TOOLS.filter(exposed),
|
|
1097
|
+
database: DATABASE_TOOLS.filter(exposed)
|
|
1098
|
+
}
|
|
1099
|
+
})
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
function deliveryOptions(input) {
|
|
1103
|
+
return cleanOptional(input);
|
|
1104
|
+
}
|
|
1105
|
+
function toRememberInput(input) {
|
|
1106
|
+
return cleanOptional(input);
|
|
1107
|
+
}
|
|
1108
|
+
function toRecallInput(input) {
|
|
1109
|
+
return cleanOptional(input);
|
|
1110
|
+
}
|
|
1111
|
+
function cleanOptional(input) {
|
|
1112
|
+
return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== void 0));
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// src/mcp/server.ts
|
|
1116
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1117
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1118
|
+
import pino from "pino";
|
|
1119
|
+
|
|
1120
|
+
// src/automation/project-sync.ts
|
|
1121
|
+
import { watch as fsWatch2 } from "fs";
|
|
1122
|
+
import { join as join2, relative, resolve as resolve3, sep } from "path";
|
|
1123
|
+
|
|
1124
|
+
// src/indexer/watch.ts
|
|
1125
|
+
import { watch as fsWatch } from "fs";
|
|
1126
|
+
import { resolve as resolve2 } from "path";
|
|
1127
|
+
var DEFAULT_DEBOUNCE_MS = 300;
|
|
1128
|
+
function createCoalescer(fn, delayMs) {
|
|
1129
|
+
let timer = null;
|
|
1130
|
+
let running = null;
|
|
1131
|
+
let dirty = false;
|
|
1132
|
+
let stopped = false;
|
|
1133
|
+
let firstTriggerAt = null;
|
|
1134
|
+
let maxWaitTimer = null;
|
|
1135
|
+
const clearTimers = () => {
|
|
1136
|
+
if (timer) {
|
|
1137
|
+
clearTimeout(timer);
|
|
1138
|
+
timer = null;
|
|
1139
|
+
}
|
|
1140
|
+
if (maxWaitTimer) {
|
|
1141
|
+
clearTimeout(maxWaitTimer);
|
|
1142
|
+
maxWaitTimer = null;
|
|
1143
|
+
}
|
|
1144
|
+
firstTriggerAt = null;
|
|
1145
|
+
};
|
|
1146
|
+
const run = () => {
|
|
1147
|
+
if (stopped || running) return;
|
|
1148
|
+
clearTimers();
|
|
1149
|
+
dirty = false;
|
|
1150
|
+
running = (async () => {
|
|
1151
|
+
try {
|
|
1152
|
+
await fn();
|
|
1153
|
+
} finally {
|
|
1154
|
+
running = null;
|
|
1155
|
+
if (dirty) schedule();
|
|
1156
|
+
}
|
|
1157
|
+
})();
|
|
1158
|
+
};
|
|
1159
|
+
const schedule = () => {
|
|
1160
|
+
if (stopped) return;
|
|
1161
|
+
dirty = true;
|
|
1162
|
+
if (firstTriggerAt === null) {
|
|
1163
|
+
firstTriggerAt = Date.now();
|
|
1164
|
+
maxWaitTimer = setTimeout(run, MAX_WAIT_MS);
|
|
1165
|
+
}
|
|
1166
|
+
if (timer) clearTimeout(timer);
|
|
1167
|
+
timer = setTimeout(run, delayMs);
|
|
1168
|
+
};
|
|
1169
|
+
return {
|
|
1170
|
+
trigger: schedule,
|
|
1171
|
+
stop: async () => {
|
|
1172
|
+
stopped = true;
|
|
1173
|
+
clearTimers();
|
|
1174
|
+
await running?.catch(() => void 0);
|
|
1175
|
+
}
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
var MAX_WAIT_MS = 5e3;
|
|
1179
|
+
function isIgnoredEvent(filename, ignoredPaths = []) {
|
|
1180
|
+
if (!filename) return false;
|
|
1181
|
+
const normalized = filename.replace(/\\/g, "/");
|
|
1182
|
+
return ignoredPaths.some((path) => normalized === path || normalized.startsWith(`${path}/`)) || normalized.startsWith(".codebuddy/") || normalized.includes("/.git/") || normalized.startsWith(".git/") || normalized.includes("/node_modules/") || normalized.startsWith("node_modules/") || normalized.endsWith(".tmp");
|
|
1183
|
+
}
|
|
1184
|
+
async function watchRepository(options = {}) {
|
|
1185
|
+
const repositoryRoot = resolve2(options.repositoryRoot ?? process.cwd());
|
|
1186
|
+
const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
1187
|
+
const runIndex = async () => {
|
|
1188
|
+
try {
|
|
1189
|
+
const result = await indexRepository({
|
|
1190
|
+
repositoryRoot,
|
|
1191
|
+
...options.now ? { now: options.now } : {}
|
|
1192
|
+
});
|
|
1193
|
+
options.onIndex?.(result);
|
|
1194
|
+
} catch (error) {
|
|
1195
|
+
options.onError?.(error);
|
|
1196
|
+
}
|
|
1197
|
+
};
|
|
1198
|
+
await runIndex();
|
|
1199
|
+
const coalescer = createCoalescer(runIndex, debounceMs);
|
|
1200
|
+
const watcher = fsWatch(repositoryRoot, { recursive: true }, (_event, filename) => {
|
|
1201
|
+
if (isIgnoredEvent(filename, options.ignoredPaths)) return;
|
|
1202
|
+
coalescer.trigger();
|
|
1203
|
+
});
|
|
1204
|
+
return new Promise((resolvePromise) => {
|
|
1205
|
+
const stop = () => {
|
|
1206
|
+
watcher.close();
|
|
1207
|
+
void coalescer.stop().then(() => resolvePromise());
|
|
1208
|
+
};
|
|
1209
|
+
if (options.signal) {
|
|
1210
|
+
if (options.signal.aborted) return stop();
|
|
1211
|
+
options.signal.addEventListener("abort", stop, { once: true });
|
|
1212
|
+
}
|
|
1213
|
+
watcher.on("error", () => stop());
|
|
1214
|
+
});
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
// src/automation/project-sync.ts
|
|
1218
|
+
function startProjectSync(options) {
|
|
1219
|
+
const repositoryRoot = resolve3(options.repositoryRoot);
|
|
1220
|
+
const logger = options.logger ?? console;
|
|
1221
|
+
const controller = new AbortController();
|
|
1222
|
+
let watcher = null;
|
|
1223
|
+
let closing = false;
|
|
1224
|
+
let memoryWatcher = null;
|
|
1225
|
+
let memoryRefresh = null;
|
|
1226
|
+
const backgroundRefresh = (task) => {
|
|
1227
|
+
let current = null;
|
|
1228
|
+
let pending = false;
|
|
1229
|
+
const schedule = () => {
|
|
1230
|
+
if (closing) return;
|
|
1231
|
+
if (current) {
|
|
1232
|
+
pending = true;
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
current = (async () => {
|
|
1236
|
+
do {
|
|
1237
|
+
pending = false;
|
|
1238
|
+
await task();
|
|
1239
|
+
} while (pending && !closing);
|
|
1240
|
+
})().finally(() => {
|
|
1241
|
+
current = null;
|
|
1242
|
+
if (pending && !closing) schedule();
|
|
1243
|
+
});
|
|
1244
|
+
};
|
|
1245
|
+
return { schedule, settled: async () => current?.catch(() => void 0) };
|
|
1246
|
+
};
|
|
1247
|
+
const graphRefresh = backgroundRefresh(
|
|
1248
|
+
() => refreshGraph().catch(
|
|
1249
|
+
(error) => logger.error(`[sync] Graphify refresh failed: ${error.message}`)
|
|
1250
|
+
)
|
|
1251
|
+
);
|
|
1252
|
+
const zvecRefresh = backgroundRefresh(async () => {
|
|
1253
|
+
const result = await refreshZvecIndex(repositoryRoot, { signal: controller.signal }).catch(
|
|
1254
|
+
(error) => ({
|
|
1255
|
+
status: "failed",
|
|
1256
|
+
reason: error.message
|
|
1257
|
+
})
|
|
1258
|
+
);
|
|
1259
|
+
if (result.status !== "disabled") logger.info(`[sync] ${result.reason}`);
|
|
1260
|
+
});
|
|
1261
|
+
const ready = (async () => {
|
|
1262
|
+
await indexRepository({ repositoryRoot });
|
|
1263
|
+
zvecRefresh.schedule();
|
|
1264
|
+
watchMemory();
|
|
1265
|
+
if (options.graphify?.enabled) {
|
|
1266
|
+
await refreshGraph().catch(
|
|
1267
|
+
(error) => logger.error(`[sync] Graphify startup refresh failed: ${error.message}`)
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
1270
|
+
watcher = watchRepository({
|
|
1271
|
+
repositoryRoot,
|
|
1272
|
+
signal: controller.signal,
|
|
1273
|
+
...options.graphify?.enabled ? {
|
|
1274
|
+
ignoredPaths: [
|
|
1275
|
+
options.graphify.graphPath ? relative(repositoryRoot, resolve3(repositoryRoot, options.graphify.graphPath)).split(sep).join("/") : "graphify-out"
|
|
1276
|
+
]
|
|
1277
|
+
} : {},
|
|
1278
|
+
onIndex: (result) => {
|
|
1279
|
+
if (result.added + result.changed + result.removed === 0) return;
|
|
1280
|
+
zvecRefresh.schedule();
|
|
1281
|
+
if (options.graphify?.enabled) graphRefresh.schedule();
|
|
1282
|
+
},
|
|
1283
|
+
onError: (error) => logger.error(`[sync] repository watch failed: ${error.message}`)
|
|
1284
|
+
});
|
|
1285
|
+
void watcher.catch(
|
|
1286
|
+
(error) => logger.error(`[sync] live repository watch unavailable: ${error.message}`)
|
|
1287
|
+
);
|
|
1288
|
+
})().catch((error) => {
|
|
1289
|
+
logger.error(`[sync] startup synchronization failed: ${error.message}`);
|
|
1290
|
+
throw error;
|
|
1291
|
+
});
|
|
1292
|
+
function watchMemory() {
|
|
1293
|
+
const onMemoryChange = options.onMemoryChange;
|
|
1294
|
+
if (closing || !onMemoryChange) return;
|
|
1295
|
+
try {
|
|
1296
|
+
memoryRefresh = createCoalescer(
|
|
1297
|
+
() => onMemoryChange().catch(
|
|
1298
|
+
(error) => logger.error(`[sync] memory reindex failed: ${error.message}`)
|
|
1299
|
+
),
|
|
1300
|
+
300
|
|
1301
|
+
);
|
|
1302
|
+
memoryWatcher = fsWatch2(
|
|
1303
|
+
join2(repositoryRoot, ".codebuddy", "memory"),
|
|
1304
|
+
{ recursive: true },
|
|
1305
|
+
(_event, filename) => {
|
|
1306
|
+
const name = filename?.replace(/\\/g, "/") ?? "";
|
|
1307
|
+
if (/^(facts\/fact|summaries\/sum)_[a-z0-9]+\.md$/.test(name)) memoryRefresh?.trigger();
|
|
1308
|
+
}
|
|
1309
|
+
);
|
|
1310
|
+
memoryWatcher.on("error", () => memoryWatcher?.close());
|
|
1311
|
+
} catch (error) {
|
|
1312
|
+
logger.error(`[sync] live memory watch unavailable: ${error.message}`);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
async function refreshGraph() {
|
|
1316
|
+
const result = await generateGraphifyGraph({
|
|
1317
|
+
repositoryRoot,
|
|
1318
|
+
...options.graphify?.graphPath ? { graphPath: options.graphify.graphPath } : {}
|
|
1319
|
+
});
|
|
1320
|
+
logger.info(`[sync] Graphify updated (${result.nodes} nodes, ${result.edges} edges)`);
|
|
1321
|
+
}
|
|
1322
|
+
return {
|
|
1323
|
+
ready,
|
|
1324
|
+
async close() {
|
|
1325
|
+
closing = true;
|
|
1326
|
+
controller.abort();
|
|
1327
|
+
memoryRefresh?.stop();
|
|
1328
|
+
memoryWatcher?.close();
|
|
1329
|
+
await watcher?.catch(() => void 0);
|
|
1330
|
+
await Promise.all([graphRefresh.settled(), zvecRefresh.settled()]);
|
|
1331
|
+
}
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// src/core/project-root.ts
|
|
1336
|
+
import { realpath, stat } from "fs/promises";
|
|
1337
|
+
import { homedir } from "os";
|
|
1338
|
+
import { isAbsolute, parse, resolve as resolve4 } from "path";
|
|
1339
|
+
async function resolveInitializedProjectRoot(configuredRoot, cwd = process.cwd()) {
|
|
1340
|
+
if (configuredRoot !== void 0 && !isAbsolute(configuredRoot)) {
|
|
1341
|
+
throw new Error("CODEBUDDY_PROJECT_ROOT must be an absolute path to an initialized project.");
|
|
1342
|
+
}
|
|
1343
|
+
const selected = resolve4(configuredRoot ?? cwd);
|
|
1344
|
+
const root = await realpath(selected);
|
|
1345
|
+
if (root === parse(root).root || root === await realpath(homedir())) {
|
|
1346
|
+
throw new Error(
|
|
1347
|
+
"Select an initialized project folder, not the filesystem root or home folder."
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1350
|
+
const initialized = await stat(configPath(root)).then(
|
|
1351
|
+
(info) => info.isFile(),
|
|
1352
|
+
() => false
|
|
1353
|
+
);
|
|
1354
|
+
if (!initialized) {
|
|
1355
|
+
throw new Error(
|
|
1356
|
+
`CodeBuddy will not scan ${root}: run codebuddy use (or codebuddy init) in your project folder first. For a desktop client, re-register CodeBuddy from that folder to save its project path.`
|
|
1357
|
+
);
|
|
1358
|
+
}
|
|
1359
|
+
return root;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// src/mcp/database.ts
|
|
1363
|
+
function createLazyDatabase(connect, options = {}) {
|
|
1364
|
+
const retryMs = options.retryMs ?? 5e3;
|
|
1365
|
+
let runtime;
|
|
1366
|
+
let pending;
|
|
1367
|
+
let failure;
|
|
1368
|
+
let closed = false;
|
|
1369
|
+
return {
|
|
1370
|
+
get() {
|
|
1371
|
+
if (runtime) return Promise.resolve(runtime);
|
|
1372
|
+
if (closed) return Promise.reject(new Error("CodeBuddy MCP server is shutting down."));
|
|
1373
|
+
if (pending) return pending;
|
|
1374
|
+
if (failure && Date.now() - failure.at < retryMs) {
|
|
1375
|
+
return Promise.reject(new Error(failure.error));
|
|
1376
|
+
}
|
|
1377
|
+
pending = connect().then(
|
|
1378
|
+
async (handle) => {
|
|
1379
|
+
if (closed) {
|
|
1380
|
+
await handle.close();
|
|
1381
|
+
throw new Error("CodeBuddy MCP server is shutting down.");
|
|
1382
|
+
}
|
|
1383
|
+
runtime = handle;
|
|
1384
|
+
failure = void 0;
|
|
1385
|
+
return handle;
|
|
1386
|
+
},
|
|
1387
|
+
(error) => {
|
|
1388
|
+
failure = { error: describeError(error), at: Date.now() };
|
|
1389
|
+
throw error;
|
|
1390
|
+
}
|
|
1391
|
+
).finally(() => {
|
|
1392
|
+
pending = void 0;
|
|
1393
|
+
});
|
|
1394
|
+
return pending;
|
|
1395
|
+
},
|
|
1396
|
+
status() {
|
|
1397
|
+
if (runtime) return { available: true, state: "ready" };
|
|
1398
|
+
if (pending) return { available: false, state: "connecting" };
|
|
1399
|
+
if (!failure) return { available: false, state: "idle" };
|
|
1400
|
+
return {
|
|
1401
|
+
available: false,
|
|
1402
|
+
state: "unavailable",
|
|
1403
|
+
error: failure.error,
|
|
1404
|
+
retryInMs: Math.max(0, failure.at + retryMs - Date.now())
|
|
1405
|
+
};
|
|
1406
|
+
},
|
|
1407
|
+
async close() {
|
|
1408
|
+
closed = true;
|
|
1409
|
+
await pending?.catch(() => void 0);
|
|
1410
|
+
const current = runtime;
|
|
1411
|
+
runtime = void 0;
|
|
1412
|
+
await current?.close();
|
|
1413
|
+
}
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
function describeError(error) {
|
|
1417
|
+
const err = error;
|
|
1418
|
+
const text = err?.message || err?.code || String(error);
|
|
1419
|
+
return String(redactSecrets(text)).slice(0, 300);
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// src/mcp/server.ts
|
|
1423
|
+
async function startMcpServer(options = {}) {
|
|
1424
|
+
const logger = pino({ level: process.env.LOG_LEVEL ?? "info" }, process.stderr);
|
|
1425
|
+
let projectRoot;
|
|
1426
|
+
try {
|
|
1427
|
+
projectRoot = await resolveInitializedProjectRoot(
|
|
1428
|
+
options.config?.projectRoot ?? process.env.CODEBUDDY_PROJECT_ROOT
|
|
1429
|
+
);
|
|
1430
|
+
} catch (error) {
|
|
1431
|
+
logger.error(error.message);
|
|
1432
|
+
await startSetupServer(error.message);
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
const config = {
|
|
1436
|
+
...options.config ?? await loadRuntimeConfig(projectRoot, { requireDatabase: false }),
|
|
1437
|
+
projectRoot
|
|
1438
|
+
};
|
|
1439
|
+
(config.postgresUrl ? codeBuddyConfigSchema : codeBuddyConfigSchema.omit({ postgresUrl: true })).parse(config);
|
|
1440
|
+
const toolProfile = resolveToolProfile();
|
|
1441
|
+
const importMemory = async (memory, respace) => {
|
|
1442
|
+
try {
|
|
1443
|
+
const result = await memory.reindexMemory(respace ? {} : { respace: false });
|
|
1444
|
+
for (const failure of result.failures) {
|
|
1445
|
+
logger.error(`[sync] memory file skipped (${failure.id}): ${failure.error}`);
|
|
1446
|
+
}
|
|
1447
|
+
} catch (error) {
|
|
1448
|
+
logger.error(`[sync] Markdown memory import failed: ${error.message}`);
|
|
1449
|
+
}
|
|
1450
|
+
};
|
|
1451
|
+
const database = createLazyDatabase(async () => {
|
|
1452
|
+
try {
|
|
1453
|
+
const runtime = await createRuntime(config, { autoBootstrap: true }).catch(
|
|
1454
|
+
(error) => {
|
|
1455
|
+
void startBundledPostgres(config.postgresUrl, error, logger).then((started) => {
|
|
1456
|
+
if (started) retryUntilConnected();
|
|
1457
|
+
});
|
|
1458
|
+
throw error;
|
|
1459
|
+
}
|
|
1460
|
+
);
|
|
1461
|
+
await importMemory(runtime.memory, true);
|
|
1462
|
+
logger.info("PostgreSQL connected; memory tools available");
|
|
1463
|
+
return runtime;
|
|
1464
|
+
} catch (error) {
|
|
1465
|
+
logger.warn(
|
|
1466
|
+
`PostgreSQL unavailable; serving file-based tools only: ${error.message}`
|
|
1467
|
+
);
|
|
1468
|
+
throw error;
|
|
1469
|
+
}
|
|
1470
|
+
});
|
|
1471
|
+
void database.get().catch(() => void 0);
|
|
1472
|
+
const retryUntilConnected = (attempt = 0) => {
|
|
1473
|
+
if (attempt >= 20 || database.status().state === "ready") return;
|
|
1474
|
+
setTimeout(() => {
|
|
1475
|
+
void database.get().then(
|
|
1476
|
+
() => void 0,
|
|
1477
|
+
() => retryUntilConnected(attempt + 1)
|
|
1478
|
+
);
|
|
1479
|
+
}, 6e3).unref();
|
|
1480
|
+
};
|
|
1481
|
+
const sync = startProjectSync({
|
|
1482
|
+
repositoryRoot: projectRoot,
|
|
1483
|
+
...config.graphify ? { graphify: config.graphify } : {},
|
|
1484
|
+
logger,
|
|
1485
|
+
// Hand edits re-import live once PostgreSQL is connected; until then the
|
|
1486
|
+
// next successful connect imports everything.
|
|
1487
|
+
onMemoryChange: async () => {
|
|
1488
|
+
if (database.status().state === "ready") {
|
|
1489
|
+
await importMemory((await database.get()).memory, false);
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
});
|
|
1493
|
+
const server = new McpServer({ name: "codebuddy", version: VERSION });
|
|
1494
|
+
registerCodeBuddyTools(server, { config, database }, toolProfile);
|
|
1495
|
+
const shutdown = async () => {
|
|
1496
|
+
logger.info("codebuddy MCP server shutting down");
|
|
1497
|
+
await server.close();
|
|
1498
|
+
await sync.close();
|
|
1499
|
+
await database.close();
|
|
1500
|
+
};
|
|
1501
|
+
let stopping;
|
|
1502
|
+
const stop = () => {
|
|
1503
|
+
stopping ??= shutdown().finally(() => process.exit(0));
|
|
1504
|
+
};
|
|
1505
|
+
process.once("SIGINT", stop);
|
|
1506
|
+
process.once("SIGTERM", stop);
|
|
1507
|
+
process.stdin.once("end", stop);
|
|
1508
|
+
try {
|
|
1509
|
+
await sync.ready;
|
|
1510
|
+
await server.connect(new StdioServerTransport());
|
|
1511
|
+
} catch (error) {
|
|
1512
|
+
await sync.close();
|
|
1513
|
+
await database.close();
|
|
1514
|
+
throw error;
|
|
1515
|
+
}
|
|
1516
|
+
logger.info(`codebuddy MCP server started over stdio (tool profile: ${toolProfile})`);
|
|
1517
|
+
}
|
|
1518
|
+
async function startSetupServer(problem) {
|
|
1519
|
+
const fix = [
|
|
1520
|
+
"Open your project folder in a terminal and run `codebuddy use` (or `codebuddy init`) if it has no .codebuddy/config.json.",
|
|
1521
|
+
"Then register CodeBuddy from that folder so the client starts it there: `codebuddy claude install` (Claude Desktop) or `codebuddy codex install` (Codex), and restart the client.",
|
|
1522
|
+
"Or set CODEBUDDY_PROJECT_ROOT to the project's absolute path in this server's env."
|
|
1523
|
+
];
|
|
1524
|
+
const server = new McpServer(
|
|
1525
|
+
{ name: "codebuddy", version: VERSION },
|
|
1526
|
+
{
|
|
1527
|
+
instructions: `CodeBuddy is connected but not serving a project: ${problem} To fix it: ${fix.join(" ")}`
|
|
1528
|
+
}
|
|
1529
|
+
);
|
|
1530
|
+
server.registerTool(
|
|
1531
|
+
"health",
|
|
1532
|
+
{
|
|
1533
|
+
title: "Health",
|
|
1534
|
+
description: "Report CodeBuddy's status. This server has no project yet; the result says why and how to fix it."
|
|
1535
|
+
},
|
|
1536
|
+
async () => serializeToolResult({ ok: false, project: null, problem, fix })
|
|
1537
|
+
);
|
|
1538
|
+
const stop = () => void server.close().finally(() => process.exit(0));
|
|
1539
|
+
process.once("SIGINT", stop);
|
|
1540
|
+
process.once("SIGTERM", stop);
|
|
1541
|
+
process.stdin.once("end", stop);
|
|
1542
|
+
await server.connect(new StdioServerTransport());
|
|
1543
|
+
}
|
|
1544
|
+
var triedBundledPostgres = false;
|
|
1545
|
+
async function startBundledPostgres(postgresUrl, error, logger) {
|
|
1546
|
+
if (triedBundledPostgres || process.env.CODEBUDDY_DOCKER_AUTOSTART === "0") return false;
|
|
1547
|
+
if (!isBundledPostgres(postgresUrl) || !isConnectionRefused(error)) return false;
|
|
1548
|
+
triedBundledPostgres = true;
|
|
1549
|
+
const { isDockerDaemonRunning } = await import("./docker-autostart-KDME7TVC.js");
|
|
1550
|
+
if (!await isDockerDaemonRunning()) {
|
|
1551
|
+
logger.warn(
|
|
1552
|
+
"PostgreSQL is down and Docker is not running; start Docker Desktop, then `codebuddy postgres up`."
|
|
1553
|
+
);
|
|
1554
|
+
return false;
|
|
1555
|
+
}
|
|
1556
|
+
const { execFile } = await import("child_process");
|
|
1557
|
+
const started = await new Promise(
|
|
1558
|
+
(done) => execFile(
|
|
1559
|
+
"docker",
|
|
1560
|
+
["start", "codebuddy-postgres"],
|
|
1561
|
+
{ timeout: 3e4 },
|
|
1562
|
+
(failed) => done(failed === null)
|
|
1563
|
+
)
|
|
1564
|
+
);
|
|
1565
|
+
if (started) {
|
|
1566
|
+
logger.info("Started the stopped codebuddy-postgres container; retrying PostgreSQL.");
|
|
1567
|
+
return true;
|
|
1568
|
+
}
|
|
1569
|
+
const { postgresUp } = await import("./postgres-docker-QQZQ2ABK.js");
|
|
1570
|
+
const result = await postgresUp().catch(() => ({ code: 1, stderr: "docker compose failed" }));
|
|
1571
|
+
if (result.code === 0) {
|
|
1572
|
+
logger.info("Started the bundled PostgreSQL container; retrying PostgreSQL.");
|
|
1573
|
+
return true;
|
|
1574
|
+
}
|
|
1575
|
+
logger.warn(
|
|
1576
|
+
`Could not start the bundled PostgreSQL container: ${String(result.stderr).trim().slice(0, 200)}`
|
|
1577
|
+
);
|
|
1578
|
+
return false;
|
|
1579
|
+
}
|
|
1580
|
+
function isBundledPostgres(postgresUrl) {
|
|
1581
|
+
try {
|
|
1582
|
+
const url = new URL(postgresUrl);
|
|
1583
|
+
return ["localhost", "127.0.0.1", "::1", "[::1]"].includes(url.hostname) && (url.port === "" || url.port === "5432") && url.username === "codebuddy";
|
|
1584
|
+
} catch {
|
|
1585
|
+
return false;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
function isConnectionRefused(error) {
|
|
1589
|
+
for (let current = error; current; ) {
|
|
1590
|
+
if (current.code === "ECONNREFUSED") return true;
|
|
1591
|
+
if (Array.isArray(current.errors) && current.errors.some((inner) => isConnectionRefused(inner))) {
|
|
1592
|
+
return true;
|
|
1593
|
+
}
|
|
1594
|
+
current = current.cause;
|
|
1595
|
+
}
|
|
1596
|
+
return false;
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
export {
|
|
1600
|
+
migrateMemoryToFiles,
|
|
1601
|
+
watchRepository,
|
|
1602
|
+
generateSuggestions,
|
|
1603
|
+
mcpToolInputSchemas,
|
|
1604
|
+
registerCodeBuddyTools,
|
|
1605
|
+
startMcpServer
|
|
1606
|
+
};
|
|
1607
|
+
//# sourceMappingURL=chunk-IMFUYEZA.js.map
|