telecodex 0.1.3 → 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.
- package/dist/bot/auth.js +9 -8
- package/dist/bot/commandSupport.js +27 -14
- package/dist/bot/handlers/messageHandlers.js +11 -9
- package/dist/bot/handlers/operationalHandlers.js +95 -82
- package/dist/bot/handlers/projectHandlers.js +95 -80
- package/dist/bot/handlers/sessionConfigHandlers.js +168 -137
- package/dist/bot/inputService.js +30 -14
- package/dist/bot/userFacingErrors.js +59 -0
- package/dist/codex/configOverrides.js +50 -0
- package/dist/codex/sessionCatalog.js +66 -12
- package/dist/runtime/bootstrap.js +60 -37
- package/dist/runtime/startTelecodex.js +28 -21
- package/dist/store/fileState.js +100 -8
- package/dist/store/projects.js +3 -0
- package/dist/store/sessions.js +3 -0
- package/dist/telegram/delivery.js +41 -5
- package/dist/telegram/formatted.js +183 -0
- package/dist/telegram/messageBuffer.js +10 -0
- package/dist/telegram/splitMessage.js +1 -1
- package/package.json +5 -3
|
@@ -1,299 +1,344 @@
|
|
|
1
1
|
import { APPROVAL_POLICIES, MODE_PRESETS, REASONING_EFFORTS, SANDBOX_MODES, WEB_SEARCH_MODES, isSessionApprovalPolicy, isSessionModePreset, isSessionReasoningEffort, isSessionSandboxMode, isSessionWebSearchMode, presetFromProfile, profileFromPreset, } from "../../config.js";
|
|
2
|
-
import {
|
|
2
|
+
import { parseCodexConfigOverrides } from "../../codex/configOverrides.js";
|
|
3
|
+
import { assertProjectScopedPath, formatProfileReply, formatReasoningEffort, getProjectForContext, requireScopedSession, resolveExistingDirectory, } from "../commandSupport.js";
|
|
4
|
+
import { codeField, replyDocument, replyError, replyNotice, replyUsage, textField } from "../../telegram/formatted.js";
|
|
5
|
+
import { wrapUserFacingHandler } from "../userFacingErrors.js";
|
|
3
6
|
export function registerSessionConfigHandlers(deps) {
|
|
4
|
-
|
|
5
|
-
|
|
7
|
+
registerDirectoryHandlers(deps);
|
|
8
|
+
registerProfileHandlers(deps);
|
|
9
|
+
registerExecutionHandlers(deps);
|
|
10
|
+
registerAdvancedHandlers(deps);
|
|
11
|
+
}
|
|
12
|
+
function registerDirectoryHandlers(deps) {
|
|
13
|
+
const { bot, config, store, projects, logger } = deps;
|
|
14
|
+
bot.command("cwd", wrapUserFacingHandler("cwd", logger, async (ctx) => {
|
|
6
15
|
const project = getProjectForContext(ctx, projects);
|
|
7
|
-
const session =
|
|
16
|
+
const session = await requireScopedSession(ctx, store, projects, config);
|
|
8
17
|
if (!project || !session)
|
|
9
18
|
return;
|
|
10
19
|
const cwd = ctx.match.trim();
|
|
11
20
|
if (!cwd) {
|
|
12
|
-
await ctx
|
|
21
|
+
await replyCurrentSetting(ctx, "Current directory", [
|
|
22
|
+
codeField("cwd", session.cwd),
|
|
23
|
+
codeField("project root", project.cwd),
|
|
24
|
+
]);
|
|
13
25
|
return;
|
|
14
26
|
}
|
|
15
27
|
try {
|
|
16
28
|
const allowed = assertProjectScopedPath(cwd, project.cwd);
|
|
17
29
|
store.setCwd(session.sessionKey, allowed);
|
|
18
|
-
await ctx
|
|
30
|
+
await replyDocument(ctx, {
|
|
31
|
+
title: "Set cwd",
|
|
32
|
+
fields: [codeField("cwd", allowed)],
|
|
33
|
+
});
|
|
19
34
|
}
|
|
20
35
|
catch (error) {
|
|
21
|
-
await ctx
|
|
36
|
+
await replyError(ctx, error instanceof Error ? error.message : String(error));
|
|
22
37
|
}
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
function registerProfileHandlers(deps) {
|
|
41
|
+
const { bot, config, store, projects, logger } = deps;
|
|
42
|
+
bot.command("mode", wrapUserFacingHandler("mode", logger, async (ctx) => {
|
|
43
|
+
const session = await requireScopedSession(ctx, store, projects, config);
|
|
26
44
|
if (!session)
|
|
27
45
|
return;
|
|
28
46
|
const preset = ctx.match.trim();
|
|
29
47
|
if (!preset) {
|
|
30
|
-
await ctx
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
].join("\n"));
|
|
48
|
+
await replyCurrentSetting(ctx, "Current preset", [
|
|
49
|
+
textField("preset", presetFromProfile(session)),
|
|
50
|
+
textField("sandbox", session.sandboxMode),
|
|
51
|
+
textField("approval", session.approvalPolicy),
|
|
52
|
+
], `Usage: /mode ${MODE_PRESETS.join("|")}`);
|
|
36
53
|
return;
|
|
37
54
|
}
|
|
38
55
|
if (!isSessionModePreset(preset)) {
|
|
39
|
-
await ctx
|
|
56
|
+
await replyInvalidValue(ctx, "Invalid preset.", `Usage: /mode ${MODE_PRESETS.join("|")}`);
|
|
40
57
|
return;
|
|
41
58
|
}
|
|
42
59
|
const profile = profileFromPreset(preset);
|
|
43
60
|
store.setSandboxMode(session.sessionKey, profile.sandboxMode);
|
|
44
61
|
store.setApprovalPolicy(session.sessionKey, profile.approvalPolicy);
|
|
45
|
-
await ctx
|
|
46
|
-
});
|
|
47
|
-
bot.command("sandbox", async (ctx) => {
|
|
48
|
-
const session =
|
|
62
|
+
await replyNotice(ctx, formatProfileReply("Preset updated.", profile.sandboxMode, profile.approvalPolicy));
|
|
63
|
+
}));
|
|
64
|
+
bot.command("sandbox", wrapUserFacingHandler("sandbox", logger, async (ctx) => {
|
|
65
|
+
const session = await requireScopedSession(ctx, store, projects, config);
|
|
49
66
|
if (!session)
|
|
50
67
|
return;
|
|
51
68
|
const sandboxMode = ctx.match.trim();
|
|
52
69
|
if (!sandboxMode) {
|
|
53
|
-
await ctx
|
|
70
|
+
await replyCurrentSetting(ctx, "Current sandbox", [textField("sandbox", session.sandboxMode)], `Usage: /sandbox ${SANDBOX_MODES.join("|")}`);
|
|
54
71
|
return;
|
|
55
72
|
}
|
|
56
73
|
if (!isSessionSandboxMode(sandboxMode)) {
|
|
57
|
-
await ctx
|
|
74
|
+
await replyInvalidValue(ctx, "Invalid sandbox.", `Usage: /sandbox ${SANDBOX_MODES.join("|")}`);
|
|
58
75
|
return;
|
|
59
76
|
}
|
|
60
77
|
store.setSandboxMode(session.sessionKey, sandboxMode);
|
|
61
|
-
await ctx
|
|
62
|
-
});
|
|
63
|
-
bot.command("approval", async (ctx) => {
|
|
64
|
-
const session =
|
|
78
|
+
await replyNotice(ctx, formatProfileReply("Sandbox updated.", sandboxMode, session.approvalPolicy));
|
|
79
|
+
}));
|
|
80
|
+
bot.command("approval", wrapUserFacingHandler("approval", logger, async (ctx) => {
|
|
81
|
+
const session = await requireScopedSession(ctx, store, projects, config);
|
|
65
82
|
if (!session)
|
|
66
83
|
return;
|
|
67
84
|
const approvalPolicy = ctx.match.trim();
|
|
68
85
|
if (!approvalPolicy) {
|
|
69
|
-
await ctx
|
|
86
|
+
await replyCurrentSetting(ctx, "Current approval", [textField("approval", session.approvalPolicy)], `Usage: /approval ${APPROVAL_POLICIES.join("|")}`);
|
|
70
87
|
return;
|
|
71
88
|
}
|
|
72
89
|
if (!isSessionApprovalPolicy(approvalPolicy)) {
|
|
73
|
-
await ctx
|
|
90
|
+
await replyInvalidValue(ctx, "Invalid approval policy.", `Usage: /approval ${APPROVAL_POLICIES.join("|")}`);
|
|
74
91
|
return;
|
|
75
92
|
}
|
|
76
93
|
store.setApprovalPolicy(session.sessionKey, approvalPolicy);
|
|
77
|
-
await ctx
|
|
78
|
-
});
|
|
79
|
-
bot.command("yolo", async (ctx) => {
|
|
80
|
-
const session =
|
|
94
|
+
await replyNotice(ctx, formatProfileReply("Approval policy updated.", session.sandboxMode, approvalPolicy));
|
|
95
|
+
}));
|
|
96
|
+
bot.command("yolo", wrapUserFacingHandler("yolo", logger, async (ctx) => {
|
|
97
|
+
const session = await requireScopedSession(ctx, store, projects, config);
|
|
81
98
|
if (!session)
|
|
82
99
|
return;
|
|
83
100
|
const value = ctx.match.trim().toLowerCase();
|
|
84
101
|
if (!value) {
|
|
85
102
|
const enabled = session.sandboxMode === "danger-full-access" && session.approvalPolicy === "never";
|
|
86
|
-
await ctx
|
|
103
|
+
await replyCurrentSetting(ctx, "Current yolo", [textField("yolo", enabled ? "on" : "off")], "Usage: /yolo on|off");
|
|
87
104
|
return;
|
|
88
105
|
}
|
|
89
106
|
if (value !== "on" && value !== "off") {
|
|
90
|
-
await ctx
|
|
107
|
+
await replyUsage(ctx, "/yolo on|off");
|
|
91
108
|
return;
|
|
92
109
|
}
|
|
93
110
|
const profile = profileFromPreset(value === "on" ? "yolo" : "write");
|
|
94
111
|
store.setSandboxMode(session.sessionKey, profile.sandboxMode);
|
|
95
112
|
store.setApprovalPolicy(session.sessionKey, profile.approvalPolicy);
|
|
96
|
-
await ctx
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
|
|
113
|
+
await replyNotice(ctx, formatProfileReply(value === "on" ? "YOLO enabled." : "YOLO disabled. Restored the write preset.", profile.sandboxMode, profile.approvalPolicy));
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
function registerExecutionHandlers(deps) {
|
|
117
|
+
const { bot, config, store, projects, logger } = deps;
|
|
118
|
+
bot.command("model", wrapUserFacingHandler("model", logger, async (ctx) => {
|
|
119
|
+
const session = await requireScopedSession(ctx, store, projects, config);
|
|
100
120
|
if (!session)
|
|
101
121
|
return;
|
|
102
122
|
const model = ctx.match.trim();
|
|
103
123
|
if (!model) {
|
|
104
|
-
await ctx
|
|
124
|
+
await replyCurrentSetting(ctx, "Current model", [textField("model", session.model)]);
|
|
105
125
|
return;
|
|
106
126
|
}
|
|
107
127
|
store.setModel(session.sessionKey, model);
|
|
108
|
-
await ctx
|
|
109
|
-
});
|
|
110
|
-
bot.command("effort", async (ctx) => {
|
|
111
|
-
const session =
|
|
128
|
+
await replyNotice(ctx, `Set model: ${model}`);
|
|
129
|
+
}));
|
|
130
|
+
bot.command("effort", wrapUserFacingHandler("effort", logger, async (ctx) => {
|
|
131
|
+
const session = await requireScopedSession(ctx, store, projects, config);
|
|
112
132
|
if (!session)
|
|
113
133
|
return;
|
|
114
134
|
const value = ctx.match.trim().toLowerCase();
|
|
115
135
|
if (!value) {
|
|
116
|
-
await ctx
|
|
136
|
+
await replyCurrentSetting(ctx, "Current reasoning effort", [textField("effort", formatReasoningEffort(session.reasoningEffort))], `Usage: /effort default|${REASONING_EFFORTS.join("|")}`);
|
|
117
137
|
return;
|
|
118
138
|
}
|
|
119
139
|
if (value !== "default" && !isSessionReasoningEffort(value)) {
|
|
120
|
-
await ctx
|
|
140
|
+
await replyInvalidValue(ctx, "Invalid reasoning effort.", `Usage: /effort default|${REASONING_EFFORTS.join("|")}`);
|
|
121
141
|
return;
|
|
122
142
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
await ctx.reply(`Set reasoning effort: ${value === "default" ? "codex-default" : value}`);
|
|
130
|
-
});
|
|
131
|
-
bot.command("web", async (ctx) => {
|
|
132
|
-
const session = getScopedSession(ctx, store, projects, config);
|
|
143
|
+
store.setReasoningEffort(session.sessionKey, value === "default" ? null : value);
|
|
144
|
+
await replyNotice(ctx, `Set reasoning effort: ${value === "default" ? "codex-default" : value}`);
|
|
145
|
+
}));
|
|
146
|
+
bot.command("web", wrapUserFacingHandler("web", logger, async (ctx) => {
|
|
147
|
+
const session = await requireScopedSession(ctx, store, projects, config);
|
|
133
148
|
if (!session)
|
|
134
149
|
return;
|
|
135
150
|
const value = ctx.match.trim().toLowerCase();
|
|
136
151
|
if (!value) {
|
|
137
|
-
await ctx
|
|
152
|
+
await replyCurrentSetting(ctx, "Current web search", [textField("web", session.webSearchMode ?? "codex-default")], `Usage: /web default|${WEB_SEARCH_MODES.join("|")}`);
|
|
138
153
|
return;
|
|
139
154
|
}
|
|
140
155
|
if (value !== "default" && !isSessionWebSearchMode(value)) {
|
|
141
|
-
await ctx
|
|
156
|
+
await replyInvalidValue(ctx, "Invalid web search mode.", `Usage: /web default|${WEB_SEARCH_MODES.join("|")}`);
|
|
142
157
|
return;
|
|
143
158
|
}
|
|
144
159
|
store.setWebSearchMode(session.sessionKey, value === "default" ? null : value);
|
|
145
|
-
await ctx
|
|
146
|
-
});
|
|
147
|
-
bot.command("network", async (ctx) => {
|
|
148
|
-
const session =
|
|
160
|
+
await replyNotice(ctx, `Set web search: ${value === "default" ? "codex-default" : value}`);
|
|
161
|
+
}));
|
|
162
|
+
bot.command("network", wrapUserFacingHandler("network", logger, async (ctx) => {
|
|
163
|
+
const session = await requireScopedSession(ctx, store, projects, config);
|
|
149
164
|
if (!session)
|
|
150
165
|
return;
|
|
151
166
|
const value = ctx.match.trim().toLowerCase();
|
|
152
167
|
if (!value) {
|
|
153
|
-
await ctx
|
|
168
|
+
await replyCurrentSetting(ctx, "Current network access", [textField("network", session.networkAccessEnabled ? "on" : "off")], "Usage: /network on|off");
|
|
154
169
|
return;
|
|
155
170
|
}
|
|
156
171
|
if (value !== "on" && value !== "off") {
|
|
157
|
-
await ctx
|
|
172
|
+
await replyUsage(ctx, "/network on|off");
|
|
158
173
|
return;
|
|
159
174
|
}
|
|
160
175
|
store.setNetworkAccessEnabled(session.sessionKey, value === "on");
|
|
161
|
-
await ctx
|
|
162
|
-
});
|
|
163
|
-
bot.command("gitcheck", async (ctx) => {
|
|
164
|
-
const session =
|
|
176
|
+
await replyNotice(ctx, `Set network access: ${value}`);
|
|
177
|
+
}));
|
|
178
|
+
bot.command("gitcheck", wrapUserFacingHandler("gitcheck", logger, async (ctx) => {
|
|
179
|
+
const session = await requireScopedSession(ctx, store, projects, config);
|
|
165
180
|
if (!session)
|
|
166
181
|
return;
|
|
167
182
|
const value = ctx.match.trim().toLowerCase();
|
|
168
183
|
if (!value) {
|
|
169
|
-
await ctx
|
|
184
|
+
await replyCurrentSetting(ctx, "Current git repo check", [textField("git check", session.skipGitRepoCheck ? "skip" : "enforce")], "Usage: /gitcheck skip|enforce");
|
|
170
185
|
return;
|
|
171
186
|
}
|
|
172
187
|
if (value !== "skip" && value !== "enforce") {
|
|
173
|
-
await ctx
|
|
188
|
+
await replyUsage(ctx, "/gitcheck skip|enforce");
|
|
174
189
|
return;
|
|
175
190
|
}
|
|
176
191
|
store.setSkipGitRepoCheck(session.sessionKey, value === "skip");
|
|
177
|
-
await ctx
|
|
178
|
-
});
|
|
179
|
-
|
|
192
|
+
await replyNotice(ctx, `Set git repo check: ${value}`);
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
function registerAdvancedHandlers(deps) {
|
|
196
|
+
const { bot, config, store, projects, codex, logger } = deps;
|
|
197
|
+
bot.command("adddir", wrapUserFacingHandler("adddir", logger, async (ctx) => {
|
|
180
198
|
const project = getProjectForContext(ctx, projects);
|
|
181
|
-
const session =
|
|
199
|
+
const session = await requireScopedSession(ctx, store, projects, config);
|
|
182
200
|
if (!project || !session)
|
|
183
201
|
return;
|
|
184
202
|
const [command, ...rest] = ctx.match.trim().split(/\s+/).filter(Boolean);
|
|
185
203
|
const args = rest.join(" ");
|
|
186
204
|
if (!command || command === "list") {
|
|
187
|
-
await ctx
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
205
|
+
await replyDocument(ctx, {
|
|
206
|
+
title: "Additional directories",
|
|
207
|
+
...(session.additionalDirectories.length > 0
|
|
208
|
+
? {
|
|
209
|
+
sections: [
|
|
210
|
+
{
|
|
211
|
+
title: "Directories",
|
|
212
|
+
fields: session.additionalDirectories.map((directory, index) => codeField(String(index + 1), directory)),
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
}
|
|
216
|
+
: {
|
|
217
|
+
fields: [textField("directories", "none")],
|
|
218
|
+
}),
|
|
219
|
+
footer: "Usage: /adddir add <path-inside-project> | /adddir add-external <absolute-path> | /adddir drop <index> | /adddir clear",
|
|
220
|
+
});
|
|
194
221
|
return;
|
|
195
222
|
}
|
|
196
223
|
if (command === "add") {
|
|
197
224
|
if (!args) {
|
|
198
|
-
await ctx
|
|
225
|
+
await replyUsage(ctx, "/adddir add <path-inside-project>");
|
|
199
226
|
return;
|
|
200
227
|
}
|
|
201
228
|
try {
|
|
202
229
|
const directory = assertProjectScopedPath(args, project.cwd);
|
|
203
230
|
const next = [...session.additionalDirectories.filter((entry) => entry !== directory), directory];
|
|
204
231
|
store.setAdditionalDirectories(session.sessionKey, next);
|
|
205
|
-
await ctx
|
|
232
|
+
await replyDocument(ctx, {
|
|
233
|
+
title: "Added additional directory",
|
|
234
|
+
fields: [codeField("directory", directory)],
|
|
235
|
+
});
|
|
206
236
|
}
|
|
207
237
|
catch (error) {
|
|
208
|
-
await ctx
|
|
238
|
+
await replyError(ctx, error instanceof Error ? error.message : String(error));
|
|
209
239
|
}
|
|
210
240
|
return;
|
|
211
241
|
}
|
|
212
242
|
if (command === "add-external") {
|
|
213
243
|
if (!args) {
|
|
214
|
-
await ctx
|
|
244
|
+
await replyUsage(ctx, "/adddir add-external <absolute-path>");
|
|
215
245
|
return;
|
|
216
246
|
}
|
|
217
247
|
try {
|
|
218
248
|
const directory = resolveExistingDirectory(args);
|
|
219
249
|
const next = [...session.additionalDirectories.filter((entry) => entry !== directory), directory];
|
|
220
250
|
store.setAdditionalDirectories(session.sessionKey, next);
|
|
221
|
-
await ctx
|
|
222
|
-
"Added external additional directory outside the project root.",
|
|
223
|
-
directory,
|
|
224
|
-
"Codex can now read files there during future runs.",
|
|
225
|
-
|
|
251
|
+
await replyDocument(ctx, {
|
|
252
|
+
title: "Added external additional directory outside the project root.",
|
|
253
|
+
fields: [codeField("directory", directory)],
|
|
254
|
+
footer: "Codex can now read files there during future runs.",
|
|
255
|
+
});
|
|
226
256
|
}
|
|
227
257
|
catch (error) {
|
|
228
|
-
await ctx
|
|
258
|
+
await replyError(ctx, error instanceof Error ? error.message : String(error));
|
|
229
259
|
}
|
|
230
260
|
return;
|
|
231
261
|
}
|
|
232
262
|
if (command === "drop") {
|
|
233
263
|
const index = Number(args);
|
|
234
264
|
if (!Number.isInteger(index) || index <= 0 || index > session.additionalDirectories.length) {
|
|
235
|
-
await ctx
|
|
265
|
+
await replyUsage(ctx, "/adddir drop <index>");
|
|
236
266
|
return;
|
|
237
267
|
}
|
|
238
268
|
const next = session.additionalDirectories.filter((_entry, entryIndex) => entryIndex !== index - 1);
|
|
239
269
|
store.setAdditionalDirectories(session.sessionKey, next);
|
|
240
|
-
await ctx
|
|
270
|
+
await replyNotice(ctx, `Removed additional directory #${index}.`);
|
|
241
271
|
return;
|
|
242
272
|
}
|
|
243
273
|
if (command === "clear") {
|
|
244
274
|
store.setAdditionalDirectories(session.sessionKey, []);
|
|
245
|
-
await ctx
|
|
275
|
+
await replyNotice(ctx, "Cleared additional directories.");
|
|
246
276
|
return;
|
|
247
277
|
}
|
|
248
|
-
await ctx
|
|
249
|
-
});
|
|
250
|
-
bot.command("schema", async (ctx) => {
|
|
251
|
-
const session =
|
|
278
|
+
await replyUsage(ctx, "/adddir list | /adddir add <path-inside-project> | /adddir add-external <absolute-path> | /adddir drop <index> | /adddir clear");
|
|
279
|
+
}));
|
|
280
|
+
bot.command("schema", wrapUserFacingHandler("schema", logger, async (ctx) => {
|
|
281
|
+
const session = await requireScopedSession(ctx, store, projects, config);
|
|
252
282
|
if (!session)
|
|
253
283
|
return;
|
|
254
284
|
const raw = ctx.match.trim();
|
|
255
285
|
if (!raw || raw === "show") {
|
|
256
|
-
await ctx
|
|
286
|
+
await replyDocument(ctx, {
|
|
287
|
+
title: "Current output schema",
|
|
288
|
+
fields: session.outputSchema
|
|
289
|
+
? [codeField("schema", session.outputSchema)]
|
|
290
|
+
: [textField("schema", "none")],
|
|
291
|
+
footer: "Usage: /schema set <JSON object> | /schema clear",
|
|
292
|
+
});
|
|
257
293
|
return;
|
|
258
294
|
}
|
|
259
295
|
if (raw === "clear") {
|
|
260
296
|
store.setOutputSchema(session.sessionKey, null);
|
|
261
|
-
await ctx
|
|
297
|
+
await replyNotice(ctx, "Cleared output schema.");
|
|
262
298
|
return;
|
|
263
299
|
}
|
|
264
300
|
if (!raw.startsWith("set ")) {
|
|
265
|
-
await ctx
|
|
301
|
+
await replyUsage(ctx, "/schema show | /schema set <JSON object> | /schema clear");
|
|
266
302
|
return;
|
|
267
303
|
}
|
|
268
304
|
try {
|
|
269
305
|
const parsed = JSON.parse(raw.slice(4).trim());
|
|
270
306
|
if (!isPlainObject(parsed)) {
|
|
271
|
-
await ctx
|
|
307
|
+
await replyError(ctx, "Output schema must be a JSON object.");
|
|
272
308
|
return;
|
|
273
309
|
}
|
|
274
310
|
const normalized = JSON.stringify(parsed);
|
|
275
311
|
store.setOutputSchema(session.sessionKey, normalized);
|
|
276
|
-
await ctx
|
|
312
|
+
await replyDocument(ctx, {
|
|
313
|
+
title: "Set output schema",
|
|
314
|
+
fields: [codeField("schema", normalized)],
|
|
315
|
+
});
|
|
277
316
|
}
|
|
278
317
|
catch (error) {
|
|
279
|
-
await ctx
|
|
318
|
+
await replyError(ctx, `Failed to parse JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
280
319
|
}
|
|
281
|
-
});
|
|
282
|
-
bot.command("codexconfig", async (ctx) => {
|
|
320
|
+
}));
|
|
321
|
+
bot.command("codexconfig", wrapUserFacingHandler("codexconfig", logger, async (ctx) => {
|
|
283
322
|
const raw = ctx.match.trim();
|
|
284
323
|
if (!raw || raw === "show") {
|
|
285
324
|
const current = store.getAppState("codex_config_overrides");
|
|
286
|
-
await ctx
|
|
325
|
+
await replyDocument(ctx, {
|
|
326
|
+
title: "Current Codex config overrides",
|
|
327
|
+
fields: current
|
|
328
|
+
? [codeField("config", current)]
|
|
329
|
+
: [textField("config", "none")],
|
|
330
|
+
footer: "Usage: /codexconfig set <JSON object> | /codexconfig clear",
|
|
331
|
+
});
|
|
287
332
|
return;
|
|
288
333
|
}
|
|
289
334
|
if (raw === "clear") {
|
|
290
335
|
store.deleteAppState("codex_config_overrides");
|
|
291
336
|
codex.setConfigOverrides(undefined);
|
|
292
|
-
await ctx
|
|
337
|
+
await replyNotice(ctx, "Cleared Codex config overrides. They will apply to future runs.");
|
|
293
338
|
return;
|
|
294
339
|
}
|
|
295
340
|
if (!raw.startsWith("set ")) {
|
|
296
|
-
await ctx
|
|
341
|
+
await replyUsage(ctx, "/codexconfig show | /codexconfig set <JSON object> | /codexconfig clear");
|
|
297
342
|
return;
|
|
298
343
|
}
|
|
299
344
|
try {
|
|
@@ -301,40 +346,26 @@ export function registerSessionConfigHandlers(deps) {
|
|
|
301
346
|
const serialized = JSON.stringify(configOverrides);
|
|
302
347
|
store.setAppState("codex_config_overrides", serialized);
|
|
303
348
|
codex.setConfigOverrides(configOverrides);
|
|
304
|
-
await ctx
|
|
349
|
+
await replyDocument(ctx, {
|
|
350
|
+
title: "Set Codex config overrides. They will apply to future runs.",
|
|
351
|
+
fields: [codeField("config", serialized)],
|
|
352
|
+
});
|
|
305
353
|
}
|
|
306
354
|
catch (error) {
|
|
307
|
-
await ctx
|
|
355
|
+
await replyError(ctx, error instanceof Error ? error.message : String(error));
|
|
308
356
|
}
|
|
309
|
-
});
|
|
357
|
+
}));
|
|
310
358
|
}
|
|
311
359
|
function isPlainObject(value) {
|
|
312
360
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
313
361
|
}
|
|
314
|
-
function
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
return parsed;
|
|
362
|
+
async function replyCurrentSetting(ctx, title, fields, footer) {
|
|
363
|
+
await replyDocument(ctx, {
|
|
364
|
+
title,
|
|
365
|
+
fields,
|
|
366
|
+
...(footer ? { footer } : {}),
|
|
367
|
+
});
|
|
321
368
|
}
|
|
322
|
-
function
|
|
323
|
-
|
|
324
|
-
return;
|
|
325
|
-
if (Array.isArray(value)) {
|
|
326
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
327
|
-
assertCodexConfigValue(value[index], `${path}[${index}]`);
|
|
328
|
-
}
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
if (isPlainObject(value)) {
|
|
332
|
-
for (const [key, child] of Object.entries(value)) {
|
|
333
|
-
if (!key)
|
|
334
|
-
throw new Error("Codex config override key cannot be empty.");
|
|
335
|
-
assertCodexConfigValue(child, `${path}.${key}`);
|
|
336
|
-
}
|
|
337
|
-
return;
|
|
338
|
-
}
|
|
339
|
-
throw new Error(`${path} may only contain string, number, boolean, array, or object values.`);
|
|
369
|
+
async function replyInvalidValue(ctx, message, usage) {
|
|
370
|
+
await replyError(ctx, message, usage);
|
|
340
371
|
}
|
package/dist/bot/inputService.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { applySessionRuntimeEvent } from "../runtime/sessionRuntime.js";
|
|
2
|
-
import {
|
|
2
|
+
import { sendReplyNotice } from "../telegram/formatted.js";
|
|
3
3
|
import { isAbortError } from "../codex/sdkRuntime.js";
|
|
4
4
|
import { numericChatId, numericMessageThreadId } from "./session.js";
|
|
5
5
|
import { describeBusyStatus, formatIsoTimestamp, isSessionBusy, sessionBufferKey, sessionLogFields, truncateSingleLine, } from "./sessionFlow.js";
|
|
@@ -28,16 +28,15 @@ export async function handleUserInput(input) {
|
|
|
28
28
|
}
|
|
29
29
|
const queued = store.enqueueInput(session.sessionKey, prompt);
|
|
30
30
|
const queueDepth = store.getQueuedInputCount(session.sessionKey);
|
|
31
|
-
await
|
|
31
|
+
await sendReplyNotice(bot, {
|
|
32
32
|
chatId: numericChatId(session),
|
|
33
33
|
messageThreadId: numericMessageThreadId(session),
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}, logger);
|
|
34
|
+
}, [
|
|
35
|
+
`The current Codex task is still ${describeBusyStatus(session.runtimeStatus)}. Your message was added to the queue.`,
|
|
36
|
+
`queue position: ${queueDepth}`,
|
|
37
|
+
`queued at: ${formatIsoTimestamp(queued.createdAt)}`,
|
|
38
|
+
"It will be processed automatically after the current run finishes.",
|
|
39
|
+
], logger);
|
|
41
40
|
return {
|
|
42
41
|
status: "queued",
|
|
43
42
|
consumed: true,
|
|
@@ -138,11 +137,10 @@ export async function recoverActiveTopicSessions(store, codex, _buffers, bot, lo
|
|
|
138
137
|
});
|
|
139
138
|
for (const session of sessions) {
|
|
140
139
|
const refreshed = await refreshSessionIfActiveTurnIsStale(session, store, codex, bot, logger);
|
|
141
|
-
await
|
|
140
|
+
await sendReplyNotice(bot, {
|
|
142
141
|
chatId: numericChatId(refreshed),
|
|
143
142
|
messageThreadId: numericMessageThreadId(refreshed),
|
|
144
|
-
|
|
145
|
-
}, logger).catch((error) => {
|
|
143
|
+
}, "telecodex restarted and cannot resume the previous streamed run state. Send the message again if you want to continue.", logger).catch((error) => {
|
|
146
144
|
logger?.warn("failed to notify session about stale sdk recovery", {
|
|
147
145
|
...sessionLogFields(refreshed),
|
|
148
146
|
error,
|
|
@@ -199,7 +197,7 @@ async function runSessionPrompt(input) {
|
|
|
199
197
|
networkAccessEnabled: session.networkAccessEnabled,
|
|
200
198
|
skipGitRepoCheck: session.skipGitRepoCheck,
|
|
201
199
|
additionalDirectories: session.additionalDirectories,
|
|
202
|
-
outputSchema:
|
|
200
|
+
outputSchema: readOutputSchema(session, store, logger),
|
|
203
201
|
},
|
|
204
202
|
prompt: toSdkInput(prompt),
|
|
205
203
|
callbacks: {
|
|
@@ -368,5 +366,23 @@ function toSdkInput(input) {
|
|
|
368
366
|
function parseOutputSchema(value) {
|
|
369
367
|
if (!value)
|
|
370
368
|
return undefined;
|
|
371
|
-
|
|
369
|
+
try {
|
|
370
|
+
return JSON.parse(value);
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
throw new Error(`Invalid stored output schema: ${error instanceof Error ? error.message : String(error)}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
function readOutputSchema(session, store, logger) {
|
|
377
|
+
try {
|
|
378
|
+
return parseOutputSchema(session.outputSchema);
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
store.setOutputSchema(session.sessionKey, null);
|
|
382
|
+
logger?.warn("cleared invalid stored output schema", {
|
|
383
|
+
...sessionLogFields(session),
|
|
384
|
+
error,
|
|
385
|
+
});
|
|
386
|
+
return undefined;
|
|
387
|
+
}
|
|
372
388
|
}
|