dsh-web-search-pro 0.2.0 → 0.2.3

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 (266) hide show
  1. package/CHANGELOG.md +179 -0
  2. package/README.md +197 -32
  3. package/assets/skills/dsh-web-search-pro/SKILL.md +14 -13
  4. package/assets/skills/dsh-web-search-pro/references/config.md +36 -0
  5. package/assets/skills/dsh-web-search-pro/references/sources.md +19 -4
  6. package/assets/skills/dsh-web-search-pro/references/troubleshooting.md +3 -1
  7. package/catalog/sources.v1.json +22 -12
  8. package/lib/actions/approval.js +50 -1
  9. package/lib/actions/approval.js.map +1 -1
  10. package/lib/actions/config.js +274 -0
  11. package/lib/actions/config.js.map +1 -0
  12. package/lib/actions/errors.js +8 -1
  13. package/lib/actions/errors.js.map +1 -1
  14. package/lib/actions/format.js +10 -0
  15. package/lib/actions/format.js.map +1 -1
  16. package/lib/actions/index-view.js +2 -2
  17. package/lib/actions/index-view.js.map +1 -1
  18. package/lib/actions/read.js +7 -3
  19. package/lib/actions/read.js.map +1 -1
  20. package/lib/actions/registry.js +3 -1
  21. package/lib/actions/registry.js.map +1 -1
  22. package/lib/actions/run.js +2 -2
  23. package/lib/actions/run.js.map +1 -1
  24. package/lib/actions/schema.js +18 -0
  25. package/lib/actions/schema.js.map +1 -1
  26. package/lib/actions/search.js +12 -6
  27. package/lib/actions/search.js.map +1 -1
  28. package/lib/actions/sources.js +116 -41
  29. package/lib/actions/sources.js.map +1 -1
  30. package/lib/actions/types.js +19 -1
  31. package/lib/actions/types.js.map +1 -1
  32. package/lib/browser-access.js +25 -0
  33. package/lib/browser-access.js.map +1 -1
  34. package/lib/catalog/recommend.js +11 -3
  35. package/lib/catalog/recommend.js.map +1 -1
  36. package/lib/catalog/schema.js +3 -1
  37. package/lib/catalog/schema.js.map +1 -1
  38. package/lib/cli/builtin-specs.js +341 -0
  39. package/lib/cli/builtin-specs.js.map +1 -0
  40. package/lib/cli/chain.js +193 -0
  41. package/lib/cli/chain.js.map +1 -0
  42. package/lib/cli/chains-spec.js +118 -0
  43. package/lib/cli/chains-spec.js.map +1 -0
  44. package/lib/cli/engine.js +99 -0
  45. package/lib/cli/engine.js.map +1 -0
  46. package/lib/cli/opencli-sites.js +83 -0
  47. package/lib/cli/opencli-sites.js.map +1 -0
  48. package/lib/cli/opencli.js +153 -0
  49. package/lib/cli/opencli.js.map +1 -0
  50. package/lib/cli/probe.js +128 -0
  51. package/lib/cli/probe.js.map +1 -0
  52. package/lib/cli/runner.js +564 -0
  53. package/lib/cli/runner.js.map +1 -0
  54. package/lib/cli/spec.js +496 -0
  55. package/lib/cli/spec.js.map +1 -0
  56. package/lib/cli/time-text.js +79 -0
  57. package/lib/cli/time-text.js.map +1 -0
  58. package/lib/cli/trial.js +57 -0
  59. package/lib/cli/trial.js.map +1 -0
  60. package/lib/client/SettingsCard.js +14 -32
  61. package/lib/client/SettingsCard.js.map +1 -1
  62. package/lib/client/agent-mark.js +27 -0
  63. package/lib/client/agent-mark.js.map +1 -0
  64. package/lib/client/card-frame.js +118 -0
  65. package/lib/client/card-frame.js.map +1 -0
  66. package/lib/client/cli-form.js +257 -0
  67. package/lib/client/cli-form.js.map +1 -0
  68. package/lib/client/field-docs.js +130 -0
  69. package/lib/client/field-docs.js.map +1 -0
  70. package/lib/client/field-keys.js +81 -0
  71. package/lib/client/field-keys.js.map +1 -0
  72. package/lib/client/field-rows.js +71 -0
  73. package/lib/client/field-rows.js.map +1 -0
  74. package/lib/client/fill.js +3 -0
  75. package/lib/client/fill.js.map +1 -0
  76. package/lib/client/form-specs.js +15 -1
  77. package/lib/client/form-specs.js.map +1 -1
  78. package/lib/client/form.js +296 -27
  79. package/lib/client/form.js.map +1 -1
  80. package/lib/client/group-advanced.js +23 -0
  81. package/lib/client/group-advanced.js.map +1 -0
  82. package/lib/client/group-agent.js +28 -0
  83. package/lib/client/group-agent.js.map +1 -0
  84. package/lib/client/group-cli.js +136 -0
  85. package/lib/client/group-cli.js.map +1 -0
  86. package/lib/client/group-common.js +29 -0
  87. package/lib/client/group-common.js.map +1 -0
  88. package/lib/client/group-judge.js +67 -0
  89. package/lib/client/group-judge.js.map +1 -0
  90. package/lib/client/group-order.js +201 -0
  91. package/lib/client/group-order.js.map +1 -0
  92. package/lib/client/group-runtime.js +33 -0
  93. package/lib/client/group-runtime.js.map +1 -0
  94. package/lib/client/group-sources.js +83 -0
  95. package/lib/client/group-sources.js.map +1 -0
  96. package/lib/client/groups-basic.js +36 -0
  97. package/lib/client/groups-basic.js.map +1 -0
  98. package/lib/client/index.js.map +1 -1
  99. package/lib/client/json-list.js +50 -0
  100. package/lib/client/json-list.js.map +1 -0
  101. package/lib/client/locales.js +666 -215
  102. package/lib/client/locales.js.map +1 -1
  103. package/lib/client/order.js +192 -0
  104. package/lib/client/order.js.map +1 -0
  105. package/lib/client/overlay-ops.js +46 -0
  106. package/lib/client/overlay-ops.js.map +1 -0
  107. package/lib/client/sources-table.js +1 -0
  108. package/lib/client/sources-table.js.map +1 -1
  109. package/lib/client/styles.js +74 -47
  110. package/lib/client/styles.js.map +1 -1
  111. package/lib/client/text-view.js +98 -0
  112. package/lib/client/text-view.js.map +1 -0
  113. package/lib/client/ui-kit.js +120 -0
  114. package/lib/client/ui-kit.js.map +1 -0
  115. package/lib/client/validators.js +22 -0
  116. package/lib/client/validators.js.map +1 -1
  117. package/lib/client.js +9151 -1987
  118. package/lib/client.js.map +1 -1
  119. package/lib/config-fillers.js +53 -0
  120. package/lib/config-fillers.js.map +1 -0
  121. package/lib/config-groups.js +49 -0
  122. package/lib/config-groups.js.map +1 -0
  123. package/lib/config.js +87 -107
  124. package/lib/config.js.map +1 -1
  125. package/lib/default-chain.js +66 -0
  126. package/lib/default-chain.js.map +1 -0
  127. package/lib/deps.js +109 -102
  128. package/lib/deps.js.map +1 -1
  129. package/lib/engine-error.js +19 -0
  130. package/lib/engine-error.js.map +1 -0
  131. package/lib/engines.js +32 -115
  132. package/lib/engines.js.map +1 -1
  133. package/lib/fetch.js +23 -4
  134. package/lib/fetch.js.map +1 -1
  135. package/lib/index.js +30 -5
  136. package/lib/index.js.map +1 -1
  137. package/lib/overlay-types.js +88 -0
  138. package/lib/overlay-types.js.map +1 -0
  139. package/lib/overlay.js +683 -0
  140. package/lib/overlay.js.map +1 -0
  141. package/lib/pipeline/candidates.js +27 -1
  142. package/lib/pipeline/candidates.js.map +1 -1
  143. package/lib/pipeline/compile.js +63 -0
  144. package/lib/pipeline/compile.js.map +1 -1
  145. package/lib/pipeline/confidence.js +64 -0
  146. package/lib/pipeline/confidence.js.map +1 -0
  147. package/lib/pipeline/fusion.js +50 -3
  148. package/lib/pipeline/fusion.js.map +1 -1
  149. package/lib/pipeline/plan.js +2 -2
  150. package/lib/pipeline/plan.js.map +1 -1
  151. package/lib/pipeline/render.js +20 -2
  152. package/lib/pipeline/render.js.map +1 -1
  153. package/lib/pipeline/run.js +21 -7
  154. package/lib/pipeline/run.js.map +1 -1
  155. package/lib/pipeline/sources-spec.js +19 -3
  156. package/lib/pipeline/sources-spec.js.map +1 -1
  157. package/lib/providers/builtin.js +13 -14
  158. package/lib/providers/builtin.js.map +1 -1
  159. package/lib/providers/deepseek.js +348 -0
  160. package/lib/providers/deepseek.js.map +1 -0
  161. package/lib/providers/http.js.map +1 -1
  162. package/lib/providers/keyed-meta.js +7 -0
  163. package/lib/providers/keyed-meta.js.map +1 -1
  164. package/lib/providers/platforms.js +118 -58
  165. package/lib/providers/platforms.js.map +1 -1
  166. package/lib/providers/registry.js.map +1 -1
  167. package/lib/restart.js +47 -0
  168. package/lib/restart.js.map +1 -0
  169. package/lib/router.js +203 -27
  170. package/lib/router.js.map +1 -1
  171. package/lib/rpc-types.js +15 -0
  172. package/lib/rpc-types.js.map +1 -0
  173. package/lib/rpc.js +85 -0
  174. package/lib/rpc.js.map +1 -0
  175. package/lib/tools.js +10 -4
  176. package/lib/tools.js.map +1 -1
  177. package/lib/types/actions/approval.d.ts +11 -0
  178. package/lib/types/actions/config.d.ts +54 -0
  179. package/lib/types/actions/format.d.ts +9 -2
  180. package/lib/types/actions/run.d.ts +1 -0
  181. package/lib/types/actions/schema.d.ts +7 -1
  182. package/lib/types/actions/sources.d.ts +29 -3
  183. package/lib/types/actions/types.d.ts +28 -2
  184. package/lib/types/browser-access.d.ts +17 -0
  185. package/lib/types/catalog/recommend.d.ts +4 -0
  186. package/lib/types/catalog/schema.d.ts +2 -0
  187. package/lib/types/cli/builtin-specs.d.ts +38 -0
  188. package/lib/types/cli/chain.d.ts +83 -0
  189. package/lib/types/cli/chains-spec.d.ts +48 -0
  190. package/lib/types/cli/engine.d.ts +50 -0
  191. package/lib/types/cli/opencli-sites.d.ts +24 -0
  192. package/lib/types/cli/opencli.d.ts +77 -0
  193. package/lib/types/cli/probe.d.ts +53 -0
  194. package/lib/types/cli/runner.d.ts +126 -0
  195. package/lib/types/cli/spec.d.ts +269 -0
  196. package/lib/types/cli/time-text.d.ts +12 -0
  197. package/lib/types/cli/trial.d.ts +19 -0
  198. package/lib/types/client/SettingsCard.d.ts +17 -1
  199. package/lib/types/client/agent-mark.d.ts +22 -0
  200. package/lib/types/client/card-frame.d.ts +35 -0
  201. package/lib/types/client/cli-form.d.ts +97 -0
  202. package/lib/types/client/field-docs.d.ts +15 -0
  203. package/lib/types/client/field-keys.d.ts +41 -0
  204. package/lib/types/client/field-rows.d.ts +70 -0
  205. package/lib/types/client/fill.d.ts +2 -0
  206. package/lib/types/client/form-specs.d.ts +12 -3
  207. package/lib/types/client/form.d.ts +79 -1
  208. package/lib/types/client/group-advanced.d.ts +9 -0
  209. package/lib/types/client/group-agent.d.ts +20 -0
  210. package/lib/types/client/group-cli.d.ts +52 -0
  211. package/lib/types/client/group-common.d.ts +12 -0
  212. package/lib/types/client/group-judge.d.ts +17 -0
  213. package/lib/types/client/group-order.d.ts +26 -0
  214. package/lib/types/client/group-runtime.d.ts +15 -0
  215. package/lib/types/client/group-sources.d.ts +46 -0
  216. package/lib/types/client/groups-basic.d.ts +18 -0
  217. package/lib/types/client/index.d.ts +8 -0
  218. package/lib/types/client/json-list.d.ts +21 -0
  219. package/lib/types/client/locales.d.ts +1079 -196
  220. package/lib/types/client/order.d.ts +102 -0
  221. package/lib/types/client/overlay-ops.d.ts +37 -0
  222. package/lib/types/client/styles.d.ts +56 -41
  223. package/lib/types/client/text-view.d.ts +22 -0
  224. package/lib/types/client/ui-kit.d.ts +200 -0
  225. package/lib/types/client/validators.d.ts +6 -0
  226. package/lib/types/config-fillers.d.ts +9 -0
  227. package/lib/types/config-groups.d.ts +23 -0
  228. package/lib/types/config.d.ts +265 -216
  229. package/lib/types/default-chain.d.ts +63 -0
  230. package/lib/types/deps.d.ts +25 -23
  231. package/lib/types/engine-error.d.ts +12 -0
  232. package/lib/types/engines.d.ts +45 -10
  233. package/lib/types/fetch.d.ts +12 -3
  234. package/lib/types/index.d.ts +164 -146
  235. package/lib/types/overlay-types.d.ts +98 -0
  236. package/lib/types/overlay.d.ts +141 -0
  237. package/lib/types/pipeline/candidates.d.ts +19 -0
  238. package/lib/types/pipeline/compile.d.ts +11 -0
  239. package/lib/types/pipeline/confidence.d.ts +35 -0
  240. package/lib/types/pipeline/fusion.d.ts +23 -1
  241. package/lib/types/pipeline/render.d.ts +16 -0
  242. package/lib/types/pipeline/run.d.ts +3 -1
  243. package/lib/types/pipeline/sources-spec.d.ts +5 -2
  244. package/lib/types/pipeline/types.d.ts +37 -0
  245. package/lib/types/providers/deepseek.d.ts +130 -0
  246. package/lib/types/providers/http.d.ts +2 -1
  247. package/lib/types/providers/keyed-meta.d.ts +5 -0
  248. package/lib/types/providers/platforms.d.ts +15 -3
  249. package/lib/types/providers/registry.d.ts +10 -0
  250. package/lib/types/restart.d.ts +52 -0
  251. package/lib/types/router.d.ts +69 -9
  252. package/lib/types/rpc-types.d.ts +79 -0
  253. package/lib/types/rpc.d.ts +41 -0
  254. package/lib/types/tools.d.ts +6 -0
  255. package/lib/types/util.d.ts +16 -1
  256. package/lib/util.js +17 -4
  257. package/lib/util.js.map +1 -1
  258. package/package.json +51 -43
  259. package/scripts/check-client-bundle.mjs +1 -1
  260. package/scripts/cli-login-check.mjs +214 -0
  261. package/lib/client/RubricEditor.js +0 -47
  262. package/lib/client/RubricEditor.js.map +0 -1
  263. package/lib/client/fields.js +0 -31
  264. package/lib/client/fields.js.map +0 -1
  265. package/lib/types/client/RubricEditor.d.ts +0 -6
  266. package/lib/types/client/fields.d.ts +0 -65
package/lib/router.js CHANGED
@@ -4,8 +4,11 @@
4
4
  * @module web-search-pro/router
5
5
  */
6
6
  import { credentialRef } from '@deepseek-ai/dsh-credentials';
7
+ import { effectiveChain } from "./default-chain.js";
7
8
  import { EngineError, } from "./engines.js";
8
- import { customKeyProblem, customPlatformAdapter } from "./providers/platforms.js";
9
+ import { customCliAdapter, customCliProviderId, customKeyProblem, customPlatformAdapter } from "./providers/platforms.js";
10
+ import { customCliPlatformIssues, isBackendId, platformBackendIssues } from "./cli/chains-spec.js";
11
+ import { DEFAULT_CUSTOM_CONFIDENCE, resolveCliAdapters } from "./cli/spec.js";
9
12
  import { SourceUnavailableError } from "./providers/unavailable.js";
10
13
  import { defaultProviderRegistry, costTierOf, routeIdOf } from "./providers/index.js";
11
14
  import { BOCHA_FALLBACK_KEY_ENV, BOCHA_KEY_ENV } from "./providers/bocha.js";
@@ -22,8 +25,25 @@ import { createPlatformCacheKey, createSearchCacheKey } from "./cache-key.js";
22
25
  import { allAttemptsBenign, allAttemptsEmpty, BackendRegistry, NoBackendError } from "./backend-registry.js";
23
26
  import { ExaClient } from "./exa-client.js";
24
27
  import { SingleFlight } from "./singleflight.js";
25
- import { mergeCandidates } from "./pipeline/candidates.js";
28
+ import { mergeCandidates, provenanceOf } from "./pipeline/candidates.js";
29
+ import { guaranteeSeats } from "./pipeline/confidence.js";
26
30
  import { fuseCandidates } from "./pipeline/fusion.js";
31
+ function originExtra(origin) {
32
+ return origin.via || origin.tag || origin.confidence !== undefined ? { origin } : {};
33
+ }
34
+ function originFromExtra(extra) {
35
+ if (!extra)
36
+ return {};
37
+ try {
38
+ const origin = JSON.parse(extra).origin;
39
+ return origin && typeof origin === 'object'
40
+ ? { ...typeof origin.via === 'string' ? { via: origin.via } : {}, ...typeof origin.tag === 'string' ? { tag: origin.tag } : {}, ...typeof origin.confidence === 'number' ? { confidence: origin.confidence } : {} }
41
+ : {};
42
+ }
43
+ catch {
44
+ return {};
45
+ }
46
+ }
27
47
  export class SearchRouter {
28
48
  ctx;
29
49
  config;
@@ -40,6 +60,11 @@ export class SearchRouter {
40
60
  syncedRevision = -1;
41
61
  /** Custom platforms (settings `customPlatforms`) this router registered: key -> spec signature + unregister. A key the registry refused stays here with its problem so it is not retried on every call. */
42
62
  custom = new Map();
63
+ /** User-defined CLI adapters (settings `cliAdapters`) this router registered as `custom-cli:<id>` platforms, with the diagnostics of the entries it ignored. */
64
+ customCli = new Map();
65
+ cliAdapterDiagnostics = [];
66
+ /** Confidence of each registered custom CLI by route id (its spec's, default 0.5), rebuilt with the registrations. */
67
+ customConfidence = new Map();
43
68
  /** Latest local probe per route id (read by providerStatuses for the credential dimension). */
44
69
  readiness = new Map();
45
70
  /** Last real call per route id: feeds the health dimension (never inferred from a local probe). */
@@ -101,6 +126,35 @@ export class SearchRouter {
101
126
  this.custom.set(key, { sig, off: () => { }, problem: 'custom platform "' + key + '" was not registered: ' + (error instanceof Error ? error.message : String(error)) });
102
127
  }
103
128
  }
129
+ this.syncCustomCliAdapters();
130
+ }
131
+ /** The same for `cliAdapters`: valid specs become `custom-cli:<id>` platforms; an invalid entry is ignored and reported. */
132
+ syncCustomCliAdapters() {
133
+ const { specs, diagnostics } = resolveCliAdapters(this.dynamic().cliAdapters);
134
+ this.cliAdapterDiagnostics = diagnostics;
135
+ this.customConfidence = new Map([...specs].map(([id, spec]) => [customCliProviderId(id), spec.confidence ?? DEFAULT_CUSTOM_CONFIDENCE]));
136
+ const next = new Map([...specs].map(([id, spec]) => [id, JSON.stringify(spec)]));
137
+ for (const [id, entry] of [...this.customCli])
138
+ if (next.get(id) !== entry.sig) {
139
+ entry.off();
140
+ this.customCli.delete(id);
141
+ }
142
+ for (const [id, sig] of next) {
143
+ if (this.customCli.has(id))
144
+ continue;
145
+ try {
146
+ this.customCli.set(id, { sig, off: this.registry.register(customCliAdapter(id, specs.get(id))) });
147
+ }
148
+ catch (error) {
149
+ this.customCli.set(id, { sig, off: () => { }, problem: 'cliAdapters.' + id + ' was not registered as ' + customCliProviderId(id) + ': ' + (error instanceof Error ? error.message : String(error)) });
150
+ }
151
+ }
152
+ }
153
+ /** Configuration problems of the CLI adapter layer for `sources.status`: ignored `cliAdapters` entries and `platformBackends` that name nothing usable. */
154
+ cliAdapterProblems() {
155
+ this.syncCustomPlatforms();
156
+ const cfg = this.dynamic();
157
+ return [...this.cliAdapterDiagnostics, ...[...this.customCli.values()].flatMap(entry => (entry.problem ? [entry.problem] : [])), ...customCliPlatformIssues(cfg.cliAdapters), ...platformBackendIssues(cfg.platformBackends, cfg.cliAdapters)];
104
158
  }
105
159
  /** Custom platforms the registry could not take (key clash, bad key), for `sources.status`. */
106
160
  customPlatformProblems() {
@@ -112,6 +166,9 @@ export class SearchRouter {
112
166
  for (const entry of this.custom.values())
113
167
  entry.off();
114
168
  this.custom.clear();
169
+ for (const entry of this.customCli.values())
170
+ entry.off();
171
+ this.customCli.clear();
115
172
  }
116
173
  backendFor(id) {
117
174
  return {
@@ -156,7 +213,9 @@ export class SearchRouter {
156
213
  const outcome = await engine.search(input.query, input.count, input.signal, this.withBindings(id, input.options));
157
214
  ticket?.settle(1, 'request counted by the router (the adapter booked none)');
158
215
  this.outcomes.set(id, { ok: true, at: new Date().toISOString() });
159
- return { ...outcome, via: outcome.via ?? engine.id };
216
+ // The source's confidence rides with every row it returned (only when below 1: the built-in sources add nothing).
217
+ const confidence = this.confidenceOf(id, outcome.backend);
218
+ return { ...outcome, via: outcome.via ?? engine.id, ...confidence < 1 ? { sources: outcome.sources.map(source => ({ ...source, confidence })) } : {} };
160
219
  }
161
220
  catch (error) {
162
221
  const code = error?.code;
@@ -219,17 +278,53 @@ export class SearchRouter {
219
278
  return { deps: await this.deps(false), config: this.dynamic(), ...cli ? { cli } : {} };
220
279
  }
221
280
  /**
222
- * The configured `engines` list with the user's `sources.*` preferences applied: disabled sources removed, `priority` ones that are
223
- * already in the list moved to the front in their listed order. An explicit `engines` of a call never goes through this.
281
+ * The default engine order for a search that names no `engines`: the configured `engines` list with the user's `sources.*`
282
+ * preferences applied, then the default rule for "常用" (common) sources.
283
+ * 1. an explicit `engines` of a call never comes through here;
284
+ * 2. `sources.disabled` ids are removed, `sources.priority` ids that are in the list move to the front in their listed order;
285
+ * 3. a common source (DeepSeek Search) whose key resolves leads what is left, while `engines` is still the default list (a
286
+ * list the user wrote is theirs: it keeps its order, and a common source in it keeps the position given). `sources.disabled`
287
+ * and `evidence.sourcePolicy: anonymous-only` switch it off.
224
288
  */
225
- preferred(ids) {
226
- const src = this.dynamic().sources;
227
- const disabled = new Set(this.canonicalIds(src?.disabled ?? []));
228
- const kept = ids.filter(id => !disabled.has(id));
229
- if (ids.length && !kept.length)
230
- throw new Error('every configured engine is disabled (sources.disabled): ' + ids.join(', '));
231
- const first = this.canonicalIds(src?.priority ?? []).filter(id => kept.includes(id));
232
- return [...first, ...kept.filter(id => !first.includes(id))];
289
+ async preferred(ids) {
290
+ const cfg = this.dynamic();
291
+ const src = cfg.sources;
292
+ return effectiveChain({
293
+ engines: ids,
294
+ priority: this.canonicalIds(src?.priority ?? []),
295
+ disabled: this.canonicalIds(src?.disabled ?? []),
296
+ anonymousOnly: cfg.evidence?.sourcePolicy === 'anonymous-only',
297
+ commonReady: await this.commonReady(),
298
+ });
299
+ }
300
+ /** Route ids of the common ("常用") sources that are ready to run now (key resolved), best `priority` first. */
301
+ async commonReady() {
302
+ const common = this.registry.list({ operation: 'search' }).filter(a => a.descriptor.common && a.descriptor.kind !== 'platform');
303
+ if (!common.length)
304
+ return [];
305
+ const env = await this.probeEnv();
306
+ const out = [];
307
+ for (const adapter of common) {
308
+ try {
309
+ const readiness = await adapter.probeLocal(env);
310
+ if (readiness.available && readiness.credential === 'configured')
311
+ out.push({ id: routeIdOf(adapter.descriptor), priority: adapter.descriptor.priority ?? 100 });
312
+ }
313
+ catch { /* a probe that throws is not ready */ }
314
+ }
315
+ return out.sort((a, b) => a.priority - b.priority).map(x => x.id);
316
+ }
317
+ /**
318
+ * Key and endpoint of the DeepSeek Fetch API for the fetch pipeline, or undefined when no key resolves (settings literal ->
319
+ * DSH credentials ref -> process environment, the same order as the search side). Resolved per call; the key is never cached.
320
+ */
321
+ async deepseekAccess() {
322
+ const cfg = this.dynamic();
323
+ const { sourceKeys, sourceBaseUrls } = await this.keyedSources(cfg, (ref, literal) => this.resolveKey(ref, literal));
324
+ const apiKey = sourceKeys?.deepseek;
325
+ if (!apiKey)
326
+ return undefined;
327
+ return { apiKey, ...sourceBaseUrls?.deepseek ? { baseUrl: sourceBaseUrls.deepseek } : {}, allowProxyFakeIp: cfg.allowProxyFakeIp, usage: this.usageRecorder() };
233
328
  }
234
329
  /** Alias / full id -> route id; ids the registry does not know are kept as written (the backend then reports them unknown). */
235
330
  canonicalIds(ids) {
@@ -242,10 +337,9 @@ export class SearchRouter {
242
337
  return diagnostics;
243
338
  const cfg = this.dynamic();
244
339
  const exaKey = await this.resolveKey(cfg.exaApiKeyEnv, cfg.exaApiKey);
340
+ // Platform providers (bilibili, youtube, ...) report their own chain readiness (src/cli/chain.ts); only Exa's MCP route is overruled here.
245
341
  const requiredCli = new Map([
246
342
  ...exaKey ? [] : [['exa', 'mcporter']],
247
- ['bilibili', 'bili'],
248
- ['youtube', 'yt-dlp'],
249
343
  ]);
250
344
  return diagnostics.map(diagnostic => {
251
345
  const dependency = requiredCli.get(diagnostic.id);
@@ -310,7 +404,7 @@ export class SearchRouter {
310
404
  try {
311
405
  const selected = await this.backends.runSelected({ query: call.query, count: call.count, signal: call.signal, skipSeam: opts.skipSeam ?? false, ...call.options ? { options: call.options } : {} }, { preferred: this.canonicalIds([call.id]), signal: call.signal });
312
406
  const sources = selected.value.sources;
313
- return sources.length ? { state: 'ok', sources } : { state: 'empty' };
407
+ return sources.length ? { state: 'ok', sources, ...selected.value.backend ? { backend: selected.value.backend } : {}, ...selected.value.notes?.length ? { notes: selected.value.notes } : {} } : { state: 'empty' };
314
408
  }
315
409
  catch (error) {
316
410
  if (call.signal.aborted)
@@ -328,6 +422,33 @@ export class SearchRouter {
328
422
  return { state: 'error', message: error instanceof Error ? error.message : String(error) };
329
423
  }
330
424
  }
425
+ /**
426
+ * The credential variables of the CLI adapters under the names the tools read, each resolved like the plugin's other secrets:
427
+ * settings literal -> DSH credentials ref (`twitterAuthTokenEnv`) -> process environment. Values only ever reach a command's environment.
428
+ */
429
+ async cliEnv() {
430
+ const cfg = this.dynamic();
431
+ const out = {};
432
+ const authToken = await this.resolveKey(cfg.twitterAuthTokenEnv ?? 'TWITTER_AUTH_TOKEN', cfg.twitterAuthToken);
433
+ const ct0 = await this.resolveKey(cfg.twitterCt0Env ?? 'TWITTER_CT0', cfg.twitterCt0);
434
+ if (authToken)
435
+ out.TWITTER_AUTH_TOKEN = authToken;
436
+ if (ct0)
437
+ out.TWITTER_CT0 = ct0;
438
+ return out;
439
+ }
440
+ /** Sync twin of {@link cliEnv} for `available()` checks: settings literal and process environment only. */
441
+ cliEnvSync() {
442
+ const cfg = this.dynamic();
443
+ const out = {};
444
+ const authToken = cfg.twitterAuthToken || process.env[cfg.twitterAuthTokenEnv ?? 'TWITTER_AUTH_TOKEN'];
445
+ const ct0 = cfg.twitterCt0 || process.env[cfg.twitterCt0Env ?? 'TWITTER_CT0'];
446
+ if (authToken)
447
+ out.TWITTER_AUTH_TOKEN = authToken;
448
+ if (ct0)
449
+ out.TWITTER_CT0 = ct0;
450
+ return out;
451
+ }
331
452
  /** Resolve a key through credentials first, then process env. */
332
453
  async resolveKey(ref, literal) {
333
454
  if (literal && literal.length > 0)
@@ -357,8 +478,10 @@ export class SearchRouter {
357
478
  const semanticScholarApiKey = await this.resolveKey(SEMANTICSCHOLAR_KEY_ENV);
358
479
  const anysearchApiKey = await this.resolveKey(ANYSEARCH_KEY_ENV);
359
480
  const keyed = await this.keyedSources(cfg, (ref, literal) => this.resolveKey(ref, literal));
481
+ const cliEnv = await this.cliEnv();
360
482
  return {
361
483
  ...keyed,
484
+ ...Object.keys(cliEnv).length ? { cliEnv } : {},
362
485
  ...openalexApiKey ? { openalexApiKey } : {},
363
486
  ...semanticScholarApiKey ? { semanticScholarApiKey } : {},
364
487
  ...anysearchApiKey ? { anysearchApiKey } : {},
@@ -395,8 +518,10 @@ export class SearchRouter {
395
518
  const semanticScholarApiKey = process.env[SEMANTICSCHOLAR_KEY_ENV];
396
519
  const anysearchApiKey = process.env[ANYSEARCH_KEY_ENV];
397
520
  const keyed = this.keyedSourcesSync(cfg);
521
+ const cliEnv = this.cliEnvSync();
398
522
  return {
399
523
  ...keyed,
524
+ ...Object.keys(cliEnv).length ? { cliEnv } : {},
400
525
  ...openalexApiKey ? { openalexApiKey } : {},
401
526
  ...semanticScholarApiKey ? { semanticScholarApiKey } : {},
402
527
  ...anysearchApiKey ? { anysearchApiKey } : {},
@@ -477,8 +602,31 @@ export class SearchRouter {
477
602
  }
478
603
  /** Problems of the `sources.*` settings (unknown ids, bad numbers), for `sources.status`. */
479
604
  sourceDiagnostics() {
605
+ this.syncCustomPlatforms();
480
606
  const src = this.dynamic().sources;
481
- return resolveSources({ priority: src?.priority, disabled: src?.disabled, budget: src?.budget }, id => this.registry.routeId(id) !== undefined).diagnostics;
607
+ return resolveSources({ priority: src?.priority, disabled: src?.disabled, budget: src?.budget, confidence: src?.confidence }, id => this.registry.routeId(id) !== undefined, isBackendId).diagnostics;
608
+ }
609
+ /** The confidence of one platform backend as a leg of a chain (`xhs`, `custom-cli:echo`): its own, without the provider's. */
610
+ backendConfidenceOf(id) {
611
+ return this.confidenceOf(id);
612
+ }
613
+ /** `sources.confidence` by canonical id (a provider's route id; a platform backend id as written), valid entries only. */
614
+ confidenceOverrides() {
615
+ const resolved = resolveSources({ confidence: this.dynamic().sources?.confidence }, id => this.registry.routeId(id) !== undefined, isBackendId);
616
+ return new Map(Object.entries(resolved.confidence).map(([id, value]) => [this.registry.routeId(id) ?? id, value]));
617
+ }
618
+ /**
619
+ * The confidence (0..1) of what `route` returned, through `backend` when it is a platform chain: `sources.confidence` of the
620
+ * provider times that of the backend that answered, each defaulting to the custom CLI's own (spec `confidence`, 0.5) and to 1
621
+ * for every built-in source, so the built-in ones are 1 unless the user set something.
622
+ */
623
+ confidenceOf(route, backend) {
624
+ this.syncCustomPlatforms();
625
+ const overrides = this.confidenceOverrides();
626
+ const own = (id) => overrides.get(id) ?? this.customConfidence.get(id) ?? 1;
627
+ const provider = this.registry.routeId(route) ?? route;
628
+ const via = backend === undefined || backend === provider ? undefined : this.registry.routeId(backend) ?? backend;
629
+ return Math.round(own(provider) * (via === undefined ? 1 : own(via)) * 10_000) / 10_000;
482
630
  }
483
631
  /** Counts a metered, non-model request (Bocha search) in the usage ledger; best effort, never throws into the search. */
484
632
  usageRecorder() {
@@ -544,19 +692,34 @@ export class SearchRouter {
544
692
  const health = cooling ? 'cooldown' : last ? (last.ok ? 'ready' : 'error') : 'unknown';
545
693
  const credential = last?.code === 'ENGINE_AUTH' ? 'rejected' : local.credential;
546
694
  const reason = !available ? (diag?.reason ?? local.reason) : undefined;
695
+ let chain;
696
+ let chainReport;
697
+ if (adapter.chain) {
698
+ try {
699
+ chainReport = await adapter.chain(env);
700
+ chain = chainReport.entries.map(entry => ({ ...entry, confidence: this.backendConfidenceOf(entry.id) }));
701
+ }
702
+ catch {
703
+ chain = undefined;
704
+ }
705
+ }
547
706
  out.push({
548
707
  id: d.id, route, aliases: [...d.aliases], label: d.label, kind: d.kind ?? 'web', ...d.domains?.length ? { domains: [...d.domains] } : {}, ...d.needsBrowser ? { needsBrowser: d.needsBrowser } : {}, operations: [...d.operations], taskProfiles: [...d.taskProfiles],
549
708
  languages: [...d.languages], regions: [...d.regions], resultKinds: [...d.resultKinds],
550
709
  ...d.sourceFamily ? { sourceFamily: d.sourceFamily } : {},
551
710
  requirements: d.requirements.map(({ env, ...r }) => ({ ...r, ...env ? { env: [...env] } : {} })), supportedFilters: [...d.supportedFilters], costModel: { ...d.costModel }, costTier: local.costTier ?? costTierOf(d),
552
711
  ...d.verification?.live ? {} : { unverified: true },
712
+ ...d.common ? { common: true } : {},
713
+ confidence: this.confidenceOf(route),
714
+ ...chain ? { chain } : {},
553
715
  ...this.requestBudgetOf(route) ? { budget: this.requestBudgetOf(route) } : {},
554
716
  readiness: {
555
717
  available,
718
+ ...chainReport ? { state: chainReport.state } : {},
556
719
  ...local.installation ? { installation: local.installation } : {},
557
720
  ...credential ? { credential } : {},
558
721
  health,
559
- ...reason ? { reason } : {},
722
+ ...reason ? { reason } : chainReport?.state === 'login_unverified' && chainReport.reason ? { reason: chainReport.reason } : {},
560
723
  ...local.diagnosticCode ? { diagnosticCode: local.diagnosticCode } : {},
561
724
  lastLocalCheck: now,
562
725
  ...last?.ok ? { lastRemoteSuccess: last.at } : {},
@@ -575,7 +738,7 @@ export class SearchRouter {
575
738
  const query = platform ? platform.query : opts.query.trim();
576
739
  if (!platform && !query)
577
740
  throw new Error('query must be a non-empty string');
578
- const ids = platform ? [platform.id] : opts.engines && opts.engines.length ? this.canonicalIds(opts.engines) : this.preferred(this.canonicalIds(cfg.engines));
741
+ const ids = platform ? [platform.id] : opts.engines && opts.engines.length ? this.canonicalIds(opts.engines) : await this.preferred(this.canonicalIds(cfg.engines));
579
742
  const nq = normQuery(platform ? query || platform.url || platform.id : query);
580
743
  const count = Math.min(Math.max(opts.count, 1), 20);
581
744
  const multi = !platform && opts.multi && ids.length > 1;
@@ -628,11 +791,12 @@ export class SearchRouter {
628
791
  if (detail?.requestedCount === undefined || detail.requestedCount >= count) {
629
792
  const result = {
630
793
  ...detail?.content ? { content: detail.content } : {},
631
- sources: shapeSources(cached.rows.map(r => ({ url: r.url, title: r.title, snippet: r.snippet, publishedAt: r.published })), count),
794
+ sources: shapeSources(cached.rows.map(r => ({ url: r.url, title: r.title, snippet: r.snippet, publishedAt: r.published, ...originFromExtra(r.extra) })), count),
632
795
  engine: detail?.engine ?? ids[0] ?? 'unknown',
633
796
  enginesTried: detail?.enginesTried ?? ids,
634
797
  fromCache: true,
635
798
  availableCount: cached.rows.length,
799
+ ...detail?.backend ? { backend: detail.backend } : {},
636
800
  ...detail?.fallbackNote ? { fallbackNote: detail.fallbackNote } : {},
637
801
  };
638
802
  this.memory.set(memoryKey, result);
@@ -648,6 +812,7 @@ export class SearchRouter {
648
812
  let fallbackNote;
649
813
  let availableCount;
650
814
  let persistExtras;
815
+ let backendId;
651
816
  if (multi) {
652
817
  // Every engine goes through the registry (probe, cooldown, quality gate,
653
818
  // attempts) in parallel; failures of one never cancel the others.
@@ -674,18 +839,19 @@ export class SearchRouter {
674
839
  throw new Error('all engines failed: ' + failures);
675
840
  }
676
841
  const merged = mergeCandidates(outputs);
677
- const ranked = fuseCandidates(merged, {
842
+ // A source with a confidence below 1 weighs a little less in the fusion, and its best result keeps a place in the list (confidence.ts).
843
+ const ranked = guaranteeSeats(fuseCandidates(merged, {
678
844
  k: cfg.rrfConstant,
679
845
  freshnessBoost: cfg.freshnessBoost,
680
846
  freshnessDays: cfg.freshnessDays,
681
847
  authorityBoost: cfg.authorityBoost,
682
848
  authorityDomains: cfg.authorityDomains,
683
849
  nProviders: outputs.length,
684
- }).slice(0, count);
850
+ }), count, ranked => ranked.candidate.contributions).slice(0, count);
685
851
  availableCount = merged.length;
686
- outcome = { sources: ranked.map(({ candidate }) => ({ url: candidate.url, ...candidate.title ? { title: candidate.title } : {}, ...candidate.snippet ? { snippet: candidate.snippet } : {}, ...candidate.publishedAt ? { publishedAt: candidate.publishedAt } : {} })) };
687
- // Per-source provenance survives in results.extra (no schema change); the cache-hit path ignores it.
688
- persistExtras = ranked.map(({ candidate, score }) => JSON.stringify({ contributions: candidate.contributions, score: Number(score.toFixed(6)) }));
852
+ outcome = { sources: ranked.map(({ candidate }) => ({ url: candidate.url, ...candidate.title ? { title: candidate.title } : {}, ...candidate.snippet ? { snippet: candidate.snippet } : {}, ...candidate.publishedAt ? { publishedAt: candidate.publishedAt } : {}, ...provenanceOf(candidate) })) };
853
+ // Per-source provenance survives in results.extra (no schema change); a cache hit reads back the origin (`via`, `tag`, `confidence`) from it.
854
+ persistExtras = ranked.map(({ candidate, score }) => JSON.stringify({ contributions: candidate.contributions, score: Number(score.toFixed(6)), ...originExtra(provenanceOf(candidate)) }));
689
855
  usedId = 'multi(' + ids.join('+') + ')';
690
856
  }
691
857
  else {
@@ -701,6 +867,8 @@ export class SearchRouter {
701
867
  outcome = selected.value;
702
868
  usedId = platform ? platform.id : selected.id;
703
869
  viaId = selected.value.via ?? selected.id;
870
+ if (platform)
871
+ backendId = selected.value.backend ?? viaId;
704
872
  enginesTried.push(...ids.slice(0, Math.max(ids.indexOf(selected.id) + 1, 1)));
705
873
  // P1-1: explain the fallback (e.g. ddg returned results but none had snippets). A single platform has nothing to fall back to.
706
874
  const lowQualityAttempts = platform ? [] : selected.attempts.filter(a => a.outcome === 'low-quality');
@@ -714,6 +882,10 @@ export class SearchRouter {
714
882
  const spent = selected.attempts.filter(a => a.outcome === 'skipped' && /request budget used up/.test(a.detail ?? ''));
715
883
  if (spent.length)
716
884
  fallbackNote = (fallbackNote ? fallbackNote + '\n' : '') + 'skipped: ' + spent.map(a => a.id + ': ' + a.detail).join('; ');
885
+ // Notes of the engine itself: chain legs passed over (with reasons), results dropped for lack of a link.
886
+ const engineNotes = selected.value.notes ?? [];
887
+ if (engineNotes.length)
888
+ fallbackNote = (fallbackNote ? fallbackNote + '\n' : '') + engineNotes.join('\n');
717
889
  }
718
890
  catch (error) {
719
891
  if (signal?.aborted)
@@ -743,8 +915,11 @@ export class SearchRouter {
743
915
  engine: platform ? viaId ?? finalId : finalId,
744
916
  status: 'ok',
745
917
  cacheKey,
746
- detail: JSON.stringify({ ...finalOutcome.content ? { content: finalOutcome.content } : {}, engine: finalId, enginesTried, requestedCount: count, ...fallbackNote ? { fallbackNote } : {} }),
747
- }, persistExtras ? finalOutcome.sources.map((source, i) => ({ ...source, extra: persistExtras[i] })) : finalOutcome.sources, platform ? 'platform-' + platform.id : finalId));
918
+ detail: JSON.stringify({ ...finalOutcome.content ? { content: finalOutcome.content } : {}, engine: finalId, ...backendId ? { backend: backendId } : {}, enginesTried, requestedCount: count, ...fallbackNote ? { fallbackNote } : {} }),
919
+ }, finalOutcome.sources.map((source, i) => {
920
+ const extra = persistExtras ? persistExtras[i] : JSON.stringify(originExtra({ ...source.via ? { via: source.via } : {}, ...source.tag ? { tag: source.tag } : {}, ...source.confidence !== undefined ? { confidence: source.confidence } : {} }));
921
+ return extra === '{}' ? source : { ...source, extra };
922
+ }), platform ? 'platform-' + platform.id : finalId));
748
923
  const result = {
749
924
  ...outcome.content ? { content: outcome.content } : {},
750
925
  sources: shapeSources(outcome.sources, count),
@@ -752,6 +927,7 @@ export class SearchRouter {
752
927
  enginesTried,
753
928
  fromCache: false,
754
929
  availableCount,
930
+ ...backendId ? { backend: backendId } : {},
755
931
  ...fallbackNote ? { fallbackNote } : {},
756
932
  };
757
933
  // 4. Warm the in-process LRU (memory-only; survives across SQLite hits).