@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.
- package/README.md +46 -0
- package/dist/cli.js +793 -110
- package/dist/http.js +356 -69
- package/dist/index.js +356 -69
- package/dist/install-skill.js +117 -12
- package/dist/lib/api-client.d.ts +81 -0
- package/dist/lib/api-client.js +119 -13
- package/dist/lib/person-values.d.ts +78 -0
- package/dist/lib/person-values.js +99 -0
- package/dist/lib/skills.d.ts +20 -0
- package/dist/lib/skills.js +18 -13
- package/dist/lib/team-setup-files.d.ts +88 -0
- package/dist/lib/team-setup-files.js +191 -0
- package/dist/lib/team-setup-types.d.ts +153 -0
- package/dist/plugin-path.d.ts +55 -0
- package/dist/plugin-path.js +544 -0
- package/dist/tools/skills.js +15 -12
- package/dist/tools/team-setup.d.ts +33 -0
- package/dist/tools/team-setup.js +242 -0
- package/package.json +1 -1
package/dist/install-skill.js
CHANGED
|
@@ -34,6 +34,28 @@ var ResponseCache = class {
|
|
|
34
34
|
};
|
|
35
35
|
|
|
36
36
|
// src/lib/api-client.ts
|
|
37
|
+
var SystemDoxApiError = class extends Error {
|
|
38
|
+
constructor(status, 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());
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -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;
|
package/dist/lib/api-client.js
CHANGED
|
@@ -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
|
+
};
|
package/dist/lib/skills.d.ts
CHANGED
|
@@ -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
|
package/dist/lib/skills.js
CHANGED
|
@@ -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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
211
|
+
readTextFiles,
|
|
212
|
+
sha256,
|
|
213
|
+
walk
|
|
209
214
|
};
|