@systemdox/mcp-server 0.1.45 → 0.1.46

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
@@ -34,6 +34,28 @@ var ResponseCache = class {
34
34
  };
35
35
 
36
36
  // src/lib/api-client.ts
37
+ var SystemDoxApiError = class extends Error {
38
+ constructor(status, text3) {
39
+ super(`SystemDox API error ${status}: ${text3}`);
40
+ this.status = status;
41
+ this.text = text3;
42
+ let body;
43
+ try {
44
+ body = JSON.parse(text3);
45
+ } catch {
46
+ body = void 0;
47
+ }
48
+ this.body = body;
49
+ const code = body?.code;
50
+ this.code = typeof code === "string" ? code : /\b(SYD-[A-Z]\d{3})\b/.exec(text3)?.[1];
51
+ }
52
+ status;
53
+ text;
54
+ code;
55
+ body;
56
+ };
57
+ var TEAM_SETUP = "/v1/team-setup";
58
+ var seg = (s) => encodeURIComponent(String(s));
37
59
  var SystemDoxApiClient = class {
38
60
  config;
39
61
  cache;
@@ -55,10 +77,7 @@ var SystemDoxApiClient = class {
55
77
  const cached = this.cache.get(cacheKey);
56
78
  if (cached !== void 0) return cached;
57
79
  const res = await this.fetchRaw(path, params);
58
- if (!res.ok) {
59
- const body = await res.text();
60
- throw new Error(`SystemDox API error ${res.status}: ${body}`);
61
- }
80
+ if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
62
81
  const data = await res.json();
63
82
  this.cache.set(cacheKey, data);
64
83
  return data;
@@ -67,10 +86,7 @@ var SystemDoxApiClient = class {
67
86
  * latest now, not the one cached before someone published another. */
68
87
  async getFresh(path) {
69
88
  const res = await this.fetchRaw(path);
70
- if (!res.ok) {
71
- const body = await res.text();
72
- throw new Error(`SystemDox API error ${res.status}: ${body}`);
73
- }
89
+ if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
74
90
  return await res.json();
75
91
  }
76
92
  async post(path, body) {
@@ -92,12 +108,101 @@ var SystemDoxApiClient = class {
92
108
  },
93
109
  body: JSON.stringify(body)
94
110
  });
95
- if (!res.ok) {
96
- const text2 = await res.text();
97
- throw new Error(`SystemDox API error ${res.status}: ${text2}`);
98
- }
111
+ if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
99
112
  return await res.json();
100
113
  }
114
+ // ── Team setup (ADR-217, specs/changes/team-setup/openapi.yaml) ──────────
115
+ // One method per operationId. Every read skips the cache: setup, sync and
116
+ // plugin-path act on what the workspace holds now.
117
+ /** getOwnTeamSetup: the key's own workspace, named in `workspace`. */
118
+ getOwnTeamSetup() {
119
+ return this.getFresh(TEAM_SETUP);
120
+ }
121
+ getTeamSetup(workspace) {
122
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}`);
123
+ }
124
+ getMarketplace(workspace) {
125
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/marketplace.json`);
126
+ }
127
+ getPart(workspace, part) {
128
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/parts/${seg(part)}`);
129
+ }
130
+ publishPartVersion(workspace, part, files) {
131
+ return this.post(`${TEAM_SETUP}/${seg(workspace)}/parts/${seg(part)}/versions`, { files });
132
+ }
133
+ getPartVersion(workspace, part, version) {
134
+ return this.getFresh(
135
+ `${TEAM_SETUP}/${seg(workspace)}/parts/${seg(part)}/versions/${seg(version)}`
136
+ );
137
+ }
138
+ getSetupSkills(workspace) {
139
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/skills`);
140
+ }
141
+ setSetupSkills(workspace, skills) {
142
+ return this.put(`${TEAM_SETUP}/${seg(workspace)}/skills`, { skills });
143
+ }
144
+ getPlugin(workspace) {
145
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/plugin`);
146
+ }
147
+ /** getPluginZip: the zip's bytes as served, and its X-Content-SHA256
148
+ * header. The caller checks the BYTES against the hash it was given. */
149
+ async getPluginZip(workspace, version) {
150
+ const res = await fetch(
151
+ this.buildUrl(`${TEAM_SETUP}/${seg(workspace)}/plugin/versions/${seg(version)}/zip`),
152
+ {
153
+ headers: {
154
+ Authorization: `Bearer ${this.config.apiKey}`,
155
+ "X-API-Key": this.config.apiKey,
156
+ Accept: "application/zip"
157
+ }
158
+ }
159
+ );
160
+ if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
161
+ return { bytes: Buffer.from(await res.arrayBuffer()), sha256: res.headers.get("x-content-sha256") };
162
+ }
163
+ listMemoryProjects(workspace) {
164
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/memory`);
165
+ }
166
+ getMemory(workspace, project) {
167
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}`);
168
+ }
169
+ /** pushMemoryVersion: a stale base throws SystemDoxApiError with status 409,
170
+ * code SYD-K010 and `body.currentVersion`. */
171
+ pushMemoryVersion(workspace, project, body) {
172
+ return this.post(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/versions`, body);
173
+ }
174
+ getMemoryVersion(workspace, project, version) {
175
+ return this.getFresh(
176
+ `${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/versions/${seg(version)}`
177
+ );
178
+ }
179
+ findMissingBlobs(workspace, project, hashes) {
180
+ return this.post(
181
+ `${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/blobs/missing`,
182
+ { hashes }
183
+ );
184
+ }
185
+ uploadBlobs(workspace, project, blobs) {
186
+ return this.post(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/blobs`, { blobs });
187
+ }
188
+ readBlobs(workspace, project, hashes) {
189
+ return this.post(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/blobs/read`, { hashes });
190
+ }
191
+ listValueAnswers(workspace) {
192
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/values`);
193
+ }
194
+ lookupValues(workspace, values) {
195
+ return this.post(`${TEAM_SETUP}/${seg(workspace)}/values`, { values });
196
+ }
197
+ answerValues(workspace, answers) {
198
+ return this.put(`${TEAM_SETUP}/${seg(workspace)}/values`, { answers });
199
+ }
200
+ fileMemoryPeople(workspace, people) {
201
+ return this.post(`${TEAM_SETUP}/${seg(workspace)}/people`, { people });
202
+ }
203
+ resolvePerson(workspace, personId) {
204
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/people/${seg(personId)}`);
205
+ }
101
206
  invalidateCache(path) {
102
207
  if (path) {
103
208
  this.cache.invalidate(this.buildUrl(path).toString());
@@ -313,12 +418,12 @@ function registerGetGuardrails(server, apiClient) {
313
418
  );
314
419
  }
315
420
  if (filtered.length === 0) {
316
- const text3 = guardrails.length === 0 ? `No checks found. Add checks at ${configLink("/docs/guardrails", await workspaceBase(apiClient))}` : `No checks found${lang ? ` for language "${lang}"` : ""}${category ? ` in category "${category}"` : ""}.`;
421
+ const text4 = guardrails.length === 0 ? `No checks found. Add checks at ${configLink("/docs/guardrails", await workspaceBase(apiClient))}` : `No checks found${lang ? ` for language "${lang}"` : ""}${category ? ` in category "${category}"` : ""}.`;
317
422
  return {
318
- content: [{ type: "text", text: text3 }]
423
+ content: [{ type: "text", text: text4 }]
319
424
  };
320
425
  }
321
- const text2 = filtered.map(
426
+ const text3 = filtered.map(
322
427
  (g) => `## ${g.title} [${g.severity}]
323
428
 
324
429
  ${g.description}
@@ -342,7 +447,7 @@ ${g.fix}
342
447
  type: "text",
343
448
  text: `Found ${filtered.length} check(s):
344
449
 
345
- ${text2}`
450
+ ${text3}`
346
451
  }
347
452
  ]
348
453
  };
@@ -432,7 +537,7 @@ ${spec.taskContent}`);
432
537
  ]
433
538
  };
434
539
  }
435
- const text2 = filtered.map(
540
+ const text3 = filtered.map(
436
541
  (s) => `- **${s.name}** (${s.draftId}) \u2014 ${s.status}, ${s.requirementCount} requirements` + (s.repoFullName ? `, repo: ${s.repoFullName}` : "")
437
542
  ).join("\n");
438
543
  return {
@@ -441,7 +546,7 @@ ${spec.taskContent}`);
441
546
  type: "text",
442
547
  text: `Found ${filtered.length} spec(s):
443
548
 
444
- ${text2}`
549
+ ${text3}`
445
550
  }
446
551
  ]
447
552
  };
@@ -494,18 +599,18 @@ function registerGetDocs(server, apiClient) {
494
599
  isError: true
495
600
  };
496
601
  }
497
- let text3 = doc.content;
602
+ let text4 = doc.content;
498
603
  if (anchor && doc.section_resolved === false) {
499
- text3 = `_Section "${anchor}" not found \u2014 returning the full document._
604
+ text4 = `_Section "${anchor}" not found \u2014 returning the full document._
500
605
 
501
- ` + text3;
606
+ ` + text4;
502
607
  } else if (anchor && doc.heading) {
503
- text3 = `## ${doc.heading}
608
+ text4 = `## ${doc.heading}
504
609
 
505
610
  ${doc.content}`;
506
611
  }
507
612
  return {
508
- content: [{ type: "text", text: text3 }]
613
+ content: [{ type: "text", text: text4 }]
509
614
  };
510
615
  }
511
616
  const params = {};
@@ -529,7 +634,7 @@ ${doc.content}`;
529
634
  ]
530
635
  };
531
636
  }
532
- const text2 = items.map((d) => {
637
+ const text3 = items.map((d) => {
533
638
  const sections = (d.outline ?? []).slice(0, 8);
534
639
  const sectionHint = sections.length > 0 ? `
535
640
  sections: ${sections.map((s) => s.anchor).join(", ")}` : "";
@@ -543,7 +648,7 @@ ${doc.content}`;
543
648
  type: "text",
544
649
  text: `Found ${items.length} document(s) (${response.total} total):
545
650
 
546
- ${text2}
651
+ ${text3}
547
652
 
548
653
  Use doc_id for full content, or doc_id + section to fetch one section.`
549
654
  }
@@ -878,13 +983,13 @@ var SEVERITY_SCORES = {
878
983
  low: 0.25
879
984
  };
880
985
  var CHARS_PER_TOKEN = 4;
881
- function estimateTokens(text2) {
882
- return Math.ceil(text2.length / CHARS_PER_TOKEN);
986
+ function estimateTokens(text3) {
987
+ return Math.ceil(text3.length / CHARS_PER_TOKEN);
883
988
  }
884
- function truncateToTokens(text2, maxTokens) {
989
+ function truncateToTokens(text3, maxTokens) {
885
990
  const maxChars = Math.max(0, maxTokens) * CHARS_PER_TOKEN;
886
- if (text2.length <= maxChars) return { text: text2, truncated: false };
887
- const head = text2.slice(0, maxChars);
991
+ if (text3.length <= maxChars) return { text: text3, truncated: false };
992
+ const head = text3.slice(0, maxChars);
888
993
  const floor = Math.floor(maxChars * 0.75);
889
994
  const paragraph = head.lastIndexOf("\n\n");
890
995
  const line = head.lastIndexOf("\n");
@@ -929,7 +1034,7 @@ function scoreSeverity(item) {
929
1034
  if (!item.severity) return 0.5;
930
1035
  return SEVERITY_SCORES[item.severity] ?? 0.5;
931
1036
  }
932
- function extractKeywords(text2) {
1037
+ function extractKeywords(text3) {
933
1038
  const stopWords = /* @__PURE__ */ new Set([
934
1039
  "the",
935
1040
  "a",
@@ -1014,7 +1119,7 @@ function extractKeywords(text2) {
1014
1119
  "implement",
1015
1120
  "create"
1016
1121
  ]);
1017
- return text2.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !stopWords.has(w));
1122
+ return text3.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !stopWords.has(w));
1018
1123
  }
1019
1124
  function scoreItems(items, taskDescription, filePaths) {
1020
1125
  const languages = detectLanguages(filePaths);
@@ -1057,20 +1162,20 @@ var MAX_TAIL = 10;
1057
1162
  var TAIL_SHARE = 0.15;
1058
1163
  var TRUNCATION_NOTICE_TOKENS = 30;
1059
1164
  function formatGuardrail(g) {
1060
- let text2 = `## ${g.title} [${g.severity}]
1165
+ let text3 = `## ${g.title} [${g.severity}]
1061
1166
 
1062
1167
  ${g.description}`;
1063
- if (g.pattern) text2 += `
1168
+ if (g.pattern) text3 += `
1064
1169
 
1065
1170
  **Bad:** \`\`\`
1066
1171
  ${g.pattern}
1067
1172
  \`\`\``;
1068
- if (g.fix) text2 += `
1173
+ if (g.fix) text3 += `
1069
1174
 
1070
1175
  **Good:** \`\`\`
1071
1176
  ${g.fix}
1072
1177
  \`\`\``;
1073
- return text2;
1178
+ return text3;
1074
1179
  }
1075
1180
  function formatSpecHeadline(s) {
1076
1181
  return `### ${s.name}
@@ -1092,19 +1197,19 @@ function formatRules(rules) {
1092
1197
  if (!byCategory.has(cat)) byCategory.set(cat, []);
1093
1198
  byCategory.get(cat).push(r);
1094
1199
  }
1095
- let text2 = "";
1200
+ let text3 = "";
1096
1201
  for (const [cat, catRules] of byCategory) {
1097
- text2 += `### ${cat.charAt(0).toUpperCase() + cat.slice(1)}
1202
+ text3 += `### ${cat.charAt(0).toUpperCase() + cat.slice(1)}
1098
1203
  `;
1099
1204
  for (const r of catRules) {
1100
- text2 += `- ${r.text}
1205
+ text3 += `- ${r.text}
1101
1206
  `;
1102
1207
  }
1103
- text2 += "\n";
1208
+ text3 += "\n";
1104
1209
  }
1105
1210
  return `## Project Rules (${rules.length})
1106
1211
 
1107
- ${text2.trim()}`;
1212
+ ${text3.trim()}`;
1108
1213
  }
1109
1214
  async function fetchSpecBody(api, draftId) {
1110
1215
  const spec = await api.get(
@@ -1441,13 +1546,13 @@ function registerGetContext(server, apiClient) {
1441
1546
  }
1442
1547
  },
1443
1548
  async ({ task_description, file_paths, max_tokens }) => {
1444
- const { text: text2 } = await buildContext(apiClient, {
1549
+ const { text: text3 } = await buildContext(apiClient, {
1445
1550
  taskDescription: task_description,
1446
1551
  filePaths: file_paths,
1447
1552
  maxTokens: max_tokens
1448
1553
  });
1449
1554
  return {
1450
- content: [{ type: "text", text: text2 }]
1555
+ content: [{ type: "text", text: text3 }]
1451
1556
  };
1452
1557
  }
1453
1558
  );
@@ -1695,9 +1800,9 @@ function registerGetProjectRules(server, apiClient) {
1695
1800
  );
1696
1801
  }
1697
1802
  if (filtered.length === 0) {
1698
- const text3 = rules.length === 0 ? `No project rules found. Add rules at ${configLink("/docs/rules", await workspaceBase(apiClient))}` : `No project rules found${category ? ` in category "${category}"` : ""}.`;
1803
+ const text4 = rules.length === 0 ? `No project rules found. Add rules at ${configLink("/docs/rules", await workspaceBase(apiClient))}` : `No project rules found${category ? ` in category "${category}"` : ""}.`;
1699
1804
  return {
1700
- content: [{ type: "text", text: text3 }]
1805
+ content: [{ type: "text", text: text4 }]
1701
1806
  };
1702
1807
  }
1703
1808
  const grouped = /* @__PURE__ */ new Map();
@@ -1706,25 +1811,25 @@ function registerGetProjectRules(server, apiClient) {
1706
1811
  if (!grouped.has(cat)) grouped.set(cat, []);
1707
1812
  grouped.get(cat).push(rule);
1708
1813
  }
1709
- let text2 = `Found ${filtered.length} project rule(s):
1814
+ let text3 = `Found ${filtered.length} project rule(s):
1710
1815
 
1711
1816
  `;
1712
1817
  for (const [cat, catRules] of grouped) {
1713
1818
  const label = CATEGORY_LABELS[cat] ?? cat;
1714
- text2 += `## ${label}
1819
+ text3 += `## ${label}
1715
1820
 
1716
1821
  `;
1717
1822
  for (const rule of catRules) {
1718
- text2 += `- ${rule.text}
1823
+ text3 += `- ${rule.text}
1719
1824
  `;
1720
1825
  }
1721
- text2 += "\n";
1826
+ text3 += "\n";
1722
1827
  }
1723
1828
  return {
1724
1829
  content: [
1725
1830
  {
1726
1831
  type: "text",
1727
- text: text2
1832
+ text: text3
1728
1833
  }
1729
1834
  ]
1730
1835
  };
@@ -1750,10 +1855,10 @@ function registerCreateProjectRule(server, apiClient) {
1750
1855
  readOnlyHint: false
1751
1856
  }
1752
1857
  },
1753
- async ({ text: text2, category }) => {
1858
+ async ({ text: text3, category }) => {
1754
1859
  const saved = await apiClient.post(
1755
1860
  "/v1/standards/rules",
1756
- { text: text2, category }
1861
+ { text: text3, category }
1757
1862
  );
1758
1863
  apiClient.invalidateCache("/v1/standards/rules");
1759
1864
  return {
@@ -1791,7 +1896,7 @@ function registerGetPromptTemplates(server, apiClient) {
1791
1896
  ]
1792
1897
  };
1793
1898
  }
1794
- const text2 = templates.map(
1899
+ const text3 = templates.map(
1795
1900
  (t) => `## ${t.name}
1796
1901
 
1797
1902
  ` + (t.description ? `*${t.description}*
@@ -1806,7 +1911,7 @@ ${t.prompt}
1806
1911
  type: "text",
1807
1912
  text: `Found ${templates.length} prompt template(s):
1808
1913
 
1809
- ${text2}`
1914
+ ${text3}`
1810
1915
  }
1811
1916
  ]
1812
1917
  };
@@ -1914,18 +2019,9 @@ async function walk(root, { skipHidden }, dir = root, out = { files: [], links:
1914
2019
  out.files.sort();
1915
2020
  return out;
1916
2021
  }
1917
- async function readSkillDirectory(directory) {
1918
- const root = resolve(directory);
1919
- if (!existsSync(join(root, ENTRY_FILE))) {
1920
- throw new Error(`${root} has no ${ENTRY_FILE}: a skill folder holds SKILL.md at its root`);
1921
- }
1922
- const walked = await walk(root, { skipHidden: true });
1923
- if (walked.links.length) {
1924
- throw new Error(`${walked.links.join(", ")}: links are not followed; copy the file into the skill folder`);
1925
- }
1926
- const paths = walked.files;
2022
+ async function readTextFiles(root, paths, what) {
1927
2023
  if (paths.length > MAX_FILES) {
1928
- throw new Error(`${root} holds ${paths.length} files: a skill holds at most ${MAX_FILES}`);
2024
+ throw new Error(`${root} holds ${paths.length} files: ${what} holds at most ${MAX_FILES}`);
1929
2025
  }
1930
2026
  const decoder = new TextDecoder("utf-8", { fatal: true });
1931
2027
  const files = [];
@@ -1935,7 +2031,7 @@ async function readSkillDirectory(directory) {
1935
2031
  throw new Error(`${path}: file names may use letters, digits, ".", "_" and "-" only`);
1936
2032
  }
1937
2033
  const raw = await readFile(join(root, ...path.split("/")));
1938
- if (raw.includes(0)) throw new Error(`${path} is binary: a skill holds text files only`);
2034
+ if (raw.includes(0)) throw new Error(`${path} is binary: ${what} holds text files only`);
1939
2035
  if (raw.length > MAX_FILE_BYTES) {
1940
2036
  throw new Error(`${path} is ${raw.length} bytes: a file holds at most ${MAX_FILE_BYTES}`);
1941
2037
  }
@@ -1951,6 +2047,18 @@ async function readSkillDirectory(directory) {
1951
2047
  }
1952
2048
  files.push({ path, content });
1953
2049
  }
2050
+ return files;
2051
+ }
2052
+ async function readSkillDirectory(directory) {
2053
+ const root = resolve(directory);
2054
+ if (!existsSync(join(root, ENTRY_FILE))) {
2055
+ throw new Error(`${root} has no ${ENTRY_FILE}: a skill folder holds SKILL.md at its root`);
2056
+ }
2057
+ const walked = await walk(root, { skipHidden: true });
2058
+ if (walked.links.length) {
2059
+ throw new Error(`${walked.links.join(", ")}: links are not followed; copy the file into the skill folder`);
2060
+ }
2061
+ const files = await readTextFiles(root, walked.files, "a skill");
1954
2062
  const skillMd = files.find((f) => f.path === ENTRY_FILE);
1955
2063
  const name = frontmatterName(skillMd.content);
1956
2064
  if (!name) throw new Error(`${ENTRY_FILE} has no frontmatter name`);
@@ -2032,8 +2140,8 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
2032
2140
  keptEdited
2033
2141
  };
2034
2142
  }
2035
- function fenceFor(text2) {
2036
- const longest = Math.max(0, ...(text2.match(/`+/g) ?? []).map((r) => r.length));
2143
+ function fenceFor(text3) {
2144
+ const longest = Math.max(0, ...(text3.match(/`+/g) ?? []).map((r) => r.length));
2037
2145
  return "`".repeat(Math.max(3, longest + 1));
2038
2146
  }
2039
2147
  function describeInstall(name, r) {
@@ -2189,8 +2297,186 @@ ${body}`
2189
2297
  );
2190
2298
  }
2191
2299
 
2300
+ // src/tools/team-setup.ts
2301
+ import { z as z14 } from "zod";
2302
+ import { existsSync as existsSync2 } from "fs";
2303
+ import { lstat as lstat2 } from "fs/promises";
2304
+ import { homedir as homedir3 } from "os";
2305
+ import { join as join2, resolve as resolve3 } from "path";
2306
+
2307
+ // src/lib/person-values.ts
2308
+ var W = "[\\p{L}\\p{N}_]";
2309
+ var D = "\\p{Nd}";
2310
+ var WORD_OR = (extra) => `[\\p{L}\\p{N}_${extra}]`;
2311
+ var PERSON_ID = "(?:con|ctc|usr)_[0-9A-Za-z]{10,40}";
2312
+ var PERSON_TOKEN = `\`${PERSON_ID}\`(?:[ \\t]*\\([^()\\n]*\\))?`;
2313
+ var BEFORE = `(?<!${WORD_OR("+.:/\\\\\\-")})(?<!${D}[ -])`;
2314
+ var AFTER = `(?!${WORD_OR(":/\\\\")}|\\.${D}|[ -]?${D}|-${W})`;
2315
+ var E164 = `${BEFORE}\\+${D}+(?:[ -]${D}+)*${AFTER}`;
2316
+ var NATIONAL = `${BEFORE}0${D}*(?:[ -]${D}+)*${AFTER}`;
2317
+ var ATEXT = "[A-Za-z0-9!#$%&'*+/=?^_{|}~\\-]";
2318
+ var EMAIL_RE = "(?<![A-Za-z0-9!#$%&*+/=?^_{|}~.@\\-])[A-Za-z0-9_]" + ATEXT + "*(?:\\." + ATEXT + "+)*@(?:[A-Za-z0-9](?:[A-Za-z0-9\\-]{0,61}[A-Za-z0-9])?\\.)+[A-Za-z]{2,63}(?![A-Za-z0-9\\-]|\\.[A-Za-z0-9])";
2319
+ var HOME_PREFIX = "(?:[A-Za-z]:\\\\+Users\\\\+|[A-Za-z]:/+Users/+|/Users/|/home/)";
2320
+ var HOME = `(?<!${WORD_OR(".~:/\\\\$}\\-")})${HOME_PREFIX}[^\\\\/\\s"'\`<>|:*?]+`;
2321
+ var KEY_PREFIX = `(?<!${W})(?:pweb_[a-z]+_key_|sk-ant-|sk_live_|rk_live_|github_pat_|gh[pousr]_|sntrys_|xox[baprs]-)[A-Za-z0-9_\\-]{4,}`;
2322
+
2323
+ // src/lib/team-setup-types.ts
2324
+ var PARTS = ["hooks", "mcp", "agents", "commands", "instructions"];
2325
+ var PERSON_ID_RE = new RegExp(`^${PERSON_ID}$`);
2326
+
2327
+ // src/tools/team-setup.ts
2328
+ var LAYOUT = {
2329
+ hooks: { files: ["hooks/hooks.json"], folders: [{ dir: "scripts", md: false }], required: "hooks/hooks.json" },
2330
+ mcp: { files: [".mcp.json"], folders: [], required: ".mcp.json" },
2331
+ agents: { files: [], folders: [{ dir: "agents", md: true }] },
2332
+ commands: { files: [], folders: [{ dir: "commands", md: true }] },
2333
+ instructions: { files: ["CLAUDE.md"], folders: [], required: "CLAUDE.md" }
2334
+ };
2335
+ function expandHome2(path) {
2336
+ return path.replace(/^~(?=$|[\\/])/, homedir3());
2337
+ }
2338
+ function text2(t) {
2339
+ return { content: [{ type: "text", text: t }] };
2340
+ }
2341
+ async function isLink(path) {
2342
+ try {
2343
+ return (await lstat2(path)).isSymbolicLink();
2344
+ } catch (e) {
2345
+ if (e.code === "ENOENT") return false;
2346
+ throw e;
2347
+ }
2348
+ }
2349
+ async function readPartDirectory(part, directory) {
2350
+ const root = resolve3(directory);
2351
+ const layout = LAYOUT[part];
2352
+ const paths = [];
2353
+ const leftOut = [];
2354
+ const links = [];
2355
+ for (const path of layout.files) {
2356
+ const segments = path.split("/");
2357
+ let present = true;
2358
+ for (let i = 1; i <= segments.length; i++) {
2359
+ const rel = segments.slice(0, i).join("/");
2360
+ if (await isLink(join2(root, ...segments.slice(0, i)))) {
2361
+ links.push(rel);
2362
+ present = false;
2363
+ break;
2364
+ }
2365
+ }
2366
+ if (present && existsSync2(join2(root, ...segments))) paths.push(path);
2367
+ }
2368
+ for (const { dir, md } of layout.folders) {
2369
+ const full = join2(root, dir);
2370
+ if (await isLink(full)) {
2371
+ links.push(dir);
2372
+ continue;
2373
+ }
2374
+ if (!existsSync2(full)) continue;
2375
+ const walked = await walk(full, { skipHidden: true });
2376
+ links.push(...walked.links.map((p) => `${dir}/${p}`));
2377
+ leftOut.push(...walked.hidden.map((p) => `${dir}/${p}`));
2378
+ for (const p of walked.files) {
2379
+ if (md && !p.endsWith(".md")) leftOut.push(`${dir}/${p}`);
2380
+ else paths.push(`${dir}/${p}`);
2381
+ }
2382
+ }
2383
+ if (links.length) {
2384
+ throw new Error(
2385
+ `${links.join(", ")}: links are not followed; copy the file into the folder. Nothing was sent.`
2386
+ );
2387
+ }
2388
+ if (layout.required && !paths.includes(layout.required)) {
2389
+ throw new Error(`${root} has no ${layout.required}: the ${part} part needs it. Nothing was sent.`);
2390
+ }
2391
+ if (!paths.length) {
2392
+ throw new Error(
2393
+ `${root} has no ${layout.folders.map((f) => `${f.dir}/*.md`).join(" or ")}: nothing to publish for ${part}.`
2394
+ );
2395
+ }
2396
+ return { files: await readTextFiles(root, paths.sort(), `the ${part} part`), leftOut };
2397
+ }
2398
+ function describePerson(id, person) {
2399
+ const lines = [`**${person.label ?? "(no label)"}**, \`${id}\``];
2400
+ if (person.mergedInto) {
2401
+ lines.push(`\`${id}\` was merged into \`${person.mergedInto}\`: these are \`${person.mergedInto}\`'s details.`);
2402
+ }
2403
+ lines.push(`- Name: ${person.name ?? "(none held)"}`);
2404
+ lines.push(`- Phone number: ${person.phone ?? "(none held)"}`);
2405
+ lines.push(`- Email address: ${person.email ?? "(none held)"}`);
2406
+ const token = `\`${person.mergedInto ?? id}\`${person.label ? ` (${person.label})` : ""}`;
2407
+ lines.push(
2408
+ "",
2409
+ `These details are for this session only. Do not write them into a memory file, a note or a commit: a memory names this person by the identifier, as ${token}. A memory file that holds the details is offered for rewriting again at the next sync.`
2410
+ );
2411
+ return lines.join("\n");
2412
+ }
2413
+ function registerTeamSetupTools(server, apiClient, { localFiles }) {
2414
+ const fileSchema = z14.object({
2415
+ path: z14.string().describe(`Path inside the part's folder, forward slashes, e.g. "hooks/hooks.json"`),
2416
+ content: z14.string().describe("The file's full text, exactly as it is")
2417
+ });
2418
+ server.registerTool(
2419
+ "publish_setup_part",
2420
+ {
2421
+ title: "Publish Setup Part",
2422
+ description: "Publish one part of your team's Claude Code setup to SystemDox: `hooks` (hooks/hooks.json and the scripts/ it calls), `mcp` (.mcp.json), `agents` (agents/*.md), `commands` (commands/*.md) or `instructions` (CLAUDE.md). Every part but instructions makes a new version of the team plugin, which each member accepts at their next update. Publishing hooks, mcp, agents or commands needs the admin role. " + (localFiles ? "Pass `directory`: the server reads only the files the part accepts, byte for byte, and refuses a link, a binary file or a file too large before anything is sent. Never retype the files into `files` when the folder is on this computer." : "Pass every file of the part in `files`, exactly as it is."),
2423
+ inputSchema: {
2424
+ part: z14.enum(PARTS).describe("Which part of the setup"),
2425
+ ...localFiles ? {
2426
+ directory: z14.string().optional().describe(
2427
+ "The folder on this computer: for hooks, the one holding hooks/hooks.json and scripts/; for mcp, the one holding .mcp.json; for instructions, the one holding CLAUDE.md; for agents or commands, the one holding agents/ or commands/"
2428
+ )
2429
+ } : {},
2430
+ files: z14.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the part")
2431
+ },
2432
+ annotations: { readOnlyHint: false }
2433
+ },
2434
+ async (args) => {
2435
+ let files;
2436
+ let leftOut = [];
2437
+ if (localFiles && args.directory) {
2438
+ ({ files, leftOut } = await readPartDirectory(args.part, expandHome2(args.directory)));
2439
+ } else if (args.files?.length) {
2440
+ files = args.files;
2441
+ } else {
2442
+ throw new Error(localFiles ? "Pass `directory` (or `files`)." : "Pass `files`.");
2443
+ }
2444
+ const { workspace } = await apiClient.getOwnTeamSetup();
2445
+ const saved = await apiClient.publishPartVersion(workspace, args.part, files);
2446
+ const lines = [`Published **${saved.part}** version ${saved.version}.`];
2447
+ if (saved.part !== "instructions" && saved.pluginVersion !== void 0) {
2448
+ lines.push(`Team plugin version ${saved.pluginVersion}; each member accepts it at their next update.`);
2449
+ }
2450
+ if (saved.replacedValues?.length) {
2451
+ lines.push(
2452
+ "Replaced by a reference to each member's own environment variable:",
2453
+ ...saved.replacedValues.map((r) => `- ${r.server} ${r.field} ${r.name}: \${${r.name}}`)
2454
+ );
2455
+ }
2456
+ if (leftOut.length) lines.push(`Left out (not part of ${args.part}): ${leftOut.join(", ")}`);
2457
+ return text2(lines.join("\n"));
2458
+ }
2459
+ );
2460
+ server.registerTool(
2461
+ "resolve_person",
2462
+ {
2463
+ title: "Resolve Person",
2464
+ description: "Read back a person that a team memory names by identifier, written as `con_\u2026` (label), `ctc_\u2026` or `usr_\u2026`: their label, name, phone number and email address, from the vault that holds them. The details are for this session only: never write them into a memory file; the memory keeps the identifier. Writes nothing.",
2465
+ inputSchema: {
2466
+ id: z14.string().regex(PERSON_ID_RE, "a con_, ctc_ or usr_ identifier, as written in the memory").describe("The identifier, e.g. con_01JC2Q8Y4M7T0X3K5N9P2R6S8V")
2467
+ },
2468
+ annotations: { readOnlyHint: true }
2469
+ },
2470
+ async (args) => {
2471
+ const { workspace } = await apiClient.getOwnTeamSetup();
2472
+ const person = await apiClient.resolvePerson(workspace, args.id);
2473
+ return text2(describePerson(args.id, person));
2474
+ }
2475
+ );
2476
+ }
2477
+
2192
2478
  // src/server.ts
2193
- var SERVER_VERSION = true ? "0.1.45" : "0.0.0-dev";
2479
+ var SERVER_VERSION = true ? "0.1.46" : "0.0.0-dev";
2194
2480
  function createServer(apiClient, { localFiles = false } = {}) {
2195
2481
  const server = new McpServer({
2196
2482
  name: "systemdox-mcp-server",
@@ -2211,6 +2497,7 @@ function createServer(apiClient, { localFiles = false } = {}) {
2211
2497
  registerCreateProjectRule(server, apiClient);
2212
2498
  registerCreatePromptTemplate(server, apiClient);
2213
2499
  registerSkillTools(server, apiClient, { localFiles });
2500
+ registerTeamSetupTools(server, apiClient, { localFiles });
2214
2501
  return server;
2215
2502
  }
2216
2503