@systemdox/mcp-server 0.1.51 → 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 CHANGED
@@ -592,9 +592,9 @@ function localDate(now) {
592
592
  return `${now.getFullYear()}-${p(now.getMonth() + 1)}-${p(now.getDate())}`;
593
593
  }
594
594
  function conflictCopyName(path, date, taken) {
595
- const stem = path.replace(/\.md$/, "");
595
+ const stem2 = path.replace(/\.md$/, "");
596
596
  for (let n = 1; ; n++) {
597
- const name = `${stem}.local-${date}${n === 1 ? "" : `-${n}`}.md`;
597
+ const name = `${stem2}.local-${date}${n === 1 ? "" : `-${n}`}.md`;
598
598
  if (!taken.has(name.toLowerCase())) return name;
599
599
  }
600
600
  }
@@ -2857,6 +2857,21 @@ var init_get_guardrails = __esm({
2857
2857
  }
2858
2858
  });
2859
2859
 
2860
+ // src/lib/spec-label.ts
2861
+ function isDecisionDraft(name) {
2862
+ return name.startsWith(DECISION_PREFIX);
2863
+ }
2864
+ function specLabel(spec) {
2865
+ return isDecisionDraft(spec.name) ? "decision record" : `${spec.requirementCount} requirements`;
2866
+ }
2867
+ var DECISION_PREFIX;
2868
+ var init_spec_label = __esm({
2869
+ "src/lib/spec-label.ts"() {
2870
+ "use strict";
2871
+ DECISION_PREFIX = "ADR: ";
2872
+ }
2873
+ });
2874
+
2860
2875
  // src/tools/get-specs.ts
2861
2876
  import { z as z2 } from "zod";
2862
2877
  function registerGetSpecs(server, apiClient2) {
@@ -2923,7 +2938,7 @@ ${spec.taskContent}`);
2923
2938
  };
2924
2939
  }
2925
2940
  const text3 = filtered.map(
2926
- (s) => `- **${s.name}** (${s.draftId}) \u2014 ${s.status}, ${s.requirementCount} requirements` + (s.repoFullName ? `, repo: ${s.repoFullName}` : "")
2941
+ (s) => `- **${s.name}** (${s.draftId}) \u2014 ${s.status}, ${specLabel(s)}` + (s.repoFullName ? `, repo: ${s.repoFullName}` : "")
2927
2942
  ).join("\n");
2928
2943
  return {
2929
2944
  content: [
@@ -2941,11 +2956,16 @@ ${text3}`
2941
2956
  var init_get_specs = __esm({
2942
2957
  "src/tools/get-specs.ts"() {
2943
2958
  "use strict";
2959
+ init_spec_label();
2944
2960
  }
2945
2961
  });
2946
2962
 
2947
2963
  // src/tools/get-docs.ts
2948
2964
  import { z as z3 } from "zod";
2965
+ function literalPrefix(pattern) {
2966
+ const wildcard = pattern.indexOf("*");
2967
+ return wildcard < 0 ? pattern : pattern.slice(0, wildcard);
2968
+ }
2949
2969
  function matchGlob(path, pattern) {
2950
2970
  const regex = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\{\{GLOBSTAR\}\}/g, ".*");
2951
2971
  return new RegExp(`^${regex}$`).test(path);
@@ -3006,11 +3026,17 @@ ${doc.content}`;
3006
3026
  const params = {};
3007
3027
  if (repo) params.repo = repo;
3008
3028
  if (search) params.q = search;
3029
+ if (path) {
3030
+ params.limit = PATH_FILTER_LIMIT;
3031
+ const prefix = literalPrefix(path);
3032
+ if (prefix) params.pathPrefix = prefix;
3033
+ }
3009
3034
  const response = await apiClient2.get(
3010
3035
  "/v1/docs",
3011
3036
  params
3012
3037
  );
3013
3038
  let items = response.items;
3039
+ const unread = path ? response.total - response.items.length : 0;
3014
3040
  if (path) {
3015
3041
  items = items.filter((item2) => matchGlob(item2.path, path));
3016
3042
  }
@@ -3040,16 +3066,20 @@ ${doc.content}`;
3040
3066
 
3041
3067
  ${text3}
3042
3068
 
3043
- Use doc_id for full content, or doc_id + section to fetch one section.`
3069
+ ` + (unread > 0 ? `${unread} more were not read. Start the path with a folder to narrow it.
3070
+
3071
+ ` : "") + "Use doc_id for full content, or doc_id + section to fetch one section."
3044
3072
  }
3045
3073
  ]
3046
3074
  };
3047
3075
  }
3048
3076
  );
3049
3077
  }
3078
+ var PATH_FILTER_LIMIT;
3050
3079
  var init_get_docs = __esm({
3051
3080
  "src/tools/get-docs.ts"() {
3052
3081
  "use strict";
3082
+ PATH_FILTER_LIMIT = "1000";
3053
3083
  }
3054
3084
  });
3055
3085
 
@@ -3152,7 +3182,9 @@ ${deps}`);
3152
3182
  }
3153
3183
  if (ctx.adrs.length > 0) {
3154
3184
  const adrs = ctx.adrs.map((a) => `- ${a.title}${a.repo ? ` (${a.repo})` : ""}`).join("\n");
3155
- sections.push(`## Architecture Decision Records (${ctx.adrs.length})
3185
+ const total = ctx.adrTotal ?? ctx.adrs.length;
3186
+ const count = total > ctx.adrs.length ? `${ctx.adrs.length} newest of ${total}` : `${ctx.adrs.length}`;
3187
+ sections.push(`## Architecture Decision Records (${count})
3156
3188
 
3157
3189
  ${adrs}`);
3158
3190
  }
@@ -3581,6 +3613,32 @@ function scoreSeverity(item2) {
3581
3613
  if (!item2.severity) return 0.5;
3582
3614
  return SEVERITY_SCORES[item2.severity] ?? 0.5;
3583
3615
  }
3616
+ function stem(word) {
3617
+ return word.length > 3 ? word.replace(/s$/, "") : word;
3618
+ }
3619
+ function stems(text3) {
3620
+ return new Set(
3621
+ text3.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean).map(stem)
3622
+ );
3623
+ }
3624
+ function titleShare(title, keywords) {
3625
+ const wanted = stems(keywords.join(" "));
3626
+ if (wanted.size === 0) return 0;
3627
+ const held = stems(title);
3628
+ let matches = 0;
3629
+ for (const word of wanted) {
3630
+ if (held.has(word)) matches++;
3631
+ }
3632
+ return matches / wanted.size;
3633
+ }
3634
+ function scoreReference(item2, keywords) {
3635
+ const share = titleShare(item2.title, keywords);
3636
+ const score = Math.max(
3637
+ item2.relevance ?? 0,
3638
+ share >= MIN_TITLE_SHARE ? share : 0
3639
+ );
3640
+ return RECORD_PATH.test(item2.path ?? "") ? score * RECORD_FACTOR : score;
3641
+ }
3584
3642
  function extractKeywords(text3) {
3585
3643
  const stopWords = /* @__PURE__ */ new Set([
3586
3644
  "the",
@@ -3677,7 +3735,8 @@ function scoreItems(items, taskDescription, filePaths) {
3677
3735
  const keywordScore = scoreKeywordMatch(item2, keywords);
3678
3736
  const recencyScore = scoreRecency(item2);
3679
3737
  const severityScore = scoreSeverity(item2);
3680
- const score = pathScore * WEIGHT_PATH + keywordScore * WEIGHT_KEYWORD + recencyScore * WEIGHT_RECENCY + severityScore * WEIGHT_SEVERITY;
3738
+ const isReference2 = item2.type === "spec" || item2.type === "doc";
3739
+ const score = isReference2 && keywords.length > 0 ? scoreReference(item2, keywords) : pathScore * WEIGHT_PATH + keywordScore * WEIGHT_KEYWORD + recencyScore * WEIGHT_RECENCY + severityScore * WEIGHT_SEVERITY;
3681
3740
  return {
3682
3741
  type: item2.type,
3683
3742
  id: item2.id,
@@ -3686,7 +3745,7 @@ function scoreItems(items, taskDescription, filePaths) {
3686
3745
  score,
3687
3746
  tokenEstimate: estimateTokens(item2.content)
3688
3747
  };
3689
- }).sort((a, b) => b.score - a.score);
3748
+ }).filter((item2) => item2.score > 0).sort((a, b) => b.score - a.score);
3690
3749
  }
3691
3750
  function selectWithinBudget(items, maxTokens) {
3692
3751
  const selected = [];
@@ -3698,7 +3757,7 @@ function selectWithinBudget(items, maxTokens) {
3698
3757
  }
3699
3758
  return { selected, tokensUsed, totalItems: items.length };
3700
3759
  }
3701
- var WEIGHT_PATH, WEIGHT_KEYWORD, WEIGHT_RECENCY, WEIGHT_SEVERITY, SEVERITY_SCORES, CHARS_PER_TOKEN;
3760
+ var WEIGHT_PATH, WEIGHT_KEYWORD, WEIGHT_RECENCY, WEIGHT_SEVERITY, SEVERITY_SCORES, MIN_TITLE_SHARE, RECORD_PATH, RECORD_FACTOR, CHARS_PER_TOKEN;
3702
3761
  var init_scoring = __esm({
3703
3762
  "src/lib/scoring.ts"() {
3704
3763
  "use strict";
@@ -3713,6 +3772,9 @@ var init_scoring = __esm({
3713
3772
  medium: 0.5,
3714
3773
  low: 0.25
3715
3774
  };
3775
+ MIN_TITLE_SHARE = 1 / 3;
3776
+ RECORD_PATH = /(^|\/)changelog\//;
3777
+ RECORD_FACTOR = 0.5;
3716
3778
  CHARS_PER_TOKEN = 4;
3717
3779
  }
3718
3780
  });
@@ -3736,7 +3798,7 @@ ${g.fix}
3736
3798
  }
3737
3799
  function formatSpecHeadline(s) {
3738
3800
  return `### ${s.name}
3739
- \`draft_id: ${s.draftId}\` \xB7 ${s.status} \xB7 ${s.requirementCount} requirements`;
3801
+ \`draft_id: ${s.draftId}\` \xB7 ${s.status} \xB7 ${specLabel(s)}`;
3740
3802
  }
3741
3803
  function formatDocHeadline(d) {
3742
3804
  return `### ${d.title}
@@ -3747,6 +3809,21 @@ function formatStack(entries2) {
3747
3809
  (e) => `- ${e.name}${e.version ? ` v${e.version}` : ""}${e.notes ? ` \u2014 ${e.notes}` : ""}`
3748
3810
  ).join("\n");
3749
3811
  }
3812
+ function selectStack(entries2, words, maxTokens) {
3813
+ const named = (e) => {
3814
+ const entry = `${e.name} ${e.notes ?? ""}`.toLowerCase();
3815
+ return words.some((w) => entry.includes(w));
3816
+ };
3817
+ const ordered = [...entries2.filter(named), ...entries2.filter((e) => !named(e))];
3818
+ const shown = [];
3819
+ for (const entry of ordered) {
3820
+ if (shown.length > 0 && estimateTokens(formatStack([...shown, entry])) > maxTokens) {
3821
+ break;
3822
+ }
3823
+ shown.push(entry);
3824
+ }
3825
+ return { text: formatStack(shown), hidden: entries2.length - shown.length };
3826
+ }
3750
3827
  function formatRules(rules) {
3751
3828
  const byCategory = /* @__PURE__ */ new Map();
3752
3829
  for (const r of rules) {
@@ -3768,6 +3845,21 @@ function formatRules(rules) {
3768
3845
 
3769
3846
  ${text3.trim()}`;
3770
3847
  }
3848
+ async function findDocs(api, taskDescription) {
3849
+ const query = taskDescription.trim().slice(0, MAX_QUERY_CHARS);
3850
+ if (query) {
3851
+ try {
3852
+ const found = await api.get("/v1/docs", {
3853
+ q: query,
3854
+ limit: String(SEARCH_RESULTS)
3855
+ });
3856
+ if (found.items.length > 0) return { ...found, searched: true };
3857
+ } catch {
3858
+ }
3859
+ }
3860
+ const listed = await api.get("/v1/docs");
3861
+ return { ...listed, searched: false };
3862
+ }
3771
3863
  async function fetchSpecBody(api, draftId) {
3772
3864
  const spec = await api.get(
3773
3865
  `/v1/specs/${encodeURIComponent(draftId)}`
@@ -3846,7 +3938,9 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
3846
3938
  const [rawGuardrails, rawSpecs, docs, stack, rules, docRepos] = await Promise.all([
3847
3939
  api.get("/v1/guardrails").catch(orEmpty([])),
3848
3940
  api.get("/v1/specs").catch(orEmpty([])),
3849
- api.get("/v1/docs").catch(orEmpty({ items: [] })),
3941
+ findDocs(api, taskDescription).catch(
3942
+ orEmpty({ items: [], searched: false })
3943
+ ),
3850
3944
  api.get("/v1/ai/settings/stack").catch(orEmpty([])),
3851
3945
  api.get("/v1/standards/rules").catch(orEmpty([])),
3852
3946
  // Capability states — FAIL-OPEN: an unreadable config (older backend, a
@@ -3859,6 +3953,8 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
3859
3953
  const specs = disabled("requirements") && disabled("design") && disabled("specs") ? [] : rawSpecs;
3860
3954
  const refs = /* @__PURE__ */ new Map();
3861
3955
  const key = (type, id) => `${type}:${id}`;
3956
+ const docTitles = new Set(docs.items.map((d) => d.title));
3957
+ const isCopyOfDoc = (s) => isDecisionDraft(s.name) && docTitles.has(s.name.slice("ADR: ".length));
3862
3958
  const scorable = [
3863
3959
  ...guardrails.map((g) => ({
3864
3960
  type: "guardrail",
@@ -3869,12 +3965,12 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
3869
3965
  severity: g.severity,
3870
3966
  updatedAt: g.updatedAt
3871
3967
  })),
3872
- ...specs.filter((s) => s.status !== "archived").map((s) => {
3968
+ ...specs.filter((s) => s.status !== "archived" && !isCopyOfDoc(s)).map((s) => {
3873
3969
  refs.set(key("spec", s.draftId), {
3874
3970
  type: "spec",
3875
3971
  id: s.draftId,
3876
3972
  title: s.name,
3877
- listLine: `- **${s.name}** \u2014 \`draft_id: ${s.draftId}\` (${s.status}, ${s.requirementCount} requirements)`,
3973
+ listLine: `- **${s.name}** \u2014 \`draft_id: ${s.draftId}\` (${s.status}, ${specLabel(s)})`,
3878
3974
  fetchHint: `get_specs(draft_id="${s.draftId}")`
3879
3975
  });
3880
3976
  return {
@@ -3885,7 +3981,7 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
3885
3981
  updatedAt: s.updatedAt
3886
3982
  };
3887
3983
  }),
3888
- ...docs.items.map((d) => {
3984
+ ...docs.items.map((d, position) => {
3889
3985
  const section = selectSection(d.outline, taskDescription);
3890
3986
  const pointer = section ? formatSectionPointer(d.repo_full_name, d.path, section) : void 0;
3891
3987
  refs.set(key("doc", d.id), {
@@ -3904,7 +4000,9 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
3904
4000
  title: d.title,
3905
4001
  content: formatDocHeadline(d),
3906
4002
  category: d.doc_type === "adr" ? "architecture" : void 0,
3907
- updatedAt: d.synced_at
4003
+ updatedAt: d.synced_at,
4004
+ path: d.path,
4005
+ relevance: docs.searched ? 1 - position / (2 * docs.items.length) : void 0
3908
4006
  };
3909
4007
  })
3910
4008
  ];
@@ -3912,9 +4010,16 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
3912
4010
  const sections = [];
3913
4011
  let overhead = 0;
3914
4012
  if (stack.length > 0) {
4013
+ const { text: text3, hidden } = selectStack(
4014
+ stack,
4015
+ [...extractKeywords(taskDescription), ...detectLanguages(filePaths)],
4016
+ Math.floor(budget * STACK_SHARE)
4017
+ );
3915
4018
  const section = `## Tech Stack
3916
4019
 
3917
- ${formatStack(stack)}`;
4020
+ ${text3}` + (hidden > 0 ? `
4021
+
4022
+ _${hidden} more with \`get_tech_stack\`._` : "");
3918
4023
  sections.push(section);
3919
4024
  overhead += estimateTokens(section);
3920
4025
  }
@@ -4033,9 +4138,10 @@ _Highest-ranked items \u2014 docs are scoped to their most relevant section. Eac
4033
4138
  const tailLines = [];
4034
4139
  for (const meta of tailCandidates) {
4035
4140
  const cost = estimateTokens(meta.listLine);
4036
- if (cost > remaining) break;
4141
+ const named = blocks.length + tailLines.length;
4142
+ if (cost > remaining && named >= MIN_REFERENCES) break;
4037
4143
  tailLines.push(meta.listLine);
4038
- remaining -= cost;
4144
+ remaining = Math.max(0, remaining - cost);
4039
4145
  }
4040
4146
  if (tailLines.length > 0) {
4041
4147
  sections.push(
@@ -4069,11 +4175,13 @@ _Ranked lower \u2014 fetch with \`get_specs\` / \`get_docs\` if you need them._
4069
4175
  _Context: ${itemsWithBody} full text, ${itemsListed} listed, ${stats.itemsOmitted} omitted \xB7 ~${tokensUsed} of ${budget} tokens` + (bodyFailures > 0 ? ` \xB7 ${bodyFailures} body fetch(es) failed` : "") + (failures.length > 0 ? ` \xB7 ${failures.length} of 5 reads failed` : "") + `_`;
4070
4176
  return { text: body + footer, stats };
4071
4177
  }
4072
- var DEFAULT_BUDGET, RESERVE_TOKENS, GUARDRAIL_SHARE, MIN_BODY_TOKENS, MAX_HYDRATED, MAX_TAIL, TAIL_SHARE, TRUNCATION_NOTICE_TOKENS, SECTION_STOP_WORDS;
4178
+ var DEFAULT_BUDGET, RESERVE_TOKENS, GUARDRAIL_SHARE, MIN_BODY_TOKENS, MAX_HYDRATED, MAX_TAIL, TAIL_SHARE, TRUNCATION_NOTICE_TOKENS, STACK_SHARE, MIN_REFERENCES, SEARCH_RESULTS, MAX_QUERY_CHARS, SECTION_STOP_WORDS;
4073
4179
  var init_context_builder = __esm({
4074
4180
  "src/lib/context-builder.ts"() {
4075
4181
  "use strict";
4182
+ init_language();
4076
4183
  init_scoring();
4184
+ init_spec_label();
4077
4185
  init_scoring();
4078
4186
  DEFAULT_BUDGET = 4e3;
4079
4187
  RESERVE_TOKENS = 200;
@@ -4083,6 +4191,10 @@ var init_context_builder = __esm({
4083
4191
  MAX_TAIL = 10;
4084
4192
  TAIL_SHARE = 0.15;
4085
4193
  TRUNCATION_NOTICE_TOKENS = 30;
4194
+ STACK_SHARE = 0.1;
4195
+ MIN_REFERENCES = 3;
4196
+ SEARCH_RESULTS = 30;
4197
+ MAX_QUERY_CHARS = 300;
4086
4198
  SECTION_STOP_WORDS = /* @__PURE__ */ new Set([
4087
4199
  "the",
4088
4200
  "a",
@@ -5234,7 +5346,7 @@ var init_server = __esm({
5234
5346
  init_create_prompt_template();
5235
5347
  init_skills2();
5236
5348
  init_team_setup();
5237
- SERVER_VERSION = true ? "0.1.51" : "0.0.0-dev";
5349
+ SERVER_VERSION = true ? "0.1.52" : "0.0.0-dev";
5238
5350
  }
5239
5351
  });
5240
5352
 
package/dist/http.js CHANGED
@@ -851,6 +851,17 @@ ${text3}`
851
851
 
852
852
  // src/tools/get-specs.ts
853
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
854
865
  function registerGetSpecs(server, apiClient) {
855
866
  server.registerTool(
856
867
  "get_specs",
@@ -915,7 +926,7 @@ ${spec.taskContent}`);
915
926
  };
916
927
  }
917
928
  const text3 = filtered.map(
918
- (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}` : "")
919
930
  ).join("\n");
920
931
  return {
921
932
  content: [
@@ -933,6 +944,11 @@ ${text3}`
933
944
 
934
945
  // src/tools/get-docs.ts
935
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
+ }
936
952
  function matchGlob(path, pattern) {
937
953
  const regex = pattern.replace(/\./g, "\\.").replace(/\*\*/g, "{{GLOBSTAR}}").replace(/\*/g, "[^/]*").replace(/\{\{GLOBSTAR\}\}/g, ".*");
938
954
  return new RegExp(`^${regex}$`).test(path);
@@ -993,11 +1009,17 @@ ${doc.content}`;
993
1009
  const params = {};
994
1010
  if (repo) params.repo = repo;
995
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
+ }
996
1017
  const response = await apiClient.get(
997
1018
  "/v1/docs",
998
1019
  params
999
1020
  );
1000
1021
  let items = response.items;
1022
+ const unread = path ? response.total - response.items.length : 0;
1001
1023
  if (path) {
1002
1024
  items = items.filter((item) => matchGlob(item.path, path));
1003
1025
  }
@@ -1027,7 +1049,9 @@ ${doc.content}`;
1027
1049
 
1028
1050
  ${text3}
1029
1051
 
1030
- 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."
1031
1055
  }
1032
1056
  ]
1033
1057
  };
@@ -1128,7 +1152,9 @@ ${deps}`);
1128
1152
  }
1129
1153
  if (ctx.adrs.length > 0) {
1130
1154
  const adrs = ctx.adrs.map((a) => `- ${a.title}${a.repo ? ` (${a.repo})` : ""}`).join("\n");
1131
- 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})
1132
1158
 
1133
1159
  ${adrs}`);
1134
1160
  }
@@ -1500,6 +1526,9 @@ var SEVERITY_SCORES = {
1500
1526
  medium: 0.5,
1501
1527
  low: 0.25
1502
1528
  };
1529
+ var MIN_TITLE_SHARE = 1 / 3;
1530
+ var RECORD_PATH = /(^|\/)changelog\//;
1531
+ var RECORD_FACTOR = 0.5;
1503
1532
  var CHARS_PER_TOKEN = 4;
1504
1533
  function estimateTokens(text3) {
1505
1534
  return Math.ceil(text3.length / CHARS_PER_TOKEN);
@@ -1552,6 +1581,32 @@ function scoreSeverity(item) {
1552
1581
  if (!item.severity) return 0.5;
1553
1582
  return SEVERITY_SCORES[item.severity] ?? 0.5;
1554
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
+ }
1555
1610
  function extractKeywords(text3) {
1556
1611
  const stopWords = /* @__PURE__ */ new Set([
1557
1612
  "the",
@@ -1648,7 +1703,8 @@ function scoreItems(items, taskDescription, filePaths) {
1648
1703
  const keywordScore = scoreKeywordMatch(item, keywords);
1649
1704
  const recencyScore = scoreRecency(item);
1650
1705
  const severityScore = scoreSeverity(item);
1651
- 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;
1652
1708
  return {
1653
1709
  type: item.type,
1654
1710
  id: item.id,
@@ -1657,7 +1713,7 @@ function scoreItems(items, taskDescription, filePaths) {
1657
1713
  score,
1658
1714
  tokenEstimate: estimateTokens(item.content)
1659
1715
  };
1660
- }).sort((a, b) => b.score - a.score);
1716
+ }).filter((item) => item.score > 0).sort((a, b) => b.score - a.score);
1661
1717
  }
1662
1718
  function selectWithinBudget(items, maxTokens) {
1663
1719
  const selected = [];
@@ -1679,6 +1735,10 @@ var MAX_HYDRATED = 8;
1679
1735
  var MAX_TAIL = 10;
1680
1736
  var TAIL_SHARE = 0.15;
1681
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;
1682
1742
  function formatGuardrail(g) {
1683
1743
  let text3 = `## ${g.title} [${g.severity}]
1684
1744
 
@@ -1697,7 +1757,7 @@ ${g.fix}
1697
1757
  }
1698
1758
  function formatSpecHeadline(s) {
1699
1759
  return `### ${s.name}
1700
- \`draft_id: ${s.draftId}\` \xB7 ${s.status} \xB7 ${s.requirementCount} requirements`;
1760
+ \`draft_id: ${s.draftId}\` \xB7 ${s.status} \xB7 ${specLabel(s)}`;
1701
1761
  }
1702
1762
  function formatDocHeadline(d) {
1703
1763
  return `### ${d.title}
@@ -1708,6 +1768,21 @@ function formatStack(entries2) {
1708
1768
  (e) => `- ${e.name}${e.version ? ` v${e.version}` : ""}${e.notes ? ` \u2014 ${e.notes}` : ""}`
1709
1769
  ).join("\n");
1710
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
+ }
1711
1786
  function formatRules(rules) {
1712
1787
  const byCategory = /* @__PURE__ */ new Map();
1713
1788
  for (const r of rules) {
@@ -1729,6 +1804,21 @@ function formatRules(rules) {
1729
1804
 
1730
1805
  ${text3.trim()}`;
1731
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
+ }
1732
1822
  async function fetchSpecBody(api, draftId) {
1733
1823
  const spec = await api.get(
1734
1824
  `/v1/specs/${encodeURIComponent(draftId)}`
@@ -1829,7 +1919,9 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
1829
1919
  const [rawGuardrails, rawSpecs, docs, stack, rules, docRepos] = await Promise.all([
1830
1920
  api.get("/v1/guardrails").catch(orEmpty([])),
1831
1921
  api.get("/v1/specs").catch(orEmpty([])),
1832
- api.get("/v1/docs").catch(orEmpty({ items: [] })),
1922
+ findDocs(api, taskDescription).catch(
1923
+ orEmpty({ items: [], searched: false })
1924
+ ),
1833
1925
  api.get("/v1/ai/settings/stack").catch(orEmpty([])),
1834
1926
  api.get("/v1/standards/rules").catch(orEmpty([])),
1835
1927
  // Capability states — FAIL-OPEN: an unreadable config (older backend, a
@@ -1842,6 +1934,8 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
1842
1934
  const specs = disabled("requirements") && disabled("design") && disabled("specs") ? [] : rawSpecs;
1843
1935
  const refs = /* @__PURE__ */ new Map();
1844
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));
1845
1939
  const scorable = [
1846
1940
  ...guardrails.map((g) => ({
1847
1941
  type: "guardrail",
@@ -1852,12 +1946,12 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
1852
1946
  severity: g.severity,
1853
1947
  updatedAt: g.updatedAt
1854
1948
  })),
1855
- ...specs.filter((s) => s.status !== "archived").map((s) => {
1949
+ ...specs.filter((s) => s.status !== "archived" && !isCopyOfDoc(s)).map((s) => {
1856
1950
  refs.set(key("spec", s.draftId), {
1857
1951
  type: "spec",
1858
1952
  id: s.draftId,
1859
1953
  title: s.name,
1860
- 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)})`,
1861
1955
  fetchHint: `get_specs(draft_id="${s.draftId}")`
1862
1956
  });
1863
1957
  return {
@@ -1868,7 +1962,7 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
1868
1962
  updatedAt: s.updatedAt
1869
1963
  };
1870
1964
  }),
1871
- ...docs.items.map((d) => {
1965
+ ...docs.items.map((d, position) => {
1872
1966
  const section = selectSection(d.outline, taskDescription);
1873
1967
  const pointer = section ? formatSectionPointer(d.repo_full_name, d.path, section) : void 0;
1874
1968
  refs.set(key("doc", d.id), {
@@ -1887,7 +1981,9 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
1887
1981
  title: d.title,
1888
1982
  content: formatDocHeadline(d),
1889
1983
  category: d.doc_type === "adr" ? "architecture" : void 0,
1890
- 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
1891
1987
  };
1892
1988
  })
1893
1989
  ];
@@ -1895,9 +1991,16 @@ async function buildContext(api, { taskDescription, filePaths = [], maxTokens })
1895
1991
  const sections = [];
1896
1992
  let overhead = 0;
1897
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
+ );
1898
1999
  const section = `## Tech Stack
1899
2000
 
1900
- ${formatStack(stack)}`;
2001
+ ${text3}` + (hidden > 0 ? `
2002
+
2003
+ _${hidden} more with \`get_tech_stack\`._` : "");
1901
2004
  sections.push(section);
1902
2005
  overhead += estimateTokens(section);
1903
2006
  }
@@ -2016,9 +2119,10 @@ _Highest-ranked items \u2014 docs are scoped to their most relevant section. Eac
2016
2119
  const tailLines = [];
2017
2120
  for (const meta of tailCandidates) {
2018
2121
  const cost = estimateTokens(meta.listLine);
2019
- if (cost > remaining) break;
2122
+ const named = blocks.length + tailLines.length;
2123
+ if (cost > remaining && named >= MIN_REFERENCES) break;
2020
2124
  tailLines.push(meta.listLine);
2021
- remaining -= cost;
2125
+ remaining = Math.max(0, remaining - cost);
2022
2126
  }
2023
2127
  if (tailLines.length > 0) {
2024
2128
  sections.push(
@@ -3001,9 +3105,9 @@ function localDate(now) {
3001
3105
  return `${now.getFullYear()}-${p(now.getMonth() + 1)}-${p(now.getDate())}`;
3002
3106
  }
3003
3107
  function conflictCopyName(path, date, taken) {
3004
- const stem = path.replace(/\.md$/, "");
3108
+ const stem2 = path.replace(/\.md$/, "");
3005
3109
  for (let n = 1; ; n++) {
3006
- const name = `${stem}.local-${date}${n === 1 ? "" : `-${n}`}.md`;
3110
+ const name = `${stem2}.local-${date}${n === 1 ? "" : `-${n}`}.md`;
3007
3111
  if (!taken.has(name.toLowerCase())) return name;
3008
3112
  }
3009
3113
  }
@@ -3890,7 +3994,7 @@ function registerTeamSetupTools(server, apiClient, { localFiles }) {
3890
3994
  }
3891
3995
 
3892
3996
  // src/server.ts
3893
- var SERVER_VERSION = true ? "0.1.51" : "0.0.0-dev";
3997
+ var SERVER_VERSION = true ? "0.1.52" : "0.0.0-dev";
3894
3998
  function createServer(apiClient, { localFiles = false, instructions, keyProblem } = {}) {
3895
3999
  const server = new McpServer(
3896
4000
  {