@akira-tl/forgerelay 0.3.3 → 0.3.5
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 +24 -0
- package/README.md +2 -2
- package/capabilities/artifacts-review/GUIDE.md +2 -4
- package/capabilities/managed-worktrees/GUIDE.md +12 -7
- package/capabilities/shell-processes/GUIDE.md +10 -8
- package/dist/artifact-tools.js +0 -128
- package/dist/capabilities.js +4 -7
- package/dist/hooks.js +12 -0
- package/dist/mcp/server-instructions.js +7 -13
- package/dist/review-checkpoints.js +4 -4
- package/dist/server.js +217 -365
- package/dist/ui/.vite/manifest.json +280 -280
- package/dist/ui/assets/{angular-html-CQmCUfkv.js → angular-html-BT2Z6jee.js} +1 -1
- package/dist/ui/assets/{angular-ts-BSbfxpMS.js → angular-ts-BwPE8YDt.js} +1 -1
- package/dist/ui/assets/{apl-CWCaH-E3.js → apl-CGbONVvU.js} +1 -1
- package/dist/ui/assets/{astro-R_p4G0xA.js → astro-DIAPy7cj.js} +1 -1
- package/dist/ui/assets/{blade-irHensDY.js → blade-Cgtd19Ky.js} +1 -1
- package/dist/ui/assets/{c-_XEQUoq6.js → c-acnvZ8tm.js} +1 -1
- package/dist/ui/assets/{cobol-Css6VQjJ.js → cobol-G-7HLVKW.js} +1 -1
- package/dist/ui/assets/{coffee-CpXUkyDm.js → coffee-DQFyKxEB.js} +1 -1
- package/dist/ui/assets/{cpp-BQHsPWwI.js → cpp-UyrmHXan.js} +1 -1
- package/dist/ui/assets/{crystal-Cbv8J9Fm.js → crystal-DuiWhEO3.js} +1 -1
- package/dist/ui/assets/{css-D8DkrS3U.js → css-D_WCbY6m.js} +1 -1
- package/dist/ui/assets/{edge-bBL_XDAG.js → edge-COkalhFi.js} +1 -1
- package/dist/ui/assets/{elixir-CQjsM9sA.js → elixir-cdlzbYR1.js} +1 -1
- package/dist/ui/assets/{elm-DLFl-jEZ.js → elm-ZwDYuYpb.js} +1 -1
- package/dist/ui/assets/{erb-BJjsdB_Q.js → erb-C8WG9TtC.js} +1 -1
- package/dist/ui/assets/{git-rebase-CqP5A2s0.js → git-rebase-CMlDVJBT.js} +1 -1
- package/dist/ui/assets/{glimmer-js-B5Hh_7xe.js → glimmer-js-DeLOQlol.js} +1 -1
- package/dist/ui/assets/{glimmer-ts-oaeSAC8s.js → glimmer-ts-DTEM04TK.js} +1 -1
- package/dist/ui/assets/{glsl-BylkOpcF.js → glsl-IJV4SB-M.js} +1 -1
- package/dist/ui/assets/{graphql-DsVRvP6S.js → graphql-PM-WDnoB.js} +1 -1
- package/dist/ui/assets/{hack-D4N5abT_.js → hack-C14sh6Lh.js} +1 -1
- package/dist/ui/assets/{haml-CIAJQIox.js → haml-B_5nS6Kx.js} +1 -1
- package/dist/ui/assets/{handlebars-DDEZyNXR.js → handlebars-eS-J0O-M.js} +1 -1
- package/dist/ui/assets/{heavy-payload-DafM8yjB.js → heavy-payload-BKB0QDOI.js} +1 -1
- package/dist/ui/assets/{html-zDy-3lvy.js → html-C3TPjrC5.js} +1 -1
- package/dist/ui/assets/{html-derivative-CZ91Y2d5.js → html-derivative-BlVZU2id.js} +1 -1
- package/dist/ui/assets/{http-ezaodZRf.js → http-BmbZzA5x.js} +1 -1
- package/dist/ui/assets/{hurl-BmL0o7TO.js → hurl-Dnd_YpOD.js} +1 -1
- package/dist/ui/assets/{java-Q2C0vwbH.js → java-ne9HdIVn.js} +1 -1
- package/dist/ui/assets/{javascript-UDhBkHE3.js → javascript-DpHT58rr.js} +1 -1
- package/dist/ui/assets/{jinja-BWRskcYS.js → jinja-DTvkZdHA.js} +1 -1
- package/dist/ui/assets/{jison-B18BY9Ds.js → jison-DUJG-tmh.js} +1 -1
- package/dist/ui/assets/{json-DNj7MpRr.js → json-DJmnBdxW.js} +1 -1
- package/dist/ui/assets/{jsx-CVpHJ6cr.js → jsx-BclJnZnN.js} +1 -1
- package/dist/ui/assets/{julia-CJBNBS9c.js → julia-P5B4pHFa.js} +1 -1
- package/dist/ui/assets/{just-Bmu2JCQT.js → just-DYaoh7C6.js} +1 -1
- package/dist/ui/assets/{latex-BB7F93KY.js → latex-CI5lnkWV.js} +1 -1
- package/dist/ui/assets/{liquid-Btnomc6E.js → liquid-D3LnAs_z.js} +1 -1
- package/dist/ui/assets/{lua-D0FYT9Vo.js → lua-dZC7ZQIY.js} +1 -1
- package/dist/ui/assets/{marko-MHLw0ZlK.js → marko-CRDrjqfS.js} +1 -1
- package/dist/ui/assets/{mdc-CSr-suGy.js → mdc-ZzwgmJVn.js} +1 -1
- package/dist/ui/assets/{nginx-ur1l6E-g.js → nginx-BAvFXGSV.js} +1 -1
- package/dist/ui/assets/{nim-B2k0IugA.js → nim-BHIFawe9.js} +1 -1
- package/dist/ui/assets/{perl-BqOHxFDn.js → perl-C_f5AnLa.js} +1 -1
- package/dist/ui/assets/{php-DzPK6O1_.js → php-DDqpegL5.js} +1 -1
- package/dist/ui/assets/{pug-D169OZkr.js → pug-CSaAZXyd.js} +1 -1
- package/dist/ui/assets/{qml-BtemJTC4.js → qml-CNHvRs_z.js} +1 -1
- package/dist/ui/assets/{r-BGVHu0bd.js → r-CAfoQND2.js} +1 -1
- package/dist/ui/assets/{razor-Diisjg1d.js → razor-BDfAPy7I.js} +1 -1
- package/dist/ui/assets/{regexp-B2F5vMZ6.js → regexp-Mug5jltQ.js} +1 -1
- package/dist/ui/assets/{review-payload-B-3I3Oj_.js → review-payload-D7GIdYDj.js} +1 -1
- package/dist/ui/assets/{rst-BxpeZwuT.js → rst-BzPP2gle.js} +1 -1
- package/dist/ui/assets/{ruby-DrCq4riN.js → ruby-CQjzokzX.js} +1 -1
- package/dist/ui/assets/{sas-CI_2W-1n.js → sas-Bp37KGr7.js} +1 -1
- package/dist/ui/assets/{scrollbar-DHsDF5w-.js → scrollbar-HUMBdLLK.js} +3 -3
- package/dist/ui/assets/{scss-BxtyHZ1p.js → scss-C_Z1DYnw.js} +1 -1
- package/dist/ui/assets/{shellscript-BY_YYrHT.js → shellscript-CLfJpn3I.js} +1 -1
- package/dist/ui/assets/{shellsession-C6uBrLmA.js → shellsession-CNc62UhY.js} +1 -1
- package/dist/ui/assets/{soy-BHM9gdPh.js → soy-CF44SpBo.js} +1 -1
- package/dist/ui/assets/{sql-XmhfEGz_.js → sql-CYnzfiQ1.js} +1 -1
- package/dist/ui/assets/{stata-BbHY7NEo.js → stata-DVeMA68W.js} +1 -1
- package/dist/ui/assets/{surrealql-CD49DZ3E.js → surrealql-52sF96Yv.js} +1 -1
- package/dist/ui/assets/{svelte-CbYsEYJY.js → svelte-ydwT7UbF.js} +1 -1
- package/dist/ui/assets/{templ-B5JoUxEB.js → templ-BXcoVoGh.js} +1 -1
- package/dist/ui/assets/{tex-DZcOfTZK.js → tex-COsYFp7O.js} +1 -1
- package/dist/ui/assets/{ts-tags-BeU-xqc0.js → ts-tags-CF--LE1s.js} +1 -1
- package/dist/ui/assets/{tsx-V7W1xGl3.js → tsx-DLBt2-2f.js} +1 -1
- package/dist/ui/assets/{twig-MIaJo5BU.js → twig-YoM-DiI5.js} +1 -1
- package/dist/ui/assets/{typescript-C0WESuVc.js → typescript-Ce9JGkNI.js} +1 -1
- package/dist/ui/assets/{vue-eJHwc9ra.js → vue-B5LDEy4l.js} +1 -1
- package/dist/ui/assets/{vue-html-CLmBujTU.js → vue-html-CPdGGk4T.js} +1 -1
- package/dist/ui/assets/{vue-vine-BP5kjIGb.js → vue-vine-B5bjgk0a.js} +1 -1
- package/dist/ui/assets/{workspace-app-C1qR5cAY.js → workspace-app-Cm9EX5x4.js} +3 -3
- package/dist/ui/assets/{xml-j7pmyP48.js → xml-Cun8xb0T.js} +1 -1
- package/dist/ui/assets/{xsl-5VFHbg6o.js → xsl-wls_CEK_.js} +1 -1
- package/dist/ui/assets/{yaml-DxFAt-KU.js → yaml-BxuXn9OL.js} +1 -1
- package/dist/ui/workspace-app.html +1 -1
- package/docs/artifact-exchange.md +4 -3
- package/docs/chatgpt-coding-workflow.md +31 -27
- package/docs/configuration.md +27 -20
- package/docs/debugging.md +3 -3
- package/docs/gotchas.md +9 -8
- package/docs/security.md +6 -5
- package/package.json +1 -1
- package/scripts/debug/accept.mjs +80 -25
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,30 @@ All notable ForgeRelay changes are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.3.5] - 2026-08-10
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- Regular `minimal` and `full` MCP modes now expose the same canonical 9-tool surface: `open_workspace`, `capability`, `close_workspace`, `read`, `write`, `edit`, `rename`, `delete`, and `bash`. Search and directory inspection use shell commands through `bash`; `full` remains only as a configuration-compatibility value.
|
|
12
|
+
- `review.changes` and `artifact.download` are now Capability Gateway-only workflows. Their dedicated top-level compatibility aliases have been removed, while review cards identify themselves as `capability` results with `capabilityName="review.changes"`.
|
|
13
|
+
- Capability fingerprints now report semantic runtime capabilities such as `review.changes` instead of implementation-shaped search/review tool names, and current Guides/docs/bootstrap instructions use the final Core Surface + Capability Gateway model.
|
|
14
|
+
|
|
15
|
+
### Removed
|
|
16
|
+
|
|
17
|
+
- Removed the regular dedicated `grep`, `glob`, `ls`, aggregate-review, and native-artifact MCP adapters after their canonical workflows moved to `bash` or the Capability Gateway.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Successful `report: true` lifecycle Hook reports are now mirrored into the model-readable structured result as well as MCP text content, so Hosts that surface only structured tool results still expose the Hook outcome to the Agent.
|
|
22
|
+
|
|
23
|
+
## [0.3.4] - 2026-08-10
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- Regular MCP tool modes now use one `bash` interface for both command start and long-process interaction. `action="run"` preserves normal shell execution while `action="process"` polls/waits, writes input, resizes PTYs, or interrupts an existing workspace-owned `processId`; top-level `write_stdin` is no longer exposed in regular modes.
|
|
28
|
+
- `close_workspace` is now the single public Workspace close operation. Checkout-backed workspaces release their logical handle, while managed-worktree-backed workspaces require `commitMessage` and run the existing Hook/commit/fast-forward/cleanup lifecycle; top-level `close_worktree` is no longer exposed.
|
|
29
|
+
- Shell/process and managed-worktree Capability Guides, Host instructions, configuration docs, debugging acceptance, and workflow docs now use the unified process and Workspace lifecycle model.
|
|
30
|
+
|
|
7
31
|
## [0.3.3] - 2026-08-10
|
|
8
32
|
|
|
9
33
|
### Added
|
package/README.md
CHANGED
|
@@ -122,7 +122,7 @@ git worktree list
|
|
|
122
122
|
git branch
|
|
123
123
|
```
|
|
124
124
|
|
|
125
|
-
When `
|
|
125
|
+
When `close_workspace` succeeds for a managed-worktree-backed workspace, ForgeRelay:
|
|
126
126
|
|
|
127
127
|
1. checks that the source checkout is clean and still on the expected target branch;
|
|
128
128
|
2. commits any remaining worktree changes;
|
|
@@ -145,7 +145,7 @@ Hook 是 ForgeRelay 的自动生命周期规则。首选方式是一个 Hook 一
|
|
|
145
145
|
"event": "BeforeTool",
|
|
146
146
|
"matcher": {
|
|
147
147
|
"tool": "bash",
|
|
148
|
-
"commandRegex": "
|
|
148
|
+
"commandRegex": "git\\s+push\\s+origin\\s+v\\d+\\.\\d+\\.\\d+"
|
|
149
149
|
},
|
|
150
150
|
"command": "npm run release:verify",
|
|
151
151
|
"timeoutSeconds": 300,
|
|
@@ -12,8 +12,6 @@ Native artifact download 默认关闭,且当前安全发布实现主要面向
|
|
|
12
12
|
2. 调用 `capability`,`name="artifact.download"`、`action="run"`,把 Host 原样提供的 native `file` 值放在 Gateway 顶层 `file`,并在 `arguments.path` 传 workspace-relative 目标路径;
|
|
13
13
|
3. 成功后使用返回的 normalized relative path 配合普通 `read` / `rename` / `delete` / shell 等工具继续处理。
|
|
14
14
|
|
|
15
|
-
0.3.3 仍保留 `download_artifact` 作为迁移期 compatibility alias,但新的 Agent workflow 不应把它视为长期 public surface。
|
|
16
|
-
|
|
17
15
|
`artifact.download` 会创建缺失的父目录,但拒绝覆盖已有目标。不要把 native file 值改造成:
|
|
18
16
|
|
|
19
17
|
- 任意 URL 字符串;
|
|
@@ -35,10 +33,10 @@ Native artifact download 默认关闭,且当前安全发布实现主要面向
|
|
|
35
33
|
|
|
36
34
|
Plain MCP Host 可以忽略这些 UI metadata;不要把 UI 是否显示当成文件操作是否成功的判据。
|
|
37
35
|
|
|
38
|
-
当 Capability catalog 广告 `review.changes` 时,本轮成功修改文件后,在最后一个相关 file mutation 之后、final response 之前通过 `capability` 调用一次,让用户看到聚合 diff;不要每改一个文件就调用一次。
|
|
36
|
+
当 Capability catalog 广告 `review.changes` 时,本轮成功修改文件后,在最后一个相关 file mutation 之后、final response 之前通过 `capability` 调用一次,让用户看到聚合 diff;不要每改一个文件就调用一次。
|
|
39
37
|
|
|
40
38
|
`review.changes` 使用 Git-backed review checkpoint。它按 workspace 跟踪 open/baseline 状态,展示自上次 review checkpoint 以来的 coherent diff,并在成功 review 后推进 baseline。当前版本要求可用的 Git workspace;checkpoint 缺失或 root 不匹配时会明确失败或使用受控 fallback,而不是凭空重建历史。
|
|
41
39
|
|
|
42
40
|
## Capability ownership
|
|
43
41
|
|
|
44
|
-
Artifact transfer 和 change review 都属于 ForgeRelay registered capability,而不是 Agent 自己的文件搬运协议。`tools/list`
|
|
42
|
+
Artifact transfer 和 change review 都属于 ForgeRelay registered capability,而不是 Agent 自己的文件搬运协议。`tools/list` 只暴露稳定 `capability` Gateway;真正可用的低频能力以当前 workspace 的 Capability catalog 为准。本指南提供流程和边界,不额外创造隐藏执行入口。
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
## 基本模型
|
|
6
6
|
|
|
7
|
-
- `workspaceId`
|
|
7
|
+
- `workspaceId` 是 Agent 的工作身份;managed worktree 是该 Workspace 的一种物理 Git backing mode,不是 Host 需要管理的第二套 lifecycle。
|
|
8
8
|
- managed worktree 使用 ForgeRelay 管理的 `forgerelay/*` 分支,不使用 detached HEAD。
|
|
9
9
|
- 创建时会记录 source checkout、base ref/base SHA、managed branch 和 target branch。
|
|
10
|
-
- 同一个物理 worktree 可以存在多个逻辑 workspace handle
|
|
10
|
+
- 同一个物理 worktree 可以存在多个逻辑 workspace handle;finalize 一个 managed-worktree-backed Workspace 时,ForgeRelay 会统一处理同一物理 worktree 的 alias/session invalidation。
|
|
11
11
|
|
|
12
12
|
## 打开与复用
|
|
13
13
|
|
|
@@ -22,18 +22,23 @@
|
|
|
22
22
|
|
|
23
23
|
不要为了“更安全”自动选择 worktree,也不要在用户没有要求时创建额外 Git 分支。
|
|
24
24
|
|
|
25
|
-
## `close_workspace`
|
|
25
|
+
## `close_workspace`
|
|
26
26
|
|
|
27
|
-
`close_workspace`
|
|
27
|
+
`close_workspace` 是唯一公开关闭入口,行为由 Workspace backing mode 决定:
|
|
28
28
|
|
|
29
|
-
`
|
|
29
|
+
- checkout-backed Workspace:只释放逻辑 `workspaceId`,不会删除 checkout 文件;
|
|
30
|
+
- managed-worktree-backed Workspace:要求提供 `commitMessage`,并完成下面的安全 finalize lifecycle。
|
|
31
|
+
|
|
32
|
+
Managed worktree finalize:
|
|
30
33
|
|
|
31
34
|
1. 要求该 worktree 的工作已经完成并验证;
|
|
32
|
-
2. 若仍有未提交修改,ForgeRelay
|
|
35
|
+
2. 若仍有未提交修改,ForgeRelay 使用 `close_workspace` 提供的 commit message 提交;
|
|
33
36
|
3. 只有 source checkout 干净、目标历史没有分叉且能够安全 fast-forward 时,才把 managed branch 集成到原 target branch;
|
|
34
|
-
4. 成功后移除 worktree 目录和 ForgeRelay
|
|
37
|
+
4. 成功后移除 worktree 目录和 ForgeRelay 管理分支,并关闭该物理 worktree 的逻辑 aliases;
|
|
35
38
|
5. 若安全 fast-forward 不成立,不把 source checkout 留在 merge-conflict 状态,而是拒绝关闭并保留 worktree 供用户/Agent 处理。
|
|
36
39
|
|
|
40
|
+
如果因为缺少 `commitMessage`、dirty source、divergence、Hook blocking 或 busy process 关闭失败,修正对应条件后继续使用**原 workspaceId** 重试;不要另开一个 worktree 来逃避失败状态。
|
|
41
|
+
|
|
37
42
|
运行中的 process 或尚未消费的 process completion 也会阻止相关逻辑 workspace/worktree 被关闭。
|
|
38
43
|
|
|
39
44
|
## 外部变化与恢复
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# ForgeRelay Shell and Processes
|
|
2
2
|
|
|
3
|
-
当命令长时间运行、需要交互式 TTY
|
|
3
|
+
当命令长时间运行、需要交互式 TTY、需要继续操作已有 `processId`,或遇到 shell/process 平台边界问题时读取本指南。
|
|
4
4
|
|
|
5
5
|
## Core process model
|
|
6
6
|
|
|
7
|
-
`bash
|
|
7
|
+
常规 tool mode 使用一个 `bash` 入口管理命令和后续 process lifecycle;Codex tool mode 仍可使用其兼容 command adapter。命令拥有本地用户权限;workspace path containment 不等于 OS sandbox。
|
|
8
8
|
|
|
9
9
|
普通 `bash` 最多在前台等待 300 秒。如果进程仍存活,ForgeRelay 不会因为 wait window 到期而杀掉它,而是返回:
|
|
10
10
|
|
|
@@ -15,17 +15,19 @@ processId: <number>
|
|
|
15
15
|
|
|
16
16
|
`processId` 是 canonical process handle。旧 `sessionId` 仅为 0.2.x compatibility alias,不应作为新代码或新 Agent workflow 的首选名称。
|
|
17
17
|
|
|
18
|
-
##
|
|
18
|
+
## `bash(action="process")`
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
普通命令使用 `bash(action="run")`,其中 `action` 可省略;如果返回 `running: true` 和 `processId`,后续仍通过同一个 `bash` tool 操作该 process:
|
|
21
21
|
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
22
|
+
- 只传 `workspaceId`、`action="process"`、`processId`:poll / wait;
|
|
23
|
+
- `input`:向正在运行的进程写入字符;
|
|
24
|
+
- `interrupt: true`:显式发送 SIGINT / Ctrl-C;
|
|
25
25
|
- `yieldTimeMs`:继续等待,单次最多 300000 ms;
|
|
26
26
|
- `maxOutputTokens`:限制本次返回的近似输出 token;
|
|
27
27
|
- `columns` / `rows`:调整已经分配 PTY 的终端尺寸。
|
|
28
28
|
|
|
29
|
+
`action="run"` 与 `action="process"` 的参数不要混用。Process ownership 始终绑定原 `workspaceId`;未知或跨 workspace 的 `processId` 会被拒绝。
|
|
30
|
+
|
|
29
31
|
等待超时不会隐式 kill process。若没有必要立即等待,可以继续其他工作;进程完成后,ForgeRelay 会把 completion notice 一次性附加到同一 logical workspace 的后续 tool result。
|
|
30
32
|
|
|
31
33
|
不要因为暂时没有输出就重复启动相同长进程;先用返回的 `processId` poll。
|
|
@@ -42,7 +44,7 @@ rows: 24
|
|
|
42
44
|
|
|
43
45
|
PTY 依赖 optional `node-pty`。缺少该依赖时 ForgeRelay 会明确报错;不要把它误诊成命令本身失败。对非 PTY process 使用 `columns` / `rows` resize 也会失败。
|
|
44
46
|
|
|
45
|
-
对需要 prompt/REPL 的程序,用 `tty
|
|
47
|
+
对需要 prompt/REPL 的程序,用 `bash(action="run", tty=true)` 启动,再通过 `bash(action="process", processId=...)` 输入或 resize;对 tests/builds/formatters 等非交互命令保持默认非 PTY,以获得更稳定的 CI-style 输出。
|
|
46
48
|
|
|
47
49
|
## Platform notes
|
|
48
50
|
|
package/dist/artifact-tools.js
CHANGED
|
@@ -2,18 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { constants as fsConstants } from "node:fs";
|
|
3
3
|
import { link, lstat, mkdir, open, readdir, unlink, } from "node:fs/promises";
|
|
4
4
|
import { isAbsolute, join, normalize, sep } from "node:path";
|
|
5
|
-
import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
|
|
6
|
-
import * as z from "zod/v4";
|
|
7
5
|
import { ArtifactError } from "./artifact-error.js";
|
|
8
|
-
import { runToolWithHooks } from "./hooks.js";
|
|
9
|
-
import { describeIncomingArtifactValue, IncomingArtifactAdapterRegistry, } from "./incoming-artifacts.js";
|
|
10
|
-
import { logEvent, workspaceLogLabel } from "./logger.js";
|
|
11
|
-
const ARTIFACT_WRITE_ANNOTATIONS = {
|
|
12
|
-
readOnlyHint: false,
|
|
13
|
-
destructiveHint: false,
|
|
14
|
-
idempotentHint: false,
|
|
15
|
-
openWorldHint: true,
|
|
16
|
-
};
|
|
17
6
|
const NO_FOLLOW = fsConstants.O_NOFOLLOW ?? 0;
|
|
18
7
|
const DIRECTORY_FLAGS = fsConstants.O_RDONLY | (fsConstants.O_DIRECTORY ?? 0) | NO_FOLLOW;
|
|
19
8
|
const PARTIAL_PREFIX = ".forgerelay-download-";
|
|
@@ -21,64 +10,9 @@ const PARTIAL_SUFFIX = ".partial";
|
|
|
21
10
|
const STALE_PARTIAL_AGE_MS = 24 * 60 * 60 * 1_000;
|
|
22
11
|
const MAX_STALE_PARTIAL_CLEANUP = 32;
|
|
23
12
|
const ARTIFACT_DOWNLOAD_PLATFORMS = new Set(["linux"]);
|
|
24
|
-
const openAIFileReferenceInputSchema = z.strictObject({
|
|
25
|
-
download_url: z.string(),
|
|
26
|
-
file_id: z.string(),
|
|
27
|
-
mime_type: z.string().nullable().optional(),
|
|
28
|
-
file_name: z.string().nullable().optional(),
|
|
29
|
-
name: z.string().nullable().optional(),
|
|
30
|
-
size: z.number().int().nonnegative().nullable().optional(),
|
|
31
|
-
});
|
|
32
13
|
export function isArtifactDownloadSupportedPlatform(platform = process.platform) {
|
|
33
14
|
return ARTIFACT_DOWNLOAD_PLATFORMS.has(platform);
|
|
34
15
|
}
|
|
35
|
-
export function registerArtifactTools(server, { config, workspaces, hooks, incomingArtifactAdapters = [], incomingArtifactRegistry, }) {
|
|
36
|
-
const incomingRegistry = incomingArtifactRegistry
|
|
37
|
-
?? new IncomingArtifactAdapterRegistry(incomingArtifactAdapters);
|
|
38
|
-
registerAppTool(server, "download_artifact", {
|
|
39
|
-
title: "Download attached or generated file",
|
|
40
|
-
description: "Stream one MCP-host-provided native file to a requested relative path inside an already-open workspace. Existing destinations, arbitrary URLs, absolute paths, traversal, symlinked parents, local source paths, and malformed file objects are rejected.",
|
|
41
|
-
inputSchema: {
|
|
42
|
-
file: openAIFileReferenceInputSchema.describe("Native file value authorized and supplied by the MCP host."),
|
|
43
|
-
workspaceId: z.string().min(1).describe("Workspace identifier returned by open_workspace."),
|
|
44
|
-
path: z.string().min(1).describe("Relative destination path inside the selected workspace. The destination must not already exist."),
|
|
45
|
-
},
|
|
46
|
-
outputSchema: {
|
|
47
|
-
path: z.string(),
|
|
48
|
-
},
|
|
49
|
-
_meta: { "openai/fileParams": ["file"] },
|
|
50
|
-
annotations: ARTIFACT_WRITE_ANNOTATIONS,
|
|
51
|
-
}, async (input) => {
|
|
52
|
-
const workspace = workspaces.getWorkspace(input.workspaceId);
|
|
53
|
-
return runToolWithHooks(hooks, {
|
|
54
|
-
tool: "download_artifact",
|
|
55
|
-
invocation: {
|
|
56
|
-
workspaceId: workspace.id,
|
|
57
|
-
workspaceRoot: workspace.root,
|
|
58
|
-
workspaceMode: workspace.mode,
|
|
59
|
-
sourceRoot: workspace.sourceRoot,
|
|
60
|
-
},
|
|
61
|
-
payload: { path: input.path },
|
|
62
|
-
changedPaths: (result) => [result.structuredContent.path],
|
|
63
|
-
operation: () => executeArtifactTool(config, input, {
|
|
64
|
-
workspace: workspaceLogLabel(workspace.root, workspace.id),
|
|
65
|
-
}, async () => {
|
|
66
|
-
const downloaded = await downloadIncomingArtifact({
|
|
67
|
-
registry: incomingRegistry,
|
|
68
|
-
workspaceId: workspace.id,
|
|
69
|
-
workspaceRoot: workspace.root,
|
|
70
|
-
maxFileBytes: config.artifactMaxFileBytes,
|
|
71
|
-
file: input.file,
|
|
72
|
-
path: input.path,
|
|
73
|
-
});
|
|
74
|
-
return {
|
|
75
|
-
publicResult: { path: downloaded.path },
|
|
76
|
-
logResult: downloaded,
|
|
77
|
-
};
|
|
78
|
-
}),
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
16
|
/**
|
|
83
17
|
* Stream a trusted native file directly into one already-open workspace.
|
|
84
18
|
*
|
|
@@ -159,53 +93,6 @@ export async function downloadIncomingArtifact({ registry, workspaceId, workspac
|
|
|
159
93
|
await workspaceHandle?.close().catch(() => undefined);
|
|
160
94
|
}
|
|
161
95
|
}
|
|
162
|
-
export function artifactToolLogFields(input) {
|
|
163
|
-
return {
|
|
164
|
-
fileProvided: input.file !== undefined,
|
|
165
|
-
fileReferenceShape: describeIncomingArtifactValue(input.file),
|
|
166
|
-
downloadUrlHostname: incomingFileDownloadHostname(input.file),
|
|
167
|
-
workspaceId: input.workspaceId,
|
|
168
|
-
path: input.path,
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
async function executeArtifactTool(config, input, logContext, operation) {
|
|
172
|
-
const startedAt = performance.now();
|
|
173
|
-
try {
|
|
174
|
-
const { publicResult, logResult } = await operation();
|
|
175
|
-
if (config.logging.toolCalls) {
|
|
176
|
-
logEvent(config.logging, "info", "artifact_tool_call", {
|
|
177
|
-
tool: "download_artifact",
|
|
178
|
-
...artifactToolLogFields(input),
|
|
179
|
-
...logContext,
|
|
180
|
-
path: logResult.path,
|
|
181
|
-
size: logResult.size,
|
|
182
|
-
sha256: logResult.sha256,
|
|
183
|
-
success: true,
|
|
184
|
-
durationMs: Math.round(performance.now() - startedAt),
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
return artifactToolResponse(publicResult);
|
|
188
|
-
}
|
|
189
|
-
catch (error) {
|
|
190
|
-
if (config.logging.toolCalls) {
|
|
191
|
-
logEvent(config.logging, "warn", "artifact_tool_call", {
|
|
192
|
-
tool: "download_artifact",
|
|
193
|
-
...artifactToolLogFields(input),
|
|
194
|
-
...logContext,
|
|
195
|
-
success: false,
|
|
196
|
-
errorCode: error instanceof ArtifactError ? error.code : "internal_error",
|
|
197
|
-
durationMs: Math.round(performance.now() - startedAt),
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
throw error;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
function artifactToolResponse(result) {
|
|
204
|
-
return {
|
|
205
|
-
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
206
|
-
structuredContent: result,
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
96
|
async function openDirectoryNoFollow(path, code, message) {
|
|
210
97
|
let handle;
|
|
211
98
|
try {
|
|
@@ -356,21 +243,6 @@ async function writeAll(handle, buffer, position) {
|
|
|
356
243
|
offset += bytesWritten;
|
|
357
244
|
}
|
|
358
245
|
}
|
|
359
|
-
function incomingFileDownloadHostname(value) {
|
|
360
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
361
|
-
return undefined;
|
|
362
|
-
}
|
|
363
|
-
const rawUrl = value.download_url;
|
|
364
|
-
if (typeof rawUrl !== "string")
|
|
365
|
-
return undefined;
|
|
366
|
-
try {
|
|
367
|
-
const hostname = new URL(rawUrl).hostname.toLowerCase();
|
|
368
|
-
return hostname.length > 0 && hostname.length <= 253 ? hostname : undefined;
|
|
369
|
-
}
|
|
370
|
-
catch {
|
|
371
|
-
return undefined;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
246
|
function incomingStreamChunk(value) {
|
|
375
247
|
if (Buffer.isBuffer(value))
|
|
376
248
|
return value;
|
package/dist/capabilities.js
CHANGED
|
@@ -21,7 +21,7 @@ const CAPABILITY_GUIDE_DEFINITIONS = [
|
|
|
21
21
|
{
|
|
22
22
|
name: "artifacts-review",
|
|
23
23
|
description: "Native artifact transfer and aggregate change review.",
|
|
24
|
-
whenToRead: "Read for host-provided files or
|
|
24
|
+
whenToRead: "Read for host-provided files or aggregate change review.",
|
|
25
25
|
enabled: (config) => config.artifactsEnabled || config.widgets === "changes",
|
|
26
26
|
},
|
|
27
27
|
{
|
|
@@ -31,7 +31,7 @@ const CAPABILITY_GUIDE_DEFINITIONS = [
|
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
33
|
name: "shell-processes",
|
|
34
|
-
description: "Long-running processes,
|
|
34
|
+
description: "Long-running bash processes, processId interaction, PTY, and platform edges.",
|
|
35
35
|
whenToRead: "Read for running or interactive command issues.",
|
|
36
36
|
},
|
|
37
37
|
];
|
|
@@ -72,13 +72,10 @@ export function buildCapabilityFingerprint(config, version, context = {}) {
|
|
|
72
72
|
"worktree.managed",
|
|
73
73
|
"filesystem.rename-move",
|
|
74
74
|
"filesystem.delete",
|
|
75
|
-
"process.
|
|
75
|
+
"process.lifecycle",
|
|
76
76
|
"hooks.lifecycle",
|
|
77
77
|
"capability-guides.read",
|
|
78
78
|
];
|
|
79
|
-
if (config.toolMode === "full") {
|
|
80
|
-
capabilities.push("inspection.search-tools");
|
|
81
|
-
}
|
|
82
79
|
if (config.subagents) {
|
|
83
80
|
capabilities.push("subagent.profiles");
|
|
84
81
|
}
|
|
@@ -89,7 +86,7 @@ export function buildCapabilityFingerprint(config, version, context = {}) {
|
|
|
89
86
|
capabilities.push("ui.mcp-app");
|
|
90
87
|
}
|
|
91
88
|
if (config.widgets === "changes") {
|
|
92
|
-
capabilities.push("review.
|
|
89
|
+
capabilities.push("review.changes");
|
|
93
90
|
}
|
|
94
91
|
return {
|
|
95
92
|
version,
|
package/dist/hooks.js
CHANGED
|
@@ -152,6 +152,10 @@ export function attachHookReports(result, executions) {
|
|
|
152
152
|
const summary = formatVisibleHookReports(executions);
|
|
153
153
|
if (!summary || !isRecord(result) || !Array.isArray(result.content))
|
|
154
154
|
return result;
|
|
155
|
+
const structuredContent = isRecord(result.structuredContent) ? result.structuredContent : undefined;
|
|
156
|
+
const structuredResult = typeof structuredContent?.result === "string"
|
|
157
|
+
? structuredContent.result
|
|
158
|
+
: undefined;
|
|
155
159
|
return {
|
|
156
160
|
...result,
|
|
157
161
|
content: [
|
|
@@ -161,6 +165,14 @@ export function attachHookReports(result, executions) {
|
|
|
161
165
|
text: summary,
|
|
162
166
|
},
|
|
163
167
|
],
|
|
168
|
+
...(structuredContent && structuredResult !== undefined
|
|
169
|
+
? {
|
|
170
|
+
structuredContent: {
|
|
171
|
+
...structuredContent,
|
|
172
|
+
result: `${structuredResult}\n\n${summary}`,
|
|
173
|
+
},
|
|
174
|
+
}
|
|
175
|
+
: {}),
|
|
164
176
|
};
|
|
165
177
|
}
|
|
166
178
|
function appendHookReportsToError(error, executions) {
|
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
export const toolNames = {
|
|
2
2
|
openWorkspace: "open_workspace",
|
|
3
3
|
closeWorkspace: "close_workspace",
|
|
4
|
-
closeWorktree: "close_worktree",
|
|
5
4
|
read: "read",
|
|
6
5
|
write: "write",
|
|
7
6
|
edit: "edit",
|
|
8
7
|
rename: "rename",
|
|
9
8
|
delete: "delete",
|
|
10
|
-
grep: "grep",
|
|
11
|
-
glob: "glob",
|
|
12
|
-
ls: "ls",
|
|
13
9
|
shell: "bash",
|
|
14
10
|
writeStdin: "write_stdin",
|
|
15
11
|
capability: "capability",
|
|
@@ -24,9 +20,9 @@ export function buildToolDescriptions(config) {
|
|
|
24
20
|
const skillCapability = config.skillsEnabled
|
|
25
21
|
? " Advertised skill paths may also be outside the workspace."
|
|
26
22
|
: "";
|
|
27
|
-
const shellSurface = config.toolMode === "
|
|
28
|
-
?
|
|
29
|
-
: "";
|
|
23
|
+
const shellSurface = config.toolMode === "codex"
|
|
24
|
+
? ""
|
|
25
|
+
: " Use shell commands for search and directory inspection instead of dedicated MCP search tools.";
|
|
30
26
|
return {
|
|
31
27
|
read: `Read a file inside an open workspace or the OS temp directory. Instruction files and advertised capability guides returned by ${toolNames.openWorkspace} are also readable when applicable.${skillCapability} Only advertised entry files and files under already-loaded advertised directories are readable outside the normal roots. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
|
|
32
28
|
write: `Create or completely overwrite a file inside an open workspace or the OS temp directory. Workspace paths may be relative; OS temp paths may be absolute. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
|
|
@@ -34,7 +30,7 @@ export function buildToolDescriptions(config) {
|
|
|
34
30
|
rename: `Rename or move one file or directory inside an open workspace or the OS temp directory without overwriting an existing destination. Source and destination must both remain inside the permitted file roots. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
|
|
35
31
|
delete: `Delete one file or directory inside an open workspace or the OS temp directory. Non-empty directories require recursive=true. An allowed root itself cannot be deleted. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
|
|
36
32
|
applyPatch: `Apply one Codex-style patch inside an open workspace or the OS temp directory. Supports adding, overwriting, updating, deleting, and moving files. Workspace paths must remain relative; absolute paths are accepted only inside the OS temp directory. Call ${toolNames.openWorkspace} first and pass workspaceId.`,
|
|
37
|
-
shell: `Run a shell
|
|
33
|
+
shell: `Run or manage a shell process inside an open workspace.${shellSurface} Commands execute with the local user's authority; workspace filesystem containment does not make shell execution a sandbox. action=run (default) starts a command and waits up to 300 seconds; action=process uses its processId to poll, wait, write input, resize a PTY, or interrupt it. Completed background commands may also be reported later for the same workspaceId. Call ${toolNames.openWorkspace} first and pass workspaceId. Expose this capability only behind strong authentication.`,
|
|
38
34
|
shellCommand: "Shell command to run with the local user's authority.",
|
|
39
35
|
};
|
|
40
36
|
}
|
|
@@ -42,7 +38,7 @@ function capabilityContractInstructions(config) {
|
|
|
42
38
|
const staleWorkspacePolicy = config.toolMode === "codex"
|
|
43
39
|
? ""
|
|
44
40
|
: ` If ${toolNames.openWorkspace} reports logical workspaces idle for more than two days, let the user choose whether to resume or close them with ${toolNames.closeWorkspace}; never close them automatically.`;
|
|
45
|
-
const workspaceLifecycle = `Use ForgeRelay as a local coding workspace. Default to the user's existing checkout. Reuse the workspaceId
|
|
41
|
+
const workspaceLifecycle = `Use ForgeRelay as a local coding workspace. Default to the user's existing checkout. Reuse the workspaceId from ${toolNames.openWorkspace}; resume or create another logical workspace only when the user asks.${staleWorkspacePolicy} Only open mode=\"worktree\" when the user explicitly asks for isolated or parallel Git work. ${toolNames.closeWorkspace} releases checkout-backed workspaces or safely finalizes managed-worktree-backed ones; managed close requires commitMessage. Read the managed-worktrees capability guide for advanced failure semantics.`;
|
|
46
42
|
const agents = `Follow instructions returned by ${toolNames.openWorkspace}. Read an availableAgentsFiles path before working under it.`;
|
|
47
43
|
const capabilityGuides = `For optional capabilities from ${toolNames.openWorkspace}, use ${toolNames.capability}; if unfamiliar, describe first and read its advertised capability guide with ${toolNames.read}.`;
|
|
48
44
|
const skills = config.skillsEnabled
|
|
@@ -63,10 +59,8 @@ function defaultWorkflowInstructions(config) {
|
|
|
63
59
|
if (config.toolMode === "codex") {
|
|
64
60
|
return `Use ${toolNames.read} for direct file reads, ${toolNames.rename} and ${toolNames.delete} for direct path moves or removals, apply_patch for content modifications, exec_command for inspection, tests, builds, and other commands, and ${toolNames.writeStdin} to poll or interact with running processes.`;
|
|
65
61
|
}
|
|
66
|
-
const inspection =
|
|
67
|
-
|
|
68
|
-
: `Use ${toolNames.shell} with command-line tools such as grep, rg, find, ls, and tree for search and directory inspection.`;
|
|
69
|
-
return joinInstructions(inspection, `Prefer ${toolNames.edit} for targeted content modifications, ${toolNames.write} only for new files or complete rewrites, ${toolNames.rename} for path moves, ${toolNames.delete} for removals, and ${toolNames.shell} for tests, builds, git inspection, package scripts, generators, formatters, and commands that are better executed by the shell. If ${toolNames.shell} returns a running process with a processId, use ${toolNames.writeStdin} only when you need to poll, wait, interact, or interrupt it; otherwise you may continue other work and consume its completion notice from a later tool result.`);
|
|
62
|
+
const inspection = `Use ${toolNames.shell} with command-line tools such as grep, rg, find, ls, and tree for search and directory inspection.`;
|
|
63
|
+
return joinInstructions(inspection, `Prefer ${toolNames.edit} for targeted content modifications, ${toolNames.write} only for new files or complete rewrites, ${toolNames.rename} for path moves, ${toolNames.delete} for removals, and ${toolNames.shell} for tests, builds, git inspection, package scripts, generators, formatters, and commands that are better executed by the shell. If ${toolNames.shell} returns a running process with a processId, call ${toolNames.shell} again with action=\"process\" when you need to poll, wait, interact, resize, or interrupt it; otherwise you may continue other work and consume its completion notice from a later tool result.`);
|
|
70
64
|
}
|
|
71
65
|
function joinInstructions(...parts) {
|
|
72
66
|
return parts
|
|
@@ -39,19 +39,19 @@ export function createReviewCheckpointManager() {
|
|
|
39
39
|
}
|
|
40
40
|
assertWorkspaceRoot(state, workspaceId, root);
|
|
41
41
|
if (!state?.gitRoot) {
|
|
42
|
-
throw new Error(state?.diagnostic ?? "
|
|
42
|
+
throw new Error(state?.diagnostic ?? "review.changes requires a Git workspace in this version.");
|
|
43
43
|
}
|
|
44
44
|
let effectiveSince = since;
|
|
45
45
|
let usedWorkspaceOpenFallback = false;
|
|
46
46
|
if (since === "last_shown" && !state.baselineRefAvailable) {
|
|
47
47
|
if (!state.openRefAvailable) {
|
|
48
|
-
throw new Error("Review checkpoints are missing;
|
|
48
|
+
throw new Error("Review checkpoints are missing; review.changes cannot reconstruct that history safely.");
|
|
49
49
|
}
|
|
50
50
|
effectiveSince = "workspace_open";
|
|
51
51
|
usedWorkspaceOpenFallback = true;
|
|
52
52
|
}
|
|
53
53
|
else if (since === "workspace_open" && !state.openRefAvailable) {
|
|
54
|
-
throw new Error("The workspace-open review checkpoint is missing;
|
|
54
|
+
throw new Error("The workspace-open review checkpoint is missing; review.changes cannot reconstruct that history safely.");
|
|
55
55
|
}
|
|
56
56
|
const baselineRef = effectiveSince === "workspace_open" ? state.openRef : state.baselineRef;
|
|
57
57
|
const baseline = (await git(state.gitRoot, ["rev-parse", "--verify", `${baselineRef}^{commit}`])).stdout.trim();
|
|
@@ -98,7 +98,7 @@ async function initializeWorkspaceState(states, workspaceId, root) {
|
|
|
98
98
|
try {
|
|
99
99
|
const eligibility = await getGitEligibility(root);
|
|
100
100
|
if (!eligibility.ok || !eligibility.gitRoot) {
|
|
101
|
-
state.diagnostic = eligibility.message ?? "
|
|
101
|
+
state.diagnostic = eligibility.message ?? "review.changes requires a Git workspace in this version.";
|
|
102
102
|
return;
|
|
103
103
|
}
|
|
104
104
|
const [openCommit, baselineCommit] = await Promise.all([
|