studiograph 1.3.48-next.21 → 1.3.48-next.211

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 (966) hide show
  1. package/README.md +26 -16
  2. package/dist/agent/agent-pool.d.ts +87 -3
  3. package/dist/agent/agent-pool.js +188 -15
  4. package/dist/agent/agent-pool.js.map +1 -1
  5. package/dist/agent/followups.d.ts +34 -0
  6. package/dist/agent/followups.js +52 -0
  7. package/dist/agent/followups.js.map +1 -0
  8. package/dist/agent/orchestrator.d.ts +101 -4
  9. package/dist/agent/orchestrator.js +464 -66
  10. package/dist/agent/orchestrator.js.map +1 -1
  11. package/dist/agent/prompts/entity-types-section.d.ts +34 -0
  12. package/dist/agent/prompts/entity-types-section.js +76 -0
  13. package/dist/agent/prompts/entity-types-section.js.map +1 -0
  14. package/dist/agent/prompts/system.md +112 -54
  15. package/dist/agent/skill-loader.d.ts +30 -17
  16. package/dist/agent/skill-loader.js +63 -30
  17. package/dist/agent/skill-loader.js.map +1 -1
  18. package/dist/agent/skills/artifact-canvas/skill.md +152 -0
  19. package/dist/agent/skills/entity-schema.md +75 -431
  20. package/dist/agent/skills/interview/entity-templates.md +38 -37
  21. package/dist/agent/skills/interview/question-domains.md +54 -49
  22. package/dist/agent/skills/interview/skill.md +84 -16
  23. package/dist/agent/skills/schema-rules.md +28 -28
  24. package/dist/agent/tools/artifact-tools.d.ts +124 -0
  25. package/dist/agent/tools/artifact-tools.js +207 -0
  26. package/dist/agent/tools/artifact-tools.js.map +1 -0
  27. package/dist/agent/tools/capture-tools.d.ts +31 -0
  28. package/dist/agent/tools/capture-tools.js +40 -0
  29. package/dist/agent/tools/capture-tools.js.map +1 -0
  30. package/dist/agent/tools/folder-tools.d.ts +47 -0
  31. package/dist/agent/tools/folder-tools.js +249 -0
  32. package/dist/agent/tools/folder-tools.js.map +1 -0
  33. package/dist/agent/tools/fs-tools.d.ts +6 -5
  34. package/dist/agent/tools/fs-tools.js +5 -5
  35. package/dist/agent/tools/fs-tools.js.map +1 -1
  36. package/dist/agent/tools/graph-tools.d.ts +48 -72
  37. package/dist/agent/tools/graph-tools.js +852 -329
  38. package/dist/agent/tools/graph-tools.js.map +1 -1
  39. package/dist/agent/tools/history-tools.d.ts +95 -0
  40. package/dist/agent/tools/history-tools.js +461 -0
  41. package/dist/agent/tools/history-tools.js.map +1 -0
  42. package/dist/agent/tools/load-skill.d.ts +6 -5
  43. package/dist/agent/tools/load-skill.js +3 -3
  44. package/dist/agent/tools/load-skill.js.map +1 -1
  45. package/dist/agent/tools/ops-tools.d.ts +5 -4
  46. package/dist/agent/tools/ops-tools.js +130 -236
  47. package/dist/agent/tools/ops-tools.js.map +1 -1
  48. package/dist/agent/tools/permission-tools.d.ts +87 -17
  49. package/dist/agent/tools/permission-tools.js +233 -43
  50. package/dist/agent/tools/permission-tools.js.map +1 -1
  51. package/dist/agent/tools/tool-loader.js +5 -4
  52. package/dist/agent/tools/tool-loader.js.map +1 -1
  53. package/dist/agent/tools/web-tools.js +58 -9
  54. package/dist/agent/tools/web-tools.js.map +1 -1
  55. package/dist/cli/commands/about.d.ts +1 -0
  56. package/dist/cli/commands/about.js +55 -3
  57. package/dist/cli/commands/about.js.map +1 -1
  58. package/dist/cli/commands/admin/audit-workspace.d.ts +23 -0
  59. package/dist/cli/commands/admin/audit-workspace.js +133 -0
  60. package/dist/cli/commands/admin/audit-workspace.js.map +1 -0
  61. package/dist/cli/commands/admin/audit.d.ts +21 -0
  62. package/dist/cli/commands/admin/audit.js +174 -0
  63. package/dist/cli/commands/admin/audit.js.map +1 -0
  64. package/dist/cli/commands/admin/backup.d.ts +54 -0
  65. package/dist/cli/commands/admin/backup.js +207 -0
  66. package/dist/cli/commands/admin/backup.js.map +1 -0
  67. package/dist/cli/commands/admin/folder.d.ts +11 -0
  68. package/dist/cli/commands/{collection.js → admin/folder.js} +33 -32
  69. package/dist/cli/commands/admin/folder.js.map +1 -0
  70. package/dist/cli/commands/admin/index.d.ts +16 -0
  71. package/dist/cli/commands/admin/index.js +46 -0
  72. package/dist/cli/commands/admin/index.js.map +1 -0
  73. package/dist/cli/commands/admin/migrate-assets.d.ts +53 -0
  74. package/dist/cli/commands/admin/migrate-assets.js +184 -0
  75. package/dist/cli/commands/admin/migrate-assets.js.map +1 -0
  76. package/dist/cli/commands/admin/migrate.d.ts +56 -0
  77. package/dist/cli/commands/admin/migrate.js +263 -0
  78. package/dist/cli/commands/admin/migrate.js.map +1 -0
  79. package/dist/cli/commands/admin/restore.d.ts +24 -0
  80. package/dist/cli/commands/admin/restore.js +228 -0
  81. package/dist/cli/commands/admin/restore.js.map +1 -0
  82. package/dist/cli/commands/admin/secrets.d.ts +23 -0
  83. package/dist/cli/commands/admin/secrets.js +256 -0
  84. package/dist/cli/commands/admin/secrets.js.map +1 -0
  85. package/dist/cli/commands/admin/settings.d.ts +28 -0
  86. package/dist/cli/commands/admin/settings.js +284 -0
  87. package/dist/cli/commands/admin/settings.js.map +1 -0
  88. package/dist/cli/commands/admin/token.d.ts +42 -0
  89. package/dist/cli/commands/admin/token.js +126 -0
  90. package/dist/cli/commands/admin/token.js.map +1 -0
  91. package/dist/cli/commands/admin/transfer-ownership.d.ts +15 -0
  92. package/dist/cli/commands/admin/transfer-ownership.js +106 -0
  93. package/dist/cli/commands/admin/transfer-ownership.js.map +1 -0
  94. package/dist/cli/commands/admin/user.d.ts +11 -0
  95. package/dist/cli/commands/admin/user.js +187 -0
  96. package/dist/cli/commands/admin/user.js.map +1 -0
  97. package/dist/cli/commands/clone.d.ts +25 -3
  98. package/dist/cli/commands/clone.js +142 -36
  99. package/dist/cli/commands/clone.js.map +1 -1
  100. package/dist/cli/commands/config.d.ts +23 -107
  101. package/dist/cli/commands/config.js +52 -260
  102. package/dist/cli/commands/config.js.map +1 -1
  103. package/dist/cli/commands/connector.d.ts +10 -13
  104. package/dist/cli/commands/connector.js +16 -58
  105. package/dist/cli/commands/connector.js.map +1 -1
  106. package/dist/cli/commands/deploy.js +215 -68
  107. package/dist/cli/commands/deploy.js.map +1 -1
  108. package/dist/cli/commands/index.js +5 -1
  109. package/dist/cli/commands/index.js.map +1 -1
  110. package/dist/cli/commands/init.js +10 -30
  111. package/dist/cli/commands/init.js.map +1 -1
  112. package/dist/cli/commands/mcp.js +54 -6
  113. package/dist/cli/commands/mcp.js.map +1 -1
  114. package/dist/cli/commands/r2.d.ts +3 -0
  115. package/dist/cli/commands/r2.js +223 -204
  116. package/dist/cli/commands/r2.js.map +1 -1
  117. package/dist/cli/commands/redeploy.js +65 -12
  118. package/dist/cli/commands/redeploy.js.map +1 -1
  119. package/dist/cli/commands/reset.d.ts +22 -6
  120. package/dist/cli/commands/reset.js +132 -34
  121. package/dist/cli/commands/reset.js.map +1 -1
  122. package/dist/cli/commands/serve.js +231 -26
  123. package/dist/cli/commands/serve.js.map +1 -1
  124. package/dist/cli/commands/sync.d.ts +1 -1
  125. package/dist/cli/commands/sync.js +237 -227
  126. package/dist/cli/commands/sync.js.map +1 -1
  127. package/dist/cli/crypto-bundle.d.ts +39 -0
  128. package/dist/cli/crypto-bundle.js +94 -0
  129. package/dist/cli/crypto-bundle.js.map +1 -0
  130. package/dist/cli/index.js +24 -26
  131. package/dist/cli/index.js.map +1 -1
  132. package/dist/cli/railway-pin.d.ts +68 -0
  133. package/dist/cli/railway-pin.js +96 -0
  134. package/dist/cli/railway-pin.js.map +1 -0
  135. package/dist/cli/railway-provisioning.d.ts +53 -0
  136. package/dist/cli/railway-provisioning.js +85 -0
  137. package/dist/cli/railway-provisioning.js.map +1 -0
  138. package/dist/cli/setup-wizard.d.ts +30 -49
  139. package/dist/cli/setup-wizard.js +39 -40
  140. package/dist/cli/setup-wizard.js.map +1 -1
  141. package/dist/core/accent-palette.d.ts +22 -0
  142. package/dist/core/accent-palette.js +47 -0
  143. package/dist/core/accent-palette.js.map +1 -0
  144. package/dist/core/artifact-asset-urls.d.ts +51 -0
  145. package/dist/core/artifact-asset-urls.js +79 -0
  146. package/dist/core/artifact-asset-urls.js.map +1 -0
  147. package/dist/core/artifact-bundle.d.ts +69 -0
  148. package/dist/core/artifact-bundle.js +305 -0
  149. package/dist/core/artifact-bundle.js.map +1 -0
  150. package/dist/core/artifact-escape.d.ts +5 -0
  151. package/dist/core/artifact-escape.js +26 -0
  152. package/dist/core/artifact-escape.js.map +1 -0
  153. package/dist/core/artifact-export.d.ts +17 -0
  154. package/dist/core/artifact-export.js +529 -0
  155. package/dist/core/artifact-export.js.map +1 -0
  156. package/dist/core/cell-anchor.d.ts +29 -0
  157. package/dist/core/cell-anchor.js +53 -0
  158. package/dist/core/cell-anchor.js.map +1 -0
  159. package/dist/core/comment-anchor.d.ts +41 -0
  160. package/dist/core/comment-anchor.js +147 -0
  161. package/dist/core/comment-anchor.js.map +1 -0
  162. package/dist/core/commit-messages.d.ts +57 -0
  163. package/dist/core/commit-messages.js +85 -0
  164. package/dist/core/commit-messages.js.map +1 -0
  165. package/dist/core/embeds.d.ts +96 -0
  166. package/dist/core/embeds.js +196 -0
  167. package/dist/core/embeds.js.map +1 -0
  168. package/dist/core/entity-body-templates.d.ts +21 -0
  169. package/dist/core/entity-body-templates.js +67 -0
  170. package/dist/core/entity-body-templates.js.map +1 -0
  171. package/dist/core/entity-types-catalog.d.ts +65 -0
  172. package/dist/core/entity-types-catalog.js +142 -0
  173. package/dist/core/entity-types-catalog.js.map +1 -0
  174. package/dist/core/field-library.d.ts +62 -0
  175. package/dist/core/field-library.js +129 -0
  176. package/dist/core/field-library.js.map +1 -0
  177. package/dist/core/folder-paths.d.ts +41 -0
  178. package/dist/core/folder-paths.js +95 -0
  179. package/dist/core/folder-paths.js.map +1 -0
  180. package/dist/core/folder-sidecar.d.ts +36 -0
  181. package/dist/core/folder-sidecar.js +91 -0
  182. package/dist/core/folder-sidecar.js.map +1 -0
  183. package/dist/core/format-registry.d.ts +170 -0
  184. package/dist/core/format-registry.js +412 -0
  185. package/dist/core/format-registry.js.map +1 -0
  186. package/dist/core/graph.d.ts +773 -14
  187. package/dist/core/graph.js +2269 -308
  188. package/dist/core/graph.js.map +1 -1
  189. package/dist/core/history-diff.d.ts +35 -0
  190. package/dist/core/history-diff.js +114 -0
  191. package/dist/core/history-diff.js.map +1 -0
  192. package/dist/core/refs.d.ts +41 -0
  193. package/dist/core/refs.js +80 -0
  194. package/dist/core/refs.js.map +1 -0
  195. package/dist/core/schema-registry.d.ts +66 -0
  196. package/dist/core/schema-registry.js +198 -37
  197. package/dist/core/schema-registry.js.map +1 -1
  198. package/dist/core/schemas/connector.d.ts +67 -0
  199. package/dist/core/schemas/connector.js +100 -0
  200. package/dist/core/schemas/connector.js.map +1 -0
  201. package/dist/core/schemas/workspace.d.ts +122 -0
  202. package/dist/core/schemas/workspace.js +211 -0
  203. package/dist/core/schemas/workspace.js.map +1 -0
  204. package/dist/core/secrets/SecretStore.d.ts +77 -0
  205. package/dist/core/secrets/SecretStore.js +13 -0
  206. package/dist/core/secrets/SecretStore.js.map +1 -0
  207. package/dist/core/secrets/SettingsResolver.d.ts +54 -0
  208. package/dist/core/secrets/SettingsResolver.js +80 -0
  209. package/dist/core/secrets/SettingsResolver.js.map +1 -0
  210. package/dist/core/secrets/SettingsStore.d.ts +30 -0
  211. package/dist/core/secrets/SettingsStore.js +9 -0
  212. package/dist/core/secrets/SettingsStore.js.map +1 -0
  213. package/dist/core/secrets/SqliteEncryptedStore.d.ts +90 -0
  214. package/dist/core/secrets/SqliteEncryptedStore.js +598 -0
  215. package/dist/core/secrets/SqliteEncryptedStore.js.map +1 -0
  216. package/dist/core/secrets/audit.d.ts +36 -0
  217. package/dist/core/secrets/audit.js +60 -0
  218. package/dist/core/secrets/audit.js.map +1 -0
  219. package/dist/core/secrets/auth-db-migration.d.ts +129 -0
  220. package/dist/core/secrets/auth-db-migration.js +653 -0
  221. package/dist/core/secrets/auth-db-migration.js.map +1 -0
  222. package/dist/core/secrets/bundle.d.ts +141 -0
  223. package/dist/core/secrets/bundle.js +435 -0
  224. package/dist/core/secrets/bundle.js.map +1 -0
  225. package/dist/core/secrets/dual-write.d.ts +81 -0
  226. package/dist/core/secrets/dual-write.js +247 -0
  227. package/dist/core/secrets/dual-write.js.map +1 -0
  228. package/dist/core/secrets/encryption.d.ts +44 -0
  229. package/dist/core/secrets/encryption.js +97 -0
  230. package/dist/core/secrets/encryption.js.map +1 -0
  231. package/dist/core/secrets/index.d.ts +49 -0
  232. package/dist/core/secrets/index.js +74 -0
  233. package/dist/core/secrets/index.js.map +1 -0
  234. package/dist/core/secrets/master-key.d.ts +38 -0
  235. package/dist/core/secrets/master-key.js +122 -0
  236. package/dist/core/secrets/master-key.js.map +1 -0
  237. package/dist/core/secrets/probes/anthropic.d.ts +98 -0
  238. package/dist/core/secrets/probes/anthropic.js +300 -0
  239. package/dist/core/secrets/probes/anthropic.js.map +1 -0
  240. package/dist/core/secrets/probes/brave.d.ts +9 -0
  241. package/dist/core/secrets/probes/brave.js +15 -0
  242. package/dist/core/secrets/probes/brave.js.map +1 -0
  243. package/dist/core/secrets/probes/r2.d.ts +15 -0
  244. package/dist/core/secrets/probes/r2.js +14 -0
  245. package/dist/core/secrets/probes/r2.js.map +1 -0
  246. package/dist/core/secrets/probes/tavily.d.ts +18 -0
  247. package/dist/core/secrets/probes/tavily.js +58 -0
  248. package/dist/core/secrets/probes/tavily.js.map +1 -0
  249. package/dist/core/secrets/probes/voyage.d.ts +13 -0
  250. package/dist/core/secrets/probes/voyage.js +52 -0
  251. package/dist/core/secrets/probes/voyage.js.map +1 -0
  252. package/dist/core/secrets/r2-structural-migration.d.ts +39 -0
  253. package/dist/core/secrets/r2-structural-migration.js +109 -0
  254. package/dist/core/secrets/r2-structural-migration.js.map +1 -0
  255. package/dist/core/secrets/read-flip.d.ts +59 -0
  256. package/dist/core/secrets/read-flip.js +87 -0
  257. package/dist/core/secrets/read-flip.js.map +1 -0
  258. package/dist/core/secrets/registry.d.ts +188 -0
  259. package/dist/core/secrets/registry.js +616 -0
  260. package/dist/core/secrets/registry.js.map +1 -0
  261. package/dist/core/types.d.ts +77 -483
  262. package/dist/core/types.js +25 -3
  263. package/dist/core/types.js.map +1 -1
  264. package/dist/core/user-config.d.ts +75 -12
  265. package/dist/core/user-config.js +155 -14
  266. package/dist/core/user-config.js.map +1 -1
  267. package/dist/core/validation.d.ts +786 -1373
  268. package/dist/core/validation.js +458 -147
  269. package/dist/core/validation.js.map +1 -1
  270. package/dist/core/workspace-manager.d.ts +87 -14
  271. package/dist/core/workspace-manager.js +222 -94
  272. package/dist/core/workspace-manager.js.map +1 -1
  273. package/dist/core/workspace.d.ts +19 -5
  274. package/dist/core/workspace.js +67 -39
  275. package/dist/core/workspace.js.map +1 -1
  276. package/dist/mcp/comment-virtual-entity.d.ts +36 -0
  277. package/dist/mcp/comment-virtual-entity.js +71 -0
  278. package/dist/mcp/comment-virtual-entity.js.map +1 -0
  279. package/dist/mcp/connector-manager.d.ts +72 -2
  280. package/dist/mcp/connector-manager.js +211 -32
  281. package/dist/mcp/connector-manager.js.map +1 -1
  282. package/dist/mcp/oauth-provider.d.ts +0 -5
  283. package/dist/mcp/oauth-provider.js +10 -22
  284. package/dist/mcp/oauth-provider.js.map +1 -1
  285. package/dist/mcp/oauth-registry.d.ts +46 -8
  286. package/dist/mcp/oauth-registry.js +52 -13
  287. package/dist/mcp/oauth-registry.js.map +1 -1
  288. package/dist/mcp/server-discovery.d.ts +73 -0
  289. package/dist/mcp/server-discovery.js +164 -0
  290. package/dist/mcp/server-discovery.js.map +1 -0
  291. package/dist/mcp/server.d.ts +24 -3
  292. package/dist/mcp/server.js +53 -10
  293. package/dist/mcp/server.js.map +1 -1
  294. package/dist/mcp/state-signer.d.ts +62 -0
  295. package/dist/mcp/state-signer.js +205 -0
  296. package/dist/mcp/state-signer.js.map +1 -0
  297. package/dist/mcp/stdio-proxy.d.ts +67 -0
  298. package/dist/mcp/stdio-proxy.js +149 -0
  299. package/dist/mcp/stdio-proxy.js.map +1 -0
  300. package/dist/mcp/tools.d.ts +65 -2
  301. package/dist/mcp/tools.js +1772 -124
  302. package/dist/mcp/tools.js.map +1 -1
  303. package/dist/server/__tests__/helpers/graph-api.d.ts +16 -0
  304. package/dist/server/__tests__/helpers/graph-api.js +16 -0
  305. package/dist/server/__tests__/helpers/graph-api.js.map +1 -0
  306. package/dist/server/artifact-asset-mint.d.ts +41 -0
  307. package/dist/server/artifact-asset-mint.js +59 -0
  308. package/dist/server/artifact-asset-mint.js.map +1 -0
  309. package/dist/server/asset-index-queue.d.ts +98 -0
  310. package/dist/server/asset-index-queue.js +193 -0
  311. package/dist/server/asset-index-queue.js.map +1 -0
  312. package/dist/server/body-write-coordinator.d.ts +161 -0
  313. package/dist/server/body-write-coordinator.js +182 -0
  314. package/dist/server/body-write-coordinator.js.map +1 -0
  315. package/dist/server/cloud-billing-flow.d.ts +32 -0
  316. package/dist/server/cloud-billing-flow.js +75 -0
  317. package/dist/server/cloud-billing-flow.js.map +1 -0
  318. package/dist/server/commit-audit.d.ts +26 -0
  319. package/dist/server/commit-audit.js +30 -0
  320. package/dist/server/commit-audit.js.map +1 -0
  321. package/dist/server/index.d.ts +29 -3
  322. package/dist/server/index.js +657 -269
  323. package/dist/server/index.js.map +1 -1
  324. package/dist/server/middleware/sanitize.d.ts +46 -0
  325. package/dist/server/middleware/sanitize.js +182 -0
  326. package/dist/server/middleware/sanitize.js.map +1 -0
  327. package/dist/server/routes/artifact-export-api.d.ts +11 -0
  328. package/dist/server/routes/artifact-export-api.js +159 -0
  329. package/dist/server/routes/artifact-export-api.js.map +1 -0
  330. package/dist/server/routes/asset-api.d.ts +76 -1
  331. package/dist/server/routes/asset-api.js +761 -61
  332. package/dist/server/routes/asset-api.js.map +1 -1
  333. package/dist/server/routes/audit-api.d.ts +24 -0
  334. package/dist/server/routes/audit-api.js +117 -0
  335. package/dist/server/routes/audit-api.js.map +1 -0
  336. package/dist/server/routes/auth-api.js +532 -63
  337. package/dist/server/routes/auth-api.js.map +1 -1
  338. package/dist/server/routes/capture-api.d.ts +10 -3
  339. package/dist/server/routes/capture-api.js +213 -25
  340. package/dist/server/routes/capture-api.js.map +1 -1
  341. package/dist/server/routes/chat-sessions-api.d.ts +13 -0
  342. package/dist/server/routes/chat-sessions-api.js +455 -0
  343. package/dist/server/routes/chat-sessions-api.js.map +1 -0
  344. package/dist/server/routes/chat.d.ts +35 -1
  345. package/dist/server/routes/chat.js +699 -54
  346. package/dist/server/routes/chat.js.map +1 -1
  347. package/dist/server/routes/comments-api.d.ts +15 -0
  348. package/dist/server/routes/comments-api.js +444 -0
  349. package/dist/server/routes/comments-api.js.map +1 -0
  350. package/dist/server/routes/config-api.d.ts +3 -2
  351. package/dist/server/routes/config-api.js +179 -49
  352. package/dist/server/routes/config-api.js.map +1 -1
  353. package/dist/server/routes/connectors-api.js +177 -42
  354. package/dist/server/routes/connectors-api.js.map +1 -1
  355. package/dist/server/routes/event-ingest-api.d.ts +15 -0
  356. package/dist/server/routes/{meeting-ingest-api.js → event-ingest-api.js} +35 -20
  357. package/dist/server/routes/event-ingest-api.js.map +1 -0
  358. package/dist/server/routes/field-library-api.d.ts +28 -0
  359. package/dist/server/routes/field-library-api.js +126 -0
  360. package/dist/server/routes/field-library-api.js.map +1 -0
  361. package/dist/server/routes/git-http.d.ts +2 -2
  362. package/dist/server/routes/git-http.js +86 -39
  363. package/dist/server/routes/git-http.js.map +1 -1
  364. package/dist/server/routes/graph-api-access.d.ts +57 -0
  365. package/dist/server/routes/graph-api-access.js +112 -0
  366. package/dist/server/routes/graph-api-access.js.map +1 -0
  367. package/dist/server/routes/graph-api.d.ts +1 -1
  368. package/dist/server/routes/graph-api.js +1455 -325
  369. package/dist/server/routes/graph-api.js.map +1 -1
  370. package/dist/server/routes/health.d.ts +18 -0
  371. package/dist/server/routes/health.js +74 -0
  372. package/dist/server/routes/health.js.map +1 -0
  373. package/dist/server/routes/import-batch.d.ts +24 -0
  374. package/dist/server/routes/import-batch.js +33 -0
  375. package/dist/server/routes/import-batch.js.map +1 -0
  376. package/dist/server/routes/insights-api.d.ts +9 -2
  377. package/dist/server/routes/insights-api.js +355 -61
  378. package/dist/server/routes/insights-api.js.map +1 -1
  379. package/dist/server/routes/mcp.d.ts +7 -3
  380. package/dist/server/routes/mcp.js +24 -8
  381. package/dist/server/routes/mcp.js.map +1 -1
  382. package/dist/server/routes/oauth-api.d.ts +67 -0
  383. package/dist/server/routes/oauth-api.js +421 -0
  384. package/dist/server/routes/oauth-api.js.map +1 -0
  385. package/dist/server/routes/permissions-api.d.ts +9 -2
  386. package/dist/server/routes/permissions-api.js +87 -31
  387. package/dist/server/routes/permissions-api.js.map +1 -1
  388. package/dist/server/routes/r2-api.d.ts +25 -0
  389. package/dist/server/routes/r2-api.js +276 -0
  390. package/dist/server/routes/r2-api.js.map +1 -0
  391. package/dist/server/routes/secrets-api.d.ts +36 -0
  392. package/dist/server/routes/secrets-api.js +308 -0
  393. package/dist/server/routes/secrets-api.js.map +1 -0
  394. package/dist/server/routes/settings-api.d.ts +29 -0
  395. package/dist/server/routes/settings-api.js +180 -0
  396. package/dist/server/routes/settings-api.js.map +1 -0
  397. package/dist/server/routes/share-api.d.ts +32 -0
  398. package/dist/server/routes/share-api.js +234 -0
  399. package/dist/server/routes/share-api.js.map +1 -0
  400. package/dist/server/routes/views-api.js +51 -0
  401. package/dist/server/routes/views-api.js.map +1 -1
  402. package/dist/server/routes/workspace-api.d.ts +2 -1
  403. package/dist/server/routes/workspace-api.js +142 -23
  404. package/dist/server/routes/workspace-api.js.map +1 -1
  405. package/dist/server/routes/ws.d.ts +3 -2
  406. package/dist/server/routes/ws.js +68 -17
  407. package/dist/server/routes/ws.js.map +1 -1
  408. package/dist/server/session-manager.d.ts +48 -5
  409. package/dist/server/session-manager.js +182 -14
  410. package/dist/server/session-manager.js.map +1 -1
  411. package/dist/server/ws-hub.d.ts +124 -37
  412. package/dist/server/ws-hub.js +0 -0
  413. package/dist/server/ws-hub.js.map +1 -1
  414. package/dist/server/yjs-manager.d.ts +278 -7
  415. package/dist/server/yjs-manager.js +830 -38
  416. package/dist/server/yjs-manager.js.map +1 -1
  417. package/dist/server/yjs-persistence.d.ts +165 -0
  418. package/dist/server/yjs-persistence.js +557 -0
  419. package/dist/server/yjs-persistence.js.map +1 -0
  420. package/dist/server/yjs-text-diff.d.ts +32 -0
  421. package/dist/server/yjs-text-diff.js +61 -0
  422. package/dist/server/yjs-text-diff.js.map +1 -0
  423. package/dist/services/access-control.d.ts +73 -0
  424. package/dist/services/access-control.js +165 -0
  425. package/dist/services/access-control.js.map +1 -0
  426. package/dist/services/artifact-asset-token.d.ts +69 -0
  427. package/dist/services/artifact-asset-token.js +94 -0
  428. package/dist/services/artifact-asset-token.js.map +1 -0
  429. package/dist/services/artifact-library-sources.d.ts +3 -0
  430. package/dist/services/artifact-library-sources.js +44 -0
  431. package/dist/services/artifact-library-sources.js.map +1 -0
  432. package/dist/services/artifact-render-export.d.ts +97 -0
  433. package/dist/services/artifact-render-export.js +467 -0
  434. package/dist/services/artifact-render-export.js.map +1 -0
  435. package/dist/services/asset-caption-service.d.ts +103 -0
  436. package/dist/services/asset-caption-service.js +186 -0
  437. package/dist/services/asset-caption-service.js.map +1 -0
  438. package/dist/services/asset-delete-authz.d.ts +31 -0
  439. package/dist/services/asset-delete-authz.js +61 -0
  440. package/dist/services/asset-delete-authz.js.map +1 -0
  441. package/dist/services/asset-finalize-service.d.ts +58 -0
  442. package/dist/services/asset-finalize-service.js +216 -0
  443. package/dist/services/asset-finalize-service.js.map +1 -0
  444. package/dist/services/asset-import-service.d.ts +111 -0
  445. package/dist/services/asset-import-service.js +394 -0
  446. package/dist/services/asset-import-service.js.map +1 -0
  447. package/dist/services/asset-index-sweep.d.ts +95 -0
  448. package/dist/services/asset-index-sweep.js +249 -0
  449. package/dist/services/asset-index-sweep.js.map +1 -0
  450. package/dist/services/asset-lifecycle-check.d.ts +22 -0
  451. package/dist/services/asset-lifecycle-check.js +80 -0
  452. package/dist/services/asset-lifecycle-check.js.map +1 -0
  453. package/dist/services/asset-listing.d.ts +72 -0
  454. package/dist/services/asset-listing.js +321 -0
  455. package/dist/services/asset-listing.js.map +1 -0
  456. package/dist/services/asset-presign-service.d.ts +68 -0
  457. package/dist/services/asset-presign-service.js +124 -0
  458. package/dist/services/asset-presign-service.js.map +1 -0
  459. package/dist/services/asset-sidecar-service.d.ts +28 -0
  460. package/dist/services/asset-sidecar-service.js +79 -0
  461. package/dist/services/asset-sidecar-service.js.map +1 -0
  462. package/dist/services/asset-upload-errors.d.ts +41 -0
  463. package/dist/services/asset-upload-errors.js +45 -0
  464. package/dist/services/asset-upload-errors.js.map +1 -0
  465. package/dist/services/asset-upload-service.d.ts +56 -0
  466. package/dist/services/asset-upload-service.js +200 -0
  467. package/dist/services/asset-upload-service.js.map +1 -0
  468. package/dist/services/asset-upload-token.d.ts +58 -0
  469. package/dist/services/asset-upload-token.js +75 -0
  470. package/dist/services/asset-upload-token.js.map +1 -0
  471. package/dist/services/assets/asset-index-service.d.ts +127 -0
  472. package/dist/services/assets/asset-index-service.js +227 -0
  473. package/dist/services/assets/asset-index-service.js.map +1 -0
  474. package/dist/services/assets/base.d.ts +128 -2
  475. package/dist/services/assets/base.js +98 -1
  476. package/dist/services/assets/base.js.map +1 -1
  477. package/dist/services/assets/index.d.ts +114 -1
  478. package/dist/services/assets/index.js +221 -0
  479. package/dist/services/assets/index.js.map +1 -1
  480. package/dist/services/assets/local.d.ts +16 -1
  481. package/dist/services/assets/local.js +110 -1
  482. package/dist/services/assets/local.js.map +1 -1
  483. package/dist/services/assets/r2-sync.d.ts +88 -0
  484. package/dist/services/assets/r2-sync.js +412 -0
  485. package/dist/services/assets/r2-sync.js.map +1 -0
  486. package/dist/services/assets/r2.d.ts +87 -1
  487. package/dist/services/assets/r2.js +203 -1
  488. package/dist/services/assets/r2.js.map +1 -1
  489. package/dist/services/auth-service.d.ts +312 -45
  490. package/dist/services/auth-service.js +1011 -195
  491. package/dist/services/auth-service.js.map +1 -1
  492. package/dist/services/chat-session-service.d.ts +188 -0
  493. package/dist/services/chat-session-service.js +512 -0
  494. package/dist/services/chat-session-service.js.map +1 -0
  495. package/dist/services/cloud-inference-billing.d.ts +64 -0
  496. package/dist/services/cloud-inference-billing.js +313 -0
  497. package/dist/services/cloud-inference-billing.js.map +1 -0
  498. package/dist/services/comment-service.d.ts +97 -0
  499. package/dist/services/comment-service.js +341 -0
  500. package/dist/services/comment-service.js.map +1 -0
  501. package/dist/services/comment-virtual-entity.d.ts +36 -0
  502. package/dist/services/comment-virtual-entity.js +71 -0
  503. package/dist/services/comment-virtual-entity.js.map +1 -0
  504. package/dist/services/convert-service.d.ts +64 -0
  505. package/dist/services/convert-service.js +68 -0
  506. package/dist/services/convert-service.js.map +1 -0
  507. package/dist/services/csv-service.d.ts +22 -17
  508. package/dist/services/csv-service.js +55 -92
  509. package/dist/services/csv-service.js.map +1 -1
  510. package/dist/services/entity-mutations.d.ts +213 -0
  511. package/dist/services/entity-mutations.js +380 -0
  512. package/dist/services/entity-mutations.js.map +1 -0
  513. package/dist/services/{meeting-processor.d.ts → event-processor.d.ts} +15 -8
  514. package/dist/services/{meeting-processor.js → event-processor.js} +124 -65
  515. package/dist/services/event-processor.js.map +1 -0
  516. package/dist/services/extract/docx.d.ts +1 -0
  517. package/dist/services/extract/docx.js +233 -0
  518. package/dist/services/extract/docx.js.map +1 -0
  519. package/dist/services/extract/index.d.ts +9 -0
  520. package/dist/services/extract/index.js +10 -0
  521. package/dist/services/extract/index.js.map +1 -0
  522. package/dist/services/extract/pdf.d.ts +13 -0
  523. package/dist/services/extract/pdf.js +69 -0
  524. package/dist/services/extract/pdf.js.map +1 -0
  525. package/dist/services/folder-creation.d.ts +33 -0
  526. package/dist/services/folder-creation.js +103 -0
  527. package/dist/services/folder-creation.js.map +1 -0
  528. package/dist/services/git.d.ts +58 -0
  529. package/dist/services/git.js +194 -55
  530. package/dist/services/git.js.map +1 -1
  531. package/dist/services/graph-maintenance.js +4 -4
  532. package/dist/services/graph-maintenance.js.map +1 -1
  533. package/dist/services/import-service.d.ts +51 -2
  534. package/dist/services/import-service.js +254 -107
  535. package/dist/services/import-service.js.map +1 -1
  536. package/dist/services/lint-service.js +10 -17
  537. package/dist/services/lint-service.js.map +1 -1
  538. package/dist/services/markdown.d.ts +12 -1
  539. package/dist/services/markdown.js +77 -9
  540. package/dist/services/markdown.js.map +1 -1
  541. package/dist/services/mention-detector.d.ts +23 -0
  542. package/dist/services/mention-detector.js +47 -0
  543. package/dist/services/mention-detector.js.map +1 -0
  544. package/dist/services/model-capabilities.d.ts +41 -0
  545. package/dist/services/model-capabilities.js +107 -0
  546. package/dist/services/model-capabilities.js.map +1 -0
  547. package/dist/services/notification-broadcast.d.ts +32 -0
  548. package/dist/services/notification-broadcast.js +38 -0
  549. package/dist/services/notification-broadcast.js.map +1 -0
  550. package/dist/services/notification-service.d.ts +41 -0
  551. package/dist/services/notification-service.js +100 -0
  552. package/dist/services/notification-service.js.map +1 -0
  553. package/dist/services/oauth-server-store.d.ts +147 -0
  554. package/dist/services/oauth-server-store.js +319 -0
  555. package/dist/services/oauth-server-store.js.map +1 -0
  556. package/dist/services/orphan-service.js +2 -2
  557. package/dist/services/orphan-service.js.map +1 -1
  558. package/dist/services/personal-folder.d.ts +40 -0
  559. package/dist/services/personal-folder.js +101 -0
  560. package/dist/services/personal-folder.js.map +1 -0
  561. package/dist/services/scratch-asset-promote.d.ts +57 -0
  562. package/dist/services/scratch-asset-promote.js +95 -0
  563. package/dist/services/scratch-asset-promote.js.map +1 -0
  564. package/dist/services/scratch-attachment-service.d.ts +196 -0
  565. package/dist/services/scratch-attachment-service.js +347 -0
  566. package/dist/services/scratch-attachment-service.js.map +1 -0
  567. package/dist/services/share-link-service.d.ts +57 -0
  568. package/dist/services/share-link-service.js +142 -0
  569. package/dist/services/share-link-service.js.map +1 -0
  570. package/dist/services/user-person-bridge.d.ts +136 -0
  571. package/dist/services/user-person-bridge.js +340 -0
  572. package/dist/services/user-person-bridge.js.map +1 -0
  573. package/dist/services/vector-service.d.ts +282 -5
  574. package/dist/services/vector-service.js +585 -29
  575. package/dist/services/vector-service.js.map +1 -1
  576. package/dist/services/workspace-access.d.ts +108 -0
  577. package/dist/services/workspace-access.js +149 -0
  578. package/dist/services/workspace-access.js.map +1 -0
  579. package/dist/services/workspace-assets-folder.d.ts +46 -0
  580. package/dist/services/workspace-assets-folder.js +75 -0
  581. package/dist/services/workspace-assets-folder.js.map +1 -0
  582. package/dist/utils/git.d.ts +17 -2
  583. package/dist/utils/git.js +23 -7
  584. package/dist/utils/git.js.map +1 -1
  585. package/dist/utils/log.d.ts +42 -0
  586. package/dist/utils/log.js +137 -0
  587. package/dist/utils/log.js.map +1 -0
  588. package/dist/utils/preflight.js +2 -2
  589. package/dist/utils/preflight.js.map +1 -1
  590. package/dist/utils/version-checker.d.ts +12 -19
  591. package/dist/utils/version-checker.js +14 -104
  592. package/dist/utils/version-checker.js.map +1 -1
  593. package/dist/web/_app/immutable/assets/0.Bs_z3Z86.css +2 -0
  594. package/dist/web/_app/immutable/assets/12.DlIf1mKh.css +1 -0
  595. package/dist/web/_app/immutable/assets/14.7d-Q37wc.css +1 -0
  596. package/dist/web/_app/immutable/assets/15.BwBFdc1D.css +1 -0
  597. package/dist/web/_app/immutable/assets/16.CmLrbzZp.css +1 -0
  598. package/dist/web/_app/immutable/assets/17.DcuK5ZVm.css +1 -0
  599. package/dist/web/_app/immutable/assets/2.DATnR31c.css +1 -0
  600. package/dist/web/_app/immutable/assets/3.Cz4H8n_O.css +1 -0
  601. package/dist/web/_app/immutable/assets/4.aDHteTDy.css +1 -0
  602. package/dist/web/_app/immutable/assets/6.CF1i5Bon.css +1 -0
  603. package/dist/web/_app/immutable/assets/ArtifactEntityView.C4ooklFD.css +1 -0
  604. package/dist/web/_app/immutable/assets/AssetLibraryPicker.BOFW0MvF.css +1 -0
  605. package/dist/web/_app/immutable/assets/BulkActionsBar.cmKvHzRK.css +1 -0
  606. package/dist/web/_app/immutable/assets/CalendarView.lBCvS3X5.css +1 -0
  607. package/dist/web/_app/immutable/assets/CodeMirrorEditor.D6hf2pNJ.css +1 -0
  608. package/dist/web/_app/immutable/assets/CopyField.DVGZtw4U.css +1 -0
  609. package/dist/web/_app/immutable/assets/FolderMembersPanel.kiYnDFHu.css +1 -0
  610. package/dist/web/_app/immutable/assets/FolderMutationDialogs.BMRF6Z3z.css +1 -0
  611. package/dist/web/_app/immutable/assets/FolderPicker.Cp46GHe2.css +1 -0
  612. package/dist/web/_app/immutable/assets/GalleryView.WBGxxUQc.css +1 -0
  613. package/dist/web/_app/immutable/assets/GraphView.BJtGL9py.css +1 -0
  614. package/dist/web/_app/immutable/assets/KanbanView.BOaI5KFL.css +1 -0
  615. package/dist/web/_app/immutable/assets/Link.vV0ne105.css +1 -0
  616. package/dist/web/_app/immutable/assets/Markdown.BZnsSOSf.css +1 -0
  617. package/dist/web/_app/immutable/assets/Rail.ilusFgD4.css +1 -0
  618. package/dist/web/_app/immutable/assets/SettingsDialog.D7-BO6S_.css +1 -0
  619. package/dist/web/_app/immutable/assets/Spinner.UBfl0pab.css +1 -0
  620. package/dist/web/_app/immutable/assets/StopFilledAlt.fNlDN65D.css +1 -0
  621. package/dist/web/_app/immutable/assets/TimelineView.xAZBJhHw.css +1 -0
  622. package/dist/web/_app/immutable/assets/WorkspaceView.C8ErUJAY.css +1 -0
  623. package/dist/web/_app/immutable/assets/button.BlLm4Dg1.css +1 -0
  624. package/dist/web/_app/immutable/assets/history-summary.LpmHXasl.css +1 -0
  625. package/dist/web/_app/immutable/assets/ibm-plex-mono-latin-400-normal.CvHOgSBP.woff +0 -0
  626. package/dist/web/_app/immutable/assets/ibm-plex-mono-latin-400-normal.DMJ8VG8y.woff2 +0 -0
  627. package/dist/web/_app/immutable/assets/ibm-plex-mono-latin-500-normal.CB9ihrfo.woff +0 -0
  628. package/dist/web/_app/immutable/assets/ibm-plex-mono-latin-500-normal.DSY6xOcd.woff2 +0 -0
  629. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-400-italic.CZTNEAuW.woff2 +0 -0
  630. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-400-italic.CsGl1sm0.woff +0 -0
  631. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-400-normal.CDDApCn2.woff2 +0 -0
  632. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-400-normal.CYLoc0-x.woff +0 -0
  633. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-500-italic.BNK2_mGO.woff2 +0 -0
  634. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-500-italic.DpEwFAQM.woff +0 -0
  635. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-500-normal.6ng42L7E.woff2 +0 -0
  636. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-500-normal.BgVn5rGT.woff +0 -0
  637. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-600-normal.Cu4Hd6ag.woff +0 -0
  638. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-600-normal.CuJfVYMP.woff2 +0 -0
  639. package/dist/web/_app/immutable/assets/inline-list.cqga56xT.css +1 -0
  640. package/dist/web/_app/immutable/assets/list-columns.BgnafZ4K.css +1 -0
  641. package/dist/web/_app/immutable/assets/realtime.DDCWxn3B.css +1 -0
  642. package/dist/web/_app/immutable/assets/select.B90KUqR4.css +1 -0
  643. package/dist/web/_app/immutable/assets/sheet.RF9RBmml.css +1 -0
  644. package/dist/web/_app/immutable/chunks/22qYtvr82.js +1 -0
  645. package/dist/web/_app/immutable/chunks/2f1VclTm.js +1 -0
  646. package/dist/web/_app/immutable/chunks/4xgUPoGj2.js +2 -0
  647. package/dist/web/_app/immutable/chunks/5CYQk4xR.js +1 -0
  648. package/dist/web/_app/immutable/chunks/6S9WOky52.js +1 -0
  649. package/dist/web/_app/immutable/chunks/9K8VREGP.js +1 -0
  650. package/dist/web/_app/immutable/chunks/B-n5q6ku.js +1 -0
  651. package/dist/web/_app/immutable/chunks/B0p2fSEV2.js +1 -0
  652. package/dist/web/_app/immutable/chunks/B2oi87jG.js +2 -0
  653. package/dist/web/_app/immutable/chunks/B5Rb52QU.js +1 -0
  654. package/dist/web/_app/immutable/chunks/B6Lt-qaJ.js +1 -0
  655. package/dist/web/_app/immutable/chunks/B6O-q2CN.js +1 -0
  656. package/dist/web/_app/immutable/chunks/B6qeIQZz.js +2 -0
  657. package/dist/web/_app/immutable/chunks/B9KfEsQw2.js +1 -0
  658. package/dist/web/_app/immutable/chunks/BB-RSeXQ.js +1 -0
  659. package/dist/web/_app/immutable/chunks/BCQQzpbj.js +1 -0
  660. package/dist/web/_app/immutable/chunks/BGdzd-Tc.js +1 -0
  661. package/dist/web/_app/immutable/chunks/BLs1h0Rh2.js +1 -0
  662. package/dist/web/_app/immutable/chunks/BPzV_xOS2.js +1 -0
  663. package/dist/web/_app/immutable/chunks/BT2C6q8n.js +1 -0
  664. package/dist/web/_app/immutable/chunks/BUesbLq2.js +1 -0
  665. package/dist/web/_app/immutable/chunks/BUrnwOw4.js +2 -0
  666. package/dist/web/_app/immutable/chunks/BV-iCKqa.js +1 -0
  667. package/dist/web/_app/immutable/chunks/BVCJtXyc.js +1 -0
  668. package/dist/web/_app/immutable/chunks/BY9a3GSt.js +6 -0
  669. package/dist/web/_app/immutable/chunks/BZi3uKF4.js +1 -0
  670. package/dist/web/_app/immutable/chunks/BbfHGU9n2.js +1 -0
  671. package/dist/web/_app/immutable/chunks/Bbqu2-5G2.js +1 -0
  672. package/dist/web/_app/immutable/chunks/BbsX1Slr.js +3 -0
  673. package/dist/web/_app/immutable/chunks/BcwjqHaZ2.js +1 -0
  674. package/dist/web/_app/immutable/chunks/BizeEpTx.js +2 -0
  675. package/dist/web/_app/immutable/chunks/BlgjVmFM2.js +14 -0
  676. package/dist/web/_app/immutable/chunks/BocSRgv12.js +1 -0
  677. package/dist/web/_app/immutable/chunks/BojBYqc3.js +1 -0
  678. package/dist/web/_app/immutable/chunks/BpCG9m962.js +1 -0
  679. package/dist/web/_app/immutable/chunks/BqvnBg_y.js +1 -0
  680. package/dist/web/_app/immutable/chunks/BsT0J1QD.js +1 -0
  681. package/dist/web/_app/immutable/chunks/BsW72fyR2.js +1 -0
  682. package/dist/web/_app/immutable/chunks/Bti_XKwx.js +2692 -0
  683. package/dist/web/_app/immutable/chunks/Bua97Had2.js +2 -0
  684. package/dist/web/_app/immutable/chunks/BuhW5Fr0.js +689 -0
  685. package/dist/web/_app/immutable/chunks/Bx74EEyn2.js +1 -0
  686. package/dist/web/_app/immutable/chunks/BxPN0T61.js +1 -0
  687. package/dist/web/_app/immutable/chunks/Bz9lrgGs.js +1 -0
  688. package/dist/web/_app/immutable/chunks/BzaxZ6j12.js +1 -0
  689. package/dist/web/_app/immutable/chunks/C1B4xDs0.js +1 -0
  690. package/dist/web/_app/immutable/chunks/C1trcC8M.js +7 -0
  691. package/dist/web/_app/immutable/chunks/C9tvkk7Y2.js +22 -0
  692. package/dist/web/_app/immutable/chunks/CBQjdDZf.js +1 -0
  693. package/dist/web/_app/immutable/chunks/CDeazMFW2.js +2 -0
  694. package/dist/web/_app/immutable/chunks/CEahbtnP.js +1 -0
  695. package/dist/web/_app/immutable/chunks/CGjVCd0e.js +8 -0
  696. package/dist/web/_app/immutable/chunks/CICK9maP.js +1 -0
  697. package/dist/web/_app/immutable/chunks/CIoNEHQ82.js +184 -0
  698. package/dist/web/_app/immutable/chunks/CP8_U450.js +1 -0
  699. package/dist/web/_app/immutable/chunks/CQ1eQ93t2.js +6 -0
  700. package/dist/web/_app/immutable/chunks/CUFQMEBn2.js +1 -0
  701. package/dist/web/_app/immutable/chunks/CYEYDNwO2.js +1 -0
  702. package/dist/web/_app/immutable/chunks/CYyIP7kP2.js +66 -0
  703. package/dist/web/_app/immutable/chunks/CZgFaimi2.js +83 -0
  704. package/dist/web/_app/immutable/chunks/CZqvCTCv.js +1 -0
  705. package/dist/web/_app/immutable/chunks/CcI9jYBV.js +1 -0
  706. package/dist/web/_app/immutable/chunks/CcWaWbrI2.js +1 -0
  707. package/dist/web/_app/immutable/chunks/CdtzRVZK.js +1 -0
  708. package/dist/web/_app/immutable/chunks/CfYG72Hc.js +1 -0
  709. package/dist/web/_app/immutable/chunks/Ch1kmm3J2.js +1 -0
  710. package/dist/web/_app/immutable/chunks/CmSSN61R2.js +1 -0
  711. package/dist/web/_app/immutable/chunks/Cqm5wAin.js +1 -0
  712. package/dist/web/_app/immutable/chunks/CuYPXpWU.js +1 -0
  713. package/dist/web/_app/immutable/chunks/CvSEodTA.js +1 -0
  714. package/dist/web/_app/immutable/chunks/CvqqrTrj.js +1 -0
  715. package/dist/web/_app/immutable/chunks/CwAmWt6h.js +1 -0
  716. package/dist/web/_app/immutable/chunks/CwP3Oa7F.js +1 -0
  717. package/dist/web/_app/immutable/chunks/CyTluew1.js +2 -0
  718. package/dist/web/_app/immutable/chunks/CzXdOgVl2.js +7 -0
  719. package/dist/web/_app/immutable/chunks/D0IAituJ.js +1 -0
  720. package/dist/web/_app/immutable/chunks/D0W7c6Sg.js +185 -0
  721. package/dist/web/_app/immutable/chunks/D0rBhnqB2.js +1 -0
  722. package/dist/web/_app/immutable/chunks/D1-GZSN1.js +1 -0
  723. package/dist/web/_app/immutable/chunks/D1yvtriI.js +1 -0
  724. package/dist/web/_app/immutable/chunks/D25NMRpl.js +1 -0
  725. package/dist/web/_app/immutable/chunks/D8wZ2xul.js +4 -0
  726. package/dist/web/_app/immutable/chunks/DA3HEX3X.js +1 -0
  727. package/dist/web/_app/immutable/chunks/DD9wj4ca2.js +1 -0
  728. package/dist/web/_app/immutable/chunks/DJ1YeiJL2.js +1 -0
  729. package/dist/web/_app/immutable/chunks/DKFZnpVC.js +5 -0
  730. package/dist/web/_app/immutable/chunks/DKJl40hl.js +1 -0
  731. package/dist/web/_app/immutable/chunks/DPUVu0T0.js +1 -0
  732. package/dist/web/_app/immutable/chunks/DQMWa2t7.js +1 -0
  733. package/dist/web/_app/immutable/chunks/DXsFrDer2.js +1 -0
  734. package/dist/web/_app/immutable/chunks/DYc5KW3F.js +1 -0
  735. package/dist/web/_app/immutable/chunks/DZcTwXBW.js +1 -0
  736. package/dist/web/_app/immutable/chunks/DcPrzUMJ.js +1 -0
  737. package/dist/web/_app/immutable/chunks/DdPO8kRu2.js +2 -0
  738. package/dist/web/_app/immutable/chunks/DeOXyJZw2.js +1 -0
  739. package/dist/web/_app/immutable/chunks/DfufUFR1.js +2 -0
  740. package/dist/web/_app/immutable/chunks/DiQg3ing.js +1 -0
  741. package/dist/web/_app/immutable/chunks/DjIqa1_y.js +2 -0
  742. package/dist/web/_app/immutable/chunks/DkJ1dkxV2.js +1 -0
  743. package/dist/web/_app/immutable/chunks/Dl63IUnH2.js +224 -0
  744. package/dist/web/_app/immutable/chunks/DmFdD6zT.js +1 -0
  745. package/dist/web/_app/immutable/chunks/Dn3R8S-U.js +1 -0
  746. package/dist/web/_app/immutable/chunks/DpNr1GQ2.js +1 -0
  747. package/dist/web/_app/immutable/chunks/DpVNDHXz.js +2 -0
  748. package/dist/web/_app/immutable/chunks/DuxziNnw.js +1 -0
  749. package/dist/web/_app/immutable/chunks/Dv3A3Wcj.js +1 -0
  750. package/dist/web/_app/immutable/chunks/FuiJnZG0.js +1 -0
  751. package/dist/web/_app/immutable/chunks/Izkulod7.js +1 -0
  752. package/dist/web/_app/immutable/chunks/KZY6ongq2.js +2 -0
  753. package/dist/web/_app/immutable/chunks/LYL8znYR2.js +23 -0
  754. package/dist/web/_app/immutable/chunks/PhN6tXuJ2.js +1 -0
  755. package/dist/web/_app/immutable/chunks/PyLyCpuL.js +1 -0
  756. package/dist/web/_app/immutable/chunks/RK4gCWN0.js +2 -0
  757. package/dist/web/_app/immutable/chunks/VsBngTf4.js +2 -0
  758. package/dist/web/_app/immutable/chunks/XcWejFm_.js +5 -0
  759. package/dist/web/_app/immutable/chunks/al_nidE9.js +1 -0
  760. package/dist/web/_app/immutable/chunks/e50-izZO2.js +1 -0
  761. package/dist/web/_app/immutable/chunks/hG-QCarB.js +1 -0
  762. package/dist/web/_app/immutable/chunks/hStvCHkX.js +1 -0
  763. package/dist/web/_app/immutable/chunks/iJVY9p0y.js +3 -0
  764. package/dist/web/_app/immutable/chunks/kNaey6uv.js +1 -0
  765. package/dist/web/_app/immutable/chunks/pIXNe0C1.js +1 -0
  766. package/dist/web/_app/immutable/chunks/pNSRq_1B.js +2 -0
  767. package/dist/web/_app/immutable/chunks/qwr5boV8.js +1 -0
  768. package/dist/web/_app/immutable/chunks/rjjyuGhb.js +1 -0
  769. package/dist/web/_app/immutable/chunks/sg-T-FoN.js +1 -0
  770. package/dist/web/_app/immutable/chunks/uBgoVlsx.js +3 -0
  771. package/dist/web/_app/immutable/chunks/ulshNz6x.js +2 -0
  772. package/dist/web/_app/immutable/chunks/xY0c-UfG.js +3 -0
  773. package/dist/web/_app/immutable/chunks/xeNvx4Bl2.js +1 -0
  774. package/dist/web/_app/immutable/chunks/xihTtKlq.js +1 -0
  775. package/dist/web/_app/immutable/entry/app.pNUW-q4U.js +2 -0
  776. package/dist/web/_app/immutable/entry/start.DxGtIGG_.js +1 -0
  777. package/dist/web/_app/immutable/nodes/0.D6ZCY_R6.js +2 -0
  778. package/dist/web/_app/immutable/nodes/1.BryH3TNr.js +1 -0
  779. package/dist/web/_app/immutable/nodes/10.CMZMvR17.js +1 -0
  780. package/dist/web/_app/immutable/nodes/11.QHx_JShx.js +1 -0
  781. package/dist/web/_app/immutable/nodes/12.CiRWbb4X.js +1 -0
  782. package/dist/web/_app/immutable/nodes/13.CYrHXCA8.js +1 -0
  783. package/dist/web/_app/immutable/nodes/14.K18eIE5Z.js +1 -0
  784. package/dist/web/_app/immutable/nodes/15.DxgnZ61h.js +1 -0
  785. package/dist/web/_app/immutable/nodes/16.Dn6iuWIo.js +1 -0
  786. package/dist/web/_app/immutable/nodes/17.viwS7vTR.js +77 -0
  787. package/dist/web/_app/immutable/nodes/2.BQkQ_uUM.js +122 -0
  788. package/dist/web/_app/immutable/nodes/3.B4pqXsqq.js +6 -0
  789. package/dist/web/_app/immutable/nodes/4.CKXEgIAP.js +11 -0
  790. package/dist/web/_app/immutable/nodes/5.A2TASl4Q.js +1 -0
  791. package/dist/web/_app/immutable/nodes/6.DaIRO3SI.js +6 -0
  792. package/dist/web/_app/immutable/nodes/7.ga2UvFm-.js +1 -0
  793. package/dist/web/_app/immutable/nodes/8.BTEWT81u.js +1 -0
  794. package/dist/web/_app/immutable/nodes/9.BVxo0E0c.js +1 -0
  795. package/dist/web/_app/version.json +1 -1
  796. package/dist/web/favicon-16.png +0 -0
  797. package/dist/web/favicon-180.png +0 -0
  798. package/dist/web/favicon-32.png +0 -0
  799. package/dist/web/favicon-48.png +0 -0
  800. package/dist/web/favicon-512.png +0 -0
  801. package/dist/web/favicon.ico +0 -0
  802. package/dist/web/favicon.svg +8 -0
  803. package/dist/web/index.html +56 -21
  804. package/dist/web/studiograph-logomark.svg +29 -0
  805. package/package.json +35 -14
  806. package/dist/agent/skills/enrich-entities.md +0 -136
  807. package/dist/agent/skills/sync-configuration.md +0 -119
  808. package/dist/agent/skills/sync-setup.md +0 -82
  809. package/dist/agent/tools/message-tools.d.ts +0 -42
  810. package/dist/agent/tools/message-tools.js +0 -106
  811. package/dist/agent/tools/message-tools.js.map +0 -1
  812. package/dist/cli/commands/app.d.ts +0 -7
  813. package/dist/cli/commands/app.js +0 -268
  814. package/dist/cli/commands/app.js.map +0 -1
  815. package/dist/cli/commands/clear.d.ts +0 -5
  816. package/dist/cli/commands/clear.js +0 -36
  817. package/dist/cli/commands/clear.js.map +0 -1
  818. package/dist/cli/commands/collection.d.ts +0 -10
  819. package/dist/cli/commands/collection.js.map +0 -1
  820. package/dist/cli/commands/join.d.ts +0 -9
  821. package/dist/cli/commands/join.js +0 -255
  822. package/dist/cli/commands/join.js.map +0 -1
  823. package/dist/cli/commands/start.d.ts +0 -7
  824. package/dist/cli/commands/start.js +0 -584
  825. package/dist/cli/commands/start.js.map +0 -1
  826. package/dist/cli/commands/update.d.ts +0 -8
  827. package/dist/cli/commands/update.js +0 -155
  828. package/dist/cli/commands/update.js.map +0 -1
  829. package/dist/cli/commands/user.d.ts +0 -7
  830. package/dist/cli/commands/user.js +0 -175
  831. package/dist/cli/commands/user.js.map +0 -1
  832. package/dist/cli/scaffolding.d.ts +0 -12
  833. package/dist/cli/scaffolding.js +0 -397
  834. package/dist/cli/scaffolding.js.map +0 -1
  835. package/dist/integrations/registry.d.ts +0 -8
  836. package/dist/integrations/registry.js +0 -7
  837. package/dist/integrations/registry.js.map +0 -1
  838. package/dist/integrations/types.d.ts +0 -43
  839. package/dist/integrations/types.js +0 -5
  840. package/dist/integrations/types.js.map +0 -1
  841. package/dist/lib/lib/utils.d.ts +0 -2
  842. package/dist/lib/lib/utils.js +0 -6
  843. package/dist/lib/lib/utils.js.map +0 -1
  844. package/dist/server/chrome/chrome.css +0 -691
  845. package/dist/server/chrome/chrome.js +0 -374
  846. package/dist/server/commit-scheduler.d.ts +0 -39
  847. package/dist/server/commit-scheduler.js +0 -113
  848. package/dist/server/commit-scheduler.js.map +0 -1
  849. package/dist/server/plugin-loader.d.ts +0 -53
  850. package/dist/server/plugin-loader.js +0 -155
  851. package/dist/server/plugin-loader.js.map +0 -1
  852. package/dist/server/routes/meeting-ingest-api.d.ts +0 -14
  853. package/dist/server/routes/meeting-ingest-api.js.map +0 -1
  854. package/dist/server/routes/messages-api.d.ts +0 -15
  855. package/dist/server/routes/messages-api.js +0 -385
  856. package/dist/server/routes/messages-api.js.map +0 -1
  857. package/dist/services/batch-processor.d.ts +0 -49
  858. package/dist/services/batch-processor.js +0 -166
  859. package/dist/services/batch-processor.js.map +0 -1
  860. package/dist/services/briefing.d.ts +0 -26
  861. package/dist/services/briefing.js +0 -230
  862. package/dist/services/briefing.js.map +0 -1
  863. package/dist/services/heartbeat.d.ts +0 -28
  864. package/dist/services/heartbeat.js +0 -183
  865. package/dist/services/heartbeat.js.map +0 -1
  866. package/dist/services/image-import-service.d.ts +0 -24
  867. package/dist/services/image-import-service.js +0 -108
  868. package/dist/services/image-import-service.js.map +0 -1
  869. package/dist/services/insights.d.ts +0 -38
  870. package/dist/services/insights.js +0 -269
  871. package/dist/services/insights.js.map +0 -1
  872. package/dist/services/meeting-processor.js.map +0 -1
  873. package/dist/services/message-service.d.ts +0 -68
  874. package/dist/services/message-service.js +0 -220
  875. package/dist/services/message-service.js.map +0 -1
  876. package/dist/utils/workspace-config.d.ts +0 -8
  877. package/dist/utils/workspace-config.js +0 -22
  878. package/dist/utils/workspace-config.js.map +0 -1
  879. package/dist/web/_app/immutable/assets/0.uPSqtsC5.css +0 -1
  880. package/dist/web/_app/immutable/assets/11.2jzI3cZY.css +0 -1
  881. package/dist/web/_app/immutable/assets/12.CT4xL4K3.css +0 -1
  882. package/dist/web/_app/immutable/assets/13.BUrHkYry.css +0 -1
  883. package/dist/web/_app/immutable/assets/2.DpQWr6q6.css +0 -1
  884. package/dist/web/_app/immutable/assets/3.BxdqT7zi.css +0 -1
  885. package/dist/web/_app/immutable/assets/4.DdNqjd0T.css +0 -1
  886. package/dist/web/_app/immutable/assets/5.DFeGxLYf.css +0 -1
  887. package/dist/web/_app/immutable/assets/6.DXZr_rI_.css +0 -1
  888. package/dist/web/_app/immutable/assets/GraphView.D9ePpZez.css +0 -1
  889. package/dist/web/_app/immutable/assets/Toaster.rN8r_Hzv.css +0 -1
  890. package/dist/web/_app/immutable/assets/ViewToolbar.BWE03S64.css +0 -1
  891. package/dist/web/_app/immutable/assets/WorkspaceView.CgGDi_Zb.css +0 -1
  892. package/dist/web/_app/immutable/assets/wikilinks.CzMBkUMB.css +0 -1
  893. package/dist/web/_app/immutable/chunks/-jG4Obyh.js +0 -4
  894. package/dist/web/_app/immutable/chunks/2nrc8f_O.js +0 -1
  895. package/dist/web/_app/immutable/chunks/7S2LGqoP.js +0 -2
  896. package/dist/web/_app/immutable/chunks/8Q3ZJIYu.js +0 -1
  897. package/dist/web/_app/immutable/chunks/B8ydT5xX.js +0 -2
  898. package/dist/web/_app/immutable/chunks/BB_5th5W.js +0 -3383
  899. package/dist/web/_app/immutable/chunks/BEn6N5c2.js +0 -1
  900. package/dist/web/_app/immutable/chunks/BFNNbCAM.js +0 -1
  901. package/dist/web/_app/immutable/chunks/BFZZEUd5.js +0 -1
  902. package/dist/web/_app/immutable/chunks/BGk8kfOE.js +0 -2
  903. package/dist/web/_app/immutable/chunks/BIBCr278.js +0 -1
  904. package/dist/web/_app/immutable/chunks/BPCkkwqe.js +0 -1
  905. package/dist/web/_app/immutable/chunks/BhlvzGQx.js +0 -1
  906. package/dist/web/_app/immutable/chunks/BlMknQeF.js +0 -1
  907. package/dist/web/_app/immutable/chunks/BpjqrlCg.js +0 -1
  908. package/dist/web/_app/immutable/chunks/BrebtJNG.js +0 -18
  909. package/dist/web/_app/immutable/chunks/ByBZ7JIT.js +0 -1
  910. package/dist/web/_app/immutable/chunks/Bz0ZjVQE.js +0 -2
  911. package/dist/web/_app/immutable/chunks/BzJTZOK2.js +0 -1
  912. package/dist/web/_app/immutable/chunks/C0LVZhvn.js +0 -1
  913. package/dist/web/_app/immutable/chunks/C3uBrOS5.js +0 -1
  914. package/dist/web/_app/immutable/chunks/C8R1sDpW.js +0 -1
  915. package/dist/web/_app/immutable/chunks/CDRHrs0g.js +0 -1
  916. package/dist/web/_app/immutable/chunks/CN5_py1I.js +0 -1
  917. package/dist/web/_app/immutable/chunks/CPne482a.js +0 -1
  918. package/dist/web/_app/immutable/chunks/CSVbGg_b.js +0 -5
  919. package/dist/web/_app/immutable/chunks/CWyU-seV.js +0 -1
  920. package/dist/web/_app/immutable/chunks/CX_61fY8.js +0 -5
  921. package/dist/web/_app/immutable/chunks/CYrVHOHA.js +0 -1
  922. package/dist/web/_app/immutable/chunks/ChsiIm-E.js +0 -1
  923. package/dist/web/_app/immutable/chunks/CojKppbh.js +0 -1
  924. package/dist/web/_app/immutable/chunks/CqkleIqs.js +0 -1
  925. package/dist/web/_app/immutable/chunks/CtYLtD7K.js +0 -1
  926. package/dist/web/_app/immutable/chunks/D-HDy5qS.js +0 -1
  927. package/dist/web/_app/immutable/chunks/D5aIYSkd.js +0 -1
  928. package/dist/web/_app/immutable/chunks/DBBXARVf.js +0 -1
  929. package/dist/web/_app/immutable/chunks/DFYOFE3o.js +0 -1
  930. package/dist/web/_app/immutable/chunks/DKaafS50.js +0 -1
  931. package/dist/web/_app/immutable/chunks/DUMOo1Pn.js +0 -1
  932. package/dist/web/_app/immutable/chunks/DWX9f6AF.js +0 -1
  933. package/dist/web/_app/immutable/chunks/DY8-EONP.js +0 -1
  934. package/dist/web/_app/immutable/chunks/DkoHPElM.js +0 -1
  935. package/dist/web/_app/immutable/chunks/DnL9f0lc.js +0 -7
  936. package/dist/web/_app/immutable/chunks/DsnmJJEf.js +0 -1
  937. package/dist/web/_app/immutable/chunks/DujahU3W.js +0 -1
  938. package/dist/web/_app/immutable/chunks/FBn-ES5k.js +0 -1
  939. package/dist/web/_app/immutable/chunks/HQpwSOb3.js +0 -1
  940. package/dist/web/_app/immutable/chunks/KyjzlWRN.js +0 -2
  941. package/dist/web/_app/immutable/chunks/PPVm8Dsz.js +0 -1
  942. package/dist/web/_app/immutable/chunks/UwYfmrPx.js +0 -2
  943. package/dist/web/_app/immutable/chunks/W25ZVI8L.js +0 -2
  944. package/dist/web/_app/immutable/chunks/WSUKABI_.js +0 -1
  945. package/dist/web/_app/immutable/chunks/XjsuEnNE.js +0 -5
  946. package/dist/web/_app/immutable/chunks/ZD6ARAtb.js +0 -1
  947. package/dist/web/_app/immutable/chunks/pBTf4stg.js +0 -2
  948. package/dist/web/_app/immutable/chunks/rGIarcnp.js +0 -1
  949. package/dist/web/_app/immutable/chunks/vxEtkL8z.js +0 -1
  950. package/dist/web/_app/immutable/chunks/wDIGUaoU.js +0 -3
  951. package/dist/web/_app/immutable/entry/app.ZHiTM8WS.js +0 -2
  952. package/dist/web/_app/immutable/entry/start.C9-FfAVc.js +0 -1
  953. package/dist/web/_app/immutable/nodes/0.D92Orz8k.js +0 -2
  954. package/dist/web/_app/immutable/nodes/1.BMdyglM_.js +0 -1
  955. package/dist/web/_app/immutable/nodes/10.BilVDHSL.js +0 -1
  956. package/dist/web/_app/immutable/nodes/11.CS9E0Hic.js +0 -1
  957. package/dist/web/_app/immutable/nodes/12.D3BOxYzG.js +0 -1
  958. package/dist/web/_app/immutable/nodes/13.CClMKYtN.js +0 -1
  959. package/dist/web/_app/immutable/nodes/2.C8KKElPL.js +0 -267
  960. package/dist/web/_app/immutable/nodes/3.C5n5uiWU.js +0 -5
  961. package/dist/web/_app/immutable/nodes/4.DcGVvgiL.js +0 -11
  962. package/dist/web/_app/immutable/nodes/5.DxIjnFQO.js +0 -4
  963. package/dist/web/_app/immutable/nodes/6.lkh6kKLj.js +0 -1
  964. package/dist/web/_app/immutable/nodes/7.Bg7SbfhA.js +0 -1
  965. package/dist/web/_app/immutable/nodes/8.DcXWCNBE.js +0 -1
  966. package/dist/web/_app/immutable/nodes/9.Cefi8Jfm.js +0 -1
package/dist/mcp/tools.js CHANGED
@@ -5,11 +5,82 @@
5
5
  * methods — no new business logic. Results are returned as JSON text so the
6
6
  * MCP client (Claude Desktop) can process structured data.
7
7
  *
8
+ * Every tool enforces per-user access control via `services/access-control.ts`.
9
+ * Callers (the HTTP /mcp route, the stdio MCP server) must resolve a user
10
+ * identity before registering tools — `registerTools` requires a user in its
11
+ * `McpAccessContext` argument so there is no anonymous MCP path.
12
+ *
8
13
  * Note: `inputSchema` is cast to `any` at each call site because the MCP SDK's
9
14
  * ZodRawShapeCompat type does not accept ZodObject in TypeScript's strict mode.
10
15
  * Runtime schema validation is handled correctly by the SDK regardless.
11
16
  */
12
17
  import { z } from 'zod';
18
+ import { ENTITY_SCHEMAS } from '../core/validation.js';
19
+ import { getAccessibleRepos, hasRepoAccess, canWriteRepo, getAccessibleEntityIds, filterWikilinks, } from '../services/access-control.js';
20
+ import { createSharedFolder, FolderCreationError } from '../services/folder-creation.js';
21
+ import { validateFolderPath } from '../core/folder-paths.js';
22
+ import { existsSync } from 'fs';
23
+ import { join } from 'path';
24
+ import { isCuratedEntityType } from '../core/entity-types-catalog.js';
25
+ import { getAgentAllowedEntityTypes } from '../agent/prompts/entity-types-section.js';
26
+ import * as commentVirtualEntity from '../services/comment-virtual-entity.js';
27
+ import { buildActor, prepareCreatePayload, prepareUpdatePayload, diagnosedUpdate, DroppedAssetError, assertEntityRefs, } from '../services/entity-mutations.js';
28
+ import { BridgeError, validateUserHandleInPayload, assertCanChangeUserHandle } from '../services/user-person-bridge.js';
29
+ import { extractAssetEmbeds, stripFrontmatter, assetEmbed } from '../core/embeds.js';
30
+ import { listRepoAssets } from '../services/asset-listing.js';
31
+ import { presignAssetUpload } from '../services/asset-presign-service.js';
32
+ import { finalizeAssetUpload } from '../services/asset-finalize-service.js';
33
+ import { importAssetAsEntity } from '../services/asset-import-service.js';
34
+ import { AssetUploadError } from '../services/asset-upload-errors.js';
35
+ import { presignMimeAllowlist, inlineUploadMimeAllowlist, resolveFormat, isInlineUploadAllowed } from '../core/format-registry.js';
36
+ import { resolveWorkspaceAssetsRepo } from '../services/workspace-assets-folder.js';
37
+ import { Workspace } from '../core/workspace.js';
38
+ import { validateArtifactBundle } from '../core/artifact-bundle.js';
39
+ import { artifactExportFilename, buildArtifactHtmlExport } from '../core/artifact-export.js';
40
+ import { ArtifactRenderExportUnavailableError, artifactPngArchiveFilename, inspectArtifactRender, nameArtifactPngFiles, renderArtifactPdf, renderArtifactPngFiles, zipArtifactPngFiles, } from '../services/artifact-render-export.js';
41
+ import { artifactLibrarySourcesForBundle } from '../services/artifact-library-sources.js';
42
+ /**
43
+ * Server identity passed to `new McpServer(...)` on BOTH transports (stdio
44
+ * `src/mcp/server.ts` + HTTP `src/server/routes/mcp.ts`). Centralized so the
45
+ * two surfaces can never drift on name/version.
46
+ */
47
+ export const MCP_SERVER_INFO = { name: 'studiograph', version: '1.0.0' };
48
+ /**
49
+ * Server-level `instructions` (MCP `ServerOptions.instructions`, surfaced in
50
+ * the `initialize` response). With Claude Code's tool-search/deferral on, this
51
+ * is what tells the client WHEN to search for Studiograph's tools — it loads
52
+ * even when individual tool schemas are deferred. Must be passed in McpServer's
53
+ * SECOND constructor arg (`new McpServer(MCP_SERVER_INFO, { instructions })`) —
54
+ * putting it in the serverInfo object silently drops it.
55
+ *
56
+ * Budget: Claude Code truncates server instructions at 2KB and front-loads
57
+ * matter most — keep this short and lead with category + when-to-reach-for-it.
58
+ */
59
+ export const MCP_INSTRUCTIONS = 'Studiograph is the team\'s knowledge workspace: a Git-backed graph of entries ' +
60
+ '(people, organizations, projects, tasks, deals, events, notes, documents, ' +
61
+ 'deliverables, skills, and uploaded assets) stored as markdown with YAML ' +
62
+ 'frontmatter and connected by [[wikilinks]].\n\n' +
63
+ 'Reach for these tools whenever the user wants to CAPTURE, FIND, READ, or ' +
64
+ 'UPDATE workspace knowledge — search before concluding something is unavailable. ' +
65
+ 'Common entry points:\n' +
66
+ '- Find / read: search_entities, list_entities, get_entity, find_related\n' +
67
+ '- Create / edit: create_entity, update_entity, patch_entity (small edits)\n' +
68
+ '- Organize: list_repos, create_repo, create_subfolder, move_entity\n' +
69
+ '- Assets (images/files): list_assets, attach_asset_to_entity, upload_asset_inline\n' +
70
+ '- Artifacts: create_artifact, inspect_artifact, export_artifact\n' +
71
+ '- History / undo: list_file_history, list_recent_deletions, restore_file\n' +
72
+ '- Saved workflows: list_skills, get_skill';
73
+ /**
74
+ * Tools pinned against Claude Code's tool-search deferral via
75
+ * `_meta["anthropic/alwaysLoad"]=true` (see PIN_META). These are the handful an
76
+ * agent needs on essentially every turn, so they stay loaded even when the rest
77
+ * of the surface is deferred (verified: code.claude.com/docs/en/mcp.md, CC
78
+ * v2.1.121+). Keep this set SMALL — each pinned tool consumes upfront context.
79
+ * Exported so tests can assert the pin lands on exactly these tools.
80
+ */
81
+ export const MCP_PINNED_TOOLS = ['search_entities', 'list_entities', 'get_entity', 'create_entity'];
82
+ const PINNED_SET = new Set(MCP_PINNED_TOOLS);
83
+ const PIN_META = { _meta: { 'anthropic/alwaysLoad': true } };
13
84
  // ── helpers ───────────────────────────────────────────────────────────────────
14
85
  function ok(data) {
15
86
  return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
@@ -18,30 +89,277 @@ function fail(err) {
18
89
  const msg = err instanceof Error ? err.message : String(err);
19
90
  return { content: [{ type: 'text', text: msg }], isError: true };
20
91
  }
21
- // Strip frontmatter fields that duplicate the top-level id/type keys
92
+ /** Deny-without-leaking helper same wording as a missing repo/entity. */
93
+ function notFound(repo) {
94
+ return fail(new Error(`Folder "${repo}" not found`));
95
+ }
96
+ /**
97
+ * Map an AssetUploadError (or any Error with a stable `code` discriminator)
98
+ * onto the MCP `fail()` envelope, embedding the code so the client can
99
+ * branch programmatically. Wraps `fail()` rather than replacing it so any
100
+ * non-typed error falls through to the existing "unknown" path.
101
+ *
102
+ * The code makes its way into the text payload as a JSON object — MCP
103
+ * clients that parse the text get a structured shape; pure-text consumers
104
+ * still get a readable line.
105
+ */
106
+ function errorToMcpResult(err) {
107
+ if (err instanceof AssetUploadError) {
108
+ return {
109
+ content: [{
110
+ type: 'text',
111
+ text: JSON.stringify({ error: err.message, code: err.code }, null, 2),
112
+ }],
113
+ isError: true,
114
+ };
115
+ }
116
+ return fail(err);
117
+ }
118
+ // Shape a graph entity into the MCP result. Strip the frontmatter fields that
119
+ // duplicate the top-level keys: `id`, `entity_type`, AND `type`.
120
+ //
121
+ // `type` is the load-bearing one: it is NOT a duplicate of `entity_type` — it's
122
+ // a real field on `event` entities (the sub-type: meeting/call/workshop). Left
123
+ // in the spread it would land AFTER `type: entityType` and shadow the canonical
124
+ // type, so search/list reported `type: "meeting"` for an event and the caller
125
+ // couldn't round-trip that back into get_entity (`Entity not found:
126
+ // meeting/<id>`). We re-expose it as `subtype` so the top-level `type` is always
127
+ // the canonical entity type, which is what get_entity resolves against.
22
128
  function shape(id, entityType, data, extra) {
23
- const { id: _id, entity_type: _et, ...rest } = data;
24
- return { id, type: entityType, ...rest, ...extra };
129
+ const { id: _id, entity_type: _et, type: subtype, ...rest } = data;
130
+ return {
131
+ id,
132
+ type: entityType,
133
+ ...(subtype !== undefined ? { subtype } : {}),
134
+ ...rest,
135
+ ...extra,
136
+ };
137
+ }
138
+ /**
139
+ * Default subfolders for entity types when the caller omits `folder_path`.
140
+ * Existing entries with no folder live at the repo root; new entries of these
141
+ * types route into a typed subfolder so the repo stays organised by default.
142
+ * An explicit `folder_path: ""` (or any explicit value) overrides this map.
143
+ */
144
+ const DEFAULT_FOLDER_BY_TYPE = {
145
+ person: 'People',
146
+ organization: 'Organizations',
147
+ };
148
+ /** Convert an absolute entity path to a repo-relative POSIX path. */
149
+ function relPathOf(absPath, repoRoot) {
150
+ return absPath.startsWith(repoRoot + '/') ? absPath.slice(repoRoot.length + 1) : absPath;
151
+ }
152
+ function slugifyArtifactId(source) {
153
+ const slug = (source || 'artifact')
154
+ .normalize('NFKD')
155
+ .replace(/[\u0300-\u036f]/g, '')
156
+ .toLowerCase()
157
+ .replace(/[^a-z0-9]+/g, '-')
158
+ .replace(/^-+|-+$/g, '')
159
+ .slice(0, 80);
160
+ return slug || 'artifact';
161
+ }
162
+ function artifactFrontmatter(bundle, options = {}) {
163
+ const data = {
164
+ name: bundle.title,
165
+ kind: bundle.kind,
166
+ viewport_width: bundle.viewport.width,
167
+ viewport_height: bundle.viewport.height,
168
+ viewport_mode: bundle.viewport.mode,
169
+ };
170
+ if (options.includeStatus)
171
+ data.status = 'draft';
172
+ return data;
173
+ }
174
+ function parseArtifactBundleFromBody(content, label) {
175
+ let rawBundle;
176
+ try {
177
+ rawBundle = JSON.parse(content);
178
+ }
179
+ catch {
180
+ throw new Error(`${label} body is not a valid JSON bundle.`);
181
+ }
182
+ const validation = validateArtifactBundle(rawBundle);
183
+ if (!validation.ok || !validation.bundle) {
184
+ throw new Error(`Invalid artifact bundle: ${validation.errors.join('; ')}`);
185
+ }
186
+ return validation.bundle;
187
+ }
188
+ function exportedData(format, bytes, mimeType) {
189
+ if (format === 'html') {
190
+ return {
191
+ mime_type: 'text/html; charset=utf-8',
192
+ content: bytes.toString(),
193
+ };
194
+ }
195
+ const buffer = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
196
+ return {
197
+ mime_type: mimeType ?? (format === 'png' ? 'image/png' : 'application/pdf'),
198
+ data_base64: buffer.toString('base64'),
199
+ byteLength: buffer.length,
200
+ };
25
201
  }
202
+ /**
203
+ * Presign MIME enum for `create_asset_upload`, derived from the format registry
204
+ * (P4-V1) so the tool's accepted set can't drift from the presign service's gate.
205
+ * z.enum needs a non-empty tuple; the registry always yields ≥1 image MIME, but
206
+ * fall back to png/jpeg defensively so a registry edit can never produce an
207
+ * empty enum that throws at module load.
208
+ */
209
+ const PRESIGN_MIME_TUPLE = (() => {
210
+ const mimes = presignMimeAllowlist();
211
+ return mimes.length > 0 ? mimes : ['image/png', 'image/jpeg'];
212
+ })();
213
+ /** Inline-upload (upload_asset_inline) caps + accepted-MIME display list,
214
+ * computed once at module load from the registry (not per /mcp request). */
215
+ const INLINE_MAX_BYTES = 6 * 1024 * 1024; // ~4.5MB pre-base64; keeps the model payload sane.
216
+ const INLINE_UPLOAD_MIME_LIST = inlineUploadMimeAllowlist().sort();
26
217
  // ── tool registration ─────────────────────────────────────────────────────────
27
- export function registerTools(server, wm, authService) {
218
+ export function registerTools(server, wm, authService, ctx) {
219
+ const { user, wsHub, apiTokenName, commentService } = ctx;
220
+ // Register through this wrapper instead of server.registerTool directly so
221
+ // the always-load pin (PIN_META) is injected from a SINGLE source
222
+ // (PINNED_SET / MCP_PINNED_TOOLS) and can't drift per-call. Non-pinned tools
223
+ // pass through untouched.
224
+ const baseRegisterTool = server.registerTool.bind(server);
225
+ const reg = ((name, def, handler) => baseRegisterTool(name, PINNED_SET.has(name) ? { ...def, ...PIN_META } : def, handler));
226
+ // External-agent identity: "<owner first name>'s <token name>", or
227
+ // "<owner first name>'s agent" as a fallback when the token has no name
228
+ // (older tokens or contexts that didn't thread the token through). First name
229
+ // only keeps the chip short while preserving cross-user disambiguation; the
230
+ // `userId` lets the client paint the chip with the owning user's avatar accent
231
+ // (so the full identity is still recoverable). Falls back to the whole
232
+ // displayName when there's no space to split on (handles, single names).
233
+ const ownerFirstName = user.displayName.trim().split(/\s+/)[0] || user.displayName;
234
+ const agentLabel = apiTokenName
235
+ ? `${ownerFirstName}'s ${apiTokenName}`
236
+ : `${ownerFirstName}'s agent`;
237
+ const agentIdentity = {
238
+ kind: 'external',
239
+ label: agentLabel,
240
+ userId: String(user.id),
241
+ };
242
+ // Match the broadcast shape used by src/server/routes/graph-api.ts so the
243
+ // realtime listener in web/src/lib/stores/realtime.svelte.ts treats MCP
244
+ // mutations identically to web-UI mutations. The `agent` tag lets clients
245
+ // distinguish agent-driven mutations from human-driven ones.
246
+ function emitEntityChange(action, repo, entityType, entityId) {
247
+ wsHub?.broadcast({
248
+ type: 'entity_change',
249
+ action,
250
+ repo,
251
+ entityType,
252
+ entityId,
253
+ actor: user.displayName,
254
+ source: 'api',
255
+ agent: agentIdentity,
256
+ timestamp: new Date().toISOString(),
257
+ });
258
+ }
259
+ /**
260
+ * Wrap a mutation tool call so the sidebar can show a live "X's agent is
261
+ * acting" chip in the affected folder. `enter` fires before the work,
262
+ * `leave` after — wsHub TTL-cleans on crash. `verb` shapes the chip copy.
263
+ */
264
+ async function withAgentActivity(repo, verb, fn) {
265
+ wsHub?.handleAgentActivity('enter', repo, agentIdentity, verb);
266
+ try {
267
+ return await fn();
268
+ }
269
+ finally {
270
+ wsHub?.handleAgentActivity('leave', repo, agentIdentity, verb);
271
+ }
272
+ }
273
+ // Entity types the MCP surface may create. Matches the agent's allowlist so
274
+ // a workspace looks identical across the main app and MCP clients: the full
275
+ // canonical catalog plus workspace custom types. See
276
+ // src/core/entity-types-catalog.ts.
277
+ const builtinTypeKeys = new Set(Object.keys(ENTITY_SCHEMAS));
278
+ const customWorkspaceTypes = wm.getSchemaRegistry().getAllTypes()
279
+ .filter(t => !builtinTypeKeys.has(t));
280
+ const mcpAllowedCreateTypes = getAgentAllowedEntityTypes(customWorkspaceTypes);
281
+ const mcpAllowedSet = new Set(mcpAllowedCreateTypes);
28
282
  // ── list_repos ────────────────────────────────────────────────────────────
29
- server.registerTool('list_repos', { description: 'List all collections in the Studiograph workspace' }, async () => ok(wm.getAllRepoConfigs()));
283
+ reg('list_repos', { description: 'List folders in the Studiograph workspace that you have access to' }, async () => ok(getAccessibleRepos(user, wm, authService)));
284
+ // ── create_repo ───────────────────────────────────────────────────────────
285
+ //
286
+ // Creates a new shared folder in the workspace and grants the
287
+ // caller folder-admin access. Shares its implementation with the web route
288
+ // POST /api/repos via services/folder-creation.ts so both surfaces behave
289
+ // identically. After creation, use create_entity with the new folder name
290
+ // to add entries. Folder names are immutable slugs; the display name is
291
+ // editable later via the web UI.
292
+ const createRepoSchema = z.object({
293
+ name: z.string().describe('Folder slug — lowercase kebab-case (letters, digits, hyphens; must start with a letter or digit). ' +
294
+ 'This becomes the folder id and directory name; it cannot be changed without a rename.'),
295
+ display_name: z.string().optional().describe('Human-readable name shown in the UI (e.g. "Client Projects"). Defaults to the slug.'),
296
+ description: z.string().optional().describe('Short description of what this folder is for.'),
297
+ });
298
+ reg('create_repo', {
299
+ description: 'Create a new shared folder in the workspace. Use this when an agent or ' +
300
+ 'user needs a fresh container for a new project, client area, or topic and no existing ' +
301
+ 'folder fits. The caller is automatically granted folder-admin access. ' +
302
+ 'Follow up with create_entity (passing the new folder name as `repo`) to seed entries. ' +
303
+ 'Folder names must be kebab-case and unique; the private per-user "entries-<id>" ' +
304
+ 'namespace is reserved and will be rejected.',
305
+ inputSchema: createRepoSchema,
306
+ }, async (args) => {
307
+ const { name, display_name, description } = createRepoSchema.parse(args);
308
+ try {
309
+ const repo = await createSharedFolder(wm, authService, { name, display_name, description }, user, wsHub);
310
+ return ok({ success: true, ...repo });
311
+ }
312
+ catch (err) {
313
+ if (err instanceof FolderCreationError) {
314
+ return fail(new Error(err.message));
315
+ }
316
+ return fail(err);
317
+ }
318
+ });
30
319
  // ── list_entities ─────────────────────────────────────────────────────────
31
320
  const listEntitiesSchema = z.object({
32
- repo: z.string().optional().describe('Collection name — omit to list across all collections'),
33
- entity_type: z.string().optional().describe('Entity type filter (project, client, person, meeting…)'),
321
+ repo: z.string().optional().describe('Folder name — omit to list across all folders'),
322
+ entity_type: z.string().optional().describe('Entity type filter (project, client, person, event…)'),
34
323
  limit: z.number().int().positive().optional().describe('Maximum number of results'),
35
324
  });
36
- server.registerTool('list_entities', { description: 'List entities in a collection (or all collections). Optionally filter by entity type.', inputSchema: listEntitiesSchema }, async (args) => {
325
+ reg('list_entities', { description: 'List entries in a folder (or all folders). Optionally filter by entity type. Each result carries the canonical `type` and a repo-relative `path` — pass either back to get_entity to open it.', inputSchema: listEntitiesSchema }, async (args) => {
37
326
  const { repo, entity_type, limit } = listEntitiesSchema.parse(args);
38
327
  try {
328
+ // Comments live in SQLite, not the filesystem graph — dispatch through
329
+ // the virtual-entity adapter when the caller asks for them explicitly.
330
+ if (entity_type === 'comment' && commentService) {
331
+ if (repo && !hasRepoAccess(user, repo, wm, authService))
332
+ return notFound(repo);
333
+ const accessibleNames = new Set(getAccessibleRepos(user, wm, authService).map(r => r.name));
334
+ const envelopes = commentVirtualEntity.listComments(commentService, { repo, limit });
335
+ const filtered = envelopes.filter(e => accessibleNames.has(e.data.repo));
336
+ return ok(filtered.map(e => shape(e.id, e.entityType, e.data, { repo: e.data.repo })));
337
+ }
39
338
  if (repo) {
40
- const entities = wm.getGraph(repo).list(entity_type, limit);
41
- return ok(entities.map(e => shape(e.id, e.entityType, e.data)));
339
+ if (!hasRepoAccess(user, repo, wm, authService))
340
+ return notFound(repo);
341
+ const graph = wm.getGraph(repo);
342
+ const repoRoot = graph.getRepoPath();
343
+ const entities = graph.list(entity_type, limit);
344
+ return ok(entities.map(e => shape(e.id, e.entityType, e.data, { path: relPathOf(e.path, repoRoot) })));
345
+ }
346
+ // No repo specified → iterate only over the user's accessible folders.
347
+ const accessible = getAccessibleRepos(user, wm, authService);
348
+ const results = [];
349
+ for (const r of accessible) {
350
+ try {
351
+ const graph = wm.getGraph(r.name);
352
+ const repoRoot = graph.getRepoPath();
353
+ const list = graph.list(entity_type);
354
+ for (const e of list) {
355
+ results.push(shape(e.id, e.entityType, e.data, { repo: r.name, path: relPathOf(e.path, repoRoot) }));
356
+ if (limit && results.length >= limit)
357
+ return ok(results);
358
+ }
359
+ }
360
+ catch { /* skip unmounted repos */ }
42
361
  }
43
- const entities = wm.list(entity_type, limit);
44
- return ok(entities.map(e => shape(e.id, e.entityType, e.data, { repo: e.repoName })));
362
+ return ok(results);
45
363
  }
46
364
  catch (err) {
47
365
  return fail(err);
@@ -49,15 +367,64 @@ export function registerTools(server, wm, authService) {
49
367
  });
50
368
  // ── get_entity ────────────────────────────────────────────────────────────
51
369
  const getEntitySchema = z.object({
52
- repo: z.string().describe('Collection name'),
53
- entity_type: z.string().describe('Entity type (project, client, person…)'),
54
- entity_id: z.string().describe('Entity ID (kebab-case slug)'),
370
+ repo: z.string().describe('Folder name'),
371
+ entity_type: z.string().optional().describe('Canonical entity type as returned in a search/list result\'s `type` (e.g. "event" — NOT a category/subtype value like "meeting"). Optional: when omitted or wrong, the entity is resolved by id.'),
372
+ entity_id: z.string().optional().describe('Entity ID (kebab-case slug). Provide entity_id or path.'),
373
+ path: z.string().optional().describe('Repo-relative file path from a search/list result (`path` field). When given, resolves directly and ignores entity_type.'),
55
374
  });
56
- server.registerTool('get_entity', { description: 'Get a specific entity — includes full markdown content and frontmatter', inputSchema: getEntitySchema }, async (args) => {
57
- const { repo, entity_type, entity_id } = getEntitySchema.parse(args);
375
+ reg('get_entity', {
376
+ description: 'Get a specific entry full markdown content and frontmatter. Robust resolution: pass the `type` and `id` ' +
377
+ '(or the `path`) exactly as returned by search_entities/list_entities. If the type is wrong or omitted, the ' +
378
+ 'entry is still resolved by id, so a search hit never fails to open.',
379
+ inputSchema: getEntitySchema,
380
+ }, async (args) => {
381
+ const { repo, entity_type, entity_id, path } = getEntitySchema.parse(args);
382
+ if (!hasRepoAccess(user, repo, wm, authService))
383
+ return notFound(repo);
384
+ if (!entity_id && !path)
385
+ return fail(new Error('get_entity requires entity_id or path'));
58
386
  try {
59
- const entity = wm.getGraph(repo).get(entity_type, entity_id);
60
- return ok({ id: entity.id, type: entity.entityType, data: entity.data, content: entity.document.content });
387
+ if (entity_type === 'comment' && commentService) {
388
+ const envelope = commentVirtualEntity.getComment(commentService, entity_id);
389
+ if (!envelope)
390
+ return fail(new Error(`Comment "${entity_id}" not found`));
391
+ if (envelope.data.repo !== repo)
392
+ return notFound(repo);
393
+ return ok({ id: envelope.id, type: envelope.entityType, data: envelope.data, content: envelope.document.content });
394
+ }
395
+ const graph = wm.getGraph(repo);
396
+ // Resolve robustly so any search/list result re-opens without the
397
+ // caller guessing the physical type: explicit path wins; else try the
398
+ // supplied type; on a miss (wrong/legacy type — e.g. an `event` whose
399
+ // `type` field is "meeting") fall back to id-only resolution.
400
+ let entity;
401
+ if (path) {
402
+ entity = graph.getByPath(path);
403
+ }
404
+ else if (entity_type) {
405
+ try {
406
+ entity = graph.get(entity_type, entity_id);
407
+ }
408
+ catch {
409
+ entity = graph.getById(entity_id);
410
+ }
411
+ }
412
+ else {
413
+ entity = graph.getById(entity_id);
414
+ }
415
+ // Prefer the live editor body when a seeded Y.Doc is open: graph reads
416
+ // disk, which lags the editor by the autosave debounce, so a user's
417
+ // freshly-placed image/asset embed may not be on disk yet. Use the
418
+ // RESOLVED type/id — getById/getByPath may differ from what was passed.
419
+ const liveBody = graph.getLiveBody(entity.entityType, entity.id);
420
+ const content = liveBody ?? entity.document.content;
421
+ return ok({
422
+ id: entity.id,
423
+ type: entity.entityType,
424
+ path: relPathOf(entity.path, graph.getRepoPath()),
425
+ data: entity.data,
426
+ content,
427
+ });
61
428
  }
62
429
  catch (err) {
63
430
  return fail(err);
@@ -67,76 +434,1053 @@ export function registerTools(server, wm, authService) {
67
434
  const searchSchema = z.object({
68
435
  query: z.string().optional().describe('Text search query'),
69
436
  entity_type: z.string().optional().describe('Filter by entity type'),
70
- repos: z.array(z.string()).optional().describe('Limit to specific collection names'),
437
+ repos: z.array(z.string()).optional().describe('Limit to specific folder names'),
71
438
  limit: z.number().int().positive().optional().describe('Maximum number of results'),
72
439
  });
73
- server.registerTool('search_entities', { description: 'Full-text search for entities across all collections', inputSchema: searchSchema }, async (args) => {
440
+ reg('search_entities', { description: 'Full-text search for entries across folders you can access. Each result carries the canonical `type` and a repo-relative `path` — pass either back to get_entity to open it.', inputSchema: searchSchema }, async (args) => {
74
441
  const { query, entity_type, repos, limit } = searchSchema.parse(args);
75
442
  try {
76
- const results = await wm.search({ query, entityType: entity_type, repoNames: repos, limit });
77
- return ok(results.map(e => shape(e.id, e.entityType, e.data, { repo: e.repoName })));
443
+ const accessibleNames = new Set(getAccessibleRepos(user, wm, authService).map(r => r.name));
444
+ const scoped = repos
445
+ ? repos.filter(r => accessibleNames.has(r))
446
+ : [...accessibleNames];
447
+ if (scoped.length === 0)
448
+ return ok([]);
449
+ const results = await wm.search({ query, entityType: entity_type, repoNames: scoped, limit });
450
+ return ok(results.map(e => shape(e.id, e.entityType, e.data, {
451
+ repo: e.repoName,
452
+ path: relPathOf(e.path, wm.getGraph(e.repoName).getRepoPath()),
453
+ })));
78
454
  }
79
455
  catch (err) {
80
456
  return fail(err);
81
457
  }
82
458
  });
83
459
  // ── create_entity ─────────────────────────────────────────────────────────
460
+ // Constrain `entity_type` to the allowlist so MCP clients (e.g. Claude Desktop)
461
+ // see the same type surface as the main agent. Existing canonical-type entities
462
+ // remain readable/updatable via other tools.
463
+ const createEntityTypeSchema = mcpAllowedCreateTypes.length > 0
464
+ ? z.enum(mcpAllowedCreateTypes)
465
+ : z.string();
84
466
  const createSchema = z.object({
85
- repo: z.string().describe('Collection name'),
86
- entity_type: z.string().describe('Entity type'),
467
+ repo: z.string().describe('Folder name'),
468
+ entity_type: createEntityTypeSchema.describe(`Entity type. Allowed: ${mcpAllowedCreateTypes.join(', ')}.`),
87
469
  entity_id: z.string().describe('Entity ID (kebab-case slug)'),
88
- data: z.record(z.unknown()).describe('Frontmatter fields as a JSON object'),
89
- content: z.string().optional().describe('Markdown body content'),
470
+ data: z.record(z.string(), z.unknown()).describe('Type-specific frontmatter fields (e.g. for person: name, email, organization, role). ' +
471
+ 'Base fields (entity_id, entity_type, created_at, updated_at, created_by, updated_by) ' +
472
+ 'are stamped automatically — do not include them.'),
473
+ content: z.string().optional().describe('Markdown body content. Write one line per paragraph — use newlines only for real paragraph/list breaks, not to wrap prose at a column width (entries render source line breaks literally).'),
474
+ folder_path: z.string().optional().describe('Repo-relative subfolder to place the entry in (e.g. "People", "clients/acme"). ' +
475
+ 'Omit to use the per-type default (person → People, organization → Organizations); ' +
476
+ 'pass an empty string to force the repo root. Parent folders are auto-created.'),
90
477
  });
91
- server.registerTool('create_entity', { description: 'Create a new entity in a collection. Commits to git automatically.', inputSchema: createSchema }, async (args) => {
92
- const { repo, entity_type, entity_id, data, content } = createSchema.parse(args);
478
+ reg('create_entity', {
479
+ description: 'Create a new entity (entry) in a folder — person, organization, project, task, note, document, etc. ' +
480
+ 'Commits to git automatically. Base frontmatter (entity_id, timestamps, author) is stamped ' +
481
+ 'automatically; `data` carries only type-specific fields. For person entries, `status` synonyms ' +
482
+ '(active/prospective/past/…) normalize to `current` | `former`. See the `folder_path` field for ' +
483
+ 'placement defaults. Response includes the final repo-relative `path`.',
484
+ inputSchema: createSchema,
485
+ }, async (args) => {
486
+ const { repo, entity_type, entity_id, data, content, folder_path } = createSchema.parse(args);
487
+ if (!hasRepoAccess(user, repo, wm, authService))
488
+ return notFound(repo);
489
+ if (!canWriteRepo(user, repo, wm, authService)) {
490
+ return fail(new Error(`You do not have write access to folder "${repo}"`));
491
+ }
492
+ // Defense in depth: MCP SDK may not enforce zod enum strictly on all clients.
493
+ if (!mcpAllowedSet.has(entity_type)) {
494
+ const hint = isCuratedEntityType(entity_type)
495
+ ? '' // shouldn't happen for any other curated type — they're all allowed
496
+ : ` Allowed: ${mcpAllowedCreateTypes.join(', ')}.`;
497
+ return fail(new Error(`Entity type "${entity_type}" is not a known canonical or workspace custom type.${hint}`));
498
+ }
499
+ // Explicit folder_path (including "") wins; otherwise apply the per-type
500
+ // default so the agent doesn't have to know the workspace convention.
501
+ const effectiveFolder = folder_path !== undefined
502
+ ? folder_path
503
+ : (DEFAULT_FOLDER_BY_TYPE[entity_type] ?? '');
504
+ // Enforce the user↔person bridge invariant on person creates, matching
505
+ // graph-api + graph-tools: at most one person entity per auth handle
506
+ // workspace-wide (validateUserHandleInPayload), and only a workspace
507
+ // owner may stamp a handle (assertCanChangeUserHandle). Without this,
508
+ // create_entity over MCP was the one write path that could plant a
509
+ // SECOND linked record — violating "multiple person entries, but only
510
+ // one linked".
511
+ if (entity_type === 'person') {
512
+ try {
513
+ const incoming = (data ?? {});
514
+ validateUserHandleInPayload(wm, incoming);
515
+ if (Object.prototype.hasOwnProperty.call(incoming, 'user_handle')) {
516
+ assertCanChangeUserHandle(user, '', incoming.user_handle);
517
+ }
518
+ }
519
+ catch (err) {
520
+ if (err instanceof BridgeError)
521
+ return fail(err);
522
+ throw err;
523
+ }
524
+ }
525
+ // Scope ref fields (today: task.assignee → person). Reject a wikilink
526
+ // that resolves to a non-person; forward references are allowed.
93
527
  try {
94
- const entity = await wm.getGraph(repo).create({
95
- entityType: entity_type,
96
- entityId: entity_id,
97
- data: data,
98
- content,
99
- });
100
- return ok({ success: true, id: entity.id, type: entity.entityType });
528
+ assertEntityRefs(wm, entity_type, data, { userExists: (id) => !!authService.findUserById(Number(id)) });
101
529
  }
102
530
  catch (err) {
103
531
  return fail(err);
104
532
  }
533
+ const actor = buildActor(user);
534
+ const options = prepareCreatePayload({
535
+ entityType: entity_type,
536
+ entityId: entity_id,
537
+ data: data,
538
+ content,
539
+ folderPath: effectiveFolder || undefined,
540
+ }, actor);
541
+ return withAgentActivity(repo, 'creating', async () => {
542
+ try {
543
+ const graph = wm.getGraph(repo);
544
+ // Detect implicit subfolder creation: if the entity lands in a folder
545
+ // that didn't exist yet, the entity_change below only refreshes the
546
+ // entry list — clients need a _folder event to refetch the folder
547
+ // tree (sidebar + list/gallery folder rows). Normalize the same way
548
+ // graph.create does so the existence check matches the real path.
549
+ const normFolder = effectiveFolder ? validateFolderPath(effectiveFolder) : '';
550
+ const folderExisted = !normFolder || existsSync(join(graph.getRepoPath(), normFolder));
551
+ const entity = await graph.create(options);
552
+ emitEntityChange('created', repo, entity.entityType, entity.id);
553
+ if (!folderExisted)
554
+ emitEntityChange('created', repo, '_folder', normFolder);
555
+ return ok({
556
+ success: true,
557
+ id: entity.id,
558
+ type: entity.entityType,
559
+ repo,
560
+ path: relPathOf(entity.path, graph.getRepoPath()),
561
+ folder_path: entity.folder_path,
562
+ });
563
+ }
564
+ catch (err) {
565
+ return fail(err);
566
+ }
567
+ });
105
568
  });
106
569
  // ── update_entity ─────────────────────────────────────────────────────────
570
+ //
571
+ // Bare success envelopes were misleading agents: callers couldn't tell
572
+ // whether their write landed, whether validation silently dropped a field,
573
+ // or whether they'd just rewritten the file with identical bytes. The
574
+ // response now includes path, commit, before/after hashes, per-section
575
+ // changed flags, and the post-write entity snapshot so agents can verify
576
+ // their intent matches reality without a follow-up get_entity round-trip.
577
+ //
578
+ // No-op detection compares semantic frontmatter (excluding auto-stamped
579
+ // `updated_at` / `updated_by`) and raw content. A no-op is reported as
580
+ // success with `noop: true` — not an error — so idempotent re-runs are
581
+ // explicit. If the caller passed data or content but the on-disk file is
582
+ // byte-identical after the write, that's a silent-drop signal: the call
583
+ // fails loudly with a message that names which inputs were ignored.
107
584
  const updateSchema = z.object({
108
- repo: z.string().describe('Collection name'),
585
+ repo: z.string().describe('Folder name'),
109
586
  entity_type: z.string().describe('Entity type'),
110
587
  entity_id: z.string().describe('Entity ID'),
111
- data: z.record(z.unknown()).optional().describe('Frontmatter fields to update (merged with existing)'),
112
- content: z.string().optional().describe('New markdown body content'),
588
+ data: z.record(z.string(), z.unknown()).optional().describe('Frontmatter fields to update (merged with existing)'),
589
+ content: z.string().optional().describe('New markdown body content (replaces the entire body). Write one line per paragraph — use newlines only for real paragraph/list breaks, not to wrap prose at a column width (entries render source line breaks literally).'),
590
+ allow_image_removal: z.boolean().optional().describe('Whole-body writes are rejected with code ASSET_REMOVAL_BLOCKED when the submitted content would drop ' +
591
+ 'image/attachment embeds that the current document still has (a common silent-data-loss bug when an agent ' +
592
+ 'edits stale content). Set true ONLY when you intend to delete those embeds. Prefer patch_entity for small edits.'),
593
+ });
594
+ reg('update_entity', {
595
+ description: 'Update (edit) an existing entity. `data` fields merge with existing frontmatter; `content` replaces the ' +
596
+ 'whole markdown body. Immutable base fields are preserved from disk; `updated_at`/`updated_by` are stamped. ' +
597
+ 'For small body edits prefer patch_entity (it can\'t drop image embeds). Returns the post-write snapshot plus ' +
598
+ 'diagnostic flags (frontmatter_changed, content_changed, noop, commit) so you can verify the write landed ' +
599
+ 'without a follow-up get_entity.',
600
+ inputSchema: updateSchema,
601
+ }, async (args) => {
602
+ const { repo, entity_type, entity_id, data, content, allow_image_removal } = updateSchema.parse(args);
603
+ if (!hasRepoAccess(user, repo, wm, authService))
604
+ return notFound(repo);
605
+ if (!canWriteRepo(user, repo, wm, authService)) {
606
+ return fail(new Error(`You do not have write access to folder "${repo}"`));
607
+ }
608
+ if (data === undefined && content === undefined) {
609
+ return fail(new Error('update_entity requires at least one of `data` or `content`'));
610
+ }
611
+ // Enforce the user↔person bridge invariant on person updates, matching
612
+ // graph-api + graph-tools: workspace-wide handle uniqueness (skipping the
613
+ // entity being written) + owner-only authz to change `user_handle`. Keeps
614
+ // update_entity from becoming a back door around "only one linked record".
615
+ if (entity_type === 'person' && data) {
616
+ try {
617
+ const incoming = data;
618
+ validateUserHandleInPayload(wm, incoming, { repo, entityId: entity_id });
619
+ if (Object.prototype.hasOwnProperty.call(incoming, 'user_handle')) {
620
+ let currentHandle = '';
621
+ try {
622
+ currentHandle = (wm.getGraph(repo).get('person', entity_id)?.data?.user_handle ?? '');
623
+ }
624
+ catch { /* new/missing entity — treat as empty */ }
625
+ assertCanChangeUserHandle(user, currentHandle, incoming.user_handle);
626
+ }
627
+ }
628
+ catch (err) {
629
+ if (err instanceof BridgeError)
630
+ return fail(err);
631
+ throw err;
632
+ }
633
+ }
634
+ // Scope ref fields (today: task.assignee → person). Reject a wikilink
635
+ // that resolves to a non-person; forward references are allowed.
636
+ try {
637
+ assertEntityRefs(wm, entity_type, data, { userExists: (id) => !!authService.findUserById(Number(id)) });
638
+ }
639
+ catch (err) {
640
+ return fail(err);
641
+ }
642
+ const actor = buildActor(user);
643
+ const options = prepareUpdatePayload({
644
+ entityType: entity_type,
645
+ entityId: entity_id,
646
+ data: data,
647
+ content,
648
+ }, actor);
649
+ return withAgentActivity(repo, 'editing', async () => {
650
+ try {
651
+ const graph = wm.getGraph(repo);
652
+ // Diagnosed update (shared with the agent surface): snapshots
653
+ // before, runs the server-write path (disk + commit serialize via
654
+ // the per-doc lock; live clients get the post-commit broadcast),
655
+ // and throws SilentDropError when the caller intended a change
656
+ // that produced no on-disk diff. The outer catch maps that to fail.
657
+ const { entity, frontmatter_changed, content_changed, noop, broadcast } = await diagnosedUpdate(graph, options, { data, content }, { allowAssetRemoval: allow_image_removal });
658
+ emitEntityChange('updated', repo, entity.entityType, entity.id);
659
+ // Repo-relative path so external agents don't see the workspace
660
+ // absolute path. The HEAD commit is captured after graph.update
661
+ // commits the transaction — null when noGit is on or no commit
662
+ // was produced (e.g. transaction self-cleared because there was
663
+ // no staged diff).
664
+ const repoRoot = graph.getRepoPath();
665
+ const relPath = entity.path.startsWith(repoRoot + '/')
666
+ ? entity.path.slice(repoRoot.length + 1)
667
+ : entity.path;
668
+ const commit = graph.getGitService()?.getHeadCommit() ?? null;
669
+ return ok({
670
+ success: true,
671
+ id: entity.id,
672
+ type: entity.entityType,
673
+ repo,
674
+ path: relPath,
675
+ commit,
676
+ frontmatter_changed,
677
+ content_changed,
678
+ noop,
679
+ broadcast,
680
+ updated_at: entity.data.updated_at ?? null,
681
+ entity: {
682
+ id: entity.id,
683
+ type: entity.entityType,
684
+ data: entity.data,
685
+ content: entity.document.content,
686
+ },
687
+ });
688
+ }
689
+ catch (err) {
690
+ if (err instanceof DroppedAssetError) {
691
+ return ok({
692
+ success: false,
693
+ code: 'ASSET_REMOVAL_BLOCKED',
694
+ droppedAssets: err.dropped,
695
+ message: err.message,
696
+ });
697
+ }
698
+ return fail(err);
699
+ }
700
+ });
701
+ });
702
+ // ── Artifact Canvas tools ────────────────────────────────────────────────
703
+ //
704
+ // MCP parity for the in-app artifact tools. External agents should not need
705
+ // to hand-roll graph entity JSON for artifacts or discover the HTTP export
706
+ // routes. These wrappers keep the same ArtifactBundle validation and commit
707
+ // semantics as the web/chat flow, while visual inspection reuses the shared
708
+ // Playwright renderer used by PDF/PNG export.
709
+ const createArtifactSchema = z.object({
710
+ repo: z.string().describe('Folder name where the artifact entry should be created or updated.'),
711
+ bundle: z.unknown().describe('ArtifactBundle object with schemaVersion, artifactId, title, kind, viewport, optional libraries (three, d3, motion), files, and entrypoint.'),
712
+ entity_id: z.string().optional().describe('Existing artifact entity id to update, or desired id for a new artifact. Defaults to a slug from bundle.artifactId/title.'),
713
+ folder_path: z.string().optional().describe('Repo-relative subfolder for a newly created artifact. Ignored when updating an existing artifact.'),
714
+ });
715
+ reg('create_artifact', {
716
+ description: 'Create or update a persisted sandboxed Artifact Canvas entry from an HTML/CSS/JS ArtifactBundle. ' +
717
+ 'Use this for slide decks, documents, social graphics, prototypes, diagrams, small interactive tools, ' +
718
+ 'artifacts using curated local libraries (three, d3, motion), and other visual outputs. ' +
719
+ 'Validates the bundle, writes an artifact graph entry, commits to git, and ' +
720
+ 'broadcasts the same live entity_change event as the web agent.',
721
+ inputSchema: createArtifactSchema,
722
+ }, async (args) => {
723
+ const { repo, bundle: rawBundle, entity_id, folder_path } = createArtifactSchema.parse(args);
724
+ if (!hasRepoAccess(user, repo, wm, authService))
725
+ return notFound(repo);
726
+ if (!canWriteRepo(user, repo, wm, authService)) {
727
+ return fail(new Error(`You do not have write access to folder "${repo}"`));
728
+ }
729
+ const validation = validateArtifactBundle(rawBundle);
730
+ if (!validation.ok || !validation.bundle) {
731
+ return fail(new Error(`Invalid artifact bundle: ${validation.errors.join('; ')}`));
732
+ }
733
+ const bundle = validation.bundle;
734
+ const graph = wm.getGraph(repo);
735
+ const actor = buildActor(user);
736
+ const body = `${JSON.stringify(bundle, null, 2)}\n`;
737
+ const createData = artifactFrontmatter(bundle, { includeStatus: true });
738
+ const updateData = artifactFrontmatter(bundle);
739
+ return withAgentActivity(repo, entity_id ? 'editing' : 'creating', async () => {
740
+ try {
741
+ const preferredId = entity_id || slugifyArtifactId(bundle.artifactId || bundle.title);
742
+ let resolvedId = preferredId;
743
+ let existing = false;
744
+ if (entity_id) {
745
+ try {
746
+ graph.get('artifact', preferredId);
747
+ existing = true;
748
+ }
749
+ catch {
750
+ existing = false;
751
+ }
752
+ }
753
+ else {
754
+ let suffix = 2;
755
+ while (suffix < 1000) {
756
+ try {
757
+ graph.get('artifact', resolvedId);
758
+ resolvedId = `${preferredId}-${suffix++}`;
759
+ }
760
+ catch {
761
+ break;
762
+ }
763
+ }
764
+ }
765
+ if (existing) {
766
+ const options = prepareUpdatePayload({
767
+ entityType: 'artifact',
768
+ entityId: resolvedId,
769
+ data: updateData,
770
+ content: body,
771
+ }, actor);
772
+ const { entity, frontmatter_changed, content_changed, noop, broadcast } = await diagnosedUpdate(graph, options, { data: updateData, content: body });
773
+ emitEntityChange('updated', repo, entity.entityType, entity.id);
774
+ return ok({
775
+ success: true,
776
+ action: 'updated',
777
+ id: entity.id,
778
+ type: entity.entityType,
779
+ repo,
780
+ path: relPathOf(entity.path, graph.getRepoPath()),
781
+ commit: graph.getGitService()?.getHeadCommit() ?? null,
782
+ frontmatter_changed,
783
+ content_changed,
784
+ noop,
785
+ broadcast,
786
+ });
787
+ }
788
+ const entity = await graph.create(prepareCreatePayload({
789
+ entityType: 'artifact',
790
+ entityId: resolvedId,
791
+ data: createData,
792
+ content: body,
793
+ folderPath: folder_path || undefined,
794
+ }, actor));
795
+ emitEntityChange('created', repo, entity.entityType, entity.id);
796
+ return ok({
797
+ success: true,
798
+ action: 'created',
799
+ id: entity.id,
800
+ type: entity.entityType,
801
+ repo,
802
+ path: relPathOf(entity.path, graph.getRepoPath()),
803
+ commit: graph.getGitService()?.getHeadCommit() ?? null,
804
+ });
805
+ }
806
+ catch (err) {
807
+ return fail(err);
808
+ }
809
+ });
810
+ });
811
+ const inspectArtifactSchema = z.object({
812
+ repo: z.string().optional().describe('Folder containing the persisted artifact entry. Required when bundle is omitted.'),
813
+ entity_id: z.string().optional().describe('Persisted artifact entity id. Required when bundle is omitted.'),
814
+ bundle: z.unknown().optional().describe('ArtifactBundle object to inspect without saving. If omitted, repo + entity_id are loaded from the graph.'),
815
+ context: z.enum(['editor', 'fullscreen']).optional().describe('"editor" renders the canvas overview/grid; "fullscreen" renders the share/fullscreen view. Defaults to editor.'),
816
+ frame_selector: z.string().optional().describe('CSS selector for individual frames/pages/slides. Defaults to display.controls.stepper.selector, then [data-artifact-frame], [data-artifact-page], .page.'),
817
+ frame_indexes: z.array(z.number()).optional().describe('Specific zero-based frame/page/slide indexes to screenshot.'),
818
+ max_frames: z.number().optional().describe('Maximum number of individual frames/pages/slides to screenshot. Defaults to 6, max 24.'),
819
+ include_page_screenshot: z.boolean().optional().describe('Whether to include a screenshot of the whole rendered page. Defaults to true.'),
820
+ include_frame_screenshots: z.boolean().optional().describe('Whether to include screenshots for selected frames/pages/slides. Defaults to true.'),
821
+ screenshot_format: z.enum(['jpeg', 'png']).optional().describe('Screenshot encoding. Defaults to jpeg to keep tool results compact.'),
822
+ base_url: z.string().optional().describe('Optional app origin such as http://localhost:3000, used only to allow same-origin /api/assets/* font/image loads during render.'),
113
823
  });
114
- server.registerTool('update_entity', { description: 'Update an existing entity. Data fields are merged with existing frontmatter.', inputSchema: updateSchema }, async (args) => {
115
- const { repo, entity_type, entity_id, data, content } = updateSchema.parse(args);
824
+ reg('inspect_artifact', {
825
+ description: 'Use Playwright to visually inspect an ArtifactBundle or persisted artifact entry. Returns a page screenshot, ' +
826
+ 'frame/page/slide metadata, optional individual frame screenshots, console messages, page errors, and failed requests. ' +
827
+ 'Use this after creating or revising artifacts to verify layout and individual slides/pages.',
828
+ inputSchema: inspectArtifactSchema,
829
+ }, async (args) => {
830
+ const parsed = inspectArtifactSchema.parse(args);
831
+ let bundle;
116
832
  try {
117
- const entity = await wm.getGraph(repo).update({
118
- entityType: entity_type,
119
- entityId: entity_id,
120
- data: data,
121
- content,
833
+ if (parsed.bundle !== undefined) {
834
+ const validation = validateArtifactBundle(parsed.bundle);
835
+ if (!validation.ok || !validation.bundle) {
836
+ return fail(new Error(`Invalid artifact bundle: ${validation.errors.join('; ')}`));
837
+ }
838
+ bundle = validation.bundle;
839
+ }
840
+ else {
841
+ if (!parsed.repo || !parsed.entity_id) {
842
+ return fail(new Error('inspect_artifact requires either bundle or both repo and entity_id.'));
843
+ }
844
+ if (!hasRepoAccess(user, parsed.repo, wm, authService))
845
+ return notFound(parsed.repo);
846
+ const entity = wm.getGraph(parsed.repo).get('artifact', parsed.entity_id);
847
+ bundle = parseArtifactBundleFromBody(entity.document.content, `Artifact ${parsed.repo}/${parsed.entity_id}`);
848
+ }
849
+ const options = {
850
+ context: parsed.context,
851
+ frameSelector: parsed.frame_selector,
852
+ frameIndexes: parsed.frame_indexes,
853
+ maxFrames: parsed.max_frames,
854
+ includePageScreenshot: parsed.include_page_screenshot,
855
+ includeFrameScreenshots: parsed.include_frame_screenshots,
856
+ screenshotFormat: parsed.screenshot_format,
857
+ baseUrl: parsed.base_url,
858
+ };
859
+ return ok({ success: true, inspection: await inspectArtifactRender(bundle, options) });
860
+ }
861
+ catch (err) {
862
+ if (err instanceof ArtifactRenderExportUnavailableError) {
863
+ return fail(new Error(`${err.message} Install with: npx playwright install chromium`));
864
+ }
865
+ return fail(err);
866
+ }
867
+ });
868
+ const exportArtifactSchema = z.object({
869
+ repo: z.string().describe('Folder containing the artifact entry.'),
870
+ entity_id: z.string().describe('Persisted artifact entity id.'),
871
+ format: z.enum(['html', 'png', 'pdf']).describe('Export format to produce. HTML returns text content; PNG/PDF return base64 bytes.'),
872
+ base_url: z.string().optional().describe('Optional app origin such as http://localhost:3000, used only to allow same-origin /api/assets/* font/image loads during PNG/PDF render.'),
873
+ });
874
+ reg('export_artifact', {
875
+ description: 'Export a persisted Artifact Canvas entry as HTML, PNG, or PDF. HTML returns text content. ' +
876
+ 'PNG/PDF are rendered with Playwright and returned as base64 bytes so MCP clients can save them.',
877
+ inputSchema: exportArtifactSchema,
878
+ }, async (args) => {
879
+ const { repo, entity_id, format, base_url } = exportArtifactSchema.parse(args);
880
+ if (!hasRepoAccess(user, repo, wm, authService))
881
+ return notFound(repo);
882
+ try {
883
+ const entity = wm.getGraph(repo).get('artifact', entity_id);
884
+ const bundle = parseArtifactBundleFromBody(entity.document.content, `Artifact ${repo}/${entity_id}`);
885
+ const typedFormat = format;
886
+ let bytes;
887
+ let filename = artifactExportFilename(bundle, typedFormat);
888
+ let mimeType;
889
+ if (typedFormat === 'html') {
890
+ bytes = buildArtifactHtmlExport(bundle, {
891
+ context: 'fullscreen',
892
+ baseHref: base_url ? `${base_url.replace(/\/+$/, '')}/` : undefined,
893
+ includeRenderHostStyles: true,
894
+ librarySources: artifactLibrarySourcesForBundle(bundle),
895
+ });
896
+ }
897
+ else if (typedFormat === 'png') {
898
+ const pngFiles = await renderArtifactPngFiles(bundle, { baseUrl: base_url });
899
+ if (pngFiles.length > 1) {
900
+ const namedFiles = nameArtifactPngFiles(bundle, pngFiles);
901
+ bytes = await zipArtifactPngFiles(namedFiles);
902
+ filename = artifactPngArchiveFilename(bundle);
903
+ mimeType = 'application/zip';
904
+ }
905
+ else {
906
+ bytes = pngFiles[0]?.bytes ?? Buffer.alloc(0);
907
+ }
908
+ }
909
+ else {
910
+ bytes = await renderArtifactPdf(bundle, { baseUrl: base_url });
911
+ }
912
+ return ok({
913
+ success: true,
914
+ repo,
915
+ entity_id,
916
+ format: typedFormat,
917
+ filename,
918
+ ...exportedData(typedFormat, bytes, mimeType),
122
919
  });
123
- return ok({ success: true, id: entity.id, type: entity.entityType });
124
920
  }
125
921
  catch (err) {
922
+ if (err instanceof ArtifactRenderExportUnavailableError) {
923
+ return fail(new Error(`${err.message} Install with: npx playwright install chromium`));
924
+ }
126
925
  return fail(err);
127
926
  }
128
927
  });
928
+ // ── patch_entity ──────────────────────────────────────────────────────────
929
+ //
930
+ // Surgical body edit: replace an exact substring without resending the
931
+ // whole body. The motivating failure was an agent fetching content,
932
+ // renaming a heading, and resending the entire body — which dropped
933
+ // editor-placed image embeds it never saw. patch_entity can't drop what it
934
+ // doesn't send: it reads the CURRENT body (live editor when open, else
935
+ // disk), applies the literal find→replace, and writes the result through
936
+ // the same guarded server-write path. The asset-drop guard still runs, so
937
+ // a patch that happens to delete an embed is rejected unless the substring
938
+ // is itself the embed being intentionally removed.
939
+ const patchEntitySchema = z.object({
940
+ repo: z.string().describe('Folder name'),
941
+ entity_type: z.string().describe('Entity type'),
942
+ entity_id: z.string().describe('Entity ID'),
943
+ find: z.string().describe('Exact substring to locate in the current body (verbatim, including whitespace).'),
944
+ replace: z.string().describe('Replacement text. Empty string deletes the matched substring.'),
945
+ expected_count: z.number().int().positive().optional().describe('Optional safety check: require `find` to occur exactly this many times. The patch is rejected with ' +
946
+ 'NO_MATCH / AMBIGUOUS_MATCH when the actual count differs, so you never edit the wrong occurrence.'),
947
+ });
948
+ reg('patch_entity', {
949
+ description: 'Surgically edit an entry body by replacing an exact substring — without rewriting the whole body. ' +
950
+ 'Prefer this over update_entity for small edits (renaming a heading, fixing a sentence): it operates on ' +
951
+ 'the live editor body and cannot drop image/asset embeds it did not touch. Returns the same diagnostic ' +
952
+ 'flags as update_entity. Use `expected_count` to guard against ambiguous matches.',
953
+ inputSchema: patchEntitySchema,
954
+ }, async (args) => {
955
+ const { repo, entity_type, entity_id, find, replace, expected_count } = patchEntitySchema.parse(args);
956
+ if (!hasRepoAccess(user, repo, wm, authService))
957
+ return notFound(repo);
958
+ if (!canWriteRepo(user, repo, wm, authService)) {
959
+ return fail(new Error(`You do not have write access to folder "${repo}"`));
960
+ }
961
+ if (find === '')
962
+ return fail(new Error('patch_entity requires a non-empty `find` string'));
963
+ if (find === replace)
964
+ return fail(new Error('patch_entity `find` and `replace` are identical — nothing to do'));
965
+ return withAgentActivity(repo, 'editing', async () => {
966
+ try {
967
+ const graph = wm.getGraph(repo);
968
+ const entity = graph.get(entity_type, entity_id);
969
+ // Patch against the live editor body when one is open, so we don't
970
+ // reintroduce the stale-read problem we're fixing.
971
+ const currentBody = graph.getLiveBody(entity_type, entity_id) ?? entity.document.content;
972
+ // Count occurrences of the literal substring (no regex).
973
+ let count = 0;
974
+ for (let i = currentBody.indexOf(find); i !== -1; i = currentBody.indexOf(find, i + find.length))
975
+ count++;
976
+ if (count === 0) {
977
+ return ok({ success: false, code: 'NO_MATCH', message: `\`find\` substring not present in ${entity_type}/${entity_id}.` });
978
+ }
979
+ if (expected_count !== undefined && count !== expected_count) {
980
+ return ok({ success: false, code: 'AMBIGUOUS_MATCH', occurrences: count, message: `Expected ${expected_count} occurrence(s) of \`find\`, found ${count}.` });
981
+ }
982
+ const newBody = currentBody.split(find).join(replace);
983
+ const options = prepareUpdatePayload({ entityType: entity_type, entityId: entity_id, content: newBody }, buildActor(user));
984
+ const { entity: updated, frontmatter_changed, content_changed, noop, broadcast } = await diagnosedUpdate(graph, options, { content: newBody });
985
+ emitEntityChange('updated', repo, updated.entityType, updated.id);
986
+ const repoRoot = graph.getRepoPath();
987
+ const relPath = updated.path.startsWith(repoRoot + '/') ? updated.path.slice(repoRoot.length + 1) : updated.path;
988
+ return ok({
989
+ success: true,
990
+ id: updated.id, type: updated.entityType, repo, path: relPath,
991
+ commit: graph.getGitService()?.getHeadCommit() ?? null,
992
+ occurrences: count, frontmatter_changed, content_changed, noop, broadcast,
993
+ });
994
+ }
995
+ catch (err) {
996
+ if (err instanceof DroppedAssetError) {
997
+ return ok({ success: false, code: 'ASSET_REMOVAL_BLOCKED', droppedAssets: err.dropped, message: err.message });
998
+ }
999
+ return fail(err);
1000
+ }
1001
+ });
1002
+ });
1003
+ // ── list_assets ─────────────────────────────────────────────────────────
1004
+ //
1005
+ // Read-only discovery of assets (images, PDFs, files) already uploaded to a
1006
+ // folder. Shares listRepoAssets() with the HTTP asset endpoints, so the
1007
+ // shape — including the render-safe `embed` string — is identical. Assets
1008
+ // are created only through the upload routes / workspace UI; this tool never
1009
+ // creates one, it only surfaces existing ones so an agent can reference them
1010
+ // in an entry body without guessing a URL or embed syntax.
1011
+ const listAssetsSchema = z.object({
1012
+ repo: z.string().describe('Folder name'),
1013
+ entity_type: z.string().optional().describe('Filter to assets owned by entries of this type'),
1014
+ entity_id: z.string().optional().describe('Filter to assets owned by this entry id'),
1015
+ });
1016
+ reg('list_assets', {
1017
+ description: 'List assets (images, PDFs, files) already uploaded to a folder. Each result includes a ready-to-paste ' +
1018
+ '`embed` — standard markdown `![name](url)` — that renders in the editor. To display an asset in an entry, ' +
1019
+ 'place that exact `embed` string via attach_asset_to_entity (or patch_entity for append/insert). ' +
1020
+ 'NEVER write `![[filename]]` transclusion syntax: it does not resolve to a stored asset and shows as dead ' +
1021
+ 'text. Assets are uploaded via the workspace UI — this tool only references existing ones. Optionally ' +
1022
+ 'filter by entity_type and/or entity_id to list a single entry\'s assets.',
1023
+ inputSchema: listAssetsSchema,
1024
+ }, async (args) => {
1025
+ const { repo, entity_type, entity_id } = listAssetsSchema.parse(args);
1026
+ if (!hasRepoAccess(user, repo, wm, authService))
1027
+ return notFound(repo);
1028
+ try {
1029
+ wm.getGraph(repo);
1030
+ }
1031
+ catch {
1032
+ return notFound(repo);
1033
+ }
1034
+ let assets = await listRepoAssets(repo, wm);
1035
+ if (entity_type)
1036
+ assets = assets.filter(a => a.entityType === entity_type);
1037
+ if (entity_id)
1038
+ assets = assets.filter(a => a.entityId === entity_id);
1039
+ return ok({
1040
+ repo,
1041
+ count: assets.length,
1042
+ assets: assets.map(a => ({
1043
+ asset_id: a.assetId,
1044
+ filename: a.filename,
1045
+ mime_type: a.contentType,
1046
+ entity_type: a.entityType,
1047
+ entity_id: a.entityId,
1048
+ entity_name: a.entityName,
1049
+ url: a.url,
1050
+ embed: a.embed,
1051
+ })),
1052
+ });
1053
+ });
1054
+ // ── attach_asset_to_entity ──────────────────────────────────────────────
1055
+ //
1056
+ // Place an already-uploaded asset (from list_assets) into an entry body
1057
+ // without rewriting the whole body. Thin convenience over the patch_entity
1058
+ // machinery: it builds the render-safe `![alt](url)` embed and either
1059
+ // appends it or swaps a placeholder for it, then writes through the same
1060
+ // guarded server-write path.
1061
+ //
1062
+ // `replace_placeholder` is the sanctioned way to fix a dead `![[Foo.png]]`
1063
+ // placeholder. Note: that swap necessarily *drops* the transclusion target
1064
+ // (`Foo.png`) the asset-drop guard recognizes, so a plain patch_entity of
1065
+ // the same span would be rejected with ASSET_REMOVAL_BLOCKED. Here we pass
1066
+ // allowAssetRemoval for replace_placeholder only — the agent is explicitly
1067
+ // targeting that placeholder and we're re-adding the image as a working
1068
+ // embed, so no real asset is lost. `append` keeps the guard on.
1069
+ const attachAssetSchema = z.object({
1070
+ repo: z.string().describe('Folder name'),
1071
+ entity_type: z.string().describe('Entity type of the target entry'),
1072
+ entity_id: z.string().describe('Entity ID of the target entry'),
1073
+ url: z.string().describe('Asset url, taken verbatim from list_assets (e.g. /api/assets/<id>/<file>)'),
1074
+ alt: z.string().optional().describe('Alt text / caption for the embed. Defaults to the asset filename.'),
1075
+ mode: z.enum(['append', 'replace_placeholder']).default('append').describe('"append" adds the embed at the end of the body. "replace_placeholder" swaps `placeholder` text ' +
1076
+ '(e.g. a dead `![[Foo.png]]` written earlier) for the working embed.'),
1077
+ placeholder: z.string().optional().describe('Required when mode="replace_placeholder": exact text to replace.'),
1078
+ expected_count: z.number().int().positive().optional().describe('For replace_placeholder: require `placeholder` to occur exactly this many times, else reject (AMBIGUOUS_MATCH).'),
1079
+ });
1080
+ reg('attach_asset_to_entity', {
1081
+ description: 'Embed an already-uploaded asset (from list_assets) into an entry body using its render-safe markdown, ' +
1082
+ 'without rewriting the whole body. mode="append" adds it at the end; mode="replace_placeholder" swaps dead ' +
1083
+ 'placeholder text (e.g. an `![[Foo.png]]` an agent wrote earlier) for the working embed. Drop-guarded on ' +
1084
+ 'append: rejects with ASSET_REMOVAL_BLOCKED if it would remove other image embeds.',
1085
+ inputSchema: attachAssetSchema,
1086
+ }, async (args) => {
1087
+ const { repo, entity_type, entity_id, url, alt, mode, placeholder, expected_count } = attachAssetSchema.parse(args);
1088
+ if (!hasRepoAccess(user, repo, wm, authService))
1089
+ return notFound(repo);
1090
+ if (!canWriteRepo(user, repo, wm, authService)) {
1091
+ return fail(new Error(`You do not have write access to folder "${repo}"`));
1092
+ }
1093
+ if (!url.trim())
1094
+ return fail(new Error('attach_asset_to_entity requires a non-empty `url`'));
1095
+ if (mode === 'replace_placeholder' && !placeholder) {
1096
+ return fail(new Error('mode="replace_placeholder" requires a non-empty `placeholder`'));
1097
+ }
1098
+ const filename = alt ?? (url.split('/').pop() || 'asset').split('?')[0];
1099
+ const embed = assetEmbed(filename, url);
1100
+ return withAgentActivity(repo, 'editing', async () => {
1101
+ try {
1102
+ const graph = wm.getGraph(repo);
1103
+ const entity = graph.get(entity_type, entity_id);
1104
+ const currentBody = graph.getLiveBody(entity_type, entity_id) ?? entity.document.content;
1105
+ let newBody;
1106
+ let occurrences;
1107
+ let allowAssetRemoval = false;
1108
+ if (mode === 'replace_placeholder') {
1109
+ const ph = placeholder;
1110
+ let count = 0;
1111
+ for (let i = currentBody.indexOf(ph); i !== -1; i = currentBody.indexOf(ph, i + ph.length))
1112
+ count++;
1113
+ if (count === 0) {
1114
+ return ok({ success: false, code: 'NO_MATCH', message: `placeholder not present in ${entity_type}/${entity_id}.` });
1115
+ }
1116
+ if (expected_count !== undefined && count !== expected_count) {
1117
+ return ok({ success: false, code: 'AMBIGUOUS_MATCH', occurrences: count, message: `Expected ${expected_count} occurrence(s) of placeholder, found ${count}.` });
1118
+ }
1119
+ occurrences = count;
1120
+ newBody = currentBody.split(ph).join(embed);
1121
+ // The placeholder we're removing is itself a recognized embed
1122
+ // target; swapping it for the working embed is the whole point.
1123
+ allowAssetRemoval = true;
1124
+ }
1125
+ else {
1126
+ if (currentBody.includes(embed)) {
1127
+ return ok({ success: false, code: 'ALREADY_PRESENT', message: `Asset embed already present in ${entity_type}/${entity_id}.` });
1128
+ }
1129
+ newBody = currentBody.trimEnd() + '\n\n' + embed + '\n';
1130
+ }
1131
+ const options = prepareUpdatePayload({ entityType: entity_type, entityId: entity_id, content: newBody }, buildActor(user));
1132
+ const { entity: updated, frontmatter_changed, content_changed, noop, broadcast } = await diagnosedUpdate(graph, options, { content: newBody }, { allowAssetRemoval });
1133
+ emitEntityChange('updated', repo, updated.entityType, updated.id);
1134
+ const repoRoot = graph.getRepoPath();
1135
+ const relPath = updated.path.startsWith(repoRoot + '/') ? updated.path.slice(repoRoot.length + 1) : updated.path;
1136
+ return ok({
1137
+ success: true,
1138
+ id: updated.id, type: updated.entityType, repo, path: relPath,
1139
+ embed, mode, occurrences,
1140
+ commit: graph.getGitService()?.getHeadCommit() ?? null,
1141
+ frontmatter_changed, content_changed, noop, broadcast,
1142
+ });
1143
+ }
1144
+ catch (err) {
1145
+ if (err instanceof DroppedAssetError) {
1146
+ return ok({ success: false, code: 'ASSET_REMOVAL_BLOCKED', droppedAssets: err.dropped, message: err.message });
1147
+ }
1148
+ return fail(err);
1149
+ }
1150
+ });
1151
+ });
1152
+ // ── create_asset_upload ─────────────────────────────────────────────────
1153
+ //
1154
+ // Step 1 of 3 for the pre-signed asset upload flow. Validates request,
1155
+ // resolves target folder (defaults to the workspace_wide Assets store),
1156
+ // mints a pre-signed PUT URL the agent uploads to directly, and returns
1157
+ // an HMAC token to pass back to finalize_asset_upload. Bytes never travel
1158
+ // through this MCP server — only authz + indexing do.
1159
+ //
1160
+ // The tool description is load-bearing: it's what the agent reads to
1161
+ // plan the three-step flow. Be explicit about each step and the exact
1162
+ // shape of the PUT.
1163
+ const createAssetUploadSchema = z.object({
1164
+ repo: z.string().optional().describe('Folder name. Omit to upload into the shared "Assets" store (created on first use).'),
1165
+ filename: z.string().describe('Filename including extension (e.g. "logo.png"). Sanitized server-side; no path segments.'),
1166
+ // Registry-driven (P4-V1): the presign path only accepts formats safe as
1167
+ // raw bytes (the app never holds them, so it can't sanitize). SVG +
1168
+ // documents are excluded here and go through upload_asset_inline instead.
1169
+ mime_type: z.enum(PRESIGN_MIME_TUPLE).describe(`Image MIME accepted by the presign flow: ${PRESIGN_MIME_TUPLE.join(', ')}. ` +
1170
+ `SVG and documents are not presignable (use upload_asset_inline).`),
1171
+ });
1172
+ reg('create_asset_upload', {
1173
+ description: 'Upload a large image (PNG/JPEG, max 20MB) via pre-signed PUT — STEP 1 of 3. ' +
1174
+ 'Returns { assetId, uploadUrl, headers, token, expiresAt, repo }.\n\n' +
1175
+ 'STEP 2 (not an MCP tool): HTTP PUT the raw image bytes to `uploadUrl` with the returned `headers` ' +
1176
+ 'verbatim (notably `Content-Type`). The URL expires in 15 minutes.\n\n' +
1177
+ 'STEP 3: call `finalize_asset_upload({ token })` to register the asset and get a permanent URL, then ' +
1178
+ 'optionally `attach_asset_to_entity({ url, ... })` to embed it in an entry body.\n\n' +
1179
+ 'For small files, prefer the one-call upload_asset_inline. ' +
1180
+ 'Errors with code "local_backend" when the workspace uses local filesystem storage (R2 only).',
1181
+ inputSchema: createAssetUploadSchema,
1182
+ }, async (args) => {
1183
+ const parsed = createAssetUploadSchema.parse(args);
1184
+ try {
1185
+ // Asset-as-embed (B2): the upload always produces a FREE-STANDING,
1186
+ // folder-owned asset. There is no entity-ownership handoff — embed it
1187
+ // into an entry afterwards via attach_asset_to_entity.
1188
+ const result = await presignAssetUpload({
1189
+ workspaceManager: wm,
1190
+ authService,
1191
+ user,
1192
+ repo: parsed.repo,
1193
+ filename: parsed.filename,
1194
+ mimeType: parsed.mime_type,
1195
+ });
1196
+ return ok({
1197
+ assetId: result.assetId,
1198
+ uploadUrl: result.uploadUrl,
1199
+ headers: result.headers,
1200
+ token: result.token,
1201
+ expiresAt: result.expiresAt,
1202
+ repo: result.repo,
1203
+ });
1204
+ }
1205
+ catch (err) {
1206
+ return errorToMcpResult(err);
1207
+ }
1208
+ });
1209
+ // ── finalize_asset_upload ───────────────────────────────────────────────
1210
+ //
1211
+ // Step 3 of 3. Verifies the upload landed at the pre-signed URL,
1212
+ // validates content-type + size at the wire, promotes the blob from
1213
+ // `pending/` into the canonical keyspace, and registers a FREE-STANDING,
1214
+ // folder-owned asset (no entity). Idempotent on token replay:
1215
+ // a second call returns the existing assetId + URL.
1216
+ //
1217
+ // Identity-bound (R1): the user calling finalize must be the same user
1218
+ // who minted the token — leaked tokens cannot be replayed by another
1219
+ // folder-writer. Workspace owners are NOT exempt.
1220
+ const finalizeAssetUploadSchema = z.object({
1221
+ token: z.string().describe('HMAC token returned by create_asset_upload.'),
1222
+ name: z.string().optional().describe('Optional display name for the asset entry. Defaults to a title-cased version of the filename stem.'),
1223
+ });
1224
+ reg('finalize_asset_upload', {
1225
+ description: 'STEP 3 of 3. Call this after PUTting bytes to the uploadUrl returned by create_asset_upload. ' +
1226
+ 'Verifies the upload landed, validates content type and size, registers a free-standing asset, and returns ' +
1227
+ '{ assetId, url, ownerType, ownerId, repo }. Embed it into an entry afterwards via attach_asset_to_entity. ' +
1228
+ 'Idempotent: calling with the same token twice returns the existing asset.\n\n' +
1229
+ 'Errors with code "upload_not_received" if the PUT never happened (or the 15-min window expired); ' +
1230
+ '"upload_validation" if content type or size doesn\'t match the presign contract; "token_invalid" if ' +
1231
+ 'the token is tampered or expired; "forbidden" if the caller is not the original presigner or has lost ' +
1232
+ 'folder access in the interim.',
1233
+ inputSchema: finalizeAssetUploadSchema,
1234
+ }, async (args) => {
1235
+ const parsed = finalizeAssetUploadSchema.parse(args);
1236
+ try {
1237
+ const result = await finalizeAssetUpload({
1238
+ workspaceManager: wm,
1239
+ authService,
1240
+ user,
1241
+ token: parsed.token,
1242
+ name: parsed.name,
1243
+ });
1244
+ // Asset-as-embed (B2): finalize registers a FREE-STANDING asset, not an
1245
+ // entity, so there's no entity_change to broadcast — it surfaces in the
1246
+ // asset manager on its next load. Its image caption/OCR IS derived for
1247
+ // search: finalize seeds an assetId-keyed index-state row + notifies the
1248
+ // live caption drain, and the caption folds into any entry that embeds
1249
+ // the asset.
1250
+ return ok({
1251
+ assetId: result.assetId,
1252
+ url: result.url,
1253
+ ownerType: result.ownerType,
1254
+ ownerId: result.ownerId,
1255
+ repo: result.repo,
1256
+ });
1257
+ }
1258
+ catch (err) {
1259
+ return errorToMcpResult(err);
1260
+ }
1261
+ });
1262
+ // ── upload_asset_inline ─────────────────────────────────────────────────────
1263
+ //
1264
+ // V3: single-call asset upload for SMALL files where the bytes can ride
1265
+ // inline (base64). Unlike create_asset_upload (R2-only presign, 3 steps,
1266
+ // bytes never touch the app), this works on local OR R2 backends and is one
1267
+ // round-trip — the tradeoff is a tight size cap (base64 inflates the model
1268
+ // context ~33%, and the bytes traverse this server). For large files or when
1269
+ // an R2 backend is available, prefer the presign flow.
1270
+ //
1271
+ // Indexing reaches web parity: after the asset entity is committed we enqueue
1272
+ // the live caption/extract drain (same as a web upload), so an inline-
1273
+ // uploaded image is captioned while the server runs.
1274
+ const uploadAssetInlineSchema = z.object({
1275
+ repo: z.string().optional().describe('Folder name. Omit to upload into the shared "Assets" store (created on first use).'),
1276
+ filename: z.string().describe('Filename including extension (e.g. "diagram.png"). Sanitized server-side; no path segments.'),
1277
+ data_base64: z.string().describe('The file bytes, base64-encoded. Keep under ~4.5MB raw (6MB base64) — larger files must use create_asset_upload.'),
1278
+ name: z.string().optional().describe('Optional display name for the asset entry. Defaults to a title-cased filename stem.'),
1279
+ folder_path: z.string().optional().describe('Optional repo-relative subfolder for the entry .md (e.g. "Diagrams"). Defaults to repo root.'),
1280
+ });
1281
+ reg('upload_asset_inline', {
1282
+ description: 'Ingest a SMALL file (image or document) as a typed entry in ONE call by passing its bytes ' +
1283
+ 'base64-encoded. The file is stored as a free-standing asset and a typed entry that embeds it ' +
1284
+ 'is created — image→material, doc/pdf→document (extracted text in the body), xlsx/csv→dataset — ' +
1285
+ 'exactly as a web import would. Returns { assetId, url, repo, entities: [{ entityType, entityId }] }.\n\n' +
1286
+ 'Size limit ~4.5MB raw (6MB base64) — for larger files use create_asset_upload (R2 presign, 3 steps). ' +
1287
+ 'Accepted types are the workspace upload set (images + documents); SVG is not yet accepted here. ' +
1288
+ 'Images are captioned for search automatically in the background.',
1289
+ inputSchema: uploadAssetInlineSchema,
1290
+ }, async (args) => {
1291
+ const parsed = uploadAssetInlineSchema.parse(args);
1292
+ // 1. Format gate: resolve the filename to a registry format and check it
1293
+ // directly via isInlineUploadAllowed — NOT via detectContentType, whose
1294
+ // CONTENT_TYPES map returns application/octet-stream for several
1295
+ // registry-accepted assets (.tsv/.psd/.ai/.aac/...), which would wrongly
1296
+ // reject them. Both the gate and the allowlist now derive from the same
1297
+ // registry predicate, so they can't disagree. Rejects SVG (transitional
1298
+ // until sanitize-html), unaccepted docs, and unknown types (fallback).
1299
+ const format = resolveFormat(parsed.filename);
1300
+ if (!isInlineUploadAllowed(format)) {
1301
+ return fail(new Error(`"${parsed.filename}" is not accepted by upload_asset_inline. ` +
1302
+ `Accepted: ${INLINE_UPLOAD_MIME_LIST.join(', ')}.`));
1303
+ }
1304
+ // 2. Decode + size gate. Node's Buffer.from(..,'base64') silently DROPS
1305
+ // invalid characters instead of throwing, so a garbage string would
1306
+ // "decode" to junk bytes. Validate the shape first (strip one optional
1307
+ // data: prefix + whitespace, then assert the strict base64 charset),
1308
+ // and re-encode-compare to catch truncation.
1309
+ const raw = parsed.data_base64.replace(/^data:[^;]+;base64,/, '').replace(/\s+/g, '');
1310
+ if (raw.length === 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(raw) || raw.length % 4 !== 0) {
1311
+ return fail(new Error('data_base64 is not valid base64.'));
1312
+ }
1313
+ const buffer = Buffer.from(raw, 'base64');
1314
+ if (buffer.length === 0 || buffer.toString('base64') !== raw) {
1315
+ return fail(new Error('data_base64 is not valid base64.'));
1316
+ }
1317
+ if (buffer.length > INLINE_MAX_BYTES) {
1318
+ return fail(new Error(`Decoded size ${buffer.length} bytes exceeds the inline ${INLINE_MAX_BYTES}-byte cap. Use create_asset_upload for larger files.`));
1319
+ }
1320
+ // 3. Resolve the target repo (default → shared Assets store) + authz.
1321
+ let repo;
1322
+ try {
1323
+ repo = parsed.repo
1324
+ ?? await resolveWorkspaceAssetsRepo(new Workspace(wm.getWorkspacePath()), wm);
1325
+ }
1326
+ catch (err) {
1327
+ return errorToMcpResult(err);
1328
+ }
1329
+ if (!canWriteRepo(user, repo, wm, authService)) {
1330
+ return fail(`You don't have permission to write to "${repo}". Editor or admin role required.`);
1331
+ }
1332
+ let graph;
1333
+ try {
1334
+ graph = wm.getGraph(repo);
1335
+ }
1336
+ catch (err) {
1337
+ return errorToMcpResult(err);
1338
+ }
1339
+ // 4. Ingest via the asset-as-embed import: store the bytes as a
1340
+ // free-standing folder-owned asset + create the typed entry(ies) whose
1341
+ // body embeds it (image→material, doc→document, xlsx→dataset×N).
1342
+ // Auto-commits. Same path as web import/drag-drop.
1343
+ try {
1344
+ const result = await withAgentActivity(repo, 'creating', () => importAssetAsEntity({
1345
+ graph,
1346
+ filename: parsed.filename,
1347
+ buffer,
1348
+ folderPath: parsed.folder_path || undefined,
1349
+ name: parsed.name,
1350
+ user: buildActor(user),
1351
+ uploaderUserId: String(user.id),
1352
+ }));
1353
+ for (const e of result.entities)
1354
+ emitEntityChange('created', repo, e.entityType, e.entityId);
1355
+ // V3 indexing parity: the created entries' bodies are vector-indexed
1356
+ // natively; the embedded image asset's caption drains in the background.
1357
+ return ok({
1358
+ assetId: result.assetId,
1359
+ url: result.asset_url,
1360
+ repo,
1361
+ entities: result.entities.map(e => ({ entityType: e.entityType, entityId: e.entityId })),
1362
+ });
1363
+ }
1364
+ catch (err) {
1365
+ return errorToMcpResult(err);
1366
+ }
1367
+ });
1368
+ // ── move_entity ───────────────────────────────────────────────────────────
1369
+ //
1370
+ // Relocate an entity to a different folder within the same repo. Wraps
1371
+ // graph.move() which renames the file on disk and commits the rename so
1372
+ // git's similarity detection preserves history. The (type, id) identity
1373
+ // is unchanged — get_entity, search, list, validate_wikilinks, and the
1374
+ // history tools all walk the entire repo tree and continue to find the
1375
+ // entry at its new path. For cross-repo transfers, the web UI's bulk-move
1376
+ // dialog or POST /api/repos/:repo/transfer is the right surface.
1377
+ const moveEntitySchema = z.object({
1378
+ repo: z.string().describe('Folder name containing the entry'),
1379
+ entity_type: z.string().describe('Entity type'),
1380
+ entity_id: z.string().describe('Entity ID'),
1381
+ folder_path: z.string().describe('Target repo-relative folder path (e.g. "People", "Clients/Acme"). ' +
1382
+ 'Empty string moves the entry to the repo root. Parent folders are auto-created.'),
1383
+ overwrite: z.boolean().optional().describe('When true, replace an existing entry at the destination. The occupant ' +
1384
+ 'must itself be a parseable entity — README/.gitkeep/etc. are never ' +
1385
+ 'clobbered. Defaults to false; the call errors with "Path already ' +
1386
+ 'occupied" when the destination is taken.'),
1387
+ });
1388
+ reg('move_entity', {
1389
+ description: 'Move an existing entry to a different folder within the same folder/repo. ' +
1390
+ 'Preserves frontmatter, markdown content, entity ID, and git history (file is renamed, not rewritten). ' +
1391
+ 'Use this to organise entries into subfolders after creation — e.g. moving a person into People/. ' +
1392
+ 'By default, errors if the destination already has an entry; pass `overwrite: true` to replace.',
1393
+ inputSchema: moveEntitySchema,
1394
+ }, async (args) => {
1395
+ const { repo, entity_type, entity_id, folder_path, overwrite } = moveEntitySchema.parse(args);
1396
+ if (!hasRepoAccess(user, repo, wm, authService))
1397
+ return notFound(repo);
1398
+ if (!canWriteRepo(user, repo, wm, authService)) {
1399
+ return fail(new Error(`You do not have write access to folder "${repo}"`));
1400
+ }
1401
+ const actor = buildActor(user);
1402
+ return withAgentActivity(repo, 'editing', async () => {
1403
+ try {
1404
+ const graph = wm.getGraph(repo);
1405
+ // If the destination folder doesn't exist yet, the move creates it —
1406
+ // emit a _folder event (alongside the entry update) so clients
1407
+ // refetch the folder tree, not just the entry list.
1408
+ const normTarget = folder_path ? validateFolderPath(folder_path) : '';
1409
+ const targetExisted = !normTarget || existsSync(join(graph.getRepoPath(), normTarget));
1410
+ const moved = await graph.move(entity_type, entity_id, folder_path, undefined, actor, overwrite);
1411
+ emitEntityChange('updated', repo, moved.entityType, moved.id);
1412
+ if (!targetExisted)
1413
+ emitEntityChange('created', repo, '_folder', normTarget);
1414
+ return ok({
1415
+ success: true,
1416
+ id: moved.id,
1417
+ type: moved.entityType,
1418
+ repo,
1419
+ path: relPathOf(moved.path, graph.getRepoPath()),
1420
+ folder_path: moved.folder_path,
1421
+ });
1422
+ }
1423
+ catch (err) {
1424
+ return fail(err);
1425
+ }
1426
+ });
1427
+ });
1428
+ // ── create_subfolder ──────────────────────────────────────────────────────
1429
+ //
1430
+ // Create a nested subfolder inside an existing folder/collection. Wraps
1431
+ // graph.createFolder(). Distinct from create_repo, which registers a new
1432
+ // top-level collection. Emits the _folder sentinel so live clients refetch
1433
+ // the folder tree (sidebar + list/gallery folder rows) without a reload.
1434
+ const createSubfolderSchema = z.object({
1435
+ repo: z.string().describe('Folder name (collection) to create the subfolder inside'),
1436
+ path: z.string().describe('Repo-relative POSIX path of the new subfolder (e.g. "Clients/Acme"). Parent segments are auto-created.'),
1437
+ display_name: z.string().optional().describe('Optional human-readable label. Omit when the title-cased folder name is the right display ' +
1438
+ '(pass it for casing the slug can\'t express, e.g. "HR" or "Schema_OS").'),
1439
+ });
1440
+ reg('create_subfolder', {
1441
+ description: 'Create a new subfolder inside an existing folder/collection. Path is repo-relative POSIX (e.g. "Clients/Acme"); parent segments are auto-created. ' +
1442
+ 'Use it to organise entries, then place entries inside via create_entity (folder_path) or move_entity. ' +
1443
+ 'For a brand-new top-level collection, use create_repo instead.',
1444
+ inputSchema: createSubfolderSchema,
1445
+ }, async (args) => {
1446
+ const { repo, path, display_name } = createSubfolderSchema.parse(args);
1447
+ if (!hasRepoAccess(user, repo, wm, authService))
1448
+ return notFound(repo);
1449
+ if (!canWriteRepo(user, repo, wm, authService)) {
1450
+ return fail(new Error(`You do not have write access to folder "${repo}"`));
1451
+ }
1452
+ const actor = buildActor(user);
1453
+ return withAgentActivity(repo, 'creating', async () => {
1454
+ try {
1455
+ const graph = wm.getGraph(repo);
1456
+ await graph.createFolder(path, undefined, actor, { displayName: display_name });
1457
+ emitEntityChange('created', repo, '_folder', path);
1458
+ return ok({ success: true, repo, path, folder_path: path });
1459
+ }
1460
+ catch (err) {
1461
+ return fail(err);
1462
+ }
1463
+ });
1464
+ });
129
1465
  // ── validate_wikilinks ────────────────────────────────────────────────────
130
1466
  const validateWikilinksSchema = z.object({
131
- repo: z.string().describe('Collection name'),
1467
+ repo: z.string().describe('Folder name'),
132
1468
  entity_type: z.string().describe('Entity type'),
133
1469
  entity_id: z.string().describe('Entity ID'),
134
1470
  });
135
- server.registerTool('validate_wikilinks', { description: 'Check which wikilinks in an entity resolve to real entities and which are broken', inputSchema: validateWikilinksSchema }, async (args) => {
1471
+ reg('validate_wikilinks', { description: 'Check which wikilinks in an entity resolve to real entities and which are broken', inputSchema: validateWikilinksSchema }, async (args) => {
136
1472
  const { repo, entity_type, entity_id } = validateWikilinksSchema.parse(args);
1473
+ if (!hasRepoAccess(user, repo, wm, authService))
1474
+ return notFound(repo);
137
1475
  try {
138
1476
  const result = wm.getGraph(repo).validateWikilinks(entity_type, entity_id);
139
- return ok({ entity_id, valid: result.valid, broken: result.broken, total: result.valid.length + result.broken.length });
1477
+ const accessibleIds = getAccessibleEntityIds(user, wm, authService);
1478
+ const valid = filterWikilinks(result.valid, accessibleIds);
1479
+ // Anything the user can't see is reported as broken rather than valid —
1480
+ // it shouldn't leak the target's existence.
1481
+ const hiddenFromValid = result.valid.filter(id => !valid.includes(id));
1482
+ const broken = [...result.broken, ...hiddenFromValid];
1483
+ return ok({ entity_id, valid, broken, total: valid.length + broken.length });
140
1484
  }
141
1485
  catch (err) {
142
1486
  return fail(err);
@@ -144,23 +1488,41 @@ export function registerTools(server, wm, authService) {
144
1488
  });
145
1489
  // ── find_related ──────────────────────────────────────────────────────────
146
1490
  const relatedSchema = z.object({
147
- repo: z.string().describe('Collection name'),
1491
+ repo: z.string().describe('Folder name'),
148
1492
  entity_type: z.string().describe('Entity type'),
149
1493
  entity_id: z.string().describe('Entity ID'),
150
1494
  direction: z.enum(['outgoing', 'incoming', 'both']).default('both')
151
1495
  .describe('outgoing = links from this entity, incoming = links to this entity'),
152
1496
  });
153
- server.registerTool('find_related', { description: 'Find entities related via wikilinks (outgoing) or backlinks (incoming)', inputSchema: relatedSchema }, async (args) => {
1497
+ reg('find_related', { description: 'Find entities related via wikilinks (outgoing) or backlinks (incoming)', inputSchema: relatedSchema }, async (args) => {
154
1498
  const { repo, entity_type, entity_id, direction } = relatedSchema.parse(args);
1499
+ if (!hasRepoAccess(user, repo, wm, authService))
1500
+ return notFound(repo);
155
1501
  try {
1502
+ const accessibleIds = getAccessibleEntityIds(user, wm, authService);
156
1503
  const results = [];
157
1504
  if (direction === 'outgoing' || direction === 'both') {
158
1505
  const related = wm.getGraph(repo).getRelated(entity_type, entity_id);
159
- results.push(...related.map(e => shape(e.id, e.entityType, e.data, { direction: 'outgoing' })));
1506
+ for (const e of related) {
1507
+ if (accessibleIds && !accessibleIds.has(e.id))
1508
+ continue;
1509
+ results.push(shape(e.id, e.entityType, e.data, { direction: 'outgoing' }));
1510
+ }
160
1511
  }
161
1512
  if (direction === 'incoming' || direction === 'both') {
1513
+ // Comments anchored to this entry surface as incoming backlinks.
1514
+ if (commentService) {
1515
+ const anchored = commentVirtualEntity.findRelatedComments(commentService, repo, entity_type, entity_id);
1516
+ for (const e of anchored) {
1517
+ results.push(shape(e.id, e.entityType, e.data, { direction: 'incoming', repo }));
1518
+ }
1519
+ }
162
1520
  const backlinks = wm.getBacklinks(repo, entity_type, entity_id);
163
- results.push(...backlinks.map(e => shape(e.id, e.entityType, e.data, { direction: 'incoming', repo: e.repoName })));
1521
+ for (const e of backlinks) {
1522
+ if (!hasRepoAccess(user, e.repoName, wm, authService))
1523
+ continue;
1524
+ results.push(shape(e.id, e.entityType, e.data, { direction: 'incoming', repo: e.repoName }));
1525
+ }
164
1526
  }
165
1527
  return ok(results);
166
1528
  }
@@ -168,84 +1530,370 @@ export function registerTools(server, wm, authService) {
168
1530
  return fail(err);
169
1531
  }
170
1532
  });
171
- // ── View management tools ────────────────────────────────────────────────
172
- if (authService) {
173
- server.registerTool('list_views', { description: 'List the current user\'s saved views.', inputSchema: z.object({}) }, async () => {
174
- try {
175
- return ok(authService.getUserViews(0));
1533
+ // ── list_skills ──────────────────────────────────────────────────────────
1534
+ //
1535
+ // Surfaces user-authored `skill` entities to external MCP clients so
1536
+ // connected agents (Claude Desktop, Cursor, etc.) can discover and run
1537
+ // the workspace's codified workflows. Only graph skills (entities) are
1538
+ // exposed; bundled filesystem skills (onboarding/interview/enrichment
1539
+ // prompts) stay internal to Studiograph's own agent.
1540
+ const listSkillsSchema = z.object({
1541
+ repo: z.string().optional().describe('Folder name — omit to list across all folders you have access to'),
1542
+ });
1543
+ function projectSkill(entity, repo) {
1544
+ const data = entity.data;
1545
+ return {
1546
+ id: entity.id,
1547
+ name: data.name || entity.id,
1548
+ description: data.description ?? '',
1549
+ repo,
1550
+ loading: data.loading === 'eager' ? 'eager' : 'on-demand',
1551
+ };
1552
+ }
1553
+ // `graph.list('skill')` is lenient — root-level .md files without a
1554
+ // declared `entity_type` (READMEs, CHANGELOGs) get tagged as whatever the
1555
+ // caller asks for. For MCP we want only real skills, so require the
1556
+ // frontmatter to explicitly say `entity_type: skill`.
1557
+ function isRealSkill(entity) {
1558
+ return entity.data.entity_type === 'skill';
1559
+ }
1560
+ reg('list_skills', {
1561
+ description: 'List skills (reusable agent instructions) available in the Studiograph workspace. ' +
1562
+ 'Skills are user-authored markdown — call get_skill to read the full body and follow ' +
1563
+ 'its instructions. Optionally filter by folder.',
1564
+ inputSchema: listSkillsSchema,
1565
+ }, async (args) => {
1566
+ const { repo } = listSkillsSchema.parse(args);
1567
+ try {
1568
+ if (repo) {
1569
+ if (!hasRepoAccess(user, repo, wm, authService))
1570
+ return notFound(repo);
1571
+ const skills = wm.getGraph(repo).list('skill');
1572
+ return ok(skills.filter(isRealSkill).map(e => projectSkill(e, repo)));
176
1573
  }
177
- catch (err) {
178
- return fail(err);
1574
+ const accessible = getAccessibleRepos(user, wm, authService);
1575
+ const results = [];
1576
+ for (const r of accessible) {
1577
+ try {
1578
+ const skills = wm.getGraph(r.name).list('skill');
1579
+ for (const e of skills) {
1580
+ if (isRealSkill(e))
1581
+ results.push(projectSkill(e, r.name));
1582
+ }
1583
+ }
1584
+ catch { /* skip unmounted repos */ }
179
1585
  }
180
- });
181
- const createViewSchema = z.object({
182
- name: z.string().describe('View name'),
183
- selected_type: z.string().optional().describe('Entity type filter (e.g. "task")'),
184
- group_by: z.string().optional().describe('Column to group by (e.g. "status")'),
185
- });
186
- server.registerTool('create_view', { description: 'Create a new saved view. Views appear as tabs in the web UI toolbar.', inputSchema: createViewSchema }, async (args) => {
187
- const { name, selected_type, group_by } = createViewSchema.parse(args);
188
- try {
189
- const config = {
190
- selectedType: selected_type ?? null,
191
- groupBy: group_by ?? null,
192
- sortKey: 'name',
193
- sortAsc: true,
194
- filters: { text: {}, enum: {}, date: {} },
195
- };
196
- const view = authService.createUserView(0, name, 'list', config);
197
- return ok({ success: true, view: { id: view.id, name: view.name, config: view.config } });
1586
+ return ok(results);
1587
+ }
1588
+ catch (err) {
1589
+ return fail(err);
1590
+ }
1591
+ });
1592
+ // ── get_skill ────────────────────────────────────────────────────────────
1593
+ //
1594
+ // Returns the full markdown body of a skill alongside its metadata. The
1595
+ // body is instructions to follow — the MCP client is the runtime. No
1596
+ // server-side agent loop, no parameter substitution in v1.
1597
+ const getSkillSchema = z.object({
1598
+ name: z.string().describe('Skill id (the entity_id of the skill entity)'),
1599
+ repo: z.string().optional().describe('Folder to look in. Omit to search across all accessible folders.'),
1600
+ });
1601
+ reg('get_skill', {
1602
+ description: 'Fetch the full content of a skill. Returns the skill\'s markdown body along with its ' +
1603
+ 'metadata. The body contains instructions to follow read it and apply it to the current task.',
1604
+ inputSchema: getSkillSchema,
1605
+ }, async (args) => {
1606
+ const { name, repo } = getSkillSchema.parse(args);
1607
+ try {
1608
+ if (repo) {
1609
+ if (!hasRepoAccess(user, repo, wm, authService))
1610
+ return notFound(repo);
1611
+ try {
1612
+ const entity = wm.getGraph(repo).get('skill', name);
1613
+ if (!isRealSkill({ data: entity.data })) {
1614
+ return fail(new Error(`Skill "${name}" not found`));
1615
+ }
1616
+ return ok({
1617
+ ...projectSkill({ id: entity.id, data: entity.data }, repo),
1618
+ content: entity.document.content,
1619
+ });
1620
+ }
1621
+ catch {
1622
+ return fail(new Error(`Skill "${name}" not found`));
1623
+ }
198
1624
  }
199
- catch (err) {
200
- return fail(err);
1625
+ // No repo — search accessible folders for the skill by id.
1626
+ const accessible = getAccessibleRepos(user, wm, authService);
1627
+ const matches = [];
1628
+ for (const r of accessible) {
1629
+ try {
1630
+ const entity = wm.getGraph(r.name).get('skill', name);
1631
+ if (!isRealSkill({ data: entity.data }))
1632
+ continue;
1633
+ matches.push({ repo: r.name, entity });
1634
+ }
1635
+ catch { /* not in this repo */ }
201
1636
  }
202
- });
203
- const updateViewSchema = z.object({
204
- id: z.number().describe('View ID'),
205
- name: z.string().optional().describe('New view name'),
206
- selected_type: z.string().optional().describe('Entity type filter (use "all" to clear)'),
207
- group_by: z.string().optional().describe('Column to group by (use "none" to clear)'),
208
- });
209
- server.registerTool('update_view', { description: 'Update a saved view\'s name or configuration.', inputSchema: updateViewSchema }, async (args) => {
210
- const { id, name, selected_type, group_by } = updateViewSchema.parse(args);
1637
+ if (matches.length === 0)
1638
+ return fail(new Error(`Skill "${name}" not found`));
1639
+ if (matches.length > 1) {
1640
+ const folders = matches.map(m => m.repo).join(', ');
1641
+ return fail(new Error(`Skill "${name}" exists in multiple folders (${folders}). Specify repo.`));
1642
+ }
1643
+ const { repo: matchRepo, entity } = matches[0];
1644
+ return ok({
1645
+ ...projectSkill({ id: entity.id, data: entity.data }, matchRepo),
1646
+ content: entity.document.content,
1647
+ });
1648
+ }
1649
+ catch (err) {
1650
+ return fail(err);
1651
+ }
1652
+ });
1653
+ // ── Git history tools ──────────────────────────────────────────────────────
1654
+ //
1655
+ // Mirrors the agent's history-tools.ts surface so external MCP clients get
1656
+ // the same primitives. Reads gated on hasRepoAccess; restore_file gated on
1657
+ // canWriteRepo. Single-entity scope only — multi-entity rollback is
1658
+ // intentionally absent (deferred to a future revert_commit).
1659
+ const listFileHistorySchema = z.object({
1660
+ repo: z.string().describe('Folder name'),
1661
+ entity_type: z.string().optional().describe('Entity type as returned by search/list. Optional — resolved by id if omitted or wrong.'),
1662
+ entity_id: z.string().optional().describe('Entry ID. Provide entity_id or path.'),
1663
+ path: z.string().optional().describe('Repo-relative file path from a search/list result. When given, resolves directly.'),
1664
+ limit: z.number().optional().describe('Max commits (default 20, max 100)'),
1665
+ });
1666
+ reg('list_file_history', { description: 'List commits that touched a single entry, newest first. Resolve by type+id or by path; a wrong/omitted type still resolves by id.', inputSchema: listFileHistorySchema }, async (args) => {
1667
+ const { repo, entity_type, entity_id, path, limit } = listFileHistorySchema.parse(args);
1668
+ try {
1669
+ if (!hasRepoAccess(user, repo, wm, authService))
1670
+ return fail(`Folder "${repo}" not found`);
1671
+ if (!entity_id && !path)
1672
+ return fail('list_file_history requires entity_id or path');
1673
+ const graph = wm.getGraph(repo);
1674
+ const gitService = graph.getGitService();
1675
+ if (!gitService)
1676
+ return fail('Folder is not git-backed');
1677
+ let entityPath;
211
1678
  try {
212
- const existing = authService.getUserView(id, 0);
213
- if (!existing)
214
- return fail(`View ${id} not found`);
215
- const fields = {};
216
- if (name)
217
- fields.name = name;
218
- if (selected_type !== undefined || group_by !== undefined) {
219
- const config = { ...existing.config };
220
- if (selected_type !== undefined)
221
- config.selectedType = selected_type === 'all' ? null : selected_type;
222
- if (group_by !== undefined)
223
- config.groupBy = group_by === 'none' ? null : group_by;
224
- fields.config = config;
225
- }
226
- authService.updateUserView(id, 0, fields);
227
- return ok({ success: true });
1679
+ // Same robust resolution as get_entity: path wins; else try the
1680
+ // supplied type; on a miss fall back to id-only.
1681
+ let entity;
1682
+ if (path)
1683
+ entity = graph.getByPath(path);
1684
+ else if (entity_type) {
1685
+ try {
1686
+ entity = graph.get(entity_type, entity_id);
1687
+ }
1688
+ catch {
1689
+ entity = graph.getById(entity_id);
1690
+ }
1691
+ }
1692
+ else
1693
+ entity = graph.getById(entity_id);
1694
+ entityPath = relPathOf(entity.path, graph.getRepoPath());
228
1695
  }
229
- catch (err) {
230
- return fail(err);
1696
+ catch {
1697
+ // Live resolution failed (entry may be deleted) → search recent
1698
+ // deletions by filename.
1699
+ const lookupId = entity_id ?? (path ? path.split('/').pop().replace(/\.md$/, '') : '');
1700
+ const idLower = `${lookupId}.md`.toLowerCase();
1701
+ const match = gitService.listRecentDeletions(30).find((d) => d.filePath.toLowerCase().endsWith(idLower));
1702
+ if (!match)
1703
+ return fail(`Entry ${entity_type ?? entity_id ?? path} not found in "${repo}".`);
1704
+ entityPath = match.filePath;
231
1705
  }
232
- });
233
- const deleteViewSchema = z.object({
234
- id: z.number().describe('View ID to delete'),
235
- });
236
- server.registerTool('delete_view', { description: 'Delete a saved view.', inputSchema: deleteViewSchema }, async (args) => {
237
- const { id } = deleteViewSchema.parse(args);
238
- try {
239
- const existing = authService.getUserView(id, 0);
240
- if (!existing)
241
- return fail(`View ${id} not found`);
242
- authService.deleteUserView(id, 0);
243
- return ok({ success: true, message: `Deleted view "${existing.name}"` });
1706
+ const cap = Math.min(100, Math.max(1, limit ?? 20));
1707
+ const commits = gitService.getFileHistory(entityPath, cap).map((c) => ({
1708
+ hash: c.hash.slice(0, 8),
1709
+ fullHash: c.hash,
1710
+ author: c.author,
1711
+ timestamp: c.timestamp,
1712
+ message: c.message,
1713
+ }));
1714
+ return ok({ repo, entityType: entity_type, entityId: entity_id, commits });
1715
+ }
1716
+ catch (err) {
1717
+ return fail(err);
1718
+ }
1719
+ });
1720
+ const listFolderHistorySchema = z.object({
1721
+ repo: z.string().describe('Folder name'),
1722
+ limit: z.number().optional().describe('Max commits (default 30, max 200)'),
1723
+ });
1724
+ reg('list_folder_history', { description: 'List recent commits across an entire folder.', inputSchema: listFolderHistorySchema }, async (args) => {
1725
+ const { repo, limit } = listFolderHistorySchema.parse(args);
1726
+ try {
1727
+ if (!hasRepoAccess(user, repo, wm, authService))
1728
+ return fail(`Folder "${repo}" not found`);
1729
+ const gitService = wm.getGraph(repo).getGitService();
1730
+ if (!gitService)
1731
+ return fail('Folder is not git-backed');
1732
+ const cap = Math.min(200, Math.max(1, limit ?? 30));
1733
+ const commits = gitService.getHistory(cap).map((c) => ({
1734
+ hash: c.hash.slice(0, 8),
1735
+ fullHash: c.hash,
1736
+ author: c.author,
1737
+ timestamp: c.timestamp,
1738
+ message: c.message,
1739
+ }));
1740
+ return ok({ repo, commits });
1741
+ }
1742
+ catch (err) {
1743
+ return fail(err);
1744
+ }
1745
+ });
1746
+ const listRecentDeletionsSchema = z.object({
1747
+ repo: z.string().describe('Folder name'),
1748
+ within_days: z.number().optional().describe('Lookback window (default 7, max 90)'),
1749
+ });
1750
+ reg('list_recent_deletions', { description: 'List entries deleted within the last N days. Use before restore_file to find a target.', inputSchema: listRecentDeletionsSchema }, async (args) => {
1751
+ const { repo, within_days } = listRecentDeletionsSchema.parse(args);
1752
+ try {
1753
+ if (!hasRepoAccess(user, repo, wm, authService))
1754
+ return fail(`Folder "${repo}" not found`);
1755
+ const gitService = wm.getGraph(repo).getGitService();
1756
+ if (!gitService)
1757
+ return fail('Folder is not git-backed');
1758
+ const days = Math.min(90, Math.max(1, within_days ?? 7));
1759
+ const raw = gitService.listRecentDeletions(days);
1760
+ // Naive coords: use filename basename for entity_id; entity_type
1761
+ // sniffed from frontmatter at lastVersionCommit.
1762
+ const deletions = raw.map((d) => {
1763
+ const idMatch = d.filePath.match(/([^/]+)\.md$/i);
1764
+ const entityId = idMatch ? idMatch[1] : null;
1765
+ const content = entityId ? gitService.getFileAtCommit(d.filePath, d.lastVersionCommit) : null;
1766
+ let entityType = null;
1767
+ if (content) {
1768
+ const fm = content.match(/^---\s*\n([\s\S]*?)\n---/);
1769
+ const tm = fm?.[1].match(/^entity_type:\s*['"]?([\w-]+)['"]?\s*$/m);
1770
+ if (tm)
1771
+ entityType = tm[1];
1772
+ }
1773
+ if (!entityId || !entityType)
1774
+ return null;
1775
+ return {
1776
+ entityType,
1777
+ entityId,
1778
+ deletedAt: d.deletedAt,
1779
+ deletedBy: d.deletedBy.name,
1780
+ deletedInCommit: d.deletedAtCommit.slice(0, 8),
1781
+ lastVersionCommit: d.lastVersionCommit,
1782
+ commitMessage: d.commitMessage,
1783
+ path: d.filePath,
1784
+ };
1785
+ }).filter((x) => x !== null);
1786
+ return ok({ repo, withinDays: days, deletions });
1787
+ }
1788
+ catch (err) {
1789
+ return fail(err);
1790
+ }
1791
+ });
1792
+ const readFileAtCommitSchema = z.object({
1793
+ repo: z.string().describe('Folder name'),
1794
+ entity_type: z.string().describe('Entity type'),
1795
+ entity_id: z.string().describe('Entry ID'),
1796
+ commit_hash: z.string().describe('Commit hash (short or full)'),
1797
+ });
1798
+ reg('read_file_at_commit', { description: 'Read an entry\'s full content at a specific commit.', inputSchema: readFileAtCommitSchema }, async (args) => {
1799
+ const { repo, entity_type, entity_id, commit_hash } = readFileAtCommitSchema.parse(args);
1800
+ try {
1801
+ if (!hasRepoAccess(user, repo, wm, authService))
1802
+ return fail(`Folder "${repo}" not found`);
1803
+ const graph = wm.getGraph(repo);
1804
+ const found = graph.getEntityAtCommit(entity_type, entity_id, commit_hash);
1805
+ if (!found)
1806
+ return fail(`Entry ${entity_type}/${entity_id} not found at commit ${commit_hash}`);
1807
+ // Surface the image/asset embeds present in the body at this commit so
1808
+ // a caller can verify whether images existed here without diffing the
1809
+ // raw markdown by eye. Asset *binaries* aren't in git — these are the
1810
+ // body references, the only image evidence git retains.
1811
+ const embeds = extractAssetEmbeds(stripFrontmatter(found.content));
1812
+ return ok({ repo, entityType: entity_type, entityId: entity_id, commitHash: commit_hash, path: found.path, content: found.content, embeds, embedCount: embeds.length });
1813
+ }
1814
+ catch (err) {
1815
+ return fail(err);
1816
+ }
1817
+ });
1818
+ const restoreFileSchema = z.object({
1819
+ repo: z.string().describe('Folder name'),
1820
+ entity_type: z.string().describe('Entity type'),
1821
+ entity_id: z.string().describe('Entry ID'),
1822
+ commit_hash: z.string().describe('Commit hash to restore from'),
1823
+ confirm: z.boolean().optional().describe('Set to true ONLY after the user has approved the preview. Defaults to false.'),
1824
+ });
1825
+ reg('restore_file', { description: 'Restore an entry to its content at a specific commit. Preview-then-confirm: first call returns the preview payload, re-invoke with confirm: true to execute.', inputSchema: restoreFileSchema }, async (args) => {
1826
+ const { repo, entity_type, entity_id, commit_hash, confirm } = restoreFileSchema.parse(args);
1827
+ try {
1828
+ if (!hasRepoAccess(user, repo, wm, authService))
1829
+ return fail(`Folder "${repo}" not found`);
1830
+ if (!canWriteRepo(user, repo, wm, authService))
1831
+ return fail(`You don't have permission to write to "${repo}". Editor or admin role required.`);
1832
+ const graph = wm.getGraph(repo);
1833
+ if (confirm !== true) {
1834
+ const found = graph.getEntityAtCommit(entity_type, entity_id, commit_hash);
1835
+ if (!found)
1836
+ return fail(`Entry ${entity_type}/${entity_id} not found at commit ${commit_hash}`);
1837
+ let action = 'recreate';
1838
+ try {
1839
+ graph.get(entity_type, entity_id);
1840
+ action = 'overwrite';
1841
+ }
1842
+ catch { }
1843
+ const sizeBytes = Buffer.byteLength(found.content, 'utf-8');
1844
+ const shortHash = commit_hash.length > 8 ? commit_hash.slice(0, 8) : commit_hash;
1845
+ // Embeds the restored body will re-link. Restore rewrites the .md
1846
+ // body only; asset binaries live outside git and are NOT touched by
1847
+ // restore — they remain wherever they are (intact unless separately
1848
+ // deleted), so restoring the body re-links any that still exist.
1849
+ const embeds = extractAssetEmbeds(stripFrontmatter(found.content));
1850
+ const assetNote = embeds.length
1851
+ ? `Restores the body and re-links ${embeds.length} embedded asset reference(s): ${embeds.join(', ')}. ` +
1852
+ `Asset binaries are stored outside git and are not modified by restore (they remain intact unless separately deleted). `
1853
+ : '';
1854
+ return ok({
1855
+ success: false,
1856
+ code: 'PREVIEW_REQUIRED',
1857
+ preview: { action, targetPath: found.path, sizeBytes, commitHash: shortHash, embeds, embedCount: embeds.length },
1858
+ message: `Will ${action} ${entity_type}/${entity_id} from ${shortHash} (${sizeBytes} bytes). ${assetNote}Re-invoke with confirm: true after approval.`,
1859
+ });
244
1860
  }
245
- catch (err) {
246
- return fail(err);
1861
+ const result = await withAgentActivity(repo, 'editing', async () => {
1862
+ const { PathConflictError } = await import('../core/graph.js');
1863
+ try {
1864
+ return await graph.restoreFile(entity_type, entity_id, commit_hash);
1865
+ }
1866
+ catch (err) {
1867
+ if (err instanceof PathConflictError) {
1868
+ return { conflict: { path: err.path, occupiedBy: err.occupiedBy } };
1869
+ }
1870
+ throw err;
1871
+ }
1872
+ });
1873
+ if ('conflict' in result) {
1874
+ return ok({
1875
+ success: false,
1876
+ code: 'PATH_CONFLICT',
1877
+ conflict: result.conflict,
1878
+ message: `Cannot restore — ${result.conflict.path} is occupied by ${result.conflict.occupiedBy.entityType}/${result.conflict.occupiedBy.entityId}.`,
1879
+ });
247
1880
  }
248
- });
249
- }
1881
+ emitEntityChange(result.action === 'recreate' ? 'created' : 'updated', repo, entity_type, entity_id);
1882
+ const shortHash = commit_hash.length > 8 ? commit_hash.slice(0, 8) : commit_hash;
1883
+ return ok({
1884
+ success: true,
1885
+ repo,
1886
+ entityType: entity_type,
1887
+ entityId: entity_id,
1888
+ action: result.action,
1889
+ path: result.path,
1890
+ commitHash: shortHash,
1891
+ message: `Restored ${entity_type}/${entity_id} from ${shortHash} (${result.action}).`,
1892
+ });
1893
+ }
1894
+ catch (err) {
1895
+ return fail(err);
1896
+ }
1897
+ });
250
1898
  }
251
1899
  //# sourceMappingURL=tools.js.map