dsh-copilot-key 1.0.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/README.md +76 -0
- package/cordis.patch.yml +15 -0
- package/lib/index.js +314 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# dsh-copilot-key (plugin)
|
|
2
|
+
|
|
3
|
+
DeepSeek Harness 插件:在 App 里管理 [Copilot 键钩子](../hook/)。
|
|
4
|
+
|
|
5
|
+
- 钩子(`DshCopilotKey.exe` + 登录计划任务)负责按键本身 —— App 没开也能把 DSH 拉起来;
|
|
6
|
+
- 本插件负责**管理**它,并把它变成 agent 可用的能力。
|
|
7
|
+
|
|
8
|
+
## 安装
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
dsh plugin --profile <profile> add dsh-copilot-key
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
桌面端(DeepSeek Harness.exe)的 profile 由 Electron 独占管理,CLI 会拒绝 `--profile desktop`:
|
|
15
|
+
请在 **设置 → 插件 → 添加插件** 里填 `dsh-copilot-key`。
|
|
16
|
+
|
|
17
|
+
## 钩子在哪
|
|
18
|
+
|
|
19
|
+
解析顺序:
|
|
20
|
+
|
|
21
|
+
1. profile patch 层里 `- id: copilot-key` 的 `config.directory`;
|
|
22
|
+
2. 环境变量 `DSH_COPILOT_KEY_DIR`;
|
|
23
|
+
3. `%USERPROFILE%\copilot-key`。
|
|
24
|
+
|
|
25
|
+
`copilot_key_status` 会显示当前解析到的路径以及可执行文件是否存在。
|
|
26
|
+
|
|
27
|
+
## 工具
|
|
28
|
+
|
|
29
|
+
| 工具 | 参数 | 作用 |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| `copilot_key_status` | – | 钩子进程/PID、计划任务状态、config.ini、watcher.log 末尾 |
|
|
32
|
+
| `copilot_key_start` | – | 未运行时启动钩子(GUI 进程,不闪控制台) |
|
|
33
|
+
| `copilot_key_stop` | – | 停止钩子(计划任务可能按崩溃自动重启把它拉回来) |
|
|
34
|
+
| `copilot_key_restart` | – | 停掉所有钩子进程并重新起一个 |
|
|
35
|
+
| `copilot_key_trigger` | – | 等效按一下这个键 |
|
|
36
|
+
| `copilot_key_config` | `trigger` `port` `url` `launcher` `dryrun` | 不传参=读取;传参=修改(校验 + 时间戳备份) |
|
|
37
|
+
| `copilot_key_log` | `file`(watcher/launcher/capture) `lines` | 看日志 |
|
|
38
|
+
|
|
39
|
+
钩子**每次按键都重新读** `config.ini`,所以改完配置下一次按键就生效,不用重启钩子。
|
|
40
|
+
|
|
41
|
+
## 配置(profile patch 层可选覆盖)
|
|
42
|
+
|
|
43
|
+
```yaml
|
|
44
|
+
- id: copilot-key
|
|
45
|
+
name: dsh-copilot-key
|
|
46
|
+
config:
|
|
47
|
+
directory: D:/tools/copilot-key # 钩子所在目录
|
|
48
|
+
exeName: DshCopilotKey.exe
|
|
49
|
+
taskName: DshCopilotKey
|
|
50
|
+
logLines: 10
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 两种安装方式的区别
|
|
54
|
+
|
|
55
|
+
| 方式 | 依赖怎么来 | 需要额外操作吗 |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| `npm` / 市场安装(推荐) | DSH 会给**装在 profile 里**的插件解析 `@deepseek-ai/*` 共享包(runtime 的 sharedPackages),peer 依赖由平台提供 | 不需要 |
|
|
58
|
+
| `link:` 本地源码安装 | 插件真实路径在 profile 之外,Node 从那里往上找不到 `@deepseek-ai/*` | 需要把依赖闭包放进插件自己的 `node_modules`(或让它成为 profile 内的一份拷贝) |
|
|
59
|
+
|
|
60
|
+
`link:` 装法下钩子目录可以用环境变量 `DSH_COPILOT_KEY_DIR` 指过去;也可以在 profile 的
|
|
61
|
+
patch 层里写死:
|
|
62
|
+
|
|
63
|
+
```yaml
|
|
64
|
+
- id: copilot-key
|
|
65
|
+
name: dsh-copilot-key
|
|
66
|
+
config:
|
|
67
|
+
directory: C:/dsh/copilot-key
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## 兼容性
|
|
71
|
+
|
|
72
|
+
实测 DSH 内核 **0.1.7-rc.2**;用到 `@deepseek-ai/dsh-tools` 的 `defineTool`、`dsh.bundle.patch` 与 `dsh.profile.bundles`。
|
|
73
|
+
|
|
74
|
+
## License
|
|
75
|
+
|
|
76
|
+
MIT
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# dsh-copilot-key bundle patch: manage the Copilot-key hook from the app.
|
|
2
|
+
#
|
|
3
|
+
# A single PLAIN insert (no config block), so enabling/disabling this plugin can
|
|
4
|
+
# hot-mount without a harness restart. Defaults live in the plugin's Config schema
|
|
5
|
+
# (lib/index.js); a profile can still override any of them with its own
|
|
6
|
+
# `- id: copilot-key` row under the patch layer.
|
|
7
|
+
#
|
|
8
|
+
# Division of labour (decided 2026-09-26):
|
|
9
|
+
# * DshCopilotKey.exe + the logon scheduled task keep owning the key itself, so
|
|
10
|
+
# pressing the key still starts the app when the app is not running;
|
|
11
|
+
# * this plugin manages that hook (status / start / stop / restart / trigger /
|
|
12
|
+
# config.ini / logs) and exposes it as agent tools.
|
|
13
|
+
- insert:
|
|
14
|
+
- id: copilot-key
|
|
15
|
+
name: 'dsh-copilot-key'
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-copilot-key - manage the Copilot-key hook from inside DeepSeek Harness.
|
|
3
|
+
*
|
|
4
|
+
* Division of labour (2026-09-26):
|
|
5
|
+
* * DshCopilotKey.exe plus its logon scheduled task keep owning the key itself, so
|
|
6
|
+
* pressing the Copilot key still starts DeepSeek Harness when the app is closed;
|
|
7
|
+
* * this plugin manages that hook from the running app: status, start / stop /
|
|
8
|
+
* restart, manual trigger, config.ini editing and log tailing, exposed as
|
|
9
|
+
* copilot_key_* agent tools.
|
|
10
|
+
*
|
|
11
|
+
* The hook reads config.ini on every key press, so edits made here take effect on the
|
|
12
|
+
* next press without restarting the hook.
|
|
13
|
+
*/
|
|
14
|
+
import { spawn, spawnSync } from 'node:child_process'
|
|
15
|
+
import { copyFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
16
|
+
import { dirname, join } from 'node:path'
|
|
17
|
+
import { fileURLToPath } from 'node:url'
|
|
18
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
19
|
+
import Schema from '@deepseek-ai/schemastery'
|
|
20
|
+
|
|
21
|
+
export const name = 'copilot-key'
|
|
22
|
+
export const inject = ['tools']
|
|
23
|
+
|
|
24
|
+
/** Trigger names the hook understands (see the hook/ directory in this repository). */
|
|
25
|
+
const TRIGGERS = ['winshift-f23', 'f23']
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Where the hook lives by default: $DSH_COPILOT_KEY_DIR when set, otherwise
|
|
29
|
+
* <%USERPROFILE%>\copilot-key. No machine specific path is baked in; a profile can
|
|
30
|
+
* still override `directory` with its own `- id: copilot-key` config row.
|
|
31
|
+
*/
|
|
32
|
+
function defaultDirectory() {
|
|
33
|
+
const fromEnv = String(process.env.DSH_COPILOT_KEY_DIR || '').trim()
|
|
34
|
+
if (fromEnv !== '') return fromEnv
|
|
35
|
+
const home = process.env.USERPROFILE || process.env.HOME || '.'
|
|
36
|
+
return join(home, 'copilot-key')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const Config = Schema.object({
|
|
40
|
+
directory: Schema.string().default(''),
|
|
41
|
+
exeName: Schema.string().default('DshCopilotKey.exe'),
|
|
42
|
+
taskName: Schema.string().default('DshCopilotKey'),
|
|
43
|
+
logLines: Schema.number().default(10),
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
function textOutput() {
|
|
47
|
+
return { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: String(value) }] }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** One line per mount, so it is obvious when this plugin was last loaded. */
|
|
51
|
+
function noteMount(message) {
|
|
52
|
+
try {
|
|
53
|
+
const file = join(dirname(fileURLToPath(import.meta.url)), '..', 'plugin.log')
|
|
54
|
+
writeFileSync(file, new Date().toISOString() + ' ' + message + String.fromCharCode(13) + String.fromCharCode(10), { flag: 'a' })
|
|
55
|
+
} catch { /* diagnostics only */ }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function apply(ctx, config) {
|
|
59
|
+
noteMount('applied')
|
|
60
|
+
const configured = String(config.directory || '').trim()
|
|
61
|
+
const dir = configured !== '' ? configured : defaultDirectory()
|
|
62
|
+
const exeName = String(config.exeName || 'DshCopilotKey.exe')
|
|
63
|
+
const taskName = String(config.taskName || 'DshCopilotKey')
|
|
64
|
+
const exe = join(dir, exeName)
|
|
65
|
+
const ini = join(dir, 'config.ini')
|
|
66
|
+
const LOG_FILES = { watcher: join(dir, 'watcher.log'), launcher: join(dir, 'launcher.log'), capture: join(dir, 'capture.log') }
|
|
67
|
+
const defaultLines = Math.max(1, Math.min(200, Number(config.logLines) || 10))
|
|
68
|
+
|
|
69
|
+
// ------------------------------------------------------------------ helpers
|
|
70
|
+
|
|
71
|
+
/** Run one Windows tool without ever flashing a console window. */
|
|
72
|
+
const run = (file, args, timeout = 15000) => spawnSync(file, args, { windowsHide: true, encoding: 'utf8', timeout })
|
|
73
|
+
|
|
74
|
+
const parseIni = (text) => {
|
|
75
|
+
const out = {}
|
|
76
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
77
|
+
const line = raw.trim()
|
|
78
|
+
if (line === '' || line.startsWith('#') || line.startsWith(';')) continue
|
|
79
|
+
const eq = line.indexOf('=')
|
|
80
|
+
if (eq < 1) continue
|
|
81
|
+
out[line.slice(0, eq).trim()] = line.slice(eq + 1).trim()
|
|
82
|
+
}
|
|
83
|
+
return out
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const readConfig = () => {
|
|
87
|
+
if (!existsSync(ini)) throw new Error('config.ini not found at ' + ini + ' - is the Copilot-key tool installed?')
|
|
88
|
+
return parseIni(readFileSync(ini, 'utf8'))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Rewrite individual keys in place, keeping every comment; one backup per call. */
|
|
92
|
+
const writeConfig = (patch) => {
|
|
93
|
+
const text = readFileSync(ini, 'utf8')
|
|
94
|
+
const stamp = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14)
|
|
95
|
+
const backup = ini + '.bak-' + stamp
|
|
96
|
+
copyFileSync(ini, backup)
|
|
97
|
+
const lines = text.split(/\r?\n/)
|
|
98
|
+
const pending = new Map(Object.entries(patch))
|
|
99
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
100
|
+
const m = /^(\s*)([A-Za-z0-9_.-]+)(\s*)=/.exec(lines[i])
|
|
101
|
+
if (m === null || !pending.has(m[2])) continue
|
|
102
|
+
lines[i] = m[2] + ' = ' + pending.get(m[2])
|
|
103
|
+
pending.delete(m[2])
|
|
104
|
+
}
|
|
105
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === '') lines.pop()
|
|
106
|
+
for (const entry of pending) lines.push(entry[0] + ' = ' + entry[1])
|
|
107
|
+
writeFileSync(ini, lines.join('\r\n') + '\r\n', 'utf8')
|
|
108
|
+
return backup
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** PIDs of the running hook processes. */
|
|
112
|
+
const hookPids = () => {
|
|
113
|
+
const result = run('tasklist', ['/FI', 'IMAGENAME eq ' + exeName, '/FO', 'CSV', '/NH'])
|
|
114
|
+
const pids = []
|
|
115
|
+
for (const line of String(result.stdout || '').split(/\r?\n/)) {
|
|
116
|
+
const m = /^"([^"]+)","(\d+)"/.exec(line.trim())
|
|
117
|
+
if (m !== null && m[1].toLowerCase() === exeName.toLowerCase()) pids.push(Number(m[2]))
|
|
118
|
+
}
|
|
119
|
+
return pids
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const taskState = () => {
|
|
123
|
+
const result = run('schtasks', ['/Query', '/TN', taskName, '/FO', 'LIST'])
|
|
124
|
+
const text = String(result.stdout || '') + String(result.stderr || '')
|
|
125
|
+
if (result.status !== 0 && /not exist|cannot find|找不到|不存在/i.test(text)) return { registered: false }
|
|
126
|
+
const line = text.split(/\r?\n/).find((entry) => /^\s*(Status|状态)\s*[::]/i.test(entry))
|
|
127
|
+
const state = line === undefined ? undefined : line.split(/[::]/).slice(1).join(':').trim()
|
|
128
|
+
return { registered: result.status === 0, ...(state === undefined || state === '' ? {} : { state }) }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const tail = (file, count) => {
|
|
132
|
+
if (!existsSync(file)) return []
|
|
133
|
+
const lines = readFileSync(file, 'utf8').split(/\r?\n/).filter((line) => line.trim() !== '')
|
|
134
|
+
return lines.slice(-count)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const snapshot = () => {
|
|
138
|
+
const pids = hookPids()
|
|
139
|
+
const running = pids.length > 0
|
|
140
|
+
const cfg = existsSync(ini) ? readConfig() : {}
|
|
141
|
+
return { running, pids, task: taskState(), config: cfg, exe }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const describe = (state) => [
|
|
145
|
+
'Copilot-key hook: ' + (state.running ? 'RUNNING (pid ' + state.pids.join(', ') + ')' : 'not running'),
|
|
146
|
+
'Scheduled task ' + taskName + ': ' + (state.task.registered ? 'registered' + (state.task.state === undefined ? '' : ' (' + state.task.state + ')') : 'NOT registered'),
|
|
147
|
+
'Trigger: ' + (state.config.trigger === undefined ? '(unset)' : state.config.trigger) + ' | dryrun: ' + (state.config.dryrun === undefined ? '0' : state.config.dryrun),
|
|
148
|
+
'Target: port ' + (state.config.port === undefined ? '(unset)' : state.config.port) + ' | ' + (state.config.url === undefined ? '(unset)' : state.config.url),
|
|
149
|
+
'Launcher: ' + (state.config.launcher === undefined ? '(unset)' : state.config.launcher) + (state.config.launcher !== undefined && !existsSync(state.config.launcher) ? ' [missing on disk]' : ''),
|
|
150
|
+
'Executable: ' + state.exe + (existsSync(state.exe) ? '' : ' [MISSING]'),
|
|
151
|
+
].join('\n')
|
|
152
|
+
|
|
153
|
+
const requireExe = () => {
|
|
154
|
+
if (!existsSync(exe)) throw new Error('hook executable not found: ' + exe)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const stopHook = () => {
|
|
158
|
+
const before = hookPids()
|
|
159
|
+
if (before.length === 0) return { killed: [], before }
|
|
160
|
+
run('taskkill', ['/IM', exeName, '/F'])
|
|
161
|
+
const after = hookPids()
|
|
162
|
+
return { killed: before.filter((pid) => !after.includes(pid)), before, after }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const startHook = () => {
|
|
166
|
+
const already = hookPids()
|
|
167
|
+
if (already.length > 0) return { started: false, pids: already, reason: 'already running' }
|
|
168
|
+
requireExe()
|
|
169
|
+
const child = spawn(exe, ['watch'], { detached: true, stdio: 'ignore', windowsHide: true })
|
|
170
|
+
child.unref()
|
|
171
|
+
return { started: true, pid: child.pid }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// -------------------------------------------------------------------- tools
|
|
175
|
+
|
|
176
|
+
ctx.tools.register(defineTool({
|
|
177
|
+
name: 'copilot_key_status',
|
|
178
|
+
description: 'Report the Copilot-key hook: whether it is running (and its PIDs), the logon scheduled task state, and the effective config.ini values. Read-only.',
|
|
179
|
+
parameters: {},
|
|
180
|
+
output: textOutput(),
|
|
181
|
+
async execute() {
|
|
182
|
+
const state = snapshot()
|
|
183
|
+
const recent = tail(LOG_FILES.watcher, 5)
|
|
184
|
+
return describe(state)
|
|
185
|
+
+ (recent.length === 0 ? '\nwatcher.log: (no entries)' : '\nwatcher.log (last ' + recent.length + '):\n ' + recent.join('\n '))
|
|
186
|
+
},
|
|
187
|
+
}))
|
|
188
|
+
|
|
189
|
+
ctx.tools.register(defineTool({
|
|
190
|
+
name: 'copilot_key_start',
|
|
191
|
+
description: 'Start the Copilot-key hook if it is not already running. The hook is a GUI process, so nothing flashes on screen.',
|
|
192
|
+
parameters: {},
|
|
193
|
+
output: textOutput(),
|
|
194
|
+
async execute() {
|
|
195
|
+
const result = startHook()
|
|
196
|
+
if (!result.started) return 'Copilot-key hook is already running (pid ' + result.pids.join(', ') + ').'
|
|
197
|
+
await new Promise((resolve) => setTimeout(resolve, 800))
|
|
198
|
+
const pids = hookPids()
|
|
199
|
+
return pids.length > 0
|
|
200
|
+
? 'Copilot-key hook started (pid ' + pids.join(', ') + ').'
|
|
201
|
+
: 'Copilot-key hook was launched but no process is running now - check the executable and watcher.log.'
|
|
202
|
+
},
|
|
203
|
+
}))
|
|
204
|
+
|
|
205
|
+
ctx.tools.register(defineTool({
|
|
206
|
+
name: 'copilot_key_stop',
|
|
207
|
+
description: 'Stop the Copilot-key hook (taskkill). Note: the DshCopilotKey scheduled task is configured to restart on failure, so it may come back; uninstall.ps1 removes it for good.',
|
|
208
|
+
parameters: {},
|
|
209
|
+
output: textOutput(),
|
|
210
|
+
async execute() {
|
|
211
|
+
const result = stopHook()
|
|
212
|
+
if (result.before.length === 0) return 'Copilot-key hook was not running.'
|
|
213
|
+
const survived = hookPids()
|
|
214
|
+
return 'Stopped the Copilot-key hook (killed pid ' + result.killed.join(', ') + ').'
|
|
215
|
+
+ (survived.length > 0 ? ' It is running again already (pid ' + survived.join(', ') + ') - the scheduled task relaunched it.' : '')
|
|
216
|
+
},
|
|
217
|
+
}))
|
|
218
|
+
|
|
219
|
+
ctx.tools.register(defineTool({
|
|
220
|
+
name: 'copilot_key_restart',
|
|
221
|
+
description: 'Restart the Copilot-key hook: stop every running hook process, then start one fresh.',
|
|
222
|
+
parameters: {},
|
|
223
|
+
output: textOutput(),
|
|
224
|
+
async execute() {
|
|
225
|
+
stopHook()
|
|
226
|
+
await new Promise((resolve) => setTimeout(resolve, 500))
|
|
227
|
+
const result = startHook()
|
|
228
|
+
await new Promise((resolve) => setTimeout(resolve, 800))
|
|
229
|
+
const pids = hookPids()
|
|
230
|
+
return (result.started ? 'Restarted' : 'Start skipped (' + result.reason + ') for') + ' the Copilot-key hook; now running: '
|
|
231
|
+
+ (pids.length > 0 ? 'pid ' + pids.join(', ') : 'none')
|
|
232
|
+
},
|
|
233
|
+
}))
|
|
234
|
+
|
|
235
|
+
ctx.tools.register(defineTool({
|
|
236
|
+
name: 'copilot_key_trigger',
|
|
237
|
+
description: 'Fire the Copilot-key action once, exactly as pressing the physical key would (DshCopilotKey.exe trigger). Requires the hook to be running.',
|
|
238
|
+
parameters: {},
|
|
239
|
+
output: textOutput(),
|
|
240
|
+
async execute() {
|
|
241
|
+
requireExe()
|
|
242
|
+
const result = run(exe, ['trigger'], 20000)
|
|
243
|
+
const text = String(result.stdout || '').trim()
|
|
244
|
+
const error = String(result.stderr || '').trim()
|
|
245
|
+
return 'trigger exit=' + result.status
|
|
246
|
+
+ (text === '' ? '' : '\nstdout: ' + text)
|
|
247
|
+
+ (error === '' ? '' : '\nstderr: ' + error)
|
|
248
|
+
+ '\n(If nothing happened, the hook is probably not running - call copilot_key_start first.)'
|
|
249
|
+
},
|
|
250
|
+
}))
|
|
251
|
+
|
|
252
|
+
ctx.tools.register(defineTool({
|
|
253
|
+
name: 'copilot_key_config',
|
|
254
|
+
description: 'Read config.ini, or change one or more keys. The hook re-reads the file on every key press, so changes apply on the next press without restarting it. A timestamped backup is written next to the file.',
|
|
255
|
+
parameters: {
|
|
256
|
+
trigger: { type: 'string' },
|
|
257
|
+
port: { type: 'integer' },
|
|
258
|
+
url: { type: 'string' },
|
|
259
|
+
launcher: { type: 'string' },
|
|
260
|
+
dryrun: { type: 'string' },
|
|
261
|
+
},
|
|
262
|
+
output: textOutput(),
|
|
263
|
+
async execute(args) {
|
|
264
|
+
const patch = {}
|
|
265
|
+
if (args.trigger !== undefined && args.trigger !== null && args.trigger !== '') {
|
|
266
|
+
if (!TRIGGERS.includes(args.trigger)) throw new Error('trigger must be one of: ' + TRIGGERS.join(', '))
|
|
267
|
+
patch.trigger = args.trigger
|
|
268
|
+
}
|
|
269
|
+
if (args.port !== undefined && args.port !== null) {
|
|
270
|
+
const port = Number(args.port)
|
|
271
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('port must be an integer between 1 and 65535')
|
|
272
|
+
patch.port = String(port)
|
|
273
|
+
}
|
|
274
|
+
if (args.url !== undefined && args.url !== null && args.url !== '') {
|
|
275
|
+
if (!/^https?:\/\//i.test(args.url)) throw new Error('url must start with http:// or https://')
|
|
276
|
+
patch.url = args.url
|
|
277
|
+
}
|
|
278
|
+
if (args.launcher !== undefined && args.launcher !== null && args.launcher !== '') patch.launcher = args.launcher
|
|
279
|
+
if (args.dryrun !== undefined && args.dryrun !== null && args.dryrun !== '') {
|
|
280
|
+
const value = String(args.dryrun).toLowerCase()
|
|
281
|
+
if (!['0', '1', 'true', 'false'].includes(value)) throw new Error('dryrun must be 0/1 (or true/false)')
|
|
282
|
+
patch.dryrun = value === '1' || value === 'true' ? '1' : '0'
|
|
283
|
+
}
|
|
284
|
+
const before = readConfig()
|
|
285
|
+
if (Object.keys(patch).length === 0) {
|
|
286
|
+
return 'config.ini (' + ini + '):\n' + Object.entries(before).map(([k, v]) => ' ' + k + ' = ' + v).join('\n')
|
|
287
|
+
}
|
|
288
|
+
const backup = writeConfig(patch)
|
|
289
|
+
const after = readConfig()
|
|
290
|
+
const changed = Object.keys(patch).map((key) => ' ' + key + ': ' + (before[key] === undefined ? '(unset)' : before[key]) + ' -> ' + after[key]).join('\n')
|
|
291
|
+
return 'Updated ' + ini + '\n' + changed + '\nBackup: ' + backup
|
|
292
|
+
+ (patch.dryrun === '1' ? '\nNote: dryrun=1 means the key now only logs and does not launch anything.' : '')
|
|
293
|
+
},
|
|
294
|
+
}))
|
|
295
|
+
|
|
296
|
+
ctx.tools.register(defineTool({
|
|
297
|
+
name: 'copilot_key_log',
|
|
298
|
+
description: 'Tail one of the Copilot-key logs: watcher (every key press), launcher (the cold-start script) or capture (raw key codes).',
|
|
299
|
+
parameters: {
|
|
300
|
+
file: { type: 'string', default: 'watcher' },
|
|
301
|
+
lines: { type: 'integer', default: defaultLines },
|
|
302
|
+
},
|
|
303
|
+
output: textOutput(),
|
|
304
|
+
async execute(args) {
|
|
305
|
+
const key = String(args.file || 'watcher').toLowerCase()
|
|
306
|
+
const file = LOG_FILES[key]
|
|
307
|
+
if (file === undefined) throw new Error('file must be one of: ' + Object.keys(LOG_FILES).join(', '))
|
|
308
|
+
const count = Math.max(1, Math.min(200, Number(args.lines) || defaultLines))
|
|
309
|
+
const entries = tail(file, count)
|
|
310
|
+
if (entries.length === 0) return file + ': (no entries)'
|
|
311
|
+
return file + ' (last ' + entries.length + '):\n ' + entries.join('\n ')
|
|
312
|
+
},
|
|
313
|
+
}))
|
|
314
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-copilot-key",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Manage the Windows Copilot-key hook (DshCopilotKey.exe) from inside DeepSeek Harness: status, start/stop/restart, trigger, config.ini editing and logs.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"default": "./lib/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"lib",
|
|
16
|
+
"cordis.patch.yml",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"keywords": [
|
|
20
|
+
"deepseek",
|
|
21
|
+
"dsh",
|
|
22
|
+
"dsh-plugin",
|
|
23
|
+
"copilot-key",
|
|
24
|
+
"keyboard-hook",
|
|
25
|
+
"windows",
|
|
26
|
+
"automation"
|
|
27
|
+
],
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=22"
|
|
30
|
+
},
|
|
31
|
+
"dsh": {
|
|
32
|
+
"bundle": {
|
|
33
|
+
"patch": "./cordis.patch.yml"
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@deepseek-ai/schemastery": "^3.18.2"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
41
|
+
"@deepseek-ai/dsh-tools": "*"
|
|
42
|
+
},
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git+https://github.com/plumeume/dsh-copilot-key.git",
|
|
46
|
+
"directory": "plugin"
|
|
47
|
+
},
|
|
48
|
+
"homepage": "https://github.com/plumeume/dsh-copilot-key",
|
|
49
|
+
"bugs": {
|
|
50
|
+
"url": "https://github.com/plumeume/dsh-copilot-key/issues"
|
|
51
|
+
},
|
|
52
|
+
"license": "MIT"
|
|
53
|
+
}
|