@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.
Files changed (246) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +10 -18
  3. package/capabilities/host-integration/GUIDE.md +2 -2
  4. package/dist/activity/{audit-store.js → history/audit-store.js} +129 -21
  5. package/dist/activity/history/bash-output-store.js +250 -0
  6. package/dist/activity/{host-turn-store.js → history/host-turn-store.js} +4 -1
  7. package/dist/activity/{query-service.js → history/query-service.js} +13 -7
  8. package/dist/activity/{lifecycle.js → runtime/lifecycle.js} +1 -1
  9. package/dist/activity/{mcp-query-tools.js → runtime/mcp-query-tools.js} +3 -3
  10. package/dist/activity/storage/history.js +12 -0
  11. package/dist/activity/storage/paths.js +56 -0
  12. package/dist/activity/storage/segmented-log.js +149 -0
  13. package/dist/cli/setup-support.js +116 -0
  14. package/dist/cli.js +18 -126
  15. package/dist/lsp/code-intelligence.js +1 -1
  16. package/dist/lsp/test-support/server-fixture.js +9 -9
  17. package/dist/mcp/filesystem/filesystem-tools.js +245 -0
  18. package/dist/mcp/hooks/command-runner.js +79 -0
  19. package/dist/{hook-cli.js → mcp/hooks/hook-cli.js} +1 -1
  20. package/dist/{hooks.js → mcp/hooks/hooks.js} +3 -80
  21. package/dist/{oauth-provider.js → mcp/oauth/oauth-provider.js} +1 -1
  22. package/dist/{oauth-store.js → mcp/oauth/oauth-store.js} +5 -4
  23. package/dist/{operations → mcp/operations}/native-bulk-mutations.js +2 -2
  24. package/dist/mcp/panel/app.js +143 -0
  25. package/dist/{mcp-app-template.js → mcp/panel/mcp-app-template.js} +2 -38
  26. package/dist/mcp/process/runtime.js +257 -0
  27. package/dist/mcp/process/tools.js +346 -0
  28. package/dist/mcp/server/core/activity-support.js +70 -0
  29. package/dist/{capabilities.js → mcp/server/core/capabilities.js} +5 -3
  30. package/dist/{capability-registry.js → mcp/server/core/capability-registry.js} +2 -2
  31. package/dist/mcp/server/core/capability-support.js +90 -0
  32. package/dist/mcp/server/core/schemas.js +216 -0
  33. package/dist/mcp/server/core/tool-support.js +207 -0
  34. package/dist/mcp/server/core/version.js +9 -0
  35. package/dist/mcp/server/operations/runtime/filesystem-tools.js +400 -0
  36. package/dist/mcp/server/operations/runtime/operation-runtime.js +523 -0
  37. package/dist/mcp/server/transport/http-server.js +301 -0
  38. package/dist/mcp/server/transport/http-support.js +43 -0
  39. package/dist/mcp/server/workspace/runtime/workspace-open-presentation.js +222 -0
  40. package/dist/mcp/server/workspace/runtime/workspace-open-schema.js +187 -0
  41. package/dist/mcp/server/workspace/runtime/workspace-open.js +552 -0
  42. package/dist/mcp/server/workspace/runtime/workspace-tools.js +601 -0
  43. package/dist/{config.js → runtime/config/config.js} +3 -3
  44. package/dist/{user-config.js → runtime/config/user-config.js} +14 -54
  45. package/dist/{db → runtime/state/db}/client.js +1 -1
  46. package/dist/{db → runtime/state/db}/migrations.js +34 -3
  47. package/dist/runtime/state/lock/file-lock.js +130 -0
  48. package/dist/runtime/testing/server-fixture.js +190 -0
  49. package/dist/server.js +139 -4192
  50. package/dist/subagents/providers/adapters/claude.js +86 -52
  51. package/dist/subagents/providers/adapters/codex.js +103 -39
  52. package/dist/subagents/providers/adapters/opencode.js +114 -84
  53. package/dist/subagents/providers/adapters/pi.js +59 -8
  54. package/dist/subagents/providers/availability.js +15 -30
  55. package/dist/subagents/providers/runtime/external-command.js +56 -0
  56. package/dist/subagents/sessions/capability.js +1 -1
  57. package/dist/subagents/sessions/execution.js +1 -1
  58. package/dist/subagents/sessions/mcp/activity-audit-test-support.js +30 -0
  59. package/dist/subagents/sessions/store.js +1 -1
  60. package/dist/ui/.vite/manifest.json +342 -409
  61. package/dist/ui/activity-panel-app.html +2 -4
  62. package/dist/ui/assets/activity-panel-app-7Xe0alhm.css +1 -0
  63. package/dist/ui/assets/activity-panel-app-Sl1Jro8v.js +115 -0
  64. package/dist/ui/assets/angular-html-B_yn91wW.js +1 -0
  65. package/dist/ui/assets/{angular-ts-Bxh4-O1-.js → angular-ts-DSgtloKz.js} +1 -1
  66. package/dist/ui/assets/{apl-BmYHKQ6m.js → apl-NatWtF8X.js} +1 -1
  67. package/dist/ui/assets/{astro-C51vqDKq.js → astro-BYFEVBuN.js} +1 -1
  68. package/dist/ui/assets/{blade-CdhVsmwq.js → blade-DoO7yvMR.js} +1 -1
  69. package/dist/ui/assets/c-Rl2hMCcg.js +1 -0
  70. package/dist/ui/assets/{cobol-Cf6azoN_.js → cobol-ByC5mWiB.js} +1 -1
  71. package/dist/ui/assets/{coffee-vVdGzlmt.js → coffee-Dez77Qov.js} +1 -1
  72. package/dist/ui/assets/cpp-DtaAQT9h.js +1 -0
  73. package/dist/ui/assets/{crystal-5UZy6KIX.js → crystal-Bgmbf8ec.js} +1 -1
  74. package/dist/ui/assets/css-C1tjBtVN.js +1 -0
  75. package/dist/ui/assets/{edge-CESgo4ia.js → edge-DeH6u8Wg.js} +1 -1
  76. package/dist/ui/assets/{elixir-9FKfw4cJ.js → elixir-_H3KnmWT.js} +1 -1
  77. package/dist/ui/assets/{elm-Dv7A0aLa.js → elm-CsCe3U28.js} +1 -1
  78. package/dist/ui/assets/{erb-DHFmDvM0.js → erb-CbM-L2g8.js} +1 -1
  79. package/dist/ui/assets/{git-rebase-utbU6CzV.js → git-rebase-D0s5pDh9.js} +1 -1
  80. package/dist/ui/assets/{glimmer-js-t2kiCkyB.js → glimmer-js-BfOToq_A.js} +1 -1
  81. package/dist/ui/assets/{glimmer-ts-F0Jv5okV.js → glimmer-ts-CCUOkL2K.js} +1 -1
  82. package/dist/ui/assets/glsl-DH4ec9dE.js +1 -0
  83. package/dist/ui/assets/graphql-DilAMBoI.js +1 -0
  84. package/dist/ui/assets/{hack-8yjdbaSA.js → hack-BDkNDZlF.js} +1 -1
  85. package/dist/ui/assets/haml-D_QBp6K7.js +1 -0
  86. package/dist/ui/assets/{handlebars-BBFEg4Lo.js → handlebars-DePUrTpC.js} +1 -1
  87. package/dist/ui/assets/heavy-payload-DOwZpO6n.js +290 -0
  88. package/dist/ui/assets/html-By4dNnM4.js +1 -0
  89. package/dist/ui/assets/{html-derivative-DJAQcZyc.js → html-derivative-DDw9Ef4Q.js} +1 -1
  90. package/dist/ui/assets/{http-Dg5LSYRK.js → http-B0QxWc9M.js} +1 -1
  91. package/dist/ui/assets/{hurl-Y5uYzMZR.js → hurl-vMQ4lda1.js} +1 -1
  92. package/dist/ui/assets/java-DYRe2lWG.js +1 -0
  93. package/dist/ui/assets/javascript-CxyGqwcL.js +1 -0
  94. package/dist/ui/assets/{jinja-Bt30h8Ik.js → jinja-JUwV-fZt.js} +1 -1
  95. package/dist/ui/assets/{jison-D5uZ1Nyo.js → jison-7ZqCEvS8.js} +1 -1
  96. package/dist/ui/assets/json-dbltgiiA.js +1 -0
  97. package/dist/ui/assets/jsx-oDopT7YQ.js +1 -0
  98. package/dist/ui/assets/{julia-76hRaaGz.js → julia-CkKiNSav.js} +1 -1
  99. package/dist/ui/assets/{just-CDE7W0vW.js → just-DuqCAKJ3.js} +1 -1
  100. package/dist/ui/assets/{latex-CVO2Byab.js → latex-sudcRDSn.js} +1 -1
  101. package/dist/ui/assets/{liquid-CA-AS3EX.js → liquid-Di-08Js7.js} +1 -1
  102. package/dist/ui/assets/lua-YlqdMoV0.js +1 -0
  103. package/dist/ui/assets/{marko-_BWrPfny.js → marko-CJRNyJDb.js} +1 -1
  104. package/dist/ui/assets/{mdc-PyJv85eS.js → mdc-t12CzZHs.js} +1 -1
  105. package/dist/ui/assets/{nginx-DAWVw8yu.js → nginx-57-b4XwT.js} +1 -1
  106. package/dist/ui/assets/{nim-CKE1dzpU.js → nim-BzrswYQa.js} +1 -1
  107. package/dist/ui/assets/{perl-BBhWva19.js → perl-D9JR_iIh.js} +1 -1
  108. package/dist/ui/assets/{php-ChIeaAoF.js → php-Py2gFHil.js} +1 -1
  109. package/dist/ui/assets/{pug-CMgI5afi.js → pug-PzLLy0h1.js} +1 -1
  110. package/dist/ui/assets/{qml-CjxTkcFH.js → qml-By8fwgV4.js} +1 -1
  111. package/dist/ui/assets/r-CcezTbLq.js +1 -0
  112. package/dist/ui/assets/{razor-i4DbHr8p.js → razor-dG-vdhL_.js} +1 -1
  113. package/dist/ui/assets/regexp-BlsQ-N_8.js +1 -0
  114. package/dist/ui/assets/{rst-oy_97Kl4.js → rst-C2awDCat.js} +1 -1
  115. package/dist/ui/assets/{ruby-C5zYZIyl.js → ruby-zA2W_49d.js} +1 -1
  116. package/dist/ui/assets/{sas-BnMEh2S3.js → sas-D49bhyaN.js} +1 -1
  117. package/dist/ui/assets/scss-BQb-cp01.js +1 -0
  118. package/dist/ui/assets/shellscript-Dga4H_Ni.js +1 -0
  119. package/dist/ui/assets/{shellsession-BpE5o7_3.js → shellsession-n3-5CqcF.js} +1 -1
  120. package/dist/ui/assets/{soy-COesmhTY.js → soy-XYBOf_BA.js} +1 -1
  121. package/dist/ui/assets/sql-ET4u5Cpl.js +1 -0
  122. package/dist/ui/assets/{stata-CGQEuSZq.js → stata-CLHcJHO_.js} +1 -1
  123. package/dist/ui/assets/{surrealql-Bz4JbU-9.js → surrealql-BdjQ60T4.js} +1 -1
  124. package/dist/ui/assets/{svelte-1jM2ejXj.js → svelte-B2RPtKJe.js} +1 -1
  125. package/dist/ui/assets/{templ-C-VwELcp.js → templ-CrByPW_W.js} +1 -1
  126. package/dist/ui/assets/{tex-t1rUiQCm.js → tex-DZBkg363.js} +1 -1
  127. package/dist/ui/assets/{ts-tags-Bewoa6pS.js → ts-tags-DJR3Gphb.js} +1 -1
  128. package/dist/ui/assets/tsx-CajzROyy.js +1 -0
  129. package/dist/ui/assets/{twig-DVg-vELl.js → twig-BZjk5Zda.js} +1 -1
  130. package/dist/ui/assets/typescript-B-EWToLj.js +1 -0
  131. package/dist/ui/assets/{vue-wPQ7UlMy.js → vue-D_rHCCr8.js} +1 -1
  132. package/dist/ui/assets/{vue-html-BHtJ8geX.js → vue-html-CC4WlvVt.js} +1 -1
  133. package/dist/ui/assets/{vue-vine-CNSGAWwc.js → vue-vine-BgNMVgpk.js} +1 -1
  134. package/dist/ui/assets/xml-BI86-WbH.js +1 -0
  135. package/dist/ui/assets/{xsl-3_vJ27aE.js → xsl-DXeLZV4J.js} +1 -1
  136. package/dist/ui/assets/yaml-60_PFlWa.js +1 -0
  137. package/dist/workspaces/bootstrap.js +72 -0
  138. package/dist/workspaces/context.js +302 -0
  139. package/dist/workspaces/conversation-test-support.js +86 -0
  140. package/dist/{git-worktrees.js → workspaces/git/git-worktrees.js} +1 -1
  141. package/dist/workspaces/inventory.js +131 -0
  142. package/dist/workspaces/paths.js +55 -0
  143. package/dist/{remote-auth.js → workspaces/relay/auth/remote-auth.js} +2 -2
  144. package/dist/workspaces/relay/result-support.js +111 -0
  145. package/dist/workspaces/relay/tests/test-support.js +203 -0
  146. package/dist/{remote-mcp-connection-pool.js → workspaces/relay/transport/remote-mcp-connection-pool.js} +1 -1
  147. package/dist/workspaces/relay/types.js +1 -0
  148. package/dist/{remote-workspace-relay.js → workspaces/relay/workspace-relay.js} +23 -172
  149. package/dist/workspaces/resources/skills.js +285 -0
  150. package/dist/{review-checkpoints.js → workspaces/review/review-checkpoints.js} +2 -2
  151. package/dist/workspaces/sessions.js +457 -0
  152. package/dist/{workspace-store.js → workspaces/state/workspace-store.js} +2 -2
  153. package/dist/workspaces.js +69 -991
  154. package/docs/chatgpt-coding-workflow.md +3 -11
  155. package/docs/configuration.md +12 -25
  156. package/docs/gotchas.md +0 -30
  157. package/docs/roadmap.md +6 -10
  158. package/docs/security.md +1 -5
  159. package/docs/setup.md +0 -11
  160. package/package.json +6 -8
  161. package/scripts/ci/architecture.mjs +109 -0
  162. package/scripts/ci/verify.mjs +5 -0
  163. package/scripts/debug/accept/bootstrap.mjs +326 -0
  164. package/scripts/debug/accept/harness.mjs +76 -0
  165. package/scripts/debug/accept/support.mjs +432 -0
  166. package/scripts/debug/accept.mjs +43 -806
  167. package/scripts/debug/relay-accept/support.mjs +135 -0
  168. package/scripts/debug/relay-accept.mjs +26 -115
  169. package/scripts/debug/traffic/support.mjs +167 -0
  170. package/scripts/debug/traffic/traffic-audit.mjs +4 -161
  171. package/scripts/{lsp-interop-support.test.mjs → lsp/interop-support.test.mjs} +1 -1
  172. package/scripts/{lsp-interop.mjs → lsp/interop.mjs} +3 -3
  173. package/scripts/wiki/sync.mjs +21 -10
  174. package/dist/activity/bash-output-store.js +0 -127
  175. package/dist/pi-tools.js +0 -110
  176. package/dist/skills.js +0 -127
  177. package/dist/ui/assets/activity-panel-app-CUAN6zyW.js +0 -2
  178. package/dist/ui/assets/angular-html-BkgDQl-G.js +0 -1
  179. package/dist/ui/assets/c-IBzBpxAO.js +0 -1
  180. package/dist/ui/assets/chunk-EyZ2wyi3.js +0 -1
  181. package/dist/ui/assets/cpp-CVHeO-Jk.js +0 -1
  182. package/dist/ui/assets/css-DEfus9J5.js +0 -1
  183. package/dist/ui/assets/glsl-_ooCjaeB.js +0 -1
  184. package/dist/ui/assets/graphql-B9LqP-cY.js +0 -1
  185. package/dist/ui/assets/haml-BU5EoZBn.js +0 -1
  186. package/dist/ui/assets/heavy-payload-CgzrutLm.js +0 -4
  187. package/dist/ui/assets/html-D-C6YZk4.js +0 -1
  188. package/dist/ui/assets/java-ppHvycj5.js +0 -1
  189. package/dist/ui/assets/javascript-DDTjKLlQ.js +0 -1
  190. package/dist/ui/assets/json-Cw91r8h1.js +0 -1
  191. package/dist/ui/assets/jsx-04X2FT_1.js +0 -1
  192. package/dist/ui/assets/lua-CiyBp52Y.js +0 -1
  193. package/dist/ui/assets/r-DfFga4si.js +0 -1
  194. package/dist/ui/assets/regexp-iC9_nbSI.js +0 -1
  195. package/dist/ui/assets/review-payload-BrLbezbq.js +0 -1
  196. package/dist/ui/assets/scrollbar-CbhpdW05.js +0 -287
  197. package/dist/ui/assets/scss-BfZXsAWQ.js +0 -1
  198. package/dist/ui/assets/shellscript-BC2srNaB.js +0 -1
  199. package/dist/ui/assets/sql-Ccv_yeUm.js +0 -1
  200. package/dist/ui/assets/tsx-CTXzrBkN.js +0 -1
  201. package/dist/ui/assets/typescript--ElHoFkw.js +0 -1
  202. package/dist/ui/assets/workspace-app-Bhj96tsR.js +0 -1
  203. package/dist/ui/assets/workspace-app-CxwJuZyS.js +0 -111
  204. package/dist/ui/assets/workspace-app-D6UR0AFl.js +0 -5
  205. package/dist/ui/assets/workspace-app-ldjBmCJR.css +0 -1
  206. package/dist/ui/assets/workspace-lifecycle-app-Cqfhx9pV.js +0 -1
  207. package/dist/ui/assets/xml-JnmX6vyS.js +0 -1
  208. package/dist/ui/assets/yaml-OAcZkP-w.js +0 -1
  209. package/dist/ui/workspace-app.html +0 -18
  210. package/dist/ui/workspace-lifecycle-app.html +0 -18
  211. package/docs/agent-profile-schema.md +0 -114
  212. package/docs/artifact-exchange.md +0 -65
  213. package/docs/versioning.md +0 -205
  214. /package/capabilities/{managed-worktrees → workspace/managed-worktrees}/GUIDE.md +0 -0
  215. /package/capabilities/{workspace-tasks → workspace/workspace-tasks}/GUIDE.md +0 -0
  216. /package/dist/activity/{process-output-audit.js → runtime/process-output-audit.js} +0 -0
  217. /package/dist/{artifact-error.js → mcp/artifacts/artifact-error.js} +0 -0
  218. /package/dist/{artifact-tools.js → mcp/artifacts/artifact-tools.js} +0 -0
  219. /package/dist/{incoming-artifacts.js → mcp/artifacts/incoming-artifacts.js} +0 -0
  220. /package/dist/{advertised-files.js → mcp/filesystem/advertised-files.js} +0 -0
  221. /package/dist/{apply-patch.js → mcp/filesystem/apply-patch.js} +0 -0
  222. /package/dist/{file-mutations.js → mcp/filesystem/file-mutations.js} +0 -0
  223. /package/dist/{roots.js → mcp/filesystem/roots.js} +0 -0
  224. /package/dist/{oauth → mcp/oauth}/public-url.js +0 -0
  225. /package/dist/{oauth → mcp/oauth}/router.js +0 -0
  226. /package/dist/{operations → mcp/operations}/batch/executor.js +0 -0
  227. /package/dist/{operations → mcp/operations}/batch/scheduler.js +0 -0
  228. /package/dist/{operations → mcp/operations}/batch/types.js +0 -0
  229. /package/dist/{operations → mcp/operations}/bulk-mutation.js +0 -0
  230. /package/dist/{operations → mcp/operations}/bulk-read.js +0 -0
  231. /package/dist/{operations → mcp/operations}/core-operation-executor.js +0 -0
  232. /package/dist/{process-platform.js → mcp/process/process-platform.js} +0 -0
  233. /package/dist/{process-sessions.js → mcp/process/process-sessions.js} +0 -0
  234. /package/dist/{request-meta.js → mcp/request-meta.js} +0 -0
  235. /package/dist/{mcp-sessions.js → mcp/server/transport/mcp-sessions.js} +0 -0
  236. /package/dist/{server-shutdown.js → mcp/server/transport/server-shutdown.js} +0 -0
  237. /package/dist/{logger.js → runtime/logging/logger.js} +0 -0
  238. /package/dist/{db → runtime/state/db}/schema.js +0 -0
  239. /package/dist/{composite-activity.js → workspaces/composite/composite-activity.js} +0 -0
  240. /package/dist/{composite-workspaces.js → workspaces/composite/composite-workspaces.js} +0 -0
  241. /package/dist/{git.js → workspaces/git/git.js} +0 -0
  242. /package/dist/{workspace-presentation.js → workspaces/presentation/workspace-presentation.js} +0 -0
  243. /package/dist/{remote-transport.js → workspaces/relay/transport/remote-transport.js} +0 -0
  244. /package/dist/{workspace-task-reminders.js → workspaces/tasks/workspace-task-reminders.js} +0 -0
  245. /package/dist/{workspace-tasks.js → workspaces/tasks/workspace-tasks.js} +0 -0
  246. /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 "./user-config.js";
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 "./logger.js";
6
- import { resolveShellCommand, terminateProcessTree } from "./process-platform.js";
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("'", "&#39;");
24
24
  }
25
25
  function formHtml(params) {
26
- const scopeText = params.scopes.length > 0 ? params.scopes.join(" ") : "devspace";
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 "./db/client.js";
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
- if (["localhost", "127.0.0.1", "[::1]"].includes(parsed.hostname))
13
- return true;
14
- return allowedHosts.includes(parsed.hostname);
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 "../pi-tools.js";
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
+ }