dsh-toolfold 0.1.7 → 0.1.9

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/lib/index.js CHANGED
@@ -1,137 +1,274 @@
1
+ import { createRequire } from "node:module";
2
+ import z from "schemastery";
3
+ import { readFileSync } from "node:fs";
4
+ import { resolve } from "node:path";
5
+ //#region src/host/index.js
1
6
  /**
2
- * dsh-toolfold — HOST half.
3
- *
4
- * Registers the `toolfold` settings namespace in the DSH settings service
5
- * (persisted in `~/.dsh/settings.yaml`, the same document every product and
6
- * family plugin uses) and serves the browser half through one same-origin
7
- * JSON route:
8
- *
9
- * GET /api/dsh-toolfold/settings → { ok, value: { value, revision, writable } }
10
- * POST /api/dsh-toolfold/settings → body { op: 'set'|'unset', field, value?,
11
- * expectedRevision? }
12
- * → { ok, value: { value, revision, writable } }
13
- *
14
- * The browser half prefers the official `settingsScope` transport when the
15
- * deployment exposes this namespace, then this route, then browser
16
- * localStorage as a degraded fallback. `value` is always the fully resolved
17
- * section (schema defaults + composition base + the user's settings.yaml
18
- * overrides), so the client never re-implements the resolution.
19
- *
20
- * The same package also declares `dsh.bundle.patch` (see cordis.patch.yml),
21
- * which makes `dsh plugin --profile <name> add <this package>` install AND
22
- * mount both halves in one command.
23
- */
24
- import { settingsNamespace } from '@deepseek-ai/dsh-settings'
25
- import z from 'schemastery'
26
-
27
- export const name = 'toolfold'
28
- export const inject = ['settings', 'webServer']
29
-
30
- const NS = settingsNamespace('toolfold')
31
- const API_PATH = '/api/dsh-toolfold/settings'
32
- const FIELDS = ['durMs', 'keepThink', 'splitThink', 'stats']
33
-
7
+ * dsh-toolfold — HOST half.
8
+ *
9
+ * Registers the `toolfold` settings namespace in the DSH settings service
10
+ * (persisted in `~/.dsh/settings.yaml`, the same document every product and
11
+ * family plugin uses) and serves the browser half through one same-origin
12
+ * JSON route:
13
+ *
14
+ * GET /api/dsh-toolfold/settings → { ok, value: { value, revision, writable } }
15
+ * POST /api/dsh-toolfold/settings → body { op: 'set'|'unset', field, value?,
16
+ * expectedRevision? }
17
+ * → { ok, value: { value, revision, writable } }
18
+ *
19
+ * Every success response additionally carries `dsh: { version, state }` — the
20
+ * running DSH product version and its compatibility with the supported range
21
+ * ('ok' | 'old' | 'new' | 'unknown'). DSH does not enforce `engines.dsh`
22
+ * anywhere, so the host half reports the mismatch itself and the settings
23
+ * card warns the user (once-only console warning + hover-tip icon).
24
+ *
25
+ * The browser half prefers the official `settingsScope` transport when the
26
+ * deployment exposes this namespace, then this route, then browser
27
+ * localStorage as a degraded fallback. `value` is always the fully resolved
28
+ * section (schema defaults + composition base + the user's settings.yaml
29
+ * overrides), so the client never re-implements the resolution.
30
+ *
31
+ * The same package also declares `dsh.bundle.patch` (see cordis.patch.yml),
32
+ * which makes `dsh plugin --profile <name> add <this package>` install AND
33
+ * mount both halves in one command.
34
+ */
35
+ const name = "toolfold";
36
+ const inject = ["settings", "webServer"];
37
+ const NS = "toolfold";
38
+ const API_PATH = "/api/dsh-toolfold/settings";
39
+ const FIELDS = [
40
+ "enabled",
41
+ "durMs",
42
+ "thinkMode",
43
+ "keepThink",
44
+ "thinkAuto",
45
+ "splitThink",
46
+ "stats"
47
+ ];
34
48
  const SCHEMA = z.object({
35
- durMs: z.number().step(10).min(0).max(2000).default(240),
36
- keepThink: z.boolean().default(false),
37
- splitThink: z.boolean().default(true),
38
- stats: z.boolean().default(false),
39
- })
40
-
49
+ enabled: z.boolean().default(true),
50
+ durMs: z.number().step(10).min(0).max(2e3).default(240),
51
+ thinkMode: z.union([
52
+ z.const("auto"),
53
+ z.const("keep"),
54
+ z.const("hide")
55
+ ]),
56
+ keepThink: z.boolean().default(false),
57
+ thinkAuto: z.boolean().default(true),
58
+ splitThink: z.boolean().default(true),
59
+ stats: z.boolean().default(false)
60
+ });
61
+ /**
62
+ * Resolve the canonical think mode for one section. An explicitly set,
63
+ * valid thinkMode wins; otherwise derive it from the deprecated keys so
64
+ * pre-merge preferences survive the upgrade; default 'auto'. Total —
65
+ * never throws. (Client twin: resolveThinkMode in src/client/settings.js.)
66
+ */
67
+ function resolveThinkMode(section) {
68
+ var mode = section !== void 0 && section !== null ? section.thinkMode : void 0;
69
+ if (mode === "keep" || mode === "hide" || mode === "auto") return mode;
70
+ if (section !== void 0 && section !== null && section.thinkAuto === false) return section.keepThink === true ? "keep" : "hide";
71
+ return "auto";
72
+ }
73
+ const DSH_MIN = "0.1.2-rc.1";
74
+ const DSH_MAX = "0.1.3";
75
+ /** Parse "v1.2.3-rc.4+build" into comparable parts; null on junk. */
76
+ function parseVersion(value) {
77
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(String(value));
78
+ if (!match) return null;
79
+ return {
80
+ core: [
81
+ Number(match[1]),
82
+ Number(match[2]),
83
+ Number(match[3])
84
+ ],
85
+ pre: match[4] === void 0 ? null : match[4].split(".")
86
+ };
87
+ }
88
+ /** Compare two parsed versions (semver precedence, prerelease-aware): -1/0/1. */
89
+ function compareParsed(a, b) {
90
+ for (let i = 0; i < 3; i++) if (a.core[i] !== b.core[i]) return a.core[i] < b.core[i] ? -1 : 1;
91
+ if (a.pre === null && b.pre === null) return 0;
92
+ if (a.pre === null) return 1;
93
+ if (b.pre === null) return -1;
94
+ const len = Math.max(a.pre.length, b.pre.length);
95
+ for (let i = 0; i < len; i++) {
96
+ const x = a.pre[i];
97
+ const y = b.pre[i];
98
+ if (x === void 0) return -1;
99
+ if (y === void 0) return 1;
100
+ const xn = /^\d+$/.test(x);
101
+ const yn = /^\d+$/.test(y);
102
+ if (xn && yn) {
103
+ const dx = Number(x);
104
+ const dy = Number(y);
105
+ if (dx !== dy) return dx < dy ? -1 : 1;
106
+ } else if (xn !== yn) return xn ? -1 : 1;
107
+ else if (x !== y) return x < y ? -1 : 1;
108
+ }
109
+ return 0;
110
+ }
111
+ /**
112
+ * The @deepseek-ai/dsh version this process was launched from: process.argv[1]
113
+ * is the CLI's entry, and module resolution from that file reaches the
114
+ * @deepseek-ai/dsh package.json it belongs to. Null when undetectable
115
+ * (workers, dev launchers without a resolvable package).
116
+ */
117
+ let cachedDshVersion;
118
+ function dshVersion() {
119
+ if (cachedDshVersion !== void 0) return cachedDshVersion;
120
+ cachedDshVersion = null;
121
+ try {
122
+ const entry = process.argv && process.argv[1];
123
+ if (!entry) return cachedDshVersion;
124
+ const pkgPath = createRequire(resolve(entry)).resolve("@deepseek-ai/dsh/package.json");
125
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
126
+ if (pkg && pkg.name === "@deepseek-ai/dsh" && typeof pkg.version === "string" && pkg.version !== "") cachedDshVersion = pkg.version;
127
+ } catch (error) {}
128
+ return cachedDshVersion;
129
+ }
130
+ /** Running DSH vs the supported range: 'ok' | 'old' | 'new' | 'unknown'. */
131
+ function dshCompat() {
132
+ const version = dshVersion();
133
+ if (version === null) return {
134
+ version: null,
135
+ state: "unknown"
136
+ };
137
+ const parsed = parseVersion(version);
138
+ if (parsed === null) return {
139
+ version,
140
+ state: "unknown"
141
+ };
142
+ const min = parseVersion(DSH_MIN);
143
+ const max = parseVersion(DSH_MAX);
144
+ let state = "ok";
145
+ if (compareParsed(parsed, min) < 0) state = "old";
146
+ else if (compareParsed(parsed, max) >= 0) state = "new";
147
+ return {
148
+ version,
149
+ state
150
+ };
151
+ }
41
152
  /** Write one JSON response. */
42
153
  function json(res, status, body) {
43
- res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })
44
- res.end(JSON.stringify(body))
154
+ res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
155
+ res.end(JSON.stringify(body));
45
156
  }
46
-
47
157
  /** Read a JSON request body (bounded). */
48
158
  function readJsonBody(req) {
49
- return new Promise((resolve, reject) => {
50
- let size = 0
51
- const chunks = []
52
- req.on('data', (chunk) => {
53
- size += chunk.length
54
- if (size > 64 * 1024) {
55
- reject(new Error('body-too-large'))
56
- req.destroy()
57
- return
58
- }
59
- chunks.push(chunk)
60
- })
61
- req.on('end', () => {
62
- if (chunks.length === 0) {
63
- resolve({})
64
- return
65
- }
66
- try {
67
- resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')))
68
- } catch {
69
- reject(new Error('invalid-json'))
70
- }
71
- })
72
- req.on('error', reject)
73
- })
159
+ return new Promise((resolve, reject) => {
160
+ let size = 0;
161
+ const chunks = [];
162
+ req.on("data", (chunk) => {
163
+ size += chunk.length;
164
+ if (size > 65536) {
165
+ reject(/* @__PURE__ */ new Error("body-too-large"));
166
+ req.destroy();
167
+ return;
168
+ }
169
+ chunks.push(chunk);
170
+ });
171
+ req.on("end", () => {
172
+ if (chunks.length === 0) {
173
+ resolve({});
174
+ return;
175
+ }
176
+ try {
177
+ resolve(JSON.parse(Buffer.concat(chunks).toString("utf8")));
178
+ } catch {
179
+ reject(/* @__PURE__ */ new Error("invalid-json"));
180
+ }
181
+ });
182
+ req.on("error", reject);
183
+ });
74
184
  }
75
-
76
- export function apply(ctx) {
77
- // Fiber-scoped registration: removed when this plugin is stopped/removed.
78
- ctx.settings.register(NS, SCHEMA, { base: {} })
79
-
80
- /** Current resolved section + revision + writability, as one JSON view. */
81
- const snapshot = () => {
82
- let value = ctx.settings.get(NS)
83
- let revision
84
- for (const descriptor of ctx.settings.describe()) {
85
- if (descriptor.ns === NS) {
86
- value = descriptor.value
87
- revision = descriptor.revision
88
- break
89
- }
90
- }
91
- return {
92
- ok: true,
93
- value: {
94
- value,
95
- ...(revision === undefined ? {} : { revision }),
96
- writable: ctx.settings.writable,
97
- },
98
- }
99
- }
100
-
101
- const handler = (req, res) => {
102
- if (req.method === 'GET') {
103
- try {
104
- json(res, 200, snapshot())
105
- } catch (error) {
106
- json(res, 500, { ok: false, error: error instanceof Error ? error.message : String(error) })
107
- }
108
- return
109
- }
110
- if (req.method === 'POST') {
111
- readJsonBody(req).then((body) => {
112
- const field = String(body?.field ?? '')
113
- if (!FIELDS.includes(field)) {
114
- json(res, 400, { ok: false, error: 'invalid-field' })
115
- return
116
- }
117
- const op = body?.op === 'unset' ? 'unset' : 'set'
118
- const ops = op === 'unset'
119
- ? [{ op: 'unset', path: [field] }]
120
- : [{ op: 'set', path: [field], value: body.value }]
121
- const expectedRevision = typeof body?.expectedRevision === 'number' ? body.expectedRevision : undefined
122
- return ctx.settings.mutate(NS, ops, expectedRevision)
123
- .then(() => json(res, 200, snapshot()))
124
- .catch((error) => json(res, 400, { ok: false, error: error instanceof Error ? error.message : String(error) }))
125
- }, (error) => {
126
- json(res, 400, { ok: false, error: error instanceof Error ? error.message : String(error) })
127
- })
128
- return
129
- }
130
- json(res, 405, { ok: false, error: 'method-not-allowed' })
131
- }
132
-
133
- ctx.effect(() => {
134
- const dispose = ctx.webServer.register({ kind: 'exact', path: API_PATH, handler })
135
- return () => dispose()
136
- })
185
+ function apply(ctx) {
186
+ ctx.settings.register(NS, SCHEMA, { base: {} });
187
+ /** Current resolved section + revision + writability, as one JSON view. */
188
+ const snapshot = () => {
189
+ let value = ctx.settings.get(NS);
190
+ let revision;
191
+ for (const descriptor of ctx.settings.describe()) if (descriptor.ns === NS) {
192
+ value = descriptor.value;
193
+ revision = descriptor.revision;
194
+ break;
195
+ }
196
+ return {
197
+ ok: true,
198
+ value: {
199
+ value: {
200
+ ...value,
201
+ thinkMode: resolveThinkMode(value)
202
+ },
203
+ ...revision === void 0 ? {} : { revision },
204
+ writable: ctx.settings.writable
205
+ }
206
+ };
207
+ };
208
+ const handler = (req, res) => {
209
+ if (req.method === "GET") {
210
+ try {
211
+ json(res, 200, {
212
+ ...snapshot(),
213
+ dsh: dshCompat()
214
+ });
215
+ } catch (error) {
216
+ json(res, 500, {
217
+ ok: false,
218
+ error: error instanceof Error ? error.message : String(error)
219
+ });
220
+ }
221
+ return;
222
+ }
223
+ if (req.method === "POST") {
224
+ readJsonBody(req).then((body) => {
225
+ const field = String(body?.field ?? "");
226
+ if (!FIELDS.includes(field)) {
227
+ json(res, 400, {
228
+ ok: false,
229
+ error: "invalid-field"
230
+ });
231
+ return;
232
+ }
233
+ const ops = (body?.op === "unset" ? "unset" : "set") === "unset" ? [{
234
+ op: "unset",
235
+ path: [field]
236
+ }] : [{
237
+ op: "set",
238
+ path: [field],
239
+ value: body.value
240
+ }];
241
+ const expectedRevision = typeof body?.expectedRevision === "number" ? body.expectedRevision : void 0;
242
+ return ctx.settings.mutate(NS, ops, expectedRevision).then(() => json(res, 200, {
243
+ ...snapshot(),
244
+ dsh: dshCompat()
245
+ })).catch((error) => json(res, 400, {
246
+ ok: false,
247
+ error: error instanceof Error ? error.message : String(error)
248
+ }));
249
+ }, (error) => {
250
+ json(res, 400, {
251
+ ok: false,
252
+ error: error instanceof Error ? error.message : String(error)
253
+ });
254
+ });
255
+ return;
256
+ }
257
+ json(res, 405, {
258
+ ok: false,
259
+ error: "method-not-allowed"
260
+ });
261
+ };
262
+ ctx.effect(() => {
263
+ const dispose = ctx.webServer.register({
264
+ kind: "exact",
265
+ path: API_PATH,
266
+ handler
267
+ });
268
+ return () => dispose();
269
+ });
137
270
  }
271
+ //#endregion
272
+ export { apply, inject, name };
273
+
274
+ //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "name": "dsh-toolfold",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "工具调用与思考的折叠显示:连续工具调用折叠为最后一个调用(可展开),已完成的思考随组折叠(可保留、展开时按原顺序显示),进行中的思考保持独立显示。",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/Minecraftbe/dsh-toolfold.git"
8
+ },
5
9
  "type": "module",
6
10
  "main": "lib/index.js",
7
11
  "exports": {
@@ -23,19 +27,29 @@
23
27
  }
24
28
  },
25
29
  "files": [
26
- "lib",
30
+ "lib/index.js",
31
+ "lib/client.js",
27
32
  "cordis.patch.yml",
28
33
  "scripts",
29
34
  "README.md",
30
35
  "README.en.md",
31
36
  "CHANGELOG.md"
32
37
  ],
38
+ "engines": {
39
+ "dsh": ">=0.1.2-rc.1 <0.1.3"
40
+ },
33
41
  "peerDependencies": {
34
- "@deepseek-ai/dsh-settings": ">=0.1.0-rc.7 <0.1.1 || >=0.1.1-rc.0 <0.1.2",
35
42
  "schemastery": "^3.18.0"
36
43
  },
44
+ "devDependencies": {
45
+ "jsdom": "29.1.1",
46
+ "playwright": "1.61.1",
47
+ "tsdown": "^0.22.2"
48
+ },
37
49
  "license": "MIT",
38
50
  "scripts": {
51
+ "build": "tsdown",
52
+ "build:watch": "tsdown --watch",
39
53
  "test:engine": "node tools/engine-smoke.mjs",
40
54
  "probe:live": "node tools/live-probe.mjs",
41
55
  "install:dsh": "node scripts/install-dsh.cjs",
@@ -1,69 +0,0 @@
1
- /**
2
- * build-dynamic.js — generate lib/dynamic-body.js (the `code.client` body for
3
- * cordis_define) from lib/client.js.
4
- *
5
- * The dynamic runner wraps the body in `async () => { ... }` and evaluates it
6
- * in a scope where `React` is a closure parameter, so the body must:
7
- * - NOT redeclare `var React` (var hoisting would shadow the closure
8
- * parameter and the settings card would silently never render);
9
- * - end with `return plugin;` (a bare function body has no exports).
10
- *
11
- * Usage: node lib/build-dynamic.js
12
- */
13
- 'use strict';
14
-
15
- const fs = require('fs');
16
- const path = require('path');
17
-
18
- const srcPath = path.join(__dirname, 'client.js');
19
- const outPath = path.join(__dirname, 'dynamic-body.js');
20
-
21
- let src = fs.readFileSync(srcPath, 'utf8');
22
-
23
- // 1. Extract the factory function body: everything between
24
- // "factory: function (require) {" and the final "\n});".
25
- const factoryMarker = 'factory: function (require) {';
26
- const start = src.indexOf(factoryMarker);
27
- if (start === -1) throw new Error('factory marker not found in client.js');
28
- const bodyStart = start + factoryMarker.length;
29
-
30
- const endMarker = '\n});';
31
- const end = src.lastIndexOf(endMarker);
32
- if (end === -1) throw new Error('module end marker not found in client.js');
33
-
34
- let body = src.slice(bodyStart, end);
35
- // Drop the factory's own closing brace (the trailing " }").
36
- body = body.replace(/\n[ \t]*\}\s*$/, '');
37
-
38
- // 2. Remove the CommonJS export boilerplate.
39
- const boilerplate =
40
- ' var module = { exports: {} };\n' +
41
- ' var exports = module.exports;\n' +
42
- ' Object.defineProperty(exports, Symbol.toStringTag, { value: \'Module\' });\n';
43
- if (body.indexOf(boilerplate) === -1) throw new Error('module boilerplate not found');
44
- body = body.replace(boilerplate, '');
45
-
46
- // 3. Replace the React require block with an explanatory comment (see header).
47
- const reactBlock =
48
- ' // React is a platform seed word in the web shell; the settings card\n' +
49
- ' // needs it, the folding engine does not.\n' +
50
- ' var React = null;\n' +
51
- ' try {\n' +
52
- ' React = require(\'react\');\n' +
53
- ' } catch (err) {\n' +
54
- ' React = null;\n' +
55
- ' }\n';
56
- if (body.indexOf(reactBlock) === -1) throw new Error('React block not found');
57
- body = body.replace(reactBlock,
58
- ' // React is the runner\'s closure parameter; the folding engine does not\n' +
59
- ' // need it directly, and redeclaring it here would shadow the parameter.\n');
60
-
61
- // 4. Replace the module.exports tail with a bare `return plugin;`.
62
- const tail =
63
- ' module.exports = plugin;\n' +
64
- ' return module.exports;';
65
- if (body.indexOf(tail) === -1) throw new Error('export tail not found');
66
- body = body.replace(tail, 'return plugin;\n');
67
-
68
- fs.writeFileSync(outPath, body, 'utf8');
69
- console.log('wrote', outPath, body.length, 'bytes');