@systemdox/mcp-server 0.1.50 → 0.1.52
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/dist/cli.js +144 -27
- package/dist/http.js +126 -19
- package/dist/index.js +126 -19
- package/dist/install-skill.js +4 -1
- package/dist/lib/context-builder.js +98 -11
- package/dist/lib/scoring.d.ts +5 -0
- package/dist/lib/skills.d.ts +7 -0
- package/dist/lib/skills.js +5 -1
- package/dist/lib/spec-label.d.ts +5 -0
- package/dist/server.js +126 -19
- package/dist/setup-mcp.js +2 -0
- package/dist/tools/get-docs.js +14 -1
- package/dist/tools/skills.js +5 -2
- package/package.json +1 -1
|
@@ -82,6 +82,9 @@ var SEVERITY_SCORES = {
|
|
|
82
82
|
medium: 0.5,
|
|
83
83
|
low: 0.25
|
|
84
84
|
};
|
|
85
|
+
var MIN_TITLE_SHARE = 1 / 3;
|
|
86
|
+
var RECORD_PATH = /(^|\/)changelog\//;
|
|
87
|
+
var RECORD_FACTOR = 0.5;
|
|
85
88
|
var CHARS_PER_TOKEN = 4;
|
|
86
89
|
function estimateTokens(text) {
|
|
87
90
|
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
@@ -134,6 +137,32 @@ function scoreSeverity(item) {
|
|
|
134
137
|
if (!item.severity) return 0.5;
|
|
135
138
|
return SEVERITY_SCORES[item.severity] ?? 0.5;
|
|
136
139
|
}
|
|
140
|
+
function stem(word) {
|
|
141
|
+
return word.length > 3 ? word.replace(/s$/, "") : word;
|
|
142
|
+
}
|
|
143
|
+
function stems(text) {
|
|
144
|
+
return new Set(
|
|
145
|
+
text.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean).map(stem)
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
function titleShare(title, keywords) {
|
|
149
|
+
const wanted = stems(keywords.join(" "));
|
|
150
|
+
if (wanted.size === 0) return 0;
|
|
151
|
+
const held = stems(title);
|
|
152
|
+
let matches = 0;
|
|
153
|
+
for (const word of wanted) {
|
|
154
|
+
if (held.has(word)) matches++;
|
|
155
|
+
}
|
|
156
|
+
return matches / wanted.size;
|
|
157
|
+
}
|
|
158
|
+
function scoreReference(item, keywords) {
|
|
159
|
+
const share = titleShare(item.title, keywords);
|
|
160
|
+
const score = Math.max(
|
|
161
|
+
item.relevance ?? 0,
|
|
162
|
+
share >= MIN_TITLE_SHARE ? share : 0
|
|
163
|
+
);
|
|
164
|
+
return RECORD_PATH.test(item.path ?? "") ? score * RECORD_FACTOR : score;
|
|
165
|
+
}
|
|
137
166
|
function extractKeywords(text) {
|
|
138
167
|
const stopWords = /* @__PURE__ */ new Set([
|
|
139
168
|
"the",
|
|
@@ -230,7 +259,8 @@ function scoreItems(items, taskDescription, filePaths) {
|
|
|
230
259
|
const keywordScore = scoreKeywordMatch(item, keywords);
|
|
231
260
|
const recencyScore = scoreRecency(item);
|
|
232
261
|
const severityScore = scoreSeverity(item);
|
|
233
|
-
const
|
|
262
|
+
const isReference = item.type === "spec" || item.type === "doc";
|
|
263
|
+
const score = isReference && keywords.length > 0 ? scoreReference(item, keywords) : pathScore * WEIGHT_PATH + keywordScore * WEIGHT_KEYWORD + recencyScore * WEIGHT_RECENCY + severityScore * WEIGHT_SEVERITY;
|
|
234
264
|
return {
|
|
235
265
|
type: item.type,
|
|
236
266
|
id: item.id,
|
|
@@ -239,7 +269,7 @@ function scoreItems(items, taskDescription, filePaths) {
|
|
|
239
269
|
score,
|
|
240
270
|
tokenEstimate: estimateTokens(item.content)
|
|
241
271
|
};
|
|
242
|
-
}).sort((a, b) => b.score - a.score);
|
|
272
|
+
}).filter((item) => item.score > 0).sort((a, b) => b.score - a.score);
|
|
243
273
|
}
|
|
244
274
|
function selectWithinBudget(items, maxTokens) {
|
|
245
275
|
const selected = [];
|
|
@@ -252,6 +282,15 @@ function selectWithinBudget(items, maxTokens) {
|
|
|
252
282
|
return { selected, tokensUsed, totalItems: items.length };
|
|
253
283
|
}
|
|
254
284
|
|
|
285
|
+
// src/lib/spec-label.ts
|
|
286
|
+
var DECISION_PREFIX = "ADR: ";
|
|
287
|
+
function isDecisionDraft(name) {
|
|
288
|
+
return name.startsWith(DECISION_PREFIX);
|
|
289
|
+
}
|
|
290
|
+
function specLabel(spec) {
|
|
291
|
+
return isDecisionDraft(spec.name) ? "decision record" : `${spec.requirementCount} requirements`;
|
|
292
|
+
}
|
|
293
|
+
|
|
255
294
|
// src/lib/context-builder.ts
|
|
256
295
|
var DEFAULT_BUDGET = 4e3;
|
|
257
296
|
var RESERVE_TOKENS = 200;
|
|
@@ -261,6 +300,10 @@ var MAX_HYDRATED = 8;
|
|
|
261
300
|
var MAX_TAIL = 10;
|
|
262
301
|
var TAIL_SHARE = 0.15;
|
|
263
302
|
var TRUNCATION_NOTICE_TOKENS = 30;
|
|
303
|
+
var STACK_SHARE = 0.1;
|
|
304
|
+
var MIN_REFERENCES = 3;
|
|
305
|
+
var SEARCH_RESULTS = 30;
|
|
306
|
+
var MAX_QUERY_CHARS = 300;
|
|
264
307
|
function formatGuardrail(g) {
|
|
265
308
|
let text = `## ${g.title} [${g.severity}]
|
|
266
309
|
|
|
@@ -279,7 +322,7 @@ ${g.fix}
|
|
|
279
322
|
}
|
|
280
323
|
function formatSpecHeadline(s) {
|
|
281
324
|
return `### ${s.name}
|
|
282
|
-
\`draft_id: ${s.draftId}\` \xB7 ${s.status} \xB7 ${s
|
|
325
|
+
\`draft_id: ${s.draftId}\` \xB7 ${s.status} \xB7 ${specLabel(s)}`;
|
|
283
326
|
}
|
|
284
327
|
function formatDocHeadline(d) {
|
|
285
328
|
return `### ${d.title}
|
|
@@ -290,6 +333,21 @@ function formatStack(entries) {
|
|
|
290
333
|
(e) => `- ${e.name}${e.version ? ` v${e.version}` : ""}${e.notes ? ` \u2014 ${e.notes}` : ""}`
|
|
291
334
|
).join("\n");
|
|
292
335
|
}
|
|
336
|
+
function selectStack(entries, words, maxTokens) {
|
|
337
|
+
const named = (e) => {
|
|
338
|
+
const entry = `${e.name} ${e.notes ?? ""}`.toLowerCase();
|
|
339
|
+
return words.some((w) => entry.includes(w));
|
|
340
|
+
};
|
|
341
|
+
const ordered = [...entries.filter(named), ...entries.filter((e) => !named(e))];
|
|
342
|
+
const shown = [];
|
|
343
|
+
for (const entry of ordered) {
|
|
344
|
+
if (shown.length > 0 && estimateTokens(formatStack([...shown, entry])) > maxTokens) {
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
shown.push(entry);
|
|
348
|
+
}
|
|
349
|
+
return { text: formatStack(shown), hidden: entries.length - shown.length };
|
|
350
|
+
}
|
|
293
351
|
function formatRules(rules) {
|
|
294
352
|
const byCategory = /* @__PURE__ */ new Map();
|
|
295
353
|
for (const r of rules) {
|
|
@@ -311,6 +369,21 @@ function formatRules(rules) {
|
|
|
311
369
|
|
|
312
370
|
${text.trim()}`;
|
|
313
371
|
}
|
|
372
|
+
async function findDocs(api, taskDescription) {
|
|
373
|
+
const query = taskDescription.trim().slice(0, MAX_QUERY_CHARS);
|
|
374
|
+
if (query) {
|
|
375
|
+
try {
|
|
376
|
+
const found = await api.get("/v1/docs", {
|
|
377
|
+
q: query,
|
|
378
|
+
limit: String(SEARCH_RESULTS)
|
|
379
|
+
});
|
|
380
|
+
if (found.items.length > 0) return { ...found, searched: true };
|
|
381
|
+
} catch {
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const listed = await api.get("/v1/docs");
|
|
385
|
+
return { ...listed, searched: false };
|
|
386
|
+
}
|
|
314
387
|
async function fetchSpecBody(api, draftId) {
|
|
315
388
|
const spec = await api.get(
|
|
316
389
|
`/v1/specs/${encodeURIComponent(draftId)}`
|
|
@@ -411,7 +484,9 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
411
484
|
const [rawGuardrails, rawSpecs, docs, stack, rules, docRepos] = await Promise.all([
|
|
412
485
|
api.get("/v1/guardrails").catch(orEmpty([])),
|
|
413
486
|
api.get("/v1/specs").catch(orEmpty([])),
|
|
414
|
-
api
|
|
487
|
+
findDocs(api, taskDescription).catch(
|
|
488
|
+
orEmpty({ items: [], searched: false })
|
|
489
|
+
),
|
|
415
490
|
api.get("/v1/ai/settings/stack").catch(orEmpty([])),
|
|
416
491
|
api.get("/v1/standards/rules").catch(orEmpty([])),
|
|
417
492
|
// Capability states — FAIL-OPEN: an unreadable config (older backend, a
|
|
@@ -424,6 +499,8 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
424
499
|
const specs = disabled("requirements") && disabled("design") && disabled("specs") ? [] : rawSpecs;
|
|
425
500
|
const refs = /* @__PURE__ */ new Map();
|
|
426
501
|
const key = (type, id) => `${type}:${id}`;
|
|
502
|
+
const docTitles = new Set(docs.items.map((d) => d.title));
|
|
503
|
+
const isCopyOfDoc = (s) => isDecisionDraft(s.name) && docTitles.has(s.name.slice("ADR: ".length));
|
|
427
504
|
const scorable = [
|
|
428
505
|
...guardrails.map((g) => ({
|
|
429
506
|
type: "guardrail",
|
|
@@ -434,12 +511,12 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
434
511
|
severity: g.severity,
|
|
435
512
|
updatedAt: g.updatedAt
|
|
436
513
|
})),
|
|
437
|
-
...specs.filter((s) => s.status !== "archived").map((s) => {
|
|
514
|
+
...specs.filter((s) => s.status !== "archived" && !isCopyOfDoc(s)).map((s) => {
|
|
438
515
|
refs.set(key("spec", s.draftId), {
|
|
439
516
|
type: "spec",
|
|
440
517
|
id: s.draftId,
|
|
441
518
|
title: s.name,
|
|
442
|
-
listLine: `- **${s.name}** \u2014 \`draft_id: ${s.draftId}\` (${s.status}, ${s
|
|
519
|
+
listLine: `- **${s.name}** \u2014 \`draft_id: ${s.draftId}\` (${s.status}, ${specLabel(s)})`,
|
|
443
520
|
fetchHint: `get_specs(draft_id="${s.draftId}")`
|
|
444
521
|
});
|
|
445
522
|
return {
|
|
@@ -450,7 +527,7 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
450
527
|
updatedAt: s.updatedAt
|
|
451
528
|
};
|
|
452
529
|
}),
|
|
453
|
-
...docs.items.map((d) => {
|
|
530
|
+
...docs.items.map((d, position) => {
|
|
454
531
|
const section = selectSection(d.outline, taskDescription);
|
|
455
532
|
const pointer = section ? formatSectionPointer(d.repo_full_name, d.path, section) : void 0;
|
|
456
533
|
refs.set(key("doc", d.id), {
|
|
@@ -469,7 +546,9 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
469
546
|
title: d.title,
|
|
470
547
|
content: formatDocHeadline(d),
|
|
471
548
|
category: d.doc_type === "adr" ? "architecture" : void 0,
|
|
472
|
-
updatedAt: d.synced_at
|
|
549
|
+
updatedAt: d.synced_at,
|
|
550
|
+
path: d.path,
|
|
551
|
+
relevance: docs.searched ? 1 - position / (2 * docs.items.length) : void 0
|
|
473
552
|
};
|
|
474
553
|
})
|
|
475
554
|
];
|
|
@@ -477,9 +556,16 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
477
556
|
const sections = [];
|
|
478
557
|
let overhead = 0;
|
|
479
558
|
if (stack.length > 0) {
|
|
559
|
+
const { text, hidden } = selectStack(
|
|
560
|
+
stack,
|
|
561
|
+
[...extractKeywords(taskDescription), ...detectLanguages(filePaths)],
|
|
562
|
+
Math.floor(budget * STACK_SHARE)
|
|
563
|
+
);
|
|
480
564
|
const section = `## Tech Stack
|
|
481
565
|
|
|
482
|
-
${
|
|
566
|
+
${text}` + (hidden > 0 ? `
|
|
567
|
+
|
|
568
|
+
_${hidden} more with \`get_tech_stack\`._` : "");
|
|
483
569
|
sections.push(section);
|
|
484
570
|
overhead += estimateTokens(section);
|
|
485
571
|
}
|
|
@@ -598,9 +684,10 @@ _Highest-ranked items \u2014 docs are scoped to their most relevant section. Eac
|
|
|
598
684
|
const tailLines = [];
|
|
599
685
|
for (const meta of tailCandidates) {
|
|
600
686
|
const cost = estimateTokens(meta.listLine);
|
|
601
|
-
|
|
687
|
+
const named = blocks.length + tailLines.length;
|
|
688
|
+
if (cost > remaining && named >= MIN_REFERENCES) break;
|
|
602
689
|
tailLines.push(meta.listLine);
|
|
603
|
-
remaining
|
|
690
|
+
remaining = Math.max(0, remaining - cost);
|
|
604
691
|
}
|
|
605
692
|
if (tailLines.length > 0) {
|
|
606
693
|
sections.push(
|
package/dist/lib/scoring.d.ts
CHANGED
|
@@ -15,6 +15,10 @@ interface ScorableItem {
|
|
|
15
15
|
category?: string;
|
|
16
16
|
severity?: string;
|
|
17
17
|
updatedAt?: string;
|
|
18
|
+
/** Where a document's repository file lives. */
|
|
19
|
+
path?: string;
|
|
20
|
+
/** 0 to 1, from the order the document search returned it in. */
|
|
21
|
+
relevance?: number;
|
|
18
22
|
}
|
|
19
23
|
export declare function estimateTokens(text: string): number;
|
|
20
24
|
/**
|
|
@@ -26,6 +30,7 @@ export declare function truncateToTokens(text: string, maxTokens: number): {
|
|
|
26
30
|
text: string;
|
|
27
31
|
truncated: boolean;
|
|
28
32
|
};
|
|
33
|
+
export declare function extractKeywords(text: string): string[];
|
|
29
34
|
export declare function scoreItems(items: ScorableItem[], taskDescription: string, filePaths: string[]): ScoredItem[];
|
|
30
35
|
export declare function selectWithinBudget(items: ScoredItem[], maxTokens: number): {
|
|
31
36
|
selected: ScoredItem[];
|
package/dist/lib/skills.d.ts
CHANGED
|
@@ -103,5 +103,12 @@ export declare function installSkill(version: SkillVersionFiles, dest: string, {
|
|
|
103
103
|
}): Promise<InstallResult>;
|
|
104
104
|
/** A fence longer than any backtick run in `text`, so content cannot close it. */
|
|
105
105
|
export declare function fenceFor(text: string): string;
|
|
106
|
+
/**
|
|
107
|
+
* What follows an install. Claude Code watches its skills folders, so a running
|
|
108
|
+
* session needs no restart, unless the top-level folder did not exist when the
|
|
109
|
+
* session started (code.claude.com/docs/en/skills, "Edit a skill during a
|
|
110
|
+
* session"): `/reload-skills` covers that, and reading the file works anywhere.
|
|
111
|
+
*/
|
|
112
|
+
export declare function afterInstall(skillMd: string): string;
|
|
106
113
|
/** Human summary of an install, shared by the tool and the CLI. */
|
|
107
114
|
export declare function describeInstall(name: string, r: InstallResult): string;
|
package/dist/lib/skills.js
CHANGED
|
@@ -182,6 +182,9 @@ function fenceFor(text) {
|
|
|
182
182
|
const longest = Math.max(0, ...(text.match(/`+/g) ?? []).map((r) => r.length));
|
|
183
183
|
return "`".repeat(Math.max(3, longest + 1));
|
|
184
184
|
}
|
|
185
|
+
function afterInstall(skillMd) {
|
|
186
|
+
return `No restart is needed: a running Claude Code session picks it up. If one does not list it yet, read ${skillMd} and follow it, or run /reload-skills.`;
|
|
187
|
+
}
|
|
185
188
|
function describeInstall(name, r) {
|
|
186
189
|
const lines = [
|
|
187
190
|
`Installed ${name} version ${r.version} into ${r.dest} (${r.written.length} file(s)).`
|
|
@@ -191,7 +194,7 @@ function describeInstall(name, r) {
|
|
|
191
194
|
lines.push(`Kept, edited here and no longer in the skill: ${r.keptEdited.join(", ")}`);
|
|
192
195
|
}
|
|
193
196
|
if (r.keptUntracked.length) lines.push(`Kept, added here: ${r.keptUntracked.join(", ")}`);
|
|
194
|
-
lines.push("
|
|
197
|
+
lines.push(afterInstall(join(r.dest, "SKILL.md")));
|
|
195
198
|
return lines.join("\n");
|
|
196
199
|
}
|
|
197
200
|
export {
|
|
@@ -201,6 +204,7 @@ export {
|
|
|
201
204
|
MAX_FILE_BYTES,
|
|
202
205
|
MAX_TOTAL_BYTES,
|
|
203
206
|
SkillInstallRefused,
|
|
207
|
+
afterInstall,
|
|
204
208
|
defaultSkillDir,
|
|
205
209
|
describeInstall,
|
|
206
210
|
fenceFor,
|
package/dist/server.js
CHANGED
|
@@ -188,6 +188,17 @@ ${text3}`
|
|
|
188
188
|
|
|
189
189
|
// src/tools/get-specs.ts
|
|
190
190
|
import { z as z2 } from "zod";
|
|
191
|
+
|
|
192
|
+
// src/lib/spec-label.ts
|
|
193
|
+
var DECISION_PREFIX = "ADR: ";
|
|
194
|
+
function isDecisionDraft(name) {
|
|
195
|
+
return name.startsWith(DECISION_PREFIX);
|
|
196
|
+
}
|
|
197
|
+
function specLabel(spec) {
|
|
198
|
+
return isDecisionDraft(spec.name) ? "decision record" : `${spec.requirementCount} requirements`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/tools/get-specs.ts
|
|
191
202
|
function registerGetSpecs(server, apiClient) {
|
|
192
203
|
server.registerTool(
|
|
193
204
|
"get_specs",
|
|
@@ -252,7 +263,7 @@ ${spec.taskContent}`);
|
|
|
252
263
|
};
|
|
253
264
|
}
|
|
254
265
|
const text3 = filtered.map(
|
|
255
|
-
(s) => `- **${s.name}** (${s.draftId}) \u2014 ${s.status}, ${s
|
|
266
|
+
(s) => `- **${s.name}** (${s.draftId}) \u2014 ${s.status}, ${specLabel(s)}` + (s.repoFullName ? `, repo: ${s.repoFullName}` : "")
|
|
256
267
|
).join("\n");
|
|
257
268
|
return {
|
|
258
269
|
content: [
|
|
@@ -270,6 +281,11 @@ ${text3}`
|
|
|
270
281
|
|
|
271
282
|
// src/tools/get-docs.ts
|
|
272
283
|
import { z as z3 } from "zod";
|
|
284
|
+
var PATH_FILTER_LIMIT = "1000";
|
|
285
|
+
function literalPrefix(pattern) {
|
|
286
|
+
const wildcard = pattern.indexOf("*");
|
|
287
|
+
return wildcard < 0 ? pattern : pattern.slice(0, wildcard);
|
|
288
|
+
}
|
|
273
289
|
function matchGlob(path, pattern) {
|
|
274
290
|
const regex = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\{\{GLOBSTAR\}\}/g, ".*");
|
|
275
291
|
return new RegExp(`^${regex}$`).test(path);
|
|
@@ -330,11 +346,17 @@ ${doc.content}`;
|
|
|
330
346
|
const params = {};
|
|
331
347
|
if (repo) params.repo = repo;
|
|
332
348
|
if (search) params.q = search;
|
|
349
|
+
if (path) {
|
|
350
|
+
params.limit = PATH_FILTER_LIMIT;
|
|
351
|
+
const prefix = literalPrefix(path);
|
|
352
|
+
if (prefix) params.pathPrefix = prefix;
|
|
353
|
+
}
|
|
333
354
|
const response = await apiClient.get(
|
|
334
355
|
"/v1/docs",
|
|
335
356
|
params
|
|
336
357
|
);
|
|
337
358
|
let items = response.items;
|
|
359
|
+
const unread = path ? response.total - response.items.length : 0;
|
|
338
360
|
if (path) {
|
|
339
361
|
items = items.filter((item) => matchGlob(item.path, path));
|
|
340
362
|
}
|
|
@@ -364,7 +386,9 @@ ${doc.content}`;
|
|
|
364
386
|
|
|
365
387
|
${text3}
|
|
366
388
|
|
|
367
|
-
|
|
389
|
+
` + (unread > 0 ? `${unread} more were not read. Start the path with a folder to narrow it.
|
|
390
|
+
|
|
391
|
+
` : "") + "Use doc_id for full content, or doc_id + section to fetch one section."
|
|
368
392
|
}
|
|
369
393
|
]
|
|
370
394
|
};
|
|
@@ -465,7 +489,9 @@ ${deps}`);
|
|
|
465
489
|
}
|
|
466
490
|
if (ctx.adrs.length > 0) {
|
|
467
491
|
const adrs = ctx.adrs.map((a) => `- ${a.title}${a.repo ? ` (${a.repo})` : ""}`).join("\n");
|
|
468
|
-
|
|
492
|
+
const total = ctx.adrTotal ?? ctx.adrs.length;
|
|
493
|
+
const count = total > ctx.adrs.length ? `${ctx.adrs.length} newest of ${total}` : `${ctx.adrs.length}`;
|
|
494
|
+
sections.push(`## Architecture Decision Records (${count})
|
|
469
495
|
|
|
470
496
|
${adrs}`);
|
|
471
497
|
}
|
|
@@ -837,6 +863,9 @@ var SEVERITY_SCORES = {
|
|
|
837
863
|
medium: 0.5,
|
|
838
864
|
low: 0.25
|
|
839
865
|
};
|
|
866
|
+
var MIN_TITLE_SHARE = 1 / 3;
|
|
867
|
+
var RECORD_PATH = /(^|\/)changelog\//;
|
|
868
|
+
var RECORD_FACTOR = 0.5;
|
|
840
869
|
var CHARS_PER_TOKEN = 4;
|
|
841
870
|
function estimateTokens(text3) {
|
|
842
871
|
return Math.ceil(text3.length / CHARS_PER_TOKEN);
|
|
@@ -889,6 +918,32 @@ function scoreSeverity(item) {
|
|
|
889
918
|
if (!item.severity) return 0.5;
|
|
890
919
|
return SEVERITY_SCORES[item.severity] ?? 0.5;
|
|
891
920
|
}
|
|
921
|
+
function stem(word) {
|
|
922
|
+
return word.length > 3 ? word.replace(/s$/, "") : word;
|
|
923
|
+
}
|
|
924
|
+
function stems(text3) {
|
|
925
|
+
return new Set(
|
|
926
|
+
text3.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean).map(stem)
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
function titleShare(title, keywords) {
|
|
930
|
+
const wanted = stems(keywords.join(" "));
|
|
931
|
+
if (wanted.size === 0) return 0;
|
|
932
|
+
const held = stems(title);
|
|
933
|
+
let matches = 0;
|
|
934
|
+
for (const word of wanted) {
|
|
935
|
+
if (held.has(word)) matches++;
|
|
936
|
+
}
|
|
937
|
+
return matches / wanted.size;
|
|
938
|
+
}
|
|
939
|
+
function scoreReference(item, keywords) {
|
|
940
|
+
const share = titleShare(item.title, keywords);
|
|
941
|
+
const score = Math.max(
|
|
942
|
+
item.relevance ?? 0,
|
|
943
|
+
share >= MIN_TITLE_SHARE ? share : 0
|
|
944
|
+
);
|
|
945
|
+
return RECORD_PATH.test(item.path ?? "") ? score * RECORD_FACTOR : score;
|
|
946
|
+
}
|
|
892
947
|
function extractKeywords(text3) {
|
|
893
948
|
const stopWords = /* @__PURE__ */ new Set([
|
|
894
949
|
"the",
|
|
@@ -985,7 +1040,8 @@ function scoreItems(items, taskDescription, filePaths) {
|
|
|
985
1040
|
const keywordScore = scoreKeywordMatch(item, keywords);
|
|
986
1041
|
const recencyScore = scoreRecency(item);
|
|
987
1042
|
const severityScore = scoreSeverity(item);
|
|
988
|
-
const
|
|
1043
|
+
const isReference = item.type === "spec" || item.type === "doc";
|
|
1044
|
+
const score = isReference && keywords.length > 0 ? scoreReference(item, keywords) : pathScore * WEIGHT_PATH + keywordScore * WEIGHT_KEYWORD + recencyScore * WEIGHT_RECENCY + severityScore * WEIGHT_SEVERITY;
|
|
989
1045
|
return {
|
|
990
1046
|
type: item.type,
|
|
991
1047
|
id: item.id,
|
|
@@ -994,7 +1050,7 @@ function scoreItems(items, taskDescription, filePaths) {
|
|
|
994
1050
|
score,
|
|
995
1051
|
tokenEstimate: estimateTokens(item.content)
|
|
996
1052
|
};
|
|
997
|
-
}).sort((a, b) => b.score - a.score);
|
|
1053
|
+
}).filter((item) => item.score > 0).sort((a, b) => b.score - a.score);
|
|
998
1054
|
}
|
|
999
1055
|
function selectWithinBudget(items, maxTokens) {
|
|
1000
1056
|
const selected = [];
|
|
@@ -1016,6 +1072,10 @@ var MAX_HYDRATED = 8;
|
|
|
1016
1072
|
var MAX_TAIL = 10;
|
|
1017
1073
|
var TAIL_SHARE = 0.15;
|
|
1018
1074
|
var TRUNCATION_NOTICE_TOKENS = 30;
|
|
1075
|
+
var STACK_SHARE = 0.1;
|
|
1076
|
+
var MIN_REFERENCES = 3;
|
|
1077
|
+
var SEARCH_RESULTS = 30;
|
|
1078
|
+
var MAX_QUERY_CHARS = 300;
|
|
1019
1079
|
function formatGuardrail(g) {
|
|
1020
1080
|
let text3 = `## ${g.title} [${g.severity}]
|
|
1021
1081
|
|
|
@@ -1034,7 +1094,7 @@ ${g.fix}
|
|
|
1034
1094
|
}
|
|
1035
1095
|
function formatSpecHeadline(s) {
|
|
1036
1096
|
return `### ${s.name}
|
|
1037
|
-
\`draft_id: ${s.draftId}\` \xB7 ${s.status} \xB7 ${s
|
|
1097
|
+
\`draft_id: ${s.draftId}\` \xB7 ${s.status} \xB7 ${specLabel(s)}`;
|
|
1038
1098
|
}
|
|
1039
1099
|
function formatDocHeadline(d) {
|
|
1040
1100
|
return `### ${d.title}
|
|
@@ -1045,6 +1105,21 @@ function formatStack(entries2) {
|
|
|
1045
1105
|
(e) => `- ${e.name}${e.version ? ` v${e.version}` : ""}${e.notes ? ` \u2014 ${e.notes}` : ""}`
|
|
1046
1106
|
).join("\n");
|
|
1047
1107
|
}
|
|
1108
|
+
function selectStack(entries2, words, maxTokens) {
|
|
1109
|
+
const named = (e) => {
|
|
1110
|
+
const entry = `${e.name} ${e.notes ?? ""}`.toLowerCase();
|
|
1111
|
+
return words.some((w) => entry.includes(w));
|
|
1112
|
+
};
|
|
1113
|
+
const ordered = [...entries2.filter(named), ...entries2.filter((e) => !named(e))];
|
|
1114
|
+
const shown = [];
|
|
1115
|
+
for (const entry of ordered) {
|
|
1116
|
+
if (shown.length > 0 && estimateTokens(formatStack([...shown, entry])) > maxTokens) {
|
|
1117
|
+
break;
|
|
1118
|
+
}
|
|
1119
|
+
shown.push(entry);
|
|
1120
|
+
}
|
|
1121
|
+
return { text: formatStack(shown), hidden: entries2.length - shown.length };
|
|
1122
|
+
}
|
|
1048
1123
|
function formatRules(rules) {
|
|
1049
1124
|
const byCategory = /* @__PURE__ */ new Map();
|
|
1050
1125
|
for (const r of rules) {
|
|
@@ -1066,6 +1141,21 @@ function formatRules(rules) {
|
|
|
1066
1141
|
|
|
1067
1142
|
${text3.trim()}`;
|
|
1068
1143
|
}
|
|
1144
|
+
async function findDocs(api, taskDescription) {
|
|
1145
|
+
const query = taskDescription.trim().slice(0, MAX_QUERY_CHARS);
|
|
1146
|
+
if (query) {
|
|
1147
|
+
try {
|
|
1148
|
+
const found = await api.get("/v1/docs", {
|
|
1149
|
+
q: query,
|
|
1150
|
+
limit: String(SEARCH_RESULTS)
|
|
1151
|
+
});
|
|
1152
|
+
if (found.items.length > 0) return { ...found, searched: true };
|
|
1153
|
+
} catch {
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
const listed = await api.get("/v1/docs");
|
|
1157
|
+
return { ...listed, searched: false };
|
|
1158
|
+
}
|
|
1069
1159
|
async function fetchSpecBody(api, draftId) {
|
|
1070
1160
|
const spec = await api.get(
|
|
1071
1161
|
`/v1/specs/${encodeURIComponent(draftId)}`
|
|
@@ -1166,7 +1256,9 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
1166
1256
|
const [rawGuardrails, rawSpecs, docs, stack, rules, docRepos] = await Promise.all([
|
|
1167
1257
|
api.get("/v1/guardrails").catch(orEmpty([])),
|
|
1168
1258
|
api.get("/v1/specs").catch(orEmpty([])),
|
|
1169
|
-
api
|
|
1259
|
+
findDocs(api, taskDescription).catch(
|
|
1260
|
+
orEmpty({ items: [], searched: false })
|
|
1261
|
+
),
|
|
1170
1262
|
api.get("/v1/ai/settings/stack").catch(orEmpty([])),
|
|
1171
1263
|
api.get("/v1/standards/rules").catch(orEmpty([])),
|
|
1172
1264
|
// Capability states — FAIL-OPEN: an unreadable config (older backend, a
|
|
@@ -1179,6 +1271,8 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
1179
1271
|
const specs = disabled("requirements") && disabled("design") && disabled("specs") ? [] : rawSpecs;
|
|
1180
1272
|
const refs = /* @__PURE__ */ new Map();
|
|
1181
1273
|
const key = (type, id) => `${type}:${id}`;
|
|
1274
|
+
const docTitles = new Set(docs.items.map((d) => d.title));
|
|
1275
|
+
const isCopyOfDoc = (s) => isDecisionDraft(s.name) && docTitles.has(s.name.slice("ADR: ".length));
|
|
1182
1276
|
const scorable = [
|
|
1183
1277
|
...guardrails.map((g) => ({
|
|
1184
1278
|
type: "guardrail",
|
|
@@ -1189,12 +1283,12 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
1189
1283
|
severity: g.severity,
|
|
1190
1284
|
updatedAt: g.updatedAt
|
|
1191
1285
|
})),
|
|
1192
|
-
...specs.filter((s) => s.status !== "archived").map((s) => {
|
|
1286
|
+
...specs.filter((s) => s.status !== "archived" && !isCopyOfDoc(s)).map((s) => {
|
|
1193
1287
|
refs.set(key("spec", s.draftId), {
|
|
1194
1288
|
type: "spec",
|
|
1195
1289
|
id: s.draftId,
|
|
1196
1290
|
title: s.name,
|
|
1197
|
-
listLine: `- **${s.name}** \u2014 \`draft_id: ${s.draftId}\` (${s.status}, ${s
|
|
1291
|
+
listLine: `- **${s.name}** \u2014 \`draft_id: ${s.draftId}\` (${s.status}, ${specLabel(s)})`,
|
|
1198
1292
|
fetchHint: `get_specs(draft_id="${s.draftId}")`
|
|
1199
1293
|
});
|
|
1200
1294
|
return {
|
|
@@ -1205,7 +1299,7 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
1205
1299
|
updatedAt: s.updatedAt
|
|
1206
1300
|
};
|
|
1207
1301
|
}),
|
|
1208
|
-
...docs.items.map((d) => {
|
|
1302
|
+
...docs.items.map((d, position) => {
|
|
1209
1303
|
const section = selectSection(d.outline, taskDescription);
|
|
1210
1304
|
const pointer = section ? formatSectionPointer(d.repo_full_name, d.path, section) : void 0;
|
|
1211
1305
|
refs.set(key("doc", d.id), {
|
|
@@ -1224,7 +1318,9 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
1224
1318
|
title: d.title,
|
|
1225
1319
|
content: formatDocHeadline(d),
|
|
1226
1320
|
category: d.doc_type === "adr" ? "architecture" : void 0,
|
|
1227
|
-
updatedAt: d.synced_at
|
|
1321
|
+
updatedAt: d.synced_at,
|
|
1322
|
+
path: d.path,
|
|
1323
|
+
relevance: docs.searched ? 1 - position / (2 * docs.items.length) : void 0
|
|
1228
1324
|
};
|
|
1229
1325
|
})
|
|
1230
1326
|
];
|
|
@@ -1232,9 +1328,16 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
1232
1328
|
const sections = [];
|
|
1233
1329
|
let overhead = 0;
|
|
1234
1330
|
if (stack.length > 0) {
|
|
1331
|
+
const { text: text3, hidden } = selectStack(
|
|
1332
|
+
stack,
|
|
1333
|
+
[...extractKeywords(taskDescription), ...detectLanguages(filePaths)],
|
|
1334
|
+
Math.floor(budget * STACK_SHARE)
|
|
1335
|
+
);
|
|
1235
1336
|
const section = `## Tech Stack
|
|
1236
1337
|
|
|
1237
|
-
${
|
|
1338
|
+
${text3}` + (hidden > 0 ? `
|
|
1339
|
+
|
|
1340
|
+
_${hidden} more with \`get_tech_stack\`._` : "");
|
|
1238
1341
|
sections.push(section);
|
|
1239
1342
|
overhead += estimateTokens(section);
|
|
1240
1343
|
}
|
|
@@ -1353,9 +1456,10 @@ _Highest-ranked items \u2014 docs are scoped to their most relevant section. Eac
|
|
|
1353
1456
|
const tailLines = [];
|
|
1354
1457
|
for (const meta of tailCandidates) {
|
|
1355
1458
|
const cost = estimateTokens(meta.listLine);
|
|
1356
|
-
|
|
1459
|
+
const named = blocks.length + tailLines.length;
|
|
1460
|
+
if (cost > remaining && named >= MIN_REFERENCES) break;
|
|
1357
1461
|
tailLines.push(meta.listLine);
|
|
1358
|
-
remaining
|
|
1462
|
+
remaining = Math.max(0, remaining - cost);
|
|
1359
1463
|
}
|
|
1360
1464
|
if (tailLines.length > 0) {
|
|
1361
1465
|
sections.push(
|
|
@@ -1607,6 +1711,9 @@ function fenceFor(text3) {
|
|
|
1607
1711
|
const longest = Math.max(0, ...(text3.match(/`+/g) ?? []).map((r) => r.length));
|
|
1608
1712
|
return "`".repeat(Math.max(3, longest + 1));
|
|
1609
1713
|
}
|
|
1714
|
+
function afterInstall(skillMd) {
|
|
1715
|
+
return `No restart is needed: a running Claude Code session picks it up. If one does not list it yet, read ${skillMd} and follow it, or run /reload-skills.`;
|
|
1716
|
+
}
|
|
1610
1717
|
function describeInstall(name, r) {
|
|
1611
1718
|
const lines = [
|
|
1612
1719
|
`Installed ${name} version ${r.version} into ${r.dest} (${r.written.length} file(s)).`
|
|
@@ -1616,7 +1723,7 @@ function describeInstall(name, r) {
|
|
|
1616
1723
|
lines.push(`Kept, edited here and no longer in the skill: ${r.keptEdited.join(", ")}`);
|
|
1617
1724
|
}
|
|
1618
1725
|
if (r.keptUntracked.length) lines.push(`Kept, added here: ${r.keptUntracked.join(", ")}`);
|
|
1619
|
-
lines.push("
|
|
1726
|
+
lines.push(afterInstall(join(r.dest, "SKILL.md")));
|
|
1620
1727
|
return lines.join("\n");
|
|
1621
1728
|
}
|
|
1622
1729
|
|
|
@@ -2498,7 +2605,7 @@ ${fence}
|
|
|
2498
2605
|
${f.content}${f.content.endsWith("\n") ? "" : "\n"}${fence}`;
|
|
2499
2606
|
}).join("\n\n");
|
|
2500
2607
|
return text(
|
|
2501
|
-
`Skill **${version.name}** version ${version.version}: write these ${version.files.length} file(s) into ${dest}/ exactly as shown (the content between the fences), creating folders as needed. If a file there differs from ${MANIFEST_FILE}'s hash for it, it was edited: ask before replacing it. Then write ${MANIFEST_FILE} with this content
|
|
2608
|
+
`Skill **${version.name}** version ${version.version}: write these ${version.files.length} file(s) into ${dest}/ exactly as shown (the content between the fences), creating folders as needed. If a file there differs from ${MANIFEST_FILE}'s hash for it, it was edited: ask before replacing it. Then write ${MANIFEST_FILE} with this content. ${afterInstall(`${dest}/SKILL.md`)}
|
|
2502
2609
|
|
|
2503
2610
|
### ${MANIFEST_FILE}
|
|
2504
2611
|
|
|
@@ -2655,9 +2762,9 @@ function localDate(now) {
|
|
|
2655
2762
|
return `${now.getFullYear()}-${p(now.getMonth() + 1)}-${p(now.getDate())}`;
|
|
2656
2763
|
}
|
|
2657
2764
|
function conflictCopyName(path, date, taken) {
|
|
2658
|
-
const
|
|
2765
|
+
const stem2 = path.replace(/\.md$/, "");
|
|
2659
2766
|
for (let n = 1; ; n++) {
|
|
2660
|
-
const name = `${
|
|
2767
|
+
const name = `${stem2}.local-${date}${n === 1 ? "" : `-${n}`}.md`;
|
|
2661
2768
|
if (!taken.has(name.toLowerCase())) return name;
|
|
2662
2769
|
}
|
|
2663
2770
|
}
|
|
@@ -3544,7 +3651,7 @@ function registerTeamSetupTools(server, apiClient, { localFiles }) {
|
|
|
3544
3651
|
}
|
|
3545
3652
|
|
|
3546
3653
|
// src/server.ts
|
|
3547
|
-
var SERVER_VERSION = true ? "0.1.
|
|
3654
|
+
var SERVER_VERSION = true ? "0.1.52" : "0.0.0-dev";
|
|
3548
3655
|
function createServer(apiClient, { localFiles = false, instructions, keyProblem } = {}) {
|
|
3549
3656
|
const server = new McpServer(
|
|
3550
3657
|
{
|
package/dist/setup-mcp.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/setup-mcp.ts
|
|
4
4
|
import { existsSync as existsSync3, readFileSync } from "fs";
|
|
5
|
+
import { mkdir as mkdir4 } from "fs/promises";
|
|
5
6
|
import { join as join4 } from "path";
|
|
6
7
|
|
|
7
8
|
// src/lib/cache.ts
|
|
@@ -591,6 +592,7 @@ async function setupMcpServer(opts) {
|
|
|
591
592
|
}
|
|
592
593
|
try {
|
|
593
594
|
if ((await readCredentials())[workspace]?.apiKey !== apiKey) await writeCredentials(workspace, apiKey);
|
|
595
|
+
await mkdir4(join4(homeDir(), ".claude", "skills"), { recursive: true });
|
|
594
596
|
const several = Object.keys(await readCredentials()).length > 1;
|
|
595
597
|
for (const target of selected) {
|
|
596
598
|
const removed = await writeServerEntry(target.path, apiUrl, several ? workspace : void 0);
|
package/dist/tools/get-docs.js
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
// src/tools/get-docs.ts
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
var PATH_FILTER_LIMIT = "1000";
|
|
6
|
+
function literalPrefix(pattern) {
|
|
7
|
+
const wildcard = pattern.indexOf("*");
|
|
8
|
+
return wildcard < 0 ? pattern : pattern.slice(0, wildcard);
|
|
9
|
+
}
|
|
5
10
|
function matchGlob(path, pattern) {
|
|
6
11
|
const regex = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\{\{GLOBSTAR\}\}/g, ".*");
|
|
7
12
|
return new RegExp(`^${regex}$`).test(path);
|
|
@@ -62,11 +67,17 @@ ${doc.content}`;
|
|
|
62
67
|
const params = {};
|
|
63
68
|
if (repo) params.repo = repo;
|
|
64
69
|
if (search) params.q = search;
|
|
70
|
+
if (path) {
|
|
71
|
+
params.limit = PATH_FILTER_LIMIT;
|
|
72
|
+
const prefix = literalPrefix(path);
|
|
73
|
+
if (prefix) params.pathPrefix = prefix;
|
|
74
|
+
}
|
|
65
75
|
const response = await apiClient.get(
|
|
66
76
|
"/v1/docs",
|
|
67
77
|
params
|
|
68
78
|
);
|
|
69
79
|
let items = response.items;
|
|
80
|
+
const unread = path ? response.total - response.items.length : 0;
|
|
70
81
|
if (path) {
|
|
71
82
|
items = items.filter((item) => matchGlob(item.path, path));
|
|
72
83
|
}
|
|
@@ -96,7 +107,9 @@ ${doc.content}`;
|
|
|
96
107
|
|
|
97
108
|
${text}
|
|
98
109
|
|
|
99
|
-
|
|
110
|
+
` + (unread > 0 ? `${unread} more were not read. Start the path with a folder to narrow it.
|
|
111
|
+
|
|
112
|
+
` : "") + "Use doc_id for full content, or doc_id + section to fetch one section."
|
|
100
113
|
}
|
|
101
114
|
]
|
|
102
115
|
};
|