@hzxwonder/dsh-mcp 0.4.0 → 0.6.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/.agents/skills/deepseek-worker/SKILL.md +61 -17
- package/README.md +97 -37
- package/config/agents.md +9 -0
- package/config/routing.md +45 -10
- package/config/worker.patch.yml +12 -2
- package/package.json +5 -1
- package/server.json +21 -6
- package/src/cli.js +4 -0
- package/src/doctor.js +230 -0
- package/src/monitor.html +77 -0
- package/src/monitor.js +76 -0
- package/src/provider.js +301 -5
- package/src/server.js +29 -9
- package/src/setup-terminal.js +183 -0
- package/src/setup.js +76 -71
- package/src/worker.js +68 -17
- package/src/setup-ui.js +0 -112
package/src/setup.js
CHANGED
|
@@ -4,12 +4,11 @@ import { homedir } from 'node:os';
|
|
|
4
4
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
6
|
import { isDeepStrictEqual, parseArgs, parseEnv } from 'node:util';
|
|
7
|
-
import { createInterface } from 'node:readline';
|
|
8
|
-
import { Writable } from 'node:stream';
|
|
9
7
|
import { randomUUID } from 'node:crypto';
|
|
10
8
|
import { parse } from 'smol-toml';
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
9
|
+
import { parseProviderSettings, resolveProviderSettings } from './provider.js';
|
|
10
|
+
import { runTerminalSetup } from './setup-terminal.js';
|
|
11
|
+
import { inspectRegistration } from './doctor.js';
|
|
13
12
|
|
|
14
13
|
const packageRoot = fileURLToPath(new URL('../', import.meta.url));
|
|
15
14
|
const defaultHome = join(homedir(), '.dsh-mcp');
|
|
@@ -53,6 +52,29 @@ function parseConfig(text) {
|
|
|
53
52
|
catch { throw new Error('Codex config.toml is not valid TOML. Repair this file before setup.'); }
|
|
54
53
|
}
|
|
55
54
|
|
|
55
|
+
function updateCredential(text, requestedKey) {
|
|
56
|
+
const before = parseEnv(text);
|
|
57
|
+
const key = (requestedKey ?? before.DEEPSEEK_API_KEY)?.trim();
|
|
58
|
+
if (!key || /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(key)) {
|
|
59
|
+
throw new Error('API Key 必须是非空的单行文本。');
|
|
60
|
+
}
|
|
61
|
+
if (before.DEEPSEEK_API_KEY === key) return text;
|
|
62
|
+
if (/[\r\n]/.test(before.DEEPSEEK_API_KEY ?? '')) {
|
|
63
|
+
throw new Error('旧凭据格式无法安全更新。请先备份私有 .env 文件,现有内容未改变。');
|
|
64
|
+
}
|
|
65
|
+
let replaced = false;
|
|
66
|
+
let next = text.replace(/^[\uFEFF \t]*(?:export[ \t]+)?DEEPSEEK_API_KEY[ \t]*=.*(?:\r?\n|$)/gm, () => {
|
|
67
|
+
if (replaced) return '';
|
|
68
|
+
replaced = true;
|
|
69
|
+
return `DEEPSEEK_API_KEY=${JSON.stringify(key)}\n`;
|
|
70
|
+
});
|
|
71
|
+
if (!replaced) next += `${next && !next.endsWith('\n') ? '\n' : ''}DEEPSEEK_API_KEY=${JSON.stringify(key)}\n`;
|
|
72
|
+
if (!isDeepStrictEqual({ ...parseEnv(next) }, { ...before, DEEPSEEK_API_KEY: key })) {
|
|
73
|
+
throw new Error('无法在保留其他凭据配置的同时安全更新 API Key,现有内容未改变。');
|
|
74
|
+
}
|
|
75
|
+
return next;
|
|
76
|
+
}
|
|
77
|
+
|
|
56
78
|
async function matchesPreviousBundledSkill(server, skillText) {
|
|
57
79
|
const script = server?.args?.[0];
|
|
58
80
|
if (typeof script !== 'string' || !isAbsolute(script)) return false;
|
|
@@ -89,27 +111,30 @@ export async function installCodex({
|
|
|
89
111
|
home = defaultHome,
|
|
90
112
|
codexHome = process.env.CODEX_HOME || join(homedir(), '.codex'),
|
|
91
113
|
skillsHome = join(homedir(), '.agents', 'skills'),
|
|
92
|
-
apiKey, baseUrl,
|
|
114
|
+
apiKey, baseUrl, protocol, model,
|
|
93
115
|
}) {
|
|
94
116
|
if (process.platform === 'win32') throw new Error('Setup requires macOS or Linux.');
|
|
95
117
|
home = resolve(home);
|
|
96
118
|
codexHome = resolve(codexHome);
|
|
97
119
|
skillsHome = resolve(skillsHome);
|
|
98
120
|
const configPath = join(codexHome, 'config.toml');
|
|
121
|
+
const agentsPath = join(codexHome, 'AGENTS.md');
|
|
99
122
|
const skillDirectory = join(skillsHome, 'deepseek-worker');
|
|
100
123
|
const skillPath = join(skillDirectory, 'SKILL.md');
|
|
101
124
|
const keyPath = join(home, '.env');
|
|
102
125
|
const providerPath = join(home, 'settings.json');
|
|
103
126
|
const directories = [codexHome, dirname(skillsHome), skillsHome, skillDirectory, home];
|
|
104
127
|
for (const directory of directories) await checkDirectory(directory);
|
|
105
|
-
const [config, skill, credentials, provider, bundledSkill] = await Promise.all([
|
|
128
|
+
const [config, skill, credentials, provider, bundledSkill, agents, routing] = await Promise.all([
|
|
106
129
|
readExisting(configPath), readExisting(skillPath), readExisting(keyPath), readExisting(providerPath),
|
|
107
130
|
readFile(join(packageRoot, '.agents', 'skills', 'deepseek-worker', 'SKILL.md'), 'utf8'),
|
|
131
|
+
readExisting(agentsPath), readFile(join(packageRoot, 'config', 'agents.md'), 'utf8'),
|
|
108
132
|
]);
|
|
109
133
|
const parsed = parseConfig(config.text);
|
|
110
134
|
const args = [join(packageRoot, 'src', 'server.js'), '--dsh-home', home];
|
|
111
135
|
const entry = [
|
|
112
136
|
'[mcp_servers.dsh]', `command = ${JSON.stringify(process.execPath)}`, `args = ${JSON.stringify(args)}`,
|
|
137
|
+
'enabled = true',
|
|
113
138
|
'startup_timeout_sec = 20', 'tool_timeout_sec = 960',
|
|
114
139
|
].join('\n');
|
|
115
140
|
const { text: nextConfig, previous } = managedBlock(config.text, '# BEGIN dsh-mcp', '# END dsh-mcp', entry);
|
|
@@ -124,17 +149,17 @@ export async function installCodex({
|
|
|
124
149
|
throw new Error('A different deepseek-worker skill already exists and cannot be safely upgraded. Back up or rename that skill before setup; existing files were not changed.');
|
|
125
150
|
}
|
|
126
151
|
parseConfig(nextConfig);
|
|
127
|
-
const
|
|
152
|
+
const { text: nextAgents } = managedBlock(agents.text, '<!-- BEGIN dsh-mcp -->', '<!-- END dsh-mcp -->', routing);
|
|
128
153
|
const providerSettings = parseProviderSettings(provider.text);
|
|
129
|
-
const
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
154
|
+
const previousProvider = resolveProviderSettings(providerSettings, { legacy: !providerSettings.protocol && typeof providerSettings.baseUrl === 'string' });
|
|
155
|
+
const selected = resolveProviderSettings({
|
|
156
|
+
...previousProvider,
|
|
157
|
+
...(protocol === undefined ? {} : { protocol }),
|
|
158
|
+
...(baseUrl === undefined ? {} : { baseUrl }),
|
|
159
|
+
...(model === undefined ? {} : { model }),
|
|
160
|
+
});
|
|
161
|
+
const nextProvider = JSON.stringify({ ...providerSettings, ...selected }, null, 2) + '\n';
|
|
162
|
+
const nextCredentials = updateCredential(credentials.text, apiKey);
|
|
138
163
|
|
|
139
164
|
// Check all user-file conflicts before creating credentials or Codex files.
|
|
140
165
|
for (const directory of directories) await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
@@ -145,77 +170,57 @@ export async function installCodex({
|
|
|
145
170
|
else await chmod(providerPath, 0o600);
|
|
146
171
|
if (nextConfig !== config.text) await atomicWrite(configPath, nextConfig, config.mode ?? 0o644);
|
|
147
172
|
if (skill.text !== bundledSkill || skill.mode === null) await atomicWrite(skillPath, bundledSkill, skill.mode ?? 0o644);
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function question(prompt, hidden = false) {
|
|
152
|
-
if (!process.stdin.isTTY) throw new Error('Use a terminal for prompts, or supply DEEPSEEK_API_KEY in the environment.');
|
|
153
|
-
const output = hidden ? new Writable({ write(_chunk, _encoding, callback) { callback(); } }) : process.stdout;
|
|
154
|
-
const input = createInterface({ input: process.stdin, output, terminal: true, historySize: 0 });
|
|
155
|
-
if (hidden) process.stdout.write(prompt);
|
|
156
|
-
return new Promise((accept, reject) => {
|
|
157
|
-
input.once('SIGINT', () => { input.close(); reject(new Error('Setup cancelled.')); });
|
|
158
|
-
input.once('close', () => reject(new Error('Setup input closed.')));
|
|
159
|
-
input.question(hidden ? '' : prompt, (answer) => {
|
|
160
|
-
accept(answer);
|
|
161
|
-
input.close();
|
|
162
|
-
if (hidden) process.stdout.write('\n');
|
|
163
|
-
});
|
|
164
|
-
});
|
|
173
|
+
if (nextAgents !== agents.text) await atomicWrite(agentsPath, nextAgents, agents.mode ?? 0o644);
|
|
174
|
+
return { home, configPath, skillPath, agentsPath };
|
|
165
175
|
}
|
|
166
176
|
|
|
167
177
|
export async function runSetup() {
|
|
168
178
|
const { values } = parseArgs({ options: {
|
|
169
179
|
home: { type: 'string', default: defaultHome },
|
|
170
|
-
terminal: { type: 'boolean', default: false },
|
|
171
180
|
help: { type: 'boolean', default: false },
|
|
172
181
|
} });
|
|
173
182
|
if (values.help) {
|
|
174
|
-
console.log('
|
|
175
|
-
console.log('
|
|
176
|
-
console.log('
|
|
177
|
-
console.log('
|
|
183
|
+
console.log('用法: dsh-mcp setup [--home /private/dsh-home]');
|
|
184
|
+
console.log('纯终端配置:协议 → Base URL → API Key → 探测并选择模型。');
|
|
185
|
+
console.log('再次运行可逐项修改现有配置;连接测试通过后才保存并注册 MCP。');
|
|
186
|
+
console.log('诊断 Codex 注册和工具握手: dsh-mcp doctor');
|
|
178
187
|
return;
|
|
179
188
|
}
|
|
189
|
+
if (!process.stdin.isTTY) throw new Error('配置向导需要交互式终端。请在终端运行 dsh-mcp setup。');
|
|
180
190
|
const home = resolve(values.home);
|
|
181
191
|
await checkDirectory(home);
|
|
182
192
|
const credentials = await readExisting(join(home, '.env'));
|
|
183
|
-
const
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
process.removeListener('SIGINT', cancel);
|
|
208
|
-
process.removeListener('SIGTERM', cancel);
|
|
209
|
-
}
|
|
193
|
+
const saved = await readExisting(join(home, 'settings.json'));
|
|
194
|
+
const rawSettings = parseProviderSettings(saved.text);
|
|
195
|
+
const existing = saved.mode !== null || Boolean(parseEnv(credentials.text).DEEPSEEK_API_KEY);
|
|
196
|
+
const initial = existing ? rawSettings : {
|
|
197
|
+
...(process.env.DSH_MCP_PROTOCOL ? { protocol: process.env.DSH_MCP_PROTOCOL } : {}),
|
|
198
|
+
...(process.env.DEEPSEEK_BASE_URL ? { baseUrl: process.env.DEEPSEEK_BASE_URL } : {}),
|
|
199
|
+
...(process.env.DSH_MCP_MODEL ? { model: process.env.DSH_MCP_MODEL } : {}),
|
|
200
|
+
};
|
|
201
|
+
const settings = resolveProviderSettings(initial, { legacy: existing && !rawSettings.protocol && typeof rawSettings.baseUrl === 'string' });
|
|
202
|
+
const apiKey = parseEnv(credentials.text).DEEPSEEK_API_KEY || process.env.DEEPSEEK_API_KEY || '';
|
|
203
|
+
const chosen = await runTerminalSetup({ settings, apiKey, existing });
|
|
204
|
+
const result = await installCodex({ home, ...chosen.settings, apiKey: chosen.apiKey });
|
|
205
|
+
console.log(`Codex 配置: ${result.configPath}`);
|
|
206
|
+
console.log(`用户 skill: ${result.skillPath}`);
|
|
207
|
+
console.log(`全局委派规则: ${result.agentsPath}`);
|
|
208
|
+
console.log(`私有 API Key 文件: ${join(result.home, '.env')}`);
|
|
209
|
+
console.log(`私有服务配置: ${join(result.home, 'settings.json')}`);
|
|
210
|
+
console.log('正在检查 MCP 注册和工具握手...');
|
|
211
|
+
const health = await inspectRegistration({ configPath: result.configPath });
|
|
212
|
+
if (!health.ok) {
|
|
213
|
+
console.error('API 连接通过,配置也已保存,但 MCP 加载检查失败。');
|
|
214
|
+
for (const hint of health.hints ?? []) console.error(hint);
|
|
215
|
+
if (health.error) console.error(health.error);
|
|
216
|
+
throw new Error('请运行 dsh-mcp doctor 查看注册路径和启动状态;不能据此认为 Codex 已显示该工具。');
|
|
210
217
|
}
|
|
211
|
-
console.log(`MCP
|
|
212
|
-
console.log(
|
|
213
|
-
console.log(
|
|
214
|
-
console.log(`Private provider settings: ${join(result.home, 'settings.json')}`);
|
|
215
|
-
console.log('Start a new Codex session. Codex supplies the active project_root with every delegated task.');
|
|
216
|
-
console.log('Use normal task prompts. Codex can select eligible DeepSeek tasks without an explicit tool request.');
|
|
218
|
+
console.log(`MCP 握手成功,工具: ${health.tools.join(', ')}`);
|
|
219
|
+
console.log('如果已打开的 Codex 仍未显示,请完整退出并重新打开 Codex Desktop,而不只是新开标签。');
|
|
220
|
+
console.log('确认 Codex 与安装器使用同一主机、用户和 CODEX_HOME。Auth unsupported 对本地 stdio 是正常状态。');
|
|
217
221
|
}
|
|
218
222
|
|
|
223
|
+
|
|
219
224
|
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
|
|
220
225
|
runSetup().catch((error) => { console.error(`Setup failed: ${error.message}`); process.exitCode = 1; });
|
|
221
226
|
}
|
package/src/worker.js
CHANGED
|
@@ -2,25 +2,33 @@ import { spawn, execFile } from 'node:child_process';
|
|
|
2
2
|
import { promisify } from 'node:util';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import { createReadStream } from 'node:fs';
|
|
5
|
-
import { lstat, readlink, realpath, stat } from 'node:fs/promises';
|
|
6
|
-
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
5
|
+
import { lstat, mkdtemp, readlink, realpath, rm, stat, writeFile } from 'node:fs/promises';
|
|
6
|
+
import { isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
7
|
+
import { tmpdir } from 'node:os';
|
|
7
8
|
import { fileURLToPath } from 'node:url';
|
|
8
9
|
import { readSessionOutput } from './session-output.js';
|
|
10
|
+
import { buildProviderRuntime } from './provider.js';
|
|
11
|
+
import { StringDecoder } from 'node:string_decoder';
|
|
9
12
|
|
|
10
13
|
const exec = promisify(execFile);
|
|
11
14
|
const dsh = fileURLToPath(new URL('./lib/bin.js', import.meta.resolve('@deepseek-ai/dsh/package.json')));
|
|
12
15
|
const patch = fileURLToPath(new URL('../config/worker.patch.yml', import.meta.url));
|
|
13
16
|
const MAX_OUTPUT = 8 * 1024 * 1024;
|
|
14
|
-
const THINKING_EFFORTS = new Set(['off', 'low', 'high', 'max']);
|
|
15
17
|
|
|
16
18
|
// Wait for termination before releasing the worker; kill the process group, not just dsh.
|
|
17
|
-
export function runProcess(command, { cwd, env, input, timeoutMs, signal }) {
|
|
19
|
+
export function runProcess(command, { cwd, env, input, timeoutMs, signal, onEvent }) {
|
|
18
20
|
return new Promise((done, reject) => {
|
|
19
21
|
if (signal?.aborted) return reject(new Error('Request cancelled before launch'));
|
|
20
22
|
const child = spawn(command[0], command.slice(1), {
|
|
21
23
|
cwd, env, detached: true, stdio: ['pipe', 'pipe', 'pipe'], shell: false,
|
|
22
24
|
});
|
|
23
25
|
const chunks = [];
|
|
26
|
+
const decoder = new StringDecoder('utf8');
|
|
27
|
+
let pending = '';
|
|
28
|
+
const emitLine = (line) => {
|
|
29
|
+
if (!onEvent || !line.trim()) return;
|
|
30
|
+
try { onEvent(JSON.parse(line)); } catch { /* A display failure must not stop the worker. */ }
|
|
31
|
+
};
|
|
24
32
|
let bytes = 0;
|
|
25
33
|
let stderr = Buffer.alloc(0);
|
|
26
34
|
let failure = null;
|
|
@@ -42,7 +50,17 @@ export function runProcess(command, { cwd, env, input, timeoutMs, signal }) {
|
|
|
42
50
|
child.stdout.on('data', (chunk) => {
|
|
43
51
|
bytes += chunk.length;
|
|
44
52
|
if (bytes > MAX_OUTPUT) stop('output_limit');
|
|
45
|
-
else
|
|
53
|
+
else {
|
|
54
|
+
chunks.push(chunk);
|
|
55
|
+
if (onEvent) {
|
|
56
|
+
pending += decoder.write(chunk);
|
|
57
|
+
let newline;
|
|
58
|
+
while ((newline = pending.indexOf('\n')) !== -1) {
|
|
59
|
+
emitLine(pending.slice(0, newline));
|
|
60
|
+
pending = pending.slice(newline + 1);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
46
64
|
});
|
|
47
65
|
child.stderr.on('data', (chunk) => {
|
|
48
66
|
stderr = Buffer.concat([stderr, chunk]).subarray(-32768);
|
|
@@ -54,6 +72,7 @@ export function runProcess(command, { cwd, env, input, timeoutMs, signal }) {
|
|
|
54
72
|
});
|
|
55
73
|
child.on('exit', () => kill('SIGKILL'));
|
|
56
74
|
child.on('close', (code, exitSignal) => {
|
|
75
|
+
if (onEvent) emitLine(pending + decoder.end());
|
|
57
76
|
clearTimeout(timer);
|
|
58
77
|
clearTimeout(escalation);
|
|
59
78
|
signal?.removeEventListener('abort', cancel);
|
|
@@ -123,13 +142,20 @@ async function gitSnapshot(cwd) {
|
|
|
123
142
|
return { root, files };
|
|
124
143
|
}
|
|
125
144
|
|
|
126
|
-
export function createWorker({ home, command = [process.execPath, dsh], readSession = readSessionOutput }) {
|
|
145
|
+
export function createWorker({ home, command = [process.execPath, dsh], readSession = readSessionOutput, providerRuntime = buildProviderRuntime({}, process.env.DEEPSEEK_API_KEY) }) {
|
|
146
|
+
const allowedEfforts = new Set(providerRuntime.thinkingEfforts);
|
|
147
|
+
const secret = providerRuntime.env.DEEPSEEK_API_KEY;
|
|
148
|
+
const redact = (value) => typeof value === 'string' && secret ? value.replaceAll(secret, '[REDACTED]') : value;
|
|
149
|
+
const redactObject = (value) => JSON.parse(JSON.stringify(value, (_name, item) => redact(item)));
|
|
127
150
|
let busy = false;
|
|
128
151
|
return {
|
|
129
|
-
async delegate({ project_root, task, thinking_effort, cwd = '.', permission_mode = 'read-only', session_id, timeout_seconds = 300 }, { signal } = {}) {
|
|
152
|
+
async delegate({ project_root, task, thinking_effort, cwd = '.', permission_mode = 'read-only', session_id, timeout_seconds = 300 }, { signal, onEvent } = {}) {
|
|
130
153
|
if (busy) throw new Error('DeepSeek worker is busy; serialize tasks across all projects');
|
|
131
154
|
busy = true;
|
|
132
155
|
let before;
|
|
156
|
+
let pluginDirectory;
|
|
157
|
+
const emit = (event) => { try { onEvent?.(event); } catch { /* Progress is optional. */ } };
|
|
158
|
+
let started = false;
|
|
133
159
|
try {
|
|
134
160
|
if (typeof project_root !== 'string' || !isAbsolute(project_root)) {
|
|
135
161
|
throw new Error('project_root must be an absolute directory path');
|
|
@@ -142,11 +168,23 @@ export function createWorker({ home, command = [process.execPath, dsh], readSess
|
|
|
142
168
|
throw new Error('cwd is outside project_root');
|
|
143
169
|
}
|
|
144
170
|
if (!(await stat(directory)).isDirectory()) throw new Error('cwd must be a directory');
|
|
145
|
-
if (!
|
|
146
|
-
throw new Error(
|
|
171
|
+
if (!allowedEfforts.has(thinking_effort)) {
|
|
172
|
+
throw new Error(`thinking_effort is required and must be one of: ${providerRuntime.thinkingEfforts.join(', ')} for the selected model`);
|
|
147
173
|
}
|
|
148
174
|
before = await gitSnapshot(directory);
|
|
149
|
-
const args = ['headless', '--patch', patch
|
|
175
|
+
const args = ['headless', '--patch', patch];
|
|
176
|
+
const plugin = providerRuntime.env.DSH_MCP_PI_PLUGIN;
|
|
177
|
+
if (plugin && plugin !== '@deepseek-ai/dsh-llm-pi-ai') {
|
|
178
|
+
pluginDirectory = await mkdtemp(join(tmpdir(), 'dsh-mcp-plugin-'));
|
|
179
|
+
const pluginPatch = join(pluginDirectory, 'provider.json');
|
|
180
|
+
// Cordis permits expressions in config, but plugin identities must be literal names.
|
|
181
|
+
await writeFile(pluginPatch, JSON.stringify([
|
|
182
|
+
{ id: 'llm-pi-ai', disabled: true },
|
|
183
|
+
{ insert: [{ id: 'dsh-mcp-off-only', name: plugin }] },
|
|
184
|
+
]), { mode: 0o600 });
|
|
185
|
+
args.push('--patch', pluginPatch);
|
|
186
|
+
}
|
|
187
|
+
args.push('--json', '--permission-mode', permission_mode, '--approval-policy', 'never');
|
|
150
188
|
if (session_id) args.push('--session-id', session_id);
|
|
151
189
|
args.push('-');
|
|
152
190
|
const prompt = [
|
|
@@ -159,11 +197,17 @@ export function createWorker({ home, command = [process.execPath, dsh], readSess
|
|
|
159
197
|
'Do not wait for an interactive reply. The supervisor can continue this session with a later call.',
|
|
160
198
|
'', task,
|
|
161
199
|
].join('\n');
|
|
200
|
+
emit({ type: 'run_start', model: providerRuntime.model, permission_mode });
|
|
201
|
+
started = true;
|
|
162
202
|
const result = await runProcess([...command, ...args], {
|
|
163
203
|
cwd: directory,
|
|
164
|
-
env: { ...process.env, DSH_HOME: home, DSH_PERMISSION_MODE: permission_mode, DSH_MCP_THINKING_EFFORT: thinking_effort },
|
|
165
|
-
input: prompt, timeoutMs: timeout_seconds * 1000, signal,
|
|
204
|
+
env: { ...process.env, ...providerRuntime.env, DSH_HOME: home, DSH_PERMISSION_MODE: permission_mode, DSH_MCP_THINKING_EFFORT: thinking_effort },
|
|
205
|
+
input: prompt, timeoutMs: timeout_seconds * 1000, signal, onEvent: emit,
|
|
166
206
|
});
|
|
207
|
+
if (pluginDirectory) {
|
|
208
|
+
await rm(pluginDirectory, { recursive: true, force: true });
|
|
209
|
+
pluginDirectory = undefined;
|
|
210
|
+
}
|
|
167
211
|
let run;
|
|
168
212
|
try { run = parseRun(result.stdout, result.exit_code); }
|
|
169
213
|
catch (error) { run = { status: 'failed', session_id: null, turn: null, reason: error.message }; }
|
|
@@ -190,22 +234,29 @@ export function createWorker({ home, command = [process.execPath, dsh], readSess
|
|
|
190
234
|
.filter((name) => before.files.get(name) !== after.files.get(name)).sort();
|
|
191
235
|
} else if (before) changeWarning = 'Git repository changed during execution; inspect the workspace manually.';
|
|
192
236
|
} catch (error) { changeWarning = `Cannot inspect changes: ${error.message}`; }
|
|
237
|
+
emit({ type: 'run_end', status: result.failure ?? run.status });
|
|
238
|
+
started = false;
|
|
193
239
|
return {
|
|
194
240
|
...run,
|
|
241
|
+
reason: run.reason == null ? run.reason : redactObject(run.reason),
|
|
195
242
|
status: result.failure ?? run.status,
|
|
196
243
|
answer: output?.text ?? '',
|
|
197
244
|
output_source: output ? 'session' : null,
|
|
198
245
|
session_file: output?.session_file ?? null,
|
|
199
246
|
assistant_seq: output?.assistant_seq ?? null,
|
|
200
|
-
terminal_reason: output?.terminal_reason
|
|
201
|
-
session_output_error: sessionOutputError,
|
|
247
|
+
terminal_reason: output?.terminal_reason == null ? null : redactObject(output.terminal_reason),
|
|
248
|
+
session_output_error: redact(sessionOutputError),
|
|
202
249
|
thinking_effort,
|
|
203
|
-
project_root: root, cwd: directory, model:
|
|
250
|
+
project_root: root, cwd: directory, model: providerRuntime.model, protocol: providerRuntime.protocol, permission_mode,
|
|
204
251
|
exit_code: result.exit_code, signal: result.signal,
|
|
205
252
|
changed_files: changedFiles, git_root: before?.root ?? null,
|
|
206
|
-
change_warning: changeWarning, diagnostics: result.stderr,
|
|
253
|
+
change_warning: redact(changeWarning), diagnostics: result.failure || run.status !== 'completed' ? redact(result.stderr) : '',
|
|
207
254
|
};
|
|
208
|
-
} finally {
|
|
255
|
+
} finally {
|
|
256
|
+
if (started) emit({ type: 'run_end', status: signal?.aborted ? 'cancelled' : 'failed' });
|
|
257
|
+
try { if (pluginDirectory) await rm(pluginDirectory, { recursive: true, force: true }); }
|
|
258
|
+
finally { busy = false; }
|
|
259
|
+
}
|
|
209
260
|
},
|
|
210
261
|
};
|
|
211
262
|
}
|
package/src/setup-ui.js
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import { createServer } from 'node:http';
|
|
2
|
-
import { randomBytes } from 'node:crypto';
|
|
3
|
-
import { spawn } from 'node:child_process';
|
|
4
|
-
|
|
5
|
-
const escape = (value) => String(value).replace(/[&<>"']/g, (character) => ({
|
|
6
|
-
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
|
|
7
|
-
})[character]);
|
|
8
|
-
|
|
9
|
-
function page({ baseUrl, hasSavedKey, csrf, error = '', saved = false }) {
|
|
10
|
-
return `<!doctype html>
|
|
11
|
-
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
12
|
-
<title>DeepSeek MCP settings</title><style>
|
|
13
|
-
body{font:16px/1.5 system-ui,sans-serif;background:#f4f6f8;color:#17212b;margin:0;padding:48px 20px}
|
|
14
|
-
main{max-width:540px;margin:auto;background:white;padding:32px;border:1px solid #d9e0e7;border-radius:12px}
|
|
15
|
-
h1{font-size:24px;margin-top:0}label{display:block;font-weight:600;margin-top:24px}
|
|
16
|
-
input{box-sizing:border-box;width:100%;font:inherit;padding:10px;border:1px solid #9aa9b8;border-radius:6px;margin-top:6px}
|
|
17
|
-
small{display:block;color:#4e6071;margin-top:6px}code{overflow-wrap:anywhere}button{font:inherit;background:#165bc4;color:white;border:0;padding:11px 18px;border-radius:6px;margin-top:28px;cursor:pointer}
|
|
18
|
-
.error{color:#a21818}.scope{background:#f1f4f7;padding:12px;border-radius:6px}
|
|
19
|
-
</style></head><body><main><h1>DeepSeek MCP settings</h1>
|
|
20
|
-
${saved ? '<p>Settings saved. You can close this tab.</p><p>Start a new Codex session in any project.</p>' : `
|
|
21
|
-
<p>These settings apply to the local DeepSeek Harness worker.</p>
|
|
22
|
-
<p class="scope">Codex supplies the project directory for each task. Setup does not bind this server to a project.</p>
|
|
23
|
-
${error ? `<p class="error" role="alert">${escape(error)}</p>` : ''}
|
|
24
|
-
<form method="post" autocomplete="off">
|
|
25
|
-
<input type="hidden" name="csrf" value="${csrf}">
|
|
26
|
-
<label for="apiKey">API Key</label><input id="apiKey" name="apiKey" type="password" autocomplete="new-password" ${hasSavedKey ? 'disabled placeholder="An existing key will be used"' : 'required'}>
|
|
27
|
-
<small>The key stays in your private settings file. Setup keeps an existing key unchanged.</small>
|
|
28
|
-
<label for="baseUrl">Base URL (optional)</label><input id="baseUrl" name="baseUrl" type="url" value="${escape(baseUrl)}">
|
|
29
|
-
<small>Use a Messages API root. Leave empty to use https://api.deepseek.com/anthropic.</small>
|
|
30
|
-
<small>A custom endpoint receives your API key and task data.</small>
|
|
31
|
-
<button type="submit">Save and configure Codex</button></form>`}
|
|
32
|
-
</main></body></html>`;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export async function createSetupForm({ baseUrl, hasSavedKey = false, onSubmit }) {
|
|
36
|
-
const path = `/${randomBytes(32).toString('hex')}`;
|
|
37
|
-
const csrf = randomBytes(32).toString('hex');
|
|
38
|
-
let origin;
|
|
39
|
-
let submitting = false;
|
|
40
|
-
let closing = false;
|
|
41
|
-
let timer;
|
|
42
|
-
let accept;
|
|
43
|
-
let reject;
|
|
44
|
-
const completed = new Promise((resolve, fail) => { accept = resolve; reject = fail; });
|
|
45
|
-
const server = createServer(async (request, response) => {
|
|
46
|
-
response.setHeader('Cache-Control', 'no-store');
|
|
47
|
-
response.setHeader('Referrer-Policy', 'no-referrer');
|
|
48
|
-
response.setHeader('X-Content-Type-Options', 'nosniff');
|
|
49
|
-
response.setHeader('Content-Security-Policy', "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'");
|
|
50
|
-
if (request.headers.host !== new URL(origin).host || request.url !== path) {
|
|
51
|
-
response.writeHead(404).end('Not found.');
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
const render = (options, status = 200) => {
|
|
55
|
-
response.writeHead(status, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
56
|
-
response.end(page({ baseUrl, hasSavedKey, csrf, ...options }));
|
|
57
|
-
};
|
|
58
|
-
if (request.method === 'GET') { render({}); return; }
|
|
59
|
-
if (request.method !== 'POST') { response.writeHead(405).end('Use GET or POST.'); return; }
|
|
60
|
-
if (request.headers.origin && ![origin, 'null'].includes(request.headers.origin)) {
|
|
61
|
-
response.writeHead(403).end('Invalid origin.'); return;
|
|
62
|
-
}
|
|
63
|
-
if (request.headers['content-type']?.split(';')[0] !== 'application/x-www-form-urlencoded') {
|
|
64
|
-
response.writeHead(415).end('Use the setup form.'); return;
|
|
65
|
-
}
|
|
66
|
-
if (submitting || closing) { response.writeHead(409).end('Setup is already saving.'); return; }
|
|
67
|
-
try {
|
|
68
|
-
const chunks = [];
|
|
69
|
-
let bytes = 0;
|
|
70
|
-
for await (const chunk of request) {
|
|
71
|
-
bytes += chunk.length;
|
|
72
|
-
if (bytes > 16384) { response.writeHead(413).end('Settings are too large.'); return; }
|
|
73
|
-
chunks.push(chunk);
|
|
74
|
-
}
|
|
75
|
-
const fields = new URLSearchParams(Buffer.concat(chunks).toString('utf8'));
|
|
76
|
-
// A separate form token also protects webviews with a null or missing Origin.
|
|
77
|
-
if (fields.get('csrf') !== csrf) { response.writeHead(403).end('Invalid form token.'); return; }
|
|
78
|
-
submitting = true;
|
|
79
|
-
const result = await onSubmit({ apiKey: fields.get('apiKey')?.trim() || undefined, baseUrl: fields.get('baseUrl') });
|
|
80
|
-
response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
81
|
-
response.end(page({ baseUrl, hasSavedKey, saved: true }), () => finish(null, result));
|
|
82
|
-
} catch (error) {
|
|
83
|
-
submitting = false;
|
|
84
|
-
render({ error: error.message }, 400);
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
server.requestTimeout = 30000;
|
|
88
|
-
server.headersTimeout = 10000;
|
|
89
|
-
await new Promise((resolve, fail) => {
|
|
90
|
-
server.once('error', fail);
|
|
91
|
-
server.listen(0, '127.0.0.1', resolve);
|
|
92
|
-
});
|
|
93
|
-
origin = `http://127.0.0.1:${server.address().port}`;
|
|
94
|
-
function finish(error, result) {
|
|
95
|
-
if (closing) return;
|
|
96
|
-
closing = true;
|
|
97
|
-
clearTimeout(timer);
|
|
98
|
-
server.close(() => error ? reject(error) : accept(result));
|
|
99
|
-
if (error) server.closeAllConnections();
|
|
100
|
-
else server.closeIdleConnections();
|
|
101
|
-
}
|
|
102
|
-
timer = setTimeout(() => finish(new Error('Setup form expired. Run setup again.')), 600000);
|
|
103
|
-
return { url: origin + path, completed, close: () => finish(new Error('Setup cancelled.')) };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function openSetupBrowser(url) {
|
|
107
|
-
return new Promise((resolve) => {
|
|
108
|
-
const child = spawn(process.platform === 'darwin' ? 'open' : 'xdg-open', [url], { stdio: 'ignore', shell: false });
|
|
109
|
-
child.once('error', () => resolve(false));
|
|
110
|
-
child.once('exit', (code) => resolve(code === 0));
|
|
111
|
-
});
|
|
112
|
-
}
|