studiograph 1.3.48-next.2 → 1.3.48-next.201

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 (1157) hide show
  1. package/README.md +26 -16
  2. package/dist/agent/agent-pool.d.ts +117 -0
  3. package/dist/agent/agent-pool.js +287 -0
  4. package/dist/agent/agent-pool.js.map +1 -0
  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 +113 -22
  9. package/dist/agent/orchestrator.js +574 -181
  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 +147 -69
  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/entity-schema.md +77 -433
  19. package/dist/agent/skills/interview/entity-templates.md +193 -0
  20. package/dist/agent/skills/interview/question-domains.md +391 -0
  21. package/dist/agent/skills/interview/skill.md +204 -0
  22. package/dist/agent/skills/schema-rules.md +54 -0
  23. package/dist/agent/tools/capture-tools.d.ts +31 -0
  24. package/dist/agent/tools/capture-tools.js +40 -0
  25. package/dist/agent/tools/capture-tools.js.map +1 -0
  26. package/dist/agent/tools/folder-tools.d.ts +47 -0
  27. package/dist/agent/tools/folder-tools.js +249 -0
  28. package/dist/agent/tools/folder-tools.js.map +1 -0
  29. package/dist/agent/tools/fs-tools.d.ts +7 -6
  30. package/dist/agent/tools/fs-tools.js +5 -4
  31. package/dist/agent/tools/fs-tools.js.map +1 -1
  32. package/dist/agent/tools/graph-tools.d.ts +21 -58
  33. package/dist/agent/tools/graph-tools.js +664 -333
  34. package/dist/agent/tools/graph-tools.js.map +1 -1
  35. package/dist/agent/tools/history-tools.d.ts +95 -0
  36. package/dist/agent/tools/history-tools.js +461 -0
  37. package/dist/agent/tools/history-tools.js.map +1 -0
  38. package/dist/agent/tools/load-skill.d.ts +6 -5
  39. package/dist/agent/tools/load-skill.js +3 -3
  40. package/dist/agent/tools/load-skill.js.map +1 -1
  41. package/dist/agent/tools/ops-tools.d.ts +5 -4
  42. package/dist/agent/tools/ops-tools.js +130 -236
  43. package/dist/agent/tools/ops-tools.js.map +1 -1
  44. package/dist/agent/tools/permission-tools.d.ts +87 -17
  45. package/dist/agent/tools/permission-tools.js +233 -38
  46. package/dist/agent/tools/permission-tools.js.map +1 -1
  47. package/dist/agent/tools/tool-loader.js +5 -4
  48. package/dist/agent/tools/tool-loader.js.map +1 -1
  49. package/dist/agent/tools/web-tools.js +58 -9
  50. package/dist/agent/tools/web-tools.js.map +1 -1
  51. package/dist/cli/commands/about.d.ts +1 -0
  52. package/dist/cli/commands/about.js +55 -3
  53. package/dist/cli/commands/about.js.map +1 -1
  54. package/dist/cli/commands/admin/audit-workspace.d.ts +23 -0
  55. package/dist/cli/commands/admin/audit-workspace.js +133 -0
  56. package/dist/cli/commands/admin/audit-workspace.js.map +1 -0
  57. package/dist/cli/commands/admin/audit.d.ts +21 -0
  58. package/dist/cli/commands/admin/audit.js +174 -0
  59. package/dist/cli/commands/admin/audit.js.map +1 -0
  60. package/dist/cli/commands/admin/backup.d.ts +54 -0
  61. package/dist/cli/commands/admin/backup.js +207 -0
  62. package/dist/cli/commands/admin/backup.js.map +1 -0
  63. package/dist/cli/commands/admin/folder.d.ts +11 -0
  64. package/dist/cli/commands/admin/folder.js +186 -0
  65. package/dist/cli/commands/admin/folder.js.map +1 -0
  66. package/dist/cli/commands/admin/index.d.ts +16 -0
  67. package/dist/cli/commands/admin/index.js +46 -0
  68. package/dist/cli/commands/admin/index.js.map +1 -0
  69. package/dist/cli/commands/admin/migrate-assets.d.ts +53 -0
  70. package/dist/cli/commands/admin/migrate-assets.js +184 -0
  71. package/dist/cli/commands/admin/migrate-assets.js.map +1 -0
  72. package/dist/cli/commands/admin/migrate.d.ts +56 -0
  73. package/dist/cli/commands/admin/migrate.js +263 -0
  74. package/dist/cli/commands/admin/migrate.js.map +1 -0
  75. package/dist/cli/commands/admin/restore.d.ts +24 -0
  76. package/dist/cli/commands/admin/restore.js +228 -0
  77. package/dist/cli/commands/admin/restore.js.map +1 -0
  78. package/dist/cli/commands/admin/secrets.d.ts +23 -0
  79. package/dist/cli/commands/admin/secrets.js +256 -0
  80. package/dist/cli/commands/admin/secrets.js.map +1 -0
  81. package/dist/cli/commands/admin/settings.d.ts +28 -0
  82. package/dist/cli/commands/admin/settings.js +284 -0
  83. package/dist/cli/commands/admin/settings.js.map +1 -0
  84. package/dist/cli/commands/admin/token.d.ts +42 -0
  85. package/dist/cli/commands/admin/token.js +126 -0
  86. package/dist/cli/commands/admin/token.js.map +1 -0
  87. package/dist/cli/commands/admin/transfer-ownership.d.ts +15 -0
  88. package/dist/cli/commands/admin/transfer-ownership.js +106 -0
  89. package/dist/cli/commands/admin/transfer-ownership.js.map +1 -0
  90. package/dist/cli/commands/admin/user.d.ts +11 -0
  91. package/dist/cli/commands/admin/user.js +187 -0
  92. package/dist/cli/commands/admin/user.js.map +1 -0
  93. package/dist/cli/commands/clone.d.ts +25 -3
  94. package/dist/cli/commands/clone.js +142 -36
  95. package/dist/cli/commands/clone.js.map +1 -1
  96. package/dist/cli/commands/commit.d.ts +1 -1
  97. package/dist/cli/commands/commit.js +24 -11
  98. package/dist/cli/commands/commit.js.map +1 -1
  99. package/dist/cli/commands/config.d.ts +23 -107
  100. package/dist/cli/commands/config.js +52 -260
  101. package/dist/cli/commands/config.js.map +1 -1
  102. package/dist/cli/commands/connector.d.ts +10 -13
  103. package/dist/cli/commands/connector.js +16 -58
  104. package/dist/cli/commands/connector.js.map +1 -1
  105. package/dist/cli/commands/deploy.js +215 -68
  106. package/dist/cli/commands/deploy.js.map +1 -1
  107. package/dist/cli/commands/index.js +2 -2
  108. package/dist/cli/commands/index.js.map +1 -1
  109. package/dist/cli/commands/init.js +10 -30
  110. package/dist/cli/commands/init.js.map +1 -1
  111. package/dist/cli/commands/mcp.js +12 -4
  112. package/dist/cli/commands/mcp.js.map +1 -1
  113. package/dist/cli/commands/orphans.js +14 -30
  114. package/dist/cli/commands/orphans.js.map +1 -1
  115. package/dist/cli/commands/r2.d.ts +3 -0
  116. package/dist/cli/commands/r2.js +223 -204
  117. package/dist/cli/commands/r2.js.map +1 -1
  118. package/dist/cli/commands/redeploy.js +65 -12
  119. package/dist/cli/commands/redeploy.js.map +1 -1
  120. package/dist/cli/commands/reset.d.ts +22 -6
  121. package/dist/cli/commands/reset.js +142 -63
  122. package/dist/cli/commands/reset.js.map +1 -1
  123. package/dist/cli/commands/serve.js +218 -24
  124. package/dist/cli/commands/serve.js.map +1 -1
  125. package/dist/cli/commands/sync.d.ts +1 -1
  126. package/dist/cli/commands/sync.js +372 -202
  127. package/dist/cli/commands/sync.js.map +1 -1
  128. package/dist/cli/crypto-bundle.d.ts +39 -0
  129. package/dist/cli/crypto-bundle.js +94 -0
  130. package/dist/cli/crypto-bundle.js.map +1 -0
  131. package/dist/cli/index.js +24 -35
  132. package/dist/cli/index.js.map +1 -1
  133. package/dist/cli/railway-pin.d.ts +68 -0
  134. package/dist/cli/railway-pin.js +96 -0
  135. package/dist/cli/railway-pin.js.map +1 -0
  136. package/dist/cli/railway-provisioning.d.ts +53 -0
  137. package/dist/cli/railway-provisioning.js +85 -0
  138. package/dist/cli/railway-provisioning.js.map +1 -0
  139. package/dist/cli/setup-wizard.d.ts +33 -52
  140. package/dist/cli/setup-wizard.js +43 -68
  141. package/dist/cli/setup-wizard.js.map +1 -1
  142. package/dist/core/accent-palette.d.ts +22 -0
  143. package/dist/core/accent-palette.js +47 -0
  144. package/dist/core/accent-palette.js.map +1 -0
  145. package/dist/core/cell-anchor.d.ts +29 -0
  146. package/dist/core/cell-anchor.js +53 -0
  147. package/dist/core/cell-anchor.js.map +1 -0
  148. package/dist/core/comment-anchor.d.ts +41 -0
  149. package/dist/core/comment-anchor.js +147 -0
  150. package/dist/core/comment-anchor.js.map +1 -0
  151. package/dist/core/commit-messages.d.ts +57 -0
  152. package/dist/core/commit-messages.js +85 -0
  153. package/dist/core/commit-messages.js.map +1 -0
  154. package/dist/core/embeds.d.ts +77 -0
  155. package/dist/core/embeds.js +147 -0
  156. package/dist/core/embeds.js.map +1 -0
  157. package/dist/core/entity-body-templates.d.ts +21 -0
  158. package/dist/core/entity-body-templates.js +53 -0
  159. package/dist/core/entity-body-templates.js.map +1 -0
  160. package/dist/core/entity-types-catalog.d.ts +65 -0
  161. package/dist/core/entity-types-catalog.js +140 -0
  162. package/dist/core/entity-types-catalog.js.map +1 -0
  163. package/dist/core/folder-paths.d.ts +41 -0
  164. package/dist/core/folder-paths.js +95 -0
  165. package/dist/core/folder-paths.js.map +1 -0
  166. package/dist/core/folder-sidecar.d.ts +36 -0
  167. package/dist/core/folder-sidecar.js +91 -0
  168. package/dist/core/folder-sidecar.js.map +1 -0
  169. package/dist/core/format-registry.d.ts +170 -0
  170. package/dist/core/format-registry.js +404 -0
  171. package/dist/core/format-registry.js.map +1 -0
  172. package/dist/core/graph.d.ts +731 -14
  173. package/dist/core/graph.js +2202 -315
  174. package/dist/core/graph.js.map +1 -1
  175. package/dist/core/history-diff.d.ts +35 -0
  176. package/dist/core/history-diff.js +114 -0
  177. package/dist/core/history-diff.js.map +1 -0
  178. package/dist/core/refs.d.ts +41 -0
  179. package/dist/core/refs.js +80 -0
  180. package/dist/core/refs.js.map +1 -0
  181. package/dist/core/schema-registry.d.ts +82 -0
  182. package/dist/core/schema-registry.js +238 -0
  183. package/dist/core/schema-registry.js.map +1 -1
  184. package/dist/core/schemas/connector.d.ts +67 -0
  185. package/dist/core/schemas/connector.js +100 -0
  186. package/dist/core/schemas/connector.js.map +1 -0
  187. package/dist/core/schemas/workspace.d.ts +122 -0
  188. package/dist/core/schemas/workspace.js +211 -0
  189. package/dist/core/schemas/workspace.js.map +1 -0
  190. package/dist/core/secrets/SecretStore.d.ts +77 -0
  191. package/dist/core/secrets/SecretStore.js +13 -0
  192. package/dist/core/secrets/SecretStore.js.map +1 -0
  193. package/dist/core/secrets/SettingsResolver.d.ts +54 -0
  194. package/dist/core/secrets/SettingsResolver.js +80 -0
  195. package/dist/core/secrets/SettingsResolver.js.map +1 -0
  196. package/dist/core/secrets/SettingsStore.d.ts +30 -0
  197. package/dist/core/secrets/SettingsStore.js +9 -0
  198. package/dist/core/secrets/SettingsStore.js.map +1 -0
  199. package/dist/core/secrets/SqliteEncryptedStore.d.ts +90 -0
  200. package/dist/core/secrets/SqliteEncryptedStore.js +598 -0
  201. package/dist/core/secrets/SqliteEncryptedStore.js.map +1 -0
  202. package/dist/core/secrets/audit.d.ts +36 -0
  203. package/dist/core/secrets/audit.js +60 -0
  204. package/dist/core/secrets/audit.js.map +1 -0
  205. package/dist/core/secrets/auth-db-migration.d.ts +129 -0
  206. package/dist/core/secrets/auth-db-migration.js +653 -0
  207. package/dist/core/secrets/auth-db-migration.js.map +1 -0
  208. package/dist/core/secrets/bundle.d.ts +141 -0
  209. package/dist/core/secrets/bundle.js +435 -0
  210. package/dist/core/secrets/bundle.js.map +1 -0
  211. package/dist/core/secrets/dual-write.d.ts +81 -0
  212. package/dist/core/secrets/dual-write.js +247 -0
  213. package/dist/core/secrets/dual-write.js.map +1 -0
  214. package/dist/core/secrets/encryption.d.ts +44 -0
  215. package/dist/core/secrets/encryption.js +97 -0
  216. package/dist/core/secrets/encryption.js.map +1 -0
  217. package/dist/core/secrets/index.d.ts +49 -0
  218. package/dist/core/secrets/index.js +74 -0
  219. package/dist/core/secrets/index.js.map +1 -0
  220. package/dist/core/secrets/master-key.d.ts +38 -0
  221. package/dist/core/secrets/master-key.js +122 -0
  222. package/dist/core/secrets/master-key.js.map +1 -0
  223. package/dist/core/secrets/probes/anthropic.d.ts +98 -0
  224. package/dist/core/secrets/probes/anthropic.js +300 -0
  225. package/dist/core/secrets/probes/anthropic.js.map +1 -0
  226. package/dist/core/secrets/probes/brave.d.ts +9 -0
  227. package/dist/core/secrets/probes/brave.js +15 -0
  228. package/dist/core/secrets/probes/brave.js.map +1 -0
  229. package/dist/core/secrets/probes/r2.d.ts +15 -0
  230. package/dist/core/secrets/probes/r2.js +14 -0
  231. package/dist/core/secrets/probes/r2.js.map +1 -0
  232. package/dist/core/secrets/probes/tavily.d.ts +18 -0
  233. package/dist/core/secrets/probes/tavily.js +58 -0
  234. package/dist/core/secrets/probes/tavily.js.map +1 -0
  235. package/dist/core/secrets/probes/voyage.d.ts +13 -0
  236. package/dist/core/secrets/probes/voyage.js +52 -0
  237. package/dist/core/secrets/probes/voyage.js.map +1 -0
  238. package/dist/core/secrets/r2-structural-migration.d.ts +39 -0
  239. package/dist/core/secrets/r2-structural-migration.js +109 -0
  240. package/dist/core/secrets/r2-structural-migration.js.map +1 -0
  241. package/dist/core/secrets/read-flip.d.ts +59 -0
  242. package/dist/core/secrets/read-flip.js +87 -0
  243. package/dist/core/secrets/read-flip.js.map +1 -0
  244. package/dist/core/secrets/registry.d.ts +188 -0
  245. package/dist/core/secrets/registry.js +616 -0
  246. package/dist/core/secrets/registry.js.map +1 -0
  247. package/dist/core/types.d.ts +80 -475
  248. package/dist/core/types.js +27 -3
  249. package/dist/core/types.js.map +1 -1
  250. package/dist/core/user-config.d.ts +75 -7
  251. package/dist/core/user-config.js +155 -7
  252. package/dist/core/user-config.js.map +1 -1
  253. package/dist/core/validation.d.ts +731 -1790
  254. package/dist/core/validation.js +442 -228
  255. package/dist/core/validation.js.map +1 -1
  256. package/dist/core/workspace-manager.d.ts +89 -12
  257. package/dist/core/workspace-manager.js +225 -73
  258. package/dist/core/workspace-manager.js.map +1 -1
  259. package/dist/core/workspace.d.ts +19 -5
  260. package/dist/core/workspace.js +69 -35
  261. package/dist/core/workspace.js.map +1 -1
  262. package/dist/mcp/comment-virtual-entity.d.ts +36 -0
  263. package/dist/mcp/comment-virtual-entity.js +71 -0
  264. package/dist/mcp/comment-virtual-entity.js.map +1 -0
  265. package/dist/mcp/connector-manager.d.ts +94 -13
  266. package/dist/mcp/connector-manager.js +283 -62
  267. package/dist/mcp/connector-manager.js.map +1 -1
  268. package/dist/mcp/oauth-provider.d.ts +27 -33
  269. package/dist/mcp/oauth-provider.js +65 -134
  270. package/dist/mcp/oauth-provider.js.map +1 -1
  271. package/dist/mcp/oauth-registry.d.ts +62 -0
  272. package/dist/mcp/oauth-registry.js +85 -0
  273. package/dist/mcp/oauth-registry.js.map +1 -0
  274. package/dist/mcp/server.d.ts +11 -3
  275. package/dist/mcp/server.js +30 -5
  276. package/dist/mcp/server.js.map +1 -1
  277. package/dist/mcp/state-signer.d.ts +62 -0
  278. package/dist/mcp/state-signer.js +205 -0
  279. package/dist/mcp/state-signer.js.map +1 -0
  280. package/dist/mcp/tools.d.ts +35 -2
  281. package/dist/mcp/tools.js +1361 -112
  282. package/dist/mcp/tools.js.map +1 -1
  283. package/dist/server/__tests__/helpers/graph-api.d.ts +16 -0
  284. package/dist/server/__tests__/helpers/graph-api.js +16 -0
  285. package/dist/server/__tests__/helpers/graph-api.js.map +1 -0
  286. package/dist/server/asset-index-queue.d.ts +96 -0
  287. package/dist/server/asset-index-queue.js +191 -0
  288. package/dist/server/asset-index-queue.js.map +1 -0
  289. package/dist/server/body-write-coordinator.d.ts +151 -0
  290. package/dist/server/body-write-coordinator.js +161 -0
  291. package/dist/server/body-write-coordinator.js.map +1 -0
  292. package/dist/server/cloud-billing-flow.d.ts +32 -0
  293. package/dist/server/cloud-billing-flow.js +75 -0
  294. package/dist/server/cloud-billing-flow.js.map +1 -0
  295. package/dist/server/commit-audit.d.ts +26 -0
  296. package/dist/server/commit-audit.js +30 -0
  297. package/dist/server/commit-audit.js.map +1 -0
  298. package/dist/server/index.d.ts +22 -3
  299. package/dist/server/index.js +622 -198
  300. package/dist/server/index.js.map +1 -1
  301. package/dist/server/middleware/sanitize.d.ts +46 -0
  302. package/dist/server/middleware/sanitize.js +171 -0
  303. package/dist/server/middleware/sanitize.js.map +1 -0
  304. package/dist/server/routes/asset-api.d.ts +31 -0
  305. package/dist/server/routes/asset-api.js +593 -0
  306. package/dist/server/routes/asset-api.js.map +1 -0
  307. package/dist/server/routes/audit-api.d.ts +24 -0
  308. package/dist/server/routes/audit-api.js +117 -0
  309. package/dist/server/routes/audit-api.js.map +1 -0
  310. package/dist/server/routes/auth-api.d.ts +3 -2
  311. package/dist/server/routes/auth-api.js +573 -39
  312. package/dist/server/routes/auth-api.js.map +1 -1
  313. package/dist/server/routes/capture-api.d.ts +18 -0
  314. package/dist/server/routes/capture-api.js +257 -0
  315. package/dist/server/routes/capture-api.js.map +1 -0
  316. package/dist/server/routes/chat-sessions-api.d.ts +9 -0
  317. package/dist/server/routes/chat-sessions-api.js +121 -0
  318. package/dist/server/routes/chat-sessions-api.js.map +1 -0
  319. package/dist/server/routes/chat.d.ts +12 -1
  320. package/dist/server/routes/chat.js +318 -88
  321. package/dist/server/routes/chat.js.map +1 -1
  322. package/dist/server/routes/comments-api.d.ts +15 -0
  323. package/dist/server/routes/comments-api.js +444 -0
  324. package/dist/server/routes/comments-api.js.map +1 -0
  325. package/dist/server/routes/config-api.d.ts +21 -0
  326. package/dist/server/routes/config-api.js +256 -0
  327. package/dist/server/routes/config-api.js.map +1 -0
  328. package/dist/server/routes/connectors-api.d.ts +17 -0
  329. package/dist/server/routes/connectors-api.js +410 -0
  330. package/dist/server/routes/connectors-api.js.map +1 -0
  331. package/dist/server/routes/event-ingest-api.d.ts +15 -0
  332. package/dist/server/routes/event-ingest-api.js +125 -0
  333. package/dist/server/routes/event-ingest-api.js.map +1 -0
  334. package/dist/server/routes/git-http.d.ts +3 -3
  335. package/dist/server/routes/git-http.js +86 -38
  336. package/dist/server/routes/git-http.js.map +1 -1
  337. package/dist/server/routes/graph-api-access.d.ts +57 -0
  338. package/dist/server/routes/graph-api-access.js +112 -0
  339. package/dist/server/routes/graph-api-access.js.map +1 -0
  340. package/dist/server/routes/graph-api.d.ts +1 -1
  341. package/dist/server/routes/graph-api.js +1529 -263
  342. package/dist/server/routes/graph-api.js.map +1 -1
  343. package/dist/server/routes/health.d.ts +18 -0
  344. package/dist/server/routes/health.js +74 -0
  345. package/dist/server/routes/health.js.map +1 -0
  346. package/dist/server/routes/import-batch.d.ts +24 -0
  347. package/dist/server/routes/import-batch.js +33 -0
  348. package/dist/server/routes/import-batch.js.map +1 -0
  349. package/dist/server/routes/insights-api.d.ts +9 -2
  350. package/dist/server/routes/insights-api.js +384 -38
  351. package/dist/server/routes/insights-api.js.map +1 -1
  352. package/dist/server/routes/mcp.d.ts +7 -3
  353. package/dist/server/routes/mcp.js +22 -6
  354. package/dist/server/routes/mcp.js.map +1 -1
  355. package/dist/server/routes/oauth-api.d.ts +67 -0
  356. package/dist/server/routes/oauth-api.js +421 -0
  357. package/dist/server/routes/oauth-api.js.map +1 -0
  358. package/dist/server/routes/permissions-api.d.ts +9 -2
  359. package/dist/server/routes/permissions-api.js +87 -30
  360. package/dist/server/routes/permissions-api.js.map +1 -1
  361. package/dist/server/routes/r2-api.d.ts +25 -0
  362. package/dist/server/routes/r2-api.js +276 -0
  363. package/dist/server/routes/r2-api.js.map +1 -0
  364. package/dist/server/routes/secrets-api.d.ts +36 -0
  365. package/dist/server/routes/secrets-api.js +308 -0
  366. package/dist/server/routes/secrets-api.js.map +1 -0
  367. package/dist/server/routes/settings-api.d.ts +29 -0
  368. package/dist/server/routes/settings-api.js +180 -0
  369. package/dist/server/routes/settings-api.js.map +1 -0
  370. package/dist/server/routes/share-api.d.ts +32 -0
  371. package/dist/server/routes/share-api.js +234 -0
  372. package/dist/server/routes/share-api.js.map +1 -0
  373. package/dist/server/routes/url-api.d.ts +7 -0
  374. package/dist/server/routes/url-api.js +74 -0
  375. package/dist/server/routes/url-api.js.map +1 -0
  376. package/dist/server/routes/views-api.js +75 -7
  377. package/dist/server/routes/views-api.js.map +1 -1
  378. package/dist/server/routes/workspace-api.d.ts +2 -1
  379. package/dist/server/routes/workspace-api.js +129 -35
  380. package/dist/server/routes/workspace-api.js.map +1 -1
  381. package/dist/server/routes/ws.d.ts +2 -1
  382. package/dist/server/routes/ws.js +68 -16
  383. package/dist/server/routes/ws.js.map +1 -1
  384. package/dist/server/session-manager.d.ts +48 -5
  385. package/dist/server/session-manager.js +182 -15
  386. package/dist/server/session-manager.js.map +1 -1
  387. package/dist/server/ws-hub.d.ts +138 -14
  388. package/dist/server/ws-hub.js +0 -0
  389. package/dist/server/ws-hub.js.map +1 -1
  390. package/dist/server/yjs-manager.d.ts +285 -7
  391. package/dist/server/yjs-manager.js +907 -36
  392. package/dist/server/yjs-manager.js.map +1 -1
  393. package/dist/server/yjs-persistence.d.ts +165 -0
  394. package/dist/server/yjs-persistence.js +557 -0
  395. package/dist/server/yjs-persistence.js.map +1 -0
  396. package/dist/server/yjs-text-diff.d.ts +32 -0
  397. package/dist/server/yjs-text-diff.js +61 -0
  398. package/dist/server/yjs-text-diff.js.map +1 -0
  399. package/dist/services/access-control.d.ts +73 -0
  400. package/dist/services/access-control.js +165 -0
  401. package/dist/services/access-control.js.map +1 -0
  402. package/dist/services/asset-caption-service.d.ts +102 -0
  403. package/dist/services/asset-caption-service.js +184 -0
  404. package/dist/services/asset-caption-service.js.map +1 -0
  405. package/dist/services/asset-delete-authz.d.ts +31 -0
  406. package/dist/services/asset-delete-authz.js +61 -0
  407. package/dist/services/asset-delete-authz.js.map +1 -0
  408. package/dist/services/asset-finalize-service.d.ts +58 -0
  409. package/dist/services/asset-finalize-service.js +207 -0
  410. package/dist/services/asset-finalize-service.js.map +1 -0
  411. package/dist/services/asset-import-service.d.ts +111 -0
  412. package/dist/services/asset-import-service.js +394 -0
  413. package/dist/services/asset-import-service.js.map +1 -0
  414. package/dist/services/asset-index-sweep.d.ts +92 -0
  415. package/dist/services/asset-index-sweep.js +258 -0
  416. package/dist/services/asset-index-sweep.js.map +1 -0
  417. package/dist/services/asset-lifecycle-check.d.ts +22 -0
  418. package/dist/services/asset-lifecycle-check.js +80 -0
  419. package/dist/services/asset-lifecycle-check.js.map +1 -0
  420. package/dist/services/asset-listing.d.ts +72 -0
  421. package/dist/services/asset-listing.js +321 -0
  422. package/dist/services/asset-listing.js.map +1 -0
  423. package/dist/services/asset-presign-service.d.ts +68 -0
  424. package/dist/services/asset-presign-service.js +124 -0
  425. package/dist/services/asset-presign-service.js.map +1 -0
  426. package/dist/services/asset-sidecar-service.d.ts +30 -0
  427. package/dist/services/asset-sidecar-service.js +96 -0
  428. package/dist/services/asset-sidecar-service.js.map +1 -0
  429. package/dist/services/asset-upload-errors.d.ts +41 -0
  430. package/dist/services/asset-upload-errors.js +45 -0
  431. package/dist/services/asset-upload-errors.js.map +1 -0
  432. package/dist/services/asset-upload-service.d.ts +74 -0
  433. package/dist/services/asset-upload-service.js +244 -0
  434. package/dist/services/asset-upload-service.js.map +1 -0
  435. package/dist/services/asset-upload-token.d.ts +58 -0
  436. package/dist/services/asset-upload-token.js +75 -0
  437. package/dist/services/asset-upload-token.js.map +1 -0
  438. package/dist/services/assets/asset-index-service.d.ts +127 -0
  439. package/dist/services/assets/asset-index-service.js +227 -0
  440. package/dist/services/assets/asset-index-service.js.map +1 -0
  441. package/dist/services/assets/base.d.ts +118 -1
  442. package/dist/services/assets/base.js +77 -1
  443. package/dist/services/assets/base.js.map +1 -1
  444. package/dist/services/assets/index.d.ts +103 -1
  445. package/dist/services/assets/index.js +192 -0
  446. package/dist/services/assets/index.js.map +1 -1
  447. package/dist/services/assets/local.d.ts +16 -1
  448. package/dist/services/assets/local.js +110 -1
  449. package/dist/services/assets/local.js.map +1 -1
  450. package/dist/services/assets/r2-sync.d.ts +88 -0
  451. package/dist/services/assets/r2-sync.js +412 -0
  452. package/dist/services/assets/r2-sync.js.map +1 -0
  453. package/dist/services/assets/r2.d.ts +87 -1
  454. package/dist/services/assets/r2.js +203 -1
  455. package/dist/services/assets/r2.js.map +1 -1
  456. package/dist/services/auth-service.d.ts +350 -36
  457. package/dist/services/auth-service.js +1179 -140
  458. package/dist/services/auth-service.js.map +1 -1
  459. package/dist/services/bookmark-import-service.d.ts +49 -0
  460. package/dist/services/bookmark-import-service.js +250 -0
  461. package/dist/services/bookmark-import-service.js.map +1 -0
  462. package/dist/services/chat-session-service.d.ts +96 -0
  463. package/dist/services/chat-session-service.js +316 -0
  464. package/dist/services/chat-session-service.js.map +1 -0
  465. package/dist/services/cloud-inference-billing.d.ts +64 -0
  466. package/dist/services/cloud-inference-billing.js +313 -0
  467. package/dist/services/cloud-inference-billing.js.map +1 -0
  468. package/dist/services/comment-service.d.ts +97 -0
  469. package/dist/services/comment-service.js +341 -0
  470. package/dist/services/comment-service.js.map +1 -0
  471. package/dist/services/convert-service.d.ts +64 -0
  472. package/dist/services/convert-service.js +68 -0
  473. package/dist/services/convert-service.js.map +1 -0
  474. package/dist/services/csv-service.d.ts +22 -17
  475. package/dist/services/csv-service.js +55 -92
  476. package/dist/services/csv-service.js.map +1 -1
  477. package/dist/services/entity-mutations.d.ts +210 -0
  478. package/dist/services/entity-mutations.js +377 -0
  479. package/dist/services/entity-mutations.js.map +1 -0
  480. package/dist/services/event-processor.d.ts +104 -0
  481. package/dist/services/event-processor.js +441 -0
  482. package/dist/services/event-processor.js.map +1 -0
  483. package/dist/services/extract/docx.d.ts +1 -0
  484. package/dist/services/extract/docx.js +233 -0
  485. package/dist/services/extract/docx.js.map +1 -0
  486. package/dist/services/extract/index.d.ts +9 -0
  487. package/dist/services/extract/index.js +10 -0
  488. package/dist/services/extract/index.js.map +1 -0
  489. package/dist/services/extract/pdf.d.ts +13 -0
  490. package/dist/services/extract/pdf.js +69 -0
  491. package/dist/services/extract/pdf.js.map +1 -0
  492. package/dist/services/folder-creation.d.ts +33 -0
  493. package/dist/services/folder-creation.js +103 -0
  494. package/dist/services/folder-creation.js.map +1 -0
  495. package/dist/services/git.d.ts +54 -0
  496. package/dist/services/git.js +188 -54
  497. package/dist/services/git.js.map +1 -1
  498. package/dist/services/graph-maintenance.d.ts +42 -0
  499. package/dist/services/graph-maintenance.js +143 -0
  500. package/dist/services/graph-maintenance.js.map +1 -0
  501. package/dist/services/import-service.d.ts +58 -3
  502. package/dist/services/import-service.js +316 -100
  503. package/dist/services/import-service.js.map +1 -1
  504. package/dist/services/lint-service.js +10 -17
  505. package/dist/services/lint-service.js.map +1 -1
  506. package/dist/services/markdown.d.ts +12 -1
  507. package/dist/services/markdown.js +77 -9
  508. package/dist/services/markdown.js.map +1 -1
  509. package/dist/services/memory-service.d.ts +22 -14
  510. package/dist/services/memory-service.js +53 -35
  511. package/dist/services/memory-service.js.map +1 -1
  512. package/dist/services/mention-detector.d.ts +23 -0
  513. package/dist/services/mention-detector.js +47 -0
  514. package/dist/services/mention-detector.js.map +1 -0
  515. package/dist/services/model-capabilities.d.ts +41 -0
  516. package/dist/services/model-capabilities.js +107 -0
  517. package/dist/services/model-capabilities.js.map +1 -0
  518. package/dist/services/notification-broadcast.d.ts +32 -0
  519. package/dist/services/notification-broadcast.js +38 -0
  520. package/dist/services/notification-broadcast.js.map +1 -0
  521. package/dist/services/notification-service.d.ts +41 -0
  522. package/dist/services/notification-service.js +100 -0
  523. package/dist/services/notification-service.js.map +1 -0
  524. package/dist/services/oauth-server-store.d.ts +147 -0
  525. package/dist/services/oauth-server-store.js +319 -0
  526. package/dist/services/oauth-server-store.js.map +1 -0
  527. package/dist/services/orphan-service.js +2 -2
  528. package/dist/services/orphan-service.js.map +1 -1
  529. package/dist/services/personal-folder.d.ts +40 -0
  530. package/dist/services/personal-folder.js +101 -0
  531. package/dist/services/personal-folder.js.map +1 -0
  532. package/dist/services/share-link-service.d.ts +57 -0
  533. package/dist/services/share-link-service.js +142 -0
  534. package/dist/services/share-link-service.js.map +1 -0
  535. package/dist/services/user-person-bridge.d.ts +136 -0
  536. package/dist/services/user-person-bridge.js +340 -0
  537. package/dist/services/user-person-bridge.js.map +1 -0
  538. package/dist/services/vector-service.d.ts +260 -6
  539. package/dist/services/vector-service.js +574 -31
  540. package/dist/services/vector-service.js.map +1 -1
  541. package/dist/services/workspace-access.d.ts +108 -0
  542. package/dist/services/workspace-access.js +149 -0
  543. package/dist/services/workspace-access.js.map +1 -0
  544. package/dist/services/workspace-assets-folder.d.ts +46 -0
  545. package/dist/services/workspace-assets-folder.js +75 -0
  546. package/dist/services/workspace-assets-folder.js.map +1 -0
  547. package/dist/utils/git.d.ts +17 -2
  548. package/dist/utils/git.js +23 -7
  549. package/dist/utils/git.js.map +1 -1
  550. package/dist/utils/log.d.ts +42 -0
  551. package/dist/utils/log.js +137 -0
  552. package/dist/utils/log.js.map +1 -0
  553. package/dist/utils/preflight.js +2 -2
  554. package/dist/utils/preflight.js.map +1 -1
  555. package/dist/utils/version-checker.d.ts +12 -19
  556. package/dist/utils/version-checker.js +14 -104
  557. package/dist/utils/version-checker.js.map +1 -1
  558. package/dist/web/_app/immutable/assets/0.BZHoBJEc.css +2 -0
  559. package/dist/web/_app/immutable/assets/12.DlIf1mKh.css +1 -0
  560. package/dist/web/_app/immutable/assets/13.7d-Q37wc.css +1 -0
  561. package/dist/web/_app/immutable/assets/14.BwBFdc1D.css +1 -0
  562. package/dist/web/_app/immutable/assets/15.CmLrbzZp.css +1 -0
  563. package/dist/web/_app/immutable/assets/16.B0GwXar3.css +1 -0
  564. package/dist/web/_app/immutable/assets/2.C9Vt4JHy.css +1 -0
  565. package/dist/web/_app/immutable/assets/3.BFvgCYSk.css +1 -0
  566. package/dist/web/_app/immutable/assets/4.OmdYF47-.css +1 -0
  567. package/dist/web/_app/immutable/assets/5.C6MySjPq.css +1 -0
  568. package/dist/web/_app/immutable/assets/BulkActionsBar.Bx7nxMjH.css +1 -0
  569. package/dist/web/_app/immutable/assets/CalendarView.lBCvS3X5.css +1 -0
  570. package/dist/web/_app/immutable/assets/CodeMirrorEditor.D6hf2pNJ.css +1 -0
  571. package/dist/web/_app/immutable/assets/CopyField.DVGZtw4U.css +1 -0
  572. package/dist/web/_app/immutable/assets/FolderMembersPanel.DX1oBeF_.css +1 -0
  573. package/dist/web/_app/immutable/assets/FolderMutationDialogs.D2B7KrRn.css +1 -0
  574. package/dist/web/_app/immutable/assets/FolderPicker.B5KdvRYl.css +1 -0
  575. package/dist/web/_app/immutable/assets/GalleryView.CF48FWYQ.css +1 -0
  576. package/dist/web/_app/immutable/assets/GraphView.BJtGL9py.css +1 -0
  577. package/dist/web/_app/immutable/assets/KanbanView.BOaI5KFL.css +1 -0
  578. package/dist/web/_app/immutable/assets/Link.vV0ne105.css +1 -0
  579. package/dist/web/_app/immutable/assets/Markdown.BqgpZCv5.css +1 -0
  580. package/dist/web/_app/immutable/assets/Rail.f8lsriOT.css +1 -0
  581. package/dist/web/_app/immutable/assets/SettingsDialog.DwMus2u2.css +1 -0
  582. package/dist/web/_app/immutable/assets/Spinner.UBfl0pab.css +1 -0
  583. package/dist/web/_app/immutable/assets/StopFilledAlt.fNlDN65D.css +1 -0
  584. package/dist/web/_app/immutable/assets/TimelineView.DriOGAKB.css +1 -0
  585. package/dist/web/_app/immutable/assets/WorkspaceView.u5VdwRR6.css +1 -0
  586. package/dist/web/_app/immutable/assets/button.BlLm4Dg1.css +1 -0
  587. package/dist/web/_app/immutable/assets/checkbox.DK0MgYgb.css +1 -0
  588. package/dist/web/_app/immutable/assets/dist.DDCWxn3B.css +1 -0
  589. package/dist/web/_app/immutable/assets/ibm-plex-mono-latin-400-normal.CvHOgSBP.woff +0 -0
  590. package/dist/web/_app/immutable/assets/ibm-plex-mono-latin-400-normal.DMJ8VG8y.woff2 +0 -0
  591. package/dist/web/_app/immutable/assets/ibm-plex-mono-latin-500-normal.CB9ihrfo.woff +0 -0
  592. package/dist/web/_app/immutable/assets/ibm-plex-mono-latin-500-normal.DSY6xOcd.woff2 +0 -0
  593. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-400-italic.CZTNEAuW.woff2 +0 -0
  594. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-400-italic.CsGl1sm0.woff +0 -0
  595. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-400-normal.CDDApCn2.woff2 +0 -0
  596. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-400-normal.CYLoc0-x.woff +0 -0
  597. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-500-italic.BNK2_mGO.woff2 +0 -0
  598. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-500-italic.DpEwFAQM.woff +0 -0
  599. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-500-normal.6ng42L7E.woff2 +0 -0
  600. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-500-normal.BgVn5rGT.woff +0 -0
  601. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-600-normal.Cu4Hd6ag.woff +0 -0
  602. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-600-normal.CuJfVYMP.woff2 +0 -0
  603. package/dist/web/_app/immutable/assets/inline-list.cqga56xT.css +1 -0
  604. package/dist/web/_app/immutable/assets/list-columns.DiLB0G5L.css +1 -0
  605. package/dist/web/_app/immutable/assets/select.BFL238eD.css +1 -0
  606. package/dist/web/_app/immutable/assets/sheet.4rY4hpfJ.css +1 -0
  607. package/dist/web/_app/immutable/chunks/2oYV6q4R.js +2 -0
  608. package/dist/web/_app/immutable/chunks/4XSri7g0.js +1 -0
  609. package/dist/web/_app/immutable/chunks/4iaT0dG72.js +1 -0
  610. package/dist/web/_app/immutable/chunks/5_TKU9QS2.js +1 -0
  611. package/dist/web/_app/immutable/chunks/8Yh26Z9_2.js +1 -0
  612. package/dist/web/_app/immutable/chunks/Atl8xKOm.js +2 -0
  613. package/dist/web/_app/immutable/chunks/B-Z_xZy_.js +4 -0
  614. package/dist/web/_app/immutable/chunks/B0pk0Lyx.js +1 -0
  615. package/dist/web/_app/immutable/chunks/B1RnjnNt.js +1 -0
  616. package/dist/web/_app/immutable/chunks/B2n86UNJ.js +1 -0
  617. package/dist/web/_app/immutable/chunks/B318GEUB2.js +1 -0
  618. package/dist/web/_app/immutable/chunks/B6Lt-qaJ.js +1 -0
  619. package/dist/web/_app/immutable/chunks/B8yrQWxT2.js +1 -0
  620. package/dist/web/_app/immutable/chunks/BADt8daH.js +2 -0
  621. package/dist/web/_app/immutable/chunks/BBGt-XQo.js +3 -0
  622. package/dist/web/_app/immutable/chunks/BDIUHcEz.js +1 -0
  623. package/dist/web/_app/immutable/chunks/BDSuq3KH.js +1 -0
  624. package/dist/web/_app/immutable/chunks/BGiLU5VN.js +1 -0
  625. package/dist/web/_app/immutable/chunks/BJgOZTHR.js +1 -0
  626. package/dist/web/_app/immutable/chunks/BOSiTh-42.js +1 -0
  627. package/dist/web/_app/immutable/chunks/BPxcCdjn.js +1 -0
  628. package/dist/web/_app/immutable/chunks/BVPNAzAK.js +1 -0
  629. package/dist/web/_app/immutable/chunks/BcOvJgjM.js +1 -0
  630. package/dist/web/_app/immutable/chunks/BdYQewoD.js +1 -0
  631. package/dist/web/_app/immutable/chunks/BfCWIy8t.js +8 -0
  632. package/dist/web/_app/immutable/chunks/BgRT-K6w.js +5 -0
  633. package/dist/web/_app/immutable/chunks/BibveGGd.js +1 -0
  634. package/dist/web/_app/immutable/chunks/BmM5VZ4W.js +1 -0
  635. package/dist/web/_app/immutable/chunks/BnIYzuS1.js +66 -0
  636. package/dist/web/_app/immutable/chunks/BoDmwOTJ.js +1 -0
  637. package/dist/web/_app/immutable/chunks/BoglwbZW.js +1 -0
  638. package/dist/web/_app/immutable/chunks/Bqm_YrIg2.js +23 -0
  639. package/dist/web/_app/immutable/chunks/BtMRrdc4.js +2 -0
  640. package/dist/web/_app/immutable/chunks/C-AnzvKr.js +7 -0
  641. package/dist/web/_app/immutable/chunks/C1Y3tUpz.js +1 -0
  642. package/dist/web/_app/immutable/chunks/C6VZhhwF.js +1 -0
  643. package/dist/web/_app/immutable/chunks/C8IMrD4G2.js +3 -0
  644. package/dist/web/_app/immutable/chunks/CAXXrbj52.js +1 -0
  645. package/dist/web/_app/immutable/chunks/CB-0u-Pc2.js +1 -0
  646. package/dist/web/_app/immutable/chunks/CEniyZ1Z2.js +6 -0
  647. package/dist/web/_app/immutable/chunks/CJhElIkU2.js +1 -0
  648. package/dist/web/_app/immutable/chunks/CK8tIaYT.js +1 -0
  649. package/dist/web/_app/immutable/chunks/CM8uYli8.js +1 -0
  650. package/dist/web/_app/immutable/chunks/CTzjJm_m2.js +1 -0
  651. package/dist/web/_app/immutable/chunks/CUXqa1nn2.js +1 -0
  652. package/dist/web/_app/immutable/chunks/CY0iY_WO.js +1 -0
  653. package/dist/web/_app/immutable/chunks/CbCfNNV_2.js +2692 -0
  654. package/dist/web/_app/immutable/chunks/CbuGIyDu2.js +22 -0
  655. package/dist/web/_app/immutable/chunks/CcRGXA-t.js +1 -0
  656. package/dist/web/_app/immutable/chunks/CcXmQaYs2.js +1 -0
  657. package/dist/web/_app/immutable/chunks/CcaQuaSt2.js +1 -0
  658. package/dist/web/_app/immutable/chunks/CoHaBDpN2.js +2 -0
  659. package/dist/web/_app/immutable/chunks/Cvbd3myc2.js +14 -0
  660. package/dist/web/_app/immutable/chunks/D20C1thX2.js +1 -0
  661. package/dist/web/_app/immutable/chunks/D3JoMtWC.js +7 -0
  662. package/dist/web/_app/immutable/chunks/D3ch71s_.js +1 -0
  663. package/dist/web/_app/immutable/chunks/D4xXlMEU.js +2 -0
  664. package/dist/web/_app/immutable/chunks/D5GipQqN2.js +1 -0
  665. package/dist/web/_app/immutable/chunks/D9TyJuu_.js +1 -0
  666. package/dist/web/_app/immutable/chunks/DBFijAmG.js +1 -0
  667. package/dist/web/_app/immutable/chunks/DHLjkH7T.js +1 -0
  668. package/dist/web/_app/immutable/chunks/DIfdPIt_2.js +1 -0
  669. package/dist/web/_app/immutable/chunks/DO_sw49q2.js +1 -0
  670. package/dist/web/_app/immutable/chunks/DP69PpIj.js +1 -0
  671. package/dist/web/_app/immutable/chunks/DQ3HSZMs.js +2 -0
  672. package/dist/web/_app/immutable/chunks/DQMWa2t7.js +1 -0
  673. package/dist/web/_app/immutable/chunks/DT0wzK622.js +1 -0
  674. package/dist/web/_app/immutable/chunks/DTnEJArL.js +1 -0
  675. package/dist/web/_app/immutable/chunks/DToS-G202.js +1 -0
  676. package/dist/web/_app/immutable/chunks/DZcU6mGe2.js +1 -0
  677. package/dist/web/_app/immutable/chunks/DbslMybD.js +1 -0
  678. package/dist/web/_app/immutable/chunks/DcGThsSF.js +1 -0
  679. package/dist/web/_app/immutable/chunks/DcPrzUMJ.js +1 -0
  680. package/dist/web/_app/immutable/chunks/De0g5qop2.js +1 -0
  681. package/dist/web/_app/immutable/chunks/DimS8dbZ2.js +1 -0
  682. package/dist/web/_app/immutable/chunks/DjuqhOjF.js +1 -0
  683. package/dist/web/_app/immutable/chunks/Dk6sAhpo.js +3 -0
  684. package/dist/web/_app/immutable/chunks/Dl6cnwyk.js +1 -0
  685. package/dist/web/_app/immutable/chunks/DnIQzIOz.js +1 -0
  686. package/dist/web/_app/immutable/chunks/DnXYAo60.js +1 -0
  687. package/dist/web/_app/immutable/chunks/DnrImJan.js +1 -0
  688. package/dist/web/_app/immutable/chunks/Dpuq25vu2.js +1 -0
  689. package/dist/web/_app/immutable/chunks/DrMZDWru.js +1 -0
  690. package/dist/web/_app/immutable/chunks/DsVOtl0N.js +1 -0
  691. package/dist/web/_app/immutable/chunks/DuTXsWdP.js +2 -0
  692. package/dist/web/_app/immutable/chunks/Dw-frCfE.js +1 -0
  693. package/dist/web/_app/immutable/chunks/Dz34u8Ie2.js +185 -0
  694. package/dist/web/_app/immutable/chunks/DzcFeQZU2.js +2 -0
  695. package/dist/web/_app/immutable/chunks/DzrRTvug.js +1 -0
  696. package/dist/web/_app/immutable/chunks/G5oUVQa92.js +1 -0
  697. package/dist/web/_app/immutable/chunks/HyhOhoeq2.js +2 -0
  698. package/dist/web/_app/immutable/chunks/J5nb9xxj.js +1 -0
  699. package/dist/web/_app/immutable/chunks/JCjEwqH9.js +1 -0
  700. package/dist/web/_app/immutable/chunks/JN_nsXUm2.js +1 -0
  701. package/dist/web/_app/immutable/chunks/JS2_GaB1.js +1 -0
  702. package/dist/web/_app/immutable/chunks/MtI6dyxu.js +1 -0
  703. package/dist/web/_app/immutable/chunks/N6vnY5Si2.js +1 -0
  704. package/dist/web/_app/immutable/chunks/PcEbaKam.js +2 -0
  705. package/dist/web/_app/immutable/chunks/VTYhCLh9.js +2 -0
  706. package/dist/web/_app/immutable/chunks/Wowi_89k.js +5 -0
  707. package/dist/web/_app/immutable/chunks/Y_asqJqs2.js +83 -0
  708. package/dist/web/_app/immutable/chunks/Yhg5Q4MT.js +1 -0
  709. package/dist/web/_app/immutable/chunks/f_eYccPf2.js +1 -0
  710. package/dist/web/_app/immutable/chunks/hm_8RsuL.js +1 -0
  711. package/dist/web/_app/immutable/chunks/iPCkjliu.js +3 -0
  712. package/dist/web/_app/immutable/chunks/j2AvgZGz.js +1 -0
  713. package/dist/web/_app/immutable/chunks/kNaey6uv.js +1 -0
  714. package/dist/web/_app/immutable/chunks/l1761xxj.js +1 -0
  715. package/dist/web/_app/immutable/chunks/liGRFQqV2.js +184 -0
  716. package/dist/web/_app/immutable/chunks/lq1kR86d.js +1 -0
  717. package/dist/web/_app/immutable/chunks/mIykYR4F2.js +1 -0
  718. package/dist/web/_app/immutable/chunks/rbhlO_t8.js +1 -0
  719. package/dist/web/_app/immutable/chunks/tJpAwaTl.js +1 -0
  720. package/dist/web/_app/immutable/chunks/tcegdii0.js +2 -0
  721. package/dist/web/_app/immutable/chunks/u8G2EUvz2.js +224 -0
  722. package/dist/web/_app/immutable/chunks/vgsM0KTm2.js +1 -0
  723. package/dist/web/_app/immutable/chunks/xcGI4fuV.js +1 -0
  724. package/dist/web/_app/immutable/chunks/xihTtKlq.js +1 -0
  725. package/dist/web/_app/immutable/chunks/yEYlhV9G2.js +1 -0
  726. package/dist/web/_app/immutable/entry/app.DSf_XGp8.js +2 -0
  727. package/dist/web/_app/immutable/entry/start.B7ScfxPc.js +1 -0
  728. package/dist/web/_app/immutable/nodes/0.BMSOOETY.js +2 -0
  729. package/dist/web/_app/immutable/nodes/1.B0lBUpHt.js +1 -0
  730. package/dist/web/_app/immutable/nodes/10.DmDbUH1j.js +1 -0
  731. package/dist/web/_app/immutable/nodes/11.gwY7gZdR.js +1 -0
  732. package/dist/web/_app/immutable/nodes/12.BpGHSr2y.js +1 -0
  733. package/dist/web/_app/immutable/nodes/13.Bf178QF_.js +1 -0
  734. package/dist/web/_app/immutable/nodes/14.D_x8-nk9.js +1 -0
  735. package/dist/web/_app/immutable/nodes/15.DY4XeagL.js +1 -0
  736. package/dist/web/_app/immutable/nodes/16.DPqftx8x.js +77 -0
  737. package/dist/web/_app/immutable/nodes/2.OM2LGRpi.js +109 -0
  738. package/dist/web/_app/immutable/nodes/3.e8ZoPCmC.js +6 -0
  739. package/dist/web/_app/immutable/nodes/4.BdYR1hNQ.js +11 -0
  740. package/dist/web/_app/immutable/nodes/5.B8sOFna9.js +5 -0
  741. package/dist/web/_app/immutable/nodes/6.L1BbYsmo.js +1 -0
  742. package/dist/web/_app/immutable/nodes/7.B7zoemJD.js +1 -0
  743. package/dist/web/_app/immutable/nodes/8.B1VtnfMK.js +1 -0
  744. package/dist/web/_app/immutable/nodes/9.BE3VCQzk.js +1 -0
  745. package/dist/web/_app/version.json +1 -1
  746. package/dist/web/favicon-16.png +0 -0
  747. package/dist/web/favicon-180.png +0 -0
  748. package/dist/web/favicon-32.png +0 -0
  749. package/dist/web/favicon-48.png +0 -0
  750. package/dist/web/favicon-512.png +0 -0
  751. package/dist/web/favicon.ico +0 -0
  752. package/dist/web/favicon.svg +8 -0
  753. package/dist/web/index.html +58 -19
  754. package/dist/web/studiograph-logomark.svg +29 -0
  755. package/package.json +38 -14
  756. package/dist/agent/skills/enrich-entities.md +0 -136
  757. package/dist/agent/skills/obsidian-source-setup.md +0 -246
  758. package/dist/agent/skills/skill-loader.d.ts +0 -48
  759. package/dist/agent/skills/skill-loader.js +0 -166
  760. package/dist/agent/skills/skill-loader.js.map +0 -1
  761. package/dist/agent/skills/sync-configuration.md +0 -119
  762. package/dist/agent/skills/sync-setup.md +0 -82
  763. package/dist/agent/tools/sync-tools.d.ts +0 -35
  764. package/dist/agent/tools/sync-tools.js +0 -992
  765. package/dist/agent/tools/sync-tools.js.map +0 -1
  766. package/dist/auth/github.d.ts +0 -56
  767. package/dist/auth/github.js +0 -169
  768. package/dist/auth/github.js.map +0 -1
  769. package/dist/cli/commands/access.d.ts +0 -11
  770. package/dist/cli/commands/access.js +0 -156
  771. package/dist/cli/commands/access.js.map +0 -1
  772. package/dist/cli/commands/app.d.ts +0 -7
  773. package/dist/cli/commands/app.js +0 -268
  774. package/dist/cli/commands/app.js.map +0 -1
  775. package/dist/cli/commands/auth.d.ts +0 -10
  776. package/dist/cli/commands/auth.js +0 -79
  777. package/dist/cli/commands/auth.js.map +0 -1
  778. package/dist/cli/commands/clear.d.ts +0 -5
  779. package/dist/cli/commands/clear.js +0 -36
  780. package/dist/cli/commands/clear.js.map +0 -1
  781. package/dist/cli/commands/collection.d.ts +0 -10
  782. package/dist/cli/commands/collection.js +0 -187
  783. package/dist/cli/commands/collection.js.map +0 -1
  784. package/dist/cli/commands/enrich.d.ts +0 -11
  785. package/dist/cli/commands/enrich.js +0 -135
  786. package/dist/cli/commands/enrich.js.map +0 -1
  787. package/dist/cli/commands/graphrag.d.ts +0 -12
  788. package/dist/cli/commands/graphrag.js +0 -122
  789. package/dist/cli/commands/graphrag.js.map +0 -1
  790. package/dist/cli/commands/join.d.ts +0 -9
  791. package/dist/cli/commands/join.js +0 -255
  792. package/dist/cli/commands/join.js.map +0 -1
  793. package/dist/cli/commands/members.d.ts +0 -11
  794. package/dist/cli/commands/members.js +0 -230
  795. package/dist/cli/commands/members.js.map +0 -1
  796. package/dist/cli/commands/org.d.ts +0 -10
  797. package/dist/cli/commands/org.js +0 -132
  798. package/dist/cli/commands/org.js.map +0 -1
  799. package/dist/cli/commands/provision.d.ts +0 -8
  800. package/dist/cli/commands/provision.js +0 -116
  801. package/dist/cli/commands/provision.js.map +0 -1
  802. package/dist/cli/commands/resolve.d.ts +0 -8
  803. package/dist/cli/commands/resolve.js +0 -85
  804. package/dist/cli/commands/resolve.js.map +0 -1
  805. package/dist/cli/commands/review.d.ts +0 -19
  806. package/dist/cli/commands/review.js +0 -128
  807. package/dist/cli/commands/review.js.map +0 -1
  808. package/dist/cli/commands/source.d.ts +0 -16
  809. package/dist/cli/commands/source.js +0 -159
  810. package/dist/cli/commands/source.js.map +0 -1
  811. package/dist/cli/commands/start.d.ts +0 -7
  812. package/dist/cli/commands/start.js +0 -589
  813. package/dist/cli/commands/start.js.map +0 -1
  814. package/dist/cli/commands/sync-collection.d.ts +0 -14
  815. package/dist/cli/commands/sync-collection.js +0 -355
  816. package/dist/cli/commands/sync-collection.js.map +0 -1
  817. package/dist/cli/commands/sync-entities.d.ts +0 -13
  818. package/dist/cli/commands/sync-entities.js +0 -242
  819. package/dist/cli/commands/sync-entities.js.map +0 -1
  820. package/dist/cli/commands/team.d.ts +0 -12
  821. package/dist/cli/commands/team.js +0 -185
  822. package/dist/cli/commands/team.js.map +0 -1
  823. package/dist/cli/commands/update.d.ts +0 -8
  824. package/dist/cli/commands/update.js +0 -155
  825. package/dist/cli/commands/update.js.map +0 -1
  826. package/dist/cli/commands/user.d.ts +0 -7
  827. package/dist/cli/commands/user.js +0 -153
  828. package/dist/cli/commands/user.js.map +0 -1
  829. package/dist/cli/scaffolding.d.ts +0 -12
  830. package/dist/cli/scaffolding.js +0 -397
  831. package/dist/cli/scaffolding.js.map +0 -1
  832. package/dist/cli/sync-review-interactive.d.ts +0 -31
  833. package/dist/cli/sync-review-interactive.js +0 -393
  834. package/dist/cli/sync-review-interactive.js.map +0 -1
  835. package/dist/core/migration-runner.d.ts +0 -42
  836. package/dist/core/migration-runner.js +0 -232
  837. package/dist/core/migration-runner.js.map +0 -1
  838. package/dist/core/migration-types.d.ts +0 -101
  839. package/dist/core/migration-types.js +0 -21
  840. package/dist/core/migration-types.js.map +0 -1
  841. package/dist/core/migrations/20260219-formalize-memory-location.d.ts +0 -2
  842. package/dist/core/migrations/20260219-formalize-memory-location.js +0 -35
  843. package/dist/core/migrations/20260219-formalize-memory-location.js.map +0 -1
  844. package/dist/core/migrations/20260220-add-workspace-metadata.d.ts +0 -12
  845. package/dist/core/migrations/20260220-add-workspace-metadata.js +0 -65
  846. package/dist/core/migrations/20260220-add-workspace-metadata.js.map +0 -1
  847. package/dist/core/migrations/20260220-add-workspace-readme.d.ts +0 -11
  848. package/dist/core/migrations/20260220-add-workspace-readme.js +0 -82
  849. package/dist/core/migrations/20260220-add-workspace-readme.js.map +0 -1
  850. package/dist/core/migrations/20260220-migrate-yaml-to-json.d.ts +0 -9
  851. package/dist/core/migrations/20260220-migrate-yaml-to-json.js +0 -64
  852. package/dist/core/migrations/20260220-migrate-yaml-to-json.js.map +0 -1
  853. package/dist/core/migrations/index.d.ts +0 -11
  854. package/dist/core/migrations/index.js +0 -23
  855. package/dist/core/migrations/index.js.map +0 -1
  856. package/dist/integrations/asana.d.ts +0 -26
  857. package/dist/integrations/asana.js +0 -78
  858. package/dist/integrations/asana.js.map +0 -1
  859. package/dist/integrations/figma-local.d.ts +0 -16
  860. package/dist/integrations/figma-local.js +0 -10
  861. package/dist/integrations/figma-local.js.map +0 -1
  862. package/dist/integrations/figma.d.ts +0 -17
  863. package/dist/integrations/figma.js +0 -17
  864. package/dist/integrations/figma.js.map +0 -1
  865. package/dist/integrations/granola.d.ts +0 -24
  866. package/dist/integrations/granola.js +0 -60
  867. package/dist/integrations/granola.js.map +0 -1
  868. package/dist/integrations/linear.d.ts +0 -14
  869. package/dist/integrations/linear.js +0 -80
  870. package/dist/integrations/linear.js.map +0 -1
  871. package/dist/integrations/paper-local.d.ts +0 -14
  872. package/dist/integrations/paper-local.js +0 -10
  873. package/dist/integrations/paper-local.js.map +0 -1
  874. package/dist/integrations/paper.d.ts +0 -2
  875. package/dist/integrations/paper.js +0 -10
  876. package/dist/integrations/paper.js.map +0 -1
  877. package/dist/integrations/pipedrive.d.ts +0 -26
  878. package/dist/integrations/pipedrive.js +0 -97
  879. package/dist/integrations/pipedrive.js.map +0 -1
  880. package/dist/integrations/registry.d.ts +0 -15
  881. package/dist/integrations/registry.js +0 -27
  882. package/dist/integrations/registry.js.map +0 -1
  883. package/dist/integrations/types.d.ts +0 -38
  884. package/dist/integrations/types.js +0 -9
  885. package/dist/integrations/types.js.map +0 -1
  886. package/dist/lib/lib/utils.d.ts +0 -2
  887. package/dist/lib/lib/utils.js +0 -6
  888. package/dist/lib/lib/utils.js.map +0 -1
  889. package/dist/mcp/connectors/asana.d.ts +0 -2
  890. package/dist/mcp/connectors/asana.js +0 -20
  891. package/dist/mcp/connectors/asana.js.map +0 -1
  892. package/dist/mcp/connectors/definitions.d.ts +0 -45
  893. package/dist/mcp/connectors/definitions.js +0 -32
  894. package/dist/mcp/connectors/definitions.js.map +0 -1
  895. package/dist/mcp/connectors/figma.d.ts +0 -5
  896. package/dist/mcp/connectors/figma.js +0 -21
  897. package/dist/mcp/connectors/figma.js.map +0 -1
  898. package/dist/mcp/connectors/gdrive.d.ts +0 -2
  899. package/dist/mcp/connectors/gdrive.js +0 -20
  900. package/dist/mcp/connectors/gdrive.js.map +0 -1
  901. package/dist/mcp/connectors/granola.d.ts +0 -2
  902. package/dist/mcp/connectors/granola.js +0 -12
  903. package/dist/mcp/connectors/granola.js.map +0 -1
  904. package/dist/mcp/connectors/linear.d.ts +0 -2
  905. package/dist/mcp/connectors/linear.js +0 -19
  906. package/dist/mcp/connectors/linear.js.map +0 -1
  907. package/dist/mcp/connectors/obsidian.d.ts +0 -2
  908. package/dist/mcp/connectors/obsidian.js +0 -19
  909. package/dist/mcp/connectors/obsidian.js.map +0 -1
  910. package/dist/mcp/connectors/pipedrive.d.ts +0 -2
  911. package/dist/mcp/connectors/pipedrive.js +0 -20
  912. package/dist/mcp/connectors/pipedrive.js.map +0 -1
  913. package/dist/mcp/connectors/slack.d.ts +0 -2
  914. package/dist/mcp/connectors/slack.js +0 -21
  915. package/dist/mcp/connectors/slack.js.map +0 -1
  916. package/dist/mcp/server-oauth-provider.d.ts +0 -56
  917. package/dist/mcp/server-oauth-provider.js +0 -142
  918. package/dist/mcp/server-oauth-provider.js.map +0 -1
  919. package/dist/server/chrome/chrome.css +0 -691
  920. package/dist/server/chrome/chrome.js +0 -374
  921. package/dist/server/collab-authority.d.ts +0 -70
  922. package/dist/server/collab-authority.js +0 -218
  923. package/dist/server/collab-authority.js.map +0 -1
  924. package/dist/server/collab.d.ts +0 -29
  925. package/dist/server/collab.js +0 -195
  926. package/dist/server/collab.js.map +0 -1
  927. package/dist/server/commit-scheduler.d.ts +0 -39
  928. package/dist/server/commit-scheduler.js +0 -113
  929. package/dist/server/commit-scheduler.js.map +0 -1
  930. package/dist/server/plugin-loader.d.ts +0 -53
  931. package/dist/server/plugin-loader.js +0 -155
  932. package/dist/server/plugin-loader.js.map +0 -1
  933. package/dist/server/routes/collab.d.ts +0 -6
  934. package/dist/server/routes/collab.js +0 -10
  935. package/dist/server/routes/collab.js.map +0 -1
  936. package/dist/server/routes/git-api.d.ts +0 -9
  937. package/dist/server/routes/git-api.js +0 -82
  938. package/dist/server/routes/git-api.js.map +0 -1
  939. package/dist/server/routes/meeting-ingest-api.d.ts +0 -15
  940. package/dist/server/routes/meeting-ingest-api.js +0 -323
  941. package/dist/server/routes/meeting-ingest-api.js.map +0 -1
  942. package/dist/server/routes/sync-api.d.ts +0 -26
  943. package/dist/server/routes/sync-api.js +0 -814
  944. package/dist/server/routes/sync-api.js.map +0 -1
  945. package/dist/server/routes/webhook.d.ts +0 -14
  946. package/dist/server/routes/webhook.js +0 -102
  947. package/dist/server/routes/webhook.js.map +0 -1
  948. package/dist/services/github-provisioner.d.ts +0 -36
  949. package/dist/services/github-provisioner.js +0 -126
  950. package/dist/services/github-provisioner.js.map +0 -1
  951. package/dist/services/github-service.d.ts +0 -99
  952. package/dist/services/github-service.js +0 -310
  953. package/dist/services/github-service.js.map +0 -1
  954. package/dist/services/insights.d.ts +0 -38
  955. package/dist/services/insights.js +0 -246
  956. package/dist/services/insights.js.map +0 -1
  957. package/dist/services/sync/collection-sync.d.ts +0 -60
  958. package/dist/services/sync/collection-sync.js +0 -509
  959. package/dist/services/sync/collection-sync.js.map +0 -1
  960. package/dist/services/sync/commit.d.ts +0 -60
  961. package/dist/services/sync/commit.js +0 -354
  962. package/dist/services/sync/commit.js.map +0 -1
  963. package/dist/services/sync/context-index.d.ts +0 -69
  964. package/dist/services/sync/context-index.js +0 -280
  965. package/dist/services/sync/context-index.js.map +0 -1
  966. package/dist/services/sync/data-fetcher.d.ts +0 -31
  967. package/dist/services/sync/data-fetcher.js +0 -12
  968. package/dist/services/sync/data-fetcher.js.map +0 -1
  969. package/dist/services/sync/derive.d.ts +0 -34
  970. package/dist/services/sync/derive.js +0 -164
  971. package/dist/services/sync/derive.js.map +0 -1
  972. package/dist/services/sync/enrichment-state.d.ts +0 -31
  973. package/dist/services/sync/enrichment-state.js +0 -63
  974. package/dist/services/sync/enrichment-state.js.map +0 -1
  975. package/dist/services/sync/enrichment.d.ts +0 -25
  976. package/dist/services/sync/enrichment.js +0 -121
  977. package/dist/services/sync/enrichment.js.map +0 -1
  978. package/dist/services/sync/entity-refresh.d.ts +0 -30
  979. package/dist/services/sync/entity-refresh.js +0 -275
  980. package/dist/services/sync/entity-refresh.js.map +0 -1
  981. package/dist/services/sync/frontmatter-extractor.d.ts +0 -40
  982. package/dist/services/sync/frontmatter-extractor.js +0 -279
  983. package/dist/services/sync/frontmatter-extractor.js.map +0 -1
  984. package/dist/services/sync/graph-match-state.d.ts +0 -33
  985. package/dist/services/sync/graph-match-state.js +0 -61
  986. package/dist/services/sync/graph-match-state.js.map +0 -1
  987. package/dist/services/sync/graph-match.d.ts +0 -53
  988. package/dist/services/sync/graph-match.js +0 -316
  989. package/dist/services/sync/graph-match.js.map +0 -1
  990. package/dist/services/sync/graphrag-client.d.ts +0 -43
  991. package/dist/services/sync/graphrag-client.js +0 -94
  992. package/dist/services/sync/graphrag-client.js.map +0 -1
  993. package/dist/services/sync/graphrag-config.d.ts +0 -16
  994. package/dist/services/sync/graphrag-config.js +0 -39
  995. package/dist/services/sync/graphrag-config.js.map +0 -1
  996. package/dist/services/sync/graphrag-context.d.ts +0 -14
  997. package/dist/services/sync/graphrag-context.js +0 -109
  998. package/dist/services/sync/graphrag-context.js.map +0 -1
  999. package/dist/services/sync/graphrag-indexer.d.ts +0 -30
  1000. package/dist/services/sync/graphrag-indexer.js +0 -358
  1001. package/dist/services/sync/graphrag-indexer.js.map +0 -1
  1002. package/dist/services/sync/llm.d.ts +0 -32
  1003. package/dist/services/sync/llm.js +0 -115
  1004. package/dist/services/sync/llm.js.map +0 -1
  1005. package/dist/services/sync/mcp-client.d.ts +0 -71
  1006. package/dist/services/sync/mcp-client.js +0 -299
  1007. package/dist/services/sync/mcp-client.js.map +0 -1
  1008. package/dist/services/sync/model-factory.d.ts +0 -13
  1009. package/dist/services/sync/model-factory.js +0 -38
  1010. package/dist/services/sync/model-factory.js.map +0 -1
  1011. package/dist/services/sync/name-quality.d.ts +0 -31
  1012. package/dist/services/sync/name-quality.js +0 -60
  1013. package/dist/services/sync/name-quality.js.map +0 -1
  1014. package/dist/services/sync/output-schemas.d.ts +0 -92
  1015. package/dist/services/sync/output-schemas.js +0 -43
  1016. package/dist/services/sync/output-schemas.js.map +0 -1
  1017. package/dist/services/sync/prompts.d.ts +0 -19
  1018. package/dist/services/sync/prompts.js +0 -128
  1019. package/dist/services/sync/prompts.js.map +0 -1
  1020. package/dist/services/sync/reconciler.d.ts +0 -48
  1021. package/dist/services/sync/reconciler.js +0 -294
  1022. package/dist/services/sync/reconciler.js.map +0 -1
  1023. package/dist/services/sync/rest-client.d.ts +0 -40
  1024. package/dist/services/sync/rest-client.js +0 -100
  1025. package/dist/services/sync/rest-client.js.map +0 -1
  1026. package/dist/services/sync/source-config.d.ts +0 -67
  1027. package/dist/services/sync/source-config.js +0 -304
  1028. package/dist/services/sync/source-config.js.map +0 -1
  1029. package/dist/services/sync/source-definitions/asana.d.ts +0 -14
  1030. package/dist/services/sync/source-definitions/asana.js +0 -42
  1031. package/dist/services/sync/source-definitions/asana.js.map +0 -1
  1032. package/dist/services/sync/source-definitions/definitions.d.ts +0 -8
  1033. package/dist/services/sync/source-definitions/definitions.js +0 -8
  1034. package/dist/services/sync/source-definitions/definitions.js.map +0 -1
  1035. package/dist/services/sync/source-definitions/gdrive.d.ts +0 -16
  1036. package/dist/services/sync/source-definitions/gdrive.js +0 -68
  1037. package/dist/services/sync/source-definitions/gdrive.js.map +0 -1
  1038. package/dist/services/sync/source-definitions/granola.d.ts +0 -2
  1039. package/dist/services/sync/source-definitions/granola.js +0 -21
  1040. package/dist/services/sync/source-definitions/granola.js.map +0 -1
  1041. package/dist/services/sync/source-definitions/linear.d.ts +0 -2
  1042. package/dist/services/sync/source-definitions/linear.js +0 -62
  1043. package/dist/services/sync/source-definitions/linear.js.map +0 -1
  1044. package/dist/services/sync/source-definitions/obsidian.d.ts +0 -2
  1045. package/dist/services/sync/source-definitions/obsidian.js +0 -55
  1046. package/dist/services/sync/source-definitions/obsidian.js.map +0 -1
  1047. package/dist/services/sync/source-definitions/pipedrive.d.ts +0 -2
  1048. package/dist/services/sync/source-definitions/pipedrive.js +0 -43
  1049. package/dist/services/sync/source-definitions/pipedrive.js.map +0 -1
  1050. package/dist/services/sync/staging.d.ts +0 -53
  1051. package/dist/services/sync/staging.js +0 -130
  1052. package/dist/services/sync/staging.js.map +0 -1
  1053. package/dist/services/sync/structured-extractor.d.ts +0 -57
  1054. package/dist/services/sync/structured-extractor.js +0 -584
  1055. package/dist/services/sync/structured-extractor.js.map +0 -1
  1056. package/dist/services/sync/sync-runner.d.ts +0 -32
  1057. package/dist/services/sync/sync-runner.js +0 -195
  1058. package/dist/services/sync/sync-runner.js.map +0 -1
  1059. package/dist/services/sync/sync-state.d.ts +0 -43
  1060. package/dist/services/sync/sync-state.js +0 -154
  1061. package/dist/services/sync/sync-state.js.map +0 -1
  1062. package/dist/services/sync/types.d.ts +0 -381
  1063. package/dist/services/sync/types.js +0 -8
  1064. package/dist/services/sync/types.js.map +0 -1
  1065. package/dist/services/sync/unstructured-extractor.d.ts +0 -27
  1066. package/dist/services/sync/unstructured-extractor.js +0 -185
  1067. package/dist/services/sync/unstructured-extractor.js.map +0 -1
  1068. package/dist/utils/merge-resolver.d.ts +0 -59
  1069. package/dist/utils/merge-resolver.js +0 -303
  1070. package/dist/utils/merge-resolver.js.map +0 -1
  1071. package/dist/utils/workspace-config.d.ts +0 -8
  1072. package/dist/utils/workspace-config.js +0 -22
  1073. package/dist/utils/workspace-config.js.map +0 -1
  1074. package/dist/web/_app/immutable/assets/0.DdizXOKi.css +0 -1
  1075. package/dist/web/_app/immutable/assets/10.BUrHkYry.css +0 -1
  1076. package/dist/web/_app/immutable/assets/2.n7-yW_Fs.css +0 -1
  1077. package/dist/web/_app/immutable/assets/3.BtaZagnv.css +0 -1
  1078. package/dist/web/_app/immutable/assets/4.DT7X0x5S.css +0 -1
  1079. package/dist/web/_app/immutable/assets/5.DFeGxLYf.css +0 -1
  1080. package/dist/web/_app/immutable/assets/6.CquhUpOu.css +0 -1
  1081. package/dist/web/_app/immutable/assets/7.CjbDRbuG.css +0 -1
  1082. package/dist/web/_app/immutable/assets/8.S1QdUGNM.css +0 -1
  1083. package/dist/web/_app/immutable/assets/9.CT4xL4K3.css +0 -1
  1084. package/dist/web/_app/immutable/assets/Copy.FS8bdfxr.css +0 -1
  1085. package/dist/web/_app/immutable/assets/Toaster.rN8r_Hzv.css +0 -1
  1086. package/dist/web/_app/immutable/assets/ViewToolbar.D-QW2oKe.css +0 -1
  1087. package/dist/web/_app/immutable/assets/index.D6tMDJWk.css +0 -1
  1088. package/dist/web/_app/immutable/chunks/-iaeBIWN.js +0 -1
  1089. package/dist/web/_app/immutable/chunks/37NdyH6G.js +0 -32
  1090. package/dist/web/_app/immutable/chunks/4Hhzb6pv.js +0 -1
  1091. package/dist/web/_app/immutable/chunks/72bZS3G9.js +0 -1
  1092. package/dist/web/_app/immutable/chunks/7S2LGqoP.js +0 -2
  1093. package/dist/web/_app/immutable/chunks/B340SUKR.js +0 -1
  1094. package/dist/web/_app/immutable/chunks/B3Z4_Sps.js +0 -1
  1095. package/dist/web/_app/immutable/chunks/BB_5th5W.js +0 -3383
  1096. package/dist/web/_app/immutable/chunks/BPWYOQXG.js +0 -1
  1097. package/dist/web/_app/immutable/chunks/BR77jbfR.js +0 -1
  1098. package/dist/web/_app/immutable/chunks/BS2rFAVq.js +0 -1
  1099. package/dist/web/_app/immutable/chunks/BSsDC_p4.js +0 -1
  1100. package/dist/web/_app/immutable/chunks/BTX2Jr8_.js +0 -1
  1101. package/dist/web/_app/immutable/chunks/Bq_KShUL.js +0 -4
  1102. package/dist/web/_app/immutable/chunks/Bsacq2UX.js +0 -1
  1103. package/dist/web/_app/immutable/chunks/ByunV8un.js +0 -1
  1104. package/dist/web/_app/immutable/chunks/BzPu-Eht.js +0 -1
  1105. package/dist/web/_app/immutable/chunks/C7VoTosa.js +0 -2
  1106. package/dist/web/_app/immutable/chunks/CDV5Q7RN.js +0 -1
  1107. package/dist/web/_app/immutable/chunks/CGc1sIb_.js +0 -1
  1108. package/dist/web/_app/immutable/chunks/CJ8__CWt.js +0 -1
  1109. package/dist/web/_app/immutable/chunks/CJUKmosC.js +0 -5
  1110. package/dist/web/_app/immutable/chunks/CSVbGg_b.js +0 -5
  1111. package/dist/web/_app/immutable/chunks/CT6oIU-C.js +0 -1
  1112. package/dist/web/_app/immutable/chunks/CTRxNfZk.js +0 -1
  1113. package/dist/web/_app/immutable/chunks/CWQRQXxy.js +0 -1
  1114. package/dist/web/_app/immutable/chunks/CX87mIM8.js +0 -1
  1115. package/dist/web/_app/immutable/chunks/CXH6iFbA.js +0 -1
  1116. package/dist/web/_app/immutable/chunks/CX_61fY8.js +0 -5
  1117. package/dist/web/_app/immutable/chunks/CYrVHOHA.js +0 -1
  1118. package/dist/web/_app/immutable/chunks/CdeYcEuU.js +0 -1
  1119. package/dist/web/_app/immutable/chunks/CmCtpZ1Y.js +0 -1
  1120. package/dist/web/_app/immutable/chunks/Cpsr8-Xy.js +0 -1
  1121. package/dist/web/_app/immutable/chunks/CqkleIqs.js +0 -1
  1122. package/dist/web/_app/immutable/chunks/CrdV0ttd.js +0 -1
  1123. package/dist/web/_app/immutable/chunks/CscoF-YL.js +0 -18
  1124. package/dist/web/_app/immutable/chunks/Cx4YdFMk.js +0 -1
  1125. package/dist/web/_app/immutable/chunks/D7ZqAKi9.js +0 -3
  1126. package/dist/web/_app/immutable/chunks/DGbA7IXh.js +0 -2
  1127. package/dist/web/_app/immutable/chunks/DTZE-yoq.js +0 -2
  1128. package/dist/web/_app/immutable/chunks/DbbB6Up5.js +0 -1
  1129. package/dist/web/_app/immutable/chunks/DnL9f0lc.js +0 -7
  1130. package/dist/web/_app/immutable/chunks/DtM5cEDu.js +0 -1
  1131. package/dist/web/_app/immutable/chunks/Eqahi7_S.js +0 -2
  1132. package/dist/web/_app/immutable/chunks/O9Eb9k00.js +0 -1
  1133. package/dist/web/_app/immutable/chunks/PPVm8Dsz.js +0 -1
  1134. package/dist/web/_app/immutable/chunks/QF8MAmnc.js +0 -1
  1135. package/dist/web/_app/immutable/chunks/UjzxCpxn.js +0 -1
  1136. package/dist/web/_app/immutable/chunks/WSUKABI_.js +0 -1
  1137. package/dist/web/_app/immutable/chunks/Y90OgS-9.js +0 -1
  1138. package/dist/web/_app/immutable/chunks/cLyZlkXv.js +0 -1
  1139. package/dist/web/_app/immutable/chunks/iDtAARVW.js +0 -2
  1140. package/dist/web/_app/immutable/chunks/oC_W7W7p.js +0 -1
  1141. package/dist/web/_app/immutable/chunks/rJdWIgh-.js +0 -1
  1142. package/dist/web/_app/immutable/chunks/tvOsokaa.js +0 -1
  1143. package/dist/web/_app/immutable/chunks/uLEXaPwE.js +0 -1
  1144. package/dist/web/_app/immutable/chunks/vSaooFHB.js +0 -1
  1145. package/dist/web/_app/immutable/entry/app.BeUKh4sD.js +0 -2
  1146. package/dist/web/_app/immutable/entry/start.CZJnFuQn.js +0 -1
  1147. package/dist/web/_app/immutable/nodes/0.DyF_RsLg.js +0 -2
  1148. package/dist/web/_app/immutable/nodes/1.7iDQmMPh.js +0 -1
  1149. package/dist/web/_app/immutable/nodes/10.Btl8d8HT.js +0 -1
  1150. package/dist/web/_app/immutable/nodes/2.LDRE62jN.js +0 -168
  1151. package/dist/web/_app/immutable/nodes/3.BaW1XB1x.js +0 -1
  1152. package/dist/web/_app/immutable/nodes/4.D0Q8kpkI.js +0 -12
  1153. package/dist/web/_app/immutable/nodes/5.n7j2UG9h.js +0 -4
  1154. package/dist/web/_app/immutable/nodes/6.DQYxR0iy.js +0 -2
  1155. package/dist/web/_app/immutable/nodes/7.BsURvFAp.js +0 -2
  1156. package/dist/web/_app/immutable/nodes/8.CsiErG0p.js +0 -1
  1157. package/dist/web/_app/immutable/nodes/9.CnN6OZgC.js +0 -1
@@ -2,135 +2,62 @@
2
2
  * Graph API routes
3
3
  *
4
4
  * REST façade over WorkspaceManager.
5
- * Enforces per-collection access when users are authenticated via JWT.
5
+ * Enforces per-folder access when users are authenticated via JWT.
6
6
  * API key / open mode → full access (no filtering).
7
7
  */
8
+ import { isWorkspaceOwner } from '../../services/auth-service.js';
9
+ import { WorkspaceAccessService } from '../../services/workspace-access.js';
8
10
  import { Workspace } from '../../core/workspace.js';
9
- import { existsSync } from 'fs';
10
- import { join } from 'path';
11
- /** Convert an AuthUser from the request to a GitUser for commit attribution. */
12
- function reqGitUser(req) {
13
- const user = req.user;
14
- if (!user)
15
- return undefined;
16
- return { id: String(user.id), name: user.displayName, email: user.email };
17
- }
18
- /** Check if a user can see a private repo (only the owner can). */
19
- function canSeePrivateRepo(repo, user) {
20
- if (!repo.private)
21
- return true;
22
- if (!user)
23
- return !repo.owner_id; // API key / open mode — only if no owner set
24
- if (!repo.owner_id)
25
- return false; // Private without owner — hidden from all users
26
- return user.id === repo.owner_id;
27
- }
28
- /** Get the list of repos the current user can access. */
29
- function getAccessibleRepos(req, workspaceManager, authService) {
30
- const allRepos = workspaceManager.getAllRepoConfigs();
31
- const user = req.user;
32
- // Filter out private repos owned by other users (applies to all roles)
33
- const visible = allRepos.filter(r => canSeePrivateRepo(r, user));
34
- if (!user)
35
- return visible; // API key / open mode → full access
36
- if (user.role === 'admin')
37
- return visible;
38
- const allowed = new Set(authService.getUserCollections(user.id));
39
- return visible.filter(r => r.private || allowed.has(r.name));
40
- }
41
- /** Check if the current user can access a specific repo. */
42
- function hasRepoAccess(req, repoName, workspaceManager, authService) {
43
- const user = req.user;
44
- const repoConfig = workspaceManager.getRepoConfig(repoName);
45
- if (repoConfig && !canSeePrivateRepo(repoConfig, user))
46
- return false;
47
- if (!user)
48
- return true; // API key / open mode
49
- if (repoConfig?.private && repoConfig?.owner_id === user.id)
50
- return true; // Owner of private repo
51
- if (user.role === 'admin')
52
- return true;
53
- return authService.getUserCollections(user.id).includes(repoName);
54
- }
55
- /** Check if the current user is an admin. */
56
- function isAdmin(req) {
57
- const user = req.user;
58
- if (!user)
59
- return true; // API key / open mode → admin-equivalent
60
- return user.role === 'admin';
61
- }
62
- /**
63
- * Build a set of all entity IDs visible to the current user.
64
- * Used to filter wikilinks so users don't see links to entities
65
- * in collections they can't access.
66
- */
67
- function getAccessibleEntityIds(req, workspaceManager, authService) {
68
- const user = req.user;
69
- if (!user)
70
- return null; // API key / open mode → no filtering
71
- if (user.role === 'admin') {
72
- // Admins see everything except other users' private repos
73
- const allRepos = workspaceManager.getAllRepoConfigs();
74
- const hasHiddenPrivate = allRepos.some(r => r.private && r.owner_id && r.owner_id !== user.id);
75
- if (!hasHiddenPrivate)
76
- return null; // No filtering needed
77
- }
78
- const repos = getAccessibleRepos(req, workspaceManager, authService);
79
- const ids = new Set();
80
- for (const repo of repos) {
81
- try {
82
- const graph = workspaceManager.getGraph(repo.name);
83
- for (const e of graph.search({})) {
84
- ids.add(e.id);
85
- }
86
- }
87
- catch { /* skip */ }
88
- }
89
- return ids;
90
- }
91
- /** Filter wikilinks to only those pointing to accessible entities. */
92
- function filterWikilinks(wikilinks, accessibleIds) {
93
- if (!accessibleIds)
94
- return wikilinks;
95
- return wikilinks.filter(id => accessibleIds.has(id));
96
- }
11
+ import { createSharedFolder, FolderCreationError } from '../../services/folder-creation.js';
12
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
13
+ import { dirname, join } from 'path';
14
+ import { RESERVED_ENTITY_TYPES } from '../../core/validation.js';
15
+ import { validateFolderPath } from '../../core/folder-paths.js';
16
+ import { StaleBaseRevisionError } from '../../core/graph.js';
17
+ import { checkEagerSkillPermission, filterWikilinks, getAccessibleEntityIds, getAccessibleRepos, getContentRepos, graphForAdmin as resolveGraphForAdmin, graphForRead as resolveGraphForRead, graphForWrite as resolveGraphForWrite, isFolderAdminOrOwner, reqGitUser, reqUser, } from './graph-api-access.js';
18
+ import { BridgeError, assertCanChangeUserHandle, validateUserHandleInPayload } from '../../services/user-person-bridge.js';
19
+ import { assertEntityRefs, EntityRefError, UnknownUserRefError } from '../../services/entity-mutations.js';
20
+ import { assetDeleteDenial } from '../../services/asset-delete-authz.js';
21
+ import { clearLegacyBucketCache } from '../../services/asset-listing.js';
22
+ import { log } from '../../utils/log.js';
97
23
  export async function registerGraphApiRoutes(fastify, workspaceManager, authService, wsHub, sessionManager) {
98
- // POST /api/repos create a new collection (admin only, or any user for private collections)
24
+ // ARCHITECTURAL BOUNDARY: every per-route graph resolution flows through
25
+ // this service so the act of getting a graph IS the access check. Do not
26
+ // call `workspaceManager.getGraph(repo)` from a request-scoped handler —
27
+ // use `accessService.graphFor*(reqUser(req), repo)` instead. A security
28
+ // audit (2026-05) flagged the previous opt-in pattern (route had to
29
+ // remember `hasRepoAccess()` before `getGraph()`) as an architectural
30
+ // risk; if you find yourself reaching for the raw WM here, that's the
31
+ // smell the audit was about.
32
+ const accessService = new WorkspaceAccessService(workspaceManager, authService);
33
+ const graphForRead = (req, reply, repoName) => resolveGraphForRead(accessService, req, reply, repoName);
34
+ const graphForWrite = (req, reply, repoName) => resolveGraphForWrite(accessService, req, reply, repoName);
35
+ const graphForAdmin = (req, reply, repoName) => resolveGraphForAdmin(accessService, req, reply, repoName);
36
+ // POST /api/repos — any authenticated user can create a folder.
37
+ // The creator is granted folder-admin access automatically.
38
+ // Shared logic lives in services/folder-creation.ts so the MCP tool
39
+ // create_repo behaves identically.
99
40
  fastify.post('/api/repos', async (req, reply) => {
100
41
  const user = req.user;
101
- const { name, display_name, description, private: isPrivate } = req.body || {};
102
- // Non-private collections require admin
103
- if (!isPrivate && !isAdmin(req)) {
104
- return reply.status(403).send({ error: 'Admin access required' });
105
- }
106
- if (!name || !/^[a-z0-9][a-z0-9-]*$/.test(name)) {
107
- return reply.status(400).send({ error: 'Collection name must be kebab-case' });
108
- }
42
+ const { name, display_name, description } = req.body || {};
109
43
  try {
110
- const workspace = new Workspace(workspaceManager['workspacePath']);
111
- const repo = await workspace.registerRepo({
112
- name,
113
- display_name: display_name || undefined,
114
- description,
115
- private: isPrivate,
116
- ...(isPrivate && user ? { owner_id: user.id } : {}),
117
- });
118
- workspaceManager.reloadConfig();
119
- wsHub?.broadcast({
120
- type: 'collection_created', repo: name,
121
- actor: req.user?.displayName ?? 'API',
122
- timestamp: new Date().toISOString(),
123
- });
44
+ const repo = await createSharedFolder(workspaceManager, authService, { name, display_name, description }, user, wsHub);
124
45
  return reply.status(201).send(repo);
125
46
  }
126
47
  catch (err) {
127
- return reply.status(409).send({ error: err.message });
48
+ if (err instanceof FolderCreationError) {
49
+ const status = err.code === 'invalid_name' ? 400
50
+ : err.code === 'duplicate' ? 409
51
+ : 500;
52
+ return reply.status(status).send({ error: err.message });
53
+ }
54
+ return reply.status(500).send({ error: err?.message ?? String(err) });
128
55
  }
129
56
  });
130
- // DELETE /api/repos/:repo — remove a collection and delete its files (admin only)
57
+ // DELETE /api/repos/:repo — folder admin or workspace owner only.
131
58
  fastify.delete('/api/repos/:repo', async (req, reply) => {
132
- if (!isAdmin(req)) {
133
- return reply.status(403).send({ error: 'Admin access required' });
59
+ if (!isFolderAdminOrOwner(req, req.params.repo, workspaceManager, authService)) {
60
+ return reply.status(403).send({ error: 'Folder admin access required' });
134
61
  }
135
62
  const { repo } = req.params;
136
63
  try {
@@ -138,14 +65,19 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
138
65
  const config = await workspace.loadConfig();
139
66
  const repoIndex = config.repos.findIndex(r => r.name === repo);
140
67
  if (repoIndex === -1) {
141
- return reply.status(404).send({ error: `Collection "${repo}" not found` });
68
+ return reply.status(404).send({ error: `Folder \u201C${repo}\u201D not found` });
142
69
  }
143
70
  const repoConfig = config.repos[repoIndex];
71
+ // System folders (the reserved Workspace Assets store) are an app
72
+ // invariant — undeletable, so nothing needs to recreate them on boot.
73
+ if (repoConfig.system) {
74
+ return reply.status(403).send({ error: 'This is a system folder and cannot be deleted.' });
75
+ }
144
76
  const repoPath = join(workspaceManager['workspacePath'], repoConfig.path);
145
77
  // Remove from config
146
78
  config.repos.splice(repoIndex, 1);
147
79
  workspace.writeConfig(config);
148
- workspace.pushWorkspaceConfig(`Remove collection: ${repo}`);
80
+ workspace.pushWorkspaceConfig(`Remove folder: ${repo}`);
149
81
  // Delete files
150
82
  const { rmSync } = await import('fs');
151
83
  if (existsSync(repoPath)) {
@@ -153,7 +85,7 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
153
85
  }
154
86
  workspaceManager.reloadConfig();
155
87
  wsHub?.broadcast({
156
- type: 'collection_deleted', repo,
88
+ type: 'folder_deleted', repo,
157
89
  actor: req.user?.displayName ?? 'API',
158
90
  timestamp: new Date().toISOString(),
159
91
  });
@@ -163,16 +95,16 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
163
95
  return reply.status(500).send({ error: err.message });
164
96
  }
165
97
  });
166
- // PATCH /api/repos/:repo — update a collection (rename, sync rules) (admin only)
98
+ // PATCH /api/repos/:repo — rename/update sync rules. Folder admin or workspace owner only.
167
99
  fastify.patch('/api/repos/:repo', async (req, reply) => {
168
- if (!isAdmin(req)) {
169
- return reply.status(403).send({ error: 'Admin access required' });
100
+ if (!isFolderAdminOrOwner(req, req.params.repo, workspaceManager, authService)) {
101
+ return reply.status(403).send({ error: 'Folder admin access required' });
170
102
  }
171
103
  const { repo } = req.params;
172
104
  const body = req.body || {};
173
105
  const { name: newName, display_name: newDisplayName, sync: newSync, ...rest } = body;
174
- if ('private' in rest) {
175
- return reply.status(400).send({ error: 'The private flag is immutable after creation' });
106
+ if ('personal' in rest) {
107
+ return reply.status(400).send({ error: 'The personal flag is immutable after creation' });
176
108
  }
177
109
  if (newName === undefined && newSync === undefined && newDisplayName === undefined) {
178
110
  return reply.status(400).send({ error: 'Nothing to update' });
@@ -185,12 +117,17 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
185
117
  const config = await workspace.loadConfig();
186
118
  const repoConfig = config.repos.find(r => r.name === repo);
187
119
  if (!repoConfig) {
188
- return reply.status(404).send({ error: `Collection "${repo}" not found` });
120
+ return reply.status(404).send({ error: `Folder \u201C${repo}\u201D not found` });
121
+ }
122
+ // System folders (the reserved Workspace Assets store) have a reserved
123
+ // name + fixed display; renaming/reconfiguring them is not allowed.
124
+ if (repoConfig.system) {
125
+ return reply.status(403).send({ error: 'This is a system folder and cannot be renamed or reconfigured.' });
189
126
  }
190
127
  // Handle rename
191
128
  if (newName !== undefined) {
192
129
  if (config.repos.some(r => r.name === newName)) {
193
- return reply.status(409).send({ error: `Collection "${newName}" already exists` });
130
+ return reply.status(409).send({ error: `Folder \u201C${newName}\u201D already exists` });
194
131
  }
195
132
  const wsPath = workspaceManager['workspacePath'];
196
133
  const oldPath = join(wsPath, repoConfig.path);
@@ -209,8 +146,8 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
209
146
  if (existsSync(markerPath)) {
210
147
  writeFS(markerPath, JSON.stringify({ repo_name: newName }, null, 2), 'utf-8');
211
148
  }
212
- // Update collection_access to follow the rename
213
- authService.renameCollection(repo, newName);
149
+ // Update folder_access to follow the rename
150
+ authService.renameFolder(repo, newName);
214
151
  }
215
152
  // Handle display name update (without rename)
216
153
  if (newDisplayName !== undefined && newName === undefined) {
@@ -221,7 +158,7 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
221
158
  repoConfig.sync = newSync.length > 0 ? newSync : undefined;
222
159
  }
223
160
  workspace.writeConfig(config);
224
- workspace.pushWorkspaceConfig(newName !== undefined ? `Rename collection: ${repo} → ${newName}` : `Update collection: ${repo}`);
161
+ workspace.pushWorkspaceConfig(newName !== undefined ? `Rename folder: ${repo} → ${newName}` : `Update folder: ${repo}`);
225
162
  workspaceManager.reloadConfig();
226
163
  return reply.send(repoConfig);
227
164
  }
@@ -229,50 +166,128 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
229
166
  return reply.status(500).send({ error: err.message });
230
167
  }
231
168
  });
232
- // GET /api/repos — list accessible repos (re-reads config to pick up CLI changes)
169
+ // GET /api/repos — list accessible repos (re-reads config to pick up CLI changes).
170
+ // Augments each repo with the caller's folder role ('admin' | 'editor'),
171
+ // member_count, entity_types (distinct types present), and folders (the
172
+ // flat list of nested-folder paths in the repo, used by the sidebar to
173
+ // render the active-path expansion).
233
174
  fastify.get('/api/repos', async (req, reply) => {
234
175
  workspaceManager.reloadConfig();
235
- const repos = getAccessibleRepos(req, workspaceManager, authService);
236
- return reply.send(repos);
176
+ // System folders (the reserved Workspace Assets store) are accessible but
177
+ // excluded from the sidebar — they're surfaced via the asset manager, not
178
+ // the folder list. They stay in getAccessibleRepos so authz + the asset
179
+ // manager (/api/workspace/assets) still see them.
180
+ const repos = getContentRepos(req, workspaceManager, authService);
181
+ const user = req.user;
182
+ const enriched = repos.map(r => {
183
+ const memberCount = authService.getFolderMembers(r.name).length;
184
+ const callerRole = user ? authService.getFolderRole(user.id, r.name) : null;
185
+ let entityTypes = [];
186
+ // `folders` carries the per-folder display_name override (when one
187
+ // exists in the folder's .studiograph-folder.json sidecar); the client
188
+ // falls back to toDisplayName(slug) when display_name is absent.
189
+ let folders = [];
190
+ try {
191
+ // Direct WM access OK here — `repos` came from getAccessibleRepos
192
+ // above, so every iteration is over a folder the caller can read.
193
+ const graph = workspaceManager.getGraph(r.name);
194
+ entityTypes = graph.listEntityTypes();
195
+ folders = graph.listFolders().map(f => {
196
+ const entry = { path: f.path };
197
+ if (f.display_name)
198
+ entry.display_name = f.display_name;
199
+ if (f.id)
200
+ entry.id = f.id;
201
+ return entry;
202
+ });
203
+ }
204
+ catch { /* repo not mounted */ }
205
+ return {
206
+ ...r,
207
+ caller_role: callerRole,
208
+ member_count: memberCount,
209
+ entity_types: entityTypes,
210
+ folders,
211
+ };
212
+ });
213
+ return reply.send(enriched);
237
214
  });
215
+ // GET /api/admin/folders — workspace-owner-only metadata list of every folder
216
+ // (name, display_name, creator, member count). No content access implied —
217
+ // owners still need to join a folder to read its entries.
218
+ fastify.get('/api/admin/folders', async (req, reply) => {
219
+ const user = req.user;
220
+ if (user && !isWorkspaceOwner(user.role)) {
221
+ return reply.status(403).send({ error: 'Workspace owner access required' });
222
+ }
223
+ workspaceManager.reloadConfig();
224
+ const all = workspaceManager.getAllRepoConfigs();
225
+ const result = all.map(r => {
226
+ const members = authService.getFolderMembers(r.name);
227
+ const admins = members.filter(m => m.role === 'admin');
228
+ const creator = r.owner_id
229
+ ? members.find(m => m.user.id === r.owner_id)?.user
230
+ : admins[0]?.user;
231
+ let entryCount = 0;
232
+ // Direct WM access OK — this endpoint is owner-only (gated above) and
233
+ // returns metadata for every folder the workspace owner needs to see
234
+ // for admin purposes. No content is leaked: only entry counts.
235
+ try {
236
+ entryCount = workspaceManager.getGraph(r.name).search({}).length;
237
+ }
238
+ catch { /* skip */ }
239
+ return {
240
+ name: r.name,
241
+ display_name: r.display_name ?? r.name,
242
+ description: r.description,
243
+ member_count: members.length,
244
+ admin_count: admins.length,
245
+ entry_count: entryCount,
246
+ creator: creator ? { id: creator.id, displayName: creator.displayName, email: creator.email } : null,
247
+ caller_is_member: user ? members.some(m => m.user.id === user.id) : true,
248
+ };
249
+ });
250
+ return reply.send(result);
251
+ });
252
+ // Build the lightweight summary objects used by /entities?summary=true.
253
+ // Extracted so the per-repo and bulk endpoints share the same shape.
254
+ const EXCLUDED_SUMMARY_FIELDS = new Set([
255
+ 'entity_type', 'entity_id', 'created_by', 'updated_by',
256
+ 'description', 'content', 'context', 'rationale', 'alternatives', 'consequences',
257
+ 'steps', 'guidelines', 'bio', 'agenda', 'summary', 'decision', 'action_items',
258
+ ]);
259
+ function buildSummaries(entities, accessibleIds) {
260
+ return entities.map(e => {
261
+ const extra = Object.fromEntries(Object.entries(e.data ?? {}).filter(([k, v]) => !EXCLUDED_SUMMARY_FIELDS.has(k) && k !== 'name' && k !== 'created_at' && k !== 'updated_at' &&
262
+ (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || Array.isArray(v) || v instanceof Date)));
263
+ return {
264
+ id: e.id,
265
+ entityType: e.entityType,
266
+ folder_path: e.folder_path ?? '',
267
+ data: {
268
+ name: e.data.name,
269
+ created_at: e.data.created_at,
270
+ updated_at: e.data.updated_at,
271
+ ...extra,
272
+ },
273
+ document: { wikilinks: filterWikilinks(e.document?.wikilinks ?? [], accessibleIds) },
274
+ };
275
+ });
276
+ }
238
277
  // GET /api/repos/:repo/entities — list all entities in a repo
239
278
  // ?summary=true returns lightweight objects (no content, minimal data) for sidebar/graph
240
279
  fastify.get('/api/repos/:repo/entities', async (req, reply) => {
241
280
  const { repo } = req.params;
242
- if (!hasRepoAccess(req, repo, workspaceManager, authService)) {
243
- return reply.status(404).send({ error: `Collection "${repo}" not found` });
244
- }
245
- let graph;
246
- try {
247
- graph = workspaceManager.getGraph(repo);
248
- }
249
- catch (err) {
250
- return reply.status(404).send({ error: err.message });
251
- }
281
+ // Access enforced by graphForRead \u2014 do not call workspaceManager.getGraph
282
+ // directly here. The audit (2026-05) flagged direct WM access from routes
283
+ // as the primary architectural risk; the service is the gate.
284
+ const graph = graphForRead(req, reply, repo);
285
+ if (!graph)
286
+ return;
252
287
  const entities = graph.search({});
253
288
  const accessibleIds = getAccessibleEntityIds(req, workspaceManager, authService);
254
289
  if (req.query.summary === 'true') {
255
- const EXCLUDED_SUMMARY_FIELDS = new Set([
256
- 'entity_type', 'entity_id', 'created_by', 'updated_by',
257
- 'description', 'content', 'context', 'rationale', 'alternatives', 'consequences',
258
- 'steps', 'guidelines', 'bio', 'agenda', 'summary', 'decision', 'action_items',
259
- ]);
260
- const summaries = entities.map(e => {
261
- const extra = Object.fromEntries(Object.entries(e.data ?? {}).filter(([k, v]) => !EXCLUDED_SUMMARY_FIELDS.has(k) && k !== 'name' && k !== 'created_at' && k !== 'updated_at' &&
262
- (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || Array.isArray(v))));
263
- return {
264
- id: e.id,
265
- entityType: e.entityType,
266
- data: {
267
- name: e.data.name,
268
- created_at: e.data.created_at,
269
- updated_at: e.data.updated_at,
270
- ...extra,
271
- },
272
- document: { wikilinks: filterWikilinks(e.document?.wikilinks ?? [], accessibleIds) },
273
- };
274
- });
275
- return reply.send(summaries);
290
+ return reply.send(buildSummaries(entities, accessibleIds));
276
291
  }
277
292
  // Full response — filter wikilinks in document
278
293
  const filtered = entities.map(e => {
@@ -282,19 +297,57 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
282
297
  });
283
298
  return reply.send(filtered);
284
299
  });
300
+ // GET /api/workspace/entities?summary=true — bulk entity summaries for ALL
301
+ // accessible repos in a single round-trip. Replaces 15+ parallel
302
+ // /api/repos/:repo/entities?summary=true calls. Important on dev where each
303
+ // request has Vite middleware overhead and the browser's HTTP/1.1 6-slot
304
+ // limit serializes them, blocking other navigation fetches.
305
+ // Response shape: `{ [repoName]: EntitySummary[] }`.
306
+ fastify.get('/api/workspace/entities', async (req, reply) => {
307
+ // Mirror the per-repo endpoint's contract: only summary=true is supported
308
+ // here. Returning full bodies for the whole workspace would be huge and
309
+ // isn't a use case we have today.
310
+ if (req.query.summary !== 'true') {
311
+ return reply.status(400).send({ error: 'summary=true is required for the bulk entities endpoint' });
312
+ }
313
+ const repos = getContentRepos(req, workspaceManager, authService);
314
+ // Single-pass: collect the per-repo entity arrays AND the accessible-ids
315
+ // set in one walk. The previous shape called graph.search({}) twice per
316
+ // repo (once via getAccessibleEntityIds, once in this loop), doubling
317
+ // disk I/O on the most-trafficked endpoint.
318
+ const user = reqUser(req);
319
+ const collectIds = user !== undefined; // unauthenticated → no wikilink filtering
320
+ const accessibleIds = collectIds ? new Set() : null;
321
+ const perRepoEntities = [];
322
+ for (const r of repos) {
323
+ let graph;
324
+ // Direct WM access OK — `repos` is the access-filtered list above.
325
+ try {
326
+ graph = workspaceManager.getGraph(r.name);
327
+ }
328
+ catch {
329
+ continue;
330
+ } // skip unmounted repos rather than failing the whole request
331
+ const entities = graph.search({});
332
+ perRepoEntities.push({ name: r.name, entities });
333
+ if (accessibleIds) {
334
+ for (const e of entities)
335
+ accessibleIds.add(e.id);
336
+ }
337
+ }
338
+ const result = {};
339
+ for (const { name, entities } of perRepoEntities) {
340
+ result[name] = buildSummaries(entities, accessibleIds);
341
+ }
342
+ return reply.send(result);
343
+ });
285
344
  // GET /api/repos/:repo/entities/:type — list entities of a type
286
345
  fastify.get('/api/repos/:repo/entities/:type', async (req, reply) => {
287
346
  const { repo, type } = req.params;
288
- if (!hasRepoAccess(req, repo, workspaceManager, authService)) {
289
- return reply.status(404).send({ error: `Collection "${repo}" not found` });
290
- }
291
- let graph;
292
- try {
293
- graph = workspaceManager.getGraph(repo);
294
- }
295
- catch (err) {
296
- return reply.status(404).send({ error: err.message });
297
- }
347
+ // Access enforced by graphForRead (WorkspaceAccessService).
348
+ const graph = graphForRead(req, reply, repo);
349
+ if (!graph)
350
+ return;
298
351
  const entities = graph.search({ entityType: type });
299
352
  const accessibleIds = getAccessibleEntityIds(req, workspaceManager, authService);
300
353
  const filtered = entities.map(e => {
@@ -307,17 +360,20 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
307
360
  // GET /api/repos/:repo/entities/:type/:id — get single entity
308
361
  fastify.get('/api/repos/:repo/entities/:type/:id', async (req, reply) => {
309
362
  const { repo, type, id } = req.params;
310
- if (!hasRepoAccess(req, repo, workspaceManager, authService)) {
311
- return reply.status(404).send({ error: `Collection "${repo}" not found` });
312
- }
313
- let graph;
363
+ // Access enforced by graphForRead (WorkspaceAccessService).
364
+ const graph = graphForRead(req, reply, repo);
365
+ if (!graph)
366
+ return;
367
+ let entity;
314
368
  try {
315
- graph = workspaceManager.getGraph(repo);
369
+ entity = graph.get(type, id);
316
370
  }
317
371
  catch (err) {
318
- return reply.status(404).send({ error: err.message });
372
+ return reply.status(404).send({ error: err.message ?? `Entity not found: ${type}/${id}` });
319
373
  }
320
- const entity = graph.get(type, id);
374
+ // graph.get may also return null for a missing entity without throwing
375
+ // (depends on the backing graph implementation). Treat that as 404 so
376
+ // we don't blow up on the wikilinks access below.
321
377
  if (!entity) {
322
378
  return reply.status(404).send({ error: `Entity not found: ${type}/${id}` });
323
379
  }
@@ -330,20 +386,144 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
330
386
  }
331
387
  return reply.send(entity);
332
388
  });
333
- // GET /api/repos/:repo/search?q=&type=&tags=search within a repo
334
- fastify.get('/api/repos/:repo/search', async (req, reply) => {
335
- const { repo } = req.params;
336
- if (!hasRepoAccess(req, repo, workspaceManager, authService)) {
337
- return reply.status(404).send({ error: `Collection "${repo}" not found` });
338
- }
339
- const { q, type, tags } = req.query;
340
- let graph;
389
+ // GET /api/repos/:repo/entities/:type/:id/historyentity commit history
390
+ fastify.get('/api/repos/:repo/entities/:type/:id/history', async (req, reply) => {
391
+ const { repo, type, id } = req.params;
392
+ // Access enforced by graphForRead (WorkspaceAccessService).
393
+ const graph = graphForRead(req, reply, repo);
394
+ if (!graph)
395
+ return;
396
+ const gitService = graph.getGitService();
397
+ if (!gitService) {
398
+ return reply.send([]);
399
+ }
400
+ const limit = Math.min(parseInt(req.query.limit ?? '20', 10) || 20, 50);
401
+ // Resolve the actual on-disk path via graph.get() — it walks nested
402
+ // folders to find the file. The previous hardcoded `${plural}/${id}.md`
403
+ // missed any entry not at the legacy flat path (entries created in a
404
+ // sub-folder, MCP entries that landed elsewhere, etc.) and silently
405
+ // returned an empty history. Convert absolute path → repo-relative
406
+ // for git log.
407
+ let filePath;
341
408
  try {
342
- graph = workspaceManager.getGraph(repo);
409
+ const entity = graph.get(type, id);
410
+ const repoRoot = graph.getRepoPath();
411
+ filePath = entity.path.startsWith(repoRoot + '/')
412
+ ? entity.path.slice(repoRoot.length + 1)
413
+ : entity.path;
343
414
  }
344
415
  catch (err) {
345
416
  return reply.status(404).send({ error: err.message });
346
417
  }
418
+ try {
419
+ const history = gitService.getFileHistory(filePath, limit);
420
+ // Opt-in per-commit change summary for the History panel. Off by
421
+ // default so the eager gating fetch on every entity-page load stays
422
+ // cheap; the panel requests it only when expanded.
423
+ if (req.query.changes === 'true') {
424
+ return reply.send(history.map((c) => ({ ...c, changes: graph.getCommitChange(filePath, c.hash) })));
425
+ }
426
+ return reply.send(history);
427
+ }
428
+ catch {
429
+ return reply.send([]);
430
+ }
431
+ });
432
+ // POST /api/repos/:repo/entities/:type/:id/restore — restore an entry to
433
+ // its content at a past commit. Single-entity scope. Editor-or-admin role
434
+ // required (canWriteRepo). Returns 200 with the restore result, 404 if
435
+ // the entry doesn't exist at that commit, 409 PATH_CONFLICT if a different
436
+ // entry now occupies the target path, 403 if the caller lacks write access.
437
+ fastify.post('/api/repos/:repo/entities/:type/:id/restore', async (req, reply) => {
438
+ const { repo, type, id } = req.params;
439
+ const { commitHash } = req.body ?? {};
440
+ if (!commitHash || typeof commitHash !== 'string') {
441
+ return reply.status(400).send({ error: 'commitHash is required' });
442
+ }
443
+ // Access enforced by graphForWrite: 404 if no read access, 403 if no write.
444
+ const graph = graphForWrite(req, reply, repo);
445
+ if (!graph)
446
+ return;
447
+ // Flush any deferred autosave for this repo first — otherwise a restore
448
+ // could overwrite a pending (uncommitted) edit on disk and commit only
449
+ // the restored version, silently discarding the in-flight work. Matches
450
+ // rename/move/change-type, which all flush before mutating.
451
+ if (sessionManager)
452
+ await sessionManager.flush(repo);
453
+ try {
454
+ // restoreFile reconciles any live editor Y.Doc against the restored disk
455
+ // content internally (via the BodyWriteCoordinator's per-doc lock), so an
456
+ // open editor shows the restored version instead of syncing stale content
457
+ // back over it. That lives in graph.restoreFile so every restore caller
458
+ // (this route, the agent tool, the MCP tool) gets it.
459
+ const result = await graph.restoreFile(type, id, commitHash, reqGitUser(req));
460
+ wsHub?.broadcast({
461
+ type: 'entity_change',
462
+ action: result.action === 'recreate' ? 'created' : 'updated',
463
+ repo,
464
+ entityType: type,
465
+ entityId: id,
466
+ actor: reqUser(req)?.displayName ?? 'API',
467
+ source: 'api',
468
+ timestamp: new Date().toISOString(),
469
+ });
470
+ return reply.send({
471
+ success: true,
472
+ action: result.action,
473
+ path: result.path,
474
+ });
475
+ }
476
+ catch (err) {
477
+ // PathConflictError carries an error code distinct from generic
478
+ // failures so the UI can render an actionable conflict message.
479
+ if (err?.code === 'PATH_CONFLICT' && err?.occupiedBy) {
480
+ return reply.status(409).send({
481
+ error: 'PATH_CONFLICT',
482
+ path: err.path,
483
+ occupiedBy: err.occupiedBy,
484
+ });
485
+ }
486
+ const message = err instanceof Error ? err.message : 'Restore failed';
487
+ // "not found at commit" / "did not exist at commit" → 404 so the
488
+ // client can show "that version isn't available" instead of a
489
+ // generic 500.
490
+ if (/not found at commit|did not exist at commit/i.test(message)) {
491
+ return reply.status(404).send({ error: message });
492
+ }
493
+ // Type-mismatch refusal (restoring would silently change entity_type)
494
+ // is a client-actionable conflict, not a server fault → 409.
495
+ if (/would change its type|was type '/i.test(message)) {
496
+ return reply.status(409).send({ error: message });
497
+ }
498
+ return reply.status(500).send({ error: message });
499
+ }
500
+ });
501
+ // GET /api/schema/:entityType/fields — field metadata for an entity type
502
+ fastify.get('/api/schema/:entityType/fields', async (req, reply) => {
503
+ const { entityType } = req.params;
504
+ const registry = workspaceManager.getSchemaRegistry();
505
+ const fields = registry.getFieldMetadata(entityType);
506
+ return reply.send(fields);
507
+ });
508
+ // GET /api/formats — the format registry, serialized for the client.
509
+ //
510
+ // Single source of truth for ingestion: the web client builds its accept
511
+ // lists, disposition matrix, and supported-types copy from this instead of
512
+ // maintaining parallel (and drifting) constants. Mirrors the
513
+ // GET /api/schema/:type/fields pattern. Workspace-invariant + auth-gated
514
+ // by the global middleware; no per-repo access needed.
515
+ fastify.get('/api/formats', async (_req, reply) => {
516
+ const { serializeForClient } = await import('../../core/format-registry.js');
517
+ return reply.send(serializeForClient());
518
+ });
519
+ // GET /api/repos/:repo/search?q=&type=&tags= — search within a repo
520
+ fastify.get('/api/repos/:repo/search', async (req, reply) => {
521
+ const { repo } = req.params;
522
+ // Access enforced by graphForRead (WorkspaceAccessService).
523
+ const graph = graphForRead(req, reply, repo);
524
+ if (!graph)
525
+ return;
526
+ const { q, type, tags } = req.query;
347
527
  const results = graph.search({
348
528
  query: q,
349
529
  entityType: type,
@@ -359,19 +539,18 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
359
539
  });
360
540
  // GET /api/graph — full workspace graph topology (nodes + edges)
361
541
  fastify.get('/api/graph', async (req, reply) => {
362
- const repos = getAccessibleRepos(req, workspaceManager, authService);
542
+ const repos = getContentRepos(req, workspaceManager, authService);
363
543
  const nodes = [];
364
544
  const edgeSet = new Set();
365
545
  const edges = [];
366
546
  const entityIds = new Set();
367
547
  for (const repoConfig of repos) {
368
548
  try {
549
+ // Direct WM access OK — `repos` came from getAccessibleRepos above.
369
550
  const graph = workspaceManager.getGraph(repoConfig.name);
370
551
  const entities = graph.search({});
371
552
  for (const entity of entities) {
372
553
  const nodeId = entity.id;
373
- if (nodeId.toLowerCase().startsWith('readme'))
374
- continue;
375
554
  if (!entityIds.has(nodeId)) {
376
555
  entityIds.add(nodeId);
377
556
  nodes.push({
@@ -399,10 +578,10 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
399
578
  const validEdges = edges.filter(e => entityIds.has(e.source) && entityIds.has(e.target));
400
579
  return reply.send({ nodes, edges: validEdges });
401
580
  });
402
- // GET /api/search?q=&type=&tags=&repos= — cross-repo search
581
+ // GET /api/search?q=&type=&tags=&repos=&mode= — cross-repo search
403
582
  fastify.get('/api/search', async (req, reply) => {
404
- const { q, type, tags, repos } = req.query;
405
- const accessibleNames = new Set(getAccessibleRepos(req, workspaceManager, authService).map(r => r.name));
583
+ const { q, type, tags, repos, mode } = req.query;
584
+ const accessibleNames = new Set(getContentRepos(req, workspaceManager, authService).map(r => r.name));
406
585
  // Filter requested repos to only accessible ones
407
586
  let repoNames = repos ? repos.split(',').map(r => r.trim()) : undefined;
408
587
  if (repoNames) {
@@ -411,12 +590,35 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
411
590
  else {
412
591
  repoNames = [...accessibleNames];
413
592
  }
593
+ const keywordMode = mode === 'keyword';
414
594
  const results = await workspaceManager.search({
415
595
  query: q,
416
596
  entityType: type,
417
597
  tags: tags ? tags.split(',').map(t => t.trim()) : undefined,
418
598
  repoNames,
599
+ mode: keywordMode ? 'keyword' : undefined,
600
+ // Autosuggest dropdown caps at 20 visible rows; capping server-side
601
+ // avoids serializing a long tail the client throws away.
602
+ limit: keywordMode ? 20 : undefined,
419
603
  });
604
+ // Autosuggest doesn't need the markdown body or wikilinks — the
605
+ // dropdown renders {name, type, repo} only. Returning a slim shape
606
+ // cuts response size from ~hundreds of KB to a few KB on big
607
+ // workspaces, and skips the per-entity wikilink visibility filter.
608
+ if (keywordMode) {
609
+ const slim = results.map(e => ({
610
+ id: e.id,
611
+ path: e.path,
612
+ folder_path: e.folder_path,
613
+ entityType: e.entityType,
614
+ repoName: e.repoName,
615
+ data: {
616
+ name: e.data?.name,
617
+ updated_at: e.data?.updated_at,
618
+ },
619
+ }));
620
+ return reply.send(slim);
621
+ }
420
622
  const accessibleIds = getAccessibleEntityIds(req, workspaceManager, authService);
421
623
  const filtered = results.map((e) => {
422
624
  if (!e.document?.wikilinks?.length)
@@ -428,20 +630,29 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
428
630
  // PUT /api/repos/:repo/entities/:type/:id — update entity (data and/or content)
429
631
  fastify.put('/api/repos/:repo/entities/:type/:id', async (req, reply) => {
430
632
  const { repo, type, id } = req.params;
431
- if (!hasRepoAccess(req, repo, workspaceManager, authService)) {
432
- return reply.status(404).send({ error: `Collection "${repo}" not found` });
433
- }
434
- let graph;
633
+ // Access enforced by graphForWrite \u2014 PUT updates an entity, write required.
634
+ const graph = graphForWrite(req, reply, repo);
635
+ if (!graph)
636
+ return;
637
+ const eagerError = checkEagerSkillPermission(req, type, req.body.data, repo, workspaceManager, authService);
638
+ if (eagerError)
639
+ return reply.status(403).send({ error: eagerError });
435
640
  try {
436
- graph = workspaceManager.getGraph(repo);
437
- }
438
- catch (err) {
439
- return reply.status(404).send({ error: err.message });
440
- }
441
- try {
442
- // Optimistic concurrency: reject if entity was modified after client last fetched it
641
+ // Optimistic concurrency. Two preconditions:
642
+ // - `X-Studiograph-Base-Body-Hash` (Phase 3a): sha256 hex of the
643
+ // body the client last loaded. Enforced INSIDE the per-doc lock
644
+ // by graph.update via `baseRevision`, so it composes with
645
+ // serialization — two body-writing PUTs from the same baseline
646
+ // can't both pass and back-stomp each other.
647
+ // - `If-Unmodified-Since` (legacy): kept for non-web clients;
648
+ // RFC-7231 second-precision so aliases sub-millisecond writes,
649
+ // which is why the body-hash form is preferred.
650
+ // Body-hash header is IGNORED on metadata-only PUTs — a stale body
651
+ // baseline is irrelevant when the PUT doesn't touch the body, and
652
+ // enforcing it would cause spurious 412s after an agent body-write.
653
+ const baseBodyHash = req.headers['x-studiograph-base-body-hash'];
443
654
  const ifUnmodifiedSince = req.headers['if-unmodified-since'];
444
- if (ifUnmodifiedSince) {
655
+ if (ifUnmodifiedSince && !baseBodyHash) {
445
656
  const current = graph.get(type, id);
446
657
  const entityUpdatedAt = current?.data?.updated_at;
447
658
  if (entityUpdatedAt && new Date(entityUpdatedAt) > new Date(ifUnmodifiedSince)) {
@@ -449,6 +660,40 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
449
660
  }
450
661
  }
451
662
  const gitUser = reqGitUser(req);
663
+ // Workspace-wide uniqueness check for `user_handle` on person entities.
664
+ // Skip the entity being updated so we don't reject it for clashing
665
+ // with itself. Also gate WHO can change the bridge: only workspace
666
+ // owners may stamp/clear/rewrite user_handle. Other folder-writers
667
+ // see the field but can't edit it (defense against impersonation
668
+ // through the agent's Current User prompt).
669
+ if (type === 'person' && req.body.data) {
670
+ try {
671
+ validateUserHandleInPayload(workspaceManager, req.body.data, { repo, entityId: id });
672
+ if (Object.prototype.hasOwnProperty.call(req.body.data, 'user_handle')) {
673
+ const current = graph.get('person', id);
674
+ const currentHandle = (current?.data?.user_handle ?? '');
675
+ assertCanChangeUserHandle(reqUser(req), currentHandle, req.body.data.user_handle);
676
+ }
677
+ }
678
+ catch (err) {
679
+ if (err instanceof BridgeError) {
680
+ const status = err.code === 'OWNER_REQUIRED' ? 403 : 409;
681
+ return reply.status(status).send({ error: err.message, code: err.code, ...err.details });
682
+ }
683
+ throw err;
684
+ }
685
+ }
686
+ // Scope ref fields (today: task.assignee → person). Rejects a wikilink
687
+ // that resolves to a non-person; forward references are allowed.
688
+ try {
689
+ assertEntityRefs(workspaceManager, type, req.body.data, { userExists: (id) => !!authService.findUserById(Number(id)) });
690
+ }
691
+ catch (err) {
692
+ if (err instanceof EntityRefError || err instanceof UnknownUserRefError) {
693
+ return reply.status(400).send({ error: err.message, code: err.code, field: err.field });
694
+ }
695
+ throw err;
696
+ }
452
697
  const updated = await graph.update({
453
698
  entityType: type,
454
699
  entityId: id,
@@ -457,6 +702,12 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
457
702
  commitMessage: `Update ${type}/${id} from web UI`,
458
703
  user: gitUser,
459
704
  skipCommit: !!sessionManager, // defer commit if scheduler is active
705
+ source: 'autosave',
706
+ // Forward the body-hash precondition into graph.update so the
707
+ // check runs INSIDE the per-doc lock — see Phase 3 P1 fix.
708
+ // Only meaningful for body writes; ignored when content is
709
+ // undefined (graph.update gates on content !== undefined too).
710
+ baseRevision: req.body.content !== undefined ? baseBodyHash : undefined,
460
711
  });
461
712
  if (sessionManager && gitUser) {
462
713
  sessionManager.markDirty(repo, gitUser, type, id, updated.path);
@@ -469,6 +720,19 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
469
720
  return reply.send(updated);
470
721
  }
471
722
  catch (err) {
723
+ // Translate the locked-section baseRevision mismatch into the
724
+ // same 412 shape the route used to emit pre-lock. Preserves the
725
+ // client-side contract (code: STALE_BODY_HASH + currentBodyHash
726
+ // + entity).
727
+ if (err instanceof StaleBaseRevisionError) {
728
+ const current = graph.get(type, id);
729
+ return reply.status(412).send({
730
+ error: 'Precondition Failed: body was modified',
731
+ code: 'STALE_BODY_HASH',
732
+ currentBodyHash: err.actual,
733
+ entity: current,
734
+ });
735
+ }
472
736
  return reply.status(400).send({ error: err.message });
473
737
  }
474
738
  });
@@ -479,16 +743,10 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
479
743
  if (!type || !id || !newType) {
480
744
  return reply.status(400).send({ error: 'entityType, entityId, and newType are required' });
481
745
  }
482
- if (!hasRepoAccess(req, repo, workspaceManager, authService)) {
483
- return reply.status(404).send({ error: `Collection "${repo}" not found` });
484
- }
485
- let graph;
486
- try {
487
- graph = workspaceManager.getGraph(repo);
488
- }
489
- catch (err) {
490
- return reply.status(404).send({ error: err.message });
491
- }
746
+ // Access enforced by graphForRead (WorkspaceAccessService).
747
+ const graph = graphForRead(req, reply, repo);
748
+ if (!graph)
749
+ return;
492
750
  try {
493
751
  // Flush any pending session saves before moving the file
494
752
  if (sessionManager) {
@@ -513,45 +771,121 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
513
771
  return reply.status(status).send({ error: err.message });
514
772
  }
515
773
  });
516
- // POST /api/repos/:repo/transfermove or copy entity to another collection
517
- fastify.post('/api/repos/:repo/transfer', async (req, reply) => {
518
- const sourceRepo = req.params.repo;
519
- const { entityType, entityId, targetRepo, mode } = req.body ?? {};
520
- if (!entityType || !entityId || !targetRepo || !mode) {
521
- return reply.status(400).send({ error: 'entityType, entityId, targetRepo, and mode are required' });
774
+ // POST /api/repos/:repo/renamerename an entity (changes its id/filename)
775
+ fastify.post('/api/repos/:repo/rename', async (req, reply) => {
776
+ const { repo } = req.params;
777
+ const { entityType: type, entityId: id, newId } = req.body ?? {};
778
+ if (!type || !id || !newId) {
779
+ return reply.status(400).send({ error: 'entityType, entityId, and newId are required' });
522
780
  }
523
- if (sourceRepo === targetRepo) {
524
- return reply.status(400).send({ error: 'Source and target collections must be different' });
781
+ if (!/^[a-z0-9][a-z0-9_-]*$/.test(newId)) {
782
+ return reply.status(400).send({ error: 'New ID must be kebab-case (lowercase letters, numbers, hyphens, underscores)' });
525
783
  }
526
- if (!hasRepoAccess(req, sourceRepo, workspaceManager, authService)) {
527
- return reply.status(404).send({ error: `Collection "${sourceRepo}" not found` });
784
+ if (id === newId) {
785
+ return reply.status(400).send({ error: 'New ID must differ from the current ID' });
528
786
  }
529
- if (!hasRepoAccess(req, targetRepo, workspaceManager, authService)) {
530
- return reply.status(404).send({ error: `Collection "${targetRepo}" not found` });
787
+ // Access enforced by graphForWrite (WorkspaceAccessService).
788
+ const graph = graphForWrite(req, reply, repo);
789
+ if (!graph)
790
+ return;
791
+ // Workspace-wide uniqueness — ids must be unique across all folders
792
+ const conflict = workspaceManager.findEntityById(newId);
793
+ if (conflict) {
794
+ return reply.status(409).send({ error: `An entry with id "${newId}" already exists in folder "${conflict.repoName}".` });
531
795
  }
532
- let sourceGraph, targetGraph;
533
796
  try {
534
- sourceGraph = workspaceManager.getGraph(sourceRepo);
535
- targetGraph = workspaceManager.getGraph(targetRepo);
797
+ // Flush pending session saves so we don't PUT to the old path after rename
798
+ if (sessionManager) {
799
+ await sessionManager.flush(repo);
800
+ }
801
+ const gitUser = reqGitUser(req);
802
+ const renamed = await graph.rename(type, id, newId, undefined, gitUser);
803
+ // Rewrite any [[wikilinks]] that still point at the old id, across
804
+ // every folder the caller can access. Per-repo — cross-folder links
805
+ // are rare but supported.
806
+ const refactorMsg = `Refactor wikilinks: ${id} \u2192 ${newId}`;
807
+ for (const other of getAccessibleRepos(req, workspaceManager, authService)) {
808
+ try {
809
+ // Direct WM access OK — iterating the access-filtered list.
810
+ const otherGraph = workspaceManager.getGraph(other.name);
811
+ await otherGraph.refactorWikilink(id, newId, refactorMsg);
812
+ }
813
+ catch { /* skip unloadable repo */ }
814
+ }
815
+ wsHub?.broadcast({
816
+ type: 'entity_change', action: 'deleted', repo, entityType: type, entityId: id,
817
+ actor: req.user?.displayName ?? 'API',
818
+ source: 'api', timestamp: new Date().toISOString(),
819
+ });
820
+ wsHub?.broadcast({
821
+ type: 'entity_change', action: 'created', repo, entityType: type, entityId: newId,
822
+ actor: req.user?.displayName ?? 'API',
823
+ source: 'api', timestamp: new Date().toISOString(),
824
+ });
825
+ return reply.send({ success: true, entity: renamed, newUrl: `/${repo}/${type}/${newId}` });
536
826
  }
537
827
  catch (err) {
538
- return reply.status(404).send({ error: err.message });
828
+ const status = err.message?.includes('already exists') ? 409 : 400;
829
+ return reply.status(status).send({ error: err.message });
539
830
  }
831
+ });
832
+ // POST /api/repos/:repo/transfer — move or copy entity to another folder
833
+ fastify.post('/api/repos/:repo/transfer', async (req, reply) => {
834
+ const sourceRepo = req.params.repo;
835
+ const { entityType, entityId, targetRepo, mode, targetFolderPath } = req.body ?? {};
836
+ if (!entityType || !entityId || !targetRepo || !mode) {
837
+ return reply.status(400).send({ error: 'entityType, entityId, targetRepo, and mode are required' });
838
+ }
839
+ if (sourceRepo === targetRepo) {
840
+ return reply.status(400).send({ error: 'Source and target folders must be different' });
841
+ }
842
+ // Workspace Assets store ownership gate (codex P1 / D2). Moving an `asset`
843
+ // OUT of a workspace_wide store reassigns its index repo, after which the
844
+ // uploader/owner delete rule no longer applies — so the move itself must
845
+ // honor that rule. assetDeleteDenial is a no-op for non-workspace_wide
846
+ // sources, so normal folder→folder moves are unaffected.
847
+ if (entityType === 'asset') {
848
+ const denial = assetDeleteDenial(workspaceManager.getAssetIndex?.(), workspaceManager.getRepoConfig(sourceRepo), reqUser(req), sourceRepo, entityType, entityId);
849
+ if (denial)
850
+ return reply.status(403).send({ error: denial });
851
+ }
852
+ // Access enforced by graphForWrite — both source (delete on move)
853
+ // and target (create) require write access. Resolved up-front so a
854
+ // target-side denial never surfaces only after source has been
855
+ // mutated. Audit (2026-05) flagged the previous opt-in pattern.
856
+ const sourceGraph = graphForWrite(req, reply, sourceRepo);
857
+ if (!sourceGraph)
858
+ return;
859
+ const targetGraph = graphForWrite(req, reply, targetRepo);
860
+ if (!targetGraph)
861
+ return;
540
862
  try {
541
- // Flush pending saves
542
- if (sessionManager)
863
+ // Phase 0 — prevalidate. Audit (2026-05) required this: previously
864
+ // the route created in target, broadcast `created`, and only then
865
+ // tried to delete from source. A delete failure left a duplicate
866
+ // entry the client had already been told about.
867
+ if (sessionManager) {
543
868
  await sessionManager.flush(sourceRepo);
544
- // Read source entity
869
+ if (targetRepo !== sourceRepo)
870
+ await sessionManager.flush(targetRepo);
871
+ }
872
+ // Source must exist; target must not collide. Both checks run BEFORE
873
+ // any mutation so we never enter phase 2 unless phase 1 will succeed.
545
874
  const source = sourceGraph.get(entityType, entityId);
546
- // Check for conflicts in target
875
+ const sourcePath = source.path;
876
+ const sourceRawMarkdown = readFileSync(sourcePath, 'utf-8');
547
877
  try {
548
878
  targetGraph.get(entityType, entityId);
549
879
  return reply.status(409).send({ error: `"${entityId}" already exists in ${targetRepo}` });
550
880
  }
551
881
  catch { /* not found = good */ }
552
- // Create in target
553
882
  const gitUser = reqGitUser(req);
554
883
  const { entity_type, entity_id, ...dataFields } = source.data;
884
+ // Phase 1 — create in target. No broadcast yet: a half-completed
885
+ // move (created in target, source still present) must NOT be
886
+ // observable to clients, or they will display two copies until the
887
+ // next refresh. `targetFolderPath` lets the caller drop the entry
888
+ // straight into a subfolder of the target repo.
555
889
  const created = await targetGraph.create({
556
890
  entityType: entityType,
557
891
  entityId,
@@ -559,21 +893,123 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
559
893
  content: source.document?.content ?? '',
560
894
  commitMessage: `${mode === 'move' ? 'Move' : 'Copy'} ${entityType}: ${entityId} from ${sourceRepo}`,
561
895
  user: gitUser,
896
+ folderPath: targetFolderPath,
562
897
  });
898
+ // Phase 2 — for `move` only: delete from source. If this fails, run
899
+ // a compensating delete on target so we don't leave a duplicate.
900
+ // The compensating delete is best-effort; if it also fails, surface
901
+ // a 500 with both error messages so the residue is visible (the
902
+ // previous behavior silently succeeded and left orphans).
903
+ if (mode === 'move') {
904
+ try {
905
+ // keepAssets: the moved entity's immutable-keyed blobs are shared
906
+ // (same assetId in both repos' bodies during the window); the
907
+ // source delete must not destroy them.
908
+ await sourceGraph.delete(entityType, entityId, undefined, gitUser, { keepAssets: true });
909
+ // Re-point ownership to the target repo ONLY after the source
910
+ // delete succeeds. Doing it after means a failed source delete
911
+ // (handled below) leaves the index pointing at the source repo,
912
+ // which is correct for the entity we're about to restore there.
913
+ // Blobs/URLs are unchanged (immutable keys); only the index's
914
+ // repo column moves, which is what serve-time authz reads.
915
+ workspaceManager.getAssetIndex?.()?.reassignOwner(sourceRepo, entityType, entityId, { repo: targetRepo });
916
+ }
917
+ catch (deleteErr) {
918
+ const deleteMsg = deleteErr instanceof Error ? deleteErr.message : String(deleteErr);
919
+ let recoveryMsg = null;
920
+ try {
921
+ let restoreSource = !existsSync(sourcePath);
922
+ if (!restoreSource) {
923
+ try {
924
+ restoreSource = readFileSync(sourcePath, 'utf-8') !== sourceRawMarkdown;
925
+ }
926
+ catch {
927
+ restoreSource = true;
928
+ }
929
+ }
930
+ if (restoreSource) {
931
+ mkdirSync(dirname(sourcePath), { recursive: true });
932
+ writeFileSync(sourcePath, sourceRawMarkdown, 'utf-8');
933
+ const recoveryPaths = [sourcePath];
934
+ const folderMarkerPath = join(dirname(sourcePath), '.gitkeep');
935
+ if (existsSync(folderMarkerPath)) {
936
+ rmSync(folderMarkerPath, { force: true });
937
+ recoveryPaths.push(folderMarkerPath);
938
+ }
939
+ const sourceAny = sourceGraph;
940
+ const recoveryGit = sourceAny.gitService;
941
+ const recoveryUser = gitUser ?? sourceAny.gitUser;
942
+ if (!recoveryGit || !recoveryUser) {
943
+ throw new Error('source graph git service or git user unavailable');
944
+ }
945
+ recoveryGit.commitFiles(recoveryPaths, `Recover ${entityType}: ${entityId} after failed move to ${targetRepo}`, recoveryUser);
946
+ }
947
+ }
948
+ catch (recoveryErr) {
949
+ recoveryMsg = recoveryErr instanceof Error ? recoveryErr.message : String(recoveryErr);
950
+ log.error('[critical] cross-repo move source recovery failed', {
951
+ sourceRepo,
952
+ targetRepo,
953
+ entityType,
954
+ entityId,
955
+ sourcePath,
956
+ deleteError: deleteMsg,
957
+ recoveryError: recoveryMsg,
958
+ capturedContent: sourceRawMarkdown,
959
+ });
960
+ }
961
+ try {
962
+ // keepAssets: ownership was NOT reassigned (that happens only
963
+ // after a successful source delete), so the blobs still belong
964
+ // to the source entity we just restored. Destroying them here
965
+ // would leave the recovered source pointing at dead URLs.
966
+ await targetGraph.delete(entityType, entityId, undefined, gitUser, { keepAssets: true });
967
+ if (recoveryMsg) {
968
+ return reply.status(500).send({
969
+ error: `Move failed: source delete of ${entityType}/${entityId} failed (${deleteMsg}). Target rolled back, but source recovery commit failed (${recoveryMsg}); manual source repo recovery is required.`,
970
+ code: 'MOVE_SOURCE_RECOVERY_FAILED',
971
+ sourceRepo,
972
+ targetRepo,
973
+ });
974
+ }
975
+ return reply.status(500).send({
976
+ error: `Move failed: source delete of ${entityType}/${entityId} failed (${deleteMsg}). Target rolled back; folders are back to their pre-move state.`,
977
+ });
978
+ }
979
+ catch (compErr) {
980
+ const compMsg = compErr instanceof Error ? compErr.message : String(compErr);
981
+ if (recoveryMsg) {
982
+ return reply.status(500).send({
983
+ error: `Move failed and could not be rolled back. Source delete failed (${deleteMsg}); source recovery commit failed (${recoveryMsg}); compensating target delete also failed (${compMsg}). Captured source content was logged for manual recovery.`,
984
+ code: 'MOVE_RECOVERY_AND_ROLLBACK_FAILED',
985
+ sourceRepo,
986
+ targetRepo,
987
+ });
988
+ }
989
+ return reply.status(500).send({
990
+ error: `Move failed and could not be rolled back. Source delete failed (${deleteMsg}); compensating target delete also failed (${compMsg}). Entry now exists in BOTH ${sourceRepo} and ${targetRepo} — manual cleanup required.`,
991
+ code: 'MOVE_DUPLICATE',
992
+ sourceRepo,
993
+ targetRepo,
994
+ });
995
+ }
996
+ }
997
+ }
998
+ // Phase 3 — both phases succeeded; only now broadcast. Order:
999
+ // source-delete first so subscribers don't briefly observe the
1000
+ // entity in both folders during the network race.
563
1001
  const actor = req.user?.displayName ?? 'API';
564
1002
  const ts = new Date().toISOString();
565
- wsHub?.broadcast({
566
- type: 'entity_change', action: 'created', repo: targetRepo, entityType, entityId,
567
- actor, source: 'api', timestamp: ts,
568
- });
569
- // Delete from source if moving
570
1003
  if (mode === 'move') {
571
- await sourceGraph.delete(entityType, entityId, undefined, gitUser);
572
1004
  wsHub?.broadcast({
573
1005
  type: 'entity_change', action: 'deleted', repo: sourceRepo, entityType, entityId,
574
1006
  actor, source: 'api', timestamp: ts,
575
1007
  });
576
1008
  }
1009
+ wsHub?.broadcast({
1010
+ type: 'entity_change', action: 'created', repo: targetRepo, entityType, entityId,
1011
+ actor, source: 'api', timestamp: ts,
1012
+ });
577
1013
  return reply.send({
578
1014
  success: true,
579
1015
  entity: created,
@@ -584,27 +1020,208 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
584
1020
  return reply.status(400).send({ error: err.message });
585
1021
  }
586
1022
  });
587
- // DELETE /api/repos/:repo/entities/:type/:iddelete an entity
588
- fastify.delete('/api/repos/:repo/entities/:type/:id', async (req, reply) => {
589
- const { repo, type, id } = req.params;
590
- if (!hasRepoAccess(req, repo, workspaceManager, authService)) {
591
- return reply.status(404).send({ error: `Collection "${repo}" not found` });
1023
+ // POST /api/repos/:repo/transfer-entities — bulk move a list of entries
1024
+ // into a single target folder (in this or another repo). The web UI's
1025
+ // multi-select Move flow used to loop and POST per entry; this collapses
1026
+ // to one round-trip per source repo and one commit per repo touched.
1027
+ fastify.post('/api/repos/:repo/transfer-entities', async (req, reply) => {
1028
+ const sourceRepo = req.params.repo;
1029
+ const { entries, targetRepo, targetFolder } = req.body ?? {};
1030
+ if (!Array.isArray(entries) || !targetRepo || targetFolder === undefined) {
1031
+ return reply.status(400).send({ error: 'entries, targetRepo, and targetFolder are required' });
1032
+ }
1033
+ // Workspace Assets store ownership gate (codex P1 / D2): same rule as the
1034
+ // single /transfer route, applied per asset entry. No-op when the source
1035
+ // isn't workspace_wide.
1036
+ for (const e of entries) {
1037
+ if (e.entityType !== 'asset')
1038
+ continue;
1039
+ const denial = assetDeleteDenial(workspaceManager.getAssetIndex?.(), workspaceManager.getRepoConfig(sourceRepo), reqUser(req), sourceRepo, e.entityType, e.entityId);
1040
+ if (denial)
1041
+ return reply.status(403).send({ error: denial });
1042
+ }
1043
+ // Access enforced by graphForWrite — both source (mutate) and
1044
+ // target (write) require write access. Resolved up-front so a
1045
+ // target-side denial never surfaces only after source mutation.
1046
+ const sourceGraph = graphForWrite(req, reply, sourceRepo);
1047
+ if (!sourceGraph)
1048
+ return;
1049
+ const targetGraph = graphForWrite(req, reply, targetRepo);
1050
+ if (!targetGraph)
1051
+ return;
1052
+ try {
1053
+ if (sessionManager) {
1054
+ await sessionManager.flush(sourceRepo);
1055
+ if (targetRepo !== sourceRepo)
1056
+ await sessionManager.flush(targetRepo);
1057
+ }
1058
+ const result = await sourceGraph.moveEntitiesToRepo(entries.map(e => ({ entityType: e.entityType, entityId: e.entityId })), targetGraph, targetFolder, { user: reqGitUser(req) });
1059
+ const actor = req.user?.displayName ?? 'API';
1060
+ const ts = new Date().toISOString();
1061
+ wsHub?.broadcast({ type: 'repo_sync', repo: sourceRepo, source: 'api', actor, timestamp: ts });
1062
+ if (targetRepo !== sourceRepo) {
1063
+ wsHub?.broadcast({ type: 'repo_sync', repo: targetRepo, source: 'api', actor, timestamp: ts });
1064
+ }
1065
+ return reply.send({ success: true, ...result });
1066
+ }
1067
+ catch (err) {
1068
+ const status = err?.name === 'InvalidFolderPathError' ? 400
1069
+ : err.message?.includes('not found') ? 404
1070
+ : err.message?.includes('already occupied') ? 409
1071
+ : 400;
1072
+ return reply.status(status).send({ error: err.message });
592
1073
  }
593
- let graph;
1074
+ });
1075
+ // POST /api/repos/:repo/transfer-folder — move an entire folder (with all
1076
+ // descendants) into another repo in one shot. Replaces the per-entry loop
1077
+ // the web UI used to run, which produced 2 git commits per entry; this
1078
+ // route performs a single FS-level rename and one commit per repo.
1079
+ fastify.post('/api/repos/:repo/transfer-folder', async (req, reply) => {
1080
+ const sourceRepo = req.params.repo;
1081
+ const { sourceFolder, targetRepo, targetFolder } = req.body ?? {};
1082
+ if (!sourceFolder || !targetRepo || targetFolder === undefined) {
1083
+ return reply.status(400).send({ error: 'sourceFolder, targetRepo, and targetFolder are required' });
1084
+ }
1085
+ if (sourceRepo === targetRepo && sourceFolder === targetFolder) {
1086
+ return reply.status(400).send({ error: 'Source and target are the same' });
1087
+ }
1088
+ // Workspace Assets store gate (codex P1 / D2). Moving a folder OUT of a
1089
+ // workspace_wide store relocates+reassigns every asset under it, escaping
1090
+ // the per-asset uploader/owner delete rule. A folder-level bulk move in
1091
+ // the shared store is an admin action — restrict it to a workspace owner.
1092
+ // (No-op for normal folders; the manager never folder-moves the store.)
1093
+ if (workspaceManager.getRepoConfig(sourceRepo)?.workspace_wide && !isWorkspaceOwner(reqUser(req)?.role ?? '')) {
1094
+ return reply.status(403).send({ error: 'Only a workspace owner can move folders out of the shared Assets store.' });
1095
+ }
1096
+ // Access enforced by graphForWrite — both source (mutate) and
1097
+ // target (write) require write access. Resolved up-front so a
1098
+ // target-side denial never surfaces only after source mutation.
1099
+ const sourceGraph = graphForWrite(req, reply, sourceRepo);
1100
+ if (!sourceGraph)
1101
+ return;
1102
+ const targetGraph = graphForWrite(req, reply, targetRepo);
1103
+ if (!targetGraph)
1104
+ return;
594
1105
  try {
595
- graph = workspaceManager.getGraph(repo);
1106
+ // Flush any deferred saves on either side so we don't strand a
1107
+ // pending edit in the source folder mid-move.
1108
+ if (sessionManager) {
1109
+ await sessionManager.flush(sourceRepo);
1110
+ if (targetRepo !== sourceRepo)
1111
+ await sessionManager.flush(targetRepo);
1112
+ }
1113
+ const stats = await sourceGraph.moveFolderToRepo(sourceFolder, targetGraph, targetFolder, { user: reqGitUser(req) });
1114
+ const actor = req.user?.displayName ?? 'API';
1115
+ const ts = new Date().toISOString();
1116
+ // One refresh-trigger per repo. The receiving and sending lists
1117
+ // both need to update; using `repo_sync` keeps the realtime
1118
+ // handler simple — it already calls workspace.refreshEntities
1119
+ // on this event type. Avoids emitting 2× entries-moved entity_change
1120
+ // broadcasts (one created, one deleted) per file.
1121
+ wsHub?.broadcast({ type: 'repo_sync', repo: sourceRepo, source: 'api', actor, timestamp: ts });
1122
+ if (targetRepo !== sourceRepo) {
1123
+ wsHub?.broadcast({ type: 'repo_sync', repo: targetRepo, source: 'api', actor, timestamp: ts });
1124
+ }
1125
+ return reply.send({ success: true, ...stats });
596
1126
  }
597
1127
  catch (err) {
598
- return reply.status(404).send({ error: err.message });
1128
+ const status = err?.name === 'InvalidFolderPathError' ? 400
1129
+ : err.message?.includes('not found') ? 404
1130
+ : err.message?.includes('not empty') ? 409
1131
+ : 400;
1132
+ return reply.status(status).send({ error: err.message });
599
1133
  }
1134
+ });
1135
+ // Workspace Assets store delete gate (codex P1 / D2) — uploader/owner only.
1136
+ // Pure decision lives in services/asset-delete-authz.ts (unit-tested); this
1137
+ // closure binds it to the route's workspaceManager.
1138
+ const denyAssetDelete = (user, repo, entityType, entityId) => assetDeleteDenial(workspaceManager.getAssetIndex?.(), workspaceManager.getRepoConfig(repo), user, repo, entityType, entityId);
1139
+ // DELETE /api/repos/:repo/entities/:type/:id — delete an entity
1140
+ fastify.delete('/api/repos/:repo/entities/:type/:id', async (req, reply) => {
1141
+ const { repo, type, id } = req.params;
1142
+ const denial = denyAssetDelete(reqUser(req), repo, type, id);
1143
+ if (denial)
1144
+ return reply.status(403).send({ error: denial });
1145
+ // Access enforced by graphForRead (WorkspaceAccessService).
1146
+ const graph = graphForRead(req, reply, repo);
1147
+ if (!graph)
1148
+ return;
600
1149
  try {
1150
+ // Soft bridge-awareness: deleting a person that's linked to an auth
1151
+ // account orphans that user (their "My Tasks"/@mentions stop
1152
+ // resolving). We don't BLOCK the delete — the link is a soft
1153
+ // relationship — but we surface a warning so the caller knows, and the
1154
+ // orphan is then caught by the Profile panel's unlinked affordance.
1155
+ let bridgeWarning;
1156
+ if (type === 'person') {
1157
+ try {
1158
+ const handle = graph.get('person', id)?.data?.user_handle;
1159
+ if (typeof handle === 'string' && handle.length > 0) {
1160
+ bridgeWarning = `This record was linked to the "${handle}" account. Deleting it unlinks that user — their "My Tasks", @mentions, and task assignments stop resolving until they link a new record (Settings → Profile → "Create & link").`;
1161
+ log.warn(`[bridge] deleting linked person ${repo}/${id} (was user ${handle})`);
1162
+ }
1163
+ }
1164
+ catch { /* not readable / already gone — let graph.delete surface it */ }
1165
+ }
601
1166
  await graph.delete(type, id, `Delete ${type}/${id} from web UI`, reqGitUser(req));
1167
+ // Deleting an asset entity removes its blob(s); drop the cached bucket
1168
+ // walk so a legacy twin doesn't linger in the manager (codex P2).
1169
+ if (type === 'asset')
1170
+ clearLegacyBucketCache();
602
1171
  wsHub?.broadcast({
603
1172
  type: 'entity_change', action: 'deleted', repo, entityType: type, entityId: id,
604
1173
  actor: req.user?.displayName ?? 'API',
605
1174
  source: 'api', timestamp: new Date().toISOString(),
606
1175
  });
607
- return reply.send({ ok: true });
1176
+ return reply.send({ ok: true, ...(bridgeWarning ? { warning: bridgeWarning } : {}) });
1177
+ }
1178
+ catch (err) {
1179
+ return reply.status(400).send({ error: err.message });
1180
+ }
1181
+ });
1182
+ // POST /api/repos/:repo/entities/bulk-delete — delete many entities in
1183
+ // one git transaction + one WS broadcast. Replaces N parallel
1184
+ // DELETE /entities/:type/:id calls, which on a 500-entry bulk delete
1185
+ // produced 500 git commits + 500 entity_change broadcasts and froze
1186
+ // the UI for minutes.
1187
+ fastify.post('/api/repos/:repo/entities/bulk-delete', async (req, reply) => {
1188
+ const { repo } = req.params;
1189
+ // Access enforced by graphForWrite (WorkspaceAccessService).
1190
+ const graph = graphForWrite(req, reply, repo);
1191
+ if (!graph)
1192
+ return;
1193
+ const body = req.body;
1194
+ if (!body?.entities || !Array.isArray(body.entities)) {
1195
+ return reply.status(400).send({ error: 'Expected { entities: [{ entityType, entityId }] }' });
1196
+ }
1197
+ // Apply the workspace-Assets delete gate per entity (codex P1 / D2):
1198
+ // uploader/owner-only. Denied entries are reported, not silently dropped.
1199
+ const deleteUser = reqUser(req);
1200
+ const denied = [];
1201
+ const allowed = body.entities.filter(e => {
1202
+ const d = denyAssetDelete(deleteUser, repo, e.entityType, e.entityId);
1203
+ if (d) {
1204
+ denied.push({ entityType: e.entityType, entityId: e.entityId, error: d });
1205
+ return false;
1206
+ }
1207
+ return true;
1208
+ });
1209
+ try {
1210
+ const result = await graph.bulkDelete(allowed.map(e => ({ entityType: e.entityType, entityId: e.entityId })), {
1211
+ user: reqGitUser(req),
1212
+ commitMessage: `Bulk delete ${allowed.length} ${allowed.length === 1 ? 'entry' : 'entries'} from web UI`,
1213
+ });
1214
+ if (result.deleted.length > 0) {
1215
+ wsHub?.broadcast({
1216
+ type: 'repo_sync', repo,
1217
+ actor: req.user?.displayName ?? 'API',
1218
+ source: 'api', timestamp: new Date().toISOString(),
1219
+ });
1220
+ }
1221
+ return reply.send({
1222
+ deleted: result.deleted.length,
1223
+ errors: [...result.errors, ...denied],
1224
+ });
608
1225
  }
609
1226
  catch (err) {
610
1227
  return reply.status(400).send({ error: err.message });
@@ -613,51 +1230,700 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
613
1230
  // POST /api/repos/:repo/entities — create a new entity
614
1231
  fastify.post('/api/repos/:repo/entities', async (req, reply) => {
615
1232
  const { repo } = req.params;
616
- if (!hasRepoAccess(req, repo, workspaceManager, authService)) {
617
- return reply.status(404).send({ error: `Collection "${repo}" not found` });
618
- }
619
- const { entityType, entityId, data = {}, content = '' } = req.body;
1233
+ // Access enforced by graphForWrite (WorkspaceAccessService).
1234
+ const graph = graphForWrite(req, reply, repo);
1235
+ if (!graph)
1236
+ return;
1237
+ const { entityType, entityId, data = {}, content = '', folder_path } = req.body;
620
1238
  if (!entityType || !entityId) {
621
1239
  return reply.status(400).send({ error: 'entityType and entityId are required' });
622
1240
  }
623
- let graph;
1241
+ // Refuse to re-mint a retired type (`asset`, Phase F). Otherwise an
1242
+ // unknown entityType falls back to EntityBaseSchema and writes a
1243
+ // base-only stub of exactly the type asset-as-embed removed.
1244
+ if (RESERVED_ENTITY_TYPES.has(entityType)) {
1245
+ return reply.status(400).send({ error: `Entity type '${entityType}' was retired and can no longer be created.` });
1246
+ }
1247
+ const eagerError = checkEagerSkillPermission(req, entityType, data, repo, workspaceManager, authService);
1248
+ if (eagerError)
1249
+ return reply.status(403).send({ error: eagerError });
1250
+ // Person entities can carry a `user_handle` pointer to an auth account;
1251
+ // enforce workspace-wide uniqueness AND owner-only authz before
1252
+ // delegating to the per-repo create path. Stamping a handle on a new
1253
+ // entity is the same trust action as stamping it on an existing one,
1254
+ // so non-owners can't create a pre-linked person entity either.
1255
+ if (entityType === 'person') {
1256
+ try {
1257
+ validateUserHandleInPayload(workspaceManager, data);
1258
+ if (Object.prototype.hasOwnProperty.call(data, 'user_handle')) {
1259
+ assertCanChangeUserHandle(reqUser(req), '', data.user_handle);
1260
+ }
1261
+ }
1262
+ catch (err) {
1263
+ if (err instanceof BridgeError) {
1264
+ const status = err.code === 'OWNER_REQUIRED' ? 403 : 409;
1265
+ return reply.status(status).send({ error: err.message, code: err.code, ...err.details });
1266
+ }
1267
+ throw err;
1268
+ }
1269
+ }
1270
+ // Scope ref fields (today: task.assignee → person). Rejects a wikilink
1271
+ // that resolves to a non-person; forward references are allowed.
624
1272
  try {
625
- graph = workspaceManager.getGraph(repo);
1273
+ assertEntityRefs(workspaceManager, entityType, data, { userExists: (id) => !!authService.findUserById(Number(id)) });
626
1274
  }
627
1275
  catch (err) {
628
- return reply.status(404).send({ error: err.message });
1276
+ if (err instanceof EntityRefError || err instanceof UnknownUserRefError) {
1277
+ return reply.status(400).send({ error: err.message, code: err.code, field: err.field });
1278
+ }
1279
+ throw err;
629
1280
  }
630
1281
  try {
1282
+ // Detect implicit subfolder creation: if the entry lands in a folder
1283
+ // that didn't exist yet, the entity_change below only refreshes the
1284
+ // entry list — clients need a _folder event to refetch the folder tree
1285
+ // (sidebar + list/gallery folder rows). Mirrors the MCP create_entity
1286
+ // path; normalize the same way graph.create does so the existence
1287
+ // check matches the real on-disk path.
1288
+ const normFolder = folder_path ? validateFolderPath(folder_path) : '';
1289
+ const folderExisted = !normFolder || existsSync(join(graph.getRepoPath(), normFolder));
631
1290
  const entity = await graph.create({
632
1291
  entityType: entityType,
633
1292
  entityId,
634
1293
  data: { entity_type: entityType, entity_id: entityId, ...data },
635
1294
  content,
1295
+ folderPath: folder_path,
636
1296
  commitMessage: `Create ${entityType}/${entityId} from web UI`,
637
1297
  user: reqGitUser(req),
638
1298
  });
1299
+ const actor = req.user?.displayName ?? 'API';
1300
+ const ts = new Date().toISOString();
639
1301
  wsHub?.broadcast({
640
1302
  type: 'entity_change', action: 'created', repo, entityType, entityId,
1303
+ actor, source: 'api', timestamp: ts,
1304
+ });
1305
+ if (!folderExisted) {
1306
+ wsHub?.broadcast({
1307
+ type: 'entity_change', action: 'created', repo, entityType: '_folder', entityId: normFolder,
1308
+ actor, source: 'api', timestamp: ts,
1309
+ });
1310
+ }
1311
+ return reply.status(201).send(entity);
1312
+ }
1313
+ catch (err) {
1314
+ return reply.status(400).send({ error: err.message });
1315
+ }
1316
+ });
1317
+ // POST /api/repos/:repo/move — move entry to a different folder within the
1318
+ // same repo. Distinct from /transfer (cross-repo) and /rename (id change).
1319
+ // The entry's type and id are unchanged; only its folder_path moves.
1320
+ fastify.post('/api/repos/:repo/move', async (req, reply) => {
1321
+ const { repo } = req.params;
1322
+ const { entityType, entityId, folder_path } = req.body ?? {};
1323
+ if (!entityType || !entityId || folder_path === undefined) {
1324
+ return reply.status(400).send({ error: 'entityType, entityId, and folder_path are required' });
1325
+ }
1326
+ // Access enforced by graphForWrite (WorkspaceAccessService).
1327
+ const graph = graphForWrite(req, reply, repo);
1328
+ if (!graph)
1329
+ return;
1330
+ try {
1331
+ if (sessionManager)
1332
+ await sessionManager.flush(repo);
1333
+ // If the destination folder doesn't exist yet, the move creates it —
1334
+ // emit a _folder event (alongside the entry update) so clients refetch
1335
+ // the folder tree, not just the entry list. Mirrors MCP move_entity.
1336
+ const normTarget = folder_path ? validateFolderPath(folder_path) : '';
1337
+ const targetExisted = !normTarget || existsSync(join(graph.getRepoPath(), normTarget));
1338
+ const moved = await graph.move(entityType, entityId, folder_path, undefined, reqGitUser(req));
1339
+ const actor = req.user?.displayName ?? 'API';
1340
+ const ts = new Date().toISOString();
1341
+ wsHub?.broadcast({
1342
+ type: 'entity_change', action: 'updated', repo, entityType, entityId,
1343
+ actor, source: 'api', timestamp: ts,
1344
+ });
1345
+ if (!targetExisted) {
1346
+ wsHub?.broadcast({
1347
+ type: 'entity_change', action: 'created', repo, entityType: '_folder', entityId: normTarget,
1348
+ actor, source: 'api', timestamp: ts,
1349
+ });
1350
+ }
1351
+ return reply.send({ success: true, entity: moved });
1352
+ }
1353
+ catch (err) {
1354
+ const status = err?.name === 'InvalidFolderPathError' ? 400
1355
+ : err.message?.includes('already occupied') ? 409
1356
+ : err.message?.includes('not found') ? 404
1357
+ : 400;
1358
+ return reply.status(status).send({ error: err.message });
1359
+ }
1360
+ });
1361
+ // POST /api/repos/:repo/folders — create a folder (with .gitkeep). Auto-creates parents.
1362
+ // Optional display_name is written to the folder's .studiograph-folder.json
1363
+ // sidecar when set; otherwise the client falls back to toDisplayName(slug).
1364
+ fastify.post('/api/repos/:repo/folders', async (req, reply) => {
1365
+ const { repo } = req.params;
1366
+ const { path, display_name } = req.body ?? {};
1367
+ if (!path || typeof path !== 'string') {
1368
+ return reply.status(400).send({ error: 'path is required' });
1369
+ }
1370
+ // Access enforced by graphForWrite (WorkspaceAccessService).
1371
+ const graph = graphForWrite(req, reply, repo);
1372
+ if (!graph)
1373
+ return;
1374
+ try {
1375
+ await graph.createFolder(path, undefined, reqGitUser(req), {
1376
+ displayName: display_name,
1377
+ });
1378
+ // Emit the _folder sentinel so OTHER connected clients refetch the
1379
+ // folder tree (sidebar + list/gallery folder rows). The local creator
1380
+ // already updated optimistically via workspace.addFolder.
1381
+ wsHub?.broadcast({
1382
+ type: 'entity_change', action: 'created', repo, entityType: '_folder', entityId: validateFolderPath(path),
641
1383
  actor: req.user?.displayName ?? 'API',
642
1384
  source: 'api', timestamp: new Date().toISOString(),
643
1385
  });
644
- return reply.status(201).send(entity);
1386
+ return reply.status(201).send({ success: true, path });
645
1387
  }
646
1388
  catch (err) {
647
1389
  return reply.status(400).send({ error: err.message });
648
1390
  }
649
1391
  });
1392
+ // PATCH /api/repos/:repo/folders — rename / move a folder within the repo,
1393
+ // optionally updating its display_name in the same commit. Three shapes:
1394
+ // { from, to } — slug-only rename / move
1395
+ // { from, to, display_name } — slug change + display update
1396
+ // { from, to: from, display_name } — display-only update (no FS move)
1397
+ // `display_name: ""` (or null) clears the override and reverts to the slug
1398
+ // fallback.
1399
+ fastify.patch('/api/repos/:repo/folders', async (req, reply) => {
1400
+ const { repo } = req.params;
1401
+ const { from, to, display_name } = req.body ?? {};
1402
+ if (!from || !to) {
1403
+ return reply.status(400).send({ error: 'from and to are required' });
1404
+ }
1405
+ // Access enforced by graphForWrite (WorkspaceAccessService).
1406
+ const graph = graphForWrite(req, reply, repo);
1407
+ if (!graph)
1408
+ return;
1409
+ try {
1410
+ if (sessionManager)
1411
+ await sessionManager.flush(repo);
1412
+ await graph.renameFolder(from, to, undefined, reqGitUser(req), {
1413
+ displayName: display_name === undefined ? undefined : display_name,
1414
+ });
1415
+ // Rename/move relocates entries on disk, so the _folder sentinel's
1416
+ // refreshRepoAfterFolderMutation (tree + entity summaries) is exactly
1417
+ // what other clients need. Carries the new path as the entityId.
1418
+ wsHub?.broadcast({
1419
+ type: 'entity_change', action: 'updated', repo, entityType: '_folder', entityId: validateFolderPath(to),
1420
+ actor: req.user?.displayName ?? 'API',
1421
+ source: 'api', timestamp: new Date().toISOString(),
1422
+ });
1423
+ return reply.send({ success: true, from, to });
1424
+ }
1425
+ catch (err) {
1426
+ const status = err?.name === 'InvalidFolderPathError' ? 400
1427
+ : err.message?.includes('not found') ? 404
1428
+ : err.message?.includes('already exists') ? 409
1429
+ : 400;
1430
+ return reply.status(status).send({ error: err.message });
1431
+ }
1432
+ });
1433
+ // DELETE /api/repos/:repo/folders?path=X&recursive=true — delete a folder.
1434
+ // Without recursive=true, non-empty folders return 409 with blast-radius
1435
+ // counts so the UI can confirm before retrying.
1436
+ fastify.delete('/api/repos/:repo/folders', async (req, reply) => {
1437
+ const { repo } = req.params;
1438
+ const { path, recursive } = req.query;
1439
+ if (!path) {
1440
+ return reply.status(400).send({ error: 'path query parameter is required' });
1441
+ }
1442
+ // Access enforced by graphForWrite (WorkspaceAccessService).
1443
+ const graph = graphForWrite(req, reply, repo);
1444
+ if (!graph)
1445
+ return;
1446
+ // Workspace Assets store gate (codex P1 / D2). A recursive delete in a
1447
+ // workspace_wide store destroys every asset under the folder, bypassing
1448
+ // the per-asset uploader/owner rule — restrict it to a workspace owner.
1449
+ // (Non-recursive delete is blocked on non-empty folders by FOLDER_NOT_EMPTY,
1450
+ // so it can't take assets with it.)
1451
+ if (recursive === 'true' && workspaceManager.getRepoConfig(repo)?.workspace_wide && !isWorkspaceOwner(reqUser(req)?.role ?? '')) {
1452
+ return reply.status(403).send({ error: 'Only a workspace owner can recursively delete folders in the shared Assets store.' });
1453
+ }
1454
+ try {
1455
+ if (sessionManager)
1456
+ await sessionManager.flush(repo);
1457
+ const result = await graph.deleteFolder(path, {
1458
+ recursive: recursive === 'true',
1459
+ user: reqGitUser(req),
1460
+ });
1461
+ // Emit the _folder sentinel so other clients drop the folder (and any
1462
+ // entries it held, via the entity-summary refresh) from their view.
1463
+ wsHub?.broadcast({
1464
+ type: 'entity_change', action: 'deleted', repo, entityType: '_folder', entityId: validateFolderPath(path),
1465
+ actor: req.user?.displayName ?? 'API',
1466
+ source: 'api', timestamp: new Date().toISOString(),
1467
+ });
1468
+ return reply.send({ success: true, ...result });
1469
+ }
1470
+ catch (err) {
1471
+ if (err?.code === 'FOLDER_NOT_EMPTY') {
1472
+ return reply.status(409).send({
1473
+ error: err.message,
1474
+ code: 'FOLDER_NOT_EMPTY',
1475
+ blast_radius: err.blastRadius,
1476
+ });
1477
+ }
1478
+ const status = err?.name === 'InvalidFolderPathError' ? 400
1479
+ : err.message?.includes('not found') ? 404
1480
+ : 400;
1481
+ return reply.status(status).send({ error: err.message });
1482
+ }
1483
+ });
650
1484
  // POST /api/repos/:repo/commit — flush deferred commits (navigate-away signal)
651
1485
  fastify.post('/api/repos/:repo/commit', async (req, reply) => {
652
1486
  if (!sessionManager) {
653
1487
  return reply.send({ committed: false });
654
1488
  }
655
1489
  const { repo } = req.params;
656
- if (!hasRepoAccess(req, repo, workspaceManager, authService)) {
657
- return reply.status(404).send({ error: `Collection "${repo}" not found` });
1490
+ // Access enforced via accessService.canRead \u2014 flush operates on the
1491
+ // SessionManager (no graph instance needed), so a boolean check is
1492
+ // sufficient. Pending commits were already authored by an authenticated
1493
+ // writer; this endpoint just flushes them on navigate-away.
1494
+ if (!accessService.canRead(reqUser(req), repo)) {
1495
+ return reply.status(404).send({ error: `Folder \u201C${repo}\u201D not found` });
658
1496
  }
659
1497
  const committed = await sessionManager.flush(repo);
660
1498
  return reply.send({ committed });
661
1499
  });
1500
+ // POST /api/repos/:repo/import — import files into a folder.
1501
+ //
1502
+ // Routing by extension:
1503
+ // .md / .txt / .csv → direct write via importFile() — no LLM
1504
+ // round-trip, preserves the source faithfully. Markdown frontmatter
1505
+ // (entity_type, entity_id) is honored when present; otherwise
1506
+ // sensible defaults.
1507
+ // .pdf + opaque binaries → asset wrapper via uploadAsset() —
1508
+ // creates an `asset` entity, stores the binary in R2 or the local
1509
+ // volume, writes a PDF text sidecar for vector indexing.
1510
+ // .docx → text extraction + agent capture
1511
+ // pipeline (until the mammoth → markdown workstream lands).
1512
+ fastify.post('/api/repos/:repo/import', async (req, reply) => {
1513
+ const { repo } = req.params;
1514
+ // Access enforced by graphForWrite — import creates entities, so write
1515
+ // is required. Graph captured here is reused by the inner closures so
1516
+ // they don't need to re-resolve (which would also bypass the gate).
1517
+ const repoGraph = graphForWrite(req, reply, repo);
1518
+ if (!repoGraph)
1519
+ return;
1520
+ const body = req.body;
1521
+ if (!body?.files || !Array.isArray(body.files)) {
1522
+ return reply.status(400).send({ error: 'Expected { files: [{ filename, content }] }' });
1523
+ }
1524
+ // Reject paths with traversal segments; importFile slugifies the rest.
1525
+ // Empty / undefined = repo root (existing behavior).
1526
+ const targetFolderPath = (body.targetFolderPath ?? '').trim();
1527
+ if (targetFolderPath.split('/').some(seg => seg === '..' || seg === '.')) {
1528
+ return reply.status(400).send({ error: 'Invalid targetFolderPath: contains traversal segments' });
1529
+ }
1530
+ const captureRepo = body.agentDecides ? undefined : repo;
1531
+ const gitUser = reqGitUser(req);
1532
+ // Cancel support (CANCEL): the web client aborts the fetch via an
1533
+ // AbortController. Listen on the RESPONSE stream's `close` — it fires when
1534
+ // the connection drops, and `writableEnded` is false iff we haven't
1535
+ // finished sending the response yet, i.e. a genuine mid-import client
1536
+ // abort (vs the normal post-response close, where writableEnded is true).
1537
+ // `reply.raw` is the ServerResponse, the canonical place to detect that
1538
+ // the peer went away while we're still working; `req.raw` (the parsed
1539
+ // request) is a less reliable signal for an abort during processing.
1540
+ // Best-effort: the batch runner only reads this between batches, so a
1541
+ // single-batch import still commits its completed prefix (additive +
1542
+ // collision-safe). The listener is on this request's per-request
1543
+ // ServerResponse, so it's GC'd with the request — no cross-request leak.
1544
+ let clientGone = false;
1545
+ reply.raw.on('close', () => {
1546
+ if (!reply.raw.writableEnded)
1547
+ clientGone = true;
1548
+ });
1549
+ try {
1550
+ const { extractText, importFile } = await import('../../services/import-service.js');
1551
+ const { importAssetAsEntity } = await import('../../services/asset-import-service.js');
1552
+ const { resolveFormat, isIndexable, FALLBACK_ASSET_ENTRY } = await import('../../core/format-registry.js');
1553
+ const results = [];
1554
+ const CONCURRENCY = 3;
1555
+ async function processDirect(file) {
1556
+ // Reuse the access-checked graph captured in the outer scope. Do
1557
+ // NOT re-resolve via workspaceManager.getGraph here — the audit's
1558
+ // boundary is the outer graphForWrite call.
1559
+ const graph = repoGraph;
1560
+ const result = await importFile(graph, file.filename, file.content, {
1561
+ user: gitUser,
1562
+ // Caller-chosen destination subfolder; importFile combines it
1563
+ // with each file's own relative directory.
1564
+ folderPath: targetFolderPath || undefined,
1565
+ // Defer commits — we batch them into one transaction at the
1566
+ // end of the request so a 500-file import is one git commit
1567
+ // instead of 500. Per-file commits dominated wall time
1568
+ // (~50-200ms each) before this; batching collapses that to
1569
+ // one commit for the whole batch.
1570
+ skipCommit: true,
1571
+ });
1572
+ if (result.status === 'created' && result.path) {
1573
+ return {
1574
+ filename: file.filename,
1575
+ status: 'created',
1576
+ entities: [{ entityId: result.entityId, entityType: result.entityType }],
1577
+ entry: {
1578
+ operation: 'create',
1579
+ entityType: result.entityType,
1580
+ entityId: result.entityId,
1581
+ filePath: result.path,
1582
+ },
1583
+ sidecarPaths: result.sidecarPaths,
1584
+ };
1585
+ }
1586
+ return { filename: file.filename, status: result.status, error: result.error };
1587
+ }
1588
+ async function processAsset(file) {
1589
+ // Reuse the outer access-checked graph; re-resolving would bypass
1590
+ // the architectural boundary even though the closure captures repo.
1591
+ const graph = repoGraph;
1592
+ try {
1593
+ const buffer = Buffer.from(file.content, 'base64');
1594
+ // Asset-as-embed (Phase B): store the binary folder-owned + create a
1595
+ // properly-TYPED entity (document / dataset×N / material) that embeds
1596
+ // it via asset_ref. No self-owning `asset` entity, no text sidecar
1597
+ // (the seeded body is indexed natively). xlsx fans out to N datasets,
1598
+ // so this returns 1..N entities — `commitEntries` carries them all.
1599
+ const result = await importAssetAsEntity({
1600
+ graph,
1601
+ filename: file.filename,
1602
+ buffer,
1603
+ folderPath: targetFolderPath || undefined,
1604
+ user: gitUser,
1605
+ uploaderUserId: reqUser(req)?.id != null ? String(reqUser(req).id) : null,
1606
+ // Defer commits — the dispatcher's batched commitPending call at
1607
+ // the bottom of the route covers every entry in one transaction.
1608
+ skipCommit: true,
1609
+ });
1610
+ return {
1611
+ filename: file.filename,
1612
+ status: 'created',
1613
+ entities: result.entities.map(e => ({ entityId: e.entityId, entityType: e.entityType })),
1614
+ commitEntries: result.entities.map(e => ({
1615
+ operation: 'create',
1616
+ entityType: e.entityType,
1617
+ entityId: e.entityId,
1618
+ filePath: e.filePath,
1619
+ })),
1620
+ };
1621
+ }
1622
+ catch (err) {
1623
+ return { filename: file.filename, status: 'error', error: err.message ?? 'Asset import failed' };
1624
+ }
1625
+ }
1626
+ async function processViaAgent(file) {
1627
+ try {
1628
+ const text = await extractText(file.filename, file.content);
1629
+ if (!text) {
1630
+ log.warn(`[import] No text extracted from ${file.filename}`);
1631
+ return { filename: file.filename, status: 'error', error: 'No text content extracted' };
1632
+ }
1633
+ log.info(`[import] Extracted ${text.length} chars from ${file.filename}`);
1634
+ const captureRes = await fastify.inject({
1635
+ method: 'POST',
1636
+ url: '/api/capture',
1637
+ headers: {
1638
+ 'content-type': 'application/json',
1639
+ ...(req.headers.authorization ? { authorization: req.headers.authorization } : {}),
1640
+ ...(req.headers.cookie ? { cookie: req.headers.cookie } : {}),
1641
+ },
1642
+ payload: JSON.stringify({ text: `[Imported from ${file.filename}]\n\n${text}`, ...(captureRepo ? { repo: captureRepo } : {}) }),
1643
+ });
1644
+ const captureBody = JSON.parse(captureRes.body);
1645
+ if (captureRes.statusCode !== 200 || !captureBody.success) {
1646
+ log.warn(`[import] Capture failed for ${file.filename}: ${captureRes.statusCode} ${captureBody.error ?? JSON.stringify(captureBody).slice(0, 200)}`);
1647
+ return { filename: file.filename, status: 'error', error: captureBody.error ?? 'Agent processing failed' };
1648
+ }
1649
+ return { filename: file.filename, status: 'created', entities: captureBody.entities };
1650
+ }
1651
+ catch (err) {
1652
+ return { filename: file.filename, status: 'error', error: err.message ?? 'Processing failed' };
1653
+ }
1654
+ }
1655
+ // Route a single file off the format registry — the one source of
1656
+ // truth for every ingestion surface. P0 is a parity refactor: the
1657
+ // routing below reproduces today's behavior exactly. The only
1658
+ // intentional follow-ups are flagged inline (docx/svg transitional
1659
+ // paths; the FALLBACK reject that P0b flips to lenient).
1660
+ async function routeByFormat(file, entry) {
1661
+ // Transitional formats keep the current agent-extraction path until
1662
+ // their real handler lands. After P2 this is svg only — it routes
1663
+ // here until sanitize-html is wired (P3); routing it to uploadAsset
1664
+ // before then would store unsanitized svg (stored-XSS). docx flipped
1665
+ // to the asset path in P2 (uploads as asset, converts on demand).
1666
+ if (entry.transitional === 'agent-extract') {
1667
+ return processViaAgent(file);
1668
+ }
1669
+ switch (entry.handler) {
1670
+ case 'importDirect':
1671
+ return processDirect(file);
1672
+ case 'uploadAsset':
1673
+ // P0 parity: genuinely-unknown types are still rejected. The
1674
+ // lenient generic-asset fallback (accept unknowns as assets) is
1675
+ // P0b — a deliberate behavior change made by removing exactly
1676
+ // this guard.
1677
+ if (entry === FALLBACK_ASSET_ENTRY) {
1678
+ return { filename: file.filename, status: 'error', error: `Unsupported file: ${file.filename}` };
1679
+ }
1680
+ return processAsset(file);
1681
+ default:
1682
+ // importBookmarks is not routed through the dispatcher until
1683
+ // P8 (content-sniff); bookmark exports still use the dedicated
1684
+ // POST /api/repos/:repo/import-bookmarks endpoint, so a bare
1685
+ // .html lands here and is rejected — matching today.
1686
+ return { filename: file.filename, status: 'error', error: `Unsupported file: ${file.filename}` };
1687
+ }
1688
+ }
1689
+ async function processFile(file) {
1690
+ if (!file.filename) {
1691
+ return { filename: file.filename, status: 'error', error: 'Missing filename' };
1692
+ }
1693
+ // Resolve the format once and thread it through routing.
1694
+ // `searchable` (OV3 server contract) reflects the registry's
1695
+ // DECLARED indexability for the file — the client (P5) renders the
1696
+ // honest "stored as-is / not searchable" callouts from it.
1697
+ // KNOWN LIMITATION: this is declared, not actual. A scanned/encrypted
1698
+ // PDF declares searchable:true (indexAs:'sidecar') but yields no text,
1699
+ // so it won't be flagged "stored as-is" even though its content is
1700
+ // effectively filename-only. Closing that gap needs the extractor to
1701
+ // report actual text-yield back into this flag (P3-indexing scope);
1702
+ // the negative claim stays accurate, the positive one is best-effort.
1703
+ const entry = resolveFormat(file.filename);
1704
+ const searchable = isIndexable(entry);
1705
+ const result = await routeByFormat(file, entry);
1706
+ return { ...result, searchable };
1707
+ }
1708
+ // Process files in cancellable batches of CONCURRENCY. The runner
1709
+ // checks the abort flag between batches: if the client disconnected
1710
+ // (web AbortController), it stops dispatching the remainder, lets the
1711
+ // in-flight batch settle, and returns the completed prefix — which the
1712
+ // commitPending step below commits in one transaction (no uncommitted
1713
+ // files left behind). Cancel is partial + honest: import is additive
1714
+ // and collision-safe.
1715
+ const { runImportBatches } = await import('./import-batch.js');
1716
+ const { results: batched, cancelled } = await runImportBatches(body.files, processFile, { concurrency: CONCURRENCY, isAborted: () => clientGone });
1717
+ results.push(...batched);
1718
+ // Direct-write path deferred git commits — flush them all in one
1719
+ // transaction now. Collapses N per-file commits into one batch
1720
+ // commit (~50ms vs N×50ms). Agent-path files committed inline via
1721
+ // /api/capture so they're not in this set.
1722
+ const pendingEntries = results.flatMap(r => {
1723
+ const pf = r;
1724
+ // Asset-import files carry commitEntries (1..N); direct-write files
1725
+ // carry a single entry. Prefer the plural, fall back to the singular.
1726
+ if (pf.commitEntries && pf.commitEntries.length > 0)
1727
+ return pf.commitEntries;
1728
+ return pf.entry ? [pf.entry] : [];
1729
+ });
1730
+ // Collect every sidecar path produced by the import — these stage in
1731
+ // the same commit as the entity files, so the folder display name
1732
+ // and the entries it labels land atomically. De-dup because two
1733
+ // imported files inside the same Schema_OS/widgets/ would otherwise
1734
+ // produce duplicate sidecar entries.
1735
+ const sidecarPaths = Array.from(new Set(results.flatMap(r => r.sidecarPaths ?? [])));
1736
+ // commitPending failure used to be swallowed and the route returned
1737
+ // success=true even though the files were never committed. The audit
1738
+ // (2026-05) flagged this: a 500-file import that failed mid-commit
1739
+ // looked successful to the client but left N untracked files. Now
1740
+ // the failure propagates as 500 with a partial-failure shape so the
1741
+ // caller knows which entries are stranded.
1742
+ let commitError = null;
1743
+ if (pendingEntries.length > 0) {
1744
+ try {
1745
+ await repoGraph.commitPending(pendingEntries, {
1746
+ commitMessage: `Import ${pendingEntries.length} ${pendingEntries.length === 1 ? 'entry' : 'entries'} from web UI`,
1747
+ user: gitUser,
1748
+ extraPaths: sidecarPaths.length > 0 ? sidecarPaths : undefined,
1749
+ });
1750
+ }
1751
+ catch (err) {
1752
+ commitError = err instanceof Error ? err.message : String(err);
1753
+ log.warn(`[import] batch commit failed for ${repo}: ${commitError}`);
1754
+ }
1755
+ }
1756
+ const created = results.filter(r => r.status === 'created').length;
1757
+ const entityCount = results.reduce((sum, r) => sum + (r.entities?.length ?? 0), 0);
1758
+ // Single repo_sync broadcast for the whole import. The realtime
1759
+ // store's repo_sync handler refreshes the repo's entity list once
1760
+ // — vs. one entity_change per file, which would do N re-renders
1761
+ // on every connected client and freeze the UI for large imports.
1762
+ // Skip when commit failed: there's nothing for clients to refresh
1763
+ // beyond stranded uncommitted files, and the broadcast would lie
1764
+ // about persistence.
1765
+ if (created > 0 && !commitError) {
1766
+ wsHub?.broadcast({
1767
+ type: 'repo_sync', repo,
1768
+ actor: req.user?.displayName ?? 'API',
1769
+ source: 'api', timestamp: new Date().toISOString(),
1770
+ });
1771
+ // Asset caption/extract enqueue is no longer wired here — it fires
1772
+ // from the single VectorService.onAssetPending chokepoint when the
1773
+ // asset's index state is seeded (covers every upload path uniformly).
1774
+ }
1775
+ const errors = results.filter(r => r.status === 'error').map(r => r.error);
1776
+ if (commitError) {
1777
+ // Files are on disk but uncommitted — partial failure. Surface as
1778
+ // 500 with details so the client doesn't show "imported N entries"
1779
+ // for entries that won't survive a server restart.
1780
+ return reply.status(500).send({
1781
+ success: false,
1782
+ error: `Import wrote ${pendingEntries.length} entries to disk but the git commit failed: ${commitError}`,
1783
+ commitError,
1784
+ uncommittedEntries: pendingEntries.length,
1785
+ imported: created,
1786
+ entityCount,
1787
+ skipped: errors.length,
1788
+ errors,
1789
+ results,
1790
+ });
1791
+ }
1792
+ return {
1793
+ success: true,
1794
+ imported: created,
1795
+ entityCount,
1796
+ skipped: errors.length,
1797
+ errors,
1798
+ results,
1799
+ // Cancel summary (CANCEL): when the client disconnected mid-import we
1800
+ // committed the completed prefix above and report N of M honestly.
1801
+ // The aborting client typically won't read this (its fetch rejected),
1802
+ // but it keeps the contract honest for any caller that does.
1803
+ cancelled,
1804
+ total: body.files.length,
1805
+ };
1806
+ }
1807
+ catch (err) {
1808
+ return reply.status(400).send({
1809
+ success: false,
1810
+ error: err instanceof Error ? err.message : 'Unknown error',
1811
+ });
1812
+ }
1813
+ });
1814
+ // POST /api/repos/:repo/import-bookmarks — import browser bookmark exports as reference entities
1815
+ fastify.post('/api/repos/:repo/import-bookmarks', async (req, reply) => {
1816
+ const { repo } = req.params;
1817
+ // Access enforced by graphForWrite — bookmark import creates entities.
1818
+ const graph = graphForWrite(req, reply, repo);
1819
+ if (!graph)
1820
+ return;
1821
+ const body = req.body;
1822
+ if (!body?.content || typeof body.content !== 'string') {
1823
+ return reply.status(400).send({ error: 'Expected { content: string }' });
1824
+ }
1825
+ try {
1826
+ const { isNetscapeBookmarkFile, importBookmarks } = await import('../../services/bookmark-import-service.js');
1827
+ if (!isNetscapeBookmarkFile(body.content)) {
1828
+ return reply.status(400).send({
1829
+ error: "This doesn't appear to be a browser bookmark export. Export bookmarks from your browser's bookmark manager to generate the correct format.",
1830
+ });
1831
+ }
1832
+ const user = reqGitUser(req);
1833
+ const result = await importBookmarks(graph, body.content, user);
1834
+ // Single git commit for all created entities
1835
+ if (result.created > 0) {
1836
+ const git = graph.getGitService();
1837
+ if (git) {
1838
+ const commitUser = user ?? { id: 'import', name: 'Import', email: 'import@studiograph' };
1839
+ git.commitAll(`Import ${result.created} bookmark${result.created !== 1 ? 's' : ''} as references`, commitUser);
1840
+ }
1841
+ }
1842
+ return {
1843
+ success: true,
1844
+ created: result.created,
1845
+ skipped: result.skipped,
1846
+ errors: result.errors,
1847
+ };
1848
+ }
1849
+ catch (err) {
1850
+ return reply.status(400).send({
1851
+ success: false,
1852
+ error: err instanceof Error ? err.message : 'Unknown error',
1853
+ });
1854
+ }
1855
+ });
1856
+ // POST /api/repos/:repo/assets — generic asset upload.
1857
+ //
1858
+ // Multipart `file` field. Creates a wrapper `asset` entity whose
1859
+ // frontmatter points at the stored binary (R2 or local volume) via
1860
+ // `asset_url`. PDFs additionally get a text sidecar at
1861
+ // `<workspace>/.studiograph/assets-text/asset/<id>.txt` so the vector
1862
+ // indexer can RAG them without bloating the markdown body. See
1863
+ // docs/asset-architecture.md.
1864
+ fastify.post('/api/repos/:repo/assets', async (req, reply) => {
1865
+ const { repo } = req.params;
1866
+ // Access enforced by graphForWrite (WorkspaceAccessService).
1867
+ const graph = graphForWrite(req, reply, repo);
1868
+ if (!graph)
1869
+ return;
1870
+ // Per-route fileSize override — the global multipart limit (10MB) is
1871
+ // tuned for inline image embeds; full asset uploads accept up to the
1872
+ // 100MB cap enforced inside AssetService.upload().
1873
+ let file;
1874
+ try {
1875
+ file = await req.file({ limits: { fileSize: 100 * 1024 * 1024 } });
1876
+ }
1877
+ catch (err) {
1878
+ return reply.status(400).send({ error: err?.message ?? 'Invalid multipart upload' });
1879
+ }
1880
+ if (!file)
1881
+ return reply.status(400).send({ error: 'No file uploaded' });
1882
+ const buffer = await file.toBuffer();
1883
+ try {
1884
+ // Asset-as-embed (B2): uploading to the Assets store produces a
1885
+ // FREE-STANDING, folder-owned asset — NOT an `asset` entity. The asset
1886
+ // manager lists it directly; the homepage capture embeds its URL into
1887
+ // the entry it creates and deletes it by assetId if unused. Folder-owned
1888
+ // assets are repo-scoped, so the `folder` subpath doesn't apply here.
1889
+ const stem = file.filename.replace(/\.[^.]+$/, '');
1890
+ const displayName = req.query.name?.trim()
1891
+ || stem.replace(/[_-]+/g, ' ').replace(/\b\w/g, c => c.toUpperCase())
1892
+ || file.filename;
1893
+ const stored = await graph.uploadAsset('material', // ignored for ownership when `owner` is set
1894
+ 'asset', // ignored
1895
+ file.filename, buffer, undefined, {
1896
+ owner: { type: 'folder', id: repo },
1897
+ uploaderUserId: reqUser(req)?.id != null ? String(reqUser(req).id) : null,
1898
+ name: displayName,
1899
+ });
1900
+ wsHub?.broadcast({
1901
+ type: 'repo_sync',
1902
+ repo,
1903
+ actor: req.user?.displayName ?? 'API',
1904
+ source: 'api',
1905
+ timestamp: new Date().toISOString(),
1906
+ });
1907
+ // NOTE: image caption/OCR indexing is NOT derived for this
1908
+ // free-standing asset — the caption sweep + live drain key on `asset`
1909
+ // ENTITIES, which this no longer creates. It stays filename-only in
1910
+ // vector search until the caption pipeline is rewired to be
1911
+ // assets.db-row-keyed (tracked follow-up).
1912
+ return reply.send({
1913
+ success: true,
1914
+ assetId: stored.assetId,
1915
+ asset_url: stored.publicUrl,
1916
+ asset_kind: stored.mediaType,
1917
+ filename: stored.filename,
1918
+ });
1919
+ }
1920
+ catch (err) {
1921
+ const msg = err instanceof Error ? err.message : String(err);
1922
+ const status = msg.includes('not configured') ? 501
1923
+ : msg.includes('Unsupported asset extension') ? 415
1924
+ : 400;
1925
+ return reply.status(status).send({ success: false, error: msg });
1926
+ }
1927
+ });
662
1928
  }
663
1929
  //# sourceMappingURL=graph-api.js.map