@deadragdoll/tellymcp 0.0.5 → 0.0.7

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.
@@ -90,4 +90,6 @@ SOCKS5_PROXY=
90
90
 
91
91
  # Логирование.
92
92
  LOG_LEVEL=info
93
+ LOG_FILE_ENABLED=false
94
+ LOG_FILE_PATH=.tellymcp/log.jsonl
93
95
  ENABLE_LOGFEED=0
@@ -112,8 +112,9 @@ SOCKS5_PROXY=
112
112
 
113
113
  # Логирование.
114
114
  LOG_LEVEL=info
115
- ENABLE_LOGFEED=0
116
115
  LOG_FILE_ENABLED=false
116
+ LOG_FILE_PATH=.tellymcp/log.jsonl
117
+ ENABLE_LOGFEED=0
117
118
 
118
119
  # Секреты шлюза.
119
120
  SESSION_SECRET=change_me_session_secret
package/README-ru.md CHANGED
@@ -7,16 +7,73 @@
7
7
  [![node >= 24](https://img.shields.io/badge/node-%3E%3D24-339933)](https://nodejs.org/)
8
8
  [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
9
9
 
10
- TellyMCP — это Telegram Human-in-the-Loop MCP server для coding agents.
10
+ TellyMCP — это self-hosted Telegram control plane для coding agents.
11
11
 
12
- Он позволяет агенту:
12
+ Он привязывает реальные agent-сессии к Telegram, делает их доступными с телефона и даёт им работать вместе между локальными и удалёнными машинами.
13
+
14
+ Он не завязан на одного вендора или один coding assistant. Если агент умеет работать с MCP server, он может использовать TellyMCP.
15
+
16
+ ## Зачем он нужен
17
+
18
+ Coding agents полезны ровно до того момента, пока они не остаются одни в терминале:
19
+
20
+ - им нужно уточнение, пока тебя нет за компьютером
21
+ - им нужен approval перед рискованным действием
22
+ - им нужно передать скриншот, файл или note между сессиями
23
+ - им нужно быстро подключить человека или другого агента, не ломая общий workflow
24
+
25
+ TellyMCP даёт каждой сессии мобильную панель управления и collaboration layer:
26
+
27
+ - `Live` tmux view и лёгкое управление из Telegram
28
+ - session-scoped inbox и уведомления
29
+ - workspace-aware handoff для файлов и note
30
+ - локальную и удалённую коллаборацию между сессиями
31
+ - поддержку mixed agent setups, если они умеют говорить по MCP
32
+
33
+ ## Ключевые идеи продукта
34
+
35
+ - `Live` tmux view и управление внутри Telegram Mini App
36
+ - `Collab`-сценарии для локальных и удалённых agent-сессий
37
+ - `.mcp-xchange` как workspace-level handoff шина для note, файлов и скриншотов
38
+ - MCP-native pairing сессий и session-scoped tools
39
+ - optional gateway mode для multi-machine и multi-bot проектов
40
+
41
+ ## Human-in-the-loop — это только один слой системы
42
+
43
+ Telegram HITL здесь тоже есть, но он не исчерпывает продукт:
13
44
 
14
45
  - задавать человеку уточняющие вопросы через Telegram
15
46
  - получать несвязанные входящие сообщения позже через inbox
16
- - привязывать несколько agent-сессий
17
- - работать с локальными и удалёнными партнёрскими сессиями
18
- - открывать Live tmux view внутри Telegram Mini App
19
- - обмениваться note, скриншотами и файлами через `.mcp-xchange`
47
+ - уведомлять человека о прогрессе, blockers и approvals
48
+
49
+ ## Что отличает TellyMCP от простого Telegram bot bridge
50
+
51
+ - он завязан на сессии, а не только на чат
52
+ - он понимает локальные и удалённые collaboration targets
53
+ - у него есть live terminal surface, а не только обмен сообщениями
54
+ - он передаёт файлы через workspace-aware exchange paths, а не просто через ad hoc upload
55
+ - он может работать как standalone node или как gateway-backed control plane
56
+
57
+ ## Типовые сценарии
58
+
59
+ - держать долгоживущего агента доступным с телефона
60
+ - запускать рядом разных агентов, если каждый умеет подключаться по MCP
61
+ - подруливать tmux-сессией без ноутбука
62
+ - маршрутизировать работу между `frontend`, `backend`, `review` и другими локальными сессиями
63
+ - работать с удалёнными сессиями через gateway-backed project
64
+ - передавать note, скриншоты и реальные файлы через `.mcp-xchange`
65
+ - проверять локальный веб-интерфейс через `browser_*` tools и отправлять результат обратно в Telegram
66
+
67
+ ## Группы инструментов
68
+
69
+ - pairing и session context
70
+ - Telegram ask/notify/inbox
71
+ - `Live` tmux control
72
+ - browser inspection и screenshots
73
+ - partner notes и partner files
74
+ - tools sync и version checks
75
+
76
+ Полный список MCP tools лучше держать ниже по README и в самом MCP server, а не на первом экране.
20
77
 
21
78
  ## Prerequisites
22
79
 
@@ -172,6 +229,99 @@ tellymcp run --env .env
172
229
 
173
230
  - `https://your-host.example/api/mcp`
174
231
 
232
+ ## Docker: только для инфраструктуры или для `gateway`-only
233
+
234
+ Docker больше не является основным способом запуска TellyMCP, но один container path поддерживается:
235
+
236
+ - запуск только `gateway`-ноды в контейнере
237
+
238
+ Это вариант именно для чистого control-plane узла:
239
+
240
+ - без локальных agent sessions
241
+ - без локального `tmux`
242
+ - не для `client`
243
+ - не для `both`
244
+
245
+ Плюс `docker-compose.yml` по-прежнему может поднимать локальную инфраструктуру:
246
+
247
+ - `redis` для всех режимов
248
+ - `postgres` для `gateway` / `both`
249
+ - `rabbitmq` только если нужен durable fanout на шлюзе
250
+
251
+ Только Redis, для `standalone` или `client`:
252
+
253
+ ```bash
254
+ docker compose up -d redis
255
+ ```
256
+
257
+ Redis + Postgres, для `gateway` или `both`:
258
+
259
+ ```bash
260
+ docker compose --profile gateway up -d
261
+ ```
262
+
263
+ Добавить RabbitMQ при необходимости:
264
+
265
+ ```bash
266
+ docker compose --profile gateway --profile rmq up -d
267
+ ```
268
+
269
+ Полный gateway stack в Docker:
270
+
271
+ 1. Скопировать пример:
272
+
273
+ ```bash
274
+ cp .env.example.gateway .env-gateway
275
+ ```
276
+
277
+ 2. Отредактировать `.env-gateway` и задать минимум:
278
+
279
+ - `TELEGRAM_BOT_TOKEN`
280
+ - `TELEGRAM_BOT_USERNAME`
281
+ - `WEBAPP_PUBLIC_URL`
282
+ - `GATEWAY_PUBLIC_URL`
283
+ - `GATEWAY_WS_URL`
284
+ - `MCP_HTTP_BEARER_TOKEN`
285
+
286
+ 3. Запустить:
287
+
288
+ ```bash
289
+ docker compose up -d
290
+ ```
291
+
292
+ Это поднимет:
293
+
294
+ - `redis`
295
+ - `postgres`
296
+ - `tellymcp-gateway`
297
+
298
+ Внутри Docker compose сам переопределяет:
299
+
300
+ - `MCP_HTTP_HOST=0.0.0.0`
301
+ - `REDIS_HOST=redis`
302
+ - `DB_HOST=postgres`
303
+
304
+ Ожидаемые endpoint'ы:
305
+
306
+ - `http://127.0.0.1:8080/api/healthz`
307
+ - `http://127.0.0.1:8080/api/mcp`
308
+ - `http://127.0.0.1:8080/api/webapp`
309
+ - `http://127.0.0.1:8080/api/gateway`
310
+
311
+ Остановить всё:
312
+
313
+ ```bash
314
+ docker compose down
315
+ ```
316
+
317
+ Сам TellyMCP при этом запускается напрямую на хосте:
318
+
319
+ ```bash
320
+ tellymcp run --env .env
321
+ ```
322
+
323
+ Для `client` и `both` по-прежнему рекомендуется именно хостовый запуск.
324
+
175
325
  ## Как начать работу с ботом изнутри агента
176
326
 
177
327
  После подключения MCP можно просто написать агенту обычной фразой, что нужно привязаться к Telegram.
@@ -275,6 +425,9 @@ tellymcp mcp --help
275
425
  - `PROXY_USE=http|socks5`
276
426
  - `HTTP_PROXY`
277
427
  - `SOCKS5_PROXY`
428
+ - `LOG_LEVEL`
429
+ - `LOG_FILE_ENABLED`
430
+ - `LOG_FILE_PATH`
278
431
 
279
432
  Только client:
280
433
 
package/README.md CHANGED
@@ -7,50 +7,73 @@
7
7
  [![node >= 24](https://img.shields.io/badge/node-%3E%3D24-339933)](https://nodejs.org/)
8
8
  [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
9
9
 
10
- TellyMCP is a Telegram Human-in-the-Loop MCP server for coding agents.
10
+ TellyMCP is a self-hosted Telegram control plane for coding agents.
11
11
 
12
- It lets an agent:
12
+ It pairs real agent sessions with Telegram, keeps them reachable from mobile, and lets them collaborate across local and remote machines.
13
+
14
+ It is not tied to one vendor or one coding assistant. If your agent can talk to an MCP server, it can use TellyMCP.
15
+
16
+ ## Why it exists
17
+
18
+ Coding agents are useful until they leave the terminal:
19
+
20
+ - they need clarification while you are away from the desk
21
+ - they need approval before doing something risky
22
+ - they need screenshots, files, or notes passed between sessions
23
+ - they need a human or another agent to unblock work without breaking flow
24
+
25
+ TellyMCP gives each session a mobile control surface and a collaboration layer:
26
+
27
+ - `Live` tmux view and light control from Telegram
28
+ - session-scoped inbox and notifications
29
+ - workspace-aware file and note handoffs
30
+ - local and remote session collaboration
31
+ - support for mixed agent setups, as long as they speak MCP
32
+
33
+ ## Core ideas
34
+
35
+ - `Live` tmux view and control inside Telegram Mini App
36
+ - `Collab` flows for local and remote agent sessions
37
+ - `.mcp-xchange` as a workspace-level handoff bus for notes, files, and screenshots
38
+ - MCP-native session pairing and session-scoped tools
39
+ - optional gateway mode for cross-machine and cross-bot projects
40
+
41
+ ## Human-in-the-loop is one layer, not the whole product
42
+
43
+ Telegram HITL is still supported, but it is not the whole story:
13
44
 
14
45
  - ask a human for clarification through Telegram
15
46
  - receive unsolicited Telegram messages later through an inbox
16
- - pair multiple agent sessions
17
- - collaborate across local and remote sessions
18
- - open a live tmux view inside Telegram Mini App
19
- - exchange notes, screenshots, and files through `.mcp-xchange`
20
-
21
- Current tools:
22
-
23
- - `create_session_pair_code`
24
- - `clear_session_pairing`
25
- - `set_session_context`
26
- - `set_tmux_target`
27
- - `get_tmux_target`
28
- - `get_session_context`
29
- - `clear_session_context`
30
- - `rename_session`
31
- - `notify_telegram`
32
- - `get_telegram_inbox_count`
33
- - `get_telegram_inbox`
34
- - `delete_telegram_inbox_message`
35
- - `ask_user_telegram`
36
- - `browser_open`
37
- - `browser_reload`
38
- - `browser_click`
39
- - `browser_fill`
40
- - `browser_press`
41
- - `browser_wait_for`
42
- - `browser_wait_for_url`
43
- - `browser_console`
44
- - `browser_errors`
45
- - `browser_network_failures`
46
- - `browser_clear_logs`
47
- - `browser_dom`
48
- - `browser_computed_style`
49
- - `browser_screenshot`
50
- - `browser_close`
51
- - `refresh_tools_markdown`
52
- - `send_partner_note`
53
- - `send_partner_file`
47
+ - notify a human about progress, blockers, and approvals
48
+
49
+ ## What makes it different from a simple Telegram bot bridge
50
+
51
+ - it is session-based, not just chat-based
52
+ - it understands local and remote collaboration targets
53
+ - it has a live terminal surface, not only message exchange
54
+ - it moves files through workspace-aware exchange paths, not just ad hoc uploads
55
+ - it can run as a standalone node or as a gateway-backed control plane
56
+
57
+ ## Typical use cases
58
+
59
+ - keep a long-running agent reachable from your phone
60
+ - run different agents side by side, as long as each one can connect over MCP
61
+ - steer a tmux-based session without opening a laptop
62
+ - route work between `frontend`, `backend`, `review`, or other local sessions
63
+ - collaborate with remote sessions through a gateway-backed project
64
+ - send notes, screenshots, and real files through `.mcp-xchange`
65
+ - inspect or screenshot a local web app with `browser_*` tools and send results back to Telegram
66
+
67
+ ## Tool groups
68
+
69
+ - session pairing and context
70
+ - Telegram ask/notify/inbox
71
+ - `Live` tmux control
72
+ - browser inspection and screenshots
73
+ - partner notes and partner files
74
+ - tools sync and version checks
75
+
76
+ The full MCP tool surface is documented later in this README and through the MCP server itself.
54
77
 
55
78
  ## Prerequisites
56
79
 
@@ -389,10 +412,11 @@ Canonical instructions:
389
412
  - the `TOOLS.md` version marker
390
413
  - the file content itself
391
414
 
392
- Logs are written in two places at the same time:
415
+ Logs use one runtime model:
393
416
 
394
- - pretty console output to `stderr`
395
- - JSONL file at `.telegram-human-mcp/log.jsonl`
417
+ - `pino-pretty` console output to `stderr`
418
+ - optional JSONL file sink via `LOG_FILE_ENABLED=true` and `LOG_FILE_PATH=...`
419
+ - optional in-app `LogFeed` buffer for Telegram/UI diagnostics when `ENABLE_LOGFEED=1`
396
420
 
397
421
  If Telegram access requires a proxy, the bot transport can use:
398
422
 
@@ -529,10 +553,10 @@ Recommended local dev settings:
529
553
  - install browser binaries once with `npx playwright install chromium`
530
554
  - install browser binaries once with `tellymcp browser install`
531
555
 
532
- Recommended Docker settings:
556
+ Recommended headless server settings:
533
557
 
534
558
  - `BROWSER_HEADLESS=true`
535
- - target the host dev server through `http://host.docker.internal:3000`
559
+ - target the app through a reachable host or LAN address, for example `http://127.0.0.1:3000` or `http://192.168.x.x:3000`
536
560
 
537
561
  Current browser tools:
538
562
 
@@ -806,75 +830,111 @@ If `MCP_HTTP_BEARER_TOKEN` is configured:
806
830
  `yarn dev:gw:telegram` is still available, but it only starts the `telegram_mcp` feature node.
807
831
  It does not expose HTTP by itself anymore. `/mcp`, `/webapp`, and `/healthz` are now served only through the Moleculer API gateway aliases in the full `dev:gw` / `start:gw` runtime, or through a separate gateway node in the same namespace.
808
832
 
809
- ## Optional Docker deployment
833
+ ## Optional Docker infrastructure
834
+
835
+ Docker is no longer the default way to run TellyMCP, but there is one supported container path:
810
836
 
811
- Docker is no longer required for the default product install flow.
837
+ - `gateway`-only container deployment
812
838
 
813
- This repository still includes a single-container deployment path without an internal nginx layer for ops/self-hosted scenarios.
839
+ This is intended for a pure control-plane node:
814
840
 
815
- Inside the container:
841
+ - no local agent sessions
842
+ - no local `tmux`
843
+ - no `client` mode
844
+ - no `both` mode
816
845
 
817
- - `node` runs the MCP HTTP service on `0.0.0.0:8787`
818
- - `redis-server` runs on `127.0.0.1:6379`
819
- - the application itself serves:
820
- - `/mcp`
821
- - `/webapp`
822
- - `/healthz`
823
- - `/sessions`
824
- - `/prune`
846
+ The repository also keeps Docker for local infrastructure:
825
847
 
826
- This means an external reverse proxy can forward directly to container port `8787`, while all app routing stays inside the Node service.
848
+ - `redis` for all modes
849
+ - `postgres` for `gateway` / `both`
850
+ - `rabbitmq` only if you want durable fanout on the gateway
827
851
 
828
- Build the image fully inside Docker:
852
+ Start Redis only, for `standalone` or `client` mode:
829
853
 
830
854
  ```bash
831
- docker compose build
855
+ docker compose up -d redis
832
856
  ```
833
857
 
834
- Run it:
858
+ Start Redis + Postgres, for `gateway` or `both` mode:
835
859
 
836
860
  ```bash
837
- docker compose up -d
861
+ docker compose --profile gateway up -d
838
862
  ```
839
863
 
840
- Stop it:
864
+ Add RabbitMQ only when you need it:
841
865
 
842
866
  ```bash
843
- docker compose down
867
+ docker compose --profile gateway --profile rmq up -d
844
868
  ```
845
869
 
846
- The compose file:
870
+ Run a full gateway container stack with Redis and Postgres:
847
871
 
848
- - builds the image from this repository
849
- - injects `.env`
850
- - overrides runtime networking so the app talks to local in-container Redis and listens on `0.0.0.0:8787`
851
- - publishes only `8787:8787`
852
- - keeps `host.docker.internal` available for optional host-side development integrations
853
- - persists Redis state in `./data/redis`
872
+ 1. Copy the example:
854
873
 
855
- After startup:
874
+ ```bash
875
+ cp .env.example.gateway .env-gateway
876
+ ```
856
877
 
857
- - MCP is reachable at `http://<host>:8787/mcp`
858
- - Mini App static/API routes are reachable under `http://<host>:8787/webapp/`
859
- - health check is at `http://<host>:8787/healthz`
878
+ 2. Edit `.env-gateway` and set at minimum:
860
879
 
861
- Recommended external reverse proxy pattern:
880
+ - `TELEGRAM_BOT_TOKEN`
881
+ - `TELEGRAM_BOT_USERNAME`
882
+ - `WEBAPP_PUBLIC_URL`
883
+ - `GATEWAY_PUBLIC_URL`
884
+ - `GATEWAY_WS_URL`
885
+ - `MCP_HTTP_BEARER_TOKEN`
862
886
 
863
- - external proxy forwards `/mcp` to `http://<container-host>:8787/mcp`
864
- - external proxy forwards `/webapp/` to `http://<container-host>:8787/webapp/`
865
- - or, if you prefer, the external proxy can forward a wider prefix directly to `http://<container-host>:8787`
866
- - no direct external access is needed to in-container Redis
867
- - `tmux` access is expected to be direct from the running `tellymcp` process
887
+ 3. Start the stack:
868
888
 
869
- Important:
889
+ ```bash
890
+ docker compose up -d
891
+ ```
892
+
893
+ This starts:
894
+
895
+ - `redis`
896
+ - `postgres`
897
+ - `tellymcp-gateway`
898
+
899
+ Inside Docker, compose overrides:
900
+
901
+ - `MCP_HTTP_HOST=0.0.0.0`
902
+ - `REDIS_HOST=redis`
903
+ - `DB_HOST=postgres`
904
+
905
+ Public endpoint expectations stay the same:
906
+
907
+ - `http://127.0.0.1:8080/api/healthz`
908
+ - `http://127.0.0.1:8080/api/mcp`
909
+ - `http://127.0.0.1:8080/api/webapp`
910
+ - `http://127.0.0.1:8080/api/gateway`
911
+
912
+ Stop everything:
913
+
914
+ ```bash
915
+ docker compose down
916
+ ```
917
+
918
+ Default published ports:
919
+
920
+ - Redis: `6379`
921
+ - Postgres: `5432`
922
+ - RabbitMQ AMQP: `5672`
923
+ - RabbitMQ UI: `15672`
924
+
925
+ The TellyMCP process itself should run directly on the host:
926
+
927
+ ```bash
928
+ tellymcp run --env .env
929
+ ```
930
+
931
+ This keeps:
870
932
 
871
- - pairing state
872
- - active session bindings
873
- - inbox messages
874
- - menu payload buffers
875
- - WebApp launch/session state
933
+ - direct `tmux` access
934
+ - simpler debugging
935
+ - the same runtime model for `standalone`, `client`, `gateway`, and `both`
876
936
 
877
- are all stored in Redis. In the Docker deployment they survive restarts because `./data/redis` is mounted into the container and Redis AOF is enabled.
937
+ For `client` and `both`, host execution is still the recommended model.
878
938
 
879
939
  Optional if the local tmux server uses a non-default socket:
880
940
 
package/dist/cli.js CHANGED
@@ -82,7 +82,7 @@ async function getPlaywrightBrowserStatus(browserEnabled) {
82
82
  }
83
83
  function printHelp() {
84
84
  const tmux = getTmuxStatus();
85
- printBanner("CLI", "Telegram Human-in-the-Loop MCP server");
85
+ printBanner("CLI", "Telegram control plane for MCP-connected coding agents");
86
86
  printSection("Usage", [
87
87
  " tellymcp init <client|gateway|both> [directory]",
88
88
  " tellymcp run [--env <file>]",
@@ -1,19 +1,23 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.PlaygroundDisabledError = exports.SessionIdleTimeoutExceededError = exports.TokenInvalidError = exports.TokenNotFoundError = exports.SessionTokenSignatureInvalidError = exports.SessionTokenInvalidFormatError = exports.SessionTokenMismatchError = exports.SessionTokenNotFoundError = exports.SessionTokenRevokedError = exports.SessionTokenNotActiveError = exports.SessionTokenExpiredError = exports.SessionTokenInvalidError = exports.SessionValidationError = exports.SessionRefreshError = exports.SessionMaxLifetimeExceededError = exports.SessionStealedError = exports.SessionInvalidError = exports.SessionExpiredError = exports.SessionNotFoundError = exports.ForbiddenError = exports.UnauthorizedError = exports.BackendError = void 0;
4
- const graphql_1 = require("graphql");
5
- class BackendError extends graphql_1.GraphQLError {
6
- constructor(message, code, type, data) {
7
- super(message, {
8
- extensions: {
9
- code: code,
10
- type: type || code,
11
- data,
12
- },
13
- });
3
+ exports.PlaygroundDisabledError = exports.SessionIdleTimeoutExceededError = exports.TokenInvalidError = exports.TokenNotFoundError = exports.SessionTokenSignatureInvalidError = exports.SessionTokenInvalidFormatError = exports.SessionTokenMismatchError = exports.SessionTokenNotFoundError = exports.SessionTokenRevokedError = exports.SessionTokenNotActiveError = exports.SessionTokenExpiredError = exports.SessionTokenInvalidError = exports.SessionValidationError = exports.SessionRefreshError = exports.SessionMaxLifetimeExceededError = exports.SessionStealedError = exports.SessionInvalidError = exports.SessionExpiredError = exports.SessionNotFoundError = exports.ForbiddenError = exports.UnauthorizedError = exports.wrapUnhandledBackendError = exports.buildUnhandledBackendErrorCode = exports.BackendError = void 0;
4
+ class BackendError extends Error {
5
+ statusCode;
6
+ code;
7
+ data;
8
+ constructor(message, statusCode = 500, code, data) {
9
+ super(message);
10
+ this.name = "BackendError";
11
+ this.statusCode = statusCode;
12
+ this.code = code || String(statusCode);
13
+ this.data = data;
14
14
  }
15
15
  }
16
16
  exports.BackendError = BackendError;
17
+ const buildUnhandledBackendErrorCode = (rawName) => `XC_${rawName.toUpperCase()}`;
18
+ exports.buildUnhandledBackendErrorCode = buildUnhandledBackendErrorCode;
19
+ const wrapUnhandledBackendError = (err, rawName) => new BackendError(err.message, 502, (0, exports.buildUnhandledBackendErrorCode)(rawName));
20
+ exports.wrapUnhandledBackendError = wrapUnhandledBackendError;
17
21
  class UnauthorizedError extends BackendError {
18
22
  constructor(message = "Unauthorized", data) {
19
23
  super(message, 401, "UNAUTHORIZED", data);
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createPinoTargets = createPinoTargets;
4
+ function createPinoTargets(config) {
5
+ const targets = [
6
+ {
7
+ target: "pino-pretty",
8
+ level: config.level,
9
+ options: {
10
+ destination: 2,
11
+ colorize: true,
12
+ translateTime: "SYS:yyyy-mm-dd HH:MM:ss.l",
13
+ ignore: "pid,hostname",
14
+ singleLine: false,
15
+ },
16
+ },
17
+ ];
18
+ if (config.fileEnabled) {
19
+ targets.unshift({
20
+ target: "pino/file",
21
+ level: config.level,
22
+ options: {
23
+ destination: config.filePath,
24
+ mkdir: true,
25
+ },
26
+ });
27
+ }
28
+ return targets;
29
+ }
@@ -8,11 +8,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
8
8
  const moleculer_1 = require("moleculer");
9
9
  const dotenv_1 = __importDefault(require("dotenv"));
10
10
  require("module-alias/register");
11
- const promises_1 = __importDefault(require("node:fs/promises"));
12
- const node_path_1 = __importDefault(require("node:path"));
11
+ const pino_1 = __importDefault(require("pino"));
13
12
  const logfeed_1 = require("./lib/mixins/logfeed");
14
13
  const session_errors_1 = require("./lib/mixins/session.errors");
15
- const tracer_1 = require("./lib/middlewares/tracer");
14
+ const pinoTargets_1 = require("./lib/pinoTargets");
16
15
  dotenv_1.default.config({ path: process.env.ENV_FILE ?? ".env" });
17
16
  /**
18
17
  * Moleculer ServiceBroker configuration file 1
@@ -40,72 +39,34 @@ dotenv_1.default.config({ path: process.env.ENV_FILE ?? ".env" });
40
39
  * }
41
40
  */
42
41
  process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
42
+ const pinoTransport = pino_1.default.transport({
43
+ targets: (0, pinoTargets_1.createPinoTargets)({
44
+ level: process.env.LOG_LEVEL || "info",
45
+ fileEnabled: process.env.LOG_FILE_ENABLED === "true",
46
+ filePath: process.env.LOG_FILE_PATH || ".tellymcp/log.jsonl",
47
+ }),
48
+ });
43
49
  const logger = [
44
50
  {
45
- type: "Console",
51
+ type: "Pino",
46
52
  options: {
47
- colors: true,
48
- moduleColors: true,
49
- formatter: process.env.LOGFORMATTER ?? "short",
50
- objectPrinter: null,
51
- autoPadding: false,
53
+ pino: {
54
+ options: {
55
+ name: "tellymcp-broker",
56
+ level: process.env.LOG_LEVEL || "info",
57
+ timestamp: pino_1.default.stdTimeFunctions.isoTime,
58
+ },
59
+ destination: pinoTransport,
60
+ },
52
61
  },
53
62
  },
54
63
  ];
55
64
  const metricsEnabled = process.env.MOLECULER_METRICS === "true";
56
65
  const metricsPort = +(process.env.METRICS_PORT || 3030);
57
66
  const metricsPath = process.env.METRICS_PATH || "/metrics";
58
- const logFileEnabled = process.env.LOG_FILE_ENABLED === "true";
59
67
  const logFeedEnabled = process.env.ENABLE_LOGFEED != null
60
68
  ? !["0", "false", "no", "off"].includes(process.env.ENABLE_LOGFEED.toLowerCase())
61
69
  : process.env.LOGFEED_ENABLED !== "false";
62
- const logFileFolder = process.env.LOG_FILE_FOLDER || "./logs";
63
- const logFileName = process.env.LOG_FILE_NAME || "moleculer-{date}.log";
64
- const logFileRetentionDays = +(process.env.LOG_FILE_RETENTION_DAYS || 14);
65
- const cleanupLogFiles = async () => {
66
- if (!logFileEnabled || !Number.isFinite(logFileRetentionDays) || logFileRetentionDays <= 0) {
67
- return;
68
- }
69
- const logFolder = node_path_1.default.resolve(logFileFolder);
70
- const now = Date.now();
71
- const maxAgeMs = logFileRetentionDays * 24 * 60 * 60 * 1000;
72
- const filenamePattern = new RegExp(`^${logFileName
73
- .replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
74
- .replace(/\\\{date\\\}/g, "\\d{4}-\\d{2}-\\d{2}")
75
- .replace(/\\\{nodeID\\\}/g, "[^/]+")
76
- .replace(/\\\{namespace\\\}/g, "[^/]+")}$`);
77
- try {
78
- const entries = await promises_1.default.readdir(logFolder, { withFileTypes: true });
79
- await Promise.all(entries
80
- .filter(entry => entry.isFile() && filenamePattern.test(entry.name))
81
- .map(async (entry) => {
82
- const filePath = node_path_1.default.join(logFolder, entry.name);
83
- const stats = await promises_1.default.stat(filePath);
84
- if (now - stats.mtimeMs > maxAgeMs) {
85
- await promises_1.default.unlink(filePath);
86
- }
87
- }));
88
- }
89
- catch (error) {
90
- if (error.code !== "ENOENT") {
91
- console.warn("LOG_FILE cleanup failed:", error);
92
- }
93
- }
94
- };
95
- if (logFileEnabled) {
96
- void cleanupLogFiles();
97
- logger.push({
98
- type: "File",
99
- options: {
100
- level: process.env.LOG_FILE_LEVEL || process.env.LOG_LEVEL || "info",
101
- folder: logFileFolder,
102
- filename: logFileName,
103
- formatter: process.env.LOG_FILE_FORMATTER || "json",
104
- eol: "\n",
105
- interval: +(process.env.LOG_FILE_INTERVAL_MS || 1000),
106
- },
107
- });
108
- }
109
70
  if (logFeedEnabled) {
110
71
  logger.push(new logfeed_1.LogFeedLogger({
111
72
  level: process.env.LOGFEED_LEVEL || process.env.LOG_LEVEL || "info",
@@ -206,18 +167,15 @@ const brokerConfig = {
206
167
  // Enable action & event parameter validation. More info: https://moleculer.services/docs/0.14/validating.html
207
168
  validator: true,
208
169
  errorHandler: (err, { ctx, service }) => {
209
- // ctx.service.logger.error("errorHandler", err);
210
170
  if (err instanceof session_errors_1.BackendError) {
211
- // ctx.meta.$statusCode = err.extensions.code;
212
171
  return err;
213
172
  }
214
173
  else if (err instanceof Error) {
215
- // ctx.meta.$statusCode = 502;
216
174
  const rawName = ctx?.action?.rawName ||
217
175
  ctx?.action?.name ||
218
176
  service?.name ||
219
177
  "UNKNOWN";
220
- return new session_errors_1.BackendError(err.message, 502, `XC_${String(rawName).toUpperCase()}`);
178
+ return (0, session_errors_1.wrapUnhandledBackendError)(err, String(rawName));
221
179
  }
222
180
  return err;
223
181
  },
@@ -259,15 +217,11 @@ const brokerConfig = {
259
217
  },
260
218
  },
261
219
  // Register custom middlewares
262
- middlewares: [(0, tracer_1.createTracerMiddleware)()],
220
+ middlewares: [],
263
221
  // Register custom REPL commands.
264
222
  replCommands: null,
265
223
  // Called after broker created.
266
224
  // created(broker: ServiceBroker): void {},
267
- // Called after broker started.
268
- started() {
269
- console.log("ALLOWED ORIGINS", (process.env.ORIGINS ?? "").split(","));
270
- },
271
225
  // Called after broker stopped.
272
226
  // async stopped(broker: ServiceBroker): Promise<void> {},
273
227
  };
@@ -165,9 +165,18 @@ const envSchema = z.object({
165
165
  LOG_LEVEL: z
166
166
  .enum(["fatal", "error", "warn", "info", "debug", "trace", "silent"])
167
167
  .default("info"),
168
+ LOG_FILE_ENABLED: z
169
+ .string()
170
+ .optional()
171
+ .transform((value) => value === "true"),
172
+ LOG_FILE_PATH: z.string().min(1).default(".tellymcp/log.jsonl"),
168
173
  });
169
174
  function loadConfig() {
170
- if ((0, node_fs_1.existsSync)(".env")) {
175
+ const explicitEnvFile = process.env.ENV_FILE?.trim();
176
+ if (explicitEnvFile) {
177
+ process.loadEnvFile(explicitEnvFile);
178
+ }
179
+ else if ((0, node_fs_1.existsSync)(".env")) {
171
180
  process.loadEnvFile(".env");
172
181
  }
173
182
  const parsed = envSchema.parse(process.env);
@@ -312,6 +321,8 @@ function loadConfig() {
312
321
  },
313
322
  logging: {
314
323
  level: parsed.LOG_LEVEL,
324
+ fileEnabled: parsed.LOG_FILE_ENABLED,
325
+ filePath: parsed.LOG_FILE_PATH,
315
326
  },
316
327
  };
317
328
  }
@@ -136,7 +136,7 @@ function createMcpHttpHandler(runtime, input) {
136
136
  if (requestUrl.pathname === "/healthz") {
137
137
  writeJson(res, 200, {
138
138
  ok: true,
139
- service: "telegram-human-mcp",
139
+ service: "tellymcp",
140
140
  transport: "streamable-http",
141
141
  });
142
142
  return;
@@ -5,8 +5,8 @@ const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
5
5
  const registry_1 = require("../../../shared/api/tool-registry/registry");
6
6
  function createMcpServer(tools) {
7
7
  const server = new mcp_js_1.McpServer({
8
- name: "telegram-human-mcp",
9
- version: "1.0.0",
8
+ name: "tellymcp",
9
+ version: "0.0.6",
10
10
  });
11
11
  (0, registry_1.registerTools)(server, tools);
12
12
  return server;
@@ -241,7 +241,7 @@ class GatewayHttpService {
241
241
  if (pathname === "/gateway/healthz") {
242
242
  writeJson(res, 200, {
243
243
  ok: true,
244
- service: "telegram-human-mcp-gateway",
244
+ service: "tellymcp-gateway",
245
245
  mode: this.config.distributed.mode,
246
246
  databaseConfigured: Boolean(process.env.DB_HOST && process.env.DB_NAME),
247
247
  s3Configured: Boolean(this.config.distributed.gatewayS3Bucket),
@@ -4,10 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.createLogger = createLogger;
7
- const node_fs_1 = require("node:fs");
8
- const node_path_1 = require("node:path");
9
7
  const pino_1 = __importDefault(require("pino"));
10
- const DEFAULT_LOG_FILE_PATH = ".telegram-human-mcp/log.jsonl";
8
+ const pinoTargets_1 = require("../../../../../../../lib/pinoTargets");
11
9
  function write(logger, level, message, meta) {
12
10
  if (meta) {
13
11
  logger[level](meta, message);
@@ -16,32 +14,15 @@ function write(logger, level, message, meta) {
16
14
  logger[level](message);
17
15
  }
18
16
  function createLogger(config) {
19
- (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(DEFAULT_LOG_FILE_PATH), { recursive: true });
20
17
  const transport = pino_1.default.transport({
21
- targets: [
22
- {
23
- target: "pino/file",
24
- level: config.logging.level,
25
- options: {
26
- destination: DEFAULT_LOG_FILE_PATH,
27
- mkdir: true,
28
- },
29
- },
30
- {
31
- target: "pino-pretty",
32
- level: config.logging.level,
33
- options: {
34
- destination: 2,
35
- colorize: true,
36
- translateTime: "SYS:standard",
37
- ignore: "pid,hostname",
38
- singleLine: false,
39
- },
40
- },
41
- ],
18
+ targets: (0, pinoTargets_1.createPinoTargets)({
19
+ level: config.logging.level,
20
+ fileEnabled: config.logging.fileEnabled,
21
+ filePath: config.logging.filePath,
22
+ }),
42
23
  });
43
24
  const baseLogger = (0, pino_1.default)({
44
- name: "telegram-human-mcp",
25
+ name: "tellymcp",
45
26
  level: config.logging.level,
46
27
  timestamp: pino_1.default.stdTimeFunctions.isoTime,
47
28
  }, transport);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deadragdoll/tellymcp",
3
- "version": "0.0.5",
4
- "description": "TellyMCP - Telegram Human-in-the-Loop MCP Server",
3
+ "version": "0.0.7",
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": {
7
7
  "tellymcp": "dist/cli.js"
@@ -75,7 +75,6 @@
75
75
  "amqplib": "0.10.9",
76
76
  "dotenv": "^17.4.2",
77
77
  "grammy": "1.43.0",
78
- "graphql": "^16.14.0",
79
78
  "https-proxy-agent": "^7.0.6",
80
79
  "ioredis": "5.10.1",
81
80
  "knex": "^3.2.10",
@@ -88,7 +87,6 @@
88
87
  "pino": "^10.3.1",
89
88
  "pino-pretty": "^13.1.3",
90
89
  "playwright": "^1.60.0",
91
- "redis": "5.12.1",
92
90
  "socks-proxy-agent": "^8.0.5",
93
91
  "ws": "8.20.1",
94
92
  "zod": "4.4.3"
@@ -1,172 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createTracerMiddleware = void 0;
4
- const traceContext_1 = require("../traceContext");
5
- const trace_1 = require("../trace");
6
- const shouldSkipTrace = (actionName, ctx) => Boolean(ctx?.meta?.$traceInternal ||
7
- !actionName ||
8
- actionName === "context" ||
9
- actionName === "graphql.publish" ||
10
- (actionName === "rest" && ctx?.params?.req?.url === "/api/graphql"));
11
- const createTracerMiddleware = () => {
12
- let broker = null;
13
- const resolveTracer = (action) => {
14
- const rawName = String(action?.rawName || action?.name || "");
15
- if (action?.tracer)
16
- return { tracer: action.tracer, source: "action.tracer" };
17
- if (action?.schema?.tracer)
18
- return { tracer: action.schema.tracer, source: "action.schema.tracer" };
19
- if (action?.service?.schema?.actions?.[rawName]?.tracer) {
20
- return {
21
- tracer: action.service.schema.actions[rawName].tracer,
22
- source: "service.schema.actions[rawName].tracer",
23
- };
24
- }
25
- return {};
26
- };
27
- const callTrace = async (ctx, actionName, params, silent = false) => {
28
- if (!broker) {
29
- return null;
30
- }
31
- try {
32
- return await broker.call(actionName, params, {
33
- requestID: ctx.requestID,
34
- meta: {
35
- user: ctx.meta?.user,
36
- $traceInternal: true,
37
- },
38
- });
39
- }
40
- catch (error) {
41
- if (!silent) {
42
- broker.logger.warn("Tracer call failed", {
43
- actionName,
44
- sourceAction: ctx.action?.name || null,
45
- error: error instanceof Error ? error.message : String(error),
46
- });
47
- }
48
- return null;
49
- }
50
- };
51
- return {
52
- created(localBroker) {
53
- broker = localBroker;
54
- },
55
- localAction(next, action) {
56
- const actionName = String(action?.rawName || action?.name || "");
57
- const serviceName = String(action?.service?.name || "");
58
- return async function traceAction(ctx) {
59
- if (serviceName === "trace" || shouldSkipTrace(actionName, ctx)) {
60
- return next(ctx);
61
- }
62
- const resolvedTracer = resolveTracer(ctx?.action || action);
63
- const tracer = resolvedTracer.tracer;
64
- let traceMeta = null;
65
- const startSessionName = tracer?.startSession;
66
- const tracerTag = tracer?.tag;
67
- const shouldStartRootSession = Boolean(startSessionName);
68
- if (!shouldStartRootSession) {
69
- traceMeta = await (0, traceContext_1.loadTraceContext)();
70
- }
71
- let startedSession = false;
72
- if (shouldStartRootSession) {
73
- await (0, traceContext_1.deleteTraceContext)().catch(() => null);
74
- const session = (await callTrace(ctx, "trace.startSession", {
75
- name: startSessionName,
76
- tag: tracerTag || null,
77
- source: actionName,
78
- meta: (0, trace_1.sanitizeTraceValue)({
79
- action: actionName,
80
- params: ctx.params,
81
- }),
82
- }, false));
83
- if (session?.session_id) {
84
- traceMeta = {
85
- sessionId: String(session.session_id),
86
- name: startSessionName ?? null,
87
- tag: tracerTag || null,
88
- rootAction: actionName,
89
- startedBy: actionName,
90
- };
91
- await (0, traceContext_1.saveTraceContext)(traceMeta);
92
- startedSession = true;
93
- }
94
- }
95
- const sessionId = String(traceMeta?.sessionId || "").trim();
96
- const hasSession = Boolean(sessionId);
97
- const startedAt = Date.now();
98
- if (hasSession) {
99
- await (0, traceContext_1.touchTraceContext)().catch(() => null);
100
- await callTrace(ctx, "trace.log", {
101
- session_id: sessionId,
102
- level: (0, trace_1.normalizeTraceLevel)(tracer?.level || "debug"),
103
- action: actionName,
104
- state: "started",
105
- marker: tracer?.marker || null,
106
- step: tracer?.step || "action",
107
- message: actionName,
108
- data: (0, trace_1.buildTraceStartData)(ctx, actionName, tracer, startedSession),
109
- }, false);
110
- }
111
- try {
112
- const result = await next(ctx);
113
- const durationMs = Date.now() - startedAt;
114
- if (hasSession) {
115
- await callTrace(ctx, "trace.log", {
116
- session_id: sessionId,
117
- level: (0, trace_1.normalizeTraceLevel)(tracer?.level || "debug"),
118
- action: actionName,
119
- state: "succeeded",
120
- marker: tracer?.marker || null,
121
- step: tracer?.step || "action",
122
- message: actionName,
123
- data: (0, trace_1.buildTraceSuccessData)(ctx, actionName, result, durationMs, tracer),
124
- }, false);
125
- if (tracer?.stopSession) {
126
- await callTrace(ctx, "trace.endSession", {
127
- session_id: sessionId,
128
- status: "succeeded",
129
- summary: `${actionName} completed`,
130
- meta: (0, trace_1.sanitizeTraceValue)({
131
- action: actionName,
132
- durationMs,
133
- }),
134
- }, false);
135
- await (0, traceContext_1.deleteTraceContext)();
136
- }
137
- }
138
- return result;
139
- }
140
- catch (error) {
141
- const durationMs = Date.now() - startedAt;
142
- if (hasSession) {
143
- await callTrace(ctx, "trace.log", {
144
- session_id: sessionId,
145
- level: "error",
146
- action: actionName,
147
- state: "failed",
148
- marker: tracer?.marker || null,
149
- step: tracer?.step || "action",
150
- message: actionName,
151
- data: (0, trace_1.buildTraceErrorData)(ctx, actionName, error, durationMs, tracer),
152
- }, false);
153
- if (tracer?.stopSession || startedSession) {
154
- await callTrace(ctx, "trace.endSession", {
155
- session_id: sessionId,
156
- status: "failed",
157
- summary: error instanceof Error ? error.message : `Action failed: ${actionName}`,
158
- meta: (0, trace_1.sanitizeTraceValue)({
159
- action: actionName,
160
- durationMs,
161
- }),
162
- }, false);
163
- await (0, traceContext_1.deleteTraceContext)();
164
- }
165
- }
166
- throw error;
167
- }
168
- };
169
- },
170
- };
171
- };
172
- exports.createTracerMiddleware = createTracerMiddleware;
package/dist/lib/trace.js DELETED
@@ -1,147 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.buildTraceErrorData = exports.buildTraceSuccessData = exports.buildTraceStartData = exports.buildTraceMetaSummary = exports.sanitizeTraceValue = exports.normalizeTraceLevel = void 0;
4
- const TRACE_MAX_DEPTH = 4;
5
- const TRACE_MAX_ARRAY = 25;
6
- const TRACE_MAX_KEYS = 40;
7
- const TRACE_MAX_STRING = 1000;
8
- const truncateString = (value) => value.length > TRACE_MAX_STRING ? `${value.slice(0, TRACE_MAX_STRING)}...` : value;
9
- const normalizeTraceLevel = (value) => {
10
- const level = String(value || "info").toLowerCase();
11
- if (["fatal", "error", "warn", "info", "debug", "trace"].includes(level)) {
12
- return level;
13
- }
14
- return "info";
15
- };
16
- exports.normalizeTraceLevel = normalizeTraceLevel;
17
- const sanitizeError = (error, depth, seen) => {
18
- if (!(error instanceof Error)) {
19
- return (0, exports.sanitizeTraceValue)(error, depth, seen);
20
- }
21
- return {
22
- name: error.name,
23
- message: truncateString(error.message || ""),
24
- stack: truncateString(error.stack || ""),
25
- };
26
- };
27
- const sanitizeTraceValue = (value, depth = 0, seen = new WeakSet()) => {
28
- if (value == null || typeof value === "boolean" || typeof value === "number") {
29
- return value;
30
- }
31
- if (typeof value === "string") {
32
- return truncateString(value);
33
- }
34
- if (typeof value === "bigint") {
35
- return value.toString();
36
- }
37
- if (typeof value === "symbol") {
38
- return value.toString();
39
- }
40
- if (typeof value === "function") {
41
- return `[Function ${value.name || "anonymous"}]`;
42
- }
43
- if (value instanceof Date) {
44
- return value.toISOString();
45
- }
46
- if (value instanceof Error) {
47
- return sanitizeError(value, depth, seen);
48
- }
49
- if (Buffer.isBuffer(value)) {
50
- return `[Buffer ${value.length}]`;
51
- }
52
- if (value instanceof Set) {
53
- return {
54
- type: "Set",
55
- size: value.size,
56
- values: Array.from(value.values())
57
- .slice(0, TRACE_MAX_ARRAY)
58
- .map(item => (0, exports.sanitizeTraceValue)(item, depth + 1, seen)),
59
- };
60
- }
61
- if (value instanceof Map) {
62
- return {
63
- type: "Map",
64
- size: value.size,
65
- entries: Array.from(value.entries())
66
- .slice(0, TRACE_MAX_ARRAY)
67
- .map(([key, item]) => [
68
- (0, exports.sanitizeTraceValue)(key, depth + 1, seen),
69
- (0, exports.sanitizeTraceValue)(item, depth + 1, seen),
70
- ]),
71
- };
72
- }
73
- if (Array.isArray(value)) {
74
- if (depth >= TRACE_MAX_DEPTH) {
75
- return `[Array(${value.length})]`;
76
- }
77
- return value
78
- .slice(0, TRACE_MAX_ARRAY)
79
- .map(item => (0, exports.sanitizeTraceValue)(item, depth + 1, seen));
80
- }
81
- if (typeof value === "object") {
82
- const objectValue = value;
83
- if (seen.has(objectValue)) {
84
- return "[Circular]";
85
- }
86
- seen.add(objectValue);
87
- if (depth >= TRACE_MAX_DEPTH) {
88
- return `[${objectValue?.constructor?.name || "Object"}]`;
89
- }
90
- const keys = Object.keys(objectValue).slice(0, TRACE_MAX_KEYS);
91
- const result = {};
92
- for (const key of keys) {
93
- result[key] = (0, exports.sanitizeTraceValue)(objectValue[key], depth + 1, seen);
94
- }
95
- if (Object.keys(objectValue).length > TRACE_MAX_KEYS) {
96
- result.__truncatedKeys = Object.keys(objectValue).length - TRACE_MAX_KEYS;
97
- }
98
- return result;
99
- }
100
- return String(value);
101
- };
102
- exports.sanitizeTraceValue = sanitizeTraceValue;
103
- const pickFields = (source, fields) => {
104
- if (fields === false) {
105
- return undefined;
106
- }
107
- if (fields === true || fields == null) {
108
- return (0, exports.sanitizeTraceValue)(source);
109
- }
110
- if (!source || typeof source !== "object") {
111
- return (0, exports.sanitizeTraceValue)(source);
112
- }
113
- const result = {};
114
- for (const key of fields) {
115
- result[key] = (0, exports.sanitizeTraceValue)(source[key]);
116
- }
117
- return result;
118
- };
119
- const buildTraceMetaSummary = (ctx) => (0, exports.sanitizeTraceValue)({
120
- requestID: ctx.requestID || null,
121
- userSub: ctx.meta?.user?.sub || null,
122
- });
123
- exports.buildTraceMetaSummary = buildTraceMetaSummary;
124
- const buildTraceStartData = (ctx, actionName, tracer, startedSession = false) => (0, exports.sanitizeTraceValue)({
125
- action: actionName,
126
- startedSession,
127
- params: pickFields(ctx.params, tracer?.captureParams),
128
- meta: (0, exports.buildTraceMetaSummary)(ctx),
129
- });
130
- exports.buildTraceStartData = buildTraceStartData;
131
- const buildTraceSuccessData = (ctx, actionName, result, durationMs, tracer) => (0, exports.sanitizeTraceValue)({
132
- action: actionName,
133
- durationMs,
134
- result: tracer?.captureResult === undefined
135
- ? undefined
136
- : pickFields(result, tracer.captureResult),
137
- });
138
- exports.buildTraceSuccessData = buildTraceSuccessData;
139
- const buildTraceErrorData = (ctx, actionName, error, durationMs, tracer) => (0, exports.sanitizeTraceValue)({
140
- action: actionName,
141
- durationMs,
142
- error: tracer?.captureError === undefined
143
- ? sanitizeError(error, 0, new WeakSet())
144
- : pickFields(error, tracer.captureError),
145
- meta: (0, exports.buildTraceMetaSummary)(ctx),
146
- });
147
- exports.buildTraceErrorData = buildTraceErrorData;
@@ -1,116 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.deleteTraceContext = exports.touchTraceContext = exports.loadTraceContext = exports.saveTraceContext = void 0;
37
- const redis = __importStar(require("redis"));
38
- const trace_1 = require("./trace");
39
- const TRACE_CONTEXT_TTL_SEC = Math.max(1, Number(process.env.TRACE_CONTEXT_TTL_SEC || 2));
40
- const TRACE_CONTEXT_KEY = "trace:active";
41
- let traceRedisClient = null;
42
- let traceRedisConnectPromise = null;
43
- const createTraceRedisClient = () => redis.createClient({
44
- socket: {
45
- host: process.env.REDIS_HOST || "localhost",
46
- port: +(process.env.REDIS_PORT || 6379),
47
- },
48
- database: +(process.env.REDIS_DB || 0),
49
- name: `${process.env.APP_NAME || process.env.APPNAME || "app"}:trace`,
50
- });
51
- const getTraceRedisClient = async () => {
52
- if (traceRedisClient?.isOpen) {
53
- return traceRedisClient;
54
- }
55
- if (!traceRedisClient) {
56
- traceRedisClient = createTraceRedisClient();
57
- }
58
- if (!traceRedisConnectPromise) {
59
- traceRedisConnectPromise = traceRedisClient.connect().then(() => traceRedisClient);
60
- }
61
- try {
62
- return await traceRedisConnectPromise;
63
- }
64
- finally {
65
- traceRedisConnectPromise = null;
66
- }
67
- };
68
- const saveTraceContext = async (trace) => {
69
- if (!trace?.sessionId) {
70
- return false;
71
- }
72
- const client = await getTraceRedisClient();
73
- await client.setEx(TRACE_CONTEXT_KEY, TRACE_CONTEXT_TTL_SEC, JSON.stringify((0, trace_1.sanitizeTraceValue)(trace)));
74
- return true;
75
- };
76
- exports.saveTraceContext = saveTraceContext;
77
- const loadTraceContext = async () => {
78
- const client = await getTraceRedisClient();
79
- const raw = await client.get(TRACE_CONTEXT_KEY);
80
- if (!raw) {
81
- return null;
82
- }
83
- try {
84
- const parsed = JSON.parse(Buffer.isBuffer(raw) ? raw.toString("utf-8") : String(raw));
85
- if (!parsed || typeof parsed !== "object") {
86
- return null;
87
- }
88
- return {
89
- sessionId: String(parsed.sessionId || ""),
90
- name: parsed.name || null,
91
- tag: parsed.tag || null,
92
- rootAction: parsed.rootAction || null,
93
- startedBy: parsed.startedBy || null,
94
- };
95
- }
96
- catch {
97
- return null;
98
- }
99
- };
100
- exports.loadTraceContext = loadTraceContext;
101
- const touchTraceContext = async () => {
102
- const client = await getTraceRedisClient();
103
- const exists = await client.exists(TRACE_CONTEXT_KEY);
104
- if (!exists) {
105
- return false;
106
- }
107
- await client.expire(TRACE_CONTEXT_KEY, TRACE_CONTEXT_TTL_SEC);
108
- return true;
109
- };
110
- exports.touchTraceContext = touchTraceContext;
111
- const deleteTraceContext = async () => {
112
- const client = await getTraceRedisClient();
113
- await client.del(TRACE_CONTEXT_KEY);
114
- return true;
115
- };
116
- exports.deleteTraceContext = deleteTraceContext;