tgcloud-mcp 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +125 -2
- package/bin/tgcloud-mcp.mjs +2 -0
- package/dist/config.js +12 -0
- package/dist/index.js +31 -0
- package/dist/resources.js +49 -0
- package/dist/runner.js +111 -0
- package/dist/tools/data.js +63 -0
- package/dist/tools/lifecycle.js +77 -0
- package/dist/tools/sync.js +79 -0
- package/dist/tools/webhook.js +38 -0
- package/dist/utils.js +42 -0
- package/package.json +53 -4
- package/resources/docs/cli.md +85 -0
- package/resources/docs/project-structure.md +98 -0
- package/resources/docs/sdk-api.md +31 -0
- package/resources/docs/sdk-db.md +87 -0
- package/resources/docs/sdk-fetch.md +29 -0
- package/skills/tgcloud/SKILL.md +73 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alexander Khmara
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,126 @@
|
|
|
1
|
-
#
|
|
1
|
+
# tgcloud-mcp — MCP-сервер для Telegram serverless bots
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/sdamarketing/tgcloud_mcp/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/tgcloud-mcp)
|
|
5
|
+
[](https://github.com/sdamarketing/tgcloud_mcp/pkgs/container/tgcloud-mcp)
|
|
6
|
+
|
|
7
|
+
Учит AI-ассистента управлять serverless-ботами Telegram ([core.telegram.org/bots/serverless](https://core.telegram.org/bots/serverless)):
|
|
8
|
+
вы говорите ассистенту «создай бота», «задеплой», «покажи статус вебхука» — а он выполняет
|
|
9
|
+
это через `tgcloud` CLI, не трогая терминал руками.
|
|
10
|
+
|
|
11
|
+
Работает с любым MCP-клиентом: **opencode, Claude Code, Claude Desktop, Cursor, VS Code (Copilot), Windsurf, Zed**.
|
|
12
|
+
|
|
13
|
+
## Что умеет
|
|
14
|
+
|
|
15
|
+
14 инструментов — полный срез команд `tgcloud` CLI:
|
|
16
|
+
|
|
17
|
+
| Категория | Инструменты | Что делают |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| **Жизненный цикл** | `create_project`, `init_project`, `login_bot`, `add_module` | скаффолд проекта, привязка бота по токену @BotFather, новые хендлеры и lib-модули |
|
|
20
|
+
| **Синхронизация** | `status`, `diff`, `push`, `pull`, `fetch`, `reset` | просмотр изменений, атомарный деплой, синхронизация с облаком |
|
|
21
|
+
| **Данные** | `run_handler`, `migrate` | прогон хендлера без деплоя (с логами), миграции БД с dry-run |
|
|
22
|
+
| **Вебхук** | `webhook_status`, `webhook_sync` | диагностика «бот молчит», перенастройка вебхука |
|
|
23
|
+
|
|
24
|
+
Плюс MCP-**ресурсы** `tgcloud://docs/*` — встроенная справка платформы
|
|
25
|
+
(структура проекта, правила импортов, `sdk/db`, `sdk/api`, `sdk/fetch`, CLI).
|
|
26
|
+
|
|
27
|
+
## Безопасность
|
|
28
|
+
|
|
29
|
+
- **Деструктивное требует подтверждения.** `reset`, `push --force`, `webhook sync --drop-pending`
|
|
30
|
+
и применение миграций отказываются работать без `confirm: true` — ассистент сначала покажет,
|
|
31
|
+
что будет изменено, и спросит согласие.
|
|
32
|
+
- **Токены не утекают.** Токен бота передаётся в CLI через stdin и маскируется во всех выводах —
|
|
33
|
+
ассистент никогда его не видит в ответах инструментов.
|
|
34
|
+
|
|
35
|
+
## Установка
|
|
36
|
+
|
|
37
|
+
Требуется Node.js 20+ (CLI платформы tgcloud требует 18+).
|
|
38
|
+
|
|
39
|
+
**Из npm:**
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install -g tgcloud-mcp
|
|
43
|
+
tgcloud-mcp setup # мастер: настройка MCP-клиента
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**В одну команду (macOS / Linux / WSL)** — клон в `~/.tgcloud-mcp` + мастер:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
curl -fsSL https://raw.githubusercontent.com/sdamarketing/tgcloud_mcp/main/install.sh | bash
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**Docker:**
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
docker run -i --rm ghcr.io/sdamarketing/tgcloud-mcp
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Из исходников:**
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
git clone https://github.com/sdamarketing/tgcloud_mcp.git
|
|
62
|
+
cd tgcloud_mcp
|
|
63
|
+
npm install && npm test # build + smoke + e2e
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Подробно (токены, клиенты, переменные, решение проблем) — **[docs/SETUP.md](docs/SETUP.md)**.
|
|
67
|
+
|
|
68
|
+
## Подключение к агенту
|
|
69
|
+
|
|
70
|
+
Конфиг MCP-клиента (пример для opencode / Claude Code / Cursor — формат `mcpServers` одинаковый):
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"mcpServers": {
|
|
75
|
+
"tgcloud": {
|
|
76
|
+
"command": "tgcloud-mcp"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Переменные окружения (опционально):
|
|
83
|
+
|
|
84
|
+
| Переменная | По умолчанию | Назначение |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| `TGCLOUD_CLI` | `npx` | Команда запуска CLI (можно указать глобально установленный `tgcloud`) |
|
|
87
|
+
| `TGCLOUD_CLI_ARGS` | `tgcloud` | Аргументы-префикс перед субкомандой |
|
|
88
|
+
| `TGCLOUD_TIMEOUT_MS` | `120000` | Таймаут одной команды CLI |
|
|
89
|
+
|
|
90
|
+
## Пример диалога
|
|
91
|
+
|
|
92
|
+
> **Вы:** создай эхо-бота в ~/bots/echo
|
|
93
|
+
> **Агент:** `create_project` → `login_bot` (спросит токен у @BotFather) → правит `handlers/message.js` → `push` → `webhook_sync` — бот жив.
|
|
94
|
+
> **Вы:** напиши тест и проверь
|
|
95
|
+
> **Агент:** `run_handler` с payload `{ chat: { id: 1 }, text: "hi" }` — показывает вывод и логи.
|
|
96
|
+
|
|
97
|
+
## Кукбук
|
|
98
|
+
|
|
99
|
+
📗 **[Serverless-бот с мини-аппом за вечер](docs/cookbook-srl-bot.md)** — пошаговый
|
|
100
|
+
рецепт на примере реального бота «Дневник обучения»: скаффолд, токены, база и миграции,
|
|
101
|
+
inline-кнопки, Mini App с endpoints. Самоснятые форматы CLI и грабли включены.
|
|
102
|
+
|
|
103
|
+
## Скилл для агентов
|
|
104
|
+
|
|
105
|
+
В репозитории лежит скилл `skills/tgcloud` — процедурные знания для агента:
|
|
106
|
+
структура проекта, правила bare-импортов, рецепты (`create → login → run → push`),
|
|
107
|
+
guardrails платформы. Установка: скопируйте каталог в `~/.agents/skills/tgcloud`
|
|
108
|
+
или подключите через ваш менеджер скиллов.
|
|
109
|
+
|
|
110
|
+
## Структура репозитория
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
src/
|
|
114
|
+
├─ index.ts # сервер, регистрация инструментов/ресурсов
|
|
115
|
+
├─ config.ts # env-конфиг
|
|
116
|
+
├─ runner.ts # запуск tgcloud CLI: spawn, таймауты, маскировка секретов
|
|
117
|
+
├─ utils.ts # runTool/result-хелперы, dangerTool (confirm-guard)
|
|
118
|
+
└─ tools/ # lifecycle, sync, data, webhook
|
|
119
|
+
resources/docs/ # справка платформы → MCP-ресурсы
|
|
120
|
+
skills/tgcloud/ # скилл для AI-агентов
|
|
121
|
+
scripts/smoke-test.mjs # stdio smoke-тест
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Лицензия
|
|
125
|
+
|
|
126
|
+
MIT
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function loadConfig() {
|
|
2
|
+
const commandArgs = (process.env.TGCLOUD_CLI_ARGS ?? 'tgcloud')
|
|
3
|
+
.split(' ')
|
|
4
|
+
.map((s) => s.trim())
|
|
5
|
+
.filter(Boolean);
|
|
6
|
+
const timeoutMs = Number(process.env.TGCLOUD_TIMEOUT_MS ?? '120000');
|
|
7
|
+
return {
|
|
8
|
+
command: process.env.TGCLOUD_CLI ?? 'npx',
|
|
9
|
+
commandArgs,
|
|
10
|
+
timeoutMs: Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 120000,
|
|
11
|
+
};
|
|
12
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { McpServer } from '@modelcontextprotocol/server';
|
|
4
|
+
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
|
|
5
|
+
import { loadConfig } from './config.js';
|
|
6
|
+
import { TgcloudRunner } from './runner.js';
|
|
7
|
+
import { registerLifecycleTools } from './tools/lifecycle.js';
|
|
8
|
+
import { registerSyncTools } from './tools/sync.js';
|
|
9
|
+
import { registerDataTools } from './tools/data.js';
|
|
10
|
+
import { registerWebhookTools } from './tools/webhook.js';
|
|
11
|
+
import { registerDocResources } from './resources.js';
|
|
12
|
+
async function main() {
|
|
13
|
+
const config = loadConfig();
|
|
14
|
+
const runner = new TgcloudRunner(config);
|
|
15
|
+
const server = new McpServer({
|
|
16
|
+
name: 'tgcloud',
|
|
17
|
+
version: JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version,
|
|
18
|
+
});
|
|
19
|
+
registerLifecycleTools(server, runner);
|
|
20
|
+
registerSyncTools(server, runner);
|
|
21
|
+
registerDataTools(server, runner);
|
|
22
|
+
registerWebhookTools(server, runner);
|
|
23
|
+
registerDocResources(server);
|
|
24
|
+
const transport = new StdioServerTransport();
|
|
25
|
+
await server.connect(transport);
|
|
26
|
+
}
|
|
27
|
+
main().catch((cause) => {
|
|
28
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
29
|
+
process.stderr.write(`tgcloud-mcp failed to start: ${message}\n`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
const DOCS_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'resources', 'docs');
|
|
5
|
+
const DOCS = [
|
|
6
|
+
{
|
|
7
|
+
name: 'tgcloud-project-structure',
|
|
8
|
+
uri: 'tgcloud://docs/project-structure',
|
|
9
|
+
title: 'Структура проекта tgcloud и правила импортов',
|
|
10
|
+
file: 'project-structure.md',
|
|
11
|
+
description: 'handlers/, lib/, schema.js, .tgcloud/; только bare-импорты, без относительных путей и расширений',
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: 'tgcloud-cli',
|
|
15
|
+
uri: 'tgcloud://docs/cli',
|
|
16
|
+
title: 'Справочник tgcloud CLI',
|
|
17
|
+
file: 'cli.md',
|
|
18
|
+
description: 'init, login, add, run, status, diff, push, pull, fetch, reset, migrate, webhook, completion',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'tgcloud-sdk-db',
|
|
22
|
+
uri: 'tgcloud://docs/sdk-db',
|
|
23
|
+
title: 'sdk/db — база данных',
|
|
24
|
+
file: 'sdk-db.md',
|
|
25
|
+
description: 'schema.js, таблицы и модификаторы, query builder, insert/update/delete, raw SQL',
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'tgcloud-sdk-api',
|
|
29
|
+
uri: 'tgcloud://docs/sdk-api',
|
|
30
|
+
title: 'sdk/api — Telegram Bot API',
|
|
31
|
+
file: 'sdk-api.md',
|
|
32
|
+
description: 'Вызов любых методов Bot API, развёрнутый result, BotApiError',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'tgcloud-sdk-fetch',
|
|
36
|
+
uri: 'tgcloud://docs/sdk-fetch',
|
|
37
|
+
title: 'sdk/fetch — HTTP-клиент',
|
|
38
|
+
file: 'sdk-fetch.md',
|
|
39
|
+
description: 'fetch с body-хелперами (json/form/text), лимит 32 МБ, только текстовые ответы',
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
export function registerDocResources(server) {
|
|
43
|
+
for (const doc of DOCS) {
|
|
44
|
+
server.registerResource(doc.name, doc.uri, { title: doc.title, description: doc.description, mimeType: 'text/markdown' }, async (uri) => {
|
|
45
|
+
const text = await readFile(join(DOCS_DIR, doc.file), 'utf8');
|
|
46
|
+
return { contents: [{ uri: uri.href, text, mimeType: 'text/markdown' }] };
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
package/dist/runner.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { statSync } from 'node:fs';
|
|
3
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
4
|
+
import { maskSecrets } from './utils.js';
|
|
5
|
+
export class TgcloudError extends Error {
|
|
6
|
+
code;
|
|
7
|
+
stderr;
|
|
8
|
+
constructor(message, code, stderr) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.stderr = stderr;
|
|
12
|
+
this.name = 'TgcloudError';
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export class TgcloudRunner {
|
|
16
|
+
config;
|
|
17
|
+
constructor(config) {
|
|
18
|
+
this.config = config;
|
|
19
|
+
}
|
|
20
|
+
/** Проверяет project_dir: абсолютный путь к существующему каталогу. */
|
|
21
|
+
resolveProjectDir(dir) {
|
|
22
|
+
if (!isAbsolute(dir)) {
|
|
23
|
+
throw new Error(`project_dir должен быть абсолютным путём, получено: ${dir}`);
|
|
24
|
+
}
|
|
25
|
+
let st;
|
|
26
|
+
try {
|
|
27
|
+
st = statSync(dir);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
throw new Error(`Каталог не существует: ${dir}`);
|
|
31
|
+
}
|
|
32
|
+
if (!st.isDirectory()) {
|
|
33
|
+
throw new Error(`Не является каталогом: ${dir}`);
|
|
34
|
+
}
|
|
35
|
+
return resolve(dir);
|
|
36
|
+
}
|
|
37
|
+
/** Запуск произвольной команды (npm create и т.п.). */
|
|
38
|
+
exec(command, args, opts) {
|
|
39
|
+
return this.spawn(command, args, opts);
|
|
40
|
+
}
|
|
41
|
+
/** Запуск tgcloud CLI: tgcloud <args...> в каталоге проекта. */
|
|
42
|
+
tgcloud(args, opts) {
|
|
43
|
+
return this.spawn(this.config.command, [...this.config.commandArgs, ...args], opts);
|
|
44
|
+
}
|
|
45
|
+
/** Тот же запуск, но с ошибкой при ненулевом коде возврата. */
|
|
46
|
+
async tgcloudOrThrow(args, opts) {
|
|
47
|
+
const out = await this.tgcloud(args, opts);
|
|
48
|
+
const text = [out.stdout, out.stderr].filter(Boolean).join('\n').trim();
|
|
49
|
+
if (out.code !== 0) {
|
|
50
|
+
throw new TgcloudError(`tgcloud завершился с кодом ${out.code}:\n${text}`, out.code, out.stderr);
|
|
51
|
+
}
|
|
52
|
+
return text || '(команда выполнена, вывод пуст)';
|
|
53
|
+
}
|
|
54
|
+
spawn(command, args, opts) {
|
|
55
|
+
const timeoutMs = opts.timeoutMs ?? this.config.timeoutMs;
|
|
56
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
57
|
+
const child = spawn(command, args, {
|
|
58
|
+
cwd: opts.cwd,
|
|
59
|
+
env: { ...process.env, ...opts.env },
|
|
60
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
61
|
+
});
|
|
62
|
+
let stdout = '';
|
|
63
|
+
let stderr = '';
|
|
64
|
+
let settled = false;
|
|
65
|
+
const timer = setTimeout(() => {
|
|
66
|
+
if (!settled) {
|
|
67
|
+
settled = true;
|
|
68
|
+
child.kill('SIGKILL');
|
|
69
|
+
rejectPromise(new Error(`Таймаут команды (${timeoutMs} мс): ${command} ${args.join(' ')}`));
|
|
70
|
+
}
|
|
71
|
+
}, timeoutMs);
|
|
72
|
+
child.stdout.on('data', (d) => {
|
|
73
|
+
stdout += d.toString();
|
|
74
|
+
});
|
|
75
|
+
child.stderr.on('data', (d) => {
|
|
76
|
+
stderr += d.toString();
|
|
77
|
+
});
|
|
78
|
+
child.on('error', (err) => {
|
|
79
|
+
if (!settled) {
|
|
80
|
+
settled = true;
|
|
81
|
+
clearTimeout(timer);
|
|
82
|
+
rejectPromise(new Error(`Не удалось запустить ${command}: ${err.message}`));
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
child.on('close', (code) => {
|
|
86
|
+
if (!settled) {
|
|
87
|
+
settled = true;
|
|
88
|
+
clearTimeout(timer);
|
|
89
|
+
resolvePromise({
|
|
90
|
+
code: code ?? -1,
|
|
91
|
+
stdout: maskSecrets(stdout),
|
|
92
|
+
stderr: maskSecrets(stderr),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
if (opts.input !== undefined) {
|
|
97
|
+
child.stdin.write(opts.input);
|
|
98
|
+
}
|
|
99
|
+
child.stdin.end();
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
/** Форматирует вывод команды в читаемый текст для агента. */
|
|
103
|
+
format(out) {
|
|
104
|
+
const parts = [`exit code: ${out.code}`];
|
|
105
|
+
if (out.stdout.trim())
|
|
106
|
+
parts.push(`--- stdout ---\n${out.stdout.trim()}`);
|
|
107
|
+
if (out.stderr.trim())
|
|
108
|
+
parts.push(`--- stderr ---\n${out.stderr.trim()}`);
|
|
109
|
+
return parts.join('\n');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as z from 'zod/v4';
|
|
2
|
+
import { dangerTool, runTool } from '../utils.js';
|
|
3
|
+
const projectDir = z
|
|
4
|
+
.string()
|
|
5
|
+
.describe('Абсолютный путь к каталогу проекта tgcloud (содержит .tgcloud/)');
|
|
6
|
+
const confirm = z
|
|
7
|
+
.boolean()
|
|
8
|
+
.optional()
|
|
9
|
+
.describe('Обязательный confirm: true для применения миграций, удаляющих данные (deprecated-колонки/таблицы)');
|
|
10
|
+
export function registerDataTools(server, runner) {
|
|
11
|
+
server.registerTool('run_handler', {
|
|
12
|
+
title: 'Прогнать хендлер без деплоя',
|
|
13
|
+
description: 'Выполняет модуль на платформе c локальными файлами, без деплоя (tgcloud run). ' +
|
|
14
|
+
'Быстрый способ проверить хендлер: payload и контекст — в формате JSON5. ' +
|
|
15
|
+
'Возвращает вывод и захваченные console-логи.',
|
|
16
|
+
inputSchema: z.object({
|
|
17
|
+
project_dir: projectDir,
|
|
18
|
+
module: z.string().describe('Путь модуля, например handlers/message'),
|
|
19
|
+
args: z
|
|
20
|
+
.string()
|
|
21
|
+
.optional()
|
|
22
|
+
.describe('Аргумент-хендлера в JSON5, например \'{ chat: { id: 1 }, text: "hi" }\''),
|
|
23
|
+
ctx: z.string().optional().describe('Контекст выполнения в JSON5 (передаётся как --ctx)'),
|
|
24
|
+
}),
|
|
25
|
+
}, async (args) => runTool(async () => {
|
|
26
|
+
const cwd = runner.resolveProjectDir(args.project_dir);
|
|
27
|
+
const cliArgs = ['run', args.module];
|
|
28
|
+
if (args.args !== undefined)
|
|
29
|
+
cliArgs.push(args.args);
|
|
30
|
+
if (args.ctx !== undefined)
|
|
31
|
+
cliArgs.push('--ctx', args.ctx);
|
|
32
|
+
return runner.format(await runner.tgcloud(cliArgs, { cwd }));
|
|
33
|
+
}));
|
|
34
|
+
server.registerTool('migrate', {
|
|
35
|
+
title: 'Миграции базы данных',
|
|
36
|
+
description: 'Применяет изменения schema.js к базе (tgcloud migrate). ' +
|
|
37
|
+
'По умолчанию dry_run=true — только показывает diff без изменений (--dry-run). ' +
|
|
38
|
+
'Применение (dry_run=false) требует confirm: true и идёт неинтерактивно через --yes: ' +
|
|
39
|
+
'если в схеме есть .deprecated(...)-колонки/таблицы, данные будут удалены безвозвратно. ' +
|
|
40
|
+
'В интерактивном терминале migrate без флагов спрашивает по каждому изменению ' +
|
|
41
|
+
'([y]es / [n]o / [q]uit); для агента используй только --dry-run и --yes.',
|
|
42
|
+
inputSchema: z.object({
|
|
43
|
+
project_dir: projectDir,
|
|
44
|
+
dry_run: z
|
|
45
|
+
.boolean()
|
|
46
|
+
.optional()
|
|
47
|
+
.describe('true (по умолчанию) — только предпросмотр изменений (--dry-run)'),
|
|
48
|
+
confirm,
|
|
49
|
+
}),
|
|
50
|
+
}, async (args) => {
|
|
51
|
+
if (args.dry_run !== false) {
|
|
52
|
+
return runTool(async () => {
|
|
53
|
+
const cwd = runner.resolveProjectDir(args.project_dir);
|
|
54
|
+
return runner.format(await runner.tgcloud(['migrate', '--dry-run'], { cwd }));
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return dangerTool(args, async () => {
|
|
58
|
+
const cwd = runner.resolveProjectDir(args.project_dir);
|
|
59
|
+
const out = await runner.tgcloud(['migrate', '--yes'], { cwd });
|
|
60
|
+
return runner.format(out);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import * as z from 'zod/v4';
|
|
2
|
+
import { runTool } from '../utils.js';
|
|
3
|
+
const projectDir = z
|
|
4
|
+
.string()
|
|
5
|
+
.describe('Абсолютный путь к каталогу проекта tgcloud (содержит .tgcloud/)');
|
|
6
|
+
export function registerLifecycleTools(server, runner) {
|
|
7
|
+
server.registerTool('create_project', {
|
|
8
|
+
title: 'Создать проект бота',
|
|
9
|
+
description: 'Скаффолдит новый проект serverless-бота через `npm create @tgcloud/bot <name>` ' +
|
|
10
|
+
'внутри каталога parent_dir. Требуется Node.js 18+ и доступ к npm registry.',
|
|
11
|
+
inputSchema: z.object({
|
|
12
|
+
parent_dir: z.string().describe('Абсолютный путь к каталогу, где будет создан проект'),
|
|
13
|
+
name: z
|
|
14
|
+
.string()
|
|
15
|
+
.regex(/^[a-z0-9][a-z0-9._-]*$/i, 'Имя проекта: латиница, цифры, точки, - и _')
|
|
16
|
+
.describe('Имя каталога нового проекта, например my-bot'),
|
|
17
|
+
}),
|
|
18
|
+
}, async (args) => runTool(async () => {
|
|
19
|
+
const parent = runner.resolveProjectDir(args.parent_dir);
|
|
20
|
+
const out = await runner.exec('npm', ['create', '--yes', '@tgcloud/bot', args.name], {
|
|
21
|
+
cwd: parent,
|
|
22
|
+
});
|
|
23
|
+
return runner.format(out);
|
|
24
|
+
}));
|
|
25
|
+
server.registerTool('init_project', {
|
|
26
|
+
title: 'Инициализировать пустой проект',
|
|
27
|
+
description: 'Скаффолдит проект tgcloud в существующем (обычно пустом) каталоге: schema.js, handlers/, lib/. Работает офлайн.',
|
|
28
|
+
inputSchema: z.object({ project_dir: projectDir }),
|
|
29
|
+
}, async (args) => runTool(async () => {
|
|
30
|
+
const cwd = runner.resolveProjectDir(args.project_dir);
|
|
31
|
+
return runner.format(await runner.tgcloud(['init'], { cwd }));
|
|
32
|
+
}));
|
|
33
|
+
server.registerTool('login_bot', {
|
|
34
|
+
title: 'Привязать бота к проекту',
|
|
35
|
+
description: 'Выполняет `tgcloud login`: привязывает бота к проекту по CLI access token ' +
|
|
36
|
+
'(формат app<id>:<secret>; @BotFather → ваш бот → Serverless → CLI Access → Access token). ' +
|
|
37
|
+
'Это НЕ API-токен бота вида 123456:AA…. ' +
|
|
38
|
+
'CLI требует интерактивный терминал, поэтому токен передаётся через stdin с ' +
|
|
39
|
+
'TGCLOUD_ASSUME_TTY=1; во всех выводах токен маскируется. ' +
|
|
40
|
+
'НИКОГДА не показывай токен пользователю после вызова. ' +
|
|
41
|
+
'Для CI вместо login достаточно переменной окружения TGCLOUD_TOKEN на shell.',
|
|
42
|
+
inputSchema: z.object({
|
|
43
|
+
project_dir: projectDir,
|
|
44
|
+
token: z
|
|
45
|
+
.string()
|
|
46
|
+
.regex(/^app\d+:[A-Za-z0-9_-]+$/, 'Ожидается CLI access token формата app<id>:<secret>')
|
|
47
|
+
.describe('CLI access token из @BotFather (Serverless → CLI Access), формат app<id>:<secret>'),
|
|
48
|
+
}),
|
|
49
|
+
}, async (args) => runTool(async () => {
|
|
50
|
+
const cwd = runner.resolveProjectDir(args.project_dir);
|
|
51
|
+
const out = await runner.tgcloud(['login'], {
|
|
52
|
+
cwd,
|
|
53
|
+
input: `${args.token}\n`,
|
|
54
|
+
env: { TGCLOUD_ASSUME_TTY: '1' },
|
|
55
|
+
});
|
|
56
|
+
return runner.format(out);
|
|
57
|
+
}));
|
|
58
|
+
server.registerTool('add_module', {
|
|
59
|
+
title: 'Добавить модуль',
|
|
60
|
+
description: 'Скаффолдит модуль через `tgcloud add <target>` (в tgcloud/-layout). ' +
|
|
61
|
+
'handlers/<type> — хендлер апдейтов (плоско, один уровень; типы: message, callback_query, ' +
|
|
62
|
+
'inline_query, chat_member, my_chat_member и др.); endpoints/<name> — серверная функция ' +
|
|
63
|
+
'Mini App (POST /api/<name>); lib/<path> — общий модуль (вложенные пути допускаются). ' +
|
|
64
|
+
'Существующие файлы не перезаписываются.',
|
|
65
|
+
inputSchema: z.object({
|
|
66
|
+
project_dir: projectDir,
|
|
67
|
+
target: z
|
|
68
|
+
.string()
|
|
69
|
+
.regex(/^(handlers|endpoints|lib)\/[a-z0-9._/-]+$/i, 'target: handlers/<type>, endpoints/<name> или lib/<path>')
|
|
70
|
+
.describe('Путь модуля, например handlers/callback_query, endpoints/getProfile или lib/cart'),
|
|
71
|
+
}),
|
|
72
|
+
}, async (args) => runTool(async () => {
|
|
73
|
+
const cwd = runner.resolveProjectDir(args.project_dir);
|
|
74
|
+
const out = await runner.tgcloud(['add', args.target], { cwd });
|
|
75
|
+
return runner.format(out);
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import * as z from 'zod/v4';
|
|
2
|
+
import { dangerTool, runTool } from '../utils.js';
|
|
3
|
+
const projectDir = z
|
|
4
|
+
.string()
|
|
5
|
+
.describe('Абсолютный путь к каталогу проекта tgcloud (содержит .tgcloud/)');
|
|
6
|
+
const confirm = z
|
|
7
|
+
.boolean()
|
|
8
|
+
.optional()
|
|
9
|
+
.describe('Обязательный confirm: true для деструктивных операций (флаг --force, reset и т.п.)');
|
|
10
|
+
export function registerSyncTools(server, runner) {
|
|
11
|
+
server.registerTool('status', {
|
|
12
|
+
title: 'Статус проекта',
|
|
13
|
+
description: 'Показывает локальные изменения относительно задеплоенной версии (tgcloud status). Работает офлайн.',
|
|
14
|
+
inputSchema: z.object({ project_dir: projectDir }),
|
|
15
|
+
}, async (args) => runTool(async () => {
|
|
16
|
+
const cwd = runner.resolveProjectDir(args.project_dir);
|
|
17
|
+
return runner.format(await runner.tgcloud(['status'], { cwd }));
|
|
18
|
+
}));
|
|
19
|
+
server.registerTool('diff', {
|
|
20
|
+
title: 'Построчный diff',
|
|
21
|
+
description: 'Построчное сравнение локального проекта с задеплоенной версией (tgcloud diff). Работает офлайн.',
|
|
22
|
+
inputSchema: z.object({ project_dir: projectDir }),
|
|
23
|
+
}, async (args) => runTool(async () => {
|
|
24
|
+
const cwd = runner.resolveProjectDir(args.project_dir);
|
|
25
|
+
const out = await runner.tgcloud(['diff'], { cwd });
|
|
26
|
+
return out.stdout.trim() || '(изменений нет)';
|
|
27
|
+
}));
|
|
28
|
+
server.registerTool('push', {
|
|
29
|
+
title: 'Деплой в облако',
|
|
30
|
+
description: 'Загружает изменённые модули в облако атомарным батчем (tgcloud push). ' +
|
|
31
|
+
'Без files — зеркалирует всё локальное состояние. ' +
|
|
32
|
+
'force=true (пропуск проверок конкурентности) — деструктивно, требует confirm: true.',
|
|
33
|
+
inputSchema: z.object({
|
|
34
|
+
project_dir: projectDir,
|
|
35
|
+
files: z
|
|
36
|
+
.array(z.string())
|
|
37
|
+
.optional()
|
|
38
|
+
.describe('Конкретные файлы/каталоги для деплоя; без них — всё изменённое'),
|
|
39
|
+
force: z.boolean().optional().describe('Передать --force (опасно, требует confirm: true)'),
|
|
40
|
+
confirm,
|
|
41
|
+
}),
|
|
42
|
+
}, async (args) => {
|
|
43
|
+
const exec = async () => {
|
|
44
|
+
const cwd = runner.resolveProjectDir(args.project_dir);
|
|
45
|
+
const cliArgs = ['push'];
|
|
46
|
+
if (args.files?.length)
|
|
47
|
+
cliArgs.push(...args.files);
|
|
48
|
+
if (args.force)
|
|
49
|
+
cliArgs.push('--force');
|
|
50
|
+
return runner.format(await runner.tgcloud(cliArgs, { cwd }));
|
|
51
|
+
};
|
|
52
|
+
return args.force ? dangerTool(args, exec) : runTool(exec);
|
|
53
|
+
});
|
|
54
|
+
server.registerTool('pull', {
|
|
55
|
+
title: 'Синхронизировать из облака',
|
|
56
|
+
description: 'Приводит локальный проект к состоянию облака: обновляет референс-копию и рабочие файлы (tgcloud pull).',
|
|
57
|
+
inputSchema: z.object({ project_dir: projectDir }),
|
|
58
|
+
}, async (args) => runTool(async () => {
|
|
59
|
+
const cwd = runner.resolveProjectDir(args.project_dir);
|
|
60
|
+
return runner.format(await runner.tgcloud(['pull'], { cwd }));
|
|
61
|
+
}));
|
|
62
|
+
server.registerTool('fetch', {
|
|
63
|
+
title: 'Обновить референс-копию',
|
|
64
|
+
description: 'Обновляет локальную референс-копию задеплоенного состояния, НЕ трогая рабочие файлы (tgcloud fetch). Полезно перед разбором конфликтов.',
|
|
65
|
+
inputSchema: z.object({ project_dir: projectDir }),
|
|
66
|
+
}, async (args) => runTool(async () => {
|
|
67
|
+
const cwd = runner.resolveProjectDir(args.project_dir);
|
|
68
|
+
return runner.format(await runner.tgcloud(['fetch'], { cwd }));
|
|
69
|
+
}));
|
|
70
|
+
server.registerTool('reset', {
|
|
71
|
+
title: 'Откатить локальные изменения',
|
|
72
|
+
description: 'ДЕСТРУКТИВНО: отбрасывает все локальные изменения и восстанавливает рабочий каталог ' +
|
|
73
|
+
'к последнему известному состоянию облака (tgcloud reset). Требует confirm: true.',
|
|
74
|
+
inputSchema: z.object({ project_dir: projectDir, confirm }),
|
|
75
|
+
}, async (args) => dangerTool(args, async () => {
|
|
76
|
+
const cwd = runner.resolveProjectDir(args.project_dir);
|
|
77
|
+
return runner.format(await runner.tgcloud(['reset'], { cwd, input: 'y\n' }));
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as z from 'zod/v4';
|
|
2
|
+
import { dangerTool, runTool } from '../utils.js';
|
|
3
|
+
const projectDir = z
|
|
4
|
+
.string()
|
|
5
|
+
.describe('Абсолютный путь к каталогу проекта tgcloud (содержит .tgcloud/)');
|
|
6
|
+
export function registerWebhookTools(server, runner) {
|
|
7
|
+
server.registerTool('webhook_status', {
|
|
8
|
+
title: 'Статус вебхука',
|
|
9
|
+
description: 'Показывает текущее состояние вебхука бота (tgcloud webhook): URL, allowed_updates, ' +
|
|
10
|
+
'накопившиеся апдейты и ошибки доставки. Первый шаг диагностики «бот молчит».',
|
|
11
|
+
inputSchema: z.object({ project_dir: projectDir }),
|
|
12
|
+
}, async (args) => runTool(async () => {
|
|
13
|
+
const cwd = runner.resolveProjectDir(args.project_dir);
|
|
14
|
+
return runner.format(await runner.tgcloud(['webhook'], { cwd }));
|
|
15
|
+
}));
|
|
16
|
+
server.registerTool('webhook_sync', {
|
|
17
|
+
title: 'Перенастроить вебхук',
|
|
18
|
+
description: 'Перенаправляет вебхук на проект и пересобирает allowed_updates (tgcloud webhook sync). ' +
|
|
19
|
+
'drop_pending=true отбрасывает накопившиеся апдейты — деструктивно, требует confirm: true.',
|
|
20
|
+
inputSchema: z.object({
|
|
21
|
+
project_dir: projectDir,
|
|
22
|
+
drop_pending: z
|
|
23
|
+
.boolean()
|
|
24
|
+
.optional()
|
|
25
|
+
.describe('Отбросить накопившиеся апдейты (--drop-pending, требует confirm: true)'),
|
|
26
|
+
confirm: z.boolean().optional().describe('Обязательный confirm: true при drop_pending=true'),
|
|
27
|
+
}),
|
|
28
|
+
}, async (args) => {
|
|
29
|
+
const exec = async () => {
|
|
30
|
+
const cwd = runner.resolveProjectDir(args.project_dir);
|
|
31
|
+
const cliArgs = ['webhook', 'sync'];
|
|
32
|
+
if (args.drop_pending)
|
|
33
|
+
cliArgs.push('--drop-pending');
|
|
34
|
+
return runner.format(await runner.tgcloud(cliArgs, { cwd }));
|
|
35
|
+
};
|
|
36
|
+
return args.drop_pending ? dangerTool(args, exec) : runTool(exec);
|
|
37
|
+
});
|
|
38
|
+
}
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Маскирует секреты (токены ботов и т.п.) в тексте, который уходит агенту. */
|
|
2
|
+
export function maskSecrets(text) {
|
|
3
|
+
return text
|
|
4
|
+
// Токен Telegram-бота: 123456789:AA...
|
|
5
|
+
.replace(/\b\d{6,12}:[A-Za-z0-9_-]{30,}\b/g, '***')
|
|
6
|
+
// Пары вида token=... / "token": "..." (длинные значения)
|
|
7
|
+
.replace(/((?:token|secret|password|authorization)["'\s:=]+)[^\s"',]{12,}/gi, '$1***');
|
|
8
|
+
}
|
|
9
|
+
export function jsonResult(data) {
|
|
10
|
+
const text = maskSecrets(data === undefined ? 'null' : JSON.stringify(data, null, 2));
|
|
11
|
+
return { content: [{ type: 'text', text }] };
|
|
12
|
+
}
|
|
13
|
+
export function textResult(text) {
|
|
14
|
+
return { content: [{ type: 'text', text: maskSecrets(text) }] };
|
|
15
|
+
}
|
|
16
|
+
export function errorResult(message) {
|
|
17
|
+
return { content: [{ type: 'text', text: maskSecrets(message) }], isError: true };
|
|
18
|
+
}
|
|
19
|
+
export async function runTool(fn) {
|
|
20
|
+
try {
|
|
21
|
+
const data = await fn();
|
|
22
|
+
return typeof data === 'string' ? textResult(data) : jsonResult(data);
|
|
23
|
+
}
|
|
24
|
+
catch (cause) {
|
|
25
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
26
|
+
return errorResult(message);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Guard для деструктивных операций: без confirm: true отказывает.
|
|
31
|
+
* Агент обязан показать пользователю, что будет изменено, получить явное
|
|
32
|
+
* согласие и только потом повторить вызов с confirm=true.
|
|
33
|
+
*/
|
|
34
|
+
export async function dangerTool(args, fn) {
|
|
35
|
+
return runTool(async () => {
|
|
36
|
+
if (args.confirm !== true) {
|
|
37
|
+
throw new Error('Отказано: деструктивная операция. Покажите пользователю, что именно будет изменено, ' +
|
|
38
|
+
'получите явное подтверждение и повторите вызов с confirm=true.');
|
|
39
|
+
}
|
|
40
|
+
return fn();
|
|
41
|
+
});
|
|
42
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,55 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tgcloud-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "MCP server for Telegram serverless bots (@tgcloud) — project lifecycle, deploys, migrations and webhooks through the tgcloud CLI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"tgcloud-mcp": "bin/tgcloud-mcp.mjs"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=20"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"start": "node dist/index.js",
|
|
16
|
+
"typecheck": "tsc --noEmit",
|
|
17
|
+
"smoke": "node scripts/smoke-test.mjs",
|
|
18
|
+
"setup": "node scripts/setup.mjs",
|
|
19
|
+
"links": "node scripts/generate-install-links.mjs",
|
|
20
|
+
"docs:check": "node scripts/check-docs-drift.mjs",
|
|
21
|
+
"update": "node scripts/self-update.mjs",
|
|
22
|
+
"test": "npm run build && npm run smoke && node tests/e2e.mjs",
|
|
23
|
+
"prepublishOnly": "npm run test"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@modelcontextprotocol/server": "^2.1.0",
|
|
27
|
+
"zod": "^4.2.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^24.0.0",
|
|
31
|
+
"typescript": "^5.7.0"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"bin",
|
|
36
|
+
"resources",
|
|
37
|
+
"skills",
|
|
38
|
+
"README.md",
|
|
39
|
+
"LICENSE"
|
|
40
|
+
],
|
|
41
|
+
"keywords": [
|
|
42
|
+
"mcp",
|
|
43
|
+
"telegram",
|
|
44
|
+
"bots",
|
|
45
|
+
"serverless",
|
|
46
|
+
"tgcloud"
|
|
47
|
+
],
|
|
48
|
+
"license": "MIT",
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://github.com/sdamarketing/tgcloud_mcp.git"
|
|
52
|
+
},
|
|
53
|
+
"homepage": "https://github.com/sdamarketing/tgcloud_mcp",
|
|
54
|
+
"bugs": "https://github.com/sdamarketing/tgcloud_mcp/issues"
|
|
55
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# tgcloud CLI — справочник
|
|
2
|
+
|
|
3
|
+
Требуется Node.js 18+. CLI — локальная dev-зависимость проекта (`@tgcloud/cli`),
|
|
4
|
+
вызов из каталога проекта: `npx tgcloud <cmd>`.
|
|
5
|
+
|
|
6
|
+
## Старт
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm create @tgcloud/bot my-bot # скаффолд (CLI ставится в проект)
|
|
10
|
+
npx tgcloud init # скаффолд в текущем каталоге (офлайн)
|
|
11
|
+
npx tgcloud login # привязка бота — ИНТЕРАКТИВНО (нужен TTY)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
**Токен** — CLI access token формата `app<id>:<secret>`
|
|
15
|
+
(@BotFather → ваш бот → Serverless → CLI Access → Access token).
|
|
16
|
+
Это НЕ Bot API токен вида `123456:AA…` — он будет отвергнут.
|
|
17
|
+
|
|
18
|
+
Неинтерактивный логин:
|
|
19
|
+
```bash
|
|
20
|
+
printf 'app...\n' | TGCLOUD_ASSUME_TTY=1 npx tgcloud login
|
|
21
|
+
# или для CI: export TGCLOUD_TOKEN='app...' (тогда login не нужен вообще)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Модули
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx tgcloud add handlers/callback_query # хендлер апдейта
|
|
28
|
+
npx tgcloud add endpoints/getProfile # endpoint для Mini App
|
|
29
|
+
npx tgcloud add lib/cart # общий модуль
|
|
30
|
+
npx tgcloud add handlers # без имени — покажет ошибку-подсказку
|
|
31
|
+
```
|
|
32
|
+
Существующие файлы не перезаписываются.
|
|
33
|
+
|
|
34
|
+
## Запуск без деплоя (server-side, локальные файлы)
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npx tgcloud run handlers/message '{ chat: { id: 1 }, text: "hi" }'
|
|
38
|
+
npx tgcloud run handlers/message "$(cat args.json5)"
|
|
39
|
+
npx tgcloud run endpoints/getProfile '{}' --ctx '{ initData: { user: { id: 1 } } }'
|
|
40
|
+
```
|
|
41
|
+
Аргументы/контекст — JSON5. Вывод `console.*` захватывается и отображается.
|
|
42
|
+
|
|
43
|
+
## Деплой и синхронизация
|
|
44
|
+
|
|
45
|
+
**Деплой никогда не трогает базу** — миграции отдельным шагом.
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx tgcloud status # локальные изменения vs снапшот (офлайн)
|
|
49
|
+
npx tgcloud diff [file] # построчный diff (офлайн)
|
|
50
|
+
npx tgcloud push [files..] # атомарный деплой модулей (+ static-билд мини-аппа);
|
|
51
|
+
# сообщит о pending-изменениях БД; обновляет вебхук
|
|
52
|
+
npx tgcloud push --force # пропустить проверки конкурентности (ОПАСНО)
|
|
53
|
+
npx tgcloud fetch # обновить локальный снапшот из облака (файлы не трогает)
|
|
54
|
+
npx tgcloud pull # облако → снапшот + рабочие файлы
|
|
55
|
+
npx tgcloud reset [file] # отбросить локальные изменения (офлайн, из снапшота)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## База данных
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
npx tgcloud push # сначала деплой schema.js; сообщит о pending-изменениях
|
|
62
|
+
npx tgcloud migrate # применить изменения (ИНТЕРАКТИВНО)
|
|
63
|
+
npx tgcloud migrate --dry-run # предпросмотр без применения
|
|
64
|
+
```
|
|
65
|
+
Удаление — только через `.deprecated('reason')` на колонке/таблице/индексе;
|
|
66
|
+
удаление декларации ничего не дропает. Смена типа колонки — руками через `db.run()`.
|
|
67
|
+
**Foreign keys нет** — `.references()`/`foreignKey()` бросают при декларации.
|
|
68
|
+
|
|
69
|
+
## Вебхук
|
|
70
|
+
|
|
71
|
+
Платформа сама управляет вебхуком (из задеплоенных handlers/*) и обновляет его на push.
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npx tgcloud webhook # состояние и синхронность
|
|
75
|
+
npx tgcloud webhook sync [--drop-pending] # починить рассинхрон
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Прочее
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npx tgcloud upgrade # миграция layout проектов pre-0.2.0 → tgcloud/
|
|
82
|
+
npx tgcloud completion <bash|zsh|fish>
|
|
83
|
+
TG_CLOUD_API_URL=… # переопределение базового URL API
|
|
84
|
+
TGCLOUD_BETA=1 # /beta-окружение API
|
|
85
|
+
```
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Структура проекта tgcloud
|
|
2
|
+
|
|
3
|
+
Скаффолд (`npm create @tgcloud/bot my-bot`, @tgcloud/cli 0.2.x):
|
|
4
|
+
|
|
5
|
+
```plaintext
|
|
6
|
+
my-bot/
|
|
7
|
+
├─ tgcloud/ # весь платформенный код — деплоится только он
|
|
8
|
+
│ ├─ handlers/ # хендлеры апдейтов — плоско, один уровень
|
|
9
|
+
│ │ ├─ message.js
|
|
10
|
+
│ │ └─ callback_query.js
|
|
11
|
+
│ ├─ endpoints/ # серверные функции для Mini App (POST /api/<name>)
|
|
12
|
+
│ ├─ lib/ # общие модули; подкаталоги разрешены
|
|
13
|
+
│ │ └─ internal/util.js
|
|
14
|
+
│ └─ schema.js # схема БД — один файл
|
|
15
|
+
├─ tgcloud.jsonc # конфиг проекта (static-блок для Mini App)
|
|
16
|
+
├─ docs/tgcloud-sdk.md # полный SDK-референс (в скаффолде)
|
|
17
|
+
├─ AGENTS.md # ориентация для AI-агентов (в скаффолде)
|
|
18
|
+
└─ .tgcloud/ # состояние CLI: креды, снапшот, кэш (gitignored)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Деплоятся только `.js` под `tgcloud/` (`schema.js`, `lib/`, `handlers/`, `endpoints/`)
|
|
22
|
+
плюс static-билд мини-аппа, если указан в `tgcloud.jsonc`. Имя модуля — путь
|
|
23
|
+
внутри `tgcloud/` без расширения: `tgcloud/handlers/message.js` → `handlers/message`.
|
|
24
|
+
|
|
25
|
+
## Правила импортов
|
|
26
|
+
|
|
27
|
+
**Проектные модули — относительным путём С расширением `.js`; SDK — по имени.**
|
|
28
|
+
|
|
29
|
+
```javascript
|
|
30
|
+
// из tgcloud/handlers/message.js:
|
|
31
|
+
import { users } from '../schema.js'; // ✅
|
|
32
|
+
import { addItem } from '../lib/cart.js'; // ✅
|
|
33
|
+
import { users } from '../schema'; // ❌ без .js — не компилируется
|
|
34
|
+
import x from '../../src/x.js'; // ❌ вне tgcloud/ ничего недоступно
|
|
35
|
+
import { db, api, fetch, BotApiError } from 'sdk'; // ✅ SDK по имени
|
|
36
|
+
import { table, integer, text, eq, sql } from 'sdk/db'; // ✅ подмодули SDK
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Исключение: имя модуля без расширения (`from 'lib/cart'`) тоже резолвится, но
|
|
40
|
+
пишите относительные импорты — это канонический стиль платформы.
|
|
41
|
+
|
|
42
|
+
В рантайме — V8 isolate: **нет файловой системы, нет npm-пакетов**.
|
|
43
|
+
Доступны только `sdk` и модули проекта.
|
|
44
|
+
|
|
45
|
+
## Хендлеры (handlers/)
|
|
46
|
+
|
|
47
|
+
Файл по имени типа апдейта (`message`, `callback_query`, `inline_query`,
|
|
48
|
+
`chat_member`, `my_chat_member`, …). `export default` вызывается с payload
|
|
49
|
+
соответствующего типа; второй аргумент — `ctx` (полный `Update` — `ctx.update`):
|
|
50
|
+
|
|
51
|
+
```javascript
|
|
52
|
+
import { api } from 'sdk';
|
|
53
|
+
|
|
54
|
+
export default async function (message, ctx) {
|
|
55
|
+
await api.sendMessage({
|
|
56
|
+
chat_id: message.chat.id,
|
|
57
|
+
text: `You said: ${message.text ?? '(no text)'}`,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Endpoints (endpoints/) — серверная сторона Mini App
|
|
63
|
+
|
|
64
|
+
`tgcloud/endpoints/<name>.js` вызывается мини-аппом как `POST /api/<name>`:
|
|
65
|
+
|
|
66
|
+
```javascript
|
|
67
|
+
import { EndpointError } from 'sdk';
|
|
68
|
+
|
|
69
|
+
export default async function (input, ctx) {
|
|
70
|
+
// input — JSON-тело (объект); возвращаемое значение уйдёт как JSON.
|
|
71
|
+
// ctx.initData — проверенные платформой init data; ctx.initData.user — вызывающий.
|
|
72
|
+
if (!ctx.initData.user) throw new EndpointError('Unauthorized', { code: 'AUTH' });
|
|
73
|
+
return { ok: true };
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
- `EndpointError(description, { code })` → 400 с description/parameters;
|
|
78
|
+
любое другое исключение → безликий 500.
|
|
79
|
+
- Вызов из фронта: `Telegram.WebApp.Serverless.call('<name>', input, (err, result) => …)`
|
|
80
|
+
из официального `telegram-web-app.js` (сам шлёт init data).
|
|
81
|
+
- Имена: буквы, цифры, `_`, не с цифры. Скаффолд: `tgcloud add endpoints/<name>`.
|
|
82
|
+
Тест: `tgcloud run endpoints/<name> '{}' --ctx '{ initData: { user: { id: 1 } } }'`.
|
|
83
|
+
|
|
84
|
+
## Mini App (фронтенд)
|
|
85
|
+
|
|
86
|
+
- `tgcloud.jsonc` → `{ "static": { "source": "dist" } }` — build-папка деплоится и
|
|
87
|
+
хостится как Mini App на `https://app<app_id>.tgcloud.ai/` (`push` печатает точный URL).
|
|
88
|
+
Опции: `"spa": true`, `immutable`, `headers`, `redirects`; шорткат `"static": "dist"`;
|
|
89
|
+
`"static": false` — убрать сайт при следующем push.
|
|
90
|
+
- **Собирай перед push** — push заливает папку как есть, build не запускает.
|
|
91
|
+
- Фронт — обычный браузерный код с npm; недоступен из модулей tgcloud/ и наоборот.
|
|
92
|
+
Общается с ботом только через endpoints.
|
|
93
|
+
- Не добавляй `X-Frame-Options`/строгий `frame-ancestors` — Mini App открывается в iframe.
|
|
94
|
+
|
|
95
|
+
## Логирование
|
|
96
|
+
|
|
97
|
+
`console.log/info/warn/error/trace`; вывод захватывается при `tgcloud run`,
|
|
98
|
+
`error`/`trace` со стектрейсом, строки помечаются `[file:line]`.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# sdk/api — Telegram Bot API
|
|
2
|
+
|
|
3
|
+
Прямой доступ ко всему Telegram Bot API: `api.<method>(params)`.
|
|
4
|
+
Поддерживаются все текущие и будущие методы без обновления SDK.
|
|
5
|
+
|
|
6
|
+
```javascript
|
|
7
|
+
import { api, BotApiError } from 'sdk'; // или from 'sdk/api'
|
|
8
|
+
|
|
9
|
+
const me = await api.getMe(); // результат развёрнут из { ok, result }
|
|
10
|
+
await api.sendMessage({ chat_id: id, text: 'Hello!' });
|
|
11
|
+
await api.editMessageText({ chat_id, message_id, text: 'Updated' });
|
|
12
|
+
await api.answerCallbackQuery({ callback_query_id, text: 'Done' });
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
- Параметры — snake_case Bot API (`chat_id`, `message_id`, `reply_markup`, …).
|
|
16
|
+
- Ответ разворачивается из envelope `result`.
|
|
17
|
+
- Ошибки бросают `BotApiError` с полями `.code`, `.description`, `.method`, `.parameters`.
|
|
18
|
+
|
|
19
|
+
```javascript
|
|
20
|
+
import { api, BotApiError } from 'sdk';
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
await api.deleteMessage({ chat_id, message_id });
|
|
24
|
+
} catch (e) {
|
|
25
|
+
if (e instanceof BotApiError && e.code === 400) {
|
|
26
|
+
// 400 — сообщение уже удалено; ок
|
|
27
|
+
} else {
|
|
28
|
+
throw e;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# sdk/db — база данных
|
|
2
|
+
|
|
3
|
+
SQLite-подобная БД. Всё асинхронно — всегда `await`.
|
|
4
|
+
|
|
5
|
+
## Схема (schema.js, один файл в корне)
|
|
6
|
+
|
|
7
|
+
```javascript
|
|
8
|
+
import { table, integer, text, boolean, json, index, unique, check, sql } from 'sdk/db';
|
|
9
|
+
|
|
10
|
+
export const users = table('users', {
|
|
11
|
+
id: integer('id').primaryKey({ autoIncrement: true }),
|
|
12
|
+
tgId: integer('tg_id').unique(),
|
|
13
|
+
name: text('name').notNull(),
|
|
14
|
+
lang: text('lang').default('en'),
|
|
15
|
+
isAdmin: boolean('is_admin').default(false),
|
|
16
|
+
prefs: json('prefs'),
|
|
17
|
+
created: integer('created_at', { mode: 'timestamp' }).default(sql`(unixepoch())`),
|
|
18
|
+
}, (t) => ({
|
|
19
|
+
createdIdx: index('idx_users_created').on(t.created),
|
|
20
|
+
uqEmail: unique('uq_email').on(t.email),
|
|
21
|
+
chk: check('chk_done', sql`${t.done} in (0, 1)`),
|
|
22
|
+
lower: index('idx_lower').on(sql`lower(${t.email})`), // expression index
|
|
23
|
+
active: index('idx_active').on(t.userId).where(sql`done = 0`), // partial index
|
|
24
|
+
}));
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Модификаторы колонок: `.primaryKey({autoIncrement})`, `.notNull()`, `.unique()`,
|
|
28
|
+
`.default(v)`, `.generatedAlwaysAs(sql\`...\`, { mode: 'stored' | 'virtual' })`,
|
|
29
|
+
`.deprecated('reason')` — пометить на удаление (применится следующим `migrate` как warning-drop).
|
|
30
|
+
|
|
31
|
+
Удалить таблицу: `table(...).deprecated('unused')`. Дропы происходят ТОЛЬКО через
|
|
32
|
+
`.deprecated()` — удаление декларации из schema.js ничего не дропает.
|
|
33
|
+
Смена типа колонки не автоматическая — вручную через `db.run(...)`.
|
|
34
|
+
|
|
35
|
+
**Foreign keys нет**: `.references()`/`foreignKey()` бросают при декларации
|
|
36
|
+
(рантайм работает с FK off) — целостность обеспечивает код приложения.
|
|
37
|
+
|
|
38
|
+
**Деплой не трогает базу**: `push` только сообщает о pending-изменениях,
|
|
39
|
+
применение — отдельным `tgcloud migrate` (интерактивно).
|
|
40
|
+
|
|
41
|
+
## Запросы
|
|
42
|
+
|
|
43
|
+
```javascript
|
|
44
|
+
import { db } from 'sdk';
|
|
45
|
+
import { todos } from 'schema';
|
|
46
|
+
import { eq, and, desc, asc, count, sql } from 'sdk/db';
|
|
47
|
+
|
|
48
|
+
await db.select().from(todos).all(); // все строки
|
|
49
|
+
await db.select().from(todos).where(eq(todos.id, 1)).get();// первая или null
|
|
50
|
+
await db.select().from(todos).values(); // массивы значений
|
|
51
|
+
await db.select({ id: todos.id, n: count() })
|
|
52
|
+
.from(todos).groupBy(todos.userId).having(sql`count(*) > ${1}`).all();
|
|
53
|
+
await db.$count(todos); // count(*)
|
|
54
|
+
await db.$count(todos, eq(todos.done, false)); // count с фильтром
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Цепочки: `.where(c1, c2)` (AND), `.orderBy(desc(t.x), asc(t.id))`, `.limit(n)`, `.offset(n)`,
|
|
58
|
+
`.groupBy(col)`, `.having(cond)`, `.distinct()`.
|
|
59
|
+
|
|
60
|
+
Операторы из `sdk/db`: `eq ne gt gte lt lte like notLike isNull isNotNull and or not
|
|
61
|
+
between notBetween inArray notInArray count sum avg min max asc desc sql`.
|
|
62
|
+
|
|
63
|
+
## Insert / Update / Delete
|
|
64
|
+
|
|
65
|
+
```javascript
|
|
66
|
+
await db.insert(todos).values({ userId: 1, text: 'Buy milk' }).run();
|
|
67
|
+
await db.insert(todos).values([{ text: 'A' }, { text: 'B' }]).run(); // батч
|
|
68
|
+
await db.insert(todos).values({ text: 'X' }).returning().run(); // вернуть строку
|
|
69
|
+
await db.insert(users).values({ tgId: 42, name: 'Ann' })
|
|
70
|
+
.onConflictDoUpdate({ target: users.tgId, set: { name: 'Ann' } }).run(); // upsert
|
|
71
|
+
await db.update(todos).set({ done: true }).where(eq(todos.id, 1)).run();
|
|
72
|
+
await db.delete(todos).where(eq(todos.id, 1)).run();
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Raw SQL
|
|
76
|
+
|
|
77
|
+
```javascript
|
|
78
|
+
await db.run('UPDATE todos SET done = 1 WHERE id = :id', { ':id': 5 }); // запись
|
|
79
|
+
await db.all(sql`SELECT * FROM todos WHERE done = ${false}`); // несколько строк
|
|
80
|
+
await db.get(sql`SELECT count(*) AS c FROM todos`); // одна строка
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Тег `sql` биндит `${value}` как параметр, `${table.column}` — как экранированный
|
|
84
|
+
идентификатор; `sql.raw('...')` — литерал без биндинга. Вложенные фрагменты `sql` склеиваются.
|
|
85
|
+
|
|
86
|
+
> Raw-запросы не привязаны к таблицам: boolean → 0/1, json → строка, timestamp → число.
|
|
87
|
+
> Конверсия значений работает только в table-bound билдере.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# sdk/fetch — HTTP-клиент
|
|
2
|
+
|
|
3
|
+
Fetch-подобный клиент для внешних API и вебхуков. Ограничения: ответ только
|
|
4
|
+
текстовый (бинарные payload не поддерживаются), лимит ответа — 32 МБ.
|
|
5
|
+
|
|
6
|
+
```javascript
|
|
7
|
+
import { fetch } from 'sdk'; // или from 'sdk/fetch'
|
|
8
|
+
|
|
9
|
+
const res = await fetch('https://api.example.com/users', {
|
|
10
|
+
method: 'POST',
|
|
11
|
+
headers: { 'Content-Type': 'application/json' },
|
|
12
|
+
body: JSON.stringify({ name: 'Pavel' }),
|
|
13
|
+
});
|
|
14
|
+
if (!res.ok) throw new Error(res.statusText);
|
|
15
|
+
const data = await res.json();
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Body-хелперы (выставляют Content-Type)
|
|
19
|
+
|
|
20
|
+
```javascript
|
|
21
|
+
await fetch(url, { method: 'POST', body: fetch.body.json({ a: 1 }) }); // application/json
|
|
22
|
+
await fetch(url, { method: 'POST', body: fetch.body.form({ a: 1 }) }); // x-www-form-urlencoded
|
|
23
|
+
await fetch(url, { method: 'POST', body: fetch.body.text('hi') }); // text/plain
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Ответ
|
|
27
|
+
|
|
28
|
+
- `res.status` (number), `res.statusText`, `res.ok` (200–299), `res.url`, `res.headers`
|
|
29
|
+
- `res.json()`, `res.text()`, `res.body` (ReadableStream — поточное чтение)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tgcloud
|
|
3
|
+
description: Управление serverless-ботами Telegram (@tgcloud) через tgcloud-mcp — скаффолд, токены, хендлеры, деплой, миграции БД, вебхуки. Use when the user asks to create/deploy/debug a Telegram serverless bot, mentions tgcloud, @tgcloud/bot, or works in a project with handlers/ + lib/ + schema.js.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Skill: tgcloud
|
|
7
|
+
|
|
8
|
+
Все операции — через MCP-инструменты сервера `tgcloud` (репозиторий tgcloud_mcp).
|
|
9
|
+
Если инструментов нет — предложи установить: `npm install -g tgcloud-mcp` и добавить
|
|
10
|
+
в конфиг MCP-клиента `{"mcpServers": {"tgcloud": {"command": "tgcloud-mcp"}}}`.
|
|
11
|
+
|
|
12
|
+
Встроенная справка платформы доступна как MCP-ресурсы `tgcloud://docs/*` —
|
|
13
|
+
читай ДО написания кода бота.
|
|
14
|
+
|
|
15
|
+
## Правила платформы (коротко)
|
|
16
|
+
|
|
17
|
+
- Структура: весь платформенный код в `tgcloud/` — `handlers/` (плоско, по типу
|
|
18
|
+
апдейта), `endpoints/` (серверные функции Mini App), `lib/` (можно вложенно),
|
|
19
|
+
`schema.js`. Рядом `tgcloud.jsonc`, `AGENTS.md`, `docs/tgcloud-sdk.md`.
|
|
20
|
+
`.tgcloud/` (креды/кэш) не коммитить.
|
|
21
|
+
- Импорты: проектные модули — ОТНОСИТЕЛЬНЫМ путём С расширением `.js`
|
|
22
|
+
(`import { users } from '../schema.js'`); SDK — по имени
|
|
23
|
+
(`import { api, db, fetch, BotApiError } from 'sdk'`). Без .js у относительного
|
|
24
|
+
импорта — НЕ компилируется. Вне tgcloud/ недоступно ничего.
|
|
25
|
+
- SDK: `api` — весь Bot API (ошибки — `BotApiError` с `.code`/`.description`);
|
|
26
|
+
`db` — query builder + raw SQL; `fetch` — текстовый HTTP, лимит 32 МБ;
|
|
27
|
+
`EndpointError` — корректный отказ endpoint'а (400).
|
|
28
|
+
- Всё асинхронно, всегда `await`. Логи — `console.*` (видны при `run_handler`).
|
|
29
|
+
- БД: без foreign keys; дропы только через `.deprecated('reason')`;
|
|
30
|
+
`push` не трогает БД — миграции отдельным шагом.
|
|
31
|
+
- Токен для `login_bot` — CLI access token формата `app<id>:<secret>`
|
|
32
|
+
(@BotFather → бот → Serverless → CLI Access), НЕ Bot API токен.
|
|
33
|
+
|
|
34
|
+
## Рецепты
|
|
35
|
+
|
|
36
|
+
### Новый бот с нуля
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
create_project { parent_dir, name } # npm create @tgcloud/bot
|
|
40
|
+
→ спроси у пользователя CLI-токен из @BotFather
|
|
41
|
+
login_bot { project_dir, token } # токен никуда не выводим
|
|
42
|
+
→ add_module / правим handlers/message.js, schema.js
|
|
43
|
+
run_handler { project_dir, module: 'handlers/message', args: '{ chat: { id: 1 }, text: "hi" }' }
|
|
44
|
+
push { project_dir }
|
|
45
|
+
migrate { project_dir } # dry_run=true по умолчанию — предпросмотр
|
|
46
|
+
webhook_sync { project_dir } # бот жив
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### «Бот молчит»
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
webhook_status { project_dir } # URL, allowed_updates, pending, ошибки доставки
|
|
53
|
+
status / diff { project_dir } # что не задеплоено
|
|
54
|
+
push { project_dir } # доставить изменения
|
|
55
|
+
webhook_sync { project_dir } # перепривязать вебхук
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Изменение схемы БД
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
1. правим schema.js
|
|
62
|
+
2. push { project_dir } # деплой схемы
|
|
63
|
+
3. migrate { project_dir } # dry-run предпросмотр
|
|
64
|
+
4. показать пользователю изменения; если есть .deprecated(...) — предупредить об удалении данных
|
|
65
|
+
5. migrate { project_dir, dry_run: false, confirm: true }
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Guardrails (жёстко)
|
|
69
|
+
|
|
70
|
+
- `reset`, `push force`, `webhook_sync drop_pending`, `migrate dry_run:false` —
|
|
71
|
+
ТОЛЬКО с `confirm: true` после явного согласия пользователя.
|
|
72
|
+
- Токен бота никогда не печатаем, не коммитим, не кладём в файлы — только в `login_bot`.
|
|
73
|
+
- Не придумывай команды CLI — список в ресурсе `tgcloud://docs/cli`.
|