bikky 0.4.0 → 0.4.2

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 (342) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/CODE_OF_CONDUCT.md +80 -0
  3. package/CONTRIBUTING.md +2 -2
  4. package/README.md +48 -14
  5. package/SECURITY.md +58 -0
  6. package/SUPPORT.md +23 -0
  7. package/dist/config.d.ts +19 -0
  8. package/dist/config.js +72 -0
  9. package/dist/daemon/extraction.d.ts +12 -2
  10. package/dist/daemon/extraction.js +85 -133
  11. package/dist/daemon/transcript-sources.d.ts +26 -0
  12. package/dist/daemon/transcript-sources.js +193 -0
  13. package/dist/daemon/watcher.d.ts +3 -2
  14. package/dist/daemon/watcher.js +51 -2
  15. package/dist/install.d.ts +9 -1
  16. package/dist/install.js +62 -34
  17. package/dist/mcp/api.d.ts +4 -3
  18. package/dist/mcp/api.js +4 -3
  19. package/dist/mcp/tools.js +317 -93
  20. package/dist/search-scope.d.ts +24 -0
  21. package/dist/search-scope.js +174 -0
  22. package/docs/config/fully-hosted.md +1 -1
  23. package/docs/config/hosted-models.md +1 -1
  24. package/docs/configuration.md +34 -5
  25. package/docs/privacy-first.md +140 -0
  26. package/package.json +27 -5
  27. package/dist/cli.d.ts.map +0 -1
  28. package/dist/cli.js.map +0 -1
  29. package/dist/config.d.ts.map +0 -1
  30. package/dist/config.js.map +0 -1
  31. package/dist/config.test.d.ts +0 -9
  32. package/dist/config.test.d.ts.map +0 -1
  33. package/dist/config.test.js +0 -576
  34. package/dist/config.test.js.map +0 -1
  35. package/dist/daemon/capture-policy.d.ts.map +0 -1
  36. package/dist/daemon/capture-policy.js.map +0 -1
  37. package/dist/daemon/capture-policy.test.d.ts +0 -2
  38. package/dist/daemon/capture-policy.test.d.ts.map +0 -1
  39. package/dist/daemon/capture-policy.test.js +0 -48
  40. package/dist/daemon/capture-policy.test.js.map +0 -1
  41. package/dist/daemon/consolidation.d.ts.map +0 -1
  42. package/dist/daemon/consolidation.js.map +0 -1
  43. package/dist/daemon/entity-typing.d.ts.map +0 -1
  44. package/dist/daemon/entity-typing.js.map +0 -1
  45. package/dist/daemon/entity-typing.test.d.ts +0 -2
  46. package/dist/daemon/entity-typing.test.d.ts.map +0 -1
  47. package/dist/daemon/entity-typing.test.js +0 -50
  48. package/dist/daemon/entity-typing.test.js.map +0 -1
  49. package/dist/daemon/episode-summary.d.ts.map +0 -1
  50. package/dist/daemon/episode-summary.js.map +0 -1
  51. package/dist/daemon/episode-summary.test.d.ts +0 -2
  52. package/dist/daemon/episode-summary.test.d.ts.map +0 -1
  53. package/dist/daemon/episode-summary.test.js +0 -104
  54. package/dist/daemon/episode-summary.test.js.map +0 -1
  55. package/dist/daemon/extraction-quality.test.d.ts +0 -2
  56. package/dist/daemon/extraction-quality.test.d.ts.map +0 -1
  57. package/dist/daemon/extraction-quality.test.js +0 -283
  58. package/dist/daemon/extraction-quality.test.js.map +0 -1
  59. package/dist/daemon/extraction-rules.d.ts.map +0 -1
  60. package/dist/daemon/extraction-rules.js.map +0 -1
  61. package/dist/daemon/extraction-rules.test.d.ts +0 -2
  62. package/dist/daemon/extraction-rules.test.d.ts.map +0 -1
  63. package/dist/daemon/extraction-rules.test.js +0 -203
  64. package/dist/daemon/extraction-rules.test.js.map +0 -1
  65. package/dist/daemon/extraction.d.ts.map +0 -1
  66. package/dist/daemon/extraction.js.map +0 -1
  67. package/dist/daemon/extraction.test.d.ts +0 -2
  68. package/dist/daemon/extraction.test.d.ts.map +0 -1
  69. package/dist/daemon/extraction.test.js +0 -225
  70. package/dist/daemon/extraction.test.js.map +0 -1
  71. package/dist/daemon/index.d.ts.map +0 -1
  72. package/dist/daemon/index.js.map +0 -1
  73. package/dist/daemon/loop.d.ts.map +0 -1
  74. package/dist/daemon/loop.js.map +0 -1
  75. package/dist/daemon/loop.test.d.ts +0 -2
  76. package/dist/daemon/loop.test.d.ts.map +0 -1
  77. package/dist/daemon/loop.test.js +0 -85
  78. package/dist/daemon/loop.test.js.map +0 -1
  79. package/dist/daemon/maintenance-state.d.ts.map +0 -1
  80. package/dist/daemon/maintenance-state.js.map +0 -1
  81. package/dist/daemon/maintenance-state.test.d.ts +0 -2
  82. package/dist/daemon/maintenance-state.test.d.ts.map +0 -1
  83. package/dist/daemon/maintenance-state.test.js +0 -56
  84. package/dist/daemon/maintenance-state.test.js.map +0 -1
  85. package/dist/daemon/qdrant.d.ts.map +0 -1
  86. package/dist/daemon/qdrant.js.map +0 -1
  87. package/dist/daemon/qdrant.test.d.ts +0 -8
  88. package/dist/daemon/qdrant.test.d.ts.map +0 -1
  89. package/dist/daemon/qdrant.test.js +0 -265
  90. package/dist/daemon/qdrant.test.js.map +0 -1
  91. package/dist/daemon/relations-vocab.d.ts.map +0 -1
  92. package/dist/daemon/relations-vocab.js.map +0 -1
  93. package/dist/daemon/relations-vocab.test.d.ts +0 -2
  94. package/dist/daemon/relations-vocab.test.d.ts.map +0 -1
  95. package/dist/daemon/relations-vocab.test.js +0 -69
  96. package/dist/daemon/relations-vocab.test.js.map +0 -1
  97. package/dist/daemon/relations.d.ts.map +0 -1
  98. package/dist/daemon/relations.js.map +0 -1
  99. package/dist/daemon/relations.test.d.ts +0 -2
  100. package/dist/daemon/relations.test.d.ts.map +0 -1
  101. package/dist/daemon/relations.test.js +0 -36
  102. package/dist/daemon/relations.test.js.map +0 -1
  103. package/dist/daemon/session-index.d.ts.map +0 -1
  104. package/dist/daemon/session-index.js.map +0 -1
  105. package/dist/daemon/session-index.test.d.ts +0 -2
  106. package/dist/daemon/session-index.test.d.ts.map +0 -1
  107. package/dist/daemon/session-index.test.js +0 -60
  108. package/dist/daemon/session-index.test.js.map +0 -1
  109. package/dist/daemon/session-summary.d.ts.map +0 -1
  110. package/dist/daemon/session-summary.js.map +0 -1
  111. package/dist/daemon/session-summary.test.d.ts +0 -2
  112. package/dist/daemon/session-summary.test.d.ts.map +0 -1
  113. package/dist/daemon/session-summary.test.js +0 -162
  114. package/dist/daemon/session-summary.test.js.map +0 -1
  115. package/dist/daemon/staleness.d.ts.map +0 -1
  116. package/dist/daemon/staleness.js.map +0 -1
  117. package/dist/daemon/staleness.test.d.ts +0 -7
  118. package/dist/daemon/staleness.test.d.ts.map +0 -1
  119. package/dist/daemon/staleness.test.js +0 -128
  120. package/dist/daemon/staleness.test.js.map +0 -1
  121. package/dist/daemon/watcher-health.d.ts.map +0 -1
  122. package/dist/daemon/watcher-health.js.map +0 -1
  123. package/dist/daemon/watcher-health.test.d.ts +0 -5
  124. package/dist/daemon/watcher-health.test.d.ts.map +0 -1
  125. package/dist/daemon/watcher-health.test.js +0 -119
  126. package/dist/daemon/watcher-health.test.js.map +0 -1
  127. package/dist/daemon/watcher.d.ts.map +0 -1
  128. package/dist/daemon/watcher.js.map +0 -1
  129. package/dist/daemon/watcher.test.d.ts +0 -9
  130. package/dist/daemon/watcher.test.d.ts.map +0 -1
  131. package/dist/daemon/watcher.test.js +0 -204
  132. package/dist/daemon/watcher.test.js.map +0 -1
  133. package/dist/daemon/workstream-resolver.d.ts.map +0 -1
  134. package/dist/daemon/workstream-resolver.js.map +0 -1
  135. package/dist/daemon/workstream-resolver.test.d.ts +0 -2
  136. package/dist/daemon/workstream-resolver.test.d.ts.map +0 -1
  137. package/dist/daemon/workstream-resolver.test.js +0 -128
  138. package/dist/daemon/workstream-resolver.test.js.map +0 -1
  139. package/dist/daemon/workstream-summary.d.ts.map +0 -1
  140. package/dist/daemon/workstream-summary.js.map +0 -1
  141. package/dist/daemon/workstream-summary.test.d.ts +0 -2
  142. package/dist/daemon/workstream-summary.test.d.ts.map +0 -1
  143. package/dist/daemon/workstream-summary.test.js +0 -89
  144. package/dist/daemon/workstream-summary.test.js.map +0 -1
  145. package/dist/install.d.ts.map +0 -1
  146. package/dist/install.js.map +0 -1
  147. package/dist/install.test.d.ts +0 -9
  148. package/dist/install.test.d.ts.map +0 -1
  149. package/dist/install.test.js +0 -126
  150. package/dist/install.test.js.map +0 -1
  151. package/dist/lib/qdrant-client.d.ts.map +0 -1
  152. package/dist/lib/qdrant-client.js.map +0 -1
  153. package/dist/lib/qdrant-client.test.d.ts +0 -8
  154. package/dist/lib/qdrant-client.test.d.ts.map +0 -1
  155. package/dist/lib/qdrant-client.test.js +0 -274
  156. package/dist/lib/qdrant-client.test.js.map +0 -1
  157. package/dist/lib/qdrant-pool.d.ts.map +0 -1
  158. package/dist/lib/qdrant-pool.js.map +0 -1
  159. package/dist/lifecycle.d.ts.map +0 -1
  160. package/dist/lifecycle.js.map +0 -1
  161. package/dist/lifecycle.test.d.ts +0 -8
  162. package/dist/lifecycle.test.d.ts.map +0 -1
  163. package/dist/lifecycle.test.js +0 -74
  164. package/dist/lifecycle.test.js.map +0 -1
  165. package/dist/llm/embedding/index.d.ts.map +0 -1
  166. package/dist/llm/embedding/index.js.map +0 -1
  167. package/dist/llm/embedding/index.test.d.ts +0 -8
  168. package/dist/llm/embedding/index.test.d.ts.map +0 -1
  169. package/dist/llm/embedding/index.test.js +0 -100
  170. package/dist/llm/embedding/index.test.js.map +0 -1
  171. package/dist/llm/embedding/providers/bedrock.d.ts.map +0 -1
  172. package/dist/llm/embedding/providers/bedrock.js.map +0 -1
  173. package/dist/llm/embedding/providers/bedrock.test.d.ts +0 -2
  174. package/dist/llm/embedding/providers/bedrock.test.d.ts.map +0 -1
  175. package/dist/llm/embedding/providers/bedrock.test.js +0 -24
  176. package/dist/llm/embedding/providers/bedrock.test.js.map +0 -1
  177. package/dist/llm/embedding/providers/index.d.ts.map +0 -1
  178. package/dist/llm/embedding/providers/index.js.map +0 -1
  179. package/dist/llm/embedding/providers/ollama.d.ts.map +0 -1
  180. package/dist/llm/embedding/providers/ollama.js.map +0 -1
  181. package/dist/llm/embedding/providers/ollama.test.d.ts +0 -2
  182. package/dist/llm/embedding/providers/ollama.test.d.ts.map +0 -1
  183. package/dist/llm/embedding/providers/ollama.test.js +0 -54
  184. package/dist/llm/embedding/providers/ollama.test.js.map +0 -1
  185. package/dist/llm/embedding/providers/openai.d.ts.map +0 -1
  186. package/dist/llm/embedding/providers/openai.js.map +0 -1
  187. package/dist/llm/embedding/providers/openai.test.d.ts +0 -2
  188. package/dist/llm/embedding/providers/openai.test.d.ts.map +0 -1
  189. package/dist/llm/embedding/providers/openai.test.js +0 -48
  190. package/dist/llm/embedding/providers/openai.test.js.map +0 -1
  191. package/dist/llm/embedding/providers/portkey.d.ts.map +0 -1
  192. package/dist/llm/embedding/providers/portkey.js.map +0 -1
  193. package/dist/llm/embedding/providers/portkey.test.d.ts +0 -2
  194. package/dist/llm/embedding/providers/portkey.test.d.ts.map +0 -1
  195. package/dist/llm/embedding/providers/portkey.test.js +0 -56
  196. package/dist/llm/embedding/providers/portkey.test.js.map +0 -1
  197. package/dist/llm/embedding/registry.d.ts.map +0 -1
  198. package/dist/llm/embedding/registry.js.map +0 -1
  199. package/dist/llm/embedding/registry.test.d.ts +0 -7
  200. package/dist/llm/embedding/registry.test.d.ts.map +0 -1
  201. package/dist/llm/embedding/registry.test.js +0 -68
  202. package/dist/llm/embedding/registry.test.js.map +0 -1
  203. package/dist/llm/embedding/types.d.ts.map +0 -1
  204. package/dist/llm/embedding/types.js.map +0 -1
  205. package/dist/llm/errors.d.ts.map +0 -1
  206. package/dist/llm/errors.js.map +0 -1
  207. package/dist/llm/errors.test.d.ts +0 -2
  208. package/dist/llm/errors.test.d.ts.map +0 -1
  209. package/dist/llm/errors.test.js +0 -103
  210. package/dist/llm/errors.test.js.map +0 -1
  211. package/dist/llm/fetch.d.ts.map +0 -1
  212. package/dist/llm/fetch.js.map +0 -1
  213. package/dist/llm/index.d.ts.map +0 -1
  214. package/dist/llm/index.js.map +0 -1
  215. package/dist/llm/inference/index.d.ts.map +0 -1
  216. package/dist/llm/inference/index.js.map +0 -1
  217. package/dist/llm/inference/index.test.d.ts +0 -6
  218. package/dist/llm/inference/index.test.d.ts.map +0 -1
  219. package/dist/llm/inference/index.test.js +0 -150
  220. package/dist/llm/inference/index.test.js.map +0 -1
  221. package/dist/llm/inference/providers/bedrock.d.ts.map +0 -1
  222. package/dist/llm/inference/providers/bedrock.js.map +0 -1
  223. package/dist/llm/inference/providers/bedrock.test.d.ts +0 -2
  224. package/dist/llm/inference/providers/bedrock.test.d.ts.map +0 -1
  225. package/dist/llm/inference/providers/bedrock.test.js +0 -68
  226. package/dist/llm/inference/providers/bedrock.test.js.map +0 -1
  227. package/dist/llm/inference/providers/index.d.ts.map +0 -1
  228. package/dist/llm/inference/providers/index.js.map +0 -1
  229. package/dist/llm/inference/providers/ollama.d.ts.map +0 -1
  230. package/dist/llm/inference/providers/ollama.js.map +0 -1
  231. package/dist/llm/inference/providers/ollama.test.d.ts +0 -2
  232. package/dist/llm/inference/providers/ollama.test.d.ts.map +0 -1
  233. package/dist/llm/inference/providers/ollama.test.js +0 -57
  234. package/dist/llm/inference/providers/ollama.test.js.map +0 -1
  235. package/dist/llm/inference/providers/openai.d.ts.map +0 -1
  236. package/dist/llm/inference/providers/openai.js.map +0 -1
  237. package/dist/llm/inference/providers/openai.test.d.ts +0 -2
  238. package/dist/llm/inference/providers/openai.test.d.ts.map +0 -1
  239. package/dist/llm/inference/providers/openai.test.js +0 -82
  240. package/dist/llm/inference/providers/openai.test.js.map +0 -1
  241. package/dist/llm/inference/providers/portkey.d.ts.map +0 -1
  242. package/dist/llm/inference/providers/portkey.js.map +0 -1
  243. package/dist/llm/inference/providers/portkey.test.d.ts +0 -2
  244. package/dist/llm/inference/providers/portkey.test.d.ts.map +0 -1
  245. package/dist/llm/inference/providers/portkey.test.js +0 -48
  246. package/dist/llm/inference/providers/portkey.test.js.map +0 -1
  247. package/dist/llm/inference/registry.d.ts.map +0 -1
  248. package/dist/llm/inference/registry.js.map +0 -1
  249. package/dist/llm/inference/registry.test.d.ts +0 -6
  250. package/dist/llm/inference/registry.test.d.ts.map +0 -1
  251. package/dist/llm/inference/registry.test.js +0 -63
  252. package/dist/llm/inference/registry.test.js.map +0 -1
  253. package/dist/llm/inference/types.d.ts.map +0 -1
  254. package/dist/llm/inference/types.js.map +0 -1
  255. package/dist/llm/telemetry.d.ts.map +0 -1
  256. package/dist/llm/telemetry.js.map +0 -1
  257. package/dist/llm/telemetry.test.d.ts +0 -5
  258. package/dist/llm/telemetry.test.d.ts.map +0 -1
  259. package/dist/llm/telemetry.test.js +0 -89
  260. package/dist/llm/telemetry.test.js.map +0 -1
  261. package/dist/llm/types.d.ts.map +0 -1
  262. package/dist/llm/types.js.map +0 -1
  263. package/dist/logger.d.ts.map +0 -1
  264. package/dist/logger.js.map +0 -1
  265. package/dist/logger.test.d.ts +0 -5
  266. package/dist/logger.test.d.ts.map +0 -1
  267. package/dist/logger.test.js +0 -103
  268. package/dist/logger.test.js.map +0 -1
  269. package/dist/mcp/api.d.ts.map +0 -1
  270. package/dist/mcp/api.js.map +0 -1
  271. package/dist/mcp/helpers.d.ts.map +0 -1
  272. package/dist/mcp/helpers.js.map +0 -1
  273. package/dist/mcp/helpers.test.d.ts +0 -5
  274. package/dist/mcp/helpers.test.d.ts.map +0 -1
  275. package/dist/mcp/helpers.test.js +0 -530
  276. package/dist/mcp/helpers.test.js.map +0 -1
  277. package/dist/mcp/index.d.ts.map +0 -1
  278. package/dist/mcp/index.js.map +0 -1
  279. package/dist/mcp/taxonomy.d.ts.map +0 -1
  280. package/dist/mcp/taxonomy.js.map +0 -1
  281. package/dist/mcp/taxonomy.test.d.ts +0 -5
  282. package/dist/mcp/taxonomy.test.d.ts.map +0 -1
  283. package/dist/mcp/taxonomy.test.js +0 -215
  284. package/dist/mcp/taxonomy.test.js.map +0 -1
  285. package/dist/mcp/tools.d.ts.map +0 -1
  286. package/dist/mcp/tools.js.map +0 -1
  287. package/dist/mcp/types.d.ts.map +0 -1
  288. package/dist/mcp/types.js.map +0 -1
  289. package/dist/postinstall.d.ts.map +0 -1
  290. package/dist/postinstall.js.map +0 -1
  291. package/dist/privacy/redaction.d.ts.map +0 -1
  292. package/dist/privacy/redaction.js.map +0 -1
  293. package/dist/privacy/redaction.test.d.ts +0 -2
  294. package/dist/privacy/redaction.test.d.ts.map +0 -1
  295. package/dist/privacy/redaction.test.js +0 -51
  296. package/dist/privacy/redaction.test.js.map +0 -1
  297. package/dist/prompts/brief.d.ts.map +0 -1
  298. package/dist/prompts/brief.js.map +0 -1
  299. package/dist/prompts/contradiction.d.ts.map +0 -1
  300. package/dist/prompts/contradiction.js.map +0 -1
  301. package/dist/prompts/distill.d.ts.map +0 -1
  302. package/dist/prompts/distill.js.map +0 -1
  303. package/dist/prompts/entity-typing.d.ts.map +0 -1
  304. package/dist/prompts/entity-typing.js.map +0 -1
  305. package/dist/prompts/episode-summary.d.ts.map +0 -1
  306. package/dist/prompts/episode-summary.js.map +0 -1
  307. package/dist/prompts/extraction.d.ts.map +0 -1
  308. package/dist/prompts/extraction.js.map +0 -1
  309. package/dist/prompts/index.d.ts.map +0 -1
  310. package/dist/prompts/index.js.map +0 -1
  311. package/dist/prompts/prompts.test.d.ts +0 -8
  312. package/dist/prompts/prompts.test.d.ts.map +0 -1
  313. package/dist/prompts/prompts.test.js +0 -140
  314. package/dist/prompts/prompts.test.js.map +0 -1
  315. package/dist/prompts/relations.d.ts.map +0 -1
  316. package/dist/prompts/relations.js.map +0 -1
  317. package/dist/prompts/workstream-summary.d.ts.map +0 -1
  318. package/dist/prompts/workstream-summary.js.map +0 -1
  319. package/dist/provenance/actor.d.ts.map +0 -1
  320. package/dist/provenance/actor.js.map +0 -1
  321. package/dist/provenance/actor.test.d.ts +0 -2
  322. package/dist/provenance/actor.test.d.ts.map +0 -1
  323. package/dist/provenance/actor.test.js +0 -49
  324. package/dist/provenance/actor.test.js.map +0 -1
  325. package/dist/render.d.ts.map +0 -1
  326. package/dist/render.js.map +0 -1
  327. package/dist/render.test.d.ts +0 -8
  328. package/dist/render.test.d.ts.map +0 -1
  329. package/dist/render.test.js +0 -244
  330. package/dist/render.test.js.map +0 -1
  331. package/dist/routing.d.ts.map +0 -1
  332. package/dist/routing.js.map +0 -1
  333. package/dist/routing.test.d.ts +0 -2
  334. package/dist/routing.test.d.ts.map +0 -1
  335. package/dist/routing.test.js +0 -79
  336. package/dist/routing.test.js.map +0 -1
  337. package/dist/status.d.ts.map +0 -1
  338. package/dist/status.js.map +0 -1
  339. package/dist/status.test.d.ts +0 -5
  340. package/dist/status.test.d.ts.map +0 -1
  341. package/dist/status.test.js +0 -203
  342. package/dist/status.test.js.map +0 -1
package/dist/mcp/tools.js CHANGED
@@ -8,6 +8,7 @@ import { contentHash, daysSince, lastActivityDate, computeCombinedScore, buildFi
8
8
  import { ready, setupError, setReady, log, embed, getEmbeddingConfig, qdrantReq, ensureCollectionsAll, qdrantUpsert, qdrantSearch, qdrantScroll, qdrantSetPayload, qdrantGetPoints, rebuildPool, hasPool, listDestinations, resolveDest, findPointById, } from "./api.js";
9
9
  import { DestinationNotFoundError } from "../routing.js";
10
10
  import { saveConfig, loadConfig, EXTRACTION_HEALTH_PATH } from "../config.js";
11
+ import { availableSearchScopes, resolveSearchScope, SearchScopeNotFoundError, } from "../search-scope.js";
11
12
  import { existsSync, readFileSync } from "node:fs";
12
13
  import { inspectWatcherPaths, formatIssue, repairSuspiciousWatcherPaths } from "../daemon/watcher-health.js";
13
14
  import { normalizeActorId, resolveActorIdentity } from "../provenance/actor.js";
@@ -18,6 +19,7 @@ import { addRedactionPayload, combineRedactions, redactStorageText, } from "../p
18
19
  const NUDGE_INTERVAL_MS = 10 * 60 * 1000;
19
20
  const MEMORY_RECALL_DEFAULT_LIMIT = 10;
20
21
  const MEMORY_RECALL_MAX_LIMIT = 50;
22
+ const searchScopeSchema = z.union([z.string(), z.array(z.string())]).optional();
21
23
  let lastStoreTime = Date.now();
22
24
  let heartbeatCount = 0;
23
25
  // ---------------------------------------------------------------------------
@@ -67,6 +69,63 @@ function resolveDestOrError(input) {
67
69
  };
68
70
  }
69
71
  }
72
+ function withDestination(point, destination) {
73
+ return { ...point, _destination: destination };
74
+ }
75
+ function structuredScopedFact(point) {
76
+ return {
77
+ ...structuredFact(point),
78
+ destination: point._destination.name,
79
+ };
80
+ }
81
+ function formatScopedFact(point, includeDestination) {
82
+ const formatted = formatFact(point);
83
+ return includeDestination ? `[${point._destination.name}] ${formatted}` : formatted;
84
+ }
85
+ function resolveSearchScopeOrError(args) {
86
+ if (args.destination && args.search_scope !== undefined) {
87
+ return {
88
+ error: {
89
+ content: [{ type: "text", text: JSON.stringify({
90
+ status: "ambiguous_search_scope",
91
+ message: "Use either destination for a single destination override or search_scope for routed/all/list search, not both.",
92
+ }, null, 2) }],
93
+ isError: true,
94
+ },
95
+ };
96
+ }
97
+ if (args.destination) {
98
+ const resolved = resolveDestOrError({ ...args.input, destination: args.destination });
99
+ if (resolved.error)
100
+ return { error: resolved.error };
101
+ return {
102
+ scope: {
103
+ name: resolved.dest.name,
104
+ description: resolved.dest.description ?? `Search only the '${resolved.dest.name}' destination.`,
105
+ requested: resolved.dest.name,
106
+ destinations: [resolved.dest],
107
+ },
108
+ };
109
+ }
110
+ const cfg = loadConfig();
111
+ const dests = listDestinations();
112
+ try {
113
+ return { scope: resolveSearchScope(args.search_scope, cfg, dests, args.input) };
114
+ }
115
+ catch (e) {
116
+ const msg = e instanceof Error ? e.message : String(e);
117
+ return {
118
+ error: {
119
+ content: [{ type: "text", text: JSON.stringify({
120
+ status: e instanceof SearchScopeNotFoundError ? "search_scope_not_found" : "search_scope_error",
121
+ message: msg,
122
+ available_search_scopes: availableSearchScopes(cfg, dests),
123
+ }, null, 2) }],
124
+ isError: true,
125
+ },
126
+ };
127
+ }
128
+ }
70
129
  // Add actor identity payload fields. Workspace was removed in v0.4.0 — physical
71
130
  // separation now happens via routing destinations (see routing.ts).
72
131
  function addActorPayload(payload, actor, actorIdOverride) {
@@ -218,6 +277,7 @@ export function registerTools(mcp) {
218
277
  "Use this when memory tools return a 'setup_required' error, or once at session start if you're not sure bikky is wired up. Reports which credentials are missing and includes onboarding instructions if anything is incomplete.",
219
278
  "Read-only — safe to call any time.",
220
279
  ].join(" "), {}, async () => {
280
+ const cfg = loadConfig();
221
281
  const dests = listDestinations();
222
282
  const status = {
223
283
  ready,
@@ -245,6 +305,7 @@ export function registerTools(mcp) {
245
305
  } })(),
246
306
  collection: d.collection,
247
307
  default: d.default ?? false,
308
+ ...(d.description ? { description: d.description } : {}),
248
309
  connected: false,
249
310
  collection_exists: false,
250
311
  };
@@ -263,6 +324,10 @@ export function registerTools(mcp) {
263
324
  destStatus.push(block);
264
325
  }
265
326
  status["destinations"] = destStatus;
327
+ status["default_search_scope"] = cfg.default_search_scope;
328
+ status["search_scopes"] = availableSearchScopes(cfg, dests);
329
+ status["search_scope_hint"] =
330
+ "Read/search tools accept search_scope: 'routed', 'all', a destination name, a configured scope name, a comma-separated destination list, or an array of destination names. Use destination only when you want an exact single-destination override.";
266
331
  try {
267
332
  await embed("test");
268
333
  status["embedding_connected"] = true;
@@ -271,8 +336,11 @@ export function registerTools(mcp) {
271
336
  // Watcher / extraction health (issue #58)
272
337
  const warnings = [];
273
338
  try {
274
- const cfg = loadConfig();
275
339
  status["watcher_path"] = cfg.watchers.copilot.path;
340
+ status["watcher_paths"] = {
341
+ copilot: cfg.watchers.copilot.path,
342
+ claude: cfg.watchers.claude.path,
343
+ };
276
344
  for (const issue of inspectWatcherPaths(cfg)) {
277
345
  warnings.push(formatIssue(issue));
278
346
  }
@@ -284,17 +352,19 @@ export function registerTools(mcp) {
284
352
  status["extraction_last_tick_at"] = health.last_tick_at ?? null;
285
353
  status["extraction_last_active_session_at"] = health.last_active_session_at ?? null;
286
354
  status["extraction_active_session_count"] = health.active_session_count ?? 0;
355
+ if (health.sources)
356
+ status["extraction_sources"] = health.sources;
287
357
  if (health.last_active_session_at) {
288
358
  const hours = (Date.now() - Date.parse(health.last_active_session_at)) / 3_600_000;
289
359
  status["extraction_hours_since_active_session"] = Math.round(hours * 10) / 10;
290
360
  if (hours > 6) {
291
- warnings.push(`Watcher has not seen any active Copilot sessions for ${Math.round(hours)}h — ` +
361
+ warnings.push(`Watcher has not seen any active transcript sources for ${Math.round(hours)}h — ` +
292
362
  `check watcher_path (${health.watcher_path ?? "unknown"}) and that the daemon is running.`);
293
363
  }
294
364
  }
295
365
  else {
296
366
  status["extraction_hours_since_active_session"] = null;
297
- warnings.push("Daemon has never observed an active Copilot session — extraction may be stalled.");
367
+ warnings.push("Daemon has never observed an active transcript source — extraction may be stalled.");
298
368
  }
299
369
  }
300
370
  else {
@@ -316,6 +386,25 @@ export function registerTools(mcp) {
316
386
  }
317
387
  return { content: [{ type: "text", text: JSON.stringify(status, null, 2) }] };
318
388
  });
389
+ // ── memory_search_scopes ─────────────────────────────────────────────────
390
+ mcp.tool("memory_search_scopes", [
391
+ "List the configured memory search scopes and destination descriptions.",
392
+ "Use this before memory_recall, memory_entity, or memory_relations when multiple destinations exist so you can choose the right search_scope.",
393
+ "Read-only — returns built-in scopes ('routed', 'all'), destination-name scopes, configured named scopes, and the default_search_scope.",
394
+ ].join(" "), {}, async () => {
395
+ const cfg = loadConfig();
396
+ const dests = listDestinations();
397
+ return {
398
+ content: [{ type: "text", text: JSON.stringify({
399
+ default_search_scope: cfg.default_search_scope,
400
+ scopes: availableSearchScopes(cfg, dests),
401
+ usage: {
402
+ search_scope: "Pass one scope name, 'routed', 'all', a destination name, a comma-separated destination list, or an array of destination names.",
403
+ destination: "Use this older parameter only for an exact single-destination override. Do not combine it with search_scope.",
404
+ },
405
+ }, null, 2) }],
406
+ };
407
+ });
319
408
  // ── configure_credentials ───────────────────────────────────────────────
320
409
  mcp.tool("configure_credentials", [
321
410
  "Persist Qdrant and embedding credentials to ~/.bikky/config.json and bring the memory system online.",
@@ -719,6 +808,7 @@ export function registerTools(mcp) {
719
808
  " 3. Conflict/replacement check — recall similar facts when you suspect new information may supersede an older fact. Deduplication during memory_store is automatic.",
720
809
  "Combine the natural-language query with structured filters (category, domain, entity, date range, metadata) for tighter results.",
721
810
  "If you have a known entity name and want everything about it, prefer memory_entity. For 'what does X own/use?' style questions, prefer memory_relations.",
811
+ "When multiple Qdrant destinations are configured, use search_scope to choose 'routed' (routing/default behavior), 'all', a destination name, a configured scope name, a comma-separated destination list, or an array of destination names. Call memory_search_scopes to inspect available scopes and descriptions.",
722
812
  `By default output is human-readable text. Use output_format=json for machine-parseable results with separate results and related arrays. Default limit is ${MEMORY_RECALL_DEFAULT_LIMIT}; maximum effective limit is ${MEMORY_RECALL_MAX_LIMIT}.`,
723
813
  ].join("\n"), {
724
814
  query: z.string().describe("Natural-language description of what you're looking for. Embedded and matched semantically — full sentences work better than keyword lists."),
@@ -727,7 +817,8 @@ export function registerTools(mcp) {
727
817
  kind: z.string().optional().describe("Filter by kind: fact, summary, distilled, relation. Optional. Telemetry is excluded by default."),
728
818
  memory_subtype: z.string().optional().describe("Filter by memory subtype (must be valid for the chosen kind). Optional."),
729
819
  workspace_id: z.string().optional().describe("[Removed in v0.4.0] No-op."),
730
- destination: z.string().optional().describe("Optional destination override. When set, queries that destination by name. Hard-errors if no such destination exists. Omit to let routing rules decide."),
820
+ destination: z.string().optional().describe("Optional legacy single-destination override. Do not combine with search_scope. Prefer search_scope for routed/all/list search."),
821
+ search_scope: searchScopeSchema.describe("Optional read/search scope. Accepts 'routed', 'all', a destination name, a configured scope name, a comma-separated destination list, or an array of destination names. Omit to use config.default_search_scope."),
731
822
  actor_id: z.string().optional().describe("Filter to facts captured by or associated with this stable actor identity. Optional."),
732
823
  include_legacy_workspace: z.boolean().optional().describe("[Removed in v0.4.0] No-op."),
733
824
  entity: z.string().optional().describe("Restrict to facts mentioning this entity (case-insensitive). For full entity context prefer memory_entity."),
@@ -743,22 +834,25 @@ export function registerTools(mcp) {
743
834
  graph_depth: z.number().optional().default(0).describe("Entity-graph traversal depth. 0 = vector search only (fast, default). 1 = also surface up to ceil(limit / 2) extra 1-hop entity-related facts (slower; use when the user asks 'what's connected to X?'). In JSON output these are returned separately as related."),
744
835
  output_format: z.enum(["text", "json"]).optional().default("text").describe("Response format. text = backward-compatible human-readable lines (default). json = parseable object with query, limit metadata, results, related, counts, and optional nudge."),
745
836
  metadata_filter: z.record(z.string(), z.string()).optional().describe("Exact-match filter on the metadata map stored with each fact. All key/value pairs must match (AND logic)."),
746
- }, async ({ query, category, domain, kind, memory_subtype, workspace_id: _workspace_id, destination, actor_id, include_legacy_workspace: _include_legacy_workspace, entity, episode_id, workstream_key, task_key, repo, branch, review_status, since, until, limit, graph_depth, output_format, metadata_filter, }) => {
837
+ }, async ({ query, category, domain, kind, memory_subtype, workspace_id: _workspace_id, destination, search_scope, actor_id, include_legacy_workspace: _include_legacy_workspace, entity, episode_id, workstream_key, task_key, repo, branch, review_status, since, until, limit, graph_depth, output_format, metadata_filter, }) => {
747
838
  const guard = requireReady();
748
839
  if (guard)
749
840
  return guard;
750
841
  const requestedLimit = limit ?? MEMORY_RECALL_DEFAULT_LIMIT;
751
842
  const effectiveLimit = clampRecallLimit(limit);
752
843
  const actorFilter = resolveActorIdentity({ actorId: actor_id, useGitFallback: false });
753
- const resolved = resolveDestOrError(routingInput({
844
+ const scopeResolved = resolveSearchScopeOrError({
754
845
  destination,
755
- content: query,
756
- entities: entity ? [entity] : [],
757
- metadata: metadata_filter,
758
- }));
759
- if (resolved.error)
760
- return resolved.error;
761
- const dest = resolved.dest;
846
+ search_scope,
847
+ input: routingInput({
848
+ content: query,
849
+ entities: entity ? [entity] : [],
850
+ metadata: metadata_filter,
851
+ }),
852
+ });
853
+ if (scopeResolved.error)
854
+ return scopeResolved.error;
855
+ const searchScope = scopeResolved.scope;
762
856
  const redactedQuery = redactStorageText(query);
763
857
  const vector = await embed(redactedQuery.text);
764
858
  const normalizedKind = kind ? normalizeKind(kind) : undefined;
@@ -792,12 +886,42 @@ export function registerTools(mcp) {
792
886
  metadata: metadata_filter,
793
887
  excludeKinds: MEMORY_RECALL_EXCLUDED_KINDS,
794
888
  });
795
- const results = await qdrantSearch(dest, vector, filter, effectiveLimit * 2);
796
- if (!results.result?.length) {
889
+ const searchedDestinations = searchScope.destinations.map((dest) => dest.name);
890
+ const failedDestinations = [];
891
+ const scopedResults = [];
892
+ for (const dest of searchScope.destinations) {
893
+ try {
894
+ const results = await qdrantSearch(dest, vector, filter, effectiveLimit * 2);
895
+ scopedResults.push(...(results.result ?? []).map((point) => withDestination(point, dest)));
896
+ }
897
+ catch (e) {
898
+ failedDestinations.push({
899
+ destination: dest.name,
900
+ error: e instanceof Error ? e.message : String(e),
901
+ });
902
+ }
903
+ }
904
+ if (failedDestinations.length === searchScope.destinations.length && searchScope.destinations.length > 0) {
905
+ return {
906
+ content: [{ type: "text", text: JSON.stringify({
907
+ status: "search_failed",
908
+ query: redactedQuery.text,
909
+ search_scope: searchScope.name,
910
+ searched_destinations: searchedDestinations,
911
+ failed_destinations: failedDestinations,
912
+ }, null, 2) }],
913
+ isError: true,
914
+ };
915
+ }
916
+ if (scopedResults.length === 0) {
797
917
  const nudge = buildMemoryNudge();
798
918
  if (output_format === "json") {
799
919
  return { content: [{ type: "text", text: JSON.stringify({
800
920
  query: redactedQuery.text,
921
+ search_scope: searchScope.name,
922
+ search_scope_description: searchScope.description,
923
+ searched_destinations: searchedDestinations,
924
+ failed_destinations: failedDestinations,
801
925
  requested_limit: requestedLimit,
802
926
  effective_limit: effectiveLimit,
803
927
  max_limit: MEMORY_RECALL_MAX_LIMIT,
@@ -811,31 +935,61 @@ export function registerTools(mcp) {
811
935
  ...(redactedQuery.redacted ? { query_redaction: redactedQuery } : {}),
812
936
  }, null, 2) }] };
813
937
  }
814
- const text = nudge ? `No matching facts found.\n\n${nudge}` : "No matching facts found.";
938
+ const warning = failedDestinations.length > 0
939
+ ? `\n\nSearch warnings: ${failedDestinations.map((failure) => `${failure.destination}: ${failure.error}`).join("; ")}`
940
+ : "";
941
+ const text = nudge
942
+ ? `No matching facts found.${warning}\n\n${nudge}`
943
+ : `No matching facts found.${warning}`;
815
944
  return { content: [{ type: "text", text }] };
816
945
  }
817
- const ranked = results.result
946
+ const ranked = scopedResults
818
947
  .map((r) => ({ ...r, _combinedScore: computeCombinedScore(r) }))
819
948
  .sort((a, b) => b._combinedScore - a._combinedScore)
820
949
  .slice(0, effectiveLimit);
821
- const lines = ranked.map((r) => formatFact(r));
822
- let related = { points: [] };
950
+ const includeDestination = searchScope.destinations.length > 1 || searchScope.name === "all";
951
+ const lines = ranked.map((r) => formatScopedFact(r, includeDestination));
952
+ const related = { points: [], errors: [] };
823
953
  if ((graph_depth ?? 0) >= 1) {
824
- related = await graphTraversal(dest, ranked, effectiveLimit);
954
+ const byDestination = new Map();
955
+ for (const point of ranked) {
956
+ const existing = byDestination.get(point._destination.name);
957
+ if (existing) {
958
+ existing.points.push(point);
959
+ }
960
+ else {
961
+ byDestination.set(point._destination.name, { dest: point._destination, points: [point] });
962
+ }
963
+ }
964
+ for (const entry of byDestination.values()) {
965
+ const traversal = await graphTraversal(entry.dest, entry.points, effectiveLimit);
966
+ related.points.push(...traversal.points.map((point) => withDestination(point, entry.dest)));
967
+ if (traversal.error) {
968
+ related.errors.push({ destination: entry.dest.name, error: traversal.error });
969
+ }
970
+ }
971
+ related.points = related.points.slice(0, Math.ceil(effectiveLimit / 2));
825
972
  if (related.points.length > 0) {
826
973
  lines.push("", "── Related (1-hop) ──");
827
- lines.push(...related.points.map((r) => formatFact(r)));
974
+ lines.push(...related.points.map((r) => formatScopedFact(r, includeDestination)));
828
975
  }
829
- else if (related.error) {
830
- lines.push("", `(graph traversal failed: ${related.error})`);
976
+ if (related.errors.length > 0) {
977
+ lines.push("", `(graph traversal warnings: ${related.errors.map((failure) => `${failure.destination}: ${failure.error}`).join("; ")})`);
831
978
  }
832
979
  }
980
+ if (failedDestinations.length > 0) {
981
+ lines.push("", `(search warnings: ${failedDestinations.map((failure) => `${failure.destination}: ${failure.error}`).join("; ")})`);
982
+ }
833
983
  const nudge = buildMemoryNudge();
834
984
  if (nudge)
835
985
  lines.push("", nudge);
836
986
  if (output_format === "json") {
837
987
  return { content: [{ type: "text", text: JSON.stringify({
838
988
  query: redactedQuery.text,
989
+ search_scope: searchScope.name,
990
+ search_scope_description: searchScope.description,
991
+ searched_destinations: searchedDestinations,
992
+ failed_destinations: failedDestinations,
839
993
  requested_limit: requestedLimit,
840
994
  effective_limit: effectiveLimit,
841
995
  max_limit: MEMORY_RECALL_MAX_LIMIT,
@@ -843,9 +997,9 @@ export function registerTools(mcp) {
843
997
  graph_depth: graph_depth ?? 0,
844
998
  result_count: ranked.length,
845
999
  related_count: related.points.length,
846
- results: ranked.map((r) => structuredFact(r)),
847
- related: related.points.map((r) => structuredFact(r)),
848
- ...(related.error ? { graph_error: related.error } : {}),
1000
+ results: ranked.map((r) => structuredScopedFact(r)),
1001
+ related: related.points.map((r) => structuredScopedFact(r)),
1002
+ ...(related.errors.length > 0 ? { graph_errors: related.errors } : {}),
849
1003
  ...(nudge ? { nudge } : {}),
850
1004
  ...(redactedQuery.redacted ? { query_redaction: redactedQuery } : {}),
851
1005
  }, null, 2) }] };
@@ -861,76 +1015,112 @@ export function registerTools(mcp) {
861
1015
  name: z.string().describe("Entity name (case-insensitive, e.g. 'qdrant', 'workspace_id'). Should match the lowercase canonical form used when facts were stored."),
862
1016
  limit: z.number().optional().default(20).describe("Max facts to return (default 20). Relations are always returned in full, capped at 50 each direction."),
863
1017
  workspace_id: z.string().optional().describe("[Removed in v0.4.0] No-op."),
864
- destination: z.string().optional().describe("Optional destination override. Omit to let routing rules decide."),
1018
+ destination: z.string().optional().describe("Optional legacy single-destination override. Do not combine with search_scope."),
1019
+ search_scope: searchScopeSchema.describe("Optional read/search scope. Accepts 'routed', 'all', a destination name, a configured scope name, a comma-separated destination list, or an array of destination names. Omit to use config.default_search_scope."),
865
1020
  include_legacy_workspace: z.boolean().optional().describe("[Removed in v0.4.0] No-op."),
866
- }, async ({ name, limit, workspace_id: _workspace_id, destination, include_legacy_workspace: _include_legacy_workspace }) => {
1021
+ }, async ({ name, limit, workspace_id: _workspace_id, destination, search_scope, include_legacy_workspace: _include_legacy_workspace }) => {
867
1022
  const guard = requireReady();
868
1023
  if (guard)
869
1024
  return guard;
870
1025
  const entityName = name.toLowerCase();
871
- const resolved = resolveDestOrError(routingInput({ destination, entities: [entityName] }));
872
- if (resolved.error)
873
- return resolved.error;
874
- const dest = resolved.dest;
875
- // Look up the daemon-classified entity type, if any.
876
- let entityType = null;
877
- try {
878
- const typeFilter = buildFilter({}) ?? { must: [] };
879
- typeFilter.must.push({ key: "kind", match: { value: "entity_type" } });
880
- typeFilter.must.push({ key: "entity_name", match: { value: entityName } });
881
- const typePoints = await qdrantScroll(dest, typeFilter, 1);
882
- const typePoint = typePoints.result?.points?.[0];
883
- const payload = typePoint?.payload;
884
- if (payload?.entity_type) {
885
- entityType = String(payload.entity_type);
1026
+ const scopeResolved = resolveSearchScopeOrError({
1027
+ destination,
1028
+ search_scope,
1029
+ input: routingInput({ entities: [entityName] }),
1030
+ });
1031
+ if (scopeResolved.error)
1032
+ return scopeResolved.error;
1033
+ const searchScope = scopeResolved.scope;
1034
+ const effectiveLimit = Math.max(1, Math.trunc(limit ?? 20));
1035
+ const entityTypes = new Map();
1036
+ const factPoints = [];
1037
+ const relationPoints = [];
1038
+ const failures = [];
1039
+ for (const dest of searchScope.destinations) {
1040
+ // Look up the daemon-classified entity type, if any.
1041
+ try {
1042
+ const typeFilter = buildFilter({}) ?? { must: [] };
1043
+ typeFilter.must.push({ key: "kind", match: { value: "entity_type" } });
1044
+ typeFilter.must.push({ key: "entity_name", match: { value: entityName } });
1045
+ const typePoints = await qdrantScroll(dest, typeFilter, 1);
1046
+ const typePoint = typePoints.result?.points?.[0];
1047
+ const payload = typePoint?.payload;
1048
+ if (payload?.entity_type) {
1049
+ entityTypes.set(dest.name, String(payload.entity_type));
1050
+ }
886
1051
  }
1052
+ catch {
1053
+ // Type lookup is best-effort — never fails the request.
1054
+ }
1055
+ try {
1056
+ const factsFilter = buildFilter({}) ?? { must: [] };
1057
+ factsFilter.must.push({ key: "entities", match: { value: entityName } });
1058
+ const facts = await qdrantScroll(dest, factsFilter, effectiveLimit);
1059
+ factPoints.push(...(facts.result?.points ?? []).map((point) => withDestination(point, dest)));
1060
+ const fromFilter = buildFilter({}) ?? { must: [] };
1061
+ fromFilter.must.push({ key: "from_entity", match: { value: entityName } });
1062
+ const relationsFrom = await qdrantScroll(dest, fromFilter, 50);
1063
+ const toFilter = buildFilter({}) ?? { must: [] };
1064
+ toFilter.must.push({ key: "to_entity", match: { value: entityName } });
1065
+ const relationsTo = await qdrantScroll(dest, toFilter, 50);
1066
+ relationPoints.push(...[
1067
+ ...(relationsFrom.result?.points ?? []),
1068
+ ...(relationsTo.result?.points ?? []),
1069
+ ].map((point) => withDestination(point, dest)));
1070
+ }
1071
+ catch (e) {
1072
+ failures.push({ destination: dest.name, error: e instanceof Error ? e.message : String(e) });
1073
+ }
1074
+ }
1075
+ if (failures.length === searchScope.destinations.length && searchScope.destinations.length > 0) {
1076
+ return {
1077
+ content: [{ type: "text", text: JSON.stringify({
1078
+ status: "search_failed",
1079
+ entity: entityName,
1080
+ search_scope: searchScope.name,
1081
+ searched_destinations: searchScope.destinations.map((dest) => dest.name),
1082
+ failed_destinations: failures,
1083
+ }, null, 2) }],
1084
+ isError: true,
1085
+ };
887
1086
  }
888
- catch {
889
- // Type lookup is best-effort — never fails the request.
890
- }
891
- const factsFilter = buildFilter({}) ?? { must: [] };
892
- factsFilter.must.push({ key: "entities", match: { value: entityName } });
893
- const facts = await qdrantScroll(dest, factsFilter, limit ?? 20);
894
- const fromFilter = buildFilter({}) ?? { must: [] };
895
- fromFilter.must.push({ key: "from_entity", match: { value: entityName } });
896
- const relationsFrom = await qdrantScroll(dest, fromFilter, 50);
897
- const toFilter = buildFilter({}) ?? { must: [] };
898
- toFilter.must.push({ key: "to_entity", match: { value: entityName } });
899
- const relationsTo = await qdrantScroll(dest, toFilter, 50);
900
1087
  const output = [];
901
- const factPoints = facts.result?.points ?? [];
1088
+ const includeDestination = searchScope.destinations.length > 1 || searchScope.name === "all";
1089
+ const entityTypeValues = [...new Set(entityTypes.values())];
1090
+ const entityType = entityTypeValues.length === 1 ? entityTypeValues[0] : null;
902
1091
  if (factPoints.length > 0) {
903
1092
  const header = entityType
904
- ? `## Facts about ${name} [type: ${entityType}] (${factPoints.length})`
905
- : `## Facts about ${name} (${factPoints.length})`;
1093
+ ? `## Facts about ${name} [type: ${entityType}] (${Math.min(factPoints.length, effectiveLimit)})`
1094
+ : `## Facts about ${name} (${Math.min(factPoints.length, effectiveLimit)})`;
906
1095
  output.push(header);
907
- for (const p of factPoints) {
1096
+ for (const p of factPoints.slice(0, effectiveLimit)) {
908
1097
  if (p.payload.category !== "relation") {
909
- output.push(`- ${formatFact(p)}`);
1098
+ output.push(`- ${formatScopedFact(p, includeDestination)}`);
910
1099
  }
911
1100
  }
912
1101
  }
913
1102
  else if (entityType) {
914
1103
  output.push(`## ${name} [type: ${entityType}]`);
915
1104
  }
916
- const allRelations = [
917
- ...(relationsFrom.result?.points ?? []),
918
- ...(relationsTo.result?.points ?? []),
919
- ];
920
1105
  const seen = new Set();
921
- const uniqueRelations = allRelations.filter((r) => {
922
- if (seen.has(r.id))
1106
+ const uniqueRelations = relationPoints.filter((r) => {
1107
+ const key = `${r._destination.name}:${r.id}`;
1108
+ if (seen.has(key))
923
1109
  return false;
924
- seen.add(r.id);
1110
+ seen.add(key);
925
1111
  return true;
926
1112
  });
927
1113
  if (uniqueRelations.length > 0) {
928
1114
  output.push(`\n## Relations (${uniqueRelations.length})`);
929
1115
  for (const r of uniqueRelations) {
930
1116
  const p = r.payload;
931
- output.push(`- ${p.from_entity} --[${p.relation_type}]--> ${p.to_entity}`);
1117
+ const prefix = includeDestination ? `[${r._destination.name}] ` : "";
1118
+ output.push(`- ${prefix}${p.from_entity} --[${p.relation_type}]--> ${p.to_entity}`);
932
1119
  }
933
1120
  }
1121
+ if (failures.length > 0) {
1122
+ output.push(`\nSearch warnings: ${failures.map((failure) => `${failure.destination}: ${failure.error}`).join("; ")}`);
1123
+ }
934
1124
  if (output.length === 0) {
935
1125
  return { content: [{ type: "text", text: `No facts or relations found for '${name}'.` }] };
936
1126
  }
@@ -946,50 +1136,84 @@ export function registerTools(mcp) {
946
1136
  relation_type: z.string().optional().describe("Filter to a specific edge label (e.g. 'owns', 'uses', 'decided', 'prefers', 'works-on'). Optional."),
947
1137
  direction: z.enum(["from", "to", "both"]).optional().default("both").describe("Which side of the edge the entity is on. 'from' = entity is the source (X --[?]--> ?). 'to' = entity is the target (? --[?]--> X). 'both' = either (default)."),
948
1138
  workspace_id: z.string().optional().describe("[Removed in v0.4.0] No-op."),
949
- destination: z.string().optional().describe("Optional destination override. Omit to let routing rules decide."),
1139
+ destination: z.string().optional().describe("Optional legacy single-destination override. Do not combine with search_scope."),
1140
+ search_scope: searchScopeSchema.describe("Optional read/search scope. Accepts 'routed', 'all', a destination name, a configured scope name, a comma-separated destination list, or an array of destination names. Omit to use config.default_search_scope."),
950
1141
  include_legacy_workspace: z.boolean().optional().describe("[Removed in v0.4.0] No-op."),
951
- }, async ({ entity, relation_type, direction, workspace_id: _workspace_id, destination, include_legacy_workspace: _include_legacy_workspace }) => {
1142
+ }, async ({ entity, relation_type, direction, workspace_id: _workspace_id, destination, search_scope, include_legacy_workspace: _include_legacy_workspace }) => {
952
1143
  const guard = requireReady();
953
1144
  if (guard)
954
1145
  return guard;
955
1146
  const entityName = entity.toLowerCase();
956
- const resolved = resolveDestOrError(routingInput({ destination, entities: [entityName] }));
957
- if (resolved.error)
958
- return resolved.error;
959
- const dest = resolved.dest;
1147
+ const scopeResolved = resolveSearchScopeOrError({
1148
+ destination,
1149
+ search_scope,
1150
+ input: routingInput({ entities: [entityName] }),
1151
+ });
1152
+ if (scopeResolved.error)
1153
+ return scopeResolved.error;
1154
+ const searchScope = scopeResolved.scope;
960
1155
  const results = [];
961
- if (direction === "from" || direction === "both") {
962
- const filter = buildFilter({}) ?? { must: [] };
963
- filter.must.push({ key: "from_entity", match: { value: entityName } });
964
- if (relation_type) {
965
- filter.must.push({ key: "relation_type", match: { value: relation_type.toLowerCase() } });
1156
+ const failures = [];
1157
+ for (const dest of searchScope.destinations) {
1158
+ try {
1159
+ if (direction === "from" || direction === "both") {
1160
+ const filter = buildFilter({}) ?? { must: [] };
1161
+ filter.must.push({ key: "from_entity", match: { value: entityName } });
1162
+ if (relation_type) {
1163
+ filter.must.push({ key: "relation_type", match: { value: relation_type.toLowerCase() } });
1164
+ }
1165
+ const r = await qdrantScroll(dest, filter, 50);
1166
+ results.push(...(r.result?.points ?? []).map((point) => withDestination(point, dest)));
1167
+ }
1168
+ if (direction === "to" || direction === "both") {
1169
+ const filter = buildFilter({}) ?? { must: [] };
1170
+ filter.must.push({ key: "to_entity", match: { value: entityName } });
1171
+ if (relation_type) {
1172
+ filter.must.push({ key: "relation_type", match: { value: relation_type.toLowerCase() } });
1173
+ }
1174
+ const r = await qdrantScroll(dest, filter, 50);
1175
+ results.push(...(r.result?.points ?? []).map((point) => withDestination(point, dest)));
1176
+ }
966
1177
  }
967
- const r = await qdrantScroll(dest, filter, 50);
968
- results.push(...(r.result?.points ?? []));
969
- }
970
- if (direction === "to" || direction === "both") {
971
- const filter = buildFilter({}) ?? { must: [] };
972
- filter.must.push({ key: "to_entity", match: { value: entityName } });
973
- if (relation_type) {
974
- filter.must.push({ key: "relation_type", match: { value: relation_type.toLowerCase() } });
1178
+ catch (e) {
1179
+ failures.push({ destination: dest.name, error: e instanceof Error ? e.message : String(e) });
975
1180
  }
976
- const r = await qdrantScroll(dest, filter, 50);
977
- results.push(...(r.result?.points ?? []));
1181
+ }
1182
+ if (failures.length === searchScope.destinations.length && searchScope.destinations.length > 0) {
1183
+ return {
1184
+ content: [{ type: "text", text: JSON.stringify({
1185
+ status: "search_failed",
1186
+ entity: entityName,
1187
+ search_scope: searchScope.name,
1188
+ searched_destinations: searchScope.destinations.map((dest) => dest.name),
1189
+ failed_destinations: failures,
1190
+ }, null, 2) }],
1191
+ isError: true,
1192
+ };
978
1193
  }
979
1194
  const seen = new Set();
980
1195
  const unique = results.filter((r) => {
981
- if (seen.has(r.id))
1196
+ const key = `${r._destination.name}:${r.id}`;
1197
+ if (seen.has(key))
982
1198
  return false;
983
- seen.add(r.id);
1199
+ seen.add(key);
984
1200
  return true;
985
1201
  });
986
1202
  if (unique.length === 0) {
987
- return { content: [{ type: "text", text: `No relations found for '${entity}'.` }] };
1203
+ const warning = failures.length > 0
1204
+ ? ` Search warnings: ${failures.map((failure) => `${failure.destination}: ${failure.error}`).join("; ")}`
1205
+ : "";
1206
+ return { content: [{ type: "text", text: `No relations found for '${entity}'.${warning}` }] };
988
1207
  }
1208
+ const includeDestination = searchScope.destinations.length > 1 || searchScope.name === "all";
989
1209
  const lines = unique.map((r) => {
990
1210
  const p = r.payload;
991
- return `${p.from_entity} --[${p.relation_type}]--> ${p.to_entity} (confidence: ${p.confidence}, id: ${r.id})`;
1211
+ const prefix = includeDestination ? `[${r._destination.name}] ` : "";
1212
+ return `${prefix}${p.from_entity} --[${p.relation_type}]--> ${p.to_entity} (confidence: ${p.confidence}, id: ${r.id})`;
992
1213
  });
1214
+ if (failures.length > 0) {
1215
+ lines.push("", `Search warnings: ${failures.map((failure) => `${failure.destination}: ${failure.error}`).join("; ")}`);
1216
+ }
993
1217
  return { content: [{ type: "text", text: lines.join("\n") }] };
994
1218
  });
995
1219
  // ── memory_forget ───────────────────────────────────────────────────────
@@ -0,0 +1,24 @@
1
+ import type { BikkyConfig, Destination, SearchScopeTarget } from "./config.js";
2
+ import { type RoutingInput } from "./routing.js";
3
+ export type SearchScopeInput = SearchScopeTarget | null | undefined;
4
+ export interface AvailableSearchScope {
5
+ name: string;
6
+ description: string;
7
+ destinations: "routed" | "all" | string[];
8
+ default: boolean;
9
+ source: "builtin" | "destination" | "config";
10
+ }
11
+ export interface ResolvedSearchScope {
12
+ name: string;
13
+ description: string;
14
+ requested: SearchScopeTarget;
15
+ destinations: Destination[];
16
+ }
17
+ export declare class SearchScopeNotFoundError extends Error {
18
+ readonly scope: string;
19
+ readonly available: string[];
20
+ constructor(scope: string, available: string[]);
21
+ }
22
+ export declare function availableSearchScopes(config: BikkyConfig, destinations: ReadonlyArray<Destination>): AvailableSearchScope[];
23
+ export declare function resolveSearchScope(input: SearchScopeInput, config: BikkyConfig, destinations: ReadonlyArray<Destination>, routing: RoutingInput): ResolvedSearchScope;
24
+ //# sourceMappingURL=search-scope.d.ts.map