dsh-plugin-lcu 0.1.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/LICENSE +21 -0
- package/README.md +316 -0
- package/cordis.patch.yml +23 -0
- package/dev-types/dsh.d.ts +292 -0
- package/docs/README.zh.md +291 -0
- package/lib/approval.js +152 -0
- package/lib/approval.js.map +1 -0
- package/lib/connection.js +384 -0
- package/lib/connection.js.map +1 -0
- package/lib/diag.js +52 -0
- package/lib/diag.js.map +1 -0
- package/lib/host-guard.js +137 -0
- package/lib/host-guard.js.map +1 -0
- package/lib/index.js +318 -0
- package/lib/index.js.map +1 -0
- package/lib/tool.js +262 -0
- package/lib/tool.js.map +1 -0
- package/lib/types/approval.d.ts +71 -0
- package/lib/types/approval.d.ts.map +1 -0
- package/lib/types/connection.d.ts +178 -0
- package/lib/types/connection.d.ts.map +1 -0
- package/lib/types/diag.d.ts +17 -0
- package/lib/types/diag.d.ts.map +1 -0
- package/lib/types/host-guard.d.ts +46 -0
- package/lib/types/host-guard.d.ts.map +1 -0
- package/lib/types/index.d.ts +49 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/tool.d.ts +90 -0
- package/lib/types/tool.d.ts.map +1 -0
- package/package.json +79 -0
- package/scripts/codex-baseline.json +10 -0
- package/scripts/gen-presets.mjs +266 -0
- package/scripts/probe-lcu.mjs +154 -0
- package/scripts/update-codex.mjs +525 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* gen-presets.mjs — derive DSH agent presets from the installed `ptc` preset.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists
|
|
6
|
+
* ---------------
|
|
7
|
+
* DSH agent presets have **no inheritance** (`config.plugins` is a complete
|
|
8
|
+
* child-plugin list) and `cordis.patch.yml` patches **replace** a whole entry's
|
|
9
|
+
* config rather than deep-merging it. So a custom preset has to restate the full
|
|
10
|
+
* plugin list of whatever it is based on. This script reads that base list
|
|
11
|
+
* straight out of the *installed* app and regenerates our preset rows, so a DSH
|
|
12
|
+
* upgrade is one re-run instead of a hand merge.
|
|
13
|
+
*
|
|
14
|
+
* It is deliberately dependency-free (plain Node, no YAML library): the base
|
|
15
|
+
* file is copied line-for-line and only surgically edited, and every expected
|
|
16
|
+
* edit is asserted — if a DSH upgrade changes the shape, this script fails loudly
|
|
17
|
+
* instead of emitting a subtly wrong preset.
|
|
18
|
+
*
|
|
19
|
+
* Usage
|
|
20
|
+
* -----
|
|
21
|
+
* node scripts/gen-presets.mjs # write into the desktop profile patch
|
|
22
|
+
* node scripts/gen-presets.mjs --dry-run # print, write nothing
|
|
23
|
+
* node scripts/gen-presets.mjs --out FILE # write to FILE instead
|
|
24
|
+
* node scripts/gen-presets.mjs --with-heavy # also emit preset-heavy (needs dsh-plugin-lcu)
|
|
25
|
+
* node scripts/gen-presets.mjs --asar PATH # override the app.asar path
|
|
26
|
+
* node scripts/gen-presets.mjs --profile DIR # override the profile directory
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { closeSync, openSync, readFileSync, readSync, writeFileSync } from 'node:fs'
|
|
30
|
+
import { homedir } from 'node:os'
|
|
31
|
+
import { join } from 'node:path'
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Defaults
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
const DEFAULT_ASAR =
|
|
38
|
+
'/Applications/DeepSeek Harness.app/Contents/Resources/app.asar'
|
|
39
|
+
const BASE_PRESET = 'dsh/node_modules/@deepseek-ai/dsh-web-app/presets/ptc.patch.yml'
|
|
40
|
+
const DEFAULT_PROFILE = join(homedir(), '.dsh', 'profiles', 'desktop')
|
|
41
|
+
|
|
42
|
+
const BEGIN = '# >>> BEGIN generated presets (scripts/gen-presets.mjs) >>>'
|
|
43
|
+
const END = '# <<< END generated presets <<<'
|
|
44
|
+
|
|
45
|
+
/** The two presets this script owns. `daily` is a faithful PTC + Codex; `heavy`
|
|
46
|
+
* flips the tool presentation to `both` so LCU's `js` is callable natively
|
|
47
|
+
* instead of being nested inside a `run_code` program. */
|
|
48
|
+
const PRESETS = {
|
|
49
|
+
daily: {
|
|
50
|
+
id: 'daily',
|
|
51
|
+
name: '日常',
|
|
52
|
+
description: '日常编码:PTC 程序化工具调用 + Codex 委派',
|
|
53
|
+
order: 5,
|
|
54
|
+
presentation: null, // keep the base's `ptc`
|
|
55
|
+
enable: ['tool-subagent-codex'],
|
|
56
|
+
append: [],
|
|
57
|
+
},
|
|
58
|
+
heavy: {
|
|
59
|
+
id: 'heavy',
|
|
60
|
+
name: '重活',
|
|
61
|
+
description: '超前/困难的重活:需要 Computer 或 Browser 时用',
|
|
62
|
+
order: 6,
|
|
63
|
+
// `both` so LCU's `js` stays callable natively: in pure `ptc` presentation
|
|
64
|
+
// the model would have to nest a whole CUA program inside a run_code string.
|
|
65
|
+
// No tool row is added here — `dsh-plugin-lcu` is mounted at the root and
|
|
66
|
+
// registers its tools per Agent by checking this preset's id.
|
|
67
|
+
presentation: 'both',
|
|
68
|
+
enable: ['tool-subagent-codex'],
|
|
69
|
+
append: [],
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Minimal asar reader (Electron's asar is a simple header + flat blob)
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Read one file out of an asar archive without Electron.
|
|
79
|
+
* @param asarPath - absolute path to the `.asar` file
|
|
80
|
+
* @param innerPath - slash-separated path inside the archive
|
|
81
|
+
* @returns the file contents
|
|
82
|
+
*/
|
|
83
|
+
function readAsarFile(asarPath, innerPath) {
|
|
84
|
+
const fd = openSync(asarPath, 'r')
|
|
85
|
+
try {
|
|
86
|
+
const head = Buffer.alloc(16)
|
|
87
|
+
readSync(fd, head, 0, 16, 0)
|
|
88
|
+
const baseOffset = 8 + head.readUInt32LE(4)
|
|
89
|
+
const jsonSize = head.readUInt32LE(12)
|
|
90
|
+
const json = Buffer.alloc(jsonSize)
|
|
91
|
+
readSync(fd, json, 0, jsonSize, 16)
|
|
92
|
+
let node = JSON.parse(json.toString('utf8'))
|
|
93
|
+
for (const segment of innerPath.split('/').filter(Boolean)) {
|
|
94
|
+
node = node.files?.[segment]
|
|
95
|
+
if (node === undefined) throw new Error(`not in archive: ${innerPath}`)
|
|
96
|
+
}
|
|
97
|
+
if (node.files !== undefined) throw new Error(`is a directory: ${innerPath}`)
|
|
98
|
+
const buf = Buffer.alloc(node.size)
|
|
99
|
+
readSync(fd, buf, 0, node.size, baseOffset + Number(node.offset))
|
|
100
|
+
return buf
|
|
101
|
+
} finally {
|
|
102
|
+
closeSync(fd)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// Line surgery
|
|
108
|
+
// ---------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
/** Indentation (0-based column of the first non-space char); -1 for blank lines. */
|
|
111
|
+
const indentOf = (line) => (line.trim() === '' ? -1 : line.search(/\S/))
|
|
112
|
+
|
|
113
|
+
/** Index of the child row `- id: <rowId>` inside a plugin list. */
|
|
114
|
+
function findRow(lines, rowId) {
|
|
115
|
+
const index = lines.findIndex((line) => line.trim().startsWith(`- id: ${rowId}`))
|
|
116
|
+
if (index < 0) throw new Error(`plugin row not found: ${rowId}`)
|
|
117
|
+
return index
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Iterate the direct/indirect body lines of the row starting at `start`,
|
|
122
|
+
* stopping at the next row or any line dedented to the row's own level.
|
|
123
|
+
*/
|
|
124
|
+
function* body(lines, start) {
|
|
125
|
+
const base = indentOf(lines[start])
|
|
126
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
127
|
+
const ind = indentOf(lines[i])
|
|
128
|
+
if (ind === -1) continue
|
|
129
|
+
if (ind <= base) return
|
|
130
|
+
yield i
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Remove `disabled: true` from one row, turning it on.
|
|
136
|
+
* @returns a new line array
|
|
137
|
+
*/
|
|
138
|
+
function enableRow(lines, rowId) {
|
|
139
|
+
const start = findRow(lines, rowId)
|
|
140
|
+
for (const i of body(lines, start)) {
|
|
141
|
+
if (lines[i].trim().startsWith('disabled:')) {
|
|
142
|
+
return lines.filter((_, k) => k !== i)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return lines // already enabled — idempotent
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Rewrite `mode:` inside the `tool-presentation` row. */
|
|
149
|
+
function setPresentationMode(lines, mode) {
|
|
150
|
+
const start = findRow(lines, 'tool-presentation')
|
|
151
|
+
for (const i of body(lines, start)) {
|
|
152
|
+
if (/^\s*mode:\s*\S+\s*$/.test(lines[i])) {
|
|
153
|
+
const next = [...lines]
|
|
154
|
+
next[i] = lines[i].replace(/mode:\s*\S+\s*$/, `mode: ${mode}`)
|
|
155
|
+
return next
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
throw new Error('tool-presentation row has no `mode:` field')
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Extract the `plugins:` sub-tree (the row list) from a preset patch file. */
|
|
162
|
+
function extractPlugins(lines) {
|
|
163
|
+
const index = lines.findIndex((line) => /^\s*plugins:\s*$/.test(line))
|
|
164
|
+
if (index < 0) throw new Error('base preset has no `plugins:` key')
|
|
165
|
+
const base = indentOf(lines[index])
|
|
166
|
+
const out = []
|
|
167
|
+
for (let i = index + 1; i < lines.length; i += 1) {
|
|
168
|
+
const ind = indentOf(lines[i])
|
|
169
|
+
if (ind !== -1 && ind <= base) break
|
|
170
|
+
out.push(lines[i])
|
|
171
|
+
}
|
|
172
|
+
if (out.every((line) => line.trim() === '')) throw new Error('base `plugins:` is empty')
|
|
173
|
+
while (out.length > 0 && out[out.length - 1].trim() === '') out.pop()
|
|
174
|
+
return out
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// Preset rendering
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
function renderPreset(spec, basePlugins) {
|
|
182
|
+
const plugins = spec.enable.reduce(enableRow, [...basePlugins])
|
|
183
|
+
const withMode = spec.presentation === null
|
|
184
|
+
? plugins
|
|
185
|
+
: setPresentationMode(plugins, spec.presentation)
|
|
186
|
+
const out = [
|
|
187
|
+
` - id: preset-${spec.id}`,
|
|
188
|
+
" name: '@deepseek-ai/dsh-agent-preset'",
|
|
189
|
+
' config:',
|
|
190
|
+
` id: ${spec.id}`,
|
|
191
|
+
` name: '${spec.name}'`,
|
|
192
|
+
` description: '${spec.description}'`,
|
|
193
|
+
` order: ${spec.order}`,
|
|
194
|
+
' plugins:',
|
|
195
|
+
...withMode,
|
|
196
|
+
...spec.append,
|
|
197
|
+
]
|
|
198
|
+
return out
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
// Main
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
|
|
205
|
+
function parseArgs(argv) {
|
|
206
|
+
const opts = {
|
|
207
|
+
dryRun: false,
|
|
208
|
+
withHeavy: false,
|
|
209
|
+
out: undefined,
|
|
210
|
+
asar: DEFAULT_ASAR,
|
|
211
|
+
profile: DEFAULT_PROFILE,
|
|
212
|
+
}
|
|
213
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
214
|
+
const arg = argv[i]
|
|
215
|
+
if (arg === '--dry-run') opts.dryRun = true
|
|
216
|
+
else if (arg === '--with-heavy') opts.withHeavy = true
|
|
217
|
+
else if (arg === '--out') opts.out = argv[++i]
|
|
218
|
+
else if (arg === '--asar') opts.asar = argv[++i]
|
|
219
|
+
else if (arg === '--profile') opts.profile = argv[++i]
|
|
220
|
+
else throw new Error(`unknown argument: ${arg}`)
|
|
221
|
+
}
|
|
222
|
+
return opts
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function buildBlock(baseLines, { withHeavy }) {
|
|
226
|
+
const basePlugins = extractPlugins(baseLines)
|
|
227
|
+
const specs = withHeavy ? [PRESETS.daily, PRESETS.heavy] : [PRESETS.daily]
|
|
228
|
+
const lines = [BEGIN, '- insert:']
|
|
229
|
+
for (const spec of specs) lines.push(...renderPreset(spec, basePlugins))
|
|
230
|
+
lines.push(END)
|
|
231
|
+
return { block: lines.join('\n'), basePlugins, specs }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Replace the marked block in place, or append one. */
|
|
235
|
+
function spliceBlock(text, block) {
|
|
236
|
+
const begin = text.indexOf(BEGIN)
|
|
237
|
+
const end = text.indexOf(END)
|
|
238
|
+
if (begin >= 0 && end > begin) {
|
|
239
|
+
return text.slice(0, begin) + block + text.slice(end + END.length)
|
|
240
|
+
}
|
|
241
|
+
const separator = text.endsWith('\n') ? '\n' : '\n\n'
|
|
242
|
+
return `${text}${separator}${block}\n`
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function main() {
|
|
246
|
+
const opts = parseArgs(process.argv.slice(2))
|
|
247
|
+
const baseLines = readAsarFile(opts.asar, BASE_PRESET).toString('utf8').split('\n')
|
|
248
|
+
const { block, basePlugins, specs } = buildBlock(baseLines, opts)
|
|
249
|
+
const verb = opts.dryRun ? 'would write' : 'wrote'
|
|
250
|
+
const rowCount = basePlugins.filter((line) => /^\s*- id: /.test(line)).length
|
|
251
|
+
|
|
252
|
+
if (opts.dryRun) {
|
|
253
|
+
process.stdout.write(`${block}\n`)
|
|
254
|
+
} else {
|
|
255
|
+
const target = opts.out ?? join(opts.profile, 'cordis.patch.yml')
|
|
256
|
+
const before = readFileSync(target, 'utf8')
|
|
257
|
+
writeFileSync(target, spliceBlock(before, block), { mode: 0o600 })
|
|
258
|
+
process.stderr.write(`${verb} ${target}\n`)
|
|
259
|
+
}
|
|
260
|
+
process.stderr.write(
|
|
261
|
+
`base: ${BASE_PRESET} (${rowCount} top-level rows)\n`
|
|
262
|
+
+ `presets: ${specs.map((s) => s.id).join(', ')}\n`,
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
main()
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* probe-lcu.mjs — talk to the installed `lcu` MCP server directly.
|
|
4
|
+
*
|
|
5
|
+
* A diagnostic for `dsh-plugin-lcu` development: it connects the way the plugin
|
|
6
|
+
* will (newline-delimited JSON-RPC over stdio, `capabilities.elicitation`
|
|
7
|
+
* declared), prints what the server advertises, and optionally calls one tool.
|
|
8
|
+
*
|
|
9
|
+
* Usage
|
|
10
|
+
* -----
|
|
11
|
+
* node scripts/probe-lcu.mjs # initialize + tools/list
|
|
12
|
+
* node scripts/probe-lcu.mjs --instructions # also dump server instructions
|
|
13
|
+
* node scripts/probe-lcu.mjs --call js --code 'return await cua.listApps()'
|
|
14
|
+
* node scripts/probe-lcu.mjs --command /path/to/lcu # override the binary
|
|
15
|
+
*
|
|
16
|
+
* It never answers elicitations, so any call that needs an approval fails closed
|
|
17
|
+
* exactly like a host that cannot present the request.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { spawn } from 'node:child_process'
|
|
21
|
+
import { homedir } from 'node:os'
|
|
22
|
+
import { join } from 'node:path'
|
|
23
|
+
import { createInterface } from 'node:readline'
|
|
24
|
+
|
|
25
|
+
const DEFAULT_COMMAND = join(homedir(), '.local/share/lcu/current/bin/lcu')
|
|
26
|
+
|
|
27
|
+
/** Newest protocol revision the MCP SDK pair here is expected to negotiate. */
|
|
28
|
+
const PROTOCOL_VERSION = '2025-06-18'
|
|
29
|
+
|
|
30
|
+
function parseArgs(argv) {
|
|
31
|
+
const opts = { command: DEFAULT_COMMAND, instructions: false, call: undefined, code: undefined, limit: 2000 }
|
|
32
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
33
|
+
const arg = argv[i]
|
|
34
|
+
if (arg === '--command') opts.command = argv[++i]
|
|
35
|
+
else if (arg === '--instructions') opts.instructions = true
|
|
36
|
+
else if (arg === '--call') opts.call = argv[++i]
|
|
37
|
+
else if (arg === '--code') opts.code = argv[++i]
|
|
38
|
+
else if (arg === '--limit') opts.limit = Number(argv[++i])
|
|
39
|
+
else throw new Error(`unknown argument: ${arg}`)
|
|
40
|
+
}
|
|
41
|
+
return opts
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** One request/response session with the LCU MCP server. */
|
|
45
|
+
class LcuSession {
|
|
46
|
+
constructor(command) {
|
|
47
|
+
this.nextId = 1
|
|
48
|
+
this.pending = new Map()
|
|
49
|
+
this.notifications = []
|
|
50
|
+
this.stderr = ''
|
|
51
|
+
this.child = spawn(command, [], { stdio: ['pipe', 'pipe', 'pipe'], env: process.env })
|
|
52
|
+
this.child.stderr.on('data', (chunk) => { this.stderr += chunk.toString() })
|
|
53
|
+
this.child.on('exit', (code) => {
|
|
54
|
+
for (const { reject } of this.pending.values()) reject(new Error(`lcu exited (${code})\n${this.stderr.slice(-800)}`))
|
|
55
|
+
this.pending.clear()
|
|
56
|
+
})
|
|
57
|
+
createInterface({ input: this.child.stdout }).on('line', (line) => this.#onLine(line))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#onLine(line) {
|
|
61
|
+
if (line.trim() === '') return
|
|
62
|
+
let message
|
|
63
|
+
try { message = JSON.parse(line) } catch { return }
|
|
64
|
+
if (message.id !== undefined && message.method === undefined) {
|
|
65
|
+
const entry = this.pending.get(message.id)
|
|
66
|
+
if (entry === undefined) return
|
|
67
|
+
this.pending.delete(message.id)
|
|
68
|
+
if (message.error !== undefined) entry.reject(new Error(JSON.stringify(message.error)))
|
|
69
|
+
else entry.resolve(message.result)
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
this.notifications.push(message)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
request(method, params, timeoutMs = 60_000) {
|
|
76
|
+
const id = this.nextId++
|
|
77
|
+
this.child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`)
|
|
78
|
+
return new Promise((resolve, reject) => {
|
|
79
|
+
const timer = setTimeout(() => {
|
|
80
|
+
this.pending.delete(id)
|
|
81
|
+
reject(new Error(`${method} timed out after ${timeoutMs}ms`))
|
|
82
|
+
}, timeoutMs)
|
|
83
|
+
this.pending.set(id, {
|
|
84
|
+
resolve: (value) => { clearTimeout(timer); resolve(value) },
|
|
85
|
+
reject: (error) => { clearTimeout(timer); reject(error) },
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
notify(method, params) {
|
|
91
|
+
this.child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', method, params })}\n`)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
close() { this.child.kill('SIGTERM') }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function main() {
|
|
98
|
+
const opts = parseArgs(process.argv.slice(2))
|
|
99
|
+
const session = new LcuSession(opts.command)
|
|
100
|
+
try {
|
|
101
|
+
const initialized = await session.request('initialize', {
|
|
102
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
103
|
+
capabilities: { elicitation: {} },
|
|
104
|
+
clientInfo: { name: 'dsh-plugin-lcu-probe', version: '0.0.1' },
|
|
105
|
+
}, 120_000)
|
|
106
|
+
console.log('=== initialize ===')
|
|
107
|
+
console.log(JSON.stringify({
|
|
108
|
+
protocolVersion: initialized.protocolVersion,
|
|
109
|
+
serverInfo: initialized.serverInfo,
|
|
110
|
+
capabilities: initialized.capabilities,
|
|
111
|
+
instructionBytes: initialized.instructions?.length ?? 0,
|
|
112
|
+
}, null, 2))
|
|
113
|
+
|
|
114
|
+
if (opts.instructions) {
|
|
115
|
+
console.log('\n=== instructions ===')
|
|
116
|
+
console.log(initialized.instructions ?? '(none)')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
session.notify('notifications/initialized')
|
|
120
|
+
|
|
121
|
+
const listed = await session.request('tools/list', {}, 120_000)
|
|
122
|
+
console.log('\n=== tools ===')
|
|
123
|
+
for (const tool of listed.tools ?? []) {
|
|
124
|
+
const required = tool.inputSchema?.required ?? []
|
|
125
|
+
const props = Object.keys(tool.inputSchema?.properties ?? {})
|
|
126
|
+
console.log(` ${tool.name.padEnd(26)} required=[${required.join(',')}] props=[${props.join(',')}]`)
|
|
127
|
+
}
|
|
128
|
+
const hostOnly = (listed.tools ?? []).filter((tool) => tool.name !== 'js' && tool.name !== 'js_reset')
|
|
129
|
+
if (hostOnly.length > 0) {
|
|
130
|
+
console.log(`\n ⚠ host-only tools that must NOT reach the model: ${hostOnly.map((t) => t.name).join(', ')}`)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (opts.call !== undefined) {
|
|
134
|
+
console.log(`\n=== tools/call ${opts.call} ===`)
|
|
135
|
+
const result = await session.request('tools/call', {
|
|
136
|
+
name: opts.call,
|
|
137
|
+
arguments: opts.code === undefined ? {} : { code: opts.code },
|
|
138
|
+
}, 300_000)
|
|
139
|
+
const text = (result.content ?? []).filter((item) => item.type === 'text').map((item) => item.text).join('\n')
|
|
140
|
+
console.log(`isError: ${result.isError === true}`)
|
|
141
|
+
console.log(text.slice(0, opts.limit) || `(no text; content types: ${(result.content ?? []).map((i) => i.type).join(',')})`)
|
|
142
|
+
if (text.length > opts.limit) console.log(`… (${text.length - opts.limit} more chars; raise --limit)`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (session.notifications.length > 0) {
|
|
146
|
+
console.log(`\n=== ${session.notifications.length} notification(s) ===`)
|
|
147
|
+
for (const note of session.notifications.slice(0, 5)) console.log(` ${note.method}`)
|
|
148
|
+
}
|
|
149
|
+
} finally {
|
|
150
|
+
session.close()
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
await main()
|