@akira-tl/forgerelay 0.2.4 → 0.2.6
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 +17 -0
- package/dist/artifact-tools.js +2 -3
- package/dist/logger.js +77 -10
- package/dist/mcp/server-instructions.js +4 -4
- package/dist/mcp-app-template.js +45 -0
- package/dist/mcp-sessions.js +24 -22
- package/dist/process-sessions.js +135 -108
- package/dist/roots.js +1 -1
- package/dist/server.js +165 -88
- package/docs/chatgpt-coding-workflow.md +3 -2
- package/docs/configuration.md +11 -6
- package/docs/debugging.md +35 -7
- package/docs/gotchas.md +22 -1
- package/docs/roadmap.md +26 -0
- package/docs/security.md +3 -2
- package/package.json +2 -2
- package/scripts/debug/accept.mjs +79 -3
- package/scripts/debug/runtime.mjs +2 -1
package/docs/roadmap.md
CHANGED
|
@@ -82,6 +82,26 @@ Hooks v1 的目标是给用户和 Agent 一个很小、自动、可组合的生
|
|
|
82
82
|
|
|
83
83
|
0.2 不引入审批 UI、HTTP/prompt/agent handler、Git 字符串解析器或插件注册表。只有出现真实需求时再扩展 handler 类型。
|
|
84
84
|
|
|
85
|
+
### 0.2.5 — MCP App template reliability
|
|
86
|
+
|
|
87
|
+
0.2.5 收敛 ChatGPT/MCP App 模板身份与排障链路:
|
|
88
|
+
|
|
89
|
+
- 当前 UI resource URI 由实际构建出的 JavaScript/CSS 内容哈希生成,而不是仅依赖 npm 版本;
|
|
90
|
+
- `ui://forgerelay/workspace-app.html` 继续作为 legacy pointer;
|
|
91
|
+
- 历史 `workspace-app-*.html` URI 通过兼容 resource template 继续读取当前模板,避免旧 metadata snapshot 直接变成 missing resource;
|
|
92
|
+
- debug 日志区分 current / legacy / historical template read,并保留 asset request trace;
|
|
93
|
+
- 7677 acceptance 覆盖 tool metadata、resource list/template list、三类 template read 和静态 bundle HTTP fetch。
|
|
94
|
+
|
|
95
|
+
### 0.2.6 — identity and transport terminology
|
|
96
|
+
|
|
97
|
+
0.2.6 整理 ForgeRelay 中多个 `session` 含义,不改变 workspace-first 架构:
|
|
98
|
+
|
|
99
|
+
- `workspaceId` 是唯一持久的逻辑工作身份,跨请求和 transport 重连保持连续;
|
|
100
|
+
- `requestId` 只追踪单次 HTTP/JSON-RPC 请求,不持久化;
|
|
101
|
+
- MCP 协议层 session 在内部和 debug 输出中明确称为 `transportSessionId`,业务状态不得依赖它;
|
|
102
|
+
- 后台命令句柄以 `processId` / process handle 为 canonical 名称,旧 `sessionId` 在 0.2.x 保留 deprecated alias 兼容窗口;
|
|
103
|
+
- 为 stateless MCP transport 做准备,同时保留旧协议兼容 adapter,避免把 transport 生命周期重新提升成 ForgeRelay 会话模型。
|
|
104
|
+
|
|
85
105
|
## 0.3 — LSP code intelligence v1
|
|
86
106
|
|
|
87
107
|
LSP is moderate implementation complexity if ForgeRelay does not become a
|
|
@@ -90,6 +110,12 @@ language-server installer.
|
|
|
90
110
|
The first version should launch only language servers already available on the
|
|
91
111
|
user's machine or explicitly configured by the user/project.
|
|
92
112
|
|
|
113
|
+
During 0.3 development, MCP App UI hardening can land alongside the LSP work when
|
|
114
|
+
it does not distort the code-intelligence scope. In particular, evaluate a more
|
|
115
|
+
self-contained template/bootstrap bundle, reduce avoidable external chunk fetches,
|
|
116
|
+
and keep the current content-hash/compatibility-resource contract intact. This is
|
|
117
|
+
reliability work, not a requirement to move application state into the UI.
|
|
118
|
+
|
|
93
119
|
Initial operations:
|
|
94
120
|
|
|
95
121
|
- diagnostics;
|
package/docs/security.md
CHANGED
|
@@ -124,8 +124,9 @@ Do not describe ForgeRelay as a sandboxed coding environment.
|
|
|
124
124
|
|
|
125
125
|
Shell execution has a 300-second foreground wait ceiling, not a 300-second
|
|
126
126
|
process lifetime. When `bash` is still running after that window, ForgeRelay
|
|
127
|
-
returns a
|
|
128
|
-
wait, interact, or explicitly interrupt it.
|
|
127
|
+
returns a canonical `processId` and leaves the process alive. `write_stdin` can
|
|
128
|
+
poll, wait, interact, or explicitly interrupt it. The former process `sessionId`
|
|
129
|
+
remains a deprecated compatibility alias during 0.2.x. An asynchronously completed process
|
|
129
130
|
is reported on a later tool result for the same logical workspace ID, including
|
|
130
131
|
error-result paths, and is never broadcast to another workspace ID. Explicitly
|
|
131
132
|
resuming the same workspace ID in another conversation intentionally transfers
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akira-tl/forgerelay",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.6",
|
|
4
4
|
"description": "Local development control plane for MCP coding agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://github.com/Akira-TL/forgerelay#readme",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"debug:accept": "node scripts/debug/accept.mjs",
|
|
42
42
|
"postinstall": "node scripts/fix-node-pty-permissions.mjs",
|
|
43
43
|
"start": "node dist/cli.js serve",
|
|
44
|
-
"test": "tsx src/config.test.ts && tsx src/logger.test.ts && tsx src/hooks.test.ts && tsx src/mcp/server-instructions.test.ts && tsx src/request-meta.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/roots.test.ts && tsx src/file-mutations.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
|
|
44
|
+
"test": "tsx src/config.test.ts && tsx src/logger.test.ts && tsx src/mcp-app-template.test.ts && tsx src/hooks.test.ts && tsx src/mcp/server-instructions.test.ts && tsx src/request-meta.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/roots.test.ts && tsx src/file-mutations.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
|
|
45
45
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
46
46
|
"release:check": "node scripts/release-version.mjs check",
|
|
47
47
|
"release:tag-check": "node scripts/release-version.mjs tag",
|
package/scripts/debug/accept.mjs
CHANGED
|
@@ -38,7 +38,13 @@ await assertDebugPortFree();
|
|
|
38
38
|
rmSync(acceptanceRoot, { recursive: true, force: true });
|
|
39
39
|
mkdirSync(acceptanceRoot, { recursive: true });
|
|
40
40
|
|
|
41
|
-
const { env } = createDebugEnvironment({
|
|
41
|
+
const { env } = createDebugEnvironment({
|
|
42
|
+
ownerToken,
|
|
43
|
+
stateDir,
|
|
44
|
+
worktreeRoot,
|
|
45
|
+
hookLog,
|
|
46
|
+
widgets: "full",
|
|
47
|
+
});
|
|
42
48
|
const server = spawn(process.execPath, ["--import", "tsx", "src/cli.ts", "serve"], {
|
|
43
49
|
cwd: repoRoot,
|
|
44
50
|
env,
|
|
@@ -120,12 +126,76 @@ try {
|
|
|
120
126
|
assert.match(bashTool?.description ?? "", /write_stdin/);
|
|
121
127
|
const writeStdinTool = tools.find((tool) => tool.name === "write_stdin");
|
|
122
128
|
assert.equal(writeStdinTool?.inputSchema?.properties?.yieldTimeMs?.maximum, 300000);
|
|
129
|
+
assert.match(
|
|
130
|
+
writeStdinTool?.inputSchema?.properties?.processId?.description ?? "",
|
|
131
|
+
/Canonical process identifier/,
|
|
132
|
+
);
|
|
133
|
+
assert.match(
|
|
134
|
+
writeStdinTool?.inputSchema?.properties?.sessionId?.description ?? "",
|
|
135
|
+
/Deprecated alias for processId/,
|
|
136
|
+
);
|
|
123
137
|
const openWorkspaceTool = tools.find((tool) => tool.name === "open_workspace");
|
|
124
138
|
assert.ok(openWorkspaceTool?.inputSchema?.properties?.workspaceId);
|
|
125
139
|
assert.ok(openWorkspaceTool?.inputSchema?.properties?.newWorkspace);
|
|
126
140
|
assert.ok(openWorkspaceTool?.outputSchema?.properties?.staleWorkspaces);
|
|
141
|
+
const templateUri = bashTool?._meta?.ui?.resourceUri;
|
|
142
|
+
assert.match(
|
|
143
|
+
templateUri ?? "",
|
|
144
|
+
/^ui:\/\/forgerelay\/workspace-app-[0-9a-f]{12}\.html$/,
|
|
145
|
+
JSON.stringify(bashTool ?? {}),
|
|
146
|
+
);
|
|
147
|
+
assert.deepEqual(bashTool?._meta?.ui?.visibility, ["model", "app"]);
|
|
148
|
+
assert.equal(bashTool?._meta?.["openai/outputTemplate"], templateUri);
|
|
127
149
|
pass("MCP tools/list", `${toolNames.length} tools: ${toolNames.join(", ")}`);
|
|
128
150
|
|
|
151
|
+
const resources = mcpRequest(oauth.accessToken, sessionId, {
|
|
152
|
+
jsonrpc: "2.0",
|
|
153
|
+
id: 21,
|
|
154
|
+
method: "resources/list",
|
|
155
|
+
params: {},
|
|
156
|
+
}).message.result.resources;
|
|
157
|
+
assert.ok(resources.some((resource) => resource.uri === templateUri));
|
|
158
|
+
assert.ok(resources.some((resource) => resource.uri === "ui://forgerelay/workspace-app.html"));
|
|
159
|
+
|
|
160
|
+
const resourceTemplates = mcpRequest(oauth.accessToken, sessionId, {
|
|
161
|
+
jsonrpc: "2.0",
|
|
162
|
+
id: 22,
|
|
163
|
+
method: "resources/templates/list",
|
|
164
|
+
params: {},
|
|
165
|
+
}).message.result.resourceTemplates;
|
|
166
|
+
assert.ok(resourceTemplates.some(
|
|
167
|
+
(resourceTemplate) => resourceTemplate.uriTemplate === "ui://forgerelay/workspace-app-{revision}.html",
|
|
168
|
+
));
|
|
169
|
+
|
|
170
|
+
const readTemplate = (id, uri) => mcpRequest(oauth.accessToken, sessionId, {
|
|
171
|
+
jsonrpc: "2.0",
|
|
172
|
+
id,
|
|
173
|
+
method: "resources/read",
|
|
174
|
+
params: { uri },
|
|
175
|
+
}).message.result.contents[0];
|
|
176
|
+
|
|
177
|
+
const template = readTemplate(23, templateUri);
|
|
178
|
+
assert.equal(template.uri, templateUri);
|
|
179
|
+
assert.equal(template.mimeType, "text/html;profile=mcp-app");
|
|
180
|
+
assert.match(template.text ?? "", /<script type="module" crossorigin src="[^"]+\/mcp-app-assets\//);
|
|
181
|
+
assert.ok(template._meta?.ui?.csp?.resourceDomains?.includes(debugBaseUrl));
|
|
182
|
+
const scriptUrl = template.text?.match(/<script type="module" crossorigin src="([^"]+)"/)?.[1];
|
|
183
|
+
assert.ok(scriptUrl);
|
|
184
|
+
const scriptAsset = curlRequest({ method: "GET", url: scriptUrl });
|
|
185
|
+
assert.equal(scriptAsset.status, 200, scriptAsset.body);
|
|
186
|
+
|
|
187
|
+
const legacyTemplate = readTemplate(24, "ui://forgerelay/workspace-app.html");
|
|
188
|
+
assert.equal(legacyTemplate.uri, "ui://forgerelay/workspace-app.html");
|
|
189
|
+
assert.equal(legacyTemplate.mimeType, "text/html;profile=mcp-app");
|
|
190
|
+
assert.equal(legacyTemplate.text, template.text);
|
|
191
|
+
|
|
192
|
+
const historicalUri = "ui://forgerelay/workspace-app-0.2.4.html";
|
|
193
|
+
const historicalTemplate = readTemplate(25, historicalUri);
|
|
194
|
+
assert.equal(historicalTemplate.uri, historicalUri);
|
|
195
|
+
assert.equal(historicalTemplate.mimeType, "text/html;profile=mcp-app");
|
|
196
|
+
assert.equal(historicalTemplate.text, template.text);
|
|
197
|
+
pass("MCP app template", `${templateUri} + legacy/history compatibility -> ${scriptUrl}`);
|
|
198
|
+
|
|
129
199
|
const opened = callTool(oauth.accessToken, sessionId, 3, "open_workspace", {
|
|
130
200
|
path: checkoutWorkspace,
|
|
131
201
|
});
|
|
@@ -155,7 +225,7 @@ try {
|
|
|
155
225
|
});
|
|
156
226
|
assert.match(shell.structuredContent.result, /debug-bash-ok/);
|
|
157
227
|
assert.equal(shell.structuredContent.running, false);
|
|
158
|
-
pass("bash", "foreground command completed through
|
|
228
|
+
pass("bash", "foreground command completed through ProcessManager");
|
|
159
229
|
|
|
160
230
|
const failedEdit = callTool(oauth.accessToken, sessionId, 7, "edit", {
|
|
161
231
|
workspaceId,
|
|
@@ -364,7 +434,13 @@ function initializeRequest(id) {
|
|
|
364
434
|
method: "initialize",
|
|
365
435
|
params: {
|
|
366
436
|
protocolVersion: "2025-06-18",
|
|
367
|
-
capabilities: {
|
|
437
|
+
capabilities: {
|
|
438
|
+
extensions: {
|
|
439
|
+
"io.modelcontextprotocol/ui": {
|
|
440
|
+
mimeTypes: ["text/html;profile=mcp-app"],
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
},
|
|
368
444
|
clientInfo: { name: "forgerelay-debug-acceptance", version: "1.0.0" },
|
|
369
445
|
},
|
|
370
446
|
};
|
|
@@ -20,6 +20,7 @@ export function createDebugEnvironment({
|
|
|
20
20
|
stateDir = resolve(debugRoot, "state"),
|
|
21
21
|
worktreeRoot = resolve(debugRoot, "worktrees"),
|
|
22
22
|
hookLog = debugHookLog,
|
|
23
|
+
widgets = process.env.FORGERELAY_DEBUG_WIDGETS ?? "off",
|
|
23
24
|
} = {}) {
|
|
24
25
|
const token = ownerToken ?? process.env.FORGERELAY_DEBUG_OWNER_TOKEN ?? createDebugOwnerToken();
|
|
25
26
|
mkdirSync(debugRoot, { recursive: true });
|
|
@@ -37,7 +38,7 @@ export function createDebugEnvironment({
|
|
|
37
38
|
FORGERELAY_WORKTREE_ROOT: worktreeRoot,
|
|
38
39
|
FORGERELAY_OAUTH_OWNER_TOKEN: token,
|
|
39
40
|
FORGERELAY_TOOL_MODE: "full",
|
|
40
|
-
FORGERELAY_WIDGETS:
|
|
41
|
+
FORGERELAY_WIDGETS: widgets,
|
|
41
42
|
FORGERELAY_LOG_LEVEL: process.env.FORGERELAY_LOG_LEVEL ?? "info",
|
|
42
43
|
FORGERELAY_LOG_FORMAT: process.env.FORGERELAY_LOG_FORMAT ?? "pretty",
|
|
43
44
|
FORGERELAY_DEBUG_HOOK_RECORDER: debugHookRecorder,
|