@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
package/dist/http.js
CHANGED
|
@@ -466,6 +466,9 @@ function fenceFor(text3) {
|
|
|
466
466
|
const longest = Math.max(0, ...(text3.match(/`+/g) ?? []).map((r) => r.length));
|
|
467
467
|
return "`".repeat(Math.max(3, longest + 1));
|
|
468
468
|
}
|
|
469
|
+
function afterInstall(skillMd) {
|
|
470
|
+
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.`;
|
|
471
|
+
}
|
|
469
472
|
function describeInstall(name, r) {
|
|
470
473
|
const lines = [
|
|
471
474
|
`Installed ${name} version ${r.version} into ${r.dest} (${r.written.length} file(s)).`
|
|
@@ -475,7 +478,7 @@ function describeInstall(name, r) {
|
|
|
475
478
|
lines.push(`Kept, edited here and no longer in the skill: ${r.keptEdited.join(", ")}`);
|
|
476
479
|
}
|
|
477
480
|
if (r.keptUntracked.length) lines.push(`Kept, added here: ${r.keptUntracked.join(", ")}`);
|
|
478
|
-
lines.push("
|
|
481
|
+
lines.push(afterInstall(join(r.dest, "SKILL.md")));
|
|
479
482
|
return lines.join("\n");
|
|
480
483
|
}
|
|
481
484
|
|
|
@@ -848,6 +851,17 @@ ${text3}`
|
|
|
848
851
|
|
|
849
852
|
// src/tools/get-specs.ts
|
|
850
853
|
import { z as z2 } from "zod";
|
|
854
|
+
|
|
855
|
+
// src/lib/spec-label.ts
|
|
856
|
+
var DECISION_PREFIX = "ADR: ";
|
|
857
|
+
function isDecisionDraft(name) {
|
|
858
|
+
return name.startsWith(DECISION_PREFIX);
|
|
859
|
+
}
|
|
860
|
+
function specLabel(spec) {
|
|
861
|
+
return isDecisionDraft(spec.name) ? "decision record" : `${spec.requirementCount} requirements`;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// src/tools/get-specs.ts
|
|
851
865
|
function registerGetSpecs(server, apiClient) {
|
|
852
866
|
server.registerTool(
|
|
853
867
|
"get_specs",
|
|
@@ -912,7 +926,7 @@ ${spec.taskContent}`);
|
|
|
912
926
|
};
|
|
913
927
|
}
|
|
914
928
|
const text3 = filtered.map(
|
|
915
|
-
(s) => `- **${s.name}** (${s.draftId}) \u2014 ${s.status}, ${s
|
|
929
|
+
(s) => `- **${s.name}** (${s.draftId}) \u2014 ${s.status}, ${specLabel(s)}` + (s.repoFullName ? `, repo: ${s.repoFullName}` : "")
|
|
916
930
|
).join("\n");
|
|
917
931
|
return {
|
|
918
932
|
content: [
|
|
@@ -930,6 +944,11 @@ ${text3}`
|
|
|
930
944
|
|
|
931
945
|
// src/tools/get-docs.ts
|
|
932
946
|
import { z as z3 } from "zod";
|
|
947
|
+
var PATH_FILTER_LIMIT = "1000";
|
|
948
|
+
function literalPrefix(pattern) {
|
|
949
|
+
const wildcard = pattern.indexOf("*");
|
|
950
|
+
return wildcard < 0 ? pattern : pattern.slice(0, wildcard);
|
|
951
|
+
}
|
|
933
952
|
function matchGlob(path, pattern) {
|
|
934
953
|
const regex = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\{\{GLOBSTAR\}\}/g, ".*");
|
|
935
954
|
return new RegExp(`^${regex}$`).test(path);
|
|
@@ -990,11 +1009,17 @@ ${doc.content}`;
|
|
|
990
1009
|
const params = {};
|
|
991
1010
|
if (repo) params.repo = repo;
|
|
992
1011
|
if (search) params.q = search;
|
|
1012
|
+
if (path) {
|
|
1013
|
+
params.limit = PATH_FILTER_LIMIT;
|
|
1014
|
+
const prefix = literalPrefix(path);
|
|
1015
|
+
if (prefix) params.pathPrefix = prefix;
|
|
1016
|
+
}
|
|
993
1017
|
const response = await apiClient.get(
|
|
994
1018
|
"/v1/docs",
|
|
995
1019
|
params
|
|
996
1020
|
);
|
|
997
1021
|
let items = response.items;
|
|
1022
|
+
const unread = path ? response.total - response.items.length : 0;
|
|
998
1023
|
if (path) {
|
|
999
1024
|
items = items.filter((item) => matchGlob(item.path, path));
|
|
1000
1025
|
}
|
|
@@ -1024,7 +1049,9 @@ ${doc.content}`;
|
|
|
1024
1049
|
|
|
1025
1050
|
${text3}
|
|
1026
1051
|
|
|
1027
|
-
|
|
1052
|
+
` + (unread > 0 ? `${unread} more were not read. Start the path with a folder to narrow it.
|
|
1053
|
+
|
|
1054
|
+
` : "") + "Use doc_id for full content, or doc_id + section to fetch one section."
|
|
1028
1055
|
}
|
|
1029
1056
|
]
|
|
1030
1057
|
};
|
|
@@ -1125,7 +1152,9 @@ ${deps}`);
|
|
|
1125
1152
|
}
|
|
1126
1153
|
if (ctx.adrs.length > 0) {
|
|
1127
1154
|
const adrs = ctx.adrs.map((a) => `- ${a.title}${a.repo ? ` (${a.repo})` : ""}`).join("\n");
|
|
1128
|
-
|
|
1155
|
+
const total = ctx.adrTotal ?? ctx.adrs.length;
|
|
1156
|
+
const count = total > ctx.adrs.length ? `${ctx.adrs.length} newest of ${total}` : `${ctx.adrs.length}`;
|
|
1157
|
+
sections.push(`## Architecture Decision Records (${count})
|
|
1129
1158
|
|
|
1130
1159
|
${adrs}`);
|
|
1131
1160
|
}
|
|
@@ -1497,6 +1526,9 @@ var SEVERITY_SCORES = {
|
|
|
1497
1526
|
medium: 0.5,
|
|
1498
1527
|
low: 0.25
|
|
1499
1528
|
};
|
|
1529
|
+
var MIN_TITLE_SHARE = 1 / 3;
|
|
1530
|
+
var RECORD_PATH = /(^|\/)changelog\//;
|
|
1531
|
+
var RECORD_FACTOR = 0.5;
|
|
1500
1532
|
var CHARS_PER_TOKEN = 4;
|
|
1501
1533
|
function estimateTokens(text3) {
|
|
1502
1534
|
return Math.ceil(text3.length / CHARS_PER_TOKEN);
|
|
@@ -1549,6 +1581,32 @@ function scoreSeverity(item) {
|
|
|
1549
1581
|
if (!item.severity) return 0.5;
|
|
1550
1582
|
return SEVERITY_SCORES[item.severity] ?? 0.5;
|
|
1551
1583
|
}
|
|
1584
|
+
function stem(word) {
|
|
1585
|
+
return word.length > 3 ? word.replace(/s$/, "") : word;
|
|
1586
|
+
}
|
|
1587
|
+
function stems(text3) {
|
|
1588
|
+
return new Set(
|
|
1589
|
+
text3.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean).map(stem)
|
|
1590
|
+
);
|
|
1591
|
+
}
|
|
1592
|
+
function titleShare(title, keywords) {
|
|
1593
|
+
const wanted = stems(keywords.join(" "));
|
|
1594
|
+
if (wanted.size === 0) return 0;
|
|
1595
|
+
const held = stems(title);
|
|
1596
|
+
let matches = 0;
|
|
1597
|
+
for (const word of wanted) {
|
|
1598
|
+
if (held.has(word)) matches++;
|
|
1599
|
+
}
|
|
1600
|
+
return matches / wanted.size;
|
|
1601
|
+
}
|
|
1602
|
+
function scoreReference(item, keywords) {
|
|
1603
|
+
const share = titleShare(item.title, keywords);
|
|
1604
|
+
const score = Math.max(
|
|
1605
|
+
item.relevance ?? 0,
|
|
1606
|
+
share >= MIN_TITLE_SHARE ? share : 0
|
|
1607
|
+
);
|
|
1608
|
+
return RECORD_PATH.test(item.path ?? "") ? score * RECORD_FACTOR : score;
|
|
1609
|
+
}
|
|
1552
1610
|
function extractKeywords(text3) {
|
|
1553
1611
|
const stopWords = /* @__PURE__ */ new Set([
|
|
1554
1612
|
"the",
|
|
@@ -1645,7 +1703,8 @@ function scoreItems(items, taskDescription, filePaths) {
|
|
|
1645
1703
|
const keywordScore = scoreKeywordMatch(item, keywords);
|
|
1646
1704
|
const recencyScore = scoreRecency(item);
|
|
1647
1705
|
const severityScore = scoreSeverity(item);
|
|
1648
|
-
const
|
|
1706
|
+
const isReference = item.type === "spec" || item.type === "doc";
|
|
1707
|
+
const score = isReference && keywords.length > 0 ? scoreReference(item, keywords) : pathScore * WEIGHT_PATH + keywordScore * WEIGHT_KEYWORD + recencyScore * WEIGHT_RECENCY + severityScore * WEIGHT_SEVERITY;
|
|
1649
1708
|
return {
|
|
1650
1709
|
type: item.type,
|
|
1651
1710
|
id: item.id,
|
|
@@ -1654,7 +1713,7 @@ function scoreItems(items, taskDescription, filePaths) {
|
|
|
1654
1713
|
score,
|
|
1655
1714
|
tokenEstimate: estimateTokens(item.content)
|
|
1656
1715
|
};
|
|
1657
|
-
}).sort((a, b) => b.score - a.score);
|
|
1716
|
+
}).filter((item) => item.score > 0).sort((a, b) => b.score - a.score);
|
|
1658
1717
|
}
|
|
1659
1718
|
function selectWithinBudget(items, maxTokens) {
|
|
1660
1719
|
const selected = [];
|
|
@@ -1676,6 +1735,10 @@ var MAX_HYDRATED = 8;
|
|
|
1676
1735
|
var MAX_TAIL = 10;
|
|
1677
1736
|
var TAIL_SHARE = 0.15;
|
|
1678
1737
|
var TRUNCATION_NOTICE_TOKENS = 30;
|
|
1738
|
+
var STACK_SHARE = 0.1;
|
|
1739
|
+
var MIN_REFERENCES = 3;
|
|
1740
|
+
var SEARCH_RESULTS = 30;
|
|
1741
|
+
var MAX_QUERY_CHARS = 300;
|
|
1679
1742
|
function formatGuardrail(g) {
|
|
1680
1743
|
let text3 = `## ${g.title} [${g.severity}]
|
|
1681
1744
|
|
|
@@ -1694,7 +1757,7 @@ ${g.fix}
|
|
|
1694
1757
|
}
|
|
1695
1758
|
function formatSpecHeadline(s) {
|
|
1696
1759
|
return `### ${s.name}
|
|
1697
|
-
\`draft_id: ${s.draftId}\` \xB7 ${s.status} \xB7 ${s
|
|
1760
|
+
\`draft_id: ${s.draftId}\` \xB7 ${s.status} \xB7 ${specLabel(s)}`;
|
|
1698
1761
|
}
|
|
1699
1762
|
function formatDocHeadline(d) {
|
|
1700
1763
|
return `### ${d.title}
|
|
@@ -1705,6 +1768,21 @@ function formatStack(entries2) {
|
|
|
1705
1768
|
(e) => `- ${e.name}${e.version ? ` v${e.version}` : ""}${e.notes ? ` \u2014 ${e.notes}` : ""}`
|
|
1706
1769
|
).join("\n");
|
|
1707
1770
|
}
|
|
1771
|
+
function selectStack(entries2, words, maxTokens) {
|
|
1772
|
+
const named = (e) => {
|
|
1773
|
+
const entry = `${e.name} ${e.notes ?? ""}`.toLowerCase();
|
|
1774
|
+
return words.some((w) => entry.includes(w));
|
|
1775
|
+
};
|
|
1776
|
+
const ordered = [...entries2.filter(named), ...entries2.filter((e) => !named(e))];
|
|
1777
|
+
const shown = [];
|
|
1778
|
+
for (const entry of ordered) {
|
|
1779
|
+
if (shown.length > 0 && estimateTokens(formatStack([...shown, entry])) > maxTokens) {
|
|
1780
|
+
break;
|
|
1781
|
+
}
|
|
1782
|
+
shown.push(entry);
|
|
1783
|
+
}
|
|
1784
|
+
return { text: formatStack(shown), hidden: entries2.length - shown.length };
|
|
1785
|
+
}
|
|
1708
1786
|
function formatRules(rules) {
|
|
1709
1787
|
const byCategory = /* @__PURE__ */ new Map();
|
|
1710
1788
|
for (const r of rules) {
|
|
@@ -1726,6 +1804,21 @@ function formatRules(rules) {
|
|
|
1726
1804
|
|
|
1727
1805
|
${text3.trim()}`;
|
|
1728
1806
|
}
|
|
1807
|
+
async function findDocs(api, taskDescription) {
|
|
1808
|
+
const query = taskDescription.trim().slice(0, MAX_QUERY_CHARS);
|
|
1809
|
+
if (query) {
|
|
1810
|
+
try {
|
|
1811
|
+
const found = await api.get("/v1/docs", {
|
|
1812
|
+
q: query,
|
|
1813
|
+
limit: String(SEARCH_RESULTS)
|
|
1814
|
+
});
|
|
1815
|
+
if (found.items.length > 0) return { ...found, searched: true };
|
|
1816
|
+
} catch {
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
const listed = await api.get("/v1/docs");
|
|
1820
|
+
return { ...listed, searched: false };
|
|
1821
|
+
}
|
|
1729
1822
|
async function fetchSpecBody(api, draftId) {
|
|
1730
1823
|
const spec = await api.get(
|
|
1731
1824
|
`/v1/specs/${encodeURIComponent(draftId)}`
|
|
@@ -1826,7 +1919,9 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
1826
1919
|
const [rawGuardrails, rawSpecs, docs, stack, rules, docRepos] = await Promise.all([
|
|
1827
1920
|
api.get("/v1/guardrails").catch(orEmpty([])),
|
|
1828
1921
|
api.get("/v1/specs").catch(orEmpty([])),
|
|
1829
|
-
api
|
|
1922
|
+
findDocs(api, taskDescription).catch(
|
|
1923
|
+
orEmpty({ items: [], searched: false })
|
|
1924
|
+
),
|
|
1830
1925
|
api.get("/v1/ai/settings/stack").catch(orEmpty([])),
|
|
1831
1926
|
api.get("/v1/standards/rules").catch(orEmpty([])),
|
|
1832
1927
|
// Capability states — FAIL-OPEN: an unreadable config (older backend, a
|
|
@@ -1839,6 +1934,8 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
1839
1934
|
const specs = disabled("requirements") && disabled("design") && disabled("specs") ? [] : rawSpecs;
|
|
1840
1935
|
const refs = /* @__PURE__ */ new Map();
|
|
1841
1936
|
const key = (type, id) => `${type}:${id}`;
|
|
1937
|
+
const docTitles = new Set(docs.items.map((d) => d.title));
|
|
1938
|
+
const isCopyOfDoc = (s) => isDecisionDraft(s.name) && docTitles.has(s.name.slice("ADR: ".length));
|
|
1842
1939
|
const scorable = [
|
|
1843
1940
|
...guardrails.map((g) => ({
|
|
1844
1941
|
type: "guardrail",
|
|
@@ -1849,12 +1946,12 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
1849
1946
|
severity: g.severity,
|
|
1850
1947
|
updatedAt: g.updatedAt
|
|
1851
1948
|
})),
|
|
1852
|
-
...specs.filter((s) => s.status !== "archived").map((s) => {
|
|
1949
|
+
...specs.filter((s) => s.status !== "archived" && !isCopyOfDoc(s)).map((s) => {
|
|
1853
1950
|
refs.set(key("spec", s.draftId), {
|
|
1854
1951
|
type: "spec",
|
|
1855
1952
|
id: s.draftId,
|
|
1856
1953
|
title: s.name,
|
|
1857
|
-
listLine: `- **${s.name}** \u2014 \`draft_id: ${s.draftId}\` (${s.status}, ${s
|
|
1954
|
+
listLine: `- **${s.name}** \u2014 \`draft_id: ${s.draftId}\` (${s.status}, ${specLabel(s)})`,
|
|
1858
1955
|
fetchHint: `get_specs(draft_id="${s.draftId}")`
|
|
1859
1956
|
});
|
|
1860
1957
|
return {
|
|
@@ -1865,7 +1962,7 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
1865
1962
|
updatedAt: s.updatedAt
|
|
1866
1963
|
};
|
|
1867
1964
|
}),
|
|
1868
|
-
...docs.items.map((d) => {
|
|
1965
|
+
...docs.items.map((d, position) => {
|
|
1869
1966
|
const section = selectSection(d.outline, taskDescription);
|
|
1870
1967
|
const pointer = section ? formatSectionPointer(d.repo_full_name, d.path, section) : void 0;
|
|
1871
1968
|
refs.set(key("doc", d.id), {
|
|
@@ -1884,7 +1981,9 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
1884
1981
|
title: d.title,
|
|
1885
1982
|
content: formatDocHeadline(d),
|
|
1886
1983
|
category: d.doc_type === "adr" ? "architecture" : void 0,
|
|
1887
|
-
updatedAt: d.synced_at
|
|
1984
|
+
updatedAt: d.synced_at,
|
|
1985
|
+
path: d.path,
|
|
1986
|
+
relevance: docs.searched ? 1 - position / (2 * docs.items.length) : void 0
|
|
1888
1987
|
};
|
|
1889
1988
|
})
|
|
1890
1989
|
];
|
|
@@ -1892,9 +1991,16 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
1892
1991
|
const sections = [];
|
|
1893
1992
|
let overhead = 0;
|
|
1894
1993
|
if (stack.length > 0) {
|
|
1994
|
+
const { text: text3, hidden } = selectStack(
|
|
1995
|
+
stack,
|
|
1996
|
+
[...extractKeywords(taskDescription), ...detectLanguages(filePaths)],
|
|
1997
|
+
Math.floor(budget * STACK_SHARE)
|
|
1998
|
+
);
|
|
1895
1999
|
const section = `## Tech Stack
|
|
1896
2000
|
|
|
1897
|
-
${
|
|
2001
|
+
${text3}` + (hidden > 0 ? `
|
|
2002
|
+
|
|
2003
|
+
_${hidden} more with \`get_tech_stack\`._` : "");
|
|
1898
2004
|
sections.push(section);
|
|
1899
2005
|
overhead += estimateTokens(section);
|
|
1900
2006
|
}
|
|
@@ -2013,9 +2119,10 @@ _Highest-ranked items \u2014 docs are scoped to their most relevant section. Eac
|
|
|
2013
2119
|
const tailLines = [];
|
|
2014
2120
|
for (const meta of tailCandidates) {
|
|
2015
2121
|
const cost = estimateTokens(meta.listLine);
|
|
2016
|
-
|
|
2122
|
+
const named = blocks.length + tailLines.length;
|
|
2123
|
+
if (cost > remaining && named >= MIN_REFERENCES) break;
|
|
2017
2124
|
tailLines.push(meta.listLine);
|
|
2018
|
-
remaining
|
|
2125
|
+
remaining = Math.max(0, remaining - cost);
|
|
2019
2126
|
}
|
|
2020
2127
|
if (tailLines.length > 0) {
|
|
2021
2128
|
sections.push(
|
|
@@ -2841,7 +2948,7 @@ ${fence}
|
|
|
2841
2948
|
${f.content}${f.content.endsWith("\n") ? "" : "\n"}${fence}`;
|
|
2842
2949
|
}).join("\n\n");
|
|
2843
2950
|
return text(
|
|
2844
|
-
`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
|
|
2951
|
+
`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`)}
|
|
2845
2952
|
|
|
2846
2953
|
### ${MANIFEST_FILE}
|
|
2847
2954
|
|
|
@@ -2998,9 +3105,9 @@ function localDate(now) {
|
|
|
2998
3105
|
return `${now.getFullYear()}-${p(now.getMonth() + 1)}-${p(now.getDate())}`;
|
|
2999
3106
|
}
|
|
3000
3107
|
function conflictCopyName(path, date, taken) {
|
|
3001
|
-
const
|
|
3108
|
+
const stem2 = path.replace(/\.md$/, "");
|
|
3002
3109
|
for (let n = 1; ; n++) {
|
|
3003
|
-
const name = `${
|
|
3110
|
+
const name = `${stem2}.local-${date}${n === 1 ? "" : `-${n}`}.md`;
|
|
3004
3111
|
if (!taken.has(name.toLowerCase())) return name;
|
|
3005
3112
|
}
|
|
3006
3113
|
}
|
|
@@ -3887,7 +3994,7 @@ function registerTeamSetupTools(server, apiClient, { localFiles }) {
|
|
|
3887
3994
|
}
|
|
3888
3995
|
|
|
3889
3996
|
// src/server.ts
|
|
3890
|
-
var SERVER_VERSION = true ? "0.1.
|
|
3997
|
+
var SERVER_VERSION = true ? "0.1.52" : "0.0.0-dev";
|
|
3891
3998
|
function createServer(apiClient, { localFiles = false, instructions, keyProblem } = {}) {
|
|
3892
3999
|
const server = new McpServer(
|
|
3893
4000
|
{
|
package/dist/index.js
CHANGED
|
@@ -447,6 +447,9 @@ function fenceFor(text3) {
|
|
|
447
447
|
const longest = Math.max(0, ...(text3.match(/`+/g) ?? []).map((r) => r.length));
|
|
448
448
|
return "`".repeat(Math.max(3, longest + 1));
|
|
449
449
|
}
|
|
450
|
+
function afterInstall(skillMd) {
|
|
451
|
+
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.`;
|
|
452
|
+
}
|
|
450
453
|
function describeInstall(name, r) {
|
|
451
454
|
const lines = [
|
|
452
455
|
`Installed ${name} version ${r.version} into ${r.dest} (${r.written.length} file(s)).`
|
|
@@ -456,7 +459,7 @@ function describeInstall(name, r) {
|
|
|
456
459
|
lines.push(`Kept, edited here and no longer in the skill: ${r.keptEdited.join(", ")}`);
|
|
457
460
|
}
|
|
458
461
|
if (r.keptUntracked.length) lines.push(`Kept, added here: ${r.keptUntracked.join(", ")}`);
|
|
459
|
-
lines.push("
|
|
462
|
+
lines.push(afterInstall(join(r.dest, "SKILL.md")));
|
|
460
463
|
return lines.join("\n");
|
|
461
464
|
}
|
|
462
465
|
|
|
@@ -867,6 +870,17 @@ ${text3}`
|
|
|
867
870
|
|
|
868
871
|
// src/tools/get-specs.ts
|
|
869
872
|
import { z as z2 } from "zod";
|
|
873
|
+
|
|
874
|
+
// src/lib/spec-label.ts
|
|
875
|
+
var DECISION_PREFIX = "ADR: ";
|
|
876
|
+
function isDecisionDraft(name) {
|
|
877
|
+
return name.startsWith(DECISION_PREFIX);
|
|
878
|
+
}
|
|
879
|
+
function specLabel(spec) {
|
|
880
|
+
return isDecisionDraft(spec.name) ? "decision record" : `${spec.requirementCount} requirements`;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// src/tools/get-specs.ts
|
|
870
884
|
function registerGetSpecs(server, apiClient2) {
|
|
871
885
|
server.registerTool(
|
|
872
886
|
"get_specs",
|
|
@@ -931,7 +945,7 @@ ${spec.taskContent}`);
|
|
|
931
945
|
};
|
|
932
946
|
}
|
|
933
947
|
const text3 = filtered.map(
|
|
934
|
-
(s) => `- **${s.name}** (${s.draftId}) \u2014 ${s.status}, ${s
|
|
948
|
+
(s) => `- **${s.name}** (${s.draftId}) \u2014 ${s.status}, ${specLabel(s)}` + (s.repoFullName ? `, repo: ${s.repoFullName}` : "")
|
|
935
949
|
).join("\n");
|
|
936
950
|
return {
|
|
937
951
|
content: [
|
|
@@ -949,6 +963,11 @@ ${text3}`
|
|
|
949
963
|
|
|
950
964
|
// src/tools/get-docs.ts
|
|
951
965
|
import { z as z3 } from "zod";
|
|
966
|
+
var PATH_FILTER_LIMIT = "1000";
|
|
967
|
+
function literalPrefix(pattern) {
|
|
968
|
+
const wildcard = pattern.indexOf("*");
|
|
969
|
+
return wildcard < 0 ? pattern : pattern.slice(0, wildcard);
|
|
970
|
+
}
|
|
952
971
|
function matchGlob(path, pattern) {
|
|
953
972
|
const regex = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\{\{GLOBSTAR\}\}/g, ".*");
|
|
954
973
|
return new RegExp(`^${regex}$`).test(path);
|
|
@@ -1009,11 +1028,17 @@ ${doc.content}`;
|
|
|
1009
1028
|
const params = {};
|
|
1010
1029
|
if (repo) params.repo = repo;
|
|
1011
1030
|
if (search) params.q = search;
|
|
1031
|
+
if (path) {
|
|
1032
|
+
params.limit = PATH_FILTER_LIMIT;
|
|
1033
|
+
const prefix = literalPrefix(path);
|
|
1034
|
+
if (prefix) params.pathPrefix = prefix;
|
|
1035
|
+
}
|
|
1012
1036
|
const response = await apiClient2.get(
|
|
1013
1037
|
"/v1/docs",
|
|
1014
1038
|
params
|
|
1015
1039
|
);
|
|
1016
1040
|
let items = response.items;
|
|
1041
|
+
const unread = path ? response.total - response.items.length : 0;
|
|
1017
1042
|
if (path) {
|
|
1018
1043
|
items = items.filter((item) => matchGlob(item.path, path));
|
|
1019
1044
|
}
|
|
@@ -1043,7 +1068,9 @@ ${doc.content}`;
|
|
|
1043
1068
|
|
|
1044
1069
|
${text3}
|
|
1045
1070
|
|
|
1046
|
-
|
|
1071
|
+
` + (unread > 0 ? `${unread} more were not read. Start the path with a folder to narrow it.
|
|
1072
|
+
|
|
1073
|
+
` : "") + "Use doc_id for full content, or doc_id + section to fetch one section."
|
|
1047
1074
|
}
|
|
1048
1075
|
]
|
|
1049
1076
|
};
|
|
@@ -1144,7 +1171,9 @@ ${deps}`);
|
|
|
1144
1171
|
}
|
|
1145
1172
|
if (ctx.adrs.length > 0) {
|
|
1146
1173
|
const adrs = ctx.adrs.map((a) => `- ${a.title}${a.repo ? ` (${a.repo})` : ""}`).join("\n");
|
|
1147
|
-
|
|
1174
|
+
const total = ctx.adrTotal ?? ctx.adrs.length;
|
|
1175
|
+
const count = total > ctx.adrs.length ? `${ctx.adrs.length} newest of ${total}` : `${ctx.adrs.length}`;
|
|
1176
|
+
sections.push(`## Architecture Decision Records (${count})
|
|
1148
1177
|
|
|
1149
1178
|
${adrs}`);
|
|
1150
1179
|
}
|
|
@@ -1516,6 +1545,9 @@ var SEVERITY_SCORES = {
|
|
|
1516
1545
|
medium: 0.5,
|
|
1517
1546
|
low: 0.25
|
|
1518
1547
|
};
|
|
1548
|
+
var MIN_TITLE_SHARE = 1 / 3;
|
|
1549
|
+
var RECORD_PATH = /(^|\/)changelog\//;
|
|
1550
|
+
var RECORD_FACTOR = 0.5;
|
|
1519
1551
|
var CHARS_PER_TOKEN = 4;
|
|
1520
1552
|
function estimateTokens(text3) {
|
|
1521
1553
|
return Math.ceil(text3.length / CHARS_PER_TOKEN);
|
|
@@ -1568,6 +1600,32 @@ function scoreSeverity(item) {
|
|
|
1568
1600
|
if (!item.severity) return 0.5;
|
|
1569
1601
|
return SEVERITY_SCORES[item.severity] ?? 0.5;
|
|
1570
1602
|
}
|
|
1603
|
+
function stem(word) {
|
|
1604
|
+
return word.length > 3 ? word.replace(/s$/, "") : word;
|
|
1605
|
+
}
|
|
1606
|
+
function stems(text3) {
|
|
1607
|
+
return new Set(
|
|
1608
|
+
text3.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean).map(stem)
|
|
1609
|
+
);
|
|
1610
|
+
}
|
|
1611
|
+
function titleShare(title, keywords) {
|
|
1612
|
+
const wanted = stems(keywords.join(" "));
|
|
1613
|
+
if (wanted.size === 0) return 0;
|
|
1614
|
+
const held = stems(title);
|
|
1615
|
+
let matches = 0;
|
|
1616
|
+
for (const word of wanted) {
|
|
1617
|
+
if (held.has(word)) matches++;
|
|
1618
|
+
}
|
|
1619
|
+
return matches / wanted.size;
|
|
1620
|
+
}
|
|
1621
|
+
function scoreReference(item, keywords) {
|
|
1622
|
+
const share = titleShare(item.title, keywords);
|
|
1623
|
+
const score = Math.max(
|
|
1624
|
+
item.relevance ?? 0,
|
|
1625
|
+
share >= MIN_TITLE_SHARE ? share : 0
|
|
1626
|
+
);
|
|
1627
|
+
return RECORD_PATH.test(item.path ?? "") ? score * RECORD_FACTOR : score;
|
|
1628
|
+
}
|
|
1571
1629
|
function extractKeywords(text3) {
|
|
1572
1630
|
const stopWords = /* @__PURE__ */ new Set([
|
|
1573
1631
|
"the",
|
|
@@ -1664,7 +1722,8 @@ function scoreItems(items, taskDescription, filePaths) {
|
|
|
1664
1722
|
const keywordScore = scoreKeywordMatch(item, keywords);
|
|
1665
1723
|
const recencyScore = scoreRecency(item);
|
|
1666
1724
|
const severityScore = scoreSeverity(item);
|
|
1667
|
-
const
|
|
1725
|
+
const isReference = item.type === "spec" || item.type === "doc";
|
|
1726
|
+
const score = isReference && keywords.length > 0 ? scoreReference(item, keywords) : pathScore * WEIGHT_PATH + keywordScore * WEIGHT_KEYWORD + recencyScore * WEIGHT_RECENCY + severityScore * WEIGHT_SEVERITY;
|
|
1668
1727
|
return {
|
|
1669
1728
|
type: item.type,
|
|
1670
1729
|
id: item.id,
|
|
@@ -1673,7 +1732,7 @@ function scoreItems(items, taskDescription, filePaths) {
|
|
|
1673
1732
|
score,
|
|
1674
1733
|
tokenEstimate: estimateTokens(item.content)
|
|
1675
1734
|
};
|
|
1676
|
-
}).sort((a, b) => b.score - a.score);
|
|
1735
|
+
}).filter((item) => item.score > 0).sort((a, b) => b.score - a.score);
|
|
1677
1736
|
}
|
|
1678
1737
|
function selectWithinBudget(items, maxTokens) {
|
|
1679
1738
|
const selected = [];
|
|
@@ -1695,6 +1754,10 @@ var MAX_HYDRATED = 8;
|
|
|
1695
1754
|
var MAX_TAIL = 10;
|
|
1696
1755
|
var TAIL_SHARE = 0.15;
|
|
1697
1756
|
var TRUNCATION_NOTICE_TOKENS = 30;
|
|
1757
|
+
var STACK_SHARE = 0.1;
|
|
1758
|
+
var MIN_REFERENCES = 3;
|
|
1759
|
+
var SEARCH_RESULTS = 30;
|
|
1760
|
+
var MAX_QUERY_CHARS = 300;
|
|
1698
1761
|
function formatGuardrail(g) {
|
|
1699
1762
|
let text3 = `## ${g.title} [${g.severity}]
|
|
1700
1763
|
|
|
@@ -1713,7 +1776,7 @@ ${g.fix}
|
|
|
1713
1776
|
}
|
|
1714
1777
|
function formatSpecHeadline(s) {
|
|
1715
1778
|
return `### ${s.name}
|
|
1716
|
-
\`draft_id: ${s.draftId}\` \xB7 ${s.status} \xB7 ${s
|
|
1779
|
+
\`draft_id: ${s.draftId}\` \xB7 ${s.status} \xB7 ${specLabel(s)}`;
|
|
1717
1780
|
}
|
|
1718
1781
|
function formatDocHeadline(d) {
|
|
1719
1782
|
return `### ${d.title}
|
|
@@ -1724,6 +1787,21 @@ function formatStack(entries2) {
|
|
|
1724
1787
|
(e) => `- ${e.name}${e.version ? ` v${e.version}` : ""}${e.notes ? ` \u2014 ${e.notes}` : ""}`
|
|
1725
1788
|
).join("\n");
|
|
1726
1789
|
}
|
|
1790
|
+
function selectStack(entries2, words, maxTokens) {
|
|
1791
|
+
const named = (e) => {
|
|
1792
|
+
const entry = `${e.name} ${e.notes ?? ""}`.toLowerCase();
|
|
1793
|
+
return words.some((w) => entry.includes(w));
|
|
1794
|
+
};
|
|
1795
|
+
const ordered = [...entries2.filter(named), ...entries2.filter((e) => !named(e))];
|
|
1796
|
+
const shown = [];
|
|
1797
|
+
for (const entry of ordered) {
|
|
1798
|
+
if (shown.length > 0 && estimateTokens(formatStack([...shown, entry])) > maxTokens) {
|
|
1799
|
+
break;
|
|
1800
|
+
}
|
|
1801
|
+
shown.push(entry);
|
|
1802
|
+
}
|
|
1803
|
+
return { text: formatStack(shown), hidden: entries2.length - shown.length };
|
|
1804
|
+
}
|
|
1727
1805
|
function formatRules(rules) {
|
|
1728
1806
|
const byCategory = /* @__PURE__ */ new Map();
|
|
1729
1807
|
for (const r of rules) {
|
|
@@ -1745,6 +1823,21 @@ function formatRules(rules) {
|
|
|
1745
1823
|
|
|
1746
1824
|
${text3.trim()}`;
|
|
1747
1825
|
}
|
|
1826
|
+
async function findDocs(api, taskDescription) {
|
|
1827
|
+
const query = taskDescription.trim().slice(0, MAX_QUERY_CHARS);
|
|
1828
|
+
if (query) {
|
|
1829
|
+
try {
|
|
1830
|
+
const found = await api.get("/v1/docs", {
|
|
1831
|
+
q: query,
|
|
1832
|
+
limit: String(SEARCH_RESULTS)
|
|
1833
|
+
});
|
|
1834
|
+
if (found.items.length > 0) return { ...found, searched: true };
|
|
1835
|
+
} catch {
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
const listed = await api.get("/v1/docs");
|
|
1839
|
+
return { ...listed, searched: false };
|
|
1840
|
+
}
|
|
1748
1841
|
async function fetchSpecBody(api, draftId) {
|
|
1749
1842
|
const spec = await api.get(
|
|
1750
1843
|
`/v1/specs/${encodeURIComponent(draftId)}`
|
|
@@ -1845,7 +1938,9 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
1845
1938
|
const [rawGuardrails, rawSpecs, docs, stack, rules, docRepos] = await Promise.all([
|
|
1846
1939
|
api.get("/v1/guardrails").catch(orEmpty([])),
|
|
1847
1940
|
api.get("/v1/specs").catch(orEmpty([])),
|
|
1848
|
-
api
|
|
1941
|
+
findDocs(api, taskDescription).catch(
|
|
1942
|
+
orEmpty({ items: [], searched: false })
|
|
1943
|
+
),
|
|
1849
1944
|
api.get("/v1/ai/settings/stack").catch(orEmpty([])),
|
|
1850
1945
|
api.get("/v1/standards/rules").catch(orEmpty([])),
|
|
1851
1946
|
// Capability states — FAIL-OPEN: an unreadable config (older backend, a
|
|
@@ -1858,6 +1953,8 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
1858
1953
|
const specs = disabled("requirements") && disabled("design") && disabled("specs") ? [] : rawSpecs;
|
|
1859
1954
|
const refs = /* @__PURE__ */ new Map();
|
|
1860
1955
|
const key = (type, id) => `${type}:${id}`;
|
|
1956
|
+
const docTitles = new Set(docs.items.map((d) => d.title));
|
|
1957
|
+
const isCopyOfDoc = (s) => isDecisionDraft(s.name) && docTitles.has(s.name.slice("ADR: ".length));
|
|
1861
1958
|
const scorable = [
|
|
1862
1959
|
...guardrails.map((g) => ({
|
|
1863
1960
|
type: "guardrail",
|
|
@@ -1868,12 +1965,12 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
1868
1965
|
severity: g.severity,
|
|
1869
1966
|
updatedAt: g.updatedAt
|
|
1870
1967
|
})),
|
|
1871
|
-
...specs.filter((s) => s.status !== "archived").map((s) => {
|
|
1968
|
+
...specs.filter((s) => s.status !== "archived" && !isCopyOfDoc(s)).map((s) => {
|
|
1872
1969
|
refs.set(key("spec", s.draftId), {
|
|
1873
1970
|
type: "spec",
|
|
1874
1971
|
id: s.draftId,
|
|
1875
1972
|
title: s.name,
|
|
1876
|
-
listLine: `- **${s.name}** \u2014 \`draft_id: ${s.draftId}\` (${s.status}, ${s
|
|
1973
|
+
listLine: `- **${s.name}** \u2014 \`draft_id: ${s.draftId}\` (${s.status}, ${specLabel(s)})`,
|
|
1877
1974
|
fetchHint: `get_specs(draft_id="${s.draftId}")`
|
|
1878
1975
|
});
|
|
1879
1976
|
return {
|
|
@@ -1884,7 +1981,7 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
1884
1981
|
updatedAt: s.updatedAt
|
|
1885
1982
|
};
|
|
1886
1983
|
}),
|
|
1887
|
-
...docs.items.map((d) => {
|
|
1984
|
+
...docs.items.map((d, position) => {
|
|
1888
1985
|
const section = selectSection(d.outline, taskDescription);
|
|
1889
1986
|
const pointer = section ? formatSectionPointer(d.repo_full_name, d.path, section) : void 0;
|
|
1890
1987
|
refs.set(key("doc", d.id), {
|
|
@@ -1903,7 +2000,9 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
1903
2000
|
title: d.title,
|
|
1904
2001
|
content: formatDocHeadline(d),
|
|
1905
2002
|
category: d.doc_type === "adr" ? "architecture" : void 0,
|
|
1906
|
-
updatedAt: d.synced_at
|
|
2003
|
+
updatedAt: d.synced_at,
|
|
2004
|
+
path: d.path,
|
|
2005
|
+
relevance: docs.searched ? 1 - position / (2 * docs.items.length) : void 0
|
|
1907
2006
|
};
|
|
1908
2007
|
})
|
|
1909
2008
|
];
|
|
@@ -1911,9 +2010,16 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
|
|
|
1911
2010
|
const sections = [];
|
|
1912
2011
|
let overhead = 0;
|
|
1913
2012
|
if (stack.length > 0) {
|
|
2013
|
+
const { text: text3, hidden } = selectStack(
|
|
2014
|
+
stack,
|
|
2015
|
+
[...extractKeywords(taskDescription), ...detectLanguages(filePaths)],
|
|
2016
|
+
Math.floor(budget * STACK_SHARE)
|
|
2017
|
+
);
|
|
1914
2018
|
const section = `## Tech Stack
|
|
1915
2019
|
|
|
1916
|
-
${
|
|
2020
|
+
${text3}` + (hidden > 0 ? `
|
|
2021
|
+
|
|
2022
|
+
_${hidden} more with \`get_tech_stack\`._` : "");
|
|
1917
2023
|
sections.push(section);
|
|
1918
2024
|
overhead += estimateTokens(section);
|
|
1919
2025
|
}
|
|
@@ -2032,9 +2138,10 @@ _Highest-ranked items \u2014 docs are scoped to their most relevant section. Eac
|
|
|
2032
2138
|
const tailLines = [];
|
|
2033
2139
|
for (const meta of tailCandidates) {
|
|
2034
2140
|
const cost = estimateTokens(meta.listLine);
|
|
2035
|
-
|
|
2141
|
+
const named = blocks.length + tailLines.length;
|
|
2142
|
+
if (cost > remaining && named >= MIN_REFERENCES) break;
|
|
2036
2143
|
tailLines.push(meta.listLine);
|
|
2037
|
-
remaining
|
|
2144
|
+
remaining = Math.max(0, remaining - cost);
|
|
2038
2145
|
}
|
|
2039
2146
|
if (tailLines.length > 0) {
|
|
2040
2147
|
sections.push(
|
|
@@ -2860,7 +2967,7 @@ ${fence}
|
|
|
2860
2967
|
${f.content}${f.content.endsWith("\n") ? "" : "\n"}${fence}`;
|
|
2861
2968
|
}).join("\n\n");
|
|
2862
2969
|
return text(
|
|
2863
|
-
`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
|
|
2970
|
+
`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`)}
|
|
2864
2971
|
|
|
2865
2972
|
### ${MANIFEST_FILE}
|
|
2866
2973
|
|
|
@@ -3017,9 +3124,9 @@ function localDate(now) {
|
|
|
3017
3124
|
return `${now.getFullYear()}-${p(now.getMonth() + 1)}-${p(now.getDate())}`;
|
|
3018
3125
|
}
|
|
3019
3126
|
function conflictCopyName(path, date, taken) {
|
|
3020
|
-
const
|
|
3127
|
+
const stem2 = path.replace(/\.md$/, "");
|
|
3021
3128
|
for (let n = 1; ; n++) {
|
|
3022
|
-
const name = `${
|
|
3129
|
+
const name = `${stem2}.local-${date}${n === 1 ? "" : `-${n}`}.md`;
|
|
3023
3130
|
if (!taken.has(name.toLowerCase())) return name;
|
|
3024
3131
|
}
|
|
3025
3132
|
}
|
|
@@ -3906,7 +4013,7 @@ function registerTeamSetupTools(server, apiClient2, { localFiles }) {
|
|
|
3906
4013
|
}
|
|
3907
4014
|
|
|
3908
4015
|
// src/server.ts
|
|
3909
|
-
var SERVER_VERSION = true ? "0.1.
|
|
4016
|
+
var SERVER_VERSION = true ? "0.1.52" : "0.0.0-dev";
|
|
3910
4017
|
function createServer(apiClient2, { localFiles = false, instructions, keyProblem: keyProblem2 } = {}) {
|
|
3911
4018
|
const server = new McpServer(
|
|
3912
4019
|
{
|
package/dist/install-skill.js
CHANGED
|
@@ -385,6 +385,9 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
|
|
|
385
385
|
keptEdited
|
|
386
386
|
};
|
|
387
387
|
}
|
|
388
|
+
function afterInstall(skillMd) {
|
|
389
|
+
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.`;
|
|
390
|
+
}
|
|
388
391
|
function describeInstall(name, r) {
|
|
389
392
|
const lines = [
|
|
390
393
|
`Installed ${name} version ${r.version} into ${r.dest} (${r.written.length} file(s)).`
|
|
@@ -394,7 +397,7 @@ function describeInstall(name, r) {
|
|
|
394
397
|
lines.push(`Kept, edited here and no longer in the skill: ${r.keptEdited.join(", ")}`);
|
|
395
398
|
}
|
|
396
399
|
if (r.keptUntracked.length) lines.push(`Kept, added here: ${r.keptUntracked.join(", ")}`);
|
|
397
|
-
lines.push("
|
|
400
|
+
lines.push(afterInstall(join(r.dest, "SKILL.md")));
|
|
398
401
|
return lines.join("\n");
|
|
399
402
|
}
|
|
400
403
|
|