studiograph 1.3.47 → 1.3.48-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (1057) 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 +573 -181
  10. package/dist/agent/orchestrator.js.map +1 -1
  11. package/dist/agent/prompts/entity-types-section.d.ts +36 -0
  12. package/dist/agent/prompts/entity-types-section.js +90 -0
  13. package/dist/agent/prompts/entity-types-section.js.map +1 -0
  14. package/dist/agent/prompts/system.md +148 -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 +635 -332
  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.d.ts +18 -0
  50. package/dist/agent/tools/web-tools.js +283 -0
  51. package/dist/agent/tools/web-tools.js.map +1 -0
  52. package/dist/cli/commands/about.d.ts +1 -0
  53. package/dist/cli/commands/about.js +55 -3
  54. package/dist/cli/commands/about.js.map +1 -1
  55. package/dist/cli/commands/admin/audit-workspace.d.ts +23 -0
  56. package/dist/cli/commands/admin/audit-workspace.js +133 -0
  57. package/dist/cli/commands/admin/audit-workspace.js.map +1 -0
  58. package/dist/cli/commands/admin/audit.d.ts +21 -0
  59. package/dist/cli/commands/admin/audit.js +174 -0
  60. package/dist/cli/commands/admin/audit.js.map +1 -0
  61. package/dist/cli/commands/admin/backup.d.ts +54 -0
  62. package/dist/cli/commands/admin/backup.js +207 -0
  63. package/dist/cli/commands/admin/backup.js.map +1 -0
  64. package/dist/cli/commands/admin/folder.d.ts +11 -0
  65. package/dist/cli/commands/admin/folder.js +186 -0
  66. package/dist/cli/commands/admin/folder.js.map +1 -0
  67. package/dist/cli/commands/admin/index.d.ts +16 -0
  68. package/dist/cli/commands/admin/index.js +46 -0
  69. package/dist/cli/commands/admin/index.js.map +1 -0
  70. package/dist/cli/commands/admin/migrate-assets.d.ts +53 -0
  71. package/dist/cli/commands/admin/migrate-assets.js +184 -0
  72. package/dist/cli/commands/admin/migrate-assets.js.map +1 -0
  73. package/dist/cli/commands/admin/migrate.d.ts +56 -0
  74. package/dist/cli/commands/admin/migrate.js +263 -0
  75. package/dist/cli/commands/admin/migrate.js.map +1 -0
  76. package/dist/cli/commands/admin/restore.d.ts +24 -0
  77. package/dist/cli/commands/admin/restore.js +228 -0
  78. package/dist/cli/commands/admin/restore.js.map +1 -0
  79. package/dist/cli/commands/admin/secrets.d.ts +23 -0
  80. package/dist/cli/commands/admin/secrets.js +256 -0
  81. package/dist/cli/commands/admin/secrets.js.map +1 -0
  82. package/dist/cli/commands/admin/settings.d.ts +28 -0
  83. package/dist/cli/commands/admin/settings.js +284 -0
  84. package/dist/cli/commands/admin/settings.js.map +1 -0
  85. package/dist/cli/commands/admin/token.d.ts +42 -0
  86. package/dist/cli/commands/admin/token.js +126 -0
  87. package/dist/cli/commands/admin/token.js.map +1 -0
  88. package/dist/cli/commands/admin/transfer-ownership.d.ts +15 -0
  89. package/dist/cli/commands/admin/transfer-ownership.js +106 -0
  90. package/dist/cli/commands/admin/transfer-ownership.js.map +1 -0
  91. package/dist/cli/commands/admin/user.d.ts +11 -0
  92. package/dist/cli/commands/admin/user.js +187 -0
  93. package/dist/cli/commands/admin/user.js.map +1 -0
  94. package/dist/cli/commands/clone.d.ts +25 -3
  95. package/dist/cli/commands/clone.js +142 -36
  96. package/dist/cli/commands/clone.js.map +1 -1
  97. package/dist/cli/commands/commit.d.ts +1 -1
  98. package/dist/cli/commands/commit.js +24 -11
  99. package/dist/cli/commands/commit.js.map +1 -1
  100. package/dist/cli/commands/config.d.ts +23 -107
  101. package/dist/cli/commands/config.js +52 -260
  102. package/dist/cli/commands/config.js.map +1 -1
  103. package/dist/cli/commands/connector.d.ts +10 -13
  104. package/dist/cli/commands/connector.js +16 -58
  105. package/dist/cli/commands/connector.js.map +1 -1
  106. package/dist/cli/commands/deploy.js +215 -68
  107. package/dist/cli/commands/deploy.js.map +1 -1
  108. package/dist/cli/commands/index.js +2 -2
  109. package/dist/cli/commands/index.js.map +1 -1
  110. package/dist/cli/commands/init.js +10 -30
  111. package/dist/cli/commands/init.js.map +1 -1
  112. package/dist/cli/commands/mcp.js +12 -4
  113. package/dist/cli/commands/mcp.js.map +1 -1
  114. package/dist/cli/commands/orphans.js +14 -30
  115. package/dist/cli/commands/orphans.js.map +1 -1
  116. package/dist/cli/commands/r2.d.ts +3 -0
  117. package/dist/cli/commands/r2.js +223 -204
  118. package/dist/cli/commands/r2.js.map +1 -1
  119. package/dist/cli/commands/redeploy.js +65 -12
  120. package/dist/cli/commands/redeploy.js.map +1 -1
  121. package/dist/cli/commands/reset.d.ts +22 -6
  122. package/dist/cli/commands/reset.js +142 -63
  123. package/dist/cli/commands/reset.js.map +1 -1
  124. package/dist/cli/commands/serve.js +209 -24
  125. package/dist/cli/commands/serve.js.map +1 -1
  126. package/dist/cli/commands/sync.d.ts +1 -1
  127. package/dist/cli/commands/sync.js +372 -202
  128. package/dist/cli/commands/sync.js.map +1 -1
  129. package/dist/cli/crypto-bundle.d.ts +39 -0
  130. package/dist/cli/crypto-bundle.js +94 -0
  131. package/dist/cli/crypto-bundle.js.map +1 -0
  132. package/dist/cli/index.js +24 -35
  133. package/dist/cli/index.js.map +1 -1
  134. package/dist/cli/railway-pin.d.ts +68 -0
  135. package/dist/cli/railway-pin.js +96 -0
  136. package/dist/cli/railway-pin.js.map +1 -0
  137. package/dist/cli/railway-provisioning.d.ts +53 -0
  138. package/dist/cli/railway-provisioning.js +85 -0
  139. package/dist/cli/railway-provisioning.js.map +1 -0
  140. package/dist/cli/setup-wizard.d.ts +33 -52
  141. package/dist/cli/setup-wizard.js +43 -68
  142. package/dist/cli/setup-wizard.js.map +1 -1
  143. package/dist/core/accent-palette.d.ts +22 -0
  144. package/dist/core/accent-palette.js +47 -0
  145. package/dist/core/accent-palette.js.map +1 -0
  146. package/dist/core/cell-anchor.d.ts +29 -0
  147. package/dist/core/cell-anchor.js +53 -0
  148. package/dist/core/cell-anchor.js.map +1 -0
  149. package/dist/core/comment-anchor.d.ts +41 -0
  150. package/dist/core/comment-anchor.js +147 -0
  151. package/dist/core/comment-anchor.js.map +1 -0
  152. package/dist/core/commit-messages.d.ts +57 -0
  153. package/dist/core/commit-messages.js +85 -0
  154. package/dist/core/commit-messages.js.map +1 -0
  155. package/dist/core/embeds.d.ts +56 -0
  156. package/dist/core/embeds.js +103 -0
  157. package/dist/core/embeds.js.map +1 -0
  158. package/dist/core/entity-body-templates.d.ts +21 -0
  159. package/dist/core/entity-body-templates.js +53 -0
  160. package/dist/core/entity-body-templates.js.map +1 -0
  161. package/dist/core/entity-types-catalog.d.ts +65 -0
  162. package/dist/core/entity-types-catalog.js +140 -0
  163. package/dist/core/entity-types-catalog.js.map +1 -0
  164. package/dist/core/folder-paths.d.ts +41 -0
  165. package/dist/core/folder-paths.js +95 -0
  166. package/dist/core/folder-paths.js.map +1 -0
  167. package/dist/core/folder-sidecar.d.ts +36 -0
  168. package/dist/core/folder-sidecar.js +91 -0
  169. package/dist/core/folder-sidecar.js.map +1 -0
  170. package/dist/core/graph.d.ts +675 -14
  171. package/dist/core/graph.js +2118 -314
  172. package/dist/core/graph.js.map +1 -1
  173. package/dist/core/history-diff.d.ts +35 -0
  174. package/dist/core/history-diff.js +114 -0
  175. package/dist/core/history-diff.js.map +1 -0
  176. package/dist/core/schema-registry.d.ts +59 -0
  177. package/dist/core/schema-registry.js +198 -0
  178. package/dist/core/schema-registry.js.map +1 -1
  179. package/dist/core/schemas/connector.d.ts +67 -0
  180. package/dist/core/schemas/connector.js +100 -0
  181. package/dist/core/schemas/connector.js.map +1 -0
  182. package/dist/core/schemas/workspace.d.ts +122 -0
  183. package/dist/core/schemas/workspace.js +211 -0
  184. package/dist/core/schemas/workspace.js.map +1 -0
  185. package/dist/core/secrets/SecretStore.d.ts +77 -0
  186. package/dist/core/secrets/SecretStore.js +13 -0
  187. package/dist/core/secrets/SecretStore.js.map +1 -0
  188. package/dist/core/secrets/SettingsResolver.d.ts +54 -0
  189. package/dist/core/secrets/SettingsResolver.js +80 -0
  190. package/dist/core/secrets/SettingsResolver.js.map +1 -0
  191. package/dist/core/secrets/SettingsStore.d.ts +30 -0
  192. package/dist/core/secrets/SettingsStore.js +9 -0
  193. package/dist/core/secrets/SettingsStore.js.map +1 -0
  194. package/dist/core/secrets/SqliteEncryptedStore.d.ts +90 -0
  195. package/dist/core/secrets/SqliteEncryptedStore.js +598 -0
  196. package/dist/core/secrets/SqliteEncryptedStore.js.map +1 -0
  197. package/dist/core/secrets/audit.d.ts +36 -0
  198. package/dist/core/secrets/audit.js +60 -0
  199. package/dist/core/secrets/audit.js.map +1 -0
  200. package/dist/core/secrets/auth-db-migration.d.ts +129 -0
  201. package/dist/core/secrets/auth-db-migration.js +653 -0
  202. package/dist/core/secrets/auth-db-migration.js.map +1 -0
  203. package/dist/core/secrets/bundle.d.ts +141 -0
  204. package/dist/core/secrets/bundle.js +435 -0
  205. package/dist/core/secrets/bundle.js.map +1 -0
  206. package/dist/core/secrets/dual-write.d.ts +81 -0
  207. package/dist/core/secrets/dual-write.js +247 -0
  208. package/dist/core/secrets/dual-write.js.map +1 -0
  209. package/dist/core/secrets/encryption.d.ts +44 -0
  210. package/dist/core/secrets/encryption.js +97 -0
  211. package/dist/core/secrets/encryption.js.map +1 -0
  212. package/dist/core/secrets/index.d.ts +49 -0
  213. package/dist/core/secrets/index.js +74 -0
  214. package/dist/core/secrets/index.js.map +1 -0
  215. package/dist/core/secrets/master-key.d.ts +38 -0
  216. package/dist/core/secrets/master-key.js +122 -0
  217. package/dist/core/secrets/master-key.js.map +1 -0
  218. package/dist/core/secrets/probes/anthropic.d.ts +98 -0
  219. package/dist/core/secrets/probes/anthropic.js +300 -0
  220. package/dist/core/secrets/probes/anthropic.js.map +1 -0
  221. package/dist/core/secrets/probes/brave.d.ts +9 -0
  222. package/dist/core/secrets/probes/brave.js +15 -0
  223. package/dist/core/secrets/probes/brave.js.map +1 -0
  224. package/dist/core/secrets/probes/r2.d.ts +15 -0
  225. package/dist/core/secrets/probes/r2.js +14 -0
  226. package/dist/core/secrets/probes/r2.js.map +1 -0
  227. package/dist/core/secrets/probes/voyage.d.ts +13 -0
  228. package/dist/core/secrets/probes/voyage.js +52 -0
  229. package/dist/core/secrets/probes/voyage.js.map +1 -0
  230. package/dist/core/secrets/r2-structural-migration.d.ts +39 -0
  231. package/dist/core/secrets/r2-structural-migration.js +109 -0
  232. package/dist/core/secrets/r2-structural-migration.js.map +1 -0
  233. package/dist/core/secrets/read-flip.d.ts +59 -0
  234. package/dist/core/secrets/read-flip.js +87 -0
  235. package/dist/core/secrets/read-flip.js.map +1 -0
  236. package/dist/core/secrets/registry.d.ts +188 -0
  237. package/dist/core/secrets/registry.js +598 -0
  238. package/dist/core/secrets/registry.js.map +1 -0
  239. package/dist/core/types.d.ts +77 -455
  240. package/dist/core/types.js +17 -3
  241. package/dist/core/types.js.map +1 -1
  242. package/dist/core/user-config.d.ts +75 -7
  243. package/dist/core/user-config.js +155 -7
  244. package/dist/core/user-config.js.map +1 -1
  245. package/dist/core/validation.d.ts +717 -1791
  246. package/dist/core/validation.js +357 -228
  247. package/dist/core/validation.js.map +1 -1
  248. package/dist/core/workspace-manager.d.ts +79 -12
  249. package/dist/core/workspace-manager.js +213 -73
  250. package/dist/core/workspace-manager.js.map +1 -1
  251. package/dist/core/workspace.d.ts +19 -5
  252. package/dist/core/workspace.js +69 -35
  253. package/dist/core/workspace.js.map +1 -1
  254. package/dist/mcp/comment-virtual-entity.d.ts +36 -0
  255. package/dist/mcp/comment-virtual-entity.js +71 -0
  256. package/dist/mcp/comment-virtual-entity.js.map +1 -0
  257. package/dist/mcp/connector-manager.d.ts +94 -13
  258. package/dist/mcp/connector-manager.js +283 -62
  259. package/dist/mcp/connector-manager.js.map +1 -1
  260. package/dist/mcp/oauth-provider.d.ts +27 -33
  261. package/dist/mcp/oauth-provider.js +65 -134
  262. package/dist/mcp/oauth-provider.js.map +1 -1
  263. package/dist/mcp/oauth-registry.d.ts +62 -0
  264. package/dist/mcp/oauth-registry.js +85 -0
  265. package/dist/mcp/oauth-registry.js.map +1 -0
  266. package/dist/mcp/server.d.ts +11 -3
  267. package/dist/mcp/server.js +30 -5
  268. package/dist/mcp/server.js.map +1 -1
  269. package/dist/mcp/state-signer.d.ts +41 -0
  270. package/dist/mcp/state-signer.js +120 -0
  271. package/dist/mcp/state-signer.js.map +1 -0
  272. package/dist/mcp/tools.d.ts +35 -2
  273. package/dist/mcp/tools.js +1051 -117
  274. package/dist/mcp/tools.js.map +1 -1
  275. package/dist/server/__tests__/helpers/graph-api.d.ts +18 -0
  276. package/dist/server/__tests__/helpers/graph-api.js +16 -0
  277. package/dist/server/__tests__/helpers/graph-api.js.map +1 -0
  278. package/dist/server/body-write-coordinator.d.ts +151 -0
  279. package/dist/server/body-write-coordinator.js +161 -0
  280. package/dist/server/body-write-coordinator.js.map +1 -0
  281. package/dist/server/cloud-billing-flow.d.ts +32 -0
  282. package/dist/server/cloud-billing-flow.js +75 -0
  283. package/dist/server/cloud-billing-flow.js.map +1 -0
  284. package/dist/server/commit-audit.d.ts +26 -0
  285. package/dist/server/commit-audit.js +30 -0
  286. package/dist/server/commit-audit.js.map +1 -0
  287. package/dist/server/index.d.ts +14 -3
  288. package/dist/server/index.js +451 -193
  289. package/dist/server/index.js.map +1 -1
  290. package/dist/server/middleware/sanitize.d.ts +46 -0
  291. package/dist/server/middleware/sanitize.js +171 -0
  292. package/dist/server/middleware/sanitize.js.map +1 -0
  293. package/dist/server/routes/asset-api.d.ts +10 -0
  294. package/dist/server/routes/asset-api.js +294 -0
  295. package/dist/server/routes/asset-api.js.map +1 -0
  296. package/dist/server/routes/audit-api.d.ts +24 -0
  297. package/dist/server/routes/audit-api.js +117 -0
  298. package/dist/server/routes/audit-api.js.map +1 -0
  299. package/dist/server/routes/auth-api.d.ts +3 -2
  300. package/dist/server/routes/auth-api.js +519 -38
  301. package/dist/server/routes/auth-api.js.map +1 -1
  302. package/dist/server/routes/capture-api.d.ts +18 -0
  303. package/dist/server/routes/capture-api.js +257 -0
  304. package/dist/server/routes/capture-api.js.map +1 -0
  305. package/dist/server/routes/chat.d.ts +4 -1
  306. package/dist/server/routes/chat.js +298 -88
  307. package/dist/server/routes/chat.js.map +1 -1
  308. package/dist/server/routes/comments-api.d.ts +15 -0
  309. package/dist/server/routes/comments-api.js +444 -0
  310. package/dist/server/routes/comments-api.js.map +1 -0
  311. package/dist/server/routes/config-api.d.ts +21 -0
  312. package/dist/server/routes/config-api.js +256 -0
  313. package/dist/server/routes/config-api.js.map +1 -0
  314. package/dist/server/routes/connectors-api.d.ts +17 -0
  315. package/dist/server/routes/connectors-api.js +410 -0
  316. package/dist/server/routes/connectors-api.js.map +1 -0
  317. package/dist/server/routes/event-ingest-api.d.ts +15 -0
  318. package/dist/server/routes/event-ingest-api.js +125 -0
  319. package/dist/server/routes/event-ingest-api.js.map +1 -0
  320. package/dist/server/routes/git-http.d.ts +3 -3
  321. package/dist/server/routes/git-http.js +86 -38
  322. package/dist/server/routes/git-http.js.map +1 -1
  323. package/dist/server/routes/graph-api-access.d.ts +35 -0
  324. package/dist/server/routes/graph-api-access.js +105 -0
  325. package/dist/server/routes/graph-api-access.js.map +1 -0
  326. package/dist/server/routes/graph-api.d.ts +3 -2
  327. package/dist/server/routes/graph-api.js +1321 -263
  328. package/dist/server/routes/graph-api.js.map +1 -1
  329. package/dist/server/routes/health.d.ts +18 -0
  330. package/dist/server/routes/health.js +74 -0
  331. package/dist/server/routes/health.js.map +1 -0
  332. package/dist/server/routes/insights-api.d.ts +1 -2
  333. package/dist/server/routes/insights-api.js +149 -41
  334. package/dist/server/routes/insights-api.js.map +1 -1
  335. package/dist/server/routes/mcp.d.ts +7 -3
  336. package/dist/server/routes/mcp.js +22 -6
  337. package/dist/server/routes/mcp.js.map +1 -1
  338. package/dist/server/routes/permissions-api.d.ts +9 -2
  339. package/dist/server/routes/permissions-api.js +87 -30
  340. package/dist/server/routes/permissions-api.js.map +1 -1
  341. package/dist/server/routes/r2-api.d.ts +25 -0
  342. package/dist/server/routes/r2-api.js +276 -0
  343. package/dist/server/routes/r2-api.js.map +1 -0
  344. package/dist/server/routes/secrets-api.d.ts +36 -0
  345. package/dist/server/routes/secrets-api.js +308 -0
  346. package/dist/server/routes/secrets-api.js.map +1 -0
  347. package/dist/server/routes/settings-api.d.ts +29 -0
  348. package/dist/server/routes/settings-api.js +180 -0
  349. package/dist/server/routes/settings-api.js.map +1 -0
  350. package/dist/server/routes/url-api.d.ts +7 -0
  351. package/dist/server/routes/url-api.js +74 -0
  352. package/dist/server/routes/url-api.js.map +1 -0
  353. package/dist/server/routes/views-api.js +81 -7
  354. package/dist/server/routes/views-api.js.map +1 -1
  355. package/dist/server/routes/workspace-api.d.ts +2 -1
  356. package/dist/server/routes/workspace-api.js +124 -35
  357. package/dist/server/routes/workspace-api.js.map +1 -1
  358. package/dist/server/routes/ws.d.ts +2 -1
  359. package/dist/server/routes/ws.js +64 -16
  360. package/dist/server/routes/ws.js.map +1 -1
  361. package/dist/server/session-manager.d.ts +48 -5
  362. package/dist/server/session-manager.js +182 -15
  363. package/dist/server/session-manager.js.map +1 -1
  364. package/dist/server/ws-hub.d.ts +138 -14
  365. package/dist/server/ws-hub.js +0 -0
  366. package/dist/server/ws-hub.js.map +1 -1
  367. package/dist/server/yjs-manager.d.ts +285 -7
  368. package/dist/server/yjs-manager.js +907 -36
  369. package/dist/server/yjs-manager.js.map +1 -1
  370. package/dist/server/yjs-persistence.d.ts +165 -0
  371. package/dist/server/yjs-persistence.js +557 -0
  372. package/dist/server/yjs-persistence.js.map +1 -0
  373. package/dist/server/yjs-text-diff.d.ts +32 -0
  374. package/dist/server/yjs-text-diff.js +61 -0
  375. package/dist/server/yjs-text-diff.js.map +1 -0
  376. package/dist/services/access-control.d.ts +73 -0
  377. package/dist/services/access-control.js +146 -0
  378. package/dist/services/access-control.js.map +1 -0
  379. package/dist/services/asset-listing.d.ts +39 -0
  380. package/dist/services/asset-listing.js +125 -0
  381. package/dist/services/asset-listing.js.map +1 -0
  382. package/dist/services/asset-upload-service.d.ts +53 -0
  383. package/dist/services/asset-upload-service.js +201 -0
  384. package/dist/services/asset-upload-service.js.map +1 -0
  385. package/dist/services/assets/asset-index-service.d.ts +82 -0
  386. package/dist/services/assets/asset-index-service.js +167 -0
  387. package/dist/services/assets/asset-index-service.js.map +1 -0
  388. package/dist/services/assets/base.d.ts +44 -0
  389. package/dist/services/assets/base.js +55 -0
  390. package/dist/services/assets/base.js.map +1 -1
  391. package/dist/services/assets/index.d.ts +69 -1
  392. package/dist/services/assets/index.js +125 -0
  393. package/dist/services/assets/index.js.map +1 -1
  394. package/dist/services/assets/local.d.ts +12 -1
  395. package/dist/services/assets/local.js +100 -1
  396. package/dist/services/assets/local.js.map +1 -1
  397. package/dist/services/assets/r2-sync.d.ts +88 -0
  398. package/dist/services/assets/r2-sync.js +412 -0
  399. package/dist/services/assets/r2-sync.js.map +1 -0
  400. package/dist/services/assets/r2.d.ts +34 -1
  401. package/dist/services/assets/r2.js +93 -1
  402. package/dist/services/assets/r2.js.map +1 -1
  403. package/dist/services/auth-service.d.ts +314 -36
  404. package/dist/services/auth-service.js +1126 -139
  405. package/dist/services/auth-service.js.map +1 -1
  406. package/dist/services/bookmark-import-service.d.ts +49 -0
  407. package/dist/services/bookmark-import-service.js +250 -0
  408. package/dist/services/bookmark-import-service.js.map +1 -0
  409. package/dist/services/cloud-inference-billing.d.ts +64 -0
  410. package/dist/services/cloud-inference-billing.js +313 -0
  411. package/dist/services/cloud-inference-billing.js.map +1 -0
  412. package/dist/services/comment-service.d.ts +97 -0
  413. package/dist/services/comment-service.js +341 -0
  414. package/dist/services/comment-service.js.map +1 -0
  415. package/dist/services/csv-service.d.ts +22 -17
  416. package/dist/services/csv-service.js +55 -92
  417. package/dist/services/csv-service.js.map +1 -1
  418. package/dist/services/entity-mutations.d.ts +161 -0
  419. package/dist/services/entity-mutations.js +296 -0
  420. package/dist/services/entity-mutations.js.map +1 -0
  421. package/dist/services/event-processor.d.ts +104 -0
  422. package/dist/services/event-processor.js +441 -0
  423. package/dist/services/event-processor.js.map +1 -0
  424. package/dist/services/folder-creation.d.ts +33 -0
  425. package/dist/services/folder-creation.js +103 -0
  426. package/dist/services/folder-creation.js.map +1 -0
  427. package/dist/services/git.d.ts +54 -0
  428. package/dist/services/git.js +188 -54
  429. package/dist/services/git.js.map +1 -1
  430. package/dist/services/graph-maintenance.d.ts +42 -0
  431. package/dist/services/graph-maintenance.js +143 -0
  432. package/dist/services/graph-maintenance.js.map +1 -0
  433. package/dist/services/import-service.d.ts +45 -2
  434. package/dist/services/import-service.js +304 -100
  435. package/dist/services/import-service.js.map +1 -1
  436. package/dist/services/lint-service.js +9 -16
  437. package/dist/services/lint-service.js.map +1 -1
  438. package/dist/services/markdown.d.ts +12 -1
  439. package/dist/services/markdown.js +71 -6
  440. package/dist/services/markdown.js.map +1 -1
  441. package/dist/services/memory-service.d.ts +22 -14
  442. package/dist/services/memory-service.js +53 -35
  443. package/dist/services/memory-service.js.map +1 -1
  444. package/dist/services/mention-detector.d.ts +23 -0
  445. package/dist/services/mention-detector.js +47 -0
  446. package/dist/services/mention-detector.js.map +1 -0
  447. package/dist/services/notification-broadcast.d.ts +32 -0
  448. package/dist/services/notification-broadcast.js +38 -0
  449. package/dist/services/notification-broadcast.js.map +1 -0
  450. package/dist/services/notification-service.d.ts +41 -0
  451. package/dist/services/notification-service.js +100 -0
  452. package/dist/services/notification-service.js.map +1 -0
  453. package/dist/services/personal-folder.d.ts +40 -0
  454. package/dist/services/personal-folder.js +101 -0
  455. package/dist/services/personal-folder.js.map +1 -0
  456. package/dist/services/user-person-bridge.d.ts +118 -0
  457. package/dist/services/user-person-bridge.js +306 -0
  458. package/dist/services/user-person-bridge.js.map +1 -0
  459. package/dist/services/vector-service.d.ts +146 -6
  460. package/dist/services/vector-service.js +276 -13
  461. package/dist/services/vector-service.js.map +1 -1
  462. package/dist/services/workspace-access.d.ts +106 -0
  463. package/dist/services/workspace-access.js +149 -0
  464. package/dist/services/workspace-access.js.map +1 -0
  465. package/dist/utils/git.d.ts +17 -2
  466. package/dist/utils/git.js +23 -7
  467. package/dist/utils/git.js.map +1 -1
  468. package/dist/utils/log.d.ts +42 -0
  469. package/dist/utils/log.js +137 -0
  470. package/dist/utils/log.js.map +1 -0
  471. package/dist/utils/preflight.js +2 -2
  472. package/dist/utils/preflight.js.map +1 -1
  473. package/dist/utils/version-checker.d.ts +12 -19
  474. package/dist/utils/version-checker.js +14 -104
  475. package/dist/utils/version-checker.js.map +1 -1
  476. package/dist/web/_app/immutable/assets/0.DDqq11xY.css +2 -0
  477. package/dist/web/_app/immutable/assets/12.DlIf1mKh.css +1 -0
  478. package/dist/web/_app/immutable/assets/13.7d-Q37wc.css +1 -0
  479. package/dist/web/_app/immutable/assets/14.CmLrbzZp.css +1 -0
  480. package/dist/web/_app/immutable/assets/2.CGK2ky-K.css +1 -0
  481. package/dist/web/_app/immutable/assets/3.CV0abekd.css +1 -0
  482. package/dist/web/_app/immutable/assets/4.fGsEpo95.css +1 -0
  483. package/dist/web/_app/immutable/assets/AgentOverlay.DqzjAOm4.css +1 -0
  484. package/dist/web/_app/immutable/assets/AssetManagerModal.BKJfPAPk.css +1 -0
  485. package/dist/web/_app/immutable/assets/CalendarView.bNewSxkH.css +1 -0
  486. package/dist/web/_app/immutable/assets/FolderMembersPanel.DX1oBeF_.css +1 -0
  487. package/dist/web/_app/immutable/assets/FolderMutationDialogs.BMRF6Z3z.css +1 -0
  488. package/dist/web/_app/immutable/assets/FolderPicker.B5KdvRYl.css +1 -0
  489. package/dist/web/_app/immutable/assets/GalleryView.DgvSjS0I.css +1 -0
  490. package/dist/web/_app/immutable/assets/GraphView.BJtGL9py.css +1 -0
  491. package/dist/web/_app/immutable/assets/KanbanView.D0QbZFd6.css +1 -0
  492. package/dist/web/_app/immutable/assets/Markdown.B4lW7Llh.css +1 -0
  493. package/dist/web/_app/immutable/assets/SettingsDialog.DYWYMFSO.css +1 -0
  494. package/dist/web/_app/immutable/assets/Spinner.UBfl0pab.css +1 -0
  495. package/dist/web/_app/immutable/assets/StopFilledAlt.clodJdp5.css +1 -0
  496. package/dist/web/_app/immutable/assets/TimelineView.DA-H6dvv.css +1 -0
  497. package/dist/web/_app/immutable/assets/WorkspaceView.BS1ildu7.css +1 -0
  498. package/dist/web/_app/immutable/assets/button.BlLm4Dg1.css +1 -0
  499. package/dist/web/_app/immutable/assets/formatting.B912lpdG.css +1 -0
  500. package/dist/web/_app/immutable/assets/ibm-plex-mono-latin-400-normal.CvHOgSBP.woff +0 -0
  501. package/dist/web/_app/immutable/assets/ibm-plex-mono-latin-400-normal.DMJ8VG8y.woff2 +0 -0
  502. package/dist/web/_app/immutable/assets/ibm-plex-mono-latin-500-normal.CB9ihrfo.woff +0 -0
  503. package/dist/web/_app/immutable/assets/ibm-plex-mono-latin-500-normal.DSY6xOcd.woff2 +0 -0
  504. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-400-italic.CZTNEAuW.woff2 +0 -0
  505. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-400-italic.CsGl1sm0.woff +0 -0
  506. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-400-normal.CDDApCn2.woff2 +0 -0
  507. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-400-normal.CYLoc0-x.woff +0 -0
  508. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-500-italic.BNK2_mGO.woff2 +0 -0
  509. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-500-italic.DpEwFAQM.woff +0 -0
  510. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-500-normal.6ng42L7E.woff2 +0 -0
  511. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-500-normal.BgVn5rGT.woff +0 -0
  512. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-600-normal.Cu4Hd6ag.woff +0 -0
  513. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-600-normal.CuJfVYMP.woff2 +0 -0
  514. package/dist/web/_app/immutable/assets/inline-list.cqga56xT.css +1 -0
  515. package/dist/web/_app/immutable/assets/list-columns.DCMVmy_Q.css +1 -0
  516. package/dist/web/_app/immutable/assets/realtime.DDCWxn3B.css +1 -0
  517. package/dist/web/_app/immutable/assets/sheet.DJPuqnTg.css +1 -0
  518. package/dist/web/_app/immutable/assets/speech-dictation.C5CYFhS1.css +1 -0
  519. package/dist/web/_app/immutable/chunks/0iDDDyaZ.js +1 -0
  520. package/dist/web/_app/immutable/chunks/2LgrBrpS2.js +2 -0
  521. package/dist/web/_app/immutable/chunks/3EfPAYzi2.js +1 -0
  522. package/dist/web/_app/immutable/chunks/7Bu427my2.js +184 -0
  523. package/dist/web/_app/immutable/chunks/96h3ktuk.js +1 -0
  524. package/dist/web/_app/immutable/chunks/A6FJA1AF.js +1 -0
  525. package/dist/web/_app/immutable/chunks/B-FAwCqe2.js +1 -0
  526. package/dist/web/_app/immutable/chunks/B-V5eMSw2.js +1 -0
  527. package/dist/web/_app/immutable/chunks/B-i9_YVM.js +1 -0
  528. package/dist/web/_app/immutable/chunks/B15-kvCe2.js +1 -0
  529. package/dist/web/_app/immutable/chunks/B1KZ7zY32.js +2 -0
  530. package/dist/web/_app/immutable/chunks/B4gpzeQ2.js +1 -0
  531. package/dist/web/_app/immutable/chunks/B6Lt-qaJ2.js +1 -0
  532. package/dist/web/_app/immutable/chunks/B7tbGyiU2.js +1 -0
  533. package/dist/web/_app/immutable/chunks/BBxt8lpa.js +1 -0
  534. package/dist/web/_app/immutable/chunks/BCaRi2AF.js +1 -0
  535. package/dist/web/_app/immutable/chunks/BJ6Z7eAM.js +2 -0
  536. package/dist/web/_app/immutable/chunks/BM_vIwVC.js +83 -0
  537. package/dist/web/_app/immutable/chunks/BQULA6er.js +4 -0
  538. package/dist/web/_app/immutable/chunks/BQW-EDgP2.js +224 -0
  539. package/dist/web/_app/immutable/chunks/BR6jMpLB.js +1 -0
  540. package/dist/web/_app/immutable/chunks/BVnuLlVg.js +1 -0
  541. package/dist/web/_app/immutable/chunks/BddNmQ8j.js +1 -0
  542. package/dist/web/_app/immutable/chunks/BeuunHPO.js +1 -0
  543. package/dist/web/_app/immutable/chunks/BhTmSo4n2.js +1 -0
  544. package/dist/web/_app/immutable/chunks/BikW7mty2.js +1 -0
  545. package/dist/web/_app/immutable/chunks/BkyFvJvz.js +1 -0
  546. package/dist/web/_app/immutable/chunks/BlyFGPFV.js +22 -0
  547. package/dist/web/_app/immutable/chunks/Bo5dghSu.js +3 -0
  548. package/dist/web/_app/immutable/chunks/BpPyho8Y.js +3 -0
  549. package/dist/web/_app/immutable/chunks/BsuvyNDN2.js +2 -0
  550. package/dist/web/_app/immutable/chunks/BuKi0vpu2.js +7 -0
  551. package/dist/web/_app/immutable/chunks/BuuYnIjE.js +1 -0
  552. package/dist/web/_app/immutable/chunks/By4lVKNC.js +2 -0
  553. package/dist/web/_app/immutable/chunks/ByQGCIwO.js +5 -0
  554. package/dist/web/_app/immutable/chunks/ByidpUhu.js +1 -0
  555. package/dist/web/_app/immutable/chunks/C35TIzaB.js +1 -0
  556. package/dist/web/_app/immutable/chunks/C5dM5op-.js +1 -0
  557. package/dist/web/_app/immutable/chunks/C6Nx0jpM.js +1 -0
  558. package/dist/web/_app/immutable/chunks/C6pVyOEl2.js +1 -0
  559. package/dist/web/_app/immutable/chunks/C80GKPZ9.js +1 -0
  560. package/dist/web/_app/immutable/chunks/C94TJqPI.js +1 -0
  561. package/dist/web/_app/immutable/chunks/C9PETsiT.js +1 -0
  562. package/dist/web/_app/immutable/chunks/CAuAQ81a2.js +1 -0
  563. package/dist/web/_app/immutable/chunks/CBnzSITi2.js +1 -0
  564. package/dist/web/_app/immutable/chunks/CC9p2p6w.js +1 -0
  565. package/dist/web/_app/immutable/chunks/CCLR-AJL2.js +185 -0
  566. package/dist/web/_app/immutable/chunks/CD0X7XPW.js +6 -0
  567. package/dist/web/_app/immutable/chunks/CDTjuBSm.js +1 -0
  568. package/dist/web/_app/immutable/chunks/CH-KYFIm.js +64 -0
  569. package/dist/web/_app/immutable/chunks/CHj8fObQ2.js +9 -0
  570. package/dist/web/_app/immutable/chunks/CK-UnH-n2.js +1 -0
  571. package/dist/web/_app/immutable/chunks/CKH5BEAh.js +1 -0
  572. package/dist/web/_app/immutable/chunks/CPSupDJp2.js +2 -0
  573. package/dist/web/_app/immutable/chunks/CSUrYOis.js +1 -0
  574. package/dist/web/_app/immutable/chunks/CYTTTma0.js +3 -0
  575. package/dist/web/_app/immutable/chunks/Cbaut_us2.js +1 -0
  576. package/dist/web/_app/immutable/chunks/CiIpSx5n2.js +1 -0
  577. package/dist/web/_app/immutable/chunks/CkLfUXwy.js +1 -0
  578. package/dist/web/_app/immutable/chunks/CokE4L8B2.js +1 -0
  579. package/dist/web/_app/immutable/chunks/CpLdnbh3.js +1 -0
  580. package/dist/web/_app/immutable/chunks/CvDrXJk9.js +1 -0
  581. package/dist/web/_app/immutable/chunks/D00MFB9_2.js +1 -0
  582. package/dist/web/_app/immutable/chunks/D41CbXmM.js +1 -0
  583. package/dist/web/_app/immutable/chunks/D5Hj-MPO2.js +1 -0
  584. package/dist/web/_app/immutable/chunks/D6nLEmZy2.js +1 -0
  585. package/dist/web/_app/immutable/chunks/D71ZlQBM2.js +2 -0
  586. package/dist/web/_app/immutable/chunks/D9ql9vXU.js +1 -0
  587. package/dist/web/_app/immutable/chunks/DAXW5iLp.js +1 -0
  588. package/dist/web/_app/immutable/chunks/DBzQ8LyQ2.js +2 -0
  589. package/dist/web/_app/immutable/chunks/DIb5rAPU2.js +1 -0
  590. package/dist/web/_app/immutable/chunks/DOmH4G9o.js +14 -0
  591. package/dist/web/_app/immutable/chunks/DQMWa2t72.js +1 -0
  592. package/dist/web/_app/immutable/chunks/DRsswX8S.js +1 -0
  593. package/dist/web/_app/immutable/chunks/DTCHhxD5.js +1 -0
  594. package/dist/web/_app/immutable/chunks/DUwgGBxa.js +1 -0
  595. package/dist/web/_app/immutable/chunks/DVcy5Pp82.js +1 -0
  596. package/dist/web/_app/immutable/chunks/DWygwzcO.js +1 -0
  597. package/dist/web/_app/immutable/chunks/DYXDRmoo.js +1 -0
  598. package/dist/web/_app/immutable/chunks/DZ-YyRVM.js +1 -0
  599. package/dist/web/_app/immutable/chunks/D_HVOuHV.js +1 -0
  600. package/dist/web/_app/immutable/chunks/DaoOQFPb.js +1 -0
  601. package/dist/web/_app/immutable/chunks/DdLkNvPf.js +23 -0
  602. package/dist/web/_app/immutable/chunks/DiMQob8p.js +1 -0
  603. package/dist/web/_app/immutable/chunks/DiQJ9DLy.js +1 -0
  604. package/dist/web/_app/immutable/chunks/DlORZqfE.js +1 -0
  605. package/dist/web/_app/immutable/chunks/DmeYl7eL.js +2 -0
  606. package/dist/web/_app/immutable/chunks/Dng3OcRd2.js +1 -0
  607. package/dist/web/_app/immutable/chunks/DoKMFfJd.js +1 -0
  608. package/dist/web/_app/immutable/chunks/DpzG2mVI2.js +1 -0
  609. package/dist/web/_app/immutable/chunks/DvJSCDuZ2.js +1 -0
  610. package/dist/web/_app/immutable/chunks/DvPuaTCl2.js +5 -0
  611. package/dist/web/_app/immutable/chunks/ItgU4ei5.js +1 -0
  612. package/dist/web/_app/immutable/chunks/JJDMrjj62.js +1 -0
  613. package/dist/web/_app/immutable/chunks/OTTMDMTW.js +1 -0
  614. package/dist/web/_app/immutable/chunks/RmiWpCin2.js +1 -0
  615. package/dist/web/_app/immutable/chunks/ZTiqmKJy2.js +1 -0
  616. package/dist/web/_app/immutable/chunks/ahC5WoXX2.js +1 -0
  617. package/dist/web/_app/immutable/chunks/e3SY5t2O2.js +2692 -0
  618. package/dist/web/_app/immutable/chunks/g09ZrevW.js +1 -0
  619. package/dist/web/_app/immutable/chunks/go8N1nWD2.js +3 -0
  620. package/dist/web/_app/immutable/chunks/nTAmRNKw2.js +1 -0
  621. package/dist/web/_app/immutable/chunks/oBQOivfH2.js +1 -0
  622. package/dist/web/_app/immutable/chunks/oUTRxd0f.js +1 -0
  623. package/dist/web/_app/immutable/chunks/s_7BJJYy.js +2 -0
  624. package/dist/web/_app/immutable/chunks/uuFM-ygL.js +1 -0
  625. package/dist/web/_app/immutable/chunks/v7me-ydg.js +2 -0
  626. package/dist/web/_app/immutable/chunks/xihTtKlq.js +1 -0
  627. package/dist/web/_app/immutable/chunks/zl16E4yc2.js +1 -0
  628. package/dist/web/_app/immutable/entry/app.CbuUtkoM.js +2 -0
  629. package/dist/web/_app/immutable/entry/start.DeZyKwOT.js +1 -0
  630. package/dist/web/_app/immutable/nodes/0.C4PMtdx9.js +2 -0
  631. package/dist/web/_app/immutable/nodes/1.BwVfPLMn.js +1 -0
  632. package/dist/web/_app/immutable/nodes/10.DPr_SYan.js +1 -0
  633. package/dist/web/_app/immutable/nodes/11.CJAmEchE.js +1 -0
  634. package/dist/web/_app/immutable/nodes/12.ChMWXPMk.js +1 -0
  635. package/dist/web/_app/immutable/nodes/13.8vOKq8ue.js +1 -0
  636. package/dist/web/_app/immutable/nodes/14.BOSNmj9h.js +1 -0
  637. package/dist/web/_app/immutable/nodes/2.7N4BLwhX.js +109 -0
  638. package/dist/web/_app/immutable/nodes/3.Cd-tx8Ej.js +2 -0
  639. package/dist/web/_app/immutable/nodes/4.DtuT-syy.js +14 -0
  640. package/dist/web/_app/immutable/nodes/5.YwNHOrkt.js +1 -0
  641. package/dist/web/_app/immutable/nodes/6.BSPlYQuq.js +1 -0
  642. package/dist/web/_app/immutable/nodes/7.DPXzwW0H.js +1 -0
  643. package/dist/web/_app/immutable/nodes/8.Dhpk7w3X.js +1 -0
  644. package/dist/web/_app/immutable/nodes/9.Dd4iAQN5.js +1 -0
  645. package/dist/web/_app/version.json +1 -1
  646. package/dist/web/favicon-16.png +0 -0
  647. package/dist/web/favicon-180.png +0 -0
  648. package/dist/web/favicon-32.png +0 -0
  649. package/dist/web/favicon-48.png +0 -0
  650. package/dist/web/favicon-512.png +0 -0
  651. package/dist/web/favicon.ico +0 -0
  652. package/dist/web/favicon.svg +8 -0
  653. package/dist/web/index.html +54 -19
  654. package/dist/web/studiograph-logomark.svg +29 -0
  655. package/package.json +34 -14
  656. package/dist/agent/skills/enrich-entities.md +0 -136
  657. package/dist/agent/skills/obsidian-source-setup.md +0 -246
  658. package/dist/agent/skills/skill-loader.d.ts +0 -48
  659. package/dist/agent/skills/skill-loader.js +0 -166
  660. package/dist/agent/skills/skill-loader.js.map +0 -1
  661. package/dist/agent/skills/sync-configuration.md +0 -119
  662. package/dist/agent/skills/sync-setup.md +0 -82
  663. package/dist/agent/tools/sync-tools.d.ts +0 -35
  664. package/dist/agent/tools/sync-tools.js +0 -992
  665. package/dist/agent/tools/sync-tools.js.map +0 -1
  666. package/dist/auth/github.d.ts +0 -56
  667. package/dist/auth/github.js +0 -169
  668. package/dist/auth/github.js.map +0 -1
  669. package/dist/cli/commands/access.d.ts +0 -11
  670. package/dist/cli/commands/access.js +0 -156
  671. package/dist/cli/commands/access.js.map +0 -1
  672. package/dist/cli/commands/app.d.ts +0 -7
  673. package/dist/cli/commands/app.js +0 -268
  674. package/dist/cli/commands/app.js.map +0 -1
  675. package/dist/cli/commands/auth.d.ts +0 -10
  676. package/dist/cli/commands/auth.js +0 -79
  677. package/dist/cli/commands/auth.js.map +0 -1
  678. package/dist/cli/commands/clear.d.ts +0 -5
  679. package/dist/cli/commands/clear.js +0 -36
  680. package/dist/cli/commands/clear.js.map +0 -1
  681. package/dist/cli/commands/collection.d.ts +0 -10
  682. package/dist/cli/commands/collection.js +0 -187
  683. package/dist/cli/commands/collection.js.map +0 -1
  684. package/dist/cli/commands/enrich.d.ts +0 -11
  685. package/dist/cli/commands/enrich.js +0 -135
  686. package/dist/cli/commands/enrich.js.map +0 -1
  687. package/dist/cli/commands/graphrag.d.ts +0 -12
  688. package/dist/cli/commands/graphrag.js +0 -122
  689. package/dist/cli/commands/graphrag.js.map +0 -1
  690. package/dist/cli/commands/join.d.ts +0 -9
  691. package/dist/cli/commands/join.js +0 -255
  692. package/dist/cli/commands/join.js.map +0 -1
  693. package/dist/cli/commands/members.d.ts +0 -11
  694. package/dist/cli/commands/members.js +0 -230
  695. package/dist/cli/commands/members.js.map +0 -1
  696. package/dist/cli/commands/org.d.ts +0 -10
  697. package/dist/cli/commands/org.js +0 -132
  698. package/dist/cli/commands/org.js.map +0 -1
  699. package/dist/cli/commands/provision.d.ts +0 -8
  700. package/dist/cli/commands/provision.js +0 -116
  701. package/dist/cli/commands/provision.js.map +0 -1
  702. package/dist/cli/commands/resolve.d.ts +0 -8
  703. package/dist/cli/commands/resolve.js +0 -85
  704. package/dist/cli/commands/resolve.js.map +0 -1
  705. package/dist/cli/commands/review.d.ts +0 -19
  706. package/dist/cli/commands/review.js +0 -128
  707. package/dist/cli/commands/review.js.map +0 -1
  708. package/dist/cli/commands/source.d.ts +0 -16
  709. package/dist/cli/commands/source.js +0 -159
  710. package/dist/cli/commands/source.js.map +0 -1
  711. package/dist/cli/commands/start.d.ts +0 -7
  712. package/dist/cli/commands/start.js +0 -589
  713. package/dist/cli/commands/start.js.map +0 -1
  714. package/dist/cli/commands/sync-collection.d.ts +0 -14
  715. package/dist/cli/commands/sync-collection.js +0 -355
  716. package/dist/cli/commands/sync-collection.js.map +0 -1
  717. package/dist/cli/commands/sync-entities.d.ts +0 -13
  718. package/dist/cli/commands/sync-entities.js +0 -242
  719. package/dist/cli/commands/sync-entities.js.map +0 -1
  720. package/dist/cli/commands/team.d.ts +0 -12
  721. package/dist/cli/commands/team.js +0 -185
  722. package/dist/cli/commands/team.js.map +0 -1
  723. package/dist/cli/commands/update.d.ts +0 -8
  724. package/dist/cli/commands/update.js +0 -155
  725. package/dist/cli/commands/update.js.map +0 -1
  726. package/dist/cli/commands/user.d.ts +0 -7
  727. package/dist/cli/commands/user.js +0 -153
  728. package/dist/cli/commands/user.js.map +0 -1
  729. package/dist/cli/scaffolding.d.ts +0 -12
  730. package/dist/cli/scaffolding.js +0 -397
  731. package/dist/cli/scaffolding.js.map +0 -1
  732. package/dist/cli/sync-review-interactive.d.ts +0 -31
  733. package/dist/cli/sync-review-interactive.js +0 -393
  734. package/dist/cli/sync-review-interactive.js.map +0 -1
  735. package/dist/core/migration-runner.d.ts +0 -42
  736. package/dist/core/migration-runner.js +0 -232
  737. package/dist/core/migration-runner.js.map +0 -1
  738. package/dist/core/migration-types.d.ts +0 -101
  739. package/dist/core/migration-types.js +0 -21
  740. package/dist/core/migration-types.js.map +0 -1
  741. package/dist/core/migrations/20260219-formalize-memory-location.d.ts +0 -2
  742. package/dist/core/migrations/20260219-formalize-memory-location.js +0 -35
  743. package/dist/core/migrations/20260219-formalize-memory-location.js.map +0 -1
  744. package/dist/core/migrations/20260220-add-workspace-metadata.d.ts +0 -12
  745. package/dist/core/migrations/20260220-add-workspace-metadata.js +0 -65
  746. package/dist/core/migrations/20260220-add-workspace-metadata.js.map +0 -1
  747. package/dist/core/migrations/20260220-add-workspace-readme.d.ts +0 -11
  748. package/dist/core/migrations/20260220-add-workspace-readme.js +0 -82
  749. package/dist/core/migrations/20260220-add-workspace-readme.js.map +0 -1
  750. package/dist/core/migrations/20260220-migrate-yaml-to-json.d.ts +0 -9
  751. package/dist/core/migrations/20260220-migrate-yaml-to-json.js +0 -64
  752. package/dist/core/migrations/20260220-migrate-yaml-to-json.js.map +0 -1
  753. package/dist/core/migrations/index.d.ts +0 -11
  754. package/dist/core/migrations/index.js +0 -23
  755. package/dist/core/migrations/index.js.map +0 -1
  756. package/dist/integrations/asana.d.ts +0 -26
  757. package/dist/integrations/asana.js +0 -78
  758. package/dist/integrations/asana.js.map +0 -1
  759. package/dist/integrations/figma-local.d.ts +0 -16
  760. package/dist/integrations/figma-local.js +0 -10
  761. package/dist/integrations/figma-local.js.map +0 -1
  762. package/dist/integrations/figma.d.ts +0 -17
  763. package/dist/integrations/figma.js +0 -17
  764. package/dist/integrations/figma.js.map +0 -1
  765. package/dist/integrations/granola.d.ts +0 -24
  766. package/dist/integrations/granola.js +0 -60
  767. package/dist/integrations/granola.js.map +0 -1
  768. package/dist/integrations/linear.d.ts +0 -14
  769. package/dist/integrations/linear.js +0 -80
  770. package/dist/integrations/linear.js.map +0 -1
  771. package/dist/integrations/paper-local.d.ts +0 -14
  772. package/dist/integrations/paper-local.js +0 -10
  773. package/dist/integrations/paper-local.js.map +0 -1
  774. package/dist/integrations/paper.d.ts +0 -2
  775. package/dist/integrations/paper.js +0 -10
  776. package/dist/integrations/paper.js.map +0 -1
  777. package/dist/integrations/pipedrive.d.ts +0 -26
  778. package/dist/integrations/pipedrive.js +0 -97
  779. package/dist/integrations/pipedrive.js.map +0 -1
  780. package/dist/integrations/registry.d.ts +0 -15
  781. package/dist/integrations/registry.js +0 -27
  782. package/dist/integrations/registry.js.map +0 -1
  783. package/dist/integrations/types.d.ts +0 -38
  784. package/dist/integrations/types.js +0 -9
  785. package/dist/integrations/types.js.map +0 -1
  786. package/dist/lib/lib/utils.d.ts +0 -2
  787. package/dist/lib/lib/utils.js +0 -6
  788. package/dist/lib/lib/utils.js.map +0 -1
  789. package/dist/mcp/connectors/asana.d.ts +0 -2
  790. package/dist/mcp/connectors/asana.js +0 -20
  791. package/dist/mcp/connectors/asana.js.map +0 -1
  792. package/dist/mcp/connectors/definitions.d.ts +0 -45
  793. package/dist/mcp/connectors/definitions.js +0 -32
  794. package/dist/mcp/connectors/definitions.js.map +0 -1
  795. package/dist/mcp/connectors/figma.d.ts +0 -5
  796. package/dist/mcp/connectors/figma.js +0 -21
  797. package/dist/mcp/connectors/figma.js.map +0 -1
  798. package/dist/mcp/connectors/gdrive.d.ts +0 -2
  799. package/dist/mcp/connectors/gdrive.js +0 -20
  800. package/dist/mcp/connectors/gdrive.js.map +0 -1
  801. package/dist/mcp/connectors/granola.d.ts +0 -2
  802. package/dist/mcp/connectors/granola.js +0 -12
  803. package/dist/mcp/connectors/granola.js.map +0 -1
  804. package/dist/mcp/connectors/linear.d.ts +0 -2
  805. package/dist/mcp/connectors/linear.js +0 -19
  806. package/dist/mcp/connectors/linear.js.map +0 -1
  807. package/dist/mcp/connectors/obsidian.d.ts +0 -2
  808. package/dist/mcp/connectors/obsidian.js +0 -19
  809. package/dist/mcp/connectors/obsidian.js.map +0 -1
  810. package/dist/mcp/connectors/pipedrive.d.ts +0 -2
  811. package/dist/mcp/connectors/pipedrive.js +0 -20
  812. package/dist/mcp/connectors/pipedrive.js.map +0 -1
  813. package/dist/mcp/connectors/slack.d.ts +0 -2
  814. package/dist/mcp/connectors/slack.js +0 -21
  815. package/dist/mcp/connectors/slack.js.map +0 -1
  816. package/dist/mcp/server-oauth-provider.d.ts +0 -56
  817. package/dist/mcp/server-oauth-provider.js +0 -142
  818. package/dist/mcp/server-oauth-provider.js.map +0 -1
  819. package/dist/server/chrome/chrome.css +0 -691
  820. package/dist/server/chrome/chrome.js +0 -374
  821. package/dist/server/collab-authority.d.ts +0 -70
  822. package/dist/server/collab-authority.js +0 -218
  823. package/dist/server/collab-authority.js.map +0 -1
  824. package/dist/server/collab.d.ts +0 -29
  825. package/dist/server/collab.js +0 -195
  826. package/dist/server/collab.js.map +0 -1
  827. package/dist/server/commit-scheduler.d.ts +0 -39
  828. package/dist/server/commit-scheduler.js +0 -113
  829. package/dist/server/commit-scheduler.js.map +0 -1
  830. package/dist/server/plugin-loader.d.ts +0 -53
  831. package/dist/server/plugin-loader.js +0 -155
  832. package/dist/server/plugin-loader.js.map +0 -1
  833. package/dist/server/routes/collab.d.ts +0 -6
  834. package/dist/server/routes/collab.js +0 -10
  835. package/dist/server/routes/collab.js.map +0 -1
  836. package/dist/server/routes/git-api.d.ts +0 -9
  837. package/dist/server/routes/git-api.js +0 -82
  838. package/dist/server/routes/git-api.js.map +0 -1
  839. package/dist/server/routes/meeting-ingest-api.d.ts +0 -15
  840. package/dist/server/routes/meeting-ingest-api.js +0 -323
  841. package/dist/server/routes/meeting-ingest-api.js.map +0 -1
  842. package/dist/server/routes/sync-api.d.ts +0 -26
  843. package/dist/server/routes/sync-api.js +0 -814
  844. package/dist/server/routes/sync-api.js.map +0 -1
  845. package/dist/server/routes/webhook.d.ts +0 -14
  846. package/dist/server/routes/webhook.js +0 -102
  847. package/dist/server/routes/webhook.js.map +0 -1
  848. package/dist/services/github-provisioner.d.ts +0 -36
  849. package/dist/services/github-provisioner.js +0 -126
  850. package/dist/services/github-provisioner.js.map +0 -1
  851. package/dist/services/github-service.d.ts +0 -99
  852. package/dist/services/github-service.js +0 -310
  853. package/dist/services/github-service.js.map +0 -1
  854. package/dist/services/insights.d.ts +0 -38
  855. package/dist/services/insights.js +0 -246
  856. package/dist/services/insights.js.map +0 -1
  857. package/dist/services/sync/collection-sync.d.ts +0 -60
  858. package/dist/services/sync/collection-sync.js +0 -509
  859. package/dist/services/sync/collection-sync.js.map +0 -1
  860. package/dist/services/sync/commit.d.ts +0 -60
  861. package/dist/services/sync/commit.js +0 -354
  862. package/dist/services/sync/commit.js.map +0 -1
  863. package/dist/services/sync/context-index.d.ts +0 -69
  864. package/dist/services/sync/context-index.js +0 -280
  865. package/dist/services/sync/context-index.js.map +0 -1
  866. package/dist/services/sync/data-fetcher.d.ts +0 -31
  867. package/dist/services/sync/data-fetcher.js +0 -12
  868. package/dist/services/sync/data-fetcher.js.map +0 -1
  869. package/dist/services/sync/derive.d.ts +0 -34
  870. package/dist/services/sync/derive.js +0 -164
  871. package/dist/services/sync/derive.js.map +0 -1
  872. package/dist/services/sync/enrichment-state.d.ts +0 -31
  873. package/dist/services/sync/enrichment-state.js +0 -63
  874. package/dist/services/sync/enrichment-state.js.map +0 -1
  875. package/dist/services/sync/enrichment.d.ts +0 -25
  876. package/dist/services/sync/enrichment.js +0 -121
  877. package/dist/services/sync/enrichment.js.map +0 -1
  878. package/dist/services/sync/entity-refresh.d.ts +0 -30
  879. package/dist/services/sync/entity-refresh.js +0 -275
  880. package/dist/services/sync/entity-refresh.js.map +0 -1
  881. package/dist/services/sync/frontmatter-extractor.d.ts +0 -40
  882. package/dist/services/sync/frontmatter-extractor.js +0 -279
  883. package/dist/services/sync/frontmatter-extractor.js.map +0 -1
  884. package/dist/services/sync/graph-match-state.d.ts +0 -33
  885. package/dist/services/sync/graph-match-state.js +0 -61
  886. package/dist/services/sync/graph-match-state.js.map +0 -1
  887. package/dist/services/sync/graph-match.d.ts +0 -53
  888. package/dist/services/sync/graph-match.js +0 -316
  889. package/dist/services/sync/graph-match.js.map +0 -1
  890. package/dist/services/sync/graphrag-client.d.ts +0 -43
  891. package/dist/services/sync/graphrag-client.js +0 -94
  892. package/dist/services/sync/graphrag-client.js.map +0 -1
  893. package/dist/services/sync/graphrag-config.d.ts +0 -16
  894. package/dist/services/sync/graphrag-config.js +0 -39
  895. package/dist/services/sync/graphrag-config.js.map +0 -1
  896. package/dist/services/sync/graphrag-context.d.ts +0 -14
  897. package/dist/services/sync/graphrag-context.js +0 -109
  898. package/dist/services/sync/graphrag-context.js.map +0 -1
  899. package/dist/services/sync/graphrag-indexer.d.ts +0 -30
  900. package/dist/services/sync/graphrag-indexer.js +0 -358
  901. package/dist/services/sync/graphrag-indexer.js.map +0 -1
  902. package/dist/services/sync/llm.d.ts +0 -32
  903. package/dist/services/sync/llm.js +0 -115
  904. package/dist/services/sync/llm.js.map +0 -1
  905. package/dist/services/sync/mcp-client.d.ts +0 -71
  906. package/dist/services/sync/mcp-client.js +0 -299
  907. package/dist/services/sync/mcp-client.js.map +0 -1
  908. package/dist/services/sync/model-factory.d.ts +0 -13
  909. package/dist/services/sync/model-factory.js +0 -38
  910. package/dist/services/sync/model-factory.js.map +0 -1
  911. package/dist/services/sync/name-quality.d.ts +0 -31
  912. package/dist/services/sync/name-quality.js +0 -60
  913. package/dist/services/sync/name-quality.js.map +0 -1
  914. package/dist/services/sync/output-schemas.d.ts +0 -92
  915. package/dist/services/sync/output-schemas.js +0 -43
  916. package/dist/services/sync/output-schemas.js.map +0 -1
  917. package/dist/services/sync/prompts.d.ts +0 -19
  918. package/dist/services/sync/prompts.js +0 -128
  919. package/dist/services/sync/prompts.js.map +0 -1
  920. package/dist/services/sync/reconciler.d.ts +0 -48
  921. package/dist/services/sync/reconciler.js +0 -294
  922. package/dist/services/sync/reconciler.js.map +0 -1
  923. package/dist/services/sync/rest-client.d.ts +0 -40
  924. package/dist/services/sync/rest-client.js +0 -100
  925. package/dist/services/sync/rest-client.js.map +0 -1
  926. package/dist/services/sync/source-config.d.ts +0 -67
  927. package/dist/services/sync/source-config.js +0 -304
  928. package/dist/services/sync/source-config.js.map +0 -1
  929. package/dist/services/sync/source-definitions/asana.d.ts +0 -14
  930. package/dist/services/sync/source-definitions/asana.js +0 -42
  931. package/dist/services/sync/source-definitions/asana.js.map +0 -1
  932. package/dist/services/sync/source-definitions/definitions.d.ts +0 -8
  933. package/dist/services/sync/source-definitions/definitions.js +0 -8
  934. package/dist/services/sync/source-definitions/definitions.js.map +0 -1
  935. package/dist/services/sync/source-definitions/gdrive.d.ts +0 -16
  936. package/dist/services/sync/source-definitions/gdrive.js +0 -68
  937. package/dist/services/sync/source-definitions/gdrive.js.map +0 -1
  938. package/dist/services/sync/source-definitions/granola.d.ts +0 -2
  939. package/dist/services/sync/source-definitions/granola.js +0 -21
  940. package/dist/services/sync/source-definitions/granola.js.map +0 -1
  941. package/dist/services/sync/source-definitions/linear.d.ts +0 -2
  942. package/dist/services/sync/source-definitions/linear.js +0 -62
  943. package/dist/services/sync/source-definitions/linear.js.map +0 -1
  944. package/dist/services/sync/source-definitions/obsidian.d.ts +0 -2
  945. package/dist/services/sync/source-definitions/obsidian.js +0 -55
  946. package/dist/services/sync/source-definitions/obsidian.js.map +0 -1
  947. package/dist/services/sync/source-definitions/pipedrive.d.ts +0 -2
  948. package/dist/services/sync/source-definitions/pipedrive.js +0 -43
  949. package/dist/services/sync/source-definitions/pipedrive.js.map +0 -1
  950. package/dist/services/sync/staging.d.ts +0 -53
  951. package/dist/services/sync/staging.js +0 -130
  952. package/dist/services/sync/staging.js.map +0 -1
  953. package/dist/services/sync/structured-extractor.d.ts +0 -57
  954. package/dist/services/sync/structured-extractor.js +0 -584
  955. package/dist/services/sync/structured-extractor.js.map +0 -1
  956. package/dist/services/sync/sync-runner.d.ts +0 -32
  957. package/dist/services/sync/sync-runner.js +0 -195
  958. package/dist/services/sync/sync-runner.js.map +0 -1
  959. package/dist/services/sync/sync-state.d.ts +0 -43
  960. package/dist/services/sync/sync-state.js +0 -154
  961. package/dist/services/sync/sync-state.js.map +0 -1
  962. package/dist/services/sync/types.d.ts +0 -381
  963. package/dist/services/sync/types.js +0 -8
  964. package/dist/services/sync/types.js.map +0 -1
  965. package/dist/services/sync/unstructured-extractor.d.ts +0 -27
  966. package/dist/services/sync/unstructured-extractor.js +0 -185
  967. package/dist/services/sync/unstructured-extractor.js.map +0 -1
  968. package/dist/utils/merge-resolver.d.ts +0 -59
  969. package/dist/utils/merge-resolver.js +0 -303
  970. package/dist/utils/merge-resolver.js.map +0 -1
  971. package/dist/utils/workspace-config.d.ts +0 -8
  972. package/dist/utils/workspace-config.js +0 -22
  973. package/dist/utils/workspace-config.js.map +0 -1
  974. package/dist/web/_app/immutable/assets/0.DdizXOKi.css +0 -1
  975. package/dist/web/_app/immutable/assets/10.BUrHkYry.css +0 -1
  976. package/dist/web/_app/immutable/assets/2.n7-yW_Fs.css +0 -1
  977. package/dist/web/_app/immutable/assets/3.BtaZagnv.css +0 -1
  978. package/dist/web/_app/immutable/assets/4.DT7X0x5S.css +0 -1
  979. package/dist/web/_app/immutable/assets/5.DFeGxLYf.css +0 -1
  980. package/dist/web/_app/immutable/assets/6.CquhUpOu.css +0 -1
  981. package/dist/web/_app/immutable/assets/7.CjbDRbuG.css +0 -1
  982. package/dist/web/_app/immutable/assets/8.S1QdUGNM.css +0 -1
  983. package/dist/web/_app/immutable/assets/9.CT4xL4K3.css +0 -1
  984. package/dist/web/_app/immutable/assets/Copy.FS8bdfxr.css +0 -1
  985. package/dist/web/_app/immutable/assets/Toaster.rN8r_Hzv.css +0 -1
  986. package/dist/web/_app/immutable/assets/ViewToolbar.D-QW2oKe.css +0 -1
  987. package/dist/web/_app/immutable/assets/index.D6tMDJWk.css +0 -1
  988. package/dist/web/_app/immutable/chunks/37NdyH6G.js +0 -32
  989. package/dist/web/_app/immutable/chunks/4Hhzb6pv.js +0 -1
  990. package/dist/web/_app/immutable/chunks/72bZS3G9.js +0 -1
  991. package/dist/web/_app/immutable/chunks/7S2LGqoP.js +0 -2
  992. package/dist/web/_app/immutable/chunks/B-7hHz7B.js +0 -1
  993. package/dist/web/_app/immutable/chunks/B340SUKR.js +0 -1
  994. package/dist/web/_app/immutable/chunks/BB_5th5W.js +0 -3383
  995. package/dist/web/_app/immutable/chunks/BDoWDZaG.js +0 -1
  996. package/dist/web/_app/immutable/chunks/BFyl1ZHO.js +0 -1
  997. package/dist/web/_app/immutable/chunks/BS2rFAVq.js +0 -1
  998. package/dist/web/_app/immutable/chunks/BSsDC_p4.js +0 -1
  999. package/dist/web/_app/immutable/chunks/BTX2Jr8_.js +0 -1
  1000. package/dist/web/_app/immutable/chunks/BWNY8zw6.js +0 -2
  1001. package/dist/web/_app/immutable/chunks/BdPUVvAi.js +0 -1
  1002. package/dist/web/_app/immutable/chunks/BmsJVLyy.js +0 -1
  1003. package/dist/web/_app/immutable/chunks/BoAosqzE.js +0 -1
  1004. package/dist/web/_app/immutable/chunks/Bs-tL5OR.js +0 -1
  1005. package/dist/web/_app/immutable/chunks/Bsacq2UX.js +0 -1
  1006. package/dist/web/_app/immutable/chunks/BuewkNdS.js +0 -4
  1007. package/dist/web/_app/immutable/chunks/ByunV8un.js +0 -1
  1008. package/dist/web/_app/immutable/chunks/C7VoTosa.js +0 -2
  1009. package/dist/web/_app/immutable/chunks/CDV5Q7RN.js +0 -1
  1010. package/dist/web/_app/immutable/chunks/CJ8__CWt.js +0 -1
  1011. package/dist/web/_app/immutable/chunks/CJUKmosC.js +0 -5
  1012. package/dist/web/_app/immutable/chunks/CSVbGg_b.js +0 -5
  1013. package/dist/web/_app/immutable/chunks/CT6oIU-C.js +0 -1
  1014. package/dist/web/_app/immutable/chunks/CTRxNfZk.js +0 -1
  1015. package/dist/web/_app/immutable/chunks/CWQRQXxy.js +0 -1
  1016. package/dist/web/_app/immutable/chunks/CXH6iFbA.js +0 -1
  1017. package/dist/web/_app/immutable/chunks/CX_61fY8.js +0 -5
  1018. package/dist/web/_app/immutable/chunks/CYrVHOHA.js +0 -1
  1019. package/dist/web/_app/immutable/chunks/CdeYcEuU.js +0 -1
  1020. package/dist/web/_app/immutable/chunks/CmCtpZ1Y.js +0 -1
  1021. package/dist/web/_app/immutable/chunks/CqkleIqs.js +0 -1
  1022. package/dist/web/_app/immutable/chunks/CrdV0ttd.js +0 -1
  1023. package/dist/web/_app/immutable/chunks/CscoF-YL.js +0 -18
  1024. package/dist/web/_app/immutable/chunks/Cx4YdFMk.js +0 -1
  1025. package/dist/web/_app/immutable/chunks/D7ZqAKi9.js +0 -3
  1026. package/dist/web/_app/immutable/chunks/DGbA7IXh.js +0 -2
  1027. package/dist/web/_app/immutable/chunks/DbbB6Up5.js +0 -1
  1028. package/dist/web/_app/immutable/chunks/DnL9f0lc.js +0 -7
  1029. package/dist/web/_app/immutable/chunks/DtM5cEDu.js +0 -1
  1030. package/dist/web/_app/immutable/chunks/Eqahi7_S.js +0 -2
  1031. package/dist/web/_app/immutable/chunks/FFlLQIiS.js +0 -1
  1032. package/dist/web/_app/immutable/chunks/O9Eb9k00.js +0 -1
  1033. package/dist/web/_app/immutable/chunks/PPVm8Dsz.js +0 -1
  1034. package/dist/web/_app/immutable/chunks/QF8MAmnc.js +0 -1
  1035. package/dist/web/_app/immutable/chunks/UjzxCpxn.js +0 -1
  1036. package/dist/web/_app/immutable/chunks/WSUKABI_.js +0 -1
  1037. package/dist/web/_app/immutable/chunks/Y90OgS-9.js +0 -1
  1038. package/dist/web/_app/immutable/chunks/cLyZlkXv.js +0 -1
  1039. package/dist/web/_app/immutable/chunks/iDtAARVW.js +0 -2
  1040. package/dist/web/_app/immutable/chunks/oC_W7W7p.js +0 -1
  1041. package/dist/web/_app/immutable/chunks/rJdWIgh-.js +0 -1
  1042. package/dist/web/_app/immutable/chunks/tvOsokaa.js +0 -1
  1043. package/dist/web/_app/immutable/chunks/uLEXaPwE.js +0 -1
  1044. package/dist/web/_app/immutable/chunks/vSaooFHB.js +0 -1
  1045. package/dist/web/_app/immutable/entry/app.DAR-mtfg.js +0 -2
  1046. package/dist/web/_app/immutable/entry/start.xDnyRaR1.js +0 -1
  1047. package/dist/web/_app/immutable/nodes/0.D0aV-Zro.js +0 -2
  1048. package/dist/web/_app/immutable/nodes/1.C7QoM8qD.js +0 -1
  1049. package/dist/web/_app/immutable/nodes/10.DrDOAjOD.js +0 -1
  1050. package/dist/web/_app/immutable/nodes/2.B0OgNuKW.js +0 -168
  1051. package/dist/web/_app/immutable/nodes/3.BZ4qHGZi.js +0 -1
  1052. package/dist/web/_app/immutable/nodes/4.hm-mxuuj.js +0 -12
  1053. package/dist/web/_app/immutable/nodes/5.Bdbt4fjx.js +0 -4
  1054. package/dist/web/_app/immutable/nodes/6.BXrCI40c.js +0 -2
  1055. package/dist/web/_app/immutable/nodes/7.Chd4i9bm.js +0 -2
  1056. package/dist/web/_app/immutable/nodes/8.B8SF-aNs.js +0 -1
  1057. package/dist/web/_app/immutable/nodes/9.YDeu-RnO.js +0 -1
@@ -2,135 +2,58 @@
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
- }
97
- 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)
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, 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 { log } from '../../utils/log.js';
19
+ export async function registerGraphApiRoutes(fastify, workspaceManager, authService, wsHub, sessionManager, yjsManager) {
20
+ // ARCHITECTURAL BOUNDARY: every per-route graph resolution flows through
21
+ // this service so the act of getting a graph IS the access check. Do not
22
+ // call `workspaceManager.getGraph(repo)` from a request-scoped handler —
23
+ // use `accessService.graphFor*(reqUser(req), repo)` instead. A security
24
+ // audit (2026-05) flagged the previous opt-in pattern (route had to
25
+ // remember `hasRepoAccess()` before `getGraph()`) as an architectural
26
+ // risk; if you find yourself reaching for the raw WM here, that's the
27
+ // smell the audit was about.
28
+ const accessService = new WorkspaceAccessService(workspaceManager, authService);
29
+ const graphForRead = (req, reply, repoName) => resolveGraphForRead(accessService, req, reply, repoName);
30
+ const graphForWrite = (req, reply, repoName) => resolveGraphForWrite(accessService, req, reply, repoName);
31
+ const graphForAdmin = (req, reply, repoName) => resolveGraphForAdmin(accessService, req, reply, repoName);
32
+ // POST /api/repos — any authenticated user can create a folder.
33
+ // The creator is granted folder-admin access automatically.
34
+ // Shared logic lives in services/folder-creation.ts so the MCP tool
35
+ // create_repo behaves identically.
99
36
  fastify.post('/api/repos', async (req, reply) => {
100
37
  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
- }
38
+ const { name, display_name, description } = req.body || {};
109
39
  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
- });
40
+ const repo = await createSharedFolder(workspaceManager, authService, { name, display_name, description }, user, wsHub);
124
41
  return reply.status(201).send(repo);
125
42
  }
126
43
  catch (err) {
127
- return reply.status(409).send({ error: err.message });
44
+ if (err instanceof FolderCreationError) {
45
+ const status = err.code === 'invalid_name' ? 400
46
+ : err.code === 'duplicate' ? 409
47
+ : 500;
48
+ return reply.status(status).send({ error: err.message });
49
+ }
50
+ return reply.status(500).send({ error: err?.message ?? String(err) });
128
51
  }
129
52
  });
130
- // DELETE /api/repos/:repo — remove a collection and delete its files (admin only)
53
+ // DELETE /api/repos/:repo — folder admin or workspace owner only.
131
54
  fastify.delete('/api/repos/:repo', async (req, reply) => {
132
- if (!isAdmin(req)) {
133
- return reply.status(403).send({ error: 'Admin access required' });
55
+ if (!isFolderAdminOrOwner(req, req.params.repo, workspaceManager, authService)) {
56
+ return reply.status(403).send({ error: 'Folder admin access required' });
134
57
  }
135
58
  const { repo } = req.params;
136
59
  try {
@@ -138,14 +61,14 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
138
61
  const config = await workspace.loadConfig();
139
62
  const repoIndex = config.repos.findIndex(r => r.name === repo);
140
63
  if (repoIndex === -1) {
141
- return reply.status(404).send({ error: `Collection "${repo}" not found` });
64
+ return reply.status(404).send({ error: `Folder \u201C${repo}\u201D not found` });
142
65
  }
143
66
  const repoConfig = config.repos[repoIndex];
144
67
  const repoPath = join(workspaceManager['workspacePath'], repoConfig.path);
145
68
  // Remove from config
146
69
  config.repos.splice(repoIndex, 1);
147
70
  workspace.writeConfig(config);
148
- workspace.pushWorkspaceConfig(`Remove collection: ${repo}`);
71
+ workspace.pushWorkspaceConfig(`Remove folder: ${repo}`);
149
72
  // Delete files
150
73
  const { rmSync } = await import('fs');
151
74
  if (existsSync(repoPath)) {
@@ -153,7 +76,7 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
153
76
  }
154
77
  workspaceManager.reloadConfig();
155
78
  wsHub?.broadcast({
156
- type: 'collection_deleted', repo,
79
+ type: 'folder_deleted', repo,
157
80
  actor: req.user?.displayName ?? 'API',
158
81
  timestamp: new Date().toISOString(),
159
82
  });
@@ -163,16 +86,16 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
163
86
  return reply.status(500).send({ error: err.message });
164
87
  }
165
88
  });
166
- // PATCH /api/repos/:repo — update a collection (rename, sync rules) (admin only)
89
+ // PATCH /api/repos/:repo — rename/update sync rules. Folder admin or workspace owner only.
167
90
  fastify.patch('/api/repos/:repo', async (req, reply) => {
168
- if (!isAdmin(req)) {
169
- return reply.status(403).send({ error: 'Admin access required' });
91
+ if (!isFolderAdminOrOwner(req, req.params.repo, workspaceManager, authService)) {
92
+ return reply.status(403).send({ error: 'Folder admin access required' });
170
93
  }
171
94
  const { repo } = req.params;
172
95
  const body = req.body || {};
173
96
  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' });
97
+ if ('personal' in rest) {
98
+ return reply.status(400).send({ error: 'The personal flag is immutable after creation' });
176
99
  }
177
100
  if (newName === undefined && newSync === undefined && newDisplayName === undefined) {
178
101
  return reply.status(400).send({ error: 'Nothing to update' });
@@ -185,12 +108,12 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
185
108
  const config = await workspace.loadConfig();
186
109
  const repoConfig = config.repos.find(r => r.name === repo);
187
110
  if (!repoConfig) {
188
- return reply.status(404).send({ error: `Collection "${repo}" not found` });
111
+ return reply.status(404).send({ error: `Folder \u201C${repo}\u201D not found` });
189
112
  }
190
113
  // Handle rename
191
114
  if (newName !== undefined) {
192
115
  if (config.repos.some(r => r.name === newName)) {
193
- return reply.status(409).send({ error: `Collection "${newName}" already exists` });
116
+ return reply.status(409).send({ error: `Folder \u201C${newName}\u201D already exists` });
194
117
  }
195
118
  const wsPath = workspaceManager['workspacePath'];
196
119
  const oldPath = join(wsPath, repoConfig.path);
@@ -209,8 +132,8 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
209
132
  if (existsSync(markerPath)) {
210
133
  writeFS(markerPath, JSON.stringify({ repo_name: newName }, null, 2), 'utf-8');
211
134
  }
212
- // Update collection_access to follow the rename
213
- authService.renameCollection(repo, newName);
135
+ // Update folder_access to follow the rename
136
+ authService.renameFolder(repo, newName);
214
137
  }
215
138
  // Handle display name update (without rename)
216
139
  if (newDisplayName !== undefined && newName === undefined) {
@@ -221,7 +144,7 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
221
144
  repoConfig.sync = newSync.length > 0 ? newSync : undefined;
222
145
  }
223
146
  workspace.writeConfig(config);
224
- workspace.pushWorkspaceConfig(newName !== undefined ? `Rename collection: ${repo} → ${newName}` : `Update collection: ${repo}`);
147
+ workspace.pushWorkspaceConfig(newName !== undefined ? `Rename folder: ${repo} → ${newName}` : `Update folder: ${repo}`);
225
148
  workspaceManager.reloadConfig();
226
149
  return reply.send(repoConfig);
227
150
  }
@@ -229,50 +152,124 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
229
152
  return reply.status(500).send({ error: err.message });
230
153
  }
231
154
  });
232
- // GET /api/repos — list accessible repos (re-reads config to pick up CLI changes)
155
+ // GET /api/repos — list accessible repos (re-reads config to pick up CLI changes).
156
+ // Augments each repo with the caller's folder role ('admin' | 'editor'),
157
+ // member_count, entity_types (distinct types present), and folders (the
158
+ // flat list of nested-folder paths in the repo, used by the sidebar to
159
+ // render the active-path expansion).
233
160
  fastify.get('/api/repos', async (req, reply) => {
234
161
  workspaceManager.reloadConfig();
235
162
  const repos = getAccessibleRepos(req, workspaceManager, authService);
236
- return reply.send(repos);
163
+ const user = req.user;
164
+ const enriched = repos.map(r => {
165
+ const memberCount = authService.getFolderMembers(r.name).length;
166
+ const callerRole = user ? authService.getFolderRole(user.id, r.name) : null;
167
+ let entityTypes = [];
168
+ // `folders` carries the per-folder display_name override (when one
169
+ // exists in the folder's .studiograph-folder.json sidecar); the client
170
+ // falls back to toDisplayName(slug) when display_name is absent.
171
+ let folders = [];
172
+ try {
173
+ // Direct WM access OK here — `repos` came from getAccessibleRepos
174
+ // above, so every iteration is over a folder the caller can read.
175
+ const graph = workspaceManager.getGraph(r.name);
176
+ entityTypes = graph.listEntityTypes();
177
+ folders = graph.listFolders().map(f => {
178
+ const entry = { path: f.path };
179
+ if (f.display_name)
180
+ entry.display_name = f.display_name;
181
+ if (f.id)
182
+ entry.id = f.id;
183
+ return entry;
184
+ });
185
+ }
186
+ catch { /* repo not mounted */ }
187
+ return {
188
+ ...r,
189
+ caller_role: callerRole,
190
+ member_count: memberCount,
191
+ entity_types: entityTypes,
192
+ folders,
193
+ };
194
+ });
195
+ return reply.send(enriched);
196
+ });
197
+ // GET /api/admin/folders — workspace-owner-only metadata list of every folder
198
+ // (name, display_name, creator, member count). No content access implied —
199
+ // owners still need to join a folder to read its entries.
200
+ fastify.get('/api/admin/folders', async (req, reply) => {
201
+ const user = req.user;
202
+ if (user && !isWorkspaceOwner(user.role)) {
203
+ return reply.status(403).send({ error: 'Workspace owner access required' });
204
+ }
205
+ workspaceManager.reloadConfig();
206
+ const all = workspaceManager.getAllRepoConfigs();
207
+ const result = all.map(r => {
208
+ const members = authService.getFolderMembers(r.name);
209
+ const admins = members.filter(m => m.role === 'admin');
210
+ const creator = r.owner_id
211
+ ? members.find(m => m.user.id === r.owner_id)?.user
212
+ : admins[0]?.user;
213
+ let entryCount = 0;
214
+ // Direct WM access OK — this endpoint is owner-only (gated above) and
215
+ // returns metadata for every folder the workspace owner needs to see
216
+ // for admin purposes. No content is leaked: only entry counts.
217
+ try {
218
+ entryCount = workspaceManager.getGraph(r.name).search({}).length;
219
+ }
220
+ catch { /* skip */ }
221
+ return {
222
+ name: r.name,
223
+ display_name: r.display_name ?? r.name,
224
+ description: r.description,
225
+ member_count: members.length,
226
+ admin_count: admins.length,
227
+ entry_count: entryCount,
228
+ creator: creator ? { id: creator.id, displayName: creator.displayName, email: creator.email } : null,
229
+ caller_is_member: user ? members.some(m => m.user.id === user.id) : true,
230
+ };
231
+ });
232
+ return reply.send(result);
237
233
  });
234
+ // Build the lightweight summary objects used by /entities?summary=true.
235
+ // Extracted so the per-repo and bulk endpoints share the same shape.
236
+ const EXCLUDED_SUMMARY_FIELDS = new Set([
237
+ 'entity_type', 'entity_id', 'created_by', 'updated_by',
238
+ 'description', 'content', 'context', 'rationale', 'alternatives', 'consequences',
239
+ 'steps', 'guidelines', 'bio', 'agenda', 'summary', 'decision', 'action_items',
240
+ ]);
241
+ function buildSummaries(entities, accessibleIds) {
242
+ return entities.map(e => {
243
+ const extra = Object.fromEntries(Object.entries(e.data ?? {}).filter(([k, v]) => !EXCLUDED_SUMMARY_FIELDS.has(k) && k !== 'name' && k !== 'created_at' && k !== 'updated_at' &&
244
+ (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || Array.isArray(v) || v instanceof Date)));
245
+ return {
246
+ id: e.id,
247
+ entityType: e.entityType,
248
+ folder_path: e.folder_path ?? '',
249
+ data: {
250
+ name: e.data.name,
251
+ created_at: e.data.created_at,
252
+ updated_at: e.data.updated_at,
253
+ ...extra,
254
+ },
255
+ document: { wikilinks: filterWikilinks(e.document?.wikilinks ?? [], accessibleIds) },
256
+ };
257
+ });
258
+ }
238
259
  // GET /api/repos/:repo/entities — list all entities in a repo
239
260
  // ?summary=true returns lightweight objects (no content, minimal data) for sidebar/graph
240
261
  fastify.get('/api/repos/:repo/entities', async (req, reply) => {
241
262
  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
- }
263
+ // Access enforced by graphForRead \u2014 do not call workspaceManager.getGraph
264
+ // directly here. The audit (2026-05) flagged direct WM access from routes
265
+ // as the primary architectural risk; the service is the gate.
266
+ const graph = graphForRead(req, reply, repo);
267
+ if (!graph)
268
+ return;
252
269
  const entities = graph.search({});
253
270
  const accessibleIds = getAccessibleEntityIds(req, workspaceManager, authService);
254
271
  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);
272
+ return reply.send(buildSummaries(entities, accessibleIds));
276
273
  }
277
274
  // Full response — filter wikilinks in document
278
275
  const filtered = entities.map(e => {
@@ -282,19 +279,57 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
282
279
  });
283
280
  return reply.send(filtered);
284
281
  });
285
- // GET /api/repos/:repo/entities/:typelist entities of a type
286
- fastify.get('/api/repos/:repo/entities/:type', async (req, reply) => {
287
- const { repo, type } = req.params;
288
- if (!hasRepoAccess(req, repo, workspaceManager, authService)) {
289
- return reply.status(404).send({ error: `Collection "${repo}" not found` });
282
+ // GET /api/workspace/entities?summary=truebulk entity summaries for ALL
283
+ // accessible repos in a single round-trip. Replaces 15+ parallel
284
+ // /api/repos/:repo/entities?summary=true calls. Important on dev where each
285
+ // request has Vite middleware overhead and the browser's HTTP/1.1 6-slot
286
+ // limit serializes them, blocking other navigation fetches.
287
+ // Response shape: `{ [repoName]: EntitySummary[] }`.
288
+ fastify.get('/api/workspace/entities', async (req, reply) => {
289
+ // Mirror the per-repo endpoint's contract: only summary=true is supported
290
+ // here. Returning full bodies for the whole workspace would be huge and
291
+ // isn't a use case we have today.
292
+ if (req.query.summary !== 'true') {
293
+ return reply.status(400).send({ error: 'summary=true is required for the bulk entities endpoint' });
290
294
  }
291
- let graph;
292
- try {
293
- graph = workspaceManager.getGraph(repo);
295
+ const repos = getAccessibleRepos(req, workspaceManager, authService);
296
+ // Single-pass: collect the per-repo entity arrays AND the accessible-ids
297
+ // set in one walk. The previous shape called graph.search({}) twice per
298
+ // repo (once via getAccessibleEntityIds, once in this loop), doubling
299
+ // disk I/O on the most-trafficked endpoint.
300
+ const user = reqUser(req);
301
+ const collectIds = user !== undefined; // unauthenticated → no wikilink filtering
302
+ const accessibleIds = collectIds ? new Set() : null;
303
+ const perRepoEntities = [];
304
+ for (const r of repos) {
305
+ let graph;
306
+ // Direct WM access OK — `repos` is the access-filtered list above.
307
+ try {
308
+ graph = workspaceManager.getGraph(r.name);
309
+ }
310
+ catch {
311
+ continue;
312
+ } // skip unmounted repos rather than failing the whole request
313
+ const entities = graph.search({});
314
+ perRepoEntities.push({ name: r.name, entities });
315
+ if (accessibleIds) {
316
+ for (const e of entities)
317
+ accessibleIds.add(e.id);
318
+ }
294
319
  }
295
- catch (err) {
296
- return reply.status(404).send({ error: err.message });
320
+ const result = {};
321
+ for (const { name, entities } of perRepoEntities) {
322
+ result[name] = buildSummaries(entities, accessibleIds);
297
323
  }
324
+ return reply.send(result);
325
+ });
326
+ // GET /api/repos/:repo/entities/:type — list entities of a type
327
+ fastify.get('/api/repos/:repo/entities/:type', async (req, reply) => {
328
+ const { repo, type } = req.params;
329
+ // Access enforced by graphForRead (WorkspaceAccessService).
330
+ const graph = graphForRead(req, reply, repo);
331
+ if (!graph)
332
+ return;
298
333
  const entities = graph.search({ entityType: type });
299
334
  const accessibleIds = getAccessibleEntityIds(req, workspaceManager, authService);
300
335
  const filtered = entities.map(e => {
@@ -307,17 +342,20 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
307
342
  // GET /api/repos/:repo/entities/:type/:id — get single entity
308
343
  fastify.get('/api/repos/:repo/entities/:type/:id', async (req, reply) => {
309
344
  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;
345
+ // Access enforced by graphForRead (WorkspaceAccessService).
346
+ const graph = graphForRead(req, reply, repo);
347
+ if (!graph)
348
+ return;
349
+ let entity;
314
350
  try {
315
- graph = workspaceManager.getGraph(repo);
351
+ entity = graph.get(type, id);
316
352
  }
317
353
  catch (err) {
318
- return reply.status(404).send({ error: err.message });
354
+ return reply.status(404).send({ error: err.message ?? `Entity not found: ${type}/${id}` });
319
355
  }
320
- const entity = graph.get(type, id);
356
+ // graph.get may also return null for a missing entity without throwing
357
+ // (depends on the backing graph implementation). Treat that as 404 so
358
+ // we don't blow up on the wikilinks access below.
321
359
  if (!entity) {
322
360
  return reply.status(404).send({ error: `Entity not found: ${type}/${id}` });
323
361
  }
@@ -330,20 +368,128 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
330
368
  }
331
369
  return reply.send(entity);
332
370
  });
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;
371
+ // GET /api/repos/:repo/entities/:type/:id/historyentity commit history
372
+ fastify.get('/api/repos/:repo/entities/:type/:id/history', async (req, reply) => {
373
+ const { repo, type, id } = req.params;
374
+ // Access enforced by graphForRead (WorkspaceAccessService).
375
+ const graph = graphForRead(req, reply, repo);
376
+ if (!graph)
377
+ return;
378
+ const gitService = graph.getGitService();
379
+ if (!gitService) {
380
+ return reply.send([]);
381
+ }
382
+ const limit = Math.min(parseInt(req.query.limit ?? '20', 10) || 20, 50);
383
+ // Resolve the actual on-disk path via graph.get() — it walks nested
384
+ // folders to find the file. The previous hardcoded `${plural}/${id}.md`
385
+ // missed any entry not at the legacy flat path (entries created in a
386
+ // sub-folder, MCP entries that landed elsewhere, etc.) and silently
387
+ // returned an empty history. Convert absolute path → repo-relative
388
+ // for git log.
389
+ let filePath;
341
390
  try {
342
- graph = workspaceManager.getGraph(repo);
391
+ const entity = graph.get(type, id);
392
+ const repoRoot = graph.getRepoPath();
393
+ filePath = entity.path.startsWith(repoRoot + '/')
394
+ ? entity.path.slice(repoRoot.length + 1)
395
+ : entity.path;
343
396
  }
344
397
  catch (err) {
345
398
  return reply.status(404).send({ error: err.message });
346
399
  }
400
+ try {
401
+ const history = gitService.getFileHistory(filePath, limit);
402
+ // Opt-in per-commit change summary for the History panel. Off by
403
+ // default so the eager gating fetch on every entity-page load stays
404
+ // cheap; the panel requests it only when expanded.
405
+ if (req.query.changes === 'true') {
406
+ return reply.send(history.map((c) => ({ ...c, changes: graph.getCommitChange(filePath, c.hash) })));
407
+ }
408
+ return reply.send(history);
409
+ }
410
+ catch {
411
+ return reply.send([]);
412
+ }
413
+ });
414
+ // POST /api/repos/:repo/entities/:type/:id/restore — restore an entry to
415
+ // its content at a past commit. Single-entity scope. Editor-or-admin role
416
+ // required (canWriteRepo). Returns 200 with the restore result, 404 if
417
+ // the entry doesn't exist at that commit, 409 PATH_CONFLICT if a different
418
+ // entry now occupies the target path, 403 if the caller lacks write access.
419
+ fastify.post('/api/repos/:repo/entities/:type/:id/restore', async (req, reply) => {
420
+ const { repo, type, id } = req.params;
421
+ const { commitHash } = req.body ?? {};
422
+ if (!commitHash || typeof commitHash !== 'string') {
423
+ return reply.status(400).send({ error: 'commitHash is required' });
424
+ }
425
+ // Access enforced by graphForWrite: 404 if no read access, 403 if no write.
426
+ const graph = graphForWrite(req, reply, repo);
427
+ if (!graph)
428
+ return;
429
+ // Flush any deferred autosave for this repo first — otherwise a restore
430
+ // could overwrite a pending (uncommitted) edit on disk and commit only
431
+ // the restored version, silently discarding the in-flight work. Matches
432
+ // rename/move/change-type, which all flush before mutating.
433
+ if (sessionManager)
434
+ await sessionManager.flush(repo);
435
+ try {
436
+ const result = await graph.restoreFile(type, id, commitHash, reqGitUser(req));
437
+ wsHub?.broadcast({
438
+ type: 'entity_change',
439
+ action: result.action === 'recreate' ? 'created' : 'updated',
440
+ repo,
441
+ entityType: type,
442
+ entityId: id,
443
+ actor: reqUser(req)?.displayName ?? 'API',
444
+ source: 'api',
445
+ timestamp: new Date().toISOString(),
446
+ });
447
+ return reply.send({
448
+ success: true,
449
+ action: result.action,
450
+ path: result.path,
451
+ });
452
+ }
453
+ catch (err) {
454
+ // PathConflictError carries an error code distinct from generic
455
+ // failures so the UI can render an actionable conflict message.
456
+ if (err?.code === 'PATH_CONFLICT' && err?.occupiedBy) {
457
+ return reply.status(409).send({
458
+ error: 'PATH_CONFLICT',
459
+ path: err.path,
460
+ occupiedBy: err.occupiedBy,
461
+ });
462
+ }
463
+ const message = err instanceof Error ? err.message : 'Restore failed';
464
+ // "not found at commit" / "did not exist at commit" → 404 so the
465
+ // client can show "that version isn't available" instead of a
466
+ // generic 500.
467
+ if (/not found at commit|did not exist at commit/i.test(message)) {
468
+ return reply.status(404).send({ error: message });
469
+ }
470
+ // Type-mismatch refusal (restoring would silently change entity_type)
471
+ // is a client-actionable conflict, not a server fault → 409.
472
+ if (/would change its type|was type '/i.test(message)) {
473
+ return reply.status(409).send({ error: message });
474
+ }
475
+ return reply.status(500).send({ error: message });
476
+ }
477
+ });
478
+ // GET /api/schema/:entityType/fields — field metadata for an entity type
479
+ fastify.get('/api/schema/:entityType/fields', async (req, reply) => {
480
+ const { entityType } = req.params;
481
+ const registry = workspaceManager.getSchemaRegistry();
482
+ const fields = registry.getFieldMetadata(entityType);
483
+ return reply.send(fields);
484
+ });
485
+ // GET /api/repos/:repo/search?q=&type=&tags= — search within a repo
486
+ fastify.get('/api/repos/:repo/search', async (req, reply) => {
487
+ const { repo } = req.params;
488
+ // Access enforced by graphForRead (WorkspaceAccessService).
489
+ const graph = graphForRead(req, reply, repo);
490
+ if (!graph)
491
+ return;
492
+ const { q, type, tags } = req.query;
347
493
  const results = graph.search({
348
494
  query: q,
349
495
  entityType: type,
@@ -366,12 +512,11 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
366
512
  const entityIds = new Set();
367
513
  for (const repoConfig of repos) {
368
514
  try {
515
+ // Direct WM access OK — `repos` came from getAccessibleRepos above.
369
516
  const graph = workspaceManager.getGraph(repoConfig.name);
370
517
  const entities = graph.search({});
371
518
  for (const entity of entities) {
372
519
  const nodeId = entity.id;
373
- if (nodeId.toLowerCase().startsWith('readme'))
374
- continue;
375
520
  if (!entityIds.has(nodeId)) {
376
521
  entityIds.add(nodeId);
377
522
  nodes.push({
@@ -399,9 +544,9 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
399
544
  const validEdges = edges.filter(e => entityIds.has(e.source) && entityIds.has(e.target));
400
545
  return reply.send({ nodes, edges: validEdges });
401
546
  });
402
- // GET /api/search?q=&type=&tags=&repos= — cross-repo search
547
+ // GET /api/search?q=&type=&tags=&repos=&mode= — cross-repo search
403
548
  fastify.get('/api/search', async (req, reply) => {
404
- const { q, type, tags, repos } = req.query;
549
+ const { q, type, tags, repos, mode } = req.query;
405
550
  const accessibleNames = new Set(getAccessibleRepos(req, workspaceManager, authService).map(r => r.name));
406
551
  // Filter requested repos to only accessible ones
407
552
  let repoNames = repos ? repos.split(',').map(r => r.trim()) : undefined;
@@ -411,12 +556,35 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
411
556
  else {
412
557
  repoNames = [...accessibleNames];
413
558
  }
559
+ const keywordMode = mode === 'keyword';
414
560
  const results = await workspaceManager.search({
415
561
  query: q,
416
562
  entityType: type,
417
563
  tags: tags ? tags.split(',').map(t => t.trim()) : undefined,
418
564
  repoNames,
565
+ mode: keywordMode ? 'keyword' : undefined,
566
+ // Autosuggest dropdown caps at 20 visible rows; capping server-side
567
+ // avoids serializing a long tail the client throws away.
568
+ limit: keywordMode ? 20 : undefined,
419
569
  });
570
+ // Autosuggest doesn't need the markdown body or wikilinks — the
571
+ // dropdown renders {name, type, repo} only. Returning a slim shape
572
+ // cuts response size from ~hundreds of KB to a few KB on big
573
+ // workspaces, and skips the per-entity wikilink visibility filter.
574
+ if (keywordMode) {
575
+ const slim = results.map(e => ({
576
+ id: e.id,
577
+ path: e.path,
578
+ folder_path: e.folder_path,
579
+ entityType: e.entityType,
580
+ repoName: e.repoName,
581
+ data: {
582
+ name: e.data?.name,
583
+ updated_at: e.data?.updated_at,
584
+ },
585
+ }));
586
+ return reply.send(slim);
587
+ }
420
588
  const accessibleIds = getAccessibleEntityIds(req, workspaceManager, authService);
421
589
  const filtered = results.map((e) => {
422
590
  if (!e.document?.wikilinks?.length)
@@ -428,20 +596,29 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
428
596
  // PUT /api/repos/:repo/entities/:type/:id — update entity (data and/or content)
429
597
  fastify.put('/api/repos/:repo/entities/:type/:id', async (req, reply) => {
430
598
  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;
599
+ // Access enforced by graphForWrite \u2014 PUT updates an entity, write required.
600
+ const graph = graphForWrite(req, reply, repo);
601
+ if (!graph)
602
+ return;
603
+ const eagerError = checkEagerSkillPermission(req, type, req.body.data, repo, workspaceManager, authService);
604
+ if (eagerError)
605
+ return reply.status(403).send({ error: eagerError });
435
606
  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
607
+ // Optimistic concurrency. Two preconditions:
608
+ // - `X-Studiograph-Base-Body-Hash` (Phase 3a): sha256 hex of the
609
+ // body the client last loaded. Enforced INSIDE the per-doc lock
610
+ // by graph.update via `baseRevision`, so it composes with
611
+ // serialization — two body-writing PUTs from the same baseline
612
+ // can't both pass and back-stomp each other.
613
+ // - `If-Unmodified-Since` (legacy): kept for non-web clients;
614
+ // RFC-7231 second-precision so aliases sub-millisecond writes,
615
+ // which is why the body-hash form is preferred.
616
+ // Body-hash header is IGNORED on metadata-only PUTs — a stale body
617
+ // baseline is irrelevant when the PUT doesn't touch the body, and
618
+ // enforcing it would cause spurious 412s after an agent body-write.
619
+ const baseBodyHash = req.headers['x-studiograph-base-body-hash'];
443
620
  const ifUnmodifiedSince = req.headers['if-unmodified-since'];
444
- if (ifUnmodifiedSince) {
621
+ if (ifUnmodifiedSince && !baseBodyHash) {
445
622
  const current = graph.get(type, id);
446
623
  const entityUpdatedAt = current?.data?.updated_at;
447
624
  if (entityUpdatedAt && new Date(entityUpdatedAt) > new Date(ifUnmodifiedSince)) {
@@ -449,6 +626,29 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
449
626
  }
450
627
  }
451
628
  const gitUser = reqGitUser(req);
629
+ // Workspace-wide uniqueness check for `user_handle` on person entities.
630
+ // Skip the entity being updated so we don't reject it for clashing
631
+ // with itself. Also gate WHO can change the bridge: only workspace
632
+ // owners may stamp/clear/rewrite user_handle. Other folder-writers
633
+ // see the field but can't edit it (defense against impersonation
634
+ // through the agent's Current User prompt).
635
+ if (type === 'person' && req.body.data) {
636
+ try {
637
+ validateUserHandleInPayload(workspaceManager, req.body.data, { repo, entityId: id });
638
+ if (Object.prototype.hasOwnProperty.call(req.body.data, 'user_handle')) {
639
+ const current = graph.get('person', id);
640
+ const currentHandle = (current?.data?.user_handle ?? '');
641
+ assertCanChangeUserHandle(reqUser(req), currentHandle, req.body.data.user_handle);
642
+ }
643
+ }
644
+ catch (err) {
645
+ if (err instanceof BridgeError) {
646
+ const status = err.code === 'OWNER_REQUIRED' ? 403 : 409;
647
+ return reply.status(status).send({ error: err.message, code: err.code, ...err.details });
648
+ }
649
+ throw err;
650
+ }
651
+ }
452
652
  const updated = await graph.update({
453
653
  entityType: type,
454
654
  entityId: id,
@@ -457,6 +657,12 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
457
657
  commitMessage: `Update ${type}/${id} from web UI`,
458
658
  user: gitUser,
459
659
  skipCommit: !!sessionManager, // defer commit if scheduler is active
660
+ source: 'autosave',
661
+ // Forward the body-hash precondition into graph.update so the
662
+ // check runs INSIDE the per-doc lock — see Phase 3 P1 fix.
663
+ // Only meaningful for body writes; ignored when content is
664
+ // undefined (graph.update gates on content !== undefined too).
665
+ baseRevision: req.body.content !== undefined ? baseBodyHash : undefined,
460
666
  });
461
667
  if (sessionManager && gitUser) {
462
668
  sessionManager.markDirty(repo, gitUser, type, id, updated.path);
@@ -469,6 +675,19 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
469
675
  return reply.send(updated);
470
676
  }
471
677
  catch (err) {
678
+ // Translate the locked-section baseRevision mismatch into the
679
+ // same 412 shape the route used to emit pre-lock. Preserves the
680
+ // client-side contract (code: STALE_BODY_HASH + currentBodyHash
681
+ // + entity).
682
+ if (err instanceof StaleBaseRevisionError) {
683
+ const current = graph.get(type, id);
684
+ return reply.status(412).send({
685
+ error: 'Precondition Failed: body was modified',
686
+ code: 'STALE_BODY_HASH',
687
+ currentBodyHash: err.actual,
688
+ entity: current,
689
+ });
690
+ }
472
691
  return reply.status(400).send({ error: err.message });
473
692
  }
474
693
  });
@@ -479,16 +698,10 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
479
698
  if (!type || !id || !newType) {
480
699
  return reply.status(400).send({ error: 'entityType, entityId, and newType are required' });
481
700
  }
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
- }
701
+ // Access enforced by graphForRead (WorkspaceAccessService).
702
+ const graph = graphForRead(req, reply, repo);
703
+ if (!graph)
704
+ return;
492
705
  try {
493
706
  // Flush any pending session saves before moving the file
494
707
  if (sessionManager) {
@@ -513,45 +726,111 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
513
726
  return reply.status(status).send({ error: err.message });
514
727
  }
515
728
  });
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' });
729
+ // POST /api/repos/:repo/renamerename an entity (changes its id/filename)
730
+ fastify.post('/api/repos/:repo/rename', async (req, reply) => {
731
+ const { repo } = req.params;
732
+ const { entityType: type, entityId: id, newId } = req.body ?? {};
733
+ if (!type || !id || !newId) {
734
+ return reply.status(400).send({ error: 'entityType, entityId, and newId are required' });
522
735
  }
523
- if (sourceRepo === targetRepo) {
524
- return reply.status(400).send({ error: 'Source and target collections must be different' });
736
+ if (!/^[a-z0-9][a-z0-9_-]*$/.test(newId)) {
737
+ return reply.status(400).send({ error: 'New ID must be kebab-case (lowercase letters, numbers, hyphens, underscores)' });
525
738
  }
526
- if (!hasRepoAccess(req, sourceRepo, workspaceManager, authService)) {
527
- return reply.status(404).send({ error: `Collection "${sourceRepo}" not found` });
739
+ if (id === newId) {
740
+ return reply.status(400).send({ error: 'New ID must differ from the current ID' });
528
741
  }
529
- if (!hasRepoAccess(req, targetRepo, workspaceManager, authService)) {
530
- return reply.status(404).send({ error: `Collection "${targetRepo}" not found` });
742
+ // Access enforced by graphForWrite (WorkspaceAccessService).
743
+ const graph = graphForWrite(req, reply, repo);
744
+ if (!graph)
745
+ return;
746
+ // Workspace-wide uniqueness — ids must be unique across all folders
747
+ const conflict = workspaceManager.findEntityById(newId);
748
+ if (conflict) {
749
+ return reply.status(409).send({ error: `An entry with id "${newId}" already exists in folder "${conflict.repoName}".` });
531
750
  }
532
- let sourceGraph, targetGraph;
533
751
  try {
534
- sourceGraph = workspaceManager.getGraph(sourceRepo);
535
- targetGraph = workspaceManager.getGraph(targetRepo);
752
+ // Flush pending session saves so we don't PUT to the old path after rename
753
+ if (sessionManager) {
754
+ await sessionManager.flush(repo);
755
+ }
756
+ const gitUser = reqGitUser(req);
757
+ const renamed = await graph.rename(type, id, newId, undefined, gitUser);
758
+ // Rewrite any [[wikilinks]] that still point at the old id, across
759
+ // every folder the caller can access. Per-repo — cross-folder links
760
+ // are rare but supported.
761
+ const refactorMsg = `Refactor wikilinks: ${id} \u2192 ${newId}`;
762
+ for (const other of getAccessibleRepos(req, workspaceManager, authService)) {
763
+ try {
764
+ // Direct WM access OK — iterating the access-filtered list.
765
+ const otherGraph = workspaceManager.getGraph(other.name);
766
+ await otherGraph.refactorWikilink(id, newId, refactorMsg);
767
+ }
768
+ catch { /* skip unloadable repo */ }
769
+ }
770
+ wsHub?.broadcast({
771
+ type: 'entity_change', action: 'deleted', repo, entityType: type, entityId: id,
772
+ actor: req.user?.displayName ?? 'API',
773
+ source: 'api', timestamp: new Date().toISOString(),
774
+ });
775
+ wsHub?.broadcast({
776
+ type: 'entity_change', action: 'created', repo, entityType: type, entityId: newId,
777
+ actor: req.user?.displayName ?? 'API',
778
+ source: 'api', timestamp: new Date().toISOString(),
779
+ });
780
+ return reply.send({ success: true, entity: renamed, newUrl: `/${repo}/${type}/${newId}` });
536
781
  }
537
782
  catch (err) {
538
- return reply.status(404).send({ error: err.message });
783
+ const status = err.message?.includes('already exists') ? 409 : 400;
784
+ return reply.status(status).send({ error: err.message });
785
+ }
786
+ });
787
+ // POST /api/repos/:repo/transfer — move or copy entity to another folder
788
+ fastify.post('/api/repos/:repo/transfer', async (req, reply) => {
789
+ const sourceRepo = req.params.repo;
790
+ const { entityType, entityId, targetRepo, mode, targetFolderPath } = req.body ?? {};
791
+ if (!entityType || !entityId || !targetRepo || !mode) {
792
+ return reply.status(400).send({ error: 'entityType, entityId, targetRepo, and mode are required' });
539
793
  }
794
+ if (sourceRepo === targetRepo) {
795
+ return reply.status(400).send({ error: 'Source and target folders must be different' });
796
+ }
797
+ // Access enforced by graphForWrite — both source (delete on move)
798
+ // and target (create) require write access. Resolved up-front so a
799
+ // target-side denial never surfaces only after source has been
800
+ // mutated. Audit (2026-05) flagged the previous opt-in pattern.
801
+ const sourceGraph = graphForWrite(req, reply, sourceRepo);
802
+ if (!sourceGraph)
803
+ return;
804
+ const targetGraph = graphForWrite(req, reply, targetRepo);
805
+ if (!targetGraph)
806
+ return;
540
807
  try {
541
- // Flush pending saves
542
- if (sessionManager)
808
+ // Phase 0 — prevalidate. Audit (2026-05) required this: previously
809
+ // the route created in target, broadcast `created`, and only then
810
+ // tried to delete from source. A delete failure left a duplicate
811
+ // entry the client had already been told about.
812
+ if (sessionManager) {
543
813
  await sessionManager.flush(sourceRepo);
544
- // Read source entity
814
+ if (targetRepo !== sourceRepo)
815
+ await sessionManager.flush(targetRepo);
816
+ }
817
+ // Source must exist; target must not collide. Both checks run BEFORE
818
+ // any mutation so we never enter phase 2 unless phase 1 will succeed.
545
819
  const source = sourceGraph.get(entityType, entityId);
546
- // Check for conflicts in target
820
+ const sourcePath = source.path;
821
+ const sourceRawMarkdown = readFileSync(sourcePath, 'utf-8');
547
822
  try {
548
823
  targetGraph.get(entityType, entityId);
549
824
  return reply.status(409).send({ error: `"${entityId}" already exists in ${targetRepo}` });
550
825
  }
551
826
  catch { /* not found = good */ }
552
- // Create in target
553
827
  const gitUser = reqGitUser(req);
554
828
  const { entity_type, entity_id, ...dataFields } = source.data;
829
+ // Phase 1 — create in target. No broadcast yet: a half-completed
830
+ // move (created in target, source still present) must NOT be
831
+ // observable to clients, or they will display two copies until the
832
+ // next refresh. `targetFolderPath` lets the caller drop the entry
833
+ // straight into a subfolder of the target repo.
555
834
  const created = await targetGraph.create({
556
835
  entityType: entityType,
557
836
  entityId,
@@ -559,21 +838,123 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
559
838
  content: source.document?.content ?? '',
560
839
  commitMessage: `${mode === 'move' ? 'Move' : 'Copy'} ${entityType}: ${entityId} from ${sourceRepo}`,
561
840
  user: gitUser,
841
+ folderPath: targetFolderPath,
562
842
  });
843
+ // Phase 2 — for `move` only: delete from source. If this fails, run
844
+ // a compensating delete on target so we don't leave a duplicate.
845
+ // The compensating delete is best-effort; if it also fails, surface
846
+ // a 500 with both error messages so the residue is visible (the
847
+ // previous behavior silently succeeded and left orphans).
848
+ if (mode === 'move') {
849
+ try {
850
+ // keepAssets: the moved entity's immutable-keyed blobs are shared
851
+ // (same assetId in both repos' bodies during the window); the
852
+ // source delete must not destroy them.
853
+ await sourceGraph.delete(entityType, entityId, undefined, gitUser, { keepAssets: true });
854
+ // Re-point ownership to the target repo ONLY after the source
855
+ // delete succeeds. Doing it after means a failed source delete
856
+ // (handled below) leaves the index pointing at the source repo,
857
+ // which is correct for the entity we're about to restore there.
858
+ // Blobs/URLs are unchanged (immutable keys); only the index's
859
+ // repo column moves, which is what serve-time authz reads.
860
+ workspaceManager.getAssetIndex?.()?.reassignOwner(sourceRepo, entityType, entityId, { repo: targetRepo });
861
+ }
862
+ catch (deleteErr) {
863
+ const deleteMsg = deleteErr instanceof Error ? deleteErr.message : String(deleteErr);
864
+ let recoveryMsg = null;
865
+ try {
866
+ let restoreSource = !existsSync(sourcePath);
867
+ if (!restoreSource) {
868
+ try {
869
+ restoreSource = readFileSync(sourcePath, 'utf-8') !== sourceRawMarkdown;
870
+ }
871
+ catch {
872
+ restoreSource = true;
873
+ }
874
+ }
875
+ if (restoreSource) {
876
+ mkdirSync(dirname(sourcePath), { recursive: true });
877
+ writeFileSync(sourcePath, sourceRawMarkdown, 'utf-8');
878
+ const recoveryPaths = [sourcePath];
879
+ const folderMarkerPath = join(dirname(sourcePath), '.gitkeep');
880
+ if (existsSync(folderMarkerPath)) {
881
+ rmSync(folderMarkerPath, { force: true });
882
+ recoveryPaths.push(folderMarkerPath);
883
+ }
884
+ const sourceAny = sourceGraph;
885
+ const recoveryGit = sourceAny.gitService;
886
+ const recoveryUser = gitUser ?? sourceAny.gitUser;
887
+ if (!recoveryGit || !recoveryUser) {
888
+ throw new Error('source graph git service or git user unavailable');
889
+ }
890
+ recoveryGit.commitFiles(recoveryPaths, `Recover ${entityType}: ${entityId} after failed move to ${targetRepo}`, recoveryUser);
891
+ }
892
+ }
893
+ catch (recoveryErr) {
894
+ recoveryMsg = recoveryErr instanceof Error ? recoveryErr.message : String(recoveryErr);
895
+ log.error('[critical] cross-repo move source recovery failed', {
896
+ sourceRepo,
897
+ targetRepo,
898
+ entityType,
899
+ entityId,
900
+ sourcePath,
901
+ deleteError: deleteMsg,
902
+ recoveryError: recoveryMsg,
903
+ capturedContent: sourceRawMarkdown,
904
+ });
905
+ }
906
+ try {
907
+ // keepAssets: ownership was NOT reassigned (that happens only
908
+ // after a successful source delete), so the blobs still belong
909
+ // to the source entity we just restored. Destroying them here
910
+ // would leave the recovered source pointing at dead URLs.
911
+ await targetGraph.delete(entityType, entityId, undefined, gitUser, { keepAssets: true });
912
+ if (recoveryMsg) {
913
+ return reply.status(500).send({
914
+ 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.`,
915
+ code: 'MOVE_SOURCE_RECOVERY_FAILED',
916
+ sourceRepo,
917
+ targetRepo,
918
+ });
919
+ }
920
+ return reply.status(500).send({
921
+ error: `Move failed: source delete of ${entityType}/${entityId} failed (${deleteMsg}). Target rolled back; folders are back to their pre-move state.`,
922
+ });
923
+ }
924
+ catch (compErr) {
925
+ const compMsg = compErr instanceof Error ? compErr.message : String(compErr);
926
+ if (recoveryMsg) {
927
+ return reply.status(500).send({
928
+ 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.`,
929
+ code: 'MOVE_RECOVERY_AND_ROLLBACK_FAILED',
930
+ sourceRepo,
931
+ targetRepo,
932
+ });
933
+ }
934
+ return reply.status(500).send({
935
+ 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.`,
936
+ code: 'MOVE_DUPLICATE',
937
+ sourceRepo,
938
+ targetRepo,
939
+ });
940
+ }
941
+ }
942
+ }
943
+ // Phase 3 — both phases succeeded; only now broadcast. Order:
944
+ // source-delete first so subscribers don't briefly observe the
945
+ // entity in both folders during the network race.
563
946
  const actor = req.user?.displayName ?? 'API';
564
947
  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
948
  if (mode === 'move') {
571
- await sourceGraph.delete(entityType, entityId, undefined, gitUser);
572
949
  wsHub?.broadcast({
573
950
  type: 'entity_change', action: 'deleted', repo: sourceRepo, entityType, entityId,
574
951
  actor, source: 'api', timestamp: ts,
575
952
  });
576
953
  }
954
+ wsHub?.broadcast({
955
+ type: 'entity_change', action: 'created', repo: targetRepo, entityType, entityId,
956
+ actor, source: 'api', timestamp: ts,
957
+ });
577
958
  return reply.send({
578
959
  success: true,
579
960
  entity: created,
@@ -584,19 +965,107 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
584
965
  return reply.status(400).send({ error: err.message });
585
966
  }
586
967
  });
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` });
968
+ // POST /api/repos/:repo/transfer-entities — bulk move a list of entries
969
+ // into a single target folder (in this or another repo). The web UI's
970
+ // multi-select Move flow used to loop and POST per entry; this collapses
971
+ // to one round-trip per source repo and one commit per repo touched.
972
+ fastify.post('/api/repos/:repo/transfer-entities', async (req, reply) => {
973
+ const sourceRepo = req.params.repo;
974
+ const { entries, targetRepo, targetFolder } = req.body ?? {};
975
+ if (!Array.isArray(entries) || !targetRepo || targetFolder === undefined) {
976
+ return reply.status(400).send({ error: 'entries, targetRepo, and targetFolder are required' });
977
+ }
978
+ // Access enforced by graphForWrite — both source (mutate) and
979
+ // target (write) require write access. Resolved up-front so a
980
+ // target-side denial never surfaces only after source mutation.
981
+ const sourceGraph = graphForWrite(req, reply, sourceRepo);
982
+ if (!sourceGraph)
983
+ return;
984
+ const targetGraph = graphForWrite(req, reply, targetRepo);
985
+ if (!targetGraph)
986
+ return;
987
+ try {
988
+ if (sessionManager) {
989
+ await sessionManager.flush(sourceRepo);
990
+ if (targetRepo !== sourceRepo)
991
+ await sessionManager.flush(targetRepo);
992
+ }
993
+ const result = await sourceGraph.moveEntitiesToRepo(entries.map(e => ({ entityType: e.entityType, entityId: e.entityId })), targetGraph, targetFolder, { user: reqGitUser(req) });
994
+ const actor = req.user?.displayName ?? 'API';
995
+ const ts = new Date().toISOString();
996
+ wsHub?.broadcast({ type: 'repo_sync', repo: sourceRepo, source: 'api', actor, timestamp: ts });
997
+ if (targetRepo !== sourceRepo) {
998
+ wsHub?.broadcast({ type: 'repo_sync', repo: targetRepo, source: 'api', actor, timestamp: ts });
999
+ }
1000
+ return reply.send({ success: true, ...result });
592
1001
  }
593
- let graph;
1002
+ catch (err) {
1003
+ const status = err?.name === 'InvalidFolderPathError' ? 400
1004
+ : err.message?.includes('not found') ? 404
1005
+ : err.message?.includes('already occupied') ? 409
1006
+ : 400;
1007
+ return reply.status(status).send({ error: err.message });
1008
+ }
1009
+ });
1010
+ // POST /api/repos/:repo/transfer-folder — move an entire folder (with all
1011
+ // descendants) into another repo in one shot. Replaces the per-entry loop
1012
+ // the web UI used to run, which produced 2 git commits per entry; this
1013
+ // route performs a single FS-level rename and one commit per repo.
1014
+ fastify.post('/api/repos/:repo/transfer-folder', async (req, reply) => {
1015
+ const sourceRepo = req.params.repo;
1016
+ const { sourceFolder, targetRepo, targetFolder } = req.body ?? {};
1017
+ if (!sourceFolder || !targetRepo || targetFolder === undefined) {
1018
+ return reply.status(400).send({ error: 'sourceFolder, targetRepo, and targetFolder are required' });
1019
+ }
1020
+ if (sourceRepo === targetRepo && sourceFolder === targetFolder) {
1021
+ return reply.status(400).send({ error: 'Source and target are the same' });
1022
+ }
1023
+ // Access enforced by graphForWrite — both source (mutate) and
1024
+ // target (write) require write access. Resolved up-front so a
1025
+ // target-side denial never surfaces only after source mutation.
1026
+ const sourceGraph = graphForWrite(req, reply, sourceRepo);
1027
+ if (!sourceGraph)
1028
+ return;
1029
+ const targetGraph = graphForWrite(req, reply, targetRepo);
1030
+ if (!targetGraph)
1031
+ return;
594
1032
  try {
595
- graph = workspaceManager.getGraph(repo);
1033
+ // Flush any deferred saves on either side so we don't strand a
1034
+ // pending edit in the source folder mid-move.
1035
+ if (sessionManager) {
1036
+ await sessionManager.flush(sourceRepo);
1037
+ if (targetRepo !== sourceRepo)
1038
+ await sessionManager.flush(targetRepo);
1039
+ }
1040
+ const stats = await sourceGraph.moveFolderToRepo(sourceFolder, targetGraph, targetFolder, { user: reqGitUser(req) });
1041
+ const actor = req.user?.displayName ?? 'API';
1042
+ const ts = new Date().toISOString();
1043
+ // One refresh-trigger per repo. The receiving and sending lists
1044
+ // both need to update; using `repo_sync` keeps the realtime
1045
+ // handler simple — it already calls workspace.refreshEntities
1046
+ // on this event type. Avoids emitting 2× entries-moved entity_change
1047
+ // broadcasts (one created, one deleted) per file.
1048
+ wsHub?.broadcast({ type: 'repo_sync', repo: sourceRepo, source: 'api', actor, timestamp: ts });
1049
+ if (targetRepo !== sourceRepo) {
1050
+ wsHub?.broadcast({ type: 'repo_sync', repo: targetRepo, source: 'api', actor, timestamp: ts });
1051
+ }
1052
+ return reply.send({ success: true, ...stats });
596
1053
  }
597
1054
  catch (err) {
598
- return reply.status(404).send({ error: err.message });
1055
+ const status = err?.name === 'InvalidFolderPathError' ? 400
1056
+ : err.message?.includes('not found') ? 404
1057
+ : err.message?.includes('not empty') ? 409
1058
+ : 400;
1059
+ return reply.status(status).send({ error: err.message });
599
1060
  }
1061
+ });
1062
+ // DELETE /api/repos/:repo/entities/:type/:id — delete an entity
1063
+ fastify.delete('/api/repos/:repo/entities/:type/:id', async (req, reply) => {
1064
+ const { repo, type, id } = req.params;
1065
+ // Access enforced by graphForRead (WorkspaceAccessService).
1066
+ const graph = graphForRead(req, reply, repo);
1067
+ if (!graph)
1068
+ return;
600
1069
  try {
601
1070
  await graph.delete(type, id, `Delete ${type}/${id} from web UI`, reqGitUser(req));
602
1071
  wsHub?.broadcast({
@@ -610,54 +1079,643 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
610
1079
  return reply.status(400).send({ error: err.message });
611
1080
  }
612
1081
  });
1082
+ // POST /api/repos/:repo/entities/bulk-delete — delete many entities in
1083
+ // one git transaction + one WS broadcast. Replaces N parallel
1084
+ // DELETE /entities/:type/:id calls, which on a 500-entry bulk delete
1085
+ // produced 500 git commits + 500 entity_change broadcasts and froze
1086
+ // the UI for minutes.
1087
+ fastify.post('/api/repos/:repo/entities/bulk-delete', async (req, reply) => {
1088
+ const { repo } = req.params;
1089
+ // Access enforced by graphForWrite (WorkspaceAccessService).
1090
+ const graph = graphForWrite(req, reply, repo);
1091
+ if (!graph)
1092
+ return;
1093
+ const body = req.body;
1094
+ if (!body?.entities || !Array.isArray(body.entities)) {
1095
+ return reply.status(400).send({ error: 'Expected { entities: [{ entityType, entityId }] }' });
1096
+ }
1097
+ try {
1098
+ const result = await graph.bulkDelete(body.entities.map(e => ({ entityType: e.entityType, entityId: e.entityId })), {
1099
+ user: reqGitUser(req),
1100
+ commitMessage: `Bulk delete ${body.entities.length} ${body.entities.length === 1 ? 'entry' : 'entries'} from web UI`,
1101
+ });
1102
+ if (result.deleted.length > 0) {
1103
+ wsHub?.broadcast({
1104
+ type: 'repo_sync', repo,
1105
+ actor: req.user?.displayName ?? 'API',
1106
+ source: 'api', timestamp: new Date().toISOString(),
1107
+ });
1108
+ }
1109
+ return reply.send({
1110
+ deleted: result.deleted.length,
1111
+ errors: result.errors,
1112
+ });
1113
+ }
1114
+ catch (err) {
1115
+ return reply.status(400).send({ error: err.message });
1116
+ }
1117
+ });
613
1118
  // POST /api/repos/:repo/entities — create a new entity
614
1119
  fastify.post('/api/repos/:repo/entities', async (req, reply) => {
615
1120
  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;
1121
+ // Access enforced by graphForWrite (WorkspaceAccessService).
1122
+ const graph = graphForWrite(req, reply, repo);
1123
+ if (!graph)
1124
+ return;
1125
+ const { entityType, entityId, data = {}, content = '', folder_path } = req.body;
620
1126
  if (!entityType || !entityId) {
621
1127
  return reply.status(400).send({ error: 'entityType and entityId are required' });
622
1128
  }
623
- let graph;
624
- try {
625
- graph = workspaceManager.getGraph(repo);
626
- }
627
- catch (err) {
628
- return reply.status(404).send({ error: err.message });
1129
+ const eagerError = checkEagerSkillPermission(req, entityType, data, repo, workspaceManager, authService);
1130
+ if (eagerError)
1131
+ return reply.status(403).send({ error: eagerError });
1132
+ // Person entities can carry a `user_handle` pointer to an auth account;
1133
+ // enforce workspace-wide uniqueness AND owner-only authz before
1134
+ // delegating to the per-repo create path. Stamping a handle on a new
1135
+ // entity is the same trust action as stamping it on an existing one,
1136
+ // so non-owners can't create a pre-linked person entity either.
1137
+ if (entityType === 'person') {
1138
+ try {
1139
+ validateUserHandleInPayload(workspaceManager, data);
1140
+ if (Object.prototype.hasOwnProperty.call(data, 'user_handle')) {
1141
+ assertCanChangeUserHandle(reqUser(req), '', data.user_handle);
1142
+ }
1143
+ }
1144
+ catch (err) {
1145
+ if (err instanceof BridgeError) {
1146
+ const status = err.code === 'OWNER_REQUIRED' ? 403 : 409;
1147
+ return reply.status(status).send({ error: err.message, code: err.code, ...err.details });
1148
+ }
1149
+ throw err;
1150
+ }
629
1151
  }
630
1152
  try {
1153
+ // Detect implicit subfolder creation: if the entry lands in a folder
1154
+ // that didn't exist yet, the entity_change below only refreshes the
1155
+ // entry list — clients need a _folder event to refetch the folder tree
1156
+ // (sidebar + list/gallery folder rows). Mirrors the MCP create_entity
1157
+ // path; normalize the same way graph.create does so the existence
1158
+ // check matches the real on-disk path.
1159
+ const normFolder = folder_path ? validateFolderPath(folder_path) : '';
1160
+ const folderExisted = !normFolder || existsSync(join(graph.getRepoPath(), normFolder));
631
1161
  const entity = await graph.create({
632
1162
  entityType: entityType,
633
1163
  entityId,
634
1164
  data: { entity_type: entityType, entity_id: entityId, ...data },
635
1165
  content,
1166
+ folderPath: folder_path,
636
1167
  commitMessage: `Create ${entityType}/${entityId} from web UI`,
637
1168
  user: reqGitUser(req),
638
1169
  });
1170
+ const actor = req.user?.displayName ?? 'API';
1171
+ const ts = new Date().toISOString();
639
1172
  wsHub?.broadcast({
640
1173
  type: 'entity_change', action: 'created', repo, entityType, entityId,
1174
+ actor, source: 'api', timestamp: ts,
1175
+ });
1176
+ if (!folderExisted) {
1177
+ wsHub?.broadcast({
1178
+ type: 'entity_change', action: 'created', repo, entityType: '_folder', entityId: normFolder,
1179
+ actor, source: 'api', timestamp: ts,
1180
+ });
1181
+ }
1182
+ return reply.status(201).send(entity);
1183
+ }
1184
+ catch (err) {
1185
+ return reply.status(400).send({ error: err.message });
1186
+ }
1187
+ });
1188
+ // POST /api/repos/:repo/move — move entry to a different folder within the
1189
+ // same repo. Distinct from /transfer (cross-repo) and /rename (id change).
1190
+ // The entry's type and id are unchanged; only its folder_path moves.
1191
+ fastify.post('/api/repos/:repo/move', async (req, reply) => {
1192
+ const { repo } = req.params;
1193
+ const { entityType, entityId, folder_path } = req.body ?? {};
1194
+ if (!entityType || !entityId || folder_path === undefined) {
1195
+ return reply.status(400).send({ error: 'entityType, entityId, and folder_path are required' });
1196
+ }
1197
+ // Access enforced by graphForWrite (WorkspaceAccessService).
1198
+ const graph = graphForWrite(req, reply, repo);
1199
+ if (!graph)
1200
+ return;
1201
+ try {
1202
+ if (sessionManager)
1203
+ await sessionManager.flush(repo);
1204
+ // If the destination folder doesn't exist yet, the move creates it —
1205
+ // emit a _folder event (alongside the entry update) so clients refetch
1206
+ // the folder tree, not just the entry list. Mirrors MCP move_entity.
1207
+ const normTarget = folder_path ? validateFolderPath(folder_path) : '';
1208
+ const targetExisted = !normTarget || existsSync(join(graph.getRepoPath(), normTarget));
1209
+ const moved = await graph.move(entityType, entityId, folder_path, undefined, reqGitUser(req));
1210
+ const actor = req.user?.displayName ?? 'API';
1211
+ const ts = new Date().toISOString();
1212
+ wsHub?.broadcast({
1213
+ type: 'entity_change', action: 'updated', repo, entityType, entityId,
1214
+ actor, source: 'api', timestamp: ts,
1215
+ });
1216
+ if (!targetExisted) {
1217
+ wsHub?.broadcast({
1218
+ type: 'entity_change', action: 'created', repo, entityType: '_folder', entityId: normTarget,
1219
+ actor, source: 'api', timestamp: ts,
1220
+ });
1221
+ }
1222
+ return reply.send({ success: true, entity: moved });
1223
+ }
1224
+ catch (err) {
1225
+ const status = err?.name === 'InvalidFolderPathError' ? 400
1226
+ : err.message?.includes('already occupied') ? 409
1227
+ : err.message?.includes('not found') ? 404
1228
+ : 400;
1229
+ return reply.status(status).send({ error: err.message });
1230
+ }
1231
+ });
1232
+ // POST /api/repos/:repo/folders — create a folder (with .gitkeep). Auto-creates parents.
1233
+ // Optional display_name is written to the folder's .studiograph-folder.json
1234
+ // sidecar when set; otherwise the client falls back to toDisplayName(slug).
1235
+ fastify.post('/api/repos/:repo/folders', async (req, reply) => {
1236
+ const { repo } = req.params;
1237
+ const { path, display_name } = req.body ?? {};
1238
+ if (!path || typeof path !== 'string') {
1239
+ return reply.status(400).send({ error: 'path is required' });
1240
+ }
1241
+ // Access enforced by graphForWrite (WorkspaceAccessService).
1242
+ const graph = graphForWrite(req, reply, repo);
1243
+ if (!graph)
1244
+ return;
1245
+ try {
1246
+ await graph.createFolder(path, undefined, reqGitUser(req), {
1247
+ displayName: display_name,
1248
+ });
1249
+ // Emit the _folder sentinel so OTHER connected clients refetch the
1250
+ // folder tree (sidebar + list/gallery folder rows). The local creator
1251
+ // already updated optimistically via workspace.addFolder.
1252
+ wsHub?.broadcast({
1253
+ type: 'entity_change', action: 'created', repo, entityType: '_folder', entityId: validateFolderPath(path),
641
1254
  actor: req.user?.displayName ?? 'API',
642
1255
  source: 'api', timestamp: new Date().toISOString(),
643
1256
  });
644
- return reply.status(201).send(entity);
1257
+ return reply.status(201).send({ success: true, path });
645
1258
  }
646
1259
  catch (err) {
647
1260
  return reply.status(400).send({ error: err.message });
648
1261
  }
649
1262
  });
1263
+ // PATCH /api/repos/:repo/folders — rename / move a folder within the repo,
1264
+ // optionally updating its display_name in the same commit. Three shapes:
1265
+ // { from, to } — slug-only rename / move
1266
+ // { from, to, display_name } — slug change + display update
1267
+ // { from, to: from, display_name } — display-only update (no FS move)
1268
+ // `display_name: ""` (or null) clears the override and reverts to the slug
1269
+ // fallback.
1270
+ fastify.patch('/api/repos/:repo/folders', async (req, reply) => {
1271
+ const { repo } = req.params;
1272
+ const { from, to, display_name } = req.body ?? {};
1273
+ if (!from || !to) {
1274
+ return reply.status(400).send({ error: 'from and to are required' });
1275
+ }
1276
+ // Access enforced by graphForWrite (WorkspaceAccessService).
1277
+ const graph = graphForWrite(req, reply, repo);
1278
+ if (!graph)
1279
+ return;
1280
+ try {
1281
+ if (sessionManager)
1282
+ await sessionManager.flush(repo);
1283
+ await graph.renameFolder(from, to, undefined, reqGitUser(req), {
1284
+ displayName: display_name === undefined ? undefined : display_name,
1285
+ });
1286
+ // Rename/move relocates entries on disk, so the _folder sentinel's
1287
+ // refreshRepoAfterFolderMutation (tree + entity summaries) is exactly
1288
+ // what other clients need. Carries the new path as the entityId.
1289
+ wsHub?.broadcast({
1290
+ type: 'entity_change', action: 'updated', repo, entityType: '_folder', entityId: validateFolderPath(to),
1291
+ actor: req.user?.displayName ?? 'API',
1292
+ source: 'api', timestamp: new Date().toISOString(),
1293
+ });
1294
+ return reply.send({ success: true, from, to });
1295
+ }
1296
+ catch (err) {
1297
+ const status = err?.name === 'InvalidFolderPathError' ? 400
1298
+ : err.message?.includes('not found') ? 404
1299
+ : err.message?.includes('already exists') ? 409
1300
+ : 400;
1301
+ return reply.status(status).send({ error: err.message });
1302
+ }
1303
+ });
1304
+ // DELETE /api/repos/:repo/folders?path=X&recursive=true — delete a folder.
1305
+ // Without recursive=true, non-empty folders return 409 with blast-radius
1306
+ // counts so the UI can confirm before retrying.
1307
+ fastify.delete('/api/repos/:repo/folders', async (req, reply) => {
1308
+ const { repo } = req.params;
1309
+ const { path, recursive } = req.query;
1310
+ if (!path) {
1311
+ return reply.status(400).send({ error: 'path query parameter is required' });
1312
+ }
1313
+ // Access enforced by graphForWrite (WorkspaceAccessService).
1314
+ const graph = graphForWrite(req, reply, repo);
1315
+ if (!graph)
1316
+ return;
1317
+ try {
1318
+ if (sessionManager)
1319
+ await sessionManager.flush(repo);
1320
+ const result = await graph.deleteFolder(path, {
1321
+ recursive: recursive === 'true',
1322
+ user: reqGitUser(req),
1323
+ });
1324
+ // Emit the _folder sentinel so other clients drop the folder (and any
1325
+ // entries it held, via the entity-summary refresh) from their view.
1326
+ wsHub?.broadcast({
1327
+ type: 'entity_change', action: 'deleted', repo, entityType: '_folder', entityId: validateFolderPath(path),
1328
+ actor: req.user?.displayName ?? 'API',
1329
+ source: 'api', timestamp: new Date().toISOString(),
1330
+ });
1331
+ return reply.send({ success: true, ...result });
1332
+ }
1333
+ catch (err) {
1334
+ if (err?.code === 'FOLDER_NOT_EMPTY') {
1335
+ return reply.status(409).send({
1336
+ error: err.message,
1337
+ code: 'FOLDER_NOT_EMPTY',
1338
+ blast_radius: err.blastRadius,
1339
+ });
1340
+ }
1341
+ const status = err?.name === 'InvalidFolderPathError' ? 400
1342
+ : err.message?.includes('not found') ? 404
1343
+ : 400;
1344
+ return reply.status(status).send({ error: err.message });
1345
+ }
1346
+ });
650
1347
  // POST /api/repos/:repo/commit — flush deferred commits (navigate-away signal)
651
1348
  fastify.post('/api/repos/:repo/commit', async (req, reply) => {
652
1349
  if (!sessionManager) {
653
1350
  return reply.send({ committed: false });
654
1351
  }
655
1352
  const { repo } = req.params;
656
- if (!hasRepoAccess(req, repo, workspaceManager, authService)) {
657
- return reply.status(404).send({ error: `Collection "${repo}" not found` });
1353
+ // Access enforced via accessService.canRead \u2014 flush operates on the
1354
+ // SessionManager (no graph instance needed), so a boolean check is
1355
+ // sufficient. Pending commits were already authored by an authenticated
1356
+ // writer; this endpoint just flushes them on navigate-away.
1357
+ if (!accessService.canRead(reqUser(req), repo)) {
1358
+ return reply.status(404).send({ error: `Folder \u201C${repo}\u201D not found` });
658
1359
  }
659
1360
  const committed = await sessionManager.flush(repo);
660
1361
  return reply.send({ committed });
661
1362
  });
1363
+ // POST /api/repos/:repo/import — import files into a folder.
1364
+ //
1365
+ // Routing by extension:
1366
+ // .md / .txt / .csv → direct write via importFile() — no LLM
1367
+ // round-trip, preserves the source faithfully. Markdown frontmatter
1368
+ // (entity_type, entity_id) is honored when present; otherwise
1369
+ // sensible defaults.
1370
+ // .pdf + opaque binaries → asset wrapper via uploadAsset() —
1371
+ // creates an `asset` entity, stores the binary in R2 or the local
1372
+ // volume, writes a PDF text sidecar for vector indexing.
1373
+ // .docx → text extraction + agent capture
1374
+ // pipeline (until the mammoth → markdown workstream lands).
1375
+ fastify.post('/api/repos/:repo/import', async (req, reply) => {
1376
+ const { repo } = req.params;
1377
+ // Access enforced by graphForWrite — import creates entities, so write
1378
+ // is required. Graph captured here is reused by the inner closures so
1379
+ // they don't need to re-resolve (which would also bypass the gate).
1380
+ const repoGraph = graphForWrite(req, reply, repo);
1381
+ if (!repoGraph)
1382
+ return;
1383
+ const body = req.body;
1384
+ if (!body?.files || !Array.isArray(body.files)) {
1385
+ return reply.status(400).send({ error: 'Expected { files: [{ filename, content }] }' });
1386
+ }
1387
+ // Reject paths with traversal segments; importFile slugifies the rest.
1388
+ // Empty / undefined = repo root (existing behavior).
1389
+ const targetFolderPath = (body.targetFolderPath ?? '').trim();
1390
+ if (targetFolderPath.split('/').some(seg => seg === '..' || seg === '.')) {
1391
+ return reply.status(400).send({ error: 'Invalid targetFolderPath: contains traversal segments' });
1392
+ }
1393
+ const captureRepo = body.agentDecides ? undefined : repo;
1394
+ const gitUser = reqGitUser(req);
1395
+ try {
1396
+ const { extractText, isSupportedFile, importFile } = await import('../../services/import-service.js');
1397
+ const { isAssetUploadExtension } = await import('../../services/assets/base.js');
1398
+ const { uploadAsset } = await import('../../services/asset-upload-service.js');
1399
+ const { extname } = await import('path');
1400
+ const results = [];
1401
+ // Direct-write extensions never need an LLM. Verbatim preservation —
1402
+ // markdown stays markdown, CSV stays CSV.
1403
+ const DIRECT_WRITE_EXTS = new Set(['.md', '.txt', '.csv']);
1404
+ const CONCURRENCY = 3;
1405
+ async function processDirect(file) {
1406
+ // Reuse the access-checked graph captured in the outer scope. Do
1407
+ // NOT re-resolve via workspaceManager.getGraph here — the audit's
1408
+ // boundary is the outer graphForWrite call.
1409
+ const graph = repoGraph;
1410
+ const result = await importFile(graph, file.filename, file.content, {
1411
+ user: gitUser,
1412
+ // Caller-chosen destination subfolder; importFile combines it
1413
+ // with each file's own relative directory.
1414
+ folderPath: targetFolderPath || undefined,
1415
+ // Defer commits — we batch them into one transaction at the
1416
+ // end of the request so a 500-file import is one git commit
1417
+ // instead of 500. Per-file commits dominated wall time
1418
+ // (~50-200ms each) before this; batching collapses that to
1419
+ // one commit for the whole batch.
1420
+ skipCommit: true,
1421
+ });
1422
+ if (result.status === 'created' && result.path) {
1423
+ return {
1424
+ filename: file.filename,
1425
+ status: 'created',
1426
+ entities: [{ entityId: result.entityId, entityType: result.entityType }],
1427
+ entry: {
1428
+ operation: 'create',
1429
+ entityType: result.entityType,
1430
+ entityId: result.entityId,
1431
+ filePath: result.path,
1432
+ },
1433
+ sidecarPaths: result.sidecarPaths,
1434
+ };
1435
+ }
1436
+ return { filename: file.filename, status: result.status, error: result.error };
1437
+ }
1438
+ async function processAsset(file) {
1439
+ // Reuse the outer access-checked graph; re-resolving would bypass
1440
+ // the architectural boundary even though the closure captures repo.
1441
+ const graph = repoGraph;
1442
+ try {
1443
+ const buffer = Buffer.from(file.content, 'base64');
1444
+ const result = await uploadAsset({
1445
+ graph,
1446
+ filename: file.filename,
1447
+ buffer,
1448
+ folderPath: targetFolderPath || undefined,
1449
+ user: gitUser,
1450
+ workspacePath: graph.getWorkspaceRoot(),
1451
+ // Defer commits — the dispatcher's batched commitPending
1452
+ // call at the bottom of the route covers every entry in
1453
+ // the same transaction.
1454
+ skipCommit: true,
1455
+ });
1456
+ return {
1457
+ filename: file.filename,
1458
+ status: 'created',
1459
+ entities: [{ entityId: result.entityId, entityType: result.entityType }],
1460
+ entry: {
1461
+ operation: 'create',
1462
+ entityType: 'asset',
1463
+ entityId: result.entityId,
1464
+ filePath: result.filePath,
1465
+ },
1466
+ // PDF text sidecars: include in extraPaths so they land in
1467
+ // the same commit as the entity files.
1468
+ sidecarPaths: result.sidecarPath ? [result.sidecarPath] : undefined,
1469
+ };
1470
+ }
1471
+ catch (err) {
1472
+ return { filename: file.filename, status: 'error', error: err.message ?? 'Asset upload failed' };
1473
+ }
1474
+ }
1475
+ async function processViaAgent(file) {
1476
+ try {
1477
+ const text = await extractText(file.filename, file.content);
1478
+ if (!text) {
1479
+ log.warn(`[import] No text extracted from ${file.filename}`);
1480
+ return { filename: file.filename, status: 'error', error: 'No text content extracted' };
1481
+ }
1482
+ log.info(`[import] Extracted ${text.length} chars from ${file.filename}`);
1483
+ const captureRes = await fastify.inject({
1484
+ method: 'POST',
1485
+ url: '/api/capture',
1486
+ headers: {
1487
+ 'content-type': 'application/json',
1488
+ ...(req.headers.authorization ? { authorization: req.headers.authorization } : {}),
1489
+ ...(req.headers.cookie ? { cookie: req.headers.cookie } : {}),
1490
+ },
1491
+ payload: JSON.stringify({ text: `[Imported from ${file.filename}]\n\n${text}`, ...(captureRepo ? { repo: captureRepo } : {}) }),
1492
+ });
1493
+ const captureBody = JSON.parse(captureRes.body);
1494
+ if (captureRes.statusCode !== 200 || !captureBody.success) {
1495
+ log.warn(`[import] Capture failed for ${file.filename}: ${captureRes.statusCode} ${captureBody.error ?? JSON.stringify(captureBody).slice(0, 200)}`);
1496
+ return { filename: file.filename, status: 'error', error: captureBody.error ?? 'Agent processing failed' };
1497
+ }
1498
+ return { filename: file.filename, status: 'created', entities: captureBody.entities };
1499
+ }
1500
+ catch (err) {
1501
+ return { filename: file.filename, status: 'error', error: err.message ?? 'Processing failed' };
1502
+ }
1503
+ }
1504
+ async function processFile(file) {
1505
+ if (!file.filename) {
1506
+ return { filename: file.filename, status: 'error', error: 'Missing filename' };
1507
+ }
1508
+ const ext = extname(file.filename).toLowerCase();
1509
+ // Direct-write text formats first — never collide with asset
1510
+ // detection. Then asset extensions (images, PDF, design files,
1511
+ // archives, …). Then the agent-extraction fallback for .docx
1512
+ // until the mammoth workstream lands. Anything else: reject.
1513
+ if (DIRECT_WRITE_EXTS.has(ext))
1514
+ return processDirect(file);
1515
+ if (isAssetUploadExtension(file.filename))
1516
+ return processAsset(file);
1517
+ if (isSupportedFile(file.filename))
1518
+ return processViaAgent(file);
1519
+ return { filename: file.filename, status: 'error', error: `Unsupported file: ${file.filename}` };
1520
+ }
1521
+ // Process files in batches of CONCURRENCY
1522
+ for (let i = 0; i < body.files.length; i += CONCURRENCY) {
1523
+ const batch = body.files.slice(i, i + CONCURRENCY);
1524
+ const batchResults = await Promise.all(batch.map(processFile));
1525
+ results.push(...batchResults);
1526
+ }
1527
+ // Direct-write path deferred git commits — flush them all in one
1528
+ // transaction now. Collapses N per-file commits into one batch
1529
+ // commit (~50ms vs N×50ms). Agent-path files committed inline via
1530
+ // /api/capture so they're not in this set.
1531
+ const pendingEntries = results
1532
+ .map(r => r.entry)
1533
+ .filter((e) => Boolean(e));
1534
+ // Collect every sidecar path produced by the import — these stage in
1535
+ // the same commit as the entity files, so the folder display name
1536
+ // and the entries it labels land atomically. De-dup because two
1537
+ // imported files inside the same Schema_OS/widgets/ would otherwise
1538
+ // produce duplicate sidecar entries.
1539
+ const sidecarPaths = Array.from(new Set(results.flatMap(r => r.sidecarPaths ?? [])));
1540
+ // commitPending failure used to be swallowed and the route returned
1541
+ // success=true even though the files were never committed. The audit
1542
+ // (2026-05) flagged this: a 500-file import that failed mid-commit
1543
+ // looked successful to the client but left N untracked files. Now
1544
+ // the failure propagates as 500 with a partial-failure shape so the
1545
+ // caller knows which entries are stranded.
1546
+ let commitError = null;
1547
+ if (pendingEntries.length > 0) {
1548
+ try {
1549
+ await repoGraph.commitPending(pendingEntries, {
1550
+ commitMessage: `Import ${pendingEntries.length} ${pendingEntries.length === 1 ? 'entry' : 'entries'} from web UI`,
1551
+ user: gitUser,
1552
+ extraPaths: sidecarPaths.length > 0 ? sidecarPaths : undefined,
1553
+ });
1554
+ }
1555
+ catch (err) {
1556
+ commitError = err instanceof Error ? err.message : String(err);
1557
+ log.warn(`[import] batch commit failed for ${repo}: ${commitError}`);
1558
+ }
1559
+ }
1560
+ const created = results.filter(r => r.status === 'created').length;
1561
+ const entityCount = results.reduce((sum, r) => sum + (r.entities?.length ?? 0), 0);
1562
+ // Single repo_sync broadcast for the whole import. The realtime
1563
+ // store's repo_sync handler refreshes the repo's entity list once
1564
+ // — vs. one entity_change per file, which would do N re-renders
1565
+ // on every connected client and freeze the UI for large imports.
1566
+ // Skip when commit failed: there's nothing for clients to refresh
1567
+ // beyond stranded uncommitted files, and the broadcast would lie
1568
+ // about persistence.
1569
+ if (created > 0 && !commitError) {
1570
+ wsHub?.broadcast({
1571
+ type: 'repo_sync', repo,
1572
+ actor: req.user?.displayName ?? 'API',
1573
+ source: 'api', timestamp: new Date().toISOString(),
1574
+ });
1575
+ }
1576
+ const errors = results.filter(r => r.status === 'error').map(r => r.error);
1577
+ if (commitError) {
1578
+ // Files are on disk but uncommitted — partial failure. Surface as
1579
+ // 500 with details so the client doesn't show "imported N entries"
1580
+ // for entries that won't survive a server restart.
1581
+ return reply.status(500).send({
1582
+ success: false,
1583
+ error: `Import wrote ${pendingEntries.length} entries to disk but the git commit failed: ${commitError}`,
1584
+ commitError,
1585
+ uncommittedEntries: pendingEntries.length,
1586
+ imported: created,
1587
+ entityCount,
1588
+ skipped: errors.length,
1589
+ errors,
1590
+ results,
1591
+ });
1592
+ }
1593
+ return {
1594
+ success: true,
1595
+ imported: created,
1596
+ entityCount,
1597
+ skipped: errors.length,
1598
+ errors,
1599
+ results,
1600
+ };
1601
+ }
1602
+ catch (err) {
1603
+ return reply.status(400).send({
1604
+ success: false,
1605
+ error: err instanceof Error ? err.message : 'Unknown error',
1606
+ });
1607
+ }
1608
+ });
1609
+ // POST /api/repos/:repo/import-bookmarks — import browser bookmark exports as reference entities
1610
+ fastify.post('/api/repos/:repo/import-bookmarks', async (req, reply) => {
1611
+ const { repo } = req.params;
1612
+ // Access enforced by graphForWrite — bookmark import creates entities.
1613
+ const graph = graphForWrite(req, reply, repo);
1614
+ if (!graph)
1615
+ return;
1616
+ const body = req.body;
1617
+ if (!body?.content || typeof body.content !== 'string') {
1618
+ return reply.status(400).send({ error: 'Expected { content: string }' });
1619
+ }
1620
+ try {
1621
+ const { isNetscapeBookmarkFile, importBookmarks } = await import('../../services/bookmark-import-service.js');
1622
+ if (!isNetscapeBookmarkFile(body.content)) {
1623
+ return reply.status(400).send({
1624
+ error: "This doesn't appear to be a browser bookmark export. Export bookmarks from your browser's bookmark manager to generate the correct format.",
1625
+ });
1626
+ }
1627
+ const user = reqGitUser(req);
1628
+ const result = await importBookmarks(graph, body.content, user);
1629
+ // Single git commit for all created entities
1630
+ if (result.created > 0) {
1631
+ const git = graph.getGitService();
1632
+ if (git) {
1633
+ const commitUser = user ?? { id: 'import', name: 'Import', email: 'import@studiograph' };
1634
+ git.commitAll(`Import ${result.created} bookmark${result.created !== 1 ? 's' : ''} as references`, commitUser);
1635
+ }
1636
+ }
1637
+ return {
1638
+ success: true,
1639
+ created: result.created,
1640
+ skipped: result.skipped,
1641
+ errors: result.errors,
1642
+ };
1643
+ }
1644
+ catch (err) {
1645
+ return reply.status(400).send({
1646
+ success: false,
1647
+ error: err instanceof Error ? err.message : 'Unknown error',
1648
+ });
1649
+ }
1650
+ });
1651
+ // POST /api/repos/:repo/assets — generic asset upload.
1652
+ //
1653
+ // Multipart `file` field. Creates a wrapper `asset` entity whose
1654
+ // frontmatter points at the stored binary (R2 or local volume) via
1655
+ // `asset_url`. PDFs additionally get a text sidecar at
1656
+ // `<workspace>/.studiograph/assets-text/asset/<id>.txt` so the vector
1657
+ // indexer can RAG them without bloating the markdown body. See
1658
+ // docs/asset-architecture.md.
1659
+ fastify.post('/api/repos/:repo/assets', async (req, reply) => {
1660
+ const { repo } = req.params;
1661
+ // Access enforced by graphForWrite (WorkspaceAccessService).
1662
+ const graph = graphForWrite(req, reply, repo);
1663
+ if (!graph)
1664
+ return;
1665
+ // Per-route fileSize override — the global multipart limit (10MB) is
1666
+ // tuned for inline image embeds; full asset uploads accept up to the
1667
+ // 100MB cap enforced inside AssetService.upload().
1668
+ let file;
1669
+ try {
1670
+ file = await req.file({ limits: { fileSize: 100 * 1024 * 1024 } });
1671
+ }
1672
+ catch (err) {
1673
+ return reply.status(400).send({ error: err?.message ?? 'Invalid multipart upload' });
1674
+ }
1675
+ if (!file)
1676
+ return reply.status(400).send({ error: 'No file uploaded' });
1677
+ const buffer = await file.toBuffer();
1678
+ const folderPath = (req.query.folder ?? '').trim();
1679
+ if (folderPath.split('/').some(seg => seg === '..' || seg === '.')) {
1680
+ return reply.status(400).send({ error: 'Invalid folder: contains traversal segments' });
1681
+ }
1682
+ try {
1683
+ const { uploadAsset } = await import('../../services/asset-upload-service.js');
1684
+ const result = await uploadAsset({
1685
+ graph,
1686
+ filename: file.filename,
1687
+ buffer,
1688
+ folderPath: folderPath || undefined,
1689
+ name: req.query.name?.trim() || undefined,
1690
+ user: reqGitUser(req),
1691
+ workspacePath: graph.getWorkspaceRoot(),
1692
+ });
1693
+ wsHub?.broadcast({
1694
+ type: 'repo_sync',
1695
+ repo,
1696
+ actor: req.user?.displayName ?? 'API',
1697
+ source: 'api',
1698
+ timestamp: new Date().toISOString(),
1699
+ });
1700
+ return reply.send({
1701
+ success: true,
1702
+ entity: {
1703
+ entityId: result.entityId,
1704
+ entityType: result.entityType,
1705
+ asset_url: result.asset_url,
1706
+ asset_kind: result.asset_kind,
1707
+ asset_size: result.asset_size,
1708
+ pages: result.pages,
1709
+ },
1710
+ });
1711
+ }
1712
+ catch (err) {
1713
+ const msg = err instanceof Error ? err.message : String(err);
1714
+ const status = msg.includes('not configured') ? 501
1715
+ : msg.includes('Unsupported asset extension') ? 415
1716
+ : 400;
1717
+ return reply.status(status).send({ success: false, error: msg });
1718
+ }
1719
+ });
662
1720
  }
663
1721
  //# sourceMappingURL=graph-api.js.map