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 +104 -139
- package/dist/bot/auth.js +9 -8
- package/dist/bot/commandSupport.js +27 -14
- package/dist/bot/createBot.js +43 -0
- package/dist/bot/handlers/messageHandlers.js +6 -5
- package/dist/bot/handlers/operationalHandlers.js +84 -72
- package/dist/bot/handlers/projectHandlers.js +90 -76
- package/dist/bot/handlers/sessionConfigHandlers.js +141 -111
- package/dist/bot/inputService.js +30 -14
- package/dist/codex/configOverrides.js +50 -0
- package/dist/codex/sessionCatalog.js +66 -12
- package/dist/runtime/bootstrap.js +60 -37
- package/dist/runtime/startTelecodex.js +28 -21
- package/dist/store/fileState.js +100 -8
- package/dist/store/projects.js +3 -0
- package/dist/store/sessions.js +3 -0
- package/dist/telegram/delivery.js +41 -5
- package/dist/telegram/formatted.js +183 -0
- package/dist/telegram/messageBuffer.js +10 -0
- package/dist/telegram/splitMessage.js +1 -1
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -1,186 +1,151 @@
|
|
|
1
1
|
# telecodex
|
|
2
2
|
|
|
3
|
-
Telegram
|
|
3
|
+
Use Telegram forum topics as a remote interface for local Codex.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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`
|
|
58
|
-
|
|
29
|
+
Installing `telecodex` does not replace the separate `codex` CLI. The bot uses
|
|
30
|
+
your local Codex installation at runtime.
|
|
59
31
|
|
|
60
|
-
##
|
|
32
|
+
## First Launch
|
|
61
33
|
|
|
62
|
-
|
|
34
|
+
On first launch, `telecodex`:
|
|
63
35
|
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
42
|
+
The first successful sender becomes the admin for that bot instance.
|
|
69
43
|
|
|
70
|
-
|
|
71
|
-
npm run dev
|
|
72
|
-
```
|
|
44
|
+
Optional security override:
|
|
73
45
|
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
##
|
|
50
|
+
## How It Works
|
|
78
51
|
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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
|
-
|
|
70
|
+
2. Create a fresh topic for a new Codex thread:
|
|
92
71
|
|
|
93
|
-
```
|
|
94
|
-
|
|
72
|
+
```text
|
|
73
|
+
/thread new My Task
|
|
95
74
|
```
|
|
96
75
|
|
|
97
|
-
|
|
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
|
-
|
|
78
|
+
```text
|
|
79
|
+
/thread list
|
|
80
|
+
/thread resume <threadId>
|
|
81
|
+
```
|
|
102
82
|
|
|
103
|
-
|
|
83
|
+
4. Send normal messages in the topic to work with Codex.
|
|
104
84
|
|
|
105
|
-
|
|
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
|
-
|
|
87
|
+
### General
|
|
112
88
|
|
|
113
|
-
|
|
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
|
-
|
|
93
|
+
### Admin
|
|
116
94
|
|
|
117
|
-
- `
|
|
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
|
-
|
|
99
|
+
### Project
|
|
120
100
|
|
|
121
|
-
-
|
|
122
|
-
-
|
|
123
|
-
-
|
|
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
|
-
|
|
105
|
+
### Threads
|
|
133
106
|
|
|
134
|
-
-
|
|
135
|
-
-
|
|
136
|
-
-
|
|
137
|
-
-
|
|
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
|
-
|
|
112
|
+
### Session Configuration
|
|
142
113
|
|
|
143
|
-
-
|
|
144
|
-
-
|
|
145
|
-
-
|
|
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
|
-
##
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
100
|
+
await replyError(input.ctx, input.exhaustedLabel);
|
|
100
101
|
return true;
|
|
101
102
|
}
|
|
102
|
-
await input.ctx
|
|
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
|
|
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 {
|
|
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
|
|
165
|
+
await sendReplyNotice(bot, {
|
|
167
166
|
chatId: numericChatId(session),
|
|
168
167
|
messageThreadId: numericMessageThreadId(session),
|
|
169
|
-
|
|
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
|
+
}
|
package/dist/bot/createBot.js
CHANGED
|
@@ -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,
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
69
|
+
await replyError(ctx, error instanceof Error ? error.message : String(error));
|
|
69
70
|
}
|
|
70
71
|
});
|
|
71
72
|
}
|