@akira-tl/forgerelay 0.8.7 → 0.8.9
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 +18 -0
- package/README.md +10 -18
- package/capabilities/host-integration/GUIDE.md +2 -2
- package/dist/activity/{audit-store.js → history/audit-store.js} +129 -21
- package/dist/activity/history/bash-output-store.js +250 -0
- package/dist/activity/{host-turn-store.js → history/host-turn-store.js} +4 -1
- package/dist/activity/{query-service.js → history/query-service.js} +13 -7
- package/dist/activity/{lifecycle.js → runtime/lifecycle.js} +1 -1
- package/dist/activity/{mcp-query-tools.js → runtime/mcp-query-tools.js} +3 -3
- package/dist/activity/storage/history.js +12 -0
- package/dist/activity/storage/paths.js +56 -0
- package/dist/activity/storage/segmented-log.js +149 -0
- package/dist/cli/setup-support.js +116 -0
- package/dist/cli.js +18 -126
- package/dist/lsp/code-intelligence.js +1 -1
- package/dist/lsp/test-support/server-fixture.js +9 -9
- package/dist/mcp/filesystem/filesystem-tools.js +245 -0
- package/dist/mcp/hooks/command-runner.js +79 -0
- package/dist/{hook-cli.js → mcp/hooks/hook-cli.js} +1 -1
- package/dist/{hooks.js → mcp/hooks/hooks.js} +3 -80
- package/dist/{oauth-provider.js → mcp/oauth/oauth-provider.js} +1 -1
- package/dist/{oauth-store.js → mcp/oauth/oauth-store.js} +5 -4
- package/dist/{operations → mcp/operations}/native-bulk-mutations.js +2 -2
- package/dist/mcp/panel/app.js +143 -0
- package/dist/{mcp-app-template.js → mcp/panel/mcp-app-template.js} +2 -38
- package/dist/mcp/process/runtime.js +257 -0
- package/dist/mcp/process/tools.js +346 -0
- package/dist/mcp/server/core/activity-support.js +70 -0
- package/dist/{capabilities.js → mcp/server/core/capabilities.js} +5 -3
- package/dist/{capability-registry.js → mcp/server/core/capability-registry.js} +2 -2
- package/dist/mcp/server/core/capability-support.js +90 -0
- package/dist/mcp/server/core/schemas.js +216 -0
- package/dist/mcp/server/core/tool-support.js +207 -0
- package/dist/mcp/server/core/version.js +9 -0
- package/dist/mcp/server/operations/runtime/filesystem-tools.js +400 -0
- package/dist/mcp/server/operations/runtime/operation-runtime.js +523 -0
- package/dist/mcp/server/transport/http-server.js +301 -0
- package/dist/mcp/server/transport/http-support.js +43 -0
- package/dist/mcp/server/workspace/runtime/workspace-open-presentation.js +222 -0
- package/dist/mcp/server/workspace/runtime/workspace-open-schema.js +187 -0
- package/dist/mcp/server/workspace/runtime/workspace-open.js +552 -0
- package/dist/mcp/server/workspace/runtime/workspace-tools.js +601 -0
- package/dist/{config.js → runtime/config/config.js} +3 -3
- package/dist/{user-config.js → runtime/config/user-config.js} +14 -54
- package/dist/{db → runtime/state/db}/client.js +1 -1
- package/dist/{db → runtime/state/db}/migrations.js +34 -3
- package/dist/runtime/state/lock/file-lock.js +130 -0
- package/dist/runtime/testing/server-fixture.js +190 -0
- package/dist/server.js +139 -4192
- package/dist/subagents/providers/adapters/claude.js +86 -52
- package/dist/subagents/providers/adapters/codex.js +103 -39
- package/dist/subagents/providers/adapters/opencode.js +114 -84
- package/dist/subagents/providers/adapters/pi.js +59 -8
- package/dist/subagents/providers/availability.js +15 -30
- package/dist/subagents/providers/runtime/external-command.js +56 -0
- package/dist/subagents/sessions/capability.js +1 -1
- package/dist/subagents/sessions/execution.js +1 -1
- package/dist/subagents/sessions/mcp/activity-audit-test-support.js +30 -0
- package/dist/subagents/sessions/store.js +1 -1
- package/dist/ui/.vite/manifest.json +342 -409
- package/dist/ui/activity-panel-app.html +2 -4
- package/dist/ui/assets/activity-panel-app-7Xe0alhm.css +1 -0
- package/dist/ui/assets/activity-panel-app-Sl1Jro8v.js +115 -0
- package/dist/ui/assets/angular-html-B_yn91wW.js +1 -0
- package/dist/ui/assets/{angular-ts-Bxh4-O1-.js → angular-ts-DSgtloKz.js} +1 -1
- package/dist/ui/assets/{apl-BmYHKQ6m.js → apl-NatWtF8X.js} +1 -1
- package/dist/ui/assets/{astro-C51vqDKq.js → astro-BYFEVBuN.js} +1 -1
- package/dist/ui/assets/{blade-CdhVsmwq.js → blade-DoO7yvMR.js} +1 -1
- package/dist/ui/assets/c-Rl2hMCcg.js +1 -0
- package/dist/ui/assets/{cobol-Cf6azoN_.js → cobol-ByC5mWiB.js} +1 -1
- package/dist/ui/assets/{coffee-vVdGzlmt.js → coffee-Dez77Qov.js} +1 -1
- package/dist/ui/assets/cpp-DtaAQT9h.js +1 -0
- package/dist/ui/assets/{crystal-5UZy6KIX.js → crystal-Bgmbf8ec.js} +1 -1
- package/dist/ui/assets/css-C1tjBtVN.js +1 -0
- package/dist/ui/assets/{edge-CESgo4ia.js → edge-DeH6u8Wg.js} +1 -1
- package/dist/ui/assets/{elixir-9FKfw4cJ.js → elixir-_H3KnmWT.js} +1 -1
- package/dist/ui/assets/{elm-Dv7A0aLa.js → elm-CsCe3U28.js} +1 -1
- package/dist/ui/assets/{erb-DHFmDvM0.js → erb-CbM-L2g8.js} +1 -1
- package/dist/ui/assets/{git-rebase-utbU6CzV.js → git-rebase-D0s5pDh9.js} +1 -1
- package/dist/ui/assets/{glimmer-js-t2kiCkyB.js → glimmer-js-BfOToq_A.js} +1 -1
- package/dist/ui/assets/{glimmer-ts-F0Jv5okV.js → glimmer-ts-CCUOkL2K.js} +1 -1
- package/dist/ui/assets/glsl-DH4ec9dE.js +1 -0
- package/dist/ui/assets/graphql-DilAMBoI.js +1 -0
- package/dist/ui/assets/{hack-8yjdbaSA.js → hack-BDkNDZlF.js} +1 -1
- package/dist/ui/assets/haml-D_QBp6K7.js +1 -0
- package/dist/ui/assets/{handlebars-BBFEg4Lo.js → handlebars-DePUrTpC.js} +1 -1
- package/dist/ui/assets/heavy-payload-DOwZpO6n.js +290 -0
- package/dist/ui/assets/html-By4dNnM4.js +1 -0
- package/dist/ui/assets/{html-derivative-DJAQcZyc.js → html-derivative-DDw9Ef4Q.js} +1 -1
- package/dist/ui/assets/{http-Dg5LSYRK.js → http-B0QxWc9M.js} +1 -1
- package/dist/ui/assets/{hurl-Y5uYzMZR.js → hurl-vMQ4lda1.js} +1 -1
- package/dist/ui/assets/java-DYRe2lWG.js +1 -0
- package/dist/ui/assets/javascript-CxyGqwcL.js +1 -0
- package/dist/ui/assets/{jinja-Bt30h8Ik.js → jinja-JUwV-fZt.js} +1 -1
- package/dist/ui/assets/{jison-D5uZ1Nyo.js → jison-7ZqCEvS8.js} +1 -1
- package/dist/ui/assets/json-dbltgiiA.js +1 -0
- package/dist/ui/assets/jsx-oDopT7YQ.js +1 -0
- package/dist/ui/assets/{julia-76hRaaGz.js → julia-CkKiNSav.js} +1 -1
- package/dist/ui/assets/{just-CDE7W0vW.js → just-DuqCAKJ3.js} +1 -1
- package/dist/ui/assets/{latex-CVO2Byab.js → latex-sudcRDSn.js} +1 -1
- package/dist/ui/assets/{liquid-CA-AS3EX.js → liquid-Di-08Js7.js} +1 -1
- package/dist/ui/assets/lua-YlqdMoV0.js +1 -0
- package/dist/ui/assets/{marko-_BWrPfny.js → marko-CJRNyJDb.js} +1 -1
- package/dist/ui/assets/{mdc-PyJv85eS.js → mdc-t12CzZHs.js} +1 -1
- package/dist/ui/assets/{nginx-DAWVw8yu.js → nginx-57-b4XwT.js} +1 -1
- package/dist/ui/assets/{nim-CKE1dzpU.js → nim-BzrswYQa.js} +1 -1
- package/dist/ui/assets/{perl-BBhWva19.js → perl-D9JR_iIh.js} +1 -1
- package/dist/ui/assets/{php-ChIeaAoF.js → php-Py2gFHil.js} +1 -1
- package/dist/ui/assets/{pug-CMgI5afi.js → pug-PzLLy0h1.js} +1 -1
- package/dist/ui/assets/{qml-CjxTkcFH.js → qml-By8fwgV4.js} +1 -1
- package/dist/ui/assets/r-CcezTbLq.js +1 -0
- package/dist/ui/assets/{razor-i4DbHr8p.js → razor-dG-vdhL_.js} +1 -1
- package/dist/ui/assets/regexp-BlsQ-N_8.js +1 -0
- package/dist/ui/assets/{rst-oy_97Kl4.js → rst-C2awDCat.js} +1 -1
- package/dist/ui/assets/{ruby-C5zYZIyl.js → ruby-zA2W_49d.js} +1 -1
- package/dist/ui/assets/{sas-BnMEh2S3.js → sas-D49bhyaN.js} +1 -1
- package/dist/ui/assets/scss-BQb-cp01.js +1 -0
- package/dist/ui/assets/shellscript-Dga4H_Ni.js +1 -0
- package/dist/ui/assets/{shellsession-BpE5o7_3.js → shellsession-n3-5CqcF.js} +1 -1
- package/dist/ui/assets/{soy-COesmhTY.js → soy-XYBOf_BA.js} +1 -1
- package/dist/ui/assets/sql-ET4u5Cpl.js +1 -0
- package/dist/ui/assets/{stata-CGQEuSZq.js → stata-CLHcJHO_.js} +1 -1
- package/dist/ui/assets/{surrealql-Bz4JbU-9.js → surrealql-BdjQ60T4.js} +1 -1
- package/dist/ui/assets/{svelte-1jM2ejXj.js → svelte-B2RPtKJe.js} +1 -1
- package/dist/ui/assets/{templ-C-VwELcp.js → templ-CrByPW_W.js} +1 -1
- package/dist/ui/assets/{tex-t1rUiQCm.js → tex-DZBkg363.js} +1 -1
- package/dist/ui/assets/{ts-tags-Bewoa6pS.js → ts-tags-DJR3Gphb.js} +1 -1
- package/dist/ui/assets/tsx-CajzROyy.js +1 -0
- package/dist/ui/assets/{twig-DVg-vELl.js → twig-BZjk5Zda.js} +1 -1
- package/dist/ui/assets/typescript-B-EWToLj.js +1 -0
- package/dist/ui/assets/{vue-wPQ7UlMy.js → vue-D_rHCCr8.js} +1 -1
- package/dist/ui/assets/{vue-html-BHtJ8geX.js → vue-html-CC4WlvVt.js} +1 -1
- package/dist/ui/assets/{vue-vine-CNSGAWwc.js → vue-vine-BgNMVgpk.js} +1 -1
- package/dist/ui/assets/xml-BI86-WbH.js +1 -0
- package/dist/ui/assets/{xsl-3_vJ27aE.js → xsl-DXeLZV4J.js} +1 -1
- package/dist/ui/assets/yaml-60_PFlWa.js +1 -0
- package/dist/workspaces/bootstrap.js +72 -0
- package/dist/workspaces/context.js +302 -0
- package/dist/workspaces/conversation-test-support.js +86 -0
- package/dist/{git-worktrees.js → workspaces/git/git-worktrees.js} +1 -1
- package/dist/workspaces/inventory.js +131 -0
- package/dist/workspaces/paths.js +55 -0
- package/dist/{remote-auth.js → workspaces/relay/auth/remote-auth.js} +2 -2
- package/dist/workspaces/relay/result-support.js +111 -0
- package/dist/workspaces/relay/tests/test-support.js +203 -0
- package/dist/{remote-mcp-connection-pool.js → workspaces/relay/transport/remote-mcp-connection-pool.js} +1 -1
- package/dist/workspaces/relay/types.js +1 -0
- package/dist/{remote-workspace-relay.js → workspaces/relay/workspace-relay.js} +23 -172
- package/dist/workspaces/resources/skills.js +285 -0
- package/dist/{review-checkpoints.js → workspaces/review/review-checkpoints.js} +2 -2
- package/dist/workspaces/sessions.js +457 -0
- package/dist/{workspace-store.js → workspaces/state/workspace-store.js} +2 -2
- package/dist/workspaces.js +69 -991
- package/docs/chatgpt-coding-workflow.md +3 -11
- package/docs/configuration.md +12 -25
- package/docs/gotchas.md +0 -30
- package/docs/roadmap.md +6 -10
- package/docs/security.md +1 -5
- package/docs/setup.md +0 -11
- package/package.json +6 -8
- package/scripts/ci/architecture.mjs +109 -0
- package/scripts/ci/verify.mjs +5 -0
- package/scripts/debug/accept/bootstrap.mjs +326 -0
- package/scripts/debug/accept/harness.mjs +76 -0
- package/scripts/debug/accept/support.mjs +432 -0
- package/scripts/debug/accept.mjs +43 -806
- package/scripts/debug/relay-accept/support.mjs +135 -0
- package/scripts/debug/relay-accept.mjs +26 -115
- package/scripts/debug/traffic/support.mjs +167 -0
- package/scripts/debug/traffic/traffic-audit.mjs +4 -161
- package/scripts/{lsp-interop-support.test.mjs → lsp/interop-support.test.mjs} +1 -1
- package/scripts/{lsp-interop.mjs → lsp/interop.mjs} +3 -3
- package/scripts/wiki/sync.mjs +21 -10
- package/dist/activity/bash-output-store.js +0 -127
- package/dist/pi-tools.js +0 -110
- package/dist/skills.js +0 -127
- package/dist/ui/assets/activity-panel-app-CUAN6zyW.js +0 -2
- package/dist/ui/assets/angular-html-BkgDQl-G.js +0 -1
- package/dist/ui/assets/c-IBzBpxAO.js +0 -1
- package/dist/ui/assets/chunk-EyZ2wyi3.js +0 -1
- package/dist/ui/assets/cpp-CVHeO-Jk.js +0 -1
- package/dist/ui/assets/css-DEfus9J5.js +0 -1
- package/dist/ui/assets/glsl-_ooCjaeB.js +0 -1
- package/dist/ui/assets/graphql-B9LqP-cY.js +0 -1
- package/dist/ui/assets/haml-BU5EoZBn.js +0 -1
- package/dist/ui/assets/heavy-payload-CgzrutLm.js +0 -4
- package/dist/ui/assets/html-D-C6YZk4.js +0 -1
- package/dist/ui/assets/java-ppHvycj5.js +0 -1
- package/dist/ui/assets/javascript-DDTjKLlQ.js +0 -1
- package/dist/ui/assets/json-Cw91r8h1.js +0 -1
- package/dist/ui/assets/jsx-04X2FT_1.js +0 -1
- package/dist/ui/assets/lua-CiyBp52Y.js +0 -1
- package/dist/ui/assets/r-DfFga4si.js +0 -1
- package/dist/ui/assets/regexp-iC9_nbSI.js +0 -1
- package/dist/ui/assets/review-payload-BrLbezbq.js +0 -1
- package/dist/ui/assets/scrollbar-CbhpdW05.js +0 -287
- package/dist/ui/assets/scss-BfZXsAWQ.js +0 -1
- package/dist/ui/assets/shellscript-BC2srNaB.js +0 -1
- package/dist/ui/assets/sql-Ccv_yeUm.js +0 -1
- package/dist/ui/assets/tsx-CTXzrBkN.js +0 -1
- package/dist/ui/assets/typescript--ElHoFkw.js +0 -1
- package/dist/ui/assets/workspace-app-Bhj96tsR.js +0 -1
- package/dist/ui/assets/workspace-app-CxwJuZyS.js +0 -111
- package/dist/ui/assets/workspace-app-D6UR0AFl.js +0 -5
- package/dist/ui/assets/workspace-app-ldjBmCJR.css +0 -1
- package/dist/ui/assets/workspace-lifecycle-app-Cqfhx9pV.js +0 -1
- package/dist/ui/assets/xml-JnmX6vyS.js +0 -1
- package/dist/ui/assets/yaml-OAcZkP-w.js +0 -1
- package/dist/ui/workspace-app.html +0 -18
- package/dist/ui/workspace-lifecycle-app.html +0 -18
- package/docs/agent-profile-schema.md +0 -114
- package/docs/artifact-exchange.md +0 -65
- package/docs/versioning.md +0 -205
- /package/capabilities/{managed-worktrees → workspace/managed-worktrees}/GUIDE.md +0 -0
- /package/capabilities/{workspace-tasks → workspace/workspace-tasks}/GUIDE.md +0 -0
- /package/dist/activity/{process-output-audit.js → runtime/process-output-audit.js} +0 -0
- /package/dist/{artifact-error.js → mcp/artifacts/artifact-error.js} +0 -0
- /package/dist/{artifact-tools.js → mcp/artifacts/artifact-tools.js} +0 -0
- /package/dist/{incoming-artifacts.js → mcp/artifacts/incoming-artifacts.js} +0 -0
- /package/dist/{advertised-files.js → mcp/filesystem/advertised-files.js} +0 -0
- /package/dist/{apply-patch.js → mcp/filesystem/apply-patch.js} +0 -0
- /package/dist/{file-mutations.js → mcp/filesystem/file-mutations.js} +0 -0
- /package/dist/{roots.js → mcp/filesystem/roots.js} +0 -0
- /package/dist/{oauth → mcp/oauth}/public-url.js +0 -0
- /package/dist/{oauth → mcp/oauth}/router.js +0 -0
- /package/dist/{operations → mcp/operations}/batch/executor.js +0 -0
- /package/dist/{operations → mcp/operations}/batch/scheduler.js +0 -0
- /package/dist/{operations → mcp/operations}/batch/types.js +0 -0
- /package/dist/{operations → mcp/operations}/bulk-mutation.js +0 -0
- /package/dist/{operations → mcp/operations}/bulk-read.js +0 -0
- /package/dist/{operations → mcp/operations}/core-operation-executor.js +0 -0
- /package/dist/{process-platform.js → mcp/process/process-platform.js} +0 -0
- /package/dist/{process-sessions.js → mcp/process/process-sessions.js} +0 -0
- /package/dist/{request-meta.js → mcp/request-meta.js} +0 -0
- /package/dist/{mcp-sessions.js → mcp/server/transport/mcp-sessions.js} +0 -0
- /package/dist/{server-shutdown.js → mcp/server/transport/server-shutdown.js} +0 -0
- /package/dist/{logger.js → runtime/logging/logger.js} +0 -0
- /package/dist/{db → runtime/state/db}/schema.js +0 -0
- /package/dist/{composite-activity.js → workspaces/composite/composite-activity.js} +0 -0
- /package/dist/{composite-workspaces.js → workspaces/composite/composite-workspaces.js} +0 -0
- /package/dist/{git.js → workspaces/git/git.js} +0 -0
- /package/dist/{workspace-presentation.js → workspaces/presentation/workspace-presentation.js} +0 -0
- /package/dist/{remote-transport.js → workspaces/relay/transport/remote-transport.js} +0 -0
- /package/dist/{workspace-task-reminders.js → workspaces/tasks/workspace-task-reminders.js} +0 -0
- /package/dist/{workspace-tasks.js → workspaces/tasks/workspace-tasks.js} +0 -0
- /package/scripts/{lsp-interop-support.mjs → lsp/interop-support.mjs} +0 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { access, mkdir, readFile, realpath, writeFile, } from "node:fs/promises";
|
|
3
|
+
import { dirname } from "node:path";
|
|
4
|
+
import { createPatch } from "diff";
|
|
5
|
+
import { resolveCanonicalAllowedPath } from "./roots.js";
|
|
6
|
+
const DEFAULT_MAX_LINES = 2_000;
|
|
7
|
+
const DEFAULT_MAX_BYTES = 50 * 1_024;
|
|
8
|
+
const mutationQueues = new Map();
|
|
9
|
+
function textContent(text) {
|
|
10
|
+
return [{ type: "text", text }];
|
|
11
|
+
}
|
|
12
|
+
function failed(error) {
|
|
13
|
+
return {
|
|
14
|
+
content: textContent(error instanceof Error ? error.message : String(error)),
|
|
15
|
+
isError: true,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export async function readFileTool(input, context) {
|
|
19
|
+
try {
|
|
20
|
+
const path = await resolveCanonicalAllowedPath(input.path, context.cwd, context.readRoots ?? [context.root]);
|
|
21
|
+
await access(path, constants.R_OK);
|
|
22
|
+
const buffer = await readFile(path);
|
|
23
|
+
const mimeType = supportedImageMimeType(buffer);
|
|
24
|
+
if (mimeType) {
|
|
25
|
+
return {
|
|
26
|
+
content: [
|
|
27
|
+
{ type: "text", text: `Read image file [${mimeType}]` },
|
|
28
|
+
{ type: "image", data: buffer.toString("base64"), mimeType },
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return { content: textContent(readText(buffer.toString("utf8"), input)) };
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
return failed(error);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export async function writeFileTool(input, context) {
|
|
39
|
+
try {
|
|
40
|
+
const path = await resolveCanonicalAllowedPath(input.path, context.cwd, context.fileRoots ?? [context.root]);
|
|
41
|
+
await withMutationQueue(path, async () => {
|
|
42
|
+
await mkdir(dirname(path), { recursive: true });
|
|
43
|
+
await writeFile(path, input.content, "utf8");
|
|
44
|
+
});
|
|
45
|
+
return { content: textContent(`Successfully wrote ${input.content.length} bytes to ${input.path}`) };
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
return failed(error);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export async function editFileTool(input, context, signal) {
|
|
52
|
+
try {
|
|
53
|
+
const path = await resolveCanonicalAllowedPath(input.path, context.cwd, context.fileRoots ?? [context.root]);
|
|
54
|
+
const details = await withMutationQueue(path, async () => {
|
|
55
|
+
signal?.throwIfAborted();
|
|
56
|
+
await access(path, constants.R_OK | constants.W_OK);
|
|
57
|
+
signal?.throwIfAborted();
|
|
58
|
+
const raw = await readFile(path, "utf8");
|
|
59
|
+
signal?.throwIfAborted();
|
|
60
|
+
const applied = applyExactEdits(raw, input.edits, input.path);
|
|
61
|
+
await writeFile(path, applied.output, "utf8");
|
|
62
|
+
signal?.throwIfAborted();
|
|
63
|
+
return applied.details;
|
|
64
|
+
});
|
|
65
|
+
return {
|
|
66
|
+
content: textContent(`Successfully replaced ${input.edits.length} block(s) in ${input.path}.`),
|
|
67
|
+
details,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
return failed(error);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export async function preflightEditFiles(paths, edits, context, signal) {
|
|
75
|
+
const seen = new Map();
|
|
76
|
+
for (const path of paths) {
|
|
77
|
+
signal?.throwIfAborted();
|
|
78
|
+
const absolute = await resolveCanonicalAllowedPath(path, context.cwd, context.fileRoots ?? [context.root]);
|
|
79
|
+
const canonical = await realpath(absolute);
|
|
80
|
+
const previous = seen.get(canonical);
|
|
81
|
+
if (previous) {
|
|
82
|
+
throw new Error(`Bulk Edit targets overlap: ${previous} and ${path} resolve to the same file.`);
|
|
83
|
+
}
|
|
84
|
+
seen.set(canonical, path);
|
|
85
|
+
await access(absolute, constants.R_OK | constants.W_OK);
|
|
86
|
+
const content = await readFile(absolute, "utf8");
|
|
87
|
+
applyExactEdits(content, edits, path);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function readText(content, input) {
|
|
91
|
+
const lines = content.split("\n");
|
|
92
|
+
const startIndex = Math.max(0, (input.offset ?? 1) - 1);
|
|
93
|
+
if (startIndex >= lines.length) {
|
|
94
|
+
throw new Error(`Offset ${input.offset} is beyond end of file (${lines.length} lines total)`);
|
|
95
|
+
}
|
|
96
|
+
const explicitEnd = input.limit === undefined
|
|
97
|
+
? lines.length
|
|
98
|
+
: Math.min(lines.length, startIndex + input.limit);
|
|
99
|
+
const selected = lines.slice(startIndex, explicitEnd);
|
|
100
|
+
const limited = truncateLinesAndBytes(selected, DEFAULT_MAX_LINES, DEFAULT_MAX_BYTES);
|
|
101
|
+
const startLine = startIndex + 1;
|
|
102
|
+
const endLine = startLine + limited.lines.length - 1;
|
|
103
|
+
let output = limited.lines.join("\n");
|
|
104
|
+
if (limited.truncated) {
|
|
105
|
+
const nextOffset = endLine + 1;
|
|
106
|
+
output += `\n\n[Showing lines ${startLine}-${endLine} of ${lines.length}. Use offset=${nextOffset} to continue.]`;
|
|
107
|
+
}
|
|
108
|
+
else if (explicitEnd < lines.length) {
|
|
109
|
+
const remaining = lines.length - explicitEnd;
|
|
110
|
+
output += `\n\n[${remaining} more lines in file. Use offset=${explicitEnd + 1} to continue.]`;
|
|
111
|
+
}
|
|
112
|
+
return output;
|
|
113
|
+
}
|
|
114
|
+
function truncateLinesAndBytes(lines, maxLines, maxBytes) {
|
|
115
|
+
const output = [];
|
|
116
|
+
let bytes = 0;
|
|
117
|
+
for (const line of lines) {
|
|
118
|
+
if (output.length >= maxLines)
|
|
119
|
+
return { lines: output, truncated: true };
|
|
120
|
+
const prefix = output.length === 0 ? "" : "\n";
|
|
121
|
+
const lineBytes = Buffer.byteLength(prefix + line, "utf8");
|
|
122
|
+
if (bytes + lineBytes > maxBytes) {
|
|
123
|
+
if (output.length === 0) {
|
|
124
|
+
return {
|
|
125
|
+
lines: [`[Line exceeds ${maxBytes} byte read limit. Use bash for byte-oriented inspection.]`],
|
|
126
|
+
truncated: true,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return { lines: output, truncated: true };
|
|
130
|
+
}
|
|
131
|
+
output.push(line);
|
|
132
|
+
bytes += lineBytes;
|
|
133
|
+
}
|
|
134
|
+
return { lines: output, truncated: false };
|
|
135
|
+
}
|
|
136
|
+
function applyExactEdits(rawContent, edits, displayPath) {
|
|
137
|
+
if (!Array.isArray(edits) || edits.length === 0) {
|
|
138
|
+
throw new Error("Edit tool input is invalid. edits must contain at least one replacement.");
|
|
139
|
+
}
|
|
140
|
+
const { bom, text } = stripBom(rawContent);
|
|
141
|
+
const lineEnding = detectLineEnding(text);
|
|
142
|
+
const original = normalizeLineEndings(text);
|
|
143
|
+
const prepared = edits.map((edit, index) => {
|
|
144
|
+
if (typeof edit.oldText !== "string" || typeof edit.newText !== "string" || edit.oldText.length === 0) {
|
|
145
|
+
throw new Error(`Edit ${index + 1} must contain non-empty oldText and string newText.`);
|
|
146
|
+
}
|
|
147
|
+
const oldText = normalizeLineEndings(edit.oldText);
|
|
148
|
+
const newText = normalizeLineEndings(edit.newText);
|
|
149
|
+
const start = original.indexOf(oldText);
|
|
150
|
+
if (start < 0) {
|
|
151
|
+
throw new Error(`Could not edit ${displayPath}: oldText for replacement ${index + 1} did not match.`);
|
|
152
|
+
}
|
|
153
|
+
if (original.indexOf(oldText, start + oldText.length) >= 0) {
|
|
154
|
+
throw new Error(`Could not edit ${displayPath}: oldText for replacement ${index + 1} must be unique but matched multiple locations.`);
|
|
155
|
+
}
|
|
156
|
+
return { start, end: start + oldText.length, newText };
|
|
157
|
+
}).sort((left, right) => left.start - right.start);
|
|
158
|
+
for (let index = 1; index < prepared.length; index += 1) {
|
|
159
|
+
if (prepared[index].start < prepared[index - 1].end) {
|
|
160
|
+
throw new Error(`Could not edit ${displayPath}: replacements overlap in the original file.`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
let cursor = 0;
|
|
164
|
+
let updated = "";
|
|
165
|
+
for (const edit of prepared) {
|
|
166
|
+
updated += original.slice(cursor, edit.start);
|
|
167
|
+
updated += edit.newText;
|
|
168
|
+
cursor = edit.end;
|
|
169
|
+
}
|
|
170
|
+
updated += original.slice(cursor);
|
|
171
|
+
const firstChangedLine = prepared.length === 0
|
|
172
|
+
? undefined
|
|
173
|
+
: original.slice(0, prepared[0].start).split("\n").length;
|
|
174
|
+
const patch = createPatch(displayPath, original, updated, "", "", { context: 3 });
|
|
175
|
+
const diff = patch;
|
|
176
|
+
const output = bom + restoreLineEndings(updated, lineEnding);
|
|
177
|
+
return { output, details: { diff, patch, firstChangedLine } };
|
|
178
|
+
}
|
|
179
|
+
function stripBom(content) {
|
|
180
|
+
return content.charCodeAt(0) === 0xfeff
|
|
181
|
+
? { bom: "\ufeff", text: content.slice(1) }
|
|
182
|
+
: { bom: "", text: content };
|
|
183
|
+
}
|
|
184
|
+
function detectLineEnding(content) {
|
|
185
|
+
if (content.includes("\r\n"))
|
|
186
|
+
return "\r\n";
|
|
187
|
+
if (content.includes("\r"))
|
|
188
|
+
return "\r";
|
|
189
|
+
return "\n";
|
|
190
|
+
}
|
|
191
|
+
function normalizeLineEndings(content) {
|
|
192
|
+
return content.replace(/\r\n?/g, "\n");
|
|
193
|
+
}
|
|
194
|
+
function restoreLineEndings(content, lineEnding) {
|
|
195
|
+
return lineEnding === "\n" ? content : content.replaceAll("\n", lineEnding);
|
|
196
|
+
}
|
|
197
|
+
function supportedImageMimeType(buffer) {
|
|
198
|
+
if (buffer.length >= 8 && buffer.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]))) {
|
|
199
|
+
return "image/png";
|
|
200
|
+
}
|
|
201
|
+
if (buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {
|
|
202
|
+
return "image/jpeg";
|
|
203
|
+
}
|
|
204
|
+
const header6 = buffer.subarray(0, 6).toString("ascii");
|
|
205
|
+
if (header6 === "GIF87a" || header6 === "GIF89a")
|
|
206
|
+
return "image/gif";
|
|
207
|
+
if (buffer.length >= 12 &&
|
|
208
|
+
buffer.subarray(0, 4).toString("ascii") === "RIFF" &&
|
|
209
|
+
buffer.subarray(8, 12).toString("ascii") === "WEBP")
|
|
210
|
+
return "image/webp";
|
|
211
|
+
if (buffer.length >= 2 && buffer.subarray(0, 2).toString("ascii") === "BM")
|
|
212
|
+
return "image/bmp";
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
async function withMutationQueue(path, operation) {
|
|
216
|
+
const canonical = await canonicalMutationPath(path);
|
|
217
|
+
const previous = mutationQueues.get(canonical) ?? Promise.resolve();
|
|
218
|
+
let release;
|
|
219
|
+
const current = new Promise((resolve) => {
|
|
220
|
+
release = resolve;
|
|
221
|
+
});
|
|
222
|
+
mutationQueues.set(canonical, previous.then(() => current));
|
|
223
|
+
await previous;
|
|
224
|
+
try {
|
|
225
|
+
return await operation();
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
release();
|
|
229
|
+
if (mutationQueues.get(canonical) === current)
|
|
230
|
+
mutationQueues.delete(canonical);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async function canonicalMutationPath(path) {
|
|
234
|
+
try {
|
|
235
|
+
return await realpath(path);
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
try {
|
|
239
|
+
return `${await realpath(dirname(path))}/${path.slice(dirname(path).length + 1)}`;
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return path;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { terminateProcessTree } from "../process/process-platform.js";
|
|
3
|
+
const MAX_CAPTURE_BYTES = 64 * 1024;
|
|
4
|
+
export function executeHookCommand(input) {
|
|
5
|
+
input.signal?.throwIfAborted();
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const child = spawn(input.executable, input.args, {
|
|
8
|
+
cwd: input.cwd,
|
|
9
|
+
env: input.env,
|
|
10
|
+
detached: input.detached,
|
|
11
|
+
windowsHide: true,
|
|
12
|
+
windowsVerbatimArguments: input.windowsVerbatimArguments,
|
|
13
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
14
|
+
});
|
|
15
|
+
let stdout = "";
|
|
16
|
+
let stderr = "";
|
|
17
|
+
let timedOut = false;
|
|
18
|
+
let forceKillTimer;
|
|
19
|
+
let aborted = false;
|
|
20
|
+
const abort = () => {
|
|
21
|
+
if (aborted)
|
|
22
|
+
return;
|
|
23
|
+
aborted = true;
|
|
24
|
+
terminateProcessTree(child, "SIGTERM", input.detached);
|
|
25
|
+
forceKillTimer = setTimeout(() => {
|
|
26
|
+
terminateProcessTree(child, "SIGKILL", input.detached);
|
|
27
|
+
}, 500);
|
|
28
|
+
forceKillTimer.unref();
|
|
29
|
+
};
|
|
30
|
+
input.signal?.addEventListener("abort", abort, { once: true });
|
|
31
|
+
child.stdout?.on("data", (chunk) => {
|
|
32
|
+
stdout = appendCaptured(stdout, chunk);
|
|
33
|
+
});
|
|
34
|
+
child.stderr?.on("data", (chunk) => {
|
|
35
|
+
stderr = appendCaptured(stderr, chunk);
|
|
36
|
+
});
|
|
37
|
+
const timeout = setTimeout(() => {
|
|
38
|
+
timedOut = true;
|
|
39
|
+
terminateProcessTree(child, "SIGTERM", input.detached);
|
|
40
|
+
forceKillTimer = setTimeout(() => {
|
|
41
|
+
terminateProcessTree(child, "SIGKILL", input.detached);
|
|
42
|
+
}, 500);
|
|
43
|
+
forceKillTimer.unref();
|
|
44
|
+
}, input.timeoutMs);
|
|
45
|
+
timeout.unref();
|
|
46
|
+
child.once("error", (error) => {
|
|
47
|
+
clearTimeout(timeout);
|
|
48
|
+
if (forceKillTimer)
|
|
49
|
+
clearTimeout(forceKillTimer);
|
|
50
|
+
input.signal?.removeEventListener("abort", abort);
|
|
51
|
+
reject(error);
|
|
52
|
+
});
|
|
53
|
+
child.once("close", (exitCode, signal) => {
|
|
54
|
+
clearTimeout(timeout);
|
|
55
|
+
if (forceKillTimer)
|
|
56
|
+
clearTimeout(forceKillTimer);
|
|
57
|
+
input.signal?.removeEventListener("abort", abort);
|
|
58
|
+
if (aborted) {
|
|
59
|
+
reject(input.signal?.reason instanceof Error
|
|
60
|
+
? input.signal.reason
|
|
61
|
+
: Object.assign(new Error("Hook execution cancelled by Host."), { name: "AbortError" }));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
resolve({ exitCode, signal, stdout, stderr, timedOut });
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function appendCaptured(current, chunk) {
|
|
69
|
+
if (Buffer.byteLength(current) >= MAX_CAPTURE_BYTES)
|
|
70
|
+
return current;
|
|
71
|
+
const next = current + chunk.toString();
|
|
72
|
+
if (Buffer.byteLength(next) <= MAX_CAPTURE_BYTES)
|
|
73
|
+
return next;
|
|
74
|
+
return Buffer.from(next).subarray(0, MAX_CAPTURE_BYTES).toString("utf8");
|
|
75
|
+
}
|
|
76
|
+
export function hookFailureOutput(stdout, stderr) {
|
|
77
|
+
const output = (stderr.trim() || stdout.trim()).replace(/\s+/g, " ");
|
|
78
|
+
return output.length > 1_000 ? `${output.slice(0, 997)}...` : output;
|
|
79
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { HOOK_EVENTS, loadProjectHookConfig, mergeHookConfigs, parseHookConfig, } from "./hooks.js";
|
|
3
|
-
import { loadForgeRelayFiles } from "
|
|
3
|
+
import { loadForgeRelayFiles } from "../../runtime/config/user-config.js";
|
|
4
4
|
export async function checkHookConfiguration(projectRoot, globalHooks = loadGlobalHooks()) {
|
|
5
5
|
const globalEntries = flattenHooks(globalHooks, "global");
|
|
6
6
|
const project = await loadProjectHookConfig(projectRoot);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import { readFile, readdir } from "node:fs/promises";
|
|
3
2
|
import { join } from "node:path";
|
|
4
3
|
import { performance } from "node:perf_hooks";
|
|
5
|
-
import { commandPreview, logEvent, workspaceLogLabel } from "
|
|
6
|
-
import { resolveShellCommand
|
|
4
|
+
import { commandPreview, logEvent, workspaceLogLabel } from "../../runtime/logging/logger.js";
|
|
5
|
+
import { resolveShellCommand } from "../process/process-platform.js";
|
|
6
|
+
import { executeHookCommand, hookFailureOutput } from "./command-runner.js";
|
|
7
7
|
export const HOOK_EVENTS = [
|
|
8
8
|
"WorkspaceOpen",
|
|
9
9
|
"BeforeTool",
|
|
@@ -19,7 +19,6 @@ const DEFAULT_HOOK_TIMEOUT_SECONDS = 30;
|
|
|
19
19
|
const MAX_HOOK_TIMEOUT_SECONDS = 300;
|
|
20
20
|
const PROJECT_HOOKS_PATH = join(".forgerelay", "hooks.json");
|
|
21
21
|
const PROJECT_HOOKS_DIR = join(".forgerelay", "hooks");
|
|
22
|
-
const MAX_CAPTURE_BYTES = 64 * 1024;
|
|
23
22
|
const BLOCKING_EVENTS = new Set(["BeforeTool", "BeforeWorktreeClose"]);
|
|
24
23
|
const EVENT_SET = new Set(HOOK_EVENTS);
|
|
25
24
|
export class HookExecutionError extends Error {
|
|
@@ -529,82 +528,6 @@ function hookEnvironment(baseEnv, event, invocation) {
|
|
|
529
528
|
FORGERELAY_TOOL_NAME: typeof invocation.payload?.tool === "string" ? invocation.payload.tool : undefined,
|
|
530
529
|
};
|
|
531
530
|
}
|
|
532
|
-
function executeHookCommand(input) {
|
|
533
|
-
input.signal?.throwIfAborted();
|
|
534
|
-
return new Promise((resolve, reject) => {
|
|
535
|
-
const child = spawn(input.executable, input.args, {
|
|
536
|
-
cwd: input.cwd,
|
|
537
|
-
env: input.env,
|
|
538
|
-
detached: input.detached,
|
|
539
|
-
windowsHide: true,
|
|
540
|
-
windowsVerbatimArguments: input.windowsVerbatimArguments,
|
|
541
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
542
|
-
});
|
|
543
|
-
let stdout = "";
|
|
544
|
-
let stderr = "";
|
|
545
|
-
let timedOut = false;
|
|
546
|
-
let forceKillTimer;
|
|
547
|
-
let aborted = false;
|
|
548
|
-
const abort = () => {
|
|
549
|
-
if (aborted)
|
|
550
|
-
return;
|
|
551
|
-
aborted = true;
|
|
552
|
-
terminateProcessTree(child, "SIGTERM", input.detached);
|
|
553
|
-
forceKillTimer = setTimeout(() => {
|
|
554
|
-
terminateProcessTree(child, "SIGKILL", input.detached);
|
|
555
|
-
}, 500);
|
|
556
|
-
forceKillTimer.unref();
|
|
557
|
-
};
|
|
558
|
-
input.signal?.addEventListener("abort", abort, { once: true });
|
|
559
|
-
child.stdout?.on("data", (chunk) => {
|
|
560
|
-
stdout = appendCaptured(stdout, chunk);
|
|
561
|
-
});
|
|
562
|
-
child.stderr?.on("data", (chunk) => {
|
|
563
|
-
stderr = appendCaptured(stderr, chunk);
|
|
564
|
-
});
|
|
565
|
-
const timeout = setTimeout(() => {
|
|
566
|
-
timedOut = true;
|
|
567
|
-
terminateProcessTree(child, "SIGTERM", input.detached);
|
|
568
|
-
forceKillTimer = setTimeout(() => {
|
|
569
|
-
terminateProcessTree(child, "SIGKILL", input.detached);
|
|
570
|
-
}, 500);
|
|
571
|
-
forceKillTimer.unref();
|
|
572
|
-
}, input.timeoutMs);
|
|
573
|
-
timeout.unref();
|
|
574
|
-
child.once("error", (error) => {
|
|
575
|
-
clearTimeout(timeout);
|
|
576
|
-
if (forceKillTimer)
|
|
577
|
-
clearTimeout(forceKillTimer);
|
|
578
|
-
input.signal?.removeEventListener("abort", abort);
|
|
579
|
-
reject(error);
|
|
580
|
-
});
|
|
581
|
-
child.once("close", (exitCode, signal) => {
|
|
582
|
-
clearTimeout(timeout);
|
|
583
|
-
if (forceKillTimer)
|
|
584
|
-
clearTimeout(forceKillTimer);
|
|
585
|
-
input.signal?.removeEventListener("abort", abort);
|
|
586
|
-
if (aborted) {
|
|
587
|
-
reject(input.signal?.reason instanceof Error
|
|
588
|
-
? input.signal.reason
|
|
589
|
-
: Object.assign(new Error("Hook execution cancelled by Host."), { name: "AbortError" }));
|
|
590
|
-
return;
|
|
591
|
-
}
|
|
592
|
-
resolve({ exitCode, signal, stdout, stderr, timedOut });
|
|
593
|
-
});
|
|
594
|
-
});
|
|
595
|
-
}
|
|
596
|
-
function appendCaptured(current, chunk) {
|
|
597
|
-
if (Buffer.byteLength(current) >= MAX_CAPTURE_BYTES)
|
|
598
|
-
return current;
|
|
599
|
-
const next = current + chunk.toString();
|
|
600
|
-
if (Buffer.byteLength(next) <= MAX_CAPTURE_BYTES)
|
|
601
|
-
return next;
|
|
602
|
-
return Buffer.from(next).subarray(0, MAX_CAPTURE_BYTES).toString("utf8");
|
|
603
|
-
}
|
|
604
|
-
function hookFailureOutput(stdout, stderr) {
|
|
605
|
-
const output = (stderr.trim() || stdout.trim()).replace(/\s+/g, " ");
|
|
606
|
-
return output.length > 1_000 ? `${output.slice(0, 997)}...` : output;
|
|
607
|
-
}
|
|
608
531
|
function errorMessage(error) {
|
|
609
532
|
return error instanceof Error ? error.message : String(error);
|
|
610
533
|
}
|
|
@@ -23,7 +23,7 @@ function htmlEscape(value) {
|
|
|
23
23
|
.replaceAll("'", "'");
|
|
24
24
|
}
|
|
25
25
|
function formHtml(params) {
|
|
26
|
-
const scopeText = params.scopes.length > 0 ? params.scopes.join(" ") : "
|
|
26
|
+
const scopeText = params.scopes.length > 0 ? params.scopes.join(" ") : "forgerelay";
|
|
27
27
|
const resourceText = params.resource?.href ?? "ForgeRelay MCP endpoint";
|
|
28
28
|
const error = params.error
|
|
29
29
|
? `<p class="error">${htmlEscape(params.error)}</p>`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { InvalidRequestError } from "@modelcontextprotocol/sdk/server/auth/errors.js";
|
|
3
|
-
import { openDatabase } from "
|
|
3
|
+
import { openDatabase } from "../../runtime/state/db/client.js";
|
|
4
4
|
function redirectHostAllowed(redirectUri, allowedHosts) {
|
|
5
5
|
let parsed;
|
|
6
6
|
try {
|
|
@@ -9,9 +9,10 @@ function redirectHostAllowed(redirectUri, allowedHosts) {
|
|
|
9
9
|
catch {
|
|
10
10
|
return false;
|
|
11
11
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(parsed.hostname);
|
|
13
|
+
if (loopback)
|
|
14
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
15
|
+
return parsed.protocol === "https:" && allowedHosts.includes(parsed.hostname);
|
|
15
16
|
}
|
|
16
17
|
export class SqliteOAuthStore {
|
|
17
18
|
database;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { preflightDeletePaths } from "../file-mutations.js";
|
|
2
|
-
import { preflightEditFiles } from "../
|
|
1
|
+
import { preflightDeletePaths } from "../filesystem/file-mutations.js";
|
|
2
|
+
import { preflightEditFiles } from "../filesystem/filesystem-tools.js";
|
|
3
3
|
import { openAiConversationScopeId } from "../request-meta.js";
|
|
4
4
|
import { executeSequentialBulkMutation } from "./bulk-mutation.js";
|
|
5
5
|
export class NativeBulkMutationExecutor {
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
|
|
4
|
+
import { logEvent, transportSessionIdPrefix } from "../../runtime/logging/logger.js";
|
|
5
|
+
import { MCP_APP_RESOURCE_TEMPLATE_REVISION, readActivityPanelAppManifestEntry, resolveActivityPanelAppIdentity, } from "./mcp-app-template.js";
|
|
6
|
+
const cachedIdentities = new Map();
|
|
7
|
+
/**
|
|
8
|
+
* Build the complete Host-facing Activity Panel resource contract. Keeping URI
|
|
9
|
+
* identity, CSP, bootstrap HTML, and tool metadata in one module prevents a UI
|
|
10
|
+
* revision from changing on one surface while another surface keeps stale Host
|
|
11
|
+
* cache metadata.
|
|
12
|
+
*/
|
|
13
|
+
export function createActivityPanelApp(config, fallbackRevision) {
|
|
14
|
+
const identity = currentIdentity(config, fallbackRevision);
|
|
15
|
+
const domain = new URL(config.publicBaseUrl).origin;
|
|
16
|
+
const csp = activityPanelCsp(config);
|
|
17
|
+
return {
|
|
18
|
+
uri: identity.uri,
|
|
19
|
+
resourceMetadata: {
|
|
20
|
+
description: "ForgeRelay unified Workspace and Activity UI for one Host Turn.",
|
|
21
|
+
_meta: { ui: { domain, csp } },
|
|
22
|
+
},
|
|
23
|
+
toolMeta: config.widgets === "off"
|
|
24
|
+
? { _meta: {} }
|
|
25
|
+
: {
|
|
26
|
+
_meta: {
|
|
27
|
+
ui: {
|
|
28
|
+
resourceUri: identity.uri,
|
|
29
|
+
visibility: ["model", "app"],
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
readResource: (requestedUri, transportSessionId) => readActivityPanelAppResource(config, fallbackRevision, requestedUri, transportSessionId),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function activityPanelAssetDirectory() {
|
|
37
|
+
return fileURLToPath(new URL("../../../dist/ui", import.meta.url));
|
|
38
|
+
}
|
|
39
|
+
export function setActivityPanelAssetHeaders(res) {
|
|
40
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
41
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, HEAD, OPTIONS");
|
|
42
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Range");
|
|
43
|
+
res.setHeader("Cross-Origin-Resource-Policy", "cross-origin");
|
|
44
|
+
}
|
|
45
|
+
function appResourceContractRevision(config) {
|
|
46
|
+
return [
|
|
47
|
+
MCP_APP_RESOURCE_TEMPLATE_REVISION,
|
|
48
|
+
`publicBaseUrls=${JSON.stringify(config.publicBaseUrls)}`,
|
|
49
|
+
].join("\0");
|
|
50
|
+
}
|
|
51
|
+
function currentIdentity(config, fallbackRevision) {
|
|
52
|
+
const contractRevision = appResourceContractRevision(config);
|
|
53
|
+
const cacheKey = `${fallbackRevision}\0${contractRevision}`;
|
|
54
|
+
let identity = cachedIdentities.get(cacheKey);
|
|
55
|
+
if (!identity) {
|
|
56
|
+
identity = resolveActivityPanelAppIdentity({
|
|
57
|
+
manifestUrl: uiManifestUrl(),
|
|
58
|
+
buildDirectoryUrl: uiBuildDirectoryUrl(),
|
|
59
|
+
fallbackRevision,
|
|
60
|
+
resourceTemplateRevision: contractRevision,
|
|
61
|
+
});
|
|
62
|
+
cachedIdentities.set(cacheKey, identity);
|
|
63
|
+
}
|
|
64
|
+
return identity;
|
|
65
|
+
}
|
|
66
|
+
function activityPanelCsp(config) {
|
|
67
|
+
return {
|
|
68
|
+
resourceDomains: [...config.publicBaseUrls],
|
|
69
|
+
connectDomains: [...config.publicBaseUrls],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
async function readActivityPanelAppResource(config, fallbackRevision, requestedUri, transportSessionId) {
|
|
73
|
+
const identity = currentIdentity(config, fallbackRevision);
|
|
74
|
+
const entry = readActivityPanelAppManifestEntry(uiManifestUrl());
|
|
75
|
+
const domain = new URL(config.publicBaseUrl).origin;
|
|
76
|
+
const csp = activityPanelCsp(config);
|
|
77
|
+
try {
|
|
78
|
+
await assertMcpAppAssets(entry);
|
|
79
|
+
const result = {
|
|
80
|
+
contents: [{
|
|
81
|
+
uri: requestedUri,
|
|
82
|
+
mimeType: RESOURCE_MIME_TYPE,
|
|
83
|
+
text: activityPanelAppHtml(config, entry),
|
|
84
|
+
_meta: {
|
|
85
|
+
ui: { domain, csp },
|
|
86
|
+
// MCP Apps defines these values under `_meta.ui`. Inspector 2.3.0
|
|
87
|
+
// still reads them directly from the content-item metadata, so mirror
|
|
88
|
+
// them until that installed-host compatibility gap is retired.
|
|
89
|
+
domain,
|
|
90
|
+
csp,
|
|
91
|
+
},
|
|
92
|
+
}],
|
|
93
|
+
};
|
|
94
|
+
logEvent(config.logging, "debug", "mcp_app_template_read", {
|
|
95
|
+
requestedUri,
|
|
96
|
+
currentUri: identity.uri,
|
|
97
|
+
transportSessionIdPrefix: transportSessionIdPrefix(transportSessionId),
|
|
98
|
+
});
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
logEvent(config.logging, "warn", "mcp_app_template_read_failed", {
|
|
103
|
+
requestedUri,
|
|
104
|
+
currentUri: identity.uri,
|
|
105
|
+
error: error instanceof Error ? error.message : String(error),
|
|
106
|
+
transportSessionIdPrefix: transportSessionIdPrefix(transportSessionId),
|
|
107
|
+
});
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function activityPanelAppHtml(config, entry) {
|
|
112
|
+
const baseUrl = `${config.publicBaseUrl.replace(/\/+$/, "")}/mcp-app-assets`;
|
|
113
|
+
const assetUrl = (assetPath) => `${baseUrl}/${assetPath.replace(/^\/+/, "")}`;
|
|
114
|
+
const stylesheets = (entry.css ?? [])
|
|
115
|
+
.map((stylesheet) => ` <link rel="stylesheet" crossorigin href="${assetUrl(stylesheet)}" />`)
|
|
116
|
+
.join("\n");
|
|
117
|
+
return `<!doctype html>
|
|
118
|
+
<html lang="en">
|
|
119
|
+
<head>
|
|
120
|
+
<meta charset="UTF-8" />
|
|
121
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
122
|
+
<title>ForgeRelay Activity Panel</title>
|
|
123
|
+
<script type="module" crossorigin src="${assetUrl(entry.file)}"></script>
|
|
124
|
+
${stylesheets}
|
|
125
|
+
</head>
|
|
126
|
+
<body>
|
|
127
|
+
<main id="app" class="shell">
|
|
128
|
+
<section class="empty">Waiting for Activity Panel state.</section>
|
|
129
|
+
</main>
|
|
130
|
+
</body>
|
|
131
|
+
</html>`;
|
|
132
|
+
}
|
|
133
|
+
async function assertMcpAppAssets(entry) {
|
|
134
|
+
const candidates = [entry.file, ...(entry.css ?? [])].map((assetPath) => new URL(`../../../dist/ui/${assetPath}`, import.meta.url));
|
|
135
|
+
for (const candidate of candidates)
|
|
136
|
+
await access(candidate);
|
|
137
|
+
}
|
|
138
|
+
function uiManifestUrl() {
|
|
139
|
+
return new URL("../../../dist/ui/.vite/manifest.json", import.meta.url);
|
|
140
|
+
}
|
|
141
|
+
function uiBuildDirectoryUrl() {
|
|
142
|
+
return new URL("../../../dist/ui/", import.meta.url);
|
|
143
|
+
}
|