@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/cli.js CHANGED
@@ -51,6 +51,146 @@ var init_config = __esm({
51
51
  }
52
52
  });
53
53
 
54
+ // src/lib/team-setup-files.ts
55
+ import { execFile } from "child_process";
56
+ import { chmod, mkdir, readFile, rename, rm, writeFile } from "fs/promises";
57
+ import { homedir, userInfo } from "os";
58
+ import { dirname, join } from "path";
59
+ import { promisify } from "util";
60
+ function isWorkspace(value) {
61
+ return typeof value === "string" && value.length <= 64 && SLUG_RE.test(value);
62
+ }
63
+ function homeDir() {
64
+ return homedir();
65
+ }
66
+ function systemdoxDir() {
67
+ return join(homeDir(), ".claude", "systemdox");
68
+ }
69
+ function credentialsPath() {
70
+ return join(systemdoxDir(), "credentials.json");
71
+ }
72
+ function workspaceDir(workspace) {
73
+ if (!isWorkspace(workspace)) throw new Error(`${JSON.stringify(workspace)} is not a workspace name`);
74
+ return join(systemdoxDir(), workspace);
75
+ }
76
+ function statePath(workspace) {
77
+ return join(workspaceDir(workspace), "state.json");
78
+ }
79
+ function pluginRoot(workspace) {
80
+ return join(workspaceDir(workspace), "plugin");
81
+ }
82
+ function pluginDir(workspace, version) {
83
+ return join(pluginRoot(workspace), String(version));
84
+ }
85
+ async function readJson(path, empty) {
86
+ let text3;
87
+ try {
88
+ text3 = await readFile(path, "utf8");
89
+ } catch (e) {
90
+ if (e.code === "ENOENT") return empty;
91
+ throw e;
92
+ }
93
+ try {
94
+ return JSON.parse(text3);
95
+ } catch {
96
+ throw new Error(`${path} is not valid JSON: fix or remove it, then run this again`);
97
+ }
98
+ }
99
+ async function writeJsonAtomically(path, value, mode) {
100
+ await mkdir(dirname(path), { recursive: true });
101
+ const tmp = `${path}.${process.pid}.tmp`;
102
+ await writeFile(tmp, JSON.stringify(value, null, 2) + "\n", mode === void 0 ? "utf8" : { encoding: "utf8", mode });
103
+ await rename(tmp, path);
104
+ }
105
+ async function readCredentials() {
106
+ return readJson(credentialsPath(), {});
107
+ }
108
+ async function apiKeyFor(workspace, env = process.env) {
109
+ if (env.SYSTEMDOX_API_KEY) return env.SYSTEMDOX_API_KEY;
110
+ const entry = (await readCredentials())[workspace];
111
+ return typeof entry?.apiKey === "string" && entry.apiKey ? entry.apiKey : void 0;
112
+ }
113
+ function apiUrlFrom(env = process.env) {
114
+ return requireHttpsApiUrl(env.SYSTEMDOX_API_URL || API_URL_DEFAULT);
115
+ }
116
+ async function readState(workspace) {
117
+ return readJson(statePath(workspace), {});
118
+ }
119
+ async function writeState(workspace, state) {
120
+ await writeJsonAtomically(statePath(workspace), state);
121
+ }
122
+ async function updateState(workspace, change) {
123
+ const state = await readState(workspace);
124
+ change(state);
125
+ await writeState(workspace, state);
126
+ return state;
127
+ }
128
+ async function removeFolder(path) {
129
+ await rm(path, { recursive: true, force: true });
130
+ }
131
+ function escapeRegExp(s) {
132
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
133
+ }
134
+ function marketplaceWorkspace(marketplaceUrl, apiUrl) {
135
+ let url;
136
+ let api;
137
+ try {
138
+ url = new URL(marketplaceUrl);
139
+ api = new URL(requireHttpsApiUrl(apiUrl));
140
+ } catch {
141
+ return void 0;
142
+ }
143
+ if (url.protocol !== "https:" || url.origin !== api.origin || url.username || url.password) return void 0;
144
+ const base = api.pathname.replace(/\/+$/, "");
145
+ const m = new RegExp(`^${escapeRegExp(base)}/v1/team-setup/([^/]+)/marketplace\\.json$`).exec(url.pathname);
146
+ return m && isWorkspace(m[1]) ? m[1] : void 0;
147
+ }
148
+ async function runMarketplaceAuth(io, env = process.env) {
149
+ const url = env.CLAUDE_CODE_MARKETPLACE_URL;
150
+ if (!url) {
151
+ io.err("marketplace-auth: CLAUDE_CODE_MARKETPLACE_URL is not set; Claude Code sets it when it fetches a marketplace.");
152
+ return 1;
153
+ }
154
+ let apiUrl;
155
+ try {
156
+ apiUrl = apiUrlFrom(env);
157
+ } catch (e) {
158
+ io.err(`marketplace-auth: ${e.message}`);
159
+ return 1;
160
+ }
161
+ const workspace = marketplaceWorkspace(url, apiUrl);
162
+ if (!workspace) {
163
+ io.err(
164
+ `marketplace-auth: refusing to give a SystemDox key for ${url}: it is not a SystemDox team marketplace on ${apiUrl}`
165
+ );
166
+ return 1;
167
+ }
168
+ let key;
169
+ try {
170
+ key = await apiKeyFor(workspace, env);
171
+ } catch (e) {
172
+ io.err(`marketplace-auth: ${e.message}`);
173
+ return 1;
174
+ }
175
+ if (!key) {
176
+ io.err(
177
+ `marketplace-auth: no SystemDox key for ${workspace}. Set SYSTEMDOX_API_KEY, or run npx -y @systemdox/mcp-server setup.`
178
+ );
179
+ return 1;
180
+ }
181
+ io.out(JSON.stringify({ Authorization: `Bearer ${key}` }));
182
+ return 0;
183
+ }
184
+ var SLUG_RE, run;
185
+ var init_team_setup_files = __esm({
186
+ "src/lib/team-setup-files.ts"() {
187
+ "use strict";
188
+ init_config();
189
+ SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
190
+ run = promisify(execFile);
191
+ }
192
+ });
193
+
54
194
  // src/lib/cache.ts
55
195
  var ResponseCache;
56
196
  var init_cache = __esm({
@@ -88,11 +228,33 @@ var init_cache = __esm({
88
228
  });
89
229
 
90
230
  // src/lib/api-client.ts
91
- var SystemDoxApiClient;
231
+ var SystemDoxApiError, TEAM_SETUP, seg, SystemDoxApiClient;
92
232
  var init_api_client = __esm({
93
233
  "src/lib/api-client.ts"() {
94
234
  "use strict";
95
235
  init_cache();
236
+ SystemDoxApiError = class extends Error {
237
+ constructor(status, text3) {
238
+ super(`SystemDox API error ${status}: ${text3}`);
239
+ this.status = status;
240
+ this.text = text3;
241
+ let body;
242
+ try {
243
+ body = JSON.parse(text3);
244
+ } catch {
245
+ body = void 0;
246
+ }
247
+ this.body = body;
248
+ const code = body?.code;
249
+ this.code = typeof code === "string" ? code : /\b(SYD-[A-Z]\d{3})\b/.exec(text3)?.[1];
250
+ }
251
+ status;
252
+ text;
253
+ code;
254
+ body;
255
+ };
256
+ TEAM_SETUP = "/v1/team-setup";
257
+ seg = (s) => encodeURIComponent(String(s));
96
258
  SystemDoxApiClient = class {
97
259
  config;
98
260
  cache;
@@ -114,10 +276,7 @@ var init_api_client = __esm({
114
276
  const cached = this.cache.get(cacheKey);
115
277
  if (cached !== void 0) return cached;
116
278
  const res = await this.fetchRaw(path, params);
117
- if (!res.ok) {
118
- const body = await res.text();
119
- throw new Error(`SystemDox API error ${res.status}: ${body}`);
120
- }
279
+ if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
121
280
  const data = await res.json();
122
281
  this.cache.set(cacheKey, data);
123
282
  return data;
@@ -126,10 +285,7 @@ var init_api_client = __esm({
126
285
  * latest now, not the one cached before someone published another. */
127
286
  async getFresh(path) {
128
287
  const res = await this.fetchRaw(path);
129
- if (!res.ok) {
130
- const body = await res.text();
131
- throw new Error(`SystemDox API error ${res.status}: ${body}`);
132
- }
288
+ if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
133
289
  return await res.json();
134
290
  }
135
291
  async post(path, body) {
@@ -151,12 +307,101 @@ var init_api_client = __esm({
151
307
  },
152
308
  body: JSON.stringify(body)
153
309
  });
154
- if (!res.ok) {
155
- const text2 = await res.text();
156
- throw new Error(`SystemDox API error ${res.status}: ${text2}`);
157
- }
310
+ if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
158
311
  return await res.json();
159
312
  }
313
+ // ── Team setup (ADR-217, specs/changes/team-setup/openapi.yaml) ──────────
314
+ // One method per operationId. Every read skips the cache: setup, sync and
315
+ // plugin-path act on what the workspace holds now.
316
+ /** getOwnTeamSetup: the key's own workspace, named in `workspace`. */
317
+ getOwnTeamSetup() {
318
+ return this.getFresh(TEAM_SETUP);
319
+ }
320
+ getTeamSetup(workspace) {
321
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}`);
322
+ }
323
+ getMarketplace(workspace) {
324
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/marketplace.json`);
325
+ }
326
+ getPart(workspace, part) {
327
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/parts/${seg(part)}`);
328
+ }
329
+ publishPartVersion(workspace, part, files) {
330
+ return this.post(`${TEAM_SETUP}/${seg(workspace)}/parts/${seg(part)}/versions`, { files });
331
+ }
332
+ getPartVersion(workspace, part, version) {
333
+ return this.getFresh(
334
+ `${TEAM_SETUP}/${seg(workspace)}/parts/${seg(part)}/versions/${seg(version)}`
335
+ );
336
+ }
337
+ getSetupSkills(workspace) {
338
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/skills`);
339
+ }
340
+ setSetupSkills(workspace, skills) {
341
+ return this.put(`${TEAM_SETUP}/${seg(workspace)}/skills`, { skills });
342
+ }
343
+ getPlugin(workspace) {
344
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/plugin`);
345
+ }
346
+ /** getPluginZip: the zip's bytes as served, and its X-Content-SHA256
347
+ * header. The caller checks the BYTES against the hash it was given. */
348
+ async getPluginZip(workspace, version) {
349
+ const res = await fetch(
350
+ this.buildUrl(`${TEAM_SETUP}/${seg(workspace)}/plugin/versions/${seg(version)}/zip`),
351
+ {
352
+ headers: {
353
+ Authorization: `Bearer ${this.config.apiKey}`,
354
+ "X-API-Key": this.config.apiKey,
355
+ Accept: "application/zip"
356
+ }
357
+ }
358
+ );
359
+ if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
360
+ return { bytes: Buffer.from(await res.arrayBuffer()), sha256: res.headers.get("x-content-sha256") };
361
+ }
362
+ listMemoryProjects(workspace) {
363
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/memory`);
364
+ }
365
+ getMemory(workspace, project) {
366
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}`);
367
+ }
368
+ /** pushMemoryVersion: a stale base throws SystemDoxApiError with status 409,
369
+ * code SYD-K010 and `body.currentVersion`. */
370
+ pushMemoryVersion(workspace, project, body) {
371
+ return this.post(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/versions`, body);
372
+ }
373
+ getMemoryVersion(workspace, project, version) {
374
+ return this.getFresh(
375
+ `${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/versions/${seg(version)}`
376
+ );
377
+ }
378
+ findMissingBlobs(workspace, project, hashes) {
379
+ return this.post(
380
+ `${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/blobs/missing`,
381
+ { hashes }
382
+ );
383
+ }
384
+ uploadBlobs(workspace, project, blobs) {
385
+ return this.post(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/blobs`, { blobs });
386
+ }
387
+ readBlobs(workspace, project, hashes) {
388
+ return this.post(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/blobs/read`, { hashes });
389
+ }
390
+ listValueAnswers(workspace) {
391
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/values`);
392
+ }
393
+ lookupValues(workspace, values) {
394
+ return this.post(`${TEAM_SETUP}/${seg(workspace)}/values`, { values });
395
+ }
396
+ answerValues(workspace, answers) {
397
+ return this.put(`${TEAM_SETUP}/${seg(workspace)}/values`, { answers });
398
+ }
399
+ fileMemoryPeople(workspace, people) {
400
+ return this.post(`${TEAM_SETUP}/${seg(workspace)}/people`, { people });
401
+ }
402
+ resolvePerson(workspace, personId) {
403
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/people/${seg(personId)}`);
404
+ }
160
405
  invalidateCache(path) {
161
406
  if (path) {
162
407
  this.cache.invalidate(this.buildUrl(path).toString());
@@ -190,10 +435,10 @@ var init_api_client = __esm({
190
435
 
191
436
  // src/lib/skills.ts
192
437
  import { createHash } from "crypto";
193
- import { lstat, mkdir, readdir, readFile, rm, writeFile } from "fs/promises";
438
+ import { lstat, mkdir as mkdir2, readdir, readFile as readFile2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
194
439
  import { existsSync } from "fs";
195
- import { homedir } from "os";
196
- import { dirname, join, relative, resolve, sep } from "path";
440
+ import { homedir as homedir2 } from "os";
441
+ import { dirname as dirname2, join as join2, relative, resolve, sep } from "path";
197
442
  function sha256(content) {
198
443
  return createHash("sha256").update(content, "utf8").digest("hex");
199
444
  }
@@ -210,12 +455,12 @@ function frontmatterName(skillMd) {
210
455
  return line ? line[1].replace(/^(['"])(.*)\1$/, "$2") : void 0;
211
456
  }
212
457
  function defaultSkillDir(name) {
213
- return join(homedir(), ".claude", "skills", name);
458
+ return join2(homedir2(), ".claude", "skills", name);
214
459
  }
215
460
  async function walk(root, { skipHidden }, dir = root, out = { files: [], links: [], hidden: [] }) {
216
461
  for (const entry of await readdir(dir, { withFileTypes: true })) {
217
462
  if (SKIPPED.has(entry.name) || entry.name.endsWith(".pyc")) continue;
218
- const full = join(dir, entry.name);
463
+ const full = join2(dir, entry.name);
219
464
  const rel = relative(root, full).split(sep).join("/");
220
465
  if (entry.isSymbolicLink()) out.links.push(rel);
221
466
  else if (skipHidden && entry.name.startsWith(".")) out.hidden.push(rel);
@@ -225,18 +470,9 @@ async function walk(root, { skipHidden }, dir = root, out = { files: [], links:
225
470
  out.files.sort();
226
471
  return out;
227
472
  }
228
- async function readSkillDirectory(directory) {
229
- const root = resolve(directory);
230
- if (!existsSync(join(root, ENTRY_FILE))) {
231
- throw new Error(`${root} has no ${ENTRY_FILE}: a skill folder holds SKILL.md at its root`);
232
- }
233
- const walked = await walk(root, { skipHidden: true });
234
- if (walked.links.length) {
235
- throw new Error(`${walked.links.join(", ")}: links are not followed; copy the file into the skill folder`);
236
- }
237
- const paths = walked.files;
473
+ async function readTextFiles(root, paths, what) {
238
474
  if (paths.length > MAX_FILES) {
239
- throw new Error(`${root} holds ${paths.length} files: a skill holds at most ${MAX_FILES}`);
475
+ throw new Error(`${root} holds ${paths.length} files: ${what} holds at most ${MAX_FILES}`);
240
476
  }
241
477
  const decoder = new TextDecoder("utf-8", { fatal: true });
242
478
  const files = [];
@@ -245,8 +481,8 @@ async function readSkillDirectory(directory) {
245
481
  if (!isValidPath(path)) {
246
482
  throw new Error(`${path}: file names may use letters, digits, ".", "_" and "-" only`);
247
483
  }
248
- const raw = await readFile(join(root, ...path.split("/")));
249
- if (raw.includes(0)) throw new Error(`${path} is binary: a skill holds text files only`);
484
+ const raw = await readFile2(join2(root, ...path.split("/")));
485
+ if (raw.includes(0)) throw new Error(`${path} is binary: ${what} holds text files only`);
250
486
  if (raw.length > MAX_FILE_BYTES) {
251
487
  throw new Error(`${path} is ${raw.length} bytes: a file holds at most ${MAX_FILE_BYTES}`);
252
488
  }
@@ -262,6 +498,18 @@ async function readSkillDirectory(directory) {
262
498
  }
263
499
  files.push({ path, content });
264
500
  }
501
+ return files;
502
+ }
503
+ async function readSkillDirectory(directory) {
504
+ const root = resolve(directory);
505
+ if (!existsSync(join2(root, ENTRY_FILE))) {
506
+ throw new Error(`${root} has no ${ENTRY_FILE}: a skill folder holds SKILL.md at its root`);
507
+ }
508
+ const walked = await walk(root, { skipHidden: true });
509
+ if (walked.links.length) {
510
+ throw new Error(`${walked.links.join(", ")}: links are not followed; copy the file into the skill folder`);
511
+ }
512
+ const files = await readTextFiles(root, walked.files, "a skill");
265
513
  const skillMd = files.find((f) => f.path === ENTRY_FILE);
266
514
  const name = frontmatterName(skillMd.content);
267
515
  if (!name) throw new Error(`${ENTRY_FILE} has no frontmatter name`);
@@ -275,7 +523,7 @@ function within(dest, path) {
275
523
  }
276
524
  async function readManifest(dest) {
277
525
  try {
278
- return JSON.parse(await readFile(join(dest, MANIFEST_FILE), "utf8"));
526
+ return JSON.parse(await readFile2(join2(dest, MANIFEST_FILE), "utf8"));
279
527
  } catch {
280
528
  return void 0;
281
529
  }
@@ -299,7 +547,7 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
299
547
  const edited = [];
300
548
  const untracked = [];
301
549
  for (const path of present) {
302
- const content = await readFile(join(dest, ...path.split("/")), "utf8");
550
+ const content = await readFile2(join2(dest, ...path.split("/")), "utf8");
303
551
  if (Object.hasOwn(known, path)) {
304
552
  if (sha256(content) !== known[path]) edited.push(path);
305
553
  } else if (!incoming.has(path) || sha256(content) !== incoming.get(path).sha256) {
@@ -312,8 +560,8 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
312
560
  const written = [];
313
561
  for (const f of version.files) {
314
562
  const full = within(dest, f.path);
315
- await mkdir(dirname(full), { recursive: true });
316
- await writeFile(full, f.content, "utf8");
563
+ await mkdir2(dirname2(full), { recursive: true });
564
+ await writeFile2(full, f.content, "utf8");
317
565
  written.push(f.path);
318
566
  }
319
567
  const removed = [];
@@ -324,7 +572,7 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
324
572
  keptEdited.push(path);
325
573
  continue;
326
574
  }
327
- await rm(within(dest, path));
575
+ await rm2(within(dest, path));
328
576
  removed.push(path);
329
577
  }
330
578
  const next = {
@@ -333,7 +581,7 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
333
581
  installedAt: (/* @__PURE__ */ new Date()).toISOString(),
334
582
  files: Object.fromEntries(version.files.map((f) => [f.path, f.sha256]))
335
583
  };
336
- await writeFile(join(dest, MANIFEST_FILE), JSON.stringify(next, null, 2) + "\n", "utf8");
584
+ await writeFile2(join2(dest, MANIFEST_FILE), JSON.stringify(next, null, 2) + "\n", "utf8");
337
585
  return {
338
586
  dest,
339
587
  version: version.version,
@@ -343,8 +591,8 @@ async function installSkill(version, dest, { overwrite = false } = {}) {
343
591
  keptEdited
344
592
  };
345
593
  }
346
- function fenceFor(text2) {
347
- const longest = Math.max(0, ...(text2.match(/`+/g) ?? []).map((r) => r.length));
594
+ function fenceFor(text3) {
595
+ const longest = Math.max(0, ...(text3.match(/`+/g) ?? []).map((r) => r.length));
348
596
  return "`".repeat(Math.max(3, longest + 1));
349
597
  }
350
598
  function describeInstall(name, r) {
@@ -469,6 +717,226 @@ var init_install_skill = __esm({
469
717
  }
470
718
  });
471
719
 
720
+ // src/plugin-path.ts
721
+ var plugin_path_exports = {};
722
+ __export(plugin_path_exports, {
723
+ MAX_UNPACKED_BYTES: () => MAX_UNPACKED_BYTES,
724
+ PLUGIN_PATH_USAGE: () => PLUGIN_PATH_USAGE,
725
+ PluginRefused: () => PluginRefused,
726
+ parsePluginPathArgs: () => parsePluginPathArgs,
727
+ readZipEntries: () => readZipEntries,
728
+ runPluginPath: () => runPluginPath,
729
+ unpackZip: () => unpackZip
730
+ });
731
+ import { createHash as createHash2, randomBytes } from "crypto";
732
+ import { existsSync as existsSync2 } from "fs";
733
+ import { mkdir as mkdir3, rename as rename2, writeFile as writeFile3 } from "fs/promises";
734
+ import { dirname as dirname3, join as join3, resolve as resolve3, sep as sep2 } from "path";
735
+ import { inflateRawSync } from "zlib";
736
+ function parsePluginPathArgs(argv) {
737
+ const args2 = {};
738
+ for (let i = 0; i < argv.length; i++) {
739
+ const a = argv[i];
740
+ if (a === "--version" || a === "--sha256") {
741
+ const value = argv[++i];
742
+ if (!value) throw new Error(`${a} needs a value
743
+ ${PLUGIN_PATH_USAGE}`);
744
+ if (a === "--sha256") {
745
+ if (!SHA256_RE.test(value)) throw new Error(`--sha256 must be 64 lowercase hex characters, got ${value}`);
746
+ args2.sha256 = value;
747
+ } else if (/^[1-9]\d{0,8}$/.test(value)) args2.version = Number(value);
748
+ else throw new Error(`--version must be a whole number, got ${value}`);
749
+ } else if (a.startsWith("-")) throw new Error(`Unknown option ${a}
750
+ ${PLUGIN_PATH_USAGE}`);
751
+ else if (!args2.workspace) args2.workspace = a;
752
+ else throw new Error(`Unexpected argument ${a}
753
+ ${PLUGIN_PATH_USAGE}`);
754
+ }
755
+ if (!args2.workspace || !args2.version || !args2.sha256) throw new Error(PLUGIN_PATH_USAGE);
756
+ if (!isWorkspace(args2.workspace)) throw new Error(`${args2.workspace} is not a workspace name`);
757
+ return args2;
758
+ }
759
+ function refuse(name, why) {
760
+ throw new PluginRefused(`refused the plugin entry ${JSON.stringify(name)}: it ${why}. Nothing was unpacked.`);
761
+ }
762
+ function readZipEntries(zip) {
763
+ let eocd = -1;
764
+ for (let i = zip.length - 22; i >= Math.max(0, zip.length - 22 - 65535); i--) {
765
+ if (zip.readUInt32LE(i) === EOCD) {
766
+ eocd = i;
767
+ break;
768
+ }
769
+ }
770
+ if (eocd < 0) throw new PluginRefused("the download is not a zip. Nothing was unpacked.");
771
+ const count = zip.readUInt16LE(eocd + 10);
772
+ let at = zip.readUInt32LE(eocd + 16);
773
+ if (count === 65535 || at === 4294967295) throw new PluginRefused("the zip is a ZIP64 archive, which a plugin never is. Nothing was unpacked.");
774
+ const entries = [];
775
+ let total = 0;
776
+ for (let n = 0; n < count; n++) {
777
+ if (at + 46 > zip.length || zip.readUInt32LE(at) !== CENTRAL) {
778
+ throw new PluginRefused("the zip's directory is damaged. Nothing was unpacked.");
779
+ }
780
+ const madeBy = zip.readUInt16LE(at + 4);
781
+ const flags = zip.readUInt16LE(at + 8);
782
+ const method = zip.readUInt16LE(at + 10);
783
+ const compressedSize = zip.readUInt32LE(at + 20);
784
+ const size = zip.readUInt32LE(at + 24);
785
+ const nameLength = zip.readUInt16LE(at + 28);
786
+ const extraLength = zip.readUInt16LE(at + 30);
787
+ const commentLength = zip.readUInt16LE(at + 32);
788
+ const external = zip.readUInt32LE(at + 38);
789
+ const offset = zip.readUInt32LE(at + 42);
790
+ const name = zip.subarray(at + 46, at + 46 + nameLength).toString(flags & 2048 ? "utf8" : "latin1");
791
+ at += 46 + nameLength + extraLength + commentLength;
792
+ const unixMode = madeBy >> 8 === 3 ? external >>> 16 : 0;
793
+ const segments = name.split(/[\\/]/);
794
+ if (!name || /^[\\/]/.test(name) || /^[A-Za-z]:/.test(name)) refuse(name, "is absolute");
795
+ if (segments.includes("..")) refuse(name, "climbs out of the plugin folder (..)");
796
+ if (name.includes(":") || name.includes("\0")) refuse(name, "is not a plain relative path");
797
+ if ((unixMode & S_IFMT) === S_IFLNK) refuse(name, "is a link");
798
+ const directory = name.endsWith("/") || (unixMode & S_IFMT) === S_IFDIR;
799
+ if (!directory && unixMode && (unixMode & S_IFMT) !== S_IFREG) refuse(name, "is not a regular file");
800
+ if (flags & 1) refuse(name, "is encrypted");
801
+ if (!directory && method !== 0 && method !== 8) refuse(name, `uses compression method ${method}`);
802
+ total += size;
803
+ if (total > MAX_UNPACKED_BYTES) refuse(name, `takes the unpacked size past ${MAX_UNPACKED_BYTES} bytes`);
804
+ entries.push({ name, directory, mode: unixMode ? unixMode & 511 : void 0, method, compressedSize, size, offset });
805
+ }
806
+ return entries;
807
+ }
808
+ function entryData(zip, entry) {
809
+ const at = entry.offset;
810
+ if (at + 30 > zip.length || zip.readUInt32LE(at) !== LOCAL) refuse(entry.name, "has a damaged header");
811
+ const start = at + 30 + zip.readUInt16LE(at + 26) + zip.readUInt16LE(at + 28);
812
+ const raw = zip.subarray(start, start + entry.compressedSize);
813
+ let data;
814
+ try {
815
+ data = entry.method === 0 ? raw : inflateRawSync(raw, { maxOutputLength: Math.max(1, entry.size) + 1 });
816
+ } catch (e) {
817
+ refuse(entry.name, `cannot be unpacked (${e.message})`);
818
+ }
819
+ if (data.length !== entry.size) refuse(entry.name, "unpacks to a size other than the one it declares");
820
+ return data;
821
+ }
822
+ async function unpackZip(zip, dest) {
823
+ const entries = readZipEntries(zip);
824
+ const root = resolve3(dest);
825
+ const targets = entries.map((e) => {
826
+ const full = resolve3(root, ...e.name.split(/[\\/]/).filter(Boolean));
827
+ if (full !== root && !full.startsWith(root + sep2)) refuse(e.name, "climbs out of the plugin folder");
828
+ return full;
829
+ });
830
+ const data = entries.map((e) => e.directory ? void 0 : entryData(zip, e));
831
+ await mkdir3(root, { recursive: true });
832
+ for (const [i, entry] of entries.entries()) {
833
+ if (entry.directory) {
834
+ await mkdir3(targets[i], { recursive: true });
835
+ continue;
836
+ }
837
+ await mkdir3(dirname3(targets[i]), { recursive: true });
838
+ await writeFile3(targets[i], data[i], { mode: entry.mode ?? 420 });
839
+ }
840
+ return entries.filter((e) => !e.directory).length;
841
+ }
842
+ async function runPluginPath(argv, io) {
843
+ let args2;
844
+ try {
845
+ args2 = parsePluginPathArgs(argv);
846
+ } catch (e) {
847
+ io.err(e.message);
848
+ return 1;
849
+ }
850
+ const { workspace, version, sha256: sha2562 } = args2;
851
+ const folder = pluginDir(workspace, version);
852
+ try {
853
+ const state = await readState(workspace);
854
+ if (existsSync2(folder) && state.plugin?.[String(version)]?.sha256 === sha2562) {
855
+ io.out(folder);
856
+ return 0;
857
+ }
858
+ } catch (e) {
859
+ io.err(e.message);
860
+ return 1;
861
+ }
862
+ let client = io.client;
863
+ if (!client) {
864
+ try {
865
+ const apiUrl = apiUrlFrom();
866
+ const apiKey = await apiKeyFor(workspace);
867
+ if (!apiKey) {
868
+ io.err(
869
+ `No SystemDox key for ${workspace}. Set SYSTEMDOX_API_KEY, or run npx -y @systemdox/mcp-server setup, then run this again.`
870
+ );
871
+ return 1;
872
+ }
873
+ client = new SystemDoxApiClient({ apiKey, apiUrl, cacheTtlMs: 0, defaultOrg: void 0 });
874
+ } catch (e) {
875
+ io.err(e.message);
876
+ return 1;
877
+ }
878
+ }
879
+ let zip;
880
+ try {
881
+ ({ bytes: zip } = await client.getPluginZip(workspace, version));
882
+ } catch (e) {
883
+ io.err(e.message);
884
+ return 1;
885
+ }
886
+ const got = createHash2("sha256").update(zip).digest("hex");
887
+ if (got !== sha2562) {
888
+ io.err(
889
+ `The download of ${workspace}'s team plugin version ${version} has SHA-256 ${got}, not the ${sha2562} Claude Code named. Nothing was unpacked.`
890
+ );
891
+ return 2;
892
+ }
893
+ const tmp = join3(pluginRoot(workspace), `.${version}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`);
894
+ try {
895
+ await unpackZip(zip, tmp);
896
+ if (existsSync2(folder) && (await readState(workspace)).plugin?.[String(version)]?.sha256 === sha2562) {
897
+ await removeFolder(tmp);
898
+ } else {
899
+ if (existsSync2(folder)) await removeFolder(folder);
900
+ try {
901
+ await rename2(tmp, folder);
902
+ } catch (e) {
903
+ if (!existsSync2(folder)) throw e;
904
+ await removeFolder(tmp);
905
+ }
906
+ }
907
+ await updateState(workspace, (state) => {
908
+ state.plugin = { ...state.plugin, [String(version)]: { sha256: sha2562, installedAt: (/* @__PURE__ */ new Date()).toISOString() } };
909
+ });
910
+ } catch (e) {
911
+ await removeFolder(tmp);
912
+ io.err(e.message);
913
+ return e instanceof PluginRefused ? 2 : 1;
914
+ }
915
+ io.err(`Unpacked ${workspace}'s team plugin version ${version}.`);
916
+ io.out(folder);
917
+ return 0;
918
+ }
919
+ var PLUGIN_PATH_USAGE, MAX_UNPACKED_BYTES, SHA256_RE, PluginRefused, EOCD, CENTRAL, LOCAL, S_IFMT, S_IFLNK, S_IFDIR, S_IFREG;
920
+ var init_plugin_path = __esm({
921
+ "src/plugin-path.ts"() {
922
+ "use strict";
923
+ init_api_client();
924
+ init_team_setup_files();
925
+ PLUGIN_PATH_USAGE = "Usage: systemdox-mcp-server plugin-path <workspace> --version <n> --sha256 <hash>";
926
+ MAX_UNPACKED_BYTES = 20 * 1024 * 1024;
927
+ SHA256_RE = /^[0-9a-f]{64}$/;
928
+ PluginRefused = class extends Error {
929
+ };
930
+ EOCD = 101010256;
931
+ CENTRAL = 33639248;
932
+ LOCAL = 67324752;
933
+ S_IFMT = 61440;
934
+ S_IFLNK = 40960;
935
+ S_IFDIR = 16384;
936
+ S_IFREG = 32768;
937
+ }
938
+ });
939
+
472
940
  // src/lib/language.ts
473
941
  function detectLanguage(filePath) {
474
942
  const ext = filePath.slice(filePath.lastIndexOf("."));
@@ -601,12 +1069,12 @@ function registerGetGuardrails(server2, apiClient2) {
601
1069
  );
602
1070
  }
603
1071
  if (filtered.length === 0) {
604
- 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}"` : ""}.`;
1072
+ 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}"` : ""}.`;
605
1073
  return {
606
- content: [{ type: "text", text: text3 }]
1074
+ content: [{ type: "text", text: text4 }]
607
1075
  };
608
1076
  }
609
- const text2 = filtered.map(
1077
+ const text3 = filtered.map(
610
1078
  (g) => `## ${g.title} [${g.severity}]
611
1079
 
612
1080
  ${g.description}
@@ -630,7 +1098,7 @@ ${g.fix}
630
1098
  type: "text",
631
1099
  text: `Found ${filtered.length} check(s):
632
1100
 
633
- ${text2}`
1101
+ ${text3}`
634
1102
  }
635
1103
  ]
636
1104
  };
@@ -741,7 +1209,7 @@ ${spec.taskContent}`);
741
1209
  ]
742
1210
  };
743
1211
  }
744
- const text2 = filtered.map(
1212
+ const text3 = filtered.map(
745
1213
  (s) => `- **${s.name}** (${s.draftId}) \u2014 ${s.status}, ${s.requirementCount} requirements` + (s.repoFullName ? `, repo: ${s.repoFullName}` : "")
746
1214
  ).join("\n");
747
1215
  return {
@@ -750,7 +1218,7 @@ ${spec.taskContent}`);
750
1218
  type: "text",
751
1219
  text: `Found ${filtered.length} spec(s):
752
1220
 
753
- ${text2}`
1221
+ ${text3}`
754
1222
  }
755
1223
  ]
756
1224
  };
@@ -808,18 +1276,18 @@ function registerGetDocs(server2, apiClient2) {
808
1276
  isError: true
809
1277
  };
810
1278
  }
811
- let text3 = doc.content;
1279
+ let text4 = doc.content;
812
1280
  if (anchor && doc.section_resolved === false) {
813
- text3 = `_Section "${anchor}" not found \u2014 returning the full document._
1281
+ text4 = `_Section "${anchor}" not found \u2014 returning the full document._
814
1282
 
815
- ` + text3;
1283
+ ` + text4;
816
1284
  } else if (anchor && doc.heading) {
817
- text3 = `## ${doc.heading}
1285
+ text4 = `## ${doc.heading}
818
1286
 
819
1287
  ${doc.content}`;
820
1288
  }
821
1289
  return {
822
- content: [{ type: "text", text: text3 }]
1290
+ content: [{ type: "text", text: text4 }]
823
1291
  };
824
1292
  }
825
1293
  const params = {};
@@ -843,7 +1311,7 @@ ${doc.content}`;
843
1311
  ]
844
1312
  };
845
1313
  }
846
- const text2 = items.map((d) => {
1314
+ const text3 = items.map((d) => {
847
1315
  const sections = (d.outline ?? []).slice(0, 8);
848
1316
  const sectionHint = sections.length > 0 ? `
849
1317
  sections: ${sections.map((s) => s.anchor).join(", ")}` : "";
@@ -857,7 +1325,7 @@ ${doc.content}`;
857
1325
  type: "text",
858
1326
  text: `Found ${items.length} document(s) (${response.total} total):
859
1327
 
860
- ${text2}
1328
+ ${text3}
861
1329
 
862
1330
  Use doc_id for full content, or doc_id + section to fetch one section.`
863
1331
  }
@@ -1202,13 +1670,13 @@ var init_get_plan_board = __esm({
1202
1670
  });
1203
1671
 
1204
1672
  // src/lib/scoring.ts
1205
- function estimateTokens(text2) {
1206
- return Math.ceil(text2.length / CHARS_PER_TOKEN);
1673
+ function estimateTokens(text3) {
1674
+ return Math.ceil(text3.length / CHARS_PER_TOKEN);
1207
1675
  }
1208
- function truncateToTokens(text2, maxTokens) {
1676
+ function truncateToTokens(text3, maxTokens) {
1209
1677
  const maxChars = Math.max(0, maxTokens) * CHARS_PER_TOKEN;
1210
- if (text2.length <= maxChars) return { text: text2, truncated: false };
1211
- const head = text2.slice(0, maxChars);
1678
+ if (text3.length <= maxChars) return { text: text3, truncated: false };
1679
+ const head = text3.slice(0, maxChars);
1212
1680
  const floor = Math.floor(maxChars * 0.75);
1213
1681
  const paragraph = head.lastIndexOf("\n\n");
1214
1682
  const line = head.lastIndexOf("\n");
@@ -1253,7 +1721,7 @@ function scoreSeverity(item) {
1253
1721
  if (!item.severity) return 0.5;
1254
1722
  return SEVERITY_SCORES[item.severity] ?? 0.5;
1255
1723
  }
1256
- function extractKeywords(text2) {
1724
+ function extractKeywords(text3) {
1257
1725
  const stopWords = /* @__PURE__ */ new Set([
1258
1726
  "the",
1259
1727
  "a",
@@ -1338,7 +1806,7 @@ function extractKeywords(text2) {
1338
1806
  "implement",
1339
1807
  "create"
1340
1808
  ]);
1341
- return text2.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !stopWords.has(w));
1809
+ return text3.toLowerCase().replace(/[^a-z0-9\s-]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !stopWords.has(w));
1342
1810
  }
1343
1811
  function scoreItems(items, taskDescription, filePaths) {
1344
1812
  const languages = detectLanguages(filePaths);
@@ -1391,20 +1859,20 @@ var init_scoring = __esm({
1391
1859
 
1392
1860
  // src/lib/context-builder.ts
1393
1861
  function formatGuardrail(g) {
1394
- let text2 = `## ${g.title} [${g.severity}]
1862
+ let text3 = `## ${g.title} [${g.severity}]
1395
1863
 
1396
1864
  ${g.description}`;
1397
- if (g.pattern) text2 += `
1865
+ if (g.pattern) text3 += `
1398
1866
 
1399
1867
  **Bad:** \`\`\`
1400
1868
  ${g.pattern}
1401
1869
  \`\`\``;
1402
- if (g.fix) text2 += `
1870
+ if (g.fix) text3 += `
1403
1871
 
1404
1872
  **Good:** \`\`\`
1405
1873
  ${g.fix}
1406
1874
  \`\`\``;
1407
- return text2;
1875
+ return text3;
1408
1876
  }
1409
1877
  function formatSpecHeadline(s) {
1410
1878
  return `### ${s.name}
@@ -1426,19 +1894,19 @@ function formatRules(rules) {
1426
1894
  if (!byCategory.has(cat)) byCategory.set(cat, []);
1427
1895
  byCategory.get(cat).push(r);
1428
1896
  }
1429
- let text2 = "";
1897
+ let text3 = "";
1430
1898
  for (const [cat, catRules] of byCategory) {
1431
- text2 += `### ${cat.charAt(0).toUpperCase() + cat.slice(1)}
1899
+ text3 += `### ${cat.charAt(0).toUpperCase() + cat.slice(1)}
1432
1900
  `;
1433
1901
  for (const r of catRules) {
1434
- text2 += `- ${r.text}
1902
+ text3 += `- ${r.text}
1435
1903
  `;
1436
1904
  }
1437
- text2 += "\n";
1905
+ text3 += "\n";
1438
1906
  }
1439
1907
  return `## Project Rules (${rules.length})
1440
1908
 
1441
- ${text2.trim()}`;
1909
+ ${text3.trim()}`;
1442
1910
  }
1443
1911
  async function fetchSpecBody(api, draftId) {
1444
1912
  const spec = await api.get(
@@ -1792,13 +2260,13 @@ function registerGetContext(server2, apiClient2) {
1792
2260
  }
1793
2261
  },
1794
2262
  async ({ task_description, file_paths, max_tokens }) => {
1795
- const { text: text2 } = await buildContext(apiClient2, {
2263
+ const { text: text3 } = await buildContext(apiClient2, {
1796
2264
  taskDescription: task_description,
1797
2265
  filePaths: file_paths,
1798
2266
  maxTokens: max_tokens
1799
2267
  });
1800
2268
  return {
1801
- content: [{ type: "text", text: text2 }]
2269
+ content: [{ type: "text", text: text3 }]
1802
2270
  };
1803
2271
  }
1804
2272
  );
@@ -2061,9 +2529,9 @@ function registerGetProjectRules(server2, apiClient2) {
2061
2529
  );
2062
2530
  }
2063
2531
  if (filtered.length === 0) {
2064
- 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}"` : ""}.`;
2532
+ 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}"` : ""}.`;
2065
2533
  return {
2066
- content: [{ type: "text", text: text3 }]
2534
+ content: [{ type: "text", text: text4 }]
2067
2535
  };
2068
2536
  }
2069
2537
  const grouped = /* @__PURE__ */ new Map();
@@ -2072,25 +2540,25 @@ function registerGetProjectRules(server2, apiClient2) {
2072
2540
  if (!grouped.has(cat)) grouped.set(cat, []);
2073
2541
  grouped.get(cat).push(rule);
2074
2542
  }
2075
- let text2 = `Found ${filtered.length} project rule(s):
2543
+ let text3 = `Found ${filtered.length} project rule(s):
2076
2544
 
2077
2545
  `;
2078
2546
  for (const [cat, catRules] of grouped) {
2079
2547
  const label = CATEGORY_LABELS[cat] ?? cat;
2080
- text2 += `## ${label}
2548
+ text3 += `## ${label}
2081
2549
 
2082
2550
  `;
2083
2551
  for (const rule of catRules) {
2084
- text2 += `- ${rule.text}
2552
+ text3 += `- ${rule.text}
2085
2553
  `;
2086
2554
  }
2087
- text2 += "\n";
2555
+ text3 += "\n";
2088
2556
  }
2089
2557
  return {
2090
2558
  content: [
2091
2559
  {
2092
2560
  type: "text",
2093
- text: text2
2561
+ text: text3
2094
2562
  }
2095
2563
  ]
2096
2564
  };
@@ -2131,10 +2599,10 @@ function registerCreateProjectRule(server2, apiClient2) {
2131
2599
  readOnlyHint: false
2132
2600
  }
2133
2601
  },
2134
- async ({ text: text2, category }) => {
2602
+ async ({ text: text3, category }) => {
2135
2603
  const saved = await apiClient2.post(
2136
2604
  "/v1/standards/rules",
2137
- { text: text2, category }
2605
+ { text: text3, category }
2138
2606
  );
2139
2607
  apiClient2.invalidateCache("/v1/standards/rules");
2140
2608
  return {
@@ -2177,7 +2645,7 @@ function registerGetPromptTemplates(server2, apiClient2) {
2177
2645
  ]
2178
2646
  };
2179
2647
  }
2180
- const text2 = templates.map(
2648
+ const text3 = templates.map(
2181
2649
  (t) => `## ${t.name}
2182
2650
 
2183
2651
  ` + (t.description ? `*${t.description}*
@@ -2192,7 +2660,7 @@ ${t.prompt}
2192
2660
  type: "text",
2193
2661
  text: `Found ${templates.length} prompt template(s):
2194
2662
 
2195
- ${text2}`
2663
+ ${text3}`
2196
2664
  }
2197
2665
  ]
2198
2666
  };
@@ -2252,10 +2720,10 @@ var init_create_prompt_template = __esm({
2252
2720
 
2253
2721
  // src/tools/skills.ts
2254
2722
  import { z as z13 } from "zod";
2255
- import { homedir as homedir2 } from "os";
2256
- import { basename as basename2, resolve as resolve3 } from "path";
2723
+ import { homedir as homedir3 } from "os";
2724
+ import { basename as basename2, resolve as resolve4 } from "path";
2257
2725
  function expandHome(path) {
2258
- return path.replace(/^~(?=$|[\\/])/, homedir2());
2726
+ return path.replace(/^~(?=$|[\\/])/, homedir3());
2259
2727
  }
2260
2728
  function text(t) {
2261
2729
  return { content: [{ type: "text", text: t }] };
@@ -2357,7 +2825,7 @@ Left out (hidden files are never part of a skill): ${skipped.join(", ")}` : "")
2357
2825
  );
2358
2826
  if (localFiles) {
2359
2827
  const dest2 = args2.target ? expandHome(args2.target) : defaultSkillDir(version.name);
2360
- if (basename2(resolve3(dest2)) !== version.name) {
2828
+ if (basename2(resolve4(dest2)) !== version.name) {
2361
2829
  throw new Error(`target must be a folder named ${version.name} (e.g. .claude/skills/${version.name}); got ${dest2}`);
2362
2830
  }
2363
2831
  const result = await installSkill(version, dest2, { overwrite: args2.overwrite });
@@ -2401,6 +2869,203 @@ var init_skills2 = __esm({
2401
2869
  }
2402
2870
  });
2403
2871
 
2872
+ // src/lib/person-values.ts
2873
+ var W, D, WORD_OR, PERSON_ID, PERSON_TOKEN, BEFORE, AFTER, E164, NATIONAL, ATEXT, EMAIL_RE, HOME_PREFIX, HOME, KEY_PREFIX;
2874
+ var init_person_values = __esm({
2875
+ "src/lib/person-values.ts"() {
2876
+ "use strict";
2877
+ W = "[\\p{L}\\p{N}_]";
2878
+ D = "\\p{Nd}";
2879
+ WORD_OR = (extra) => `[\\p{L}\\p{N}_${extra}]`;
2880
+ PERSON_ID = "(?:con|ctc|usr)_[0-9A-Za-z]{10,40}";
2881
+ PERSON_TOKEN = `\`${PERSON_ID}\`(?:[ \\t]*\\([^()\\n]*\\))?`;
2882
+ BEFORE = `(?<!${WORD_OR("+.:/\\\\\\-")})(?<!${D}[ -])`;
2883
+ AFTER = `(?!${WORD_OR(":/\\\\")}|\\.${D}|[ -]?${D}|-${W})`;
2884
+ E164 = `${BEFORE}\\+${D}+(?:[ -]${D}+)*${AFTER}`;
2885
+ NATIONAL = `${BEFORE}0${D}*(?:[ -]${D}+)*${AFTER}`;
2886
+ ATEXT = "[A-Za-z0-9!#$%&'*+/=?^_{|}~\\-]";
2887
+ 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])";
2888
+ HOME_PREFIX = "(?:[A-Za-z]:\\\\+Users\\\\+|[A-Za-z]:/+Users/+|/Users/|/home/)";
2889
+ HOME = `(?<!${WORD_OR(".~:/\\\\$}\\-")})${HOME_PREFIX}[^\\\\/\\s"'\`<>|:*?]+`;
2890
+ 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,}`;
2891
+ }
2892
+ });
2893
+
2894
+ // src/lib/team-setup-types.ts
2895
+ var PARTS, PERSON_ID_RE;
2896
+ var init_team_setup_types = __esm({
2897
+ "src/lib/team-setup-types.ts"() {
2898
+ "use strict";
2899
+ init_person_values();
2900
+ PARTS = ["hooks", "mcp", "agents", "commands", "instructions"];
2901
+ PERSON_ID_RE = new RegExp(`^${PERSON_ID}$`);
2902
+ }
2903
+ });
2904
+
2905
+ // src/tools/team-setup.ts
2906
+ import { z as z14 } from "zod";
2907
+ import { existsSync as existsSync3 } from "fs";
2908
+ import { lstat as lstat2 } from "fs/promises";
2909
+ import { homedir as homedir4 } from "os";
2910
+ import { join as join4, resolve as resolve5 } from "path";
2911
+ function expandHome2(path) {
2912
+ return path.replace(/^~(?=$|[\\/])/, homedir4());
2913
+ }
2914
+ function text2(t) {
2915
+ return { content: [{ type: "text", text: t }] };
2916
+ }
2917
+ async function isLink(path) {
2918
+ try {
2919
+ return (await lstat2(path)).isSymbolicLink();
2920
+ } catch (e) {
2921
+ if (e.code === "ENOENT") return false;
2922
+ throw e;
2923
+ }
2924
+ }
2925
+ async function readPartDirectory(part, directory) {
2926
+ const root = resolve5(directory);
2927
+ const layout = LAYOUT[part];
2928
+ const paths = [];
2929
+ const leftOut = [];
2930
+ const links = [];
2931
+ for (const path of layout.files) {
2932
+ const segments = path.split("/");
2933
+ let present = true;
2934
+ for (let i = 1; i <= segments.length; i++) {
2935
+ const rel = segments.slice(0, i).join("/");
2936
+ if (await isLink(join4(root, ...segments.slice(0, i)))) {
2937
+ links.push(rel);
2938
+ present = false;
2939
+ break;
2940
+ }
2941
+ }
2942
+ if (present && existsSync3(join4(root, ...segments))) paths.push(path);
2943
+ }
2944
+ for (const { dir, md } of layout.folders) {
2945
+ const full = join4(root, dir);
2946
+ if (await isLink(full)) {
2947
+ links.push(dir);
2948
+ continue;
2949
+ }
2950
+ if (!existsSync3(full)) continue;
2951
+ const walked = await walk(full, { skipHidden: true });
2952
+ links.push(...walked.links.map((p) => `${dir}/${p}`));
2953
+ leftOut.push(...walked.hidden.map((p) => `${dir}/${p}`));
2954
+ for (const p of walked.files) {
2955
+ if (md && !p.endsWith(".md")) leftOut.push(`${dir}/${p}`);
2956
+ else paths.push(`${dir}/${p}`);
2957
+ }
2958
+ }
2959
+ if (links.length) {
2960
+ throw new Error(
2961
+ `${links.join(", ")}: links are not followed; copy the file into the folder. Nothing was sent.`
2962
+ );
2963
+ }
2964
+ if (layout.required && !paths.includes(layout.required)) {
2965
+ throw new Error(`${root} has no ${layout.required}: the ${part} part needs it. Nothing was sent.`);
2966
+ }
2967
+ if (!paths.length) {
2968
+ throw new Error(
2969
+ `${root} has no ${layout.folders.map((f) => `${f.dir}/*.md`).join(" or ")}: nothing to publish for ${part}.`
2970
+ );
2971
+ }
2972
+ return { files: await readTextFiles(root, paths.sort(), `the ${part} part`), leftOut };
2973
+ }
2974
+ function describePerson(id, person) {
2975
+ const lines = [`**${person.label ?? "(no label)"}**, \`${id}\``];
2976
+ if (person.mergedInto) {
2977
+ lines.push(`\`${id}\` was merged into \`${person.mergedInto}\`: these are \`${person.mergedInto}\`'s details.`);
2978
+ }
2979
+ lines.push(`- Name: ${person.name ?? "(none held)"}`);
2980
+ lines.push(`- Phone number: ${person.phone ?? "(none held)"}`);
2981
+ lines.push(`- Email address: ${person.email ?? "(none held)"}`);
2982
+ const token = `\`${person.mergedInto ?? id}\`${person.label ? ` (${person.label})` : ""}`;
2983
+ lines.push(
2984
+ "",
2985
+ `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.`
2986
+ );
2987
+ return lines.join("\n");
2988
+ }
2989
+ function registerTeamSetupTools(server2, apiClient2, { localFiles }) {
2990
+ const fileSchema = z14.object({
2991
+ path: z14.string().describe(`Path inside the part's folder, forward slashes, e.g. "hooks/hooks.json"`),
2992
+ content: z14.string().describe("The file's full text, exactly as it is")
2993
+ });
2994
+ server2.registerTool(
2995
+ "publish_setup_part",
2996
+ {
2997
+ title: "Publish Setup Part",
2998
+ 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."),
2999
+ inputSchema: {
3000
+ part: z14.enum(PARTS).describe("Which part of the setup"),
3001
+ ...localFiles ? {
3002
+ directory: z14.string().optional().describe(
3003
+ "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/"
3004
+ )
3005
+ } : {},
3006
+ files: z14.array(fileSchema).optional().describe(localFiles ? "Only when there is no folder to read" : "Every file of the part")
3007
+ },
3008
+ annotations: { readOnlyHint: false }
3009
+ },
3010
+ async (args2) => {
3011
+ let files;
3012
+ let leftOut = [];
3013
+ if (localFiles && args2.directory) {
3014
+ ({ files, leftOut } = await readPartDirectory(args2.part, expandHome2(args2.directory)));
3015
+ } else if (args2.files?.length) {
3016
+ files = args2.files;
3017
+ } else {
3018
+ throw new Error(localFiles ? "Pass `directory` (or `files`)." : "Pass `files`.");
3019
+ }
3020
+ const { workspace } = await apiClient2.getOwnTeamSetup();
3021
+ const saved = await apiClient2.publishPartVersion(workspace, args2.part, files);
3022
+ const lines = [`Published **${saved.part}** version ${saved.version}.`];
3023
+ if (saved.part !== "instructions" && saved.pluginVersion !== void 0) {
3024
+ lines.push(`Team plugin version ${saved.pluginVersion}; each member accepts it at their next update.`);
3025
+ }
3026
+ if (saved.replacedValues?.length) {
3027
+ lines.push(
3028
+ "Replaced by a reference to each member's own environment variable:",
3029
+ ...saved.replacedValues.map((r) => `- ${r.server} ${r.field} ${r.name}: \${${r.name}}`)
3030
+ );
3031
+ }
3032
+ if (leftOut.length) lines.push(`Left out (not part of ${args2.part}): ${leftOut.join(", ")}`);
3033
+ return text2(lines.join("\n"));
3034
+ }
3035
+ );
3036
+ server2.registerTool(
3037
+ "resolve_person",
3038
+ {
3039
+ title: "Resolve Person",
3040
+ 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.",
3041
+ inputSchema: {
3042
+ 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")
3043
+ },
3044
+ annotations: { readOnlyHint: true }
3045
+ },
3046
+ async (args2) => {
3047
+ const { workspace } = await apiClient2.getOwnTeamSetup();
3048
+ const person = await apiClient2.resolvePerson(workspace, args2.id);
3049
+ return text2(describePerson(args2.id, person));
3050
+ }
3051
+ );
3052
+ }
3053
+ var LAYOUT;
3054
+ var init_team_setup = __esm({
3055
+ "src/tools/team-setup.ts"() {
3056
+ "use strict";
3057
+ init_skills();
3058
+ init_team_setup_types();
3059
+ LAYOUT = {
3060
+ hooks: { files: ["hooks/hooks.json"], folders: [{ dir: "scripts", md: false }], required: "hooks/hooks.json" },
3061
+ mcp: { files: [".mcp.json"], folders: [], required: ".mcp.json" },
3062
+ agents: { files: [], folders: [{ dir: "agents", md: true }] },
3063
+ commands: { files: [], folders: [{ dir: "commands", md: true }] },
3064
+ instructions: { files: ["CLAUDE.md"], folders: [], required: "CLAUDE.md" }
3065
+ };
3066
+ }
3067
+ });
3068
+
2404
3069
  // src/server.ts
2405
3070
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2406
3071
  function createServer(apiClient2, { localFiles = false } = {}) {
@@ -2423,6 +3088,7 @@ function createServer(apiClient2, { localFiles = false } = {}) {
2423
3088
  registerCreateProjectRule(server2, apiClient2);
2424
3089
  registerCreatePromptTemplate(server2, apiClient2);
2425
3090
  registerSkillTools(server2, apiClient2, { localFiles });
3091
+ registerTeamSetupTools(server2, apiClient2, { localFiles });
2426
3092
  return server2;
2427
3093
  }
2428
3094
  var SERVER_VERSION;
@@ -2444,7 +3110,8 @@ var init_server = __esm({
2444
3110
  init_get_prompt_templates();
2445
3111
  init_create_prompt_template();
2446
3112
  init_skills2();
2447
- SERVER_VERSION = true ? "0.1.45" : "0.0.0-dev";
3113
+ init_team_setup();
3114
+ SERVER_VERSION = true ? "0.1.46" : "0.0.0-dev";
2448
3115
  }
2449
3116
  });
2450
3117
 
@@ -2478,17 +3145,18 @@ var init_index = __esm({
2478
3145
 
2479
3146
  // src/cli.ts
2480
3147
  init_config();
2481
- import { readFile as readFile2, writeFile as writeFile2, chmod } from "fs/promises";
2482
- import { existsSync as existsSync2 } from "fs";
2483
- import { homedir as homedir3 } from "os";
2484
- import { join as join2 } from "path";
3148
+ init_team_setup_files();
3149
+ import { readFile as readFile3, writeFile as writeFile4, chmod as chmod2 } from "fs/promises";
3150
+ import { existsSync as existsSync4 } from "fs";
3151
+ import { homedir as homedir5 } from "os";
3152
+ import { join as join5 } from "path";
2485
3153
  import { createInterface } from "readline";
2486
3154
  function prompt(question) {
2487
3155
  const rl = createInterface({ input: process.stdin, output: process.stdout });
2488
- return new Promise((resolve4) => {
3156
+ return new Promise((resolve6) => {
2489
3157
  rl.question(question, (answer) => {
2490
3158
  rl.close();
2491
- resolve4(answer.trim());
3159
+ resolve6(answer.trim());
2492
3160
  });
2493
3161
  });
2494
3162
  }
@@ -2508,45 +3176,45 @@ async function validateApiKey(apiKey, apiUrl) {
2508
3176
  }
2509
3177
  function findConfigTargets() {
2510
3178
  const targets = [];
2511
- const projectMcp = join2(process.cwd(), ".mcp.json");
3179
+ const projectMcp = join5(process.cwd(), ".mcp.json");
2512
3180
  targets.push({
2513
3181
  name: "Claude Code (project)",
2514
3182
  path: projectMcp,
2515
3183
  detected: true
2516
3184
  // Always available
2517
3185
  });
2518
- const claudeUser = join2(homedir3(), ".claude.json");
3186
+ const claudeUser = join5(homedir5(), ".claude.json");
2519
3187
  targets.push({
2520
3188
  name: "Claude Code (user \u2014 all projects)",
2521
3189
  path: claudeUser,
2522
3190
  detected: true
2523
3191
  // Always available
2524
3192
  });
2525
- const cursorProject = join2(process.cwd(), ".cursor", "mcp.json");
2526
- const cursorDir = join2(process.cwd(), ".cursor");
3193
+ const cursorProject = join5(process.cwd(), ".cursor", "mcp.json");
3194
+ const cursorDir = join5(process.cwd(), ".cursor");
2527
3195
  targets.push({
2528
3196
  name: "Cursor (project)",
2529
3197
  path: cursorProject,
2530
- detected: existsSync2(cursorDir)
3198
+ detected: existsSync4(cursorDir)
2531
3199
  });
2532
- const vscodeProject = join2(process.cwd(), ".vscode", "mcp.json");
2533
- const vscodeDir = join2(process.cwd(), ".vscode");
3200
+ const vscodeProject = join5(process.cwd(), ".vscode", "mcp.json");
3201
+ const vscodeDir = join5(process.cwd(), ".vscode");
2534
3202
  targets.push({
2535
3203
  name: "VS Code (project)",
2536
3204
  path: vscodeProject,
2537
- detected: existsSync2(vscodeDir)
3205
+ detected: existsSync4(vscodeDir)
2538
3206
  });
2539
- const cursorGlobal = join2(homedir3(), ".cursor", "mcp.json");
3207
+ const cursorGlobal = join5(homedir5(), ".cursor", "mcp.json");
2540
3208
  targets.push({
2541
3209
  name: "Cursor (global)",
2542
3210
  path: cursorGlobal,
2543
- detected: existsSync2(join2(homedir3(), ".cursor"))
3211
+ detected: existsSync4(join5(homedir5(), ".cursor"))
2544
3212
  });
2545
3213
  return targets;
2546
3214
  }
2547
3215
  async function readOrCreateConfig(path) {
2548
3216
  try {
2549
- const content = await readFile2(path, "utf-8");
3217
+ const content = await readFile3(path, "utf-8");
2550
3218
  return JSON.parse(content);
2551
3219
  } catch {
2552
3220
  return { mcpServers: {} };
@@ -2554,13 +3222,13 @@ async function readOrCreateConfig(path) {
2554
3222
  }
2555
3223
  async function writeConfig(path, config2) {
2556
3224
  const dir = path.substring(0, path.lastIndexOf("/"));
2557
- if (!existsSync2(dir)) {
2558
- const { mkdir: mkdir2 } = await import("fs/promises");
2559
- await mkdir2(dir, { recursive: true });
3225
+ if (!existsSync4(dir)) {
3226
+ const { mkdir: mkdir4 } = await import("fs/promises");
3227
+ await mkdir4(dir, { recursive: true });
2560
3228
  }
2561
- await writeFile2(path, JSON.stringify(config2, null, 2) + "\n", { mode: 384 });
3229
+ await writeFile4(path, JSON.stringify(config2, null, 2) + "\n", { mode: 384 });
2562
3230
  try {
2563
- await chmod(path, 384);
3231
+ await chmod2(path, 384);
2564
3232
  } catch {
2565
3233
  }
2566
3234
  }
@@ -2636,7 +3304,7 @@ async function setup() {
2636
3304
  console.log(` Configured: ${target.name}`);
2637
3305
  }
2638
3306
  console.log(
2639
- "\n Done! Restart your AI coding tool to activate the SystemDox MCP server.\n The server provides 17 tools: get_context, get_checks,\n get_project_rules, get_prompt_templates, get_specs, get_docs,\n get_tech_stack, get_planning_context, get_plan_board, create_check,\n create_project_rule, create_prompt_template, update_spec,\n report_decision, publish_skill, list_skills, install_skill.\n\n get_guardrails and create_guardrail remain registered as deprecated\n aliases of get_checks and create_check \u2014 existing configs keep working.\n"
3307
+ "\n Done! Restart your AI coding tool to activate the SystemDox MCP server.\n The server provides 19 tools: get_context, get_checks,\n get_project_rules, get_prompt_templates, get_specs, get_docs,\n get_tech_stack, get_planning_context, get_plan_board, create_check,\n create_project_rule, create_prompt_template, update_spec,\n report_decision, publish_skill, list_skills, install_skill,\n publish_setup_part, resolve_person.\n\n get_guardrails and create_guardrail remain registered as deprecated\n aliases of get_checks and create_check \u2014 existing configs keep working.\n"
2640
3308
  );
2641
3309
  }
2642
3310
  var args = process.argv.slice(2);
@@ -2652,6 +3320,12 @@ if (command === "setup") {
2652
3320
  Promise.resolve().then(() => (init_install_skill(), install_skill_exports)).then(
2653
3321
  ({ runInstallSkill: runInstallSkill2 }) => runInstallSkill2(args.slice(1), { out: console.log, err: console.error })
2654
3322
  ).then((code) => process.exit(code));
3323
+ } else if (command === "marketplace-auth") {
3324
+ runMarketplaceAuth({ out: console.log, err: console.error }).then((code) => process.exit(code));
3325
+ } else if (command === "plugin-path") {
3326
+ Promise.resolve().then(() => (init_plugin_path(), plugin_path_exports)).then(
3327
+ ({ runPluginPath: runPluginPath2 }) => runPluginPath2(args.slice(1), { out: console.log, err: console.error })
3328
+ ).then((code) => process.exit(code));
2655
3329
  } else if (command === "--help" || command === "-h") {
2656
3330
  console.log(`
2657
3331
  SystemDox MCP Server
@@ -2662,10 +3336,19 @@ if (command === "setup") {
2662
3336
  systemdox-mcp-server install-skill <name> [--version n] [--target dir] [--overwrite]
2663
3337
  Install a skill published to SystemDox
2664
3338
  (default folder: ~/.claude/skills/<name>)
3339
+ systemdox-mcp-server plugin-path <workspace> --version <n> --sha256 <hash>
3340
+ Print the folder of your team's plugin version,
3341
+ downloading and checking it first when needed
3342
+ (Claude Code runs it)
3343
+ systemdox-mcp-server marketplace-auth
3344
+ Print the header for your team's marketplace
3345
+ (Claude Code runs it)
2665
3346
  systemdox-mcp-server --help Show this help message
2666
3347
 
2667
3348
  Environment variables:
2668
- SYSTEMDOX_API_KEY Your SystemDox API key (required)
3349
+ SYSTEMDOX_API_KEY Your SystemDox API key (required; plugin-path and
3350
+ marketplace-auth also read
3351
+ ~/.claude/systemdox/credentials.json)
2669
3352
  SYSTEMDOX_API_URL API URL (default: ${API_URL_DEFAULT})
2670
3353
  SYSTEMDOX_CACHE_TTL Cache TTL in seconds (default: 300)
2671
3354
  SYSTEMDOX_DEFAULT_ORG Default GitHub org