@forgeax/game 0.3.4 → 0.3.6

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/dist/main.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/main.ts
4
- import { resolve as resolve15 } from "node:path";
4
+ import { resolve as resolve17 } from "node:path";
5
5
 
6
6
  // src/mcp/protocol.ts
7
7
  var MCP_PROTOCOL_VERSION = "2024-11-05";
@@ -342,7 +342,7 @@ function engineGameId(root) {
342
342
  try {
343
343
  const manifest = JSON.parse(readFileSync(join2(root, "forge.json"), "utf8"));
344
344
  const pkg = JSON.parse(readFileSync(join2(root, "package.json"), "utf8"));
345
- return typeof manifest.id === "string" && SLUG_RE.test(manifest.id) && typeof manifest.entry === "string" && typeof pkg.dependencies?.["@forgeax/engine"] === "string" ? manifest.id : undefined;
345
+ return typeof manifest.id === "string" && SLUG_RE.test(manifest.id) && (manifest.schemaVersion === "2.0.0" ? typeof manifest.defaultScene === "string" && /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i.test(manifest.defaultScene) : (manifest.schemaVersion === undefined || manifest.schemaVersion === "1.0.0") && typeof manifest.entry === "string") && typeof pkg.dependencies?.["@forgeax/engine"] === "string" ? manifest.id : undefined;
346
346
  } catch {
347
347
  return;
348
348
  }
@@ -427,7 +427,8 @@ reasoning and game-code edits; the plugin owns the exact Engine Preview adapter.
427
427
 
428
428
  In an empty standalone directory, \`forgeax-game init\` creates the released Engine
429
429
  game and installs this guidance. In an existing exact Engine game it refreshes the
430
- binding idempotently. Standalone game source belongs in \`<game-root>/src\`; Studio's
430
+ binding idempotently. Follow \`forge.json\` and the installed Engine authoring layout
431
+ (v2 uses \`assets/\` and \`plugins[]\`), rather than prescribing \`src/\`. Studio's
431
432
  \`.forgeax/games/<slug>\` layout is a different hosted-project boundary and must not be
432
433
  invented here. Before claiming a requested game complete, replace the Empty template
433
434
  identity in \`forge.json\`, \`package.json\`, and README, keep tests/controls accurate,
@@ -438,7 +439,12 @@ transition or rule; renaming the Empty template test is not completion.
438
439
  Any run-tool error means the game is not previewed: never curl, reuse, open, or report
439
440
  an existing localhost port. HTTP 200 is not ownership evidence. Only a successful tool
440
441
  result containing \`preview.status: ready\`, root, build digest, instance ID, and its own
441
- \`preview_url\` authorizes a Preview claim.
442
+ \`preview_url\` authorizes a Preview claim, not a gameplay-verification claim.
443
+ For playable-game requests, check the requested interactions and visible feedback
444
+ with an available browser tool. If unavailable, report gameplay as unverified and
445
+ provide manual checks; do not stop useful implementation or invent test results.
446
+ Use the installed Engine contract for Host access and UI lifecycle, and verify
447
+ requested UI is mounted rather than silently skipping it when a Host lookup fails.
442
448
  Never substitute Studio, Editor, PATH, or a source checkout for the installed Engine
443
449
  release.`;
444
450
 
@@ -710,8 +716,8 @@ import { existsSync as existsSync3, lstatSync as lstatSync3, readFileSync as rea
710
716
  import { isAbsolute as isAbsolute2, relative as relative3, resolve as resolve4, sep as sep2 } from "node:path";
711
717
 
712
718
  // src/engine/constants.ts
713
- var ENGINE_VERSION = "0.1.7";
714
- var ENGINE_COMMIT = "984e80ac55b5f4f2afed3211c854b482a4018199";
719
+ var ENGINE_VERSION = "0.1.26";
720
+ var ENGINE_COMMIT = "f3d0db12405e168e4204e32a9cde32e0df8d87ae";
715
721
  var ENGINE_SDK_PACKAGE = "@forgeax/engine-sdk";
716
722
  var PNPM_VERSION = "11.7.0";
717
723
 
@@ -732,7 +738,7 @@ import { createRequire } from "node:module";
732
738
  import { basename, delimiter, dirname as dirname4, isAbsolute, relative as relative2, resolve as resolve3, sep } from "node:path";
733
739
  import { tmpdir } from "node:os";
734
740
  import { fileURLToPath as fileURLToPath2 } from "node:url";
735
- var SDK_MANIFEST_SCHEMA = "1.6.0";
741
+ var SDK_MANIFEST_SCHEMA = "1.7.0";
736
742
  var SDK_CLI_RELATIVE = ["bin", "forgeax.mjs"];
737
743
  var CARRIER_ENVIRONMENT_ALLOWLIST = [
738
744
  "HOME",
@@ -1690,7 +1696,7 @@ async function startEnginePreview(projectRoot, gameRoot, options = {}) {
1690
1696
  }
1691
1697
  const token = randomBytes(32).toString("hex");
1692
1698
  const previewInstanceId = randomUUID();
1693
- const child = spawn(process.execPath, [release.cliPath, "preview", "--json"], {
1699
+ const child = spawn(process.execPath, [release.cliPath, "preview", "--port", "0", "--json"], {
1694
1700
  cwd: release.gameRoot,
1695
1701
  detached: true,
1696
1702
  env: { ...process.env, FORGEAX_PREVIEW_INSTANCE_TOKEN: token },
@@ -2269,6 +2275,7 @@ not running check requested; no Engine child was launched.`;
2269
2275
  `preview.state_file: ${result.paths.state}`,
2270
2276
  `preview.stdout_log: ${result.paths.stdout}`,
2271
2277
  `preview.stderr_log: ${result.paths.stderr}`,
2278
+ "This tool verifies Engine build and Preview ownership only; gameplay, input, and visible UI have not been tested by this tool.",
2272
2279
  "Open only this returned loopback URL. HTTP availability without this exact verified identity is not Preview evidence."
2273
2280
  ].join(`
2274
2281
  `);
@@ -2946,12 +2953,12 @@ var RELEASE_IDENTITY_MIME = "application/vnd.forgeax.game-release-identity+json"
2946
2953
  var RELEASE_IDENTITY = Object.freeze({
2947
2954
  schema: RELEASE_IDENTITY_SCHEMA,
2948
2955
  gamePackage: "@forgeax/game",
2949
- gameVersion: "0.3.4",
2956
+ gameVersion: "0.3.5",
2950
2957
  gameBin: "forgeax-game",
2951
2958
  engineSdkPackage: ENGINE_SDK_PACKAGE,
2952
2959
  engineSdkVersion: ENGINE_VERSION,
2953
2960
  engineSourceCommit: ENGINE_COMMIT,
2954
- carrierIntegrity: "sha512-f3Nz+ZoIOLVjNeKPock46+gwmC1wOWh9TJupQPl25+wZbIN1dJmki5LK6mNhchPxF7fffarbcFWGMqpKHkjQSw==",
2961
+ carrierIntegrity: "sha512-G5ovsbdzkWWeMfFy3MxVk0qcAvWnZxJnlypMGgxFUDaKr3Wv9uVuhPV88LZ1A9mhFJJSfrE1d5R3WvHgulygRQ==",
2955
2962
  sdkManifestDigest: `sha256:${"0".repeat(64)}`,
2956
2963
  sdkTreeDigest: `sha256:${"0".repeat(64)}`,
2957
2964
  fullZipDigest: `sha256:${"0".repeat(64)}`,
@@ -3226,9 +3233,9 @@ async function startHttpMcpServer(spec, options) {
3226
3233
  }
3227
3234
 
3228
3235
  // src/cli/dispatch.ts
3229
- import { existsSync as existsSync12, readFileSync as readFileSync15, writeFileSync as writeFileSync11 } from "node:fs";
3236
+ import { existsSync as existsSync12, readFileSync as readFileSync17, writeFileSync as writeFileSync12 } from "node:fs";
3230
3237
  import { arch as arch2, homedir as homedir5, platform as platform2 } from "node:os";
3231
- import { join as join11, resolve as resolve14 } from "node:path";
3238
+ import { join as join12, resolve as resolve16 } from "node:path";
3232
3239
 
3233
3240
  // src/install/clients.ts
3234
3241
  import { homedir as homedir2 } from "node:os";
@@ -3356,7 +3363,7 @@ function launchSpec(mode) {
3356
3363
  }
3357
3364
  return {
3358
3365
  command: "npx",
3359
- args: ["-y", "-p", "@forgeax/game@0.3.4", "forgeax-game", "mcp"]
3366
+ args: ["-y", "-p", "@forgeax/game@0.3.5", "forgeax-game", "mcp"]
3360
3367
  };
3361
3368
  }
3362
3369
  function asset3dLaunchSpec(mode) {
@@ -4270,17 +4277,17 @@ var PROVIDER_BUNDLE_SCHEMA = "forgeax.asset3d-provider-bundle/1.0.0";
4270
4277
  var PROVIDER_RESULT_SCHEMA = "forgeax.asset3d-search-result/1.0.0";
4271
4278
  var PROVIDER_RECEIPT_SCHEMA = "forgeax.asset3d-search-receipt/1.0.0";
4272
4279
  var K0_PACKAGE_SHA256 = "4937fe00e2b919319c70c82db53ebe359b275ed662c026153ecb97f7601262b4";
4273
- var RESULT_SCHEMA_SHA256 = "ab6e5e1e794d5428efef362fe32d4c54b6541e12539bae5f6ff27680c9f380ba";
4280
+ var RESULT_SCHEMA_SHA256 = "f97a9aa8b2ce0662c37d314211aae52951cbd0b76b3a7c140dc608be4712c415";
4274
4281
  var RECEIPT_SCHEMA_SHA256 = "e0dc9e9fe9872f09af9fca6d0c17d22aca63c2b546c55c0b669c581f7059b9c4";
4275
- var ASSET3D_PROVIDER_COMMIT = "c181c48fbffc933a7ce9a0836f7878ca5e6d77e1";
4282
+ var ASSET3D_PROVIDER_COMMIT = "a274238712c1dced99e4aa3148a0d15904ce2a90";
4276
4283
  var BUNDLED_ASSET3D_PROVIDERS = Object.freeze({
4277
4284
  "darwin-arm64": {
4278
- sha256: "9492c507a3afe5cafcd50b5f782b313c8716c2e52f5def06592c6a100c0459db",
4279
- relativePath: "asset3d/provider/asset3d-search-provider-c181c48fbffc933a7ce9a0836f7878ca5e6d77e1-darwin-arm64.tar.gz"
4285
+ sha256: "4f2d971c037a32eaed20e51e13a88bff91d2ea8f4d37894526c089376c3bacba",
4286
+ relativePath: "asset3d/provider/asset3d-search-provider-a274238712c1dced99e4aa3148a0d15904ce2a90-darwin-arm64.tar.gz"
4280
4287
  },
4281
4288
  "linux-x64": {
4282
- sha256: "e6ac3df76c8b82f9fe2c063dd3f221324a9dad5176fd52535a5144b0ff6002d8",
4283
- relativePath: "asset3d/provider/asset3d-search-provider-c181c48fbffc933a7ce9a0836f7878ca5e6d77e1-linux-x64.tar.gz"
4289
+ sha256: "af97e7e267b558960156175c840c3c2d2cd7aabf68f1f72c7d64b6d50742a79e",
4290
+ relativePath: "asset3d/provider/asset3d-search-provider-a274238712c1dced99e4aa3148a0d15904ce2a90-linux-x64.tar.gz"
4284
4291
  }
4285
4292
  });
4286
4293
  var INSTALL_SCHEMA = "forgeax.asset3d-install/1.0.0";
@@ -4675,7 +4682,8 @@ function installSkills(projectRoot, tgz, clients, previous) {
4675
4682
  for (const client of clients) {
4676
4683
  const mount = SKILL_MOUNTS[client];
4677
4684
  const destination = resolve10(projectRoot, mount, SKILL_ID);
4678
- const projected = [...K0_FILES.map((file) => [file, extractK0(tgz, file)]), ["SKILL.md", extractK0(tgz, "PLATFORM.md")]];
4685
+ const adapter = readFileSync12(resolve10(dirname10(tgz), "adapter", "SKILL.md"));
4686
+ const projected = [...K0_FILES.map((file) => [`legacy/${file}`, extractK0(tgz, file)]), ["SKILL.md", adapter]];
4679
4687
  if (existsSync8(destination)) {
4680
4688
  const priorOwned = new Set(previous?.skills.filter((entry) => confined3(destination, entry.path)).map((entry) => entry.path) ?? []);
4681
4689
  const unexpected = filesUnder2(destination).filter((path) => !priorOwned.has(path));
@@ -4957,19 +4965,19 @@ import {
4957
4965
  cpSync as cpSync2,
4958
4966
  existsSync as existsSync9,
4959
4967
  fstatSync,
4960
- lstatSync as lstatSync8,
4968
+ lstatSync as lstatSync9,
4961
4969
  mkdirSync as mkdirSync10,
4962
4970
  openSync as openSync4,
4963
- readFileSync as readFileSync13,
4971
+ readFileSync as readFileSync14,
4964
4972
  readdirSync as readdirSync7,
4965
- realpathSync as realpathSync8,
4973
+ realpathSync as realpathSync9,
4966
4974
  renameSync as renameSync7,
4967
4975
  rmSync as rmSync6,
4968
4976
  statSync as statSync6,
4969
4977
  unlinkSync as unlinkSync3,
4970
4978
  writeFileSync as writeFileSync10
4971
4979
  } from "node:fs";
4972
- import { isAbsolute as isAbsolute4, relative as relative9, resolve as resolve11, sep as sep6 } from "node:path";
4980
+ import { isAbsolute as isAbsolute5, relative as relative10, resolve as resolve12, sep as sep7 } from "node:path";
4973
4981
 
4974
4982
  // src/asset3d/schema.ts
4975
4983
  var ID = /^[A-Za-z0-9._-]{1,128}$/;
@@ -4988,7 +4996,7 @@ var ERROR_CODES = new Set([
4988
4996
  "batch_timeout",
4989
4997
  "internal_error"
4990
4998
  ]);
4991
- var ROLES = new Set(["primary-model", "animation", "auxiliary-model", "texture", "metadata"]);
4999
+ var ROLES = new Set(["primary-pack", "auxiliary-pack", "primary-model", "animation", "auxiliary-model", "texture", "metadata"]);
4992
5000
  function exactKeys(record, allowed, field) {
4993
5001
  const extras = Object.keys(record).filter((key) => !allowed.includes(key));
4994
5002
  if (extras.length)
@@ -5078,9 +5086,12 @@ function parseProviderResult(input, expectedCommit, expectedOriginSetDigest) {
5078
5086
  }
5079
5087
  if (row.status !== "ok")
5080
5088
  throw new Error("provider_result_invalid: status");
5081
- exactKeys(row, ["status", "queryIndex", "query", "provider", "providerAssetId", "assetName", "deliveredFormat", "sha256", "bytes", "primaryModel", "manifest", "originSetDigest", "downloaded_to"], "success");
5089
+ const isPack = row.deliveredFormat === "pack";
5090
+ const primaryKey = isPack ? "primaryPack" : "primaryModel";
5091
+ const primaryRole = isPack ? "primary-pack" : "primary-model";
5092
+ exactKeys(row, ["status", "queryIndex", "query", "provider", "providerAssetId", "assetName", "deliveredFormat", "sha256", "bytes", primaryKey, "manifest", "originSetDigest", "downloaded_to"], "success");
5082
5093
  okCount++;
5083
- if (row.provider !== "ea-3d" || row.deliveredFormat !== "glb" || typeof row.providerAssetId !== "string" || !ID.test(row.providerAssetId) || row.providerAssetId === "." || row.providerAssetId === "..") {
5094
+ if (row.provider !== "ea-3d" || row.deliveredFormat !== "glb" && !isPack || typeof row.providerAssetId !== "string" || !ID.test(row.providerAssetId) || row.providerAssetId === "." || row.providerAssetId === "..") {
5084
5095
  throw new Error("provider_result_invalid: success identity");
5085
5096
  }
5086
5097
  const assetName = text(row.assetName, 1, 128, "assetName");
@@ -5108,13 +5119,18 @@ function parseProviderResult(input, expectedCommit, expectedOriginSetDigest) {
5108
5119
  if ((role === "primary-model" || role === "animation" || role === "auxiliary-model") && !path.toLowerCase().endsWith(".glb")) {
5109
5120
  throw new Error("provider_result_invalid: model entry must be GLB");
5110
5121
  }
5122
+ if ((role === "primary-pack" || role === "auxiliary-pack") && (!isPack || !/\.pack\.(json|ts)$/i.test(path))) {
5123
+ throw new Error("provider_result_invalid: pack entry must be authored Pack");
5124
+ }
5125
+ if (isPack && role === "primary-model")
5126
+ throw new Error("provider_result_invalid: multiple primary formats");
5111
5127
  return { path, role, bytes: entryBytes, sha256: item.sha256 };
5112
5128
  });
5113
5129
  const sorted = [...manifest].sort((left, right) => Buffer.from(left.path).compare(Buffer.from(right.path)));
5114
5130
  if (manifest.some((entry, index) => entry.path !== sorted[index].path))
5115
5131
  throw new Error("provider_result_invalid: manifest must be UTF-8 path sorted");
5116
- const primary = manifest.filter((entry) => entry.role === "primary-model");
5117
- if (primary.length !== 1 || row.primaryModel !== primary[0].path)
5132
+ const primary = manifest.filter((entry) => entry.role === primaryRole);
5133
+ if (primary.length !== 1 || row[primaryKey] !== primary[0].path)
5118
5134
  throw new Error("provider_result_invalid: primary model");
5119
5135
  if (manifest.reduce((sum, entry) => sum + entry.bytes, 0) !== itemBytes || aggregate(manifest) !== row.sha256) {
5120
5136
  throw new Error("provider_result_invalid: byte or aggregate digest mismatch");
@@ -5128,10 +5144,9 @@ function parseProviderResult(input, expectedCommit, expectedOriginSetDigest) {
5128
5144
  provider: "ea-3d",
5129
5145
  providerAssetId: row.providerAssetId,
5130
5146
  assetName,
5131
- deliveredFormat: "glb",
5132
5147
  sha256: row.sha256,
5133
5148
  bytes: itemBytes,
5134
- primaryModel: row.primaryModel,
5149
+ ...isPack ? { deliveredFormat: "pack", primaryPack: row.primaryPack } : { deliveredFormat: "glb", primaryModel: row.primaryModel },
5135
5150
  manifest,
5136
5151
  originSetDigest: expectedOriginSetDigest,
5137
5152
  ...row.downloaded_to === undefined ? {} : { downloaded_to: row.downloaded_to }
@@ -5142,24 +5157,117 @@ function parseProviderResult(input, expectedCommit, expectedOriginSetDigest) {
5142
5157
  return { schemaVersion: PROVIDER_RESULT_SCHEMA, total, succeeded, failed, results, ...receipt ? { receipt } : {} };
5143
5158
  }
5144
5159
 
5160
+ // src/asset3d/pack-readback.ts
5161
+ import { lstatSync as lstatSync8, readFileSync as readFileSync13, realpathSync as realpathSync8 } from "node:fs";
5162
+ import { isAbsolute as isAbsolute4, relative as relative9, resolve as resolve11, sep as sep6 } from "node:path";
5163
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
5164
+ function object(value) {
5165
+ if (!value || typeof value !== "object" || Array.isArray(value))
5166
+ throw new Error("engine_pack_readback_invalid");
5167
+ return value;
5168
+ }
5169
+ function readPackBuildCatalog(projectRoot, sourcePaths, buildValue) {
5170
+ const build = object(buildValue);
5171
+ const runtime = object(build.runtime);
5172
+ if (build.schemaVersion !== "1.0.0" || !Array.isArray(build.artifacts) || build.artifacts.length > 1e4) {
5173
+ throw new Error("engine_pack_build_manifest_invalid");
5174
+ }
5175
+ const dist = resolve11(projectRoot, "dist");
5176
+ const canonicalDist = realpathSync8(dist);
5177
+ if (canonicalDist !== resolve11(realpathSync8(projectRoot), "dist") || lstatSync8(dist).isSymbolicLink()) {
5178
+ throw new Error("engine_pack_build_path_escape");
5179
+ }
5180
+ const artifacts = new Map;
5181
+ let total = 0;
5182
+ for (const raw of build.artifacts) {
5183
+ const entry = object(raw);
5184
+ const path = safeRelativePath(entry.path);
5185
+ if (artifacts.has(path) || !Number.isSafeInteger(entry.bytes) || entry.bytes < 0 || entry.bytes > 256 * 1024 * 1024 || typeof entry.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(entry.sha256)) {
5186
+ throw new Error("engine_pack_build_manifest_invalid");
5187
+ }
5188
+ total += entry.bytes;
5189
+ if (total > 1024 * 1024 * 1024)
5190
+ throw new Error("engine_pack_build_too_large");
5191
+ const file = resolve11(dist, path);
5192
+ const rel = relative9(canonicalDist, realpathSync8(file));
5193
+ if (isAbsolute4(rel) || rel === ".." || rel.startsWith(`..${sep6}`))
5194
+ throw new Error("engine_pack_build_path_escape");
5195
+ const stat = lstatSync8(file);
5196
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size !== entry.bytes)
5197
+ throw new Error("engine_pack_build_file_invalid");
5198
+ const bytes = readFileSync13(file);
5199
+ if (sha2562(bytes) !== entry.sha256)
5200
+ throw new Error("engine_pack_build_digest_mismatch");
5201
+ artifacts.set(path, path.endsWith(".json") ? bytes : Buffer.alloc(0));
5202
+ }
5203
+ const indexPath = safeRelativePath(runtime.packIndexUrl);
5204
+ const indexBytes = artifacts.get(indexPath);
5205
+ if (!indexBytes || indexBytes.length > 16 * 1024 * 1024)
5206
+ throw new Error("engine_pack_catalog_missing");
5207
+ const index = JSON.parse(indexBytes.toString("utf8"));
5208
+ if (!Array.isArray(index))
5209
+ throw new Error("engine_pack_catalog_invalid");
5210
+ const sources = new Set(sourcePaths);
5211
+ const rows = [];
5212
+ const seen = new Set;
5213
+ const inspect = [];
5214
+ for (const raw of index) {
5215
+ const row = object(raw);
5216
+ if (typeof row.sourcePath !== "string" || !sources.has(row.sourcePath))
5217
+ continue;
5218
+ if (typeof row.guid !== "string" || !UUID.test(row.guid) || seen.has(row.guid) || typeof row.kind !== "string" || typeof row.packageUrl !== "string" || !row.packageUrl.startsWith("/") || row.packageUrl.startsWith("//")) {
5219
+ throw new Error("engine_pack_catalog_invalid");
5220
+ }
5221
+ seen.add(row.guid);
5222
+ const packagePath = safeRelativePath(row.packageUrl.slice(1));
5223
+ const bytes = artifacts.get(packagePath);
5224
+ if (!bytes || bytes.length > 16 * 1024 * 1024)
5225
+ throw new Error("engine_pack_product_missing");
5226
+ const pack = object(JSON.parse(bytes.toString("utf8")));
5227
+ if (!Array.isArray(pack.assets))
5228
+ throw new Error("engine_pack_product_invalid");
5229
+ const matches = pack.assets.map(object).filter((asset) => asset.guid === row.guid && asset.kind === row.kind);
5230
+ if (matches.length !== 1)
5231
+ throw new Error("engine_pack_product_guid_missing");
5232
+ rows.push({
5233
+ guid: row.guid,
5234
+ kind: row.kind,
5235
+ source: row.sourcePath,
5236
+ sourcePath: row.sourcePath,
5237
+ ...typeof row.name === "string" ? { name: row.name } : {},
5238
+ packageUrl: row.packageUrl
5239
+ });
5240
+ inspect.push({ guid: row.guid, kind: row.kind, packagePath, packageSha256: sha2562(bytes), verified: true });
5241
+ }
5242
+ if (!rows.some((row) => row.kind === "scene" || row.kind === "mesh"))
5243
+ throw new Error("engine_pack_readback_missing_renderable");
5244
+ return {
5245
+ authority: "engine-build-catalog",
5246
+ verify: { artifacts: artifacts.size, bytes: total, catalogSha256: sha2562(indexBytes) },
5247
+ list: rows,
5248
+ rows,
5249
+ inspect
5250
+ };
5251
+ }
5252
+
5145
5253
  // src/asset3d/transaction.ts
5146
5254
  function confined4(root, candidate) {
5147
- const rel = relative9(root, candidate);
5148
- return rel === "" || !isAbsolute4(rel) && rel !== ".." && !rel.startsWith(`..${sep6}`);
5255
+ const rel = relative10(root, candidate);
5256
+ return rel === "" || !isAbsolute5(rel) && rel !== ".." && !rel.startsWith(`..${sep7}`);
5149
5257
  }
5150
5258
  function forgeaxRoot(projectRoot) {
5151
- return resolve11(projectRoot, ".forgeax");
5259
+ return resolve12(projectRoot, ".forgeax");
5152
5260
  }
5153
5261
  function journalPath(projectRoot, execution) {
5154
- return resolve11(forgeaxRoot(projectRoot), "asset3d-transactions", `${execution}.json`);
5262
+ return resolve12(forgeaxRoot(projectRoot), "asset3d-transactions", `${execution}.json`);
5155
5263
  }
5156
5264
  function providerResultPath(projectRoot, execution) {
5157
- return resolve11(forgeaxRoot(projectRoot), "asset3d-results", `${execution}.json`);
5265
+ return resolve12(forgeaxRoot(projectRoot), "asset3d-results", `${execution}.json`);
5158
5266
  }
5159
5267
  function readInstall(projectRoot) {
5160
5268
  let manifest;
5161
5269
  try {
5162
- manifest = JSON.parse(readFileSync13(resolve11(forgeaxRoot(projectRoot), "asset3d-install.json"), "utf8"));
5270
+ manifest = JSON.parse(readFileSync14(resolve12(forgeaxRoot(projectRoot), "asset3d-install.json"), "utf8"));
5163
5271
  } catch {
5164
5272
  throw new Error("asset3d_not_installed");
5165
5273
  }
@@ -5172,7 +5280,7 @@ function readJournal(projectRoot, execution) {
5172
5280
  throw new Error("asset3d_execution_invalid");
5173
5281
  let journal;
5174
5282
  try {
5175
- journal = JSON.parse(readFileSync13(journalPath(projectRoot, execution), "utf8"));
5283
+ journal = JSON.parse(readFileSync14(journalPath(projectRoot, execution), "utf8"));
5176
5284
  } catch {
5177
5285
  throw new Error("asset3d_execution_not_found");
5178
5286
  }
@@ -5187,7 +5295,7 @@ function updateJournal(projectRoot, journal, patch) {
5187
5295
  return next;
5188
5296
  }
5189
5297
  function beginAsset3d(projectRootInput, queries, options = {}) {
5190
- const projectRoot = realpathSync8(projectRootInput);
5298
+ const projectRoot = realpathSync9(projectRootInput);
5191
5299
  if (queries.length < 1 || queries.length > 16 || queries.some((query) => [...query].length < 1 || [...query].length > 200)) {
5192
5300
  throw new Error("asset3d_queries_invalid: expected 1..16 queries of 1..200 characters");
5193
5301
  }
@@ -5195,9 +5303,9 @@ function beginAsset3d(projectRootInput, queries, options = {}) {
5195
5303
  const release = resolveEngineRelease(projectRoot, options.carrierPluginRoot === undefined ? {} : { pluginRoot: options.carrierPluginRoot });
5196
5304
  const execution = randomUUID4();
5197
5305
  const relativeOutput = `workspace/asset3d/${execution}`;
5198
- const quarantine = resolve11(projectRoot, ".forgeax", "asset3d-quarantine");
5199
- const allowedQuarantineRoot = resolve11(quarantine, relativeOutput);
5200
- ensurePrivateDir(resolve11(projectRoot, ".forgeax", "asset3d-transactions"));
5306
+ const quarantine = resolve12(projectRoot, ".forgeax", "asset3d-quarantine");
5307
+ const allowedQuarantineRoot = resolve12(quarantine, relativeOutput);
5308
+ ensurePrivateDir(resolve12(projectRoot, ".forgeax", "asset3d-transactions"));
5201
5309
  ensurePrivateDir(allowedQuarantineRoot);
5202
5310
  const journal = {
5203
5311
  schemaVersion: TRANSACTION_SCHEMA,
@@ -5216,7 +5324,7 @@ function beginAsset3d(projectRootInput, queries, options = {}) {
5216
5324
  return { execution, output_dir: relativeOutput };
5217
5325
  }
5218
5326
  function asset3dSearchOutputDir(projectRootInput, execution, queries) {
5219
- const projectRoot = realpathSync8(projectRootInput);
5327
+ const projectRoot = realpathSync9(projectRootInput);
5220
5328
  const journal = readJournal(projectRoot, execution);
5221
5329
  if (journal.state !== "begun") {
5222
5330
  throw new Error(`asset3d_execution_state_invalid: ${journal.state}`);
@@ -5225,14 +5333,14 @@ function asset3dSearchOutputDir(projectRootInput, execution, queries) {
5225
5333
  throw new Error("asset3d_search_query_identity_mismatch");
5226
5334
  }
5227
5335
  const outputDir = `workspace/asset3d/${execution}`;
5228
- const expectedRoot = resolve11(forgeaxRoot(projectRoot), "asset3d-quarantine", outputDir);
5229
- if (journal.allowedQuarantineRoot !== expectedRoot || !existsSync9(expectedRoot) || realpathSync8(expectedRoot) !== expectedRoot) {
5336
+ const expectedRoot = resolve12(forgeaxRoot(projectRoot), "asset3d-quarantine", outputDir);
5337
+ if (journal.allowedQuarantineRoot !== expectedRoot || !existsSync9(expectedRoot) || realpathSync9(expectedRoot) !== expectedRoot) {
5230
5338
  throw new Error("asset3d_search_output_identity_mismatch");
5231
5339
  }
5232
5340
  return outputDir;
5233
5341
  }
5234
5342
  function recordAsset3dProviderResult(projectRootInput, execution, providerResult) {
5235
- const projectRoot = realpathSync8(projectRootInput);
5343
+ const projectRoot = realpathSync9(projectRootInput);
5236
5344
  const journal = readJournal(projectRoot, execution);
5237
5345
  if (journal.state !== "begun") {
5238
5346
  throw new Error(`asset3d_execution_state_invalid: ${journal.state}`);
@@ -5241,19 +5349,19 @@ function recordAsset3dProviderResult(projectRootInput, execution, providerResult
5241
5349
  if (bytes < 1 || bytes > MAX_JSON_BYTES) {
5242
5350
  throw new Error("provider_result_too_large: expected 1 byte..1 MiB");
5243
5351
  }
5244
- ensurePrivateDir(resolve11(forgeaxRoot(projectRoot), "asset3d-results"));
5352
+ ensurePrivateDir(resolve12(forgeaxRoot(projectRoot), "asset3d-results"));
5245
5353
  atomicWrite(providerResultPath(projectRoot, execution), providerResult, 384);
5246
5354
  updateJournal(projectRoot, journal, { state: "provider_complete" });
5247
5355
  }
5248
5356
  function fileBytesChecked(root, entry) {
5249
- const source = resolve11(root, entry.path);
5357
+ const source = resolve12(root, entry.path);
5250
5358
  if (!confined4(root, source))
5251
5359
  throw new Error("asset_manifest_escape");
5252
- const canonicalRoot = realpathSync8(root);
5253
- const canonicalSource = realpathSync8(source);
5360
+ const canonicalRoot = realpathSync9(root);
5361
+ const canonicalSource = realpathSync9(source);
5254
5362
  if (!confined4(canonicalRoot, canonicalSource))
5255
5363
  throw new Error("asset_manifest_escape");
5256
- const before = lstatSync8(source);
5364
+ const before = lstatSync9(source);
5257
5365
  if (!before.isFile() || before.isSymbolicLink())
5258
5366
  throw new Error("asset_manifest_not_regular");
5259
5367
  const fd = openSync4(source, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
@@ -5261,7 +5369,7 @@ function fileBytesChecked(root, entry) {
5261
5369
  const opened = fstatSync(fd);
5262
5370
  if (!opened.isFile() || opened.size !== entry.bytes)
5263
5371
  throw new Error("asset_manifest_bytes_mismatch");
5264
- const bytes = readFileSync13(fd);
5372
+ const bytes = readFileSync14(fd);
5265
5373
  if (sha2562(bytes) !== entry.sha256)
5266
5374
  throw new Error("asset_manifest_digest_mismatch");
5267
5375
  return bytes;
@@ -5270,9 +5378,9 @@ function fileBytesChecked(root, entry) {
5270
5378
  }
5271
5379
  }
5272
5380
  function acquireLock2(projectRoot, assetId, timeoutMs) {
5273
- const root = resolve11(forgeaxRoot(projectRoot), "asset3d-locks");
5381
+ const root = resolve12(forgeaxRoot(projectRoot), "asset3d-locks");
5274
5382
  ensurePrivateDir(root);
5275
- const path = resolve11(root, `${assetId}.lock`);
5383
+ const path = resolve12(root, `${assetId}.lock`);
5276
5384
  const token = randomUUID4();
5277
5385
  const deadline = Date.now() + timeoutMs;
5278
5386
  for (;; ) {
@@ -5283,7 +5391,7 @@ function acquireLock2(projectRoot, assetId, timeoutMs) {
5283
5391
  closeSync3(fd);
5284
5392
  return () => {
5285
5393
  try {
5286
- const current = JSON.parse(readFileSync13(path, "utf8"));
5394
+ const current = JSON.parse(readFileSync14(path, "utf8"));
5287
5395
  if (current.token === token)
5288
5396
  unlinkSync3(path);
5289
5397
  } catch {}
@@ -5332,17 +5440,17 @@ function engineAddedRows(projectRoot, asset) {
5332
5440
  if (!Array.isArray(subAssets)) {
5333
5441
  if (typeof asset.metaPath !== "string")
5334
5442
  throw new Error("engine_readback_missing_meta");
5335
- const metaPath = resolve11(projectRoot, asset.metaPath);
5443
+ const metaPath = resolve12(projectRoot, asset.metaPath);
5336
5444
  if (!confined4(projectRoot, metaPath))
5337
5445
  throw new Error("engine_readback_meta_escape");
5338
- const canonical = realpathSync8(metaPath);
5339
- const info = lstatSync8(metaPath);
5446
+ const canonical = realpathSync9(metaPath);
5447
+ const info = lstatSync9(metaPath);
5340
5448
  if (!confined4(projectRoot, canonical) || !info.isFile() || info.isSymbolicLink()) {
5341
5449
  throw new Error("engine_readback_meta_invalid");
5342
5450
  }
5343
5451
  let meta;
5344
5452
  try {
5345
- meta = JSON.parse(readFileSync13(metaPath, "utf8"));
5453
+ meta = JSON.parse(readFileSync14(metaPath, "utf8"));
5346
5454
  } catch {
5347
5455
  throw new Error("engine_readback_meta_invalid");
5348
5456
  }
@@ -5353,10 +5461,10 @@ function engineAddedRows(projectRoot, asset) {
5353
5461
  const projectPath = (value) => {
5354
5462
  if (typeof value !== "string")
5355
5463
  return value;
5356
- const absolute = resolve11(projectRoot, value);
5464
+ const absolute = resolve12(projectRoot, value);
5357
5465
  if (!confined4(projectRoot, absolute))
5358
5466
  throw new Error("engine_readback_path_escape");
5359
- return relative9(projectRoot, absolute).split(sep6).join("/");
5467
+ return relative10(projectRoot, absolute).split(sep7).join("/");
5360
5468
  };
5361
5469
  return subAssets.map((row) => ({ source: projectPath(asset.source), metaPath: projectPath(asset.metaPath), reused: asset.reused, ...row }));
5362
5470
  }
@@ -5369,24 +5477,38 @@ function engineReadback(projectRoot, assetRelative, add, carrierPluginRoot) {
5369
5477
  if (add && guids.length === 0)
5370
5478
  throw new Error("engine_readback_missing_guid");
5371
5479
  const inspect = guids.map((guid) => engineCommand(projectRoot, ["asset", "inspect", guid, "--json"], carrierPluginRoot).value);
5372
- const listed = Array.isArray(list.value) ? list.value : [];
5480
+ const listed = Array.isArray(list.value) ? list.value : Array.isArray(list.value?.assets) ? list.value.assets : [];
5373
5481
  for (const guid of guids)
5374
5482
  if (!listed.some((entry) => entry.guid === guid))
5375
5483
  throw new Error("engine_catalog_readback_missing");
5376
- return { ...add ? { add: add.value } : {}, verify: verify.value, list: list.value, inspect, rows };
5484
+ return { ...add ? { add: add.value } : {}, verify: verify.value, list: listed, inspect, rows };
5485
+ }
5486
+ function packReadback(projectRoot, item, carrierPluginRoot) {
5487
+ const release = resolveEngineRelease(projectRoot, carrierPluginRoot === undefined ? {} : { pluginRoot: carrierPluginRoot });
5488
+ const result = spawnSync4(process.execPath, [release.cliPath, "build", "--json"], {
5489
+ cwd: projectRoot,
5490
+ encoding: "utf8",
5491
+ timeout: 150000,
5492
+ maxBuffer: 8 * 1024 * 1024
5493
+ });
5494
+ if (result.error || result.status !== 0)
5495
+ throw new Error("engine_pack_build_failed");
5496
+ const envelope = parseEnvelope(result.stdout, "build");
5497
+ const prefix = `assets/3d/ea-3d/${item.providerAssetId}/`;
5498
+ return readPackBuildCatalog(projectRoot, item.manifest.map((entry) => prefix + entry.path), envelope.value);
5377
5499
  }
5378
5500
  function priorProvenance(destination) {
5379
5501
  try {
5380
- return JSON.parse(readFileSync13(resolve11(destination, ".forgeax-asset.json"), "utf8"));
5502
+ return JSON.parse(readFileSync14(resolve12(destination, ".forgeax-asset.json"), "utf8"));
5381
5503
  } catch {
5382
5504
  return;
5383
5505
  }
5384
5506
  }
5385
5507
  function liveFilesMatch(destination, item) {
5386
5508
  return item.manifest.every((entry) => {
5387
- const path = resolve11(destination, entry.path);
5509
+ const path = resolve12(destination, entry.path);
5388
5510
  try {
5389
- return confined4(destination, path) && statSync6(path).isFile() && statSync6(path).size === entry.bytes && sha2562(readFileSync13(path)) === entry.sha256;
5511
+ return confined4(destination, path) && statSync6(path).isFile() && statSync6(path).size === entry.bytes && sha2562(readFileSync14(path)) === entry.sha256;
5390
5512
  } catch {
5391
5513
  return false;
5392
5514
  }
@@ -5395,21 +5517,21 @@ function liveFilesMatch(destination, item) {
5395
5517
  function quarantineFiles(root, directory = root) {
5396
5518
  const files = [];
5397
5519
  for (const name of readdirSync7(directory)) {
5398
- const path = resolve11(directory, name);
5399
- const info = lstatSync8(path);
5520
+ const path = resolve12(directory, name);
5521
+ const info = lstatSync9(path);
5400
5522
  if (info.isSymbolicLink())
5401
5523
  throw new Error("asset_quarantine_symlink_rejected");
5402
5524
  if (info.isDirectory())
5403
5525
  files.push(...quarantineFiles(root, path));
5404
5526
  else if (info.isFile())
5405
- files.push(relative9(root, path).split(sep6).join("/"));
5527
+ files.push(relative10(root, path).split(sep7).join("/"));
5406
5528
  else
5407
5529
  throw new Error("asset_quarantine_special_file_rejected");
5408
5530
  }
5409
5531
  return files;
5410
5532
  }
5411
5533
  function validateQuarantine(journal, items) {
5412
- const root = realpathSync8(journal.allowedQuarantineRoot);
5534
+ const root = realpathSync9(journal.allowedQuarantineRoot);
5413
5535
  const expected = items.flatMap((item) => item.manifest.map((entry) => entry.path)).sort();
5414
5536
  if (new Set(expected).size !== expected.length)
5415
5537
  throw new Error("provider_result_invalid: cross-item manifest collision");
@@ -5418,10 +5540,10 @@ function validateQuarantine(journal, items) {
5418
5540
  throw new Error("asset_quarantine_undeclared_file");
5419
5541
  }
5420
5542
  function stageItem(projectRoot, journal, item, destination, prior) {
5421
- const root = realpathSync8(journal.allowedQuarantineRoot);
5422
- const stageRoot = resolve11(forgeaxRoot(projectRoot), "asset3d-staging", journal.execution, item.providerAssetId);
5543
+ const root = realpathSync9(journal.allowedQuarantineRoot);
5544
+ const stageRoot = resolve12(forgeaxRoot(projectRoot), "asset3d-staging", journal.execution, item.providerAssetId);
5423
5545
  rmSync6(stageRoot, { recursive: true, force: true });
5424
- ensurePrivateDir(resolve11(stageRoot, ".."));
5546
+ ensurePrivateDir(resolve12(stageRoot, ".."));
5425
5547
  if (existsSync9(destination))
5426
5548
  cpSync2(destination, stageRoot, { recursive: true, errorOnExist: true, force: false });
5427
5549
  else
@@ -5431,17 +5553,17 @@ function stageItem(projectRoot, journal, item, destination, prior) {
5431
5553
  for (const previous of previousFiles) {
5432
5554
  if (typeof previous.relativePath !== "string" || nextPaths.has(previous.relativePath))
5433
5555
  continue;
5434
- const stale = resolve11(stageRoot, previous.relativePath);
5556
+ const stale = resolve12(stageRoot, previous.relativePath);
5435
5557
  if (!confined4(stageRoot, stale))
5436
5558
  throw new Error("asset_provenance_path_escape");
5437
5559
  rmSync6(stale, { force: true });
5438
5560
  rmSync6(`${stale}.meta.json`, { force: true });
5439
5561
  }
5440
5562
  for (const entry of item.manifest) {
5441
- const target = resolve11(stageRoot, entry.path);
5563
+ const target = resolve12(stageRoot, entry.path);
5442
5564
  if (!confined4(stageRoot, target))
5443
5565
  throw new Error("asset_manifest_escape");
5444
- mkdirSync10(resolve11(target, ".."), { recursive: true, mode: 448 });
5566
+ mkdirSync10(resolve12(target, ".."), { recursive: true, mode: 448 });
5445
5567
  atomicWrite(target, fileBytesChecked(root, entry), 384);
5446
5568
  }
5447
5569
  return stageRoot;
@@ -5457,7 +5579,8 @@ function provenance(item, install, readback, refreshed) {
5457
5579
  originSetDigest: install.originSetDigest,
5458
5580
  aggregateSha256: item.sha256,
5459
5581
  bytes: item.bytes,
5460
- primaryModel: item.primaryModel,
5582
+ deliveredFormat: item.deliveredFormat,
5583
+ ...item.deliveredFormat === "pack" ? { primaryPack: item.primaryPack } : { primaryModel: item.primaryModel },
5461
5584
  engine: { version: ENGINE_VERSION, commit: ENGINE_COMMIT },
5462
5585
  refreshed,
5463
5586
  files: item.manifest.map((entry) => ({
@@ -5471,8 +5594,8 @@ function provenance(item, install, readback, refreshed) {
5471
5594
  };
5472
5595
  }
5473
5596
  function commitItem(projectRoot, initialJournal, install, item, refresh, lockTimeoutMs, carrierPluginRoot) {
5474
- const destination = resolve11(projectRoot, "assets", "3d", "ea-3d", item.providerAssetId);
5475
- const assetRoot = resolve11(projectRoot, "assets", "3d", "ea-3d");
5597
+ const destination = resolve12(projectRoot, "assets", "3d", "ea-3d", item.providerAssetId);
5598
+ const assetRoot = resolve12(projectRoot, "assets", "3d", "ea-3d");
5476
5599
  ensurePrivateDir(assetRoot);
5477
5600
  if (!confined4(assetRoot, destination))
5478
5601
  throw new Error("asset_destination_escape");
@@ -5483,29 +5606,30 @@ function commitItem(projectRoot, initialJournal, install, item, refresh, lockTim
5483
5606
  const priorDigest = typeof prior?.aggregateSha256 === "string" ? prior.aggregateSha256 : undefined;
5484
5607
  if (priorDigest === item.sha256 && liveFilesMatch(destination, item)) {
5485
5608
  const priorRows = Array.isArray(prior?.files) ? prior.files.flatMap((entry) => entry.engineRows ?? []) : [];
5486
- const baseReadback = engineReadback(projectRoot, `assets/3d/ea-3d/${item.providerAssetId}`, undefined, carrierPluginRoot);
5609
+ const baseReadback = item.deliveredFormat === "pack" ? packReadback(projectRoot, item, carrierPluginRoot) : engineReadback(projectRoot, `assets/3d/ea-3d/${item.providerAssetId}`, undefined, carrierPluginRoot);
5487
5610
  const guids = priorRows.flatMap((row) => typeof row.guid === "string" ? [row.guid] : []);
5488
5611
  const listed = Array.isArray(baseReadback.list) ? baseReadback.list : [];
5489
5612
  if (guids.length === 0 || guids.some((guid) => !listed.some((entry) => entry.guid === guid)))
5490
5613
  throw new Error("asset_reuse_readback_failed");
5491
- const readback = { ...baseReadback, inspect: guids.map((guid) => engineCommand(projectRoot, ["asset", "inspect", guid, "--json"], carrierPluginRoot).value) };
5614
+ const readback = item.deliveredFormat === "pack" ? baseReadback : { ...baseReadback, inspect: guids.map((guid) => engineCommand(projectRoot, ["asset", "inspect", guid, "--json"], carrierPluginRoot).value) };
5492
5615
  journal = updateJournal(projectRoot, journal, { state: "committed", previousDigest: priorDigest, newDigest: item.sha256 });
5493
- return { providerAssetId: item.providerAssetId, digest: item.sha256, bytes: item.bytes, reused: true, refreshed: false, sourcePath: relative9(projectRoot, destination), provenancePath: relative9(projectRoot, resolve11(destination, ".forgeax-asset.json")), engine: { version: ENGINE_VERSION, commit: ENGINE_COMMIT }, catalog: readback, rows: priorRows };
5616
+ return { providerAssetId: item.providerAssetId, digest: item.sha256, bytes: item.bytes, reused: true, refreshed: false, sourcePath: relative10(projectRoot, destination), provenancePath: relative10(projectRoot, resolve12(destination, ".forgeax-asset.json")), engine: { version: ENGINE_VERSION, commit: ENGINE_COMMIT }, catalog: readback, rows: priorRows };
5494
5617
  }
5495
5618
  if (priorDigest && priorDigest !== item.sha256 && !refresh)
5496
5619
  throw new Error("asset_changed");
5497
5620
  const stage = stageItem(projectRoot, journal, item, destination, prior);
5498
- const backup = resolve11(forgeaxRoot(projectRoot), "asset3d-backups", journal.execution, item.providerAssetId);
5499
- ensurePrivateDir(resolve11(backup, ".."));
5621
+ const backup = resolve12(forgeaxRoot(projectRoot), "asset3d-backups", journal.execution, item.providerAssetId);
5622
+ ensurePrivateDir(resolve12(backup, ".."));
5500
5623
  journal = updateJournal(projectRoot, journal, { state: "committing", destination, backupPath: existsSync9(destination) ? backup : undefined, previousDigest: priorDigest, newDigest: item.sha256 });
5501
5624
  if (existsSync9(destination))
5502
5625
  renameSync7(destination, backup);
5503
5626
  renameSync7(stage, destination);
5504
5627
  try {
5505
5628
  const assetRelative = `assets/3d/ea-3d/${item.providerAssetId}`;
5506
- const add = engineCommand(projectRoot, ["asset", "add", assetRelative, "--reimport-policy", "semantic-only", "--json"], carrierPluginRoot);
5507
- const readback = engineReadback(projectRoot, assetRelative, add, carrierPluginRoot);
5508
- atomicWrite(resolve11(destination, ".forgeax-asset.json"), `${JSON.stringify(provenance(item, install, readback, priorDigest !== undefined), null, 2)}
5629
+ const readback = item.deliveredFormat === "pack" ? packReadback(projectRoot, item, carrierPluginRoot) : engineReadback(projectRoot, assetRelative, engineCommand(projectRoot, ["asset", "add", assetRelative, "--reimport-policy", "semantic-only", "--json"], carrierPluginRoot), carrierPluginRoot);
5630
+ if (item.deliveredFormat === "pack" && !liveFilesMatch(destination, item))
5631
+ throw new Error("engine_pack_source_changed");
5632
+ atomicWrite(resolve12(destination, ".forgeax-asset.json"), `${JSON.stringify(provenance(item, install, readback, priorDigest !== undefined), null, 2)}
5509
5633
  `);
5510
5634
  journal = updateJournal(projectRoot, journal, { state: "engine_verified" });
5511
5635
  if (existsSync9(backup))
@@ -5524,7 +5648,7 @@ function commitItem(projectRoot, initialJournal, install, item, refresh, lockTim
5524
5648
  }
5525
5649
  }
5526
5650
  function commitAsset3d(options) {
5527
- const projectRoot = realpathSync8(options.projectRoot);
5651
+ const projectRoot = realpathSync9(options.projectRoot);
5528
5652
  const install = readInstall(projectRoot);
5529
5653
  let journal = readJournal(projectRoot, options.execution);
5530
5654
  if (journal.state !== "begun" && journal.state !== "provider_complete" && journal.state !== "validated") {
@@ -5536,7 +5660,7 @@ function commitAsset3d(options) {
5536
5660
  let providerResult = options.providerResult;
5537
5661
  if (providerResult === undefined) {
5538
5662
  try {
5539
- providerResult = readFileSync13(providerResultPath(projectRoot, options.execution));
5663
+ providerResult = readFileSync14(providerResultPath(projectRoot, options.execution));
5540
5664
  } catch {
5541
5665
  throw new Error("asset3d_provider_result_missing: call search_asset for this execution first");
5542
5666
  }
@@ -5587,7 +5711,7 @@ function commitAsset3d(options) {
5587
5711
  };
5588
5712
  }
5589
5713
  function abortAsset3d(projectRootInput, execution) {
5590
- const projectRoot = realpathSync8(projectRootInput);
5714
+ const projectRoot = realpathSync9(projectRootInput);
5591
5715
  const journal = readJournal(projectRoot, execution);
5592
5716
  if (journal.state === "committed" || journal.state === "complete" || journal.state === "engine_verified") {
5593
5717
  throw new Error("asset3d_execution_already_committed");
@@ -5598,20 +5722,20 @@ function abortAsset3d(projectRootInput, execution) {
5598
5722
  return { execution, aborted: true };
5599
5723
  }
5600
5724
  function doctorAsset3d(projectRootInput, options = {}) {
5601
- const projectRoot = realpathSync8(projectRootInput);
5725
+ const projectRoot = realpathSync9(projectRootInput);
5602
5726
  const install = readInstall(projectRoot);
5603
5727
  const release = resolveEngineRelease(projectRoot, options.carrierPluginRoot === undefined ? {} : { pluginRoot: options.carrierPluginRoot });
5604
5728
  const recovered = [];
5605
- const root = resolve11(forgeaxRoot(projectRoot), "asset3d-transactions");
5729
+ const root = resolve12(forgeaxRoot(projectRoot), "asset3d-transactions");
5606
5730
  if (existsSync9(root))
5607
5731
  for (const file of readdirSync7(root).filter((name) => name.endsWith(".json"))) {
5608
5732
  try {
5609
- const path = resolve11(root, file);
5610
- const journal = JSON.parse(readFileSync13(path, "utf8"));
5733
+ const path = resolve12(root, file);
5734
+ const journal = JSON.parse(readFileSync14(path, "utf8"));
5611
5735
  if (journal.schemaVersion !== TRANSACTION_SCHEMA || journal.state !== "committing" || !journal.destination)
5612
5736
  continue;
5613
- const assetsRoot = resolve11(projectRoot, "assets", "3d", "ea-3d");
5614
- const backupsRoot = resolve11(forgeaxRoot(projectRoot), "asset3d-backups");
5737
+ const assetsRoot = resolve12(projectRoot, "assets", "3d", "ea-3d");
5738
+ const backupsRoot = resolve12(forgeaxRoot(projectRoot), "asset3d-backups");
5615
5739
  if (!confined4(assetsRoot, journal.destination) || journal.backupPath && !confined4(backupsRoot, journal.backupPath))
5616
5740
  continue;
5617
5741
  rmSync6(journal.destination, { recursive: true, force: true });
@@ -5621,13 +5745,13 @@ function doctorAsset3d(projectRootInput, options = {}) {
5621
5745
  recovered.push(journal.execution);
5622
5746
  } catch {}
5623
5747
  }
5624
- return { installed: true, recovered, engine: { version: release.version, commit: release.commit }, provider: { commit: install.providerCommit, cache: install.providerCache, live: existsSync9(resolve11(install.providerCache, "venv", "bin", "python")) } };
5748
+ return { installed: true, recovered, engine: { version: release.version, commit: release.commit }, provider: { commit: install.providerCommit, cache: install.providerCache, live: existsSync9(resolve12(install.providerCache, "venv", "bin", "python")) } };
5625
5749
  }
5626
5750
 
5627
5751
  // src/asset3d/aw-access.ts
5628
5752
  import { spawnSync as spawnSync5 } from "node:child_process";
5629
5753
  import { existsSync as existsSync10 } from "node:fs";
5630
- import { resolve as resolve12 } from "node:path";
5754
+ import { resolve as resolve13 } from "node:path";
5631
5755
  var AW_SERVICE_PATH = "/trpc.oasismetric.omcontentserver.http";
5632
5756
  var ACCESS_CHECK_SCHEMA = "forgeax.asset3d-access-check/1.0.0";
5633
5757
  var DEFAULT_AW_PUBLIC_SERVICE_ROOT = "http://lb-pl74wsqg-5wi8ujmy1fq2746r.clb.usw-tencentclb.com:8008/trpc.oasismetric.omcontentserver.http";
@@ -5660,7 +5784,7 @@ function resolveAssetLibrarySelection(options = {}) {
5660
5784
  return { library, serviceRoot: normalizeAssetLibraryServiceRoot(configured) };
5661
5785
  }
5662
5786
  function checkAssetLibraryProviderAccess(options) {
5663
- const command = resolve12(options.providerCache, "bin", "asset3d-search");
5787
+ const command = resolve13(options.providerCache, "bin", "asset3d-search");
5664
5788
  if (!existsSync10(command))
5665
5789
  throw new Error("asset3d_provider_not_prepared");
5666
5790
  const result = spawnSync5(command, ["--check-aw-access"], {
@@ -5672,7 +5796,7 @@ function checkAssetLibraryProviderAccess(options) {
5672
5796
  ASSET3D_CATALOG_BASE_URL: "",
5673
5797
  AW_API_BASE_URL: options.serviceRoot,
5674
5798
  AW_API_DEPOT_NAME: options.depotName,
5675
- AW_API_CREDENTIAL_FILE: resolve12(options.credentialFile),
5799
+ AW_API_CREDENTIAL_FILE: resolve13(options.credentialFile),
5676
5800
  AW_API_SANDBOX_KEY: ""
5677
5801
  }
5678
5802
  });
@@ -5698,9 +5822,9 @@ function checkAssetLibraryProviderAccess(options) {
5698
5822
  }
5699
5823
 
5700
5824
  // src/asset3d/credentials.ts
5701
- import { chmodSync as chmodSync5, existsSync as existsSync11, lstatSync as lstatSync9, readFileSync as readFileSync14, statSync as statSync7, unlinkSync as unlinkSync4 } from "node:fs";
5825
+ import { chmodSync as chmodSync5, existsSync as existsSync11, lstatSync as lstatSync10, readFileSync as readFileSync15, statSync as statSync7, unlinkSync as unlinkSync4 } from "node:fs";
5702
5826
  import { homedir as homedir4 } from "node:os";
5703
- import { dirname as dirname11, isAbsolute as isAbsolute5, resolve as resolve13 } from "node:path";
5827
+ import { dirname as dirname11, isAbsolute as isAbsolute6, resolve as resolve14 } from "node:path";
5704
5828
  var AW_CREDENTIAL_SCHEMA = "forgeax.asset3d-credential/1.0.0";
5705
5829
  var AW_KEY_ENV = "FORGEAX_ASSET3D_AW_SANDBOX_KEY";
5706
5830
  var MAX_CREDENTIAL_BYTES = 4096;
@@ -5715,22 +5839,22 @@ function validateKey(value) {
5715
5839
  }
5716
5840
  function defaultAwCredentialFile() {
5717
5841
  const configured = process.env.FORGEAX_ASSET3D_CREDENTIAL_FILE;
5718
- return resolve13(configured || resolve13(homedir4(), ".forgeax", "credentials", "asset3d-aw.json"));
5842
+ return resolve14(configured || resolve14(homedir4(), ".forgeax", "credentials", "asset3d-aw.json"));
5719
5843
  }
5720
5844
  function readAwCredential(pathInput) {
5721
- const path = resolve13(pathInput);
5722
- if (!isAbsolute5(pathInput))
5845
+ const path = resolve14(pathInput);
5846
+ if (!isAbsolute6(pathInput))
5723
5847
  throw new Error("asset3d_credential_path_invalid: absolute path required");
5724
5848
  if (!existsSync11(path))
5725
5849
  return;
5726
5850
  try {
5727
- const metadata = lstatSync9(path);
5851
+ const metadata = lstatSync10(path);
5728
5852
  const wrongOwner = typeof process.getuid === "function" && metadata.uid !== process.getuid();
5729
5853
  if (!metadata.isFile() || metadata.isSymbolicLink() || wrongOwner)
5730
5854
  throw new Error;
5731
5855
  if ((metadata.mode & 63) !== 0 || metadata.size < 1 || metadata.size > MAX_CREDENTIAL_BYTES)
5732
5856
  throw new Error;
5733
- const parsed = JSON.parse(readFileSync14(path, "utf8"));
5857
+ const parsed = JSON.parse(readFileSync15(path, "utf8"));
5734
5858
  if (Object.keys(parsed).sort().join(",") !== "provider,sandboxKey,schemaVersion")
5735
5859
  throw new Error;
5736
5860
  if (parsed.schemaVersion !== AW_CREDENTIAL_SCHEMA || parsed.provider !== "aw" || typeof parsed.sandboxKey !== "string")
@@ -5792,13 +5916,13 @@ async function acquireAwKey(path) {
5792
5916
  return { key: await promptAwKey(), source: "prompt" };
5793
5917
  }
5794
5918
  function writeAwCredential(pathInput, keyInput) {
5795
- if (!isAbsolute5(pathInput))
5919
+ if (!isAbsolute6(pathInput))
5796
5920
  throw new Error("asset3d_credential_path_invalid: absolute path required");
5797
- const path = resolve13(pathInput);
5921
+ const path = resolve14(pathInput);
5798
5922
  const key = validateKey(keyInput);
5799
5923
  const parent = dirname11(path);
5800
5924
  ensurePrivateDir(parent);
5801
- const parentMetadata = lstatSync9(parent);
5925
+ const parentMetadata = lstatSync10(parent);
5802
5926
  const wrongParentOwner = typeof process.getuid === "function" && parentMetadata.uid !== process.getuid();
5803
5927
  if (!parentMetadata.isDirectory() || parentMetadata.isSymbolicLink() || wrongParentOwner) {
5804
5928
  throw new Error("asset3d_credential_path_invalid: parent must be an owned regular directory");
@@ -5807,7 +5931,7 @@ function writeAwCredential(pathInput, keyInput) {
5807
5931
  const existed = existsSync11(path);
5808
5932
  if (existed)
5809
5933
  readAwCredential(path);
5810
- const previous = existed ? readFileSync14(path) : undefined;
5934
+ const previous = existed ? readFileSync15(path) : undefined;
5811
5935
  const previousMode = existed ? statSync7(path).mode & 511 : undefined;
5812
5936
  const bytes = `${JSON.stringify({ schemaVersion: AW_CREDENTIAL_SCHEMA, provider: "aw", sandboxKey: key }, null, 2)}
5813
5937
  `;
@@ -5872,7 +5996,7 @@ async function runDormantAsset3dMcp() {
5872
5996
  result: {
5873
5997
  protocolVersion,
5874
5998
  capabilities: { tools: {} },
5875
- serverInfo: { name: "asset3d-search", version: "0.3.4" }
5999
+ serverInfo: { name: "asset3d-search", version: "0.3.5" }
5876
6000
  }
5877
6001
  };
5878
6002
  } else if (request2.method === "tools/list") {
@@ -5914,7 +6038,7 @@ function rewriteSearchTool(tool) {
5914
6038
  const required = Array.isArray(inputSchema.required) ? inputSchema.required.filter((field) => typeof field === "string" && field !== "output_dir") : [];
5915
6039
  return {
5916
6040
  ...tool,
5917
- description: `${typeof tool.description === "string" ? `${tool.description} ` : ""}Run forgeax-game asset3d begin first and pass its execution ID. The bridge injects the transaction-owned output directory.`,
6041
+ description: `${typeof tool.description === "string" ? `${tool.description} ` : ""}Run forgeax-game asset3d begin first and pass its execution ID. The bridge injects the transaction-owned output directory. For native Pack preservation select output_format=asset when advertised; glb explicitly requests conversion.`,
5918
6042
  inputSchema: {
5919
6043
  ...inputSchema,
5920
6044
  properties: {
@@ -5961,10 +6085,21 @@ function transformAsset3dSearchCall(projectRoot, request2) {
5961
6085
  if (typeof args.execution !== "string") {
5962
6086
  throw new Error("asset3d_execution_required: run `forgeax-game asset3d begin` first");
5963
6087
  }
5964
- if (!Array.isArray(args.queries) || args.queries.some((query) => typeof query !== "string")) {
6088
+ if (!Array.isArray(args.queries)) {
5965
6089
  throw new Error("asset3d_queries_invalid");
5966
6090
  }
5967
- const outputDir = asset3dSearchOutputDir(projectRoot, args.execution, args.queries);
6091
+ const queries = args.queries.map((query) => {
6092
+ if (typeof query === "string")
6093
+ return query;
6094
+ if (query && typeof query === "object" && !Array.isArray(query)) {
6095
+ const selected = query;
6096
+ if (Object.keys(selected).every((key) => key === "content" || key === "assetId") && typeof selected.content === "string" && typeof selected.assetId === "string" && /^[A-Za-z0-9._-]{1,128}$/.test(selected.assetId) && ![".", ".."].includes(selected.assetId)) {
6097
+ return selected.content;
6098
+ }
6099
+ }
6100
+ throw new Error("asset3d_queries_invalid");
6101
+ });
6102
+ const outputDir = asset3dSearchOutputDir(projectRoot, args.execution, queries);
5968
6103
  const { execution: _execution, output_dir: _callerOutput, ...providerArguments } = args;
5969
6104
  return {
5970
6105
  ...request2,
@@ -6080,12 +6215,130 @@ async function runAsset3dMcpProxy(projectRoot, launch) {
6080
6215
  };
6081
6216
  process.once("SIGINT", stopChild);
6082
6217
  process.once("SIGTERM", stopChild);
6083
- return await new Promise((resolve14, reject) => {
6218
+ return await new Promise((resolve15, reject) => {
6084
6219
  child.once("error", reject);
6085
- child.once("close", (code, signal) => resolve14(code ?? (signal ? 1 : 0)));
6220
+ child.once("close", (code, signal) => resolve15(code ?? (signal ? 1 : 0)));
6086
6221
  });
6087
6222
  }
6088
6223
 
6224
+ // src/asset3d/host-config.ts
6225
+ function asset3dHostMode(client, root) {
6226
+ return inspectConfig(client, root, launchSpec("local")).state === "current" ? "local" : "npx";
6227
+ }
6228
+ function inspectAsset3dHost(client, root, mode = asset3dHostMode(client, root)) {
6229
+ const launch = asset3dLaunchSpec(mode);
6230
+ const status = inspectConfig(client, root, launch, ASSET3D_SERVER_KEY);
6231
+ if (status.state !== "different")
6232
+ return status;
6233
+ const published = asset3dLaunchSpec("npx");
6234
+ if (mode === "local" && inspectConfig(client, root, published, ASSET3D_SERVER_KEY).state === "current") {
6235
+ return { ...status, state: "outdated" };
6236
+ }
6237
+ const previous = configuredGameVersion(client, root, ASSET3D_SERVER_KEY);
6238
+ const current = published.args.find((arg) => arg.startsWith("@forgeax/game@"))?.slice("@forgeax/game@".length);
6239
+ if (!previous || !current || !/^\d+\.\d+\.\d+$/.test(previous) || !/^\d+\.\d+\.\d+$/.test(current))
6240
+ return status;
6241
+ const oldParts = previous.split(".").map(Number);
6242
+ const newParts = current.split(".").map(Number);
6243
+ const differing = oldParts.findIndex((part, index) => part !== newParts[index]);
6244
+ if (differing < 0 || oldParts[differing] >= newParts[differing])
6245
+ return status;
6246
+ const oldLaunch = { ...published, args: published.args.map((arg) => arg === `@forgeax/game@${current}` ? `@forgeax/game@${previous}` : arg) };
6247
+ if (inspectConfig(client, root, oldLaunch, ASSET3D_SERVER_KEY).state !== "current")
6248
+ return status;
6249
+ return { ...status, state: "outdated" };
6250
+ }
6251
+ function refreshAsset3dHost(client, root) {
6252
+ const status = inspectAsset3dHost(client, root);
6253
+ if (status.state === "outdated") {
6254
+ const result = applyConfig(client, root, asset3dLaunchSpec(asset3dHostMode(client, root)), ASSET3D_SERVER_KEY);
6255
+ process.stdout.write(`UPDATED ${client.label} Asset3D bridge: ${result.path}
6256
+ `);
6257
+ } else if (status.state === "different" || status.state === "invalid") {
6258
+ process.stderr.write(`WARN ${client.label}: existing ${ASSET3D_SERVER_KEY} configuration was preserved; it is not a recognized older package launcher.
6259
+ `);
6260
+ }
6261
+ }
6262
+
6263
+ // src/devkit/engine-mounts.ts
6264
+ import { lstatSync as lstatSync11, readFileSync as readFileSync16, readdirSync as readdirSync8, readlinkSync as readlinkSync2, rmdirSync, unlinkSync as unlinkSync5, writeFileSync as writeFileSync11 } from "node:fs";
6265
+ import { dirname as dirname12, join as join11, resolve as resolve15 } from "node:path";
6266
+ var HOSTS = {
6267
+ ".agents/skills": ["codex"],
6268
+ ".claude/skills": ["claude"],
6269
+ ".cursor/skills": ["cursor"],
6270
+ ".codebuddy/skills": ["codebuddy", "workbuddy"],
6271
+ ".workbuddy/skills": ["workbuddy"]
6272
+ };
6273
+ function pruneUnselectedEngineMounts(root, clients) {
6274
+ const manifestPath = join11(root, ".forgeax", "skill-install-manifest.json");
6275
+ const regular = (path) => {
6276
+ try {
6277
+ const stat = lstatSync11(path);
6278
+ return stat.isFile() && !stat.isSymbolicLink();
6279
+ } catch {
6280
+ return false;
6281
+ }
6282
+ };
6283
+ const directory = (path) => {
6284
+ try {
6285
+ const stat = lstatSync11(path);
6286
+ return stat.isDirectory() && !stat.isSymbolicLink();
6287
+ } catch {
6288
+ return false;
6289
+ }
6290
+ };
6291
+ if (!directory(join11(root, ".forgeax")) || !regular(manifestPath))
6292
+ return [];
6293
+ let manifest;
6294
+ try {
6295
+ manifest = JSON.parse(readFileSync16(manifestPath, "utf8"));
6296
+ } catch {
6297
+ return [];
6298
+ }
6299
+ if (!manifest || manifest.schemaVersion !== "1.0.0" || manifest.sourceRoot !== "skills" || !Array.isArray(manifest.mounts))
6300
+ return [];
6301
+ const removed = [];
6302
+ for (const mount of manifest.mounts) {
6303
+ if (!mount || typeof mount.root !== "string" || !Object.hasOwn(HOSTS, mount.root))
6304
+ continue;
6305
+ const hosts = HOSTS[mount.root];
6306
+ if (!hosts || hosts.some((host) => clients.includes(host)) || !Array.isArray(mount.skills))
6307
+ continue;
6308
+ if (!mount.skills.length || !mount.skills.every((id) => /^forgeax-engine-[a-z0-9-]+$/.test(id)) || new Set(mount.skills).size !== mount.skills.length)
6309
+ continue;
6310
+ const path = join11(root, mount.root);
6311
+ if (!directory(dirname12(path)) || !directory(path))
6312
+ continue;
6313
+ const expected = [".gitignore", ...mount.skills].sort();
6314
+ if (JSON.stringify(readdirSync8(path).sort()) !== JSON.stringify(expected))
6315
+ continue;
6316
+ const ignore = join11(path, ".gitignore");
6317
+ const expectedIgnore = ["# BEGIN FORGEAX MANAGED SKILLS", ...mount.skills.map((id) => `/${id}`), "# END FORGEAX MANAGED SKILLS", ""].join(`
6318
+ `);
6319
+ if (!regular(ignore) || readFileSync16(ignore, "utf8") !== expectedIgnore)
6320
+ continue;
6321
+ if (!mount.skills.every((id) => {
6322
+ const link = join11(path, id);
6323
+ return lstatSync11(link).isSymbolicLink() && resolve15(path, readlinkSync2(link)) === resolve15(root, "skills", id);
6324
+ }))
6325
+ continue;
6326
+ for (const id of mount.skills)
6327
+ unlinkSync5(join11(path, id));
6328
+ unlinkSync5(ignore);
6329
+ rmdirSync(path);
6330
+ if (readdirSync8(dirname12(path)).length === 0)
6331
+ rmdirSync(dirname12(path));
6332
+ removed.push(mount.root);
6333
+ }
6334
+ if (removed.length) {
6335
+ manifest.mounts = manifest.mounts.filter((mount) => !mount || !removed.includes(mount.root));
6336
+ writeFileSync11(manifestPath, `${JSON.stringify(manifest, null, 2)}
6337
+ `);
6338
+ }
6339
+ return removed;
6340
+ }
6341
+
6089
6342
  // src/cli/dispatch.ts
6090
6343
  var HELP = `ForgeaX game development plugin
6091
6344
 
@@ -6152,23 +6405,23 @@ function requireProject() {
6152
6405
  return project.root;
6153
6406
  }
6154
6407
  function updateAgentsFile(root) {
6155
- const path = join11(root, "AGENTS.md");
6156
- const existing = existsSync12(path) ? readFileSync15(path, "utf8") : undefined;
6408
+ const path = join12(root, "AGENTS.md");
6409
+ const existing = existsSync12(path) ? readFileSync17(path, "utf8") : undefined;
6157
6410
  const content = upsertBlock(existing, ROUTING_TEXT);
6158
6411
  if (content === existing)
6159
6412
  return { path, changed: false };
6160
- writeFileSync11(path, content);
6413
+ writeFileSync12(path, content);
6161
6414
  return { path, changed: true };
6162
6415
  }
6163
6416
  function removeAgentsBlock(root) {
6164
- const path = join11(root, "AGENTS.md");
6417
+ const path = join12(root, "AGENTS.md");
6165
6418
  if (!existsSync12(path))
6166
6419
  return { path, changed: false };
6167
- const existing = readFileSync15(path, "utf8");
6420
+ const existing = readFileSync17(path, "utf8");
6168
6421
  const content = removeBlock(existing);
6169
6422
  if (content === existing)
6170
6423
  return { path, changed: false };
6171
- writeFileSync11(path, content);
6424
+ writeFileSync12(path, content);
6172
6425
  return { path, changed: true };
6173
6426
  }
6174
6427
  async function installCommand(args) {
@@ -6190,6 +6443,7 @@ async function installCommand(args) {
6190
6443
  }
6191
6444
  try {
6192
6445
  const result = applyConfig(client, project.root ?? process.cwd(), launch);
6446
+ refreshAsset3dHost(client, project.root ?? process.cwd());
6193
6447
  process.stdout.write(`${result.changed ? "UPDATED" : "CURRENT"} ${client.label}: ${result.path}${result.backup ? ` (backup: ${result.backup})` : ""}
6194
6448
  `);
6195
6449
  if (client.postInstallNote)
@@ -6237,6 +6491,10 @@ async function initCommand(args) {
6237
6491
  reportMissingClients(selection.missing);
6238
6492
  const hosts = selection.selected;
6239
6493
  const devkit = installDevKit(root, hosts);
6494
+ const removedMounts = pruneUnselectedEngineMounts(root, hosts);
6495
+ if (removedMounts.length)
6496
+ process.stdout.write(`Removed unused Engine-generated skill mounts: ${removedMounts.join(", ")}.
6497
+ `);
6240
6498
  process.stdout.write(`Bound Engine game ${slug} at ${selectedGame2}.
6241
6499
  `);
6242
6500
  process.stdout.write(`Engine ${release.version} (${release.commit}).
@@ -6267,7 +6525,7 @@ async function useCommand(args) {
6267
6525
  throw new Error(`game ${JSON.stringify(slug)} not found. Available: ${listGames(root).join(", ") || "(none)"}`);
6268
6526
  }
6269
6527
  if (listGames(root).length > 1) {
6270
- writeFileSync11(join11(root, ".forgeax", "active-game.json"), `${JSON.stringify({ version: 1, slug }, null, 2)}
6528
+ writeFileSync12(join12(root, ".forgeax", "active-game.json"), `${JSON.stringify({ version: 1, slug }, null, 2)}
6271
6529
  `, "utf8");
6272
6530
  }
6273
6531
  process.stdout.write(`Active game: ${slug}
@@ -6369,7 +6627,7 @@ async function uninstallCommand(args) {
6369
6627
  const agents = removeAgentsBlock(root);
6370
6628
  process.stdout.write(`${agents.changed ? "REMOVED" : "ABSENT "} routing block: ${agents.path}
6371
6629
  `);
6372
- process.stdout.write(`KEPT your games and project metadata: ${join11(root, ".forgeax")}
6630
+ process.stdout.write(`KEPT your games and project metadata: ${join12(root, ".forgeax")}
6373
6631
  `);
6374
6632
  } else {
6375
6633
  process.stdout.write(`INFO no ForgeaX project bound; only client configuration was touched.
@@ -6569,6 +6827,7 @@ async function updateCommand(args) {
6569
6827
  for (const client of configured) {
6570
6828
  const previousVersion = configuredGameVersion(client, root);
6571
6829
  const result = applyConfig(client, root, launch);
6830
+ refreshAsset3dHost(client, root);
6572
6831
  process.stdout.write(`${result.changed ? "UPDATED" : "CURRENT"} ${client.label}: ${result.path} (plugin ${formatVersionTransition(previousVersion)})
6573
6832
  `);
6574
6833
  }
@@ -6686,12 +6945,12 @@ async function asset3dCommand(args) {
6686
6945
  if (selection.selected.length === 0) {
6687
6946
  throw new Error("asset3d_client_missing: run `forgeax-game install --ide <client>` before enabling Asset3D");
6688
6947
  }
6689
- const bridge = asset3dLaunchSpec("npx");
6948
+ const localMode = selection.selected.some((id) => asset3dHostMode(findClient(id), projectRoot) === "local");
6690
6949
  for (const id of selection.selected) {
6691
6950
  const client = findClient(id);
6692
6951
  if (!client)
6693
6952
  throw new Error(`asset3d_client_invalid: ${id}`);
6694
- const state = inspectConfig(client, projectRoot, bridge, ASSET3D_SERVER_KEY);
6953
+ const state = inspectAsset3dHost(client, projectRoot);
6695
6954
  if (state.state === "different") {
6696
6955
  throw new Error(`asset3d_client_server_conflict: ${client.label} already has a different ${ASSET3D_SERVER_KEY} entry`);
6697
6956
  }
@@ -6709,7 +6968,7 @@ async function asset3dCommand(args) {
6709
6968
  const bundled = BUNDLED_ASSET3D_PROVIDERS[target];
6710
6969
  if (!bundled)
6711
6970
  throw new Error(`asset3d_provider_target_unreleased: ${target}`);
6712
- const providerCache = resolve14(homedir5(), ".forgeax", "providers", "asset3d-search", bundled.sha256);
6971
+ const providerCache = resolve16(homedir5(), ".forgeax", "providers", "asset3d-search", bundled.sha256);
6713
6972
  let preparedCache = providerCache;
6714
6973
  if (!existsSync12(providerCache)) {
6715
6974
  const providerBundle = packagedAsset3dProvider(bundled.relativePath);
@@ -6735,14 +6994,14 @@ async function asset3dCommand(args) {
6735
6994
  awDepotName: library.library,
6736
6995
  awCredentialFile: credentialFile,
6737
6996
  clients,
6738
- gamePluginLaunch: launchSpec("npx"),
6997
+ gamePluginLaunch: launchSpec(localMode ? "local" : "npx"),
6739
6998
  replaceOwned: true
6740
6999
  });
6741
7000
  for (const id of selection.selected) {
6742
7001
  const client = findClient(id);
6743
7002
  if (!client)
6744
7003
  throw new Error(`asset3d_client_invalid: ${id}`);
6745
- const applied = applyConfig(client, projectRoot, bridge, ASSET3D_SERVER_KEY);
7004
+ const applied = applyConfig(client, projectRoot, asset3dLaunchSpec(asset3dHostMode(client, projectRoot)), ASSET3D_SERVER_KEY);
6746
7005
  process.stdout.write(`${applied.changed ? "UPDATED" : "CURRENT"} ${client.label} Asset3D bridge: ${applied.path}
6747
7006
  `);
6748
7007
  if (applied.changed && client.postInstallNote)
@@ -6791,7 +7050,7 @@ async function asset3dCommand(args) {
6791
7050
  else if (arg === "--aw-depot-name")
6792
7051
  awDepotName = parseAssetLibraryId(value);
6793
7052
  else if (arg === "--aw-credential-file")
6794
- awCredentialFile = resolve14(value);
7053
+ awCredentialFile = resolve16(value);
6795
7054
  else
6796
7055
  clients = canonicalAsset3dClients(value);
6797
7056
  continue;
@@ -6995,7 +7254,7 @@ function parseMcpArgs(args) {
6995
7254
  } else
6996
7255
  throw new Error(`unknown MCP option: ${arg}`);
6997
7256
  }
6998
- return { transport, host, port, root: resolve15(root), requireAuth, allowedOrigins };
7257
+ return { transport, host, port, root: resolve17(root), requireAuth, allowedOrigins };
6999
7258
  }
7000
7259
  async function runMcp(args) {
7001
7260
  const options = parseMcpArgs(args);