@foxden-app/foxclaw 0.11.1 → 0.12.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/dist/dsh/ui.js ADDED
@@ -0,0 +1,342 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+ import { randomBytes } from 'node:crypto';
5
+ import { DshClient } from './client.js';
6
+ import { DshEngineAdapter } from './adapter.js';
7
+ /** DSH controls shared by the unified bot and the optional dedicated bot. */
8
+ export class DshUi {
9
+ config;
10
+ store;
11
+ logger;
12
+ messaging;
13
+ actions = new Map();
14
+ approvals = new Map();
15
+ adapter;
16
+ get pendingApprovals() { return this.approvals.size; }
17
+ constructor(config, store, logger, messaging) {
18
+ this.config = config;
19
+ this.store = store;
20
+ this.logger = logger;
21
+ this.messaging = messaging;
22
+ if (!config.dsh)
23
+ throw new Error('DSH backend is not configured');
24
+ const dsh = config.dsh;
25
+ this.adapter = new DshEngineAdapter({
26
+ createClient: scopeId => {
27
+ const client = new DshClient(dsh, scopeId, logger);
28
+ client.permissionHandler = (request, signal) => this.requestPermission(scopeId, request, signal);
29
+ client.cancelPermissions = () => {
30
+ for (const pending of this.approvals.values())
31
+ if (pending.scopeId === scopeId)
32
+ pending.resolve({ outcome: { outcome: 'cancelled' } });
33
+ };
34
+ return client;
35
+ },
36
+ preferences: scopeId => {
37
+ const binding = store.getBinding(scopeId);
38
+ const settings = store.getChatSettings(scopeId);
39
+ return { cwd: binding?.cwd || config.defaultCwd, threadId: binding?.threadId || null, model: settings?.model ?? null, effort: settings?.reasoningEffort ?? null, access: settings?.accessPreset ?? this.defaultAccess() };
40
+ },
41
+ bind: (scopeId, id, cwd) => {
42
+ store.setBinding(scopeId, id, cwd);
43
+ const settings = store.getChatSettings(scopeId);
44
+ store.setScopeBackendBinding(scopeId, 'dsh', id, cwd, { model: settings?.model ?? null, reasoningEffort: settings?.reasoningEffort ?? null, accessPreset: settings?.accessPreset ?? this.defaultAccess() });
45
+ },
46
+ });
47
+ }
48
+ defaultAccess() { return this.config.defaultSandboxMode === 'danger-full-access' ? 'full-access' : this.config.defaultSandboxMode === 'read-only' ? 'read-only' : 'default'; }
49
+ copy(locale, zh, en) { return locale === 'zh' ? zh : en; }
50
+ status(scopeId, locale) {
51
+ const settings = this.store.getChatSettings(scopeId);
52
+ return `• **${this.copy(locale, '推理档位', 'Reasoning')}**: \`${settings?.reasoningEffort ?? 'default'}\`\n• **${this.copy(locale, 'DSH 权限', 'DSH access')}**: \`${settings?.accessPreset ?? this.defaultAccess()}\`\n• **${this.copy(locale, '配置', 'Settings')}**: \`/setup\` · \`/plugins\``;
53
+ }
54
+ button(scopeId, kind, value, text, cwd) {
55
+ for (const [key, action] of this.actions)
56
+ if (action.expires < Date.now())
57
+ this.actions.delete(key);
58
+ const key = randomBytes(6).toString('hex');
59
+ this.actions.set(key, { scopeId, kind, value, ...(cwd ? { cwd } : {}), expires: Date.now() + Math.max(this.config.telegramPanelTtlMs, 300000) });
60
+ return { text, callback_data: `dsh:a:${key}` };
61
+ }
62
+ async panel(scopeId, text, keyboard, orchestrator, messageId) {
63
+ if (messageId)
64
+ await orchestrator.editMessage(scopeId, messageId, text, keyboard);
65
+ else
66
+ messageId = await orchestrator.sendMessage(scopeId, text, keyboard);
67
+ orchestrator.scheduleStalePanelDeletion(scopeId, messageId);
68
+ }
69
+ async setup(scopeId, locale, orchestrator, messageId) {
70
+ const settings = this.store.getChatSettings(scopeId);
71
+ const binding = this.store.getBinding(scopeId);
72
+ const access = settings?.accessPreset ?? this.defaultAccess();
73
+ const mode = settings?.activeTurnMessageMode ?? 'queue';
74
+ const rows = [
75
+ [{ text: this.copy(locale, '🎯 模型', '🎯 Model'), callback_data: 'dsh:models' }, { text: this.copy(locale, '🧠 推理档位', '🧠 Reasoning'), callback_data: 'dsh:efforts' }],
76
+ ['read-only', 'default', 'full-access'].map(value => this.button(scopeId, 'access', value, `${access === value ? '✓ ' : ''}${this.copy(locale, value === 'read-only' ? '只读' : value === 'default' ? '工作区写入' : '完全访问', value)}`)),
77
+ [{ text: this.copy(locale, '📁 会话', '📁 Sessions'), callback_data: 'dsh:threads' }, { text: this.copy(locale, '🧩 插件配置', '🧩 Plugins'), callback_data: 'dsh:plugins' }],
78
+ [{ text: mode === 'queue' ? this.copy(locale, '⏳ 排队', '⏳ Queue') : this.copy(locale, '⚡ 中断接管', '⚡ Interrupt & steer'), callback_data: 'engine:setup:active_mode' }],
79
+ [{ text: this.copy(locale, '✨ 新建会话', '✨ New session'), callback_data: 'engine:setup:new' }, { text: this.copy(locale, '🔌 切换后端', '🔌 Backend'), callback_data: 'engine:setup:backend' }],
80
+ [{ text: this.copy(locale, '🔄 刷新', '🔄 Refresh'), callback_data: 'engine:setup:main' }],
81
+ ];
82
+ await this.panel(scopeId, `⚙️ **DeepSeek Harness**\n\n${this.copy(locale, '模型', 'Model')}: \`${settings?.model ?? 'default'}\`\n${this.copy(locale, '推理档位', 'Reasoning')}: \`${settings?.reasoningEffort ?? 'default'}\`\n${this.copy(locale, '权限', 'Access')}: \`${access}\`\n${this.copy(locale, '会话', 'Session')}: \`${binding?.threadId || 'new'}\`\n${this.copy(locale, '工作目录', 'Directory')}: \`${binding?.cwd || this.config.defaultCwd}\``, rows, orchestrator, messageId);
83
+ }
84
+ async models(scopeId, locale, orchestrator, messageId) {
85
+ const models = await this.adapter.listModels(scopeId);
86
+ const current = this.store.getChatSettings(scopeId)?.model;
87
+ const buttons = [this.button(scopeId, 'model', '', this.copy(locale, '默认模型', 'Default model')), ...models.map(model => this.button(scopeId, 'model', model.id, `${model.id === current ? '✓ ' : ''}${model.name}`))];
88
+ const rows = [];
89
+ for (let index = 0; index < buttons.length; index += 2)
90
+ rows.push(buttons.slice(index, index + 2));
91
+ rows.push([{ text: this.copy(locale, '返回设置', 'Back'), callback_data: 'engine:setup:main' }]);
92
+ await this.panel(scopeId, this.copy(locale, '🎯 **DSH 模型**', '🎯 **DSH models**'), rows, orchestrator, messageId);
93
+ }
94
+ async efforts(scopeId, locale, orchestrator, messageId) {
95
+ const choices = await this.adapter.listEfforts(scopeId);
96
+ const rows = [[this.button(scopeId, 'effort', '', this.copy(locale, '默认档位', 'Default effort'))]];
97
+ for (const choice of choices)
98
+ rows.push([this.button(scopeId, 'effort', choice.value, choice.name)]);
99
+ rows.push([{ text: this.copy(locale, '返回设置', 'Back'), callback_data: 'engine:setup:main' }]);
100
+ await this.panel(scopeId, choices.length ? this.copy(locale, '🧠 **当前模型的推理档位**', '🧠 **Reasoning for this model**') : this.copy(locale, '当前模型未提供可切换的推理档位。', 'This model exposes no reasoning selector.'), rows, orchestrator, messageId);
101
+ }
102
+ async selectModel(scopeId, value, orchestrator) {
103
+ await this.adapter.selectModel(scopeId, value || null);
104
+ this.store.setChatModel(scopeId, value || null);
105
+ this.store.setChatEffort(scopeId, null);
106
+ this.store.setChatServiceTier(scopeId, null);
107
+ orchestrator.syncCurrentBackendSettings(scopeId);
108
+ }
109
+ async selectEffort(scopeId, value, orchestrator) {
110
+ await this.adapter.selectEffort(scopeId, value || null);
111
+ this.store.setChatEngineEffort(scopeId, value || null);
112
+ orchestrator.syncCurrentBackendSettings(scopeId);
113
+ }
114
+ async selectAccess(scopeId, value, orchestrator) {
115
+ if (value !== 'read-only' && value !== 'default' && value !== 'full-access')
116
+ throw new Error('Use read-only, default, or full-access');
117
+ if (orchestrator.hasActiveTurn(scopeId))
118
+ throw new Error('Interrupt the active turn before changing DSH permissions');
119
+ await this.adapter.setAccess(scopeId, value);
120
+ this.store.setChatAccessPreset(scopeId, value);
121
+ orchestrator.syncCurrentBackendSettings(scopeId);
122
+ }
123
+ async threads(scopeId, locale, orchestrator, messageId, cursor) {
124
+ const page = await this.adapter.listSessions(scopeId, cursor);
125
+ const rows = page.sessions.map(session => [this.button(scopeId, 'open', session.sessionId, (session.title || session.sessionId).slice(0, 60), session.cwd)]);
126
+ if (page.nextCursor)
127
+ rows.push([this.button(scopeId, 'page', page.nextCursor, this.copy(locale, '下一页', 'Next page'))]);
128
+ rows.push([{ text: this.copy(locale, '返回设置', 'Back'), callback_data: 'engine:setup:main' }]);
129
+ await this.panel(scopeId, this.copy(locale, '📁 **DSH 持久会话**', '📁 **Persistent DSH sessions**'), rows, orchestrator, messageId);
130
+ }
131
+ async open(scopeId, id, cwd, orchestrator) {
132
+ if (orchestrator.hasActiveTurn(scopeId))
133
+ throw new Error('Interrupt the active turn before opening another DSH session');
134
+ if (!cwd) {
135
+ let cursor;
136
+ do {
137
+ const page = await this.adapter.listSessions(scopeId, cursor);
138
+ cwd = page.sessions.find(session => session.sessionId === id)?.cwd;
139
+ cursor = page.nextCursor ?? undefined;
140
+ } while (!cwd && cursor);
141
+ }
142
+ if (!cwd)
143
+ throw new Error('DSH session not found');
144
+ const previous = this.store.getBinding(scopeId);
145
+ this.store.setBinding(scopeId, id, cwd);
146
+ try {
147
+ await this.adapter.sessionForScope(scopeId);
148
+ }
149
+ catch (error) {
150
+ if (previous)
151
+ this.store.setBinding(scopeId, previous.threadId, previous.cwd);
152
+ else
153
+ this.store.clearBinding(scopeId);
154
+ throw error;
155
+ }
156
+ orchestrator.syncCurrentBackendSettings(scopeId);
157
+ }
158
+ async plugins(scopeId, locale, orchestrator) {
159
+ const dsh = this.config.dsh;
160
+ const home = dsh.home || process.env.DSH_HOME || path.join(os.homedir(), '.dsh');
161
+ let dependencies = {};
162
+ try {
163
+ const manifest = JSON.parse(await fs.readFile(path.join(home, 'profiles', dsh.profile, 'package.json'), 'utf8'));
164
+ if (typeof manifest === 'object' && manifest !== null && 'dependencies' in manifest && typeof manifest.dependencies === 'object' && manifest.dependencies !== null)
165
+ dependencies = manifest.dependencies;
166
+ }
167
+ catch (error) {
168
+ this.logger.debug('dsh.plugin_manifest_unavailable', { error: String(error) });
169
+ }
170
+ await orchestrator.sendMessage(scopeId, `🧩 **DSH ${this.copy(locale, '插件配置', 'plugin configuration')}**\n\nProfile: \`${dsh.profile}\`\n${Object.entries(dependencies).map(([name, version]) => `• ${name}: ${String(version)}`).join('\n') || this.copy(locale, 'Profile 尚未初始化或没有显式依赖。', 'Profile is not initialized or has no explicit dependencies.')}\n• foxclaw-permissions\n\n${this.copy(locale, '已配置补丁', 'Configured patches')}:\n${dsh.patches.map(patch => `• ${patch}`).join('\n') || '—'}\n\n${this.copy(locale, '安装/移除插件请在终端执行,随后重启 foxclaw', 'Install/remove plugins in a terminal, then restart foxclaw')}:\n\`dsh plugin --profile ${dsh.profile} add <package>\`\n\`dsh plugin --profile ${dsh.profile} remove <package>\``);
171
+ }
172
+ async command(scopeId, command, args, locale, orchestrator) {
173
+ try {
174
+ switch (command) {
175
+ case 'setup':
176
+ await this.setup(scopeId, locale, orchestrator);
177
+ return true;
178
+ case 'models':
179
+ await this.models(scopeId, locale, orchestrator);
180
+ return true;
181
+ case 'model':
182
+ if (args) {
183
+ await this.selectModel(scopeId, args === 'default' ? '' : args, orchestrator);
184
+ await this.setup(scopeId, locale, orchestrator);
185
+ }
186
+ else
187
+ await this.models(scopeId, locale, orchestrator);
188
+ return true;
189
+ case 'effort':
190
+ if (args) {
191
+ await this.selectEffort(scopeId, args === 'default' ? '' : args, orchestrator);
192
+ await this.setup(scopeId, locale, orchestrator);
193
+ }
194
+ else
195
+ await this.efforts(scopeId, locale, orchestrator);
196
+ return true;
197
+ case 'access':
198
+ case 'permissions':
199
+ if (args)
200
+ await this.selectAccess(scopeId, args, orchestrator);
201
+ await this.setup(scopeId, locale, orchestrator);
202
+ return true;
203
+ case 'threads':
204
+ await this.threads(scopeId, locale, orchestrator);
205
+ return true;
206
+ case 'open':
207
+ if (args) {
208
+ await this.open(scopeId, args, undefined, orchestrator);
209
+ await this.setup(scopeId, locale, orchestrator);
210
+ }
211
+ else
212
+ await this.threads(scopeId, locale, orchestrator);
213
+ return true;
214
+ case 'plugins':
215
+ await this.plugins(scopeId, locale, orchestrator);
216
+ return true;
217
+ case 'where':
218
+ case 'config':
219
+ await this.setup(scopeId, locale, orchestrator);
220
+ return true;
221
+ case 'update':
222
+ await orchestrator.sendMessage(scopeId, this.copy(locale, '请在终端执行 `foxclaw update` 升级此独立 DSH Bot。', 'Run `foxclaw update` in a terminal to upgrade this dedicated DSH bot.'));
223
+ return true;
224
+ case 'help':
225
+ case 'start':
226
+ await orchestrator.sendMessage(scopeId, this.copy(locale, '**DSH 命令**\n/setup · 设置\n/models · 模型\n/model <provider/model|default>\n/effort [档位|default]\n/permissions [read-only|default|full-access]\n/threads · 持久会话\n/open <会话 ID>\n/new [目录] · 新建\n/interrupt · 中断\n/active <queue|steer>\n/plugins · 插件配置\n/backend · 切换后端\n/status · 状态', '**DSH commands**\n/setup · Settings\n/models · Models\n/model <provider/model|default>\n/effort [level|default]\n/permissions [read-only|default|full-access]\n/threads · Persistent sessions\n/open <session ID>\n/new [directory]\n/interrupt\n/active <queue|steer>\n/plugins · Plugin configuration\n/backend\n/status'));
227
+ return true;
228
+ case 'backend':
229
+ case 'backends':
230
+ case 'engine':
231
+ case 'engines':
232
+ case 'status':
233
+ case 'active':
234
+ case 'queue':
235
+ case 'steer':
236
+ case 'interrupt':
237
+ case 'stop':
238
+ case 'abort':
239
+ case 'new':
240
+ case 'clear': return false;
241
+ default:
242
+ await orchestrator.sendMessage(scopeId, this.copy(locale, `DSH 的 ACP 接口未提供 /${command}。发送 /help 查看可用操作。`, `DSH ACP does not expose /${command}. Send /help for available commands.`));
243
+ return true;
244
+ }
245
+ }
246
+ catch (error) {
247
+ await orchestrator.sendMessage(scopeId, `⚠️ ${error instanceof Error ? error.message : String(error)}`);
248
+ return true;
249
+ }
250
+ }
251
+ async callback(scopeId, data, locale, orchestrator, event) {
252
+ const answer = (text = '') => this.messaging.answerCallback(event?.callbackQueryId ?? '', text);
253
+ if (!data.startsWith('dsh:')) {
254
+ if (orchestrator.getBackendDescriptorForScope(scopeId).engineType === 'dsh' && data.startsWith('engine:') && !['engine:setup:main', 'engine:setup:active_mode', 'engine:setup:new', 'engine:setup:backend'].includes(data) && !data.startsWith('engine:backend:')) {
255
+ await answer(this.copy(locale, '请重新打开 DSH 设置', 'Open DSH settings again'));
256
+ return true;
257
+ }
258
+ return false;
259
+ }
260
+ if (data.startsWith('dsh:p:')) {
261
+ const [, , key, index] = data.split(':');
262
+ const pending = this.approvals.get(key);
263
+ const option = pending?.options[Number(index)];
264
+ if (!pending || pending.scopeId !== scopeId || !option) {
265
+ await answer(this.copy(locale, '审批已失效', 'Approval expired'));
266
+ return true;
267
+ }
268
+ pending.resolve({ outcome: { outcome: 'selected', optionId: option.optionId } });
269
+ await answer(this.copy(locale, '已提交', 'Submitted'));
270
+ return true;
271
+ }
272
+ if (orchestrator.getBackendDescriptorForScope(scopeId).engineType !== 'dsh') {
273
+ await answer(this.copy(locale, '请先切换到 DSH', 'Switch to DSH first'));
274
+ return true;
275
+ }
276
+ await answer();
277
+ try {
278
+ if (data === 'dsh:models')
279
+ await this.models(scopeId, locale, orchestrator, event?.messageId);
280
+ else if (data === 'dsh:efforts')
281
+ await this.efforts(scopeId, locale, orchestrator, event?.messageId);
282
+ else if (data === 'dsh:threads')
283
+ await this.threads(scopeId, locale, orchestrator, event?.messageId);
284
+ else if (data === 'dsh:plugins')
285
+ await this.plugins(scopeId, locale, orchestrator);
286
+ else if (data.startsWith('dsh:a:')) {
287
+ const action = this.actions.get(data.slice(6));
288
+ if (!action || action.scopeId !== scopeId || action.expires < Date.now())
289
+ throw new Error(this.copy(locale, '面板已失效,请重新打开 /setup', 'Panel expired; open /setup again'));
290
+ if (action.kind === 'model')
291
+ await this.selectModel(scopeId, action.value, orchestrator);
292
+ else if (action.kind === 'effort')
293
+ await this.selectEffort(scopeId, action.value, orchestrator);
294
+ else if (action.kind === 'access')
295
+ await this.selectAccess(scopeId, action.value, orchestrator);
296
+ else if (action.kind === 'open')
297
+ await this.open(scopeId, action.value, action.cwd, orchestrator);
298
+ else if (action.kind === 'page') {
299
+ await this.threads(scopeId, locale, orchestrator, event?.messageId, action.value);
300
+ return true;
301
+ }
302
+ await this.setup(scopeId, locale, orchestrator, event?.messageId);
303
+ }
304
+ }
305
+ catch (error) {
306
+ await orchestrator.sendMessage(scopeId, `⚠️ ${error instanceof Error ? error.message : String(error)}`);
307
+ }
308
+ return true;
309
+ }
310
+ async requestPermission(scopeId, request, signal) {
311
+ if (signal.aborted)
312
+ return { outcome: { outcome: 'cancelled' } };
313
+ const key = randomBytes(6).toString('hex');
314
+ const locale = this.store.getChatSettings(scopeId)?.locale ?? 'zh';
315
+ let settle;
316
+ const result = new Promise(resolve => { settle = resolve; });
317
+ const pending = { scopeId, resolve: settle, options: request.options, messageId: null };
318
+ this.approvals.set(key, pending);
319
+ const abort = () => settle({ outcome: { outcome: 'cancelled' } });
320
+ signal.addEventListener('abort', abort, { once: true });
321
+ try {
322
+ pending.messageId = await this.messaging.sendRichMarkdown(scopeId, `${this.copy(locale, '🔐 **DSH 请求审批**', '🔐 **DSH permission request**')}\n\`${request.toolCall.title || request.toolCall.toolCallId}\``, [request.options.map((option, index) => ({ text: option.kind === 'allow_once' ? this.copy(locale, '允许本次', 'Allow once') : option.kind === 'reject_once' ? this.copy(locale, '拒绝', 'Reject') : option.name, callback_data: `dsh:p:${key}:${index}` }))]);
323
+ return await result;
324
+ }
325
+ catch (error) {
326
+ this.logger.warn('dsh.approval_delivery_failed', { error: String(error) });
327
+ return { outcome: { outcome: 'cancelled' } };
328
+ }
329
+ finally {
330
+ this.approvals.delete(key);
331
+ signal.removeEventListener('abort', abort);
332
+ if (pending.messageId)
333
+ await this.messaging.editRichMarkdown(scopeId, pending.messageId, this.copy(locale, '🔐 DSH 审批已结束。', '🔐 DSH permission request resolved.'), []).catch(() => { });
334
+ }
335
+ }
336
+ async stop() {
337
+ for (const pending of this.approvals.values())
338
+ pending.resolve({ outcome: { outcome: 'cancelled' } });
339
+ this.actions.clear();
340
+ await this.adapter.stop();
341
+ }
342
+ }
package/dist/i18n.d.ts CHANGED
@@ -1499,4 +1499,8 @@ export declare function getAntigravityTelegramCommands(locale: AppLocale): Array
1499
1499
  command: string;
1500
1500
  description: string;
1501
1501
  }>;
1502
+ export declare function getDshTelegramCommands(locale: AppLocale): Array<{
1503
+ command: string;
1504
+ description: string;
1505
+ }>;
1502
1506
  export {};
package/dist/i18n.js CHANGED
@@ -1627,3 +1627,15 @@ export function getAntigravityTelegramCommands(locale) {
1627
1627
  { command: 'interrupt', description: 'Stop active turn' },
1628
1628
  ];
1629
1629
  }
1630
+ export function getDshTelegramCommands(locale) {
1631
+ const descriptions = locale === 'zh' ? {
1632
+ help: 'DSH 命令', setup: 'DSH 设置', backend: '切换后端', new: '新建会话', threads: '持久会话列表', open: '打开会话',
1633
+ models: '模型列表', model: '选择模型', effort: '推理档位', permissions: '权限设置', plugins: '插件配置',
1634
+ active: '插话或排队', queue: '排队下一轮', steer: '中断并接管', status: '运行状态', interrupt: '中断任务',
1635
+ } : {
1636
+ help: 'DSH commands', setup: 'DSH settings', backend: 'Switch backend', new: 'New session', threads: 'Persistent sessions', open: 'Open session',
1637
+ models: 'Models', model: 'Select model', effort: 'Reasoning effort', permissions: 'Permissions', plugins: 'Plugin configuration',
1638
+ active: 'Steer or queue', queue: 'Queue next turn', steer: 'Interrupt and steer', status: 'Runtime status', interrupt: 'Stop task',
1639
+ };
1640
+ return Object.entries(descriptions).map(([command, description]) => ({ command, description }));
1641
+ }
package/dist/main.js CHANGED
@@ -349,7 +349,7 @@ async function runSendVoiceCli() {
349
349
  throw new Error('Telegram voice files must be 50MB or smaller.');
350
350
  }
351
351
  const inferredBotId = parsed.botId ?? inferTelegramBotId(process.env.CODEX_HOME) ?? inferTelegramBotId(config.codexHome);
352
- const allTokens = [...config.codexBotTokens, ...config.antigravityBotTokens];
352
+ const allTokens = [...config.codexBotTokens, ...config.antigravityBotTokens, ...(config.dshBotToken ? [config.dshBotToken] : [])];
353
353
  const { botId, botToken } = resolveTelegramVoiceTarget(allTokens, inferredBotId);
354
354
  const { BridgeStore } = await import('./store/database.js');
355
355
  const store = new BridgeStore(config.storePath);
@@ -395,7 +395,7 @@ async function runSendMediaCli() {
395
395
  }
396
396
  const plan = planTelegramOutboundMedia(filePath);
397
397
  const inferredBotId = parsed.botId ?? inferTelegramBotId(process.env.CODEX_HOME) ?? inferTelegramBotId(config.codexHome);
398
- const allTokens = [...config.codexBotTokens, ...config.antigravityBotTokens];
398
+ const allTokens = [...config.codexBotTokens, ...config.antigravityBotTokens, ...(config.dshBotToken ? [config.dshBotToken] : [])];
399
399
  const { botId, botToken } = resolveTelegramVoiceTarget(allTokens, inferredBotId);
400
400
  const { BridgeStore } = await import('./store/database.js');
401
401
  const store = new BridgeStore(config.storePath);
@@ -605,7 +605,7 @@ function truncateStatusLine(value, maxLength) {
605
605
  return normalized.length <= maxLength ? normalized : `${normalized.slice(0, Math.max(0, maxLength - 1))}…`;
606
606
  }
607
607
  async function runServeCli() {
608
- const [{ BridgeMessagingRouter }, { TelegramMessagingPort }, { WeixinChannelAdapter }, { WeixinMessagingPort }, { attachIlinkRuntimeFromBridgeLogger }, { loadWeixinAccount }, { Logger }, { BridgeStore }, { TelegramGateway }, { CodexAppClient }, { BridgeSessionCore }, { TelegramChannelAdapter }, { AuthCandidateMirror }, { CrossNodeAuthSync }, { OpencodeTelegramRuntime }, { UnifiedBridgeCore }, { AntigravityAppClient }, { AntigravityAuthManager, linkAntigravityAuthTokens },] = await Promise.all([
608
+ const [{ BridgeMessagingRouter }, { TelegramMessagingPort }, { WeixinChannelAdapter }, { WeixinMessagingPort }, { attachIlinkRuntimeFromBridgeLogger }, { loadWeixinAccount }, { Logger }, { BridgeStore }, { TelegramGateway }, { CodexAppClient }, { BridgeSessionCore }, { TelegramChannelAdapter }, { AuthCandidateMirror }, { CrossNodeAuthSync }, { OpencodeTelegramRuntime }, { DshTelegramRuntime }, { UnifiedBridgeCore }, { AntigravityAppClient }, { AntigravityAuthManager, linkAntigravityAuthTokens },] = await Promise.all([
609
609
  import('./channels/bridge_messaging_router.js'),
610
610
  import('./channels/telegram/telegram_messaging_port.js'),
611
611
  import('./channels/weixin/weixin_channel_adapter.js'),
@@ -621,6 +621,7 @@ async function runServeCli() {
621
621
  import('./auth/mirror.js'),
622
622
  import('./auth/cross_node_sync.js'),
623
623
  import('./opencode/runtime.js'),
624
+ import('./dsh/runtime.js'),
624
625
  import('./antigravity/controller.js'),
625
626
  import('./antigravity/client.js'),
626
627
  import('./antigravity/auth.js'),
@@ -639,11 +640,47 @@ async function runServeCli() {
639
640
  let activeAuthMirror = null;
640
641
  let activeAuthSync = null;
641
642
  let activeOpencodeRuntime = null;
643
+ let activeDshRuntime = null;
642
644
  let sharedCodexApp = null;
643
645
  let sharedAntigravityApp = null;
644
646
  let sharedAntigravityAuth = null;
645
647
  try {
646
648
  store = new BridgeStore(config.storePath);
649
+ if (config.dshBotToken) {
650
+ activeDshRuntime = new DshTelegramRuntime(config, store, logger);
651
+ await activeDshRuntime.start();
652
+ logger.info('dsh.bridge.started', activeDshRuntime.getRuntimeStatus());
653
+ }
654
+ if (config.opencodeBotToken) {
655
+ activeOpencodeRuntime = new OpencodeTelegramRuntime(config, store, logger);
656
+ await activeOpencodeRuntime.start();
657
+ logger.info('opencode.bridge.started', activeOpencodeRuntime.getRuntimeStatus());
658
+ }
659
+ if (config.dshBotToken && config.codexBotTokens.length === 0 && config.antigravityBotTokens.length === 0) {
660
+ const writeDshStatus = (running = true) => {
661
+ const status = activeDshRuntime.getRuntimeStatus();
662
+ writeRuntimeStatus(config.statusPath, {
663
+ running, connected: running && status.connected, userAgent: 'DSH ACP', botUsername: status.username,
664
+ currentBindings: store.countBindings(), pendingApprovals: status.pendingApprovals, pendingUserInputs: 0, queuedTurns: store.countQueuedTurnInputs(),
665
+ activeTurns: running ? status.activeTurns + (activeOpencodeRuntime?.getRuntimeStatus().activeTurns ?? 0) : 0, lastError: null, updatedAt: new Date().toISOString(),
666
+ channels: { telegram: running, weixin: false }, bots: [{ ...status, connected: running && status.connected }, ...(activeOpencodeRuntime ? [{ id: 'opencode', username: activeOpencodeRuntime.getRuntimeStatus().botUsername, connected: running && activeOpencodeRuntime.getRuntimeStatus().connected, activeTurns: activeOpencodeRuntime.getRuntimeStatus().activeTurns }] : [])],
667
+ });
668
+ };
669
+ writeDshStatus();
670
+ const timer = setInterval(writeDshStatus, 5000);
671
+ const shutdown = async () => {
672
+ clearInterval(timer);
673
+ await activeDshRuntime.stop();
674
+ await activeOpencodeRuntime?.stop();
675
+ writeDshStatus(false);
676
+ store.close();
677
+ processLock.release();
678
+ process.exit(0);
679
+ };
680
+ process.on('SIGINT', () => void shutdown());
681
+ process.on('SIGTERM', () => void shutdown());
682
+ return;
683
+ }
647
684
  const selfUpdater = createSelfUpdateRuntime({
648
685
  entryPoint,
649
686
  nodePath: process.execPath,
@@ -658,12 +695,7 @@ async function runServeCli() {
658
695
  if (config.codexCliBin) {
659
696
  sharedCodexApp = new CodexAppClient(config.codexCliBin, config.codexAppLaunchCmd, config.codexAppAutolaunch, config.codexAppServerStatePath, config.codexAppServerLogPath, logger, config.codexHome ? { CODEX_HOME: config.codexHome } : null, codexApiProviderOverrides);
660
697
  }
661
- if (config.opencodeBotToken) {
662
- activeOpencodeRuntime = new OpencodeTelegramRuntime(config, store, logger);
663
- await activeOpencodeRuntime.start();
664
- logger.info('opencode.bridge.started', activeOpencodeRuntime.getRuntimeStatus());
665
- }
666
- const totalBots = config.codexBotTokens.length + config.antigravityBotTokens.length;
698
+ const totalBots = config.codexBotTokens.length + config.antigravityBotTokens.length + (config.dshBotToken ? 1 : 0);
667
699
  const namespacedScopes = totalBots > 1;
668
700
  const seeds = [];
669
701
  const canonicalAuthDir = config.codexAuthDir ?? config.codexHome ?? path.join(os.homedir(), '.codex');
@@ -809,27 +841,27 @@ async function runServeCli() {
809
841
  ...(first?.codexAppServer ? { codexAppServer: first.codexAppServer } : {}),
810
842
  botUsername: first?.botUsername ?? null,
811
843
  currentBindings: store.countBindings(),
812
- pendingApprovals: store.countPendingApprovals(),
844
+ pendingApprovals: store.countPendingApprovals() + statuses.reduce((sum, status) => sum + status.dshPendingApprovals, 0) + (activeDshRuntime?.getRuntimeStatus().pendingApprovals ?? 0),
813
845
  pendingUserInputs: store.countPendingUserInputs(),
814
846
  queuedTurns: store.countQueuedTurnInputs(),
815
847
  activeTurns: statuses.reduce((sum, status) => sum + status.activeTurns, 0)
816
- + (weixinStatus?.activeTurns ?? 0),
848
+ + (weixinStatus?.activeTurns ?? 0) + (activeDshRuntime?.getRuntimeStatus().activeTurns ?? 0),
817
849
  lastError: statuses.find((status) => status.lastError)?.lastError
818
850
  ?? weixinStatus?.lastError
819
851
  ?? null,
820
852
  updatedAt: new Date().toISOString(),
821
853
  channels: { telegram: running, weixin: running && config.wxEnabled },
822
- bots: runtimes.map((runtime, index) => ({
823
- id: runtime.id,
824
- username: statuses[index]?.botUsername ?? runtime.bot.username,
825
- connected: running && Boolean(statuses[index]?.connected),
826
- activeTurns: running ? (statuses[index]?.activeTurns ?? 0) : 0,
827
- runtimeKind: runtime.sharedDefaultRuntime ? 'default' : 'isolated',
828
- defaultBackend: runtime.defaultBackend,
829
- codexHome: statuses[index]?.codexHome ?? runtime.home,
830
- antigravityHome: runtime.antigravityHome,
831
- ...(statuses[index]?.codexAppServer ? { codexAppServer: statuses[index].codexAppServer } : {}),
832
- })),
854
+ bots: [...runtimes.map((runtime, index) => ({
855
+ id: runtime.id,
856
+ username: statuses[index]?.botUsername ?? runtime.bot.username,
857
+ connected: running && Boolean(statuses[index]?.connected),
858
+ activeTurns: running ? (statuses[index]?.activeTurns ?? 0) : 0,
859
+ runtimeKind: runtime.sharedDefaultRuntime ? 'default' : 'isolated',
860
+ defaultBackend: runtime.defaultBackend,
861
+ codexHome: statuses[index]?.codexHome ?? runtime.home,
862
+ antigravityHome: runtime.antigravityHome,
863
+ ...(statuses[index]?.codexAppServer ? { codexAppServer: statuses[index].codexAppServer } : {}),
864
+ })), ...(activeDshRuntime ? [activeDshRuntime.getRuntimeStatus()] : [])],
833
865
  ...(weixinStatus ? {
834
866
  weixinRuntime: {
835
867
  connected: running && weixinStatus.connected,
@@ -844,6 +876,7 @@ async function runServeCli() {
844
876
  });
845
877
  };
846
878
  const authSyncLocalIdle = () => runtimes.every((runtime) => runtime.core.isIdleForServiceUpdate())
879
+ && (activeDshRuntime?.getRuntimeStatus().activeTurns ?? 0) === 0
847
880
  && (!activeWeixinCore || activeWeixinCore.isIdleForServiceUpdate())
848
881
  && mirror.isIdle();
849
882
  const clusterUpdateScheduler = createClusterUpdateScheduler({
@@ -896,19 +929,19 @@ async function runServeCli() {
896
929
  statusUpdated: () => writeAggregateStatus(),
897
930
  getServiceStatus: async () => ({
898
931
  currentVersion: readPackageVersion(),
899
- bots: await Promise.all(runtimes.map(async (runtime) => {
900
- const status = runtime.core.getRuntimeStatus();
901
- return {
902
- id: runtime.id,
903
- username: status.botUsername ?? runtime.bot.username,
904
- connected: status.connected,
905
- activeTurns: status.activeTurns,
906
- runtimeKind: runtime.sharedDefaultRuntime ? 'default' : 'isolated',
907
- currentAuth: await runtime.core.getCurrentAuthLabel().catch(() => null),
908
- codexHome: status.codexHome ?? runtime.home,
909
- ...(status.codexAppServer ? { codexAppServer: status.codexAppServer } : {}),
910
- };
911
- })),
932
+ bots: [...await Promise.all(runtimes.map(async (runtime) => {
933
+ const status = runtime.core.getRuntimeStatus();
934
+ return {
935
+ id: runtime.id,
936
+ username: status.botUsername ?? runtime.bot.username,
937
+ connected: status.connected,
938
+ activeTurns: status.activeTurns,
939
+ runtimeKind: runtime.sharedDefaultRuntime ? 'default' : 'isolated',
940
+ currentAuth: await runtime.core.getCurrentAuthLabel().catch(() => null),
941
+ codexHome: status.codexHome ?? runtime.home,
942
+ ...(status.codexAppServer ? { codexAppServer: status.codexAppServer } : {}),
943
+ };
944
+ })), ...(activeDshRuntime ? [activeDshRuntime.getRuntimeStatus()] : [])],
912
945
  ...(activeWeixinCore ? {
913
946
  weixinRuntime: {
914
947
  connected: activeWeixinCore.getRuntimeStatus().connected,
@@ -1027,6 +1060,7 @@ async function runServeCli() {
1027
1060
  await activeWeixinCore?.stop();
1028
1061
  await Promise.all(runtimes.map((runtime) => runtime.telegram.stop()));
1029
1062
  await activeOpencodeRuntime?.stop();
1063
+ await activeDshRuntime?.stop();
1030
1064
  writeAggregateStatus(false);
1031
1065
  const allManaged = [...managedApps, ...(sharedCodexApp ? [sharedCodexApp] : [])];
1032
1066
  await Promise.all(allManaged.map((app) => app.stop({ terminateServer: true }).catch((error) => {
@@ -1047,6 +1081,7 @@ async function runServeCli() {
1047
1081
  await activeWeixinCore?.stop().catch(() => { });
1048
1082
  await Promise.allSettled(activeTelegramAdapters.map((adapter) => adapter.stop()));
1049
1083
  await activeOpencodeRuntime?.stop().catch(() => { });
1084
+ await activeDshRuntime?.stop().catch(() => { });
1050
1085
  await Promise.allSettled(managedApps.map((app) => app.stop({ terminateServer: true })));
1051
1086
  store?.close();
1052
1087
  processLock.release();
@@ -1537,10 +1572,23 @@ async function configureEnvInteractively(envPath, existed) {
1537
1572
  else {
1538
1573
  skipped.push('TG_ALLOWED_USER_ID');
1539
1574
  }
1575
+ const dshToken = sanitizeEnvInput(await rl.question('DeepSeek Harness Telegram bot token (DSH_BOT_TOKEN) [optional]: '));
1576
+ if (dshToken) {
1577
+ updates.DSH_BOT_TOKEN = dshToken;
1578
+ if (!codexTokens && !existed) {
1579
+ updates.CODEX_BOT_TOKENS = '';
1580
+ skipped.splice(skipped.indexOf('CODEX_BOT_TOKENS'), 1);
1581
+ }
1582
+ const dshSource = sanitizeEnvInput(await rl.question('DeepSeek Harness source directory (DSH_SOURCE_DIR) [Enter to use installed dsh]: '));
1583
+ if (dshSource)
1584
+ updates.DSH_SOURCE_DIR = normalizeUserPath(dshSource);
1585
+ if (!/^\d+:[A-Za-z0-9_-]+$/.test(dshToken))
1586
+ warnings.push('DSH_BOT_TOKEN does not look like a standard Telegram bot token.');
1587
+ }
1540
1588
  const cwdDefault = defaultInitCwd();
1541
1589
  const cwdPrompt = cwdDefault
1542
- ? `Default Codex workspace (DEFAULT_CWD) [${cwdDefault}]: `
1543
- : 'Default Codex workspace (DEFAULT_CWD): ';
1590
+ ? `Default workspace (DEFAULT_CWD) [${cwdDefault}]: `
1591
+ : 'Default workspace (DEFAULT_CWD): ';
1544
1592
  const cwdAnswer = sanitizeEnvInput(await rl.question(cwdPrompt));
1545
1593
  const cwd = cwdAnswer ? normalizeUserPath(cwdAnswer) : cwdDefault;
1546
1594
  if (cwd) {
@@ -1741,12 +1789,24 @@ function runDoctorChecks() {
1741
1789
  ...(process.env.ANTIGRAVITY_BOT_TOKENS ?? '').split(','),
1742
1790
  process.env.ANTIGRAVITY_BOT_TOKEN ?? '',
1743
1791
  ].map((value) => value.trim()).filter(Boolean);
1792
+ const dshToken = process.env.DSH_BOT_TOKEN?.trim();
1793
+ const dshFlag = process.env.DSH_ENABLED;
1794
+ const dshEnabled = dshFlag ? dshFlag !== 'false' && dshFlag !== '0' : Boolean(dshToken || process.env.DSH_SOURCE_DIR?.trim() || process.env.DSH_CLI_BIN?.trim());
1744
1795
  const checks = [
1745
1796
  ['node >= 24', Number(process.versions.node.split('.')[0]) >= 24],
1746
- ['codex cli available', hasConfiguredCommand(configuredCodexBin, 'codex')],
1747
- ['telegram bot token(s) configured', codexTokens.length > 0 || antigravityTokens.length > 0],
1797
+ ['telegram bot token(s) configured', codexTokens.length > 0 || antigravityTokens.length > 0 || Boolean(dshToken)],
1748
1798
  ['telegram allowed user configured', Boolean(process.env.TG_ALLOWED_USER_ID)],
1749
1799
  ];
1800
+ if (codexTokens.length > 0 || process.env.WX_ENABLED === 'true' || process.env.WX_ENABLED === '1')
1801
+ checks.push(['codex cli available', hasConfiguredCommand(configuredCodexBin, 'codex')]);
1802
+ if (dshEnabled) {
1803
+ const source = process.env.DSH_SOURCE_DIR?.trim();
1804
+ checks.push(['DSH CLI available', source ? fs.existsSync(path.join(path.resolve(source), 'apps/cli/src/bin.ts')) : hasConfiguredCommand(process.env.DSH_CLI_BIN, 'dsh')]);
1805
+ if (dshToken)
1806
+ checks.push(['DSH uses an independent Telegram bot', ![...codexTokens, ...antigravityTokens, process.env.OPENCODE_BOT_TOKEN?.trim()].includes(dshToken)]);
1807
+ }
1808
+ if (dshToken)
1809
+ checks.push(['DSH backend enabled for its Telegram bot', dshEnabled]);
1750
1810
  if (process.env.OPENCODE_BOT_TOKEN?.trim()) {
1751
1811
  const configuredOpencodeBin = process.env.OPENCODE_CLI_BIN;
1752
1812
  checks.push(['opencode cli available', hasConfiguredCommand(configuredOpencodeBin, 'opencode')]);
@@ -74,6 +74,8 @@ export declare class BridgeStore {
74
74
  setChatSettings(chatId: string, model: string | null, reasoningEffort: ReasoningEffortValue | null, locale?: AppLocale | null): void;
75
75
  setChatModel(chatId: string, model: string | null): void;
76
76
  setChatEffort(chatId: string, effort: ReasoningEffortValue | null): void;
77
+ /** Persist an adapter-advertised reasoning id without imposing Codex's vocabulary. */
78
+ setChatEngineEffort(chatId: string, effort: string | null): void;
77
79
  setChatLocale(chatId: string, locale: AppLocale): void;
78
80
  setChatAccessPreset(chatId: string, accessPreset: AccessPresetValue | null): void;
79
81
  setChatCollaborationMode(chatId: string, collaborationMode: CollaborationModeValue | null): void;
@@ -140,7 +142,7 @@ export declare class BridgeStore {
140
142
  countQueuedTurnInputs(scopeId?: string): number;
141
143
  updateQueuedTurnInputStatus(queueId: string, status: QueuedTurnInputStatus, error?: string | null): void;
142
144
  cancelQueuedTurnInputs(scopeId: string): number;
143
- requeueInterruptedQueuedTurnInputs(): number;
145
+ requeueInterruptedQueuedTurnInputs(scopeId?: string): number;
144
146
  savePendingAttachmentBatch(record: PendingAttachmentBatchRecord): void;
145
147
  getPendingAttachmentBatch(batchId: string): PendingAttachmentBatchRecord | null;
146
148
  findPendingAttachmentBatchByMediaGroup(scopeId: string, mediaGroupId: string): PendingAttachmentBatchRecord | null;