dsh-tg-bridge 1.1.0 → 1.1.1

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/bridge.mjs CHANGED
@@ -800,7 +800,7 @@ async function startProgress(chatId, threadId, lang = 'ru') {
800
800
  // Stop is available for as long as the turn runs; the keyboard is cleared
801
801
  // when the status turns into the final line, so a stale button cannot be
802
802
  // pressed after the work is over.
803
- reply_markup: stopKeyboard(sessionKey(chatId, threadId)),
803
+ reply_markup: stopKeyboard(sessionKey(chatId, threadId), lang),
804
804
  ...(threadId > 0 ? { message_thread_id: threadId } : {}),
805
805
  });
806
806
  messageId = res.message_id;
@@ -878,8 +878,10 @@ async function startProgress(chatId, threadId, lang = 'ru') {
878
878
  if (result?.error) {
879
879
  // A failed turn leaves the status message as the error report and swaps
880
880
  // «Стоп» for «Повторить»: the person's next move is a retry, not a stop.
881
- const text = `⚠️ Ход не удался за ${Math.round((Date.now() - started) / 1000)} с\n${String(result.error).slice(0, 1200)}`;
882
- await edit(text, errorKeyboard(sessionKey(chatId, threadId)));
881
+ const failedFor = Math.round((Date.now() - started) / 1000);
882
+ const unit = lang === 'en' ? 's' : lang === 'zh' ? '秒' : 'с';
883
+ const text = `⚠️ ${t(lang, 'status.failed')} · ${failedFor} ${unit}\n${String(result.error).slice(0, 1200)}`;
884
+ await edit(text, errorKeyboard(sessionKey(chatId, threadId), lang));
883
885
  return true;
884
886
  }
885
887
  await edit(body(true), null);
@@ -1340,23 +1342,23 @@ const lastPrompt = new Map();
1340
1342
  const approvals = new Map();
1341
1343
  let approvalSeq = 0;
1342
1344
 
1343
- function stopKeyboard(key) {
1345
+ function stopKeyboard(key, lang = 'ru') {
1344
1346
  return {
1345
1347
  inline_keyboard: [
1346
1348
  [
1347
- { text: '⏹ Стоп', callback_data: 'stop' },
1348
- { text: '📄 Логи', callback_data: 'logs' },
1349
+ { text: `⏹ ${t(lang, 'btn.stop')}`, callback_data: 'stop' },
1350
+ { text: `📄 ${t(lang, 'btn.logs')}`, callback_data: 'logs' },
1349
1351
  ],
1350
1352
  ],
1351
1353
  };
1352
1354
  }
1353
1355
 
1354
- function errorKeyboard(key) {
1356
+ function errorKeyboard(key, lang = 'ru') {
1355
1357
  return {
1356
1358
  inline_keyboard: [
1357
1359
  [
1358
- { text: '🔁 Повторить', callback_data: 'retry' },
1359
- { text: '🩺 Диагноз', callback_data: 'doctor' },
1360
+ { text: `🔁 ${t(lang, 'btn.retry')}`, callback_data: 'retry' },
1361
+ { text: `🩺 ${t(lang, 'btn.doctor')}`, callback_data: 'doctor' },
1360
1362
  ],
1361
1363
  ],
1362
1364
  };
package/control.mjs CHANGED
@@ -700,20 +700,10 @@ export async function handleControl(ctx) {
700
700
  }
701
701
 
702
702
  case '/help': {
703
- say(
704
- [
705
- 'Задачу можно писать обычным текстом. Каждая ветка форума — отдельная сессия.',
706
- '',
707
- 'Сессии: /new · /status · /sessions [n] · /resume <id>',
708
- 'Модель: /models · /model <id> [both]',
709
- 'Расширения: /plugins [headless] · /plugin add <pkg> · /plugin remove <name> · /plugin disable|enable <id>',
710
- 'Скиллы: /skills · /skills <name>',
711
- 'Персона: /soul · /soul <текст>',
712
- 'Служебное: /stop · /restart confirm · /app restart confirm',
713
- 'Темы: /seed <текст> — засеять пустую тему и закрепить сообщение',
714
- 'Команды можно писать и слитно: /apprestart.confirm = /app restart confirm',
715
- ].join('\n'),
716
- );
703
+ // One translated body keeps the help in step with the menu; the Russian
704
+ // text stays as the fallback when a key is missing.
705
+ const helpLang = normalizeLang(state.lang?.[key] || config.lang) || 'ru';
706
+ say(t(helpLang, 'help.body'));
717
707
  return true;
718
708
  }
719
709
 
package/i18n.mjs CHANGED
@@ -1,171 +1,186 @@
1
- // i18n — three locales for the user-visible surfaces.
2
- //
3
- // Scope: everything a person sees without asking for it — the Telegram command
4
- // menu, the live status message, turn errors and the everyday commands. Long
5
- // explanatory texts stay in the source language until they are translated
6
- // properly; a half-translated sentence is worse than a consistent one.
7
- //
8
- // Language resolution order: per-topic choice (/lang) -> config.lang -> the
9
- // sender's Telegram language_code -> 'ru'.
10
- import { readFileSync } from 'node:fs';
11
- import { join } from 'node:path';
12
-
13
- export const LANGS = ['en', 'ru', 'zh'];
14
-
15
- const DICT = {
16
- en: {
17
- 'status.working': 'Working',
18
- 'status.done': 'Done',
19
- 'status.failed': 'Turn failed',
20
- 'status.queued': 'queued',
21
- 'status.stop': 'Stop',
22
- 'status.cache': 'cache',
23
- 'status.steps': 'steps',
24
- 'status.thinking': 'waiting for the model',
25
- 'err.timeout': 'no activity for {min} min — turn interrupted',
26
- 'err.generic': 'something went wrong',
27
- 'msg.started': 'Telegram bridge for DSH is running (headless). Send a task; /help for commands.',
28
- 'msg.unknownCommand': 'Unknown command: {cmd}. Open /help — it lists everything this bot can do.',
29
- 'msg.langSet': 'Language set to English.',
30
- 'cmd.help.title': 'Commands',
31
- 'cmd.help.hint': 'Send any text to start a task. In a forum topic: /cd <project> binds the topic to that project.',
32
- 'cmd.cd.bound': 'Topic bound to project:\n{path}\n\nAll turns run from this directory.',
33
- 'cmd.cd.reset': 'Topic directory reset to the shared one.',
34
- 'cmd.cd.none': 'No such directory: {path}',
35
- 'cmd.new.done': 'New session in this topic. Context cleared.',
36
- 'cmd.stop.killed': 'Turn stopped.',
37
- 'cmd.stop.nothing': 'Nothing to stop.',
38
- 'menu.status': 'Sessions, model, active turns',
39
- 'menu.cd': 'Bind this topic to a project directory',
40
- 'menu.send': 'Send a file from the project into the chat',
41
- 'menu.prices': 'Model prices from the router',
42
- 'menu.stop': 'Stop the current turn',
43
- 'menu.new': 'New session in this topic',
44
- 'menu.seed': 'Seed an empty topic with text and pin it',
45
- 'menu.sessions': 'Recent sessions',
46
- 'menu.resume': 'Continue a session by id',
47
- 'menu.models': 'Available models',
48
- 'menu.model': 'Show or switch the model',
49
- 'menu.skills': 'Installed skills',
50
- 'menu.soul': 'Agent persona',
51
- 'menu.plugins': 'Installed plugins',
52
- 'menu.plugin': 'Enable or disable a plugin',
53
- 'menu.app': 'Desktop app control',
54
- 'menu.restart': 'Restart the agent',
55
- 'menu.help': 'All commands and hints',
56
- 'menu.lang': 'Interface language: en, ru, zh',
57
- },
58
- ru: {
59
- 'status.working': 'Работаю',
60
- 'status.done': 'Готово',
61
- 'status.failed': 'Ход не удался',
62
- 'status.queued': 'в очереди',
63
- 'status.stop': 'Стоп',
64
- 'status.cache': 'кэш',
65
- 'status.steps': 'шагов',
66
- 'status.thinking': 'жду модель',
67
- 'err.timeout': 'нет активности {min} мин — ход прерван',
68
- 'err.generic': 'что-то пошло не так',
69
- 'msg.started': 'Telegram-мост DSH запущен (headless). Пиши задачу; /help — команды.',
70
- 'msg.unknownCommand': 'Неизвестная команда: {cmd}. Открой /help — там всё, что умеет бот.',
71
- 'msg.langSet': 'Язык переключён на русский.',
72
- 'cmd.help.title': 'Команды',
73
- 'cmd.help.hint': 'Любой текст — задача агенту. В теме форума: /cd <проект> привяжет тему к проекту.',
74
- 'cmd.cd.bound': 'Тема привязана к проекту:\n{path}\n\nДальше все команды идут отсюда.',
75
- 'cmd.cd.reset': 'Каталог темы сброшен на общий.',
76
- 'cmd.cd.none': 'Нет такого каталога: {path}',
77
- 'cmd.new.done': 'Новая сессия в этой ветке. Контекст очищен.',
78
- 'cmd.stop.killed': 'Ход остановлен.',
79
- 'cmd.stop.nothing': 'Нечего останавливать.',
80
- 'menu.status': 'Сессии, модель, активные ходы',
81
- 'menu.cd': 'Привязать тему к каталогу проекта',
82
- 'menu.send': 'Прислать файл из проекта в чат',
83
- 'menu.prices': 'Цены моделей у роутера',
84
- 'menu.stop': 'Остановить текущий ход',
85
- 'menu.new': 'Новая сессия в этой теме',
86
- 'menu.seed': 'Засеять пустую тему текстом и закрепить',
87
- 'menu.sessions': 'Последние сессии',
88
- 'menu.resume': 'Продолжить сессию по id',
89
- 'menu.models': 'Доступные модели',
90
- 'menu.model': 'Показать или сменить модель',
91
- 'menu.skills': 'Установленные скиллы',
92
- 'menu.soul': 'Персона агента',
93
- 'menu.plugins': 'Установленные плагины',
94
- 'menu.plugin': 'Включить или выключить плагин',
95
- 'menu.app': 'Управление десктоп-приложением',
96
- 'menu.restart': 'Перезапустить агента',
97
- 'menu.help': 'Все команды и подсказки',
98
- 'menu.lang': 'Язык интерфейса: en, ru, zh',
99
- },
100
- zh: {
101
- 'status.working': '处理中',
102
- 'status.done': '完成',
103
- 'status.failed': '运行失败',
104
- 'status.queued': '排队中',
105
- 'status.stop': '停止',
106
- 'status.cache': '缓存',
107
- 'status.steps': '步',
108
- 'status.thinking': '等待模型',
109
- 'err.timeout': '{min} 分钟无活动 — 已中断',
110
- 'err.generic': '出现错误',
111
- 'msg.started': 'DSH 的 Telegram 桥已启动(headless)。直接发送任务;/help 查看命令。',
112
- 'msg.unknownCommand': '未知命令:{cmd}。打开 /help 查看机器人支持的全部命令。',
113
- 'msg.langSet': '界面语言已切换为中文。',
114
- 'cmd.help.title': '命令',
115
- 'cmd.help.hint': '发送任意文本即可开始任务。在论坛话题中:/cd <项目> 将该话题绑定到项目目录。',
116
- 'cmd.cd.bound': '话题已绑定到项目:\n{path}\n\n之后所有操作都在此目录执行。',
117
- 'cmd.cd.reset': '话题目录已恢复为默认。',
118
- 'cmd.cd.none': '目录不存在:{path}',
119
- 'cmd.new.done': '已在本话题开始新会话,上下文已清空。',
120
- 'cmd.stop.killed': '已停止当前任务。',
121
- 'cmd.stop.nothing': '没有可停止的任务。',
122
- 'menu.status': '会话、模型、进行中的任务',
123
- 'menu.cd': '将本话题绑定到项目目录',
124
- 'menu.send': '把项目中的文件发送到聊天',
125
- 'menu.prices': '路由器的模型价格',
126
- 'menu.stop': '停止当前任务',
127
- 'menu.new': '在本话题开始新会话',
128
- 'menu.seed': '用文本初始化空话题并置顶',
129
- 'menu.sessions': '最近的会话',
130
- 'menu.resume': '按 id 继续会话',
131
- 'menu.models': '可用模型',
132
- 'menu.model': '查看或切换模型',
133
- 'menu.skills': '已安装的技能',
134
- 'menu.soul': '代理人格设定',
135
- 'menu.plugins': '已安装的插件',
136
- 'menu.plugin': '启用或停用插件',
137
- 'menu.app': '桌面应用控制',
138
- 'menu.restart': '重启代理',
139
- 'menu.help': '全部命令与提示',
140
- 'menu.lang': '界面语言:en、ru、zh',
141
- },
142
- };
143
-
144
- /** Telegram sends zh-hans / zh-hans-cn / zh-CN; all of them mean the same locale. */
145
- export function normalizeLang(code) {
146
- const raw = String(code || '').toLowerCase();
147
- if (!raw) return '';
148
- if (raw.startsWith('zh')) return 'zh';
149
- if (raw.startsWith('en')) return 'en';
150
- if (raw.startsWith('ru')) return 'ru';
151
- return '';
152
- }
153
-
154
- export function t(lang, key, vars = {}) {
155
- const table = DICT[lang] || DICT.ru;
156
- const text = table[key] ?? DICT.ru[key] ?? key;
157
- return String(text).replace(/\{(\w+)\}/g, (_, name) => (vars[name] ?? `{${name}}`));
158
- }
159
-
160
- /** Menu rows for setMyCommands, translated per locale. */
161
- export function menuFor(lang, commands) {
162
- return commands.map((c) => ({ command: c.command, description: t(lang, `menu.${c.command}`) }));
163
- }
164
-
165
- /** Language for a message: topic choice, then config default, then Telegram. */
166
- export function resolveLang(state, key, msg, config = {}) {
167
- const chosen = state?.lang?.[key] || config.lang;
168
- // A foreign user must land in English, not Russian: Telegram reports the
169
- // sender's own locale, and everything outside ru/zh/en falls back to en.
170
- return normalizeLang(chosen) || normalizeLang(msg?.from?.language_code) || 'en';
171
- }
1
+ // i18n — three locales for the user-visible surfaces.
2
+ //
3
+ // Scope: everything a person sees without asking for it — the Telegram command
4
+ // menu, the live status message, turn errors and the everyday commands. Long
5
+ // explanatory texts stay in the source language until they are translated
6
+ // properly; a half-translated sentence is worse than a consistent one.
7
+ //
8
+ // Language resolution order: per-topic choice (/lang) -> config.lang -> the
9
+ // sender's Telegram language_code -> 'ru'.
10
+ import { readFileSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+
13
+ export const LANGS = ['en', 'ru', 'zh'];
14
+
15
+ const DICT = {
16
+ en: {
17
+ 'status.working': 'Working',
18
+ 'status.done': 'Done',
19
+ 'status.failed': 'Turn failed',
20
+ 'status.queued': 'queued',
21
+ 'status.stop': 'Stop',
22
+ 'help.body': "Write the task as plain text. Each forum topic is its own session:\n\nWork: /cd \u003cproject\u003e · /new · /stop · /send \u003cfile\u003e · /seed \u003ctext\u003e\nSessions: /status · /sessions [n] · /resume \u003cid\u003e\nModels: /models · /model \u003cid\u003e · /prices\nExtensions: /skills · /plugins · /plugin add|remove · /soul\nOther: /lang · /doctor · /logs [errors|N] · /app · /restart · /help",
23
+ 'btn.stop': 'Stop',
24
+ 'btn.logs': 'Logs',
25
+ 'btn.retry': 'Retry',
26
+ 'btn.doctor': 'Doctor',
27
+ 'status.cache': 'cache',
28
+ 'status.steps': 'steps',
29
+ 'status.thinking': 'waiting for the model',
30
+ 'err.timeout': 'no activity for {min} min — turn interrupted',
31
+ 'err.generic': 'something went wrong',
32
+ 'msg.started': 'Telegram bridge for DSH is running (headless). Send a task; /help for commands.',
33
+ 'msg.unknownCommand': 'Unknown command: {cmd}. Open /help — it lists everything this bot can do.',
34
+ 'msg.langSet': 'Language set to English.',
35
+ 'cmd.help.title': 'Commands',
36
+ 'cmd.help.hint': 'Send any text to start a task. In a forum topic: /cd <project> binds the topic to that project.',
37
+ 'cmd.cd.bound': 'Topic bound to project:\n{path}\n\nAll turns run from this directory.',
38
+ 'cmd.cd.reset': 'Topic directory reset to the shared one.',
39
+ 'cmd.cd.none': 'No such directory: {path}',
40
+ 'cmd.new.done': 'New session in this topic. Context cleared.',
41
+ 'cmd.stop.killed': 'Turn stopped.',
42
+ 'cmd.stop.nothing': 'Nothing to stop.',
43
+ 'menu.status': 'Sessions, model, active turns',
44
+ 'menu.cd': 'Bind this topic to a project directory',
45
+ 'menu.send': 'Send a file from the project into the chat',
46
+ 'menu.prices': 'Model prices from the router',
47
+ 'menu.stop': 'Stop the current turn',
48
+ 'menu.new': 'New session in this topic',
49
+ 'menu.seed': 'Seed an empty topic with text and pin it',
50
+ 'menu.sessions': 'Recent sessions',
51
+ 'menu.resume': 'Continue a session by id',
52
+ 'menu.models': 'Available models',
53
+ 'menu.model': 'Show or switch the model',
54
+ 'menu.skills': 'Installed skills',
55
+ 'menu.soul': 'Agent persona',
56
+ 'menu.plugins': 'Installed plugins',
57
+ 'menu.plugin': 'Enable or disable a plugin',
58
+ 'menu.app': 'Desktop app control',
59
+ 'menu.restart': 'Restart the agent',
60
+ 'menu.help': 'All commands and hints',
61
+ 'menu.lang': 'Interface language: en, ru, zh',
62
+ },
63
+ ru: {
64
+ 'status.working': 'Работаю',
65
+ 'status.done': 'Готово',
66
+ 'status.failed': 'Ход не удался',
67
+ 'status.queued': 'в очереди',
68
+ 'status.stop': 'Стоп',
69
+ 'help.body': "Задачу можно писать обычным текстом. Каждая ветка форума — отдельная сессия:\n\nРабота: /cd \u003cпроект\u003e · /new · /stop · /send \u003cфайл\u003e · /seed \u003cтекст\u003e\nСессии: /status · /sessions [n] · /resume \u003cid\u003e\nМодели: /models · /model \u003cid\u003e · /prices\nРасширения: /skills · /plugins · /plugin add|remove · /soul\nПрочее: /lang · /doctor · /logs [errors|N] · /app · /restart · /help",
70
+ 'btn.stop': 'Стоп',
71
+ 'btn.logs': 'Логи',
72
+ 'btn.retry': 'Повторить',
73
+ 'btn.doctor': 'Диагноз',
74
+ 'status.cache': 'кэш',
75
+ 'status.steps': 'шагов',
76
+ 'status.thinking': 'жду модель',
77
+ 'err.timeout': 'нет активности {min} мин — ход прерван',
78
+ 'err.generic': 'что-то пошло не так',
79
+ 'msg.started': 'Telegram-мост DSH запущен (headless). Пиши задачу; /help — команды.',
80
+ 'msg.unknownCommand': 'Неизвестная команда: {cmd}. Открой /help — там всё, что умеет бот.',
81
+ 'msg.langSet': 'Язык переключён на русский.',
82
+ 'cmd.help.title': 'Команды',
83
+ 'cmd.help.hint': 'Любой текст — задача агенту. В теме форума: /cd <проект> привяжет тему к проекту.',
84
+ 'cmd.cd.bound': 'Тема привязана к проекту:\n{path}\n\nДальше все команды идут отсюда.',
85
+ 'cmd.cd.reset': 'Каталог темы сброшен на общий.',
86
+ 'cmd.cd.none': 'Нет такого каталога: {path}',
87
+ 'cmd.new.done': 'Новая сессия в этой ветке. Контекст очищен.',
88
+ 'cmd.stop.killed': 'Ход остановлен.',
89
+ 'cmd.stop.nothing': 'Нечего останавливать.',
90
+ 'menu.status': 'Сессии, модель, активные ходы',
91
+ 'menu.cd': 'Привязать тему к каталогу проекта',
92
+ 'menu.send': 'Прислать файл из проекта в чат',
93
+ 'menu.prices': 'Цены моделей у роутера',
94
+ 'menu.stop': 'Остановить текущий ход',
95
+ 'menu.new': 'Новая сессия в этой теме',
96
+ 'menu.seed': 'Засеять пустую тему текстом и закрепить',
97
+ 'menu.sessions': 'Последние сессии',
98
+ 'menu.resume': 'Продолжить сессию по id',
99
+ 'menu.models': 'Доступные модели',
100
+ 'menu.model': 'Показать или сменить модель',
101
+ 'menu.skills': 'Установленные скиллы',
102
+ 'menu.soul': 'Персона агента',
103
+ 'menu.plugins': 'Установленные плагины',
104
+ 'menu.plugin': 'Включить или выключить плагин',
105
+ 'menu.app': 'Управление десктоп-приложением',
106
+ 'menu.restart': 'Перезапустить агента',
107
+ 'menu.help': 'Все команды и подсказки',
108
+ 'menu.lang': 'Язык интерфейса: en, ru, zh',
109
+ },
110
+ zh: {
111
+ 'status.working': '处理中',
112
+ 'status.done': '完成',
113
+ 'status.failed': '运行失败',
114
+ 'status.queued': '排队中',
115
+ 'status.stop': '停止',
116
+ 'help.body': "任务直接以文本发送。每个论坛话题是一个独立会话:\n\n工作: /cd \u003c项目\u003e · /new · /stop · /send \u003c文件\u003e · /seed \u003c文本\u003e\n会话: /status · /sessions [n] · /resume \u003cid\u003e\n模型: /models · /model \u003cid\u003e · /prices\n扩展: /skills · /plugins · /plugin add|remove · /soul\n其他: /lang · /doctor · /logs [errors|N] · /app · /restart · /help",
117
+ 'btn.stop': '停止',
118
+ 'btn.logs': '日志',
119
+ 'btn.retry': '重试',
120
+ 'btn.doctor': '诊断',
121
+ 'status.cache': '缓存',
122
+ 'status.steps': '步',
123
+ 'status.thinking': '等待模型',
124
+ 'err.timeout': '{min} 分钟无活动 — 已中断',
125
+ 'err.generic': '出现错误',
126
+ 'msg.started': 'DSH 的 Telegram 桥已启动(headless)。直接发送任务;/help 查看命令。',
127
+ 'msg.unknownCommand': '未知命令:{cmd}。打开 /help 查看机器人支持的全部命令。',
128
+ 'msg.langSet': '界面语言已切换为中文。',
129
+ 'cmd.help.title': '命令',
130
+ 'cmd.help.hint': '发送任意文本即可开始任务。在论坛话题中:/cd <项目> 将该话题绑定到项目目录。',
131
+ 'cmd.cd.bound': '话题已绑定到项目:\n{path}\n\n之后所有操作都在此目录执行。',
132
+ 'cmd.cd.reset': '话题目录已恢复为默认。',
133
+ 'cmd.cd.none': '目录不存在:{path}',
134
+ 'cmd.new.done': '已在本话题开始新会话,上下文已清空。',
135
+ 'cmd.stop.killed': '已停止当前任务。',
136
+ 'cmd.stop.nothing': '没有可停止的任务。',
137
+ 'menu.status': '会话、模型、进行中的任务',
138
+ 'menu.cd': '将本话题绑定到项目目录',
139
+ 'menu.send': '把项目中的文件发送到聊天',
140
+ 'menu.prices': '路由器的模型价格',
141
+ 'menu.stop': '停止当前任务',
142
+ 'menu.new': '在本话题开始新会话',
143
+ 'menu.seed': '用文本初始化空话题并置顶',
144
+ 'menu.sessions': '最近的会话',
145
+ 'menu.resume': '按 id 继续会话',
146
+ 'menu.models': '可用模型',
147
+ 'menu.model': '查看或切换模型',
148
+ 'menu.skills': '已安装的技能',
149
+ 'menu.soul': '代理人格设定',
150
+ 'menu.plugins': '已安装的插件',
151
+ 'menu.plugin': '启用或停用插件',
152
+ 'menu.app': '桌面应用控制',
153
+ 'menu.restart': '重启代理',
154
+ 'menu.help': '全部命令与提示',
155
+ 'menu.lang': '界面语言:en、ru、zh',
156
+ },
157
+ };
158
+
159
+ /** Telegram sends zh-hans / zh-hans-cn / zh-CN; all of them mean the same locale. */
160
+ export function normalizeLang(code) {
161
+ const raw = String(code || '').toLowerCase();
162
+ if (!raw) return '';
163
+ if (raw.startsWith('zh')) return 'zh';
164
+ if (raw.startsWith('en')) return 'en';
165
+ if (raw.startsWith('ru')) return 'ru';
166
+ return '';
167
+ }
168
+
169
+ export function t(lang, key, vars = {}) {
170
+ const table = DICT[lang] || DICT.ru;
171
+ const text = table[key] ?? DICT.ru[key] ?? key;
172
+ return String(text).replace(/\{(\w+)\}/g, (_, name) => (vars[name] ?? `{${name}}`));
173
+ }
174
+
175
+ /** Menu rows for setMyCommands, translated per locale. */
176
+ export function menuFor(lang, commands) {
177
+ return commands.map((c) => ({ command: c.command, description: t(lang, `menu.${c.command}`) }));
178
+ }
179
+
180
+ /** Language for a message: topic choice, then config default, then Telegram. */
181
+ export function resolveLang(state, key, msg, config = {}) {
182
+ const chosen = state?.lang?.[key] || config.lang;
183
+ // A foreign user must land in English, not Russian: Telegram reports the
184
+ // sender's own locale, and everything outside ru/zh/en falls back to en.
185
+ return normalizeLang(chosen) || normalizeLang(msg?.from?.language_code) || 'en';
186
+ }
package/package.json CHANGED
@@ -1,51 +1,50 @@
1
1
  {
2
- "name": "dsh-tg-bridge",
3
- "version": "1.1.0",
4
- "description": "Telegram bridge for DeepSeek Harness: text, voice, photos and project-per-topic sessions without opening the desktop app",
5
- "type": "module",
6
- "main": "bridge.mjs",
7
- "bin": {
8
- "dsh-tg": "./cli.mjs"
9
- },
10
- "files": [
11
- "cli.mjs",
12
- "i18n.mjs",
13
- "bridge.mjs",
14
- "setup.mjs",
15
- "control.mjs",
16
- "bridge-kick.mjs",
17
- "bridge-restart-idle.mjs",
18
- "restart-after-send.mjs",
19
- "prices.mjs",
20
- "bot-menu.mjs",
21
- "transcribe.py",
22
- "patches/",
23
- "docs/",
24
- "config.example.json",
25
- "install.sh",
26
- "install.ps1",
27
- "README.md",
28
- "LICENSE"
29
- ],
30
- "scripts": {
31
- "start": "node bridge.mjs",
32
- "setup": "node setup.mjs",
33
- "check": "node setup.mjs --check",
34
- "menu": "node bot-menu.mjs",
35
- "doctor": "node bridge-kick.mjs"
36
- },
37
- "engines": {
38
- "node": ">=20"
39
- },
40
- "keywords": [
41
- "deepseek-harness",
42
- "dsh",
43
- "telegram",
44
- "bridge",
45
- "bot",
46
- "whisper",
47
- "voice"
48
- ],
49
- "license": "MIT",
50
- "dependencies": {}
51
- }
2
+ "name": "dsh-tg-bridge",
3
+ "version": "1.1.1",
4
+ "description": "Telegram bridge for DeepSeek Harness: text, voice, photos and project-per-topic sessions without opening the desktop app",
5
+ "type": "module",
6
+ "main": "bridge.mjs",
7
+ "bin": {
8
+ "dsh-tg": "./cli.mjs"
9
+ },
10
+ "scripts": {
11
+ "start": "node bridge.mjs",
12
+ "setup": "node setup.mjs",
13
+ "check": "node setup.mjs --check",
14
+ "menu": "node bot-menu.mjs",
15
+ "doctor": "node bridge-kick.mjs"
16
+ },
17
+ "engines": {
18
+ "node": "\u003e=20"
19
+ },
20
+ "keywords": [
21
+ "deepseek-harness",
22
+ "dsh",
23
+ "telegram",
24
+ "bridge",
25
+ "bot",
26
+ "whisper",
27
+ "voice"
28
+ ],
29
+ "license": "MIT",
30
+ "dependencies": {
31
+
32
+ },
33
+ "files": [
34
+ "bridge.mjs",
35
+ "control.mjs",
36
+ "i18n.mjs",
37
+ "prices.mjs",
38
+ "setup.mjs",
39
+ "bot-menu.mjs",
40
+ "bridge-kick.mjs",
41
+ "bridge-restart-idle.mjs",
42
+ "restart-after-send.mjs",
43
+ "transcribe.py",
44
+ "cli.mjs",
45
+ "README.md",
46
+ "LICENSE",
47
+ "config.example.json",
48
+ "patches/"
49
+ ]
50
+ }
package/docs/CHANGELOG.md DELETED
@@ -1,45 +0,0 @@
1
- # CHANGELOG — DSH Telegram bridge
2
-
3
- ## 2026-10-24 — v1.1.0
4
-
5
- - **Три языка**: `i18n.mjs` (en/ru/zh), `t()`, нормализация `zh-hans → zh`.
6
- - **Меню на трёх языках**: `setMyCommands` для default/`en`/`zh` (Telegram не принимает `zh-hans`).
7
- - **`/lang en|ru|zh`**: язык хранится на тему; новый пользователь получает свой по `language_code`,
8
- всё вне ru/zh → `en`.
9
- - **Локализованы**: стартовое сообщение, заголовки прогресса, ошибка хода, `/cd`, `/new`, `/stop`,
10
- ответ на неизвестную команду.
11
- - **Неизвестная команда больше не запускает модель** — отвечает подсказкой.
12
- - **Сторож**: пауза 25 с и повторная проверка pid перед тревогой; `requireDesktop: false`,
13
- алерт говорит про мост, а не про десктоп-приложение.
14
- - Исправлен импорт i18n в `bridge.mjs` (был `resolveLang is not defined` и застрявшее сообщение).
15
-
16
- ## 2026-10-23
17
-
18
- - **Таймауты переработаны**: убран жёсткий потолок (`turnTimeoutMs: 0`), тишина больше не
19
- убивает ход; любой байт stdout/stderr продлевает окно; при молчании раз в 2 минуты
20
- обновляется статус (heartbeat), чтобы счётчик времени не замирал.
21
- - **Fallback-цепочка расширена до 7 моделей**: taynik opus-5-5, unorouter glm-5.3-flash:free,
22
- neuraldeep qwen3.6-35b-a3b, ikhdev free-mimo, unorouter deepseek-v4.1-flash:free,
23
- taynik opus-4-7, taynik sonnet-4-7.
24
- - **Триггеры переключения**: добавлены `AUTH`, `403`, `insufficient_user_quota`, `UNKNOWN_MODEL`,
25
- `MISSING_CREDENTIAL`; сообщение в чат: «баланс основной модели исчерпан — работаю на X (free)».
26
- - **Провайдеры**: в профиль headless добавлены `taynik`, `neuraldeep`, `ikhdev`;
27
- ключи в `.credentials.yaml` и проброшены в env дочернего процесса CLI.
28
- - **Субагенты**: `agentOptions` → `unorouter/deepseek-v4.1-flash`.
29
- - **Команды**: диспетчер берёт единый список `CONTROL_COMMANDS` (был жёсткий whitelist без `/cd`,
30
- `/prices`, `/seed` — они уходили в ИИ как текст).
31
- - **Lock**: `restart.lock` не даёт setup и кику стартовать одновременно (причина двух поллеров и 409).
32
- - **Сессии**: при ответе «was recorded in … not …» мост сбрасывает сессию и начинает новую.
33
- - **Документация**: STATE.md, CHANGELOG.md, UX-ANALYSIS.md, обновлён ROADMAP.md.
34
-
35
- ## 2026-10-22
36
-
37
- - Журнал ходов `pending/` + ack после отправки + реплей при старте (фикстура переиграна один раз).
38
- - Двухфазный рестарт: `restart.pending` → exit(75) → супервизор; минутный `DSH-Bridge-Kick`.
39
- - `/cd` — привязка темы к проекту; `setup.mjs` ищет мосты по pid-файлу (у elevated-процесса пустой cmdline).
40
- - Файлы: `/send` (sendDocument) и приём документов в `inbox/`.
41
- - IPv4 принудительно (`dns` + `net`), backoff поллинга, коды `TG_NET`/`TG_POLL`.
42
- - Песочница: `workspace-write` + явный пресет `permission` (id сервиса — `permission`, не `permission-presets`).
43
- - Прогресс: одно статус-сообщение, шаги, кэш в финале.
44
- - Vision: gemini объявлен с `input: [text, image]`; канарейка «red, 42».
45
- - Бэкап конфигов и проектов в приватные репозитории GitHub (`agent-config`, 7 проектов).
@@ -1,23 +0,0 @@
1
- # Критерии приёмки публичного релиза
2
-
3
- Дата: 24.10.2026. Репозиторий: `dsh-tg-bridge-oss`.
4
- Проверка — команда или факт, а не намерение.
5
-
6
- ## R. Готовность к публикации
7
-
8
- | # | Критерий | Проверка |
9
- |---|---|---|
10
- | R1 | README объясняет, что это, зачем, и подключает с нуля за ≤5 шагов | чтение + прогон шагов на чистой копии |
11
- | R2 | `config.example.json` без личных данных: без chat id, без путей пользователя | `grep -E "[0-9]{7,}" config.example.json` пусто |
12
- | R3 | Есть `LICENSE` (MIT) с именем владельца | файл существует, 20+ строк |
13
- | R4 | `package.json`: имя, версия, описание, scripts (`setup`, `check`, `menu`) | `node -e "require('./package.json')"` |
14
- | R5 | Документация в `docs/`: STATE, CHANGELOG, ROADMAP, ACCEPTANCE, UX-ANALYSIS | 5 файлов, лишних дублей нет |
15
- | R6 | Нет секретов и личных id в индексе git | `git ls-files` + grep по токенам/путям |
16
- | R7 | `git status` чист, есть коммит релиза с тегом версии | `git log --oneline -1`, `git status --short` |
17
- | R8 | `node setup.mjs --check` не падает на свежем клоне | exit code 0 |
18
- | R9 | Все runtime-файлы на месте и проходят `node --check` | 5 файлов, синтаксис 0 |
19
-
20
- ## Что считается «готово»
21
-
22
- Релиз считается готовым, когда R1–R9 пройдены **все**, а в README есть раздел
23
- «Проверка» с командами, которые пользователь может выполнить сам.
package/docs/ROADMAP.md DELETED
@@ -1,51 +0,0 @@
1
- # DSH ↔ Telegram: дорожная карта
2
-
3
- Обновлено: 23.10.2026. Источник истины по состоянию — `STATE.md`, история — `CHANGELOG.md`,
4
- UX-разбор — `UX-ANALYSIS.md`, задание агенту DSH — `DSH-ROADMAP-HANDOFF.md`.
5
-
6
- ## Сделано (22–23.10)
7
-
8
- - Журнал ходов + ack + реплей; двухфазный рестарт; минутный супервизор; лок setup/kick.
9
- - Один поллер; pid-файл; поиск процессов по pid, а не по пустому cmdline elevated-процесса.
10
- - 18 команд с единым списком; `/cd` (проект на тему), `/prices`, `/send`, `/sessions`, `/model`.
11
- - Файлы в обе стороны; входящие документы в `inbox/`.
12
- - Песочница `workspace-write` + явный пресет; проверено: `C:\Windows` закрыт, проект открыт.
13
- - Прогресс: одно статус-сообщение, шаги, текст модели, кэш; heartbeat каждые 2 мин.
14
- - Таймауты: жёсткого потолка нет, тишина не убивает ход.
15
- - Сеть: IPv4 принудительно, backoff поллинга, коды ошибок.
16
- - Fallback: 7 моделей; провайдеры taynik/neuraldeep/ikhdev; сообщение «баланс исчерпан — работаю на X (free)».
17
- - Субагенты: `agentOptions` → unorouter/deepseek-v4.1-flash.
18
-
19
- ## P0 — следующие (стабильность и контроль)
20
-
21
- | # | Задача | Приёмка |
22
- |---|---|---|
23
- | 1 | `/doctor` + `/logs <код>` | одна команда: PASS/FAIL по pid, getMe, 409, песочнице, модели, диску |
24
- | 2 | Fail-fast на 403/quota | при исчерпанном балансе переключение сразу, без 20-минутного хода |
25
- | 3 | Неизвестная команда → меню | `/abc` не запускает модель и не тратит деньги |
26
- | 4 | Approvals кнопками | удаление внутри проекта ждёт «Разрешить/Запретить»; отказ = fail-closed |
27
- | 5 | Кнопки «Стоп»/«Повторить» | нажатие останавливает ход; «Повторить» перезапускает тот же промпт |
28
-
29
- ## P1 — паритет с UI
30
-
31
- 6. Стриминг ответа правками сообщения (троттлинг 1.5–2 с).
32
- 7. Итог хода: время, шаги, токены, кэш, стоимость, модель (включая free-переключение).
33
- 8. Очередь с «Отменить»; `/compact`, `/retry`, `/cost`, `/find`, `/rename`.
34
- 9. Worktree: обёртка `git worktree add .worktrees/<name>`, diff в чат.
35
- 10. Health-probe моделей раз в час → `models-health.json`, предупреждение при деградации.
36
- 11. Авто-артефакты: ход создал файл → кнопка «прислать».
37
-
38
- ## P2 — продукт и open source
39
-
40
- 12. Конфиг вместо хардкода (user id, пути, язык, лимиты).
41
- 13. Установщики: Windows (schtasks+VBS), macOS (launchd), Linux (systemd --user).
42
- 14. i18n `en`/`ru`, `/lang`.
43
- 15. JSONL-логи со стабильными кодами и hint.
44
- 16. Публичный репозиторий: README EN, MIT, CI на трёх ОС, скриншоты.
45
-
46
- ## Риски
47
-
48
- - Внутренний API DSH — не публичный контракт → версия-гейт и CLI-fallback.
49
- - Approvals без answerer'а = fail-closed: плагин ставится вместе с мостом.
50
- - Free-провайдеры нестабильны (503 capacity) → нужна цепочка, а не одна модель.
51
- - `create_worktree` на этой машине не работает до фикса PATH.
package/docs/STATE.md DELETED
@@ -1,64 +0,0 @@
1
- # STATE — DSH Telegram bridge
2
-
3
- Обновлено: 23.10.2026. Формат: что работает → что сломано → что дальше.
4
- Проверять состояние: `node setup.mjs` (он же пишет `setup.log`), затем `tail bridge.log`.
5
-
6
- ## Работает (проверено на живой системе)
7
-
8
- - **Один поллер**: pid-файл + liveness; минутный супервизор `DSH-Bridge-Kick` (schtasks, код 0);
9
- лок `restart.lock` — setup и кик не стартуют одновременно.
10
- - **Журнал ходов**: `pending/<update_id>.json` (temp+fsync+rename) → offset → ход → отправка → ack;
11
- реплей при старте; дедуп по `update_id`, `done.json` с TTL.
12
- - **Двухфазный рестарт**: `restart.pending` → exit(75) → супервизор поднимает.
13
- - **Команды**: 18, единый список `CONTROL_COMMANDS`; `/cd`, `/prices`, `/send`, `/sessions`, `/model`.
14
- - **Проект на тему**: `/cd <проект>` → cwd темы; песочница `workspace-write` режет всё вне каталога.
15
- - **Файлы**: `/send <путь>` (sendDocument), входящие документы → `inbox/`, путь уходит в ход.
16
- - **Прогресс**: одно статус-сообщение на ход, правится; шаги, текст модели, счётчик времени,
17
- кэш в финале; heartbeat каждые 2 минуты при молчании.
18
- - **Таймауты**: жёсткого потолка нет (`turnTimeoutMs: 0`); тишина не убивает ход.
19
- - **Сеть**: IPv4 принудительно, backoff поллинга 5→30 с, коды `TG_NET`/`TG_POLL`.
20
- - **Модели**: основная unorouter deepseek-v4.1-flash-1017; fallback-цепочка из 7 моделей,
21
- первая рабочая free — `taynik/`; при 403/квоте в чат уходит «баланс исчерпан — работаю на X (free)».
22
- - **Субагенты**: `agentOptions` → unorouter/deepseek-v4.1-flash.
23
- - **Вижн**: gemini через `--patch vision.yml`; read_image работает (канарейка «red, 42»).
24
-
25
- ## Известные проблемы
26
-
27
- 1. **Нет инлайн-кнопок** (`reply_markup`/`callback_query` отсутствуют): «Стоп», «Повторить»,
28
- «Прислать файл», approvals — только текстом.
29
- 2. **Нет approvals**: политика `never`, удаление внутри проекта проходит без подтверждения.
30
- 3. **Неизвестная `/команда`** уходит в ИИ-ход (тратит деньги) вместо подсказки.
31
- 4. **Нет `/doctor`** — диагноз собирается вручную.
32
- 5. **`create_worktree` падает** `0xC0000135` (git не виден процессу); ручной `git worktree add` работает.
33
- 6. **Баланс unorouter** близок к нулю; free-провайдеры частично нестабильны (503 capacity).
34
- 7. **Ответ не стримится**: промежуточный текст виден только строкой в статусе.
35
- 8. **opencode_zen** без ключа; DeepSeek-ключ в credentials невалиден (401).
36
-
37
- ## Следующие доработки
38
-
39
- **P0 (стабильность и контроль)**
40
- 1. `/doctor` + `/logs <код>`: pid, getMe, свежие 409, режим песочницы, модель, диск.
41
- 2. Fail-fast: 403/quota ловить сразу (до долгого хода), а не после 20 минут.
42
- 3. Неизвестная команда → меню, без запуска модели.
43
- 4. Approvals кнопками через `dsh-user-approval` (answerer → inline keyboard).
44
- 5. Инлайн-кнопки: «Стоп» под статусом, «Повторить» под ошибкой.
45
-
46
- **P1 (паритет с UI)**
47
- 6. Стриминг ответа правками сообщения (троттлинг 1.5–2 с).
48
- 7. Итог хода: время, шаги, токены, кэш, стоимость, модель (включая free-переключение).
49
- 8. Очередь с кнопкой «Отменить»; `/compact`, `/retry`, `/cost`, `/find`, `/rename`.
50
- 9. Worktree: обёртка `git worktree add .worktrees/<name>` + diff в чат.
51
- 10. Периодический health-probe моделей (раз в час) с записью в `models-health.json`.
52
-
53
- **P2 (продукт)**
54
- 11. Конфиг вместо хардкода, установщики Windows/macOS/Linux, i18n en/ru.
55
- 12. JSONL-логи со стабильными кодами и hint; публичный репозиторий (README EN, MIT, CI).
56
-
57
- ## Как проверить
58
-
59
- ```
60
- node setup.mjs # старт/рестарт + самодиагностика
61
- node test-control.mjs # /cd привязывает каталог
62
- node bridge-kick.mjs # супервизор: поднимает мост, если pid мёртв
63
- tail -n 50 bridge.log # ходы, ошибки, переключения моделей
64
- ```
@@ -1,62 +0,0 @@
1
- # UX-анализ Telegram-моста DSH (без внедрения)
2
-
3
- Дата: 23.10.2026. Основано на живом коде `~/.dsh/tg-bridge` и ответах Bot API.
4
-
5
- ## 1. Факты
6
-
7
- - Команды **зарегистрированы**: `getMyCommands` в default scope отдаёт 18 штук
8
- (`new cd status stop prices send seed sessions resume models model skills soul plugins plugin app restart help`).
9
- Отдельных scope для чата/групп нет — работает default, этого достаточно.
10
- - В коде 18 обработчиков; неизвестная `/команда` **не даёт подсказку**, а уходит в ИИ как обычный текст.
11
- - Инлайн-кнопок нет вообще: `reply_markup` — 0 вхождений, `callback_query` — 0. Значит нет
12
- «Стоп» кнопкой, «Повторить», «Прислать файл», подтверждений.
13
- - Статус-сообщение есть: создаётся на ход, правится (инструменты, текст, счётчик времени, кэш в финале).
14
- - Typing-индикатор есть (обновляется каждые 4.5 с).
15
- - Ответ приходит целиком в конце: промежуточный текст модели виден только строкой `• текст: …` в статусе.
16
-
17
- ## 2. Что из Hermes стоит перенести (по убыванию пользы)
18
-
19
- ### P0 — ощутимо меняет работу
20
- 1. **Инлайн-кнопки + callback.** «Стоп» под статусом, «Повторить» под ошибкой,
21
- «Прислать файл» после хода, «Разрешить / Запретить / Всегда» для approvals.
22
- Сейчас всё это только текстом.
23
- 2. **Неизвестная команда — не ход.** Сейчас `/fuck` запускает модель. Должно быть:
24
- «Нет такой команды» + меню. Это ещё и экономит деньги.
25
- 3. **`/doctor` и `/logs <код>`.** Диагноз одной командой: pid, getMe, 409, песочница, модель, диск.
26
- Без этого каждый сбой превращается в переписку.
27
- 4. **Стриминг ответа.** Редактировать одно сообщение по мере генерации (троттлинг 1.5-2 с),
28
- а не отдавать готовый блок через 4 минуты. Telegram позволяет ~20 правок в минуту на чат.
29
-
30
- ### P1 — комфорт
31
- 5. **Очередь с кнопкой «Отменить»**: «в очереди (2), отменить» вместо молчания.
32
- 6. **Итог хода**: время, шаги, токены, кэш %, стоимость. Сейчас есть только кэш.
33
- 7. **Авто-артефакты**: если ход создал файлы — кнопки «прислать».
34
- 8. **Команды, которых не хватает**: `/compact`, `/retry`, `/cost`, `/find <текст>` по сессиям,
35
- `/rename` темы, `/pin`.
36
- 9. **`/help` по группам**: «работа», «сессии», «модели», «диагностика» — 18 строк подряд читать тяжело.
37
- 10. **Ошибки с кодом и hint**: `SANDBOX_DENY`, `DSH_SPAWN_FAIL`, `TG_CONFLICT`, `SESSION_CWD`.
38
-
39
- ### P2 — полировка
40
- 11. Голосовые ответы (TTS) опционально — для длинных объяснений.
41
- 12. `/stats`: сколько ходов, средняя длительность, доля ошибок за неделю.
42
- 13. Настройка «показывать текст модели» / «только инструменты» — кому-то шумно.
43
- 14. Автопереименование темы по первой задаче (как в Hermes), с подтверждением.
44
-
45
- ## 3. Ограничения Telegram, которые нельзя обойти
46
-
47
- - 4096 символов на сообщение → уже режется на части; правки статуса должны оставаться в одном сообщении.
48
- - ~20 правок в минуту на чат → троттлинг обязателен (сейчас 3.5 с — можно 1.5-2 с).
49
- - Инлайн-клавиатура: до 64 кнопок, но на телефоне читаемо максимум 4-6.
50
- - В группах команды работают, но меню показывается по default scope; отдельные scope не нужны.
51
-
52
- ## 4. Что уже хорошо и трогать не надо
53
-
54
- - Журнал + ack + реплей; двухфазный рестарт; лок setup/kick; один поллер.
55
- - Статус-сообщение с шагами и кэшем; typing; IPv4-фикс; HTML-код с копированием.
56
- - `/cd` на проект + песочница, `/send` и приём документов, `/prices`.
57
-
58
- ## 5. Вывод
59
-
60
- Разрыв с Hermes не в командах (они есть), а в **интерактиве**: нет кнопок и callback,
61
- нет стриминга ответа, неизвестные команды уходят в модель, нет диагностики.
62
- Четыре пункта P0 закрывают 80% ощущения «работает как Hermes».
@@ -1,120 +0,0 @@
1
- # DSH ↔ Telegram: план доработки для агента DSH
2
-
3
- Автор: Hermes. Дата: 22.10.2026. Файл лежит на той же машине, что и мост:
4
- `C:\Users\bistr\Projects\dsh-tg-bridge-oss\DSH-ROADMAP-HANDOFF.md`
5
-
6
- Задача: довести мост до уровня, на котором владелец работает через Telegram так же
7
- полноценно, как в UI Hermes, и при этом использует максимум возможностей DSH.
8
-
9
- > **Обновление 23.10.2026 — уже сделано, не переделывать:** жёсткий таймаут убран
10
- > (`turnTimeoutMs: 0`), тишина не убивает ход, heartbeat раз в 2 минуты; fallback-цепочка
11
- > из 7 моделей (taynik/neuraldeep/ikhdev/unorouter free) с сообщением «баланс исчерпан —
12
- > работаю на X (free)»; триггеры переключения включают AUTH/403/quota; субагенты на
13
- > `unorouter/deepseek-v4.1-flash`; единый `CONTROL_COMMANDS`; `restart.lock`.
14
- > Актуальный статус — `STATE.md`, история — `CHANGELOG.md`.
15
-
16
- ## 0. Проверенное состояние на момент передачи
17
-
18
- - Мост: один процесс, pid в `C:\Users\bistr\.dsh\tg-bridge\bridge.pid`; минутный супервизор
19
- `DSH-Bridge-Kick` (schtasks, LastTaskResult 0); лок `restart.lock` защищает от гонки setup/kick.
20
- - Журнал ходов: `pending/<update_id>.json` → сдвиг offset → ход → отправка → ack; реплей при старте.
21
- - Команды: 18, список в `control.mjs` → `CONTROL_COMMANDS` (мост импортирует его, не дублирует).
22
- - `/cd <проект>` привязывает тему к каталогу; песочница `workspace-write` ограничивает записи
23
- этим каталогом (проверено: `C:\Windows` запрещён, проект разрешён).
24
- - Файлы: `/send <путь>` работает; входящие документы сохраняются в `inbox/` и передаются ходу.
25
- - Сеть: IPv4 принудительно, backoff поллинга 5→30 с, коды `TG_NET` / `TG_POLL`.
26
- - Модели: deepseek-v4.1-flash-1017; вижн через gemini; кэш 99,3% при продолжении сессии.
27
- - Профиль headless: `sandbox-policy` = workspace-write, `approval` = never, сервис пресетов
28
- `permission` с таблицей пресетов и `defaultPreset: workspace-write`.
29
-
30
- ## 1. Правила работы (не нарушать)
31
-
32
- 1. Не перезапускать мост изнутри хода: процесс — родитель CLI. Только `restart.pending` → exit(75).
33
- 2. Любой рестарт — через `node setup.mjs` (он берёт `restart.lock`); руками не убивать.
34
- 3. После правки — `node --check` каждого изменённого файла и проверка чтением назад.
35
- 4. Секреты не логировать; токен только из `.credentials.yaml`.
36
- 5. Изменения в профиле DSH — с бэкапом `cordis.patch.yml.bak-<дата>`.
37
- 6. Перед «готово» — измеримый тест, а не «должно работать».
38
-
39
- ## 2. P0 — стабильность и контроль
40
-
41
- ### 2.1 Живой прогресс + кнопка «Стоп»
42
- Файлы: `bridge.mjs` (`runTurnLive`, `onEvent`), спека `PROGRESS.md` рядом.
43
- Сделать:
44
- - одно сообщение на ход; первый `status`/`tool_call` создаёт его, дальше `editMessageText`;
45
- - троттлинг 1.5-2 с, игнор `400 message is not modified`, `retry_after` на 429;
46
- - шапка: `Работаю · 0:42 · шаг 3/7`, последние 3-5 инструментов, после 5 с тишины «жду модель»;
47
- - финал заменяет статус ответом (сохраняется HTML-код);
48
- - inline-кнопка «Стоп» → существующий `stopTurn(key)`;
49
- - на старте моста незавершённые статусы помечать `[!] прервано`.
50
- Приёмка: ход из 5 инструментов = 1 сообщение + 1 финальная правка; «Стоп» реально убивает ход.
51
-
52
- ### 2.2 Подтверждения опасных действий
53
- На чём: `@deepseek-ai/dsh-user-approval` (политика `ask`, answerer'ы — waterfall-слушатели
54
- `approval/request`; без answerer'а действие fail-closed).
55
- Сделать:
56
- - локальный плагин-answerer, который на `approval/request` отправляет в тему сообщение с
57
- inline-кнопками «Разрешить / Запретить / Всегда» и ждёт ответ (таймаут → deny);
58
- - переключить профиль `headless` на `approval: ask`, добавить пару в таблицу `permission`
59
- (`workspace-write` + `ask`) и `defaultPreset`;
60
- - «Всегда» — только для текущей сессии.
61
- Приёмка: попытка удалить каталог внутри проекта останавливает ход и ждёт кнопку;
62
- «Запретить» → инструмент получает отказ; «Разрешить» → выполняется.
63
-
64
- ### 2.3 `dsh-tg doctor`
65
- Файл: `scripts/doctor.mjs`, запуск `node scripts/doctor.mjs`.
66
- Проверки: один процесс и pid-файл, `getMe`, свежие 409 за 90 с, режим песочницы, доступность
67
- профиля, модель и её input, диск, права на задачу планировщика, версия DSH.
68
- Выход: PASS/FAIL по пунктам + код возврата 1 при любом FAIL.
69
- Приёмка: при убитом мосте doctor говорит `FAIL pid`, при двух поллерах — `FAIL 409`.
70
-
71
- ### 2.4 Сессии при смене каталога
72
- Уже сделано: при ответе «was recorded in ... not ...» мост удаляет сессию и начинает новую.
73
- Проверить: `/cd` в теме, где уже был ход → следующий ход новый `session-id`, ошибки нет.
74
-
75
- ### 2.5 Worktree
76
- Симптом: `create_worktree` падает `0xC0000135` (не находит бинарник; git не виден процессу).
77
- Сделать: либо пробросить PATH к `git.exe` в env дочернего процесса, либо обёртку в мосте:
78
- `git worktree add .worktrees/<name> -b <name>` внутри каталога проекта.
79
- Приёмка: ветка создана, `git worktree list` её показывает, файлы пишутся только в неё.
80
-
81
- ## 3. P1 — паритет с UI
82
-
83
- 1. **API-транспорт**: `@deepseek-ai/dsh-api-gateway` (`/api/remote.mux`, клиент `lib/client.node.js`)
84
- вместо спавна CLI — поток событий, вложения, approvals без парсинга stdout. CLI оставить fallback.
85
- 2. **Сессии**: `/sessions` с токенами, кэшем и стоимостью хода; `/resume`, переименование,
86
- поиск (`dsh-session-query`).
87
- 3. **Jobs**: `/job` — расписание через `dsh-schedule`, результат в тему.
88
- 4. **Plan mode**: `/plan`, отрисовка плана и подтверждение.
89
- 5. **Субагенты**: `/agents` — список, статус, стоп.
90
- 6. **Артефакты**: агент создал файл → кнопка «прислать» (лимит 50 МБ).
91
- 7. **Сжатие**: показывать, когда сработала компакция (кэш падает — владелец должен понимать почему).
92
-
93
- ## 4. P2 — продукт и open source
94
-
95
- 1. Конфиг вместо хардкода: user id, путь к DSH, язык, лимиты.
96
- 2. Установщики: Windows (schtasks + VBS), macOS (launchd), Linux (systemd --user).
97
- 3. i18n `en`/`ru` (`/lang`, автоопределение по `language_code`).
98
- 4. Логи JSONL + стабильные коды (`TG_CONFLICT`, `DSH_SPAWN_FAIL`, `SANDBOX_DENY`,
99
- `ATTACH_MISSING`, `SESSION_CWD`) + поле `hint`.
100
- 5. Публичный репозиторий: README EN, MIT, CI на трёх ОС, скриншоты.
101
- 6. Опциональный плагин-обёртка (approvals + прогресс) с версия-гейтом.
102
-
103
- ## 5. Грабли, уже стоившие времени (не повторять)
104
-
105
- - Сервис пресетов имеет id **`permission`**, а не `permission-presets`; таблицу пресетов нужно
106
- объявлять целиком, иначе `match no preset` и каждый ход падает за 5 с.
107
- - Сессия DSH привязана к каталогу: смена cwd ломает resume (мост теперь сбрасывает сессию).
108
- - Диспетчер команд брал жёсткий список; новые команды молча уходили в ИИ. Теперь `CONTROL_COMMANDS`.
109
- - Задача планировщика под `RunLevel Highest` не убивается `taskkill` (access denied) — только
110
- `Stop-ScheduledTask`.
111
- - У elevated-процесса `Win32_Process.CommandLine` пустой: искать мосты по pid-файлу, не по cmdline.
112
- - `setup` и кик, стартовавшие одновременно, дают двух поллеров и вечный 409 — лок обязателен.
113
- - `fetch` выбирает IPv6 и висит: `dns.setDefaultResultOrder('ipv4first')` +
114
- `net.setDefaultAutoSelectFamily(false)`.
115
- - Правка `.ps1`/`.vbs` под Windows: CRLF, ASCII, иначе PowerShell 5.1 ломается на кириллице.
116
-
117
- ## 6. Порядок выполнения
118
-
119
- P0.1 → P0.2 → P0.3 → P0.4 → P0.5, затем P1 по списку. После каждого пункта — приёмка из
120
- описания, запись в `STATE.md` моста (создать, если нет) и короткий отчёт владельцу в Telegram.
@@ -1,119 +0,0 @@
1
- # DSH ↔ Telegram Bridge — план упаковки в open-source (v2)
2
-
3
- Дата: 22.10.2026. Автор: Hermes. Основание: запрос Aleks (голос, 22.10) + фактический аудит
4
- текущего моста `C:\Users\bistr\.dsh\tg-bridge` и 296 пакетов DSH из `app.asar`.
5
-
6
- ## 1. Цель
7
-
8
- Публичный репозиторий, который ставится одной командой на Windows/macOS/Linux и даёт
9
- пользователю DSH полноценный Telegram-интерфейс: текст, голос, фото, файлы, кнопки
10
- подтверждений, вопросы агента, планы, задачи по расписанию, субагенты — с логами, по которым
11
- любой агент (DSH, Hermes, Codex, Claude Code) сам находит причину сбоя.
12
-
13
- ## 2. Что есть сейчас (факты)
14
-
15
- - `bridge.mjs` (1144 строки): long-polling getUpdates → спавн `dsh --profile headless --json` на
16
- каждое сообщение → один сеанс DSH на чат/тему.
17
- - Работает: текст, голос (faster-whisper small, локально), фото (gemini через vision-патч),
18
- темы форума, меню 15 команд, журнал `pending/` + ack, двухфазный рестарт, минутный кик.
19
- - Ограничения спавна CLI: нет канала вложений (`--help`: только task/--json/--session-id), нет
20
- стриминга в реальном времени, нет approvals/ask-user (интерактив недоступен), стоимость —
21
- старт Electron-процесса на каждое сообщение (~2-3 с), 30-минутный потолок хода.
22
-
23
- ## 3. Целевая архитектура v2: клиент API, а не спавн CLI
24
-
25
- В DSH есть `@deepseek-ai/dsh-api-gateway` с типизированным клиентом
26
- (`lib/client.node.js`) и мультиплексом `/api/remote.mux`, плюс контроллеры
27
- `dsh-api-session-controller`, `dsh-api-job-controller`, `dsh-api-workspace-controller`,
28
- `dsh-api-remote-access`. Это тот же канал, которым пользуется штатный web-UI.
29
-
30
- Даёт бесплатно:
31
- - поток событий сессии (status/tool_call/tool_result/text) без парсинга stdout;
32
- - approvals и `ask_user` как запросы к клиенту → кнопки Telegram;
33
- - загрузку/выгрузку вложений (файлы, изображения) нативно;
34
- - список сессий, переименование, поиск, сжатие;
35
- - jobs/schedule — расписания живут в DSH, а не в крон-задачах ОС;
36
- - стабильные коды ошибок (`remote-error-codes`) вместо «fetch failed».
37
-
38
- План: два транспорта за одним интерфейсом.
39
- - `transport=api` (по умолчанию, если DSH ≥ 0.2.1 и API включён);
40
- - `transport=cli` (fallback, текущий код) — для старых сборок и аварийного режима.
41
-
42
- Это снимает большинство пунктов «90% UI» без изобретения протокола.
43
-
44
- ## 4. Кроссплатформенность
45
-
46
- | Слой | Windows | macOS | Linux |
47
- |---|---|---|---|
48
- | Автозапуск | Task Scheduler (S4U, hidden VBS) | launchd plist `~/Library/LaunchAgents` | systemd --user unit |
49
- | Супервизор 1 мин | задача `*-Kick` | `StartInterval 60` | `Restart=always` + timer |
50
- | Скрытый запуск | `wscript` wrapper | нет окон | нет окон |
51
- | Пид/синглтон | pid-файл + liveness (EPERM=жив) | то же | то же |
52
-
53
- Установщик `npx dsh-tg setup` определяет ОС, ставит нужный юнит, проверяет права и пишет,
54
- что именно сделал (каждый шаг в `setup.log`).
55
-
56
- ## 5. Упаковка и i18n
57
-
58
- - npm-пакет `dsh-telegram-bridge`, bin `dsh-tg` (`setup`, `run`, `doctor`, `logs`, `restart`).
59
- - Опционально плагин DSH (`dsh plugin add`), если API-режим: тогда мост — это headless-профиль
60
- с плагином, без отдельного демона.
61
- - Языки: `en` (по умолчанию) + `ru`; строки в `locales/<lang>.json`, выбор командой `/lang`,
62
- автоопределение из Telegram `language_code`. В DSH уже есть `dsh-client-locale` — можно
63
- переиспользовать формат.
64
-
65
- ## 6. Логи и диагностика (главное требование Aleks)
66
-
67
- - Единый JSONL-лог: `ts, level, code, session, chat, tool, ms, msg` + человекочитаемая строка.
68
- - Каждая ошибка = стабильный `code` (`TG_CONFLICT`, `DSH_SPAWN_FAIL`, `DSH_TIMEOUT`,
69
- `NET_IPV6`, `ATTACH_MISSING`, `PID_STALE`, `JOURNAL_CORRUPT`) и поле `hint` с готовым
70
- действием. Агент читает код и действует без археологии.
71
- - `dsh-tg doctor` — самодиагностика: один поллер, pid-файл, getMe, свежие 409, диск, права,
72
- версия DSH, доступность профиля, наличие vision-модели. Выход: PASS/FAIL по каждому пункту.
73
- - Секреты: токен и ключи только из credentials/env; в логах — `[REDACTED]`, тест на это в CI.
74
- - Ротация: 5 МБ × 3 файла, без потери последних строк при падении.
75
-
76
- ## 7. Матрица паритета с UI (что закрываем в v2)
77
-
78
- | Возможность UI | Telegram | Как |
79
- |---|---|---|
80
- | Текст, стриминг шагов | ✅ | заголовок сообщения + editInPlace (throttle 1.5-2 с) |
81
- | Фото/файлы на вход | ✅ | upload → attachment service |
82
- | Файлы на выход | ✅ | deliverables → sendDocument |
83
- | Голос | ✅ | локальный whisper (уже есть), опционально STT-плагин DSH |
84
- | Approvals | ✅ | inline-клавиатура Да/Нет/Всегда, таймаут = deny |
85
- | ask_user (вопросы агента) | ✅ | inline-кнопки + свободный текст |
86
- | Plan mode | ✅ | `/plan` + отрисовка плана и подтверждение |
87
- | Todo/прогресс | ✅ | одно редактируемое сообщение |
88
- | Список сессий | ✅ | `/chats` → inline-пагинация |
89
- | MCP/скиллы | ✅ | наследуются профилем, без изменений |
90
- | Терминал | ⚠️ | вывод построчно, интерактив — только через `/exec` |
91
- | Субагенты | ⚠️ | события + `/agents`, без отдельного чата на каждого |
92
- | Jobs/расписания | ✅ | нативно из DSH + уведомление в чат |
93
- | Плагин-маркет | ❌ | вне Telegram, ссылка на UI |
94
- | Редактор файлов | ❌ | заменяется файлами и `/download` |
95
-
96
- ## 8. Этапы
97
-
98
- 1. **Каркас пакета** (1-2 дня): структура репо, конфиг, i18n, `doctor`, JSONL-логи, CI на трёх ОС,
99
- перенос текущего CLI-транспорта как `transport=cli`.
100
- 2. **API-транспорт** (2-4 дня): спайк на `client.node.js` → события сессии, затем approvals и
101
- ask_user кнопками; переключатель транспорта.
102
- 3. **Вложения и файлы** (1-2 дня): вход/выход, лимиты 50 МБ, превью.
103
- 4. **Супервизор и установка** (1-2 дня): три юнита, идемпотентный `setup`, `restart.pending`.
104
- 5. **Наблюдаемость** (1 день): коды ошибок + hints, `dsh-tg logs --grep`, экспорт сессии.
105
- 6. **Публикация**: README (EN), скриншоты, гайд «поставить за 5 минут», CONTRIBUTING, MIT.
106
-
107
- ## 9. Риски
108
-
109
- - API DSH не документирован как публичный контракт → версия-гейт и fallback на CLI обязательны.
110
- - Разные ОС-супервизоры → CI-матрица, ручной чек-лист на macOS (нет машины под рукой).
111
- - Утечка токена в логах → тест на редактирование в CI.
112
- - Windows: окна консолей → только VBS/скрытые обёртки, проверка «нет чёрных окон» в приёмке.
113
-
114
- ## 10. Открытые вопросы (к Aleks)
115
-
116
- 1. Транспорт v2: API gateway сразу или сначала CLI-пакет, API — вторым релизом?
117
- 2. Формат поставки: npm + `npx setup`, плагин DSH, или и то и другое?
118
- 3. Языки: en+ru достаточно для первого релиза?
119
- 4. Приоритет после базовой упаковки: approvals/ask-user, jobs, или файлы/делвераблы?
@@ -1,94 +0,0 @@
1
- # Живой прогресс в Telegram — спецификация для tg-bridge
2
-
3
- Цель: один редактируемый статус-блок на ход, как в Hermes, вместо сообщений на каждое событие.
4
-
5
- ## 1. Модель
6
-
7
- - На первом событии хода мост создаёт статус-сообщение и запоминает `message_id` по ключу
8
- `chat:thread:turn`.
9
- - Все последующие события **редактируют** это сообщение (`editMessageText`), не создают новые.
10
- - Финальный ответ: статус редактируется в готовый текст ответа (сохраняется «копировать» у кода),
11
- либо удаляется и ответ уходит отдельным сообщением — выбрать одно и не смешивать.
12
- - Ошибка хода: статус редактируется в `⚠️ <code>: <короткий смысл>` + строка `hint`.
13
-
14
- ## 2. Что показывать
15
-
16
- ```
17
- 🟡 Работаю · 0:42 · шаг 3/7
18
- ▸ read_image canary.png
19
- ▸ terminal powershell -NoProfile …
20
- ⏳ жду модель (12 с)
21
- ```
22
-
23
- - Заголовок: статус (🟡 работает / 🟢 готово / 🔴 ошибка), прошедшее время, счётчик шагов.
24
- - Последние 3-5 инструментов, по одному в строке: имя + усечённый аргумент (40 симв.).
25
- - Текущее ожидание, если нет событий > 5 с (`⏳ жду модель`).
26
- - Опционально: токены/стоимость из `status.usage`.
27
-
28
- ## 3. Троттлинг и лимиты Telegram
29
-
30
- - Минимум 1.5-2 с между правками одного сообщения; события копить и склеивать.
31
- - Игнорировать `400 message is not modified`.
32
- - На `429` — уважать `retry_after`, не ретраить сразу.
33
- - Длина статуса ≤ 3500 символов; обрезать историю шагов, а не ответ.
34
- - Максимум одно статус-сообщение на тему чата; при новом ходе старое финализируется.
35
-
36
- ## 4. Скелет
37
-
38
- ```js
39
- const statuses = new Map(); // key -> {id, startedAt, steps, lastEdit, pending, timer}
40
-
41
- function render(state) {
42
- const el = Math.floor((Date.now() - state.startedAt) / 1000);
43
- const hdr = state.error ? '🔴 Ошибка' : state.done ? '🟢 Готово' : '🟡 Работаю';
44
- const lines = [`${hdr} · ${Math.floor(el/60)}:${String(el%60).padStart(2,'0')} · шаг ${state.step || 0}`];
45
- for (const s of state.steps.slice(-4)) lines.push(`▸ ${s.name} ${truncate(s.arg, 40)}`);
46
- if (!state.done && Date.now() - state.lastEvent > 5000) lines.push('⏳ жду модель');
47
- return lines.join('\n');
48
- }
49
-
50
- function onEvent(key, ev) {
51
- const st = statuses.get(key) ?? { startedAt: Date.now(), steps: [], lastEvent: Date.now() };
52
- st.lastEvent = Date.now();
53
- if (ev.type === 'tool_call') { st.step++; st.steps.push({ name: ev.name, arg: ev.input }); }
54
- if (ev.type === 'status' && ev.phase === 'step_start') st.step = ev.step;
55
- statuses.set(key, st);
56
- scheduleEdit(key); // 1.5-2 s debounce, one edit per window
57
- }
58
-
59
- async function scheduleEdit(key) {
60
- const st = statuses.get(key);
61
- if (st.timer) return;
62
- const wait = Math.max(0, 1500 - (Date.now() - (st.lastEdit || 0)));
63
- st.timer = setTimeout(async () => {
64
- st.timer = null; st.lastEdit = Date.now();
65
- try {
66
- if (st.id) await tg('editMessageText', { chat_id: st.chat, message_id: st.id, text: render(st) });
67
- else { const m = await tg('sendMessage', { chat_id: st.chat, text: render(st) }); st.id = m.message_id; }
68
- } catch (e) { if (!/not modified/.test(String(e))) log('status edit failed: ' + e.message); }
69
- }, wait);
70
- }
71
-
72
- function finish(key, finalText, opts) {
73
- const st = statuses.get(key); if (!st) return sendFinal(key, finalText, opts);
74
- statuses.delete(key);
75
- return tg('editMessageText', { chat_id: st.chat, message_id: st.id, text: finalText, parse_mode: 'HTML', ...opts })
76
- .catch(() => sendFinal(key, finalText, opts));
77
- }
78
- ```
79
-
80
- ## 5. Дополнительно (информативность)
81
-
82
- - `⏹ Стоп` inline-кнопкой под статусом (уже есть `/stop` — привязать к callback).
83
- - При втором сообщении во время хода: `⏳ в очереди (2)` вместо тишины.
84
- - Тайпинг (`sendChatAction typing`) раз в 5 с, пока статус не редактировался.
85
- - После хода: одна строка итога `🟢 Готово · 19 с · 2 инструмента` в конце ответа (опционально).
86
- - Ошибки: стабильные коды (`DSH_TIMEOUT`, `DSH_SPAWN_FAIL`, `TG_CONFLICT`, `ATTACH_MISSING`) + hint.
87
-
88
- ## 6. Приёмка
89
-
90
- 1. Ход из 5 инструментов даёт ровно одно сообщение-статус и одну финальную правку.
91
- 2. При падении моста посреди хода статус не остаётся «🟡 Работаю» навсегда: на старте помечать
92
- незавершённые статусы как `⚠️ прервано`.
93
- 3. При 429 статус не теряется, а догоняет состояние.
94
- 4. Telegram не получает больше 1 правки в 1.5 с на чат.
package/install.ps1 DELETED
@@ -1,36 +0,0 @@
1
- # One-line install for Windows:
2
- # irm https://raw.githubusercontent.com/aleksvega/dsh-tg-bridge/main/install.ps1 | iex
3
- $ErrorActionPreference = 'Stop'
4
-
5
- $Repo = if ($env:DSH_TG_REPO) { $env:DSH_TG_REPO } else { 'https://github.com/aleksvega/dsh-tg-bridge.git' }
6
- $Dest = if ($env:DSH_TG_HOME) { $env:DSH_TG_HOME } else { Join-Path $env:USERPROFILE '.dsh\tg-bridge' }
7
-
8
- foreach ($tool in 'git', 'node') {
9
- if (-not (Get-Command $tool -ErrorAction SilentlyContinue)) { throw "$tool is required" }
10
- }
11
- $major = [int]((node -v).TrimStart('v').Split('.')[0])
12
- if ($major -lt 20) { throw "node >= 20 is required (found $(node -v))" }
13
-
14
- if (Test-Path (Join-Path $Dest '.git')) {
15
- Write-Host "updating $Dest"
16
- git -C $Dest pull --ff-only
17
- } else {
18
- Write-Host "installing into $Dest"
19
- New-Item -ItemType Directory -Force -Path (Split-Path $Dest -Parent) | Out-Null
20
- git clone --depth 1 $Repo $Dest
21
- }
22
-
23
- Push-Location $Dest
24
- try {
25
- if (-not (Test-Path 'config.json')) {
26
- Copy-Item 'config.example.json' 'config.json'
27
- Write-Host 'created config.json - edit allowedUserIds and cwd'
28
- }
29
- node cli.mjs check
30
- } finally { Pop-Location }
31
-
32
- Write-Host ''
33
- Write-Host 'Next:'
34
- Write-Host " 1. edit $Dest\config.json (allowedUserIds, cwd, vision/voice paths)"
35
- Write-Host " 2. node $Dest\cli.mjs setup"
36
- Write-Host ' 3. in Telegram: /doctor, /help, /cd <project>'
package/install.sh DELETED
@@ -1,32 +0,0 @@
1
- #!/usr/bin/env bash
2
- # One-line install:
3
- # curl -fsSL https://raw.githubusercontent.com/aleksvega/dsh-tg-bridge/main/install.sh | bash
4
- set -euo pipefail
5
-
6
- REPO="${DSH_TG_REPO:-https://github.com/aleksvega/dsh-tg-bridge.git}"
7
- DEST="${DSH_TG_HOME:-$HOME/.dsh/tg-bridge}"
8
-
9
- command -v git >/dev/null || { echo "git is required"; exit 1; }
10
- command -v node >/dev/null || { echo "node >= 20 is required"; exit 1; }
11
- node -e 'process.exit(Number(process.versions.node.split(".")[0]) >= 20 ? 0 : 1)' \
12
- || { echo "node >= 20 is required (found $(node -v))"; exit 1; }
13
-
14
- if [ -d "$DEST/.git" ]; then
15
- echo "updating $DEST"
16
- git -C "$DEST" pull --ff-only
17
- else
18
- echo "installing into $DEST"
19
- mkdir -p "$(dirname "$DEST")"
20
- git clone --depth 1 "$REPO" "$DEST"
21
- fi
22
-
23
- cd "$DEST"
24
- [ -f config.json ] || { cp config.example.json config.json; echo "created config.json — edit allowedUserIds and cwd"; }
25
-
26
- echo
27
- node cli.mjs check || true
28
- echo
29
- echo "Next:"
30
- echo " 1. edit $DEST/config.json (allowedUserIds, cwd, vision/voice paths)"
31
- echo " 2. node $DEST/cli.mjs setup"
32
- echo " 3. in Telegram: /doctor, /help, /cd <project>"