@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/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
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The files the team setup keeps on a member's computer (ADR-217,
|
|
3
|
+
* specs/changes/team-setup/contract.md "Files on a member's computer"):
|
|
4
|
+
*
|
|
5
|
+
* - `~/.claude/systemdox/credentials.json`: `{ "<workspace>": { "apiKey" } }`,
|
|
6
|
+
* readable by the member alone (mode 600; on Windows an ACL for the member
|
|
7
|
+
* alone);
|
|
8
|
+
* - `~/.claude/systemdox/<workspace>/state.json`: the installed versions and
|
|
9
|
+
* hashes, each memory project's base manifest and version, the accepted
|
|
10
|
+
* SHA-256s. Never a value and never an answer about a person;
|
|
11
|
+
* - `~/.claude/systemdox/<workspace>/plugin/<n>/`: plugin version n, unpacked.
|
|
12
|
+
*
|
|
13
|
+
* Every path starts at `homeDir()`, the one place the home folder is read, so
|
|
14
|
+
* a test points HOME (USERPROFILE on Windows) at a temporary folder.
|
|
15
|
+
*
|
|
16
|
+
* Also here: the API key's order (SYSTEMDOX_API_KEY, then credentials.json for
|
|
17
|
+
* the workspace) and `marketplace-auth`, the marketplace's `headersHelper`,
|
|
18
|
+
* which reads only these files and the environment.
|
|
19
|
+
*/
|
|
20
|
+
/** A workspace is a tenant slug (openapi Slug): it names a folder, so nothing
|
|
21
|
+
* else is accepted. */
|
|
22
|
+
export declare function isWorkspace(value: unknown): value is string;
|
|
23
|
+
/** The member's home folder: `os.homedir()`, which reads HOME (USERPROFILE on
|
|
24
|
+
* Windows) on every call. */
|
|
25
|
+
export declare function homeDir(): string;
|
|
26
|
+
export declare function systemdoxDir(): string;
|
|
27
|
+
export declare function credentialsPath(): string;
|
|
28
|
+
export declare function workspaceDir(workspace: string): string;
|
|
29
|
+
export declare function statePath(workspace: string): string;
|
|
30
|
+
export declare function pluginRoot(workspace: string): string;
|
|
31
|
+
export declare function pluginDir(workspace: string, version: number): string;
|
|
32
|
+
export type Credentials = Record<string, {
|
|
33
|
+
apiKey: string;
|
|
34
|
+
}>;
|
|
35
|
+
export declare function readCredentials(): Promise<Credentials>;
|
|
36
|
+
/** Restrict `path` to the member alone: mode 600, and on Windows an ACL with
|
|
37
|
+
* inheritance removed and one full-control entry for the current user. */
|
|
38
|
+
export declare function restrictToMember(path: string): Promise<void>;
|
|
39
|
+
/** Save the key for `workspace` in credentials.json, readable by the member alone. */
|
|
40
|
+
export declare function writeCredentials(workspace: string, apiKey: string): Promise<void>;
|
|
41
|
+
/** The key for `workspace`, in the contract's order: SYSTEMDOX_API_KEY, then
|
|
42
|
+
* credentials.json for the workspace. Undefined when neither has one. */
|
|
43
|
+
export declare function apiKeyFor(workspace: string, env?: NodeJS.ProcessEnv): Promise<string | undefined>;
|
|
44
|
+
/** SYSTEMDOX_API_URL or the default, https only (throws otherwise). */
|
|
45
|
+
export declare function apiUrlFrom(env?: NodeJS.ProcessEnv): string;
|
|
46
|
+
export interface TeamSetupState {
|
|
47
|
+
/** Plugin versions unpacked under plugin/<n>/, keyed by n: the zip's SHA-256. */
|
|
48
|
+
plugin?: Record<string, {
|
|
49
|
+
sha256: string;
|
|
50
|
+
installedAt: string;
|
|
51
|
+
}>;
|
|
52
|
+
/** The team instructions as last installed (`setup`). */
|
|
53
|
+
instructions?: {
|
|
54
|
+
version: number;
|
|
55
|
+
sha256: string;
|
|
56
|
+
};
|
|
57
|
+
/** Each memory project's base: the version synced last and its manifest. */
|
|
58
|
+
memory?: Record<string, {
|
|
59
|
+
version: number;
|
|
60
|
+
manifest: Record<string, string>;
|
|
61
|
+
}>;
|
|
62
|
+
/** The SHA-256s the member accepted (`setup --yes`). */
|
|
63
|
+
accepted?: string[];
|
|
64
|
+
}
|
|
65
|
+
export declare function readState(workspace: string): Promise<TeamSetupState>;
|
|
66
|
+
export declare function writeState(workspace: string, state: TeamSetupState): Promise<void>;
|
|
67
|
+
/** Read, change and write state.json in one step. */
|
|
68
|
+
export declare function updateState(workspace: string, change: (state: TeamSetupState) => void): Promise<TeamSetupState>;
|
|
69
|
+
/** Remove a folder this code made (a temporary unpack). */
|
|
70
|
+
export declare function removeFolder(path: string): Promise<void>;
|
|
71
|
+
/**
|
|
72
|
+
* The workspace a marketplace URL is for, or undefined when the key must not
|
|
73
|
+
* go there: the URL must be https, on the origin of the API URL, with a path
|
|
74
|
+
* `<api path>/v1/team-setup/<workspace>/marketplace.json`.
|
|
75
|
+
*/
|
|
76
|
+
export declare function marketplaceWorkspace(marketplaceUrl: string, apiUrl: string): string | undefined;
|
|
77
|
+
export interface CommandIo {
|
|
78
|
+
out: (line: string) => void;
|
|
79
|
+
err: (line: string) => void;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* `marketplace-auth`: Claude Code runs it with CLAUDE_CODE_MARKETPLACE_URL
|
|
83
|
+
* set. On a SystemDox marketplace URL it prints exactly
|
|
84
|
+
* `{"Authorization":"Bearer <key>"}` for that workspace and returns 0;
|
|
85
|
+
* otherwise nothing on stdout, the URL on stderr, and 1. No network call,
|
|
86
|
+
* nothing written.
|
|
87
|
+
*/
|
|
88
|
+
export declare function runMarketplaceAuth(io: CommandIo, env?: NodeJS.ProcessEnv): Promise<number>;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/lib/team-setup-files.ts
|
|
4
|
+
import { execFile } from "child_process";
|
|
5
|
+
import { chmod, mkdir, readFile, rename, rm, writeFile } from "fs/promises";
|
|
6
|
+
import { homedir, userInfo } from "os";
|
|
7
|
+
import { dirname, join } from "path";
|
|
8
|
+
import { promisify } from "util";
|
|
9
|
+
|
|
10
|
+
// src/lib/config.ts
|
|
11
|
+
var API_URL_DEFAULT = "https://api.puglieseweb.com/systemdox";
|
|
12
|
+
function requireHttpsApiUrl(apiUrl) {
|
|
13
|
+
let parsed;
|
|
14
|
+
try {
|
|
15
|
+
parsed = new URL(apiUrl);
|
|
16
|
+
} catch {
|
|
17
|
+
throw new Error(
|
|
18
|
+
`SYSTEMDOX_API_URL is not a valid URL: ${apiUrl}`
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
if (parsed.protocol !== "https:") {
|
|
22
|
+
throw new Error(
|
|
23
|
+
`SYSTEMDOX_API_URL must use https:// \u2014 refusing to send the API key over ${parsed.protocol}// in cleartext. Got: ${apiUrl}`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
return apiUrl;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// src/lib/team-setup-files.ts
|
|
30
|
+
var SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
31
|
+
function isWorkspace(value) {
|
|
32
|
+
return typeof value === "string" && value.length <= 64 && SLUG_RE.test(value);
|
|
33
|
+
}
|
|
34
|
+
function homeDir() {
|
|
35
|
+
return homedir();
|
|
36
|
+
}
|
|
37
|
+
function systemdoxDir() {
|
|
38
|
+
return join(homeDir(), ".claude", "systemdox");
|
|
39
|
+
}
|
|
40
|
+
function credentialsPath() {
|
|
41
|
+
return join(systemdoxDir(), "credentials.json");
|
|
42
|
+
}
|
|
43
|
+
function workspaceDir(workspace) {
|
|
44
|
+
if (!isWorkspace(workspace)) throw new Error(`${JSON.stringify(workspace)} is not a workspace name`);
|
|
45
|
+
return join(systemdoxDir(), workspace);
|
|
46
|
+
}
|
|
47
|
+
function statePath(workspace) {
|
|
48
|
+
return join(workspaceDir(workspace), "state.json");
|
|
49
|
+
}
|
|
50
|
+
function pluginRoot(workspace) {
|
|
51
|
+
return join(workspaceDir(workspace), "plugin");
|
|
52
|
+
}
|
|
53
|
+
function pluginDir(workspace, version) {
|
|
54
|
+
return join(pluginRoot(workspace), String(version));
|
|
55
|
+
}
|
|
56
|
+
async function readJson(path, empty) {
|
|
57
|
+
let text;
|
|
58
|
+
try {
|
|
59
|
+
text = await readFile(path, "utf8");
|
|
60
|
+
} catch (e) {
|
|
61
|
+
if (e.code === "ENOENT") return empty;
|
|
62
|
+
throw e;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
return JSON.parse(text);
|
|
66
|
+
} catch {
|
|
67
|
+
throw new Error(`${path} is not valid JSON: fix or remove it, then run this again`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function writeJsonAtomically(path, value, mode) {
|
|
71
|
+
await mkdir(dirname(path), { recursive: true });
|
|
72
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
73
|
+
await writeFile(tmp, JSON.stringify(value, null, 2) + "\n", mode === void 0 ? "utf8" : { encoding: "utf8", mode });
|
|
74
|
+
await rename(tmp, path);
|
|
75
|
+
}
|
|
76
|
+
async function readCredentials() {
|
|
77
|
+
return readJson(credentialsPath(), {});
|
|
78
|
+
}
|
|
79
|
+
var run = promisify(execFile);
|
|
80
|
+
async function restrictToMember(path) {
|
|
81
|
+
await chmod(path, 384);
|
|
82
|
+
if (process.platform === "win32") {
|
|
83
|
+
await run("icacls", [path, "/inheritance:r", "/grant:r", `${userInfo().username}:F`], { windowsHide: true });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function writeCredentials(workspace, apiKey) {
|
|
87
|
+
if (!isWorkspace(workspace)) throw new Error(`${JSON.stringify(workspace)} is not a workspace name`);
|
|
88
|
+
if (!apiKey) throw new Error("an API key is required");
|
|
89
|
+
const path = credentialsPath();
|
|
90
|
+
const credentials = await readCredentials();
|
|
91
|
+
credentials[workspace] = { apiKey };
|
|
92
|
+
await writeJsonAtomically(path, credentials, 384);
|
|
93
|
+
await restrictToMember(path);
|
|
94
|
+
}
|
|
95
|
+
async function apiKeyFor(workspace, env = process.env) {
|
|
96
|
+
if (env.SYSTEMDOX_API_KEY) return env.SYSTEMDOX_API_KEY;
|
|
97
|
+
const entry = (await readCredentials())[workspace];
|
|
98
|
+
return typeof entry?.apiKey === "string" && entry.apiKey ? entry.apiKey : void 0;
|
|
99
|
+
}
|
|
100
|
+
function apiUrlFrom(env = process.env) {
|
|
101
|
+
return requireHttpsApiUrl(env.SYSTEMDOX_API_URL || API_URL_DEFAULT);
|
|
102
|
+
}
|
|
103
|
+
async function readState(workspace) {
|
|
104
|
+
return readJson(statePath(workspace), {});
|
|
105
|
+
}
|
|
106
|
+
async function writeState(workspace, state) {
|
|
107
|
+
await writeJsonAtomically(statePath(workspace), state);
|
|
108
|
+
}
|
|
109
|
+
async function updateState(workspace, change) {
|
|
110
|
+
const state = await readState(workspace);
|
|
111
|
+
change(state);
|
|
112
|
+
await writeState(workspace, state);
|
|
113
|
+
return state;
|
|
114
|
+
}
|
|
115
|
+
async function removeFolder(path) {
|
|
116
|
+
await rm(path, { recursive: true, force: true });
|
|
117
|
+
}
|
|
118
|
+
function escapeRegExp(s) {
|
|
119
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
120
|
+
}
|
|
121
|
+
function marketplaceWorkspace(marketplaceUrl, apiUrl) {
|
|
122
|
+
let url;
|
|
123
|
+
let api;
|
|
124
|
+
try {
|
|
125
|
+
url = new URL(marketplaceUrl);
|
|
126
|
+
api = new URL(requireHttpsApiUrl(apiUrl));
|
|
127
|
+
} catch {
|
|
128
|
+
return void 0;
|
|
129
|
+
}
|
|
130
|
+
if (url.protocol !== "https:" || url.origin !== api.origin || url.username || url.password) return void 0;
|
|
131
|
+
const base = api.pathname.replace(/\/+$/, "");
|
|
132
|
+
const m = new RegExp(`^${escapeRegExp(base)}/v1/team-setup/([^/]+)/marketplace\\.json$`).exec(url.pathname);
|
|
133
|
+
return m && isWorkspace(m[1]) ? m[1] : void 0;
|
|
134
|
+
}
|
|
135
|
+
async function runMarketplaceAuth(io, env = process.env) {
|
|
136
|
+
const url = env.CLAUDE_CODE_MARKETPLACE_URL;
|
|
137
|
+
if (!url) {
|
|
138
|
+
io.err("marketplace-auth: CLAUDE_CODE_MARKETPLACE_URL is not set; Claude Code sets it when it fetches a marketplace.");
|
|
139
|
+
return 1;
|
|
140
|
+
}
|
|
141
|
+
let apiUrl;
|
|
142
|
+
try {
|
|
143
|
+
apiUrl = apiUrlFrom(env);
|
|
144
|
+
} catch (e) {
|
|
145
|
+
io.err(`marketplace-auth: ${e.message}`);
|
|
146
|
+
return 1;
|
|
147
|
+
}
|
|
148
|
+
const workspace = marketplaceWorkspace(url, apiUrl);
|
|
149
|
+
if (!workspace) {
|
|
150
|
+
io.err(
|
|
151
|
+
`marketplace-auth: refusing to give a SystemDox key for ${url}: it is not a SystemDox team marketplace on ${apiUrl}`
|
|
152
|
+
);
|
|
153
|
+
return 1;
|
|
154
|
+
}
|
|
155
|
+
let key;
|
|
156
|
+
try {
|
|
157
|
+
key = await apiKeyFor(workspace, env);
|
|
158
|
+
} catch (e) {
|
|
159
|
+
io.err(`marketplace-auth: ${e.message}`);
|
|
160
|
+
return 1;
|
|
161
|
+
}
|
|
162
|
+
if (!key) {
|
|
163
|
+
io.err(
|
|
164
|
+
`marketplace-auth: no SystemDox key for ${workspace}. Set SYSTEMDOX_API_KEY, or run npx -y @systemdox/mcp-server setup.`
|
|
165
|
+
);
|
|
166
|
+
return 1;
|
|
167
|
+
}
|
|
168
|
+
io.out(JSON.stringify({ Authorization: `Bearer ${key}` }));
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
export {
|
|
172
|
+
apiKeyFor,
|
|
173
|
+
apiUrlFrom,
|
|
174
|
+
credentialsPath,
|
|
175
|
+
homeDir,
|
|
176
|
+
isWorkspace,
|
|
177
|
+
marketplaceWorkspace,
|
|
178
|
+
pluginDir,
|
|
179
|
+
pluginRoot,
|
|
180
|
+
readCredentials,
|
|
181
|
+
readState,
|
|
182
|
+
removeFolder,
|
|
183
|
+
restrictToMember,
|
|
184
|
+
runMarketplaceAuth,
|
|
185
|
+
statePath,
|
|
186
|
+
systemdoxDir,
|
|
187
|
+
updateState,
|
|
188
|
+
workspaceDir,
|
|
189
|
+
writeCredentials,
|
|
190
|
+
writeState
|
|
191
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The team-setup shapes of specs/changes/team-setup/openapi.yaml
|
|
3
|
+
* (components.schemas), as the client reads and sends them, and the two
|
|
4
|
+
* enumerations it checks locally (the parts, the PersonId pattern).
|
|
5
|
+
*/
|
|
6
|
+
export declare const PARTS: readonly ["hooks", "mcp", "agents", "commands", "instructions"];
|
|
7
|
+
export type PartName = (typeof PARTS)[number];
|
|
8
|
+
export interface FileIn {
|
|
9
|
+
path: string;
|
|
10
|
+
content: string;
|
|
11
|
+
}
|
|
12
|
+
export interface FileMeta {
|
|
13
|
+
path: string;
|
|
14
|
+
bytes: number;
|
|
15
|
+
sha256: string;
|
|
16
|
+
}
|
|
17
|
+
export interface PartVersionSummary {
|
|
18
|
+
part: PartName;
|
|
19
|
+
version: number;
|
|
20
|
+
publishedAt: string;
|
|
21
|
+
publishedBy: string;
|
|
22
|
+
files: FileMeta[];
|
|
23
|
+
}
|
|
24
|
+
export interface PartDetail {
|
|
25
|
+
part: PartName;
|
|
26
|
+
latestVersion: number;
|
|
27
|
+
versions: PartVersionSummary[];
|
|
28
|
+
}
|
|
29
|
+
export interface PartVersionFiles extends Omit<PartVersionSummary, "files"> {
|
|
30
|
+
files: (FileMeta & {
|
|
31
|
+
content: string;
|
|
32
|
+
})[];
|
|
33
|
+
}
|
|
34
|
+
export interface PartPublished extends PartVersionSummary {
|
|
35
|
+
/** Absent for `instructions`. */
|
|
36
|
+
pluginVersion?: number;
|
|
37
|
+
/** `mcp` only. */
|
|
38
|
+
replacedValues?: {
|
|
39
|
+
server: string;
|
|
40
|
+
field: "env" | "headers";
|
|
41
|
+
name: string;
|
|
42
|
+
}[];
|
|
43
|
+
}
|
|
44
|
+
export interface PluginVersion {
|
|
45
|
+
version: number;
|
|
46
|
+
sha256: string;
|
|
47
|
+
bytes: number;
|
|
48
|
+
createdAt: string;
|
|
49
|
+
createdBy?: string;
|
|
50
|
+
parts: Record<string, number>;
|
|
51
|
+
skills: Record<string, number>;
|
|
52
|
+
hookCommands?: string[];
|
|
53
|
+
mcpServers?: {
|
|
54
|
+
name: string;
|
|
55
|
+
command?: string;
|
|
56
|
+
args?: string[];
|
|
57
|
+
url?: string;
|
|
58
|
+
}[];
|
|
59
|
+
command: string;
|
|
60
|
+
}
|
|
61
|
+
export interface Marketplace {
|
|
62
|
+
name: string;
|
|
63
|
+
owner: {
|
|
64
|
+
name: string;
|
|
65
|
+
};
|
|
66
|
+
plugins: {
|
|
67
|
+
name: string;
|
|
68
|
+
version: string;
|
|
69
|
+
description: string;
|
|
70
|
+
source: {
|
|
71
|
+
source: "command";
|
|
72
|
+
command: string;
|
|
73
|
+
timeout: number;
|
|
74
|
+
};
|
|
75
|
+
}[];
|
|
76
|
+
}
|
|
77
|
+
export interface MemoryProjectSummary {
|
|
78
|
+
project: string;
|
|
79
|
+
latestVersion: number;
|
|
80
|
+
fileCount: number;
|
|
81
|
+
bytes: number;
|
|
82
|
+
updatedAt: string;
|
|
83
|
+
spaceId: string;
|
|
84
|
+
}
|
|
85
|
+
export interface TeamSetup {
|
|
86
|
+
workspace: string;
|
|
87
|
+
minClaudeCodeVersion: string;
|
|
88
|
+
marketplaceUrl: string;
|
|
89
|
+
plugin?: PluginVersion;
|
|
90
|
+
parts: Record<string, number>;
|
|
91
|
+
skills: string[];
|
|
92
|
+
memory: MemoryProjectSummary[];
|
|
93
|
+
}
|
|
94
|
+
/** `{path: sha256}`. */
|
|
95
|
+
export type Manifest = Record<string, string>;
|
|
96
|
+
export interface MemoryVersion {
|
|
97
|
+
project: string;
|
|
98
|
+
version: number;
|
|
99
|
+
createdAt: string;
|
|
100
|
+
createdBy?: string;
|
|
101
|
+
files: Record<string, {
|
|
102
|
+
sha256: string;
|
|
103
|
+
bytes: number;
|
|
104
|
+
syncedBy: string;
|
|
105
|
+
syncedAt: string;
|
|
106
|
+
}>;
|
|
107
|
+
}
|
|
108
|
+
export interface BlobRefusal {
|
|
109
|
+
sha256: string;
|
|
110
|
+
code: string;
|
|
111
|
+
detail: string;
|
|
112
|
+
/** SYD-K014 only: each finding's kind and line, never the value. */
|
|
113
|
+
personalData?: {
|
|
114
|
+
kind: "phone" | "email" | "home-path";
|
|
115
|
+
line: number;
|
|
116
|
+
}[];
|
|
117
|
+
}
|
|
118
|
+
export interface FoundValue {
|
|
119
|
+
kind: "phone" | "email";
|
|
120
|
+
value: string;
|
|
121
|
+
}
|
|
122
|
+
export interface ValueAnswer extends FoundValue {
|
|
123
|
+
answer: "ours" | "member" | "person" | "unanswered";
|
|
124
|
+
personId?: string;
|
|
125
|
+
label?: string;
|
|
126
|
+
}
|
|
127
|
+
export interface ValueAnswerListed {
|
|
128
|
+
kind: "phone" | "email";
|
|
129
|
+
answer: "ours" | "member" | "person";
|
|
130
|
+
personId?: string;
|
|
131
|
+
label?: string;
|
|
132
|
+
answeredBy: string;
|
|
133
|
+
answeredAt: string;
|
|
134
|
+
}
|
|
135
|
+
export interface ValueAnswerResults {
|
|
136
|
+
stored: ValueAnswer[];
|
|
137
|
+
refused: {
|
|
138
|
+
index: number;
|
|
139
|
+
code: string;
|
|
140
|
+
detail: string;
|
|
141
|
+
}[];
|
|
142
|
+
}
|
|
143
|
+
export interface Person {
|
|
144
|
+
personId: string;
|
|
145
|
+
kind: "contact" | "staff-contact" | "member";
|
|
146
|
+
name?: string;
|
|
147
|
+
email?: string;
|
|
148
|
+
phone?: string;
|
|
149
|
+
label?: string;
|
|
150
|
+
mergedInto?: string;
|
|
151
|
+
}
|
|
152
|
+
/** openapi PersonId. */
|
|
153
|
+
export declare const PERSON_ID_RE: RegExp;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `systemdox-mcp-server plugin-path <workspace> --version <n> --sha256 <hash>`
|
|
3
|
+
* (ADR-217 Amendment 1, specs/changes/team-setup/contract.md "plugin-path").
|
|
4
|
+
*
|
|
5
|
+
* The team marketplace's plugin entry is this command. Claude Code runs it on
|
|
6
|
+
* install, on update and once per session while the plugin is enabled, and
|
|
7
|
+
* takes the folder it prints as the plugin. So the usual run makes no network
|
|
8
|
+
* call: a version already unpacked, whose hash state.json records, is printed
|
|
9
|
+
* as it is. Otherwise the zip is downloaded with the member's key, refused
|
|
10
|
+
* unless its SHA-256 is <hash>, checked entry by entry BEFORE anything is
|
|
11
|
+
* written, unpacked into a temporary folder beside plugin/<n>/ and renamed
|
|
12
|
+
* into place; then the hash is recorded.
|
|
13
|
+
*
|
|
14
|
+
* Stdout carries only the folder; everything else goes to stderr.
|
|
15
|
+
* Exit codes: 0 the folder is printed; 1 usage, no key, or the API refused;
|
|
16
|
+
* 2 the SHA-256 is not <hash>, or an entry was refused.
|
|
17
|
+
*/
|
|
18
|
+
import { SystemDoxApiClient } from "./lib/api-client.js";
|
|
19
|
+
import { type CommandIo } from "./lib/team-setup-files.js";
|
|
20
|
+
export declare const PLUGIN_PATH_USAGE = "Usage: systemdox-mcp-server plugin-path <workspace> --version <n> --sha256 <hash>";
|
|
21
|
+
/** The most a plugin may take once unpacked. */
|
|
22
|
+
export declare const MAX_UNPACKED_BYTES: number;
|
|
23
|
+
export interface PluginPathArgs {
|
|
24
|
+
workspace: string;
|
|
25
|
+
version: number;
|
|
26
|
+
sha256: string;
|
|
27
|
+
}
|
|
28
|
+
export declare function parsePluginPathArgs(argv: string[]): PluginPathArgs;
|
|
29
|
+
/** A download or an entry refused: exit 2, nothing unpacked. */
|
|
30
|
+
export declare class PluginRefused extends Error {
|
|
31
|
+
}
|
|
32
|
+
export interface ZipEntry {
|
|
33
|
+
name: string;
|
|
34
|
+
directory: boolean;
|
|
35
|
+
/** Unix permission bits, when the zip was made on Unix. */
|
|
36
|
+
mode: number | undefined;
|
|
37
|
+
method: number;
|
|
38
|
+
compressedSize: number;
|
|
39
|
+
size: number;
|
|
40
|
+
/** Offset of the entry's local header. */
|
|
41
|
+
offset: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The zip's central directory, every entry checked: none may be absolute,
|
|
45
|
+
* contain `..`, be a link, or take the unpacked size past MAX_UNPACKED_BYTES.
|
|
46
|
+
* Throws PluginRefused naming the first entry at fault.
|
|
47
|
+
*/
|
|
48
|
+
export declare function readZipEntries(zip: Buffer): ZipEntry[];
|
|
49
|
+
/** Unpack `zip` into `dest`, which must not exist yet. Every entry is checked
|
|
50
|
+
* before the first is written. */
|
|
51
|
+
export declare function unpackZip(zip: Buffer, dest: string): Promise<number>;
|
|
52
|
+
export interface PluginPathIo extends CommandIo {
|
|
53
|
+
client?: Pick<SystemDoxApiClient, "getPluginZip">;
|
|
54
|
+
}
|
|
55
|
+
export declare function runPluginPath(argv: string[], io: PluginPathIo): Promise<number>;
|