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
@@ -7,11 +7,20 @@
7
7
  * - Manage wikilinks
8
8
  * - Upload assets
9
9
  */
10
- import { Type as T } from '@sinclair/typebox';
10
+ import { Type as T } from 'typebox';
11
11
  import { ENTITY_SCHEMAS } from '../../core/validation.js';
12
+ import { describeZodObject } from '../../core/schema-registry.js';
12
13
  import { lintWorkspace } from '../../services/lint-service.js';
13
14
  import { findOrphans } from '../../services/orphan-service.js';
14
- import { isAdminOrOwner } from '../../services/auth-service.js';
15
+ import * as access from '../../services/access-control.js';
16
+ import { isCuratedEntityType } from '../../core/entity-types-catalog.js';
17
+ import { getAgentAllowedEntityTypes } from '../prompts/entity-types-section.js';
18
+ import { BridgeError, assertCanChangeUserHandle, validateUserHandleInPayload } from '../../services/user-person-bridge.js';
19
+ import { buildActor, prepareCreatePayload, prepareUpdatePayload, diagnosedUpdate, DroppedAssetError, assertEntityRefs, EntityRefError, UnknownUserRefError, } from '../../services/entity-mutations.js';
20
+ import { listRepoAssets } from '../../services/asset-listing.js';
21
+ import { assetEmbed } from '../../core/embeds.js';
22
+ import * as commentVirtualEntity from '../../services/comment-virtual-entity.js';
23
+ import { promoteScratchFileToAssets, PromoteScratchError } from '../../services/scratch-asset-promote.js';
15
24
  /**
16
25
  * Helper to wrap tool results in AgentToolResult format
17
26
  */
@@ -31,139 +40,251 @@ function cleanData(data) {
31
40
  return rest;
32
41
  }
33
42
  /**
34
- * Per-request user context for collection access filtering.
35
- * When getCurrentUser returns a non-admin user, tools filter results
36
- * to only collections the user has been granted access to.
43
+ * Undo JSON-string escapes that sometimes leak into content when the LLM
44
+ * serializes markdown as a quoted string instead of raw text. Handles `\n`,
45
+ * `\t`, `\r`, `\"`, `\\` in a single pass so escape sequences aren't
46
+ * re-processed. Also strips stray surrounding quotes from a fully-quoted
47
+ * payload.
37
48
  */
38
- /** Check if a user can see a private repo (only the owner can). */
39
- function canSeePrivateRepo(repoConfig, user) {
40
- if (!repoConfig?.private)
41
- return true;
42
- if (!user)
43
- return !repoConfig.owner_id; // API key / open mode — only if no owner set
44
- if (!repoConfig.owner_id)
45
- return false; // Private without owner — hidden from all users
46
- return user.id === repoConfig.owner_id;
47
- }
48
- /** Check if the current user is an admin. Returns an error result if not. */
49
- function adminOnly(getUser) {
50
- const user = getUser();
51
- if (!user)
52
- return null; // API key / open mode → allow
53
- if (isAdminOrOwner(user.role))
54
- return null;
55
- return 'This operation requires admin access.';
49
+ function sanitizeLlmContent(content) {
50
+ if (!content)
51
+ return '';
52
+ const unescaped = content.replace(/\\([ntr"\\])/g, (_, c) => {
53
+ if (c === 'n')
54
+ return '\n';
55
+ if (c === 't')
56
+ return '\t';
57
+ if (c === 'r')
58
+ return '\r';
59
+ return c;
60
+ });
61
+ return unescaped.replace(/^"+|"+$/g, '');
56
62
  }
63
+ /**
64
+ * Per-request user context for folder access filtering.
65
+ * When getCurrentUser returns a user, tools filter results to only
66
+ * folders the user is a member of (via folder_access).
67
+ */
57
68
  function getAccessibleRepoNames(workspaceManager, getCurrentUser, authService) {
58
69
  const user = getCurrentUser();
59
70
  if (!user)
60
71
  return null; // API key / open mode → no filtering
61
72
  if (!authService)
62
73
  return null;
63
- const allRepos = workspaceManager.getAllRepoConfigs();
64
- if (isAdminOrOwner(user.role)) {
65
- // Admins/owners see all non-private + their own private repos
66
- const hidden = allRepos.filter(r => !canSeePrivateRepo(r, user)).map(r => r.name);
67
- if (hidden.length === 0)
68
- return null; // No filtering needed
69
- const visible = new Set(allRepos.filter(r => canSeePrivateRepo(r, user)).map(r => r.name));
70
- return visible;
71
- }
72
- const allowed = authService.getUserCollections(user.id);
73
- const visible = allRepos.filter(r => canSeePrivateRepo(r, user) && (r.private && r.owner_id === user.id || allowed.includes(r.name)));
74
- return new Set(visible.map(r => r.name));
74
+ // Delegate to the shared helper so personal-folder restrictions and
75
+ // owner-on-shared-folder semantics apply consistently with the HTTP layer.
76
+ return new Set(access.getAccessibleRepos(user, workspaceManager, authService).map(r => r.name));
75
77
  }
76
78
  function canAccessRepo(repoName, workspaceManager, getCurrentUser, authService) {
77
79
  const user = getCurrentUser();
78
- const repoConfig = workspaceManager.getRepoConfig(repoName);
79
- if (!canSeePrivateRepo(repoConfig, user))
80
- return false;
81
80
  if (!user)
82
81
  return true;
83
- if (repoConfig?.private && repoConfig?.owner_id === user.id)
84
- return true;
85
- if (isAdminOrOwner(user.role))
86
- return true;
87
82
  if (!authService)
88
83
  return true;
89
- return authService.getUserCollections(user.id).includes(repoName);
84
+ return access.hasRepoAccess(user, repoName, workspaceManager, authService);
90
85
  }
91
- export function createGraphTools(workspaceManager, gitUser, workspacePath, onHighlight, onMutation, getCurrentUser, authService) {
86
+ export function createGraphTools(workspaceManager, gitUser, workspacePath, onHighlight, onMutation, getCurrentUser, authService,
87
+ /**
88
+ * Called at the start (`enter`) and end (`leave`) of any mutation tool
89
+ * call. Surfaces the active-agent chip in the sidebar via the WS hub. The
90
+ * orchestrator wires this through to wsHub.handleAgentActivity.
91
+ */
92
+ onAgentActivity,
93
+ /**
94
+ * Optional CommentService. When supplied, the generic read tools
95
+ * (`get_entity`, `list_entities`, `get_related`) dispatch the virtual
96
+ * `comment` entity type through the shared adapter — exactly as the MCP
97
+ * surface does. A graceful no-op when absent (stdio / tests), so comments
98
+ * simply don't appear rather than erroring.
99
+ */
100
+ commentService,
101
+ /**
102
+ * Optional ChatSessionService. When supplied (web chat), enables
103
+ * `save_file_to_assets` to promote a conversation-scratch image into the
104
+ * curated asset library. A graceful no-op when absent (stdio / tests).
105
+ */
106
+ chatSessionService,
107
+ /**
108
+ * Resolves the active turn's RAW client session id. Lets `save_file_to_assets`
109
+ * scope "this image" to the current conversation's scratch images rather than
110
+ * the user's other chats. Returns null outside a web chat turn.
111
+ */
112
+ getActiveSessionId) {
92
113
  const getUser = getCurrentUser ?? (() => null);
93
114
  const authSvc = authService ?? null;
94
- /** Resolve "private" or "entries" to the current user's actual private collection name. */
115
+ const commentSvc = commentService ?? null;
116
+ const chatSessionSvc = chatSessionService ?? null;
117
+ const getSessionId = getActiveSessionId ?? (() => null);
118
+ /**
119
+ * Resolve the identity to stamp on writes. If the tool call is running on
120
+ * behalf of a real user (chat / quick-capture from the web), credit them
121
+ * by their handle via the shared `buildActor` so MCP and agent surfaces
122
+ * produce byte-identical git authors for the same user. Fall back to the
123
+ * orchestrator-level gitUser for unauthenticated contexts (CLI, tests,
124
+ * server-only automations).
125
+ */
126
+ function effectiveGitUser() {
127
+ const user = getUser();
128
+ if (user?.handle)
129
+ return buildActor(user);
130
+ return gitUser;
131
+ }
132
+ /** Resolve "private" or "entries" to the current user's actual private folder name. */
95
133
  function resolvePrivateRepo(repoName) {
96
134
  if (repoName !== 'private' && repoName !== 'entries')
97
135
  return repoName;
98
136
  const user = getUser();
99
137
  if (!user)
100
138
  return repoName;
101
- const privateRepo = workspaceManager.getUserPrivateRepo(user.id);
102
- return privateRepo?.name ?? repoName;
139
+ const personalRepo = workspaceManager.getUserPersonalRepo(user.id);
140
+ return personalRepo?.name ?? repoName;
141
+ }
142
+ /**
143
+ * Eager skills are inlined into every system prompt for everyone with folder
144
+ * access. Creating one in a shared folder requires admin/owner so a rogue
145
+ * member can't bloat everyone's context. Eager skills in the user's own
146
+ * private folder are fine — they only bloat that user's own prompt.
147
+ * Returns an error string or null if the operation is allowed.
148
+ */
149
+ function checkEagerSkillPermission(entityType, data, repoName) {
150
+ if (entityType !== 'skill')
151
+ return null;
152
+ if (!data || data.loading !== 'eager')
153
+ return null;
154
+ const user = getUser();
155
+ if (!user)
156
+ return null; // non-authenticated contexts (CLI, tests) bypass
157
+ const personalRepo = workspaceManager.getUserPersonalRepo(user.id);
158
+ if (personalRepo && personalRepo.name === repoName)
159
+ return null;
160
+ if (authSvc?.isFolderAdmin(user.id, repoName))
161
+ return null;
162
+ if (user.role === 'owner')
163
+ return null;
164
+ return `Creating an eager skill in a shared folder requires folder admin or workspace owner. Save this skill to your private folder (repo: "private") or set loading to "on-demand".`;
165
+ }
166
+ // Compute the set of entity types the agent is allowed to create in this
167
+ // workspace: the full canonical catalog plus any custom types defined via
168
+ // schema_extensions. The launch-time allowlist was removed — the catalog
169
+ // now grows with the user via the New Entry picker's progressive disclosure.
170
+ const builtinTypeKeys = new Set(Object.keys(ENTITY_SCHEMAS));
171
+ const customWorkspaceTypes = workspaceManager.getSchemaRegistry().getAllTypes()
172
+ .filter(t => !builtinTypeKeys.has(t));
173
+ const agentAllowedTypes = getAgentAllowedEntityTypes(customWorkspaceTypes);
174
+ const agentAllowedTypeSet = new Set(agentAllowedTypes);
175
+ function rejectDisallowedCreateType(entityType) {
176
+ if (agentAllowedTypeSet.has(entityType))
177
+ return null;
178
+ const hint = isCuratedEntityType(entityType)
179
+ ? '' // shouldn't happen for any other curated type — they're all allowed
180
+ : ` Allowed types: ${agentAllowedTypes.join(', ')}.`;
181
+ return wrapToolResult({
182
+ success: false,
183
+ error: `Entry type "${entityType}" is not a known canonical or workspace custom type.${hint}`,
184
+ });
103
185
  }
104
186
  return [
105
187
  // Create entity
106
188
  {
107
189
  name: 'create_entity',
108
190
  label: 'Create Entry',
109
- description: 'Create a new entry in a collection. Always validate required fields before calling. Collection is auto-resolved for app-defined entry types.',
191
+ description: 'Create a new entry in a folder. Always validate required fields before calling. Folder is auto-resolved for app-defined entry types.',
110
192
  parameters: T.Object({
111
- repo: T.Optional(T.String({ description: 'Collection name (auto-resolved for app entity types)' })),
112
- entityType: T.String({ description: 'Entity type (project, client, decision, etc.)' }),
193
+ repo: T.Optional(T.String({ description: 'Folder name (auto-resolved for app entity types)' })),
194
+ entityType: T.Union(agentAllowedTypes.map(t => T.Literal(t)), { description: `Entity type. Must be one of: ${agentAllowedTypes.join(', ')}.` }),
113
195
  entityId: T.String({ description: 'Unique entity ID in kebab-case' }),
114
196
  data: T.Object({}, { description: 'Entity data (YAML frontmatter)', additionalProperties: true }),
115
- content: T.Optional(T.String({ description: 'Markdown content (optional)' })),
197
+ content: T.Optional(T.String({ description: 'Markdown content (optional). Write one line per paragraph — newlines only for real paragraph/list breaks, not to wrap prose at a column width (entries render source line breaks literally).' })),
198
+ folder_path: T.Optional(T.String({ description: 'Repo-relative subfolder for the entry (e.g. "clients/acme"). Omit to land at repo root. Parent folders are auto-created.' })),
116
199
  commitMessage: T.Optional(T.String({ description: 'Custom commit message (optional)' })),
117
200
  }),
118
201
  execute: async (toolCallId, params) => {
119
202
  try {
203
+ // Defense in depth: LLMs occasionally pass strings that don't match the
204
+ // enum schema. Runtime check ensures the launch allowlist is enforced
205
+ // even if the schema layer is bypassed.
206
+ const typeRejection = rejectDisallowedCreateType(params.entityType);
207
+ if (typeRejection)
208
+ return typeRejection;
120
209
  let repoName = params.repo;
121
210
  if (!repoName) {
122
- repoName = workspaceManager.getAppForEntityType(params.entityType);
123
- if (!repoName)
124
- return wrapToolResult({ success: false, error: `repo is required — could not auto-resolve repo for type "${params.entityType}"` });
211
+ return wrapToolResult({ success: false, error: `repo is required — must specify which folder to use for type "${params.entityType}"` });
125
212
  }
126
213
  repoName = resolvePrivateRepo(repoName);
127
214
  if (!canAccessRepo(repoName, workspaceManager, getUser, authSvc)) {
128
- return wrapToolResult({ success: false, error: `Collection "${repoName}" not found` });
215
+ return wrapToolResult({ success: false, error: `Folder "${repoName}" not found` });
129
216
  }
217
+ const eagerError = checkEagerSkillPermission(params.entityType, params.data, repoName);
218
+ if (eagerError)
219
+ return wrapToolResult({ success: false, error: eagerError });
130
220
  const graph = workspaceManager.getGraph(repoName);
131
221
  // Workspace-level dedup: warn if an entity with this ID already exists elsewhere
132
222
  const existing = workspaceManager.findEntityById(params.entityId);
133
223
  if (existing && existing.repoName !== repoName) {
134
- return wrapToolResult({ success: false, error: `An entity with ID "${params.entityId}" already exists in the "${existing.repoName}" collection (type: ${existing.data?.entity_type}). Use a unique ID or link to the existing entity with [[${params.entityId}]].` });
224
+ return wrapToolResult({ success: false, error: `An entity with ID "${params.entityId}" already exists in the "${existing.repoName}" folder (type: ${existing.data?.entity_type}). Use a unique ID or link to the existing entity with [[${params.entityId}]].` });
135
225
  }
136
- // Add timestamps and user info
137
- const now = new Date().toISOString();
138
- const dataWithMeta = {
139
- ...params.data,
140
- entity_id: params.entityId,
141
- created_at: now,
142
- updated_at: now,
143
- created_by: gitUser.id,
144
- updated_by: gitUser.id,
145
- };
146
- // Sanitize LLM JSON output: double-escaped newlines and stray quotes
147
- let sanitizedContent = (params.content || '').replace(/\\n/g, '\n').replace(/^"+|"+$/g, '');
148
- const entity = await graph.create({
226
+ const actor = effectiveGitUser();
227
+ const sanitizedContent = sanitizeLlmContent(params.content);
228
+ // Workspace-wide uniqueness check for `user_handle` on person
229
+ // entities (one user ↔ one person, workspace-wide), plus owner-
230
+ // only authz so a non-owner agent session can't stamp a
231
+ // pre-linked person via generic create_entity.
232
+ if (params.entityType === 'person') {
233
+ try {
234
+ const incoming = params.data ?? {};
235
+ validateUserHandleInPayload(workspaceManager, incoming);
236
+ if (Object.prototype.hasOwnProperty.call(incoming, 'user_handle')) {
237
+ assertCanChangeUserHandle(getUser(), '', incoming.user_handle);
238
+ }
239
+ }
240
+ catch (err) {
241
+ if (err instanceof BridgeError) {
242
+ return wrapToolResult({ success: false, error: err.message, code: err.code, ...err.details });
243
+ }
244
+ throw err;
245
+ }
246
+ }
247
+ // Scope ref fields (today: task.assignee → person). Reject a wikilink
248
+ // that resolves to a non-person; forward references are allowed.
249
+ try {
250
+ assertEntityRefs(workspaceManager, params.entityType, params.data ?? {}, authSvc ? { userExists: (id) => !!authSvc.findUserById(Number(id)) } : undefined);
251
+ }
252
+ catch (err) {
253
+ if (err instanceof EntityRefError || err instanceof UnknownUserRefError) {
254
+ return wrapToolResult({ success: false, error: err.message, code: err.code, field: err.field });
255
+ }
256
+ throw err;
257
+ }
258
+ const options = prepareCreatePayload({
149
259
  entityType: params.entityType,
150
260
  entityId: params.entityId,
151
- data: dataWithMeta,
261
+ data: params.data ?? {},
152
262
  content: sanitizedContent,
263
+ folderPath: params.folder_path,
153
264
  commitMessage: params.commitMessage,
154
- });
155
- onMutation?.({ action: 'created', repo: repoName, entityType: params.entityType, entityId: params.entityId });
156
- return wrapToolResult({
157
- success: true,
158
- entity: {
159
- entityId: entity.id,
160
- entityType: entity.entityType,
161
- repo: repoName,
162
- path: entity.path,
163
- data: cleanData(entity.data),
164
- },
165
- message: `Created ${params.entityType}: ${params.entityId} in ${repoName}. Auto-committed — ready to push.`,
166
- });
265
+ }, actor);
266
+ // Surface a live "<user>'s agent is acting" chip in the sidebar for
267
+ // the duration of the mutation. `leave` is always emitted via the
268
+ // inner finally so a thrown error doesn't leave the chip stuck.
269
+ onAgentActivity?.('enter', repoName);
270
+ try {
271
+ const entity = await graph.create(options);
272
+ onMutation?.({ action: 'created', repo: repoName, entityType: params.entityType, entityId: params.entityId });
273
+ return wrapToolResult({
274
+ success: true,
275
+ entity: {
276
+ entityId: entity.id,
277
+ entityType: entity.entityType,
278
+ repo: repoName,
279
+ path: entity.path,
280
+ data: cleanData(entity.data),
281
+ },
282
+ message: `Created ${params.entityType}: ${params.entityId} in ${repoName}. Auto-committed — ready to push.`,
283
+ });
284
+ }
285
+ finally {
286
+ onAgentActivity?.('leave', repoName);
287
+ }
167
288
  }
168
289
  catch (error) {
169
290
  return wrapToolResult({
@@ -179,7 +300,7 @@ export function createGraphTools(workspaceManager, gitUser, workspacePath, onHig
179
300
  label: 'Get Entry',
180
301
  description: 'Retrieve full entry details. Use repo, entityType, entityId from search/list results. entityType is optional — if omitted it is read from the file.',
181
302
  parameters: T.Object({
182
- repo: T.String({ description: 'Collection name' }),
303
+ repo: T.String({ description: 'Folder name' }),
183
304
  entityType: T.Optional(T.String({ description: 'Entity type (optional — resolved from frontmatter if omitted)' })),
184
305
  entityId: T.String({ description: 'Entity ID' }),
185
306
  }),
@@ -187,12 +308,50 @@ export function createGraphTools(workspaceManager, gitUser, workspacePath, onHig
187
308
  try {
188
309
  const repo = resolvePrivateRepo(params.repo);
189
310
  if (!canAccessRepo(repo, workspaceManager, getUser, authSvc)) {
190
- return wrapToolResult({ success: false, error: `Collection "${repo}" not found` });
311
+ return wrapToolResult({ success: false, error: `Folder "${repo}" not found` });
312
+ }
313
+ // Comments live in SQLite, not the filesystem graph — dispatch
314
+ // through the shared virtual-entity adapter (mirrors the MCP surface).
315
+ if (params.entityType === 'comment' && commentSvc) {
316
+ const envelope = commentVirtualEntity.getComment(commentSvc, params.entityId);
317
+ if (!envelope || envelope.data.repo !== repo) {
318
+ return wrapToolResult({ success: false, error: `Comment "${params.entityId}" not found` });
319
+ }
320
+ return wrapToolResult({
321
+ success: true,
322
+ entity: {
323
+ entityId: envelope.id,
324
+ entityType: envelope.entityType,
325
+ repo,
326
+ data: cleanData(envelope.data),
327
+ content: envelope.document.content,
328
+ wikilinks: envelope.document.wikilinks,
329
+ },
330
+ });
191
331
  }
192
332
  const graph = workspaceManager.getGraph(repo);
193
- const entity = params.entityType
194
- ? graph.get(params.entityType, params.entityId)
195
- : graph.getById(params.entityId);
333
+ // If the agent passes the wrong entityType (e.g. based on stale
334
+ // search results), graph.get computes the wrong filesystem path
335
+ // and throws. Fall back to getById, which scans all type subdirs
336
+ // under the folder and resolves the actual location.
337
+ let entity;
338
+ if (params.entityType) {
339
+ try {
340
+ entity = graph.get(params.entityType, params.entityId);
341
+ }
342
+ catch {
343
+ entity = graph.getById(params.entityId);
344
+ }
345
+ }
346
+ else {
347
+ entity = graph.getById(params.entityId);
348
+ }
349
+ // Prefer the live editor body when a seeded Y.Doc is open. graph.get
350
+ // reads disk, which lags the editor by the autosave debounce — a
351
+ // user's freshly-placed image embed lives in the Y.Doc before it's
352
+ // committed. Returning disk content is what let the agent edit then
353
+ // resend a body missing those embeds, silently dropping images.
354
+ const liveBody = graph.getLiveBody(entity.entityType, entity.id);
196
355
  return wrapToolResult({
197
356
  success: true,
198
357
  entity: {
@@ -201,7 +360,7 @@ export function createGraphTools(workspaceManager, gitUser, workspacePath, onHig
201
360
  repo,
202
361
  path: entity.path,
203
362
  data: cleanData(entity.data),
204
- content: entity.document.content,
363
+ content: liveBody ?? entity.document.content,
205
364
  wikilinks: entity.document.wikilinks,
206
365
  },
207
366
  });
@@ -218,73 +377,435 @@ export function createGraphTools(workspaceManager, gitUser, workspacePath, onHig
218
377
  {
219
378
  name: 'update_entity',
220
379
  label: 'Update Entry',
221
- description: 'Update an existing entry. Collection is auto-resolved for app-defined types.',
380
+ description: 'Update an existing entry. Folder is auto-resolved for app-defined types.',
222
381
  parameters: T.Object({
223
- repo: T.Optional(T.String({ description: 'Collection name (auto-resolved for app entity types)' })),
382
+ repo: T.Optional(T.String({ description: 'Folder name (auto-resolved for app entity types)' })),
224
383
  entityType: T.String({ description: 'Entity type' }),
225
384
  entityId: T.String({ description: 'Entity ID' }),
226
385
  data: T.Optional(T.Object({}, { description: 'Updated data fields', additionalProperties: true })),
227
- content: T.Optional(T.String({ description: 'Updated markdown content' })),
386
+ content: T.Optional(T.String({ description: 'Updated markdown body (replaces the entire body). Write one line per paragraph — newlines only for real paragraph/list breaks, not to wrap prose at a column width (entries render source line breaks literally).' })),
228
387
  commitMessage: T.Optional(T.String({ description: 'Custom commit message' })),
388
+ allowImageRemoval: T.Optional(T.Boolean({ description: 'Set true only to deliberately delete image/asset embeds the document currently has; otherwise such a whole-body write is rejected (ASSET_REMOVAL_BLOCKED). Prefer get_entity then patch_entity for small edits.' })),
229
389
  }),
230
390
  execute: async (toolCallId, params) => {
231
391
  try {
232
392
  let repoName = params.repo;
233
393
  if (!repoName) {
234
- repoName = workspaceManager.getAppForEntityType(params.entityType);
235
- if (!repoName)
236
- return wrapToolResult({ success: false, error: `repo is required — could not auto-resolve repo for type "${params.entityType}"` });
394
+ return wrapToolResult({ success: false, error: `repo is required — must specify which folder to use for type "${params.entityType}"` });
237
395
  }
238
396
  repoName = resolvePrivateRepo(repoName);
239
397
  if (!canAccessRepo(repoName, workspaceManager, getUser, authSvc)) {
240
- return wrapToolResult({ success: false, error: `Collection "${repoName}" not found` });
398
+ return wrapToolResult({ success: false, error: `Folder "${repoName}" not found` });
241
399
  }
400
+ const eagerError = checkEagerSkillPermission(params.entityType, params.data, repoName);
401
+ if (eagerError)
402
+ return wrapToolResult({ success: false, error: eagerError });
242
403
  const graph = workspaceManager.getGraph(repoName);
243
- // Add update metadata
244
- const dataWithMeta = params.data
245
- ? {
246
- ...params.data,
247
- updated_at: new Date().toISOString(),
248
- updated_by: gitUser.id,
249
- }
250
- : undefined;
251
- // Sanitize LLM JSON output: double-escaped newlines and stray quotes
404
+ const actor = effectiveGitUser();
252
405
  const sanitizedContent = params.content !== undefined
253
- ? params.content.replace(/\\n/g, '\n').replace(/^"+|"+$/g, '') : params.content;
254
- const entity = await graph.update({
406
+ ? sanitizeLlmContent(params.content)
407
+ : params.content;
408
+ // Workspace-wide uniqueness check + owner-only authz for
409
+ // `user_handle` on person entities. Generic update_entity must
410
+ // not be a back door around the ownerOnly() gate on the dedicated
411
+ // link/unlink tools — otherwise a member could ask the agent to
412
+ // "set this person's user_handle to mine" and bypass the gate.
413
+ if (params.entityType === 'person' && params.data) {
414
+ try {
415
+ validateUserHandleInPayload(workspaceManager, params.data, { repo: repoName, entityId: params.entityId });
416
+ if (Object.prototype.hasOwnProperty.call(params.data, 'user_handle')) {
417
+ let currentHandle = '';
418
+ try {
419
+ const current = graph.get('person', params.entityId);
420
+ currentHandle = (current?.data?.user_handle ?? '');
421
+ }
422
+ catch { /* new entity / missing — treat as empty */ }
423
+ assertCanChangeUserHandle(getUser(), currentHandle, params.data.user_handle);
424
+ }
425
+ }
426
+ catch (err) {
427
+ if (err instanceof BridgeError) {
428
+ return wrapToolResult({ success: false, error: err.message, code: err.code, ...err.details });
429
+ }
430
+ throw err;
431
+ }
432
+ }
433
+ // Scope ref fields (today: task.assignee → person). Reject a wikilink
434
+ // that resolves to a non-person; forward references are allowed.
435
+ try {
436
+ assertEntityRefs(workspaceManager, params.entityType, params.data, authSvc ? { userExists: (id) => !!authSvc.findUserById(Number(id)) } : undefined);
437
+ }
438
+ catch (err) {
439
+ if (err instanceof EntityRefError || err instanceof UnknownUserRefError) {
440
+ return wrapToolResult({ success: false, error: err.message, code: err.code, field: err.field });
441
+ }
442
+ throw err;
443
+ }
444
+ const options = prepareUpdatePayload({
255
445
  entityType: params.entityType,
256
446
  entityId: params.entityId,
257
- data: dataWithMeta,
447
+ data: params.data,
258
448
  content: sanitizedContent,
259
449
  commitMessage: params.commitMessage,
450
+ }, actor);
451
+ onAgentActivity?.('enter', repoName);
452
+ try {
453
+ // Shared diagnosed update (c6ef9297 contract): throws
454
+ // SilentDropError when the caller intended a change that left the
455
+ // file byte-identical, surfaced via the outer catch so the agent
456
+ // isn't misled by a success envelope.
457
+ const { entity, frontmatter_changed, content_changed, noop, broadcast } = await diagnosedUpdate(graph, options, { data: params.data, content: sanitizedContent }, { allowAssetRemoval: params.allowImageRemoval });
458
+ onMutation?.({ action: 'updated', repo: repoName, entityType: params.entityType, entityId: params.entityId });
459
+ return wrapToolResult({
460
+ success: true,
461
+ entity: {
462
+ entityId: entity.id,
463
+ entityType: entity.entityType,
464
+ repo: repoName,
465
+ data: cleanData(entity.data),
466
+ },
467
+ frontmatter_changed,
468
+ content_changed,
469
+ noop,
470
+ broadcast,
471
+ message: noop
472
+ ? `No change to ${params.entityType}: ${params.entityId} — on-disk state already matched.`
473
+ : `Updated ${params.entityType}: ${params.entityId}. Auto-committed — ready to push.`,
474
+ });
475
+ }
476
+ finally {
477
+ onAgentActivity?.('leave', repoName);
478
+ }
479
+ }
480
+ catch (error) {
481
+ if (error instanceof DroppedAssetError) {
482
+ return wrapToolResult({
483
+ success: false,
484
+ code: 'ASSET_REMOVAL_BLOCKED',
485
+ droppedAssets: error.dropped,
486
+ error: error.message,
487
+ });
488
+ }
489
+ return wrapToolResult({
490
+ success: false,
491
+ error: error instanceof Error ? error.message : 'Unknown error',
260
492
  });
261
- onMutation?.({ action: 'updated', repo: repoName, entityType: params.entityType, entityId: params.entityId });
493
+ }
494
+ },
495
+ },
496
+ // Surgical body edit (find/replace) — parity with the MCP patch_entity.
497
+ // Lets the in-app agent change a small slice of a long entry without
498
+ // regenerating (and risking dropping image embeds from) the whole body.
499
+ {
500
+ name: 'patch_entity',
501
+ label: 'Patch Entry',
502
+ description: 'Surgically edit an entry body by replacing an exact substring — without rewriting the whole body. ' +
503
+ 'Prefer this over update_entity for small edits (rename a heading, fix a sentence): it edits the live ' +
504
+ 'editor body and cannot drop image/asset embeds it did not touch. Use expectedCount to guard against ' +
505
+ 'ambiguous matches. For data/frontmatter changes use update_entity.',
506
+ parameters: T.Object({
507
+ repo: T.String({ description: 'Folder name' }),
508
+ entityType: T.String({ description: 'Entity type' }),
509
+ entityId: T.String({ description: 'Entity ID' }),
510
+ find: T.String({ description: 'Exact substring to locate in the current body (verbatim, including whitespace).' }),
511
+ replace: T.String({ description: 'Replacement text. Empty string deletes the matched substring.' }),
512
+ expectedCount: T.Optional(T.Number({ description: 'Optional safety check: require `find` to occur exactly this many times, else reject with NO_MATCH / AMBIGUOUS_MATCH.' })),
513
+ commitMessage: T.Optional(T.String({ description: 'Custom commit message' })),
514
+ }),
515
+ execute: async (toolCallId, params) => {
516
+ try {
517
+ const repoName = resolvePrivateRepo(params.repo);
518
+ if (!canAccessRepo(repoName, workspaceManager, getUser, authSvc)) {
519
+ return wrapToolResult({ success: false, error: `Folder "${repoName}" not found` });
520
+ }
521
+ if (params.find === '') {
522
+ return wrapToolResult({ success: false, error: 'patch_entity requires a non-empty `find` string' });
523
+ }
524
+ if (params.find === params.replace) {
525
+ return wrapToolResult({ success: false, error: 'patch_entity `find` and `replace` are identical — nothing to do' });
526
+ }
527
+ const graph = workspaceManager.getGraph(repoName);
528
+ const entity = graph.get(params.entityType, params.entityId);
529
+ // Patch against the live editor body when one is open, so we don't
530
+ // reintroduce the stale-read problem (editing a body that lags the
531
+ // user's in-editor typing / freshly-placed embeds).
532
+ const currentBody = graph.getLiveBody(params.entityType, params.entityId) ?? entity.document.content;
533
+ // Count literal (non-regex) occurrences.
534
+ let count = 0;
535
+ for (let i = currentBody.indexOf(params.find); i !== -1; i = currentBody.indexOf(params.find, i + params.find.length))
536
+ count++;
537
+ if (count === 0) {
538
+ return wrapToolResult({ success: false, code: 'NO_MATCH', error: `\`find\` substring not present in ${params.entityType}/${params.entityId}.` });
539
+ }
540
+ if (params.expectedCount !== undefined && count !== params.expectedCount) {
541
+ return wrapToolResult({ success: false, code: 'AMBIGUOUS_MATCH', occurrences: count, error: `Expected ${params.expectedCount} occurrence(s) of \`find\`, found ${count}.` });
542
+ }
543
+ const newBody = currentBody.split(params.find).join(params.replace);
544
+ const actor = effectiveGitUser();
545
+ const options = prepareUpdatePayload({
546
+ entityType: params.entityType,
547
+ entityId: params.entityId,
548
+ content: newBody,
549
+ commitMessage: params.commitMessage,
550
+ }, actor);
551
+ onAgentActivity?.('enter', repoName);
552
+ try {
553
+ const { entity: updated, frontmatter_changed, content_changed, noop, broadcast } = await diagnosedUpdate(graph, options, { content: newBody });
554
+ onMutation?.({ action: 'updated', repo: repoName, entityType: params.entityType, entityId: params.entityId });
555
+ return wrapToolResult({
556
+ success: true,
557
+ entity: {
558
+ entityId: updated.id,
559
+ entityType: updated.entityType,
560
+ repo: repoName,
561
+ data: cleanData(updated.data),
562
+ },
563
+ occurrences: count,
564
+ frontmatter_changed,
565
+ content_changed,
566
+ noop,
567
+ broadcast,
568
+ message: `Patched ${params.entityType}: ${params.entityId} (${count} occurrence${count === 1 ? '' : 's'}). Auto-committed — ready to push.`,
569
+ });
570
+ }
571
+ finally {
572
+ onAgentActivity?.('leave', repoName);
573
+ }
574
+ }
575
+ catch (error) {
576
+ if (error instanceof DroppedAssetError) {
577
+ return wrapToolResult({ success: false, code: 'ASSET_REMOVAL_BLOCKED', droppedAssets: error.dropped, error: error.message });
578
+ }
579
+ return wrapToolResult({ success: false, error: error instanceof Error ? error.message : 'Unknown error' });
580
+ }
581
+ },
582
+ },
583
+ // List assets already uploaded to a folder
584
+ {
585
+ name: 'list_assets',
586
+ label: 'List Assets',
587
+ description: 'List images/files already uploaded to a folder, with a ready-to-paste markdown embed for each. ' +
588
+ 'Use this to find an asset a user placed (e.g. a screenshot) so you can embed it into an entry — ' +
589
+ 'assets live in storage (R2/local), NOT on the local filesystem, so import_file cannot find them. ' +
590
+ 'Each result has an `embed` string: use it verbatim and NEVER write ![[filename]] transclusion syntax ' +
591
+ '(it does not render). Optionally filter by entityType/entityId. This tool only references existing ' +
592
+ 'assets — uploads happen via the workspace UI or import_file.',
593
+ parameters: T.Object({
594
+ repo: T.String({ description: 'Folder name' }),
595
+ entityType: T.Optional(T.String({ description: 'Filter to assets owned by entries of this type' })),
596
+ entityId: T.Optional(T.String({ description: 'Filter to assets owned by this entry id' })),
597
+ }),
598
+ execute: async (toolCallId, params) => {
599
+ try {
600
+ const repoName = resolvePrivateRepo(params.repo);
601
+ if (!canAccessRepo(repoName, workspaceManager, getUser, authSvc)) {
602
+ return wrapToolResult({ success: false, error: `Folder "${repoName}" not found` });
603
+ }
604
+ let assets = await listRepoAssets(repoName, workspaceManager);
605
+ if (params.entityType)
606
+ assets = assets.filter(a => a.entityType === params.entityType);
607
+ if (params.entityId)
608
+ assets = assets.filter(a => a.entityId === params.entityId);
262
609
  return wrapToolResult({
263
610
  success: true,
264
- entity: {
265
- entityId: entity.id,
266
- entityType: entity.entityType,
267
- repo: repoName,
268
- data: cleanData(entity.data),
269
- },
270
- message: `Updated ${params.entityType}: ${params.entityId}. Auto-committed — ready to push.`,
611
+ repo: repoName,
612
+ count: assets.length,
613
+ assets: assets.map(a => ({
614
+ asset_id: a.assetId,
615
+ filename: a.filename,
616
+ mime_type: a.contentType,
617
+ entityType: a.entityType,
618
+ entityId: a.entityId,
619
+ entityName: a.entityName,
620
+ url: a.url,
621
+ embed: a.embed,
622
+ })),
271
623
  });
272
624
  }
273
625
  catch (error) {
626
+ return wrapToolResult({ success: false, error: error instanceof Error ? error.message : 'Unknown error' });
627
+ }
628
+ },
629
+ },
630
+ // Embed an already-uploaded asset into an entry body
631
+ {
632
+ name: 'attach_asset_to_entity',
633
+ label: 'Attach Asset',
634
+ description: 'Embed an already-uploaded asset (from list_assets) into an entry body using its render-safe markdown, ' +
635
+ 'without rewriting the whole body. mode "append" adds it at the end; mode "replace_placeholder" swaps dead ' +
636
+ 'placeholder text (e.g. an ![[Foo.png]] written earlier) for the working embed. Drop-guarded: rejects with ' +
637
+ 'ASSET_REMOVAL_BLOCKED if it would remove other image embeds (append only — replace_placeholder is exempt ' +
638
+ 'since the placeholder it removes is the dead reference being fixed).',
639
+ parameters: T.Object({
640
+ repo: T.String({ description: 'Folder name' }),
641
+ entityType: T.String({ description: 'Entity type of the target entry' }),
642
+ entityId: T.String({ description: 'Entity ID of the target entry' }),
643
+ url: T.String({ description: 'Asset url, taken verbatim from list_assets' }),
644
+ alt: T.Optional(T.String({ description: 'Alt text / caption. Defaults to the asset filename.' })),
645
+ mode: T.Optional(T.Union([T.Literal('append'), T.Literal('replace_placeholder')], { description: 'append (default) or replace_placeholder' })),
646
+ placeholder: T.Optional(T.String({ description: 'Required for replace_placeholder: exact text to replace' })),
647
+ expectedCount: T.Optional(T.Number({ description: 'For replace_placeholder: require this many occurrences, else reject' })),
648
+ }),
649
+ execute: async (toolCallId, params) => {
650
+ try {
651
+ const repoName = resolvePrivateRepo(params.repo);
652
+ if (!canAccessRepo(repoName, workspaceManager, getUser, authSvc)) {
653
+ return wrapToolResult({ success: false, error: `Folder "${repoName}" not found` });
654
+ }
655
+ const url = params.url ?? '';
656
+ if (!url.trim())
657
+ return wrapToolResult({ success: false, error: 'url is required' });
658
+ const mode = params.mode ?? 'append';
659
+ if (mode === 'replace_placeholder' && !params.placeholder) {
660
+ return wrapToolResult({ success: false, error: 'mode "replace_placeholder" requires a non-empty placeholder' });
661
+ }
662
+ const filename = params.alt ?? (url.split('/').pop() || 'asset').split('?')[0];
663
+ const embed = assetEmbed(filename, url);
664
+ const graph = workspaceManager.getGraph(repoName);
665
+ const entity = graph.get(params.entityType, params.entityId);
666
+ const currentBody = graph.getLiveBody(params.entityType, params.entityId) ?? entity.document.content;
667
+ let newBody;
668
+ let occurrences;
669
+ let allowAssetRemoval = false;
670
+ if (mode === 'replace_placeholder') {
671
+ const ph = params.placeholder;
672
+ let count = 0;
673
+ for (let i = currentBody.indexOf(ph); i !== -1; i = currentBody.indexOf(ph, i + ph.length))
674
+ count++;
675
+ if (count === 0) {
676
+ return wrapToolResult({ success: false, code: 'NO_MATCH', error: `placeholder not present in ${params.entityType}/${params.entityId}.` });
677
+ }
678
+ if (params.expectedCount !== undefined && count !== params.expectedCount) {
679
+ return wrapToolResult({ success: false, code: 'AMBIGUOUS_MATCH', occurrences: count, error: `Expected ${params.expectedCount} occurrence(s) of placeholder, found ${count}.` });
680
+ }
681
+ occurrences = count;
682
+ newBody = currentBody.split(ph).join(embed);
683
+ // The placeholder we're removing is itself a recognized embed
684
+ // target; swapping it for the working embed is the whole point.
685
+ allowAssetRemoval = true;
686
+ }
687
+ else {
688
+ if (currentBody.includes(embed)) {
689
+ return wrapToolResult({ success: false, code: 'ALREADY_PRESENT', error: `Asset embed already present in ${params.entityType}/${params.entityId}.` });
690
+ }
691
+ newBody = currentBody.trimEnd() + '\n\n' + embed + '\n';
692
+ }
693
+ const options = prepareUpdatePayload({
694
+ entityType: params.entityType,
695
+ entityId: params.entityId,
696
+ content: newBody,
697
+ }, effectiveGitUser());
698
+ onAgentActivity?.('enter', repoName);
699
+ try {
700
+ const { entity: updated, frontmatter_changed, content_changed, noop, broadcast } = await diagnosedUpdate(graph, options, { content: newBody }, { allowAssetRemoval });
701
+ onMutation?.({ action: 'updated', repo: repoName, entityType: params.entityType, entityId: params.entityId });
702
+ return wrapToolResult({
703
+ success: true,
704
+ entity: { entityId: updated.id, entityType: updated.entityType, repo: repoName },
705
+ embed,
706
+ mode,
707
+ occurrences,
708
+ frontmatter_changed,
709
+ content_changed,
710
+ noop,
711
+ broadcast,
712
+ message: `Embedded ${filename} into ${params.entityType}: ${params.entityId}. Auto-committed — ready to push.`,
713
+ });
714
+ }
715
+ finally {
716
+ onAgentActivity?.('leave', repoName);
717
+ }
718
+ }
719
+ catch (error) {
720
+ if (error instanceof DroppedAssetError) {
721
+ return wrapToolResult({ success: false, code: 'ASSET_REMOVAL_BLOCKED', droppedAssets: error.dropped, error: error.message });
722
+ }
723
+ return wrapToolResult({ success: false, error: error instanceof Error ? error.message : 'Unknown error' });
724
+ }
725
+ },
726
+ },
727
+ // Promote a composer/chat scratch file (image or doc) into the asset library.
728
+ {
729
+ name: 'save_file_to_assets',
730
+ label: 'Save File to Assets',
731
+ description: 'Save a file the user attached to THIS chat (a composer/scratch image or document) into the curated ' +
732
+ 'asset library so it persists beyond the conversation. Use this when the user says something like "save ' +
733
+ 'this to my assets". By default it saves the most-recently-attached file to the shared Assets folder ' +
734
+ '(the user can then move it wherever they like); pass `filename` to disambiguate when several are ' +
735
+ 'attached, or `folder` to save into a specific folder you have write access to. Only works for files ' +
736
+ 'the user attached to the chat (and not ones already picked from the library) — it cannot save arbitrary ' +
737
+ 'URLs or filesystem paths.',
738
+ parameters: T.Object({
739
+ filename: T.Optional(T.String({ description: 'Which attached file to save, by filename (defaults to the most recent).' })),
740
+ folder: T.Optional(T.String({ description: 'Destination folder name. Defaults to the shared Assets library.' })),
741
+ }),
742
+ execute: async (toolCallId, params) => {
743
+ try {
744
+ const user = getUser();
745
+ if (!user)
746
+ return wrapToolResult({ success: false, error: 'Saving files requires a signed-in user.' });
747
+ if (!chatSessionSvc) {
748
+ return wrapToolResult({ success: false, error: 'Saving chat files is not available in this context.' });
749
+ }
750
+ const sessionId = getSessionId();
751
+ if (!sessionId) {
752
+ return wrapToolResult({ success: false, code: 'NO_SESSION', error: 'No active conversation to save a file from.' });
753
+ }
754
+ // Scope to THIS conversation's OWN uploads (not library picks, which are
755
+ // already saved; not the user's other chats).
756
+ const files = chatSessionSvc.getScratchFilesBySession(sessionId)
757
+ .filter((f) => f.userId === user.id && f.source === 'upload');
758
+ if (files.length === 0) {
759
+ return wrapToolResult({ success: false, code: 'NO_FILE', error: 'No uploaded file is attached to this chat to save.' });
760
+ }
761
+ // Resolve which scratch file: by filename if given, else most recent.
762
+ let target = files[0];
763
+ if (params.filename) {
764
+ const want = String(params.filename).toLowerCase();
765
+ const match = files.find(f => f.filename.toLowerCase() === want)
766
+ ?? files.find(f => f.filename.toLowerCase().includes(want));
767
+ if (!match) {
768
+ return wrapToolResult({
769
+ success: false, code: 'NO_MATCH',
770
+ error: `No attached file matches "${params.filename}".`,
771
+ attached: files.map(f => f.filename),
772
+ });
773
+ }
774
+ target = match;
775
+ }
776
+ const folder = params.folder ? resolvePrivateRepo(params.folder) : undefined;
777
+ const result = await promoteScratchFileToAssets(workspaceManager, chatSessionSvc, authSvc, {
778
+ sessionId: target.sessionId,
779
+ fileId: target.fileId,
780
+ user,
781
+ targetRepo: folder,
782
+ });
783
+ onMutation?.({ action: 'updated', repo: result.repo, entityType: 'material', entityId: result.assetId, repoSync: true });
274
784
  return wrapToolResult({
275
- success: false,
276
- error: error instanceof Error ? error.message : 'Unknown error',
785
+ success: true,
786
+ asset_id: result.assetId,
787
+ filename: result.filename,
788
+ name: result.displayName,
789
+ url: result.url,
790
+ repo: result.repo,
791
+ message: `Saved "${result.displayName}" to the ${result.repo} folder. You can move it to another folder from the asset library.`,
277
792
  });
278
793
  }
794
+ catch (error) {
795
+ if (error instanceof PromoteScratchError) {
796
+ return wrapToolResult({ success: false, code: error.code, error: error.message });
797
+ }
798
+ return wrapToolResult({ success: false, error: error instanceof Error ? error.message : 'Unknown error' });
799
+ }
279
800
  },
280
801
  },
281
802
  // Delete entity
282
803
  {
283
804
  name: 'delete_entity',
284
805
  label: 'Delete Entry',
285
- description: 'Delete an entry. Use with caution! Collection is auto-resolved for app-defined types.',
806
+ description: 'Delete an entry. Use with caution! Folder is auto-resolved for app-defined types.',
286
807
  parameters: T.Object({
287
- repo: T.Optional(T.String({ description: 'Collection name (auto-resolved for app entity types)' })),
808
+ repo: T.Optional(T.String({ description: 'Folder name (auto-resolved for app entity types)' })),
288
809
  entityType: T.String({ description: 'Entity type' }),
289
810
  entityId: T.String({ description: 'Entity ID' }),
290
811
  commitMessage: T.Optional(T.String({ description: 'Custom commit message' })),
@@ -293,21 +814,39 @@ export function createGraphTools(workspaceManager, gitUser, workspacePath, onHig
293
814
  try {
294
815
  let repoName = params.repo;
295
816
  if (!repoName) {
296
- repoName = workspaceManager.getAppForEntityType(params.entityType);
297
- if (!repoName)
298
- return wrapToolResult({ success: false, error: `repo is required — could not auto-resolve repo for type "${params.entityType}"` });
817
+ return wrapToolResult({ success: false, error: `repo is required — must specify which folder to use for type "${params.entityType}"` });
299
818
  }
300
819
  repoName = resolvePrivateRepo(repoName);
301
820
  if (!canAccessRepo(repoName, workspaceManager, getUser, authSvc)) {
302
- return wrapToolResult({ success: false, error: `Collection "${repoName}" not found` });
821
+ return wrapToolResult({ success: false, error: `Folder "${repoName}" not found` });
303
822
  }
304
823
  const graph = workspaceManager.getGraph(repoName);
305
- await graph.delete(params.entityType, params.entityId, params.commitMessage);
306
- onMutation?.({ action: 'deleted', repo: repoName, entityType: params.entityType, entityId: params.entityId });
307
- return wrapToolResult({
308
- success: true,
309
- message: `Deleted ${params.entityType}: ${params.entityId}. Auto-committed — ready to push.`,
310
- });
824
+ // Soft bridge-awareness: deleting a person linked to an auth account
825
+ // orphans that user. Don't block surface a warning so the agent can
826
+ // tell the user, and the Profile panel catches the orphan after.
827
+ let bridgeWarning;
828
+ if (params.entityType === 'person') {
829
+ try {
830
+ const handle = graph.get('person', params.entityId)?.data?.user_handle;
831
+ if (typeof handle === 'string' && handle.length > 0) {
832
+ bridgeWarning = `Note: this record was linked to the "${handle}" user account. Deleting it unlinks them — their "My Tasks", @mentions, and assignments stop resolving until they link a new record.`;
833
+ }
834
+ }
835
+ catch { /* not found — let graph.delete surface it */ }
836
+ }
837
+ onAgentActivity?.('enter', repoName);
838
+ try {
839
+ await graph.delete(params.entityType, params.entityId, params.commitMessage);
840
+ onMutation?.({ action: 'deleted', repo: repoName, entityType: params.entityType, entityId: params.entityId });
841
+ return wrapToolResult({
842
+ success: true,
843
+ message: `Deleted ${params.entityType}: ${params.entityId}. Auto-committed — ready to push.`,
844
+ ...(bridgeWarning ? { warning: bridgeWarning } : {}),
845
+ });
846
+ }
847
+ finally {
848
+ onAgentActivity?.('leave', repoName);
849
+ }
311
850
  }
312
851
  catch (error) {
313
852
  return wrapToolResult({
@@ -321,46 +860,94 @@ export function createGraphTools(workspaceManager, gitUser, workspacePath, onHig
321
860
  {
322
861
  name: 'import_file',
323
862
  label: 'Import File',
324
- description: 'Import a local file as a Studiograph entry. Supports .md and .txt (→ note) and .csv (→ dataset). The file is read from the local filesystem and converted to an entry with appropriate frontmatter.',
863
+ description: 'Import a local file as a Studiograph entry. Text formats become typed entries (.md/.txt → note, .csv → dataset). Binary formats become typed entries that embed a free-standing asset: images/opaque files material, PDF/Word → document, spreadsheets → dataset.',
325
864
  parameters: T.Object({
326
865
  path: T.String({ description: 'Absolute path or ~/relative path to the file' }),
327
- collection: T.String({ description: 'Target collection name' }),
328
- entityType: T.Optional(T.String({ description: 'Override entity type (default: auto-detect from extension)' })),
866
+ folder: T.String({ description: 'Target folder name' }),
867
+ entityType: T.Optional(T.String({ description: 'Override entity type (default: auto-detect from extension; ignored for binary imports, which route by kind)' })),
329
868
  entityId: T.Optional(T.String({ description: 'Override entity ID (default: from filename)' })),
330
869
  }),
331
870
  execute: async (toolCallId, params) => {
332
871
  try {
333
- if (!canAccessRepo(params.collection, workspaceManager, getUser, authSvc)) {
334
- return wrapToolResult({ success: false, error: `Collection "${params.collection}" not found` });
872
+ if (!canAccessRepo(params.folder, workspaceManager, getUser, authSvc)) {
873
+ return wrapToolResult({ success: false, error: `Folder "${params.folder}" not found` });
335
874
  }
336
875
  const { readFileSync, existsSync } = await import('fs');
337
- const { resolve } = await import('path');
876
+ const { resolve, basename, sep } = await import('path');
338
877
  const { homedir } = await import('os');
339
878
  const { importFile, isSupportedFile } = await import('../../services/import-service.js');
879
+ const { isAssetUploadExtension } = await import('../../services/assets/base.js');
880
+ const { importAssetAsEntity } = await import('../../services/asset-import-service.js');
340
881
  const filePath = params.path.startsWith('~/')
341
882
  ? resolve(homedir(), params.path.slice(2))
342
883
  : resolve(params.path);
343
884
  if (!existsSync(filePath)) {
344
885
  return wrapToolResult({ success: false, error: `File not found: ${filePath}` });
345
886
  }
887
+ const graph = workspaceManager.getGraph(params.folder);
888
+ const fname = basename(filePath);
889
+ // Path-exfil guard: the agent reads bytes off disk and uploads
890
+ // them as a workspace asset, accessible via /api/assets/* by any
891
+ // folder member. Without an allowlist, an LLM prompt injection in
892
+ // an imported note can ask the agent to grab ~/.ssh/id_rsa, .env
893
+ // files, kubeconfigs, etc. by renaming them to a permitted
894
+ // extension. Restrict to roots a user would plausibly drop files
895
+ // from: the workspace itself, ~/Downloads, ~/Documents.
896
+ const home = homedir();
897
+ const workspaceRoot = graph.getWorkspaceRoot();
898
+ const allowedRoots = [
899
+ workspaceRoot,
900
+ resolve(home, 'Downloads'),
901
+ resolve(home, 'Documents'),
902
+ ].filter((r) => Boolean(r)).map(r => r.endsWith(sep) ? r : r + sep);
903
+ const isInAllowedRoot = allowedRoots.some(root => filePath === root.slice(0, -1) || filePath.startsWith(root));
904
+ if (!isInAllowedRoot) {
905
+ return wrapToolResult({
906
+ success: false,
907
+ error: `Path "${filePath}" is outside the import allowlist. Files must live under the workspace, ~/Downloads, or ~/Documents.`,
908
+ });
909
+ }
910
+ // Binary extensions → asset-as-embed import: store a free-standing
911
+ // asset + create the typed entry(ies) that embed it (image→material,
912
+ // doc/pdf→document, xlsx→dataset×N). The text-import path doesn't know
913
+ // how to handle .indd / .pptx / etc. Parity with the web import
914
+ // dispatcher (importAssetAsEntity).
915
+ if (isAssetUploadExtension(fname)) {
916
+ const buffer = readFileSync(filePath);
917
+ const result = await importAssetAsEntity({
918
+ graph,
919
+ filename: fname,
920
+ buffer,
921
+ });
922
+ for (const e of result.entities) {
923
+ onMutation?.({ action: 'created', repo: params.folder, entityType: e.entityType, entityId: e.entityId });
924
+ }
925
+ const first = result.entities[0];
926
+ return wrapToolResult({
927
+ success: true,
928
+ entityType: first?.entityType,
929
+ entityId: first?.entityId,
930
+ assetId: result.assetId,
931
+ entities: result.entities.map(e => ({ entityType: e.entityType, entityId: e.entityId })),
932
+ message: `Imported ${fname} as ${result.entities.map(e => `${e.entityType}/${e.entityId}`).join(', ')} in ${params.folder}`,
933
+ });
934
+ }
346
935
  if (!isSupportedFile(filePath)) {
347
- return wrapToolResult({ success: false, error: `Unsupported file type. Supported: .md, .txt, .csv` });
936
+ return wrapToolResult({ success: false, error: `Unsupported file type for ${fname}` });
348
937
  }
349
- const graph = workspaceManager.getGraph(params.collection);
350
938
  const content = readFileSync(filePath);
351
- const { basename } = await import('path');
352
- const result = await importFile(graph, basename(filePath), content, {
939
+ const result = await importFile(graph, fname, content, {
353
940
  entityType: params.entityType,
354
941
  entityId: params.entityId,
355
942
  });
356
943
  if (result.status === 'created') {
357
- onMutation?.({ action: 'created', repo: params.collection, entityType: result.entityType, entityId: result.entityId });
944
+ onMutation?.({ action: 'created', repo: params.folder, entityType: result.entityType, entityId: result.entityId });
358
945
  }
359
946
  return wrapToolResult({
360
947
  success: result.status === 'created',
361
948
  ...result,
362
949
  message: result.status === 'created'
363
- ? `Imported ${basename(filePath)} as ${result.entityType}/${result.entityId} in ${params.collection}`
950
+ ? `Imported ${fname} as ${result.entityType}/${result.entityId} in ${params.folder}`
364
951
  : result.error,
365
952
  });
366
953
  }
@@ -376,9 +963,9 @@ export function createGraphTools(workspaceManager, gitUser, workspacePath, onHig
376
963
  {
377
964
  name: 'rename_entity',
378
965
  label: 'Rename Entry',
379
- description: 'Rename an entry by changing its ID. Preserves all data, content, and source_ref. The old ID will no longer exist. Collection is auto-resolved for app-defined types.',
966
+ description: 'Rename an entry by changing its ID. Preserves all data, content, and source_ref. The old ID will no longer exist. Folder is auto-resolved for app-defined types.',
380
967
  parameters: T.Object({
381
- repo: T.Optional(T.String({ description: 'Collection name (auto-resolved for app entity types)' })),
968
+ repo: T.Optional(T.String({ description: 'Folder name (auto-resolved for app entity types)' })),
382
969
  entityType: T.String({ description: 'Entity type' }),
383
970
  oldId: T.String({ description: 'Current entity ID' }),
384
971
  newId: T.String({ description: 'New entity ID (kebab-case)' }),
@@ -387,13 +974,11 @@ export function createGraphTools(workspaceManager, gitUser, workspacePath, onHig
387
974
  try {
388
975
  let repoName = params.repo;
389
976
  if (!repoName) {
390
- repoName = workspaceManager.getAppForEntityType(params.entityType);
391
- if (!repoName)
392
- return wrapToolResult({ success: false, error: `repo is required — could not auto-resolve repo for type "${params.entityType}"` });
977
+ return wrapToolResult({ success: false, error: `repo is required — must specify which folder to use for type "${params.entityType}"` });
393
978
  }
394
979
  repoName = resolvePrivateRepo(repoName);
395
980
  if (!canAccessRepo(repoName, workspaceManager, getUser, authSvc)) {
396
- return wrapToolResult({ success: false, error: `Collection "${repoName}" not found` });
981
+ return wrapToolResult({ success: false, error: `Folder "${repoName}" not found` });
397
982
  }
398
983
  const graph = workspaceManager.getGraph(repoName);
399
984
  const result = await graph.rename(params.entityType, params.oldId, params.newId);
@@ -413,41 +998,41 @@ export function createGraphTools(workspaceManager, gitUser, workspacePath, onHig
413
998
  }
414
999
  },
415
1000
  },
416
- // Move entity between repos
1001
+ // Transfer entity across repos (cross-repo move). Distinct from
1002
+ // folder-tools' `move_entity`, which relocates within one repo.
417
1003
  {
418
- name: 'move_entity',
419
- label: 'Move Entry',
420
- description: 'Move an entry from one collection to another (admin only). Preserves all data, content, and wikilinks.',
1004
+ name: 'transfer_entity',
1005
+ label: 'Transfer Entry',
1006
+ description: 'Move an entry from one repo to another. Preserves all data, content, and wikilinks. Requires access to both folders. For relocating within the same repo (folder_path change), use move_entity instead.',
421
1007
  parameters: T.Object({
422
- sourceRepo: T.String({ description: 'Source collection name' }),
423
- targetRepo: T.String({ description: 'Target collection name' }),
1008
+ sourceRepo: T.String({ description: 'Source repo name' }),
1009
+ targetRepo: T.String({ description: 'Target repo name' }),
424
1010
  entityType: T.String({ description: 'Entity type' }),
425
1011
  entityId: T.String({ description: 'Entity ID' }),
426
1012
  commitMessage: T.Optional(T.String({ description: 'Custom commit message (optional)' })),
427
1013
  }),
428
1014
  execute: async (toolCallId, params) => {
429
1015
  try {
430
- const denied = adminOnly(getUser);
431
- if (denied)
432
- return wrapToolResult({ success: false, error: denied });
433
1016
  if (!canAccessRepo(params.sourceRepo, workspaceManager, getUser, authSvc)) {
434
- return wrapToolResult({ success: false, error: `Collection "${params.sourceRepo}" not found` });
1017
+ return wrapToolResult({ success: false, error: `Folder "${params.sourceRepo}" not found` });
435
1018
  }
436
1019
  if (!canAccessRepo(params.targetRepo, workspaceManager, getUser, authSvc)) {
437
- return wrapToolResult({ success: false, error: `Collection "${params.targetRepo}" not found` });
1020
+ return wrapToolResult({ success: false, error: `Folder "${params.targetRepo}" not found` });
438
1021
  }
439
1022
  const sourceGraph = workspaceManager.getGraph(params.sourceRepo);
440
1023
  const targetGraph = workspaceManager.getGraph(params.targetRepo);
441
1024
  // Read the full entity from source
442
1025
  const entity = sourceGraph.get(params.entityType, params.entityId);
443
1026
  // Create in target repo
1027
+ const actor = effectiveGitUser();
444
1028
  const now = new Date().toISOString();
445
1029
  await targetGraph.create({
446
1030
  entityType: params.entityType,
447
1031
  entityId: params.entityId,
448
- data: { ...entity.data, updated_at: now, updated_by: gitUser.id },
1032
+ data: { ...entity.data, updated_at: now, updated_by: actor.id },
449
1033
  content: entity.document.content || '',
450
1034
  commitMessage: params.commitMessage || `Move ${params.entityType} ${params.entityId} from ${params.sourceRepo} to ${params.targetRepo}`,
1035
+ user: actor,
451
1036
  });
452
1037
  // Delete from source repo
453
1038
  await sourceGraph.delete(params.entityType, params.entityId, `Move ${params.entityType} ${params.entityId} to ${params.targetRepo}`);
@@ -470,11 +1055,11 @@ export function createGraphTools(workspaceManager, gitUser, workspacePath, onHig
470
1055
  {
471
1056
  name: 'search_entities',
472
1057
  label: 'Search Entries',
473
- description: 'Search for entries across collections with filters.',
1058
+ description: 'Search for entries across folders with filters.',
474
1059
  parameters: T.Object({
475
1060
  query: T.Optional(T.String({ description: 'Search query (full-text)' })),
476
1061
  entityType: T.Optional(T.String({ description: 'Filter by entity type' })),
477
- repoNames: T.Optional(T.Array(T.String(), { description: 'Limit to specific collections' })),
1062
+ repoNames: T.Optional(T.Array(T.String(), { description: 'Limit to specific folders' })),
478
1063
  visibility: T.Optional(T.Array(T.String(), { description: 'Filter by visibility (public, team, admin, restricted)' })),
479
1064
  tags: T.Optional(T.Array(T.String(), { description: 'Filter by tags' })),
480
1065
  dateRange: T.Optional(T.Object({
@@ -486,7 +1071,7 @@ export function createGraphTools(workspaceManager, gitUser, workspacePath, onHig
486
1071
  }),
487
1072
  execute: async (toolCallId, params) => {
488
1073
  try {
489
- // Scope search to accessible collections
1074
+ // Scope search to accessible folders
490
1075
  const allowed = getAccessibleRepoNames(workspaceManager, getUser, authSvc);
491
1076
  let repoNames = params.repoNames?.map((r) => resolvePrivateRepo(r));
492
1077
  if (allowed) {
@@ -534,8 +1119,32 @@ export function createGraphTools(workspaceManager, gitUser, workspacePath, onHig
534
1119
  }),
535
1120
  execute: async (toolCallId, params) => {
536
1121
  try {
1122
+ // Comments live in SQLite, not the filesystem graph — dispatch
1123
+ // through the shared virtual-entity adapter (mirrors the MCP surface),
1124
+ // filtering to the folders the caller can see.
1125
+ if (params.entityType === 'comment' && commentSvc) {
1126
+ const allowedComments = getAccessibleRepoNames(workspaceManager, getUser, authSvc);
1127
+ // Filter to accessible folders BEFORE applying the caller's limit:
1128
+ // a pre-filter SQL cap could be consumed entirely by comments in
1129
+ // folders the caller can't see, starving the visible ones. The
1130
+ // service still bounds the underlying fetch (<=500) as a backstop.
1131
+ const visible = commentVirtualEntity
1132
+ .listComments(commentSvc, {})
1133
+ .filter(e => !allowedComments || allowedComments.has(e.data.repo));
1134
+ const envelopes = params.limit ? visible.slice(0, params.limit) : visible;
1135
+ return wrapToolResult({
1136
+ success: true,
1137
+ count: envelopes.length,
1138
+ results: envelopes.map(e => ({
1139
+ repo: e.data.repo,
1140
+ entityId: e.id,
1141
+ entityType: e.entityType,
1142
+ data: cleanData(e.data),
1143
+ })),
1144
+ });
1145
+ }
537
1146
  let results = workspaceManager.list(params.entityType, params.limit);
538
- // Filter to accessible collections
1147
+ // Filter to accessible folders
539
1148
  const allowed = getAccessibleRepoNames(workspaceManager, getUser, authSvc);
540
1149
  if (allowed) {
541
1150
  results = results.filter(r => allowed.has(r.repoName));
@@ -563,9 +1172,9 @@ export function createGraphTools(workspaceManager, gitUser, workspacePath, onHig
563
1172
  {
564
1173
  name: 'get_related',
565
1174
  label: 'Get Related Entries',
566
- description: 'Get entries related to a given entry via wikilinks (cross-collection supported). Pass repo, entityType, entityId from search/list results.',
1175
+ description: 'Get entries related to a given entry via wikilinks (cross-folder supported). Pass repo, entityType, entityId from search/list results.',
567
1176
  parameters: T.Object({
568
- repo: T.String({ description: 'Collection name' }),
1177
+ repo: T.String({ description: 'Folder name' }),
569
1178
  entityType: T.String({ description: 'Entity type' }),
570
1179
  entityId: T.String({ description: 'Entity ID' }),
571
1180
  }),
@@ -573,23 +1182,38 @@ export function createGraphTools(workspaceManager, gitUser, workspacePath, onHig
573
1182
  try {
574
1183
  const repo = resolvePrivateRepo(params.repo);
575
1184
  if (!canAccessRepo(repo, workspaceManager, getUser, authSvc)) {
576
- return wrapToolResult({ success: false, error: `Collection "${repo}" not found` });
1185
+ return wrapToolResult({ success: false, error: `Folder "${repo}" not found` });
577
1186
  }
578
1187
  let related = workspaceManager.getRelated(repo, params.entityType, params.entityId);
579
- // Filter cross-collection results to accessible repos
1188
+ // Filter cross-folder results to accessible repos
580
1189
  const allowed = getAccessibleRepoNames(workspaceManager, getUser, authSvc);
581
1190
  if (allowed) {
582
1191
  related = related.filter(r => allowed.has(r.repoName));
583
1192
  }
1193
+ const relatedResults = related.map(r => ({
1194
+ repo: r.repoName,
1195
+ entityId: r.id,
1196
+ entityType: r.entityType,
1197
+ data: cleanData(r.data),
1198
+ }));
1199
+ // Comments anchored to this entry surface as related entries
1200
+ // (mirrors the MCP find_related incoming branch). The caller already
1201
+ // passed canAccessRepo for `repo`, so these are in-scope.
1202
+ if (commentSvc) {
1203
+ const anchored = commentVirtualEntity.findRelatedComments(commentSvc, repo, params.entityType, params.entityId);
1204
+ for (const e of anchored) {
1205
+ relatedResults.push({
1206
+ repo,
1207
+ entityId: e.id,
1208
+ entityType: e.entityType,
1209
+ data: cleanData(e.data),
1210
+ });
1211
+ }
1212
+ }
584
1213
  return wrapToolResult({
585
1214
  success: true,
586
- count: related.length,
587
- related: related.map(r => ({
588
- repo: r.repoName,
589
- entityId: r.id,
590
- entityType: r.entityType,
591
- data: cleanData(r.data),
592
- })),
1215
+ count: relatedResults.length,
1216
+ related: relatedResults,
593
1217
  });
594
1218
  }
595
1219
  catch (error) {
@@ -604,19 +1228,19 @@ export function createGraphTools(workspaceManager, gitUser, workspacePath, onHig
604
1228
  {
605
1229
  name: 'get_backlinks',
606
1230
  label: 'Get Backlinks',
607
- description: 'Get entries that link to a given entry (cross-collection supported). Pass repo, entityType, entityId from search/list results.',
1231
+ description: 'Get entries that link to a given entry (cross-folder supported). Pass repo, entityType, entityId from search/list results.',
608
1232
  parameters: T.Object({
609
- repo: T.String({ description: 'Collection name' }),
1233
+ repo: T.String({ description: 'Folder name' }),
610
1234
  entityType: T.String({ description: 'Entity type' }),
611
1235
  entityId: T.String({ description: 'Entity ID' }),
612
1236
  }),
613
1237
  execute: async (toolCallId, params) => {
614
1238
  try {
615
1239
  if (!canAccessRepo(params.repo, workspaceManager, getUser, authSvc)) {
616
- return wrapToolResult({ success: false, error: `Collection "${params.repo}" not found` });
1240
+ return wrapToolResult({ success: false, error: `Folder "${params.repo}" not found` });
617
1241
  }
618
1242
  let backlinks = workspaceManager.getBacklinks(params.repo, params.entityType, params.entityId);
619
- // Filter cross-collection results to accessible repos
1243
+ // Filter cross-folder results to accessible repos
620
1244
  const allowed = getAccessibleRepoNames(workspaceManager, getUser, authSvc);
621
1245
  if (allowed) {
622
1246
  backlinks = backlinks.filter(r => allowed.has(r.repoName));
@@ -644,16 +1268,16 @@ export function createGraphTools(workspaceManager, gitUser, workspacePath, onHig
644
1268
  {
645
1269
  name: 'validate_wikilinks',
646
1270
  label: 'Validate Wikilinks',
647
- description: 'Validate all wikilinks in an entry (checks same collection and cross-collection)',
1271
+ description: 'Validate all wikilinks in an entry (checks same folder and cross-folder)',
648
1272
  parameters: T.Object({
649
- repo: T.String({ description: 'Collection name' }),
1273
+ repo: T.String({ description: 'Folder name' }),
650
1274
  entityType: T.String({ description: 'Entity type' }),
651
1275
  entityId: T.String({ description: 'Entity ID' }),
652
1276
  }),
653
1277
  execute: async (toolCallId, params) => {
654
1278
  try {
655
1279
  if (!canAccessRepo(params.repo, workspaceManager, getUser, authSvc)) {
656
- return wrapToolResult({ success: false, error: `Collection "${params.repo}" not found` });
1280
+ return wrapToolResult({ success: false, error: `Folder "${params.repo}" not found` });
657
1281
  }
658
1282
  const validation = workspaceManager.validateWikilinks(params.repo, params.entityType, params.entityId);
659
1283
  return wrapToolResult({
@@ -688,7 +1312,7 @@ export function createGraphTools(workspaceManager, gitUser, workspacePath, onHig
688
1312
  const summary = workspaceManager.getSummary();
689
1313
  const allowed = getAccessibleRepoNames(workspaceManager, getUser, authSvc);
690
1314
  if (allowed) {
691
- // Filter summary to accessible collections only
1315
+ // Filter summary to accessible folders only
692
1316
  const filteredByRepo = {};
693
1317
  let filteredTotal = 0;
694
1318
  for (const [repo, count] of Object.entries(summary.entitiesByRepo)) {
@@ -700,19 +1324,19 @@ export function createGraphTools(workspaceManager, gitUser, workspacePath, onHig
700
1324
  return wrapToolResult({
701
1325
  success: true,
702
1326
  summary: {
703
- totalCollections: allowed.size,
1327
+ totalFolders: allowed.size,
704
1328
  totalEntities: filteredTotal,
705
- entitiesByCollection: filteredByRepo,
1329
+ entitiesByFolder: filteredByRepo,
706
1330
  },
707
1331
  });
708
1332
  }
709
1333
  return wrapToolResult({
710
1334
  success: true,
711
1335
  summary: {
712
- totalCollections: summary.totalRepos,
1336
+ totalFolders: summary.totalRepos,
713
1337
  totalEntities: summary.totalEntities,
714
1338
  entitiesByType: summary.entitiesByType,
715
- entitiesByCollection: summary.entitiesByRepo,
1339
+ entitiesByFolder: summary.entitiesByRepo,
716
1340
  },
717
1341
  });
718
1342
  }
@@ -724,15 +1348,15 @@ export function createGraphTools(workspaceManager, gitUser, workspacePath, onHig
724
1348
  }
725
1349
  },
726
1350
  },
727
- // List collections
1351
+ // List folders
728
1352
  {
729
1353
  name: 'list_repos',
730
- label: 'List Collections',
731
- description: 'List all collections in the workspace',
1354
+ label: 'List Folders',
1355
+ description: 'List all folders in the workspace',
732
1356
  parameters: T.Object({}),
733
1357
  execute: async (toolCallId, params) => {
734
1358
  try {
735
- let repos = workspaceManager.getAllRepoConfigs();
1359
+ let repos = workspaceManager.getContentRepoConfigs();
736
1360
  const allowed = getAccessibleRepoNames(workspaceManager, getUser, authSvc);
737
1361
  if (allowed) {
738
1362
  repos = repos.filter(r => allowed.has(r.name));
@@ -740,12 +1364,12 @@ export function createGraphTools(workspaceManager, gitUser, workspacePath, onHig
740
1364
  return wrapToolResult({
741
1365
  success: true,
742
1366
  count: repos.length,
743
- collections: repos.map(r => ({
1367
+ folders: repos.map(r => ({
744
1368
  name: r.name,
745
1369
  type: r.type,
746
1370
  description: r.description,
747
1371
  path: r.path,
748
- private: r.private || false,
1372
+ personal: r.personal || false,
749
1373
  })),
750
1374
  });
751
1375
  }
@@ -757,40 +1381,35 @@ export function createGraphTools(workspaceManager, gitUser, workspacePath, onHig
757
1381
  }
758
1382
  },
759
1383
  },
760
- // Create a new collection
1384
+ // Create a new folder
761
1385
  {
762
- name: 'create_collection',
763
- label: 'Create Collection',
764
- description: 'Create a new collection in the workspace. A collection is a git-backed container for entries. Private collections are only visible to the creating user.',
1386
+ name: 'create_folder',
1387
+ label: 'Create Folder',
1388
+ description: 'Create a new shared folder in the workspace. Folders are git-backed containers for entries. New folders are shared with teammates who have access; the creator is granted admin. Every user already has a personal "My Folder" folder auto-provisioned — do not use this tool to create another one.',
765
1389
  parameters: T.Object({
766
- name: T.String({ description: 'Collection name in kebab-case (e.g., "client-projects")' }),
767
- description: T.Optional(T.String({ description: 'Short description of the collection' })),
768
- private: T.Optional(T.Boolean({ description: 'If true, creates a private collection visible only to you' })),
1390
+ name: T.String({ description: 'Folder name in kebab-case (e.g., "client-projects")' }),
1391
+ description: T.Optional(T.String({ description: 'Short description of the folder' })),
769
1392
  }),
770
1393
  execute: async (toolCallId, params) => {
771
1394
  try {
772
1395
  const user = getUser();
773
- // Non-private collections require admin; private collections can be created by anyone
774
- if (!params.private && user && !isAdminOrOwner(user.role)) {
775
- return wrapToolResult({ success: false, error: 'Only admins can create collections' });
776
- }
777
1396
  if (!/^[a-z0-9][a-z0-9-]*$/.test(params.name)) {
778
- return wrapToolResult({ success: false, error: 'Collection name must be kebab-case (lowercase letters, numbers, hyphens)' });
1397
+ return wrapToolResult({ success: false, error: 'Folder name must be kebab-case (lowercase letters, numbers, hyphens)' });
779
1398
  }
780
1399
  const { Workspace } = await import('../../core/workspace.js');
781
1400
  const workspace = new Workspace(workspacePath);
782
1401
  const repo = await workspace.registerRepo({
783
1402
  name: params.name,
784
1403
  description: params.description,
785
- private: params.private,
786
- ...(params.private && user ? { owner_id: user.id } : {}),
1404
+ owner_id: user?.id,
787
1405
  });
788
- // Reload workspace manager to pick up the new collection
789
1406
  workspaceManager.reloadConfig();
790
- onMutation?.({ action: 'created', repo: repo.name, entityType: '_collection', entityId: repo.name });
1407
+ if (user && authSvc)
1408
+ authSvc.grantFolderAccess(user.id, repo.name, 'admin');
1409
+ onMutation?.({ action: 'created', repo: repo.name, entityType: '_folder', entityId: repo.name });
791
1410
  return wrapToolResult({
792
1411
  success: true,
793
- collection: { name: repo.name, path: repo.path, description: repo.description },
1412
+ folder: { name: repo.name, path: repo.path, description: repo.description },
794
1413
  });
795
1414
  }
796
1415
  catch (error) {
@@ -804,7 +1423,7 @@ export function createGraphTools(workspaceManager, gitUser, workspacePath, onHig
804
1423
  label: 'Query Dataset',
805
1424
  description: 'Query rows from a dataset entity with optional filtering',
806
1425
  parameters: T.Object({
807
- repo: T.String({ description: 'Collection name' }),
1426
+ repo: T.String({ description: 'Folder name' }),
808
1427
  entityId: T.String({ description: 'Dataset entity ID' }),
809
1428
  filter: T.Optional(T.Object({}, { description: 'Filter conditions (column: value or { gte, lte, contains })', additionalProperties: true })),
810
1429
  limit: T.Optional(T.Number({ description: 'Max rows to return' })),
@@ -812,21 +1431,24 @@ export function createGraphTools(workspaceManager, gitUser, workspacePath, onHig
812
1431
  execute: async (toolCallId, params) => {
813
1432
  try {
814
1433
  if (!canAccessRepo(params.repo, workspaceManager, getUser, authSvc)) {
815
- return wrapToolResult({ success: false, error: `Collection "${params.repo}" not found` });
1434
+ return wrapToolResult({ success: false, error: `Folder "${params.repo}" not found` });
816
1435
  }
817
1436
  const graph = workspaceManager.getGraph(params.repo);
818
- const entity = graph.get('dataset', params.entityId);
819
- const schema = entity.data.schema;
1437
+ // Validates the entity exists and is a dataset before querying.
1438
+ graph.get('dataset', params.entityId);
820
1439
  let rows = graph.getDatasetRows(params.entityId, params.filter);
821
1440
  const total = rows.length;
822
1441
  if (params.limit && params.limit > 0) {
823
1442
  rows = rows.slice(0, params.limit);
824
1443
  }
1444
+ // Columns from the header, not the result rows — so a zero-match
1445
+ // filter or header-only dataset still reports the real columns.
1446
+ const columns = graph.getDatasetColumns(params.entityId);
825
1447
  return wrapToolResult({
826
1448
  success: true,
827
1449
  rows,
828
1450
  total,
829
- schema,
1451
+ columns,
830
1452
  });
831
1453
  }
832
1454
  catch (error) {
@@ -843,7 +1465,7 @@ export function createGraphTools(workspaceManager, gitUser, workspacePath, onHig
843
1465
  label: 'Append Rows',
844
1466
  description: 'Append rows to a dataset entity',
845
1467
  parameters: T.Object({
846
- repo: T.String({ description: 'Collection name' }),
1468
+ repo: T.String({ description: 'Folder name' }),
847
1469
  entityId: T.String({ description: 'Dataset entity ID' }),
848
1470
  rows: T.Array(T.Object({}, { additionalProperties: true }), { description: 'Rows to append' }),
849
1471
  commitMessage: T.Optional(T.String({ description: 'Custom commit message' })),
@@ -851,13 +1473,14 @@ export function createGraphTools(workspaceManager, gitUser, workspacePath, onHig
851
1473
  execute: async (toolCallId, params) => {
852
1474
  try {
853
1475
  if (!canAccessRepo(params.repo, workspaceManager, getUser, authSvc)) {
854
- return wrapToolResult({ success: false, error: `Collection "${params.repo}" not found` });
1476
+ return wrapToolResult({ success: false, error: `Folder "${params.repo}" not found` });
855
1477
  }
856
1478
  const graph = workspaceManager.getGraph(params.repo);
857
1479
  // Get current row count before appending
858
1480
  const beforeRows = graph.getDatasetRows(params.entityId);
859
1481
  await graph.appendDatasetRows(params.entityId, params.rows, params.commitMessage);
860
1482
  const afterRows = graph.getDatasetRows(params.entityId);
1483
+ onMutation?.({ action: 'updated', repo: params.repo, entityType: 'dataset', entityId: params.entityId });
861
1484
  return wrapToolResult({
862
1485
  success: true,
863
1486
  rowsAdded: afterRows.length - beforeRows.length,
@@ -877,30 +1500,22 @@ export function createGraphTools(workspaceManager, gitUser, workspacePath, onHig
877
1500
  {
878
1501
  name: 'get_entity_schema',
879
1502
  label: 'Get Entry Schema',
880
- description: 'Get the schema definition for an entry type (useful for understanding required fields)',
1503
+ description: 'Get the authoritative schema for an entry type. Returns each field with its type, whether it is optional, the description from the Zod schema, the full enum value list when applicable, and any default. Call this before writing enum-shaped fields (stage, status, category, priority, etc.) — the validator only accepts the values listed under `enum`.',
881
1504
  parameters: T.Object({
882
1505
  entityType: T.String({ description: 'Entity type' }),
883
1506
  }),
884
1507
  execute: async (toolCallId, params) => {
885
1508
  try {
886
- const schema = ENTITY_SCHEMAS[params.entityType];
887
- if (!schema) {
1509
+ const registry = workspaceManager.getSchemaRegistry();
1510
+ const knownTypes = new Set(registry.getAllTypes());
1511
+ if (!knownTypes.has(params.entityType)) {
888
1512
  return wrapToolResult({
889
1513
  success: false,
890
- error: `Unknown entity type: ${params.entityType}`,
1514
+ error: `Unknown entity type: ${params.entityType}. Known types: ${[...knownTypes].sort().join(', ')}`,
891
1515
  });
892
1516
  }
893
- // Extract schema shape (simplified)
894
- const shape = schema.shape || {};
895
- const fields = {};
896
- for (const [key, value] of Object.entries(shape)) {
897
- const field = value;
898
- fields[key] = {
899
- type: field._def.typeName || 'unknown',
900
- optional: field.isOptional() || false,
901
- description: field._def.description || '',
902
- };
903
- }
1517
+ const schema = registry.getSchema(params.entityType);
1518
+ const fields = describeZodObject(schema);
904
1519
  return wrapToolResult({
905
1520
  success: true,
906
1521
  entityType: params.entityType,
@@ -919,15 +1534,15 @@ export function createGraphTools(workspaceManager, gitUser, workspacePath, onHig
919
1534
  {
920
1535
  name: 'lint_entities',
921
1536
  label: 'Lint Entries',
922
- description: 'Check for missing recommended fields across entries. Returns incomplete entries with their missing fields. Use to discover entries that need enrichment. Optionally filter by collection or entry type.',
1537
+ description: 'Check for missing recommended fields across entries. Returns incomplete entries with their missing fields. Use to discover entries that need enrichment. Optionally filter by folder or entry type.',
923
1538
  parameters: T.Object({
924
- repo: T.Optional(T.String({ description: 'Optional: limit to specific collection' })),
1539
+ repo: T.Optional(T.String({ description: 'Optional: limit to specific folder' })),
925
1540
  entityType: T.Optional(T.String({ description: 'Optional: limit to specific entity type (e.g. "project", "deal")' })),
926
1541
  }),
927
1542
  execute: async (toolCallId, params) => {
928
1543
  try {
929
1544
  if (params.repo && !canAccessRepo(params.repo, workspaceManager, getUser, authSvc)) {
930
- return wrapToolResult({ success: false, error: `Collection "${params.repo}" not found` });
1545
+ return wrapToolResult({ success: false, error: `Folder "${params.repo}" not found` });
931
1546
  }
932
1547
  let issues = await lintWorkspace(workspaceManager, {
933
1548
  repo: params.repo,
@@ -977,11 +1592,11 @@ export function createGraphTools(workspaceManager, gitUser, workspacePath, onHig
977
1592
  parameters: T.Object({
978
1593
  entityTypes: T.Optional(T.Array(T.String(), { description: 'Entity types to check (e.g. ["person", "client"])' })),
979
1594
  minAgeDays: T.Optional(T.Number({ description: 'Only flag entities older than N days' })),
980
- repos: T.Optional(T.Array(T.String(), { description: 'Collection names to scan (default: all)' })),
1595
+ repos: T.Optional(T.Array(T.String(), { description: 'Folder names to scan (default: all)' })),
981
1596
  }),
982
1597
  execute: async (toolCallId, params) => {
983
1598
  try {
984
- // Scope to accessible collections
1599
+ // Scope to accessible folders
985
1600
  const allowed = getAccessibleRepoNames(workspaceManager, getUser, authSvc);
986
1601
  let repos = params.repos;
987
1602
  if (allowed) {
@@ -1032,98 +1647,6 @@ export function createGraphTools(workspaceManager, gitUser, workspacePath, onHig
1032
1647
  });
1033
1648
  },
1034
1649
  },
1035
- // ── View Management Tools ──
1036
- {
1037
- name: 'list_views',
1038
- label: 'List Views',
1039
- description: 'List the current user\'s saved views.',
1040
- parameters: T.Object({}),
1041
- execute: async () => {
1042
- if (!authSvc)
1043
- return wrapToolResult({ success: false, error: 'Auth service not available' });
1044
- const userId = getUser()?.id ?? 0;
1045
- const views = authSvc.getUserViews(userId);
1046
- return wrapToolResult({ views });
1047
- },
1048
- },
1049
- {
1050
- name: 'create_view',
1051
- label: 'Create View',
1052
- description: 'Create a new saved view for the current user. Views appear as tabs in the toolbar.',
1053
- parameters: T.Object({
1054
- name: T.String({ description: 'View name (e.g. "My Tasks", "Open Projects")' }),
1055
- selectedType: T.Optional(T.String({ description: 'Entity type filter (e.g. "task", "project"). Omit for all types.' })),
1056
- groupBy: T.Optional(T.String({ description: 'Column key to group by (e.g. "status", "priority")' })),
1057
- }),
1058
- execute: async (_toolCallId, params) => {
1059
- if (!authSvc)
1060
- return wrapToolResult({ success: false, error: 'Auth service not available' });
1061
- const userId = getUser()?.id ?? 0;
1062
- const config = {
1063
- selectedType: params.selectedType ?? null,
1064
- groupBy: params.groupBy ?? null,
1065
- sortKey: 'name',
1066
- sortAsc: true,
1067
- filters: { text: {}, enum: {}, date: {} },
1068
- };
1069
- const view = authSvc.createUserView(userId, params.name, 'list', config);
1070
- return wrapToolResult({
1071
- success: true,
1072
- view: { id: view.id, name: view.name, viewType: view.viewType, config: view.config },
1073
- message: `Created view "${params.name}". It will appear as a tab in the toolbar.`,
1074
- });
1075
- },
1076
- },
1077
- {
1078
- name: 'update_view',
1079
- label: 'Update View',
1080
- description: 'Update an existing saved view\'s name or configuration.',
1081
- parameters: T.Object({
1082
- id: T.Number({ description: 'View ID' }),
1083
- name: T.Optional(T.String({ description: 'New view name' })),
1084
- selectedType: T.Optional(T.String({ description: 'Entity type filter (use "all" to clear)' })),
1085
- groupBy: T.Optional(T.String({ description: 'Column key to group by (use "none" to clear)' })),
1086
- }),
1087
- execute: async (_toolCallId, params) => {
1088
- if (!authSvc)
1089
- return wrapToolResult({ success: false, error: 'Auth service not available' });
1090
- const userId = getUser()?.id ?? 0;
1091
- const existing = authSvc.getUserView(params.id, userId);
1092
- if (!existing)
1093
- return wrapToolResult({ success: false, error: `View ${params.id} not found` });
1094
- const fields = {};
1095
- if (params.name)
1096
- fields.name = params.name;
1097
- if (params.selectedType !== undefined || params.groupBy !== undefined) {
1098
- const config = { ...existing.config };
1099
- if (params.selectedType !== undefined)
1100
- config.selectedType = params.selectedType === 'all' ? null : params.selectedType;
1101
- if (params.groupBy !== undefined)
1102
- config.groupBy = params.groupBy === 'none' ? null : params.groupBy;
1103
- fields.config = config;
1104
- }
1105
- authSvc.updateUserView(params.id, userId, fields);
1106
- return wrapToolResult({ success: true, message: `Updated view "${existing.name}".` });
1107
- },
1108
- },
1109
- {
1110
- name: 'delete_view',
1111
- label: 'Delete View',
1112
- description: 'Delete a saved view.',
1113
- parameters: T.Object({
1114
- id: T.Number({ description: 'View ID to delete' }),
1115
- }),
1116
- execute: async (_toolCallId, params) => {
1117
- if (!authSvc)
1118
- return wrapToolResult({ success: false, error: 'Auth service not available' });
1119
- const userId = getUser()?.id ?? 0;
1120
- const existing = authSvc.getUserView(params.id, userId);
1121
- if (!existing)
1122
- return wrapToolResult({ success: false, error: `View ${params.id} not found` });
1123
- authSvc.deleteUserView(params.id, userId);
1124
- return wrapToolResult({ success: true, message: `Deleted view "${existing.name}".` });
1125
- },
1126
- },
1127
1650
  // Mark onboarding complete
1128
1651
  {
1129
1652
  name: 'mark_onboarded',