godot-cli 0.16.2 → 0.17.1

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.
Files changed (49) hide show
  1. package/dist/commands/configure.js +24 -1
  2. package/dist/commands/configure.js.map +1 -1
  3. package/dist/commands/install-plugin.js +99 -7
  4. package/dist/commands/install-plugin.js.map +1 -1
  5. package/dist/commands/login.js +53 -15
  6. package/dist/commands/login.js.map +1 -1
  7. package/dist/lib/configure-agent.d.ts +20 -0
  8. package/dist/lib/configure-agent.js +115 -0
  9. package/dist/lib/configure-agent.js.map +1 -0
  10. package/dist/lib/enroll.d.ts +21 -0
  11. package/dist/lib/enroll.js +100 -0
  12. package/dist/lib/enroll.js.map +1 -0
  13. package/dist/lib/install-server.d.ts +25 -0
  14. package/dist/lib/install-server.js +234 -0
  15. package/dist/lib/install-server.js.map +1 -0
  16. package/dist/lib/setup-mcp.d.ts +28 -0
  17. package/dist/lib/setup-mcp.js +61 -1
  18. package/dist/lib/setup-mcp.js.map +1 -1
  19. package/dist/lib/types.d.ts +138 -0
  20. package/dist/lib.d.ts +4 -1
  21. package/dist/lib.js +3 -0
  22. package/dist/lib.js.map +1 -1
  23. package/dist/utils/addon-deps.js +2 -2
  24. package/dist/utils/addon-deps.js.map +1 -1
  25. package/dist/utils/agents.d.ts +12 -0
  26. package/dist/utils/agents.js +24 -0
  27. package/dist/utils/agents.js.map +1 -1
  28. package/dist/utils/cloud-login.d.ts +22 -4
  29. package/dist/utils/cloud-login.js +40 -17
  30. package/dist/utils/cloud-login.js.map +1 -1
  31. package/dist/utils/connection.d.ts +4 -7
  32. package/dist/utils/connection.js +8 -10
  33. package/dist/utils/connection.js.map +1 -1
  34. package/dist/utils/enroll.d.ts +39 -0
  35. package/dist/utils/enroll.js +66 -0
  36. package/dist/utils/enroll.js.map +1 -0
  37. package/dist/utils/machine-credentials.d.ts +74 -0
  38. package/dist/utils/machine-credentials.js +182 -0
  39. package/dist/utils/machine-credentials.js.map +1 -0
  40. package/dist/utils/project-identity.d.ts +76 -0
  41. package/dist/utils/project-identity.js +111 -0
  42. package/dist/utils/project-identity.js.map +1 -0
  43. package/dist/utils/project-marker.d.ts +64 -0
  44. package/dist/utils/project-marker.js +192 -0
  45. package/dist/utils/project-marker.js.map +1 -0
  46. package/dist/utils/server-source.d.ts +84 -0
  47. package/dist/utils/server-source.js +217 -0
  48. package/dist/utils/server-source.js.map +1 -0
  49. package/package.json +1 -1
@@ -0,0 +1,182 @@
1
+ import * as fs from 'fs';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
+ import { execFileSync } from 'child_process';
5
+ /**
6
+ * The shared **machine credential store** at `~/.ai-game-dev/` — a single per-machine home for the
7
+ * ai-game.dev account credential (`credentials.json`). Engines, CLIs, and the local server all read
8
+ * this store, so **sign-in happens once per machine** (design `06-engine-plugins.md` / D12).
9
+ *
10
+ * This is the TypeScript peer of the shared C# `MachineCredentialStore`
11
+ * (`MCP-Plugin-dotnet/McpPlugin/src/AgentConfig/MachineCredentialStore.cs`). The on-disk contract is
12
+ * matched byte-for-byte so a credential written by `godot-cli login` is read by the engine plugin
13
+ * (which auto-adopts it on editor boot) and vice-versa:
14
+ *
15
+ * - **POSIX** — `credentials.json` is written plaintext with `0600` permissions inside a `0700`
16
+ * directory.
17
+ * - **Windows** — the file content is **DPAPI-encrypted** with the current user's key
18
+ * (`CryptProtectData`, `CurrentUser` scope, no entropy) so the on-disk bytes are never plaintext.
19
+ * Node has no built-in DPAPI, so we interop via PowerShell's `System.Security.Cryptography.
20
+ * ProtectedData`, which is a managed wrapper over the very same `CryptProtectData`/
21
+ * `CryptUnprotectData` calls the C# store uses — the blobs are cross-readable.
22
+ *
23
+ * Credentials are NEVER written to a project file / VCS. The optional `--project` per-project override
24
+ * (project-local `.godot-mcp/credentials.json`, gitignored) is handled by `credentials.ts`, not here.
25
+ */
26
+ /** Directory name under the user home that holds the store (matches C# `MachineCredentialStore.DirectoryName`). */
27
+ export const MACHINE_STORE_DIR_NAME = '.ai-game-dev';
28
+ /** File name of the secret credential document. */
29
+ export const MACHINE_CREDENTIALS_FILE_NAME = 'credentials.json';
30
+ /**
31
+ * Optional env override for the store directory (advanced use / tests). When set, this exact
32
+ * directory is used verbatim; otherwise the store lives at `~/.ai-game-dev`. The DEFAULT matches the
33
+ * C# store so cross-tool interop holds — the override never changes the production path.
34
+ */
35
+ export const MACHINE_STORE_DIR_ENV = 'AI_GAME_DEV_CREDENTIALS_DIR';
36
+ // Schema version of the persisted document (matches C# `MachineCredentials.Version`).
37
+ const SCHEMA_VERSION = 1;
38
+ /** Absolute path of the store directory (honoring the env override). */
39
+ export function getMachineStoreDir(baseDirOverride) {
40
+ if (baseDirOverride)
41
+ return baseDirOverride;
42
+ const envDir = process.env[MACHINE_STORE_DIR_ENV];
43
+ if (envDir && envDir.trim().length > 0)
44
+ return envDir;
45
+ return path.join(os.homedir(), MACHINE_STORE_DIR_NAME);
46
+ }
47
+ /** Absolute path of the secret credential file. */
48
+ export function getMachineCredentialsPath(baseDirOverride) {
49
+ return path.join(getMachineStoreDir(baseDirOverride), MACHINE_CREDENTIALS_FILE_NAME);
50
+ }
51
+ /** True when a credential file exists in the store. */
52
+ export function machineCredentialsExist(baseDirOverride) {
53
+ return fs.existsSync(getMachineCredentialsPath(baseDirOverride));
54
+ }
55
+ /**
56
+ * Read and (on Windows) decrypt the stored credentials, or `null` when none are present / empty.
57
+ * Throws on a decryption failure or malformed JSON (a corrupt or foreign-user credential); callers on
58
+ * a hot path should use {@link readMachineAccessToken}, which swallows those.
59
+ */
60
+ export function readMachineCredentials(baseDirOverride) {
61
+ const credentialsPath = getMachineCredentialsPath(baseDirOverride);
62
+ if (!fs.existsSync(credentialsPath)) {
63
+ return null;
64
+ }
65
+ const raw = fs.readFileSync(credentialsPath);
66
+ if (raw.length === 0) {
67
+ return null;
68
+ }
69
+ const plaintext = isWindows() ? unprotectDpapi(raw) : raw;
70
+ const json = plaintext.toString('utf8');
71
+ if (json.trim().length === 0) {
72
+ return null;
73
+ }
74
+ const parsed = JSON.parse(json);
75
+ return parsed;
76
+ }
77
+ /**
78
+ * Encrypt (Windows) / restrict (POSIX) and write `credentials` to the store, creating the store
79
+ * directory with owner-only permissions if needed.
80
+ */
81
+ export function writeMachineCredentials(credentials, baseDirOverride) {
82
+ const dir = getMachineStoreDir(baseDirOverride);
83
+ const credentialsPath = path.join(dir, MACHINE_CREDENTIALS_FILE_NAME);
84
+ ensureStoreDirectory(dir);
85
+ const json = serialize(credentials);
86
+ const plaintext = Buffer.from(json, 'utf8');
87
+ const bytes = isWindows() ? protectDpapi(plaintext) : plaintext;
88
+ fs.writeFileSync(credentialsPath, bytes, { mode: 0o600 });
89
+ setPosixPermissions(credentialsPath, 0o600);
90
+ }
91
+ /**
92
+ * Convenience reader for the persisted access token. Swallows a missing / malformed / undecryptable
93
+ * file (returns undefined) so a broken store can never crash a command; `login` re-authenticates and
94
+ * overwrites it.
95
+ */
96
+ export function readMachineAccessToken(baseDirOverride) {
97
+ let credentials;
98
+ try {
99
+ credentials = readMachineCredentials(baseDirOverride);
100
+ }
101
+ catch {
102
+ return undefined;
103
+ }
104
+ const token = credentials?.accessToken;
105
+ return typeof token === 'string' && token.trim().length > 0 ? token : undefined;
106
+ }
107
+ /** Delete the stored credentials (sign-out). No-op when none exist. */
108
+ export function deleteMachineCredentials(baseDirOverride) {
109
+ const credentialsPath = getMachineCredentialsPath(baseDirOverride);
110
+ if (fs.existsSync(credentialsPath)) {
111
+ fs.rmSync(credentialsPath, { force: true });
112
+ }
113
+ }
114
+ // ── Serialization ────────────────────────────────────────────────────────────────────────────────
115
+ function serialize(credentials) {
116
+ // Mirror the C# store's camelCase + WhenWritingNull: always emit `version`, omit null/undefined
117
+ // optional fields. Indentation is cosmetic (the reader is whitespace-insensitive).
118
+ const doc = { version: credentials.version ?? SCHEMA_VERSION };
119
+ if (credentials.accessToken != null)
120
+ doc.accessToken = credentials.accessToken;
121
+ if (credentials.refreshToken != null)
122
+ doc.refreshToken = credentials.refreshToken;
123
+ if (credentials.expiresAt != null)
124
+ doc.expiresAt = credentials.expiresAt;
125
+ if (credentials.serverTarget != null)
126
+ doc.serverTarget = credentials.serverTarget;
127
+ if (credentials.subject != null)
128
+ doc.subject = credentials.subject;
129
+ return JSON.stringify(doc, null, 2) + '\n';
130
+ }
131
+ // ── Filesystem permissions ───────────────────────────────────────────────────────────────────────
132
+ function ensureStoreDirectory(dir) {
133
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
134
+ setPosixPermissions(dir, 0o700);
135
+ }
136
+ function setPosixPermissions(target, mode) {
137
+ if (isWindows())
138
+ return; // chmod is a no-op on Windows; DPAPI provides at-rest protection there.
139
+ try {
140
+ fs.chmodSync(target, mode);
141
+ }
142
+ catch {
143
+ // Best-effort only — never fail persistence over a chmod (e.g. exotic filesystems).
144
+ }
145
+ }
146
+ // ── Windows DPAPI via PowerShell (System.Security.Cryptography.ProtectedData) ───────────────────────
147
+ // ProtectedData.Protect/Unprotect wrap CryptProtectData/CryptUnprotectData with the SAME CurrentUser
148
+ // scope + null entropy the C# MachineCredentialStore uses, so the on-disk blobs are cross-readable.
149
+ function isWindows() {
150
+ return process.platform === 'win32';
151
+ }
152
+ const DPAPI_PROTECT_SCRIPT = "Add-Type -AssemblyName System.Security; " +
153
+ "$in = [Console]::In.ReadToEnd().Trim(); " +
154
+ "$bytes = [Convert]::FromBase64String($in); " +
155
+ "$prot = [System.Security.Cryptography.ProtectedData]::Protect($bytes, $null, 'CurrentUser'); " +
156
+ "[Console]::Out.Write([Convert]::ToBase64String($prot))";
157
+ const DPAPI_UNPROTECT_SCRIPT = "Add-Type -AssemblyName System.Security; " +
158
+ "$in = [Console]::In.ReadToEnd().Trim(); " +
159
+ "$bytes = [Convert]::FromBase64String($in); " +
160
+ "$plain = [System.Security.Cryptography.ProtectedData]::Unprotect($bytes, $null, 'CurrentUser'); " +
161
+ "[Console]::Out.Write([Convert]::ToBase64String($plain))";
162
+ function protectDpapi(plaintext) {
163
+ return runDpapi(DPAPI_PROTECT_SCRIPT, plaintext, 'encrypt');
164
+ }
165
+ function unprotectDpapi(cipher) {
166
+ return runDpapi(DPAPI_UNPROTECT_SCRIPT, cipher, 'decrypt');
167
+ }
168
+ function runDpapi(script, input, op) {
169
+ try {
170
+ const out = execFileSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', script], {
171
+ input: input.toString('base64'),
172
+ encoding: 'utf8',
173
+ windowsHide: true,
174
+ });
175
+ return Buffer.from(out.trim(), 'base64');
176
+ }
177
+ catch (err) {
178
+ const message = err instanceof Error ? err.message : String(err);
179
+ throw new Error(`Failed to ${op} the machine credential store via DPAPI: ${message}`);
180
+ }
181
+ }
182
+ //# 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,76 @@
1
+ /**
2
+ * The single canonical derivation of a project's **routing pin** and its
3
+ * **deterministic local port** from the project root path — the TypeScript port of the
4
+ * shared 7.0 library `com.IvanMurzak.McpPlugin.AgentConfig.ProjectIdentity`
5
+ * (`MCP-Plugin-dotnet/McpPlugin/src/AgentConfig/ProjectIdentity.cs`), which the Godot addon
6
+ * consumes via `GodotProjectIdentity`. Every runtime (Unity/Godot/Unreal plugins, the .NET
7
+ * sidecar, and the engine CLIs) derives identical values with no shared state and no probing,
8
+ * so an agent session launched in a project folder routes strictly to that project's engine
9
+ * instance (design `06-engine-plugins.md` § D14/D15).
10
+ *
11
+ * Algorithm (kept verbatim from the shipped Unity `GeneratePortFromDirectory` / the C# reference):
12
+ * 1. Trim trailing directory separators (`/` and `\`) so `/a/b` and `/a/b/` are the same project.
13
+ * Separators are NOT converted — `C:\a` and `C:/a` hash differently (do NOT `path.normalize`).
14
+ * 2. Lowercase with an invariant fold (`ToLowerInvariant`-equivalent — see {@link toLowerInvariant}).
15
+ * 3. UTF-8 encode, then SHA-256 hash.
16
+ * 4. pin = the first 4 bytes of the hash as 8 lowercase hex chars.
17
+ * 5. port = 20000 + (littleEndianUInt32(first 4 bytes) % 10000). Range 20000-29999.
18
+ *
19
+ * Cross-language parity (C# vs TS) is pinned by the committed golden-vector file
20
+ * (`ProjectIdentity.GoldenVectors.json`) and reproduced byte-for-byte by
21
+ * `tests/project-identity.test.ts`.
22
+ */
23
+ /** Inclusive lower bound of the deterministic local-port range. */
24
+ export declare const MIN_PORT = 20000;
25
+ /** Inclusive upper bound of the deterministic local-port range. */
26
+ export declare const MAX_PORT = 29999;
27
+ /** Number of ports in the deterministic range (10000). */
28
+ export declare const PORT_RANGE: number;
29
+ /** Number of hex characters in the routing pin (first 4 bytes of the hash). */
30
+ export declare const PIN_LENGTH = 8;
31
+ /** The resolved identity for a project root. */
32
+ export interface ProjectIdentity {
33
+ /** The routing pin: first 8 lowercase hex chars of the SHA-256 of the normalized project root. */
34
+ pin: string;
35
+ /** The resolved local port — the hash-derived port unless an explicit override was supplied. */
36
+ port: number;
37
+ /** True when {@link port} came from an explicit user override rather than the hash. */
38
+ portIsOverridden: boolean;
39
+ }
40
+ /**
41
+ * Invariant lowercasing that matches C# `string.ToLowerInvariant()`.
42
+ *
43
+ * JS `String.prototype.toLowerCase()` and C# `ToLowerInvariant()` disagree on some Unicode
44
+ * characters. The one that matters for real project paths — and the one the golden-vector file
45
+ * explicitly pins (`unicodeDivergence`) — is **U+0130 LATIN CAPITAL LETTER I WITH DOT ABOVE**:
46
+ * C# `ToLowerInvariant` leaves it unchanged, whereas a naive JS `toLowerCase()` folds it to
47
+ * `i` + U+0307 (COMBINING DOT ABOVE), producing a DIFFERENT hash. Preserve U+0130 so the TS
48
+ * derivation reproduces the canonical C# value byte-for-byte; every other character lowercases
49
+ * identically under both implementations.
50
+ */
51
+ export declare function toLowerInvariant(value: string): string;
52
+ /**
53
+ * The exact string that is UTF-8/SHA-256 hashed: the project root with trailing directory
54
+ * separators trimmed, then invariant-lowercased. Exposed so callers/tests can reproduce the
55
+ * pre-hash string.
56
+ */
57
+ export declare function normalize(projectRoot: string): string;
58
+ /** The routing pin only (first 8 lowercase hex chars of the hash). Never affected by overrides. */
59
+ export declare function derivePin(projectRoot: string): string;
60
+ /**
61
+ * The pure hash-derived port (ignores any override). Byte-for-byte equivalent of the shipped
62
+ * Unity `GeneratePortFromDirectory` when given the same directory string.
63
+ */
64
+ export declare function derivePort(projectRoot: string): number;
65
+ /**
66
+ * The FULL project-path hash: the complete 64-char lowercase hex SHA-256 of the normalized
67
+ * project root — the `projectPathHash` an engine plugin sends in its hub instance-metadata
68
+ * handshake. The routing pin is a case-insensitive prefix of this value by construction.
69
+ */
70
+ export declare function deriveProjectPathHash(projectRoot: string): string;
71
+ /**
72
+ * Derive the identity for `projectRoot`. When `portOverride` is non-null (the user's explicit
73
+ * override from the project marker) it always wins for {@link ProjectIdentity.port}; the
74
+ * {@link ProjectIdentity.pin} is always hash-derived.
75
+ */
76
+ export declare function deriveProjectIdentity(projectRoot: string, portOverride?: number | null): ProjectIdentity;
@@ -0,0 +1,111 @@
1
+ import { createHash } from 'crypto';
2
+ /**
3
+ * The single canonical derivation of a project's **routing pin** and its
4
+ * **deterministic local port** from the project root path — the TypeScript port of the
5
+ * shared 7.0 library `com.IvanMurzak.McpPlugin.AgentConfig.ProjectIdentity`
6
+ * (`MCP-Plugin-dotnet/McpPlugin/src/AgentConfig/ProjectIdentity.cs`), which the Godot addon
7
+ * consumes via `GodotProjectIdentity`. Every runtime (Unity/Godot/Unreal plugins, the .NET
8
+ * sidecar, and the engine CLIs) derives identical values with no shared state and no probing,
9
+ * so an agent session launched in a project folder routes strictly to that project's engine
10
+ * instance (design `06-engine-plugins.md` § D14/D15).
11
+ *
12
+ * Algorithm (kept verbatim from the shipped Unity `GeneratePortFromDirectory` / the C# reference):
13
+ * 1. Trim trailing directory separators (`/` and `\`) so `/a/b` and `/a/b/` are the same project.
14
+ * Separators are NOT converted — `C:\a` and `C:/a` hash differently (do NOT `path.normalize`).
15
+ * 2. Lowercase with an invariant fold (`ToLowerInvariant`-equivalent — see {@link toLowerInvariant}).
16
+ * 3. UTF-8 encode, then SHA-256 hash.
17
+ * 4. pin = the first 4 bytes of the hash as 8 lowercase hex chars.
18
+ * 5. port = 20000 + (littleEndianUInt32(first 4 bytes) % 10000). Range 20000-29999.
19
+ *
20
+ * Cross-language parity (C# vs TS) is pinned by the committed golden-vector file
21
+ * (`ProjectIdentity.GoldenVectors.json`) and reproduced byte-for-byte by
22
+ * `tests/project-identity.test.ts`.
23
+ */
24
+ /** Inclusive lower bound of the deterministic local-port range. */
25
+ export const MIN_PORT = 20000;
26
+ /** Inclusive upper bound of the deterministic local-port range. */
27
+ export const MAX_PORT = 29999;
28
+ /** Number of ports in the deterministic range (10000). */
29
+ export const PORT_RANGE = MAX_PORT - MIN_PORT + 1;
30
+ /** Number of hex characters in the routing pin (first 4 bytes of the hash). */
31
+ export const PIN_LENGTH = 8;
32
+ const SEPARATOR_FORWARD = '/';
33
+ const SEPARATOR_BACK = String.fromCharCode(92); // backslash
34
+ /**
35
+ * Invariant lowercasing that matches C# `string.ToLowerInvariant()`.
36
+ *
37
+ * JS `String.prototype.toLowerCase()` and C# `ToLowerInvariant()` disagree on some Unicode
38
+ * characters. The one that matters for real project paths — and the one the golden-vector file
39
+ * explicitly pins (`unicodeDivergence`) — is **U+0130 LATIN CAPITAL LETTER I WITH DOT ABOVE**:
40
+ * C# `ToLowerInvariant` leaves it unchanged, whereas a naive JS `toLowerCase()` folds it to
41
+ * `i` + U+0307 (COMBINING DOT ABOVE), producing a DIFFERENT hash. Preserve U+0130 so the TS
42
+ * derivation reproduces the canonical C# value byte-for-byte; every other character lowercases
43
+ * identically under both implementations.
44
+ */
45
+ export function toLowerInvariant(value) {
46
+ let result = '';
47
+ for (const ch of value) {
48
+ result += ch === 'İ' ? ch : ch.toLowerCase();
49
+ }
50
+ return result;
51
+ }
52
+ /**
53
+ * The exact string that is UTF-8/SHA-256 hashed: the project root with trailing directory
54
+ * separators trimmed, then invariant-lowercased. Exposed so callers/tests can reproduce the
55
+ * pre-hash string.
56
+ */
57
+ export function normalize(projectRoot) {
58
+ return toLowerInvariant(trimTrailingSeparators(projectRoot));
59
+ }
60
+ /** The routing pin only (first 8 lowercase hex chars of the hash). Never affected by overrides. */
61
+ export function derivePin(projectRoot) {
62
+ return hashOf(projectRoot).subarray(0, PIN_LENGTH / 2).toString('hex');
63
+ }
64
+ /**
65
+ * The pure hash-derived port (ignores any override). Byte-for-byte equivalent of the shipped
66
+ * Unity `GeneratePortFromDirectory` when given the same directory string.
67
+ */
68
+ export function derivePort(projectRoot) {
69
+ return portFromHash(hashOf(projectRoot));
70
+ }
71
+ /**
72
+ * The FULL project-path hash: the complete 64-char lowercase hex SHA-256 of the normalized
73
+ * project root — the `projectPathHash` an engine plugin sends in its hub instance-metadata
74
+ * handshake. The routing pin is a case-insensitive prefix of this value by construction.
75
+ */
76
+ export function deriveProjectPathHash(projectRoot) {
77
+ return hashOf(projectRoot).toString('hex');
78
+ }
79
+ /**
80
+ * Derive the identity for `projectRoot`. When `portOverride` is non-null (the user's explicit
81
+ * override from the project marker) it always wins for {@link ProjectIdentity.port}; the
82
+ * {@link ProjectIdentity.pin} is always hash-derived.
83
+ */
84
+ export function deriveProjectIdentity(projectRoot, portOverride) {
85
+ const hash = hashOf(projectRoot);
86
+ const pin = hash.subarray(0, PIN_LENGTH / 2).toString('hex');
87
+ if (portOverride !== undefined && portOverride !== null) {
88
+ return { pin, port: portOverride, portIsOverridden: true };
89
+ }
90
+ return { pin, port: portFromHash(hash), portIsOverridden: false };
91
+ }
92
+ function hashOf(projectRoot) {
93
+ if (projectRoot === null || projectRoot === undefined) {
94
+ throw new TypeError('projectRoot must be a string');
95
+ }
96
+ return createHash('sha256').update(Buffer.from(normalize(projectRoot), 'utf8')).digest();
97
+ }
98
+ function portFromHash(hash) {
99
+ // First 4 bytes as an explicit little-endian uint32 — matches the C# byte-shift
100
+ // (`hash[0] | hash[1]<<8 | hash[2]<<16 | hash[3]<<24`) and is CPU-endianness independent.
101
+ const value = hash.readUInt32LE(0);
102
+ return MIN_PORT + (value % PORT_RANGE);
103
+ }
104
+ function trimTrailingSeparators(path) {
105
+ let end = path.length;
106
+ while (end > 1 && (path[end - 1] === SEPARATOR_FORWARD || path[end - 1] === SEPARATOR_BACK)) {
107
+ end--;
108
+ }
109
+ return end === path.length ? path : path.slice(0, end);
110
+ }
111
+ //# sourceMappingURL=project-identity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-identity.js","sourceRoot":"","sources":["../../src/utils/project-identity.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * The tool-neutral, committable **project marker** `<project>/.ai-game-dev/project.json`
3
+ * (design 06 / D15). It is NON-secret — credentials NEVER go here — and records:
4
+ *
5
+ * - `serverTarget` — the hub URL the enrolled plugin should connect to (hosted
6
+ * `https://ai-game.dev` or a local `http://localhost:<port>`), returned by the
7
+ * enrollment-redeem endpoint.
8
+ * - `pin` — the D14 routing pin (first 8 hex of the ProjectIdentity SHA256), so
9
+ * the plugin, the CLIs, and `configure` all agree which project the session
10
+ * routes to.
11
+ * - `port` — the deterministic local port (SHA256→20000–29999) recorded for a
12
+ * localhost target so a terminal-written config and the plugin never diverge.
13
+ * - `portOverride` — an explicit user port override (D15) that always wins.
14
+ *
15
+ * ProjectIdentity resolution and every config writer consult it, so an override
16
+ * or target can never silently diverge between the plugin and a terminal-written
17
+ * config.
18
+ */
19
+ export declare const PROJECT_MARKER_RELATIVE_PATH: string;
20
+ export interface ProjectMarker {
21
+ /** The enrolled server target URL (hosted or local). */
22
+ serverTarget?: string;
23
+ /** The D14 routing pin (first 8 hex of the ProjectIdentity hash). */
24
+ pin?: string;
25
+ /** The deterministic local port (recorded for localhost targets). */
26
+ port?: number;
27
+ /** An explicit user port override (always wins over the derived port). */
28
+ portOverride?: number;
29
+ /** Forward-compatible: unknown keys are preserved on rewrite. */
30
+ [key: string]: unknown;
31
+ }
32
+ /** Absolute path of the marker file for a project. */
33
+ export declare function getProjectMarkerPath(projectPath: string): string;
34
+ /** Read the marker, or null when absent / empty / malformed. Never throws. */
35
+ export declare function readProjectMarker(projectPath: string): ProjectMarker | null;
36
+ /**
37
+ * Write the marker, MERGING over any existing document so a re-enroll never drops
38
+ * a previously-set `portOverride` or forward-compatible key. Creates the
39
+ * `.ai-game-dev/` directory if needed. The supplied `patch` fields overwrite the
40
+ * existing values; every other existing field is preserved.
41
+ */
42
+ export declare function writeProjectMarker(projectPath: string, patch: ProjectMarker): ProjectMarker;
43
+ /** True when `url`'s host is a loopback address (localhost / 127.0.0.1 / ::1). Never throws. */
44
+ export declare function isLocalhostUrl(url: string): boolean;
45
+ /**
46
+ * Append (or replace) the `/p/<pin>` routing segment on an MCP-server URL —
47
+ * the D14 project pin the plugin routes on. A URL already ending in `/p/<hex>`
48
+ * has its pin REPLACED (idempotent re-enroll); otherwise `/p/<pin>` is appended
49
+ * to the existing path. Query/hash are preserved. Returns the input unchanged on
50
+ * a malformed URL. Pure.
51
+ */
52
+ export declare function applyPinToUrl(url: string, pin: string): string;
53
+ /**
54
+ * Upsert the D14 pin into the URL of the `ai-game-developer` entry of every
55
+ * EXISTING project-local agent config (design 06 — "upserts the D14 pin into any
56
+ * existing project-local agent config entry"), so a hosted/local server the user
57
+ * added manually before enrolling becomes pinned. Best-effort: user-global
58
+ * configs (Claude Desktop, Cline, …) are skipped; a config with no
59
+ * `ai-game-developer` entry or no URL is left untouched; a malformed/unreadable
60
+ * file is skipped without throwing. Returns the absolute paths that were
61
+ * rewritten. JSON configs rewrite the `url`/`serverUrl` value; the Codex TOML
62
+ * config rewrites its `url = "…"` line.
63
+ */
64
+ export declare function upsertPinIntoAgentConfigs(projectPath: string, pin: string): string[];
@@ -0,0 +1,192 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { PIN_LENGTH } from './project-identity.js';
4
+ import { agentRegistry, MCP_SERVER_NAME } from './agents.js';
5
+ /**
6
+ * The tool-neutral, committable **project marker** `<project>/.ai-game-dev/project.json`
7
+ * (design 06 / D15). It is NON-secret — credentials NEVER go here — and records:
8
+ *
9
+ * - `serverTarget` — the hub URL the enrolled plugin should connect to (hosted
10
+ * `https://ai-game.dev` or a local `http://localhost:<port>`), returned by the
11
+ * enrollment-redeem endpoint.
12
+ * - `pin` — the D14 routing pin (first 8 hex of the ProjectIdentity SHA256), so
13
+ * the plugin, the CLIs, and `configure` all agree which project the session
14
+ * routes to.
15
+ * - `port` — the deterministic local port (SHA256→20000–29999) recorded for a
16
+ * localhost target so a terminal-written config and the plugin never diverge.
17
+ * - `portOverride` — an explicit user port override (D15) that always wins.
18
+ *
19
+ * ProjectIdentity resolution and every config writer consult it, so an override
20
+ * or target can never silently diverge between the plugin and a terminal-written
21
+ * config.
22
+ */
23
+ export const PROJECT_MARKER_RELATIVE_PATH = path.join('.ai-game-dev', 'project.json');
24
+ /** Absolute path of the marker file for a project. */
25
+ export function getProjectMarkerPath(projectPath) {
26
+ return path.join(projectPath, PROJECT_MARKER_RELATIVE_PATH);
27
+ }
28
+ /** Read the marker, or null when absent / empty / malformed. Never throws. */
29
+ export function readProjectMarker(projectPath) {
30
+ const markerPath = getProjectMarkerPath(projectPath);
31
+ if (!fs.existsSync(markerPath))
32
+ return null;
33
+ try {
34
+ const raw = fs.readFileSync(markerPath, 'utf-8');
35
+ if (raw.trim().length === 0)
36
+ return null;
37
+ const parsed = JSON.parse(raw);
38
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
39
+ return null;
40
+ return parsed;
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ }
46
+ /**
47
+ * Write the marker, MERGING over any existing document so a re-enroll never drops
48
+ * a previously-set `portOverride` or forward-compatible key. Creates the
49
+ * `.ai-game-dev/` directory if needed. The supplied `patch` fields overwrite the
50
+ * existing values; every other existing field is preserved.
51
+ */
52
+ export function writeProjectMarker(projectPath, patch) {
53
+ const markerPath = getProjectMarkerPath(projectPath);
54
+ const dir = path.dirname(markerPath);
55
+ if (!fs.existsSync(dir)) {
56
+ fs.mkdirSync(dir, { recursive: true });
57
+ }
58
+ const existing = readProjectMarker(projectPath) ?? {};
59
+ const merged = { ...existing, ...patch };
60
+ fs.writeFileSync(markerPath, JSON.stringify(merged, null, 2) + '\n');
61
+ return merged;
62
+ }
63
+ /** True when `url`'s host is a loopback address (localhost / 127.0.0.1 / ::1). Never throws. */
64
+ export function isLocalhostUrl(url) {
65
+ try {
66
+ const host = new URL(url).hostname.toLowerCase();
67
+ return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]';
68
+ }
69
+ catch {
70
+ return false;
71
+ }
72
+ }
73
+ /**
74
+ * Append (or replace) the `/p/<pin>` routing segment on an MCP-server URL —
75
+ * the D14 project pin the plugin routes on. A URL already ending in `/p/<hex>`
76
+ * has its pin REPLACED (idempotent re-enroll); otherwise `/p/<pin>` is appended
77
+ * to the existing path. Query/hash are preserved. Returns the input unchanged on
78
+ * a malformed URL. Pure.
79
+ */
80
+ export function applyPinToUrl(url, pin) {
81
+ let parsed;
82
+ try {
83
+ parsed = new URL(url);
84
+ }
85
+ catch {
86
+ return url;
87
+ }
88
+ const segments = parsed.pathname.split('/').filter((s) => s.length > 0);
89
+ const hexLen = PIN_LENGTH; // 8
90
+ const isPinHex = (s) => new RegExp(`^[0-9a-f]{${hexLen}}$`, 'i').test(s);
91
+ if (segments.length >= 2 && segments[segments.length - 2] === 'p' && isPinHex(segments[segments.length - 1])) {
92
+ segments[segments.length - 1] = pin;
93
+ }
94
+ else {
95
+ segments.push('p', pin);
96
+ }
97
+ parsed.pathname = '/' + segments.join('/');
98
+ return parsed.toString();
99
+ }
100
+ /** True when `configPath` resolves INSIDE `projectPath` (a project-local, not user-global, config). */
101
+ function isProjectLocalConfig(configPath, projectPath) {
102
+ const root = path.resolve(projectPath) + path.sep;
103
+ return path.resolve(configPath).startsWith(root);
104
+ }
105
+ /**
106
+ * Upsert the D14 pin into the URL of the `ai-game-developer` entry of every
107
+ * EXISTING project-local agent config (design 06 — "upserts the D14 pin into any
108
+ * existing project-local agent config entry"), so a hosted/local server the user
109
+ * added manually before enrolling becomes pinned. Best-effort: user-global
110
+ * configs (Claude Desktop, Cline, …) are skipped; a config with no
111
+ * `ai-game-developer` entry or no URL is left untouched; a malformed/unreadable
112
+ * file is skipped without throwing. Returns the absolute paths that were
113
+ * rewritten. JSON configs rewrite the `url`/`serverUrl` value; the Codex TOML
114
+ * config rewrites its `url = "…"` line.
115
+ */
116
+ export function upsertPinIntoAgentConfigs(projectPath, pin) {
117
+ const updated = [];
118
+ for (const agent of agentRegistry) {
119
+ let configPath;
120
+ try {
121
+ configPath = agent.getConfigPath(projectPath);
122
+ }
123
+ catch {
124
+ continue;
125
+ }
126
+ if (!isProjectLocalConfig(configPath, projectPath))
127
+ continue;
128
+ if (!fs.existsSync(configPath))
129
+ continue;
130
+ try {
131
+ if (agent.configFormat === 'toml') {
132
+ if (upsertPinInTomlConfig(configPath, pin))
133
+ updated.push(configPath);
134
+ }
135
+ else {
136
+ if (upsertPinInJsonConfig(configPath, agent.bodyPath, pin))
137
+ updated.push(configPath);
138
+ }
139
+ }
140
+ catch {
141
+ // Best-effort: a single unreadable/malformed config never fails enrollment.
142
+ }
143
+ }
144
+ return updated;
145
+ }
146
+ /** Rewrite the `url`/`serverUrl` of `bodyPath.ai-game-developer` in a JSON config. Returns true when changed. */
147
+ function upsertPinInJsonConfig(configPath, bodyPath, pin) {
148
+ const root = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
149
+ if (!root || typeof root !== 'object' || Array.isArray(root))
150
+ return false;
151
+ const body = root[bodyPath];
152
+ if (!body || typeof body !== 'object' || Array.isArray(body))
153
+ return false;
154
+ const entry = body[MCP_SERVER_NAME];
155
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
156
+ return false;
157
+ // The registry uses `url` for most agents and `serverUrl` for Antigravity.
158
+ const urlKey = typeof entry['url'] === 'string' ? 'url' : typeof entry['serverUrl'] === 'string' ? 'serverUrl' : null;
159
+ if (!urlKey)
160
+ return false;
161
+ const current = entry[urlKey];
162
+ const pinned = applyPinToUrl(current, pin);
163
+ if (pinned === current)
164
+ return false;
165
+ entry[urlKey] = pinned;
166
+ fs.writeFileSync(configPath, JSON.stringify(root, null, 2) + '\n');
167
+ return true;
168
+ }
169
+ /** Rewrite the `url = "…"` line under `[mcp_servers.ai-game-developer]` in a Codex TOML config. Returns true when changed. */
170
+ function upsertPinInTomlConfig(configPath, pin) {
171
+ const lines = fs.readFileSync(configPath, 'utf-8').split('\n');
172
+ const header = `[mcp_servers.${MCP_SERVER_NAME}]`;
173
+ const sectionIdx = lines.findIndex((l) => l.trim() === header);
174
+ if (sectionIdx < 0)
175
+ return false;
176
+ for (let i = sectionIdx + 1; i < lines.length; i++) {
177
+ const trimmed = lines[i].trim();
178
+ if (trimmed.startsWith('['))
179
+ break; // next section
180
+ const m = trimmed.match(/^url\s*=\s*"([^"]*)"\s*$/);
181
+ if (m) {
182
+ const pinned = applyPinToUrl(m[1], pin);
183
+ if (pinned === m[1])
184
+ return false;
185
+ lines[i] = `url = "${pinned}"`;
186
+ fs.writeFileSync(configPath, lines.join('\n'));
187
+ return true;
188
+ }
189
+ }
190
+ return false;
191
+ }
192
+ //# sourceMappingURL=project-marker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-marker.js","sourceRoot":"","sources":["../../src/utils/project-marker.ts"],"names":[],"mappings":"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"}