@deadragdoll/tellymcp 0.0.14 → 0.0.15

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.
Files changed (44) hide show
  1. package/.env.example.client +19 -39
  2. package/.env.example.gateway +40 -64
  3. package/CHANGELOG.md +27 -0
  4. package/README-ru.md +66 -6
  5. package/README.md +66 -6
  6. package/TOOLS.md +38 -1
  7. package/config/templates/env.both.template +4 -5
  8. package/config/templates/env.client.template +4 -17
  9. package/config/templates/env.gateway.template +4 -29
  10. package/dist/cli.js +225 -47
  11. package/dist/configureServer.js +966 -0
  12. package/dist/envMigration.js +316 -0
  13. package/dist/moleculer.config.js +1 -3
  14. package/dist/services/features/telegram-mcp/approval.service.js +1 -1
  15. package/dist/services/features/telegram-mcp/collaboration.service.js +2 -2
  16. package/dist/services/features/telegram-mcp/ensuredb.service.js +1 -1
  17. package/dist/services/features/telegram-mcp/gateway.service.js +1 -1
  18. package/dist/services/features/telegram-mcp/mcp-server.service.js +2 -0
  19. package/dist/services/features/telegram-mcp/session-context.service.js +25 -1
  20. package/dist/services/features/telegram-mcp/src/app/bootstrap/runtime.js +70 -66
  21. package/dist/services/features/telegram-mcp/src/app/config/env.js +10 -36
  22. package/dist/services/features/telegram-mcp/src/app/config/environmentContract.js +66 -0
  23. package/dist/services/features/telegram-mcp/src/entities/request/model/schema.js +28 -2
  24. package/dist/services/features/telegram-mcp/src/features/browser/model/browserService.js +2 -2
  25. package/dist/services/features/telegram-mcp/src/features/collaboration/model/gatewaySessionsService.js +5 -5
  26. package/dist/services/features/telegram-mcp/src/features/distributed-client/model/gatewayClientAccess.js +1 -1
  27. package/dist/services/features/telegram-mcp/src/features/distributed-client/model/gatewayCollaborationBackend.js +4 -4
  28. package/dist/services/features/telegram-mcp/src/features/distributed-gateway/model/gatewayHttpService.js +0 -1
  29. package/dist/services/features/telegram-mcp/src/features/distributed-gateway/model/remoteConsoleActionClient.js +4 -3
  30. package/dist/services/features/telegram-mcp/src/features/notify/model/notifyService.js +4 -4
  31. package/dist/services/features/telegram-mcp/src/features/session-context/model/getRuntimeDiagnosticsTool.js +30 -0
  32. package/dist/services/features/telegram-mcp/src/features/session-context/model/sessionContextService.js +169 -7
  33. package/dist/services/features/telegram-mcp/src/shared/integrations/memory/processLocalStateStore.js +260 -0
  34. package/dist/services/features/telegram-mcp/src/shared/integrations/object-storage/minioExchangeStore.js +1 -1
  35. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transport.js +1 -1
  36. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportConsoleRegistry.js +2 -2
  37. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportMessageFlow.js +1 -1
  38. package/dist/services/features/telegram-mcp/src/shared/integrations/telegram/transportProjectState.js +2 -2
  39. package/dist/services/features/telegram-mcp/src/shared/lib/gatewayScope.js +5 -5
  40. package/dist/services/features/telegram-mcp/src/shared/lib/project-identity/projectIdentity.js +10 -0
  41. package/docs/STANDALONE-ru.md +29 -4
  42. package/docs/STANDALONE.md +29 -4
  43. package/package.json +1 -1
  44. package/scripts/postinstall.js +33 -1
@@ -0,0 +1,260 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ProcessLocalStateStore = void 0;
4
+ const redactSecrets_1 = require("../../lib/redact-secrets/redactSecrets");
5
+ function principalKey(principal) {
6
+ return `${principal.telegramChatId}:${principal.telegramUserId}`;
7
+ }
8
+ function xchangeKey(sessionId, filePath) {
9
+ return `${sessionId}\u0000${filePath}`;
10
+ }
11
+ class ProcessLocalStateStore {
12
+ pairCodes = new Map();
13
+ bindings = new Map();
14
+ principalSessions = new Map();
15
+ principalActiveSessions = new Map();
16
+ locales = new Map();
17
+ authorizedPrincipals = new Map();
18
+ requests = new Map();
19
+ requestQueue = [];
20
+ activeRequestId = null;
21
+ menuPayloads = new Map();
22
+ xchangeFileMetas = new Map();
23
+ browserAttachments = new Map();
24
+ browserRecordings = new Map();
25
+ projectMenuViews = new Map();
26
+ outgoingNotices = new Map();
27
+ gatewayClientUuid;
28
+ constructor(input = {}) {
29
+ this.gatewayClientUuid = input.gatewayClientUuid?.trim() || null;
30
+ this.onGatewayClientUuidChange = input.onGatewayClientUuidChange;
31
+ }
32
+ onGatewayClientUuidChange;
33
+ async createPairCode(record, ttlSeconds) {
34
+ const existing = this.readExpiring(this.pairCodes, record.code);
35
+ if (existing)
36
+ return false;
37
+ this.pairCodes.set(record.code, { value: record, expiresAt: Date.now() + ttlSeconds * 1000 });
38
+ return true;
39
+ }
40
+ async consumePairCode(code) {
41
+ const record = this.readExpiring(this.pairCodes, code);
42
+ this.pairCodes.delete(code);
43
+ return record;
44
+ }
45
+ async getBinding(sessionId) {
46
+ return this.bindings.get(sessionId) ?? null;
47
+ }
48
+ async setBinding(binding) {
49
+ const previous = this.bindings.get(binding.sessionId);
50
+ if (previous)
51
+ this.detachSession(previous, binding.sessionId);
52
+ this.bindings.set(binding.sessionId, binding);
53
+ const key = principalKey(binding);
54
+ const sessions = this.principalSessions.get(key) ?? new Set();
55
+ sessions.add(binding.sessionId);
56
+ this.principalSessions.set(key, sessions);
57
+ this.principalActiveSessions.set(key, binding.sessionId);
58
+ }
59
+ async clearBinding(sessionId) {
60
+ const binding = this.bindings.get(sessionId);
61
+ this.bindings.delete(sessionId);
62
+ if (binding)
63
+ this.detachSession(binding, sessionId);
64
+ }
65
+ async getActiveSessionIdForPrincipal(principal) {
66
+ return this.principalActiveSessions.get(principalKey(principal)) ?? null;
67
+ }
68
+ async getActiveSessionIdForTelegramUser(telegramUserId) {
69
+ for (const [sessionId, binding] of this.bindings) {
70
+ if (binding.telegramUserId === telegramUserId) {
71
+ return this.principalActiveSessions.get(principalKey(binding)) ?? sessionId;
72
+ }
73
+ }
74
+ return null;
75
+ }
76
+ async clearActiveSessionIdForPrincipal(principal) {
77
+ this.principalActiveSessions.delete(principalKey(principal));
78
+ }
79
+ async setActiveSessionIdForPrincipal(principal, sessionId) {
80
+ const key = principalKey(principal);
81
+ const sessions = this.principalSessions.get(key) ?? new Set();
82
+ sessions.add(sessionId);
83
+ this.principalSessions.set(key, sessions);
84
+ this.principalActiveSessions.set(key, sessionId);
85
+ }
86
+ async listBoundSessionIdsForPrincipal(principal) {
87
+ return Array.from(this.principalSessions.get(principalKey(principal)) ?? []);
88
+ }
89
+ async listBoundPrincipals() {
90
+ const principals = new Map();
91
+ for (const binding of this.bindings.values()) {
92
+ principals.set(principalKey(binding), {
93
+ telegramChatId: binding.telegramChatId,
94
+ telegramUserId: binding.telegramUserId,
95
+ });
96
+ }
97
+ return Array.from(principals.values());
98
+ }
99
+ async resetRuntimeState() {
100
+ this.activeRequestId = null;
101
+ this.requestQueue.length = 0;
102
+ }
103
+ async getActive() {
104
+ return this.activeRequestId ? this.requests.get(this.activeRequestId) ?? null : null;
105
+ }
106
+ async createPending(request) {
107
+ this.requests.set(request.requestId, request);
108
+ this.activeRequestId = request.requestId;
109
+ }
110
+ async updatePending(request) {
111
+ this.requests.set(request.requestId, request);
112
+ }
113
+ async resolvePending(requestId, resolution) {
114
+ const current = this.requests.get(requestId);
115
+ if (current) {
116
+ this.requests.set(requestId, {
117
+ ...current,
118
+ status: resolution.status,
119
+ ...(resolution.answer ? { answer: (0, redactSecrets_1.redactSecrets)(resolution.answer) } : {}),
120
+ ...(resolution.receivedAt ? { receivedAt: resolution.receivedAt } : {}),
121
+ ...(resolution.fallbackUsed ? { fallbackIfTimeout: resolution.fallbackUsed } : {}),
122
+ });
123
+ }
124
+ this.activeRequestId = null;
125
+ }
126
+ async enqueue(request) {
127
+ this.requests.set(request.requestId, request);
128
+ this.requestQueue.push(request.requestId);
129
+ }
130
+ async dequeueNext() {
131
+ const requestId = this.requestQueue.shift();
132
+ return requestId ? this.requests.get(requestId) ?? null : null;
133
+ }
134
+ async createMenuPayload(record, ttlSeconds) {
135
+ this.menuPayloads.set(record.key, { value: record, expiresAt: Date.now() + ttlSeconds * 1000 });
136
+ }
137
+ async getMenuPayload(key) {
138
+ return this.readExpiring(this.menuPayloads, key);
139
+ }
140
+ async getUserLocale(telegramUserId) {
141
+ return this.locales.get(telegramUserId) ?? null;
142
+ }
143
+ async setUserLocale(telegramUserId, locale) {
144
+ this.locales.set(telegramUserId, locale);
145
+ }
146
+ async isAdminAuthorized(principal) {
147
+ return this.authorizedPrincipals.has(principalKey(principal));
148
+ }
149
+ async setAdminAuthorized(principal) {
150
+ this.authorizedPrincipals.set(principalKey(principal), principal);
151
+ }
152
+ async clearAdminAuthorized(principal) {
153
+ this.authorizedPrincipals.delete(principalKey(principal));
154
+ }
155
+ async listAdminAuthorizedPrincipals() {
156
+ return Array.from(this.authorizedPrincipals.values());
157
+ }
158
+ async setXchangeFileMeta(meta) {
159
+ this.xchangeFileMetas.set(xchangeKey(meta.sessionId, meta.filePath), meta);
160
+ }
161
+ async listXchangeFileMetas(sessionId) {
162
+ return Array.from(this.xchangeFileMetas.values())
163
+ .filter((meta) => meta.sessionId === sessionId)
164
+ .sort((left, right) => right.uploadedAt.localeCompare(left.uploadedAt));
165
+ }
166
+ async getXchangeFileMeta(sessionId, filePath) {
167
+ return this.xchangeFileMetas.get(xchangeKey(sessionId, filePath)) ?? null;
168
+ }
169
+ async deleteXchangeFileMeta(sessionId, filePath) {
170
+ return this.xchangeFileMetas.delete(xchangeKey(sessionId, filePath));
171
+ }
172
+ async pruneAll() {
173
+ const deletedKeys = this.countEntries();
174
+ this.pairCodes.clear();
175
+ this.bindings.clear();
176
+ this.principalSessions.clear();
177
+ this.principalActiveSessions.clear();
178
+ this.locales.clear();
179
+ this.authorizedPrincipals.clear();
180
+ this.requests.clear();
181
+ this.requestQueue.length = 0;
182
+ this.activeRequestId = null;
183
+ this.menuPayloads.clear();
184
+ this.xchangeFileMetas.clear();
185
+ this.browserAttachments.clear();
186
+ this.browserRecordings.clear();
187
+ this.projectMenuViews.clear();
188
+ this.outgoingNotices.clear();
189
+ this.gatewayClientUuid = null;
190
+ return { deletedKeys };
191
+ }
192
+ async getGatewayClientUuid() { return this.gatewayClientUuid; }
193
+ async setGatewayClientUuid(clientUuid) {
194
+ this.gatewayClientUuid = clientUuid;
195
+ this.onGatewayClientUuidChange?.(clientUuid);
196
+ }
197
+ async getBrowserAttachment(sessionId) {
198
+ return this.browserAttachments.get(sessionId) ?? null;
199
+ }
200
+ async setBrowserAttachment(record) {
201
+ this.browserAttachments.set(record.sessionId, record);
202
+ }
203
+ async clearBrowserAttachment(sessionId) { this.browserAttachments.delete(sessionId); }
204
+ async getBrowserRecording(sessionId) {
205
+ return this.browserRecordings.get(sessionId) ?? null;
206
+ }
207
+ async setBrowserRecording(record) {
208
+ this.browserRecordings.set(record.sessionId, record);
209
+ }
210
+ async clearBrowserRecording(sessionId) { this.browserRecordings.delete(sessionId); }
211
+ async setProjectMenuViewState(state) {
212
+ this.projectMenuViews.set(xchangeKey(state.projectUuid, state.sessionId), state);
213
+ }
214
+ async listProjectMenuViewStates(projectUuid) {
215
+ return Array.from(this.projectMenuViews.values()).filter((state) => state.projectUuid === projectUuid);
216
+ }
217
+ async deleteProjectMenuViewState(sessionId, projectUuid) {
218
+ return this.projectMenuViews.delete(xchangeKey(projectUuid, sessionId));
219
+ }
220
+ async setOutgoingDeliveryNotice(notice) {
221
+ this.outgoingNotices.set(notice.deliveryUuid, notice);
222
+ }
223
+ async listOutgoingDeliveryNotices() {
224
+ return Array.from(this.outgoingNotices.values());
225
+ }
226
+ async deleteOutgoingDeliveryNotice(deliveryUuid) {
227
+ return this.outgoingNotices.delete(deliveryUuid);
228
+ }
229
+ readExpiring(store, key) {
230
+ const record = store.get(key);
231
+ if (!record)
232
+ return null;
233
+ if (record.expiresAt <= Date.now()) {
234
+ store.delete(key);
235
+ return null;
236
+ }
237
+ return record.value;
238
+ }
239
+ detachSession(principal, sessionId) {
240
+ const key = principalKey(principal);
241
+ const sessions = this.principalSessions.get(key);
242
+ sessions?.delete(sessionId);
243
+ if (!sessions?.size)
244
+ this.principalSessions.delete(key);
245
+ if (this.principalActiveSessions.get(key) === sessionId) {
246
+ const next = sessions?.values().next().value;
247
+ if (next)
248
+ this.principalActiveSessions.set(key, next);
249
+ else
250
+ this.principalActiveSessions.delete(key);
251
+ }
252
+ }
253
+ countEntries() {
254
+ return this.pairCodes.size + this.bindings.size + this.locales.size +
255
+ this.authorizedPrincipals.size + this.requests.size + this.menuPayloads.size +
256
+ this.xchangeFileMetas.size + this.browserAttachments.size + this.browserRecordings.size +
257
+ this.projectMenuViews.size + this.outgoingNotices.size + (this.gatewayClientUuid ? 1 : 0);
258
+ }
259
+ }
260
+ exports.ProcessLocalStateStore = ProcessLocalStateStore;
@@ -29,7 +29,7 @@ class MinioExchangeStore {
29
29
  terminalConfig;
30
30
  exchangeDirName;
31
31
  logger;
32
- constructor(_callBroker, _bindingStore, terminalConfig, exchangeDirName, _vfsScope, logger, _distributedMode = "client", _gatewayPublicUrl, _gatewayAuthToken) {
32
+ constructor(terminalConfig, exchangeDirName, logger) {
33
33
  this.terminalConfig = terminalConfig;
34
34
  this.exchangeDirName = exchangeDirName;
35
35
  this.logger = logger;
@@ -218,7 +218,7 @@ class TelegramTransport {
218
218
  }
219
219
  isAdminAuthEnabled() {
220
220
  return (this.isAdminBotProfile() &&
221
- Boolean(this.config.distributed.gatewayToken?.trim()));
221
+ Boolean(this.config.distributed.gatewayScopeToken?.trim()));
222
222
  }
223
223
  isAdminBotProfile() {
224
224
  return this.config.distributed.mode === "gateway";
@@ -26,8 +26,8 @@ class TransportConsoleRegistry {
26
26
  ...(input?.principal
27
27
  ? { telegram_user_id: input.principal.telegramUserId }
28
28
  : {}),
29
- ...(this.host.config.distributed.gatewayToken
30
- ? { gateway_token: this.host.config.distributed.gatewayToken }
29
+ ...(this.host.config.distributed.gatewayScopeToken
30
+ ? { gateway_token: this.host.config.distributed.gatewayScopeToken }
31
31
  : {}),
32
32
  });
33
33
  return Array.isArray(response.sessions) ? response.sessions : [];
@@ -73,7 +73,7 @@ class TransportMessageFlow {
73
73
  await this.host.replyText(ctx, await this.host.tForContext(ctx, "menu:admin.auth.disabled"), { sessionId: "gateway-auth", kind: "transport" });
74
74
  return true;
75
75
  }
76
- if (authToken !== this.host.config.distributed.gatewayToken) {
76
+ if (authToken !== this.host.config.distributed.gatewayScopeToken) {
77
77
  await this.host.replyText(ctx, await this.host.tForContext(ctx, "menu:admin.auth.invalid"), { sessionId: "gateway-auth", kind: "transport" });
78
78
  return true;
79
79
  }
@@ -46,8 +46,8 @@ class TransportProjectState {
46
46
  this.host.config.telegram.botUsername ||
47
47
  "telegram-mcp client",
48
48
  bot_username: this.host.config.telegram.botUsername,
49
- ...(this.host.config.distributed.gatewayToken
50
- ? { gateway_token: this.host.config.distributed.gatewayToken }
49
+ ...(this.host.config.distributed.gatewayScopeToken
50
+ ? { gateway_token: this.host.config.distributed.gatewayScopeToken }
51
51
  : {}),
52
52
  meta: {
53
53
  telegram_chat_id: principal.telegramChatId,
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.normalizeGatewayToken = normalizeGatewayToken;
4
- exports.gatewayTokenToScopeKey = gatewayTokenToScopeKey;
4
+ exports.gatewayScopeTokenToScopeKey = gatewayScopeTokenToScopeKey;
5
5
  exports.resolveGatewayScopeKey = resolveGatewayScopeKey;
6
6
  const node_crypto_1 = require("node:crypto");
7
7
  function normalizeGatewayToken(value) {
@@ -11,13 +11,13 @@ function normalizeGatewayToken(value) {
11
11
  const token = value.trim();
12
12
  return token ? token : null;
13
13
  }
14
- function gatewayTokenToScopeKey(token) {
14
+ function gatewayScopeTokenToScopeKey(token) {
15
15
  return (0, node_crypto_1.createHash)("sha256").update(token).digest("hex");
16
16
  }
17
17
  function resolveGatewayScopeKey(input) {
18
- const gatewayToken = normalizeGatewayToken(input.gateway_token);
19
- if (gatewayToken) {
20
- return gatewayTokenToScopeKey(gatewayToken);
18
+ const gatewayScopeToken = normalizeGatewayToken(input.gateway_token);
19
+ if (gatewayScopeToken) {
20
+ return gatewayScopeTokenToScopeKey(gatewayScopeToken);
21
21
  }
22
22
  const scopeKey = normalizeGatewayToken(input.scope_key);
23
23
  return scopeKey;
@@ -117,6 +117,10 @@ function readSessionMarkerState(inputCwd, logger) {
117
117
  parsed.last_notified_tools_hash.trim()
118
118
  ? { lastNotifiedToolsHash: parsed.last_notified_tools_hash.trim() }
119
119
  : {}),
120
+ ...(typeof parsed.gateway_client_uuid === "string" &&
121
+ parsed.gateway_client_uuid.trim()
122
+ ? { gatewayClientUuid: parsed.gateway_client_uuid.trim() }
123
+ : {}),
120
124
  ...(typeof parsed.updated_at === "string" && parsed.updated_at.trim()
121
125
  ? { updatedAt: parsed.updated_at.trim() }
122
126
  : {}),
@@ -166,6 +170,12 @@ function writeSessionMarkerState(input) {
166
170
  : current?.lastNotifiedToolsHash
167
171
  ? { last_notified_tools_hash: current.lastNotifiedToolsHash }
168
172
  : {}),
173
+ ...(typeof input.gatewayClientUuid === "string" &&
174
+ input.gatewayClientUuid.trim()
175
+ ? { gateway_client_uuid: input.gatewayClientUuid.trim() }
176
+ : current?.gatewayClientUuid
177
+ ? { gateway_client_uuid: current.gatewayClientUuid }
178
+ : {}),
169
179
  created_at: now,
170
180
  updated_at: now,
171
181
  ...(current ? { first_seen_local_session_id: current.localSessionId } : {}),
@@ -24,7 +24,17 @@
24
24
  ## 1. Установка
25
25
 
26
26
  ```bash
27
- npm install -g @deadragdoll/tellymcp
27
+ sudo apt install -y python3 make g++
28
+ npm config set ignore-scripts false
29
+ npm install -g @deadragdoll/tellymcp --foreground-scripts
30
+ ```
31
+
32
+ На Linux native addon `node-pty` собирается локально; для Linux ARM64 это
33
+ обязательно, потому что зависимость не поставляет подходящий готовый binary.
34
+ Если после предыдущей установки `pty.node` отсутствует, выполни:
35
+
36
+ ```bash
37
+ npm rebuild -g @deadragdoll/tellymcp --foreground-scripts
28
38
  ```
29
39
 
30
40
  Опционально:
@@ -64,7 +74,7 @@ DB_PASSWORD=
64
74
  DB_NAME=
65
75
  GATEWAY_PUBLIC_URL=https://your-domain.example/api/gateway
66
76
  GATEWAY_WS_URL=wss://your-domain.example/api/gateway/ws
67
- GATEWAY_TOKEN=change_me_gateway_token
77
+ GATEWAY_SCOPE_TOKEN=change_me_scope_token
68
78
  GATEWAY_AUTH_TOKEN=put_strong_shared_transport_token_here
69
79
  ROOT_PREFIX=/api
70
80
  PORT=8080
@@ -83,13 +93,16 @@ DISTRIBUTED_MODE=gateway
83
93
  DISTRIBUTED_MODE=client
84
94
  GATEWAY_PUBLIC_URL=https://your-domain.example/api/gateway
85
95
  GATEWAY_WS_URL=wss://your-domain.example/api/gateway/ws
86
- GATEWAY_TOKEN=change_me_gateway_token
96
+ GATEWAY_SCOPE_TOKEN=change_me_scope_token
87
97
  GATEWAY_AUTH_TOKEN=put_strong_shared_transport_token_here
88
98
  GATEWAY_USER_UUID=put_owner_uuid_here
89
99
  ```
90
100
 
101
+ На машине agent/client Redis не нужен. Временное runtime-состояние хранится
102
+ локально, а стабильный gateway client UUID — в `.mcpsession.json`.
103
+
91
104
  Используйте один и тот же стойкий `GATEWAY_AUTH_TOKEN` на gateway и всех клиентах.
92
- Не смешивайте его с `GATEWAY_TOKEN`: последний разделяет данные gateway по scope,
105
+ Не смешивайте его с `GATEWAY_SCOPE_TOKEN`: последний разделяет данные gateway по scope,
93
106
  но не аутентифицирует HTTP- или WebSocket-транспорт. Сгенерируйте токен один раз,
94
107
  например командой `openssl rand -hex 32`, и не используйте пример значения выше.
95
108
 
@@ -192,10 +205,22 @@ http://127.0.0.1:8787/mcp
192
205
 
193
206
  ## 9. Проверки
194
207
 
208
+ Настройка dotenv через локальный web-конфигуратор:
209
+
210
+ ```bash
211
+ tellymcp configure
212
+ ```
213
+
195
214
  ```bash
196
215
  tellymcp doctor --env .env
197
216
  ```
198
217
 
218
+ Перед запуском старый env можно нормализовать командой:
219
+
220
+ ```bash
221
+ tellymcp migrate-env ./old.env > ./.migrated-env
222
+ ```
223
+
199
224
  Разрушительная очистка:
200
225
 
201
226
  ```bash
@@ -24,7 +24,17 @@ The recommended topology is:
24
24
  ## 1. Install
25
25
 
26
26
  ```bash
27
- npm install -g @deadragdoll/tellymcp
27
+ sudo apt install -y python3 make g++
28
+ npm config set ignore-scripts false
29
+ npm install -g @deadragdoll/tellymcp --foreground-scripts
30
+ ```
31
+
32
+ Linux installs compile the native `node-pty` addon locally; this is required on
33
+ Linux ARM64 because the dependency does not ship a matching prebuilt binary.
34
+ If `pty.node` is missing after an earlier install, run:
35
+
36
+ ```bash
37
+ npm rebuild -g @deadragdoll/tellymcp --foreground-scripts
28
38
  ```
29
39
 
30
40
  Optional:
@@ -64,7 +74,7 @@ DB_PASSWORD=
64
74
  DB_NAME=
65
75
  GATEWAY_PUBLIC_URL=https://your-domain.example/api/gateway
66
76
  GATEWAY_WS_URL=wss://your-domain.example/api/gateway/ws
67
- GATEWAY_TOKEN=change_me_gateway_token
77
+ GATEWAY_SCOPE_TOKEN=change_me_scope_token
68
78
  GATEWAY_AUTH_TOKEN=put_strong_shared_transport_token_here
69
79
  ROOT_PREFIX=/api
70
80
  PORT=8080
@@ -83,13 +93,16 @@ Minimum client settings:
83
93
  DISTRIBUTED_MODE=client
84
94
  GATEWAY_PUBLIC_URL=https://your-domain.example/api/gateway
85
95
  GATEWAY_WS_URL=wss://your-domain.example/api/gateway/ws
86
- GATEWAY_TOKEN=change_me_gateway_token
96
+ GATEWAY_SCOPE_TOKEN=change_me_scope_token
87
97
  GATEWAY_AUTH_TOKEN=put_strong_shared_transport_token_here
88
98
  GATEWAY_USER_UUID=put_owner_uuid_here
89
99
  ```
90
100
 
101
+ The agent/client machine does not need Redis. Its transient runtime state is
102
+ local, while the stable gateway client UUID is stored in `.mcpsession.json`.
103
+
91
104
  Use the same strong `GATEWAY_AUTH_TOKEN` on the gateway and every client. Keep it
92
- separate from `GATEWAY_TOKEN`, which scopes gateway data but does not authenticate
105
+ separate from `GATEWAY_SCOPE_TOKEN`, which scopes gateway data but does not authenticate
93
106
  the HTTP or WebSocket transport. Generate it once, for example with
94
107
  `openssl rand -hex 32`, and do not use the illustrative value above.
95
108
 
@@ -192,10 +205,22 @@ Use the MCP HTTP endpoint exposed by `tellymcp run`.
192
205
 
193
206
  ## 9. Health Checks
194
207
 
208
+ Guided dotenv setup:
209
+
210
+ ```bash
211
+ tellymcp configure
212
+ ```
213
+
195
214
  ```bash
196
215
  tellymcp doctor --env .env
197
216
  ```
198
217
 
218
+ Normalize a legacy env before startup:
219
+
220
+ ```bash
221
+ tellymcp migrate-env ./old.env > ./.migrated-env
222
+ ```
223
+
199
224
  Destructive cleanup:
200
225
 
201
226
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deadragdoll/tellymcp",
3
- "version": "0.0.14",
3
+ "version": "0.0.15",
4
4
  "description": "TellyMCP - Telegram control plane for MCP-connected coding agents",
5
5
  "main": "dist/services/features/telegram-mcp/runtime.service.js",
6
6
  "bin": {
@@ -15,11 +15,43 @@ function line(value = "") {
15
15
  process.stdout.write(`${value}\n`);
16
16
  }
17
17
 
18
+ function checkNativePty() {
19
+ try {
20
+ const nodePty = require("node-pty");
21
+ return typeof nodePty.spawn === "function"
22
+ ? { available: true }
23
+ : {
24
+ available: false,
25
+ message: "node-pty loaded without a spawn function.",
26
+ };
27
+ } catch (error) {
28
+ const message = error instanceof Error ? error.message : String(error);
29
+ return {
30
+ available: false,
31
+ message: message.split("\n", 1)[0] || "node-pty failed to load.",
32
+ };
33
+ }
34
+ }
35
+
18
36
  line();
19
37
  line(`${pc.bold(pc.cyan("TellyMCP"))} ${pc.dim("installed")}`);
20
38
  line();
21
39
 
22
- line(`${pc.green("OK")} Built-in PTY terminal runtime is enabled.`);
40
+ const nativePty = checkNativePty();
41
+ if (nativePty.available) {
42
+ line(`${pc.green("OK")} Built-in PTY native module loaded.`);
43
+ } else {
44
+ line(`${pc.red("ERROR")} Built-in PTY native module is unavailable.`);
45
+ line(` ${nativePty.message}`);
46
+ line(
47
+ ` Platform: ${process.platform}-${process.arch}, Node ${process.versions.node}`,
48
+ );
49
+ if (process.platform === "linux") {
50
+ line(" Debian/Ubuntu prerequisites: sudo apt install -y python3 make g++");
51
+ }
52
+ line(" Ensure scripts are enabled: npm config set ignore-scripts false");
53
+ line(" Rebuild: npm rebuild -g @deadragdoll/tellymcp --foreground-scripts");
54
+ }
23
55
 
24
56
  line();
25
57
  line(`${pc.yellow("INFO")} Browser tools need Playwright browser binaries.`);