deepseek-web-retrieval 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +93 -244
- package/README.md +16 -11
- package/dist/cli.js +1366 -615
- package/docs/agents/codex.md +28 -7
- package/docs/agents/opencode-v2.md +1 -1
- package/docs/agents/pi.md +88 -86
- package/docs/design/installer-design.md +72 -9
- package/docs/hosts.jsonc +2 -2
- package/docs/integration.md +14 -9
- package/docs/research/control-matrix.md +48 -13
- package/docs/research/mcp-compatibility.md +9 -4
- package/examples/pi.settings.jsonc +17 -0
- package/index.ts +2 -2
- package/package.json +35 -4
- package/src/core/auth.ts +4 -6
- package/src/core/client.ts +65 -53
- package/src/core/constants.ts +24 -24
- package/src/core/descriptions.ts +30 -6
- package/src/core/domains.ts +56 -67
- package/src/core/env.ts +112 -0
- package/src/core/format.ts +53 -50
- package/src/core/index.ts +9 -9
- package/src/core/jsonc.ts +6 -6
- package/src/core/object.ts +4 -0
- package/src/core/options.ts +20 -25
- package/src/core/search.ts +45 -44
- package/src/core/transport.ts +109 -113
- package/src/core/types.ts +47 -47
- package/src/core/url.ts +20 -20
- package/src/installer/apply.ts +256 -81
- package/src/installer/cli.ts +135 -104
- package/src/installer/detect.ts +46 -31
- package/src/installer/formats/index.ts +26 -28
- package/src/installer/formats/json.ts +81 -77
- package/src/installer/formats/rules.ts +55 -28
- package/src/installer/formats/toml.ts +88 -81
- package/src/installer/formats/value.ts +34 -30
- package/src/installer/hosts/codex.ts +100 -54
- package/src/installer/hosts/index.ts +126 -41
- package/src/installer/hosts/opencode.ts +70 -51
- package/src/installer/hosts/pi.ts +269 -75
- package/src/installer/hosts/shared.ts +19 -14
- package/src/installer/operation-lock.ts +54 -0
- package/src/installer/ownership.ts +129 -0
- package/src/installer/paths.ts +7 -5
- package/src/installer/persistence.ts +66 -0
- package/src/installer/render.ts +29 -29
- package/src/installer/revert.ts +33 -29
- package/src/installer/rulesContent.ts +24 -13
- package/src/installer/state.ts +116 -31
- package/src/installer/types.ts +74 -56
- package/src/installer/uninstall.ts +40 -17
- package/src/mcp/auth.ts +7 -8
- package/src/mcp/cli.ts +38 -20
- package/src/mcp/client.ts +35 -0
- package/src/mcp/server.ts +68 -97
- package/src/opencode/auth.ts +24 -26
- package/src/opencode/plugin.ts +99 -87
- package/src/pi/extension.ts +164 -0
- package/src/pi/settings.ts +49 -0
- package/src/mcp/config.ts +0 -112
package/src/installer/cli.ts
CHANGED
|
@@ -1,154 +1,174 @@
|
|
|
1
|
-
import { homedir } from "node:os"
|
|
2
|
-
import { resolve } from "node:path"
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { renderPlan } from "./render"
|
|
6
|
-
import { listRecords } from "./state"
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
const HOSTS = Object.keys(HOST_PLANNERS) as readonly HostName[]
|
|
11
|
-
const HOST_CHOICES = `<${HOSTS.join("|")}
|
|
12
|
-
const SCOPES: readonly Scope[] = ["project", "global"]
|
|
13
|
-
const TIERS: readonly Tier[] = ["optional", "preferred", "forced", "off"]
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { applyPlan } from "./apply";
|
|
4
|
+
import { HOST_PLANNERS, HOST_VERSION_PROBES, planForHost } from "./hosts";
|
|
5
|
+
import { renderPlan } from "./render";
|
|
6
|
+
import { listRecords } from "./state";
|
|
7
|
+
import type { HostName, InstallerInput, Scope, Tier } from "./types";
|
|
8
|
+
import { uninstall } from "./uninstall";
|
|
9
|
+
|
|
10
|
+
const HOSTS = Object.keys(HOST_PLANNERS) as readonly HostName[];
|
|
11
|
+
const HOST_CHOICES = `<${HOSTS.join("|")}>`;
|
|
12
|
+
const SCOPES: readonly Scope[] = ["project", "global"];
|
|
13
|
+
const TIERS: readonly Tier[] = ["optional", "preferred", "forced", "off"];
|
|
14
14
|
|
|
15
15
|
const USAGE = `Usage:
|
|
16
16
|
deepseek-web-retrieval init --host ${HOST_CHOICES} --scope <project|global> \
|
|
17
17
|
--search <optional|preferred|forced|off> --fetch <optional|preferred|forced|off> \
|
|
18
|
-
[--package <npm ref>] [--cwd <dir>] [--apply]
|
|
18
|
+
[--path <extension|mcp>] [--package <npm ref>] [--cwd <dir>] [--apply]
|
|
19
19
|
deepseek-web-retrieval uninstall --host ${HOST_CHOICES} --scope <project|global> [--cwd <dir>]
|
|
20
20
|
deepseek-web-retrieval list [--cwd <dir>]
|
|
21
21
|
|
|
22
22
|
init prints the full plan (every file, old value → new value) and writes nothing
|
|
23
|
-
until --apply is passed. uninstall restores what the plan applied
|
|
23
|
+
until --apply is passed. uninstall restores what the plan applied. --path picks
|
|
24
|
+
the integration mechanism for hosts with more than one (Pi: extension by
|
|
25
|
+
default, mcp for the stdio MCP server). --package overrides the package ref
|
|
26
|
+
written into host config; the Pi extension path turns it into the packages
|
|
27
|
+
source (a bare name is prefixed with npm:, a ref that already starts with npm:
|
|
28
|
+
is written as-is).`;
|
|
24
29
|
|
|
25
30
|
interface Flags {
|
|
26
|
-
readonly values: Map<string, string
|
|
27
|
-
readonly booleans: Set<string
|
|
28
|
-
readonly problems: readonly string[]
|
|
31
|
+
readonly values: Map<string, string>;
|
|
32
|
+
readonly booleans: Set<string>;
|
|
33
|
+
readonly problems: readonly string[];
|
|
29
34
|
}
|
|
30
35
|
|
|
31
|
-
const VALUE_FLAGS = new Set(["host", "scope", "search", "fetch", "package", "cwd"])
|
|
32
|
-
const BOOLEAN_FLAGS = new Set(["apply"])
|
|
36
|
+
const VALUE_FLAGS = new Set(["host", "scope", "search", "fetch", "path", "package", "cwd"]);
|
|
37
|
+
const BOOLEAN_FLAGS = new Set(["apply"]);
|
|
38
|
+
const PATH_CHOICES: readonly ["extension", "mcp"] = ["extension", "mcp"];
|
|
33
39
|
|
|
34
40
|
function parseFlags(args: readonly string[]): Flags {
|
|
35
|
-
const values = new Map<string, string>()
|
|
36
|
-
const booleans = new Set<string>()
|
|
37
|
-
const problems: string[] = []
|
|
41
|
+
const values = new Map<string, string>();
|
|
42
|
+
const booleans = new Set<string>();
|
|
43
|
+
const problems: string[] = [];
|
|
38
44
|
for (let i = 0; i < args.length; i += 1) {
|
|
39
|
-
const arg = args[i] as string
|
|
45
|
+
const arg = args[i] as string;
|
|
40
46
|
if (!arg.startsWith("--")) {
|
|
41
|
-
problems.push(`unexpected argument "${arg}" (flags only)`)
|
|
42
|
-
continue
|
|
47
|
+
problems.push(`unexpected argument "${arg}" (flags only)`);
|
|
48
|
+
continue;
|
|
43
49
|
}
|
|
44
|
-
const eq = arg.indexOf("=")
|
|
45
|
-
const name = eq === -1 ? arg.slice(2) : arg.slice(2, eq)
|
|
50
|
+
const eq = arg.indexOf("=");
|
|
51
|
+
const name = eq === -1 ? arg.slice(2) : arg.slice(2, eq);
|
|
46
52
|
if (BOOLEAN_FLAGS.has(name)) {
|
|
47
|
-
|
|
48
|
-
|
|
53
|
+
if (eq !== -1) {
|
|
54
|
+
problems.push(`--${name} does not accept a value; use --${name} to enable it or omit it`);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
booleans.add(name);
|
|
58
|
+
continue;
|
|
49
59
|
}
|
|
50
60
|
if (!VALUE_FLAGS.has(name)) {
|
|
51
|
-
problems.push(`unknown flag --${name}`)
|
|
52
|
-
continue
|
|
61
|
+
problems.push(`unknown flag --${name}`);
|
|
62
|
+
continue;
|
|
53
63
|
}
|
|
54
64
|
if (eq !== -1) {
|
|
55
|
-
values.set(name, arg.slice(eq + 1))
|
|
56
|
-
continue
|
|
65
|
+
values.set(name, arg.slice(eq + 1));
|
|
66
|
+
continue;
|
|
57
67
|
}
|
|
58
|
-
const next = args[i + 1]
|
|
68
|
+
const next = args[i + 1];
|
|
59
69
|
if (next === undefined || next.startsWith("--")) {
|
|
60
|
-
problems.push(`--${name} requires a value`)
|
|
61
|
-
continue
|
|
70
|
+
problems.push(`--${name} requires a value`);
|
|
71
|
+
continue;
|
|
62
72
|
}
|
|
63
|
-
values.set(name, next)
|
|
64
|
-
i += 1
|
|
73
|
+
values.set(name, next);
|
|
74
|
+
i += 1;
|
|
65
75
|
}
|
|
66
|
-
return { values, booleans, problems }
|
|
76
|
+
return { values, booleans, problems };
|
|
67
77
|
}
|
|
68
78
|
|
|
69
79
|
/** Where the CLI writes; injectable so tests can assert command output. */
|
|
70
80
|
export interface CliIo {
|
|
71
|
-
readonly out?: (line: string) => void
|
|
72
|
-
readonly err?: (line: string) => void
|
|
81
|
+
readonly out?: (line: string) => void;
|
|
82
|
+
readonly err?: (line: string) => void;
|
|
73
83
|
}
|
|
74
84
|
|
|
75
85
|
/** CLI entry for the init/uninstall/list subcommands; returns the process exit code. */
|
|
76
86
|
export async function runInstallerCli(argv: readonly string[], io: CliIo = {}): Promise<number> {
|
|
77
|
-
const out = io.out ?? ((line: string) => console.log(line))
|
|
78
|
-
const err = io.err ?? ((line: string) => console.error(line))
|
|
79
|
-
const [command, ...rest] = argv
|
|
87
|
+
const out = io.out ?? ((line: string) => console.log(line));
|
|
88
|
+
const err = io.err ?? ((line: string) => console.error(line));
|
|
89
|
+
const [command, ...rest] = argv;
|
|
80
90
|
if (command === undefined || command === "help" || command === "--help") {
|
|
81
|
-
out(USAGE)
|
|
82
|
-
return command === undefined ? 1 : 0
|
|
91
|
+
out(USAGE);
|
|
92
|
+
return command === undefined ? 1 : 0;
|
|
83
93
|
}
|
|
84
|
-
const flags = parseFlags(rest)
|
|
94
|
+
const flags = parseFlags(rest);
|
|
85
95
|
if (flags.problems.length > 0) {
|
|
86
|
-
for (const problem of flags.problems) err(`error: ${problem}`)
|
|
87
|
-
err(`\n${USAGE}`)
|
|
88
|
-
return 1
|
|
96
|
+
for (const problem of flags.problems) err(`error: ${problem}`);
|
|
97
|
+
err(`\n${USAGE}`);
|
|
98
|
+
return 1;
|
|
89
99
|
}
|
|
90
|
-
const cwd = resolve(flags.values.get("cwd") ?? process.cwd())
|
|
91
|
-
const home = process.env.HOME ?? homedir()
|
|
92
|
-
const configHome = resolveConfigHome(home)
|
|
100
|
+
const cwd = resolve(flags.values.get("cwd") ?? process.cwd());
|
|
101
|
+
const home = process.env.HOME ?? homedir();
|
|
102
|
+
const configHome = resolveConfigHome(home);
|
|
93
103
|
|
|
94
104
|
if (command === "list") {
|
|
95
|
-
|
|
105
|
+
let records: ReturnType<typeof listRecords>;
|
|
106
|
+
try {
|
|
107
|
+
records = listRecords({ home, cwd, configHome });
|
|
108
|
+
} catch (error) {
|
|
109
|
+
err(error instanceof Error ? error.message : String(error));
|
|
110
|
+
return 1;
|
|
111
|
+
}
|
|
96
112
|
if (records.length === 0) {
|
|
97
|
-
out("No installs recorded.")
|
|
98
|
-
return 0
|
|
113
|
+
out("No installs recorded.");
|
|
114
|
+
return 0;
|
|
99
115
|
}
|
|
100
116
|
for (const record of records) {
|
|
101
|
-
out(
|
|
102
|
-
|
|
117
|
+
out(
|
|
118
|
+
`${record.host} (${record.scope}) — search=${record.tiers.search}, fetch=${record.tiers.fetch}, installed ${record.createdAt}`,
|
|
119
|
+
);
|
|
120
|
+
for (const file of record.files) out(` ${file.created ? "created" : "modified"} ${file.file}`);
|
|
103
121
|
}
|
|
104
|
-
return 0
|
|
122
|
+
return 0;
|
|
105
123
|
}
|
|
106
124
|
|
|
107
125
|
if (command === "uninstall") {
|
|
108
|
-
const required = requireInput(flags, cwd, home, configHome, false)
|
|
109
|
-
if (!required.ok) return usageError(required.problems, err)
|
|
126
|
+
const required = requireInput(flags, cwd, home, configHome, false);
|
|
127
|
+
if (!required.ok) return usageError(required.problems, err);
|
|
110
128
|
try {
|
|
111
|
-
for (const line of uninstall(required.input)) out(line)
|
|
112
|
-
return 0
|
|
129
|
+
for (const line of uninstall(required.input)) out(line);
|
|
130
|
+
return 0;
|
|
113
131
|
} catch (error) {
|
|
114
|
-
err(error instanceof Error ? error.message : String(error))
|
|
115
|
-
return 1
|
|
132
|
+
err(error instanceof Error ? error.message : String(error));
|
|
133
|
+
return 1;
|
|
116
134
|
}
|
|
117
135
|
}
|
|
118
136
|
|
|
119
137
|
if (command !== "init") {
|
|
120
|
-
err(`Unknown command "${command}".\n\n${USAGE}`)
|
|
121
|
-
return 1
|
|
138
|
+
err(`Unknown command "${command}".\n\n${USAGE}`);
|
|
139
|
+
return 1;
|
|
122
140
|
}
|
|
123
141
|
|
|
124
|
-
const required = requireInput(flags, cwd, home, configHome, true)
|
|
125
|
-
if (!required.ok) return usageError(required.problems, err)
|
|
126
|
-
const { input } = required
|
|
127
|
-
const
|
|
142
|
+
const required = requireInput(flags, cwd, home, configHome, true);
|
|
143
|
+
if (!required.ok) return usageError(required.problems, err);
|
|
144
|
+
const { input } = required;
|
|
145
|
+
const probe = HOST_VERSION_PROBES[input.host];
|
|
146
|
+
const hostVersion = probe?.(err);
|
|
147
|
+
const plan = planForHost(input.host, hostVersion === undefined ? input : { ...input, hostVersion });
|
|
128
148
|
if (plan.errors.length > 0) {
|
|
129
|
-
for (const error of plan.errors) err(`error: ${error.message}`)
|
|
130
|
-
err("Nothing was written.")
|
|
131
|
-
return 1
|
|
149
|
+
for (const error of plan.errors) err(`error: ${error.message}`);
|
|
150
|
+
err("Nothing was written.");
|
|
151
|
+
return 1;
|
|
132
152
|
}
|
|
133
|
-
out(renderPlan(plan))
|
|
153
|
+
out(renderPlan(plan));
|
|
134
154
|
if (!flags.booleans.has("apply")) {
|
|
135
|
-
out("\nPlan only — re-run with --apply to write these changes.")
|
|
136
|
-
return 0
|
|
155
|
+
out("\nPlan only — re-run with --apply to write these changes.");
|
|
156
|
+
return 0;
|
|
137
157
|
}
|
|
138
158
|
try {
|
|
139
|
-
const result = applyPlan(plan, input)
|
|
140
|
-
for (const line of result.lines) out(line)
|
|
141
|
-
return result.ok ? 0 : 1
|
|
159
|
+
const result = applyPlan(plan, input);
|
|
160
|
+
for (const line of result.lines) out(line);
|
|
161
|
+
return result.ok ? 0 : 1;
|
|
142
162
|
} catch (error) {
|
|
143
|
-
err(error instanceof Error ? error.message : String(error))
|
|
144
|
-
return 1
|
|
163
|
+
err(error instanceof Error ? error.message : String(error));
|
|
164
|
+
return 1;
|
|
145
165
|
}
|
|
146
166
|
}
|
|
147
167
|
|
|
148
168
|
function usageError(problems: readonly string[], err: (line: string) => void): number {
|
|
149
|
-
for (const problem of problems) err(`error: ${problem}`)
|
|
150
|
-
err(`\n${USAGE}`)
|
|
151
|
-
return 1
|
|
169
|
+
for (const problem of problems) err(`error: ${problem}`);
|
|
170
|
+
err(`\n${USAGE}`);
|
|
171
|
+
return 1;
|
|
152
172
|
}
|
|
153
173
|
|
|
154
174
|
/**
|
|
@@ -156,14 +176,14 @@ function usageError(problems: readonly string[], err: (line: string) => void): n
|
|
|
156
176
|
* environment read happens here, at the edge, and flows down as explicit input.
|
|
157
177
|
*/
|
|
158
178
|
function resolveConfigHome(home: string): string | undefined {
|
|
159
|
-
const xdg = process.env.XDG_CONFIG_HOME
|
|
160
|
-
if (xdg && home === (process.env.HOME ?? "")) return xdg
|
|
161
|
-
return undefined
|
|
179
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
180
|
+
if (xdg && home === (process.env.HOME ?? "")) return xdg;
|
|
181
|
+
return undefined;
|
|
162
182
|
}
|
|
163
183
|
|
|
164
184
|
type RequireInputResult =
|
|
165
185
|
| { readonly ok: true; readonly input: InstallerInput }
|
|
166
|
-
| { readonly ok: false; readonly problems: readonly string[] }
|
|
186
|
+
| { readonly ok: false; readonly problems: readonly string[] };
|
|
167
187
|
|
|
168
188
|
function requireInput(
|
|
169
189
|
flags: Flags,
|
|
@@ -172,33 +192,43 @@ function requireInput(
|
|
|
172
192
|
configHome: string | undefined,
|
|
173
193
|
needsTiers: boolean,
|
|
174
194
|
): RequireInputResult {
|
|
175
|
-
const problems: string[] = []
|
|
176
|
-
const host = flags.values.get("host")
|
|
177
|
-
const scope = flags.values.get("scope")
|
|
178
|
-
const search = needsTiers ? (flags.values.get("search") ?? "") : "optional"
|
|
179
|
-
const fetch = needsTiers ? (flags.values.get("fetch") ?? "") : "optional"
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (
|
|
183
|
-
|
|
195
|
+
const problems: string[] = [];
|
|
196
|
+
const host = flags.values.get("host");
|
|
197
|
+
const scope = flags.values.get("scope");
|
|
198
|
+
const search = needsTiers ? (flags.values.get("search") ?? "") : "optional";
|
|
199
|
+
const fetch = needsTiers ? (flags.values.get("fetch") ?? "") : "optional";
|
|
200
|
+
const path = flags.values.get("path");
|
|
201
|
+
if (host === undefined) problems.push(`--host is required (one of: ${HOSTS.join(", ")})`);
|
|
202
|
+
else if (!HOSTS.includes(host as HostName))
|
|
203
|
+
problems.push(`invalid --host value "${host}" (one of: ${HOSTS.join(", ")})`);
|
|
204
|
+
if (scope === undefined) problems.push(`--scope is required (one of: ${SCOPES.join(", ")})`);
|
|
205
|
+
else if (!SCOPES.includes(scope as Scope))
|
|
206
|
+
problems.push(`invalid --scope value "${scope}" (one of: ${SCOPES.join(", ")})`);
|
|
207
|
+
if (path !== undefined) {
|
|
208
|
+
if (host !== "pi") {
|
|
209
|
+
problems.push(`--path is only supported for host pi (${host ?? "no host"} has a single integration path)`);
|
|
210
|
+
} else if (!(PATH_CHOICES as readonly string[]).includes(path)) {
|
|
211
|
+
problems.push(`invalid --path value "${path}" (one of: ${PATH_CHOICES.join(", ")})`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
184
214
|
if (needsTiers) {
|
|
185
215
|
if (!TIERS.includes(search as Tier)) {
|
|
186
216
|
problems.push(
|
|
187
217
|
flags.values.has("search")
|
|
188
218
|
? `invalid --search value "${search}" (one of: ${TIERS.join(", ")})`
|
|
189
219
|
: `--search is required (one of: ${TIERS.join(", ")})`,
|
|
190
|
-
)
|
|
220
|
+
);
|
|
191
221
|
}
|
|
192
222
|
if (!TIERS.includes(fetch as Tier)) {
|
|
193
223
|
problems.push(
|
|
194
224
|
flags.values.has("fetch")
|
|
195
225
|
? `invalid --fetch value "${fetch}" (one of: ${TIERS.join(", ")})`
|
|
196
226
|
: `--fetch is required (one of: ${TIERS.join(", ")})`,
|
|
197
|
-
)
|
|
227
|
+
);
|
|
198
228
|
}
|
|
199
229
|
}
|
|
200
|
-
if (home === "") problems.push("HOME is not set and the user home cannot be resolved; cannot proceed")
|
|
201
|
-
if (problems.length > 0) return { ok: false, problems }
|
|
230
|
+
if (home === "") problems.push("HOME is not set and the user home cannot be resolved; cannot proceed");
|
|
231
|
+
if (problems.length > 0) return { ok: false, problems };
|
|
202
232
|
return {
|
|
203
233
|
ok: true,
|
|
204
234
|
input: {
|
|
@@ -210,6 +240,7 @@ function requireInput(
|
|
|
210
240
|
home,
|
|
211
241
|
cwd,
|
|
212
242
|
configHome,
|
|
243
|
+
...(path !== undefined ? { path: path as "extension" | "mcp" } : {}),
|
|
213
244
|
},
|
|
214
|
-
}
|
|
245
|
+
};
|
|
215
246
|
}
|
package/src/installer/detect.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs"
|
|
2
|
-
import { removeBlock } from "./formats/rules"
|
|
3
|
-
import { isPlainObject } from "./formats/value"
|
|
4
|
-
import type { HostName } from "./types"
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { removeBlock } from "./formats/rules";
|
|
3
|
+
import { isPlainObject } from "./formats/value";
|
|
4
|
+
import type { HostName } from "./types";
|
|
5
5
|
|
|
6
6
|
/** Shared plan error: both capabilities off leaves nothing to install. */
|
|
7
|
-
export const BOTH_CAPABILITIES_OFF = "Both capabilities are off; nothing would be installed."
|
|
7
|
+
export const BOTH_CAPABILITIES_OFF = "Both capabilities are off; nothing would be installed.";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Shared Forced-tier cost warning; the README's "Privacy and cost" section carries
|
|
@@ -12,20 +12,20 @@ export const BOTH_CAPABILITIES_OFF = "Both capabilities are off; nothing would b
|
|
|
12
12
|
*/
|
|
13
13
|
export const FORCED_COST_NOTE =
|
|
14
14
|
"Forced routing is deterministic, and Search and Fetch are billed per successful request — " +
|
|
15
|
-
'see the README\'s "Privacy and cost" section before applying.'
|
|
15
|
+
'see the README\'s "Privacy and cost" section before applying.';
|
|
16
16
|
|
|
17
17
|
/** Shared note when a previous deepseek entry is being replaced. */
|
|
18
18
|
export function replacingExistingEntryNote(path: string): string {
|
|
19
|
-
return `An existing ${path} entry is being replaced; its previous form is recorded for uninstall
|
|
19
|
+
return `An existing ${path} entry is being replaced; its previous form is recorded for uninstall.`;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
/** Shared note for third-party servers left registered. */
|
|
23
23
|
export function othersStayRegisteredNote(others: readonly string[]): string {
|
|
24
|
-
return `Other MCP servers stay registered (${others.join(", ")}); routing to DeepSeek is not exclusive while they offer web tools
|
|
24
|
+
return `Other MCP servers stay registered (${others.join(", ")}); routing to DeepSeek is not exclusive while they offer web tools.`;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const WEB_TOOL_HINT =
|
|
28
|
-
/(web[_ -]?(search|fetch)|search|fetch|browse|scrape|crawl|tavily|brave|exa|perplexity|firecrawl|jina|serp|duckduckgo|reader)/i
|
|
28
|
+
/(web[_ -]?(search|fetch)|search|fetch|browse|scrape|crawl|tavily|brave|exa|perplexity|firecrawl|jina|serp|duckduckgo|reader)/i;
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Heuristic web-capability test for a registered third-party MCP server, matching
|
|
@@ -34,16 +34,30 @@ const WEB_TOOL_HINT =
|
|
|
34
34
|
* "not exclusive while they offer web tools" note.
|
|
35
35
|
*/
|
|
36
36
|
export function looksWebCapable(name: string, entry: unknown): boolean {
|
|
37
|
-
if (WEB_TOOL_HINT.test(name)) return true
|
|
38
|
-
if (!isPlainObject(entry)) return false
|
|
39
|
-
const command = typeof entry.command === "string" ? entry.command : ""
|
|
40
|
-
const url = typeof entry.url === "string" ? entry.url : ""
|
|
41
|
-
const args = Array.isArray(entry.args)
|
|
42
|
-
|
|
37
|
+
if (WEB_TOOL_HINT.test(name)) return true;
|
|
38
|
+
if (!isPlainObject(entry)) return false;
|
|
39
|
+
const command = typeof entry.command === "string" ? entry.command : "";
|
|
40
|
+
const url = typeof entry.url === "string" ? entry.url : "";
|
|
41
|
+
const args = Array.isArray(entry.args)
|
|
42
|
+
? entry.args.filter((arg): arg is string => typeof arg === "string").join(" ")
|
|
43
|
+
: "";
|
|
44
|
+
return WEB_TOOL_HINT.test(`${command} ${args} ${url}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Matches a source or package ref that is this package in another form — a fork
|
|
49
|
+
* (`github:owner/deepseek-web-retrieval`), a pinned version
|
|
50
|
+
* (`npm:deepseek-web-retrieval@<version>`), a packed tarball
|
|
51
|
+
* (`npm:file:…/deepseek-web-retrieval-<version>.tgz`) — but not an unrelated
|
|
52
|
+
* package whose name merely contains ours (`deepseek-web-retrieval-extra`).
|
|
53
|
+
* The match is on the package-name boundary, not a raw substring.
|
|
54
|
+
*/
|
|
55
|
+
export function looksLikeOwnPackage(source: string): boolean {
|
|
56
|
+
return /(^|[/:@])deepseek-web-retrieval($|[@/]|-\d)/.test(source);
|
|
43
57
|
}
|
|
44
58
|
|
|
45
59
|
const STEERING_HINT =
|
|
46
|
-
/(web[_ ]?(search|fetch)|websearch|webfetch|tavily|exa|brave|perplexity|firecrawl|jina|serp|duckduckgo|google|bing)/i
|
|
60
|
+
/(web[_ ]?(search|fetch)|websearch|webfetch|tavily|exa|brave|perplexity|firecrawl|jina|serp|duckduckgo|google|bing)/i;
|
|
47
61
|
|
|
48
62
|
/**
|
|
49
63
|
* Detect-phase scan of the rules file the plan would write to: lines steering the
|
|
@@ -51,35 +65,36 @@ const STEERING_HINT =
|
|
|
51
65
|
* deepseek mentions are ours to make, so both are ignored.
|
|
52
66
|
*/
|
|
53
67
|
export function findCompetitorSteering(host: HostName, file: string): string[] {
|
|
54
|
-
if (!existsSync(file)) return []
|
|
55
|
-
let text: string
|
|
68
|
+
if (!existsSync(file)) return [];
|
|
69
|
+
let text: string;
|
|
56
70
|
try {
|
|
57
|
-
text = readFileSync(file, "utf8")
|
|
71
|
+
text = readFileSync(file, "utf8");
|
|
58
72
|
} catch {
|
|
59
|
-
return []
|
|
73
|
+
return [];
|
|
60
74
|
}
|
|
61
75
|
return removeBlock(host, text)
|
|
62
76
|
.split("\n")
|
|
63
77
|
.map((line) => line.trim())
|
|
64
78
|
.filter((line) => line.length > 0 && STEERING_HINT.test(line) && !line.includes("deepseek"))
|
|
65
|
-
.slice(0, 2)
|
|
79
|
+
.slice(0, 2);
|
|
66
80
|
}
|
|
67
81
|
|
|
68
82
|
/** Shared note for steering the detect phase found; conflicts are surfaced, not resolved. */
|
|
69
83
|
export function competitorSteeringNote(file: string, lines: readonly string[]): string {
|
|
70
|
-
const quoted = lines.map((line) => `"${line}"`).join(", ")
|
|
71
|
-
return `Existing rules in ${file} steer toward other web tools (${quoted}); the plan surfaces this conflict — it does not resolve it
|
|
84
|
+
const quoted = lines.map((line) => `"${line}"`).join(", ");
|
|
85
|
+
return `Existing rules in ${file} steer toward other web tools (${quoted}); the plan surfaces this conflict — it does not resolve it.`;
|
|
72
86
|
}
|
|
73
87
|
|
|
74
88
|
/** Splits other registered servers into web-capable competitors and bystanders. */
|
|
75
|
-
export function splitCompetitors(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
89
|
+
export function splitCompetitors(servers: Record<string, unknown>): {
|
|
90
|
+
readonly competitors: readonly string[];
|
|
91
|
+
readonly bystanders: readonly string[];
|
|
92
|
+
} {
|
|
93
|
+
const competitors: string[] = [];
|
|
94
|
+
const bystanders: string[] = [];
|
|
80
95
|
for (const [name, entry] of Object.entries(servers)) {
|
|
81
|
-
if (name === "deepseek") continue
|
|
82
|
-
|
|
96
|
+
if (name === "deepseek") continue;
|
|
97
|
+
(looksWebCapable(name, entry) ? competitors : bystanders).push(name);
|
|
83
98
|
}
|
|
84
|
-
return { competitors, bystanders }
|
|
99
|
+
return { competitors, bystanders };
|
|
85
100
|
}
|
|
@@ -1,33 +1,32 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { tomlCodec } from "./toml"
|
|
1
|
+
import { atomicWrite } from "../persistence";
|
|
2
|
+
import type { ConfigOp, FileEdit, HostName, RecordedFile, RecordedOp } from "../types";
|
|
3
|
+
import { jsonCodec } from "./json";
|
|
4
|
+
import { rulesCodec } from "./rules";
|
|
5
|
+
import { tomlCodec } from "./toml";
|
|
7
6
|
|
|
8
|
-
export type ConfigFormat = FileEdit["format"]
|
|
7
|
+
export type ConfigFormat = FileEdit["format"];
|
|
9
8
|
|
|
10
9
|
/** A config file as the codec read it: parsed, or carrying the parse error. */
|
|
11
10
|
export interface LoadedConfig {
|
|
12
|
-
readonly exists: boolean
|
|
13
|
-
readonly text: string
|
|
14
|
-
readonly value: Record<string, unknown
|
|
11
|
+
readonly exists: boolean;
|
|
12
|
+
readonly text: string;
|
|
13
|
+
readonly value: Record<string, unknown>;
|
|
15
14
|
/** True when serializing the parsed value would drop comments (TOML only; JSON edits preserve them). */
|
|
16
|
-
readonly lossy: boolean
|
|
17
|
-
readonly parseError?: string
|
|
15
|
+
readonly lossy: boolean;
|
|
16
|
+
readonly parseError?: string;
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
/** What applying one edit produced, beyond the new text: everything undo needs. */
|
|
21
20
|
export interface AppliedText {
|
|
22
|
-
readonly text: string
|
|
23
|
-
readonly recorded: readonly RecordedOp[]
|
|
24
|
-
readonly createdParents: readonly (readonly string[])[]
|
|
21
|
+
readonly text: string;
|
|
22
|
+
readonly recorded: readonly RecordedOp[];
|
|
23
|
+
readonly createdParents: readonly (readonly string[])[];
|
|
25
24
|
}
|
|
26
25
|
|
|
27
26
|
/** What reverting one recorded file produced: the text plus notes about skipped work. */
|
|
28
27
|
export interface RevertedText {
|
|
29
|
-
readonly text: string
|
|
30
|
-
readonly notes: readonly string[]
|
|
28
|
+
readonly text: string;
|
|
29
|
+
readonly notes: readonly string[];
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
/**
|
|
@@ -36,23 +35,22 @@ export interface RevertedText {
|
|
|
36
35
|
* revert reverses a recorded file, and isEffectivelyEmpty gates whole-file deletion.
|
|
37
36
|
*/
|
|
38
37
|
export interface FormatCodec {
|
|
39
|
-
readonly format: ConfigFormat
|
|
40
|
-
load(file: string): LoadedConfig
|
|
41
|
-
apply(text: string, edit: FileEdit): AppliedText
|
|
42
|
-
verify(text: string, edit: FileEdit, op?: ConfigOp): boolean
|
|
43
|
-
revert(text: string, recorded: RecordedFile): RevertedText
|
|
44
|
-
isEffectivelyEmpty(text: string): boolean
|
|
38
|
+
readonly format: ConfigFormat;
|
|
39
|
+
load(file: string): LoadedConfig;
|
|
40
|
+
apply(text: string, edit: FileEdit): AppliedText;
|
|
41
|
+
verify(text: string, edit: FileEdit, op?: ConfigOp): boolean;
|
|
42
|
+
revert(text: string, recorded: RecordedFile): RevertedText;
|
|
43
|
+
isEffectivelyEmpty(text: string): boolean;
|
|
45
44
|
}
|
|
46
45
|
|
|
47
46
|
/** Shared write step: every codec consumer writes config files through this. */
|
|
48
47
|
export function writeConfig(file: string, serialized: string): void {
|
|
49
|
-
|
|
50
|
-
writeFileSync(file, serialized)
|
|
48
|
+
atomicWrite(file, serialized);
|
|
51
49
|
}
|
|
52
50
|
|
|
53
51
|
/** The one entry point to a format's codec; only the rules codec cares about the host. */
|
|
54
52
|
export function codecFor(format: ConfigFormat, host: HostName): FormatCodec {
|
|
55
|
-
if (format === "json") return jsonCodec
|
|
56
|
-
if (format === "toml") return tomlCodec
|
|
57
|
-
return rulesCodec(host)
|
|
53
|
+
if (format === "json") return jsonCodec;
|
|
54
|
+
if (format === "toml") return tomlCodec;
|
|
55
|
+
return rulesCodec(host);
|
|
58
56
|
}
|