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
@@ -4,14 +4,34 @@
4
4
  * Provides unified entity management across any repo type.
5
5
  * Integrates Git, Markdown, and Asset services.
6
6
  */
7
- import { join, dirname } from 'path';
8
- import { existsSync, readdirSync, statSync, mkdirSync } from 'fs';
7
+ import { join, dirname, relative } from 'path';
8
+ import { existsSync, readdirSync, rmSync, statSync, writeFileSync, mkdirSync } from 'fs';
9
9
  import { GitService } from '../services/git.js';
10
10
  import { MarkdownService } from '../services/markdown.js';
11
11
  import { AssetService } from '../services/assets/index.js';
12
- import { ENTITY_SCHEMAS, EntityBaseSchema, SINGULAR_TO_PLURAL, PLURAL_TO_SINGULAR } from './validation.js';
13
- import { CSVService } from '../services/csv-service.js';
12
+ import { ENTITY_SCHEMAS, EntityBaseSchema, PLURAL_TO_SINGULAR } from './validation.js';
13
+ import { folderPathFromFile, normalizeFolderPath, validateFolderPath, InvalidFolderPathError } from './folder-paths.js';
14
+ import { FOLDER_SIDECAR, folderSidecarPath, readFolderSidecar, removeFolderSidecar, writeFolderSidecar } from './folder-sidecar.js';
14
15
  import { z } from 'zod';
16
+ /** Names skipped during recursive repo walks. */
17
+ const WALK_IGNORE = new Set(['.git', '.studiograph', 'node_modules']);
18
+ /**
19
+ * Thrown by `restoreFile` when the path the recreate would land at is
20
+ * currently occupied by a different entity. Carries the occupant's
21
+ * coordinates so callers (agent tool, REST endpoint) can surface a
22
+ * useful error to the user.
23
+ */
24
+ export class PathConflictError extends Error {
25
+ path;
26
+ occupiedBy;
27
+ code = 'PATH_CONFLICT';
28
+ constructor(path, occupiedBy) {
29
+ super(`Path ${path} is occupied by ${occupiedBy.entityType}/${occupiedBy.entityId}`);
30
+ this.path = path;
31
+ this.occupiedBy = occupiedBy;
32
+ this.name = 'PathConflictError';
33
+ }
34
+ }
15
35
  export class BaseGraphManager {
16
36
  repoPath;
17
37
  repoName;
@@ -20,9 +40,10 @@ export class BaseGraphManager {
20
40
  markdownService;
21
41
  assetService;
22
42
  gitUser;
23
- csvService;
24
43
  vectorService;
25
44
  schemaRegistry;
45
+ workspaceRoot;
46
+ mutationCallbacks;
26
47
  constructor(config) {
27
48
  this.repoPath = config.repoPath;
28
49
  this.repoName = config.repoName;
@@ -30,12 +51,12 @@ export class BaseGraphManager {
30
51
  this.noGit = config.noGit ?? false;
31
52
  this.vectorService = config.vectorService;
32
53
  this.schemaRegistry = config.schemaRegistry;
54
+ this.workspaceRoot = config.workspaceRoot;
33
55
  // Initialize services
34
56
  if (!this.noGit) {
35
57
  this.gitService = new GitService(this.repoPath);
36
58
  }
37
59
  this.markdownService = new MarkdownService();
38
- this.csvService = new CSVService();
39
60
  // Asset service is optional (can be disabled for repos without assets)
40
61
  if (config.assetConfig) {
41
62
  this.assetService = new AssetService(config.assetConfig);
@@ -43,10 +64,41 @@ export class BaseGraphManager {
43
64
  // Ensure repo is initialized
44
65
  this.ensureRepo();
45
66
  }
46
- /** Expose git service for deferred commit operations (e.g. CommitScheduler). */
67
+ /** Expose git service for deferred commit operations (SessionManager). */
47
68
  getGitService() {
48
69
  return this.gitService;
49
70
  }
71
+ /** Absolute repo path on disk. Used by callers that need to convert
72
+ * an entity's absolute path to a repo-relative one (e.g. for `git log`). */
73
+ getRepoPath() {
74
+ return this.repoPath;
75
+ }
76
+ /**
77
+ * Find an entity at a specific commit (existing or deleted) and
78
+ * return its repo-relative path + content. Public surface for the
79
+ * history tools — wraps the private `findEntityInCommit` walker.
80
+ * Returns null when no .md file at that commit matches the
81
+ * (entityType, entityId) pair.
82
+ */
83
+ getEntityAtCommit(entityType, entityId, commitHash) {
84
+ return this.findEntityInCommit(entityType, entityId, commitHash);
85
+ }
86
+ /** Workspace root passed in via GraphConfig. Used by the asset upload
87
+ * service to anchor PDF text sidecars at workspace level (so all repos
88
+ * share the same `assets-text/` tree, mirroring the shared
89
+ * `.studiograph/assets/` directory the local backend uses). */
90
+ getWorkspaceRoot() {
91
+ return this.workspaceRoot;
92
+ }
93
+ /**
94
+ * Install post-mutation callbacks. Called once after construction by the
95
+ * server bootstrap (or test setup) so the Yjs persistence layer can react
96
+ * to structural mutations without route-layer code calling persistence
97
+ * APIs directly.
98
+ */
99
+ setMutationCallbacks(callbacks) {
100
+ this.mutationCallbacks = callbacks;
101
+ }
50
102
  /**
51
103
  * Ensure repository exists
52
104
  */
@@ -57,11 +109,127 @@ export class BaseGraphManager {
57
109
  // GitService automatically initializes in constructor
58
110
  }
59
111
  /**
60
- * Get entity file path: {repo}/{plural-type}/{entityId}.md
112
+ * Compute the on-disk path for a new entity.
113
+ *
114
+ * - When `folderPath` is provided, the entry lives at {repo}/{folderPath}/{id}.md
115
+ * regardless of type. This is the nested-folders layout.
116
+ * - Without `folderPath` (or empty string), the entry lands at the repo
117
+ * root: {repo}/{id}.md. The historical {repo}/{plural-type}/{id}.md
118
+ * fallback was confusing — entries hidden under a typed directory the
119
+ * list view never showed at repo root, contradicting the agent tool
120
+ * description that says "Omit to land at repo root."
121
+ *
122
+ * Note: this is for *new* paths only. Existing entries living under the
123
+ * legacy {plural-type}/ directories continue to be readable via
124
+ * findEntityFile() — only the create path moved.
125
+ */
126
+ getEntityPath(entityType, entityId, folderPath) {
127
+ const normalized = folderPath ? normalizeFolderPath(folderPath) : '';
128
+ if (normalized) {
129
+ return join(this.repoPath, normalized, `${entityId}.md`);
130
+ }
131
+ return join(this.repoPath, `${entityId}.md`);
132
+ }
133
+ // ────────────────────────────────────────────────────────────────────────────
134
+ // Recursive file walk + (type, id) → path index
135
+ //
136
+ // With nested folders, an entry can live at any depth under the repo root.
137
+ // We can't compute its path from (type, id) alone — we have to find it.
138
+ // The index caches that lookup; mutations invalidate it via invalidateIndex().
139
+ // ────────────────────────────────────────────────────────────────────────────
140
+ fileIndex;
141
+ /** Drop the cached file index so the next read rebuilds it. */
142
+ invalidateIndex() {
143
+ this.fileIndex = undefined;
144
+ }
145
+ /**
146
+ * Walk every .md file under this repo, calling `visit` with the absolute
147
+ * path. Skips hidden directories (.git, .studiograph, etc.) and files that
148
+ * start with ".".
149
+ */
150
+ walkMarkdownFiles(visit) {
151
+ if (!existsSync(this.repoPath))
152
+ return;
153
+ const stack = [this.repoPath];
154
+ while (stack.length > 0) {
155
+ const dir = stack.pop();
156
+ let entries;
157
+ try {
158
+ entries = readdirSync(dir);
159
+ }
160
+ catch {
161
+ continue;
162
+ }
163
+ for (const entry of entries) {
164
+ if (entry.startsWith('.'))
165
+ continue;
166
+ if (WALK_IGNORE.has(entry))
167
+ continue;
168
+ const full = join(dir, entry);
169
+ let st;
170
+ try {
171
+ st = statSync(full);
172
+ }
173
+ catch {
174
+ continue;
175
+ }
176
+ if (st.isDirectory()) {
177
+ stack.push(full);
178
+ }
179
+ else if (st.isFile() && entry.endsWith('.md')) {
180
+ visit(full);
181
+ }
182
+ }
183
+ }
184
+ }
185
+ /**
186
+ * Build (or fetch the cached) "type/id" → absolute path index.
187
+ * Type comes from frontmatter `entity_type`; if missing, falls back to the
188
+ * top-level plural-type directory name (legacy layout). Files without either
189
+ * (e.g. README.md) are skipped — they aren't entities.
61
190
  */
62
- getEntityPath(entityType, entityId) {
63
- const pluralDir = SINGULAR_TO_PLURAL[entityType] || `${entityType}s`;
64
- return join(this.repoPath, pluralDir, `${entityId}.md`);
191
+ getFileIndex() {
192
+ if (this.fileIndex)
193
+ return this.fileIndex;
194
+ const index = new Map();
195
+ this.walkMarkdownFiles((filePath) => {
196
+ try {
197
+ const doc = this.markdownService.parseFile(filePath);
198
+ const fmType = doc.frontmatter.entity_type;
199
+ const inferred = this.inferLegacyType(filePath);
200
+ const type = fmType || inferred;
201
+ if (!type)
202
+ return; // README, CONTRIBUTING, etc.
203
+ const id = filePath.split('/').pop().replace(/\.md$/, '');
204
+ index.set(`${type}/${id}`, filePath);
205
+ }
206
+ catch { /* skip unparsable */ }
207
+ });
208
+ this.fileIndex = index;
209
+ return index;
210
+ }
211
+ /**
212
+ * Infer entity type from a file's location for legacy layouts where the
213
+ * file sits directly inside a top-level plural-type directory
214
+ * ({repo}/meetings/foo.md). Returns undefined for any other layout.
215
+ */
216
+ inferLegacyType(absPath) {
217
+ const rel = relative(this.repoPath, absPath);
218
+ const parts = rel.split('/');
219
+ if (parts.length !== 2)
220
+ return undefined; // not at depth 1
221
+ return PLURAL_TO_SINGULAR[parts[0]];
222
+ }
223
+ /**
224
+ * Resolve an entity's absolute path by (type, id). Returns undefined if not
225
+ * found. Uses the file index — the index keys by `${entityType}/${entityId}`
226
+ * which is type-aware, unlike the on-disk filename. (We previously had a
227
+ * legacy {plural-type}/{id}.md fast path here; once the create path moved
228
+ * to flat repo-root layout, that fast path could return a different
229
+ * type's file at the same id.)
230
+ */
231
+ findEntityFile(entityType, entityId) {
232
+ return this.getFileIndex().get(`${entityType}/${entityId}`);
65
233
  }
66
234
  /**
67
235
  * Validate entity data against schema
@@ -111,7 +279,7 @@ export class BaseGraphManager {
111
279
  }
112
280
  catch (error) {
113
281
  if (error instanceof z.ZodError) {
114
- const messages = error.errors.map(e => `${e.path.join('.')}: ${e.message}`);
282
+ const messages = error.issues.map(e => `${e.path.join('.')}: ${e.message}`);
115
283
  throw new Error(`Validation failed for ${entityType}:\n${messages.join('\n')}`);
116
284
  }
117
285
  throw error;
@@ -121,12 +289,18 @@ export class BaseGraphManager {
121
289
  * Create a new entity
122
290
  */
123
291
  async create(options) {
124
- const { entityType, entityId, data, content = '', commitMessage, skipCommit, user } = options;
292
+ const { entityType, entityId, data, content = '', commitMessage, skipCommit, folderPath, user } = options;
293
+ const normalizedFolder = folderPath ? validateFolderPath(folderPath) : '';
125
294
  // Prepopulate date from created_at if not provided
126
295
  if (!data.date && data.created_at) {
127
296
  data.date = String(data.created_at).split('T')[0];
128
297
  }
129
- // Add missing schema fields with empty defaults so the frontmatter is complete
298
+ // Add missing schema fields with empty defaults so the frontmatter is complete.
299
+ // Only string and array fields get populated — empty string isn't a valid
300
+ // member of number/boolean/enum/url types, and seeding them with '' caused
301
+ // optional numeric fields like `deal_value` to fail validation as
302
+ // "Expected number, received string". Anything that can't legitimately
303
+ // hold '' is left absent so the optional check passes.
130
304
  const schema = ENTITY_SCHEMAS[entityType];
131
305
  if (schema) {
132
306
  const shape = schema.shape;
@@ -139,28 +313,44 @@ export class BaseGraphManager {
139
313
  // Unwrap ZodOptional/ZodDefault to get the inner type
140
314
  const inner = field instanceof z.ZodOptional ? field._def.innerType
141
315
  : field instanceof z.ZodDefault ? field._def.innerType : field;
142
- if (inner instanceof z.ZodEnum) {
143
- // Don't populate enum fields with empty string — leave them unset
316
+ if (inner instanceof z.ZodArray) {
317
+ data[key] = [];
144
318
  continue;
145
319
  }
146
- const isArray = inner instanceof z.ZodArray;
147
- data[key] = isArray ? [] : '';
320
+ if (inner instanceof z.ZodString) {
321
+ // Skip URL-typed strings empty string fails URL validation.
322
+ if (inner._def.checks?.some((c) => c.def?.format === 'url'))
323
+ continue;
324
+ data[key] = '';
325
+ continue;
326
+ }
327
+ // Numbers, booleans, enums, literals, unions: leave absent.
148
328
  }
149
329
  }
150
330
  }
151
331
  // Validate data
152
332
  const validatedData = this.validateEntity(entityType, data);
153
- // Check if entity already exists
154
- const entityPath = this.getEntityPath(entityType, entityId);
155
- if (existsSync(entityPath)) {
333
+ // Reject if (type, id) is already taken anywhere in the repo — IDs are
334
+ // unique within (repo, type) regardless of folder.
335
+ const existingPath = this.findEntityFile(entityType, entityId);
336
+ if (existingPath) {
156
337
  throw new Error(`Entity already exists: ${entityType}/${entityId}`);
157
338
  }
339
+ // Compute target path. With folderPath set, entries land at
340
+ // {repo}/{folderPath}/{id}.md (parents auto-created); without, fall back
341
+ // to legacy {plural-type}/{id}.md.
342
+ const entityPath = this.getEntityPath(entityType, entityId, normalizedFolder);
343
+ mkdirSync(dirname(entityPath), { recursive: true });
158
344
  // Write markdown file
159
345
  this.markdownService.writeFile(entityPath, {
160
346
  frontmatter: validatedData,
161
347
  content,
162
348
  wikilinks: this.markdownService.extractWikilinks(content, validatedData),
163
349
  });
350
+ // The folder now has an .md child, so any pre-existing .gitkeep is
351
+ // redundant. Drop it as part of the same commit so the repo stays tidy.
352
+ const removedMarker = this.removeFolderMarker(dirname(entityPath));
353
+ this.invalidateIndex();
164
354
  if (this.gitService && !skipCommit) {
165
355
  const transaction = this.gitService.createTransaction();
166
356
  transaction.add({
@@ -169,21 +359,42 @@ export class BaseGraphManager {
169
359
  entityId,
170
360
  filePath: entityPath,
171
361
  });
362
+ if (removedMarker) {
363
+ transaction.add({
364
+ operation: 'delete',
365
+ entityType,
366
+ entityId,
367
+ filePath: removedMarker,
368
+ });
369
+ }
172
370
  const message = commitMessage || `Create ${entityType}: ${entityId}`;
173
371
  await transaction.commit(user ?? this.gitUser, message);
174
372
  }
175
373
  // Return created entity
176
374
  const created = this.get(entityType, entityId);
177
- // Update vector index (fire-and-forget, non-fatal)
178
- this.vectorService?.upsert(this.repoName, created).catch(err => console.warn(`[vector] index failed for ${entityType}/${entityId}:`, err.message));
375
+ // Update vector index (fire-and-forget, non-fatal). Failures are
376
+ // tracked on VectorService for /api/health visibility the audit
377
+ // (2026-05) flagged that silent .catch(console.warn) made a steady
378
+ // index-failure leak undetectable until search results went stale.
379
+ if (this.vectorService) {
380
+ const v = this.vectorService;
381
+ v.beginOp();
382
+ v.upsert(this.repoName, created)
383
+ .then(() => v.recordSuccess())
384
+ .catch(err => {
385
+ v.recordFailure('upsert', `${entityType}/${entityId}`, err);
386
+ console.warn(`[vector] index failed for ${entityType}/${entityId}:`, err.message);
387
+ });
388
+ }
179
389
  return created;
180
390
  }
181
391
  /**
182
- * Get an entity by type and ID
392
+ * Get an entity by type and ID. Searches nested folders if the entry isn't
393
+ * at the legacy {plural-type}/{id}.md path.
183
394
  */
184
395
  get(entityType, entityId) {
185
- const entityPath = this.getEntityPath(entityType, entityId);
186
- if (!existsSync(entityPath)) {
396
+ const entityPath = this.findEntityFile(entityType, entityId);
397
+ if (!entityPath) {
187
398
  throw new Error(`Entity not found: ${entityType}/${entityId}`);
188
399
  }
189
400
  const document = this.markdownService.parseFile(entityPath);
@@ -205,53 +416,62 @@ export class BaseGraphManager {
205
416
  return {
206
417
  id: entityId,
207
418
  path: entityPath,
419
+ folder_path: folderPathFromFile(entityPath, this.repoPath),
208
420
  entityType,
209
421
  data: validatedData,
210
422
  document,
211
423
  };
212
424
  }
213
425
  /**
214
- * Get an entity by ID only (type resolved from frontmatter)
426
+ * Get an entity by ID only (type resolved from the filesystem).
427
+ *
428
+ * Walks the file index to find any entry with this id, regardless of folder
429
+ * depth or type. Used by the agent's `get_entity` tool when the caller
430
+ * doesn't know the entity's type.
215
431
  */
216
432
  getById(entityId) {
217
- const entityPath = this.getEntityPath('', entityId);
218
- if (!existsSync(entityPath)) {
433
+ if (!existsSync(this.repoPath)) {
219
434
  throw new Error(`Entity not found: ${entityId}`);
220
435
  }
221
- const document = this.markdownService.parseFile(entityPath);
222
- const entityType = document.frontmatter.entity_type ?? this.repoName;
223
- let validatedData;
224
- try {
225
- validatedData = this.validateEntity(entityType, document.frontmatter);
226
- }
227
- catch {
228
- validatedData = this.validateEntity(entityType, document.frontmatter, true);
436
+ for (const [key] of this.getFileIndex()) {
437
+ const slash = key.indexOf('/');
438
+ if (slash === -1)
439
+ continue;
440
+ const id = key.slice(slash + 1);
441
+ if (id === entityId) {
442
+ const type = key.slice(0, slash);
443
+ return this.get(type, entityId);
444
+ }
229
445
  }
230
- return {
231
- id: entityId,
232
- path: entityPath,
233
- entityType,
234
- data: validatedData,
235
- document,
236
- };
446
+ throw new Error(`Entity not found: ${entityId}`);
237
447
  }
238
448
  /**
239
449
  * Update an existing entity
240
450
  */
241
451
  async update(options) {
242
- const { entityType, entityId, data, content, commitMessage, skipCommit, user } = options;
452
+ const { entityType, entityId, data, content, commitMessage, skipCommit, user, skipYjsSync } = options;
243
453
  // Get existing entity
244
454
  const existing = this.get(entityType, entityId);
245
- // Merge data (if provided) and add update metadata
455
+ // Merge data (if provided) and always stamp update metadata. Even
456
+ // content-only edits (editor autosave) must update `updated_by` /
457
+ // `updated_at` — otherwise the audit trail freezes at whatever the
458
+ // last data-change user was, which for scaffolded files is
459
+ // "studiograph".
460
+ const updater = (user ?? this.gitUser).id;
461
+ const now = new Date().toISOString();
246
462
  const mergedData = data
247
463
  ? {
248
464
  ...existing.data,
249
465
  ...data,
250
466
  entity_type: entityType,
251
- updated_at: new Date().toISOString(),
252
- updated_by: (user ?? this.gitUser).id,
467
+ updated_at: now,
468
+ updated_by: updater,
253
469
  }
254
- : existing.data;
470
+ : {
471
+ ...existing.data,
472
+ updated_at: now,
473
+ updated_by: updater,
474
+ };
255
475
  // Validate merged data — use lenient validation when no new data is provided,
256
476
  // since the existing entity may have been stored without all required fields.
257
477
  // Raw fallback coerces Date objects to YYYY-MM-DD so gray-matter doesn't
@@ -281,13 +501,30 @@ export class BaseGraphManager {
281
501
  }
282
502
  // Merge content (if provided)
283
503
  const mergedContent = content !== undefined ? content : existing.document.content;
284
- // Write updated markdown file
285
- const entityPath = this.getEntityPath(entityType, entityId);
504
+ // Update in place — write to wherever the entry already lives, not
505
+ // wherever a fresh getEntityPath() computation would put it. Otherwise
506
+ // an entry in a nested folder gets duplicated to the legacy plural-type
507
+ // dir on first update.
508
+ const entityPath = existing.path;
286
509
  this.markdownService.writeFile(entityPath, {
287
510
  frontmatter: validatedData,
288
511
  content: mergedContent,
289
512
  wikilinks: this.markdownService.extractWikilinks(mergedContent, validatedData),
290
513
  });
514
+ // Fire onContentUpdate SYNCHRONOUSLY before the git commit's await yield.
515
+ // This guarantees the Y.Doc is reconciled with the new .md content before
516
+ // any concurrent autosave PUT can land and overwrite it. Without the
517
+ // strict ordering, an autosave that arrived during the git commit would
518
+ // write stale Y.Text content over the just-written .md, silently losing
519
+ // server-bypass writes (MCP, agent, event processor).
520
+ if (!skipYjsSync && this.mutationCallbacks?.onContentUpdate) {
521
+ try {
522
+ this.mutationCallbacks.onContentUpdate(this.repoName, entityType, entityId);
523
+ }
524
+ catch (err) {
525
+ console.warn(`[graph] onContentUpdate callback failed for ${entityType}/${entityId}:`, err);
526
+ }
527
+ }
291
528
  if (this.gitService && !skipCommit) {
292
529
  const transaction = this.gitService.createTransaction();
293
530
  transaction.add({
@@ -301,19 +538,150 @@ export class BaseGraphManager {
301
538
  }
302
539
  // Return updated entity
303
540
  const updated = this.get(entityType, entityId);
304
- // Update vector index (fire-and-forget, non-fatal)
305
- this.vectorService?.upsert(this.repoName, updated).catch(err => console.warn(`[vector] index failed for ${entityType}/${entityId}:`, err.message));
541
+ // Update vector index (fire-and-forget, non-fatal). Tracked on the
542
+ // service so /api/health surfaces a steady failure stream instead of
543
+ // burying it in stdout warnings.
544
+ if (this.vectorService) {
545
+ const v = this.vectorService;
546
+ v.beginOp();
547
+ v.upsert(this.repoName, updated)
548
+ .then(() => v.recordSuccess())
549
+ .catch(err => {
550
+ v.recordFailure('upsert', `${entityType}/${entityId}`, err);
551
+ console.warn(`[vector] index failed for ${entityType}/${entityId}:`, err.message);
552
+ });
553
+ }
306
554
  return updated;
307
555
  }
308
556
  /**
309
557
  * Delete an entity
310
558
  */
559
+ /**
560
+ * Delete multiple entities in a single git transaction. Each entity's
561
+ * file is removed from disk; folder markers (.gitkeep) are added where
562
+ * the deletion left a folder empty. Then everything commits as one
563
+ * atomic operation.
564
+ *
565
+ * Compared to looping `delete()` per entity, this is a real performance
566
+ * primitive: one fork of git, one index update, one commit, one ref
567
+ * advance — instead of N. Bulk delete of a few hundred entities goes
568
+ * from minutes to seconds.
569
+ *
570
+ * Errors per entity (e.g. "not found") don't abort the batch — they're
571
+ * collected in `errors` and the rest still commit. Returns both the
572
+ * successfully-deleted set (for vector cleanup + WS broadcast) and the
573
+ * errors so the caller can surface partial failures.
574
+ */
575
+ async bulkDelete(specs, options) {
576
+ const deleted = [];
577
+ const errors = [];
578
+ const transaction = this.gitService?.createTransaction();
579
+ for (const { entityType, entityId } of specs) {
580
+ try {
581
+ const existing = this.get(entityType, entityId);
582
+ rmSync(existing.path, { force: true });
583
+ // Folder marker if the deletion emptied the folder — same lifecycle
584
+ // semantics as single delete().
585
+ const addedMarker = this.ensureFolderMarker(dirname(existing.path));
586
+ if (transaction) {
587
+ transaction.add({ operation: 'delete', entityType, entityId, filePath: existing.path });
588
+ if (addedMarker) {
589
+ transaction.add({ operation: 'create', entityType, entityId, filePath: addedMarker });
590
+ }
591
+ }
592
+ deleted.push({ entityType, entityId, path: existing.path });
593
+ }
594
+ catch (err) {
595
+ errors.push({
596
+ entityType,
597
+ entityId,
598
+ error: err instanceof Error ? err.message : String(err),
599
+ });
600
+ }
601
+ }
602
+ this.invalidateIndex();
603
+ if (transaction && deleted.length > 0) {
604
+ const message = options?.commitMessage
605
+ ?? `Delete ${deleted.length} ${deleted.length === 1 ? 'entry' : 'entries'} from web UI`;
606
+ await transaction.commit(options?.user ?? this.gitUser, message);
607
+ }
608
+ // Vector index cleanup, fire-and-forget (matches single delete() behavior).
609
+ // Failures recorded on VectorService for /api/health visibility.
610
+ for (const { entityType, entityId } of deleted) {
611
+ if (!this.vectorService)
612
+ continue;
613
+ const v = this.vectorService;
614
+ v.beginOp();
615
+ v.delete(this.repoName, entityType, entityId)
616
+ .then(() => v.recordSuccess())
617
+ .catch(err => {
618
+ v.recordFailure('delete', `${entityType}/${entityId}`, err);
619
+ console.warn(`[vector] delete failed for ${entityType}/${entityId}:`, err.message);
620
+ });
621
+ }
622
+ return { deleted, errors };
623
+ }
624
+ /**
625
+ * Commit a set of already-on-disk file changes as a single git
626
+ * operation. Used by callers (e.g. the import route) that perform many
627
+ * `skipCommit: true` writes and want one transaction at the end instead
628
+ * of per-file commits.
629
+ *
630
+ * `entries` describes what each file represents so the transaction can
631
+ * be staged correctly. No-op when nothing is staged or git isn't
632
+ * available.
633
+ */
634
+ async commitPending(entries, options) {
635
+ if (!this.gitService)
636
+ return { committed: false };
637
+ if (entries.length === 0 && !options.extraPaths?.length)
638
+ return { committed: false };
639
+ const transaction = this.gitService.createTransaction();
640
+ for (const e of entries)
641
+ transaction.add(e);
642
+ // Auxiliary paths (folder-display sidecars from the import flow) are
643
+ // staged alongside the entity files so they land in the same commit.
644
+ // The transaction only uses `filePath` for staging — the other fields
645
+ // are metadata, so we fill them with sentinel values.
646
+ if (options.extraPaths?.length) {
647
+ for (const filePath of options.extraPaths) {
648
+ transaction.add({
649
+ operation: 'create',
650
+ entityType: '_sidecar',
651
+ entityId: '_sidecar',
652
+ filePath,
653
+ });
654
+ }
655
+ }
656
+ await transaction.commit(options.user ?? this.gitUser, options.commitMessage);
657
+ return { committed: true };
658
+ }
311
659
  async delete(entityType, entityId, commitMessage, user) {
312
660
  // Verify entity exists
313
661
  const existing = this.get(entityType, entityId);
314
662
  // Delete the flat entity file
315
- const { rmSync } = await import('fs');
316
663
  rmSync(existing.path, { force: true });
664
+ // Asset entities own a binary blob (and optionally a PDF text sidecar).
665
+ // Both must be cleaned up — leaving them orphans the storage layer and
666
+ // poisons future entity_id reuse with a stale text dump. Fire-and-forget:
667
+ // the markdown is the system of record, so a stuck binary shouldn't
668
+ // block the delete commit. (Same approach as vector-index cleanup below.)
669
+ if (entityType === 'asset' && this.assetService) {
670
+ this.assetService.deleteAll('asset', entityId).catch(err => console.warn(`[asset] deleteAll failed for asset/${entityId}: ${err?.message ?? err}`));
671
+ if (this.workspaceRoot) {
672
+ const sidecarPath = join(this.workspaceRoot, '.studiograph', 'assets-text', 'asset', `${entityId}.txt`);
673
+ try {
674
+ rmSync(sidecarPath, { force: true });
675
+ }
676
+ catch { /* sidecar cleanup is best-effort */ }
677
+ }
678
+ }
679
+ // Folder lifecycle: deleting an entry never deletes its containing folder.
680
+ // If this was the last .md in the folder, write a .gitkeep so it stays
681
+ // visible to listFolders() (and therefore to the sidebar). A user who
682
+ // wants the folder gone calls deleteFolder() explicitly.
683
+ const addedMarker = this.ensureFolderMarker(dirname(existing.path));
684
+ this.invalidateIndex();
317
685
  if (this.gitService) {
318
686
  const transaction = this.gitService.createTransaction();
319
687
  transaction.add({
@@ -322,11 +690,42 @@ export class BaseGraphManager {
322
690
  entityId,
323
691
  filePath: existing.path,
324
692
  });
693
+ if (addedMarker) {
694
+ transaction.add({
695
+ operation: 'create',
696
+ entityType,
697
+ entityId,
698
+ filePath: addedMarker,
699
+ });
700
+ }
325
701
  const message = commitMessage || `Delete ${entityType}: ${entityId}`;
326
702
  await transaction.commit(user ?? this.gitUser, message);
327
703
  }
328
- // Remove from vector index (fire-and-forget, non-fatal)
329
- this.vectorService?.delete(this.repoName, entityType, entityId).catch(err => console.warn(`[vector] delete failed for ${entityType}/${entityId}:`, err.message));
704
+ // Remove from vector index (fire-and-forget, non-fatal). Tracked on
705
+ // the service so a leaking error stream is visible at /api/health.
706
+ if (this.vectorService) {
707
+ const v = this.vectorService;
708
+ v.beginOp();
709
+ v.delete(this.repoName, entityType, entityId)
710
+ .then(() => v.recordSuccess())
711
+ .catch(err => {
712
+ v.recordFailure('delete', `${entityType}/${entityId}`, err);
713
+ console.warn(`[vector] delete failed for ${entityType}/${entityId}:`, err.message);
714
+ });
715
+ }
716
+ // Notify Yjs persistence. Awaited (not fire-and-forget) because docName
717
+ // reuse is a real scenario: delete X then create a new entity at X. If the
718
+ // delete callback fired async, it could race the new entity's snapshot and
719
+ // unlink it after creation. Failures are logged but don't throw — orphaned
720
+ // snapshot files are a leak, not a correctness issue.
721
+ if (this.mutationCallbacks?.onDelete) {
722
+ try {
723
+ await this.mutationCallbacks.onDelete(this.repoName, entityType, entityId);
724
+ }
725
+ catch (err) {
726
+ console.warn(`[graph] onDelete callback failed for ${entityType}/${entityId}:`, err);
727
+ }
728
+ }
330
729
  }
331
730
  /**
332
731
  * Rename an entity (change its ID). Preserves all data, content, and source_ref.
@@ -334,7 +733,8 @@ export class BaseGraphManager {
334
733
  */
335
734
  async rename(entityType, oldId, newId, commitMessage, user) {
336
735
  const existing = this.get(entityType, oldId);
337
- const newPath = this.getEntityPath(entityType, newId);
736
+ // Rename keeps the entry in its current folder — only the filename changes.
737
+ const newPath = join(dirname(existing.path), `${newId}.md`);
338
738
  if (existsSync(newPath)) {
339
739
  throw new Error(`Entity ${entityType}/${newId} already exists`);
340
740
  }
@@ -348,27 +748,61 @@ export class BaseGraphManager {
348
748
  // Remove old file
349
749
  const { rmSync } = await import('fs');
350
750
  rmSync(existing.path, { force: true });
351
- // Git: stage the rename
751
+ this.invalidateIndex();
352
752
  if (this.gitService) {
353
- const { spawnSync } = await import('child_process');
354
- spawnSync('git', ['add', newPath], { cwd: this.repoPath, stdio: 'ignore' });
355
- spawnSync('git', ['add', existing.path], { cwd: this.repoPath, stdio: 'ignore' });
356
- const transaction = this.gitService.createTransaction();
357
753
  const message = commitMessage || `Rename ${entityType}: ${oldId} → ${newId}`;
358
- await transaction.commit(user ?? this.gitUser, message);
754
+ this.gitService.commitFiles([existing.path, newPath], message, user ?? this.gitUser);
755
+ }
756
+ // Update vector index — failure tracked on the service so a steady
757
+ // leak surfaces at /api/health rather than getting swallowed entirely.
758
+ if (this.vectorService) {
759
+ const v = this.vectorService;
760
+ v.beginOp();
761
+ v.delete(this.repoName, entityType, oldId)
762
+ .then(() => v.recordSuccess())
763
+ .catch(err => v.recordFailure('delete', `${entityType}/${oldId}`, err));
764
+ }
765
+ // Notify Yjs persistence to rename the snapshot file alongside. Awaited
766
+ // because rename oldId→newId followed by creating a new entity at oldId
767
+ // would otherwise let the async rename clobber the new oldId snapshot.
768
+ if (this.mutationCallbacks?.onRename) {
769
+ try {
770
+ await this.mutationCallbacks.onRename(this.repoName, entityType, oldId, newId);
771
+ }
772
+ catch (err) {
773
+ console.warn(`[graph] onRename callback failed for ${entityType}/${oldId}→${newId}:`, err);
774
+ }
359
775
  }
360
- // Update vector index
361
- this.vectorService?.delete(this.repoName, entityType, oldId).catch(() => { });
362
776
  return this.get(entityType, newId);
363
777
  }
364
778
  /**
365
- * Change an entity's type. Moves the file from oldType/ to newType/ directory.
366
- * Preserves all data and content. Updates entity_type in frontmatter.
779
+ * Change an entity's type. Updates frontmatter only the file stays in
780
+ * its current folder. In the type-agnostic folder model, type is a
781
+ * frontmatter property independent of location. (Legacy behavior moved
782
+ * the file between {plural-type}/ dirs; that's no longer correct because
783
+ * those dirs are now ordinary user-visible folders, not type markers.)
784
+ *
785
+ * If the user wants the file to also move (e.g. out of a "meetings/"
786
+ * folder when changed to a note), they invoke move() separately.
367
787
  */
368
788
  async changeType(entityId, fromType, toType, commitMessage, user) {
789
+ // Asset entities are bound to a binary in the storage backend plus
790
+ // (for PDFs) a text sidecar. Changing TO asset would leave the entity
791
+ // without an asset_url / asset_kind / blob; changing FROM asset would
792
+ // orphan the binary and sidecar. Both directions must be a
793
+ // delete-and-recreate, not a type swap.
794
+ if (toType === 'asset' || fromType === 'asset') {
795
+ const direction = toType === 'asset' ? 'to' : 'from';
796
+ throw new Error(`Cannot change_type ${direction} 'asset' — assets are bound to a binary in storage. ` +
797
+ (toType === 'asset'
798
+ ? 'Delete this entry and re-upload the file via the asset upload route to convert it to an asset.'
799
+ : 'Delete this asset entry (which removes the binary) and create a fresh entry of the target type.'));
800
+ }
369
801
  const existing = this.get(fromType, entityId);
370
- const newPath = this.getEntityPath(toType, entityId);
371
- if (existsSync(newPath)) {
802
+ // (type, id) uniqueness check — if an entry with the new type already
803
+ // exists with this id, refuse rather than silently colliding in the index.
804
+ const collision = this.findEntityFile(toType, entityId);
805
+ if (collision && collision !== existing.path) {
372
806
  throw new Error(`Entity ${toType}/${entityId} already exists`);
373
807
  }
374
808
  // Read current content
@@ -408,40 +842,330 @@ export class BaseGraphManager {
408
842
  }
409
843
  }
410
844
  }
411
- // Ensure target directory exists and write new file
412
- mkdirSync(dirname(newPath), { recursive: true });
413
- this.markdownService.writeFile(newPath, doc);
414
- // Remove old file
415
- const { rmSync } = await import('fs');
416
- rmSync(existing.path, { force: true });
417
- // Git: stage the move and commit directly
845
+ // Write back to the same path file stays in its current folder.
846
+ this.markdownService.writeFile(existing.path, doc);
847
+ this.invalidateIndex();
418
848
  if (this.gitService) {
419
- const { spawnSync } = await import('child_process');
420
- const gitUser = user ?? this.gitUser;
421
849
  const message = commitMessage || `Change type: ${entityId} (${fromType} → ${toType})`;
422
- spawnSync('git', ['add', newPath], { cwd: this.repoPath, stdio: 'ignore' });
423
- spawnSync('git', ['add', existing.path], { cwd: this.repoPath, stdio: 'ignore' });
424
- spawnSync('git', [
425
- '-c', `user.name=${gitUser.name}`,
426
- '-c', `user.email=${gitUser.email}`,
427
- 'commit', '-m', message,
428
- ], { cwd: this.repoPath, stdio: 'ignore' });
429
- }
430
- // Update vector index: remove old entry, re-index at new type
850
+ this.gitService.commitFiles([existing.path], message, user ?? this.gitUser);
851
+ }
852
+ // Update vector index: remove old entry, re-index at new type. Both ops
853
+ // tracked on the service so /api/health surfaces a leaking failure
854
+ // stream — previous .catch(() => {}) form was completely silent.
431
855
  const updated = this.get(toType, entityId);
432
- this.vectorService?.delete(this.repoName, fromType, entityId).catch(() => { });
433
- this.vectorService?.upsert(this.repoName, updated).catch(() => { });
856
+ if (this.vectorService) {
857
+ const v = this.vectorService;
858
+ v.beginOp();
859
+ v.delete(this.repoName, fromType, entityId)
860
+ .then(() => v.recordSuccess())
861
+ .catch(err => v.recordFailure('delete', `${fromType}/${entityId}`, err));
862
+ v.beginOp();
863
+ v.upsert(this.repoName, updated)
864
+ .then(() => v.recordSuccess())
865
+ .catch(err => v.recordFailure('upsert', `${toType}/${entityId}`, err));
866
+ }
867
+ // Notify Yjs persistence to move the snapshot from the old type's path to
868
+ // the new one (the docName changes, so the snapshot path changes). Awaited
869
+ // for the same reason as rename — preserves ordering when types are
870
+ // changed back and forth quickly.
871
+ if (this.mutationCallbacks?.onChangeType) {
872
+ try {
873
+ await this.mutationCallbacks.onChangeType(this.repoName, entityId, fromType, toType);
874
+ }
875
+ catch (err) {
876
+ console.warn(`[graph] onChangeType callback failed for ${entityId} (${fromType}→${toType}):`, err);
877
+ }
878
+ }
434
879
  return updated;
435
880
  }
436
881
  /**
437
- * List entities of a given type
882
+ * Move an entry to a different folder within the same repo. The filename
883
+ * (and therefore the entity id) and type are unchanged. Auto-creates parent
884
+ * folders. Refuses if the target path already has an entry with this id.
885
+ */
886
+ async move(entityType, entityId, targetFolderPath, commitMessage, user) {
887
+ const existing = this.get(entityType, entityId);
888
+ const normalized = normalizeFolderPath(targetFolderPath);
889
+ const newPath = join(this.repoPath, normalized, `${entityId}.md`);
890
+ if (newPath === existing.path)
891
+ return existing; // no-op
892
+ if (existsSync(newPath)) {
893
+ throw new Error(`Path already occupied: ${normalized}/${entityId}.md`);
894
+ }
895
+ mkdirSync(dirname(newPath), { recursive: true });
896
+ const { renameSync } = await import('fs');
897
+ renameSync(existing.path, newPath);
898
+ // Folder lifecycle: source stays addressable via .gitkeep if it lost its
899
+ // last .md; destination drops any redundant .gitkeep now that it has one.
900
+ const filesToCommit = [existing.path, newPath];
901
+ const addedSourceMarker = this.ensureFolderMarker(dirname(existing.path));
902
+ if (addedSourceMarker)
903
+ filesToCommit.push(addedSourceMarker);
904
+ const removedDestMarker = this.removeFolderMarker(dirname(newPath));
905
+ if (removedDestMarker)
906
+ filesToCommit.push(removedDestMarker);
907
+ this.invalidateIndex();
908
+ if (this.gitService) {
909
+ const message = commitMessage || `Move ${entityType}: ${entityId} → ${normalized || '(root)'}`;
910
+ this.gitService.commitFiles(filesToCommit, message, user ?? this.gitUser);
911
+ }
912
+ return this.get(entityType, entityId);
913
+ }
914
+ /**
915
+ * Restore an entity to its content at a specific commit. Single-entity
916
+ * scope only — multi-entity rollback (e.g. "undo this commit's effects
917
+ * across all touched files") is a separate, more cautious flow that
918
+ * isn't built yet.
919
+ *
920
+ * Two paths through the method:
921
+ * - **Overwrite**: the entity exists today at path `P`. Read content
922
+ * from `git show <hash>:<P>` and write it back to `P`.
923
+ * - **Recreate**: the entity is gone today. Walk the historical
924
+ * commit's tree to find the .md file whose basename matches
925
+ * `entityId` AND whose frontmatter `entity_type` matches
926
+ * `entityType`. That path is the restore target. The walk only
927
+ * fires when the entity is truly absent — overwrites stay cheap.
928
+ *
929
+ * Throws a `PathConflictError` if the recreate target path is now
930
+ * occupied by a DIFFERENT entity (different type+id) — recreating
931
+ * over it would silently overwrite unrelated work. Caller surfaces
932
+ * the conflict to the user; resolution is "move or delete the
933
+ * occupant first."
934
+ */
935
+ async restoreFile(entityType, entityId, commitHash, user) {
936
+ if (!this.gitService) {
937
+ throw new Error('Cannot restore — git service is not available for this repo');
938
+ }
939
+ const existingPath = this.findEntityFile(entityType, entityId);
940
+ let targetPath;
941
+ let action;
942
+ let content;
943
+ if (existingPath) {
944
+ // Overwrite path. Read content at the historical commit using the
945
+ // CURRENT path (git tracks files by content + path; if the entity
946
+ // was renamed or moved across the file's lifetime, --follow would
947
+ // be needed — out of scope for v1, the user can resolve by
948
+ // restoring at a commit where the path matched).
949
+ const relPath = relative(this.repoPath, existingPath);
950
+ content = this.gitService.getFileAtCommit(relPath, commitHash);
951
+ if (content === null) {
952
+ throw new Error(`File ${relPath} did not exist at commit ${commitHash}`);
953
+ }
954
+ targetPath = existingPath;
955
+ action = 'overwrite';
956
+ }
957
+ else {
958
+ // Recreate path. Walk the historical commit's tree to find the
959
+ // file whose basename matches entityId and frontmatter type
960
+ // matches entityType. This is the only place we do a full
961
+ // tree-walk — it only runs when the entity is gone today.
962
+ const found = this.findEntityInCommit(entityType, entityId, commitHash);
963
+ if (!found) {
964
+ throw new Error(`Entity ${entityType}/${entityId} not found at commit ${commitHash}`);
965
+ }
966
+ targetPath = join(this.repoPath, found.path);
967
+ content = found.content;
968
+ action = 'recreate';
969
+ // Path-conflict check: nothing else may occupy targetPath today.
970
+ if (existsSync(targetPath)) {
971
+ const occupant = this.identifyEntityAtPath(targetPath);
972
+ if (occupant && (occupant.entityType !== entityType || occupant.entityId !== entityId)) {
973
+ throw new PathConflictError(found.path, occupant);
974
+ }
975
+ }
976
+ }
977
+ mkdirSync(dirname(targetPath), { recursive: true });
978
+ writeFileSync(targetPath, content);
979
+ // Folder lifecycle: if recreating dropped a file into a directory
980
+ // that had been emptied (and now carries .gitkeep), drop the marker
981
+ // so the directory exists via the .md file alone.
982
+ const filesToCommit = [targetPath];
983
+ if (action === 'recreate') {
984
+ const removedDestMarker = this.removeFolderMarker(dirname(targetPath));
985
+ if (removedDestMarker)
986
+ filesToCommit.push(removedDestMarker);
987
+ }
988
+ this.invalidateIndex();
989
+ const shortHash = commitHash.length > 8 ? commitHash.slice(0, 8) : commitHash;
990
+ const message = `Restore ${entityType}/${entityId} from ${shortHash}`;
991
+ this.gitService.commitFiles(filesToCommit, message, user ?? this.gitUser);
992
+ return { action, path: targetPath };
993
+ }
994
+ /**
995
+ * Walk a commit's tree looking for a .md file whose basename matches
996
+ * `entityId` AND whose frontmatter `entity_type` matches `entityType`.
997
+ * Returns repo-relative path + content when found, otherwise null.
998
+ * Used by `restoreFile` for the recreate-from-deletion case.
999
+ */
1000
+ findEntityInCommit(entityType, entityId, commitHash) {
1001
+ if (!this.gitService)
1002
+ return null;
1003
+ const filenameLower = `${entityId}.md`.toLowerCase();
1004
+ const tree = this.gitService.listFilesAtCommit(commitHash);
1005
+ for (const path of tree) {
1006
+ if (!path.toLowerCase().endsWith(filenameLower))
1007
+ continue;
1008
+ // basename match — read content and check frontmatter type.
1009
+ const content = this.gitService.getFileAtCommit(path, commitHash);
1010
+ if (content === null)
1011
+ continue;
1012
+ try {
1013
+ const doc = this.markdownService.parseContent(content);
1014
+ const fmType = doc.frontmatter.entity_type;
1015
+ const inferred = this.inferLegacyType(join(this.repoPath, path));
1016
+ const type = fmType || inferred;
1017
+ if (type === entityType)
1018
+ return { path, content };
1019
+ }
1020
+ catch {
1021
+ // unparsable file — skip
1022
+ }
1023
+ }
1024
+ return null;
1025
+ }
1026
+ /**
1027
+ * Identify the entity (if any) currently occupying `absPath`. Returns
1028
+ * null when the path holds something that isn't an entity (README, etc).
1029
+ * Mirrors the inference rules used in `getFileIndex`.
1030
+ */
1031
+ identifyEntityAtPath(absPath) {
1032
+ try {
1033
+ const doc = this.markdownService.parseFile(absPath);
1034
+ const fmType = doc.frontmatter.entity_type;
1035
+ const inferred = this.inferLegacyType(absPath);
1036
+ const type = fmType || inferred;
1037
+ if (!type)
1038
+ return null;
1039
+ const id = absPath.split('/').pop().replace(/\.md$/, '');
1040
+ return { entityType: type, entityId: id };
1041
+ }
1042
+ catch {
1043
+ return null;
1044
+ }
1045
+ }
1046
+ /**
1047
+ * Enumerate the distinct entity types present in this repo. Used by
1048
+ * /api/repos to let the sidebar render the Views list without a full
1049
+ * entity fetch per folder.
1050
+ *
1051
+ * Derived from the file index (which reads frontmatter), so it works for
1052
+ * entries in any folder — not just the legacy {plural-type}/ dirs.
1053
+ */
1054
+ listEntityTypes() {
1055
+ if (!existsSync(this.repoPath))
1056
+ return [];
1057
+ const types = new Set();
1058
+ for (const key of this.getFileIndex().keys()) {
1059
+ const slash = key.indexOf('/');
1060
+ if (slash > 0)
1061
+ types.add(key.slice(0, slash));
1062
+ }
1063
+ return [...types].sort();
1064
+ }
1065
+ /**
1066
+ * Folder lifecycle: a directory is treated as a first-class persistent
1067
+ * object. It exists from the moment it's created (createFolder, the destination
1068
+ * of a moved entry, etc.) until it's explicitly removed via a folder-level
1069
+ * operation (deleteFolder, renameFolder, moveFolderToRepo). Entry-level
1070
+ * operations (create, delete, move, transfer) never decide whether a folder
1071
+ * lives or dies.
1072
+ *
1073
+ * `listFolders()` requires either a .md child or a .gitkeep marker for a
1074
+ * directory to surface in the API, so the two helpers below are the single
1075
+ * place that adds and removes the marker:
1076
+ *
1077
+ * - `ensureFolderMarker` — call after an op that may have left `dir`
1078
+ * without .md children. Writes `.gitkeep` and returns its path so the
1079
+ * caller can stage it in the same commit.
1080
+ * - `removeFolderMarker` — call after an op that placed an .md into `dir`.
1081
+ * Drops a now-redundant `.gitkeep` so the repo stays tidy.
1082
+ *
1083
+ * Both no-op for the repo root (always implicit) and for paths that don't
1084
+ * exist. Both return `null` when nothing changed.
1085
+ */
1086
+ ensureFolderMarker(dir) {
1087
+ if (dir === this.repoPath)
1088
+ return null;
1089
+ if (!existsSync(dir))
1090
+ return null;
1091
+ const entries = readdirSync(dir);
1092
+ const hasMd = entries.some(e => e.endsWith('.md') && !e.startsWith('.'));
1093
+ if (hasMd)
1094
+ return null;
1095
+ const keepPath = join(dir, '.gitkeep');
1096
+ if (existsSync(keepPath))
1097
+ return null;
1098
+ writeFileSync(keepPath, '');
1099
+ return keepPath;
1100
+ }
1101
+ removeFolderMarker(dir) {
1102
+ if (dir === this.repoPath)
1103
+ return null;
1104
+ if (!existsSync(dir))
1105
+ return null;
1106
+ const keepPath = join(dir, '.gitkeep');
1107
+ if (!existsSync(keepPath))
1108
+ return null;
1109
+ const entries = readdirSync(dir);
1110
+ const hasMd = entries.some(e => e.endsWith('.md') && !e.startsWith('.'));
1111
+ if (!hasMd)
1112
+ return null; // marker still load-bearing
1113
+ rmSync(keepPath, { force: true });
1114
+ return keepPath;
1115
+ }
1116
+ /**
1117
+ * Walk up from `dir`, removing each directory that became empty after a
1118
+ * folder-level removal (deleteFolder, renameFolder, moveFolderToRepo).
1119
+ * Stops at the repo root. Silently no-ops if a directory is missing,
1120
+ * non-empty, or can't be removed.
1121
+ *
1122
+ * NOT called by entry-level operations (create/delete/move/transfer) —
1123
+ * they preserve folder structure regardless of whether they emptied a
1124
+ * directory. Use `ensureFolderMarker` instead for those paths.
1125
+ */
1126
+ async pruneEmptyAncestors(dir) {
1127
+ const { rmSync } = await import('fs');
1128
+ const repoRoot = this.repoPath.replace(/\/+$/, '');
1129
+ let current = dir;
1130
+ while (current.startsWith(repoRoot + '/') && current !== repoRoot) {
1131
+ if (!existsSync(current)) {
1132
+ current = dirname(current);
1133
+ continue;
1134
+ }
1135
+ let entries;
1136
+ try {
1137
+ entries = readdirSync(current);
1138
+ }
1139
+ catch {
1140
+ return;
1141
+ }
1142
+ // Treat .gitkeep as "this folder is intentionally kept" — don't auto-prune.
1143
+ if (entries.length > 0)
1144
+ return;
1145
+ try {
1146
+ rmSync(current, { recursive: true, force: true });
1147
+ }
1148
+ catch {
1149
+ return;
1150
+ }
1151
+ current = dirname(current);
1152
+ }
1153
+ }
1154
+ /**
1155
+ * List entities of a given type (or all types if not specified).
1156
+ *
1157
+ * Walks the entire repo tree so entries in nested folders are discovered.
1158
+ * The type filter is applied against frontmatter `entity_type`; legacy
1159
+ * entries without explicit frontmatter type fall back to the depth-1
1160
+ * plural-type directory name (so `meetings/foo.md` still resolves as a
1161
+ * meeting). Files with no inferable type (README.md, CONTRIBUTING.md, etc.)
1162
+ * are skipped.
438
1163
  */
439
1164
  list(entityType, limit) {
440
1165
  if (!existsSync(this.repoPath))
441
1166
  return [];
442
1167
  const results = [];
443
- // Lenient validation for list(): try strict schema first, fall back to
444
- // EntityBaseSchema, then raw data so app-defined types are still discovered.
1168
+ const seen = new Set(); // "type/id" dedup if a file ends up indexed twice
445
1169
  const validateLenient = (type, data) => {
446
1170
  try {
447
1171
  return this.validateEntity(type, data);
@@ -455,97 +1179,36 @@ export class BaseGraphManager {
455
1179
  }
456
1180
  }
457
1181
  };
458
- const seenIds = new Set();
459
- // Process a flat .md file as an entity
460
- const tryAddFile = (filePath, id, inferredType) => {
461
- if (seenIds.has(id))
462
- return;
463
- try {
464
- const doc = this.markdownService.parseFile(filePath);
465
- const fmType = doc.frontmatter.entity_type;
466
- const resolvedType = entityType ?? fmType ?? inferredType ?? this.repoName;
467
- if (entityType && fmType && fmType !== entityType)
468
- return;
469
- const validatedData = validateLenient(resolvedType, doc.frontmatter);
470
- seenIds.add(id);
471
- results.push({
472
- id,
473
- path: filePath,
474
- entityType: resolvedType,
475
- data: validatedData,
476
- document: doc,
477
- });
478
- }
479
- catch { /* skip invalid */ }
480
- };
481
- // Scan plural type subfolders for flat .md files
482
- for (const entry of readdirSync(this.repoPath)) {
483
- if (entry.startsWith('.'))
484
- continue;
485
- const fullPath = join(this.repoPath, entry);
1182
+ this.walkMarkdownFiles((filePath) => {
1183
+ let doc;
486
1184
  try {
487
- if (!statSync(fullPath).isDirectory())
488
- continue;
1185
+ doc = this.markdownService.parseFile(filePath);
489
1186
  }
490
1187
  catch {
491
- continue;
492
- }
493
- const singularType = PLURAL_TO_SINGULAR[entry];
494
- if (!singularType)
495
- continue; // skip unknown directories
496
- try {
497
- for (const file of readdirSync(fullPath)) {
498
- if (file.startsWith('.') || !file.endsWith('.md'))
499
- continue;
500
- const filePath = join(fullPath, file);
501
- try {
502
- if (!statSync(filePath).isFile())
503
- continue;
504
- }
505
- catch {
506
- continue;
507
- }
508
- const id = file.replace(/\.md$/, '');
509
- tryAddFile(filePath, id, singularType);
510
- }
511
- }
512
- catch { /* skip unreadable */ }
513
- }
514
- // Also discover flat .md files at repo root (README, etc.)
515
- for (const entry of readdirSync(this.repoPath)) {
516
- if (entry.startsWith('.'))
517
- continue;
518
- if (!entry.endsWith('.md'))
519
- continue;
520
- const fullPath = join(this.repoPath, entry);
521
- try {
522
- if (!statSync(fullPath).isFile())
523
- continue;
524
- }
525
- catch {
526
- continue;
527
- }
528
- const id = entry.replace(/\.md$/, '');
529
- if (seenIds.has(id))
530
- continue;
531
- try {
532
- const doc = this.markdownService.parseFile(fullPath);
533
- const fmType = doc.frontmatter.entity_type;
534
- const resolvedType = entityType ?? fmType ?? this.repoName;
535
- if (entityType && fmType && fmType !== entityType)
536
- continue;
537
- const validatedData = validateLenient(resolvedType, doc.frontmatter);
538
- seenIds.add(id);
539
- results.push({
540
- id,
541
- path: fullPath,
542
- entityType: resolvedType,
543
- data: validatedData,
544
- document: doc,
545
- });
1188
+ return;
546
1189
  }
547
- catch { /* skip invalid */ }
548
- }
1190
+ const fmType = doc.frontmatter.entity_type;
1191
+ const inferred = this.inferLegacyType(filePath);
1192
+ const resolvedType = (fmType ?? inferred);
1193
+ if (!resolvedType)
1194
+ return; // README, etc.
1195
+ if (entityType && resolvedType !== entityType)
1196
+ return;
1197
+ const id = filePath.split('/').pop().replace(/\.md$/, '');
1198
+ const key = `${resolvedType}/${id}`;
1199
+ if (seen.has(key))
1200
+ return;
1201
+ seen.add(key);
1202
+ const validatedData = validateLenient(resolvedType, doc.frontmatter);
1203
+ results.push({
1204
+ id,
1205
+ path: filePath,
1206
+ folder_path: folderPathFromFile(filePath, this.repoPath),
1207
+ entityType: resolvedType,
1208
+ data: validatedData,
1209
+ document: doc,
1210
+ });
1211
+ });
549
1212
  if (limit && limit > 0)
550
1213
  return results.slice(0, limit);
551
1214
  return results;
@@ -709,31 +1372,43 @@ export class BaseGraphManager {
709
1372
  }
710
1373
  return entities;
711
1374
  }
1375
+ /**
1376
+ * Resolve a markdown file (anywhere under the repo) to its (type, id),
1377
+ * tolerating both legacy {plural-type}/{id}.md and nested-folder layouts.
1378
+ * Returns undefined for files without a recognizable entity type.
1379
+ */
1380
+ resolveFileToEntity(absPath) {
1381
+ const filename = absPath.split('/').pop();
1382
+ if (!filename || !filename.endsWith('.md'))
1383
+ return undefined;
1384
+ const id = filename.replace(/\.md$/, '');
1385
+ let fmType;
1386
+ try {
1387
+ fmType = this.markdownService.parseFile(absPath).frontmatter.entity_type;
1388
+ }
1389
+ catch { /* unparsable — fall back to inference */ }
1390
+ const type = fmType || this.inferLegacyType(absPath);
1391
+ if (!type)
1392
+ return undefined;
1393
+ return { type: type, id };
1394
+ }
712
1395
  /**
713
1396
  * Get entities related to a given entity (via wikilinks)
714
1397
  */
715
1398
  getRelated(entityType, entityId) {
716
1399
  const entity = this.get(entityType, entityId);
717
1400
  const related = [];
718
- // Find all wikilinked entities
719
1401
  for (const link of entity.document.wikilinks) {
720
- // Try to find the linked entity in all type subfolders
721
1402
  const found = this.markdownService.findEntity(link, [this.repoPath]);
722
- if (found) {
723
- const relPath = found.replace(this.repoPath + '/', '');
724
- const parts = relPath.split('/');
725
- // New layout: {plural-type}/{entityId}.md
726
- if (parts.length === 2 && parts[1].endsWith('.md')) {
727
- const linkedId = parts[1].replace(/\.md$/, '');
728
- try {
729
- const doc = this.markdownService.parseFile(found);
730
- const linkedType = doc.frontmatter.entity_type ?? PLURAL_TO_SINGULAR[parts[0]] ?? this.repoName;
731
- const linkedEntity = this.get(linkedType, linkedId);
732
- related.push(linkedEntity);
733
- }
734
- catch { /* skip invalid */ }
735
- }
1403
+ if (!found)
1404
+ continue;
1405
+ const resolved = this.resolveFileToEntity(found);
1406
+ if (!resolved)
1407
+ continue;
1408
+ try {
1409
+ related.push(this.get(resolved.type, resolved.id));
736
1410
  }
1411
+ catch { /* skip invalid */ }
737
1412
  }
738
1413
  return related;
739
1414
  }
@@ -744,19 +1419,13 @@ export class BaseGraphManager {
744
1419
  const backlinks = this.markdownService.findBacklinks(entityId, [this.repoPath]);
745
1420
  const results = [];
746
1421
  for (const { file } of backlinks) {
747
- const relPath = file.replace(this.repoPath + '/', '');
748
- const parts = relPath.split('/');
749
- // New layout: {plural-type}/{entityId}.md
750
- if (parts.length === 2 && parts[1].endsWith('.md')) {
751
- const linkedId = parts[1].replace(/\.md$/, '');
752
- try {
753
- const doc = this.markdownService.parseFile(file);
754
- const linkedType = doc.frontmatter.entity_type ?? PLURAL_TO_SINGULAR[parts[0]] ?? this.repoName;
755
- const linkedEntity = this.get(linkedType, linkedId);
756
- results.push(linkedEntity);
757
- }
758
- catch { /* skip invalid */ }
1422
+ const resolved = this.resolveFileToEntity(file);
1423
+ if (!resolved)
1424
+ continue;
1425
+ try {
1426
+ results.push(this.get(resolved.type, resolved.id));
759
1427
  }
1428
+ catch { /* skip invalid */ }
760
1429
  }
761
1430
  return results;
762
1431
  }
@@ -771,26 +1440,20 @@ export class BaseGraphManager {
771
1440
  * Refactor a wikilink (rename entity and update all references)
772
1441
  */
773
1442
  async refactorWikilink(oldId, newId, commitMessage) {
774
- // Find all files that reference the old entity ID
775
1443
  const backlinks = this.markdownService.findBacklinks(oldId, [this.repoPath]);
776
- // Update each file
777
1444
  if (this.gitService) {
778
1445
  const transaction = this.gitService.createTransaction();
779
1446
  for (const { file } of backlinks) {
780
1447
  this.markdownService.updateWikilinks(file, oldId, newId);
781
- const relPath = file.replace(this.repoPath + '/', '');
782
- const parts = relPath.split('/');
783
- // New layout: {plural-type}/{entityId}.md
784
- if (parts.length === 2 && parts[1].endsWith('.md')) {
785
- const entityId = parts[1].replace(/\.md$/, '');
786
- const doc = this.markdownService.parseFile(file);
787
- transaction.add({
788
- operation: 'update',
789
- entityType: doc.frontmatter.entity_type ?? PLURAL_TO_SINGULAR[parts[0]] ?? parts[0],
790
- entityId,
791
- filePath: file,
792
- });
793
- }
1448
+ const resolved = this.resolveFileToEntity(file);
1449
+ if (!resolved)
1450
+ continue;
1451
+ transaction.add({
1452
+ operation: 'update',
1453
+ entityType: resolved.type,
1454
+ entityId: resolved.id,
1455
+ filePath: file,
1456
+ });
794
1457
  }
795
1458
  const message = commitMessage || `Refactor wikilink: ${oldId} → ${newId}`;
796
1459
  await transaction.commit(this.gitUser, message);
@@ -802,6 +1465,491 @@ export class BaseGraphManager {
802
1465
  }
803
1466
  return { updated: backlinks.length };
804
1467
  }
1468
+ // ────────────────────────────────────────────────────────────────────────────
1469
+ // Folder CRUD — operate on directories within the repo, not on entries.
1470
+ // Sharing remains a repo-level concept; folders carry no permissions or
1471
+ // metadata of their own. Empty folders are kept around via .gitkeep so git
1472
+ // tracks them.
1473
+ // ────────────────────────────────────────────────────────────────────────────
1474
+ /**
1475
+ * List every non-empty folder in this repo, plus folders explicitly kept
1476
+ * via .gitkeep. Returns repo-relative POSIX paths, sorted, deduplicated.
1477
+ * The repo root ("") is intentionally omitted — it's implicit.
1478
+ */
1479
+ listFolders() {
1480
+ if (!existsSync(this.repoPath))
1481
+ return [];
1482
+ const folders = new Set();
1483
+ const sidecarByPath = new Map();
1484
+ const stack = [this.repoPath];
1485
+ const repoRoot = this.repoPath.replace(/\/+$/, '');
1486
+ while (stack.length > 0) {
1487
+ const dir = stack.pop();
1488
+ let entries;
1489
+ try {
1490
+ entries = readdirSync(dir);
1491
+ }
1492
+ catch {
1493
+ continue;
1494
+ }
1495
+ let kept = false;
1496
+ let hasSidecar = false;
1497
+ for (const entry of entries) {
1498
+ if (entry === '.gitkeep')
1499
+ kept = true;
1500
+ if (entry === FOLDER_SIDECAR)
1501
+ hasSidecar = true;
1502
+ if (entry.startsWith('.'))
1503
+ continue;
1504
+ if (WALK_IGNORE.has(entry))
1505
+ continue;
1506
+ const full = join(dir, entry);
1507
+ let st;
1508
+ try {
1509
+ st = statSync(full);
1510
+ }
1511
+ catch {
1512
+ continue;
1513
+ }
1514
+ if (st.isDirectory())
1515
+ stack.push(full);
1516
+ }
1517
+ if (dir === repoRoot)
1518
+ continue;
1519
+ // Include this directory if it has any .md files OR a .gitkeep marker
1520
+ // OR a sidecar (which carries display metadata even for empty folders).
1521
+ const hasMarkdown = entries.some(e => e.endsWith('.md') && !e.startsWith('.'));
1522
+ if (hasMarkdown || kept || hasSidecar) {
1523
+ const folderPath = folderPathFromFile(join(dir, 'placeholder.md'), this.repoPath);
1524
+ folders.add(folderPath);
1525
+ if (hasSidecar) {
1526
+ const display = readFolderSidecar(dir);
1527
+ if (display !== null)
1528
+ sidecarByPath.set(folderPath, display);
1529
+ }
1530
+ }
1531
+ }
1532
+ return [...folders]
1533
+ .filter(p => p.length > 0)
1534
+ .sort()
1535
+ .map(p => {
1536
+ const entry = { path: p, name: p.slice(p.lastIndexOf('/') + 1) };
1537
+ const display = sidecarByPath.get(p);
1538
+ if (display)
1539
+ entry.display_name = display;
1540
+ return entry;
1541
+ });
1542
+ }
1543
+ /**
1544
+ * Read the display_name override for a single folder, or null if no sidecar
1545
+ * exists / is unreadable. Used by mutation paths that need the current
1546
+ * override before deciding whether to rewrite it.
1547
+ */
1548
+ readFolderDisplayName(folderPath) {
1549
+ const normalized = validateFolderPath(folderPath);
1550
+ if (!normalized)
1551
+ return null;
1552
+ return readFolderSidecar(join(this.repoPath, normalized));
1553
+ }
1554
+ /**
1555
+ * Write or remove the per-folder display_name sidecar. Pass `null` /
1556
+ * empty-string to remove (returns to slug fallback). Returns the absolute
1557
+ * path of the sidecar (for git staging) when one was written or removed,
1558
+ * or null when no change was needed (idempotent).
1559
+ *
1560
+ * Caller is responsible for committing — the helper only touches the
1561
+ * filesystem. Folder must already exist; this never creates one.
1562
+ */
1563
+ writeFolderDisplayName(folderPath, displayName) {
1564
+ const normalized = validateFolderPath(folderPath);
1565
+ if (!normalized) {
1566
+ throw new InvalidFolderPathError('Folder path cannot be empty');
1567
+ }
1568
+ const abs = join(this.repoPath, normalized);
1569
+ if (!existsSync(abs)) {
1570
+ throw new Error(`Folder not found: ${normalized}`);
1571
+ }
1572
+ const trimmed = displayName?.trim();
1573
+ if (!trimmed) {
1574
+ const removed = removeFolderSidecar(abs);
1575
+ if (!removed)
1576
+ return null;
1577
+ this.invalidateIndex();
1578
+ return removed;
1579
+ }
1580
+ const existing = readFolderSidecar(abs);
1581
+ if (existing === trimmed)
1582
+ return null;
1583
+ const sidecarPath = writeFolderSidecar(abs, trimmed);
1584
+ this.invalidateIndex();
1585
+ return sidecarPath;
1586
+ }
1587
+ /**
1588
+ * Bulk-write display_name sidecars for many folders at once. Used by the
1589
+ * import path to record original-casing overrides for every nested
1590
+ * directory it slugified. First-writer-wins: existing sidecars are NOT
1591
+ * overwritten, so a second import doesn't clobber a user's manual rename.
1592
+ *
1593
+ * Returns the list of sidecar paths actually written (for git staging).
1594
+ * Skipped paths (folder missing, slug already matches display, sidecar
1595
+ * already exists) are quietly omitted.
1596
+ */
1597
+ recordFolderDisplays(entries) {
1598
+ const written = [];
1599
+ for (const entry of entries) {
1600
+ const normalized = validateFolderPath(entry.path);
1601
+ if (!normalized)
1602
+ continue;
1603
+ const abs = join(this.repoPath, normalized);
1604
+ if (!existsSync(abs))
1605
+ continue;
1606
+ const sidecarPath = folderSidecarPath(abs);
1607
+ if (existsSync(sidecarPath))
1608
+ continue; // first-writer-wins
1609
+ const display = entry.display_name.trim();
1610
+ if (!display)
1611
+ continue;
1612
+ written.push(writeFolderSidecar(abs, display));
1613
+ }
1614
+ if (written.length > 0)
1615
+ this.invalidateIndex();
1616
+ return written;
1617
+ }
1618
+ /**
1619
+ * Create a folder at the given path (mkdir -p semantics) and drop a
1620
+ * .gitkeep so git tracks it even when empty. Validates the path. No-op if
1621
+ * the folder already exists; .gitkeep is added if missing.
1622
+ */
1623
+ async createFolder(folderPath, commitMessage, user, options) {
1624
+ const normalized = validateFolderPath(folderPath);
1625
+ if (!normalized) {
1626
+ throw new InvalidFolderPathError('Folder path cannot be empty (use the repo root directly)');
1627
+ }
1628
+ const abs = join(this.repoPath, normalized);
1629
+ mkdirSync(abs, { recursive: true });
1630
+ const keep = join(abs, '.gitkeep');
1631
+ if (!existsSync(keep)) {
1632
+ writeFileSync(keep, '');
1633
+ }
1634
+ // Sidecar is written here (not after the commit) so it joins the same
1635
+ // commit as .gitkeep — folder creation is atomic from git's perspective.
1636
+ const sidecarPath = options?.displayName
1637
+ ? this.writeFolderDisplayName(normalized, options.displayName)
1638
+ : null;
1639
+ this.invalidateIndex();
1640
+ if (this.gitService) {
1641
+ const message = commitMessage || `Create folder: ${normalized}`;
1642
+ const files = sidecarPath ? [keep, sidecarPath] : [keep];
1643
+ this.gitService.commitFiles(files, message, user ?? this.gitUser);
1644
+ }
1645
+ }
1646
+ /**
1647
+ * Rename / move a folder within the repo. Moves the entire subtree
1648
+ * atomically via filesystem rename. Refuses if the target path is already
1649
+ * occupied.
1650
+ */
1651
+ async renameFolder(fromPath, toPath, commitMessage, user, options) {
1652
+ const fromNormalized = validateFolderPath(fromPath);
1653
+ const toNormalized = validateFolderPath(toPath);
1654
+ if (!fromNormalized)
1655
+ throw new InvalidFolderPathError('Source folder path required');
1656
+ if (!toNormalized)
1657
+ throw new InvalidFolderPathError('Target folder path required');
1658
+ // Display-only update: same path, just rewrite the sidecar.
1659
+ if (fromNormalized === toNormalized) {
1660
+ if (options?.displayName === undefined)
1661
+ return;
1662
+ const sidecarPath = this.writeFolderDisplayName(fromNormalized, options.displayName);
1663
+ if (sidecarPath && this.gitService) {
1664
+ const message = commitMessage || `Update folder display name: ${fromNormalized}`;
1665
+ this.gitService.commitFiles([sidecarPath], message, user ?? this.gitUser);
1666
+ }
1667
+ return;
1668
+ }
1669
+ const fromAbs = join(this.repoPath, fromNormalized);
1670
+ const toAbs = join(this.repoPath, toNormalized);
1671
+ if (!existsSync(fromAbs)) {
1672
+ throw new Error(`Folder not found: ${fromNormalized}`);
1673
+ }
1674
+ if (existsSync(toAbs)) {
1675
+ throw new Error(`Target folder already exists: ${toNormalized}`);
1676
+ }
1677
+ mkdirSync(dirname(toAbs), { recursive: true });
1678
+ const { renameSync } = await import('fs');
1679
+ renameSync(fromAbs, toAbs);
1680
+ // The sidecar moved with the folder via FS rename. Update it now if the
1681
+ // caller passed a new display name (or `null` to clear). Idempotent — no
1682
+ // sidecar write happens when display matches the existing override.
1683
+ let sidecarPath = null;
1684
+ if (options?.displayName !== undefined) {
1685
+ sidecarPath = this.writeFolderDisplayName(toNormalized, options.displayName);
1686
+ }
1687
+ this.invalidateIndex();
1688
+ if (this.gitService) {
1689
+ const message = commitMessage || `Rename folder: ${fromNormalized} → ${toNormalized}`;
1690
+ const files = sidecarPath ? [fromAbs, toAbs, sidecarPath] : [fromAbs, toAbs];
1691
+ this.gitService.commitFiles(files, message, user ?? this.gitUser);
1692
+ }
1693
+ await this.pruneEmptyAncestors(dirname(fromAbs));
1694
+ }
1695
+ /**
1696
+ * Move a folder (and its full subtree) into a different repo at `targetFolderPath`.
1697
+ *
1698
+ * The naive way to do this is to iterate the source's entries and call
1699
+ * `transfer` per entry — that's what the web UI used to do, and it costs
1700
+ * 2 git commits per entry (1600 commits for 800 files), each blocking the
1701
+ * Node event loop via `execSync`. This method performs the move as a
1702
+ * single FS-level rename + one commit per repo, regardless of how many
1703
+ * files are inside.
1704
+ *
1705
+ * Returns the count of entries + subfolders moved so the caller can
1706
+ * surface progress.
1707
+ */
1708
+ async moveFolderToRepo(sourceFolderPath, target, targetFolderPath, options) {
1709
+ const sourceNormalized = validateFolderPath(sourceFolderPath);
1710
+ if (!sourceNormalized) {
1711
+ throw new InvalidFolderPathError('Source folder path required');
1712
+ }
1713
+ const targetNormalized = targetFolderPath ? validateFolderPath(targetFolderPath) : '';
1714
+ if (targetFolderPath && !targetNormalized) {
1715
+ throw new InvalidFolderPathError('Invalid target folder path');
1716
+ }
1717
+ if (target === this && sourceNormalized === targetNormalized) {
1718
+ return { entriesDeleted: 0, subfoldersDeleted: 0 };
1719
+ }
1720
+ const sourceAbs = join(this.repoPath, sourceNormalized);
1721
+ if (!existsSync(sourceAbs)) {
1722
+ throw new Error(`Source folder not found: ${sourceNormalized}`);
1723
+ }
1724
+ const targetAbs = targetNormalized
1725
+ ? join(target.repoPath, targetNormalized)
1726
+ : target.repoPath;
1727
+ // Refuse if target already has visible content — caller must move into
1728
+ // an empty / nonexistent folder. (Hidden files like .studiograph are OK.)
1729
+ if (existsSync(targetAbs)) {
1730
+ const visible = readdirSync(targetAbs).filter(n => !n.startsWith('.'));
1731
+ if (visible.length > 0) {
1732
+ throw new Error(`Target folder is not empty: ${targetNormalized || '/'}`);
1733
+ }
1734
+ }
1735
+ const stats = this.countFolderContents(sourceAbs);
1736
+ // Atomic FS rename if possible. Falls back to copy + delete if the two
1737
+ // repos sit on different filesystems (rare, but possible on Docker
1738
+ // bind mounts).
1739
+ const { renameSync, cpSync, rmSync } = await import('fs');
1740
+ mkdirSync(dirname(targetAbs), { recursive: true });
1741
+ if (existsSync(targetAbs)) {
1742
+ // Empty pre-existing dir — drop it so renameSync can take its place.
1743
+ rmSync(targetAbs, { recursive: true, force: true });
1744
+ }
1745
+ try {
1746
+ renameSync(sourceAbs, targetAbs);
1747
+ }
1748
+ catch (err) {
1749
+ if (err?.code === 'EXDEV') {
1750
+ cpSync(sourceAbs, targetAbs, { recursive: true });
1751
+ rmSync(sourceAbs, { recursive: true, force: true });
1752
+ }
1753
+ else {
1754
+ throw err;
1755
+ }
1756
+ }
1757
+ this.invalidateIndex();
1758
+ target.invalidateIndex();
1759
+ const user = options?.user ?? this.gitUser;
1760
+ const sourceMessage = options?.commitMessage
1761
+ ?? `Move folder ${sourceNormalized} → ${target.repoName}:${targetNormalized || '/'}`;
1762
+ const targetMessage = options?.commitMessage
1763
+ ?? `Move folder from ${this.repoName}:${sourceNormalized} → ${targetNormalized || '/'}`;
1764
+ if (this.gitService) {
1765
+ this.gitService.commitFiles([sourceAbs], sourceMessage, user);
1766
+ }
1767
+ if (target.gitService) {
1768
+ target.gitService.commitFiles([targetAbs], targetMessage, user);
1769
+ }
1770
+ await this.pruneEmptyAncestors(dirname(sourceAbs));
1771
+ return stats;
1772
+ }
1773
+ /**
1774
+ * Move a batch of entries (potentially across many source folders) into a
1775
+ * single target folder, optionally in a different repo. The web UI's
1776
+ * multi-select bulk move used to call /move or /transfer per entry — that
1777
+ * was 1-2 sync git commits per entry, blocking the event loop on each one.
1778
+ * This collapses the operation to a single FS rename per file plus one
1779
+ * commit on each repo touched.
1780
+ */
1781
+ async moveEntitiesToRepo(entries, target, targetFolderPath, options) {
1782
+ if (entries.length === 0)
1783
+ return { moved: 0 };
1784
+ const targetNormalized = targetFolderPath ? validateFolderPath(targetFolderPath) : '';
1785
+ if (targetFolderPath && !targetNormalized) {
1786
+ throw new InvalidFolderPathError('Invalid target folder path');
1787
+ }
1788
+ const targetDir = targetNormalized
1789
+ ? join(target.repoPath, targetNormalized)
1790
+ : target.repoPath;
1791
+ // Resolve all source paths up front and validate target conflicts before
1792
+ // any FS work — partial moves on conflict would leave the user with a
1793
+ // confusing half-state.
1794
+ const moves = [];
1795
+ for (const entry of entries) {
1796
+ const existing = this.get(entry.entityType, entry.entityId);
1797
+ const dst = join(targetDir, `${entry.entityId}.md`);
1798
+ if (existing.path === dst)
1799
+ continue; // already there — no-op
1800
+ if (existsSync(dst)) {
1801
+ throw new Error(`Path already occupied: ${target.repoName}:${targetNormalized ? targetNormalized + '/' : ''}${entry.entityId}.md`);
1802
+ }
1803
+ moves.push({ src: existing.path, dst });
1804
+ }
1805
+ if (moves.length === 0)
1806
+ return { moved: 0 };
1807
+ const { renameSync, cpSync } = await import('fs');
1808
+ mkdirSync(targetDir, { recursive: true });
1809
+ for (const m of moves) {
1810
+ try {
1811
+ renameSync(m.src, m.dst);
1812
+ }
1813
+ catch (err) {
1814
+ if (err?.code === 'EXDEV') {
1815
+ cpSync(m.src, m.dst);
1816
+ rmSync(m.src);
1817
+ }
1818
+ else {
1819
+ throw err;
1820
+ }
1821
+ }
1822
+ }
1823
+ // Folder lifecycle: ensure markers in every source dir we may have
1824
+ // emptied, drop the marker in the (single) target dir now that it has
1825
+ // .md children. Helpers no-op for the repo root and for paths that
1826
+ // are already in the right state, so calling them per dir is cheap.
1827
+ const sourceMarkers = [];
1828
+ const visitedSourceDirs = new Set();
1829
+ for (const m of moves) {
1830
+ const sourceDir = dirname(m.src);
1831
+ if (visitedSourceDirs.has(sourceDir))
1832
+ continue;
1833
+ visitedSourceDirs.add(sourceDir);
1834
+ const added = this.ensureFolderMarker(sourceDir);
1835
+ if (added)
1836
+ sourceMarkers.push(added);
1837
+ }
1838
+ const removedTargetMarker = target.removeFolderMarker(targetDir);
1839
+ this.invalidateIndex();
1840
+ if (target !== this)
1841
+ target.invalidateIndex();
1842
+ const user = options?.user ?? this.gitUser;
1843
+ const where = target === this
1844
+ ? (targetNormalized || '(root)')
1845
+ : `${target.repoName}:${targetNormalized || '(root)'}`;
1846
+ const sourceMessage = options?.commitMessage ?? `Move ${moves.length} entries → ${where}`;
1847
+ if (this.gitService) {
1848
+ const sourcePaths = [...moves.map(m => m.src), ...sourceMarkers];
1849
+ const dstStaging = target === this
1850
+ ? [...moves.map(m => m.dst), ...(removedTargetMarker ? [removedTargetMarker] : [])]
1851
+ : [];
1852
+ this.gitService.commitFiles([...sourcePaths, ...dstStaging], sourceMessage, user);
1853
+ }
1854
+ if (target !== this && target.gitService) {
1855
+ const targetMessage = options?.commitMessage
1856
+ ?? `Receive ${moves.length} entries from ${this.repoName}`;
1857
+ const targetPaths = [...moves.map(m => m.dst), ...(removedTargetMarker ? [removedTargetMarker] : [])];
1858
+ target.gitService.commitFiles(targetPaths, targetMessage, user);
1859
+ }
1860
+ return { moved: moves.length };
1861
+ }
1862
+ /**
1863
+ * Delete a folder. Refuses non-empty folders unless `recursive: true`.
1864
+ * Returns the count of entries + subfolders removed (for blast-radius UI).
1865
+ */
1866
+ async deleteFolder(folderPath, options) {
1867
+ const normalized = validateFolderPath(folderPath);
1868
+ if (!normalized) {
1869
+ throw new InvalidFolderPathError('Cannot delete the repo root via deleteFolder');
1870
+ }
1871
+ const abs = join(this.repoPath, normalized);
1872
+ if (!existsSync(abs)) {
1873
+ throw new Error(`Folder not found: ${normalized}`);
1874
+ }
1875
+ const stats = this.countFolderContents(abs);
1876
+ if (!options?.recursive && (stats.entriesDeleted > 0 || stats.subfoldersDeleted > 0)) {
1877
+ const err = new Error(`Folder is not empty: ${normalized} (${stats.entriesDeleted} entries, ${stats.subfoldersDeleted} subfolders). Pass recursive: true to delete contents.`);
1878
+ err.code = 'FOLDER_NOT_EMPTY';
1879
+ err.blastRadius = stats;
1880
+ throw err;
1881
+ }
1882
+ const { rmSync } = await import('fs');
1883
+ rmSync(abs, { recursive: true, force: true });
1884
+ this.invalidateIndex();
1885
+ if (this.gitService) {
1886
+ const message = options?.commitMessage || `Delete folder: ${normalized}`;
1887
+ this.gitService.commitFiles([abs], message, options?.user ?? this.gitUser);
1888
+ }
1889
+ await this.pruneEmptyAncestors(dirname(abs));
1890
+ return stats;
1891
+ }
1892
+ /**
1893
+ * Count the entries (.md files with entity_type) and subfolders inside an
1894
+ * absolute directory, recursively. Used to surface blast radius in the
1895
+ * delete-folder approval UI.
1896
+ */
1897
+ countFolderContents(absDir) {
1898
+ let entriesDeleted = 0;
1899
+ const subfolders = new Set();
1900
+ const stack = [absDir];
1901
+ while (stack.length > 0) {
1902
+ const dir = stack.pop();
1903
+ let entries;
1904
+ try {
1905
+ entries = readdirSync(dir);
1906
+ }
1907
+ catch {
1908
+ continue;
1909
+ }
1910
+ for (const entry of entries) {
1911
+ if (entry.startsWith('.'))
1912
+ continue;
1913
+ if (WALK_IGNORE.has(entry))
1914
+ continue;
1915
+ const full = join(dir, entry);
1916
+ let st;
1917
+ try {
1918
+ st = statSync(full);
1919
+ }
1920
+ catch {
1921
+ continue;
1922
+ }
1923
+ if (st.isDirectory()) {
1924
+ subfolders.add(full);
1925
+ stack.push(full);
1926
+ }
1927
+ else if (st.isFile() && entry.endsWith('.md')) {
1928
+ entriesDeleted += 1;
1929
+ }
1930
+ }
1931
+ }
1932
+ return { entriesDeleted, subfoldersDeleted: subfolders.size };
1933
+ }
1934
+ /**
1935
+ * Return the storage backend the graph is bound to, or `undefined` if
1936
+ * no asset service is configured. Used by routes that need to branch
1937
+ * between local-stream and R2-redirect behavior.
1938
+ */
1939
+ getAssetStorage() {
1940
+ return this.assetService?.getStorage();
1941
+ }
1942
+ /**
1943
+ * Mint a short-lived pre-signed URL for an asset. Only supported by
1944
+ * remote backends (R2); the local backend throws. `method` selects
1945
+ * the verb the URL is signed against — HEAD callers need a HEAD URL.
1946
+ */
1947
+ async getAssetSignedUrl(entityType, entityId, filename, keyPrefix, expiresIn, method = 'GET') {
1948
+ if (!this.assetService) {
1949
+ throw new Error('Asset service is not configured for this graph');
1950
+ }
1951
+ return await this.assetService.getSignedUrl(entityType, entityId, filename, keyPrefix, expiresIn, method);
1952
+ }
805
1953
  /**
806
1954
  * Upload asset for an entity
807
1955
  */
@@ -821,84 +1969,63 @@ export class BaseGraphManager {
821
1969
  return await this.assetService.list(entityType, entityId, keyPrefix);
822
1970
  }
823
1971
  /**
824
- * Delete an asset
1972
+ * List every asset in the backend in one round-trip, tagged with
1973
+ * (entityType, entityId) origin. Workspace-wide since the asset backend
1974
+ * is shared across repos — callers filter by repo via {@link listEntityIds}.
825
1975
  */
826
- async deleteAsset(entityType, entityId, filename, keyPrefix) {
1976
+ async listAllAssets() {
827
1977
  if (!this.assetService) {
828
1978
  throw new Error('Asset service is not configured for this graph');
829
1979
  }
830
- return await this.assetService.delete(entityType, entityId, filename, keyPrefix);
1980
+ return await this.assetService.listAll();
831
1981
  }
832
1982
  /**
833
- * Delete all assets for an entity (when entity is deleted)
1983
+ * Enumerate (type, id) pairs in this repo. Used by endpoints (like
1984
+ * /api/repos/:repo/assets) that need to answer "does entity X belong to
1985
+ * this repo?".
1986
+ *
1987
+ * Backed by the file index, which reads frontmatter — so entries in
1988
+ * nested folders are included alongside legacy {plural-type}/ entries.
834
1989
  */
835
- async deleteAllAssets(entityType, entityId, keyPrefix) {
836
- if (!this.assetService) {
837
- throw new Error('Asset service is not configured for this graph');
1990
+ listEntityIds() {
1991
+ if (!existsSync(this.repoPath))
1992
+ return [];
1993
+ const results = [];
1994
+ for (const key of this.getFileIndex().keys()) {
1995
+ const slash = key.indexOf('/');
1996
+ if (slash <= 0)
1997
+ continue;
1998
+ results.push({ entityType: key.slice(0, slash), id: key.slice(slash + 1) });
838
1999
  }
839
- return await this.assetService.deleteAll(entityType, entityId, keyPrefix);
2000
+ return results;
840
2001
  }
841
2002
  /**
842
- * Get dataset rows, optionally filtered
2003
+ * Resolve the absolute on-disk markdown path for an entity. Goes through
2004
+ * the file index so it works for both the flat repo-root layout and
2005
+ * arbitrary nested folders. Use this anywhere callers need a path —
2006
+ * never reconstruct one from `${plural-type}/${id}.md`, which assumes
2007
+ * the legacy typed-folder layout and silently misses everything else.
843
2008
  */
844
- getDatasetRows(entityId, filter) {
845
- const entity = this.get('dataset', entityId);
846
- if (entity.data.entity_type !== 'dataset') {
847
- throw new Error(`Entity ${entityId} is not a dataset`);
848
- }
849
- const schema = entity.data.schema;
850
- const rows = this.csvService.parseRows(entity.document.content, schema);
851
- if (filter && Object.keys(filter).length > 0) {
852
- return this.csvService.filterRows(rows, filter);
853
- }
854
- return rows;
2009
+ getEntityFilePath(entityType, entityId) {
2010
+ return this.findEntityFile(entityType, entityId);
855
2011
  }
856
2012
  /**
857
- * Append rows to a dataset entity
2013
+ * Delete an asset
858
2014
  */
859
- async appendDatasetRows(entityId, newRows, commitMessage) {
860
- const entity = this.get('dataset', entityId);
861
- if (entity.data.entity_type !== 'dataset') {
862
- throw new Error(`Entity ${entityId} is not a dataset`);
2015
+ async deleteAsset(entityType, entityId, filename, keyPrefix) {
2016
+ if (!this.assetService) {
2017
+ throw new Error('Asset service is not configured for this graph');
863
2018
  }
864
- const schema = entity.data.schema;
865
- // Validate new rows against schema
866
- const validatedRows = newRows.map(row => {
867
- const stringRow = {};
868
- for (const [k, v] of Object.entries(row)) {
869
- stringRow[k] = v !== null && v !== undefined ? String(v) : '';
870
- }
871
- return this.csvService.validateRow(stringRow, schema);
872
- });
873
- // Parse existing rows and append
874
- const existingRows = this.csvService.parseRows(entity.document.content, schema);
875
- const allRows = [...existingRows, ...validatedRows];
876
- // Stringify back to CSV
877
- const newContent = this.csvService.stringifyRows(allRows, schema);
878
- // Update the entity file (update content, bump updated_at)
879
- const entityPath = this.getEntityPath('dataset', entityId);
880
- const updatedData = {
881
- ...entity.data,
882
- updated_at: new Date().toISOString(),
883
- updated_by: this.gitUser.id,
884
- };
885
- this.markdownService.writeFile(entityPath, {
886
- frontmatter: updatedData,
887
- content: newContent,
888
- wikilinks: this.markdownService.extractWikilinks(newContent, updatedData),
889
- });
890
- if (this.gitService) {
891
- const transaction = this.gitService.createTransaction();
892
- transaction.add({
893
- operation: 'update',
894
- entityType: 'dataset',
895
- entityId,
896
- filePath: entityPath,
897
- });
898
- const message = commitMessage || `Append ${validatedRows.length} row(s) to dataset: ${entityId}`;
899
- await transaction.commit(this.gitUser, message);
2019
+ return await this.assetService.delete(entityType, entityId, filename, keyPrefix);
2020
+ }
2021
+ /**
2022
+ * Delete all assets for an entity (when entity is deleted)
2023
+ */
2024
+ async deleteAllAssets(entityType, entityId, keyPrefix) {
2025
+ if (!this.assetService) {
2026
+ throw new Error('Asset service is not configured for this graph');
900
2027
  }
901
- return this.get('dataset', entityId);
2028
+ return await this.assetService.deleteAll(entityType, entityId, keyPrefix);
902
2029
  }
903
2030
  /**
904
2031
  * Get repository status