@akira-tl/forgerelay 0.5.6 → 0.6.0
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/CHANGELOG.md +43 -0
- package/README.md +7 -3
- package/capabilities/host-integration/GUIDE.md +15 -9
- package/dist/activity/host-turn-store.js +9 -7
- package/dist/activity/lifecycle.js +1 -1
- package/dist/activity/mcp-query-tools.js +110 -18
- package/dist/activity/query-service.js +4 -4
- package/dist/activity/ui/contract.js +2 -0
- package/dist/cli.js +47 -32
- package/dist/config.js +31 -3
- package/dist/db/migrations.js +30 -0
- package/dist/db/schema.js +2 -0
- package/dist/logger.js +6 -5
- package/dist/lsp/test-support/server-fixture.js +1 -1
- package/dist/mcp/server-instructions.js +7 -6
- package/dist/mcp-app-template.js +108 -12
- package/dist/oauth/public-url.js +14 -0
- package/dist/oauth/router.js +49 -0
- package/dist/process-sessions.js +2 -1
- package/dist/request-meta.js +4 -0
- package/dist/server.js +250 -84
- package/dist/skills.js +58 -1
- package/dist/ui/.vite/manifest.json +360 -312
- package/dist/ui/activity-panel-app.html +17 -0
- package/dist/ui/assets/activity-panel-app-CjZVvVNc.js +2 -0
- package/dist/ui/assets/angular-html-BkgDQl-G.js +1 -0
- package/dist/ui/assets/{angular-ts-BwPE8YDt.js → angular-ts-Bxh4-O1-.js} +1 -1
- package/dist/ui/assets/{apl-CGbONVvU.js → apl-BmYHKQ6m.js} +1 -1
- package/dist/ui/assets/{astro-DIAPy7cj.js → astro-C51vqDKq.js} +1 -1
- package/dist/ui/assets/{blade-Cgtd19Ky.js → blade-CdhVsmwq.js} +1 -1
- package/dist/ui/assets/c-IBzBpxAO.js +1 -0
- package/dist/ui/assets/chunk-EyZ2wyi3.js +1 -0
- package/dist/ui/assets/{cobol-G-7HLVKW.js → cobol-Cf6azoN_.js} +1 -1
- package/dist/ui/assets/{coffee-DQFyKxEB.js → coffee-vVdGzlmt.js} +1 -1
- package/dist/ui/assets/cpp-CVHeO-Jk.js +1 -0
- package/dist/ui/assets/{crystal-DuiWhEO3.js → crystal-5UZy6KIX.js} +1 -1
- package/dist/ui/assets/css-DEfus9J5.js +1 -0
- package/dist/ui/assets/{edge-COkalhFi.js → edge-CESgo4ia.js} +1 -1
- package/dist/ui/assets/{elixir-cdlzbYR1.js → elixir-9FKfw4cJ.js} +1 -1
- package/dist/ui/assets/{elm-ZwDYuYpb.js → elm-Dv7A0aLa.js} +1 -1
- package/dist/ui/assets/{erb-C8WG9TtC.js → erb-DHFmDvM0.js} +1 -1
- package/dist/ui/assets/{git-rebase-CMlDVJBT.js → git-rebase-utbU6CzV.js} +1 -1
- package/dist/ui/assets/{glimmer-js-DeLOQlol.js → glimmer-js-t2kiCkyB.js} +1 -1
- package/dist/ui/assets/{glimmer-ts-DTEM04TK.js → glimmer-ts-F0Jv5okV.js} +1 -1
- package/dist/ui/assets/glsl-_ooCjaeB.js +1 -0
- package/dist/ui/assets/graphql-B9LqP-cY.js +1 -0
- package/dist/ui/assets/{hack-C14sh6Lh.js → hack-8yjdbaSA.js} +1 -1
- package/dist/ui/assets/haml-BU5EoZBn.js +1 -0
- package/dist/ui/assets/{handlebars-eS-J0O-M.js → handlebars-BBFEg4Lo.js} +1 -1
- package/dist/ui/assets/{heavy-payload-BKB0QDOI.js → heavy-payload-vGgBRvNX.js} +1 -1
- package/dist/ui/assets/html-D-C6YZk4.js +1 -0
- package/dist/ui/assets/{html-derivative-BlVZU2id.js → html-derivative-DJAQcZyc.js} +1 -1
- package/dist/ui/assets/{http-BmbZzA5x.js → http-Dg5LSYRK.js} +1 -1
- package/dist/ui/assets/{hurl-Dnd_YpOD.js → hurl-Y5uYzMZR.js} +1 -1
- package/dist/ui/assets/java-ppHvycj5.js +1 -0
- package/dist/ui/assets/javascript-DDTjKLlQ.js +1 -0
- package/dist/ui/assets/{jinja-DTvkZdHA.js → jinja-Bt30h8Ik.js} +1 -1
- package/dist/ui/assets/{jison-DUJG-tmh.js → jison-D5uZ1Nyo.js} +1 -1
- package/dist/ui/assets/json-Cw91r8h1.js +1 -0
- package/dist/ui/assets/jsx-04X2FT_1.js +1 -0
- package/dist/ui/assets/{julia-P5B4pHFa.js → julia-76hRaaGz.js} +1 -1
- package/dist/ui/assets/{just-DYaoh7C6.js → just-CDE7W0vW.js} +1 -1
- package/dist/ui/assets/{latex-CI5lnkWV.js → latex-CVO2Byab.js} +1 -1
- package/dist/ui/assets/{liquid-D3LnAs_z.js → liquid-CA-AS3EX.js} +1 -1
- package/dist/ui/assets/lua-CiyBp52Y.js +1 -0
- package/dist/ui/assets/{marko-CRDrjqfS.js → marko-_BWrPfny.js} +1 -1
- package/dist/ui/assets/{mdc-ZzwgmJVn.js → mdc-PyJv85eS.js} +1 -1
- package/dist/ui/assets/{nginx-BAvFXGSV.js → nginx-DAWVw8yu.js} +1 -1
- package/dist/ui/assets/{nim-BHIFawe9.js → nim-CKE1dzpU.js} +1 -1
- package/dist/ui/assets/{perl-C_f5AnLa.js → perl-BBhWva19.js} +1 -1
- package/dist/ui/assets/{php-DDqpegL5.js → php-ChIeaAoF.js} +1 -1
- package/dist/ui/assets/{pug-CSaAZXyd.js → pug-CMgI5afi.js} +1 -1
- package/dist/ui/assets/{qml-CNHvRs_z.js → qml-CjxTkcFH.js} +1 -1
- package/dist/ui/assets/r-DfFga4si.js +1 -0
- package/dist/ui/assets/{razor-BDfAPy7I.js → razor-i4DbHr8p.js} +1 -1
- package/dist/ui/assets/regexp-iC9_nbSI.js +1 -0
- package/dist/ui/assets/{review-payload-D7GIdYDj.js → review-payload-4erWKckt.js} +1 -1
- package/dist/ui/assets/{rst-BzPP2gle.js → rst-oy_97Kl4.js} +1 -1
- package/dist/ui/assets/{ruby-CQjzokzX.js → ruby-C5zYZIyl.js} +1 -1
- package/dist/ui/assets/{sas-Bp37KGr7.js → sas-BnMEh2S3.js} +1 -1
- package/dist/ui/assets/{scrollbar-HUMBdLLK.js → scrollbar-CaOPzUJd.js} +6 -6
- package/dist/ui/assets/scss-BfZXsAWQ.js +1 -0
- package/dist/ui/assets/shellscript-BC2srNaB.js +1 -0
- package/dist/ui/assets/{shellsession-CNc62UhY.js → shellsession-BpE5o7_3.js} +1 -1
- package/dist/ui/assets/{soy-CF44SpBo.js → soy-COesmhTY.js} +1 -1
- package/dist/ui/assets/sql-Ccv_yeUm.js +1 -0
- package/dist/ui/assets/{stata-DVeMA68W.js → stata-CGQEuSZq.js} +1 -1
- package/dist/ui/assets/{surrealql-52sF96Yv.js → surrealql-Bz4JbU-9.js} +1 -1
- package/dist/ui/assets/{svelte-ydwT7UbF.js → svelte-1jM2ejXj.js} +1 -1
- package/dist/ui/assets/{templ-BXcoVoGh.js → templ-C-VwELcp.js} +1 -1
- package/dist/ui/assets/{tex-COsYFp7O.js → tex-t1rUiQCm.js} +1 -1
- package/dist/ui/assets/{ts-tags-CF--LE1s.js → ts-tags-Bewoa6pS.js} +1 -1
- package/dist/ui/assets/tsx-CTXzrBkN.js +1 -0
- package/dist/ui/assets/{twig-YoM-DiI5.js → twig-DVg-vELl.js} +1 -1
- package/dist/ui/assets/typescript--ElHoFkw.js +1 -0
- package/dist/ui/assets/{vue-html-CPdGGk4T.js → vue-html-BHtJ8geX.js} +1 -1
- package/dist/ui/assets/{vue-vine-B5bjgk0a.js → vue-vine-CNSGAWwc.js} +1 -1
- package/dist/ui/assets/{vue-B5LDEy4l.js → vue-wPQ7UlMy.js} +1 -1
- package/dist/ui/assets/workspace-app-CcrHAUIn.css +1 -0
- package/dist/ui/assets/workspace-app-DJmkPYJC.js +1 -0
- package/dist/ui/assets/workspace-app-DkAiSl_0.js +111 -0
- package/dist/ui/assets/workspace-app-QyauBrJX.js +5 -0
- package/dist/ui/assets/workspace-lifecycle-app-BIXEo53I.js +1 -0
- package/dist/ui/assets/xml-JnmX6vyS.js +1 -0
- package/dist/ui/assets/{xsl-wls_CEK_.js → xsl-3_vJ27aE.js} +1 -1
- package/dist/ui/assets/yaml-OAcZkP-w.js +1 -0
- package/dist/ui/workspace-app.html +5 -2
- package/dist/ui/workspace-lifecycle-app.html +18 -0
- package/dist/workspaces.js +11 -0
- package/docs/configuration.md +36 -4
- package/docs/debugging.md +75 -17
- package/docs/gotchas.md +15 -9
- package/docs/setup.md +24 -10
- package/package.json +4 -3
- package/scripts/debug/accept.mjs +79 -27
- package/scripts/debug/host/run.sh +180 -0
- package/scripts/debug/host/serve.mjs +64 -0
- package/scripts/debug/runtime.mjs +57 -2
- package/scripts/debug/runtime.test.mjs +74 -0
- package/scripts/debug/serve.mjs +4 -5
- package/scripts/dev-server.mjs +32 -3
- package/scripts/release/release-gate.test.mjs +43 -0
- package/scripts/release/release-version.test.mjs +107 -0
- package/scripts/release-parity.mjs +70 -46
- package/scripts/release-proof.mjs +2 -2
- package/scripts/release-proof.test.mjs +18 -0
- package/scripts/release-version.mjs +17 -8
- package/dist/ui/assets/angular-html-BT2Z6jee.js +0 -1
- package/dist/ui/assets/c-acnvZ8tm.js +0 -1
- package/dist/ui/assets/cpp-UyrmHXan.js +0 -1
- package/dist/ui/assets/css-D_WCbY6m.js +0 -1
- package/dist/ui/assets/glsl-IJV4SB-M.js +0 -1
- package/dist/ui/assets/graphql-PM-WDnoB.js +0 -1
- package/dist/ui/assets/haml-B_5nS6Kx.js +0 -1
- package/dist/ui/assets/html-C3TPjrC5.js +0 -1
- package/dist/ui/assets/java-ne9HdIVn.js +0 -1
- package/dist/ui/assets/javascript-DpHT58rr.js +0 -1
- package/dist/ui/assets/json-DJmnBdxW.js +0 -1
- package/dist/ui/assets/jsx-BclJnZnN.js +0 -1
- package/dist/ui/assets/lua-dZC7ZQIY.js +0 -1
- package/dist/ui/assets/r-CAfoQND2.js +0 -1
- package/dist/ui/assets/regexp-Mug5jltQ.js +0 -1
- package/dist/ui/assets/scss-C_Z1DYnw.js +0 -1
- package/dist/ui/assets/shellscript-CLfJpn3I.js +0 -1
- package/dist/ui/assets/sql-CYnzfiQ1.js +0 -1
- package/dist/ui/assets/tsx-DLBt2-2f.js +0 -1
- package/dist/ui/assets/typescript-Ce9JGkNI.js +0 -1
- package/dist/ui/assets/workspace-app-Cm9EX5x4.js +0 -117
- package/dist/ui/assets/workspace-app-DHvE2ojW.css +0 -1
- package/dist/ui/assets/xml-Cun8xb0T.js +0 -1
- package/dist/ui/assets/yaml-BxuXn9OL.js +0 -1
- package/scripts/release/tag-local-ci.mjs +0 -144
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,49 @@ All notable ForgeRelay changes are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.6.0] - 2026-08-27
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Added the unified ForgeRelay Panel for ChatGPT MCP App hosts: one Host Turn now keeps the Workspace Summary above a collapsible Activity Panel, groups live Activities, lazily loads detailed operation data, and streams durable Bash output without turning follow-up process control into duplicate top-level Activities.
|
|
12
|
+
- Added a local MCP Inspector debug host on the dedicated `127.0.0.1:7677` path so the real HTTP/OAuth/MCP App lifecycle can be inspected without touching the normal product port.
|
|
13
|
+
- Added release-candidate publication support: `vX.Y.Z-rc.N` tags run the normal release gate, publish npm packages on the `next` dist-tag, and create GitHub prereleases while stable releases continue to use `latest`.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Promoted the validated `0.6.0-rc.1` build to the stable 0.6 line without additional runtime changes.
|
|
18
|
+
- Skills are now advertised by name and loaded through the virtual `skills://<name>` namespace; real local Skill paths stay out of normal Agent-facing metadata, and files inside an activated Skill use `skills://<name>/<relative-path>`.
|
|
19
|
+
- `publicBaseUrl` may now be either one routed URL or an ordered list of routed public URLs. The first remains canonical, every configured hostname joins the Host allowlist, and the complete ordered list participates in MCP App resource identity and CSP metadata.
|
|
20
|
+
- Host Turn scoping now prefers Host conversation metadata, then the MCP transport session, then the current connection, preserving one Workspace-specific panel even on Hosts such as Inspector that do not provide OpenAI conversation metadata.
|
|
21
|
+
- When an Agent only needs to wait for an already running Bash process, `bash(action="process", processId=..., yieldTimeMs=...)` now acts as one bounded wait and reuses the same process ID if the window expires, reducing repeated short polling.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Preserved routed public base paths through OAuth endpoints and local debug configuration instead of collapsing deployment URLs back to the origin.
|
|
26
|
+
- Preserved complete Workspace IDs in diagnostic logging and kept Activity Panel state attached across successive tool results.
|
|
27
|
+
- Explicit Bash process waits now honor `yieldTimeMs` even when the process has already produced buffered output, so continuously logging commands can still be efficiently awaited to completion.
|
|
28
|
+
|
|
29
|
+
## [0.6.0-rc.1] - 2026-08-27
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- Added the unified ForgeRelay Panel for ChatGPT MCP App hosts: one Host Turn now keeps the Workspace Summary above a collapsible Activity Panel, groups live Activities, lazily loads detailed operation data, and streams durable Bash output without turning follow-up process control into duplicate top-level Activities.
|
|
34
|
+
- Added a local MCP Inspector debug host on the dedicated `127.0.0.1:7677` path so the real HTTP/OAuth/MCP App lifecycle can be inspected without touching the normal product port.
|
|
35
|
+
- Added release-candidate publication support: `vX.Y.Z-rc.N` tags run the normal release gate, publish npm packages on the `next` dist-tag, and create GitHub prereleases while stable releases continue to use `latest`.
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- Skills are now advertised by name and loaded through the virtual `skills://<name>` namespace; real local Skill paths stay out of normal Agent-facing metadata, and files inside an activated Skill use `skills://<name>/<relative-path>`.
|
|
40
|
+
- `publicBaseUrl` may now be either one routed URL or an ordered list of routed public URLs. The first remains canonical, every configured hostname joins the Host allowlist, and the complete ordered list participates in MCP App resource identity and CSP metadata.
|
|
41
|
+
- Host Turn scoping now prefers Host conversation metadata, then the MCP transport session, then the current connection, preserving one Workspace-specific panel even on Hosts such as Inspector that do not provide OpenAI conversation metadata.
|
|
42
|
+
- When an Agent only needs to wait for an already running Bash process, `bash(action="process", processId=..., yieldTimeMs=...)` now acts as one bounded wait and reuses the same process ID if the window expires, reducing repeated short polling.
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- Preserved routed public base paths through OAuth endpoints and local debug configuration instead of collapsing deployment URLs back to the origin.
|
|
47
|
+
- Preserved complete Workspace IDs in diagnostic logging and kept Activity Panel state attached across successive tool results.
|
|
48
|
+
- Explicit Bash process waits now honor `yieldTimeMs` even when the process has already produced buffered output, so continuously logging commands can still be efficiently awaited to completion.
|
|
49
|
+
|
|
7
50
|
## [0.5.6] - 2026-08-17
|
|
8
51
|
|
|
9
52
|
### Fixed
|
package/README.md
CHANGED
|
@@ -55,18 +55,22 @@ http://127.0.0.1:7676/mcp
|
|
|
55
55
|
|
|
56
56
|
If the MCP host cannot reach localhost, put ForgeRelay behind a public HTTPS
|
|
57
57
|
tunnel or reverse proxy such as Cloudflare Tunnel, ngrok, Pinggy, Tailscale
|
|
58
|
-
Funnel, or your own proxy. During setup, enter the public
|
|
58
|
+
Funnel, or your own proxy. During setup, enter the public base URL before the
|
|
59
|
+
final `/mcp`; routed prefixes are allowed:
|
|
59
60
|
|
|
60
61
|
```text
|
|
61
|
-
https://your-tunnel-host.example.com
|
|
62
|
+
https://your-tunnel-host.example.com/forgerelay/main
|
|
62
63
|
```
|
|
63
64
|
|
|
64
65
|
The client then connects to:
|
|
65
66
|
|
|
66
67
|
```text
|
|
67
|
-
https://your-tunnel-host.example.com/mcp
|
|
68
|
+
https://your-tunnel-host.example.com/forgerelay/main/mcp
|
|
68
69
|
```
|
|
69
70
|
|
|
71
|
+
`publicBaseUrl` may also be an ordered list when multiple public entries are
|
|
72
|
+
valid; each entry may use its own route and the first is canonical.
|
|
73
|
+
|
|
70
74
|
ForgeRelay uses an Owner-password OAuth approval flow. `forgerelay init` prints
|
|
71
75
|
the password and stores it in the active config directory. New installations use:
|
|
72
76
|
|
|
@@ -6,14 +6,20 @@
|
|
|
6
6
|
|
|
7
7
|
ForgeRelay 本地监听一个 HTTP origin,MCP endpoint 是 `/mcp`。当 Host 无法直接访问 localhost 时,需要用户自己提供 public HTTPS tunnel/reverse proxy;ForgeRelay 不创建 tunnel。
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
公网部署继续使用 `publicBaseUrl`;它既可以是单个字符串,也可以是多个 URL 的列表。每个 URL 可以带自己的路由前缀,第一项是 canonical:
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
{
|
|
13
|
+
"publicBaseUrl": [
|
|
14
|
+
"https://forge.example.com/forgerelay/main",
|
|
15
|
+
"https://forge-alt.example.com/relay"
|
|
16
|
+
]
|
|
17
|
+
}
|
|
14
18
|
```
|
|
15
19
|
|
|
16
|
-
|
|
20
|
+
对应 canonical Host MCP URL 为 `https://forge.example.com/forgerelay/main/mcp`。全部配置项的 hostname 都会进入派生 Host-header allowlist;MCP、OAuth 与 MCP App asset URL 使用第一项作为 canonical base。单个字符串形式保持完全兼容,不需要迁移。
|
|
21
|
+
|
|
22
|
+
临时覆盖使用 `FORGERELAY_PUBLIC_BASE_URL`;多个 URL 用逗号分隔。Host-header/403 问题先运行 `forgerelay doctor` 检查 resolved public base URLs 与 allowed hosts;`FORGERELAY_ALLOWED_HOSTS="*"` 只适合明确的本地调试。
|
|
17
23
|
|
|
18
24
|
当 ForgeRelay 绑定 loopback、但 `publicBaseUrl` 指向公网 tunnel/reverse proxy 时,ForgeRelay 会自动信任恰好 1 个上游代理 hop,让 Express 与 OAuth rate limiter 使用一致的客户端 IP。不要把 Express `trust proxy` 设为无条件 `true`;直接监听 `0.0.0.0` 等非 loopback 地址时也不会自动开启代理信任。可用 `FORGERELAY_TRUST_PROXY=0|1` 显式覆盖。
|
|
19
25
|
|
|
@@ -47,13 +53,13 @@ npm run build
|
|
|
47
53
|
npm run debug:accept
|
|
48
54
|
```
|
|
49
55
|
|
|
50
|
-
ForgeRelay
|
|
56
|
+
ForgeRelay 对新的项目工作只广告统一 Panel 的 content-hashed App resource:
|
|
51
57
|
|
|
52
58
|
```text
|
|
53
|
-
ui://forgerelay/
|
|
59
|
+
ui://forgerelay/activity-panel-app-<hash>.html
|
|
54
60
|
```
|
|
55
61
|
|
|
56
|
-
`resources/list` 与 `resources/read`
|
|
62
|
+
`activity_panel(workspaceId)` 是唯一的新 render tool;`open_workspace` 只返回 Workspace handle/context,不单独挂 UI。`resources/list` 与 `resources/read` 的当前 App metadata 都应包含 `_meta.ui.domain`,其值使用 canonical `publicBaseUrl` 的 origin;标准 `ui.domain` 本身不塞路由。CSP 的 resource/connect entries 使用完整 `publicBaseUrl` 列表,因此每个入口可以有不同路由。完整、有序的 public base URL 列表与 bundle/template contract 一起进入 MCP App resource identity;修改任一域名或路由都必须生成新的 `ui://` URI,不能让 Host 继续命中旧 metadata cache。`resources/read` 应返回 `text/html;profile=mcp-app`,并且 HTML 引用的 canonical routed `/mcp-app-assets/` 资源必须可达。ForgeRelay 仍保留 legacy `workspace-app` 与 Workspace Lifecycle URI/template 作为历史卡片读取兼容入口,但它们不代表当前产品存在第二个 App。
|
|
57
63
|
|
|
58
64
|
需要 live trace 时,可用:
|
|
59
65
|
|
|
@@ -9,14 +9,15 @@ export class HostTurnStore {
|
|
|
9
9
|
this.now = options.now ?? (() => new Date());
|
|
10
10
|
this.nextTurnId = options.turnId ?? (() => `turn_${randomUUID().replaceAll("-", "")}`);
|
|
11
11
|
}
|
|
12
|
-
begin(conversationScopeId) {
|
|
12
|
+
begin(conversationScopeId, workspaceId) {
|
|
13
13
|
const turnId = this.nextTurnId();
|
|
14
14
|
const createdAt = this.now().toISOString();
|
|
15
|
-
this.database.sqlite.prepare(`insert into activity_host_turns (turn_id, conversation_scope_id, created_at)
|
|
16
|
-
values (?, ?, ?)`).run(turnId, conversationScopeId ?? null, createdAt);
|
|
15
|
+
this.database.sqlite.prepare(`insert into activity_host_turns (turn_id, conversation_scope_id, workspace_id, created_at)
|
|
16
|
+
values (?, ?, ?, ?)`).run(turnId, conversationScopeId ?? null, workspaceId, createdAt);
|
|
17
17
|
return {
|
|
18
18
|
turnId,
|
|
19
19
|
...(conversationScopeId ? { conversationScopeId } : {}),
|
|
20
|
+
workspaceId,
|
|
20
21
|
createdAt,
|
|
21
22
|
};
|
|
22
23
|
}
|
|
@@ -24,13 +25,13 @@ export class HostTurnStore {
|
|
|
24
25
|
const row = this.database.sqlite.prepare("select * from activity_host_turns where turn_id = ?").get(turnId);
|
|
25
26
|
return row ? rowToTurn(row) : undefined;
|
|
26
27
|
}
|
|
27
|
-
current(conversationScopeId) {
|
|
28
|
-
if (!conversationScopeId)
|
|
28
|
+
current(conversationScopeId, workspaceId) {
|
|
29
|
+
if (!conversationScopeId || !workspaceId)
|
|
29
30
|
return undefined;
|
|
30
31
|
const row = this.database.sqlite.prepare(`select * from activity_host_turns
|
|
31
|
-
where conversation_scope_id = ?
|
|
32
|
+
where conversation_scope_id = ? and workspace_id = ?
|
|
32
33
|
order by rowid desc
|
|
33
|
-
limit 1`).get(conversationScopeId);
|
|
34
|
+
limit 1`).get(conversationScopeId, workspaceId);
|
|
34
35
|
return row ? rowToTurn(row) : undefined;
|
|
35
36
|
}
|
|
36
37
|
close() {
|
|
@@ -41,6 +42,7 @@ function rowToTurn(row) {
|
|
|
41
42
|
return {
|
|
42
43
|
turnId: row.turn_id,
|
|
43
44
|
...(row.conversation_scope_id ? { conversationScopeId: row.conversation_scope_id } : {}),
|
|
45
|
+
...(row.workspace_id ? { workspaceId: row.workspace_id } : {}),
|
|
44
46
|
createdAt: row.created_at,
|
|
45
47
|
};
|
|
46
48
|
}
|
|
@@ -46,7 +46,7 @@ export class ActivityLifecycle {
|
|
|
46
46
|
start(options) {
|
|
47
47
|
const activityId = options.activityId ?? this.activityId();
|
|
48
48
|
const turnId = options.turnId
|
|
49
|
-
?? this.turnIdForConversation?.(options.conversationScopeId)
|
|
49
|
+
?? this.turnIdForConversation?.(options.conversationScopeId, options.workspace.id)
|
|
50
50
|
?? this.turnId();
|
|
51
51
|
const request = normalizeAuditValue(options.request);
|
|
52
52
|
this.auditStore.append({
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
|
|
2
2
|
import * as z from "zod/v4";
|
|
3
|
-
import {
|
|
3
|
+
import { logEvent, transportSessionIdPrefix } from "../logger.js";
|
|
4
|
+
import { hostConversationScopeId } from "../request-meta.js";
|
|
5
|
+
import { ACTIVITY_PANEL_DEFAULT_EXPANDED_META_KEY, ACTIVITY_PANEL_WORKSPACE_META_KEY, } from "./ui/contract.js";
|
|
4
6
|
const READ_ONLY_ANNOTATIONS = {
|
|
5
7
|
readOnlyHint: true,
|
|
6
8
|
destructiveHint: false,
|
|
@@ -9,6 +11,7 @@ const READ_ONLY_ANNOTATIONS = {
|
|
|
9
11
|
};
|
|
10
12
|
const activitySummarySchema = z.object({
|
|
11
13
|
activityId: z.string(),
|
|
14
|
+
parentActivityId: z.string().optional(),
|
|
12
15
|
tool: z.string(),
|
|
13
16
|
kind: z.string(),
|
|
14
17
|
status: z.enum(["working", "done", "error"]),
|
|
@@ -24,6 +27,12 @@ const activitySummarySchema = z.object({
|
|
|
24
27
|
startedAt: z.string(),
|
|
25
28
|
finishedAt: z.string().optional(),
|
|
26
29
|
durationMs: z.number().nonnegative().optional(),
|
|
30
|
+
children: z.object({
|
|
31
|
+
total: z.number().int().nonnegative(),
|
|
32
|
+
working: z.number().int().nonnegative(),
|
|
33
|
+
done: z.number().int().nonnegative(),
|
|
34
|
+
error: z.number().int().nonnegative(),
|
|
35
|
+
}).optional(),
|
|
27
36
|
});
|
|
28
37
|
const snapshotOutputSchema = {
|
|
29
38
|
turnId: z.string(),
|
|
@@ -31,48 +40,110 @@ const snapshotOutputSchema = {
|
|
|
31
40
|
changed: z.boolean(),
|
|
32
41
|
state: z.enum(["working", "done", "error"]),
|
|
33
42
|
activities: z.array(activitySummarySchema),
|
|
43
|
+
[ACTIVITY_PANEL_WORKSPACE_META_KEY]: z.record(z.string(), z.unknown()).optional(),
|
|
34
44
|
};
|
|
35
|
-
export function registerActivityQueryTools(server, queries) {
|
|
45
|
+
export function registerActivityQueryTools(server, queries, connectionScopeId, panelMeta = {}, panelDefaultExpanded = false, logging, workspacePanelState) {
|
|
46
|
+
const panelUi = typeof panelMeta.ui === "object" && panelMeta.ui !== null
|
|
47
|
+
? panelMeta.ui
|
|
48
|
+
: {};
|
|
36
49
|
registerAppTool(server, "activity_panel", {
|
|
37
|
-
title: "Begin
|
|
38
|
-
description: "Begin one ForgeRelay Host Turn
|
|
39
|
-
inputSchema: {
|
|
50
|
+
title: "Begin ForgeRelay Panel",
|
|
51
|
+
description: "Begin one ForgeRelay Host Turn for project work in workspaceId as the single ForgeRelay UI render tool for that Workspace. If this Host Turn calls open_workspace, open_workspace must run first and its returned workspaceId must be passed here. A different workspaceId creates a different card. Call exactly once after workspace resolution and before the first non-lifecycle ForgeRelay work tool.",
|
|
52
|
+
inputSchema: {
|
|
53
|
+
workspaceId: z.string().min(1).describe("Workspace identifier returned by open_workspace for the project work in this Host Turn."),
|
|
54
|
+
},
|
|
40
55
|
outputSchema: snapshotOutputSchema,
|
|
41
|
-
_meta: {
|
|
56
|
+
_meta: {
|
|
57
|
+
...panelMeta,
|
|
58
|
+
ui: {
|
|
59
|
+
...panelUi,
|
|
60
|
+
visibility: ["model", "app"],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
42
63
|
annotations: {
|
|
43
64
|
...READ_ONLY_ANNOTATIONS,
|
|
44
65
|
idempotentHint: false,
|
|
45
66
|
},
|
|
46
|
-
}, async (
|
|
47
|
-
const
|
|
67
|
+
}, async ({ workspaceId }, extra) => {
|
|
68
|
+
const workspace = workspacePanelState?.(workspaceId);
|
|
69
|
+
if (!workspace) {
|
|
70
|
+
throw new Error(`No Workspace presentation is available for ${workspaceId}. Call open_workspace for that workspace before activity_panel.`);
|
|
71
|
+
}
|
|
72
|
+
const snapshot = queries.beginTurn(hostConversationScopeId(extra._meta, extra.sessionId, connectionScopeId), workspaceId);
|
|
73
|
+
if (logging) {
|
|
74
|
+
logEvent(logging, "debug", "activity_panel_call", {
|
|
75
|
+
turnId: snapshot.turnId,
|
|
76
|
+
revision: snapshot.revision,
|
|
77
|
+
state: snapshot.state,
|
|
78
|
+
activities: snapshot.activities.length,
|
|
79
|
+
workspaceId,
|
|
80
|
+
transportSessionIdPrefix: transportSessionIdPrefix(extra.sessionId),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
48
83
|
return {
|
|
84
|
+
_meta: {
|
|
85
|
+
[ACTIVITY_PANEL_DEFAULT_EXPANDED_META_KEY]: panelDefaultExpanded,
|
|
86
|
+
[ACTIVITY_PANEL_WORKSPACE_META_KEY]: workspace,
|
|
87
|
+
},
|
|
49
88
|
content: [{
|
|
50
89
|
type: "text",
|
|
51
|
-
text: `Started ForgeRelay
|
|
90
|
+
text: `Started ForgeRelay Panel Host Turn ${snapshot.turnId} for ${workspaceId}.`,
|
|
52
91
|
}],
|
|
53
|
-
structuredContent: {
|
|
92
|
+
structuredContent: {
|
|
93
|
+
...snapshot,
|
|
94
|
+
[ACTIVITY_PANEL_WORKSPACE_META_KEY]: workspace,
|
|
95
|
+
},
|
|
54
96
|
};
|
|
55
97
|
});
|
|
56
98
|
registerAppTool(server, "activity_snapshot", {
|
|
57
99
|
title: "Read Activity snapshot",
|
|
58
100
|
description: "App-only data source for lightweight Activity summaries in one durable Host Turn.",
|
|
59
101
|
inputSchema: {
|
|
60
|
-
turnId: z.string(),
|
|
102
|
+
turnId: z.string().optional().describe("Existing Host Turn identifier. Omit only for initial App bootstrap; ForgeRelay then resolves the current Host Turn from conversation metadata."),
|
|
103
|
+
workspaceId: z.string().min(1).optional().describe("Workspace identifier used by the App during initial bootstrap so ForgeRelay can restore the Workspace section."),
|
|
61
104
|
knownRevision: z.number().int().nonnegative().optional(),
|
|
62
105
|
},
|
|
63
106
|
outputSchema: snapshotOutputSchema,
|
|
64
107
|
_meta: { ui: { visibility: ["app"] } },
|
|
65
108
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
66
|
-
}, async ({ turnId, knownRevision }) => {
|
|
67
|
-
const
|
|
109
|
+
}, async ({ turnId, workspaceId, knownRevision }, extra) => {
|
|
110
|
+
const conversationScopeId = hostConversationScopeId(extra._meta, extra.sessionId, connectionScopeId);
|
|
111
|
+
const resolvedTurnId = turnId ?? queries.currentTurnId(conversationScopeId, workspaceId);
|
|
112
|
+
if (!resolvedTurnId) {
|
|
113
|
+
throw new Error("Activity snapshot bootstrap could not resolve the current Host Turn from conversation and workspace metadata.");
|
|
114
|
+
}
|
|
115
|
+
const snapshot = queries.snapshot(resolvedTurnId, knownRevision);
|
|
116
|
+
if (logging) {
|
|
117
|
+
logEvent(logging, "debug", "activity_snapshot_call", {
|
|
118
|
+
turnId: resolvedTurnId,
|
|
119
|
+
bootstrap: turnId === undefined,
|
|
120
|
+
knownRevision,
|
|
121
|
+
revision: snapshot.revision,
|
|
122
|
+
changed: snapshot.changed,
|
|
123
|
+
state: snapshot.state,
|
|
124
|
+
activities: snapshot.activities.length,
|
|
125
|
+
transportSessionIdPrefix: transportSessionIdPrefix(extra.sessionId),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
const workspace = workspaceId ? workspacePanelState?.(workspaceId) : undefined;
|
|
129
|
+
if (workspaceId && !workspace) {
|
|
130
|
+
throw new Error(`No Workspace presentation is available for ${workspaceId}.`);
|
|
131
|
+
}
|
|
68
132
|
return {
|
|
133
|
+
_meta: {
|
|
134
|
+
[ACTIVITY_PANEL_DEFAULT_EXPANDED_META_KEY]: panelDefaultExpanded,
|
|
135
|
+
...(workspace ? { [ACTIVITY_PANEL_WORKSPACE_META_KEY]: workspace } : {}),
|
|
136
|
+
},
|
|
69
137
|
content: [{
|
|
70
138
|
type: "text",
|
|
71
139
|
text: snapshot.changed
|
|
72
|
-
? `Activity snapshot ${
|
|
73
|
-
: `Activity snapshot ${
|
|
140
|
+
? `Activity snapshot ${resolvedTurnId} revision ${snapshot.revision}.`
|
|
141
|
+
: `Activity snapshot ${resolvedTurnId} unchanged at revision ${snapshot.revision}.`,
|
|
74
142
|
}],
|
|
75
|
-
structuredContent: {
|
|
143
|
+
structuredContent: {
|
|
144
|
+
...snapshot,
|
|
145
|
+
...(workspace ? { [ACTIVITY_PANEL_WORKSPACE_META_KEY]: workspace } : {}),
|
|
146
|
+
},
|
|
76
147
|
};
|
|
77
148
|
});
|
|
78
149
|
registerAppTool(server, "activity_detail", {
|
|
@@ -90,8 +161,18 @@ export function registerActivityQueryTools(server, queries) {
|
|
|
90
161
|
},
|
|
91
162
|
_meta: { ui: { visibility: ["app"] } },
|
|
92
163
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
93
|
-
}, async ({ turnId, activityId }) => {
|
|
164
|
+
}, async ({ turnId, activityId }, extra) => {
|
|
94
165
|
const detail = queries.detail(turnId, activityId);
|
|
166
|
+
if (logging) {
|
|
167
|
+
logEvent(logging, "debug", "activity_detail_call", {
|
|
168
|
+
turnId,
|
|
169
|
+
activityId,
|
|
170
|
+
tool: detail.activity.tool,
|
|
171
|
+
kind: detail.activity.kind,
|
|
172
|
+
state: detail.activity.state,
|
|
173
|
+
transportSessionIdPrefix: transportSessionIdPrefix(extra.sessionId),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
95
176
|
return {
|
|
96
177
|
content: [{ type: "text", text: `Activity detail ${activityId}.` }],
|
|
97
178
|
structuredContent: { ...detail },
|
|
@@ -119,8 +200,19 @@ export function registerActivityQueryTools(server, queries) {
|
|
|
119
200
|
},
|
|
120
201
|
_meta: { ui: { visibility: ["app"] } },
|
|
121
202
|
annotations: READ_ONLY_ANNOTATIONS,
|
|
122
|
-
}, async ({ turnId, outputId }) => {
|
|
203
|
+
}, async ({ turnId, outputId }, extra) => {
|
|
123
204
|
const output = queries.bashOutput(turnId, outputId);
|
|
205
|
+
if (logging) {
|
|
206
|
+
logEvent(logging, "debug", "activity_output_call", {
|
|
207
|
+
turnId,
|
|
208
|
+
outputId,
|
|
209
|
+
activityId: output.activityId,
|
|
210
|
+
processId: output.processId,
|
|
211
|
+
status: output.status,
|
|
212
|
+
outputBytes: Buffer.byteLength(output.output),
|
|
213
|
+
transportSessionIdPrefix: transportSessionIdPrefix(extra.sessionId),
|
|
214
|
+
});
|
|
215
|
+
}
|
|
124
216
|
return {
|
|
125
217
|
content: [{ type: "text", text: `Bash output ${outputId}.` }],
|
|
126
218
|
structuredContent: { ...output },
|
|
@@ -7,12 +7,12 @@ export class ActivityQueryService {
|
|
|
7
7
|
this.audit = audit;
|
|
8
8
|
this.outputs = outputs;
|
|
9
9
|
}
|
|
10
|
-
beginTurn(conversationScopeId) {
|
|
11
|
-
const turn = this.turns.begin(conversationScopeId);
|
|
10
|
+
beginTurn(conversationScopeId, workspaceId) {
|
|
11
|
+
const turn = this.turns.begin(conversationScopeId, workspaceId);
|
|
12
12
|
return this.snapshot(turn.turnId);
|
|
13
13
|
}
|
|
14
|
-
currentTurnId(conversationScopeId) {
|
|
15
|
-
return this.turns.current(conversationScopeId)?.turnId;
|
|
14
|
+
currentTurnId(conversationScopeId, workspaceId) {
|
|
15
|
+
return this.turns.current(conversationScopeId, workspaceId)?.turnId;
|
|
16
16
|
}
|
|
17
17
|
snapshot(turnId, knownRevision) {
|
|
18
18
|
this.requireTurn(turnId);
|
package/dist/cli.js
CHANGED
|
@@ -21,6 +21,7 @@ import { createLocalAgentStore } from "./local-agent-store.js";
|
|
|
21
21
|
import { generateOwnerToken, loadDevspaceFiles, resolveSubagentsFlag, writeDevspaceAuth, writeDevspaceConfig, } from "./user-config.js";
|
|
22
22
|
import { expandHomePath } from "./roots.js";
|
|
23
23
|
import { shutdownHttpServer } from "./server-shutdown.js";
|
|
24
|
+
import { publicEndpointUrl } from "./oauth/public-url.js";
|
|
24
25
|
const require = createRequire(import.meta.url);
|
|
25
26
|
const SUPPORTED_NODE_RANGE = ">=20.12 <27";
|
|
26
27
|
async function main(argv) {
|
|
@@ -113,25 +114,30 @@ async function runInit({ force }) {
|
|
|
113
114
|
});
|
|
114
115
|
const port = Number(portAnswer);
|
|
115
116
|
prompts.note([
|
|
116
|
-
"ForgeRelay needs
|
|
117
|
+
"ForgeRelay needs one or more public base URLs so ChatGPT or Claude can reach this MCP server.",
|
|
117
118
|
"Create a tunnel or reverse proxy with Cloudflare Tunnel, ngrok, Pinggy, Tailscale Funnel, or your own HTTPS proxy.",
|
|
118
|
-
"
|
|
119
|
+
"Each URL may include its own route prefix. Separate multiple URLs with commas; the first is canonical.",
|
|
119
120
|
"",
|
|
120
|
-
"Example: https://
|
|
121
|
+
"Example: https://forge.example.com/forgerelay/main, https://alias.example.com/relay",
|
|
121
122
|
].join("\n"), "Public URL required");
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
123
|
+
const defaultPublicBaseUrls = Array.isArray(files.config.publicBaseUrl)
|
|
124
|
+
? files.config.publicBaseUrl.join(", ")
|
|
125
|
+
: files.config.publicBaseUrl ?? "";
|
|
126
|
+
const publicBaseUrls = normalizePublicBaseUrlsInput(await textPrompt({
|
|
127
|
+
message: defaultPublicBaseUrls
|
|
128
|
+
? `What are the public base URLs? Press Enter to keep ${defaultPublicBaseUrls}`
|
|
129
|
+
: "What are the public base URLs?",
|
|
130
|
+
placeholder: defaultPublicBaseUrls || "https://your-tunnel-host.example.com",
|
|
131
|
+
defaultValue: defaultPublicBaseUrls,
|
|
132
|
+
validate: validateRequiredPublicBaseUrls,
|
|
129
133
|
}));
|
|
134
|
+
const publicBaseUrl = compactPublicBaseUrlConfig(publicBaseUrls);
|
|
130
135
|
const config = {
|
|
131
136
|
host: files.config.host ?? "127.0.0.1",
|
|
132
137
|
port,
|
|
133
138
|
allowedRoots,
|
|
134
139
|
publicBaseUrl,
|
|
140
|
+
allowedHosts: files.config.allowedHosts,
|
|
135
141
|
workflowInstructions: files.config.workflowInstructions,
|
|
136
142
|
appendInstructions: files.config.appendInstructions,
|
|
137
143
|
subagents: resolveSubagentsFlag(files.config),
|
|
@@ -145,7 +151,7 @@ async function runInit({ force }) {
|
|
|
145
151
|
`Config: ${configPath}`,
|
|
146
152
|
`Auth: ${authPath}`,
|
|
147
153
|
`Local MCP URL: http://${config.host}:${config.port}/mcp`,
|
|
148
|
-
...(
|
|
154
|
+
...publicBaseUrls.map((baseUrl, index) => `${index === 0 ? "Public MCP URL" : "Public MCP alias"}: ${publicEndpointUrl(baseUrl, "mcp").toString()}`),
|
|
149
155
|
];
|
|
150
156
|
prompts.note(lines.join("\n"), "ForgeRelay configured");
|
|
151
157
|
prompts.note([
|
|
@@ -222,8 +228,9 @@ async function runDoctor() {
|
|
|
222
228
|
try {
|
|
223
229
|
const config = loadConfig();
|
|
224
230
|
console.log(`Local MCP URL: http://${config.host}:${config.port}/mcp`);
|
|
231
|
+
console.log(`Public base URLs: ${config.publicBaseUrls.join(", ")}`);
|
|
225
232
|
console.log(`Public base URL: ${config.publicBaseUrl}`);
|
|
226
|
-
console.log(`Public MCP URL: ${
|
|
233
|
+
console.log(`Public MCP URL: ${publicEndpointUrl(config.publicBaseUrl, "mcp").toString()}`);
|
|
227
234
|
console.log(`Tool mode: ${config.toolMode}`);
|
|
228
235
|
console.log(`Widgets: ${config.widgets}`);
|
|
229
236
|
console.log(`Trust proxy: ${config.logging.trustProxy ? "one hop" : "off"}`);
|
|
@@ -248,12 +255,11 @@ function runConfigCommand(args) {
|
|
|
248
255
|
throw new Error(`Unknown config command: ${subcommand}`);
|
|
249
256
|
}
|
|
250
257
|
if (key !== "publicBaseUrl") {
|
|
251
|
-
throw new Error("Only `forgerelay config set publicBaseUrl <url|null>` is supported right now.");
|
|
258
|
+
throw new Error("Only `forgerelay config set publicBaseUrl <url[,url...]|null>` is supported right now.");
|
|
252
259
|
}
|
|
253
260
|
const value = rest.join(" ").trim();
|
|
254
|
-
if (!value)
|
|
261
|
+
if (!value)
|
|
255
262
|
throw new Error("Missing publicBaseUrl value.");
|
|
256
|
-
}
|
|
257
263
|
writeDevspaceConfig({
|
|
258
264
|
...files.config,
|
|
259
265
|
publicBaseUrl: normalizeOptionalPublicBaseUrl(value),
|
|
@@ -270,7 +276,7 @@ function printHelp() {
|
|
|
270
276
|
" forgerelay init Create or update ~/.forgerelay/config.json and auth.json",
|
|
271
277
|
" forgerelay doctor Show config, runtime, and native dependency status",
|
|
272
278
|
" forgerelay config get Print persisted config",
|
|
273
|
-
" forgerelay config set publicBaseUrl <url|null>",
|
|
279
|
+
" forgerelay config set publicBaseUrl <url[,url...]|null>",
|
|
274
280
|
" forgerelay hooks list [--project <path>]",
|
|
275
281
|
" forgerelay hooks check [--project <path>]",
|
|
276
282
|
" forgerelay agents ls List subagent sessions",
|
|
@@ -279,7 +285,7 @@ function printHelp() {
|
|
|
279
285
|
" forgerelay -v, --version Print the installed version",
|
|
280
286
|
"",
|
|
281
287
|
"For temporary tunnels:",
|
|
282
|
-
" FORGERELAY_PUBLIC_BASE_URL=https://example.trycloudflare.com forgerelay serve",
|
|
288
|
+
" FORGERELAY_PUBLIC_BASE_URL=https://example.trycloudflare.com/forgerelay/debug forgerelay serve",
|
|
283
289
|
].join("\n"));
|
|
284
290
|
}
|
|
285
291
|
async function runAgentsCommand(args) {
|
|
@@ -542,11 +548,27 @@ function printVersion() {
|
|
|
542
548
|
}
|
|
543
549
|
console.log(packageJson.version);
|
|
544
550
|
}
|
|
551
|
+
function isNullConfigValue(value) {
|
|
552
|
+
const normalized = value.trim().toLowerCase();
|
|
553
|
+
return !normalized || normalized === "null" || normalized === "none";
|
|
554
|
+
}
|
|
545
555
|
function normalizeOptionalPublicBaseUrl(value) {
|
|
546
556
|
const trimmed = value.trim();
|
|
547
|
-
if (
|
|
557
|
+
if (isNullConfigValue(trimmed))
|
|
548
558
|
return null;
|
|
549
|
-
return
|
|
559
|
+
return compactPublicBaseUrlConfig(normalizePublicBaseUrlsInput(trimmed));
|
|
560
|
+
}
|
|
561
|
+
function normalizePublicBaseUrlsInput(value) {
|
|
562
|
+
const entries = value
|
|
563
|
+
.split(",")
|
|
564
|
+
.map((entry) => entry.trim())
|
|
565
|
+
.filter(Boolean);
|
|
566
|
+
if (entries.length === 0)
|
|
567
|
+
throw new Error("Enter at least one public base URL.");
|
|
568
|
+
return Array.from(new Set(entries.map((entry) => normalizePublicBaseUrl(entry))));
|
|
569
|
+
}
|
|
570
|
+
function compactPublicBaseUrlConfig(baseUrls) {
|
|
571
|
+
return baseUrls.length === 1 ? baseUrls[0] : baseUrls;
|
|
550
572
|
}
|
|
551
573
|
function normalizePublicBaseUrl(value) {
|
|
552
574
|
const trimmed = value.trim();
|
|
@@ -572,23 +594,16 @@ function validatePort(value) {
|
|
|
572
594
|
? undefined
|
|
573
595
|
: "Enter a port between 1 and 65535.";
|
|
574
596
|
}
|
|
575
|
-
function
|
|
597
|
+
function validateRequiredPublicBaseUrls(value) {
|
|
576
598
|
const trimmed = value?.trim() ?? "";
|
|
577
599
|
if (!trimmed)
|
|
578
|
-
return "Enter
|
|
579
|
-
if (trimmed.endsWith("/mcp"))
|
|
580
|
-
return "Enter the base URL only, without /mcp.";
|
|
581
|
-
return validatePublicBaseUrl(trimmed);
|
|
582
|
-
}
|
|
583
|
-
function validatePublicBaseUrl(value) {
|
|
600
|
+
return "Enter at least one public base URL from your tunnel or reverse proxy.";
|
|
584
601
|
try {
|
|
585
|
-
|
|
586
|
-
return
|
|
587
|
-
? undefined
|
|
588
|
-
: "Use an http or https URL.";
|
|
602
|
+
normalizePublicBaseUrlsInput(trimmed);
|
|
603
|
+
return undefined;
|
|
589
604
|
}
|
|
590
|
-
catch {
|
|
591
|
-
return
|
|
605
|
+
catch (error) {
|
|
606
|
+
return error instanceof Error ? error.message : String(error);
|
|
592
607
|
}
|
|
593
608
|
}
|
|
594
609
|
function assertSupportedNode() {
|
package/dist/config.js
CHANGED
|
@@ -3,7 +3,7 @@ import { homedir } from "node:os";
|
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import { expandHomePath } from "./roots.js";
|
|
5
5
|
import { mergeHookConfigs, parseHookConfig } from "./hooks.js";
|
|
6
|
-
import { forgerelayAgentsDir, forgerelaySkillsDir, loadForgeRelayFiles } from "./user-config.js";
|
|
6
|
+
import { forgerelayAgentsDir, forgerelaySkillsDir, loadForgeRelayFiles, } from "./user-config.js";
|
|
7
7
|
const DEFAULT_OAUTH_ACCESS_TOKEN_TTL_SECONDS = 60 * 60;
|
|
8
8
|
const DEFAULT_OAUTH_REFRESH_TOKEN_TTL_SECONDS = 30 * 24 * 60 * 60;
|
|
9
9
|
const DEFAULT_ARTIFACT_MAX_FILE_BYTES = 100 * 1024 * 1024;
|
|
@@ -186,17 +186,41 @@ function parseSystemInstructionsPath(value) {
|
|
|
186
186
|
}
|
|
187
187
|
return resolve(expandHomePath(value.trim()));
|
|
188
188
|
}
|
|
189
|
+
function parsePublicBaseUrls(value, fallback) {
|
|
190
|
+
if (value === null)
|
|
191
|
+
return fallback;
|
|
192
|
+
const raw = Array.isArray(value)
|
|
193
|
+
? value.map((entry) => entry.trim()).filter(Boolean)
|
|
194
|
+
: value?.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
195
|
+
if (value !== undefined && (!raw || raw.length === 0)) {
|
|
196
|
+
throw new Error("FORGERELAY_PUBLIC_BASE_URL must contain at least one public base URL.");
|
|
197
|
+
}
|
|
198
|
+
const normalized = (raw ?? fallback).map((entry) => parsePublicBaseUrl(entry));
|
|
199
|
+
return Array.from(new Set(normalized));
|
|
200
|
+
}
|
|
201
|
+
function resolvePublicDeployment(env, fileConfig, host, port) {
|
|
202
|
+
const localBaseUrls = [parsePublicBaseUrl(localPublicBaseUrl(host, port))];
|
|
203
|
+
const envBaseUrl = productEnv(env, "PUBLIC_BASE_URL");
|
|
204
|
+
const baseUrls = envBaseUrl !== undefined
|
|
205
|
+
? parsePublicBaseUrls(envBaseUrl, localBaseUrls)
|
|
206
|
+
: parsePublicBaseUrls(fileConfig.publicBaseUrl, localBaseUrls);
|
|
207
|
+
return {
|
|
208
|
+
baseUrls,
|
|
209
|
+
canonicalBaseUrl: baseUrls[0],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
189
212
|
export function loadConfig(env = process.env) {
|
|
190
213
|
const files = loadForgeRelayFiles(env);
|
|
191
214
|
const host = env.HOST ?? files.config.host ?? "127.0.0.1";
|
|
192
215
|
const port = parsePort(env.PORT ?? files.config.port);
|
|
193
|
-
const
|
|
216
|
+
const publicDeployment = resolvePublicDeployment(env, files.config, host, port);
|
|
217
|
+
const publicBaseUrl = publicDeployment.canonicalBaseUrl;
|
|
194
218
|
const derivedAllowedHosts = [
|
|
195
219
|
"localhost",
|
|
196
220
|
"127.0.0.1",
|
|
197
221
|
"::1",
|
|
198
222
|
host,
|
|
199
|
-
new URL(
|
|
223
|
+
...publicDeployment.baseUrls.map((baseUrl) => new URL(baseUrl).hostname),
|
|
200
224
|
...(files.config.allowedHosts ?? []),
|
|
201
225
|
];
|
|
202
226
|
return {
|
|
@@ -206,10 +230,14 @@ export function loadConfig(env = process.env) {
|
|
|
206
230
|
allowedRoots: parseAllowedRoots(productEnv(env, "ALLOWED_ROOTS") ?? files.config.allowedRoots),
|
|
207
231
|
allowedHosts: parseAllowedHosts(productEnv(env, "ALLOWED_HOSTS"), derivedAllowedHosts),
|
|
208
232
|
publicBaseUrl,
|
|
233
|
+
publicBaseUrls: publicDeployment.baseUrls,
|
|
209
234
|
toolMode: parseToolMode(env),
|
|
210
235
|
workflowInstructions: parseWorkflowInstructions(productEnv(env, "WORKFLOW_INSTRUCTIONS"), files.config.workflowInstructions),
|
|
211
236
|
appendInstructions: parseAppendInstructions(productEnv(env, "APPEND_INSTRUCTIONS"), files.config.appendInstructions),
|
|
212
237
|
widgets: parseWidgetMode(productEnv(env, "WIDGETS")),
|
|
238
|
+
activityPanelExpanded: productEnv(env, "ACTIVITY_PANEL_EXPANDED") === undefined
|
|
239
|
+
? files.config.activityPanelExpanded === true
|
|
240
|
+
: parseBoolean(productEnv(env, "ACTIVITY_PANEL_EXPANDED")),
|
|
213
241
|
stateDir: resolve(expandHomePath(productEnv(env, "STATE_DIR") ?? files.config.stateDir ?? defaultStateDir())),
|
|
214
242
|
worktreeRoot: resolve(expandHomePath(productEnv(env, "WORKTREE_ROOT") ?? files.config.worktreeRoot ?? defaultWorktreeRoot())),
|
|
215
243
|
artifactsEnabled: productEnv(env, "ARTIFACTS") === undefined
|