@akira-tl/forgerelay 0.8.8 → 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 +12 -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 +116 -4202
  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 -410
  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 +68 -995
  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-7AckaxIR.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-Bol1_mcs.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-BqJp0c5e.js +0 -1
  196. package/dist/ui/assets/scrollbar-CvE-I-jG.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-BLW7p6IZ.js +0 -114
  203. package/dist/ui/assets/workspace-app-BjNqR0en.js +0 -1
  204. package/dist/ui/assets/workspace-app-CmaYU4DW.js +0 -3
  205. package/dist/ui/assets/workspace-app-D2bJ5fjt.css +0 -1
  206. package/dist/ui/assets/workspace-lifecycle-app-BVbI2Ilf.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
@@ -1,13 +1,10 @@
1
1
  import { randomBytes, randomUUID } from "node:crypto";
2
- import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
2
+ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync, } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, join, resolve } from "node:path";
5
- import { expandHomePath } from "./roots.js";
6
- import { mergeHookConfigs, parseHookFile, } from "./hooks.js";
7
- const AUTH_LOCK_RETRY_MS = 10;
8
- const AUTH_LOCK_TIMEOUT_MS = 5_000;
9
- const AUTH_LOCK_STALE_MS = 30_000;
10
- const AUTH_LOCK_SLEEP = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
5
+ import { withFileLock } from "../state/lock/file-lock.js";
6
+ import { expandHomePath } from "../../mcp/filesystem/roots.js";
7
+ import { mergeHookConfigs, parseHookFile, } from "../../mcp/hooks/hooks.js";
11
8
  export function forgerelayConfigDir(env = process.env) {
12
9
  const explicit = env.FORGERELAY_CONFIG_DIR;
13
10
  if (explicit)
@@ -60,10 +57,10 @@ export function writeForgeRelayConfig(config, env = process.env) {
60
57
  writeJsonFile(filePath, config, 0o600);
61
58
  return filePath;
62
59
  }
63
- export function writeForgeRelayAuth(auth, env = process.env) {
60
+ export async function writeForgeRelayAuth(auth, env = process.env) {
64
61
  const filePath = forgerelayAuthPath(env);
65
62
  mkdirSync(forgerelayConfigDir(env), { recursive: true });
66
- return withAuthFileLock(filePath, () => {
63
+ return withFileLock(`${filePath}.lock`, () => {
67
64
  writeJsonFile(filePath, auth, 0o600);
68
65
  return filePath;
69
66
  });
@@ -74,12 +71,12 @@ export function generateOwnerToken() {
74
71
  export function generateInstanceId() {
75
72
  return `forge-${randomUUID()}`;
76
73
  }
77
- export function ensureForgeRelayInstanceId(env = process.env) {
74
+ export async function ensureForgeRelayInstanceId(env = process.env) {
78
75
  const existing = loadForgeRelayFiles(env).auth.instanceId?.trim();
79
76
  if (existing)
80
77
  return existing;
81
78
  let resolved = "";
82
- updateForgeRelayAuth((auth) => {
79
+ await updateForgeRelayAuth((auth) => {
83
80
  const current = auth.instanceId?.trim();
84
81
  if (current) {
85
82
  resolved = current;
@@ -97,7 +94,7 @@ function normalizeRemoteAlias(alias) {
97
94
  }
98
95
  return normalized;
99
96
  }
100
- export function writeForgeRelayRemote(alias, remote, env = process.env) {
97
+ export async function writeForgeRelayRemote(alias, remote, env = process.env) {
101
98
  const normalizedAlias = normalizeRemoteAlias(alias);
102
99
  return updateForgeRelayAuth((auth) => {
103
100
  const remotes = { ...(auth.remotes ?? {}) };
@@ -113,7 +110,7 @@ export function writeForgeRelayRemote(alias, remote, env = process.env) {
113
110
  return { ...auth, remotes };
114
111
  }, env);
115
112
  }
116
- export function renameForgeRelayRemote(fromAlias, toAlias, env = process.env) {
113
+ export async function renameForgeRelayRemote(fromAlias, toAlias, env = process.env) {
117
114
  const from = normalizeRemoteAlias(fromAlias);
118
115
  const to = normalizeRemoteAlias(toAlias);
119
116
  return updateForgeRelayAuth((auth) => {
@@ -128,7 +125,7 @@ export function renameForgeRelayRemote(fromAlias, toAlias, env = process.env) {
128
125
  return { ...auth, remotes };
129
126
  }, env);
130
127
  }
131
- export function removeForgeRelayRemote(alias, env = process.env) {
128
+ export async function removeForgeRelayRemote(alias, env = process.env) {
132
129
  const normalizedAlias = normalizeRemoteAlias(alias);
133
130
  return updateForgeRelayAuth((auth) => {
134
131
  const remotes = { ...(auth.remotes ?? {}) };
@@ -142,7 +139,7 @@ export function ensureForgeRelayDefaultSkills(env = process.env) {
142
139
  const targetPath = join(forgerelaySkillsDir(env), "subagent-delegation", "SKILL.md");
143
140
  if (existsSync(targetPath))
144
141
  return [];
145
- const sourcePath = new URL("../skills/subagent-delegation/SKILL.md", import.meta.url);
142
+ const sourcePath = new URL("../../../skills/subagent-delegation/SKILL.md", import.meta.url);
146
143
  mkdirSync(dirname(targetPath), { recursive: true });
147
144
  writeFileSync(targetPath, readFileSync(sourcePath, "utf8"), { mode: 0o644 });
148
145
  return [targetPath];
@@ -181,52 +178,15 @@ function readJsonFile(filePath) {
181
178
  throw new Error(`Unable to read ${filePath}: ${reason}`);
182
179
  }
183
180
  }
184
- function updateForgeRelayAuth(update, env) {
181
+ async function updateForgeRelayAuth(update, env) {
185
182
  const filePath = forgerelayAuthPath(env);
186
183
  mkdirSync(forgerelayConfigDir(env), { recursive: true });
187
- return withAuthFileLock(filePath, () => {
184
+ return withFileLock(`${filePath}.lock`, () => {
188
185
  const auth = existsSync(filePath) ? readJsonFile(filePath) : {};
189
186
  writeJsonFile(filePath, update(auth), 0o600);
190
187
  return filePath;
191
188
  });
192
189
  }
193
- function withAuthFileLock(filePath, operation) {
194
- const lockPath = `${filePath}.lock`;
195
- const deadline = Date.now() + AUTH_LOCK_TIMEOUT_MS;
196
- for (;;) {
197
- try {
198
- const fd = openSync(lockPath, "wx", 0o600);
199
- closeSync(fd);
200
- break;
201
- }
202
- catch (error) {
203
- const code = error.code;
204
- if (code !== "EEXIST")
205
- throw error;
206
- try {
207
- if (Date.now() - statSync(lockPath).mtimeMs > AUTH_LOCK_STALE_MS) {
208
- rmSync(lockPath, { force: true });
209
- continue;
210
- }
211
- }
212
- catch (statError) {
213
- if (statError.code === "ENOENT")
214
- continue;
215
- throw statError;
216
- }
217
- if (Date.now() >= deadline) {
218
- throw new Error(`Timed out waiting for ForgeRelay auth lock: ${lockPath}`);
219
- }
220
- Atomics.wait(AUTH_LOCK_SLEEP, 0, 0, AUTH_LOCK_RETRY_MS);
221
- }
222
- }
223
- try {
224
- return operation();
225
- }
226
- finally {
227
- rmSync(lockPath, { force: true });
228
- }
229
- }
230
190
  function writeJsonFile(filePath, value, mode) {
231
191
  const tempPath = `${filePath}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
232
192
  try {
@@ -5,7 +5,7 @@ import { drizzle } from "drizzle-orm/better-sqlite3";
5
5
  import * as schema from "./schema.js";
6
6
  import { migrateDatabase } from "./migrations.js";
7
7
  export function databasePath(stateDir) {
8
- return join(stateDir, "devspace.sqlite");
8
+ return join(stateDir, "forgerelay.sqlite");
9
9
  }
10
10
  export function openDatabase(stateDir) {
11
11
  mkdirSync(stateDir, { recursive: true, mode: 0o700 });
@@ -84,18 +84,28 @@ const migrations = [
84
84
  name: "workspace-context-components",
85
85
  up: migrateWorkspaceContextComponents,
86
86
  },
87
+ {
88
+ version: 18,
89
+ name: "file-backed-activity-audit",
90
+ up: migrateFileBackedActivityAudit,
91
+ },
92
+ {
93
+ version: 19,
94
+ name: "file-backed-bash-metadata",
95
+ up: migrateFileBackedBashMetadata,
96
+ },
87
97
  ];
88
98
  export function migrateDatabase(sqlite) {
89
99
  const migrate = sqlite.transaction(() => {
90
100
  sqlite.exec(`
91
- create table if not exists devspace_schema_migrations (
101
+ create table if not exists forgerelay_schema_migrations (
92
102
  version integer primary key,
93
103
  name text not null,
94
104
  applied_at text not null
95
105
  );
96
106
  `);
97
- const applied = new Set(sqlite.prepare("select version from devspace_schema_migrations").all().map((row) => row.version));
98
- const recordMigration = sqlite.prepare("insert into devspace_schema_migrations (version, name, applied_at) values (?, ?, ?)");
107
+ const applied = new Set(sqlite.prepare("select version from forgerelay_schema_migrations").all().map((row) => row.version));
108
+ const recordMigration = sqlite.prepare("insert into forgerelay_schema_migrations (version, name, applied_at) values (?, ?, ?)");
99
109
  for (const migration of migrations) {
100
110
  if (applied.has(migration.version))
101
111
  continue;
@@ -418,6 +428,27 @@ function migrateActivityHostTurnWorkspace(sqlite) {
418
428
  on activity_host_turns(conversation_scope_id, workspace_id, created_at desc);
419
429
  `);
420
430
  }
431
+ function migrateFileBackedActivityAudit(sqlite) {
432
+ if (tableExists(sqlite, "activity_audit_events")) {
433
+ addColumnIfMissing(sqlite, "activity_audit_events", "payload_file", "text");
434
+ addColumnIfMissing(sqlite, "activity_audit_events", "payload_offset", "integer");
435
+ addColumnIfMissing(sqlite, "activity_audit_events", "payload_length", "integer");
436
+ }
437
+ if (tableExists(sqlite, "bash_output_streams")) {
438
+ addColumnIfMissing(sqlite, "bash_output_streams", "log_file", "text");
439
+ addColumnIfMissing(sqlite, "bash_output_streams", "output_bytes", "integer not null default 0");
440
+ }
441
+ }
442
+ function migrateFileBackedBashMetadata(sqlite) {
443
+ if (!tableExists(sqlite, "bash_output_streams"))
444
+ return;
445
+ addColumnIfMissing(sqlite, "bash_output_streams", "command_file", "text");
446
+ addColumnIfMissing(sqlite, "bash_output_streams", "command_offset", "integer");
447
+ addColumnIfMissing(sqlite, "bash_output_streams", "command_length", "integer");
448
+ addColumnIfMissing(sqlite, "bash_output_streams", "error_file", "text");
449
+ addColumnIfMissing(sqlite, "bash_output_streams", "error_offset", "integer");
450
+ addColumnIfMissing(sqlite, "bash_output_streams", "error_length", "integer");
451
+ }
421
452
  function tableExists(sqlite, table) {
422
453
  return Boolean(sqlite.prepare("select 1 from sqlite_master where type = 'table' and name = ?").get(table));
423
454
  }
@@ -0,0 +1,130 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { open, readFile, rename, rm, stat } from "node:fs/promises";
3
+ const DEFAULT_RETRY_MS = 10;
4
+ const DEFAULT_TIMEOUT_MS = 5_000;
5
+ const DEFAULT_STALE_MS = 30_000;
6
+ /**
7
+ * Cross-process lock acquisition must yield the event loop: these locks guard
8
+ * tiny JSON state updates, but a contended lock can otherwise freeze every MCP
9
+ * request handled by the same Node process. The owner token also prevents a
10
+ * process that lost an abandoned lock from deleting a later owner's lock when
11
+ * it eventually reaches cleanup.
12
+ */
13
+ export async function withFileLock(lockPath, operation, options = {}) {
14
+ const retryMs = positiveInteger(options.retryMs, DEFAULT_RETRY_MS, "retryMs");
15
+ const timeoutMs = positiveInteger(options.timeoutMs, DEFAULT_TIMEOUT_MS, "timeoutMs");
16
+ const staleMs = positiveInteger(options.staleMs, DEFAULT_STALE_MS, "staleMs");
17
+ const mode = options.mode ?? 0o600;
18
+ const owner = {
19
+ pid: process.pid,
20
+ token: randomBytes(16).toString("hex"),
21
+ };
22
+ const serializedOwner = `${JSON.stringify(owner)}\n`;
23
+ const deadline = Date.now() + timeoutMs;
24
+ for (;;) {
25
+ try {
26
+ const handle = await open(lockPath, "wx", mode);
27
+ try {
28
+ await handle.writeFile(serializedOwner, "utf8");
29
+ }
30
+ finally {
31
+ await handle.close();
32
+ }
33
+ break;
34
+ }
35
+ catch (error) {
36
+ if (error.code !== "EEXIST")
37
+ throw error;
38
+ }
39
+ if (await reclaimAbandonedLock(lockPath, staleMs))
40
+ continue;
41
+ if (Date.now() >= deadline) {
42
+ throw new Error(`Timed out waiting for file lock: ${lockPath}`);
43
+ }
44
+ await delay(Math.min(retryMs, Math.max(1, deadline - Date.now())));
45
+ }
46
+ try {
47
+ return await operation();
48
+ }
49
+ finally {
50
+ await releaseOwnedLock(lockPath, owner);
51
+ }
52
+ }
53
+ async function reclaimAbandonedLock(lockPath, staleMs) {
54
+ let owner;
55
+ let ageMs;
56
+ try {
57
+ const [contents, info] = await Promise.all([
58
+ readFile(lockPath, "utf8"),
59
+ stat(lockPath),
60
+ ]);
61
+ owner = parseOwner(contents);
62
+ ageMs = Date.now() - info.mtimeMs;
63
+ }
64
+ catch (error) {
65
+ if (error.code === "ENOENT")
66
+ return true;
67
+ throw error;
68
+ }
69
+ // A valid live PID is stronger evidence than mtime. This avoids declaring a
70
+ // long but healthy operation stale merely because the event loop was paused.
71
+ if (owner && processIsAlive(owner.pid))
72
+ return false;
73
+ if (!owner && ageMs <= staleMs)
74
+ return false;
75
+ const stalePath = `${lockPath}.${process.pid}.${randomBytes(8).toString("hex")}.stale`;
76
+ try {
77
+ await rename(lockPath, stalePath);
78
+ }
79
+ catch (error) {
80
+ if (error.code === "ENOENT")
81
+ return true;
82
+ throw error;
83
+ }
84
+ await rm(stalePath, { force: true });
85
+ return true;
86
+ }
87
+ async function releaseOwnedLock(lockPath, owner) {
88
+ let current;
89
+ try {
90
+ current = parseOwner(await readFile(lockPath, "utf8"));
91
+ }
92
+ catch (error) {
93
+ if (error.code === "ENOENT")
94
+ return;
95
+ throw error;
96
+ }
97
+ if (!current || current.pid !== owner.pid || current.token !== owner.token)
98
+ return;
99
+ await rm(lockPath, { force: true });
100
+ }
101
+ function parseOwner(contents) {
102
+ try {
103
+ const value = JSON.parse(contents);
104
+ return Number.isSafeInteger(value.pid) && (value.pid ?? 0) > 0 && typeof value.token === "string" && value.token
105
+ ? { pid: value.pid, token: value.token }
106
+ : undefined;
107
+ }
108
+ catch {
109
+ return undefined;
110
+ }
111
+ }
112
+ function processIsAlive(pid) {
113
+ try {
114
+ process.kill(pid, 0);
115
+ return true;
116
+ }
117
+ catch (error) {
118
+ return error.code !== "ESRCH";
119
+ }
120
+ }
121
+ function positiveInteger(value, fallback, name) {
122
+ const resolved = value ?? fallback;
123
+ if (!Number.isSafeInteger(resolved) || resolved <= 0) {
124
+ throw new Error(`File lock ${name} must be a positive integer.`);
125
+ }
126
+ return resolved;
127
+ }
128
+ function delay(ms) {
129
+ return new Promise((resolve) => setTimeout(resolve, ms));
130
+ }
@@ -0,0 +1,190 @@
1
+ import assert from "node:assert/strict";
2
+ import { execFile } from "node:child_process";
3
+ import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { promisify } from "node:util";
7
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
8
+ import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.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 "../config/config.js";
15
+ import { parseHookConfig } from "../../mcp/hooks/hooks.js";
16
+ import { CodeIntelligenceManager } from "../../lsp/runtime/manager.js";
17
+ import { ProcessManager } from "../../mcp/process/process-sessions.js";
18
+ import { createReviewCheckpointManager } from "../../workspaces/review/review-checkpoints.js";
19
+ import { createMcpServer } from "../../server.js";
20
+ import { SqliteWorkspaceStore } from "../../workspaces/state/workspace-store.js";
21
+ import { WorkspaceRegistry } from "../../workspaces.js";
22
+ const execFileAsync = promisify(execFile);
23
+ /**
24
+ * Build the real in-memory MCP seam used by server integration tests. The
25
+ * fixture owns all durable stores and runtime managers so tests exercise the
26
+ * same composition path while keeping filesystem state isolated per test.
27
+ */
28
+ export async function fixture(t, options = {}) {
29
+ const root = await mkdtemp(join(tmpdir(), "forgerelay-server-test-"));
30
+ const project = join(root, "project");
31
+ const agentDir = join(root, "agent");
32
+ const stateDir = join(root, ".state");
33
+ await mkdir(join(project, ".forgerelay", "agents"), { recursive: true });
34
+ await mkdir(agentDir, { recursive: true });
35
+ await writeFile(join(agentDir, "AGENTS.md"), "global instructions\n");
36
+ await writeFile(join(project, "AGENTS.md"), "project instructions\n");
37
+ await writeFile(join(project, ".forgerelay", "agents", "reviewer.md"), [
38
+ "---",
39
+ "name: reviewer",
40
+ "description: Reviews project changes.",
41
+ "provider: codex",
42
+ "---",
43
+ "Review changes.",
44
+ ].join("\n"));
45
+ if (options.git) {
46
+ await writeFile(join(project, "README.md"), "hello\n");
47
+ await git(project, ["init"]);
48
+ await git(project, ["config", "user.email", "forgerelay@example.com"]);
49
+ await git(project, ["config", "user.name", "ForgeRelay Test"]);
50
+ await git(project, ["add", "."]);
51
+ await git(project, ["commit", "-m", "Initial commit"]);
52
+ }
53
+ const loadedConfig = loadConfig({
54
+ FORGERELAY_CONFIG_DIR: join(root, ".config"),
55
+ FORGERELAY_STATE_DIR: stateDir,
56
+ FORGERELAY_ALLOWED_ROOTS: root,
57
+ FORGERELAY_WORKTREE_ROOT: join(root, ".worktrees"),
58
+ FORGERELAY_AGENT_DIR: agentDir,
59
+ FORGERELAY_WIDGETS: "full",
60
+ FORGERELAY_TOOL_MODE: "full",
61
+ FORGERELAY_OAUTH_OWNER_TOKEN: "test-owner-token-that-is-long-enough",
62
+ PORT: "1",
63
+ ...options.env,
64
+ });
65
+ const config = options.hooks
66
+ ? { ...loadedConfig, hooks: parseHookConfig(options.hooks) }
67
+ : loadedConfig;
68
+ const store = new SqliteWorkspaceStore(stateDir);
69
+ const workspaces = new WorkspaceRegistry(config, store);
70
+ const auditStore = new ActivityAuditStore(stateDir);
71
+ const bashOutputStore = new BashOutputStore(stateDir);
72
+ let hostTurnSequence = 0;
73
+ const hostTurnStore = new HostTurnStore(stateDir, {
74
+ turnId: () => `turn_host_test_${++hostTurnSequence}`,
75
+ });
76
+ const activityQueries = new ActivityQueryService(hostTurnStore, auditStore, bashOutputStore);
77
+ const processSessions = options.processSessions ?? new ProcessManager({ outputAudit: bashOutputStore });
78
+ let activitySequence = 0;
79
+ let turnSequence = 0;
80
+ const activityLifecycle = new ActivityLifecycle(auditStore, {
81
+ activityId: () => `act_test_${++activitySequence}`,
82
+ turnId: () => `turn_test_${++turnSequence}`,
83
+ turnIdForConversation: (conversationScopeId, workspaceId) => activityQueries.currentTurnId(conversationScopeId, workspaceId),
84
+ });
85
+ const codeIntelligence = new CodeIntelligenceManager(config);
86
+ const server = createMcpServer(config, workspaces, createReviewCheckpointManager(), processSessions, [], options.incomingArtifactAdapters ?? [], codeIntelligence, activityLifecycle, bashOutputStore, activityQueries);
87
+ const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
88
+ const client = new Client({ name: "forgerelay-test-client", version: "1.0.0" });
89
+ await Promise.all([
90
+ client.connect(clientTransport),
91
+ server.connect(serverTransport),
92
+ ]);
93
+ let closed = false;
94
+ const close = async () => {
95
+ if (closed)
96
+ return;
97
+ closed = true;
98
+ await client.close();
99
+ await server.close();
100
+ await codeIntelligence.shutdown();
101
+ processSessions.shutdown();
102
+ hostTurnStore.close();
103
+ bashOutputStore.close();
104
+ auditStore.close();
105
+ store.close();
106
+ };
107
+ t.after(async () => {
108
+ await close();
109
+ await rm(root, { recursive: true, force: true });
110
+ });
111
+ return {
112
+ client,
113
+ project,
114
+ config,
115
+ stateDir,
116
+ store,
117
+ workspaces,
118
+ processSessions,
119
+ activityLifecycle,
120
+ codeIntelligence,
121
+ auditStore,
122
+ bashOutputStore,
123
+ hostTurnStore,
124
+ activityQueries,
125
+ close,
126
+ };
127
+ }
128
+ export async function git(cwd, args) {
129
+ await execFileAsync("git", args, { cwd });
130
+ }
131
+ export async function callOpen(client, path, conversationScopeId, mode, newWorktree) {
132
+ const params = {
133
+ name: "open_workspace",
134
+ arguments: {
135
+ path,
136
+ ...(mode ? { mode } : {}),
137
+ ...(newWorktree ? { newWorktree: true } : {}),
138
+ },
139
+ ...(conversationScopeId
140
+ ? { _meta: { "openai/session": conversationScopeId } }
141
+ : {}),
142
+ };
143
+ return client.callTool(params);
144
+ }
145
+ export function structuredContent(result) {
146
+ assert.ok(result.structuredContent);
147
+ return result.structuredContent;
148
+ }
149
+ export function responseText(result) {
150
+ const content = result.content;
151
+ assert.ok(Array.isArray(content));
152
+ const first = content[0];
153
+ assert.equal(first?.type, "text");
154
+ assert.equal(typeof first?.text, "string");
155
+ return first?.text;
156
+ }
157
+ export function allResponseText(result) {
158
+ const content = result.content;
159
+ assert.ok(Array.isArray(content));
160
+ return content
161
+ .filter((entry) => typeof entry === "object" &&
162
+ entry !== null &&
163
+ entry.type === "text" &&
164
+ typeof entry.text === "string")
165
+ .map((entry) => entry.text)
166
+ .join("\n");
167
+ }
168
+ export async function waitForCompletedProcess(processSessions) {
169
+ const deadline = performance.now() + 5_000;
170
+ while (processSessions.stats().completed === 0 && performance.now() < deadline) {
171
+ await new Promise((resolve) => setTimeout(resolve, 25));
172
+ }
173
+ assert.equal(processSessions.stats().completed, 1);
174
+ }
175
+ export async function waitForToolText(client, params, expected, timeoutMs = 5_000) {
176
+ const deadline = performance.now() + timeoutMs;
177
+ let result = await client.callTool(params);
178
+ while (!expected.test(allResponseText(result)) && performance.now() < deadline) {
179
+ await new Promise((resolve) => setTimeout(resolve, 25));
180
+ result = await client.callTool(params);
181
+ }
182
+ return result;
183
+ }
184
+ export function responseCard(result) {
185
+ const metadata = result._meta;
186
+ assert.ok(metadata && typeof metadata === "object");
187
+ const card = metadata.card;
188
+ assert.ok(card && typeof card === "object");
189
+ return card;
190
+ }