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.
@@ -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 { assertProjectScopedPath, formatProfileReply, formatReasoningEffort, getProjectForContext, getScopedSession, resolveExistingDirectory, } from "../commandSupport.js";
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
- const { bot, config, store, projects, codex } = deps;
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 = getScopedSession(ctx, store, projects, config);
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.reply(`Current directory: ${session.cwd}\nProject root: ${project.cwd}`);
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.reply(`Set cwd:\n${allowed}`);
29
+ await replyDocument(ctx, {
30
+ title: "Set cwd",
31
+ fields: [codeField("cwd", allowed)],
32
+ });
19
33
  }
20
34
  catch (error) {
21
- await ctx.reply(error instanceof Error ? error.message : String(error));
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 = getScopedSession(ctx, store, projects, config);
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.reply([
31
- `Current preset: ${presetFromProfile(session)}`,
32
- `sandbox: ${session.sandboxMode}`,
33
- `approval: ${session.approvalPolicy}`,
34
- `Usage: /mode ${MODE_PRESETS.join("|")}`,
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.reply(`Invalid preset.\nUsage: /mode ${MODE_PRESETS.join("|")}`);
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.reply(formatProfileReply("Preset updated.", profile.sandboxMode, profile.approvalPolicy));
61
+ await replyNotice(ctx, formatProfileReply("Preset updated.", profile.sandboxMode, profile.approvalPolicy));
46
62
  });
47
63
  bot.command("sandbox", async (ctx) => {
48
- const session = getScopedSession(ctx, store, projects, config);
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.reply(`Current sandbox: ${session.sandboxMode}\nUsage: /sandbox ${SANDBOX_MODES.join("|")}`);
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.reply(`Invalid sandbox.\nUsage: /sandbox ${SANDBOX_MODES.join("|")}`);
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.reply(formatProfileReply("Sandbox updated.", sandboxMode, session.approvalPolicy));
77
+ await replyNotice(ctx, formatProfileReply("Sandbox updated.", sandboxMode, session.approvalPolicy));
62
78
  });
63
79
  bot.command("approval", async (ctx) => {
64
- const session = getScopedSession(ctx, store, projects, config);
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.reply(`Current approval: ${session.approvalPolicy}\nUsage: /approval ${APPROVAL_POLICIES.join("|")}`);
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.reply(`Invalid approval policy.\nUsage: /approval ${APPROVAL_POLICIES.join("|")}`);
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.reply(formatProfileReply("Approval policy updated.", session.sandboxMode, approvalPolicy));
93
+ await replyNotice(ctx, formatProfileReply("Approval policy updated.", session.sandboxMode, approvalPolicy));
78
94
  });
79
95
  bot.command("yolo", async (ctx) => {
80
- const session = getScopedSession(ctx, store, projects, config);
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.reply(`Current yolo: ${enabled ? "on" : "off"}\nUsage: /yolo on|off`);
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.reply("Usage: /yolo on|off");
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.reply(formatProfileReply(value === "on" ? "YOLO enabled." : "YOLO disabled. Restored the write preset.", profile.sandboxMode, profile.approvalPolicy));
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 = getScopedSession(ctx, store, projects, config);
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.reply(`Current model: ${session.model}`);
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.reply(`Set model: ${model}`);
127
+ await replyNotice(ctx, `Set model: ${model}`);
109
128
  });
110
129
  bot.command("effort", async (ctx) => {
111
- const session = getScopedSession(ctx, store, projects, config);
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.reply(`Current reasoning effort: ${formatReasoningEffort(session.reasoningEffort)}\nUsage: /effort default|${REASONING_EFFORTS.join("|")}`);
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.reply(`Invalid reasoning effort.\nUsage: /effort default|${REASONING_EFFORTS.join("|")}`);
139
+ await replyInvalidValue(ctx, "Invalid reasoning effort.", `Usage: /effort default|${REASONING_EFFORTS.join("|")}`);
121
140
  return;
122
141
  }
123
- if (value === "default") {
124
- store.setReasoningEffort(session.sessionKey, null);
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 = getScopedSession(ctx, store, projects, config);
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.reply(`Current web search: ${session.webSearchMode ?? "codex-default"}\nUsage: /web default|${WEB_SEARCH_MODES.join("|")}`);
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.reply(`Invalid web search mode.\nUsage: /web default|${WEB_SEARCH_MODES.join("|")}`);
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.reply(`Set web search: ${value === "default" ? "codex-default" : value}`);
159
+ await replyNotice(ctx, `Set web search: ${value === "default" ? "codex-default" : value}`);
146
160
  });
147
161
  bot.command("network", async (ctx) => {
148
- const session = getScopedSession(ctx, store, projects, config);
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.reply(`Current network access: ${session.networkAccessEnabled ? "on" : "off"}\nUsage: /network on|off`);
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.reply("Usage: /network on|off");
171
+ await replyUsage(ctx, "/network on|off");
158
172
  return;
159
173
  }
160
174
  store.setNetworkAccessEnabled(session.sessionKey, value === "on");
161
- await ctx.reply(`Set network access: ${value}`);
175
+ await replyNotice(ctx, `Set network access: ${value}`);
162
176
  });
163
177
  bot.command("gitcheck", async (ctx) => {
164
- const session = getScopedSession(ctx, store, projects, config);
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.reply(`Current git repo check: ${session.skipGitRepoCheck ? "skip" : "enforce"}\nUsage: /gitcheck skip|enforce`);
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.reply("Usage: /gitcheck skip|enforce");
187
+ await replyUsage(ctx, "/gitcheck skip|enforce");
174
188
  return;
175
189
  }
176
190
  store.setSkipGitRepoCheck(session.sessionKey, value === "skip");
177
- await ctx.reply(`Set git repo check: ${value}`);
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 = getScopedSession(ctx, store, projects, config);
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.reply(session.additionalDirectories.length === 0
188
- ? "additional directories: none\nUsage: /adddir add <path-inside-project> | /adddir add-external <absolute-path> | /adddir drop <index> | /adddir clear"
189
- : [
190
- "additional directories:",
191
- ...session.additionalDirectories.map((directory, index) => `${index + 1}. ${directory}`),
192
- "Usage: /adddir add <path-inside-project> | /adddir add-external <absolute-path> | /adddir drop <index> | /adddir clear",
193
- ].join("\n"));
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.reply("Usage: /adddir add <path-inside-project>");
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.reply(`Added additional directory:\n${directory}`);
231
+ await replyDocument(ctx, {
232
+ title: "Added additional directory",
233
+ fields: [codeField("directory", directory)],
234
+ });
206
235
  }
207
236
  catch (error) {
208
- await ctx.reply(error instanceof Error ? error.message : String(error));
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.reply("Usage: /adddir add-external <absolute-path>");
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.reply([
222
- "Added external additional directory outside the project root.",
223
- directory,
224
- "Codex can now read files there during future runs.",
225
- ].join("\n"));
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.reply(error instanceof Error ? error.message : String(error));
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.reply("Usage: /adddir drop <index>");
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.reply(`Removed additional directory #${index}.`);
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.reply("Cleared additional directories.");
274
+ await replyNotice(ctx, "Cleared additional directories.");
246
275
  return;
247
276
  }
248
- await ctx.reply("Usage: /adddir list | /adddir add <path-inside-project> | /adddir add-external <absolute-path> | /adddir drop <index> | /adddir clear");
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 = getScopedSession(ctx, store, projects, config);
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.reply(session.outputSchema ? `Current output schema:\n${session.outputSchema}` : "Current output schema: none\nUsage: /schema set <JSON object> | /schema clear");
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.reply("Cleared output schema.");
296
+ await replyNotice(ctx, "Cleared output schema.");
262
297
  return;
263
298
  }
264
299
  if (!raw.startsWith("set ")) {
265
- await ctx.reply("Usage: /schema show | /schema set <JSON object> | /schema clear");
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.reply("Output schema must be a JSON object.");
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.reply(`Set output schema:\n${normalized}`);
311
+ await replyDocument(ctx, {
312
+ title: "Set output schema",
313
+ fields: [codeField("schema", normalized)],
314
+ });
277
315
  }
278
316
  catch (error) {
279
- await ctx.reply(`Failed to parse JSON: ${error instanceof Error ? error.message : String(error)}`);
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.reply(current ? `Current Codex config overrides:\n${current}` : "Current Codex config overrides: none\nUsage: /codexconfig set <JSON object> | /codexconfig clear");
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.reply("Cleared Codex config overrides. They will apply to future runs.");
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.reply("Usage: /codexconfig show | /codexconfig set <JSON object> | /codexconfig clear");
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.reply(`Set Codex config overrides. They will apply to future runs.\n${serialized}`);
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.reply(error instanceof Error ? error.message : String(error));
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 parseCodexConfigOverrides(raw) {
315
- const parsed = JSON.parse(raw);
316
- if (!isPlainObject(parsed)) {
317
- throw new Error("Codex config overrides must be a JSON object.");
318
- }
319
- assertCodexConfigValue(parsed, "config");
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 assertCodexConfigValue(value, path) {
323
- if (typeof value === "string" || typeof value === "number" || typeof value === "boolean")
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
  }
@@ -1,5 +1,5 @@
1
1
  import { applySessionRuntimeEvent } from "../runtime/sessionRuntime.js";
2
- import { sendPlainChunks } from "../telegram/delivery.js";
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 sendPlainChunks(bot, {
31
+ await sendReplyNotice(bot, {
32
32
  chatId: numericChatId(session),
33
33
  messageThreadId: numericMessageThreadId(session),
34
- text: [
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
- ].join("\n"),
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 sendPlainChunks(bot, {
140
+ await sendReplyNotice(bot, {
142
141
  chatId: numericChatId(refreshed),
143
142
  messageThreadId: numericMessageThreadId(refreshed),
144
- text: "telecodex restarted and cannot resume the previous streamed run state. Send the message again if you want to continue.",
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: parseOutputSchema(session.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
- return JSON.parse(value);
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
+ }