wiki-viewer 1.4.3 → 1.5.1

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 (218) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/app-path-routes-manifest.json +5 -0
  3. package/.next/standalone/.next/build-manifest.json +3 -3
  4. package/.next/standalone/.next/prerender-manifest.json +3 -3
  5. package/.next/standalone/.next/routes-manifest.json +34 -0
  6. package/.next/standalone/.next/server/app/_global-error.html +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  12. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  14. package/.next/standalone/.next/server/app/_not-found.rsc +1 -1
  15. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  19. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  20. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  21. package/.next/standalone/.next/server/app/api/agent/activity/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/agent/events/[...path]/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/agent/files/[...path]/route.js +8 -7
  24. package/.next/standalone/.next/server/app/api/agent/files/[...path]/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route/app-paths-manifest.json +3 -0
  26. package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route/build-manifest.json +9 -0
  27. package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route/server-reference-manifest.json +4 -0
  28. package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route.js +17 -0
  29. package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route.js.map +5 -0
  30. package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route.js.nft.json +1 -0
  31. package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route_client-reference-manifest.js +3 -0
  32. package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route/app-paths-manifest.json +3 -0
  33. package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route/build-manifest.json +9 -0
  34. package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route/server-reference-manifest.json +4 -0
  35. package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route.js +15 -0
  36. package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route.js.map +5 -0
  37. package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route.js.nft.json +1 -0
  38. package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route_client-reference-manifest.js +3 -0
  39. package/.next/standalone/.next/server/app/api/agent/fs/move/route/app-paths-manifest.json +3 -0
  40. package/.next/standalone/.next/server/app/api/agent/fs/move/route/build-manifest.json +9 -0
  41. package/.next/standalone/.next/server/app/api/agent/fs/move/route/server-reference-manifest.json +4 -0
  42. package/.next/standalone/.next/server/app/api/agent/fs/move/route.js +15 -0
  43. package/.next/standalone/.next/server/app/api/agent/fs/move/route.js.map +5 -0
  44. package/.next/standalone/.next/server/app/api/agent/fs/move/route.js.nft.json +1 -0
  45. package/.next/standalone/.next/server/app/api/agent/fs/move/route_client-reference-manifest.js +3 -0
  46. package/.next/standalone/.next/server/app/api/agent/fs/search/route/app-paths-manifest.json +3 -0
  47. package/.next/standalone/.next/server/app/api/agent/fs/search/route/build-manifest.json +9 -0
  48. package/.next/standalone/.next/server/app/api/agent/fs/search/route/server-reference-manifest.json +4 -0
  49. package/.next/standalone/.next/server/app/api/agent/fs/search/route.js +15 -0
  50. package/.next/standalone/.next/server/app/api/agent/fs/search/route.js.map +5 -0
  51. package/.next/standalone/.next/server/app/api/agent/fs/search/route.js.nft.json +1 -0
  52. package/.next/standalone/.next/server/app/api/agent/fs/search/route_client-reference-manifest.js +3 -0
  53. package/.next/standalone/.next/server/app/api/agent/internal/span/route.js.nft.json +1 -1
  54. package/.next/standalone/.next/server/app/api/agent/settings/route.js +1 -1
  55. package/.next/standalone/.next/server/app/api/agent/settings/route.js.nft.json +1 -1
  56. package/.next/standalone/.next/server/app/api/agent/sidecar/[...path]/route.js.nft.json +1 -1
  57. package/.next/standalone/.next/server/app/api/app-proxy/[...path]/route.js +1 -1
  58. package/.next/standalone/.next/server/app/api/app-proxy/[...path]/route.js.nft.json +1 -1
  59. package/.next/standalone/.next/server/app/api/assets/[...path]/route.js.nft.json +1 -1
  60. package/.next/standalone/.next/server/app/api/system/browse/route.js.nft.json +1 -1
  61. package/.next/standalone/.next/server/app/api/system/clear-root/route.js.nft.json +1 -1
  62. package/.next/standalone/.next/server/app/api/system/reveal/route.js.nft.json +1 -1
  63. package/.next/standalone/.next/server/app/api/system/root-status/route.js.nft.json +1 -1
  64. package/.next/standalone/.next/server/app/api/system/set-root/route.js.nft.json +1 -1
  65. package/.next/standalone/.next/server/app/api/upload/[...path]/route.js.nft.json +1 -1
  66. package/.next/standalone/.next/server/app/api/wiki/app/route.js.nft.json +1 -1
  67. package/.next/standalone/.next/server/app/api/wiki/content/route.js +2 -2
  68. package/.next/standalone/.next/server/app/api/wiki/content/route.js.nft.json +1 -1
  69. package/.next/standalone/.next/server/app/api/wiki/file/route.js.nft.json +1 -1
  70. package/.next/standalone/.next/server/app/api/wiki/folder/route.js.nft.json +1 -1
  71. package/.next/standalone/.next/server/app/api/wiki/move/route.js +3 -3
  72. package/.next/standalone/.next/server/app/api/wiki/move/route.js.nft.json +1 -1
  73. package/.next/standalone/.next/server/app/api/wiki/new-file/route.js.nft.json +1 -1
  74. package/.next/standalone/.next/server/app/api/wiki/page/route.js.nft.json +1 -1
  75. package/.next/standalone/.next/server/app/api/wiki/presence/route/app-paths-manifest.json +3 -0
  76. package/.next/standalone/.next/server/app/api/wiki/presence/route/build-manifest.json +9 -0
  77. package/.next/standalone/.next/server/app/api/wiki/presence/route/server-reference-manifest.json +4 -0
  78. package/.next/standalone/.next/server/app/api/wiki/presence/route.js +14 -0
  79. package/.next/standalone/.next/server/app/api/wiki/presence/route.js.map +5 -0
  80. package/.next/standalone/.next/server/app/api/wiki/presence/route.js.nft.json +1 -0
  81. package/.next/standalone/.next/server/app/api/wiki/presence/route_client-reference-manifest.js +3 -0
  82. package/.next/standalone/.next/server/app/api/wiki/route.js.nft.json +1 -1
  83. package/.next/standalone/.next/server/app/api/wiki/slugs/route.js.nft.json +1 -1
  84. package/.next/standalone/.next/server/app/api/wiki/upload/route.js.nft.json +1 -1
  85. package/.next/standalone/.next/server/app/api/wiki/watch/route.js.nft.json +1 -1
  86. package/.next/standalone/.next/server/app/index.html +1 -1
  87. package/.next/standalone/.next/server/app/index.rsc +2 -2
  88. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  89. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +2 -2
  90. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  91. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  92. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  93. package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
  94. package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  95. package/.next/standalone/.next/server/app-paths-manifest.json +5 -0
  96. package/.next/standalone/.next/server/chunks/0p91_next_dist_esm_build_templates_app-route_0d2jbo5.js +3 -0
  97. package/.next/standalone/.next/server/chunks/0p91_next_dist_esm_build_templates_app-route_0ytral2.js +3 -0
  98. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0-ao6a_._.js +1 -1
  99. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0269d2e._.js +1 -1
  100. package/.next/standalone/.next/server/chunks/[root-of-the-server]__05f6x0i._.js +3 -0
  101. package/.next/standalone/.next/server/chunks/[root-of-the-server]__06-dtd0._.js +3 -0
  102. package/.next/standalone/.next/server/chunks/[root-of-the-server]__06sek6_._.js +1 -1
  103. package/.next/standalone/.next/server/chunks/[root-of-the-server]__09s9qkd._.js +1 -1
  104. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d75a5q._.js +1 -1
  105. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d_da6q._.js +3 -0
  106. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e7si9p._.js +1 -1
  107. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0hhhvm7._.js +1 -1
  108. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0kk26wc._.js +3 -0
  109. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0kur4ro._.js +15 -0
  110. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0p12-6~._.js +1 -1
  111. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qc2~6l._.js +15 -0
  112. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0s~i040._.js +3 -0
  113. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0w47wil._.js +1 -1
  114. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xq124z._.js +3 -0
  115. package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0l..bxz._.js → [root-of-the-server]__0xz-_0v._.js} +2 -2
  116. package/.next/standalone/.next/server/chunks/[root-of-the-server]__10w2q~o._.js +1 -1
  117. package/.next/standalone/.next/server/chunks/_079xuln._.js +3 -0
  118. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_agent_fs_file_[___path]_route_actions_0gb11t~.js +3 -0
  119. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_agent_fs_ls_[[___path]]_route_actions_0wfg4nl.js +3 -0
  120. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_agent_fs_move_route_actions_0z34mqq.js +3 -0
  121. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_agent_fs_search_route_actions_0ut_ug..js +3 -0
  122. package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_wiki_presence_route_actions_0x9d7f4.js +3 -0
  123. package/.next/standalone/.next/server/chunks/ssr/{0~w0_katex_dist_katex_mjs_055boxx._.js → 0~w0_katex_dist_katex_mjs_00650~e._.js} +1 -1
  124. package/.next/standalone/.next/server/chunks/ssr/12y~_mermaid_dist_chunks_mermaid_core_diagram-OG6HWLK6_mjs_0t5sus6._.js +1 -1
  125. package/.next/standalone/.next/server/chunks/ssr/12y~_mermaid_dist_chunks_mermaid_core_sankeyDiagram-5OEKKPKP_mjs_11ifrnu._.js +1 -1
  126. package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__134_q7l._.js → [root-of-the-server]__12ahxbl._.js} +2 -2
  127. package/.next/standalone/.next/server/chunks/ssr/_0pqaawz._.js +3 -3
  128. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0rc1vh5._.js → node_modules__pnpm_02gx8km._.js} +1 -1
  129. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0t4unpg._.js → node_modules__pnpm_02mew~s._.js} +2 -2
  130. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_12-ni1n._.js → node_modules__pnpm_03ij5.g._.js} +2 -2
  131. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_12~a4c_._.js → node_modules__pnpm_03v21p-._.js} +2 -2
  132. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0f~_mm3._.js → node_modules__pnpm_05qsply._.js} +2 -2
  133. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0alwr5e._.js → node_modules__pnpm_06pgebo._.js} +2 -2
  134. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_02zxe7o._.js → node_modules__pnpm_08ibh0w._.js} +2 -2
  135. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0hr027i._.js → node_modules__pnpm_099cv.b._.js} +1 -1
  136. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0znncy9._.js → node_modules__pnpm_0cwpt9p._.js} +2 -2
  137. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0-pisfi._.js → node_modules__pnpm_0f9fpy5._.js} +1 -1
  138. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0zhbnni._.js → node_modules__pnpm_0f_uzgy._.js} +1 -1
  139. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0-w-d-.._.js → node_modules__pnpm_0fixtms._.js} +1 -1
  140. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_03z0vkh._.js → node_modules__pnpm_0gofu~q._.js} +1 -1
  141. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_10i6lh6._.js → node_modules__pnpm_0iojvye._.js} +2 -2
  142. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0-hfev1._.js → node_modules__pnpm_0li2vzs._.js} +1 -1
  143. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_13kfy_z._.js → node_modules__pnpm_0m5-azh._.js} +1 -1
  144. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_071f6vl._.js → node_modules__pnpm_0n1kfcp._.js} +1 -1
  145. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_07uh2kc._.js → node_modules__pnpm_0rebktf._.js} +1 -1
  146. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0okw1r.._.js → node_modules__pnpm_0tkiej3._.js} +2 -2
  147. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0xuz.8f._.js → node_modules__pnpm_0v_nwsz._.js} +1 -1
  148. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0zb6573._.js → node_modules__pnpm_0wffzo0._.js} +1 -1
  149. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0d7jxsf._.js → node_modules__pnpm_0wouz19._.js} +1 -1
  150. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0egenev._.js → node_modules__pnpm_0xxwmv2._.js} +1 -1
  151. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0j9bq9s._.js → node_modules__pnpm_0zzzsrj._.js} +1 -1
  152. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0irsd.b._.js → node_modules__pnpm_0~.6dgb._.js} +1 -1
  153. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0erwv_m._.js → node_modules__pnpm_0~2~c30._.js} +1 -1
  154. package/.next/standalone/.next/server/chunks/ssr/{node_modules__pnpm_0y9k6h~._.js → node_modules__pnpm_10413dq._.js} +2 -2
  155. package/.next/standalone/.next/server/functions-config-manifest.json +5 -0
  156. package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
  157. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  158. package/.next/standalone/.next/server/pages/404.html +1 -1
  159. package/.next/standalone/.next/server/pages/500.html +1 -1
  160. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  161. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  162. package/.next/standalone/.next/static/chunks/{11s9zwr7edrs7.js → 17~r3.8awh3qg.js} +12 -12
  163. package/.next/standalone/README.md +79 -16
  164. package/.next/standalone/agents/bootstrap-prompt.md +12 -1
  165. package/.next/standalone/agents/wiki-viewer-skill/SKILL.md +199 -6
  166. package/.next/standalone/docs/file-vs-collab-authority.md +124 -0
  167. package/.next/standalone/package.json +1 -1
  168. package/.next/standalone/packages/wiki-viewer-mcp/README.md +194 -0
  169. package/.next/standalone/packages/wiki-viewer-mcp/dist/http-client.js +220 -0
  170. package/.next/standalone/packages/wiki-viewer-mcp/dist/index.js +472 -0
  171. package/.next/standalone/packages/wiki-viewer-mcp/dist/register.js +94 -0
  172. package/.next/standalone/packages/wiki-viewer-mcp/dist/state-cache.js +26 -0
  173. package/.next/standalone/packages/wiki-viewer-mcp/package-lock.json +1716 -0
  174. package/.next/standalone/packages/wiki-viewer-mcp/package.json +41 -0
  175. package/.next/standalone/packages/wiki-viewer-mcp/src/http-client.ts +344 -0
  176. package/.next/standalone/packages/wiki-viewer-mcp/src/index.ts +575 -0
  177. package/.next/standalone/packages/wiki-viewer-mcp/src/register.ts +144 -0
  178. package/.next/standalone/packages/wiki-viewer-mcp/src/state-cache.ts +45 -0
  179. package/.next/standalone/packages/wiki-viewer-mcp/tsconfig.json +19 -0
  180. package/.next/standalone/pnpm-lock.yaml +9 -9
  181. package/.next/standalone/src/app/api/agent/files/[...path]/route.ts +11 -1
  182. package/.next/standalone/src/app/api/agent/fs/file/[...path]/route.ts +344 -0
  183. package/.next/standalone/src/app/api/agent/fs/ls/[[...path]]/route.ts +147 -0
  184. package/.next/standalone/src/app/api/agent/fs/move/route.ts +125 -0
  185. package/.next/standalone/src/app/api/agent/fs/search/route.ts +236 -0
  186. package/.next/standalone/src/app/api/agent/register/route.ts +3 -3
  187. package/.next/standalone/src/app/api/agents/install/route.ts +44 -2
  188. package/.next/standalone/src/app/api/system/auth-settings/route.ts +1 -0
  189. package/.next/standalone/src/app/api/wiki/move/route.ts +10 -1
  190. package/.next/standalone/src/app/api/wiki/presence/route.ts +55 -0
  191. package/.next/standalone/src/components/ai-panel/ai-panel.tsx +98 -103
  192. package/.next/standalone/src/components/ai-panel/token-section.tsx +8 -7
  193. package/.next/standalone/src/components/auth-settings-sheet.tsx +81 -22
  194. package/.next/standalone/src/components/editor/editor.tsx +38 -0
  195. package/.next/standalone/src/lib/proof/audit.ts +79 -0
  196. package/.next/standalone/src/lib/proof/auth.ts +2 -2
  197. package/.next/standalone/src/lib/proof/collab-state.ts +79 -0
  198. package/.next/standalone/src/lib/proof/lease.ts +104 -0
  199. package/.next/standalone/src/lib/proof/ops-applier.ts +74 -25
  200. package/.next/standalone/src/lib/proof/raw-fs.ts +198 -0
  201. package/.next/standalone/src/lib/proof/registry.ts +1 -1
  202. package/.next/standalone/src/lib/proof/sidecar.ts +38 -1
  203. package/.next/standalone/src/lib/proof/types.ts +13 -1
  204. package/.next/standalone/src/tests/proof/agent-fs.test.ts +824 -0
  205. package/.next/standalone/src/tests/proof/agents-install.test.ts +53 -0
  206. package/.next/standalone/src/tests/proof/collab-state.test.ts +499 -0
  207. package/.next/standalone/src/tests/proof/reconcile-sidecar.test.ts +195 -0
  208. package/.next/standalone/src/tests/proof/sidecar-lifecycle.test.ts +177 -0
  209. package/.next/standalone/tsconfig.tsbuildinfo +1 -1
  210. package/README.md +79 -16
  211. package/package.json +1 -1
  212. package/.next/standalone/.next/server/chunks/[root-of-the-server]__09~~-nd._.js +0 -3
  213. package/.next/standalone/.next/server/chunks/[root-of-the-server]__0w87kiz._.js +0 -3
  214. package/.next/standalone/.next/server/chunks/[root-of-the-server]__12ssbhv._.js +0 -3
  215. package/.next/standalone/.next/server/chunks/node_modules__pnpm_0g0fhfk._.js +0 -3
  216. /package/.next/standalone/.next/static/{kzOuSAQArtvD7uiAJ6ZLh → 3G_3qm2a9nOX7oi5k0mPV}/_buildManifest.js +0 -0
  217. /package/.next/standalone/.next/static/{kzOuSAQArtvD7uiAJ6ZLh → 3G_3qm2a9nOX7oi5k0mPV}/_clientMiddlewareManifest.js +0 -0
  218. /package/.next/standalone/.next/static/{kzOuSAQArtvD7uiAJ6ZLh → 3G_3qm2a9nOX7oi5k0mPV}/_ssgManifest.js +0 -0
@@ -0,0 +1,144 @@
1
+ /**
2
+ * TOFU registration flow for wiki-viewer.
3
+ *
4
+ * Pure function `register()` is testable with a mock fetch.
5
+ * The CLI wrapper lives in index.ts.
6
+ *
7
+ * Flow:
8
+ * POST /api/agent/register → {registrationId, pollUrl, status:"pending"}
9
+ * human approves in AI Panel
10
+ * GET <pollUrl> → 202 pending | 200 approved | 410 denied | 404 expired
11
+ * returns {token, agentId}
12
+ */
13
+
14
+ export interface RegisterScope {
15
+ paths: string[];
16
+ ops: Array<"read" | "mutate" | "delete">;
17
+ }
18
+
19
+ export interface RegisterOptions {
20
+ baseUrl: string;
21
+ id: string; // must match ^ai:[a-z][a-z0-9-]{0,30}$
22
+ displayName: string;
23
+ scope: RegisterScope;
24
+ /** Override fetch for testing */
25
+ fetch?: typeof globalThis.fetch;
26
+ /** How long to wait between polls (ms, default 3000) */
27
+ pollIntervalMs?: number;
28
+ /** Total time to wait before giving up (ms, default 300_000) */
29
+ timeoutMs?: number;
30
+ /** Called each time we get a "pending" poll response */
31
+ onPending?: (registrationId: string, attempt: number) => void;
32
+ }
33
+
34
+ export interface RegisterResult {
35
+ token: string;
36
+ agentId: string;
37
+ }
38
+
39
+ export async function register(opts: RegisterOptions): Promise<RegisterResult> {
40
+ const _fetch = opts.fetch ?? globalThis.fetch;
41
+ const pollIntervalMs = opts.pollIntervalMs ?? 3_000;
42
+ const timeoutMs = opts.timeoutMs ?? 300_000;
43
+ const base = opts.baseUrl.replace(/\/$/, "");
44
+
45
+ // ── Step 1: POST registration ──────────────────────────────────────────────
46
+ const postRes = await _fetch(`${base}/api/agent/register`, {
47
+ method: "POST",
48
+ headers: { "Content-Type": "application/json" },
49
+ body: JSON.stringify({
50
+ id: opts.id,
51
+ displayName: opts.displayName,
52
+ scope: opts.scope,
53
+ }),
54
+ });
55
+
56
+ if (!postRes.ok) {
57
+ const body = await postRes.text().catch(() => "");
58
+ throw new Error(`Registration request failed (${postRes.status}): ${body}`);
59
+ }
60
+
61
+ const { registrationId, pollUrl } = await postRes.json() as {
62
+ registrationId: string;
63
+ pollUrl: string;
64
+ status: string;
65
+ };
66
+
67
+ // Resolve pollUrl — may be absolute or relative
68
+ const resolvedPollUrl = pollUrl.startsWith("http")
69
+ ? pollUrl
70
+ : `${base}${pollUrl.startsWith("/") ? "" : "/"}${pollUrl}`;
71
+
72
+ // ── Step 2: Poll until approved / denied / timeout ─────────────────────────
73
+ const deadline = Date.now() + timeoutMs;
74
+ let attempt = 0;
75
+
76
+ while (Date.now() < deadline) {
77
+ await sleep(pollIntervalMs);
78
+ attempt++;
79
+
80
+ const pollRes = await _fetch(resolvedPollUrl);
81
+
82
+ if (pollRes.status === 200) {
83
+ const body = await pollRes.json() as {
84
+ status: string;
85
+ agentId?: string;
86
+ token?: string;
87
+ };
88
+ if (body.status === "approved" && body.agentId && body.token) {
89
+ return { token: body.token, agentId: body.agentId };
90
+ }
91
+ if (body.status === "denied") {
92
+ throw new RegistrationDeniedError("Registration denied by the operator.");
93
+ }
94
+ } else if (pollRes.status === 202) {
95
+ // Still pending — fire callback and loop
96
+ opts.onPending?.(registrationId, attempt);
97
+ } else if (pollRes.status === 410) {
98
+ const body = await pollRes.json().catch(() => ({}) as Record<string, unknown>) as { status?: string };
99
+ throw new RegistrationDeniedError(
100
+ `Registration ${body.status ?? "denied/consumed"} — it can no longer be approved.`,
101
+ );
102
+ } else if (pollRes.status === 404) {
103
+ throw new RegistrationExpiredError(
104
+ "Registration expired (404). Try registering again.",
105
+ );
106
+ } else {
107
+ throw new Error(`Unexpected poll status ${pollRes.status}.`);
108
+ }
109
+ }
110
+
111
+ throw new RegistrationTimeoutError(
112
+ `Timed out after ${Math.round(timeoutMs / 1000)}s. ` +
113
+ `The request may still be pending in the wiki-viewer AI Panel (id: ${registrationId}).`,
114
+ );
115
+ }
116
+
117
+ // ── Typed errors ──────────────────────────────────────────────────────────────
118
+
119
+ export class RegistrationDeniedError extends Error {
120
+ constructor(message: string) {
121
+ super(message);
122
+ this.name = "RegistrationDeniedError";
123
+ }
124
+ }
125
+
126
+ export class RegistrationExpiredError extends Error {
127
+ constructor(message: string) {
128
+ super(message);
129
+ this.name = "RegistrationExpiredError";
130
+ }
131
+ }
132
+
133
+ export class RegistrationTimeoutError extends Error {
134
+ constructor(message: string) {
135
+ super(message);
136
+ this.name = "RegistrationTimeoutError";
137
+ }
138
+ }
139
+
140
+ // ── Helpers ───────────────────────────────────────────────────────────────────
141
+
142
+ function sleep(ms: number): Promise<void> {
143
+ return new Promise((resolve) => setTimeout(resolve, ms));
144
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Per-path cache for last-known file metadata from GET responses.
3
+ * Drives If-Match and collab-state checks on subsequent writes.
4
+ */
5
+
6
+ export type CollabState = "active" | "tracked" | "untracked" | "not-markdown";
7
+
8
+ export interface PathState {
9
+ /** sha256 from ETag header (without quotes) */
10
+ sha256: string;
11
+ collabState: CollabState;
12
+ /** X-Collab-Revision value, null for non-markdown */
13
+ collabRevision: number | null;
14
+ /** X-Collab-Snapshot URL, null for non-markdown */
15
+ collabSnapshot: string | null;
16
+ /** When this cache entry was last updated (ms) */
17
+ fetchedAt: number;
18
+ }
19
+
20
+ const cache = new Map<string, PathState>();
21
+
22
+ export function set(path: string, state: PathState): void {
23
+ cache.set(normalisePath(path), state);
24
+ }
25
+
26
+ export function get(path: string): PathState | undefined {
27
+ return cache.get(normalisePath(path));
28
+ }
29
+
30
+ export function del(path: string): void {
31
+ cache.delete(normalisePath(path));
32
+ }
33
+
34
+ export function rename(from: string, to: string): void {
35
+ const s = cache.get(normalisePath(from));
36
+ if (s) {
37
+ cache.delete(normalisePath(from));
38
+ cache.set(normalisePath(to), s);
39
+ }
40
+ }
41
+
42
+ /** Normalise leading slash away so 'foo/bar' and '/foo/bar' are the same key */
43
+ function normalisePath(p: string): string {
44
+ return p.replace(/^\/+/, "");
45
+ }
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "outDir": "dist",
7
+ "rootDir": "src",
8
+ "declaration": true,
9
+ "declarationMap": true,
10
+ "sourceMap": true,
11
+ "strict": true,
12
+ "esModuleInterop": true,
13
+ "skipLibCheck": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "resolveJsonModule": true
16
+ },
17
+ "include": ["src"],
18
+ "exclude": ["src/__tests__", "dist", "node_modules"]
19
+ }
@@ -23,9 +23,18 @@ importers:
23
23
  kysely:
24
24
  specifier: 0.28.5
25
25
  version: 0.28.5
26
+ next:
27
+ specifier: 16.2.4
28
+ version: 16.2.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
26
29
  proper-lockfile:
27
30
  specifier: ^4.1.2
28
31
  version: 4.1.2
32
+ react:
33
+ specifier: 19.2.5
34
+ version: 19.2.5
35
+ react-dom:
36
+ specifier: 19.2.5
37
+ version: 19.2.5(react@19.2.5)
29
38
  rehype-parse:
30
39
  specifier: ^9.0.1
31
40
  version: 9.0.1
@@ -210,9 +219,6 @@ importers:
210
219
  mermaid:
211
220
  specifier: ^11.14.0
212
221
  version: 11.15.0
213
- next:
214
- specifier: 16.2.4
215
- version: 16.2.4(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
216
222
  next-themes:
217
223
  specifier: ^0.4.6
218
224
  version: 0.4.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
@@ -225,12 +231,6 @@ importers:
225
231
  pptx-preview:
226
232
  specifier: ^1.0.7
227
233
  version: 1.0.7
228
- react:
229
- specifier: 19.2.5
230
- version: 19.2.5
231
- react-dom:
232
- specifier: 19.2.5
233
- version: 19.2.5(react@19.2.5)
234
234
  react-markdown:
235
235
  specifier: ^10.1.0
236
236
  version: 10.1.0(@types/react@19.2.14)(react@19.2.5)
@@ -21,6 +21,7 @@ import { idempotency } from "@/lib/proof/idempotency";
21
21
  import { getRootDir, safeRootPath } from "@/lib/root-dir";
22
22
  import type { Op } from "@/lib/proof/types";
23
23
  import { checkAndConsume } from "@/lib/proof/rate-limit";
24
+ import { computeCollabState } from "@/lib/proof/collab-state";
24
25
 
25
26
  export const runtime = "nodejs";
26
27
 
@@ -67,7 +68,16 @@ export async function GET(
67
68
  return NextResponse.json({ error: "NOT_FOUND", message: "File not found" }, { status: 404 });
68
69
  }
69
70
 
70
- return NextResponse.json(snapshot);
71
+ const collab = await computeCollabState(rootDir, rel);
72
+ const collabHeaders: Record<string, string> = {
73
+ "X-Collab-State": collab.state,
74
+ "X-Collab-Revision": String(collab.revision),
75
+ };
76
+ if (collab.snapshotUrl) {
77
+ collabHeaders["X-Collab-Snapshot"] = collab.snapshotUrl;
78
+ }
79
+
80
+ return NextResponse.json(snapshot, { headers: collabHeaders });
71
81
  }
72
82
 
73
83
  export async function POST(
@@ -0,0 +1,344 @@
1
+ /**
2
+ * Tier-1 Raw FS — file read/write/delete.
3
+ *
4
+ * GET /api/agent/fs/file/<path> Read raw bytes (Range supported).
5
+ * PUT /api/agent/fs/file/<path> Atomic whole-file write.
6
+ * DELETE /api/agent/fs/file/<path> Delete file (+ sidecar for .md).
7
+ */
8
+ export const runtime = "nodejs";
9
+
10
+ import { readFile, stat, unlink, rm } from "node:fs/promises";
11
+ import path from "node:path";
12
+ import { NextResponse } from "next/server";
13
+ import { checkAuth, enforceScope } from "@/lib/proof/auth";
14
+ import { getRootDir, safeRootPath } from "@/lib/root-dir";
15
+ import { withFileMutex } from "@/lib/proof/mutex";
16
+ import { readSidecar, emptySidecar, deleteSidecar } from "@/lib/proof/sidecar";
17
+ import { reconcileSidecar } from "@/lib/proof/ops-applier";
18
+ import { writeAuditRow } from "@/lib/proof/audit";
19
+ import {
20
+ safeAbsPath,
21
+ sha256ofBuf,
22
+ extractShaHex,
23
+ mimeByExt,
24
+ isMarkdown,
25
+ atomicWrite,
26
+ ensureParentDir,
27
+ } from "@/lib/proof/raw-fs";
28
+ import { computeCollabState } from "@/lib/proof/collab-state";
29
+
30
+ // ── Helpers ──────────────────────────────────────────────────────────────────
31
+
32
+ function rel(segments: string[]): string {
33
+ return segments.join("/");
34
+ }
35
+
36
+ function errJson(code: string, message: string, status: number): NextResponse {
37
+ return NextResponse.json({ error: code, message }, { status });
38
+ }
39
+
40
+ // ── GET ──────────────────────────────────────────────────────────────────────
41
+
42
+ export async function GET(
43
+ req: Request,
44
+ { params }: { params: Promise<{ path: string[] }> },
45
+ ): Promise<NextResponse> {
46
+ const auth = await checkAuth(req);
47
+ if (!auth.ok) return errJson("UNAUTHORIZED", auth.message ?? "Unauthorized", 401);
48
+
49
+ const { path: segments } = await params;
50
+ const relPath = rel(segments);
51
+
52
+ const basic = safeRootPath(relPath);
53
+ if (!basic) return errJson("INVALID_PATH", "Path traversal rejected", 400);
54
+
55
+ const absPath = await safeAbsPath(relPath);
56
+ if (!absPath) return errJson("INVALID_PATH", "Path rejected (symlink escape or denied)", 400);
57
+
58
+ const scope = enforceScope(auth.agent, { filePath: relPath, op: "read" });
59
+ if (!scope.ok) return errJson(scope.code, scope.message, 403);
60
+
61
+ let data: Buffer;
62
+ let fileStat: Awaited<ReturnType<typeof stat>>;
63
+ try {
64
+ [data, fileStat] = await Promise.all([readFile(absPath), stat(absPath)]);
65
+ } catch (e) {
66
+ if ((e as NodeJS.ErrnoException).code === "ENOENT") {
67
+ return errJson("NOT_FOUND", "File not found", 404);
68
+ }
69
+ throw e;
70
+ }
71
+
72
+ if (fileStat.isDirectory()) {
73
+ return errJson("IS_DIRECTORY", "Path is a directory; use /api/agent/fs/ls/", 400);
74
+ }
75
+
76
+ const sha = sha256ofBuf(data);
77
+ const baseHeaders: Record<string, string> = {
78
+ "ETag": `"${sha}"`,
79
+ "X-File-Size": String(fileStat.size),
80
+ "X-File-Mtime": fileStat.mtime.toISOString(),
81
+ "Content-Type": mimeByExt(relPath),
82
+ "Accept-Ranges": "bytes",
83
+ };
84
+
85
+ // X-Collab-* headers (§3.5 mode signal)
86
+ const rootDir = getRootDir();
87
+ const collab = await computeCollabState(rootDir, relPath);
88
+ if (collab.state !== "not-markdown") {
89
+ baseHeaders["X-Collab-State"] = collab.state;
90
+ baseHeaders["X-Collab-Revision"] = String(collab.revision);
91
+ baseHeaders["X-Collab-Snapshot"] = collab.snapshotUrl!;
92
+ } else {
93
+ baseHeaders["X-Collab-State"] = collab.state;
94
+ }
95
+
96
+ const rangeHeader = req.headers.get("range");
97
+ if (rangeHeader) {
98
+ const match = /^bytes=(\d+)-(\d*)$/.exec(rangeHeader);
99
+ if (!match) {
100
+ return new NextResponse("Invalid Range header", { status: 416 });
101
+ }
102
+ const start = parseInt(match[1]!, 10);
103
+ const endRaw = match[2] ? parseInt(match[2], 10) : data.length - 1;
104
+ if (start > endRaw || start >= data.length) {
105
+ return new NextResponse(null, {
106
+ status: 416,
107
+ headers: { "Content-Range": `bytes */${data.length}` },
108
+ });
109
+ }
110
+ const end = Math.min(endRaw, data.length - 1);
111
+ const slice = data.slice(start, end + 1);
112
+ return new NextResponse(slice as unknown as BodyInit, {
113
+ status: 206,
114
+ headers: {
115
+ ...baseHeaders,
116
+ "Content-Range": `bytes ${start}-${end}/${data.length}`,
117
+ "Content-Length": String(slice.length),
118
+ },
119
+ });
120
+ }
121
+
122
+ return new NextResponse(data as unknown as BodyInit, {
123
+ status: 200,
124
+ headers: { ...baseHeaders, "Content-Length": String(data.length) },
125
+ });
126
+ }
127
+
128
+ // ── PUT ──────────────────────────────────────────────────────────────────────
129
+
130
+ export async function PUT(
131
+ req: Request,
132
+ { params }: { params: Promise<{ path: string[] }> },
133
+ ): Promise<NextResponse> {
134
+ const auth = await checkAuth(req);
135
+ if (!auth.ok) return errJson("UNAUTHORIZED", auth.message ?? "Unauthorized", 401);
136
+
137
+ const { path: segments } = await params;
138
+ const relPath = rel(segments);
139
+ const url = new URL(req.url);
140
+ const mkdirs = url.searchParams.get("mkdirs") === "true";
141
+ const force = url.searchParams.get("force") === "true";
142
+
143
+ const basic = safeRootPath(relPath);
144
+ if (!basic) return errJson("INVALID_PATH", "Path traversal rejected", 400);
145
+
146
+ const absPath = await safeAbsPath(relPath);
147
+ if (!absPath) return errJson("INVALID_PATH", "Path rejected (symlink escape or denied)", 400);
148
+
149
+ const scope = enforceScope(auth.agent, { filePath: relPath, op: "mutate" });
150
+ if (!scope.ok) return errJson(scope.code, scope.message, 403);
151
+
152
+ const ifMatch = req.headers.get("if-match");
153
+ const bodyBuf = Buffer.from(await req.arrayBuffer());
154
+ const newSha = sha256ofBuf(bodyBuf);
155
+
156
+ // Check existing file
157
+ let existingSha: string | undefined;
158
+ let existed = false;
159
+ try {
160
+ const existing = await readFile(absPath);
161
+ existingSha = sha256ofBuf(existing);
162
+ existed = true;
163
+ } catch (e) {
164
+ if ((e as NodeJS.ErrnoException).code !== "ENOENT") throw e;
165
+ }
166
+
167
+ if (existed) {
168
+ // Overwrite: If-Match required by default (unless ?force=true)
169
+ if (!force && !ifMatch) {
170
+ return errJson(
171
+ "PRECONDITION_REQUIRED",
172
+ "If-Match header required for overwrites (use ?force=true to bypass with audit)",
173
+ 412,
174
+ );
175
+ }
176
+ if (ifMatch) {
177
+ const provided = extractShaHex(ifMatch);
178
+ const current = extractShaHex(existingSha!);
179
+ if (provided !== current) {
180
+ return errJson("PRECONDITION_FAILED", "If-Match sha256 mismatch", 412);
181
+ }
182
+ }
183
+ } else {
184
+ // Create: ensure parent dir exists
185
+ const parentOk = await ensureParentDir(absPath, mkdirs);
186
+ if (!parentOk) {
187
+ return errJson(
188
+ "PARENT_NOT_FOUND",
189
+ "Parent directory does not exist (use ?mkdirs=true to create)",
190
+ 400,
191
+ );
192
+ }
193
+ }
194
+
195
+ const rootDir = getRootDir();
196
+ // R6: for .md, re-check collab state atomically inside write mutex
197
+ const ifCollabMatch = req.headers.get("if-collab-match");
198
+
199
+ if (isMarkdown(relPath)) {
200
+ // R1: acquire shared mutex; R2: reconcile eagerly inside it
201
+ return await withFileMutex(relPath, async () => {
202
+ // R6: atomic collab-state check (closes TOCTOU race)
203
+ const { state, revision, snapshotUrl } = await computeCollabState(rootDir, relPath);
204
+ if (state === "active") {
205
+ const matchVal = ifCollabMatch?.trim();
206
+ if (!force && (!matchVal || matchVal !== String(revision))) {
207
+ return NextResponse.json(
208
+ {
209
+ error: "COLLAB_ACTIVE",
210
+ message:
211
+ "File is in active collaborative session. Use block-ops (Tier-2) or supply a matching If-Collab-Match header. Use ?force=true to override (audited).",
212
+ snapshotUrl,
213
+ revision,
214
+ },
215
+ { status: 409 },
216
+ );
217
+ }
218
+ }
219
+
220
+ await atomicWrite(absPath, bodyBuf);
221
+
222
+ const content = bodyBuf.toString("utf-8");
223
+ const sidecar = (await readSidecar(rootDir, relPath)) ?? emptySidecar(relPath);
224
+ await reconcileSidecar({
225
+ rootDir,
226
+ mdPath: relPath,
227
+ content,
228
+ sidecar,
229
+ by: auth.agent.id,
230
+ eventType: "file.rawWritten",
231
+ fingerprint: newSha,
232
+ });
233
+
234
+ writeAuditRow({
235
+ agentId: auth.agent.id,
236
+ op: "put",
237
+ path: relPath,
238
+ oldSha: existingSha,
239
+ newSha,
240
+ forced: force,
241
+ });
242
+
243
+ const st = await stat(absPath);
244
+ return NextResponse.json({
245
+ path: relPath,
246
+ sha256: newSha,
247
+ size: st.size,
248
+ mtime: st.mtime.toISOString(),
249
+ created: !existed,
250
+ });
251
+ });
252
+ }
253
+
254
+ // Non-.md: atomic write + audit (no mutex, no sidecar)
255
+ await atomicWrite(absPath, bodyBuf);
256
+ writeAuditRow({
257
+ agentId: auth.agent.id,
258
+ op: "put",
259
+ path: relPath,
260
+ oldSha: existingSha,
261
+ newSha,
262
+ forced: force,
263
+ });
264
+ const st = await stat(absPath);
265
+ return NextResponse.json({
266
+ path: relPath,
267
+ sha256: newSha,
268
+ size: st.size,
269
+ mtime: st.mtime.toISOString(),
270
+ created: !existed,
271
+ });
272
+ }
273
+
274
+ // ── DELETE ───────────────────────────────────────────────────────────────────
275
+
276
+ export async function DELETE(
277
+ req: Request,
278
+ { params }: { params: Promise<{ path: string[] }> },
279
+ ): Promise<NextResponse> {
280
+ const auth = await checkAuth(req);
281
+ if (!auth.ok) return errJson("UNAUTHORIZED", auth.message ?? "Unauthorized", 401);
282
+
283
+ const { path: segments } = await params;
284
+ const relPath = rel(segments);
285
+ const url = new URL(req.url);
286
+ const recursive = url.searchParams.get("recursive") === "true";
287
+
288
+ const basic = safeRootPath(relPath);
289
+ if (!basic) return errJson("INVALID_PATH", "Path traversal rejected", 400);
290
+
291
+ const absPath = await safeAbsPath(relPath);
292
+ if (!absPath) return errJson("INVALID_PATH", "Path rejected (symlink escape or denied)", 400);
293
+
294
+ // Requires "delete" scope op
295
+ const scope = enforceScope(auth.agent, { filePath: relPath, op: "delete" });
296
+ if (!scope.ok) return errJson(scope.code, scope.message, 403);
297
+
298
+ const ifMatch = req.headers.get("if-match");
299
+
300
+ let fileStat: Awaited<ReturnType<typeof stat>>;
301
+ try {
302
+ fileStat = await stat(absPath);
303
+ } catch (e) {
304
+ if ((e as NodeJS.ErrnoException).code === "ENOENT") {
305
+ return errJson("NOT_FOUND", "File not found", 404);
306
+ }
307
+ throw e;
308
+ }
309
+
310
+ if (fileStat.isDirectory()) {
311
+ if (!recursive) {
312
+ return errJson("IS_DIRECTORY", "Use ?recursive=true to delete a directory", 400);
313
+ }
314
+ // Directory delete — no single sha, skip If-Match; require recursive flag as confirmation
315
+ await rm(absPath, { recursive: true, force: true });
316
+ writeAuditRow({ agentId: auth.agent.id, op: "delete", path: relPath, forced: false });
317
+ return NextResponse.json({ deleted: relPath });
318
+ }
319
+
320
+ // File: If-Match required as confirmation
321
+ if (!ifMatch) {
322
+ return errJson("PRECONDITION_REQUIRED", "If-Match header required for delete", 412);
323
+ }
324
+
325
+ const existing = await readFile(absPath);
326
+ const existingSha = sha256ofBuf(existing);
327
+ if (extractShaHex(ifMatch) !== extractShaHex(existingSha)) {
328
+ return errJson("PRECONDITION_FAILED", "If-Match sha256 mismatch", 412);
329
+ }
330
+
331
+ const rootDir = getRootDir();
332
+
333
+ if (isMarkdown(relPath)) {
334
+ await withFileMutex(relPath, async () => {
335
+ await unlink(absPath);
336
+ await deleteSidecar(rootDir, relPath);
337
+ });
338
+ } else {
339
+ await unlink(absPath);
340
+ }
341
+
342
+ writeAuditRow({ agentId: auth.agent.id, op: "delete", path: relPath, oldSha: existingSha, forced: false });
343
+ return NextResponse.json({ deleted: relPath });
344
+ }