telecodex 0.1.4 → 0.1.5
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.
|
@@ -2,9 +2,10 @@ import { contextLogFields, requireScopedSession, } from "../commandSupport.js";
|
|
|
2
2
|
import { handleUserInput, handleUserText } from "../inputService.js";
|
|
3
3
|
import { telegramImageMessageToCodexInput } from "../../telegram/attachments.js";
|
|
4
4
|
import { replyError, replyNotice } from "../../telegram/formatted.js";
|
|
5
|
+
import { wrapUserFacingHandler } from "../userFacingErrors.js";
|
|
5
6
|
export function registerMessageHandlers(deps) {
|
|
6
7
|
const { bot, config, store, projects, codex, buffers, logger } = deps;
|
|
7
|
-
bot.on("message:text", async (ctx) => {
|
|
8
|
+
bot.on("message:text", wrapUserFacingHandler("message:text", logger, async (ctx) => {
|
|
8
9
|
const text = ctx.message.text;
|
|
9
10
|
logger?.info("received telegram text message", {
|
|
10
11
|
...contextLogFields(ctx),
|
|
@@ -30,8 +31,8 @@ export function registerMessageHandlers(deps) {
|
|
|
30
31
|
bot,
|
|
31
32
|
...(logger ? { logger } : {}),
|
|
32
33
|
});
|
|
33
|
-
});
|
|
34
|
-
bot.on(["message:photo", "message:document"], async (ctx) => {
|
|
34
|
+
}));
|
|
35
|
+
bot.on(["message:photo", "message:document"], wrapUserFacingHandler("message:attachment", logger, async (ctx) => {
|
|
35
36
|
const session = await requireScopedSession(ctx, store, projects, config);
|
|
36
37
|
if (!session) {
|
|
37
38
|
logger?.warn("ignored telegram attachment because no scoped session was available", {
|
|
@@ -68,5 +69,5 @@ export function registerMessageHandlers(deps) {
|
|
|
68
69
|
});
|
|
69
70
|
await replyError(ctx, error instanceof Error ? error.message : String(error));
|
|
70
71
|
}
|
|
71
|
-
});
|
|
72
|
+
}));
|
|
72
73
|
}
|
|
@@ -5,12 +5,13 @@ import { refreshSessionIfActiveTurnIsStale } from "../inputService.js";
|
|
|
5
5
|
import { contextLogFields, formatHelpText, formatPrivateStatus, formatProjectStatus, formatReasoningEffort, getProjectForContext, hasTopicContext, isPrivateChat, parseSubcommand, requireScopedSession, } from "../commandSupport.js";
|
|
6
6
|
import { formatIsoTimestamp, sessionLogFields } from "../sessionFlow.js";
|
|
7
7
|
import { codeField, replyDocument, replyError, replyNotice, replyUsage, textField } from "../../telegram/formatted.js";
|
|
8
|
+
import { wrapUserFacingHandler } from "../userFacingErrors.js";
|
|
8
9
|
export function registerOperationalHandlers(deps) {
|
|
9
10
|
const { bot, config, store, projects, codex, logger } = deps;
|
|
10
|
-
bot.command(["start", "help"], async (ctx) => {
|
|
11
|
+
bot.command(["start", "help"], wrapUserFacingHandler("help", logger, async (ctx) => {
|
|
11
12
|
await replyNotice(ctx, formatHelpText(ctx, projects));
|
|
12
|
-
});
|
|
13
|
-
bot.command("admin", async (ctx) => {
|
|
13
|
+
}));
|
|
14
|
+
bot.command("admin", wrapUserFacingHandler("admin", logger, async (ctx) => {
|
|
14
15
|
if (!isPrivateChat(ctx)) {
|
|
15
16
|
await replyNotice(ctx, "Use /admin in the bot private chat.");
|
|
16
17
|
return;
|
|
@@ -62,8 +63,8 @@ export function registerOperationalHandlers(deps) {
|
|
|
62
63
|
return;
|
|
63
64
|
}
|
|
64
65
|
await replyUsage(ctx, "/admin | /admin rebind | /admin cancel");
|
|
65
|
-
});
|
|
66
|
-
bot.command("status", async (ctx) => {
|
|
66
|
+
}));
|
|
67
|
+
bot.command("status", wrapUserFacingHandler("status", logger, async (ctx) => {
|
|
67
68
|
if (isPrivateChat(ctx)) {
|
|
68
69
|
await replyNotice(ctx, formatPrivateStatus(store, projects));
|
|
69
70
|
return;
|
|
@@ -113,8 +114,8 @@ export function registerOperationalHandlers(deps) {
|
|
|
113
114
|
textField("effort", formatReasoningEffort(latestSession.reasoningEffort)),
|
|
114
115
|
],
|
|
115
116
|
});
|
|
116
|
-
});
|
|
117
|
-
bot.command("queue", async (ctx) => {
|
|
117
|
+
}));
|
|
118
|
+
bot.command("queue", wrapUserFacingHandler("queue", logger, async (ctx) => {
|
|
118
119
|
const session = await requireScopedSession(ctx, store, projects, config);
|
|
119
120
|
if (!session)
|
|
120
121
|
return;
|
|
@@ -155,8 +156,8 @@ export function registerOperationalHandlers(deps) {
|
|
|
155
156
|
return;
|
|
156
157
|
}
|
|
157
158
|
await replyUsage(ctx, "/queue | /queue drop <id> | /queue clear");
|
|
158
|
-
});
|
|
159
|
-
bot.command("stop", async (ctx) => {
|
|
159
|
+
}));
|
|
160
|
+
bot.command("stop", wrapUserFacingHandler("stop", logger, async (ctx) => {
|
|
160
161
|
const session = await requireScopedSession(ctx, store, projects, config);
|
|
161
162
|
if (!session)
|
|
162
163
|
return;
|
|
@@ -177,7 +178,7 @@ export function registerOperationalHandlers(deps) {
|
|
|
177
178
|
});
|
|
178
179
|
await replyError(ctx, `Interrupt failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
179
180
|
}
|
|
180
|
-
});
|
|
181
|
+
}));
|
|
181
182
|
}
|
|
182
183
|
function formatQueuedPreview(items) {
|
|
183
184
|
if (items.length === 0)
|
|
@@ -2,10 +2,11 @@ import path from "node:path";
|
|
|
2
2
|
import { contextLogFields, ensureTopicSession, formatPrivateProjectList, formatProjectStatus, formatTopicName, getProjectForContext, hasTopicContext, isPrivateChat, isSupergroupChat, parseSubcommand, postTopicReadyMessage, requireScopedSession, resolveExistingDirectory, } from "../commandSupport.js";
|
|
3
3
|
import { formatSessionRuntimeStatus } from "../../runtime/sessionRuntime.js";
|
|
4
4
|
import { codeField, replyDocument, replyError, replyNotice, replyUsage, textField } from "../../telegram/formatted.js";
|
|
5
|
+
import { wrapUserFacingHandler } from "../userFacingErrors.js";
|
|
5
6
|
const PROJECT_REQUIRED_MESSAGE = "This supergroup has no project bound yet.\nRun /project bind <absolute-path> first.";
|
|
6
7
|
export function registerProjectHandlers(deps) {
|
|
7
8
|
const { bot, config, store, projects, logger } = deps;
|
|
8
|
-
bot.command("project", async (ctx) => {
|
|
9
|
+
bot.command("project", wrapUserFacingHandler("project", logger, async (ctx) => {
|
|
9
10
|
const { command, args } = parseSubcommand(ctx.match.trim());
|
|
10
11
|
if (isPrivateChat(ctx)) {
|
|
11
12
|
if (!command || command === "list" || command === "status") {
|
|
@@ -67,8 +68,8 @@ export function registerProjectHandlers(deps) {
|
|
|
67
68
|
return;
|
|
68
69
|
}
|
|
69
70
|
await replyUsage(ctx, ["/project", "/project bind <absolute-path>", "/project unbind"]);
|
|
70
|
-
});
|
|
71
|
-
bot.command("thread", async (ctx) => {
|
|
71
|
+
}));
|
|
72
|
+
bot.command("thread", wrapUserFacingHandler("thread", logger, async (ctx) => {
|
|
72
73
|
if (isPrivateChat(ctx)) {
|
|
73
74
|
await replyNotice(ctx, "The thread command is only available inside project supergroups.");
|
|
74
75
|
return;
|
|
@@ -112,7 +113,7 @@ export function registerProjectHandlers(deps) {
|
|
|
112
113
|
return;
|
|
113
114
|
}
|
|
114
115
|
await replyUsage(ctx, ["/thread list", "/thread resume <threadId>", "/thread new <topic-name>"]);
|
|
115
|
-
});
|
|
116
|
+
}));
|
|
116
117
|
bot.on(["message:forum_topic_created", "message:forum_topic_edited"], async (ctx) => {
|
|
117
118
|
const threadId = ctx.message.message_thread_id;
|
|
118
119
|
if (threadId == null)
|
|
@@ -2,6 +2,7 @@ import { APPROVAL_POLICIES, MODE_PRESETS, REASONING_EFFORTS, SANDBOX_MODES, WEB_
|
|
|
2
2
|
import { parseCodexConfigOverrides } from "../../codex/configOverrides.js";
|
|
3
3
|
import { assertProjectScopedPath, formatProfileReply, formatReasoningEffort, getProjectForContext, requireScopedSession, resolveExistingDirectory, } from "../commandSupport.js";
|
|
4
4
|
import { codeField, replyDocument, replyError, replyNotice, replyUsage, textField } from "../../telegram/formatted.js";
|
|
5
|
+
import { wrapUserFacingHandler } from "../userFacingErrors.js";
|
|
5
6
|
export function registerSessionConfigHandlers(deps) {
|
|
6
7
|
registerDirectoryHandlers(deps);
|
|
7
8
|
registerProfileHandlers(deps);
|
|
@@ -9,8 +10,8 @@ export function registerSessionConfigHandlers(deps) {
|
|
|
9
10
|
registerAdvancedHandlers(deps);
|
|
10
11
|
}
|
|
11
12
|
function registerDirectoryHandlers(deps) {
|
|
12
|
-
const { bot, config, store, projects } = deps;
|
|
13
|
-
bot.command("cwd", async (ctx) => {
|
|
13
|
+
const { bot, config, store, projects, logger } = deps;
|
|
14
|
+
bot.command("cwd", wrapUserFacingHandler("cwd", logger, async (ctx) => {
|
|
14
15
|
const project = getProjectForContext(ctx, projects);
|
|
15
16
|
const session = await requireScopedSession(ctx, store, projects, config);
|
|
16
17
|
if (!project || !session)
|
|
@@ -34,11 +35,11 @@ function registerDirectoryHandlers(deps) {
|
|
|
34
35
|
catch (error) {
|
|
35
36
|
await replyError(ctx, error instanceof Error ? error.message : String(error));
|
|
36
37
|
}
|
|
37
|
-
});
|
|
38
|
+
}));
|
|
38
39
|
}
|
|
39
40
|
function registerProfileHandlers(deps) {
|
|
40
|
-
const { bot, config, store, projects } = deps;
|
|
41
|
-
bot.command("mode", async (ctx) => {
|
|
41
|
+
const { bot, config, store, projects, logger } = deps;
|
|
42
|
+
bot.command("mode", wrapUserFacingHandler("mode", logger, async (ctx) => {
|
|
42
43
|
const session = await requireScopedSession(ctx, store, projects, config);
|
|
43
44
|
if (!session)
|
|
44
45
|
return;
|
|
@@ -59,8 +60,8 @@ function registerProfileHandlers(deps) {
|
|
|
59
60
|
store.setSandboxMode(session.sessionKey, profile.sandboxMode);
|
|
60
61
|
store.setApprovalPolicy(session.sessionKey, profile.approvalPolicy);
|
|
61
62
|
await replyNotice(ctx, formatProfileReply("Preset updated.", profile.sandboxMode, profile.approvalPolicy));
|
|
62
|
-
});
|
|
63
|
-
bot.command("sandbox", async (ctx) => {
|
|
63
|
+
}));
|
|
64
|
+
bot.command("sandbox", wrapUserFacingHandler("sandbox", logger, async (ctx) => {
|
|
64
65
|
const session = await requireScopedSession(ctx, store, projects, config);
|
|
65
66
|
if (!session)
|
|
66
67
|
return;
|
|
@@ -75,8 +76,8 @@ function registerProfileHandlers(deps) {
|
|
|
75
76
|
}
|
|
76
77
|
store.setSandboxMode(session.sessionKey, sandboxMode);
|
|
77
78
|
await replyNotice(ctx, formatProfileReply("Sandbox updated.", sandboxMode, session.approvalPolicy));
|
|
78
|
-
});
|
|
79
|
-
bot.command("approval", async (ctx) => {
|
|
79
|
+
}));
|
|
80
|
+
bot.command("approval", wrapUserFacingHandler("approval", logger, async (ctx) => {
|
|
80
81
|
const session = await requireScopedSession(ctx, store, projects, config);
|
|
81
82
|
if (!session)
|
|
82
83
|
return;
|
|
@@ -91,8 +92,8 @@ function registerProfileHandlers(deps) {
|
|
|
91
92
|
}
|
|
92
93
|
store.setApprovalPolicy(session.sessionKey, approvalPolicy);
|
|
93
94
|
await replyNotice(ctx, formatProfileReply("Approval policy updated.", session.sandboxMode, approvalPolicy));
|
|
94
|
-
});
|
|
95
|
-
bot.command("yolo", async (ctx) => {
|
|
95
|
+
}));
|
|
96
|
+
bot.command("yolo", wrapUserFacingHandler("yolo", logger, async (ctx) => {
|
|
96
97
|
const session = await requireScopedSession(ctx, store, projects, config);
|
|
97
98
|
if (!session)
|
|
98
99
|
return;
|
|
@@ -110,11 +111,11 @@ function registerProfileHandlers(deps) {
|
|
|
110
111
|
store.setSandboxMode(session.sessionKey, profile.sandboxMode);
|
|
111
112
|
store.setApprovalPolicy(session.sessionKey, profile.approvalPolicy);
|
|
112
113
|
await replyNotice(ctx, formatProfileReply(value === "on" ? "YOLO enabled." : "YOLO disabled. Restored the write preset.", profile.sandboxMode, profile.approvalPolicy));
|
|
113
|
-
});
|
|
114
|
+
}));
|
|
114
115
|
}
|
|
115
116
|
function registerExecutionHandlers(deps) {
|
|
116
|
-
const { bot, config, store, projects } = deps;
|
|
117
|
-
bot.command("model", async (ctx) => {
|
|
117
|
+
const { bot, config, store, projects, logger } = deps;
|
|
118
|
+
bot.command("model", wrapUserFacingHandler("model", logger, async (ctx) => {
|
|
118
119
|
const session = await requireScopedSession(ctx, store, projects, config);
|
|
119
120
|
if (!session)
|
|
120
121
|
return;
|
|
@@ -125,8 +126,8 @@ function registerExecutionHandlers(deps) {
|
|
|
125
126
|
}
|
|
126
127
|
store.setModel(session.sessionKey, model);
|
|
127
128
|
await replyNotice(ctx, `Set model: ${model}`);
|
|
128
|
-
});
|
|
129
|
-
bot.command("effort", async (ctx) => {
|
|
129
|
+
}));
|
|
130
|
+
bot.command("effort", wrapUserFacingHandler("effort", logger, async (ctx) => {
|
|
130
131
|
const session = await requireScopedSession(ctx, store, projects, config);
|
|
131
132
|
if (!session)
|
|
132
133
|
return;
|
|
@@ -141,8 +142,8 @@ function registerExecutionHandlers(deps) {
|
|
|
141
142
|
}
|
|
142
143
|
store.setReasoningEffort(session.sessionKey, value === "default" ? null : value);
|
|
143
144
|
await replyNotice(ctx, `Set reasoning effort: ${value === "default" ? "codex-default" : value}`);
|
|
144
|
-
});
|
|
145
|
-
bot.command("web", async (ctx) => {
|
|
145
|
+
}));
|
|
146
|
+
bot.command("web", wrapUserFacingHandler("web", logger, async (ctx) => {
|
|
146
147
|
const session = await requireScopedSession(ctx, store, projects, config);
|
|
147
148
|
if (!session)
|
|
148
149
|
return;
|
|
@@ -157,8 +158,8 @@ function registerExecutionHandlers(deps) {
|
|
|
157
158
|
}
|
|
158
159
|
store.setWebSearchMode(session.sessionKey, value === "default" ? null : value);
|
|
159
160
|
await replyNotice(ctx, `Set web search: ${value === "default" ? "codex-default" : value}`);
|
|
160
|
-
});
|
|
161
|
-
bot.command("network", async (ctx) => {
|
|
161
|
+
}));
|
|
162
|
+
bot.command("network", wrapUserFacingHandler("network", logger, async (ctx) => {
|
|
162
163
|
const session = await requireScopedSession(ctx, store, projects, config);
|
|
163
164
|
if (!session)
|
|
164
165
|
return;
|
|
@@ -173,8 +174,8 @@ function registerExecutionHandlers(deps) {
|
|
|
173
174
|
}
|
|
174
175
|
store.setNetworkAccessEnabled(session.sessionKey, value === "on");
|
|
175
176
|
await replyNotice(ctx, `Set network access: ${value}`);
|
|
176
|
-
});
|
|
177
|
-
bot.command("gitcheck", async (ctx) => {
|
|
177
|
+
}));
|
|
178
|
+
bot.command("gitcheck", wrapUserFacingHandler("gitcheck", logger, async (ctx) => {
|
|
178
179
|
const session = await requireScopedSession(ctx, store, projects, config);
|
|
179
180
|
if (!session)
|
|
180
181
|
return;
|
|
@@ -189,11 +190,11 @@ function registerExecutionHandlers(deps) {
|
|
|
189
190
|
}
|
|
190
191
|
store.setSkipGitRepoCheck(session.sessionKey, value === "skip");
|
|
191
192
|
await replyNotice(ctx, `Set git repo check: ${value}`);
|
|
192
|
-
});
|
|
193
|
+
}));
|
|
193
194
|
}
|
|
194
195
|
function registerAdvancedHandlers(deps) {
|
|
195
|
-
const { bot, config, store, projects, codex } = deps;
|
|
196
|
-
bot.command("adddir", async (ctx) => {
|
|
196
|
+
const { bot, config, store, projects, codex, logger } = deps;
|
|
197
|
+
bot.command("adddir", wrapUserFacingHandler("adddir", logger, async (ctx) => {
|
|
197
198
|
const project = getProjectForContext(ctx, projects);
|
|
198
199
|
const session = await requireScopedSession(ctx, store, projects, config);
|
|
199
200
|
if (!project || !session)
|
|
@@ -275,8 +276,8 @@ function registerAdvancedHandlers(deps) {
|
|
|
275
276
|
return;
|
|
276
277
|
}
|
|
277
278
|
await replyUsage(ctx, "/adddir list | /adddir add <path-inside-project> | /adddir add-external <absolute-path> | /adddir drop <index> | /adddir clear");
|
|
278
|
-
});
|
|
279
|
-
bot.command("schema", async (ctx) => {
|
|
279
|
+
}));
|
|
280
|
+
bot.command("schema", wrapUserFacingHandler("schema", logger, async (ctx) => {
|
|
280
281
|
const session = await requireScopedSession(ctx, store, projects, config);
|
|
281
282
|
if (!session)
|
|
282
283
|
return;
|
|
@@ -316,8 +317,8 @@ function registerAdvancedHandlers(deps) {
|
|
|
316
317
|
catch (error) {
|
|
317
318
|
await replyError(ctx, `Failed to parse JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
318
319
|
}
|
|
319
|
-
});
|
|
320
|
-
bot.command("codexconfig", async (ctx) => {
|
|
320
|
+
}));
|
|
321
|
+
bot.command("codexconfig", wrapUserFacingHandler("codexconfig", logger, async (ctx) => {
|
|
321
322
|
const raw = ctx.match.trim();
|
|
322
323
|
if (!raw || raw === "show") {
|
|
323
324
|
const current = store.getAppState("codex_config_overrides");
|
|
@@ -353,7 +354,7 @@ function registerAdvancedHandlers(deps) {
|
|
|
353
354
|
catch (error) {
|
|
354
355
|
await replyError(ctx, error instanceof Error ? error.message : String(error));
|
|
355
356
|
}
|
|
356
|
-
});
|
|
357
|
+
}));
|
|
357
358
|
}
|
|
358
359
|
function isPlainObject(value) {
|
|
359
360
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { GrammyError, HttpError } from "grammy";
|
|
2
|
+
import { replyError } from "../telegram/formatted.js";
|
|
3
|
+
import { contextLogFields } from "./commandSupport.js";
|
|
4
|
+
export function wrapUserFacingHandler(handlerName, logger, handler) {
|
|
5
|
+
return async (ctx) => {
|
|
6
|
+
try {
|
|
7
|
+
await handler(ctx);
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
logger?.error("telegram handler failed", {
|
|
11
|
+
handler: handlerName,
|
|
12
|
+
...contextLogFields(ctx),
|
|
13
|
+
error,
|
|
14
|
+
});
|
|
15
|
+
await safeReplyError(ctx, describeUserFacingError(error), logger, handlerName, error);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
async function safeReplyError(ctx, message, logger, handlerName, originalError) {
|
|
20
|
+
try {
|
|
21
|
+
await replyError(ctx, message);
|
|
22
|
+
}
|
|
23
|
+
catch (replyFailure) {
|
|
24
|
+
logger?.error("failed to send telegram handler error reply", {
|
|
25
|
+
handler: handlerName,
|
|
26
|
+
...contextLogFields(ctx),
|
|
27
|
+
error: replyFailure,
|
|
28
|
+
originalError,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function describeUserFacingError(error) {
|
|
33
|
+
if (error instanceof GrammyError) {
|
|
34
|
+
return describeTelegramError(error);
|
|
35
|
+
}
|
|
36
|
+
if (error instanceof HttpError) {
|
|
37
|
+
return "Telegram request failed before it reached the API. Check the local network and try again.";
|
|
38
|
+
}
|
|
39
|
+
if (error instanceof Error) {
|
|
40
|
+
return error.message;
|
|
41
|
+
}
|
|
42
|
+
return String(error);
|
|
43
|
+
}
|
|
44
|
+
function describeTelegramError(error) {
|
|
45
|
+
const description = error.description || error.message;
|
|
46
|
+
const normalized = description.toLowerCase();
|
|
47
|
+
if (error.method === "createForumTopic") {
|
|
48
|
+
if (normalized.includes("not enough rights")) {
|
|
49
|
+
return "Telegram rejected topic creation because the bot lacks permission to create topics. Promote the bot to admin and grant topic management, then try again.";
|
|
50
|
+
}
|
|
51
|
+
if (normalized.includes("forum is disabled") ||
|
|
52
|
+
normalized.includes("chat is not a forum") ||
|
|
53
|
+
normalized.includes("topics are not enabled")) {
|
|
54
|
+
return "This supergroup does not currently allow forum topics. Enable topics for the group and try again.";
|
|
55
|
+
}
|
|
56
|
+
return `Failed to create the Telegram topic: ${description}`;
|
|
57
|
+
}
|
|
58
|
+
return description;
|
|
59
|
+
}
|