deepseek-web-retrieval 0.3.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 +96 -230
- package/README.md +53 -25
- package/dist/cli.js +1358 -617
- package/docs/agents/codex.md +32 -11
- package/docs/agents/opencode-v2.md +5 -5
- package/docs/agents/pi.md +92 -90
- 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/docs/research/search-api-defaults.md +309 -0
- 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 +30 -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 +21 -26
- 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 -124
package/dist/cli.js
CHANGED
|
@@ -16,13 +16,82 @@ var __export = (target, all) => {
|
|
|
16
16
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
17
17
|
|
|
18
18
|
// src/core/constants.ts
|
|
19
|
-
var SEARCH_ENDPOINT = "https://api.deepseek.com/v1/tools/search", FETCH_ENDPOINT = "https://api.deepseek.com/v1/tools/fetch", DEFAULT_SEARCH_COUNT = 10, DEFAULT_MAX_RESULTS =
|
|
19
|
+
var SEARCH_ENDPOINT = "https://api.deepseek.com/v1/tools/search", FETCH_ENDPOINT = "https://api.deepseek.com/v1/tools/fetch", DEFAULT_SEARCH_COUNT = 10, DEFAULT_MAX_RESULTS = 10, DEFAULT_MIN_RERANK = 0, DEFAULT_MIN_AUTHORITY = 0, DEFAULT_MAX_SUMMARY_CHARS = 1200, DEFAULT_FETCH_MAX_CHARS = 16000, DEFAULT_TIMEOUT_MS = 30000, MIN_TIMEOUT_MS = 1000, MAX_TIMEOUT_MS = 120000, MAX_QUERY_CHARS = 128, MAX_URL_CHARS = 8192, MAX_ERROR_BODY_CHARS = 2000, MAX_RESPONSE_BYTES, FETCH_TRUNCATION_MARKER = `
|
|
20
20
|
|
|
21
21
|
[content truncated]`, DEFAULT_API_KEY_SOURCE = "auto", DEFAULT_MAX_RETRIES = 0, MAX_RETRIES = 5, DEFAULT_RETRY_BASE_DELAY_MS = 250, MAX_RETRY_DELAY_MS = 1e4, API_KEY_ENV = "DEEPSEEK_API_KEY", ENABLE_SEARCH_ENV = "DEEPSEEK_ENABLE_SEARCH", ENABLE_FETCH_ENV = "DEEPSEEK_ENABLE_FETCH";
|
|
22
22
|
var init_constants = __esm(() => {
|
|
23
23
|
MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
|
|
24
24
|
});
|
|
25
25
|
|
|
26
|
+
// src/installer/persistence.ts
|
|
27
|
+
import { randomUUID } from "node:crypto";
|
|
28
|
+
import {
|
|
29
|
+
closeSync,
|
|
30
|
+
fchmodSync,
|
|
31
|
+
fsyncSync,
|
|
32
|
+
lstatSync,
|
|
33
|
+
mkdirSync,
|
|
34
|
+
openSync,
|
|
35
|
+
readFileSync,
|
|
36
|
+
realpathSync,
|
|
37
|
+
renameSync,
|
|
38
|
+
rmSync,
|
|
39
|
+
statSync,
|
|
40
|
+
writeFileSync
|
|
41
|
+
} from "node:fs";
|
|
42
|
+
import { basename, dirname, join } from "node:path";
|
|
43
|
+
function atomicWrite(file, text, mode) {
|
|
44
|
+
let target = file;
|
|
45
|
+
let existingMode;
|
|
46
|
+
try {
|
|
47
|
+
const entry = lstatSync(file);
|
|
48
|
+
if (entry.isSymbolicLink())
|
|
49
|
+
target = realpathSync(file);
|
|
50
|
+
existingMode = statSync(target).mode & 511;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error.code !== "ENOENT")
|
|
53
|
+
throw error;
|
|
54
|
+
try {
|
|
55
|
+
if (lstatSync(file).isSymbolicLink())
|
|
56
|
+
throw new Error(`Cannot write dangling symlink ${file}`);
|
|
57
|
+
} catch (linkError) {
|
|
58
|
+
if (linkError.code !== "ENOENT")
|
|
59
|
+
throw linkError;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
const dir = dirname(target);
|
|
63
|
+
mkdirSync(dir, { recursive: true });
|
|
64
|
+
const temporary = join(dir, `.${basename(target)}.${randomUUID()}.tmp`);
|
|
65
|
+
let fd;
|
|
66
|
+
try {
|
|
67
|
+
fd = openSync(temporary, "wx", 384);
|
|
68
|
+
writeFileSync(fd, text);
|
|
69
|
+
if (readFileSync(temporary, "utf8") !== text)
|
|
70
|
+
throw new Error(`Incomplete write to ${file}`);
|
|
71
|
+
fchmodSync(fd, mode ?? existingMode ?? 438 & ~process.umask());
|
|
72
|
+
fsyncSync(fd);
|
|
73
|
+
closeSync(fd);
|
|
74
|
+
fd = undefined;
|
|
75
|
+
renameSync(temporary, target);
|
|
76
|
+
syncDirectory(dir);
|
|
77
|
+
} finally {
|
|
78
|
+
if (fd !== undefined)
|
|
79
|
+
closeSync(fd);
|
|
80
|
+
rmSync(temporary, { force: true });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function syncDirectory(dir) {
|
|
84
|
+
if (process.platform === "win32")
|
|
85
|
+
return;
|
|
86
|
+
const fd = openSync(dir, "r");
|
|
87
|
+
try {
|
|
88
|
+
fsyncSync(fd);
|
|
89
|
+
} finally {
|
|
90
|
+
closeSync(fd);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
var init_persistence = () => {};
|
|
94
|
+
|
|
26
95
|
// node_modules/jsonc-parser/lib/esm/impl/scanner.js
|
|
27
96
|
function createScanner(text, ignoreTrivia = false) {
|
|
28
97
|
const len = text.length;
|
|
@@ -750,10 +819,10 @@ function parse(text, errors = [], options = ParseOptions.DEFAULT) {
|
|
|
750
819
|
currentParent = previousParents.pop();
|
|
751
820
|
},
|
|
752
821
|
onArrayBegin: () => {
|
|
753
|
-
const
|
|
754
|
-
onValue(
|
|
822
|
+
const array = [];
|
|
823
|
+
onValue(array);
|
|
755
824
|
previousParents.push(currentParent);
|
|
756
|
-
currentParent =
|
|
825
|
+
currentParent = array;
|
|
757
826
|
currentProperty = null;
|
|
758
827
|
},
|
|
759
828
|
onArrayEnd: () => {
|
|
@@ -1379,10 +1448,12 @@ var init_jsonc = __esm(() => {
|
|
|
1379
1448
|
init_main();
|
|
1380
1449
|
});
|
|
1381
1450
|
|
|
1382
|
-
// src/
|
|
1451
|
+
// src/core/object.ts
|
|
1383
1452
|
function isPlainObject(value) {
|
|
1384
1453
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1385
1454
|
}
|
|
1455
|
+
|
|
1456
|
+
// src/installer/formats/value.ts
|
|
1386
1457
|
function deepEqual(a, b) {
|
|
1387
1458
|
if (a === b)
|
|
1388
1459
|
return true;
|
|
@@ -1432,13 +1503,14 @@ function appendEntries(existing, entries, path) {
|
|
|
1432
1503
|
const appended = entries.filter((entry) => !list.some((item) => deepEqual(item, entry)));
|
|
1433
1504
|
return { list, appended };
|
|
1434
1505
|
}
|
|
1506
|
+
var init_value = () => {};
|
|
1435
1507
|
|
|
1436
1508
|
// src/installer/formats/json.ts
|
|
1437
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
1509
|
+
import { existsSync, readFileSync as readFileSync2 } from "node:fs";
|
|
1438
1510
|
function loadJson(file) {
|
|
1439
1511
|
if (!existsSync(file))
|
|
1440
1512
|
return { exists: false, text: "", value: {}, lossy: false };
|
|
1441
|
-
const text =
|
|
1513
|
+
const text = readFileSync2(file, "utf8");
|
|
1442
1514
|
try {
|
|
1443
1515
|
return { exists: true, text, value: parseJsonc(text), lossy: false };
|
|
1444
1516
|
} catch (error) {
|
|
@@ -1547,6 +1619,7 @@ var JSON_FORMATTING, jsonCodec;
|
|
|
1547
1619
|
var init_json = __esm(() => {
|
|
1548
1620
|
init_main();
|
|
1549
1621
|
init_jsonc();
|
|
1622
|
+
init_value();
|
|
1550
1623
|
JSON_FORMATTING = { insertSpaces: true, tabSize: 2, insertFinalNewline: false };
|
|
1551
1624
|
jsonCodec = {
|
|
1552
1625
|
format: "json",
|
|
@@ -1569,7 +1642,7 @@ var init_json = __esm(() => {
|
|
|
1569
1642
|
});
|
|
1570
1643
|
|
|
1571
1644
|
// src/installer/formats/rules.ts
|
|
1572
|
-
import { existsSync as existsSync2, readFileSync as
|
|
1645
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
|
|
1573
1646
|
function rulesMarkers(host) {
|
|
1574
1647
|
return {
|
|
1575
1648
|
begin: `<!-- deepseek-web-retrieval:${host}:rules:begin -->`,
|
|
@@ -1591,28 +1664,47 @@ ${block}
|
|
|
1591
1664
|
`;
|
|
1592
1665
|
}
|
|
1593
1666
|
function removeBlock(host, text) {
|
|
1594
|
-
const
|
|
1595
|
-
|
|
1596
|
-
if (start === -1)
|
|
1667
|
+
const range = blockRange(host, text);
|
|
1668
|
+
if (range === undefined)
|
|
1597
1669
|
return text;
|
|
1598
|
-
const
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
after = after.replace(/^\n{1,2}/, "");
|
|
1603
|
-
const before = text.slice(0, start);
|
|
1604
|
-
return before.endsWith(`
|
|
1670
|
+
const before = text.slice(0, range.start).trimEnd();
|
|
1671
|
+
const after = text.slice(range.end).replace(/^\n{1,2}/, "").trimStart();
|
|
1672
|
+
if (before && after)
|
|
1673
|
+
return `${before}
|
|
1605
1674
|
|
|
1606
|
-
|
|
1675
|
+
${after}`;
|
|
1676
|
+
if (before)
|
|
1677
|
+
return `${before}
|
|
1678
|
+
`;
|
|
1679
|
+
return after;
|
|
1607
1680
|
}
|
|
1608
1681
|
function hasBlock(host, text) {
|
|
1682
|
+
return blockRange(host, text) !== undefined;
|
|
1683
|
+
}
|
|
1684
|
+
function blockRange(host, text) {
|
|
1609
1685
|
const markers = rulesMarkers(host);
|
|
1610
|
-
|
|
1686
|
+
const start = text.indexOf(markers.begin);
|
|
1687
|
+
const end = text.indexOf(markers.end);
|
|
1688
|
+
if (start === -1 && end === -1)
|
|
1689
|
+
return;
|
|
1690
|
+
if (start === -1 || end < start || text.indexOf(markers.begin, start + markers.begin.length) !== -1 || text.indexOf(markers.end, end + markers.end.length) !== -1) {
|
|
1691
|
+
throw new Error(`malformed rules block for ${host}: expected one begin marker followed by one end marker; repair the markers before retrying`);
|
|
1692
|
+
}
|
|
1693
|
+
return { start, end: end + markers.end.length };
|
|
1611
1694
|
}
|
|
1612
1695
|
function rulesCodec(host) {
|
|
1613
1696
|
return {
|
|
1614
1697
|
format: "rules",
|
|
1615
|
-
load: (file) =>
|
|
1698
|
+
load: (file) => {
|
|
1699
|
+
const exists = existsSync2(file);
|
|
1700
|
+
const text = exists ? readFileSync3(file, "utf8") : "";
|
|
1701
|
+
try {
|
|
1702
|
+
blockRange(host, text);
|
|
1703
|
+
return { exists, text, value: {}, lossy: false };
|
|
1704
|
+
} catch (error) {
|
|
1705
|
+
return { exists, text, value: {}, lossy: false, parseError: String(error) };
|
|
1706
|
+
}
|
|
1707
|
+
},
|
|
1616
1708
|
apply: (text, edit2) => {
|
|
1617
1709
|
if (edit2.format !== "rules")
|
|
1618
1710
|
throw new Error(`rules codec cannot apply a ${edit2.format} edit`);
|
|
@@ -2669,30 +2761,30 @@ function stringifyInlineTable(obj, depth, numberAsFloat, strictTemporal) {
|
|
|
2669
2761
|
}
|
|
2670
2762
|
return res + " }";
|
|
2671
2763
|
}
|
|
2672
|
-
function stringifyArray(
|
|
2673
|
-
if (
|
|
2764
|
+
function stringifyArray(array, depth, numberAsFloat, strictTemporal) {
|
|
2765
|
+
if (array.length === 0)
|
|
2674
2766
|
return "[]";
|
|
2675
2767
|
let res = "[ ";
|
|
2676
|
-
for (let i = 0;i <
|
|
2768
|
+
for (let i = 0;i < array.length; i++) {
|
|
2677
2769
|
if (i)
|
|
2678
2770
|
res += ", ";
|
|
2679
|
-
if (
|
|
2771
|
+
if (array[i] === null || array[i] === undefined) {
|
|
2680
2772
|
throw new TypeError("arrays cannot contain null or undefined values");
|
|
2681
2773
|
}
|
|
2682
|
-
res += stringifyValue(
|
|
2774
|
+
res += stringifyValue(array[i], extendedTypeOf(array[i]), depth - 1, numberAsFloat, strictTemporal);
|
|
2683
2775
|
}
|
|
2684
2776
|
return res + " ]";
|
|
2685
2777
|
}
|
|
2686
|
-
function stringifyArrayTable(
|
|
2778
|
+
function stringifyArrayTable(array, key, depth, numberAsFloat, strictTemporal) {
|
|
2687
2779
|
if (depth === 0) {
|
|
2688
2780
|
throw new Error("Could not stringify the object: maximum object depth exceeded");
|
|
2689
2781
|
}
|
|
2690
2782
|
let res = "";
|
|
2691
|
-
for (let i = 0;i <
|
|
2783
|
+
for (let i = 0;i < array.length; i++) {
|
|
2692
2784
|
res += `${res && `
|
|
2693
2785
|
`}[[${key}]]
|
|
2694
2786
|
`;
|
|
2695
|
-
res += stringifyTable(0,
|
|
2787
|
+
res += stringifyTable(0, array[i], key, depth, numberAsFloat, strictTemporal);
|
|
2696
2788
|
}
|
|
2697
2789
|
return res;
|
|
2698
2790
|
}
|
|
@@ -2813,11 +2905,11 @@ var init_dist = __esm(() => {
|
|
|
2813
2905
|
});
|
|
2814
2906
|
|
|
2815
2907
|
// src/installer/formats/toml.ts
|
|
2816
|
-
import { existsSync as existsSync3, readFileSync as
|
|
2908
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "node:fs";
|
|
2817
2909
|
function loadToml(file) {
|
|
2818
2910
|
if (!existsSync3(file))
|
|
2819
2911
|
return { exists: false, text: "", value: {}, lossy: false };
|
|
2820
|
-
const text =
|
|
2912
|
+
const text = readFileSync4(file, "utf8");
|
|
2821
2913
|
try {
|
|
2822
2914
|
return { exists: true, text, value: parse3(text), lossy: hasTomlComment(text) };
|
|
2823
2915
|
} catch (error) {
|
|
@@ -2863,7 +2955,7 @@ function applyTomlOps(value, ops) {
|
|
|
2863
2955
|
const parent = navigate(target, op.path, createdParents);
|
|
2864
2956
|
const key = op.path[op.path.length - 1];
|
|
2865
2957
|
if (op.op === "set") {
|
|
2866
|
-
const previous = Object.
|
|
2958
|
+
const previous = Object.hasOwn(parent, key) ? parent[key] : undefined;
|
|
2867
2959
|
parent[key] = structuredClone(op.value);
|
|
2868
2960
|
recorded.push({ op: "set", path: op.path, applied: structuredClone(op.value), previous });
|
|
2869
2961
|
continue;
|
|
@@ -2945,6 +3037,7 @@ function tomlOpHolds(text, op) {
|
|
|
2945
3037
|
var tomlCodec;
|
|
2946
3038
|
var init_toml = __esm(() => {
|
|
2947
3039
|
init_dist();
|
|
3040
|
+
init_value();
|
|
2948
3041
|
tomlCodec = {
|
|
2949
3042
|
format: "toml",
|
|
2950
3043
|
load: loadToml,
|
|
@@ -2971,11 +3064,8 @@ var init_toml = __esm(() => {
|
|
|
2971
3064
|
});
|
|
2972
3065
|
|
|
2973
3066
|
// src/installer/formats/index.ts
|
|
2974
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2975
|
-
import { dirname } from "node:path";
|
|
2976
3067
|
function writeConfig(file, serialized) {
|
|
2977
|
-
|
|
2978
|
-
writeFileSync(file, serialized);
|
|
3068
|
+
atomicWrite(file, serialized);
|
|
2979
3069
|
}
|
|
2980
3070
|
function codecFor(format2, host) {
|
|
2981
3071
|
if (format2 === "json")
|
|
@@ -2985,140 +3075,737 @@ function codecFor(format2, host) {
|
|
|
2985
3075
|
return rulesCodec(host);
|
|
2986
3076
|
}
|
|
2987
3077
|
var init_formats = __esm(() => {
|
|
3078
|
+
init_persistence();
|
|
2988
3079
|
init_json();
|
|
2989
3080
|
init_rules();
|
|
2990
3081
|
init_toml();
|
|
2991
3082
|
});
|
|
2992
3083
|
|
|
2993
|
-
// src/installer/
|
|
2994
|
-
import { existsSync as existsSync4, readFileSync as
|
|
2995
|
-
|
|
2996
|
-
|
|
3084
|
+
// src/installer/revert.ts
|
|
3085
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5, rmdirSync, unlinkSync } from "node:fs";
|
|
3086
|
+
import { dirname as dirname2 } from "node:path";
|
|
3087
|
+
function pruneEmptyDir(dir) {
|
|
3088
|
+
try {
|
|
3089
|
+
rmdirSync(dir);
|
|
3090
|
+
} catch {}
|
|
2997
3091
|
}
|
|
2998
|
-
function
|
|
2999
|
-
|
|
3092
|
+
function revertRecordedFile(file, host, opts) {
|
|
3093
|
+
const lines = [];
|
|
3094
|
+
if (!existsSync4(file.file)) {
|
|
3095
|
+
lines.push(`already absent: ${file.file}${opts.label}`);
|
|
3096
|
+
return lines;
|
|
3097
|
+
}
|
|
3098
|
+
const codec = codecFor(file.format, host);
|
|
3099
|
+
const text = readFileSync5(file.file, "utf8");
|
|
3100
|
+
if (file.format === "rules") {
|
|
3101
|
+
const edit2 = { file: file.file, format: "rules", block: file.blockApplied ?? "" };
|
|
3102
|
+
if (!codec.verify(text, edit2)) {
|
|
3103
|
+
lines.push(`no rules block${opts.label} in ${file.file}; left untouched`);
|
|
3104
|
+
return lines;
|
|
3105
|
+
}
|
|
3106
|
+
const updated = codec.revert(text, file).text;
|
|
3107
|
+
if (opts.whole && codec.isEffectivelyEmpty(updated)) {
|
|
3108
|
+
unlinkSync(file.file);
|
|
3109
|
+
pruneEmptyDir(dirname2(file.file));
|
|
3110
|
+
lines.push(`deleted ${file.file} (installer-created, now empty)`);
|
|
3111
|
+
} else {
|
|
3112
|
+
writeConfig(file.file, updated);
|
|
3113
|
+
lines.push(`removed rules block${opts.label} from ${file.file}`);
|
|
3114
|
+
}
|
|
3115
|
+
return lines;
|
|
3116
|
+
}
|
|
3117
|
+
const reverted = codec.revert(text, file);
|
|
3118
|
+
if (opts.whole && file.created && codec.isEffectivelyEmpty(reverted.text)) {
|
|
3119
|
+
unlinkSync(file.file);
|
|
3120
|
+
pruneEmptyDir(dirname2(file.file));
|
|
3121
|
+
lines.push(`deleted ${file.file} (installer-created, now empty)`);
|
|
3122
|
+
} else {
|
|
3123
|
+
writeConfig(file.file, reverted.text);
|
|
3124
|
+
lines.push(`reverted artifacts${opts.label} in ${file.file}`);
|
|
3125
|
+
}
|
|
3126
|
+
lines.push(...reverted.notes.map((note) => ` note: ${note}`));
|
|
3127
|
+
return lines;
|
|
3000
3128
|
}
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
const
|
|
3009
|
-
return
|
|
3129
|
+
var init_revert = __esm(() => {
|
|
3130
|
+
init_formats();
|
|
3131
|
+
});
|
|
3132
|
+
|
|
3133
|
+
// src/installer/paths.ts
|
|
3134
|
+
import { join as pathJoin } from "node:path";
|
|
3135
|
+
function join2(...parts) {
|
|
3136
|
+
const nonEmpty = parts.filter((part) => part.length > 0);
|
|
3137
|
+
return nonEmpty.length === 0 ? "" : pathJoin(...nonEmpty);
|
|
3010
3138
|
}
|
|
3011
|
-
function
|
|
3012
|
-
|
|
3013
|
-
|
|
3139
|
+
function configHome(roots) {
|
|
3140
|
+
return roots.configHome ?? join2(roots.home, ".config");
|
|
3141
|
+
}
|
|
3142
|
+
function stateDir(roots, scope) {
|
|
3143
|
+
return scope === "global" ? join2(configHome(roots), "deepseek-web-retrieval") : join2(roots.cwd, ".deepseek-web-retrieval");
|
|
3144
|
+
}
|
|
3145
|
+
var init_paths = () => {};
|
|
3146
|
+
|
|
3147
|
+
// src/installer/state.ts
|
|
3148
|
+
import { existsSync as existsSync5, lstatSync as lstatSync2, readdirSync, readFileSync as readFileSync6, rmSync as rmSync2 } from "node:fs";
|
|
3149
|
+
import { isAbsolute, join as join3 } from "node:path";
|
|
3150
|
+
import { z as z2 } from "zod/v4";
|
|
3151
|
+
function statePathFor(input) {
|
|
3152
|
+
return join3(stateDir(input, input.scope), `install.${input.host}.json`);
|
|
3153
|
+
}
|
|
3154
|
+
function readRecordFile(file, host, scope) {
|
|
3014
3155
|
let text;
|
|
3015
3156
|
try {
|
|
3016
|
-
text =
|
|
3157
|
+
text = readFileSync6(file, "utf8");
|
|
3158
|
+
} catch (error) {
|
|
3159
|
+
if (error.code === "ENOENT") {
|
|
3160
|
+
try {
|
|
3161
|
+
lstatSync2(file);
|
|
3162
|
+
} catch (statError) {
|
|
3163
|
+
if (statError.code === "ENOENT")
|
|
3164
|
+
return;
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
throw new InstallRecordError(file, "unreadable", error.code ?? "read failed");
|
|
3168
|
+
}
|
|
3169
|
+
let value;
|
|
3170
|
+
try {
|
|
3171
|
+
value = JSON.parse(text);
|
|
3017
3172
|
} catch {
|
|
3173
|
+
throw new InstallRecordError(file, "invalid", "malformed JSON");
|
|
3174
|
+
}
|
|
3175
|
+
const result = recordSchema.safeParse(value);
|
|
3176
|
+
if (!result.success) {
|
|
3177
|
+
throw new InstallRecordError(file, "invalid", "unsupported version or malformed record structure");
|
|
3178
|
+
}
|
|
3179
|
+
if (result.data.host !== host || result.data.scope !== scope) {
|
|
3180
|
+
throw new InstallRecordError(file, "invalid", "host or scope does not match its location");
|
|
3181
|
+
}
|
|
3182
|
+
return result.data;
|
|
3183
|
+
}
|
|
3184
|
+
function readRecord(input) {
|
|
3185
|
+
return readRecordFile(statePathFor(input), input.host, input.scope);
|
|
3186
|
+
}
|
|
3187
|
+
function claimedByOtherScope(input, files) {
|
|
3188
|
+
const other = input.scope === "global" ? "project" : "global";
|
|
3189
|
+
const record = readRecord({ ...input, scope: other });
|
|
3190
|
+
if (record === undefined)
|
|
3191
|
+
return [];
|
|
3192
|
+
return files.filter((file) => record.files.some((candidate) => candidate.file === file));
|
|
3193
|
+
}
|
|
3194
|
+
function writeRecord(input, record) {
|
|
3195
|
+
const file = statePathFor(input);
|
|
3196
|
+
atomicWrite(file, `${JSON.stringify(record, null, 2)}
|
|
3197
|
+
`, 384);
|
|
3198
|
+
}
|
|
3199
|
+
function appendedByPrevious(record, path) {
|
|
3200
|
+
if (record === undefined)
|
|
3018
3201
|
return [];
|
|
3202
|
+
const entries = [];
|
|
3203
|
+
for (const file of record.files) {
|
|
3204
|
+
for (const op of file.ops) {
|
|
3205
|
+
if (op.op !== "append-array" || op.path.join("\x00") !== path.join("\x00"))
|
|
3206
|
+
continue;
|
|
3207
|
+
if (Array.isArray(op.applied))
|
|
3208
|
+
entries.push(...op.applied);
|
|
3209
|
+
}
|
|
3019
3210
|
}
|
|
3020
|
-
return
|
|
3021
|
-
`).map((line) => line.trim()).filter((line) => line.length > 0 && STEERING_HINT.test(line) && !line.includes("deepseek")).slice(0, 2);
|
|
3211
|
+
return entries;
|
|
3022
3212
|
}
|
|
3023
|
-
function
|
|
3024
|
-
const
|
|
3025
|
-
|
|
3213
|
+
function deleteRecord(input) {
|
|
3214
|
+
const file = statePathFor(input);
|
|
3215
|
+
if (existsSync5(file))
|
|
3216
|
+
rmSync2(file);
|
|
3026
3217
|
}
|
|
3027
|
-
function
|
|
3028
|
-
const
|
|
3029
|
-
const
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3218
|
+
function listRecords(roots) {
|
|
3219
|
+
const records = [];
|
|
3220
|
+
for (const scope of ["global", "project"]) {
|
|
3221
|
+
const dir = stateDir(roots, scope);
|
|
3222
|
+
let names;
|
|
3223
|
+
try {
|
|
3224
|
+
names = readdirSync(dir);
|
|
3225
|
+
} catch (error) {
|
|
3226
|
+
if (error.code === "ENOENT")
|
|
3227
|
+
continue;
|
|
3228
|
+
throw new InstallRecordError(dir, "unreadable", error.code ?? "list failed");
|
|
3229
|
+
}
|
|
3230
|
+
for (const name of names) {
|
|
3231
|
+
if (!name.startsWith("install.") || !name.endsWith(".json"))
|
|
3232
|
+
continue;
|
|
3233
|
+
const record = readRecordFile(join3(dir, name), name.slice("install.".length, -".json".length), scope);
|
|
3234
|
+
if (record !== undefined)
|
|
3235
|
+
records.push(record);
|
|
3236
|
+
}
|
|
3034
3237
|
}
|
|
3035
|
-
return
|
|
3238
|
+
return records;
|
|
3036
3239
|
}
|
|
3037
|
-
var
|
|
3038
|
-
var
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3240
|
+
var keyPath, tier, operation, recordSchema, InstallRecordError;
|
|
3241
|
+
var init_state = __esm(() => {
|
|
3242
|
+
init_paths();
|
|
3243
|
+
init_persistence();
|
|
3244
|
+
keyPath = z2.array(z2.string().min(1)).min(1);
|
|
3245
|
+
tier = z2.enum(["optional", "preferred", "forced", "off"]);
|
|
3246
|
+
operation = z2.discriminatedUnion("op", [
|
|
3247
|
+
z2.object({ op: z2.literal("set"), path: keyPath, applied: z2.unknown(), previous: z2.unknown().optional() }),
|
|
3248
|
+
z2.object({
|
|
3249
|
+
op: z2.literal("append-array"),
|
|
3250
|
+
path: keyPath,
|
|
3251
|
+
applied: z2.array(z2.unknown()),
|
|
3252
|
+
previous: z2.unknown().optional()
|
|
3253
|
+
})
|
|
3254
|
+
]);
|
|
3255
|
+
recordSchema = z2.object({
|
|
3256
|
+
version: z2.literal(1),
|
|
3257
|
+
host: z2.enum(["opencode", "pi", "codex"]),
|
|
3258
|
+
scope: z2.enum(["global", "project"]),
|
|
3259
|
+
tiers: z2.object({ search: tier, fetch: tier }),
|
|
3260
|
+
createdAt: z2.string().refine((value) => Number.isFinite(Date.parse(value))),
|
|
3261
|
+
files: z2.array(z2.object({
|
|
3262
|
+
file: z2.string().refine(isAbsolute),
|
|
3263
|
+
format: z2.enum(["json", "toml", "rules"]),
|
|
3264
|
+
created: z2.boolean(),
|
|
3265
|
+
shared: z2.literal(true).optional(),
|
|
3266
|
+
ops: z2.array(operation),
|
|
3267
|
+
blockApplied: z2.string().optional(),
|
|
3268
|
+
createdParents: z2.array(keyPath).optional()
|
|
3269
|
+
}))
|
|
3270
|
+
});
|
|
3271
|
+
InstallRecordError = class InstallRecordError extends Error {
|
|
3272
|
+
file;
|
|
3273
|
+
kind;
|
|
3274
|
+
constructor(file, kind, detail) {
|
|
3275
|
+
super(`${kind === "invalid" ? "Invalid install record" : "Cannot read install record"} at ${file} (${detail}). ` + "Restore a valid record from backup or repair its permissions before retrying; configuration was not changed. Do not delete undo state to bypass this check.");
|
|
3276
|
+
this.file = file;
|
|
3277
|
+
this.kind = kind;
|
|
3278
|
+
this.name = "InstallRecordError";
|
|
3279
|
+
}
|
|
3280
|
+
};
|
|
3043
3281
|
});
|
|
3044
3282
|
|
|
3045
|
-
// src/installer/
|
|
3046
|
-
|
|
3047
|
-
|
|
3283
|
+
// src/installer/ownership.ts
|
|
3284
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync7, realpathSync as realpathSync2, rmSync as rmSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
3285
|
+
import { basename as basename2, dirname as dirname3, join as join4, resolve } from "node:path";
|
|
3286
|
+
function canonicalPath(file) {
|
|
3287
|
+
const absolute = resolve(file);
|
|
3288
|
+
if (existsSync6(absolute))
|
|
3289
|
+
return realpathSync2(absolute);
|
|
3290
|
+
return join4(canonicalPath(dirname3(absolute)), basename2(absolute));
|
|
3291
|
+
}
|
|
3292
|
+
function ownerOf(input) {
|
|
3293
|
+
return { recordPath: canonicalPath(statePathFor(input)), scope: input.scope, cwd: canonicalPath(input.cwd) };
|
|
3294
|
+
}
|
|
3295
|
+
function claimPaths(input, files) {
|
|
3296
|
+
return [
|
|
3297
|
+
...new Set(files.filter((file) => file.format !== "rules").map((file) => {
|
|
3298
|
+
const target = canonicalPath(file.file);
|
|
3299
|
+
return join4(dirname3(target), `.${basename2(target)}.deepseek-web-retrieval.${input.host}.owner.json`);
|
|
3300
|
+
}))
|
|
3301
|
+
];
|
|
3048
3302
|
}
|
|
3049
|
-
function
|
|
3050
|
-
|
|
3303
|
+
function readOwner(file) {
|
|
3304
|
+
if (!existsSync6(file))
|
|
3305
|
+
return;
|
|
3306
|
+
try {
|
|
3307
|
+
const value = JSON.parse(readFileSync7(file, "utf8"));
|
|
3308
|
+
if (isPlainObject(value) && typeof value.recordPath === "string" && typeof value.cwd === "string" && (value.scope === "project" || value.scope === "global")) {
|
|
3309
|
+
return { recordPath: value.recordPath, cwd: value.cwd, scope: value.scope };
|
|
3310
|
+
}
|
|
3311
|
+
} catch {}
|
|
3312
|
+
throw new Error(`Cannot read config ownership in ${file}; repair the ownership record before retrying.`);
|
|
3051
3313
|
}
|
|
3052
|
-
function
|
|
3053
|
-
|
|
3314
|
+
function assertOwner(input, claim) {
|
|
3315
|
+
const owner = readOwner(claim);
|
|
3316
|
+
if (owner === undefined || owner.recordPath === ownerOf(input).recordPath)
|
|
3317
|
+
return;
|
|
3318
|
+
throw new Error(`A ${input.host} install is already recorded at the ${owner.scope} scope (${owner.recordPath}) and claims the same config file. ` + `Uninstall it first: deepseek-web-retrieval uninstall --host ${input.host} --scope ${owner.scope} --cwd ${JSON.stringify(owner.cwd)}. ` + "Two install records must not edit one file.");
|
|
3054
3319
|
}
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3320
|
+
function assertFileOwnership(input, files) {
|
|
3321
|
+
for (const claim of claimPaths(input, files))
|
|
3322
|
+
assertOwner(input, claim);
|
|
3323
|
+
}
|
|
3324
|
+
function fileOwnershipNotes(input, files) {
|
|
3325
|
+
return claimPaths(input, files.filter((file) => file.shared)).map((claim) => `Shared config ownership is recorded in ${claim}; uninstall releases it. Keep this local metadata with the config.`);
|
|
3326
|
+
}
|
|
3327
|
+
function acquireFileOwnership(input, files) {
|
|
3328
|
+
const acquired = [];
|
|
3329
|
+
const owner = ownerOf(input);
|
|
3330
|
+
const releaseNew = () => {
|
|
3331
|
+
for (const claim of acquired.reverse())
|
|
3332
|
+
releaseClaim(input, claim);
|
|
3333
|
+
};
|
|
3334
|
+
try {
|
|
3335
|
+
for (const claim of claimPaths(input, files.filter((file) => file.shared))) {
|
|
3336
|
+
assertOwner(input, claim);
|
|
3337
|
+
mkdirSync2(dirname3(claim), { recursive: true });
|
|
3338
|
+
try {
|
|
3339
|
+
writeFileSync2(claim, `${JSON.stringify(owner)}
|
|
3340
|
+
`, { flag: "wx", mode: 384 });
|
|
3341
|
+
acquired.push(claim);
|
|
3342
|
+
} catch (error) {
|
|
3343
|
+
if (error.code !== "EEXIST")
|
|
3344
|
+
throw error;
|
|
3345
|
+
assertOwner(input, claim);
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
return releaseNew;
|
|
3349
|
+
} catch (error) {
|
|
3350
|
+
releaseNew();
|
|
3351
|
+
throw error;
|
|
3064
3352
|
}
|
|
3065
|
-
lines.push("- Never send localhost or private-network URLs to the DeepSeek tools; use local tools for those (the", " server blocks private hosts by default).", "- DeepSeek requests are billed per successful call; retrieve only what the task needs.");
|
|
3066
|
-
return lines;
|
|
3067
3353
|
}
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3354
|
+
function releaseClaim(input, claim) {
|
|
3355
|
+
const owner = readOwner(claim);
|
|
3356
|
+
if (owner === undefined)
|
|
3357
|
+
return;
|
|
3358
|
+
assertOwner(input, claim);
|
|
3359
|
+
rmSync3(claim);
|
|
3360
|
+
pruneEmptyDir(dirname3(claim));
|
|
3361
|
+
}
|
|
3362
|
+
function releaseFileOwnership(input, files) {
|
|
3363
|
+
for (const claim of claimPaths(input, files))
|
|
3364
|
+
releaseClaim(input, claim);
|
|
3365
|
+
}
|
|
3366
|
+
var init_ownership = __esm(() => {
|
|
3367
|
+
init_revert();
|
|
3368
|
+
init_state();
|
|
3071
3369
|
});
|
|
3072
3370
|
|
|
3073
|
-
// src/installer/
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3371
|
+
// src/installer/operation-lock.ts
|
|
3372
|
+
import { mkdirSync as mkdirSync3, rmSync as rmSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
3373
|
+
import { dirname as dirname4 } from "node:path";
|
|
3374
|
+
function withOperationLock(files, run) {
|
|
3375
|
+
const locks = [...new Set(files.map((file) => `${canonicalPath(file)}.deepseek-web-retrieval.lock`))].sort();
|
|
3376
|
+
const acquired = [];
|
|
3377
|
+
let outcome;
|
|
3378
|
+
try {
|
|
3379
|
+
for (const lock of locks) {
|
|
3380
|
+
mkdirSync3(dirname4(lock), { recursive: true });
|
|
3381
|
+
try {
|
|
3382
|
+
mkdirSync3(lock, { mode: 448 });
|
|
3383
|
+
} catch (error) {
|
|
3384
|
+
if (error.code !== "EEXIST")
|
|
3385
|
+
throw error;
|
|
3386
|
+
throw new Error(`Installer operation is locked at ${lock}. Wait for the other init/uninstall to finish. ` + "If it was interrupted, verify the config and install record and ensure no installer is running before removing this lock directory; retry with a new plan.");
|
|
3387
|
+
}
|
|
3388
|
+
acquired.push(lock);
|
|
3389
|
+
writeFileSync3(`${lock}/owner.json`, JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }), {
|
|
3390
|
+
mode: 384
|
|
3391
|
+
});
|
|
3392
|
+
}
|
|
3393
|
+
outcome = { ok: true, value: run() };
|
|
3394
|
+
} catch (error) {
|
|
3395
|
+
outcome = { ok: false, error };
|
|
3080
3396
|
}
|
|
3081
|
-
|
|
3082
|
-
errors
|
|
3397
|
+
{
|
|
3398
|
+
const errors = [];
|
|
3399
|
+
for (const lock of acquired.reverse()) {
|
|
3400
|
+
try {
|
|
3401
|
+
rmSync4(lock, { recursive: true });
|
|
3402
|
+
pruneEmptyDir(dirname4(lock));
|
|
3403
|
+
} catch (error) {
|
|
3404
|
+
errors.push(`${lock}: ${String(error)}`);
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
if (errors.length > 0) {
|
|
3408
|
+
throw new Error(`${outcome.ok ? "Operation finished" : String(outcome.error)}; could not release operation lock(s): ${errors.join("; ")}`, { cause: outcome.ok ? undefined : outcome.error });
|
|
3409
|
+
}
|
|
3083
3410
|
}
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
return { notes, errors, abort: false };
|
|
3411
|
+
if (!outcome.ok)
|
|
3412
|
+
throw outcome.error;
|
|
3413
|
+
return outcome.value;
|
|
3088
3414
|
}
|
|
3089
|
-
var
|
|
3090
|
-
|
|
3415
|
+
var init_operation_lock = __esm(() => {
|
|
3416
|
+
init_ownership();
|
|
3417
|
+
init_revert();
|
|
3091
3418
|
});
|
|
3092
3419
|
|
|
3093
|
-
// src/installer/
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
}
|
|
3100
|
-
function planCodex(input, _previous) {
|
|
3101
|
-
const paths = codexPaths(input);
|
|
3102
|
-
const loaded = codecFor("toml", input.host).load(paths.config);
|
|
3103
|
-
const tiers = { search: input.search, fetch: input.fetch };
|
|
3104
|
-
const header = planHeader(input, paths, loaded, "TOML");
|
|
3105
|
-
if (header.abort) {
|
|
3106
|
-
return { host: "codex", scope: input.scope, tiers, edits: [], notes: header.notes, errors: header.errors };
|
|
3420
|
+
// src/installer/apply.ts
|
|
3421
|
+
import { existsSync as existsSync7, readFileSync as readFileSync8, rmSync as rmSync5 } from "node:fs";
|
|
3422
|
+
import { dirname as dirname5 } from "node:path";
|
|
3423
|
+
function applyPlan(plan, input) {
|
|
3424
|
+
if (plan.errors.length > 0) {
|
|
3425
|
+
throw new Error(`refusing to apply a plan with errors: ${plan.errors.map((error) => error.message).join("; ")}`);
|
|
3107
3426
|
}
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
const
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3427
|
+
return withOperationLock([statePathFor(input)], () => withOperationLock([...plan.edits, ...plan.reverts ?? []].map((file) => file.file), () => applyLocked(plan, input)));
|
|
3428
|
+
}
|
|
3429
|
+
function applyLocked(plan, input) {
|
|
3430
|
+
const touched = [...plan.edits, ...plan.reverts ?? []];
|
|
3431
|
+
assertFileOwnership(input, touched);
|
|
3432
|
+
assertNoDrift(plan);
|
|
3433
|
+
const previous = readRecord(input);
|
|
3434
|
+
if (Object.hasOwn(plan, "previousRecord") && !deepEqual(previous, plan.previousRecord)) {
|
|
3435
|
+
throw new Error("The install record changed since the plan was rendered. Re-run init to plan against the current install.");
|
|
3116
3436
|
}
|
|
3117
|
-
const
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3437
|
+
const prepared = plan.edits.map((edit2) => prepareEdit(edit2, plan.host));
|
|
3438
|
+
const snapshots = snapshotTouchedFiles(plan);
|
|
3439
|
+
const files = [];
|
|
3440
|
+
const lines = [];
|
|
3441
|
+
const releaseNewClaims = acquireFileOwnership(input, touched);
|
|
3442
|
+
try {
|
|
3443
|
+
for (const item of prepared) {
|
|
3444
|
+
item.write();
|
|
3445
|
+
const desired = item.edit.format === "rules" ? [] : item.edit.ops;
|
|
3446
|
+
files.push(mergeRecorded(item.recorded, previous?.files.find((file) => file.file === item.edit.file), desired));
|
|
3447
|
+
lines.push(`${item.recorded.created ? "created" : "updated"} ${item.edit.file}`);
|
|
3448
|
+
}
|
|
3449
|
+
if ((plan.reverts ?? []).length > 0) {
|
|
3450
|
+
lines.push(...applyReverts(plan));
|
|
3451
|
+
}
|
|
3452
|
+
const verification = verifyEdits(plan.edits, plan.host);
|
|
3453
|
+
if (!verification.ok) {
|
|
3454
|
+
throw new Error(`verification failed: ${verification.lines.filter((line) => line.startsWith("FAIL")).join("; ")}`);
|
|
3455
|
+
}
|
|
3456
|
+
lines.push(...verification.lines);
|
|
3457
|
+
const record = {
|
|
3458
|
+
version: 1,
|
|
3459
|
+
host: plan.host,
|
|
3460
|
+
scope: plan.scope,
|
|
3461
|
+
tiers: plan.tiers,
|
|
3462
|
+
createdAt: new Date().toISOString(),
|
|
3463
|
+
files
|
|
3464
|
+
};
|
|
3465
|
+
writeRecord(input, record);
|
|
3466
|
+
} catch (error) {
|
|
3467
|
+
const rollback = rollbackApply(input, previous, snapshots);
|
|
3468
|
+
if (rollback.complete) {
|
|
3469
|
+
try {
|
|
3470
|
+
releaseNewClaims();
|
|
3471
|
+
} catch (releaseError) {
|
|
3472
|
+
rollback.complete = false;
|
|
3473
|
+
rollback.report.push(`could not release new config ownership: ${errorMessage(releaseError)}`);
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
const status = rollback.complete ? "rolled_back" : "recovery_required";
|
|
3477
|
+
const outcome = rollback.complete ? "was rolled back" : "recovery is required";
|
|
3478
|
+
const guidance = rollback.complete ? "Fix the cause and re-run init." : "Restore the reported files and ownership metadata before re-running init.";
|
|
3479
|
+
throw new ApplyError(status, `apply failed and ${outcome} (${errorMessage(error)}). Rollback report: ${rollback.report.join("; ")}. ${guidance}`, error);
|
|
3480
|
+
}
|
|
3481
|
+
try {
|
|
3482
|
+
releaseFileOwnership(input, (previous?.files ?? []).filter((file) => !files.some((current) => current.file === file.file)));
|
|
3483
|
+
} catch (error) {
|
|
3484
|
+
throw new ApplyError("recovery_required", `apply was written and verified, but old config ownership could not be released (${errorMessage(error)}). ` + "The new install record is retained; repair the reported ownership metadata before re-running init.", error);
|
|
3485
|
+
}
|
|
3486
|
+
return { lines, ok: true };
|
|
3487
|
+
}
|
|
3488
|
+
function errorMessage(error) {
|
|
3489
|
+
return error instanceof Error ? error.message : String(error);
|
|
3490
|
+
}
|
|
3491
|
+
function assertNoDrift(plan) {
|
|
3492
|
+
const drifts = [];
|
|
3493
|
+
for (const edit2 of plan.edits) {
|
|
3494
|
+
if (edit2.format === "rules")
|
|
3495
|
+
continue;
|
|
3496
|
+
const loaded = codecFor(edit2.format, plan.host).load(edit2.file);
|
|
3497
|
+
if (loaded.parseError) {
|
|
3498
|
+
drifts.push(`${edit2.file} no longer parses (${loaded.parseError})`);
|
|
3499
|
+
continue;
|
|
3500
|
+
}
|
|
3501
|
+
for (const op of edit2.ops) {
|
|
3502
|
+
if (op.op !== "set")
|
|
3503
|
+
continue;
|
|
3504
|
+
const current = valueAt(loaded.value, op.path);
|
|
3505
|
+
if (!deepEqual(current, op.previous)) {
|
|
3506
|
+
drifts.push(`${edit2.file}: ${op.path.join(".")} changed since the plan was rendered`);
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
}
|
|
3510
|
+
if (drifts.length > 0) {
|
|
3511
|
+
throw new Error(`refusing to apply — the config changed since the plan was rendered: ${drifts.join("; ")}. ` + "Re-run init to re-plan against the current files.");
|
|
3512
|
+
}
|
|
3513
|
+
}
|
|
3514
|
+
function snapshotTouchedFiles(plan) {
|
|
3515
|
+
const snapshots = new Map;
|
|
3516
|
+
const touched = [...plan.edits.map((edit2) => edit2.file), ...(plan.reverts ?? []).map((revert) => revert.file)];
|
|
3517
|
+
for (const file of new Set(touched)) {
|
|
3518
|
+
snapshots.set(file, existsSync7(file) ? { exists: true, text: readFileSync8(file, "utf8") } : { exists: false, text: "" });
|
|
3519
|
+
}
|
|
3520
|
+
return snapshots;
|
|
3521
|
+
}
|
|
3522
|
+
function rollbackApply(input, previous, snapshots) {
|
|
3523
|
+
const report = [];
|
|
3524
|
+
let complete = true;
|
|
3525
|
+
for (const [file, snapshot] of [...snapshots.entries()].reverse()) {
|
|
3526
|
+
try {
|
|
3527
|
+
if (snapshot.exists) {
|
|
3528
|
+
if (existsSync7(file) && readFileSync8(file, "utf8") === snapshot.text)
|
|
3529
|
+
continue;
|
|
3530
|
+
writeConfig(file, snapshot.text);
|
|
3531
|
+
if (readFileSync8(file, "utf8") !== snapshot.text)
|
|
3532
|
+
throw new Error("restored contents differ from the snapshot");
|
|
3533
|
+
} else {
|
|
3534
|
+
if (!existsSync7(file))
|
|
3535
|
+
continue;
|
|
3536
|
+
rmSync5(file);
|
|
3537
|
+
if (existsSync7(file))
|
|
3538
|
+
throw new Error("created file still exists after removal");
|
|
3539
|
+
pruneEmptyDir(dirname5(file));
|
|
3540
|
+
}
|
|
3541
|
+
report.push(`restored ${file}`);
|
|
3542
|
+
} catch (restoreError) {
|
|
3543
|
+
complete = false;
|
|
3544
|
+
report.push(`could not restore ${file}: ${errorMessage(restoreError)}`);
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
try {
|
|
3548
|
+
if (previous !== undefined)
|
|
3549
|
+
writeRecord(input, previous);
|
|
3550
|
+
else
|
|
3551
|
+
deleteRecord(input);
|
|
3552
|
+
if (previous === undefined ? existsSync7(statePathFor(input)) : !deepEqual(readRecord(input), previous)) {
|
|
3553
|
+
throw new Error("restored install record differs from its previous state");
|
|
3554
|
+
}
|
|
3555
|
+
} catch (restoreError) {
|
|
3556
|
+
complete = false;
|
|
3557
|
+
report.push(`could not restore the install record: ${errorMessage(restoreError)}`);
|
|
3558
|
+
}
|
|
3559
|
+
return { complete, report };
|
|
3560
|
+
}
|
|
3561
|
+
function prepareEdit(edit2, host) {
|
|
3562
|
+
const codec = codecFor(edit2.format, host);
|
|
3563
|
+
const loaded = codec.load(edit2.file);
|
|
3564
|
+
if (loaded.parseError) {
|
|
3565
|
+
throw new Error(`${edit2.file} does not parse: ${loaded.parseError}`);
|
|
3566
|
+
}
|
|
3567
|
+
const applied = codec.apply(loaded.text, edit2);
|
|
3568
|
+
const recorded = edit2.format === "rules" ? { file: edit2.file, format: "rules", created: !loaded.exists, ops: [], blockApplied: edit2.block } : {
|
|
3569
|
+
file: edit2.file,
|
|
3570
|
+
format: edit2.format,
|
|
3571
|
+
created: !loaded.exists,
|
|
3572
|
+
...edit2.shared ? { shared: true } : {},
|
|
3573
|
+
ops: applied.recorded,
|
|
3574
|
+
createdParents: applied.createdParents
|
|
3575
|
+
};
|
|
3576
|
+
return {
|
|
3577
|
+
edit: edit2,
|
|
3578
|
+
recorded,
|
|
3579
|
+
write: () => writeConfig(edit2.file, applied.text)
|
|
3580
|
+
};
|
|
3581
|
+
}
|
|
3582
|
+
function applyReverts(plan) {
|
|
3583
|
+
const lines = [];
|
|
3584
|
+
for (const revert of plan.reverts ?? []) {
|
|
3585
|
+
const whole = !plan.edits.some((edit2) => edit2.file === revert.file);
|
|
3586
|
+
lines.push(...revertRecordedFile(revert, plan.host, { label: " (previous init)", whole }));
|
|
3587
|
+
}
|
|
3588
|
+
return lines;
|
|
3589
|
+
}
|
|
3590
|
+
function mergeRecorded(current, before, desired) {
|
|
3591
|
+
if (before === undefined)
|
|
3592
|
+
return current;
|
|
3593
|
+
const ops = current.ops.map((op) => {
|
|
3594
|
+
const old = before.ops.find((candidate) => candidate.op === op.op && candidate.path.join("\x00") === op.path.join("\x00"));
|
|
3595
|
+
if (old === undefined)
|
|
3596
|
+
return op;
|
|
3597
|
+
if (op.op === "set" && old.op === "set" && deepEqual(op.previous, old.applied)) {
|
|
3598
|
+
return { ...op, previous: old.previous };
|
|
3599
|
+
}
|
|
3600
|
+
if (op.op === "append-array" && old.op === "append-array") {
|
|
3601
|
+
const wanted = desired.find((candidate) => candidate.op === "append-array" && candidate.path.join("\x00") === op.path.join("\x00"));
|
|
3602
|
+
const kept = (Array.isArray(old.applied) ? old.applied : []).filter((entry) => (wanted?.entries ?? []).some((candidate) => deepEqual(candidate, entry)));
|
|
3603
|
+
return { ...op, applied: [...kept, ...Array.isArray(op.applied) ? op.applied : []] };
|
|
3604
|
+
}
|
|
3605
|
+
return op;
|
|
3606
|
+
});
|
|
3607
|
+
return {
|
|
3608
|
+
...current,
|
|
3609
|
+
created: before.created,
|
|
3610
|
+
ops,
|
|
3611
|
+
createdParents: [...before.createdParents ?? [], ...current.createdParents ?? []]
|
|
3612
|
+
};
|
|
3613
|
+
}
|
|
3614
|
+
function verifyEdits(edits, host) {
|
|
3615
|
+
const lines = [];
|
|
3616
|
+
let ok = true;
|
|
3617
|
+
for (const edit2 of edits) {
|
|
3618
|
+
if (!existsSync7(edit2.file)) {
|
|
3619
|
+
lines.push(`FAIL ${edit2.file} does not exist after apply`);
|
|
3620
|
+
ok = false;
|
|
3621
|
+
continue;
|
|
3622
|
+
}
|
|
3623
|
+
const codec = codecFor(edit2.format, host);
|
|
3624
|
+
const text = readFileSync8(edit2.file, "utf8");
|
|
3625
|
+
if (edit2.format === "rules") {
|
|
3626
|
+
const good = codec.verify(text, edit2);
|
|
3627
|
+
lines.push(`${good ? "pass" : "FAIL"} ${edit2.file}: rules block ${good ? "present" : "missing"}`);
|
|
3628
|
+
ok = ok && good;
|
|
3629
|
+
continue;
|
|
3630
|
+
}
|
|
3631
|
+
for (const op of edit2.ops) {
|
|
3632
|
+
const good = codec.verify(text, edit2, op);
|
|
3633
|
+
lines.push(`${good ? "pass" : "FAIL"} ${edit2.file}: ${describeOp(op)} ${good ? "holds" : "missing"}`);
|
|
3634
|
+
ok = ok && good;
|
|
3635
|
+
}
|
|
3636
|
+
}
|
|
3637
|
+
return { lines, ok };
|
|
3638
|
+
}
|
|
3639
|
+
function describeOp(op) {
|
|
3640
|
+
if (op.op === "set")
|
|
3641
|
+
return `set ${op.path.join(".")}`;
|
|
3642
|
+
return `append ${op.path.join(".")}`;
|
|
3643
|
+
}
|
|
3644
|
+
var ApplyError;
|
|
3645
|
+
var init_apply = __esm(() => {
|
|
3646
|
+
init_formats();
|
|
3647
|
+
init_value();
|
|
3648
|
+
init_operation_lock();
|
|
3649
|
+
init_ownership();
|
|
3650
|
+
init_revert();
|
|
3651
|
+
init_state();
|
|
3652
|
+
ApplyError = class ApplyError extends Error {
|
|
3653
|
+
status;
|
|
3654
|
+
constructor(status, message, cause) {
|
|
3655
|
+
super(message, { cause });
|
|
3656
|
+
this.status = status;
|
|
3657
|
+
this.name = "ApplyError";
|
|
3658
|
+
}
|
|
3659
|
+
};
|
|
3660
|
+
});
|
|
3661
|
+
|
|
3662
|
+
// src/installer/detect.ts
|
|
3663
|
+
import { existsSync as existsSync8, readFileSync as readFileSync9 } from "node:fs";
|
|
3664
|
+
function replacingExistingEntryNote(path) {
|
|
3665
|
+
return `An existing ${path} entry is being replaced; its previous form is recorded for uninstall.`;
|
|
3666
|
+
}
|
|
3667
|
+
function othersStayRegisteredNote(others) {
|
|
3668
|
+
return `Other MCP servers stay registered (${others.join(", ")}); routing to DeepSeek is not exclusive while they offer web tools.`;
|
|
3669
|
+
}
|
|
3670
|
+
function looksWebCapable(name, entry) {
|
|
3671
|
+
if (WEB_TOOL_HINT.test(name))
|
|
3672
|
+
return true;
|
|
3673
|
+
if (!isPlainObject(entry))
|
|
3674
|
+
return false;
|
|
3675
|
+
const command = typeof entry.command === "string" ? entry.command : "";
|
|
3676
|
+
const url = typeof entry.url === "string" ? entry.url : "";
|
|
3677
|
+
const args = Array.isArray(entry.args) ? entry.args.filter((arg) => typeof arg === "string").join(" ") : "";
|
|
3678
|
+
return WEB_TOOL_HINT.test(`${command} ${args} ${url}`);
|
|
3679
|
+
}
|
|
3680
|
+
function looksLikeOwnPackage(source) {
|
|
3681
|
+
return /(^|[/:@])deepseek-web-retrieval($|[@/]|-\d)/.test(source);
|
|
3682
|
+
}
|
|
3683
|
+
function findCompetitorSteering(host, file) {
|
|
3684
|
+
if (!existsSync8(file))
|
|
3685
|
+
return [];
|
|
3686
|
+
let text;
|
|
3687
|
+
try {
|
|
3688
|
+
text = readFileSync9(file, "utf8");
|
|
3689
|
+
} catch {
|
|
3690
|
+
return [];
|
|
3691
|
+
}
|
|
3692
|
+
return removeBlock(host, text).split(`
|
|
3693
|
+
`).map((line) => line.trim()).filter((line) => line.length > 0 && STEERING_HINT.test(line) && !line.includes("deepseek")).slice(0, 2);
|
|
3694
|
+
}
|
|
3695
|
+
function competitorSteeringNote(file, lines) {
|
|
3696
|
+
const quoted = lines.map((line) => `"${line}"`).join(", ");
|
|
3697
|
+
return `Existing rules in ${file} steer toward other web tools (${quoted}); the plan surfaces this conflict — it does not resolve it.`;
|
|
3698
|
+
}
|
|
3699
|
+
function splitCompetitors(servers) {
|
|
3700
|
+
const competitors = [];
|
|
3701
|
+
const bystanders = [];
|
|
3702
|
+
for (const [name, entry] of Object.entries(servers)) {
|
|
3703
|
+
if (name === "deepseek")
|
|
3704
|
+
continue;
|
|
3705
|
+
(looksWebCapable(name, entry) ? competitors : bystanders).push(name);
|
|
3706
|
+
}
|
|
3707
|
+
return { competitors, bystanders };
|
|
3708
|
+
}
|
|
3709
|
+
var BOTH_CAPABILITIES_OFF = "Both capabilities are off; nothing would be installed.", FORCED_COST_NOTE, WEB_TOOL_HINT, STEERING_HINT;
|
|
3710
|
+
var init_detect = __esm(() => {
|
|
3711
|
+
init_rules();
|
|
3712
|
+
init_value();
|
|
3713
|
+
FORCED_COST_NOTE = "Forced routing is deterministic, and Search and Fetch are billed per successful request — " + `see the README's "Privacy and cost" section before applying.`;
|
|
3714
|
+
WEB_TOOL_HINT = /(web[_ -]?(search|fetch)|search|fetch|browse|scrape|crawl|tavily|brave|exa|perplexity|firecrawl|jina|serp|duckduckgo|reader)/i;
|
|
3715
|
+
STEERING_HINT = /(web[_ ]?(search|fetch)|websearch|webfetch|tavily|exa|brave|perplexity|firecrawl|jina|serp|duckduckgo|google|bing)/i;
|
|
3716
|
+
});
|
|
3717
|
+
|
|
3718
|
+
// src/installer/rulesContent.ts
|
|
3719
|
+
function rulesLines(ids, search, fetch) {
|
|
3720
|
+
const lines = ["DeepSeek retrieval guidance:"];
|
|
3721
|
+
if (search === "preferred") {
|
|
3722
|
+
lines.push(`- Use ${ids.search} when the task needs fresh external information: current docs, version numbers,`, " changelogs, or anything you cannot know from the repository alone. Do not search for facts already", " present locally.");
|
|
3723
|
+
}
|
|
3724
|
+
if (fetch === "preferred") {
|
|
3725
|
+
lines.push(`- Use ${ids.fetch} to read a specific public web page as Markdown when its content is needed.`);
|
|
3726
|
+
}
|
|
3727
|
+
lines.push("- Never send localhost or private-network URLs to the DeepSeek tools; use local tools for those (the", " server blocks private hosts by default).", "- DeepSeek requests are billed per successful call; retrieve only what the task needs.");
|
|
3728
|
+
return lines;
|
|
3729
|
+
}
|
|
3730
|
+
var MCP_TOOL_IDS, RAW_MCP_TOOL_IDS;
|
|
3731
|
+
var init_rulesContent = __esm(() => {
|
|
3732
|
+
MCP_TOOL_IDS = {
|
|
3733
|
+
search: "mcp__deepseek__deepseek_web_search",
|
|
3734
|
+
fetch: "mcp__deepseek__deepseek_fetch"
|
|
3735
|
+
};
|
|
3736
|
+
RAW_MCP_TOOL_IDS = { search: "deepseek_web_search", fetch: "deepseek_fetch" };
|
|
3737
|
+
});
|
|
3738
|
+
|
|
3739
|
+
// src/installer/hosts/shared.ts
|
|
3740
|
+
function planHeader(input, paths, loaded, dialect) {
|
|
3741
|
+
const notes = [];
|
|
3742
|
+
const errors = [];
|
|
3743
|
+
if (loaded.parseError) {
|
|
3744
|
+
errors.push({ message: `${paths.config} exists but does not parse as ${dialect}: ${loaded.parseError}` });
|
|
3745
|
+
return { notes, errors, abort: true };
|
|
3746
|
+
}
|
|
3747
|
+
if (input.search === "off" && input.fetch === "off") {
|
|
3748
|
+
errors.push({ message: BOTH_CAPABILITIES_OFF });
|
|
3749
|
+
}
|
|
3750
|
+
const rules = codecFor("rules", input.host).load(paths.rules);
|
|
3751
|
+
if (rules.parseError) {
|
|
3752
|
+
errors.push({ message: `${paths.rules}: ${rules.parseError}` });
|
|
3753
|
+
return { notes, errors, abort: true };
|
|
3754
|
+
}
|
|
3755
|
+
const steering = findCompetitorSteering(input.host, paths.rules);
|
|
3756
|
+
if (steering.length > 0)
|
|
3757
|
+
notes.push(competitorSteeringNote(paths.rules, steering));
|
|
3758
|
+
return { notes, errors, abort: false };
|
|
3759
|
+
}
|
|
3760
|
+
var init_shared = __esm(() => {
|
|
3761
|
+
init_detect();
|
|
3762
|
+
init_formats();
|
|
3763
|
+
});
|
|
3764
|
+
|
|
3765
|
+
// src/installer/hosts/codex.ts
|
|
3766
|
+
import { spawnSync } from "node:child_process";
|
|
3767
|
+
function probeCodexVersion(err) {
|
|
3768
|
+
const result = spawnSync("codex", ["--version"], { timeout: 5000, encoding: "utf8", env: process.env });
|
|
3769
|
+
const version = result.status === 0 ? parseCodexVersion(result.stdout ?? "") : undefined;
|
|
3770
|
+
if (version === undefined) {
|
|
3771
|
+
err("note: codex was not found on PATH or printed no version; the version-drift check was skipped");
|
|
3772
|
+
}
|
|
3773
|
+
return version;
|
|
3774
|
+
}
|
|
3775
|
+
function parseCodexVersion(output) {
|
|
3776
|
+
return /\bcodex(?:-cli)?\s+(\d+\.\d+\.\d+)\b/.exec(output)?.[1];
|
|
3777
|
+
}
|
|
3778
|
+
function codexPaths(input) {
|
|
3779
|
+
return {
|
|
3780
|
+
config: join2(input.home, ".codex/config.toml"),
|
|
3781
|
+
rules: input.scope === "global" ? join2(input.home, ".codex/AGENTS.md") : join2(input.cwd, "AGENTS.md")
|
|
3782
|
+
};
|
|
3783
|
+
}
|
|
3784
|
+
function planCodex(input, _previous) {
|
|
3785
|
+
const paths = codexPaths(input);
|
|
3786
|
+
const loaded = codecFor("toml", input.host).load(paths.config);
|
|
3787
|
+
const tiers = { search: input.search, fetch: input.fetch };
|
|
3788
|
+
const header = planHeader(input, paths, loaded, "TOML");
|
|
3789
|
+
if (header.abort) {
|
|
3790
|
+
return { host: "codex", scope: input.scope, tiers, edits: [], notes: header.notes, errors: header.errors };
|
|
3791
|
+
}
|
|
3792
|
+
const notes = [...header.notes];
|
|
3793
|
+
const errors = [...header.errors];
|
|
3794
|
+
const edits = [];
|
|
3795
|
+
if (input.hostVersion !== undefined && input.hostVersion !== CODEX_TESTED_VERSION) {
|
|
3796
|
+
notes.push(`Codex ${input.hostVersion} is installed, but this installer's Codex facts are verified against ` + `${CODEX_TESTED_VERSION} (the docs/hosts.jsonc lock). Kill-switch and trap behavior may have drifted ` + "between the two — re-verify the tiers if routing misbehaves.");
|
|
3797
|
+
}
|
|
3798
|
+
const tools = loaded.value.tools;
|
|
3799
|
+
if (isPlainObject(tools) && "web_search" in tools) {
|
|
3800
|
+
errors.push({
|
|
3801
|
+
message: `${paths.config} sets [tools] web_search, which is a silent no-op at Codex 0.163.0 (only the top-level ` + "web_search key controls the tool). Remove it first so the config cannot mislead."
|
|
3802
|
+
});
|
|
3803
|
+
}
|
|
3804
|
+
const servers = loaded.value.mcp_servers;
|
|
3805
|
+
if (isPlainObject(servers)) {
|
|
3806
|
+
if ("deepseek" in servers)
|
|
3807
|
+
notes.push(replacingExistingEntryNote("mcp_servers.deepseek"));
|
|
3808
|
+
const { competitors, bystanders } = splitCompetitors(servers);
|
|
3122
3809
|
if ((input.search === "forced" || input.fetch === "forced") && competitors.length > 0) {
|
|
3123
3810
|
errors.push({
|
|
3124
3811
|
message: `Forced cannot deliver exclusive routing: [mcp_servers.${competitors.join("], [mcp_servers.")}] look(s) web-capable, ` + "and Codex offers no per-server kill switch. Remove the server(s), disable their tools by hand via their " + "disabled_tools key, or choose a lower tier."
|
|
@@ -3128,27 +3815,29 @@ function planCodex(input, _previous) {
|
|
|
3128
3815
|
if (stayRegistered.length > 0)
|
|
3129
3816
|
notes.push(othersStayRegisteredNote(stayRegistered));
|
|
3130
3817
|
}
|
|
3131
|
-
const
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3818
|
+
const existingEntry = isPlainObject(servers) ? servers.deepseek : undefined;
|
|
3819
|
+
const rawToolIds = Object.values(RAW_MCP_TOOL_IDS);
|
|
3820
|
+
const keptDisabled = isPlainObject(existingEntry) && Array.isArray(existingEntry.disabled_tools) ? existingEntry.disabled_tools.filter((name) => typeof name === "string" && !rawToolIds.includes(name)) : [];
|
|
3821
|
+
const offDisabled = [
|
|
3822
|
+
...input.search === "off" ? [RAW_MCP_TOOL_IDS.search] : [],
|
|
3823
|
+
...input.fetch === "off" ? [RAW_MCP_TOOL_IDS.fetch] : []
|
|
3824
|
+
];
|
|
3136
3825
|
const entry = {
|
|
3137
3826
|
command: "npx",
|
|
3138
3827
|
args: ["-y", input.packageRef],
|
|
3139
|
-
...
|
|
3828
|
+
...keptDisabled.length > 0 || offDisabled.length > 0 ? { disabled_tools: [...keptDisabled, ...offDisabled] } : {},
|
|
3140
3829
|
env_vars: [API_KEY_ENV]
|
|
3141
3830
|
};
|
|
3142
|
-
const
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3831
|
+
const ops = [{ op: "set", path: ["mcp_servers", "deepseek"], value: entry, previous: existingEntry }];
|
|
3832
|
+
if (offDisabled.length > 0) {
|
|
3833
|
+
notes.push(`Off capabilities use Codex's own disabled_tools key (${offDisabled.join(", ")}): the tools are removed from ` + "the model's context while the server keeps running and stays re-enableable by deleting the entries.");
|
|
3834
|
+
}
|
|
3146
3835
|
if (input.search === "forced") {
|
|
3147
3836
|
const previous = typeof loaded.value.web_search === "string" ? loaded.value.web_search : undefined;
|
|
3148
3837
|
ops.push({ op: "set", path: ["web_search"], value: "disabled", previous });
|
|
3149
3838
|
notes.push(previous === undefined ? 'web_search is being set to "disabled" (no previous value existed; the default is "cached").' : `web_search is being set to "disabled"; the previous value "${previous}" is snapshotted for uninstall.`);
|
|
3150
3839
|
}
|
|
3151
|
-
edits.push({ file: paths.config, format: "toml", ops });
|
|
3840
|
+
edits.push({ file: paths.config, format: "toml", shared: true, ops });
|
|
3152
3841
|
notes.push(`Codex stdio children do not inherit the parent environment, so the entry forwards ${API_KEY_ENV} explicitly ` + "via env_vars; the key must be set in the shell that launches Codex.");
|
|
3153
3842
|
if (input.scope === "project") {
|
|
3154
3843
|
notes.push("Codex config lives in ~/.codex/config.toml only (user level), so the server registration and kill switch are " + "written there even for a project-scope install; the rules file stays project-local.");
|
|
@@ -3157,7 +3846,7 @@ function planCodex(input, _previous) {
|
|
|
3157
3846
|
notes.push(`${paths.config} contains comments; the TOML rewrite will drop them.`);
|
|
3158
3847
|
}
|
|
3159
3848
|
if (input.search === "forced" || input.fetch === "forced") {
|
|
3160
|
-
notes.push(
|
|
3849
|
+
notes.push(`Forced search is exclusion: the built-in web_search tool is disabled ("disabled" mode). ${FORCED_COST_NOTE}`);
|
|
3161
3850
|
}
|
|
3162
3851
|
if (input.search === "optional" || input.fetch === "optional") {
|
|
3163
3852
|
notes.push('Note: Codex ships a built-in web_search tool (default "cached" mode, an OpenAI index) that stays available ' + "alongside the DeepSeek tools under the optional tier.");
|
|
@@ -3172,18 +3861,24 @@ function planCodex(input, _previous) {
|
|
|
3172
3861
|
}
|
|
3173
3862
|
return { host: "codex", scope: input.scope, tiers, edits, notes, errors };
|
|
3174
3863
|
}
|
|
3864
|
+
var CODEX_TESTED_VERSION = "0.163.0";
|
|
3175
3865
|
var init_codex = __esm(() => {
|
|
3176
|
-
init_formats();
|
|
3177
|
-
init_detect();
|
|
3178
3866
|
init_constants();
|
|
3867
|
+
init_detect();
|
|
3868
|
+
init_formats();
|
|
3179
3869
|
init_rules();
|
|
3870
|
+
init_value();
|
|
3871
|
+
init_paths();
|
|
3180
3872
|
init_rulesContent();
|
|
3181
3873
|
init_shared();
|
|
3182
3874
|
});
|
|
3183
3875
|
|
|
3184
3876
|
// src/installer/hosts/opencode.ts
|
|
3185
3877
|
function opencodePaths(input) {
|
|
3186
|
-
return input.scope === "global" ? {
|
|
3878
|
+
return input.scope === "global" ? {
|
|
3879
|
+
config: join2(configHome(input), "opencode/opencode.jsonc"),
|
|
3880
|
+
rules: join2(configHome(input), "opencode/AGENTS.md")
|
|
3881
|
+
} : { config: join2(input.cwd, "opencode.jsonc"), rules: join2(input.cwd, "AGENTS.md") };
|
|
3187
3882
|
}
|
|
3188
3883
|
function planOpenCode(input, previous) {
|
|
3189
3884
|
const paths = opencodePaths(input);
|
|
@@ -3202,14 +3897,17 @@ function planOpenCode(input, previous) {
|
|
|
3202
3897
|
});
|
|
3203
3898
|
}
|
|
3204
3899
|
const plugins = Array.isArray(loaded.value.plugins) ? loaded.value.plugins : [];
|
|
3205
|
-
const
|
|
3206
|
-
|
|
3900
|
+
const trackedPackages = new Set(appendedByPrevious(previous, ["plugins"]).filter((entry) => isPlainObject(entry) && typeof entry.package === "string").map((entry) => entry.package));
|
|
3901
|
+
const pluginPackages = plugins.filter((entry) => isPlainObject(entry) && typeof entry.package === "string");
|
|
3902
|
+
const excused = previous === undefined ? new Set : new Set([...trackedPackages, input.packageRef]);
|
|
3903
|
+
const foreignOwn = pluginPackages.filter((entry) => looksLikeOwnPackage(entry.package) && !excused.has(entry.package));
|
|
3904
|
+
if (foreignOwn.length > 0) {
|
|
3207
3905
|
errors.push({
|
|
3208
3906
|
message: `${paths.config} already has a plugins entry referencing deepseek-web-retrieval that this installer did not ` + "write. Remove it (or run uninstall if it came from a previous init), then retry."
|
|
3209
3907
|
});
|
|
3210
3908
|
}
|
|
3211
|
-
if (
|
|
3212
|
-
notes.push("A previous init wrote the plugins entry; re-applying
|
|
3909
|
+
if (previous !== undefined && pluginPackages.some((entry) => excused.has(entry.package))) {
|
|
3910
|
+
notes.push("A previous init wrote the plugins entry; re-applying keeps it the only entry (a changed package ref or tier " + "options replace it, an unchanged one is a no-op) and the original snapshots carry over.");
|
|
3213
3911
|
}
|
|
3214
3912
|
if (errors.length > 0) {
|
|
3215
3913
|
return { host: "opencode", scope: input.scope, tiers, edits, notes, errors };
|
|
@@ -3253,7 +3951,7 @@ function planOpenCode(input, previous) {
|
|
|
3253
3951
|
notes.push("Search tier optional: the deepseek provider is registered but the built-in provider stays the default engine; " + "set websearch.provider to deepseek (or rerun with --search forced) to route search through DeepSeek.");
|
|
3254
3952
|
}
|
|
3255
3953
|
if (input.search === "forced" || input.fetch === "forced") {
|
|
3256
|
-
notes.push(
|
|
3954
|
+
notes.push(`Forced tier replaces built-in routing. ${FORCED_COST_NOTE}`);
|
|
3257
3955
|
}
|
|
3258
3956
|
if (input.fetch === "preferred") {
|
|
3259
3957
|
edits.push({
|
|
@@ -3267,19 +3965,164 @@ function planOpenCode(input, previous) {
|
|
|
3267
3965
|
}
|
|
3268
3966
|
var OPENCODE_TOOL_IDS;
|
|
3269
3967
|
var init_opencode = __esm(() => {
|
|
3270
|
-
init_formats();
|
|
3271
3968
|
init_detect();
|
|
3969
|
+
init_formats();
|
|
3272
3970
|
init_rules();
|
|
3971
|
+
init_value();
|
|
3972
|
+
init_paths();
|
|
3273
3973
|
init_rulesContent();
|
|
3974
|
+
init_state();
|
|
3274
3975
|
init_shared();
|
|
3275
3976
|
OPENCODE_TOOL_IDS = { search: "websearch (provider deepseek)", fetch: "deepseek_fetch" };
|
|
3276
3977
|
});
|
|
3277
3978
|
|
|
3979
|
+
// src/pi/settings.ts
|
|
3980
|
+
var SETTINGS_NAMESPACE = "deepseekWebRetrieval";
|
|
3981
|
+
var init_settings = () => {};
|
|
3982
|
+
|
|
3278
3983
|
// src/installer/hosts/pi.ts
|
|
3279
3984
|
function piPaths(input) {
|
|
3280
|
-
|
|
3985
|
+
const mcp = input.scope === "global" ? join2(input.home, ".pi/agent/mcp.json") : join2(input.cwd, ".pi/mcp.json");
|
|
3986
|
+
const settings = input.scope === "global" ? join2(input.home, ".pi/agent/settings.json") : join2(input.cwd, ".pi/settings.json");
|
|
3987
|
+
const rules = input.scope === "global" ? join2(input.home, ".pi/agent/AGENTS.md") : join2(input.cwd, "AGENTS.md");
|
|
3988
|
+
return { config: input.path === "mcp" ? mcp : settings, rules };
|
|
3989
|
+
}
|
|
3990
|
+
function piSourceRef(packageRef) {
|
|
3991
|
+
return packageRef.startsWith("npm:") ? packageRef : `npm:${packageRef}`;
|
|
3992
|
+
}
|
|
3993
|
+
function planPi(input, previous) {
|
|
3994
|
+
return input.path === "mcp" ? planPiMcp(input) : planPiExtension(input, previous);
|
|
3995
|
+
}
|
|
3996
|
+
function planPiExtension(input, previous) {
|
|
3997
|
+
const paths = piPaths(input);
|
|
3998
|
+
const loaded = codecFor("json", input.host).load(paths.config);
|
|
3999
|
+
const tiers = { search: input.search, fetch: input.fetch };
|
|
4000
|
+
const header = planHeader(input, paths, loaded, "JSON");
|
|
4001
|
+
if (header.abort) {
|
|
4002
|
+
return { host: "pi", scope: input.scope, tiers, edits: [], notes: header.notes, errors: header.errors };
|
|
4003
|
+
}
|
|
4004
|
+
const notes = [...header.notes];
|
|
4005
|
+
const errors = [...header.errors];
|
|
4006
|
+
const edits = [];
|
|
4007
|
+
const packages = Array.isArray(loaded.value.packages) ? loaded.value.packages : [];
|
|
4008
|
+
const source = piSourceRef(input.packageRef);
|
|
4009
|
+
const trackedSources = appendedByPrevious(previous, ["packages"]).filter((entry) => typeof entry === "string");
|
|
4010
|
+
const excused = previous === undefined ? new Set : new Set([...trackedSources, source]);
|
|
4011
|
+
const sources = packages.filter((entry) => typeof entry === "string");
|
|
4012
|
+
const foreignOwn = sources.filter((entry) => looksLikeOwnPackage(entry) && !excused.has(entry));
|
|
4013
|
+
if (foreignOwn.length > 0) {
|
|
4014
|
+
errors.push({
|
|
4015
|
+
message: `${paths.config} already lists ${foreignOwn.join(", ")} — this package (or a fork of it) that this installer did not ` + "write. Remove it (pi remove <source>, or uninstall if it came from a previous init), then retry."
|
|
4016
|
+
});
|
|
4017
|
+
}
|
|
4018
|
+
if (previous !== undefined && sources.some((entry) => excused.has(entry))) {
|
|
4019
|
+
notes.push("A previous init wrote the packages entry; re-applying reuses it (an unchanged source deduplicates, a changed " + "--package ref replaces the old source) and the original snapshots carry over.");
|
|
4020
|
+
}
|
|
4021
|
+
const forced = input.search === "forced" || input.fetch === "forced";
|
|
4022
|
+
if (forced) {
|
|
4023
|
+
const ownSources = new Set([...trackedSources, source]);
|
|
4024
|
+
const unexcludable = [...collectSources(loaded.value.packages), ...collectSources(loaded.value.extensions)].filter((entry) => !ownSources.has(entry) && looksWebCapable(entry, undefined));
|
|
4025
|
+
if (unexcludable.length > 0) {
|
|
4026
|
+
errors.push({
|
|
4027
|
+
message: `Forced cannot deliver exclusive routing: settings sources ${unexcludable.join(", ")} look web-capable, ` + "and Pi offers no per-package kill switch. Remove them (pi remove <source>) or choose a lower tier."
|
|
4028
|
+
});
|
|
4029
|
+
}
|
|
4030
|
+
}
|
|
4031
|
+
if (errors.length > 0) {
|
|
4032
|
+
return { host: "pi", scope: input.scope, tiers, edits: [], notes, errors };
|
|
4033
|
+
}
|
|
4034
|
+
const ops = [
|
|
4035
|
+
{ op: "append-array", path: ["packages"], entries: [source], existingCount: packages.length }
|
|
4036
|
+
];
|
|
4037
|
+
const namespace = isPlainObject(loaded.value[SETTINGS_NAMESPACE]) ? loaded.value[SETTINGS_NAMESPACE] : {};
|
|
4038
|
+
if (input.search === "off") {
|
|
4039
|
+
ops.push({
|
|
4040
|
+
op: "set",
|
|
4041
|
+
path: [SETTINGS_NAMESPACE, "enableSearch"],
|
|
4042
|
+
value: false,
|
|
4043
|
+
previous: typeof namespace.enableSearch === "boolean" ? namespace.enableSearch : undefined
|
|
4044
|
+
});
|
|
4045
|
+
}
|
|
4046
|
+
if (input.fetch === "off") {
|
|
4047
|
+
ops.push({
|
|
4048
|
+
op: "set",
|
|
4049
|
+
path: [SETTINGS_NAMESPACE, "enableFetch"],
|
|
4050
|
+
value: false,
|
|
4051
|
+
previous: typeof namespace.enableFetch === "boolean" ? namespace.enableFetch : undefined
|
|
4052
|
+
});
|
|
4053
|
+
}
|
|
4054
|
+
edits.push({ file: paths.config, format: "json", ops });
|
|
4055
|
+
notes.push("Pi resolves the packages source at session start; the first launch after apply fetches the package from npm " + "(pi update --extensions refreshes it later).");
|
|
4056
|
+
notes.push(`The extension reads ${API_KEY_ENV} from the Pi process environment; no env forwarding is needed. ` + "Set it in the shell that launches Pi.");
|
|
4057
|
+
if (input.search === "off" || input.fetch === "off") {
|
|
4058
|
+
notes.push(`Off capability switches are written into the "${SETTINGS_NAMESPACE}" settings namespace; the extension ` + "reads them at load (they outrank the DEEPSEEK_ENABLE_* environment variables).");
|
|
4059
|
+
}
|
|
4060
|
+
notes.push(`Extension tools are declared to the model with exposure "direct" by Pi's own default; no exposure key is needed.`);
|
|
4061
|
+
if (forced) {
|
|
4062
|
+
edits.push(...planMcpCompetitorDisables(input, notes, errors));
|
|
4063
|
+
}
|
|
4064
|
+
if (errors.length > 0) {
|
|
4065
|
+
return { host: "pi", scope: input.scope, tiers, edits: [], notes, errors };
|
|
4066
|
+
}
|
|
4067
|
+
if (input.search === "preferred" || input.fetch === "preferred") {
|
|
4068
|
+
edits.push({
|
|
4069
|
+
file: paths.rules,
|
|
4070
|
+
format: "rules",
|
|
4071
|
+
block: buildRulesBlock(input.host, rulesLines(RAW_MCP_TOOL_IDS, input.search, input.fetch))
|
|
4072
|
+
});
|
|
4073
|
+
notes.push(`Rules block upserted in ${paths.rules}; Pi loads AGENTS.md context files from the working directory and its parents.`);
|
|
4074
|
+
}
|
|
4075
|
+
if (forced) {
|
|
4076
|
+
notes.push("Forced tier: Pi 1.3.0 ships no built-in web tool, so the registered tools are the only web path (de-facto). " + FORCED_COST_NOTE);
|
|
4077
|
+
}
|
|
4078
|
+
return { host: "pi", scope: input.scope, tiers, edits, notes, errors };
|
|
4079
|
+
}
|
|
4080
|
+
function planCompetitorDisables(servers, competitors, notes, errors) {
|
|
4081
|
+
const ops = [];
|
|
4082
|
+
const excluded = [];
|
|
4083
|
+
const unexcludable = [];
|
|
4084
|
+
for (const name of competitors) {
|
|
4085
|
+
const current = servers[name];
|
|
4086
|
+
if (!isPlainObject(current)) {
|
|
4087
|
+
unexcludable.push(name);
|
|
4088
|
+
continue;
|
|
4089
|
+
}
|
|
4090
|
+
const previous = typeof current.enabled === "boolean" ? current.enabled : undefined;
|
|
4091
|
+
ops.push({ op: "set", path: ["mcpServers", name, "enabled"], value: false, previous });
|
|
4092
|
+
excluded.push(name);
|
|
4093
|
+
}
|
|
4094
|
+
if (excluded.length > 0) {
|
|
4095
|
+
notes.push(`Forced tier: web-capable MCP servers are disabled so DeepSeek routing is exclusive (${excluded.join(", ")}; ` + "prior enabled values are snapshotted for uninstall).");
|
|
4096
|
+
}
|
|
4097
|
+
if (unexcludable.length > 0) {
|
|
4098
|
+
errors.push({
|
|
4099
|
+
message: `Forced cannot deliver exclusive routing: mcpServers entries ${unexcludable.join(", ")} look web-capable ` + "but are not objects, so the plan cannot set their enabled key. Fix or remove them, or choose a lower tier."
|
|
4100
|
+
});
|
|
4101
|
+
}
|
|
4102
|
+
return ops;
|
|
4103
|
+
}
|
|
4104
|
+
function planMcpCompetitorDisables(input, notes, errors) {
|
|
4105
|
+
const mcpFile = input.scope === "global" ? join2(input.home, ".pi/agent/mcp.json") : join2(input.cwd, ".pi/mcp.json");
|
|
4106
|
+
const loaded = codecFor("json", input.host).load(mcpFile);
|
|
4107
|
+
if (loaded.parseError) {
|
|
4108
|
+
errors.push({ message: `${mcpFile} does not parse: ${loaded.parseError}` });
|
|
4109
|
+
return [];
|
|
4110
|
+
}
|
|
4111
|
+
const servers = isPlainObject(loaded.value.mcpServers) ? loaded.value.mcpServers : undefined;
|
|
4112
|
+
if (!isPlainObject(servers))
|
|
4113
|
+
return [];
|
|
4114
|
+
const { competitors, bystanders } = splitCompetitors(servers);
|
|
4115
|
+
const edits = [];
|
|
4116
|
+
if (competitors.length > 0) {
|
|
4117
|
+
const ops = planCompetitorDisables(servers, competitors, notes, errors);
|
|
4118
|
+
if (ops.length > 0)
|
|
4119
|
+
edits.push({ file: mcpFile, format: "json", ops });
|
|
4120
|
+
}
|
|
4121
|
+
if (bystanders.length > 0)
|
|
4122
|
+
notes.push(othersStayRegisteredNote(bystanders));
|
|
4123
|
+
return edits;
|
|
3281
4124
|
}
|
|
3282
|
-
function
|
|
4125
|
+
function planPiMcp(input) {
|
|
3283
4126
|
const paths = piPaths(input);
|
|
3284
4127
|
const loaded = codecFor("json", input.host).load(paths.config);
|
|
3285
4128
|
const tiers = { search: input.search, fetch: input.fetch };
|
|
@@ -3304,9 +4147,7 @@ function planPi(input, _previous) {
|
|
|
3304
4147
|
};
|
|
3305
4148
|
const servers = loaded.value.mcpServers;
|
|
3306
4149
|
const existingEntry = isPlainObject(servers) ? servers.deepseek : undefined;
|
|
3307
|
-
const ops = [
|
|
3308
|
-
{ op: "set", path: ["mcpServers", "deepseek"], value: entry, previous: existingEntry }
|
|
3309
|
-
];
|
|
4150
|
+
const ops = [{ op: "set", path: ["mcpServers", "deepseek"], value: entry, previous: existingEntry }];
|
|
3310
4151
|
edits.push({ file: paths.config, format: "json", ops });
|
|
3311
4152
|
if (isPlainObject(servers)) {
|
|
3312
4153
|
if (servers.deepseek !== undefined)
|
|
@@ -3314,28 +4155,7 @@ function planPi(input, _previous) {
|
|
|
3314
4155
|
const { competitors, bystanders } = splitCompetitors(servers);
|
|
3315
4156
|
const forced = input.search === "forced" || input.fetch === "forced";
|
|
3316
4157
|
if (forced) {
|
|
3317
|
-
|
|
3318
|
-
const unexcludable = [];
|
|
3319
|
-
for (const name of competitors) {
|
|
3320
|
-
const current = servers[name];
|
|
3321
|
-
if (!isPlainObject(current)) {
|
|
3322
|
-
unexcludable.push(name);
|
|
3323
|
-
continue;
|
|
3324
|
-
}
|
|
3325
|
-
const previous = typeof current.enabled === "boolean" ? current.enabled : undefined;
|
|
3326
|
-
if (previous === false)
|
|
3327
|
-
continue;
|
|
3328
|
-
ops.push({ op: "set", path: ["mcpServers", name, "enabled"], value: false, previous });
|
|
3329
|
-
excluded.push(name);
|
|
3330
|
-
}
|
|
3331
|
-
if (excluded.length > 0) {
|
|
3332
|
-
notes.push(`Forced tier: web-capable MCP servers are disabled so DeepSeek routing is exclusive (${excluded.join(", ")}; ` + "prior enabled values are snapshotted for uninstall).");
|
|
3333
|
-
}
|
|
3334
|
-
if (unexcludable.length > 0) {
|
|
3335
|
-
errors.push({
|
|
3336
|
-
message: `Forced cannot deliver exclusive routing: mcpServers entries ${unexcludable.join(", ")} look web-capable ` + "but are not objects, so the plan cannot set their enabled key. Fix or remove them, or choose a lower tier."
|
|
3337
|
-
});
|
|
3338
|
-
}
|
|
4158
|
+
ops.push(...planCompetitorDisables(servers, competitors, notes, errors));
|
|
3339
4159
|
}
|
|
3340
4160
|
const stayRegistered = forced ? bystanders : [...competitors, ...bystanders];
|
|
3341
4161
|
if (stayRegistered.length > 0)
|
|
@@ -3343,9 +4163,6 @@ function planPi(input, _previous) {
|
|
|
3343
4163
|
}
|
|
3344
4164
|
notes.push('"exposure": "direct" is required: Pi 1.3.0 defaults MCP tools to codemode, which hides them from the model.');
|
|
3345
4165
|
notes.push(`\`${API_KEY_ENV}\` is written as "\${${API_KEY_ENV}}" so the key expands from the shell that launches Pi and stays out of the file.`);
|
|
3346
|
-
if (loaded.lossy) {
|
|
3347
|
-
notes.push(`${paths.config} contains comments; the JSON edit preserves them.`);
|
|
3348
|
-
}
|
|
3349
4166
|
if (input.search === "preferred" || input.fetch === "preferred") {
|
|
3350
4167
|
edits.push({
|
|
3351
4168
|
file: paths.rules,
|
|
@@ -3359,81 +4176,56 @@ function planPi(input, _previous) {
|
|
|
3359
4176
|
}
|
|
3360
4177
|
return { host: "pi", scope: input.scope, tiers, edits, notes, errors };
|
|
3361
4178
|
}
|
|
4179
|
+
function collectSources(value) {
|
|
4180
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
4181
|
+
}
|
|
3362
4182
|
var init_pi = __esm(() => {
|
|
3363
|
-
init_formats();
|
|
3364
|
-
init_detect();
|
|
3365
4183
|
init_constants();
|
|
4184
|
+
init_settings();
|
|
4185
|
+
init_detect();
|
|
4186
|
+
init_formats();
|
|
3366
4187
|
init_rules();
|
|
4188
|
+
init_value();
|
|
4189
|
+
init_paths();
|
|
3367
4190
|
init_rulesContent();
|
|
4191
|
+
init_state();
|
|
3368
4192
|
init_shared();
|
|
3369
4193
|
});
|
|
3370
4194
|
|
|
3371
|
-
// src/installer/state.ts
|
|
3372
|
-
import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync5, readdirSync, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
3373
|
-
import { join as join2 } from "node:path";
|
|
3374
|
-
function statePathFor(input) {
|
|
3375
|
-
return join2(stateDir(input, input.scope), `install.${input.host}.json`);
|
|
3376
|
-
}
|
|
3377
|
-
function readRecord(input) {
|
|
3378
|
-
const file = statePathFor(input);
|
|
3379
|
-
if (!existsSync5(file))
|
|
3380
|
-
return;
|
|
3381
|
-
try {
|
|
3382
|
-
return JSON.parse(readFileSync5(file, "utf8"));
|
|
3383
|
-
} catch {
|
|
3384
|
-
return;
|
|
3385
|
-
}
|
|
3386
|
-
}
|
|
3387
|
-
function claimedByOtherScope(input, files) {
|
|
3388
|
-
const other = input.scope === "global" ? "project" : "global";
|
|
3389
|
-
const record = readRecord({ ...input, scope: other });
|
|
3390
|
-
if (record === undefined)
|
|
3391
|
-
return [];
|
|
3392
|
-
return files.filter((file) => record.files.some((candidate) => candidate.file === file));
|
|
3393
|
-
}
|
|
3394
|
-
function writeRecord(input, record) {
|
|
3395
|
-
const file = statePathFor(input);
|
|
3396
|
-
mkdirSync2(join2(file, ".."), { recursive: true });
|
|
3397
|
-
writeFileSync2(file, `${JSON.stringify(record, null, 2)}
|
|
3398
|
-
`, { mode: 384 });
|
|
3399
|
-
chmodSync(file, 384);
|
|
3400
|
-
}
|
|
3401
|
-
function deleteRecord(input) {
|
|
3402
|
-
const file = statePathFor(input);
|
|
3403
|
-
if (existsSync5(file))
|
|
3404
|
-
rmSync(file);
|
|
3405
|
-
}
|
|
3406
|
-
function listRecords(roots) {
|
|
3407
|
-
const records = [];
|
|
3408
|
-
for (const scope of ["global", "project"]) {
|
|
3409
|
-
const dir = stateDir(roots, scope);
|
|
3410
|
-
if (!existsSync5(dir))
|
|
3411
|
-
continue;
|
|
3412
|
-
for (const name of readdirSync(dir)) {
|
|
3413
|
-
if (!name.startsWith("install.") || !name.endsWith(".json"))
|
|
3414
|
-
continue;
|
|
3415
|
-
try {
|
|
3416
|
-
records.push(JSON.parse(readFileSync5(join2(dir, name), "utf8")));
|
|
3417
|
-
} catch {}
|
|
3418
|
-
}
|
|
3419
|
-
}
|
|
3420
|
-
return records;
|
|
3421
|
-
}
|
|
3422
|
-
var init_state = () => {};
|
|
3423
|
-
|
|
3424
4195
|
// src/installer/hosts/index.ts
|
|
3425
4196
|
function planForHost(host, input) {
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
4197
|
+
try {
|
|
4198
|
+
const previous = readRecord(input);
|
|
4199
|
+
const planned = withCrossScopeGuard(HOST_PLANNERS[host](input, previous), input);
|
|
4200
|
+
return {
|
|
4201
|
+
...withSecretGuard(withOrphanReverts(withLegacySharedConfigGuard(planned, previous), previous)),
|
|
4202
|
+
previousRecord: previous
|
|
4203
|
+
};
|
|
4204
|
+
} catch (error) {
|
|
4205
|
+
if (!(error instanceof InstallRecordError))
|
|
4206
|
+
throw error;
|
|
4207
|
+
return {
|
|
4208
|
+
host,
|
|
4209
|
+
scope: input.scope,
|
|
4210
|
+
tiers: { search: input.search, fetch: input.fetch },
|
|
4211
|
+
edits: [],
|
|
4212
|
+
notes: [],
|
|
4213
|
+
errors: [{ message: error.message }]
|
|
4214
|
+
};
|
|
4215
|
+
}
|
|
3429
4216
|
}
|
|
3430
4217
|
function withCrossScopeGuard(plan, input) {
|
|
3431
4218
|
if (plan.errors.length > 0)
|
|
3432
4219
|
return plan;
|
|
4220
|
+
try {
|
|
4221
|
+
assertFileOwnership(input, plan.edits);
|
|
4222
|
+
} catch (error) {
|
|
4223
|
+
return { ...plan, edits: [], reverts: undefined, errors: [{ message: String(error) }] };
|
|
4224
|
+
}
|
|
3433
4225
|
const other = input.scope === "global" ? "project" : "global";
|
|
3434
4226
|
const overlap = claimedByOtherScope(input, plan.edits.map((edit2) => edit2.file));
|
|
3435
4227
|
if (overlap.length === 0)
|
|
3436
|
-
return plan;
|
|
4228
|
+
return { ...plan, notes: [...plan.notes, ...fileOwnershipNotes(input, plan.edits)] };
|
|
3437
4229
|
return {
|
|
3438
4230
|
...plan,
|
|
3439
4231
|
edits: [],
|
|
@@ -3445,6 +4237,30 @@ function withCrossScopeGuard(plan, input) {
|
|
|
3445
4237
|
]
|
|
3446
4238
|
};
|
|
3447
4239
|
}
|
|
4240
|
+
function withLegacySharedConfigGuard(plan, previous) {
|
|
4241
|
+
if (plan.errors.length > 0 || previous !== undefined)
|
|
4242
|
+
return plan;
|
|
4243
|
+
for (const edit2 of plan.edits) {
|
|
4244
|
+
if (edit2.format === "rules" || !edit2.shared)
|
|
4245
|
+
continue;
|
|
4246
|
+
for (const op of edit2.ops) {
|
|
4247
|
+
const old = op.previous;
|
|
4248
|
+
if (op.op !== "set" || !isPlainObject(old) || old.command !== "npx" || !Array.isArray(old.args) || old.args[0] !== "-y" || !Array.isArray(old.env_vars) || !old.env_vars.includes(API_KEY_ENV))
|
|
4249
|
+
continue;
|
|
4250
|
+
return {
|
|
4251
|
+
...plan,
|
|
4252
|
+
edits: [],
|
|
4253
|
+
reverts: undefined,
|
|
4254
|
+
errors: [
|
|
4255
|
+
{
|
|
4256
|
+
message: `${edit2.file} contains an installer-style registration without an install record for this location. ` + "It may belong to an older install in another project. Re-run init or uninstall from that original project and scope first; " + "if the registration was created manually, remove it before retrying. No existing registration was adopted."
|
|
4257
|
+
}
|
|
4258
|
+
]
|
|
4259
|
+
};
|
|
4260
|
+
}
|
|
4261
|
+
}
|
|
4262
|
+
return plan;
|
|
4263
|
+
}
|
|
3448
4264
|
function withSecretGuard(plan) {
|
|
3449
4265
|
for (const edit2 of plan.edits) {
|
|
3450
4266
|
if (edit2.format === "rules")
|
|
@@ -3483,224 +4299,52 @@ function findSecretLike(value, at) {
|
|
|
3483
4299
|
}
|
|
3484
4300
|
return;
|
|
3485
4301
|
}
|
|
3486
|
-
function withOrphanReverts(plan,
|
|
4302
|
+
function withOrphanReverts(plan, previous) {
|
|
3487
4303
|
if (plan.errors.length > 0)
|
|
3488
4304
|
return plan;
|
|
3489
4305
|
if (previous === undefined)
|
|
3490
4306
|
return plan;
|
|
3491
4307
|
const reverts = [];
|
|
3492
4308
|
for (const file of previous.files) {
|
|
3493
|
-
const edit2 = plan.edits.find((candidate) => candidate.file === file.file);
|
|
3494
|
-
if (edit2 === undefined) {
|
|
3495
|
-
reverts.push(file);
|
|
3496
|
-
continue;
|
|
3497
|
-
}
|
|
3498
|
-
if (edit2.format === "rules" || file.format === "rules")
|
|
3499
|
-
continue;
|
|
3500
|
-
const orphanOps = file.ops.filter((op) => !edit2.ops.some((candidate) => candidate.op === op.op && candidate.path.join("\x00") === op.path.join("\x00")));
|
|
3501
|
-
if (orphanOps.length > 0)
|
|
3502
|
-
reverts.push({ ...file, ops: orphanOps });
|
|
3503
|
-
}
|
|
3504
|
-
if (reverts.length === 0)
|
|
3505
|
-
return plan;
|
|
3506
|
-
return { ...plan, reverts };
|
|
3507
|
-
}
|
|
3508
|
-
var HOST_PLANNERS;
|
|
3509
|
-
var init_hosts = __esm(() => {
|
|
3510
|
-
init_codex();
|
|
3511
|
-
init_opencode();
|
|
3512
|
-
init_pi();
|
|
3513
|
-
init_constants();
|
|
3514
|
-
init_state();
|
|
3515
|
-
HOST_PLANNERS = {
|
|
3516
|
-
opencode: planOpenCode,
|
|
3517
|
-
pi: planPi,
|
|
3518
|
-
codex: planCodex
|
|
3519
|
-
};
|
|
3520
|
-
});
|
|
3521
|
-
|
|
3522
|
-
// src/installer/revert.ts
|
|
3523
|
-
import { existsSync as existsSync6, readFileSync as readFileSync6, rmdirSync, unlinkSync } from "node:fs";
|
|
3524
|
-
import { dirname as dirname2 } from "node:path";
|
|
3525
|
-
function pruneEmptyDir(dir) {
|
|
3526
|
-
try {
|
|
3527
|
-
rmdirSync(dir);
|
|
3528
|
-
} catch {}
|
|
3529
|
-
}
|
|
3530
|
-
function revertRecordedFile(file, host, opts) {
|
|
3531
|
-
const lines = [];
|
|
3532
|
-
if (!existsSync6(file.file)) {
|
|
3533
|
-
lines.push(`already absent: ${file.file}${opts.label}`);
|
|
3534
|
-
return lines;
|
|
3535
|
-
}
|
|
3536
|
-
const codec = codecFor(file.format, host);
|
|
3537
|
-
const text = readFileSync6(file.file, "utf8");
|
|
3538
|
-
if (file.format === "rules") {
|
|
3539
|
-
const edit2 = { file: file.file, format: "rules", block: file.blockApplied ?? "" };
|
|
3540
|
-
if (!codec.verify(text, edit2)) {
|
|
3541
|
-
lines.push(`no rules block${opts.label} in ${file.file}; left untouched`);
|
|
3542
|
-
return lines;
|
|
3543
|
-
}
|
|
3544
|
-
const updated = codec.revert(text, file).text;
|
|
3545
|
-
if (opts.whole && codec.isEffectivelyEmpty(updated)) {
|
|
3546
|
-
unlinkSync(file.file);
|
|
3547
|
-
pruneEmptyDir(dirname2(file.file));
|
|
3548
|
-
lines.push(`deleted ${file.file} (installer-created, now empty)`);
|
|
3549
|
-
} else {
|
|
3550
|
-
writeConfig(file.file, updated);
|
|
3551
|
-
lines.push(`removed rules block${opts.label} from ${file.file}`);
|
|
3552
|
-
}
|
|
3553
|
-
return lines;
|
|
3554
|
-
}
|
|
3555
|
-
const reverted = codec.revert(text, file);
|
|
3556
|
-
if (opts.whole && file.created && codec.isEffectivelyEmpty(reverted.text)) {
|
|
3557
|
-
unlinkSync(file.file);
|
|
3558
|
-
pruneEmptyDir(dirname2(file.file));
|
|
3559
|
-
lines.push(`deleted ${file.file} (installer-created, now empty)`);
|
|
3560
|
-
} else {
|
|
3561
|
-
writeConfig(file.file, reverted.text);
|
|
3562
|
-
lines.push(`reverted artifacts${opts.label} in ${file.file}`);
|
|
3563
|
-
}
|
|
3564
|
-
lines.push(...reverted.notes.map((note) => ` note: ${note}`));
|
|
3565
|
-
return lines;
|
|
3566
|
-
}
|
|
3567
|
-
var init_revert = __esm(() => {
|
|
3568
|
-
init_formats();
|
|
3569
|
-
});
|
|
3570
|
-
|
|
3571
|
-
// src/installer/apply.ts
|
|
3572
|
-
import { existsSync as existsSync7, readFileSync as readFileSync7 } from "node:fs";
|
|
3573
|
-
function applyPlan(plan, input) {
|
|
3574
|
-
if (plan.errors.length > 0) {
|
|
3575
|
-
throw new Error(`refusing to apply a plan with errors: ${plan.errors.map((error) => error.message).join("; ")}`);
|
|
3576
|
-
}
|
|
3577
|
-
assertNoDrift(plan);
|
|
3578
|
-
const previous = readRecord(input);
|
|
3579
|
-
const prepared = plan.edits.map((edit2) => prepareEdit(edit2, plan.host));
|
|
3580
|
-
const files = [];
|
|
3581
|
-
const lines = [];
|
|
3582
|
-
for (const item of prepared) {
|
|
3583
|
-
item.write();
|
|
3584
|
-
files.push(mergeRecorded(item.recorded, previous?.files.find((file) => file.file === item.edit.file)));
|
|
3585
|
-
lines.push(`${item.recorded.created ? "created" : "updated"} ${item.edit.file}`);
|
|
3586
|
-
}
|
|
3587
|
-
if ((plan.reverts ?? []).length > 0) {
|
|
3588
|
-
lines.push(...applyReverts(plan));
|
|
3589
|
-
}
|
|
3590
|
-
const record = {
|
|
3591
|
-
version: 1,
|
|
3592
|
-
host: plan.host,
|
|
3593
|
-
scope: plan.scope,
|
|
3594
|
-
tiers: plan.tiers,
|
|
3595
|
-
createdAt: new Date().toISOString(),
|
|
3596
|
-
files
|
|
3597
|
-
};
|
|
3598
|
-
writeRecord(input, record);
|
|
3599
|
-
const verification = verifyEdits(plan.edits, plan.host);
|
|
3600
|
-
lines.push(...verification.lines);
|
|
3601
|
-
return { lines, ok: verification.ok };
|
|
3602
|
-
}
|
|
3603
|
-
function assertNoDrift(plan) {
|
|
3604
|
-
const drifts = [];
|
|
3605
|
-
for (const edit2 of plan.edits) {
|
|
3606
|
-
if (edit2.format === "rules")
|
|
3607
|
-
continue;
|
|
3608
|
-
const loaded = codecFor(edit2.format, plan.host).load(edit2.file);
|
|
3609
|
-
if (loaded.parseError) {
|
|
3610
|
-
drifts.push(`${edit2.file} no longer parses (${loaded.parseError})`);
|
|
3611
|
-
continue;
|
|
3612
|
-
}
|
|
3613
|
-
for (const op of edit2.ops) {
|
|
3614
|
-
if (op.op !== "set")
|
|
3615
|
-
continue;
|
|
3616
|
-
const current = valueAt(loaded.value, op.path);
|
|
3617
|
-
if (!deepEqual(current, op.previous)) {
|
|
3618
|
-
drifts.push(`${edit2.file}: ${op.path.join(".")} changed since the plan was rendered`);
|
|
3619
|
-
}
|
|
3620
|
-
}
|
|
3621
|
-
}
|
|
3622
|
-
if (drifts.length > 0) {
|
|
3623
|
-
throw new Error(`refusing to apply — the config changed since the plan was rendered: ${drifts.join("; ")}. ` + "Re-run init to re-plan against the current files.");
|
|
3624
|
-
}
|
|
3625
|
-
}
|
|
3626
|
-
function prepareEdit(edit2, host) {
|
|
3627
|
-
const codec = codecFor(edit2.format, host);
|
|
3628
|
-
const loaded = codec.load(edit2.file);
|
|
3629
|
-
if (loaded.parseError) {
|
|
3630
|
-
throw new Error(`${edit2.file} does not parse: ${loaded.parseError}`);
|
|
3631
|
-
}
|
|
3632
|
-
const applied = codec.apply(loaded.text, edit2);
|
|
3633
|
-
const recorded = edit2.format === "rules" ? { file: edit2.file, format: "rules", created: !loaded.exists, ops: [], blockApplied: edit2.block } : { file: edit2.file, format: edit2.format, created: !loaded.exists, ops: applied.recorded, createdParents: applied.createdParents };
|
|
3634
|
-
return {
|
|
3635
|
-
edit: edit2,
|
|
3636
|
-
recorded,
|
|
3637
|
-
write: () => writeConfig(edit2.file, applied.text)
|
|
3638
|
-
};
|
|
3639
|
-
}
|
|
3640
|
-
function applyReverts(plan) {
|
|
3641
|
-
const lines = [];
|
|
3642
|
-
for (const revert of plan.reverts ?? []) {
|
|
3643
|
-
const whole = !plan.edits.some((edit2) => edit2.file === revert.file);
|
|
3644
|
-
lines.push(...revertRecordedFile(revert, plan.host, { label: " (previous init)", whole }));
|
|
3645
|
-
}
|
|
3646
|
-
return lines;
|
|
3647
|
-
}
|
|
3648
|
-
function mergeRecorded(current, before) {
|
|
3649
|
-
if (before === undefined)
|
|
3650
|
-
return current;
|
|
3651
|
-
const ops = current.ops.map((op) => {
|
|
3652
|
-
const old = before.ops.find((candidate) => candidate.op === op.op && candidate.path.join("\x00") === op.path.join("\x00"));
|
|
3653
|
-
if (old === undefined)
|
|
3654
|
-
return op;
|
|
3655
|
-
if (op.op === "set" && old.op === "set" && deepEqual(op.previous, old.applied)) {
|
|
3656
|
-
return { ...op, previous: old.previous };
|
|
3657
|
-
}
|
|
3658
|
-
if (op.op === "append-array" && old.op === "append-array") {
|
|
3659
|
-
return { ...op, applied: [...old.applied, ...op.applied] };
|
|
3660
|
-
}
|
|
3661
|
-
return op;
|
|
3662
|
-
});
|
|
3663
|
-
return {
|
|
3664
|
-
...current,
|
|
3665
|
-
created: before.created,
|
|
3666
|
-
ops,
|
|
3667
|
-
createdParents: [...before.createdParents ?? [], ...current.createdParents ?? []]
|
|
3668
|
-
};
|
|
3669
|
-
}
|
|
3670
|
-
function verifyEdits(edits, host) {
|
|
3671
|
-
const lines = [];
|
|
3672
|
-
let ok = true;
|
|
3673
|
-
for (const edit2 of edits) {
|
|
3674
|
-
if (!existsSync7(edit2.file)) {
|
|
3675
|
-
lines.push(`FAIL ${edit2.file} does not exist after apply`);
|
|
3676
|
-
ok = false;
|
|
3677
|
-
continue;
|
|
3678
|
-
}
|
|
3679
|
-
const codec = codecFor(edit2.format, host);
|
|
3680
|
-
const text = readFileSync7(edit2.file, "utf8");
|
|
3681
|
-
if (edit2.format === "rules") {
|
|
3682
|
-
const good = codec.verify(text, edit2);
|
|
3683
|
-
lines.push(`${good ? "pass" : "FAIL"} ${edit2.file}: rules block ${good ? "present" : "missing"}`);
|
|
3684
|
-
ok = ok && good;
|
|
3685
|
-
continue;
|
|
3686
|
-
}
|
|
3687
|
-
for (const op of edit2.ops) {
|
|
3688
|
-
const good = codec.verify(text, edit2, op);
|
|
3689
|
-
lines.push(`${good ? "pass" : "FAIL"} ${edit2.file}: ${describeOp(op)} ${good ? "holds" : "missing"}`);
|
|
3690
|
-
ok = ok && good;
|
|
4309
|
+
const edit2 = plan.edits.find((candidate) => candidate.file === file.file);
|
|
4310
|
+
if (edit2 === undefined) {
|
|
4311
|
+
reverts.push(file);
|
|
4312
|
+
continue;
|
|
3691
4313
|
}
|
|
4314
|
+
if (edit2.format === "rules" || file.format === "rules")
|
|
4315
|
+
continue;
|
|
4316
|
+
const orphanOps = file.ops.flatMap((op) => {
|
|
4317
|
+
const claimed = edit2.ops.find((candidate) => candidate.op === op.op && candidate.path.join("\x00") === op.path.join("\x00"));
|
|
4318
|
+
if (op.op !== "append-array" || claimed === undefined || claimed.op !== "append-array") {
|
|
4319
|
+
return claimed === undefined ? [op] : [];
|
|
4320
|
+
}
|
|
4321
|
+
const dropped = (Array.isArray(op.applied) ? op.applied : []).filter((entry) => !(claimed.entries ?? []).some((wanted) => deepEqual(wanted, entry)));
|
|
4322
|
+
return dropped.length > 0 ? [{ ...op, applied: dropped }] : [];
|
|
4323
|
+
});
|
|
4324
|
+
if (orphanOps.length > 0)
|
|
4325
|
+
reverts.push({ ...file, ops: orphanOps });
|
|
3692
4326
|
}
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
if (op.op === "set")
|
|
3697
|
-
return `set ${op.path.join(".")}`;
|
|
3698
|
-
return `append ${op.path.join(".")}`;
|
|
4327
|
+
if (reverts.length === 0)
|
|
4328
|
+
return plan;
|
|
4329
|
+
return { ...plan, reverts };
|
|
3699
4330
|
}
|
|
3700
|
-
var
|
|
3701
|
-
|
|
3702
|
-
|
|
4331
|
+
var HOST_PLANNERS, HOST_VERSION_PROBES;
|
|
4332
|
+
var init_hosts = __esm(() => {
|
|
4333
|
+
init_constants();
|
|
4334
|
+
init_value();
|
|
4335
|
+
init_ownership();
|
|
3703
4336
|
init_state();
|
|
4337
|
+
init_codex();
|
|
4338
|
+
init_opencode();
|
|
4339
|
+
init_pi();
|
|
4340
|
+
HOST_PLANNERS = {
|
|
4341
|
+
opencode: planOpenCode,
|
|
4342
|
+
pi: planPi,
|
|
4343
|
+
codex: planCodex
|
|
4344
|
+
};
|
|
4345
|
+
HOST_VERSION_PROBES = {
|
|
4346
|
+
codex: probeCodexVersion
|
|
4347
|
+
};
|
|
3704
4348
|
});
|
|
3705
4349
|
|
|
3706
4350
|
// src/installer/render.ts
|
|
@@ -3764,25 +4408,43 @@ function preview(value) {
|
|
|
3764
4408
|
|
|
3765
4409
|
// src/installer/uninstall.ts
|
|
3766
4410
|
function uninstall(input) {
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
4411
|
+
return withOperationLock([statePathFor(input)], () => {
|
|
4412
|
+
const record = readRecord(input);
|
|
4413
|
+
if (record === undefined) {
|
|
4414
|
+
throw new Error(`No ${input.host} install is recorded for the ${input.scope} scope.`);
|
|
4415
|
+
}
|
|
4416
|
+
return withOperationLock(record.files.map((file) => file.file), () => uninstallLocked(input, record));
|
|
4417
|
+
});
|
|
4418
|
+
}
|
|
4419
|
+
function uninstallLocked(input, record) {
|
|
4420
|
+
assertFileOwnership(input, record.files);
|
|
3771
4421
|
const other = input.scope === "global" ? "project" : "global";
|
|
3772
4422
|
const overlap = claimedByOtherScope(input, record.files.map((file) => file.file));
|
|
3773
4423
|
if (overlap.length > 0) {
|
|
3774
4424
|
throw new Error(`A ${input.host} install is also recorded at the ${other} scope and claims the same file(s): ${overlap.join(", ")}. ` + `Uninstall the ${other}-scope install first so this rollback cannot clobber its entries.`);
|
|
3775
4425
|
}
|
|
3776
4426
|
const lines = [];
|
|
4427
|
+
for (const file of record.files) {
|
|
4428
|
+
if (file.format !== "rules")
|
|
4429
|
+
continue;
|
|
4430
|
+
const loaded = codecFor("rules", record.host).load(file.file);
|
|
4431
|
+
if (loaded.parseError)
|
|
4432
|
+
throw new Error(`${file.file}: ${loaded.parseError}`);
|
|
4433
|
+
}
|
|
3777
4434
|
for (const file of [...record.files].reverse()) {
|
|
3778
4435
|
lines.push(...revertRecordedFile(file, record.host, { label: "", whole: true }));
|
|
3779
4436
|
}
|
|
3780
4437
|
deleteRecord(input);
|
|
4438
|
+
releaseFileOwnership(input, record.files);
|
|
3781
4439
|
pruneEmptyDir(stateDir(input, input.scope));
|
|
3782
4440
|
lines.push(`removed install record (${input.host}, ${input.scope})`);
|
|
3783
4441
|
return lines;
|
|
3784
4442
|
}
|
|
3785
4443
|
var init_uninstall = __esm(() => {
|
|
4444
|
+
init_formats();
|
|
4445
|
+
init_operation_lock();
|
|
4446
|
+
init_ownership();
|
|
4447
|
+
init_paths();
|
|
3786
4448
|
init_revert();
|
|
3787
4449
|
init_state();
|
|
3788
4450
|
});
|
|
@@ -3793,7 +4455,7 @@ __export(exports_cli, {
|
|
|
3793
4455
|
runInstallerCli: () => runInstallerCli
|
|
3794
4456
|
});
|
|
3795
4457
|
import { homedir } from "node:os";
|
|
3796
|
-
import { resolve } from "node:path";
|
|
4458
|
+
import { resolve as resolve2 } from "node:path";
|
|
3797
4459
|
function parseFlags(args) {
|
|
3798
4460
|
const values = new Map;
|
|
3799
4461
|
const booleans = new Set;
|
|
@@ -3807,6 +4469,10 @@ function parseFlags(args) {
|
|
|
3807
4469
|
const eq = arg.indexOf("=");
|
|
3808
4470
|
const name = eq === -1 ? arg.slice(2) : arg.slice(2, eq);
|
|
3809
4471
|
if (BOOLEAN_FLAGS.has(name)) {
|
|
4472
|
+
if (eq !== -1) {
|
|
4473
|
+
problems.push(`--${name} does not accept a value; use --${name} to enable it or omit it`);
|
|
4474
|
+
continue;
|
|
4475
|
+
}
|
|
3810
4476
|
booleans.add(name);
|
|
3811
4477
|
continue;
|
|
3812
4478
|
}
|
|
@@ -3844,11 +4510,17 @@ async function runInstallerCli(argv, io = {}) {
|
|
|
3844
4510
|
${USAGE}`);
|
|
3845
4511
|
return 1;
|
|
3846
4512
|
}
|
|
3847
|
-
const cwd =
|
|
4513
|
+
const cwd = resolve2(flags.values.get("cwd") ?? process.cwd());
|
|
3848
4514
|
const home = process.env.HOME ?? homedir();
|
|
3849
4515
|
const configHome2 = resolveConfigHome(home);
|
|
3850
4516
|
if (command === "list") {
|
|
3851
|
-
|
|
4517
|
+
let records;
|
|
4518
|
+
try {
|
|
4519
|
+
records = listRecords({ home, cwd, configHome: configHome2 });
|
|
4520
|
+
} catch (error) {
|
|
4521
|
+
err(error instanceof Error ? error.message : String(error));
|
|
4522
|
+
return 1;
|
|
4523
|
+
}
|
|
3852
4524
|
if (records.length === 0) {
|
|
3853
4525
|
out("No installs recorded.");
|
|
3854
4526
|
return 0;
|
|
@@ -3883,7 +4555,9 @@ ${USAGE}`);
|
|
|
3883
4555
|
if (!required.ok)
|
|
3884
4556
|
return usageError(required.problems, err);
|
|
3885
4557
|
const { input } = required;
|
|
3886
|
-
const
|
|
4558
|
+
const probe = HOST_VERSION_PROBES[input.host];
|
|
4559
|
+
const hostVersion = probe?.(err);
|
|
4560
|
+
const plan = planForHost(input.host, hostVersion === undefined ? input : { ...input, hostVersion });
|
|
3887
4561
|
if (plan.errors.length > 0) {
|
|
3888
4562
|
for (const error of plan.errors)
|
|
3889
4563
|
err(`error: ${error.message}`);
|
|
@@ -3925,6 +4599,7 @@ function requireInput(flags, cwd, home, configHome2, needsTiers) {
|
|
|
3925
4599
|
const scope = flags.values.get("scope");
|
|
3926
4600
|
const search = needsTiers ? flags.values.get("search") ?? "" : "optional";
|
|
3927
4601
|
const fetch = needsTiers ? flags.values.get("fetch") ?? "" : "optional";
|
|
4602
|
+
const path = flags.values.get("path");
|
|
3928
4603
|
if (host === undefined)
|
|
3929
4604
|
problems.push(`--host is required (one of: ${HOSTS.join(", ")})`);
|
|
3930
4605
|
else if (!HOSTS.includes(host))
|
|
@@ -3933,6 +4608,13 @@ function requireInput(flags, cwd, home, configHome2, needsTiers) {
|
|
|
3933
4608
|
problems.push(`--scope is required (one of: ${SCOPES.join(", ")})`);
|
|
3934
4609
|
else if (!SCOPES.includes(scope))
|
|
3935
4610
|
problems.push(`invalid --scope value "${scope}" (one of: ${SCOPES.join(", ")})`);
|
|
4611
|
+
if (path !== undefined) {
|
|
4612
|
+
if (host !== "pi") {
|
|
4613
|
+
problems.push(`--path is only supported for host pi (${host ?? "no host"} has a single integration path)`);
|
|
4614
|
+
} else if (!PATH_CHOICES.includes(path)) {
|
|
4615
|
+
problems.push(`invalid --path value "${path}" (one of: ${PATH_CHOICES.join(", ")})`);
|
|
4616
|
+
}
|
|
4617
|
+
}
|
|
3936
4618
|
if (needsTiers) {
|
|
3937
4619
|
if (!TIERS.includes(search)) {
|
|
3938
4620
|
problems.push(flags.values.has("search") ? `invalid --search value "${search}" (one of: ${TIERS.join(", ")})` : `--search is required (one of: ${TIERS.join(", ")})`);
|
|
@@ -3955,14 +4637,15 @@ function requireInput(flags, cwd, home, configHome2, needsTiers) {
|
|
|
3955
4637
|
packageRef: flags.values.get("package") ?? "deepseek-web-retrieval",
|
|
3956
4638
|
home,
|
|
3957
4639
|
cwd,
|
|
3958
|
-
configHome: configHome2
|
|
4640
|
+
configHome: configHome2,
|
|
4641
|
+
...path !== undefined ? { path } : {}
|
|
3959
4642
|
}
|
|
3960
4643
|
};
|
|
3961
4644
|
}
|
|
3962
|
-
var HOSTS, HOST_CHOICES, SCOPES, TIERS, USAGE, VALUE_FLAGS, BOOLEAN_FLAGS;
|
|
4645
|
+
var HOSTS, HOST_CHOICES, SCOPES, TIERS, USAGE, VALUE_FLAGS, BOOLEAN_FLAGS, PATH_CHOICES;
|
|
3963
4646
|
var init_cli = __esm(() => {
|
|
3964
|
-
init_hosts();
|
|
3965
4647
|
init_apply();
|
|
4648
|
+
init_hosts();
|
|
3966
4649
|
init_state();
|
|
3967
4650
|
init_uninstall();
|
|
3968
4651
|
HOSTS = Object.keys(HOST_PLANNERS);
|
|
@@ -3970,14 +4653,20 @@ var init_cli = __esm(() => {
|
|
|
3970
4653
|
SCOPES = ["project", "global"];
|
|
3971
4654
|
TIERS = ["optional", "preferred", "forced", "off"];
|
|
3972
4655
|
USAGE = `Usage:
|
|
3973
|
-
deepseek-web-retrieval init --host ${HOST_CHOICES} --scope <project|global> --search <optional|preferred|forced|off> --fetch <optional|preferred|forced|off> [--package <npm ref>] [--cwd <dir>] [--apply]
|
|
4656
|
+
deepseek-web-retrieval init --host ${HOST_CHOICES} --scope <project|global> --search <optional|preferred|forced|off> --fetch <optional|preferred|forced|off> [--path <extension|mcp>] [--package <npm ref>] [--cwd <dir>] [--apply]
|
|
3974
4657
|
deepseek-web-retrieval uninstall --host ${HOST_CHOICES} --scope <project|global> [--cwd <dir>]
|
|
3975
4658
|
deepseek-web-retrieval list [--cwd <dir>]
|
|
3976
4659
|
|
|
3977
4660
|
init prints the full plan (every file, old value → new value) and writes nothing
|
|
3978
|
-
until --apply is passed. uninstall restores what the plan applied
|
|
3979
|
-
|
|
4661
|
+
until --apply is passed. uninstall restores what the plan applied. --path picks
|
|
4662
|
+
the integration mechanism for hosts with more than one (Pi: extension by
|
|
4663
|
+
default, mcp for the stdio MCP server). --package overrides the package ref
|
|
4664
|
+
written into host config; the Pi extension path turns it into the packages
|
|
4665
|
+
source (a bare name is prefixed with npm:, a ref that already starts with npm:
|
|
4666
|
+
is written as-is).`;
|
|
4667
|
+
VALUE_FLAGS = new Set(["host", "scope", "search", "fetch", "path", "package", "cwd"]);
|
|
3980
4668
|
BOOLEAN_FLAGS = new Set(["apply"]);
|
|
4669
|
+
PATH_CHOICES = ["extension", "mcp"];
|
|
3981
4670
|
});
|
|
3982
4671
|
|
|
3983
4672
|
// src/mcp/cli.ts
|
|
@@ -3990,17 +4679,7 @@ function apiKeyFromEnv(env = process.env) {
|
|
|
3990
4679
|
return key ? key : undefined;
|
|
3991
4680
|
}
|
|
3992
4681
|
|
|
3993
|
-
// src/
|
|
3994
|
-
init_constants();
|
|
3995
|
-
var MISSING_API_KEY_MESSAGE = `DeepSeek API key not found. Set ${API_KEY_ENV} in the MCP server's env configuration.`;
|
|
3996
|
-
function requireApiKey(env) {
|
|
3997
|
-
const apiKey = apiKeyFromEnv(env);
|
|
3998
|
-
if (!apiKey)
|
|
3999
|
-
throw new Error(MISSING_API_KEY_MESSAGE);
|
|
4000
|
-
return apiKey;
|
|
4001
|
-
}
|
|
4002
|
-
|
|
4003
|
-
// src/mcp/config.ts
|
|
4682
|
+
// src/core/env.ts
|
|
4004
4683
|
init_constants();
|
|
4005
4684
|
|
|
4006
4685
|
// src/core/options.ts
|
|
@@ -4128,7 +4807,7 @@ function resolveSearchOptions(input) {
|
|
|
4128
4807
|
maxResults: clampInt(input?.maxResults, DEFAULT_MAX_RESULTS, 0, 50),
|
|
4129
4808
|
minRerank: finiteNumber(input?.minRerank, DEFAULT_MIN_RERANK),
|
|
4130
4809
|
minAuthority: finiteNumber(input?.minAuthority, DEFAULT_MIN_AUTHORITY),
|
|
4131
|
-
allowUnknownAuthority: input?.allowUnknownAuthority
|
|
4810
|
+
allowUnknownAuthority: input?.allowUnknownAuthority !== false,
|
|
4132
4811
|
blockedDomains: normalizeDomains(input?.blockedDomains),
|
|
4133
4812
|
includeDomains: normalizeDomains(input?.includeDomains),
|
|
4134
4813
|
language: optionalText(input?.language),
|
|
@@ -4173,7 +4852,7 @@ function clampInt(value, fallback, min, max) {
|
|
|
4173
4852
|
return Math.min(max, Math.max(min, number));
|
|
4174
4853
|
}
|
|
4175
4854
|
|
|
4176
|
-
// src/
|
|
4855
|
+
// src/core/env.ts
|
|
4177
4856
|
var NUMBER_ENV = {
|
|
4178
4857
|
count: "DEEPSEEK_SEARCH_COUNT",
|
|
4179
4858
|
maxResults: "DEEPSEEK_SEARCH_MAX_RESULTS",
|
|
@@ -4195,10 +4874,8 @@ var DOMAINS_ENV = {
|
|
|
4195
4874
|
fetchAllowedDomains: "DEEPSEEK_FETCH_ALLOWED_DOMAINS",
|
|
4196
4875
|
fetchBlockedDomains: "DEEPSEEK_FETCH_BLOCKED_DOMAINS"
|
|
4197
4876
|
};
|
|
4198
|
-
var FLAG_ENV = {
|
|
4199
|
-
allowUnknownAuthority: "DEEPSEEK_SEARCH_ALLOW_UNKNOWN_AUTHORITY"
|
|
4200
|
-
};
|
|
4201
4877
|
var FALSE_ONLY_ENV = {
|
|
4878
|
+
allowUnknownAuthority: "DEEPSEEK_SEARCH_ALLOW_UNKNOWN_AUTHORITY",
|
|
4202
4879
|
fetchBlockPrivateHosts: "DEEPSEEK_FETCH_BLOCK_PRIVATE_HOSTS",
|
|
4203
4880
|
searchEnabled: ENABLE_SEARCH_ENV,
|
|
4204
4881
|
fetchEnabled: ENABLE_FETCH_ENV
|
|
@@ -4212,7 +4889,7 @@ function pluginOptionsFromEnv(env) {
|
|
|
4212
4889
|
putNumber(options, "maxResults", env);
|
|
4213
4890
|
putNumber(options, "minRerank", env);
|
|
4214
4891
|
putNumber(options, "minAuthority", env);
|
|
4215
|
-
|
|
4892
|
+
putFalseOnly(options, "allowUnknownAuthority", env);
|
|
4216
4893
|
putDomains(options, "blockedDomains", env);
|
|
4217
4894
|
putDomains(options, "includeDomains", env);
|
|
4218
4895
|
putText(options, "language", env);
|
|
@@ -4248,10 +4925,6 @@ function putText(target, key, env) {
|
|
|
4248
4925
|
if (trimmed)
|
|
4249
4926
|
target[key] = trimmed;
|
|
4250
4927
|
}
|
|
4251
|
-
function putBoolean(target, key, env) {
|
|
4252
|
-
if (isTrueValue(env[FLAG_ENV[key]]))
|
|
4253
|
-
target[key] = true;
|
|
4254
|
-
}
|
|
4255
4928
|
function putFalseOnly(target, key, env) {
|
|
4256
4929
|
if (isFalseValue(env[FALSE_ONLY_ENV[key]]))
|
|
4257
4930
|
target[key] = false;
|
|
@@ -4264,18 +4937,145 @@ function putDomains(target, key, env) {
|
|
|
4264
4937
|
if (domains.length > 0)
|
|
4265
4938
|
target[key] = domains;
|
|
4266
4939
|
}
|
|
4267
|
-
function isTrueValue(value) {
|
|
4268
|
-
const trimmed = value?.trim().toLowerCase();
|
|
4269
|
-
return trimmed === "true" || trimmed === "1" || trimmed === "yes" || trimmed === "on";
|
|
4270
|
-
}
|
|
4271
4940
|
function isFalseValue(value) {
|
|
4272
4941
|
const trimmed = value?.trim().toLowerCase();
|
|
4273
4942
|
return trimmed === "false" || trimmed === "0" || trimmed === "no" || trimmed === "off";
|
|
4274
4943
|
}
|
|
4275
4944
|
|
|
4945
|
+
// src/mcp/auth.ts
|
|
4946
|
+
init_constants();
|
|
4947
|
+
var MISSING_API_KEY_MESSAGE = `DeepSeek API key not found. Set ${API_KEY_ENV} in the MCP server's env configuration.`;
|
|
4948
|
+
function requireApiKey(env) {
|
|
4949
|
+
const apiKey = apiKeyFromEnv(env);
|
|
4950
|
+
if (!apiKey)
|
|
4951
|
+
throw new Error(MISSING_API_KEY_MESSAGE);
|
|
4952
|
+
return apiKey;
|
|
4953
|
+
}
|
|
4954
|
+
|
|
4955
|
+
// src/mcp/client.ts
|
|
4956
|
+
import { CLIENT_INFO_META_KEY } from "@modelcontextprotocol/server";
|
|
4957
|
+
var JSON_REPLACING_CLIENT_NAMES = new Set(["codex-mcp-client"]);
|
|
4958
|
+
function clientNameOf(server, ctx) {
|
|
4959
|
+
const envelope = ctx.mcpReq.envelope;
|
|
4960
|
+
const name = envelope?.[CLIENT_INFO_META_KEY];
|
|
4961
|
+
if (typeof name === "object" && name !== null && "name" in name && typeof name.name === "string") {
|
|
4962
|
+
return name.name;
|
|
4963
|
+
}
|
|
4964
|
+
return server.server.getClientVersion()?.name;
|
|
4965
|
+
}
|
|
4966
|
+
function isJsonReplacingClient(name) {
|
|
4967
|
+
return name !== undefined && JSON_REPLACING_CLIENT_NAMES.has(name);
|
|
4968
|
+
}
|
|
4969
|
+
function omitStructuredContent(server, ctx) {
|
|
4970
|
+
return isJsonReplacingClient(clientNameOf(server, ctx));
|
|
4971
|
+
}
|
|
4972
|
+
|
|
4276
4973
|
// src/mcp/server.ts
|
|
4277
4974
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
4278
4975
|
import * as z from "zod/v4";
|
|
4976
|
+
// package.json
|
|
4977
|
+
var package_default = {
|
|
4978
|
+
$schema: "https://json.schemastore.org/package.json",
|
|
4979
|
+
name: "deepseek-web-retrieval",
|
|
4980
|
+
version: "0.5.0",
|
|
4981
|
+
description: "Token-efficient DeepSeek Search and Fetch for coding agents, starting with OpenCode v2.",
|
|
4982
|
+
type: "module",
|
|
4983
|
+
license: "MIT",
|
|
4984
|
+
repository: {
|
|
4985
|
+
type: "git",
|
|
4986
|
+
url: "git+https://github.com/gwokhou/deepseek-web-retrieval.git"
|
|
4987
|
+
},
|
|
4988
|
+
homepage: "https://github.com/gwokhou/deepseek-web-retrieval#readme",
|
|
4989
|
+
bugs: "https://github.com/gwokhou/deepseek-web-retrieval/issues",
|
|
4990
|
+
keywords: [
|
|
4991
|
+
"opencode",
|
|
4992
|
+
"opencode-plugin",
|
|
4993
|
+
"pi",
|
|
4994
|
+
"pi-extension",
|
|
4995
|
+
"deepseek",
|
|
4996
|
+
"websearch",
|
|
4997
|
+
"webfetch",
|
|
4998
|
+
"coding-agent",
|
|
4999
|
+
"mcp"
|
|
5000
|
+
],
|
|
5001
|
+
exports: {
|
|
5002
|
+
".": "./index.ts",
|
|
5003
|
+
"./core": "./src/core/index.ts",
|
|
5004
|
+
"./mcp": "./src/mcp/server.ts",
|
|
5005
|
+
"./pi": "./src/pi/extension.ts"
|
|
5006
|
+
},
|
|
5007
|
+
pi: {
|
|
5008
|
+
extensions: [
|
|
5009
|
+
"./src/pi/extension.ts"
|
|
5010
|
+
]
|
|
5011
|
+
},
|
|
5012
|
+
bin: {
|
|
5013
|
+
"deepseek-web-retrieval": "dist/cli.js"
|
|
5014
|
+
},
|
|
5015
|
+
files: [
|
|
5016
|
+
"index.ts",
|
|
5017
|
+
"src",
|
|
5018
|
+
"dist",
|
|
5019
|
+
"docs",
|
|
5020
|
+
"README.md",
|
|
5021
|
+
"LICENSE",
|
|
5022
|
+
"CHANGELOG.md",
|
|
5023
|
+
"examples"
|
|
5024
|
+
],
|
|
5025
|
+
scripts: {
|
|
5026
|
+
lint: "biome check --error-on-warnings .",
|
|
5027
|
+
"lint:ci": "biome ci --error-on-warnings .",
|
|
5028
|
+
"lint:fix": "biome check --write --error-on-warnings .",
|
|
5029
|
+
format: "biome format --write .",
|
|
5030
|
+
typecheck: "tsc --noEmit",
|
|
5031
|
+
build: "bun build src/mcp/cli.ts --target=node --format=esm --outdir=dist --external @modelcontextprotocol/server --external @modelcontextprotocol/server/stdio --external zod --external zod/v4",
|
|
5032
|
+
prepublishOnly: "bun run check:release",
|
|
5033
|
+
test: "bun test",
|
|
5034
|
+
"test:package": "node scripts/test-package.mjs",
|
|
5035
|
+
"test:e2e:keyless": "bun run test:e2e pi pi-extension installer reinit upgrade pkg capability-off",
|
|
5036
|
+
"check:release": "bun run check && bun run test:package && bun run test:e2e:keyless",
|
|
5037
|
+
"test:e2e": "bun run test/e2e/run.ts",
|
|
5038
|
+
"docs:matrix": "bun run scripts/render-matrix.ts",
|
|
5039
|
+
"test:coverage": "bun test --coverage",
|
|
5040
|
+
check: "bun run lint && bun run typecheck && bun run docs:matrix --check && bun run build && bun test"
|
|
5041
|
+
},
|
|
5042
|
+
peerDependencies: {
|
|
5043
|
+
"@earendil-works/pi-ai": "^1.3.0",
|
|
5044
|
+
"@earendil-works/pi-coding-agent": "^1.3.0",
|
|
5045
|
+
"@opencode/plugin": "^2.0.32"
|
|
5046
|
+
},
|
|
5047
|
+
peerDependenciesMeta: {
|
|
5048
|
+
"@earendil-works/pi-ai": {
|
|
5049
|
+
optional: true
|
|
5050
|
+
},
|
|
5051
|
+
"@earendil-works/pi-coding-agent": {
|
|
5052
|
+
optional: true
|
|
5053
|
+
},
|
|
5054
|
+
"@opencode/plugin": {
|
|
5055
|
+
optional: true
|
|
5056
|
+
}
|
|
5057
|
+
},
|
|
5058
|
+
devDependencies: {
|
|
5059
|
+
"@biomejs/biome": "2.5.15",
|
|
5060
|
+
"@earendil-works/pi-ai": "1.3.0",
|
|
5061
|
+
"@earendil-works/pi-coding-agent": "1.3.0",
|
|
5062
|
+
"@modelcontextprotocol/client": "^2.3.1",
|
|
5063
|
+
"@opencode/plugin": "2.0.32",
|
|
5064
|
+
"@types/bun": "1.4.0",
|
|
5065
|
+
"@types/node": "^26.6.5",
|
|
5066
|
+
typescript: "5.9.3"
|
|
5067
|
+
},
|
|
5068
|
+
engines: {
|
|
5069
|
+
bun: ">=1.3.0",
|
|
5070
|
+
node: ">=20"
|
|
5071
|
+
},
|
|
5072
|
+
dependencies: {
|
|
5073
|
+
"@modelcontextprotocol/server": "^2.3.1",
|
|
5074
|
+
"jsonc-parser": "^3.3.1",
|
|
5075
|
+
"smol-toml": "^1.9.1",
|
|
5076
|
+
zod: "^4"
|
|
5077
|
+
}
|
|
5078
|
+
};
|
|
4279
5079
|
|
|
4280
5080
|
// src/core/client.ts
|
|
4281
5081
|
init_constants();
|
|
@@ -4357,6 +5157,14 @@ function formatSearchResults(results) {
|
|
|
4357
5157
|
|
|
4358
5158
|
`);
|
|
4359
5159
|
}
|
|
5160
|
+
function toStructuredResult(result) {
|
|
5161
|
+
return {
|
|
5162
|
+
url: result.url,
|
|
5163
|
+
...result.title ? { title: result.title } : {},
|
|
5164
|
+
...result.content ? { content: result.content } : {},
|
|
5165
|
+
...result.published !== undefined ? { published: result.published } : {}
|
|
5166
|
+
};
|
|
5167
|
+
}
|
|
4360
5168
|
function escapeBrackets(value) {
|
|
4361
5169
|
return value.replaceAll("[", "\\[").replaceAll("]", "\\]");
|
|
4362
5170
|
}
|
|
@@ -4439,8 +5247,12 @@ function processResults(raw, options) {
|
|
|
4439
5247
|
if (!url || seen.has(url))
|
|
4440
5248
|
continue;
|
|
4441
5249
|
const rerank = item.rerank_score;
|
|
4442
|
-
if (typeof rerank
|
|
5250
|
+
if (typeof rerank === "number" && Number.isFinite(rerank)) {
|
|
5251
|
+
if (rerank < options.minRerank)
|
|
5252
|
+
continue;
|
|
5253
|
+
} else if (options.minRerank > 0) {
|
|
4443
5254
|
continue;
|
|
5255
|
+
}
|
|
4444
5256
|
const authority = item.authority;
|
|
4445
5257
|
const knownAuthority = typeof authority === "number" && Number.isFinite(authority) && authority >= 0;
|
|
4446
5258
|
if (knownAuthority) {
|
|
@@ -4682,22 +5494,26 @@ async function searchWeb(query, options, request) {
|
|
|
4682
5494
|
query: builtQuery,
|
|
4683
5495
|
count: options.count,
|
|
4684
5496
|
need_content: false
|
|
4685
|
-
}, "DeepSeek Search", {
|
|
5497
|
+
}, "DeepSeek Search", { ...request, timeoutMs: timeout });
|
|
4686
5498
|
if (!Array.isArray(payload.results)) {
|
|
4687
5499
|
throw new Error("DeepSeek Search returned a response without a results array");
|
|
4688
5500
|
}
|
|
4689
5501
|
return processResults(payload.results, options);
|
|
4690
5502
|
}
|
|
4691
5503
|
async function fetchPage(url, options, request) {
|
|
4692
|
-
|
|
5504
|
+
const target = parseFetchUrl(url.href);
|
|
5505
|
+
assertFetchHostPolicy(target, options);
|
|
4693
5506
|
const timeout = effectiveTimeout(options.timeoutMs, request.timeoutMs);
|
|
4694
|
-
const payload = await postJson(FETCH_ENDPOINT, { url:
|
|
5507
|
+
const payload = await postJson(FETCH_ENDPOINT, { url: target.href }, "DeepSeek Fetch", {
|
|
5508
|
+
...request,
|
|
5509
|
+
timeoutMs: timeout
|
|
5510
|
+
});
|
|
4695
5511
|
const title = typeof payload.title === "string" ? payload.title.trim() : "";
|
|
4696
5512
|
const content = typeof payload.content === "string" ? payload.content : "";
|
|
4697
5513
|
if (!content.trim()) {
|
|
4698
5514
|
throw new Error("DeepSeek Fetch returned no page content (the page may be empty or unavailable)");
|
|
4699
5515
|
}
|
|
4700
|
-
return { url:
|
|
5516
|
+
return { url: target.href, title, content };
|
|
4701
5517
|
}
|
|
4702
5518
|
async function fetchDocument(url, options, request) {
|
|
4703
5519
|
if (typeof url !== "string" && !(url instanceof URL))
|
|
@@ -4711,6 +5527,15 @@ async function fetchDocument(url, options, request) {
|
|
|
4711
5527
|
truncated: formatted.truncated
|
|
4712
5528
|
};
|
|
4713
5529
|
}
|
|
5530
|
+
function createRetrievalClient(dependencies = {}) {
|
|
5531
|
+
const bind = (request) => ({ ...request, ...dependencies });
|
|
5532
|
+
return {
|
|
5533
|
+
searchWeb: (query, options, request) => searchWeb(query, options, bind(request)),
|
|
5534
|
+
fetchPage: (url, options, request) => fetchPage(url, options, bind(request)),
|
|
5535
|
+
fetchDocument: (url, options, request) => fetchDocument(url, options, bind(request))
|
|
5536
|
+
};
|
|
5537
|
+
}
|
|
5538
|
+
var retrievalClient = createRetrievalClient();
|
|
4714
5539
|
|
|
4715
5540
|
// src/mcp/server.ts
|
|
4716
5541
|
init_constants();
|
|
@@ -4721,120 +5546,34 @@ var FETCH_OUTPUT_TRUNCATED = "Output is truncated to the configured size.";
|
|
|
4721
5546
|
var SEARCH_RANKED_FILTERED = "Results are ranked by relevance and source authority, deduplicated by URL, and capped at the configured maximum.";
|
|
4722
5547
|
var SEARCH_EMPTY_IS_NORMAL = "An empty result list is normal, not an error.";
|
|
4723
5548
|
var SEARCH_QUERY_OPERATORS = "Use site:/-site: operators in the query to include or exclude domains; the full query must stay within 128 characters.";
|
|
4724
|
-
|
|
4725
|
-
var
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
version: "0.3.0",
|
|
4729
|
-
description: "Token-efficient DeepSeek Search and Fetch for coding agents, starting with OpenCode v2.",
|
|
4730
|
-
type: "module",
|
|
4731
|
-
license: "MIT",
|
|
4732
|
-
repository: {
|
|
4733
|
-
type: "git",
|
|
4734
|
-
url: "git+https://github.com/gwokhou/deepseek-web-retrieval.git"
|
|
4735
|
-
},
|
|
4736
|
-
homepage: "https://github.com/gwokhou/deepseek-web-retrieval#readme",
|
|
4737
|
-
bugs: "https://github.com/gwokhou/deepseek-web-retrieval/issues",
|
|
4738
|
-
keywords: [
|
|
4739
|
-
"opencode",
|
|
4740
|
-
"opencode-plugin",
|
|
4741
|
-
"deepseek",
|
|
4742
|
-
"websearch",
|
|
4743
|
-
"webfetch",
|
|
4744
|
-
"coding-agent",
|
|
4745
|
-
"mcp"
|
|
4746
|
-
],
|
|
4747
|
-
exports: {
|
|
4748
|
-
".": "./index.ts",
|
|
4749
|
-
"./core": "./src/core/index.ts",
|
|
4750
|
-
"./mcp": "./src/mcp/server.ts"
|
|
4751
|
-
},
|
|
4752
|
-
bin: {
|
|
4753
|
-
"deepseek-web-retrieval": "dist/cli.js"
|
|
4754
|
-
},
|
|
4755
|
-
files: [
|
|
4756
|
-
"index.ts",
|
|
4757
|
-
"src",
|
|
4758
|
-
"dist",
|
|
4759
|
-
"docs",
|
|
4760
|
-
"README.md",
|
|
4761
|
-
"LICENSE",
|
|
4762
|
-
"CHANGELOG.md",
|
|
4763
|
-
"examples"
|
|
4764
|
-
],
|
|
4765
|
-
scripts: {
|
|
4766
|
-
typecheck: "tsc --noEmit",
|
|
4767
|
-
build: "bun build src/mcp/cli.ts --target=node --format=esm --outdir=dist --external @modelcontextprotocol/server --external @modelcontextprotocol/server/stdio --external zod --external zod/v4",
|
|
4768
|
-
prepublishOnly: "bun run typecheck && bun run build",
|
|
4769
|
-
test: "bun test",
|
|
4770
|
-
"test:e2e": "bun run test/e2e/run.ts",
|
|
4771
|
-
"docs:matrix": "bun run scripts/render-matrix.ts",
|
|
4772
|
-
"test:coverage": "bun test --coverage",
|
|
4773
|
-
check: "bun run typecheck && bun run docs:matrix --check && bun run build && bun test"
|
|
4774
|
-
},
|
|
4775
|
-
peerDependencies: {
|
|
4776
|
-
"@opencode/plugin": "^2.0.32"
|
|
4777
|
-
},
|
|
4778
|
-
devDependencies: {
|
|
4779
|
-
"@modelcontextprotocol/client": "^2.3.1",
|
|
4780
|
-
"@opencode/plugin": "2.0.32",
|
|
4781
|
-
"@types/bun": "1.4.0",
|
|
4782
|
-
"@types/node": "^26.6.5",
|
|
4783
|
-
typescript: "5.9.3"
|
|
4784
|
-
},
|
|
4785
|
-
engines: {
|
|
4786
|
-
bun: ">=1.3.0",
|
|
4787
|
-
node: ">=20"
|
|
4788
|
-
},
|
|
4789
|
-
dependencies: {
|
|
4790
|
-
"@modelcontextprotocol/server": "^2.3.1",
|
|
4791
|
-
"jsonc-parser": "^3.3.1",
|
|
4792
|
-
"smol-toml": "^1.9.1",
|
|
4793
|
-
zod: "^4"
|
|
4794
|
-
}
|
|
4795
|
-
};
|
|
5549
|
+
var SEARCH_TOOL_DESCRIPTION = "Search the web with DeepSeek's in-house search engine and return a short, filtered result " + "list (title, URL, summary). " + `${SEARCH_RANKED_FILTERED} ` + `${SEARCH_EMPTY_IS_NORMAL} ` + `${SEARCH_QUERY_OPERATORS} ` + "To read a full page, pass a result URL to the deepseek_fetch tool.";
|
|
5550
|
+
var FETCH_TOOL_DESCRIPTION = "Fetch one web page as clean Markdown through DeepSeek's Fetch API. " + `${FETCH_SERVICE_LIMITS} ` + `${FETCH_OUTPUT_TRUNCATED}`;
|
|
5551
|
+
var SEARCH_QUERY_PARAMETER = "Search query, 128 characters or fewer including operators (site:, -site:, quoted phrases, lang:).";
|
|
5552
|
+
var FETCH_URL_PARAMETER = "Absolute http:// or https:// URL to fetch. Credentials are rejected.";
|
|
4796
5553
|
|
|
4797
5554
|
// src/mcp/server.ts
|
|
4798
5555
|
var MCP_SERVER_NAME = "deepseek-web-retrieval";
|
|
4799
5556
|
var SEARCH_INPUT = z.object({
|
|
4800
|
-
query: z.string().min(1).describe(
|
|
4801
|
-
});
|
|
4802
|
-
var SEARCH_OUTPUT = z.object({
|
|
4803
|
-
results: z.array(z.object({
|
|
4804
|
-
url: z.string(),
|
|
4805
|
-
title: z.string().optional(),
|
|
4806
|
-
content: z.string().optional(),
|
|
4807
|
-
published: z.number().optional()
|
|
4808
|
-
}))
|
|
5557
|
+
query: z.string().min(1).describe(SEARCH_QUERY_PARAMETER)
|
|
4809
5558
|
});
|
|
4810
5559
|
var FETCH_INPUT = z.object({
|
|
4811
|
-
url: z.string().min(1).describe(
|
|
4812
|
-
});
|
|
4813
|
-
var FETCH_OUTPUT = z.object({
|
|
4814
|
-
url: z.string(),
|
|
4815
|
-
title: z.string(),
|
|
4816
|
-
content: z.string(),
|
|
4817
|
-
truncated: z.boolean()
|
|
5560
|
+
url: z.string().min(1).describe(FETCH_URL_PARAMETER)
|
|
4818
5561
|
});
|
|
4819
|
-
|
|
4820
|
-
var FETCH_DESCRIPTION = "Fetch one web page as clean Markdown through DeepSeek's Fetch API. " + `${FETCH_SERVICE_LIMITS} ` + `${FETCH_OUTPUT_TRUNCATED}`;
|
|
4821
|
-
function createDeepSeekServer(config, env = process.env) {
|
|
5562
|
+
function createDeepSeekServer(config, env = process.env, client = retrievalClient) {
|
|
4822
5563
|
const server = new McpServer({ name: MCP_SERVER_NAME, version: package_default.version });
|
|
4823
5564
|
if (config.search.enabled) {
|
|
4824
5565
|
server.registerTool("deepseek_web_search", {
|
|
4825
5566
|
title: "DeepSeek Web Search",
|
|
4826
|
-
description:
|
|
5567
|
+
description: SEARCH_TOOL_DESCRIPTION,
|
|
4827
5568
|
inputSchema: SEARCH_INPUT,
|
|
4828
|
-
outputSchema: SEARCH_OUTPUT,
|
|
4829
5569
|
annotations: { readOnlyHint: true }
|
|
4830
5570
|
}, async ({ query }, ctx) => {
|
|
4831
5571
|
try {
|
|
4832
5572
|
const request = requestContext(config, env, ctx.mcpReq.signal);
|
|
4833
|
-
const results = await searchWeb(query, config.search, request);
|
|
4834
|
-
return {
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
};
|
|
5573
|
+
const results = await client.searchWeb(query, config.search, request);
|
|
5574
|
+
return callResult(server, ctx, formatSearchResults(results), {
|
|
5575
|
+
results: results.map(toStructuredResult)
|
|
5576
|
+
});
|
|
4838
5577
|
} catch (error) {
|
|
4839
5578
|
return toErrorResult(error);
|
|
4840
5579
|
}
|
|
@@ -4843,23 +5582,19 @@ function createDeepSeekServer(config, env = process.env) {
|
|
|
4843
5582
|
if (config.fetch.enabled) {
|
|
4844
5583
|
server.registerTool("deepseek_fetch", {
|
|
4845
5584
|
title: "DeepSeek Fetch",
|
|
4846
|
-
description:
|
|
5585
|
+
description: FETCH_TOOL_DESCRIPTION,
|
|
4847
5586
|
inputSchema: FETCH_INPUT,
|
|
4848
|
-
outputSchema: FETCH_OUTPUT,
|
|
4849
5587
|
annotations: { readOnlyHint: true }
|
|
4850
5588
|
}, async ({ url }, ctx) => {
|
|
4851
5589
|
try {
|
|
4852
5590
|
const request = requestContext(config, env, ctx.mcpReq.signal);
|
|
4853
|
-
const page = await fetchDocument(url, config.fetch, request);
|
|
4854
|
-
return {
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
truncated: page.truncated
|
|
4861
|
-
}
|
|
4862
|
-
};
|
|
5591
|
+
const page = await client.fetchDocument(url, config.fetch, request);
|
|
5592
|
+
return callResult(server, ctx, page.content, {
|
|
5593
|
+
url: page.url,
|
|
5594
|
+
title: page.title,
|
|
5595
|
+
content: page.content,
|
|
5596
|
+
truncated: page.truncated
|
|
5597
|
+
});
|
|
4863
5598
|
} catch (error) {
|
|
4864
5599
|
return toErrorResult(error);
|
|
4865
5600
|
}
|
|
@@ -4870,13 +5605,10 @@ function createDeepSeekServer(config, env = process.env) {
|
|
|
4870
5605
|
function requestContext(config, env, signal) {
|
|
4871
5606
|
return { signal, auth: { apiKey: requireApiKey(env) }, retry: config.retry };
|
|
4872
5607
|
}
|
|
4873
|
-
function
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
...result.content ? { content: result.content } : {},
|
|
4878
|
-
...result.published !== undefined ? { published: result.published } : {}
|
|
4879
|
-
};
|
|
5608
|
+
function callResult(server, ctx, text, structured) {
|
|
5609
|
+
if (omitStructuredContent(server, ctx))
|
|
5610
|
+
return { content: [{ type: "text", text }] };
|
|
5611
|
+
return { content: [{ type: "text", text }], structuredContent: structured };
|
|
4880
5612
|
}
|
|
4881
5613
|
function servePrecheck(config) {
|
|
4882
5614
|
if (config.search.enabled || config.fetch.enabled)
|
|
@@ -4905,7 +5637,16 @@ if (precheck) {
|
|
|
4905
5637
|
console.error(precheck);
|
|
4906
5638
|
process.exit(1);
|
|
4907
5639
|
}
|
|
4908
|
-
var handle = serveStdio(() =>
|
|
5640
|
+
var handle = serveStdio(() => {
|
|
5641
|
+
const server = createDeepSeekServer(config, env);
|
|
5642
|
+
server.server.oninitialized = () => {
|
|
5643
|
+
const info = server.server.getClientVersion();
|
|
5644
|
+
if (info === undefined)
|
|
5645
|
+
return;
|
|
5646
|
+
console.error(`mcp client: ${info.name} ${info.version ?? "unknown"} — ` + (isJsonReplacingClient(info.name) ? "text-only results (this client replaces content with serialized structuredContent)" : "text + structuredContent results"));
|
|
5647
|
+
};
|
|
5648
|
+
return server;
|
|
5649
|
+
});
|
|
4909
5650
|
var tools = [
|
|
4910
5651
|
config.search.enabled ? `search (max ${config.search.maxResults} results)` : null,
|
|
4911
5652
|
config.fetch.enabled ? `fetch (max ${config.fetch.maxChars} chars)` : null
|