studiograph 1.3.48-next.13 → 1.3.48-next.131

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 (1008) hide show
  1. package/README.md +7 -7
  2. package/dist/agent/agent-pool.d.ts +27 -1
  3. package/dist/agent/agent-pool.js +55 -12
  4. package/dist/agent/agent-pool.js.map +1 -1
  5. package/dist/agent/followups.d.ts +34 -0
  6. package/dist/agent/followups.js +52 -0
  7. package/dist/agent/followups.js.map +1 -0
  8. package/dist/agent/orchestrator.d.ts +29 -2
  9. package/dist/agent/orchestrator.js +251 -38
  10. package/dist/agent/orchestrator.js.map +1 -1
  11. package/dist/agent/prompts/entity-types-section.d.ts +30 -0
  12. package/dist/agent/prompts/entity-types-section.js +83 -0
  13. package/dist/agent/prompts/entity-types-section.js.map +1 -0
  14. package/dist/agent/prompts/system.md +106 -47
  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 +79 -343
  19. package/dist/agent/skills/interview/entity-templates.md +38 -37
  20. package/dist/agent/skills/interview/question-domains.md +54 -49
  21. package/dist/agent/skills/interview/skill.md +89 -14
  22. package/dist/agent/tools/capture-tools.d.ts +31 -0
  23. package/dist/agent/tools/capture-tools.js +40 -0
  24. package/dist/agent/tools/capture-tools.js.map +1 -0
  25. package/dist/agent/tools/folder-tools.d.ts +47 -0
  26. package/dist/agent/tools/folder-tools.js +249 -0
  27. package/dist/agent/tools/folder-tools.js.map +1 -0
  28. package/dist/agent/tools/fs-tools.d.ts +6 -5
  29. package/dist/agent/tools/fs-tools.js +5 -5
  30. package/dist/agent/tools/fs-tools.js.map +1 -1
  31. package/dist/agent/tools/graph-tools.d.ts +25 -72
  32. package/dist/agent/tools/graph-tools.js +362 -366
  33. package/dist/agent/tools/graph-tools.js.map +1 -1
  34. package/dist/agent/tools/history-tools.d.ts +95 -0
  35. package/dist/agent/tools/history-tools.js +461 -0
  36. package/dist/agent/tools/history-tools.js.map +1 -0
  37. package/dist/agent/tools/load-skill.d.ts +6 -5
  38. package/dist/agent/tools/load-skill.js +3 -3
  39. package/dist/agent/tools/load-skill.js.map +1 -1
  40. package/dist/agent/tools/message-tools.d.ts +13 -11
  41. package/dist/agent/tools/message-tools.js +31 -39
  42. package/dist/agent/tools/message-tools.js.map +1 -1
  43. package/dist/agent/tools/ops-tools.d.ts +5 -4
  44. package/dist/agent/tools/ops-tools.js +99 -212
  45. package/dist/agent/tools/ops-tools.js.map +1 -1
  46. package/dist/agent/tools/permission-tools.d.ts +39 -16
  47. package/dist/agent/tools/permission-tools.js +66 -41
  48. package/dist/agent/tools/permission-tools.js.map +1 -1
  49. package/dist/agent/tools/tool-loader.js +5 -4
  50. package/dist/agent/tools/tool-loader.js.map +1 -1
  51. package/dist/agent/tools/web-tools.js +58 -9
  52. package/dist/agent/tools/web-tools.js.map +1 -1
  53. package/dist/cli/commands/about.d.ts +1 -0
  54. package/dist/cli/commands/about.js +55 -3
  55. package/dist/cli/commands/about.js.map +1 -1
  56. package/dist/cli/commands/audit.d.ts +21 -0
  57. package/dist/cli/commands/audit.js +174 -0
  58. package/dist/cli/commands/audit.js.map +1 -0
  59. package/dist/cli/commands/clear.d.ts +5 -0
  60. package/dist/cli/commands/clear.js +144 -1
  61. package/dist/cli/commands/clear.js.map +1 -1
  62. package/dist/cli/commands/clone.d.ts +1 -1
  63. package/dist/cli/commands/clone.js +82 -23
  64. package/dist/cli/commands/clone.js.map +1 -1
  65. package/dist/cli/commands/deploy.js +79 -18
  66. package/dist/cli/commands/deploy.js.map +1 -1
  67. package/dist/cli/commands/folder.d.ts +11 -0
  68. package/dist/cli/commands/folder.js +186 -0
  69. package/dist/cli/commands/folder.js.map +1 -0
  70. package/dist/cli/commands/index.js +2 -2
  71. package/dist/cli/commands/index.js.map +1 -1
  72. package/dist/cli/commands/init.js +16 -14
  73. package/dist/cli/commands/init.js.map +1 -1
  74. package/dist/cli/commands/join.d.ts +1 -1
  75. package/dist/cli/commands/join.js +26 -14
  76. package/dist/cli/commands/join.js.map +1 -1
  77. package/dist/cli/commands/mcp.js +12 -4
  78. package/dist/cli/commands/mcp.js.map +1 -1
  79. package/dist/cli/commands/r2.d.ts +3 -0
  80. package/dist/cli/commands/r2.js +196 -1
  81. package/dist/cli/commands/r2.js.map +1 -1
  82. package/dist/cli/commands/redeploy.js +41 -1
  83. package/dist/cli/commands/redeploy.js.map +1 -1
  84. package/dist/cli/commands/secrets.d.ts +23 -0
  85. package/dist/cli/commands/secrets.js +256 -0
  86. package/dist/cli/commands/secrets.js.map +1 -0
  87. package/dist/cli/commands/serve.js +143 -24
  88. package/dist/cli/commands/serve.js.map +1 -1
  89. package/dist/cli/commands/start.js +140 -71
  90. package/dist/cli/commands/start.js.map +1 -1
  91. package/dist/cli/commands/sync.d.ts +1 -1
  92. package/dist/cli/commands/sync.js +509 -163
  93. package/dist/cli/commands/sync.js.map +1 -1
  94. package/dist/cli/commands/user.js +8 -24
  95. package/dist/cli/commands/user.js.map +1 -1
  96. package/dist/cli/index.js +8 -7
  97. package/dist/cli/index.js.map +1 -1
  98. package/dist/cli/railway-provisioning.d.ts +53 -0
  99. package/dist/cli/railway-provisioning.js +85 -0
  100. package/dist/cli/railway-provisioning.js.map +1 -0
  101. package/dist/cli/scaffolding.d.ts +4 -2
  102. package/dist/cli/scaffolding.js +42 -47
  103. package/dist/cli/scaffolding.js.map +1 -1
  104. package/dist/cli/setup-wizard.d.ts +30 -49
  105. package/dist/cli/setup-wizard.js +35 -38
  106. package/dist/cli/setup-wizard.js.map +1 -1
  107. package/dist/core/accent-palette.d.ts +22 -0
  108. package/dist/core/accent-palette.js +47 -0
  109. package/dist/core/accent-palette.js.map +1 -0
  110. package/dist/core/entity-body-templates.d.ts +21 -0
  111. package/dist/core/entity-body-templates.js +48 -0
  112. package/dist/core/entity-body-templates.js.map +1 -0
  113. package/dist/core/entity-types-catalog.d.ts +65 -0
  114. package/dist/core/entity-types-catalog.js +136 -0
  115. package/dist/core/entity-types-catalog.js.map +1 -0
  116. package/dist/core/feature-flags.d.ts +22 -0
  117. package/dist/core/feature-flags.js +15 -0
  118. package/dist/core/feature-flags.js.map +1 -0
  119. package/dist/core/features.d.ts +16 -0
  120. package/dist/core/features.js +48 -0
  121. package/dist/core/features.js.map +1 -0
  122. package/dist/core/folder-paths.d.ts +41 -0
  123. package/dist/core/folder-paths.js +95 -0
  124. package/dist/core/folder-paths.js.map +1 -0
  125. package/dist/core/folder-sidecar.d.ts +16 -0
  126. package/dist/core/folder-sidecar.js +45 -0
  127. package/dist/core/folder-sidecar.js.map +1 -0
  128. package/dist/core/graph.d.ts +471 -19
  129. package/dist/core/graph.js +1411 -284
  130. package/dist/core/graph.js.map +1 -1
  131. package/dist/core/schema-registry.js +1 -19
  132. package/dist/core/schema-registry.js.map +1 -1
  133. package/dist/core/schemas/connector.d.ts +67 -0
  134. package/dist/core/schemas/connector.js +100 -0
  135. package/dist/core/schemas/connector.js.map +1 -0
  136. package/dist/core/schemas/workspace.d.ts +122 -0
  137. package/dist/core/schemas/workspace.js +211 -0
  138. package/dist/core/schemas/workspace.js.map +1 -0
  139. package/dist/core/secrets/SecretStore.d.ts +77 -0
  140. package/dist/core/secrets/SecretStore.js +13 -0
  141. package/dist/core/secrets/SecretStore.js.map +1 -0
  142. package/dist/core/secrets/SettingsResolver.d.ts +54 -0
  143. package/dist/core/secrets/SettingsResolver.js +80 -0
  144. package/dist/core/secrets/SettingsResolver.js.map +1 -0
  145. package/dist/core/secrets/SettingsStore.d.ts +30 -0
  146. package/dist/core/secrets/SettingsStore.js +9 -0
  147. package/dist/core/secrets/SettingsStore.js.map +1 -0
  148. package/dist/core/secrets/SqliteEncryptedStore.d.ts +78 -0
  149. package/dist/core/secrets/SqliteEncryptedStore.js +581 -0
  150. package/dist/core/secrets/SqliteEncryptedStore.js.map +1 -0
  151. package/dist/core/secrets/audit.d.ts +36 -0
  152. package/dist/core/secrets/audit.js +60 -0
  153. package/dist/core/secrets/audit.js.map +1 -0
  154. package/dist/core/secrets/auth-db-migration.d.ts +129 -0
  155. package/dist/core/secrets/auth-db-migration.js +653 -0
  156. package/dist/core/secrets/auth-db-migration.js.map +1 -0
  157. package/dist/core/secrets/bundle.d.ts +141 -0
  158. package/dist/core/secrets/bundle.js +435 -0
  159. package/dist/core/secrets/bundle.js.map +1 -0
  160. package/dist/core/secrets/dual-write.d.ts +76 -0
  161. package/dist/core/secrets/dual-write.js +236 -0
  162. package/dist/core/secrets/dual-write.js.map +1 -0
  163. package/dist/core/secrets/encryption.d.ts +44 -0
  164. package/dist/core/secrets/encryption.js +97 -0
  165. package/dist/core/secrets/encryption.js.map +1 -0
  166. package/dist/core/secrets/importer.d.ts +94 -0
  167. package/dist/core/secrets/importer.js +319 -0
  168. package/dist/core/secrets/importer.js.map +1 -0
  169. package/dist/core/secrets/index.d.ts +49 -0
  170. package/dist/core/secrets/index.js +74 -0
  171. package/dist/core/secrets/index.js.map +1 -0
  172. package/dist/core/secrets/master-key.d.ts +38 -0
  173. package/dist/core/secrets/master-key.js +122 -0
  174. package/dist/core/secrets/master-key.js.map +1 -0
  175. package/dist/core/secrets/probes/anthropic.d.ts +98 -0
  176. package/dist/core/secrets/probes/anthropic.js +300 -0
  177. package/dist/core/secrets/probes/anthropic.js.map +1 -0
  178. package/dist/core/secrets/probes/brave.d.ts +9 -0
  179. package/dist/core/secrets/probes/brave.js +15 -0
  180. package/dist/core/secrets/probes/brave.js.map +1 -0
  181. package/dist/core/secrets/probes/r2.d.ts +15 -0
  182. package/dist/core/secrets/probes/r2.js +14 -0
  183. package/dist/core/secrets/probes/r2.js.map +1 -0
  184. package/dist/core/secrets/probes/voyage.d.ts +13 -0
  185. package/dist/core/secrets/probes/voyage.js +52 -0
  186. package/dist/core/secrets/probes/voyage.js.map +1 -0
  187. package/dist/core/secrets/read-flip.d.ts +59 -0
  188. package/dist/core/secrets/read-flip.js +87 -0
  189. package/dist/core/secrets/read-flip.js.map +1 -0
  190. package/dist/core/secrets/registry.d.ts +188 -0
  191. package/dist/core/secrets/registry.js +531 -0
  192. package/dist/core/secrets/registry.js.map +1 -0
  193. package/dist/core/types.d.ts +70 -483
  194. package/dist/core/types.js +6 -3
  195. package/dist/core/types.js.map +1 -1
  196. package/dist/core/user-config.d.ts +52 -12
  197. package/dist/core/user-config.js +119 -14
  198. package/dist/core/user-config.js.map +1 -1
  199. package/dist/core/validation.d.ts +603 -1409
  200. package/dist/core/validation.js +248 -146
  201. package/dist/core/validation.js.map +1 -1
  202. package/dist/core/workspace-manager.d.ts +35 -14
  203. package/dist/core/workspace-manager.js +105 -56
  204. package/dist/core/workspace-manager.js.map +1 -1
  205. package/dist/core/workspace.d.ts +19 -5
  206. package/dist/core/workspace.js +66 -39
  207. package/dist/core/workspace.js.map +1 -1
  208. package/dist/mcp/connector-manager.d.ts +16 -1
  209. package/dist/mcp/connector-manager.js +44 -5
  210. package/dist/mcp/connector-manager.js.map +1 -1
  211. package/dist/mcp/server.d.ts +11 -3
  212. package/dist/mcp/server.js +30 -5
  213. package/dist/mcp/server.js.map +1 -1
  214. package/dist/mcp/tools.d.ts +27 -2
  215. package/dist/mcp/tools.js +544 -113
  216. package/dist/mcp/tools.js.map +1 -1
  217. package/dist/server/__tests__/helpers/graph-api.d.ts +18 -0
  218. package/dist/server/__tests__/helpers/graph-api.js +16 -0
  219. package/dist/server/__tests__/helpers/graph-api.js.map +1 -0
  220. package/dist/server/commit-audit.d.ts +26 -0
  221. package/dist/server/commit-audit.js +30 -0
  222. package/dist/server/commit-audit.js.map +1 -0
  223. package/dist/server/index.d.ts +14 -3
  224. package/dist/server/index.js +385 -177
  225. package/dist/server/index.js.map +1 -1
  226. package/dist/server/middleware/sanitize.d.ts +46 -0
  227. package/dist/server/middleware/sanitize.js +171 -0
  228. package/dist/server/middleware/sanitize.js.map +1 -0
  229. package/dist/server/routes/asset-api.js +217 -65
  230. package/dist/server/routes/asset-api.js.map +1 -1
  231. package/dist/server/routes/audit-api.d.ts +24 -0
  232. package/dist/server/routes/audit-api.js +117 -0
  233. package/dist/server/routes/audit-api.js.map +1 -0
  234. package/dist/server/routes/auth-api.js +220 -44
  235. package/dist/server/routes/auth-api.js.map +1 -1
  236. package/dist/server/routes/capture-api.d.ts +10 -3
  237. package/dist/server/routes/capture-api.js +180 -24
  238. package/dist/server/routes/capture-api.js.map +1 -1
  239. package/dist/server/routes/chat.d.ts +4 -1
  240. package/dist/server/routes/chat.js +178 -46
  241. package/dist/server/routes/chat.js.map +1 -1
  242. package/dist/server/routes/config-api.d.ts +21 -0
  243. package/dist/server/routes/config-api.js +256 -0
  244. package/dist/server/routes/config-api.js.map +1 -0
  245. package/dist/server/routes/connectors-api.js +24 -18
  246. package/dist/server/routes/connectors-api.js.map +1 -1
  247. package/dist/server/routes/event-ingest-api.d.ts +15 -0
  248. package/dist/server/routes/event-ingest-api.js +125 -0
  249. package/dist/server/routes/event-ingest-api.js.map +1 -0
  250. package/dist/server/routes/features-api.d.ts +19 -0
  251. package/dist/server/routes/features-api.js +28 -0
  252. package/dist/server/routes/features-api.js.map +1 -0
  253. package/dist/server/routes/git-http.d.ts +2 -2
  254. package/dist/server/routes/git-http.js +69 -34
  255. package/dist/server/routes/git-http.js.map +1 -1
  256. package/dist/server/routes/graph-api-access.d.ts +35 -0
  257. package/dist/server/routes/graph-api-access.js +93 -0
  258. package/dist/server/routes/graph-api-access.js.map +1 -0
  259. package/dist/server/routes/graph-api.d.ts +3 -2
  260. package/dist/server/routes/graph-api.js +1014 -315
  261. package/dist/server/routes/graph-api.js.map +1 -1
  262. package/dist/server/routes/health.d.ts +18 -0
  263. package/dist/server/routes/health.js +74 -0
  264. package/dist/server/routes/health.js.map +1 -0
  265. package/dist/server/routes/insights-api.js +156 -33
  266. package/dist/server/routes/insights-api.js.map +1 -1
  267. package/dist/server/routes/mcp.d.ts +6 -3
  268. package/dist/server/routes/mcp.js +22 -6
  269. package/dist/server/routes/mcp.js.map +1 -1
  270. package/dist/server/routes/messages-api.d.ts +3 -3
  271. package/dist/server/routes/messages-api.js +308 -120
  272. package/dist/server/routes/messages-api.js.map +1 -1
  273. package/dist/server/routes/permissions-api.d.ts +9 -2
  274. package/dist/server/routes/permissions-api.js +87 -31
  275. package/dist/server/routes/permissions-api.js.map +1 -1
  276. package/dist/server/routes/secrets-api.d.ts +36 -0
  277. package/dist/server/routes/secrets-api.js +308 -0
  278. package/dist/server/routes/secrets-api.js.map +1 -0
  279. package/dist/server/routes/settings-api.d.ts +29 -0
  280. package/dist/server/routes/settings-api.js +180 -0
  281. package/dist/server/routes/settings-api.js.map +1 -0
  282. package/dist/server/routes/workspace-api.d.ts +2 -1
  283. package/dist/server/routes/workspace-api.js +131 -20
  284. package/dist/server/routes/workspace-api.js.map +1 -1
  285. package/dist/server/routes/ws.d.ts +3 -2
  286. package/dist/server/routes/ws.js +68 -18
  287. package/dist/server/routes/ws.js.map +1 -1
  288. package/dist/server/session-manager.d.ts +48 -5
  289. package/dist/server/session-manager.js +175 -11
  290. package/dist/server/session-manager.js.map +1 -1
  291. package/dist/server/ws-hub.d.ts +134 -22
  292. package/dist/server/ws-hub.js +0 -0
  293. package/dist/server/ws-hub.js.map +1 -1
  294. package/dist/server/yjs-manager.d.ts +107 -7
  295. package/dist/server/yjs-manager.js +444 -38
  296. package/dist/server/yjs-manager.js.map +1 -1
  297. package/dist/server/yjs-persistence.d.ts +143 -0
  298. package/dist/server/yjs-persistence.js +543 -0
  299. package/dist/server/yjs-persistence.js.map +1 -0
  300. package/dist/server/yjs-text-diff.d.ts +32 -0
  301. package/dist/server/yjs-text-diff.js +61 -0
  302. package/dist/server/yjs-text-diff.js.map +1 -0
  303. package/dist/services/access-control.d.ts +53 -0
  304. package/dist/services/access-control.js +132 -0
  305. package/dist/services/access-control.js.map +1 -0
  306. package/dist/services/asset-upload-service.d.ts +53 -0
  307. package/dist/services/asset-upload-service.js +200 -0
  308. package/dist/services/asset-upload-service.js.map +1 -0
  309. package/dist/services/assets/base.d.ts +38 -0
  310. package/dist/services/assets/base.js +55 -0
  311. package/dist/services/assets/base.js.map +1 -1
  312. package/dist/services/assets/index.d.ts +41 -1
  313. package/dist/services/assets/index.js +65 -0
  314. package/dist/services/assets/index.js.map +1 -1
  315. package/dist/services/assets/local.d.ts +7 -1
  316. package/dist/services/assets/local.js +83 -0
  317. package/dist/services/assets/local.js.map +1 -1
  318. package/dist/services/assets/r2.d.ts +28 -1
  319. package/dist/services/assets/r2.js +75 -1
  320. package/dist/services/assets/r2.js.map +1 -1
  321. package/dist/services/auth-service.d.ts +184 -36
  322. package/dist/services/auth-service.js +763 -138
  323. package/dist/services/auth-service.js.map +1 -1
  324. package/dist/services/briefing.d.ts +1 -1
  325. package/dist/services/briefing.js +12 -35
  326. package/dist/services/briefing.js.map +1 -1
  327. package/dist/services/event-processor.d.ts +104 -0
  328. package/dist/services/event-processor.js +443 -0
  329. package/dist/services/event-processor.js.map +1 -0
  330. package/dist/services/folder-creation.d.ts +33 -0
  331. package/dist/services/folder-creation.js +103 -0
  332. package/dist/services/folder-creation.js.map +1 -0
  333. package/dist/services/git.d.ts +44 -0
  334. package/dist/services/git.js +162 -55
  335. package/dist/services/git.js.map +1 -1
  336. package/dist/services/heartbeat.d.ts +19 -12
  337. package/dist/services/heartbeat.js +162 -107
  338. package/dist/services/heartbeat.js.map +1 -1
  339. package/dist/services/import-service.d.ts +37 -2
  340. package/dist/services/import-service.js +222 -123
  341. package/dist/services/import-service.js.map +1 -1
  342. package/dist/services/lint-service.js +7 -14
  343. package/dist/services/lint-service.js.map +1 -1
  344. package/dist/services/markdown.js +6 -2
  345. package/dist/services/markdown.js.map +1 -1
  346. package/dist/services/message-service.d.ts +62 -21
  347. package/dist/services/message-service.js +160 -43
  348. package/dist/services/message-service.js.map +1 -1
  349. package/dist/services/personal-folder.d.ts +40 -0
  350. package/dist/services/personal-folder.js +101 -0
  351. package/dist/services/personal-folder.js.map +1 -0
  352. package/dist/services/vector-service.d.ts +73 -3
  353. package/dist/services/vector-service.js +106 -7
  354. package/dist/services/vector-service.js.map +1 -1
  355. package/dist/services/workspace-access.d.ts +106 -0
  356. package/dist/services/workspace-access.js +149 -0
  357. package/dist/services/workspace-access.js.map +1 -0
  358. package/dist/utils/git.js +3 -3
  359. package/dist/utils/git.js.map +1 -1
  360. package/dist/utils/log.d.ts +42 -0
  361. package/dist/utils/log.js +137 -0
  362. package/dist/utils/log.js.map +1 -0
  363. package/dist/utils/preflight.js +2 -2
  364. package/dist/utils/preflight.js.map +1 -1
  365. package/dist/utils/version-checker.d.ts +4 -1
  366. package/dist/utils/version-checker.js +8 -8
  367. package/dist/utils/version-checker.js.map +1 -1
  368. package/dist/web/_app/immutable/assets/0.Bp6xXZUi.css +2 -0
  369. package/dist/web/_app/immutable/assets/11.DlIf1mKh.css +1 -0
  370. package/dist/web/_app/immutable/assets/12.Dmlt28l2.css +1 -0
  371. package/dist/web/_app/immutable/assets/13.CmLrbzZp.css +1 -0
  372. package/dist/web/_app/immutable/assets/2.oo1ac0x_.css +1 -0
  373. package/dist/web/_app/immutable/assets/3.DJJsQzDb.css +1 -0
  374. package/dist/web/_app/immutable/assets/4.CZ-fNJ91.css +1 -0
  375. package/dist/web/_app/immutable/assets/AgentOverlay.BfWESlQU.css +1 -0
  376. package/dist/web/_app/immutable/assets/AssetManagerModal.ZfasE2L3.css +1 -0
  377. package/dist/web/_app/immutable/assets/CalendarView.CeNagvyK.css +1 -0
  378. package/dist/web/_app/immutable/assets/ChatPanel.po0gYtVH.css +1 -0
  379. package/dist/web/_app/immutable/assets/FolderMembersPanel.Bepw-r0F.css +1 -0
  380. package/dist/web/_app/immutable/assets/FolderMutationDialogs.BMRF6Z3z.css +1 -0
  381. package/dist/web/_app/immutable/assets/FolderPicker.B5KdvRYl.css +1 -0
  382. package/dist/web/_app/immutable/assets/GalleryView.Dy-qQjEc.css +1 -0
  383. package/dist/web/_app/immutable/assets/GraphView.BHnGIltS.css +1 -0
  384. package/dist/web/_app/immutable/assets/KanbanView.G3NZ0uHb.css +1 -0
  385. package/dist/web/_app/immutable/assets/SettingsDialog.tBn8y9DD.css +1 -0
  386. package/dist/web/_app/immutable/assets/Spinner.UBfl0pab.css +1 -0
  387. package/dist/web/_app/immutable/assets/TagFilter.DNipjifq.css +1 -0
  388. package/dist/web/_app/immutable/assets/TagPickerSheet.DJPuqnTg.css +1 -0
  389. package/dist/web/_app/immutable/assets/WorkspaceView.B9zA92UB.css +1 -0
  390. package/dist/web/_app/immutable/assets/button.BlLm4Dg1.css +1 -0
  391. package/dist/web/_app/immutable/assets/dialog.BxVal_19.css +1 -0
  392. package/dist/web/_app/immutable/assets/dist.B5hGjnfj.css +1 -0
  393. package/dist/web/_app/immutable/assets/dist.DDCWxn3B.css +1 -0
  394. package/dist/web/_app/immutable/assets/dropdown-menu.BSsUUQ3o.css +1 -0
  395. package/dist/web/_app/immutable/assets/entity-row.Bl5FyNCt.css +1 -0
  396. package/dist/web/_app/immutable/assets/greeting.Bm7lTneV.css +1 -0
  397. package/dist/web/_app/immutable/assets/ibm-plex-mono-latin-400-normal.CvHOgSBP.woff +0 -0
  398. package/dist/web/_app/immutable/assets/ibm-plex-mono-latin-400-normal.DMJ8VG8y.woff2 +0 -0
  399. package/dist/web/_app/immutable/assets/ibm-plex-mono-latin-500-normal.CB9ihrfo.woff +0 -0
  400. package/dist/web/_app/immutable/assets/ibm-plex-mono-latin-500-normal.DSY6xOcd.woff2 +0 -0
  401. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-400-italic.CZTNEAuW.woff2 +0 -0
  402. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-400-italic.CsGl1sm0.woff +0 -0
  403. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-400-normal.CDDApCn2.woff2 +0 -0
  404. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-400-normal.CYLoc0-x.woff +0 -0
  405. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-500-italic.BNK2_mGO.woff2 +0 -0
  406. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-500-italic.DpEwFAQM.woff +0 -0
  407. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-500-normal.6ng42L7E.woff2 +0 -0
  408. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-500-normal.BgVn5rGT.woff +0 -0
  409. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-600-normal.Cu4Hd6ag.woff +0 -0
  410. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-600-normal.CuJfVYMP.woff2 +0 -0
  411. package/dist/web/_app/immutable/assets/inline-list.DORLo4xF.css +1 -0
  412. package/dist/web/_app/immutable/assets/notifications.clodJdp5.css +1 -0
  413. package/dist/web/_app/immutable/assets/radio-group.GJJRVPUJ.css +1 -0
  414. package/dist/web/_app/immutable/assets/select.D_AV992j.css +1 -0
  415. package/dist/web/_app/immutable/chunks/-KAQXGuI2.js +1 -0
  416. package/dist/web/_app/immutable/chunks/-wzwcH00.js +1 -0
  417. package/dist/web/_app/immutable/chunks/07l_1Zpm2.js +1 -0
  418. package/dist/web/_app/immutable/chunks/38oWKNzM2.js +6 -0
  419. package/dist/web/_app/immutable/chunks/54VmiJfH.js +1 -0
  420. package/dist/web/_app/immutable/chunks/6LRpFvjo.js +185 -0
  421. package/dist/web/_app/immutable/chunks/7R0Rp3Mz.js +1 -0
  422. package/dist/web/_app/immutable/chunks/9bCTHkeS.js +1 -0
  423. package/dist/web/_app/immutable/chunks/B-822WnD2.js +22 -0
  424. package/dist/web/_app/immutable/chunks/B0HUvliX2.js +2 -0
  425. package/dist/web/_app/immutable/chunks/B12eXGpE.js +1 -0
  426. package/dist/web/_app/immutable/chunks/B5af0X0p2.js +1 -0
  427. package/dist/web/_app/immutable/chunks/B6iYNwo72.js +1 -0
  428. package/dist/web/_app/immutable/chunks/B7EMtoAB.js +1 -0
  429. package/dist/web/_app/immutable/chunks/B7L4gmPR.js +2 -0
  430. package/dist/web/_app/immutable/chunks/B9qBGxh62.js +3 -0
  431. package/dist/web/_app/immutable/chunks/BDJYj9yi.js +2 -0
  432. package/dist/web/_app/immutable/chunks/BEpMEjHO.js +1 -0
  433. package/dist/web/_app/immutable/chunks/BErNpj-S2.js +1 -0
  434. package/dist/web/_app/immutable/chunks/BHBk4zfV.js +1 -0
  435. package/dist/web/_app/immutable/chunks/BHH-JGNF.js +1 -0
  436. package/dist/web/_app/immutable/chunks/BKe5wPiN.js +1 -0
  437. package/dist/web/_app/immutable/chunks/BNTlsKDJ.js +1 -0
  438. package/dist/web/_app/immutable/chunks/BOgcXgis2.js +1 -0
  439. package/dist/web/_app/immutable/chunks/BOyyDrjn2.js +1 -0
  440. package/dist/web/_app/immutable/chunks/BPR_ufQ0.js +41 -0
  441. package/dist/web/_app/immutable/chunks/BRcFsc6v.js +5 -0
  442. package/dist/web/_app/immutable/chunks/BT3KAag_.js +3 -0
  443. package/dist/web/_app/immutable/chunks/BX2T6xiZ.js +224 -0
  444. package/dist/web/_app/immutable/chunks/BZnJ8tbJ.js +1 -0
  445. package/dist/web/_app/immutable/chunks/Ba1Yzb7u.js +1 -0
  446. package/dist/web/_app/immutable/chunks/BcsvgA3B2.js +1 -0
  447. package/dist/web/_app/immutable/chunks/BfbS7rGT.js +1 -0
  448. package/dist/web/_app/immutable/chunks/BgagL41Z.js +1 -0
  449. package/dist/web/_app/immutable/chunks/BiUh0Sik.js +1 -0
  450. package/dist/web/_app/immutable/chunks/Biajc_oU.js +1 -0
  451. package/dist/web/_app/immutable/chunks/Bl9Pzopy.js +1 -0
  452. package/dist/web/_app/immutable/chunks/BmcU05mO.js +1 -0
  453. package/dist/web/_app/immutable/chunks/Bmgp0jlT.js +1 -0
  454. package/dist/web/_app/immutable/chunks/BmgyOoEp.js +1 -0
  455. package/dist/web/_app/immutable/chunks/Bq0-jBGC2.js +1 -0
  456. package/dist/web/_app/immutable/chunks/BqGj0_GS.js +1 -0
  457. package/dist/web/_app/immutable/chunks/Bsc9K3Pz.js +1 -0
  458. package/dist/web/_app/immutable/chunks/Bssb691Q.js +1 -0
  459. package/dist/web/_app/immutable/chunks/BuzQceOn.js +1 -0
  460. package/dist/web/_app/immutable/chunks/BzeVNab0.js +1 -0
  461. package/dist/web/_app/immutable/chunks/C-OaypFN.js +1 -0
  462. package/dist/web/_app/immutable/chunks/C0fjDjvE2.js +1 -0
  463. package/dist/web/_app/immutable/chunks/C0pTXwEX.js +2 -0
  464. package/dist/web/_app/immutable/chunks/C3Enww9L2.js +1 -0
  465. package/dist/web/_app/immutable/chunks/C3oa0Cks.js +1 -0
  466. package/dist/web/_app/immutable/chunks/CC-YWp-f.js +1 -0
  467. package/dist/web/_app/immutable/chunks/CGdKvXV92.js +1 -0
  468. package/dist/web/_app/immutable/chunks/CH0siYFL.js +1 -0
  469. package/dist/web/_app/immutable/chunks/CIchr8Tk2.js +1 -0
  470. package/dist/web/_app/immutable/chunks/CK_yXDQI.js +1 -0
  471. package/dist/web/_app/immutable/chunks/CLPtMig0.js +1 -0
  472. package/dist/web/_app/immutable/chunks/CLSxJmTj.js +2 -0
  473. package/dist/web/_app/immutable/chunks/CPQNcHIB.js +3 -0
  474. package/dist/web/_app/immutable/chunks/CPt2D5aU.js +1 -0
  475. package/dist/web/_app/immutable/chunks/CQ-2yccd.js +7 -0
  476. package/dist/web/_app/immutable/chunks/CS6VEwn7.js +1 -0
  477. package/dist/web/_app/immutable/chunks/CSGIGrrK.js +1 -0
  478. package/dist/web/_app/immutable/chunks/CSsDOIrk.js +1 -0
  479. package/dist/web/_app/immutable/chunks/CVXvEWhv2.js +1 -0
  480. package/dist/web/_app/immutable/chunks/CaChtuYU2.js +1 -0
  481. package/dist/web/_app/immutable/chunks/CbiwvM3R2.js +1 -0
  482. package/dist/web/_app/immutable/chunks/CdCuBMLm.js +156 -0
  483. package/dist/web/_app/immutable/chunks/CfqjDDMu.js +1 -0
  484. package/dist/web/_app/immutable/chunks/CjGzUnEF2.js +1 -0
  485. package/dist/web/_app/immutable/chunks/Cjgbiel7.js +1 -0
  486. package/dist/web/_app/immutable/chunks/ClNEskRf.js +1 -0
  487. package/dist/web/_app/immutable/chunks/CnqSKqA62.js +1 -0
  488. package/dist/web/_app/immutable/chunks/CqyqKPuh2.js +1 -0
  489. package/dist/web/_app/immutable/chunks/CrnX-oFV.js +1 -0
  490. package/dist/web/_app/immutable/chunks/CsGahE3a.js +1 -0
  491. package/dist/web/_app/immutable/chunks/CsXIliss.js +1 -0
  492. package/dist/web/_app/immutable/chunks/Ct3quP0F.js +1 -0
  493. package/dist/web/_app/immutable/chunks/CvZaxjRC.js +1 -0
  494. package/dist/web/_app/immutable/chunks/CwD4G56R.js +5 -0
  495. package/dist/web/_app/immutable/chunks/CxalqrMB.js +1 -0
  496. package/dist/web/_app/immutable/chunks/D1Ap3evT2.js +1 -0
  497. package/dist/web/_app/immutable/chunks/D7qgUfnr.js +2487 -0
  498. package/dist/web/_app/immutable/chunks/D9-_Euza2.js +1 -0
  499. package/dist/web/_app/immutable/chunks/DCCaoKt42.js +1 -0
  500. package/dist/web/_app/immutable/chunks/DDKweOZr2.js +23 -0
  501. package/dist/web/_app/immutable/chunks/DHeU-hHI2.js +2 -0
  502. package/dist/web/_app/immutable/chunks/DHepUItL.js +1 -0
  503. package/dist/web/_app/immutable/chunks/DL_KbCKq.js +8 -0
  504. package/dist/web/_app/immutable/chunks/DNtqqgv32.js +1 -0
  505. package/dist/web/_app/immutable/chunks/DRl7vwNC2.js +184 -0
  506. package/dist/web/_app/immutable/chunks/DTc5W_V6.js +1 -0
  507. package/dist/web/_app/immutable/chunks/DZetJu9k2.js +1 -0
  508. package/dist/web/_app/immutable/chunks/DaiJs3St.js +1 -0
  509. package/dist/web/_app/immutable/chunks/DhuJRrEX2.js +1 -0
  510. package/dist/web/_app/immutable/chunks/DiP2EplP.js +3 -0
  511. package/dist/web/_app/immutable/chunks/Dm25ot5x2.js +1 -0
  512. package/dist/web/_app/immutable/chunks/DqOcb9RX2.js +83 -0
  513. package/dist/web/_app/immutable/chunks/DrbUJCAE.js +1 -0
  514. package/dist/web/_app/immutable/chunks/Dt4HY2cT2.js +2 -0
  515. package/dist/web/_app/immutable/chunks/DtGUCMv8.js +6 -0
  516. package/dist/web/_app/immutable/chunks/Du2-B3c8.js +1 -0
  517. package/dist/web/_app/immutable/chunks/DwdkE4UD2.js +1 -0
  518. package/dist/web/_app/immutable/chunks/DxWesPbn.js +1 -0
  519. package/dist/web/_app/immutable/chunks/DyYZqUV72.js +1 -0
  520. package/dist/web/_app/immutable/chunks/HfUnhvrv2.js +1 -0
  521. package/dist/web/_app/immutable/chunks/Je8-BtCk2.js +1 -0
  522. package/dist/web/_app/immutable/chunks/LAN53VbP2.js +5 -0
  523. package/dist/web/_app/immutable/chunks/MSB7eA4_.js +1 -0
  524. package/dist/web/_app/immutable/chunks/NPe9WyW1.js +1 -0
  525. package/dist/web/_app/immutable/chunks/O6IzcxXJ.js +2 -0
  526. package/dist/web/_app/immutable/chunks/SXhc9rJW.js +1 -0
  527. package/dist/web/_app/immutable/chunks/TuBvdkhV2.js +1 -0
  528. package/dist/web/_app/immutable/chunks/U40J2SnU.js +1 -0
  529. package/dist/web/_app/immutable/chunks/VS3l21Gu.js +1 -0
  530. package/dist/web/_app/immutable/chunks/YEPEvlbU2.js +1 -0
  531. package/dist/web/_app/immutable/chunks/Z7TNRcgK.js +11 -0
  532. package/dist/web/_app/immutable/chunks/ZCekGjo6.js +5 -0
  533. package/dist/web/_app/immutable/chunks/_rnxQ655.js +4 -0
  534. package/dist/web/_app/immutable/chunks/dTXrX5Bw.js +2 -0
  535. package/dist/web/_app/immutable/chunks/gNXLhsdy.js +1 -0
  536. package/dist/web/_app/immutable/chunks/hKSTEN36.js +1 -0
  537. package/dist/web/_app/immutable/chunks/jZZTbzL5.js +1 -0
  538. package/dist/web/_app/immutable/chunks/l4FIBKss.js +1 -0
  539. package/dist/web/_app/immutable/chunks/l_4snZnh2.js +1 -0
  540. package/dist/web/_app/immutable/chunks/q6R6T-lK.js +1 -0
  541. package/dist/web/_app/immutable/chunks/qgprkMcL.js +1 -0
  542. package/dist/web/_app/immutable/chunks/rOisBLC62.js +14 -0
  543. package/dist/web/_app/immutable/chunks/t08TznXc2.js +1 -0
  544. package/dist/web/_app/immutable/chunks/t62M88qj.js +1 -0
  545. package/dist/web/_app/immutable/chunks/uCohGWC42.js +1 -0
  546. package/dist/web/_app/immutable/chunks/uqxmB78b.js +1 -0
  547. package/dist/web/_app/immutable/chunks/vwTMEIJQ.js +1 -0
  548. package/dist/web/_app/immutable/chunks/w1FCELam.js +64 -0
  549. package/dist/web/_app/immutable/chunks/zmjgin7A.js +1 -0
  550. package/dist/web/_app/immutable/chunks/zumDjcgJ.js +1 -0
  551. package/dist/web/_app/immutable/entry/app.B3JEO5sy.js +2 -0
  552. package/dist/web/_app/immutable/entry/start.Dk84vnrL.js +1 -0
  553. package/dist/web/_app/immutable/nodes/0.BKLFgYTV.js +2 -0
  554. package/dist/web/_app/immutable/nodes/1.Djf_1kN1.js +1 -0
  555. package/dist/web/_app/immutable/nodes/10.De6eGWLx.js +1 -0
  556. package/dist/web/_app/immutable/nodes/11.CDsfSRTw.js +1 -0
  557. package/dist/web/_app/immutable/nodes/12.BeKkKpkY.js +1 -0
  558. package/dist/web/_app/immutable/nodes/13.BcI8xwSy.js +1 -0
  559. package/dist/web/_app/immutable/nodes/2.ZMZ-QAZ7.js +106 -0
  560. package/dist/web/_app/immutable/nodes/3.C1ZiAfle.js +2 -0
  561. package/dist/web/_app/immutable/nodes/4.Chmqy9KF.js +2 -0
  562. package/dist/web/_app/immutable/nodes/5.Bpvr_Vcq.js +1 -0
  563. package/dist/web/_app/immutable/nodes/6.BHa_gOs3.js +1 -0
  564. package/dist/web/_app/immutable/nodes/7.D0hg4Eap.js +1 -0
  565. package/dist/web/_app/immutable/nodes/8.3ZUUxJqR.js +1 -0
  566. package/dist/web/_app/immutable/nodes/9.Ducy-oqR.js +1 -0
  567. package/dist/web/_app/version.json +1 -1
  568. package/dist/web/favicon-16.png +0 -0
  569. package/dist/web/favicon-180.png +0 -0
  570. package/dist/web/favicon-32.png +0 -0
  571. package/dist/web/favicon-48.png +0 -0
  572. package/dist/web/favicon-512.png +0 -0
  573. package/dist/web/favicon.ico +0 -0
  574. package/dist/web/favicon.svg +8 -0
  575. package/dist/web/index.html +64 -21
  576. package/dist/web/studiograph-logomark.svg +29 -0
  577. package/package.json +28 -14
  578. package/dist/agent/role-loader.d.ts +0 -24
  579. package/dist/agent/role-loader.js +0 -67
  580. package/dist/agent/role-loader.js.map +0 -1
  581. package/dist/agent/skills/enrich-entities.md +0 -136
  582. package/dist/agent/skills/obsidian-source-setup.md +0 -246
  583. package/dist/agent/skills/skill-loader.d.ts +0 -48
  584. package/dist/agent/skills/skill-loader.js +0 -166
  585. package/dist/agent/skills/skill-loader.js.map +0 -1
  586. package/dist/agent/skills/sync-configuration.md +0 -119
  587. package/dist/agent/skills/sync-setup.md +0 -82
  588. package/dist/agent/tools/sync-tools.d.ts +0 -35
  589. package/dist/agent/tools/sync-tools.js +0 -992
  590. package/dist/agent/tools/sync-tools.js.map +0 -1
  591. package/dist/auth/github.d.ts +0 -56
  592. package/dist/auth/github.js +0 -169
  593. package/dist/auth/github.js.map +0 -1
  594. package/dist/cli/commands/access.d.ts +0 -11
  595. package/dist/cli/commands/access.js +0 -156
  596. package/dist/cli/commands/access.js.map +0 -1
  597. package/dist/cli/commands/app.d.ts +0 -7
  598. package/dist/cli/commands/app.js +0 -268
  599. package/dist/cli/commands/app.js.map +0 -1
  600. package/dist/cli/commands/auth.d.ts +0 -10
  601. package/dist/cli/commands/auth.js +0 -79
  602. package/dist/cli/commands/auth.js.map +0 -1
  603. package/dist/cli/commands/collection.d.ts +0 -10
  604. package/dist/cli/commands/collection.js +0 -185
  605. package/dist/cli/commands/collection.js.map +0 -1
  606. package/dist/cli/commands/enrich.d.ts +0 -11
  607. package/dist/cli/commands/enrich.js +0 -135
  608. package/dist/cli/commands/enrich.js.map +0 -1
  609. package/dist/cli/commands/graphrag.d.ts +0 -12
  610. package/dist/cli/commands/graphrag.js +0 -122
  611. package/dist/cli/commands/graphrag.js.map +0 -1
  612. package/dist/cli/commands/members.d.ts +0 -11
  613. package/dist/cli/commands/members.js +0 -230
  614. package/dist/cli/commands/members.js.map +0 -1
  615. package/dist/cli/commands/org.d.ts +0 -10
  616. package/dist/cli/commands/org.js +0 -132
  617. package/dist/cli/commands/org.js.map +0 -1
  618. package/dist/cli/commands/provision.d.ts +0 -8
  619. package/dist/cli/commands/provision.js +0 -116
  620. package/dist/cli/commands/provision.js.map +0 -1
  621. package/dist/cli/commands/resolve.d.ts +0 -8
  622. package/dist/cli/commands/resolve.js +0 -85
  623. package/dist/cli/commands/resolve.js.map +0 -1
  624. package/dist/cli/commands/review.d.ts +0 -19
  625. package/dist/cli/commands/review.js +0 -128
  626. package/dist/cli/commands/review.js.map +0 -1
  627. package/dist/cli/commands/source.d.ts +0 -16
  628. package/dist/cli/commands/source.js +0 -159
  629. package/dist/cli/commands/source.js.map +0 -1
  630. package/dist/cli/commands/sync-collection.d.ts +0 -14
  631. package/dist/cli/commands/sync-collection.js +0 -355
  632. package/dist/cli/commands/sync-collection.js.map +0 -1
  633. package/dist/cli/commands/sync-entities.d.ts +0 -13
  634. package/dist/cli/commands/sync-entities.js +0 -242
  635. package/dist/cli/commands/sync-entities.js.map +0 -1
  636. package/dist/cli/commands/team.d.ts +0 -12
  637. package/dist/cli/commands/team.js +0 -185
  638. package/dist/cli/commands/team.js.map +0 -1
  639. package/dist/cli/sync-review-interactive.d.ts +0 -31
  640. package/dist/cli/sync-review-interactive.js +0 -393
  641. package/dist/cli/sync-review-interactive.js.map +0 -1
  642. package/dist/core/migration-runner.d.ts +0 -42
  643. package/dist/core/migration-runner.js +0 -232
  644. package/dist/core/migration-runner.js.map +0 -1
  645. package/dist/core/migration-types.d.ts +0 -101
  646. package/dist/core/migration-types.js +0 -21
  647. package/dist/core/migration-types.js.map +0 -1
  648. package/dist/core/migrations/20260219-formalize-memory-location.d.ts +0 -2
  649. package/dist/core/migrations/20260219-formalize-memory-location.js +0 -35
  650. package/dist/core/migrations/20260219-formalize-memory-location.js.map +0 -1
  651. package/dist/core/migrations/20260220-add-workspace-metadata.d.ts +0 -12
  652. package/dist/core/migrations/20260220-add-workspace-metadata.js +0 -65
  653. package/dist/core/migrations/20260220-add-workspace-metadata.js.map +0 -1
  654. package/dist/core/migrations/20260220-add-workspace-readme.d.ts +0 -11
  655. package/dist/core/migrations/20260220-add-workspace-readme.js +0 -82
  656. package/dist/core/migrations/20260220-add-workspace-readme.js.map +0 -1
  657. package/dist/core/migrations/20260220-migrate-yaml-to-json.d.ts +0 -9
  658. package/dist/core/migrations/20260220-migrate-yaml-to-json.js +0 -64
  659. package/dist/core/migrations/20260220-migrate-yaml-to-json.js.map +0 -1
  660. package/dist/core/migrations/index.d.ts +0 -11
  661. package/dist/core/migrations/index.js +0 -23
  662. package/dist/core/migrations/index.js.map +0 -1
  663. package/dist/integrations/asana.d.ts +0 -26
  664. package/dist/integrations/asana.js +0 -78
  665. package/dist/integrations/asana.js.map +0 -1
  666. package/dist/integrations/figma-local.d.ts +0 -16
  667. package/dist/integrations/figma-local.js +0 -10
  668. package/dist/integrations/figma-local.js.map +0 -1
  669. package/dist/integrations/figma.d.ts +0 -17
  670. package/dist/integrations/figma.js +0 -17
  671. package/dist/integrations/figma.js.map +0 -1
  672. package/dist/integrations/granola.d.ts +0 -24
  673. package/dist/integrations/granola.js +0 -60
  674. package/dist/integrations/granola.js.map +0 -1
  675. package/dist/integrations/linear.d.ts +0 -14
  676. package/dist/integrations/linear.js +0 -80
  677. package/dist/integrations/linear.js.map +0 -1
  678. package/dist/integrations/paper-local.d.ts +0 -14
  679. package/dist/integrations/paper-local.js +0 -10
  680. package/dist/integrations/paper-local.js.map +0 -1
  681. package/dist/integrations/paper.d.ts +0 -2
  682. package/dist/integrations/paper.js +0 -10
  683. package/dist/integrations/paper.js.map +0 -1
  684. package/dist/integrations/pipedrive.d.ts +0 -26
  685. package/dist/integrations/pipedrive.js +0 -97
  686. package/dist/integrations/pipedrive.js.map +0 -1
  687. package/dist/integrations/registry.d.ts +0 -8
  688. package/dist/integrations/registry.js +0 -7
  689. package/dist/integrations/registry.js.map +0 -1
  690. package/dist/integrations/types.d.ts +0 -43
  691. package/dist/integrations/types.js +0 -5
  692. package/dist/integrations/types.js.map +0 -1
  693. package/dist/lib/lib/utils.d.ts +0 -2
  694. package/dist/lib/lib/utils.js +0 -6
  695. package/dist/lib/lib/utils.js.map +0 -1
  696. package/dist/mcp/connectors/asana.d.ts +0 -2
  697. package/dist/mcp/connectors/asana.js +0 -20
  698. package/dist/mcp/connectors/asana.js.map +0 -1
  699. package/dist/mcp/connectors/definitions.d.ts +0 -45
  700. package/dist/mcp/connectors/definitions.js +0 -32
  701. package/dist/mcp/connectors/definitions.js.map +0 -1
  702. package/dist/mcp/connectors/figma.d.ts +0 -5
  703. package/dist/mcp/connectors/figma.js +0 -21
  704. package/dist/mcp/connectors/figma.js.map +0 -1
  705. package/dist/mcp/connectors/gdrive.d.ts +0 -2
  706. package/dist/mcp/connectors/gdrive.js +0 -20
  707. package/dist/mcp/connectors/gdrive.js.map +0 -1
  708. package/dist/mcp/connectors/granola.d.ts +0 -2
  709. package/dist/mcp/connectors/granola.js +0 -12
  710. package/dist/mcp/connectors/granola.js.map +0 -1
  711. package/dist/mcp/connectors/linear.d.ts +0 -2
  712. package/dist/mcp/connectors/linear.js +0 -19
  713. package/dist/mcp/connectors/linear.js.map +0 -1
  714. package/dist/mcp/connectors/obsidian.d.ts +0 -2
  715. package/dist/mcp/connectors/obsidian.js +0 -19
  716. package/dist/mcp/connectors/obsidian.js.map +0 -1
  717. package/dist/mcp/connectors/pipedrive.d.ts +0 -2
  718. package/dist/mcp/connectors/pipedrive.js +0 -20
  719. package/dist/mcp/connectors/pipedrive.js.map +0 -1
  720. package/dist/mcp/connectors/slack.d.ts +0 -2
  721. package/dist/mcp/connectors/slack.js +0 -21
  722. package/dist/mcp/connectors/slack.js.map +0 -1
  723. package/dist/mcp/server-oauth-provider.d.ts +0 -56
  724. package/dist/mcp/server-oauth-provider.js +0 -142
  725. package/dist/mcp/server-oauth-provider.js.map +0 -1
  726. package/dist/server/agents/agent-loader.d.ts +0 -16
  727. package/dist/server/agents/agent-loader.js +0 -175
  728. package/dist/server/agents/agent-loader.js.map +0 -1
  729. package/dist/server/agents/agent-runner.d.ts +0 -33
  730. package/dist/server/agents/agent-runner.js +0 -187
  731. package/dist/server/agents/agent-runner.js.map +0 -1
  732. package/dist/server/agents/agent-runtime.d.ts +0 -39
  733. package/dist/server/agents/agent-runtime.js +0 -214
  734. package/dist/server/agents/agent-runtime.js.map +0 -1
  735. package/dist/server/agents/base-agent.d.ts +0 -30
  736. package/dist/server/agents/base-agent.js +0 -88
  737. package/dist/server/agents/base-agent.js.map +0 -1
  738. package/dist/server/agents/executive-digest.d.ts +0 -16
  739. package/dist/server/agents/executive-digest.js +0 -115
  740. package/dist/server/agents/executive-digest.js.map +0 -1
  741. package/dist/server/agents/graph-steward.d.ts +0 -16
  742. package/dist/server/agents/graph-steward.js +0 -59
  743. package/dist/server/agents/graph-steward.js.map +0 -1
  744. package/dist/server/agents/meeting-followup.d.ts +0 -16
  745. package/dist/server/agents/meeting-followup.js +0 -88
  746. package/dist/server/agents/meeting-followup.js.map +0 -1
  747. package/dist/server/agents/pipeline-monitor.d.ts +0 -15
  748. package/dist/server/agents/pipeline-monitor.js +0 -86
  749. package/dist/server/agents/pipeline-monitor.js.map +0 -1
  750. package/dist/server/agents/types.d.ts +0 -81
  751. package/dist/server/agents/types.js +0 -41
  752. package/dist/server/agents/types.js.map +0 -1
  753. package/dist/server/chrome/chrome.css +0 -691
  754. package/dist/server/chrome/chrome.js +0 -374
  755. package/dist/server/collab-authority.d.ts +0 -70
  756. package/dist/server/collab-authority.js +0 -218
  757. package/dist/server/collab-authority.js.map +0 -1
  758. package/dist/server/collab.d.ts +0 -29
  759. package/dist/server/collab.js +0 -195
  760. package/dist/server/collab.js.map +0 -1
  761. package/dist/server/commit-scheduler.d.ts +0 -39
  762. package/dist/server/commit-scheduler.js +0 -113
  763. package/dist/server/commit-scheduler.js.map +0 -1
  764. package/dist/server/plugin-loader.d.ts +0 -53
  765. package/dist/server/plugin-loader.js +0 -155
  766. package/dist/server/plugin-loader.js.map +0 -1
  767. package/dist/server/routes/agents-api.d.ts +0 -10
  768. package/dist/server/routes/agents-api.js +0 -25
  769. package/dist/server/routes/agents-api.js.map +0 -1
  770. package/dist/server/routes/collab.d.ts +0 -6
  771. package/dist/server/routes/collab.js +0 -10
  772. package/dist/server/routes/collab.js.map +0 -1
  773. package/dist/server/routes/git-api.d.ts +0 -9
  774. package/dist/server/routes/git-api.js +0 -82
  775. package/dist/server/routes/git-api.js.map +0 -1
  776. package/dist/server/routes/meeting-ingest-api.d.ts +0 -14
  777. package/dist/server/routes/meeting-ingest-api.js +0 -98
  778. package/dist/server/routes/meeting-ingest-api.js.map +0 -1
  779. package/dist/server/routes/sync-api.d.ts +0 -26
  780. package/dist/server/routes/sync-api.js +0 -848
  781. package/dist/server/routes/sync-api.js.map +0 -1
  782. package/dist/server/routes/webhook.d.ts +0 -14
  783. package/dist/server/routes/webhook.js +0 -102
  784. package/dist/server/routes/webhook.js.map +0 -1
  785. package/dist/services/batch-processor.d.ts +0 -49
  786. package/dist/services/batch-processor.js +0 -166
  787. package/dist/services/batch-processor.js.map +0 -1
  788. package/dist/services/csv-service.d.ts +0 -36
  789. package/dist/services/csv-service.js +0 -143
  790. package/dist/services/csv-service.js.map +0 -1
  791. package/dist/services/github-provisioner.d.ts +0 -36
  792. package/dist/services/github-provisioner.js +0 -126
  793. package/dist/services/github-provisioner.js.map +0 -1
  794. package/dist/services/github-service.d.ts +0 -99
  795. package/dist/services/github-service.js +0 -310
  796. package/dist/services/github-service.js.map +0 -1
  797. package/dist/services/image-import-service.d.ts +0 -24
  798. package/dist/services/image-import-service.js +0 -108
  799. package/dist/services/image-import-service.js.map +0 -1
  800. package/dist/services/insights.d.ts +0 -38
  801. package/dist/services/insights.js +0 -269
  802. package/dist/services/insights.js.map +0 -1
  803. package/dist/services/meeting-processor.d.ts +0 -91
  804. package/dist/services/meeting-processor.js +0 -331
  805. package/dist/services/meeting-processor.js.map +0 -1
  806. package/dist/services/sync/collection-sync.d.ts +0 -60
  807. package/dist/services/sync/collection-sync.js +0 -509
  808. package/dist/services/sync/collection-sync.js.map +0 -1
  809. package/dist/services/sync/commit.d.ts +0 -60
  810. package/dist/services/sync/commit.js +0 -354
  811. package/dist/services/sync/commit.js.map +0 -1
  812. package/dist/services/sync/context-index.d.ts +0 -69
  813. package/dist/services/sync/context-index.js +0 -280
  814. package/dist/services/sync/context-index.js.map +0 -1
  815. package/dist/services/sync/data-fetcher.d.ts +0 -31
  816. package/dist/services/sync/data-fetcher.js +0 -12
  817. package/dist/services/sync/data-fetcher.js.map +0 -1
  818. package/dist/services/sync/derive.d.ts +0 -34
  819. package/dist/services/sync/derive.js +0 -164
  820. package/dist/services/sync/derive.js.map +0 -1
  821. package/dist/services/sync/enrichment-state.d.ts +0 -31
  822. package/dist/services/sync/enrichment-state.js +0 -63
  823. package/dist/services/sync/enrichment-state.js.map +0 -1
  824. package/dist/services/sync/enrichment.d.ts +0 -25
  825. package/dist/services/sync/enrichment.js +0 -121
  826. package/dist/services/sync/enrichment.js.map +0 -1
  827. package/dist/services/sync/entity-refresh.d.ts +0 -30
  828. package/dist/services/sync/entity-refresh.js +0 -275
  829. package/dist/services/sync/entity-refresh.js.map +0 -1
  830. package/dist/services/sync/frontmatter-extractor.d.ts +0 -40
  831. package/dist/services/sync/frontmatter-extractor.js +0 -279
  832. package/dist/services/sync/frontmatter-extractor.js.map +0 -1
  833. package/dist/services/sync/graph-match-state.d.ts +0 -33
  834. package/dist/services/sync/graph-match-state.js +0 -61
  835. package/dist/services/sync/graph-match-state.js.map +0 -1
  836. package/dist/services/sync/graph-match.d.ts +0 -53
  837. package/dist/services/sync/graph-match.js +0 -316
  838. package/dist/services/sync/graph-match.js.map +0 -1
  839. package/dist/services/sync/graphrag-client.d.ts +0 -43
  840. package/dist/services/sync/graphrag-client.js +0 -94
  841. package/dist/services/sync/graphrag-client.js.map +0 -1
  842. package/dist/services/sync/graphrag-config.d.ts +0 -16
  843. package/dist/services/sync/graphrag-config.js +0 -39
  844. package/dist/services/sync/graphrag-config.js.map +0 -1
  845. package/dist/services/sync/graphrag-context.d.ts +0 -14
  846. package/dist/services/sync/graphrag-context.js +0 -109
  847. package/dist/services/sync/graphrag-context.js.map +0 -1
  848. package/dist/services/sync/graphrag-indexer.d.ts +0 -30
  849. package/dist/services/sync/graphrag-indexer.js +0 -358
  850. package/dist/services/sync/graphrag-indexer.js.map +0 -1
  851. package/dist/services/sync/llm.d.ts +0 -32
  852. package/dist/services/sync/llm.js +0 -115
  853. package/dist/services/sync/llm.js.map +0 -1
  854. package/dist/services/sync/mcp-client.d.ts +0 -71
  855. package/dist/services/sync/mcp-client.js +0 -299
  856. package/dist/services/sync/mcp-client.js.map +0 -1
  857. package/dist/services/sync/model-factory.d.ts +0 -13
  858. package/dist/services/sync/model-factory.js +0 -38
  859. package/dist/services/sync/model-factory.js.map +0 -1
  860. package/dist/services/sync/name-quality.d.ts +0 -31
  861. package/dist/services/sync/name-quality.js +0 -60
  862. package/dist/services/sync/name-quality.js.map +0 -1
  863. package/dist/services/sync/output-schemas.d.ts +0 -92
  864. package/dist/services/sync/output-schemas.js +0 -43
  865. package/dist/services/sync/output-schemas.js.map +0 -1
  866. package/dist/services/sync/prompts.d.ts +0 -19
  867. package/dist/services/sync/prompts.js +0 -128
  868. package/dist/services/sync/prompts.js.map +0 -1
  869. package/dist/services/sync/reconciler.d.ts +0 -48
  870. package/dist/services/sync/reconciler.js +0 -294
  871. package/dist/services/sync/reconciler.js.map +0 -1
  872. package/dist/services/sync/rest-client.d.ts +0 -40
  873. package/dist/services/sync/rest-client.js +0 -100
  874. package/dist/services/sync/rest-client.js.map +0 -1
  875. package/dist/services/sync/source-config.d.ts +0 -67
  876. package/dist/services/sync/source-config.js +0 -304
  877. package/dist/services/sync/source-config.js.map +0 -1
  878. package/dist/services/sync/source-definitions/asana.d.ts +0 -14
  879. package/dist/services/sync/source-definitions/asana.js +0 -42
  880. package/dist/services/sync/source-definitions/asana.js.map +0 -1
  881. package/dist/services/sync/source-definitions/definitions.d.ts +0 -8
  882. package/dist/services/sync/source-definitions/definitions.js +0 -8
  883. package/dist/services/sync/source-definitions/definitions.js.map +0 -1
  884. package/dist/services/sync/source-definitions/gdrive.d.ts +0 -16
  885. package/dist/services/sync/source-definitions/gdrive.js +0 -68
  886. package/dist/services/sync/source-definitions/gdrive.js.map +0 -1
  887. package/dist/services/sync/source-definitions/granola.d.ts +0 -2
  888. package/dist/services/sync/source-definitions/granola.js +0 -21
  889. package/dist/services/sync/source-definitions/granola.js.map +0 -1
  890. package/dist/services/sync/source-definitions/linear.d.ts +0 -2
  891. package/dist/services/sync/source-definitions/linear.js +0 -62
  892. package/dist/services/sync/source-definitions/linear.js.map +0 -1
  893. package/dist/services/sync/source-definitions/obsidian.d.ts +0 -2
  894. package/dist/services/sync/source-definitions/obsidian.js +0 -55
  895. package/dist/services/sync/source-definitions/obsidian.js.map +0 -1
  896. package/dist/services/sync/source-definitions/pipedrive.d.ts +0 -2
  897. package/dist/services/sync/source-definitions/pipedrive.js +0 -43
  898. package/dist/services/sync/source-definitions/pipedrive.js.map +0 -1
  899. package/dist/services/sync/staging.d.ts +0 -53
  900. package/dist/services/sync/staging.js +0 -130
  901. package/dist/services/sync/staging.js.map +0 -1
  902. package/dist/services/sync/structured-extractor.d.ts +0 -57
  903. package/dist/services/sync/structured-extractor.js +0 -584
  904. package/dist/services/sync/structured-extractor.js.map +0 -1
  905. package/dist/services/sync/sync-runner.d.ts +0 -32
  906. package/dist/services/sync/sync-runner.js +0 -195
  907. package/dist/services/sync/sync-runner.js.map +0 -1
  908. package/dist/services/sync/sync-state.d.ts +0 -43
  909. package/dist/services/sync/sync-state.js +0 -154
  910. package/dist/services/sync/sync-state.js.map +0 -1
  911. package/dist/services/sync/types.d.ts +0 -381
  912. package/dist/services/sync/types.js +0 -8
  913. package/dist/services/sync/types.js.map +0 -1
  914. package/dist/services/sync/unstructured-extractor.d.ts +0 -27
  915. package/dist/services/sync/unstructured-extractor.js +0 -185
  916. package/dist/services/sync/unstructured-extractor.js.map +0 -1
  917. package/dist/utils/merge-resolver.d.ts +0 -59
  918. package/dist/utils/merge-resolver.js +0 -303
  919. package/dist/utils/merge-resolver.js.map +0 -1
  920. package/dist/utils/workspace-config.d.ts +0 -8
  921. package/dist/utils/workspace-config.js +0 -22
  922. package/dist/utils/workspace-config.js.map +0 -1
  923. package/dist/web/_app/immutable/assets/0.QUqBcyuu.css +0 -1
  924. package/dist/web/_app/immutable/assets/11.2jzI3cZY.css +0 -1
  925. package/dist/web/_app/immutable/assets/12.CT4xL4K3.css +0 -1
  926. package/dist/web/_app/immutable/assets/13.BUrHkYry.css +0 -1
  927. package/dist/web/_app/immutable/assets/2.5jNm7Pm1.css +0 -1
  928. package/dist/web/_app/immutable/assets/3.BxdqT7zi.css +0 -1
  929. package/dist/web/_app/immutable/assets/4.BjPwtpNd.css +0 -1
  930. package/dist/web/_app/immutable/assets/5.DFeGxLYf.css +0 -1
  931. package/dist/web/_app/immutable/assets/6.DXZr_rI_.css +0 -1
  932. package/dist/web/_app/immutable/assets/GraphView.D9ePpZez.css +0 -1
  933. package/dist/web/_app/immutable/assets/Toaster.rN8r_Hzv.css +0 -1
  934. package/dist/web/_app/immutable/assets/ViewToolbar.CZjsdjy4.css +0 -1
  935. package/dist/web/_app/immutable/assets/WorkspaceView.B-1DbivC.css +0 -1
  936. package/dist/web/_app/immutable/assets/wikilinks.CtSydmQl.css +0 -1
  937. package/dist/web/_app/immutable/chunks/3--t-YNA.js +0 -1
  938. package/dist/web/_app/immutable/chunks/5OU-4caN.js +0 -1
  939. package/dist/web/_app/immutable/chunks/7S2LGqoP.js +0 -2
  940. package/dist/web/_app/immutable/chunks/B6FUcAv5.js +0 -1
  941. package/dist/web/_app/immutable/chunks/B9gDgSlp.js +0 -1
  942. package/dist/web/_app/immutable/chunks/BB_5th5W.js +0 -3383
  943. package/dist/web/_app/immutable/chunks/BFtIWl5z.js +0 -1
  944. package/dist/web/_app/immutable/chunks/BHKpz6_7.js +0 -1
  945. package/dist/web/_app/immutable/chunks/BNfrhBeA.js +0 -1
  946. package/dist/web/_app/immutable/chunks/BgO5HlCg.js +0 -2
  947. package/dist/web/_app/immutable/chunks/BqnODQDb.js +0 -1
  948. package/dist/web/_app/immutable/chunks/BrebtJNG.js +0 -18
  949. package/dist/web/_app/immutable/chunks/BvIFhEn7.js +0 -2
  950. package/dist/web/_app/immutable/chunks/BvfdTHv8.js +0 -1
  951. package/dist/web/_app/immutable/chunks/BxGm65PH.js +0 -1
  952. package/dist/web/_app/immutable/chunks/BxyxnEKw.js +0 -1
  953. package/dist/web/_app/immutable/chunks/BygFIGq4.js +0 -1
  954. package/dist/web/_app/immutable/chunks/Bz0ZjVQE.js +0 -2
  955. package/dist/web/_app/immutable/chunks/C0LVZhvn.js +0 -1
  956. package/dist/web/_app/immutable/chunks/CIb_hHMy.js +0 -1
  957. package/dist/web/_app/immutable/chunks/CNKK6nZb.js +0 -1
  958. package/dist/web/_app/immutable/chunks/CQ5FjHZK.js +0 -1
  959. package/dist/web/_app/immutable/chunks/CSVbGg_b.js +0 -5
  960. package/dist/web/_app/immutable/chunks/CX_61fY8.js +0 -5
  961. package/dist/web/_app/immutable/chunks/CYrVHOHA.js +0 -1
  962. package/dist/web/_app/immutable/chunks/CaEaeeKb.js +0 -1
  963. package/dist/web/_app/immutable/chunks/Ch6e_wpP.js +0 -1
  964. package/dist/web/_app/immutable/chunks/Ci4zzCVB.js +0 -2
  965. package/dist/web/_app/immutable/chunks/CknqBVQE.js +0 -1
  966. package/dist/web/_app/immutable/chunks/Cmfiirqp.js +0 -4
  967. package/dist/web/_app/immutable/chunks/CqkleIqs.js +0 -1
  968. package/dist/web/_app/immutable/chunks/CrFpCC6p.js +0 -1
  969. package/dist/web/_app/immutable/chunks/CxX8hcT6.js +0 -1
  970. package/dist/web/_app/immutable/chunks/D0T1ps1G.js +0 -1
  971. package/dist/web/_app/immutable/chunks/D2ZEj9xD.js +0 -1
  972. package/dist/web/_app/immutable/chunks/D3sy0IkA.js +0 -2
  973. package/dist/web/_app/immutable/chunks/DFYOFE3o.js +0 -1
  974. package/dist/web/_app/immutable/chunks/DHWoAC4y.js +0 -1
  975. package/dist/web/_app/immutable/chunks/DYYBfXU3.js +0 -1
  976. package/dist/web/_app/immutable/chunks/DiNPRA8T.js +0 -1
  977. package/dist/web/_app/immutable/chunks/DkjsqHkM.js +0 -1
  978. package/dist/web/_app/immutable/chunks/DnL9f0lc.js +0 -7
  979. package/dist/web/_app/immutable/chunks/DpMCynC3.js +0 -1
  980. package/dist/web/_app/immutable/chunks/DqrybUuA.js +0 -1
  981. package/dist/web/_app/immutable/chunks/DsnmJJEf.js +0 -1
  982. package/dist/web/_app/immutable/chunks/KyjzlWRN.js +0 -2
  983. package/dist/web/_app/immutable/chunks/O9MAeAgV.js +0 -5
  984. package/dist/web/_app/immutable/chunks/PPVm8Dsz.js +0 -1
  985. package/dist/web/_app/immutable/chunks/SKSQ4m85.js +0 -2
  986. package/dist/web/_app/immutable/chunks/WEBTyD-H.js +0 -1
  987. package/dist/web/_app/immutable/chunks/WSUKABI_.js +0 -1
  988. package/dist/web/_app/immutable/chunks/bBXT5iJP.js +0 -1
  989. package/dist/web/_app/immutable/chunks/vxEtkL8z.js +0 -1
  990. package/dist/web/_app/immutable/chunks/wDIGUaoU.js +0 -3
  991. package/dist/web/_app/immutable/chunks/xkNhbnff.js +0 -1
  992. package/dist/web/_app/immutable/chunks/zoVX1Zab.js +0 -1
  993. package/dist/web/_app/immutable/entry/app.GfE4z2GO.js +0 -2
  994. package/dist/web/_app/immutable/entry/start.H-LFJK9u.js +0 -1
  995. package/dist/web/_app/immutable/nodes/0.Bwi7tTNC.js +0 -2
  996. package/dist/web/_app/immutable/nodes/1.DgNJPEWk.js +0 -1
  997. package/dist/web/_app/immutable/nodes/10.BW_XCD2O.js +0 -1
  998. package/dist/web/_app/immutable/nodes/11.DM7RX3Ms.js +0 -1
  999. package/dist/web/_app/immutable/nodes/12.DKgssZo8.js +0 -1
  1000. package/dist/web/_app/immutable/nodes/13.DCdmTre-.js +0 -1
  1001. package/dist/web/_app/immutable/nodes/2.DOA5QBVG.js +0 -266
  1002. package/dist/web/_app/immutable/nodes/3.bpoHQwAI.js +0 -5
  1003. package/dist/web/_app/immutable/nodes/4.ye0Vy7A1.js +0 -11
  1004. package/dist/web/_app/immutable/nodes/5.rxpnYrx_.js +0 -4
  1005. package/dist/web/_app/immutable/nodes/6.DP8HSxtc.js +0 -1
  1006. package/dist/web/_app/immutable/nodes/7.C1h4Bf-E.js +0 -1
  1007. package/dist/web/_app/immutable/nodes/8.BzmQ7S7Y.js +0 -1
  1008. package/dist/web/_app/immutable/nodes/9.CEakpXpF.js +0 -1
@@ -2,132 +2,55 @@
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 { isAdminOrOwner } from '../../services/auth-service.js';
8
+ import { isWorkspaceOwner } from '../../services/auth-service.js';
9
+ import { WorkspaceAccessService } from '../../services/workspace-access.js';
9
10
  import { Workspace } from '../../core/workspace.js';
10
- import { existsSync } from 'fs';
11
- import { join } from 'path';
12
- /** Convert an AuthUser from the request to a GitUser for commit attribution. */
13
- function reqGitUser(req) {
14
- const user = req.user;
15
- if (!user)
16
- return undefined;
17
- return { id: String(user.id), name: user.displayName, email: user.email };
18
- }
19
- /** Check if a user can see a private repo (only the owner can). */
20
- function canSeePrivateRepo(repo, user) {
21
- if (!repo.private)
22
- return true;
23
- if (!user)
24
- return !repo.owner_id; // API key / open mode — only if no owner set
25
- if (!repo.owner_id)
26
- return false; // Private without owner hidden from all users
27
- return user.id === repo.owner_id;
28
- }
29
- /** Get the list of repos the current user can access. */
30
- function getAccessibleRepos(req, workspaceManager, authService) {
31
- const allRepos = workspaceManager.getAllRepoConfigs();
32
- const user = req.user;
33
- // Filter out private repos owned by other users (applies to all roles)
34
- const visible = allRepos.filter(r => canSeePrivateRepo(r, user));
35
- if (!user)
36
- return visible; // API key / open mode → full access
37
- if (isAdminOrOwner(user.role))
38
- return visible;
39
- const allowed = new Set(authService.getUserCollections(user.id));
40
- return visible.filter(r => r.private || allowed.has(r.name));
41
- }
42
- /** Check if the current user can access a specific repo. */
43
- function hasRepoAccess(req, repoName, workspaceManager, authService) {
44
- const user = req.user;
45
- const repoConfig = workspaceManager.getRepoConfig(repoName);
46
- if (repoConfig && !canSeePrivateRepo(repoConfig, user))
47
- return false;
48
- if (!user)
49
- return true; // API key / open mode
50
- if (repoConfig?.private && repoConfig?.owner_id === user.id)
51
- return true; // Owner of private repo
52
- if (isAdminOrOwner(user.role))
53
- return true;
54
- return authService.getUserCollections(user.id).includes(repoName);
55
- }
56
- /** Check if the current user is an admin. */
57
- function isAdmin(req) {
58
- const user = req.user;
59
- if (!user)
60
- return true; // API key / open mode → admin-equivalent
61
- return isAdminOrOwner(user.role);
62
- }
63
- /**
64
- * Build a set of all entity IDs visible to the current user.
65
- * Used to filter wikilinks so users don't see links to entities
66
- * in collections they can't access.
67
- */
68
- function getAccessibleEntityIds(req, workspaceManager, authService) {
69
- const user = req.user;
70
- if (!user)
71
- return null; // API key / open mode → no filtering
72
- if (isAdminOrOwner(user.role)) {
73
- // Admins see everything except other users' private repos
74
- const allRepos = workspaceManager.getAllRepoConfigs();
75
- const hasHiddenPrivate = allRepos.some(r => r.private && r.owner_id && r.owner_id !== user.id);
76
- if (!hasHiddenPrivate)
77
- return null; // No filtering needed
78
- }
79
- const repos = getAccessibleRepos(req, workspaceManager, authService);
80
- const ids = new Set();
81
- for (const repo of repos) {
82
- try {
83
- const graph = workspaceManager.getGraph(repo.name);
84
- for (const e of graph.search({})) {
85
- ids.add(e.id);
86
- }
87
- }
88
- catch { /* skip */ }
89
- }
90
- return ids;
91
- }
92
- /** Filter wikilinks to only those pointing to accessible entities. */
93
- function filterWikilinks(wikilinks, accessibleIds) {
94
- if (!accessibleIds)
95
- return wikilinks;
96
- return wikilinks.filter(id => accessibleIds.has(id));
97
- }
98
- export async function registerGraphApiRoutes(fastify, workspaceManager, authService, wsHub, sessionManager) {
99
- // POST /api/repos — create a new collection (admin/owner only)
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 { checkEagerSkillPermission, filterWikilinks, getAccessibleEntityIds, getAccessibleRepos, graphForAdmin as resolveGraphForAdmin, graphForRead as resolveGraphForRead, graphForWrite as resolveGraphForWrite, isFolderAdminOrOwner, reqGitUser, reqUser, } from './graph-api-access.js';
15
+ import { log } from '../../utils/log.js';
16
+ export async function registerGraphApiRoutes(fastify, workspaceManager, authService, wsHub, sessionManager, yjsManager) {
17
+ // ARCHITECTURAL BOUNDARY: every per-route graph resolution flows through
18
+ // this service so the act of getting a graph IS the access check. Do not
19
+ // call `workspaceManager.getGraph(repo)` from a request-scoped handler —
20
+ // use `accessService.graphFor*(reqUser(req), repo)` instead. A security
21
+ // audit (2026-05) flagged the previous opt-in pattern (route had to
22
+ // remember `hasRepoAccess()` before `getGraph()`) as an architectural
23
+ // risk; if you find yourself reaching for the raw WM here, that's the
24
+ // smell the audit was about.
25
+ const accessService = new WorkspaceAccessService(workspaceManager, authService);
26
+ const graphForRead = (req, reply, repoName) => resolveGraphForRead(accessService, req, reply, repoName);
27
+ const graphForWrite = (req, reply, repoName) => resolveGraphForWrite(accessService, req, reply, repoName);
28
+ const graphForAdmin = (req, reply, repoName) => resolveGraphForAdmin(accessService, req, reply, repoName);
29
+ // POST /api/repos — any authenticated user can create a folder.
30
+ // The creator is granted folder-admin access automatically.
31
+ // Shared logic lives in services/folder-creation.ts so the MCP tool
32
+ // create_repo behaves identically.
100
33
  fastify.post('/api/repos', async (req, reply) => {
101
- if (!isAdmin(req)) {
102
- return reply.status(403).send({ error: 'Admin access required' });
103
- }
34
+ const user = req.user;
104
35
  const { name, display_name, description } = req.body || {};
105
- if (!name || !/^[a-z0-9][a-z0-9-]*$/.test(name)) {
106
- return reply.status(400).send({ error: 'Collection name must be kebab-case' });
107
- }
108
36
  try {
109
- const workspace = new Workspace(workspaceManager['workspacePath']);
110
- const repo = await workspace.registerRepo({
111
- name,
112
- display_name: display_name || undefined,
113
- description,
114
- });
115
- workspaceManager.reloadConfig();
116
- wsHub?.broadcast({
117
- type: 'collection_created', repo: name,
118
- actor: req.user?.displayName ?? 'API',
119
- timestamp: new Date().toISOString(),
120
- });
37
+ const repo = await createSharedFolder(workspaceManager, authService, { name, display_name, description }, user, wsHub);
121
38
  return reply.status(201).send(repo);
122
39
  }
123
40
  catch (err) {
124
- return reply.status(409).send({ error: err.message });
41
+ if (err instanceof FolderCreationError) {
42
+ const status = err.code === 'invalid_name' ? 400
43
+ : err.code === 'duplicate' ? 409
44
+ : 500;
45
+ return reply.status(status).send({ error: err.message });
46
+ }
47
+ return reply.status(500).send({ error: err?.message ?? String(err) });
125
48
  }
126
49
  });
127
- // DELETE /api/repos/:repo — remove a collection and delete its files (admin only)
50
+ // DELETE /api/repos/:repo — folder admin or workspace owner only.
128
51
  fastify.delete('/api/repos/:repo', async (req, reply) => {
129
- if (!isAdmin(req)) {
130
- return reply.status(403).send({ error: 'Admin access required' });
52
+ if (!isFolderAdminOrOwner(req, req.params.repo, workspaceManager, authService)) {
53
+ return reply.status(403).send({ error: 'Folder admin access required' });
131
54
  }
132
55
  const { repo } = req.params;
133
56
  try {
@@ -135,14 +58,14 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
135
58
  const config = await workspace.loadConfig();
136
59
  const repoIndex = config.repos.findIndex(r => r.name === repo);
137
60
  if (repoIndex === -1) {
138
- return reply.status(404).send({ error: `Collection "${repo}" not found` });
61
+ return reply.status(404).send({ error: `Folder \u201C${repo}\u201D not found` });
139
62
  }
140
63
  const repoConfig = config.repos[repoIndex];
141
64
  const repoPath = join(workspaceManager['workspacePath'], repoConfig.path);
142
65
  // Remove from config
143
66
  config.repos.splice(repoIndex, 1);
144
67
  workspace.writeConfig(config);
145
- workspace.pushWorkspaceConfig(`Remove collection: ${repo}`);
68
+ workspace.pushWorkspaceConfig(`Remove folder: ${repo}`);
146
69
  // Delete files
147
70
  const { rmSync } = await import('fs');
148
71
  if (existsSync(repoPath)) {
@@ -150,7 +73,7 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
150
73
  }
151
74
  workspaceManager.reloadConfig();
152
75
  wsHub?.broadcast({
153
- type: 'collection_deleted', repo,
76
+ type: 'folder_deleted', repo,
154
77
  actor: req.user?.displayName ?? 'API',
155
78
  timestamp: new Date().toISOString(),
156
79
  });
@@ -160,16 +83,16 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
160
83
  return reply.status(500).send({ error: err.message });
161
84
  }
162
85
  });
163
- // PATCH /api/repos/:repo — update a collection (rename, sync rules) (admin only)
86
+ // PATCH /api/repos/:repo — rename/update sync rules. Folder admin or workspace owner only.
164
87
  fastify.patch('/api/repos/:repo', async (req, reply) => {
165
- if (!isAdmin(req)) {
166
- return reply.status(403).send({ error: 'Admin access required' });
88
+ if (!isFolderAdminOrOwner(req, req.params.repo, workspaceManager, authService)) {
89
+ return reply.status(403).send({ error: 'Folder admin access required' });
167
90
  }
168
91
  const { repo } = req.params;
169
92
  const body = req.body || {};
170
93
  const { name: newName, display_name: newDisplayName, sync: newSync, ...rest } = body;
171
- if ('private' in rest) {
172
- return reply.status(400).send({ error: 'The private flag is immutable after creation' });
94
+ if ('personal' in rest) {
95
+ return reply.status(400).send({ error: 'The personal flag is immutable after creation' });
173
96
  }
174
97
  if (newName === undefined && newSync === undefined && newDisplayName === undefined) {
175
98
  return reply.status(400).send({ error: 'Nothing to update' });
@@ -182,12 +105,12 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
182
105
  const config = await workspace.loadConfig();
183
106
  const repoConfig = config.repos.find(r => r.name === repo);
184
107
  if (!repoConfig) {
185
- return reply.status(404).send({ error: `Collection "${repo}" not found` });
108
+ return reply.status(404).send({ error: `Folder \u201C${repo}\u201D not found` });
186
109
  }
187
110
  // Handle rename
188
111
  if (newName !== undefined) {
189
112
  if (config.repos.some(r => r.name === newName)) {
190
- return reply.status(409).send({ error: `Collection "${newName}" already exists` });
113
+ return reply.status(409).send({ error: `Folder \u201C${newName}\u201D already exists` });
191
114
  }
192
115
  const wsPath = workspaceManager['workspacePath'];
193
116
  const oldPath = join(wsPath, repoConfig.path);
@@ -206,8 +129,8 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
206
129
  if (existsSync(markerPath)) {
207
130
  writeFS(markerPath, JSON.stringify({ repo_name: newName }, null, 2), 'utf-8');
208
131
  }
209
- // Update collection_access to follow the rename
210
- authService.renameCollection(repo, newName);
132
+ // Update folder_access to follow the rename
133
+ authService.renameFolder(repo, newName);
211
134
  }
212
135
  // Handle display name update (without rename)
213
136
  if (newDisplayName !== undefined && newName === undefined) {
@@ -218,7 +141,7 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
218
141
  repoConfig.sync = newSync.length > 0 ? newSync : undefined;
219
142
  }
220
143
  workspace.writeConfig(config);
221
- workspace.pushWorkspaceConfig(newName !== undefined ? `Rename collection: ${repo} → ${newName}` : `Update collection: ${repo}`);
144
+ workspace.pushWorkspaceConfig(newName !== undefined ? `Rename folder: ${repo} → ${newName}` : `Update folder: ${repo}`);
222
145
  workspaceManager.reloadConfig();
223
146
  return reply.send(repoConfig);
224
147
  }
@@ -226,50 +149,122 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
226
149
  return reply.status(500).send({ error: err.message });
227
150
  }
228
151
  });
229
- // GET /api/repos — list accessible repos (re-reads config to pick up CLI changes)
152
+ // GET /api/repos — list accessible repos (re-reads config to pick up CLI changes).
153
+ // Augments each repo with the caller's folder role ('admin' | 'editor'),
154
+ // member_count, entity_types (distinct types present), and folders (the
155
+ // flat list of nested-folder paths in the repo, used by the sidebar to
156
+ // render the active-path expansion).
230
157
  fastify.get('/api/repos', async (req, reply) => {
231
158
  workspaceManager.reloadConfig();
232
159
  const repos = getAccessibleRepos(req, workspaceManager, authService);
233
- return reply.send(repos);
160
+ const user = req.user;
161
+ const enriched = repos.map(r => {
162
+ const memberCount = authService.getFolderMembers(r.name).length;
163
+ const callerRole = user ? authService.getFolderRole(user.id, r.name) : null;
164
+ let entityTypes = [];
165
+ // `folders` carries the per-folder display_name override (when one
166
+ // exists in the folder's .studiograph-folder.json sidecar); the client
167
+ // falls back to toDisplayName(slug) when display_name is absent.
168
+ let folders = [];
169
+ try {
170
+ // Direct WM access OK here — `repos` came from getAccessibleRepos
171
+ // above, so every iteration is over a folder the caller can read.
172
+ const graph = workspaceManager.getGraph(r.name);
173
+ entityTypes = graph.listEntityTypes();
174
+ folders = graph.listFolders().map(f => {
175
+ const entry = { path: f.path };
176
+ if (f.display_name)
177
+ entry.display_name = f.display_name;
178
+ return entry;
179
+ });
180
+ }
181
+ catch { /* repo not mounted */ }
182
+ return {
183
+ ...r,
184
+ caller_role: callerRole,
185
+ member_count: memberCount,
186
+ entity_types: entityTypes,
187
+ folders,
188
+ };
189
+ });
190
+ return reply.send(enriched);
191
+ });
192
+ // GET /api/admin/folders — workspace-owner-only metadata list of every folder
193
+ // (name, display_name, creator, member count). No content access implied —
194
+ // owners still need to join a folder to read its entries.
195
+ fastify.get('/api/admin/folders', async (req, reply) => {
196
+ const user = req.user;
197
+ if (user && !isWorkspaceOwner(user.role)) {
198
+ return reply.status(403).send({ error: 'Workspace owner access required' });
199
+ }
200
+ workspaceManager.reloadConfig();
201
+ const all = workspaceManager.getAllRepoConfigs();
202
+ const result = all.map(r => {
203
+ const members = authService.getFolderMembers(r.name);
204
+ const admins = members.filter(m => m.role === 'admin');
205
+ const creator = r.owner_id
206
+ ? members.find(m => m.user.id === r.owner_id)?.user
207
+ : admins[0]?.user;
208
+ let entryCount = 0;
209
+ // Direct WM access OK — this endpoint is owner-only (gated above) and
210
+ // returns metadata for every folder the workspace owner needs to see
211
+ // for admin purposes. No content is leaked: only entry counts.
212
+ try {
213
+ entryCount = workspaceManager.getGraph(r.name).search({}).length;
214
+ }
215
+ catch { /* skip */ }
216
+ return {
217
+ name: r.name,
218
+ display_name: r.display_name ?? r.name,
219
+ description: r.description,
220
+ member_count: members.length,
221
+ admin_count: admins.length,
222
+ entry_count: entryCount,
223
+ creator: creator ? { id: creator.id, displayName: creator.displayName, email: creator.email } : null,
224
+ caller_is_member: user ? members.some(m => m.user.id === user.id) : true,
225
+ };
226
+ });
227
+ return reply.send(result);
234
228
  });
229
+ // Build the lightweight summary objects used by /entities?summary=true.
230
+ // Extracted so the per-repo and bulk endpoints share the same shape.
231
+ const EXCLUDED_SUMMARY_FIELDS = new Set([
232
+ 'entity_type', 'entity_id', 'created_by', 'updated_by',
233
+ 'description', 'content', 'context', 'rationale', 'alternatives', 'consequences',
234
+ 'steps', 'guidelines', 'bio', 'agenda', 'summary', 'decision', 'action_items',
235
+ ]);
236
+ function buildSummaries(entities, accessibleIds) {
237
+ return entities.map(e => {
238
+ const extra = Object.fromEntries(Object.entries(e.data ?? {}).filter(([k, v]) => !EXCLUDED_SUMMARY_FIELDS.has(k) && k !== 'name' && k !== 'created_at' && k !== 'updated_at' &&
239
+ (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || Array.isArray(v) || v instanceof Date)));
240
+ return {
241
+ id: e.id,
242
+ entityType: e.entityType,
243
+ folder_path: e.folder_path ?? '',
244
+ data: {
245
+ name: e.data.name,
246
+ created_at: e.data.created_at,
247
+ updated_at: e.data.updated_at,
248
+ ...extra,
249
+ },
250
+ document: { wikilinks: filterWikilinks(e.document?.wikilinks ?? [], accessibleIds) },
251
+ };
252
+ });
253
+ }
235
254
  // GET /api/repos/:repo/entities — list all entities in a repo
236
255
  // ?summary=true returns lightweight objects (no content, minimal data) for sidebar/graph
237
256
  fastify.get('/api/repos/:repo/entities', async (req, reply) => {
238
257
  const { repo } = req.params;
239
- if (!hasRepoAccess(req, repo, workspaceManager, authService)) {
240
- return reply.status(404).send({ error: `Collection "${repo}" not found` });
241
- }
242
- let graph;
243
- try {
244
- graph = workspaceManager.getGraph(repo);
245
- }
246
- catch (err) {
247
- return reply.status(404).send({ error: err.message });
248
- }
258
+ // Access enforced by graphForRead \u2014 do not call workspaceManager.getGraph
259
+ // directly here. The audit (2026-05) flagged direct WM access from routes
260
+ // as the primary architectural risk; the service is the gate.
261
+ const graph = graphForRead(req, reply, repo);
262
+ if (!graph)
263
+ return;
249
264
  const entities = graph.search({});
250
265
  const accessibleIds = getAccessibleEntityIds(req, workspaceManager, authService);
251
266
  if (req.query.summary === 'true') {
252
- const EXCLUDED_SUMMARY_FIELDS = new Set([
253
- 'entity_type', 'entity_id', 'created_by', 'updated_by',
254
- 'description', 'content', 'context', 'rationale', 'alternatives', 'consequences',
255
- 'steps', 'guidelines', 'bio', 'agenda', 'summary', 'decision', 'action_items',
256
- ]);
257
- const summaries = entities.map(e => {
258
- const extra = Object.fromEntries(Object.entries(e.data ?? {}).filter(([k, v]) => !EXCLUDED_SUMMARY_FIELDS.has(k) && k !== 'name' && k !== 'created_at' && k !== 'updated_at' &&
259
- (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || Array.isArray(v) || v instanceof Date)));
260
- return {
261
- id: e.id,
262
- entityType: e.entityType,
263
- data: {
264
- name: e.data.name,
265
- created_at: e.data.created_at,
266
- updated_at: e.data.updated_at,
267
- ...extra,
268
- },
269
- document: { wikilinks: filterWikilinks(e.document?.wikilinks ?? [], accessibleIds) },
270
- };
271
- });
272
- return reply.send(summaries);
267
+ return reply.send(buildSummaries(entities, accessibleIds));
273
268
  }
274
269
  // Full response — filter wikilinks in document
275
270
  const filtered = entities.map(e => {
@@ -279,19 +274,57 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
279
274
  });
280
275
  return reply.send(filtered);
281
276
  });
282
- // GET /api/repos/:repo/entities/:typelist entities of a type
283
- fastify.get('/api/repos/:repo/entities/:type', async (req, reply) => {
284
- const { repo, type } = req.params;
285
- if (!hasRepoAccess(req, repo, workspaceManager, authService)) {
286
- return reply.status(404).send({ error: `Collection "${repo}" not found` });
277
+ // GET /api/workspace/entities?summary=truebulk entity summaries for ALL
278
+ // accessible repos in a single round-trip. Replaces 15+ parallel
279
+ // /api/repos/:repo/entities?summary=true calls. Important on dev where each
280
+ // request has Vite middleware overhead and the browser's HTTP/1.1 6-slot
281
+ // limit serializes them, blocking other navigation fetches.
282
+ // Response shape: `{ [repoName]: EntitySummary[] }`.
283
+ fastify.get('/api/workspace/entities', async (req, reply) => {
284
+ // Mirror the per-repo endpoint's contract: only summary=true is supported
285
+ // here. Returning full bodies for the whole workspace would be huge and
286
+ // isn't a use case we have today.
287
+ if (req.query.summary !== 'true') {
288
+ return reply.status(400).send({ error: 'summary=true is required for the bulk entities endpoint' });
287
289
  }
288
- let graph;
289
- try {
290
- graph = workspaceManager.getGraph(repo);
290
+ const repos = getAccessibleRepos(req, workspaceManager, authService);
291
+ // Single-pass: collect the per-repo entity arrays AND the accessible-ids
292
+ // set in one walk. The previous shape called graph.search({}) twice per
293
+ // repo (once via getAccessibleEntityIds, once in this loop), doubling
294
+ // disk I/O on the most-trafficked endpoint.
295
+ const user = reqUser(req);
296
+ const collectIds = user !== undefined; // unauthenticated → no wikilink filtering
297
+ const accessibleIds = collectIds ? new Set() : null;
298
+ const perRepoEntities = [];
299
+ for (const r of repos) {
300
+ let graph;
301
+ // Direct WM access OK — `repos` is the access-filtered list above.
302
+ try {
303
+ graph = workspaceManager.getGraph(r.name);
304
+ }
305
+ catch {
306
+ continue;
307
+ } // skip unmounted repos rather than failing the whole request
308
+ const entities = graph.search({});
309
+ perRepoEntities.push({ name: r.name, entities });
310
+ if (accessibleIds) {
311
+ for (const e of entities)
312
+ accessibleIds.add(e.id);
313
+ }
291
314
  }
292
- catch (err) {
293
- return reply.status(404).send({ error: err.message });
315
+ const result = {};
316
+ for (const { name, entities } of perRepoEntities) {
317
+ result[name] = buildSummaries(entities, accessibleIds);
294
318
  }
319
+ return reply.send(result);
320
+ });
321
+ // GET /api/repos/:repo/entities/:type — list entities of a type
322
+ fastify.get('/api/repos/:repo/entities/:type', async (req, reply) => {
323
+ const { repo, type } = req.params;
324
+ // Access enforced by graphForRead (WorkspaceAccessService).
325
+ const graph = graphForRead(req, reply, repo);
326
+ if (!graph)
327
+ return;
295
328
  const entities = graph.search({ entityType: type });
296
329
  const accessibleIds = getAccessibleEntityIds(req, workspaceManager, authService);
297
330
  const filtered = entities.map(e => {
@@ -304,17 +337,20 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
304
337
  // GET /api/repos/:repo/entities/:type/:id — get single entity
305
338
  fastify.get('/api/repos/:repo/entities/:type/:id', async (req, reply) => {
306
339
  const { repo, type, id } = req.params;
307
- if (!hasRepoAccess(req, repo, workspaceManager, authService)) {
308
- return reply.status(404).send({ error: `Collection "${repo}" not found` });
309
- }
310
- let graph;
340
+ // Access enforced by graphForRead (WorkspaceAccessService).
341
+ const graph = graphForRead(req, reply, repo);
342
+ if (!graph)
343
+ return;
344
+ let entity;
311
345
  try {
312
- graph = workspaceManager.getGraph(repo);
346
+ entity = graph.get(type, id);
313
347
  }
314
348
  catch (err) {
315
- return reply.status(404).send({ error: err.message });
349
+ return reply.status(404).send({ error: err.message ?? `Entity not found: ${type}/${id}` });
316
350
  }
317
- const entity = graph.get(type, id);
351
+ // graph.get may also return null for a missing entity without throwing
352
+ // (depends on the backing graph implementation). Treat that as 404 so
353
+ // we don't blow up on the wikilinks access below.
318
354
  if (!entity) {
319
355
  return reply.status(404).send({ error: `Entity not found: ${type}/${id}` });
320
356
  }
@@ -330,25 +366,32 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
330
366
  // GET /api/repos/:repo/entities/:type/:id/history — entity commit history
331
367
  fastify.get('/api/repos/:repo/entities/:type/:id/history', async (req, reply) => {
332
368
  const { repo, type, id } = req.params;
333
- if (!hasRepoAccess(req, repo, workspaceManager, authService)) {
334
- return reply.status(404).send({ error: `Collection "${repo}" not found` });
369
+ // Access enforced by graphForRead (WorkspaceAccessService).
370
+ const graph = graphForRead(req, reply, repo);
371
+ if (!graph)
372
+ return;
373
+ const gitService = graph.getGitService();
374
+ if (!gitService) {
375
+ return reply.send([]);
335
376
  }
336
- let graph;
377
+ const limit = Math.min(parseInt(req.query.limit ?? '20', 10) || 20, 50);
378
+ // Resolve the actual on-disk path via graph.get() — it walks nested
379
+ // folders to find the file. The previous hardcoded `${plural}/${id}.md`
380
+ // missed any entry not at the legacy flat path (entries created in a
381
+ // sub-folder, MCP entries that landed elsewhere, etc.) and silently
382
+ // returned an empty history. Convert absolute path → repo-relative
383
+ // for git log.
384
+ let filePath;
337
385
  try {
338
- graph = workspaceManager.getGraph(repo);
386
+ const entity = graph.get(type, id);
387
+ const repoRoot = graph.getRepoPath();
388
+ filePath = entity.path.startsWith(repoRoot + '/')
389
+ ? entity.path.slice(repoRoot.length + 1)
390
+ : entity.path;
339
391
  }
340
392
  catch (err) {
341
393
  return reply.status(404).send({ error: err.message });
342
394
  }
343
- const gitService = graph.getGitService();
344
- if (!gitService) {
345
- return reply.send([]);
346
- }
347
- const limit = Math.min(parseInt(req.query.limit ?? '20', 10) || 20, 50);
348
- const { PLURAL_TO_SINGULAR } = await import('../../core/validation.js');
349
- // Reverse lookup: singular type → plural folder name
350
- const pluralDir = Object.entries(PLURAL_TO_SINGULAR).find(([, v]) => v === type)?.[0] ?? `${type}s`;
351
- const filePath = `${pluralDir}/${id}.md`;
352
395
  try {
353
396
  const history = gitService.getFileHistory(filePath, limit);
354
397
  return reply.send(history);
@@ -357,6 +400,59 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
357
400
  return reply.send([]);
358
401
  }
359
402
  });
403
+ // POST /api/repos/:repo/entities/:type/:id/restore — restore an entry to
404
+ // its content at a past commit. Single-entity scope. Editor-or-admin role
405
+ // required (canWriteRepo). Returns 200 with the restore result, 404 if
406
+ // the entry doesn't exist at that commit, 409 PATH_CONFLICT if a different
407
+ // entry now occupies the target path, 403 if the caller lacks write access.
408
+ fastify.post('/api/repos/:repo/entities/:type/:id/restore', async (req, reply) => {
409
+ const { repo, type, id } = req.params;
410
+ const { commitHash } = req.body ?? {};
411
+ if (!commitHash || typeof commitHash !== 'string') {
412
+ return reply.status(400).send({ error: 'commitHash is required' });
413
+ }
414
+ // Access enforced by graphForWrite: 404 if no read access, 403 if no write.
415
+ const graph = graphForWrite(req, reply, repo);
416
+ if (!graph)
417
+ return;
418
+ try {
419
+ const result = await graph.restoreFile(type, id, commitHash, reqGitUser(req));
420
+ wsHub?.broadcast({
421
+ type: 'entity_change',
422
+ action: result.action === 'recreate' ? 'created' : 'updated',
423
+ repo,
424
+ entityType: type,
425
+ entityId: id,
426
+ actor: reqUser(req)?.displayName ?? 'API',
427
+ source: 'api',
428
+ timestamp: new Date().toISOString(),
429
+ });
430
+ return reply.send({
431
+ success: true,
432
+ action: result.action,
433
+ path: result.path,
434
+ });
435
+ }
436
+ catch (err) {
437
+ // PathConflictError carries an error code distinct from generic
438
+ // failures so the UI can render an actionable conflict message.
439
+ if (err?.code === 'PATH_CONFLICT' && err?.occupiedBy) {
440
+ return reply.status(409).send({
441
+ error: 'PATH_CONFLICT',
442
+ path: err.path,
443
+ occupiedBy: err.occupiedBy,
444
+ });
445
+ }
446
+ const message = err instanceof Error ? err.message : 'Restore failed';
447
+ // "not found at commit" / "did not exist at commit" → 404 so the
448
+ // client can show "that version isn't available" instead of a
449
+ // generic 500.
450
+ if (/not found at commit|did not exist at commit/i.test(message)) {
451
+ return reply.status(404).send({ error: message });
452
+ }
453
+ return reply.status(500).send({ error: message });
454
+ }
455
+ });
360
456
  // GET /api/schema/:entityType/fields — field metadata for an entity type
361
457
  fastify.get('/api/schema/:entityType/fields', async (req, reply) => {
362
458
  const { entityType } = req.params;
@@ -367,17 +463,11 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
367
463
  // GET /api/repos/:repo/search?q=&type=&tags= — search within a repo
368
464
  fastify.get('/api/repos/:repo/search', async (req, reply) => {
369
465
  const { repo } = req.params;
370
- if (!hasRepoAccess(req, repo, workspaceManager, authService)) {
371
- return reply.status(404).send({ error: `Collection "${repo}" not found` });
372
- }
466
+ // Access enforced by graphForRead (WorkspaceAccessService).
467
+ const graph = graphForRead(req, reply, repo);
468
+ if (!graph)
469
+ return;
373
470
  const { q, type, tags } = req.query;
374
- let graph;
375
- try {
376
- graph = workspaceManager.getGraph(repo);
377
- }
378
- catch (err) {
379
- return reply.status(404).send({ error: err.message });
380
- }
381
471
  const results = graph.search({
382
472
  query: q,
383
473
  entityType: type,
@@ -400,12 +490,11 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
400
490
  const entityIds = new Set();
401
491
  for (const repoConfig of repos) {
402
492
  try {
493
+ // Direct WM access OK — `repos` came from getAccessibleRepos above.
403
494
  const graph = workspaceManager.getGraph(repoConfig.name);
404
495
  const entities = graph.search({});
405
496
  for (const entity of entities) {
406
497
  const nodeId = entity.id;
407
- if (nodeId.toLowerCase().startsWith('readme'))
408
- continue;
409
498
  if (!entityIds.has(nodeId)) {
410
499
  entityIds.add(nodeId);
411
500
  nodes.push({
@@ -462,16 +551,13 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
462
551
  // PUT /api/repos/:repo/entities/:type/:id — update entity (data and/or content)
463
552
  fastify.put('/api/repos/:repo/entities/:type/:id', async (req, reply) => {
464
553
  const { repo, type, id } = req.params;
465
- if (!hasRepoAccess(req, repo, workspaceManager, authService)) {
466
- return reply.status(404).send({ error: `Collection "${repo}" not found` });
467
- }
468
- let graph;
469
- try {
470
- graph = workspaceManager.getGraph(repo);
471
- }
472
- catch (err) {
473
- return reply.status(404).send({ error: err.message });
474
- }
554
+ // Access enforced by graphForWrite \u2014 PUT updates an entity, write required.
555
+ const graph = graphForWrite(req, reply, repo);
556
+ if (!graph)
557
+ return;
558
+ const eagerError = checkEagerSkillPermission(req, type, req.body.data, repo, workspaceManager, authService);
559
+ if (eagerError)
560
+ return reply.status(403).send({ error: eagerError });
475
561
  try {
476
562
  // Optimistic concurrency: reject if entity was modified after client last fetched it
477
563
  const ifUnmodifiedSince = req.headers['if-unmodified-since'];
@@ -491,6 +577,14 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
491
577
  commitMessage: `Update ${type}/${id} from web UI`,
492
578
  user: gitUser,
493
579
  skipCommit: !!sessionManager, // defer commit if scheduler is active
580
+ // Skip the Yjs sync callback. Web-editor autosave PUTs carry
581
+ // content sourced from the live Y.Text — mirroring the just-
582
+ // written disk content back into the Y.Doc would race in-flight
583
+ // keystrokes typed during the round trip and revert them.
584
+ // Server-initiated writes that bypass Yjs (MCP, agent, meeting
585
+ // processor) leave skipYjsSync unset so onContentUpdate fires
586
+ // and the Y.Doc picks up their writes.
587
+ skipYjsSync: true,
494
588
  });
495
589
  if (sessionManager && gitUser) {
496
590
  sessionManager.markDirty(repo, gitUser, type, id, updated.path);
@@ -513,16 +607,10 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
513
607
  if (!type || !id || !newType) {
514
608
  return reply.status(400).send({ error: 'entityType, entityId, and newType are required' });
515
609
  }
516
- if (!hasRepoAccess(req, repo, workspaceManager, authService)) {
517
- return reply.status(404).send({ error: `Collection "${repo}" not found` });
518
- }
519
- let graph;
520
- try {
521
- graph = workspaceManager.getGraph(repo);
522
- }
523
- catch (err) {
524
- return reply.status(404).send({ error: err.message });
525
- }
610
+ // Access enforced by graphForRead (WorkspaceAccessService).
611
+ const graph = graphForRead(req, reply, repo);
612
+ if (!graph)
613
+ return;
526
614
  try {
527
615
  // Flush any pending session saves before moving the file
528
616
  if (sessionManager) {
@@ -547,45 +635,111 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
547
635
  return reply.status(status).send({ error: err.message });
548
636
  }
549
637
  });
550
- // POST /api/repos/:repo/transfermove or copy entity to another collection
551
- fastify.post('/api/repos/:repo/transfer', async (req, reply) => {
552
- const sourceRepo = req.params.repo;
553
- const { entityType, entityId, targetRepo, mode } = req.body ?? {};
554
- if (!entityType || !entityId || !targetRepo || !mode) {
555
- return reply.status(400).send({ error: 'entityType, entityId, targetRepo, and mode are required' });
638
+ // POST /api/repos/:repo/renamerename an entity (changes its id/filename)
639
+ fastify.post('/api/repos/:repo/rename', async (req, reply) => {
640
+ const { repo } = req.params;
641
+ const { entityType: type, entityId: id, newId } = req.body ?? {};
642
+ if (!type || !id || !newId) {
643
+ return reply.status(400).send({ error: 'entityType, entityId, and newId are required' });
556
644
  }
557
- if (sourceRepo === targetRepo) {
558
- return reply.status(400).send({ error: 'Source and target collections must be different' });
645
+ if (!/^[a-z0-9][a-z0-9_-]*$/.test(newId)) {
646
+ return reply.status(400).send({ error: 'New ID must be kebab-case (lowercase letters, numbers, hyphens, underscores)' });
559
647
  }
560
- if (!hasRepoAccess(req, sourceRepo, workspaceManager, authService)) {
561
- return reply.status(404).send({ error: `Collection "${sourceRepo}" not found` });
648
+ if (id === newId) {
649
+ return reply.status(400).send({ error: 'New ID must differ from the current ID' });
562
650
  }
563
- if (!hasRepoAccess(req, targetRepo, workspaceManager, authService)) {
564
- return reply.status(404).send({ error: `Collection "${targetRepo}" not found` });
651
+ // Access enforced by graphForWrite (WorkspaceAccessService).
652
+ const graph = graphForWrite(req, reply, repo);
653
+ if (!graph)
654
+ return;
655
+ // Workspace-wide uniqueness — ids must be unique across all folders
656
+ const conflict = workspaceManager.findEntityById(newId);
657
+ if (conflict) {
658
+ return reply.status(409).send({ error: `An entry with id "${newId}" already exists in folder "${conflict.repoName}".` });
565
659
  }
566
- let sourceGraph, targetGraph;
567
660
  try {
568
- sourceGraph = workspaceManager.getGraph(sourceRepo);
569
- targetGraph = workspaceManager.getGraph(targetRepo);
661
+ // Flush pending session saves so we don't PUT to the old path after rename
662
+ if (sessionManager) {
663
+ await sessionManager.flush(repo);
664
+ }
665
+ const gitUser = reqGitUser(req);
666
+ const renamed = await graph.rename(type, id, newId, undefined, gitUser);
667
+ // Rewrite any [[wikilinks]] that still point at the old id, across
668
+ // every folder the caller can access. Per-repo — cross-folder links
669
+ // are rare but supported.
670
+ const refactorMsg = `Refactor wikilinks: ${id} \u2192 ${newId}`;
671
+ for (const other of getAccessibleRepos(req, workspaceManager, authService)) {
672
+ try {
673
+ // Direct WM access OK — iterating the access-filtered list.
674
+ const otherGraph = workspaceManager.getGraph(other.name);
675
+ await otherGraph.refactorWikilink(id, newId, refactorMsg);
676
+ }
677
+ catch { /* skip unloadable repo */ }
678
+ }
679
+ wsHub?.broadcast({
680
+ type: 'entity_change', action: 'deleted', repo, entityType: type, entityId: id,
681
+ actor: req.user?.displayName ?? 'API',
682
+ source: 'api', timestamp: new Date().toISOString(),
683
+ });
684
+ wsHub?.broadcast({
685
+ type: 'entity_change', action: 'created', repo, entityType: type, entityId: newId,
686
+ actor: req.user?.displayName ?? 'API',
687
+ source: 'api', timestamp: new Date().toISOString(),
688
+ });
689
+ return reply.send({ success: true, entity: renamed, newUrl: `/${repo}/${type}/${newId}` });
570
690
  }
571
691
  catch (err) {
572
- return reply.status(404).send({ error: err.message });
692
+ const status = err.message?.includes('already exists') ? 409 : 400;
693
+ return reply.status(status).send({ error: err.message });
694
+ }
695
+ });
696
+ // POST /api/repos/:repo/transfer — move or copy entity to another folder
697
+ fastify.post('/api/repos/:repo/transfer', async (req, reply) => {
698
+ const sourceRepo = req.params.repo;
699
+ const { entityType, entityId, targetRepo, mode, targetFolderPath } = req.body ?? {};
700
+ if (!entityType || !entityId || !targetRepo || !mode) {
701
+ return reply.status(400).send({ error: 'entityType, entityId, targetRepo, and mode are required' });
573
702
  }
703
+ if (sourceRepo === targetRepo) {
704
+ return reply.status(400).send({ error: 'Source and target folders must be different' });
705
+ }
706
+ // Access enforced by graphForWrite — both source (delete on move)
707
+ // and target (create) require write access. Resolved up-front so a
708
+ // target-side denial never surfaces only after source has been
709
+ // mutated. Audit (2026-05) flagged the previous opt-in pattern.
710
+ const sourceGraph = graphForWrite(req, reply, sourceRepo);
711
+ if (!sourceGraph)
712
+ return;
713
+ const targetGraph = graphForWrite(req, reply, targetRepo);
714
+ if (!targetGraph)
715
+ return;
574
716
  try {
575
- // Flush pending saves
576
- if (sessionManager)
717
+ // Phase 0 — prevalidate. Audit (2026-05) required this: previously
718
+ // the route created in target, broadcast `created`, and only then
719
+ // tried to delete from source. A delete failure left a duplicate
720
+ // entry the client had already been told about.
721
+ if (sessionManager) {
577
722
  await sessionManager.flush(sourceRepo);
578
- // Read source entity
723
+ if (targetRepo !== sourceRepo)
724
+ await sessionManager.flush(targetRepo);
725
+ }
726
+ // Source must exist; target must not collide. Both checks run BEFORE
727
+ // any mutation so we never enter phase 2 unless phase 1 will succeed.
579
728
  const source = sourceGraph.get(entityType, entityId);
580
- // Check for conflicts in target
729
+ const sourcePath = source.path;
730
+ const sourceRawMarkdown = readFileSync(sourcePath, 'utf-8');
581
731
  try {
582
732
  targetGraph.get(entityType, entityId);
583
733
  return reply.status(409).send({ error: `"${entityId}" already exists in ${targetRepo}` });
584
734
  }
585
735
  catch { /* not found = good */ }
586
- // Create in target
587
736
  const gitUser = reqGitUser(req);
588
737
  const { entity_type, entity_id, ...dataFields } = source.data;
738
+ // Phase 1 — create in target. No broadcast yet: a half-completed
739
+ // move (created in target, source still present) must NOT be
740
+ // observable to clients, or they will display two copies until the
741
+ // next refresh. `targetFolderPath` lets the caller drop the entry
742
+ // straight into a subfolder of the target repo.
589
743
  const created = await targetGraph.create({
590
744
  entityType: entityType,
591
745
  entityId,
@@ -593,21 +747,109 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
593
747
  content: source.document?.content ?? '',
594
748
  commitMessage: `${mode === 'move' ? 'Move' : 'Copy'} ${entityType}: ${entityId} from ${sourceRepo}`,
595
749
  user: gitUser,
750
+ folderPath: targetFolderPath,
596
751
  });
752
+ // Phase 2 — for `move` only: delete from source. If this fails, run
753
+ // a compensating delete on target so we don't leave a duplicate.
754
+ // The compensating delete is best-effort; if it also fails, surface
755
+ // a 500 with both error messages so the residue is visible (the
756
+ // previous behavior silently succeeded and left orphans).
757
+ if (mode === 'move') {
758
+ try {
759
+ await sourceGraph.delete(entityType, entityId, undefined, gitUser);
760
+ }
761
+ catch (deleteErr) {
762
+ const deleteMsg = deleteErr instanceof Error ? deleteErr.message : String(deleteErr);
763
+ let recoveryMsg = null;
764
+ try {
765
+ let restoreSource = !existsSync(sourcePath);
766
+ if (!restoreSource) {
767
+ try {
768
+ restoreSource = readFileSync(sourcePath, 'utf-8') !== sourceRawMarkdown;
769
+ }
770
+ catch {
771
+ restoreSource = true;
772
+ }
773
+ }
774
+ if (restoreSource) {
775
+ mkdirSync(dirname(sourcePath), { recursive: true });
776
+ writeFileSync(sourcePath, sourceRawMarkdown, 'utf-8');
777
+ const recoveryPaths = [sourcePath];
778
+ const folderMarkerPath = join(dirname(sourcePath), '.gitkeep');
779
+ if (existsSync(folderMarkerPath)) {
780
+ rmSync(folderMarkerPath, { force: true });
781
+ recoveryPaths.push(folderMarkerPath);
782
+ }
783
+ const sourceAny = sourceGraph;
784
+ const recoveryGit = sourceAny.gitService;
785
+ const recoveryUser = gitUser ?? sourceAny.gitUser;
786
+ if (!recoveryGit || !recoveryUser) {
787
+ throw new Error('source graph git service or git user unavailable');
788
+ }
789
+ recoveryGit.commitFiles(recoveryPaths, `Recover ${entityType}: ${entityId} after failed move to ${targetRepo}`, recoveryUser);
790
+ }
791
+ }
792
+ catch (recoveryErr) {
793
+ recoveryMsg = recoveryErr instanceof Error ? recoveryErr.message : String(recoveryErr);
794
+ log.error('[critical] cross-repo move source recovery failed', {
795
+ sourceRepo,
796
+ targetRepo,
797
+ entityType,
798
+ entityId,
799
+ sourcePath,
800
+ deleteError: deleteMsg,
801
+ recoveryError: recoveryMsg,
802
+ capturedContent: sourceRawMarkdown,
803
+ });
804
+ }
805
+ try {
806
+ await targetGraph.delete(entityType, entityId, undefined, gitUser);
807
+ if (recoveryMsg) {
808
+ return reply.status(500).send({
809
+ 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.`,
810
+ code: 'MOVE_SOURCE_RECOVERY_FAILED',
811
+ sourceRepo,
812
+ targetRepo,
813
+ });
814
+ }
815
+ return reply.status(500).send({
816
+ error: `Move failed: source delete of ${entityType}/${entityId} failed (${deleteMsg}). Target rolled back; folders are back to their pre-move state.`,
817
+ });
818
+ }
819
+ catch (compErr) {
820
+ const compMsg = compErr instanceof Error ? compErr.message : String(compErr);
821
+ if (recoveryMsg) {
822
+ return reply.status(500).send({
823
+ 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.`,
824
+ code: 'MOVE_RECOVERY_AND_ROLLBACK_FAILED',
825
+ sourceRepo,
826
+ targetRepo,
827
+ });
828
+ }
829
+ return reply.status(500).send({
830
+ 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.`,
831
+ code: 'MOVE_DUPLICATE',
832
+ sourceRepo,
833
+ targetRepo,
834
+ });
835
+ }
836
+ }
837
+ }
838
+ // Phase 3 — both phases succeeded; only now broadcast. Order:
839
+ // source-delete first so subscribers don't briefly observe the
840
+ // entity in both folders during the network race.
597
841
  const actor = req.user?.displayName ?? 'API';
598
842
  const ts = new Date().toISOString();
599
- wsHub?.broadcast({
600
- type: 'entity_change', action: 'created', repo: targetRepo, entityType, entityId,
601
- actor, source: 'api', timestamp: ts,
602
- });
603
- // Delete from source if moving
604
843
  if (mode === 'move') {
605
- await sourceGraph.delete(entityType, entityId, undefined, gitUser);
606
844
  wsHub?.broadcast({
607
845
  type: 'entity_change', action: 'deleted', repo: sourceRepo, entityType, entityId,
608
846
  actor, source: 'api', timestamp: ts,
609
847
  });
610
848
  }
849
+ wsHub?.broadcast({
850
+ type: 'entity_change', action: 'created', repo: targetRepo, entityType, entityId,
851
+ actor, source: 'api', timestamp: ts,
852
+ });
611
853
  return reply.send({
612
854
  success: true,
613
855
  entity: created,
@@ -618,19 +860,107 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
618
860
  return reply.status(400).send({ error: err.message });
619
861
  }
620
862
  });
621
- // DELETE /api/repos/:repo/entities/:type/:iddelete an entity
622
- fastify.delete('/api/repos/:repo/entities/:type/:id', async (req, reply) => {
623
- const { repo, type, id } = req.params;
624
- if (!hasRepoAccess(req, repo, workspaceManager, authService)) {
625
- return reply.status(404).send({ error: `Collection "${repo}" not found` });
863
+ // POST /api/repos/:repo/transfer-entities — bulk move a list of entries
864
+ // into a single target folder (in this or another repo). The web UI's
865
+ // multi-select Move flow used to loop and POST per entry; this collapses
866
+ // to one round-trip per source repo and one commit per repo touched.
867
+ fastify.post('/api/repos/:repo/transfer-entities', async (req, reply) => {
868
+ const sourceRepo = req.params.repo;
869
+ const { entries, targetRepo, targetFolder } = req.body ?? {};
870
+ if (!Array.isArray(entries) || !targetRepo || targetFolder === undefined) {
871
+ return reply.status(400).send({ error: 'entries, targetRepo, and targetFolder are required' });
872
+ }
873
+ // Access enforced by graphForWrite — both source (mutate) and
874
+ // target (write) require write access. Resolved up-front so a
875
+ // target-side denial never surfaces only after source mutation.
876
+ const sourceGraph = graphForWrite(req, reply, sourceRepo);
877
+ if (!sourceGraph)
878
+ return;
879
+ const targetGraph = graphForWrite(req, reply, targetRepo);
880
+ if (!targetGraph)
881
+ return;
882
+ try {
883
+ if (sessionManager) {
884
+ await sessionManager.flush(sourceRepo);
885
+ if (targetRepo !== sourceRepo)
886
+ await sessionManager.flush(targetRepo);
887
+ }
888
+ const result = await sourceGraph.moveEntitiesToRepo(entries.map(e => ({ entityType: e.entityType, entityId: e.entityId })), targetGraph, targetFolder, { user: reqGitUser(req) });
889
+ const actor = req.user?.displayName ?? 'API';
890
+ const ts = new Date().toISOString();
891
+ wsHub?.broadcast({ type: 'repo_sync', repo: sourceRepo, source: 'api', actor, timestamp: ts });
892
+ if (targetRepo !== sourceRepo) {
893
+ wsHub?.broadcast({ type: 'repo_sync', repo: targetRepo, source: 'api', actor, timestamp: ts });
894
+ }
895
+ return reply.send({ success: true, ...result });
896
+ }
897
+ catch (err) {
898
+ const status = err?.name === 'InvalidFolderPathError' ? 400
899
+ : err.message?.includes('not found') ? 404
900
+ : err.message?.includes('already occupied') ? 409
901
+ : 400;
902
+ return reply.status(status).send({ error: err.message });
626
903
  }
627
- let graph;
904
+ });
905
+ // POST /api/repos/:repo/transfer-folder — move an entire folder (with all
906
+ // descendants) into another repo in one shot. Replaces the per-entry loop
907
+ // the web UI used to run, which produced 2 git commits per entry; this
908
+ // route performs a single FS-level rename and one commit per repo.
909
+ fastify.post('/api/repos/:repo/transfer-folder', async (req, reply) => {
910
+ const sourceRepo = req.params.repo;
911
+ const { sourceFolder, targetRepo, targetFolder } = req.body ?? {};
912
+ if (!sourceFolder || !targetRepo || targetFolder === undefined) {
913
+ return reply.status(400).send({ error: 'sourceFolder, targetRepo, and targetFolder are required' });
914
+ }
915
+ if (sourceRepo === targetRepo && sourceFolder === targetFolder) {
916
+ return reply.status(400).send({ error: 'Source and target are the same' });
917
+ }
918
+ // Access enforced by graphForWrite — both source (mutate) and
919
+ // target (write) require write access. Resolved up-front so a
920
+ // target-side denial never surfaces only after source mutation.
921
+ const sourceGraph = graphForWrite(req, reply, sourceRepo);
922
+ if (!sourceGraph)
923
+ return;
924
+ const targetGraph = graphForWrite(req, reply, targetRepo);
925
+ if (!targetGraph)
926
+ return;
628
927
  try {
629
- graph = workspaceManager.getGraph(repo);
928
+ // Flush any deferred saves on either side so we don't strand a
929
+ // pending edit in the source folder mid-move.
930
+ if (sessionManager) {
931
+ await sessionManager.flush(sourceRepo);
932
+ if (targetRepo !== sourceRepo)
933
+ await sessionManager.flush(targetRepo);
934
+ }
935
+ const stats = await sourceGraph.moveFolderToRepo(sourceFolder, targetGraph, targetFolder, { user: reqGitUser(req) });
936
+ const actor = req.user?.displayName ?? 'API';
937
+ const ts = new Date().toISOString();
938
+ // One refresh-trigger per repo. The receiving and sending lists
939
+ // both need to update; using `repo_sync` keeps the realtime
940
+ // handler simple — it already calls workspace.refreshEntities
941
+ // on this event type. Avoids emitting 2× entries-moved entity_change
942
+ // broadcasts (one created, one deleted) per file.
943
+ wsHub?.broadcast({ type: 'repo_sync', repo: sourceRepo, source: 'api', actor, timestamp: ts });
944
+ if (targetRepo !== sourceRepo) {
945
+ wsHub?.broadcast({ type: 'repo_sync', repo: targetRepo, source: 'api', actor, timestamp: ts });
946
+ }
947
+ return reply.send({ success: true, ...stats });
630
948
  }
631
949
  catch (err) {
632
- return reply.status(404).send({ error: err.message });
950
+ const status = err?.name === 'InvalidFolderPathError' ? 400
951
+ : err.message?.includes('not found') ? 404
952
+ : err.message?.includes('not empty') ? 409
953
+ : 400;
954
+ return reply.status(status).send({ error: err.message });
633
955
  }
956
+ });
957
+ // DELETE /api/repos/:repo/entities/:type/:id — delete an entity
958
+ fastify.delete('/api/repos/:repo/entities/:type/:id', async (req, reply) => {
959
+ const { repo, type, id } = req.params;
960
+ // Access enforced by graphForRead (WorkspaceAccessService).
961
+ const graph = graphForRead(req, reply, repo);
962
+ if (!graph)
963
+ return;
634
964
  try {
635
965
  await graph.delete(type, id, `Delete ${type}/${id} from web UI`, reqGitUser(req));
636
966
  wsHub?.broadcast({
@@ -644,29 +974,63 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
644
974
  return reply.status(400).send({ error: err.message });
645
975
  }
646
976
  });
647
- // POST /api/repos/:repo/entities — create a new entity
648
- fastify.post('/api/repos/:repo/entities', async (req, reply) => {
977
+ // POST /api/repos/:repo/entities/bulk-deletedelete many entities in
978
+ // one git transaction + one WS broadcast. Replaces N parallel
979
+ // DELETE /entities/:type/:id calls, which on a 500-entry bulk delete
980
+ // produced 500 git commits + 500 entity_change broadcasts and froze
981
+ // the UI for minutes.
982
+ fastify.post('/api/repos/:repo/entities/bulk-delete', async (req, reply) => {
649
983
  const { repo } = req.params;
650
- if (!hasRepoAccess(req, repo, workspaceManager, authService)) {
651
- return reply.status(404).send({ error: `Collection "${repo}" not found` });
652
- }
653
- const { entityType, entityId, data = {}, content = '' } = req.body;
654
- if (!entityType || !entityId) {
655
- return reply.status(400).send({ error: 'entityType and entityId are required' });
984
+ // Access enforced by graphForWrite (WorkspaceAccessService).
985
+ const graph = graphForWrite(req, reply, repo);
986
+ if (!graph)
987
+ return;
988
+ const body = req.body;
989
+ if (!body?.entities || !Array.isArray(body.entities)) {
990
+ return reply.status(400).send({ error: 'Expected { entities: [{ entityType, entityId }] }' });
656
991
  }
657
- let graph;
658
992
  try {
659
- graph = workspaceManager.getGraph(repo);
993
+ const result = await graph.bulkDelete(body.entities.map(e => ({ entityType: e.entityType, entityId: e.entityId })), {
994
+ user: reqGitUser(req),
995
+ commitMessage: `Bulk delete ${body.entities.length} ${body.entities.length === 1 ? 'entry' : 'entries'} from web UI`,
996
+ });
997
+ if (result.deleted.length > 0) {
998
+ wsHub?.broadcast({
999
+ type: 'repo_sync', repo,
1000
+ actor: req.user?.displayName ?? 'API',
1001
+ source: 'api', timestamp: new Date().toISOString(),
1002
+ });
1003
+ }
1004
+ return reply.send({
1005
+ deleted: result.deleted.length,
1006
+ errors: result.errors,
1007
+ });
660
1008
  }
661
1009
  catch (err) {
662
- return reply.status(404).send({ error: err.message });
1010
+ return reply.status(400).send({ error: err.message });
663
1011
  }
1012
+ });
1013
+ // POST /api/repos/:repo/entities — create a new entity
1014
+ fastify.post('/api/repos/:repo/entities', async (req, reply) => {
1015
+ const { repo } = req.params;
1016
+ // Access enforced by graphForWrite (WorkspaceAccessService).
1017
+ const graph = graphForWrite(req, reply, repo);
1018
+ if (!graph)
1019
+ return;
1020
+ const { entityType, entityId, data = {}, content = '', folder_path } = req.body;
1021
+ if (!entityType || !entityId) {
1022
+ return reply.status(400).send({ error: 'entityType and entityId are required' });
1023
+ }
1024
+ const eagerError = checkEagerSkillPermission(req, entityType, data, repo, workspaceManager, authService);
1025
+ if (eagerError)
1026
+ return reply.status(403).send({ error: eagerError });
664
1027
  try {
665
1028
  const entity = await graph.create({
666
1029
  entityType: entityType,
667
1030
  entityId,
668
1031
  data: { entity_type: entityType, entity_id: entityId, ...data },
669
1032
  content,
1033
+ folderPath: folder_path,
670
1034
  commitMessage: `Create ${entityType}/${entityId} from web UI`,
671
1035
  user: reqGitUser(req),
672
1036
  });
@@ -681,44 +1045,266 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
681
1045
  return reply.status(400).send({ error: err.message });
682
1046
  }
683
1047
  });
1048
+ // POST /api/repos/:repo/move — move entry to a different folder within the
1049
+ // same repo. Distinct from /transfer (cross-repo) and /rename (id change).
1050
+ // The entry's type and id are unchanged; only its folder_path moves.
1051
+ fastify.post('/api/repos/:repo/move', async (req, reply) => {
1052
+ const { repo } = req.params;
1053
+ const { entityType, entityId, folder_path } = req.body ?? {};
1054
+ if (!entityType || !entityId || folder_path === undefined) {
1055
+ return reply.status(400).send({ error: 'entityType, entityId, and folder_path are required' });
1056
+ }
1057
+ // Access enforced by graphForWrite (WorkspaceAccessService).
1058
+ const graph = graphForWrite(req, reply, repo);
1059
+ if (!graph)
1060
+ return;
1061
+ try {
1062
+ if (sessionManager)
1063
+ await sessionManager.flush(repo);
1064
+ const moved = await graph.move(entityType, entityId, folder_path, undefined, reqGitUser(req));
1065
+ wsHub?.broadcast({
1066
+ type: 'entity_change', action: 'updated', repo, entityType, entityId,
1067
+ actor: req.user?.displayName ?? 'API',
1068
+ source: 'api', timestamp: new Date().toISOString(),
1069
+ });
1070
+ return reply.send({ success: true, entity: moved });
1071
+ }
1072
+ catch (err) {
1073
+ const status = err?.name === 'InvalidFolderPathError' ? 400
1074
+ : err.message?.includes('already occupied') ? 409
1075
+ : err.message?.includes('not found') ? 404
1076
+ : 400;
1077
+ return reply.status(status).send({ error: err.message });
1078
+ }
1079
+ });
1080
+ // POST /api/repos/:repo/folders — create a folder (with .gitkeep). Auto-creates parents.
1081
+ // Optional display_name is written to the folder's .studiograph-folder.json
1082
+ // sidecar when set; otherwise the client falls back to toDisplayName(slug).
1083
+ fastify.post('/api/repos/:repo/folders', async (req, reply) => {
1084
+ const { repo } = req.params;
1085
+ const { path, display_name } = req.body ?? {};
1086
+ if (!path || typeof path !== 'string') {
1087
+ return reply.status(400).send({ error: 'path is required' });
1088
+ }
1089
+ // Access enforced by graphForWrite (WorkspaceAccessService).
1090
+ const graph = graphForWrite(req, reply, repo);
1091
+ if (!graph)
1092
+ return;
1093
+ try {
1094
+ await graph.createFolder(path, undefined, reqGitUser(req), {
1095
+ displayName: display_name,
1096
+ });
1097
+ return reply.status(201).send({ success: true, path });
1098
+ }
1099
+ catch (err) {
1100
+ return reply.status(400).send({ error: err.message });
1101
+ }
1102
+ });
1103
+ // PATCH /api/repos/:repo/folders — rename / move a folder within the repo,
1104
+ // optionally updating its display_name in the same commit. Three shapes:
1105
+ // { from, to } — slug-only rename / move
1106
+ // { from, to, display_name } — slug change + display update
1107
+ // { from, to: from, display_name } — display-only update (no FS move)
1108
+ // `display_name: ""` (or null) clears the override and reverts to the slug
1109
+ // fallback.
1110
+ fastify.patch('/api/repos/:repo/folders', async (req, reply) => {
1111
+ const { repo } = req.params;
1112
+ const { from, to, display_name } = req.body ?? {};
1113
+ if (!from || !to) {
1114
+ return reply.status(400).send({ error: 'from and to are required' });
1115
+ }
1116
+ // Access enforced by graphForWrite (WorkspaceAccessService).
1117
+ const graph = graphForWrite(req, reply, repo);
1118
+ if (!graph)
1119
+ return;
1120
+ try {
1121
+ if (sessionManager)
1122
+ await sessionManager.flush(repo);
1123
+ await graph.renameFolder(from, to, undefined, reqGitUser(req), {
1124
+ displayName: display_name === undefined ? undefined : display_name,
1125
+ });
1126
+ return reply.send({ success: true, from, to });
1127
+ }
1128
+ catch (err) {
1129
+ const status = err?.name === 'InvalidFolderPathError' ? 400
1130
+ : err.message?.includes('not found') ? 404
1131
+ : err.message?.includes('already exists') ? 409
1132
+ : 400;
1133
+ return reply.status(status).send({ error: err.message });
1134
+ }
1135
+ });
1136
+ // DELETE /api/repos/:repo/folders?path=X&recursive=true — delete a folder.
1137
+ // Without recursive=true, non-empty folders return 409 with blast-radius
1138
+ // counts so the UI can confirm before retrying.
1139
+ fastify.delete('/api/repos/:repo/folders', async (req, reply) => {
1140
+ const { repo } = req.params;
1141
+ const { path, recursive } = req.query;
1142
+ if (!path) {
1143
+ return reply.status(400).send({ error: 'path query parameter is required' });
1144
+ }
1145
+ // Access enforced by graphForWrite (WorkspaceAccessService).
1146
+ const graph = graphForWrite(req, reply, repo);
1147
+ if (!graph)
1148
+ return;
1149
+ try {
1150
+ if (sessionManager)
1151
+ await sessionManager.flush(repo);
1152
+ const result = await graph.deleteFolder(path, {
1153
+ recursive: recursive === 'true',
1154
+ user: reqGitUser(req),
1155
+ });
1156
+ return reply.send({ success: true, ...result });
1157
+ }
1158
+ catch (err) {
1159
+ if (err?.code === 'FOLDER_NOT_EMPTY') {
1160
+ return reply.status(409).send({
1161
+ error: err.message,
1162
+ code: 'FOLDER_NOT_EMPTY',
1163
+ blast_radius: err.blastRadius,
1164
+ });
1165
+ }
1166
+ const status = err?.name === 'InvalidFolderPathError' ? 400
1167
+ : err.message?.includes('not found') ? 404
1168
+ : 400;
1169
+ return reply.status(status).send({ error: err.message });
1170
+ }
1171
+ });
684
1172
  // POST /api/repos/:repo/commit — flush deferred commits (navigate-away signal)
685
1173
  fastify.post('/api/repos/:repo/commit', async (req, reply) => {
686
1174
  if (!sessionManager) {
687
1175
  return reply.send({ committed: false });
688
1176
  }
689
1177
  const { repo } = req.params;
690
- if (!hasRepoAccess(req, repo, workspaceManager, authService)) {
691
- return reply.status(404).send({ error: `Collection "${repo}" not found` });
1178
+ // Access enforced via accessService.canRead \u2014 flush operates on the
1179
+ // SessionManager (no graph instance needed), so a boolean check is
1180
+ // sufficient. Pending commits were already authored by an authenticated
1181
+ // writer; this endpoint just flushes them on navigate-away.
1182
+ if (!accessService.canRead(reqUser(req), repo)) {
1183
+ return reply.status(404).send({ error: `Folder \u201C${repo}\u201D not found` });
692
1184
  }
693
1185
  const committed = await sessionManager.flush(repo);
694
1186
  return reply.send({ committed });
695
1187
  });
696
- // POST /api/repos/:repo/import — import files via agent processing
1188
+ // POST /api/repos/:repo/import — import files into a folder.
1189
+ //
1190
+ // Routing by extension:
1191
+ // .md / .txt / .csv → direct write via importFile() — no LLM
1192
+ // round-trip, preserves the source faithfully. Markdown frontmatter
1193
+ // (entity_type, entity_id) is honored when present; otherwise
1194
+ // sensible defaults.
1195
+ // .pdf + opaque binaries → asset wrapper via uploadAsset() —
1196
+ // creates an `asset` entity, stores the binary in R2 or the local
1197
+ // volume, writes a PDF text sidecar for vector indexing.
1198
+ // .docx → text extraction + agent capture
1199
+ // pipeline (until the mammoth → markdown workstream lands).
697
1200
  fastify.post('/api/repos/:repo/import', async (req, reply) => {
698
1201
  const { repo } = req.params;
699
- if (!hasRepoAccess(req, repo, workspaceManager, authService)) {
700
- return reply.status(403).send({ error: 'Access denied' });
701
- }
1202
+ // Access enforced by graphForWrite — import creates entities, so write
1203
+ // is required. Graph captured here is reused by the inner closures so
1204
+ // they don't need to re-resolve (which would also bypass the gate).
1205
+ const repoGraph = graphForWrite(req, reply, repo);
1206
+ if (!repoGraph)
1207
+ return;
702
1208
  const body = req.body;
703
1209
  if (!body?.files || !Array.isArray(body.files)) {
704
1210
  return reply.status(400).send({ error: 'Expected { files: [{ filename, content }] }' });
705
1211
  }
1212
+ // Reject paths with traversal segments; importFile slugifies the rest.
1213
+ // Empty / undefined = repo root (existing behavior).
1214
+ const targetFolderPath = (body.targetFolderPath ?? '').trim();
1215
+ if (targetFolderPath.split('/').some(seg => seg === '..' || seg === '.')) {
1216
+ return reply.status(400).send({ error: 'Invalid targetFolderPath: contains traversal segments' });
1217
+ }
706
1218
  const captureRepo = body.agentDecides ? undefined : repo;
1219
+ const gitUser = reqGitUser(req);
707
1220
  try {
708
- const { extractText, isSupportedFile } = await import('../../services/import-service.js');
1221
+ const { extractText, isSupportedFile, importFile } = await import('../../services/import-service.js');
1222
+ const { isAssetUploadExtension } = await import('../../services/assets/base.js');
1223
+ const { uploadAsset } = await import('../../services/asset-upload-service.js');
1224
+ const { extname } = await import('path');
709
1225
  const results = [];
1226
+ // Direct-write extensions never need an LLM. Verbatim preservation —
1227
+ // markdown stays markdown, CSV stays CSV.
1228
+ const DIRECT_WRITE_EXTS = new Set(['.md', '.txt', '.csv']);
710
1229
  const CONCURRENCY = 3;
711
- async function processFile(file) {
712
- if (!file.filename || !isSupportedFile(file.filename)) {
713
- return { filename: file.filename, status: 'error', error: `Unsupported file: ${file.filename}` };
1230
+ async function processDirect(file) {
1231
+ // Reuse the access-checked graph captured in the outer scope. Do
1232
+ // NOT re-resolve via workspaceManager.getGraph here the audit's
1233
+ // boundary is the outer graphForWrite call.
1234
+ const graph = repoGraph;
1235
+ const result = await importFile(graph, file.filename, file.content, {
1236
+ user: gitUser,
1237
+ // Caller-chosen destination subfolder; importFile combines it
1238
+ // with each file's own relative directory.
1239
+ folderPath: targetFolderPath || undefined,
1240
+ // Defer commits — we batch them into one transaction at the
1241
+ // end of the request so a 500-file import is one git commit
1242
+ // instead of 500. Per-file commits dominated wall time
1243
+ // (~50-200ms each) before this; batching collapses that to
1244
+ // one commit for the whole batch.
1245
+ skipCommit: true,
1246
+ });
1247
+ if (result.status === 'created' && result.path) {
1248
+ return {
1249
+ filename: file.filename,
1250
+ status: 'created',
1251
+ entities: [{ entityId: result.entityId, entityType: result.entityType }],
1252
+ entry: {
1253
+ operation: 'create',
1254
+ entityType: result.entityType,
1255
+ entityId: result.entityId,
1256
+ filePath: result.path,
1257
+ },
1258
+ sidecarPaths: result.sidecarPaths,
1259
+ };
1260
+ }
1261
+ return { filename: file.filename, status: result.status, error: result.error };
1262
+ }
1263
+ async function processAsset(file) {
1264
+ // Reuse the outer access-checked graph; re-resolving would bypass
1265
+ // the architectural boundary even though the closure captures repo.
1266
+ const graph = repoGraph;
1267
+ try {
1268
+ const buffer = Buffer.from(file.content, 'base64');
1269
+ const result = await uploadAsset({
1270
+ graph,
1271
+ filename: file.filename,
1272
+ buffer,
1273
+ folderPath: targetFolderPath || undefined,
1274
+ user: gitUser,
1275
+ workspacePath: graph.getWorkspaceRoot(),
1276
+ // Defer commits — the dispatcher's batched commitPending
1277
+ // call at the bottom of the route covers every entry in
1278
+ // the same transaction.
1279
+ skipCommit: true,
1280
+ });
1281
+ return {
1282
+ filename: file.filename,
1283
+ status: 'created',
1284
+ entities: [{ entityId: result.entityId, entityType: result.entityType }],
1285
+ entry: {
1286
+ operation: 'create',
1287
+ entityType: 'asset',
1288
+ entityId: result.entityId,
1289
+ filePath: result.filePath,
1290
+ },
1291
+ // PDF text sidecars: include in extraPaths so they land in
1292
+ // the same commit as the entity files.
1293
+ sidecarPaths: result.sidecarPath ? [result.sidecarPath] : undefined,
1294
+ };
1295
+ }
1296
+ catch (err) {
1297
+ return { filename: file.filename, status: 'error', error: err.message ?? 'Asset upload failed' };
714
1298
  }
1299
+ }
1300
+ async function processViaAgent(file) {
715
1301
  try {
716
1302
  const text = await extractText(file.filename, file.content);
717
1303
  if (!text) {
718
- console.warn(`[import] No text extracted from ${file.filename}`);
1304
+ log.warn(`[import] No text extracted from ${file.filename}`);
719
1305
  return { filename: file.filename, status: 'error', error: 'No text content extracted' };
720
1306
  }
721
- console.log(`[import] Extracted ${text.length} chars from ${file.filename}`);
1307
+ log.info(`[import] Extracted ${text.length} chars from ${file.filename}`);
722
1308
  const captureRes = await fastify.inject({
723
1309
  method: 'POST',
724
1310
  url: '/api/capture',
@@ -731,7 +1317,7 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
731
1317
  });
732
1318
  const captureBody = JSON.parse(captureRes.body);
733
1319
  if (captureRes.statusCode !== 200 || !captureBody.success) {
734
- console.warn(`[import] Capture failed for ${file.filename}: ${captureRes.statusCode} ${captureBody.error ?? JSON.stringify(captureBody).slice(0, 200)}`);
1320
+ log.warn(`[import] Capture failed for ${file.filename}: ${captureRes.statusCode} ${captureBody.error ?? JSON.stringify(captureBody).slice(0, 200)}`);
735
1321
  return { filename: file.filename, status: 'error', error: captureBody.error ?? 'Agent processing failed' };
736
1322
  }
737
1323
  return { filename: file.filename, status: 'created', entities: captureBody.entities };
@@ -740,20 +1326,101 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
740
1326
  return { filename: file.filename, status: 'error', error: err.message ?? 'Processing failed' };
741
1327
  }
742
1328
  }
1329
+ async function processFile(file) {
1330
+ if (!file.filename) {
1331
+ return { filename: file.filename, status: 'error', error: 'Missing filename' };
1332
+ }
1333
+ const ext = extname(file.filename).toLowerCase();
1334
+ // Direct-write text formats first — never collide with asset
1335
+ // detection. Then asset extensions (images, PDF, design files,
1336
+ // archives, …). Then the agent-extraction fallback for .docx
1337
+ // until the mammoth workstream lands. Anything else: reject.
1338
+ if (DIRECT_WRITE_EXTS.has(ext))
1339
+ return processDirect(file);
1340
+ if (isAssetUploadExtension(file.filename))
1341
+ return processAsset(file);
1342
+ if (isSupportedFile(file.filename))
1343
+ return processViaAgent(file);
1344
+ return { filename: file.filename, status: 'error', error: `Unsupported file: ${file.filename}` };
1345
+ }
743
1346
  // Process files in batches of CONCURRENCY
744
1347
  for (let i = 0; i < body.files.length; i += CONCURRENCY) {
745
1348
  const batch = body.files.slice(i, i + CONCURRENCY);
746
1349
  const batchResults = await Promise.all(batch.map(processFile));
747
1350
  results.push(...batchResults);
748
1351
  }
1352
+ // Direct-write path deferred git commits — flush them all in one
1353
+ // transaction now. Collapses N per-file commits into one batch
1354
+ // commit (~50ms vs N×50ms). Agent-path files committed inline via
1355
+ // /api/capture so they're not in this set.
1356
+ const pendingEntries = results
1357
+ .map(r => r.entry)
1358
+ .filter((e) => Boolean(e));
1359
+ // Collect every sidecar path produced by the import — these stage in
1360
+ // the same commit as the entity files, so the folder display name
1361
+ // and the entries it labels land atomically. De-dup because two
1362
+ // imported files inside the same Schema_OS/widgets/ would otherwise
1363
+ // produce duplicate sidecar entries.
1364
+ const sidecarPaths = Array.from(new Set(results.flatMap(r => r.sidecarPaths ?? [])));
1365
+ // commitPending failure used to be swallowed and the route returned
1366
+ // success=true even though the files were never committed. The audit
1367
+ // (2026-05) flagged this: a 500-file import that failed mid-commit
1368
+ // looked successful to the client but left N untracked files. Now
1369
+ // the failure propagates as 500 with a partial-failure shape so the
1370
+ // caller knows which entries are stranded.
1371
+ let commitError = null;
1372
+ if (pendingEntries.length > 0) {
1373
+ try {
1374
+ await repoGraph.commitPending(pendingEntries, {
1375
+ commitMessage: `Import ${pendingEntries.length} ${pendingEntries.length === 1 ? 'entry' : 'entries'} from web UI`,
1376
+ user: gitUser,
1377
+ extraPaths: sidecarPaths.length > 0 ? sidecarPaths : undefined,
1378
+ });
1379
+ }
1380
+ catch (err) {
1381
+ commitError = err instanceof Error ? err.message : String(err);
1382
+ log.warn(`[import] batch commit failed for ${repo}: ${commitError}`);
1383
+ }
1384
+ }
749
1385
  const created = results.filter(r => r.status === 'created').length;
750
1386
  const entityCount = results.reduce((sum, r) => sum + (r.entities?.length ?? 0), 0);
1387
+ // Single repo_sync broadcast for the whole import. The realtime
1388
+ // store's repo_sync handler refreshes the repo's entity list once
1389
+ // — vs. one entity_change per file, which would do N re-renders
1390
+ // on every connected client and freeze the UI for large imports.
1391
+ // Skip when commit failed: there's nothing for clients to refresh
1392
+ // beyond stranded uncommitted files, and the broadcast would lie
1393
+ // about persistence.
1394
+ if (created > 0 && !commitError) {
1395
+ wsHub?.broadcast({
1396
+ type: 'repo_sync', repo,
1397
+ actor: req.user?.displayName ?? 'API',
1398
+ source: 'api', timestamp: new Date().toISOString(),
1399
+ });
1400
+ }
1401
+ const errors = results.filter(r => r.status === 'error').map(r => r.error);
1402
+ if (commitError) {
1403
+ // Files are on disk but uncommitted — partial failure. Surface as
1404
+ // 500 with details so the client doesn't show "imported N entries"
1405
+ // for entries that won't survive a server restart.
1406
+ return reply.status(500).send({
1407
+ success: false,
1408
+ error: `Import wrote ${pendingEntries.length} entries to disk but the git commit failed: ${commitError}`,
1409
+ commitError,
1410
+ uncommittedEntries: pendingEntries.length,
1411
+ imported: created,
1412
+ entityCount,
1413
+ skipped: errors.length,
1414
+ errors,
1415
+ results,
1416
+ });
1417
+ }
751
1418
  return {
752
1419
  success: true,
753
1420
  imported: created,
754
1421
  entityCount,
755
- skipped: results.filter(r => r.status === 'error').length,
756
- errors: results.filter(r => r.status === 'error').map(r => r.error),
1422
+ skipped: errors.length,
1423
+ errors,
757
1424
  results,
758
1425
  };
759
1426
  }
@@ -767,9 +1434,10 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
767
1434
  // POST /api/repos/:repo/import-bookmarks — import browser bookmark exports as reference entities
768
1435
  fastify.post('/api/repos/:repo/import-bookmarks', async (req, reply) => {
769
1436
  const { repo } = req.params;
770
- if (!hasRepoAccess(req, repo, workspaceManager, authService)) {
771
- return reply.status(403).send({ error: 'Access denied' });
772
- }
1437
+ // Access enforced by graphForWrite — bookmark import creates entities.
1438
+ const graph = graphForWrite(req, reply, repo);
1439
+ if (!graph)
1440
+ return;
773
1441
  const body = req.body;
774
1442
  if (!body?.content || typeof body.content !== 'string') {
775
1443
  return reply.status(400).send({ error: 'Expected { content: string }' });
@@ -781,7 +1449,6 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
781
1449
  error: "This doesn't appear to be a browser bookmark export. Export bookmarks from your browser's bookmark manager to generate the correct format.",
782
1450
  });
783
1451
  }
784
- const graph = workspaceManager.getGraph(repo);
785
1452
  const user = reqGitUser(req);
786
1453
  const result = await importBookmarks(graph, body.content, user);
787
1454
  // Single git commit for all created entities
@@ -806,41 +1473,73 @@ export async function registerGraphApiRoutes(fastify, workspaceManager, authServ
806
1473
  });
807
1474
  }
808
1475
  });
809
- // POST /api/repos/:repo/import-imagesimport images as reference entities
810
- fastify.post('/api/repos/:repo/import-images', { bodyLimit: 50 * 1024 * 1024 }, async (req, reply) => {
1476
+ // POST /api/repos/:repo/assetsgeneric asset upload.
1477
+ //
1478
+ // Multipart `file` field. Creates a wrapper `asset` entity whose
1479
+ // frontmatter points at the stored binary (R2 or local volume) via
1480
+ // `asset_url`. PDFs additionally get a text sidecar at
1481
+ // `<workspace>/.studiograph/assets-text/asset/<id>.txt` so the vector
1482
+ // indexer can RAG them without bloating the markdown body. See
1483
+ // docs/asset-architecture.md.
1484
+ fastify.post('/api/repos/:repo/assets', async (req, reply) => {
811
1485
  const { repo } = req.params;
812
- if (!hasRepoAccess(req, repo, workspaceManager, authService)) {
813
- return reply.status(403).send({ error: 'Access denied' });
1486
+ // Access enforced by graphForWrite (WorkspaceAccessService).
1487
+ const graph = graphForWrite(req, reply, repo);
1488
+ if (!graph)
1489
+ return;
1490
+ // Per-route fileSize override — the global multipart limit (10MB) is
1491
+ // tuned for inline image embeds; full asset uploads accept up to the
1492
+ // 100MB cap enforced inside AssetService.upload().
1493
+ let file;
1494
+ try {
1495
+ file = await req.file({ limits: { fileSize: 100 * 1024 * 1024 } });
814
1496
  }
815
- const body = req.body;
816
- if (!body?.files || !Array.isArray(body.files)) {
817
- return reply.status(400).send({ error: 'Expected { files: [{ filename, content }] }' });
1497
+ catch (err) {
1498
+ return reply.status(400).send({ error: err?.message ?? 'Invalid multipart upload' });
1499
+ }
1500
+ if (!file)
1501
+ return reply.status(400).send({ error: 'No file uploaded' });
1502
+ const buffer = await file.toBuffer();
1503
+ const folderPath = (req.query.folder ?? '').trim();
1504
+ if (folderPath.split('/').some(seg => seg === '..' || seg === '.')) {
1505
+ return reply.status(400).send({ error: 'Invalid folder: contains traversal segments' });
818
1506
  }
819
1507
  try {
820
- const { importImages } = await import('../../services/image-import-service.js');
821
- const graph = workspaceManager.getGraph(repo);
822
- const user = reqGitUser(req);
823
- const result = await importImages(graph, body.files, user);
824
- // Single git commit for all created entities
825
- if (result.created > 0) {
826
- const git = graph.getGitService();
827
- if (git) {
828
- const commitUser = user ?? { id: 'import', name: 'Import', email: 'import@studiograph' };
829
- git.commitAll(`Import ${result.created} image${result.created !== 1 ? 's' : ''} as references`, commitUser);
830
- }
831
- }
832
- return {
1508
+ const { uploadAsset } = await import('../../services/asset-upload-service.js');
1509
+ const result = await uploadAsset({
1510
+ graph,
1511
+ filename: file.filename,
1512
+ buffer,
1513
+ folderPath: folderPath || undefined,
1514
+ name: req.query.name?.trim() || undefined,
1515
+ user: reqGitUser(req),
1516
+ workspacePath: graph.getWorkspaceRoot(),
1517
+ });
1518
+ wsHub?.broadcast({
1519
+ type: 'repo_sync',
1520
+ repo,
1521
+ actor: req.user?.displayName ?? 'API',
1522
+ source: 'api',
1523
+ timestamp: new Date().toISOString(),
1524
+ });
1525
+ return reply.send({
833
1526
  success: true,
834
- created: result.created,
835
- skipped: result.skipped,
836
- errors: result.errors,
837
- };
1527
+ entity: {
1528
+ entityId: result.entityId,
1529
+ entityType: result.entityType,
1530
+ asset_url: result.asset_url,
1531
+ asset_kind: result.asset_kind,
1532
+ asset_size: result.asset_size,
1533
+ pages: result.pages,
1534
+ },
1535
+ });
838
1536
  }
839
1537
  catch (err) {
840
- return reply.status(400).send({
841
- success: false,
842
- error: err instanceof Error ? err.message : 'Unknown error',
843
- });
1538
+ const msg = err instanceof Error ? err.message : String(err);
1539
+ const status = msg.includes('not configured') ? 501
1540
+ : msg.includes('Unsupported asset extension') ? 415
1541
+ : 400;
1542
+ return reply.status(status).send({ success: false, error: msg });
844
1543
  }
845
1544
  });
846
1545
  }