dsh-web-search-pro 0.2.0 → 0.2.1

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 (80) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +75 -6
  3. package/assets/skills/dsh-web-search-pro/SKILL.md +1 -1
  4. package/assets/skills/dsh-web-search-pro/references/sources.md +11 -2
  5. package/catalog/sources.v1.json +20 -12
  6. package/lib/actions/search.js +5 -4
  7. package/lib/actions/search.js.map +1 -1
  8. package/lib/actions/sources.js +42 -25
  9. package/lib/actions/sources.js.map +1 -1
  10. package/lib/cli/builtin-specs.js +306 -0
  11. package/lib/cli/builtin-specs.js.map +1 -0
  12. package/lib/cli/chain.js +192 -0
  13. package/lib/cli/chain.js.map +1 -0
  14. package/lib/cli/chains-spec.js +98 -0
  15. package/lib/cli/chains-spec.js.map +1 -0
  16. package/lib/cli/engine.js +83 -0
  17. package/lib/cli/engine.js.map +1 -0
  18. package/lib/cli/opencli.js +105 -0
  19. package/lib/cli/opencli.js.map +1 -0
  20. package/lib/cli/probe.js +128 -0
  21. package/lib/cli/probe.js.map +1 -0
  22. package/lib/cli/runner.js +374 -0
  23. package/lib/cli/runner.js.map +1 -0
  24. package/lib/cli/spec.js +416 -0
  25. package/lib/cli/spec.js.map +1 -0
  26. package/lib/client/SettingsCard.js +1 -1
  27. package/lib/client/SettingsCard.js.map +1 -1
  28. package/lib/client/form-specs.js +2 -0
  29. package/lib/client/form-specs.js.map +1 -1
  30. package/lib/client/form.js +14 -3
  31. package/lib/client/form.js.map +1 -1
  32. package/lib/client/locales.js +4 -0
  33. package/lib/client/locales.js.map +1 -1
  34. package/lib/client/validators.js +15 -0
  35. package/lib/client/validators.js.map +1 -1
  36. package/lib/client.js +1425 -3
  37. package/lib/client.js.map +1 -1
  38. package/lib/config.js +4 -0
  39. package/lib/config.js.map +1 -1
  40. package/lib/deps.js +109 -102
  41. package/lib/deps.js.map +1 -1
  42. package/lib/engine-error.js +19 -0
  43. package/lib/engine-error.js.map +1 -0
  44. package/lib/engines.js +31 -114
  45. package/lib/engines.js.map +1 -1
  46. package/lib/pipeline/candidates.js.map +1 -1
  47. package/lib/pipeline/run.js +4 -2
  48. package/lib/pipeline/run.js.map +1 -1
  49. package/lib/providers/builtin.js +10 -14
  50. package/lib/providers/builtin.js.map +1 -1
  51. package/lib/providers/platforms.js +114 -55
  52. package/lib/providers/platforms.js.map +1 -1
  53. package/lib/providers/registry.js.map +1 -1
  54. package/lib/router.js +63 -6
  55. package/lib/router.js.map +1 -1
  56. package/lib/types/cli/builtin-specs.d.ts +30 -0
  57. package/lib/types/cli/chain.d.ts +83 -0
  58. package/lib/types/cli/chains-spec.d.ts +39 -0
  59. package/lib/types/cli/engine.d.ts +43 -0
  60. package/lib/types/cli/opencli.d.ts +38 -0
  61. package/lib/types/cli/probe.d.ts +53 -0
  62. package/lib/types/cli/runner.d.ts +87 -0
  63. package/lib/types/cli/spec.d.ts +176 -0
  64. package/lib/types/client/form-specs.d.ts +1 -1
  65. package/lib/types/client/locales.d.ts +4 -0
  66. package/lib/types/client/validators.d.ts +6 -0
  67. package/lib/types/config.d.ts +11 -0
  68. package/lib/types/deps.d.ts +25 -23
  69. package/lib/types/engine-error.d.ts +12 -0
  70. package/lib/types/engines.d.ts +16 -9
  71. package/lib/types/index.d.ts +4 -0
  72. package/lib/types/pipeline/candidates.d.ts +2 -0
  73. package/lib/types/pipeline/run.d.ts +2 -1
  74. package/lib/types/providers/platforms.d.ts +15 -3
  75. package/lib/types/providers/registry.d.ts +3 -0
  76. package/lib/types/router.d.ts +13 -0
  77. package/lib/types/util.d.ts +6 -1
  78. package/lib/util.js +7 -3
  79. package/lib/util.js.map +1 -1
  80. package/package.json +2 -2
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Declarative CLI adapter spec (dev-plan M12): what a standalone command-line tool is, which of its commands the plugin
3
+ * may run, and how its output maps to search sources. Pure data plus validation, no Node imports, so the settings card
4
+ * (client bundle) validates `cliAdapters` with the very function the server uses.
5
+ *
6
+ * The read-only guarantee lives here and applies to built-in and user-defined specs alike:
7
+ * - argv is an array of tokens, never a shell string (the runner spawns without a shell);
8
+ * - a spec declares `allowedSubcommands`, and every command word its argv uses (the leading literal tokens before the
9
+ * first flag or placeholder) must be in that list;
10
+ * - no command word, declared subcommand or flag may match the global deny-list (login, post, delete*, like, ...,
11
+ * and the flags that make a CLI read browser cookies or carry secrets in argv).
12
+ * @module web-search-pro/cli/spec
13
+ */
14
+ export declare const OUTPUT_FORMATS: readonly ["json", "ndjson", "yaml", "text"];
15
+ export type OutputFormat = typeof OUTPUT_FORMATS[number];
16
+ /** How far a spec has been checked against the real tool (recorded in the spec and shown in `sources.status`). */
17
+ export declare const CLI_VERIFICATIONS: readonly ["live", "contract-only", "docs-only"];
18
+ export type CliVerification = typeof CLI_VERIFICATIONS[number];
19
+ export declare const CLI_ERROR_CODES: readonly ["CLI_NOT_FOUND", "CLI_CONTRACT_MISMATCH", "CLI_NOT_LOGGED_IN", "ENGINE_EMPTY", "CLI_FAILED"];
20
+ export type CliErrorCode = typeof CLI_ERROR_CODES[number];
21
+ /** One piece of a joined field: a path, optionally with text put before / after the value. */
22
+ export type JoinPart = string | {
23
+ path: string;
24
+ prefix?: string;
25
+ suffix?: string;
26
+ };
27
+ /** `{ template }` fills `{path}` placeholders from the item and yields nothing when one is empty; `when` makes it apply to one kind of item. */
28
+ export interface TemplateSource {
29
+ template: string;
30
+ when?: {
31
+ path: string;
32
+ equals: string | number | boolean;
33
+ };
34
+ }
35
+ /**
36
+ * Where a field of a source comes from, for one parsed item:
37
+ * - a dotted path (`data.title`, `items.0.url`);
38
+ * - a list of paths and templates: the first non-empty one wins;
39
+ * - `{ template }`: `{path}` placeholders over the item, dropped when any of them is empty;
40
+ * - `{ join }`: the non-empty parts joined with `sep` (default ` | `);
41
+ * - `{ epoch }`: a Unix timestamp (seconds) as an ISO date; `{ date }`: the date part (first 10 characters) of an ISO timestamp.
42
+ */
43
+ export type FieldSource = string | readonly (string | TemplateSource)[] | TemplateSource | {
44
+ join: readonly JoinPart[];
45
+ sep?: string;
46
+ } | {
47
+ epoch: string;
48
+ } | {
49
+ date: string;
50
+ };
51
+ export interface CliOutputSpec {
52
+ format: OutputFormat;
53
+ /** json / yaml: dotted path of the item array; absent = the root array, else the first array-valued property. */
54
+ itemsPath?: string;
55
+ /** json / yaml: every entry must hold, else the output is a failure (a versioned envelope's `ok` and `schema_version`). */
56
+ expect?: readonly {
57
+ path: string;
58
+ equals: string | number | boolean;
59
+ }[];
60
+ /** Paths of the tool's own error text, read when `expect` fails (first non-empty one). */
61
+ errorPaths?: readonly string[];
62
+ /** text: how a line becomes an item (`tsv`: tab separated columns; `url-lines`: lines holding a URL, `url` + `title`). */
63
+ lines?: {
64
+ mode: 'tsv';
65
+ columns: readonly string[];
66
+ } | {
67
+ mode: 'url-lines';
68
+ };
69
+ fields: {
70
+ url: FieldSource;
71
+ title?: FieldSource;
72
+ snippet?: FieldSource;
73
+ publishedAt?: FieldSource;
74
+ };
75
+ /** Strip HTML tags from title and snippet. */
76
+ stripTags?: boolean;
77
+ /** Values read as "empty" (`None`). */
78
+ emptyValues?: readonly string[];
79
+ /** Longest snippet kept (default 400). */
80
+ snippetMax?: number;
81
+ /** Longest title kept (default 300). */
82
+ titleMax?: number;
83
+ /** Drop items without a title (a line that is not a result). */
84
+ requireTitle?: boolean;
85
+ }
86
+ export interface CliSearchSpec {
87
+ /** Arguments after the binary. `{query}` and `{count}` are the placeholders; a token may embed one (`ytsearch{count}:{query}`). */
88
+ argv: readonly string[];
89
+ /** Largest `{count}` sent (the search call's count is clamped to 1..maxCount). */
90
+ maxCount?: number;
91
+ output: CliOutputSpec;
92
+ /** Case-insensitive fragments of the tool's output that mean "no results" rather than a failure. */
93
+ emptyWhen?: readonly string[];
94
+ /** Case-insensitive fragments of the tool's output that mean "not logged in" (maps to CLI_NOT_LOGGED_IN). */
95
+ notLoggedInPatterns?: readonly string[];
96
+ }
97
+ export interface CliProbeSpec {
98
+ versionArgs: readonly string[];
99
+ /** `X.Y.Z` the detected version must reach. */
100
+ minVersion?: string;
101
+ helpArgs: readonly string[];
102
+ /** Fragments the help output must contain: the subcommands and flags that prove this is the tool the spec describes. */
103
+ mustContain: readonly string[];
104
+ }
105
+ export interface CliLoginSpec {
106
+ /** What the user runs themselves (`xhs login`); never run by the plugin. */
107
+ command: string;
108
+ /** Home-relative files that exist once the user has logged in. Only their existence is checked, never their content. */
109
+ savedCredentialPaths?: readonly string[];
110
+ /** The tool reads the browser's cookies by itself when it has no saved login: then it is never run without one. */
111
+ readsBrowserCookiesWithoutLogin?: boolean;
112
+ }
113
+ export interface CliAdapterSpec {
114
+ id: string;
115
+ /** Command names tried in order on PATH. */
116
+ bins: readonly string[];
117
+ /** What the package is called, which can differ from the command (`pip install pyzhihu-cli` runs `zhihu`). */
118
+ packageNote: string;
119
+ /** Platform ids the spec can serve (`search.run platform=`); a user spec's platform is `custom-cli:<id>`. */
120
+ platforms: readonly string[];
121
+ probe: CliProbeSpec;
122
+ /** Every command word the argv templates use. */
123
+ allowedSubcommands: readonly string[];
124
+ search: CliSearchSpec;
125
+ /** Per-platform overrides of `search` (gh serves github, github-issues and github-code with different commands). */
126
+ searches?: Readonly<Record<string, CliSearchSpec>>;
127
+ read?: CliSearchSpec;
128
+ env: {
129
+ required?: readonly string[];
130
+ passthrough?: readonly string[];
131
+ set?: Readonly<Record<string, string>>;
132
+ };
133
+ needsLogin: boolean;
134
+ login?: CliLoginSpec;
135
+ timeoutMs: number;
136
+ maxOutputBytes: number;
137
+ /** Built-ins only: how far the spec was checked. User-defined specs carry none (they are unverified by definition). */
138
+ verification?: {
139
+ status: CliVerification;
140
+ version?: string;
141
+ date?: string;
142
+ note?: string;
143
+ };
144
+ }
145
+ /** Command words that write, log in, or change local or remote state. Matched exactly, or as `<word>-...` / `<word>_...`. */
146
+ export declare const DENIED_SUBCOMMANDS: readonly string[];
147
+ /** Command words denied by prefix alone (`delete`, `delete-note`, `deleteNote`, `remove...`). */
148
+ export declare const DENIED_PREFIXES: readonly string[];
149
+ /** Flags (without leading dashes) that read browser cookies or carry credentials in argv. */
150
+ export declare const DENIED_FLAGS: readonly string[];
151
+ /** The command words of an argv: leading literal tokens before the first flag or placeholder token. */
152
+ export declare function commandWords(argv: readonly string[]): string[];
153
+ /** Problems of one argv template with respect to the read-only guard (empty = fine). */
154
+ export declare function argvProblems(argv: readonly string[], allowed: readonly string[], where: string): string[];
155
+ export interface CliSpecValidation {
156
+ ok: boolean;
157
+ errors: string[];
158
+ spec?: CliAdapterSpec;
159
+ }
160
+ /**
161
+ * Validate one adapter spec (built-in or user-defined). Never throws. A user-defined spec (`origin: 'user'`) may not carry a
162
+ * `verification` (it is unverified by definition) and its `platforms` are ignored: it is registered as `custom-cli:<id>`.
163
+ */
164
+ export declare function validateCliAdapterSpec(raw: unknown, origin?: 'builtin' | 'user'): CliSpecValidation;
165
+ /** Validate the `cliAdapters` setting: the valid specs by id and the problems of the rest (an invalid entry is ignored, never fatal). */
166
+ export declare function resolveCliAdapters(input: unknown): {
167
+ specs: Map<string, CliAdapterSpec>;
168
+ diagnostics: string[];
169
+ };
170
+ /** Search spec of one platform: the platform's override, else the default. */
171
+ export declare function searchSpecFor(spec: CliAdapterSpec, platform: string): CliSearchSpec;
172
+ export declare function compareVersions(left: string, right: string): number;
173
+ /** The first `X.Y.Z` (or `X.Y`) in a tool's version output. */
174
+ export declare function parseVersion(output: string): string | undefined;
175
+ /** `platformBackends`: ordered backend ids per platform. Pure shape check; ids are checked against the known backends by the caller. */
176
+ export declare function platformBackendProblems(input: unknown): string[];
@@ -12,7 +12,7 @@
12
12
  */
13
13
  export type Json = Record<string, unknown>;
14
14
  /** Top-level config fields (one Host write each). */
15
- export type TopField = 'engines' | 'parallelEngines' | 'searchMaxResults' | 'timeoutMs' | 'fetchDefaultChars' | 'exaContentsPerUrlChars' | 'exaContentsTotalChars' | 'exaApiKeyEnv' | 'jinaApiKeyEnv' | 'githubTokenEnv' | 'bochaApiKeyEnv' | 'bochaBaseUrl' | 'bochaSummary' | 'searxngUrl' | 'openalexMailto' | 'enableCliBackends' | 'opencliEnabled' | 'agentReachEnabled' | 'providerId' | 'registerProvider' | 'toolSurface' | 'playwright' | 'ttlSeconds' | 'memoryCacheEntries' | 'rrfConstant' | 'freshnessBoost' | 'freshnessDays' | 'authorityBoost' | 'authorityDomains' | 'dbPath' | 'allowProxyFakeIp' | 'platformRules' | 'customPlatforms' | 'browserBindings' | 'verbose';
15
+ export type TopField = 'engines' | 'parallelEngines' | 'searchMaxResults' | 'timeoutMs' | 'fetchDefaultChars' | 'exaContentsPerUrlChars' | 'exaContentsTotalChars' | 'exaApiKeyEnv' | 'jinaApiKeyEnv' | 'githubTokenEnv' | 'bochaApiKeyEnv' | 'bochaBaseUrl' | 'bochaSummary' | 'searxngUrl' | 'openalexMailto' | 'enableCliBackends' | 'opencliEnabled' | 'agentReachEnabled' | 'providerId' | 'registerProvider' | 'toolSurface' | 'playwright' | 'ttlSeconds' | 'memoryCacheEntries' | 'rrfConstant' | 'freshnessBoost' | 'freshnessDays' | 'authorityBoost' | 'authorityDomains' | 'dbPath' | 'allowProxyFakeIp' | 'platformRules' | 'customPlatforms' | 'browserBindings' | 'platformBackends' | 'cliAdapters' | 'verbose';
16
16
  /** Options inside `evidence` / `provider`, addressed by their dotted config path. */
17
17
  export type PathField = 'evidence.autoProviders' | 'evidence.sourcePolicy' | 'evidence.maxRounds' | 'evidence.maxQueries' | 'evidence.judge.mode' | 'evidence.hybridBorderline' | 'evidence.judge.provider' | 'evidence.maxJevQuestions' | 'evidence.judge.allowLlm' | 'evidence.judge.providers' | 'evidence.coverage.mode' | 'evidence.coverage.provider' | 'evidence.coverage.thresholds.weak' | 'evidence.coverage.thresholds.covered' | 'evidence.budget.perSearchInputTokens' | 'evidence.budget.dailyInputTokens' | 'evidence.budget.timezone' | 'evidence.budget.providers' | 'provider.evidence' | 'provider.deadlineMs' | 'sources.priority' | 'sources.disabled';
18
18
  /** One rubric override entry (`evidence.rubrics.<id>`) and one keyed-source field. */
@@ -72,6 +72,10 @@ export declare const zh: {
72
72
  readonly platformRulesHint: "按平台设置 item/title/link/text 选择器。";
73
73
  readonly customPlatforms: "自定义平台(JSON)";
74
74
  readonly customPlatformsHint: "按平台定义搜索 URL 与选择器;登录态请通过浏览器绑定引用 AuthProfile。";
75
+ readonly platformBackends: "平台后端顺序(JSON)";
76
+ readonly platformBackendsHint: "每个平台依次尝试的后端,如 {\"xiaohongshu\": [\"xhs\", \"opencli\", \"browser-opencli\"]}。id:各 CLI(xhs、rdt、zhihu、twitter、bili、yt-dlp、omnireach、wx-search-cli、gh、tanso)、opencli、browser-opencli、browser-search、rest、custom-cli:<id>。缺失或未登录的后端会被跳过并说明原因。";
77
+ readonly cliAdapters: "自定义 CLI 适配(JSON)";
78
+ readonly cliAdaptersHint: "按 id 定义只读 CLI 适配规格(argv 数组、输出字段、探测契约、allowedSubcommands)。写操作和读取浏览器 Cookie 的参数会被拒绝;无效条目被忽略并在 sources.status 说明。登录始终由你自己运行 CLI。";
75
79
  readonly browserBindings: "浏览器绑定(JSON)";
76
80
  readonly browserBindingsHint: "把平台绑定到 dsh-browser AuthProfile 与 RulePack。";
77
81
  readonly verbose: "详细诊断日志";
@@ -48,3 +48,9 @@ export declare function evidenceIssues(ev: Json): Issue[];
48
48
  export declare function sourcesIssues(sources: Json): Issue[];
49
49
  /** Rubric override ids in a settings object that name no built-in rubric (the server ignores them). */
50
50
  export declare function unknownRubricIds(rubrics: unknown): string[];
51
+ /**
52
+ * Problems of the CLI adapter settings, from the server's own functions: `cliAdapters` entries the server would ignore
53
+ * (read-only guard, bad argv, secrets in env) and `platformBackends` that name an unknown platform or a backend that
54
+ * cannot serve it.
55
+ */
56
+ export declare function cliIssues(platformBackends: unknown, cliAdapters: unknown): Issue[];
@@ -214,6 +214,13 @@ export interface Config {
214
214
  customPlatforms?: Record<string, CustomPlatformSpec>;
215
215
  /** Per-platform binding to domain-scoped dsh-browser auth/rule profiles. */
216
216
  browserBindings?: Record<string, BrowserBinding>;
217
+ /**
218
+ * Ordered backend chain per platform (dev-plan M12): backend ids such as `xhs`, `opencli` (standalone OpenCLI), `browser-opencli`
219
+ * (dsh-browser), `browser-search`, `rest`, `gh`, `custom-cli:<id>`. A platform not listed uses its default chain.
220
+ */
221
+ platformBackends?: Record<string, string[]>;
222
+ /** User-defined CLI adapter specs by id (validated like the built-in ones; invalid entries are ignored with a diagnostic in sources.status). */
223
+ cliAdapters?: Record<string, Record<string, unknown>>;
217
224
  /** Snapshot options. The browser runtime itself (channel/headless/storageStatePath) is provided by the dsh-browser plugin via the `browser` service. */
218
225
  playwright: {
219
226
  /** Gate the playwright fallback backend in read.fetch. */
@@ -313,6 +320,8 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
313
320
  authProfile: z<string, string, "plain">;
314
321
  rulePack: z<string, string, "plain">;
315
322
  }>>, string>>, "volatile">;
323
+ platformBackends: z<NoInfer<import("@deepseek-ai/cosmokit").Dict<string[], string>>, NoInfer<import("@deepseek-ai/cosmokit").Dict<string[], string>>, "volatile">;
324
+ cliAdapters: z<NoInfer<import("@deepseek-ai/cosmokit").Dict<import("@deepseek-ai/cosmokit").Dict<any, string>, string>>, NoInfer<import("@deepseek-ai/cosmokit").Dict<import("@deepseek-ai/cosmokit").Dict<any, string>, string>>, "volatile">;
316
325
  playwright: z<Schemastery.ObjectS<NoInfer<{
317
326
  enabled: z<boolean, boolean, "volatile-defined">;
318
327
  snapshotDir: z<string, string, "plain">;
@@ -771,6 +780,8 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
771
780
  authProfile: z<string, string, "plain">;
772
781
  rulePack: z<string, string, "plain">;
773
782
  }>>, string>>, "volatile">;
783
+ platformBackends: z<NoInfer<import("@deepseek-ai/cosmokit").Dict<string[], string>>, NoInfer<import("@deepseek-ai/cosmokit").Dict<string[], string>>, "volatile">;
784
+ cliAdapters: z<NoInfer<import("@deepseek-ai/cosmokit").Dict<import("@deepseek-ai/cosmokit").Dict<any, string>, string>>, NoInfer<import("@deepseek-ai/cosmokit").Dict<import("@deepseek-ai/cosmokit").Dict<any, string>, string>>, "volatile">;
774
785
  playwright: z<Schemastery.ObjectS<NoInfer<{
775
786
  enabled: z<boolean, boolean, "volatile-defined">;
776
787
  snapshotDir: z<string, string, "plain">;
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * External dependency detection and install for the CLI/platform backends.
3
- * Most backends shell out to tools installed outside DSH (bili, yt-dlp,
4
- * twitter, and mcporter). This module reports which are present and how to
5
- * install them; the sources.deps / sources.install actions expose it to the model. Each entry probes
6
- * the command the backend actually executes (the twitter backend runs
7
- * `twitter`, so finding `agent-reach` on PATH says nothing about it).
3
+ * Backends shell out to tools installed outside DSH: the built-in CLI adapter specs (bili, yt-dlp, twitter, xhs, zhihu,
4
+ * rdt, omnireach, gh, wx-search-cli, tanso, the standalone opencli) plus the user's own `cliAdapters`, and mcporter. This
5
+ * module reports which are present, whether the command on PATH really has the contract the adapter needs (a same-named
6
+ * program with another contract is `incompatible`, never `detected`), and how to install them; the sources.deps /
7
+ * sources.install actions expose it to the model. Probes are local, cached with a TTL, and never run a search or a login.
8
8
  *
9
9
  * Install is intentionally a MODEL-FACING TOOL, not a browser settings button:
10
10
  * a browser button running winget/pip/npm would be arbitrary command execution
@@ -12,6 +12,9 @@
12
12
  * tool-permission/approval pipeline. The card points at this tool instead.
13
13
  * @module web-search-pro/deps
14
14
  */
15
+ import { BILI_CLI_INSTALLS, BILI_CLI_REVISION, BILI_CLI_SOURCE, BILI_CLI_VERSION, TWITTER_CLI_INSTALLS } from './cli/builtin-specs.ts';
16
+ import { type CliInstallation } from './cli/probe.ts';
17
+ export { BILI_CLI_INSTALLS, BILI_CLI_REVISION, BILI_CLI_SOURCE, BILI_CLI_VERSION, TWITTER_CLI_INSTALLS };
15
18
  export interface DepInfo {
16
19
  id: string;
17
20
  label: string;
@@ -27,6 +30,10 @@ export interface DepInfo {
27
30
  version?: string;
28
31
  /** Why a command found on PATH is not compatible. */
29
32
  diagnostic?: string;
33
+ /** `missing` (not on PATH), `detected` (present with the contract the adapter needs), `incompatible` (present, wrong contract or too old). */
34
+ installation?: CliInstallation;
35
+ /** How far the adapter was checked against the real tool: `live`, `contract-only`, `docs-only` (absent: user-defined or not a CLI adapter). */
36
+ verification?: string;
30
37
  /** No backend of this plugin executes it; it is only an install helper, so its absence is not a gap. */
31
38
  optional?: boolean;
32
39
  installs: {
@@ -34,31 +41,26 @@ export interface DepInfo {
34
41
  command: string;
35
42
  }[];
36
43
  }
37
- export declare const BILI_CLI_VERSION = "0.6.2";
38
- export declare const BILI_CLI_REVISION = "489607468f967e0e11f3cdff6efc022d011e982a";
39
- export declare const BILI_CLI_SOURCE = "git+https://github.com/public-clis/bilibili-cli@489607468f967e0e11f3cdff6efc022d011e982a";
40
- export declare const BILI_CLI_INSTALLS: {
41
- installer: string;
42
- command: string;
43
- }[];
44
- interface DepProbeResult {
44
+ interface ProbeResult {
45
45
  available: boolean;
46
46
  version?: string;
47
47
  diagnostic?: string;
48
48
  }
49
49
  /** Validate the public-clis bili command rather than trusting an ambiguous package name. */
50
- export declare function evaluateBiliCli(versionOutput: string, searchHelpOutput: string): DepProbeResult;
50
+ export declare function evaluateBiliCli(versionOutput: string, searchHelpOutput: string): ProbeResult;
51
51
  /**
52
52
  * The twitter backend runs `twitter search <query> -n N` (twitter-cli). Another program that happens to be
53
53
  * called `twitter` must not pass: require a successful `search --help` that actually describes a search command.
54
54
  */
55
- export declare function evaluateTwitterCli(searchHelpOutput: string, exitCode: number): DepProbeResult;
56
- export declare const TWITTER_CLI_INSTALLS: {
57
- installer: string;
58
- command: string;
59
- }[];
60
- /** Detect all backends. */
61
- export declare function detectDeps(): Promise<DepInfo[]>;
55
+ export declare function evaluateTwitterCli(searchHelpOutput: string, exitCode: number): ProbeResult;
56
+ export declare const DEP_IDS: readonly string[];
57
+ /** Detect every dependency: the spec-backed CLIs through their cached contract probes, the rest by presence on PATH. */
58
+ export declare function detectDeps(options?: {
59
+ config?: {
60
+ cliAdapters?: unknown;
61
+ };
62
+ force?: boolean;
63
+ }): Promise<DepInfo[]>;
62
64
  /** Run the install command for one backend + installer. */
63
65
  export declare function installDep(id: string, installer: string): Promise<{
64
66
  code: number;
@@ -66,5 +68,5 @@ export declare function installDep(id: string, installer: string): Promise<{
66
68
  stderr: string;
67
69
  timedOut: boolean;
68
70
  }>;
69
- export declare const DEP_IDS: string[];
70
- export {};
71
+ /** The installer `sources.install` uses when none is named: the first one listed for the dependency. */
72
+ export declare function defaultInstaller(id: string): string;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The error every search engine and CLI adapter throws. Leaf module (no imports), so both engines.ts and the CLI adapter
3
+ * layer can extend it without an import cycle.
4
+ * @module web-search-pro/engine-error
5
+ */
6
+ export declare class EngineError extends Error {
7
+ readonly code: string;
8
+ readonly retryable: boolean;
9
+ readonly retryAfterMs?: number | undefined;
10
+ /** `retryAfterMs`: the service's own wait hint (Retry-After); the router uses it as the cooldown. */
11
+ constructor(message: string, code: string, retryable?: boolean, retryAfterMs?: number | undefined);
12
+ }
@@ -10,16 +10,23 @@ import type { BrowserService } from './browser-service.ts';
10
10
  import { type BrowserMethod } from './browser-access.ts';
11
11
  import type { CustomPlatformSpec } from './config.ts';
12
12
  import { type ExaSearchRequest } from './exa-client.ts';
13
+ import { EngineError } from './engine-error.ts';
13
14
  export interface SearchOutcome {
14
15
  /** Provider-generated answer/summary text, when any. */
15
16
  content?: string;
16
17
  sources: WebSearchSource[];
17
18
  /** Id of the concrete backend that answered (a platform provider with a fallback chain names the leg that ran). */
18
19
  via?: string;
20
+ /** The backend id of the platform chain that answered (`bili`, `browser-opencli`), for display and history. */
21
+ backend?: string;
22
+ /** Short notes for the caller: backends skipped on the way, results dropped. */
23
+ notes?: string[];
19
24
  }
20
25
  export interface Engine {
21
26
  id: string;
22
27
  label: string;
28
+ /** Backend id in a platform chain (`xhs`, `browser-opencli`); absent for plain engines. */
29
+ backend?: string;
23
30
  /** Cheap local availability check; must not do network I/O. */
24
31
  available(): boolean;
25
32
  /** Browser-service method this engine depends on (dsh-browser is optional). */
@@ -50,13 +57,7 @@ export interface EngineSearchOptions {
50
57
  /** Feed URL of the `rss` platform (it has no fixed endpoint). */
51
58
  url?: string;
52
59
  }
53
- export declare class EngineError extends Error {
54
- readonly code: string;
55
- readonly retryable: boolean;
56
- readonly retryAfterMs?: number | undefined;
57
- /** `retryAfterMs`: the service's own wait hint (Retry-After); the router uses it as the cooldown. */
58
- constructor(message: string, code: string, retryable?: boolean, retryAfterMs?: number | undefined);
59
- }
60
+ export { EngineError };
60
61
  /** One metered request of a non-model provider (Bocha search): counted in the usage ledger, tokens n/a, price unknown. */
61
62
  export interface UsageRecorder {
62
63
  record(entry: {
@@ -140,13 +141,19 @@ export declare function githubEngine(deps: EngineDeps): Engine;
140
141
  export declare function githubCodeEngine(deps: EngineDeps): Engine;
141
142
  export declare function githubIssuesEngine(deps: EngineDeps): Engine;
142
143
  export declare function bilibiliEngine(deps: EngineDeps): Engine;
143
- /** Exact argv contract supported by public-clis/bilibili-cli v0.6.2+. */
144
+ /** Exact argv contract supported by public-clis/bilibili-cli v0.6.2+ (the `bili` spec). */
144
145
  export declare function biliSearchArgs(query: string, count: number): string[];
145
- /** Parse and validate bili-cli's versioned JSON envelope. */
146
+ /** Parse and validate bili-cli's versioned JSON envelope (the `bili` spec). */
146
147
  export declare function parseBilibiliSearchOutput(output: string): WebSearchSource[];
147
148
  export declare function v2exEngine(allowProxyFakeIp?: boolean): Engine;
148
149
  export declare function youtubeEngine(deps: EngineDeps, cli?: typeof runCli): Engine;
150
+ /** Platforms the dsh-browser OpenCLI bridge has a site adapter for (platform -> adapter name). */
151
+ export declare const OPENCLI_PLATFORMS: Record<string, string>;
149
152
  export declare function opencliEngine(platform: string, deps: EngineDeps): Engine;
153
+ /**
154
+ * The legacy `agentreach-twitter` engine, now a thin view of the `twitter` CLI spec (the platform chain uses the spec
155
+ * directly; this keeps the old engine id and its install message). Credentials are checked by `available()`.
156
+ */
150
157
  export declare function agentReachEngine(platform: string, deps: EngineDeps): Engine;
151
158
  export declare function arxivEngine(allowProxyFakeIp?: boolean): Engine;
152
159
  export declare function pubmedEngine(allowProxyFakeIp?: boolean): Engine;
@@ -114,6 +114,8 @@ declare const _default: {
114
114
  authProfile: import("@deepseek-ai/schemastery").default<string, string, "plain">;
115
115
  rulePack: import("@deepseek-ai/schemastery").default<string, string, "plain">;
116
116
  }>>, string>>, "volatile">;
117
+ platformBackends: import("@deepseek-ai/schemastery").default<NoInfer<import("@deepseek-ai/cosmokit").Dict<string[], string>>, NoInfer<import("@deepseek-ai/cosmokit").Dict<string[], string>>, "volatile">;
118
+ cliAdapters: import("@deepseek-ai/schemastery").default<NoInfer<import("@deepseek-ai/cosmokit").Dict<import("@deepseek-ai/cosmokit").Dict<any, string>, string>>, NoInfer<import("@deepseek-ai/cosmokit").Dict<import("@deepseek-ai/cosmokit").Dict<any, string>, string>>, "volatile">;
117
119
  playwright: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<NoInfer<{
118
120
  enabled: import("@deepseek-ai/schemastery").default<boolean, boolean, "volatile-defined">;
119
121
  snapshotDir: import("@deepseek-ai/schemastery").default<string, string, "plain">;
@@ -572,6 +574,8 @@ declare const _default: {
572
574
  authProfile: import("@deepseek-ai/schemastery").default<string, string, "plain">;
573
575
  rulePack: import("@deepseek-ai/schemastery").default<string, string, "plain">;
574
576
  }>>, string>>, "volatile">;
577
+ platformBackends: import("@deepseek-ai/schemastery").default<NoInfer<import("@deepseek-ai/cosmokit").Dict<string[], string>>, NoInfer<import("@deepseek-ai/cosmokit").Dict<string[], string>>, "volatile">;
578
+ cliAdapters: import("@deepseek-ai/schemastery").default<NoInfer<import("@deepseek-ai/cosmokit").Dict<import("@deepseek-ai/cosmokit").Dict<any, string>, string>>, NoInfer<import("@deepseek-ai/cosmokit").Dict<import("@deepseek-ai/cosmokit").Dict<any, string>, string>>, "volatile">;
575
579
  playwright: import("@deepseek-ai/schemastery").default<Schemastery.ObjectS<NoInfer<{
576
580
  enabled: import("@deepseek-ai/schemastery").default<boolean, boolean, "volatile-defined">;
577
581
  snapshotDir: import("@deepseek-ai/schemastery").default<string, string, "plain">;
@@ -17,6 +17,8 @@ export interface ProviderSource {
17
17
  /** One provider's ordered result list for one query (index 0 = rank 1). */
18
18
  export interface ProviderOutput {
19
19
  providerId: string;
20
+ /** The backend of a platform chain that answered (display only). */
21
+ backend?: string;
20
22
  query: string;
21
23
  sources: readonly ProviderSource[];
22
24
  }
@@ -43,7 +43,8 @@ export interface ProviderCall {
43
43
  }
44
44
  export type ProviderOutcome = {
45
45
  state: 'ok';
46
- sources: readonly ProviderSource[];
46
+ sources: readonly ProviderSource[]; /** The backend of a platform chain that answered. */
47
+ backend?: string;
47
48
  }
48
49
  /** `detail` carries the provider's own explanation of an empty answer (e.g. a login hint), when it gave one. */
49
50
  | {
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Platform search backends as registry providers (dev-plan M8b): one provider per site or community, with the same
3
3
  * descriptor + adapter shape as the web engines, so a platform is planned, probed, cooled down, singleflighted,
4
- * persisted and reported exactly like any other source. A platform with several backends (twitter: OpenCLI, then
5
- * twitter-cli) is ONE provider with an internal ordered chain. User `customPlatforms` become providers too
4
+ * persisted and reported exactly like any other source. A platform with several backends (twitter: twitter-cli, standalone
5
+ * OpenCLI, dsh-browser OpenCLI) is ONE provider with an internal ordered chain, resolved by src/cli/chain.ts (dev-plan M12). User `customPlatforms` become providers too
6
6
  * ({@link customPlatformAdapter}); the router registers and unregisters them as the settings change.
7
7
  * The legacy platform ids that already were web providers (github, bilibili, v2ex, youtube, arxiv, pubmed) are
8
8
  * declared in builtin.ts and carry `kind: 'platform'` as well.
@@ -10,9 +10,15 @@
10
10
  */
11
11
  import { type Engine } from '../engines.ts';
12
12
  import type { CustomPlatformSpec } from '../config.ts';
13
- import type { ProviderAdapter } from './registry.ts';
13
+ import { type CliAdapterSpec } from '../cli/spec.ts';
14
+ import type { ProviderAdapter, ProviderDescriptor } from './registry.ts';
14
15
  /** Chain `engines` into one provider: the first available leg that answers wins; empty and failing legs fall through in order. */
15
16
  export declare function chainEngine(id: string, label: string, chain: readonly Engine[]): Engine;
17
+ /**
18
+ * A platform provider whose backends are an ordered chain (`resolveChain`): the engine is the chain, the local probe is the
19
+ * readiness of every leg (cached contract probes, no search, no login), and `chain` reports each leg for `sources.status`.
20
+ */
21
+ export declare function chainProvider(platformId: string, d: ProviderDescriptor): ProviderAdapter;
16
22
  export declare const platformAdapters: readonly ProviderAdapter[];
17
23
  /** Registry id of a custom platform: its key when that is a valid id, else a stable hash of it (the key stays the route alias). */
18
24
  export declare function customProviderId(key: string): string;
@@ -23,3 +29,9 @@ export declare function customKeyProblem(key: string): string | undefined;
23
29
  * reaches the next call even before the router re-registers the provider.
24
30
  */
25
31
  export declare function customPlatformAdapter(key: string, spec: CustomPlatformSpec): ProviderAdapter;
32
+ /** Registry id and route id of a user-defined CLI adapter: `custom-cli:<id>` (also its backend id in `platformBackends`). */
33
+ export declare const customCliProviderId: (id: string) => string;
34
+ /** A user-defined CLI adapter as a platform provider whose chain is the adapter itself (an edit of `platformBackends` can still reorder it). */
35
+ export declare function customCliAdapter(id: string, spec: CliAdapterSpec): ProviderAdapter;
36
+ /** A user spec as validated for registration: the problem text, or undefined when it can be registered. */
37
+ export declare function customCliProblem(id: string, raw: unknown): string | undefined;
@@ -17,6 +17,7 @@ import type { BrowserMethod } from '../browser-access.ts';
17
17
  import type { ResolvedConfig } from '../config.ts';
18
18
  import type { CompiledQuery } from '../pipeline/compile.ts';
19
19
  import type { TaskSpec } from '../pipeline/types.ts';
20
+ import type { ChainReport } from '../cli/chain.ts';
20
21
  export type Operation = 'search';
21
22
  /** `web`: a general search engine; `platform`: one site or community (addressable as `search.run platform=<route id>`). */
22
23
  export type ProviderKind = 'web' | 'platform';
@@ -126,6 +127,8 @@ export interface ProviderAdapter {
126
127
  /** Local readiness: declared services, keys, commands. No network, no login, no model call. */
127
128
  probeLocal(env: ProbeEnv): Readiness | Promise<Readiness>;
128
129
  create(deps: EngineDeps, config: ResolvedConfig): Engine;
130
+ /** Platforms with an ordered backend chain (dev-plan M12): the state of every leg for `sources.status`. Local, no search. */
131
+ chain?(env: ProbeEnv): Promise<ChainReport>;
129
132
  /** Optional native query compilation (constraints -> provider options); absent = the core compiler's rules / plain query. */
130
133
  compile?(task: Pick<TaskSpec, 'goal' | 'query' | 'needs' | 'constraints'>, now: Date): CompiledQuery;
131
134
  }
@@ -8,6 +8,7 @@ import type { WebSearchRequest, WebSearchResult } from '@deepseek-ai/dsh-web';
8
8
  import type { Store } from './store.ts';
9
9
  import type { ResolvedConfig } from './config.ts';
10
10
  import { type EngineSearchOptions } from './engines.ts';
11
+ import type { ChainEntryReport, ChainReport } from './cli/chain.ts';
11
12
  import { type CostTier, type ProviderDescriptor, type Readiness, type ProviderRegistry } from './providers/index.ts';
12
13
  import { type RequestBudgetState } from './pipeline/ledger.ts';
13
14
  import { LruCache } from './memory-cache.ts';
@@ -61,6 +62,8 @@ export interface RouterSearchResult {
61
62
  availableCount?: number;
62
63
  /** Human-readable explanation of why the router fell back to `engine` (P1-1). */
63
64
  fallbackNote?: string;
65
+ /** Platform search: the backend of its chain that answered (`bili`, `browser-opencli`). */
66
+ backend?: string;
64
67
  }
65
68
  /** One provider as `sources.status` reports it: the registry descriptor plus local readiness by dimension. */
66
69
  export interface ProviderReport {
@@ -92,7 +95,10 @@ export interface ProviderReport {
92
95
  budget?: RequestBudgetState;
93
96
  /** Not verified against the live service (descriptor.verification). */
94
97
  unverified?: boolean;
98
+ /** Platforms with an ordered backend chain: each backend, in order, with its readiness or the reason it is skipped. */
99
+ chain?: ChainEntryReport[];
95
100
  readiness: Readiness & {
101
+ state?: ChainReport['state'];
96
102
  lastLocalCheck: string;
97
103
  lastRemoteSuccess?: string;
98
104
  lastError?: string;
@@ -115,6 +121,9 @@ export declare class SearchRouter {
115
121
  private syncedRevision;
116
122
  /** 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. */
117
123
  private readonly custom;
124
+ /** User-defined CLI adapters (settings `cliAdapters`) this router registered as `custom-cli:<id>` platforms, with the diagnostics of the entries it ignored. */
125
+ private readonly customCli;
126
+ private cliAdapterDiagnostics;
118
127
  /** Latest local probe per route id (read by providerStatuses for the credential dimension). */
119
128
  private readonly readiness;
120
129
  /** Last real call per route id: feeds the health dimension (never inferred from a local probe). */
@@ -128,6 +137,10 @@ export declare class SearchRouter {
128
137
  * with a provider already registered is not registered (a user platform never replaces a built-in source); see {@link customPlatformProblems}.
129
138
  */
130
139
  private syncCustomPlatforms;
140
+ /** The same for `cliAdapters`: valid specs become `custom-cli:<id>` platforms; an invalid entry is ignored and reported. */
141
+ private syncCustomCliAdapters;
142
+ /** Configuration problems of the CLI adapter layer for `sources.status`: ignored `cliAdapters` entries and `platformBackends` that name nothing usable. */
143
+ cliAdapterProblems(): string[];
131
144
  /** Custom platforms the registry could not take (key clash, bad key), for `sources.status`. */
132
145
  customPlatformProblems(): string[];
133
146
  /** Unregister what this router put into the registry (plugin unload). */
@@ -76,6 +76,10 @@ export interface CliResult {
76
76
  stdout: string;
77
77
  stderr: string;
78
78
  timedOut: boolean;
79
+ /** The command could not be started at all (not on PATH, not executable). */
80
+ spawnFailed?: boolean;
81
+ /** Output went past `maxOutput` and the rest was dropped. */
82
+ truncated?: boolean;
79
83
  }
80
84
  /**
81
85
  * Run an external CLI (opencli / bili / yt-dlp / agent-reach / npm).
@@ -85,7 +89,8 @@ export interface CliResult {
85
89
  export declare function runCli(bin: string, args: string[], opts?: {
86
90
  timeoutMs?: number;
87
91
  signal: AbortSignal | undefined;
88
- env?: Record<string, string>;
92
+ env?: Record<string, string>; /** Run with exactly this environment instead of extending the process environment (credentials are not inherited). */
93
+ cleanEnv?: boolean;
89
94
  cwd?: string;
90
95
  maxOutput?: number;
91
96
  outputEncoding?: string;
package/lib/util.js CHANGED
@@ -199,6 +199,8 @@ export function runCli(bin, args, opts = { signal: undefined }) {
199
199
  let stderrBytes = 0;
200
200
  let child;
201
201
  let settled = false;
202
+ let spawnFailed = false;
203
+ let truncated = false;
202
204
  const finish = (code, timedOut) => {
203
205
  if (settled)
204
206
  return;
@@ -210,7 +212,7 @@ export function runCli(bin, args, opts = { signal: undefined }) {
210
212
  const encoding = opts.outputEncoding ?? 'utf-8';
211
213
  const stdout = new TextDecoder(encoding).decode(Buffer.concat(stdoutChunks, stdoutBytes));
212
214
  const stderr = new TextDecoder(encoding).decode(Buffer.concat(stderrChunks, stderrBytes));
213
- resolve({ code, stdout, stderr, timedOut });
215
+ resolve({ code, stdout, stderr, timedOut, ...spawnFailed ? { spawnFailed } : {}, ...truncated ? { truncated } : {} });
214
216
  };
215
217
  const timer = opts.timeoutMs ? setTimeout(() => finish(-1, true), opts.timeoutMs) : undefined;
216
218
  const onAbort = () => {
@@ -219,12 +221,14 @@ export function runCli(bin, args, opts = { signal: undefined }) {
219
221
  finish(-1, false);
220
222
  };
221
223
  child = crossSpawn(bin, args, {
222
- env: { ...process.env, ...opts.env },
224
+ env: opts.cleanEnv ? { ...opts.env } : { ...process.env, ...opts.env },
223
225
  cwd: opts.cwd,
224
226
  windowsHide: process.platform === 'win32',
225
227
  });
226
228
  child.stdout?.on('data', (d) => {
227
229
  const remaining = maxOutput - stdoutBytes;
230
+ if (d.length > remaining)
231
+ truncated = true;
228
232
  if (remaining <= 0)
229
233
  return;
230
234
  const chunk = d.length <= remaining ? d : d.subarray(0, remaining);
@@ -239,7 +243,7 @@ export function runCli(bin, args, opts = { signal: undefined }) {
239
243
  stderrChunks.push(chunk);
240
244
  stderrBytes += chunk.length;
241
245
  });
242
- child.on('error', () => finish(-1, false));
246
+ child.on('error', () => { spawnFailed = true; finish(-1, false); });
243
247
  child.on('close', (code) => finish(code ?? -1, false));
244
248
  if (opts.signal?.aborted)
245
249
  onAbort();