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