@baizor/gamedev-cli-core
v0.4.0
Published
Shared TypeScript CLI core for the AI Game Dev engine CLIs (Unity/Unreal/Godot): project identity/pin, OAuth device login, machine credential store, and setup-mcp/install-plugin logic. Consumed as thin adapters by the three engine CLIs.
Readme
AI-Game-Dev-CLI-Core
Shared TypeScript CLI core for the AI Game Dev engine CLIs (Unity / Unreal / Godot),
published to npm as @baizor/gamedev-cli-core via npm Trusted Publishing (OIDC, tokenless).
This package is the single source of truth for CLI logic; the three engine CLIs are thin,
engine-specific adapters over it. The shared modules land here through the auth-fixes design
(tasks b2/b3):
Landed (b2 — correctness/security core):
- project identity / pin —
derivePin/derivePort/deriveProjectPathHash(v1) and the…V2variants (separator-normalized, the B5 fix), gated byte-for-byte against the SAME golden vectors as the C# LIB (test/golden-vectors/, vendored fromMCP-Plugin-dotnet). - machine credential store —
MachineCredentialStoreat~/.ai-game-dev/credentials.json, fullMachineCredentials, DPAPI on Windows /0600on POSIX, atomic (crash-safe) writes. - OAuth 2.1 device-grant login —
deviceLogin(RFC 8628,/oauth/device_authorization+/oauth/token) with token rotation and a cleanlogin requiredon family-revoke.
- project identity / pin —
Landed (b3 — shared modules + the engine-adapter contract):
- engine-adapter contract —
EngineAdapter(+unityAdapter/unrealAdapter/godotAdapter), the single typed seam that carries every per-engine difference:serverName, project markers,stdioSupported+stdioArgs, the server install-dir layout,loginServerTarget, and the OAuthclientId. No engine specifics live anywhere else in the package. - agent-config writers —
JsonAiAgentConfig/TomlAiAgentConfig, byte-for-byte parity with the C#com.IvanMurzak.McpPlugin.AgentConfig, gated bytest/golden-vectors/AgentConfig.GoldenVectors.json, plus the engine-neutralagentRegistry. - setup-mcp policy —
setupMcp/resolveSetupMcpPlan: pins the routing URL by default (/mcp/p/<pin-v2>http,project=<pin>stdio; B4), with a--no-pinescape hatch, and writes a static credential only on an explicit--tokenopt-in (M7 — the default config is credential-free; the pin is routing-only, not part of the OAuth resource — M8). - install-plugin policy —
resolveInstallTarget: resolves the project pathpositional → --path → cwd(B1) then marker-probes it, failing with a message listing exactly what was checked. Ancestor walk-up is out of scope (M5). - enroll —
runEnroll/redeemEnrollmentCode: writes the v2 pin (the B5 fix replaces the Unity CLI's local\→/workaround) and records the AS-rootserverTarget, never a pinned hub URL (b2 review MED-2). - server-download —
downloadServerwith a fail-closedSHA256SUMSverify-before-execute gate and a dependency-free in-processparseZipunzip. - project-marker, validation, ui/progress utilities.
- engine-adapter contract —
Landed (c1/c2 + DCR — desktop browser sign-in):
- OAuth 2.1 authorization-code login —
authCodeLogin(RFC 8252 native app, RFC 7636 PKCE S256,stateCSRF check, an ephemeral127.0.0.1loopback listener, RFC 8707resource), returning the sameMachineCredentialsshape asdeviceLogin. - Discovery + dynamic client registration —
discoverAuthorizationServer(RFC 8414/.well-known/oauth-authorization-server, falling back to the hardcoded paths) andresolveClientRegistration/registerClient(RFC 7591)./oauth/authorizeresolvesclient_idby an exact registry lookup whose only writer isPOST /oauth/register— which mints its OWN id — so a hardcoded client id is always rejected withinvalid_client. The minted id is persisted per authorization server in~/.ai-game-dev/oauth-clients.json(ClientRegistrationStore, same atomic owner-only write as the credential store) and reused on every later launch; only the loopback port floats, so one registration serves every run. Oninvalid_clientthe flow re-registers exactly once. The device grant deliberately keeps its static client id — the server binds each refresh-token family to the id used at issue time, so moving it would invalidate every existing CLI login.
- OAuth 2.1 authorization-code login —
Landed (unified-machine-auth c3 — the shared refresher/provider + login plumbing):
MachineCredentialProvider— THE single entry point for credential access + refresh. The three engine CLIs (W2: d2/e2/f2) and the desktop App (W3) obtain access tokens EXCLUSIVELY viagetAccessToken({family})/refresh({family}); nothing else re-implements refresh. It is family-aware over store schema v2 (agent/plugin/legacyplanes), runs every refresh under the cross-processMachineCredentialLockwith a double-checked re-read (a peer's rotation is adopted without a network call), presents the family's storedclientId(component default ONLY forfamilies.legacy), omitsscope/resourceon refresh (P0-3), keeps the previous refresh token when the server does not rotate one, treatsinvalid_grantafter a post-failure re-read as family death (one structured telemetry event, other families untouched, never loops), rate-limits to one attempt per family per skew window, and surfaces busy locks / unreadable stores as typed non-sign-out errors.- RFC 8693 token exchange —
HttpTokenExchangeClient(the frozen a5 wire shape:subject_token= fresh agent access token, exact URNs,scope=mcp:plugin,audienceonly as the exacturn:agd:hub), deriving the plugin family from the agent family. - Login-surface commit plumbing —
commitAgentLogin(the F1/F2 two-lock-hold sequence: agent family under hold 1 → exchange → plugin family + v1 mirror under hold 2; a failed exchange leaves the committed agent family and a "partially authorized, retrying" state resumable viaderivePluginFamily),commitToolsOnlyLogin(the--tools-only/ O10 plugin-only mint for CI — F10), the D6/F7 account-switch guard (evaluateAccountSwitch/runAccountSwitchGuard; decline revokes the just-minted family and leaves the store untouched;runEnrollroutes through the same guard),signOutMachineWide(F6: best-effort RFC 7009 revocation of every family, then the lock-guarded store delete), andrevokeTokenBestEffort(RFC 7009). Every commit re-verifies the world under its lock hold before writing (commitFamilyUnderHold): a guard premise that changed during the confirm dialog, a subject switched between the two holds, a store deleted by a concurrent sign-out (never recreated), or a store turned unreadable (never overwritten) each abort with a typedCommitAbortReasonresult — never a throw, never a mixed-account or resurrected store.
A small semver utility slice is also exposed. The package has zero runtime dependencies.
Requirements
- Node.js >= 22.14.0
- npm >= 11.5.1 (required for OIDC Trusted Publishing; below this the publish silently degrades)
Develop
npm install # install dependencies
npm run build # tsc -> dist/ (ESM + .d.ts)
npm test # vitest run
npm run typecheck # tsc --noEmitRelease (npm Trusted Publishing / OIDC)
Releases are cut from the release.yml GitHub Actions workflow, which publishes to npm over OIDC
(no npm token, no --provenance flag — provenance is attached automatically). Run it from your
machine with the GitHub CLI:
gh workflow run release.yml -f version=X.Y.ZThe workflow enforces the Node >= 22.14.0 / npm >= 11.5.1 floors, sets the package version from the
version input, builds, and publishes.
Trusted Publishing binding (owner, one-time)
The npm Trusted Publisher for @baizor/gamedev-cli-core is bound to this exact repo + workflow
(.github/workflows/release.yml). The publish step lives directly in that workflow file — it is
not factored into a reusable workflow, because reusable-workflow bindings are a known npm TP
limitation for workflow_dispatch.
License
MIT © Ivan Murzak
