studiograph 1.1.3 → 1.2.0-beta.10

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 (356) hide show
  1. package/README.md +2 -8
  2. package/dist/agent/orchestrator.d.ts +53 -0
  3. package/dist/agent/orchestrator.js +197 -12
  4. package/dist/agent/orchestrator.js.map +1 -1
  5. package/dist/agent/prompts/system.md +43 -11
  6. package/dist/agent/skill-loader.d.ts +48 -0
  7. package/dist/agent/skill-loader.js +166 -0
  8. package/dist/agent/skill-loader.js.map +1 -0
  9. package/dist/agent/skills/{bundled/enrich-entities.md → enrich-entities.md} +12 -0
  10. package/dist/agent/skills/entity-schema.md +502 -0
  11. package/dist/agent/skills/obsidian-source-setup.md +246 -0
  12. package/dist/agent/skills/sync-configuration.md +144 -0
  13. package/dist/agent/skills/sync-setup.md +84 -0
  14. package/dist/agent/tools/connector-tools.d.ts +37 -0
  15. package/dist/agent/tools/connector-tools.js +132 -0
  16. package/dist/agent/tools/connector-tools.js.map +1 -0
  17. package/dist/agent/tools/fs-tools.d.ts +39 -0
  18. package/dist/agent/tools/fs-tools.js +106 -0
  19. package/dist/agent/tools/fs-tools.js.map +1 -0
  20. package/dist/agent/tools/graph-tools.d.ts +33 -1
  21. package/dist/agent/tools/graph-tools.js +192 -44
  22. package/dist/agent/tools/graph-tools.js.map +1 -1
  23. package/dist/agent/tools/load-skill.d.ts +1 -1
  24. package/dist/agent/tools/load-skill.js +1 -1
  25. package/dist/agent/tools/load-skill.js.map +1 -1
  26. package/dist/agent/tools/ops-tools.d.ts +24 -0
  27. package/dist/agent/tools/ops-tools.js +542 -0
  28. package/dist/agent/tools/ops-tools.js.map +1 -0
  29. package/dist/agent/tools/sync-tools.d.ts +33 -0
  30. package/dist/agent/tools/sync-tools.js +947 -0
  31. package/dist/agent/tools/sync-tools.js.map +1 -0
  32. package/dist/auth/github.d.ts +11 -8
  33. package/dist/auth/github.js +56 -75
  34. package/dist/auth/github.js.map +1 -1
  35. package/dist/cli/colors.d.ts +54 -0
  36. package/dist/cli/colors.js +133 -0
  37. package/dist/cli/colors.js.map +1 -0
  38. package/dist/cli/commands/app.js +121 -18
  39. package/dist/cli/commands/app.js.map +1 -1
  40. package/dist/cli/commands/auth.d.ts +1 -1
  41. package/dist/cli/commands/auth.js +26 -10
  42. package/dist/cli/commands/auth.js.map +1 -1
  43. package/dist/cli/commands/clone.d.ts +9 -0
  44. package/dist/cli/commands/clone.js +167 -0
  45. package/dist/cli/commands/clone.js.map +1 -0
  46. package/dist/cli/commands/commit.d.ts +8 -0
  47. package/dist/cli/commands/commit.js +44 -0
  48. package/dist/cli/commands/commit.js.map +1 -0
  49. package/dist/cli/commands/connector.js +10 -2
  50. package/dist/cli/commands/connector.js.map +1 -1
  51. package/dist/cli/commands/deploy.d.ts +4 -6
  52. package/dist/cli/commands/deploy.js +292 -115
  53. package/dist/cli/commands/deploy.js.map +1 -1
  54. package/dist/cli/commands/enrich.d.ts +11 -0
  55. package/dist/cli/commands/enrich.js +135 -0
  56. package/dist/cli/commands/enrich.js.map +1 -0
  57. package/dist/cli/commands/graphrag.d.ts +12 -0
  58. package/dist/cli/commands/graphrag.js +122 -0
  59. package/dist/cli/commands/graphrag.js.map +1 -0
  60. package/dist/cli/commands/init.js +29 -196
  61. package/dist/cli/commands/init.js.map +1 -1
  62. package/dist/cli/commands/join.js +23 -3
  63. package/dist/cli/commands/join.js.map +1 -1
  64. package/dist/cli/commands/orphans.d.ts +8 -0
  65. package/dist/cli/commands/orphans.js +125 -0
  66. package/dist/cli/commands/orphans.js.map +1 -0
  67. package/dist/cli/commands/provision.js +11 -7
  68. package/dist/cli/commands/provision.js.map +1 -1
  69. package/dist/cli/commands/r2.js +8 -2
  70. package/dist/cli/commands/r2.js.map +1 -1
  71. package/dist/cli/commands/redeploy.d.ts +9 -0
  72. package/dist/cli/commands/redeploy.js +203 -0
  73. package/dist/cli/commands/redeploy.js.map +1 -0
  74. package/dist/cli/commands/reset.d.ts +12 -0
  75. package/dist/cli/commands/reset.js +137 -0
  76. package/dist/cli/commands/reset.js.map +1 -0
  77. package/dist/cli/commands/resolve.d.ts +8 -0
  78. package/dist/cli/commands/resolve.js +85 -0
  79. package/dist/cli/commands/resolve.js.map +1 -0
  80. package/dist/cli/commands/review.d.ts +19 -0
  81. package/dist/cli/commands/review.js +128 -0
  82. package/dist/cli/commands/review.js.map +1 -0
  83. package/dist/cli/commands/serve.js +55 -8
  84. package/dist/cli/commands/serve.js.map +1 -1
  85. package/dist/cli/commands/source.d.ts +16 -0
  86. package/dist/cli/commands/source.js +159 -0
  87. package/dist/cli/commands/source.js.map +1 -0
  88. package/dist/cli/commands/start.js +307 -55
  89. package/dist/cli/commands/start.js.map +1 -1
  90. package/dist/cli/commands/sync-entities.d.ts +13 -0
  91. package/dist/cli/commands/sync-entities.js +242 -0
  92. package/dist/cli/commands/sync-entities.js.map +1 -0
  93. package/dist/cli/commands/sync.js +81 -5
  94. package/dist/cli/commands/sync.js.map +1 -1
  95. package/dist/cli/index.js +45 -9
  96. package/dist/cli/index.js.map +1 -1
  97. package/dist/cli/scaffolding.d.ts +2 -1
  98. package/dist/cli/scaffolding.js +38 -23
  99. package/dist/cli/scaffolding.js.map +1 -1
  100. package/dist/cli/setup-wizard.d.ts +30 -0
  101. package/dist/cli/setup-wizard.js +244 -0
  102. package/dist/cli/setup-wizard.js.map +1 -0
  103. package/dist/cli/sync-review-interactive.d.ts +31 -0
  104. package/dist/cli/sync-review-interactive.js +393 -0
  105. package/dist/cli/sync-review-interactive.js.map +1 -0
  106. package/dist/cli/theme.d.ts +31 -0
  107. package/dist/cli/theme.js +116 -0
  108. package/dist/cli/theme.js.map +1 -0
  109. package/dist/core/graph.d.ts +40 -12
  110. package/dist/core/graph.js +344 -104
  111. package/dist/core/graph.js.map +1 -1
  112. package/dist/core/migration-runner.d.ts +4 -1
  113. package/dist/core/migration-runner.js +14 -3
  114. package/dist/core/migration-runner.js.map +1 -1
  115. package/dist/core/types.d.ts +2 -4
  116. package/dist/core/types.js +1 -2
  117. package/dist/core/types.js.map +1 -1
  118. package/dist/core/user-config.d.ts +12 -0
  119. package/dist/core/user-config.js.map +1 -1
  120. package/dist/core/workspace-manager.d.ts +6 -1
  121. package/dist/core/workspace-manager.js +29 -4
  122. package/dist/core/workspace-manager.js.map +1 -1
  123. package/dist/core/workspace.d.ts +13 -5
  124. package/dist/core/workspace.js +115 -33
  125. package/dist/core/workspace.js.map +1 -1
  126. package/dist/lib/lib/utils.d.ts +2 -0
  127. package/dist/lib/lib/utils.js +6 -0
  128. package/dist/lib/lib/utils.js.map +1 -0
  129. package/dist/mcp/connector-manager.d.ts +7 -3
  130. package/dist/mcp/connector-manager.js +61 -16
  131. package/dist/mcp/connector-manager.js.map +1 -1
  132. package/dist/mcp/connectors/asana.d.ts +2 -0
  133. package/dist/mcp/connectors/asana.js +20 -0
  134. package/dist/mcp/connectors/asana.js.map +1 -0
  135. package/dist/mcp/connectors/definitions.d.ts +3 -1
  136. package/dist/mcp/connectors/definitions.js +19 -127
  137. package/dist/mcp/connectors/definitions.js.map +1 -1
  138. package/dist/mcp/connectors/figma.d.ts +5 -0
  139. package/dist/mcp/connectors/figma.js +21 -0
  140. package/dist/mcp/connectors/figma.js.map +1 -0
  141. package/dist/mcp/connectors/gdrive.d.ts +2 -0
  142. package/dist/mcp/connectors/gdrive.js +20 -0
  143. package/dist/mcp/connectors/gdrive.js.map +1 -0
  144. package/dist/mcp/connectors/granola.d.ts +2 -0
  145. package/dist/mcp/connectors/granola.js +12 -0
  146. package/dist/mcp/connectors/granola.js.map +1 -0
  147. package/dist/mcp/connectors/linear.d.ts +2 -0
  148. package/dist/mcp/connectors/linear.js +19 -0
  149. package/dist/mcp/connectors/linear.js.map +1 -0
  150. package/dist/mcp/connectors/obsidian.d.ts +2 -0
  151. package/dist/mcp/connectors/obsidian.js +19 -0
  152. package/dist/mcp/connectors/obsidian.js.map +1 -0
  153. package/dist/mcp/connectors/pipedrive.d.ts +2 -0
  154. package/dist/mcp/connectors/pipedrive.js +20 -0
  155. package/dist/mcp/connectors/pipedrive.js.map +1 -0
  156. package/dist/mcp/connectors/slack.d.ts +2 -0
  157. package/dist/mcp/connectors/slack.js +21 -0
  158. package/dist/mcp/connectors/slack.js.map +1 -0
  159. package/dist/mcp/oauth-provider.d.ts +41 -0
  160. package/dist/mcp/oauth-provider.js +160 -0
  161. package/dist/mcp/oauth-provider.js.map +1 -0
  162. package/dist/server/index.js +97 -11
  163. package/dist/server/index.js.map +1 -1
  164. package/dist/server/plugin-loader.d.ts +8 -0
  165. package/dist/server/plugin-loader.js +54 -2
  166. package/dist/server/plugin-loader.js.map +1 -1
  167. package/dist/server/routes/chat.d.ts +3 -2
  168. package/dist/server/routes/chat.js +67 -16
  169. package/dist/server/routes/chat.js.map +1 -1
  170. package/dist/server/routes/git-api.d.ts +9 -0
  171. package/dist/server/routes/git-api.js +82 -0
  172. package/dist/server/routes/git-api.js.map +1 -0
  173. package/dist/server/routes/graph-api.d.ts +2 -2
  174. package/dist/server/routes/graph-api.js +164 -3
  175. package/dist/server/routes/graph-api.js.map +1 -1
  176. package/dist/server/routes/workspace-api.d.ts +9 -0
  177. package/dist/server/routes/workspace-api.js +170 -0
  178. package/dist/server/routes/workspace-api.js.map +1 -0
  179. package/dist/services/assets/base.d.ts +2 -2
  180. package/dist/services/assets/base.js +4 -4
  181. package/dist/services/assets/base.js.map +1 -1
  182. package/dist/services/assets/index.d.ts +6 -6
  183. package/dist/services/assets/index.js +12 -12
  184. package/dist/services/assets/index.js.map +1 -1
  185. package/dist/services/git.d.ts +31 -0
  186. package/dist/services/git.js +92 -3
  187. package/dist/services/git.js.map +1 -1
  188. package/dist/services/github-provisioner.d.ts +8 -3
  189. package/dist/services/github-provisioner.js +35 -8
  190. package/dist/services/github-provisioner.js.map +1 -1
  191. package/dist/services/lint-service.js +8 -1
  192. package/dist/services/lint-service.js.map +1 -1
  193. package/dist/services/markdown.js +3 -3
  194. package/dist/services/markdown.js.map +1 -1
  195. package/dist/services/memory-service.d.ts +1 -1
  196. package/dist/services/memory-service.js +2 -3
  197. package/dist/services/memory-service.js.map +1 -1
  198. package/dist/services/orphan-service.d.ts +31 -0
  199. package/dist/services/orphan-service.js +100 -0
  200. package/dist/services/orphan-service.js.map +1 -0
  201. package/dist/services/sync/commit.d.ts +60 -0
  202. package/dist/services/sync/commit.js +370 -0
  203. package/dist/services/sync/commit.js.map +1 -0
  204. package/dist/services/sync/context-index.d.ts +69 -0
  205. package/dist/services/sync/context-index.js +280 -0
  206. package/dist/services/sync/context-index.js.map +1 -0
  207. package/dist/services/sync/derive.d.ts +34 -0
  208. package/dist/services/sync/derive.js +164 -0
  209. package/dist/services/sync/derive.js.map +1 -0
  210. package/dist/services/sync/enrichment-state.d.ts +31 -0
  211. package/dist/services/sync/enrichment-state.js +63 -0
  212. package/dist/services/sync/enrichment-state.js.map +1 -0
  213. package/dist/services/sync/enrichment.d.ts +25 -0
  214. package/dist/services/sync/enrichment.js +121 -0
  215. package/dist/services/sync/enrichment.js.map +1 -0
  216. package/dist/services/sync/frontmatter-extractor.d.ts +40 -0
  217. package/dist/services/sync/frontmatter-extractor.js +273 -0
  218. package/dist/services/sync/frontmatter-extractor.js.map +1 -0
  219. package/dist/services/sync/graph-match-state.d.ts +33 -0
  220. package/dist/services/sync/graph-match-state.js +61 -0
  221. package/dist/services/sync/graph-match-state.js.map +1 -0
  222. package/dist/services/sync/graph-match.d.ts +53 -0
  223. package/dist/services/sync/graph-match.js +316 -0
  224. package/dist/services/sync/graph-match.js.map +1 -0
  225. package/dist/services/sync/graphrag-client.d.ts +43 -0
  226. package/dist/services/sync/graphrag-client.js +94 -0
  227. package/dist/services/sync/graphrag-client.js.map +1 -0
  228. package/dist/services/sync/graphrag-config.d.ts +16 -0
  229. package/dist/services/sync/graphrag-config.js +39 -0
  230. package/dist/services/sync/graphrag-config.js.map +1 -0
  231. package/dist/services/sync/graphrag-context.d.ts +14 -0
  232. package/dist/services/sync/graphrag-context.js +109 -0
  233. package/dist/services/sync/graphrag-context.js.map +1 -0
  234. package/dist/services/sync/graphrag-indexer.d.ts +30 -0
  235. package/dist/services/sync/graphrag-indexer.js +358 -0
  236. package/dist/services/sync/graphrag-indexer.js.map +1 -0
  237. package/dist/services/sync/llm.d.ts +32 -0
  238. package/dist/services/sync/llm.js +115 -0
  239. package/dist/services/sync/llm.js.map +1 -0
  240. package/dist/services/sync/mcp-client.d.ts +59 -0
  241. package/dist/services/sync/mcp-client.js +285 -0
  242. package/dist/services/sync/mcp-client.js.map +1 -0
  243. package/dist/services/sync/model-factory.d.ts +10 -0
  244. package/dist/services/sync/model-factory.js +24 -0
  245. package/dist/services/sync/model-factory.js.map +1 -0
  246. package/dist/services/sync/name-quality.d.ts +31 -0
  247. package/dist/services/sync/name-quality.js +60 -0
  248. package/dist/services/sync/name-quality.js.map +1 -0
  249. package/dist/services/sync/output-schemas.d.ts +92 -0
  250. package/dist/services/sync/output-schemas.js +43 -0
  251. package/dist/services/sync/output-schemas.js.map +1 -0
  252. package/dist/services/sync/prompts.d.ts +19 -0
  253. package/dist/services/sync/prompts.js +128 -0
  254. package/dist/services/sync/prompts.js.map +1 -0
  255. package/dist/services/sync/reconciler.d.ts +48 -0
  256. package/dist/services/sync/reconciler.js +295 -0
  257. package/dist/services/sync/reconciler.js.map +1 -0
  258. package/dist/services/sync/source-config.d.ts +45 -0
  259. package/dist/services/sync/source-config.js +208 -0
  260. package/dist/services/sync/source-config.js.map +1 -0
  261. package/dist/services/sync/source-definitions/asana.d.ts +15 -0
  262. package/dist/services/sync/source-definitions/asana.js +48 -0
  263. package/dist/services/sync/source-definitions/asana.js.map +1 -0
  264. package/dist/services/sync/source-definitions/definitions.d.ts +21 -0
  265. package/dist/services/sync/source-definitions/definitions.js +26 -0
  266. package/dist/services/sync/source-definitions/definitions.js.map +1 -0
  267. package/dist/services/sync/source-definitions/gdrive.d.ts +16 -0
  268. package/dist/services/sync/source-definitions/gdrive.js +68 -0
  269. package/dist/services/sync/source-definitions/gdrive.js.map +1 -0
  270. package/dist/services/sync/source-definitions/granola.d.ts +2 -0
  271. package/dist/services/sync/source-definitions/granola.js +28 -0
  272. package/dist/services/sync/source-definitions/granola.js.map +1 -0
  273. package/dist/services/sync/source-definitions/linear.d.ts +2 -0
  274. package/dist/services/sync/source-definitions/linear.js +60 -0
  275. package/dist/services/sync/source-definitions/linear.js.map +1 -0
  276. package/dist/services/sync/source-definitions/obsidian.d.ts +2 -0
  277. package/dist/services/sync/source-definitions/obsidian.js +55 -0
  278. package/dist/services/sync/source-definitions/obsidian.js.map +1 -0
  279. package/dist/services/sync/source-definitions/pipedrive.d.ts +2 -0
  280. package/dist/services/sync/source-definitions/pipedrive.js +52 -0
  281. package/dist/services/sync/source-definitions/pipedrive.js.map +1 -0
  282. package/dist/services/sync/staging.d.ts +53 -0
  283. package/dist/services/sync/staging.js +131 -0
  284. package/dist/services/sync/staging.js.map +1 -0
  285. package/dist/services/sync/structured-extractor.d.ts +49 -0
  286. package/dist/services/sync/structured-extractor.js +344 -0
  287. package/dist/services/sync/structured-extractor.js.map +1 -0
  288. package/dist/services/sync/sync-runner.d.ts +32 -0
  289. package/dist/services/sync/sync-runner.js +195 -0
  290. package/dist/services/sync/sync-runner.js.map +1 -0
  291. package/dist/services/sync/sync-state.d.ts +43 -0
  292. package/dist/services/sync/sync-state.js +154 -0
  293. package/dist/services/sync/sync-state.js.map +1 -0
  294. package/dist/services/sync/types.d.ts +203 -0
  295. package/dist/services/sync/types.js +8 -0
  296. package/dist/services/sync/types.js.map +1 -0
  297. package/dist/services/sync/unstructured-extractor.d.ts +29 -0
  298. package/dist/services/sync/unstructured-extractor.js +197 -0
  299. package/dist/services/sync/unstructured-extractor.js.map +1 -0
  300. package/dist/services/vector-service.d.ts +11 -0
  301. package/dist/services/vector-service.js +42 -0
  302. package/dist/services/vector-service.js.map +1 -1
  303. package/dist/utils/git.d.ts +31 -4
  304. package/dist/utils/git.js +65 -7
  305. package/dist/utils/git.js.map +1 -1
  306. package/dist/utils/merge-resolver.d.ts +60 -0
  307. package/dist/utils/merge-resolver.js +304 -0
  308. package/dist/utils/merge-resolver.js.map +1 -0
  309. package/dist/utils/preflight.d.ts +2 -1
  310. package/dist/utils/preflight.js +8 -1
  311. package/dist/utils/preflight.js.map +1 -1
  312. package/dist/utils/workspace-config.d.ts +8 -0
  313. package/dist/utils/workspace-config.js +22 -0
  314. package/dist/utils/workspace-config.js.map +1 -0
  315. package/dist/web/_app/env.js +1 -0
  316. package/dist/web/_app/immutable/assets/0.BwJV4fvl.css +1 -0
  317. package/dist/web/_app/immutable/assets/2.DRHi7ABa.css +1 -0
  318. package/dist/web/_app/immutable/assets/3.CnAQdPKU.css +1 -0
  319. package/dist/web/_app/immutable/assets/4.BG92MufE.css +1 -0
  320. package/dist/web/_app/immutable/assets/5.CfrQIPYs.css +1 -0
  321. package/dist/web/_app/immutable/assets/ChatPanel.BNzaCFTL.css +1 -0
  322. package/dist/web/_app/immutable/assets/editor.D6Rjo7RE.css +1 -0
  323. package/dist/web/_app/immutable/chunks/4QY4j-jX.js +1 -0
  324. package/dist/web/_app/immutable/chunks/B4G4uWkV.js +1 -0
  325. package/dist/web/_app/immutable/chunks/BB_5th5W.js +3383 -0
  326. package/dist/web/_app/immutable/chunks/CUzqHQY_.js +1 -0
  327. package/dist/web/_app/immutable/chunks/CYrVHOHA.js +1 -0
  328. package/dist/web/_app/immutable/chunks/CksE5s8n.js +1 -0
  329. package/dist/web/_app/immutable/chunks/CqkleIqs.js +1 -0
  330. package/dist/web/_app/immutable/chunks/D0JBkEPE.js +86 -0
  331. package/dist/web/_app/immutable/chunks/D2K5GzLj.js +1 -0
  332. package/dist/web/_app/immutable/chunks/DEkoussQ.js +2 -0
  333. package/dist/web/_app/immutable/chunks/DW-KmHRi.js +1 -0
  334. package/dist/web/_app/immutable/chunks/DcCr4z9k.js +1 -0
  335. package/dist/web/_app/immutable/chunks/Dg2wWwJu.js +2 -0
  336. package/dist/web/_app/immutable/chunks/Dh_H7Owr.js +18 -0
  337. package/dist/web/_app/immutable/chunks/DnlgZ_Tk.js +5 -0
  338. package/dist/web/_app/immutable/chunks/DtVH--hH.js +6 -0
  339. package/dist/web/_app/immutable/chunks/PPVm8Dsz.js +1 -0
  340. package/dist/web/_app/immutable/chunks/SBEriEQR.js +1 -0
  341. package/dist/web/_app/immutable/chunks/WSUKABI_.js +1 -0
  342. package/dist/web/_app/immutable/chunks/YTERt3Ul.js +6 -0
  343. package/dist/web/_app/immutable/chunks/m_R7HatS.js +1 -0
  344. package/dist/web/_app/immutable/chunks/vlmEnjfO.js +1 -0
  345. package/dist/web/_app/immutable/entry/app.B9pwpU_x.js +2 -0
  346. package/dist/web/_app/immutable/entry/start.CDuNCEco.js +1 -0
  347. package/dist/web/_app/immutable/nodes/0.YEQhw1us.js +1 -0
  348. package/dist/web/_app/immutable/nodes/1.CvmHhpRy.js +1 -0
  349. package/dist/web/_app/immutable/nodes/2.c5k1r7dq.js +1 -0
  350. package/dist/web/_app/immutable/nodes/3.B24tcdLK.js +1 -0
  351. package/dist/web/_app/immutable/nodes/4.kX2m5Pp9.js +16 -0
  352. package/dist/web/_app/immutable/nodes/5.DELt6wTD.js +2 -0
  353. package/dist/web/_app/version.json +1 -0
  354. package/dist/web/index.html +41 -0
  355. package/package.json +21 -7
  356. /package/dist/agent/skills/{bundled/gather-context.md → gather-context.md} +0 -0
@@ -0,0 +1,947 @@
1
+ /**
2
+ * Sync & Enrich Tools for Studiograph Agent
3
+ *
4
+ * These tools allow the agent to orchestrate sync and enrich operations:
5
+ * - Run the sync pipeline, apply staging, commit
6
+ * - Run LLM enrichment on entities
7
+ * - Manage source configs
8
+ */
9
+ import { Type as T } from '@sinclair/typebox';
10
+ import { spawnSync } from 'child_process';
11
+ import { gitPush, gitPull } from '../../utils/git.js';
12
+ import { existsSync } from 'fs';
13
+ import { join } from 'path';
14
+ import { homedir } from 'os';
15
+ import { runSync } from '../../services/sync/sync-runner.js';
16
+ import { commitStaging, applyStaging, commitRepos } from '../../services/sync/commit.js';
17
+ import { runEnrichment } from '../../services/sync/enrichment.js';
18
+ import { StagingManager } from '../../services/sync/staging.js';
19
+ import { SourceConfigManager } from '../../services/sync/source-config.js';
20
+ import { ConnectorManager } from '../../mcp/connector-manager.js';
21
+ import { SOURCE_DEFINITIONS } from '../../services/sync/source-definitions/definitions.js';
22
+ import { wrapToolResult } from './graph-tools.js';
23
+ import { VectorService } from '../../services/vector-service.js';
24
+ import { BaseGraphManager } from '../../core/graph.js';
25
+ import { loadUserConfig } from '../../core/user-config.js';
26
+ /**
27
+ * Create sync tools for the agent
28
+ */
29
+ export function createSyncTools(workspacePath, workspaceConfig, registryRef) {
30
+ const getRegistry = () => registryRef?.registry ?? null;
31
+ return [
32
+ // Run sync pipeline
33
+ {
34
+ name: 'sync_run',
35
+ label: 'Run Sync',
36
+ description: 'Run the sync pipeline to extract data from external sources into staging. Review with sync_status before committing.',
37
+ parameters: T.Object({
38
+ sources: T.Optional(T.Array(T.String(), { description: 'Source names to sync (default: all enabled)' })),
39
+ dryRun: T.Optional(T.Boolean({ description: 'Preview without writing staging files' })),
40
+ incremental: T.Optional(T.Boolean({ description: 'Only sync records updated since last sync' })),
41
+ }),
42
+ execute: async (toolCallId, params) => {
43
+ try {
44
+ const progress = [];
45
+ const result = await runSync({
46
+ workspacePath,
47
+ sources: params.sources,
48
+ dryRun: params.dryRun,
49
+ incremental: params.incremental,
50
+ schemaExtensions: workspaceConfig.schema_extensions,
51
+ }, (msg) => progress.push(msg));
52
+ return wrapToolResult({
53
+ success: true,
54
+ result,
55
+ progress: progress.join('\n'),
56
+ });
57
+ }
58
+ catch (error) {
59
+ return wrapToolResult({
60
+ success: false,
61
+ error: error instanceof Error ? error.message : 'Unknown error',
62
+ });
63
+ }
64
+ },
65
+ },
66
+ // Check staging status
67
+ {
68
+ name: 'sync_status',
69
+ label: 'Sync Status',
70
+ description: 'Check what is currently in staging (pending review). Shows counts of creates, updates, and skips by entity type.',
71
+ parameters: T.Object({}),
72
+ execute: async (toolCallId, params) => {
73
+ try {
74
+ const staging = new StagingManager(workspacePath);
75
+ const summary = staging.getSummary();
76
+ return wrapToolResult({
77
+ success: true,
78
+ hasStaging: summary.total > 0,
79
+ summary,
80
+ });
81
+ }
82
+ catch (error) {
83
+ return wrapToolResult({
84
+ success: false,
85
+ error: error instanceof Error ? error.message : 'Unknown error',
86
+ });
87
+ }
88
+ },
89
+ },
90
+ // Apply staging to entity files (no git)
91
+ {
92
+ name: 'sync_apply',
93
+ label: 'Apply Staging',
94
+ description: 'Apply staging files to the knowledge graph (writes entity files, does not git commit). Always confirm with the user before calling this.',
95
+ parameters: T.Object({
96
+ force: T.Optional(T.Boolean({ description: 'Force apply even with uncommitted changes in repos' })),
97
+ }),
98
+ execute: async (toolCallId, params) => {
99
+ try {
100
+ const progress = [];
101
+ const result = await applyStaging(workspacePath, (msg) => progress.push(msg), { force: params.force });
102
+ return wrapToolResult({
103
+ success: true,
104
+ result: { created: result.created, updated: result.updated, skipped: result.skipped },
105
+ progress: progress.join('\n'),
106
+ nextSteps: 'Review changes with workspace_review, then commit with workspace_commit or studiograph commit',
107
+ });
108
+ }
109
+ catch (error) {
110
+ return wrapToolResult({
111
+ success: false,
112
+ error: error instanceof Error ? error.message : 'Unknown error',
113
+ });
114
+ }
115
+ },
116
+ },
117
+ // Git commit across workspace repos
118
+ {
119
+ name: 'workspace_commit',
120
+ label: 'Commit Changes',
121
+ description: 'Git commit entity changes across all workspace repos. Works after sync_apply, enrich_run, or manual edits. Always confirm with the user before calling this.',
122
+ parameters: T.Object({
123
+ messagePrefix: T.Optional(T.String({ description: 'Commit message prefix (default: "update")' })),
124
+ }),
125
+ execute: async (toolCallId, params) => {
126
+ try {
127
+ const progress = [];
128
+ await commitRepos(workspacePath, (msg) => progress.push(msg), { messagePrefix: params.messagePrefix ?? 'update' });
129
+ return wrapToolResult({
130
+ success: true,
131
+ progress: progress.join('\n'),
132
+ });
133
+ }
134
+ catch (error) {
135
+ return wrapToolResult({
136
+ success: false,
137
+ error: error instanceof Error ? error.message : 'Unknown error',
138
+ });
139
+ }
140
+ },
141
+ },
142
+ // Commit staging to graph (deprecated — kept for backwards compatibility)
143
+ {
144
+ name: 'sync_commit',
145
+ label: 'Commit Staging (deprecated)',
146
+ description: 'DEPRECATED: Use sync_apply then workspace_commit instead. Commits staging files and auto git-commits.',
147
+ parameters: T.Object({
148
+ force: T.Optional(T.Boolean({ description: 'Force commit even with uncommitted changes in repos' })),
149
+ }),
150
+ execute: async (toolCallId, params) => {
151
+ try {
152
+ const progress = [];
153
+ progress.push('Note: sync_commit is deprecated. Use sync_apply then workspace_commit instead.');
154
+ const result = await commitStaging(workspacePath, (msg) => progress.push(msg), { force: params.force, noGit: false });
155
+ return wrapToolResult({
156
+ success: true,
157
+ result,
158
+ progress: progress.join('\n'),
159
+ });
160
+ }
161
+ catch (error) {
162
+ return wrapToolResult({
163
+ success: false,
164
+ error: error instanceof Error ? error.message : 'Unknown error',
165
+ });
166
+ }
167
+ },
168
+ },
169
+ // Abort staging
170
+ {
171
+ name: 'sync_abort',
172
+ label: 'Abort Staging',
173
+ description: 'Discard all staging files without committing.',
174
+ parameters: T.Object({}),
175
+ execute: async (toolCallId, params) => {
176
+ try {
177
+ const staging = new StagingManager(workspacePath);
178
+ const hadStaging = staging.hasStaging();
179
+ staging.clear();
180
+ return wrapToolResult({
181
+ success: true,
182
+ hadStaging,
183
+ message: hadStaging ? 'Staging cleared.' : 'No staging files to clear.',
184
+ });
185
+ }
186
+ catch (error) {
187
+ return wrapToolResult({
188
+ success: false,
189
+ error: error instanceof Error ? error.message : 'Unknown error',
190
+ });
191
+ }
192
+ },
193
+ },
194
+ // List sources and connectors
195
+ {
196
+ name: 'sync_list_sources',
197
+ label: 'List Sync Sources',
198
+ description: 'Read sync source configurations. Returns full source configs with entity_mappings, field maps, transforms, and all settings. Also lists available connectors and known source definitions. Use this whenever users ask about their source config.',
199
+ parameters: T.Object({}),
200
+ execute: async (toolCallId, params) => {
201
+ try {
202
+ const configMgr = new SourceConfigManager(workspacePath);
203
+ const sources = configMgr.getAll();
204
+ // Load connectors but strip sensitive fields
205
+ const connectors = ConnectorManager.loadConfigs().map(c => ({
206
+ name: c.name,
207
+ type: c.type,
208
+ description: c.description,
209
+ }));
210
+ const availableDefinitions = Object.keys(SOURCE_DEFINITIONS);
211
+ return wrapToolResult({
212
+ success: true,
213
+ sources,
214
+ connectors,
215
+ availableDefinitions,
216
+ });
217
+ }
218
+ catch (error) {
219
+ return wrapToolResult({
220
+ success: false,
221
+ error: error instanceof Error ? error.message : 'Unknown error',
222
+ });
223
+ }
224
+ },
225
+ },
226
+ // Set up a new source
227
+ {
228
+ name: 'sync_setup_source',
229
+ label: 'Setup Sync Source',
230
+ description: 'Add or reconfigure a sync source. Uses default mappings from source definitions when available.',
231
+ parameters: T.Object({
232
+ name: T.String({ description: 'Source name (lowercase, alphanumeric, hyphens/underscores)' }),
233
+ connector: T.Optional(T.String({ description: 'Connector name (defaults to source name)' })),
234
+ entity_mappings: T.Optional(T.Array(T.Object({}, { additionalProperties: true }), { description: 'Entity mappings (overrides defaults from source definition)' })),
235
+ force: T.Optional(T.Boolean({ description: 'Overwrite existing source config' })),
236
+ }),
237
+ execute: async (toolCallId, params) => {
238
+ try {
239
+ const configMgr = new SourceConfigManager(workspacePath);
240
+ const connectorName = params.connector ?? params.name;
241
+ // Check if source already exists
242
+ const existing = configMgr.get(params.name);
243
+ if (existing && !params.force) {
244
+ return wrapToolResult({
245
+ success: false,
246
+ error: `Source "${params.name}" already exists. Use force: true to overwrite.`,
247
+ });
248
+ }
249
+ // Verify connector exists
250
+ const connectors = ConnectorManager.loadConfigs();
251
+ const connector = connectors.find(c => c.name === connectorName);
252
+ if (!connector) {
253
+ return wrapToolResult({
254
+ success: false,
255
+ error: `Connector "${connectorName}" not found. Set up the connector first using the CLI: studiograph connector add ${connectorName}`,
256
+ availableConnectors: connectors.map(c => c.name),
257
+ });
258
+ }
259
+ // Build source config
260
+ const definition = SOURCE_DEFINITIONS[params.name] ?? SOURCE_DEFINITIONS[connectorName];
261
+ const mappings = params.entity_mappings ?? definition?.defaultMappings ?? [];
262
+ const config = {
263
+ name: params.name,
264
+ connector: connectorName,
265
+ enabled: true,
266
+ added_at: new Date().toISOString(),
267
+ entity_mappings: mappings,
268
+ };
269
+ // Validate
270
+ const validation = configMgr.validate(config);
271
+ if (!validation.valid) {
272
+ return wrapToolResult({
273
+ success: false,
274
+ error: 'Invalid source config',
275
+ validationErrors: validation.errors,
276
+ });
277
+ }
278
+ configMgr.save(config);
279
+ return wrapToolResult({
280
+ success: true,
281
+ message: `Source "${params.name}" configured with ${mappings.length} entity mapping(s).`,
282
+ config,
283
+ });
284
+ }
285
+ catch (error) {
286
+ return wrapToolResult({
287
+ success: false,
288
+ error: error instanceof Error ? error.message : 'Unknown error',
289
+ });
290
+ }
291
+ },
292
+ },
293
+ // Remove a source
294
+ {
295
+ name: 'sync_remove_source',
296
+ label: 'Remove Sync Source',
297
+ description: 'Remove a sync source configuration.',
298
+ parameters: T.Object({
299
+ name: T.String({ description: 'Source name to remove' }),
300
+ }),
301
+ execute: async (toolCallId, params) => {
302
+ try {
303
+ const configMgr = new SourceConfigManager(workspacePath);
304
+ const removed = configMgr.remove(params.name);
305
+ return wrapToolResult({
306
+ success: true,
307
+ removed,
308
+ message: removed
309
+ ? `Source "${params.name}" removed.`
310
+ : `Source "${params.name}" not found.`,
311
+ });
312
+ }
313
+ catch (error) {
314
+ return wrapToolResult({
315
+ success: false,
316
+ error: error instanceof Error ? error.message : 'Unknown error',
317
+ });
318
+ }
319
+ },
320
+ },
321
+ // ── Connector introspection for sync config building ────────────────────
322
+ // Inspect a connector's tools, categorized for sync
323
+ {
324
+ name: 'sync_inspect_connector',
325
+ label: 'Inspect Connector for Sync',
326
+ description: 'Inspect a connector\'s MCP tools to understand what data types are available for sync. Auto-categorizes tools by purpose (list, get, search, create) and detects syncable entity types with their list/get tool pairs. Use this when building a new source config to understand what the connector offers.',
327
+ parameters: T.Object({
328
+ connector: T.String({ description: 'Connector name (e.g. "pipedrive", "linear", "granola")' }),
329
+ }),
330
+ execute: async (toolCallId, params) => {
331
+ try {
332
+ const reg = getRegistry();
333
+ const tools = reg?.connectors.get(params.connector);
334
+ if (!tools) {
335
+ const available = reg ? [...reg.connectors.keys()] : [];
336
+ return wrapToolResult({
337
+ success: false,
338
+ error: available.length === 0
339
+ ? 'No connectors loaded yet. Connectors may still be initializing, or none are configured.'
340
+ : `Connector "${params.connector}" not found`,
341
+ availableConnectors: available,
342
+ });
343
+ }
344
+ // Categorize tools by purpose
345
+ const categories = {
346
+ list: [],
347
+ get: [],
348
+ search: [],
349
+ create: [],
350
+ update: [],
351
+ delete: [],
352
+ other: [],
353
+ };
354
+ for (const tool of tools) {
355
+ const n = tool.localName;
356
+ if (/(?:^list_|_list$|_list_)/.test(n))
357
+ categories.list.push({ name: n, description: tool.description });
358
+ else if (/(?:^get_|_get$|^read_|_read$)/.test(n))
359
+ categories.get.push({ name: n, description: tool.description });
360
+ else if (/(?:^search_|_search$)/.test(n))
361
+ categories.search.push({ name: n, description: tool.description });
362
+ else if (/(?:^create_|_create$)/.test(n))
363
+ categories.create.push({ name: n, description: tool.description });
364
+ else if (/(?:^update_|_update$)/.test(n))
365
+ categories.update.push({ name: n, description: tool.description });
366
+ else if (/(?:^delete_|_delete$|^remove_|_remove$)/.test(n))
367
+ categories.delete.push({ name: n, description: tool.description });
368
+ else
369
+ categories.other.push({ name: n, description: tool.description });
370
+ }
371
+ // Detect syncable entity types (list + optional get pairs)
372
+ const toolNames = new Set(tools.map(t => t.localName));
373
+ const detected = [];
374
+ for (const tool of categories.list) {
375
+ // Extract entity type from tool name
376
+ let sourceType = null;
377
+ const listMatch = tool.name.match(/^list_(.+?)s?$/);
378
+ if (listMatch)
379
+ sourceType = listMatch[1];
380
+ if (!sourceType) {
381
+ const suffixMatch = tool.name.match(/^(.+?)s?_list(?:_|$)/);
382
+ if (suffixMatch)
383
+ sourceType = suffixMatch[1];
384
+ }
385
+ if (!sourceType)
386
+ continue;
387
+ if (detected.some(d => d.source_type === sourceType))
388
+ continue;
389
+ // Find corresponding get tool
390
+ const getVariants = [`get_${sourceType}`, `${sourceType}_get`, `${sourceType}s_get`, `read_${sourceType}`];
391
+ const getTool = getVariants.find(g => toolNames.has(g)) ?? null;
392
+ detected.push({ source_type: sourceType, list_tool: tool.name, get_tool: getTool });
393
+ }
394
+ return wrapToolResult({
395
+ success: true,
396
+ connector: params.connector,
397
+ totalTools: tools.length,
398
+ categories: Object.fromEntries(Object.entries(categories).filter(([_, v]) => v.length > 0)),
399
+ syncableTypes: detected,
400
+ hint: detected.length > 0
401
+ ? 'Use sync_sample_data to fetch a sample record from any list tool and see available fields for field_map building.'
402
+ : 'No list tools detected. This connector may not support sync, or tools may use non-standard naming.',
403
+ });
404
+ }
405
+ catch (error) {
406
+ return wrapToolResult({
407
+ success: false,
408
+ error: error instanceof Error ? error.message : 'Unknown error',
409
+ });
410
+ }
411
+ },
412
+ },
413
+ // Fetch sample data from a connector to inspect field shapes
414
+ {
415
+ name: 'sync_sample_data',
416
+ label: 'Fetch Sample Data',
417
+ description: 'Fetch a sample record from a connector to inspect its field structure. Calls a list tool with limit 1 and returns the field names, types, nesting, and sample values. Use this to auto-build field_map entries for sync source configs.',
418
+ parameters: T.Object({
419
+ connector: T.String({ description: 'Connector name (e.g. "pipedrive", "linear")' }),
420
+ tool: T.String({ description: 'Tool name to call (e.g. "deals_list", "list_issues")' }),
421
+ arguments: T.Optional(T.Object({}, { additionalProperties: true, description: 'Extra arguments to pass to the tool (limit is auto-set to 1 if the tool accepts it)' })),
422
+ }),
423
+ execute: async (toolCallId, params) => {
424
+ try {
425
+ const reg = getRegistry();
426
+ const tools = reg?.connectors.get(params.connector);
427
+ if (!tools) {
428
+ const available = reg ? [...reg.connectors.keys()] : [];
429
+ return wrapToolResult({
430
+ success: false,
431
+ error: available.length === 0
432
+ ? 'No connectors loaded yet. Connectors may still be initializing, or none are configured.'
433
+ : `Connector "${params.connector}" not found`,
434
+ availableConnectors: available,
435
+ });
436
+ }
437
+ const tool = tools.find(t => t.localName === params.tool);
438
+ if (!tool) {
439
+ return wrapToolResult({
440
+ success: false,
441
+ error: `Tool "${params.tool}" not found in ${params.connector}`,
442
+ availableTools: tools.slice(0, 30).map(t => t.localName),
443
+ });
444
+ }
445
+ // Build arguments — try to limit to 1 record
446
+ const args = { ...params.arguments };
447
+ const schema = tool.parameters;
448
+ const props = schema?.properties || {};
449
+ if ('limit' in props && !args.limit)
450
+ args.limit = 1;
451
+ if ('first' in props && !args.first)
452
+ args.first = 1;
453
+ if ('per_page' in props && !args.per_page)
454
+ args.per_page = 1;
455
+ if ('page_size' in props && !args.page_size)
456
+ args.page_size = 1;
457
+ // Call the tool
458
+ const result = await tool.execute(toolCallId, args);
459
+ const text = result?.content?.[0]?.text ?? '';
460
+ // Parse the response
461
+ let data;
462
+ try {
463
+ data = JSON.parse(text);
464
+ }
465
+ catch {
466
+ // Some tools return double-wrapped content
467
+ try {
468
+ const outer = JSON.parse(text);
469
+ if (outer.content?.[0]?.text) {
470
+ data = JSON.parse(outer.content[0].text);
471
+ }
472
+ }
473
+ catch {
474
+ return wrapToolResult({
475
+ success: true,
476
+ rawResponse: text.slice(0, 3000),
477
+ note: 'Response is not JSON. Showing raw text for manual inspection.',
478
+ });
479
+ }
480
+ }
481
+ // Extract the first record from common response shapes
482
+ let record = null;
483
+ if (Array.isArray(data)) {
484
+ record = data[0];
485
+ }
486
+ else if (data?.data && Array.isArray(data.data)) {
487
+ record = data.data[0];
488
+ }
489
+ else if (data?.items && Array.isArray(data.items)) {
490
+ record = data.items[0];
491
+ }
492
+ else if (data?.results && Array.isArray(data.results)) {
493
+ record = data.results[0];
494
+ }
495
+ else if (data?.nodes && Array.isArray(data.nodes)) {
496
+ record = data.nodes[0];
497
+ }
498
+ else if (typeof data === 'object' && data !== null && !Array.isArray(data)) {
499
+ // Might be a single record response
500
+ record = data;
501
+ }
502
+ if (!record) {
503
+ return wrapToolResult({
504
+ success: true,
505
+ note: 'No records returned. The source may be empty or the tool may need different arguments.',
506
+ responseShape: typeof data,
507
+ responseKeys: data && typeof data === 'object' ? Object.keys(data) : [],
508
+ });
509
+ }
510
+ // Analyze field structure
511
+ const analyzeFields = (obj, prefix = '') => {
512
+ const fields = [];
513
+ if (!obj || typeof obj !== 'object')
514
+ return fields;
515
+ for (const [key, value] of Object.entries(obj)) {
516
+ const path = prefix ? `${prefix}.${key}` : key;
517
+ const type = value === null ? 'null'
518
+ : Array.isArray(value) ? `array(${value.length})`
519
+ : typeof value;
520
+ // Truncate long strings for display
521
+ let sample = value;
522
+ if (typeof value === 'string' && value.length > 100) {
523
+ sample = value.slice(0, 100) + '...';
524
+ }
525
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
526
+ fields.push({ path, type: 'object', sample: `{${Object.keys(value).join(', ')}}` });
527
+ // Recurse one level into objects
528
+ fields.push(...analyzeFields(value, path));
529
+ }
530
+ else if (Array.isArray(value) && value.length > 0 && typeof value[0] === 'object') {
531
+ fields.push({ path, type: `array(${value.length})`, sample: `[{${Object.keys(value[0]).join(', ')}}]` });
532
+ }
533
+ else {
534
+ fields.push({ path, type, sample });
535
+ }
536
+ }
537
+ return fields;
538
+ };
539
+ const fields = analyzeFields(record);
540
+ // Suggest field_map entries for common entity fields
541
+ const topLevelKeys = Object.keys(record);
542
+ const suggestions = {};
543
+ const nameFields = ['title', 'name', 'subject', 'summary'];
544
+ const statusFields = ['status', 'state', 'stage'];
545
+ const dateFields = ['created_at', 'createdAt', 'date', 'start_date', 'due_date', 'dueDate'];
546
+ const descFields = ['description', 'content', 'body', 'notes', 'note'];
547
+ for (const key of topLevelKeys) {
548
+ if (nameFields.includes(key))
549
+ suggestions[key] = 'name';
550
+ else if (statusFields.includes(key))
551
+ suggestions[key] = 'status';
552
+ else if (dateFields.includes(key))
553
+ suggestions[key] = 'date (use date-iso transform)';
554
+ else if (descFields.includes(key))
555
+ suggestions[key] = 'description (or use as content_from)';
556
+ }
557
+ return wrapToolResult({
558
+ success: true,
559
+ connector: params.connector,
560
+ tool: params.tool,
561
+ fieldCount: fields.length,
562
+ fields,
563
+ topLevelKeys,
564
+ suggestedFieldMap: Object.keys(suggestions).length > 0 ? suggestions : undefined,
565
+ hint: 'Use these field paths in field_map when configuring sync_setup_source. Nested fields use dot notation (e.g. "org_id.name": "organization").',
566
+ });
567
+ }
568
+ catch (error) {
569
+ return wrapToolResult({
570
+ success: false,
571
+ error: error instanceof Error ? error.message : 'Unknown error',
572
+ });
573
+ }
574
+ },
575
+ },
576
+ // ── Enrich tools ──────────────────────────────────────────────────────────
577
+ // Run enrichment
578
+ {
579
+ name: 'enrich_run',
580
+ label: 'Enrich Entities',
581
+ description: 'Enrich entities with cross-source context via LLM. Writes changes in-place (no staging). Review with workspace_review, then commit with workspace_commit. Always confirm with the user before calling this.',
582
+ parameters: T.Object({
583
+ entityTypes: T.Optional(T.Array(T.String(), { description: 'Only enrich specific entity types' })),
584
+ dryRun: T.Optional(T.Boolean({ description: 'Preview without writing changes' })),
585
+ minRefs: T.Optional(T.Number({ description: 'Minimum context references required (default: 2)' })),
586
+ maxCompleteness: T.Optional(T.Number({ description: 'Maximum completeness to qualify (default: 0.6)' })),
587
+ }),
588
+ execute: async (toolCallId, params) => {
589
+ try {
590
+ const progress = [];
591
+ const result = await runEnrichment({
592
+ workspacePath,
593
+ entityTypes: params.entityTypes,
594
+ dryRun: params.dryRun,
595
+ minRefs: params.minRefs,
596
+ maxCompleteness: params.maxCompleteness,
597
+ schemaExtensions: workspaceConfig.schema_extensions,
598
+ }, (msg) => progress.push(msg));
599
+ return wrapToolResult({
600
+ success: true,
601
+ result,
602
+ progress: progress.join('\n'),
603
+ nextSteps: result.enriched > 0 && !params.dryRun
604
+ ? 'Review changes with workspace_review, then commit with workspace_commit or studiograph commit'
605
+ : undefined,
606
+ });
607
+ }
608
+ catch (error) {
609
+ return wrapToolResult({
610
+ success: false,
611
+ error: error instanceof Error ? error.message : 'Unknown error',
612
+ });
613
+ }
614
+ },
615
+ },
616
+ // Abort enrichment (revert uncommitted changes)
617
+ {
618
+ name: 'enrich_abort',
619
+ label: 'Abort Enrichment',
620
+ description: 'Discard uncommitted enrichment changes by reverting all entity files (git checkout + git clean).',
621
+ parameters: T.Object({}),
622
+ execute: async (toolCallId, _params) => {
623
+ try {
624
+ const repos = workspaceConfig.repos ?? [];
625
+ const reverted = [];
626
+ for (const repo of repos) {
627
+ const repoPath = join(workspacePath, repo.path);
628
+ if (!existsSync(repoPath))
629
+ continue;
630
+ spawnSync('git', ['checkout', '--', '.'], { cwd: repoPath, encoding: 'utf-8' });
631
+ spawnSync('git', ['clean', '-fd'], { cwd: repoPath, encoding: 'utf-8' });
632
+ reverted.push(repo.name);
633
+ }
634
+ return wrapToolResult({
635
+ success: true,
636
+ reverted,
637
+ message: reverted.length > 0
638
+ ? `Reverted changes in: ${reverted.join(', ')}`
639
+ : 'No changes to revert.',
640
+ });
641
+ }
642
+ catch (error) {
643
+ return wrapToolResult({
644
+ success: false,
645
+ error: String(error instanceof Error ? error.message : error),
646
+ });
647
+ }
648
+ },
649
+ },
650
+ // Reset workspace (discard uncommitted changes)
651
+ {
652
+ name: 'workspace_reset',
653
+ label: 'Reset Workspace',
654
+ description: 'Discard ALL uncommitted changes across workspace repos: reverts modified files (git checkout) and removes untracked files (git clean -fd). Also clears staging if present. This is destructive — always confirm with the user before calling.',
655
+ parameters: T.Object({}),
656
+ execute: async (toolCallId, _params) => {
657
+ try {
658
+ const repos = workspaceConfig.repos ?? [];
659
+ const resetRepos = [];
660
+ const details = [];
661
+ for (const repo of repos) {
662
+ const repoPath = join(workspacePath, repo.path);
663
+ if (!existsSync(repoPath))
664
+ continue;
665
+ // Check if there are changes to reset
666
+ const status = spawnSync('git', ['status', '--porcelain'], { cwd: repoPath, encoding: 'utf-8' });
667
+ if (status.status !== 0 || !status.stdout.trim())
668
+ continue;
669
+ const fileCount = status.stdout.trim().split('\n').length;
670
+ // Revert tracked file changes
671
+ spawnSync('git', ['checkout', '--', '.'], { cwd: repoPath, encoding: 'utf-8' });
672
+ // Remove untracked files and directories
673
+ spawnSync('git', ['clean', '-fd'], { cwd: repoPath, encoding: 'utf-8' });
674
+ resetRepos.push(repo.name);
675
+ details.push(`${repo.name}: ${fileCount} file(s) reset`);
676
+ }
677
+ // Clear staging too
678
+ let clearedStaging = false;
679
+ try {
680
+ const staging = new StagingManager(workspacePath);
681
+ if (staging.hasStaging()) {
682
+ staging.clear();
683
+ clearedStaging = true;
684
+ }
685
+ }
686
+ catch { /* staging dir may not exist */ }
687
+ const parts = [];
688
+ if (resetRepos.length > 0)
689
+ parts.push(`Reset ${resetRepos.length} repo(s): ${resetRepos.join(', ')}`);
690
+ if (clearedStaging)
691
+ parts.push('Cleared staging');
692
+ return wrapToolResult({
693
+ success: true,
694
+ resetRepos,
695
+ details,
696
+ clearedStaging,
697
+ message: parts.length > 0 ? parts.join('. ') + '.' : 'No changes to reset.',
698
+ });
699
+ }
700
+ catch (error) {
701
+ return wrapToolResult({
702
+ success: false,
703
+ error: String(error instanceof Error ? error.message : error),
704
+ });
705
+ }
706
+ },
707
+ },
708
+ // Reindex search
709
+ {
710
+ name: 'workspace_reindex',
711
+ label: 'Reindex Search',
712
+ description: 'Rebuild the search index (LanceDB) for all workspace repos. Run after sync, enrich, or manual entity edits to update search results.',
713
+ parameters: T.Object({
714
+ repo: T.Optional(T.String({ description: 'Reindex a single repo only' })),
715
+ }),
716
+ execute: async (toolCallId, params) => {
717
+ try {
718
+ const userConfig = loadUserConfig();
719
+ const dbPath = userConfig.vector_db_path || join(homedir(), '.studiograph', 'vector');
720
+ const vectorService = new VectorService(dbPath, userConfig.voyage_api_key);
721
+ await vectorService.init();
722
+ const allRepos = workspaceConfig.repos ?? [];
723
+ const repos = params.repo
724
+ ? allRepos.filter(r => r.name === params.repo)
725
+ : allRepos;
726
+ if (params.repo && repos.length === 0) {
727
+ return wrapToolResult({
728
+ success: false,
729
+ error: `Repo "${params.repo}" not found in workspace`,
730
+ availableRepos: allRepos.map(r => r.name),
731
+ });
732
+ }
733
+ const repoResults = {};
734
+ let totalIndexed = 0;
735
+ for (const repo of repos) {
736
+ const repoPath = join(workspacePath, repo.path);
737
+ if (!existsSync(repoPath))
738
+ continue;
739
+ try {
740
+ const graph = new BaseGraphManager({
741
+ repoPath,
742
+ repoName: repo.name,
743
+ gitUser: { id: 'reindex', name: 'reindex', email: 'reindex@studiograph.local' },
744
+ });
745
+ const entities = graph.list();
746
+ if (entities.length > 0) {
747
+ await vectorService.upsertMany(repo.name, entities);
748
+ }
749
+ vectorService.setLastIndexedTime(repo.name);
750
+ repoResults[repo.name] = entities.length;
751
+ totalIndexed += entities.length;
752
+ }
753
+ catch (err) {
754
+ repoResults[repo.name] = -1; // signal failure
755
+ }
756
+ }
757
+ return wrapToolResult({
758
+ success: true,
759
+ indexed: totalIndexed,
760
+ repos: repoResults,
761
+ message: `Indexed ${totalIndexed} entities across ${repos.length} repo(s)`,
762
+ });
763
+ }
764
+ catch (error) {
765
+ return wrapToolResult({
766
+ success: false,
767
+ error: error instanceof Error ? error.message : 'Unknown error',
768
+ });
769
+ }
770
+ },
771
+ },
772
+ // ── Graph status & review ─────────────────────────────────────────────────
773
+ // Show changed files across repos
774
+ {
775
+ name: 'workspace_status',
776
+ label: 'Workspace Status',
777
+ description: 'Show uncommitted git changes across all workspace repos. Use this when users ask about pending changes, what files are modified, or whether there are changes ready to commit.',
778
+ parameters: T.Object({}),
779
+ execute: async (toolCallId, params) => {
780
+ try {
781
+ const repos = workspaceConfig.repos ?? [];
782
+ const changes = {};
783
+ let totalChanged = 0;
784
+ for (const repo of repos) {
785
+ const repoPath = join(workspacePath, repo.path);
786
+ if (!existsSync(repoPath))
787
+ continue;
788
+ const result = spawnSync('git', ['status', '--porcelain'], { cwd: repoPath, encoding: 'utf-8' });
789
+ if (result.status === 0 && result.stdout.trim()) {
790
+ const files = result.stdout.trim().split('\n').filter(Boolean);
791
+ changes[repo.name] = files;
792
+ totalChanged += files.length;
793
+ }
794
+ }
795
+ return wrapToolResult({
796
+ success: true,
797
+ totalChanged,
798
+ changes,
799
+ message: totalChanged > 0
800
+ ? `${totalChanged} file${totalChanged === 1 ? '' : 's'} changed across ${Object.keys(changes).length} repo${Object.keys(changes).length === 1 ? '' : 's'}`
801
+ : 'No uncommitted changes.',
802
+ });
803
+ }
804
+ catch (error) {
805
+ return wrapToolResult({
806
+ success: false,
807
+ error: error instanceof Error ? error.message : 'Unknown error',
808
+ });
809
+ }
810
+ },
811
+ },
812
+ // Show diffs for review
813
+ {
814
+ name: 'workspace_review',
815
+ label: 'Review Changes',
816
+ description: 'Show git diffs for uncommitted entity changes across workspace repos. Use this to preview what will be committed. Optionally filter by entity type or specific file path.',
817
+ parameters: T.Object({
818
+ entityType: T.Optional(T.String({ description: 'Filter diff to a specific entity type (e.g. "person", "deal")' })),
819
+ file: T.Optional(T.String({ description: 'Show diff for a specific file path' })),
820
+ committed: T.Optional(T.Boolean({ description: 'Review last commit instead of uncommitted changes' })),
821
+ }),
822
+ execute: async (toolCallId, params) => {
823
+ try {
824
+ const repos = workspaceConfig.repos ?? [];
825
+ const diffArgs = params.committed
826
+ ? ['diff', 'HEAD~1..HEAD']
827
+ : ['diff'];
828
+ const diffs = {};
829
+ for (const repo of repos) {
830
+ const repoPath = join(workspacePath, repo.path);
831
+ if (!existsSync(repoPath))
832
+ continue;
833
+ const args = [...diffArgs];
834
+ if (params.file) {
835
+ args.push('--', params.file);
836
+ }
837
+ else if (params.entityType) {
838
+ args.push('--', `${params.entityType}/`);
839
+ }
840
+ const result = spawnSync('git', args, { cwd: repoPath, encoding: 'utf-8' });
841
+ if (result.status === 0 && result.stdout.trim()) {
842
+ // Truncate very large diffs to avoid overwhelming the context
843
+ const diff = result.stdout;
844
+ diffs[repo.name] = diff.length > 10000
845
+ ? diff.slice(0, 10000) + '\n\n... (truncated, use --file for specific entities)'
846
+ : diff;
847
+ }
848
+ }
849
+ return wrapToolResult({
850
+ success: true,
851
+ hasDiffs: Object.keys(diffs).length > 0,
852
+ diffs,
853
+ message: Object.keys(diffs).length > 0
854
+ ? `Diffs from ${Object.keys(diffs).length} repo(s)`
855
+ : 'No changes found.',
856
+ });
857
+ }
858
+ catch (error) {
859
+ return wrapToolResult({
860
+ success: false,
861
+ error: error instanceof Error ? error.message : 'Unknown error',
862
+ });
863
+ }
864
+ },
865
+ },
866
+ // Push all repos
867
+ {
868
+ name: 'workspace_push',
869
+ label: 'Push Repos',
870
+ description: 'Git push all workspace repos to their remotes. Always confirm with the user before calling this. IMPORTANT: Pushing does NOT automatically update a Railway deployment. If the workspace is deployed to Railway, call redeploy_railway after pushing to trigger a fresh deploy with the latest commits.',
871
+ parameters: T.Object({}),
872
+ execute: async (_toolCallId, _params) => {
873
+ try {
874
+ const repos = workspaceConfig.repos ?? [];
875
+ const results = {};
876
+ for (const repo of repos) {
877
+ const repoPath = join(workspacePath, repo.path);
878
+ if (!existsSync(repoPath))
879
+ continue;
880
+ try {
881
+ results[repo.name] = gitPush(repoPath);
882
+ }
883
+ catch (err) {
884
+ results[repo.name] = `error: ${err instanceof Error ? err.message : String(err)}`;
885
+ }
886
+ }
887
+ const pushed = Object.values(results).filter(r => r === 'pushed').length;
888
+ return wrapToolResult({
889
+ success: true,
890
+ results,
891
+ message: pushed > 0
892
+ ? `Pushed ${pushed} repo${pushed === 1 ? '' : 's'}`
893
+ : 'All repos already up to date.',
894
+ });
895
+ }
896
+ catch (error) {
897
+ return wrapToolResult({
898
+ success: false,
899
+ error: error instanceof Error ? error.message : 'Unknown error',
900
+ });
901
+ }
902
+ },
903
+ },
904
+ // Pull all repos
905
+ {
906
+ name: 'workspace_pull',
907
+ label: 'Pull Repos',
908
+ description: 'Git pull all workspace repos from their remotes. Reports conflicts if any.',
909
+ parameters: T.Object({}),
910
+ execute: async (_toolCallId, _params) => {
911
+ try {
912
+ const repos = workspaceConfig.repos ?? [];
913
+ const results = {};
914
+ for (const repo of repos) {
915
+ const repoPath = join(workspacePath, repo.path);
916
+ if (!existsSync(repoPath))
917
+ continue;
918
+ try {
919
+ results[repo.name] = gitPull(repoPath);
920
+ }
921
+ catch (err) {
922
+ results[repo.name] = `error: ${err instanceof Error ? err.message : String(err)}`;
923
+ }
924
+ }
925
+ const updated = Object.values(results).filter(r => r === 'updated').length;
926
+ const conflicts = Object.values(results).filter(r => r === 'conflict').length;
927
+ return wrapToolResult({
928
+ success: conflicts === 0,
929
+ results,
930
+ message: conflicts > 0
931
+ ? `${conflicts} repo${conflicts === 1 ? '' : 's'} with merge conflicts — resolve manually`
932
+ : updated > 0
933
+ ? `Pulled updates in ${updated} repo${updated === 1 ? '' : 's'}`
934
+ : 'All repos already up to date.',
935
+ });
936
+ }
937
+ catch (error) {
938
+ return wrapToolResult({
939
+ success: false,
940
+ error: error instanceof Error ? error.message : 'Unknown error',
941
+ });
942
+ }
943
+ },
944
+ },
945
+ ];
946
+ }
947
+ //# sourceMappingURL=sync-tools.js.map