nodedex 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (469) hide show
  1. package/adapters/claude-code-watcher.mjs +336 -0
  2. package/adapters/hermes-statedb-watcher.mjs +234 -0
  3. package/adapters/nodedex-capture-core.mjs +129 -0
  4. package/adapters/nodedex-capture.mjs +169 -0
  5. package/dist/agent-protocol.d.ts +7 -0
  6. package/dist/agent-protocol.d.ts.map +1 -0
  7. package/dist/agent-protocol.js +38 -0
  8. package/dist/agent-protocol.js.map +1 -0
  9. package/dist/api-server.d.ts +5 -0
  10. package/dist/api-server.d.ts.map +1 -0
  11. package/dist/api-server.js +351 -0
  12. package/dist/api-server.js.map +1 -0
  13. package/dist/boot-env.d.ts +2 -0
  14. package/dist/boot-env.d.ts.map +1 -0
  15. package/dist/boot-env.js +12 -0
  16. package/dist/boot-env.js.map +1 -0
  17. package/dist/engine/__tests__/search-core.test.d.ts +2 -0
  18. package/dist/engine/__tests__/search-core.test.d.ts.map +1 -0
  19. package/dist/engine/__tests__/search-core.test.js +139 -0
  20. package/dist/engine/__tests__/search-core.test.js.map +1 -0
  21. package/dist/engine/ai-provider.d.ts +45 -0
  22. package/dist/engine/ai-provider.d.ts.map +1 -0
  23. package/dist/engine/ai-provider.js +5 -0
  24. package/dist/engine/ai-provider.js.map +1 -0
  25. package/dist/engine/embeddings.d.ts +51 -0
  26. package/dist/engine/embeddings.d.ts.map +1 -0
  27. package/dist/engine/embeddings.js +89 -0
  28. package/dist/engine/embeddings.js.map +1 -0
  29. package/dist/engine/providers/__tests__/failure-policy.test.d.ts +2 -0
  30. package/dist/engine/providers/__tests__/failure-policy.test.d.ts.map +1 -0
  31. package/dist/engine/providers/__tests__/failure-policy.test.js +134 -0
  32. package/dist/engine/providers/__tests__/failure-policy.test.js.map +1 -0
  33. package/dist/engine/providers/__tests__/model-caps.test.d.ts +2 -0
  34. package/dist/engine/providers/__tests__/model-caps.test.d.ts.map +1 -0
  35. package/dist/engine/providers/__tests__/model-caps.test.js +38 -0
  36. package/dist/engine/providers/__tests__/model-caps.test.js.map +1 -0
  37. package/dist/engine/providers/__tests__/openai-structured.test.d.ts +2 -0
  38. package/dist/engine/providers/__tests__/openai-structured.test.d.ts.map +1 -0
  39. package/dist/engine/providers/__tests__/openai-structured.test.js +73 -0
  40. package/dist/engine/providers/__tests__/openai-structured.test.js.map +1 -0
  41. package/dist/engine/providers/__tests__/usage-ledger.test.d.ts +2 -0
  42. package/dist/engine/providers/__tests__/usage-ledger.test.d.ts.map +1 -0
  43. package/dist/engine/providers/__tests__/usage-ledger.test.js +108 -0
  44. package/dist/engine/providers/__tests__/usage-ledger.test.js.map +1 -0
  45. package/dist/engine/providers/anthropic.d.ts +17 -0
  46. package/dist/engine/providers/anthropic.d.ts.map +1 -0
  47. package/dist/engine/providers/anthropic.js +125 -0
  48. package/dist/engine/providers/anthropic.js.map +1 -0
  49. package/dist/engine/providers/failure-policy.d.ts +56 -0
  50. package/dist/engine/providers/failure-policy.d.ts.map +1 -0
  51. package/dist/engine/providers/failure-policy.js +120 -0
  52. package/dist/engine/providers/failure-policy.js.map +1 -0
  53. package/dist/engine/providers/gemini.d.ts +22 -0
  54. package/dist/engine/providers/gemini.d.ts.map +1 -0
  55. package/dist/engine/providers/gemini.js +180 -0
  56. package/dist/engine/providers/gemini.js.map +1 -0
  57. package/dist/engine/providers/index.d.ts +8 -0
  58. package/dist/engine/providers/index.d.ts.map +1 -0
  59. package/dist/engine/providers/index.js +67 -0
  60. package/dist/engine/providers/index.js.map +1 -0
  61. package/dist/engine/providers/local.d.ts +12 -0
  62. package/dist/engine/providers/local.d.ts.map +1 -0
  63. package/dist/engine/providers/local.js +46 -0
  64. package/dist/engine/providers/local.js.map +1 -0
  65. package/dist/engine/providers/model-caps.d.ts +6 -0
  66. package/dist/engine/providers/model-caps.d.ts.map +1 -0
  67. package/dist/engine/providers/model-caps.js +49 -0
  68. package/dist/engine/providers/model-caps.js.map +1 -0
  69. package/dist/engine/providers/openai.d.ts +30 -0
  70. package/dist/engine/providers/openai.d.ts.map +1 -0
  71. package/dist/engine/providers/openai.js +309 -0
  72. package/dist/engine/providers/openai.js.map +1 -0
  73. package/dist/engine/providers/usage-ledger.d.ts +69 -0
  74. package/dist/engine/providers/usage-ledger.d.ts.map +1 -0
  75. package/dist/engine/providers/usage-ledger.js +209 -0
  76. package/dist/engine/providers/usage-ledger.js.map +1 -0
  77. package/dist/engine/search-core.d.ts +40 -0
  78. package/dist/engine/search-core.d.ts.map +1 -0
  79. package/dist/engine/search-core.js +109 -0
  80. package/dist/engine/search-core.js.map +1 -0
  81. package/dist/engine/vector-math.d.ts +5 -0
  82. package/dist/engine/vector-math.d.ts.map +1 -0
  83. package/dist/engine/vector-math.js +25 -0
  84. package/dist/engine/vector-math.js.map +1 -0
  85. package/dist/home-env.d.ts +26 -0
  86. package/dist/home-env.d.ts.map +1 -0
  87. package/dist/home-env.js +87 -0
  88. package/dist/home-env.js.map +1 -0
  89. package/dist/mcp-server.d.ts +13 -0
  90. package/dist/mcp-server.d.ts.map +1 -0
  91. package/dist/mcp-server.js +79 -0
  92. package/dist/mcp-server.js.map +1 -0
  93. package/dist/middleware/auth.d.ts +23 -0
  94. package/dist/middleware/auth.d.ts.map +1 -0
  95. package/dist/middleware/auth.js +104 -0
  96. package/dist/middleware/auth.js.map +1 -0
  97. package/dist/middleware/auto-recall.d.ts +7 -0
  98. package/dist/middleware/auto-recall.d.ts.map +1 -0
  99. package/dist/middleware/auto-recall.js +257 -0
  100. package/dist/middleware/auto-recall.js.map +1 -0
  101. package/dist/middleware/auto-reflect.d.ts +4 -0
  102. package/dist/middleware/auto-reflect.d.ts.map +1 -0
  103. package/dist/middleware/auto-reflect.js +5 -0
  104. package/dist/middleware/auto-reflect.js.map +1 -0
  105. package/dist/middleware/reflect/apply-flag-verdict.d.ts +27 -0
  106. package/dist/middleware/reflect/apply-flag-verdict.d.ts.map +1 -0
  107. package/dist/middleware/reflect/apply-flag-verdict.js +57 -0
  108. package/dist/middleware/reflect/apply-flag-verdict.js.map +1 -0
  109. package/dist/middleware/reflect/arc-entity-resolve.d.ts +29 -0
  110. package/dist/middleware/reflect/arc-entity-resolve.d.ts.map +1 -0
  111. package/dist/middleware/reflect/arc-entity-resolve.js +356 -0
  112. package/dist/middleware/reflect/arc-entity-resolve.js.map +1 -0
  113. package/dist/middleware/reflect/arc-inactivity-timer.d.ts +47 -0
  114. package/dist/middleware/reflect/arc-inactivity-timer.d.ts.map +1 -0
  115. package/dist/middleware/reflect/arc-inactivity-timer.js +175 -0
  116. package/dist/middleware/reflect/arc-inactivity-timer.js.map +1 -0
  117. package/dist/middleware/reflect/arc-pipeline.d.ts +33 -0
  118. package/dist/middleware/reflect/arc-pipeline.d.ts.map +1 -0
  119. package/dist/middleware/reflect/arc-pipeline.js +498 -0
  120. package/dist/middleware/reflect/arc-pipeline.js.map +1 -0
  121. package/dist/middleware/reflect/comprehend-pergroup.d.ts +100 -0
  122. package/dist/middleware/reflect/comprehend-pergroup.d.ts.map +1 -0
  123. package/dist/middleware/reflect/comprehend-pergroup.js +610 -0
  124. package/dist/middleware/reflect/comprehend-pergroup.js.map +1 -0
  125. package/dist/middleware/reflect/comprehend.d.ts +237 -0
  126. package/dist/middleware/reflect/comprehend.d.ts.map +1 -0
  127. package/dist/middleware/reflect/comprehend.js +706 -0
  128. package/dist/middleware/reflect/comprehend.js.map +1 -0
  129. package/dist/middleware/reflect/config.d.ts +34 -0
  130. package/dist/middleware/reflect/config.d.ts.map +1 -0
  131. package/dist/middleware/reflect/config.js +131 -0
  132. package/dist/middleware/reflect/config.js.map +1 -0
  133. package/dist/middleware/reflect/context.d.ts +138 -0
  134. package/dist/middleware/reflect/context.d.ts.map +1 -0
  135. package/dist/middleware/reflect/context.js +619 -0
  136. package/dist/middleware/reflect/context.js.map +1 -0
  137. package/dist/middleware/reflect/cost-breakdown.d.ts +69 -0
  138. package/dist/middleware/reflect/cost-breakdown.d.ts.map +1 -0
  139. package/dist/middleware/reflect/cost-breakdown.js +63 -0
  140. package/dist/middleware/reflect/cost-breakdown.js.map +1 -0
  141. package/dist/middleware/reflect/cost-guard.d.ts +102 -0
  142. package/dist/middleware/reflect/cost-guard.d.ts.map +1 -0
  143. package/dist/middleware/reflect/cost-guard.js +243 -0
  144. package/dist/middleware/reflect/cost-guard.js.map +1 -0
  145. package/dist/middleware/reflect/cost-pricing.d.ts +54 -0
  146. package/dist/middleware/reflect/cost-pricing.d.ts.map +1 -0
  147. package/dist/middleware/reflect/cost-pricing.js +148 -0
  148. package/dist/middleware/reflect/cost-pricing.js.map +1 -0
  149. package/dist/middleware/reflect/cross-group-link.d.ts +61 -0
  150. package/dist/middleware/reflect/cross-group-link.d.ts.map +1 -0
  151. package/dist/middleware/reflect/cross-group-link.js +212 -0
  152. package/dist/middleware/reflect/cross-group-link.js.map +1 -0
  153. package/dist/middleware/reflect/dedup-by-source-and-value.d.ts +70 -0
  154. package/dist/middleware/reflect/dedup-by-source-and-value.d.ts.map +1 -0
  155. package/dist/middleware/reflect/dedup-by-source-and-value.js +0 -0
  156. package/dist/middleware/reflect/dedup-by-source-and-value.js.map +1 -0
  157. package/dist/middleware/reflect/describe-roots.d.ts +58 -0
  158. package/dist/middleware/reflect/describe-roots.d.ts.map +1 -0
  159. package/dist/middleware/reflect/describe-roots.js +266 -0
  160. package/dist/middleware/reflect/describe-roots.js.map +1 -0
  161. package/dist/middleware/reflect/flag-reviewer-startup.d.ts +16 -0
  162. package/dist/middleware/reflect/flag-reviewer-startup.d.ts.map +1 -0
  163. package/dist/middleware/reflect/flag-reviewer-startup.js +107 -0
  164. package/dist/middleware/reflect/flag-reviewer-startup.js.map +1 -0
  165. package/dist/middleware/reflect/flag-reviewer.d.ts +69 -0
  166. package/dist/middleware/reflect/flag-reviewer.d.ts.map +1 -0
  167. package/dist/middleware/reflect/flag-reviewer.js +520 -0
  168. package/dist/middleware/reflect/flag-reviewer.js.map +1 -0
  169. package/dist/middleware/reflect/inline-dedup.d.ts +26 -0
  170. package/dist/middleware/reflect/inline-dedup.d.ts.map +1 -0
  171. package/dist/middleware/reflect/inline-dedup.js +131 -0
  172. package/dist/middleware/reflect/inline-dedup.js.map +1 -0
  173. package/dist/middleware/reflect/justify-decisions.d.ts +37 -0
  174. package/dist/middleware/reflect/justify-decisions.d.ts.map +1 -0
  175. package/dist/middleware/reflect/justify-decisions.js +159 -0
  176. package/dist/middleware/reflect/justify-decisions.js.map +1 -0
  177. package/dist/middleware/reflect/nl-accept.d.ts +35 -0
  178. package/dist/middleware/reflect/nl-accept.d.ts.map +1 -0
  179. package/dist/middleware/reflect/nl-accept.js +167 -0
  180. package/dist/middleware/reflect/nl-accept.js.map +1 -0
  181. package/dist/middleware/reflect/pass0.d.ts +20 -0
  182. package/dist/middleware/reflect/pass0.d.ts.map +1 -0
  183. package/dist/middleware/reflect/pass0.js +423 -0
  184. package/dist/middleware/reflect/pass0.js.map +1 -0
  185. package/dist/middleware/reflect/pass1.d.ts +17 -0
  186. package/dist/middleware/reflect/pass1.d.ts.map +1 -0
  187. package/dist/middleware/reflect/pass1.js +241 -0
  188. package/dist/middleware/reflect/pass1.js.map +1 -0
  189. package/dist/middleware/reflect/pass2-quarantine.d.ts +129 -0
  190. package/dist/middleware/reflect/pass2-quarantine.d.ts.map +1 -0
  191. package/dist/middleware/reflect/pass2-quarantine.js +272 -0
  192. package/dist/middleware/reflect/pass2-quarantine.js.map +1 -0
  193. package/dist/middleware/reflect/pass2-seams.d.ts +205 -0
  194. package/dist/middleware/reflect/pass2-seams.d.ts.map +1 -0
  195. package/dist/middleware/reflect/pass2-seams.js +279 -0
  196. package/dist/middleware/reflect/pass2-seams.js.map +1 -0
  197. package/dist/middleware/reflect/pass2-split-orchestrator.d.ts +37 -0
  198. package/dist/middleware/reflect/pass2-split-orchestrator.d.ts.map +1 -0
  199. package/dist/middleware/reflect/pass2-split-orchestrator.js +531 -0
  200. package/dist/middleware/reflect/pass2-split-orchestrator.js.map +1 -0
  201. package/dist/middleware/reflect/pass2.d.ts +17 -0
  202. package/dist/middleware/reflect/pass2.d.ts.map +1 -0
  203. package/dist/middleware/reflect/pass2.js +324 -0
  204. package/dist/middleware/reflect/pass2.js.map +1 -0
  205. package/dist/middleware/reflect/pass2a.d.ts +141 -0
  206. package/dist/middleware/reflect/pass2a.d.ts.map +1 -0
  207. package/dist/middleware/reflect/pass2a.js +404 -0
  208. package/dist/middleware/reflect/pass2a.js.map +1 -0
  209. package/dist/middleware/reflect/pass2b.d.ts +108 -0
  210. package/dist/middleware/reflect/pass2b.d.ts.map +1 -0
  211. package/dist/middleware/reflect/pass2b.js +480 -0
  212. package/dist/middleware/reflect/pass2b.js.map +1 -0
  213. package/dist/middleware/reflect/pass2c.d.ts +113 -0
  214. package/dist/middleware/reflect/pass2c.d.ts.map +1 -0
  215. package/dist/middleware/reflect/pass2c.js +360 -0
  216. package/dist/middleware/reflect/pass2c.js.map +1 -0
  217. package/dist/middleware/reflect/pass3-batch.d.ts +62 -0
  218. package/dist/middleware/reflect/pass3-batch.d.ts.map +1 -0
  219. package/dist/middleware/reflect/pass3-batch.js +139 -0
  220. package/dist/middleware/reflect/pass3-batch.js.map +1 -0
  221. package/dist/middleware/reflect/pass3.d.ts +23 -0
  222. package/dist/middleware/reflect/pass3.d.ts.map +1 -0
  223. package/dist/middleware/reflect/pass3.js +371 -0
  224. package/dist/middleware/reflect/pass3.js.map +1 -0
  225. package/dist/middleware/reflect/pass4-slice.d.ts +25 -0
  226. package/dist/middleware/reflect/pass4-slice.d.ts.map +1 -0
  227. package/dist/middleware/reflect/pass4-slice.js +315 -0
  228. package/dist/middleware/reflect/pass4-slice.js.map +1 -0
  229. package/dist/middleware/reflect/pass4.d.ts +30 -0
  230. package/dist/middleware/reflect/pass4.d.ts.map +1 -0
  231. package/dist/middleware/reflect/pass4.js +193 -0
  232. package/dist/middleware/reflect/pass4.js.map +1 -0
  233. package/dist/middleware/reflect/pass5.d.ts +22 -0
  234. package/dist/middleware/reflect/pass5.d.ts.map +1 -0
  235. package/dist/middleware/reflect/pass5.js +178 -0
  236. package/dist/middleware/reflect/pass5.js.map +1 -0
  237. package/dist/middleware/reflect/pass_judge.d.ts +44 -0
  238. package/dist/middleware/reflect/pass_judge.d.ts.map +1 -0
  239. package/dist/middleware/reflect/pass_judge.js +263 -0
  240. package/dist/middleware/reflect/pass_judge.js.map +1 -0
  241. package/dist/middleware/reflect/pipeline-flags.d.ts +140 -0
  242. package/dist/middleware/reflect/pipeline-flags.d.ts.map +1 -0
  243. package/dist/middleware/reflect/pipeline-flags.js +314 -0
  244. package/dist/middleware/reflect/pipeline-flags.js.map +1 -0
  245. package/dist/middleware/reflect/pipeline.d.ts +237 -0
  246. package/dist/middleware/reflect/pipeline.d.ts.map +1 -0
  247. package/dist/middleware/reflect/pipeline.js +3114 -0
  248. package/dist/middleware/reflect/pipeline.js.map +1 -0
  249. package/dist/middleware/reflect/promptOverride.d.ts +14 -0
  250. package/dist/middleware/reflect/promptOverride.d.ts.map +1 -0
  251. package/dist/middleware/reflect/promptOverride.js +28 -0
  252. package/dist/middleware/reflect/promptOverride.js.map +1 -0
  253. package/dist/middleware/reflect/provenance-check.d.ts +48 -0
  254. package/dist/middleware/reflect/provenance-check.d.ts.map +1 -0
  255. package/dist/middleware/reflect/provenance-check.js +180 -0
  256. package/dist/middleware/reflect/provenance-check.js.map +1 -0
  257. package/dist/middleware/reflect/provenance-reviewer.d.ts +52 -0
  258. package/dist/middleware/reflect/provenance-reviewer.d.ts.map +1 -0
  259. package/dist/middleware/reflect/provenance-reviewer.js +253 -0
  260. package/dist/middleware/reflect/provenance-reviewer.js.map +1 -0
  261. package/dist/middleware/reflect/prune-collapsed-types.d.ts +11 -0
  262. package/dist/middleware/reflect/prune-collapsed-types.d.ts.map +1 -0
  263. package/dist/middleware/reflect/prune-collapsed-types.js +32 -0
  264. package/dist/middleware/reflect/prune-collapsed-types.js.map +1 -0
  265. package/dist/middleware/reflect/recognize-root.d.ts +75 -0
  266. package/dist/middleware/reflect/recognize-root.d.ts.map +1 -0
  267. package/dist/middleware/reflect/recognize-root.js +204 -0
  268. package/dist/middleware/reflect/recognize-root.js.map +1 -0
  269. package/dist/middleware/reflect/render-agent-flag.d.ts +25 -0
  270. package/dist/middleware/reflect/render-agent-flag.d.ts.map +1 -0
  271. package/dist/middleware/reflect/render-agent-flag.js +39 -0
  272. package/dist/middleware/reflect/render-agent-flag.js.map +1 -0
  273. package/dist/middleware/reflect/retrieve-graph-slice.d.ts +54 -0
  274. package/dist/middleware/reflect/retrieve-graph-slice.d.ts.map +1 -0
  275. package/dist/middleware/reflect/retrieve-graph-slice.js +173 -0
  276. package/dist/middleware/reflect/retrieve-graph-slice.js.map +1 -0
  277. package/dist/middleware/reflect/root-relatedness.d.ts +31 -0
  278. package/dist/middleware/reflect/root-relatedness.d.ts.map +1 -0
  279. package/dist/middleware/reflect/root-relatedness.js +92 -0
  280. package/dist/middleware/reflect/root-relatedness.js.map +1 -0
  281. package/dist/middleware/reflect/schema-heal.d.ts +22 -0
  282. package/dist/middleware/reflect/schema-heal.d.ts.map +1 -0
  283. package/dist/middleware/reflect/schema-heal.js +119 -0
  284. package/dist/middleware/reflect/schema-heal.js.map +1 -0
  285. package/dist/middleware/reflect/schema-validator.d.ts +85 -0
  286. package/dist/middleware/reflect/schema-validator.d.ts.map +1 -0
  287. package/dist/middleware/reflect/schema-validator.js +196 -0
  288. package/dist/middleware/reflect/schema-validator.js.map +1 -0
  289. package/dist/middleware/reflect/stage-audit-graph.d.ts +115 -0
  290. package/dist/middleware/reflect/stage-audit-graph.d.ts.map +1 -0
  291. package/dist/middleware/reflect/stage-audit-graph.js +563 -0
  292. package/dist/middleware/reflect/stage-audit-graph.js.map +1 -0
  293. package/dist/middleware/reflect/stage-d-resolve-graph.d.ts +87 -0
  294. package/dist/middleware/reflect/stage-d-resolve-graph.d.ts.map +1 -0
  295. package/dist/middleware/reflect/stage-d-resolve-graph.js +256 -0
  296. package/dist/middleware/reflect/stage-d-resolve-graph.js.map +1 -0
  297. package/dist/middleware/reflect/synthesizeFromSceneCard.d.ts +15 -0
  298. package/dist/middleware/reflect/synthesizeFromSceneCard.d.ts.map +1 -0
  299. package/dist/middleware/reflect/synthesizeFromSceneCard.js +91 -0
  300. package/dist/middleware/reflect/synthesizeFromSceneCard.js.map +1 -0
  301. package/dist/middleware/reflect/types.d.ts +261 -0
  302. package/dist/middleware/reflect/types.d.ts.map +1 -0
  303. package/dist/middleware/reflect/types.js +3 -0
  304. package/dist/middleware/reflect/types.js.map +1 -0
  305. package/dist/middleware/reflect/v2-integrate.d.ts +120 -0
  306. package/dist/middleware/reflect/v2-integrate.d.ts.map +1 -0
  307. package/dist/middleware/reflect/v2-integrate.js +388 -0
  308. package/dist/middleware/reflect/v2-integrate.js.map +1 -0
  309. package/dist/middleware/reflect/v2-judge.d.ts +44 -0
  310. package/dist/middleware/reflect/v2-judge.d.ts.map +1 -0
  311. package/dist/middleware/reflect/v2-judge.js +191 -0
  312. package/dist/middleware/reflect/v2-judge.js.map +1 -0
  313. package/dist/relation-sets.d.ts +2 -0
  314. package/dist/relation-sets.d.ts.map +1 -0
  315. package/dist/relation-sets.js +35 -0
  316. package/dist/relation-sets.js.map +1 -0
  317. package/dist/routes/__tests__/flags.test.d.ts +2 -0
  318. package/dist/routes/__tests__/flags.test.d.ts.map +1 -0
  319. package/dist/routes/__tests__/flags.test.js +257 -0
  320. package/dist/routes/__tests__/flags.test.js.map +1 -0
  321. package/dist/routes/__tests__/models-catalog.test.d.ts +2 -0
  322. package/dist/routes/__tests__/models-catalog.test.d.ts.map +1 -0
  323. package/dist/routes/__tests__/models-catalog.test.js +130 -0
  324. package/dist/routes/__tests__/models-catalog.test.js.map +1 -0
  325. package/dist/routes/__tests__/reflect-pause-drain.test.d.ts +2 -0
  326. package/dist/routes/__tests__/reflect-pause-drain.test.d.ts.map +1 -0
  327. package/dist/routes/__tests__/reflect-pause-drain.test.js +38 -0
  328. package/dist/routes/__tests__/reflect-pause-drain.test.js.map +1 -0
  329. package/dist/routes/__tests__/spend-pause-drain.test.d.ts +2 -0
  330. package/dist/routes/__tests__/spend-pause-drain.test.d.ts.map +1 -0
  331. package/dist/routes/__tests__/spend-pause-drain.test.js +38 -0
  332. package/dist/routes/__tests__/spend-pause-drain.test.js.map +1 -0
  333. package/dist/routes/admin.d.ts +49 -0
  334. package/dist/routes/admin.d.ts.map +1 -0
  335. package/dist/routes/admin.js +471 -0
  336. package/dist/routes/admin.js.map +1 -0
  337. package/dist/routes/blocks.d.ts +4 -0
  338. package/dist/routes/blocks.d.ts.map +1 -0
  339. package/dist/routes/blocks.js +893 -0
  340. package/dist/routes/blocks.js.map +1 -0
  341. package/dist/routes/chat-proxy.d.ts +5 -0
  342. package/dist/routes/chat-proxy.d.ts.map +1 -0
  343. package/dist/routes/chat-proxy.js +225 -0
  344. package/dist/routes/chat-proxy.js.map +1 -0
  345. package/dist/routes/conversations.d.ts +4 -0
  346. package/dist/routes/conversations.d.ts.map +1 -0
  347. package/dist/routes/conversations.js +139 -0
  348. package/dist/routes/conversations.js.map +1 -0
  349. package/dist/routes/flags.d.ts +4 -0
  350. package/dist/routes/flags.d.ts.map +1 -0
  351. package/dist/routes/flags.js +151 -0
  352. package/dist/routes/flags.js.map +1 -0
  353. package/dist/routes/inject.d.ts +4 -0
  354. package/dist/routes/inject.d.ts.map +1 -0
  355. package/dist/routes/inject.js +183 -0
  356. package/dist/routes/inject.js.map +1 -0
  357. package/dist/routes/mcp-http.d.ts +5 -0
  358. package/dist/routes/mcp-http.d.ts.map +1 -0
  359. package/dist/routes/mcp-http.js +94 -0
  360. package/dist/routes/mcp-http.js.map +1 -0
  361. package/dist/routes/quarantine.d.ts +4 -0
  362. package/dist/routes/quarantine.d.ts.map +1 -0
  363. package/dist/routes/quarantine.js +66 -0
  364. package/dist/routes/quarantine.js.map +1 -0
  365. package/dist/routes/recall.d.ts +5 -0
  366. package/dist/routes/recall.d.ts.map +1 -0
  367. package/dist/routes/recall.js +573 -0
  368. package/dist/routes/recall.js.map +1 -0
  369. package/dist/routes/reflect.d.ts +5 -0
  370. package/dist/routes/reflect.d.ts.map +1 -0
  371. package/dist/routes/reflect.js +231 -0
  372. package/dist/routes/reflect.js.map +1 -0
  373. package/dist/routes/session.d.ts +4 -0
  374. package/dist/routes/session.d.ts.map +1 -0
  375. package/dist/routes/session.js +418 -0
  376. package/dist/routes/session.js.map +1 -0
  377. package/dist/routes/state.d.ts +116 -0
  378. package/dist/routes/state.d.ts.map +1 -0
  379. package/dist/routes/state.js +621 -0
  380. package/dist/routes/state.js.map +1 -0
  381. package/dist/routes/usage.d.ts +3 -0
  382. package/dist/routes/usage.d.ts.map +1 -0
  383. package/dist/routes/usage.js +141 -0
  384. package/dist/routes/usage.js.map +1 -0
  385. package/dist/routes/workspace.d.ts +5 -0
  386. package/dist/routes/workspace.d.ts.map +1 -0
  387. package/dist/routes/workspace.js +435 -0
  388. package/dist/routes/workspace.js.map +1 -0
  389. package/dist/server.d.ts +13 -0
  390. package/dist/server.d.ts.map +1 -0
  391. package/dist/server.js +298 -0
  392. package/dist/server.js.map +1 -0
  393. package/dist/store/__tests__/backup.test.d.ts +2 -0
  394. package/dist/store/__tests__/backup.test.d.ts.map +1 -0
  395. package/dist/store/__tests__/backup.test.js +53 -0
  396. package/dist/store/__tests__/backup.test.js.map +1 -0
  397. package/dist/store/__tests__/quality.test.d.ts +2 -0
  398. package/dist/store/__tests__/quality.test.d.ts.map +1 -0
  399. package/dist/store/__tests__/quality.test.js +75 -0
  400. package/dist/store/__tests__/quality.test.js.map +1 -0
  401. package/dist/store/backup.d.ts +14 -0
  402. package/dist/store/backup.d.ts.map +1 -0
  403. package/dist/store/backup.js +95 -0
  404. package/dist/store/backup.js.map +1 -0
  405. package/dist/store/database.d.ts +407 -0
  406. package/dist/store/database.d.ts.map +1 -0
  407. package/dist/store/database.js +2004 -0
  408. package/dist/store/database.js.map +1 -0
  409. package/dist/store/quality.d.ts +25 -0
  410. package/dist/store/quality.d.ts.map +1 -0
  411. package/dist/store/quality.js +48 -0
  412. package/dist/store/quality.js.map +1 -0
  413. package/dist/tools/__tests__/assemble-block-chains.test.d.ts +2 -0
  414. package/dist/tools/__tests__/assemble-block-chains.test.d.ts.map +1 -0
  415. package/dist/tools/__tests__/assemble-block-chains.test.js +118 -0
  416. package/dist/tools/__tests__/assemble-block-chains.test.js.map +1 -0
  417. package/dist/tools/__tests__/filter-roots-by-concepts.test.d.ts +2 -0
  418. package/dist/tools/__tests__/filter-roots-by-concepts.test.d.ts.map +1 -0
  419. package/dist/tools/__tests__/filter-roots-by-concepts.test.js +68 -0
  420. package/dist/tools/__tests__/filter-roots-by-concepts.test.js.map +1 -0
  421. package/dist/tools/__tests__/flag-surface.test.d.ts +2 -0
  422. package/dist/tools/__tests__/flag-surface.test.d.ts.map +1 -0
  423. package/dist/tools/__tests__/flag-surface.test.js +130 -0
  424. package/dist/tools/__tests__/flag-surface.test.js.map +1 -0
  425. package/dist/tools/core.d.ts +5 -0
  426. package/dist/tools/core.d.ts.map +1 -0
  427. package/dist/tools/core.js +962 -0
  428. package/dist/tools/core.js.map +1 -0
  429. package/dist/tools/derive.d.ts +5 -0
  430. package/dist/tools/derive.d.ts.map +1 -0
  431. package/dist/tools/derive.js +182 -0
  432. package/dist/tools/derive.js.map +1 -0
  433. package/dist/tools/flag-surface.d.ts +26 -0
  434. package/dist/tools/flag-surface.d.ts.map +1 -0
  435. package/dist/tools/flag-surface.js +59 -0
  436. package/dist/tools/flag-surface.js.map +1 -0
  437. package/dist/tools/helpers.d.ts +99 -0
  438. package/dist/tools/helpers.d.ts.map +1 -0
  439. package/dist/tools/helpers.js +243 -0
  440. package/dist/tools/helpers.js.map +1 -0
  441. package/dist/tools/projects.d.ts +5 -0
  442. package/dist/tools/projects.d.ts.map +1 -0
  443. package/dist/tools/projects.js +175 -0
  444. package/dist/tools/projects.js.map +1 -0
  445. package/dist/tools/system.d.ts +5 -0
  446. package/dist/tools/system.d.ts.map +1 -0
  447. package/dist/tools/system.js +1361 -0
  448. package/dist/tools/system.js.map +1 -0
  449. package/dist/tools/tasks.d.ts +5 -0
  450. package/dist/tools/tasks.d.ts.map +1 -0
  451. package/dist/tools/tasks.js +289 -0
  452. package/dist/tools/tasks.js.map +1 -0
  453. package/package.json +69 -0
  454. package/scripts/nodedex-entry.mjs +396 -0
  455. package/tui-dist/App.js +185 -0
  456. package/tui-dist/api.js +197 -0
  457. package/tui-dist/cli.js +53 -0
  458. package/tui-dist/components.js +63 -0
  459. package/tui-dist/config.js +242 -0
  460. package/tui-dist/connect-snippets.js +98 -0
  461. package/tui-dist/feed.js +51 -0
  462. package/tui-dist/health.js +465 -0
  463. package/tui-dist/hooks.js +23 -0
  464. package/tui-dist/memory.js +220 -0
  465. package/tui-dist/onboarding.js +498 -0
  466. package/tui-dist/review.js +193 -0
  467. package/tui-dist/servers.js +556 -0
  468. package/tui-dist/smoke.js +15 -0
  469. package/tui-dist/theme.js +106 -0
@@ -0,0 +1,1361 @@
1
+ import { z } from "zod";
2
+ import { blockEmbeddingText } from "../engine/embeddings.js";
3
+ import { getLLMProvider } from "../engine/providers/index.js";
4
+ import { ok, err, cosineSim } from "./helpers.js";
5
+ import { buildAgentFlagSurface } from "./flag-surface.js";
6
+ import { runArcExtraction } from "../middleware/reflect/arc-pipeline.js";
7
+ import { protocolBlock } from "../agent-protocol.js";
8
+ import fs from "fs";
9
+ import path from "path";
10
+ import crypto from "crypto";
11
+ import { fileURLToPath } from "url";
12
+ export function registerSystemTools(server, db, embeddings) {
13
+ // ─── Tool: workspace_onboard ─────────────────────────────────────
14
+ // The persistent UPGRADE over the advisory `instructions` field: on a host where
15
+ // the agent can write its OWN standing config, persist Nodedex's reflexes there so
16
+ // they're reliably followed + survive (vs an instructions field a host may not
17
+ // surface). Capability-checked, consent-gated, content-scoped — the agent does the
18
+ // write (we can't reach the host's config); we supply the content + the contract.
19
+ server.tool("workspace_onboard", `ONE-TIME setup. Offer to make Nodedex's two reflexes (check dead-ends/constraints before proposing; traverse-don't-search) PERMANENT in this project, instead of relying on per-connect instructions a host may not surface. Call this ONCE on your first connect in a project.
20
+ The result gives you a 4-step contract: (1) CHECK whether you can persist standing instructions (file-write tool + a config you read every session, e.g. CLAUDE.md / rules file); (2) if so, EXPLAIN the reason to the user so they're not confused why a tool wants to edit config, and ASK permission; (3) on yes, write protocol_block VERBATIM (marked block — replace in place if markers already exist, never duplicate); (4) if you CAN'T persist or they decline, do nothing — the session instructions already apply.`, {}, async () => {
21
+ try {
22
+ return ok({
23
+ reason_for_user: "Nodedex works best if two reflexes stay always-on across our sessions: (1) before I propose something I check what's already been tried or ruled out, and (2) I walk the memory graph instead of guessing. I can save a short, clearly-marked note to this project's agent config so they persist across sessions — you can delete it anytime. Want me to?",
24
+ step_1_check_capability: "Determine whether YOU can persist standing instructions yourself: do you have a file-write tool AND a persistent prompt/rules file you read every session (e.g. ./CLAUDE.md, .cursor/rules, a system-prompt config)? If NOT → skip to step_4.",
25
+ step_2_explain_and_ask: "If capable: tell the user `reason_for_user` (so an MCP tool editing their config isn't confusing) and ASK permission explicitly. Wait for a clear yes.",
26
+ step_3_persist: "On yes: write `protocol_block` VERBATIM into that config. It is wrapped in nodedex:protocol markers — if those markers already exist there, REPLACE that block in place (never duplicate). Do this ONCE; no need to call workspace_onboard again.",
27
+ step_4_fallback: "If you CAN'T persist (no writable standing config) or the user declines: do nothing further. The per-connect MCP instructions already cover you for this session — just proceed normally.",
28
+ protocol_block: protocolBlock(),
29
+ });
30
+ }
31
+ catch (error) {
32
+ return err("ONBOARD_FAILED", String(error));
33
+ }
34
+ });
35
+ // ─── Tool: workspace_install_capture ─────────────────────────────
36
+ // Deploy the NON-INTRUSIVE capture adapter into the host so finished turns flow
37
+ // into the pipeline. The MCP server is PASSIVE — it can't grab the agent's output
38
+ // itself; capture must be PUSHED by the host. This hands the agent the adapter
39
+ // source + a consent-gated 4-step deploy contract (the agent writes the file + wires
40
+ // it; we can't reach the host's filesystem). Out-of-path tee: the agent's own LLM is
41
+ // NEVER touched. Configurable: pick which of response/user/reasoning to capture.
42
+ // Single source of truth: read the canonical adapters/nodedex-capture.mjs from disk
43
+ // (resolves the same from src/tools or dist/tools) so the tool never drifts from it.
44
+ const readCaptureAdapter = () => {
45
+ const moduleDir = path.dirname(fileURLToPath(import.meta.url));
46
+ const candidates = [
47
+ path.resolve(moduleDir, "../../adapters/nodedex-capture.mjs"), // src/tools|dist/tools → server/adapters
48
+ path.resolve(process.cwd(), "adapters/nodedex-capture.mjs"),
49
+ path.resolve(process.cwd(), "Nodedex/server/adapters/nodedex-capture.mjs"),
50
+ ];
51
+ for (const p of candidates) {
52
+ try {
53
+ if (fs.existsSync(p))
54
+ return { filename: "nodedex-capture.mjs", source: fs.readFileSync(p, "utf8") };
55
+ }
56
+ catch { /* try next */ }
57
+ }
58
+ throw new Error(`capture adapter not found (looked in: ${candidates.join(" | ")})`);
59
+ };
60
+ server.tool("workspace_install_capture", `Deploy Nodedex's capture adapter into THIS host so each finished turn is fed to the memory pipeline. Without it, nothing is ever stored — the MCP server is passive and can't read your output itself. Call this ONCE per host during setup, after the user agrees.
61
+ The adapter is a NON-INTRUSIVE tee: it sends a COPY of {user_message, agent_response, reasoning} to POST /api/reflect/trigger, fire-and-forget, OUT of your LLM's path — your own model call is never touched or slowed. You choose which fields to capture (response/user/reasoning).
62
+ The result gives you adapter_source (write it to a file) + wiring_examples (call it in your host's post-turn seam) + a 4-step contract: (1) CHECK you can write a file AND have a post-turn seam to call it from; (2) EXPLAIN reason_for_user + ASK permission (an MCP tool wiring capture into their agent shouldn't surprise them); (3) on yes, WRITE adapter_filename verbatim + add the one-line call in the post-turn seam + (optional) set NODEDEX_CAPTURE_* env to scope fields; (4) if you can't, do nothing — capture just won't run on this host.`, {}, async () => {
63
+ try {
64
+ const { filename, source } = readCaptureAdapter();
65
+ return ok({
66
+ reason_for_user: "Nodedex only remembers what it's shown. Because it's a passive tool, it can't see my replies on its own — so to build memory, a tiny adapter sends a COPY of each turn (your message, my response, and my reasoning if available) to your local Nodedex server. It runs OUTSIDE my model call, so it never changes or slows what you already have, and you control which parts are captured — or turn it off anytime. Want me to set it up?",
67
+ what_it_does: "Out-of-path tee → POST {NODEDEX_URL}/api/reflect/trigger, fire-and-forget. The server does the rest (debounce, dedup, async extraction). Honors the server-side reflect-pause as the global off switch.",
68
+ post_endpoint: "POST {NODEDEX_URL or http://localhost:3001}/api/reflect/trigger",
69
+ config: {
70
+ note: "Set via env on the host, or per-call `capture` override. Default: all on.",
71
+ NODEDEX_CAPTURE_RESPONSE: "the agent's emitted answer — the SUBSTRATE; off ⇒ nothing is sent (default on)",
72
+ NODEDEX_CAPTURE_USER: "the user's message (default on)",
73
+ NODEDEX_CAPTURE_REASONING: "the agent's reasoning/thinking, captured when the host exposes it (default on)",
74
+ NODEDEX_URL: "where the Nodedex server lives (default http://localhost:3001)",
75
+ NODEDEX_CAPTURE_BUFFER: "on ⇒ buffer to ~/.nodedex/capture-buffer.jsonl when the server is down, flush on next success (default off)",
76
+ },
77
+ adapter_filename: filename,
78
+ adapter_source: source,
79
+ wiring_examples: {
80
+ generic: "import { nodedexCapture } from './nodedex-capture.mjs';\n// after your turn completes:\nnodedexCapture({ userMessage, agentResponse: out.text, agentId: sessionId });",
81
+ openai_shape: "import { nodedexCapture, extractReasoning } from './nodedex-capture.mjs';\nconst c = await openai.chat.completions.create({ /* your call, untouched */ });\nconst msg = c.choices[0].message;\nnodedexCapture({ userMessage, agentResponse: msg.content, reasoning: extractReasoning(msg), agentId: sessionId });",
82
+ framework_callback: "// In an Agent SDK / LangChain onTurnEnd, or a Stop hook: register nodedexCapture so it runs once per completed turn.",
83
+ scoped_fields: "// capture response only (skip user + reasoning) for this call:\nnodedexCapture(turn, { capture: { user: false, reasoning: false } });",
84
+ },
85
+ step_1_check_capability: "Confirm you CAN deploy: you have a file-write tool AND a place that runs once per completed turn (a post-turn callback / Stop hook / the line right after your model call). If not → step_4.",
86
+ step_2_explain_and_ask: "Tell the user `reason_for_user` and ASK permission — wiring capture into their agent shouldn't surprise them. Wait for a clear yes.",
87
+ step_3_deploy: "On yes: (a) write `adapter_source` to `adapter_filename` next to your agent code; (b) add the matching one-liner from wiring_examples in your post-turn seam; (c) OPTIONAL: set NODEDEX_CAPTURE_* env to scope which fields are sent. Do this ONCE per host.",
88
+ step_4_fallback: "If you can't write files / have no post-turn seam, or the user declines: do nothing. Capture simply won't run here — reads still work; the graph just won't grow from this host.",
89
+ });
90
+ }
91
+ catch (error) {
92
+ return err("INSTALL_CAPTURE_FAILED", String(error));
93
+ }
94
+ });
95
+ // ─── Tool: workspace_create_type ─────────────────────────────────
96
+ server.tool("workspace_create_type", `Create a new custom block type that extends a core type (e.g., 'concept' extending 'note').`, {
97
+ name: z.string().describe("Type name (lowercase, unspaced)"),
98
+ extends: z.string().describe("Core type to extend (e.g., 'note', 'fact', 'entity')"),
99
+ description: z.string().describe("What this type is used for"),
100
+ typical_fields: z.array(z.string()).optional().describe("Fields often used in 'unique' or 'has' for this type"),
101
+ }, async (params) => {
102
+ try {
103
+ const success = db.createBlockType(params);
104
+ if (!success)
105
+ return err("DUPLICATE_TYPE", `Type '${params.name}' already exists`);
106
+ return ok({ type: params.name, extends: params.extends, description: params.description });
107
+ }
108
+ catch (error) {
109
+ return err("CREATE_TYPE_FAILED", String(error));
110
+ }
111
+ });
112
+ // ─── Tool: workspace_create_relation_type ────────────────────────
113
+ server.tool("workspace_create_relation_type", `Create a new custom relation type (e.g., 'uses', 'depends_on').`, {
114
+ name: z.string().describe("Relation name (lowercase, unspaced)"),
115
+ inverse: z.string().optional().describe("The inverse relation name (e.g., 'used_by' for 'uses')"),
116
+ description: z.string().describe("What this relation signifies"),
117
+ }, async (params) => {
118
+ try {
119
+ const success = db.createRelationType(params);
120
+ if (!success)
121
+ return err("DUPLICATE_RELATION_TYPE", `Relation type '${params.name}' already exists`);
122
+ return ok({ relation_type: params.name, inverse: params.inverse || null, description: params.description });
123
+ }
124
+ catch (error) {
125
+ return err("CREATE_RELATION_TYPE_FAILED", String(error));
126
+ }
127
+ });
128
+ // ─── Tool: workspace_gc ──────────────────────────────────────────
129
+ server.tool("workspace_gc", `Run lifecycle garbage collection. Archives expired blocks (based on TTL) and marks old permanent blocks as stale.`, {}, async () => {
130
+ try {
131
+ const results = db.runGC();
132
+ return ok({
133
+ message: "Garbage collection complete",
134
+ archived_count: results.archived,
135
+ protected_count: results.protected,
136
+ promoted_count: results.promoted,
137
+ });
138
+ }
139
+ catch (error) {
140
+ return err("GC_FAILED", String(error));
141
+ }
142
+ });
143
+ // ─── Tool: workspace_stats ───────────────────────────────────────
144
+ server.tool("workspace_stats", `ORIENT — the landscape of the graph: how many blocks of each type/status exist, plus whether semantic embeddings are available. Call it to get your bearings (e.g. at session start) before drilling in.
145
+ The 'extraction' field reports EXTRACTION FRESHNESS — extraction is async (a background pipeline writes the graph AFTER you finish), so for a few seconds your latest work is NOT yet queryable. It disambiguates the look-alike "empty" states a plain query can't:
146
+ • pending — turns captured but not yet in the graph: a query may MISS them, so don't read "graph returned nothing" as "nothing exists". pending.failed=true means the last extraction attempt FAILED (re-trigger) rather than just being queued.
147
+ • recent[] — recent arcs, each { topic (what it was about, so you recognize it), turns, blocks, chain (workspace_get this to read the whole story) }. blocks:0 means it ran and found nothing worth saving — final, not an error.
148
+ The 'flags' field reports SELF-MAINTENANCE items the system couldn't resolve alone and ROUTED TO YOU — the small residue the auto-cleaner refuses to guess on (it needs your conversation context, e.g. "are these two entries the same thing, and whose are they?"). needs_your_input = how many wait; items[] are plain-English questions (no ids/schema to fill). Answer from context or ask the user. Usually empty — the system cleans the clear cases itself.
149
+ Without agent_id this is the most recent activity (single-agent: that's you); pass your agent_id to scope it to YOUR work.
150
+ For relevant ROOTS use workspace_filter; for a specific block use workspace_get.`, { agent_id: z.string().optional().describe("Your agent_id — scopes extraction freshness to YOUR recent work. Omit for the most recent activity across all agents.") }, async ({ agent_id }) => {
151
+ try {
152
+ const stats = db.getStats();
153
+ return ok({
154
+ ...stats,
155
+ embeddings_enabled: embeddings.isAvailable(),
156
+ extraction: db.getExtractionStatus(agent_id),
157
+ flags: buildAgentFlagSurface(db),
158
+ });
159
+ }
160
+ catch (error) {
161
+ return err("STATS_FAILED", String(error));
162
+ }
163
+ });
164
+ // ─── Tool: workspace_artifact_save ──────────────────────────────
165
+ server.tool("workspace_artifact_save", `Save a concrete output (code, document, data, image) produced by agent work.
166
+
167
+ Storage is automatic based on size:
168
+ < 8KB → stored inline in the block (fully searchable, part of the knowledge graph)
169
+ 8KB–5MB → written to data/artifacts/<block_id>/<filename> on disk, block stores the path + SHA256
170
+ > 5MB → block stores reference only (path, URL, or external key — content not stored)
171
+
172
+ Always link to the task that produced it via task_id.
173
+ Use workspace_get(id, "content") to read back inline artifacts.
174
+ Use the returned path to read back file-based artifacts.`, {
175
+ filename: z.string().describe("Filename with extension, e.g. 'summary.md', 'analysis.py', 'results.json'"),
176
+ content: z.string().optional().describe("The artifact content as a string. Omit for external/binary artifacts."),
177
+ mime_type: z.string().optional().describe("MIME type, e.g. 'text/markdown', 'text/x-python', 'application/json'. Auto-detected from filename if omitted."),
178
+ label: z.string().optional().describe("Block label (auto-generated from filename if omitted)"),
179
+ essence: z.string().describe("One sentence: what this artifact is and what produced it"),
180
+ task_id: z.string().optional().describe("Task block ID or label that produced this artifact"),
181
+ agent_id: z.string().optional().describe("Agent that produced this artifact"),
182
+ external_path: z.string().optional().describe("For large/binary artifacts: the path, URL, or external key where the content lives"),
183
+ save_context: z.object({
184
+ triggered_by: z.array(z.string()).optional().describe("Block IDs or labels that caused this artifact — creates prompted_by relations"),
185
+ problem_being_solved: z.string().optional(),
186
+ }).optional().describe("Causal chain — triggered_by links this artifact to what caused it"),
187
+ }, async (params) => {
188
+ try {
189
+ const INLINE_THRESHOLD = 8 * 1024; // 8KB
190
+ const FILE_THRESHOLD = 5 * 1024 * 1024; // 5MB
191
+ // Detect MIME type from filename if not provided
192
+ const ext = path.extname(params.filename).toLowerCase();
193
+ const mimeMap = {
194
+ ".md": "text/markdown", ".txt": "text/plain", ".py": "text/x-python",
195
+ ".ts": "text/typescript", ".js": "text/javascript", ".json": "application/json",
196
+ ".html": "text/html", ".css": "text/css", ".csv": "text/csv",
197
+ ".yaml": "text/yaml", ".yml": "text/yaml", ".sh": "text/x-sh",
198
+ };
199
+ const mimeType = params.mime_type || mimeMap[ext] || "application/octet-stream";
200
+ const label = params.label || `artifact_${params.filename.replace(/[^a-z0-9]/gi, "_").toLowerCase()}`;
201
+ const contentBytes = params.content ? Buffer.byteLength(params.content, "utf8") : 0;
202
+ const sha256 = params.content
203
+ ? crypto.createHash("sha256").update(params.content).digest("hex").slice(0, 16)
204
+ : null;
205
+ let storage;
206
+ let storedPath = null;
207
+ let inlineBody = null;
208
+ if (params.external_path) {
209
+ // External reference — just store the pointer
210
+ storage = "external";
211
+ storedPath = params.external_path;
212
+ }
213
+ else if (!params.content) {
214
+ return err("CONTENT_REQUIRED", "Provide content or external_path");
215
+ }
216
+ else if (contentBytes <= INLINE_THRESHOLD) {
217
+ // Small — store inline in the block
218
+ storage = "inline";
219
+ inlineBody = params.content;
220
+ }
221
+ else if (contentBytes <= FILE_THRESHOLD) {
222
+ // Medium — write to disk
223
+ storage = "file";
224
+ }
225
+ else {
226
+ // Large — store reference only, warn user to provide external_path next time
227
+ storage = "truncated_reference";
228
+ }
229
+ // Create the artifact block first (we need the ID for the file path)
230
+ const block = db.createBlock({
231
+ label,
232
+ type: "artifact",
233
+ essence: params.essence,
234
+ content: {
235
+ unique: {
236
+ filename: params.filename,
237
+ mime_type: mimeType,
238
+ size_bytes: String(contentBytes),
239
+ storage,
240
+ sha256: sha256 || "",
241
+ produced_by: params.agent_id || "",
242
+ },
243
+ has: {
244
+ ...(inlineBody ? { body: inlineBody } : {}),
245
+ ...(storedPath ? { path: storedPath } : {}),
246
+ },
247
+ },
248
+ ttl: "permanent",
249
+ });
250
+ // For file storage: now write to disk using the block ID
251
+ if (storage === "file" && params.content) {
252
+ const dbDir = path.resolve(process.cwd(), "../../data");
253
+ const artifactDir = path.join(dbDir, "artifacts", block.id);
254
+ fs.mkdirSync(artifactDir, { recursive: true });
255
+ storedPath = path.join(artifactDir, params.filename);
256
+ fs.writeFileSync(storedPath, params.content, "utf8");
257
+ // Update block with the path
258
+ const content = typeof block.content === "string" ? JSON.parse(block.content) : block.content;
259
+ db.updateBlock(block.id, {
260
+ content: { ...content, has: { ...content.has, path: storedPath } }
261
+ }, "file path set after write");
262
+ }
263
+ // Link to task if provided
264
+ if (params.task_id) {
265
+ const task = db.getBlock(params.task_id);
266
+ if (task) {
267
+ db.createRelation({
268
+ source_id: block.id,
269
+ target_id: task.id,
270
+ type: "produced_by",
271
+ bidirectional: false,
272
+ });
273
+ }
274
+ }
275
+ // prompted_by relations from save_context.triggered_by
276
+ if (params.save_context?.triggered_by?.length) {
277
+ for (const targetRef of params.save_context.triggered_by) {
278
+ const targetBlock = db.getBlock(targetRef);
279
+ if (targetBlock) {
280
+ db.createRelation({ source_id: block.id, target_id: targetBlock.id, type: "prompted_by" });
281
+ }
282
+ }
283
+ }
284
+ // Compute quality score — artifact blocks score on unique{} + has{} + relations
285
+ {
286
+ const c = typeof block.content === "string" ? JSON.parse(block.content) : (block.content || {});
287
+ let qScore = 1; // essence always present
288
+ // is_a not set by artifact_save → skip
289
+ if (c.unique && Object.keys(c.unique).length >= 2)
290
+ qScore++; // filename + mime_type etc.
291
+ // concepts not set → skip
292
+ if (db.getRelations(block.id).length > 0)
293
+ qScore++;
294
+ db.updateBlock(block.id, { quality_score: qScore });
295
+ }
296
+ // Coordinates check
297
+ const hasCausalChain = (params.save_context?.triggered_by?.length ?? 0) > 0 ||
298
+ !!params.task_id;
299
+ const missingCoords = hasCausalChain ? undefined
300
+ : "No triggered_by — add save_context.triggered_by or task_id to establish causal chain.";
301
+ return ok({
302
+ id: block.id,
303
+ label: block.label,
304
+ storage,
305
+ filename: params.filename,
306
+ size_bytes: contentBytes,
307
+ sha256,
308
+ path: storedPath || null,
309
+ ...(missingCoords ? { missing_coordinates: missingCoords } : {}),
310
+ hint: storage === "inline"
311
+ ? "Content stored in block. Read with workspace_get(id, 'content') → has.body"
312
+ : storage === "file"
313
+ ? `Content written to disk. Path: ${storedPath}`
314
+ : storage === "external"
315
+ ? `External reference stored. Path/URL: ${storedPath}`
316
+ : "Content too large for storage. Store externally and use external_path parameter.",
317
+ });
318
+ }
319
+ catch (error) {
320
+ return err("ARTIFACT_SAVE_FAILED", String(error));
321
+ }
322
+ });
323
+ // ─── Tool: workspace_challenge ───────────────────────────────────
324
+ server.tool("workspace_challenge", `Challenge a fact or decision block made by another agent. Opens a dispute reasoning block.`, {
325
+ id: z.string().describe("Block ID or label to challenge"),
326
+ reasoning: z.string().describe("Why is this block incorrect or sub-optimal?"),
327
+ }, async (params) => {
328
+ try {
329
+ const original = db.getBlock(params.id);
330
+ if (!original)
331
+ return err("BLOCK_NOT_FOUND", `Block '${params.id}' not found`);
332
+ // Create a dispute block
333
+ const challengeBlock = db.createBlock({
334
+ label: `challenge_to_${original.label}`.slice(0, 60),
335
+ type: "note",
336
+ essence: `Challenge to: ${original.essence}`,
337
+ content: {
338
+ dispute_reasoning: params.reasoning,
339
+ challenged_block_id: original.id,
340
+ },
341
+ ttl: "project",
342
+ });
343
+ // Link them
344
+ db.createRelation({
345
+ source_id: challengeBlock.id,
346
+ target_id: original.id,
347
+ type: "challenges",
348
+ bidirectional: false,
349
+ });
350
+ // Mark original as contested
351
+ const originalContent = JSON.parse(original.content);
352
+ db.updateBlock(original.id, {
353
+ content: { ...originalContent, contested: true, contested_by: challengeBlock.id },
354
+ }, "Challenged");
355
+ return ok({
356
+ challenged: true,
357
+ original_id: original.id,
358
+ challenge_block_id: challengeBlock.id,
359
+ status: "contested",
360
+ });
361
+ }
362
+ catch (error) {
363
+ return err("CHALLENGE_FAILED", String(error));
364
+ }
365
+ });
366
+ // ─── Tool: workspace_stale ───────────────────────────────────────
367
+ server.tool("workspace_stale", `Find blocks that have gone stale — not recently accessed relative to their usage history.
368
+ Useful for workspace maintenance: find outdated decisions, forgotten facts, or blocks that need review.
369
+ Staleness score = days_inactive / log(access_count + 2). Higher = staler.`, {
370
+ threshold: z.number().optional().describe("Staleness score cutoff (default: 3.0). Lower = more results."),
371
+ type: z.string().optional().describe("Filter by block type (e.g. 'decision', 'fact')"),
372
+ }, async (params) => {
373
+ try {
374
+ const threshold = params.threshold ?? 3.0;
375
+ const now = Date.now();
376
+ const allBlocks = db.getAllBlocks().filter((b) => b.status === "active");
377
+ const stale = allBlocks
378
+ .filter((b) => !params.type || b.type === params.type)
379
+ .map((b) => {
380
+ const days = (now - new Date(b.last_accessed).getTime()) / 86400000;
381
+ const score = Math.round((days / Math.log(b.access_count + 2)) * 10) / 10;
382
+ return { id: b.id, label: b.label, type: b.type, essence: b.essence,
383
+ staleness_score: score, days_inactive: Math.floor(days),
384
+ suggestion: score > 10 ? "consider archiving" : score > 6 ? "needs review" : "slightly stale" };
385
+ })
386
+ .filter((b) => b.staleness_score > threshold)
387
+ .sort((a, b) => b.staleness_score - a.staleness_score);
388
+ return ok({
389
+ total_stale: stale.length,
390
+ threshold,
391
+ blocks: stale.slice(0, 20),
392
+ hint: stale.length > 0
393
+ ? "Use workspace_get(id) to review, workspace_update to refresh, or workspace_forget to archive."
394
+ : "Workspace is fresh — no stale blocks found.",
395
+ });
396
+ }
397
+ catch (error) {
398
+ return err("STALE_FAILED", String(error));
399
+ }
400
+ });
401
+ // ─── Tool: workspace_resolve ──────────────────────────────────────
402
+ server.tool("workspace_resolve", `Resolve a contradiction or conflict between two blocks.
403
+ Use when two blocks say conflicting things about the same topic, or when a block is outdated.
404
+ Actions: keep_this (archive other), keep_other (archive this), archive_this (archive current block only).`, {
405
+ block_id: z.string().describe("The block ID in conflict (the one you're acting on)"),
406
+ action: z.enum(["keep_this", "keep_other", "archive_this"]).describe("keep_this = archive the other block | keep_other = archive this block | archive_this = archive this block only"),
407
+ other_id: z.string().optional().describe("The other block ID (required for keep_this or keep_other)"),
408
+ reason: z.string().optional().describe("Why this resolution was chosen — stored in history"),
409
+ }, async (params) => {
410
+ try {
411
+ const block = db.getBlock(params.block_id);
412
+ if (!block)
413
+ return err("BLOCK_NOT_FOUND", `Block '${params.block_id}' not found`);
414
+ const other = params.other_id ? db.getBlock(params.other_id) : null;
415
+ if ((params.action === "keep_this" || params.action === "keep_other") && !other) {
416
+ return err("OTHER_REQUIRED", "other_id is required for keep_this / keep_other actions");
417
+ }
418
+ if (params.action === "keep_this") {
419
+ db.archiveBlock(other.id, params.reason || `Conflict resolved: kept '${block.label}'`);
420
+ return ok({ resolved: true, kept: block.label, archived: other.label, action: "keep_this" });
421
+ }
422
+ if (params.action === "keep_other") {
423
+ db.archiveBlock(block.id, params.reason || `Conflict resolved: kept '${other.label}'`);
424
+ return ok({ resolved: true, kept: other.label, archived: block.label, action: "keep_other" });
425
+ }
426
+ if (params.action === "archive_this") {
427
+ db.archiveBlock(block.id, params.reason || "Archived via conflict resolution");
428
+ return ok({ resolved: true, archived: block.label, action: "archive_this" });
429
+ }
430
+ return err("INVALID_ACTION", "action must be: keep_this | keep_other | archive_this");
431
+ }
432
+ catch (error) {
433
+ return err("RESOLVE_FAILED", String(error));
434
+ }
435
+ });
436
+ // ─── Tool: workspace_export ───────────────────────────────────────
437
+ server.tool("workspace_export", `Export workspace blocks and relations for backup, sharing, or interop with other memory systems.
438
+
439
+ Formats:
440
+ json → raw block array (default) — good for backup and reimport
441
+ markdown → human-readable document — good for sharing with non-agents
442
+ json-ld → JSON-LD with @context — standard linked-data format for interop with other
443
+ knowledge graph systems (Zep, Mem0, custom agents)`, {
444
+ format: z.enum(["json", "markdown", "json-ld"]).optional().describe("Output format (default: json)"),
445
+ include_archived: z.boolean().optional().describe("Include archived blocks (default: false)"),
446
+ type: z.string().optional().describe("Filter by block type (e.g. 'fact', 'decision')"),
447
+ include_relations: z.boolean().optional().describe("Include relations in export (default: true for json-ld)"),
448
+ }, async (params) => {
449
+ try {
450
+ const allBlocks = db.getAllBlocks();
451
+ const allRelations = db.getAllRelations();
452
+ const filtered = allBlocks.filter((b) => {
453
+ if (!params.include_archived && b.status === "archived")
454
+ return false;
455
+ if (params.type && b.type !== params.type)
456
+ return false;
457
+ return true;
458
+ });
459
+ const filteredIds = new Set(filtered.map(b => b.id));
460
+ if (params.format === "markdown") {
461
+ const lines = [
462
+ `# Workspace Export`,
463
+ `> Generated: ${new Date().toISOString()} | Blocks: ${filtered.length}`,
464
+ ``,
465
+ ];
466
+ for (const b of filtered) {
467
+ lines.push(`## ${b.label}`);
468
+ lines.push(`**Type:** ${b.type} | **TTL:** ${b.ttl}`);
469
+ lines.push(`**Essence:** ${b.essence}`);
470
+ if (b.source)
471
+ lines.push(`**Source:** ${b.source}`);
472
+ try {
473
+ const content = JSON.parse(b.content);
474
+ if (content.unique && Object.keys(content.unique).length) {
475
+ lines.push(`**Properties:**`);
476
+ for (const [k, v] of Object.entries(content.unique))
477
+ lines.push(`- ${k}: ${v}`);
478
+ }
479
+ }
480
+ catch { /* ignore */ }
481
+ const rels = allRelations.filter(r => r.source_id === b.id && filteredIds.has(r.target_id));
482
+ if (rels.length > 0) {
483
+ lines.push(`**Relations:**`);
484
+ rels.forEach(r => {
485
+ const tgt = filtered.find(x => x.id === r.target_id);
486
+ lines.push(`- [${r.type}] → ${tgt?.label || r.target_id}`);
487
+ });
488
+ }
489
+ lines.push(`*Created: ${b.created_at} | Accessed: ${b.access_count}x*`);
490
+ lines.push(``);
491
+ }
492
+ return ok({ format: "markdown", block_count: filtered.length, export: lines.join("\n") });
493
+ }
494
+ if (params.format === "json-ld") {
495
+ // JSON-LD format — standard linked data, interoperable with other knowledge graph systems
496
+ const context = {
497
+ "@vocab": "https://wmcs.agent/ontology#",
498
+ "label": { "@id": "rdfs:label" },
499
+ "essence": { "@id": "wmcs:essence" },
500
+ "type": { "@id": "wmcs:blockType" },
501
+ "created_at": { "@id": "dcterms:created", "@type": "xsd:dateTime" },
502
+ "source": { "@id": "dcterms:source" },
503
+ "relations": { "@id": "wmcs:hasRelation", "@container": "@set" },
504
+ "rdfs": "http://www.w3.org/2000/01/rdf-schema#",
505
+ "wmcs": "https://wmcs.agent/ontology#",
506
+ "xsd": "http://www.w3.org/2001/XMLSchema#",
507
+ "dcterms": "http://purl.org/dc/terms/",
508
+ };
509
+ const graph = filtered.map(b => {
510
+ const content = (() => { try {
511
+ return JSON.parse(b.content);
512
+ }
513
+ catch {
514
+ return {};
515
+ } })();
516
+ const rels = allRelations
517
+ .filter(r => r.source_id === b.id && filteredIds.has(r.target_id))
518
+ .map(r => ({ "@type": r.type, "target": r.target_id }));
519
+ return {
520
+ "@id": `wmcs:block/${b.id}`,
521
+ "@type": `wmcs:${b.type}`,
522
+ "label": b.label,
523
+ "essence": b.essence,
524
+ "created_at": b.created_at,
525
+ ...(b.source ? { "source": b.source } : {}),
526
+ ...(content.unique ? { "properties": content.unique } : {}),
527
+ ...(content.concepts?.length ? { "concepts": content.concepts } : {}),
528
+ ...(rels.length > 0 ? { "relations": rels } : {}),
529
+ };
530
+ });
531
+ return ok({
532
+ format: "json-ld",
533
+ block_count: filtered.length,
534
+ relation_count: allRelations.filter(r => filteredIds.has(r.source_id) && filteredIds.has(r.target_id)).length,
535
+ export: { "@context": context, "@graph": graph },
536
+ });
537
+ }
538
+ // Default: JSON with relations
539
+ const withRelations = params.include_relations !== false;
540
+ const exportData = filtered.map(b => ({
541
+ ...b,
542
+ ...(withRelations ? {
543
+ relations: allRelations
544
+ .filter(r => r.source_id === b.id)
545
+ .map(r => ({ type: r.type, target_id: r.target_id }))
546
+ } : {}),
547
+ }));
548
+ return ok({ format: "json", block_count: filtered.length, export: exportData });
549
+ }
550
+ catch (error) {
551
+ return err("EXPORT_FAILED", String(error));
552
+ }
553
+ });
554
+ // ─── Tool: workspace_find_skill ──────────────────────────────────
555
+ server.tool("workspace_find_skill", `Find stored skills, procedures, or reusable knowledge that can help with a problem.
556
+ Searches across ALL block types — not just 'process'. Cross-domain retrieval: a debugging technique may surface when solving a negotiation problem if they share abstract concepts.
557
+ Each result tells you WHAT it is, WHY it matched, and HOW to apply it.
558
+ Use this before solving a problem to check if you've already stored a relevant approach.`, {
559
+ problem: z.string().describe("Describe what you are trying to do or the problem you face"),
560
+ concepts: z.array(z.string()).optional().describe("Explicit concept tags to search for (e.g. ['systematic_elimination', 'rate_limiting']). Added on top of extracted concepts."),
561
+ block_types: z.array(z.string()).optional().describe("Restrict to specific types (e.g. ['process', 'fact']). Default: all types."),
562
+ limit: z.number().optional().describe("Max results to return. Default: 5"),
563
+ }, async (params) => {
564
+ try {
565
+ const limit = params.limit ?? 5;
566
+ const STOPWORDS = new Set(["the", "is", "a", "an", "to", "of", "in", "for", "on", "with", "and", "or", "but", "it", "this", "that", "how", "what", "why", "can", "do", "be", "are", "was", "were", "will", "i", "my", "we", "our"]);
567
+ // Extract concepts from problem description
568
+ const queryConcepts = [
569
+ ...params.problem.toLowerCase().replace(/[^a-z0-9_ ]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !STOPWORDS.has(w)),
570
+ ...(params.concepts ?? []).map((c) => c.toLowerCase()),
571
+ ];
572
+ const allBlocks = db.getAllBlocks().filter((b) => {
573
+ if (b.status === "archived")
574
+ return false;
575
+ if (params.block_types?.length)
576
+ return params.block_types.includes(b.type);
577
+ return true;
578
+ });
579
+ // Score each block on three axes
580
+ const scored = [];
581
+ // Semantic vector for the problem
582
+ let queryVec = null;
583
+ if (embeddings.isAvailable()) {
584
+ try {
585
+ queryVec = await embeddings.embed(params.problem);
586
+ }
587
+ catch { /* ignore */ }
588
+ }
589
+ for (const block of allBlocks) {
590
+ let blockConcepts = [];
591
+ let blockContent = {};
592
+ try {
593
+ blockContent = typeof block.content === "string" ? JSON.parse(block.content) : block.content;
594
+ blockConcepts = (blockContent.concepts || []).map((c) => c.toLowerCase());
595
+ }
596
+ catch { /* ignore */ }
597
+ // Concept overlap
598
+ const matched = queryConcepts.filter((qc) => blockConcepts.some((bc) => bc.includes(qc) || qc.includes(bc)));
599
+ // Semantic similarity
600
+ let semSim = 0;
601
+ if (queryVec && block.embedding) {
602
+ try {
603
+ const bv = JSON.parse(block.embedding);
604
+ semSim = cosineSim(queryVec, bv);
605
+ }
606
+ catch { /* ignore */ }
607
+ }
608
+ // Keyword hit
609
+ const probLower = params.problem.toLowerCase();
610
+ const keywordHit = block.label.toLowerCase().split("_").some((w) => probLower.includes(w)) ||
611
+ block.essence.toLowerCase().split(" ").some((w) => w.length > 3 && probLower.includes(w));
612
+ const score = matched.length * 0.4 + // concept overlap — strongest signal
613
+ semSim * 0.45 + // semantic meaning
614
+ (keywordHit ? 0.15 : 0); // surface keyword bonus
615
+ if (score > 0.1 || matched.length > 0) {
616
+ scored.push({ block, score, matchedConcepts: matched, semanticSim: semSim, keywordHit });
617
+ }
618
+ }
619
+ scored.sort((a, b) => b.score - a.score);
620
+ const top = scored.slice(0, limit);
621
+ if (top.length === 0) {
622
+ return ok({
623
+ found: 0,
624
+ skills: [],
625
+ hint: "No matching skills found. Consider saving a process block with workspace_remember(type:'process', concepts:[...]).",
626
+ });
627
+ }
628
+ // Build clear, structured output — each result tells the agent exactly what it is
629
+ const skills = top.map(({ block, score, matchedConcepts, semanticSim, keywordHit }) => {
630
+ let content = {};
631
+ try {
632
+ content = typeof block.content === "string" ? JSON.parse(block.content) : block.content;
633
+ }
634
+ catch { /* ignore */ }
635
+ // Determine WHY it was matched — shown to agent so they can judge relevance
636
+ const reasons = [];
637
+ if (matchedConcepts.length > 0)
638
+ reasons.push(`shared concepts: ${matchedConcepts.join(", ")}`);
639
+ if (semanticSim > 0.65)
640
+ reasons.push(`semantically similar (${Math.round(semanticSim * 100)}%)`);
641
+ if (keywordHit)
642
+ reasons.push("keyword match");
643
+ // Extract steps / procedure from 'has' field if available
644
+ const steps = [];
645
+ if (content.has) {
646
+ for (const [k, v] of Object.entries(content.has)) {
647
+ if (Array.isArray(v))
648
+ steps.push(...v.map((s) => `${k}: ${s}`));
649
+ else if (typeof v === "string")
650
+ steps.push(`${k}: ${v}`);
651
+ }
652
+ }
653
+ // Determine if this is a transferable pattern vs domain-specific knowledge
654
+ const isTransferable = matchedConcepts.length > 0 && semanticSim < 0.75;
655
+ const domainNote = isTransferable
656
+ ? `This ${block.type} is from a different domain but shares abstract concepts with your problem.`
657
+ : `This ${block.type} directly relates to your problem.`;
658
+ return {
659
+ id: block.id,
660
+ label: block.label,
661
+ type: block.type,
662
+ what_it_is: block.essence,
663
+ match_reason: reasons.join(" + ") || "weak match",
664
+ match_score: Math.round(score * 100) / 100,
665
+ is_transferable: isTransferable,
666
+ domain_note: domainNote,
667
+ concepts: content.concepts || [],
668
+ steps: steps.length > 0 ? steps : undefined,
669
+ created_by: block.created_by || null,
670
+ how_to_use: block.type === "process"
671
+ ? "Apply the steps in 'content' field. Adapt to your specific context."
672
+ : block.type === "fact"
673
+ ? "Use as reference data. Verify if outdated (check updated_at)."
674
+ : block.type === "decision"
675
+ ? "Treat as established direction. Challenge via workspace_challenge() if you disagree."
676
+ : "Use as supporting knowledge. Call workspace_get(id) for full detail.",
677
+ full_detail: `workspace_get("${block.id}")`,
678
+ };
679
+ });
680
+ return ok({
681
+ found: skills.length,
682
+ query_concepts: queryConcepts.slice(0, 10),
683
+ skills,
684
+ hint: skills.some((s) => s.is_transferable)
685
+ ? "Some results are cross-domain transfers (different topic, same pattern). Check match_reason and domain_note before applying."
686
+ : "Results are direct matches. Apply with confidence.",
687
+ });
688
+ }
689
+ catch (error) {
690
+ return err("FIND_SKILL_FAILED", String(error));
691
+ }
692
+ });
693
+ // ─── Tool: workspace_reembed ─────────────────────────────────────
694
+ server.tool("workspace_reembed", `Backfill semantic embeddings for blocks that are missing them.
695
+ Blocks without embeddings are invisible to semantic search, concept recall, workspace_find_skill,
696
+ and workspace_infer_relations — they silently return no results. Run this once after connecting
697
+ a Gemini API key, or after importing blocks from another source.
698
+ Use force:true to regenerate embeddings for ALL blocks (e.g., after changing the embedding model).`, {
699
+ force: z.boolean().optional().describe("Re-embed ALL blocks, not just missing ones. Default: false"),
700
+ limit: z.number().optional().describe("Max blocks to process in one call (to avoid rate limits). Default: 50"),
701
+ }, async (params) => {
702
+ if (!embeddings.isAvailable()) {
703
+ return err("EMBEDDINGS_UNAVAILABLE", "Embedding provider not available. Set GEMINI_API_KEY or OPENAI_API_KEY in .env and restart.");
704
+ }
705
+ try {
706
+ const force = params.force ?? false;
707
+ const limit = params.limit ?? 50;
708
+ const targets = force
709
+ ? db.getAllBlocks().filter((b) => b.status !== "archived")
710
+ : db.getBlocksWithoutEmbeddings();
711
+ const batch = targets.slice(0, limit);
712
+ let embedded = 0;
713
+ let skipped = 0;
714
+ let errors = 0;
715
+ const failed = [];
716
+ for (const block of batch) {
717
+ // Skip sensitive blocks — don't send encrypted content to external API
718
+ if (block.is_sensitive) {
719
+ skipped++;
720
+ continue;
721
+ }
722
+ try {
723
+ const vec = await embeddings.embedForBlock({
724
+ essence: block.essence,
725
+ concepts: block.concepts,
726
+ });
727
+ if (vec) {
728
+ db.updateEmbedding(block.id, vec);
729
+ embedded++;
730
+ }
731
+ else {
732
+ errors++;
733
+ failed.push(block.label);
734
+ }
735
+ }
736
+ catch {
737
+ errors++;
738
+ failed.push(block.label);
739
+ }
740
+ }
741
+ // Save once after batch
742
+ db.save();
743
+ const remaining = targets.length - batch.length;
744
+ return ok({
745
+ processed: batch.length,
746
+ embedded,
747
+ skipped_sensitive: skipped,
748
+ errors,
749
+ failed_labels: failed.length > 0 ? failed : undefined,
750
+ remaining_after_this_call: remaining,
751
+ hint: remaining > 0
752
+ ? `${remaining} blocks still need embeddings. Call workspace_reembed again (with limit:${limit}) to continue.`
753
+ : "All blocks now have embeddings. Semantic search and skill retrieval are fully operational.",
754
+ });
755
+ }
756
+ catch (error) {
757
+ return err("REEMBED_FAILED", String(error));
758
+ }
759
+ });
760
+ // ─── Tool: workspace_review_pending ──────────────────────────────
761
+ server.tool("workspace_review_pending", `Review inferred relations waiting for approval. Three modes:
762
+ 1. No params → list all pending (see what needs review)
763
+ 2. approve_ids / reject_ids → manually act on specific relation IDs
764
+ 3. use_ai:true → Gemini evaluates every pending relation and auto-approves/rejects/corrects
765
+
766
+ use_ai mode asks Gemini: "Is this relation type accurate between these two blocks?"
767
+ - Confident yes → approved (active)
768
+ - No → rejected (deleted)
769
+ - Better type exists → relation type is corrected then approved`, {
770
+ approve_ids: z.array(z.string()).optional().describe("Relation IDs to approve"),
771
+ reject_ids: z.array(z.string()).optional().describe("Relation IDs to reject"),
772
+ use_ai: z.boolean().optional().describe("Let Gemini batch-evaluate all pending relations. Default: false"),
773
+ }, async (params) => {
774
+ try {
775
+ const approved = [];
776
+ const rejected = [];
777
+ const corrected = [];
778
+ // ── Manual actions ───────────────────────────────────────
779
+ for (const id of (params.approve_ids ?? [])) {
780
+ db.approveRelation(id);
781
+ approved.push(id);
782
+ }
783
+ for (const id of (params.reject_ids ?? [])) {
784
+ db.rejectRelation(id);
785
+ rejected.push(id);
786
+ }
787
+ // ── AI bulk review ───────────────────────────────────────
788
+ if (params.use_ai) {
789
+ const reviewProvider = getLLMProvider();
790
+ if (!reviewProvider.isAvailable()) {
791
+ return err("NO_AI_KEY", "AI provider not available. Set an API key in .env to use AI review.");
792
+ }
793
+ const reviewSysInstr = `You are a knowledge graph curator. Given two knowledge blocks and a proposed relation type, decide if the relation is accurate.
794
+
795
+ Valid relation types: implements, enables, depends_on, affects, describes, conflicts_with, replaces, related_to, part_of
796
+
797
+ Respond with EXACTLY one of:
798
+ - "approve" — relation type is correct
799
+ - "reject" — no meaningful relation between these blocks
800
+ - "change:NEW_TYPE" — relation exists but type is wrong (e.g. "change:enables")
801
+
802
+ Nothing else. One word or one phrase.`;
803
+ const pending = db.getPendingRelations();
804
+ for (const r of pending) {
805
+ const prompt = `${reviewSysInstr}\n\nBlock A: [${r.source_label}] "${r.source_label}"
806
+ Block B: [${r.target_label}] "${r.target_label}"
807
+ Proposed relation (A → B): ${r.type}`;
808
+ try {
809
+ const reviewText = await reviewProvider.generate(prompt);
810
+ const answer = (reviewText ?? "").trim().toLowerCase();
811
+ if (answer === "approve") {
812
+ db.approveRelation(r.id);
813
+ approved.push(r.id);
814
+ }
815
+ else if (answer === "reject") {
816
+ db.rejectRelation(r.id);
817
+ rejected.push(r.id);
818
+ }
819
+ else if (answer.startsWith("change:")) {
820
+ const newType = answer.replace("change:", "").trim().replace(/[^a-z_]/g, "");
821
+ if (newType) {
822
+ // Update type then approve
823
+ db["db"].prepare(`UPDATE relations SET type = ?, status = 'active' WHERE id = ?`).run(newType, r.id);
824
+ corrected.push({ id: r.id, old_type: r.type, new_type: newType });
825
+ approved.push(r.id);
826
+ }
827
+ else {
828
+ db.approveRelation(r.id);
829
+ approved.push(r.id);
830
+ }
831
+ }
832
+ }
833
+ catch { /* skip on error */ }
834
+ }
835
+ }
836
+ const stillPending = db.getPendingRelations();
837
+ return ok({
838
+ approved: approved.length,
839
+ rejected: rejected.length,
840
+ corrected: corrected.length > 0 ? corrected : undefined,
841
+ still_pending: stillPending.length,
842
+ pending_relations: stillPending.map((r) => ({
843
+ id: r.id,
844
+ from: r.source_label,
845
+ type: r.type,
846
+ to: r.target_label,
847
+ inferred_by: r.created_by,
848
+ })),
849
+ hint: stillPending.length > 0
850
+ ? "Pass approve_ids/reject_ids to act manually, or use_ai:true for Gemini batch review."
851
+ : "No pending relations — graph is fully reviewed.",
852
+ });
853
+ }
854
+ catch (error) {
855
+ return err("REVIEW_PENDING_FAILED", String(error));
856
+ }
857
+ });
858
+ // ─── Tool: workspace_gaps ────────────────────────────────────────
859
+ server.tool("workspace_gaps", `Detect knowledge gaps and structural issues in the workspace.
860
+ Runs three passes:
861
+ 1. Orphan blocks — blocks with zero relations (isolated, under-connected)
862
+ 2. Task coverage — open tasks with no linked decision or constraint (committed work with no direction)
863
+ 3. Open questions — blocks of type 'question' that have no linked answer block
864
+ Use this to self-direct: find what's missing and what needs attention.`, {
865
+ project: z.string().optional().describe("Scope to a specific project (by name). Omit for workspace-wide."),
866
+ }, async (params) => {
867
+ try {
868
+ const allBlocks = db.getAllBlocks().filter((b) => b.status !== "archived");
869
+ const allRelations = db.getAllRelations(false);
870
+ // Build set of block IDs with at least one relation
871
+ const connectedIds = new Set();
872
+ for (const rel of allRelations) {
873
+ connectedIds.add(rel.source_id);
874
+ connectedIds.add(rel.target_id);
875
+ }
876
+ // Filter to project scope if given
877
+ let scopeBlocks = allBlocks;
878
+ if (params.project) {
879
+ const projectKey = params.project.toLowerCase();
880
+ const projectBlock = allBlocks.find((b) => b.type === "project" && b.label.toLowerCase() === projectKey);
881
+ if (projectBlock) {
882
+ const projectRelatedIds = new Set([
883
+ projectBlock.id,
884
+ ...allRelations
885
+ .filter((r) => r.source_id === projectBlock.id || r.target_id === projectBlock.id)
886
+ .map((r) => r.source_id === projectBlock.id ? r.target_id : r.source_id),
887
+ ]);
888
+ scopeBlocks = allBlocks.filter((b) => projectRelatedIds.has(b.id));
889
+ }
890
+ }
891
+ // ── Pass 1: Orphan blocks ─────────────────────────────────
892
+ const orphans = scopeBlocks
893
+ .filter((b) => b.type !== "project" &&
894
+ !connectedIds.has(b.id) &&
895
+ !b.label.startsWith("agent_session_") // session blocks are always unlinked by design
896
+ )
897
+ .map((b) => ({ id: b.id, label: b.label, type: b.type, essence: b.essence, created_at: b.created_at }));
898
+ // ── Pass 2: Task coverage ─────────────────────────────────
899
+ const tasks = scopeBlocks.filter((b) => b.type === "task");
900
+ const taskGaps = [];
901
+ for (const task of tasks) {
902
+ try {
903
+ const c = JSON.parse(task.content);
904
+ const status = c?.unique?.status || c?.status || "";
905
+ if (["done", "completed", "archived"].includes(String(status).toLowerCase()))
906
+ continue;
907
+ }
908
+ catch { /* check anyway */ }
909
+ const taskRels = allRelations.filter((r) => r.source_id === task.id || r.target_id === task.id);
910
+ const linkedTypes = new Set(taskRels.map((r) => {
911
+ const otherId = r.source_id === task.id ? r.target_id : r.source_id;
912
+ const other = allBlocks.find((b) => b.id === otherId);
913
+ return other?.type;
914
+ }));
915
+ const missing = [];
916
+ if (!linkedTypes.has("decision"))
917
+ missing.push("decision");
918
+ if (!linkedTypes.has("constraint"))
919
+ missing.push("constraint");
920
+ if (missing.length > 0) {
921
+ taskGaps.push({ id: task.id, label: task.label, essence: task.essence, missing });
922
+ }
923
+ }
924
+ // ── Pass 3: Open questions ────────────────────────────────
925
+ const questions = scopeBlocks.filter((b) => b.type === "question");
926
+ const openQuestions = [];
927
+ for (const q of questions) {
928
+ const qRels = allRelations.filter((r) => r.source_id === q.id || r.target_id === q.id);
929
+ const hasAnswer = qRels.some((r) => {
930
+ const otherId = r.source_id === q.id ? r.target_id : r.source_id;
931
+ const other = allBlocks.find((b) => b.id === otherId);
932
+ return other && ["fact", "decision", "answer"].includes(other.type);
933
+ });
934
+ if (!hasAnswer) {
935
+ openQuestions.push({ id: q.id, label: q.label, essence: q.essence });
936
+ }
937
+ }
938
+ // ── Pass 4: Open near-duplicate conflicts ─────────────────
939
+ const openConflicts = db.getOpenConflicts();
940
+ // ── Pass 5: Unlinked dead ends ────────────────────────────
941
+ // Dead end blocks that have no `contradicts` relation — the failure was
942
+ // captured but never linked to what it conflicted with, making it harder
943
+ // to navigate to from the thing it negates.
944
+ const deadEnds = scopeBlocks.filter((b) => b.type === "dead_end");
945
+ const unlinkedDeadEnds = deadEnds.filter((b) => {
946
+ const rels = allRelations.filter((r) => r.source_id === b.id || r.target_id === b.id);
947
+ return !rels.some((r) => r.type === "contradicts");
948
+ }).map((b) => ({ id: b.id, label: b.label, essence: b.essence,
949
+ hint: "Add a `contradicts` relation linking this to the block it conflicts with" }));
950
+ // ── Pass 6: Drafts never promoted ────────────────────────
951
+ // Draft blocks older than 7 days that haven't been promoted to fact/decision
952
+ const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
953
+ const staleDrafts = scopeBlocks.filter((b) => {
954
+ if (b.type !== "draft")
955
+ return false;
956
+ const c = (() => { try {
957
+ return JSON.parse(b.content);
958
+ }
959
+ catch {
960
+ return {};
961
+ } })();
962
+ const draftStatus = c?.unique?.draft_status;
963
+ return draftStatus !== "promoted" && b.created_at < sevenDaysAgo;
964
+ }).map((b) => ({ id: b.id, label: b.label, essence: b.essence, created_at: b.created_at,
965
+ hint: "Promote to fact/decision or archive if no longer needed" }));
966
+ // ── Pass 7: next_questions from project blocks ─────────────
967
+ const nextQuestions = [];
968
+ for (const b of scopeBlocks) {
969
+ if (b.type !== "project")
970
+ continue;
971
+ try {
972
+ const c = JSON.parse(b.content);
973
+ const nq = c?.next_questions || c?.has?.next_questions || [];
974
+ for (const q of nq)
975
+ nextQuestions.push({ project: b.label, question: q });
976
+ }
977
+ catch { /* skip */ }
978
+ }
979
+ const totalGaps = orphans.length + taskGaps.length + openQuestions.length +
980
+ openConflicts.length + unlinkedDeadEnds.length + staleDrafts.length;
981
+ return ok({
982
+ project: params.project ?? "all",
983
+ scoped_blocks: scopeBlocks.length,
984
+ gaps: {
985
+ orphan_blocks: { count: orphans.length, blocks: orphans },
986
+ task_coverage: { count: taskGaps.length, tasks: taskGaps },
987
+ open_questions: { count: openQuestions.length, questions: openQuestions },
988
+ open_conflicts: { count: openConflicts.length, conflicts: openConflicts },
989
+ unlinked_dead_ends: { count: unlinkedDeadEnds.length, dead_ends: unlinkedDeadEnds },
990
+ stale_drafts: { count: staleDrafts.length, drafts: staleDrafts },
991
+ next_questions: { count: nextQuestions.length, items: nextQuestions },
992
+ },
993
+ summary: totalGaps === 0
994
+ ? `No gaps detected.${nextQuestions.length ? ` ${nextQuestions.length} next question(s) queued from project blocks.` : " Knowledge graph looks healthy."}`
995
+ : `Found ${orphans.length} orphans, ${taskGaps.length} under-specified tasks, ${openQuestions.length} unanswered questions, ${openConflicts.length} duplicate conflicts, ${unlinkedDeadEnds.length} unlinked dead ends, ${staleDrafts.length} stale drafts.${nextQuestions.length ? ` ${nextQuestions.length} next question(s) from projects.` : ""}`,
996
+ });
997
+ }
998
+ catch (error) {
999
+ return err("GAPS_FAILED", String(error));
1000
+ }
1001
+ });
1002
+ // ─── Tool: workspace_resolve_conflict ────────────────────────────
1003
+ server.tool("workspace_resolve_conflict", `Resolve a near-duplicate conflict between two blocks.
1004
+ Use workspace_gaps() to find open conflicts first.
1005
+
1006
+ Resolution options:
1007
+ - keep_a: archive block_b, keep block_a as-is
1008
+ - keep_b: archive block_a, keep block_b as-is
1009
+ - merge: write a combined essence to block_a, archive block_b`, {
1010
+ conflict_id: z.string().describe("Conflict ID from workspace_gaps() output"),
1011
+ resolution: z.enum(["keep_a", "keep_b", "merge"]).describe("How to resolve: keep_a, keep_b, or merge"),
1012
+ merged_essence: z.string().optional().describe("Required if resolution is 'merge' — the combined essence for the surviving block"),
1013
+ reason: z.string().optional().describe("Why you chose this resolution"),
1014
+ }, async (params) => {
1015
+ try {
1016
+ const conflicts = db.getOpenConflicts();
1017
+ const conflict = conflicts.find((c) => c.id === params.conflict_id);
1018
+ if (!conflict) {
1019
+ return err("CONFLICT_NOT_FOUND", `Conflict '${params.conflict_id}' not found or already resolved. Use workspace_gaps() to see open conflicts.`);
1020
+ }
1021
+ const blockA = db.getBlock(conflict.block_a.id);
1022
+ const blockB = db.getBlock(conflict.block_b.id);
1023
+ if (!blockA || !blockB) {
1024
+ return err("BLOCK_NOT_FOUND", "One or both blocks in this conflict no longer exist.");
1025
+ }
1026
+ let keptId;
1027
+ let archivedId;
1028
+ if (params.resolution === "keep_a") {
1029
+ keptId = blockA.id;
1030
+ archivedId = blockB.id;
1031
+ db.archiveBlock(blockB.id, `resolved near-duplicate conflict ${params.conflict_id}: kept ${blockA.label}`);
1032
+ }
1033
+ else if (params.resolution === "keep_b") {
1034
+ keptId = blockB.id;
1035
+ archivedId = blockA.id;
1036
+ db.archiveBlock(blockA.id, `resolved near-duplicate conflict ${params.conflict_id}: kept ${blockB.label}`);
1037
+ }
1038
+ else {
1039
+ // merge
1040
+ if (!params.merged_essence) {
1041
+ return err("MERGE_REQUIRES_ESSENCE", "Resolution 'merge' requires a merged_essence string.");
1042
+ }
1043
+ keptId = blockA.id;
1044
+ archivedId = blockB.id;
1045
+ db.updateBlock(blockA.id, { essence: params.merged_essence }, `merged with ${blockB.label} (conflict ${params.conflict_id})`, undefined, true);
1046
+ db.archiveBlock(blockB.id, `merged into ${blockA.label} via conflict ${params.conflict_id}`);
1047
+ }
1048
+ db.resolveConflict(params.conflict_id, `${params.resolution}${params.reason ? `: ${params.reason}` : ""}`);
1049
+ return ok({
1050
+ resolved: true,
1051
+ conflict_id: params.conflict_id,
1052
+ resolution: params.resolution,
1053
+ kept_block: keptId,
1054
+ archived_block: archivedId,
1055
+ hint: params.resolution === "merge"
1056
+ ? `Blocks merged. Run workspace_infer_relations() to update graph links.`
1057
+ : `Conflict resolved. Archived block is preserved in history.`,
1058
+ });
1059
+ }
1060
+ catch (error) {
1061
+ return err("RESOLVE_FAILED", String(error));
1062
+ }
1063
+ });
1064
+ // ─── Tool: workspace_review ──────────────────────────────────────
1065
+ // Agent↔Gemini dialog: ask Gemini to review a block and suggest improvements.
1066
+ // Gemini's response is stored as gemini_suggestions in the block AND as a linked note.
1067
+ // The agent stays in control — suggestions must be explicitly accepted via workspace_update.
1068
+ server.tool("workspace_review", `Ask Gemini to review a block and suggest structural improvements.
1069
+
1070
+ This is the agent↔Gemini dialogue tool. Gemini analyzes the block and returns:
1071
+ - Suggested is_a (parent category)
1072
+ - Suggested unique{} properties (what makes it distinctive)
1073
+ - Suggested has{} content (examples, extensions, caveats)
1074
+ - Better concepts (domain-agnostic patterns)
1075
+ - Essence rewrite if the current one is vague
1076
+ - Conflict check against common knowledge
1077
+
1078
+ Gemini's suggestions are stored on the block as gemini_suggestions{} so future sessions can see them.
1079
+ The agent decides what to accept — call workspace_update() to promote any suggestion to canonical data.
1080
+ This keeps agent authority intact: Gemini advises, agent decides.`, {
1081
+ id: z.string().describe("Block ID or label to review"),
1082
+ question: z.string().optional().describe("Specific question for Gemini (e.g. 'Is my is_a classification right?' or 'What properties am I missing?'). Default: general quality review."),
1083
+ save_response: z.boolean().optional().describe("Save Gemini's response as a linked note block for future recall. Default: true"),
1084
+ }, async (params) => {
1085
+ try {
1086
+ const reviewProvider = getLLMProvider();
1087
+ if (!reviewProvider.isAvailable())
1088
+ return err("NO_AI_KEY", "AI provider not available — check your API key in .env");
1089
+ const block = db.getBlock(params.id);
1090
+ if (!block)
1091
+ return err("BLOCK_NOT_FOUND", `Block '${params.id}' not found`);
1092
+ const content = (() => { try {
1093
+ return JSON.parse(block.content);
1094
+ }
1095
+ catch {
1096
+ return {};
1097
+ } })();
1098
+ const concepts = (() => { try {
1099
+ return JSON.parse(block.concepts || "[]");
1100
+ }
1101
+ catch {
1102
+ return [];
1103
+ } })();
1104
+ const prompt = `You are a knowledge quality reviewer for a semantic knowledge graph.
1105
+ Review this knowledge block and return structured suggestions to improve it.
1106
+
1107
+ BLOCK:
1108
+ - label: ${block.label}
1109
+ - type: ${block.type}
1110
+ - essence: ${block.essence}
1111
+ - is_a: ${content.is_a || "NOT SET"}
1112
+ - unique properties: ${content.unique ? JSON.stringify(content.unique) : "NONE"}
1113
+ - has: ${content.has ? JSON.stringify(content.has) : "NONE"}
1114
+ - concepts: ${concepts.join(", ") || "NONE"}
1115
+
1116
+ AGENT QUESTION: ${params.question || "General review — what is this block missing to be maximally useful for cross-domain recall and reasoning?"}
1117
+
1118
+ Return ONLY valid JSON with this exact shape:
1119
+ {
1120
+ "is_a": "suggested parent category string",
1121
+ "unique": { "key": "value" },
1122
+ "has": { "key": "value" },
1123
+ "concepts": ["abstract1", "abstract2", "abstract3"],
1124
+ "essence_improvement": "improved one-liner or null if current is good",
1125
+ "quality_verdict": "thin|acceptable|rich",
1126
+ "quality_reasoning": "one sentence explaining the verdict",
1127
+ "conflict_check": "any factual concerns or null"
1128
+ }`;
1129
+ const reviewText = await reviewProvider.generate(prompt);
1130
+ const raw = (reviewText ?? "").replace(/```json\n?/g, "").replace(/```\n?/g, "").trim();
1131
+ let suggestions = {};
1132
+ try {
1133
+ suggestions = JSON.parse(raw);
1134
+ }
1135
+ catch {
1136
+ return err("AI_PARSE_ERROR", `AI provider returned non-JSON: ${raw.slice(0, 200)}`);
1137
+ }
1138
+ // Store suggestions on the block (not overwriting canonical fields)
1139
+ const updatedContent = {
1140
+ ...content,
1141
+ gemini_suggestions: {
1142
+ ...suggestions,
1143
+ reviewed_at: new Date().toISOString(),
1144
+ question: params.question || "general review",
1145
+ },
1146
+ };
1147
+ db.updateBlock(block.id, { content: JSON.stringify(updatedContent) }, `AI review stored as suggestions — agent must accept via workspace_update`, "ai_review");
1148
+ // Optionally save as a linked note block for future recall
1149
+ let reviewBlockId = null;
1150
+ if (params.save_response !== false) {
1151
+ const reviewBlock = db.createBlock({
1152
+ label: `gemini_review_${block.label}`.slice(0, 60),
1153
+ type: "note",
1154
+ essence: `Gemini review of '${block.label}': ${suggestions.quality_verdict || "reviewed"} — ${suggestions.quality_reasoning || ""}`,
1155
+ content: { review_of: block.id, suggestions },
1156
+ concepts: concepts.slice(0, 4),
1157
+ ttl: "permanent",
1158
+ });
1159
+ db.createRelation({ source_id: reviewBlock.id, target_id: block.id, type: "review_of", created_by: "gemini" });
1160
+ reviewBlockId = reviewBlock.id;
1161
+ }
1162
+ return ok({
1163
+ block_id: block.id,
1164
+ block_label: block.label,
1165
+ quality_verdict: suggestions.quality_verdict,
1166
+ quality_reasoning: suggestions.quality_reasoning,
1167
+ suggestions: {
1168
+ is_a: suggestions.is_a,
1169
+ unique: suggestions.unique,
1170
+ has: suggestions.has,
1171
+ concepts: suggestions.concepts,
1172
+ essence_improvement: suggestions.essence_improvement,
1173
+ conflict_check: suggestions.conflict_check,
1174
+ },
1175
+ review_block_id: reviewBlockId,
1176
+ next_step: `To accept suggestions: workspace_update("${block.id}", { is_a: "...", unique: {...}, concepts: [...] }). Suggestions also stored in block as gemini_suggestions{}.`,
1177
+ });
1178
+ }
1179
+ catch (error) {
1180
+ return err("REVIEW_FAILED", String(error));
1181
+ }
1182
+ });
1183
+ // ─── Tool: workspace_enrich ──────────────────────────────────────
1184
+ server.tool("workspace_enrich", `Batch-upgrade keyword-only concepts to abstract semantic patterns using Gemini Flash.
1185
+ Targets blocks where concepts were auto-extracted (keyword_auto source) or have ≤3 concepts.
1186
+ Run this in the background after a session to improve search quality over time.
1187
+ Returns per-block results: old concepts → new concepts.`, {
1188
+ limit: z.number().optional().describe("Max blocks to enrich in this batch (default: 20)"),
1189
+ block_ids: z.array(z.string()).optional().describe("Specific block IDs to enrich (overrides auto-selection)"),
1190
+ }, async (params) => {
1191
+ try {
1192
+ const enrichProvider = getLLMProvider();
1193
+ if (!enrichProvider.isAvailable())
1194
+ return err("NO_AI_KEY", "AI provider not available — check your API key in .env");
1195
+ // Select targets: blocks with keyword_auto concepts or ≤3 concepts
1196
+ let targets;
1197
+ if (params.block_ids && params.block_ids.length > 0) {
1198
+ targets = params.block_ids
1199
+ .map((id) => db.getBlock(id))
1200
+ .filter(Boolean);
1201
+ }
1202
+ else {
1203
+ const allBlocks = db.getAllBlocks();
1204
+ targets = allBlocks.filter((b) => {
1205
+ try {
1206
+ const content = JSON.parse(b.content);
1207
+ // Skip already-merged blocks; target keyword_auto, gemini_reflect, or sparse concepts
1208
+ if (content.concepts_source === "merged")
1209
+ return false;
1210
+ const concepts = JSON.parse(b.concepts || "[]");
1211
+ return content.concepts_source === "keyword_auto"
1212
+ || content.concepts_source === "gemini_reflect"
1213
+ || concepts.length <= 3;
1214
+ }
1215
+ catch {
1216
+ return false;
1217
+ }
1218
+ }).slice(0, params.limit ?? 20);
1219
+ }
1220
+ if (targets.length === 0) {
1221
+ return ok({ enriched: 0, message: "No blocks need enrichment." });
1222
+ }
1223
+ const enrichSysInstr = `You are a knowledge graph concept extractor.
1224
+ Given a block's label, type, and essence, return 3-6 abstract conceptual tags.
1225
+ Tags should be ABSTRACT PATTERNS (e.g., "iterative_refinement", "context_switching", "trust_signal")
1226
+ NOT literal keywords from the text (e.g., not "workflow" from "workflow optimization").
1227
+ Respond ONLY with a JSON array of strings, no explanation.`;
1228
+ const results = [];
1229
+ for (const block of targets) {
1230
+ try {
1231
+ let content = {};
1232
+ try {
1233
+ content = JSON.parse(block.content);
1234
+ }
1235
+ catch { /* skip */ }
1236
+ // Read from first-class concepts column first; fall back to content JSON
1237
+ const oldConcepts = (() => {
1238
+ try {
1239
+ return JSON.parse(block.concepts || "[]");
1240
+ }
1241
+ catch {
1242
+ return [];
1243
+ }
1244
+ })();
1245
+ const prompt = `${enrichSysInstr}
1246
+
1247
+ Block label: "${block.label}"
1248
+ Type: ${block.type}
1249
+ Essence: "${block.essence}"
1250
+ Agent concepts (concrete, domain-specific — DO NOT remove these): ${JSON.stringify(oldConcepts)}
1251
+
1252
+ Your job: add 3-5 ABSTRACT cross-domain pattern tags that complement the agent's concepts.
1253
+ These should capture the underlying patterns so this block surfaces in unexpected but relevant searches.
1254
+ Example: agent has ["glymphatic","amyloid","alzheimers"] → you add ["waste_clearance","nocturnal_maintenance","neurodegeneration_risk"]
1255
+ Do NOT repeat what the agent already has. Do NOT replace — only ADD.
1256
+
1257
+ Return ONLY a JSON array of your additional abstract tags (not the full merged list).`;
1258
+ const enrichText = await enrichProvider.generate(prompt);
1259
+ const raw = (enrichText ?? "").trim().replace(/```json\n?|```/g, "");
1260
+ const aiConcepts = JSON.parse(raw);
1261
+ if (Array.isArray(aiConcepts) && aiConcepts.length > 0) {
1262
+ // MERGE: agent concepts (concrete) + AI concepts (abstract) — never replace
1263
+ const merged = [...new Set([...oldConcepts, ...aiConcepts])].slice(0, 12);
1264
+ content.agent_concepts = oldConcepts; // preserve originals
1265
+ content.gemini_concepts = aiConcepts; // AI's additions (field name kept for compat)
1266
+ content.concepts = merged; // full merged set for search
1267
+ content.concepts_source = "merged";
1268
+ db.updateBlock(block.id, {
1269
+ content: JSON.stringify(content),
1270
+ concepts: merged, // update first-class column too
1271
+ }, "concepts enriched by workspace_enrich (merged)", "system");
1272
+ // Re-embed with full merged concept set for better semantic coverage
1273
+ if (embeddings.isAvailable()) {
1274
+ const embText = blockEmbeddingText({ essence: block.essence, concepts: merged });
1275
+ const vec = await embeddings.embed(embText);
1276
+ if (vec)
1277
+ db.updateEmbedding(block.id, vec);
1278
+ }
1279
+ results.push({ id: block.id, label: block.label, old_concepts: oldConcepts, new_concepts: merged, status: "enriched" });
1280
+ }
1281
+ else {
1282
+ results.push({ id: block.id, label: block.label, old_concepts: oldConcepts, new_concepts: oldConcepts, status: "unchanged" });
1283
+ }
1284
+ }
1285
+ catch (blockErr) {
1286
+ results.push({ id: block.id, label: block.label, old_concepts: [], new_concepts: [], status: `error: ${String(blockErr)}` });
1287
+ }
1288
+ }
1289
+ const enrichedCount = results.filter((r) => r.status === "enriched").length;
1290
+ return ok({
1291
+ enriched: enrichedCount,
1292
+ total_processed: results.length,
1293
+ results,
1294
+ });
1295
+ }
1296
+ catch (error) {
1297
+ return err("ENRICH_FAILED", String(error));
1298
+ }
1299
+ });
1300
+ // ─── Tool: workspace_extract_arc (DEBT 5 Phase 7) ────────────────────────
1301
+ // Trigger arc extraction over a range of pass01_done turns for an agent.
1302
+ // Per design §3.1 trigger model. Composes with the same backend
1303
+ // (runArcExtraction) as the phase tag detector and /api/conversations/.../
1304
+ // extract endpoint — different invocation paths, same downstream effect.
1305
+ //
1306
+ // Naming per inventory §10: workspace_extract_arc — verb stem `extract` is
1307
+ // unused in the existing tool family; fits system.ts module.
1308
+ server.tool("workspace_extract_arc", `Trigger arc extraction over a range of conversation turns for an agent.
1309
+ Reads pass01_done turns (Phase 2 captured Pass 0-1 outputs), consolidates
1310
+ them into a single arc input (D1 raw transcripts + D4 sew-as-event framing),
1311
+ runs Pass 2-5, writes blocks with line-level provenance (D3 source_excerpt
1312
+ column), creates a conversation_turn_ranges row, and flips the turns to
1313
+ 'extracted' status.
1314
+
1315
+ Defaults: if start_turn / end_turn are omitted, extracts ALL pass01_done turns
1316
+ for the agent. If re_extract is true, creates a 're-extract' range (vs 'arc')
1317
+ so the second extraction event is auditable as intentional.
1318
+
1319
+ Requires NODEDEX_ARC_EXTRACTION=1 in env for per-turn capture to populate the
1320
+ table (otherwise no pass01_done turns will exist and this returns 'no_turns').`, {
1321
+ agent_id: z.string().describe("A stable identifier for the agent/session (e.g. the x-nodedex-agent-id request header, or your host's session id)"),
1322
+ start_turn: z.number().int().positive().optional().describe("Turn number to start from (default: first pass01_done turn)"),
1323
+ end_turn: z.number().int().positive().optional().describe("Turn number to end at, inclusive (default: latest pass01_done turn)"),
1324
+ re_extract: z.boolean().optional().describe("When true, marks the range as 're-extract' (vs 'arc'). Default false."),
1325
+ }, async (params) => {
1326
+ try {
1327
+ if (params.start_turn !== undefined && params.end_turn !== undefined && params.end_turn < params.start_turn) {
1328
+ return err("INVALID_RANGE", `end_turn (${params.end_turn}) cannot be less than start_turn (${params.start_turn})`);
1329
+ }
1330
+ const result = await runArcExtraction(db, {
1331
+ agent_id: params.agent_id,
1332
+ start_turn: params.start_turn,
1333
+ end_turn: params.end_turn,
1334
+ re_extract: params.re_extract === true,
1335
+ trigger_source: "mcp_tool",
1336
+ });
1337
+ if (result.status === "no_turns") {
1338
+ return err("NO_TURNS", `No pass01_done turns found for agent ${params.agent_id} in range. Ensure NODEDEX_ARC_EXTRACTION=1 is set so per-turn capture populates conversation_turns.`);
1339
+ }
1340
+ if (result.status === "pipeline_failed") {
1341
+ return err("PIPELINE_FAILED", result.error ?? "arc extraction pipeline failed");
1342
+ }
1343
+ if (result.status === "pipeline_incomplete") {
1344
+ return err("PIPELINE_INCOMPLETE", result.error ?? "arc extraction incomplete after retries — turns left re-extractable, retry later");
1345
+ }
1346
+ return ok({
1347
+ range_id: result.range_id,
1348
+ turns_consumed: result.turns_consumed,
1349
+ start_turn: result.start_turn,
1350
+ end_turn: result.end_turn,
1351
+ saved_blocks: result.reflect_result?.saved ?? 0,
1352
+ updated_blocks: result.reflect_result?.updated ?? 0,
1353
+ saved_labels: result.reflect_result?.saved_labels ?? [],
1354
+ });
1355
+ }
1356
+ catch (error) {
1357
+ return err("EXTRACT_ARC_FAILED", String(error));
1358
+ }
1359
+ });
1360
+ }
1361
+ //# sourceMappingURL=system.js.map