@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,131 @@
1
+ import { basename, resolve } from "node:path";
2
+ import { assertAllowedPath } from "../mcp/filesystem/roots.js";
3
+ import { canonicalPath } from "./paths.js";
4
+ const WORKSPACE_STALE_REMINDER_MS = 2 * 24 * 60 * 60 * 1_000;
5
+ /** Build bounded read-only projections over persistent Workspace sessions. */
6
+ export class WorkspaceInventoryService {
7
+ config;
8
+ store;
9
+ workspaces;
10
+ sessions;
11
+ constructor(config, store, workspaces, sessions) {
12
+ this.config = config;
13
+ this.store = store;
14
+ this.workspaces = workspaces;
15
+ this.sessions = sessions;
16
+ }
17
+ async inspectWorkspace(workspaceId) {
18
+ let session = this.store?.getSession(workspaceId);
19
+ if (!session) {
20
+ const workspace = this.workspaces.get(workspaceId);
21
+ if (workspace)
22
+ session = workspaceSessionSnapshot(workspace);
23
+ }
24
+ if (!session)
25
+ throw new Error(`Unknown workspaceId: ${workspaceId}.`);
26
+ const entry = await this.inventoryEntryForSession(session, Date.now(), false);
27
+ const { current: _current, ...inspection } = entry;
28
+ return { kind: "workspace", location: "local", ...inspection };
29
+ }
30
+ async listWorkspaces(input = {}, openOptions = {}) {
31
+ this.sessions.pruneIdleWorkspaceSessions(openOptions.protectedWorkspaceIds ?? new Set());
32
+ const now = Date.now();
33
+ const sessions = this.store
34
+ ? this.store.listSessions()
35
+ : [...this.workspaces.values()].map(workspaceSessionSnapshot);
36
+ const currentWorkspaceIds = new Set(openOptions.conversationScopeId && this.store
37
+ ? this.store
38
+ .listConversationBindings()
39
+ .filter((binding) => binding.conversationScopeId === openOptions.conversationScopeId)
40
+ .map((binding) => binding.workspaceSessionId)
41
+ : []);
42
+ const workspaceIdFilter = input.workspaceId
43
+ ? this.store?.getSession(input.workspaceId)?.id ?? input.workspaceId
44
+ : undefined;
45
+ const rootKey = input.root
46
+ ? await canonicalPath(assertAllowedPath(input.root, [...this.config.allowedRoots, this.config.worktreeRoot]))
47
+ : undefined;
48
+ const entries = await Promise.all(sessions.map((session) => this.inventoryEntryForSession(session, now, currentWorkspaceIds.has(session.id))));
49
+ const filtered = [];
50
+ for (let index = 0; index < sessions.length; index += 1) {
51
+ const session = sessions[index];
52
+ const entry = entries[index];
53
+ if (!session || !entry)
54
+ continue;
55
+ if (workspaceIdFilter && entry.workspaceId !== workspaceIdFilter)
56
+ continue;
57
+ if (input.status && entry.status !== input.status)
58
+ continue;
59
+ if (input.state && entry.state !== input.state)
60
+ continue;
61
+ if (input.mode && entry.mode !== input.mode)
62
+ continue;
63
+ if (input.staleOnly && entry.state !== "stale")
64
+ continue;
65
+ if (rootKey) {
66
+ const sessionRootKey = await canonicalPath(session.root);
67
+ const sourceRootKey = session.sourceRoot ? await canonicalPath(session.sourceRoot) : undefined;
68
+ if (sessionRootKey !== rootKey && sourceRootKey !== rootKey)
69
+ continue;
70
+ }
71
+ filtered.push(entry);
72
+ }
73
+ const summary = filtered.reduce((counts, entry) => {
74
+ counts[entry.state] += 1;
75
+ return counts;
76
+ }, { active: 0, stale: 0, invalid: 0, closed: 0 });
77
+ const offset = Math.max(0, input.offset ?? 0);
78
+ const limit = Math.min(100, Math.max(1, input.limit ?? 50));
79
+ return {
80
+ workspaces: filtered.slice(offset, offset + limit),
81
+ summary: { total: entries.length, matching: filtered.length, ...summary },
82
+ page: { offset, limit, hasMore: offset + limit < filtered.length },
83
+ };
84
+ }
85
+ async inventoryEntryForSession(session, now, current) {
86
+ const rootValid = await this.sessions.validSessionRoot(session) !== undefined;
87
+ const lastUsedAt = Date.parse(session.lastUsedAt);
88
+ const idleMs = Number.isFinite(lastUsedAt) ? Math.max(0, now - lastUsedAt) : 0;
89
+ const state = session.status !== "active"
90
+ ? "closed"
91
+ : !rootValid
92
+ ? "invalid"
93
+ : idleMs >= WORKSPACE_STALE_REMINDER_MS
94
+ ? "stale"
95
+ : "active";
96
+ const projectRoot = session.sourceRoot ?? session.root;
97
+ return {
98
+ label: `${basename(resolve(projectRoot)) || "workspace"}/${session.id}`,
99
+ workspaceId: session.id,
100
+ root: session.root,
101
+ status: session.status,
102
+ state,
103
+ mode: session.mode,
104
+ sourceRoot: session.sourceRoot,
105
+ branch: session.branch,
106
+ targetBranch: session.targetBranch,
107
+ managed: session.managed,
108
+ createdAt: session.createdAt,
109
+ lastUsedAt: session.lastUsedAt,
110
+ idleMs,
111
+ rootValid,
112
+ current,
113
+ };
114
+ }
115
+ }
116
+ function workspaceSessionSnapshot(workspace) {
117
+ return {
118
+ id: workspace.id,
119
+ root: workspace.root,
120
+ status: "active",
121
+ mode: workspace.mode,
122
+ sourceRoot: workspace.sourceRoot,
123
+ baseRef: workspace.worktree?.baseRef,
124
+ baseSha: workspace.worktree?.baseSha,
125
+ branch: workspace.worktree?.branch,
126
+ targetBranch: workspace.worktree?.targetBranch,
127
+ managed: workspace.worktree?.managed ?? false,
128
+ createdAt: "",
129
+ lastUsedAt: "",
130
+ };
131
+ }
@@ -0,0 +1,55 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { mkdir, realpath, stat } from "node:fs/promises";
3
+ import { basename, dirname, resolve } from "node:path";
4
+ export function canonicalPersistedWorkspacePath(path) {
5
+ const missingSegments = [];
6
+ let candidate = path;
7
+ while (true) {
8
+ try {
9
+ return resolve(realpathSync(candidate), ...missingSegments.slice().reverse());
10
+ }
11
+ catch (error) {
12
+ if (!isErrnoException(error) || (error.code !== "ENOENT" && error.code !== "ENOTDIR")) {
13
+ return resolve(path);
14
+ }
15
+ const parent = dirname(candidate);
16
+ if (parent === candidate)
17
+ return resolve(path);
18
+ missingSegments.push(basename(candidate));
19
+ candidate = parent;
20
+ }
21
+ }
22
+ }
23
+ export async function canonicalPath(path) {
24
+ const missingSegments = [];
25
+ let candidate = path;
26
+ while (true) {
27
+ try {
28
+ return resolve(await realpath(candidate), ...missingSegments.slice().reverse());
29
+ }
30
+ catch (error) {
31
+ if (!isErrnoException(error) || (error.code !== "ENOENT" && error.code !== "ENOTDIR")) {
32
+ throw error;
33
+ }
34
+ const parent = dirname(candidate);
35
+ if (parent === candidate)
36
+ return path;
37
+ missingSegments.push(basename(candidate));
38
+ candidate = parent;
39
+ }
40
+ }
41
+ }
42
+ export async function ensureCheckoutWorkspaceRoot(path, ops = { stat, mkdir }) {
43
+ try {
44
+ return await ops.stat(path);
45
+ }
46
+ catch (error) {
47
+ if (!isErrnoException(error) || error.code !== "ENOENT")
48
+ throw error;
49
+ }
50
+ await ops.mkdir(path, { recursive: true });
51
+ return await ops.stat(path);
52
+ }
53
+ function isErrnoException(error) {
54
+ return error instanceof Error && "code" in error;
55
+ }
@@ -2,7 +2,7 @@ import { readFileSync } from "node:fs";
2
2
  import { UnauthorizedError } from "@modelcontextprotocol/sdk/client/auth.js";
3
3
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
4
4
  import { StreamableHTTPClientTransport, StreamableHTTPError, } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
5
- import { publicEndpointUrl } from "./oauth/public-url.js";
5
+ import { publicEndpointUrl } from "../../../mcp/oauth/public-url.js";
6
6
  const REMOTE_AUTH_TIMEOUT_MS = 15_000;
7
7
  const packageVersion = readForgeRelayVersion();
8
8
  export function normalizeRemoteServiceTarget(value) {
@@ -112,7 +112,7 @@ async function exchangeCliCredential(target, credential) {
112
112
  };
113
113
  }
114
114
  function readForgeRelayVersion() {
115
- const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
115
+ const packageJson = JSON.parse(readFileSync(new URL("../../../../package.json", import.meta.url), "utf8"));
116
116
  if (typeof packageJson.version !== "string" || packageJson.version.length === 0) {
117
117
  throw new Error("Unable to read ForgeRelay package version.");
118
118
  }
@@ -0,0 +1,111 @@
1
+ export function assertRemoteToolSucceeded(alias, tool, result) {
2
+ if (result.isError !== true)
3
+ return;
4
+ throw new Error(`Remote ForgeRelay ${alias} ${tool} failed: ${toolResultText(result)}`);
5
+ }
6
+ export function stringField(structured, field, label) {
7
+ const value = structured?.[field];
8
+ if (typeof value !== "string" || !value) {
9
+ throw new Error(`${label} did not include ${field}.`);
10
+ }
11
+ return value;
12
+ }
13
+ export function copyStringField(source, target, field) {
14
+ const value = source[field];
15
+ if (typeof value === "string")
16
+ target[field] = value;
17
+ }
18
+ export function copyBooleanField(source, target, field) {
19
+ const value = source[field];
20
+ if (typeof value === "boolean")
21
+ target[field] = value;
22
+ }
23
+ export function copyNumberField(source, target, field) {
24
+ const value = source[field];
25
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0)
26
+ target[field] = value;
27
+ }
28
+ export function safeTaskSummary(value) {
29
+ if (!value || typeof value !== "object")
30
+ return undefined;
31
+ const summary = value;
32
+ if (summary.level !== "summary" ||
33
+ summary.version !== 1 ||
34
+ typeof summary.revision !== "number" ||
35
+ !Number.isInteger(summary.revision) ||
36
+ summary.revision < 0 ||
37
+ !Array.isArray(summary.lists)) {
38
+ return undefined;
39
+ }
40
+ const lists = [];
41
+ for (const value of summary.lists) {
42
+ if (!value || typeof value !== "object")
43
+ return undefined;
44
+ const list = value;
45
+ if (typeof list.id !== "string" ||
46
+ typeof list.name !== "string" ||
47
+ (list.state !== "active" && list.state !== "archived") ||
48
+ typeof list.revision !== "number" || !Number.isInteger(list.revision) || list.revision <= 0 ||
49
+ typeof list.taskCount !== "number" || !Number.isInteger(list.taskCount) || list.taskCount < 0 ||
50
+ typeof list.unfinishedTaskCount !== "number" || !Number.isInteger(list.unfinishedTaskCount) || list.unfinishedTaskCount < 0) {
51
+ return undefined;
52
+ }
53
+ lists.push({
54
+ id: list.id,
55
+ name: list.name,
56
+ state: list.state,
57
+ revision: list.revision,
58
+ taskCount: list.taskCount,
59
+ unfinishedTaskCount: list.unfinishedTaskCount,
60
+ });
61
+ }
62
+ return {
63
+ level: "summary",
64
+ version: 1,
65
+ revision: summary.revision,
66
+ lists,
67
+ };
68
+ }
69
+ export function toolResultText(result) {
70
+ return (result.content ?? [])
71
+ .filter((entry) => entry.type === "text")
72
+ .map((entry) => entry.text)
73
+ .join("\n") || "remote tool returned an error";
74
+ }
75
+ export function remapToolResultWorkspaceId(result, remoteWorkspaceId, gatewayWorkspaceId) {
76
+ return {
77
+ ...result,
78
+ content: (result.content ?? []).map((entry) => entry.type === "text"
79
+ ? { ...entry, text: entry.text.split(remoteWorkspaceId).join(gatewayWorkspaceId) }
80
+ : entry),
81
+ ...(result._meta
82
+ ? { _meta: replaceExactWorkspaceId(result._meta, remoteWorkspaceId, gatewayWorkspaceId) }
83
+ : {}),
84
+ ...(result.structuredContent
85
+ ? {
86
+ structuredContent: replaceExactWorkspaceId(result.structuredContent, remoteWorkspaceId, gatewayWorkspaceId),
87
+ }
88
+ : {}),
89
+ };
90
+ }
91
+ export function replaceExactWorkspaceId(value, from, to) {
92
+ if (typeof value === "string")
93
+ return value.split(from).join(to);
94
+ if (Array.isArray(value))
95
+ return value.map((entry) => replaceExactWorkspaceId(entry, from, to));
96
+ if (!value || typeof value !== "object")
97
+ return value;
98
+ return Object.fromEntries(Object.entries(value)
99
+ .map(([key, entry]) => [key, replaceExactWorkspaceId(entry, from, to)]));
100
+ }
101
+ export function sanitizedRemoteError(error, remoteWorkspaceId, gatewayWorkspaceId) {
102
+ let message = errorMessage(error);
103
+ if (remoteWorkspaceId && gatewayWorkspaceId) {
104
+ message = message.split(remoteWorkspaceId).join(gatewayWorkspaceId);
105
+ }
106
+ message = message.replace(/(^|[^A-Za-z0-9_])ws_[0-9a-f]{10}(?=$|[^A-Za-z0-9_])/g, "$1[remote-workspace]");
107
+ return new Error(message, { cause: error });
108
+ }
109
+ export function errorMessage(error) {
110
+ return error instanceof Error ? error.message : String(error);
111
+ }
@@ -0,0 +1,203 @@
1
+ import assert from "node:assert/strict";
2
+ import { execFileSync } from "node:child_process";
3
+ import { mkdir, writeFile } from "node:fs/promises";
4
+ import { createRequire, syncBuiltinESMExports } from "node:module";
5
+ import { join } from "node:path";
6
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
7
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
8
+ import { CallToolResultSchema } from "@modelcontextprotocol/sdk/types.js";
9
+ import { ActivityAuditStore } from "../../../activity/history/audit-store.js";
10
+ import { BashOutputStore } from "../../../activity/history/bash-output-store.js";
11
+ import { HostTurnStore } from "../../../activity/history/host-turn-store.js";
12
+ import { ActivityLifecycle } from "../../../activity/runtime/lifecycle.js";
13
+ import { ActivityQueryService } from "../../../activity/history/query-service.js";
14
+ import { loadConfig } from "../../../runtime/config/config.js";
15
+ import { CodeIntelligenceManager } from "../../../lsp/runtime/manager.js";
16
+ import { createReviewCheckpointManager } from "../../review/review-checkpoints.js";
17
+ import { ProcessManager } from "../../../mcp/process/process-sessions.js";
18
+ import { createMcpServer, createServer } from "../../../server.js";
19
+ import { SqliteWorkspaceStore } from "../../state/workspace-store.js";
20
+ import { WorkspaceRegistry } from "../../../workspaces.js";
21
+ const childProcessModule = createRequire(import.meta.url)("node:child_process");
22
+ const cleanProductEnv = Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.startsWith("FORGERELAY_")));
23
+ export async function setupGitRepository(root) {
24
+ await mkdir(root, { recursive: true });
25
+ execFileSync("git", ["init", "-b", "main"], { cwd: root, stdio: "ignore" });
26
+ execFileSync("git", ["config", "user.name", "ForgeRelay Test"], { cwd: root });
27
+ execFileSync("git", ["config", "user.email", "forgerelay-test@example.invalid"], { cwd: root });
28
+ await writeFile(join(root, "README.md"), "relay worktree fixture\n");
29
+ execFileSync("git", ["add", "README.md"], { cwd: root });
30
+ execFileSync("git", ["commit", "-m", "TEST: (relay) initialize worktree fixture"], { cwd: root, stdio: "ignore" });
31
+ }
32
+ export async function startForge(t, options) {
33
+ const configDir = options.existingConfigDir ?? join(options.root, "config");
34
+ const stateDir = join(options.root, "state");
35
+ await mkdir(configDir, { recursive: true });
36
+ await mkdir(stateDir, { recursive: true });
37
+ if (!options.existingConfigDir) {
38
+ await writeFile(join(configDir, "auth.json"), JSON.stringify({
39
+ ownerToken: options.ownerToken,
40
+ instanceId: options.instanceId,
41
+ }, null, 2), { mode: 0o600 });
42
+ }
43
+ await writeFile(join(configDir, "config.json"), JSON.stringify({
44
+ host: "127.0.0.1",
45
+ port: 7676,
46
+ allowedRoots: [options.allowedRoot],
47
+ publicBaseUrl: "http://127.0.0.1:7676",
48
+ stateDir,
49
+ worktreeRoot: join(options.root, "worktrees"),
50
+ ...(options.taskReminderInterval !== undefined ? { taskReminderInterval: options.taskReminderInterval } : {}),
51
+ ...(options.hooks ? { hooks: options.hooks } : {}),
52
+ }, null, 2));
53
+ const env = {
54
+ ...cleanProductEnv,
55
+ FORGERELAY_CONFIG_DIR: configDir,
56
+ FORGERELAY_TOOL_MODE: options.toolMode ?? "minimal",
57
+ FORGERELAY_WIDGETS: "off",
58
+ FORGERELAY_SKILLS: "0",
59
+ };
60
+ const running = createServer(loadConfig(env));
61
+ const httpServer = running.app.listen(0, "127.0.0.1");
62
+ await new Promise((resolve) => httpServer.once("listening", resolve));
63
+ const port = httpServer.address().port;
64
+ t.after(async () => {
65
+ await running.close();
66
+ await new Promise((resolve) => httpServer.close(() => resolve()));
67
+ });
68
+ return {
69
+ endpoint: `http://127.0.0.1:${port}`,
70
+ ownerToken: options.ownerToken,
71
+ openAdditionalEndpoint: async () => {
72
+ const additionalServer = running.app.listen(0, "127.0.0.1");
73
+ await new Promise((resolve) => additionalServer.once("listening", resolve));
74
+ const additionalPort = additionalServer.address().port;
75
+ t.after(() => new Promise((resolve) => additionalServer.close(() => resolve())));
76
+ return `http://127.0.0.1:${additionalPort}`;
77
+ },
78
+ };
79
+ }
80
+ export async function startGatewayClient(t, options) {
81
+ const stateDir = options.stateDir ?? join(options.root, "state");
82
+ await mkdir(stateDir, { recursive: true });
83
+ await writeFile(join(options.configDir, "config.json"), JSON.stringify({
84
+ allowedRoots: [options.allowedRoot],
85
+ stateDir,
86
+ ...(options.hooks ? { hooks: options.hooks } : {}),
87
+ }, null, 2));
88
+ const config = loadConfig({
89
+ ...cleanProductEnv,
90
+ FORGERELAY_CONFIG_DIR: options.configDir,
91
+ FORGERELAY_TOOL_MODE: options.toolMode ?? "minimal",
92
+ FORGERELAY_WIDGETS: "off",
93
+ FORGERELAY_SKILLS: "0",
94
+ });
95
+ const workspaceStore = new SqliteWorkspaceStore(stateDir);
96
+ const workspaces = new WorkspaceRegistry(config, workspaceStore);
97
+ const auditStore = new ActivityAuditStore(stateDir);
98
+ const bashOutputStore = new BashOutputStore(stateDir);
99
+ const hostTurnStore = new HostTurnStore(stateDir);
100
+ const activityQueries = new ActivityQueryService(hostTurnStore, auditStore, bashOutputStore);
101
+ const processSessions = new ProcessManager({ outputAudit: bashOutputStore });
102
+ const activityLifecycle = new ActivityLifecycle(auditStore, {
103
+ turnIdForConversation: (conversationScopeId, workspaceId) => activityQueries.currentTurnId(conversationScopeId, workspaceId),
104
+ });
105
+ const codeIntelligence = new CodeIntelligenceManager(config);
106
+ const server = createMcpServer(config, workspaces, createReviewCheckpointManager(), processSessions, [], [], codeIntelligence, activityLifecycle, bashOutputStore, activityQueries);
107
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
108
+ const client = new Client({ name: "forgerelay-workspace-relay-test", version: "1.0.0" });
109
+ await Promise.all([client.connect(clientTransport), server.connect(serverTransport)]);
110
+ t.after(async () => {
111
+ await client.close();
112
+ await server.close();
113
+ await codeIntelligence.shutdown();
114
+ processSessions.shutdown();
115
+ hostTurnStore.close();
116
+ bashOutputStore.close();
117
+ auditStore.close();
118
+ workspaceStore.close();
119
+ });
120
+ return client;
121
+ }
122
+ export async function installFakeSsh(t, root, sshLog) {
123
+ const fakeSshPath = join(root, "fake-ssh.cjs");
124
+ await writeFile(fakeSshPath, fakeSshRelaySource());
125
+ const originalSpawn = childProcessModule.spawn;
126
+ childProcessModule.spawn = ((command, args = [], options = {}) => {
127
+ if (command !== "ssh")
128
+ return originalSpawn(command, args, options);
129
+ return originalSpawn(process.execPath, [fakeSshPath, ...args], {
130
+ ...options,
131
+ env: {
132
+ ...process.env,
133
+ ...(options.env ?? {}),
134
+ TEST_SSH_LOG: sshLog,
135
+ },
136
+ });
137
+ });
138
+ syncBuiltinESMExports();
139
+ t.after(() => {
140
+ childProcessModule.spawn = originalSpawn;
141
+ syncBuiltinESMExports();
142
+ });
143
+ }
144
+ function fakeSshRelaySource() {
145
+ return `#!/usr/bin/env node
146
+ const fs = require("node:fs");
147
+ const net = require("node:net");
148
+ const args = process.argv.slice(2);
149
+ fs.appendFileSync(process.env.TEST_SSH_LOG, JSON.stringify(args) + "\\n");
150
+ const forwardIndex = args.indexOf("-L");
151
+ const forward = forwardIndex >= 0 ? args[forwardIndex + 1] : undefined;
152
+ if (!forward) {
153
+ process.stderr.write("fake ssh: missing -L\\n");
154
+ process.exit(40);
155
+ }
156
+ const match = /^127\\.0\\.0\\.1:(\\d+):([^:]+):(\\d+)$/.exec(forward);
157
+ if (!match) {
158
+ process.stderr.write("fake ssh: invalid forward " + forward + "\\n");
159
+ process.exit(41);
160
+ }
161
+ const localPort = Number(match[1]);
162
+ const remoteHost = match[2];
163
+ const remotePort = Number(match[3]);
164
+ const sockets = new Set();
165
+ const server = net.createServer((client) => {
166
+ const upstream = net.connect(remotePort, remoteHost);
167
+ sockets.add(client);
168
+ sockets.add(upstream);
169
+ client.on("close", () => sockets.delete(client));
170
+ upstream.on("close", () => sockets.delete(upstream));
171
+ client.pipe(upstream);
172
+ upstream.pipe(client);
173
+ });
174
+ server.on("error", (error) => {
175
+ process.stderr.write(String(error) + "\\n");
176
+ process.exit(42);
177
+ });
178
+ server.listen(localPort, "127.0.0.1", () => {
179
+ process.stderr.write("debug1: Local forwarding listening on 127.0.0.1 port " + localPort + ".\\n");
180
+ });
181
+ const stop = () => {
182
+ for (const socket of sockets) socket.destroy();
183
+ server.close(() => process.exit(0));
184
+ };
185
+ process.on("SIGTERM", stop);
186
+ process.on("SIGINT", stop);
187
+ `;
188
+ }
189
+ export function structuredContent(result) {
190
+ const parsed = parseToolResult(result);
191
+ assert.ok(parsed.structuredContent);
192
+ return parsed.structuredContent;
193
+ }
194
+ export function resultText(result) {
195
+ const parsed = parseToolResult(result);
196
+ return (parsed.content ?? [])
197
+ .filter((entry) => entry.type === "text")
198
+ .map((entry) => entry.text)
199
+ .join("\n");
200
+ }
201
+ function parseToolResult(result) {
202
+ return CallToolResultSchema.parse(result);
203
+ }
@@ -1,4 +1,4 @@
1
- import { connectRemoteMcpClient } from "./remote-auth.js";
1
+ import { connectRemoteMcpClient } from "../auth/remote-auth.js";
2
2
  import { openRemoteServiceEndpoint, } from "./remote-transport.js";
3
3
  export class RemoteMcpConnectionPool {
4
4
  entries = new Map();
@@ -0,0 +1 @@
1
+ export {};