@forgeax/game 0.2.2 → 0.2.4
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/README.md +31 -2
- package/assets/skills/forgeax-game/references/engine-skills.md +2 -2
- package/dist/main.js +474 -33
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ One executable provides both surfaces:
|
|
|
10
10
|
- `forgeax-game` with no arguments runs the stdio MCP server.
|
|
11
11
|
- `forgeax-game <command>` performs one-time setup and project operations.
|
|
12
12
|
|
|
13
|
-
The package has one exact runtime dependency, `@forgeax/game-runtime@0.3.
|
|
13
|
+
The package has one exact runtime dependency, `@forgeax/game-runtime@0.3.33`, and
|
|
14
14
|
externalizes it from the Game bundle.
|
|
15
15
|
|
|
16
16
|
> [!IMPORTANT]
|
|
@@ -133,6 +133,8 @@ one-time actions on every turn.
|
|
|
133
133
|
| `forgeax://status` | Resource | Preferred read-only project, service, and next-action status |
|
|
134
134
|
| `forgeax_status_lite` | Tool | Status fallback for clients without MCP resource support |
|
|
135
135
|
| `forgeax_run_current_game` | Tool | Build or reuse the active game's static preview, return its URL and health identity, and identify the runtime log file |
|
|
136
|
+
| `forgeax_generate_image` | Tool | Text-to-image, or image-to-image with a local `image`; saves a PNG/JPG into the active game's `assets/` and returns its path |
|
|
137
|
+
| `forgeax_generate_3d` | Tool | Text-to-3D (`prompt`) or image-to-3D (public https `image` URL); runs the async job to completion and saves a `.glb` into `assets/` |
|
|
136
138
|
|
|
137
139
|
When this plugin cold-starts the managed Runtime, runtime output is written to:
|
|
138
140
|
|
|
@@ -158,6 +160,33 @@ The game SDK is available at `.forgeax/engine-sdk/`. Use its declaration files a
|
|
|
158
160
|
`templates/game-default` or `templates/game-empty` before writing imports. If an API
|
|
159
161
|
is absent, inspect `source/<package>/src/` instead of guessing.
|
|
160
162
|
|
|
163
|
+
### Asset generation (LiteLLM)
|
|
164
|
+
|
|
165
|
+
`forgeax_generate_image` and `forgeax_generate_3d` produce art and 3D assets through a
|
|
166
|
+
LiteLLM gateway and save them into the active game's `assets/` directory, returning the
|
|
167
|
+
project-relative path to reference from game code. Configure via environment:
|
|
168
|
+
|
|
169
|
+
| Variable | Required | Default |
|
|
170
|
+
|:--|:-:|:--|
|
|
171
|
+
| `FORGEAX_LITELLM_API_KEY` | yes | — (secret; never commit it) |
|
|
172
|
+
| `FORGEAX_LITELLM_BASE_URL` | no | the shared ForgeaX gateway |
|
|
173
|
+
| `FORGEAX_GEN_IMAGE_MODEL` | no | `gemini-3-pro-image` |
|
|
174
|
+
| `FORGEAX_GEN_3D_TEXT_MODEL` | no | `tripo-3d-text` |
|
|
175
|
+
| `FORGEAX_GEN_3D_IMAGE_MODEL` | no | `tripo-3d-image` |
|
|
176
|
+
| `FORGEAX_COS_BUCKET` / `FORGEAX_COS_REGION` | for local image-to-3D | — |
|
|
177
|
+
| `FORGEAX_COS_SECRET_ID` / `FORGEAX_COS_SECRET_KEY` | for local image-to-3D | — (secret; never commit) |
|
|
178
|
+
|
|
179
|
+
- **Text-to-image / image-to-image**: `forgeax_generate_image({ prompt, image? })`. A
|
|
180
|
+
local `image` path switches to editing that image with the prompt.
|
|
181
|
+
- **Text-to-3D**: `forgeax_generate_3d({ prompt })` — submits, polls to completion
|
|
182
|
+
(~1–2 min), and downloads the `.glb`.
|
|
183
|
+
- **Image-to-3D**: `forgeax_generate_3d({ image })`. `image` is a **public https URL**,
|
|
184
|
+
or a **local file path** when COS is configured — the file is uploaded to the COS
|
|
185
|
+
bucket and passed to the backend as a short-lived presigned URL (the private bucket
|
|
186
|
+
stays private; the URL expires within the hour). This makes the "generate a concept
|
|
187
|
+
image, then turn it into a mesh" flow work end to end. Without COS, only a public URL
|
|
188
|
+
is accepted, because the 3D endpoint rejects local paths and inline base64.
|
|
189
|
+
|
|
161
190
|
## Supported clients
|
|
162
191
|
|
|
163
192
|
| Client ID | Config path | Scope |
|
|
@@ -197,7 +226,7 @@ real Studio-versus-Play validation boundary.
|
|
|
197
226
|
The published package also carries the host-install metadata needed to expose that
|
|
198
227
|
Skill directly to Codex, Claude Code, Cursor and other configured clients. A harness
|
|
199
228
|
checkout is optional compatibility support, not a user prerequisite. Inside a ForgeaX
|
|
200
|
-
project, `install`, `init`, and `
|
|
229
|
+
project, `install`, `init`, and `update` refresh it automatically. It can also be
|
|
201
230
|
installed explicitly:
|
|
202
231
|
|
|
203
232
|
```bash
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Engine skills available in Runtime 0.3.
|
|
1
|
+
# Engine skills available in Runtime 0.3.33
|
|
2
2
|
|
|
3
3
|
Derived from the Engine skills carried by the installed Runtime package. Invoke only
|
|
4
4
|
ids listed here; Engine package names and skill ids are not interchangeable.
|
|
@@ -15,7 +15,7 @@ ids listed here; Engine package names and skill ids are not interchangeable.
|
|
|
15
15
|
| `forgeax-engine-material` | Build and debug visible ForgeaX materials through the single MaterialAsset route. |
|
|
16
16
|
| `forgeax-engine-math` | forgeax-engine 的纯函数数学库:vec / mat / quat / euler / color 的 out-param 风格函数, 从 Transform.world mat4 读 pose,screenToRay / rayAabbIntersects 做拾取。 |
|
|
17
17
|
| `forgeax-engine-physics` | forgeax-engine 物理:给 entity 挂 Transform + RigidBody + Collider 即被每帧驱动位置。 |
|
|
18
|
-
| `forgeax-engine-render-pipeline` | forgeax-engine
|
|
18
|
+
| `forgeax-engine-render-pipeline` | forgeax-engine 渲染管线、后处理与 typed RenderGraph。Use when toggling camera effects, authoring a custom RenderPipeline, contributing compute or raster RenderFeature work, or tracing graph |
|
|
19
19
|
| `forgeax-engine-rhi` | forgeax-engine 底层渲染硬件接口(贡献者向):spec-aligned 纯接口、opaque handle、 math-free、双实现同发、capability gate。Use when contributing to or debugging the RHI backend, reading capability gates, or tr |
|
|
20
20
|
| `forgeax-engine-rhi-debug` | forgeax-engine 的 RHI 帧录制 + 确定性 replay + 离线 per-draw inspect(RenderDoc 思想)。 |
|
|
21
21
|
| `forgeax-engine-sdk` | Build and verify the portable ForgeaX browser-game SDK ZIP from an Engine checkout. |
|
package/dist/main.js
CHANGED
|
@@ -248,7 +248,7 @@ function runStdioServer(spec) {
|
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
// src/mcp/forgeax-server.ts
|
|
251
|
-
import { readFileSync as
|
|
251
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
252
252
|
|
|
253
253
|
// src/status/collect.ts
|
|
254
254
|
import { readFileSync as readFileSync4 } from "node:fs";
|
|
@@ -664,6 +664,14 @@ reasoning and game-code edits; the plugin owns ForgeaX Runtime lifecycle and fee
|
|
|
664
664
|
- Reading runtime errors or engine logs: read the file path returned in
|
|
665
665
|
\`runtime_logs.local_file\` with your own file-reading tool. Log tailing is
|
|
666
666
|
deliberately not an MCP tool — the log is a file, so read it like one.
|
|
667
|
+
- Generating art or 3D assets ("make a sprite", "I need a texture", "generate a
|
|
668
|
+
model of…"): call \`forgeax_generate_image\` (text-to-image, or image-to-image
|
|
669
|
+
with a local \`image\`) or \`forgeax_generate_3d\` (text-to-3D via \`prompt\`,
|
|
670
|
+
image-to-3D via \`image\`). Both save into the active game's \`assets/\` directory
|
|
671
|
+
and return the project-relative path to reference from code. Image-to-3D accepts a
|
|
672
|
+
public https URL, or a local file path when COS is configured (it is uploaded and
|
|
673
|
+
passed as a short-lived presigned URL). They need \`FORGEAX_LITELLM_API_KEY\` (and
|
|
674
|
+
\`FORGEAX_COS_*\` for local-file image-to-3D) in the environment.
|
|
667
675
|
- Creating a game, switching the active game, installing or upgrading the plugin:
|
|
668
676
|
these are one-time operations and are CLI subcommands, not MCP tools. Run
|
|
669
677
|
\`npx -y -p @forgeax/game forgeax-game <init|use|doctor|devkit|upgrade>\`.
|
|
@@ -1652,11 +1660,432 @@ function resolveSlug(root, requested) {
|
|
|
1652
1660
|
return { slug, dir };
|
|
1653
1661
|
}
|
|
1654
1662
|
|
|
1663
|
+
// src/gen/generate.ts
|
|
1664
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync7, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "node:fs";
|
|
1665
|
+
import { basename, extname, join as join7, relative as relative3 } from "node:path";
|
|
1666
|
+
|
|
1667
|
+
// src/gen/config.ts
|
|
1668
|
+
var DEFAULT_LITELLM_BASE_URL = "http://21.214.33.175:4000";
|
|
1669
|
+
var DEFAULT_MODELS = {
|
|
1670
|
+
textToImage: "gemini-3-pro-image",
|
|
1671
|
+
textTo3d: "tripo-3d-text",
|
|
1672
|
+
imageTo3d: "tripo-3d-image"
|
|
1673
|
+
};
|
|
1674
|
+
function env(name) {
|
|
1675
|
+
const value = process.env[name]?.trim();
|
|
1676
|
+
return value ? value : undefined;
|
|
1677
|
+
}
|
|
1678
|
+
function resolveLiteLlmConfig() {
|
|
1679
|
+
const baseUrl = (env("FORGEAX_LITELLM_BASE_URL") ?? DEFAULT_LITELLM_BASE_URL).replace(/\/+$/, "");
|
|
1680
|
+
const apiKey = env("FORGEAX_LITELLM_API_KEY");
|
|
1681
|
+
if (!apiKey) {
|
|
1682
|
+
throw new Error("FORGEAX_LITELLM_API_KEY is not set. Export the LiteLLM key so the asset tools can reach the gateway, e.g. `export FORGEAX_LITELLM_API_KEY=sk-...`.");
|
|
1683
|
+
}
|
|
1684
|
+
return {
|
|
1685
|
+
baseUrl,
|
|
1686
|
+
apiKey,
|
|
1687
|
+
models: {
|
|
1688
|
+
textToImage: env("FORGEAX_GEN_IMAGE_MODEL") ?? DEFAULT_MODELS.textToImage,
|
|
1689
|
+
textTo3d: env("FORGEAX_GEN_3D_TEXT_MODEL") ?? DEFAULT_MODELS.textTo3d,
|
|
1690
|
+
imageTo3d: env("FORGEAX_GEN_3D_IMAGE_MODEL") ?? DEFAULT_MODELS.imageTo3d
|
|
1691
|
+
}
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
// src/gen/cos.ts
|
|
1696
|
+
import { createHash as createHash2, createHmac } from "node:crypto";
|
|
1697
|
+
var DEFAULT_EXPIRES_SEC = 3600;
|
|
1698
|
+
var SKEW_SEC = 60;
|
|
1699
|
+
function env2(name) {
|
|
1700
|
+
const value = process.env[name]?.trim();
|
|
1701
|
+
return value ? value : undefined;
|
|
1702
|
+
}
|
|
1703
|
+
function resolveCosConfig() {
|
|
1704
|
+
const bucket = env2("FORGEAX_COS_BUCKET");
|
|
1705
|
+
const region = env2("FORGEAX_COS_REGION");
|
|
1706
|
+
const secretId = env2("FORGEAX_COS_SECRET_ID");
|
|
1707
|
+
const secretKey = env2("FORGEAX_COS_SECRET_KEY");
|
|
1708
|
+
if (!bucket || !region || !secretId || !secretKey)
|
|
1709
|
+
return;
|
|
1710
|
+
return { bucket, region, secretId, secretKey };
|
|
1711
|
+
}
|
|
1712
|
+
function cosHost(cfg) {
|
|
1713
|
+
return `${cfg.bucket}.cos.${cfg.region}.myqcloud.com`;
|
|
1714
|
+
}
|
|
1715
|
+
function rfc3986(value) {
|
|
1716
|
+
return encodeURIComponent(value).replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
1717
|
+
}
|
|
1718
|
+
function formatKv(map) {
|
|
1719
|
+
const lowered = {};
|
|
1720
|
+
for (const [k, v] of Object.entries(map))
|
|
1721
|
+
lowered[k.toLowerCase()] = v;
|
|
1722
|
+
const keys = Object.keys(lowered).sort();
|
|
1723
|
+
return {
|
|
1724
|
+
serialized: keys.map((k) => `${rfc3986(k)}=${rfc3986(lowered[k])}`).join("&"),
|
|
1725
|
+
keyList: keys.map((k) => rfc3986(k)).join(";")
|
|
1726
|
+
};
|
|
1727
|
+
}
|
|
1728
|
+
function buildAuthorization(cfg, opts) {
|
|
1729
|
+
const start = opts.nowSec - SKEW_SEC;
|
|
1730
|
+
const end = opts.nowSec + (opts.expiresSec ?? DEFAULT_EXPIRES_SEC);
|
|
1731
|
+
const signTime = `${start};${end}`;
|
|
1732
|
+
const signKey = createHmac("sha1", cfg.secretKey).update(signTime).digest("hex");
|
|
1733
|
+
const { serialized: paramStr, keyList: paramList } = formatKv(opts.params ?? {});
|
|
1734
|
+
const { serialized: headerStr, keyList: headerList } = formatKv(opts.headers ?? {});
|
|
1735
|
+
const httpString = `${opts.method.toLowerCase()}
|
|
1736
|
+
${opts.pathname}
|
|
1737
|
+
${paramStr}
|
|
1738
|
+
${headerStr}
|
|
1739
|
+
`;
|
|
1740
|
+
const httpStringSha1 = createHash2("sha1").update(httpString).digest("hex");
|
|
1741
|
+
const stringToSign = `sha1
|
|
1742
|
+
${signTime}
|
|
1743
|
+
${httpStringSha1}
|
|
1744
|
+
`;
|
|
1745
|
+
const signature = createHmac("sha1", signKey).update(stringToSign).digest("hex");
|
|
1746
|
+
return [
|
|
1747
|
+
"q-sign-algorithm=sha1",
|
|
1748
|
+
`q-ak=${cfg.secretId}`,
|
|
1749
|
+
`q-sign-time=${signTime}`,
|
|
1750
|
+
`q-key-time=${signTime}`,
|
|
1751
|
+
`q-header-list=${headerList}`,
|
|
1752
|
+
`q-url-param-list=${paramList}`,
|
|
1753
|
+
`q-signature=${signature}`
|
|
1754
|
+
].join("&");
|
|
1755
|
+
}
|
|
1756
|
+
function presignGetUrl(cfg, key, expiresSec = DEFAULT_EXPIRES_SEC, nowSec = Math.floor(Date.now() / 1000)) {
|
|
1757
|
+
const pathname = key.startsWith("/") ? key : `/${key}`;
|
|
1758
|
+
const auth = buildAuthorization(cfg, { method: "get", pathname, nowSec, expiresSec });
|
|
1759
|
+
return `https://${cosHost(cfg)}${pathname}?${auth}`;
|
|
1760
|
+
}
|
|
1761
|
+
async function uploadObject(cfg, key, bytes, contentType) {
|
|
1762
|
+
const host = cosHost(cfg);
|
|
1763
|
+
const pathname = key.startsWith("/") ? key : `/${key}`;
|
|
1764
|
+
const auth = buildAuthorization(cfg, {
|
|
1765
|
+
method: "put",
|
|
1766
|
+
pathname,
|
|
1767
|
+
headers: { host },
|
|
1768
|
+
nowSec: Math.floor(Date.now() / 1000),
|
|
1769
|
+
expiresSec: 600
|
|
1770
|
+
});
|
|
1771
|
+
const ctrl = new AbortController;
|
|
1772
|
+
const timer = setTimeout(() => ctrl.abort(), 60000);
|
|
1773
|
+
try {
|
|
1774
|
+
const res = await fetch(`https://${host}${pathname}`, {
|
|
1775
|
+
method: "PUT",
|
|
1776
|
+
headers: { authorization: auth, "content-type": contentType },
|
|
1777
|
+
body: bytes,
|
|
1778
|
+
signal: ctrl.signal
|
|
1779
|
+
});
|
|
1780
|
+
if (!res.ok) {
|
|
1781
|
+
const body = await res.text().catch(() => "");
|
|
1782
|
+
throw new Error(`COS upload of ${key} failed: ${res.status} ${res.statusText}${body ? ` — ${body.slice(0, 400)}` : ""}`);
|
|
1783
|
+
}
|
|
1784
|
+
} finally {
|
|
1785
|
+
clearTimeout(timer);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
async function uploadAndPresign(cfg, key, bytes, contentType, expiresSec = DEFAULT_EXPIRES_SEC) {
|
|
1789
|
+
await uploadObject(cfg, key, bytes, contentType);
|
|
1790
|
+
return presignGetUrl(cfg, key, expiresSec);
|
|
1791
|
+
}
|
|
1792
|
+
|
|
1793
|
+
// src/gen/litellm.ts
|
|
1794
|
+
var TASK_TIMEOUT_MS = 420000;
|
|
1795
|
+
var POLL_INTERVAL_MS = 3000;
|
|
1796
|
+
var REQUEST_TIMEOUT_MS = 60000;
|
|
1797
|
+
function authHeaders(cfg) {
|
|
1798
|
+
return { authorization: `Bearer ${cfg.apiKey}` };
|
|
1799
|
+
}
|
|
1800
|
+
async function request(url, init, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
1801
|
+
const ctrl = new AbortController;
|
|
1802
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
1803
|
+
try {
|
|
1804
|
+
const res = await fetch(url, { ...init, signal: ctrl.signal });
|
|
1805
|
+
if (!res.ok) {
|
|
1806
|
+
const body = await res.text().catch(() => "");
|
|
1807
|
+
throw new Error(`LiteLLM ${init.method ?? "GET"} ${url} failed: ${res.status} ${res.statusText}${body ? ` — ${body.slice(0, 600)}` : ""}`);
|
|
1808
|
+
}
|
|
1809
|
+
return res;
|
|
1810
|
+
} catch (err) {
|
|
1811
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
1812
|
+
throw new Error(`LiteLLM request timed out after ${timeoutMs}ms: ${url}`);
|
|
1813
|
+
}
|
|
1814
|
+
throw err;
|
|
1815
|
+
} finally {
|
|
1816
|
+
clearTimeout(timer);
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
function decodeImagePayload(item) {
|
|
1820
|
+
if (!item)
|
|
1821
|
+
return {};
|
|
1822
|
+
const b64 = typeof item.b64_json === "string" ? item.b64_json : undefined;
|
|
1823
|
+
const url = typeof item.url === "string" ? item.url : undefined;
|
|
1824
|
+
return { b64, url };
|
|
1825
|
+
}
|
|
1826
|
+
function sniffImageExt(bytes) {
|
|
1827
|
+
if (bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255)
|
|
1828
|
+
return "jpg";
|
|
1829
|
+
return "png";
|
|
1830
|
+
}
|
|
1831
|
+
async function materializeImage(payload) {
|
|
1832
|
+
if (payload.b64) {
|
|
1833
|
+
const bytes = new Uint8Array(Buffer.from(payload.b64, "base64"));
|
|
1834
|
+
return { bytes, ext: sniffImageExt(bytes) };
|
|
1835
|
+
}
|
|
1836
|
+
if (payload.url) {
|
|
1837
|
+
const res = await request(payload.url, { method: "GET" });
|
|
1838
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
1839
|
+
return { bytes, ext: sniffImageExt(bytes) };
|
|
1840
|
+
}
|
|
1841
|
+
throw new Error("LiteLLM image response contained neither b64_json nor url.");
|
|
1842
|
+
}
|
|
1843
|
+
async function generateImage(cfg, opts) {
|
|
1844
|
+
const res = await request(`${cfg.baseUrl}/v1/images/generations`, {
|
|
1845
|
+
method: "POST",
|
|
1846
|
+
headers: { ...authHeaders(cfg), "content-type": "application/json" },
|
|
1847
|
+
body: JSON.stringify({ model: opts.model, prompt: opts.prompt, n: 1, ...opts.size ? { size: opts.size } : {} })
|
|
1848
|
+
});
|
|
1849
|
+
const json = await res.json();
|
|
1850
|
+
return materializeImage(decodeImagePayload(json.data?.[0]));
|
|
1851
|
+
}
|
|
1852
|
+
async function editImage(cfg, opts) {
|
|
1853
|
+
const form = new FormData;
|
|
1854
|
+
form.set("model", opts.model);
|
|
1855
|
+
form.set("prompt", opts.prompt);
|
|
1856
|
+
form.set("n", "1");
|
|
1857
|
+
form.set("image", new Blob([opts.image]), opts.filename);
|
|
1858
|
+
const res = await request(`${cfg.baseUrl}/v1/images/edits`, {
|
|
1859
|
+
method: "POST",
|
|
1860
|
+
headers: authHeaders(cfg),
|
|
1861
|
+
body: form
|
|
1862
|
+
});
|
|
1863
|
+
const json = await res.json();
|
|
1864
|
+
return materializeImage(decodeImagePayload(json.data?.[0]));
|
|
1865
|
+
}
|
|
1866
|
+
async function submit3dTask(cfg, body) {
|
|
1867
|
+
const res = await request(`${cfg.baseUrl}/v1/3d/generations`, {
|
|
1868
|
+
method: "POST",
|
|
1869
|
+
headers: { ...authHeaders(cfg), "content-type": "application/json" },
|
|
1870
|
+
body: JSON.stringify(body)
|
|
1871
|
+
});
|
|
1872
|
+
const json = await res.json();
|
|
1873
|
+
if (!json.id)
|
|
1874
|
+
throw new Error(`LiteLLM 3D submit returned no task id: ${JSON.stringify(json).slice(0, 400)}`);
|
|
1875
|
+
return json.id;
|
|
1876
|
+
}
|
|
1877
|
+
async function poll3dTask(cfg, id, onProgress) {
|
|
1878
|
+
const deadline = Date.now() + TASK_TIMEOUT_MS;
|
|
1879
|
+
for (;; ) {
|
|
1880
|
+
const res = await request(`${cfg.baseUrl}/v1/3d/tasks/${encodeURIComponent(id)}`, {
|
|
1881
|
+
method: "GET",
|
|
1882
|
+
headers: authHeaders(cfg)
|
|
1883
|
+
});
|
|
1884
|
+
const state = await res.json();
|
|
1885
|
+
if (typeof state.progress === "number")
|
|
1886
|
+
onProgress?.(state.progress);
|
|
1887
|
+
const status = state.status?.toLowerCase();
|
|
1888
|
+
if (status === "succeeded" || status === "success" || status === "completed")
|
|
1889
|
+
return state;
|
|
1890
|
+
if (status === "failed" || status === "error" || status === "cancelled") {
|
|
1891
|
+
throw new Error(`LiteLLM 3D task ${id} ${state.status}${state.error ? `: ${JSON.stringify(state.error).slice(0, 300)}` : ""}`);
|
|
1892
|
+
}
|
|
1893
|
+
if (Date.now() > deadline) {
|
|
1894
|
+
throw new Error(`LiteLLM 3D task ${id} did not finish within ${TASK_TIMEOUT_MS / 1000}s (last status: ${state.status}).`);
|
|
1895
|
+
}
|
|
1896
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
async function downloadMesh(state) {
|
|
1900
|
+
const assets = (state.data ?? []).map((d) => ({
|
|
1901
|
+
url: typeof d.url === "string" ? d.url : undefined,
|
|
1902
|
+
type: typeof d.type === "string" ? d.type : "",
|
|
1903
|
+
format: typeof d.format === "string" ? d.format : ""
|
|
1904
|
+
}));
|
|
1905
|
+
const mesh = assets.find((a) => a.url && (a.type === "mesh" || /\.(glb|gltf|obj|fbx|usdz)/i.test(a.url ?? ""))) ?? assets.find((a) => a.url);
|
|
1906
|
+
if (!mesh?.url)
|
|
1907
|
+
throw new Error(`LiteLLM 3D task ${state.id} completed with no downloadable mesh asset.`);
|
|
1908
|
+
const res = await request(mesh.url, { method: "GET" });
|
|
1909
|
+
const bytes = new Uint8Array(await res.arrayBuffer());
|
|
1910
|
+
const ext = mesh.format || (mesh.url.match(/\.([a-z0-9]+)(?:\?|$)/i)?.[1] ?? "glb").toLowerCase();
|
|
1911
|
+
return { bytes, ext, assetType: mesh.type || "mesh" };
|
|
1912
|
+
}
|
|
1913
|
+
async function generate3dFromText(cfg, opts) {
|
|
1914
|
+
const id = await submit3dTask(cfg, { model: opts.model, prompt: opts.prompt });
|
|
1915
|
+
return downloadMesh(await poll3dTask(cfg, id, opts.onProgress));
|
|
1916
|
+
}
|
|
1917
|
+
async function generate3dFromImageUrl(cfg, opts) {
|
|
1918
|
+
const id = await submit3dTask(cfg, { model: opts.model, image_url: opts.imageUrl, ...opts.prompt ? { prompt: opts.prompt } : {} });
|
|
1919
|
+
return downloadMesh(await poll3dTask(cfg, id, opts.onProgress));
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
// src/gen/generate.ts
|
|
1923
|
+
function assetsDirFor(cwd, explicitGame) {
|
|
1924
|
+
const binding = resolveProject(cwd);
|
|
1925
|
+
if (!binding.root) {
|
|
1926
|
+
throw new Error(`No ForgeaX project found from ${binding.searchedFrom}. Run \`forgeax-game init --game <slug>\` in the workspace first, or pass \`game\`/\`target_dir\`.`);
|
|
1927
|
+
}
|
|
1928
|
+
const slug = explicitGame?.trim() || activeGame(binding.root);
|
|
1929
|
+
if (!slug) {
|
|
1930
|
+
const games = listGames(binding.root);
|
|
1931
|
+
throw new Error(`No active game to save the asset into.${games.length ? ` Pass one of: ${games.join(", ")}` : " Create one with `forgeax-game init --game <slug>`."}`);
|
|
1932
|
+
}
|
|
1933
|
+
const dir = gameDir(binding.root, slug);
|
|
1934
|
+
if (!dir)
|
|
1935
|
+
throw new Error(`Game ${JSON.stringify(slug)} not found in this project.`);
|
|
1936
|
+
const assets = join7(dir, "assets");
|
|
1937
|
+
mkdirSync7(assets, { recursive: true });
|
|
1938
|
+
return { dir: assets, root: binding.root, slug };
|
|
1939
|
+
}
|
|
1940
|
+
function safeStem(preferred, fallback) {
|
|
1941
|
+
const source = (preferred ?? fallback).toLowerCase();
|
|
1942
|
+
const stem = source.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
|
|
1943
|
+
return stem || "asset";
|
|
1944
|
+
}
|
|
1945
|
+
function uniquePath(dir, stem, ext) {
|
|
1946
|
+
let candidate = join7(dir, `${stem}.${ext}`);
|
|
1947
|
+
for (let i = 1;existsSync4(candidate); i += 1)
|
|
1948
|
+
candidate = join7(dir, `${stem}-${i}.${ext}`);
|
|
1949
|
+
return candidate;
|
|
1950
|
+
}
|
|
1951
|
+
function logProgress(label) {
|
|
1952
|
+
let last = -1;
|
|
1953
|
+
return (pct) => {
|
|
1954
|
+
const step = Math.floor(pct / 10);
|
|
1955
|
+
if (step !== last) {
|
|
1956
|
+
last = step;
|
|
1957
|
+
process.stderr.write(`[forgeax] ${label}: ${pct}%
|
|
1958
|
+
`);
|
|
1959
|
+
}
|
|
1960
|
+
};
|
|
1961
|
+
}
|
|
1962
|
+
var GAME_PROPERTY = {
|
|
1963
|
+
game: {
|
|
1964
|
+
type: "string",
|
|
1965
|
+
description: "Game slug to save the asset into. Defaults to the active game."
|
|
1966
|
+
},
|
|
1967
|
+
target_dir: {
|
|
1968
|
+
type: "string",
|
|
1969
|
+
description: "Directory to resolve the ForgeaX project from. Defaults to the server working directory."
|
|
1970
|
+
},
|
|
1971
|
+
name: {
|
|
1972
|
+
type: "string",
|
|
1973
|
+
description: "Base file name for the saved asset (without extension). Defaults to a slug of the prompt."
|
|
1974
|
+
}
|
|
1975
|
+
};
|
|
1976
|
+
var GENERATE_IMAGE_SCHEMA = {
|
|
1977
|
+
type: "object",
|
|
1978
|
+
properties: {
|
|
1979
|
+
prompt: { type: "string", description: "What to draw. Required for both text-to-image and editing an input image." },
|
|
1980
|
+
image: {
|
|
1981
|
+
type: "string",
|
|
1982
|
+
description: "Optional local image path to edit (image-to-image). When set, the prompt describes the desired change."
|
|
1983
|
+
},
|
|
1984
|
+
model: { type: "string", description: "Override the image model. Defaults to the configured text-to-image model." },
|
|
1985
|
+
...GAME_PROPERTY
|
|
1986
|
+
},
|
|
1987
|
+
required: ["prompt"],
|
|
1988
|
+
additionalProperties: false
|
|
1989
|
+
};
|
|
1990
|
+
var GENERATE_3D_SCHEMA = {
|
|
1991
|
+
type: "object",
|
|
1992
|
+
properties: {
|
|
1993
|
+
prompt: { type: "string", description: "Text description for text-to-3D. Provide this or `image`." },
|
|
1994
|
+
image: {
|
|
1995
|
+
type: "string",
|
|
1996
|
+
description: "Image for image-to-3D: a public https URL, or a local file path when COS is configured (FORGEAX_COS_*) — local files are uploaded to COS and passed as a short-lived presigned URL. Without COS, only a public URL works."
|
|
1997
|
+
},
|
|
1998
|
+
model: { type: "string", description: "Override the 3D model. Defaults to the configured text/image-to-3D model." },
|
|
1999
|
+
...GAME_PROPERTY
|
|
2000
|
+
},
|
|
2001
|
+
additionalProperties: false
|
|
2002
|
+
};
|
|
2003
|
+
var HTTP_URL_RE = /^https?:\/\//i;
|
|
2004
|
+
async function generateImageTool(args, cwd) {
|
|
2005
|
+
const prompt = typeof args.prompt === "string" ? args.prompt.trim() : "";
|
|
2006
|
+
if (!prompt)
|
|
2007
|
+
throw new Error("`prompt` is required.");
|
|
2008
|
+
const cfg = resolveLiteLlmConfig();
|
|
2009
|
+
const targetDir = typeof args.target_dir === "string" ? args.target_dir : cwd;
|
|
2010
|
+
const { dir, root, slug } = assetsDirFor(targetDir, typeof args.game === "string" ? args.game : undefined);
|
|
2011
|
+
const model = typeof args.model === "string" && args.model.trim() ? args.model.trim() : cfg.models.textToImage;
|
|
2012
|
+
let result;
|
|
2013
|
+
let mode;
|
|
2014
|
+
const inputImage = typeof args.image === "string" ? args.image.trim() : "";
|
|
2015
|
+
if (inputImage) {
|
|
2016
|
+
if (HTTP_URL_RE.test(inputImage)) {
|
|
2017
|
+
throw new Error("Image-to-image expects a LOCAL image path, not a URL. Download it first, then pass the path.");
|
|
2018
|
+
}
|
|
2019
|
+
if (!existsSync4(inputImage))
|
|
2020
|
+
throw new Error(`Input image not found: ${inputImage}`);
|
|
2021
|
+
const bytes = new Uint8Array(readFileSync6(inputImage));
|
|
2022
|
+
result = await editImage(cfg, { model, prompt, image: bytes, filename: basename(inputImage) });
|
|
2023
|
+
mode = "image-to-image";
|
|
2024
|
+
} else {
|
|
2025
|
+
result = await generateImage(cfg, { model, prompt });
|
|
2026
|
+
mode = "text-to-image";
|
|
2027
|
+
}
|
|
2028
|
+
const stem = safeStem(typeof args.name === "string" ? args.name : undefined, prompt);
|
|
2029
|
+
const outPath = uniquePath(dir, stem, result.ext);
|
|
2030
|
+
writeFileSync4(outPath, result.bytes);
|
|
2031
|
+
const rel = relative3(root, outPath);
|
|
2032
|
+
return `Saved ${mode} asset to \`${rel}\` (game: ${slug}, model: ${model}, ${result.bytes.length} bytes). Reference it from game code by this path.`;
|
|
2033
|
+
}
|
|
2034
|
+
function imageContentType(path) {
|
|
2035
|
+
const ext = extname(path).toLowerCase();
|
|
2036
|
+
if (ext === ".jpg" || ext === ".jpeg")
|
|
2037
|
+
return "image/jpeg";
|
|
2038
|
+
if (ext === ".webp")
|
|
2039
|
+
return "image/webp";
|
|
2040
|
+
return "image/png";
|
|
2041
|
+
}
|
|
2042
|
+
async function resolveImageUrlFor3d(image, slug) {
|
|
2043
|
+
if (HTTP_URL_RE.test(image))
|
|
2044
|
+
return image;
|
|
2045
|
+
if (!existsSync4(image))
|
|
2046
|
+
throw new Error(`Input image not found: ${image}`);
|
|
2047
|
+
const cos = resolveCosConfig();
|
|
2048
|
+
if (!cos) {
|
|
2049
|
+
throw new Error("Image-to-3D from a local file needs COS configured (FORGEAX_COS_BUCKET/REGION/SECRET_ID/SECRET_KEY) so the image can be hosted for the backend to fetch. Alternatively pass a public https URL.");
|
|
2050
|
+
}
|
|
2051
|
+
const bytes = new Uint8Array(readFileSync6(image));
|
|
2052
|
+
const stem = safeStem(basename(image, extname(image)), "input");
|
|
2053
|
+
const ext = (extname(image).replace(".", "") || "png").toLowerCase();
|
|
2054
|
+
const key = `forgeax/${slug}/${stem}-${Date.now()}.${ext}`;
|
|
2055
|
+
return uploadAndPresign(cos, key, bytes, imageContentType(image));
|
|
2056
|
+
}
|
|
2057
|
+
async function generate3dTool(args, cwd) {
|
|
2058
|
+
const prompt = typeof args.prompt === "string" ? args.prompt.trim() : "";
|
|
2059
|
+
const image = typeof args.image === "string" ? args.image.trim() : "";
|
|
2060
|
+
if (!prompt && !image)
|
|
2061
|
+
throw new Error("Provide `prompt` (text-to-3D) or `image` (image-to-3D).");
|
|
2062
|
+
const cfg = resolveLiteLlmConfig();
|
|
2063
|
+
const targetDir = typeof args.target_dir === "string" ? args.target_dir : cwd;
|
|
2064
|
+
const { dir, root, slug } = assetsDirFor(targetDir, typeof args.game === "string" ? args.game : undefined);
|
|
2065
|
+
let result;
|
|
2066
|
+
let mode;
|
|
2067
|
+
if (image) {
|
|
2068
|
+
const imageUrl = await resolveImageUrlFor3d(image, slug);
|
|
2069
|
+
const model = typeof args.model === "string" && args.model.trim() ? args.model.trim() : cfg.models.imageTo3d;
|
|
2070
|
+
result = await generate3dFromImageUrl(cfg, { model, imageUrl, prompt: prompt || undefined, onProgress: logProgress("image-to-3D") });
|
|
2071
|
+
mode = "image-to-3D";
|
|
2072
|
+
} else {
|
|
2073
|
+
const model = typeof args.model === "string" && args.model.trim() ? args.model.trim() : cfg.models.textTo3d;
|
|
2074
|
+
result = await generate3dFromText(cfg, { model, prompt, onProgress: logProgress("text-to-3D") });
|
|
2075
|
+
mode = "text-to-3D";
|
|
2076
|
+
}
|
|
2077
|
+
const stem = safeStem(typeof args.name === "string" ? args.name : undefined, prompt || "model");
|
|
2078
|
+
const outPath = uniquePath(dir, stem, result.ext);
|
|
2079
|
+
writeFileSync4(outPath, result.bytes);
|
|
2080
|
+
const rel = relative3(root, outPath);
|
|
2081
|
+
return `Saved ${mode} ${result.assetType} to \`${rel}\` (game: ${slug}, ${result.bytes.length} bytes). Reference it from game code by this path.`;
|
|
2082
|
+
}
|
|
2083
|
+
|
|
1655
2084
|
// src/mcp/forgeax-server.ts
|
|
1656
2085
|
function packageVersion() {
|
|
1657
|
-
for (const
|
|
2086
|
+
for (const relative4 of ["../package.json", "../../package.json"]) {
|
|
1658
2087
|
try {
|
|
1659
|
-
const version = JSON.parse(
|
|
2088
|
+
const version = JSON.parse(readFileSync7(new URL(relative4, import.meta.url), "utf8")).version;
|
|
1660
2089
|
if (version)
|
|
1661
2090
|
return version;
|
|
1662
2091
|
} catch {}
|
|
@@ -1698,18 +2127,30 @@ function createForgeaxMcpServer() {
|
|
|
1698
2127
|
description: 'Build, preview, reload, or verify the active game. One call covers what the user means by "run it", "let me see it", "reload", or "does it work": it installs the selected Runtime when needed, builds or reuses a static preview, returns a preview URL to open, and reports the Runtime log file. Read an available `runtime_logs.local_file` with your own file tool — log tailing is intentionally not a tool. Call this after a requested game change, not for ordinary edits the user has not asked to see.',
|
|
1699
2128
|
inputSchema: RUN_TOOL_SCHEMA,
|
|
1700
2129
|
run: async (args, ctx) => runCurrentGame(args, ctx.cwd)
|
|
2130
|
+
},
|
|
2131
|
+
{
|
|
2132
|
+
name: "forgeax_generate_image",
|
|
2133
|
+
description: "Generate a game image asset from a text prompt (text-to-image), or edit a local image when `image` is set (image-to-image). Saves the PNG/JPG into the active game's `assets/` directory and returns its project-relative path to reference from game code. Backed by the ForgeaX LiteLLM gateway; requires FORGEAX_LITELLM_API_KEY. Use when the user asks for a sprite, texture, icon, background, or concept art.",
|
|
2134
|
+
inputSchema: GENERATE_IMAGE_SCHEMA,
|
|
2135
|
+
run: async (args, ctx) => generateImageTool(args, ctx.cwd)
|
|
2136
|
+
},
|
|
2137
|
+
{
|
|
2138
|
+
name: "forgeax_generate_3d",
|
|
2139
|
+
description: "Generate a 3D model (.glb) for the game. Provide `prompt` for text-to-3D, or `image` for image-to-3D — a public https URL, or a local file path when COS is configured (the file is uploaded and passed as a short-lived presigned URL). Runs the async generation to completion (~1–2 min) and saves the mesh into the active game's `assets/` directory, returning its project-relative path. Backed by the ForgeaX LiteLLM gateway; requires FORGEAX_LITELLM_API_KEY (and FORGEAX_COS_* for local-file image-to-3D).",
|
|
2140
|
+
inputSchema: GENERATE_3D_SCHEMA,
|
|
2141
|
+
run: async (args, ctx) => generate3dTool(args, ctx.cwd)
|
|
1701
2142
|
}
|
|
1702
2143
|
]
|
|
1703
2144
|
};
|
|
1704
2145
|
}
|
|
1705
2146
|
|
|
1706
2147
|
// src/cli/dispatch.ts
|
|
1707
|
-
import { existsSync as
|
|
1708
|
-
import { basename, join as
|
|
2148
|
+
import { existsSync as existsSync6, readFileSync as readFileSync9, rmSync as rmSync2, writeFileSync as writeFileSync6 } from "node:fs";
|
|
2149
|
+
import { basename as basename2, join as join9 } from "node:path";
|
|
1709
2150
|
|
|
1710
2151
|
// src/install/clients.ts
|
|
1711
2152
|
import { homedir as homedir3 } from "node:os";
|
|
1712
|
-
import { join as
|
|
2153
|
+
import { join as join8, resolve as resolve4 } from "node:path";
|
|
1713
2154
|
var HOME = homedir3();
|
|
1714
2155
|
var CLIENTS = [
|
|
1715
2156
|
{
|
|
@@ -1717,7 +2158,7 @@ var CLIENTS = [
|
|
|
1717
2158
|
label: "Codex CLI",
|
|
1718
2159
|
format: "toml",
|
|
1719
2160
|
scope: "user",
|
|
1720
|
-
path: () =>
|
|
2161
|
+
path: () => join8(HOME, ".codex", "config.toml"),
|
|
1721
2162
|
commandShape: "split",
|
|
1722
2163
|
postInstallNote: "Restart Codex, then run /mcp to confirm the server is connected."
|
|
1723
2164
|
},
|
|
@@ -1726,7 +2167,7 @@ var CLIENTS = [
|
|
|
1726
2167
|
label: "Claude Code",
|
|
1727
2168
|
format: "json",
|
|
1728
2169
|
scope: "user",
|
|
1729
|
-
path: () =>
|
|
2170
|
+
path: () => join8(HOME, ".claude.json"),
|
|
1730
2171
|
serverMapKey: ["mcpServers"],
|
|
1731
2172
|
commandShape: "split",
|
|
1732
2173
|
postInstallNote: "Restart Claude Code, then run /mcp to confirm the server is connected."
|
|
@@ -1736,7 +2177,7 @@ var CLIENTS = [
|
|
|
1736
2177
|
label: "Cursor",
|
|
1737
2178
|
format: "json",
|
|
1738
2179
|
scope: "user",
|
|
1739
|
-
path: () =>
|
|
2180
|
+
path: () => join8(HOME, ".cursor", "mcp.json"),
|
|
1740
2181
|
serverMapKey: ["mcpServers"],
|
|
1741
2182
|
commandShape: "split",
|
|
1742
2183
|
postInstallNote: "Reload Cursor, then check Settings > MCP."
|
|
@@ -1746,7 +2187,7 @@ var CLIENTS = [
|
|
|
1746
2187
|
label: "Trae (project)",
|
|
1747
2188
|
format: "json",
|
|
1748
2189
|
scope: "project",
|
|
1749
|
-
path: (projectRoot) =>
|
|
2190
|
+
path: (projectRoot) => join8(projectRoot, ".trae", "mcp.json"),
|
|
1750
2191
|
serverMapKey: ["mcpServers"],
|
|
1751
2192
|
commandShape: "split",
|
|
1752
2193
|
postInstallNote: "Reload Trae, then check the project MCP server list."
|
|
@@ -1757,7 +2198,7 @@ var CLIENTS = [
|
|
|
1757
2198
|
label: "CodeBuddy / WorkBuddy",
|
|
1758
2199
|
format: "json",
|
|
1759
2200
|
scope: "user",
|
|
1760
|
-
path: () =>
|
|
2201
|
+
path: () => join8(HOME, ".codebuddy", ".mcp.json"),
|
|
1761
2202
|
serverMapKey: ["mcpServers"],
|
|
1762
2203
|
commandShape: "split",
|
|
1763
2204
|
postInstallNote: "Restart CodeBuddy or WorkBuddy, then run /mcp to confirm the server is connected."
|
|
@@ -1767,7 +2208,7 @@ var CLIENTS = [
|
|
|
1767
2208
|
label: "Windsurf",
|
|
1768
2209
|
format: "json",
|
|
1769
2210
|
scope: "user",
|
|
1770
|
-
path: () =>
|
|
2211
|
+
path: () => join8(HOME, ".codeium", "windsurf", "mcp_config.json"),
|
|
1771
2212
|
serverMapKey: ["mcpServers"],
|
|
1772
2213
|
commandShape: "split",
|
|
1773
2214
|
postInstallNote: "Reload Windsurf to pick up the new server."
|
|
@@ -1777,7 +2218,7 @@ var CLIENTS = [
|
|
|
1777
2218
|
label: "VS Code (workspace)",
|
|
1778
2219
|
format: "json",
|
|
1779
2220
|
scope: "project",
|
|
1780
|
-
path: (projectRoot) =>
|
|
2221
|
+
path: (projectRoot) => join8(projectRoot, ".vscode", "mcp.json"),
|
|
1781
2222
|
serverMapKey: ["servers"],
|
|
1782
2223
|
commandShape: "split",
|
|
1783
2224
|
postInstallNote: 'Open .vscode/mcp.json and click Start, or run "MCP: List Servers".'
|
|
@@ -1787,7 +2228,7 @@ var CLIENTS = [
|
|
|
1787
2228
|
label: "ZCode",
|
|
1788
2229
|
format: "json",
|
|
1789
2230
|
scope: "user",
|
|
1790
|
-
path: () =>
|
|
2231
|
+
path: () => join8(HOME, ".zcode", "cli", "config.json"),
|
|
1791
2232
|
serverMapKey: ["mcp", "servers"],
|
|
1792
2233
|
commandShape: "split",
|
|
1793
2234
|
postInstallNote: "Start a new ZCode session, then run /mcp status to confirm the server is connected."
|
|
@@ -1797,7 +2238,7 @@ var CLIENTS = [
|
|
|
1797
2238
|
label: "OpenCode",
|
|
1798
2239
|
format: "json",
|
|
1799
2240
|
scope: "user",
|
|
1800
|
-
path: () =>
|
|
2241
|
+
path: () => join8(HOME, ".config", "opencode", "opencode.json"),
|
|
1801
2242
|
serverMapKey: ["mcp"],
|
|
1802
2243
|
commandShape: "argv",
|
|
1803
2244
|
extraEntryFields: { type: "local", enabled: true },
|
|
@@ -1821,7 +2262,7 @@ function launchSpec(mode) {
|
|
|
1821
2262
|
}
|
|
1822
2263
|
|
|
1823
2264
|
// src/install/write-config.ts
|
|
1824
|
-
import { copyFileSync as copyFileSync2, existsSync as
|
|
2265
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync5, mkdirSync as mkdirSync8, readFileSync as readFileSync8, writeFileSync as writeFileSync5 } from "node:fs";
|
|
1825
2266
|
import { dirname as dirname4 } from "node:path";
|
|
1826
2267
|
|
|
1827
2268
|
// src/install/toml-section.ts
|
|
@@ -2031,11 +2472,11 @@ function mergeTomlConfig(existing, entry) {
|
|
|
2031
2472
|
}
|
|
2032
2473
|
function inspectConfig(spec, projectRoot, launch) {
|
|
2033
2474
|
const path = spec.path(projectRoot);
|
|
2034
|
-
if (!
|
|
2475
|
+
if (!existsSync5(path))
|
|
2035
2476
|
return { path, state: "missing" };
|
|
2036
2477
|
let existing;
|
|
2037
2478
|
try {
|
|
2038
|
-
existing =
|
|
2479
|
+
existing = readFileSync8(path, "utf8");
|
|
2039
2480
|
if (spec.format === "toml") {
|
|
2040
2481
|
const header = `mcp_servers.${SERVER_KEY}`;
|
|
2041
2482
|
if (!hasTomlTable(existing, header)) {
|
|
@@ -2080,25 +2521,25 @@ function inspectConfig(spec, projectRoot, launch) {
|
|
|
2080
2521
|
}
|
|
2081
2522
|
function applyConfig(spec, projectRoot, launch) {
|
|
2082
2523
|
const path = spec.path(projectRoot);
|
|
2083
|
-
const existing =
|
|
2524
|
+
const existing = existsSync5(path) ? readFileSync8(path, "utf8") : undefined;
|
|
2084
2525
|
const entry = buildEntry(spec, launch);
|
|
2085
2526
|
const merged = spec.format === "toml" ? mergeTomlConfig(existing, entry) : mergeJsonConfig(existing, spec, entry);
|
|
2086
2527
|
if (!merged.changed)
|
|
2087
2528
|
return { path, changed: false };
|
|
2088
|
-
|
|
2529
|
+
mkdirSync8(dirname4(path), { recursive: true });
|
|
2089
2530
|
let backup;
|
|
2090
2531
|
if (existing !== undefined) {
|
|
2091
2532
|
backup = `${path}.bak.latest`;
|
|
2092
2533
|
copyFileSync2(path, backup);
|
|
2093
2534
|
}
|
|
2094
|
-
|
|
2535
|
+
writeFileSync5(path, merged.content);
|
|
2095
2536
|
return { path, changed: true, ...backup ? { backup } : {} };
|
|
2096
2537
|
}
|
|
2097
2538
|
function removeConfig(spec, projectRoot) {
|
|
2098
2539
|
const path = spec.path(projectRoot);
|
|
2099
|
-
if (!
|
|
2540
|
+
if (!existsSync5(path))
|
|
2100
2541
|
return { path, changed: false };
|
|
2101
|
-
const existing =
|
|
2542
|
+
const existing = readFileSync8(path, "utf8");
|
|
2102
2543
|
let content;
|
|
2103
2544
|
if (spec.format === "toml") {
|
|
2104
2545
|
content = removeTomlTable(existing, `mcp_servers.${SERVER_KEY}`);
|
|
@@ -2126,7 +2567,7 @@ function removeConfig(spec, projectRoot) {
|
|
|
2126
2567
|
return { path, changed: false };
|
|
2127
2568
|
const backup = `${path}.bak.latest`;
|
|
2128
2569
|
copyFileSync2(path, backup);
|
|
2129
|
-
|
|
2570
|
+
writeFileSync5(path, content);
|
|
2130
2571
|
return { path, changed: true, backup };
|
|
2131
2572
|
}
|
|
2132
2573
|
|
|
@@ -2330,23 +2771,23 @@ function requireProject() {
|
|
|
2330
2771
|
return project.root;
|
|
2331
2772
|
}
|
|
2332
2773
|
function updateAgentsFile(root) {
|
|
2333
|
-
const path =
|
|
2334
|
-
const existing =
|
|
2774
|
+
const path = join9(root, "AGENTS.md");
|
|
2775
|
+
const existing = existsSync6(path) ? readFileSync9(path, "utf8") : undefined;
|
|
2335
2776
|
const content = upsertBlock(existing, ROUTING_TEXT);
|
|
2336
2777
|
if (content === existing)
|
|
2337
2778
|
return { path, changed: false };
|
|
2338
|
-
|
|
2779
|
+
writeFileSync6(path, content);
|
|
2339
2780
|
return { path, changed: true };
|
|
2340
2781
|
}
|
|
2341
2782
|
function removeAgentsBlock(root) {
|
|
2342
|
-
const path =
|
|
2343
|
-
if (!
|
|
2783
|
+
const path = join9(root, "AGENTS.md");
|
|
2784
|
+
if (!existsSync6(path))
|
|
2344
2785
|
return { path, changed: false };
|
|
2345
|
-
const existing =
|
|
2786
|
+
const existing = readFileSync9(path, "utf8");
|
|
2346
2787
|
const content = removeBlock(existing);
|
|
2347
2788
|
if (content === existing)
|
|
2348
2789
|
return { path, changed: false };
|
|
2349
|
-
|
|
2790
|
+
writeFileSync6(path, content);
|
|
2350
2791
|
return { path, changed: true };
|
|
2351
2792
|
}
|
|
2352
2793
|
async function apiPost(path, body) {
|
|
@@ -2425,7 +2866,7 @@ async function installCommand(args) {
|
|
|
2425
2866
|
return failures === 0 ? 0 : 1;
|
|
2426
2867
|
}
|
|
2427
2868
|
function defaultSlug(root) {
|
|
2428
|
-
const raw =
|
|
2869
|
+
const raw = basename2(root).toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
2429
2870
|
return SLUG_RE.test(raw) ? raw : "my-game";
|
|
2430
2871
|
}
|
|
2431
2872
|
var INIT_USAGE = "usage: forgeax-game init [--game <slug>] [--ide codex,claude,cursor,...]";
|
|
@@ -2581,7 +3022,7 @@ async function uninstallCommand(args) {
|
|
|
2581
3022
|
const agents = removeAgentsBlock(root);
|
|
2582
3023
|
process.stdout.write(`${agents.changed ? "REMOVED" : "ABSENT "} routing block: ${agents.path}
|
|
2583
3024
|
`);
|
|
2584
|
-
process.stdout.write(`KEPT your games and project metadata: ${
|
|
3025
|
+
process.stdout.write(`KEPT your games and project metadata: ${join9(root, ".forgeax")}
|
|
2585
3026
|
`);
|
|
2586
3027
|
} else {
|
|
2587
3028
|
process.stdout.write(`INFO no ForgeaX project bound; only client configuration was touched.
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
+
"packageManager": "bun@1.3.14",
|
|
2
3
|
"name": "@forgeax/game",
|
|
3
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.4",
|
|
4
5
|
"private": false,
|
|
5
6
|
"type": "module",
|
|
6
7
|
"description": "@forgeax/game \u2014 ForgeaX game development as a plugin for MCP-capable agent CLIs (Codex, Claude Code, Cursor, Trae, ZCode, OpenCode, WorkBuddy). Single binary, dual mode: no args = stdio MCP server, subcommand = CLI.",
|
|
@@ -41,7 +42,7 @@
|
|
|
41
42
|
],
|
|
42
43
|
"license": "MIT",
|
|
43
44
|
"dependencies": {
|
|
44
|
-
"@forgeax/game-runtime": "0.3.
|
|
45
|
+
"@forgeax/game-runtime": "0.3.33"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|
|
47
48
|
"@types/bun": "^1.2.0",
|