unity-mcp-cli 0.83.1 → 0.84.0

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.
@@ -0,0 +1,251 @@
1
+ // Copyright (c) 2024 Ivan Murzak. All rights reserved.
2
+ // Licensed under the Apache License, Version 2.0.
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
5
+ import { CLOUD_SERVER_BASE_URL } from './config.js';
6
+ import { deriveProjectPin } from './port.js';
7
+ import { agentRegistry, MCP_SERVER_NAME } from './agents.js';
8
+ import { writeProjectMarker } from './project-marker.js';
9
+ /**
10
+ * Agent-driven enrollment (D13): redeem a one-time enrollment code — minted by the server's
11
+ * `enroll_engine_plugin` tool from an already-authorized agent session — for a plugin credential,
12
+ * with NO second browser hop. The redeemed credential is planted in the shared machine store, the
13
+ * server target it was minted for is recorded in the project marker, and the D14 project pin is
14
+ * upserted into any existing project-local agent config so the plugin boots pointed at the right
15
+ * hub. Codes are burned server-side on first redeem attempt; a spent/invalid code yields a uniform
16
+ * error surfaced here as an actionable message.
17
+ */
18
+ /** Raised on any enrollment-redeem failure. Carries the HTTP status when one was received. */
19
+ export class EnrollmentError extends Error {
20
+ constructor(message, status) {
21
+ super(message);
22
+ this.name = 'EnrollmentError';
23
+ this.status = status;
24
+ }
25
+ }
26
+ function nonEmptyString(value) {
27
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
28
+ }
29
+ function numberOrUndefined(value) {
30
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
31
+ }
32
+ /**
33
+ * Normalize the redeem response. The Authorization Server's JSON key casing is not re-derivable
34
+ * from this repo (the AS lives in a separate service), so both snake_case and camelCase variants
35
+ * of every field are accepted defensively; `expires_in` seconds are converted to an absolute
36
+ * `expiresAt` ISO timestamp when no explicit `expires_at` is present.
37
+ */
38
+ export function normalizeRedeemResponse(data) {
39
+ const accessToken = nonEmptyString(data.access_token) ?? nonEmptyString(data.accessToken);
40
+ const refreshToken = nonEmptyString(data.refresh_token) ?? nonEmptyString(data.refreshToken);
41
+ const serverTarget = nonEmptyString(data.server_target) ??
42
+ nonEmptyString(data.serverTarget) ??
43
+ nonEmptyString(data.server_target_url) ??
44
+ nonEmptyString(data.serverTargetUrl);
45
+ const subject = nonEmptyString(data.subject) ?? nonEmptyString(data.sub);
46
+ let expiresAt = nonEmptyString(data.expires_at) ?? nonEmptyString(data.expiresAt);
47
+ const expiresIn = numberOrUndefined(data.expires_in) ?? numberOrUndefined(data.expiresIn);
48
+ if (!expiresAt && expiresIn !== undefined) {
49
+ expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString();
50
+ }
51
+ return { accessToken, refreshToken, expiresAt, serverTarget, subject };
52
+ }
53
+ /**
54
+ * Redeem an enrollment code against `POST <baseUrl>/api/auth/enroll/redeem` with body
55
+ * `{enroll_code}`. The code travels only in the request BODY (never a query string). A non-2xx
56
+ * response is surfaced as an actionable `EnrollmentError` (invalid/expired/already-used codes all
57
+ * return a uniform server error; burn-on-first-attempt is server-side).
58
+ */
59
+ export async function redeemEnrollmentCode(code, opts = {}) {
60
+ const baseUrl = opts.baseUrl ?? CLOUD_SERVER_BASE_URL;
61
+ const doFetch = opts.fetchImpl ?? fetch;
62
+ const url = `${baseUrl}/api/auth/enroll/redeem`;
63
+ const controller = new AbortController();
64
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? 30000);
65
+ let response;
66
+ try {
67
+ response = await doFetch(url, {
68
+ method: 'POST',
69
+ headers: { 'Content-Type': 'application/json' },
70
+ body: JSON.stringify({ enroll_code: code }),
71
+ signal: controller.signal,
72
+ });
73
+ }
74
+ catch (err) {
75
+ throw new EnrollmentError(`Could not reach the enrollment server at ${url}: ${err instanceof Error ? err.message : String(err)}`);
76
+ }
77
+ finally {
78
+ clearTimeout(timer);
79
+ }
80
+ if (!response.ok) {
81
+ throw new EnrollmentError(`Enrollment failed (HTTP ${response.status}). The enrollment code may be invalid, expired, ` +
82
+ `or already used — ask the agent to issue a fresh code and try again.`, response.status);
83
+ }
84
+ let data;
85
+ try {
86
+ data = (await response.json());
87
+ }
88
+ catch {
89
+ throw new EnrollmentError('Enrollment server returned a malformed (non-JSON) response.');
90
+ }
91
+ const credential = normalizeRedeemResponse(data);
92
+ if (!credential.accessToken) {
93
+ throw new EnrollmentError('Enrollment response did not contain an access token.');
94
+ }
95
+ return credential;
96
+ }
97
+ // ---------------------------------------------------------------------------
98
+ // Enrollment code resolution (--enroll <code> vs --enroll-stdin)
99
+ // ---------------------------------------------------------------------------
100
+ /**
101
+ * Resolve the enrollment code from `--enroll <code>` (argv) or `--enroll-stdin` (stdin), enforcing
102
+ * mutual exclusion. `--enroll-stdin` reads via the injected `readStdin` so the code NEVER lands in
103
+ * argv / shell history. `readStdin` is only invoked in the stdin mode.
104
+ */
105
+ export function resolveEnrollCode(opts, readStdin) {
106
+ if (opts.enroll && opts.enrollStdin) {
107
+ throw new Error('Use either --enroll <code> or --enroll-stdin, not both.');
108
+ }
109
+ if (opts.enrollStdin) {
110
+ const code = readStdin().trim();
111
+ if (!code)
112
+ throw new Error('No enrollment code received on stdin.');
113
+ return code;
114
+ }
115
+ if (opts.enroll) {
116
+ const code = opts.enroll.trim();
117
+ if (!code)
118
+ throw new Error('Enrollment code (--enroll) is empty.');
119
+ return code;
120
+ }
121
+ throw new Error('An enrollment code is required: pass --enroll <code> or --enroll-stdin.');
122
+ }
123
+ // ---------------------------------------------------------------------------
124
+ // Project pin upsert (D14)
125
+ // ---------------------------------------------------------------------------
126
+ /**
127
+ * Add (or replace) the `/p/<pin>` routing segment on a config URL so an agent session launched in
128
+ * this project folder routes strictly to this project's engine (design 06 D14). Existing `/p/<pin>`
129
+ * segments are replaced; the port / host / scheme are preserved.
130
+ */
131
+ export function pinUrl(rawUrl, pin) {
132
+ const stripExistingPin = (segments) => {
133
+ const idx = segments.findIndex((s) => s === 'p');
134
+ if (idx >= 0 && idx === segments.length - 2 && /^[0-9a-f]{8}$/i.test(segments[idx + 1])) {
135
+ return segments.slice(0, idx);
136
+ }
137
+ return segments;
138
+ };
139
+ try {
140
+ const url = new URL(rawUrl);
141
+ let segments = stripExistingPin(url.pathname.split('/').filter(Boolean));
142
+ segments = [...segments, 'p', pin];
143
+ url.pathname = '/' + segments.join('/');
144
+ return url.toString().replace(/\/$/, '');
145
+ }
146
+ catch {
147
+ const base = rawUrl.replace(/\/+$/, '').replace(/\/p\/[0-9a-f]{8}$/i, '');
148
+ return `${base}/p/${pin}`;
149
+ }
150
+ }
151
+ /**
152
+ * The exact project-root STRING to feed into ProjectIdentity so the CLI-derived pin matches the
153
+ * plugin's. The Unity plugin registers with `ProjectIdentity.DerivePin(UnityMcpPluginEditor
154
+ * .ProjectRootPath)`, where `ProjectRootPath => Path.GetDirectoryName(Application.dataPath)`.
155
+ * `Application.dataPath` is forward-slash on EVERY platform (incl. Windows: `C:/proj/Assets`), so
156
+ * the plugin hashes a forward-slash root like `C:/proj`. Node's `path.resolve` yields BACKSLASHES
157
+ * on Windows (`C:\proj`), which the ProjectIdentity golden vectors hash to a DIFFERENT pin — so we
158
+ * convert separators to `/` here (a no-op on POSIX) to route to the same engine instance.
159
+ */
160
+ export function projectRootForIdentity(projectPath) {
161
+ return path.resolve(projectPath).replace(/\\/g, '/');
162
+ }
163
+ /**
164
+ * Upsert the project pin into every EXISTING project-local (project-scoped) JSON agent config that
165
+ * carries an `ai-game-developer` server entry with a `url` / `serverUrl`. Global client config
166
+ * files (Claude Desktop, Antigravity, Cline, the Copilot CLI) are never touched — the golden path
167
+ * writes no user-global entry. TOML (Codex) configs are left to the server binary's own
168
+ * `configure`. Returns the list of files actually rewritten.
169
+ */
170
+ export function upsertProjectPinIntoConfigs(projectPath, pin) {
171
+ const resolvedProject = path.resolve(projectPath);
172
+ const updatedFiles = [];
173
+ for (const agent of agentRegistry) {
174
+ if (agent.configFormat !== 'json')
175
+ continue;
176
+ const configPath = agent.getConfigPath(resolvedProject);
177
+ // Project-scoped only: the config path must live inside the project directory.
178
+ const relative = path.relative(resolvedProject, configPath);
179
+ if (relative.startsWith('..') || path.isAbsolute(relative))
180
+ continue;
181
+ if (!fs.existsSync(configPath))
182
+ continue;
183
+ let root;
184
+ try {
185
+ const parsed = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
186
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
187
+ continue;
188
+ root = parsed;
189
+ }
190
+ catch {
191
+ continue;
192
+ }
193
+ const body = root[agent.bodyPath];
194
+ if (!body || typeof body !== 'object' || Array.isArray(body))
195
+ continue;
196
+ const entry = body[MCP_SERVER_NAME];
197
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
198
+ continue;
199
+ const entryRecord = entry;
200
+ let changed = false;
201
+ for (const key of ['url', 'serverUrl']) {
202
+ const current = entryRecord[key];
203
+ if (typeof current === 'string' && current.length > 0) {
204
+ const pinned = pinUrl(current, pin);
205
+ if (pinned !== current) {
206
+ entryRecord[key] = pinned;
207
+ changed = true;
208
+ }
209
+ }
210
+ }
211
+ if (changed) {
212
+ fs.writeFileSync(configPath, JSON.stringify(root, null, 2) + '\n');
213
+ updatedFiles.push(configPath);
214
+ }
215
+ }
216
+ return { updatedFiles };
217
+ }
218
+ /**
219
+ * Execute the full enrollment side effect: redeem → persist the plugin credential to the SHARED
220
+ * machine store → write the project marker with the server target → upsert the D14 pin into
221
+ * existing project-local configs. NEVER writes a project token file / `cloudToken` config.
222
+ */
223
+ export async function runEnroll(opts) {
224
+ const credential = await redeemEnrollmentCode(opts.code, {
225
+ baseUrl: opts.baseUrl,
226
+ fetchImpl: opts.fetchImpl,
227
+ });
228
+ const serverTarget = credential.serverTarget ?? opts.baseUrl ?? CLOUD_SERVER_BASE_URL;
229
+ // Persist to the shared machine credential store (0600 / DPAPI) — never a project file.
230
+ opts.store.write({
231
+ accessToken: credential.accessToken,
232
+ refreshToken: credential.refreshToken,
233
+ expiresAt: credential.expiresAt,
234
+ serverTarget,
235
+ subject: credential.subject,
236
+ });
237
+ // Record the enrolled server target in the committable project marker.
238
+ const markerPath = writeProjectMarker(opts.projectPath, { serverTarget });
239
+ // Upsert the D14 pin into existing project-local agent configs. The pin is derived from the
240
+ // forward-slash project root so it matches the plugin's pin (see projectRootForIdentity).
241
+ const pin = deriveProjectPin(projectRootForIdentity(opts.projectPath));
242
+ const { updatedFiles } = upsertProjectPinIntoConfigs(opts.projectPath, pin);
243
+ return {
244
+ serverTarget,
245
+ pin,
246
+ credentialPath: opts.store.credentialsPath,
247
+ markerPath,
248
+ pinnedConfigs: updatedFiles,
249
+ };
250
+ }
251
+ //# sourceMappingURL=enroll.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enroll.js","sourceRoot":"","sources":["../../src/utils/enroll.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * TypeScript client of the shared machine credential store — the same on-disk contract the
3
+ * plugin's C# `MachineCredentialStore` (MCP-Plugin-dotnet, com.IvanMurzak.McpPlugin.AgentConfig)
4
+ * reads and writes. A single ai-game.dev account credential lives once per machine at
5
+ * `~/.ai-game-dev/credentials.json`, so `login` writes it here and every engine plugin/CLI reads
6
+ * it — sign-in happens once per machine, never per project, and the credential is NEVER written
7
+ * into a project file / VCS.
8
+ *
9
+ * At-rest protection matches the C# store byte-for-byte so the plugin can read what the CLI wrote:
10
+ * - POSIX — plaintext JSON, file mode 0600, inside a 0700 directory.
11
+ * - Windows — DPAPI-encrypted (CurrentUser scope, no entropy) via
12
+ * System.Security.Cryptography.ProtectedData, invoked through PowerShell. This is
13
+ * interoperable with the C# store's CryptProtectData/CryptUnprotectData (the description
14
+ * string and CRYPTPROTECT_UI_FORBIDDEN flag do not affect decryptability).
15
+ */
16
+ /** Directory name under the user home (or a project root) that holds the store. */
17
+ export declare const MACHINE_STORE_DIR_NAME = ".ai-game-dev";
18
+ /** File name of the secret credential document. */
19
+ export declare const CREDENTIALS_FILE_NAME = "credentials.json";
20
+ /**
21
+ * The secret credential material persisted in the store. Mirrors the C# `MachineCredentials`
22
+ * schema (camelCase JSON keys). Unknown fields are preserved on read for forward-compatibility.
23
+ */
24
+ export interface MachineCredentials {
25
+ /** Schema version of the persisted document (currently 1). */
26
+ version?: number;
27
+ /** The current short-lived JWT access token (MCP audience). */
28
+ accessToken?: string;
29
+ /** The rotating refresh token used to mint a new access token before `expiresAt`. */
30
+ refreshToken?: string;
31
+ /** ISO-8601 absolute expiry of `accessToken`; used to schedule proactive refresh. */
32
+ expiresAt?: string;
33
+ /** The server target the credential was issued for (hosted https://ai-game.dev or a local URL). */
34
+ serverTarget?: string;
35
+ /** The account id (`sub`) the credential resolves to. Audit/diagnostic only. */
36
+ subject?: string;
37
+ [key: string]: unknown;
38
+ }
39
+ /**
40
+ * The shared machine credential store. Defaults to `~/.ai-game-dev/`; pass an explicit
41
+ * `baseDirectory` for tests or for the `--project` per-project store
42
+ * (`<project>/.ai-game-dev/`).
43
+ */
44
+ export declare class MachineCredentialStore {
45
+ private readonly _baseDirectory;
46
+ constructor(baseDirectory?: string);
47
+ /** Absolute path of the store directory. */
48
+ get baseDirectory(): string;
49
+ /** Absolute path of the secret credential file. */
50
+ get credentialsPath(): string;
51
+ /** True when a credential file exists in the store. */
52
+ get exists(): boolean;
53
+ /**
54
+ * Encrypt (Windows) / restrict (POSIX) and write `credentials` to the store, creating the
55
+ * store directory with owner-only permissions if needed. `version` is always written as 1;
56
+ * undefined fields are omitted (matching the C# `WhenWritingNull` policy).
57
+ */
58
+ write(credentials: MachineCredentials): void;
59
+ /** Read and decrypt the stored credentials, or null when none are present. */
60
+ read(): MachineCredentials | null;
61
+ /** Delete the stored credentials (sign-out). No-op when none exist. */
62
+ delete(): void;
63
+ private ensureBaseDirectory;
64
+ }
@@ -0,0 +1,114 @@
1
+ // Copyright (c) 2024 Ivan Murzak. All rights reserved.
2
+ // Licensed under the Apache License, Version 2.0.
3
+ import * as fs from 'fs';
4
+ import * as os from 'os';
5
+ import * as path from 'path';
6
+ import { execFileSync } from 'child_process';
7
+ /**
8
+ * TypeScript client of the shared machine credential store — the same on-disk contract the
9
+ * plugin's C# `MachineCredentialStore` (MCP-Plugin-dotnet, com.IvanMurzak.McpPlugin.AgentConfig)
10
+ * reads and writes. A single ai-game.dev account credential lives once per machine at
11
+ * `~/.ai-game-dev/credentials.json`, so `login` writes it here and every engine plugin/CLI reads
12
+ * it — sign-in happens once per machine, never per project, and the credential is NEVER written
13
+ * into a project file / VCS.
14
+ *
15
+ * At-rest protection matches the C# store byte-for-byte so the plugin can read what the CLI wrote:
16
+ * - POSIX — plaintext JSON, file mode 0600, inside a 0700 directory.
17
+ * - Windows — DPAPI-encrypted (CurrentUser scope, no entropy) via
18
+ * System.Security.Cryptography.ProtectedData, invoked through PowerShell. This is
19
+ * interoperable with the C# store's CryptProtectData/CryptUnprotectData (the description
20
+ * string and CRYPTPROTECT_UI_FORBIDDEN flag do not affect decryptability).
21
+ */
22
+ /** Directory name under the user home (or a project root) that holds the store. */
23
+ export const MACHINE_STORE_DIR_NAME = '.ai-game-dev';
24
+ /** File name of the secret credential document. */
25
+ export const CREDENTIALS_FILE_NAME = 'credentials.json';
26
+ const isWindows = process.platform === 'win32';
27
+ /**
28
+ * The shared machine credential store. Defaults to `~/.ai-game-dev/`; pass an explicit
29
+ * `baseDirectory` for tests or for the `--project` per-project store
30
+ * (`<project>/.ai-game-dev/`).
31
+ */
32
+ export class MachineCredentialStore {
33
+ constructor(baseDirectory) {
34
+ this._baseDirectory = baseDirectory ?? path.join(os.homedir(), MACHINE_STORE_DIR_NAME);
35
+ }
36
+ /** Absolute path of the store directory. */
37
+ get baseDirectory() {
38
+ return this._baseDirectory;
39
+ }
40
+ /** Absolute path of the secret credential file. */
41
+ get credentialsPath() {
42
+ return path.join(this._baseDirectory, CREDENTIALS_FILE_NAME);
43
+ }
44
+ /** True when a credential file exists in the store. */
45
+ get exists() {
46
+ return fs.existsSync(this.credentialsPath);
47
+ }
48
+ /**
49
+ * Encrypt (Windows) / restrict (POSIX) and write `credentials` to the store, creating the
50
+ * store directory with owner-only permissions if needed. `version` is always written as 1;
51
+ * undefined fields are omitted (matching the C# `WhenWritingNull` policy).
52
+ */
53
+ write(credentials) {
54
+ this.ensureBaseDirectory();
55
+ const document = { ...credentials, version: 1 };
56
+ const json = JSON.stringify(document, null, 2);
57
+ const plaintext = Buffer.from(json, 'utf-8');
58
+ const bytes = isWindows ? dpapiTransform('Protect', plaintext) : plaintext;
59
+ fs.writeFileSync(this.credentialsPath, bytes);
60
+ if (!isWindows) {
61
+ fs.chmodSync(this.credentialsPath, 0o600);
62
+ }
63
+ }
64
+ /** Read and decrypt the stored credentials, or null when none are present. */
65
+ read() {
66
+ if (!fs.existsSync(this.credentialsPath)) {
67
+ return null;
68
+ }
69
+ const raw = fs.readFileSync(this.credentialsPath);
70
+ if (raw.length === 0) {
71
+ return null;
72
+ }
73
+ const plaintext = isWindows ? dpapiTransform('Unprotect', raw) : raw;
74
+ const json = plaintext.toString('utf-8');
75
+ if (json.trim().length === 0) {
76
+ return null;
77
+ }
78
+ return JSON.parse(json);
79
+ }
80
+ /** Delete the stored credentials (sign-out). No-op when none exist. */
81
+ delete() {
82
+ if (fs.existsSync(this.credentialsPath)) {
83
+ fs.rmSync(this.credentialsPath);
84
+ }
85
+ }
86
+ ensureBaseDirectory() {
87
+ fs.mkdirSync(this._baseDirectory, { recursive: true });
88
+ if (!isWindows) {
89
+ fs.chmodSync(this._baseDirectory, 0o700);
90
+ }
91
+ }
92
+ }
93
+ /**
94
+ * Run a Windows DPAPI Protect/Unprotect round trip through PowerShell's
95
+ * System.Security.Cryptography.ProtectedData (CurrentUser scope, no entropy) — interoperable
96
+ * with the C# store's CryptProtectData/CryptUnprotectData. Input and output are passed as
97
+ * base64 through an environment variable so the plaintext never lands in argv or the process
98
+ * table. Only ever invoked on Windows.
99
+ */
100
+ function dpapiTransform(action, input) {
101
+ const script = "$ErrorActionPreference='Stop';" +
102
+ 'Add-Type -AssemblyName System.Security;' +
103
+ '$in=[Convert]::FromBase64String($env:AIGD_DPAPI_IN);' +
104
+ `$out=[System.Security.Cryptography.ProtectedData]::${action}($in,$null,[System.Security.Cryptography.DataProtectionScope]::CurrentUser);` +
105
+ '[Convert]::ToBase64String($out)';
106
+ const stdout = execFileSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], {
107
+ encoding: 'utf-8',
108
+ env: { ...process.env, AIGD_DPAPI_IN: input.toString('base64') },
109
+ timeout: 20000,
110
+ windowsHide: true,
111
+ });
112
+ return Buffer.from(stdout.trim(), 'base64');
113
+ }
114
+ //# sourceMappingURL=machine-credentials.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"machine-credentials.js","sourceRoot":"","sources":["../../src/utils/machine-credentials.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * The CLI's MANAGED server directory — the machine-shared home the `install-plugin --with-server`
3
+ * download lands in, and the directory `configure --agent` proxies to. Lives beside the shared
4
+ * machine credential store (`~/.ai-game-dev/`) so the server binary is fetched once per machine,
5
+ * not once per project, and is NEVER on PATH (design 06/09: the binary lives in the CLI's managed
6
+ * dir). Layout mirrors the plugin's `Library/mcp-server/<rid>/` — one folder per RID + a `version`
7
+ * marker.
8
+ */
9
+ export declare function managedServerRootDir(homeDir?: string): string;
10
+ export declare function managedServerDir(rid: string, homeDir?: string): string;
11
+ /** The server executable file name for a RID (`.exe` suffix on Windows RIDs). */
12
+ export declare function managedServerBinaryName(rid: string): string;
13
+ export declare function managedServerBinaryPath(rid: string, homeDir?: string): string;
14
+ /** Absolute path of the `version` marker recording which server version is staged for a RID. */
15
+ export declare function managedServerVersionPath(rid: string, homeDir?: string): string;
16
+ /** The release-asset zip NAME for a RID (`gamedev-mcp-server-<rid>.zip`) — the SHA256SUMS key. */
17
+ export declare function serverZipName(rid: string): string;
18
+ /** The GitHub release download URL of the per-RID server zip, pinned to `version`. */
19
+ export declare function serverZipUrl(rid: string, version: string): string;
20
+ /** The GitHub release download URL of the `SHA256SUMS` integrity manifest, pinned to `version`. */
21
+ export declare function serverShaSumsUrl(version: string): string;
22
+ /**
23
+ * Look up the expected SHA-256 (lowercase hex) for `fileName` in a `SHA256SUMS` manifest.
24
+ * Accepts the canonical `<hex> <name>` line shape and the `<hex> *<name>` binary-mode variant.
25
+ * Returns null when no line names `fileName` (exact match — mirrors the C# exact-key Ordinal
26
+ * lookup), so a missing entry fails closed at the call site.
27
+ */
28
+ export declare function parseSha256Sums(manifest: string, fileName: string): string | null;
29
+ /** SHA-256 of a buffer as lowercase hex. */
30
+ export declare function sha256Hex(buffer: Buffer): string;
31
+ /**
32
+ * Extract a `.zip` into `destDir` using the best available platform archive tool. Node ships no
33
+ * zip reader, so this shells out: bsdtar (`tar -xf`, present on Windows 10+ and macOS) and `unzip`
34
+ * (POSIX) cover every supported host, with a PowerShell `Expand-Archive` fallback on Windows. The
35
+ * caller verifies the extracted binary exists afterwards, so a silent no-op tool is still caught.
36
+ */
37
+ export declare function extractZip(zipPath: string, destDir: string): void;
38
+ export interface DownloadServerOptions {
39
+ /** Host RID; defaults to `resolveHostRid()`. */
40
+ rid?: string;
41
+ /** Server version to fetch; defaults to `DEFAULT_SERVER_VERSION`. */
42
+ version?: string;
43
+ /**
44
+ * Offline/CI escape hatch: a local zip path OR a URL to fetch the zip from. When set, the
45
+ * download uses this source and the SHA256SUMS integrity gate is SKIPPED (explicit-trust
46
+ * override — mirrors the addon `--source` pattern). Otherwise the pinned release zip is fetched
47
+ * and verified fail-closed against the release's SHA256SUMS.
48
+ */
49
+ source?: string;
50
+ /** Home directory override (tests). */
51
+ homeDir?: string;
52
+ /** `fetch` injection (tests). */
53
+ fetchImpl?: typeof fetch;
54
+ /** Extraction injection (tests) — `(zipPath, destDir) => void`. */
55
+ extractImpl?: (zipPath: string, destDir: string) => void;
56
+ /** Optional progress reporter. */
57
+ onProgress?: (message: string) => void;
58
+ }
59
+ export interface DownloadServerResult {
60
+ rid: string;
61
+ version: string;
62
+ /** Absolute path of the published server binary. */
63
+ binaryPath: string;
64
+ /** True when the zip passed SHA256SUMS verification; false for a `--server-source` override. */
65
+ verified: boolean;
66
+ }
67
+ /**
68
+ * Download (or copy from `--server-source`), verify against the release SHA256SUMS (fail-closed),
69
+ * extract, and atomically publish the pinned GameDev-MCP-Server binary for the host RID into the
70
+ * CLI's managed directory. Never launches the binary. Returns the published path.
71
+ */
72
+ export declare function downloadServerBinary(opts?: DownloadServerOptions): Promise<DownloadServerResult>;
73
+ export interface ConfigureProxyOptions {
74
+ agentId: string;
75
+ projectPath: string;
76
+ /** Optional explicit server URL forwarded to the binary's `configure --url`. */
77
+ url?: string;
78
+ rid?: string;
79
+ homeDir?: string;
80
+ /** Runner injection (tests) — `(binaryPath, args, cwd) => void`. */
81
+ runImpl?: (binaryPath: string, args: string[], cwd: string) => void;
82
+ }
83
+ export interface ConfigureProxyResult {
84
+ binaryPath: string;
85
+ args: string[];
86
+ cwd: string;
87
+ }
88
+ /**
89
+ * Proxy `configure --agent <id>` to the managed GameDev-MCP-Server binary's `configure`
90
+ * subcommand (design 06/09 Phase 3) so the shared C# configurator registry — with the derived
91
+ * `port=` + `project=` pin — is reachable from the terminal. The binary derives the pin/port from
92
+ * its working directory, so we run it with `cwd` = the resolved project path. When no managed
93
+ * binary is installed, throws a clear, actionable error pointing at `install-plugin --with-server`.
94
+ */
95
+ export declare function proxyConfigure(opts: ConfigureProxyOptions): ConfigureProxyResult;