codex-anywhere 0.1.0
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/CONTRIBUTING.md +98 -0
- package/LICENSE +73 -0
- package/README.md +163 -0
- package/dist/agentMessageStreams.d.ts +3 -0
- package/dist/agentMessageStreams.d.ts.map +1 -0
- package/dist/agentMessageStreams.js +10 -0
- package/dist/agentMessageStreams.js.map +1 -0
- package/dist/app.d.ts +17 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +18 -0
- package/dist/app.js.map +1 -0
- package/dist/approval.d.ts +12 -0
- package/dist/approval.d.ts.map +1 -0
- package/dist/approval.js +42 -0
- package/dist/approval.js.map +1 -0
- package/dist/bootstrap.d.ts +22 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +31 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/bridge.d.ts +22 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +2861 -0
- package/dist/bridge.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +58 -0
- package/dist/cli.js.map +1 -0
- package/dist/codexAppServer.d.ts +13 -0
- package/dist/codexAppServer.d.ts.map +1 -0
- package/dist/codexAppServer.js +115 -0
- package/dist/codexAppServer.js.map +1 -0
- package/dist/configuration.d.ts +17 -0
- package/dist/configuration.d.ts.map +1 -0
- package/dist/configuration.js +27 -0
- package/dist/configuration.js.map +1 -0
- package/dist/harnessState.d.ts +15 -0
- package/dist/harnessState.d.ts.map +1 -0
- package/dist/harnessState.js +55 -0
- package/dist/harnessState.js.map +1 -0
- package/dist/harnessStateCli.d.ts +3 -0
- package/dist/harnessStateCli.d.ts.map +1 -0
- package/dist/harnessStateCli.js +13 -0
- package/dist/harnessStateCli.js.map +1 -0
- package/dist/interactive.d.ts +8 -0
- package/dist/interactive.d.ts.map +1 -0
- package/dist/interactive.js +24 -0
- package/dist/interactive.js.map +1 -0
- package/dist/localCommandInteractions.d.ts +22 -0
- package/dist/localCommandInteractions.d.ts.map +1 -0
- package/dist/localCommandInteractions.js +412 -0
- package/dist/localCommandInteractions.js.map +1 -0
- package/dist/omxCommands.d.ts +10 -0
- package/dist/omxCommands.d.ts.map +1 -0
- package/dist/omxCommands.js +146 -0
- package/dist/omxCommands.js.map +1 -0
- package/dist/onboarding.d.ts +3 -0
- package/dist/onboarding.d.ts.map +1 -0
- package/dist/onboarding.js +41 -0
- package/dist/onboarding.js.map +1 -0
- package/dist/paths.d.ts +3 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +22 -0
- package/dist/paths.js.map +1 -0
- package/dist/persistence.d.ts +6 -0
- package/dist/persistence.d.ts.map +1 -0
- package/dist/persistence.js +61 -0
- package/dist/persistence.js.map +1 -0
- package/dist/preflight.d.ts +3 -0
- package/dist/preflight.d.ts.map +1 -0
- package/dist/preflight.js +25 -0
- package/dist/preflight.js.map +1 -0
- package/dist/service.d.ts +58 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +322 -0
- package/dist/service.js.map +1 -0
- package/dist/sessions.d.ts +7 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/sessions.js +20 -0
- package/dist/sessions.js.map +1 -0
- package/dist/slashCommands.d.ts +13 -0
- package/dist/slashCommands.d.ts.map +1 -0
- package/dist/slashCommands.js +150 -0
- package/dist/slashCommands.js.map +1 -0
- package/dist/telegram.d.ts +21 -0
- package/dist/telegram.d.ts.map +1 -0
- package/dist/telegram.js +103 -0
- package/dist/telegram.js.map +1 -0
- package/dist/telegramFormatting.d.ts +18 -0
- package/dist/telegramFormatting.d.ts.map +1 -0
- package/dist/telegramFormatting.js +380 -0
- package/dist/telegramFormatting.js.map +1 -0
- package/dist/threadState.d.ts +3 -0
- package/dist/threadState.d.ts.map +1 -0
- package/dist/threadState.js +28 -0
- package/dist/threadState.js.map +1 -0
- package/dist/turnControls.d.ts +7 -0
- package/dist/turnControls.d.ts.map +1 -0
- package/dist/turnControls.js +20 -0
- package/dist/turnControls.js.map +1 -0
- package/dist/types.d.ts +121 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +62 -0
package/dist/bridge.js
ADDED
|
@@ -0,0 +1,2861 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { clearInterval, setInterval } from "node:timers";
|
|
7
|
+
import { setTimeout as sleep } from "node:timers/promises";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
|
+
import { formatApprovalCallbackData, formatShellCallbackData, parseApprovalCallbackData, parseShellCallbackData, } from "./approval.js";
|
|
10
|
+
import { agentStreamKey, streamGroupId } from "./agentMessageStreams.js";
|
|
11
|
+
import { CodexAppServerClient } from "./codexAppServer.js";
|
|
12
|
+
import { formatInteractiveCallbackData, parseInteractiveCallbackData, } from "./interactive.js";
|
|
13
|
+
import { buildAgentThreadInteractiveSession, buildApprovalPolicyInteractiveSession, buildCollaborationInteractiveSession, buildExperimentalInteractiveSession, buildFastInteractiveSession, buildFeedbackInteractiveSession, buildLocalInteractiveFollowUpSteps, buildMentionInteractiveSession, buildModelInteractiveSession, buildPersonalityInteractiveSession, buildPlanInteractiveSession, buildRenameInteractiveSession, buildReviewInteractiveSession, buildVerboseInteractiveSession, } from "./localCommandInteractions.js";
|
|
14
|
+
import { loadState, saveConfig, saveState } from "./persistence.js";
|
|
15
|
+
import { formatSessionCallbackData, parseSessionCallbackData } from "./sessions.js";
|
|
16
|
+
import { codexSlashHelpText, isRecognizedCodexSlashCommand, isSupportedCodexSlashCommand, isTaskBlockingSlashCommand, isUnsupportedTelegramOnlyCodexCommand, normalizeApprovalPolicy, normalizeReasoningEffort, parseTelegramSlashCommand, } from "./slashCommands.js";
|
|
17
|
+
import { buildOmxHelpText, planOmxCommand } from "./omxCommands.js";
|
|
18
|
+
import { TelegramBotApi } from "./telegram.js";
|
|
19
|
+
import { escapeTelegramHtml, formatApprovalPromptHtml, formatCommandCompletionHtml, formatFileChangeCompletionHtml, formatPendingInputActionHtml, formatTurnCompletionHtml, renderAssistantTextHtml, splitTelegramChunks, } from "./telegramFormatting.js";
|
|
20
|
+
import { reconcileActiveTurnIdFromThreadRead } from "./threadState.js";
|
|
21
|
+
import { formatTurnControlCallbackData, parseTurnControlCallbackData, } from "./turnControls.js";
|
|
22
|
+
const execFileAsync = promisify(execFile);
|
|
23
|
+
export class CodexAnywhereBridge {
|
|
24
|
+
#configPath;
|
|
25
|
+
#statePath;
|
|
26
|
+
#config;
|
|
27
|
+
#telegram;
|
|
28
|
+
#codex;
|
|
29
|
+
#pendingApprovals = new Map();
|
|
30
|
+
#pendingShellCommands = new Map();
|
|
31
|
+
#pendingInteractiveSessions = new Map();
|
|
32
|
+
#pendingInteractiveSessionByChat = new Map();
|
|
33
|
+
#items = new Map();
|
|
34
|
+
#streams = new Map();
|
|
35
|
+
#typingIntervals = new Map();
|
|
36
|
+
#initialized = false;
|
|
37
|
+
#state = {
|
|
38
|
+
version: 1,
|
|
39
|
+
lastUpdateId: null,
|
|
40
|
+
chats: {},
|
|
41
|
+
};
|
|
42
|
+
constructor(config, configPath, statePath, deps) {
|
|
43
|
+
this.#config = config;
|
|
44
|
+
this.#configPath = configPath;
|
|
45
|
+
this.#statePath = statePath;
|
|
46
|
+
this.#telegram = deps?.telegram ?? new TelegramBotApi(config.telegramBotToken);
|
|
47
|
+
this.#codex = deps?.codex ?? new CodexAppServerClient(["codex", "app-server"]);
|
|
48
|
+
if (deps?.initialState) {
|
|
49
|
+
this.#state = deps.initialState;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async initialize(options) {
|
|
53
|
+
if (this.#initialized) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
this.#state = await loadState(this.#statePath);
|
|
57
|
+
await this.#codex.start();
|
|
58
|
+
await this.#codex.initialize();
|
|
59
|
+
await this.#registerTelegramCommands();
|
|
60
|
+
if (options?.printStartupHelp ?? true) {
|
|
61
|
+
this.#printStartupHelp();
|
|
62
|
+
}
|
|
63
|
+
this.#initialized = true;
|
|
64
|
+
}
|
|
65
|
+
async runLoops() {
|
|
66
|
+
await Promise.all([this.#pollTelegramLoop(), this.#consumeCodexLoop()]);
|
|
67
|
+
}
|
|
68
|
+
async run() {
|
|
69
|
+
await this.initialize();
|
|
70
|
+
await this.runLoops();
|
|
71
|
+
}
|
|
72
|
+
async handleUpdateForTest(update) {
|
|
73
|
+
await this.#handleUpdate(update);
|
|
74
|
+
}
|
|
75
|
+
async #pollTelegramLoop() {
|
|
76
|
+
while (true) {
|
|
77
|
+
try {
|
|
78
|
+
const offset = this.#state.lastUpdateId === null ? null : this.#state.lastUpdateId + 1;
|
|
79
|
+
const updates = await this.#telegram.getUpdates(offset, this.#config.pollTimeoutSeconds);
|
|
80
|
+
for (const update of updates) {
|
|
81
|
+
this.#state.lastUpdateId = update.update_id;
|
|
82
|
+
await this.#saveState();
|
|
83
|
+
await this.#handleUpdate(update);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
this.#logRuntimeError("telegram poll", error);
|
|
88
|
+
await sleep(1000);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async #consumeCodexLoop() {
|
|
93
|
+
while (true) {
|
|
94
|
+
try {
|
|
95
|
+
const message = await this.#codex.nextMessage();
|
|
96
|
+
if (typeof message.method !== "string") {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if ("id" in message) {
|
|
100
|
+
await this.#handleServerRequest(message);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
await this.#handleNotification(message.method, message.params ?? {});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
this.#logRuntimeError("codex event", error);
|
|
108
|
+
await sleep(1000);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async #handleUpdate(update) {
|
|
113
|
+
if (update.callback_query) {
|
|
114
|
+
await this.#handleCallbackQuery(update.callback_query);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (update.message?.text || update.message?.caption || update.message?.photo || update.message?.document) {
|
|
118
|
+
await this.#handleMessage(update.message);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async #handleMessage(message) {
|
|
122
|
+
if (message.chat.type !== "private") {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const text = message.text?.trim() ?? message.caption?.trim() ?? "";
|
|
126
|
+
const userId = message.from?.id;
|
|
127
|
+
if (typeof userId !== "number") {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (this.#config.ownerUserId === null) {
|
|
131
|
+
await this.#maybePairOwner(userId, message.chat.id, text);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (userId !== this.#config.ownerUserId) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (hasTelegramImage(message)) {
|
|
138
|
+
if (await this.#handlePendingInteractiveTextInput(message.chat.id, text || "/cancel")) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
await this.#handleImageMessage(message, text);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (!text) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (await this.#handlePendingInteractiveTextInput(message.chat.id, text)) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const explicitOmxTeam = parseExplicitOmxTeamInvocation(text);
|
|
151
|
+
if (explicitOmxTeam) {
|
|
152
|
+
await this.#handleOmxCommand(message.chat.id, explicitOmxTeam);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const slashCommand = parseTelegramSlashCommand(text);
|
|
156
|
+
if (slashCommand) {
|
|
157
|
+
switch (slashCommand.name) {
|
|
158
|
+
case "start":
|
|
159
|
+
await this.#sendText(message.chat.id, [
|
|
160
|
+
"Codex Anywhere is ready.",
|
|
161
|
+
"Send a task, try /help, or use /resume to browse sessions.",
|
|
162
|
+
"You can also send a screenshot with an optional caption.",
|
|
163
|
+
].join("\n"));
|
|
164
|
+
return;
|
|
165
|
+
case "help":
|
|
166
|
+
await this.#sendText(message.chat.id, codexSlashHelpText());
|
|
167
|
+
return;
|
|
168
|
+
case "new":
|
|
169
|
+
await this.#startNewThread(message.chat.id);
|
|
170
|
+
return;
|
|
171
|
+
case "resume":
|
|
172
|
+
await this.#showSessions(message.chat.id);
|
|
173
|
+
return;
|
|
174
|
+
case "interrupt":
|
|
175
|
+
await this.#interruptTurn(message.chat.id);
|
|
176
|
+
return;
|
|
177
|
+
case "esc":
|
|
178
|
+
case "ese":
|
|
179
|
+
await this.#interruptTurn(message.chat.id);
|
|
180
|
+
return;
|
|
181
|
+
case "cancel":
|
|
182
|
+
await this.#sendText(message.chat.id, "No active interactive prompt to cancel.");
|
|
183
|
+
return;
|
|
184
|
+
case "status":
|
|
185
|
+
await this.#sendStatus(message.chat.id);
|
|
186
|
+
return;
|
|
187
|
+
case "sh":
|
|
188
|
+
await this.#runShellCommand(message.chat.id, slashCommand.args);
|
|
189
|
+
return;
|
|
190
|
+
case "omx":
|
|
191
|
+
await this.#handleOmxCommand(message.chat.id, slashCommand.args);
|
|
192
|
+
return;
|
|
193
|
+
case "workspace":
|
|
194
|
+
await this.#handleWorkspaceCommand(message.chat.id, slashCommand.args);
|
|
195
|
+
return;
|
|
196
|
+
default:
|
|
197
|
+
if (isRecognizedCodexSlashCommand(slashCommand.name)) {
|
|
198
|
+
await this.#handleCodexSlashCommand(message.chat.id, slashCommand.name, slashCommand.args);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
await this.#sendText(message.chat.id, `Unknown command: /${slashCommand.name}\nUse /help to see supported commands.`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
await this.#submitChatInput(message.chat.id, [{ type: "text", text }]);
|
|
206
|
+
}
|
|
207
|
+
async #handleImageMessage(message, caption) {
|
|
208
|
+
const imagePath = await this.#downloadTelegramImage(message);
|
|
209
|
+
if (!imagePath) {
|
|
210
|
+
await this.#sendText(message.chat.id, "I could not read that image from Telegram.");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const input = [];
|
|
214
|
+
if (caption) {
|
|
215
|
+
input.push({ type: "text", text: caption });
|
|
216
|
+
}
|
|
217
|
+
input.push({ type: "localImage", path: imagePath });
|
|
218
|
+
await this.#submitChatInput(message.chat.id, input);
|
|
219
|
+
}
|
|
220
|
+
async #submitChatInput(chatId, input) {
|
|
221
|
+
const state = this.#chatState(chatId);
|
|
222
|
+
const preparedInput = consumePendingMention(state, input);
|
|
223
|
+
if (!state.activeTurnId) {
|
|
224
|
+
await this.#startTurn(chatId, preparedInput);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
await this.#reconcileActiveTurnState(chatId);
|
|
228
|
+
if (!state.activeTurnId) {
|
|
229
|
+
await this.#startTurn(chatId, preparedInput);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (state.queueNextArmed) {
|
|
233
|
+
state.queuedTurnInput = preparedInput;
|
|
234
|
+
state.queueNextArmed = false;
|
|
235
|
+
await this.#saveState();
|
|
236
|
+
await this.#sendHtmlText(chatId, formatPendingInputActionHtml("queued", preparedInput));
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
state.pendingTurnInput = preparedInput;
|
|
240
|
+
await this.#saveState();
|
|
241
|
+
await this.#sendTurnControls(chatId, state.activeTurnId);
|
|
242
|
+
}
|
|
243
|
+
async #downloadTelegramImage(message) {
|
|
244
|
+
const fileId = bestTelegramImageFileId(message);
|
|
245
|
+
if (!fileId) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
const file = await this.#telegram.getFile(fileId);
|
|
249
|
+
const extension = telegramFileExtension(file.file_path, message.document?.file_name);
|
|
250
|
+
const directory = path.join(os.tmpdir(), "codex-anywhere-telegram-images");
|
|
251
|
+
await fs.mkdir(directory, { recursive: true });
|
|
252
|
+
const targetPath = path.join(directory, `${Date.now()}-${randomBytes(4).toString("hex")}${extension}`);
|
|
253
|
+
const bytes = await this.#telegram.downloadFile(file.file_path);
|
|
254
|
+
await fs.writeFile(targetPath, bytes);
|
|
255
|
+
return targetPath;
|
|
256
|
+
}
|
|
257
|
+
async #handleCallbackQuery(callback) {
|
|
258
|
+
const userId = callback.from?.id;
|
|
259
|
+
if (typeof userId !== "number" || userId !== this.#config.ownerUserId) {
|
|
260
|
+
await this.#telegram.answerCallbackQuery(callback.id, "Not allowed");
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const parsed = parseApprovalCallbackData(callback.data ?? "");
|
|
264
|
+
if (parsed) {
|
|
265
|
+
const approval = this.#pendingApprovals.get(parsed.token);
|
|
266
|
+
if (!approval) {
|
|
267
|
+
await this.#telegram.answerCallbackQuery(callback.id, "Approval expired");
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
await this.#resolveApproval(approval, parsed.action);
|
|
271
|
+
this.#pendingApprovals.delete(parsed.token);
|
|
272
|
+
await this.#telegram.answerCallbackQuery(callback.id, `Recorded: ${parsed.action}`);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const shellParsed = parseShellCallbackData(callback.data ?? "");
|
|
276
|
+
const interactiveParsed = parseInteractiveCallbackData(callback.data ?? "");
|
|
277
|
+
const sessionParsed = parseSessionCallbackData(callback.data ?? "");
|
|
278
|
+
const turnControlParsed = parseTurnControlCallbackData(callback.data ?? "");
|
|
279
|
+
if (interactiveParsed) {
|
|
280
|
+
await this.#handleInteractiveCallback(callback.id, interactiveParsed);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (sessionParsed) {
|
|
284
|
+
await this.#handleSessionCallback(callback.id, callback.message?.chat.id ?? null, sessionParsed);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (turnControlParsed) {
|
|
288
|
+
await this.#handleTurnControlCallback(callback.id, callback.message?.chat.id ?? null, turnControlParsed);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (!shellParsed) {
|
|
292
|
+
await this.#telegram.answerCallbackQuery(callback.id, "Unknown action");
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const pendingShell = this.#pendingShellCommands.get(shellParsed.token);
|
|
296
|
+
if (!pendingShell) {
|
|
297
|
+
await this.#telegram.answerCallbackQuery(callback.id, "Shell confirmation expired");
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
this.#pendingShellCommands.delete(shellParsed.token);
|
|
301
|
+
if (shellParsed.action === "cancel") {
|
|
302
|
+
await this.#telegram.answerCallbackQuery(callback.id, "Cancelled");
|
|
303
|
+
await this.#sendText(pendingShell.chatId, "Cancelled shell command.");
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
await this.#telegram.answerCallbackQuery(callback.id, "Running command");
|
|
307
|
+
await this.#executeShellCommand(pendingShell.chatId, pendingShell.command);
|
|
308
|
+
}
|
|
309
|
+
async #handleCodexSlashCommand(chatId, name, args) {
|
|
310
|
+
if (isUnsupportedTelegramOnlyCodexCommand(name)) {
|
|
311
|
+
await this.#sendText(chatId, `/${name} is a TUI/Desktop-oriented command and is not meaningful in the Telegram bridge yet.`);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (!isSupportedCodexSlashCommand(name)) {
|
|
315
|
+
await this.#sendText(chatId, `/${name} is not supported in Codex Anywhere yet.`);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const state = this.#chatState(chatId);
|
|
319
|
+
if (isTaskBlockingSlashCommand(name) && state.activeTurnId) {
|
|
320
|
+
await this.#reconcileActiveTurnState(chatId);
|
|
321
|
+
}
|
|
322
|
+
if (isTaskBlockingSlashCommand(name) && state.activeTurnId) {
|
|
323
|
+
await this.#sendText(chatId, `/${name} is disabled while a task is in progress.`);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
switch (name) {
|
|
327
|
+
case "fork":
|
|
328
|
+
await this.#forkThread(chatId);
|
|
329
|
+
return;
|
|
330
|
+
case "init":
|
|
331
|
+
await this.#runInitCommand(chatId);
|
|
332
|
+
return;
|
|
333
|
+
case "compact":
|
|
334
|
+
await this.#compactThread(chatId);
|
|
335
|
+
return;
|
|
336
|
+
case "review":
|
|
337
|
+
await this.#startReview(chatId, args);
|
|
338
|
+
return;
|
|
339
|
+
case "rename":
|
|
340
|
+
await this.#renameThread(chatId, args);
|
|
341
|
+
return;
|
|
342
|
+
case "model":
|
|
343
|
+
await this.#handleModelCommand(chatId, args);
|
|
344
|
+
return;
|
|
345
|
+
case "personality":
|
|
346
|
+
await this.#handlePersonalityCommand(chatId, args);
|
|
347
|
+
return;
|
|
348
|
+
case "fast":
|
|
349
|
+
await this.#handleFastCommand(chatId, args);
|
|
350
|
+
return;
|
|
351
|
+
case "plan":
|
|
352
|
+
await this.#handlePlanCommand(chatId, args);
|
|
353
|
+
return;
|
|
354
|
+
case "collab":
|
|
355
|
+
await this.#handleCollabCommand(chatId, args);
|
|
356
|
+
return;
|
|
357
|
+
case "agent":
|
|
358
|
+
case "subagents":
|
|
359
|
+
await this.#handleAgentCommand(chatId);
|
|
360
|
+
return;
|
|
361
|
+
case "approvals":
|
|
362
|
+
case "permissions":
|
|
363
|
+
await this.#handleApprovalPolicyCommand(chatId, args);
|
|
364
|
+
return;
|
|
365
|
+
case "clear":
|
|
366
|
+
await this.#clearThread(chatId);
|
|
367
|
+
return;
|
|
368
|
+
case "diff":
|
|
369
|
+
await this.#sendGitDiff(chatId);
|
|
370
|
+
return;
|
|
371
|
+
case "mention":
|
|
372
|
+
await this.#handleMentionCommand(chatId, args);
|
|
373
|
+
return;
|
|
374
|
+
case "copy":
|
|
375
|
+
await this.#copyLastOutput(chatId);
|
|
376
|
+
return;
|
|
377
|
+
case "verbose":
|
|
378
|
+
await this.#handleVerboseCommand(chatId, args);
|
|
379
|
+
return;
|
|
380
|
+
case "feedback":
|
|
381
|
+
await this.#handleFeedbackCommand(chatId);
|
|
382
|
+
return;
|
|
383
|
+
case "skills":
|
|
384
|
+
await this.#sendSkills(chatId);
|
|
385
|
+
return;
|
|
386
|
+
case "mcp":
|
|
387
|
+
await this.#sendMcpServers(chatId);
|
|
388
|
+
return;
|
|
389
|
+
case "apps":
|
|
390
|
+
await this.#sendApps(chatId);
|
|
391
|
+
return;
|
|
392
|
+
case "plugins":
|
|
393
|
+
await this.#sendPlugins(chatId);
|
|
394
|
+
return;
|
|
395
|
+
case "logout":
|
|
396
|
+
case "quit":
|
|
397
|
+
case "exit":
|
|
398
|
+
await this.#logoutAccount(chatId);
|
|
399
|
+
return;
|
|
400
|
+
case "rollout":
|
|
401
|
+
await this.#sendRolloutPath(chatId);
|
|
402
|
+
return;
|
|
403
|
+
case "stop":
|
|
404
|
+
case "clean":
|
|
405
|
+
await this.#stopBackgroundTerminals(chatId);
|
|
406
|
+
return;
|
|
407
|
+
case "experimental":
|
|
408
|
+
await this.#handleExperimentalCommand(chatId, args);
|
|
409
|
+
return;
|
|
410
|
+
case "new":
|
|
411
|
+
case "resume":
|
|
412
|
+
case "status":
|
|
413
|
+
return;
|
|
414
|
+
default:
|
|
415
|
+
await this.#sendText(chatId, `/${name} is not implemented in Codex Anywhere yet.`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
async #handleOmxCommand(chatId, args) {
|
|
419
|
+
let plan;
|
|
420
|
+
try {
|
|
421
|
+
plan = planOmxCommand(args);
|
|
422
|
+
}
|
|
423
|
+
catch (error) {
|
|
424
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
425
|
+
await this.#sendText(chatId, message);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
if (plan.kind === "help") {
|
|
429
|
+
await this.#sendText(chatId, buildOmxHelpText());
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
if (plan.kind === "skill") {
|
|
433
|
+
await this.#submitChatInput(chatId, [{ type: "text", text: plan.skillText ?? "" }]);
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
if (plan.kind === "unsupported") {
|
|
437
|
+
await this.#sendText(chatId, plan.message ?? "That OMX command is not supported in Telegram yet.");
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
await this.#runOmxCommand(chatId, plan.argv);
|
|
441
|
+
}
|
|
442
|
+
async #handleWorkspaceCommand(chatId, args) {
|
|
443
|
+
const trimmed = args.trim();
|
|
444
|
+
if (!trimmed) {
|
|
445
|
+
await this.#sendText(chatId, [
|
|
446
|
+
`Current workspace: ${this.#config.workspaceCwd}`,
|
|
447
|
+
"Usage: /workspace <path>",
|
|
448
|
+
].join("\n"));
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (Object.values(this.#state.chats).some((state) => Boolean(state.activeTurnId))) {
|
|
452
|
+
await this.#sendText(chatId, "Cannot change workspace while a task is in progress.");
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
const targetPath = resolveWorkspacePath(trimmed, this.#config.workspaceCwd, os.homedir());
|
|
456
|
+
let stats;
|
|
457
|
+
try {
|
|
458
|
+
stats = await fs.stat(targetPath);
|
|
459
|
+
}
|
|
460
|
+
catch {
|
|
461
|
+
await this.#sendText(chatId, `Workspace path does not exist: ${targetPath}`);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
if (!stats.isDirectory()) {
|
|
465
|
+
await this.#sendText(chatId, `Workspace path is not a directory: ${targetPath}`);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
const previousWorkspace = this.#config.workspaceCwd;
|
|
469
|
+
if (targetPath === previousWorkspace) {
|
|
470
|
+
await this.#sendText(chatId, `Already using workspace: ${targetPath}`);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
this.#config.workspaceCwd = targetPath;
|
|
474
|
+
await saveConfig(this.#configPath, this.#config);
|
|
475
|
+
for (const state of Object.values(this.#state.chats)) {
|
|
476
|
+
state.threadId = null;
|
|
477
|
+
state.freshThread = false;
|
|
478
|
+
state.activeTurnId = null;
|
|
479
|
+
state.turnControlTurnId = null;
|
|
480
|
+
state.turnControlMessageId = null;
|
|
481
|
+
state.queueNextArmed = false;
|
|
482
|
+
state.queuedTurnInput = null;
|
|
483
|
+
state.pendingTurnInput = null;
|
|
484
|
+
state.pendingMention = null;
|
|
485
|
+
state.lastAssistantMessage = null;
|
|
486
|
+
}
|
|
487
|
+
await this.#saveState();
|
|
488
|
+
await this.#sendText(chatId, [
|
|
489
|
+
`Workspace changed to ${targetPath}`,
|
|
490
|
+
`Previous workspace: ${previousWorkspace}`,
|
|
491
|
+
"Detached current thread/session state for this bot instance.",
|
|
492
|
+
].join("\n"));
|
|
493
|
+
}
|
|
494
|
+
async #registerTelegramCommands() {
|
|
495
|
+
try {
|
|
496
|
+
await this.#telegram.setMyCommands(telegramCommands());
|
|
497
|
+
}
|
|
498
|
+
catch (error) {
|
|
499
|
+
this.#logRuntimeError("setMyCommands", error);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
async #handleInteractiveCallback(callbackQueryId, parsed) {
|
|
503
|
+
const session = this.#pendingInteractiveSessions.get(parsed.token);
|
|
504
|
+
if (!session) {
|
|
505
|
+
await this.#telegram.answerCallbackQuery(callbackQueryId, "Prompt expired");
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
if (parsed.action === "cancel") {
|
|
509
|
+
await this.#cancelInteractiveSession(session, "Cancelled");
|
|
510
|
+
await this.#telegram.answerCallbackQuery(callbackQueryId, "Cancelled");
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
const step = session.steps[session.currentStepIndex];
|
|
514
|
+
if (!step) {
|
|
515
|
+
await this.#telegram.answerCallbackQuery(callbackQueryId, "Prompt finished");
|
|
516
|
+
this.#cleanupInteractiveSession(session);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (step.kind === "url") {
|
|
520
|
+
if (parsed.action === "open") {
|
|
521
|
+
await this.#telegram.answerCallbackQuery(callbackQueryId, "Open the link, then press Done when finished.");
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
if (parsed.action === "done") {
|
|
525
|
+
session.answers[step.key] = true;
|
|
526
|
+
await this.#telegram.answerCallbackQuery(callbackQueryId, "Recorded");
|
|
527
|
+
await this.#advanceInteractiveSession(session);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
await this.#telegram.answerCallbackQuery(callbackQueryId, "Use the link or Done button");
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (parsed.action !== "choose" || parsed.value === null) {
|
|
534
|
+
await this.#telegram.answerCallbackQuery(callbackQueryId, "Unknown selection");
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (step.kind === "boolean") {
|
|
538
|
+
session.answers[step.key] = parsed.value === "true";
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
const option = step.options?.find((entry) => entry.value === parsed.value);
|
|
542
|
+
if (!option) {
|
|
543
|
+
await this.#telegram.answerCallbackQuery(callbackQueryId, "Choice unavailable");
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
session.answers[step.key] = option.value;
|
|
547
|
+
}
|
|
548
|
+
await this.#telegram.answerCallbackQuery(callbackQueryId, "Recorded");
|
|
549
|
+
await this.#advanceInteractiveSession(session);
|
|
550
|
+
}
|
|
551
|
+
async #handlePendingInteractiveTextInput(chatId, text) {
|
|
552
|
+
const token = this.#pendingInteractiveSessionByChat.get(chatId);
|
|
553
|
+
if (!token) {
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
const session = this.#pendingInteractiveSessions.get(token);
|
|
557
|
+
if (!session) {
|
|
558
|
+
this.#pendingInteractiveSessionByChat.delete(chatId);
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
561
|
+
if (text === "/cancel") {
|
|
562
|
+
await this.#cancelInteractiveSession(session, "Cancelled");
|
|
563
|
+
return true;
|
|
564
|
+
}
|
|
565
|
+
const step = session.steps[session.currentStepIndex];
|
|
566
|
+
if (!step) {
|
|
567
|
+
this.#cleanupInteractiveSession(session);
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
if (text.startsWith("/") && step.kind !== "url") {
|
|
571
|
+
await this.#sendText(chatId, "Reply with your answer for the active prompt, or send /cancel.");
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
574
|
+
if (step.kind === "text") {
|
|
575
|
+
session.answers[step.key] = text;
|
|
576
|
+
await this.#advanceInteractiveSession(session);
|
|
577
|
+
return true;
|
|
578
|
+
}
|
|
579
|
+
if (step.kind === "number") {
|
|
580
|
+
const value = Number(text);
|
|
581
|
+
if (Number.isNaN(value)) {
|
|
582
|
+
await this.#sendText(chatId, "Please reply with a number, or send /cancel.");
|
|
583
|
+
return true;
|
|
584
|
+
}
|
|
585
|
+
session.answers[step.key] = value;
|
|
586
|
+
await this.#advanceInteractiveSession(session);
|
|
587
|
+
return true;
|
|
588
|
+
}
|
|
589
|
+
await this.#sendText(chatId, "Use the buttons for the active prompt, or send /cancel.");
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
async #startToolInteractiveSession(chatId, requestId, params) {
|
|
593
|
+
const questions = Array.isArray(params.questions) ? params.questions : [];
|
|
594
|
+
const steps = questions
|
|
595
|
+
.map((question, index) => buildToolInteractiveStep(question, index))
|
|
596
|
+
.filter((step) => Boolean(step));
|
|
597
|
+
if (steps.length === 0) {
|
|
598
|
+
await this.#codex.respond(requestId, { answers: {} });
|
|
599
|
+
await this.#sendText(chatId, "No interactive choices were available for that prompt.");
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
await this.#cancelExistingInteractiveSession(chatId);
|
|
603
|
+
const session = {
|
|
604
|
+
requestId,
|
|
605
|
+
chatId,
|
|
606
|
+
kind: "tool",
|
|
607
|
+
token: randomBytes(4).toString("hex"),
|
|
608
|
+
title: "Codex needs your input.",
|
|
609
|
+
steps,
|
|
610
|
+
currentStepIndex: 0,
|
|
611
|
+
answers: {},
|
|
612
|
+
};
|
|
613
|
+
await this.#sendInteractiveStep(session);
|
|
614
|
+
}
|
|
615
|
+
async #startMcpInteractiveSession(chatId, requestId, params) {
|
|
616
|
+
const built = buildMcpInteractiveSession(params);
|
|
617
|
+
if (!built) {
|
|
618
|
+
await this.#codex.respond(requestId, { action: "cancel", content: null, _meta: null });
|
|
619
|
+
await this.#sendText(chatId, "This interactive MCP prompt is not supported in Telegram yet.");
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
await this.#cancelExistingInteractiveSession(chatId);
|
|
623
|
+
const session = {
|
|
624
|
+
requestId,
|
|
625
|
+
chatId,
|
|
626
|
+
kind: "mcp",
|
|
627
|
+
token: randomBytes(4).toString("hex"),
|
|
628
|
+
title: built.title,
|
|
629
|
+
steps: built.steps,
|
|
630
|
+
currentStepIndex: 0,
|
|
631
|
+
answers: {},
|
|
632
|
+
meta: built.meta,
|
|
633
|
+
};
|
|
634
|
+
await this.#sendInteractiveStep(session);
|
|
635
|
+
}
|
|
636
|
+
async #startLocalSlashInteractiveSession(chatId, spec) {
|
|
637
|
+
await this.#cancelExistingInteractiveSession(chatId);
|
|
638
|
+
const session = {
|
|
639
|
+
requestId: null,
|
|
640
|
+
chatId,
|
|
641
|
+
kind: "local",
|
|
642
|
+
token: randomBytes(4).toString("hex"),
|
|
643
|
+
title: spec.title,
|
|
644
|
+
steps: [...spec.steps],
|
|
645
|
+
currentStepIndex: 0,
|
|
646
|
+
answers: {},
|
|
647
|
+
meta: { ...spec.meta, type: "localSlashCommand" },
|
|
648
|
+
};
|
|
649
|
+
await this.#sendInteractiveStep(session);
|
|
650
|
+
}
|
|
651
|
+
async #cancelExistingInteractiveSession(chatId) {
|
|
652
|
+
const existingToken = this.#pendingInteractiveSessionByChat.get(chatId);
|
|
653
|
+
if (!existingToken) {
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
const session = this.#pendingInteractiveSessions.get(existingToken);
|
|
657
|
+
if (!session) {
|
|
658
|
+
this.#pendingInteractiveSessionByChat.delete(chatId);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
await this.#cancelInteractiveSession(session, "Superseded by a new prompt.");
|
|
662
|
+
}
|
|
663
|
+
async #advanceInteractiveSession(session) {
|
|
664
|
+
session.currentStepIndex += 1;
|
|
665
|
+
this.#expandLocalInteractiveSession(session);
|
|
666
|
+
if (session.currentStepIndex >= session.steps.length) {
|
|
667
|
+
await this.#completeInteractiveSession(session);
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
await this.#sendInteractiveStep(session);
|
|
671
|
+
}
|
|
672
|
+
#expandLocalInteractiveSession(session) {
|
|
673
|
+
if (session.kind !== "local") {
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
const meta = session.meta;
|
|
677
|
+
if (!meta || meta.type !== "localSlashCommand" || meta.followUpAdded === true) {
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
const command = asString(meta.command);
|
|
681
|
+
if (!command) {
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
const steps = buildLocalInteractiveFollowUpSteps(command, session.answers);
|
|
685
|
+
if (steps.length === 0) {
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
session.steps.push(...steps);
|
|
689
|
+
meta.followUpAdded = true;
|
|
690
|
+
}
|
|
691
|
+
async #sendInteractiveStep(session) {
|
|
692
|
+
const step = session.steps[session.currentStepIndex];
|
|
693
|
+
if (!step) {
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
this.#stopTypingIndicator(session.chatId);
|
|
697
|
+
this.#pendingInteractiveSessions.set(session.token, session);
|
|
698
|
+
this.#pendingInteractiveSessionByChat.set(session.chatId, session.token);
|
|
699
|
+
const prefix = session.steps.length > 1
|
|
700
|
+
? `(${session.currentStepIndex + 1}/${session.steps.length}) `
|
|
701
|
+
: "";
|
|
702
|
+
const text = `${session.title}\n\n${prefix}${step.prompt}`;
|
|
703
|
+
if (step.kind === "choice" || step.kind === "boolean") {
|
|
704
|
+
const options = step.kind === "boolean"
|
|
705
|
+
? [
|
|
706
|
+
{ label: "Yes", value: "true" },
|
|
707
|
+
{ label: "No", value: "false" },
|
|
708
|
+
]
|
|
709
|
+
: step.options ?? [];
|
|
710
|
+
const replyMarkup = {
|
|
711
|
+
inline_keyboard: [
|
|
712
|
+
...options.map((option) => [
|
|
713
|
+
{
|
|
714
|
+
text: option.label,
|
|
715
|
+
callback_data: formatInteractiveCallbackData(session.token, "choose", option.value),
|
|
716
|
+
},
|
|
717
|
+
]),
|
|
718
|
+
[{ text: "Cancel", callback_data: formatInteractiveCallbackData(session.token, "cancel") }],
|
|
719
|
+
],
|
|
720
|
+
};
|
|
721
|
+
await this.#sendText(session.chatId, text, replyMarkup);
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
if (step.kind === "url") {
|
|
725
|
+
const url = step.options?.[0]?.value ?? "";
|
|
726
|
+
const replyMarkup = {
|
|
727
|
+
inline_keyboard: [
|
|
728
|
+
[{ text: "Open link", url }],
|
|
729
|
+
[{ text: "Done", callback_data: formatInteractiveCallbackData(session.token, "done") }],
|
|
730
|
+
[{ text: "Cancel", callback_data: formatInteractiveCallbackData(session.token, "cancel") }],
|
|
731
|
+
],
|
|
732
|
+
};
|
|
733
|
+
await this.#sendText(session.chatId, text, replyMarkup);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
await this.#sendText(session.chatId, `${text}\n\nReply with your answer in chat, or send /cancel.`);
|
|
737
|
+
}
|
|
738
|
+
async #completeInteractiveSession(session) {
|
|
739
|
+
if (session.kind === "tool") {
|
|
740
|
+
const answers = Object.fromEntries(Object.entries(session.answers).map(([key, value]) => [key, { answers: [String(value)] }]));
|
|
741
|
+
await this.#codex.respond(session.requestId, { answers });
|
|
742
|
+
await this.#sendText(session.chatId, "Submitted your answers.");
|
|
743
|
+
}
|
|
744
|
+
else if (session.kind === "local") {
|
|
745
|
+
await this.#completeLocalInteractiveSession(session);
|
|
746
|
+
}
|
|
747
|
+
else {
|
|
748
|
+
await this.#codex.respond(session.requestId, {
|
|
749
|
+
action: "accept",
|
|
750
|
+
content: session.answers,
|
|
751
|
+
_meta: session.meta ?? null,
|
|
752
|
+
});
|
|
753
|
+
await this.#sendText(session.chatId, "Submitted your response.");
|
|
754
|
+
}
|
|
755
|
+
this.#cleanupInteractiveSession(session);
|
|
756
|
+
}
|
|
757
|
+
async #cancelInteractiveSession(session, message) {
|
|
758
|
+
if (session.kind === "tool") {
|
|
759
|
+
await this.#codex.respond(session.requestId, { answers: {} });
|
|
760
|
+
}
|
|
761
|
+
else if (session.kind === "mcp") {
|
|
762
|
+
await this.#codex.respond(session.requestId, {
|
|
763
|
+
action: "cancel",
|
|
764
|
+
content: null,
|
|
765
|
+
_meta: session.meta ?? null,
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
this.#cleanupInteractiveSession(session);
|
|
769
|
+
await this.#sendText(session.chatId, message);
|
|
770
|
+
}
|
|
771
|
+
async #completeLocalInteractiveSession(session) {
|
|
772
|
+
const meta = session.meta;
|
|
773
|
+
const command = meta ? asString(meta.command) : null;
|
|
774
|
+
this.#cleanupInteractiveSession(session);
|
|
775
|
+
switch (command) {
|
|
776
|
+
case "model":
|
|
777
|
+
await this.#applyLocalModelSelection(session.chatId, session.answers);
|
|
778
|
+
return;
|
|
779
|
+
case "personality":
|
|
780
|
+
await this.#applyLocalPersonalitySelection(session.chatId, session.answers);
|
|
781
|
+
return;
|
|
782
|
+
case "fast":
|
|
783
|
+
await this.#applyLocalFastSelection(session.chatId, session.answers);
|
|
784
|
+
return;
|
|
785
|
+
case "plan":
|
|
786
|
+
case "collab":
|
|
787
|
+
await this.#applyLocalCollaborationSelection(session.chatId, session.answers);
|
|
788
|
+
return;
|
|
789
|
+
case "permissions":
|
|
790
|
+
await this.#applyLocalApprovalPolicySelection(session.chatId, session.answers);
|
|
791
|
+
return;
|
|
792
|
+
case "experimental":
|
|
793
|
+
await this.#applyLocalExperimentalSelection(session.chatId, session.answers);
|
|
794
|
+
return;
|
|
795
|
+
case "feedback":
|
|
796
|
+
await this.#applyLocalFeedbackSelection(session.chatId, session.answers);
|
|
797
|
+
return;
|
|
798
|
+
case "agent":
|
|
799
|
+
await this.#applyLocalAgentSelection(session.chatId, session.answers);
|
|
800
|
+
return;
|
|
801
|
+
case "mention":
|
|
802
|
+
await this.#applyLocalMentionSelection(session.chatId, session.answers);
|
|
803
|
+
return;
|
|
804
|
+
case "verbose":
|
|
805
|
+
await this.#applyLocalVerboseSelection(session.chatId, session.answers);
|
|
806
|
+
return;
|
|
807
|
+
case "rename":
|
|
808
|
+
await this.#applyLocalRenameSelection(session.chatId, session.answers);
|
|
809
|
+
return;
|
|
810
|
+
case "review":
|
|
811
|
+
await this.#applyLocalReviewSelection(session.chatId, session.answers);
|
|
812
|
+
return;
|
|
813
|
+
default:
|
|
814
|
+
await this.#sendText(session.chatId, "Unsupported local interactive command.");
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
#cleanupInteractiveSession(session) {
|
|
818
|
+
this.#pendingInteractiveSessions.delete(session.token);
|
|
819
|
+
const current = this.#pendingInteractiveSessionByChat.get(session.chatId);
|
|
820
|
+
if (current === session.token) {
|
|
821
|
+
this.#pendingInteractiveSessionByChat.delete(session.chatId);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
async #forkThread(chatId) {
|
|
825
|
+
const state = this.#chatState(chatId);
|
|
826
|
+
if (!state.threadId) {
|
|
827
|
+
await this.#sendText(chatId, "No current thread to fork. Send a task or use /new first.");
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
const result = await this.#codex.call("thread/fork", {
|
|
831
|
+
threadId: state.threadId,
|
|
832
|
+
...threadSessionOverrides(this.#config, state),
|
|
833
|
+
});
|
|
834
|
+
const thread = result.thread;
|
|
835
|
+
const threadId = asString(thread?.id);
|
|
836
|
+
if (!threadId) {
|
|
837
|
+
throw new Error("Codex did not return a forked thread id.");
|
|
838
|
+
}
|
|
839
|
+
state.threadId = threadId;
|
|
840
|
+
state.freshThread = true;
|
|
841
|
+
state.activeTurnId = null;
|
|
842
|
+
await this.#saveState();
|
|
843
|
+
await this.#sendText(chatId, `🍴 Forked into a new thread.\nThread: ${threadId}`);
|
|
844
|
+
}
|
|
845
|
+
async #runInitCommand(chatId) {
|
|
846
|
+
const agentsPath = path.join(this.#config.workspaceCwd, "AGENTS.md");
|
|
847
|
+
try {
|
|
848
|
+
await fs.access(agentsPath);
|
|
849
|
+
await this.#sendText(chatId, `AGENTS.md already exists at ${agentsPath}. Skipping /init to avoid overwriting it.`);
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
catch {
|
|
853
|
+
// Missing file is expected here.
|
|
854
|
+
}
|
|
855
|
+
await this.#sendTask(chatId, [
|
|
856
|
+
"Create an AGENTS.md file for this repository.",
|
|
857
|
+
"Inspect the codebase first, then write concise project-specific instructions for Codex.",
|
|
858
|
+
"Include build/test/lint commands, important conventions, and workflow guidance.",
|
|
859
|
+
].join(" "));
|
|
860
|
+
}
|
|
861
|
+
async #runOmxCommand(chatId, argv) {
|
|
862
|
+
const renderedCommand = `omx ${argv.join(" ")}`.trim();
|
|
863
|
+
try {
|
|
864
|
+
const result = await execFileAsync("omx", argv, {
|
|
865
|
+
cwd: this.#config.workspaceCwd,
|
|
866
|
+
maxBuffer: 1024 * 1024 * 8,
|
|
867
|
+
timeout: 120_000,
|
|
868
|
+
});
|
|
869
|
+
await this.#sendHtmlText(chatId, [
|
|
870
|
+
"<b>OMX command finished</b>",
|
|
871
|
+
`<code>${escapeTelegramHtml(renderedCommand)}</code>`,
|
|
872
|
+
"",
|
|
873
|
+
`<pre>${escapeTelegramHtml(truncateOmxOutput(result.stdout || result.stderr || "(no output)"))}</pre>`,
|
|
874
|
+
].join("\n"));
|
|
875
|
+
}
|
|
876
|
+
catch (error) {
|
|
877
|
+
if (isMissingExecutableError(error, "omx")) {
|
|
878
|
+
await this.#sendText(chatId, [
|
|
879
|
+
"OMX is not installed in this environment.",
|
|
880
|
+
"Install oh-my-codex first, then try /omx again.",
|
|
881
|
+
"Terminal setup command: omx setup",
|
|
882
|
+
].join("\n"));
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
const stdout = error && typeof error === "object" && "stdout" in error ? String(error.stdout ?? "") : "";
|
|
886
|
+
const stderr = error && typeof error === "object" && "stderr" in error ? String(error.stderr ?? "") : "";
|
|
887
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
888
|
+
const details = [stderr.trim(), stdout.trim(), message].filter(Boolean).join("\n\n");
|
|
889
|
+
await this.#sendHtmlText(chatId, [
|
|
890
|
+
"<b>OMX command failed</b>",
|
|
891
|
+
`<code>${escapeTelegramHtml(renderedCommand)}</code>`,
|
|
892
|
+
"",
|
|
893
|
+
`<pre>${escapeTelegramHtml(truncateOmxOutput(details || "(no output)"))}</pre>`,
|
|
894
|
+
].join("\n"));
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
async #compactThread(chatId) {
|
|
898
|
+
const threadId = await this.#resumeThread(chatId, true);
|
|
899
|
+
await this.#codex.call("thread/compact/start", { threadId });
|
|
900
|
+
await this.#sendText(chatId, "🗜️ Requested thread compaction.");
|
|
901
|
+
}
|
|
902
|
+
async #startReview(chatId, args) {
|
|
903
|
+
if (!args.trim()) {
|
|
904
|
+
await this.#startLocalSlashInteractiveSession(chatId, buildReviewInteractiveSession());
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
await this.#runReview(chatId, parseReviewTarget(args));
|
|
908
|
+
}
|
|
909
|
+
async #runReview(chatId, target) {
|
|
910
|
+
const threadId = await this.#resumeThread(chatId, true);
|
|
911
|
+
const result = await this.#codex.call("review/start", {
|
|
912
|
+
threadId,
|
|
913
|
+
target,
|
|
914
|
+
});
|
|
915
|
+
const reviewThreadId = asString(result.reviewThreadId) ?? threadId;
|
|
916
|
+
await this.#sendText(chatId, `🔍 Started review.\nReview thread: ${reviewThreadId}`);
|
|
917
|
+
}
|
|
918
|
+
async #renameThread(chatId, args) {
|
|
919
|
+
const name = args.trim();
|
|
920
|
+
if (!name) {
|
|
921
|
+
const state = this.#chatState(chatId);
|
|
922
|
+
if (!state.threadId) {
|
|
923
|
+
await this.#sendText(chatId, "No current thread to rename. Send a task or use /new first.");
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
await this.#startLocalSlashInteractiveSession(chatId, buildRenameInteractiveSession());
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
const state = this.#chatState(chatId);
|
|
930
|
+
if (!state.threadId) {
|
|
931
|
+
await this.#sendText(chatId, "No current thread to rename. Send a task or use /new first.");
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
await this.#codex.call("thread/name/set", {
|
|
935
|
+
threadId: state.threadId,
|
|
936
|
+
name,
|
|
937
|
+
});
|
|
938
|
+
await this.#sendText(chatId, `✏️ Renamed thread to: ${name}`);
|
|
939
|
+
}
|
|
940
|
+
async #handleModelCommand(chatId, args) {
|
|
941
|
+
const state = this.#chatState(chatId);
|
|
942
|
+
const trimmed = args.trim();
|
|
943
|
+
const response = await this.#codex.call("model/list", {
|
|
944
|
+
limit: 100,
|
|
945
|
+
includeHidden: false,
|
|
946
|
+
});
|
|
947
|
+
const models = Array.isArray(response.data) ? response.data : [];
|
|
948
|
+
if (!trimmed) {
|
|
949
|
+
const session = buildModelInteractiveSession(models, state);
|
|
950
|
+
if (!session) {
|
|
951
|
+
await this.#sendText(chatId, "No models were available to choose from.");
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
await this.#startLocalSlashInteractiveSession(chatId, session);
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
if (trimmed === "status") {
|
|
958
|
+
const lines = [
|
|
959
|
+
`model override: ${state.model ?? "(default)"}`,
|
|
960
|
+
`reasoning effort: ${state.reasoningEffort ?? "(default)"}`,
|
|
961
|
+
"",
|
|
962
|
+
"Available models:",
|
|
963
|
+
...models
|
|
964
|
+
.map((entry) => formatModelSummary(entry))
|
|
965
|
+
.filter((line) => Boolean(line))
|
|
966
|
+
.slice(0, 20),
|
|
967
|
+
"",
|
|
968
|
+
"Usage: /model <model> [minimal|low|medium|high]",
|
|
969
|
+
"Usage: /model reset",
|
|
970
|
+
];
|
|
971
|
+
await this.#sendText(chatId, lines.join("\n"));
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
if (trimmed === "reset") {
|
|
975
|
+
state.model = null;
|
|
976
|
+
state.reasoningEffort = null;
|
|
977
|
+
await this.#saveState();
|
|
978
|
+
await this.#sendText(chatId, "Model override cleared.");
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
const parts = trimmed.split(/\s+/).filter(Boolean);
|
|
982
|
+
const model = parts[0] ?? "";
|
|
983
|
+
const effort = parts[1] ? normalizeReasoningEffort(parts[1]) : null;
|
|
984
|
+
const knownModel = models.find((entry) => entry && typeof entry === "object" && asString(entry.model) === model);
|
|
985
|
+
if (!knownModel) {
|
|
986
|
+
await this.#sendText(chatId, `Unknown model: ${model}\nRun /model to list available models.`);
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
if (parts[1] && !effort) {
|
|
990
|
+
await this.#sendText(chatId, "Usage: /model <model> [minimal|low|medium|high]");
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
state.model = model;
|
|
994
|
+
state.reasoningEffort = effort;
|
|
995
|
+
await this.#saveState();
|
|
996
|
+
await this.#sendText(chatId, `Model override set to ${model}${effort ? ` (${effort})` : ""}.`);
|
|
997
|
+
}
|
|
998
|
+
async #handlePersonalityCommand(chatId, args) {
|
|
999
|
+
const state = this.#chatState(chatId);
|
|
1000
|
+
const trimmed = args.trim().toLowerCase();
|
|
1001
|
+
if (!trimmed) {
|
|
1002
|
+
await this.#startLocalSlashInteractiveSession(chatId, buildPersonalityInteractiveSession(state));
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
if (trimmed === "status") {
|
|
1006
|
+
await this.#sendText(chatId, `Personality: ${state.personality ?? "default"}.`);
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
if (trimmed === "reset") {
|
|
1010
|
+
state.personality = null;
|
|
1011
|
+
await this.#saveState();
|
|
1012
|
+
await this.#sendText(chatId, "Personality reset to default.");
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
if (!["friendly", "pragmatic", "none"].includes(trimmed)) {
|
|
1016
|
+
await this.#sendText(chatId, "Usage: /personality [friendly|pragmatic|none|status|reset]");
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
state.personality = trimmed;
|
|
1020
|
+
await this.#saveState();
|
|
1021
|
+
await this.#sendText(chatId, `Personality set to ${trimmed}.`);
|
|
1022
|
+
}
|
|
1023
|
+
async #handleFastCommand(chatId, args) {
|
|
1024
|
+
const state = this.#chatState(chatId);
|
|
1025
|
+
const value = args.trim().toLowerCase();
|
|
1026
|
+
if (!value) {
|
|
1027
|
+
await this.#startLocalSlashInteractiveSession(chatId, buildFastInteractiveSession(state));
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
if (value === "status") {
|
|
1031
|
+
await this.#sendText(chatId, `Fast mode is ${state.serviceTier === "fast" ? "on" : "off"}.`);
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
if (value === "on") {
|
|
1035
|
+
state.serviceTier = "fast";
|
|
1036
|
+
await this.#saveState();
|
|
1037
|
+
await this.#sendText(chatId, "Fast mode enabled.");
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
if (value === "off") {
|
|
1041
|
+
state.serviceTier = null;
|
|
1042
|
+
await this.#saveState();
|
|
1043
|
+
await this.#sendText(chatId, "Fast mode disabled.");
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
await this.#sendText(chatId, "Usage: /fast [on|off|status]");
|
|
1047
|
+
}
|
|
1048
|
+
async #handlePlanCommand(chatId, args) {
|
|
1049
|
+
const trimmed = args.trim();
|
|
1050
|
+
if (trimmed) {
|
|
1051
|
+
const collaborationMode = await this.#resolvePlanCollaborationMode();
|
|
1052
|
+
if (!collaborationMode) {
|
|
1053
|
+
await this.#sendText(chatId, "Plan mode is unavailable in this session.");
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
await this.#sendTask(chatId, trimmed, {
|
|
1057
|
+
collaborationMode,
|
|
1058
|
+
});
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
const session = buildPlanInteractiveSession(await this.#listCollaborationModes(), this.#chatState(chatId));
|
|
1062
|
+
if (!session) {
|
|
1063
|
+
await this.#sendText(chatId, "No plan modes were available.");
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
await this.#startLocalSlashInteractiveSession(chatId, session);
|
|
1067
|
+
}
|
|
1068
|
+
async #handleCollabCommand(chatId, args) {
|
|
1069
|
+
const state = this.#chatState(chatId);
|
|
1070
|
+
const trimmed = args.trim();
|
|
1071
|
+
if (!trimmed) {
|
|
1072
|
+
const session = buildCollaborationInteractiveSession(await this.#listCollaborationModes(), state);
|
|
1073
|
+
if (!session) {
|
|
1074
|
+
await this.#sendText(chatId, "No collaboration modes were available.");
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
await this.#startLocalSlashInteractiveSession(chatId, session);
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
if (trimmed === "status") {
|
|
1081
|
+
await this.#sendText(chatId, `Collaboration mode: ${state.collaborationModeName ?? "default"}.`);
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
if (trimmed === "reset") {
|
|
1085
|
+
state.collaborationMode = null;
|
|
1086
|
+
state.collaborationModeName = null;
|
|
1087
|
+
await this.#saveState();
|
|
1088
|
+
await this.#sendText(chatId, "Collaboration mode reset to default.");
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
const collaborationMode = await this.#resolveNamedCollaborationMode(trimmed);
|
|
1092
|
+
if (!collaborationMode) {
|
|
1093
|
+
await this.#sendText(chatId, `Unknown collaboration mode: ${trimmed}`);
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
state.collaborationMode = collaborationMode;
|
|
1097
|
+
state.collaborationModeName = trimmed;
|
|
1098
|
+
await this.#saveState();
|
|
1099
|
+
await this.#sendText(chatId, `Collaboration mode set to ${trimmed}.`);
|
|
1100
|
+
}
|
|
1101
|
+
async #handleApprovalPolicyCommand(chatId, args) {
|
|
1102
|
+
const state = this.#chatState(chatId);
|
|
1103
|
+
const trimmed = args.trim().toLowerCase();
|
|
1104
|
+
if (!trimmed) {
|
|
1105
|
+
await this.#startLocalSlashInteractiveSession(chatId, buildApprovalPolicyInteractiveSession(state));
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
if (trimmed === "status") {
|
|
1109
|
+
await this.#sendText(chatId, [
|
|
1110
|
+
`approval policy: ${state.approvalPolicy ?? "on-request"}`,
|
|
1111
|
+
"Usage: /permissions [untrusted|on-failure|on-request|never]",
|
|
1112
|
+
].join("\n"));
|
|
1113
|
+
return;
|
|
1114
|
+
}
|
|
1115
|
+
const policy = normalizeApprovalPolicy(trimmed);
|
|
1116
|
+
if (!policy) {
|
|
1117
|
+
await this.#sendText(chatId, "Usage: /permissions [untrusted|on-failure|on-request|never]");
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
state.approvalPolicy = policy;
|
|
1121
|
+
await this.#saveState();
|
|
1122
|
+
await this.#sendText(chatId, `Approval policy set to ${policy}.`);
|
|
1123
|
+
}
|
|
1124
|
+
async #clearThread(chatId) {
|
|
1125
|
+
await this.#startNewThread(chatId, true);
|
|
1126
|
+
await this.#sendText(chatId, "🧹 Started a fresh thread. Telegram message history remains visible, but Codex context is new.");
|
|
1127
|
+
}
|
|
1128
|
+
async #sendGitDiff(chatId) {
|
|
1129
|
+
try {
|
|
1130
|
+
const status = await execFileAsync("git", ["status", "--short"], {
|
|
1131
|
+
cwd: this.#config.workspaceCwd,
|
|
1132
|
+
maxBuffer: 1024 * 1024,
|
|
1133
|
+
});
|
|
1134
|
+
const staged = await execFileAsync("git", ["diff", "--cached", "--stat", "--patch"], {
|
|
1135
|
+
cwd: this.#config.workspaceCwd,
|
|
1136
|
+
maxBuffer: 1024 * 1024,
|
|
1137
|
+
});
|
|
1138
|
+
const unstaged = await execFileAsync("git", ["diff", "--stat", "--patch"], {
|
|
1139
|
+
cwd: this.#config.workspaceCwd,
|
|
1140
|
+
maxBuffer: 1024 * 1024,
|
|
1141
|
+
});
|
|
1142
|
+
const text = [
|
|
1143
|
+
"Git status:",
|
|
1144
|
+
status.stdout.trim() || "(clean)",
|
|
1145
|
+
staged.stdout.trim() ? `\nStaged diff:\n${staged.stdout.trim()}` : "",
|
|
1146
|
+
unstaged.stdout.trim() ? `\nUnstaged diff:\n${unstaged.stdout.trim()}` : "",
|
|
1147
|
+
]
|
|
1148
|
+
.filter(Boolean)
|
|
1149
|
+
.join("\n");
|
|
1150
|
+
await this.#sendText(chatId, text);
|
|
1151
|
+
}
|
|
1152
|
+
catch (error) {
|
|
1153
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1154
|
+
await this.#sendText(chatId, `Failed to compute git diff: ${message}`);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
async #handleMentionCommand(chatId, args) {
|
|
1158
|
+
const query = args.trim();
|
|
1159
|
+
if (!query) {
|
|
1160
|
+
await this.#sendText(chatId, "Usage: /mention <file query>");
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
const files = await this.#searchWorkspaceFiles(query);
|
|
1164
|
+
const session = buildMentionInteractiveSession(files);
|
|
1165
|
+
if (!session) {
|
|
1166
|
+
await this.#sendText(chatId, `No files matched: ${query}`);
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
await this.#startLocalSlashInteractiveSession(chatId, session);
|
|
1170
|
+
}
|
|
1171
|
+
async #copyLastOutput(chatId) {
|
|
1172
|
+
const last = this.#chatState(chatId).lastAssistantMessage;
|
|
1173
|
+
if (!last) {
|
|
1174
|
+
await this.#sendText(chatId, "`/copy` is unavailable before the first completed Codex output.");
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
await this.#sendText(chatId, last);
|
|
1178
|
+
}
|
|
1179
|
+
async #sendSkills(chatId) {
|
|
1180
|
+
const response = await this.#codex.call("skills/list", {
|
|
1181
|
+
cwds: [this.#config.workspaceCwd],
|
|
1182
|
+
});
|
|
1183
|
+
const data = Array.isArray(response.data) ? response.data : [];
|
|
1184
|
+
const lines = ["Skills:"];
|
|
1185
|
+
for (const entry of data) {
|
|
1186
|
+
const skills = Array.isArray(entry.skills)
|
|
1187
|
+
? entry.skills
|
|
1188
|
+
: [];
|
|
1189
|
+
for (const skill of skills.slice(0, 20)) {
|
|
1190
|
+
const name = asString(skill.name);
|
|
1191
|
+
const description = asString(skill.description);
|
|
1192
|
+
if (name) {
|
|
1193
|
+
lines.push(`- ${name}${description ? `: ${description}` : ""}`);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
await this.#sendText(chatId, lines.join("\n"));
|
|
1198
|
+
}
|
|
1199
|
+
async #sendMcpServers(chatId) {
|
|
1200
|
+
const response = await this.#codex.call("mcpServerStatus/list", {
|
|
1201
|
+
limit: 100,
|
|
1202
|
+
detail: "toolsAndAuthOnly",
|
|
1203
|
+
});
|
|
1204
|
+
const data = Array.isArray(response.data) ? response.data : [];
|
|
1205
|
+
const lines = ["MCP servers:"];
|
|
1206
|
+
for (const server of data.slice(0, 20)) {
|
|
1207
|
+
const entry = server;
|
|
1208
|
+
const name = asString(entry.name);
|
|
1209
|
+
const auth = asString(entry.authStatus) ?? "unknown";
|
|
1210
|
+
const tools = entry.tools && typeof entry.tools === "object" ? Object.keys(entry.tools).length : 0;
|
|
1211
|
+
if (name) {
|
|
1212
|
+
lines.push(`- ${name}: auth=${auth}, tools=${tools}`);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
await this.#sendText(chatId, lines.join("\n"));
|
|
1216
|
+
}
|
|
1217
|
+
async #sendApps(chatId) {
|
|
1218
|
+
const state = this.#chatState(chatId);
|
|
1219
|
+
const response = await this.#codex.call("app/list", {
|
|
1220
|
+
limit: 50,
|
|
1221
|
+
threadId: state.threadId,
|
|
1222
|
+
});
|
|
1223
|
+
const data = Array.isArray(response.data) ? response.data : [];
|
|
1224
|
+
const lines = ["Apps:"];
|
|
1225
|
+
for (const app of data.slice(0, 20)) {
|
|
1226
|
+
const entry = app;
|
|
1227
|
+
const name = asString(entry.name);
|
|
1228
|
+
const enabled = entry.isEnabled === true ? "enabled" : "disabled";
|
|
1229
|
+
const access = entry.isAccessible === true ? "accessible" : "needs-auth";
|
|
1230
|
+
if (name) {
|
|
1231
|
+
lines.push(`- ${name}: ${enabled}, ${access}`);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
await this.#sendText(chatId, lines.join("\n"));
|
|
1235
|
+
}
|
|
1236
|
+
async #sendPlugins(chatId) {
|
|
1237
|
+
const response = await this.#codex.call("plugin/list", {
|
|
1238
|
+
cwds: [this.#config.workspaceCwd],
|
|
1239
|
+
});
|
|
1240
|
+
const marketplaces = Array.isArray(response.marketplaces) ? response.marketplaces : [];
|
|
1241
|
+
const lines = ["Plugins:"];
|
|
1242
|
+
for (const marketplace of marketplaces) {
|
|
1243
|
+
const plugins = Array.isArray(marketplace.plugins)
|
|
1244
|
+
? marketplace.plugins
|
|
1245
|
+
: [];
|
|
1246
|
+
for (const plugin of plugins.slice(0, 20)) {
|
|
1247
|
+
const name = asString(plugin.name);
|
|
1248
|
+
const installed = plugin.installed === true ? "installed" : "not-installed";
|
|
1249
|
+
const enabled = plugin.enabled === true ? "enabled" : "disabled";
|
|
1250
|
+
if (name) {
|
|
1251
|
+
lines.push(`- ${name}: ${installed}, ${enabled}`);
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
await this.#sendText(chatId, lines.join("\n"));
|
|
1256
|
+
}
|
|
1257
|
+
async #logoutAccount(chatId) {
|
|
1258
|
+
await this.#codex.call("account/logout");
|
|
1259
|
+
await this.#sendText(chatId, "Logged out of Codex.");
|
|
1260
|
+
}
|
|
1261
|
+
async #stopBackgroundTerminals(chatId) {
|
|
1262
|
+
const state = this.#chatState(chatId);
|
|
1263
|
+
if (!state.threadId) {
|
|
1264
|
+
await this.#sendText(chatId, "No current thread. Nothing to stop.");
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
await this.#codex.call("thread/backgroundTerminals/clean", {
|
|
1268
|
+
threadId: state.threadId,
|
|
1269
|
+
});
|
|
1270
|
+
await this.#sendText(chatId, "Stopping all background terminals for the current thread.");
|
|
1271
|
+
}
|
|
1272
|
+
async #handleExperimentalCommand(chatId, args) {
|
|
1273
|
+
const trimmed = args.trim();
|
|
1274
|
+
if (!trimmed) {
|
|
1275
|
+
const response = await this.#codex.call("experimentalFeature/list", {
|
|
1276
|
+
limit: 100,
|
|
1277
|
+
});
|
|
1278
|
+
const data = Array.isArray(response.data) ? response.data : [];
|
|
1279
|
+
const session = buildExperimentalInteractiveSession(data);
|
|
1280
|
+
if (!session) {
|
|
1281
|
+
await this.#sendText(chatId, "No experimental features were available.");
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
await this.#startLocalSlashInteractiveSession(chatId, session);
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
if (trimmed === "status") {
|
|
1288
|
+
const response = await this.#codex.call("experimentalFeature/list", {
|
|
1289
|
+
limit: 100,
|
|
1290
|
+
});
|
|
1291
|
+
const data = Array.isArray(response.data) ? response.data : [];
|
|
1292
|
+
const lines = ["Experimental features:"];
|
|
1293
|
+
for (const feature of data.slice(0, 20)) {
|
|
1294
|
+
const entry = feature;
|
|
1295
|
+
const name = asString(entry.name);
|
|
1296
|
+
const stage = asString(entry.stage) ?? "unknown";
|
|
1297
|
+
const enabled = entry.enabled === true ? "on" : "off";
|
|
1298
|
+
if (name) {
|
|
1299
|
+
lines.push(`- ${name}: ${enabled} (${stage})`);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
await this.#sendText(chatId, lines.join("\n"));
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1305
|
+
const parts = trimmed.split(/\s+/).filter(Boolean);
|
|
1306
|
+
if (parts.length !== 2 || !["on", "off"].includes(parts[1].toLowerCase())) {
|
|
1307
|
+
await this.#sendText(chatId, "Usage: /experimental <feature> <on|off>");
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
const featureName = parts[0];
|
|
1311
|
+
const enabled = parts[1].toLowerCase() === "on";
|
|
1312
|
+
await this.#codex.call("experimentalFeature/enablement/set", {
|
|
1313
|
+
enablement: { [featureName]: enabled },
|
|
1314
|
+
});
|
|
1315
|
+
await this.#sendText(chatId, `Experimental feature ${featureName} set to ${enabled ? "on" : "off"}.`);
|
|
1316
|
+
}
|
|
1317
|
+
async #handleFeedbackCommand(chatId) {
|
|
1318
|
+
await this.#startLocalSlashInteractiveSession(chatId, buildFeedbackInteractiveSession());
|
|
1319
|
+
}
|
|
1320
|
+
async #handleVerboseCommand(chatId, args) {
|
|
1321
|
+
const state = this.#chatState(chatId);
|
|
1322
|
+
const value = args.trim().toLowerCase();
|
|
1323
|
+
if (!value) {
|
|
1324
|
+
await this.#startLocalSlashInteractiveSession(chatId, buildVerboseInteractiveSession(state));
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
if (value === "status") {
|
|
1328
|
+
await this.#sendText(chatId, `Detailed tool/file messages are ${state.verbose ? "on" : "off"}.`);
|
|
1329
|
+
return;
|
|
1330
|
+
}
|
|
1331
|
+
if (value === "on") {
|
|
1332
|
+
state.verbose = true;
|
|
1333
|
+
await this.#saveState();
|
|
1334
|
+
await this.#sendText(chatId, "Detailed tool/file messages enabled.");
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
if (value === "off") {
|
|
1338
|
+
state.verbose = false;
|
|
1339
|
+
await this.#saveState();
|
|
1340
|
+
await this.#sendText(chatId, "Detailed tool/file messages disabled.");
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
await this.#sendText(chatId, "Usage: /verbose [on|off|status]");
|
|
1344
|
+
}
|
|
1345
|
+
async #handleAgentCommand(chatId) {
|
|
1346
|
+
const state = this.#chatState(chatId);
|
|
1347
|
+
const response = await this.#codex.call("thread/list", {
|
|
1348
|
+
limit: 50,
|
|
1349
|
+
sortKey: "updated_at",
|
|
1350
|
+
cwd: this.#config.workspaceCwd,
|
|
1351
|
+
sourceKinds: [
|
|
1352
|
+
"cli",
|
|
1353
|
+
"vscode",
|
|
1354
|
+
"exec",
|
|
1355
|
+
"appServer",
|
|
1356
|
+
"subAgent",
|
|
1357
|
+
"subAgentReview",
|
|
1358
|
+
"subAgentCompact",
|
|
1359
|
+
"subAgentThreadSpawn",
|
|
1360
|
+
"subAgentOther",
|
|
1361
|
+
],
|
|
1362
|
+
});
|
|
1363
|
+
const session = buildAgentThreadInteractiveSession(Array.isArray(response.data) ? response.data : [], state.threadId);
|
|
1364
|
+
if (!session) {
|
|
1365
|
+
await this.#sendText(chatId, "No agent threads were available.");
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
await this.#startLocalSlashInteractiveSession(chatId, session);
|
|
1369
|
+
}
|
|
1370
|
+
async #showSessions(chatId) {
|
|
1371
|
+
const currentThreadId = this.#chatState(chatId).threadId;
|
|
1372
|
+
const response = await this.#codex.call("thread/list", {
|
|
1373
|
+
limit: 8,
|
|
1374
|
+
sortKey: "updated_at",
|
|
1375
|
+
sourceKinds: [
|
|
1376
|
+
"cli",
|
|
1377
|
+
"vscode",
|
|
1378
|
+
"exec",
|
|
1379
|
+
"appServer",
|
|
1380
|
+
"subAgent",
|
|
1381
|
+
"subAgentReview",
|
|
1382
|
+
"subAgentCompact",
|
|
1383
|
+
"subAgentThreadSpawn",
|
|
1384
|
+
"subAgentOther",
|
|
1385
|
+
],
|
|
1386
|
+
});
|
|
1387
|
+
const threads = Array.isArray(response.data) ? [...response.data] : [];
|
|
1388
|
+
if (currentThreadId
|
|
1389
|
+
&& !threads.some((entry) => entry && typeof entry === "object" && asString(entry.id) === currentThreadId)) {
|
|
1390
|
+
try {
|
|
1391
|
+
const currentResponse = await this.#codex.call("thread/read", {
|
|
1392
|
+
threadId: currentThreadId,
|
|
1393
|
+
includeTurns: false,
|
|
1394
|
+
});
|
|
1395
|
+
if (currentResponse.thread && typeof currentResponse.thread === "object") {
|
|
1396
|
+
threads.unshift(currentResponse.thread);
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
catch (error) {
|
|
1400
|
+
this.#logRuntimeError("thread/read current session", error);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
if (threads.length === 0) {
|
|
1404
|
+
await this.#sendText(chatId, "No recent sessions were found.");
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
threads.sort((left, right) => {
|
|
1408
|
+
const leftId = left && typeof left === "object" ? asString(left.id) : null;
|
|
1409
|
+
const rightId = right && typeof right === "object" ? asString(right.id) : null;
|
|
1410
|
+
if (leftId === currentThreadId) {
|
|
1411
|
+
return -1;
|
|
1412
|
+
}
|
|
1413
|
+
if (rightId === currentThreadId) {
|
|
1414
|
+
return 1;
|
|
1415
|
+
}
|
|
1416
|
+
return 0;
|
|
1417
|
+
});
|
|
1418
|
+
await this.#sendHtmlText(chatId, "<b>Sessions</b>");
|
|
1419
|
+
for (const rawThread of threads) {
|
|
1420
|
+
if (!rawThread || typeof rawThread !== "object") {
|
|
1421
|
+
continue;
|
|
1422
|
+
}
|
|
1423
|
+
const thread = rawThread;
|
|
1424
|
+
const threadId = asString(thread.id);
|
|
1425
|
+
if (!threadId) {
|
|
1426
|
+
continue;
|
|
1427
|
+
}
|
|
1428
|
+
const isCurrent = currentThreadId === threadId;
|
|
1429
|
+
const replyMarkup = {
|
|
1430
|
+
inline_keyboard: [
|
|
1431
|
+
isCurrent
|
|
1432
|
+
? [
|
|
1433
|
+
{
|
|
1434
|
+
text: "Status",
|
|
1435
|
+
callback_data: formatSessionCallbackData("status", threadId),
|
|
1436
|
+
},
|
|
1437
|
+
]
|
|
1438
|
+
: [
|
|
1439
|
+
{
|
|
1440
|
+
text: "Take Over",
|
|
1441
|
+
callback_data: formatSessionCallbackData("takeover", threadId),
|
|
1442
|
+
},
|
|
1443
|
+
{
|
|
1444
|
+
text: "Status",
|
|
1445
|
+
callback_data: formatSessionCallbackData("status", threadId),
|
|
1446
|
+
},
|
|
1447
|
+
],
|
|
1448
|
+
],
|
|
1449
|
+
};
|
|
1450
|
+
await this.#sendHtmlText(chatId, formatSessionCardHtml(thread, currentThreadId), replyMarkup);
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
async #handleSessionCallback(callbackQueryId, chatId, parsed) {
|
|
1454
|
+
if (chatId === null) {
|
|
1455
|
+
await this.#telegram.answerCallbackQuery(callbackQueryId, "Chat unavailable");
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
if (parsed.action === "takeover") {
|
|
1459
|
+
const state = this.#chatState(chatId);
|
|
1460
|
+
state.threadId = parsed.threadId;
|
|
1461
|
+
state.freshThread = false;
|
|
1462
|
+
state.activeTurnId = null;
|
|
1463
|
+
await this.#saveState();
|
|
1464
|
+
await this.#codex.call("thread/resume", {
|
|
1465
|
+
threadId: parsed.threadId,
|
|
1466
|
+
...threadSessionOverrides(this.#config, state),
|
|
1467
|
+
});
|
|
1468
|
+
await this.#telegram.answerCallbackQuery(callbackQueryId, "Session taken over");
|
|
1469
|
+
await this.#sendHtmlText(chatId, `Took over session <code>${escapeTelegramHtml(parsed.threadId)}</code>.`);
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
const response = await this.#codex.call("thread/read", {
|
|
1473
|
+
threadId: parsed.threadId,
|
|
1474
|
+
includeTurns: false,
|
|
1475
|
+
});
|
|
1476
|
+
const thread = response.thread;
|
|
1477
|
+
if (!thread) {
|
|
1478
|
+
await this.#telegram.answerCallbackQuery(callbackQueryId, "Session unavailable");
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
await this.#telegram.answerCallbackQuery(callbackQueryId, "Showing session");
|
|
1482
|
+
await this.#sendHtmlText(chatId, formatSessionStatusHtml(thread, this.#chatState(chatId).threadId));
|
|
1483
|
+
}
|
|
1484
|
+
async #sendRolloutPath(chatId) {
|
|
1485
|
+
const state = this.#chatState(chatId);
|
|
1486
|
+
if (!state.threadId) {
|
|
1487
|
+
await this.#sendText(chatId, "No current thread. Send a task or use /new first.");
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
const response = await this.#codex.call("thread/read", {
|
|
1491
|
+
threadId: state.threadId,
|
|
1492
|
+
includeTurns: false,
|
|
1493
|
+
});
|
|
1494
|
+
const thread = response.thread;
|
|
1495
|
+
const rolloutPath = asString(thread?.path);
|
|
1496
|
+
await this.#sendText(chatId, rolloutPath ? `Rollout path: ${rolloutPath}` : "No rollout path available.");
|
|
1497
|
+
}
|
|
1498
|
+
async #sendTurnControls(chatId, turnId) {
|
|
1499
|
+
const state = this.#chatState(chatId);
|
|
1500
|
+
const hasPendingInput = Array.isArray(state.pendingTurnInput) && state.pendingTurnInput.length > 0;
|
|
1501
|
+
const replyMarkup = {
|
|
1502
|
+
inline_keyboard: [
|
|
1503
|
+
[
|
|
1504
|
+
{
|
|
1505
|
+
text: hasPendingInput ? "Steer" : "Queue Next",
|
|
1506
|
+
callback_data: formatTurnControlCallbackData(hasPendingInput ? "steer" : "queue", turnId),
|
|
1507
|
+
},
|
|
1508
|
+
{
|
|
1509
|
+
text: hasPendingInput ? "Queue Next" : "Interrupt",
|
|
1510
|
+
callback_data: formatTurnControlCallbackData(hasPendingInput ? "queue" : "interrupt", turnId),
|
|
1511
|
+
},
|
|
1512
|
+
...(hasPendingInput
|
|
1513
|
+
? [
|
|
1514
|
+
{
|
|
1515
|
+
text: "Cancel",
|
|
1516
|
+
callback_data: formatTurnControlCallbackData("cancel", turnId),
|
|
1517
|
+
},
|
|
1518
|
+
]
|
|
1519
|
+
: []),
|
|
1520
|
+
],
|
|
1521
|
+
],
|
|
1522
|
+
};
|
|
1523
|
+
const message = await this.#sendText(chatId, hasPendingInput
|
|
1524
|
+
? "Turn active — steer with your message or queue it for next?"
|
|
1525
|
+
: "Turn active.", replyMarkup);
|
|
1526
|
+
state.turnControlTurnId = turnId;
|
|
1527
|
+
state.turnControlMessageId = message.message_id;
|
|
1528
|
+
await this.#saveState();
|
|
1529
|
+
}
|
|
1530
|
+
async #handleTurnControlCallback(callbackQueryId, chatId, parsed) {
|
|
1531
|
+
if (chatId === null) {
|
|
1532
|
+
await this.#telegram.answerCallbackQuery(callbackQueryId, "Chat unavailable");
|
|
1533
|
+
return;
|
|
1534
|
+
}
|
|
1535
|
+
const state = this.#chatState(chatId);
|
|
1536
|
+
if (state.activeTurnId !== parsed.turnId) {
|
|
1537
|
+
await this.#clearTurnControls(chatId, parsed.turnId);
|
|
1538
|
+
await this.#telegram.answerCallbackQuery(callbackQueryId, "Turn is no longer active");
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
if (parsed.action === "steer") {
|
|
1542
|
+
if (!state.pendingTurnInput) {
|
|
1543
|
+
await this.#clearTurnControls(chatId, parsed.turnId);
|
|
1544
|
+
await this.#telegram.answerCallbackQuery(callbackQueryId, "No pending message");
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
const input = state.pendingTurnInput;
|
|
1548
|
+
state.pendingTurnInput = null;
|
|
1549
|
+
await this.#saveState();
|
|
1550
|
+
await this.#clearTurnControls(chatId, parsed.turnId);
|
|
1551
|
+
await this.#codex.call("turn/steer", {
|
|
1552
|
+
threadId: state.threadId,
|
|
1553
|
+
input,
|
|
1554
|
+
expectedTurnId: state.activeTurnId,
|
|
1555
|
+
});
|
|
1556
|
+
await this.#telegram.answerCallbackQuery(callbackQueryId, "Steering current turn");
|
|
1557
|
+
await this.#sendHtmlText(chatId, formatPendingInputActionHtml("steered", input));
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
if (parsed.action === "queue") {
|
|
1561
|
+
if (state.pendingTurnInput) {
|
|
1562
|
+
state.queuedTurnInput = state.pendingTurnInput;
|
|
1563
|
+
state.pendingTurnInput = null;
|
|
1564
|
+
await this.#saveState();
|
|
1565
|
+
await this.#clearTurnControls(chatId, parsed.turnId);
|
|
1566
|
+
await this.#telegram.answerCallbackQuery(callbackQueryId, "Queued for next turn");
|
|
1567
|
+
await this.#sendHtmlText(chatId, formatPendingInputActionHtml("queued", state.queuedTurnInput));
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
state.queueNextArmed = true;
|
|
1571
|
+
await this.#saveState();
|
|
1572
|
+
await this.#clearTurnControls(chatId, parsed.turnId);
|
|
1573
|
+
await this.#telegram.answerCallbackQuery(callbackQueryId, "Next message will be queued");
|
|
1574
|
+
await this.#sendHtmlText(chatId, formatPendingInputActionHtml("armed"));
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
if (parsed.action === "cancel") {
|
|
1578
|
+
state.pendingTurnInput = null;
|
|
1579
|
+
await this.#saveState();
|
|
1580
|
+
await this.#clearTurnControls(chatId, parsed.turnId);
|
|
1581
|
+
await this.#telegram.answerCallbackQuery(callbackQueryId, "Cancelled");
|
|
1582
|
+
await this.#sendText(chatId, "Cancelled.");
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
await this.#clearTurnControls(chatId, parsed.turnId);
|
|
1586
|
+
await this.#telegram.answerCallbackQuery(callbackQueryId, "Interrupting");
|
|
1587
|
+
await this.#interruptTurn(chatId);
|
|
1588
|
+
}
|
|
1589
|
+
async #handleServerRequest(message) {
|
|
1590
|
+
const method = String(message.method);
|
|
1591
|
+
const params = message.params ?? {};
|
|
1592
|
+
const threadId = typeof params.threadId === "string" ? params.threadId : null;
|
|
1593
|
+
const chatId = threadId ? this.#findChatIdByThread(threadId) : null;
|
|
1594
|
+
if (chatId === null) {
|
|
1595
|
+
await this.#codex.respond(message.id, autoDeclineResult(method));
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
if (method === "item/commandExecution/requestApproval" ||
|
|
1599
|
+
method === "item/fileChange/requestApproval" ||
|
|
1600
|
+
method === "item/permissions/requestApproval") {
|
|
1601
|
+
const token = randomBytes(4).toString("hex");
|
|
1602
|
+
this.#pendingApprovals.set(token, {
|
|
1603
|
+
requestId: message.id,
|
|
1604
|
+
chatId,
|
|
1605
|
+
method,
|
|
1606
|
+
params,
|
|
1607
|
+
});
|
|
1608
|
+
await this.#sendApprovalPrompt(chatId, token, method, params);
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
if (method === "item/tool/requestUserInput") {
|
|
1612
|
+
await this.#startToolInteractiveSession(chatId, message.id, params);
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
if (method === "mcpServer/elicitation/request") {
|
|
1616
|
+
await this.#startMcpInteractiveSession(chatId, message.id, params);
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
await this.#codex.respond(message.id, autoDeclineResult(method));
|
|
1620
|
+
}
|
|
1621
|
+
async #handleNotification(method, params) {
|
|
1622
|
+
const threadId = typeof params.threadId === "string" ? params.threadId : null;
|
|
1623
|
+
if (method === "item/started") {
|
|
1624
|
+
const item = params.item;
|
|
1625
|
+
if (item && typeof item.id === "string") {
|
|
1626
|
+
this.#items.set(item.id, item);
|
|
1627
|
+
}
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
if (method === "turn/started" && threadId) {
|
|
1631
|
+
const chatId = this.#findChatIdByThread(threadId);
|
|
1632
|
+
const turn = params.turn;
|
|
1633
|
+
const turnId = typeof turn?.id === "string" ? turn.id : null;
|
|
1634
|
+
if (chatId !== null && turnId) {
|
|
1635
|
+
this.#chatState(chatId).activeTurnId = turnId;
|
|
1636
|
+
this.#chatState(chatId).freshThread = false;
|
|
1637
|
+
await this.#saveState();
|
|
1638
|
+
this.#startTypingIndicator(chatId);
|
|
1639
|
+
}
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
if (method === "item/agentMessage/delta" && threadId) {
|
|
1643
|
+
const turnId = asString(params.turnId);
|
|
1644
|
+
const itemId = asString(params.itemId);
|
|
1645
|
+
const chatId = this.#findChatIdByThread(threadId);
|
|
1646
|
+
if (!turnId || !itemId || chatId === null) {
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
const stream = this.#getOrCreateAgentStream(threadId, turnId, chatId, itemId);
|
|
1650
|
+
stream.text += asString(params.delta) ?? "";
|
|
1651
|
+
await this.#flushStream(stream, false);
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
if (method === "item/completed" && threadId) {
|
|
1655
|
+
const chatId = this.#findChatIdByThread(threadId);
|
|
1656
|
+
const turnId = asString(params.turnId);
|
|
1657
|
+
const item = params.item;
|
|
1658
|
+
const itemType = asString(item?.type);
|
|
1659
|
+
if (chatId === null || !turnId || !item || !itemType) {
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1662
|
+
if (itemType === "agentMessage") {
|
|
1663
|
+
const itemId = asString(item.id);
|
|
1664
|
+
if (itemId) {
|
|
1665
|
+
this.#items.set(itemId, item);
|
|
1666
|
+
}
|
|
1667
|
+
const stream = itemId !== null ? this.#getOrCreateAgentStream(threadId, turnId, chatId, itemId) : null;
|
|
1668
|
+
if (stream) {
|
|
1669
|
+
const itemText = asString(item.text);
|
|
1670
|
+
if (itemText) {
|
|
1671
|
+
stream.text = itemText;
|
|
1672
|
+
}
|
|
1673
|
+
await this.#flushStream(stream, true);
|
|
1674
|
+
}
|
|
1675
|
+
const messageText = asString(item.text) ?? stream?.text ?? null;
|
|
1676
|
+
const phase = asString(item.phase) ?? stream?.phase;
|
|
1677
|
+
if (messageText && phase !== "commentary") {
|
|
1678
|
+
this.#chatState(chatId).lastAssistantMessage = messageText;
|
|
1679
|
+
await this.#saveState();
|
|
1680
|
+
}
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
if (itemType === "commandExecution") {
|
|
1684
|
+
await this.#sendHtmlText(chatId, formatCommandCompletionHtml(item, this.#chatState(chatId).verbose));
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
if (itemType === "fileChange") {
|
|
1688
|
+
await this.#sendHtmlText(chatId, formatFileChangeCompletionHtml(item, this.#chatState(chatId).verbose));
|
|
1689
|
+
}
|
|
1690
|
+
return;
|
|
1691
|
+
}
|
|
1692
|
+
if (method === "turn/completed" && threadId) {
|
|
1693
|
+
const chatId = this.#findChatIdByThread(threadId);
|
|
1694
|
+
const turn = params.turn;
|
|
1695
|
+
const turnId = asString(turn?.id);
|
|
1696
|
+
if (chatId === null || !turn || !turnId) {
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
const state = this.#chatState(chatId);
|
|
1700
|
+
if (state.activeTurnId === turnId) {
|
|
1701
|
+
state.activeTurnId = null;
|
|
1702
|
+
await this.#saveState();
|
|
1703
|
+
}
|
|
1704
|
+
if (!state.queuedTurnInput && state.pendingTurnInput) {
|
|
1705
|
+
state.queuedTurnInput = state.pendingTurnInput;
|
|
1706
|
+
}
|
|
1707
|
+
state.pendingTurnInput = null;
|
|
1708
|
+
const streams = this.#streamsForTurn(threadId, turnId);
|
|
1709
|
+
let lastNonCommentaryMessage = null;
|
|
1710
|
+
for (const stream of streams) {
|
|
1711
|
+
await this.#flushStream(stream, true);
|
|
1712
|
+
if (stream.text && stream.phase !== "commentary") {
|
|
1713
|
+
lastNonCommentaryMessage = stream.text;
|
|
1714
|
+
}
|
|
1715
|
+
this.#streams.delete(stream.streamId);
|
|
1716
|
+
}
|
|
1717
|
+
if (lastNonCommentaryMessage) {
|
|
1718
|
+
state.lastAssistantMessage = lastNonCommentaryMessage;
|
|
1719
|
+
await this.#saveState();
|
|
1720
|
+
}
|
|
1721
|
+
this.#stopTypingIndicator(chatId);
|
|
1722
|
+
await this.#clearTurnControls(chatId, turnId);
|
|
1723
|
+
const queuedTurnInput = state.queuedTurnInput;
|
|
1724
|
+
state.queuedTurnInput = null;
|
|
1725
|
+
const status = asString(turn.status) ?? "unknown";
|
|
1726
|
+
const error = turn.error?.message;
|
|
1727
|
+
const completionHtml = formatTurnCompletionHtml(status, typeof error === "string" ? error : null);
|
|
1728
|
+
if (completionHtml) {
|
|
1729
|
+
await this.#sendHtmlText(chatId, completionHtml);
|
|
1730
|
+
}
|
|
1731
|
+
if (queuedTurnInput) {
|
|
1732
|
+
await this.#sendHtmlText(chatId, formatPendingInputActionHtml("starting", queuedTurnInput));
|
|
1733
|
+
await this.#startTurn(chatId, queuedTurnInput);
|
|
1734
|
+
}
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
if (method === "error" && threadId) {
|
|
1738
|
+
const chatId = this.#findChatIdByThread(threadId);
|
|
1739
|
+
if (chatId === null) {
|
|
1740
|
+
return;
|
|
1741
|
+
}
|
|
1742
|
+
this.#stopTypingIndicator(chatId);
|
|
1743
|
+
const error = params.error?.message;
|
|
1744
|
+
await this.#sendText(chatId, `❌ Error: ${typeof error === "string" ? error : "unknown error"}`);
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
async #maybePairOwner(userId, chatId, text) {
|
|
1748
|
+
const slashCommand = parseTelegramSlashCommand(text);
|
|
1749
|
+
if (slashCommand?.name !== "start") {
|
|
1750
|
+
await this.#sendText(chatId, "Send /start to pair this Telegram account with Codex Anywhere.");
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
this.#config.ownerUserId = userId;
|
|
1754
|
+
await saveConfig(this.#configPath, this.#config);
|
|
1755
|
+
await this.#sendText(chatId, [
|
|
1756
|
+
"✅ Codex Anywhere paired.",
|
|
1757
|
+
"You can now send a task like `fix tests`.",
|
|
1758
|
+
"Use /help to see commands, /resume to browse sessions, or send a screenshot with a caption.",
|
|
1759
|
+
].join("\n"));
|
|
1760
|
+
}
|
|
1761
|
+
async #startNewThread(chatId, silent = false) {
|
|
1762
|
+
const result = await this.#codex.call("thread/start", threadSessionOverrides(this.#config, this.#chatState(chatId)));
|
|
1763
|
+
const thread = result.thread;
|
|
1764
|
+
const threadId = asString(thread?.id);
|
|
1765
|
+
if (!threadId) {
|
|
1766
|
+
throw new Error("Codex did not return a thread id.");
|
|
1767
|
+
}
|
|
1768
|
+
const state = this.#chatState(chatId);
|
|
1769
|
+
state.threadId = threadId;
|
|
1770
|
+
state.freshThread = true;
|
|
1771
|
+
state.activeTurnId = null;
|
|
1772
|
+
await this.#saveState();
|
|
1773
|
+
if (!silent) {
|
|
1774
|
+
await this.#sendText(chatId, `🆕 Started a fresh Codex thread.\nThread: ${threadId}`);
|
|
1775
|
+
}
|
|
1776
|
+
return threadId;
|
|
1777
|
+
}
|
|
1778
|
+
async #resumeThread(chatId, silent = false) {
|
|
1779
|
+
const state = this.#chatState(chatId);
|
|
1780
|
+
if (!state.threadId) {
|
|
1781
|
+
return await this.#startNewThread(chatId, silent);
|
|
1782
|
+
}
|
|
1783
|
+
if (state.freshThread) {
|
|
1784
|
+
return state.threadId;
|
|
1785
|
+
}
|
|
1786
|
+
try {
|
|
1787
|
+
await this.#codex.call("thread/resume", {
|
|
1788
|
+
threadId: state.threadId,
|
|
1789
|
+
...threadSessionOverrides(this.#config, state),
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1792
|
+
catch (error) {
|
|
1793
|
+
if (!isMissingRolloutResumeError(error)) {
|
|
1794
|
+
throw error;
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
if (!silent) {
|
|
1798
|
+
await this.#sendText(chatId, `🔄 Resumed thread ${state.threadId}`);
|
|
1799
|
+
}
|
|
1800
|
+
return state.threadId;
|
|
1801
|
+
}
|
|
1802
|
+
async #interruptTurn(chatId) {
|
|
1803
|
+
const state = this.#chatState(chatId);
|
|
1804
|
+
if (state.activeTurnId) {
|
|
1805
|
+
await this.#reconcileActiveTurnState(chatId);
|
|
1806
|
+
}
|
|
1807
|
+
if (!state.threadId || !state.activeTurnId) {
|
|
1808
|
+
await this.#sendText(chatId, "No active turn to interrupt.");
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
await this.#codex.call("turn/interrupt", {
|
|
1812
|
+
threadId: state.threadId,
|
|
1813
|
+
turnId: state.activeTurnId,
|
|
1814
|
+
});
|
|
1815
|
+
await this.#sendText(chatId, `⏹️ Interrupt requested for ${state.activeTurnId}`);
|
|
1816
|
+
}
|
|
1817
|
+
async #runShellCommand(chatId, command) {
|
|
1818
|
+
if (!command) {
|
|
1819
|
+
await this.#sendText(chatId, "Usage: /sh <command>");
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
const token = randomBytes(4).toString("hex");
|
|
1823
|
+
this.#pendingShellCommands.set(token, { chatId, command });
|
|
1824
|
+
const replyMarkup = {
|
|
1825
|
+
inline_keyboard: [
|
|
1826
|
+
[
|
|
1827
|
+
{
|
|
1828
|
+
text: "Run shell command",
|
|
1829
|
+
callback_data: formatShellCallbackData(token, "run"),
|
|
1830
|
+
},
|
|
1831
|
+
{
|
|
1832
|
+
text: "Cancel",
|
|
1833
|
+
callback_data: formatShellCallbackData(token, "cancel"),
|
|
1834
|
+
},
|
|
1835
|
+
],
|
|
1836
|
+
],
|
|
1837
|
+
};
|
|
1838
|
+
await this.#sendText(chatId, [
|
|
1839
|
+
"💻 Confirm explicit shell command.",
|
|
1840
|
+
`Command: ${command}`,
|
|
1841
|
+
"This adds an extra confirmation step before Codex runs it.",
|
|
1842
|
+
].join("\n"), replyMarkup);
|
|
1843
|
+
}
|
|
1844
|
+
async #sendTask(chatId, text, extraParams) {
|
|
1845
|
+
await this.#startTurn(chatId, [{ type: "text", text }], extraParams);
|
|
1846
|
+
}
|
|
1847
|
+
async #startTurn(chatId, input, extraParams) {
|
|
1848
|
+
const threadId = await this.#resumeThread(chatId, true);
|
|
1849
|
+
const params = {
|
|
1850
|
+
threadId,
|
|
1851
|
+
input,
|
|
1852
|
+
...turnSessionOverrides(this.#config, this.#chatState(chatId)),
|
|
1853
|
+
...(extraParams ?? {}),
|
|
1854
|
+
};
|
|
1855
|
+
const result = await this.#codex.call("turn/start", params);
|
|
1856
|
+
const turn = result.turn;
|
|
1857
|
+
const turnId = asString(turn?.id);
|
|
1858
|
+
if (!turnId) {
|
|
1859
|
+
throw new Error("Codex did not return a turn id.");
|
|
1860
|
+
}
|
|
1861
|
+
const state = this.#chatState(chatId);
|
|
1862
|
+
state.activeTurnId = turnId;
|
|
1863
|
+
state.freshThread = false;
|
|
1864
|
+
await this.#saveState();
|
|
1865
|
+
}
|
|
1866
|
+
async #sendStatus(chatId) {
|
|
1867
|
+
await this.#reconcileActiveTurnState(chatId);
|
|
1868
|
+
const state = this.#chatState(chatId);
|
|
1869
|
+
let rateLimits = "unavailable";
|
|
1870
|
+
try {
|
|
1871
|
+
const response = await this.#codex.call("account/rateLimits/read");
|
|
1872
|
+
const snapshot = response.rateLimits;
|
|
1873
|
+
rateLimits = formatRateLimitSnapshot(snapshot);
|
|
1874
|
+
}
|
|
1875
|
+
catch (error) {
|
|
1876
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1877
|
+
rateLimits = `error: ${message}`;
|
|
1878
|
+
}
|
|
1879
|
+
const lines = [
|
|
1880
|
+
`workspace: ${this.#config.workspaceCwd}`,
|
|
1881
|
+
`owner: ${this.#config.ownerUserId ?? "unpaired"}`,
|
|
1882
|
+
`thread: ${state.threadId ?? "none"}`,
|
|
1883
|
+
`active turn: ${state.activeTurnId ?? "none"}`,
|
|
1884
|
+
`model override: ${state.model ?? "(default)"}`,
|
|
1885
|
+
`reasoning effort: ${state.reasoningEffort ?? "(default)"}`,
|
|
1886
|
+
`personality: ${state.personality ?? "(default)"}`,
|
|
1887
|
+
`collaboration mode: ${state.collaborationModeName ?? "(default)"}`,
|
|
1888
|
+
`fast mode: ${state.serviceTier === "fast" ? "on" : "off"}`,
|
|
1889
|
+
`verbose tool/file messages: ${state.verbose ? "on" : "off"}`,
|
|
1890
|
+
`approval policy: ${state.approvalPolicy ?? "on-request"}`,
|
|
1891
|
+
`rate limits: ${rateLimits}`,
|
|
1892
|
+
`config: ${this.#configPath}`,
|
|
1893
|
+
];
|
|
1894
|
+
await this.#sendText(chatId, lines.join("\n"));
|
|
1895
|
+
}
|
|
1896
|
+
async #sendApprovalPrompt(chatId, token, method, params) {
|
|
1897
|
+
this.#stopTypingIndicator(chatId);
|
|
1898
|
+
const text = formatApprovalPromptHtml(method, params, this.#items);
|
|
1899
|
+
const replyMarkup = {
|
|
1900
|
+
inline_keyboard: [
|
|
1901
|
+
[
|
|
1902
|
+
{ text: "Approve", callback_data: formatApprovalCallbackData(token, "approve") },
|
|
1903
|
+
{ text: "Approve session", callback_data: formatApprovalCallbackData(token, "session") },
|
|
1904
|
+
],
|
|
1905
|
+
[
|
|
1906
|
+
{ text: "Decline", callback_data: formatApprovalCallbackData(token, "decline") },
|
|
1907
|
+
{ text: "Cancel", callback_data: formatApprovalCallbackData(token, "cancel") },
|
|
1908
|
+
],
|
|
1909
|
+
],
|
|
1910
|
+
};
|
|
1911
|
+
await this.#sendHtmlText(chatId, text, replyMarkup);
|
|
1912
|
+
}
|
|
1913
|
+
async #resolveApproval(approval, action) {
|
|
1914
|
+
if (approval.method === "item/permissions/requestApproval") {
|
|
1915
|
+
const result = action === "decline" || action === "cancel"
|
|
1916
|
+
? { permissions: {}, scope: "turn" }
|
|
1917
|
+
: {
|
|
1918
|
+
permissions: approval.params.permissions ?? {},
|
|
1919
|
+
scope: action === "session" ? "session" : "turn",
|
|
1920
|
+
};
|
|
1921
|
+
await this.#codex.respond(approval.requestId, result);
|
|
1922
|
+
await this.#sendText(approval.chatId, `Permission decision sent: ${action}`);
|
|
1923
|
+
return;
|
|
1924
|
+
}
|
|
1925
|
+
const decision = action === "approve"
|
|
1926
|
+
? "accept"
|
|
1927
|
+
: action === "session"
|
|
1928
|
+
? "acceptForSession"
|
|
1929
|
+
: action === "decline"
|
|
1930
|
+
? "decline"
|
|
1931
|
+
: "cancel";
|
|
1932
|
+
await this.#codex.respond(approval.requestId, { decision });
|
|
1933
|
+
await this.#sendText(approval.chatId, `Approval decision sent: ${decision}`);
|
|
1934
|
+
}
|
|
1935
|
+
async #flushStream(stream, force) {
|
|
1936
|
+
const now = Date.now();
|
|
1937
|
+
if (!force && now - stream.lastFlushAt < this.#config.streamEditIntervalMs) {
|
|
1938
|
+
return;
|
|
1939
|
+
}
|
|
1940
|
+
const raw = stream.text || "🤖 Working on it…";
|
|
1941
|
+
const chunks = splitTelegramChunks(raw);
|
|
1942
|
+
const needsChunking = force && chunks.length > 1;
|
|
1943
|
+
// During streaming: show clamped tail in one message.
|
|
1944
|
+
// On final flush with overflow: render all chunks as separate messages.
|
|
1945
|
+
if (needsChunking) {
|
|
1946
|
+
// Edit the existing streaming message with the first chunk.
|
|
1947
|
+
const firstHtml = renderAssistantTextHtml(chunks[0]);
|
|
1948
|
+
if (stream.messageId === null) {
|
|
1949
|
+
const message = await this.#telegram.sendMessage(stream.chatId, firstHtml, undefined, "HTML");
|
|
1950
|
+
stream.messageId = message.message_id;
|
|
1951
|
+
}
|
|
1952
|
+
else {
|
|
1953
|
+
await this.#telegram.editMessageText(stream.chatId, stream.messageId, firstHtml, undefined, "HTML");
|
|
1954
|
+
}
|
|
1955
|
+
// Send remaining chunks as new messages.
|
|
1956
|
+
for (let i = 1; i < chunks.length; i++) {
|
|
1957
|
+
const html = renderAssistantTextHtml(chunks[i]);
|
|
1958
|
+
await this.#telegram.sendMessage(stream.chatId, html, undefined, "HTML");
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
else {
|
|
1962
|
+
const html = renderAssistantTextHtml(clampTelegramText(raw));
|
|
1963
|
+
if (stream.messageId === null) {
|
|
1964
|
+
const message = await this.#telegram.sendMessage(stream.chatId, html, undefined, "HTML");
|
|
1965
|
+
stream.messageId = message.message_id;
|
|
1966
|
+
}
|
|
1967
|
+
else {
|
|
1968
|
+
await this.#telegram.editMessageText(stream.chatId, stream.messageId, html, undefined, "HTML");
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
stream.lastFlushAt = now;
|
|
1972
|
+
}
|
|
1973
|
+
async #sendText(chatId, text, replyMarkup) {
|
|
1974
|
+
const chunks = splitTelegramChunks(text);
|
|
1975
|
+
let last;
|
|
1976
|
+
for (const [i, chunk] of chunks.entries()) {
|
|
1977
|
+
last = await this.#telegram.sendMessage(chatId, chunk, i === chunks.length - 1 ? replyMarkup : undefined);
|
|
1978
|
+
}
|
|
1979
|
+
return last;
|
|
1980
|
+
}
|
|
1981
|
+
async #sendHtmlText(chatId, text, replyMarkup) {
|
|
1982
|
+
const chunks = splitTelegramChunks(text);
|
|
1983
|
+
let last;
|
|
1984
|
+
for (const [i, chunk] of chunks.entries()) {
|
|
1985
|
+
last = await this.#telegram.sendMessage(chatId, chunk, i === chunks.length - 1 ? replyMarkup : undefined, "HTML");
|
|
1986
|
+
}
|
|
1987
|
+
return last;
|
|
1988
|
+
}
|
|
1989
|
+
async #clearTurnControls(chatId, turnId) {
|
|
1990
|
+
const state = this.#chatState(chatId);
|
|
1991
|
+
if (state.turnControlTurnId !== turnId || state.turnControlMessageId === null) {
|
|
1992
|
+
return;
|
|
1993
|
+
}
|
|
1994
|
+
const messageId = state.turnControlMessageId;
|
|
1995
|
+
state.turnControlTurnId = null;
|
|
1996
|
+
state.turnControlMessageId = null;
|
|
1997
|
+
await this.#saveState();
|
|
1998
|
+
try {
|
|
1999
|
+
await this.#telegram.deleteMessage(chatId, messageId);
|
|
2000
|
+
}
|
|
2001
|
+
catch (error) {
|
|
2002
|
+
this.#logRuntimeError("deleteMessage", error);
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
#startTypingIndicator(chatId) {
|
|
2006
|
+
this.#stopTypingIndicator(chatId);
|
|
2007
|
+
const tick = () => {
|
|
2008
|
+
void this.#telegram.sendChatAction(chatId, "typing").catch((error) => {
|
|
2009
|
+
this.#logRuntimeError("sendChatAction", error);
|
|
2010
|
+
});
|
|
2011
|
+
};
|
|
2012
|
+
tick();
|
|
2013
|
+
const interval = setInterval(tick, 4000);
|
|
2014
|
+
interval.unref?.();
|
|
2015
|
+
this.#typingIntervals.set(chatId, interval);
|
|
2016
|
+
}
|
|
2017
|
+
#stopTypingIndicator(chatId) {
|
|
2018
|
+
const interval = this.#typingIntervals.get(chatId);
|
|
2019
|
+
if (!interval) {
|
|
2020
|
+
return;
|
|
2021
|
+
}
|
|
2022
|
+
clearInterval(interval);
|
|
2023
|
+
this.#typingIntervals.delete(chatId);
|
|
2024
|
+
}
|
|
2025
|
+
#getOrCreateAgentStream(threadId, turnId, chatId, itemId) {
|
|
2026
|
+
const item = this.#items.get(itemId);
|
|
2027
|
+
const phase = asString(item?.phase);
|
|
2028
|
+
const streamId = agentStreamKey(threadId, turnId, streamGroupId(itemId, phase));
|
|
2029
|
+
const existing = this.#streams.get(streamId);
|
|
2030
|
+
if (existing) {
|
|
2031
|
+
return existing;
|
|
2032
|
+
}
|
|
2033
|
+
const stream = {
|
|
2034
|
+
threadId,
|
|
2035
|
+
turnId,
|
|
2036
|
+
streamId,
|
|
2037
|
+
chatId,
|
|
2038
|
+
text: "",
|
|
2039
|
+
messageId: null,
|
|
2040
|
+
lastFlushAt: 0,
|
|
2041
|
+
phase,
|
|
2042
|
+
};
|
|
2043
|
+
this.#streams.set(streamId, stream);
|
|
2044
|
+
return stream;
|
|
2045
|
+
}
|
|
2046
|
+
#streamsForTurn(threadId, turnId) {
|
|
2047
|
+
const prefix = `${threadId}:${turnId}:`;
|
|
2048
|
+
return [...this.#streams.values()].filter((stream) => stream.streamId.startsWith(prefix));
|
|
2049
|
+
}
|
|
2050
|
+
async #executeShellCommand(chatId, command) {
|
|
2051
|
+
const threadId = await this.#resumeThread(chatId, true);
|
|
2052
|
+
await this.#codex.call("thread/shellCommand", {
|
|
2053
|
+
threadId,
|
|
2054
|
+
command,
|
|
2055
|
+
});
|
|
2056
|
+
await this.#sendText(chatId, [
|
|
2057
|
+
"💻 Submitted shell command.",
|
|
2058
|
+
`Command: ${command}`,
|
|
2059
|
+
"Note: /sh uses Codex's explicit shell-command flow.",
|
|
2060
|
+
].join("\n"));
|
|
2061
|
+
}
|
|
2062
|
+
async #applyLocalModelSelection(chatId, answers) {
|
|
2063
|
+
const state = this.#chatState(chatId);
|
|
2064
|
+
const model = asString(answers.model);
|
|
2065
|
+
if (!model) {
|
|
2066
|
+
await this.#sendText(chatId, "Model selection was incomplete.");
|
|
2067
|
+
return;
|
|
2068
|
+
}
|
|
2069
|
+
if (model === "__reset__") {
|
|
2070
|
+
state.model = null;
|
|
2071
|
+
state.reasoningEffort = null;
|
|
2072
|
+
await this.#saveState();
|
|
2073
|
+
await this.#sendText(chatId, "Model override cleared.");
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
2076
|
+
const effortValue = asString(answers.reasoningEffort);
|
|
2077
|
+
const effort = effortValue && effortValue !== "__default__"
|
|
2078
|
+
? normalizeReasoningEffort(effortValue)
|
|
2079
|
+
: null;
|
|
2080
|
+
state.model = model;
|
|
2081
|
+
state.reasoningEffort = effort;
|
|
2082
|
+
await this.#saveState();
|
|
2083
|
+
await this.#sendText(chatId, `Model override set to ${model}${effort ? ` (${effort})` : ""}.`);
|
|
2084
|
+
}
|
|
2085
|
+
async #applyLocalPersonalitySelection(chatId, answers) {
|
|
2086
|
+
const value = asString(answers.personality);
|
|
2087
|
+
if (!value) {
|
|
2088
|
+
await this.#sendText(chatId, "Personality selection was incomplete.");
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2091
|
+
const state = this.#chatState(chatId);
|
|
2092
|
+
state.personality = value === "__default__" ? null : value;
|
|
2093
|
+
await this.#saveState();
|
|
2094
|
+
await this.#sendText(chatId, `Personality set to ${state.personality ?? "default"}.`);
|
|
2095
|
+
}
|
|
2096
|
+
async #applyLocalFastSelection(chatId, answers) {
|
|
2097
|
+
const state = this.#chatState(chatId);
|
|
2098
|
+
const serviceTier = asString(answers.serviceTier);
|
|
2099
|
+
if (!serviceTier) {
|
|
2100
|
+
await this.#sendText(chatId, "Fast mode selection was incomplete.");
|
|
2101
|
+
return;
|
|
2102
|
+
}
|
|
2103
|
+
state.serviceTier = serviceTier === "fast" ? "fast" : null;
|
|
2104
|
+
await this.#saveState();
|
|
2105
|
+
await this.#sendText(chatId, `Fast mode ${state.serviceTier === "fast" ? "enabled" : "disabled"}.`);
|
|
2106
|
+
}
|
|
2107
|
+
async #applyLocalApprovalPolicySelection(chatId, answers) {
|
|
2108
|
+
const policy = asString(answers.approvalPolicy);
|
|
2109
|
+
if (!policy) {
|
|
2110
|
+
await this.#sendText(chatId, "Approval policy selection was incomplete.");
|
|
2111
|
+
return;
|
|
2112
|
+
}
|
|
2113
|
+
this.#chatState(chatId).approvalPolicy = policy;
|
|
2114
|
+
await this.#saveState();
|
|
2115
|
+
await this.#sendText(chatId, `Approval policy set to ${policy}.`);
|
|
2116
|
+
}
|
|
2117
|
+
async #applyLocalCollaborationSelection(chatId, answers) {
|
|
2118
|
+
const modeName = asString(answers.collaborationModeName);
|
|
2119
|
+
if (!modeName) {
|
|
2120
|
+
await this.#sendText(chatId, "Collaboration mode selection was incomplete.");
|
|
2121
|
+
return;
|
|
2122
|
+
}
|
|
2123
|
+
const state = this.#chatState(chatId);
|
|
2124
|
+
if (modeName === "__reset__") {
|
|
2125
|
+
state.collaborationMode = null;
|
|
2126
|
+
state.collaborationModeName = null;
|
|
2127
|
+
await this.#saveState();
|
|
2128
|
+
await this.#sendText(chatId, "Collaboration mode reset to default.");
|
|
2129
|
+
return;
|
|
2130
|
+
}
|
|
2131
|
+
const collaborationMode = await this.#resolveNamedCollaborationMode(modeName);
|
|
2132
|
+
if (!collaborationMode) {
|
|
2133
|
+
await this.#sendText(chatId, `Unknown collaboration mode: ${modeName}`);
|
|
2134
|
+
return;
|
|
2135
|
+
}
|
|
2136
|
+
state.collaborationMode = collaborationMode;
|
|
2137
|
+
state.collaborationModeName = modeName;
|
|
2138
|
+
await this.#saveState();
|
|
2139
|
+
await this.#sendText(chatId, `Collaboration mode set to ${modeName}.`);
|
|
2140
|
+
}
|
|
2141
|
+
async #applyLocalExperimentalSelection(chatId, answers) {
|
|
2142
|
+
const featureName = asString(answers.featureName);
|
|
2143
|
+
const enabledValue = asString(answers.enabled);
|
|
2144
|
+
if (!featureName || !enabledValue) {
|
|
2145
|
+
await this.#sendText(chatId, "Experimental feature selection was incomplete.");
|
|
2146
|
+
return;
|
|
2147
|
+
}
|
|
2148
|
+
const enabled = enabledValue === "on";
|
|
2149
|
+
await this.#codex.call("experimentalFeature/enablement/set", {
|
|
2150
|
+
enablement: { [featureName]: enabled },
|
|
2151
|
+
});
|
|
2152
|
+
await this.#sendText(chatId, `Experimental feature ${featureName} set to ${enabled ? "on" : "off"}.`);
|
|
2153
|
+
}
|
|
2154
|
+
async #applyLocalFeedbackSelection(chatId, answers) {
|
|
2155
|
+
const classification = asString(answers.classification);
|
|
2156
|
+
const includeLogs = answers.includeLogs === true;
|
|
2157
|
+
if (!classification) {
|
|
2158
|
+
await this.#sendText(chatId, "Feedback selection was incomplete.");
|
|
2159
|
+
return;
|
|
2160
|
+
}
|
|
2161
|
+
const state = this.#chatState(chatId);
|
|
2162
|
+
const params = {
|
|
2163
|
+
classification,
|
|
2164
|
+
reason: null,
|
|
2165
|
+
threadId: state.threadId,
|
|
2166
|
+
includeLogs,
|
|
2167
|
+
extraLogFiles: null,
|
|
2168
|
+
};
|
|
2169
|
+
const result = await this.#codex.call("feedback/upload", params);
|
|
2170
|
+
const threadId = asString(result.threadId) ?? "<unknown>";
|
|
2171
|
+
await this.#sendText(chatId, `Feedback submitted.\nThread ID: ${threadId}`);
|
|
2172
|
+
}
|
|
2173
|
+
async #applyLocalAgentSelection(chatId, answers) {
|
|
2174
|
+
const threadId = asString(answers.threadId);
|
|
2175
|
+
if (!threadId) {
|
|
2176
|
+
await this.#sendText(chatId, "Agent selection was incomplete.");
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
const state = this.#chatState(chatId);
|
|
2180
|
+
state.threadId = threadId;
|
|
2181
|
+
state.freshThread = false;
|
|
2182
|
+
state.activeTurnId = null;
|
|
2183
|
+
await this.#saveState();
|
|
2184
|
+
await this.#codex.call("thread/resume", {
|
|
2185
|
+
threadId,
|
|
2186
|
+
...threadSessionOverrides(this.#config, state),
|
|
2187
|
+
});
|
|
2188
|
+
await this.#sendText(chatId, `Active agent thread set to ${threadId}.`);
|
|
2189
|
+
}
|
|
2190
|
+
async #applyLocalMentionSelection(chatId, answers) {
|
|
2191
|
+
const mentionPath = asString(answers.mentionPath);
|
|
2192
|
+
if (!mentionPath) {
|
|
2193
|
+
await this.#sendText(chatId, "Mention selection was incomplete.");
|
|
2194
|
+
return;
|
|
2195
|
+
}
|
|
2196
|
+
const state = this.#chatState(chatId);
|
|
2197
|
+
state.pendingMention = {
|
|
2198
|
+
name: path.basename(mentionPath),
|
|
2199
|
+
path: mentionPath,
|
|
2200
|
+
};
|
|
2201
|
+
await this.#saveState();
|
|
2202
|
+
await this.#sendText(chatId, `File mention ready: ${mentionPath}\nSend your next message and it will include this file.`);
|
|
2203
|
+
}
|
|
2204
|
+
async #applyLocalVerboseSelection(chatId, answers) {
|
|
2205
|
+
const value = asString(answers.verboseMode);
|
|
2206
|
+
if (!value) {
|
|
2207
|
+
await this.#sendText(chatId, "Verbose selection was incomplete.");
|
|
2208
|
+
return;
|
|
2209
|
+
}
|
|
2210
|
+
const state = this.#chatState(chatId);
|
|
2211
|
+
state.verbose = value === "on";
|
|
2212
|
+
await this.#saveState();
|
|
2213
|
+
await this.#sendText(chatId, `Detailed tool/file messages ${state.verbose ? "enabled" : "disabled"}.`);
|
|
2214
|
+
}
|
|
2215
|
+
async #applyLocalRenameSelection(chatId, answers) {
|
|
2216
|
+
const name = asString(answers.threadName)?.trim();
|
|
2217
|
+
if (!name) {
|
|
2218
|
+
await this.#sendText(chatId, "Thread name was incomplete.");
|
|
2219
|
+
return;
|
|
2220
|
+
}
|
|
2221
|
+
await this.#renameThread(chatId, name);
|
|
2222
|
+
}
|
|
2223
|
+
async #applyLocalReviewSelection(chatId, answers) {
|
|
2224
|
+
const targetKind = asString(answers.targetKind);
|
|
2225
|
+
if (!targetKind) {
|
|
2226
|
+
await this.#sendText(chatId, "Review target selection was incomplete.");
|
|
2227
|
+
return;
|
|
2228
|
+
}
|
|
2229
|
+
if (targetKind === "uncommittedChanges") {
|
|
2230
|
+
await this.#runReview(chatId, { type: "uncommittedChanges" });
|
|
2231
|
+
return;
|
|
2232
|
+
}
|
|
2233
|
+
const targetValue = asString(answers.targetValue)?.trim();
|
|
2234
|
+
if (!targetValue) {
|
|
2235
|
+
await this.#sendText(chatId, "Review target details were incomplete.");
|
|
2236
|
+
return;
|
|
2237
|
+
}
|
|
2238
|
+
switch (targetKind) {
|
|
2239
|
+
case "baseBranch":
|
|
2240
|
+
await this.#runReview(chatId, { type: "baseBranch", branch: targetValue });
|
|
2241
|
+
return;
|
|
2242
|
+
case "commit":
|
|
2243
|
+
await this.#runReview(chatId, { type: "commit", sha: targetValue, title: null });
|
|
2244
|
+
return;
|
|
2245
|
+
case "custom":
|
|
2246
|
+
await this.#runReview(chatId, { type: "custom", instructions: targetValue });
|
|
2247
|
+
return;
|
|
2248
|
+
default:
|
|
2249
|
+
await this.#sendText(chatId, "Unsupported review target.");
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
async #reconcileActiveTurnState(chatId) {
|
|
2253
|
+
const state = this.#chatState(chatId);
|
|
2254
|
+
if (!state.threadId || !state.activeTurnId) {
|
|
2255
|
+
return;
|
|
2256
|
+
}
|
|
2257
|
+
const nextActiveTurnId = await this.#readResolvedActiveTurnId(state);
|
|
2258
|
+
if (nextActiveTurnId === state.activeTurnId) {
|
|
2259
|
+
return;
|
|
2260
|
+
}
|
|
2261
|
+
state.activeTurnId = nextActiveTurnId;
|
|
2262
|
+
await this.#saveState();
|
|
2263
|
+
}
|
|
2264
|
+
async #readResolvedActiveTurnId(state) {
|
|
2265
|
+
const threadId = state.threadId;
|
|
2266
|
+
if (!threadId) {
|
|
2267
|
+
return null;
|
|
2268
|
+
}
|
|
2269
|
+
try {
|
|
2270
|
+
const response = await this.#codex.call("thread/read", {
|
|
2271
|
+
threadId,
|
|
2272
|
+
includeTurns: true,
|
|
2273
|
+
});
|
|
2274
|
+
return reconcileActiveTurnIdFromThreadRead(response.thread, state.activeTurnId);
|
|
2275
|
+
}
|
|
2276
|
+
catch (includeTurnsError) {
|
|
2277
|
+
try {
|
|
2278
|
+
const response = await this.#codex.call("thread/read", {
|
|
2279
|
+
threadId,
|
|
2280
|
+
includeTurns: false,
|
|
2281
|
+
});
|
|
2282
|
+
return reconcileActiveTurnIdFromThreadRead(response.thread, state.activeTurnId);
|
|
2283
|
+
}
|
|
2284
|
+
catch (statusOnlyError) {
|
|
2285
|
+
this.#logRuntimeError("thread/read", includeTurnsError);
|
|
2286
|
+
this.#logRuntimeError("thread/read fallback", statusOnlyError);
|
|
2287
|
+
return state.activeTurnId;
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
async #listCollaborationModes() {
|
|
2292
|
+
const response = await this.#codex.call("collaborationMode/list", {});
|
|
2293
|
+
return Array.isArray(response.data) ? response.data : [];
|
|
2294
|
+
}
|
|
2295
|
+
async #resolveNamedCollaborationMode(name) {
|
|
2296
|
+
const normalizedName = name.trim().toLowerCase();
|
|
2297
|
+
const modes = await this.#listCollaborationModes();
|
|
2298
|
+
const match = modes.find((entry) => {
|
|
2299
|
+
if (!entry || typeof entry !== "object") {
|
|
2300
|
+
return false;
|
|
2301
|
+
}
|
|
2302
|
+
return asString(entry.name)?.toLowerCase() === normalizedName;
|
|
2303
|
+
});
|
|
2304
|
+
if (!match) {
|
|
2305
|
+
return null;
|
|
2306
|
+
}
|
|
2307
|
+
return this.#buildCollaborationModeOverride(match);
|
|
2308
|
+
}
|
|
2309
|
+
async #resolvePlanCollaborationMode() {
|
|
2310
|
+
const modes = await this.#listCollaborationModes();
|
|
2311
|
+
const match = modes.find((entry) => {
|
|
2312
|
+
if (!entry || typeof entry !== "object") {
|
|
2313
|
+
return false;
|
|
2314
|
+
}
|
|
2315
|
+
const mode = entry;
|
|
2316
|
+
return asString(mode.mode) === "plan" || asString(mode.name)?.toLowerCase().includes("plan");
|
|
2317
|
+
});
|
|
2318
|
+
return match ? this.#buildCollaborationModeOverride(match) : null;
|
|
2319
|
+
}
|
|
2320
|
+
#buildCollaborationModeOverride(mask) {
|
|
2321
|
+
const model = asString(mask.model) ?? "gpt-5.4";
|
|
2322
|
+
const reasoningEffort = mask.reasoning_effort ?? mask.reasoningEffort ?? null;
|
|
2323
|
+
return {
|
|
2324
|
+
mode: asString(mask.mode) ?? "default",
|
|
2325
|
+
settings: {
|
|
2326
|
+
model,
|
|
2327
|
+
reasoning_effort: reasoningEffort,
|
|
2328
|
+
developer_instructions: null,
|
|
2329
|
+
},
|
|
2330
|
+
};
|
|
2331
|
+
}
|
|
2332
|
+
async #searchWorkspaceFiles(query) {
|
|
2333
|
+
const files = await workspaceFiles(this.#config.workspaceCwd);
|
|
2334
|
+
return bestWorkspaceFileMatches(files, query).map((file) => path.isAbsolute(file) ? file : path.join(this.#config.workspaceCwd, file));
|
|
2335
|
+
}
|
|
2336
|
+
#chatState(chatId) {
|
|
2337
|
+
const key = String(chatId);
|
|
2338
|
+
if (!this.#state.chats[key]) {
|
|
2339
|
+
this.#state.chats[key] = {
|
|
2340
|
+
threadId: null,
|
|
2341
|
+
freshThread: false,
|
|
2342
|
+
activeTurnId: null,
|
|
2343
|
+
turnControlTurnId: null,
|
|
2344
|
+
turnControlMessageId: null,
|
|
2345
|
+
verbose: false,
|
|
2346
|
+
queueNextArmed: false,
|
|
2347
|
+
queuedTurnInput: null,
|
|
2348
|
+
pendingTurnInput: null,
|
|
2349
|
+
pendingMention: null,
|
|
2350
|
+
model: null,
|
|
2351
|
+
reasoningEffort: null,
|
|
2352
|
+
personality: null,
|
|
2353
|
+
collaborationModeName: null,
|
|
2354
|
+
collaborationMode: null,
|
|
2355
|
+
serviceTier: null,
|
|
2356
|
+
approvalPolicy: null,
|
|
2357
|
+
lastAssistantMessage: null,
|
|
2358
|
+
};
|
|
2359
|
+
}
|
|
2360
|
+
return this.#state.chats[key];
|
|
2361
|
+
}
|
|
2362
|
+
#findChatIdByThread(threadId) {
|
|
2363
|
+
for (const [chatId, state] of Object.entries(this.#state.chats)) {
|
|
2364
|
+
if (state.threadId === threadId) {
|
|
2365
|
+
return Number(chatId);
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
return null;
|
|
2369
|
+
}
|
|
2370
|
+
async #saveState() {
|
|
2371
|
+
await saveState(this.#statePath, this.#state);
|
|
2372
|
+
}
|
|
2373
|
+
#printStartupHelp() {
|
|
2374
|
+
console.log(`Codex Anywhere running for workspace: ${this.#config.workspaceCwd}`);
|
|
2375
|
+
if (this.#config.ownerUserId === null) {
|
|
2376
|
+
console.log("Pairing mode: open your bot in Telegram and send /start from your account.");
|
|
2377
|
+
return;
|
|
2378
|
+
}
|
|
2379
|
+
console.log(`Paired owner user id: ${this.#config.ownerUserId}`);
|
|
2380
|
+
}
|
|
2381
|
+
#logRuntimeError(source, error) {
|
|
2382
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2383
|
+
console.error(`[codex-anywhere] ${source} error: ${message}`);
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
function clampTelegramText(text) {
|
|
2387
|
+
if (text.length <= 3900)
|
|
2388
|
+
return text;
|
|
2389
|
+
return "…\n\n" + text.slice(-(3900 - 3));
|
|
2390
|
+
}
|
|
2391
|
+
function resolveWorkspacePath(input, currentWorkspace, homeDir) {
|
|
2392
|
+
const trimmed = input.trim();
|
|
2393
|
+
if (!trimmed) {
|
|
2394
|
+
return currentWorkspace;
|
|
2395
|
+
}
|
|
2396
|
+
if (trimmed === "~") {
|
|
2397
|
+
return homeDir;
|
|
2398
|
+
}
|
|
2399
|
+
if (trimmed.startsWith("~/")) {
|
|
2400
|
+
return path.join(homeDir, trimmed.slice(2));
|
|
2401
|
+
}
|
|
2402
|
+
return path.isAbsolute(trimmed) ? path.normalize(trimmed) : path.resolve(currentWorkspace, trimmed);
|
|
2403
|
+
}
|
|
2404
|
+
function parseExplicitOmxTeamInvocation(text) {
|
|
2405
|
+
const match = /^\$team(?:\s+([\s\S]*))?$/i.exec(text.trim());
|
|
2406
|
+
if (!match) {
|
|
2407
|
+
return null;
|
|
2408
|
+
}
|
|
2409
|
+
const suffix = (match[1] ?? "").trim();
|
|
2410
|
+
return suffix ? `team ${suffix}` : "team --help";
|
|
2411
|
+
}
|
|
2412
|
+
function truncateOmxOutput(text) {
|
|
2413
|
+
const normalized = text.trim();
|
|
2414
|
+
if (!normalized) {
|
|
2415
|
+
return "(no output)";
|
|
2416
|
+
}
|
|
2417
|
+
return normalized.length > 3200 ? `${normalized.slice(0, 3200)}\n…` : normalized;
|
|
2418
|
+
}
|
|
2419
|
+
function formatWorkspaceScopeText(cwd) {
|
|
2420
|
+
const home = os.homedir();
|
|
2421
|
+
if (cwd === home) {
|
|
2422
|
+
return "~";
|
|
2423
|
+
}
|
|
2424
|
+
if (cwd.startsWith(`${home}${path.sep}`)) {
|
|
2425
|
+
return `~${cwd.slice(home.length)}`;
|
|
2426
|
+
}
|
|
2427
|
+
return cwd;
|
|
2428
|
+
}
|
|
2429
|
+
function asString(value) {
|
|
2430
|
+
return typeof value === "string" ? value : null;
|
|
2431
|
+
}
|
|
2432
|
+
function isMissingExecutableError(error, command) {
|
|
2433
|
+
if (!error || typeof error !== "object") {
|
|
2434
|
+
return false;
|
|
2435
|
+
}
|
|
2436
|
+
const code = "code" in error ? error.code : null;
|
|
2437
|
+
if (code === "ENOENT") {
|
|
2438
|
+
return true;
|
|
2439
|
+
}
|
|
2440
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2441
|
+
return message.includes(`spawn ${command} ENOENT`);
|
|
2442
|
+
}
|
|
2443
|
+
function isMissingRolloutResumeError(error) {
|
|
2444
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2445
|
+
return message.toLowerCase().includes("no rollout found for thread id");
|
|
2446
|
+
}
|
|
2447
|
+
function autoDeclineResult(method) {
|
|
2448
|
+
if (method === "item/permissions/requestApproval") {
|
|
2449
|
+
return { permissions: {}, scope: "turn" };
|
|
2450
|
+
}
|
|
2451
|
+
if (method === "item/tool/requestUserInput") {
|
|
2452
|
+
return { answers: {} };
|
|
2453
|
+
}
|
|
2454
|
+
if (method === "mcpServer/elicitation/request") {
|
|
2455
|
+
return { action: "cancel", content: null };
|
|
2456
|
+
}
|
|
2457
|
+
return { decision: "cancel" };
|
|
2458
|
+
}
|
|
2459
|
+
function threadSessionOverrides(config, state) {
|
|
2460
|
+
const params = {
|
|
2461
|
+
cwd: config.workspaceCwd,
|
|
2462
|
+
approvalPolicy: state.approvalPolicy ?? "on-request",
|
|
2463
|
+
sandbox: "workspace-write",
|
|
2464
|
+
};
|
|
2465
|
+
if (state.model) {
|
|
2466
|
+
params.model = state.model;
|
|
2467
|
+
}
|
|
2468
|
+
if (state.personality) {
|
|
2469
|
+
params.personality = state.personality;
|
|
2470
|
+
}
|
|
2471
|
+
if (state.serviceTier) {
|
|
2472
|
+
params.serviceTier = state.serviceTier;
|
|
2473
|
+
}
|
|
2474
|
+
return params;
|
|
2475
|
+
}
|
|
2476
|
+
function turnSessionOverrides(config, state) {
|
|
2477
|
+
const params = {
|
|
2478
|
+
cwd: config.workspaceCwd,
|
|
2479
|
+
approvalPolicy: state.approvalPolicy ?? "on-request",
|
|
2480
|
+
sandboxPolicy: {
|
|
2481
|
+
type: "workspaceWrite",
|
|
2482
|
+
writableRoots: [config.workspaceCwd],
|
|
2483
|
+
networkAccess: true,
|
|
2484
|
+
},
|
|
2485
|
+
};
|
|
2486
|
+
if (state.model) {
|
|
2487
|
+
params.model = state.model;
|
|
2488
|
+
}
|
|
2489
|
+
if (state.personality) {
|
|
2490
|
+
params.personality = state.personality;
|
|
2491
|
+
}
|
|
2492
|
+
if (state.collaborationMode) {
|
|
2493
|
+
params.collaborationMode = state.collaborationMode;
|
|
2494
|
+
}
|
|
2495
|
+
if (state.serviceTier) {
|
|
2496
|
+
params.serviceTier = state.serviceTier;
|
|
2497
|
+
}
|
|
2498
|
+
if (state.reasoningEffort) {
|
|
2499
|
+
params.effort = state.reasoningEffort;
|
|
2500
|
+
}
|
|
2501
|
+
return params;
|
|
2502
|
+
}
|
|
2503
|
+
function parseReviewTarget(args) {
|
|
2504
|
+
const trimmed = args.trim();
|
|
2505
|
+
if (!trimmed) {
|
|
2506
|
+
return { type: "uncommittedChanges" };
|
|
2507
|
+
}
|
|
2508
|
+
const baseMatch = /^base\s+(.+)$/i.exec(trimmed);
|
|
2509
|
+
if (baseMatch) {
|
|
2510
|
+
return { type: "baseBranch", branch: baseMatch[1].trim() };
|
|
2511
|
+
}
|
|
2512
|
+
const commitMatch = /^commit\s+([^\s]+)$/i.exec(trimmed);
|
|
2513
|
+
if (commitMatch) {
|
|
2514
|
+
return { type: "commit", sha: commitMatch[1].trim(), title: null };
|
|
2515
|
+
}
|
|
2516
|
+
return { type: "custom", instructions: trimmed };
|
|
2517
|
+
}
|
|
2518
|
+
function formatModelSummary(entry) {
|
|
2519
|
+
if (!entry || typeof entry !== "object") {
|
|
2520
|
+
return null;
|
|
2521
|
+
}
|
|
2522
|
+
const model = asString(entry.model);
|
|
2523
|
+
if (!model) {
|
|
2524
|
+
return null;
|
|
2525
|
+
}
|
|
2526
|
+
const displayName = asString(entry.displayName) ?? model;
|
|
2527
|
+
const isDefault = entry.isDefault === true ? " default" : "";
|
|
2528
|
+
return `- ${model}${isDefault}: ${displayName}`;
|
|
2529
|
+
}
|
|
2530
|
+
function formatRateLimitSnapshot(snapshot) {
|
|
2531
|
+
if (!snapshot) {
|
|
2532
|
+
return "unknown";
|
|
2533
|
+
}
|
|
2534
|
+
const primary = snapshot.primary && typeof snapshot.primary === "object"
|
|
2535
|
+
? snapshot.primary
|
|
2536
|
+
: undefined;
|
|
2537
|
+
const usedPercent = primary?.usedPercent;
|
|
2538
|
+
const resetsAt = primary?.resetsAt;
|
|
2539
|
+
const parts = [];
|
|
2540
|
+
if (typeof usedPercent === "number") {
|
|
2541
|
+
parts.push(`${Math.max(0, 100 - usedPercent)}% remaining`);
|
|
2542
|
+
}
|
|
2543
|
+
if (typeof resetsAt === "number") {
|
|
2544
|
+
parts.push(`resets at ${new Date(resetsAt * 1000).toISOString()}`);
|
|
2545
|
+
}
|
|
2546
|
+
return parts.join(", ") || "available";
|
|
2547
|
+
}
|
|
2548
|
+
function formatSessionCardHtml(thread, currentThreadId) {
|
|
2549
|
+
const threadId = asString(thread.id) ?? "<unknown>";
|
|
2550
|
+
const isCurrent = currentThreadId === threadId;
|
|
2551
|
+
const title = asString(thread.name)
|
|
2552
|
+
?? formatThreadOptionLabel(thread)
|
|
2553
|
+
?? asString(thread.preview)
|
|
2554
|
+
?? threadId;
|
|
2555
|
+
const updatedAt = typeof thread.updatedAt === "number" ? relativeTime(thread.updatedAt) : "unknown";
|
|
2556
|
+
const status = formatThreadStatus(thread.status);
|
|
2557
|
+
const source = asString(thread.source) ?? "unknown";
|
|
2558
|
+
const header = isCurrent
|
|
2559
|
+
? `● <b>${escapeTelegramHtml(title)}</b>`
|
|
2560
|
+
: `○ <b>${escapeTelegramHtml(title)}</b>`;
|
|
2561
|
+
return [
|
|
2562
|
+
header,
|
|
2563
|
+
`<code>${escapeTelegramHtml(threadId)}</code>`,
|
|
2564
|
+
`${escapeTelegramHtml(updatedAt)} · ${escapeTelegramHtml(status)} · ${escapeTelegramHtml(source)}`,
|
|
2565
|
+
].join("\n");
|
|
2566
|
+
}
|
|
2567
|
+
function formatSessionStatusHtml(thread, currentThreadId) {
|
|
2568
|
+
const threadId = asString(thread.id) ?? "<unknown>";
|
|
2569
|
+
const preview = asString(thread.preview) ?? "";
|
|
2570
|
+
const rolloutPath = asString(thread.path) ?? "(unavailable)";
|
|
2571
|
+
return [
|
|
2572
|
+
formatSessionCardHtml(thread, currentThreadId),
|
|
2573
|
+
preview ? `Preview: ${escapeTelegramHtml(preview)}` : "",
|
|
2574
|
+
`Rollout: <code>${escapeTelegramHtml(rolloutPath)}</code>`,
|
|
2575
|
+
]
|
|
2576
|
+
.filter(Boolean)
|
|
2577
|
+
.join("\n");
|
|
2578
|
+
}
|
|
2579
|
+
function formatThreadOptionLabel(thread) {
|
|
2580
|
+
const nickname = asString(thread.agentNickname);
|
|
2581
|
+
const role = asString(thread.agentRole);
|
|
2582
|
+
if (nickname && role) {
|
|
2583
|
+
return `${nickname} [${role}]`;
|
|
2584
|
+
}
|
|
2585
|
+
return nickname ?? role ?? null;
|
|
2586
|
+
}
|
|
2587
|
+
function formatThreadStatus(status) {
|
|
2588
|
+
const type = asString(status?.type);
|
|
2589
|
+
if (type !== "active") {
|
|
2590
|
+
return type ?? "unknown";
|
|
2591
|
+
}
|
|
2592
|
+
const activeFlags = Array.isArray(status?.activeFlags) ? status.activeFlags.join(", ") : "";
|
|
2593
|
+
return activeFlags ? `active (${activeFlags})` : "active";
|
|
2594
|
+
}
|
|
2595
|
+
function relativeTime(unixSeconds) {
|
|
2596
|
+
const diff = Math.max(0, Math.floor(Date.now() / 1000) - unixSeconds);
|
|
2597
|
+
if (diff < 60) {
|
|
2598
|
+
return `${diff}s ago`;
|
|
2599
|
+
}
|
|
2600
|
+
if (diff < 3600) {
|
|
2601
|
+
return `${Math.floor(diff / 60)}m ago`;
|
|
2602
|
+
}
|
|
2603
|
+
if (diff < 86400) {
|
|
2604
|
+
return `${Math.floor(diff / 3600)}h ago`;
|
|
2605
|
+
}
|
|
2606
|
+
return `${Math.floor(diff / 86400)}d ago`;
|
|
2607
|
+
}
|
|
2608
|
+
function hasTelegramImage(message) {
|
|
2609
|
+
return ((Array.isArray(message.photo) && message.photo.length > 0)
|
|
2610
|
+
|| Boolean(message.document && isImageDocument(message.document)));
|
|
2611
|
+
}
|
|
2612
|
+
function bestTelegramImageFileId(message) {
|
|
2613
|
+
if (Array.isArray(message.photo) && message.photo.length > 0) {
|
|
2614
|
+
return message.photo[message.photo.length - 1]?.file_id ?? null;
|
|
2615
|
+
}
|
|
2616
|
+
if (message.document && isImageDocument(message.document)) {
|
|
2617
|
+
return message.document.file_id;
|
|
2618
|
+
}
|
|
2619
|
+
return null;
|
|
2620
|
+
}
|
|
2621
|
+
function isImageDocument(document) {
|
|
2622
|
+
return typeof document.mime_type === "string" && document.mime_type.startsWith("image/");
|
|
2623
|
+
}
|
|
2624
|
+
function telegramFileExtension(filePath, fileName) {
|
|
2625
|
+
const extension = path.extname(filePath || fileName || "");
|
|
2626
|
+
return extension || ".jpg";
|
|
2627
|
+
}
|
|
2628
|
+
function consumePendingMention(state, input) {
|
|
2629
|
+
const pendingMention = state.pendingMention;
|
|
2630
|
+
if (!pendingMention) {
|
|
2631
|
+
return input;
|
|
2632
|
+
}
|
|
2633
|
+
state.pendingMention = null;
|
|
2634
|
+
const name = asString(pendingMention.name) ?? path.basename(asString(pendingMention.path) ?? "file");
|
|
2635
|
+
const mentionPath = asString(pendingMention.path) ?? "";
|
|
2636
|
+
const items = [...input];
|
|
2637
|
+
const firstTextIndex = items.findIndex((entry) => entry.type === "text");
|
|
2638
|
+
if (firstTextIndex >= 0) {
|
|
2639
|
+
const textItem = items[firstTextIndex];
|
|
2640
|
+
const originalText = asString(textItem.text) ?? "";
|
|
2641
|
+
items[firstTextIndex] = {
|
|
2642
|
+
...textItem,
|
|
2643
|
+
text: `@${name} ${originalText}`.trim(),
|
|
2644
|
+
};
|
|
2645
|
+
}
|
|
2646
|
+
else {
|
|
2647
|
+
items.unshift({ type: "text", text: `@${name}` });
|
|
2648
|
+
}
|
|
2649
|
+
items.push({ type: "mention", name, path: mentionPath });
|
|
2650
|
+
return items;
|
|
2651
|
+
}
|
|
2652
|
+
async function workspaceFiles(cwd) {
|
|
2653
|
+
const { stdout } = await execFileAsync("rg", ["--files"], {
|
|
2654
|
+
cwd,
|
|
2655
|
+
maxBuffer: 1024 * 1024 * 8,
|
|
2656
|
+
});
|
|
2657
|
+
return stdout
|
|
2658
|
+
.split("\n")
|
|
2659
|
+
.map((line) => line.trim())
|
|
2660
|
+
.filter(Boolean);
|
|
2661
|
+
}
|
|
2662
|
+
function bestWorkspaceFileMatches(files, query) {
|
|
2663
|
+
const normalizedQuery = query.trim().toLowerCase();
|
|
2664
|
+
if (!normalizedQuery) {
|
|
2665
|
+
return [];
|
|
2666
|
+
}
|
|
2667
|
+
const matches = files.filter((file) => file.toLowerCase().includes(normalizedQuery));
|
|
2668
|
+
matches.sort((left, right) => {
|
|
2669
|
+
const leftBase = path.basename(left).toLowerCase();
|
|
2670
|
+
const rightBase = path.basename(right).toLowerCase();
|
|
2671
|
+
const leftStarts = leftBase.startsWith(normalizedQuery) ? 0 : 1;
|
|
2672
|
+
const rightStarts = rightBase.startsWith(normalizedQuery) ? 0 : 1;
|
|
2673
|
+
if (leftStarts !== rightStarts) {
|
|
2674
|
+
return leftStarts - rightStarts;
|
|
2675
|
+
}
|
|
2676
|
+
return left.length - right.length;
|
|
2677
|
+
});
|
|
2678
|
+
return matches.slice(0, 12);
|
|
2679
|
+
}
|
|
2680
|
+
function telegramCommands() {
|
|
2681
|
+
return [
|
|
2682
|
+
{ command: "help", description: "show bot and Codex slash commands" },
|
|
2683
|
+
{ command: "status", description: "show current thread and model settings" },
|
|
2684
|
+
{ command: "new", description: "start a fresh Codex thread" },
|
|
2685
|
+
{ command: "resume", description: "browse and continue recent sessions" },
|
|
2686
|
+
{ command: "interrupt", description: "interrupt the active turn" },
|
|
2687
|
+
{ command: "esc", description: "interrupt the active turn" },
|
|
2688
|
+
{ command: "cancel", description: "cancel the active interactive prompt" },
|
|
2689
|
+
{ command: "sh", description: "run an explicit shell command" },
|
|
2690
|
+
{ command: "workspace", description: "show or change the bot workspace" },
|
|
2691
|
+
{ command: "omx", description: "run supported oh-my-codex CLI commands" },
|
|
2692
|
+
{ command: "model", description: "show or set the active model" },
|
|
2693
|
+
{ command: "personality", description: "set Codex personality" },
|
|
2694
|
+
{ command: "fast", description: "toggle Fast mode" },
|
|
2695
|
+
{ command: "plan", description: "switch to Plan mode" },
|
|
2696
|
+
{ command: "collab", description: "change collaboration mode" },
|
|
2697
|
+
{ command: "agent", description: "switch active agent thread" },
|
|
2698
|
+
{ command: "permissions", description: "set the approval policy" },
|
|
2699
|
+
{ command: "review", description: "start a code review" },
|
|
2700
|
+
{ command: "rename", description: "rename the current thread" },
|
|
2701
|
+
{ command: "fork", description: "fork the current thread" },
|
|
2702
|
+
{ command: "compact", description: "compact the current thread" },
|
|
2703
|
+
{ command: "clear", description: "start a new blank thread" },
|
|
2704
|
+
{ command: "verbose", description: "toggle detailed tool output" },
|
|
2705
|
+
{ command: "diff", description: "show the current git diff" },
|
|
2706
|
+
{ command: "copy", description: "repeat the last assistant output" },
|
|
2707
|
+
{ command: "mention", description: "attach a file to your next message" },
|
|
2708
|
+
{ command: "skills", description: "list available skills" },
|
|
2709
|
+
{ command: "mcp", description: "list MCP servers" },
|
|
2710
|
+
{ command: "apps", description: "list apps and connectors" },
|
|
2711
|
+
{ command: "plugins", description: "list installed plugins" },
|
|
2712
|
+
{ command: "feedback", description: "send feedback" },
|
|
2713
|
+
{ command: "experimental", description: "show or toggle experimental features" },
|
|
2714
|
+
{ command: "rollout", description: "show current rollout path" },
|
|
2715
|
+
{ command: "logout", description: "log out of Codex" },
|
|
2716
|
+
{ command: "quit", description: "log out of Codex" },
|
|
2717
|
+
{ command: "stop", description: "stop background terminals" },
|
|
2718
|
+
];
|
|
2719
|
+
}
|
|
2720
|
+
function buildToolInteractiveStep(question, index) {
|
|
2721
|
+
const id = asString(question.id) ?? `question_${index + 1}`;
|
|
2722
|
+
const header = asString(question.header);
|
|
2723
|
+
const questionText = asString(question.question);
|
|
2724
|
+
const prompt = [header, questionText].filter(Boolean).join("\n");
|
|
2725
|
+
const options = Array.isArray(question.options)
|
|
2726
|
+
? question.options
|
|
2727
|
+
.map((option) => {
|
|
2728
|
+
if (!option || typeof option !== "object") {
|
|
2729
|
+
return null;
|
|
2730
|
+
}
|
|
2731
|
+
const entry = option;
|
|
2732
|
+
const label = asString(entry.label);
|
|
2733
|
+
return label ? { label, value: label } : null;
|
|
2734
|
+
})
|
|
2735
|
+
.filter((entry) => Boolean(entry))
|
|
2736
|
+
: [];
|
|
2737
|
+
return {
|
|
2738
|
+
key: id,
|
|
2739
|
+
prompt: prompt || `Question ${index + 1}`,
|
|
2740
|
+
kind: options.length > 0 ? "choice" : "text",
|
|
2741
|
+
options: options.length > 0 ? options : undefined,
|
|
2742
|
+
required: true,
|
|
2743
|
+
};
|
|
2744
|
+
}
|
|
2745
|
+
function buildMcpInteractiveSession(params) {
|
|
2746
|
+
const mode = asString(params.mode);
|
|
2747
|
+
const message = asString(params.message) ?? "An MCP server needs your input.";
|
|
2748
|
+
const meta = params._meta && typeof params._meta === "object" ? params._meta : null;
|
|
2749
|
+
if (mode === "url") {
|
|
2750
|
+
const url = asString(params.url);
|
|
2751
|
+
if (!url) {
|
|
2752
|
+
return null;
|
|
2753
|
+
}
|
|
2754
|
+
return {
|
|
2755
|
+
title: message,
|
|
2756
|
+
meta,
|
|
2757
|
+
steps: [
|
|
2758
|
+
{
|
|
2759
|
+
key: "__url_ack__",
|
|
2760
|
+
prompt: message,
|
|
2761
|
+
kind: "url",
|
|
2762
|
+
options: [{ label: "Open link", value: url }],
|
|
2763
|
+
required: true,
|
|
2764
|
+
},
|
|
2765
|
+
],
|
|
2766
|
+
};
|
|
2767
|
+
}
|
|
2768
|
+
if (mode !== "form") {
|
|
2769
|
+
return null;
|
|
2770
|
+
}
|
|
2771
|
+
const schema = params.requestedSchema && typeof params.requestedSchema === "object"
|
|
2772
|
+
? params.requestedSchema
|
|
2773
|
+
: null;
|
|
2774
|
+
if (!schema) {
|
|
2775
|
+
return null;
|
|
2776
|
+
}
|
|
2777
|
+
const properties = schema.properties && typeof schema.properties === "object"
|
|
2778
|
+
? schema.properties
|
|
2779
|
+
: {};
|
|
2780
|
+
const required = Array.isArray(schema.required) ? new Set(schema.required.map(String)) : new Set();
|
|
2781
|
+
const steps = [];
|
|
2782
|
+
for (const [key, rawValue] of Object.entries(properties)) {
|
|
2783
|
+
if (!rawValue || typeof rawValue !== "object") {
|
|
2784
|
+
continue;
|
|
2785
|
+
}
|
|
2786
|
+
const value = rawValue;
|
|
2787
|
+
const prompt = [asString(value.title), asString(value.description)].filter(Boolean).join("\n") || key;
|
|
2788
|
+
const type = asString(value.type);
|
|
2789
|
+
const enumOptions = extractEnumOptions(value);
|
|
2790
|
+
if (enumOptions.length > 0) {
|
|
2791
|
+
steps.push({
|
|
2792
|
+
key,
|
|
2793
|
+
prompt,
|
|
2794
|
+
kind: "choice",
|
|
2795
|
+
options: enumOptions,
|
|
2796
|
+
required: required.has(key),
|
|
2797
|
+
});
|
|
2798
|
+
continue;
|
|
2799
|
+
}
|
|
2800
|
+
if (type === "boolean") {
|
|
2801
|
+
steps.push({
|
|
2802
|
+
key,
|
|
2803
|
+
prompt,
|
|
2804
|
+
kind: "boolean",
|
|
2805
|
+
required: required.has(key),
|
|
2806
|
+
});
|
|
2807
|
+
continue;
|
|
2808
|
+
}
|
|
2809
|
+
if (type === "number" || type === "integer") {
|
|
2810
|
+
steps.push({
|
|
2811
|
+
key,
|
|
2812
|
+
prompt,
|
|
2813
|
+
kind: "number",
|
|
2814
|
+
required: required.has(key),
|
|
2815
|
+
});
|
|
2816
|
+
continue;
|
|
2817
|
+
}
|
|
2818
|
+
if (type === "string") {
|
|
2819
|
+
steps.push({
|
|
2820
|
+
key,
|
|
2821
|
+
prompt,
|
|
2822
|
+
kind: "text",
|
|
2823
|
+
required: required.has(key),
|
|
2824
|
+
});
|
|
2825
|
+
continue;
|
|
2826
|
+
}
|
|
2827
|
+
return null;
|
|
2828
|
+
}
|
|
2829
|
+
if (steps.length === 0) {
|
|
2830
|
+
return null;
|
|
2831
|
+
}
|
|
2832
|
+
return {
|
|
2833
|
+
title: message,
|
|
2834
|
+
steps,
|
|
2835
|
+
meta,
|
|
2836
|
+
};
|
|
2837
|
+
}
|
|
2838
|
+
function extractEnumOptions(value) {
|
|
2839
|
+
const directEnum = Array.isArray(value.enum) ? value.enum : null;
|
|
2840
|
+
if (directEnum) {
|
|
2841
|
+
return directEnum
|
|
2842
|
+
.map((entry) => (typeof entry === "string" ? { label: entry, value: entry } : null))
|
|
2843
|
+
.filter((entry) => Boolean(entry));
|
|
2844
|
+
}
|
|
2845
|
+
const oneOf = Array.isArray(value.oneOf) ? value.oneOf : null;
|
|
2846
|
+
if (oneOf) {
|
|
2847
|
+
return oneOf
|
|
2848
|
+
.map((entry) => {
|
|
2849
|
+
if (!entry || typeof entry !== "object") {
|
|
2850
|
+
return null;
|
|
2851
|
+
}
|
|
2852
|
+
const option = entry;
|
|
2853
|
+
const optionValue = asString(option.const);
|
|
2854
|
+
const optionLabel = asString(option.title) ?? optionValue;
|
|
2855
|
+
return optionValue && optionLabel ? { label: optionLabel, value: optionValue } : null;
|
|
2856
|
+
})
|
|
2857
|
+
.filter((entry) => Boolean(entry));
|
|
2858
|
+
}
|
|
2859
|
+
return [];
|
|
2860
|
+
}
|
|
2861
|
+
//# sourceMappingURL=bridge.js.map
|