@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/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("Start a new Claude Code session to load it.");
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.requirementCount} requirements` + (s.repoFullName ? `, repo: ${s.repoFullName}` : "")
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
- Use doc_id for full content, or doc_id + section to fetch one section.`
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
- sections.push(`## Architecture Decision Records (${ctx.adrs.length})
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 score = pathScore * WEIGHT_PATH + keywordScore * WEIGHT_KEYWORD + recencyScore * WEIGHT_RECENCY + severityScore * WEIGHT_SEVERITY;
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.requirementCount} requirements`;
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.get("/v1/docs").catch(orEmpty({ items: [] })),
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.requirementCount} requirements)`,
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
- ${formatStack(stack)}`;
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
- if (cost > remaining) break;
2122
+ const named = blocks.length + tailLines.length;
2123
+ if (cost > remaining && named >= MIN_REFERENCES) break;
2017
2124
  tailLines.push(meta.listLine);
2018
- remaining -= cost;
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, and tell the user to start a new Claude Code session.
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 stem = path.replace(/\.md$/, "");
3108
+ const stem2 = path.replace(/\.md$/, "");
3002
3109
  for (let n = 1; ; n++) {
3003
- const name = `${stem}.local-${date}${n === 1 ? "" : `-${n}`}.md`;
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.50" : "0.0.0-dev";
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("Start a new Claude Code session to load it.");
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.requirementCount} requirements` + (s.repoFullName ? `, repo: ${s.repoFullName}` : "")
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
- Use doc_id for full content, or doc_id + section to fetch one section.`
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
- sections.push(`## Architecture Decision Records (${ctx.adrs.length})
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 score = pathScore * WEIGHT_PATH + keywordScore * WEIGHT_KEYWORD + recencyScore * WEIGHT_RECENCY + severityScore * WEIGHT_SEVERITY;
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.requirementCount} requirements`;
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.get("/v1/docs").catch(orEmpty({ items: [] })),
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.requirementCount} requirements)`,
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
- ${formatStack(stack)}`;
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
- if (cost > remaining) break;
2141
+ const named = blocks.length + tailLines.length;
2142
+ if (cost > remaining && named >= MIN_REFERENCES) break;
2036
2143
  tailLines.push(meta.listLine);
2037
- remaining -= cost;
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, and tell the user to start a new Claude Code session.
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 stem = path.replace(/\.md$/, "");
3127
+ const stem2 = path.replace(/\.md$/, "");
3021
3128
  for (let n = 1; ; n++) {
3022
- const name = `${stem}.local-${date}${n === 1 ? "" : `-${n}`}.md`;
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.50" : "0.0.0-dev";
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
  {
@@ -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("Start a new Claude Code session to load it.");
400
+ lines.push(afterInstall(join(r.dest, "SKILL.md")));
398
401
  return lines.join("\n");
399
402
  }
400
403