@aphexcms/cms-core 9.7.0 → 9.8.0

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 (326) hide show
  1. package/dist/agent-chat-state.svelte.d.ts +38 -0
  2. package/dist/agent-chat-state.svelte.d.ts.map +1 -0
  3. package/dist/agent-chat-state.svelte.js +86 -0
  4. package/dist/ai/content-workspace-tools.d.ts +3 -0
  5. package/dist/ai/content-workspace-tools.d.ts.map +1 -0
  6. package/dist/ai/content-workspace-tools.js +50 -0
  7. package/dist/ai/default-system-prompt.d.ts +2 -0
  8. package/dist/ai/default-system-prompt.d.ts.map +1 -0
  9. package/dist/ai/default-system-prompt.js +15 -0
  10. package/dist/ai/index.d.ts +2 -0
  11. package/dist/ai/index.d.ts.map +1 -0
  12. package/dist/ai/index.js +4 -0
  13. package/dist/ai/interfaces/ai-provider.d.ts +67 -0
  14. package/dist/ai/interfaces/ai-provider.d.ts.map +1 -0
  15. package/dist/ai/interfaces/ai-provider.js +16 -0
  16. package/dist/ai/run-agent-turn.d.ts +29 -0
  17. package/dist/ai/run-agent-turn.d.ts.map +1 -0
  18. package/dist/ai/run-agent-turn.js +166 -0
  19. package/dist/api/agent-chat.d.ts +27 -0
  20. package/dist/api/agent-chat.d.ts.map +1 -0
  21. package/dist/api/agent-chat.js +64 -0
  22. package/dist/api/documents.d.ts +8 -3
  23. package/dist/api/documents.d.ts.map +1 -1
  24. package/dist/api/documents.js +6 -6
  25. package/dist/api/schemas/agent-chat.d.ts +45 -0
  26. package/dist/api/schemas/agent-chat.d.ts.map +1 -0
  27. package/dist/api/schemas/agent-chat.js +35 -0
  28. package/dist/api/schemas/agent-operations.d.ts +13 -0
  29. package/dist/api/schemas/agent-operations.d.ts.map +1 -0
  30. package/dist/api/schemas/agent-operations.js +15 -0
  31. package/dist/api/schemas/documents.d.ts +22 -0
  32. package/dist/api/schemas/documents.d.ts.map +1 -1
  33. package/dist/api/schemas/documents.js +20 -2
  34. package/dist/chunk-recovery.d.ts +33 -0
  35. package/dist/chunk-recovery.d.ts.map +1 -0
  36. package/dist/chunk-recovery.js +131 -0
  37. package/dist/client/index.d.ts +1 -0
  38. package/dist/client/index.d.ts.map +1 -1
  39. package/dist/client/index.js +1 -0
  40. package/dist/client/ui.d.ts +1 -0
  41. package/dist/client/ui.d.ts.map +1 -1
  42. package/dist/client/ui.js +1 -0
  43. package/dist/components/AdminApp.svelte +37 -3
  44. package/dist/components/AdminApp.svelte.d.ts.map +1 -1
  45. package/dist/components/admin/ActivityView.svelte +251 -42
  46. package/dist/components/admin/ActivityView.svelte.d.ts.map +1 -1
  47. package/dist/components/admin/AgentChat.svelte +977 -0
  48. package/dist/components/admin/AgentChat.svelte.d.ts +17 -0
  49. package/dist/components/admin/AgentChat.svelte.d.ts.map +1 -0
  50. package/dist/components/admin/DocumentEditor.svelte +273 -60
  51. package/dist/components/admin/DocumentEditor.svelte.d.ts +6 -0
  52. package/dist/components/admin/DocumentEditor.svelte.d.ts.map +1 -1
  53. package/dist/components/admin/ObjectModal.svelte +4 -2
  54. package/dist/components/admin/fields/ArrayField.svelte +4 -1
  55. package/dist/components/admin/fields/ReferenceField.svelte +12 -1
  56. package/dist/components/admin/fields/ReferenceField.svelte.d.ts.map +1 -1
  57. package/dist/components/index.d.ts +1 -0
  58. package/dist/components/index.d.ts.map +1 -1
  59. package/dist/components/index.js +1 -0
  60. package/dist/components/layout/Sidebar.svelte +26 -2
  61. package/dist/components/layout/Sidebar.svelte.d.ts +1 -0
  62. package/dist/components/layout/Sidebar.svelte.d.ts.map +1 -1
  63. package/dist/db/interfaces/agent-change-sets.d.ts +17 -0
  64. package/dist/db/interfaces/agent-change-sets.d.ts.map +1 -0
  65. package/dist/db/interfaces/agent-change-sets.js +1 -0
  66. package/dist/db/interfaces/document.d.ts +25 -3
  67. package/dist/db/interfaces/document.d.ts.map +1 -1
  68. package/dist/db/interfaces/document.js +19 -1
  69. package/dist/db/interfaces/index.d.ts +4 -1
  70. package/dist/db/interfaces/index.d.ts.map +1 -1
  71. package/dist/db/interfaces/index.js +1 -1
  72. package/dist/document-refresh.svelte.d.ts +2 -0
  73. package/dist/document-refresh.svelte.d.ts.map +1 -1
  74. package/dist/document-refresh.svelte.js +15 -0
  75. package/dist/document-workspace-registry.svelte.d.ts +15 -0
  76. package/dist/document-workspace-registry.svelte.d.ts.map +1 -0
  77. package/dist/document-workspace-registry.svelte.js +30 -0
  78. package/dist/field-validation/utils.d.ts.map +1 -1
  79. package/dist/field-validation/utils.js +211 -29
  80. package/dist/hooks.d.ts +2 -0
  81. package/dist/hooks.d.ts.map +1 -1
  82. package/dist/hooks.js +2 -0
  83. package/dist/lib/agent-chat-state.svelte.d.ts +38 -0
  84. package/dist/lib/agent-chat-state.svelte.d.ts.map +1 -0
  85. package/dist/lib/agent-chat-state.svelte.js +87 -0
  86. package/dist/lib/agent-chat-state.svelte.js.map +1 -0
  87. package/dist/lib/ai/content-workspace-tools.d.ts +3 -0
  88. package/dist/lib/ai/content-workspace-tools.d.ts.map +1 -0
  89. package/dist/lib/ai/content-workspace-tools.js +51 -0
  90. package/dist/lib/ai/content-workspace-tools.js.map +1 -0
  91. package/dist/lib/ai/default-system-prompt.d.ts +2 -0
  92. package/dist/lib/ai/default-system-prompt.d.ts.map +1 -0
  93. package/dist/lib/ai/default-system-prompt.js +16 -0
  94. package/dist/lib/ai/default-system-prompt.js.map +1 -0
  95. package/dist/lib/ai/index.d.ts +2 -0
  96. package/dist/lib/ai/index.d.ts.map +1 -0
  97. package/dist/lib/ai/index.js +5 -0
  98. package/dist/lib/ai/index.js.map +1 -0
  99. package/dist/lib/ai/interfaces/ai-provider.d.ts +67 -0
  100. package/dist/lib/ai/interfaces/ai-provider.d.ts.map +1 -0
  101. package/dist/lib/ai/interfaces/ai-provider.js +17 -0
  102. package/dist/lib/ai/interfaces/ai-provider.js.map +1 -0
  103. package/dist/lib/ai/run-agent-turn.d.ts +29 -0
  104. package/dist/lib/ai/run-agent-turn.d.ts.map +1 -0
  105. package/dist/lib/ai/run-agent-turn.js +167 -0
  106. package/dist/lib/ai/run-agent-turn.js.map +1 -0
  107. package/dist/lib/api/agent-chat.d.ts +27 -0
  108. package/dist/lib/api/agent-chat.d.ts.map +1 -0
  109. package/dist/lib/api/agent-chat.js +65 -0
  110. package/dist/lib/api/agent-chat.js.map +1 -0
  111. package/dist/lib/api/documents.d.ts +8 -3
  112. package/dist/lib/api/documents.d.ts.map +1 -1
  113. package/dist/lib/api/documents.js +6 -6
  114. package/dist/lib/api/documents.js.map +1 -1
  115. package/dist/lib/api/schemas/agent-chat.d.ts +45 -0
  116. package/dist/lib/api/schemas/agent-chat.d.ts.map +1 -0
  117. package/dist/lib/api/schemas/agent-chat.js +36 -0
  118. package/dist/lib/api/schemas/agent-chat.js.map +1 -0
  119. package/dist/lib/api/schemas/agent-operations.d.ts +13 -0
  120. package/dist/lib/api/schemas/agent-operations.d.ts.map +1 -0
  121. package/dist/lib/api/schemas/agent-operations.js +16 -0
  122. package/dist/lib/api/schemas/agent-operations.js.map +1 -0
  123. package/dist/lib/api/schemas/documents.d.ts +22 -0
  124. package/dist/lib/api/schemas/documents.d.ts.map +1 -1
  125. package/dist/lib/api/schemas/documents.js +20 -2
  126. package/dist/lib/api/schemas/documents.js.map +1 -1
  127. package/dist/lib/chunk-recovery.d.ts +33 -0
  128. package/dist/lib/chunk-recovery.d.ts.map +1 -0
  129. package/dist/lib/chunk-recovery.js +132 -0
  130. package/dist/lib/chunk-recovery.js.map +1 -0
  131. package/dist/lib/client/index.d.ts +1 -0
  132. package/dist/lib/client/index.d.ts.map +1 -1
  133. package/dist/lib/client/index.js +1 -0
  134. package/dist/lib/client/index.js.map +1 -1
  135. package/dist/lib/client/ui.d.ts +1 -0
  136. package/dist/lib/client/ui.d.ts.map +1 -1
  137. package/dist/lib/client/ui.js +1 -0
  138. package/dist/lib/client/ui.js.map +1 -1
  139. package/dist/lib/components/index.d.ts +1 -0
  140. package/dist/lib/components/index.d.ts.map +1 -1
  141. package/dist/lib/components/index.js +1 -0
  142. package/dist/lib/components/index.js.map +1 -1
  143. package/dist/lib/db/interfaces/agent-change-sets.d.ts +17 -0
  144. package/dist/lib/db/interfaces/agent-change-sets.d.ts.map +1 -0
  145. package/dist/lib/db/interfaces/agent-change-sets.js +2 -0
  146. package/dist/lib/db/interfaces/agent-change-sets.js.map +1 -0
  147. package/dist/lib/db/interfaces/document.d.ts +25 -3
  148. package/dist/lib/db/interfaces/document.d.ts.map +1 -1
  149. package/dist/lib/db/interfaces/document.js +19 -1
  150. package/dist/lib/db/interfaces/document.js.map +1 -1
  151. package/dist/lib/db/interfaces/index.d.ts +4 -1
  152. package/dist/lib/db/interfaces/index.d.ts.map +1 -1
  153. package/dist/lib/db/interfaces/index.js +1 -1
  154. package/dist/lib/db/interfaces/index.js.map +1 -1
  155. package/dist/lib/document-refresh.svelte.d.ts +2 -0
  156. package/dist/lib/document-refresh.svelte.d.ts.map +1 -1
  157. package/dist/lib/document-refresh.svelte.js +15 -0
  158. package/dist/lib/document-refresh.svelte.js.map +1 -1
  159. package/dist/lib/document-workspace-registry.svelte.d.ts +15 -0
  160. package/dist/lib/document-workspace-registry.svelte.d.ts.map +1 -0
  161. package/dist/lib/document-workspace-registry.svelte.js +31 -0
  162. package/dist/lib/document-workspace-registry.svelte.js.map +1 -0
  163. package/dist/lib/field-validation/utils.d.ts.map +1 -1
  164. package/dist/lib/field-validation/utils.js +211 -29
  165. package/dist/lib/field-validation/utils.js.map +1 -1
  166. package/dist/lib/hooks.d.ts +2 -0
  167. package/dist/lib/hooks.d.ts.map +1 -1
  168. package/dist/lib/hooks.js +2 -0
  169. package/dist/lib/hooks.js.map +1 -1
  170. package/dist/lib/local-api/collection-api.d.ts +7 -2
  171. package/dist/lib/local-api/collection-api.d.ts.map +1 -1
  172. package/dist/lib/local-api/collection-api.js +17 -10
  173. package/dist/lib/local-api/collection-api.js.map +1 -1
  174. package/dist/lib/mcp/tools.d.ts +39 -4
  175. package/dist/lib/mcp/tools.d.ts.map +1 -1
  176. package/dist/lib/mcp/tools.js +546 -362
  177. package/dist/lib/mcp/tools.js.map +1 -1
  178. package/dist/lib/plugins/resolver.d.ts +8 -1
  179. package/dist/lib/plugins/resolver.d.ts.map +1 -1
  180. package/dist/lib/plugins/resolver.js +14 -1
  181. package/dist/lib/plugins/resolver.js.map +1 -1
  182. package/dist/lib/plugins/types.d.ts +23 -2
  183. package/dist/lib/plugins/types.d.ts.map +1 -1
  184. package/dist/lib/plugins/types.js.map +1 -1
  185. package/dist/lib/preview/stega.d.ts +2 -2
  186. package/dist/lib/preview/stega.d.ts.map +1 -1
  187. package/dist/lib/preview/stega.js +52 -29
  188. package/dist/lib/preview/stega.js.map +1 -1
  189. package/dist/lib/schema-utils/validator.d.ts.map +1 -1
  190. package/dist/lib/schema-utils/validator.js +5 -2
  191. package/dist/lib/schema-utils/validator.js.map +1 -1
  192. package/dist/lib/server/api/index.d.ts.map +1 -1
  193. package/dist/lib/server/api/index.js +8 -0
  194. package/dist/lib/server/api/index.js.map +1 -1
  195. package/dist/lib/server/api/routes/agent-change-sets.d.ts +4 -0
  196. package/dist/lib/server/api/routes/agent-change-sets.d.ts.map +1 -0
  197. package/dist/lib/server/api/routes/agent-change-sets.js +132 -0
  198. package/dist/lib/server/api/routes/agent-change-sets.js.map +1 -0
  199. package/dist/lib/server/api/routes/agent-chat.d.ts +14 -0
  200. package/dist/lib/server/api/routes/agent-chat.d.ts.map +1 -0
  201. package/dist/lib/server/api/routes/agent-chat.js +224 -0
  202. package/dist/lib/server/api/routes/agent-chat.js.map +1 -0
  203. package/dist/lib/server/api/routes/document-versions.d.ts.map +1 -1
  204. package/dist/lib/server/api/routes/document-versions.js +16 -2
  205. package/dist/lib/server/api/routes/document-versions.js.map +1 -1
  206. package/dist/lib/server/api/routes/documents-by-id.d.ts.map +1 -1
  207. package/dist/lib/server/api/routes/documents-by-id.js +11 -1
  208. package/dist/lib/server/api/routes/documents-by-id.js.map +1 -1
  209. package/dist/lib/server/api/routes/documents-publish.d.ts.map +1 -1
  210. package/dist/lib/server/api/routes/documents-publish.js +36 -3
  211. package/dist/lib/server/api/routes/documents-publish.js.map +1 -1
  212. package/dist/lib/server/api/routes/jobs.d.ts.map +1 -1
  213. package/dist/lib/server/api/routes/jobs.js +4 -2
  214. package/dist/lib/server/api/routes/jobs.js.map +1 -1
  215. package/dist/lib/server/api/routes/resolve-created-by.d.ts +13 -0
  216. package/dist/lib/server/api/routes/resolve-created-by.d.ts.map +1 -0
  217. package/dist/lib/server/api/routes/resolve-created-by.js +31 -0
  218. package/dist/lib/server/api/routes/resolve-created-by.js.map +1 -0
  219. package/dist/lib/server/index.d.ts +1 -0
  220. package/dist/lib/server/index.d.ts.map +1 -1
  221. package/dist/lib/server/index.js +1 -0
  222. package/dist/lib/server/index.js.map +1 -1
  223. package/dist/lib/services/version-service.d.ts +4 -4
  224. package/dist/lib/services/version-service.d.ts.map +1 -1
  225. package/dist/lib/services/version-service.js +15 -11
  226. package/dist/lib/services/version-service.js.map +1 -1
  227. package/dist/lib/type-gen.d.ts.map +1 -1
  228. package/dist/lib/type-gen.js +11 -0
  229. package/dist/lib/type-gen.js.map +1 -1
  230. package/dist/lib/types/agent-change-sets.d.ts +70 -0
  231. package/dist/lib/types/agent-change-sets.d.ts.map +1 -0
  232. package/dist/lib/types/agent-change-sets.js +7 -0
  233. package/dist/lib/types/agent-change-sets.js.map +1 -0
  234. package/dist/lib/types/agent-stream.d.ts +60 -0
  235. package/dist/lib/types/agent-stream.d.ts.map +1 -0
  236. package/dist/lib/types/agent-stream.js +12 -0
  237. package/dist/lib/types/agent-stream.js.map +1 -0
  238. package/dist/lib/types/agent-tools.d.ts +65 -0
  239. package/dist/lib/types/agent-tools.d.ts.map +1 -0
  240. package/dist/lib/types/agent-tools.js +2 -0
  241. package/dist/lib/types/agent-tools.js.map +1 -0
  242. package/dist/lib/types/config.d.ts +19 -0
  243. package/dist/lib/types/config.d.ts.map +1 -1
  244. package/dist/lib/types/document-workspace.d.ts +53 -0
  245. package/dist/lib/types/document-workspace.d.ts.map +1 -0
  246. package/dist/lib/types/document-workspace.js +15 -0
  247. package/dist/lib/types/document-workspace.js.map +1 -0
  248. package/dist/lib/types/document.d.ts +5 -0
  249. package/dist/lib/types/document.d.ts.map +1 -1
  250. package/dist/lib/types/index.d.ts +2 -0
  251. package/dist/lib/types/index.d.ts.map +1 -1
  252. package/dist/lib/types/index.js +2 -0
  253. package/dist/lib/types/index.js.map +1 -1
  254. package/dist/lib/types/schemas.js +1 -1
  255. package/dist/lib/types/schemas.js.map +1 -1
  256. package/dist/lib/utils/fetch-remote-file.d.ts +11 -0
  257. package/dist/lib/utils/fetch-remote-file.d.ts.map +1 -0
  258. package/dist/lib/utils/fetch-remote-file.js +169 -0
  259. package/dist/lib/utils/fetch-remote-file.js.map +1 -0
  260. package/dist/local-api/collection-api.d.ts +7 -2
  261. package/dist/local-api/collection-api.d.ts.map +1 -1
  262. package/dist/local-api/collection-api.js +17 -10
  263. package/dist/mcp/tools.d.ts +39 -4
  264. package/dist/mcp/tools.d.ts.map +1 -1
  265. package/dist/mcp/tools.js +546 -362
  266. package/dist/plugins/resolver.d.ts +8 -1
  267. package/dist/plugins/resolver.d.ts.map +1 -1
  268. package/dist/plugins/resolver.js +14 -1
  269. package/dist/plugins/types.d.ts +23 -2
  270. package/dist/plugins/types.d.ts.map +1 -1
  271. package/dist/preview/stega.d.ts +2 -2
  272. package/dist/preview/stega.d.ts.map +1 -1
  273. package/dist/preview/stega.js +52 -29
  274. package/dist/schema-utils/validator.d.ts.map +1 -1
  275. package/dist/schema-utils/validator.js +5 -2
  276. package/dist/server/api/index.d.ts.map +1 -1
  277. package/dist/server/api/index.js +8 -0
  278. package/dist/server/api/routes/agent-change-sets.d.ts +4 -0
  279. package/dist/server/api/routes/agent-change-sets.d.ts.map +1 -0
  280. package/dist/server/api/routes/agent-change-sets.js +131 -0
  281. package/dist/server/api/routes/agent-chat.d.ts +14 -0
  282. package/dist/server/api/routes/agent-chat.d.ts.map +1 -0
  283. package/dist/server/api/routes/agent-chat.js +223 -0
  284. package/dist/server/api/routes/document-versions.d.ts.map +1 -1
  285. package/dist/server/api/routes/document-versions.js +16 -2
  286. package/dist/server/api/routes/documents-by-id.d.ts.map +1 -1
  287. package/dist/server/api/routes/documents-by-id.js +11 -1
  288. package/dist/server/api/routes/documents-publish.d.ts.map +1 -1
  289. package/dist/server/api/routes/documents-publish.js +36 -3
  290. package/dist/server/api/routes/jobs.d.ts.map +1 -1
  291. package/dist/server/api/routes/jobs.js +4 -2
  292. package/dist/server/api/routes/resolve-created-by.d.ts +13 -0
  293. package/dist/server/api/routes/resolve-created-by.d.ts.map +1 -0
  294. package/dist/server/api/routes/resolve-created-by.js +30 -0
  295. package/dist/server/index.d.ts +1 -0
  296. package/dist/server/index.d.ts.map +1 -1
  297. package/dist/server/index.js +1 -0
  298. package/dist/services/version-service.d.ts +4 -4
  299. package/dist/services/version-service.d.ts.map +1 -1
  300. package/dist/services/version-service.js +15 -11
  301. package/dist/type-gen.d.ts.map +1 -1
  302. package/dist/type-gen.js +11 -0
  303. package/dist/types/agent-change-sets.d.ts +70 -0
  304. package/dist/types/agent-change-sets.d.ts.map +1 -0
  305. package/dist/types/agent-change-sets.js +6 -0
  306. package/dist/types/agent-stream.d.ts +60 -0
  307. package/dist/types/agent-stream.d.ts.map +1 -0
  308. package/dist/types/agent-stream.js +11 -0
  309. package/dist/types/agent-tools.d.ts +65 -0
  310. package/dist/types/agent-tools.d.ts.map +1 -0
  311. package/dist/types/agent-tools.js +1 -0
  312. package/dist/types/config.d.ts +19 -0
  313. package/dist/types/config.d.ts.map +1 -1
  314. package/dist/types/document-workspace.d.ts +53 -0
  315. package/dist/types/document-workspace.d.ts.map +1 -0
  316. package/dist/types/document-workspace.js +14 -0
  317. package/dist/types/document.d.ts +5 -0
  318. package/dist/types/document.d.ts.map +1 -1
  319. package/dist/types/index.d.ts +2 -0
  320. package/dist/types/index.d.ts.map +1 -1
  321. package/dist/types/index.js +2 -0
  322. package/dist/types/schemas.js +1 -1
  323. package/dist/utils/fetch-remote-file.d.ts +11 -0
  324. package/dist/utils/fetch-remote-file.d.ts.map +1 -0
  325. package/dist/utils/fetch-remote-file.js +168 -0
  326. package/package.json +5 -2
@@ -1,25 +1,26 @@
1
- // MCP tool registry — transport-agnostic.
1
+ // Content agent tools — transport-agnostic.
2
2
  //
3
- // Each tool is a plain { name, description, inputSchema, handler } descriptor.
4
- // The MCP route (routes/mcp.ts) registers these into an MCP SDK server, but the
5
- // registry itself has no MCP dependency a future in-admin AI panel can consume
6
- // the exact same tools. Input schemas are zod (matching the project's
7
- // zod-as-contract convention). Tools are built per-request and bound to an
8
- // authenticated LocalAPIContext, so every operation rides the caller's RBAC +
9
- // RLS scope.
3
+ // Each tool is an `AgentToolDefinition` (name/description/capabilities/zod schema the
4
+ // Milestone 2 contract, see references/content-copilot-phase-1-plan.md) paired with an
5
+ // `execute` function. Unlike the old per-request `McpTool[]` registry, these are defined
6
+ // ONCE, statically `execute` receives `{ aphexCMS, context }` as a call-time argument
7
+ // (matching how `aphex/event/consumer`/`aphex/job/handler` plugin parts already work) rather
8
+ // than a closure bound at registration time, so the exact same tool can be invoked by MCP,
9
+ // a future in-admin agent panel, or a plugin-contributed executor, through one execution
10
+ // service. `buildContentTools()` is a thin per-request adapter that wraps these into the
11
+ // MCP SDK's expected shape — the MCP route (routes/mcp.ts) is the only remaining MCP-specific
12
+ // code in this file.
10
13
  import { z } from 'zod';
11
14
  import { VALID_FIELD_TYPES, RESERVED_FIELDS, validateSchemaReferences } from '../schema-utils/validator.js';
12
15
  import { validateDocumentData } from '../field-validation/utils.js';
13
16
  import { validateFile } from '../utils/mime-detect.js';
17
+ import { fetchRemoteFile } from '../utils/fetch-remote-file.js';
14
18
  import { fieldWriteShape } from '../type-gen.js';
19
+ import { hasCapability, resolveCapabilities } from '../types/capabilities.js';
20
+ import { contentWorkspaceTools } from '../ai/content-workspace-tools.js';
15
21
  import { DEFAULT_BLOCK_STYLES, DEFAULT_BLOCK_DECORATORS, DEFAULT_BLOCK_LISTS } from '../components/admin/fields/richtext/block-defaults.js';
16
- const ok = (data) => ({
17
- content: [{ type: 'text', text: JSON.stringify(data, null, 2) }]
18
- });
19
- const fail = (message) => ({
20
- content: [{ type: 'text', text: message }],
21
- isError: true
22
- });
22
+ const ok = (data) => ({ success: true, data });
23
+ const fail = (message) => ({ success: false, error: message });
23
24
  function asString(args, key) {
24
25
  const v = args[key];
25
26
  return typeof v === 'string' && v.length > 0 ? v : null;
@@ -123,149 +124,178 @@ function buildWriteShapes(schema, allSchemas) {
123
124
  return { writeShapes, shapeLegend };
124
125
  }
125
126
  /**
126
- * Build the content-plane tools for one authenticated request.
127
- * Safe to expose against a live instance: all writes go through LocalAPI, so a
128
- * read-only API key is rejected by the permission layer, not by this registry.
127
+ * The content-plane tools, safe to expose against a live instance: all writes go through
128
+ * LocalAPI, so a read-only API key is rejected by the permission layer, not by this
129
+ * registry. `requiredCapabilities` here is the advertisement/execution gate for the new
130
+ * agent-tool contract; document tools additionally get real enforcement downstream from
131
+ * `CollectionAPI`'s own `PermissionChecker` (unchanged) — `asset.read`/`asset.upload` have no
132
+ * such downstream check, so `list_assets`/`upload_asset` enforce it directly in `execute`.
129
133
  */
130
- export function buildContentTools({ aphexCMS, context }) {
131
- const api = aphexCMS.localAPI;
132
- const { assetService } = aphexCMS;
133
- const orgId = context.organizationId;
134
- return [
135
- {
134
+ export const contentAgentTools = [
135
+ {
136
+ definition: {
136
137
  name: 'describe_cms',
137
138
  description: 'Orientation for building against this CMS: all content types and their relationships, the valid field-type vocabulary, and what this API key is allowed to do. Call this first. All data is derived live from the running config — never stale. For exact field/schema TypeScript signatures, read the SchemaType and Field types from the `@aphexcms/cms-core` package (and the real schemas in src/lib/schemaTypes/*.ts).',
138
- inputSchema: {},
139
- handler: async () => {
140
- const schemas = aphexCMS.config.schemaTypes;
141
- const edges = [];
142
- for (const s of schemas)
143
- collectReferences(s.name, s.fields, edges);
144
- const documentTypes = schemas
145
- .filter((s) => s.type === 'document')
146
- .map((s) => ({
147
- name: s.name,
148
- title: s.title,
149
- singleton: s.type === 'document' ? (s.singleton ?? false) : false,
150
- fieldCount: s.fields.length
151
- }));
152
- const objectTypes = schemas
153
- .filter((s) => s.type === 'object')
154
- .map((s) => ({ name: s.name, title: s.title, fieldCount: s.fields.length }));
155
- const auth = context.auth;
156
- const capabilities = auth?.type === 'api_key'
157
- ? {
158
- authType: 'api_key',
159
- canWrite: auth.permissions.includes('write'),
160
- permissions: auth.permissions,
161
- capabilities: auth.capabilities
162
- }
163
- : auth?.type === 'session'
164
- ? { authType: 'session', canWrite: true, capabilities: auth.capabilities }
165
- : { authType: 'unknown', canWrite: false };
166
- return ok({
167
- organizationId: orgId,
168
- documentTypes,
169
- objectTypes,
170
- referenceGraph: edges,
171
- validFieldTypes: VALID_FIELD_TYPES,
172
- reservedFieldNames: RESERVED_FIELDS,
173
- capabilities,
174
- typeReference: "Import SchemaType/Field from '@aphexcms/cms-core' for exact per-field-type props and validation Rule API; TypeScript enforces them. Read existing schemas in src/lib/schemaTypes/*.ts as working examples."
175
- });
176
- }
139
+ mutates: false,
140
+ requiredCapabilities: [],
141
+ execution: 'server',
142
+ inputSchema: z.object({})
177
143
  },
178
- {
144
+ execute: async (_input, { aphexCMS, context }) => {
145
+ const schemas = aphexCMS.config.schemaTypes;
146
+ const orgId = context.organizationId;
147
+ const edges = [];
148
+ for (const s of schemas)
149
+ collectReferences(s.name, s.fields, edges);
150
+ const documentTypes = schemas
151
+ .filter((s) => s.type === 'document')
152
+ .map((s) => ({
153
+ name: s.name,
154
+ title: s.title,
155
+ singleton: s.type === 'document' ? (s.singleton ?? false) : false,
156
+ fieldCount: s.fields.length
157
+ }));
158
+ const objectTypes = schemas
159
+ .filter((s) => s.type === 'object')
160
+ .map((s) => ({ name: s.name, title: s.title, fieldCount: s.fields.length }));
161
+ const auth = context.auth;
162
+ const capabilities = auth?.type === 'api_key'
163
+ ? {
164
+ authType: 'api_key',
165
+ canWrite: auth.permissions.includes('write'),
166
+ permissions: auth.permissions,
167
+ capabilities: auth.capabilities
168
+ }
169
+ : auth?.type === 'session'
170
+ ? { authType: 'session', canWrite: true, capabilities: auth.capabilities }
171
+ : { authType: 'unknown', canWrite: false };
172
+ return ok({
173
+ organizationId: orgId,
174
+ documentTypes,
175
+ objectTypes,
176
+ referenceGraph: edges,
177
+ validFieldTypes: VALID_FIELD_TYPES,
178
+ reservedFieldNames: RESERVED_FIELDS,
179
+ capabilities,
180
+ typeReference: "Import SchemaType/Field from '@aphexcms/cms-core' for exact per-field-type props and validation Rule API; TypeScript enforces them. Read existing schemas in src/lib/schemaTypes/*.ts as working examples."
181
+ });
182
+ }
183
+ },
184
+ {
185
+ definition: {
179
186
  name: 'list_collections',
180
187
  description: 'List the document collections (content types) available in this CMS, with their names and titles.',
181
- inputSchema: {},
182
- handler: async () => {
183
- const names = api.getCollectionNames();
184
- const collections = names.map((name) => {
185
- const schema = api.getCollectionSchema(name);
186
- return { name, title: schema?.title ?? name, singleton: schema?.singleton ?? false };
187
- });
188
- return ok({ collections });
189
- }
188
+ mutates: false,
189
+ requiredCapabilities: [],
190
+ execution: 'server',
191
+ inputSchema: z.object({})
190
192
  },
191
- {
193
+ execute: async (_input, { aphexCMS }) => {
194
+ const api = aphexCMS.localAPI;
195
+ const names = api.getCollectionNames();
196
+ const collections = names.map((name) => {
197
+ const schema = api.getCollectionSchema(name);
198
+ return { name, title: schema?.title ?? name, singleton: schema?.singleton ?? false };
199
+ });
200
+ return ok({ collections });
201
+ }
202
+ },
203
+ {
204
+ definition: {
192
205
  name: 'get_schema',
193
206
  description: "Get the field schema for one collection, so you know the shape to use when creating or updating its documents. Returns { schema, portableText? } — `portableText` is present when the type has rich-text (block) fields and links the open Portable Text spec plus this schema's allowed styles/marks/custom block types.",
194
- inputSchema: { collection: z.string().describe('Collection name') },
195
- handler: async (args) => {
196
- const collection = asString(args, 'collection');
197
- if (!collection)
198
- return fail('Missing required string argument: collection');
199
- const schema = api.getCollectionSchema(collection);
200
- if (!schema)
201
- return fail(`Unknown collection: ${collection}`);
202
- const portableText = portableTextGuide(schema);
203
- const { writeShapes, shapeLegend } = buildWriteShapes(schema, aphexCMS.config.schemaTypes);
204
- return ok({
205
- schema,
206
- writeShapes,
207
- ...(Object.keys(shapeLegend).length > 0 ? { shapeLegend } : {}),
208
- ...(portableText ? { portableText } : {})
209
- });
210
- }
207
+ mutates: false,
208
+ requiredCapabilities: [],
209
+ execution: 'server',
210
+ inputSchema: z.object({ collection: z.string().describe('Collection name') })
211
211
  },
212
- {
212
+ execute: async (args, { aphexCMS }) => {
213
+ const api = aphexCMS.localAPI;
214
+ const collection = asString(args, 'collection');
215
+ if (!collection)
216
+ return fail('Missing required string argument: collection');
217
+ const schema = api.getCollectionSchema(collection);
218
+ if (!schema)
219
+ return fail(`Unknown collection: ${collection}`);
220
+ const portableText = portableTextGuide(schema);
221
+ const { writeShapes, shapeLegend } = buildWriteShapes(schema, aphexCMS.config.schemaTypes);
222
+ return ok({
223
+ schema,
224
+ writeShapes,
225
+ ...(Object.keys(shapeLegend).length > 0 ? { shapeLegend } : {}),
226
+ ...(portableText ? { portableText } : {})
227
+ });
228
+ }
229
+ },
230
+ {
231
+ definition: {
213
232
  name: 'validate_document',
214
233
  description: 'Dry-run: validate document `data` against its collection schema WITHOUT saving, using the same validator as create/update. Returns field-level errors so you can fix them before create_document/update_document.',
215
- inputSchema: {
234
+ mutates: false,
235
+ requiredCapabilities: [],
236
+ execution: 'server',
237
+ inputSchema: z.object({
216
238
  collection: z.string().describe('Collection name'),
217
239
  data: z.record(z.string(), z.unknown()).describe('Document field values to validate')
218
- },
219
- handler: async (args) => {
220
- const collection = asString(args, 'collection');
221
- const data = asRecord(args, 'data');
222
- if (!collection || !data)
223
- return fail('Missing required arguments: collection (string), data (object)');
224
- const schema = api.getCollectionSchema(collection);
225
- if (!schema)
226
- return fail(`Unknown collection: ${collection}`);
227
- try {
228
- const result = await validateDocumentData(schema, data);
229
- return ok({ isValid: result.isValid, errors: result.errors });
230
- }
231
- catch (err) {
232
- return fail(`Validation failed: ${err instanceof Error ? err.message : String(err)}`);
233
- }
234
- }
240
+ })
235
241
  },
236
- {
242
+ execute: async (args, { aphexCMS }) => {
243
+ const api = aphexCMS.localAPI;
244
+ const collection = asString(args, 'collection');
245
+ const data = asRecord(args, 'data');
246
+ if (!collection || !data)
247
+ return fail('Missing required arguments: collection (string), data (object)');
248
+ const schema = api.getCollectionSchema(collection);
249
+ if (!schema)
250
+ return fail(`Unknown collection: ${collection}`);
251
+ try {
252
+ const result = await validateDocumentData(schema, data);
253
+ return ok({ isValid: result.isValid, errors: result.errors });
254
+ }
255
+ catch (err) {
256
+ return fail(`Validation failed: ${err instanceof Error ? err.message : String(err)}`);
257
+ }
258
+ }
259
+ },
260
+ {
261
+ definition: {
237
262
  name: 'validate_schema',
238
263
  description: 'Validate a proposed schema definition (structure, field types, references, reserved field names) against the current CMS, WITHOUT writing a file. Use before writing a schema .ts file. Pass the schema as JSON — validation-rule functions are not needed for structural validation.',
239
- inputSchema: {
264
+ mutates: false,
265
+ requiredCapabilities: [],
266
+ execution: 'server',
267
+ inputSchema: z.object({
240
268
  schema: z
241
269
  .record(z.string(), z.unknown())
242
270
  .describe('Proposed SchemaType as JSON (type, name, title, fields, …)')
243
- },
244
- handler: async (args) => {
245
- const proposed = asRecord(args, 'schema');
246
- if (!proposed)
247
- return fail('Missing required argument: schema (object)');
248
- // Validate the proposed schema alongside the existing ones so its
249
- // references resolve. `proposed` is external JSON asserted into SchemaType
250
- // at this boundary; validateSchemaReferences is what actually checks it.
251
- const all = [
252
- ...aphexCMS.config.schemaTypes,
253
- proposed
254
- ];
255
- try {
256
- validateSchemaReferences(all);
257
- return ok({ isValid: true, errors: [] });
258
- }
259
- catch (err) {
260
- const message = err instanceof Error ? err.message : String(err);
261
- return ok({ isValid: false, errors: message.split('\n') });
262
- }
263
- }
271
+ })
264
272
  },
265
- {
273
+ execute: async (args, { aphexCMS }) => {
274
+ const proposed = asRecord(args, 'schema');
275
+ if (!proposed)
276
+ return fail('Missing required argument: schema (object)');
277
+ // Validate the proposed schema alongside the existing ones so its
278
+ // references resolve. `proposed` is external JSON asserted into SchemaType
279
+ // at this boundary; validateSchemaReferences is what actually checks it.
280
+ const all = [...aphexCMS.config.schemaTypes, proposed];
281
+ try {
282
+ validateSchemaReferences(all);
283
+ return ok({ isValid: true, errors: [] });
284
+ }
285
+ catch (err) {
286
+ const message = err instanceof Error ? err.message : String(err);
287
+ return ok({ isValid: false, errors: message.split('\n') });
288
+ }
289
+ }
290
+ },
291
+ {
292
+ definition: {
266
293
  name: 'query_documents',
267
294
  description: 'Query documents in a collection. Supports where filters, sorting, pagination, and draft/published perspective.',
268
- inputSchema: {
295
+ mutates: false,
296
+ requiredCapabilities: ['document.read'],
297
+ execution: 'server',
298
+ inputSchema: z.object({
269
299
  collection: z.string().describe('Collection name'),
270
300
  where: z
271
301
  .record(z.string(), z.unknown())
@@ -281,239 +311,317 @@ export function buildContentTools({ aphexCMS, context }) {
281
311
  .enum(['draft', 'published'])
282
312
  .optional()
283
313
  .describe('Which content to read (default draft)')
284
- },
285
- handler: async (args) => {
286
- const collection = asString(args, 'collection');
287
- if (!collection)
288
- return fail('Missing required string argument: collection');
289
- const col = api.getCollection(collection);
290
- if (!col)
291
- return fail(`Unknown collection: ${collection}`);
292
- // `where` is arbitrary filter JSON from the MCP client. WhereTyped permits
293
- // dynamic field keys; assert the parsed object into it at this external
294
- // boundary rather than validating every possible filter shape.
295
- const where = (asRecord(args, 'where') ?? undefined);
296
- const limit = typeof args.limit === 'number' ? args.limit : undefined;
297
- const offset = typeof args.offset === 'number' ? args.offset : undefined;
298
- const sort = asString(args, 'sort') ?? undefined;
299
- try {
300
- const result = await col.find(context, {
301
- where,
302
- limit,
303
- offset,
304
- sort,
305
- perspective: perspectiveArg(args.perspective)
306
- });
307
- return ok(result);
308
- }
309
- catch (err) {
310
- return fail(`Query failed: ${err instanceof Error ? err.message : String(err)}`);
311
- }
312
- }
314
+ })
313
315
  },
314
- {
316
+ execute: async (args, { aphexCMS, context }) => {
317
+ const api = aphexCMS.localAPI;
318
+ const collection = asString(args, 'collection');
319
+ if (!collection)
320
+ return fail('Missing required string argument: collection');
321
+ const col = api.getCollection(collection);
322
+ if (!col)
323
+ return fail(`Unknown collection: ${collection}`);
324
+ // `where` is arbitrary filter JSON from the MCP client. WhereTyped permits
325
+ // dynamic field keys; assert the parsed object into it at this external
326
+ // boundary rather than validating every possible filter shape.
327
+ const where = (asRecord(args, 'where') ?? undefined);
328
+ const limit = typeof args.limit === 'number' ? args.limit : undefined;
329
+ const offset = typeof args.offset === 'number' ? args.offset : undefined;
330
+ const sort = asString(args, 'sort') ?? undefined;
331
+ try {
332
+ const result = await col.find(context, {
333
+ where,
334
+ limit,
335
+ offset,
336
+ sort,
337
+ perspective: perspectiveArg(args.perspective)
338
+ });
339
+ return ok(result);
340
+ }
341
+ catch (err) {
342
+ return fail(`Query failed: ${err instanceof Error ? err.message : String(err)}`);
343
+ }
344
+ }
345
+ },
346
+ {
347
+ definition: {
315
348
  name: 'get_document',
316
349
  description: 'Get a single document by id from a collection.',
317
- inputSchema: {
350
+ mutates: false,
351
+ requiredCapabilities: ['document.read'],
352
+ execution: 'server',
353
+ inputSchema: z.object({
318
354
  collection: z.string().describe('Collection name'),
319
355
  id: z.string().describe('Document id'),
320
356
  perspective: z.enum(['draft', 'published']).optional()
321
- },
322
- handler: async (args) => {
323
- const collection = asString(args, 'collection');
324
- const id = asString(args, 'id');
325
- if (!collection || !id)
326
- return fail('Missing required string arguments: collection, id');
327
- const col = api.getCollection(collection);
328
- if (!col)
329
- return fail(`Unknown collection: ${collection}`);
330
- try {
331
- const doc = await col.findByID(context, id, {
332
- perspective: perspectiveArg(args.perspective)
333
- });
334
- if (!doc)
335
- return fail(`Document not found: ${collection}/${id}`);
336
- return ok(doc);
337
- }
338
- catch (err) {
339
- return fail(`Get failed: ${err instanceof Error ? err.message : String(err)}`);
340
- }
341
- }
357
+ })
342
358
  },
343
- {
359
+ execute: async (args, { aphexCMS, context }) => {
360
+ const api = aphexCMS.localAPI;
361
+ const collection = asString(args, 'collection');
362
+ const id = asString(args, 'id');
363
+ if (!collection || !id)
364
+ return fail('Missing required string arguments: collection, id');
365
+ const col = api.getCollection(collection);
366
+ if (!col)
367
+ return fail(`Unknown collection: ${collection}`);
368
+ try {
369
+ const doc = await col.findByID(context, id, {
370
+ perspective: perspectiveArg(args.perspective)
371
+ });
372
+ if (!doc)
373
+ return fail(`Document not found: ${collection}/${id}`);
374
+ return ok(doc);
375
+ }
376
+ catch (err) {
377
+ return fail(`Get failed: ${err instanceof Error ? err.message : String(err)}`);
378
+ }
379
+ }
380
+ },
381
+ {
382
+ definition: {
344
383
  name: 'create_document',
345
384
  description: 'Create a document in a collection. Pass field values in `data` (matching the collection schema). Set publish:true to publish immediately, otherwise it is saved as a draft.',
346
- inputSchema: {
385
+ mutates: true,
386
+ requiredCapabilities: ['document.create'],
387
+ execution: 'server',
388
+ inputSchema: z.object({
347
389
  collection: z.string().describe('Collection name'),
348
390
  data: z
349
391
  .record(z.string(), z.unknown())
350
392
  .describe('Field values matching the collection schema'),
351
393
  publish: z.boolean().optional().describe('Publish immediately (default false)')
352
- },
353
- handler: async (args) => {
354
- const collection = asString(args, 'collection');
355
- const data = asRecord(args, 'data');
356
- if (!collection || !data)
357
- return fail('Missing required arguments: collection (string), data (object)');
358
- const col = api.getCollection(collection);
359
- if (!col)
360
- return fail(`Unknown collection: ${collection}`);
361
- try {
362
- const result = await col.create(context, data, { publish: args.publish === true });
363
- return ok(result);
364
- }
365
- catch (err) {
366
- return fail(`Create failed: ${err instanceof Error ? err.message : String(err)}`);
367
- }
368
- }
394
+ })
369
395
  },
370
- {
396
+ execute: async (args, { aphexCMS, context }) => {
397
+ const api = aphexCMS.localAPI;
398
+ const collection = asString(args, 'collection');
399
+ const data = asRecord(args, 'data');
400
+ if (!collection || !data)
401
+ return fail('Missing required arguments: collection (string), data (object)');
402
+ const col = api.getCollection(collection);
403
+ if (!col)
404
+ return fail(`Unknown collection: ${collection}`);
405
+ try {
406
+ const result = await col.create(context, data, { publish: args.publish === true });
407
+ return ok(result);
408
+ }
409
+ catch (err) {
410
+ return fail(`Create failed: ${err instanceof Error ? err.message : String(err)}`);
411
+ }
412
+ }
413
+ },
414
+ {
415
+ definition: {
371
416
  name: 'update_document',
372
- description: 'Update fields on an existing document. Only include the fields you want to change in `data`. Set publish:true to publish the result.',
373
- inputSchema: {
417
+ description: "Update fields on an existing document. Only include the fields you want to change in `data`. Set publish:true to publish the result. Pass `expectedRevision` (from a prior get_document/update_document/publish_document call's `document._meta.revision`) to guard against overwriting a change made since you last read it — a mismatch fails the call instead of silently overwriting. If `content_patch_fields`/`content_save_draft` are also available, they target the document currently open in the admin editor — prefer those for edits to that specific document, since this tool writes straight to the database and the open editor will not reflect the change until the user reloads.",
418
+ mutates: true,
419
+ requiredCapabilities: ['document.update'],
420
+ execution: 'server',
421
+ inputSchema: z.object({
374
422
  collection: z.string().describe('Collection name'),
375
423
  id: z.string().describe('Document id'),
376
424
  data: z.record(z.string(), z.unknown()).describe('Partial field values to update'),
377
- publish: z.boolean().optional().describe('Publish after updating (default false)')
378
- },
379
- handler: async (args) => {
380
- const collection = asString(args, 'collection');
381
- const id = asString(args, 'id');
382
- const data = asRecord(args, 'data');
383
- if (!collection || !id || !data)
384
- return fail('Missing required arguments: collection, id (strings), data (object)');
385
- const col = api.getCollection(collection);
386
- if (!col)
387
- return fail(`Unknown collection: ${collection}`);
388
- try {
389
- const result = await col.update(context, id, data, { publish: args.publish === true });
390
- if (!result)
391
- return fail(`Document not found: ${collection}/${id}`);
392
- return ok(result);
393
- }
394
- catch (err) {
395
- return fail(`Update failed: ${err instanceof Error ? err.message : String(err)}`);
396
- }
397
- }
425
+ publish: z.boolean().optional().describe('Publish after updating (default false)'),
426
+ expectedRevision: z
427
+ .number()
428
+ .optional()
429
+ .describe('CAS guard the revision you last read; mismatch fails instead of overwriting')
430
+ })
398
431
  },
399
- {
432
+ execute: async (args, { aphexCMS, context }) => {
433
+ const api = aphexCMS.localAPI;
434
+ const collection = asString(args, 'collection');
435
+ const id = asString(args, 'id');
436
+ const data = asRecord(args, 'data');
437
+ if (!collection || !id || !data)
438
+ return fail('Missing required arguments: collection, id (strings), data (object)');
439
+ const col = api.getCollection(collection);
440
+ if (!col)
441
+ return fail(`Unknown collection: ${collection}`);
442
+ try {
443
+ const result = await col.update(context, id, data, {
444
+ publish: args.publish === true,
445
+ expectedRevision: typeof args.expectedRevision === 'number' ? args.expectedRevision : undefined
446
+ });
447
+ if (!result)
448
+ return fail(`Document not found: ${collection}/${id}`);
449
+ return ok(result);
450
+ }
451
+ catch (err) {
452
+ return fail(`Update failed: ${err instanceof Error ? err.message : String(err)}`);
453
+ }
454
+ }
455
+ },
456
+ {
457
+ definition: {
400
458
  name: 'publish_document',
401
- description: 'Publish a document (copies its current draft to the published perspective).',
402
- inputSchema: {
459
+ description: "Publish a document (copies its current draft to the published perspective). Pass `expectedRevision` (from a prior read/write's `document._meta.revision`) to guard against publishing over a change made since you last read it.",
460
+ mutates: true,
461
+ requiredCapabilities: ['document.publish'],
462
+ execution: 'server',
463
+ inputSchema: z.object({
403
464
  collection: z.string().describe('Collection name'),
404
- id: z.string().describe('Document id')
405
- },
406
- handler: async (args) => {
407
- const collection = asString(args, 'collection');
408
- const id = asString(args, 'id');
409
- if (!collection || !id)
410
- return fail('Missing required string arguments: collection, id');
411
- const col = api.getCollection(collection);
412
- if (!col)
413
- return fail(`Unknown collection: ${collection}`);
414
- try {
415
- const doc = await col.publish(context, id);
416
- if (!doc)
417
- return fail(`Document not found: ${collection}/${id}`);
418
- return ok(doc);
419
- }
420
- catch (err) {
421
- return fail(`Publish failed: ${err instanceof Error ? err.message : String(err)}`);
422
- }
423
- }
465
+ id: z.string().describe('Document id'),
466
+ expectedRevision: z
467
+ .number()
468
+ .optional()
469
+ .describe('CAS guard the revision you last read; mismatch fails instead of overwriting')
470
+ })
424
471
  },
425
- {
472
+ execute: async (args, { aphexCMS, context }) => {
473
+ const api = aphexCMS.localAPI;
474
+ const collection = asString(args, 'collection');
475
+ const id = asString(args, 'id');
476
+ if (!collection || !id)
477
+ return fail('Missing required string arguments: collection, id');
478
+ const col = api.getCollection(collection);
479
+ if (!col)
480
+ return fail(`Unknown collection: ${collection}`);
481
+ try {
482
+ const doc = await col.publish(context, id, {
483
+ expectedRevision: typeof args.expectedRevision === 'number' ? args.expectedRevision : undefined
484
+ });
485
+ if (!doc)
486
+ return fail(`Document not found: ${collection}/${id}`);
487
+ return ok(doc);
488
+ }
489
+ catch (err) {
490
+ return fail(`Publish failed: ${err instanceof Error ? err.message : String(err)}`);
491
+ }
492
+ }
493
+ },
494
+ {
495
+ definition: {
426
496
  name: 'get_singleton',
427
497
  description: 'Get a singleton document (a type where exactly one exists, e.g. site settings — flagged `singleton: true` in describe_cms). No id needed; the canonical row is resolved (and lazily created empty on first access). Use this instead of get_document for singletons.',
428
- inputSchema: {
498
+ mutates: false,
499
+ requiredCapabilities: ['document.read'],
500
+ execution: 'server',
501
+ inputSchema: z.object({
429
502
  collection: z.string().describe('Singleton collection name'),
430
503
  perspective: z.enum(['draft', 'published']).optional()
431
- },
432
- handler: async (args) => {
433
- const collection = asString(args, 'collection');
434
- if (!collection)
435
- return fail('Missing required string argument: collection');
436
- const col = api.getCollection(collection);
437
- if (!col)
438
- return fail(`Unknown collection: ${collection}`);
439
- try {
440
- const doc = await col.get(context, { perspective: perspectiveArg(args.perspective) });
441
- return ok(doc);
442
- }
443
- catch (err) {
444
- // SingletonOperationError when the type isn't a singleton — surface its message.
445
- return fail(err instanceof Error ? err.message : String(err));
446
- }
447
- }
504
+ })
448
505
  },
449
- {
506
+ execute: async (args, { aphexCMS, context }) => {
507
+ const api = aphexCMS.localAPI;
508
+ const collection = asString(args, 'collection');
509
+ if (!collection)
510
+ return fail('Missing required string argument: collection');
511
+ const col = api.getCollection(collection);
512
+ if (!col)
513
+ return fail(`Unknown collection: ${collection}`);
514
+ try {
515
+ const doc = await col.get(context, { perspective: perspectiveArg(args.perspective) });
516
+ return ok(doc);
517
+ }
518
+ catch (err) {
519
+ // SingletonOperationError when the type isn't a singleton — surface its message.
520
+ return fail(err instanceof Error ? err.message : String(err));
521
+ }
522
+ }
523
+ },
524
+ {
525
+ definition: {
450
526
  name: 'update_singleton',
451
527
  description: 'Update a singleton document (e.g. site settings). No id needed — the canonical row is resolved by type. Include only the fields to change in `data`. Set publish:true to publish the result. Use this instead of update_document for singletons.',
452
- inputSchema: {
528
+ mutates: true,
529
+ requiredCapabilities: ['document.update'],
530
+ execution: 'server',
531
+ inputSchema: z.object({
453
532
  collection: z.string().describe('Singleton collection name'),
454
533
  data: z.record(z.string(), z.unknown()).describe('Partial field values to update'),
455
534
  publish: z.boolean().optional().describe('Publish after updating (default false)')
456
- },
457
- handler: async (args) => {
458
- const collection = asString(args, 'collection');
459
- const data = asRecord(args, 'data');
460
- if (!collection || !data)
461
- return fail('Missing required arguments: collection (string), data (object)');
462
- const col = api.getCollection(collection);
463
- if (!col)
464
- return fail(`Unknown collection: ${collection}`);
465
- const id = col.getSingletonId(context);
466
- if (!id)
467
- return fail(`'${collection}' is not a singleton. Use update_document instead.`);
468
- try {
469
- // Ensure the canonical row exists (get lazily creates it), then update.
470
- await col.get(context);
471
- const result = await col.update(context, id, data, { publish: args.publish === true });
472
- if (!result)
473
- return fail(`Failed to update singleton '${collection}'.`);
474
- return ok(result);
475
- }
476
- catch (err) {
477
- return fail(`Update failed: ${err instanceof Error ? err.message : String(err)}`);
478
- }
479
- }
535
+ })
480
536
  },
481
- {
537
+ execute: async (args, { aphexCMS, context }) => {
538
+ const api = aphexCMS.localAPI;
539
+ const collection = asString(args, 'collection');
540
+ const data = asRecord(args, 'data');
541
+ if (!collection || !data)
542
+ return fail('Missing required arguments: collection (string), data (object)');
543
+ const col = api.getCollection(collection);
544
+ if (!col)
545
+ return fail(`Unknown collection: ${collection}`);
546
+ const id = col.getSingletonId(context);
547
+ if (!id)
548
+ return fail(`'${collection}' is not a singleton. Use update_document instead.`);
549
+ try {
550
+ // Ensure the canonical row exists (get lazily creates it), then update.
551
+ await col.get(context);
552
+ const result = await col.update(context, id, data, { publish: args.publish === true });
553
+ if (!result)
554
+ return fail(`Failed to update singleton '${collection}'.`);
555
+ return ok(result);
556
+ }
557
+ catch (err) {
558
+ return fail(`Update failed: ${err instanceof Error ? err.message : String(err)}`);
559
+ }
560
+ }
561
+ },
562
+ {
563
+ definition: {
482
564
  name: 'list_assets',
483
565
  description: 'List media assets (images and files) in this organization, optionally filtered.',
484
- inputSchema: {
566
+ mutates: false,
567
+ requiredCapabilities: ['asset.read'],
568
+ execution: 'server',
569
+ inputSchema: z.object({
485
570
  search: z.string().optional().describe('Filter by filename/text'),
486
571
  assetType: z.enum(['image', 'file']).optional(),
487
572
  limit: z.number().optional(),
488
573
  offset: z.number().optional()
489
- },
490
- handler: async (args) => {
491
- const search = asString(args, 'search') ?? undefined;
492
- const assetType = args.assetType === 'image' || args.assetType === 'file' ? args.assetType : undefined;
493
- const limit = typeof args.limit === 'number' ? args.limit : undefined;
494
- const offset = typeof args.offset === 'number' ? args.offset : undefined;
495
- try {
496
- const assets = await assetService.findAssets(orgId, { search, assetType, limit, offset });
497
- return ok({ assets, count: assets.length });
498
- }
499
- catch (err) {
500
- return fail(`List assets failed: ${err instanceof Error ? err.message : String(err)}`);
501
- }
502
- }
574
+ })
503
575
  },
504
- {
576
+ execute: async (args, { aphexCMS, context }) => {
577
+ if (!context.auth || !hasCapability(context.auth, 'asset.read')) {
578
+ return fail("Forbidden: 'asset.read' capability required.");
579
+ }
580
+ const { assetService } = aphexCMS;
581
+ const orgId = context.organizationId;
582
+ const search = asString(args, 'search') ?? undefined;
583
+ const assetType = args.assetType === 'image' || args.assetType === 'file' ? args.assetType : undefined;
584
+ const limit = typeof args.limit === 'number' ? args.limit : undefined;
585
+ const offset = typeof args.offset === 'number' ? args.offset : undefined;
586
+ try {
587
+ const assets = await assetService.findAssets(orgId, { search, assetType, limit, offset });
588
+ return ok({ assets, count: assets.length });
589
+ }
590
+ catch (err) {
591
+ return fail(`List assets failed: ${err instanceof Error ? err.message : String(err)}`);
592
+ }
593
+ }
594
+ },
595
+ {
596
+ definition: {
505
597
  name: 'upload_asset',
506
- description: 'Upload an image or file from base64 data and get back a ready-to-reference value. ' +
507
- 'The response includes `imageValue` and `fileValue` — drop the matching one straight ' +
508
- 'into a document field (e.g. a blog post `coverImage`, an author `avatar`, or an inline ' +
509
- '`image` block) via update_document. File type is verified from the actual bytes, not the ' +
510
- 'declared name.',
511
- inputSchema: {
512
- data: z.string().min(1).describe('Base64-encoded file contents (no data: URI prefix).'),
598
+ description: 'Upload an image or file and get back a ready-to-reference value. Provide either ' +
599
+ '`data` (base64 file contents) or `url` (fetched server-side use this for an image ' +
600
+ 'found via search or a link you were given, not `data`, since you cannot produce raw ' +
601
+ 'file bytes yourself). The response includes `imageValue` and `fileValue` drop the ' +
602
+ 'matching one straight into a document field (e.g. a blog post `coverImage`, an author ' +
603
+ '`avatar`, or an inline `image` block) via update_document. File type is verified from ' +
604
+ 'the actual bytes, not the declared name or URL.',
605
+ mutates: true,
606
+ requiredCapabilities: ['asset.upload'],
607
+ execution: 'server',
608
+ inputSchema: z.object({
609
+ data: z
610
+ .string()
611
+ .min(1)
612
+ .optional()
613
+ .describe('Base64-encoded file contents (no data: URI prefix).'),
614
+ url: z
615
+ .string()
616
+ .url()
617
+ .optional()
618
+ .describe('An http(s) URL to fetch the file from. Provide exactly one of `data`/`url`.'),
513
619
  filename: z
514
620
  .string()
515
621
  .min(1)
516
- .describe('Original filename, e.g. "cover.png". Its extension helps typing.'),
622
+ .optional()
623
+ .describe('Original filename, e.g. "cover.png". Its extension helps typing. Required with ' +
624
+ '`data`; derived from the URL when omitted with `url`.'),
517
625
  mimeType: z
518
626
  .string()
519
627
  .optional()
@@ -521,53 +629,129 @@ export function buildContentTools({ aphexCMS, context }) {
521
629
  alt: z.string().optional().describe('Default alt text, shared across every placement.'),
522
630
  title: z.string().optional(),
523
631
  description: z.string().optional()
524
- },
525
- handler: async (args) => {
526
- const base64 = asString(args, 'data');
527
- const filename = asString(args, 'filename');
528
- if (!base64)
529
- return fail("'data' (base64 file contents) is required.");
530
- if (!filename)
531
- return fail("'filename' is required.");
532
- let buffer;
632
+ })
633
+ },
634
+ execute: async (args, { aphexCMS, context }) => {
635
+ if (!context.auth || !hasCapability(context.auth, 'asset.upload')) {
636
+ return fail("Forbidden: 'asset.upload' capability required.");
637
+ }
638
+ const { assetService } = aphexCMS;
639
+ const orgId = context.organizationId;
640
+ const base64 = asString(args, 'data');
641
+ const url = asString(args, 'url');
642
+ if (!base64 && !url)
643
+ return fail("Provide either 'data' or 'url'.");
644
+ if (base64 && url)
645
+ return fail("Provide only one of 'data' or 'url', not both.");
646
+ let buffer;
647
+ let sniffedMime = null;
648
+ let filename = asString(args, 'filename');
649
+ if (url) {
533
650
  try {
534
- buffer = Buffer.from(base64, 'base64');
651
+ const remote = await fetchRemoteFile(url);
652
+ buffer = remote.buffer;
653
+ sniffedMime = remote.contentType;
535
654
  }
536
- catch {
537
- return fail("'data' is not valid base64.");
655
+ catch (err) {
656
+ return fail(`Fetching 'url' failed: ${err instanceof Error ? err.message : String(err)}`);
538
657
  }
539
- if (buffer.length === 0)
540
- return fail("'data' decoded to zero bytes.");
541
- const declaredMime = asString(args, 'mimeType') ?? '';
542
- const validation = validateFile(buffer, filename, declaredMime);
543
- if (!validation.valid) {
544
- return fail(`Upload rejected: ${validation.error ?? 'file failed validation.'}`);
658
+ if (!filename) {
659
+ const last = new URL(url).pathname.split('/').filter(Boolean).pop();
660
+ filename = last && last.includes('.') ? last : 'upload';
545
661
  }
546
- const mimeType = validation.detectedMimeType || declaredMime || 'application/octet-stream';
662
+ }
663
+ else {
547
664
  try {
548
- const asset = await assetService.uploadAsset(orgId, {
549
- buffer,
550
- originalFilename: filename,
551
- mimeType,
552
- size: buffer.length,
553
- alt: asString(args, 'alt') ?? undefined,
554
- title: asString(args, 'title') ?? undefined,
555
- description: asString(args, 'description') ?? undefined,
556
- createdBy: context.user?.id
557
- });
558
- const ref = { _type: 'reference', _ref: asset.id };
559
- return ok({
560
- asset,
561
- // Referenceable field values — use the one matching the target field's type.
562
- imageValue: { _type: 'image', asset: ref },
563
- fileValue: { _type: 'file', asset: ref }
564
- });
665
+ buffer = Buffer.from(base64, 'base64');
565
666
  }
566
- catch (err) {
567
- return fail(`Upload failed: ${err instanceof Error ? err.message : String(err)}`);
667
+ catch {
668
+ return fail("'data' is not valid base64.");
568
669
  }
569
670
  }
671
+ if (!filename)
672
+ return fail("'filename' is required.");
673
+ if (buffer.length === 0)
674
+ return fail('File contents decoded to zero bytes.');
675
+ const declaredMime = asString(args, 'mimeType') ?? sniffedMime ?? '';
676
+ const validation = validateFile(buffer, filename, declaredMime);
677
+ if (!validation.valid) {
678
+ return fail(`Upload rejected: ${validation.error ?? 'file failed validation.'}`);
679
+ }
680
+ const mimeType = validation.detectedMimeType || declaredMime || 'application/octet-stream';
681
+ try {
682
+ const asset = await assetService.uploadAsset(orgId, {
683
+ buffer,
684
+ originalFilename: filename,
685
+ mimeType,
686
+ size: buffer.length,
687
+ alt: asString(args, 'alt') ?? undefined,
688
+ title: asString(args, 'title') ?? undefined,
689
+ description: asString(args, 'description') ?? undefined,
690
+ createdBy: context.user?.id
691
+ });
692
+ const ref = { _type: 'reference', _ref: asset.id };
693
+ return ok({
694
+ asset,
695
+ // Referenceable field values — use the one matching the target field's type.
696
+ imageValue: { _type: 'image', asset: ref },
697
+ fileValue: { _type: 'file', asset: ref }
698
+ });
699
+ }
700
+ catch (err) {
701
+ return fail(`Upload failed: ${err instanceof Error ? err.message : String(err)}`);
702
+ }
570
703
  }
571
- ];
704
+ }
705
+ ];
706
+ function toMcpResult(result) {
707
+ if (result.success) {
708
+ return { content: [{ type: 'text', text: JSON.stringify(result.data, null, 2) }] };
709
+ }
710
+ return { content: [{ type: 'text', text: result.error ?? 'Tool failed' }], isError: true };
711
+ }
712
+ /**
713
+ * The full set of tools this caller can see: core built-ins plus any
714
+ * plugin-contributed `aphex/agent/tool` parts their capabilities unlock
715
+ * (`partResolver.agentToolsForCapabilities`), plus the workspace-bridge tools when
716
+ * `documentContext` is given — the one shared list MCP, the in-admin agent runtime
717
+ * (`ai/run-agent-turn.ts`), and any other future tool-calling transport all resolve from.
718
+ * Core built-ins always win a name collision, since they're the platform's own contract.
719
+ */
720
+ export function resolveAgentTools({ aphexCMS, context }, opts) {
721
+ const coreNames = new Set(contentAgentTools.map((t) => t.definition.name));
722
+ const callerCapabilities = context.auth ? [...resolveCapabilities(context.auth)] : [];
723
+ const pluginTools = aphexCMS.partResolver
724
+ .agentToolsForCapabilities(callerCapabilities)
725
+ .filter((t) => !coreNames.has(t.definition.name));
726
+ const base = [...contentAgentTools, ...pluginTools];
727
+ if (opts?.documentContext) {
728
+ // `update_document` writes straight to the DB and bypasses the open editor. Its
729
+ // description already told the model to prefer the workspace tools here, but a prompt
730
+ // preference is not a guarantee — the model still reached for it (users had to explicitly
731
+ // ask for a refresh). Removing it as a *choice* while a document is bridged is the actual
732
+ // fix: the only path left for editing that document is the one that stays in sync.
733
+ return [
734
+ ...base.filter((t) => t.definition.name !== 'update_document'),
735
+ ...contentWorkspaceTools
736
+ ];
737
+ }
738
+ // Defense in depth: without a live document to bridge into, a `workspace`-execution tool
739
+ // (core or plugin-contributed) must never be advertised — there's nothing on the other
740
+ // end to resolve it, and `run-agent-turn.ts` would otherwise pause a turn forever.
741
+ return base.filter((t) => t.definition.execution !== 'workspace');
742
+ }
743
+ /**
744
+ * Adapt `resolveAgentTools` into the MCP SDK's expected shape for one authenticated
745
+ * request. All the actual tool logic lives in `contentAgentTools`/plugin parts above —
746
+ * this is purely a transport-shape + result-shape conversion.
747
+ */
748
+ export function buildContentTools(deps) {
749
+ const { aphexCMS, context } = deps;
750
+ return resolveAgentTools(deps).map(({ definition, execute }) => ({
751
+ name: definition.name,
752
+ description: definition.description,
753
+ inputSchema: definition.inputSchema.shape,
754
+ handler: async (args) => toMcpResult(await execute(args, { aphexCMS, context }))
755
+ }));
572
756
  }
573
757
  //# sourceMappingURL=tools.js.map