@systemdox/mcp-server 0.1.44 → 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.
@@ -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, text) {
39
+ super(`SystemDox API error ${status}: ${text}`);
40
+ this.status = status;
41
+ this.text = text;
42
+ let body;
43
+ try {
44
+ body = JSON.parse(text);
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(text)?.[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 text = await res.text();
97
- throw new Error(`SystemDox API error ${res.status}: ${text}`);
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());
@@ -1,4 +1,18 @@
1
1
  import type { ServerConfig } from "./config.js";
2
+ import type { BlobRefusal, FileIn, FoundValue, Manifest, Marketplace, MemoryProjectSummary, MemoryVersion, PartDetail, PartName, PartPublished, PartVersionFiles, Person, PluginVersion, TeamSetup, ValueAnswer, ValueAnswerListed, ValueAnswerResults } from "./team-setup-types.js";
3
+ /**
4
+ * A refused API call. The message keeps the form every caller has always
5
+ * shown (`SystemDox API error <status>: <body>`), so an SYD- code in the body
6
+ * reaches the user as the server wrote it; `code` and `body` are there for a
7
+ * caller that acts on one (a 409's `currentVersion`, a 503's SYD-K019).
8
+ */
9
+ export declare class SystemDoxApiError extends Error {
10
+ readonly status: number;
11
+ readonly text: string;
12
+ readonly code: string | undefined;
13
+ readonly body: unknown;
14
+ constructor(status: number, text: string);
15
+ }
2
16
  export declare class SystemDoxApiClient {
3
17
  private config;
4
18
  private cache;
@@ -11,6 +25,73 @@ export declare class SystemDoxApiClient {
11
25
  post<T>(path: string, body: unknown): Promise<T>;
12
26
  put<T>(path: string, body: unknown): Promise<T>;
13
27
  private mutate;
28
+ /** getOwnTeamSetup: the key's own workspace, named in `workspace`. */
29
+ getOwnTeamSetup(): Promise<TeamSetup>;
30
+ getTeamSetup(workspace: string): Promise<TeamSetup>;
31
+ getMarketplace(workspace: string): Promise<Marketplace>;
32
+ getPart(workspace: string, part: PartName): Promise<PartDetail>;
33
+ publishPartVersion(workspace: string, part: PartName, files: FileIn[]): Promise<PartPublished>;
34
+ getPartVersion(workspace: string, part: PartName, version: number | "latest"): Promise<PartVersionFiles>;
35
+ getSetupSkills(workspace: string): Promise<{
36
+ skills: string[];
37
+ }>;
38
+ setSetupSkills(workspace: string, skills: string[]): Promise<{
39
+ skills: string[];
40
+ }>;
41
+ getPlugin(workspace: string): Promise<{
42
+ name: string;
43
+ versions: PluginVersion[];
44
+ }>;
45
+ /** getPluginZip: the zip's bytes as served, and its X-Content-SHA256
46
+ * header. The caller checks the BYTES against the hash it was given. */
47
+ getPluginZip(workspace: string, version: number | "latest"): Promise<{
48
+ bytes: Buffer;
49
+ sha256: string | null;
50
+ }>;
51
+ listMemoryProjects(workspace: string): Promise<{
52
+ projects: MemoryProjectSummary[];
53
+ }>;
54
+ getMemory(workspace: string, project: string): Promise<MemoryVersion>;
55
+ /** pushMemoryVersion: a stale base throws SystemDoxApiError with status 409,
56
+ * code SYD-K010 and `body.currentVersion`. */
57
+ pushMemoryVersion(workspace: string, project: string, body: {
58
+ baseVersion: number;
59
+ manifest: Manifest;
60
+ spaceId?: string;
61
+ }): Promise<MemoryVersion>;
62
+ getMemoryVersion(workspace: string, project: string, version: number | "latest"): Promise<MemoryVersion>;
63
+ findMissingBlobs(workspace: string, project: string, hashes: string[]): Promise<{
64
+ missing: string[];
65
+ }>;
66
+ uploadBlobs(workspace: string, project: string, blobs: {
67
+ sha256: string;
68
+ content: string;
69
+ }[]): Promise<{
70
+ stored: string[];
71
+ refused: BlobRefusal[];
72
+ }>;
73
+ readBlobs(workspace: string, project: string, hashes: string[]): Promise<{
74
+ blobs: {
75
+ sha256: string;
76
+ content: string;
77
+ }[];
78
+ remaining: string[];
79
+ }>;
80
+ listValueAnswers(workspace: string): Promise<{
81
+ answers: ValueAnswerListed[];
82
+ }>;
83
+ lookupValues(workspace: string, values: FoundValue[]): Promise<{
84
+ answers: ValueAnswer[];
85
+ }>;
86
+ answerValues(workspace: string, answers: (FoundValue & {
87
+ answer: "ours" | "member";
88
+ label?: string;
89
+ })[]): Promise<ValueAnswerResults>;
90
+ fileMemoryPeople(workspace: string, people: {
91
+ values: FoundValue[];
92
+ label: string;
93
+ }[]): Promise<ValueAnswerResults>;
94
+ resolvePerson(workspace: string, personId: string): Promise<Person>;
14
95
  invalidateCache(path?: string): void;
15
96
  private buildUrl;
16
97
  private fetchRaw;
@@ -31,6 +31,28 @@ var ResponseCache = class {
31
31
  };
32
32
 
33
33
  // src/lib/api-client.ts
34
+ var SystemDoxApiError = class extends Error {
35
+ constructor(status, text) {
36
+ super(`SystemDox API error ${status}: ${text}`);
37
+ this.status = status;
38
+ this.text = text;
39
+ let body;
40
+ try {
41
+ body = JSON.parse(text);
42
+ } catch {
43
+ body = void 0;
44
+ }
45
+ this.body = body;
46
+ const code = body?.code;
47
+ this.code = typeof code === "string" ? code : /\b(SYD-[A-Z]\d{3})\b/.exec(text)?.[1];
48
+ }
49
+ status;
50
+ text;
51
+ code;
52
+ body;
53
+ };
54
+ var TEAM_SETUP = "/v1/team-setup";
55
+ var seg = (s) => encodeURIComponent(String(s));
34
56
  var SystemDoxApiClient = class {
35
57
  config;
36
58
  cache;
@@ -52,10 +74,7 @@ var SystemDoxApiClient = class {
52
74
  const cached = this.cache.get(cacheKey);
53
75
  if (cached !== void 0) return cached;
54
76
  const res = await this.fetchRaw(path, params);
55
- if (!res.ok) {
56
- const body = await res.text();
57
- throw new Error(`SystemDox API error ${res.status}: ${body}`);
58
- }
77
+ if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
59
78
  const data = await res.json();
60
79
  this.cache.set(cacheKey, data);
61
80
  return data;
@@ -64,10 +83,7 @@ var SystemDoxApiClient = class {
64
83
  * latest now, not the one cached before someone published another. */
65
84
  async getFresh(path) {
66
85
  const res = await this.fetchRaw(path);
67
- if (!res.ok) {
68
- const body = await res.text();
69
- throw new Error(`SystemDox API error ${res.status}: ${body}`);
70
- }
86
+ if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
71
87
  return await res.json();
72
88
  }
73
89
  async post(path, body) {
@@ -89,12 +105,101 @@ var SystemDoxApiClient = class {
89
105
  },
90
106
  body: JSON.stringify(body)
91
107
  });
92
- if (!res.ok) {
93
- const text = await res.text();
94
- throw new Error(`SystemDox API error ${res.status}: ${text}`);
95
- }
108
+ if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
96
109
  return await res.json();
97
110
  }
111
+ // ── Team setup (ADR-217, specs/changes/team-setup/openapi.yaml) ──────────
112
+ // One method per operationId. Every read skips the cache: setup, sync and
113
+ // plugin-path act on what the workspace holds now.
114
+ /** getOwnTeamSetup: the key's own workspace, named in `workspace`. */
115
+ getOwnTeamSetup() {
116
+ return this.getFresh(TEAM_SETUP);
117
+ }
118
+ getTeamSetup(workspace) {
119
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}`);
120
+ }
121
+ getMarketplace(workspace) {
122
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/marketplace.json`);
123
+ }
124
+ getPart(workspace, part) {
125
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/parts/${seg(part)}`);
126
+ }
127
+ publishPartVersion(workspace, part, files) {
128
+ return this.post(`${TEAM_SETUP}/${seg(workspace)}/parts/${seg(part)}/versions`, { files });
129
+ }
130
+ getPartVersion(workspace, part, version) {
131
+ return this.getFresh(
132
+ `${TEAM_SETUP}/${seg(workspace)}/parts/${seg(part)}/versions/${seg(version)}`
133
+ );
134
+ }
135
+ getSetupSkills(workspace) {
136
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/skills`);
137
+ }
138
+ setSetupSkills(workspace, skills) {
139
+ return this.put(`${TEAM_SETUP}/${seg(workspace)}/skills`, { skills });
140
+ }
141
+ getPlugin(workspace) {
142
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/plugin`);
143
+ }
144
+ /** getPluginZip: the zip's bytes as served, and its X-Content-SHA256
145
+ * header. The caller checks the BYTES against the hash it was given. */
146
+ async getPluginZip(workspace, version) {
147
+ const res = await fetch(
148
+ this.buildUrl(`${TEAM_SETUP}/${seg(workspace)}/plugin/versions/${seg(version)}/zip`),
149
+ {
150
+ headers: {
151
+ Authorization: `Bearer ${this.config.apiKey}`,
152
+ "X-API-Key": this.config.apiKey,
153
+ Accept: "application/zip"
154
+ }
155
+ }
156
+ );
157
+ if (!res.ok) throw new SystemDoxApiError(res.status, await res.text());
158
+ return { bytes: Buffer.from(await res.arrayBuffer()), sha256: res.headers.get("x-content-sha256") };
159
+ }
160
+ listMemoryProjects(workspace) {
161
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/memory`);
162
+ }
163
+ getMemory(workspace, project) {
164
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}`);
165
+ }
166
+ /** pushMemoryVersion: a stale base throws SystemDoxApiError with status 409,
167
+ * code SYD-K010 and `body.currentVersion`. */
168
+ pushMemoryVersion(workspace, project, body) {
169
+ return this.post(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/versions`, body);
170
+ }
171
+ getMemoryVersion(workspace, project, version) {
172
+ return this.getFresh(
173
+ `${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/versions/${seg(version)}`
174
+ );
175
+ }
176
+ findMissingBlobs(workspace, project, hashes) {
177
+ return this.post(
178
+ `${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/blobs/missing`,
179
+ { hashes }
180
+ );
181
+ }
182
+ uploadBlobs(workspace, project, blobs) {
183
+ return this.post(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/blobs`, { blobs });
184
+ }
185
+ readBlobs(workspace, project, hashes) {
186
+ return this.post(`${TEAM_SETUP}/${seg(workspace)}/memory/${seg(project)}/blobs/read`, { hashes });
187
+ }
188
+ listValueAnswers(workspace) {
189
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/values`);
190
+ }
191
+ lookupValues(workspace, values) {
192
+ return this.post(`${TEAM_SETUP}/${seg(workspace)}/values`, { values });
193
+ }
194
+ answerValues(workspace, answers) {
195
+ return this.put(`${TEAM_SETUP}/${seg(workspace)}/values`, { answers });
196
+ }
197
+ fileMemoryPeople(workspace, people) {
198
+ return this.post(`${TEAM_SETUP}/${seg(workspace)}/people`, { people });
199
+ }
200
+ resolvePerson(workspace, personId) {
201
+ return this.getFresh(`${TEAM_SETUP}/${seg(workspace)}/people/${seg(personId)}`);
202
+ }
98
203
  invalidateCache(path) {
99
204
  if (path) {
100
205
  this.cache.invalidate(this.buildUrl(path).toString());
@@ -124,5 +229,6 @@ var SystemDoxApiClient = class {
124
229
  }
125
230
  };
126
231
  export {
127
- SystemDoxApiClient
232
+ SystemDoxApiClient,
233
+ SystemDoxApiError
128
234
  };
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Find a person's details in a team memory file (ADR-217 Amendment 2).
3
+ *
4
+ * The client's half of the one detector of specs/changes/team-setup/contract.md
5
+ * ("People"), held with its twin `backend/layers/common/common/person_values.py`
6
+ * to the shared vectors in `specs/changes/team-setup/person-values.json`
7
+ * (test/person-values.test.mjs here, backend/tests/test_person_values.py there).
8
+ * Change one and the other in the same pull request. It finds:
9
+ *
10
+ * - **a phone number**: an E.164 number (`+44 7700 900123`), or a national
11
+ * number of 10 or 11 digits starting with 0 in the usual groupings
12
+ * (`07700 900123`, `020 7946 0321`), normalised to E.164 (+44);
13
+ * - **an email address**: an RFC 5322 addr-spec in its dot-atom form, without
14
+ * comments or a quoted local part, normalised to lower case;
15
+ * - **a home-folder path**: `C:\Users\<name>`, `/Users/<name>` or
16
+ * `/home/<name>`, which the sync rewrites to `~`.
17
+ *
18
+ * Anything inside a person token, `` `<con_|ctc_|usr_…>` (<label>) ``, is
19
+ * skipped, so a file the sync rewrote is never found again. The patterns are
20
+ * the backend's, character for character; where Python's `\w` and `\d` are
21
+ * Unicode-aware, these spell the same classes out (`u` flag).
22
+ *
23
+ * `findSecrets` mirrors the backend's `secret.key-prefix` rule
24
+ * (common/authored_skills.py): the first characters of a key, even cut short,
25
+ * are a secret. The server's other secret rules refuse a whole key at upload
26
+ * (SYD-K005); a whole key also matches this prefix rule, so the client skips
27
+ * it either way.
28
+ *
29
+ * Pure: no disk, no network, no logging.
30
+ */
31
+ export declare const PHONE = "phone";
32
+ export declare const EMAIL = "email";
33
+ export declare const HOME_PATH = "home-path";
34
+ export type PersonValueKind = typeof PHONE | typeof EMAIL | typeof HOME_PATH;
35
+ /** The kinds a value can be answered for (openapi ValueKind); a home path is
36
+ * never answered, only rewritten. */
37
+ export declare const VALUE_KINDS: readonly ["phone", "email"];
38
+ export type ValueKind = (typeof VALUE_KINDS)[number];
39
+ /** The country a national number (leading 0) belongs to. */
40
+ export declare const NATIONAL_PREFIX = "+44";
41
+ export interface Finding {
42
+ kind: PersonValueKind;
43
+ /** As written in the file. */
44
+ value: string;
45
+ /** E.164 for a phone number, lower case for an email address; for a home
46
+ * path, the value itself (it is rewritten, never answered). */
47
+ normalised: string;
48
+ /** 1-based. */
49
+ line: number;
50
+ start: number;
51
+ end: number;
52
+ }
53
+ export interface SecretFinding {
54
+ kind: "key-prefix";
55
+ line: number;
56
+ }
57
+ /** openapi PersonId, unanchored: THE one copy of the pattern on the client.
58
+ * The person token below and `resolve_person`'s input check
59
+ * (team-setup-types PERSON_ID_RE) are both built from it, so a change to the
60
+ * contract's PersonId is this one line (and the backend's `_PERSON_TOKEN`). */
61
+ export declare const PERSON_ID = "(?:con|ctc|usr)_[0-9A-Za-z]{10,40}";
62
+ /** `con_…` (label), `ctc_…` (label), `usr_…` (label): a PersonId in
63
+ * backticks, with its role label after it. Skipped by the detector. */
64
+ export declare const PERSON_TOKEN = "`(?:con|ctc|usr)_[0-9A-Za-z]{10,40}`(?:[ \\t]*\\([^()\\n]*\\))?";
65
+ /**
66
+ * The form an answer is keyed by: E.164 for a phone number, lower case for an
67
+ * email address. Throws when `value` is not one of `kind` (the server answers
68
+ * SYD-K020 for the same value).
69
+ */
70
+ export declare function normalise(kind: string, value: string): string;
71
+ /** Every phone number, email address and home-folder path in `text`, in the
72
+ * order they appear, skipping person tokens. */
73
+ export declare function findPersonValues(text: string): Finding[];
74
+ /** `text` with every home-folder path found replaced by `~`: what the sync
75
+ * sends instead (`C:\Users\jbloggs\.claude` → `~\.claude`). */
76
+ export declare function rewriteHomePaths(text: string): string;
77
+ /** The kind and line of every key prefix in `text`; never the value. */
78
+ export declare function findSecrets(text: string): SecretFinding[];
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/lib/person-values.ts
4
+ var PHONE = "phone";
5
+ var EMAIL = "email";
6
+ var HOME_PATH = "home-path";
7
+ var VALUE_KINDS = [PHONE, EMAIL];
8
+ var NATIONAL_PREFIX = "+44";
9
+ var W = "[\\p{L}\\p{N}_]";
10
+ var D = "\\p{Nd}";
11
+ var WORD_OR = (extra) => `[\\p{L}\\p{N}_${extra}]`;
12
+ var PERSON_ID = "(?:con|ctc|usr)_[0-9A-Za-z]{10,40}";
13
+ var PERSON_TOKEN = `\`${PERSON_ID}\`(?:[ \\t]*\\([^()\\n]*\\))?`;
14
+ var BEFORE = `(?<!${WORD_OR("+.:/\\\\\\-")})(?<!${D}[ -])`;
15
+ var AFTER = `(?!${WORD_OR(":/\\\\")}|\\.${D}|[ -]?${D}|-${W})`;
16
+ var E164 = `${BEFORE}\\+${D}+(?:[ -]${D}+)*${AFTER}`;
17
+ var NATIONAL = `${BEFORE}0${D}*(?:[ -]${D}+)*${AFTER}`;
18
+ var GROUP_SIZES = /* @__PURE__ */ new Set([2, 3, 4, 5, 6]);
19
+ var ATEXT = "[A-Za-z0-9!#$%&'*+/=?^_{|}~\\-]";
20
+ 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])";
21
+ var HOME_PREFIX = "(?:[A-Za-z]:\\\\+Users\\\\+|[A-Za-z]:/+Users/+|/Users/|/home/)";
22
+ var HOME = `(?<!${WORD_OR(".~:/\\\\$}\\-")})${HOME_PREFIX}[^\\\\/\\s"'\`<>|:*?]+`;
23
+ 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,}`;
24
+ var global = (source, flags = "") => new RegExp(source, `gu${flags}`);
25
+ var whole = (source, flags = "") => new RegExp(`^(?:${source})$`, `u${flags}`);
26
+ function lineOf(text, start) {
27
+ let line = 1;
28
+ for (let i = text.indexOf("\n"); i !== -1 && i < start; i = text.indexOf("\n", i + 1)) line++;
29
+ return line;
30
+ }
31
+ function phoneShape(value) {
32
+ const groups = value.replace(/^\++/, "").split(/[ -]/);
33
+ const digits = [...groups.join("")];
34
+ if (value.startsWith("+")) return digits.length >= 8 && digits.length <= 15 && digits[0] !== "0";
35
+ if (digits.length < 10 || digits.length > 11) return false;
36
+ return groups.length === 1 || groups.slice(1).every((g) => GROUP_SIZES.has([...g].length));
37
+ }
38
+ function normalise(kind, value) {
39
+ const v = typeof value === "string" ? value.trim() : "";
40
+ if (kind === PHONE) {
41
+ if (!(whole(E164).test(v) || whole(NATIONAL).test(v)) || !phoneShape(v)) {
42
+ throw new Error("not a phone number");
43
+ }
44
+ const digits = v.replace(/[ -]/g, "");
45
+ return digits.startsWith("+") ? digits : NATIONAL_PREFIX + digits.slice(1);
46
+ }
47
+ if (kind === EMAIL) {
48
+ if (!whole(EMAIL_RE).test(v)) throw new Error("not an email address");
49
+ return v.toLowerCase();
50
+ }
51
+ throw new Error(`not a value kind: ${JSON.stringify(kind)}`);
52
+ }
53
+ function findPersonValues(text) {
54
+ if (typeof text !== "string" || !text) return [];
55
+ const skipped = [...text.matchAll(global(PERSON_TOKEN))].map((m) => [m.index, m.index + m[0].length]);
56
+ const found = [];
57
+ const add = (kind, m, normalised) => {
58
+ const start = m.index;
59
+ const end = start + m[0].length;
60
+ if (skipped.some(([s, e]) => s <= start && end <= e)) return;
61
+ if (found.some((f) => f.start < end && start < f.end)) return;
62
+ found.push({ kind, value: m[0], normalised, line: lineOf(text, start), start, end });
63
+ };
64
+ for (const m of text.matchAll(global(EMAIL_RE))) add(EMAIL, m, m[0].toLowerCase());
65
+ for (const source of [E164, NATIONAL]) {
66
+ for (const m of text.matchAll(global(source))) {
67
+ if (phoneShape(m[0])) add(PHONE, m, normalise(PHONE, m[0]));
68
+ }
69
+ }
70
+ for (const m of text.matchAll(global(HOME, "i"))) add(HOME_PATH, m, m[0]);
71
+ return found.sort((a, b) => a.start - b.start);
72
+ }
73
+ function rewriteHomePaths(text) {
74
+ let out = "";
75
+ let last = 0;
76
+ for (const f of findPersonValues(text)) {
77
+ if (f.kind !== HOME_PATH) continue;
78
+ out += text.slice(last, f.start) + "~";
79
+ last = f.end;
80
+ }
81
+ return out + text.slice(last);
82
+ }
83
+ function findSecrets(text) {
84
+ if (typeof text !== "string" || !text) return [];
85
+ return [...text.matchAll(global(KEY_PREFIX))].map((m) => ({ kind: "key-prefix", line: lineOf(text, m.index) }));
86
+ }
87
+ export {
88
+ EMAIL,
89
+ HOME_PATH,
90
+ NATIONAL_PREFIX,
91
+ PERSON_ID,
92
+ PERSON_TOKEN,
93
+ PHONE,
94
+ VALUE_KINDS,
95
+ findPersonValues,
96
+ findSecrets,
97
+ normalise,
98
+ rewriteHomePaths
99
+ };
@@ -63,6 +63,26 @@ export declare function isValidPath(path: string): boolean;
63
63
  export declare function frontmatterName(skillMd: string): string | undefined;
64
64
  /** The default install folder: personal skills, every project (ADR-216 §6 Q1). */
65
65
  export declare function defaultSkillDir(name: string): string;
66
+ export interface Walked {
67
+ files: string[];
68
+ /** Symlinks and junctions: never followed, never written through. */
69
+ links: string[];
70
+ /** Dot-entries a publish leaves out (.env, .vscode/…): never part of a skill. */
71
+ hidden: string[];
72
+ }
73
+ /** `skipHidden`: a publish leaves dot-entries out; an install must see them,
74
+ * or an edited dotfile a version ships would be overwritten unseen. */
75
+ export declare function walk(root: string, { skipHidden }: {
76
+ skipHidden: boolean;
77
+ }, dir?: string, out?: Walked): Promise<Walked>;
78
+ /**
79
+ * Read `paths` (relative, forward slashes) under `root` exactly as they are
80
+ * on disk, bytes decoded as UTF-8, held to the server's limits (ADR-216):
81
+ * at most MAX_FILES files, each text with no NUL and at most MAX_FILE_BYTES,
82
+ * all together at most MAX_TOTAL_BYTES. Throws naming the first file at
83
+ * fault. `what` names the thing in the message ("a skill", "the hooks part").
84
+ */
85
+ export declare function readTextFiles(root: string, paths: string[], what: string): Promise<SkillFileIn[]>;
66
86
  /**
67
87
  * Read a skill folder exactly as it is on disk (bytes decoded as UTF-8, never
68
88
  * re-typed), ready to publish. Throws naming the first file that cannot be
@@ -56,18 +56,9 @@ async function walk(root, { skipHidden }, dir = root, out = { files: [], links:
56
56
  out.files.sort();
57
57
  return out;
58
58
  }
59
- async function readSkillDirectory(directory) {
60
- const root = resolve(directory);
61
- if (!existsSync(join(root, ENTRY_FILE))) {
62
- throw new Error(`${root} has no ${ENTRY_FILE}: a skill folder holds SKILL.md at its root`);
63
- }
64
- const walked = await walk(root, { skipHidden: true });
65
- if (walked.links.length) {
66
- throw new Error(`${walked.links.join(", ")}: links are not followed; copy the file into the skill folder`);
67
- }
68
- const paths = walked.files;
59
+ async function readTextFiles(root, paths, what) {
69
60
  if (paths.length > MAX_FILES) {
70
- throw new Error(`${root} holds ${paths.length} files: a skill holds at most ${MAX_FILES}`);
61
+ throw new Error(`${root} holds ${paths.length} files: ${what} holds at most ${MAX_FILES}`);
71
62
  }
72
63
  const decoder = new TextDecoder("utf-8", { fatal: true });
73
64
  const files = [];
@@ -77,7 +68,7 @@ async function readSkillDirectory(directory) {
77
68
  throw new Error(`${path}: file names may use letters, digits, ".", "_" and "-" only`);
78
69
  }
79
70
  const raw = await readFile(join(root, ...path.split("/")));
80
- if (raw.includes(0)) throw new Error(`${path} is binary: a skill holds text files only`);
71
+ if (raw.includes(0)) throw new Error(`${path} is binary: ${what} holds text files only`);
81
72
  if (raw.length > MAX_FILE_BYTES) {
82
73
  throw new Error(`${path} is ${raw.length} bytes: a file holds at most ${MAX_FILE_BYTES}`);
83
74
  }
@@ -93,6 +84,18 @@ async function readSkillDirectory(directory) {
93
84
  }
94
85
  files.push({ path, content });
95
86
  }
87
+ return files;
88
+ }
89
+ async function readSkillDirectory(directory) {
90
+ const root = resolve(directory);
91
+ if (!existsSync(join(root, ENTRY_FILE))) {
92
+ throw new Error(`${root} has no ${ENTRY_FILE}: a skill folder holds SKILL.md at its root`);
93
+ }
94
+ const walked = await walk(root, { skipHidden: true });
95
+ if (walked.links.length) {
96
+ throw new Error(`${walked.links.join(", ")}: links are not followed; copy the file into the skill folder`);
97
+ }
98
+ const files = await readTextFiles(root, walked.files, "a skill");
96
99
  const skillMd = files.find((f) => f.path === ENTRY_FILE);
97
100
  const name = frontmatterName(skillMd.content);
98
101
  if (!name) throw new Error(`${ENTRY_FILE} has no frontmatter name`);
@@ -205,5 +208,7 @@ export {
205
208
  isValidName,
206
209
  isValidPath,
207
210
  readSkillDirectory,
208
- sha256
211
+ readTextFiles,
212
+ sha256,
213
+ walk
209
214
  };