studiograph 1.3.48-next.2 → 1.3.48-next.200

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