@deadragdoll/tellymcp 0.0.14 → 0.0.16

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 +31 -0
  4. package/README-ru.md +67 -6
  5. package/README.md +67 -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 +232 -53
  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 +30 -4
  42. package/docs/STANDALONE.md +30 -4
  43. package/package.json +1 -1
  44. package/scripts/postinstall.js +36 -1
@@ -19,8 +19,7 @@ REDIS_HOST=127.0.0.1
19
19
  REDIS_PORT=6379
20
20
  REDIS_DB=1
21
21
 
22
- MODE=reject
23
- PAIR_CODE_TTL_SECONDS=300
22
+ TELEGRAM_REQUEST_MODE=reject
24
23
 
25
24
  MCP_HTTP_HOST=127.0.0.1
26
25
  MCP_HTTP_PORT=8787
@@ -47,7 +46,7 @@ DISTRIBUTED_MODE=both
47
46
  GATEWAY_PUBLIC_URL=
48
47
  GATEWAY_WS_URL=
49
48
  GATEWAY_WS_PATH=/api/gateway/ws
50
- GATEWAY_TOKEN=
49
+ GATEWAY_SCOPE_TOKEN=
51
50
  GATEWAY_USER_UUID=
52
51
  GATEWAY_AUTH_TOKEN=
53
52
 
@@ -61,7 +60,7 @@ DB_PORT=5432
61
60
  DB_USER=
62
61
  DB_PASSWORD=
63
62
  DB_NAME=
64
- DB_SCHEME=mcp
63
+ DB_SCHEMA=mcp
65
64
 
66
65
  # RMQ_HOST=127.0.0.1
67
66
  # RMQ_PORT=5672
@@ -113,4 +112,4 @@ BROWSER_SLOW_MO_MS=0
113
112
  LOG_LEVEL=info
114
113
  LOG_FILE_ENABLED=false
115
114
  LOG_FILE_PATH=.tellymcp/log.jsonl
116
- ENABLE_LOGFEED=0
115
+ LOGFEED_ENABLED=0
@@ -4,12 +4,8 @@
4
4
  # TELLYMCP_SESSION_ID=NEW
5
5
  # TELLYMCP_SESSION_LABEL=NEW
6
6
 
7
- REDIS_HOST=127.0.0.1
8
- REDIS_PORT=6379
9
- REDIS_DB=1
10
-
11
- MODE=reject
12
- PAIR_CODE_TTL_SECONDS=300
7
+ # Client state is process-local. Redis is gateway-only.
8
+ TELEGRAM_REQUEST_MODE=reject
13
9
 
14
10
  MCP_HTTP_HOST=127.0.0.1
15
11
  MCP_HTTP_PORT=8787
@@ -22,7 +18,7 @@ DISTRIBUTED_MODE=client
22
18
  GATEWAY_PUBLIC_URL=
23
19
  GATEWAY_WS_URL=
24
20
  GATEWAY_WS_PATH=/api/gateway/ws
25
- GATEWAY_TOKEN=
21
+ GATEWAY_SCOPE_TOKEN=
26
22
  GATEWAY_USER_UUID=
27
23
  GATEWAY_AUTH_TOKEN=
28
24
 
@@ -44,15 +40,6 @@ TERMINAL_PROMPT_SCAN_INTERVAL_SECONDS=15
44
40
  TERMINAL_PROMPT_SCAN_COOLDOWN_SECONDS=120
45
41
  TERMINAL_PROMPT_SCAN_STRATEGY=strict
46
42
  TERMINAL_PROMPT_SCAN_MIN_SCORE=5
47
- WEBAPP_ENABLED=true
48
- WEBAPP_BASE_PATH=/webapp
49
- WEBAPP_PUBLIC_URL=
50
- WEBAPP_INITDATA_TTL_SECONDS=300
51
- WEBAPP_SESSION_TTL_SECONDS=900
52
- WEBAPP_LAUNCH_MODE=fullscreen
53
- WEBAPP_VISIBLE_SCREENS=10
54
- WEBAPP_ACTION_COOLDOWN_MS=150
55
-
56
43
  MCP_XCHANGE_DIR=.mcp-xchange
57
44
 
58
45
  BROWSER_ENABLED=true
@@ -76,4 +63,4 @@ NODE_ID=agent
76
63
  LOG_LEVEL=info
77
64
  LOG_FILE_ENABLED=false
78
65
  LOG_FILE_PATH=.tellymcp/log.jsonl
79
- ENABLE_LOGFEED=0
66
+ LOGFEED_ENABLED=0
@@ -15,8 +15,7 @@ REDIS_HOST=127.0.0.1
15
15
  REDIS_PORT=6379
16
16
  REDIS_DB=1
17
17
 
18
- MODE=reject
19
- PAIR_CODE_TTL_SECONDS=300
18
+ TELEGRAM_REQUEST_MODE=reject
20
19
 
21
20
  MCP_HTTP_HOST=127.0.0.1
22
21
  MCP_HTTP_PORT=8787
@@ -43,7 +42,7 @@ DISTRIBUTED_MODE=gateway
43
42
  GATEWAY_PUBLIC_URL=
44
43
  GATEWAY_WS_URL=
45
44
  GATEWAY_WS_PATH=/api/gateway/ws
46
- GATEWAY_TOKEN=
45
+ GATEWAY_SCOPE_TOKEN=
47
46
  GATEWAY_AUTH_TOKEN=
48
47
 
49
48
  ROOT_PREFIX=/api
@@ -56,7 +55,7 @@ DB_PORT=5432
56
55
  DB_USER=
57
56
  DB_PASSWORD=
58
57
  DB_NAME=
59
- DB_SCHEME=mcp
58
+ DB_SCHEMA=mcp
60
59
 
61
60
  # RMQ_HOST=127.0.0.1
62
61
  # RMQ_PORT=5672
@@ -76,36 +75,12 @@ WEBAPP_ACTION_COOLDOWN_MS=150
76
75
 
77
76
  MCP_XCHANGE_DIR=.mcp-xchange
78
77
 
79
- TERMINAL_SHELL=bash
80
- TERMINAL_COLS=120
81
- TERMINAL_ROWS=40
82
- TERMINAL_SCROLLBACK_LINES=4000
83
-
84
- TERMINAL_NUDGE_ENABLED=true
85
- TERMINAL_NUDGE_DEBOUNCE_SECONDS=10
86
- TERMINAL_NUDGE_COOLDOWN_SECONDS=30
87
- TERMINAL_NUDGE_MESSAGE=получи newest telegram_message через list_xchange_records/get_xchange_record, выполни запрос и ответь человеку через notify_telegram; не останавливайся на анализе
88
- TERMINAL_PARTNER_NUDGE_MESSAGE=получи newest partner_note через list_xchange_records/get_xchange_record, выполни задачу в текущей консоли и обязательно отправь результат через send_partner_note или send_partner_file; только потом mark_xchange_record_read
89
- TERMINAL_PARTNER_REPLY_NUDGE_MESSAGE=получи newest partner_note через list_xchange_records/get_xchange_record; если newest note имеет kind=reply или не требует ответа, не отправляй новый send_partner_note/send_partner_file без явного нового запроса. Если этот reply завершает задачу от человека, сразу отправь финальный результат в Telegram через notify_telegram или send_file_to_telegram; только потом mark_xchange_record_read
90
- TERMINAL_CAPTURE_MODE=visible
91
- TERMINAL_CAPTURE_LINES=300
92
78
  TERMINAL_PROMPT_SCAN_ENABLED=false
93
79
  TERMINAL_PROMPT_SCAN_INTERVAL_SECONDS=15
94
80
  TERMINAL_PROMPT_SCAN_COOLDOWN_SECONDS=120
95
81
  TERMINAL_PROMPT_SCAN_STRATEGY=strict
96
82
  TERMINAL_PROMPT_SCAN_MIN_SCORE=5
97
- BROWSER_ENABLED=false
98
- BROWSER_HEADLESS=false
99
- BROWSER_DEVTOOLS=false
100
- # BROWSER_ADDRESS=http://localhost:5173
101
- BROWSER_TIMEOUT_MS=20000
102
- BROWSER_MAX_EVENTS=200
103
- BROWSER_WAIT_UNTIL=load
104
- # BROWSER_EXECUTABLE_PATH=
105
- # BROWSER_CHANNEL=chrome
106
- BROWSER_SLOW_MO_MS=0
107
-
108
83
  LOG_LEVEL=info
109
84
  LOG_FILE_ENABLED=false
110
85
  LOG_FILE_PATH=.tellymcp/log.jsonl
111
- ENABLE_LOGFEED=0
86
+ LOGFEED_ENABLED=0
package/dist/cli.js CHANGED
@@ -16,10 +16,45 @@ const ws_1 = __importDefault(require("ws"));
16
16
  const codexPluginInstaller_1 = require("./codexPluginInstaller");
17
17
  const versionHandshake_1 = require("./services/features/telegram-mcp/src/shared/lib/version/versionHandshake");
18
18
  const projectIdentity_1 = require("./services/features/telegram-mcp/src/shared/lib/project-identity/projectIdentity");
19
- const foregroundTerminalRuntime_1 = require("./services/features/telegram-mcp/src/features/foreground-terminal/model/foregroundTerminalRuntime");
19
+ const envMigration_1 = require("./envMigration");
20
+ const configureServer_1 = require("./configureServer");
20
21
  const distDir = __dirname;
21
22
  const packageRoot = node_path_1.default.resolve(distDir, "..");
22
23
  const cliPackageVersion = (0, versionHandshake_1.getTellyMcpPackageVersion)(__dirname);
24
+ function nativePtyRecoveryLines() {
25
+ return [
26
+ ...(process.platform === "linux"
27
+ ? [
28
+ "Install native build prerequisites (Debian/Ubuntu): sudo apt install -y python3 make g++",
29
+ ]
30
+ : []),
31
+ "Ensure npm lifecycle scripts are enabled: npm config set ignore-scripts false",
32
+ "Remove the broken global package: npm uninstall -g @deadragdoll/tellymcp",
33
+ "Reinstall and compile native dependencies: npm install -g @deadragdoll/tellymcp@latest --foreground-scripts",
34
+ ];
35
+ }
36
+ async function checkNativePtyRuntime() {
37
+ try {
38
+ const nodePty = await import("node-pty");
39
+ if (typeof nodePty.spawn !== "function") {
40
+ return {
41
+ available: false,
42
+ message: "node-pty loaded without a spawn function.",
43
+ };
44
+ }
45
+ return { available: true };
46
+ }
47
+ catch (error) {
48
+ const message = error instanceof Error ? error.message : String(error);
49
+ return {
50
+ available: false,
51
+ message: message.split("\n", 1)[0] || "node-pty failed to load.",
52
+ };
53
+ }
54
+ }
55
+ function isForegroundPtyClientMode(parsed) {
56
+ return (parsed.DISTRIBUTED_MODE || "client").trim() === "client";
57
+ }
23
58
  function printBanner(title, subtitle) {
24
59
  process.stdout.write(`${picocolors_1.default.bold(picocolors_1.default.cyan("TellyMCP"))} ${picocolors_1.default.bold(picocolors_1.default.white(`v${cliPackageVersion}`))} ${picocolors_1.default.dim(title)}\n`);
25
60
  if (subtitle) {
@@ -66,6 +101,8 @@ function printHelp() {
66
101
  printBanner("CLI", "Telegram control plane for MCP-connected coding agents");
67
102
  printSection("Usage", [
68
103
  " tellymcp init <client|gateway|both> [directory]",
104
+ " tellymcp configure [--no-open]",
105
+ " tellymcp migrate-env <input.env> > .migrated-env",
69
106
  " tellymcp run [--env <file>]",
70
107
  " tellymcp run --env=<file>",
71
108
  " tellymcp run --env .env-client -s backendDev",
@@ -84,6 +121,8 @@ function printHelp() {
84
121
  printSection("Examples", [
85
122
  " tellymcp init client",
86
123
  " tellymcp init gateway ./gateway-node",
124
+ " tellymcp configure",
125
+ " tellymcp migrate-env ./old.env > ./new.env",
87
126
  " tellymcp run",
88
127
  " tellymcp run --env .env.client",
89
128
  " tellymcp run --env .env-client -s backendDev",
@@ -99,7 +138,7 @@ function printHelp() {
99
138
  " tellymcp oauth key",
100
139
  ]);
101
140
  printSection("terminal", [
102
- `${picocolors_1.default.green(" OK")} built-in PTY runtime`,
141
+ " built-in PTY runtime (validated by tellymcp doctor and tellymcp run)",
103
142
  " Live view, session nudges and browser flows use the built-in terminal runtime.",
104
143
  ]);
105
144
  }
@@ -110,7 +149,9 @@ function formatMarkerEnvPath(envPath, cwd) {
110
149
  const resolvedCwd = node_path_1.default.resolve(cwd);
111
150
  const resolvedEnvPath = node_path_1.default.resolve(envPath);
112
151
  const relativePath = node_path_1.default.relative(resolvedCwd, resolvedEnvPath);
113
- return relativePath && !relativePath.startsWith("..") && !node_path_1.default.isAbsolute(relativePath)
152
+ return relativePath &&
153
+ !relativePath.startsWith("..") &&
154
+ !node_path_1.default.isAbsolute(relativePath)
114
155
  ? relativePath
115
156
  : resolvedEnvPath;
116
157
  }
@@ -157,7 +198,7 @@ function loadCliEnv(args) {
157
198
  const sessionOverride = explicitSessionOverride ?? marker?.localSessionId ?? null;
158
199
  const sessionLabelOverride = explicitSessionOverride
159
200
  ? explicitSessionOverride
160
- : marker?.sessionLabel ?? sessionOverride ?? null;
201
+ : (marker?.sessionLabel ?? sessionOverride ?? null);
161
202
  if (!(0, node_fs_1.existsSync)(envPath)) {
162
203
  fail(`Missing env file: ${envPath}`);
163
204
  }
@@ -263,9 +304,7 @@ function runOAuthCommand(args) {
263
304
  }
264
305
  function printBrowserHelp() {
265
306
  printBanner("browser helper", "Manage Playwright browser binaries used by browser_* tools");
266
- printSection("Usage", [
267
- " tellymcp browser install",
268
- ]);
307
+ printSection("Usage", [" tellymcp browser install"]);
269
308
  printSection("What this command does", [
270
309
  " Installs the bundled Playwright Chromium browser.",
271
310
  " Uses the Playwright dependency shipped with TellyMCP.",
@@ -323,12 +362,67 @@ function loadTemplate(mode) {
323
362
  : "env.both.template";
324
363
  const templatePath = node_path_1.default.join(packageRoot, templateName);
325
364
  const nestedTemplatePath = node_path_1.default.join(packageRoot, "config", "templates", templateName);
326
- const resolvedTemplatePath = (0, node_fs_1.existsSync)(templatePath) ? templatePath : nestedTemplatePath;
365
+ const resolvedTemplatePath = (0, node_fs_1.existsSync)(templatePath)
366
+ ? templatePath
367
+ : nestedTemplatePath;
327
368
  if (!(0, node_fs_1.existsSync)(resolvedTemplatePath)) {
328
369
  fail(`Missing packaged template: ${templateName}`);
329
370
  }
330
371
  return (0, node_fs_1.readFileSync)(resolvedTemplatePath, "utf8");
331
372
  }
373
+ async function runConfigure(args) {
374
+ if (args.includes("--help") || args.includes("-h")) {
375
+ printBanner("configure", "Local web configurator for a TellyMCP dotenv file");
376
+ printSection("Usage", [
377
+ " tellymcp configure [--no-open]",
378
+ " tellymcp configure [--port 8790] [--no-open]",
379
+ ]);
380
+ printSection("Safety", [
381
+ " The configurator binds only to 127.0.0.1 on a random free port.",
382
+ " Its URL contains a one-time access token.",
383
+ " The browser downloads .env-client or .env-gateway after validation.",
384
+ ]);
385
+ return;
386
+ }
387
+ for (let index = 0; index < args.length; index += 1) {
388
+ const value = args[index];
389
+ if (value === "--no-open" || value?.startsWith("--port=")) {
390
+ continue;
391
+ }
392
+ if (value === "--port") {
393
+ index += 1;
394
+ continue;
395
+ }
396
+ fail("configure does not accept a role or output path; choose Client or Gateway in the browser wizard.");
397
+ }
398
+ const portArg = readFlagValue(args, "--port");
399
+ const port = portArg === null ? 0 : Number(portArg);
400
+ if (!Number.isInteger(port) || port < 0 || port > 65535) {
401
+ fail("--port must be an integer between 0 and 65535.");
402
+ }
403
+ printBanner("configure", "Choose Client or Gateway in the local browser wizard");
404
+ await (0, configureServer_1.startConfigureServer)({
405
+ templates: {
406
+ client: loadTemplate("client"),
407
+ gateway: loadTemplate("gateway"),
408
+ },
409
+ port,
410
+ open: !args.includes("--no-open"),
411
+ onListening(url) {
412
+ printSection("Configurator", [
413
+ ` ${picocolors_1.default.green("OPEN")} ${url}`,
414
+ " Keep this terminal open until the file is saved.",
415
+ " Press Ctrl+C to cancel.",
416
+ ]);
417
+ },
418
+ onDownloaded(filename) {
419
+ printSection("Download", [
420
+ ` ${picocolors_1.default.green("OK")} ${filename} sent to the browser`,
421
+ ` next: chmod 600 ${filename}`,
422
+ ]);
423
+ },
424
+ });
425
+ }
332
426
  function initWorkspace(mode, directoryArg) {
333
427
  const targetDir = node_path_1.default.resolve(directoryArg ?? process.cwd());
334
428
  (0, node_fs_1.mkdirSync)(targetDir, { recursive: true });
@@ -354,6 +448,40 @@ function initWorkspace(mode, directoryArg) {
354
448
  " 3. tellymcp run",
355
449
  ]);
356
450
  }
451
+ function runMigrateEnv(args) {
452
+ const [inputArg, extraArg] = args;
453
+ if (!inputArg || extraArg || inputArg === "--help" || inputArg === "-h") {
454
+ printBanner("migrate-env", "Normalize a legacy TellyMCP environment file");
455
+ printSection("Usage", [
456
+ " tellymcp migrate-env <input.env> > .migrated-env",
457
+ " The role is read from DISTRIBUTED_MODE or inferred from gateway-only keys.",
458
+ " Normalized dotenv is written to stdout; migration notes go to stderr.",
459
+ ]);
460
+ if (!inputArg || inputArg === "--help" || inputArg === "-h") {
461
+ return;
462
+ }
463
+ fail("migrate-env expects exactly one input env file.");
464
+ }
465
+ const inputPath = node_path_1.default.resolve(process.cwd(), inputArg);
466
+ if (!(0, node_fs_1.existsSync)(inputPath)) {
467
+ fail(`Missing input env file: ${inputPath}`);
468
+ }
469
+ const result = (0, envMigration_1.migrateEnvironmentContent)((0, node_fs_1.readFileSync)(inputPath, "utf8"));
470
+ process.stdout.write(result.content);
471
+ const notes = [
472
+ `migrate-env: role=${result.role}`,
473
+ `migrate-env: kept ${result.keptKeys.length} keys from ${inputPath}`,
474
+ ];
475
+ if (result.renamedKeys.length > 0) {
476
+ notes.push(`migrate-env: renamed ${result.renamedKeys
477
+ .map(({ from, to }) => `${from}->${to}`)
478
+ .join(", ")}`);
479
+ }
480
+ if (result.droppedKeys.length > 0) {
481
+ notes.push(`migrate-env: dropped ${result.droppedKeys.join(", ")}`);
482
+ }
483
+ process.stderr.write(`${notes.join("\n")}\n`);
484
+ }
357
485
  function resolveRunEnvPath(args) {
358
486
  const [firstArg, secondArg] = args;
359
487
  const marker = getSessionMarkerForCwd(process.cwd());
@@ -424,9 +552,7 @@ function readFlagValue(args, flagName) {
424
552
  return null;
425
553
  }
426
554
  function printMcpConfig(args) {
427
- if (args.length === 0 ||
428
- args.includes("--help") ||
429
- args.includes("-h")) {
555
+ if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
430
556
  printMcpHelp();
431
557
  return;
432
558
  }
@@ -522,14 +648,13 @@ async function checkWebSocketUrl(url, timeoutMs = 3000) {
522
648
  }
523
649
  settled = true;
524
650
  clearTimeout(timer);
525
- socket.removeAllListeners();
526
- try {
651
+ resolve({ ok, message });
652
+ if (socket.readyState === ws_1.default.OPEN) {
527
653
  socket.close();
528
654
  }
529
- catch {
530
- // ignore
655
+ else if (socket.readyState === ws_1.default.CONNECTING) {
656
+ socket.terminate();
531
657
  }
532
- resolve({ ok, message });
533
658
  };
534
659
  const socket = new ws_1.default(url);
535
660
  const timer = setTimeout(() => {
@@ -542,23 +667,31 @@ async function checkWebSocketUrl(url, timeoutMs = 3000) {
542
667
  socket.once("unexpected-response", (_req, response) => {
543
668
  finish(false, `${url} returned HTTP ${response.statusCode ?? "unknown"} during WebSocket upgrade`);
544
669
  });
545
- socket.once("error", (error) => {
670
+ socket.on("error", (error) => {
546
671
  finish(false, `${url} failed: ${error.message}`);
547
672
  });
548
673
  socket.once("close", (code) => {
549
674
  if (!opened) {
550
675
  finish(false, `${url} closed before open (code ${code})`);
551
676
  }
677
+ socket.removeAllListeners();
552
678
  });
553
679
  });
554
680
  }
555
681
  async function runDoctor(args) {
556
682
  const { envPath, parsed } = loadCliEnv(args);
683
+ const nativePtyStatus = await checkNativePtyRuntime();
557
684
  printBanner("doctor", "Local installation diagnostics");
558
685
  printSection("terminal", [
559
- `${picocolors_1.default.green(" OK")} built-in PTY runtime`,
686
+ nativePtyStatus.available
687
+ ? `${picocolors_1.default.green(" OK")} built-in PTY runtime: native module loaded`
688
+ : `${picocolors_1.default.red(" ERROR")} built-in PTY runtime: ${nativePtyStatus.message}`,
689
+ ` platform: ${process.platform}-${process.arch}, Node ${process.versions.node}`,
560
690
  ` shell: ${parsed.TERMINAL_SHELL?.trim() || process.env.SHELL || "bash"}`,
561
691
  ` size: ${parsed.TERMINAL_COLS?.trim() || "120"}x${parsed.TERMINAL_ROWS?.trim() || "40"}`,
692
+ ...(!nativePtyStatus.available
693
+ ? nativePtyRecoveryLines().map((line) => `${picocolors_1.default.yellow(" ACTION")} ${line}`)
694
+ : []),
562
695
  ]);
563
696
  const mode = (parsed.DISTRIBUTED_MODE || "client").trim();
564
697
  const httpHost = (parsed.MCP_HTTP_HOST || "0.0.0.0").trim();
@@ -617,10 +750,15 @@ async function runDoctor(args) {
617
750
  checks.push(`${picocolors_1.default.red(" ERROR")} playwright chromium: ${playwrightStatus.message}`);
618
751
  capabilities.push(`${picocolors_1.default.red(" ERROR")} browser tools: browsers are not installed`);
619
752
  }
620
- const redisHost = (parsed.REDIS_HOST || "127.0.0.1").trim();
621
- const redisPort = Number(parsed.REDIS_PORT || 6379);
622
- const redisCheck = await checkTcpPort(redisHost, redisPort);
623
- checks.push(`${redisCheck.ok ? picocolors_1.default.green(" OK") : picocolors_1.default.red(" ERROR")} redis: ${redisCheck.message}`);
753
+ if (mode === "client") {
754
+ checks.push(`${picocolors_1.default.green(" OK")} local state: process-local (Redis is not required)`);
755
+ }
756
+ else {
757
+ const redisHost = (parsed.REDIS_HOST || "127.0.0.1").trim();
758
+ const redisPort = Number(parsed.REDIS_PORT || 6379);
759
+ const redisCheck = await checkTcpPort(redisHost, redisPort);
760
+ checks.push(`${redisCheck.ok ? picocolors_1.default.green(" OK") : picocolors_1.default.red(" ERROR")} redis: ${redisCheck.message}`);
761
+ }
624
762
  if (mode === "client") {
625
763
  const gatewayPublicUrl = parsed.GATEWAY_PUBLIC_URL?.trim();
626
764
  if (gatewayPublicUrl) {
@@ -720,9 +858,12 @@ async function runDoctor(args) {
720
858
  printSection("notes", notes);
721
859
  }
722
860
  else {
723
- printSection("notes", [`${picocolors_1.default.green(" OK")} No obvious local config issues detected.`]);
861
+ printSection("notes", [
862
+ `${picocolors_1.default.green(" OK")} No obvious local config issues detected.`,
863
+ ]);
724
864
  }
725
- if (browserEnabled && (!playwrightStatus.enabled || !playwrightStatus.installed)) {
865
+ if (browserEnabled &&
866
+ (!playwrightStatus.enabled || !playwrightStatus.installed)) {
726
867
  printSection("playwright", [
727
868
  `${picocolors_1.default.yellow(" ACTION")} Install browser binaries before using browser_* tools:`,
728
869
  " tellymcp browser install",
@@ -742,7 +883,9 @@ function normalizeExtensionFlavor(rawValue) {
742
883
  return null;
743
884
  }
744
885
  function getBundledExtensionDir(flavor) {
745
- return node_path_1.default.join(packageRoot, "packages", flavor === "firefox" ? "firefox-attach-extension" : "chrome-attach-extension", "dist");
886
+ return node_path_1.default.join(packageRoot, "packages", flavor === "firefox"
887
+ ? "firefox-attach-extension"
888
+ : "chrome-attach-extension", "dist");
746
889
  }
747
890
  function getDefaultExtensionTargetDir(flavor) {
748
891
  return node_path_1.default.resolve(process.cwd(), flavor === "firefox" ? "tellymcp-firefox-attach" : "tellymcp-chrome-attach");
@@ -783,7 +926,7 @@ async function runSystemPrune(args) {
783
926
  const dbUser = parsed.DB_USER?.trim();
784
927
  const dbPassword = parsed.DB_PASSWORD?.trim();
785
928
  const dbName = parsed.DB_NAME?.trim();
786
- const dbSchema = (parsed.DB_SCHEME || "mcp").trim();
929
+ const dbSchema = (parsed.DB_SCHEMA || "mcp").trim();
787
930
  const xchangeDir = node_path_1.default.resolve(process.cwd(), parsed.MCP_XCHANGE_DIR || ".mcp-xchange");
788
931
  const sessionMarkerPath = node_path_1.default.resolve(process.cwd(), ".mcpsession.json");
789
932
  const sqliteDbPath = node_path_1.default.join(xchangeDir, "xchange.sqlite3");
@@ -791,35 +934,39 @@ async function runSystemPrune(args) {
791
934
  printSection("Target", [
792
935
  ` env: ${envPath}`,
793
936
  ` mode: ${mode}`,
794
- ` redis: ${redisHost}:${redisPort}/${redisDb}`,
937
+ ...(mode === "client"
938
+ ? [` state: process-local (Redis skipped)`]
939
+ : [` redis: ${redisHost}:${redisPort}/${redisDb}`]),
795
940
  ...(dbHost && dbUser && dbName
796
941
  ? [` postgres: ${dbHost}:${dbPort}/${dbName} schema ${dbSchema}`]
797
942
  : [` postgres: ${picocolors_1.default.dim("skipped (not configured)")}`]),
798
943
  ` xchange dir: ${xchangeDir}`,
799
944
  ` session marker: ${sessionMarkerPath}`,
800
945
  ]);
801
- const redis = new ioredis_1.default({
802
- host: redisHost,
803
- port: redisPort,
804
- db: redisDb,
805
- ...(redisUsername ? { username: redisUsername } : {}),
806
- ...(redisPassword ? { password: redisPassword } : {}),
807
- maxRetriesPerRequest: 1,
808
- enableReadyCheck: true,
809
- });
810
946
  let deletedRedisKeys = 0;
811
- try {
812
- let cursor = "0";
813
- do {
814
- const [nextCursor, keys] = await redis.scan(cursor, "MATCH", "telegram-mcp:*", "COUNT", 500);
815
- cursor = nextCursor;
816
- if (keys.length > 0) {
817
- deletedRedisKeys += await redis.del(...keys);
818
- }
819
- } while (cursor !== "0");
820
- }
821
- finally {
822
- redis.disconnect();
947
+ if (mode !== "client") {
948
+ const redis = new ioredis_1.default({
949
+ host: redisHost,
950
+ port: redisPort,
951
+ db: redisDb,
952
+ ...(redisUsername ? { username: redisUsername } : {}),
953
+ ...(redisPassword ? { password: redisPassword } : {}),
954
+ maxRetriesPerRequest: 1,
955
+ enableReadyCheck: true,
956
+ });
957
+ try {
958
+ let cursor = "0";
959
+ do {
960
+ const [nextCursor, keys] = await redis.scan(cursor, "MATCH", "telegram-mcp:*", "COUNT", 500);
961
+ cursor = nextCursor;
962
+ if (keys.length > 0) {
963
+ deletedRedisKeys += await redis.del(...keys);
964
+ }
965
+ } while (cursor !== "0");
966
+ }
967
+ finally {
968
+ redis.disconnect();
969
+ }
823
970
  }
824
971
  let truncatedTables = [];
825
972
  if (dbHost && dbUser && dbName) {
@@ -1013,8 +1160,19 @@ async function runRuntime(args) {
1013
1160
  sessionId: parsed.TELLYMCP_SESSION_ID,
1014
1161
  sessionLabel: parsed.TELLYMCP_SESSION_LABEL,
1015
1162
  });
1016
- if ((0, foregroundTerminalRuntime_1.isForegroundPtyClientMode)(parsed)) {
1017
- await (0, foregroundTerminalRuntime_1.runForegroundPtyRuntime)({
1163
+ const nativePtyStatus = await checkNativePtyRuntime();
1164
+ if (!nativePtyStatus.available) {
1165
+ fail([
1166
+ `Built-in PTY runtime is unavailable on ${process.platform}-${process.arch} with Node ${process.versions.node}.`,
1167
+ nativePtyStatus.message,
1168
+ "",
1169
+ ...nativePtyRecoveryLines(),
1170
+ "Then rerun: tellymcp doctor --env <file>",
1171
+ ].join("\n"));
1172
+ }
1173
+ if (isForegroundPtyClientMode(parsed)) {
1174
+ const { runForegroundPtyRuntime } = await import("./services/features/telegram-mcp/src/features/foreground-terminal/model/foregroundTerminalRuntime.js");
1175
+ await runForegroundPtyRuntime({
1018
1176
  envPath,
1019
1177
  packageRoot,
1020
1178
  printBanner,
@@ -1068,11 +1226,24 @@ async function runRuntime(args) {
1068
1226
  }
1069
1227
  async function main(argv) {
1070
1228
  const [rawCommand, firstArg, secondArg] = argv;
1071
- const command = rawCommand === "init" || rawCommand === "run" || rawCommand === "help" || rawCommand === "mcp" || rawCommand === "oauth" || rawCommand === "doctor" || rawCommand === "browser" || rawCommand === "system-prune"
1072
- || rawCommand === "codex-plugin" || rawCommand === "extension"
1229
+ const command = rawCommand === "init" ||
1230
+ rawCommand === "configure" ||
1231
+ rawCommand === "migrate-env" ||
1232
+ rawCommand === "run" ||
1233
+ rawCommand === "help" ||
1234
+ rawCommand === "mcp" ||
1235
+ rawCommand === "oauth" ||
1236
+ rawCommand === "doctor" ||
1237
+ rawCommand === "browser" ||
1238
+ rawCommand === "system-prune" ||
1239
+ rawCommand === "codex-plugin" ||
1240
+ rawCommand === "extension"
1073
1241
  ? rawCommand
1074
1242
  : "help";
1075
- if (command === "help" || !rawCommand || rawCommand === "--help" || rawCommand === "-h") {
1243
+ if (command === "help" ||
1244
+ !rawCommand ||
1245
+ rawCommand === "--help" ||
1246
+ rawCommand === "-h") {
1076
1247
  printHelp();
1077
1248
  return;
1078
1249
  }
@@ -1080,6 +1251,14 @@ async function main(argv) {
1080
1251
  initWorkspace(ensureMode(firstArg), secondArg);
1081
1252
  return;
1082
1253
  }
1254
+ if (command === "configure") {
1255
+ await runConfigure(argv.slice(1));
1256
+ return;
1257
+ }
1258
+ if (command === "migrate-env") {
1259
+ runMigrateEnv(argv.slice(1));
1260
+ return;
1261
+ }
1083
1262
  if (command === "mcp") {
1084
1263
  printMcpConfig(argv.slice(1));
1085
1264
  return;