@akira-tl/forgerelay 1.1.1 → 1.2.0-rc.1
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 +39 -0
- package/README.md +14 -0
- package/dist/activity/runtime/mcp-query-tools.js +15 -5
- package/dist/cli/config/inspect.js +331 -0
- package/dist/cli/config/migrate.js +351 -0
- package/dist/cli/init/setup-config.js +41 -0
- package/dist/cli/init.js +128 -130
- package/dist/cli/maintenance.js +15 -5
- package/dist/cli/mcp/external-mcp.js +14 -10
- package/dist/cli/mcp/status.js +11 -6
- package/dist/cli.js +40 -7
- package/dist/lsp/language-server-config.js +66 -144
- package/dist/lsp/runtime/manager.js +35 -4
- package/dist/mcp/hooks/config.js +141 -0
- package/dist/mcp/hooks/external-mcp-transform.js +22 -13
- package/dist/mcp/hooks/hook-cli.js +62 -52
- package/dist/mcp/hooks/hooks.js +111 -85
- package/dist/mcp/operations/external-mcp/external-mcp-runtime.js +8 -5
- package/dist/mcp/operations/external-mcp/external-mcp.js +9 -2
- package/dist/mcp/server/core/capability-support.js +7 -0
- package/dist/mcp/server/workspace/runtime/workspace-open-presentation.js +2 -0
- package/dist/mcp/server/workspace/runtime/workspace-tools.js +2 -2
- package/dist/runtime/config/config.js +142 -64
- package/dist/runtime/config/definition/catalog.js +12 -0
- package/dist/runtime/config/definition/definition.js +115 -0
- package/dist/runtime/config/definition/external-mcp.js +58 -0
- package/dist/runtime/config/definition/general-config.js +258 -0
- package/dist/runtime/config/definition/language-servers.js +116 -0
- package/dist/runtime/config/definition/schema.js +113 -0
- package/dist/runtime/config/definition/types.js +2 -0
- package/dist/runtime/config/external-mcp-auth-store.js +105 -29
- package/dist/runtime/config/external-mcp-config.js +39 -0
- package/dist/runtime/config/external-mcp-registry.js +284 -106
- package/dist/runtime/config/inspection/live.js +77 -0
- package/dist/runtime/config/resolution/general.js +93 -0
- package/dist/runtime/config/resolution/hooks.js +315 -0
- package/dist/runtime/config/resolution/language-servers.js +237 -0
- package/dist/runtime/config/resolution/project-sources.js +159 -0
- package/dist/runtime/config/resolution/resolver.js +485 -0
- package/dist/runtime/config/resolution/types.js +1 -0
- package/dist/runtime/config/runtime/config-runtime.js +74 -0
- package/dist/runtime/config/runtime/source-refresh.js +263 -0
- package/dist/runtime/config/user-config.js +38 -31
- package/dist/runtime/security/project-execution-trust.js +34 -0
- package/dist/runtime/testing/server-fixture.js +9 -8
- package/dist/server.js +10 -6
- package/dist/subagents/profiles.js +336 -53
- package/dist/subagents/sessions/execution.js +6 -35
- package/dist/subagents/sessions/manager.js +14 -2
- package/dist/ui/.vite/manifest.json +275 -275
- package/dist/ui/activity-panel-app.html +1 -1
- package/dist/ui/assets/{activity-panel-app-Bw7ZFX2g.js → activity-panel-app-BeDiwd7D.js} +3 -3
- package/dist/ui/assets/{angular-html-DeUNP12X.js → angular-html-DuoCE7nn.js} +1 -1
- package/dist/ui/assets/{angular-ts-B3v8FKi_.js → angular-ts-D5tGk0hL.js} +1 -1
- package/dist/ui/assets/{apl-Bs3iQm4P.js → apl-lciOTPbw.js} +1 -1
- package/dist/ui/assets/{astro-D6GHecBY.js → astro-vJbqfNnJ.js} +1 -1
- package/dist/ui/assets/{blade-BMNbVOHv.js → blade-C7655Q4K.js} +1 -1
- package/dist/ui/assets/{c-BquuA6bv.js → c-DdHp1Jrw.js} +1 -1
- package/dist/ui/assets/{cobol-BuZ6kfps.js → cobol-DgvzQxZP.js} +1 -1
- package/dist/ui/assets/{coffee-DgC5uBzL.js → coffee-BIjVUOUd.js} +1 -1
- package/dist/ui/assets/{cpp-BdvWqCaB.js → cpp-CU8tGzvI.js} +1 -1
- package/dist/ui/assets/{crystal-0Q5qBvNI.js → crystal-CUQAwsg9.js} +1 -1
- package/dist/ui/assets/{css-Be_T8idh.js → css-DtMTqD3F.js} +1 -1
- package/dist/ui/assets/{edge-DaGLdgh_.js → edge-DoCX9RnL.js} +1 -1
- package/dist/ui/assets/{elixir-CMgiFIzB.js → elixir-DJxV9_U6.js} +1 -1
- package/dist/ui/assets/{elm-B-ufMlMC.js → elm-DIgWqK4r.js} +1 -1
- package/dist/ui/assets/{erb-CaGiOZM-.js → erb-BHxP0c_e.js} +1 -1
- package/dist/ui/assets/{git-rebase-CpwUWCzK.js → git-rebase-HbI_Vo8d.js} +1 -1
- package/dist/ui/assets/{glimmer-js-BRxccvnE.js → glimmer-js-CthY4Ty_.js} +1 -1
- package/dist/ui/assets/{glimmer-ts-CgQbPzlT.js → glimmer-ts-B2-_A1Xz.js} +1 -1
- package/dist/ui/assets/{glsl-D7JmHFr0.js → glsl-D9V9gq98.js} +1 -1
- package/dist/ui/assets/{graphql-C8n0eUBK.js → graphql-CddeAaS3.js} +1 -1
- package/dist/ui/assets/{hack-lVN7KVQv.js → hack-D_loGkOT.js} +1 -1
- package/dist/ui/assets/{haml-h18lG0pS.js → haml-BoWDczd0.js} +1 -1
- package/dist/ui/assets/{handlebars-lRFjRPfj.js → handlebars-ZoSOvw9y.js} +1 -1
- package/dist/ui/assets/{heavy-payload-Cqx9MOkH.js → heavy-payload-DO34LwoZ.js} +3 -3
- package/dist/ui/assets/{html-DtILqTE1.js → html-CE-obXVo.js} +1 -1
- package/dist/ui/assets/{html-derivative-CSTSmPup.js → html-derivative-Cd-smAbW.js} +1 -1
- package/dist/ui/assets/{http-DNI8fsbY.js → http-CZrkbrSl.js} +1 -1
- package/dist/ui/assets/{hurl-DgRi60JG.js → hurl-CxTuxBt1.js} +1 -1
- package/dist/ui/assets/{java-CW9pZ5bh.js → java-BshLjCat.js} +1 -1
- package/dist/ui/assets/{javascript-Cvg2UrWL.js → javascript-D2pxQTiX.js} +1 -1
- package/dist/ui/assets/{jinja-CZw_S1FB.js → jinja-kbfPP7-F.js} +1 -1
- package/dist/ui/assets/{jison-C56u_FZv.js → jison-C2mt9l1M.js} +1 -1
- package/dist/ui/assets/{json-DW2UTv1q.js → json-DU_rg_AU.js} +1 -1
- package/dist/ui/assets/{jsx-Zj5gobdD.js → jsx-D_8aQMlx.js} +1 -1
- package/dist/ui/assets/{julia-Cvcrre-L.js → julia-BmefTaVV.js} +1 -1
- package/dist/ui/assets/{just-8CZ2u6Tj.js → just-DXV8vk1S.js} +1 -1
- package/dist/ui/assets/{latex-D289AXWG.js → latex-Cjm2MvGW.js} +1 -1
- package/dist/ui/assets/{liquid-CFEX3SkZ.js → liquid-DyQDlo0u.js} +1 -1
- package/dist/ui/assets/{lua-BKpJ0ybO.js → lua-B2A1vzzy.js} +1 -1
- package/dist/ui/assets/{marko-D9maZ0a6.js → marko-B9vOkXOU.js} +1 -1
- package/dist/ui/assets/{mdc-rM9l0qWR.js → mdc-D42m6spU.js} +1 -1
- package/dist/ui/assets/{nginx-C9w6p2C9.js → nginx-TpfVxSUY.js} +1 -1
- package/dist/ui/assets/{nim-BRePHk0J.js → nim-CJr5T1FK.js} +1 -1
- package/dist/ui/assets/{perl-CskTmiPQ.js → perl-cJZcsdm9.js} +1 -1
- package/dist/ui/assets/{php-DCU_FvNI.js → php-CSZL5KRT.js} +1 -1
- package/dist/ui/assets/{pug-XLaVxTvv.js → pug-B1eMKzuA.js} +1 -1
- package/dist/ui/assets/{qml-CaONKSl-.js → qml-Dl2_7zc9.js} +1 -1
- package/dist/ui/assets/{r-BCVR0ZE3.js → r-F0aaO9gG.js} +1 -1
- package/dist/ui/assets/{razor-DDnGXxnU.js → razor-BPnkSMCw.js} +1 -1
- package/dist/ui/assets/{regexp-DWGTYYVx.js → regexp-BnGHMFbv.js} +1 -1
- package/dist/ui/assets/{rst-Dz3GWvQm.js → rst-B3CO7WYO.js} +1 -1
- package/dist/ui/assets/{ruby-DC3jGtMY.js → ruby-B6VMzT1T.js} +1 -1
- package/dist/ui/assets/{sas-7mNpvsjZ.js → sas-Df2lHVuU.js} +1 -1
- package/dist/ui/assets/{scss-uBsoCJVy.js → scss-4XxI6sgu.js} +1 -1
- package/dist/ui/assets/{shellscript-BCN9N_Jf.js → shellscript-DYASCN4r.js} +1 -1
- package/dist/ui/assets/{shellsession-Hsw46VXV.js → shellsession-Br6AGJO_.js} +1 -1
- package/dist/ui/assets/{soy-D6KV69HU.js → soy-p3ZNq1We.js} +1 -1
- package/dist/ui/assets/{sql-CVzVQkJh.js → sql-CAdv2OXL.js} +1 -1
- package/dist/ui/assets/{stata-BwUjXNhI.js → stata-Dx7yUbG_.js} +1 -1
- package/dist/ui/assets/{surrealql-CgloWqRa.js → surrealql-6YsE8HQa.js} +1 -1
- package/dist/ui/assets/{svelte-DcC3n6IC.js → svelte-YgzedwpD.js} +1 -1
- package/dist/ui/assets/{templ-DOFfnWTJ.js → templ-Bahqx3uD.js} +1 -1
- package/dist/ui/assets/{tex-ln89VPaX.js → tex-o6fTQjkH.js} +1 -1
- package/dist/ui/assets/{ts-tags-CWMzCstH.js → ts-tags-BO-Co0Ju.js} +1 -1
- package/dist/ui/assets/{tsx-DgzfiGeK.js → tsx-DtQe5AoH.js} +1 -1
- package/dist/ui/assets/{twig-Bi2itZL9.js → twig-BcUs6Kqc.js} +1 -1
- package/dist/ui/assets/{typescript-CQ24f4zW.js → typescript-BPtM8h4u.js} +1 -1
- package/dist/ui/assets/{vue-BS8jE5u6.js → vue-5KkGUzJJ.js} +1 -1
- package/dist/ui/assets/{vue-html-o38xqhyB.js → vue-html-Df_Xfsz9.js} +1 -1
- package/dist/ui/assets/{vue-vine-D2NvKv50.js → vue-vine-hUIe_u_Y.js} +1 -1
- package/dist/ui/assets/{xml-_44rx9_2.js → xml-C3Jy7as5.js} +1 -1
- package/dist/ui/assets/{xsl-B5QWQPoi.js → xsl-BnF9OuFo.js} +1 -1
- package/dist/ui/assets/{yaml-CKL2pEWe.js → yaml-DyrFgCou.js} +1 -1
- package/dist/workspaces/conversation-test-support.js +3 -11
- package/dist/workspaces/resources/skills.js +1 -1
- package/dist/workspaces/sessions.js +2 -0
- package/dist/workspaces/state/project-context.js +221 -0
- package/dist/workspaces.js +3 -1
- package/docs/chatgpt-coding-workflow.md +20 -9
- package/docs/configuration.md +103 -43
- package/docs/gotchas.md +24 -5
- package/docs/security.md +14 -3
- package/docs/setup.md +12 -7
- package/package.json +12 -3
- package/schemas/v1/config.project-local.schema.json +14 -0
- package/schemas/v1/config.project.schema.json +14 -0
- package/schemas/v1/config.user.schema.json +538 -0
- package/schemas/v1/hooks.project-local.schema.json +118 -0
- package/schemas/v1/hooks.project.schema.json +118 -0
- package/schemas/v1/hooks.user.schema.json +118 -0
- package/schemas/v1/language-servers.project-local.schema.json +230 -0
- package/schemas/v1/language-servers.project.schema.json +230 -0
- package/schemas/v1/language-servers.user.schema.json +230 -0
- package/schemas/v1/mcp.project-local.schema.json +143 -0
- package/schemas/v1/mcp.project.schema.json +143 -0
- package/schemas/v1/mcp.user.schema.json +143 -0
- package/scripts/ci/architecture.mjs +54 -4
- package/scripts/ci/cmd-acceptance.mjs +9 -9
- package/scripts/ci/config-v2-product-acceptance.mjs +639 -0
- package/scripts/ci/gates/acceptance-artifact.mjs +66 -0
- package/scripts/ci/gates/core-contract.mjs +27 -0
- package/scripts/ci/gates/platform-tests.mjs +38 -0
- package/scripts/ci/gates/prepare-windows-product.mjs +57 -0
- package/scripts/ci/gates/test-shard.mjs +71 -0
- package/scripts/ci/powershell51-acceptance.mjs +9 -9
- package/scripts/ci/pwsh-acceptance.mjs +9 -9
- package/scripts/ci/verify.mjs +2 -0
- package/scripts/ci/windows-product-acceptance.mjs +9 -10
- package/scripts/config/schema-text.mjs +4 -0
- package/scripts/config/schema-text.test.mjs +18 -0
- package/scripts/config/schema.mjs +74 -0
- package/scripts/lsp/interop.mjs +4 -1
- package/scripts/release/pack.mjs +64 -4
- package/scripts/release/release-gate.test.mjs +93 -27
- package/scripts/release-parity.mjs +19 -2
- package/templates/instructions/cmd.md +12 -0
- package/templates/instructions/fish.md +11 -0
- package/templates/instructions/powershell.md +14 -0
- package/templates/instructions/pwsh.md +14 -0
- package/templates/instructions/zsh.md +11 -0
- package/dist/runtime/instructions/powershell-skill.js +0 -65
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,45 @@ All notable ForgeRelay changes are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.2.0-rc.1] - 2026-09-13
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Added Config System v2 with generated v1 schemas, one definition/resolver model, explicit provenance, Project identity, machine-private Project Local configuration, and operator-facing `config check`, `config sources`, and `config explain` diagnostics.
|
|
12
|
+
- Added explicit `forgerelay config migrate` with dry-run, backups, atomic writes, and ForgeRelay-owned legacy normalization for External MCP, Language Servers, Lifecycle Hooks, and Subagent Profiles.
|
|
13
|
+
- Added packaged Config v2 product acceptance covering fresh setup, direct 1.1-style upgrade, explicit migration equivalence, reload/LKG, Project Local behavior, the project-execution trust seam, and isolated packaged server startup.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Unified precedence as `runtime > project-local > project > user > built-in`; External MCP, Language Servers, Hooks, and Subagent Profiles now use canonical Config v2 sources with content-fingerprint demand refresh, last-known-good fallback, and deletion semantics.
|
|
18
|
+
- Rebuilt setup so basic `init` asks only required roots and connection mode, while `init --advanced` owns common advanced options. Runtime Shell Instructions remain opt-in, and `init --force` no longer acts as an implicit migration path.
|
|
19
|
+
- Canonicalized Subagent Profile configuration under `subagents/`, kept ForgeRelay-owned Skills private to the ForgeRelay config directory, and routed legacy inputs through the same resolver instead of maintaining parallel old/new configuration architectures.
|
|
20
|
+
- Added configured-versus-applied reporting for restart-required General Config and generation retirement for long-lived runtime resources whose effective configuration changes.
|
|
21
|
+
|
|
22
|
+
### Compatibility
|
|
23
|
+
|
|
24
|
+
- Existing ForgeRelay-owned legacy configuration remains directly readable with deprecation diagnostics in v1.2.x, receives stronger removal warnings in v1.3.x, and is scheduled for parser/path removal in v1.4.0. Startup never rewrites legacy files automatically.
|
|
25
|
+
- Canonical configuration can coexist with legacy sources during migration; canonical same-scope sources shadow the corresponding legacy adapter so explicit migration preserves effective behavior.
|
|
26
|
+
|
|
27
|
+
### Security
|
|
28
|
+
|
|
29
|
+
- Separated allowed-root filesystem authority from Project Trust. Project Hooks, stdio External MCP, Project Language Servers, and Project Subagent Profiles now pass through one project-execution trust seam; v1.2.0 intentionally uses a compatibility-allow policy and does not claim a full interactive Project Trust approval UI.
|
|
30
|
+
- Kept credentials outside the ordinary Config Resolver, limited environment interpolation to definition-approved fields, and kept resolved sensitive values out of normal provenance and diagnostic output.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- Made generated Config schema freshness checks line-ending independent so Windows CRLF checkouts do not falsely report generated schemas as stale while real schema drift still fails the gate.
|
|
35
|
+
- Made Project-root regression and packaged-acceptance assertions use the same canonical realpath semantics as the runtime, covering macOS `/var` aliases and Windows short-path aliases such as `RUNNER~1` without changing Project identity behavior.
|
|
36
|
+
- Hardened cross-process `auth.json` updates on Windows by retrying only transient atomic-replace failures while the existing file lock remains held, preserving concurrent remote-auth records without weakening serialization.
|
|
37
|
+
- Updated optional LSP interoperability coverage to construct Code Intelligence through the Config v2 runtime seam, so installed servers such as `clangd` exercise the same resolver path as production.
|
|
38
|
+
|
|
39
|
+
## [1.1.2] - 2026-09-11
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- Clarified Capability discovery so Agents use only `open_workspace.capabilityCatalog` names with the `capability` gateway; semantic `capabilityFingerprint.capabilities` entries and Capability Guide descriptors are no longer presented as callable capability names.
|
|
44
|
+
- Changed Agent Skill collision precedence so a Project's `.agents/skills` definition wins over the same-named global `~/.agents/skills` definition, keeping Project-local workflow customizations authoritative.
|
|
45
|
+
|
|
7
46
|
## [1.1.1] - 2026-09-10
|
|
8
47
|
|
|
9
48
|
### Added
|
package/README.md
CHANGED
|
@@ -59,8 +59,13 @@ http://127.0.0.1:7676/mcp
|
|
|
59
59
|
|
|
60
60
|
```bash
|
|
61
61
|
forgerelay doctor
|
|
62
|
+
forgerelay config check
|
|
63
|
+
forgerelay config sources
|
|
64
|
+
forgerelay config explain <logical-path>
|
|
62
65
|
```
|
|
63
66
|
|
|
67
|
+
v1.2 的 Config System 统一解析 User、Project 和机器私有 Project Local 配置,统一优先级是 `runtime > project-local > project > user > built-in`。External MCP、Language Servers、Hooks 和 Subagent Profiles 支持按需热刷新与 last-known-good;需要重启的 General 配置会区分 configured / applied,不会自动重启 Server。Canonical JSON 可以引用 npm package 中的 `schemas/v1/*` `$schema`,受支持的敏感字段可用 `${ENV_NAME}`,诊断不会输出解析后的 secret。ForgeRelay 启动不会自动重写旧配置,需要迁移时使用 `forgerelay config migrate --dry-run ...` 先预览。
|
|
68
|
+
|
|
64
69
|
### ChatGPT 访问不到 localhost?
|
|
65
70
|
|
|
66
71
|
如果 Host 运行在云端,需要给 ForgeRelay 一个公网 HTTPS 入口。Cloudflare Tunnel、ngrok、Tailscale Funnel 或普通反向代理都可以。
|
|
@@ -120,6 +125,8 @@ ForgeRelay 给 Agent 的是真实本机执行权限,不是模拟环境。
|
|
|
120
125
|
|
|
121
126
|
文件工具受 Workspace 和 allowed roots 限制;Shell 命令使用启动 ForgeRelay 的本地用户权限执行,**Shell 不是 OS sandbox**。因此只连接你信任的 MCP Host,只开放确实需要的项目目录,并保护好 Owner password。
|
|
122
127
|
|
|
128
|
+
Allowed roots 是文件系统 / Workspace authority,不等于 Project Trust approval。v1.2.0 已把项目中的可执行配置统一接入 Project execution trust seam,但首个稳定策略是 compatibility-allow;完整的交互式 Project Trust approval UI 不是 v1.2.0 已实现功能。
|
|
129
|
+
|
|
123
130
|
ForgeRelay 默认拒绝 elevated / administrator 启动。只有你显式选择高权限运行时才会继续,并会提示系统级修改可能不可逆。
|
|
124
131
|
|
|
125
132
|
完整边界见 [安全模型](https://github.com/Akira-TL/forgerelay/wiki/Security)。
|
|
@@ -173,8 +180,13 @@ To see the configuration and Command Shell Runtime ForgeRelay actually resolved:
|
|
|
173
180
|
|
|
174
181
|
```bash
|
|
175
182
|
forgerelay doctor
|
|
183
|
+
forgerelay config check
|
|
184
|
+
forgerelay config sources
|
|
185
|
+
forgerelay config explain <logical-path>
|
|
176
186
|
```
|
|
177
187
|
|
|
188
|
+
Config System v2 in v1.2 resolves User, Project, and machine-private Project Local sources using `runtime > project-local > project > user > built-in`. External MCP, Language Servers, Hooks, and Subagent Profiles refresh on demand with last-known-good protection; restart-required General settings keep configured and applied values distinct and never auto-restart the server. Canonical JSON can point at the packaged `schemas/v1/*` `$schema` files, supported sensitive fields may use `${ENV_NAME}`, and diagnostics do not expose resolved secrets. Startup never rewrites legacy configuration automatically; preview an explicit migration with `forgerelay config migrate --dry-run ...` first.
|
|
189
|
+
|
|
178
190
|
### Host cannot reach localhost?
|
|
179
191
|
|
|
180
192
|
Cloud-hosted MCP clients need a public HTTPS route to ForgeRelay. Cloudflare Tunnel, ngrok, Tailscale Funnel, or a normal reverse proxy all work.
|
|
@@ -234,6 +246,8 @@ ForgeRelay gives an Agent real local execution capability.
|
|
|
234
246
|
|
|
235
247
|
Filesystem tools are constrained by the opened Workspace and configured allowed roots. Shell commands run with the authority of the local user running ForgeRelay; **the shell is not an OS sandbox**. Connect only MCP hosts you trust, expose only project roots you want an Agent to access, and keep the Owner password private.
|
|
236
248
|
|
|
249
|
+
Allowed roots grant filesystem/Workspace authority; they are not a Project Trust approval. v1.2.0 routes executable project configuration through a shared Project execution trust seam, but its initial policy is compatibility-allow. A full interactive Project Trust approval UI is not claimed for v1.2.0.
|
|
250
|
+
|
|
237
251
|
Elevated / administrator startup is rejected by default. It only proceeds after explicit opt-in, with a warning that system-level AI-driven changes may be irreversible.
|
|
238
252
|
|
|
239
253
|
See the [Security model](https://github.com/Akira-TL/forgerelay/wiki/Security) for the full boundary.
|
|
@@ -70,7 +70,7 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
|
|
|
70
70
|
},
|
|
71
71
|
}, async ({ workspaceId }, extra) => {
|
|
72
72
|
const requestContext = mcpHandlerRequestContext(extra);
|
|
73
|
-
const workspace = workspacePanelState?.(workspaceId);
|
|
73
|
+
const workspace = await workspacePanelState?.(workspaceId);
|
|
74
74
|
if (!workspace) {
|
|
75
75
|
throw new Error(`No Workspace presentation is available for ${workspaceId}. Call open_workspace for that workspace before activity_panel.`);
|
|
76
76
|
}
|
|
@@ -82,7 +82,7 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
|
|
|
82
82
|
_meta: {
|
|
83
83
|
...(relayed._meta ?? {}),
|
|
84
84
|
[ACTIVITY_PANEL_DEFAULT_EXPANDED_META_KEY]: panelDefaultExpanded,
|
|
85
|
-
[ACTIVITY_PANEL_WORKSPACE_META_KEY]: workspace,
|
|
85
|
+
[ACTIVITY_PANEL_WORKSPACE_META_KEY]: mergeRelayedWorkspaceConfiguration(workspace, relayed),
|
|
86
86
|
},
|
|
87
87
|
};
|
|
88
88
|
}
|
|
@@ -124,7 +124,7 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
|
|
|
124
124
|
const conversationScopeId = hostConversationScopeId(requestContext, connectionScopeId);
|
|
125
125
|
const relayed = await relay?.snapshot({ turnId, workspaceId, knownRevision }, conversationScopeId);
|
|
126
126
|
if (relayed) {
|
|
127
|
-
const workspace = workspaceId ? workspacePanelState?.(workspaceId) : undefined;
|
|
127
|
+
const workspace = workspaceId ? await workspacePanelState?.(workspaceId) : undefined;
|
|
128
128
|
if (workspaceId && !workspace) {
|
|
129
129
|
throw new Error(`No Workspace presentation is available for ${workspaceId}.`);
|
|
130
130
|
}
|
|
@@ -134,7 +134,7 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
|
|
|
134
134
|
_meta: {
|
|
135
135
|
...(relayed._meta ?? {}),
|
|
136
136
|
[ACTIVITY_PANEL_DEFAULT_EXPANDED_META_KEY]: panelDefaultExpanded,
|
|
137
|
-
[ACTIVITY_PANEL_WORKSPACE_META_KEY]: workspace,
|
|
137
|
+
[ACTIVITY_PANEL_WORKSPACE_META_KEY]: mergeRelayedWorkspaceConfiguration(workspace, relayed),
|
|
138
138
|
},
|
|
139
139
|
}
|
|
140
140
|
: relayed;
|
|
@@ -155,7 +155,7 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
|
|
|
155
155
|
transportSessionIdPrefix: transportSessionIdPrefix(requestContext.transportSessionId),
|
|
156
156
|
});
|
|
157
157
|
}
|
|
158
|
-
const workspace = workspaceId ? workspacePanelState?.(workspaceId) : undefined;
|
|
158
|
+
const workspace = workspaceId ? await workspacePanelState?.(workspaceId) : undefined;
|
|
159
159
|
if (workspaceId && !workspace) {
|
|
160
160
|
throw new Error(`No Workspace presentation is available for ${workspaceId}.`);
|
|
161
161
|
}
|
|
@@ -288,3 +288,13 @@ export function registerActivityQueryTools(server, queries, connectionScopeId, p
|
|
|
288
288
|
};
|
|
289
289
|
});
|
|
290
290
|
}
|
|
291
|
+
function mergeRelayedWorkspaceConfiguration(workspace, relayed) {
|
|
292
|
+
const meta = relayed._meta;
|
|
293
|
+
if (!meta || typeof meta !== "object")
|
|
294
|
+
return workspace;
|
|
295
|
+
const relayedWorkspace = meta[ACTIVITY_PANEL_WORKSPACE_META_KEY];
|
|
296
|
+
if (!relayedWorkspace || typeof relayedWorkspace !== "object" || Array.isArray(relayedWorkspace))
|
|
297
|
+
return workspace;
|
|
298
|
+
const configuration = relayedWorkspace.configuration;
|
|
299
|
+
return configuration === undefined ? workspace : { ...workspace, configuration };
|
|
300
|
+
}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { join, resolve } from "node:path";
|
|
2
|
+
import { forgerelayConfigDir } from "../../runtime/config/user-config.js";
|
|
3
|
+
import { ExternalMcpConfigRegistry } from "../../runtime/config/external-mcp-registry.js";
|
|
4
|
+
import { resolveGeneralConfig } from "../../runtime/config/resolution/general.js";
|
|
5
|
+
import { resolveHooksConfig } from "../../runtime/config/resolution/hooks.js";
|
|
6
|
+
import { resolveLanguageServersConfig } from "../../runtime/config/resolution/language-servers.js";
|
|
7
|
+
import { readJsonConfigSource } from "../../runtime/config/resolution/project-sources.js";
|
|
8
|
+
import { ConfigSourceRuntime } from "../../runtime/config/runtime/source-refresh.js";
|
|
9
|
+
import { resolveSubagentProfilesConfigSources } from "../../subagents/profiles.js";
|
|
10
|
+
import { ProjectContextResolver } from "../../workspaces/state/project-context.js";
|
|
11
|
+
const OFFLINE_LIVE_STATE = {
|
|
12
|
+
mode: "offline",
|
|
13
|
+
lastKnownGood: "unknown",
|
|
14
|
+
appliedValues: "unknown",
|
|
15
|
+
};
|
|
16
|
+
export async function runConfigInspection(args) {
|
|
17
|
+
let options;
|
|
18
|
+
try {
|
|
19
|
+
options = parseInspectionArgs(args);
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
23
|
+
return 2;
|
|
24
|
+
}
|
|
25
|
+
let domains;
|
|
26
|
+
try {
|
|
27
|
+
domains = await resolveInspectionDomains(options);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
31
|
+
return 2;
|
|
32
|
+
}
|
|
33
|
+
const diagnostics = domains.flatMap((domain) => [
|
|
34
|
+
...domain.diagnostics.map((diagnostic) => inspectionDiagnostic(domain.domain, diagnostic)),
|
|
35
|
+
...shadowDiagnostics(domain),
|
|
36
|
+
]);
|
|
37
|
+
const summary = summarizeDiagnostics(diagnostics);
|
|
38
|
+
if (options.command === "explain") {
|
|
39
|
+
const match = findEntry(domains, options.logicalPath);
|
|
40
|
+
if (!match) {
|
|
41
|
+
console.error(`Unknown configuration logical path: ${options.logicalPath}.`);
|
|
42
|
+
return 2;
|
|
43
|
+
}
|
|
44
|
+
const relatedSourceIds = new Set([
|
|
45
|
+
match.entry.effective.source.id,
|
|
46
|
+
...match.entry.shadowed.map((shadowed) => shadowed.source.id),
|
|
47
|
+
]);
|
|
48
|
+
const relatedDiagnostics = diagnostics.filter((diagnostic) => diagnostic.logicalPath === options.logicalPath || relatedSourceIds.has(diagnostic.source.id));
|
|
49
|
+
const output = {
|
|
50
|
+
version: 1,
|
|
51
|
+
command: "explain",
|
|
52
|
+
scope: options.scope,
|
|
53
|
+
liveState: OFFLINE_LIVE_STATE,
|
|
54
|
+
logicalPath: options.logicalPath,
|
|
55
|
+
domain: match.domain.domain,
|
|
56
|
+
effective: match.entry.effective,
|
|
57
|
+
shadowed: match.entry.shadowed,
|
|
58
|
+
tombstone: match.entry.tombstone === true,
|
|
59
|
+
diagnostics: relatedDiagnostics,
|
|
60
|
+
};
|
|
61
|
+
if (options.json)
|
|
62
|
+
console.log(JSON.stringify(output, null, 2));
|
|
63
|
+
else
|
|
64
|
+
printExplain(output);
|
|
65
|
+
return relatedDiagnostics.some((diagnostic) => diagnostic.severity === "error") ? 1 : 0;
|
|
66
|
+
}
|
|
67
|
+
if (options.command === "sources") {
|
|
68
|
+
const output = {
|
|
69
|
+
version: 1,
|
|
70
|
+
command: "sources",
|
|
71
|
+
scope: options.scope,
|
|
72
|
+
liveState: OFFLINE_LIVE_STATE,
|
|
73
|
+
summary,
|
|
74
|
+
diagnostics,
|
|
75
|
+
sources: inspectSources(domains),
|
|
76
|
+
};
|
|
77
|
+
if (options.json)
|
|
78
|
+
console.log(JSON.stringify(output, null, 2));
|
|
79
|
+
else
|
|
80
|
+
printSources(output);
|
|
81
|
+
return summary.errors > 0 ? 1 : 0;
|
|
82
|
+
}
|
|
83
|
+
const output = {
|
|
84
|
+
version: 1,
|
|
85
|
+
command: "check",
|
|
86
|
+
scope: options.scope,
|
|
87
|
+
liveState: OFFLINE_LIVE_STATE,
|
|
88
|
+
summary,
|
|
89
|
+
diagnostics,
|
|
90
|
+
};
|
|
91
|
+
if (options.json)
|
|
92
|
+
console.log(JSON.stringify(output, null, 2));
|
|
93
|
+
else
|
|
94
|
+
printCheck(output);
|
|
95
|
+
return summary.errors > 0 ? 1 : 0;
|
|
96
|
+
}
|
|
97
|
+
function parseInspectionArgs(args) {
|
|
98
|
+
const [command, ...rest] = args;
|
|
99
|
+
if (command !== "check" && command !== "sources" && command !== "explain") {
|
|
100
|
+
throw new Error("Expected config check, config sources, or config explain <logical-path>.");
|
|
101
|
+
}
|
|
102
|
+
let json = false;
|
|
103
|
+
let global = false;
|
|
104
|
+
let projectRoot;
|
|
105
|
+
let logicalPath;
|
|
106
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
107
|
+
const arg = rest[index];
|
|
108
|
+
if (arg === "--json") {
|
|
109
|
+
if (json)
|
|
110
|
+
throw new Error("--json may only be supplied once.");
|
|
111
|
+
json = true;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (arg === "--global") {
|
|
115
|
+
if (global || projectRoot)
|
|
116
|
+
throw new Error("--global and --project cannot be used together or repeated.");
|
|
117
|
+
global = true;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (arg === "--project") {
|
|
121
|
+
if (global || projectRoot)
|
|
122
|
+
throw new Error("--global and --project cannot be used together or repeated.");
|
|
123
|
+
const value = rest[index + 1];
|
|
124
|
+
if (!value || value.startsWith("--"))
|
|
125
|
+
throw new Error("--project requires a project path.");
|
|
126
|
+
projectRoot = resolve(value);
|
|
127
|
+
index += 1;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (command === "explain" && logicalPath === undefined && !arg.startsWith("--")) {
|
|
131
|
+
logicalPath = arg;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
throw new Error(`Unknown config ${command} option: ${arg}`);
|
|
135
|
+
}
|
|
136
|
+
if (command === "explain" && !logicalPath)
|
|
137
|
+
throw new Error("config explain requires a logical path.");
|
|
138
|
+
return {
|
|
139
|
+
command,
|
|
140
|
+
json,
|
|
141
|
+
scope: global
|
|
142
|
+
? { mode: "global" }
|
|
143
|
+
: { mode: "project", projectRoot: projectRoot ?? resolve(process.env.FORGERELAY_WORKSPACE_ROOT ?? process.cwd()) },
|
|
144
|
+
...(logicalPath ? { logicalPath } : {}),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
async function resolveInspectionDomains(options) {
|
|
148
|
+
const configDir = forgerelayConfigDir();
|
|
149
|
+
const sourceRuntime = new ConfigSourceRuntime();
|
|
150
|
+
const userSource = await readJsonConfigSource({
|
|
151
|
+
id: "user:config",
|
|
152
|
+
scope: "user",
|
|
153
|
+
location: join(configDir, "config.json"),
|
|
154
|
+
});
|
|
155
|
+
const project = options.scope.mode === "project"
|
|
156
|
+
? await new ProjectContextResolver(configDir).inspect(options.scope.projectRoot)
|
|
157
|
+
: undefined;
|
|
158
|
+
const projectSource = project
|
|
159
|
+
? await readJsonConfigSource({
|
|
160
|
+
id: "project:config",
|
|
161
|
+
scope: "project",
|
|
162
|
+
location: join(project.sharedConfigDir, "config.json"),
|
|
163
|
+
})
|
|
164
|
+
: undefined;
|
|
165
|
+
const projectLocalSource = project?.localConfigDir
|
|
166
|
+
? await readJsonConfigSource({
|
|
167
|
+
id: "project-local:config",
|
|
168
|
+
scope: "project-local",
|
|
169
|
+
location: join(project.localConfigDir, "config.json"),
|
|
170
|
+
})
|
|
171
|
+
: undefined;
|
|
172
|
+
const general = resolveGeneralConfig({
|
|
173
|
+
env: process.env,
|
|
174
|
+
...(userSource ? { userSource } : {}),
|
|
175
|
+
...(projectSource ? { projectSource } : {}),
|
|
176
|
+
...(projectLocalSource ? { projectLocalSource } : {}),
|
|
177
|
+
});
|
|
178
|
+
const mcp = new ExternalMcpConfigRegistry({
|
|
179
|
+
configDir,
|
|
180
|
+
environment: process.env,
|
|
181
|
+
sourceRuntime,
|
|
182
|
+
}).resolveConfiguration({
|
|
183
|
+
...(project ? { projectSharedConfigDir: project.sharedConfigDir } : {}),
|
|
184
|
+
...(project?.localConfigDir ? { projectLocalConfigDir: project.localConfigDir } : {}),
|
|
185
|
+
});
|
|
186
|
+
const languageServers = await resolveLanguageServersConfig({
|
|
187
|
+
configDir,
|
|
188
|
+
environment: process.env,
|
|
189
|
+
sourceRuntime,
|
|
190
|
+
...(project?.localConfigDir
|
|
191
|
+
? { project: { sharedConfigDir: project.sharedConfigDir, localConfigDir: project.localConfigDir } }
|
|
192
|
+
: project
|
|
193
|
+
? { projectSharedConfigDir: project.sharedConfigDir }
|
|
194
|
+
: {}),
|
|
195
|
+
});
|
|
196
|
+
const hooks = await resolveHooksConfig({
|
|
197
|
+
configDir,
|
|
198
|
+
sourceRuntime,
|
|
199
|
+
...(project?.localConfigDir
|
|
200
|
+
? { project: { sharedConfigDir: project.sharedConfigDir, localConfigDir: project.localConfigDir } }
|
|
201
|
+
: project
|
|
202
|
+
? { projectSharedConfigDir: project.sharedConfigDir }
|
|
203
|
+
: {}),
|
|
204
|
+
});
|
|
205
|
+
const subagents = resolveSubagentProfilesConfigSources({
|
|
206
|
+
configDir,
|
|
207
|
+
sourceRuntime,
|
|
208
|
+
...(project ? { projectSharedConfigDir: project.sharedConfigDir } : {}),
|
|
209
|
+
...(project?.localConfigDir ? { projectLocalConfigDir: project.localConfigDir } : {}),
|
|
210
|
+
});
|
|
211
|
+
return [general, mcp, languageServers, hooks, subagents];
|
|
212
|
+
}
|
|
213
|
+
function shadowDiagnostics(domain) {
|
|
214
|
+
return Object.values(domain.entries).flatMap((entry) => entry.shadowed.map((shadowed) => ({
|
|
215
|
+
severity: shadowed.reason === "source-shadowed" ? "warning" : "info",
|
|
216
|
+
code: shadowed.reason === "source-shadowed" ? "source_shadowed" : "shadowed_value",
|
|
217
|
+
domain: domain.domain,
|
|
218
|
+
logicalPath: entry.logicalPath,
|
|
219
|
+
source: shadowed.source,
|
|
220
|
+
message: shadowed.reason === "source-shadowed"
|
|
221
|
+
? `${entry.logicalPath} from ${shadowed.source.id} is suppressed by a canonical compatibility source.`
|
|
222
|
+
: `${entry.logicalPath} from ${shadowed.source.id} is shadowed by ${entry.effective.source.id} (${shadowed.reason}).`,
|
|
223
|
+
})));
|
|
224
|
+
}
|
|
225
|
+
function inspectionDiagnostic(domain, diagnostic) {
|
|
226
|
+
return {
|
|
227
|
+
severity: diagnostic.severity,
|
|
228
|
+
code: diagnostic.code,
|
|
229
|
+
domain,
|
|
230
|
+
...(diagnostic.logicalPath ? { logicalPath: diagnostic.logicalPath } : {}),
|
|
231
|
+
source: diagnostic.source,
|
|
232
|
+
message: diagnostic.message,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function summarizeDiagnostics(diagnostics) {
|
|
236
|
+
return {
|
|
237
|
+
errors: diagnostics.filter((diagnostic) => diagnostic.severity === "error").length,
|
|
238
|
+
warnings: diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length,
|
|
239
|
+
info: diagnostics.filter((diagnostic) => diagnostic.severity === "info").length,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
function findEntry(domains, logicalPath) {
|
|
243
|
+
for (const domain of domains) {
|
|
244
|
+
const entry = Object.values(domain.entries).find((candidate) => candidate.logicalPath === logicalPath);
|
|
245
|
+
if (entry)
|
|
246
|
+
return { domain, entry };
|
|
247
|
+
}
|
|
248
|
+
return undefined;
|
|
249
|
+
}
|
|
250
|
+
function inspectSources(domains) {
|
|
251
|
+
return domains.flatMap((domain) => {
|
|
252
|
+
const roles = new Map();
|
|
253
|
+
const roleSet = (id) => {
|
|
254
|
+
const existing = roles.get(id);
|
|
255
|
+
if (existing)
|
|
256
|
+
return existing;
|
|
257
|
+
const created = new Set();
|
|
258
|
+
roles.set(id, created);
|
|
259
|
+
return created;
|
|
260
|
+
};
|
|
261
|
+
for (const entry of Object.values(domain.entries)) {
|
|
262
|
+
roleSet(entry.effective.source.id).add("effective");
|
|
263
|
+
for (const shadowed of entry.shadowed)
|
|
264
|
+
roleSet(shadowed.source.id).add("shadowed");
|
|
265
|
+
}
|
|
266
|
+
for (const diagnostic of domain.diagnostics)
|
|
267
|
+
roleSet(diagnostic.source.id).add("diagnostic");
|
|
268
|
+
return domain.sources.flatMap((source) => {
|
|
269
|
+
const sourceRoles = roleSet(source.id);
|
|
270
|
+
if (source.id.startsWith("legacy:"))
|
|
271
|
+
sourceRoles.add("legacy");
|
|
272
|
+
if (source.kind === "environment" || source.kind === "cli")
|
|
273
|
+
sourceRoles.add("runtime");
|
|
274
|
+
if (source.kind === "built-in")
|
|
275
|
+
sourceRoles.add("built-in");
|
|
276
|
+
const meaningful = source.kind === "file" || sourceRoles.has("effective") || sourceRoles.has("shadowed") || sourceRoles.has("diagnostic");
|
|
277
|
+
if (!meaningful)
|
|
278
|
+
return [];
|
|
279
|
+
const invalid = domain.diagnostics.some((diagnostic) => diagnostic.source.id === source.id && diagnostic.severity === "error");
|
|
280
|
+
return [{
|
|
281
|
+
domain: domain.domain,
|
|
282
|
+
id: source.id,
|
|
283
|
+
scope: source.scope,
|
|
284
|
+
kind: source.kind,
|
|
285
|
+
...(source.location ? { location: source.location } : {}),
|
|
286
|
+
priority: source.priority,
|
|
287
|
+
state: invalid ? "invalid" : "valid",
|
|
288
|
+
roles: [...sourceRoles].sort(),
|
|
289
|
+
}];
|
|
290
|
+
});
|
|
291
|
+
}).sort((left, right) => left.domain.localeCompare(right.domain) || left.id.localeCompare(right.id));
|
|
292
|
+
}
|
|
293
|
+
function printExplain(output) {
|
|
294
|
+
console.log(`Config explain ${output.logicalPath}`);
|
|
295
|
+
console.log(`Effective source: ${output.effective.source.id}`);
|
|
296
|
+
console.log(`Configured value: ${formatValue(output.effective.configuredValue)}`);
|
|
297
|
+
console.log(`Effective value: ${formatValue(output.effective.effectiveValue)}`);
|
|
298
|
+
console.log(`Reload: ${output.effective.reload}`);
|
|
299
|
+
console.log(`Execution effect: ${output.effective.executionEffect}`);
|
|
300
|
+
for (const shadowed of output.shadowed) {
|
|
301
|
+
console.log(`Shadowed: ${shadowed.source.id} (${shadowed.reason})`);
|
|
302
|
+
}
|
|
303
|
+
if (output.diagnostics.length > 0) {
|
|
304
|
+
console.log("Diagnostics:");
|
|
305
|
+
for (const diagnostic of output.diagnostics) {
|
|
306
|
+
const location = diagnostic.source.location ?? diagnostic.source.id;
|
|
307
|
+
console.log(` ${diagnostic.severity.toUpperCase()} ${diagnostic.code} ${location}: ${diagnostic.message}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
console.log("Live runtime state: unknown (offline inspection; in-memory last-known-good and running applied values are unavailable).");
|
|
311
|
+
}
|
|
312
|
+
function formatValue(value) {
|
|
313
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
314
|
+
}
|
|
315
|
+
function printSources(output) {
|
|
316
|
+
console.log(`Config sources (${output.scope.mode})`);
|
|
317
|
+
for (const source of output.sources) {
|
|
318
|
+
console.log(`${source.domain} ${source.id} · ${source.state} · ${source.roles.join(", ")}` +
|
|
319
|
+
(source.location ? ` · ${source.location}` : ""));
|
|
320
|
+
}
|
|
321
|
+
console.log("Live runtime state: unknown (offline inspection; in-memory last-known-good and running applied values are unavailable).");
|
|
322
|
+
}
|
|
323
|
+
function printCheck(output) {
|
|
324
|
+
console.log(`Config check (${output.scope.mode})`);
|
|
325
|
+
for (const diagnostic of output.diagnostics) {
|
|
326
|
+
const location = diagnostic.source.location ?? diagnostic.source.id;
|
|
327
|
+
console.log(`${diagnostic.severity.toUpperCase()} ${diagnostic.domain} ${location}: ${diagnostic.message}`);
|
|
328
|
+
}
|
|
329
|
+
console.log(`Summary: ${output.summary.errors} error(s), ${output.summary.warnings} warning(s), ${output.summary.info} info`);
|
|
330
|
+
console.log("Live runtime state: unknown (offline inspection; in-memory last-known-good and running applied values are unavailable).");
|
|
331
|
+
}
|