@forgeax/game 0.3.2 → 0.3.3

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
@@ -2946,7 +2946,7 @@ var RELEASE_IDENTITY_MIME = "application/vnd.forgeax.game-release-identity+json"
2946
2946
  var RELEASE_IDENTITY = Object.freeze({
2947
2947
  schema: RELEASE_IDENTITY_SCHEMA,
2948
2948
  gamePackage: "@forgeax/game",
2949
- gameVersion: "0.3.2",
2949
+ gameVersion: "0.3.3",
2950
2950
  gameBin: "forgeax-game",
2951
2951
  engineSdkPackage: ENGINE_SDK_PACKAGE,
2952
2952
  engineSdkVersion: ENGINE_VERSION,
@@ -3349,13 +3349,24 @@ function findClient(id) {
3349
3349
  return CLIENTS.find((client) => client.id === id || client.aliases?.includes(id));
3350
3350
  }
3351
3351
  var SERVER_KEY = "forgeax";
3352
+ var ASSET3D_SERVER_KEY = "asset3d-search";
3352
3353
  function launchSpec(mode) {
3353
3354
  if (mode === "local") {
3354
3355
  return { command: process.execPath, args: [resolve9(process.argv[1] ?? ""), "mcp"] };
3355
3356
  }
3356
3357
  return {
3357
3358
  command: "npx",
3358
- args: ["-y", "-p", "@forgeax/game@0.3.2", "forgeax-game", "mcp"]
3359
+ args: ["-y", "-p", "@forgeax/game@0.3.3", "forgeax-game", "mcp"]
3360
+ };
3361
+ }
3362
+ function asset3dLaunchSpec(mode) {
3363
+ const launch = launchSpec(mode);
3364
+ if (launch.args.at(-1) !== "mcp") {
3365
+ throw new Error("asset3d_game_plugin_launch_invalid: expected MCP launch suffix");
3366
+ }
3367
+ return {
3368
+ command: launch.command,
3369
+ args: [...launch.args.slice(0, -1), "asset3d", "mcp"]
3359
3370
  };
3360
3371
  }
3361
3372
 
@@ -3494,6 +3505,16 @@ ${suffix ? `
3494
3505
  function hasTomlTable(content, header) {
3495
3506
  return [...content.matchAll(HEADER_RE)].some((match) => sameHeader(match[1], header));
3496
3507
  }
3508
+ function readTomlTable(content, header) {
3509
+ const headerLines = [...content.matchAll(HEADER_RE)];
3510
+ const ownedIndexes = headerLines.flatMap((match, index) => sameHeader(match[1], header) ? [index] : []);
3511
+ if (ownedIndexes.length !== 1)
3512
+ return;
3513
+ const ownedIndex = ownedIndexes[0];
3514
+ const owned = headerLines[ownedIndex];
3515
+ const next = headerLines[ownedIndex + 1];
3516
+ return content.slice(owned.index, next?.index ?? content.length);
3517
+ }
3497
3518
  function hasCompetingTomlDefinition(content, header) {
3498
3519
  const headerLines = [...content.matchAll(HEADER_RE)];
3499
3520
  return hasCompetingInlineOwner(content, header, headerLines);
@@ -3794,20 +3815,57 @@ function validateTomlSyntax(source) {
3794
3815
  if (brackets.length)
3795
3816
  throw new Error("unterminated TOML array or inline table");
3796
3817
  }
3797
- function mergeJsonConfig(existing, spec, entry) {
3818
+ function validTomlTableHeader(line) {
3819
+ const arrayTable = line.startsWith("[[");
3820
+ const openingWidth = arrayTable ? 2 : 1;
3821
+ let quote;
3822
+ for (let index = openingWidth;index < line.length; index++) {
3823
+ const char = line[index];
3824
+ if (quote === "basic") {
3825
+ if (char === "\\") {
3826
+ index++;
3827
+ continue;
3828
+ }
3829
+ if (char === '"')
3830
+ quote = undefined;
3831
+ continue;
3832
+ }
3833
+ if (quote === "literal") {
3834
+ if (char === "'")
3835
+ quote = undefined;
3836
+ continue;
3837
+ }
3838
+ if (char === '"') {
3839
+ quote = "basic";
3840
+ continue;
3841
+ }
3842
+ if (char === "'") {
3843
+ quote = "literal";
3844
+ continue;
3845
+ }
3846
+ const closes = arrayTable ? line.startsWith("]]", index) : char === "]";
3847
+ if (!closes)
3848
+ continue;
3849
+ const body = line.slice(openingWidth, index).trim();
3850
+ const suffix = line.slice(index + openingWidth);
3851
+ return body.length > 0 && /^[ \t]*(?:#.*)?$/.test(suffix);
3852
+ }
3853
+ return false;
3854
+ }
3855
+ function mergeJsonConfig(existing, spec, entry, serverKey = SERVER_KEY) {
3798
3856
  const path = spec.path("");
3799
3857
  const mapKey = spec.serverMapKey ?? ["mcpServers"];
3800
3858
  const wanted = JSON.stringify(entry);
3801
3859
  if (!existing || existing.trim() === "") {
3802
3860
  if (existing && JSON_WHITESPACE.test(existing)) {
3803
- let nested2 = { [SERVER_KEY]: entry };
3861
+ let nested2 = { [serverKey]: entry };
3804
3862
  for (let index = mapKey.length - 1;index >= 0; index--) {
3805
3863
  nested2 = { [mapKey[index]]: nested2 };
3806
3864
  }
3807
3865
  return { content: `${existing}${JSON.stringify(nested2)}
3808
3866
  `, changed: true };
3809
3867
  }
3810
- let nested = { [SERVER_KEY]: entry };
3868
+ let nested = { [serverKey]: entry };
3811
3869
  for (let index = mapKey.length - 1;index >= 0; index--) {
3812
3870
  nested = { [mapKey[index]]: nested };
3813
3871
  }
@@ -3822,7 +3880,7 @@ function mergeJsonConfig(existing, spec, entry) {
3822
3880
  const key = mapKey[index];
3823
3881
  const found = jsonMember(members, key, path);
3824
3882
  if (!found) {
3825
- let nested = { [SERVER_KEY]: entry };
3883
+ let nested = { [serverKey]: entry };
3826
3884
  for (let nestedIndex = mapKey.length - 1;nestedIndex >= index; nestedIndex--) {
3827
3885
  nested = { [mapKey[nestedIndex]]: nested };
3828
3886
  }
@@ -3836,29 +3894,29 @@ function mergeJsonConfig(existing, spec, entry) {
3836
3894
  members = nestedMembers;
3837
3895
  }
3838
3896
  const mapMembers = members;
3839
- const owned = jsonMember(mapMembers, SERVER_KEY, `${path}.${mapKey.join(".")}`);
3897
+ const owned = jsonMember(mapMembers, serverKey, `${path}.${mapKey.join(".")}`);
3840
3898
  if (owned) {
3841
3899
  let before;
3842
3900
  try {
3843
3901
  before = JSON.parse(source.slice(owned.value.start, owned.value.end));
3844
3902
  } catch {
3845
- throw new Error(`${path}.mcpServers.${SERVER_KEY} is not valid JSON`);
3903
+ throw new Error(`${path}.mcpServers.${serverKey} is not valid JSON`);
3846
3904
  }
3847
3905
  if (JSON.stringify(before) === wanted)
3848
3906
  return { content: existing, changed: false };
3849
3907
  return { content: replaceJsonValue(source, owned.value, wanted), changed: true };
3850
3908
  }
3851
3909
  return {
3852
- content: appendJsonMember(source, container, mapMembers, SERVER_KEY, wanted),
3910
+ content: appendJsonMember(source, container, mapMembers, serverKey, wanted),
3853
3911
  changed: true
3854
3912
  };
3855
3913
  }
3856
- function mergeTomlConfig(existing, entry) {
3914
+ function mergeTomlConfig(existing, entry, serverKey = SERVER_KEY) {
3857
3915
  if (existing !== undefined && existing.trim() !== "") {
3858
3916
  validateTomlSyntax(existing);
3859
3917
  for (const [index, line] of existing.split(/\r?\n/).entries()) {
3860
3918
  const trimmed = line.trim();
3861
- if (trimmed.startsWith("[") && !/^\[{1,2}[^\]\r\n]+\]{1,2}[ \t]*(?:#.*)?$/.test(trimmed)) {
3919
+ if (trimmed.startsWith("[") && !validTomlTableHeader(trimmed)) {
3862
3920
  throw new Error(`line ${index + 1} has an invalid TOML table header`);
3863
3921
  }
3864
3922
  }
@@ -3870,10 +3928,47 @@ function mergeTomlConfig(existing, entry) {
3870
3928
  const args = entry.args;
3871
3929
  if (Array.isArray(args))
3872
3930
  body.push(`args = ${encodeTomlStringArray(args)}`);
3873
- const content = upsertTomlTable(existing ?? "", { header: `mcp_servers.${SERVER_KEY}`, body });
3931
+ const content = upsertTomlTable(existing ?? "", { header: `mcp_servers.${serverKey}`, body });
3874
3932
  return { content, changed: content !== (existing ?? "") };
3875
3933
  }
3876
- function inspectConfig(spec, projectRoot, launch) {
3934
+ function jsonServerEntry(parsed, spec, serverKey) {
3935
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
3936
+ return;
3937
+ let cursor = parsed;
3938
+ for (const key of spec.serverMapKey ?? ["mcpServers"]) {
3939
+ if (typeof cursor !== "object" || cursor === null || Array.isArray(cursor))
3940
+ return;
3941
+ cursor = cursor[key];
3942
+ }
3943
+ if (typeof cursor !== "object" || cursor === null || Array.isArray(cursor))
3944
+ return;
3945
+ return cursor[serverKey];
3946
+ }
3947
+ function gameVersionFromEntryText(entry) {
3948
+ const pinned = entry.match(/@forgeax\/game@([0-9A-Za-z][0-9A-Za-z.+_-]*)/);
3949
+ if (pinned)
3950
+ return pinned[1];
3951
+ if (entry.includes("@forgeax/game"))
3952
+ return "unversioned";
3953
+ return "local/custom";
3954
+ }
3955
+ function configuredGameVersion(spec, projectRoot, serverKey = SERVER_KEY) {
3956
+ const path = spec.path(projectRoot);
3957
+ if (!existsSync7(path))
3958
+ return;
3959
+ try {
3960
+ const existing = readFileSync11(path, "utf8");
3961
+ if (spec.format === "toml") {
3962
+ const table = readTomlTable(existing, `mcp_servers.${serverKey}`);
3963
+ return table === undefined ? undefined : gameVersionFromEntryText(table);
3964
+ }
3965
+ const entry = jsonServerEntry(JSON.parse(existing), spec, serverKey);
3966
+ return entry === undefined ? undefined : gameVersionFromEntryText(JSON.stringify(entry));
3967
+ } catch {
3968
+ return;
3969
+ }
3970
+ }
3971
+ function inspectConfig(spec, projectRoot, launch, serverKey = SERVER_KEY) {
3877
3972
  const path = spec.path(projectRoot);
3878
3973
  if (!existsSync7(path))
3879
3974
  return { path, state: "missing" };
@@ -3881,7 +3976,7 @@ function inspectConfig(spec, projectRoot, launch) {
3881
3976
  try {
3882
3977
  existing = readFileSync11(path, "utf8");
3883
3978
  if (spec.format === "toml") {
3884
- const header = `mcp_servers.${SERVER_KEY}`;
3979
+ const header = `mcp_servers.${serverKey}`;
3885
3980
  if (!hasTomlTable(existing, header)) {
3886
3981
  if (hasCompetingTomlDefinition(existing, header)) {
3887
3982
  return {
@@ -3894,24 +3989,14 @@ function inspectConfig(spec, projectRoot, launch) {
3894
3989
  }
3895
3990
  return {
3896
3991
  path,
3897
- state: mergeTomlConfig(existing, buildEntry(spec, launch)).changed ? "different" : "current"
3992
+ state: mergeTomlConfig(existing, buildEntry(spec, launch), serverKey).changed ? "different" : "current"
3898
3993
  };
3899
3994
  }
3900
3995
  const parsed = JSON.parse(existing);
3901
3996
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
3902
3997
  return { path, state: "invalid", detail: "top level is not a JSON object" };
3903
3998
  }
3904
- let cursor = parsed;
3905
- for (const key of spec.serverMapKey ?? ["mcpServers"]) {
3906
- if (typeof cursor !== "object" || cursor === null || Array.isArray(cursor)) {
3907
- return { path, state: "not_configured" };
3908
- }
3909
- cursor = cursor[key];
3910
- }
3911
- if (typeof cursor !== "object" || cursor === null || Array.isArray(cursor)) {
3912
- return { path, state: "not_configured" };
3913
- }
3914
- const entry = cursor[SERVER_KEY];
3999
+ const entry = jsonServerEntry(parsed, spec, serverKey);
3915
4000
  if (entry === undefined)
3916
4001
  return { path, state: "not_configured" };
3917
4002
  return {
@@ -3922,11 +4007,11 @@ function inspectConfig(spec, projectRoot, launch) {
3922
4007
  return { path, state: "invalid", detail: error instanceof Error ? error.message : String(error) };
3923
4008
  }
3924
4009
  }
3925
- function applyConfig(spec, projectRoot, launch) {
4010
+ function applyConfig(spec, projectRoot, launch, serverKey = SERVER_KEY) {
3926
4011
  const path = spec.path(projectRoot);
3927
4012
  const existing = existsSync7(path) ? readFileSync11(path, "utf8") : undefined;
3928
4013
  const entry = buildEntry(spec, launch);
3929
- const merged = spec.format === "toml" ? mergeTomlConfig(existing, entry) : mergeJsonConfig(existing, spec, entry);
4014
+ const merged = spec.format === "toml" ? mergeTomlConfig(existing, entry, serverKey) : mergeJsonConfig(existing, spec, entry, serverKey);
3930
4015
  if (!merged.changed)
3931
4016
  return { path, changed: false };
3932
4017
  mkdirSync7(dirname8(path), { recursive: true });
@@ -3938,14 +4023,14 @@ function applyConfig(spec, projectRoot, launch) {
3938
4023
  writeFileSync7(path, merged.content);
3939
4024
  return { path, changed: true, ...backup ? { backup } : {} };
3940
4025
  }
3941
- function removeConfig(spec, projectRoot) {
4026
+ function removeConfig(spec, projectRoot, serverKey = SERVER_KEY) {
3942
4027
  const path = spec.path(projectRoot);
3943
4028
  if (!existsSync7(path))
3944
4029
  return { path, changed: false };
3945
4030
  const existing = readFileSync11(path, "utf8");
3946
4031
  let content;
3947
4032
  if (spec.format === "toml") {
3948
- content = removeTomlTable(existing, `mcp_servers.${SERVER_KEY}`);
4033
+ content = removeTomlTable(existing, `mcp_servers.${serverKey}`);
3949
4034
  } else {
3950
4035
  let parsed;
3951
4036
  try {
@@ -3960,9 +4045,9 @@ function removeConfig(spec, projectRoot) {
3960
4045
  return { path, changed: false };
3961
4046
  cursor = next;
3962
4047
  }
3963
- if (!(SERVER_KEY in cursor))
4048
+ if (!(serverKey in cursor))
3964
4049
  return { path, changed: false };
3965
- delete cursor[SERVER_KEY];
4050
+ delete cursor[serverKey];
3966
4051
  content = `${JSON.stringify(parsed, null, 2)}
3967
4052
  `;
3968
4053
  }
@@ -4184,7 +4269,7 @@ import { createHash as createHash6 } from "node:crypto";
4184
4269
  var PROVIDER_BUNDLE_SCHEMA = "forgeax.asset3d-provider-bundle/1.0.0";
4185
4270
  var PROVIDER_RESULT_SCHEMA = "forgeax.asset3d-search-result/1.0.0";
4186
4271
  var PROVIDER_RECEIPT_SCHEMA = "forgeax.asset3d-search-receipt/1.0.0";
4187
- var K0_PACKAGE_SHA256 = "0c3fc5d448b94efadb7fe03ee48ef3d8964434c2789505297a99c3a2715dd83b";
4272
+ var K0_PACKAGE_SHA256 = "4937fe00e2b919319c70c82db53ebe359b275ed662c026153ecb97f7601262b4";
4188
4273
  var RESULT_SCHEMA_SHA256 = "ab6e5e1e794d5428efef362fe32d4c54b6541e12539bae5f6ff27680c9f380ba";
4189
4274
  var RECEIPT_SCHEMA_SHA256 = "e0dc9e9fe9872f09af9fca6d0c17d22aca63c2b546c55c0b669c581f7059b9c4";
4190
4275
  var ASSET3D_PROVIDER_COMMIT = "c181c48fbffc933a7ce9a0836f7878ca5e6d77e1";
@@ -4501,9 +4586,19 @@ function filesUnder2(root, directory = root) {
4501
4586
  })();
4502
4587
  });
4503
4588
  }
4504
- function launchEntries(projectRoot, providerCache, origins, gamePluginLaunch, catalogBaseUrl, awApiBaseUrl, awCredentialFile) {
4589
+ function asset3dProxyLaunch(gamePluginLaunch) {
4590
+ if (gamePluginLaunch.args.at(-1) !== "mcp") {
4591
+ throw new Error("asset3d_game_plugin_launch_invalid: expected MCP launch suffix");
4592
+ }
4593
+ return {
4594
+ command: gamePluginLaunch.command,
4595
+ args: [...gamePluginLaunch.args.slice(0, -1), "asset3d", "mcp"]
4596
+ };
4597
+ }
4598
+ function launchEntries(projectRoot, providerCache, origins, gamePluginLaunch, catalogBaseUrl, awApiBaseUrl, awDepotName, awCredentialFile) {
4505
4599
  const quarantine = resolve10(projectRoot, ".forgeax", "asset3d-quarantine");
4506
4600
  const workspace = resolve10(quarantine, "workspace");
4601
+ const proxyLaunch = asset3dProxyLaunch(gamePluginLaunch);
4507
4602
  return {
4508
4603
  forgeax: {
4509
4604
  type: "stdio",
@@ -4514,8 +4609,8 @@ function launchEntries(projectRoot, providerCache, origins, gamePluginLaunch, ca
4514
4609
  },
4515
4610
  "asset3d-search": {
4516
4611
  type: "stdio",
4517
- command: resolve10(providerCache, "bin", "asset3d-search"),
4518
- args: [],
4612
+ command: proxyLaunch.command,
4613
+ args: proxyLaunch.args,
4519
4614
  env: {
4520
4615
  FBX2GLTF_BIN: resolve10(providerCache, "bin", "FBX2glTF"),
4521
4616
  MCP_SHARED_PATH: workspace,
@@ -4524,6 +4619,7 @@ function launchEntries(projectRoot, providerCache, origins, gamePluginLaunch, ca
4524
4619
  AW_DOWNLOAD_ORIGINS: origins.compactJson,
4525
4620
  ...catalogBaseUrl ? { ASSET3D_CATALOG_BASE_URL: catalogBaseUrl } : {},
4526
4621
  ...awApiBaseUrl ? { AW_API_BASE_URL: awApiBaseUrl } : {},
4622
+ ...awDepotName ? { AW_API_DEPOT_NAME: awDepotName } : {},
4527
4623
  ...awCredentialFile ? { AW_API_CREDENTIAL_FILE: awCredentialFile } : {}
4528
4624
  },
4529
4625
  toolCallTimeoutsMs: { default: 30000, tools: { search_asset: 195000 } }
@@ -4697,7 +4793,11 @@ async function publishAsset3d(options, provisioned) {
4697
4793
  if (Boolean(options.awApiBaseUrl) !== Boolean(options.awCredentialFile)) {
4698
4794
  throw new Error("asset3d_aw_config_invalid: AW API base URL and credential file must be configured together");
4699
4795
  }
4796
+ if (options.awDepotName && !options.awApiBaseUrl) {
4797
+ throw new Error("asset3d_aw_config_invalid: depot name requires an AW API base URL");
4798
+ }
4700
4799
  const awApiBaseUrl = options.awApiBaseUrl;
4800
+ const awDepotName = awApiBaseUrl ? options.awDepotName ?? "aw" : undefined;
4701
4801
  const awCredentialFile = options.awCredentialFile ? resolve10(options.awCredentialFile) : undefined;
4702
4802
  if (awCredentialFile && !isAbsolute3(options.awCredentialFile)) {
4703
4803
  throw new Error("asset3d_credential_path_invalid: absolute path required");
@@ -4713,9 +4813,14 @@ async function publishAsset3d(options, provisioned) {
4713
4813
  command: realpathSync7(options.executable ?? process.argv[1]),
4714
4814
  args: ["mcp"]
4715
4815
  };
4716
- const entries = launchEntries(projectRoot, provisioned.cache, origins, gamePluginLaunch, catalogBaseUrl, awApiBaseUrl, awCredentialFile);
4717
- if (options.verifyMcp !== false)
4718
- await verifyProviderMcp(entries["asset3d-search"]);
4816
+ const entries = launchEntries(projectRoot, provisioned.cache, origins, gamePluginLaunch, catalogBaseUrl, awApiBaseUrl, awDepotName, awCredentialFile);
4817
+ if (options.verifyMcp !== false) {
4818
+ await verifyProviderMcp({
4819
+ ...entries["asset3d-search"],
4820
+ command: resolve10(provisioned.cache, "bin", "asset3d-search"),
4821
+ args: []
4822
+ });
4823
+ }
4719
4824
  const configPath = resolve10(forgeax, "mcp.json");
4720
4825
  const merged = mergeConfig(configPath, entries, previous, options.replaceOwned === true);
4721
4826
  const clients = [...new Set(options.clients ?? Object.keys(SKILL_MOUNTS))];
@@ -4767,6 +4872,37 @@ function prepareAsset3dProvider(options) {
4767
4872
  async function installProvisionedAsset3d(options) {
4768
4873
  return publishAsset3d(options, useProvisionedCache(options.providerCache, options.expectedSha256));
4769
4874
  }
4875
+ function asset3dProviderLaunch(projectRootInput) {
4876
+ const projectRoot = realpathSync7(projectRootInput);
4877
+ const manifest = readInstallManifest(resolve10(projectRoot, ".forgeax", "asset3d-install.json"));
4878
+ if (!manifest)
4879
+ throw new Error("asset3d_not_installed");
4880
+ let config;
4881
+ try {
4882
+ config = JSON.parse(readFileSync12(resolve10(projectRoot, ".forgeax", "mcp.json"), "utf8"));
4883
+ } catch {
4884
+ throw new Error("project_mcp_config_invalid");
4885
+ }
4886
+ const entry = config?.mcpServers?.["asset3d-search"];
4887
+ if (!entry || typeof entry !== "object" || Array.isArray(entry) || entryDigest(entry) !== manifest.configEntryDigests["asset3d-search"]) {
4888
+ throw new Error("asset3d_provider_config_unowned");
4889
+ }
4890
+ const launch = entry;
4891
+ const expectedCommand = resolve10(manifest.providerCache, "bin", "asset3d-search");
4892
+ if (!existsSync8(expectedCommand) || !launch.env || typeof launch.env !== "object" || Array.isArray(launch.env)) {
4893
+ throw new Error("asset3d_provider_config_invalid");
4894
+ }
4895
+ const env3 = Object.fromEntries(Object.entries(launch.env).map(([name, value]) => {
4896
+ if (typeof value !== "string")
4897
+ throw new Error("asset3d_provider_config_invalid");
4898
+ return [name, value];
4899
+ }));
4900
+ return {
4901
+ command: expectedCommand,
4902
+ args: [],
4903
+ env: env3
4904
+ };
4905
+ }
4770
4906
  function uninstallAsset3d(projectRootInput) {
4771
4907
  const projectRoot = realpathSync7(projectRootInput);
4772
4908
  const forgeax = resolve10(projectRoot, ".forgeax");
@@ -5013,6 +5149,9 @@ function forgeaxRoot(projectRoot) {
5013
5149
  function journalPath(projectRoot, execution) {
5014
5150
  return resolve11(forgeaxRoot(projectRoot), "asset3d-transactions", `${execution}.json`);
5015
5151
  }
5152
+ function providerResultPath(projectRoot, execution) {
5153
+ return resolve11(forgeaxRoot(projectRoot), "asset3d-results", `${execution}.json`);
5154
+ }
5016
5155
  function readInstall(projectRoot) {
5017
5156
  let manifest;
5018
5157
  try {
@@ -5072,6 +5211,36 @@ function beginAsset3d(projectRootInput, queries, options = {}) {
5072
5211
  `);
5073
5212
  return { execution, output_dir: relativeOutput };
5074
5213
  }
5214
+ function asset3dSearchOutputDir(projectRootInput, execution, queries) {
5215
+ const projectRoot = realpathSync8(projectRootInput);
5216
+ const journal = readJournal(projectRoot, execution);
5217
+ if (journal.state !== "begun") {
5218
+ throw new Error(`asset3d_execution_state_invalid: ${journal.state}`);
5219
+ }
5220
+ if (queries.length !== journal.requestedCount || sha2562(canonicalJson(queries)) !== journal.queryDigest) {
5221
+ throw new Error("asset3d_search_query_identity_mismatch");
5222
+ }
5223
+ const outputDir = `workspace/asset3d/${execution}`;
5224
+ const expectedRoot = resolve11(forgeaxRoot(projectRoot), "asset3d-quarantine", outputDir);
5225
+ if (journal.allowedQuarantineRoot !== expectedRoot || !existsSync9(expectedRoot) || realpathSync8(expectedRoot) !== expectedRoot) {
5226
+ throw new Error("asset3d_search_output_identity_mismatch");
5227
+ }
5228
+ return outputDir;
5229
+ }
5230
+ function recordAsset3dProviderResult(projectRootInput, execution, providerResult) {
5231
+ const projectRoot = realpathSync8(projectRootInput);
5232
+ const journal = readJournal(projectRoot, execution);
5233
+ if (journal.state !== "begun") {
5234
+ throw new Error(`asset3d_execution_state_invalid: ${journal.state}`);
5235
+ }
5236
+ const bytes = Buffer.byteLength(providerResult);
5237
+ if (bytes < 1 || bytes > MAX_JSON_BYTES) {
5238
+ throw new Error("provider_result_too_large: expected 1 byte..1 MiB");
5239
+ }
5240
+ ensurePrivateDir(resolve11(forgeaxRoot(projectRoot), "asset3d-results"));
5241
+ atomicWrite(providerResultPath(projectRoot, execution), providerResult, 384);
5242
+ updateJournal(projectRoot, journal, { state: "provider_complete" });
5243
+ }
5075
5244
  function fileBytesChecked(root, entry) {
5076
5245
  const source = resolve11(root, entry.path);
5077
5246
  if (!confined4(root, source))
@@ -5354,14 +5523,23 @@ function commitAsset3d(options) {
5354
5523
  const projectRoot = realpathSync8(options.projectRoot);
5355
5524
  const install = readInstall(projectRoot);
5356
5525
  let journal = readJournal(projectRoot, options.execution);
5357
- if (journal.state !== "begun" && journal.state !== "validated")
5526
+ if (journal.state !== "begun" && journal.state !== "provider_complete" && journal.state !== "validated") {
5358
5527
  throw new Error(`asset3d_execution_state_invalid: ${journal.state}`);
5528
+ }
5359
5529
  if (journal.providerCommit !== install.providerCommit || journal.engineCommit !== ENGINE_COMMIT || journal.engineVersion !== ENGINE_VERSION) {
5360
5530
  throw new Error("asset3d_execution_identity_mismatch");
5361
5531
  }
5532
+ let providerResult = options.providerResult;
5533
+ if (providerResult === undefined) {
5534
+ try {
5535
+ providerResult = readFileSync13(providerResultPath(projectRoot, options.execution));
5536
+ } catch {
5537
+ throw new Error("asset3d_provider_result_missing: call search_asset for this execution first");
5538
+ }
5539
+ }
5362
5540
  let result;
5363
5541
  try {
5364
- result = parseProviderResult(options.providerResult, install.providerCommit, install.originSetDigest);
5542
+ result = parseProviderResult(providerResult, install.providerCommit, install.originSetDigest);
5365
5543
  if (result.total !== journal.requestedCount)
5366
5544
  throw new Error("provider_result_invalid: requested count mismatch");
5367
5545
  const orderedQueries = [...result.results].sort((left, right) => left.queryIndex - right.queryIndex).map((item) => item.query);
@@ -5373,6 +5551,7 @@ function commitAsset3d(options) {
5373
5551
  journal = updateJournal(projectRoot, journal, { state: "validated" });
5374
5552
  } catch (error) {
5375
5553
  rmSync6(journal.allowedQuarantineRoot, { recursive: true, force: true });
5554
+ rmSync6(providerResultPath(projectRoot, options.execution), { force: true });
5376
5555
  updateJournal(projectRoot, journal, {
5377
5556
  state: "failed",
5378
5557
  error: error instanceof Error ? error.message.slice(0, 256) : "provider validation failed"
@@ -5392,6 +5571,7 @@ function commitAsset3d(options) {
5392
5571
  }
5393
5572
  }
5394
5573
  rmSync6(journal.allowedQuarantineRoot, { recursive: true, force: true });
5574
+ rmSync6(providerResultPath(projectRoot, options.execution), { force: true });
5395
5575
  const failed = terminal.filter((entry) => entry.status === "error").length;
5396
5576
  updateJournal(projectRoot, journal, { state: failed === 0 ? "complete" : "failed" });
5397
5577
  return {
@@ -5409,6 +5589,7 @@ function abortAsset3d(projectRootInput, execution) {
5409
5589
  throw new Error("asset3d_execution_already_committed");
5410
5590
  }
5411
5591
  rmSync6(journal.allowedQuarantineRoot, { recursive: true, force: true });
5592
+ rmSync6(providerResultPath(projectRoot, execution), { force: true });
5412
5593
  updateJournal(projectRoot, journal, { state: "aborted" });
5413
5594
  return { execution, aborted: true };
5414
5595
  }
@@ -5445,13 +5626,14 @@ import { existsSync as existsSync10 } from "node:fs";
5445
5626
  import { resolve as resolve12 } from "node:path";
5446
5627
  var AW_SERVICE_PATH = "/trpc.oasismetric.omcontentserver.http";
5447
5628
  var ACCESS_CHECK_SCHEMA = "forgeax.asset3d-access-check/1.0.0";
5448
- var DEFAULT_AW_PRODUCTION_SERVICE_ROOT = "http://lb-pl74wsqg-5wi8ujmy1fq2746r.clb.usw-tencentclb.com:8008/trpc.oasismetric.omcontentserver.http";
5449
- function normalizeAwServiceRoot(input) {
5629
+ var DEFAULT_AW_PUBLIC_SERVICE_ROOT = "http://lb-pl74wsqg-5wi8ujmy1fq2746r.clb.usw-tencentclb.com:8008/trpc.oasismetric.omcontentserver.http";
5630
+ var DEFAULT_EA_LOCAL_SERVICE_ROOT = "http://test-ultrongw.woa.com/trpc.oasismetric.omcontentserver.http";
5631
+ function normalizeAssetLibraryServiceRoot(input) {
5450
5632
  let parsed;
5451
5633
  try {
5452
5634
  parsed = new URL(input);
5453
5635
  } catch {
5454
- throw new Error("asset3d_base_url_invalid: expected an HTTP(S) AW gateway or service URL");
5636
+ throw new Error("asset3d_base_url_invalid: expected an HTTP(S) EA gateway or service URL");
5455
5637
  }
5456
5638
  if (!["http:", "https:"].includes(parsed.protocol) || parsed.username || parsed.password || parsed.search || parsed.hash) {
5457
5639
  throw new Error("asset3d_base_url_invalid: credentials, query, and fragment are forbidden");
@@ -5464,11 +5646,16 @@ function normalizeAwServiceRoot(input) {
5464
5646
  parsed.pathname = path;
5465
5647
  return parsed.toString().replace(/\/$/, "");
5466
5648
  }
5467
- function resolveAwServiceRoot(explicit) {
5468
- const configured = explicit || process.env.FORGEAX_ASSET_LIBRARY_BASE_URL || DEFAULT_AW_PRODUCTION_SERVICE_ROOT;
5469
- return normalizeAwServiceRoot(configured);
5649
+ function resolveAssetLibrarySelection(options = {}) {
5650
+ const library = options.library || process.env.FORGEAX_ASSET_LIBRARY || "aw";
5651
+ if (library !== "aw" && library !== "ea") {
5652
+ throw new Error("asset3d_library_invalid: expected aw or ea");
5653
+ }
5654
+ const defaultRoot = library === "ea" ? DEFAULT_EA_LOCAL_SERVICE_ROOT : DEFAULT_AW_PUBLIC_SERVICE_ROOT;
5655
+ const configured = options.baseUrl || process.env.FORGEAX_ASSET_LIBRARY_BASE_URL || defaultRoot;
5656
+ return { library, serviceRoot: normalizeAssetLibraryServiceRoot(configured) };
5470
5657
  }
5471
- function checkAwProviderAccess(options) {
5658
+ function checkAssetLibraryProviderAccess(options) {
5472
5659
  const command = resolve12(options.providerCache, "bin", "asset3d-search");
5473
5660
  if (!existsSync10(command))
5474
5661
  throw new Error("asset3d_provider_not_prepared");
@@ -5480,6 +5667,7 @@ function checkAwProviderAccess(options) {
5480
5667
  ...process.env,
5481
5668
  ASSET3D_CATALOG_BASE_URL: "",
5482
5669
  AW_API_BASE_URL: options.serviceRoot,
5670
+ AW_API_DEPOT_NAME: options.depotName,
5483
5671
  AW_API_CREDENTIAL_FILE: resolve12(options.credentialFile),
5484
5672
  AW_API_SANDBOX_KEY: ""
5485
5673
  }
@@ -5496,7 +5684,7 @@ function checkAwProviderAccess(options) {
5496
5684
  const envelope = payload;
5497
5685
  if (result.status !== 0 || envelope.ok !== true) {
5498
5686
  const code = envelope.error?.code === "asset3d_access_validation_inconclusive" ? "asset3d_access_validation_inconclusive" : "asset3d_api_key_invalid_or_unavailable";
5499
- throw new Error(`${code}: verify the AW service address, internal network, and Sandbox Key`);
5687
+ throw new Error(`${code}: verify the selected asset-library service, network access, and Sandbox Key`);
5500
5688
  }
5501
5689
  if (envelope.schemaVersion !== ACCESS_CHECK_SCHEMA || envelope.value?.authentication !== "sandbox-key" || !Array.isArray(envelope.value.downloadOrigins)) {
5502
5690
  throw new Error("asset3d_access_validation_failed: Provider returned an invalid response");
@@ -5644,6 +5832,256 @@ function writeAwCredential(pathInput, keyInput) {
5644
5832
  };
5645
5833
  }
5646
5834
 
5835
+ // src/asset3d/mcp-proxy.ts
5836
+ import { spawn as spawn4 } from "node:child_process";
5837
+ async function runDormantAsset3dMcp() {
5838
+ let buffer = "";
5839
+ process.stdin.setEncoding("utf8");
5840
+ for await (const chunk of process.stdin) {
5841
+ buffer += chunk;
5842
+ for (;; ) {
5843
+ const newline = buffer.indexOf(`
5844
+ `);
5845
+ if (newline < 0)
5846
+ break;
5847
+ const line = buffer.slice(0, newline);
5848
+ buffer = buffer.slice(newline + 1);
5849
+ if (!line.trim())
5850
+ continue;
5851
+ let request2;
5852
+ try {
5853
+ request2 = JSON.parse(line);
5854
+ } catch {
5855
+ process.stdout.write(`${JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } })}
5856
+ `);
5857
+ continue;
5858
+ }
5859
+ if (request2.id === undefined)
5860
+ continue;
5861
+ let response;
5862
+ if (request2.method === "initialize") {
5863
+ const params = request2.params;
5864
+ const protocolVersion = params && typeof params === "object" && !Array.isArray(params) && typeof params.protocolVersion === "string" ? params.protocolVersion : "2024-11-05";
5865
+ response = {
5866
+ jsonrpc: "2.0",
5867
+ id: request2.id,
5868
+ result: {
5869
+ protocolVersion,
5870
+ capabilities: { tools: {} },
5871
+ serverInfo: { name: "asset3d-search", version: "0.3.3" }
5872
+ }
5873
+ };
5874
+ } else if (request2.method === "tools/list") {
5875
+ response = { jsonrpc: "2.0", id: request2.id, result: { tools: [] } };
5876
+ } else {
5877
+ response = { jsonrpc: "2.0", id: request2.id, error: { code: -32601, message: "Method not found" } };
5878
+ }
5879
+ process.stdout.write(`${JSON.stringify(response)}
5880
+ `);
5881
+ }
5882
+ }
5883
+ return 0;
5884
+ }
5885
+ function requestKey(id) {
5886
+ return typeof id === "string" || typeof id === "number" ? JSON.stringify(id) : undefined;
5887
+ }
5888
+ function searchToolError(id, error) {
5889
+ const message = error instanceof Error ? error.message : String(error);
5890
+ return {
5891
+ jsonrpc: "2.0",
5892
+ id,
5893
+ result: {
5894
+ content: [{ type: "text", text: message.slice(0, 512) }],
5895
+ isError: true
5896
+ }
5897
+ };
5898
+ }
5899
+ function rewriteSearchTool(tool) {
5900
+ if (tool.name !== "search_asset")
5901
+ return tool;
5902
+ const schema = tool.inputSchema;
5903
+ if (!schema || typeof schema !== "object" || Array.isArray(schema))
5904
+ return tool;
5905
+ const inputSchema = schema;
5906
+ const properties = inputSchema.properties;
5907
+ if (!properties || typeof properties !== "object" || Array.isArray(properties))
5908
+ return tool;
5909
+ const { output_dir: _outputDir, ...safeProperties } = properties;
5910
+ const required = Array.isArray(inputSchema.required) ? inputSchema.required.filter((field) => typeof field === "string" && field !== "output_dir") : [];
5911
+ return {
5912
+ ...tool,
5913
+ 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.`,
5914
+ inputSchema: {
5915
+ ...inputSchema,
5916
+ properties: {
5917
+ ...safeProperties,
5918
+ execution: {
5919
+ type: "string",
5920
+ pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
5921
+ description: "Execution ID returned by forgeax-game asset3d begin for these exact queries."
5922
+ }
5923
+ },
5924
+ required: [...new Set([...required, "execution"])]
5925
+ }
5926
+ };
5927
+ }
5928
+ function rewriteAsset3dToolsList(response) {
5929
+ const result = response.result;
5930
+ if (!result || typeof result !== "object" || Array.isArray(result))
5931
+ return response;
5932
+ const tools = result.tools;
5933
+ if (!Array.isArray(tools))
5934
+ return response;
5935
+ return {
5936
+ ...response,
5937
+ result: {
5938
+ ...result,
5939
+ tools: tools.map((tool) => tool && typeof tool === "object" && !Array.isArray(tool) ? rewriteSearchTool(tool) : tool)
5940
+ }
5941
+ };
5942
+ }
5943
+ function transformAsset3dSearchCall(projectRoot, request2) {
5944
+ if (request2.method !== "tools/call")
5945
+ return request2;
5946
+ const params = request2.params;
5947
+ if (!params || typeof params !== "object" || Array.isArray(params))
5948
+ return request2;
5949
+ const call = params;
5950
+ if (call.name !== "search_asset")
5951
+ return request2;
5952
+ const rawArguments = call.arguments;
5953
+ if (!rawArguments || typeof rawArguments !== "object" || Array.isArray(rawArguments)) {
5954
+ throw new Error("asset3d_search_arguments_invalid");
5955
+ }
5956
+ const args = rawArguments;
5957
+ if (typeof args.execution !== "string") {
5958
+ throw new Error("asset3d_execution_required: run `forgeax-game asset3d begin` first");
5959
+ }
5960
+ if (!Array.isArray(args.queries) || args.queries.some((query) => typeof query !== "string")) {
5961
+ throw new Error("asset3d_queries_invalid");
5962
+ }
5963
+ const outputDir = asset3dSearchOutputDir(projectRoot, args.execution, args.queries);
5964
+ const { execution: _execution, output_dir: _callerOutput, ...providerArguments } = args;
5965
+ return {
5966
+ ...request2,
5967
+ params: {
5968
+ ...call,
5969
+ arguments: {
5970
+ ...providerArguments,
5971
+ output_dir: outputDir
5972
+ }
5973
+ }
5974
+ };
5975
+ }
5976
+ async function runAsset3dMcpProxy(projectRoot, launch) {
5977
+ const child = spawn4(launch.command, [...launch.args], {
5978
+ stdio: ["pipe", "pipe", "pipe"],
5979
+ env: { ...process.env, ...launch.env }
5980
+ });
5981
+ child.stderr.pipe(process.stderr);
5982
+ const toolsListRequests = new Set;
5983
+ const searchExecutions = new Map;
5984
+ let inputBuffer = "";
5985
+ let outputBuffer = "";
5986
+ process.stdin.setEncoding("utf8");
5987
+ process.stdin.on("data", (chunk) => {
5988
+ inputBuffer += chunk;
5989
+ for (;; ) {
5990
+ const newline = inputBuffer.indexOf(`
5991
+ `);
5992
+ if (newline < 0)
5993
+ break;
5994
+ const line = inputBuffer.slice(0, newline);
5995
+ inputBuffer = inputBuffer.slice(newline + 1);
5996
+ if (!line.trim())
5997
+ continue;
5998
+ let request2;
5999
+ try {
6000
+ request2 = JSON.parse(line);
6001
+ const key = requestKey(request2.id);
6002
+ if (request2.method === "tools/list" && key)
6003
+ toolsListRequests.add(key);
6004
+ const params = request2.params;
6005
+ if (key && request2.method === "tools/call" && params && typeof params === "object" && !Array.isArray(params) && params.name === "search_asset") {
6006
+ const args = params.arguments;
6007
+ if (args && typeof args === "object" && !Array.isArray(args) && typeof args.execution === "string") {
6008
+ searchExecutions.set(key, args.execution);
6009
+ }
6010
+ }
6011
+ request2 = transformAsset3dSearchCall(projectRoot, request2);
6012
+ } catch (error) {
6013
+ let id = null;
6014
+ try {
6015
+ id = JSON.parse(line).id ?? null;
6016
+ } catch {}
6017
+ process.stdout.write(`${JSON.stringify(searchToolError(id, error))}
6018
+ `);
6019
+ continue;
6020
+ }
6021
+ child.stdin.write(`${JSON.stringify(request2)}
6022
+ `);
6023
+ }
6024
+ });
6025
+ process.stdin.on("end", () => child.stdin.end());
6026
+ child.stdout.setEncoding("utf8");
6027
+ child.stdout.on("data", (chunk) => {
6028
+ outputBuffer += chunk;
6029
+ for (;; ) {
6030
+ const newline = outputBuffer.indexOf(`
6031
+ `);
6032
+ if (newline < 0)
6033
+ break;
6034
+ const line = outputBuffer.slice(0, newline);
6035
+ outputBuffer = outputBuffer.slice(newline + 1);
6036
+ if (!line.trim())
6037
+ continue;
6038
+ try {
6039
+ let response = JSON.parse(line);
6040
+ const key = requestKey(response.id);
6041
+ if (key && toolsListRequests.delete(key))
6042
+ response = rewriteAsset3dToolsList(response);
6043
+ const execution = key ? searchExecutions.get(key) : undefined;
6044
+ if (key && execution) {
6045
+ searchExecutions.delete(key);
6046
+ try {
6047
+ const result = response.result;
6048
+ if (!result || typeof result !== "object" || Array.isArray(result) || result.isError === true) {
6049
+ throw new Error("asset3d_provider_result_missing");
6050
+ }
6051
+ const content = result.content;
6052
+ if (!Array.isArray(content) || content.length !== 1) {
6053
+ throw new Error("asset3d_provider_result_invalid");
6054
+ }
6055
+ const text2 = content[0];
6056
+ if (!text2 || typeof text2 !== "object" || Array.isArray(text2) || text2.type !== "text" || typeof text2.text !== "string") {
6057
+ throw new Error("asset3d_provider_result_invalid");
6058
+ }
6059
+ recordAsset3dProviderResult(projectRoot, execution, text2.text);
6060
+ } catch (error) {
6061
+ response = searchToolError(response.id, error);
6062
+ }
6063
+ }
6064
+ process.stdout.write(`${JSON.stringify(response)}
6065
+ `);
6066
+ } catch {
6067
+ process.stderr.write(`asset3d_provider_mcp_stdout_invalid
6068
+ `);
6069
+ child.kill("SIGTERM");
6070
+ }
6071
+ }
6072
+ });
6073
+ const stopChild = () => {
6074
+ if (child.exitCode === null && child.signalCode === null)
6075
+ child.kill("SIGTERM");
6076
+ };
6077
+ process.once("SIGINT", stopChild);
6078
+ process.once("SIGTERM", stopChild);
6079
+ return await new Promise((resolve14, reject) => {
6080
+ child.once("error", reject);
6081
+ child.once("close", (code, signal) => resolve14(code ?? (signal ? 1 : 0)));
6082
+ });
6083
+ }
6084
+
5647
6085
  // src/cli/dispatch.ts
5648
6086
  var HELP = `ForgeaX game development plugin
5649
6087
 
@@ -5656,14 +6094,15 @@ Usage:
5656
6094
  forgeax-game preview stop [--game <slug>] [--target-dir <path>] [--json]
5657
6095
  forgeax-game devkit install
5658
6096
  forgeax-game agents update
5659
- forgeax-game asset3d enable [--base-url <AW gateway/service URL>] [--ide ${CLIENT_CHOICES.join(",")}]
6097
+ forgeax-game asset3d enable [--library aw|ea] [--base-url <gateway/service URL>] [--ide ${CLIENT_CHOICES.join(",")}]
5660
6098
  forgeax-game asset3d install --provider-bundle <archive> --sha256 <hex> --download-origin <scheme://host:port> [--aw-base-url <URL> --aw-credential-file <absolute path>] [...]
5661
6099
  forgeax-game asset3d uninstall
5662
6100
  forgeax-game asset3d begin --query <text> [--query <text> ...] --json
5663
- forgeax-game asset3d commit --execution <uuid> --provider-result-stdin --json [--refresh]
6101
+ forgeax-game asset3d commit --execution <uuid> --json [--provider-result-stdin] [--refresh]
5664
6102
  forgeax-game asset3d abort --execution <uuid> --json
5665
6103
  forgeax-game asset3d doctor --json
5666
6104
  forgeax-game update [--ide ...]
6105
+ forgeax-game version
5667
6106
  forgeax-game help
5668
6107
 
5669
6108
  With no arguments, forgeax-game runs the stdio MCP server.
@@ -6090,6 +6529,16 @@ async function doctorCommand(args) {
6090
6529
  return warnings === 0 ? 0 : 1;
6091
6530
  }
6092
6531
  var UPDATE_USAGE = "usage: forgeax-game update [--ide codex,claude,cursor,...]";
6532
+ function versionCommand(args) {
6533
+ if (args.length > 0)
6534
+ throw new Error("usage: forgeax-game version");
6535
+ process.stdout.write(`${RELEASE_IDENTITY.gamePackage} ${RELEASE_IDENTITY.gameVersion}
6536
+ `);
6537
+ return 0;
6538
+ }
6539
+ function formatVersionTransition(previousVersion, currentVersion = RELEASE_IDENTITY.gameVersion) {
6540
+ return previousVersion === currentVersion ? currentVersion : `${previousVersion ?? "unknown"} -> ${currentVersion}`;
6541
+ }
6093
6542
  async function updateCommand(args) {
6094
6543
  const requested = parseIdeSelector(args, UPDATE_USAGE);
6095
6544
  const project = resolveProject();
@@ -6114,8 +6563,9 @@ async function updateCommand(args) {
6114
6563
  `);
6115
6564
  await verifyLaunch(launch);
6116
6565
  for (const client of configured) {
6566
+ const previousVersion = configuredGameVersion(client, root);
6117
6567
  const result = applyConfig(client, root, launch);
6118
- process.stdout.write(`${result.changed ? "UPDATED" : "CURRENT"} ${client.label}: ${result.path}
6568
+ process.stdout.write(`${result.changed ? "UPDATED" : "CURRENT"} ${client.label}: ${result.path} (plugin ${formatVersionTransition(previousVersion)})
6119
6569
  `);
6120
6570
  }
6121
6571
  if (project.root) {
@@ -6156,15 +6606,32 @@ function canonicalAsset3dClients(value) {
6156
6606
  throw new Error(`asset3d_client_invalid: ${invalid.join(", ")}`);
6157
6607
  return [...new Set(values)];
6158
6608
  }
6609
+ function parseAssetLibraryId(value) {
6610
+ if (value === "aw" || value === "ea")
6611
+ return value;
6612
+ throw new Error("asset3d_library_invalid: expected aw or ea");
6613
+ }
6159
6614
  function parseAsset3dEnableArgs(args) {
6160
6615
  const ideArgs = [];
6161
6616
  let baseUrl;
6617
+ let library;
6162
6618
  for (let index = 0;index < args.length; index++) {
6163
6619
  const arg = args[index];
6620
+ if (arg === "--library") {
6621
+ const value = args[++index];
6622
+ if (!value)
6623
+ throw new Error("--library requires aw or ea");
6624
+ library = parseAssetLibraryId(value);
6625
+ continue;
6626
+ }
6627
+ if (arg.startsWith("--library=")) {
6628
+ library = parseAssetLibraryId(arg.slice("--library=".length));
6629
+ continue;
6630
+ }
6164
6631
  if (arg === "--base-url") {
6165
6632
  const value = args[++index];
6166
6633
  if (!value)
6167
- throw new Error("--base-url requires an AW gateway/service URL");
6634
+ throw new Error("--base-url requires a gateway/service URL");
6168
6635
  baseUrl = value;
6169
6636
  continue;
6170
6637
  }
@@ -6183,14 +6650,31 @@ function parseAsset3dEnableArgs(args) {
6183
6650
  ideArgs.push(arg);
6184
6651
  continue;
6185
6652
  }
6186
- throw new Error("usage: forgeax-game asset3d enable [--base-url <AW gateway/service URL>] [--ide codex,claude,cursor,...]");
6653
+ throw new Error("usage: forgeax-game asset3d enable [--library aw|ea] [--base-url <gateway/service URL>] [--ide codex,claude,cursor,...]");
6187
6654
  }
6188
- return { requested: parseIdeSelector(ideArgs, "invalid --ide selector"), baseUrl };
6655
+ return { requested: parseIdeSelector(ideArgs, "invalid --ide selector"), baseUrl, library };
6189
6656
  }
6190
6657
  async function asset3dCommand(args) {
6191
6658
  const [operation, ...rest] = args;
6192
- const projectRoot = requireProject();
6193
6659
  try {
6660
+ if (operation === "mcp") {
6661
+ if (rest.length)
6662
+ throw new Error("usage: forgeax-game asset3d mcp");
6663
+ const binding = resolveProject();
6664
+ if (!binding.root)
6665
+ return runDormantAsset3dMcp();
6666
+ let launch;
6667
+ try {
6668
+ launch = asset3dProviderLaunch(binding.root);
6669
+ } catch (error) {
6670
+ if (error instanceof Error && error.message === "asset3d_not_installed") {
6671
+ return runDormantAsset3dMcp();
6672
+ }
6673
+ throw error;
6674
+ }
6675
+ return runAsset3dMcpProxy(binding.root, launch);
6676
+ }
6677
+ const projectRoot = requireProject();
6194
6678
  if (operation === "enable") {
6195
6679
  const parsed = parseAsset3dEnableArgs(rest);
6196
6680
  const selection = selectClients(projectRoot, parsed.requested);
@@ -6198,7 +6682,23 @@ async function asset3dCommand(args) {
6198
6682
  if (selection.selected.length === 0) {
6199
6683
  throw new Error("asset3d_client_missing: run `forgeax-game install --ide <client>` before enabling Asset3D");
6200
6684
  }
6201
- const serviceRoot = resolveAwServiceRoot(parsed.baseUrl);
6685
+ const bridge = asset3dLaunchSpec("npx");
6686
+ for (const id of selection.selected) {
6687
+ const client = findClient(id);
6688
+ if (!client)
6689
+ throw new Error(`asset3d_client_invalid: ${id}`);
6690
+ const state = inspectConfig(client, projectRoot, bridge, ASSET3D_SERVER_KEY);
6691
+ if (state.state === "different") {
6692
+ throw new Error(`asset3d_client_server_conflict: ${client.label} already has a different ${ASSET3D_SERVER_KEY} entry`);
6693
+ }
6694
+ if (state.state === "invalid") {
6695
+ throw new Error(`asset3d_client_config_invalid: ${client.label}: ${state.detail ?? state.path}`);
6696
+ }
6697
+ }
6698
+ const library = resolveAssetLibrarySelection({
6699
+ library: parsed.library,
6700
+ baseUrl: parsed.baseUrl
6701
+ });
6202
6702
  const os = platform2() === "win32" ? "windows" : platform2();
6203
6703
  const cpu = arch2();
6204
6704
  const target = `${os}-${cpu}`;
@@ -6216,20 +6716,37 @@ async function asset3dCommand(args) {
6216
6716
  const credentialWrite = writeAwCredential(credentialFile, credential.key);
6217
6717
  const clients = selection.selected.filter((id) => (id in SKILL_MOUNTS));
6218
6718
  try {
6219
- const access = checkAwProviderAccess({ providerCache: preparedCache, serviceRoot, credentialFile });
6719
+ const access = checkAssetLibraryProviderAccess({
6720
+ providerCache: preparedCache,
6721
+ depotName: library.library,
6722
+ serviceRoot: library.serviceRoot,
6723
+ credentialFile
6724
+ });
6220
6725
  const result = await installProvisionedAsset3d({
6221
6726
  projectRoot,
6222
6727
  providerCache: preparedCache,
6223
6728
  expectedSha256: bundled.sha256,
6224
6729
  downloadOrigins: access.downloadOrigins,
6225
6730
  awApiBaseUrl: access.serviceRoot,
6731
+ awDepotName: library.library,
6226
6732
  awCredentialFile: credentialFile,
6227
6733
  clients,
6228
6734
  gamePluginLaunch: launchSpec("npx"),
6229
6735
  replaceOwned: true
6230
6736
  });
6737
+ for (const id of selection.selected) {
6738
+ const client = findClient(id);
6739
+ if (!client)
6740
+ throw new Error(`asset3d_client_invalid: ${id}`);
6741
+ const applied = applyConfig(client, projectRoot, bridge, ASSET3D_SERVER_KEY);
6742
+ process.stdout.write(`${applied.changed ? "UPDATED" : "CURRENT"} ${client.label} Asset3D bridge: ${applied.path}
6743
+ `);
6744
+ if (applied.changed && client.postInstallNote)
6745
+ process.stdout.write(` ${client.postInstallNote}
6746
+ `);
6747
+ }
6231
6748
  credentialWrite.commit();
6232
- process.stdout.write(`${result.changed ? "ENABLED" : "CURRENT"} Asset3D provider ${result.providerCommit}; service=${access.serviceRoot}; authentication=${access.authentication}; skillFiles=${result.skillFiles}
6749
+ process.stdout.write(`${result.changed ? "ENABLED" : "CURRENT"} Asset3D provider ${result.providerCommit}; library=${library.library}; service=${access.serviceRoot}; authentication=${access.authentication}; skillFiles=${result.skillFiles}
6233
6750
  `);
6234
6751
  return 0;
6235
6752
  } catch (error) {
@@ -6242,6 +6759,7 @@ async function asset3dCommand(args) {
6242
6759
  let expectedSha256;
6243
6760
  let catalogBaseUrl;
6244
6761
  let awApiBaseUrl;
6762
+ let awDepotName;
6245
6763
  let awCredentialFile;
6246
6764
  const downloadOrigins = [];
6247
6765
  let clients;
@@ -6252,7 +6770,7 @@ async function asset3dCommand(args) {
6252
6770
  replaceOwned = true;
6253
6771
  continue;
6254
6772
  }
6255
- if (arg === "--provider-bundle" || arg === "--sha256" || arg === "--download-origin" || arg === "--catalog-base-url" || arg === "--aw-base-url" || arg === "--aw-credential-file" || arg === "--ide") {
6773
+ if (arg === "--provider-bundle" || arg === "--sha256" || arg === "--download-origin" || arg === "--catalog-base-url" || arg === "--aw-base-url" || arg === "--aw-depot-name" || arg === "--aw-credential-file" || arg === "--ide") {
6256
6774
  const value = rest[++index];
6257
6775
  if (!value)
6258
6776
  throw new Error(`${arg} requires a value`);
@@ -6265,7 +6783,9 @@ async function asset3dCommand(args) {
6265
6783
  else if (arg === "--catalog-base-url")
6266
6784
  catalogBaseUrl = value;
6267
6785
  else if (arg === "--aw-base-url")
6268
- awApiBaseUrl = resolveAwServiceRoot(value);
6786
+ awApiBaseUrl = normalizeAssetLibraryServiceRoot(value);
6787
+ else if (arg === "--aw-depot-name")
6788
+ awDepotName = parseAssetLibraryId(value);
6269
6789
  else if (arg === "--aw-credential-file")
6270
6790
  awCredentialFile = resolve14(value);
6271
6791
  else
@@ -6283,6 +6803,7 @@ async function asset3dCommand(args) {
6283
6803
  downloadOrigins,
6284
6804
  ...catalogBaseUrl ? { catalogBaseUrl } : {},
6285
6805
  ...awApiBaseUrl ? { awApiBaseUrl } : {},
6806
+ ...awDepotName ? { awDepotName } : {},
6286
6807
  ...awCredentialFile ? { awCredentialFile } : {},
6287
6808
  ...clients ? { clients } : {},
6288
6809
  replaceOwned
@@ -6333,11 +6854,16 @@ async function asset3dCommand(args) {
6333
6854
  execution = rest[++index];
6334
6855
  continue;
6335
6856
  }
6336
- throw new Error("usage: forgeax-game asset3d commit --execution <uuid> --provider-result-stdin --json [--refresh]");
6857
+ throw new Error("usage: forgeax-game asset3d commit --execution <uuid> --json [--provider-result-stdin] [--refresh]");
6337
6858
  }
6338
- if (!execution || !stdin)
6339
- throw new Error("usage: forgeax-game asset3d commit --execution <uuid> --provider-result-stdin --json [--refresh]");
6340
- const result = commitAsset3d({ projectRoot, execution, providerResult: await readBoundedStdin(), refresh });
6859
+ if (!execution)
6860
+ throw new Error("usage: forgeax-game asset3d commit --execution <uuid> --json [--provider-result-stdin] [--refresh]");
6861
+ const result = commitAsset3d({
6862
+ projectRoot,
6863
+ execution,
6864
+ ...stdin ? { providerResult: await readBoundedStdin() } : {},
6865
+ refresh
6866
+ });
6341
6867
  asset3dEnvelope("asset3d.commit", result.failed === 0, result);
6342
6868
  return result.failed === 0 ? 0 : 1;
6343
6869
  }
@@ -6388,6 +6914,10 @@ async function runCli(argv) {
6388
6914
  return asset3dCommand(args);
6389
6915
  case "update":
6390
6916
  return updateCommand(args);
6917
+ case "version":
6918
+ case "--version":
6919
+ case "-v":
6920
+ return versionCommand(args);
6391
6921
  case "help":
6392
6922
  case "--help":
6393
6923
  case "-h":