telecodex 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,186 +1,151 @@
1
1
  # telecodex
2
2
 
3
- Telegram bridge for local Codex built on the official TypeScript SDK.
3
+ Use Telegram forum topics as a remote interface for local Codex.
4
4
 
5
- ## Model
6
-
7
- ```text
8
- Telegram private chat
9
- -> one-time admin bootstrap
10
-
11
- Telegram forum supergroup
12
- -> one project per supergroup
13
- -> one topic per Codex thread
14
-
15
- Telegram
16
- -> grammY bot + runner
17
- -> CodexSdkRuntime
18
- -> @openai/codex-sdk
19
- -> local Codex login
20
- ```
21
-
22
- The bot talks to local Codex through `@openai/codex-sdk`, which wraps the local
23
- `codex` CLI. It does not depend on `codex app-server`.
24
-
25
- ## Runtime contract
26
-
27
- - Telegram is treated as a remote task interface, not a clone of Codex Desktop.
28
- - One topic maps to one Codex SDK thread.
29
- - Each topic has at most one active SDK run.
30
- - Follow-up messages during an active run are queued and processed in order.
31
- - The pending queue is in-memory only and is cleared on restart.
32
- - Text and image messages are mapped to Codex SDK input.
33
- - A run immediately creates a normal Telegram status message; progress edits that message.
34
- - telecodex does not use pinned messages for live state.
35
- - While a run is pending, the bot sends Telegram `typing` activity so the chat does not look dead during long SDK gaps.
36
- - `/status` is the source of truth for runtime state, active thread id, last SDK event, and queue depth.
5
+ `telecodex` connects a Telegram bot to your local `codex` CLI through the
6
+ official TypeScript SDK. It is meant for remote task execution, not as a full
7
+ clone of Codex Desktop.
37
8
 
38
9
  ## Requirements
39
10
 
40
- - Node.js 24 or newer.
41
- - A local `codex` CLI installation available on `PATH`.
42
- - A valid local Codex login:
11
+ - Node.js 24 or newer
12
+ - A local `codex` CLI installation available on `PATH`
13
+ - A valid local Codex login
14
+ - A Telegram bot token
15
+
16
+ Check Codex login first:
43
17
 
44
18
  ```bash
45
19
  codex login status
46
20
  ```
47
21
 
48
- - A Telegram bot token.
49
-
50
- ## Install from npm
22
+ ## Install
51
23
 
52
24
  ```bash
53
25
  npm install -g telecodex
54
26
  telecodex
55
27
  ```
56
28
 
57
- `telecodex` uses the local `codex` CLI at runtime, so installing this package
58
- does not replace the separate Codex CLI installation.
29
+ Installing `telecodex` does not replace the separate `codex` CLI. The bot uses
30
+ your local Codex installation at runtime.
59
31
 
60
- ## Local development
32
+ ## First Launch
61
33
 
62
- 1. Install dependencies:
34
+ On first launch, `telecodex`:
63
35
 
64
- ```bash
65
- npm install
66
- ```
36
+ 1. Finds or asks for the local `codex` binary path.
37
+ 2. Verifies local Codex login.
38
+ 3. Prompts for a Telegram bot token if none is stored yet.
39
+ 4. Generates a one-time bootstrap code if no Telegram admin is bound yet.
40
+ 5. Waits for that code in a private Telegram chat with the bot.
67
41
 
68
- 2. Start it:
42
+ The first successful sender becomes the admin for that bot instance.
69
43
 
70
- ```bash
71
- npm run dev
72
- ```
44
+ Optional security override:
73
45
 
74
- For a production-style local install during development, `npm link` exposes the
75
- same `telecodex` command globally from the current checkout.
46
+ - `TELECODEX_ALLOW_PLAINTEXT_TOKEN_FALLBACK=1` allows storing the Telegram bot
47
+ token unencrypted in local state when the system keychain is unavailable.
48
+ This is disabled by default.
76
49
 
77
- ## Automated npm release
50
+ ## How It Works
78
51
 
79
- This repository is set up for npm trusted publishing from GitHub Actions.
52
+ - One Telegram forum supergroup represents one project.
53
+ - One topic inside that supergroup represents one Codex thread.
54
+ - Work happens by sending normal messages inside the topic.
55
+ - While a run is active, follow-up messages are queued automatically.
56
+ - `/status` shows the current runtime state.
80
57
 
81
- 1. On npm, open the `telecodex` package settings and configure a trusted publisher:
82
- - Organization or user: `jiangege`
83
- - Repository: `telecodex`
84
- - Workflow filename: `publish.yml`
85
- 2. Bump the version locally:
58
+ Private chat is only for bootstrap and lightweight admin actions.
86
59
 
87
- ```bash
88
- npm version patch
60
+ ## Quick Start
61
+
62
+ Inside a Telegram forum supergroup:
63
+
64
+ 1. Bind the group to a project root:
65
+
66
+ ```text
67
+ /project bind /absolute/path/to/project
89
68
  ```
90
69
 
91
- 3. Push the branch and tag:
70
+ 2. Create a fresh topic for a new Codex thread:
92
71
 
93
- ```bash
94
- git push origin main --follow-tags
72
+ ```text
73
+ /thread new My Task
95
74
  ```
96
75
 
97
- Pushing a `v*` tag runs `.github/workflows/publish.yml`, which installs dependencies,
98
- runs `npm run check`, runs `npm test`, and publishes the package to npm when the tag
99
- matches the version in `package.json`.
76
+ 3. Or resume an existing thread:
100
77
 
101
- ## First launch
78
+ ```text
79
+ /thread list
80
+ /thread resume <threadId>
81
+ ```
102
82
 
103
- On first launch, `telecodex`:
83
+ 4. Send normal messages in the topic to work with Codex.
104
84
 
105
- 1. Finds or asks for the local `codex` binary path.
106
- 2. Verifies Codex login.
107
- 3. Asks for a Telegram bot token if none is stored yet.
108
- 4. Generates a one-time bootstrap code if no Telegram admin is bound yet.
109
- 5. Waits for that code in a private chat. The first successful sender becomes the permanent admin for this bot instance.
85
+ ## Commands
110
86
 
111
- Bootstrap codes are time-limited and attempt-limited. When a code expires or is exhausted, start telecodex again locally to issue a fresh one.
87
+ ### General
112
88
 
113
- There are no required environment variables in the normal startup path.
89
+ - `/start` or `/help` - show usage help
90
+ - `/status` - show current state
91
+ - `/stop` - interrupt the active run in the current topic
114
92
 
115
- Optional security override:
93
+ ### Admin
116
94
 
117
- - `TELECODEX_ALLOW_PLAINTEXT_TOKEN_FALLBACK=1` allows storing the Telegram bot token unencrypted in local state when the system keychain is unavailable. This is disabled by default.
95
+ - `/admin` - show admin binding and handoff state
96
+ - `/admin rebind` - issue a temporary handoff code
97
+ - `/admin cancel` - cancel a pending handoff
118
98
 
119
- ## Working model
99
+ ### Project
120
100
 
121
- - Private chat is only for bootstrap and lightweight management.
122
- - One forum supergroup represents one project.
123
- - The project root is bound once with `/project bind <absolute-path>`.
124
- - Each topic in that supergroup is one Codex thread.
125
- - Work happens by sending normal messages inside the topic.
126
- - `/thread list` shows the saved Codex threads already recorded for the bound project root or its subdirectories.
127
- - `/thread new <topic-name>` automatically creates a new topic; the first normal message inside it starts a fresh Codex thread.
128
- - `/thread resume <threadId>` automatically creates a new topic and binds it to an existing thread id.
129
- - On startup, telecodex probes stored topic bindings once and removes bindings whose Telegram topics no longer exist.
130
- - On first launch after upgrading from the old SQLite state format, telecodex imports the legacy state once and then deletes the old SQLite files (`state.sqlite` plus sidecars such as `-wal`/`-shm`).
101
+ - `/project` - show the current project binding
102
+ - `/project bind <absolute-path>` - bind the current supergroup to a project root
103
+ - `/project unbind` - remove the project binding
131
104
 
132
- ## Stored state
105
+ ### Threads
133
106
 
134
- - Telegram bot token: stored in the system keychain when available. Plaintext local fallback is disabled by default and must be opted into explicitly.
135
- - Codex thread history: read directly from Codex session files under `$CODEX_HOME/sessions` (or `~/.codex/sessions` by default).
136
- - Admin binding, project bindings, and durable Telegram topic configuration: stored as local JSON files under `~/.telecodex/state/`.
137
- - Legacy upgrade path: if `~/.telecodex/state.sqlite` exists from an older telecodex version, it is imported once into the JSON state files and then cleaned up together with any SQLite sidecar files that remain.
138
- - Runtime logs: written by `pino` to `~/.telecodex/logs/telecodex.log`.
139
- - Working directory: defaults to the directory where you ran `telecodex`.
107
+ - `/thread` - show the current attached thread id in a topic
108
+ - `/thread list` - list saved Codex threads for the current project
109
+ - `/thread new <topic-name>` - create a fresh topic for a new thread
110
+ - `/thread resume <threadId>` - create a topic and bind it to an existing thread
140
111
 
141
- ## Logs
112
+ ### Session Configuration
142
113
 
143
- - Startup prints the active log file path.
144
- - Telegram middleware errors and message edit failures are appended to the log file.
145
- - When you need to debug a running instance later, inspect `~/.telecodex/logs/telecodex.log` first.
114
+ - `/cwd <absolute-path>`
115
+ - `/mode read|write|danger|yolo`
116
+ - `/sandbox <read-only|workspace-write|danger-full-access>`
117
+ - `/approval <on-request|on-failure|never>`
118
+ - `/yolo on|off`
119
+ - `/model <model-id>`
120
+ - `/effort default|minimal|low|medium|high|xhigh`
121
+ - `/web default|disabled|cached|live`
122
+ - `/network on|off`
123
+ - `/gitcheck skip|enforce`
124
+ - `/adddir list|add <path-inside-project>|add-external <absolute-path>|drop <index>|clear`
125
+ - `/schema show|set <JSON object>|clear`
126
+ - `/codexconfig show|set <JSON object>|clear`
146
127
 
147
- ## Commands
128
+ ## Images
129
+
130
+ - Sending an image in a topic is supported.
131
+ - Telegram photos and image documents are downloaded locally and sent to Codex as
132
+ `local_image` input.
133
+ - Image output is not rendered inline in Telegram text messages.
134
+
135
+ ## Storage
136
+
137
+ - Telegram bot token: stored in the system keychain when available
138
+ - Durable local state: `~/.telecodex/state/`
139
+ - Runtime logs: `~/.telecodex/logs/telecodex.log`
140
+ - Codex thread history: read from Codex session files under `$CODEX_HOME/sessions`
141
+ (or `~/.codex/sessions` by default)
142
+
143
+ If an older `~/.telecodex/state.sqlite` exists, telecodex imports it once into
144
+ the JSON state files and then removes the old SQLite files.
145
+
146
+ ## Troubleshooting
147
+
148
+ - If startup reports a login problem, run `codex login`.
149
+ - If the bot appears idle for a long time, check `/status`.
150
+ - If you need logs, inspect `~/.telecodex/logs/telecodex.log`.
148
151
 
149
- - `/start` or `/help` - show the current usage model.
150
- - `/status` - in private chat shows global state; in a project topic shows project/thread runtime state.
151
- - `/admin` - in private chat, show admin binding and handoff status.
152
- - `/admin rebind` - in private chat, issue a time-limited handoff code for transferring control to another Telegram account.
153
- - `/admin cancel` - in private chat, cancel a pending admin handoff code.
154
- - `/project` - show the current supergroup's project binding.
155
- - `/project bind <absolute-path>` - bind the current supergroup to a project root.
156
- - `/project unbind` - remove the current supergroup's project binding.
157
- - `/thread` - in a topic, show the current attached thread id.
158
- - `/thread list` - list saved Codex threads whose working directory is inside the current project root.
159
- - `/thread new <topic-name>` - create a new topic for a fresh Codex thread.
160
- - `/thread resume <threadId>` - create a new topic and bind it to an existing saved Codex thread id from the current project.
161
- - Normal text in a topic - send that message to the current Codex thread.
162
- - `/stop` - interrupt the active SDK run.
163
- - `/cwd <absolute-path>` - switch the topic working directory inside the current project root.
164
- - `/mode read|write|danger|yolo` - switch runtime presets for the current topic.
165
- - `/sandbox <read-only|workspace-write|danger-full-access>` - set sandbox explicitly for the current topic.
166
- - `/approval <on-request|on-failure|never>` - set approval policy explicitly for the current topic.
167
- - `/yolo on|off` - quick toggle for `danger-full-access + never` on the current topic.
168
- - `/model <model-id>` - set model for the current topic.
169
- - `/effort default|minimal|low|medium|high|xhigh` - set model reasoning effort for the current topic.
170
- - `/web default|disabled|cached|live` - set Codex SDK web search mode.
171
- - `/network on|off` - set workspace network access for Codex SDK runs.
172
- - `/gitcheck skip|enforce` - control Codex SDK git repository checks.
173
- - `/adddir list|add <path-inside-project>|add-external <absolute-path>|drop <index>|clear` - manage Codex SDK additional directories. `add` stays inside the project root; `add-external` is the explicit escape hatch.
174
- - `/schema show|set <JSON object>|clear` - manage Codex SDK output schema for the current topic.
175
- - `/codexconfig show|set <JSON object>|clear` - manage global non-auth Codex SDK config overrides for future runs.
176
- - Image messages in a topic - download the Telegram image locally and send it as SDK `local_image` input.
177
-
178
- ## Notes
179
-
180
- - Long polling is managed by `@grammyjs/runner`.
181
- - Streaming updates are throttled before editing Telegram messages.
182
- - Final answers are rendered from Markdown to Telegram-safe HTML.
183
- - Project-scoped path checks resolve symlinks before enforcing the root boundary, so topic cwd changes cannot escape the bound project through symlink paths.
184
- - Because the SDK run and pending queue are in-process, a telecodex restart cannot resume a partially streamed Telegram turn and clears queued follow-up messages.
185
- - Authentication and provider switching remain owned by the local Codex installation; telecodex does not manage API keys or login state.
186
- - Interactive terminal stdin bridging and native Codex approval UI are intentionally not part of the Telegram contract. For unattended remote work, use the topic's sandbox/approval preset deliberately.
package/dist/bot/auth.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { replyError, replyNotice } from "../telegram/formatted.js";
1
2
  export function authMiddleware(input) {
2
3
  return async (ctx, next) => {
3
4
  const userId = ctx.from?.id;
@@ -10,7 +11,7 @@ export function authMiddleware(input) {
10
11
  hasTextMessage: Boolean(ctx.message?.text),
11
12
  });
12
13
  if (ctx.message?.text && ctx.chat?.type !== "private") {
13
- await ctx.reply("This message was sent as the group identity or as an anonymous admin. telecodex cannot verify the operator. Send it from your personal account instead.");
14
+ await replyError(ctx, "This message was sent as the group identity or as an anonymous admin. telecodex cannot verify the operator. Send it from your personal account instead.");
14
15
  }
15
16
  return;
16
17
  }
@@ -31,7 +32,7 @@ export function authMiddleware(input) {
31
32
  store: input.store,
32
33
  success: async () => {
33
34
  input.store.rebindAuthorizedUserId(userId);
34
- await ctx.reply("Admin handoff succeeded. This Telegram account is now authorized to use telecodex.");
35
+ await replyNotice(ctx, "Admin handoff succeeded. This Telegram account is now authorized to use telecodex.");
35
36
  },
36
37
  mismatchLabel: "Admin handoff code did not match.",
37
38
  exhaustedLabel: "Admin handoff code exhausted its attempt limit and was invalidated. Issue a new one from the currently authorized account.",
@@ -68,7 +69,7 @@ export function authMiddleware(input) {
68
69
  const claimedUserId = input.store.claimAuthorizedUserId(userId);
69
70
  if (claimedUserId === userId) {
70
71
  input.onAdminBound?.(userId);
71
- await ctx.reply("Admin binding succeeded. Only this Telegram account can use this bot from now on.");
72
+ await replyNotice(ctx, "Admin binding succeeded. Only this Telegram account can use this bot from now on.");
72
73
  return;
73
74
  }
74
75
  await deny(ctx, "An admin account has already claimed this bot.");
@@ -79,7 +80,7 @@ export function authMiddleware(input) {
79
80
  if (handled)
80
81
  return;
81
82
  }
82
- await ctx.reply("This bot is not initialized yet. Send the binding code shown in the startup logs to complete the one-time admin binding.");
83
+ await replyNotice(ctx, "This bot is not initialized yet. Send the binding code shown in the startup logs to complete the one-time admin binding.");
83
84
  };
84
85
  }
85
86
  async function handleBindingCodeMessage(input) {
@@ -92,14 +93,14 @@ async function handleBindingCodeMessage(input) {
92
93
  }
93
94
  const attempt = input.store.recordBindingCodeFailure();
94
95
  if (!attempt) {
95
- await input.ctx.reply("The binding code is no longer active. Issue a new one and try again.");
96
+ await replyError(input.ctx, "The binding code is no longer active. Issue a new one and try again.");
96
97
  return true;
97
98
  }
98
99
  if (attempt.exhausted) {
99
- await input.ctx.reply(input.exhaustedLabel);
100
+ await replyError(input.ctx, input.exhaustedLabel);
100
101
  return true;
101
102
  }
102
- await input.ctx.reply(`${input.mismatchLabel}\nRemaining attempts: ${attempt.remaining}`);
103
+ await replyError(input.ctx, input.mismatchLabel, `Remaining attempts: ${attempt.remaining}`);
103
104
  return true;
104
105
  }
105
106
  async function deny(ctx, text) {
@@ -108,6 +109,6 @@ async function deny(ctx, text) {
108
109
  return;
109
110
  }
110
111
  if (ctx.chat?.type === "private") {
111
- await ctx.reply(text);
112
+ await replyError(ctx, text);
112
113
  }
113
114
  }
@@ -2,7 +2,7 @@ import { realpathSync, statSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { APPROVAL_POLICIES, MODE_PRESETS, REASONING_EFFORTS, SANDBOX_MODES, } from "../config.js";
4
4
  import { makeSessionKey } from "../store/sessions.js";
5
- import { sendPlainChunks } from "../telegram/delivery.js";
5
+ import { replyNotice, sendReplyNotice } from "../telegram/formatted.js";
6
6
  import { numericChatId, numericMessageThreadId, sessionFromContext } from "./session.js";
7
7
  import { truncateSingleLine } from "./sessionFlow.js";
8
8
  export function getProjectForContext(ctx, projects) {
@@ -12,20 +12,12 @@ export function getProjectForContext(ctx, projects) {
12
12
  return projects.get(String(chatId));
13
13
  }
14
14
  export function getScopedSession(ctx, store, projects, config, options) {
15
- if (isPrivateChat(ctx)) {
16
- void ctx.reply("Private chat is only for admin binding and project overview. Do actual work inside project supergroup topics.");
17
- return null;
18
- }
19
15
  const project = getProjectForContext(ctx, projects);
20
- if (!project) {
21
- void ctx.reply("This supergroup has no project bound yet.\nRun /project bind <absolute-path> first.");
16
+ if (!project || isPrivateChat(ctx))
22
17
  return null;
23
- }
24
18
  const requireTopic = options?.requireTopic ?? true;
25
- if (requireTopic && !hasTopicContext(ctx)) {
26
- void ctx.reply("Use this inside a forum topic. The root chat is only for project-level commands; work happens inside topics.");
19
+ if (requireTopic && !hasTopicContext(ctx))
27
20
  return null;
28
- }
29
21
  const session = sessionFromContext(ctx, store, config);
30
22
  if (!isPathWithinRoot(session.cwd, project.cwd)) {
31
23
  store.setCwd(session.sessionKey, project.cwd);
@@ -33,6 +25,13 @@ export function getScopedSession(ctx, store, projects, config, options) {
33
25
  }
34
26
  return session;
35
27
  }
28
+ export async function requireScopedSession(ctx, store, projects, config, options) {
29
+ const session = getScopedSession(ctx, store, projects, config, options);
30
+ if (session)
31
+ return session;
32
+ await replyScopedSessionRequirement(ctx, projects, options);
33
+ return null;
34
+ }
36
35
  export function formatHelpText(ctx, projects) {
37
36
  if (isPrivateChat(ctx)) {
38
37
  return [
@@ -163,11 +162,10 @@ export function ensureTopicSession(input) {
163
162
  return input.store.get(session.sessionKey) ?? session;
164
163
  }
165
164
  export async function postTopicReadyMessage(bot, session, text, logger) {
166
- await sendPlainChunks(bot, {
165
+ await sendReplyNotice(bot, {
167
166
  chatId: numericChatId(session),
168
167
  messageThreadId: numericMessageThreadId(session),
169
- text,
170
- }, logger);
168
+ }, text, logger);
171
169
  }
172
170
  export function parseSubcommand(input) {
173
171
  const trimmed = input.trim();
@@ -252,3 +250,18 @@ function canonicalizeBoundaryPath(input) {
252
250
  return resolved;
253
251
  }
254
252
  }
253
+ async function replyScopedSessionRequirement(ctx, projects, options) {
254
+ if (isPrivateChat(ctx)) {
255
+ await replyNotice(ctx, "Private chat is only for admin binding and project overview. Do actual work inside project supergroup topics.");
256
+ return;
257
+ }
258
+ const project = getProjectForContext(ctx, projects);
259
+ if (!project) {
260
+ await replyNotice(ctx, "This supergroup has no project bound yet.\nRun /project bind <absolute-path> first.");
261
+ return;
262
+ }
263
+ const requireTopic = options?.requireTopic ?? true;
264
+ if (requireTopic && !hasTopicContext(ctx)) {
265
+ await replyNotice(ctx, "Use this inside a forum topic. The root chat is only for project-level commands; work happens inside topics.");
266
+ }
267
+ }
@@ -37,6 +37,7 @@ export function wireBot(input) {
37
37
  });
38
38
  void (async () => {
39
39
  try {
40
+ await syncBotCommands(bot, logger);
40
41
  await cleanupMissingTopicBindings({
41
42
  bot,
42
43
  store,
@@ -62,3 +63,45 @@ export function createBot(input) {
62
63
  });
63
64
  return bot;
64
65
  }
66
+ async function syncBotCommands(bot, logger) {
67
+ try {
68
+ await bot.api.setMyCommands(privateCommands, {
69
+ scope: { type: "all_private_chats" },
70
+ });
71
+ await bot.api.setMyCommands(groupCommands, {
72
+ scope: { type: "all_group_chats" },
73
+ });
74
+ }
75
+ catch (error) {
76
+ logger?.warn("failed to sync telegram bot commands", {
77
+ error: error instanceof Error ? error.message : String(error),
78
+ });
79
+ }
80
+ }
81
+ const privateCommands = [
82
+ { command: "start", description: "Show help" },
83
+ { command: "help", description: "Show help" },
84
+ { command: "status", description: "Show bot status" },
85
+ { command: "admin", description: "Show or hand off admin access" },
86
+ ];
87
+ const groupCommands = [
88
+ { command: "help", description: "Show help" },
89
+ { command: "status", description: "Show project or topic status" },
90
+ { command: "project", description: "Show, bind, or unbind project" },
91
+ { command: "thread", description: "List, resume, or create topics" },
92
+ { command: "queue", description: "List, drop, or clear queued inputs" },
93
+ { command: "stop", description: "Stop the active run" },
94
+ { command: "cwd", description: "Show or set topic directory" },
95
+ { command: "mode", description: "Switch preset mode" },
96
+ { command: "sandbox", description: "Show or set sandbox mode" },
97
+ { command: "approval", description: "Show or set approval mode" },
98
+ { command: "yolo", description: "Enable or disable YOLO mode" },
99
+ { command: "model", description: "Show or set model" },
100
+ { command: "effort", description: "Show or set reasoning effort" },
101
+ { command: "web", description: "Show or set web search" },
102
+ { command: "network", description: "Show or set network access" },
103
+ { command: "gitcheck", description: "Show or set git repo check" },
104
+ { command: "adddir", description: "List or manage extra directories" },
105
+ { command: "schema", description: "Show or set output schema" },
106
+ { command: "codexconfig", description: "Show or set Codex config" },
107
+ ];
@@ -1,6 +1,7 @@
1
- import { contextLogFields, getScopedSession, } from "../commandSupport.js";
1
+ import { contextLogFields, requireScopedSession, } from "../commandSupport.js";
2
2
  import { handleUserInput, handleUserText } from "../inputService.js";
3
3
  import { telegramImageMessageToCodexInput } from "../../telegram/attachments.js";
4
+ import { replyError, replyNotice } from "../../telegram/formatted.js";
4
5
  export function registerMessageHandlers(deps) {
5
6
  const { bot, config, store, projects, codex, buffers, logger } = deps;
6
7
  bot.on("message:text", async (ctx) => {
@@ -12,7 +13,7 @@ export function registerMessageHandlers(deps) {
12
13
  });
13
14
  if (text.startsWith("/"))
14
15
  return;
15
- const session = getScopedSession(ctx, store, projects, config);
16
+ const session = await requireScopedSession(ctx, store, projects, config);
16
17
  if (!session) {
17
18
  logger?.warn("ignored telegram text message because no scoped session was available", {
18
19
  ...contextLogFields(ctx),
@@ -31,7 +32,7 @@ export function registerMessageHandlers(deps) {
31
32
  });
32
33
  });
33
34
  bot.on(["message:photo", "message:document"], async (ctx) => {
34
- const session = getScopedSession(ctx, store, projects, config);
35
+ const session = await requireScopedSession(ctx, store, projects, config);
35
36
  if (!session) {
36
37
  logger?.warn("ignored telegram attachment because no scoped session was available", {
37
38
  ...contextLogFields(ctx),
@@ -47,7 +48,7 @@ export function registerMessageHandlers(deps) {
47
48
  message: ctx.message,
48
49
  });
49
50
  if (!prompt) {
50
- await ctx.reply("Only image attachments are supported.");
51
+ await replyNotice(ctx, "Only image attachments are supported.");
51
52
  return;
52
53
  }
53
54
  await handleUserInput({
@@ -65,7 +66,7 @@ export function registerMessageHandlers(deps) {
65
66
  ...contextLogFields(ctx),
66
67
  error,
67
68
  });
68
- await ctx.reply(error instanceof Error ? error.message : String(error));
69
+ await replyError(ctx, error instanceof Error ? error.message : String(error));
69
70
  }
70
71
  });
71
72
  }