dsh-toolfold 0.1.7

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 ADDED
@@ -0,0 +1,137 @@
1
+ /**
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
+
34
+ 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
+
41
+ /** Write one JSON response. */
42
+ function json(res, status, body) {
43
+ res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' })
44
+ res.end(JSON.stringify(body))
45
+ }
46
+
47
+ /** Read a JSON request body (bounded). */
48
+ 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
+ })
74
+ }
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
+ })
137
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "dsh-toolfold",
3
+ "version": "0.1.7",
4
+ "description": "工具调用与思考的折叠显示:连续工具调用折叠为最后一个调用(可展开),已完成的思考随组折叠(可保留、展开时按原顺序显示),进行中的思考保持独立显示。",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": {
9
+ "default": "./lib/index.js"
10
+ },
11
+ "./client": {
12
+ "default": "./lib/client.js"
13
+ },
14
+ "./package.json": "./package.json"
15
+ },
16
+ "dsh": {
17
+ "bundle": {
18
+ "patch": "./cordis.patch.yml"
19
+ },
20
+ "client": {
21
+ "platform": "web",
22
+ "inject": []
23
+ }
24
+ },
25
+ "files": [
26
+ "lib",
27
+ "cordis.patch.yml",
28
+ "scripts",
29
+ "README.md",
30
+ "README.en.md",
31
+ "CHANGELOG.md"
32
+ ],
33
+ "peerDependencies": {
34
+ "@deepseek-ai/dsh-settings": ">=0.1.0-rc.7 <0.1.1 || >=0.1.1-rc.0 <0.1.2",
35
+ "schemastery": "^3.18.0"
36
+ },
37
+ "license": "MIT",
38
+ "scripts": {
39
+ "test:engine": "node tools/engine-smoke.mjs",
40
+ "probe:live": "node tools/live-probe.mjs",
41
+ "install:dsh": "node scripts/install-dsh.cjs",
42
+ "uninstall:dsh": "node scripts/install-dsh.cjs uninstall"
43
+ }
44
+ }
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * One-click install / uninstall of dsh-toolfold into a dsh profile through
4
+ * the official plugin CLI:
5
+ *
6
+ * dsh plugin --profile <name> add <this-package>
7
+ *
8
+ * which installs the package with pnpm and — seeing the package's
9
+ * `dsh.bundle.patch` declaration — automatically appends `dsh-toolfold` to
10
+ * the profile's `dsh.profile.bundles` stack. No profile file edits.
11
+ *
12
+ * Usage:
13
+ * node scripts/install-dsh.cjs # install (default profile: web)
14
+ * node scripts/install-dsh.cjs uninstall # remove
15
+ * DSH_PROFILE=headless node scripts/install-dsh.cjs
16
+ *
17
+ * After install, restart the dsh process: bundle layers are composed at boot.
18
+ */
19
+ const { spawnSync } = require('node:child_process')
20
+ const { join } = require('node:path')
21
+
22
+ const profile = process.env.DSH_PROFILE || 'web'
23
+ const uninstall = process.argv.includes('uninstall')
24
+ // Forward slashes keep the spec unambiguous when cmd/PowerShell re-quotes it.
25
+ const pkgSpec = join(__dirname, '..').replace(/\\/g, '/')
26
+ // `pnpm add` takes a path spec; `pnpm remove` takes the dependency NAME.
27
+ const target = uninstall ? 'dsh-toolfold' : pkgSpec
28
+
29
+ /** Run one dsh CLI invocation; on Windows go through PowerShell so the dsh.ps1 / pnpm.ps1 shims resolve. */
30
+ function runDsh(args) {
31
+ const quoted = args.map((arg) => "'" + String(arg).replace(/'/g, "''") + "'").join(' ')
32
+ const result = process.platform === 'win32'
33
+ ? spawnSync('powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', `dsh ${quoted}`], { stdio: 'inherit' })
34
+ : spawnSync('dsh', args, { stdio: 'inherit' })
35
+ if (result.error !== undefined) {
36
+ console.error(`dsh CLI not found on PATH (${result.error.message}); install pnpm and put the dsh launcher on PATH`)
37
+ return 127
38
+ }
39
+ return result.status ?? 1
40
+ }
41
+
42
+ const verb = uninstall ? 'remove' : 'add'
43
+ const status = runDsh(['plugin', '--profile', profile, verb, target])
44
+ if (status !== 0) {
45
+ console.error(`dsh plugin ${verb} failed (exit ${status})`)
46
+ process.exit(status)
47
+ }
48
+ if (uninstall) {
49
+ console.log(`dsh-toolfold removed from profile "${profile}". Restart dsh to unload it.`)
50
+ } else {
51
+ console.log(`dsh-toolfold installed into profile "${profile}" and added to dsh.profile.bundles.`)
52
+ console.log('Restart dsh (the GUI process) to activate — bundle layers are composed at boot.')
53
+ }