Skip to content

Commit c669fb3

Browse files
committed
feat: add telegram-native codex command adapters
1 parent 0e32351 commit c669fb3

6 files changed

Lines changed: 376 additions & 20 deletions

File tree

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,12 @@ General:
109109

110110
- `/start` - bootstrap message
111111
- `/help` - command summary
112+
- `/status` - show current chat status, active runner mode, workdir, model override, MCP servers
113+
- `/new` - close current session and start fresh on the next message
114+
- `/exec <task>` - force a one-off `codex exec`
115+
- `/auto <task>` - force a one-off `codex exec --full-auto`
116+
- `/plan <task>` - ask Codex for a plan only, without direct file modification intent
117+
- `/model [name|reset]` - show or set the model override for the current chat
112118
- `/interrupt` - send `Ctrl+C` to current PTY session
113119
- `/stop` - terminate current PTY session
114120
- `/cron_now` - trigger daily summary immediately
@@ -126,6 +132,15 @@ GitHub skill:
126132
- `/gh run tests` -> launch test job
127133
- `/gh test status <jobId>` -> read test status/output tail
128134

135+
Telegram adaptation notes:
136+
137+
- Plain text messages behave like `codex "task description"`
138+
- `/exec` behaves like `codex exec "task"`
139+
- `/auto` behaves like `codex exec --full-auto "task"`
140+
- `/new` is implemented by the bot and resets the current chat session
141+
- `/status` is implemented by the bot and reports local runtime state
142+
- `/plan` translates to a planning-only prompt instead of passing a raw `/plan` slash command to Codex
143+
129144
## Streaming and Reasoning Visualization
130145

131146
PTY output is streamed with throttled `editMessageText` updates.

src/bot/commandUtils.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
export function extractCommandPayload(rawText = "", commandName) {
2+
const pattern = new RegExp(`^\\/${commandName}(?:@\\w+)?\\s*`, "i");
3+
return String(rawText).replace(pattern, "").trim();
4+
}
5+
6+
export function buildPlanPrompt(task) {
7+
return [
8+
"Planning mode only.",
9+
"Analyze the request and respond with a concise execution plan.",
10+
"Do not modify files.",
11+
"Do not run write commands.",
12+
"Do not claim you already made changes.",
13+
"",
14+
"Task:",
15+
task
16+
].join("\n");
17+
}

src/bot/handlers.js

Lines changed: 141 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Markup } from "telegraf";
2+
import { buildPlanPrompt, extractCommandPayload } from "./commandUtils.js";
23
import { escapeMarkdownV2, splitTelegramMessage } from "./formatter.js";
34

45
async function sendChunkedMarkdown(ctx, text, extra = {}) {
@@ -44,6 +45,7 @@ export function registerHandlers({ bot, router, ptyManager, skills, scheduler })
4445
"codex-telegram-claws ready.",
4546
"普通消息和编码任务会路由到 Codex。",
4647
"MCP 只在显式 /mcp 命令下调用。",
48+
"试试: /status, /exec, /auto, /plan, /model, /new",
4749
"GitHub 指令示例: /gh commit \"feat: init\""
4850
].join("\n")
4951
);
@@ -55,6 +57,12 @@ export function registerHandlers({ bot, router, ptyManager, skills, scheduler })
5557
[
5658
"Commands:",
5759
"/help - 显示帮助",
60+
"/status - 查看当前 chat 的运行状态",
61+
"/new - 新建会话并清空当前上下文",
62+
"/exec <task> - 强制用 codex exec 运行一次任务",
63+
"/auto <task> - 强制用 codex exec --full-auto 运行任务",
64+
"/plan <task> - 仅生成执行计划,不直接修改代码",
65+
"/model [name|reset] - 查看或设置当前 chat 的模型",
5866
"/interrupt - 向 Codex CLI 发送 Ctrl+C",
5967
"/stop - 终止当前 chat 的 PTY 会话",
6068
"/cron_now - 立即触发一次日报推送",
@@ -64,6 +72,131 @@ export function registerHandlers({ bot, router, ptyManager, skills, scheduler })
6472
);
6573
});
6674

75+
bot.command("status", async (ctx) => {
76+
const status = ptyManager.getStatus(ctx.chat.id);
77+
await sendChunkedMarkdown(
78+
ctx,
79+
[
80+
"Status:",
81+
`active: ${status.active ? "yes" : "no"}`,
82+
`active mode: ${status.activeMode || "idle"}`,
83+
`last mode: ${status.lastMode || "none"}`,
84+
`last exit: ${status.lastExitCode === null ? "n/a" : status.lastExitCode}`,
85+
`pty supported: ${
86+
status.ptySupported === null ? "unknown" : status.ptySupported ? "yes" : "no (exec fallback)"
87+
}`,
88+
`preferred model: ${status.preferredModel || "inherit codex default"}`,
89+
`command: ${status.command}`,
90+
`workdir: ${status.workdir}`,
91+
`mcp servers: ${status.mcpServers.length ? status.mcpServers.join(", ") : "none"}`
92+
].join("\n")
93+
);
94+
});
95+
96+
bot.command("new", async (ctx) => {
97+
const closed = ptyManager.closeSession(ctx.chat.id);
98+
await sendChunkedMarkdown(
99+
ctx,
100+
closed
101+
? "当前会话已关闭。下一条消息会启动一个新的 Codex 会话。"
102+
: "当前没有活动会话。下一条消息会启动新的 Codex 会话。"
103+
);
104+
});
105+
106+
bot.command("exec", async (ctx) => {
107+
const task = extractCommandPayload(ctx.message.text, "exec");
108+
if (!task) {
109+
await sendChunkedMarkdown(ctx, "用法: /exec <task>");
110+
return;
111+
}
112+
113+
const result = await ptyManager.sendPrompt(ctx, task, {
114+
forceExec: true,
115+
notice: "Running one-off `codex exec` task..."
116+
});
117+
118+
if (!result.started) {
119+
await sendChunkedMarkdown(
120+
ctx,
121+
`当前已有 ${result.activeMode || "unknown"} 任务在运行。请等待完成或先使用 /interrupt。`
122+
);
123+
}
124+
});
125+
126+
bot.command("auto", async (ctx) => {
127+
const task = extractCommandPayload(ctx.message.text, "auto");
128+
if (!task) {
129+
await sendChunkedMarkdown(ctx, "用法: /auto <task>");
130+
return;
131+
}
132+
133+
const result = await ptyManager.sendPrompt(ctx, task, {
134+
forceExec: true,
135+
fullAuto: true,
136+
notice: "Running one-off `codex exec --full-auto` task..."
137+
});
138+
139+
if (!result.started) {
140+
await sendChunkedMarkdown(
141+
ctx,
142+
`当前已有 ${result.activeMode || "unknown"} 任务在运行。请等待完成或先使用 /interrupt。`
143+
);
144+
}
145+
});
146+
147+
bot.command("plan", async (ctx) => {
148+
const task = extractCommandPayload(ctx.message.text, "plan");
149+
if (!task) {
150+
await sendChunkedMarkdown(ctx, "用法: /plan <task>");
151+
return;
152+
}
153+
154+
const result = await ptyManager.sendPrompt(ctx, buildPlanPrompt(task), {
155+
forceExec: true,
156+
notice: "Running planning-only Codex task..."
157+
});
158+
159+
if (!result.started) {
160+
await sendChunkedMarkdown(
161+
ctx,
162+
`当前已有 ${result.activeMode || "unknown"} 任务在运行。请等待完成或先使用 /interrupt。`
163+
);
164+
}
165+
});
166+
167+
bot.command("model", async (ctx) => {
168+
const value = extractCommandPayload(ctx.message.text, "model");
169+
if (!value) {
170+
const status = ptyManager.getStatus(ctx.chat.id);
171+
await sendChunkedMarkdown(
172+
ctx,
173+
`当前模型: ${status.preferredModel || "inherit codex default"}`
174+
);
175+
return;
176+
}
177+
178+
if (/^(reset|default|inherit)$/i.test(value)) {
179+
ptyManager.clearPreferredModel(ctx.chat.id);
180+
const closed = ptyManager.closeSession(ctx.chat.id);
181+
await sendChunkedMarkdown(
182+
ctx,
183+
closed
184+
? "模型已重置为 Codex 默认值,并重建了当前会话。"
185+
: "模型已重置为 Codex 默认值。"
186+
);
187+
return;
188+
}
189+
190+
ptyManager.setPreferredModel(ctx.chat.id, value);
191+
const closed = ptyManager.closeSession(ctx.chat.id);
192+
await sendChunkedMarkdown(
193+
ctx,
194+
closed
195+
? `模型已设置为 ${value},并重建了当前会话。`
196+
: `模型已设置为 ${value}。`
197+
);
198+
});
199+
67200
bot.command("interrupt", async (ctx) => {
68201
const ok = ptyManager.interrupt(ctx.chat.id);
69202
await sendChunkedMarkdown(ctx, ok ? "已发送 Ctrl+C。" : "当前 chat 没有活动 PTY 会话。");
@@ -85,7 +218,7 @@ export function registerHandlers({ bot, router, ptyManager, skills, scheduler })
85218

86219
bot.command("gh", async (ctx) => {
87220
try {
88-
const text = ctx.message.text.replace(/^\/gh(@\w+)?\s*/i, "").trim() || "help";
221+
const text = extractCommandPayload(ctx.message.text, "gh") || "help";
89222
const result = await skills.github.execute({ text: `/gh ${text}`, ctx });
90223
await sendSkillResult(ctx, result);
91224
} catch (error) {
@@ -126,7 +259,13 @@ export function registerHandlers({ bot, router, ptyManager, skills, scheduler })
126259
try {
127260
const route = await router.routeMessage(text);
128261
if (route.target === "pty") {
129-
await ptyManager.sendPrompt(ctx, route.prompt);
262+
const result = await ptyManager.sendPrompt(ctx, route.prompt);
263+
if (!result.started) {
264+
await sendChunkedMarkdown(
265+
ctx,
266+
`当前已有 ${result.activeMode || "unknown"} 任务在运行。请等待完成或先使用 /interrupt。`
267+
);
268+
}
130269
return;
131270
}
132271

0 commit comments

Comments
 (0)