@akira-tl/forgerelay 0.8.8 → 0.8.10

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 (264) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +10 -18
  3. package/capabilities/code-intelligence/GUIDE.md +7 -3
  4. package/capabilities/host-integration/GUIDE.md +2 -2
  5. package/capabilities/subagents/GUIDE.md +9 -0
  6. package/capabilities/{workspace-tasks → workspace/workspace-tasks}/GUIDE.md +6 -0
  7. package/dist/activity/{audit-store.js → history/audit-store.js} +129 -21
  8. package/dist/activity/history/bash-output-store.js +250 -0
  9. package/dist/activity/{host-turn-store.js → history/host-turn-store.js} +4 -1
  10. package/dist/activity/{query-service.js → history/query-service.js} +13 -7
  11. package/dist/activity/{lifecycle.js → runtime/lifecycle.js} +1 -1
  12. package/dist/activity/{mcp-query-tools.js → runtime/mcp-query-tools.js} +3 -3
  13. package/dist/activity/storage/history.js +12 -0
  14. package/dist/activity/storage/paths.js +56 -0
  15. package/dist/activity/storage/segmented-log.js +149 -0
  16. package/dist/cli/init.js +180 -0
  17. package/dist/cli/setup-support.js +181 -0
  18. package/dist/cli.js +23 -216
  19. package/dist/lsp/code-intelligence.js +18 -4
  20. package/dist/lsp/runtime/diagnostic-snapshots.js +52 -1
  21. package/dist/lsp/runtime/managed-language-servers.js +172 -0
  22. package/dist/lsp/runtime/manager.js +37 -2
  23. package/dist/lsp/runtime/process-launch.js +3 -0
  24. package/dist/lsp/test-support/server-fixture.js +14 -11
  25. package/dist/mcp/filesystem/filesystem-tools.js +245 -0
  26. package/dist/mcp/hooks/command-runner.js +79 -0
  27. package/dist/{hook-cli.js → mcp/hooks/hook-cli.js} +1 -1
  28. package/dist/{hooks.js → mcp/hooks/hooks.js} +3 -80
  29. package/dist/{oauth-provider.js → mcp/oauth/oauth-provider.js} +1 -1
  30. package/dist/{oauth-store.js → mcp/oauth/oauth-store.js} +5 -4
  31. package/dist/{operations → mcp/operations}/native-bulk-mutations.js +2 -2
  32. package/dist/mcp/panel/app.js +143 -0
  33. package/dist/{mcp-app-template.js → mcp/panel/mcp-app-template.js} +2 -38
  34. package/dist/{process-platform.js → mcp/process/process-platform.js} +21 -9
  35. package/dist/{process-sessions.js → mcp/process/process-sessions.js} +3 -3
  36. package/dist/mcp/process/runtime.js +257 -0
  37. package/dist/mcp/process/tools.js +346 -0
  38. package/dist/mcp/server/core/activity-support.js +70 -0
  39. package/dist/{capabilities.js → mcp/server/core/capabilities.js} +5 -3
  40. package/dist/{capability-registry.js → mcp/server/core/capability-registry.js} +9 -4
  41. package/dist/mcp/server/core/capability-support.js +90 -0
  42. package/dist/mcp/server/core/schemas.js +216 -0
  43. package/dist/mcp/server/core/tool-support.js +210 -0
  44. package/dist/mcp/server/core/version.js +9 -0
  45. package/dist/mcp/server/operations/runtime/filesystem-tools.js +455 -0
  46. package/dist/mcp/server/operations/runtime/mutation-diagnostics.js +54 -0
  47. package/dist/mcp/server/operations/runtime/operation-runtime.js +524 -0
  48. package/dist/mcp/server/transport/http-server.js +301 -0
  49. package/dist/mcp/server/transport/http-support.js +43 -0
  50. package/dist/mcp/server/workspace/runtime/workspace-open-presentation.js +242 -0
  51. package/dist/mcp/server/workspace/runtime/workspace-open-schema.js +187 -0
  52. package/dist/mcp/server/workspace/runtime/workspace-open.js +555 -0
  53. package/dist/mcp/server/workspace/runtime/workspace-tools.js +601 -0
  54. package/dist/mcp/server-instructions.js +1 -1
  55. package/dist/{config.js → runtime/config/config.js} +4 -3
  56. package/dist/{user-config.js → runtime/config/user-config.js} +14 -63
  57. package/dist/{db → runtime/state/db}/client.js +1 -1
  58. package/dist/{db → runtime/state/db}/migrations.js +34 -3
  59. package/dist/runtime/state/lock/file-lock.js +130 -0
  60. package/dist/runtime/testing/server-fixture.js +190 -0
  61. package/dist/server.js +133 -4189
  62. package/dist/subagents/providers/adapters/claude.js +86 -52
  63. package/dist/subagents/providers/adapters/codex.js +103 -39
  64. package/dist/subagents/providers/adapters/opencode.js +114 -84
  65. package/dist/subagents/providers/adapters/pi.js +59 -8
  66. package/dist/subagents/providers/availability.js +15 -30
  67. package/dist/subagents/providers/runtime/external-command.js +56 -0
  68. package/dist/subagents/sessions/capability.js +1 -1
  69. package/dist/subagents/sessions/execution.js +1 -1
  70. package/dist/subagents/sessions/mcp/activity-audit-test-support.js +30 -0
  71. package/dist/subagents/sessions/store.js +1 -1
  72. package/dist/ui/.vite/manifest.json +342 -410
  73. package/dist/ui/activity-panel-app.html +2 -4
  74. package/dist/ui/assets/activity-panel-app-7Xe0alhm.css +1 -0
  75. package/dist/ui/assets/activity-panel-app-Sl1Jro8v.js +115 -0
  76. package/dist/ui/assets/angular-html-B_yn91wW.js +1 -0
  77. package/dist/ui/assets/{angular-ts-Bxh4-O1-.js → angular-ts-DSgtloKz.js} +1 -1
  78. package/dist/ui/assets/{apl-BmYHKQ6m.js → apl-NatWtF8X.js} +1 -1
  79. package/dist/ui/assets/{astro-C51vqDKq.js → astro-BYFEVBuN.js} +1 -1
  80. package/dist/ui/assets/{blade-CdhVsmwq.js → blade-DoO7yvMR.js} +1 -1
  81. package/dist/ui/assets/c-Rl2hMCcg.js +1 -0
  82. package/dist/ui/assets/{cobol-Cf6azoN_.js → cobol-ByC5mWiB.js} +1 -1
  83. package/dist/ui/assets/{coffee-vVdGzlmt.js → coffee-Dez77Qov.js} +1 -1
  84. package/dist/ui/assets/cpp-DtaAQT9h.js +1 -0
  85. package/dist/ui/assets/{crystal-5UZy6KIX.js → crystal-Bgmbf8ec.js} +1 -1
  86. package/dist/ui/assets/css-C1tjBtVN.js +1 -0
  87. package/dist/ui/assets/{edge-CESgo4ia.js → edge-DeH6u8Wg.js} +1 -1
  88. package/dist/ui/assets/{elixir-9FKfw4cJ.js → elixir-_H3KnmWT.js} +1 -1
  89. package/dist/ui/assets/{elm-Dv7A0aLa.js → elm-CsCe3U28.js} +1 -1
  90. package/dist/ui/assets/{erb-DHFmDvM0.js → erb-CbM-L2g8.js} +1 -1
  91. package/dist/ui/assets/{git-rebase-utbU6CzV.js → git-rebase-D0s5pDh9.js} +1 -1
  92. package/dist/ui/assets/{glimmer-js-t2kiCkyB.js → glimmer-js-BfOToq_A.js} +1 -1
  93. package/dist/ui/assets/{glimmer-ts-F0Jv5okV.js → glimmer-ts-CCUOkL2K.js} +1 -1
  94. package/dist/ui/assets/glsl-DH4ec9dE.js +1 -0
  95. package/dist/ui/assets/graphql-DilAMBoI.js +1 -0
  96. package/dist/ui/assets/{hack-8yjdbaSA.js → hack-BDkNDZlF.js} +1 -1
  97. package/dist/ui/assets/haml-D_QBp6K7.js +1 -0
  98. package/dist/ui/assets/{handlebars-BBFEg4Lo.js → handlebars-DePUrTpC.js} +1 -1
  99. package/dist/ui/assets/heavy-payload-DOwZpO6n.js +290 -0
  100. package/dist/ui/assets/html-By4dNnM4.js +1 -0
  101. package/dist/ui/assets/{html-derivative-DJAQcZyc.js → html-derivative-DDw9Ef4Q.js} +1 -1
  102. package/dist/ui/assets/{http-Dg5LSYRK.js → http-B0QxWc9M.js} +1 -1
  103. package/dist/ui/assets/{hurl-Y5uYzMZR.js → hurl-vMQ4lda1.js} +1 -1
  104. package/dist/ui/assets/java-DYRe2lWG.js +1 -0
  105. package/dist/ui/assets/javascript-CxyGqwcL.js +1 -0
  106. package/dist/ui/assets/{jinja-Bt30h8Ik.js → jinja-JUwV-fZt.js} +1 -1
  107. package/dist/ui/assets/{jison-D5uZ1Nyo.js → jison-7ZqCEvS8.js} +1 -1
  108. package/dist/ui/assets/json-dbltgiiA.js +1 -0
  109. package/dist/ui/assets/jsx-oDopT7YQ.js +1 -0
  110. package/dist/ui/assets/{julia-76hRaaGz.js → julia-CkKiNSav.js} +1 -1
  111. package/dist/ui/assets/{just-CDE7W0vW.js → just-DuqCAKJ3.js} +1 -1
  112. package/dist/ui/assets/{latex-CVO2Byab.js → latex-sudcRDSn.js} +1 -1
  113. package/dist/ui/assets/{liquid-CA-AS3EX.js → liquid-Di-08Js7.js} +1 -1
  114. package/dist/ui/assets/lua-YlqdMoV0.js +1 -0
  115. package/dist/ui/assets/{marko-_BWrPfny.js → marko-CJRNyJDb.js} +1 -1
  116. package/dist/ui/assets/{mdc-PyJv85eS.js → mdc-t12CzZHs.js} +1 -1
  117. package/dist/ui/assets/{nginx-DAWVw8yu.js → nginx-57-b4XwT.js} +1 -1
  118. package/dist/ui/assets/{nim-CKE1dzpU.js → nim-BzrswYQa.js} +1 -1
  119. package/dist/ui/assets/{perl-BBhWva19.js → perl-D9JR_iIh.js} +1 -1
  120. package/dist/ui/assets/{php-ChIeaAoF.js → php-Py2gFHil.js} +1 -1
  121. package/dist/ui/assets/{pug-CMgI5afi.js → pug-PzLLy0h1.js} +1 -1
  122. package/dist/ui/assets/{qml-CjxTkcFH.js → qml-By8fwgV4.js} +1 -1
  123. package/dist/ui/assets/r-CcezTbLq.js +1 -0
  124. package/dist/ui/assets/{razor-i4DbHr8p.js → razor-dG-vdhL_.js} +1 -1
  125. package/dist/ui/assets/regexp-BlsQ-N_8.js +1 -0
  126. package/dist/ui/assets/{rst-oy_97Kl4.js → rst-C2awDCat.js} +1 -1
  127. package/dist/ui/assets/{ruby-C5zYZIyl.js → ruby-zA2W_49d.js} +1 -1
  128. package/dist/ui/assets/{sas-BnMEh2S3.js → sas-D49bhyaN.js} +1 -1
  129. package/dist/ui/assets/scss-BQb-cp01.js +1 -0
  130. package/dist/ui/assets/shellscript-Dga4H_Ni.js +1 -0
  131. package/dist/ui/assets/{shellsession-BpE5o7_3.js → shellsession-n3-5CqcF.js} +1 -1
  132. package/dist/ui/assets/{soy-COesmhTY.js → soy-XYBOf_BA.js} +1 -1
  133. package/dist/ui/assets/sql-ET4u5Cpl.js +1 -0
  134. package/dist/ui/assets/{stata-CGQEuSZq.js → stata-CLHcJHO_.js} +1 -1
  135. package/dist/ui/assets/{surrealql-Bz4JbU-9.js → surrealql-BdjQ60T4.js} +1 -1
  136. package/dist/ui/assets/{svelte-1jM2ejXj.js → svelte-B2RPtKJe.js} +1 -1
  137. package/dist/ui/assets/{templ-C-VwELcp.js → templ-CrByPW_W.js} +1 -1
  138. package/dist/ui/assets/{tex-t1rUiQCm.js → tex-DZBkg363.js} +1 -1
  139. package/dist/ui/assets/{ts-tags-Bewoa6pS.js → ts-tags-DJR3Gphb.js} +1 -1
  140. package/dist/ui/assets/tsx-CajzROyy.js +1 -0
  141. package/dist/ui/assets/{twig-DVg-vELl.js → twig-BZjk5Zda.js} +1 -1
  142. package/dist/ui/assets/typescript-B-EWToLj.js +1 -0
  143. package/dist/ui/assets/{vue-wPQ7UlMy.js → vue-D_rHCCr8.js} +1 -1
  144. package/dist/ui/assets/{vue-html-BHtJ8geX.js → vue-html-CC4WlvVt.js} +1 -1
  145. package/dist/ui/assets/{vue-vine-CNSGAWwc.js → vue-vine-BgNMVgpk.js} +1 -1
  146. package/dist/ui/assets/xml-BI86-WbH.js +1 -0
  147. package/dist/ui/assets/{xsl-3_vJ27aE.js → xsl-DXeLZV4J.js} +1 -1
  148. package/dist/ui/assets/yaml-60_PFlWa.js +1 -0
  149. package/dist/workspaces/bootstrap.js +72 -0
  150. package/dist/workspaces/context.js +337 -0
  151. package/dist/workspaces/conversation-test-support.js +86 -0
  152. package/dist/{git-worktrees.js → workspaces/git/git-worktrees.js} +1 -1
  153. package/dist/workspaces/inventory.js +131 -0
  154. package/dist/workspaces/paths.js +55 -0
  155. package/dist/{remote-auth.js → workspaces/relay/auth/remote-auth.js} +2 -2
  156. package/dist/workspaces/relay/result-support.js +111 -0
  157. package/dist/workspaces/relay/tests/test-support.js +203 -0
  158. package/dist/{remote-mcp-connection-pool.js → workspaces/relay/transport/remote-mcp-connection-pool.js} +1 -1
  159. package/dist/{remote-transport.js → workspaces/relay/transport/remote-transport.js} +30 -3
  160. package/dist/workspaces/relay/types.js +1 -0
  161. package/dist/{remote-workspace-relay.js → workspaces/relay/workspace-relay.js} +23 -172
  162. package/dist/workspaces/resources/resource-monitor.js +377 -0
  163. package/dist/workspaces/resources/skills.js +283 -0
  164. package/dist/{review-checkpoints.js → workspaces/review/review-checkpoints.js} +2 -2
  165. package/dist/workspaces/sessions.js +460 -0
  166. package/dist/{workspace-store.js → workspaces/state/workspace-store.js} +2 -2
  167. package/dist/workspaces.js +77 -993
  168. package/docs/chatgpt-coding-workflow.md +7 -14
  169. package/docs/configuration.md +26 -34
  170. package/docs/gotchas.md +0 -30
  171. package/docs/roadmap.md +6 -10
  172. package/docs/security.md +1 -5
  173. package/docs/setup.md +0 -11
  174. package/package.json +6 -9
  175. package/scripts/ci/architecture.mjs +109 -0
  176. package/scripts/ci/verify.mjs +5 -0
  177. package/scripts/debug/accept/bootstrap.mjs +326 -0
  178. package/scripts/debug/accept/harness.mjs +76 -0
  179. package/scripts/debug/accept/support.mjs +432 -0
  180. package/scripts/debug/accept.mjs +43 -806
  181. package/scripts/debug/relay-accept/support.mjs +135 -0
  182. package/scripts/debug/relay-accept.mjs +26 -115
  183. package/scripts/debug/traffic/support.mjs +167 -0
  184. package/scripts/debug/traffic/traffic-audit.mjs +4 -161
  185. package/scripts/{lsp-interop-support.test.mjs → lsp/interop-support.test.mjs} +1 -1
  186. package/scripts/{lsp-interop.mjs → lsp/interop.mjs} +3 -3
  187. package/scripts/release/publish.mjs +12 -9
  188. package/scripts/release/release-gate.test.mjs +3 -1
  189. package/scripts/release/release-version.test.mjs +13 -1
  190. package/scripts/release-parity.mjs +1 -1
  191. package/scripts/release-proof.mjs +15 -0
  192. package/scripts/release-proof.test.mjs +27 -1
  193. package/scripts/release-version.mjs +16 -1
  194. package/scripts/wiki/sync.mjs +21 -10
  195. package/dist/activity/bash-output-store.js +0 -127
  196. package/dist/pi-tools.js +0 -110
  197. package/dist/skills.js +0 -127
  198. package/dist/ui/assets/activity-panel-app-7AckaxIR.js +0 -2
  199. package/dist/ui/assets/angular-html-BkgDQl-G.js +0 -1
  200. package/dist/ui/assets/c-IBzBpxAO.js +0 -1
  201. package/dist/ui/assets/chunk-EyZ2wyi3.js +0 -1
  202. package/dist/ui/assets/cpp-CVHeO-Jk.js +0 -1
  203. package/dist/ui/assets/css-DEfus9J5.js +0 -1
  204. package/dist/ui/assets/glsl-_ooCjaeB.js +0 -1
  205. package/dist/ui/assets/graphql-B9LqP-cY.js +0 -1
  206. package/dist/ui/assets/haml-BU5EoZBn.js +0 -1
  207. package/dist/ui/assets/heavy-payload-Bol1_mcs.js +0 -4
  208. package/dist/ui/assets/html-D-C6YZk4.js +0 -1
  209. package/dist/ui/assets/java-ppHvycj5.js +0 -1
  210. package/dist/ui/assets/javascript-DDTjKLlQ.js +0 -1
  211. package/dist/ui/assets/json-Cw91r8h1.js +0 -1
  212. package/dist/ui/assets/jsx-04X2FT_1.js +0 -1
  213. package/dist/ui/assets/lua-CiyBp52Y.js +0 -1
  214. package/dist/ui/assets/r-DfFga4si.js +0 -1
  215. package/dist/ui/assets/regexp-iC9_nbSI.js +0 -1
  216. package/dist/ui/assets/review-payload-BqJp0c5e.js +0 -1
  217. package/dist/ui/assets/scrollbar-CvE-I-jG.js +0 -287
  218. package/dist/ui/assets/scss-BfZXsAWQ.js +0 -1
  219. package/dist/ui/assets/shellscript-BC2srNaB.js +0 -1
  220. package/dist/ui/assets/sql-Ccv_yeUm.js +0 -1
  221. package/dist/ui/assets/tsx-CTXzrBkN.js +0 -1
  222. package/dist/ui/assets/typescript--ElHoFkw.js +0 -1
  223. package/dist/ui/assets/workspace-app-BLW7p6IZ.js +0 -114
  224. package/dist/ui/assets/workspace-app-BjNqR0en.js +0 -1
  225. package/dist/ui/assets/workspace-app-CmaYU4DW.js +0 -3
  226. package/dist/ui/assets/workspace-app-D2bJ5fjt.css +0 -1
  227. package/dist/ui/assets/workspace-lifecycle-app-BVbI2Ilf.js +0 -1
  228. package/dist/ui/assets/xml-JnmX6vyS.js +0 -1
  229. package/dist/ui/assets/yaml-OAcZkP-w.js +0 -1
  230. package/dist/ui/workspace-app.html +0 -18
  231. package/dist/ui/workspace-lifecycle-app.html +0 -18
  232. package/docs/agent-profile-schema.md +0 -114
  233. package/docs/artifact-exchange.md +0 -65
  234. package/docs/versioning.md +0 -205
  235. package/skills/subagent-delegation/SKILL.md +0 -132
  236. /package/capabilities/{managed-worktrees → workspace/managed-worktrees}/GUIDE.md +0 -0
  237. /package/dist/activity/{process-output-audit.js → runtime/process-output-audit.js} +0 -0
  238. /package/dist/{artifact-error.js → mcp/artifacts/artifact-error.js} +0 -0
  239. /package/dist/{artifact-tools.js → mcp/artifacts/artifact-tools.js} +0 -0
  240. /package/dist/{incoming-artifacts.js → mcp/artifacts/incoming-artifacts.js} +0 -0
  241. /package/dist/{advertised-files.js → mcp/filesystem/advertised-files.js} +0 -0
  242. /package/dist/{apply-patch.js → mcp/filesystem/apply-patch.js} +0 -0
  243. /package/dist/{file-mutations.js → mcp/filesystem/file-mutations.js} +0 -0
  244. /package/dist/{roots.js → mcp/filesystem/roots.js} +0 -0
  245. /package/dist/{oauth → mcp/oauth}/public-url.js +0 -0
  246. /package/dist/{oauth → mcp/oauth}/router.js +0 -0
  247. /package/dist/{operations → mcp/operations}/batch/executor.js +0 -0
  248. /package/dist/{operations → mcp/operations}/batch/scheduler.js +0 -0
  249. /package/dist/{operations → mcp/operations}/batch/types.js +0 -0
  250. /package/dist/{operations → mcp/operations}/bulk-mutation.js +0 -0
  251. /package/dist/{operations → mcp/operations}/bulk-read.js +0 -0
  252. /package/dist/{operations → mcp/operations}/core-operation-executor.js +0 -0
  253. /package/dist/{request-meta.js → mcp/request-meta.js} +0 -0
  254. /package/dist/{mcp-sessions.js → mcp/server/transport/mcp-sessions.js} +0 -0
  255. /package/dist/{server-shutdown.js → mcp/server/transport/server-shutdown.js} +0 -0
  256. /package/dist/{logger.js → runtime/logging/logger.js} +0 -0
  257. /package/dist/{db → runtime/state/db}/schema.js +0 -0
  258. /package/dist/{composite-activity.js → workspaces/composite/composite-activity.js} +0 -0
  259. /package/dist/{composite-workspaces.js → workspaces/composite/composite-workspaces.js} +0 -0
  260. /package/dist/{git.js → workspaces/git/git.js} +0 -0
  261. /package/dist/{workspace-presentation.js → workspaces/presentation/workspace-presentation.js} +0 -0
  262. /package/dist/{workspace-task-reminders.js → workspaces/tasks/workspace-task-reminders.js} +0 -0
  263. /package/dist/{workspace-tasks.js → workspaces/tasks/workspace-tasks.js} +0 -0
  264. /package/scripts/{lsp-interop-support.mjs → lsp/interop-support.mjs} +0 -0
@@ -1,15 +1,13 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
2
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { CallToolResultSchema } from "@modelcontextprotocol/sdk/types.js";
5
- import { isRemoteMcpUnauthorized, refreshRemoteAuthentication, } from "./remote-auth.js";
6
- import { RemoteMcpConnectionPool } from "./remote-mcp-connection-pool.js";
7
- import { withRemoteServiceEndpoint } from "./remote-transport.js";
8
- import { loadForgeRelayFiles, writeForgeRelayRemote, } from "./user-config.js";
9
- const ROUTE_LOCK_RETRY_MS = 10;
10
- const ROUTE_LOCK_TIMEOUT_MS = 5_000;
11
- const ROUTE_LOCK_STALE_MS = 30_000;
12
- const ROUTE_LOCK_SLEEP = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
5
+ import { isRemoteMcpUnauthorized, refreshRemoteAuthentication, } from "./auth/remote-auth.js";
6
+ import { RemoteMcpConnectionPool } from "./transport/remote-mcp-connection-pool.js";
7
+ import { withFileLock } from "../../runtime/state/lock/file-lock.js";
8
+ import { withRemoteServiceEndpoint } from "./transport/remote-transport.js";
9
+ import { loadForgeRelayFiles, writeForgeRelayRemote, } from "../../runtime/config/user-config.js";
10
+ import { assertRemoteToolSucceeded, stringField, copyStringField, copyBooleanField, copyNumberField, safeTaskSummary, toolResultText, remapToolResultWorkspaceId, replaceExactWorkspaceId, sanitizedRemoteError, errorMessage } from "./result-support.js";
13
11
  export class RemoteWorkspaceRelay {
14
12
  routes = new Map();
15
13
  turnRoutes = new Map();
@@ -109,7 +107,7 @@ export class RemoteWorkspaceRelay {
109
107
  throw new Error("Remote open_workspace response did not include a valid workspace mode.");
110
108
  }
111
109
  const sourceRoot = typeof structured?.sourceRoot === "string" ? structured.sourceRoot : undefined;
112
- const route = this.findOrCreateRoute({
110
+ const route = await this.findOrCreateRoute({
113
111
  remoteInstanceId: resolved.remote.instanceId,
114
112
  remoteWorkspaceId,
115
113
  root,
@@ -167,7 +165,7 @@ export class RemoteWorkspaceRelay {
167
165
  ? structured.mode
168
166
  : route.mode;
169
167
  const sourceRoot = typeof structured?.sourceRoot === "string" ? structured.sourceRoot : route.sourceRoot;
170
- this.findOrCreateRoute({
168
+ await this.findOrCreateRoute({
171
169
  remoteInstanceId: route.remoteInstanceId,
172
170
  remoteWorkspaceId: route.remoteWorkspaceId,
173
171
  root,
@@ -194,6 +192,9 @@ export class RemoteWorkspaceRelay {
194
192
  async bash(gatewayWorkspaceId, input, conversationScopeId) {
195
193
  return this.callWorkspaceTool(gatewayWorkspaceId, "bash", input, conversationScopeId);
196
194
  }
195
+ async workspaceInstruction(gatewayWorkspaceId, path, conversationScopeId) {
196
+ return this.callWorkspaceTool(gatewayWorkspaceId, "workspace_instruction", { path }, conversationScopeId);
197
+ }
197
198
  async execCommand(gatewayWorkspaceId, input, conversationScopeId) {
198
199
  return this.callWorkspaceTool(gatewayWorkspaceId, "exec_command", input, conversationScopeId);
199
200
  }
@@ -296,7 +297,7 @@ export class RemoteWorkspaceRelay {
296
297
  const remoteStructured = result.structuredContent;
297
298
  if (action === "delete") {
298
299
  this.routes.delete(gatewayWorkspaceId);
299
- this.deletePersistedRoute(gatewayWorkspaceId);
300
+ await this.deletePersistedRoute(gatewayWorkspaceId);
300
301
  }
301
302
  for (const [turnId, routedWorkspaceId] of this.turnRoutes) {
302
303
  if (routedWorkspaceId === gatewayWorkspaceId)
@@ -356,35 +357,33 @@ export class RemoteWorkspaceRelay {
356
357
  this.routes.set(workspaceId, route);
357
358
  }
358
359
  }
359
- findOrCreateRoute(input) {
360
- let selected;
360
+ async findOrCreateRoute(input) {
361
361
  mkdirSync(this.routeStateDir, { recursive: true });
362
- this.withRouteFileLock(() => {
362
+ const selected = await withFileLock(`${this.routeStatePath}.lock`, () => {
363
363
  const routes = this.readRoutesFromDisk();
364
364
  const existing = [...routes.values()]
365
365
  .filter((route) => route.remoteInstanceId === input.remoteInstanceId &&
366
366
  route.remoteWorkspaceId === input.remoteWorkspaceId)
367
367
  .sort((left, right) => left.gatewayWorkspaceId.localeCompare(right.gatewayWorkspaceId))[0];
368
- selected = {
368
+ const route = {
369
369
  gatewayWorkspaceId: existing?.gatewayWorkspaceId ?? this.allocateGatewayWorkspaceId(routes),
370
370
  ...input,
371
371
  };
372
- routes.set(selected.gatewayWorkspaceId, selected);
372
+ routes.set(route.gatewayWorkspaceId, route);
373
373
  this.writeRoutesToDisk(routes);
374
+ return route;
374
375
  });
375
- if (!selected)
376
- throw new Error("Failed to persist relayed Workspace route.");
377
376
  this.routes.set(selected.gatewayWorkspaceId, selected);
378
377
  return selected;
379
378
  }
380
- deletePersistedRoute(workspaceId) {
381
- this.updatePersistedRoutes((routes) => {
379
+ async deletePersistedRoute(workspaceId) {
380
+ await this.updatePersistedRoutes((routes) => {
382
381
  routes.delete(workspaceId);
383
382
  });
384
383
  }
385
- updatePersistedRoutes(update) {
384
+ async updatePersistedRoutes(update) {
386
385
  mkdirSync(this.routeStateDir, { recursive: true });
387
- this.withRouteFileLock(() => {
386
+ await withFileLock(`${this.routeStatePath}.lock`, () => {
388
387
  const routes = this.readRoutesFromDisk();
389
388
  update(routes);
390
389
  this.writeRoutesToDisk(routes);
@@ -418,43 +417,6 @@ export class RemoteWorkspaceRelay {
418
417
  rmSync(tempPath, { force: true });
419
418
  }
420
419
  }
421
- withRouteFileLock(operation) {
422
- const lockPath = `${this.routeStatePath}.lock`;
423
- const deadline = Date.now() + ROUTE_LOCK_TIMEOUT_MS;
424
- for (;;) {
425
- try {
426
- const fd = openSync(lockPath, "wx", 0o600);
427
- closeSync(fd);
428
- break;
429
- }
430
- catch (error) {
431
- const code = error.code;
432
- if (code !== "EEXIST")
433
- throw error;
434
- try {
435
- if (Date.now() - statSync(lockPath).mtimeMs > ROUTE_LOCK_STALE_MS) {
436
- rmSync(lockPath, { force: true });
437
- continue;
438
- }
439
- }
440
- catch (statError) {
441
- if (statError.code === "ENOENT")
442
- continue;
443
- throw statError;
444
- }
445
- if (Date.now() >= deadline) {
446
- throw new Error(`Timed out waiting for relayed workspace route lock: ${lockPath}`);
447
- }
448
- Atomics.wait(ROUTE_LOCK_SLEEP, 0, 0, ROUTE_LOCK_RETRY_MS);
449
- }
450
- }
451
- try {
452
- return operation();
453
- }
454
- finally {
455
- rmSync(lockPath, { force: true });
456
- }
457
- }
458
420
  remoteByAlias(aliasInput) {
459
421
  const alias = aliasInput.trim();
460
422
  if (!alias)
@@ -522,7 +484,7 @@ export class RemoteWorkspaceRelay {
522
484
  }
523
485
  async refreshRemote(alias, remote, endpoint) {
524
486
  const refreshed = await refreshRemoteAuthentication(remote, endpoint);
525
- writeForgeRelayRemote(alias, refreshed, this.authEnv);
487
+ await writeForgeRelayRemote(alias, refreshed, this.authEnv);
526
488
  return refreshed;
527
489
  }
528
490
  allocateGatewayWorkspaceId(routes = this.routes) {
@@ -533,114 +495,3 @@ export class RemoteWorkspaceRelay {
533
495
  return workspaceId;
534
496
  }
535
497
  }
536
- function assertRemoteToolSucceeded(alias, tool, result) {
537
- if (result.isError !== true)
538
- return;
539
- throw new Error(`Remote ForgeRelay ${alias} ${tool} failed: ${toolResultText(result)}`);
540
- }
541
- function stringField(structured, field, label) {
542
- const value = structured?.[field];
543
- if (typeof value !== "string" || !value) {
544
- throw new Error(`${label} did not include ${field}.`);
545
- }
546
- return value;
547
- }
548
- function copyStringField(source, target, field) {
549
- const value = source[field];
550
- if (typeof value === "string")
551
- target[field] = value;
552
- }
553
- function copyBooleanField(source, target, field) {
554
- const value = source[field];
555
- if (typeof value === "boolean")
556
- target[field] = value;
557
- }
558
- function copyNumberField(source, target, field) {
559
- const value = source[field];
560
- if (typeof value === "number" && Number.isFinite(value) && value >= 0)
561
- target[field] = value;
562
- }
563
- function safeTaskSummary(value) {
564
- if (!value || typeof value !== "object")
565
- return undefined;
566
- const summary = value;
567
- if (summary.level !== "summary" ||
568
- summary.version !== 1 ||
569
- typeof summary.revision !== "number" ||
570
- !Number.isInteger(summary.revision) ||
571
- summary.revision < 0 ||
572
- !Array.isArray(summary.lists)) {
573
- return undefined;
574
- }
575
- const lists = [];
576
- for (const value of summary.lists) {
577
- if (!value || typeof value !== "object")
578
- return undefined;
579
- const list = value;
580
- if (typeof list.id !== "string" ||
581
- typeof list.name !== "string" ||
582
- (list.state !== "active" && list.state !== "archived") ||
583
- typeof list.revision !== "number" || !Number.isInteger(list.revision) || list.revision <= 0 ||
584
- typeof list.taskCount !== "number" || !Number.isInteger(list.taskCount) || list.taskCount < 0 ||
585
- typeof list.unfinishedTaskCount !== "number" || !Number.isInteger(list.unfinishedTaskCount) || list.unfinishedTaskCount < 0) {
586
- return undefined;
587
- }
588
- lists.push({
589
- id: list.id,
590
- name: list.name,
591
- state: list.state,
592
- revision: list.revision,
593
- taskCount: list.taskCount,
594
- unfinishedTaskCount: list.unfinishedTaskCount,
595
- });
596
- }
597
- return {
598
- level: "summary",
599
- version: 1,
600
- revision: summary.revision,
601
- lists,
602
- };
603
- }
604
- function toolResultText(result) {
605
- return (result.content ?? [])
606
- .filter((entry) => entry.type === "text")
607
- .map((entry) => entry.text)
608
- .join("\n") || "remote tool returned an error";
609
- }
610
- function remapToolResultWorkspaceId(result, remoteWorkspaceId, gatewayWorkspaceId) {
611
- return {
612
- ...result,
613
- content: (result.content ?? []).map((entry) => entry.type === "text"
614
- ? { ...entry, text: entry.text.split(remoteWorkspaceId).join(gatewayWorkspaceId) }
615
- : entry),
616
- ...(result._meta
617
- ? { _meta: replaceExactWorkspaceId(result._meta, remoteWorkspaceId, gatewayWorkspaceId) }
618
- : {}),
619
- ...(result.structuredContent
620
- ? {
621
- structuredContent: replaceExactWorkspaceId(result.structuredContent, remoteWorkspaceId, gatewayWorkspaceId),
622
- }
623
- : {}),
624
- };
625
- }
626
- function replaceExactWorkspaceId(value, from, to) {
627
- if (typeof value === "string")
628
- return value.split(from).join(to);
629
- if (Array.isArray(value))
630
- return value.map((entry) => replaceExactWorkspaceId(entry, from, to));
631
- if (!value || typeof value !== "object")
632
- return value;
633
- return Object.fromEntries(Object.entries(value)
634
- .map(([key, entry]) => [key, replaceExactWorkspaceId(entry, from, to)]));
635
- }
636
- function sanitizedRemoteError(error, remoteWorkspaceId, gatewayWorkspaceId) {
637
- let message = errorMessage(error);
638
- if (remoteWorkspaceId && gatewayWorkspaceId) {
639
- message = message.split(remoteWorkspaceId).join(gatewayWorkspaceId);
640
- }
641
- message = message.replace(/(^|[^A-Za-z0-9_])ws_[0-9a-f]{10}(?=$|[^A-Za-z0-9_])/g, "$1[remote-workspace]");
642
- return new Error(message, { cause: error });
643
- }
644
- function errorMessage(error) {
645
- return error instanceof Error ? error.message : String(error);
646
- }
@@ -0,0 +1,377 @@
1
+ import { readFileSync, realpathSync, unwatchFile, watchFile } from "node:fs";
2
+ import { relative, resolve, sep } from "node:path";
3
+ import { createTwoFilesPatch } from "diff";
4
+ import { skillSummaryFromContent } from "./skills.js";
5
+ const WATCH_INTERVAL_MS = 750;
6
+ const MAX_HISTORY = 128;
7
+ const MAX_DELIVERY_CHARACTERS = 32 * 1024;
8
+ const MAX_PATCH_CHARACTERS = 8 * 1024;
9
+ const sharedWatches = new Map();
10
+ let monitorSequence = 0;
11
+ /**
12
+ * Tracks already-advertised Workspace instruction and Skill files. The OS-level
13
+ * stat watcher is process-global and canonical-path keyed, so two Workspaces or
14
+ * conversations that refer to the same physical file still create only one
15
+ * underlying watcher. Resource changes are delivered as bounded deltas.
16
+ */
17
+ export class WorkspaceResourceMonitor {
18
+ id = `monitor-${++monitorSequence}`;
19
+ states = new Map();
20
+ trackWorkspace(input) {
21
+ const state = this.states.get(input.workspaceId) ?? {
22
+ root: input.root,
23
+ revision: 0,
24
+ tracked: new Map(),
25
+ subscriptions: new Map(),
26
+ changes: [],
27
+ deliveredRevisionByScope: new Map(),
28
+ };
29
+ state.root = input.root;
30
+ this.states.set(input.workspaceId, state);
31
+ const desired = new Map();
32
+ for (const path of input.availableInstructions) {
33
+ this.addDesired(desired, path, {
34
+ kind: "instruction-available",
35
+ displayPath: displayWorkspacePath(path, input.root),
36
+ exposeSkillBody: false,
37
+ });
38
+ }
39
+ for (const path of input.loadedInstructions) {
40
+ this.addDesired(desired, path, {
41
+ kind: "instruction-loaded",
42
+ displayPath: displayWorkspacePath(path, input.root),
43
+ exposeSkillBody: false,
44
+ });
45
+ }
46
+ for (const skill of input.skills) {
47
+ this.addDesired(desired, skill.filePath, {
48
+ kind: "skill",
49
+ displayPath: `skills://${encodeURIComponent(skill.name)}`,
50
+ skillName: skill.name,
51
+ exposeSkillBody: skill.activated,
52
+ });
53
+ }
54
+ for (const [key, stop] of state.tracked) {
55
+ if (desired.has(key))
56
+ continue;
57
+ stop();
58
+ state.tracked.delete(key);
59
+ state.subscriptions.delete(key);
60
+ }
61
+ for (const [key, item] of desired) {
62
+ state.subscriptions.set(key, item.subscription);
63
+ if (state.tracked.has(key)) {
64
+ synchronizeSharedWatch(key);
65
+ continue;
66
+ }
67
+ const subscriberId = `${this.id}:${input.workspaceId}:${key}`;
68
+ const stop = subscribeSharedWatch(item.path, subscriberId, (oldContent, newContent) => {
69
+ this.recordChange(input.workspaceId, key, oldContent, newContent);
70
+ });
71
+ state.tracked.set(key, stop);
72
+ }
73
+ }
74
+ trackLoadedInstruction(workspaceId, root, path) {
75
+ const state = this.states.get(workspaceId);
76
+ if (!state)
77
+ return;
78
+ const key = canonicalWatchKey(path);
79
+ state.subscriptions.set(key, {
80
+ kind: "instruction-loaded",
81
+ displayPath: displayWorkspacePath(path, root),
82
+ exposeSkillBody: false,
83
+ });
84
+ if (state.tracked.has(key))
85
+ return;
86
+ const subscriberId = `${this.id}:${workspaceId}:${key}`;
87
+ state.tracked.set(key, subscribeSharedWatch(path, subscriberId, (oldContent, newContent) => {
88
+ this.recordChange(workspaceId, key, oldContent, newContent);
89
+ }));
90
+ }
91
+ markSkillActivated(workspaceId, skillPath) {
92
+ const state = this.states.get(workspaceId);
93
+ if (!state)
94
+ return;
95
+ const subscription = state.subscriptions.get(canonicalWatchKey(skillPath));
96
+ if (subscription?.kind === "skill")
97
+ subscription.exposeSkillBody = true;
98
+ }
99
+ acknowledge(workspaceId, conversationScopeId) {
100
+ if (!conversationScopeId)
101
+ return;
102
+ const state = this.states.get(workspaceId);
103
+ if (!state)
104
+ return;
105
+ state.deliveredRevisionByScope.set(conversationScopeId, state.revision);
106
+ this.pruneDeliveredHistory(state);
107
+ }
108
+ claim(workspaceId, conversationScopeId) {
109
+ if (!conversationScopeId)
110
+ return undefined;
111
+ const state = this.states.get(workspaceId);
112
+ if (!state)
113
+ return undefined;
114
+ // File watcher delivery is advisory and can lag behind a tool call. Refresh
115
+ // tracked files synchronously before deciding whether this conversation has
116
+ // new context so rapid consecutive writes cannot be skipped.
117
+ for (const key of state.tracked.keys())
118
+ synchronizeSharedWatch(key);
119
+ const deliveredRevision = state.deliveredRevisionByScope.get(conversationScopeId) ?? state.revision;
120
+ const changes = state.changes.filter((change) => change.revision > deliveredRevision);
121
+ state.deliveredRevisionByScope.set(conversationScopeId, state.revision);
122
+ if (changes.length === 0)
123
+ return undefined;
124
+ const grouped = coalesceChanges(changes);
125
+ const sections = [];
126
+ const coveredComponents = new Set();
127
+ for (const change of grouped) {
128
+ const formatted = formatResourceChange(change);
129
+ if (!formatted)
130
+ continue;
131
+ sections.push(formatted.text);
132
+ for (const component of formatted.coveredComponents)
133
+ coveredComponents.add(component);
134
+ }
135
+ this.pruneDeliveredHistory(state);
136
+ if (sections.length === 0)
137
+ return undefined;
138
+ const header = "Workspace context changed after this Workspace was opened. Apply only these deltas; unchanged instructions and Skill metadata remain active:";
139
+ const joined = [header, ...sections].join("\n\n");
140
+ const text = joined.length <= MAX_DELIVERY_CHARACTERS
141
+ ? joined
142
+ : `${joined.slice(0, MAX_DELIVERY_CHARACTERS)}\n\n[Additional Workspace context deltas were truncated; reopen with context=\"auto\" to refresh metadata.]`;
143
+ return {
144
+ text,
145
+ coveredComponents: [...coveredComponents],
146
+ revision: state.revision,
147
+ };
148
+ }
149
+ isCurrentForScope(workspaceId, conversationScopeId) {
150
+ if (!conversationScopeId)
151
+ return false;
152
+ const state = this.states.get(workspaceId);
153
+ if (!state)
154
+ return false;
155
+ return state.deliveredRevisionByScope.get(conversationScopeId) === state.revision;
156
+ }
157
+ forgetWorkspace(workspaceId) {
158
+ const state = this.states.get(workspaceId);
159
+ if (!state)
160
+ return;
161
+ for (const stop of state.tracked.values())
162
+ stop();
163
+ this.states.delete(workspaceId);
164
+ }
165
+ pruneWorkspaces(activeWorkspaceIds) {
166
+ const active = new Set(activeWorkspaceIds);
167
+ for (const workspaceId of this.states.keys()) {
168
+ if (!active.has(workspaceId))
169
+ this.forgetWorkspace(workspaceId);
170
+ }
171
+ }
172
+ get watchedPhysicalFiles() {
173
+ return sharedWatches.size;
174
+ }
175
+ addDesired(desired, path, subscription) {
176
+ const key = canonicalWatchKey(path);
177
+ const current = desired.get(key);
178
+ if (current?.subscription.kind === "instruction-loaded")
179
+ return;
180
+ desired.set(key, { path, subscription });
181
+ }
182
+ recordChange(workspaceId, key, oldContent, newContent) {
183
+ if (oldContent === newContent)
184
+ return;
185
+ const state = this.states.get(workspaceId);
186
+ const subscription = state?.subscriptions.get(key);
187
+ if (!state || !subscription)
188
+ return;
189
+ state.revision += 1;
190
+ state.changes.push({
191
+ ...subscription,
192
+ revision: state.revision,
193
+ watchKey: key,
194
+ oldContent,
195
+ newContent,
196
+ });
197
+ if (state.changes.length > MAX_HISTORY) {
198
+ state.changes.splice(0, state.changes.length - MAX_HISTORY);
199
+ }
200
+ }
201
+ pruneDeliveredHistory(state) {
202
+ if (state.changes.length === 0 || state.deliveredRevisionByScope.size === 0)
203
+ return;
204
+ const floor = Math.min(...state.deliveredRevisionByScope.values());
205
+ while (state.changes[0] && state.changes[0].revision <= floor)
206
+ state.changes.shift();
207
+ }
208
+ }
209
+ function subscribeSharedWatch(inputPath, subscriberId, subscriber) {
210
+ const key = canonicalWatchKey(inputPath);
211
+ let entry = sharedWatches.get(key);
212
+ if (!entry) {
213
+ const watchPath = resolve(inputPath);
214
+ const listener = () => synchronizeSharedWatch(key);
215
+ entry = {
216
+ key,
217
+ watchPath,
218
+ content: readOptionalFile(watchPath),
219
+ subscribers: new Map(),
220
+ listener,
221
+ };
222
+ sharedWatches.set(key, entry);
223
+ watchFile(watchPath, { interval: WATCH_INTERVAL_MS, persistent: false }, listener);
224
+ }
225
+ else {
226
+ synchronizeSharedWatch(key);
227
+ }
228
+ entry.subscribers.set(subscriberId, subscriber);
229
+ return () => {
230
+ const current = sharedWatches.get(key);
231
+ if (!current)
232
+ return;
233
+ current.subscribers.delete(subscriberId);
234
+ if (current.subscribers.size > 0)
235
+ return;
236
+ unwatchFile(current.watchPath, current.listener);
237
+ sharedWatches.delete(key);
238
+ };
239
+ }
240
+ function synchronizeSharedWatch(key) {
241
+ const entry = sharedWatches.get(key);
242
+ if (!entry)
243
+ return;
244
+ const next = readOptionalFile(entry.watchPath);
245
+ if (next === entry.content)
246
+ return;
247
+ const previous = entry.content;
248
+ entry.content = next;
249
+ for (const subscriber of entry.subscribers.values())
250
+ subscriber(previous, next);
251
+ }
252
+ function canonicalWatchKey(path) {
253
+ const absolute = resolve(path);
254
+ try {
255
+ return realpathSync(absolute);
256
+ }
257
+ catch {
258
+ return absolute;
259
+ }
260
+ }
261
+ function readOptionalFile(path) {
262
+ try {
263
+ return readFileSync(path, "utf8");
264
+ }
265
+ catch (error) {
266
+ if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR")) {
267
+ return undefined;
268
+ }
269
+ throw error;
270
+ }
271
+ }
272
+ function displayWorkspacePath(path, workspaceRoot) {
273
+ const relationship = relative(workspaceRoot, path);
274
+ if (!relationship || relationship === ".." || relationship.startsWith(`..${sep}`))
275
+ return resolve(path);
276
+ return relationship.split(sep).join("/");
277
+ }
278
+ function coalesceChanges(changes) {
279
+ const grouped = new Map();
280
+ for (const change of changes) {
281
+ const existing = grouped.get(change.watchKey);
282
+ if (!existing) {
283
+ grouped.set(change.watchKey, { ...change });
284
+ continue;
285
+ }
286
+ grouped.set(change.watchKey, {
287
+ ...change,
288
+ oldContent: existing.oldContent,
289
+ });
290
+ }
291
+ return [...grouped.values()].sort((left, right) => left.revision - right.revision);
292
+ }
293
+ function formatResourceChange(change) {
294
+ if (change.oldContent === change.newContent)
295
+ return undefined;
296
+ if (change.kind === "instruction-available") {
297
+ return {
298
+ text: `Nested Workspace instruction changed: ${change.displayPath}. Its body remains lazy; read it before working under that directory.`,
299
+ coveredComponents: [],
300
+ };
301
+ }
302
+ if (change.kind === "instruction-loaded") {
303
+ return {
304
+ text: [
305
+ `Workspace instruction delta: ${change.displayPath}`,
306
+ boundedPatch(change.displayPath, change.oldContent, change.newContent),
307
+ ].join("\n"),
308
+ coveredComponents: ["agentsFiles"],
309
+ };
310
+ }
311
+ if (change.exposeSkillBody) {
312
+ return {
313
+ text: [
314
+ `Active Skill delta: ${change.displayPath}`,
315
+ boundedPatch(change.displayPath, change.oldContent, change.newContent),
316
+ ].join("\n"),
317
+ coveredComponents: skillMetadataChanged(change) ? ["skills"] : [],
318
+ };
319
+ }
320
+ const metadata = formatSkillMetadataDelta(change);
321
+ return {
322
+ text: metadata ?? `Skill content changed: ${change.displayPath}. The Skill body was not injected because it was not active when it changed; reload it before next use.`,
323
+ coveredComponents: metadata ? ["skills"] : [],
324
+ };
325
+ }
326
+ function skillMetadataChanged(change) {
327
+ try {
328
+ const oldSummary = change.oldContent === undefined
329
+ ? undefined
330
+ : skillSummaryFromContent(change.oldContent, change.watchKey);
331
+ const newSummary = change.newContent === undefined
332
+ ? undefined
333
+ : skillSummaryFromContent(change.newContent, change.watchKey);
334
+ return JSON.stringify(oldSummary) !== JSON.stringify(newSummary);
335
+ }
336
+ catch {
337
+ return true;
338
+ }
339
+ }
340
+ function formatSkillMetadataDelta(change) {
341
+ try {
342
+ const oldSummary = change.oldContent === undefined
343
+ ? undefined
344
+ : skillSummaryFromContent(change.oldContent, change.watchKey);
345
+ const newSummary = change.newContent === undefined
346
+ ? undefined
347
+ : skillSummaryFromContent(change.newContent, change.watchKey);
348
+ if (JSON.stringify(oldSummary) === JSON.stringify(newSummary))
349
+ return undefined;
350
+ if (!oldSummary) {
351
+ return `Skill metadata added: ${change.displayPath}\n+ name: ${newSummary?.name ?? change.skillName ?? "unknown"}\n+ description: ${newSummary?.description ?? ""}\n+ disable-model-invocation: ${newSummary?.disableModelInvocation === true}`;
352
+ }
353
+ if (!newSummary)
354
+ return `Skill removed: ${change.displayPath}`;
355
+ const lines = [`Skill metadata delta: ${change.displayPath}`];
356
+ for (const field of ["name", "description", "disableModelInvocation"]) {
357
+ if (oldSummary[field] === newSummary[field])
358
+ continue;
359
+ const label = field === "disableModelInvocation" ? "disable-model-invocation" : field;
360
+ lines.push(`- ${label}: ${String(oldSummary[field])}`);
361
+ lines.push(`+ ${label}: ${String(newSummary[field])}`);
362
+ }
363
+ return lines.join("\n");
364
+ }
365
+ catch {
366
+ return `Skill metadata became unreadable or changed format: ${change.displayPath}. Reload it before next use.`;
367
+ }
368
+ }
369
+ function boundedPatch(path, oldContent, newContent) {
370
+ const patch = createTwoFilesPatch(path, path, oldContent ?? "", newContent ?? "", oldContent === undefined ? "missing" : "before", newContent === undefined ? "missing" : "after", { context: 1 });
371
+ const lines = patch.split("\n");
372
+ const hunk = lines.slice(Math.max(0, lines.findIndex((line) => line.startsWith("@@"))));
373
+ const compact = hunk.join("\n").trimEnd();
374
+ if (compact.length <= MAX_PATCH_CHARACTERS)
375
+ return compact;
376
+ return `${compact.slice(0, MAX_PATCH_CHARACTERS)}\n[delta truncated]`;
377
+ }