@systemdox/mcp-server 0.1.11
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 +138 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1891 -0
- package/dist/http.d.ts +30 -0
- package/dist/http.js +1623 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1561 -0
- package/dist/lib/api-client.d.ts +14 -0
- package/dist/lib/api-client.js +118 -0
- package/dist/lib/cache.d.ts +9 -0
- package/dist/lib/config.d.ts +7 -0
- package/dist/lib/context-builder.d.ts +78 -0
- package/dist/lib/context-builder.js +575 -0
- package/dist/lib/deprecation.d.ts +11 -0
- package/dist/lib/language.d.ts +7 -0
- package/dist/lib/scoring.d.ts +35 -0
- package/dist/server.d.ts +8 -0
- package/dist/tools/create-guardrail.d.ts +3 -0
- package/dist/tools/create-project-rule.d.ts +3 -0
- package/dist/tools/create-prompt-template.d.ts +3 -0
- package/dist/tools/get-context.d.ts +3 -0
- package/dist/tools/get-docs.d.ts +3 -0
- package/dist/tools/get-guardrails.d.ts +15 -0
- package/dist/tools/get-planning-context.d.ts +3 -0
- package/dist/tools/get-project-rules.d.ts +9 -0
- package/dist/tools/get-prompt-templates.d.ts +10 -0
- package/dist/tools/get-specs.d.ts +3 -0
- package/dist/tools/get-tech-stack.d.ts +3 -0
- package/dist/tools/report-decision.d.ts +3 -0
- package/dist/tools/update-spec.d.ts +3 -0
- package/package.json +65 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1891 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// src/lib/config.ts
|
|
13
|
+
function loadConfig() {
|
|
14
|
+
const apiKey = process.env.SYSTEMDOX_API_KEY;
|
|
15
|
+
if (!apiKey) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
"SYSTEMDOX_API_KEY is required. Generate one at your SystemDox settings page."
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
apiKey,
|
|
22
|
+
apiUrl: process.env.SYSTEMDOX_API_URL ?? "https://api.puglieseweb.com/systemdox",
|
|
23
|
+
cacheTtlMs: parseInt(process.env.SYSTEMDOX_CACHE_TTL ?? "300", 10) * 1e3,
|
|
24
|
+
defaultOrg: process.env.SYSTEMDOX_DEFAULT_ORG
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
var init_config = __esm({
|
|
28
|
+
"src/lib/config.ts"() {
|
|
29
|
+
"use strict";
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// src/lib/cache.ts
|
|
34
|
+
var ResponseCache;
|
|
35
|
+
var init_cache = __esm({
|
|
36
|
+
"src/lib/cache.ts"() {
|
|
37
|
+
"use strict";
|
|
38
|
+
ResponseCache = class {
|
|
39
|
+
store = /* @__PURE__ */ new Map();
|
|
40
|
+
ttlMs;
|
|
41
|
+
constructor(ttlMs) {
|
|
42
|
+
this.ttlMs = ttlMs;
|
|
43
|
+
}
|
|
44
|
+
get(key) {
|
|
45
|
+
const entry = this.store.get(key);
|
|
46
|
+
if (!entry) return void 0;
|
|
47
|
+
if (Date.now() > entry.expiresAt) {
|
|
48
|
+
this.store.delete(key);
|
|
49
|
+
return void 0;
|
|
50
|
+
}
|
|
51
|
+
return entry.data;
|
|
52
|
+
}
|
|
53
|
+
set(key, data) {
|
|
54
|
+
this.store.set(key, {
|
|
55
|
+
data,
|
|
56
|
+
expiresAt: Date.now() + this.ttlMs
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
invalidate(key) {
|
|
60
|
+
this.store.delete(key);
|
|
61
|
+
}
|
|
62
|
+
clear() {
|
|
63
|
+
this.store.clear();
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// src/lib/api-client.ts
|
|
70
|
+
var SystemDoxApiClient;
|
|
71
|
+
var init_api_client = __esm({
|
|
72
|
+
"src/lib/api-client.ts"() {
|
|
73
|
+
"use strict";
|
|
74
|
+
init_cache();
|
|
75
|
+
SystemDoxApiClient = class {
|
|
76
|
+
config;
|
|
77
|
+
cache;
|
|
78
|
+
constructor(config2) {
|
|
79
|
+
this.config = config2;
|
|
80
|
+
this.cache = new ResponseCache(config2.cacheTtlMs);
|
|
81
|
+
}
|
|
82
|
+
async validate() {
|
|
83
|
+
const res = await this.fetchRaw("/v1/health");
|
|
84
|
+
if (!res.ok) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`SystemDox API authentication failed (${res.status}). Check your SYSTEMDOX_API_KEY.`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async get(path, params) {
|
|
91
|
+
const url = this.buildUrl(path, params);
|
|
92
|
+
const cacheKey = url.toString();
|
|
93
|
+
const cached = this.cache.get(cacheKey);
|
|
94
|
+
if (cached !== void 0) return cached;
|
|
95
|
+
const res = await this.fetchRaw(path, params);
|
|
96
|
+
if (!res.ok) {
|
|
97
|
+
const body = await res.text();
|
|
98
|
+
throw new Error(`SystemDox API error ${res.status}: ${body}`);
|
|
99
|
+
}
|
|
100
|
+
const data = await res.json();
|
|
101
|
+
this.cache.set(cacheKey, data);
|
|
102
|
+
return data;
|
|
103
|
+
}
|
|
104
|
+
async post(path, body) {
|
|
105
|
+
return this.mutate("POST", path, body);
|
|
106
|
+
}
|
|
107
|
+
async put(path, body) {
|
|
108
|
+
return this.mutate("PUT", path, body);
|
|
109
|
+
}
|
|
110
|
+
async mutate(method, path, body) {
|
|
111
|
+
const url = this.buildUrl(path);
|
|
112
|
+
const res = await fetch(url, {
|
|
113
|
+
method,
|
|
114
|
+
headers: {
|
|
115
|
+
// The API Gateway authorizer only reads the Authorization header;
|
|
116
|
+
// X-API-Key is kept for backwards compatibility.
|
|
117
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
118
|
+
"X-API-Key": this.config.apiKey,
|
|
119
|
+
"Content-Type": "application/json"
|
|
120
|
+
},
|
|
121
|
+
body: JSON.stringify(body)
|
|
122
|
+
});
|
|
123
|
+
if (!res.ok) {
|
|
124
|
+
const text = await res.text();
|
|
125
|
+
throw new Error(`SystemDox API error ${res.status}: ${text}`);
|
|
126
|
+
}
|
|
127
|
+
return await res.json();
|
|
128
|
+
}
|
|
129
|
+
invalidateCache(path) {
|
|
130
|
+
if (path) {
|
|
131
|
+
this.cache.invalidate(this.buildUrl(path).toString());
|
|
132
|
+
} else {
|
|
133
|
+
this.cache.clear();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
buildUrl(path, params) {
|
|
137
|
+
const base = this.config.apiUrl.replace(/\/+$/, "");
|
|
138
|
+
const url = new URL(`${base}${path.startsWith("/") ? "" : "/"}${path}`);
|
|
139
|
+
if (params) {
|
|
140
|
+
for (const [key, value] of Object.entries(params)) {
|
|
141
|
+
url.searchParams.set(key, value);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return url;
|
|
145
|
+
}
|
|
146
|
+
async fetchRaw(path, params) {
|
|
147
|
+
const url = this.buildUrl(path, params);
|
|
148
|
+
return fetch(url, {
|
|
149
|
+
headers: {
|
|
150
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
151
|
+
"X-API-Key": this.config.apiKey,
|
|
152
|
+
Accept: "application/json"
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// src/lib/language.ts
|
|
161
|
+
function detectLanguage(filePath) {
|
|
162
|
+
const ext = filePath.slice(filePath.lastIndexOf("."));
|
|
163
|
+
return EXTENSION_MAP[ext];
|
|
164
|
+
}
|
|
165
|
+
function detectCategories(filePaths) {
|
|
166
|
+
const categories = /* @__PURE__ */ new Set();
|
|
167
|
+
for (const filePath of filePaths) {
|
|
168
|
+
const langInfo = detectLanguage(filePath);
|
|
169
|
+
if (langInfo) {
|
|
170
|
+
for (const cat of langInfo.categories) {
|
|
171
|
+
categories.add(cat);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
for (const rule of PATH_CATEGORY_RULES) {
|
|
175
|
+
if (rule.pattern.test(filePath)) {
|
|
176
|
+
for (const cat of rule.categories) {
|
|
177
|
+
categories.add(cat);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return Array.from(categories);
|
|
183
|
+
}
|
|
184
|
+
function detectLanguages(filePaths) {
|
|
185
|
+
const languages = /* @__PURE__ */ new Set();
|
|
186
|
+
for (const filePath of filePaths) {
|
|
187
|
+
const info = detectLanguage(filePath);
|
|
188
|
+
if (info) languages.add(info.language);
|
|
189
|
+
}
|
|
190
|
+
return Array.from(languages);
|
|
191
|
+
}
|
|
192
|
+
var EXTENSION_MAP, PATH_CATEGORY_RULES;
|
|
193
|
+
var init_language = __esm({
|
|
194
|
+
"src/lib/language.ts"() {
|
|
195
|
+
"use strict";
|
|
196
|
+
EXTENSION_MAP = {
|
|
197
|
+
".py": { language: "python", categories: ["api", "backend", "data-access"] },
|
|
198
|
+
".ts": { language: "typescript", categories: ["frontend", "api"] },
|
|
199
|
+
".tsx": { language: "typescript", categories: ["frontend"] },
|
|
200
|
+
".js": { language: "javascript", categories: ["frontend"] },
|
|
201
|
+
".jsx": { language: "javascript", categories: ["frontend"] },
|
|
202
|
+
".yaml": {
|
|
203
|
+
language: "cloudformation",
|
|
204
|
+
categories: ["infrastructure", "architecture"]
|
|
205
|
+
},
|
|
206
|
+
".yml": {
|
|
207
|
+
language: "cloudformation",
|
|
208
|
+
categories: ["infrastructure", "architecture"]
|
|
209
|
+
},
|
|
210
|
+
".json": { language: "json", categories: ["configuration"] },
|
|
211
|
+
".css": { language: "css", categories: ["frontend"] },
|
|
212
|
+
".html": { language: "html", categories: ["frontend"] },
|
|
213
|
+
".sql": { language: "sql", categories: ["data-access"] },
|
|
214
|
+
".sh": { language: "shell", categories: ["infrastructure"] },
|
|
215
|
+
".bash": { language: "shell", categories: ["infrastructure"] },
|
|
216
|
+
".tf": { language: "terraform", categories: ["infrastructure"] },
|
|
217
|
+
".md": { language: "markdown", categories: ["documentation"] }
|
|
218
|
+
};
|
|
219
|
+
PATH_CATEGORY_RULES = [
|
|
220
|
+
{ pattern: /backend\/functions\//, categories: ["api", "backend"] },
|
|
221
|
+
{ pattern: /backend\/layers\//, categories: ["backend"] },
|
|
222
|
+
{ pattern: /backend\//, categories: ["backend"] },
|
|
223
|
+
{ pattern: /apps\/app\//, categories: ["frontend"] },
|
|
224
|
+
{ pattern: /apps\/website\//, categories: ["frontend"] },
|
|
225
|
+
{ pattern: /apps\/portal\//, categories: ["frontend"] },
|
|
226
|
+
{ pattern: /packages\/ui\//, categories: ["frontend"] },
|
|
227
|
+
{ pattern: /infra\//, categories: ["infrastructure"] },
|
|
228
|
+
{ pattern: /\.github\//, categories: ["infrastructure"] },
|
|
229
|
+
{ pattern: /tests?\//, categories: ["testing"] },
|
|
230
|
+
{ pattern: /spec/, categories: ["testing"] },
|
|
231
|
+
{ pattern: /docs\/adr/, categories: ["architecture"] },
|
|
232
|
+
{ pattern: /template\.yaml$/, categories: ["infrastructure", "architecture"] }
|
|
233
|
+
];
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// src/lib/deprecation.ts
|
|
238
|
+
function legacyToolDescription(canonical, description) {
|
|
239
|
+
return `DEPRECATED \u2014 use \`${canonical}\` instead. This name still works and will not be removed before ${LEGACY_TOOL_REMOVAL_EARLIEST}. ${description}`;
|
|
240
|
+
}
|
|
241
|
+
var LEGACY_TOOL_REMOVAL_EARLIEST;
|
|
242
|
+
var init_deprecation = __esm({
|
|
243
|
+
"src/lib/deprecation.ts"() {
|
|
244
|
+
"use strict";
|
|
245
|
+
LEGACY_TOOL_REMOVAL_EARLIEST = "2027-01-31";
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// src/tools/get-guardrails.ts
|
|
250
|
+
import { z } from "zod";
|
|
251
|
+
function registerGetGuardrails(server2, apiClient2) {
|
|
252
|
+
const handler = async ({
|
|
253
|
+
language,
|
|
254
|
+
category,
|
|
255
|
+
file_path
|
|
256
|
+
}) => {
|
|
257
|
+
const guardrails = await apiClient2.get("/v1/guardrails");
|
|
258
|
+
let filtered = guardrails;
|
|
259
|
+
const lang = language ?? (file_path ? detectLanguage(file_path)?.language : void 0);
|
|
260
|
+
if (lang) {
|
|
261
|
+
filtered = filtered.filter(
|
|
262
|
+
(g) => g.description.toLowerCase().includes(lang.toLowerCase()) || g.category.toLowerCase().includes(lang.toLowerCase()) || g.title.toLowerCase().includes(lang.toLowerCase())
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
if (category) {
|
|
266
|
+
filtered = filtered.filter(
|
|
267
|
+
(g) => g.category.toLowerCase() === category.toLowerCase()
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
if (filtered.length === 0) {
|
|
271
|
+
return {
|
|
272
|
+
content: [
|
|
273
|
+
{
|
|
274
|
+
type: "text",
|
|
275
|
+
text: `No checks found${lang ? ` for language "${lang}"` : ""}${category ? ` in category "${category}"` : ""}.`
|
|
276
|
+
}
|
|
277
|
+
]
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
const text = filtered.map(
|
|
281
|
+
(g) => `## ${g.title} [${g.severity}]
|
|
282
|
+
|
|
283
|
+
${g.description}
|
|
284
|
+
|
|
285
|
+
` + (g.pattern ? `**Bad pattern:**
|
|
286
|
+
\`\`\`
|
|
287
|
+
${g.pattern}
|
|
288
|
+
\`\`\`
|
|
289
|
+
|
|
290
|
+
` : "") + (g.fix ? `**Correct pattern:**
|
|
291
|
+
\`\`\`
|
|
292
|
+
${g.fix}
|
|
293
|
+
\`\`\`
|
|
294
|
+
|
|
295
|
+
` : "") + (g.fitnessTest ? `**Detection:** \`${g.fitnessTest}\`
|
|
296
|
+
` : "")
|
|
297
|
+
).join("\n---\n\n");
|
|
298
|
+
return {
|
|
299
|
+
content: [
|
|
300
|
+
{
|
|
301
|
+
type: "text",
|
|
302
|
+
text: `Found ${filtered.length} check(s):
|
|
303
|
+
|
|
304
|
+
${text}`
|
|
305
|
+
}
|
|
306
|
+
]
|
|
307
|
+
};
|
|
308
|
+
};
|
|
309
|
+
server2.registerTool(
|
|
310
|
+
"get_checks",
|
|
311
|
+
{
|
|
312
|
+
title: "Get Checks",
|
|
313
|
+
description: DESCRIPTION,
|
|
314
|
+
inputSchema: inputSchema()
|
|
315
|
+
},
|
|
316
|
+
handler
|
|
317
|
+
);
|
|
318
|
+
server2.registerTool(
|
|
319
|
+
"get_guardrails",
|
|
320
|
+
{
|
|
321
|
+
title: "Get Checks (legacy name)",
|
|
322
|
+
description: legacyToolDescription("get_checks", DESCRIPTION),
|
|
323
|
+
inputSchema: inputSchema()
|
|
324
|
+
},
|
|
325
|
+
handler
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
var DESCRIPTION, inputSchema;
|
|
329
|
+
var init_get_guardrails = __esm({
|
|
330
|
+
"src/tools/get-guardrails.ts"() {
|
|
331
|
+
"use strict";
|
|
332
|
+
init_language();
|
|
333
|
+
init_deprecation();
|
|
334
|
+
DESCRIPTION = "Retrieve SystemDox checks (banned patterns, required patterns, coding rules). Filter by language, category, or file path to get only relevant checks.";
|
|
335
|
+
inputSchema = () => ({
|
|
336
|
+
language: z.string().optional().describe(
|
|
337
|
+
"Filter by programming language (python, typescript, javascript, cloudformation)"
|
|
338
|
+
),
|
|
339
|
+
category: z.string().optional().describe(
|
|
340
|
+
"Filter by category (imports, auth, error-handling, architecture, testing, security, conventions, data-access)"
|
|
341
|
+
),
|
|
342
|
+
file_path: z.string().optional().describe(
|
|
343
|
+
"File path being edited \u2014 language is auto-detected from the extension"
|
|
344
|
+
)
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
// src/tools/get-specs.ts
|
|
350
|
+
import { z as z2 } from "zod";
|
|
351
|
+
function registerGetSpecs(server2, apiClient2) {
|
|
352
|
+
server2.registerTool(
|
|
353
|
+
"get_specs",
|
|
354
|
+
{
|
|
355
|
+
title: "Get Specs",
|
|
356
|
+
description: "Retrieve SystemDox specifications (proposal, requirements with RFC 2119 keywords, Given/When/Then scenarios). Get a specific spec by ID, filter by status, or search by keyword.",
|
|
357
|
+
inputSchema: {
|
|
358
|
+
spec_id: z2.string().optional().describe("Specific spec ID to retrieve with full content"),
|
|
359
|
+
status: z2.enum(["draft", "ready", "archived"]).optional().describe("Filter by status"),
|
|
360
|
+
search: z2.string().optional().describe("Keyword search across spec names")
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
async ({ spec_id, status, search }) => {
|
|
364
|
+
if (spec_id) {
|
|
365
|
+
const spec = await apiClient2.get(
|
|
366
|
+
`/v1/specs/${spec_id}`
|
|
367
|
+
);
|
|
368
|
+
const sections = [`# ${spec.name}
|
|
369
|
+
|
|
370
|
+
**Status:** ${spec.status}`];
|
|
371
|
+
if (spec.proposalContent) {
|
|
372
|
+
sections.push(`## Proposal
|
|
373
|
+
|
|
374
|
+
${spec.proposalContent}`);
|
|
375
|
+
}
|
|
376
|
+
if (spec.requirements.length > 0) {
|
|
377
|
+
const reqs = spec.requirements.map((r) => `### ${r.name}
|
|
378
|
+
|
|
379
|
+
${r.content}`).join("\n\n");
|
|
380
|
+
sections.push(`## Requirements
|
|
381
|
+
|
|
382
|
+
${reqs}`);
|
|
383
|
+
}
|
|
384
|
+
if (spec.taskContent) {
|
|
385
|
+
sections.push(`## Tasks
|
|
386
|
+
|
|
387
|
+
${spec.taskContent}`);
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
content: [{ type: "text", text: sections.join("\n\n") }]
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
const specs = await apiClient2.get("/v1/specs");
|
|
394
|
+
let filtered = specs;
|
|
395
|
+
if (status) {
|
|
396
|
+
filtered = filtered.filter((s) => s.status === status);
|
|
397
|
+
}
|
|
398
|
+
if (search) {
|
|
399
|
+
const term = search.toLowerCase();
|
|
400
|
+
filtered = filtered.filter(
|
|
401
|
+
(s) => s.name.toLowerCase().includes(term)
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
if (filtered.length === 0) {
|
|
405
|
+
return {
|
|
406
|
+
content: [
|
|
407
|
+
{
|
|
408
|
+
type: "text",
|
|
409
|
+
text: `No specs found${status ? ` with status "${status}"` : ""}${search ? ` matching "${search}"` : ""}.`
|
|
410
|
+
}
|
|
411
|
+
]
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
const text = filtered.map(
|
|
415
|
+
(s) => `- **${s.name}** (${s.specId}) \u2014 ${s.status}, ${s.requirementCount} requirements` + (s.repoFullName ? `, repo: ${s.repoFullName}` : "")
|
|
416
|
+
).join("\n");
|
|
417
|
+
return {
|
|
418
|
+
content: [
|
|
419
|
+
{
|
|
420
|
+
type: "text",
|
|
421
|
+
text: `Found ${filtered.length} spec(s):
|
|
422
|
+
|
|
423
|
+
${text}`
|
|
424
|
+
}
|
|
425
|
+
]
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
var init_get_specs = __esm({
|
|
431
|
+
"src/tools/get-specs.ts"() {
|
|
432
|
+
"use strict";
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// src/tools/get-docs.ts
|
|
437
|
+
import { z as z3 } from "zod";
|
|
438
|
+
function matchGlob(path, pattern) {
|
|
439
|
+
const regex = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\{\{GLOBSTAR\}\}/g, ".*");
|
|
440
|
+
return new RegExp(`^${regex}$`).test(path);
|
|
441
|
+
}
|
|
442
|
+
function registerGetDocs(server2, apiClient2) {
|
|
443
|
+
server2.registerTool(
|
|
444
|
+
"get_docs",
|
|
445
|
+
{
|
|
446
|
+
title: "Get Docs",
|
|
447
|
+
description: "Retrieve synced documentation from connected GitHub repositories. Filter by repo, file path pattern, or keyword search.",
|
|
448
|
+
inputSchema: {
|
|
449
|
+
repo: z3.string().optional().describe("Filter by repo (owner/repo format)"),
|
|
450
|
+
path: z3.string().optional().describe("File path glob pattern (e.g., docs/adr/*.md)"),
|
|
451
|
+
search: z3.string().optional().describe("Keyword search across document content"),
|
|
452
|
+
doc_id: z3.string().optional().describe(
|
|
453
|
+
"Specific document ID to retrieve full content"
|
|
454
|
+
)
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
async ({ repo, path, search, doc_id }) => {
|
|
458
|
+
if (doc_id) {
|
|
459
|
+
const doc = await apiClient2.get(
|
|
460
|
+
`/v1/docs/${encodeURIComponent(doc_id)}/content`
|
|
461
|
+
);
|
|
462
|
+
if (doc.error) {
|
|
463
|
+
return {
|
|
464
|
+
content: [
|
|
465
|
+
{
|
|
466
|
+
type: "text",
|
|
467
|
+
text: `Error retrieving document: ${doc.error}`
|
|
468
|
+
}
|
|
469
|
+
],
|
|
470
|
+
isError: true
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
content: [{ type: "text", text: doc.content }]
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
const params = {};
|
|
478
|
+
if (repo) params.repo = repo;
|
|
479
|
+
if (search) params.q = search;
|
|
480
|
+
const response = await apiClient2.get(
|
|
481
|
+
"/v1/docs",
|
|
482
|
+
params
|
|
483
|
+
);
|
|
484
|
+
let items = response.items;
|
|
485
|
+
if (path) {
|
|
486
|
+
items = items.filter((item) => matchGlob(item.path, path));
|
|
487
|
+
}
|
|
488
|
+
if (items.length === 0) {
|
|
489
|
+
return {
|
|
490
|
+
content: [
|
|
491
|
+
{
|
|
492
|
+
type: "text",
|
|
493
|
+
text: `No documents found${repo ? ` in ${repo}` : ""}${path ? ` matching "${path}"` : ""}${search ? ` for "${search}"` : ""}.`
|
|
494
|
+
}
|
|
495
|
+
]
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
const text = items.map(
|
|
499
|
+
(d) => `- **${d.title}** \u2014 \`${d.repo_full_name}/${d.path}\` (${d.doc_type}, ${d.size} bytes, synced ${d.synced_at})` + (d.snippet ? `
|
|
500
|
+
> ${d.snippet}` : "")
|
|
501
|
+
).join("\n");
|
|
502
|
+
return {
|
|
503
|
+
content: [
|
|
504
|
+
{
|
|
505
|
+
type: "text",
|
|
506
|
+
text: `Found ${items.length} document(s) (${response.total} total):
|
|
507
|
+
|
|
508
|
+
${text}
|
|
509
|
+
|
|
510
|
+
Use doc_id to retrieve full content.`
|
|
511
|
+
}
|
|
512
|
+
]
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
var init_get_docs = __esm({
|
|
518
|
+
"src/tools/get-docs.ts"() {
|
|
519
|
+
"use strict";
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
// src/tools/get-tech-stack.ts
|
|
524
|
+
function registerGetTechStack(server2, apiClient2) {
|
|
525
|
+
server2.registerTool(
|
|
526
|
+
"get_tech_stack",
|
|
527
|
+
{
|
|
528
|
+
title: "Get Tech Stack",
|
|
529
|
+
description: "Retrieve the configured technology stack (languages, frameworks, databases, infrastructure) with versions and notes."
|
|
530
|
+
},
|
|
531
|
+
async () => {
|
|
532
|
+
const stack = await apiClient2.get(
|
|
533
|
+
"/v1/ai/settings/stack"
|
|
534
|
+
);
|
|
535
|
+
if (stack.length === 0) {
|
|
536
|
+
return {
|
|
537
|
+
content: [
|
|
538
|
+
{
|
|
539
|
+
type: "text",
|
|
540
|
+
text: "No tech stack entries configured."
|
|
541
|
+
}
|
|
542
|
+
]
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
546
|
+
for (const entry of stack) {
|
|
547
|
+
const list = grouped.get(entry.category) ?? [];
|
|
548
|
+
list.push(entry);
|
|
549
|
+
grouped.set(entry.category, list);
|
|
550
|
+
}
|
|
551
|
+
const sections = Array.from(grouped.entries()).map(([category, entries]) => {
|
|
552
|
+
const items = entries.map(
|
|
553
|
+
(e) => `- ${e.name}${e.version ? ` v${e.version}` : ""}${e.notes ? ` \u2014 ${e.notes}` : ""}`
|
|
554
|
+
).join("\n");
|
|
555
|
+
return `### ${category}
|
|
556
|
+
${items}`;
|
|
557
|
+
}).join("\n\n");
|
|
558
|
+
return {
|
|
559
|
+
content: [
|
|
560
|
+
{
|
|
561
|
+
type: "text",
|
|
562
|
+
text: `Tech stack (${stack.length} entries):
|
|
563
|
+
|
|
564
|
+
${sections}`
|
|
565
|
+
}
|
|
566
|
+
]
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
var init_get_tech_stack = __esm({
|
|
572
|
+
"src/tools/get-tech-stack.ts"() {
|
|
573
|
+
"use strict";
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
// src/tools/get-planning-context.ts
|
|
578
|
+
import { z as z4 } from "zod";
|
|
579
|
+
function registerGetPlanningContext(server2, apiClient2) {
|
|
580
|
+
server2.registerTool(
|
|
581
|
+
"get_planning_context",
|
|
582
|
+
{
|
|
583
|
+
title: "Get Planning Context",
|
|
584
|
+
description: "Retrieve org-wide planning context: connected repos with summaries, cross-repo dependencies, ADRs, and active specs. Optionally focus on a target repo.",
|
|
585
|
+
inputSchema: {
|
|
586
|
+
target_repo: z4.string().optional().describe(
|
|
587
|
+
"Focus on a specific repo (owner/repo format) to get its CLAUDE.md and relevant context"
|
|
588
|
+
)
|
|
589
|
+
}
|
|
590
|
+
},
|
|
591
|
+
async ({ target_repo }) => {
|
|
592
|
+
const params = {};
|
|
593
|
+
if (target_repo) params.targetRepo = target_repo;
|
|
594
|
+
const ctx = await apiClient2.get(
|
|
595
|
+
"/v1/org/planning-context",
|
|
596
|
+
params
|
|
597
|
+
);
|
|
598
|
+
const sections = [];
|
|
599
|
+
if (ctx.repoSummaries.length > 0) {
|
|
600
|
+
const repos = ctx.repoSummaries.map(
|
|
601
|
+
(r) => `#### ${r.repo}
|
|
602
|
+
${r.purpose}
|
|
603
|
+
|
|
604
|
+
${r.summary}` + (r.conventions.length > 0 ? `
|
|
605
|
+
|
|
606
|
+
**Conventions:**
|
|
607
|
+
${r.conventions.map((c) => `- ${c}`).join("\n")}` : "")
|
|
608
|
+
).join("\n\n");
|
|
609
|
+
sections.push(`## Repositories (${ctx.repoSummaries.length})
|
|
610
|
+
|
|
611
|
+
${repos}`);
|
|
612
|
+
}
|
|
613
|
+
if (ctx.dependencies.length > 0) {
|
|
614
|
+
const deps = ctx.dependencies.map((d) => `- ${d.from} \u2192 ${d.to}`).join("\n");
|
|
615
|
+
sections.push(`## Cross-Repo Dependencies
|
|
616
|
+
|
|
617
|
+
${deps}`);
|
|
618
|
+
}
|
|
619
|
+
if (ctx.adrs.length > 0) {
|
|
620
|
+
const adrs = ctx.adrs.map((a) => `- ${a.title}${a.repo ? ` (${a.repo})` : ""}`).join("\n");
|
|
621
|
+
sections.push(`## Architecture Decision Records (${ctx.adrs.length})
|
|
622
|
+
|
|
623
|
+
${adrs}`);
|
|
624
|
+
}
|
|
625
|
+
if (ctx.specs.length > 0) {
|
|
626
|
+
const specs = ctx.specs.map((s) => `- ${s.name} [${s.status}]`).join("\n");
|
|
627
|
+
sections.push(`## Specs (${ctx.specs.length})
|
|
628
|
+
|
|
629
|
+
${specs}`);
|
|
630
|
+
}
|
|
631
|
+
return {
|
|
632
|
+
content: [
|
|
633
|
+
{
|
|
634
|
+
type: "text",
|
|
635
|
+
text: sections.join("\n\n") || "No planning context available."
|
|
636
|
+
}
|
|
637
|
+
]
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
var init_get_planning_context = __esm({
|
|
643
|
+
"src/tools/get-planning-context.ts"() {
|
|
644
|
+
"use strict";
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
// src/lib/scoring.ts
|
|
649
|
+
function estimateTokens(text) {
|
|
650
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
651
|
+
}
|
|
652
|
+
function truncateToTokens(text, maxTokens) {
|
|
653
|
+
const maxChars = Math.max(0, maxTokens) * CHARS_PER_TOKEN;
|
|
654
|
+
if (text.length <= maxChars) return { text, truncated: false };
|
|
655
|
+
const head = text.slice(0, maxChars);
|
|
656
|
+
const floor = Math.floor(maxChars * 0.75);
|
|
657
|
+
const paragraph = head.lastIndexOf("\n\n");
|
|
658
|
+
const line = head.lastIndexOf("\n");
|
|
659
|
+
let cut = maxChars;
|
|
660
|
+
if (paragraph >= floor) cut = paragraph;
|
|
661
|
+
else if (line >= floor) cut = line;
|
|
662
|
+
return { text: head.slice(0, cut).trimEnd(), truncated: true };
|
|
663
|
+
}
|
|
664
|
+
function scorePathMatch(item, languages, categories) {
|
|
665
|
+
if (languages.length === 0 && categories.length === 0) return 0.5;
|
|
666
|
+
let score = 0;
|
|
667
|
+
const itemLang = item.language?.toLowerCase() ?? "";
|
|
668
|
+
const itemCat = item.category?.toLowerCase() ?? "";
|
|
669
|
+
if (itemLang && languages.some((l) => itemLang.includes(l))) {
|
|
670
|
+
score += 0.6;
|
|
671
|
+
}
|
|
672
|
+
if (itemCat && categories.some((c) => itemCat.includes(c))) {
|
|
673
|
+
score += 0.4;
|
|
674
|
+
}
|
|
675
|
+
return Math.min(score, 1);
|
|
676
|
+
}
|
|
677
|
+
function scoreKeywordMatch(item, keywords) {
|
|
678
|
+
if (keywords.length === 0) return 0;
|
|
679
|
+
const searchable = `${item.title} ${item.content}`.toLowerCase();
|
|
680
|
+
let matches = 0;
|
|
681
|
+
for (const kw of keywords) {
|
|
682
|
+
if (searchable.includes(kw)) matches++;
|
|
683
|
+
}
|
|
684
|
+
return Math.min(matches / keywords.length, 1);
|
|
685
|
+
}
|
|
686
|
+
function scoreRecency(item) {
|
|
687
|
+
if (!item.updatedAt) return 0.5;
|
|
688
|
+
const now = Date.now();
|
|
689
|
+
const updated = new Date(item.updatedAt).getTime();
|
|
690
|
+
const daysSince = (now - updated) / (1e3 * 60 * 60 * 24);
|
|
691
|
+
if (daysSince <= 7) return 1;
|
|
692
|
+
if (daysSince <= 30) return 0.75;
|
|
693
|
+
if (daysSince <= 90) return 0.5;
|
|
694
|
+
return 0.25;
|
|
695
|
+
}
|
|
696
|
+
function scoreSeverity(item) {
|
|
697
|
+
if (!item.severity) return 0.5;
|
|
698
|
+
return SEVERITY_SCORES[item.severity] ?? 0.5;
|
|
699
|
+
}
|
|
700
|
+
function extractKeywords(text) {
|
|
701
|
+
const stopWords = /* @__PURE__ */ new Set([
|
|
702
|
+
"the",
|
|
703
|
+
"a",
|
|
704
|
+
"an",
|
|
705
|
+
"is",
|
|
706
|
+
"are",
|
|
707
|
+
"was",
|
|
708
|
+
"were",
|
|
709
|
+
"be",
|
|
710
|
+
"been",
|
|
711
|
+
"being",
|
|
712
|
+
"have",
|
|
713
|
+
"has",
|
|
714
|
+
"had",
|
|
715
|
+
"do",
|
|
716
|
+
"does",
|
|
717
|
+
"did",
|
|
718
|
+
"will",
|
|
719
|
+
"would",
|
|
720
|
+
"could",
|
|
721
|
+
"should",
|
|
722
|
+
"may",
|
|
723
|
+
"might",
|
|
724
|
+
"can",
|
|
725
|
+
"shall",
|
|
726
|
+
"to",
|
|
727
|
+
"of",
|
|
728
|
+
"in",
|
|
729
|
+
"for",
|
|
730
|
+
"on",
|
|
731
|
+
"with",
|
|
732
|
+
"at",
|
|
733
|
+
"by",
|
|
734
|
+
"from",
|
|
735
|
+
"as",
|
|
736
|
+
"into",
|
|
737
|
+
"about",
|
|
738
|
+
"between",
|
|
739
|
+
"through",
|
|
740
|
+
"after",
|
|
741
|
+
"before",
|
|
742
|
+
"during",
|
|
743
|
+
"without",
|
|
744
|
+
"this",
|
|
745
|
+
"that",
|
|
746
|
+
"these",
|
|
747
|
+
"those",
|
|
748
|
+
"it",
|
|
749
|
+
"its",
|
|
750
|
+
"and",
|
|
751
|
+
"or",
|
|
752
|
+
"but",
|
|
753
|
+
"not",
|
|
754
|
+
"no",
|
|
755
|
+
"all",
|
|
756
|
+
"each",
|
|
757
|
+
"every",
|
|
758
|
+
"both",
|
|
759
|
+
"few",
|
|
760
|
+
"more",
|
|
761
|
+
"most",
|
|
762
|
+
"other",
|
|
763
|
+
"some",
|
|
764
|
+
"such",
|
|
765
|
+
"than",
|
|
766
|
+
"too",
|
|
767
|
+
"very",
|
|
768
|
+
"just",
|
|
769
|
+
"also",
|
|
770
|
+
"only",
|
|
771
|
+
"new",
|
|
772
|
+
"add",
|
|
773
|
+
"update",
|
|
774
|
+
"fix",
|
|
775
|
+
"change",
|
|
776
|
+
"make",
|
|
777
|
+
"use",
|
|
778
|
+
"get",
|
|
779
|
+
"set",
|
|
780
|
+
"need",
|
|
781
|
+
"want",
|
|
782
|
+
"implement",
|
|
783
|
+
"create"
|
|
784
|
+
]);
|
|
785
|
+
return text.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !stopWords.has(w));
|
|
786
|
+
}
|
|
787
|
+
function scoreItems(items, taskDescription, filePaths) {
|
|
788
|
+
const languages = detectLanguages(filePaths);
|
|
789
|
+
const categories = detectCategories(filePaths);
|
|
790
|
+
const keywords = extractKeywords(taskDescription);
|
|
791
|
+
return items.map((item) => {
|
|
792
|
+
const pathScore = scorePathMatch(item, languages, categories);
|
|
793
|
+
const keywordScore = scoreKeywordMatch(item, keywords);
|
|
794
|
+
const recencyScore = scoreRecency(item);
|
|
795
|
+
const severityScore = scoreSeverity(item);
|
|
796
|
+
const score = pathScore * WEIGHT_PATH + keywordScore * WEIGHT_KEYWORD + recencyScore * WEIGHT_RECENCY + severityScore * WEIGHT_SEVERITY;
|
|
797
|
+
return {
|
|
798
|
+
type: item.type,
|
|
799
|
+
id: item.id,
|
|
800
|
+
title: item.title,
|
|
801
|
+
content: item.content,
|
|
802
|
+
score,
|
|
803
|
+
tokenEstimate: estimateTokens(item.content)
|
|
804
|
+
};
|
|
805
|
+
}).sort((a, b) => b.score - a.score);
|
|
806
|
+
}
|
|
807
|
+
function selectWithinBudget(items, maxTokens) {
|
|
808
|
+
const selected = [];
|
|
809
|
+
let tokensUsed = 0;
|
|
810
|
+
for (const item of items) {
|
|
811
|
+
if (tokensUsed + item.tokenEstimate > maxTokens) continue;
|
|
812
|
+
selected.push(item);
|
|
813
|
+
tokensUsed += item.tokenEstimate;
|
|
814
|
+
}
|
|
815
|
+
return { selected, tokensUsed, totalItems: items.length };
|
|
816
|
+
}
|
|
817
|
+
var WEIGHT_PATH, WEIGHT_KEYWORD, WEIGHT_RECENCY, WEIGHT_SEVERITY, SEVERITY_SCORES, CHARS_PER_TOKEN;
|
|
818
|
+
var init_scoring = __esm({
|
|
819
|
+
"src/lib/scoring.ts"() {
|
|
820
|
+
"use strict";
|
|
821
|
+
init_language();
|
|
822
|
+
WEIGHT_PATH = 0.4;
|
|
823
|
+
WEIGHT_KEYWORD = 0.3;
|
|
824
|
+
WEIGHT_RECENCY = 0.15;
|
|
825
|
+
WEIGHT_SEVERITY = 0.15;
|
|
826
|
+
SEVERITY_SCORES = {
|
|
827
|
+
critical: 1,
|
|
828
|
+
high: 0.75,
|
|
829
|
+
medium: 0.5,
|
|
830
|
+
low: 0.25
|
|
831
|
+
};
|
|
832
|
+
CHARS_PER_TOKEN = 4;
|
|
833
|
+
}
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
// src/lib/context-builder.ts
|
|
837
|
+
function formatGuardrail(g) {
|
|
838
|
+
let text = `## ${g.title} [${g.severity}]
|
|
839
|
+
|
|
840
|
+
${g.description}`;
|
|
841
|
+
if (g.pattern) text += `
|
|
842
|
+
|
|
843
|
+
**Bad:** \`\`\`
|
|
844
|
+
${g.pattern}
|
|
845
|
+
\`\`\``;
|
|
846
|
+
if (g.fix) text += `
|
|
847
|
+
|
|
848
|
+
**Good:** \`\`\`
|
|
849
|
+
${g.fix}
|
|
850
|
+
\`\`\``;
|
|
851
|
+
return text;
|
|
852
|
+
}
|
|
853
|
+
function formatSpecHeadline(s) {
|
|
854
|
+
return `### ${s.name}
|
|
855
|
+
\`spec_id: ${s.specId}\` \xB7 ${s.status} \xB7 ${s.requirementCount} requirements`;
|
|
856
|
+
}
|
|
857
|
+
function formatDocHeadline(d) {
|
|
858
|
+
return `### ${d.title}
|
|
859
|
+
\`doc_id: ${d.id}\` \xB7 \`${d.repo_full_name}/${d.path}\` \xB7 ${d.doc_type}`;
|
|
860
|
+
}
|
|
861
|
+
function formatStack(entries) {
|
|
862
|
+
return entries.map(
|
|
863
|
+
(e) => `- ${e.name}${e.version ? ` v${e.version}` : ""}${e.notes ? ` \u2014 ${e.notes}` : ""}`
|
|
864
|
+
).join("\n");
|
|
865
|
+
}
|
|
866
|
+
function formatRules(rules) {
|
|
867
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
868
|
+
for (const r of rules) {
|
|
869
|
+
const cat = r.category || "other";
|
|
870
|
+
if (!byCategory.has(cat)) byCategory.set(cat, []);
|
|
871
|
+
byCategory.get(cat).push(r);
|
|
872
|
+
}
|
|
873
|
+
let text = "";
|
|
874
|
+
for (const [cat, catRules] of byCategory) {
|
|
875
|
+
text += `### ${cat.charAt(0).toUpperCase() + cat.slice(1)}
|
|
876
|
+
`;
|
|
877
|
+
for (const r of catRules) {
|
|
878
|
+
text += `- ${r.text}
|
|
879
|
+
`;
|
|
880
|
+
}
|
|
881
|
+
text += "\n";
|
|
882
|
+
}
|
|
883
|
+
return `## Project Rules (${rules.length})
|
|
884
|
+
|
|
885
|
+
${text.trim()}`;
|
|
886
|
+
}
|
|
887
|
+
async function fetchSpecBody(api, specId) {
|
|
888
|
+
const spec = await api.get(
|
|
889
|
+
`/v1/specs/${encodeURIComponent(specId)}`
|
|
890
|
+
);
|
|
891
|
+
const parts = [];
|
|
892
|
+
if (spec.proposalContent) {
|
|
893
|
+
parts.push(`**Proposal**
|
|
894
|
+
|
|
895
|
+
${spec.proposalContent}`);
|
|
896
|
+
}
|
|
897
|
+
for (const r of spec.requirements ?? []) {
|
|
898
|
+
parts.push(`**${r.name}**
|
|
899
|
+
|
|
900
|
+
${r.content}`);
|
|
901
|
+
}
|
|
902
|
+
if (spec.taskContent) {
|
|
903
|
+
parts.push(`**Tasks**
|
|
904
|
+
|
|
905
|
+
${spec.taskContent}`);
|
|
906
|
+
}
|
|
907
|
+
const body = parts.join("\n\n").trim();
|
|
908
|
+
if (!body) throw new Error("spec has no content");
|
|
909
|
+
return body;
|
|
910
|
+
}
|
|
911
|
+
async function fetchDocBody(api, docId) {
|
|
912
|
+
const doc = await api.get(
|
|
913
|
+
`/v1/docs/${encodeURIComponent(docId)}/content`
|
|
914
|
+
);
|
|
915
|
+
if (doc.error) throw new Error(doc.error);
|
|
916
|
+
const body = (doc.content ?? "").trim();
|
|
917
|
+
if (!body) throw new Error("document is empty");
|
|
918
|
+
return body;
|
|
919
|
+
}
|
|
920
|
+
async function hydrate(api, item, meta) {
|
|
921
|
+
try {
|
|
922
|
+
const body = meta.type === "spec" ? await fetchSpecBody(api, meta.id) : await fetchDocBody(api, meta.id);
|
|
923
|
+
return { item, meta, body };
|
|
924
|
+
} catch (err) {
|
|
925
|
+
return {
|
|
926
|
+
item,
|
|
927
|
+
meta,
|
|
928
|
+
error: err instanceof Error ? err.message : String(err)
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
async function buildContext(api, { taskDescription, filePaths = [], maxTokens }) {
|
|
933
|
+
const budget = maxTokens ?? DEFAULT_BUDGET;
|
|
934
|
+
const [guardrails, specs, docs, stack, rules] = await Promise.all([
|
|
935
|
+
api.get("/v1/guardrails").catch(() => []),
|
|
936
|
+
api.get("/v1/specs").catch(() => []),
|
|
937
|
+
api.get("/v1/docs").catch(() => ({ items: [] })),
|
|
938
|
+
api.get("/v1/ai/settings/stack").catch(() => []),
|
|
939
|
+
api.get("/v1/standards/rules").catch(() => [])
|
|
940
|
+
]);
|
|
941
|
+
const refs = /* @__PURE__ */ new Map();
|
|
942
|
+
const key = (type, id) => `${type}:${id}`;
|
|
943
|
+
const scorable = [
|
|
944
|
+
...guardrails.map((g) => ({
|
|
945
|
+
type: "guardrail",
|
|
946
|
+
id: g.id,
|
|
947
|
+
title: g.title,
|
|
948
|
+
content: formatGuardrail(g),
|
|
949
|
+
category: g.category,
|
|
950
|
+
severity: g.severity,
|
|
951
|
+
updatedAt: g.updatedAt
|
|
952
|
+
})),
|
|
953
|
+
...specs.filter((s) => s.status !== "archived").map((s) => {
|
|
954
|
+
refs.set(key("spec", s.specId), {
|
|
955
|
+
type: "spec",
|
|
956
|
+
id: s.specId,
|
|
957
|
+
title: s.name,
|
|
958
|
+
listLine: `- **${s.name}** \u2014 \`spec_id: ${s.specId}\` (${s.status}, ${s.requirementCount} requirements)`,
|
|
959
|
+
fetchHint: `get_specs(spec_id="${s.specId}")`
|
|
960
|
+
});
|
|
961
|
+
return {
|
|
962
|
+
type: "spec",
|
|
963
|
+
id: s.specId,
|
|
964
|
+
title: s.name,
|
|
965
|
+
content: formatSpecHeadline(s),
|
|
966
|
+
updatedAt: s.updatedAt
|
|
967
|
+
};
|
|
968
|
+
}),
|
|
969
|
+
...docs.items.map((d) => {
|
|
970
|
+
refs.set(key("doc", d.id), {
|
|
971
|
+
type: "doc",
|
|
972
|
+
id: d.id,
|
|
973
|
+
title: d.title,
|
|
974
|
+
listLine: `- **${d.title}** \u2014 \`doc_id: ${d.id}\` (\`${d.repo_full_name}/${d.path}\`)`,
|
|
975
|
+
fetchHint: `get_docs(doc_id="${d.id}")`
|
|
976
|
+
});
|
|
977
|
+
return {
|
|
978
|
+
type: "doc",
|
|
979
|
+
id: d.id,
|
|
980
|
+
title: d.title,
|
|
981
|
+
content: formatDocHeadline(d),
|
|
982
|
+
category: d.doc_type === "adr" ? "architecture" : void 0,
|
|
983
|
+
updatedAt: d.synced_at
|
|
984
|
+
};
|
|
985
|
+
})
|
|
986
|
+
];
|
|
987
|
+
const ranked = scoreItems(scorable, taskDescription, filePaths);
|
|
988
|
+
const sections = [];
|
|
989
|
+
let overhead = 0;
|
|
990
|
+
if (stack.length > 0) {
|
|
991
|
+
const section = `## Tech Stack
|
|
992
|
+
|
|
993
|
+
${formatStack(stack)}`;
|
|
994
|
+
sections.push(section);
|
|
995
|
+
overhead += estimateTokens(section);
|
|
996
|
+
}
|
|
997
|
+
if (rules.length > 0) {
|
|
998
|
+
const section = formatRules(rules);
|
|
999
|
+
sections.push(section);
|
|
1000
|
+
overhead += estimateTokens(section);
|
|
1001
|
+
}
|
|
1002
|
+
let remaining = budget - RESERVE_TOKENS - overhead;
|
|
1003
|
+
const rankedGuardrails = ranked.filter((i) => i.type === "guardrail");
|
|
1004
|
+
const { selected: selectedGuardrails, tokensUsed: guardrailTokens } = selectWithinBudget(
|
|
1005
|
+
rankedGuardrails,
|
|
1006
|
+
Math.max(0, Math.floor(remaining * GUARDRAIL_SHARE))
|
|
1007
|
+
);
|
|
1008
|
+
remaining -= guardrailTokens;
|
|
1009
|
+
const rankedRefs = ranked.filter(
|
|
1010
|
+
(i) => i.type === "spec" || i.type === "doc"
|
|
1011
|
+
);
|
|
1012
|
+
const avgHeadline = rankedRefs.length ? Math.ceil(
|
|
1013
|
+
rankedRefs.slice(0, MAX_HYDRATED).reduce((sum, i) => sum + i.tokenEstimate, 0) / Math.min(rankedRefs.length, MAX_HYDRATED)
|
|
1014
|
+
) : 0;
|
|
1015
|
+
const affordable = Math.floor(
|
|
1016
|
+
Math.max(0, remaining) / (MIN_BODY_TOKENS + avgHeadline)
|
|
1017
|
+
);
|
|
1018
|
+
const shortlist = rankedRefs.slice(
|
|
1019
|
+
0,
|
|
1020
|
+
Math.max(0, Math.min(MAX_HYDRATED, affordable))
|
|
1021
|
+
);
|
|
1022
|
+
for (const item of shortlist) {
|
|
1023
|
+
remaining -= item.tokenEstimate;
|
|
1024
|
+
}
|
|
1025
|
+
remaining = Math.max(0, remaining);
|
|
1026
|
+
const hydrated = await Promise.all(
|
|
1027
|
+
shortlist.map(
|
|
1028
|
+
(item) => hydrate(api, item, refs.get(key(item.type, item.id)))
|
|
1029
|
+
)
|
|
1030
|
+
);
|
|
1031
|
+
const tailCandidates = rankedRefs.slice(shortlist.length, shortlist.length + MAX_TAIL).map((i) => refs.get(key(i.type, i.id)));
|
|
1032
|
+
const tailCost = tailCandidates.reduce(
|
|
1033
|
+
(sum, m) => sum + estimateTokens(m.listLine),
|
|
1034
|
+
0
|
|
1035
|
+
);
|
|
1036
|
+
const tailReserve = Math.min(
|
|
1037
|
+
tailCost,
|
|
1038
|
+
Math.floor(remaining * TAIL_SHARE)
|
|
1039
|
+
);
|
|
1040
|
+
const bodyBudgetStart = Math.max(0, remaining - tailReserve);
|
|
1041
|
+
let bodyBudget = bodyBudgetStart;
|
|
1042
|
+
let itemsLeft = hydrated.length;
|
|
1043
|
+
let itemsWithBody = 0;
|
|
1044
|
+
let bodyFailures = 0;
|
|
1045
|
+
const blocks = [];
|
|
1046
|
+
for (const h of hydrated) {
|
|
1047
|
+
itemsLeft--;
|
|
1048
|
+
const emit = (block) => {
|
|
1049
|
+
bodyBudget -= Math.max(
|
|
1050
|
+
0,
|
|
1051
|
+
estimateTokens(block) - h.item.tokenEstimate
|
|
1052
|
+
);
|
|
1053
|
+
blocks.push(block);
|
|
1054
|
+
};
|
|
1055
|
+
if (h.error !== void 0) {
|
|
1056
|
+
bodyFailures++;
|
|
1057
|
+
emit(
|
|
1058
|
+
`${h.item.content}
|
|
1059
|
+
|
|
1060
|
+
_Body unavailable (${h.error}) \u2014 retry with \`${h.meta.fetchHint}\`._`
|
|
1061
|
+
);
|
|
1062
|
+
continue;
|
|
1063
|
+
}
|
|
1064
|
+
const available = Math.max(0, bodyBudget);
|
|
1065
|
+
const allowance = Math.min(
|
|
1066
|
+
Math.max(MIN_BODY_TOKENS, Math.floor(available / (itemsLeft + 1))),
|
|
1067
|
+
available
|
|
1068
|
+
);
|
|
1069
|
+
if (allowance < MIN_BODY_TOKENS) {
|
|
1070
|
+
emit(
|
|
1071
|
+
`${h.item.content}
|
|
1072
|
+
|
|
1073
|
+
_Body omitted (token budget exhausted) \u2014 fetch with \`${h.meta.fetchHint}\`._`
|
|
1074
|
+
);
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
const { text: body2, truncated } = truncateToTokens(
|
|
1078
|
+
h.body,
|
|
1079
|
+
allowance - TRUNCATION_NOTICE_TOKENS
|
|
1080
|
+
);
|
|
1081
|
+
itemsWithBody++;
|
|
1082
|
+
emit(
|
|
1083
|
+
`${h.item.content}
|
|
1084
|
+
|
|
1085
|
+
${body2}` + (truncated ? `
|
|
1086
|
+
|
|
1087
|
+
_[truncated to fit the token budget \u2014 full text via \`${h.meta.fetchHint}\`]_` : "")
|
|
1088
|
+
);
|
|
1089
|
+
}
|
|
1090
|
+
remaining = Math.max(0, remaining - (bodyBudgetStart - bodyBudget));
|
|
1091
|
+
if (selectedGuardrails.length > 0) {
|
|
1092
|
+
sections.push(
|
|
1093
|
+
`## Checks (${selectedGuardrails.length})
|
|
1094
|
+
|
|
1095
|
+
` + selectedGuardrails.map((i) => i.content).join("\n\n---\n\n")
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
if (blocks.length > 0) {
|
|
1099
|
+
sections.push(
|
|
1100
|
+
`## Relevant Specs & Docs (${blocks.length})
|
|
1101
|
+
|
|
1102
|
+
_Full text of the highest-ranked items. Every item carries the ID to fetch it in full._
|
|
1103
|
+
|
|
1104
|
+
` + blocks.join("\n\n---\n\n")
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
const tailLines = [];
|
|
1108
|
+
for (const meta of tailCandidates) {
|
|
1109
|
+
const cost = estimateTokens(meta.listLine);
|
|
1110
|
+
if (cost > remaining) break;
|
|
1111
|
+
tailLines.push(meta.listLine);
|
|
1112
|
+
remaining -= cost;
|
|
1113
|
+
}
|
|
1114
|
+
if (tailLines.length > 0) {
|
|
1115
|
+
sections.push(
|
|
1116
|
+
`## Also Indexed (${tailLines.length})
|
|
1117
|
+
|
|
1118
|
+
_Ranked lower \u2014 fetch with \`get_specs\` / \`get_docs\` if you need them._
|
|
1119
|
+
|
|
1120
|
+
` + tailLines.join("\n")
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
const itemsListed = blocks.length - itemsWithBody + tailLines.length;
|
|
1124
|
+
const itemsOmitted = ranked.length - selectedGuardrails.length - blocks.length - tailLines.length;
|
|
1125
|
+
const body = sections.join("\n\n");
|
|
1126
|
+
const tokensUsed = estimateTokens(body);
|
|
1127
|
+
const stats = {
|
|
1128
|
+
budget,
|
|
1129
|
+
tokensUsed,
|
|
1130
|
+
itemsWithBody,
|
|
1131
|
+
bodyFailures,
|
|
1132
|
+
itemsListed,
|
|
1133
|
+
itemsOmitted: Math.max(0, itemsOmitted),
|
|
1134
|
+
guardrails: selectedGuardrails.length
|
|
1135
|
+
};
|
|
1136
|
+
if (sections.length === 0) {
|
|
1137
|
+
return { text: "No relevant context found for this task.", stats };
|
|
1138
|
+
}
|
|
1139
|
+
const footer = `
|
|
1140
|
+
|
|
1141
|
+
---
|
|
1142
|
+
_Context: ${itemsWithBody} full text, ${itemsListed} listed, ${stats.itemsOmitted} omitted \xB7 ~${tokensUsed} of ${budget} tokens` + (bodyFailures > 0 ? ` \xB7 ${bodyFailures} body fetch(es) failed` : "") + `_`;
|
|
1143
|
+
return { text: body + footer, stats };
|
|
1144
|
+
}
|
|
1145
|
+
var DEFAULT_BUDGET, RESERVE_TOKENS, GUARDRAIL_SHARE, MIN_BODY_TOKENS, MAX_HYDRATED, MAX_TAIL, TAIL_SHARE, TRUNCATION_NOTICE_TOKENS;
|
|
1146
|
+
var init_context_builder = __esm({
|
|
1147
|
+
"src/lib/context-builder.ts"() {
|
|
1148
|
+
"use strict";
|
|
1149
|
+
init_scoring();
|
|
1150
|
+
init_scoring();
|
|
1151
|
+
DEFAULT_BUDGET = 4e3;
|
|
1152
|
+
RESERVE_TOKENS = 200;
|
|
1153
|
+
GUARDRAIL_SHARE = 0.35;
|
|
1154
|
+
MIN_BODY_TOKENS = 150;
|
|
1155
|
+
MAX_HYDRATED = 8;
|
|
1156
|
+
MAX_TAIL = 10;
|
|
1157
|
+
TAIL_SHARE = 0.15;
|
|
1158
|
+
TRUNCATION_NOTICE_TOKENS = 30;
|
|
1159
|
+
}
|
|
1160
|
+
});
|
|
1161
|
+
|
|
1162
|
+
// src/tools/get-context.ts
|
|
1163
|
+
import { z as z5 } from "zod";
|
|
1164
|
+
function registerGetContext(server2, apiClient2) {
|
|
1165
|
+
server2.registerTool(
|
|
1166
|
+
"get_context",
|
|
1167
|
+
{
|
|
1168
|
+
title: "Get Context",
|
|
1169
|
+
description: "Intelligent context aggregator: fetches checks, specs, docs, and tech stack, ranks them by relevance to your current task, and returns the full text of the highest-ranked items within a token budget \u2014 each with the ID needed to fetch the rest. Use this instead of calling individual tools when you want curated, relevant context.",
|
|
1170
|
+
inputSchema: {
|
|
1171
|
+
task_description: z5.string().describe(
|
|
1172
|
+
"What you're working on (e.g., 'add a new REST endpoint for user preferences')"
|
|
1173
|
+
),
|
|
1174
|
+
file_paths: z5.array(z5.string()).optional().describe(
|
|
1175
|
+
"File paths being edited \u2014 used for language/category detection"
|
|
1176
|
+
),
|
|
1177
|
+
max_tokens: z5.number().optional().describe("Maximum token budget for the response (default: 4000)")
|
|
1178
|
+
}
|
|
1179
|
+
},
|
|
1180
|
+
async ({ task_description, file_paths, max_tokens }) => {
|
|
1181
|
+
const { text } = await buildContext(apiClient2, {
|
|
1182
|
+
taskDescription: task_description,
|
|
1183
|
+
filePaths: file_paths,
|
|
1184
|
+
maxTokens: max_tokens
|
|
1185
|
+
});
|
|
1186
|
+
return {
|
|
1187
|
+
content: [{ type: "text", text }]
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
var init_get_context = __esm({
|
|
1193
|
+
"src/tools/get-context.ts"() {
|
|
1194
|
+
"use strict";
|
|
1195
|
+
init_context_builder();
|
|
1196
|
+
}
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
// src/tools/create-guardrail.ts
|
|
1200
|
+
import { z as z6 } from "zod";
|
|
1201
|
+
function registerCreateGuardrail(server2, apiClient2) {
|
|
1202
|
+
const handler = async ({
|
|
1203
|
+
title,
|
|
1204
|
+
description,
|
|
1205
|
+
severity,
|
|
1206
|
+
category,
|
|
1207
|
+
pattern,
|
|
1208
|
+
fix,
|
|
1209
|
+
fitness_test
|
|
1210
|
+
}) => {
|
|
1211
|
+
const result = await apiClient2.post(
|
|
1212
|
+
"/v1/guardrails",
|
|
1213
|
+
{
|
|
1214
|
+
entries: [
|
|
1215
|
+
{
|
|
1216
|
+
title,
|
|
1217
|
+
description,
|
|
1218
|
+
severity,
|
|
1219
|
+
category: category ?? "conventions",
|
|
1220
|
+
pattern: pattern ?? "",
|
|
1221
|
+
fix: fix ?? "",
|
|
1222
|
+
fitnessTest: fitness_test ?? ""
|
|
1223
|
+
}
|
|
1224
|
+
]
|
|
1225
|
+
}
|
|
1226
|
+
);
|
|
1227
|
+
apiClient2.invalidateCache("/v1/guardrails");
|
|
1228
|
+
const saved = result[0];
|
|
1229
|
+
return {
|
|
1230
|
+
content: [
|
|
1231
|
+
{
|
|
1232
|
+
type: "text",
|
|
1233
|
+
text: `Check created: **${saved.title}** (id: ${saved.id}, severity: ${saved.severity}, category: ${saved.category})`
|
|
1234
|
+
}
|
|
1235
|
+
]
|
|
1236
|
+
};
|
|
1237
|
+
};
|
|
1238
|
+
server2.registerTool(
|
|
1239
|
+
"create_check",
|
|
1240
|
+
{
|
|
1241
|
+
title: "Create Check",
|
|
1242
|
+
description: DESCRIPTION2,
|
|
1243
|
+
inputSchema: inputSchema2(),
|
|
1244
|
+
annotations: {
|
|
1245
|
+
readOnlyHint: false
|
|
1246
|
+
}
|
|
1247
|
+
},
|
|
1248
|
+
handler
|
|
1249
|
+
);
|
|
1250
|
+
server2.registerTool(
|
|
1251
|
+
"create_guardrail",
|
|
1252
|
+
{
|
|
1253
|
+
title: "Create Check (legacy name)",
|
|
1254
|
+
description: legacyToolDescription("create_check", DESCRIPTION2),
|
|
1255
|
+
inputSchema: inputSchema2(),
|
|
1256
|
+
annotations: {
|
|
1257
|
+
readOnlyHint: false
|
|
1258
|
+
}
|
|
1259
|
+
},
|
|
1260
|
+
handler
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
1263
|
+
var DESCRIPTION2, CATEGORIES, inputSchema2;
|
|
1264
|
+
var init_create_guardrail = __esm({
|
|
1265
|
+
"src/tools/create-guardrail.ts"() {
|
|
1266
|
+
"use strict";
|
|
1267
|
+
init_deprecation();
|
|
1268
|
+
DESCRIPTION2 = "Create a new check in SystemDox. Use this when you discover a pattern that should be enforced (banned pattern, required pattern, coding rule).";
|
|
1269
|
+
CATEGORIES = [
|
|
1270
|
+
"imports",
|
|
1271
|
+
"auth",
|
|
1272
|
+
"error-handling",
|
|
1273
|
+
"architecture",
|
|
1274
|
+
"testing",
|
|
1275
|
+
"security",
|
|
1276
|
+
"conventions",
|
|
1277
|
+
"data-access"
|
|
1278
|
+
];
|
|
1279
|
+
inputSchema2 = () => ({
|
|
1280
|
+
title: z6.string().describe("Short title for the check"),
|
|
1281
|
+
description: z6.string().describe("Detailed description of what the check enforces and why"),
|
|
1282
|
+
severity: z6.enum(["critical", "high", "medium", "low"]).describe("Severity level"),
|
|
1283
|
+
category: z6.enum(CATEGORIES).optional().describe("Category (defaults to conventions)"),
|
|
1284
|
+
pattern: z6.string().optional().describe("Bad code pattern that should be avoided"),
|
|
1285
|
+
fix: z6.string().optional().describe("Correct code pattern to use instead"),
|
|
1286
|
+
fitness_test: z6.string().optional().describe("Shell command to detect violations in CI")
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
// src/tools/update-spec.ts
|
|
1292
|
+
import { z as z7 } from "zod";
|
|
1293
|
+
function registerUpdateSpec(server2, apiClient2) {
|
|
1294
|
+
server2.registerTool(
|
|
1295
|
+
"update_spec",
|
|
1296
|
+
{
|
|
1297
|
+
title: "Update Spec",
|
|
1298
|
+
description: "Update an existing SystemDox spec's proposal, tasks, or requirements. Only draft specs can be modified \u2014 published specs must be versioned first.",
|
|
1299
|
+
inputSchema: {
|
|
1300
|
+
spec_id: z7.string().describe("ID of the spec to update"),
|
|
1301
|
+
section: z7.enum(["proposal", "tasks", "requirements"]).describe("Which section to update"),
|
|
1302
|
+
content: z7.string().describe(
|
|
1303
|
+
"New markdown content for the section. For requirements, provide the full requirement content."
|
|
1304
|
+
),
|
|
1305
|
+
requirement_name: z7.string().optional().describe(
|
|
1306
|
+
"Name of the requirement (required when section is 'requirements')"
|
|
1307
|
+
)
|
|
1308
|
+
},
|
|
1309
|
+
annotations: {
|
|
1310
|
+
readOnlyHint: false
|
|
1311
|
+
}
|
|
1312
|
+
},
|
|
1313
|
+
async ({ spec_id, section, content, requirement_name }) => {
|
|
1314
|
+
const spec = await apiClient2.get(`/v1/specs/${spec_id}`);
|
|
1315
|
+
if (spec.status !== "draft") {
|
|
1316
|
+
return {
|
|
1317
|
+
content: [
|
|
1318
|
+
{
|
|
1319
|
+
type: "text",
|
|
1320
|
+
text: `Cannot modify spec "${spec.name}" \u2014 status is "${spec.status}". Only draft specs can be updated.`
|
|
1321
|
+
}
|
|
1322
|
+
],
|
|
1323
|
+
isError: true
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
const body = {};
|
|
1327
|
+
if (section === "proposal") {
|
|
1328
|
+
body.proposalContent = content;
|
|
1329
|
+
} else if (section === "tasks") {
|
|
1330
|
+
body.taskContent = content;
|
|
1331
|
+
} else if (section === "requirements") {
|
|
1332
|
+
const existing = spec.requirements;
|
|
1333
|
+
const newReq = {
|
|
1334
|
+
name: requirement_name ?? "Untitled Requirement",
|
|
1335
|
+
content
|
|
1336
|
+
};
|
|
1337
|
+
body.requirements = [...existing, newReq];
|
|
1338
|
+
}
|
|
1339
|
+
await apiClient2.put(`/v1/specs/${spec_id}`, body);
|
|
1340
|
+
apiClient2.invalidateCache(`/v1/specs/${spec_id}`);
|
|
1341
|
+
apiClient2.invalidateCache("/v1/specs");
|
|
1342
|
+
return {
|
|
1343
|
+
content: [
|
|
1344
|
+
{
|
|
1345
|
+
type: "text",
|
|
1346
|
+
text: `Updated spec "${spec.name}" \u2014 ${section} section modified.`
|
|
1347
|
+
}
|
|
1348
|
+
]
|
|
1349
|
+
};
|
|
1350
|
+
}
|
|
1351
|
+
);
|
|
1352
|
+
}
|
|
1353
|
+
var init_update_spec = __esm({
|
|
1354
|
+
"src/tools/update-spec.ts"() {
|
|
1355
|
+
"use strict";
|
|
1356
|
+
}
|
|
1357
|
+
});
|
|
1358
|
+
|
|
1359
|
+
// src/tools/report-decision.ts
|
|
1360
|
+
import { z as z8 } from "zod";
|
|
1361
|
+
function registerReportDecision(server2, apiClient2) {
|
|
1362
|
+
server2.registerTool(
|
|
1363
|
+
"report_decision",
|
|
1364
|
+
{
|
|
1365
|
+
title: "Report Decision",
|
|
1366
|
+
description: "Record an architecture decision made during development. Creates a draft ADR in SystemDox for human review.",
|
|
1367
|
+
inputSchema: {
|
|
1368
|
+
title: z8.string().describe("Decision title (e.g., 'Use WebSocket for real-time updates')"),
|
|
1369
|
+
context: z8.string().describe("What prompted this decision \u2014 the problem or constraint"),
|
|
1370
|
+
decision: z8.string().describe("What was decided and why"),
|
|
1371
|
+
consequences: z8.string().describe("What this decision means going forward \u2014 trade-offs, follow-up work"),
|
|
1372
|
+
related_files: z8.array(z8.string()).optional().describe("File paths related to this decision")
|
|
1373
|
+
},
|
|
1374
|
+
annotations: {
|
|
1375
|
+
readOnlyHint: false
|
|
1376
|
+
}
|
|
1377
|
+
},
|
|
1378
|
+
async ({ title, context, decision, consequences, related_files }) => {
|
|
1379
|
+
const adrTitle = `ADR: ${title}`;
|
|
1380
|
+
const created = await apiClient2.post("/v1/specs", {
|
|
1381
|
+
name: adrTitle
|
|
1382
|
+
});
|
|
1383
|
+
const relatedSection = related_files && related_files.length > 0 ? `
|
|
1384
|
+
|
|
1385
|
+
## Related Files
|
|
1386
|
+
|
|
1387
|
+
${related_files.map((f) => `- \`${f}\``).join("\n")}` : "";
|
|
1388
|
+
const adrContent = `# ${adrTitle}
|
|
1389
|
+
|
|
1390
|
+
**Date:** ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}
|
|
1391
|
+
**Status:** Draft (pending review)
|
|
1392
|
+
|
|
1393
|
+
## Context
|
|
1394
|
+
|
|
1395
|
+
${context}
|
|
1396
|
+
|
|
1397
|
+
## Decision
|
|
1398
|
+
|
|
1399
|
+
${decision}
|
|
1400
|
+
|
|
1401
|
+
## Consequences
|
|
1402
|
+
|
|
1403
|
+
${consequences}` + relatedSection;
|
|
1404
|
+
await apiClient2.put(`/v1/specs/${created.specId}`, {
|
|
1405
|
+
proposalContent: adrContent
|
|
1406
|
+
});
|
|
1407
|
+
apiClient2.invalidateCache("/v1/specs");
|
|
1408
|
+
return {
|
|
1409
|
+
content: [
|
|
1410
|
+
{
|
|
1411
|
+
type: "text",
|
|
1412
|
+
text: `Architecture decision recorded: **${adrTitle}** (spec id: ${created.specId})
|
|
1413
|
+
|
|
1414
|
+
Status: draft \u2014 review in SystemDox before publishing.`
|
|
1415
|
+
}
|
|
1416
|
+
]
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
);
|
|
1420
|
+
}
|
|
1421
|
+
var init_report_decision = __esm({
|
|
1422
|
+
"src/tools/report-decision.ts"() {
|
|
1423
|
+
"use strict";
|
|
1424
|
+
}
|
|
1425
|
+
});
|
|
1426
|
+
|
|
1427
|
+
// src/tools/get-project-rules.ts
|
|
1428
|
+
import { z as z9 } from "zod";
|
|
1429
|
+
function registerGetProjectRules(server2, apiClient2) {
|
|
1430
|
+
server2.registerTool(
|
|
1431
|
+
"get_project_rules",
|
|
1432
|
+
{
|
|
1433
|
+
title: "Get Project Rules",
|
|
1434
|
+
description: "Retrieve project rules defined in SystemDox Standards. These are team-defined principles covering deployment, architecture, security, testing, and process. Filter by category to get only relevant rules.",
|
|
1435
|
+
inputSchema: {
|
|
1436
|
+
category: z9.enum(["deployment", "architecture", "security", "testing", "process", "other"]).optional().describe("Filter by rule category")
|
|
1437
|
+
}
|
|
1438
|
+
},
|
|
1439
|
+
async ({ category }) => {
|
|
1440
|
+
const rules = await apiClient2.get(
|
|
1441
|
+
"/v1/standards/rules"
|
|
1442
|
+
);
|
|
1443
|
+
let filtered = rules;
|
|
1444
|
+
if (category) {
|
|
1445
|
+
filtered = filtered.filter(
|
|
1446
|
+
(r) => r.category.toLowerCase() === category.toLowerCase()
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1449
|
+
if (filtered.length === 0) {
|
|
1450
|
+
return {
|
|
1451
|
+
content: [
|
|
1452
|
+
{
|
|
1453
|
+
type: "text",
|
|
1454
|
+
text: `No project rules found${category ? ` in category "${category}"` : ""}.`
|
|
1455
|
+
}
|
|
1456
|
+
]
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
1460
|
+
for (const rule of filtered) {
|
|
1461
|
+
const cat = rule.category || "other";
|
|
1462
|
+
if (!grouped.has(cat)) grouped.set(cat, []);
|
|
1463
|
+
grouped.get(cat).push(rule);
|
|
1464
|
+
}
|
|
1465
|
+
let text = `Found ${filtered.length} project rule(s):
|
|
1466
|
+
|
|
1467
|
+
`;
|
|
1468
|
+
for (const [cat, catRules] of grouped) {
|
|
1469
|
+
const label = CATEGORY_LABELS[cat] ?? cat;
|
|
1470
|
+
text += `## ${label}
|
|
1471
|
+
|
|
1472
|
+
`;
|
|
1473
|
+
for (const rule of catRules) {
|
|
1474
|
+
text += `- ${rule.text}
|
|
1475
|
+
`;
|
|
1476
|
+
}
|
|
1477
|
+
text += "\n";
|
|
1478
|
+
}
|
|
1479
|
+
return {
|
|
1480
|
+
content: [
|
|
1481
|
+
{
|
|
1482
|
+
type: "text",
|
|
1483
|
+
text
|
|
1484
|
+
}
|
|
1485
|
+
]
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
);
|
|
1489
|
+
}
|
|
1490
|
+
var CATEGORY_LABELS;
|
|
1491
|
+
var init_get_project_rules = __esm({
|
|
1492
|
+
"src/tools/get-project-rules.ts"() {
|
|
1493
|
+
"use strict";
|
|
1494
|
+
CATEGORY_LABELS = {
|
|
1495
|
+
deployment: "Deployment",
|
|
1496
|
+
architecture: "Architecture",
|
|
1497
|
+
security: "Security",
|
|
1498
|
+
testing: "Testing",
|
|
1499
|
+
process: "Process",
|
|
1500
|
+
other: "Other"
|
|
1501
|
+
};
|
|
1502
|
+
}
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
// src/tools/create-project-rule.ts
|
|
1506
|
+
import { z as z10 } from "zod";
|
|
1507
|
+
function registerCreateProjectRule(server2, apiClient2) {
|
|
1508
|
+
server2.registerTool(
|
|
1509
|
+
"create_project_rule",
|
|
1510
|
+
{
|
|
1511
|
+
title: "Create Project Rule",
|
|
1512
|
+
description: "Create a new project rule in SystemDox Standards. Use this when you discover a team principle or convention that should be enforced across the project.",
|
|
1513
|
+
inputSchema: {
|
|
1514
|
+
text: z10.string().describe(
|
|
1515
|
+
"The rule statement \u2014 one enforceable principle per rule (e.g. 'All deployments MUST go through GitHub Actions workflows')"
|
|
1516
|
+
),
|
|
1517
|
+
category: z10.enum(["deployment", "architecture", "security", "testing", "process", "other"]).describe("Rule category")
|
|
1518
|
+
},
|
|
1519
|
+
annotations: {
|
|
1520
|
+
readOnlyHint: false
|
|
1521
|
+
}
|
|
1522
|
+
},
|
|
1523
|
+
async ({ text, category }) => {
|
|
1524
|
+
const saved = await apiClient2.post(
|
|
1525
|
+
"/v1/standards/rules",
|
|
1526
|
+
{ text, category }
|
|
1527
|
+
);
|
|
1528
|
+
apiClient2.invalidateCache("/v1/standards/rules");
|
|
1529
|
+
return {
|
|
1530
|
+
content: [
|
|
1531
|
+
{
|
|
1532
|
+
type: "text",
|
|
1533
|
+
text: `Project rule created: **${saved.text}** (id: ${saved.id}, category: ${saved.category})`
|
|
1534
|
+
}
|
|
1535
|
+
]
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
);
|
|
1539
|
+
}
|
|
1540
|
+
var init_create_project_rule = __esm({
|
|
1541
|
+
"src/tools/create-project-rule.ts"() {
|
|
1542
|
+
"use strict";
|
|
1543
|
+
}
|
|
1544
|
+
});
|
|
1545
|
+
|
|
1546
|
+
// src/tools/get-prompt-templates.ts
|
|
1547
|
+
function registerGetPromptTemplates(server2, apiClient2) {
|
|
1548
|
+
server2.registerTool(
|
|
1549
|
+
"get_prompt_templates",
|
|
1550
|
+
{
|
|
1551
|
+
title: "Get Prompt Templates",
|
|
1552
|
+
description: "Retrieve reusable prompt templates from SystemDox Standards. These are preconfigured prompts that Gen AI should use when generating content \u2014 e.g. code review checklists, documentation templates, or analysis frameworks.",
|
|
1553
|
+
inputSchema: {}
|
|
1554
|
+
},
|
|
1555
|
+
async () => {
|
|
1556
|
+
const templates = await apiClient2.get(
|
|
1557
|
+
"/v1/standards/prompt-templates"
|
|
1558
|
+
);
|
|
1559
|
+
if (templates.length === 0) {
|
|
1560
|
+
return {
|
|
1561
|
+
content: [
|
|
1562
|
+
{
|
|
1563
|
+
type: "text",
|
|
1564
|
+
text: "No prompt templates found."
|
|
1565
|
+
}
|
|
1566
|
+
]
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
const text = templates.map(
|
|
1570
|
+
(t) => `## ${t.name}
|
|
1571
|
+
|
|
1572
|
+
` + (t.description ? `*${t.description}*
|
|
1573
|
+
|
|
1574
|
+
` : "") + `\`\`\`
|
|
1575
|
+
${t.prompt}
|
|
1576
|
+
\`\`\``
|
|
1577
|
+
).join("\n\n---\n\n");
|
|
1578
|
+
return {
|
|
1579
|
+
content: [
|
|
1580
|
+
{
|
|
1581
|
+
type: "text",
|
|
1582
|
+
text: `Found ${templates.length} prompt template(s):
|
|
1583
|
+
|
|
1584
|
+
${text}`
|
|
1585
|
+
}
|
|
1586
|
+
]
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
);
|
|
1590
|
+
}
|
|
1591
|
+
var init_get_prompt_templates = __esm({
|
|
1592
|
+
"src/tools/get-prompt-templates.ts"() {
|
|
1593
|
+
"use strict";
|
|
1594
|
+
}
|
|
1595
|
+
});
|
|
1596
|
+
|
|
1597
|
+
// src/tools/create-prompt-template.ts
|
|
1598
|
+
import { z as z11 } from "zod";
|
|
1599
|
+
function registerCreatePromptTemplate(server2, apiClient2) {
|
|
1600
|
+
server2.registerTool(
|
|
1601
|
+
"create_prompt_template",
|
|
1602
|
+
{
|
|
1603
|
+
title: "Create Prompt Template",
|
|
1604
|
+
description: "Create a new reusable prompt template in SystemDox Standards. Use this to store preconfigured prompts for common AI tasks like code reviews, documentation generation, or analysis frameworks.",
|
|
1605
|
+
inputSchema: {
|
|
1606
|
+
name: z11.string().describe("Short name for the template"),
|
|
1607
|
+
description: z11.string().optional().describe("Brief description of when to use this template"),
|
|
1608
|
+
prompt: z11.string().describe("The full prompt text that Gen AI will use when generating content")
|
|
1609
|
+
},
|
|
1610
|
+
annotations: {
|
|
1611
|
+
readOnlyHint: false
|
|
1612
|
+
}
|
|
1613
|
+
},
|
|
1614
|
+
async ({ name, description, prompt: prompt2 }) => {
|
|
1615
|
+
const saved = await apiClient2.post(
|
|
1616
|
+
"/v1/standards/prompt-templates",
|
|
1617
|
+
{
|
|
1618
|
+
name,
|
|
1619
|
+
description: description ?? "",
|
|
1620
|
+
prompt: prompt2
|
|
1621
|
+
}
|
|
1622
|
+
);
|
|
1623
|
+
apiClient2.invalidateCache("/v1/standards/prompt-templates");
|
|
1624
|
+
return {
|
|
1625
|
+
content: [
|
|
1626
|
+
{
|
|
1627
|
+
type: "text",
|
|
1628
|
+
text: `Prompt template created: **${saved.name}** (id: ${saved.id})`
|
|
1629
|
+
}
|
|
1630
|
+
]
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
);
|
|
1634
|
+
}
|
|
1635
|
+
var init_create_prompt_template = __esm({
|
|
1636
|
+
"src/tools/create-prompt-template.ts"() {
|
|
1637
|
+
"use strict";
|
|
1638
|
+
}
|
|
1639
|
+
});
|
|
1640
|
+
|
|
1641
|
+
// src/server.ts
|
|
1642
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1643
|
+
function createServer(apiClient2) {
|
|
1644
|
+
const server2 = new McpServer({
|
|
1645
|
+
name: "systemdox-mcp-server",
|
|
1646
|
+
version: "0.1.0"
|
|
1647
|
+
});
|
|
1648
|
+
registerGetGuardrails(server2, apiClient2);
|
|
1649
|
+
registerGetSpecs(server2, apiClient2);
|
|
1650
|
+
registerGetDocs(server2, apiClient2);
|
|
1651
|
+
registerGetTechStack(server2, apiClient2);
|
|
1652
|
+
registerGetPlanningContext(server2, apiClient2);
|
|
1653
|
+
registerGetContext(server2, apiClient2);
|
|
1654
|
+
registerGetProjectRules(server2, apiClient2);
|
|
1655
|
+
registerGetPromptTemplates(server2, apiClient2);
|
|
1656
|
+
registerCreateGuardrail(server2, apiClient2);
|
|
1657
|
+
registerUpdateSpec(server2, apiClient2);
|
|
1658
|
+
registerReportDecision(server2, apiClient2);
|
|
1659
|
+
registerCreateProjectRule(server2, apiClient2);
|
|
1660
|
+
registerCreatePromptTemplate(server2, apiClient2);
|
|
1661
|
+
return server2;
|
|
1662
|
+
}
|
|
1663
|
+
var init_server = __esm({
|
|
1664
|
+
"src/server.ts"() {
|
|
1665
|
+
"use strict";
|
|
1666
|
+
init_get_guardrails();
|
|
1667
|
+
init_get_specs();
|
|
1668
|
+
init_get_docs();
|
|
1669
|
+
init_get_tech_stack();
|
|
1670
|
+
init_get_planning_context();
|
|
1671
|
+
init_get_context();
|
|
1672
|
+
init_create_guardrail();
|
|
1673
|
+
init_update_spec();
|
|
1674
|
+
init_report_decision();
|
|
1675
|
+
init_get_project_rules();
|
|
1676
|
+
init_create_project_rule();
|
|
1677
|
+
init_get_prompt_templates();
|
|
1678
|
+
init_create_prompt_template();
|
|
1679
|
+
}
|
|
1680
|
+
});
|
|
1681
|
+
|
|
1682
|
+
// src/index.ts
|
|
1683
|
+
var index_exports = {};
|
|
1684
|
+
__export(index_exports, {
|
|
1685
|
+
apiClient: () => apiClient,
|
|
1686
|
+
config: () => config,
|
|
1687
|
+
createServer: () => createServer
|
|
1688
|
+
});
|
|
1689
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
1690
|
+
async function main() {
|
|
1691
|
+
await apiClient.validate();
|
|
1692
|
+
const transport = new StdioServerTransport();
|
|
1693
|
+
await server.connect(transport);
|
|
1694
|
+
}
|
|
1695
|
+
var config, apiClient, server;
|
|
1696
|
+
var init_index = __esm({
|
|
1697
|
+
"src/index.ts"() {
|
|
1698
|
+
"use strict";
|
|
1699
|
+
init_config();
|
|
1700
|
+
init_api_client();
|
|
1701
|
+
init_server();
|
|
1702
|
+
init_server();
|
|
1703
|
+
config = loadConfig();
|
|
1704
|
+
apiClient = new SystemDoxApiClient(config);
|
|
1705
|
+
server = createServer(apiClient);
|
|
1706
|
+
main();
|
|
1707
|
+
}
|
|
1708
|
+
});
|
|
1709
|
+
|
|
1710
|
+
// src/cli.ts
|
|
1711
|
+
import { readFile, writeFile } from "fs/promises";
|
|
1712
|
+
import { existsSync } from "fs";
|
|
1713
|
+
import { homedir } from "os";
|
|
1714
|
+
import { join } from "path";
|
|
1715
|
+
import { createInterface } from "readline";
|
|
1716
|
+
var API_URL_DEFAULT = "https://api.puglieseweb.com/systemdox";
|
|
1717
|
+
function prompt(question) {
|
|
1718
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1719
|
+
return new Promise((resolve) => {
|
|
1720
|
+
rl.question(question, (answer) => {
|
|
1721
|
+
rl.close();
|
|
1722
|
+
resolve(answer.trim());
|
|
1723
|
+
});
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
async function validateApiKey(apiKey, apiUrl) {
|
|
1727
|
+
try {
|
|
1728
|
+
const res = await fetch(`${apiUrl.replace(/\/+$/, "")}/v1/health`, {
|
|
1729
|
+
headers: {
|
|
1730
|
+
Authorization: `Bearer ${apiKey}`,
|
|
1731
|
+
"X-API-Key": apiKey,
|
|
1732
|
+
Accept: "application/json"
|
|
1733
|
+
}
|
|
1734
|
+
});
|
|
1735
|
+
return res.ok;
|
|
1736
|
+
} catch {
|
|
1737
|
+
return false;
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
function findConfigTargets() {
|
|
1741
|
+
const targets = [];
|
|
1742
|
+
const projectMcp = join(process.cwd(), ".mcp.json");
|
|
1743
|
+
targets.push({
|
|
1744
|
+
name: "Claude Code (project)",
|
|
1745
|
+
path: projectMcp,
|
|
1746
|
+
detected: true
|
|
1747
|
+
// Always available
|
|
1748
|
+
});
|
|
1749
|
+
const claudeUser = join(homedir(), ".claude.json");
|
|
1750
|
+
targets.push({
|
|
1751
|
+
name: "Claude Code (user \u2014 all projects)",
|
|
1752
|
+
path: claudeUser,
|
|
1753
|
+
detected: true
|
|
1754
|
+
// Always available
|
|
1755
|
+
});
|
|
1756
|
+
const cursorProject = join(process.cwd(), ".cursor", "mcp.json");
|
|
1757
|
+
const cursorDir = join(process.cwd(), ".cursor");
|
|
1758
|
+
targets.push({
|
|
1759
|
+
name: "Cursor (project)",
|
|
1760
|
+
path: cursorProject,
|
|
1761
|
+
detected: existsSync(cursorDir)
|
|
1762
|
+
});
|
|
1763
|
+
const vscodeProject = join(process.cwd(), ".vscode", "mcp.json");
|
|
1764
|
+
const vscodeDir = join(process.cwd(), ".vscode");
|
|
1765
|
+
targets.push({
|
|
1766
|
+
name: "VS Code (project)",
|
|
1767
|
+
path: vscodeProject,
|
|
1768
|
+
detected: existsSync(vscodeDir)
|
|
1769
|
+
});
|
|
1770
|
+
const cursorGlobal = join(homedir(), ".cursor", "mcp.json");
|
|
1771
|
+
targets.push({
|
|
1772
|
+
name: "Cursor (global)",
|
|
1773
|
+
path: cursorGlobal,
|
|
1774
|
+
detected: existsSync(join(homedir(), ".cursor"))
|
|
1775
|
+
});
|
|
1776
|
+
return targets;
|
|
1777
|
+
}
|
|
1778
|
+
async function readOrCreateConfig(path) {
|
|
1779
|
+
try {
|
|
1780
|
+
const content = await readFile(path, "utf-8");
|
|
1781
|
+
return JSON.parse(content);
|
|
1782
|
+
} catch {
|
|
1783
|
+
return { mcpServers: {} };
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
async function writeConfig(path, config2) {
|
|
1787
|
+
const dir = path.substring(0, path.lastIndexOf("/"));
|
|
1788
|
+
if (!existsSync(dir)) {
|
|
1789
|
+
const { mkdir } = await import("fs/promises");
|
|
1790
|
+
await mkdir(dir, { recursive: true });
|
|
1791
|
+
}
|
|
1792
|
+
await writeFile(path, JSON.stringify(config2, null, 2) + "\n");
|
|
1793
|
+
}
|
|
1794
|
+
function serverEntry(apiKey, apiUrl) {
|
|
1795
|
+
const entry = {
|
|
1796
|
+
command: "npx",
|
|
1797
|
+
args: ["@systemdox/mcp-server"],
|
|
1798
|
+
env: {
|
|
1799
|
+
SYSTEMDOX_API_KEY: apiKey
|
|
1800
|
+
}
|
|
1801
|
+
};
|
|
1802
|
+
if (apiUrl !== API_URL_DEFAULT) {
|
|
1803
|
+
entry.env.SYSTEMDOX_API_URL = apiUrl;
|
|
1804
|
+
}
|
|
1805
|
+
return entry;
|
|
1806
|
+
}
|
|
1807
|
+
async function setup() {
|
|
1808
|
+
console.log("\n SystemDox MCP Server Setup\n");
|
|
1809
|
+
const apiKey = await prompt(" Enter your SystemDox API key: ");
|
|
1810
|
+
if (!apiKey) {
|
|
1811
|
+
console.error(
|
|
1812
|
+
"\n Error: API key is required.\n Generate one at your SystemDox settings page.\n"
|
|
1813
|
+
);
|
|
1814
|
+
process.exit(1);
|
|
1815
|
+
}
|
|
1816
|
+
const apiUrlInput = await prompt(
|
|
1817
|
+
` API URL [${API_URL_DEFAULT}]: `
|
|
1818
|
+
);
|
|
1819
|
+
const apiUrl = apiUrlInput || API_URL_DEFAULT;
|
|
1820
|
+
console.log("\n Validating API key...");
|
|
1821
|
+
const valid = await validateApiKey(apiKey, apiUrl);
|
|
1822
|
+
if (!valid) {
|
|
1823
|
+
console.error(
|
|
1824
|
+
"\n Error: Invalid API key or cannot reach the SystemDox API.\n Check your key and try again.\n"
|
|
1825
|
+
);
|
|
1826
|
+
process.exit(1);
|
|
1827
|
+
}
|
|
1828
|
+
console.log(" API key validated successfully.\n");
|
|
1829
|
+
const targets = findConfigTargets();
|
|
1830
|
+
const detected = targets.filter((t) => t.detected);
|
|
1831
|
+
console.log(" Available targets:");
|
|
1832
|
+
detected.forEach((t, i) => {
|
|
1833
|
+
console.log(` ${i + 1}. ${t.name} (${t.path})`);
|
|
1834
|
+
});
|
|
1835
|
+
const choiceInput = await prompt(
|
|
1836
|
+
`
|
|
1837
|
+
Which target? [1-${detected.length}, or 'all']: `
|
|
1838
|
+
);
|
|
1839
|
+
let selected;
|
|
1840
|
+
if (choiceInput.toLowerCase() === "all") {
|
|
1841
|
+
selected = detected;
|
|
1842
|
+
} else {
|
|
1843
|
+
const idx = parseInt(choiceInput, 10) - 1;
|
|
1844
|
+
if (isNaN(idx) || idx < 0 || idx >= detected.length) {
|
|
1845
|
+
console.error("\n Invalid selection.\n");
|
|
1846
|
+
process.exit(1);
|
|
1847
|
+
}
|
|
1848
|
+
selected = [detected[idx]];
|
|
1849
|
+
}
|
|
1850
|
+
const entry = serverEntry(apiKey, apiUrl);
|
|
1851
|
+
for (const target of selected) {
|
|
1852
|
+
const config2 = await readOrCreateConfig(target.path);
|
|
1853
|
+
config2.mcpServers["systemdox"] = entry;
|
|
1854
|
+
await writeConfig(target.path, config2);
|
|
1855
|
+
console.log(` Configured: ${target.name}`);
|
|
1856
|
+
}
|
|
1857
|
+
console.log(
|
|
1858
|
+
"\n Done! Restart your AI coding tool to activate the SystemDox MCP server.\n The server provides 13 tools: get_context, get_checks,\n get_project_rules, get_prompt_templates, get_specs, get_docs,\n get_tech_stack, get_planning_context, create_check,\n create_project_rule, create_prompt_template, update_spec,\n report_decision.\n\n get_guardrails and create_guardrail remain registered as deprecated\n aliases of get_checks and create_check \u2014 existing configs keep working.\n"
|
|
1859
|
+
);
|
|
1860
|
+
}
|
|
1861
|
+
var args = process.argv.slice(2);
|
|
1862
|
+
var command = args[0];
|
|
1863
|
+
if (command === "setup") {
|
|
1864
|
+
setup().catch((err) => {
|
|
1865
|
+
console.error(`
|
|
1866
|
+
Error: ${err.message}
|
|
1867
|
+
`);
|
|
1868
|
+
process.exit(1);
|
|
1869
|
+
});
|
|
1870
|
+
} else if (command === "--help" || command === "-h") {
|
|
1871
|
+
console.log(`
|
|
1872
|
+
SystemDox MCP Server
|
|
1873
|
+
|
|
1874
|
+
Usage:
|
|
1875
|
+
systemdox-mcp-server Start the MCP server (stdio transport)
|
|
1876
|
+
systemdox-mcp-server setup Configure the MCP server in your AI coding tool
|
|
1877
|
+
systemdox-mcp-server --help Show this help message
|
|
1878
|
+
|
|
1879
|
+
Environment variables:
|
|
1880
|
+
SYSTEMDOX_API_KEY Your SystemDox API key (required)
|
|
1881
|
+
SYSTEMDOX_API_URL API URL (default: ${API_URL_DEFAULT})
|
|
1882
|
+
SYSTEMDOX_CACHE_TTL Cache TTL in seconds (default: 300)
|
|
1883
|
+
SYSTEMDOX_DEFAULT_ORG Default GitHub org
|
|
1884
|
+
`);
|
|
1885
|
+
} else if (!command) {
|
|
1886
|
+
Promise.resolve().then(() => init_index());
|
|
1887
|
+
} else {
|
|
1888
|
+
console.error(` Unknown command: ${command}
|
|
1889
|
+
Run 'systemdox-mcp-server --help' for usage.`);
|
|
1890
|
+
process.exit(1);
|
|
1891
|
+
}
|