@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/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 { DEFAULT_BASE_URL, normalizeBaseUrl, parseProviderSettings } from './provider.js';
12
- import { createSetupForm, openSetupBrowser } from './setup-ui.js';
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 existingKey = parseEnv(credentials.text).DEEPSEEK_API_KEY;
152
+ const { text: nextAgents } = managedBlock(agents.text, '<!-- BEGIN dsh-mcp -->', '<!-- END dsh-mcp -->', routing);
128
153
  const providerSettings = parseProviderSettings(provider.text);
129
- const providerUrl = normalizeBaseUrl(baseUrl ?? providerSettings.baseUrl ?? DEFAULT_BASE_URL);
130
- const nextProvider = JSON.stringify({ ...providerSettings, baseUrl: providerUrl }, null, 2) + '\n';
131
- let nextCredentials = credentials.text;
132
- if (!existingKey) {
133
- const key = apiKey?.trim();
134
- if (!key || /[\r\n\0]/.test(key)) throw new Error('Provide one nonempty DeepSeek API key without line breaks.');
135
- nextCredentials += `${nextCredentials && !nextCredentials.endsWith('\n') ? '\n' : ''}DEEPSEEK_API_KEY=${JSON.stringify(key)}\n`;
136
- if (parseEnv(nextCredentials).DEEPSEEK_API_KEY !== key) throw new Error('The API key cannot be stored as a dotenv value.');
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
- return { home, configPath, skillPath };
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('Usage: dsh-mcp setup [--home /private/dsh-home] [--terminal]');
175
- console.log('Setup opens a local form for API Key and Base URL.');
176
- console.log('Use --terminal for terminal prompts. Secrets never enter command arguments.');
177
- console.log('Setup installs user-wide Codex MCP settings and a skill. Codex supplies each project path at call time.');
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 provider = parseProviderSettings((await readExisting(join(home, 'settings.json'))).text);
184
- const stored = parseEnv(credentials.text);
185
- let apiKey = process.env.DEEPSEEK_API_KEY;
186
- let baseUrl = process.env.DEEPSEEK_BASE_URL ?? provider.baseUrl ?? DEFAULT_BASE_URL;
187
- let result;
188
- if (values.terminal) {
189
- if (!stored.DEEPSEEK_API_KEY && !apiKey) apiKey = await question('DeepSeek API key (hidden): ', true);
190
- if (process.stdin.isTTY && !process.env.DEEPSEEK_BASE_URL) {
191
- baseUrl = (await question(`Base URL [${baseUrl}]: `)).trim() || baseUrl;
192
- }
193
- result = await installCodex({ home, apiKey, baseUrl });
194
- } else {
195
- const form = await createSetupForm({
196
- baseUrl, hasSavedKey: Boolean(stored.DEEPSEEK_API_KEY || apiKey),
197
- onSubmit: (input) => installCodex({ home, apiKey: apiKey || input.apiKey, baseUrl: input.baseUrl }),
198
- });
199
- const cancel = () => form.close();
200
- process.once('SIGINT', cancel);
201
- process.once('SIGTERM', cancel);
202
- try {
203
- console.log(`Setup form: ${form.url}`);
204
- if (!await openSetupBrowser(form.url)) console.log('Open the local URL in your browser to continue.');
205
- result = await form.completed;
206
- } finally {
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 configuration: ${result.configPath}`);
212
- console.log(`Skill: ${result.skillPath}`);
213
- console.log(`Private API key file: ${join(result.home, '.env')}`);
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 chunks.push(chunk);
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 (!THINKING_EFFORTS.has(thinking_effort)) {
146
- throw new Error('thinking_effort is required and must be off, low, high, or max');
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, '--json', '--permission-mode', permission_mode, '--approval-policy', 'never'];
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 ?? null,
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: 'deepseek-flash', permission_mode,
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 { busy = false; }
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
- '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
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
- }