studiograph 1.3.48-next.26 → 1.3.48-next.261

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 (1640) hide show
  1. package/README.md +27 -17
  2. package/dist/agent/agent-pool.d.ts +120 -4
  3. package/dist/agent/agent-pool.js +358 -18
  4. package/dist/agent/agent-pool.js.map +1 -1
  5. package/dist/agent/context-window.d.ts +113 -0
  6. package/dist/agent/context-window.js +459 -0
  7. package/dist/agent/context-window.js.map +1 -0
  8. package/dist/agent/followups.d.ts +34 -0
  9. package/dist/agent/followups.js +52 -0
  10. package/dist/agent/followups.js.map +1 -0
  11. package/dist/agent/orchestrator.d.ts +110 -11
  12. package/dist/agent/orchestrator.js +547 -75
  13. package/dist/agent/orchestrator.js.map +1 -1
  14. package/dist/agent/prompts/entity-types-section.d.ts +18 -9
  15. package/dist/agent/prompts/entity-types-section.js +42 -18
  16. package/dist/agent/prompts/entity-types-section.js.map +1 -1
  17. package/dist/agent/prompts/system.md +107 -19
  18. package/dist/agent/skill-loader.js +3 -9
  19. package/dist/agent/skill-loader.js.map +1 -1
  20. package/dist/agent/skills/artifact-canvas/skill.md +257 -0
  21. package/dist/agent/skills/create-skill.md +39 -0
  22. package/dist/agent/skills/entity-schema.md +67 -150
  23. package/dist/agent/skills/interview/skill.md +44 -29
  24. package/dist/agent/skills/paged-documents/skill.md +147 -0
  25. package/dist/agent/skills/schema-rules.md +28 -28
  26. package/dist/agent/tools/artifact-tools.d.ts +237 -0
  27. package/dist/agent/tools/artifact-tools.js +407 -0
  28. package/dist/agent/tools/artifact-tools.js.map +1 -0
  29. package/dist/agent/tools/capture-tools.d.ts +31 -0
  30. package/dist/agent/tools/capture-tools.js +40 -0
  31. package/dist/agent/tools/capture-tools.js.map +1 -0
  32. package/dist/agent/tools/folder-tools.d.ts +50 -0
  33. package/dist/agent/tools/folder-tools.js +340 -0
  34. package/dist/agent/tools/folder-tools.js.map +1 -0
  35. package/dist/agent/tools/fs-tools.d.ts +6 -5
  36. package/dist/agent/tools/fs-tools.js +1 -1
  37. package/dist/agent/tools/fs-tools.js.map +1 -1
  38. package/dist/agent/tools/graph-tools.d.ts +45 -73
  39. package/dist/agent/tools/graph-tools.js +765 -239
  40. package/dist/agent/tools/graph-tools.js.map +1 -1
  41. package/dist/agent/tools/history-tools.d.ts +95 -0
  42. package/dist/agent/tools/history-tools.js +465 -0
  43. package/dist/agent/tools/history-tools.js.map +1 -0
  44. package/dist/agent/tools/load-skill.d.ts +4 -3
  45. package/dist/agent/tools/load-skill.js +1 -1
  46. package/dist/agent/tools/load-skill.js.map +1 -1
  47. package/dist/agent/tools/ops-tools.d.ts +2 -1
  48. package/dist/agent/tools/ops-tools.js +125 -25
  49. package/dist/agent/tools/ops-tools.js.map +1 -1
  50. package/dist/agent/tools/permission-tools.d.ts +69 -21
  51. package/dist/agent/tools/permission-tools.js +183 -18
  52. package/dist/agent/tools/permission-tools.js.map +1 -1
  53. package/dist/agent/tools/tool-loader.js +5 -4
  54. package/dist/agent/tools/tool-loader.js.map +1 -1
  55. package/dist/agent/tools/web-tools.js +58 -9
  56. package/dist/agent/tools/web-tools.js.map +1 -1
  57. package/dist/cli/commands/about.d.ts +1 -0
  58. package/dist/cli/commands/about.js +55 -3
  59. package/dist/cli/commands/about.js.map +1 -1
  60. package/dist/cli/commands/admin/audit-workspace.d.ts +23 -0
  61. package/dist/cli/commands/admin/audit-workspace.js +133 -0
  62. package/dist/cli/commands/admin/audit-workspace.js.map +1 -0
  63. package/dist/cli/commands/admin/audit.d.ts +21 -0
  64. package/dist/cli/commands/admin/audit.js +174 -0
  65. package/dist/cli/commands/admin/audit.js.map +1 -0
  66. package/dist/cli/commands/admin/backup.d.ts +54 -0
  67. package/dist/cli/commands/admin/backup.js +207 -0
  68. package/dist/cli/commands/admin/backup.js.map +1 -0
  69. package/dist/cli/commands/admin/folder.d.ts +11 -0
  70. package/dist/cli/commands/admin/folder.js +187 -0
  71. package/dist/cli/commands/admin/folder.js.map +1 -0
  72. package/dist/cli/commands/admin/index.d.ts +16 -0
  73. package/dist/cli/commands/admin/index.js +48 -0
  74. package/dist/cli/commands/admin/index.js.map +1 -0
  75. package/dist/cli/commands/admin/migrate-asset-owners.d.ts +56 -0
  76. package/dist/cli/commands/admin/migrate-asset-owners.js +86 -0
  77. package/dist/cli/commands/admin/migrate-asset-owners.js.map +1 -0
  78. package/dist/cli/commands/admin/migrate-assets.d.ts +62 -0
  79. package/dist/cli/commands/admin/migrate-assets.js +237 -0
  80. package/dist/cli/commands/admin/migrate-assets.js.map +1 -0
  81. package/dist/cli/commands/admin/migrate.d.ts +56 -0
  82. package/dist/cli/commands/admin/migrate.js +263 -0
  83. package/dist/cli/commands/admin/migrate.js.map +1 -0
  84. package/dist/cli/commands/admin/restore.d.ts +24 -0
  85. package/dist/cli/commands/admin/restore.js +228 -0
  86. package/dist/cli/commands/admin/restore.js.map +1 -0
  87. package/dist/cli/commands/admin/secrets.d.ts +23 -0
  88. package/dist/cli/commands/admin/secrets.js +256 -0
  89. package/dist/cli/commands/admin/secrets.js.map +1 -0
  90. package/dist/cli/commands/admin/settings.d.ts +28 -0
  91. package/dist/cli/commands/admin/settings.js +284 -0
  92. package/dist/cli/commands/admin/settings.js.map +1 -0
  93. package/dist/cli/commands/admin/token.d.ts +42 -0
  94. package/dist/cli/commands/admin/token.js +126 -0
  95. package/dist/cli/commands/admin/token.js.map +1 -0
  96. package/dist/cli/commands/admin/transfer-ownership.d.ts +15 -0
  97. package/dist/cli/commands/admin/transfer-ownership.js +106 -0
  98. package/dist/cli/commands/admin/transfer-ownership.js.map +1 -0
  99. package/dist/cli/commands/admin/user.d.ts +11 -0
  100. package/dist/cli/commands/admin/user.js +187 -0
  101. package/dist/cli/commands/admin/user.js.map +1 -0
  102. package/dist/cli/commands/clone.d.ts +23 -3
  103. package/dist/cli/commands/clone.js +140 -36
  104. package/dist/cli/commands/clone.js.map +1 -1
  105. package/dist/cli/commands/config.d.ts +23 -107
  106. package/dist/cli/commands/config.js +52 -260
  107. package/dist/cli/commands/config.js.map +1 -1
  108. package/dist/cli/commands/connector.d.ts +10 -13
  109. package/dist/cli/commands/connector.js +16 -58
  110. package/dist/cli/commands/connector.js.map +1 -1
  111. package/dist/cli/commands/deploy.js +215 -68
  112. package/dist/cli/commands/deploy.js.map +1 -1
  113. package/dist/cli/commands/index.js +7 -4
  114. package/dist/cli/commands/index.js.map +1 -1
  115. package/dist/cli/commands/init.js +10 -30
  116. package/dist/cli/commands/init.js.map +1 -1
  117. package/dist/cli/commands/mcp.js +54 -6
  118. package/dist/cli/commands/mcp.js.map +1 -1
  119. package/dist/cli/commands/r2.d.ts +3 -0
  120. package/dist/cli/commands/r2.js +223 -204
  121. package/dist/cli/commands/r2.js.map +1 -1
  122. package/dist/cli/commands/redeploy.js +65 -12
  123. package/dist/cli/commands/redeploy.js.map +1 -1
  124. package/dist/cli/commands/reset.d.ts +22 -6
  125. package/dist/cli/commands/reset.js +132 -34
  126. package/dist/cli/commands/reset.js.map +1 -1
  127. package/dist/cli/commands/serve.js +242 -24
  128. package/dist/cli/commands/serve.js.map +1 -1
  129. package/dist/cli/commands/sync.d.ts +1 -1
  130. package/dist/cli/commands/sync.js +230 -227
  131. package/dist/cli/commands/sync.js.map +1 -1
  132. package/dist/cli/crypto-bundle.d.ts +39 -0
  133. package/dist/cli/crypto-bundle.js +94 -0
  134. package/dist/cli/crypto-bundle.js.map +1 -0
  135. package/dist/cli/index.js +24 -23
  136. package/dist/cli/index.js.map +1 -1
  137. package/dist/cli/railway-pin.d.ts +68 -0
  138. package/dist/cli/railway-pin.js +96 -0
  139. package/dist/cli/railway-pin.js.map +1 -0
  140. package/dist/cli/railway-provisioning.d.ts +53 -0
  141. package/dist/cli/railway-provisioning.js +85 -0
  142. package/dist/cli/railway-provisioning.js.map +1 -0
  143. package/dist/cli/setup-wizard.d.ts +30 -49
  144. package/dist/cli/setup-wizard.js +39 -40
  145. package/dist/cli/setup-wizard.js.map +1 -1
  146. package/dist/cli/theme.d.ts +1 -1
  147. package/dist/core/accent-palette.d.ts +7 -18
  148. package/dist/core/accent-palette.js +27 -38
  149. package/dist/core/accent-palette.js.map +1 -1
  150. package/dist/core/artifact-asset-resolution.d.ts +90 -0
  151. package/dist/core/artifact-asset-resolution.js +138 -0
  152. package/dist/core/artifact-asset-resolution.js.map +1 -0
  153. package/dist/core/artifact-asset-urls.d.ts +111 -0
  154. package/dist/core/artifact-asset-urls.js +174 -0
  155. package/dist/core/artifact-asset-urls.js.map +1 -0
  156. package/dist/core/artifact-bundle.d.ts +69 -0
  157. package/dist/core/artifact-bundle.js +305 -0
  158. package/dist/core/artifact-bundle.js.map +1 -0
  159. package/dist/core/artifact-escape.d.ts +5 -0
  160. package/dist/core/artifact-escape.js +26 -0
  161. package/dist/core/artifact-escape.js.map +1 -0
  162. package/dist/core/artifact-export.d.ts +83 -0
  163. package/dist/core/artifact-export.js +1567 -0
  164. package/dist/core/artifact-export.js.map +1 -0
  165. package/dist/core/artifact-frame-document.d.ts +105 -0
  166. package/dist/core/artifact-frame-document.js +1427 -0
  167. package/dist/core/artifact-frame-document.js.map +1 -0
  168. package/dist/core/artifact-frame-layout.d.ts +79 -0
  169. package/dist/core/artifact-frame-layout.js +94 -0
  170. package/dist/core/artifact-frame-layout.js.map +1 -0
  171. package/dist/core/artifact-frame-model.d.ts +198 -0
  172. package/dist/core/artifact-frame-model.js +603 -0
  173. package/dist/core/artifact-frame-model.js.map +1 -0
  174. package/dist/core/artifact-frame-selectors.d.ts +42 -0
  175. package/dist/core/artifact-frame-selectors.js +65 -0
  176. package/dist/core/artifact-frame-selectors.js.map +1 -0
  177. package/dist/core/artifact-libraries.d.ts +23 -0
  178. package/dist/core/artifact-libraries.js +229 -0
  179. package/dist/core/artifact-libraries.js.map +1 -0
  180. package/dist/core/artifact-manifest.d.ts +64 -0
  181. package/dist/core/artifact-manifest.js +88 -0
  182. package/dist/core/artifact-manifest.js.map +1 -0
  183. package/dist/core/artifact-paged-css.d.ts +64 -0
  184. package/dist/core/artifact-paged-css.js +253 -0
  185. package/dist/core/artifact-paged-css.js.map +1 -0
  186. package/dist/core/artifact-patch.d.ts +58 -0
  187. package/dist/core/artifact-patch.js +309 -0
  188. package/dist/core/artifact-patch.js.map +1 -0
  189. package/dist/core/artifact-warnings.d.ts +51 -0
  190. package/dist/core/artifact-warnings.js +165 -0
  191. package/dist/core/artifact-warnings.js.map +1 -0
  192. package/dist/core/breakpoints.d.ts +27 -0
  193. package/dist/core/breakpoints.js +28 -0
  194. package/dist/core/breakpoints.js.map +1 -0
  195. package/dist/core/cell-anchor.d.ts +29 -0
  196. package/dist/core/cell-anchor.js +53 -0
  197. package/dist/core/cell-anchor.js.map +1 -0
  198. package/dist/core/comment-anchor.d.ts +41 -0
  199. package/dist/core/comment-anchor.js +147 -0
  200. package/dist/core/comment-anchor.js.map +1 -0
  201. package/dist/core/commit-messages.d.ts +57 -0
  202. package/dist/core/commit-messages.js +85 -0
  203. package/dist/core/commit-messages.js.map +1 -0
  204. package/dist/core/embeds.d.ts +108 -0
  205. package/dist/core/embeds.js +226 -0
  206. package/dist/core/embeds.js.map +1 -0
  207. package/dist/core/entity-body-templates.d.ts +21 -0
  208. package/dist/core/entity-body-templates.js +66 -0
  209. package/dist/core/entity-body-templates.js.map +1 -0
  210. package/dist/core/entity-types-catalog.d.ts +65 -0
  211. package/dist/core/entity-types-catalog.js +142 -0
  212. package/dist/core/entity-types-catalog.js.map +1 -0
  213. package/dist/core/field-library.d.ts +62 -0
  214. package/dist/core/field-library.js +129 -0
  215. package/dist/core/field-library.js.map +1 -0
  216. package/dist/core/folder-paths.d.ts +41 -0
  217. package/dist/core/folder-paths.js +95 -0
  218. package/dist/core/folder-paths.js.map +1 -0
  219. package/dist/core/folder-sidecar.d.ts +36 -0
  220. package/dist/core/folder-sidecar.js +91 -0
  221. package/dist/core/folder-sidecar.js.map +1 -0
  222. package/dist/core/font-family.d.ts +62 -0
  223. package/dist/core/font-family.js +84 -0
  224. package/dist/core/font-family.js.map +1 -0
  225. package/dist/core/format-registry.d.ts +187 -0
  226. package/dist/core/format-registry.js +411 -0
  227. package/dist/core/format-registry.js.map +1 -0
  228. package/dist/core/graph.d.ts +770 -30
  229. package/dist/core/graph.js +2282 -386
  230. package/dist/core/graph.js.map +1 -1
  231. package/dist/core/history-diff.d.ts +35 -0
  232. package/dist/core/history-diff.js +114 -0
  233. package/dist/core/history-diff.js.map +1 -0
  234. package/dist/core/markdown-strip.d.ts +22 -0
  235. package/dist/core/markdown-strip.js +51 -0
  236. package/dist/core/markdown-strip.js.map +1 -0
  237. package/dist/core/refs.d.ts +41 -0
  238. package/dist/core/refs.js +80 -0
  239. package/dist/core/refs.js.map +1 -0
  240. package/dist/core/schema-registry.d.ts +66 -0
  241. package/dist/core/schema-registry.js +198 -37
  242. package/dist/core/schema-registry.js.map +1 -1
  243. package/dist/core/schemas/connector.d.ts +67 -0
  244. package/dist/core/schemas/connector.js +100 -0
  245. package/dist/core/schemas/connector.js.map +1 -0
  246. package/dist/core/schemas/workspace.d.ts +122 -0
  247. package/dist/core/schemas/workspace.js +211 -0
  248. package/dist/core/schemas/workspace.js.map +1 -0
  249. package/dist/core/secrets/SecretStore.d.ts +77 -0
  250. package/dist/core/secrets/SecretStore.js +13 -0
  251. package/dist/core/secrets/SecretStore.js.map +1 -0
  252. package/dist/core/secrets/SettingsResolver.d.ts +54 -0
  253. package/dist/core/secrets/SettingsResolver.js +80 -0
  254. package/dist/core/secrets/SettingsResolver.js.map +1 -0
  255. package/dist/core/secrets/SettingsStore.d.ts +30 -0
  256. package/dist/core/secrets/SettingsStore.js +9 -0
  257. package/dist/core/secrets/SettingsStore.js.map +1 -0
  258. package/dist/core/secrets/SqliteEncryptedStore.d.ts +90 -0
  259. package/dist/core/secrets/SqliteEncryptedStore.js +598 -0
  260. package/dist/core/secrets/SqliteEncryptedStore.js.map +1 -0
  261. package/dist/core/secrets/audit.d.ts +36 -0
  262. package/dist/core/secrets/audit.js +60 -0
  263. package/dist/core/secrets/audit.js.map +1 -0
  264. package/dist/core/secrets/auth-db-migration.d.ts +129 -0
  265. package/dist/core/secrets/auth-db-migration.js +653 -0
  266. package/dist/core/secrets/auth-db-migration.js.map +1 -0
  267. package/dist/core/secrets/bundle.d.ts +141 -0
  268. package/dist/core/secrets/bundle.js +435 -0
  269. package/dist/core/secrets/bundle.js.map +1 -0
  270. package/dist/core/secrets/dual-write.d.ts +81 -0
  271. package/dist/core/secrets/dual-write.js +247 -0
  272. package/dist/core/secrets/dual-write.js.map +1 -0
  273. package/dist/core/secrets/encryption.d.ts +44 -0
  274. package/dist/core/secrets/encryption.js +97 -0
  275. package/dist/core/secrets/encryption.js.map +1 -0
  276. package/dist/core/secrets/index.d.ts +49 -0
  277. package/dist/core/secrets/index.js +74 -0
  278. package/dist/core/secrets/index.js.map +1 -0
  279. package/dist/core/secrets/master-key.d.ts +38 -0
  280. package/dist/core/secrets/master-key.js +122 -0
  281. package/dist/core/secrets/master-key.js.map +1 -0
  282. package/dist/core/secrets/probes/anthropic.d.ts +98 -0
  283. package/dist/core/secrets/probes/anthropic.js +300 -0
  284. package/dist/core/secrets/probes/anthropic.js.map +1 -0
  285. package/dist/core/secrets/probes/brave.d.ts +9 -0
  286. package/dist/core/secrets/probes/brave.js +15 -0
  287. package/dist/core/secrets/probes/brave.js.map +1 -0
  288. package/dist/core/secrets/probes/r2.d.ts +15 -0
  289. package/dist/core/secrets/probes/r2.js +14 -0
  290. package/dist/core/secrets/probes/r2.js.map +1 -0
  291. package/dist/core/secrets/probes/tavily.d.ts +18 -0
  292. package/dist/core/secrets/probes/tavily.js +58 -0
  293. package/dist/core/secrets/probes/tavily.js.map +1 -0
  294. package/dist/core/secrets/probes/voyage.d.ts +13 -0
  295. package/dist/core/secrets/probes/voyage.js +52 -0
  296. package/dist/core/secrets/probes/voyage.js.map +1 -0
  297. package/dist/core/secrets/r2-structural-migration.d.ts +39 -0
  298. package/dist/core/secrets/r2-structural-migration.js +109 -0
  299. package/dist/core/secrets/r2-structural-migration.js.map +1 -0
  300. package/dist/core/secrets/read-flip.d.ts +59 -0
  301. package/dist/core/secrets/read-flip.js +87 -0
  302. package/dist/core/secrets/read-flip.js.map +1 -0
  303. package/dist/core/secrets/registry.d.ts +237 -0
  304. package/dist/core/secrets/registry.js +710 -0
  305. package/dist/core/secrets/registry.js.map +1 -0
  306. package/dist/core/seed-folder.d.ts +59 -0
  307. package/dist/core/seed-folder.js +76 -0
  308. package/dist/core/seed-folder.js.map +1 -0
  309. package/dist/core/types.d.ts +88 -480
  310. package/dist/core/types.js +53 -6
  311. package/dist/core/types.js.map +1 -1
  312. package/dist/core/user-config.d.ts +81 -14
  313. package/dist/core/user-config.js +147 -17
  314. package/dist/core/user-config.js.map +1 -1
  315. package/dist/core/validation.d.ts +828 -1472
  316. package/dist/core/validation.js +475 -163
  317. package/dist/core/validation.js.map +1 -1
  318. package/dist/core/view-scope.d.ts +88 -0
  319. package/dist/core/view-scope.js +71 -0
  320. package/dist/core/view-scope.js.map +1 -0
  321. package/dist/core/workspace-manager.d.ts +156 -11
  322. package/dist/core/workspace-manager.js +364 -92
  323. package/dist/core/workspace-manager.js.map +1 -1
  324. package/dist/core/workspace.d.ts +19 -5
  325. package/dist/core/workspace.js +72 -56
  326. package/dist/core/workspace.js.map +1 -1
  327. package/dist/mcp/comment-virtual-entity.d.ts +36 -0
  328. package/dist/mcp/comment-virtual-entity.js +71 -0
  329. package/dist/mcp/comment-virtual-entity.js.map +1 -0
  330. package/dist/mcp/connector-manager.d.ts +72 -2
  331. package/dist/mcp/connector-manager.js +211 -32
  332. package/dist/mcp/connector-manager.js.map +1 -1
  333. package/dist/mcp/oauth-provider.d.ts +0 -5
  334. package/dist/mcp/oauth-provider.js +31 -25
  335. package/dist/mcp/oauth-provider.js.map +1 -1
  336. package/dist/mcp/oauth-registry.d.ts +46 -8
  337. package/dist/mcp/oauth-registry.js +52 -13
  338. package/dist/mcp/oauth-registry.js.map +1 -1
  339. package/dist/mcp/server-discovery.d.ts +73 -0
  340. package/dist/mcp/server-discovery.js +164 -0
  341. package/dist/mcp/server-discovery.js.map +1 -0
  342. package/dist/mcp/server.d.ts +24 -3
  343. package/dist/mcp/server.js +53 -10
  344. package/dist/mcp/server.js.map +1 -1
  345. package/dist/mcp/state-signer.d.ts +62 -0
  346. package/dist/mcp/state-signer.js +205 -0
  347. package/dist/mcp/state-signer.js.map +1 -0
  348. package/dist/mcp/stdio-proxy.d.ts +67 -0
  349. package/dist/mcp/stdio-proxy.js +149 -0
  350. package/dist/mcp/stdio-proxy.js.map +1 -0
  351. package/dist/mcp/tools.d.ts +73 -2
  352. package/dist/mcp/tools.js +2289 -115
  353. package/dist/mcp/tools.js.map +1 -1
  354. package/dist/server/__tests__/helpers/graph-api.d.ts +17 -0
  355. package/dist/server/__tests__/helpers/graph-api.js +17 -0
  356. package/dist/server/__tests__/helpers/graph-api.js.map +1 -0
  357. package/dist/server/__tests__/helpers/multipart.d.ts +9 -0
  358. package/dist/server/__tests__/helpers/multipart.js +14 -0
  359. package/dist/server/__tests__/helpers/multipart.js.map +1 -0
  360. package/dist/server/artifact-asset-mint.d.ts +59 -0
  361. package/dist/server/artifact-asset-mint.js +127 -0
  362. package/dist/server/artifact-asset-mint.js.map +1 -0
  363. package/dist/server/asset-index-queue.d.ts +98 -0
  364. package/dist/server/asset-index-queue.js +193 -0
  365. package/dist/server/asset-index-queue.js.map +1 -0
  366. package/dist/server/body-write-coordinator.d.ts +161 -0
  367. package/dist/server/body-write-coordinator.js +182 -0
  368. package/dist/server/body-write-coordinator.js.map +1 -0
  369. package/dist/server/cloud-billing-flow.d.ts +32 -0
  370. package/dist/server/cloud-billing-flow.js +75 -0
  371. package/dist/server/cloud-billing-flow.js.map +1 -0
  372. package/dist/server/commit-audit.d.ts +26 -0
  373. package/dist/server/commit-audit.js +30 -0
  374. package/dist/server/commit-audit.js.map +1 -0
  375. package/dist/server/index.d.ts +50 -1
  376. package/dist/server/index.js +811 -229
  377. package/dist/server/index.js.map +1 -1
  378. package/dist/server/middleware/sanitize.d.ts +46 -0
  379. package/dist/server/middleware/sanitize.js +195 -0
  380. package/dist/server/middleware/sanitize.js.map +1 -0
  381. package/dist/server/process-guards.d.ts +37 -0
  382. package/dist/server/process-guards.js +44 -0
  383. package/dist/server/process-guards.js.map +1 -0
  384. package/dist/server/routes/ai-usage-api.d.ts +24 -0
  385. package/dist/server/routes/ai-usage-api.js +68 -0
  386. package/dist/server/routes/ai-usage-api.js.map +1 -0
  387. package/dist/server/routes/artifact-export-api.d.ts +11 -0
  388. package/dist/server/routes/artifact-export-api.js +180 -0
  389. package/dist/server/routes/artifact-export-api.js.map +1 -0
  390. package/dist/server/routes/artifact-patch-api.d.ts +19 -0
  391. package/dist/server/routes/artifact-patch-api.js +114 -0
  392. package/dist/server/routes/artifact-patch-api.js.map +1 -0
  393. package/dist/server/routes/artifact-selection-api.d.ts +15 -0
  394. package/dist/server/routes/artifact-selection-api.js +90 -0
  395. package/dist/server/routes/artifact-selection-api.js.map +1 -0
  396. package/dist/server/routes/asset-api.d.ts +88 -2
  397. package/dist/server/routes/asset-api.js +858 -112
  398. package/dist/server/routes/asset-api.js.map +1 -1
  399. package/dist/server/routes/audit-api.d.ts +24 -0
  400. package/dist/server/routes/audit-api.js +113 -0
  401. package/dist/server/routes/audit-api.js.map +1 -0
  402. package/dist/server/routes/auth-api.js +536 -44
  403. package/dist/server/routes/auth-api.js.map +1 -1
  404. package/dist/server/routes/capture-api.js +81 -9
  405. package/dist/server/routes/capture-api.js.map +1 -1
  406. package/dist/server/routes/chat-sessions-api.d.ts +13 -0
  407. package/dist/server/routes/chat-sessions-api.js +462 -0
  408. package/dist/server/routes/chat-sessions-api.js.map +1 -0
  409. package/dist/server/routes/chat.d.ts +35 -1
  410. package/dist/server/routes/chat.js +858 -68
  411. package/dist/server/routes/chat.js.map +1 -1
  412. package/dist/server/routes/comments-api.d.ts +15 -0
  413. package/dist/server/routes/comments-api.js +444 -0
  414. package/dist/server/routes/comments-api.js.map +1 -0
  415. package/dist/server/routes/config-api.d.ts +3 -2
  416. package/dist/server/routes/config-api.js +183 -57
  417. package/dist/server/routes/config-api.js.map +1 -1
  418. package/dist/server/routes/connectors-api.js +178 -45
  419. package/dist/server/routes/connectors-api.js.map +1 -1
  420. package/dist/server/routes/event-ingest-api.d.ts +15 -0
  421. package/dist/server/routes/event-ingest-api.js +125 -0
  422. package/dist/server/routes/event-ingest-api.js.map +1 -0
  423. package/dist/server/routes/field-library-api.d.ts +28 -0
  424. package/dist/server/routes/field-library-api.js +126 -0
  425. package/dist/server/routes/field-library-api.js.map +1 -0
  426. package/dist/server/routes/git-http.d.ts +3 -3
  427. package/dist/server/routes/git-http.js +90 -30
  428. package/dist/server/routes/git-http.js.map +1 -1
  429. package/dist/server/routes/graph-api-access.d.ts +63 -0
  430. package/dist/server/routes/graph-api-access.js +115 -0
  431. package/dist/server/routes/graph-api-access.js.map +1 -0
  432. package/dist/server/routes/graph-api.d.ts +2 -3
  433. package/dist/server/routes/graph-api.js +1341 -423
  434. package/dist/server/routes/graph-api.js.map +1 -1
  435. package/dist/server/routes/health.d.ts +18 -0
  436. package/dist/server/routes/health.js +116 -0
  437. package/dist/server/routes/health.js.map +1 -0
  438. package/dist/server/routes/import-batch.d.ts +24 -0
  439. package/dist/server/routes/import-batch.js +33 -0
  440. package/dist/server/routes/import-batch.js.map +1 -0
  441. package/dist/server/routes/insights-api.d.ts +9 -2
  442. package/dist/server/routes/insights-api.js +352 -66
  443. package/dist/server/routes/insights-api.js.map +1 -1
  444. package/dist/server/routes/mcp.d.ts +8 -3
  445. package/dist/server/routes/mcp.js +24 -8
  446. package/dist/server/routes/mcp.js.map +1 -1
  447. package/dist/server/routes/oauth-api.d.ts +67 -0
  448. package/dist/server/routes/oauth-api.js +421 -0
  449. package/dist/server/routes/oauth-api.js.map +1 -0
  450. package/dist/server/routes/permissions-api.d.ts +8 -3
  451. package/dist/server/routes/permissions-api.js +43 -12
  452. package/dist/server/routes/permissions-api.js.map +1 -1
  453. package/dist/server/routes/r2-api.d.ts +25 -0
  454. package/dist/server/routes/r2-api.js +273 -0
  455. package/dist/server/routes/r2-api.js.map +1 -0
  456. package/dist/server/routes/secrets-api.d.ts +36 -0
  457. package/dist/server/routes/secrets-api.js +305 -0
  458. package/dist/server/routes/secrets-api.js.map +1 -0
  459. package/dist/server/routes/settings-api.d.ts +29 -0
  460. package/dist/server/routes/settings-api.js +177 -0
  461. package/dist/server/routes/settings-api.js.map +1 -0
  462. package/dist/server/routes/share-api.d.ts +30 -6
  463. package/dist/server/routes/share-api.js +240 -31
  464. package/dist/server/routes/share-api.js.map +1 -1
  465. package/dist/server/routes/views-api.d.ts +11 -2
  466. package/dist/server/routes/views-api.js +123 -22
  467. package/dist/server/routes/views-api.js.map +1 -1
  468. package/dist/server/routes/workspace-api.d.ts +2 -1
  469. package/dist/server/routes/workspace-api.js +150 -23
  470. package/dist/server/routes/workspace-api.js.map +1 -1
  471. package/dist/server/routes/ws.d.ts +2 -1
  472. package/dist/server/routes/ws.js +73 -20
  473. package/dist/server/routes/ws.js.map +1 -1
  474. package/dist/server/session-manager.d.ts +48 -5
  475. package/dist/server/session-manager.js +182 -14
  476. package/dist/server/session-manager.js.map +1 -1
  477. package/dist/server/ws-hub.d.ts +135 -56
  478. package/dist/server/ws-hub.js +0 -0
  479. package/dist/server/ws-hub.js.map +1 -1
  480. package/dist/server/yjs-manager.d.ts +275 -18
  481. package/dist/server/yjs-manager.js +819 -70
  482. package/dist/server/yjs-manager.js.map +1 -1
  483. package/dist/server/yjs-persistence.d.ts +165 -0
  484. package/dist/server/yjs-persistence.js +557 -0
  485. package/dist/server/yjs-persistence.js.map +1 -0
  486. package/dist/server/yjs-text-diff.d.ts +32 -0
  487. package/dist/server/yjs-text-diff.js +61 -0
  488. package/dist/server/yjs-text-diff.js.map +1 -0
  489. package/dist/services/access-control.d.ts +121 -0
  490. package/dist/services/access-control.js +194 -0
  491. package/dist/services/access-control.js.map +1 -0
  492. package/dist/services/ai-usage-ledger.d.ts +75 -0
  493. package/dist/services/ai-usage-ledger.js +250 -0
  494. package/dist/services/ai-usage-ledger.js.map +1 -0
  495. package/dist/services/artifact-asset-token.d.ts +69 -0
  496. package/dist/services/artifact-asset-token.js +94 -0
  497. package/dist/services/artifact-asset-token.js.map +1 -0
  498. package/dist/services/artifact-inspect-metadata.d.ts +21 -0
  499. package/dist/services/artifact-inspect-metadata.js +66 -0
  500. package/dist/services/artifact-inspect-metadata.js.map +1 -0
  501. package/dist/services/artifact-library-sources.d.ts +33 -0
  502. package/dist/services/artifact-library-sources.js +91 -0
  503. package/dist/services/artifact-library-sources.js.map +1 -0
  504. package/dist/services/artifact-patch-runner.d.ts +52 -0
  505. package/dist/services/artifact-patch-runner.js +74 -0
  506. package/dist/services/artifact-patch-runner.js.map +1 -0
  507. package/dist/services/artifact-render-export.d.ts +230 -0
  508. package/dist/services/artifact-render-export.js +1003 -0
  509. package/dist/services/artifact-render-export.js.map +1 -0
  510. package/dist/services/artifact-selection-store.d.ts +68 -0
  511. package/dist/services/artifact-selection-store.js +90 -0
  512. package/dist/services/artifact-selection-store.js.map +1 -0
  513. package/dist/services/artifact-store.d.ts +118 -0
  514. package/dist/services/artifact-store.js +234 -0
  515. package/dist/services/artifact-store.js.map +1 -0
  516. package/dist/services/asset-caption-service.d.ts +103 -0
  517. package/dist/services/asset-caption-service.js +200 -0
  518. package/dist/services/asset-caption-service.js.map +1 -0
  519. package/dist/services/asset-delete-authz.d.ts +32 -0
  520. package/dist/services/asset-delete-authz.js +62 -0
  521. package/dist/services/asset-delete-authz.js.map +1 -0
  522. package/dist/services/asset-delete-impact.d.ts +63 -0
  523. package/dist/services/asset-delete-impact.js +96 -0
  524. package/dist/services/asset-delete-impact.js.map +1 -0
  525. package/dist/services/asset-finalize-service.d.ts +58 -0
  526. package/dist/services/asset-finalize-service.js +216 -0
  527. package/dist/services/asset-finalize-service.js.map +1 -0
  528. package/dist/services/asset-import-service.d.ts +116 -0
  529. package/dist/services/asset-import-service.js +436 -0
  530. package/dist/services/asset-import-service.js.map +1 -0
  531. package/dist/services/asset-index-sweep.d.ts +95 -0
  532. package/dist/services/asset-index-sweep.js +271 -0
  533. package/dist/services/asset-index-sweep.js.map +1 -0
  534. package/dist/services/asset-lifecycle-check.d.ts +22 -0
  535. package/dist/services/asset-lifecycle-check.js +80 -0
  536. package/dist/services/asset-lifecycle-check.js.map +1 -0
  537. package/dist/services/asset-listing.d.ts +63 -0
  538. package/dist/services/asset-listing.js +124 -0
  539. package/dist/services/asset-listing.js.map +1 -0
  540. package/dist/services/asset-move-impact.d.ts +49 -0
  541. package/dist/services/asset-move-impact.js +62 -0
  542. package/dist/services/asset-move-impact.js.map +1 -0
  543. package/dist/services/asset-presign-service.d.ts +62 -0
  544. package/dist/services/asset-presign-service.js +126 -0
  545. package/dist/services/asset-presign-service.js.map +1 -0
  546. package/dist/services/asset-reference-index.d.ts +39 -0
  547. package/dist/services/asset-reference-index.js +33 -0
  548. package/dist/services/asset-reference-index.js.map +1 -0
  549. package/dist/services/asset-reference-scan.d.ts +57 -0
  550. package/dist/services/asset-reference-scan.js +150 -0
  551. package/dist/services/asset-reference-scan.js.map +1 -0
  552. package/dist/services/asset-sidecar-service.d.ts +28 -0
  553. package/dist/services/asset-sidecar-service.js +79 -0
  554. package/dist/services/asset-sidecar-service.js.map +1 -0
  555. package/dist/services/asset-upload-errors.d.ts +41 -0
  556. package/dist/services/asset-upload-errors.js +45 -0
  557. package/dist/services/asset-upload-errors.js.map +1 -0
  558. package/dist/services/asset-upload-service.d.ts +56 -0
  559. package/dist/services/asset-upload-service.js +200 -0
  560. package/dist/services/asset-upload-service.js.map +1 -0
  561. package/dist/services/asset-upload-token.d.ts +58 -0
  562. package/dist/services/asset-upload-token.js +75 -0
  563. package/dist/services/asset-upload-token.js.map +1 -0
  564. package/dist/services/assets/asset-index-service.d.ts +158 -0
  565. package/dist/services/assets/asset-index-service.js +293 -0
  566. package/dist/services/assets/asset-index-service.js.map +1 -0
  567. package/dist/services/assets/base.d.ts +119 -4
  568. package/dist/services/assets/base.js +148 -3
  569. package/dist/services/assets/base.js.map +1 -1
  570. package/dist/services/assets/index.d.ts +90 -0
  571. package/dist/services/assets/index.js +202 -0
  572. package/dist/services/assets/index.js.map +1 -1
  573. package/dist/services/assets/local.d.ts +9 -0
  574. package/dist/services/assets/local.js +30 -4
  575. package/dist/services/assets/local.js.map +1 -1
  576. package/dist/services/assets/r2-sync.d.ts +88 -0
  577. package/dist/services/assets/r2-sync.js +412 -0
  578. package/dist/services/assets/r2-sync.js.map +1 -0
  579. package/dist/services/assets/r2.d.ts +80 -0
  580. package/dist/services/assets/r2.js +159 -4
  581. package/dist/services/assets/r2.js.map +1 -1
  582. package/dist/services/auth-service.d.ts +278 -51
  583. package/dist/services/auth-service.js +995 -272
  584. package/dist/services/auth-service.js.map +1 -1
  585. package/dist/services/chat-session-service.d.ts +189 -0
  586. package/dist/services/chat-session-service.js +544 -0
  587. package/dist/services/chat-session-service.js.map +1 -0
  588. package/dist/services/cloud-inference-billing.d.ts +64 -0
  589. package/dist/services/cloud-inference-billing.js +321 -0
  590. package/dist/services/cloud-inference-billing.js.map +1 -0
  591. package/dist/services/comment-service.d.ts +97 -0
  592. package/dist/services/comment-service.js +341 -0
  593. package/dist/services/comment-service.js.map +1 -0
  594. package/dist/services/comment-virtual-entity.d.ts +36 -0
  595. package/dist/services/comment-virtual-entity.js +71 -0
  596. package/dist/services/comment-virtual-entity.js.map +1 -0
  597. package/dist/services/convert-service.d.ts +64 -0
  598. package/dist/services/convert-service.js +68 -0
  599. package/dist/services/convert-service.js.map +1 -0
  600. package/dist/services/csv-service.d.ts +22 -17
  601. package/dist/services/csv-service.js +55 -92
  602. package/dist/services/csv-service.js.map +1 -1
  603. package/dist/services/entity-mutations.d.ts +313 -0
  604. package/dist/services/entity-mutations.js +531 -0
  605. package/dist/services/entity-mutations.js.map +1 -0
  606. package/dist/services/event-processor.d.ts +104 -0
  607. package/dist/services/event-processor.js +451 -0
  608. package/dist/services/event-processor.js.map +1 -0
  609. package/dist/services/extract/docx.d.ts +1 -0
  610. package/dist/services/extract/docx.js +233 -0
  611. package/dist/services/extract/docx.js.map +1 -0
  612. package/dist/services/extract/index.d.ts +9 -0
  613. package/dist/services/extract/index.js +10 -0
  614. package/dist/services/extract/index.js.map +1 -0
  615. package/dist/services/extract/pdf.d.ts +13 -0
  616. package/dist/services/extract/pdf.js +69 -0
  617. package/dist/services/extract/pdf.js.map +1 -0
  618. package/dist/services/folder-creation.d.ts +33 -0
  619. package/dist/services/folder-creation.js +103 -0
  620. package/dist/services/folder-creation.js.map +1 -0
  621. package/dist/services/fonts/font-metadata.d.ts +42 -0
  622. package/dist/services/fonts/font-metadata.js +126 -0
  623. package/dist/services/fonts/font-metadata.js.map +1 -0
  624. package/dist/services/fonts/font-skill-service.d.ts +66 -0
  625. package/dist/services/fonts/font-skill-service.js +137 -0
  626. package/dist/services/fonts/font-skill-service.js.map +1 -0
  627. package/dist/services/git.d.ts +58 -0
  628. package/dist/services/git.js +194 -55
  629. package/dist/services/git.js.map +1 -1
  630. package/dist/services/graph-maintenance.js +4 -4
  631. package/dist/services/graph-maintenance.js.map +1 -1
  632. package/dist/services/import-service.d.ts +51 -2
  633. package/dist/services/import-service.js +254 -107
  634. package/dist/services/import-service.js.map +1 -1
  635. package/dist/services/lint-service.js +10 -17
  636. package/dist/services/lint-service.js.map +1 -1
  637. package/dist/services/markdown.d.ts +12 -1
  638. package/dist/services/markdown.js +77 -9
  639. package/dist/services/markdown.js.map +1 -1
  640. package/dist/services/mention-detector.d.ts +23 -0
  641. package/dist/services/mention-detector.js +47 -0
  642. package/dist/services/mention-detector.js.map +1 -0
  643. package/dist/services/model-capabilities.d.ts +41 -0
  644. package/dist/services/model-capabilities.js +107 -0
  645. package/dist/services/model-capabilities.js.map +1 -0
  646. package/dist/services/notification-broadcast.d.ts +32 -0
  647. package/dist/services/notification-broadcast.js +38 -0
  648. package/dist/services/notification-broadcast.js.map +1 -0
  649. package/dist/services/notification-service.d.ts +41 -0
  650. package/dist/services/notification-service.js +100 -0
  651. package/dist/services/notification-service.js.map +1 -0
  652. package/dist/services/oauth-server-store.d.ts +147 -0
  653. package/dist/services/oauth-server-store.js +319 -0
  654. package/dist/services/oauth-server-store.js.map +1 -0
  655. package/dist/services/orphan-service.js +2 -2
  656. package/dist/services/orphan-service.js.map +1 -1
  657. package/dist/services/personal-folder.d.ts +49 -0
  658. package/dist/services/personal-folder.js +122 -0
  659. package/dist/services/personal-folder.js.map +1 -0
  660. package/dist/services/playwright-bootstrap.d.ts +78 -0
  661. package/dist/services/playwright-bootstrap.js +219 -0
  662. package/dist/services/playwright-bootstrap.js.map +1 -0
  663. package/dist/services/scratch-asset-promote.d.ts +57 -0
  664. package/dist/services/scratch-asset-promote.js +95 -0
  665. package/dist/services/scratch-asset-promote.js.map +1 -0
  666. package/dist/services/scratch-attachment-service.d.ts +201 -0
  667. package/dist/services/scratch-attachment-service.js +354 -0
  668. package/dist/services/scratch-attachment-service.js.map +1 -0
  669. package/dist/services/share-link-service.d.ts +57 -0
  670. package/dist/services/share-link-service.js +142 -0
  671. package/dist/services/share-link-service.js.map +1 -0
  672. package/dist/services/user-person-bridge.d.ts +136 -0
  673. package/dist/services/user-person-bridge.js +341 -0
  674. package/dist/services/user-person-bridge.js.map +1 -0
  675. package/dist/services/utility-model-resolver.d.ts +37 -0
  676. package/dist/services/utility-model-resolver.js +67 -0
  677. package/dist/services/utility-model-resolver.js.map +1 -0
  678. package/dist/services/vector-service.d.ts +282 -5
  679. package/dist/services/vector-service.js +594 -39
  680. package/dist/services/vector-service.js.map +1 -1
  681. package/dist/services/workspace-access.d.ts +115 -0
  682. package/dist/services/workspace-access.js +154 -0
  683. package/dist/services/workspace-access.js.map +1 -0
  684. package/dist/services/workspace-assets-folder.d.ts +46 -0
  685. package/dist/services/workspace-assets-folder.js +75 -0
  686. package/dist/services/workspace-assets-folder.js.map +1 -0
  687. package/dist/utils/git.d.ts +17 -2
  688. package/dist/utils/git.js +23 -7
  689. package/dist/utils/git.js.map +1 -1
  690. package/dist/utils/log.d.ts +42 -0
  691. package/dist/utils/log.js +137 -0
  692. package/dist/utils/log.js.map +1 -0
  693. package/dist/utils/preflight.js +2 -2
  694. package/dist/utils/preflight.js.map +1 -1
  695. package/dist/utils/version-checker.d.ts +12 -19
  696. package/dist/utils/version-checker.js +14 -104
  697. package/dist/utils/version-checker.js.map +1 -1
  698. package/dist/web/_app/immutable/assets/0.B8rPC4xg.css +2 -0
  699. package/dist/web/_app/immutable/assets/12.DlIf1mKh.css +1 -0
  700. package/dist/web/_app/immutable/assets/13.WUaKJbVc.css +1 -0
  701. package/dist/web/_app/immutable/assets/15.CItnu3nw.css +1 -0
  702. package/dist/web/_app/immutable/assets/16.C3czeWWx.css +1 -0
  703. package/dist/web/_app/immutable/assets/17.CHiwoAMO.css +1 -0
  704. package/dist/web/_app/immutable/assets/18.fUGuXQ0K.css +1 -0
  705. package/dist/web/_app/immutable/assets/19.DcuK5ZVm.css +1 -0
  706. package/dist/web/_app/immutable/assets/2.bJqlVFXX.css +1 -0
  707. package/dist/web/_app/immutable/assets/3.DDfEAhbB.css +1 -0
  708. package/dist/web/_app/immutable/assets/4.Dtodt1Fe.css +1 -0
  709. package/dist/web/_app/immutable/assets/6.JPVpkzLZ.css +1 -0
  710. package/dist/web/_app/immutable/assets/AssetLibraryPicker.BWH260L8.css +1 -0
  711. package/dist/web/_app/immutable/assets/BulkActionsBar.CXSM7tO7.css +1 -0
  712. package/dist/web/_app/immutable/assets/CalendarView.Cch9MMQW.css +1 -0
  713. package/dist/web/_app/immutable/assets/CodeMirrorEditor.D6hf2pNJ.css +1 -0
  714. package/dist/web/_app/immutable/assets/CopyField.DVGZtw4U.css +1 -0
  715. package/dist/web/_app/immutable/assets/FolderMembersPanel.BvV1lBCg.css +1 -0
  716. package/dist/web/_app/immutable/assets/FolderMutationDialogs.De2NQ55E.css +1 -0
  717. package/dist/web/_app/immutable/assets/FolderPicker.Cq1tMnu_.css +1 -0
  718. package/dist/web/_app/immutable/assets/FolderTreeField.DTSzn15s.css +1 -0
  719. package/dist/web/_app/immutable/assets/FrameCanvas.Clh5Xdr7.css +1 -0
  720. package/dist/web/_app/immutable/assets/GalleryView.Cqu3-wtr.css +1 -0
  721. package/dist/web/_app/immutable/assets/GraphView.BJtGL9py.css +1 -0
  722. package/dist/web/_app/immutable/assets/GridCell.czY6eDb_.css +1 -0
  723. package/dist/web/_app/immutable/assets/KanbanView.C5X8TbQk.css +1 -0
  724. package/dist/web/_app/immutable/assets/Link.vV0ne105.css +1 -0
  725. package/dist/web/_app/immutable/assets/Markdown.CSYKSAm0.css +1 -0
  726. package/dist/web/_app/immutable/assets/Rail.Dyxy_E0O.css +1 -0
  727. package/dist/web/_app/immutable/assets/RepoCheckList.BLeJ79Eg.css +1 -0
  728. package/dist/web/_app/immutable/assets/SaveViewDialog.Bb9E81xd.css +1 -0
  729. package/dist/web/_app/immutable/assets/SearchInput.DTB6W35f.css +1 -0
  730. package/dist/web/_app/immutable/assets/SettingsDialog.CHzfQoo_.css +1 -0
  731. package/dist/web/_app/immutable/assets/Spinner.UBfl0pab.css +1 -0
  732. package/dist/web/_app/immutable/assets/StopFilledAlt.CjuwgVxu.css +1 -0
  733. package/dist/web/_app/immutable/assets/TimelineView.u4YqiztR.css +1 -0
  734. package/dist/web/_app/immutable/assets/Version.BQq1Fihu.css +1 -0
  735. package/dist/web/_app/immutable/assets/ViewBanner.DWswnFdi.css +1 -0
  736. package/dist/web/_app/immutable/assets/WorkspaceView.Cd6TS6oo.css +1 -0
  737. package/dist/web/_app/immutable/assets/button.DOg1smbV.css +1 -0
  738. package/dist/web/_app/immutable/assets/checkbox.CAcHq8G6.css +1 -0
  739. package/dist/web/_app/immutable/assets/download-with-progress.BZlFTn09.css +1 -0
  740. package/dist/web/_app/immutable/assets/filters.RgfXKVpA.css +1 -0
  741. package/dist/web/_app/immutable/assets/ibm-plex-mono-latin-400-normal.CvHOgSBP.woff +0 -0
  742. package/dist/web/_app/immutable/assets/ibm-plex-mono-latin-400-normal.DMJ8VG8y.woff2 +0 -0
  743. package/dist/web/_app/immutable/assets/ibm-plex-mono-latin-500-normal.CB9ihrfo.woff +0 -0
  744. package/dist/web/_app/immutable/assets/ibm-plex-mono-latin-500-normal.DSY6xOcd.woff2 +0 -0
  745. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-400-italic.CZTNEAuW.woff2 +0 -0
  746. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-400-italic.CsGl1sm0.woff +0 -0
  747. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-400-normal.CDDApCn2.woff2 +0 -0
  748. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-400-normal.CYLoc0-x.woff +0 -0
  749. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-500-italic.BNK2_mGO.woff2 +0 -0
  750. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-500-italic.DpEwFAQM.woff +0 -0
  751. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-500-normal.6ng42L7E.woff2 +0 -0
  752. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-500-normal.BgVn5rGT.woff +0 -0
  753. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-600-normal.Cu4Hd6ag.woff +0 -0
  754. package/dist/web/_app/immutable/assets/ibm-plex-sans-latin-600-normal.CuJfVYMP.woff2 +0 -0
  755. package/dist/web/_app/immutable/assets/list-columns.BgnafZ4K.css +1 -0
  756. package/dist/web/_app/immutable/chunks/-MYqXlc3.js +185 -0
  757. package/dist/web/_app/immutable/chunks/-Tbr-96c2.js +1 -0
  758. package/dist/web/_app/immutable/chunks/1QRy7-wo.js +1 -0
  759. package/dist/web/_app/immutable/chunks/1_BWKAKA2.js +23 -0
  760. package/dist/web/_app/immutable/chunks/2z2ekE6D2.js +2 -0
  761. package/dist/web/_app/immutable/chunks/6guf_P8c.js +1 -0
  762. package/dist/web/_app/immutable/chunks/7l950wkn.js +1 -0
  763. package/dist/web/_app/immutable/chunks/9Sn2PwSn2.js +1025 -0
  764. package/dist/web/_app/immutable/chunks/AFZSy-Vk.js +2 -0
  765. package/dist/web/_app/immutable/chunks/B06uE1qJ.js +1 -0
  766. package/dist/web/_app/immutable/chunks/B2cjR0rG.js +83 -0
  767. package/dist/web/_app/immutable/chunks/B3u4QMoU.js +1 -0
  768. package/dist/web/_app/immutable/chunks/B4PrlkmE.js +1 -0
  769. package/dist/web/_app/immutable/chunks/B5UH1FNe.js +3 -0
  770. package/dist/web/_app/immutable/chunks/B5sxjq1L2.js +1 -0
  771. package/dist/web/_app/immutable/chunks/B6Lt-qaJ.js +1 -0
  772. package/dist/web/_app/immutable/chunks/B6wo7Stk2.js +1 -0
  773. package/dist/web/_app/immutable/chunks/B9KfEsQw2.js +1 -0
  774. package/dist/web/_app/immutable/chunks/B9MY422N.js +1 -0
  775. package/dist/web/_app/immutable/chunks/B9VQbssY.js +3 -0
  776. package/dist/web/_app/immutable/chunks/BBrP9GiZ.js +1 -0
  777. package/dist/web/_app/immutable/chunks/BEGTD2EL2.js +1 -0
  778. package/dist/web/_app/immutable/chunks/BEyPhM3C.js +1 -0
  779. package/dist/web/_app/immutable/chunks/BIeOkmiV.js +1 -0
  780. package/dist/web/_app/immutable/chunks/BIwe25Lk.js +1 -0
  781. package/dist/web/_app/immutable/chunks/BO6-nRV9.js +1 -0
  782. package/dist/web/_app/immutable/chunks/BPILs-6H.js +1 -0
  783. package/dist/web/_app/immutable/chunks/BPRQzMGs.js +1 -0
  784. package/dist/web/_app/immutable/chunks/BQE7bWgF.js +1 -0
  785. package/dist/web/_app/immutable/chunks/BU5TCj7m.js +12 -0
  786. package/dist/web/_app/immutable/chunks/BZDKT2Qw2.js +2 -0
  787. package/dist/web/_app/immutable/chunks/B_y9A6Kg.js +4 -0
  788. package/dist/web/_app/immutable/chunks/BdR7t2GN.js +3 -0
  789. package/dist/web/_app/immutable/chunks/BnYohOts2.js +1 -0
  790. package/dist/web/_app/immutable/chunks/Bpmx7Umc.js +1 -0
  791. package/dist/web/_app/immutable/chunks/BqButnP4.js +1 -0
  792. package/dist/web/_app/immutable/chunks/BtDSRU36.js +1 -0
  793. package/dist/web/_app/immutable/chunks/Bvr6nSm92.js +1 -0
  794. package/dist/web/_app/immutable/chunks/BvyVm7-j.js +184 -0
  795. package/dist/web/_app/immutable/chunks/Bx6T8G5n2.js +1 -0
  796. package/dist/web/_app/immutable/chunks/BxeOdaUQ.js +1 -0
  797. package/dist/web/_app/immutable/chunks/C1B4xDs0.js +1 -0
  798. package/dist/web/_app/immutable/chunks/C2yz630b.js +2 -0
  799. package/dist/web/_app/immutable/chunks/C5YoDj-e.js +1 -0
  800. package/dist/web/_app/immutable/chunks/C6d9nn3k.js +1 -0
  801. package/dist/web/_app/immutable/chunks/C85NG5lS.js +1 -0
  802. package/dist/web/_app/immutable/chunks/C8ZiSqQm.js +1 -0
  803. package/dist/web/_app/immutable/chunks/CB-t1Bv-.js +1 -0
  804. package/dist/web/_app/immutable/chunks/CG8YKaqc.js +5 -0
  805. package/dist/web/_app/immutable/chunks/CJbr-v9X.js +1 -0
  806. package/dist/web/_app/immutable/chunks/CKGHeJzq.js +1 -0
  807. package/dist/web/_app/immutable/chunks/CKwV6HKc.js +1 -0
  808. package/dist/web/_app/immutable/chunks/CLninaDG2.js +2 -0
  809. package/dist/web/_app/immutable/chunks/CM400nLL.js +7 -0
  810. package/dist/web/_app/immutable/chunks/CPvxN2G62.js +1 -0
  811. package/dist/web/_app/immutable/chunks/CQW66fYJ.js +1 -0
  812. package/dist/web/_app/immutable/chunks/CQl_RUVN2.js +1 -0
  813. package/dist/web/_app/immutable/chunks/CR1JY_cb.js +6 -0
  814. package/dist/web/_app/immutable/chunks/CT35tdLp.js +1 -0
  815. package/dist/web/_app/immutable/chunks/CVKrGJ6C.js +1 -0
  816. package/dist/web/_app/immutable/chunks/CWIHttTH.js +1 -0
  817. package/dist/web/_app/immutable/chunks/CWZSexLW.js +1 -0
  818. package/dist/web/_app/immutable/chunks/CXPMb3zd.js +1 -0
  819. package/dist/web/_app/immutable/chunks/CZBW5wi82.js +1 -0
  820. package/dist/web/_app/immutable/chunks/CaimSvvF.js +5 -0
  821. package/dist/web/_app/immutable/chunks/CcWaWbrI2.js +1 -0
  822. package/dist/web/_app/immutable/chunks/CcgYvk_3.js +1 -0
  823. package/dist/web/_app/immutable/chunks/Cg7m1gqN.js +1 -0
  824. package/dist/web/_app/immutable/chunks/CgCz4Fj6.js +1 -0
  825. package/dist/web/_app/immutable/chunks/CgOgBK5j.js +1 -0
  826. package/dist/web/_app/immutable/chunks/ChS3kWO-.js +2 -0
  827. package/dist/web/_app/immutable/chunks/CkivrILi.js +1 -0
  828. package/dist/web/_app/immutable/chunks/CmWflMFc.js +1 -0
  829. package/dist/web/_app/immutable/chunks/Cm_yhEon2.js +1 -0
  830. package/dist/web/_app/immutable/chunks/CnoUnj6-2.js +1 -0
  831. package/dist/web/_app/immutable/chunks/CqyFdLMa.js +1 -0
  832. package/dist/web/_app/immutable/chunks/CsIWxmio2.js +22 -0
  833. package/dist/web/_app/immutable/chunks/CtgdtZ0G2.js +1 -0
  834. package/dist/web/_app/immutable/chunks/CuYL65Bn.js +2 -0
  835. package/dist/web/_app/immutable/chunks/CuatNdzp.js +2 -0
  836. package/dist/web/_app/immutable/chunks/Cx5IiiDO2.js +2 -0
  837. package/dist/web/_app/immutable/chunks/Cxdio_Al.js +2692 -0
  838. package/dist/web/_app/immutable/chunks/CyTgggiM2.js +1 -0
  839. package/dist/web/_app/immutable/chunks/D-vvXTU3.js +2 -0
  840. package/dist/web/_app/immutable/chunks/D0_Iu3Uj.js +2 -0
  841. package/dist/web/_app/immutable/chunks/D4vfNy_f.js +1 -0
  842. package/dist/web/_app/immutable/chunks/D6toh9Mr.js +1 -0
  843. package/dist/web/_app/immutable/chunks/DAsarOLg2.js +1 -0
  844. package/dist/web/_app/immutable/chunks/DBGaXAP8.js +1 -0
  845. package/dist/web/_app/immutable/chunks/DCgKWtld.js +1 -0
  846. package/dist/web/_app/immutable/chunks/DG7uGoZb2.js +58 -0
  847. package/dist/web/_app/immutable/chunks/DIsSxB_C.js +1 -0
  848. package/dist/web/_app/immutable/chunks/DJTvBXur.js +2 -0
  849. package/dist/web/_app/immutable/chunks/DLuIHCPq.js +1 -0
  850. package/dist/web/_app/immutable/chunks/DMFH9Cnt2.js +1 -0
  851. package/dist/web/_app/immutable/chunks/DNLoQniz.js +1 -0
  852. package/dist/web/_app/immutable/chunks/DPLkAijI2.js +1 -0
  853. package/dist/web/_app/immutable/chunks/DQMWa2t7.js +1 -0
  854. package/dist/web/_app/immutable/chunks/DTOO9nUj.js +1 -0
  855. package/dist/web/_app/immutable/chunks/DaB0hU8_.js +1 -0
  856. package/dist/web/_app/immutable/chunks/Dav8Lgyy2.js +5 -0
  857. package/dist/web/_app/immutable/chunks/Ddgu2Sjp.js +1 -0
  858. package/dist/web/_app/immutable/chunks/DfUXQOZ92.js +1 -0
  859. package/dist/web/_app/immutable/chunks/DgP0Pxes.js +224 -0
  860. package/dist/web/_app/immutable/chunks/DhD1HF7R.js +1 -0
  861. package/dist/web/_app/immutable/chunks/Dhmw5GUH.js +1 -0
  862. package/dist/web/_app/immutable/chunks/DiBVOY752.js +1 -0
  863. package/dist/web/_app/immutable/chunks/DiMsLIh3.js +1 -0
  864. package/dist/web/_app/immutable/chunks/DiiXREv7.js +1 -0
  865. package/dist/web/_app/immutable/chunks/DjXtgWTP.js +1 -0
  866. package/dist/web/_app/immutable/chunks/Dk2-JtmU2.js +1 -0
  867. package/dist/web/_app/immutable/chunks/Dl0bYyV_.js +1 -0
  868. package/dist/web/_app/immutable/chunks/DqKYbIld2.js +1 -0
  869. package/dist/web/_app/immutable/chunks/DtXJCaQc2.js +1 -0
  870. package/dist/web/_app/immutable/chunks/DwuBUEvU.js +1 -0
  871. package/dist/web/_app/immutable/chunks/F5xZdLlB.js +2 -0
  872. package/dist/web/_app/immutable/chunks/F92HmKX8.js +1 -0
  873. package/dist/web/_app/immutable/chunks/FISIvbqq.js +3 -0
  874. package/dist/web/_app/immutable/chunks/GC5Izn9E2.js +1 -0
  875. package/dist/web/_app/immutable/chunks/IMJutqSu2.js +1 -0
  876. package/dist/web/_app/immutable/chunks/K5YHt1WL2.js +1 -0
  877. package/dist/web/_app/immutable/chunks/MAYbL-xn2.js +6 -0
  878. package/dist/web/_app/immutable/chunks/MMtHlrry.js +1 -0
  879. package/dist/web/_app/immutable/chunks/MtN4vJa02.js +1 -0
  880. package/dist/web/_app/immutable/chunks/Oglnp7sm.js +14 -0
  881. package/dist/web/_app/immutable/chunks/PwUOTfSv.js +2 -0
  882. package/dist/web/_app/immutable/chunks/QsO256-z.js +1 -0
  883. package/dist/web/_app/immutable/chunks/RA-JwTzo.js +1 -0
  884. package/dist/web/_app/immutable/chunks/atFSAfcd2.js +2 -0
  885. package/dist/web/_app/immutable/chunks/j_uHTBnD.js +1 -0
  886. package/dist/web/_app/immutable/chunks/joKsoiFC.js +1 -0
  887. package/dist/web/_app/immutable/chunks/kNaey6uv.js +1 -0
  888. package/dist/web/_app/immutable/chunks/nqo2e2Yv.js +1 -0
  889. package/dist/web/_app/immutable/chunks/oIZupUnz.js +1 -0
  890. package/dist/web/_app/immutable/chunks/pPsy4008.js +1 -0
  891. package/dist/web/_app/immutable/chunks/qevfNcid.js +1 -0
  892. package/dist/web/_app/immutable/chunks/rjjyuGhb.js +1 -0
  893. package/dist/web/_app/immutable/chunks/sUAtda6y2.js +2 -0
  894. package/dist/web/_app/immutable/chunks/snCOoD78.js +1 -0
  895. package/dist/web/_app/immutable/chunks/uHaS74te.js +1 -0
  896. package/dist/web/_app/immutable/chunks/w5tsuIav.js +5 -0
  897. package/dist/web/_app/immutable/chunks/xihTtKlq.js +1 -0
  898. package/dist/web/_app/immutable/entry/app.1tcNTKI-.js +2 -0
  899. package/dist/web/_app/immutable/entry/start.BJMsySI1.js +1 -0
  900. package/dist/web/_app/immutable/nodes/0.v0W5DL7W.js +2 -0
  901. package/dist/web/_app/immutable/nodes/1.81Rb3Ith.js +1 -0
  902. package/dist/web/_app/immutable/nodes/10.CAlZNFOq.js +1 -0
  903. package/dist/web/_app/immutable/nodes/11.CCfUginI.js +1 -0
  904. package/dist/web/_app/immutable/nodes/12.BJLboAyk.js +1 -0
  905. package/dist/web/_app/immutable/nodes/13.Cz4UTl2G.js +1 -0
  906. package/dist/web/_app/immutable/nodes/14.DJwakC0o.js +1 -0
  907. package/dist/web/_app/immutable/nodes/15.D2mNE6C-.js +1 -0
  908. package/dist/web/_app/immutable/nodes/16.BjohAUt9.js +1 -0
  909. package/dist/web/_app/immutable/nodes/17.BI2F37bS.js +1 -0
  910. package/dist/web/_app/immutable/nodes/18.DsOT0tDo.js +1 -0
  911. package/dist/web/_app/immutable/nodes/19.BiJWLOUT.js +77 -0
  912. package/dist/web/_app/immutable/nodes/2.Cd72nG2V.js +124 -0
  913. package/dist/web/_app/immutable/nodes/3.BIT9mm9r.js +6 -0
  914. package/dist/web/_app/immutable/nodes/4.Cn7r0Poe.js +16 -0
  915. package/dist/web/_app/immutable/nodes/5.CkJNdCZG.js +1 -0
  916. package/dist/web/_app/immutable/nodes/6.1rbIRQgQ.js +6 -0
  917. package/dist/web/_app/immutable/nodes/7.w17Zl36i.js +1 -0
  918. package/dist/web/_app/immutable/nodes/8.DqTsYuU-.js +1 -0
  919. package/dist/web/_app/immutable/nodes/9.BJhYZPfc.js +1 -0
  920. package/dist/web/_app/version.json +1 -1
  921. package/dist/web/favicon-16.png +0 -0
  922. package/dist/web/favicon-180.png +0 -0
  923. package/dist/web/favicon-32.png +0 -0
  924. package/dist/web/favicon-48.png +0 -0
  925. package/dist/web/favicon-512.png +0 -0
  926. package/dist/web/favicon.ico +0 -0
  927. package/dist/web/favicon.svg +8 -0
  928. package/dist/web/index.html +59 -20
  929. package/dist/web/studiograph-logomark.svg +29 -0
  930. package/package.json +44 -15
  931. package/dist/agent/role-loader.d.ts +0 -24
  932. package/dist/agent/role-loader.js +0 -67
  933. package/dist/agent/role-loader.js.map +0 -1
  934. package/dist/agent/skills/create-deck/content-guide.md +0 -238
  935. package/dist/agent/skills/create-deck/deck-cadence.md +0 -105
  936. package/dist/agent/skills/create-deck/diagrams.md +0 -431
  937. package/dist/agent/skills/create-deck/skill.md +0 -89
  938. package/dist/agent/skills/create-deck/slide-selection.md +0 -115
  939. package/dist/agent/skills/create-deck/slide-types.md +0 -638
  940. package/dist/agent/skills/create-document/skill.md +0 -229
  941. package/dist/agent/skills/enrich-entities.md +0 -136
  942. package/dist/agent/skills/obsidian-source-setup.md +0 -246
  943. package/dist/agent/skills/skill-loader.d.ts +0 -48
  944. package/dist/agent/skills/skill-loader.js +0 -166
  945. package/dist/agent/skills/skill-loader.js.map +0 -1
  946. package/dist/agent/skills/sync-configuration.md +0 -119
  947. package/dist/agent/skills/sync-setup.md +0 -82
  948. package/dist/agent/tools/message-tools.d.ts +0 -42
  949. package/dist/agent/tools/message-tools.js +0 -96
  950. package/dist/agent/tools/message-tools.js.map +0 -1
  951. package/dist/agent/tools/sync-tools.d.ts +0 -35
  952. package/dist/agent/tools/sync-tools.js +0 -992
  953. package/dist/agent/tools/sync-tools.js.map +0 -1
  954. package/dist/auth/github.d.ts +0 -56
  955. package/dist/auth/github.js +0 -169
  956. package/dist/auth/github.js.map +0 -1
  957. package/dist/cli/commands/access.d.ts +0 -11
  958. package/dist/cli/commands/access.js +0 -156
  959. package/dist/cli/commands/access.js.map +0 -1
  960. package/dist/cli/commands/app.d.ts +0 -7
  961. package/dist/cli/commands/app.js +0 -268
  962. package/dist/cli/commands/app.js.map +0 -1
  963. package/dist/cli/commands/auth.d.ts +0 -10
  964. package/dist/cli/commands/auth.js +0 -79
  965. package/dist/cli/commands/auth.js.map +0 -1
  966. package/dist/cli/commands/clear.d.ts +0 -5
  967. package/dist/cli/commands/clear.js +0 -36
  968. package/dist/cli/commands/clear.js.map +0 -1
  969. package/dist/cli/commands/collection.d.ts +0 -10
  970. package/dist/cli/commands/collection.js +0 -185
  971. package/dist/cli/commands/collection.js.map +0 -1
  972. package/dist/cli/commands/enrich.d.ts +0 -11
  973. package/dist/cli/commands/enrich.js +0 -135
  974. package/dist/cli/commands/enrich.js.map +0 -1
  975. package/dist/cli/commands/graphrag.d.ts +0 -12
  976. package/dist/cli/commands/graphrag.js +0 -122
  977. package/dist/cli/commands/graphrag.js.map +0 -1
  978. package/dist/cli/commands/join.d.ts +0 -9
  979. package/dist/cli/commands/join.js +0 -255
  980. package/dist/cli/commands/join.js.map +0 -1
  981. package/dist/cli/commands/members.d.ts +0 -11
  982. package/dist/cli/commands/members.js +0 -230
  983. package/dist/cli/commands/members.js.map +0 -1
  984. package/dist/cli/commands/org.d.ts +0 -10
  985. package/dist/cli/commands/org.js +0 -132
  986. package/dist/cli/commands/org.js.map +0 -1
  987. package/dist/cli/commands/provision.d.ts +0 -8
  988. package/dist/cli/commands/provision.js +0 -116
  989. package/dist/cli/commands/provision.js.map +0 -1
  990. package/dist/cli/commands/resolve.d.ts +0 -8
  991. package/dist/cli/commands/resolve.js +0 -85
  992. package/dist/cli/commands/resolve.js.map +0 -1
  993. package/dist/cli/commands/review.d.ts +0 -19
  994. package/dist/cli/commands/review.js +0 -128
  995. package/dist/cli/commands/review.js.map +0 -1
  996. package/dist/cli/commands/source.d.ts +0 -16
  997. package/dist/cli/commands/source.js +0 -159
  998. package/dist/cli/commands/source.js.map +0 -1
  999. package/dist/cli/commands/start.d.ts +0 -7
  1000. package/dist/cli/commands/start.js +0 -576
  1001. package/dist/cli/commands/start.js.map +0 -1
  1002. package/dist/cli/commands/sync-collection.d.ts +0 -14
  1003. package/dist/cli/commands/sync-collection.js +0 -355
  1004. package/dist/cli/commands/sync-collection.js.map +0 -1
  1005. package/dist/cli/commands/sync-entities.d.ts +0 -13
  1006. package/dist/cli/commands/sync-entities.js +0 -242
  1007. package/dist/cli/commands/sync-entities.js.map +0 -1
  1008. package/dist/cli/commands/team.d.ts +0 -12
  1009. package/dist/cli/commands/team.js +0 -185
  1010. package/dist/cli/commands/team.js.map +0 -1
  1011. package/dist/cli/commands/update.d.ts +0 -8
  1012. package/dist/cli/commands/update.js +0 -155
  1013. package/dist/cli/commands/update.js.map +0 -1
  1014. package/dist/cli/commands/user.d.ts +0 -7
  1015. package/dist/cli/commands/user.js +0 -169
  1016. package/dist/cli/commands/user.js.map +0 -1
  1017. package/dist/cli/scaffolding.d.ts +0 -12
  1018. package/dist/cli/scaffolding.js +0 -397
  1019. package/dist/cli/scaffolding.js.map +0 -1
  1020. package/dist/cli/sync-review-interactive.d.ts +0 -31
  1021. package/dist/cli/sync-review-interactive.js +0 -393
  1022. package/dist/cli/sync-review-interactive.js.map +0 -1
  1023. package/dist/core/feature-flags.d.ts +0 -17
  1024. package/dist/core/feature-flags.js +0 -15
  1025. package/dist/core/feature-flags.js.map +0 -1
  1026. package/dist/core/features.d.ts +0 -16
  1027. package/dist/core/features.js +0 -48
  1028. package/dist/core/features.js.map +0 -1
  1029. package/dist/core/launch-entity-types.d.ts +0 -23
  1030. package/dist/core/launch-entity-types.js +0 -48
  1031. package/dist/core/launch-entity-types.js.map +0 -1
  1032. package/dist/core/migration-runner.d.ts +0 -42
  1033. package/dist/core/migration-runner.js +0 -232
  1034. package/dist/core/migration-runner.js.map +0 -1
  1035. package/dist/core/migration-types.d.ts +0 -101
  1036. package/dist/core/migration-types.js +0 -21
  1037. package/dist/core/migration-types.js.map +0 -1
  1038. package/dist/core/migrations/20260219-formalize-memory-location.d.ts +0 -2
  1039. package/dist/core/migrations/20260219-formalize-memory-location.js +0 -35
  1040. package/dist/core/migrations/20260219-formalize-memory-location.js.map +0 -1
  1041. package/dist/core/migrations/20260220-add-workspace-metadata.d.ts +0 -12
  1042. package/dist/core/migrations/20260220-add-workspace-metadata.js +0 -65
  1043. package/dist/core/migrations/20260220-add-workspace-metadata.js.map +0 -1
  1044. package/dist/core/migrations/20260220-add-workspace-readme.d.ts +0 -11
  1045. package/dist/core/migrations/20260220-add-workspace-readme.js +0 -82
  1046. package/dist/core/migrations/20260220-add-workspace-readme.js.map +0 -1
  1047. package/dist/core/migrations/20260220-migrate-yaml-to-json.d.ts +0 -9
  1048. package/dist/core/migrations/20260220-migrate-yaml-to-json.js +0 -64
  1049. package/dist/core/migrations/20260220-migrate-yaml-to-json.js.map +0 -1
  1050. package/dist/core/migrations/index.d.ts +0 -11
  1051. package/dist/core/migrations/index.js +0 -23
  1052. package/dist/core/migrations/index.js.map +0 -1
  1053. package/dist/integrations/asana.d.ts +0 -26
  1054. package/dist/integrations/asana.js +0 -78
  1055. package/dist/integrations/asana.js.map +0 -1
  1056. package/dist/integrations/figma-local.d.ts +0 -16
  1057. package/dist/integrations/figma-local.js +0 -10
  1058. package/dist/integrations/figma-local.js.map +0 -1
  1059. package/dist/integrations/figma.d.ts +0 -17
  1060. package/dist/integrations/figma.js +0 -17
  1061. package/dist/integrations/figma.js.map +0 -1
  1062. package/dist/integrations/granola.d.ts +0 -24
  1063. package/dist/integrations/granola.js +0 -60
  1064. package/dist/integrations/granola.js.map +0 -1
  1065. package/dist/integrations/linear.d.ts +0 -14
  1066. package/dist/integrations/linear.js +0 -80
  1067. package/dist/integrations/linear.js.map +0 -1
  1068. package/dist/integrations/paper-local.d.ts +0 -14
  1069. package/dist/integrations/paper-local.js +0 -10
  1070. package/dist/integrations/paper-local.js.map +0 -1
  1071. package/dist/integrations/paper.d.ts +0 -2
  1072. package/dist/integrations/paper.js +0 -10
  1073. package/dist/integrations/paper.js.map +0 -1
  1074. package/dist/integrations/pipedrive.d.ts +0 -26
  1075. package/dist/integrations/pipedrive.js +0 -97
  1076. package/dist/integrations/pipedrive.js.map +0 -1
  1077. package/dist/integrations/registry.d.ts +0 -8
  1078. package/dist/integrations/registry.js +0 -7
  1079. package/dist/integrations/registry.js.map +0 -1
  1080. package/dist/integrations/types.d.ts +0 -43
  1081. package/dist/integrations/types.js +0 -5
  1082. package/dist/integrations/types.js.map +0 -1
  1083. package/dist/lib/lib/utils.d.ts +0 -2
  1084. package/dist/lib/lib/utils.js +0 -6
  1085. package/dist/lib/lib/utils.js.map +0 -1
  1086. package/dist/mcp/connectors/asana.d.ts +0 -2
  1087. package/dist/mcp/connectors/asana.js +0 -20
  1088. package/dist/mcp/connectors/asana.js.map +0 -1
  1089. package/dist/mcp/connectors/definitions.d.ts +0 -45
  1090. package/dist/mcp/connectors/definitions.js +0 -32
  1091. package/dist/mcp/connectors/definitions.js.map +0 -1
  1092. package/dist/mcp/connectors/figma.d.ts +0 -5
  1093. package/dist/mcp/connectors/figma.js +0 -21
  1094. package/dist/mcp/connectors/figma.js.map +0 -1
  1095. package/dist/mcp/connectors/gdrive.d.ts +0 -2
  1096. package/dist/mcp/connectors/gdrive.js +0 -20
  1097. package/dist/mcp/connectors/gdrive.js.map +0 -1
  1098. package/dist/mcp/connectors/granola.d.ts +0 -2
  1099. package/dist/mcp/connectors/granola.js +0 -12
  1100. package/dist/mcp/connectors/granola.js.map +0 -1
  1101. package/dist/mcp/connectors/linear.d.ts +0 -2
  1102. package/dist/mcp/connectors/linear.js +0 -19
  1103. package/dist/mcp/connectors/linear.js.map +0 -1
  1104. package/dist/mcp/connectors/obsidian.d.ts +0 -2
  1105. package/dist/mcp/connectors/obsidian.js +0 -19
  1106. package/dist/mcp/connectors/obsidian.js.map +0 -1
  1107. package/dist/mcp/connectors/pipedrive.d.ts +0 -2
  1108. package/dist/mcp/connectors/pipedrive.js +0 -20
  1109. package/dist/mcp/connectors/pipedrive.js.map +0 -1
  1110. package/dist/mcp/connectors/slack.d.ts +0 -2
  1111. package/dist/mcp/connectors/slack.js +0 -21
  1112. package/dist/mcp/connectors/slack.js.map +0 -1
  1113. package/dist/mcp/server-oauth-provider.d.ts +0 -56
  1114. package/dist/mcp/server-oauth-provider.js +0 -142
  1115. package/dist/mcp/server-oauth-provider.js.map +0 -1
  1116. package/dist/renderers/documents/branding.d.ts +0 -2
  1117. package/dist/renderers/documents/branding.js +0 -42
  1118. package/dist/renderers/documents/branding.js.map +0 -1
  1119. package/dist/renderers/documents/entity-types.json +0 -14
  1120. package/dist/renderers/documents/parser/frontmatter.d.ts +0 -5
  1121. package/dist/renderers/documents/parser/frontmatter.js +0 -90
  1122. package/dist/renderers/documents/parser/frontmatter.js.map +0 -1
  1123. package/dist/renderers/documents/parser/index.d.ts +0 -5
  1124. package/dist/renderers/documents/parser/index.js +0 -142
  1125. package/dist/renderers/documents/parser/index.js.map +0 -1
  1126. package/dist/renderers/documents/parser/sections.d.ts +0 -18
  1127. package/dist/renderers/documents/parser/sections.js +0 -131
  1128. package/dist/renderers/documents/parser/sections.js.map +0 -1
  1129. package/dist/renderers/documents/pdf/generator.d.ts +0 -13
  1130. package/dist/renderers/documents/pdf/generator.js +0 -79
  1131. package/dist/renderers/documents/pdf/generator.js.map +0 -1
  1132. package/dist/renderers/documents/plugin.d.ts +0 -2
  1133. package/dist/renderers/documents/plugin.js +0 -63
  1134. package/dist/renderers/documents/plugin.js.map +0 -1
  1135. package/dist/renderers/documents/public/branding/logo.svg +0 -3
  1136. package/dist/renderers/documents/public/editor/chrome.css +0 -461
  1137. package/dist/renderers/documents/public/editor/chrome.js +0 -524
  1138. package/dist/renderers/documents/public/editor/editor.css +0 -751
  1139. package/dist/renderers/documents/public/editor/editor.html +0 -140
  1140. package/dist/renderers/documents/public/editor/editor.js +0 -953
  1141. package/dist/renderers/documents/public/fonts/fonts.css +0 -145
  1142. package/dist/renderers/documents/public/viewer/viewer.css +0 -1
  1143. package/dist/renderers/documents/public/viewer/viewer.js +0 -4
  1144. package/dist/renderers/documents/renderer/back.d.ts +0 -8
  1145. package/dist/renderers/documents/renderer/back.js +0 -28
  1146. package/dist/renderers/documents/renderer/back.js.map +0 -1
  1147. package/dist/renderers/documents/renderer/content.d.ts +0 -7
  1148. package/dist/renderers/documents/renderer/content.js +0 -25
  1149. package/dist/renderers/documents/renderer/content.js.map +0 -1
  1150. package/dist/renderers/documents/renderer/cover.d.ts +0 -15
  1151. package/dist/renderers/documents/renderer/cover.js +0 -43
  1152. package/dist/renderers/documents/renderer/cover.js.map +0 -1
  1153. package/dist/renderers/documents/renderer/helpers.d.ts +0 -1
  1154. package/dist/renderers/documents/renderer/helpers.js +0 -11
  1155. package/dist/renderers/documents/renderer/helpers.js.map +0 -1
  1156. package/dist/renderers/documents/renderer/index.d.ts +0 -32
  1157. package/dist/renderers/documents/renderer/index.js +0 -80
  1158. package/dist/renderers/documents/renderer/index.js.map +0 -1
  1159. package/dist/renderers/documents/renderer/styles.d.ts +0 -16
  1160. package/dist/renderers/documents/renderer/styles.js +0 -546
  1161. package/dist/renderers/documents/renderer/styles.js.map +0 -1
  1162. package/dist/renderers/documents/routes/api.d.ts +0 -2
  1163. package/dist/renderers/documents/routes/api.js +0 -587
  1164. package/dist/renderers/documents/routes/api.js.map +0 -1
  1165. package/dist/renderers/documents/routes/editor.d.ts +0 -2
  1166. package/dist/renderers/documents/routes/editor.js +0 -31
  1167. package/dist/renderers/documents/routes/editor.js.map +0 -1
  1168. package/dist/renderers/documents/routes/viewer.d.ts +0 -2
  1169. package/dist/renderers/documents/routes/viewer.js +0 -67
  1170. package/dist/renderers/documents/routes/viewer.js.map +0 -1
  1171. package/dist/renderers/documents/types.d.ts +0 -61
  1172. package/dist/renderers/documents/types.js +0 -2
  1173. package/dist/renderers/documents/types.js.map +0 -1
  1174. package/dist/renderers/index.d.ts +0 -24
  1175. package/dist/renderers/index.js +0 -66
  1176. package/dist/renderers/index.js.map +0 -1
  1177. package/dist/renderers/slides/design-system/tokens.d.ts +0 -108
  1178. package/dist/renderers/slides/design-system/tokens.js +0 -44
  1179. package/dist/renderers/slides/design-system/tokens.js.map +0 -1
  1180. package/dist/renderers/slides/entity-types.json +0 -14
  1181. package/dist/renderers/slides/parser/comment-parser.d.ts +0 -10
  1182. package/dist/renderers/slides/parser/comment-parser.js +0 -89
  1183. package/dist/renderers/slides/parser/comment-parser.js.map +0 -1
  1184. package/dist/renderers/slides/parser/frontmatter.d.ts +0 -10
  1185. package/dist/renderers/slides/parser/frontmatter.js +0 -93
  1186. package/dist/renderers/slides/parser/frontmatter.js.map +0 -1
  1187. package/dist/renderers/slides/parser/index.d.ts +0 -7
  1188. package/dist/renderers/slides/parser/index.js +0 -569
  1189. package/dist/renderers/slides/parser/index.js.map +0 -1
  1190. package/dist/renderers/slides/parser/slide-splitter.d.ts +0 -6
  1191. package/dist/renderers/slides/parser/slide-splitter.js +0 -54
  1192. package/dist/renderers/slides/parser/slide-splitter.js.map +0 -1
  1193. package/dist/renderers/slides/parser/type-detector.d.ts +0 -6
  1194. package/dist/renderers/slides/parser/type-detector.js +0 -70
  1195. package/dist/renderers/slides/parser/type-detector.js.map +0 -1
  1196. package/dist/renderers/slides/pdf/generator.d.ts +0 -11
  1197. package/dist/renderers/slides/pdf/generator.js +0 -105
  1198. package/dist/renderers/slides/pdf/generator.js.map +0 -1
  1199. package/dist/renderers/slides/plugin.d.ts +0 -2
  1200. package/dist/renderers/slides/plugin.js +0 -70
  1201. package/dist/renderers/slides/plugin.js.map +0 -1
  1202. package/dist/renderers/slides/public/editor/.gitkeep +0 -0
  1203. package/dist/renderers/slides/public/editor/chrome.css +0 -461
  1204. package/dist/renderers/slides/public/editor/chrome.js +0 -519
  1205. package/dist/renderers/slides/public/editor/editor.css +0 -896
  1206. package/dist/renderers/slides/public/editor/editor.html +0 -131
  1207. package/dist/renderers/slides/public/editor/editor.js +0 -1038
  1208. package/dist/renderers/slides/public/fonts/fonts.css +0 -145
  1209. package/dist/renderers/slides/public/viewer/.gitkeep +0 -0
  1210. package/dist/renderers/slides/public/viewer/chrome.css +0 -62
  1211. package/dist/renderers/slides/public/viewer/transitions.css +0 -1
  1212. package/dist/renderers/slides/public/viewer/viewer.css +0 -2145
  1213. package/dist/renderers/slides/public/viewer/viewer.js +0 -303
  1214. package/dist/renderers/slides/renderer/chrome/browser-frame.d.ts +0 -5
  1215. package/dist/renderers/slides/renderer/chrome/browser-frame.js +0 -22
  1216. package/dist/renderers/slides/renderer/chrome/browser-frame.js.map +0 -1
  1217. package/dist/renderers/slides/renderer/chrome/mobile-frame.d.ts +0 -6
  1218. package/dist/renderers/slides/renderer/chrome/mobile-frame.js +0 -18
  1219. package/dist/renderers/slides/renderer/chrome/mobile-frame.js.map +0 -1
  1220. package/dist/renderers/slides/renderer/helpers.d.ts +0 -13
  1221. package/dist/renderers/slides/renderer/helpers.js +0 -33
  1222. package/dist/renderers/slides/renderer/helpers.js.map +0 -1
  1223. package/dist/renderers/slides/renderer/index.d.ts +0 -16
  1224. package/dist/renderers/slides/renderer/index.js +0 -160
  1225. package/dist/renderers/slides/renderer/index.js.map +0 -1
  1226. package/dist/renderers/slides/renderer/layouts/agenda.d.ts +0 -2
  1227. package/dist/renderers/slides/renderer/layouts/agenda.js +0 -18
  1228. package/dist/renderers/slides/renderer/layouts/agenda.js.map +0 -1
  1229. package/dist/renderers/slides/renderer/layouts/blank.d.ts +0 -2
  1230. package/dist/renderers/slides/renderer/layouts/blank.js +0 -4
  1231. package/dist/renderers/slides/renderer/layouts/blank.js.map +0 -1
  1232. package/dist/renderers/slides/renderer/layouts/columns.d.ts +0 -2
  1233. package/dist/renderers/slides/renderer/layouts/columns.js +0 -32
  1234. package/dist/renderers/slides/renderer/layouts/columns.js.map +0 -1
  1235. package/dist/renderers/slides/renderer/layouts/cover.d.ts +0 -2
  1236. package/dist/renderers/slides/renderer/layouts/cover.js +0 -18
  1237. package/dist/renderers/slides/renderer/layouts/cover.js.map +0 -1
  1238. package/dist/renderers/slides/renderer/layouts/diagram-concentric.d.ts +0 -2
  1239. package/dist/renderers/slides/renderer/layouts/diagram-concentric.js +0 -120
  1240. package/dist/renderers/slides/renderer/layouts/diagram-concentric.js.map +0 -1
  1241. package/dist/renderers/slides/renderer/layouts/diagram-matrix.d.ts +0 -2
  1242. package/dist/renderers/slides/renderer/layouts/diagram-matrix.js +0 -158
  1243. package/dist/renderers/slides/renderer/layouts/diagram-matrix.js.map +0 -1
  1244. package/dist/renderers/slides/renderer/layouts/diagram-pyramid.d.ts +0 -2
  1245. package/dist/renderers/slides/renderer/layouts/diagram-pyramid.js +0 -86
  1246. package/dist/renderers/slides/renderer/layouts/diagram-pyramid.js.map +0 -1
  1247. package/dist/renderers/slides/renderer/layouts/diagram-sitemap.d.ts +0 -2
  1248. package/dist/renderers/slides/renderer/layouts/diagram-sitemap.js +0 -146
  1249. package/dist/renderers/slides/renderer/layouts/diagram-sitemap.js.map +0 -1
  1250. package/dist/renderers/slides/renderer/layouts/diagram-venn.d.ts +0 -2
  1251. package/dist/renderers/slides/renderer/layouts/diagram-venn.js +0 -100
  1252. package/dist/renderers/slides/renderer/layouts/diagram-venn.js.map +0 -1
  1253. package/dist/renderers/slides/renderer/layouts/diagram.d.ts +0 -2
  1254. package/dist/renderers/slides/renderer/layouts/diagram.js +0 -21
  1255. package/dist/renderers/slides/renderer/layouts/diagram.js.map +0 -1
  1256. package/dist/renderers/slides/renderer/layouts/divider.d.ts +0 -2
  1257. package/dist/renderers/slides/renderer/layouts/divider.js +0 -4
  1258. package/dist/renderers/slides/renderer/layouts/divider.js.map +0 -1
  1259. package/dist/renderers/slides/renderer/layouts/end.d.ts +0 -2
  1260. package/dist/renderers/slides/renderer/layouts/end.js +0 -8
  1261. package/dist/renderers/slides/renderer/layouts/end.js.map +0 -1
  1262. package/dist/renderers/slides/renderer/layouts/framework.d.ts +0 -2
  1263. package/dist/renderers/slides/renderer/layouts/framework.js +0 -47
  1264. package/dist/renderers/slides/renderer/layouts/framework.js.map +0 -1
  1265. package/dist/renderers/slides/renderer/layouts/image-browser-2up.d.ts +0 -2
  1266. package/dist/renderers/slides/renderer/layouts/image-browser-2up.js +0 -38
  1267. package/dist/renderers/slides/renderer/layouts/image-browser-2up.js.map +0 -1
  1268. package/dist/renderers/slides/renderer/layouts/image-browser-6up.d.ts +0 -2
  1269. package/dist/renderers/slides/renderer/layouts/image-browser-6up.js +0 -30
  1270. package/dist/renderers/slides/renderer/layouts/image-browser-6up.js.map +0 -1
  1271. package/dist/renderers/slides/renderer/layouts/image-browser.d.ts +0 -2
  1272. package/dist/renderers/slides/renderer/layouts/image-browser.js +0 -18
  1273. package/dist/renderers/slides/renderer/layouts/image-browser.js.map +0 -1
  1274. package/dist/renderers/slides/renderer/layouts/image-bullets.d.ts +0 -2
  1275. package/dist/renderers/slides/renderer/layouts/image-bullets.js +0 -26
  1276. package/dist/renderers/slides/renderer/layouts/image-bullets.js.map +0 -1
  1277. package/dist/renderers/slides/renderer/layouts/image-device-pair.d.ts +0 -2
  1278. package/dist/renderers/slides/renderer/layouts/image-device-pair.js +0 -34
  1279. package/dist/renderers/slides/renderer/layouts/image-device-pair.js.map +0 -1
  1280. package/dist/renderers/slides/renderer/layouts/image-full.d.ts +0 -2
  1281. package/dist/renderers/slides/renderer/layouts/image-full.js +0 -36
  1282. package/dist/renderers/slides/renderer/layouts/image-full.js.map +0 -1
  1283. package/dist/renderers/slides/renderer/layouts/image-grid-2x2.d.ts +0 -2
  1284. package/dist/renderers/slides/renderer/layouts/image-grid-2x2.js +0 -23
  1285. package/dist/renderers/slides/renderer/layouts/image-grid-2x2.js.map +0 -1
  1286. package/dist/renderers/slides/renderer/layouts/image-keywords.d.ts +0 -2
  1287. package/dist/renderers/slides/renderer/layouts/image-keywords.js +0 -31
  1288. package/dist/renderers/slides/renderer/layouts/image-keywords.js.map +0 -1
  1289. package/dist/renderers/slides/renderer/layouts/image-mobile-3up.d.ts +0 -2
  1290. package/dist/renderers/slides/renderer/layouts/image-mobile-3up.js +0 -22
  1291. package/dist/renderers/slides/renderer/layouts/image-mobile-3up.js.map +0 -1
  1292. package/dist/renderers/slides/renderer/layouts/image-mobile.d.ts +0 -2
  1293. package/dist/renderers/slides/renderer/layouts/image-mobile.js +0 -17
  1294. package/dist/renderers/slides/renderer/layouts/image-mobile.js.map +0 -1
  1295. package/dist/renderers/slides/renderer/layouts/image-split-right.d.ts +0 -2
  1296. package/dist/renderers/slides/renderer/layouts/image-split-right.js +0 -19
  1297. package/dist/renderers/slides/renderer/layouts/image-split-right.js.map +0 -1
  1298. package/dist/renderers/slides/renderer/layouts/image-split.d.ts +0 -2
  1299. package/dist/renderers/slides/renderer/layouts/image-split.js +0 -19
  1300. package/dist/renderers/slides/renderer/layouts/image-split.js.map +0 -1
  1301. package/dist/renderers/slides/renderer/layouts/introductions.d.ts +0 -2
  1302. package/dist/renderers/slides/renderer/layouts/introductions.js +0 -41
  1303. package/dist/renderers/slides/renderer/layouts/introductions.js.map +0 -1
  1304. package/dist/renderers/slides/renderer/layouts/moodboard.d.ts +0 -15
  1305. package/dist/renderers/slides/renderer/layouts/moodboard.js +0 -63
  1306. package/dist/renderers/slides/renderer/layouts/moodboard.js.map +0 -1
  1307. package/dist/renderers/slides/renderer/layouts/next-steps.d.ts +0 -2
  1308. package/dist/renderers/slides/renderer/layouts/next-steps.js +0 -13
  1309. package/dist/renderers/slides/renderer/layouts/next-steps.js.map +0 -1
  1310. package/dist/renderers/slides/renderer/layouts/pullquote.d.ts +0 -2
  1311. package/dist/renderers/slides/renderer/layouts/pullquote.js +0 -17
  1312. package/dist/renderers/slides/renderer/layouts/pullquote.js.map +0 -1
  1313. package/dist/renderers/slides/renderer/layouts/schedule.d.ts +0 -2
  1314. package/dist/renderers/slides/renderer/layouts/schedule.js +0 -58
  1315. package/dist/renderers/slides/renderer/layouts/schedule.js.map +0 -1
  1316. package/dist/renderers/slides/renderer/layouts/section.d.ts +0 -2
  1317. package/dist/renderers/slides/renderer/layouts/section.js +0 -18
  1318. package/dist/renderers/slides/renderer/layouts/section.js.map +0 -1
  1319. package/dist/renderers/slides/renderer/layouts/statement.d.ts +0 -2
  1320. package/dist/renderers/slides/renderer/layouts/statement.js +0 -13
  1321. package/dist/renderers/slides/renderer/layouts/statement.js.map +0 -1
  1322. package/dist/renderers/slides/renderer/layouts/stats.d.ts +0 -2
  1323. package/dist/renderers/slides/renderer/layouts/stats.js +0 -26
  1324. package/dist/renderers/slides/renderer/layouts/stats.js.map +0 -1
  1325. package/dist/renderers/slides/renderer/layouts/table.d.ts +0 -2
  1326. package/dist/renderers/slides/renderer/layouts/table.js +0 -23
  1327. package/dist/renderers/slides/renderer/layouts/table.js.map +0 -1
  1328. package/dist/renderers/slides/renderer/layouts/text-body.d.ts +0 -2
  1329. package/dist/renderers/slides/renderer/layouts/text-body.js +0 -14
  1330. package/dist/renderers/slides/renderer/layouts/text-body.js.map +0 -1
  1331. package/dist/renderers/slides/renderer/placeholder.d.ts +0 -5
  1332. package/dist/renderers/slides/renderer/placeholder.js +0 -12
  1333. package/dist/renderers/slides/renderer/placeholder.js.map +0 -1
  1334. package/dist/renderers/slides/routes/api.d.ts +0 -2
  1335. package/dist/renderers/slides/routes/api.js +0 -568
  1336. package/dist/renderers/slides/routes/api.js.map +0 -1
  1337. package/dist/renderers/slides/routes/editor.d.ts +0 -2
  1338. package/dist/renderers/slides/routes/editor.js +0 -32
  1339. package/dist/renderers/slides/routes/editor.js.map +0 -1
  1340. package/dist/renderers/slides/routes/viewer.d.ts +0 -2
  1341. package/dist/renderers/slides/routes/viewer.js +0 -67
  1342. package/dist/renderers/slides/routes/viewer.js.map +0 -1
  1343. package/dist/renderers/slides/types.d.ts +0 -111
  1344. package/dist/renderers/slides/types.js +0 -2
  1345. package/dist/renderers/slides/types.js.map +0 -1
  1346. package/dist/server/agents/agent-loader.d.ts +0 -16
  1347. package/dist/server/agents/agent-loader.js +0 -175
  1348. package/dist/server/agents/agent-loader.js.map +0 -1
  1349. package/dist/server/agents/agent-runner.d.ts +0 -33
  1350. package/dist/server/agents/agent-runner.js +0 -187
  1351. package/dist/server/agents/agent-runner.js.map +0 -1
  1352. package/dist/server/agents/agent-runtime.d.ts +0 -39
  1353. package/dist/server/agents/agent-runtime.js +0 -214
  1354. package/dist/server/agents/agent-runtime.js.map +0 -1
  1355. package/dist/server/agents/base-agent.d.ts +0 -30
  1356. package/dist/server/agents/base-agent.js +0 -88
  1357. package/dist/server/agents/base-agent.js.map +0 -1
  1358. package/dist/server/agents/executive-digest.d.ts +0 -16
  1359. package/dist/server/agents/executive-digest.js +0 -115
  1360. package/dist/server/agents/executive-digest.js.map +0 -1
  1361. package/dist/server/agents/graph-steward.d.ts +0 -16
  1362. package/dist/server/agents/graph-steward.js +0 -59
  1363. package/dist/server/agents/graph-steward.js.map +0 -1
  1364. package/dist/server/agents/meeting-followup.d.ts +0 -16
  1365. package/dist/server/agents/meeting-followup.js +0 -88
  1366. package/dist/server/agents/meeting-followup.js.map +0 -1
  1367. package/dist/server/agents/pipeline-monitor.d.ts +0 -15
  1368. package/dist/server/agents/pipeline-monitor.js +0 -86
  1369. package/dist/server/agents/pipeline-monitor.js.map +0 -1
  1370. package/dist/server/agents/types.d.ts +0 -81
  1371. package/dist/server/agents/types.js +0 -41
  1372. package/dist/server/agents/types.js.map +0 -1
  1373. package/dist/server/chrome/chrome.css +0 -701
  1374. package/dist/server/chrome/chrome.js +0 -374
  1375. package/dist/server/collab-authority.d.ts +0 -70
  1376. package/dist/server/collab-authority.js +0 -218
  1377. package/dist/server/collab-authority.js.map +0 -1
  1378. package/dist/server/collab.d.ts +0 -29
  1379. package/dist/server/collab.js +0 -195
  1380. package/dist/server/collab.js.map +0 -1
  1381. package/dist/server/commit-scheduler.d.ts +0 -39
  1382. package/dist/server/commit-scheduler.js +0 -113
  1383. package/dist/server/commit-scheduler.js.map +0 -1
  1384. package/dist/server/plugin-loader.d.ts +0 -53
  1385. package/dist/server/plugin-loader.js +0 -155
  1386. package/dist/server/plugin-loader.js.map +0 -1
  1387. package/dist/server/routes/agents-api.d.ts +0 -10
  1388. package/dist/server/routes/agents-api.js +0 -25
  1389. package/dist/server/routes/agents-api.js.map +0 -1
  1390. package/dist/server/routes/collab.d.ts +0 -6
  1391. package/dist/server/routes/collab.js +0 -10
  1392. package/dist/server/routes/collab.js.map +0 -1
  1393. package/dist/server/routes/features-api.d.ts +0 -10
  1394. package/dist/server/routes/features-api.js +0 -15
  1395. package/dist/server/routes/features-api.js.map +0 -1
  1396. package/dist/server/routes/git-api.d.ts +0 -9
  1397. package/dist/server/routes/git-api.js +0 -82
  1398. package/dist/server/routes/git-api.js.map +0 -1
  1399. package/dist/server/routes/meeting-ingest-api.d.ts +0 -14
  1400. package/dist/server/routes/meeting-ingest-api.js +0 -117
  1401. package/dist/server/routes/meeting-ingest-api.js.map +0 -1
  1402. package/dist/server/routes/messages-api.d.ts +0 -15
  1403. package/dist/server/routes/messages-api.js +0 -569
  1404. package/dist/server/routes/messages-api.js.map +0 -1
  1405. package/dist/server/routes/sync-api.d.ts +0 -26
  1406. package/dist/server/routes/sync-api.js +0 -848
  1407. package/dist/server/routes/sync-api.js.map +0 -1
  1408. package/dist/server/routes/view-public.d.ts +0 -10
  1409. package/dist/server/routes/view-public.js +0 -107
  1410. package/dist/server/routes/view-public.js.map +0 -1
  1411. package/dist/server/routes/webhook.d.ts +0 -14
  1412. package/dist/server/routes/webhook.js +0 -102
  1413. package/dist/server/routes/webhook.js.map +0 -1
  1414. package/dist/services/batch-processor.d.ts +0 -49
  1415. package/dist/services/batch-processor.js +0 -166
  1416. package/dist/services/batch-processor.js.map +0 -1
  1417. package/dist/services/briefing.d.ts +0 -26
  1418. package/dist/services/briefing.js +0 -206
  1419. package/dist/services/briefing.js.map +0 -1
  1420. package/dist/services/github-provisioner.d.ts +0 -36
  1421. package/dist/services/github-provisioner.js +0 -126
  1422. package/dist/services/github-provisioner.js.map +0 -1
  1423. package/dist/services/github-service.d.ts +0 -99
  1424. package/dist/services/github-service.js +0 -310
  1425. package/dist/services/github-service.js.map +0 -1
  1426. package/dist/services/heartbeat.d.ts +0 -35
  1427. package/dist/services/heartbeat.js +0 -236
  1428. package/dist/services/heartbeat.js.map +0 -1
  1429. package/dist/services/image-import-service.d.ts +0 -24
  1430. package/dist/services/image-import-service.js +0 -108
  1431. package/dist/services/image-import-service.js.map +0 -1
  1432. package/dist/services/insights.d.ts +0 -38
  1433. package/dist/services/insights.js +0 -269
  1434. package/dist/services/insights.js.map +0 -1
  1435. package/dist/services/meeting-processor.d.ts +0 -97
  1436. package/dist/services/meeting-processor.js +0 -382
  1437. package/dist/services/meeting-processor.js.map +0 -1
  1438. package/dist/services/message-service.d.ts +0 -117
  1439. package/dist/services/message-service.js +0 -414
  1440. package/dist/services/message-service.js.map +0 -1
  1441. package/dist/services/sync/collection-sync.d.ts +0 -60
  1442. package/dist/services/sync/collection-sync.js +0 -509
  1443. package/dist/services/sync/collection-sync.js.map +0 -1
  1444. package/dist/services/sync/commit.d.ts +0 -60
  1445. package/dist/services/sync/commit.js +0 -354
  1446. package/dist/services/sync/commit.js.map +0 -1
  1447. package/dist/services/sync/context-index.d.ts +0 -69
  1448. package/dist/services/sync/context-index.js +0 -280
  1449. package/dist/services/sync/context-index.js.map +0 -1
  1450. package/dist/services/sync/data-fetcher.d.ts +0 -31
  1451. package/dist/services/sync/data-fetcher.js +0 -12
  1452. package/dist/services/sync/data-fetcher.js.map +0 -1
  1453. package/dist/services/sync/derive.d.ts +0 -34
  1454. package/dist/services/sync/derive.js +0 -164
  1455. package/dist/services/sync/derive.js.map +0 -1
  1456. package/dist/services/sync/enrichment-state.d.ts +0 -31
  1457. package/dist/services/sync/enrichment-state.js +0 -63
  1458. package/dist/services/sync/enrichment-state.js.map +0 -1
  1459. package/dist/services/sync/enrichment.d.ts +0 -25
  1460. package/dist/services/sync/enrichment.js +0 -121
  1461. package/dist/services/sync/enrichment.js.map +0 -1
  1462. package/dist/services/sync/entity-refresh.d.ts +0 -30
  1463. package/dist/services/sync/entity-refresh.js +0 -275
  1464. package/dist/services/sync/entity-refresh.js.map +0 -1
  1465. package/dist/services/sync/frontmatter-extractor.d.ts +0 -40
  1466. package/dist/services/sync/frontmatter-extractor.js +0 -279
  1467. package/dist/services/sync/frontmatter-extractor.js.map +0 -1
  1468. package/dist/services/sync/graph-match-state.d.ts +0 -33
  1469. package/dist/services/sync/graph-match-state.js +0 -61
  1470. package/dist/services/sync/graph-match-state.js.map +0 -1
  1471. package/dist/services/sync/graph-match.d.ts +0 -53
  1472. package/dist/services/sync/graph-match.js +0 -316
  1473. package/dist/services/sync/graph-match.js.map +0 -1
  1474. package/dist/services/sync/graphrag-client.d.ts +0 -43
  1475. package/dist/services/sync/graphrag-client.js +0 -94
  1476. package/dist/services/sync/graphrag-client.js.map +0 -1
  1477. package/dist/services/sync/graphrag-config.d.ts +0 -16
  1478. package/dist/services/sync/graphrag-config.js +0 -39
  1479. package/dist/services/sync/graphrag-config.js.map +0 -1
  1480. package/dist/services/sync/graphrag-context.d.ts +0 -14
  1481. package/dist/services/sync/graphrag-context.js +0 -109
  1482. package/dist/services/sync/graphrag-context.js.map +0 -1
  1483. package/dist/services/sync/graphrag-indexer.d.ts +0 -30
  1484. package/dist/services/sync/graphrag-indexer.js +0 -358
  1485. package/dist/services/sync/graphrag-indexer.js.map +0 -1
  1486. package/dist/services/sync/llm.d.ts +0 -32
  1487. package/dist/services/sync/llm.js +0 -115
  1488. package/dist/services/sync/llm.js.map +0 -1
  1489. package/dist/services/sync/mcp-client.d.ts +0 -71
  1490. package/dist/services/sync/mcp-client.js +0 -299
  1491. package/dist/services/sync/mcp-client.js.map +0 -1
  1492. package/dist/services/sync/model-factory.d.ts +0 -13
  1493. package/dist/services/sync/model-factory.js +0 -38
  1494. package/dist/services/sync/model-factory.js.map +0 -1
  1495. package/dist/services/sync/name-quality.d.ts +0 -31
  1496. package/dist/services/sync/name-quality.js +0 -60
  1497. package/dist/services/sync/name-quality.js.map +0 -1
  1498. package/dist/services/sync/output-schemas.d.ts +0 -92
  1499. package/dist/services/sync/output-schemas.js +0 -43
  1500. package/dist/services/sync/output-schemas.js.map +0 -1
  1501. package/dist/services/sync/prompts.d.ts +0 -19
  1502. package/dist/services/sync/prompts.js +0 -128
  1503. package/dist/services/sync/prompts.js.map +0 -1
  1504. package/dist/services/sync/reconciler.d.ts +0 -48
  1505. package/dist/services/sync/reconciler.js +0 -294
  1506. package/dist/services/sync/reconciler.js.map +0 -1
  1507. package/dist/services/sync/rest-client.d.ts +0 -40
  1508. package/dist/services/sync/rest-client.js +0 -100
  1509. package/dist/services/sync/rest-client.js.map +0 -1
  1510. package/dist/services/sync/source-config.d.ts +0 -67
  1511. package/dist/services/sync/source-config.js +0 -304
  1512. package/dist/services/sync/source-config.js.map +0 -1
  1513. package/dist/services/sync/source-definitions/asana.d.ts +0 -14
  1514. package/dist/services/sync/source-definitions/asana.js +0 -42
  1515. package/dist/services/sync/source-definitions/asana.js.map +0 -1
  1516. package/dist/services/sync/source-definitions/definitions.d.ts +0 -8
  1517. package/dist/services/sync/source-definitions/definitions.js +0 -8
  1518. package/dist/services/sync/source-definitions/definitions.js.map +0 -1
  1519. package/dist/services/sync/source-definitions/gdrive.d.ts +0 -16
  1520. package/dist/services/sync/source-definitions/gdrive.js +0 -68
  1521. package/dist/services/sync/source-definitions/gdrive.js.map +0 -1
  1522. package/dist/services/sync/source-definitions/granola.d.ts +0 -2
  1523. package/dist/services/sync/source-definitions/granola.js +0 -21
  1524. package/dist/services/sync/source-definitions/granola.js.map +0 -1
  1525. package/dist/services/sync/source-definitions/linear.d.ts +0 -2
  1526. package/dist/services/sync/source-definitions/linear.js +0 -62
  1527. package/dist/services/sync/source-definitions/linear.js.map +0 -1
  1528. package/dist/services/sync/source-definitions/obsidian.d.ts +0 -2
  1529. package/dist/services/sync/source-definitions/obsidian.js +0 -55
  1530. package/dist/services/sync/source-definitions/obsidian.js.map +0 -1
  1531. package/dist/services/sync/source-definitions/pipedrive.d.ts +0 -2
  1532. package/dist/services/sync/source-definitions/pipedrive.js +0 -43
  1533. package/dist/services/sync/source-definitions/pipedrive.js.map +0 -1
  1534. package/dist/services/sync/staging.d.ts +0 -53
  1535. package/dist/services/sync/staging.js +0 -130
  1536. package/dist/services/sync/staging.js.map +0 -1
  1537. package/dist/services/sync/structured-extractor.d.ts +0 -57
  1538. package/dist/services/sync/structured-extractor.js +0 -584
  1539. package/dist/services/sync/structured-extractor.js.map +0 -1
  1540. package/dist/services/sync/sync-runner.d.ts +0 -32
  1541. package/dist/services/sync/sync-runner.js +0 -195
  1542. package/dist/services/sync/sync-runner.js.map +0 -1
  1543. package/dist/services/sync/sync-state.d.ts +0 -43
  1544. package/dist/services/sync/sync-state.js +0 -154
  1545. package/dist/services/sync/sync-state.js.map +0 -1
  1546. package/dist/services/sync/types.d.ts +0 -381
  1547. package/dist/services/sync/types.js +0 -8
  1548. package/dist/services/sync/types.js.map +0 -1
  1549. package/dist/services/sync/unstructured-extractor.d.ts +0 -27
  1550. package/dist/services/sync/unstructured-extractor.js +0 -185
  1551. package/dist/services/sync/unstructured-extractor.js.map +0 -1
  1552. package/dist/utils/merge-resolver.d.ts +0 -59
  1553. package/dist/utils/merge-resolver.js +0 -303
  1554. package/dist/utils/merge-resolver.js.map +0 -1
  1555. package/dist/utils/workspace-config.d.ts +0 -8
  1556. package/dist/utils/workspace-config.js +0 -22
  1557. package/dist/utils/workspace-config.js.map +0 -1
  1558. package/dist/web/_app/immutable/assets/0.B3eM5J6m.css +0 -1
  1559. package/dist/web/_app/immutable/assets/10.2jzI3cZY.css +0 -1
  1560. package/dist/web/_app/immutable/assets/11.BbBNo8jg.css +0 -1
  1561. package/dist/web/_app/immutable/assets/12.vV-8Nqud.css +0 -1
  1562. package/dist/web/_app/immutable/assets/2.CDsrnCq3.css +0 -1
  1563. package/dist/web/_app/immutable/assets/3.Ih7su2rl.css +0 -1
  1564. package/dist/web/_app/immutable/assets/4.CwXIQz3p.css +0 -1
  1565. package/dist/web/_app/immutable/assets/ChevronLeft.BwM9mwS4.css +0 -1
  1566. package/dist/web/_app/immutable/assets/GraphView.CRV5cekT.css +0 -1
  1567. package/dist/web/_app/immutable/assets/Toaster.rN8r_Hzv.css +0 -1
  1568. package/dist/web/_app/immutable/assets/ViewToolbar.CsWTe0yn.css +0 -1
  1569. package/dist/web/_app/immutable/assets/WorkspaceView.iWtzksrr.css +0 -1
  1570. package/dist/web/_app/immutable/chunks/0kuZ2gtb.js +0 -1
  1571. package/dist/web/_app/immutable/chunks/1Ea1UXwM.js +0 -2
  1572. package/dist/web/_app/immutable/chunks/284Zhbif.js +0 -1
  1573. package/dist/web/_app/immutable/chunks/7S2LGqoP.js +0 -2
  1574. package/dist/web/_app/immutable/chunks/7WZENMwt.js +0 -1
  1575. package/dist/web/_app/immutable/chunks/B0qHb2ee.js +0 -1
  1576. package/dist/web/_app/immutable/chunks/B45od9dM.js +0 -2
  1577. package/dist/web/_app/immutable/chunks/B4hXkypI.js +0 -1
  1578. package/dist/web/_app/immutable/chunks/BB_5th5W.js +0 -3383
  1579. package/dist/web/_app/immutable/chunks/BDvg8O5k.js +0 -5
  1580. package/dist/web/_app/immutable/chunks/BGiCmAMQ.js +0 -4
  1581. package/dist/web/_app/immutable/chunks/BHgT2Ede.js +0 -1
  1582. package/dist/web/_app/immutable/chunks/BQ1nK9ru.js +0 -1
  1583. package/dist/web/_app/immutable/chunks/BS0-13V2.js +0 -1
  1584. package/dist/web/_app/immutable/chunks/BSiC7hSS.js +0 -1
  1585. package/dist/web/_app/immutable/chunks/BkxW0BJZ.js +0 -1
  1586. package/dist/web/_app/immutable/chunks/Bp7FKkGt.js +0 -1
  1587. package/dist/web/_app/immutable/chunks/BrebtJNG.js +0 -18
  1588. package/dist/web/_app/immutable/chunks/Bz0ZjVQE.js +0 -2
  1589. package/dist/web/_app/immutable/chunks/C0LVZhvn.js +0 -1
  1590. package/dist/web/_app/immutable/chunks/C0Zx4tMe.js +0 -1
  1591. package/dist/web/_app/immutable/chunks/CHJcPZZK.js +0 -2
  1592. package/dist/web/_app/immutable/chunks/CRpK3grw.js +0 -2
  1593. package/dist/web/_app/immutable/chunks/CSVbGg_b.js +0 -5
  1594. package/dist/web/_app/immutable/chunks/CX_61fY8.js +0 -5
  1595. package/dist/web/_app/immutable/chunks/CYrVHOHA.js +0 -1
  1596. package/dist/web/_app/immutable/chunks/CdIKiXW6.js +0 -1
  1597. package/dist/web/_app/immutable/chunks/CqkleIqs.js +0 -1
  1598. package/dist/web/_app/immutable/chunks/D14M5KlG.js +0 -1
  1599. package/dist/web/_app/immutable/chunks/D71c9o7J.js +0 -2
  1600. package/dist/web/_app/immutable/chunks/DFYOFE3o.js +0 -1
  1601. package/dist/web/_app/immutable/chunks/DHJAqtPb.js +0 -1
  1602. package/dist/web/_app/immutable/chunks/DOqcDZIJ.js +0 -1
  1603. package/dist/web/_app/immutable/chunks/DQNtFKv6.js +0 -1
  1604. package/dist/web/_app/immutable/chunks/DZHWps9n.js +0 -1
  1605. package/dist/web/_app/immutable/chunks/DjLrjK2H.js +0 -1
  1606. package/dist/web/_app/immutable/chunks/DjvJRtva.js +0 -1
  1607. package/dist/web/_app/immutable/chunks/DnL9f0lc.js +0 -7
  1608. package/dist/web/_app/immutable/chunks/Dqq8gyOz.js +0 -1
  1609. package/dist/web/_app/immutable/chunks/DsnmJJEf.js +0 -1
  1610. package/dist/web/_app/immutable/chunks/DtG1tx2A.js +0 -1
  1611. package/dist/web/_app/immutable/chunks/Dxmmz-Kc.js +0 -1
  1612. package/dist/web/_app/immutable/chunks/DzVOENkg.js +0 -1
  1613. package/dist/web/_app/immutable/chunks/Ee5s0qSS.js +0 -5
  1614. package/dist/web/_app/immutable/chunks/KlRV1tKP.js +0 -1
  1615. package/dist/web/_app/immutable/chunks/L7dfS1-w.js +0 -1
  1616. package/dist/web/_app/immutable/chunks/MaQouOBH.js +0 -2
  1617. package/dist/web/_app/immutable/chunks/PPVm8Dsz.js +0 -1
  1618. package/dist/web/_app/immutable/chunks/WSUKABI_.js +0 -1
  1619. package/dist/web/_app/immutable/chunks/_7ZfIgbM.js +0 -1
  1620. package/dist/web/_app/immutable/chunks/pNJmbo_B.js +0 -1
  1621. package/dist/web/_app/immutable/chunks/q4I7EnCp.js +0 -1
  1622. package/dist/web/_app/immutable/chunks/qE6Hm01v.js +0 -1
  1623. package/dist/web/_app/immutable/chunks/vxEtkL8z.js +0 -1
  1624. package/dist/web/_app/immutable/chunks/wDIGUaoU.js +0 -3
  1625. package/dist/web/_app/immutable/chunks/wZWzupWT.js +0 -1
  1626. package/dist/web/_app/immutable/entry/app.Dc6h105F.js +0 -2
  1627. package/dist/web/_app/immutable/entry/start.D4codAU1.js +0 -1
  1628. package/dist/web/_app/immutable/nodes/0.BM3j5xaK.js +0 -2
  1629. package/dist/web/_app/immutable/nodes/1.CZ2dT63B.js +0 -1
  1630. package/dist/web/_app/immutable/nodes/10.AJJtsnPU.js +0 -1
  1631. package/dist/web/_app/immutable/nodes/11.B7SEgGLF.js +0 -1
  1632. package/dist/web/_app/immutable/nodes/12.BcJKl4UQ.js +0 -1
  1633. package/dist/web/_app/immutable/nodes/2.jp27tyTq.js +0 -269
  1634. package/dist/web/_app/immutable/nodes/3.I7SH_06w.js +0 -5
  1635. package/dist/web/_app/immutable/nodes/4.Bdphc-UJ.js +0 -11
  1636. package/dist/web/_app/immutable/nodes/5.CXpOErnP.js +0 -1
  1637. package/dist/web/_app/immutable/nodes/6.1wcSxmEA.js +0 -1
  1638. package/dist/web/_app/immutable/nodes/7.D0b7WYTp.js +0 -1
  1639. package/dist/web/_app/immutable/nodes/8.D0Gxvy3F.js +0 -1
  1640. package/dist/web/_app/immutable/nodes/9.B5Nr92OR.js +0 -1
@@ -4,25 +4,121 @@
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, isAbsolute } from 'path';
8
+ import { existsSync, readdirSync, rmSync, statSync, writeFileSync, mkdirSync } from 'fs';
9
+ import { createHash, randomUUID } from 'node:crypto';
9
10
  import { GitService } from '../services/git.js';
10
11
  import { MarkdownService } from '../services/markdown.js';
11
- import { AssetService } from '../services/assets/index.js';
12
- import { ENTITY_SCHEMAS, EntityBaseSchema, SINGULAR_TO_PLURAL, PLURAL_TO_SINGULAR } from './validation.js';
12
+ import { AssetService, publicUrlForAssetId } from '../services/assets/index.js';
13
+ import { FREE_STANDING_OWNER_TYPES } from '../services/assets/asset-index-service.js';
14
+ import { ENTITY_SCHEMAS, EntityBaseSchema, PLURAL_TO_SINGULAR, RESERVED_ENTITY_TYPES, assertValidTaskDates } from './validation.js';
15
+ import { isInternalField } from './schema-registry.js';
13
16
  import { CSVService } from '../services/csv-service.js';
17
+ import { commitMessage as commitMsg, titleCaseType } from './commit-messages.js';
18
+ import { computeChange, parseFrontmatterCacheFree } from './history-diff.js';
19
+ import { folderPathFromFile, normalizeFolderPath, validateFolderPath, InvalidFolderPathError } from './folder-paths.js';
20
+ import { FOLDER_SIDECAR, readFolderSidecar, readFolderSidecarData, readFolderSidecarId, writeFolderSidecar, writeFolderSidecarData } from './folder-sidecar.js';
14
21
  import { z } from 'zod';
22
+ /** Names skipped during recursive repo walks. */
23
+ const WALK_IGNORE = new Set(['.git', '.studiograph', 'node_modules']);
24
+ /**
25
+ * Compute the canonical revision hash for a body string. Used by the
26
+ * Phase 2 baseRevision precondition. SHA-256 hex (64 chars), matches the
27
+ * format Phase 3's `X-Studiograph-Base-Body-Hash` header expects.
28
+ */
29
+ function sha256OfBody(body) {
30
+ return createHash('sha256').update(body, 'utf-8').digest('hex');
31
+ }
32
+ /**
33
+ * Thrown by `graph.update` when the git commit fails after the disk write
34
+ * has already landed. The .md is updated and observable to subsequent
35
+ * readers via `graph.get`, but no commit exists — the repo is dirty.
36
+ *
37
+ * The caller MUST surface this typed error so an operator can recover
38
+ * (manual `git add && git commit`, or `git checkout -- <path>` to revert
39
+ * disk). v3-of-the-plan claim of "auto-convergence via sessionManager"
40
+ * was false outside the autosave PUT path; Phase 2 explicitly registers
41
+ * server-side dirty files with sessionManager when available, but the
42
+ * caller still gets the typed error so it knows the write was partial.
43
+ */
44
+ export class CommitFailedDirtyError extends Error {
45
+ entityType;
46
+ entityId;
47
+ path;
48
+ code = 'COMMIT_FAILED_DIRTY';
49
+ constructor(entityType, entityId, path, cause) {
50
+ const causeMsg = cause instanceof Error ? cause.message : String(cause);
51
+ super(`Commit failed after disk write for ${entityType}/${entityId} (${path}). ` +
52
+ `Disk is updated but uncommitted (dirty). Recover with \`git add ${path} && git commit\` ` +
53
+ `or \`git checkout -- ${path}\` to revert. Underlying error: ${causeMsg}`);
54
+ this.entityType = entityType;
55
+ this.entityId = entityId;
56
+ this.path = path;
57
+ this.name = 'CommitFailedDirtyError';
58
+ this.cause = cause;
59
+ }
60
+ }
61
+ /**
62
+ * Thrown by `graph.update` when a body write supplies a `baseRevision`
63
+ * that doesn't match the current body's content hash. Caller (autosave,
64
+ * MCP, agent, event processor) refetches and retries against the new
65
+ * revision.
66
+ */
67
+ export class StaleBaseRevisionError extends Error {
68
+ entityType;
69
+ entityId;
70
+ expected;
71
+ actual;
72
+ code = 'STALE_BASE_REVISION';
73
+ constructor(entityType, entityId, expected, actual) {
74
+ super(`Stale base_revision for ${entityType}/${entityId}: caller supplied ${expected.slice(0, 12)}…, ` +
75
+ `current is ${actual.slice(0, 12)}…. Refetch current content and retry.`);
76
+ this.entityType = entityType;
77
+ this.entityId = entityId;
78
+ this.expected = expected;
79
+ this.actual = actual;
80
+ this.name = 'StaleBaseRevisionError';
81
+ }
82
+ }
83
+ /**
84
+ * Thrown by `restoreFile` when the path the recreate would land at is
85
+ * currently occupied by a different entity. Carries the occupant's
86
+ * coordinates so callers (agent tool, REST endpoint) can surface a
87
+ * useful error to the user.
88
+ */
89
+ export class PathConflictError extends Error {
90
+ path;
91
+ occupiedBy;
92
+ code = 'PATH_CONFLICT';
93
+ constructor(path, occupiedBy) {
94
+ super(`Path ${path} is occupied by ${occupiedBy.entityType}/${occupiedBy.entityId}`);
95
+ this.path = path;
96
+ this.occupiedBy = occupiedBy;
97
+ this.name = 'PathConflictError';
98
+ }
99
+ }
15
100
  export class BaseGraphManager {
16
101
  repoPath;
17
102
  repoName;
18
103
  gitService;
19
104
  noGit;
20
105
  markdownService;
106
+ csvService;
21
107
  assetService;
108
+ /** Workspace-global ownership index for immutable-keyed assets. Injected by
109
+ * WorkspaceManager (one per workspace). Undefined in coordinator-less test
110
+ * / CLI graphs — those fall back to the legacy `{type}/{id}` key scheme. */
111
+ assetIndex;
22
112
  gitUser;
23
- csvService;
24
113
  vectorService;
25
114
  schemaRegistry;
115
+ workspaceRoot;
116
+ /** Cache for history-panel change summaries, keyed by `${relPath}@${hash}`.
117
+ * A commit's diff against its parent is immutable, so this never needs
118
+ * invalidation — entries only accumulate as history is viewed. */
119
+ historyChangeCache = new Map();
120
+ persistenceCallbacks;
121
+ bodyWriteCoordinator;
26
122
  constructor(config) {
27
123
  this.repoPath = config.repoPath;
28
124
  this.repoName = config.repoName;
@@ -30,6 +126,7 @@ export class BaseGraphManager {
30
126
  this.noGit = config.noGit ?? false;
31
127
  this.vectorService = config.vectorService;
32
128
  this.schemaRegistry = config.schemaRegistry;
129
+ this.workspaceRoot = config.workspaceRoot;
33
130
  // Initialize services
34
131
  if (!this.noGit) {
35
132
  this.gitService = new GitService(this.repoPath);
@@ -43,10 +140,185 @@ export class BaseGraphManager {
43
140
  // Ensure repo is initialized
44
141
  this.ensureRepo();
45
142
  }
46
- /** Expose git service for deferred commit operations (e.g. CommitScheduler). */
143
+ /**
144
+ * Friendly display label for an entity type — custom-type label when the
145
+ * workspace defines one, otherwise the title-cased key ("deal" → "Deal").
146
+ * Used to build human-readable commit messages.
147
+ */
148
+ typeLabel(type) {
149
+ return this.schemaRegistry?.getCustomTypeLabel(type) ?? titleCaseType(type);
150
+ }
151
+ /** Build an EntityRef for a commit message from a type, id, and optional title. */
152
+ commitRef(type, id, title) {
153
+ return { typeLabel: this.typeLabel(type), title: typeof title === 'string' ? title : undefined, id };
154
+ }
155
+ /**
156
+ * Build the friendly "Edited …" message for a deferred (batched) commit,
157
+ * given the "type/id" pairs accumulated by the SessionManager. Each title is
158
+ * resolved from the in-memory index at flush time (cheap lookup); an entity
159
+ * deleted before the flush falls back to its slug id.
160
+ */
161
+ buildEditedBatchMessage(entities) {
162
+ const refs = entities.map(({ type, id }) => {
163
+ let title;
164
+ try {
165
+ title = this.get(type, id).data?.name;
166
+ }
167
+ catch { /* gone → slug */ }
168
+ return this.commitRef(type, id, title);
169
+ });
170
+ return commitMsg.editedBatch(refs);
171
+ }
172
+ /**
173
+ * Change summary for one commit in a file's history — which frontmatter
174
+ * fields moved and whether the body changed — by diffing the file at `hash`
175
+ * against its true git parent (`hash^`). Used by the entity History panel.
176
+ *
177
+ * `relPath` is repo-relative. Null parent (root commit, or file absent at the
178
+ * parent) → `created`. Result is immutable per (path, hash), so it's cached.
179
+ * Returns null only when the file isn't present at `hash`.
180
+ */
181
+ getCommitChange(relPath, hash) {
182
+ if (!this.gitService)
183
+ return null;
184
+ const cacheKey = `${relPath}@${hash}`;
185
+ const cached = this.historyChangeCache.get(cacheKey);
186
+ if (cached)
187
+ return cached;
188
+ const current = this.gitService.getFileAtCommit(relPath, hash);
189
+ if (current === null)
190
+ return null;
191
+ // getFileAtCommit returns null when `hash^` has no commit (root) OR the
192
+ // file didn't exist there — both mean "created at this commit".
193
+ const parent = this.gitService.getFileAtCommit(relPath, `${hash}^`);
194
+ const change = computeChange(current, parent);
195
+ this.historyChangeCache.set(cacheKey, change);
196
+ return change;
197
+ }
198
+ /** Expose git service for deferred commit operations (SessionManager). */
47
199
  getGitService() {
48
200
  return this.gitService;
49
201
  }
202
+ /** Absolute repo path on disk. Used by callers that need to convert
203
+ * an entity's absolute path to a repo-relative one (e.g. for `git log`). */
204
+ getRepoPath() {
205
+ return this.repoPath;
206
+ }
207
+ /**
208
+ * Find an entity at a specific commit (existing or deleted) and
209
+ * return its repo-relative path + content. Public surface for the
210
+ * history tools — wraps the private `findEntityInCommit` walker.
211
+ * Returns null when no .md file at that commit matches the
212
+ * (entityType, entityId) pair.
213
+ */
214
+ getEntityAtCommit(entityType, entityId, commitHash) {
215
+ return this.findEntityInCommit(entityType, entityId, commitHash);
216
+ }
217
+ /**
218
+ * `git pull` this repo, then reconcile any live editor Y.Doc whose .md the
219
+ * pull rewrote. Mirrors the restore-reconcile fix (see `restoreFile`): a
220
+ * pull changes files on disk WITHOUT flowing through the coordinator's
221
+ * body-write path, so an open editor still holds the pre-pull content and
222
+ * would sync it back over the pull on its next autosave (silent loss of the
223
+ * pulled-in changes). Routing each changed entity through the coordinator
224
+ * acquires the per-doc lock and smart-merges disk into the live Y.Doc.
225
+ *
226
+ * The wrinkle vs. restore: a pull is repo-wide, so we diff HEAD before/after
227
+ * to find the changed .md files and reconcile EACH one's docName rather than
228
+ * a single known entity. To avoid parsing every changed file on a large pull
229
+ * when nobody is editing, the loop is gated to the entities that actually
230
+ * have a live Y.Doc open (`coordinator.activeDocNames()`).
231
+ *
232
+ * Throws if the pull itself fails (merge conflict, no upstream, etc.) — the
233
+ * caller (POST /api/pull) catches per-repo. Reconcile only runs after a
234
+ * successful pull, so a conflicted/aborted pull never reconciles.
235
+ *
236
+ * Returns the docNames that were reconciled (empty when nothing was open,
237
+ * the repo has no git service, or the pull was a no-op fast-forward to the
238
+ * same HEAD).
239
+ */
240
+ async pullAndReconcile() {
241
+ if (!this.gitService)
242
+ return { reconciled: [] };
243
+ const before = this.gitService.getHeadCommit();
244
+ this.gitService.pull();
245
+ const after = this.gitService.getHeadCommit();
246
+ // No new commits (already up to date) or no coordinator wired → the pull
247
+ // is the only effect; freshly-opened editors seed from the new disk state.
248
+ const coordinator = this.bodyWriteCoordinator;
249
+ if (!coordinator || !before || !after || before === after) {
250
+ return { reconciled: [] };
251
+ }
252
+ // The pull changed files on disk behind the in-memory file index (entity →
253
+ // path). Drop it before reconciling: reconcileFromDisk reads through
254
+ // `graph.get`, which resolves the entity's path via that index, so a stale
255
+ // path after a remote MOVE/RENAME/DELETE would make `graph.get` read a
256
+ // now-gone file, return '', and wipe the live Y.Doc. Mirrors the
257
+ // invalidateIndex() every local mutation already does.
258
+ this.invalidateIndex();
259
+ // Gate to the entities with a live Y.Doc open in THIS repo. When nobody is
260
+ // editing here, skip the per-file parse loop entirely — the pull already
261
+ // updated disk and later openers seed from it.
262
+ const prefix = `${this.repoName}/`;
263
+ const activeDocs = new Set(coordinator.activeDocNames().filter(d => d.startsWith(prefix)));
264
+ if (activeDocs.size === 0)
265
+ return { reconciled: [] };
266
+ // entityId (the .md basename) of each open doc — lets us skip parsing a
267
+ // changed file whose id isn't open before identifying its type.
268
+ const activeIds = new Set([...activeDocs].map(d => d.split('/').slice(2).join('/')));
269
+ const reconciled = [];
270
+ for (const rel of this.gitService.diffNameOnly(before, after)) {
271
+ if (!rel.endsWith('.md'))
272
+ continue;
273
+ const stem = rel.split('/').pop().replace(/\.md$/, '');
274
+ if (!activeIds.has(stem))
275
+ continue; // no open editor by this id
276
+ const absPath = join(this.repoPath, rel);
277
+ if (!existsSync(absPath))
278
+ continue; // deleted by the pull — no live body to reconcile to
279
+ const ent = this.identifyEntityAtPath(absPath);
280
+ if (!ent)
281
+ continue;
282
+ const docName = `${this.repoName}/${ent.entityType}/${ent.entityId}`;
283
+ if (!activeDocs.has(docName))
284
+ continue; // same id, different type — not the open doc
285
+ await coordinator.reconcileFromDisk({
286
+ repo: this.repoName,
287
+ entityType: ent.entityType,
288
+ entityId: ent.entityId,
289
+ entityPath: absPath,
290
+ });
291
+ reconciled.push(docName);
292
+ }
293
+ return { reconciled };
294
+ }
295
+ /** Workspace root passed in via GraphConfig. Used by the asset upload
296
+ * service to anchor PDF text sidecars at workspace level (so all repos
297
+ * share the same `assets-text/` tree, mirroring the shared
298
+ * `.studiograph/assets/` directory the local backend uses). */
299
+ getWorkspaceRoot() {
300
+ return this.workspaceRoot;
301
+ }
302
+ /**
303
+ * Install structural-mutation callbacks. Called once after construction by
304
+ * the server bootstrap (or test setup) so the Yjs persistence layer can
305
+ * react to rename/delete/changeType without route-layer code calling
306
+ * persistence APIs directly.
307
+ */
308
+ setPersistenceCallbacks(callbacks) {
309
+ this.persistenceCallbacks = callbacks;
310
+ }
311
+ /**
312
+ * Install the body-write coordinator. graph.update delegates the body-write
313
+ * critical section (lock, base-hash, atomic disk write, commit, broadcast,
314
+ * mdRevisionHash bookkeeping, assertWritable) to the coordinator. When
315
+ * unset (test fixtures without a Y.Doc layer), body writes fall back to a
316
+ * direct disk-write + commit path with no lock/broadcast — preserves the
317
+ * legacy "no callbacks" behavior some test setups rely on.
318
+ */
319
+ setBodyWriteCoordinator(coordinator) {
320
+ this.bodyWriteCoordinator = coordinator;
321
+ }
50
322
  /**
51
323
  * Ensure repository exists
52
324
  */
@@ -57,11 +329,140 @@ export class BaseGraphManager {
57
329
  // GitService automatically initializes in constructor
58
330
  }
59
331
  /**
60
- * Get entity file path: {repo}/{plural-type}/{entityId}.md
332
+ * Compute the on-disk path for a new entity.
333
+ *
334
+ * - When `folderPath` is provided, the entry lives at {repo}/{folderPath}/{id}.md
335
+ * regardless of type. This is the nested-folders layout.
336
+ * - Without `folderPath` (or empty string), the entry lands at the repo
337
+ * root: {repo}/{id}.md. The historical {repo}/{plural-type}/{id}.md
338
+ * fallback was confusing — entries hidden under a typed directory the
339
+ * list view never showed at repo root, contradicting the agent tool
340
+ * description that says "Omit to land at repo root."
341
+ *
342
+ * Note: this is for *new* paths only. Existing entries living under the
343
+ * legacy {plural-type}/ directories continue to be readable via
344
+ * findEntityFile() — only the create path moved.
345
+ */
346
+ getEntityPath(entityType, entityId, folderPath) {
347
+ const normalized = folderPath ? normalizeFolderPath(folderPath) : '';
348
+ if (normalized) {
349
+ return join(this.repoPath, normalized, `${entityId}.md`);
350
+ }
351
+ return join(this.repoPath, `${entityId}.md`);
352
+ }
353
+ // ────────────────────────────────────────────────────────────────────────────
354
+ // Recursive file walk + (type, id) → path index
355
+ //
356
+ // With nested folders, an entry can live at any depth under the repo root.
357
+ // We can't compute its path from (type, id) alone — we have to find it.
358
+ // The index caches that lookup; mutations invalidate it via invalidateIndex().
359
+ // ────────────────────────────────────────────────────────────────────────────
360
+ fileIndex;
361
+ /**
362
+ * Cache of the parsed `list()` view, keyed by `entityType ?? '*'`. Built
363
+ * lazily on first read; invalidated alongside `fileIndex` on every
364
+ * mutation. Without this, autosuggest walks the entire markdown tree
365
+ * and parses YAML frontmatter for every entry on every keystroke (5–7s
366
+ * on workspaces with a few thousand entries).
367
+ *
368
+ * Staleness contract: identical to `fileIndex` — single-process trust
369
+ * model. Git pulls and out-of-process file edits would equally miss
370
+ * the existing `fileIndex` cache; no regression there.
371
+ */
372
+ listCache;
373
+ /** Drop the cached file index so the next read rebuilds it. */
374
+ invalidateIndex() {
375
+ this.fileIndex = undefined;
376
+ this.listCache = undefined;
377
+ }
378
+ /**
379
+ * Walk every .md file under this repo, calling `visit` with the absolute
380
+ * path. Skips hidden directories (.git, .studiograph, etc.) and files that
381
+ * start with ".".
61
382
  */
62
- getEntityPath(entityType, entityId) {
63
- const pluralDir = SINGULAR_TO_PLURAL[entityType] || `${entityType}s`;
64
- return join(this.repoPath, pluralDir, `${entityId}.md`);
383
+ walkMarkdownFiles(visit) {
384
+ if (!existsSync(this.repoPath))
385
+ return;
386
+ const stack = [this.repoPath];
387
+ while (stack.length > 0) {
388
+ const dir = stack.pop();
389
+ let entries;
390
+ try {
391
+ entries = readdirSync(dir);
392
+ }
393
+ catch {
394
+ continue;
395
+ }
396
+ for (const entry of entries) {
397
+ if (entry.startsWith('.'))
398
+ continue;
399
+ if (WALK_IGNORE.has(entry))
400
+ continue;
401
+ const full = join(dir, entry);
402
+ let st;
403
+ try {
404
+ st = statSync(full);
405
+ }
406
+ catch {
407
+ continue;
408
+ }
409
+ if (st.isDirectory()) {
410
+ stack.push(full);
411
+ }
412
+ else if (st.isFile() && entry.endsWith('.md')) {
413
+ visit(full);
414
+ }
415
+ }
416
+ }
417
+ }
418
+ /**
419
+ * Build (or fetch the cached) "type/id" → absolute path index.
420
+ * Type comes from frontmatter `entity_type`; if missing, falls back to the
421
+ * top-level plural-type directory name (legacy layout). Files without either
422
+ * (e.g. README.md) are skipped — they aren't entities.
423
+ */
424
+ getFileIndex() {
425
+ if (this.fileIndex)
426
+ return this.fileIndex;
427
+ const index = new Map();
428
+ this.walkMarkdownFiles((filePath) => {
429
+ try {
430
+ const doc = this.markdownService.parseFile(filePath);
431
+ const fmType = doc.frontmatter.entity_type;
432
+ const inferred = this.inferLegacyType(filePath);
433
+ const type = fmType || inferred;
434
+ if (!type)
435
+ return; // README, CONTRIBUTING, etc.
436
+ const id = filePath.split('/').pop().replace(/\.md$/, '');
437
+ index.set(`${type}/${id}`, filePath);
438
+ }
439
+ catch { /* skip unparsable */ }
440
+ });
441
+ this.fileIndex = index;
442
+ return index;
443
+ }
444
+ /**
445
+ * Infer entity type from a file's location for legacy layouts where the
446
+ * file sits directly inside a top-level plural-type directory
447
+ * ({repo}/meetings/foo.md). Returns undefined for any other layout.
448
+ */
449
+ inferLegacyType(absPath) {
450
+ const rel = relative(this.repoPath, absPath);
451
+ const parts = rel.split('/');
452
+ if (parts.length !== 2)
453
+ return undefined; // not at depth 1
454
+ return PLURAL_TO_SINGULAR[parts[0]];
455
+ }
456
+ /**
457
+ * Resolve an entity's absolute path by (type, id). Returns undefined if not
458
+ * found. Uses the file index — the index keys by `${entityType}/${entityId}`
459
+ * which is type-aware, unlike the on-disk filename. (We previously had a
460
+ * legacy {plural-type}/{id}.md fast path here; once the create path moved
461
+ * to flat repo-root layout, that fast path could return a different
462
+ * type's file at the same id.)
463
+ */
464
+ findEntityFile(entityType, entityId) {
465
+ return this.getFileIndex().get(`${entityType}/${entityId}`);
65
466
  }
66
467
  /**
67
468
  * Validate entity data against schema
@@ -80,12 +481,19 @@ export class BaseGraphManager {
80
481
  try {
81
482
  // Add entity_type and preprocess YAML parsing artefacts:
82
483
  // - null → undefined (Zod .optional() doesn't accept null)
484
+ // - empty string on a date field → undefined (the date vocabulary's
485
+ // `date` / `start_date` are `.date()`-typed and reject ''; old create
486
+ // code seeded '' for absent optional strings, so existing entities
487
+ // carry empty date fields. Coercing to undefined keeps their next
488
+ // update from failing validation and self-heals the stored frontmatter.)
83
489
  // - Date objects → ISO date string (unquoted YAML dates parse as Date)
84
490
  // - bare string for array field → wrap in array (single-value YAML list)
85
491
  const raw = { ...data, entity_type: entityType };
86
492
  const dataWithType = Object.fromEntries(Object.entries(raw).map(([k, v]) => {
87
493
  if (v === null)
88
494
  return [k, undefined];
495
+ if ((k === 'date' || k === 'start_date') && v === '')
496
+ return [k, undefined];
89
497
  if (typeof v === 'object' && v !== null && Object.prototype.toString.call(v) === '[object Date]')
90
498
  return [k, v.toISOString().split('T')[0]];
91
499
  // Coerce scalar string to array for known array-typed fields
@@ -107,11 +515,19 @@ export class BaseGraphManager {
107
515
  return [k, v.toLowerCase()];
108
516
  return [k, v];
109
517
  }));
110
- return schema.parse(dataWithType);
518
+ const result = schema.parse(dataWithType);
519
+ // Cross-field task rule (start <= due). Skipped in lenient mode, which
520
+ // exists to keep already-stored entities discoverable — we don't want a
521
+ // historical bad-ordering to make an entity unreadable. See
522
+ // assertValidTaskDates for why this isn't a Zod refinement.
523
+ if (!lenient && entityType === 'task') {
524
+ assertValidTaskDates(result);
525
+ }
526
+ return result;
111
527
  }
112
528
  catch (error) {
113
529
  if (error instanceof z.ZodError) {
114
- const messages = error.errors.map(e => `${e.path.join('.')}: ${e.message}`);
530
+ const messages = error.issues.map(e => `${e.path.join('.')}: ${e.message}`);
115
531
  throw new Error(`Validation failed for ${entityType}:\n${messages.join('\n')}`);
116
532
  }
117
533
  throw error;
@@ -121,12 +537,18 @@ export class BaseGraphManager {
121
537
  * Create a new entity
122
538
  */
123
539
  async create(options) {
124
- const { entityType, entityId, data, content = '', commitMessage, skipCommit, user } = options;
540
+ const { entityType, entityId, data, content = '', commitMessage, skipCommit, folderPath, user, skipVectorIndex } = options;
541
+ const normalizedFolder = folderPath ? validateFolderPath(folderPath) : '';
125
542
  // Prepopulate date from created_at if not provided
126
543
  if (!data.date && data.created_at) {
127
544
  data.date = String(data.created_at).split('T')[0];
128
545
  }
129
- // Add missing schema fields with empty defaults so the frontmatter is complete
546
+ // Add missing schema fields with empty defaults so the frontmatter is complete.
547
+ // Only string and array fields get populated — empty string isn't a valid
548
+ // member of number/boolean/enum/url types, and seeding them with '' caused
549
+ // optional numeric fields like `deal_value` to fail validation as
550
+ // "Expected number, received string". Anything that can't legitimately
551
+ // hold '' is left absent so the optional check passes.
130
552
  const schema = ENTITY_SCHEMAS[entityType];
131
553
  if (schema) {
132
554
  const shape = schema.shape;
@@ -134,37 +556,56 @@ export class BaseGraphManager {
134
556
  for (const key of Object.keys(shape)) {
135
557
  if (BASE_KEYS.has(key))
136
558
  continue;
559
+ // System-managed fields (source_file, indexable) are never seeded with
560
+ // an empty default — a directly-created entry has NO such field rather
561
+ // than a junk `''`. They're set only by the import/convert path.
562
+ if (isInternalField(shape[key]))
563
+ continue;
137
564
  if (!(key in data)) {
138
565
  const field = shape[key];
139
566
  // Unwrap ZodOptional/ZodDefault to get the inner type
140
567
  const inner = field instanceof z.ZodOptional ? field._def.innerType
141
568
  : 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
569
+ if (inner instanceof z.ZodArray) {
570
+ data[key] = [];
144
571
  continue;
145
572
  }
146
- // Don't populate URL-typed strings with '' — empty string fails URL validation.
147
- if (inner instanceof z.ZodString && inner._def.checks?.some((c) => c.kind === 'url')) {
573
+ if (inner instanceof z.ZodString) {
574
+ // Skip format-constrained strings empty string fails url/date/
575
+ // datetime validation (e.g. `start_date` is now `.date()`-typed).
576
+ const FORMATS = new Set(['url', 'date', 'datetime']);
577
+ if (inner._def.checks?.some((c) => FORMATS.has(c.def?.format)))
578
+ continue;
579
+ data[key] = '';
148
580
  continue;
149
581
  }
150
- const isArray = inner instanceof z.ZodArray;
151
- data[key] = isArray ? [] : '';
582
+ // Numbers, booleans, enums, literals, unions: leave absent.
152
583
  }
153
584
  }
154
585
  }
155
586
  // Validate data
156
587
  const validatedData = this.validateEntity(entityType, data);
157
- // Check if entity already exists
158
- const entityPath = this.getEntityPath(entityType, entityId);
159
- if (existsSync(entityPath)) {
588
+ // Reject if (type, id) is already taken anywhere in the repo — IDs are
589
+ // unique within (repo, type) regardless of folder.
590
+ const existingPath = this.findEntityFile(entityType, entityId);
591
+ if (existingPath) {
160
592
  throw new Error(`Entity already exists: ${entityType}/${entityId}`);
161
593
  }
594
+ // Compute target path. With folderPath set, entries land at
595
+ // {repo}/{folderPath}/{id}.md (parents auto-created); without, fall back
596
+ // to legacy {plural-type}/{id}.md.
597
+ const entityPath = this.getEntityPath(entityType, entityId, normalizedFolder);
598
+ mkdirSync(dirname(entityPath), { recursive: true });
162
599
  // Write markdown file
163
600
  this.markdownService.writeFile(entityPath, {
164
601
  frontmatter: validatedData,
165
602
  content,
166
603
  wikilinks: this.markdownService.extractWikilinks(content, validatedData),
167
604
  });
605
+ // The folder now has an .md child, so any pre-existing .gitkeep is
606
+ // redundant. Drop it as part of the same commit so the repo stays tidy.
607
+ const removedMarker = this.removeFolderMarker(dirname(entityPath));
608
+ this.invalidateIndex();
168
609
  if (this.gitService && !skipCommit) {
169
610
  const transaction = this.gitService.createTransaction();
170
611
  transaction.add({
@@ -173,21 +614,44 @@ export class BaseGraphManager {
173
614
  entityId,
174
615
  filePath: entityPath,
175
616
  });
176
- const message = commitMessage || `Create ${entityType}: ${entityId}`;
617
+ if (removedMarker) {
618
+ transaction.add({
619
+ operation: 'delete',
620
+ entityType,
621
+ entityId,
622
+ filePath: removedMarker,
623
+ });
624
+ }
625
+ const message = commitMessage || commitMsg.created(this.commitRef(entityType, entityId, data?.name));
177
626
  await transaction.commit(user ?? this.gitUser, message);
178
627
  }
179
628
  // Return created entity
180
629
  const created = this.get(entityType, entityId);
181
- // Update vector index (fire-and-forget, non-fatal)
182
- this.vectorService?.upsert(this.repoName, created).catch(err => console.warn(`[vector] index failed for ${entityType}/${entityId}:`, err.message));
630
+ // Update vector index (fire-and-forget, non-fatal). Failures are
631
+ // tracked on VectorService for /api/health visibility the audit
632
+ // (2026-05) flagged that silent .catch(console.warn) made a steady
633
+ // index-failure leak undetectable until search results went stale.
634
+ // `skipVectorIndex` opts out for the asset-upload stub create — see the
635
+ // CreateEntityOptions doc; the finalizing update is the single upsert.
636
+ if (this.vectorService && !skipVectorIndex) {
637
+ const v = this.vectorService;
638
+ v.beginOp();
639
+ v.upsert(this.repoName, created)
640
+ .then(() => v.recordSuccess())
641
+ .catch(err => {
642
+ v.recordFailure('upsert', `${entityType}/${entityId}`, err);
643
+ console.warn(`[vector] index failed for ${entityType}/${entityId}:`, err.message);
644
+ });
645
+ }
183
646
  return created;
184
647
  }
185
648
  /**
186
- * Get an entity by type and ID
649
+ * Get an entity by type and ID. Searches nested folders if the entry isn't
650
+ * at the legacy {plural-type}/{id}.md path.
187
651
  */
188
652
  get(entityType, entityId) {
189
- const entityPath = this.getEntityPath(entityType, entityId);
190
- if (!existsSync(entityPath)) {
653
+ const entityPath = this.findEntityFile(entityType, entityId);
654
+ if (!entityPath) {
191
655
  throw new Error(`Entity not found: ${entityType}/${entityId}`);
192
656
  }
193
657
  const document = this.markdownService.parseFile(entityPath);
@@ -209,113 +673,419 @@ export class BaseGraphManager {
209
673
  return {
210
674
  id: entityId,
211
675
  path: entityPath,
676
+ folder_path: folderPathFromFile(entityPath, this.repoPath),
212
677
  entityType,
213
678
  data: validatedData,
214
679
  document,
215
680
  };
216
681
  }
682
+ /**
683
+ * The live editor body for an entity when a seeded Y.Doc is open, else
684
+ * null. `get` reads disk, which lags the live Y.Doc by the autosave
685
+ * debounce + commit — so a user's freshly-placed image/asset embed exists
686
+ * in the editor before it reaches disk. Callers that must reflect what the
687
+ * user is actually looking at (the get_entity surfaces, the whole-body
688
+ * asset-drop guard) prefer this and fall back to disk on null.
689
+ *
690
+ * Returns null when no coordinator is wired (CLI / test fixtures) — those
691
+ * setups have no live editor layer, so disk IS the authoritative body.
692
+ */
693
+ getLiveBody(entityType, entityId) {
694
+ if (!this.bodyWriteCoordinator?.peekLiveBody)
695
+ return null;
696
+ return this.bodyWriteCoordinator.peekLiveBody({
697
+ repo: this.repoName,
698
+ entityType,
699
+ entityId,
700
+ });
701
+ }
217
702
  /**
218
703
  * Get an entity by ID only (type resolved from the filesystem).
219
704
  *
220
- * Scans every plural-type subdirectory under this repo for `{id}.md`.
221
- * Bounded by the number of entity types (~17), not the number of entities,
222
- * so still fast. Previously this computed `{repo}/s/{id}.md` by passing an
223
- * empty entity type to getEntityPath, which meant getById never found
224
- * anything — the agent's `get_entity` tool would silently fail when
225
- * called without an explicit type, producing the "search finds it but
226
- * get_entity can't resolve it" symptom.
705
+ * Walks the file index to find any entry with this id, regardless of folder
706
+ * depth or type. Used by the agent's `get_entity` tool when the caller
707
+ * doesn't know the entity's type.
227
708
  */
228
709
  getById(entityId) {
229
710
  if (!existsSync(this.repoPath)) {
230
711
  throw new Error(`Entity not found: ${entityId}`);
231
712
  }
232
- for (const [plural, singular] of Object.entries(PLURAL_TO_SINGULAR)) {
233
- const candidate = join(this.repoPath, plural, `${entityId}.md`);
234
- if (existsSync(candidate)) {
235
- return this.get(singular, entityId);
713
+ for (const [key] of this.getFileIndex()) {
714
+ const slash = key.indexOf('/');
715
+ if (slash === -1)
716
+ continue;
717
+ const id = key.slice(slash + 1);
718
+ if (id === entityId) {
719
+ const type = key.slice(0, slash);
720
+ return this.get(type, entityId);
236
721
  }
237
722
  }
238
723
  throw new Error(`Entity not found: ${entityId}`);
239
724
  }
725
+ /**
726
+ * Get an entity by its file path — repo-relative (as returned by the
727
+ * search/list tools) or absolute. The most direct resolver: the caller
728
+ * never needs the canonical type, which closes the search→get_entity
729
+ * round-trip for entries whose frontmatter `type` field differs from
730
+ * `entity_type` (e.g. an `event` carrying `type: meeting`). The path is
731
+ * contained to this repo — a `..` escape or out-of-repo path is rejected
732
+ * as not-found rather than read.
733
+ */
734
+ getByPath(filePath) {
735
+ const abs = isAbsolute(filePath) ? filePath : join(this.repoPath, filePath);
736
+ const rel = relative(this.repoPath, abs);
737
+ if (rel === '' || rel.startsWith('..') || isAbsolute(rel)) {
738
+ throw new Error(`Entity not found: ${filePath}`);
739
+ }
740
+ if (!existsSync(abs)) {
741
+ throw new Error(`Entity not found: ${filePath}`);
742
+ }
743
+ const document = this.markdownService.parseFile(abs);
744
+ const entityType = (document.frontmatter.entity_type
745
+ || this.inferLegacyType(abs)
746
+ || 'note');
747
+ const entityId = abs.split('/').pop().replace(/\.md$/, '');
748
+ let validatedData;
749
+ try {
750
+ validatedData = this.validateEntity(entityType, document.frontmatter);
751
+ }
752
+ catch {
753
+ try {
754
+ validatedData = this.validateEntity(entityType, document.frontmatter, true);
755
+ }
756
+ catch {
757
+ validatedData = document.frontmatter;
758
+ }
759
+ }
760
+ return {
761
+ id: entityId,
762
+ path: abs,
763
+ folder_path: folderPathFromFile(abs, this.repoPath),
764
+ entityType,
765
+ data: validatedData,
766
+ document,
767
+ };
768
+ }
769
+ /**
770
+ * Merge a caller's `data` onto a base frontmatter, always stamping
771
+ * `updated_at` / `updated_by`, then validate. Even content-only edits
772
+ * (editor autosave, where `data` is undefined) re-stamp the audit fields —
773
+ * otherwise the trail freezes at whatever the last data-change user was,
774
+ * which for scaffolded files is "studiograph".
775
+ *
776
+ * `baseFrontmatter` is the frontmatter to merge onto. The body-write path
777
+ * passes the CURRENT on-disk frontmatter (read inside the per-doc lock) so
778
+ * a concurrent metadata-only edit isn't clobbered; the frontmatter-only
779
+ * path passes `existing.data` (synchronous, no lock window).
780
+ *
781
+ * Validation uses lenient mode + a Date-coercion fallback when no new data
782
+ * is supplied, since the existing entity may have been stored without all
783
+ * required fields, and gray-matter would otherwise serialize Date objects
784
+ * as full ISO timestamps.
785
+ */
786
+ composeUpdatedFrontmatter(entityType, baseFrontmatter, data, user) {
787
+ const updater = (user ?? this.gitUser).id;
788
+ const now = new Date().toISOString();
789
+ const mergedData = data
790
+ ? { ...baseFrontmatter, ...data, entity_type: entityType, updated_at: now, updated_by: updater }
791
+ : { ...baseFrontmatter, updated_at: now, updated_by: updater };
792
+ // JSON Merge Patch convention: an incoming `null` means "delete this field",
793
+ // not "store null". Updates MERGE onto the existing frontmatter, so omitting
794
+ // a key can't clear it — the merge keeps the old value. A caller (the web
795
+ // editor's "Clear" button, MCP/agent field deletion) sends `null` to remove
796
+ // it. `null` was never a valid stored value (schemas use `.optional()`, not
797
+ // `.nullable()`), so stripping it here only adds a deletion path and can't
798
+ // discard a meaningful value. Done before validation so a removed required
799
+ // field surfaces as the normal "missing field" error.
800
+ const deletedKeys = [];
801
+ if (data) {
802
+ for (const [key, value] of Object.entries(data)) {
803
+ if (value === null) {
804
+ delete mergedData[key];
805
+ deletedKeys.push(key);
806
+ }
807
+ }
808
+ }
809
+ // Task completion stamp: when a data change moves status to/from `done`,
810
+ // manage `completed_at` here so every surface (app, MCP, agent) gets it
811
+ // for free. Only on a real transition, and never when the caller supplied
812
+ // completed_at explicitly (respect their intent). Skipped for content-only
813
+ // edits (data undefined) so autosave never touches it.
814
+ if (data && entityType === 'task' && !Object.prototype.hasOwnProperty.call(data, 'completed_at')) {
815
+ const priorStatus = baseFrontmatter.status;
816
+ const newStatus = mergedData.status;
817
+ if (newStatus === 'done' && priorStatus !== 'done') {
818
+ mergedData.completed_at = now;
819
+ }
820
+ else if (newStatus !== 'done' && 'completed_at' in mergedData) {
821
+ delete mergedData.completed_at;
822
+ }
823
+ }
824
+ if (data) {
825
+ const validated = this.validateEntity(entityType, mergedData);
826
+ // A deletion key that reappears after validation was re-supplied by a
827
+ // schema `.default()` (e.g. task.status → 'backlog', event.type →
828
+ // 'meeting'). The field can't actually be cleared — its absence is
829
+ // re-materialized on every read. Reject loudly rather than silently
830
+ // resetting it to the default (and firing side effects like the
831
+ // completed_at stamp above, which never lands because we throw here).
832
+ // `hasOwnProperty`, not `in`: schemas are `.passthrough()`, so a custom
833
+ // field literally named `constructor`/`toString`/etc. is clearable, but
834
+ // `in` would match it on the prototype chain and wrongly reject it.
835
+ const undeletable = deletedKeys.filter((k) => Object.prototype.hasOwnProperty.call(validated, k));
836
+ if (undeletable.length > 0) {
837
+ throw new Error(`Cannot clear field${undeletable.length > 1 ? 's' : ''} ` +
838
+ `${undeletable.join(', ')} on ${entityType}: ` +
839
+ `${undeletable.length > 1 ? 'they have' : 'it has'} a default value.`);
840
+ }
841
+ return validated;
842
+ }
843
+ try {
844
+ return this.validateEntity(entityType, mergedData);
845
+ }
846
+ catch {
847
+ try {
848
+ return this.validateEntity(entityType, mergedData, true);
849
+ }
850
+ catch {
851
+ const coerced = Object.fromEntries(Object.entries(mergedData).map(([k, v]) => {
852
+ if (v !== null && typeof v === 'object' && Object.prototype.toString.call(v) === '[object Date]') {
853
+ return [k, v.toISOString().split('T')[0]];
854
+ }
855
+ return [k, v];
856
+ }));
857
+ return coerced;
858
+ }
859
+ }
860
+ }
240
861
  /**
241
862
  * Update an existing entity
242
863
  */
243
864
  async update(options) {
244
- const { entityType, entityId, data, content, commitMessage, skipCommit, user } = options;
865
+ const { entityType, entityId, data, content, commitMessage, skipCommit, user, source, baseRevision } = options;
866
+ // Hard contract: body writes must declare provenance. A missing source
867
+ // used to warn-and-default to 'server-write', but a warning leaves a
868
+ // bypass-shaped footpath for future callers. Every production caller
869
+ // (autosave PUT, event processor, entity-mutations helper) already
870
+ // declares it; throw so a new body-write surface can't silently drift
871
+ // back into ambiguity.
872
+ if (content !== undefined && source === undefined) {
873
+ throw new Error(`update() for ${entityType}/${entityId} passed content without a source. ` +
874
+ `Body writes must declare source: 'autosave' | 'server-write'.`);
875
+ }
245
876
  // Get existing entity
246
877
  const existing = this.get(entityType, entityId);
247
- // Merge data (if provided) and add update metadata
248
- const mergedData = data
249
- ? {
250
- ...existing.data,
251
- ...data,
252
- entity_type: entityType,
253
- updated_at: new Date().toISOString(),
254
- updated_by: (user ?? this.gitUser).id,
255
- }
256
- : existing.data;
257
- // Validate merged data — use lenient validation when no new data is provided,
258
- // since the existing entity may have been stored without all required fields.
259
- // Raw fallback coerces Date objects to YYYY-MM-DD so gray-matter doesn't
260
- // serialize them as full ISO timestamps.
261
- let validatedData;
262
- if (data) {
263
- validatedData = this.validateEntity(entityType, mergedData);
878
+ // Update in place write to wherever the entry already lives, not
879
+ // wherever a fresh getEntityPath() computation would put it. Otherwise
880
+ // an entry in a nested folder gets duplicated to the legacy plural-type
881
+ // dir on first update.
882
+ const entityPath = existing.path;
883
+ if (content !== undefined && this.bodyWriteCoordinator) {
884
+ // Body write: delegate the critical section to the coordinator.
885
+ // assertWritable, per-doc lock, base-hash precondition, atomic write,
886
+ // commit, dirty-recovery, and post-commit applyAgentContent all live
887
+ // there now.
888
+ //
889
+ // Frontmatter is merged + validated INSIDE the coordinator's per-doc
890
+ // lock (via prepareFrontmatter) against the CURRENT on-disk
891
+ // frontmatter not a snapshot read here, before the lock. Editor
892
+ // autosave sends `{ content }` only; if we merged against a pre-lock
893
+ // snapshot, a metadata-only edit that commits while this write waits
894
+ // for the lock would be silently clobbered by the stale snapshot.
895
+ const result = await this.bodyWriteCoordinator.writeBody({ repo: this.repoName, entityType, entityId, entityPath }, content, {
896
+ prepareFrontmatter: (currentDiskFrontmatter) => this.composeUpdatedFrontmatter(entityType, currentDiskFrontmatter, data, user),
897
+ baseHash: baseRevision,
898
+ // Guaranteed defined: the guard at the top throws when content is
899
+ // present without a source, and this branch requires content.
900
+ source: source,
901
+ actor: user ?? this.gitUser,
902
+ skipCommit,
903
+ // Default to the friendly "Edited …" subject so server-write body
904
+ // edits (MCP/agent) match create/update/etc. The skipCommit path
905
+ // ignores this and defers to the SessionManager's batch message.
906
+ commitMessage: commitMessage ??
907
+ commitMsg.edited(this.commitRef(entityType, entityId, data?.name ?? existing.data?.name)),
908
+ });
909
+ options.onBroadcast?.(result.broadcast);
264
910
  }
265
911
  else {
266
- try {
267
- validatedData = this.validateEntity(entityType, mergedData);
912
+ // Frontmatter-only write (content === undefined) OR a test fixture
913
+ // running without a coordinator. No concurrent-lock window here: the
914
+ // read-merge-write below is synchronous, so it can't interleave with
915
+ // the coordinator's (also synchronous) critical section.
916
+ const validatedData = this.composeUpdatedFrontmatter(entityType, existing.data, data, user);
917
+ const mergedContent = content !== undefined ? content : existing.document.content;
918
+ // baseRevision is meaningless without a body change; coordinator-less
919
+ // setups exist solely for tests that don't exercise the autosave 412
920
+ // contract.
921
+ if (baseRevision !== undefined && content !== undefined) {
922
+ const currentBody = this.markdownService.parseFile(entityPath).content;
923
+ const currentHash = sha256OfBody(currentBody);
924
+ if (currentHash !== baseRevision) {
925
+ throw new StaleBaseRevisionError(entityType, entityId, baseRevision, currentHash);
926
+ }
268
927
  }
269
- catch {
928
+ this.markdownService.writeFile(entityPath, {
929
+ frontmatter: validatedData,
930
+ content: mergedContent,
931
+ wikilinks: this.markdownService.extractWikilinks(mergedContent, validatedData),
932
+ });
933
+ if (this.gitService && !skipCommit) {
934
+ const transaction = this.gitService.createTransaction();
935
+ transaction.add({
936
+ operation: 'update',
937
+ entityType,
938
+ entityId,
939
+ filePath: entityPath,
940
+ });
941
+ const message = commitMessage || commitMsg.edited(this.commitRef(entityType, entityId, validatedData?.name));
270
942
  try {
271
- validatedData = this.validateEntity(entityType, mergedData, true);
943
+ await transaction.commit(user ?? this.gitUser, message);
272
944
  }
273
- catch {
274
- const coerced = Object.fromEntries(Object.entries(mergedData).map(([k, v]) => {
275
- if (v !== null && typeof v === 'object' && Object.prototype.toString.call(v) === '[object Date]') {
276
- return [k, v.toISOString().split('T')[0]];
277
- }
278
- return [k, v];
279
- }));
280
- validatedData = coerced;
945
+ catch (err) {
946
+ throw new CommitFailedDirtyError(entityType, entityId, entityPath, err);
281
947
  }
282
948
  }
283
949
  }
284
- // Merge content (if provided)
285
- const mergedContent = content !== undefined ? content : existing.document.content;
286
- // Write updated markdown file
287
- const entityPath = this.getEntityPath(entityType, entityId);
288
- this.markdownService.writeFile(entityPath, {
289
- frontmatter: validatedData,
290
- content: mergedContent,
291
- wikilinks: this.markdownService.extractWikilinks(mergedContent, validatedData),
292
- });
293
- if (this.gitService && !skipCommit) {
294
- const transaction = this.gitService.createTransaction();
295
- transaction.add({
296
- operation: 'update',
297
- entityType,
298
- entityId,
299
- filePath: entityPath,
300
- });
301
- const message = commitMessage || `Update ${entityType}: ${entityId}`;
302
- await transaction.commit(user ?? this.gitUser, message);
303
- }
950
+ // The on-disk frontmatter has changed, so the parsed-EntityFile cache
951
+ // that list() serves from is stale until invalidated. Without this, the
952
+ // next list_entities call returns pre-update frontmatter while
953
+ // get_entity (which reads fresh from disk) returns post-update — every
954
+ // other mutation in this file already invalidates after its write.
955
+ this.invalidateIndex();
304
956
  // Return updated entity
305
957
  const updated = this.get(entityType, entityId);
306
- // Update vector index (fire-and-forget, non-fatal)
307
- this.vectorService?.upsert(this.repoName, updated).catch(err => console.warn(`[vector] index failed for ${entityType}/${entityId}:`, err.message));
958
+ // Update vector index (fire-and-forget, non-fatal). Tracked on the
959
+ // service so /api/health surfaces a steady failure stream instead of
960
+ // burying it in stdout warnings.
961
+ if (this.vectorService) {
962
+ const v = this.vectorService;
963
+ v.beginOp();
964
+ v.upsert(this.repoName, updated)
965
+ .then(() => v.recordSuccess())
966
+ .catch(err => {
967
+ v.recordFailure('upsert', `${entityType}/${entityId}`, err);
968
+ console.warn(`[vector] index failed for ${entityType}/${entityId}:`, err.message);
969
+ });
970
+ }
308
971
  return updated;
309
972
  }
310
973
  /**
311
974
  * Delete an entity
312
975
  */
976
+ /**
977
+ * Delete multiple entities in a single git transaction. Each entity's
978
+ * file is removed from disk; folder markers (.gitkeep) are added where
979
+ * the deletion left a folder empty. Then everything commits as one
980
+ * atomic operation.
981
+ *
982
+ * Compared to looping `delete()` per entity, this is a real performance
983
+ * primitive: one fork of git, one index update, one commit, one ref
984
+ * advance — instead of N. Bulk delete of a few hundred entities goes
985
+ * from minutes to seconds.
986
+ *
987
+ * Errors per entity (e.g. "not found") don't abort the batch — they're
988
+ * collected in `errors` and the rest still commit. Returns both the
989
+ * successfully-deleted set (for vector cleanup + WS broadcast) and the
990
+ * errors so the caller can surface partial failures.
991
+ */
992
+ async bulkDelete(specs, options) {
993
+ const deleted = [];
994
+ const errors = [];
995
+ const transaction = this.gitService?.createTransaction();
996
+ for (const { entityType, entityId } of specs) {
997
+ try {
998
+ const existing = this.get(entityType, entityId);
999
+ rmSync(existing.path, { force: true });
1000
+ // Folder marker if the deletion emptied the folder — same lifecycle
1001
+ // semantics as single delete().
1002
+ const addedMarker = this.ensureFolderMarker(dirname(existing.path));
1003
+ if (transaction) {
1004
+ transaction.add({ operation: 'delete', entityType, entityId, filePath: existing.path });
1005
+ if (addedMarker) {
1006
+ transaction.add({ operation: 'create', entityType, entityId, filePath: addedMarker });
1007
+ }
1008
+ }
1009
+ deleted.push({ entityType, entityId, path: existing.path, title: existing.data?.name });
1010
+ }
1011
+ catch (err) {
1012
+ errors.push({
1013
+ entityType,
1014
+ entityId,
1015
+ error: err instanceof Error ? err.message : String(err),
1016
+ });
1017
+ }
1018
+ }
1019
+ this.invalidateIndex();
1020
+ if (transaction && deleted.length > 0) {
1021
+ const message = options?.commitMessage
1022
+ ?? commitMsg.deletedBatch(deleted.map(d => this.commitRef(d.entityType, d.entityId, d.title)));
1023
+ await transaction.commit(options?.user ?? this.gitUser, message);
1024
+ }
1025
+ // No asset cleanup: deleting entries keeps their assets (assets-
1026
+ // referenced-not-owned P1 — same semantics as single delete()).
1027
+ // Vector index cleanup, fire-and-forget (matches single delete() behavior).
1028
+ // Failures recorded on VectorService for /api/health visibility.
1029
+ for (const { entityType, entityId } of deleted) {
1030
+ if (!this.vectorService)
1031
+ continue;
1032
+ const v = this.vectorService;
1033
+ v.beginOp();
1034
+ v.delete(this.repoName, entityType, entityId)
1035
+ .then(() => v.recordSuccess())
1036
+ .catch(err => {
1037
+ v.recordFailure('delete', `${entityType}/${entityId}`, err);
1038
+ console.warn(`[vector] delete failed for ${entityType}/${entityId}:`, err.message);
1039
+ });
1040
+ }
1041
+ return { deleted, errors };
1042
+ }
1043
+ /**
1044
+ * Commit a set of already-on-disk file changes as a single git
1045
+ * operation. Used by callers (e.g. the import route) that perform many
1046
+ * `skipCommit: true` writes and want one transaction at the end instead
1047
+ * of per-file commits.
1048
+ *
1049
+ * `entries` describes what each file represents so the transaction can
1050
+ * be staged correctly. No-op when nothing is staged or git isn't
1051
+ * available.
1052
+ */
1053
+ async commitPending(entries, options) {
1054
+ if (!this.gitService)
1055
+ return { committed: false };
1056
+ if (entries.length === 0 && !options.extraPaths?.length)
1057
+ return { committed: false };
1058
+ const transaction = this.gitService.createTransaction();
1059
+ for (const e of entries)
1060
+ transaction.add(e);
1061
+ // Auxiliary paths (folder-display sidecars from the import flow) are
1062
+ // staged alongside the entity files so they land in the same commit.
1063
+ // The transaction only uses `filePath` for staging — the other fields
1064
+ // are metadata, so we fill them with sentinel values.
1065
+ if (options.extraPaths?.length) {
1066
+ for (const filePath of options.extraPaths) {
1067
+ transaction.add({
1068
+ operation: 'create',
1069
+ entityType: '_sidecar',
1070
+ entityId: '_sidecar',
1071
+ filePath,
1072
+ });
1073
+ }
1074
+ }
1075
+ await transaction.commit(options.user ?? this.gitUser, options.commitMessage);
1076
+ return { committed: true };
1077
+ }
313
1078
  async delete(entityType, entityId, commitMessage, user) {
314
1079
  // Verify entity exists
315
1080
  const existing = this.get(entityType, entityId);
316
1081
  // Delete the flat entity file
317
- const { rmSync } = await import('fs');
318
1082
  rmSync(existing.path, { force: true });
1083
+ // Folder lifecycle: deleting an entry never deletes its containing folder.
1084
+ // If this was the last .md in the folder, write a .gitkeep so it stays
1085
+ // visible to listFolders() (and therefore to the sidebar). A user who
1086
+ // wants the folder gone calls deleteFolder() explicitly.
1087
+ const addedMarker = this.ensureFolderMarker(dirname(existing.path));
1088
+ this.invalidateIndex();
319
1089
  if (this.gitService) {
320
1090
  const transaction = this.gitService.createTransaction();
321
1091
  transaction.add({
@@ -324,11 +1094,45 @@ export class BaseGraphManager {
324
1094
  entityId,
325
1095
  filePath: existing.path,
326
1096
  });
327
- const message = commitMessage || `Delete ${entityType}: ${entityId}`;
1097
+ if (addedMarker) {
1098
+ transaction.add({
1099
+ operation: 'create',
1100
+ entityType,
1101
+ entityId,
1102
+ filePath: addedMarker,
1103
+ });
1104
+ }
1105
+ const message = commitMessage || commitMsg.deleted(this.commitRef(entityType, entityId, existing.data?.name));
328
1106
  await transaction.commit(user ?? this.gitUser, message);
329
1107
  }
330
- // Remove from vector index (fire-and-forget, non-fatal)
331
- this.vectorService?.delete(this.repoName, entityType, entityId).catch(err => console.warn(`[vector] delete failed for ${entityType}/${entityId}:`, err.message));
1108
+ // Assets are never deleted here: assets are library citizens (folder/
1109
+ // workspace-homed) that entries only REFERENCE, so entry deletion never
1110
+ // destroys binaries. Deliberate asset deletion is by-id (deleteAssetById).
1111
+ // Remove from vector index (fire-and-forget, non-fatal). Tracked on
1112
+ // the service so a leaking error stream is visible at /api/health.
1113
+ if (this.vectorService) {
1114
+ const v = this.vectorService;
1115
+ v.beginOp();
1116
+ v.delete(this.repoName, entityType, entityId)
1117
+ .then(() => v.recordSuccess())
1118
+ .catch(err => {
1119
+ v.recordFailure('delete', `${entityType}/${entityId}`, err);
1120
+ console.warn(`[vector] delete failed for ${entityType}/${entityId}:`, err.message);
1121
+ });
1122
+ }
1123
+ // Notify Yjs persistence. Awaited (not fire-and-forget) because docName
1124
+ // reuse is a real scenario: delete X then create a new entity at X. If the
1125
+ // delete callback fired async, it could race the new entity's snapshot and
1126
+ // unlink it after creation. Failures are logged but don't throw — orphaned
1127
+ // snapshot files are a leak, not a correctness issue.
1128
+ if (this.persistenceCallbacks?.onDelete) {
1129
+ try {
1130
+ await this.persistenceCallbacks.onDelete(this.repoName, entityType, entityId);
1131
+ }
1132
+ catch (err) {
1133
+ console.warn(`[graph] onDelete callback failed for ${entityType}/${entityId}:`, err);
1134
+ }
1135
+ }
332
1136
  }
333
1137
  /**
334
1138
  * Rename an entity (change its ID). Preserves all data, content, and source_ref.
@@ -336,7 +1140,8 @@ export class BaseGraphManager {
336
1140
  */
337
1141
  async rename(entityType, oldId, newId, commitMessage, user) {
338
1142
  const existing = this.get(entityType, oldId);
339
- const newPath = this.getEntityPath(entityType, newId);
1143
+ // Rename keeps the entry in its current folder — only the filename changes.
1144
+ const newPath = join(dirname(existing.path), `${newId}.md`);
340
1145
  if (existsSync(newPath)) {
341
1146
  throw new Error(`Entity ${entityType}/${newId} already exists`);
342
1147
  }
@@ -350,33 +1155,61 @@ export class BaseGraphManager {
350
1155
  // Remove old file
351
1156
  const { rmSync } = await import('fs');
352
1157
  rmSync(existing.path, { force: true });
353
- // Git: stage the rename and commit directly (mirrors changeType). The
354
- // older transaction-based path never called transaction.add() and threw
355
- // "No changes to commit" every time.
1158
+ this.invalidateIndex();
356
1159
  if (this.gitService) {
357
- const { spawnSync } = await import('child_process');
358
- const gitUser = user ?? this.gitUser;
359
- const message = commitMessage || `Rename ${entityType}: ${oldId} → ${newId}`;
360
- spawnSync('git', ['add', newPath], { cwd: this.repoPath, stdio: 'ignore' });
361
- spawnSync('git', ['add', existing.path], { cwd: this.repoPath, stdio: 'ignore' });
362
- spawnSync('git', [
363
- '-c', `user.name=${gitUser.name}`,
364
- '-c', `user.email=${gitUser.email}`,
365
- 'commit', '-m', message,
366
- ], { cwd: this.repoPath, stdio: 'ignore' });
367
- }
368
- // Update vector index
369
- this.vectorService?.delete(this.repoName, entityType, oldId).catch(() => { });
1160
+ const message = commitMessage || commitMsg.renamed(this.typeLabel(entityType), oldId, newId);
1161
+ this.gitService.commitFiles([existing.path, newPath], message, user ?? this.gitUser);
1162
+ }
1163
+ // Re-point asset ownership to the new id. Keys are immutable, so no blob
1164
+ // moves and the embed URLs in the body keep resolving — this just keeps
1165
+ // listByOwner/delete/transfer correct after the rename.
1166
+ this.reassignOwnedAssets(entityType, oldId, { ownerId: newId });
1167
+ // Update vector index — failure tracked on the service so a steady
1168
+ // leak surfaces at /api/health rather than getting swallowed entirely.
1169
+ if (this.vectorService) {
1170
+ const v = this.vectorService;
1171
+ v.beginOp();
1172
+ v.delete(this.repoName, entityType, oldId)
1173
+ .then(() => v.recordSuccess())
1174
+ .catch(err => v.recordFailure('delete', `${entityType}/${oldId}`, err));
1175
+ }
1176
+ // Notify Yjs persistence to rename the snapshot file alongside. Awaited
1177
+ // because rename oldId→newId followed by creating a new entity at oldId
1178
+ // would otherwise let the async rename clobber the new oldId snapshot.
1179
+ if (this.persistenceCallbacks?.onRename) {
1180
+ try {
1181
+ await this.persistenceCallbacks.onRename(this.repoName, entityType, oldId, newId);
1182
+ }
1183
+ catch (err) {
1184
+ console.warn(`[graph] onRename callback failed for ${entityType}/${oldId}→${newId}:`, err);
1185
+ }
1186
+ }
370
1187
  return this.get(entityType, newId);
371
1188
  }
372
1189
  /**
373
- * Change an entity's type. Moves the file from oldType/ to newType/ directory.
374
- * Preserves all data and content. Updates entity_type in frontmatter.
1190
+ * Change an entity's type. Updates frontmatter only the file stays in
1191
+ * its current folder. In the type-agnostic folder model, type is a
1192
+ * frontmatter property independent of location. (Legacy behavior moved
1193
+ * the file between {plural-type}/ dirs; that's no longer correct because
1194
+ * those dirs are now ordinary user-visible folders, not type markers.)
1195
+ *
1196
+ * If the user wants the file to also move (e.g. out of a "meetings/"
1197
+ * folder when changed to a note), they invoke move() separately.
375
1198
  */
376
1199
  async changeType(entityId, fromType, toType, commitMessage, user) {
1200
+ // Don't let change-type re-mint a retired type (`asset`, Phase F). Without
1201
+ // this, `toType: 'asset'` would fall back to EntityBaseSchema and write a
1202
+ // base-only stub of exactly the type we removed. (Only the retired-type
1203
+ // reservation is enforced here; general unknown-target validation incl.
1204
+ // custom types stays a route-level concern.)
1205
+ if (RESERVED_ENTITY_TYPES.has(toType)) {
1206
+ throw new Error(`Cannot change_type to '${toType}' — that entity type was retired.`);
1207
+ }
377
1208
  const existing = this.get(fromType, entityId);
378
- const newPath = this.getEntityPath(toType, entityId);
379
- if (existsSync(newPath)) {
1209
+ // (type, id) uniqueness check — if an entry with the new type already
1210
+ // exists with this id, refuse rather than silently colliding in the index.
1211
+ const collision = this.findEntityFile(toType, entityId);
1212
+ if (collision && collision !== existing.path) {
380
1213
  throw new Error(`Entity ${toType}/${entityId} already exists`);
381
1214
  }
382
1215
  // Read current content
@@ -405,6 +1238,10 @@ export class BaseGraphManager {
405
1238
  for (const key of Object.keys(shape)) {
406
1239
  if (BASE_KEYS.has(key))
407
1240
  continue;
1241
+ // Don't seed system-managed fields (source_file, indexable) — same
1242
+ // rule as the create path.
1243
+ if (isInternalField(shape[key]))
1244
+ continue;
408
1245
  if (!(key in doc.frontmatter)) {
409
1246
  const field = shape[key];
410
1247
  const inner = field instanceof z.ZodOptional ? field._def.innerType
@@ -416,70 +1253,415 @@ export class BaseGraphManager {
416
1253
  }
417
1254
  }
418
1255
  }
419
- // Ensure target directory exists and write new file
420
- mkdirSync(dirname(newPath), { recursive: true });
421
- this.markdownService.writeFile(newPath, doc);
422
- // Remove old file
423
- const { rmSync } = await import('fs');
424
- rmSync(existing.path, { force: true });
425
- // Git: stage the move and commit directly
1256
+ // Write back to the same path file stays in its current folder.
1257
+ this.markdownService.writeFile(existing.path, doc);
1258
+ this.invalidateIndex();
426
1259
  if (this.gitService) {
427
- const { spawnSync } = await import('child_process');
428
- const gitUser = user ?? this.gitUser;
429
- const message = commitMessage || `Change type: ${entityId} (${fromType} → ${toType})`;
430
- spawnSync('git', ['add', newPath], { cwd: this.repoPath, stdio: 'ignore' });
431
- spawnSync('git', ['add', existing.path], { cwd: this.repoPath, stdio: 'ignore' });
432
- spawnSync('git', [
433
- '-c', `user.name=${gitUser.name}`,
434
- '-c', `user.email=${gitUser.email}`,
435
- 'commit', '-m', message,
436
- ], { cwd: this.repoPath, stdio: 'ignore' });
437
- }
438
- // Update vector index: remove old entry, re-index at new type
1260
+ const message = commitMessage || commitMsg.changedType(this.commitRef(fromType, entityId, existing.data?.name), this.typeLabel(fromType), this.typeLabel(toType));
1261
+ this.gitService.commitFiles([existing.path], message, user ?? this.gitUser);
1262
+ }
1263
+ // Re-point asset ownership to the new type (keys immutable; no blob moves).
1264
+ // The `asset` entity type was retired (Phase F), so no entity carries a PDF
1265
+ // text sidecar keyed under assets-text/asset/ — nothing to move here.
1266
+ this.reassignOwnedAssets(fromType, entityId, { ownerType: toType });
1267
+ // Update vector index: remove old entry, re-index at new type. Both ops
1268
+ // tracked on the service so /api/health surfaces a leaking failure
1269
+ // stream previous .catch(() => {}) form was completely silent.
439
1270
  const updated = this.get(toType, entityId);
440
- this.vectorService?.delete(this.repoName, fromType, entityId).catch(() => { });
441
- this.vectorService?.upsert(this.repoName, updated).catch(() => { });
1271
+ if (this.vectorService) {
1272
+ const v = this.vectorService;
1273
+ v.beginOp();
1274
+ v.delete(this.repoName, fromType, entityId)
1275
+ .then(() => v.recordSuccess())
1276
+ .catch(err => v.recordFailure('delete', `${fromType}/${entityId}`, err));
1277
+ v.beginOp();
1278
+ v.upsert(this.repoName, updated)
1279
+ .then(() => v.recordSuccess())
1280
+ .catch(err => v.recordFailure('upsert', `${toType}/${entityId}`, err));
1281
+ }
1282
+ // Notify Yjs persistence to move the snapshot from the old type's path to
1283
+ // the new one (the docName changes, so the snapshot path changes). Awaited
1284
+ // for the same reason as rename — preserves ordering when types are
1285
+ // changed back and forth quickly.
1286
+ if (this.persistenceCallbacks?.onChangeType) {
1287
+ try {
1288
+ await this.persistenceCallbacks.onChangeType(this.repoName, entityId, fromType, toType);
1289
+ }
1290
+ catch (err) {
1291
+ console.warn(`[graph] onChangeType callback failed for ${entityId} (${fromType}→${toType}):`, err);
1292
+ }
1293
+ }
442
1294
  return updated;
443
1295
  }
444
1296
  /**
445
- * Enumerate the distinct entity types present in this repo, without
446
- * parsing any markdown files. Derived from the plural-named subdirectories
447
- * directly (e.g. `meetings/`, `projects/`). Used by /api/repos to let the
448
- * sidebar render the Views list without a full entity fetch per folder.
1297
+ * Move an entry to a different folder within the same repo. The filename
1298
+ * (and therefore the entity id) and type are unchanged. Auto-creates parent
1299
+ * folders. Refuses if the target path already has an entry with this id —
1300
+ * pass `overwrite: true` to replace it, but only when the occupant is
1301
+ * itself a parseable entity (never silently clobber arbitrary files like
1302
+ * README.md or .gitkeep).
449
1303
  */
450
- listEntityTypes() {
451
- if (!existsSync(this.repoPath))
452
- return [];
453
- const types = new Set();
454
- try {
455
- for (const entry of readdirSync(this.repoPath)) {
456
- if (entry.startsWith('.'))
457
- continue;
458
- const fullPath = join(this.repoPath, entry);
459
- try {
460
- if (!statSync(fullPath).isDirectory())
461
- continue;
462
- }
463
- catch {
464
- continue;
465
- }
466
- const singularType = PLURAL_TO_SINGULAR[entry];
467
- if (singularType)
468
- types.add(singularType);
1304
+ async move(entityType, entityId, targetFolderPath, commitMessage, user, overwrite) {
1305
+ const existing = this.get(entityType, entityId);
1306
+ const normalized = normalizeFolderPath(targetFolderPath);
1307
+ const newPath = join(this.repoPath, normalized, `${entityId}.md`);
1308
+ if (newPath === existing.path)
1309
+ return existing; // no-op
1310
+ if (existsSync(newPath)) {
1311
+ if (!overwrite) {
1312
+ throw new Error(`Path already occupied: ${normalized}/${entityId}.md`);
1313
+ }
1314
+ // Safety guard: only overwrite when the destination is itself an
1315
+ // entity .md (parseable frontmatter or legacy plural-type dir). Refuse
1316
+ // to clobber README.md, CHANGELOG.md, .gitkeep, or anything else that
1317
+ // isn't a tracked entry — that would be data loss the caller didn't
1318
+ // ask for.
1319
+ const occupant = this.identifyEntityAtPath(newPath);
1320
+ if (!occupant) {
1321
+ throw new Error(`Refusing to overwrite non-entity file at ${normalized}/${entityId}.md — ` +
1322
+ `destination must be a parseable entity for overwrite to be safe.`);
469
1323
  }
470
1324
  }
471
- catch { /* repo path unreadable */ }
472
- return [...types].sort();
1325
+ mkdirSync(dirname(newPath), { recursive: true });
1326
+ const { renameSync } = await import('fs');
1327
+ // On POSIX, renameSync atomically replaces the destination if it exists.
1328
+ // The overwrite gate above ensures we only reach this point when the
1329
+ // occupant is a tracked entity (or the path is empty).
1330
+ renameSync(existing.path, newPath);
1331
+ // Folder lifecycle: source stays addressable via .gitkeep if it lost its
1332
+ // last .md; destination drops any redundant .gitkeep now that it has one.
1333
+ const filesToCommit = [existing.path, newPath];
1334
+ const addedSourceMarker = this.ensureFolderMarker(dirname(existing.path));
1335
+ if (addedSourceMarker)
1336
+ filesToCommit.push(addedSourceMarker);
1337
+ const removedDestMarker = this.removeFolderMarker(dirname(newPath));
1338
+ if (removedDestMarker)
1339
+ filesToCommit.push(removedDestMarker);
1340
+ this.invalidateIndex();
1341
+ if (this.gitService) {
1342
+ const message = commitMessage || commitMsg.movedTo(this.commitRef(entityType, entityId, existing.data?.name), normalized || '(root)');
1343
+ this.gitService.commitFiles(filesToCommit, message, user ?? this.gitUser);
1344
+ }
1345
+ return this.get(entityType, entityId);
1346
+ }
1347
+ /**
1348
+ * Restore an entity to its content at a specific commit. Single-entity
1349
+ * scope only — multi-entity rollback (e.g. "undo this commit's effects
1350
+ * across all touched files") is a separate, more cautious flow that
1351
+ * isn't built yet.
1352
+ *
1353
+ * Two paths through the method:
1354
+ * - **Overwrite**: the entity exists today at path `P`. Read content
1355
+ * from `git show <hash>:<P>` and write it back to `P`.
1356
+ * - **Recreate**: the entity is gone today. Walk the historical
1357
+ * commit's tree to find the .md file whose basename matches
1358
+ * `entityId` AND whose frontmatter `entity_type` matches
1359
+ * `entityType`. That path is the restore target. The walk only
1360
+ * fires when the entity is truly absent — overwrites stay cheap.
1361
+ *
1362
+ * Throws a `PathConflictError` if the recreate target path is now
1363
+ * occupied by a DIFFERENT entity (different type+id) — recreating
1364
+ * over it would silently overwrite unrelated work. Caller surfaces
1365
+ * the conflict to the user; resolution is "move or delete the
1366
+ * occupant first."
1367
+ */
1368
+ async restoreFile(entityType, entityId, commitHash, user) {
1369
+ if (!this.gitService) {
1370
+ throw new Error('Cannot restore — git service is not available for this repo');
1371
+ }
1372
+ const existingPath = this.findEntityFile(entityType, entityId);
1373
+ let targetPath;
1374
+ let action;
1375
+ let content;
1376
+ if (existingPath) {
1377
+ // Overwrite path. Read content at the historical commit using the
1378
+ // CURRENT path (git tracks files by content + path; if the entity
1379
+ // was renamed or moved across the file's lifetime, --follow would
1380
+ // be needed — out of scope for v1, the user can resolve by
1381
+ // restoring at a commit where the path matched).
1382
+ const relPath = relative(this.repoPath, existingPath);
1383
+ content = this.gitService.getFileAtCommit(relPath, commitHash);
1384
+ if (content === null) {
1385
+ throw new Error(`File ${relPath} did not exist at commit ${commitHash}`);
1386
+ }
1387
+ // Guard: if the entry was change_type'd after this commit, the file path
1388
+ // is unchanged but the historical frontmatter carries the OLD
1389
+ // entity_type. Writing it back would silently revert the type, bypassing
1390
+ // change_type's collision checks, asset re-ownership, vector reindex, and
1391
+ // persistence callbacks — and the route would still broadcast an update
1392
+ // for the current type. Refuse rather than corrupt.
1393
+ // Read entity_type straight from the raw frontmatter block (no shared
1394
+ // markdown-parse layer) so this reflects exactly what's on disk at the
1395
+ // historical commit.
1396
+ const fmBlock = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
1397
+ const historicalType = fmBlock?.[1]
1398
+ .match(/^entity_type:\s*["']?([^"'\r\n]+)["']?\s*$/m)?.[1]
1399
+ ?.trim();
1400
+ if (historicalType && historicalType !== entityType) {
1401
+ throw new Error(`Cannot restore: the entry was type '${historicalType}' at that commit but is '${entityType}' now. ` +
1402
+ `Restoring would change its type without the safeguards of change_type. ` +
1403
+ `Convert it explicitly with change_type if that's intended.`);
1404
+ }
1405
+ targetPath = existingPath;
1406
+ action = 'overwrite';
1407
+ }
1408
+ else {
1409
+ // Recreate path. Walk the historical commit's tree to find the
1410
+ // file whose basename matches entityId and frontmatter type
1411
+ // matches entityType. This is the only place we do a full
1412
+ // tree-walk — it only runs when the entity is gone today.
1413
+ const found = this.findEntityInCommit(entityType, entityId, commitHash);
1414
+ if (!found) {
1415
+ throw new Error(`Entity ${entityType}/${entityId} not found at commit ${commitHash}`);
1416
+ }
1417
+ targetPath = join(this.repoPath, found.path);
1418
+ content = found.content;
1419
+ action = 'recreate';
1420
+ // Path-conflict check: nothing else may occupy targetPath today.
1421
+ if (existsSync(targetPath)) {
1422
+ const occupant = this.identifyEntityAtPath(targetPath);
1423
+ if (!occupant) {
1424
+ // A non-entity file (README, stray .md, .gitkeep) now sits at the
1425
+ // restore path. identifyEntityAtPath returning null previously fell
1426
+ // through and writeFileSync clobbered it — silent data loss. Treat
1427
+ // any unidentifiable occupant as a conflict.
1428
+ throw new PathConflictError(found.path, {
1429
+ entityType: 'file',
1430
+ entityId: relative(this.repoPath, targetPath),
1431
+ });
1432
+ }
1433
+ if (occupant.entityType !== entityType || occupant.entityId !== entityId) {
1434
+ throw new PathConflictError(found.path, occupant);
1435
+ }
1436
+ }
1437
+ }
1438
+ mkdirSync(dirname(targetPath), { recursive: true });
1439
+ writeFileSync(targetPath, content);
1440
+ // Folder lifecycle: if recreating dropped a file into a directory
1441
+ // that had been emptied (and now carries .gitkeep), drop the marker
1442
+ // so the directory exists via the .md file alone.
1443
+ const filesToCommit = [targetPath];
1444
+ if (action === 'recreate') {
1445
+ const removedDestMarker = this.removeFolderMarker(dirname(targetPath));
1446
+ if (removedDestMarker)
1447
+ filesToCommit.push(removedDestMarker);
1448
+ }
1449
+ this.invalidateIndex();
1450
+ const shortHash = commitHash.length > 8 ? commitHash.slice(0, 8) : commitHash;
1451
+ const restoredName = content ? this.markdownService.parseContent(content).frontmatter?.name : undefined;
1452
+ const message = commitMsg.restored(this.commitRef(entityType, entityId, restoredName), shortHash);
1453
+ this.gitService.commitFiles(filesToCommit, message, user ?? this.gitUser);
1454
+ // Reconcile any live editor Y.Doc against the restored disk content. restoreFile
1455
+ // writes the file directly — it does NOT flow through update()'s coordinator
1456
+ // body-write path — so an editor with a live Y.Doc still holds the pre-restore
1457
+ // content and would sync it back over the restore on the next autosave, making
1458
+ // the restore look like a no-op. Routing through the coordinator acquires the
1459
+ // per-doc lock (serializing the smart-merge against any in-flight autosave) and,
1460
+ // crucially, covers EVERY restore caller — the HTTP route, the agent history
1461
+ // tool, and the MCP tool — not just one. No-ops when no coordinator is wired
1462
+ // (CLI/tests) or no editor doc is currently active (later openers seed from disk).
1463
+ await this.bodyWriteCoordinator?.reconcileFromDisk({
1464
+ repo: this.repoName,
1465
+ entityType,
1466
+ entityId,
1467
+ entityPath: targetPath,
1468
+ });
1469
+ return { action, path: targetPath };
1470
+ }
1471
+ /**
1472
+ * Walk a commit's tree looking for a .md file whose basename matches
1473
+ * `entityId` AND whose frontmatter `entity_type` matches `entityType`.
1474
+ * Returns repo-relative path + content when found, otherwise null.
1475
+ * Used by `restoreFile` for the recreate-from-deletion case.
1476
+ */
1477
+ findEntityInCommit(entityType, entityId, commitHash) {
1478
+ if (!this.gitService)
1479
+ return null;
1480
+ const filenameLower = `${entityId}.md`.toLowerCase();
1481
+ const tree = this.gitService.listFilesAtCommit(commitHash);
1482
+ for (const path of tree) {
1483
+ if (!path.toLowerCase().endsWith(filenameLower))
1484
+ continue;
1485
+ // basename match — read content and check frontmatter type.
1486
+ const content = this.gitService.getFileAtCommit(path, commitHash);
1487
+ if (content === null)
1488
+ continue;
1489
+ try {
1490
+ // Cache-free parse: this is historical content, and the shared
1491
+ // gray-matter layer returned a stale entity_type for past-commit
1492
+ // content (see parseFrontmatterCacheFree), which would mis-match here.
1493
+ const fmType = parseFrontmatterCacheFree(content).entity_type;
1494
+ const inferred = this.inferLegacyType(join(this.repoPath, path));
1495
+ const type = fmType || inferred;
1496
+ if (type === entityType)
1497
+ return { path, content };
1498
+ }
1499
+ catch {
1500
+ // unparsable file — skip
1501
+ }
1502
+ }
1503
+ return null;
1504
+ }
1505
+ /**
1506
+ * Identify the entity (if any) currently occupying `absPath`. Returns
1507
+ * null when the path holds something that isn't an entity (README, etc).
1508
+ * Mirrors the inference rules used in `getFileIndex`.
1509
+ */
1510
+ identifyEntityAtPath(absPath) {
1511
+ try {
1512
+ const doc = this.markdownService.parseFile(absPath);
1513
+ const fmType = doc.frontmatter.entity_type;
1514
+ const inferred = this.inferLegacyType(absPath);
1515
+ const type = fmType || inferred;
1516
+ if (!type)
1517
+ return null;
1518
+ const id = absPath.split('/').pop().replace(/\.md$/, '');
1519
+ return { entityType: type, entityId: id };
1520
+ }
1521
+ catch {
1522
+ return null;
1523
+ }
1524
+ }
1525
+ /**
1526
+ * Enumerate the distinct entity types present in this repo. Used by
1527
+ * /api/repos to let the sidebar render the Views list without a full
1528
+ * entity fetch per folder.
1529
+ *
1530
+ * Derived from the file index (which reads frontmatter), so it works for
1531
+ * entries in any folder — not just the legacy {plural-type}/ dirs.
1532
+ */
1533
+ listEntityTypes() {
1534
+ if (!existsSync(this.repoPath))
1535
+ return [];
1536
+ const types = new Set();
1537
+ for (const key of this.getFileIndex().keys()) {
1538
+ const slash = key.indexOf('/');
1539
+ if (slash > 0)
1540
+ types.add(key.slice(0, slash));
1541
+ }
1542
+ return [...types].sort();
473
1543
  }
474
1544
  /**
475
- * List entities of a given type
1545
+ * Folder lifecycle: a directory is treated as a first-class persistent
1546
+ * object. It exists from the moment it's created (createFolder, the destination
1547
+ * of a moved entry, etc.) until it's explicitly removed via a folder-level
1548
+ * operation (deleteFolder, renameFolder, moveFolderToRepo). Entry-level
1549
+ * operations (create, delete, move, transfer) never decide whether a folder
1550
+ * lives or dies.
1551
+ *
1552
+ * `listFolders()` requires either a .md child or a .gitkeep marker for a
1553
+ * directory to surface in the API, so the two helpers below are the single
1554
+ * place that adds and removes the marker:
1555
+ *
1556
+ * - `ensureFolderMarker` — call after an op that may have left `dir`
1557
+ * without .md children. Writes `.gitkeep` and returns its path so the
1558
+ * caller can stage it in the same commit.
1559
+ * - `removeFolderMarker` — call after an op that placed an .md into `dir`.
1560
+ * Drops a now-redundant `.gitkeep` so the repo stays tidy.
1561
+ *
1562
+ * Both no-op for the repo root (always implicit) and for paths that don't
1563
+ * exist. Both return `null` when nothing changed.
1564
+ */
1565
+ ensureFolderMarker(dir) {
1566
+ if (dir === this.repoPath)
1567
+ return null;
1568
+ if (!existsSync(dir))
1569
+ return null;
1570
+ const entries = readdirSync(dir);
1571
+ const hasMd = entries.some(e => e.endsWith('.md') && !e.startsWith('.'));
1572
+ if (hasMd)
1573
+ return null;
1574
+ const keepPath = join(dir, '.gitkeep');
1575
+ if (existsSync(keepPath))
1576
+ return null;
1577
+ writeFileSync(keepPath, '');
1578
+ return keepPath;
1579
+ }
1580
+ removeFolderMarker(dir) {
1581
+ if (dir === this.repoPath)
1582
+ return null;
1583
+ if (!existsSync(dir))
1584
+ return null;
1585
+ const keepPath = join(dir, '.gitkeep');
1586
+ if (!existsSync(keepPath))
1587
+ return null;
1588
+ const entries = readdirSync(dir);
1589
+ const hasMd = entries.some(e => e.endsWith('.md') && !e.startsWith('.'));
1590
+ if (!hasMd)
1591
+ return null; // marker still load-bearing
1592
+ rmSync(keepPath, { force: true });
1593
+ return keepPath;
1594
+ }
1595
+ /**
1596
+ * Walk up from `dir`, removing each directory that became empty after a
1597
+ * folder-level removal (deleteFolder, renameFolder, moveFolderToRepo).
1598
+ * Stops at the repo root. Silently no-ops if a directory is missing,
1599
+ * non-empty, or can't be removed.
1600
+ *
1601
+ * NOT called by entry-level operations (create/delete/move/transfer) —
1602
+ * they preserve folder structure regardless of whether they emptied a
1603
+ * directory. Use `ensureFolderMarker` instead for those paths.
1604
+ */
1605
+ async pruneEmptyAncestors(dir) {
1606
+ const { rmSync } = await import('fs');
1607
+ const repoRoot = this.repoPath.replace(/\/+$/, '');
1608
+ let current = dir;
1609
+ while (current.startsWith(repoRoot + '/') && current !== repoRoot) {
1610
+ if (!existsSync(current)) {
1611
+ current = dirname(current);
1612
+ continue;
1613
+ }
1614
+ let entries;
1615
+ try {
1616
+ entries = readdirSync(current);
1617
+ }
1618
+ catch {
1619
+ return;
1620
+ }
1621
+ // Treat .gitkeep as "this folder is intentionally kept" — don't auto-prune.
1622
+ if (entries.length > 0)
1623
+ return;
1624
+ try {
1625
+ rmSync(current, { recursive: true, force: true });
1626
+ }
1627
+ catch {
1628
+ return;
1629
+ }
1630
+ current = dirname(current);
1631
+ }
1632
+ }
1633
+ /**
1634
+ * List entities of a given type (or all types if not specified).
1635
+ *
1636
+ * Walks the entire repo tree so entries in nested folders are discovered.
1637
+ * The type filter is applied against frontmatter `entity_type`; legacy
1638
+ * entries without explicit frontmatter type fall back to the depth-1
1639
+ * plural-type directory name (so `meetings/foo.md` still resolves as a
1640
+ * meeting). Files with no inferable type (README.md, CONTRIBUTING.md, etc.)
1641
+ * are skipped.
476
1642
  */
477
1643
  list(entityType, limit) {
478
1644
  if (!existsSync(this.repoPath))
479
1645
  return [];
1646
+ const cacheKey = entityType ?? '*';
1647
+ const cached = this.listCache?.get(cacheKey);
1648
+ if (cached) {
1649
+ return limit && limit > 0 ? cached.slice(0, limit) : cached;
1650
+ }
1651
+ // Serve a type-filtered request from the unfiltered cache if present —
1652
+ // avoids re-walking the tree for each entity type on the same warm cache.
1653
+ if (entityType) {
1654
+ const all = this.listCache?.get('*');
1655
+ if (all) {
1656
+ const filtered = all.filter(e => e.entityType === entityType);
1657
+ if (!this.listCache)
1658
+ this.listCache = new Map();
1659
+ this.listCache.set(cacheKey, filtered);
1660
+ return limit && limit > 0 ? filtered.slice(0, limit) : filtered;
1661
+ }
1662
+ }
480
1663
  const results = [];
481
- // Lenient validation for list(): try strict schema first, fall back to
482
- // EntityBaseSchema, then raw data so app-defined types are still discovered.
1664
+ const seen = new Set(); // "type/id" dedup if a file ends up indexed twice
483
1665
  const validateLenient = (type, data) => {
484
1666
  try {
485
1667
  return this.validateEntity(type, data);
@@ -493,97 +1675,39 @@ export class BaseGraphManager {
493
1675
  }
494
1676
  }
495
1677
  };
496
- const seenIds = new Set();
497
- // Process a flat .md file as an entity
498
- const tryAddFile = (filePath, id, inferredType) => {
499
- if (seenIds.has(id))
500
- return;
501
- try {
502
- const doc = this.markdownService.parseFile(filePath);
503
- const fmType = doc.frontmatter.entity_type;
504
- const resolvedType = entityType ?? fmType ?? inferredType ?? this.repoName;
505
- if (entityType && fmType && fmType !== entityType)
506
- return;
507
- const validatedData = validateLenient(resolvedType, doc.frontmatter);
508
- seenIds.add(id);
509
- results.push({
510
- id,
511
- path: filePath,
512
- entityType: resolvedType,
513
- data: validatedData,
514
- document: doc,
515
- });
516
- }
517
- catch { /* skip invalid */ }
518
- };
519
- // Scan plural type subfolders for flat .md files
520
- for (const entry of readdirSync(this.repoPath)) {
521
- if (entry.startsWith('.'))
522
- continue;
523
- const fullPath = join(this.repoPath, entry);
1678
+ this.walkMarkdownFiles((filePath) => {
1679
+ let doc;
524
1680
  try {
525
- if (!statSync(fullPath).isDirectory())
526
- continue;
1681
+ doc = this.markdownService.parseFile(filePath);
527
1682
  }
528
1683
  catch {
529
- continue;
530
- }
531
- const singularType = PLURAL_TO_SINGULAR[entry];
532
- if (!singularType)
533
- continue; // skip unknown directories
534
- try {
535
- for (const file of readdirSync(fullPath)) {
536
- if (file.startsWith('.') || !file.endsWith('.md'))
537
- continue;
538
- const filePath = join(fullPath, file);
539
- try {
540
- if (!statSync(filePath).isFile())
541
- continue;
542
- }
543
- catch {
544
- continue;
545
- }
546
- const id = file.replace(/\.md$/, '');
547
- tryAddFile(filePath, id, singularType);
548
- }
549
- }
550
- catch { /* skip unreadable */ }
551
- }
552
- // Also discover flat .md files at repo root (README, etc.)
553
- for (const entry of readdirSync(this.repoPath)) {
554
- if (entry.startsWith('.'))
555
- continue;
556
- if (!entry.endsWith('.md'))
557
- continue;
558
- const fullPath = join(this.repoPath, entry);
559
- try {
560
- if (!statSync(fullPath).isFile())
561
- continue;
562
- }
563
- catch {
564
- continue;
565
- }
566
- const id = entry.replace(/\.md$/, '');
567
- if (seenIds.has(id))
568
- continue;
569
- try {
570
- const doc = this.markdownService.parseFile(fullPath);
571
- const fmType = doc.frontmatter.entity_type;
572
- const resolvedType = entityType ?? fmType ?? this.repoName;
573
- if (entityType && fmType && fmType !== entityType)
574
- continue;
575
- const validatedData = validateLenient(resolvedType, doc.frontmatter);
576
- seenIds.add(id);
577
- results.push({
578
- id,
579
- path: fullPath,
580
- entityType: resolvedType,
581
- data: validatedData,
582
- document: doc,
583
- });
1684
+ return;
584
1685
  }
585
- catch { /* skip invalid */ }
586
- }
1686
+ const fmType = doc.frontmatter.entity_type;
1687
+ const inferred = this.inferLegacyType(filePath);
1688
+ const resolvedType = (fmType ?? inferred);
1689
+ if (!resolvedType)
1690
+ return; // README, etc.
1691
+ if (entityType && resolvedType !== entityType)
1692
+ return;
1693
+ const id = filePath.split('/').pop().replace(/\.md$/, '');
1694
+ const key = `${resolvedType}/${id}`;
1695
+ if (seen.has(key))
1696
+ return;
1697
+ seen.add(key);
1698
+ const validatedData = validateLenient(resolvedType, doc.frontmatter);
1699
+ results.push({
1700
+ id,
1701
+ path: filePath,
1702
+ folder_path: folderPathFromFile(filePath, this.repoPath),
1703
+ entityType: resolvedType,
1704
+ data: validatedData,
1705
+ document: doc,
1706
+ });
1707
+ });
1708
+ if (!this.listCache)
1709
+ this.listCache = new Map();
1710
+ this.listCache.set(cacheKey, results);
587
1711
  if (limit && limit > 0)
588
1712
  return results.slice(0, limit);
589
1713
  return results;
@@ -592,7 +1716,7 @@ export class BaseGraphManager {
592
1716
  * Search entities
593
1717
  */
594
1718
  search(options) {
595
- const { entityType, query, filters, visibility, tags, dateRange, sortBy, limit } = options;
1719
+ const { entityType, query, filters, visibility, tags, dateRange, sortBy, limit, nameOnly } = options;
596
1720
  // Get all entities (optionally filtered by type)
597
1721
  let entities = this.list(entityType);
598
1722
  // Apply query filter (search across content + all string frontmatter fields)
@@ -602,7 +1726,7 @@ export class BaseGraphManager {
602
1726
  const lowerQuery = query.toLowerCase();
603
1727
  const EXCLUDED_SEARCH_FIELDS = new Set([
604
1728
  'entity_type', 'entity_id', 'created_by', 'updated_by',
605
- 'created_at', 'updated_at', 'date', 'start_date', 'end_date', 'due_date', 'expected_close',
1729
+ 'created_at', 'updated_at', 'date', 'start_date',
606
1730
  'deal_value', 'email', 'website', 'url', 'link', 'schema',
607
1731
  ]);
608
1732
  scored = [];
@@ -619,8 +1743,8 @@ export class BaseGraphManager {
619
1743
  else if (name.includes(lowerQuery)) {
620
1744
  score = 2;
621
1745
  }
622
- // Frontmatter field match
623
- if (score < 0) {
1746
+ // Frontmatter field match — skipped in nameOnly mode (autosuggest).
1747
+ if (score < 0 && !nameOnly) {
624
1748
  const data = e.data;
625
1749
  for (const [key, val] of Object.entries(data)) {
626
1750
  if (EXCLUDED_SEARCH_FIELDS.has(key) || key === 'name')
@@ -641,8 +1765,11 @@ export class BaseGraphManager {
641
1765
  }
642
1766
  }
643
1767
  }
644
- // Body content match
645
- if (score < 0 && e.document.content.toLowerCase().includes(lowerQuery)) {
1768
+ // Body content match — skipped in nameOnly mode. Scanning every
1769
+ // entity's full markdown body is the dominant cost for keyword
1770
+ // search and is wasted on autosuggest, where the dropdown only
1771
+ // shows the name.
1772
+ if (score < 0 && !nameOnly && e.document.content.toLowerCase().includes(lowerQuery)) {
646
1773
  score = 0;
647
1774
  }
648
1775
  if (score >= 0)
@@ -747,31 +1874,43 @@ export class BaseGraphManager {
747
1874
  }
748
1875
  return entities;
749
1876
  }
1877
+ /**
1878
+ * Resolve a markdown file (anywhere under the repo) to its (type, id),
1879
+ * tolerating both legacy {plural-type}/{id}.md and nested-folder layouts.
1880
+ * Returns undefined for files without a recognizable entity type.
1881
+ */
1882
+ resolveFileToEntity(absPath) {
1883
+ const filename = absPath.split('/').pop();
1884
+ if (!filename || !filename.endsWith('.md'))
1885
+ return undefined;
1886
+ const id = filename.replace(/\.md$/, '');
1887
+ let fmType;
1888
+ try {
1889
+ fmType = this.markdownService.parseFile(absPath).frontmatter.entity_type;
1890
+ }
1891
+ catch { /* unparsable — fall back to inference */ }
1892
+ const type = fmType || this.inferLegacyType(absPath);
1893
+ if (!type)
1894
+ return undefined;
1895
+ return { type: type, id };
1896
+ }
750
1897
  /**
751
1898
  * Get entities related to a given entity (via wikilinks)
752
1899
  */
753
1900
  getRelated(entityType, entityId) {
754
1901
  const entity = this.get(entityType, entityId);
755
1902
  const related = [];
756
- // Find all wikilinked entities
757
1903
  for (const link of entity.document.wikilinks) {
758
- // Try to find the linked entity in all type subfolders
759
1904
  const found = this.markdownService.findEntity(link, [this.repoPath]);
760
- if (found) {
761
- const relPath = found.replace(this.repoPath + '/', '');
762
- const parts = relPath.split('/');
763
- // New layout: {plural-type}/{entityId}.md
764
- if (parts.length === 2 && parts[1].endsWith('.md')) {
765
- const linkedId = parts[1].replace(/\.md$/, '');
766
- try {
767
- const doc = this.markdownService.parseFile(found);
768
- const linkedType = doc.frontmatter.entity_type ?? PLURAL_TO_SINGULAR[parts[0]] ?? this.repoName;
769
- const linkedEntity = this.get(linkedType, linkedId);
770
- related.push(linkedEntity);
771
- }
772
- catch { /* skip invalid */ }
773
- }
1905
+ if (!found)
1906
+ continue;
1907
+ const resolved = this.resolveFileToEntity(found);
1908
+ if (!resolved)
1909
+ continue;
1910
+ try {
1911
+ related.push(this.get(resolved.type, resolved.id));
774
1912
  }
1913
+ catch { /* skip invalid */ }
775
1914
  }
776
1915
  return related;
777
1916
  }
@@ -782,19 +1921,13 @@ export class BaseGraphManager {
782
1921
  const backlinks = this.markdownService.findBacklinks(entityId, [this.repoPath]);
783
1922
  const results = [];
784
1923
  for (const { file } of backlinks) {
785
- const relPath = file.replace(this.repoPath + '/', '');
786
- const parts = relPath.split('/');
787
- // New layout: {plural-type}/{entityId}.md
788
- if (parts.length === 2 && parts[1].endsWith('.md')) {
789
- const linkedId = parts[1].replace(/\.md$/, '');
790
- try {
791
- const doc = this.markdownService.parseFile(file);
792
- const linkedType = doc.frontmatter.entity_type ?? PLURAL_TO_SINGULAR[parts[0]] ?? this.repoName;
793
- const linkedEntity = this.get(linkedType, linkedId);
794
- results.push(linkedEntity);
795
- }
796
- catch { /* skip invalid */ }
1924
+ const resolved = this.resolveFileToEntity(file);
1925
+ if (!resolved)
1926
+ continue;
1927
+ try {
1928
+ results.push(this.get(resolved.type, resolved.id));
797
1929
  }
1930
+ catch { /* skip invalid */ }
798
1931
  }
799
1932
  return results;
800
1933
  }
@@ -809,28 +1942,22 @@ export class BaseGraphManager {
809
1942
  * Refactor a wikilink (rename entity and update all references)
810
1943
  */
811
1944
  async refactorWikilink(oldId, newId, commitMessage) {
812
- // Find all files that reference the old entity ID
813
1945
  const backlinks = this.markdownService.findBacklinks(oldId, [this.repoPath]);
814
- // Update each file
815
1946
  if (this.gitService) {
816
1947
  const transaction = this.gitService.createTransaction();
817
1948
  for (const { file } of backlinks) {
818
1949
  this.markdownService.updateWikilinks(file, oldId, newId);
819
- const relPath = file.replace(this.repoPath + '/', '');
820
- const parts = relPath.split('/');
821
- // New layout: {plural-type}/{entityId}.md
822
- if (parts.length === 2 && parts[1].endsWith('.md')) {
823
- const entityId = parts[1].replace(/\.md$/, '');
824
- const doc = this.markdownService.parseFile(file);
825
- transaction.add({
826
- operation: 'update',
827
- entityType: doc.frontmatter.entity_type ?? PLURAL_TO_SINGULAR[parts[0]] ?? parts[0],
828
- entityId,
829
- filePath: file,
830
- });
831
- }
1950
+ const resolved = this.resolveFileToEntity(file);
1951
+ if (!resolved)
1952
+ continue;
1953
+ transaction.add({
1954
+ operation: 'update',
1955
+ entityType: resolved.type,
1956
+ entityId: resolved.id,
1957
+ filePath: file,
1958
+ });
832
1959
  }
833
- const message = commitMessage || `Refactor wikilink: ${oldId} → ${newId}`;
1960
+ const message = commitMessage || commitMsg.relinked(oldId, newId);
834
1961
  await transaction.commit(this.gitUser, message);
835
1962
  }
836
1963
  else {
@@ -840,14 +1967,772 @@ export class BaseGraphManager {
840
1967
  }
841
1968
  return { updated: backlinks.length };
842
1969
  }
1970
+ // ────────────────────────────────────────────────────────────────────────────
1971
+ // Folder CRUD — operate on directories within the repo, not on entries.
1972
+ // Sharing remains a repo-level concept; folders carry no permissions or
1973
+ // metadata of their own. Empty folders are kept around via .gitkeep so git
1974
+ // tracks them.
1975
+ // ────────────────────────────────────────────────────────────────────────────
1976
+ /**
1977
+ * List every non-empty folder in this repo, plus folders explicitly kept
1978
+ * via .gitkeep. Returns repo-relative POSIX paths, sorted, deduplicated.
1979
+ * The repo root ("") is intentionally omitted — it's implicit.
1980
+ */
1981
+ listFolders() {
1982
+ if (!existsSync(this.repoPath))
1983
+ return [];
1984
+ const folders = new Set();
1985
+ const sidecarByPath = new Map();
1986
+ const sidecarIdByPath = new Map();
1987
+ const stack = [this.repoPath];
1988
+ const repoRoot = this.repoPath.replace(/\/+$/, '');
1989
+ while (stack.length > 0) {
1990
+ const dir = stack.pop();
1991
+ let entries;
1992
+ try {
1993
+ entries = readdirSync(dir);
1994
+ }
1995
+ catch {
1996
+ continue;
1997
+ }
1998
+ let kept = false;
1999
+ let hasSidecar = false;
2000
+ for (const entry of entries) {
2001
+ if (entry === '.gitkeep')
2002
+ kept = true;
2003
+ if (entry === FOLDER_SIDECAR)
2004
+ hasSidecar = true;
2005
+ if (entry.startsWith('.'))
2006
+ continue;
2007
+ if (WALK_IGNORE.has(entry))
2008
+ continue;
2009
+ const full = join(dir, entry);
2010
+ let st;
2011
+ try {
2012
+ st = statSync(full);
2013
+ }
2014
+ catch {
2015
+ continue;
2016
+ }
2017
+ if (st.isDirectory())
2018
+ stack.push(full);
2019
+ }
2020
+ if (dir === repoRoot)
2021
+ continue;
2022
+ // Include this directory if it has any .md files OR a .gitkeep marker
2023
+ // OR a sidecar (which carries display metadata even for empty folders).
2024
+ const hasMarkdown = entries.some(e => e.endsWith('.md') && !e.startsWith('.'));
2025
+ if (hasMarkdown || kept || hasSidecar) {
2026
+ const folderPath = folderPathFromFile(join(dir, 'placeholder.md'), this.repoPath);
2027
+ folders.add(folderPath);
2028
+ if (hasSidecar) {
2029
+ const data = readFolderSidecarData(dir);
2030
+ if (data?.display_name)
2031
+ sidecarByPath.set(folderPath, data.display_name);
2032
+ if (data?.id)
2033
+ sidecarIdByPath.set(folderPath, data.id);
2034
+ }
2035
+ }
2036
+ }
2037
+ return [...folders]
2038
+ .filter(p => p.length > 0)
2039
+ .sort()
2040
+ .map(p => {
2041
+ const entry = { path: p, name: p.slice(p.lastIndexOf('/') + 1) };
2042
+ const display = sidecarByPath.get(p);
2043
+ if (display)
2044
+ entry.display_name = display;
2045
+ const id = sidecarIdByPath.get(p);
2046
+ if (id)
2047
+ entry.id = id;
2048
+ return entry;
2049
+ });
2050
+ }
2051
+ /**
2052
+ * Read the display_name override for a single folder, or null if no sidecar
2053
+ * exists / is unreadable. Used by mutation paths that need the current
2054
+ * override before deciding whether to rewrite it.
2055
+ */
2056
+ readFolderDisplayName(folderPath) {
2057
+ const normalized = validateFolderPath(folderPath);
2058
+ if (!normalized)
2059
+ return null;
2060
+ return readFolderSidecar(join(this.repoPath, normalized));
2061
+ }
2062
+ /**
2063
+ * Write or remove the per-folder display_name sidecar. Pass `null` /
2064
+ * empty-string to remove (returns to slug fallback). Returns the absolute
2065
+ * path of the sidecar (for git staging) when one was written or removed,
2066
+ * or null when no change was needed (idempotent).
2067
+ *
2068
+ * Caller is responsible for committing — the helper only touches the
2069
+ * filesystem. Folder must already exist; this never creates one.
2070
+ */
2071
+ writeFolderDisplayName(folderPath, displayName) {
2072
+ const normalized = validateFolderPath(folderPath);
2073
+ if (!normalized) {
2074
+ throw new InvalidFolderPathError('Folder path cannot be empty');
2075
+ }
2076
+ const abs = join(this.repoPath, normalized);
2077
+ if (!existsSync(abs)) {
2078
+ throw new Error(`Folder not found: ${normalized}`);
2079
+ }
2080
+ const trimmed = displayName?.trim();
2081
+ if (!trimmed) {
2082
+ // Clear the override but keep the sidecar (and its id) when one exists —
2083
+ // writeFolderSidecarData drops display_name and removes the file only if
2084
+ // nothing (no id) remains.
2085
+ const existing = readFolderSidecar(abs);
2086
+ if (existing === null)
2087
+ return null; // nothing to clear
2088
+ const result = writeFolderSidecarData(abs, { display_name: undefined });
2089
+ this.invalidateIndex();
2090
+ return result;
2091
+ }
2092
+ const existing = readFolderSidecar(abs);
2093
+ if (existing === trimmed)
2094
+ return null;
2095
+ const sidecarPath = writeFolderSidecar(abs, trimmed);
2096
+ this.invalidateIndex();
2097
+ return sidecarPath;
2098
+ }
2099
+ /**
2100
+ * Bulk-write display_name sidecars for many folders at once. Used by the
2101
+ * import path to record original-casing overrides for every nested
2102
+ * directory it slugified. First-writer-wins: existing sidecars are NOT
2103
+ * overwritten, so a second import doesn't clobber a user's manual rename.
2104
+ *
2105
+ * Returns the list of sidecar paths actually written (for git staging).
2106
+ * Skipped paths (folder missing, empty display, display_name override
2107
+ * already set) are quietly omitted.
2108
+ */
2109
+ recordFolderDisplays(entries) {
2110
+ const written = [];
2111
+ for (const entry of entries) {
2112
+ const normalized = validateFolderPath(entry.path);
2113
+ if (!normalized)
2114
+ continue;
2115
+ const abs = join(this.repoPath, normalized);
2116
+ if (!existsSync(abs))
2117
+ continue;
2118
+ // First-writer-wins on the display_name specifically — skip when an
2119
+ // override already exists (don't clobber a manual rename), but still
2120
+ // record onto an id-only sidecar (every folder now has one for its id).
2121
+ if (readFolderSidecar(abs) !== null)
2122
+ continue;
2123
+ const display = entry.display_name.trim();
2124
+ if (!display)
2125
+ continue;
2126
+ // Merge-safe: preserves the folder's id alongside the new display_name.
2127
+ written.push(writeFolderSidecar(abs, display));
2128
+ }
2129
+ if (written.length > 0)
2130
+ this.invalidateIndex();
2131
+ return written;
2132
+ }
2133
+ /**
2134
+ * Create a folder at the given path (mkdir -p semantics) and drop a
2135
+ * .gitkeep so git tracks it even when empty. Validates the path. No-op if
2136
+ * the folder already exists; .gitkeep is added if missing.
2137
+ */
2138
+ async createFolder(folderPath, commitMessage, user, options) {
2139
+ const normalized = validateFolderPath(folderPath);
2140
+ if (!normalized) {
2141
+ throw new InvalidFolderPathError('Folder path cannot be empty (use the repo root directly)');
2142
+ }
2143
+ const abs = join(this.repoPath, normalized);
2144
+ // Report whether the folder was already there so the create_subfolder tools
2145
+ // can say "already exists — reusing it" instead of silently claiming they
2146
+ // created a new folder (which would hide that entries are merging into an
2147
+ // existing one). createFolder still doubles as an idempotent ensure-folder
2148
+ // no-op for internal callers (personal-folder provisioning, rename), which
2149
+ // ignore this flag.
2150
+ const alreadyExisted = existsSync(abs);
2151
+ mkdirSync(abs, { recursive: true });
2152
+ const keep = join(abs, '.gitkeep');
2153
+ if (!existsSync(keep)) {
2154
+ writeFileSync(keep, '');
2155
+ }
2156
+ // Sidecar is written here (not after the commit) so it joins the same
2157
+ // commit as .gitkeep — folder creation is atomic from git's perspective.
2158
+ // A stable id is assigned eagerly when the folder doesn't already have one;
2159
+ // an existing id is never regenerated (createFolder doubles as an
2160
+ // ensure-folder no-op for paths that already exist).
2161
+ const sidecarPatch = {};
2162
+ if (!readFolderSidecarId(abs))
2163
+ sidecarPatch.id = randomUUID();
2164
+ if (options?.displayName !== undefined)
2165
+ sidecarPatch.display_name = options.displayName;
2166
+ const sidecarPath = Object.keys(sidecarPatch).length > 0
2167
+ ? writeFolderSidecarData(abs, sidecarPatch)
2168
+ : null;
2169
+ this.invalidateIndex();
2170
+ if (this.gitService) {
2171
+ const message = commitMessage || commitMsg.folderCreated(normalized);
2172
+ const files = sidecarPath ? [keep, sidecarPath] : [keep];
2173
+ this.gitService.commitFiles(files, message, user ?? this.gitUser);
2174
+ }
2175
+ return { alreadyExisted };
2176
+ }
2177
+ /**
2178
+ * Ensure the folder at `folderPath` (or the repo root when empty) carries a
2179
+ * stable sidecar id, minting + committing one if absent. Returns the id.
2180
+ *
2181
+ * Saved views key folder scopes on this id (D2) so a view survives folder
2182
+ * rename / move — the sidecar travels with the directory the same way an
2183
+ * entity's frontmatter travels with its .md file. Idempotent: an existing id
2184
+ * is returned untouched, never regenerated.
2185
+ */
2186
+ ensureFolderId(folderPath, user) {
2187
+ const normalized = folderPath ? validateFolderPath(folderPath) : '';
2188
+ const abs = normalized ? join(this.repoPath, normalized) : this.repoPath;
2189
+ // Never materialize a phantom folder: writeFolderSidecarData mkdirSyncs the
2190
+ // path, so minting an id for a folder that no longer exists on disk (e.g. a
2191
+ // stale folder scope after another user renamed/deleted it) would silently
2192
+ // resurrect + commit an empty directory. Refuse instead — the route turns
2193
+ // this into a 400 rather than corrupting the tree.
2194
+ if (!existsSync(abs)) {
2195
+ throw new InvalidFolderPathError(`Folder no longer exists: ${normalized || '(root)'}`);
2196
+ }
2197
+ const existing = readFolderSidecarId(abs);
2198
+ if (existing)
2199
+ return existing;
2200
+ const id = randomUUID();
2201
+ // Non-null: writing a non-empty id always leaves the sidecar non-empty.
2202
+ const sidecarPath = writeFolderSidecarData(abs, { id });
2203
+ this.invalidateIndex();
2204
+ if (this.gitService) {
2205
+ this.gitService.commitFiles([sidecarPath], `Assign stable id to folder ${normalized || '(root)'}`, user ?? this.gitUser);
2206
+ }
2207
+ return id;
2208
+ }
2209
+ /**
2210
+ * Resolve a stable sidecar folder id to its repo-relative path within this
2211
+ * repo, or null when no folder here carries that id. Returns `''` when the
2212
+ * id belongs to the repo root.
2213
+ */
2214
+ findFolderPathById(id) {
2215
+ if (readFolderSidecarId(this.repoPath) === id)
2216
+ return '';
2217
+ for (const f of this.listFolders()) {
2218
+ if (f.id === id)
2219
+ return f.path;
2220
+ }
2221
+ return null;
2222
+ }
2223
+ /**
2224
+ * All folder ids carried in this repo — the root sidecar (path `''`) plus
2225
+ * every subfolder — as `{ id, path }` pairs. Built from a single
2226
+ * `listFolders()` walk so a caller resolving many ids at once (e.g. the saved
2227
+ * views GET) can index once instead of re-walking the tree per id.
2228
+ */
2229
+ folderIdEntries() {
2230
+ const out = [];
2231
+ const rootId = readFolderSidecarId(this.repoPath);
2232
+ if (rootId)
2233
+ out.push({ id: rootId, path: '' });
2234
+ for (const f of this.listFolders()) {
2235
+ if (f.id)
2236
+ out.push({ id: f.id, path: f.path });
2237
+ }
2238
+ return out;
2239
+ }
2240
+ /**
2241
+ * Rename / move a folder within the repo. Moves the entire subtree
2242
+ * atomically via filesystem rename. Refuses if the target path is already
2243
+ * occupied.
2244
+ */
2245
+ async renameFolder(fromPath, toPath, commitMessage, user, options) {
2246
+ const fromNormalized = validateFolderPath(fromPath);
2247
+ const toNormalized = validateFolderPath(toPath);
2248
+ if (!fromNormalized)
2249
+ throw new InvalidFolderPathError('Source folder path required');
2250
+ if (!toNormalized)
2251
+ throw new InvalidFolderPathError('Target folder path required');
2252
+ // Display-only update: same path, just rewrite the sidecar.
2253
+ if (fromNormalized === toNormalized) {
2254
+ if (options?.displayName === undefined)
2255
+ return;
2256
+ const sidecarPath = this.writeFolderDisplayName(fromNormalized, options.displayName);
2257
+ if (sidecarPath && this.gitService) {
2258
+ const message = commitMessage || commitMsg.folderDisplayRenamed(fromNormalized);
2259
+ this.gitService.commitFiles([sidecarPath], message, user ?? this.gitUser);
2260
+ }
2261
+ return;
2262
+ }
2263
+ const fromAbs = join(this.repoPath, fromNormalized);
2264
+ const toAbs = join(this.repoPath, toNormalized);
2265
+ if (!existsSync(fromAbs)) {
2266
+ throw new Error(`Folder not found: ${fromNormalized}`);
2267
+ }
2268
+ if (existsSync(toAbs)) {
2269
+ throw new Error(`Target folder already exists: ${toNormalized}`);
2270
+ }
2271
+ mkdirSync(dirname(toAbs), { recursive: true });
2272
+ const { renameSync } = await import('fs');
2273
+ renameSync(fromAbs, toAbs);
2274
+ // The sidecar moved with the folder via FS rename. Update it now if the
2275
+ // caller passed a new display name (or `null` to clear). Idempotent — no
2276
+ // sidecar write happens when display matches the existing override.
2277
+ let sidecarPath = null;
2278
+ if (options?.displayName !== undefined) {
2279
+ sidecarPath = this.writeFolderDisplayName(toNormalized, options.displayName);
2280
+ }
2281
+ this.invalidateIndex();
2282
+ if (this.gitService) {
2283
+ const message = commitMessage || commitMsg.folderRenamed(fromNormalized, toNormalized);
2284
+ const files = sidecarPath ? [fromAbs, toAbs, sidecarPath] : [fromAbs, toAbs];
2285
+ this.gitService.commitFiles(files, message, user ?? this.gitUser);
2286
+ }
2287
+ await this.pruneEmptyAncestors(dirname(fromAbs));
2288
+ }
2289
+ /**
2290
+ * Move a folder (and its full subtree) into a different repo at `targetFolderPath`.
2291
+ *
2292
+ * The naive way to do this is to iterate the source's entries and call
2293
+ * `transfer` per entry — that's what the web UI used to do, and it costs
2294
+ * 2 git commits per entry (1600 commits for 800 files), each blocking the
2295
+ * Node event loop via `execSync`. This method performs the move as a
2296
+ * single FS-level rename + one commit per repo, regardless of how many
2297
+ * files are inside.
2298
+ *
2299
+ * Returns the count of entries + subfolders moved so the caller can
2300
+ * surface progress.
2301
+ */
2302
+ async moveFolderToRepo(sourceFolderPath, target, targetFolderPath, options) {
2303
+ const sourceNormalized = validateFolderPath(sourceFolderPath);
2304
+ if (!sourceNormalized) {
2305
+ throw new InvalidFolderPathError('Source folder path required');
2306
+ }
2307
+ const targetNormalized = targetFolderPath ? validateFolderPath(targetFolderPath) : '';
2308
+ if (targetFolderPath && !targetNormalized) {
2309
+ throw new InvalidFolderPathError('Invalid target folder path');
2310
+ }
2311
+ if (target === this && sourceNormalized === targetNormalized) {
2312
+ return { entriesDeleted: 0, subfoldersDeleted: 0 };
2313
+ }
2314
+ const sourceAbs = join(this.repoPath, sourceNormalized);
2315
+ if (!existsSync(sourceAbs)) {
2316
+ throw new Error(`Source folder not found: ${sourceNormalized}`);
2317
+ }
2318
+ const targetAbs = targetNormalized
2319
+ ? join(target.repoPath, targetNormalized)
2320
+ : target.repoPath;
2321
+ // Refuse if target already has visible content — caller must move into
2322
+ // an empty / nonexistent folder. (Hidden files like .studiograph are OK.)
2323
+ if (existsSync(targetAbs)) {
2324
+ const visible = readdirSync(targetAbs).filter(n => !n.startsWith('.'));
2325
+ if (visible.length > 0) {
2326
+ throw new Error(`Target folder is not empty: ${targetNormalized || '/'}`);
2327
+ }
2328
+ }
2329
+ const stats = this.countFolderContents(sourceAbs);
2330
+ // For a cross-repo move, capture the entities in the tree BEFORE the
2331
+ // rename so we can re-point their asset ownership to the target repo
2332
+ // afterward (serve-time authz reads the index's repo column).
2333
+ const ownersToReassign = [];
2334
+ if (this.assetIndex && target.repoName !== this.repoName) {
2335
+ const stack = [sourceAbs];
2336
+ while (stack.length > 0) {
2337
+ const dir = stack.pop();
2338
+ let names;
2339
+ try {
2340
+ names = readdirSync(dir);
2341
+ }
2342
+ catch {
2343
+ continue;
2344
+ }
2345
+ for (const name of names) {
2346
+ if (name.startsWith('.') || WALK_IGNORE.has(name))
2347
+ continue;
2348
+ const full = join(dir, name);
2349
+ let st;
2350
+ try {
2351
+ st = statSync(full);
2352
+ }
2353
+ catch {
2354
+ continue;
2355
+ }
2356
+ if (st.isDirectory())
2357
+ stack.push(full);
2358
+ else if (st.isFile() && name.endsWith('.md')) {
2359
+ const ent = this.identifyEntityAtPath(full);
2360
+ if (ent)
2361
+ ownersToReassign.push({ entityType: ent.entityType, entityId: ent.entityId });
2362
+ }
2363
+ }
2364
+ }
2365
+ }
2366
+ // Atomic FS rename if possible. Falls back to copy + delete if the two
2367
+ // repos sit on different filesystems (rare, but possible on Docker
2368
+ // bind mounts).
2369
+ const { renameSync, cpSync, rmSync } = await import('fs');
2370
+ mkdirSync(dirname(targetAbs), { recursive: true });
2371
+ if (existsSync(targetAbs)) {
2372
+ // Empty pre-existing dir — drop it so renameSync can take its place.
2373
+ rmSync(targetAbs, { recursive: true, force: true });
2374
+ }
2375
+ try {
2376
+ renameSync(sourceAbs, targetAbs);
2377
+ }
2378
+ catch (err) {
2379
+ if (err?.code === 'EXDEV') {
2380
+ cpSync(sourceAbs, targetAbs, { recursive: true });
2381
+ rmSync(sourceAbs, { recursive: true, force: true });
2382
+ }
2383
+ else {
2384
+ throw err;
2385
+ }
2386
+ }
2387
+ this.invalidateIndex();
2388
+ target.invalidateIndex();
2389
+ const user = options?.user ?? this.gitUser;
2390
+ const sourceMessage = options?.commitMessage
2391
+ ?? commitMsg.folderMoved(sourceNormalized, target.repoName, targetNormalized);
2392
+ const targetMessage = options?.commitMessage
2393
+ ?? commitMsg.folderReceived(this.repoName, sourceNormalized, targetNormalized);
2394
+ if (this.gitService) {
2395
+ this.gitService.commitFiles([sourceAbs], sourceMessage, user);
2396
+ }
2397
+ if (target.gitService) {
2398
+ target.gitService.commitFiles([targetAbs], targetMessage, user);
2399
+ }
2400
+ // Cross-repo: re-point asset ownership to the target repo (see capture above).
2401
+ for (const o of ownersToReassign) {
2402
+ this.assetIndex?.reassignOwner(this.repoName, o.entityType, o.entityId, { repo: target.repoName });
2403
+ }
2404
+ await this.pruneEmptyAncestors(dirname(sourceAbs));
2405
+ return stats;
2406
+ }
2407
+ /**
2408
+ * Move a batch of entries (potentially across many source folders) into a
2409
+ * single target folder, optionally in a different repo. The web UI's
2410
+ * multi-select bulk move used to call /move or /transfer per entry — that
2411
+ * was 1-2 sync git commits per entry, blocking the event loop on each one.
2412
+ * This collapses the operation to a single FS rename per file plus one
2413
+ * commit on each repo touched.
2414
+ */
2415
+ async moveEntitiesToRepo(entries, target, targetFolderPath, options) {
2416
+ if (entries.length === 0)
2417
+ return { moved: 0 };
2418
+ const targetNormalized = targetFolderPath ? validateFolderPath(targetFolderPath) : '';
2419
+ if (targetFolderPath && !targetNormalized) {
2420
+ throw new InvalidFolderPathError('Invalid target folder path');
2421
+ }
2422
+ const targetDir = targetNormalized
2423
+ ? join(target.repoPath, targetNormalized)
2424
+ : target.repoPath;
2425
+ // Resolve all source paths up front and validate target conflicts before
2426
+ // any FS work — partial moves on conflict would leave the user with a
2427
+ // confusing half-state.
2428
+ const moves = [];
2429
+ for (const entry of entries) {
2430
+ const existing = this.get(entry.entityType, entry.entityId);
2431
+ const dst = join(targetDir, `${entry.entityId}.md`);
2432
+ if (existing.path === dst)
2433
+ continue; // already there — no-op
2434
+ if (existsSync(dst)) {
2435
+ throw new Error(`Path already occupied: ${target.repoName}:${targetNormalized ? targetNormalized + '/' : ''}${entry.entityId}.md`);
2436
+ }
2437
+ moves.push({ src: existing.path, dst });
2438
+ }
2439
+ if (moves.length === 0)
2440
+ return { moved: 0 };
2441
+ const { renameSync, cpSync } = await import('fs');
2442
+ mkdirSync(targetDir, { recursive: true });
2443
+ for (const m of moves) {
2444
+ try {
2445
+ renameSync(m.src, m.dst);
2446
+ }
2447
+ catch (err) {
2448
+ if (err?.code === 'EXDEV') {
2449
+ cpSync(m.src, m.dst);
2450
+ rmSync(m.src);
2451
+ }
2452
+ else {
2453
+ throw err;
2454
+ }
2455
+ }
2456
+ }
2457
+ // Folder lifecycle: ensure markers in every source dir we may have
2458
+ // emptied, drop the marker in the (single) target dir now that it has
2459
+ // .md children. Helpers no-op for the repo root and for paths that
2460
+ // are already in the right state, so calling them per dir is cheap.
2461
+ const sourceMarkers = [];
2462
+ const visitedSourceDirs = new Set();
2463
+ for (const m of moves) {
2464
+ const sourceDir = dirname(m.src);
2465
+ if (visitedSourceDirs.has(sourceDir))
2466
+ continue;
2467
+ visitedSourceDirs.add(sourceDir);
2468
+ const added = this.ensureFolderMarker(sourceDir);
2469
+ if (added)
2470
+ sourceMarkers.push(added);
2471
+ }
2472
+ const removedTargetMarker = target.removeFolderMarker(targetDir);
2473
+ this.invalidateIndex();
2474
+ if (target !== this)
2475
+ target.invalidateIndex();
2476
+ const user = options?.user ?? this.gitUser;
2477
+ const where = target === this
2478
+ ? (targetNormalized || '(root)')
2479
+ : `${target.repoName}:${targetNormalized || '(root)'}`;
2480
+ const sourceMessage = options?.commitMessage ?? commitMsg.movedBatch(moves.length, where);
2481
+ if (this.gitService) {
2482
+ const sourcePaths = [...moves.map(m => m.src), ...sourceMarkers];
2483
+ const dstStaging = target === this
2484
+ ? [...moves.map(m => m.dst), ...(removedTargetMarker ? [removedTargetMarker] : [])]
2485
+ : [];
2486
+ this.gitService.commitFiles([...sourcePaths, ...dstStaging], sourceMessage, user);
2487
+ }
2488
+ if (target !== this && target.gitService) {
2489
+ const targetMessage = options?.commitMessage
2490
+ ?? commitMsg.receivedBatch(moves.length, this.repoName);
2491
+ const targetPaths = [...moves.map(m => m.dst), ...(removedTargetMarker ? [removedTargetMarker] : [])];
2492
+ target.gitService.commitFiles(targetPaths, targetMessage, user);
2493
+ }
2494
+ // Cross-repo move: re-point asset ownership to the target repo so the
2495
+ // serve route's per-repo authz resolves against the new repo. No blob
2496
+ // movement (keys are immutable); the index is workspace-global so source
2497
+ // and target share it. Same-repo folder moves leave repo unchanged.
2498
+ if (target.repoName !== this.repoName) {
2499
+ for (const entry of entries) {
2500
+ this.assetIndex?.reassignOwner(this.repoName, entry.entityType, entry.entityId, { repo: target.repoName });
2501
+ }
2502
+ }
2503
+ return { moved: moves.length };
2504
+ }
2505
+ /**
2506
+ * Delete a folder. Refuses non-empty folders unless `recursive: true`.
2507
+ * Returns the count of entries + subfolders removed (for blast-radius UI).
2508
+ */
2509
+ async deleteFolder(folderPath, options) {
2510
+ const normalized = validateFolderPath(folderPath);
2511
+ if (!normalized) {
2512
+ throw new InvalidFolderPathError('Cannot delete the repo root via deleteFolder');
2513
+ }
2514
+ const abs = join(this.repoPath, normalized);
2515
+ if (!existsSync(abs)) {
2516
+ throw new Error(`Folder not found: ${normalized}`);
2517
+ }
2518
+ const stats = this.countFolderContents(abs);
2519
+ if (!options?.recursive && (stats.entriesDeleted > 0 || stats.subfoldersDeleted > 0)) {
2520
+ const err = new Error(`Folder is not empty: ${normalized} (${stats.entriesDeleted} entries, ${stats.subfoldersDeleted} subfolders). Pass recursive: true to delete contents.`);
2521
+ err.code = 'FOLDER_NOT_EMPTY';
2522
+ err.blastRadius = stats;
2523
+ throw err;
2524
+ }
2525
+ const { rmSync } = await import('fs');
2526
+ rmSync(abs, { recursive: true, force: true });
2527
+ this.invalidateIndex();
2528
+ if (this.gitService) {
2529
+ const message = options?.commitMessage || commitMsg.folderDeleted(normalized);
2530
+ this.gitService.commitFiles([abs], message, options?.user ?? this.gitUser);
2531
+ }
2532
+ // No asset cleanup: deleting a subfolder deletes its ENTRIES, and entries
2533
+ // only reference assets (assets-referenced-not-owned P1). The assets are
2534
+ // homed to the repo (not the subfolder) and persist in the library.
2535
+ await this.pruneEmptyAncestors(dirname(abs));
2536
+ return stats;
2537
+ }
2538
+ /**
2539
+ * Count the entries (.md files with entity_type) and subfolders inside an
2540
+ * absolute directory, recursively. Used to surface blast radius in the
2541
+ * delete-folder approval UI.
2542
+ */
2543
+ countFolderContents(absDir) {
2544
+ let entriesDeleted = 0;
2545
+ const subfolders = new Set();
2546
+ const stack = [absDir];
2547
+ while (stack.length > 0) {
2548
+ const dir = stack.pop();
2549
+ let entries;
2550
+ try {
2551
+ entries = readdirSync(dir);
2552
+ }
2553
+ catch {
2554
+ continue;
2555
+ }
2556
+ for (const entry of entries) {
2557
+ if (entry.startsWith('.'))
2558
+ continue;
2559
+ if (WALK_IGNORE.has(entry))
2560
+ continue;
2561
+ const full = join(dir, entry);
2562
+ let st;
2563
+ try {
2564
+ st = statSync(full);
2565
+ }
2566
+ catch {
2567
+ continue;
2568
+ }
2569
+ if (st.isDirectory()) {
2570
+ subfolders.add(full);
2571
+ stack.push(full);
2572
+ }
2573
+ else if (st.isFile() && entry.endsWith('.md')) {
2574
+ entriesDeleted += 1;
2575
+ }
2576
+ }
2577
+ }
2578
+ return { entriesDeleted, subfoldersDeleted: subfolders.size };
2579
+ }
2580
+ /**
2581
+ * Return the storage backend the graph is bound to, or `undefined` if
2582
+ * no asset service is configured. Used by routes that need to branch
2583
+ * between local-stream and R2-redirect behavior.
2584
+ */
2585
+ getAssetStorage() {
2586
+ return this.assetService?.getStorage();
2587
+ }
2588
+ /** The asset storage service (raw key copy/exists), if configured. Used by
2589
+ * the one-time key migration, which operates on raw `{...}/{file}` keys. */
2590
+ getAssetService() {
2591
+ return this.assetService;
2592
+ }
2593
+ /**
2594
+ * Download the bytes of an asset OWNED by `assetEntityId` in THIS repo.
2595
+ * Used by the convert flow to turn a stored .docx/.xlsx into a derived
2596
+ * note/dataset. Backend-agnostic (local reads disk, R2 GetObjects).
2597
+ *
2598
+ * SECURITY: the asset's `asset_url` frontmatter is attacker-influenceable
2599
+ * (a folder member can create an `asset` entity whose url points at another
2600
+ * folder's storage key). So ownership is NOT taken from the url — it's
2601
+ * resolved through the authoritative asset index, scoped to
2602
+ * `(this.repoName, 'asset', assetEntityId)`, exactly like the GET
2603
+ * /api/assets serve route. The url is used only to disambiguate which of the
2604
+ * entity's own files to fetch (by filename). Unindexed legacy assets fall
2605
+ * back to the legacy `asset/{entityId}/{file}` key bound to this same entity
2606
+ * id; anything else is refused.
2607
+ */
2608
+ /**
2609
+ * Download an immutable-keyed asset's bytes by its assetId + filename. The
2610
+ * blob store is workspace-global (the key `{assetId}/{filename}` is
2611
+ * independent of repo), so any graph can fetch any asset; the caller — the
2612
+ * caption sweep — scopes to the rows it enumerated from assets.db.
2613
+ */
2614
+ async downloadAssetById(assetId, filename) {
2615
+ if (!this.assetService) {
2616
+ throw new Error('Asset service is not configured for this graph');
2617
+ }
2618
+ return await this.assetService.download(`${assetId}/${filename}`);
2619
+ }
2620
+ /** Pre-signed URL for an immutable-key asset ({assetId}/{filename}). */
2621
+ async getAssetSignedUrlByAssetId(assetId, filename, method = 'GET', expiresIn) {
2622
+ if (!this.assetService) {
2623
+ throw new Error('Asset service is not configured for this graph');
2624
+ }
2625
+ return await this.assetService.getSignedUrlByAssetId(assetId, filename, expiresIn, method);
2626
+ }
2627
+ /**
2628
+ * Mint a short-lived pre-signed URL for an asset. Only supported by
2629
+ * remote backends (R2); the local backend throws. `method` selects
2630
+ * the verb the URL is signed against — HEAD callers need a HEAD URL.
2631
+ */
2632
+ async getAssetSignedUrl(entityType, entityId, filename, keyPrefix, expiresIn, method = 'GET') {
2633
+ if (!this.assetService) {
2634
+ throw new Error('Asset service is not configured for this graph');
2635
+ }
2636
+ return await this.assetService.getSignedUrl(entityType, entityId, filename, keyPrefix, expiresIn, method);
2637
+ }
843
2638
  /**
844
2639
  * Upload asset for an entity
845
2640
  */
846
- async uploadAsset(entityType, entityId, filename, buffer, keyPrefix) {
2641
+ async uploadAsset(entityType, entityId, filename, buffer, keyPrefix,
2642
+ /** Auth user id of the uploader (web). Recorded in the asset index for
2643
+ * the "delete by uploader + workspace owner" rule. Omit for CLI/import/
2644
+ * agent uploads — stored as null (unknown → owner-only delete). */
2645
+ opts) {
847
2646
  if (!this.assetService) {
848
2647
  throw new Error('Asset service is not configured for this graph');
849
2648
  }
850
- return await this.assetService.upload(entityType, entityId, filename, buffer, keyPrefix);
2649
+ // Immutable-key path: generate a permanent assetId, store under
2650
+ // {assetId}/{filename}, and record authoritative ownership in the index.
2651
+ // The (entityType, entityId) args never shape ownership — renaming or
2652
+ // retyping the entity never moves the blob. `keyPrefix` is irrelevant here
2653
+ // (it only shaped the legacy {prefix}/{id} key).
2654
+ if (this.assetIndex) {
2655
+ // The model invariant is structural: no caller can mint an entity-owned
2656
+ // row. Omitting `owner` homes the asset to this graph's folder.
2657
+ const owner = opts?.owner ?? { type: 'folder', id: this.repoName };
2658
+ if (!FREE_STANDING_OWNER_TYPES.has(owner.type)) {
2659
+ throw new Error(`Asset owner must be free-standing (folder or workspace), got "${owner.type}" — entries reference assets, they never own them.`);
2660
+ }
2661
+ const assetId = randomUUID();
2662
+ const info = await this.assetService.uploadByAssetId(assetId, filename, buffer);
2663
+ this.assetIndex.put({
2664
+ assetId,
2665
+ repo: this.repoName,
2666
+ ownerType: owner.type,
2667
+ ownerId: owner.id,
2668
+ filename: info.filename,
2669
+ contentType: info.contentType,
2670
+ mediaType: info.mediaType,
2671
+ name: opts?.name ?? filename,
2672
+ uploaderUserId: opts?.uploaderUserId ?? null,
2673
+ size: buffer.length,
2674
+ });
2675
+ // Caption/OCR trigger (asset-as-embed): for an IMAGE row, seed a pending
2676
+ // index-state row keyed by this assetId + notify the live caption drain.
2677
+ // Fire-and-forget + image-gated inside ensureInitialAssetIndexState —
2678
+ // captioning must never block or fail the upload. Covers every path that
2679
+ // funnels through uploadAsset: import, manager upload, and editor embeds
2680
+ // (all folder-owned under assets-referenced-not-owned P1).
2681
+ this.vectorService?.ensureInitialAssetIndexState(assetId, {
2682
+ mediaType: info.mediaType,
2683
+ filename: info.filename,
2684
+ repo: this.repoName,
2685
+ }).catch(() => { });
2686
+ return { ...info, assetId, publicUrl: publicUrlForAssetId(assetId, info.filename) };
2687
+ }
2688
+ // The ownership index is a hard invariant — WorkspaceManager wires it onto
2689
+ // every graph. The old no-index branch wrote legacy {type}/{id}/{file}
2690
+ // blobs; that scheme (and its serve route) is retired, so a graph without an
2691
+ // index can no longer mint assets. Fail loud rather than write an unservable
2692
+ // legacy blob.
2693
+ throw new Error('uploadAsset requires the asset ownership index to be wired');
2694
+ }
2695
+ /** Inject the workspace-global asset ownership index (WorkspaceManager). */
2696
+ setAssetIndex(index) {
2697
+ this.assetIndex = index;
2698
+ }
2699
+ /** The asset ownership index, if wired. */
2700
+ getAssetIndex() {
2701
+ return this.assetIndex;
2702
+ }
2703
+ /** Delete a single index-tracked asset by its immutable id: blob + index row.
2704
+ * THE asset delete path (assets-referenced-not-owned: deleting an entry
2705
+ * never deletes assets, so deliberate asset deletion is always by-id).
2706
+ * Blob errors are non-fatal (the index row is the manager's source of
2707
+ * truth). No-op without an index/service. */
2708
+ async deleteAssetById(assetId, filename) {
2709
+ if (!this.assetIndex || !this.assetService)
2710
+ return;
2711
+ try {
2712
+ await this.assetService.deleteByAssetId(assetId, filename);
2713
+ }
2714
+ catch (err) {
2715
+ console.warn(`[asset] delete blob ${assetId} failed: ${err?.message ?? err}`);
2716
+ }
2717
+ // Isolated like the blob delete: when this runs from the import-failure
2718
+ // rollback, a throw here would mask the original import error the caller
2719
+ // must surface (mirrors the old rollback helper).
2720
+ try {
2721
+ this.assetIndex.delete(assetId);
2722
+ }
2723
+ catch (err) {
2724
+ console.warn(`[asset] delete index row ${assetId} failed: ${err?.message ?? err}`);
2725
+ }
2726
+ // Reap the caption index-state row (assetId-keyed). Fire-and-forget +
2727
+ // self-isolating so it can't mask the caller's error (e.g. import rollback).
2728
+ void this.vectorService?.deleteIndexStateSafe(assetId);
2729
+ }
2730
+ /** Re-point the owning coordinates of every asset owned by an entity, with
2731
+ * no blob movement (keys are immutable). Used by rename (ownerId),
2732
+ * changeType (ownerType), and cross-repo move/transfer (repo). No-op
2733
+ * without an index. */
2734
+ reassignOwnedAssets(fromType, fromId, changes) {
2735
+ this.assetIndex?.reassignOwner(this.repoName, fromType, fromId, changes);
851
2736
  }
852
2737
  /**
853
2738
  * List assets for an entity
@@ -870,61 +2755,34 @@ export class BaseGraphManager {
870
2755
  return await this.assetService.listAll();
871
2756
  }
872
2757
  /**
873
- * Enumerate (type, id) pairs in this repo by scanning the directory
874
- * structure only no markdown parse, no frontmatter validation. Used
875
- * by endpoints (like /api/repos/:repo/assets) that need to answer
876
- * "does entity X belong to this repo?" without paying the cost of
877
- * parsing every file in the repo.
2758
+ * Enumerate (type, id) pairs in this repo. Used by endpoints (like
2759
+ * /api/repos/:repo/assets) that need to answer "does entity X belong to
2760
+ * this repo?".
2761
+ *
2762
+ * Backed by the file index, which reads frontmatter — so entries in
2763
+ * nested folders are included alongside legacy {plural-type}/ entries.
878
2764
  */
879
2765
  listEntityIds() {
880
2766
  if (!existsSync(this.repoPath))
881
2767
  return [];
882
2768
  const results = [];
883
- let types = [];
884
- try {
885
- types = readdirSync(this.repoPath);
886
- }
887
- catch {
888
- return [];
889
- }
890
- for (const entry of types) {
891
- if (entry.startsWith('.'))
892
- continue;
893
- const typeDir = join(this.repoPath, entry);
894
- try {
895
- if (!statSync(typeDir).isDirectory())
896
- continue;
897
- }
898
- catch {
899
- continue;
900
- }
901
- const singularType = PLURAL_TO_SINGULAR[entry];
902
- if (!singularType)
903
- continue;
904
- let files = [];
905
- try {
906
- files = readdirSync(typeDir);
907
- }
908
- catch {
2769
+ for (const key of this.getFileIndex().keys()) {
2770
+ const slash = key.indexOf('/');
2771
+ if (slash <= 0)
909
2772
  continue;
910
- }
911
- for (const file of files) {
912
- if (file.startsWith('.') || !file.endsWith('.md'))
913
- continue;
914
- const id = file.replace(/\.md$/, '');
915
- results.push({ entityType: singularType, id });
916
- }
2773
+ results.push({ entityType: key.slice(0, slash), id: key.slice(slash + 1) });
917
2774
  }
918
2775
  return results;
919
2776
  }
920
2777
  /**
921
- * Delete an asset
2778
+ * Resolve the absolute on-disk markdown path for an entity. Goes through
2779
+ * the file index so it works for both the flat repo-root layout and
2780
+ * arbitrary nested folders. Use this anywhere callers need a path —
2781
+ * never reconstruct one from `${plural-type}/${id}.md`, which assumes
2782
+ * the legacy typed-folder layout and silently misses everything else.
922
2783
  */
923
- async deleteAsset(entityType, entityId, filename, keyPrefix) {
924
- if (!this.assetService) {
925
- throw new Error('Asset service is not configured for this graph');
926
- }
927
- return await this.assetService.delete(entityType, entityId, filename, keyPrefix);
2784
+ getEntityFilePath(entityType, entityId) {
2785
+ return this.findEntityFile(entityType, entityId);
928
2786
  }
929
2787
  /**
930
2788
  * Delete all assets for an entity (when entity is deleted)
@@ -936,66 +2794,104 @@ export class BaseGraphManager {
936
2794
  return await this.assetService.deleteAll(entityType, entityId, keyPrefix);
937
2795
  }
938
2796
  /**
939
- * Get dataset rows, optionally filtered
2797
+ * Resolve the authoritative CSV body for a dataset: the live editor body
2798
+ * when one exists (the grid may be ahead of disk), else the on-disk body.
2799
+ */
2800
+ datasetBody(entity, entityId) {
2801
+ return this.getLiveBody('dataset', entityId) ?? entity.document.content;
2802
+ }
2803
+ /**
2804
+ * Get dataset rows, optionally filtered. Columns come from the CSV header.
940
2805
  */
941
2806
  getDatasetRows(entityId, filter) {
942
2807
  const entity = this.get('dataset', entityId);
943
2808
  if (entity.data.entity_type !== 'dataset') {
944
2809
  throw new Error(`Entity ${entityId} is not a dataset`);
945
2810
  }
946
- const schema = entity.data.schema;
947
- const rows = this.csvService.parseRows(entity.document.content, schema);
2811
+ const rows = this.csvService.parseRows(this.datasetBody(entity, entityId));
948
2812
  if (filter && Object.keys(filter).length > 0) {
949
2813
  return this.csvService.filterRows(rows, filter);
950
2814
  }
951
2815
  return rows;
952
2816
  }
953
2817
  /**
954
- * Append rows to a dataset entity
2818
+ * Column names for a dataset, read from the CSV header. Independent of the
2819
+ * rows so callers get the real columns even for a header-only dataset or a
2820
+ * zero-match filter (where deriving columns from result rows would be empty).
2821
+ */
2822
+ getDatasetColumns(entityId) {
2823
+ const entity = this.get('dataset', entityId);
2824
+ if (entity.data.entity_type !== 'dataset') {
2825
+ throw new Error(`Entity ${entityId} is not a dataset`);
2826
+ }
2827
+ return this.csvService.columns(this.datasetBody(entity, entityId));
2828
+ }
2829
+ /**
2830
+ * Append rows to a dataset entity.
2831
+ *
2832
+ * The existing CSV body is preserved verbatim and the new rows are appended
2833
+ * after it — we do NOT re-parse and re-stringify existing rows. That keeps
2834
+ * datasets with duplicate column headers from collapsing (csv-parse's
2835
+ * object mode keeps only the last same-named column) and avoids reformatting
2836
+ * unrelated cells. New rows are mapped to the existing header columns; keys
2837
+ * not in the header are dropped (query the columns first). When the body has
2838
+ * no header yet, the columns are taken from the new rows.
2839
+ *
2840
+ * The write routes through `update()` so it shares the BodyWriteCoordinator
2841
+ * contract (per-doc lock, base-hash precondition, applyAgentContent, commit)
2842
+ * instead of writing to disk directly — same guarantees the agent's markdown
2843
+ * writes get. `baseRevision` makes a concurrent change since our read fail
2844
+ * loudly rather than silently back-stomp.
955
2845
  */
956
2846
  async appendDatasetRows(entityId, newRows, commitMessage) {
957
2847
  const entity = this.get('dataset', entityId);
958
2848
  if (entity.data.entity_type !== 'dataset') {
959
2849
  throw new Error(`Entity ${entityId} is not a dataset`);
960
2850
  }
961
- const schema = entity.data.schema;
962
- // Validate new rows against schema
963
- const validatedRows = newRows.map(row => {
964
- const stringRow = {};
2851
+ const base = this.datasetBody(entity, entityId);
2852
+ // Normalize incoming rows to string cells (agents may send numbers/bools).
2853
+ const normalizedRows = newRows.map(row => {
2854
+ const out = {};
965
2855
  for (const [k, v] of Object.entries(row)) {
966
- stringRow[k] = v !== null && v !== undefined ? String(v) : '';
2856
+ out[k] = v !== null && v !== undefined ? String(v) : '';
967
2857
  }
968
- return this.csvService.validateRow(stringRow, schema);
2858
+ return out;
969
2859
  });
970
- // Parse existing rows and append
971
- const existingRows = this.csvService.parseRows(entity.document.content, schema);
972
- const allRows = [...existingRows, ...validatedRows];
973
- // Stringify back to CSV
974
- const newContent = this.csvService.stringifyRows(allRows, schema);
975
- // Update the entity file (update content, bump updated_at)
976
- const entityPath = this.getEntityPath('dataset', entityId);
977
- const updatedData = {
978
- ...entity.data,
979
- updated_at: new Date().toISOString(),
980
- updated_by: this.gitUser.id,
981
- };
982
- this.markdownService.writeFile(entityPath, {
983
- frontmatter: updatedData,
2860
+ let newContent;
2861
+ const headerColumns = this.csvService.columns(base);
2862
+ if (headerColumns.length === 0) {
2863
+ // No header yet: derive columns from the new rows and write a fresh CSV.
2864
+ const columns = [];
2865
+ for (const row of normalizedRows) {
2866
+ for (const key of Object.keys(row)) {
2867
+ if (!columns.includes(key))
2868
+ columns.push(key);
2869
+ }
2870
+ }
2871
+ newContent = this.csvService.stringifyRows(normalizedRows, columns);
2872
+ }
2873
+ else {
2874
+ // Preserve the existing body byte-for-byte; append only the new rows.
2875
+ const appended = this.csvService.stringifyRows(normalizedRows, headerColumns, false);
2876
+ const baseTrimmed = base.replace(/\n+$/, '');
2877
+ newContent = appended ? `${baseTrimmed}\n${appended}` : `${baseTrimmed}\n`;
2878
+ }
2879
+ return this.update({
2880
+ entityType: 'dataset',
2881
+ entityId,
984
2882
  content: newContent,
985
- wikilinks: this.markdownService.extractWikilinks(newContent, updatedData),
2883
+ source: 'server-write',
2884
+ // Precondition is checked against DISK inside the coordinator's lock, so
2885
+ // baseRevision must be the DISK hash — not `base`, which is the LIVE body
2886
+ // (datasetBody prefers the live ytext). When the CSV editor has unsaved
2887
+ // edits the live body is ahead of disk; hashing it here would always
2888
+ // mismatch the disk check and throw StaleBaseRevisionError (masked today
2889
+ // by the live-typing gate, exposed once that gate is removed). The
2890
+ // appended `newContent` still derives from the live body, so live edits
2891
+ // are preserved; applyAgentContent then merges the rows into the editor.
2892
+ baseRevision: sha256OfBody(entity.document.content),
2893
+ commitMessage: commitMessage || `Append ${normalizedRows.length} row(s) to dataset: ${entityId}`,
986
2894
  });
987
- if (this.gitService) {
988
- const transaction = this.gitService.createTransaction();
989
- transaction.add({
990
- operation: 'update',
991
- entityType: 'dataset',
992
- entityId,
993
- filePath: entityPath,
994
- });
995
- const message = commitMessage || `Append ${validatedRows.length} row(s) to dataset: ${entityId}`;
996
- await transaction.commit(this.gitUser, message);
997
- }
998
- return this.get('dataset', entityId);
999
2895
  }
1000
2896
  /**
1001
2897
  * Get repository status