@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/index.js CHANGED
@@ -70,6 +70,28 @@ var ResponseCache = class {
70
70
  };
71
71
 
72
72
  // src/lib/api-client.ts
73
+ var SystemDoxApiError = class extends Error {
74
+ constructor(status, text3) {
75
+ super(`SystemDox API error ${status}: ${text3}`);
76
+ this.status = status;
77
+ this.text = text3;
78
+ let body;
79
+ try {
80
+ body = JSON.parse(text3);
81
+ } catch {
82
+ body = void 0;
83
+ }
84
+ this.body = body;
85
+ const code = body?.code;
86
+ this.code = typeof code === "string" ? code : /\b(SYD-[A-Z]\d{3})\b/.exec(text3)?.[1];
87
+ }
88
+ status;
89
+ text;
90
+ code;
91
+ body;
92
+ };
93
+ var TEAM_SETUP = "/v1/team-setup";
94
+ var seg = (s) => encodeURIComponent(String(s));
73
95
  var SystemDoxApiClient = class {
74
96
  config;
75
97
  cache;
@@ -91,10 +113,7 @@ var SystemDoxApiClient = class {
91
113
  const cached = this.cache.get(cacheKey);
92
114
  if (cached !== void 0) return cached;
93
115
  const res = await this.fetchRaw(path, params);
94
- if (!res.ok) {
95
- const body = await res.text();
96
- throw new Error(`SystemDox API error ${res.status}: ${body}`);
97
- }
116
+ if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
98
117
  const data = await res.json();
99
118
  this.cache.set(cacheKey, data);
100
119
  return data;
@@ -103,10 +122,7 @@ var SystemDoxApiClient = class {
103
122
  * latest now, not the one cached before someone published another. */
104
123
  async getFresh(path) {
105
124
  const res = await this.fetchRaw(path);
106
- if (!res.ok) {
107
- const body = await res.text();
108
- throw new Error(`SystemDox API error ${res.status}: ${body}`);
109
- }
125
+ if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
110
126
  return await res.json();
111
127
  }
112
128
  async post(path, body) {
@@ -128,12 +144,101 @@ var SystemDoxApiClient = class {
128
144
  },
129
145
  body: JSON.stringify(body)
130
146
  });
131
- if (!res.ok) {
132
- const text2 = await res.text();
133
- throw new Error(`SystemDox API error ${res.status}: ${text2}`);
134
- }
147
+ if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
135
148
  return await res.json();
136
149
  }
150
+ // ── Team setup (ADR-217, specs/changes/team-setup/openapi.yaml) ──────────
151
+ // One method per operationId. Every read skips the cache: setup, sync and
152
+ // plugin-path act on what the workspace holds now.
153
+ /** getOwnTeamSetup: the key's own workspace, named in `workspace`. */
154
+ getOwnTeamSetup() {
155
+ return this.getFresh(TEAM_SETUP);
156
+ }
157
+ getTeamSetup(workspace) {
158
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}`);
159
+ }
160
+ getMarketplace(workspace) {
161
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/marketplace.json`);
162
+ }
163
+ getPart(workspace, part) {
164
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/parts/${seg(part)}`);
165
+ }
166
+ publishPartVersion(workspace, part, files) {
167
+ return this.post(`${TEAM_SETUP}/${seg(workspace)}/parts/${seg(part)}/versions`, { files });
168
+ }
169
+ getPartVersion(workspace, part, version) {
170
+ return this.getFresh(
171
+ `${TEAM_SETUP}/${seg(workspace)}/parts/${seg(part)}/versions/${seg(version)}`
172
+ );
173
+ }
174
+ getSetupSkills(workspace) {
175
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/skills`);
176
+ }
177
+ setSetupSkills(workspace, skills) {
178
+ return this.put(`${TEAM_SETUP}/${seg(workspace)}/skills`, { skills });
179
+ }
180
+ getPlugin(workspace) {
181
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/plugin`);
182
+ }
183
+ /** getPluginZip: the zip's bytes as served, and its X-Content-SHA256
184
+ * header. The caller checks the BYTES against the hash it was given. */
185
+ async getPluginZip(workspace, version) {
186
+ const res = await fetch(
187
+ this.buildUrl(`${TEAM_SETUP}/${seg(workspace)}/plugin/versions/${seg(version)}/zip`),
188
+ {
189
+ headers: {
190
+ Authorization: `Bearer ${this.config.apiKey}`,
191
+ "X-API-Key": this.config.apiKey,
192
+ Accept: "application/zip"
193
+ }
194
+ }
195
+ );
196
+ if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
197
+ return { bytes: Buffer.from(await res.arrayBuffer()), sha256: res.headers.get("x-content-sha256") };
198
+ }
199
+ listMemoryProjects(workspace) {
200
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/memory`);
201
+ }
202
+ getMemory(workspace, project) {
203
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}`);
204
+ }
205
+ /** pushMemoryVersion: a stale base throws SystemDoxApiError with status 409,
206
+ * code SYD-K010 and `body.currentVersion`. */
207
+ pushMemoryVersion(workspace, project, body) {
208
+ return this.post(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/versions`, body);
209
+ }
210
+ getMemoryVersion(workspace, project, version) {
211
+ return this.getFresh(
212
+ `${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/versions/${seg(version)}`
213
+ );
214
+ }
215
+ findMissingBlobs(workspace, project, hashes) {
216
+ return this.post(
217
+ `${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/blobs/missing`,
218
+ { hashes }
219
+ );
220
+ }
221
+ uploadBlobs(workspace, project, blobs) {
222
+ return this.post(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/blobs`, { blobs });
223
+ }
224
+ readBlobs(workspace, project, hashes) {
225
+ return this.post(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/blobs/read`, { hashes });
226
+ }
227
+ listValueAnswers(workspace) {
228
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/values`);
229
+ }
230
+ lookupValues(workspace, values) {
231
+ return this.post(`${TEAM_SETUP}/${seg(workspace)}/values`, { values });
232
+ }
233
+ answerValues(workspace, answers) {
234
+ return this.put(`${TEAM_SETUP}/${seg(workspace)}/values`, { answers });
235
+ }
236
+ fileMemoryPeople(workspace, people) {
237
+ return this.post(`${TEAM_SETUP}/${seg(workspace)}/people`, { people });
238
+ }
239
+ resolvePerson(workspace, personId) {
240
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/people/${seg(personId)}`);
241
+ }
137
242
  invalidateCache(path) {
138
243
  if (path) {
139
244
  this.cache.invalidate(this.buildUrl(path).toString());
@@ -295,12 +400,12 @@ function registerGetGuardrails(server2, apiClient2) {
295
400
  );
296
401
  }
297
402
  if (filtered.length === 0) {
298
- const text3 = guardrails.length === 0 ? `No checks found. Add checks at ${configLink("/docs/guardrails", await workspaceBase(apiClient2))}` : `No checks found${lang ? ` for language "${lang}"` : ""}${category ? ` in category "${category}"` : ""}.`;
403
+ const text4 = guardrails.length === 0 ? `No checks found. Add checks at ${configLink("/docs/guardrails", await workspaceBase(apiClient2))}` : `No checks found${lang ? ` for language "${lang}"` : ""}${category ? ` in category "${category}"` : ""}.`;
299
404
  return {
300
- content: [{ type: "text", text: text3 }]
405
+ content: [{ type: "text", text: text4 }]
301
406
  };
302
407
  }
303
- const text2 = filtered.map(
408
+ const text3 = filtered.map(
304
409
  (g) => `## ${g.title} [${g.severity}]
305
410
 
306
411
  ${g.description}
@@ -324,7 +429,7 @@ ${g.fix}
324
429
  type: "text",
325
430
  text: `Found ${filtered.length} check(s):
326
431
 
327
- ${text2}`
432
+ ${text3}`
328
433
  }
329
434
  ]
330
435
  };
@@ -414,7 +519,7 @@ ${spec.taskContent}`);
414
519
  ]
415
520
  };
416
521
  }
417
- const text2 = filtered.map(
522
+ const text3 = filtered.map(
418
523
  (s) => `- **${s.name}** (${s.draftId}) \u2014 ${s.status}, ${s.requirementCount} requirements` + (s.repoFullName ? `, repo: ${s.repoFullName}` : "")
419
524
  ).join("\n");
420
525
  return {
@@ -423,7 +528,7 @@ ${spec.taskContent}`);
423
528
  type: "text",
424
529
  text: `Found ${filtered.length} spec(s):
425
530
 
426
- ${text2}`
531
+ ${text3}`
427
532
  }
428
533
  ]
429
534
  };
@@ -476,18 +581,18 @@ function registerGetDocs(server2, apiClient2) {
476
581
  isError: true
477
582
  };
478
583
  }
479
- let text3 = doc.content;
584
+ let text4 = doc.content;
480
585
  if (anchor && doc.section_resolved === false) {
481
- text3 = `_Section "${anchor}" not found \u2014 returning the full document._
586
+ text4 = `_Section "${anchor}" not found \u2014 returning the full document._
482
587
 
483
- ` + text3;
588
+ ` + text4;
484
589
  } else if (anchor && doc.heading) {
485
- text3 = `## ${doc.heading}
590
+ text4 = `## ${doc.heading}
486
591
 
487
592
  ${doc.content}`;
488
593
  }
489
594
  return {
490
- content: [{ type: "text", text: text3 }]
595
+ content: [{ type: "text", text: text4 }]
491
596
  };
492
597
  }
493
598
  const params = {};
@@ -511,7 +616,7 @@ ${doc.content}`;
511
616
  ]
512
617
  };
513
618
  }
514
- const text2 = items.map((d) => {
619
+ const text3 = items.map((d) => {
515
620
  const sections = (d.outline ?? []).slice(0, 8);
516
621
  const sectionHint = sections.length > 0 ? `
517
622
  sections: ${sections.map((s) => s.anchor).join(", ")}` : "";
@@ -525,7 +630,7 @@ ${doc.content}`;
525
630
  type: "text",
526
631
  text: `Found ${items.length} document(s) (${response.total} total):
527
632
 
528
- ${text2}
633
+ ${text3}
529
634
 
530
635
  Use doc_id for full content, or doc_id + section to fetch one section.`
531
636
  }
@@ -860,13 +965,13 @@ var SEVERITY_SCORES = {
860
965
  low: 0.25
861
966
  };
862
967
  var CHARS_PER_TOKEN = 4;
863
- function estimateTokens(text2) {
864
- return Math.ceil(text2.length / CHARS_PER_TOKEN);
968
+ function estimateTokens(text3) {
969
+ return Math.ceil(text3.length / CHARS_PER_TOKEN);
865
970
  }
866
- function truncateToTokens(text2, maxTokens) {
971
+ function truncateToTokens(text3, maxTokens) {
867
972
  const maxChars = Math.max(0, maxTokens) * CHARS_PER_TOKEN;
868
- if (text2.length <= maxChars) return { text: text2, truncated: false };
869
- const head = text2.slice(0, maxChars);
973
+ if (text3.length <= maxChars) return { text: text3, truncated: false };
974
+ const head = text3.slice(0, maxChars);
870
975
  const floor = Math.floor(maxChars * 0.75);
871
976
  const paragraph = head.lastIndexOf("\n\n");
872
977
  const line = head.lastIndexOf("\n");
@@ -911,7 +1016,7 @@ function scoreSeverity(item) {
911
1016
  if (!item.severity) return 0.5;
912
1017
  return SEVERITY_SCORES[item.severity] ?? 0.5;
913
1018
  }
914
- function extractKeywords(text2) {
1019
+ function extractKeywords(text3) {
915
1020
  const stopWords = /* @__PURE__ */ new Set([
916
1021
  "the",
917
1022
  "a",
@@ -996,7 +1101,7 @@ function extractKeywords(text2) {
996
1101
  "implement",
997
1102
  "create"
998
1103
  ]);
999
- return text2.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !stopWords.has(w));
1104
+ return text3.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !stopWords.has(w));
1000
1105
  }
1001
1106
  function scoreItems(items, taskDescription, filePaths) {
1002
1107
  const languages = detectLanguages(filePaths);
@@ -1039,20 +1144,20 @@ var MAX_TAIL = 10;
1039
1144
  var TAIL_SHARE = 0.15;
1040
1145
  var TRUNCATION_NOTICE_TOKENS = 30;
1041
1146
  function formatGuardrail(g) {
1042
- let text2 = `## ${g.title} [${g.severity}]
1147
+ let text3 = `## ${g.title} [${g.severity}]
1043
1148
 
1044
1149
  ${g.description}`;
1045
- if (g.pattern) text2 += `
1150
+ if (g.pattern) text3 += `
1046
1151
 
1047
1152
  **Bad:** \`\`\`
1048
1153
  ${g.pattern}
1049
1154
  \`\`\``;
1050
- if (g.fix) text2 += `
1155
+ if (g.fix) text3 += `
1051
1156
 
1052
1157
  **Good:** \`\`\`
1053
1158
  ${g.fix}
1054
1159
  \`\`\``;
1055
- return text2;
1160
+ return text3;
1056
1161
  }
1057
1162
  function formatSpecHeadline(s) {
1058
1163
  return `### ${s.name}
@@ -1074,19 +1179,19 @@ function formatRules(rules) {
1074
1179
  if (!byCategory.has(cat)) byCategory.set(cat, []);
1075
1180
  byCategory.get(cat).push(r);
1076
1181
  }
1077
- let text2 = "";
1182
+ let text3 = "";
1078
1183
  for (const [cat, catRules] of byCategory) {
1079
- text2 += `### ${cat.charAt(0).toUpperCase() + cat.slice(1)}
1184
+ text3 += `### ${cat.charAt(0).toUpperCase() + cat.slice(1)}
1080
1185
  `;
1081
1186
  for (const r of catRules) {
1082
- text2 += `- ${r.text}
1187
+ text3 += `- ${r.text}
1083
1188
  `;
1084
1189
  }
1085
- text2 += "\n";
1190
+ text3 += "\n";
1086
1191
  }
1087
1192
  return `## Project Rules (${rules.length})
1088
1193
 
1089
- ${text2.trim()}`;
1194
+ ${text3.trim()}`;
1090
1195
  }
1091
1196
  async function fetchSpecBody(api, draftId) {
1092
1197
  const spec = await api.get(
@@ -1423,13 +1528,13 @@ function registerGetContext(server2, apiClient2) {
1423
1528
  }
1424
1529
  },
1425
1530
  async ({ task_description, file_paths, max_tokens }) => {
1426
- const { text: text2 } = await buildContext(apiClient2, {
1531
+ const { text: text3 } = await buildContext(apiClient2, {
1427
1532
  taskDescription: task_description,
1428
1533
  filePaths: file_paths,
1429
1534
  maxTokens: max_tokens
1430
1535
  });
1431
1536
  return {
1432
- content: [{ type: "text", text: text2 }]
1537
+ content: [{ type: "text", text: text3 }]
1433
1538
  };
1434
1539
  }
1435
1540
  );
@@ -1677,9 +1782,9 @@ function registerGetProjectRules(server2, apiClient2) {
1677
1782
  );
1678
1783
  }
1679
1784
  if (filtered.length === 0) {
1680
- const text3 = rules.length === 0 ? `No project rules found. Add rules at ${configLink("/docs/rules", await workspaceBase(apiClient2))}` : `No project rules found${category ? ` in category "${category}"` : ""}.`;
1785
+ const text4 = rules.length === 0 ? `No project rules found. Add rules at ${configLink("/docs/rules", await workspaceBase(apiClient2))}` : `No project rules found${category ? ` in category "${category}"` : ""}.`;
1681
1786
  return {
1682
- content: [{ type: "text", text: text3 }]
1787
+ content: [{ type: "text", text: text4 }]
1683
1788
  };
1684
1789
  }
1685
1790
  const grouped = /* @__PURE__ */ new Map();
@@ -1688,25 +1793,25 @@ function registerGetProjectRules(server2, apiClient2) {
1688
1793
  if (!grouped.has(cat)) grouped.set(cat, []);
1689
1794
  grouped.get(cat).push(rule);
1690
1795
  }
1691
- let text2 = `Found ${filtered.length} project rule(s):
1796
+ let text3 = `Found ${filtered.length} project rule(s):
1692
1797
 
1693
1798
  `;
1694
1799
  for (const [cat, catRules] of grouped) {
1695
1800
  const label = CATEGORY_LABELS[cat] ?? cat;
1696
- text2 += `## ${label}
1801
+ text3 += `## ${label}
1697
1802
 
1698
1803
  `;
1699
1804
  for (const rule of catRules) {
1700
- text2 += `- ${rule.text}
1805
+ text3 += `- ${rule.text}
1701
1806
  `;
1702
1807
  }
1703
- text2 += "\n";
1808
+ text3 += "\n";
1704
1809
  }
1705
1810
  return {
1706
1811
  content: [
1707
1812
  {
1708
1813
  type: "text",
1709
- text: text2
1814
+ text: text3
1710
1815
  }
1711
1816
  ]
1712
1817
  };
@@ -1732,10 +1837,10 @@ function registerCreateProjectRule(server2, apiClient2) {
1732
1837
  readOnlyHint: false
1733
1838
  }
1734
1839
  },
1735
- async ({ text: text2, category }) => {
1840
+ async ({ text: text3, category }) => {
1736
1841
  const saved = await apiClient2.post(
1737
1842
  "/v1/standards/rules",
1738
- { text: text2, category }
1843
+ { text: text3, category }
1739
1844
  );
1740
1845
  apiClient2.invalidateCache("/v1/standards/rules");
1741
1846
  return {
@@ -1773,7 +1878,7 @@ function registerGetPromptTemplates(server2, apiClient2) {
1773
1878
  ]
1774
1879
  };
1775
1880
  }
1776
- const text2 = templates.map(
1881
+ const text3 = templates.map(
1777
1882
  (t) => `## ${t.name}
1778
1883
 
1779
1884
  ` + (t.description ? `*${t.description}*
@@ -1788,7 +1893,7 @@ ${t.prompt}
1788
1893
  type: "text",
1789
1894
  text: `Found ${templates.length} prompt template(s):
1790
1895
 
1791
- ${text2}`
1896
+ ${text3}`
1792
1897
  }
1793
1898
  ]
1794
1899
  };
@@ -1896,18 +2001,9 @@ async function walk(root, { skipHidden }, dir = root, out = { files: [], links:
1896
2001
  out.files.sort();
1897
2002
  return out;
1898
2003
  }
1899
- async function readSkillDirectory(directory) {
1900
- const root = resolve(directory);
1901
- if (!existsSync(join(root, ENTRY_FILE))) {
1902
- throw new Error(`${root} has no ${ENTRY_FILE}: a skill folder holds SKILL.md at its root`);
1903
- }
1904
- const walked = await walk(root, { skipHidden: true });
1905
- if (walked.links.length) {
1906
- throw new Error(`${walked.links.join(", ")}: links are not followed; copy the file into the skill folder`);
1907
- }
1908
- const paths = walked.files;
2004
+ async function readTextFiles(root, paths, what) {
1909
2005
  if (paths.length > MAX_FILES) {
1910
- throw new Error(`${root} holds ${paths.length} files: a skill holds at most ${MAX_FILES}`);
2006
+ throw new Error(`${root} holds ${paths.length} files: ${what} holds at most ${MAX_FILES}`);
1911
2007
  }
1912
2008
  const decoder = new TextDecoder("utf-8", { fatal: true });
1913
2009
  const files = [];
@@ -1917,7 +2013,7 @@ async function readSkillDirectory(directory) {
1917
2013
  throw new Error(`${path}: file names may use letters, digits, ".", "_" and "-" only`);
1918
2014
  }
1919
2015
  const raw = await readFile(join(root, ...path.split("/")));
1920
- if (raw.includes(0)) throw new Error(`${path} is binary: a skill holds text files only`);
2016
+ if (raw.includes(0)) throw new Error(`${path} is binary: ${what} holds text files only`);
1921
2017
  if (raw.length > MAX_FILE_BYTES) {
1922
2018
  throw new Error(`${path} is ${raw.length} bytes: a file holds at most ${MAX_FILE_BYTES}`);
1923
2019
  }
@@ -1933,6 +2029,18 @@ async function readSkillDirectory(directory) {
1933
2029
  }
1934
2030
  files.push({ path, content });
1935
2031
  }
2032
+ return files;
2033
+ }
2034
+ async function readSkillDirectory(directory) {
2035
+ const root = resolve(directory);
2036
+ if (!existsSync(join(root, ENTRY_FILE))) {
2037
+ throw new Error(`${root} has no ${ENTRY_FILE}: a skill folder holds SKILL.md at its root`);
2038
+ }
2039
+ const walked = await walk(root, { skipHidden: true });
2040
+ if (walked.links.length) {
2041
+ throw new Error(`${walked.links.join(", ")}: links are not followed; copy the file into the skill folder`);
2042
+ }
2043
+ const files = await readTextFiles(root, walked.files, "a skill");
1936
2044
  const skillMd = files.find((f) => f.path === ENTRY_FILE);
1937
2045
  const name = frontmatterName(skillMd.content);
1938
2046
  if (!name) throw new Error(`${ENTRY_FILE} has no frontmatter name`);
@@ -2014,8 +2122,8 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
2014
2122
  keptEdited
2015
2123
  };
2016
2124
  }
2017
- function fenceFor(text2) {
2018
- const longest = Math.max(0, ...(text2.match(/`+/g) ?? []).map((r) => r.length));
2125
+ function fenceFor(text3) {
2126
+ const longest = Math.max(0, ...(text3.match(/`+/g) ?? []).map((r) => r.length));
2019
2127
  return "`".repeat(Math.max(3, longest + 1));
2020
2128
  }
2021
2129
  function describeInstall(name, r) {
@@ -2171,8 +2279,186 @@ ${body}`
2171
2279
  );
2172
2280
  }
2173
2281
 
2282
+ // src/tools/team-setup.ts
2283
+ import { z as z14 } from "zod";
2284
+ import { existsSync as existsSync2 } from "fs";
2285
+ import { lstat as lstat2 } from "fs/promises";
2286
+ import { homedir as homedir3 } from "os";
2287
+ import { join as join2, resolve as resolve3 } from "path";
2288
+
2289
+ // src/lib/person-values.ts
2290
+ var W = "[\\p{L}\\p{N}_]";
2291
+ var D = "\\p{Nd}";
2292
+ var WORD_OR = (extra) => `[\\p{L}\\p{N}_${extra}]`;
2293
+ var PERSON_ID = "(?:con|ctc|usr)_[0-9A-Za-z]{10,40}";
2294
+ var PERSON_TOKEN = `\`${PERSON_ID}\`(?:[ \\t]*\\([^()\\n]*\\))?`;
2295
+ var BEFORE = `(?<!${WORD_OR("+.:/\\\\\\-")})(?<!${D}[ -])`;
2296
+ var AFTER = `(?!${WORD_OR(":/\\\\")}|\\.${D}|[ -]?${D}|-${W})`;
2297
+ var E164 = `${BEFORE}\\+${D}+(?:[ -]${D}+)*${AFTER}`;
2298
+ var NATIONAL = `${BEFORE}0${D}*(?:[ -]${D}+)*${AFTER}`;
2299
+ var ATEXT = "[A-Za-z0-9!#$%&'*+/=?^_{|}~\\-]";
2300
+ 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])";
2301
+ var HOME_PREFIX = "(?:[A-Za-z]:\\\\+Users\\\\+|[A-Za-z]:/+Users/+|/Users/|/home/)";
2302
+ var HOME = `(?<!${WORD_OR(".~:/\\\\$}\\-")})${HOME_PREFIX}[^\\\\/\\s"'\`<>|:*?]+`;
2303
+ 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,}`;
2304
+
2305
+ // src/lib/team-setup-types.ts
2306
+ var PARTS = ["hooks", "mcp", "agents", "commands", "instructions"];
2307
+ var PERSON_ID_RE = new RegExp(`^${PERSON_ID}$`);
2308
+
2309
+ // src/tools/team-setup.ts
2310
+ var LAYOUT = {
2311
+ hooks: { files: ["hooks/hooks.json"], folders: [{ dir: "scripts", md: false }], required: "hooks/hooks.json" },
2312
+ mcp: { files: [".mcp.json"], folders: [], required: ".mcp.json" },
2313
+ agents: { files: [], folders: [{ dir: "agents", md: true }] },
2314
+ commands: { files: [], folders: [{ dir: "commands", md: true }] },
2315
+ instructions: { files: ["CLAUDE.md"], folders: [], required: "CLAUDE.md" }
2316
+ };
2317
+ function expandHome2(path) {
2318
+ return path.replace(/^~(?=$|[\\/])/, homedir3());
2319
+ }
2320
+ function text2(t) {
2321
+ return { content: [{ type: "text", text: t }] };
2322
+ }
2323
+ async function isLink(path) {
2324
+ try {
2325
+ return (await lstat2(path)).isSymbolicLink();
2326
+ } catch (e) {
2327
+ if (e.code === "ENOENT") return false;
2328
+ throw e;
2329
+ }
2330
+ }
2331
+ async function readPartDirectory(part, directory) {
2332
+ const root = resolve3(directory);
2333
+ const layout = LAYOUT[part];
2334
+ const paths = [];
2335
+ const leftOut = [];
2336
+ const links = [];
2337
+ for (const path of layout.files) {
2338
+ const segments = path.split("/");
2339
+ let present = true;
2340
+ for (let i = 1; i <= segments.length; i++) {
2341
+ const rel = segments.slice(0, i).join("/");
2342
+ if (await isLink(join2(root, ...segments.slice(0, i)))) {
2343
+ links.push(rel);
2344
+ present = false;
2345
+ break;
2346
+ }
2347
+ }
2348
+ if (present && existsSync2(join2(root, ...segments))) paths.push(path);
2349
+ }
2350
+ for (const { dir, md } of layout.folders) {
2351
+ const full = join2(root, dir);
2352
+ if (await isLink(full)) {
2353
+ links.push(dir);
2354
+ continue;
2355
+ }
2356
+ if (!existsSync2(full)) continue;
2357
+ const walked = await walk(full, { skipHidden: true });
2358
+ links.push(...walked.links.map((p) => `${dir}/${p}`));
2359
+ leftOut.push(...walked.hidden.map((p) => `${dir}/${p}`));
2360
+ for (const p of walked.files) {
2361
+ if (md && !p.endsWith(".md")) leftOut.push(`${dir}/${p}`);
2362
+ else paths.push(`${dir}/${p}`);
2363
+ }
2364
+ }
2365
+ if (links.length) {
2366
+ throw new Error(
2367
+ `${links.join(", ")}: links are not followed; copy the file into the folder. Nothing was sent.`
2368
+ );
2369
+ }
2370
+ if (layout.required && !paths.includes(layout.required)) {
2371
+ throw new Error(`${root} has no ${layout.required}: the ${part} part needs it. Nothing was sent.`);
2372
+ }
2373
+ if (!paths.length) {
2374
+ throw new Error(
2375
+ `${root} has no ${layout.folders.map((f) => `${f.dir}/*.md`).join(" or ")}: nothing to publish for ${part}.`
2376
+ );
2377
+ }
2378
+ return { files: await readTextFiles(root, paths.sort(), `the ${part} part`), leftOut };
2379
+ }
2380
+ function describePerson(id, person) {
2381
+ const lines = [`**${person.label ?? "(no label)"}**, \`${id}\``];
2382
+ if (person.mergedInto) {
2383
+ lines.push(`\`${id}\` was merged into \`${person.mergedInto}\`: these are \`${person.mergedInto}\`'s details.`);
2384
+ }
2385
+ lines.push(`- Name: ${person.name ?? "(none held)"}`);
2386
+ lines.push(`- Phone number: ${person.phone ?? "(none held)"}`);
2387
+ lines.push(`- Email address: ${person.email ?? "(none held)"}`);
2388
+ const token = `\`${person.mergedInto ?? id}\`${person.label ? ` (${person.label})` : ""}`;
2389
+ lines.push(
2390
+ "",
2391
+ `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.`
2392
+ );
2393
+ return lines.join("\n");
2394
+ }
2395
+ function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
2396
+ const fileSchema = z14.object({
2397
+ path: z14.string().describe(`Path inside the part's folder, forward slashes, e.g. "hooks/hooks.json"`),
2398
+ content: z14.string().describe("The file's full text, exactly as it is")
2399
+ });
2400
+ server2.registerTool(
2401
+ "publish_setup_part",
2402
+ {
2403
+ title: "Publish Setup Part",
2404
+ 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."),
2405
+ inputSchema: {
2406
+ part: z14.enum(PARTS).describe("Which part of the setup"),
2407
+ ...localFiles ? {
2408
+ directory: z14.string().optional().describe(
2409
+ "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/"
2410
+ )
2411
+ } : {},
2412
+ files: z14.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the part")
2413
+ },
2414
+ annotations: { readOnlyHint: false }
2415
+ },
2416
+ async (args) => {
2417
+ let files;
2418
+ let leftOut = [];
2419
+ if (localFiles && args.directory) {
2420
+ ({ files, leftOut } = await readPartDirectory(args.part, expandHome2(args.directory)));
2421
+ } else if (args.files?.length) {
2422
+ files = args.files;
2423
+ } else {
2424
+ throw new Error(localFiles ? "Pass `directory` (or `files`)." : "Pass `files`.");
2425
+ }
2426
+ const { workspace } = await apiClient2.getOwnTeamSetup();
2427
+ const saved = await apiClient2.publishPartVersion(workspace, args.part, files);
2428
+ const lines = [`Published **${saved.part}** version ${saved.version}.`];
2429
+ if (saved.part !== "instructions" && saved.pluginVersion !== void 0) {
2430
+ lines.push(`Team plugin version ${saved.pluginVersion}; each member accepts it at their next update.`);
2431
+ }
2432
+ if (saved.replacedValues?.length) {
2433
+ lines.push(
2434
+ "Replaced by a reference to each member's own environment variable:",
2435
+ ...saved.replacedValues.map((r) => `- ${r.server} ${r.field} ${r.name}: \${${r.name}}`)
2436
+ );
2437
+ }
2438
+ if (leftOut.length) lines.push(`Left out (not part of ${args.part}): ${leftOut.join(", ")}`);
2439
+ return text2(lines.join("\n"));
2440
+ }
2441
+ );
2442
+ server2.registerTool(
2443
+ "resolve_person",
2444
+ {
2445
+ title: "Resolve Person",
2446
+ 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.",
2447
+ inputSchema: {
2448
+ 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")
2449
+ },
2450
+ annotations: { readOnlyHint: true }
2451
+ },
2452
+ async (args) => {
2453
+ const { workspace } = await apiClient2.getOwnTeamSetup();
2454
+ const person = await apiClient2.resolvePerson(workspace, args.id);
2455
+ return text2(describePerson(args.id, person));
2456
+ }
2457
+ );
2458
+ }
2459
+
2174
2460
  // src/server.ts
2175
- var SERVER_VERSION = true ? "0.1.45" : "0.0.0-dev";
2461
+ var SERVER_VERSION = true ? "0.1.46" : "0.0.0-dev";
2176
2462
  function createServer(apiClient2, { localFiles = false } = {}) {
2177
2463
  const server2 = new McpServer({
2178
2464
  name: "systemdox-mcp-server",
@@ -2193,6 +2479,7 @@ function createServer(apiClient2, { localFiles = false } = {}) {
2193
2479
  registerCreateProjectRule(server2, apiClient2);
2194
2480
  registerCreatePromptTemplate(server2, apiClient2);
2195
2481
  registerSkillTools(server2, apiClient2, { localFiles });
2482
+ registerTeamSetupTools(server2, apiClient2, { localFiles });
2196
2483
  return server2;
2197
2484
  }
2198
2485