telecodex 0.1.0 → 0.1.2
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 +41 -4
- package/dist/bot/auth.js +62 -13
- package/dist/bot/commandSupport.js +23 -8
- package/dist/bot/createBot.js +16 -3
- package/dist/bot/handlers/operationalHandlers.js +52 -0
- package/dist/bot/handlers/projectHandlers.js +67 -8
- package/dist/bot/handlers/sessionConfigHandlers.js +27 -6
- package/dist/bot/topicCleanup.js +80 -0
- package/dist/cli.js +0 -0
- package/dist/codex/sessionCatalog.js +215 -0
- package/dist/config.js +0 -1
- package/dist/runtime/appPaths.js +4 -1
- package/dist/runtime/bindingCodes.js +5 -0
- package/dist/runtime/bootstrap.js +26 -17
- package/dist/runtime/startTelecodex.js +5 -0
- package/dist/store/fileState.js +370 -0
- package/dist/store/legacyMigration.js +160 -0
- package/dist/store/projects.js +11 -33
- package/dist/store/sessions.js +240 -207
- package/dist/telegram/renderer.js +40 -3
- package/package.json +2 -2
- package/dist/store/db.js +0 -267
package/dist/store/sessions.js
CHANGED
|
@@ -1,22 +1,23 @@
|
|
|
1
|
-
import { DEFAULT_SESSION_PROFILE,
|
|
1
|
+
import { DEFAULT_SESSION_PROFILE, } from "../config.js";
|
|
2
|
+
export const BINDING_CODE_TTL_MS = 15 * 60 * 1000;
|
|
3
|
+
export const BINDING_CODE_MAX_ATTEMPTS = 5;
|
|
2
4
|
export class SessionStore {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
5
|
+
storage;
|
|
6
|
+
runtimeStateBySession = new Map();
|
|
7
|
+
outputMessageBySession = new Map();
|
|
8
|
+
queueBySession = new Map();
|
|
9
|
+
nextQueuedInputId = 1;
|
|
10
|
+
constructor(storage) {
|
|
11
|
+
this.storage = storage;
|
|
6
12
|
}
|
|
7
13
|
getAppState(key) {
|
|
8
|
-
|
|
9
|
-
return row?.value ?? null;
|
|
14
|
+
return this.storage.getAppState(key);
|
|
10
15
|
}
|
|
11
16
|
setAppState(key, value) {
|
|
12
|
-
this.
|
|
13
|
-
.prepare(`INSERT INTO app_state (key, value, updated_at)
|
|
14
|
-
VALUES (?, ?, ?)
|
|
15
|
-
ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at`)
|
|
16
|
-
.run(key, value, new Date().toISOString());
|
|
17
|
+
this.storage.setAppState(key, value);
|
|
17
18
|
}
|
|
18
19
|
deleteAppState(key) {
|
|
19
|
-
this.
|
|
20
|
+
this.storage.deleteAppState(key);
|
|
20
21
|
}
|
|
21
22
|
getAuthorizedUserId() {
|
|
22
23
|
const value = this.getAppState("authorized_user_id");
|
|
@@ -26,296 +27,331 @@ export class SessionStore {
|
|
|
26
27
|
return Number.isSafeInteger(userId) ? userId : null;
|
|
27
28
|
}
|
|
28
29
|
getBootstrapCode() {
|
|
29
|
-
|
|
30
|
+
const binding = this.getBindingCodeState();
|
|
31
|
+
return binding?.mode === "bootstrap" ? binding.code : null;
|
|
30
32
|
}
|
|
31
33
|
setBootstrapCode(code) {
|
|
32
|
-
this.
|
|
34
|
+
this.issueBindingCode({
|
|
35
|
+
code,
|
|
36
|
+
mode: "bootstrap",
|
|
37
|
+
});
|
|
33
38
|
}
|
|
34
39
|
clearBootstrapCode() {
|
|
40
|
+
this.clearBindingCode();
|
|
41
|
+
}
|
|
42
|
+
getBindingCodeState(now = new Date()) {
|
|
43
|
+
const code = this.getAppState("bootstrap_code");
|
|
44
|
+
const createdAt = this.getAppState("binding_code_created_at");
|
|
45
|
+
const expiresAt = this.getAppState("binding_code_expires_at");
|
|
46
|
+
if (!code || !createdAt || !expiresAt)
|
|
47
|
+
return null;
|
|
48
|
+
const expiresAtMs = Date.parse(expiresAt);
|
|
49
|
+
if (!Number.isFinite(expiresAtMs) || expiresAtMs <= now.getTime()) {
|
|
50
|
+
this.clearBindingCode();
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const attempts = normalizeNonNegativeInteger(this.getAppState("binding_code_attempts"));
|
|
54
|
+
if (attempts >= BINDING_CODE_MAX_ATTEMPTS) {
|
|
55
|
+
this.clearBindingCode();
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
code,
|
|
60
|
+
mode: normalizeBindingCodeMode(this.getAppState("binding_code_mode")),
|
|
61
|
+
createdAt,
|
|
62
|
+
expiresAt,
|
|
63
|
+
attempts,
|
|
64
|
+
maxAttempts: BINDING_CODE_MAX_ATTEMPTS,
|
|
65
|
+
issuedByUserId: normalizeOptionalUserId(this.getAppState("binding_code_issued_by_user_id")),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
issueBindingCode(input) {
|
|
69
|
+
const now = input.now ?? new Date();
|
|
70
|
+
const createdAt = now.toISOString();
|
|
71
|
+
const expiresAt = new Date(now.getTime() + (input.ttlMs ?? BINDING_CODE_TTL_MS)).toISOString();
|
|
72
|
+
this.setAppState("bootstrap_code", input.code);
|
|
73
|
+
this.setAppState("binding_code_mode", input.mode);
|
|
74
|
+
this.setAppState("binding_code_created_at", createdAt);
|
|
75
|
+
this.setAppState("binding_code_expires_at", expiresAt);
|
|
76
|
+
this.setAppState("binding_code_attempts", "0");
|
|
77
|
+
if (input.issuedByUserId == null) {
|
|
78
|
+
this.deleteAppState("binding_code_issued_by_user_id");
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
this.setAppState("binding_code_issued_by_user_id", String(input.issuedByUserId));
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
code: input.code,
|
|
85
|
+
mode: input.mode,
|
|
86
|
+
createdAt,
|
|
87
|
+
expiresAt,
|
|
88
|
+
attempts: 0,
|
|
89
|
+
maxAttempts: BINDING_CODE_MAX_ATTEMPTS,
|
|
90
|
+
issuedByUserId: input.issuedByUserId ?? null,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
recordBindingCodeFailure(now = new Date()) {
|
|
94
|
+
const state = this.getBindingCodeState(now);
|
|
95
|
+
if (!state)
|
|
96
|
+
return null;
|
|
97
|
+
const attempts = state.attempts + 1;
|
|
98
|
+
if (attempts >= state.maxAttempts) {
|
|
99
|
+
this.clearBindingCode();
|
|
100
|
+
return {
|
|
101
|
+
attempts,
|
|
102
|
+
remaining: 0,
|
|
103
|
+
exhausted: true,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
this.setAppState("binding_code_attempts", String(attempts));
|
|
107
|
+
return {
|
|
108
|
+
attempts,
|
|
109
|
+
remaining: state.maxAttempts - attempts,
|
|
110
|
+
exhausted: false,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
clearBindingCode() {
|
|
35
114
|
this.deleteAppState("bootstrap_code");
|
|
115
|
+
this.deleteAppState("binding_code_mode");
|
|
116
|
+
this.deleteAppState("binding_code_created_at");
|
|
117
|
+
this.deleteAppState("binding_code_expires_at");
|
|
118
|
+
this.deleteAppState("binding_code_attempts");
|
|
119
|
+
this.deleteAppState("binding_code_issued_by_user_id");
|
|
36
120
|
}
|
|
37
121
|
claimAuthorizedUserId(userId) {
|
|
38
122
|
const existing = this.getAuthorizedUserId();
|
|
39
123
|
if (existing != null)
|
|
40
124
|
return existing;
|
|
41
|
-
this.
|
|
125
|
+
this.setAppState("authorized_user_id", String(userId));
|
|
126
|
+
this.clearBindingCode();
|
|
42
127
|
const current = this.getAuthorizedUserId();
|
|
43
128
|
if (current == null)
|
|
44
129
|
throw new Error("Failed to persist authorized Telegram user id");
|
|
45
|
-
this.clearBootstrapCode();
|
|
46
130
|
return current;
|
|
47
131
|
}
|
|
132
|
+
rebindAuthorizedUserId(userId) {
|
|
133
|
+
this.setAppState("authorized_user_id", String(userId));
|
|
134
|
+
this.clearBindingCode();
|
|
135
|
+
}
|
|
48
136
|
clearAuthorizedUserId() {
|
|
49
137
|
this.deleteAppState("authorized_user_id");
|
|
138
|
+
this.clearBindingCode();
|
|
50
139
|
}
|
|
51
140
|
getOrCreate(input) {
|
|
52
141
|
const existing = this.get(input.sessionKey);
|
|
53
142
|
if (existing)
|
|
54
143
|
return existing;
|
|
55
144
|
const now = new Date().toISOString();
|
|
56
|
-
this.
|
|
57
|
-
.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
145
|
+
this.storage.putSession({
|
|
146
|
+
sessionKey: input.sessionKey,
|
|
147
|
+
chatId: input.chatId,
|
|
148
|
+
messageThreadId: input.messageThreadId,
|
|
149
|
+
telegramTopicName: input.telegramTopicName ?? null,
|
|
150
|
+
codexThreadId: null,
|
|
151
|
+
cwd: input.defaultCwd,
|
|
152
|
+
model: input.defaultModel,
|
|
153
|
+
sandboxMode: DEFAULT_SESSION_PROFILE.sandboxMode,
|
|
154
|
+
approvalPolicy: DEFAULT_SESSION_PROFILE.approvalPolicy,
|
|
155
|
+
reasoningEffort: null,
|
|
156
|
+
webSearchMode: null,
|
|
157
|
+
networkAccessEnabled: true,
|
|
158
|
+
skipGitRepoCheck: true,
|
|
159
|
+
additionalDirectories: [],
|
|
160
|
+
outputSchema: null,
|
|
161
|
+
createdAt: now,
|
|
162
|
+
updatedAt: now,
|
|
163
|
+
});
|
|
61
164
|
const created = this.get(input.sessionKey);
|
|
62
165
|
if (!created)
|
|
63
166
|
throw new Error("Session insert failed");
|
|
64
167
|
return created;
|
|
65
168
|
}
|
|
66
169
|
get(sessionKey) {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
.get(sessionKey);
|
|
70
|
-
return row ? mapSessionRow(row) : null;
|
|
170
|
+
const stored = this.storage.getSession(sessionKey);
|
|
171
|
+
return mapStoredSession(stored, this.runtimeStateBySession.get(sessionKey), this.outputMessageBySession.get(sessionKey));
|
|
71
172
|
}
|
|
72
173
|
getByThreadId(threadId) {
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
.get(threadId);
|
|
76
|
-
return row ? mapSessionRow(row) : null;
|
|
174
|
+
const stored = this.storage.getSessionByThreadId(threadId);
|
|
175
|
+
return stored ? mapStoredSession(stored, this.runtimeStateBySession.get(stored.sessionKey), this.outputMessageBySession.get(stored.sessionKey)) : null;
|
|
77
176
|
}
|
|
78
177
|
listTopicSessions() {
|
|
79
|
-
|
|
80
|
-
.
|
|
81
|
-
.
|
|
82
|
-
|
|
178
|
+
return this.storage
|
|
179
|
+
.listSessions()
|
|
180
|
+
.filter((session) => session.messageThreadId != null)
|
|
181
|
+
.map((session) => mapStoredSession(session, this.runtimeStateBySession.get(session.sessionKey), this.outputMessageBySession.get(session.sessionKey)))
|
|
182
|
+
.filter((session) => session != null);
|
|
83
183
|
}
|
|
84
184
|
remove(sessionKey) {
|
|
85
|
-
this.
|
|
86
|
-
this.
|
|
185
|
+
this.queueBySession.delete(sessionKey);
|
|
186
|
+
this.runtimeStateBySession.delete(sessionKey);
|
|
187
|
+
this.outputMessageBySession.delete(sessionKey);
|
|
188
|
+
this.storage.removeSession(sessionKey);
|
|
87
189
|
}
|
|
88
190
|
enqueueInput(sessionKey, input) {
|
|
89
191
|
const now = new Date().toISOString();
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
192
|
+
const queued = {
|
|
193
|
+
id: this.nextQueuedInputId,
|
|
194
|
+
sessionKey,
|
|
195
|
+
text: formatCodexInputPreview(input),
|
|
196
|
+
input: cloneStoredCodexInput(input),
|
|
197
|
+
createdAt: now,
|
|
198
|
+
updatedAt: now,
|
|
199
|
+
};
|
|
200
|
+
this.nextQueuedInputId += 1;
|
|
201
|
+
const queue = this.queueBySession.get(sessionKey) ?? [];
|
|
202
|
+
queue.push(queued);
|
|
203
|
+
this.queueBySession.set(sessionKey, queue);
|
|
204
|
+
return cloneQueuedInput(queued);
|
|
100
205
|
}
|
|
101
206
|
getQueuedInput(id) {
|
|
102
|
-
const
|
|
103
|
-
|
|
207
|
+
for (const queue of this.queueBySession.values()) {
|
|
208
|
+
const match = queue.find((item) => item.id === id);
|
|
209
|
+
if (match)
|
|
210
|
+
return cloneQueuedInput(match);
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
104
213
|
}
|
|
105
214
|
getQueuedInputCount(sessionKey) {
|
|
106
|
-
|
|
107
|
-
.prepare("SELECT COUNT(*) AS count FROM queued_inputs WHERE session_key = ?")
|
|
108
|
-
.get(sessionKey);
|
|
109
|
-
return row?.count ?? 0;
|
|
215
|
+
return this.queueBySession.get(sessionKey)?.length ?? 0;
|
|
110
216
|
}
|
|
111
217
|
peekNextQueuedInput(sessionKey) {
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
return row ? mapQueuedInputRow(row) : null;
|
|
218
|
+
const queue = this.queueBySession.get(sessionKey);
|
|
219
|
+
const [next] = queue ?? [];
|
|
220
|
+
return next ? cloneQueuedInput(next) : null;
|
|
116
221
|
}
|
|
117
222
|
listQueuedInputs(sessionKey, limit = 5) {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
.all(sessionKey, limit);
|
|
121
|
-
return rows.map(mapQueuedInputRow);
|
|
223
|
+
const queue = this.queueBySession.get(sessionKey) ?? [];
|
|
224
|
+
return queue.slice(0, limit).map(cloneQueuedInput);
|
|
122
225
|
}
|
|
123
226
|
removeQueuedInput(id) {
|
|
124
|
-
|
|
227
|
+
for (const [sessionKey, queue] of this.queueBySession.entries()) {
|
|
228
|
+
const index = queue.findIndex((item) => item.id === id);
|
|
229
|
+
if (index < 0)
|
|
230
|
+
continue;
|
|
231
|
+
queue.splice(index, 1);
|
|
232
|
+
if (queue.length === 0) {
|
|
233
|
+
this.queueBySession.delete(sessionKey);
|
|
234
|
+
}
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
125
237
|
}
|
|
126
238
|
removeQueuedInputForSession(sessionKey, id) {
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
239
|
+
const queue = this.queueBySession.get(sessionKey);
|
|
240
|
+
if (!queue)
|
|
241
|
+
return false;
|
|
242
|
+
const index = queue.findIndex((item) => item.id === id);
|
|
243
|
+
if (index < 0)
|
|
244
|
+
return false;
|
|
245
|
+
queue.splice(index, 1);
|
|
246
|
+
if (queue.length === 0) {
|
|
247
|
+
this.queueBySession.delete(sessionKey);
|
|
248
|
+
}
|
|
249
|
+
return true;
|
|
131
250
|
}
|
|
132
251
|
clearQueuedInputs(sessionKey) {
|
|
133
|
-
const
|
|
134
|
-
|
|
252
|
+
const queue = this.queueBySession.get(sessionKey) ?? [];
|
|
253
|
+
this.queueBySession.delete(sessionKey);
|
|
254
|
+
return queue.length;
|
|
135
255
|
}
|
|
136
256
|
bindThread(sessionKey, threadId) {
|
|
137
|
-
this.
|
|
138
|
-
|
|
257
|
+
this.patchDurableSession(sessionKey, {
|
|
258
|
+
codexThreadId: threadId,
|
|
139
259
|
});
|
|
140
260
|
}
|
|
141
261
|
setTelegramTopicName(sessionKey, topicName) {
|
|
142
|
-
this.
|
|
262
|
+
this.patchDurableSession(sessionKey, {
|
|
263
|
+
telegramTopicName: topicName,
|
|
264
|
+
});
|
|
143
265
|
}
|
|
144
266
|
setRuntimeState(sessionKey, state) {
|
|
145
|
-
this.
|
|
146
|
-
runtime_status: state.status,
|
|
147
|
-
runtime_status_detail: state.detail,
|
|
148
|
-
runtime_status_updated_at: state.updatedAt,
|
|
149
|
-
active_turn_id: state.activeTurnId,
|
|
150
|
-
});
|
|
267
|
+
this.runtimeStateBySession.set(sessionKey, state);
|
|
151
268
|
}
|
|
152
269
|
setOutputMessage(sessionKey, messageId) {
|
|
153
|
-
|
|
270
|
+
if (messageId == null) {
|
|
271
|
+
this.outputMessageBySession.delete(sessionKey);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
this.outputMessageBySession.set(sessionKey, messageId);
|
|
154
275
|
}
|
|
155
276
|
setCwd(sessionKey, cwd) {
|
|
156
|
-
this.
|
|
277
|
+
this.patchDurableSession(sessionKey, { cwd });
|
|
157
278
|
}
|
|
158
279
|
setModel(sessionKey, model) {
|
|
159
|
-
this.
|
|
280
|
+
this.patchDurableSession(sessionKey, { model });
|
|
160
281
|
}
|
|
161
282
|
setSandboxMode(sessionKey, sandboxMode) {
|
|
162
|
-
this.
|
|
163
|
-
sandbox_mode: sandboxMode,
|
|
164
|
-
});
|
|
283
|
+
this.patchDurableSession(sessionKey, { sandboxMode });
|
|
165
284
|
}
|
|
166
285
|
setApprovalPolicy(sessionKey, approvalPolicy) {
|
|
167
|
-
this.
|
|
286
|
+
this.patchDurableSession(sessionKey, { approvalPolicy });
|
|
168
287
|
}
|
|
169
288
|
setReasoningEffort(sessionKey, reasoningEffort) {
|
|
170
|
-
this.
|
|
289
|
+
this.patchDurableSession(sessionKey, { reasoningEffort });
|
|
171
290
|
}
|
|
172
291
|
setWebSearchMode(sessionKey, webSearchMode) {
|
|
173
|
-
this.
|
|
292
|
+
this.patchDurableSession(sessionKey, { webSearchMode });
|
|
174
293
|
}
|
|
175
294
|
setNetworkAccessEnabled(sessionKey, enabled) {
|
|
176
|
-
this.
|
|
295
|
+
this.patchDurableSession(sessionKey, { networkAccessEnabled: enabled });
|
|
177
296
|
}
|
|
178
297
|
setSkipGitRepoCheck(sessionKey, skip) {
|
|
179
|
-
this.
|
|
298
|
+
this.patchDurableSession(sessionKey, { skipGitRepoCheck: skip });
|
|
180
299
|
}
|
|
181
300
|
setAdditionalDirectories(sessionKey, directories) {
|
|
182
|
-
this.
|
|
301
|
+
this.patchDurableSession(sessionKey, { additionalDirectories: [...directories] });
|
|
183
302
|
}
|
|
184
303
|
setOutputSchema(sessionKey, outputSchema) {
|
|
185
|
-
this.
|
|
304
|
+
this.patchDurableSession(sessionKey, { outputSchema });
|
|
186
305
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (entries.length === 0)
|
|
190
|
-
return;
|
|
191
|
-
const setSql = entries.map(([key]) => `${key} = ?`).join(", ");
|
|
192
|
-
const values = entries.map(([, value]) => value);
|
|
193
|
-
values.push(new Date().toISOString(), sessionKey);
|
|
194
|
-
this.db.prepare(`UPDATE sessions SET ${setSql}, updated_at = ? WHERE session_key = ?`).run(...values);
|
|
306
|
+
patchDurableSession(sessionKey, patch) {
|
|
307
|
+
this.storage.patchSession(sessionKey, patch);
|
|
195
308
|
}
|
|
196
309
|
}
|
|
197
310
|
export function makeSessionKey(chatId, messageThreadId) {
|
|
198
311
|
return messageThreadId == null ? String(chatId) : `${chatId}:${messageThreadId}`;
|
|
199
312
|
}
|
|
200
|
-
function
|
|
313
|
+
function mapStoredSession(stored, runtimeState, outputMessageId) {
|
|
314
|
+
if (!stored)
|
|
315
|
+
return null;
|
|
316
|
+
const runtime = runtimeState ?? {
|
|
317
|
+
status: "idle",
|
|
318
|
+
detail: null,
|
|
319
|
+
updatedAt: stored.updatedAt,
|
|
320
|
+
activeTurnId: null,
|
|
321
|
+
};
|
|
201
322
|
return {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
sandboxMode: normalizeSandboxMode(row.sandbox_mode),
|
|
210
|
-
approvalPolicy: normalizeApprovalPolicy(row.approval_policy),
|
|
211
|
-
reasoningEffort: normalizeReasoningEffort(row.reasoning_effort),
|
|
212
|
-
webSearchMode: normalizeWebSearchMode(row.web_search_mode),
|
|
213
|
-
networkAccessEnabled: normalizeBoolean(row.network_access_enabled, true),
|
|
214
|
-
skipGitRepoCheck: normalizeBoolean(row.skip_git_repo_check, true),
|
|
215
|
-
additionalDirectories: normalizeStringArray(row.additional_directories),
|
|
216
|
-
outputSchema: normalizeOutputSchema(row.output_schema),
|
|
217
|
-
runtimeStatus: normalizeRuntimeStatus(row.runtime_status, row.active_turn_id),
|
|
218
|
-
runtimeStatusDetail: row.runtime_status_detail ?? null,
|
|
219
|
-
runtimeStatusUpdatedAt: row.runtime_status_updated_at ?? row.updated_at,
|
|
220
|
-
activeTurnId: row.active_turn_id,
|
|
221
|
-
outputMessageId: row.output_message_id,
|
|
222
|
-
createdAt: row.created_at,
|
|
223
|
-
updatedAt: row.updated_at,
|
|
323
|
+
...stored,
|
|
324
|
+
additionalDirectories: [...stored.additionalDirectories],
|
|
325
|
+
runtimeStatus: runtime.status,
|
|
326
|
+
runtimeStatusDetail: runtime.detail,
|
|
327
|
+
runtimeStatusUpdatedAt: runtime.updatedAt,
|
|
328
|
+
activeTurnId: runtime.activeTurnId,
|
|
329
|
+
outputMessageId: outputMessageId ?? null,
|
|
224
330
|
};
|
|
225
331
|
}
|
|
226
|
-
function
|
|
332
|
+
function cloneQueuedInput(input) {
|
|
227
333
|
return {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
text: row.text,
|
|
231
|
-
input: parseStoredCodexInput(row.input_json, row.text),
|
|
232
|
-
createdAt: row.created_at,
|
|
233
|
-
updatedAt: row.updated_at,
|
|
334
|
+
...input,
|
|
335
|
+
input: cloneStoredCodexInput(input.input),
|
|
234
336
|
};
|
|
235
337
|
}
|
|
236
|
-
function
|
|
237
|
-
return
|
|
238
|
-
}
|
|
239
|
-
function normalizeApprovalPolicy(value) {
|
|
240
|
-
return value && isSessionApprovalPolicy(value) ? value : DEFAULT_SESSION_PROFILE.approvalPolicy;
|
|
338
|
+
function cloneStoredCodexInput(input) {
|
|
339
|
+
return typeof input === "string" ? input : input.map((item) => ({ ...item }));
|
|
241
340
|
}
|
|
242
|
-
function
|
|
243
|
-
return value
|
|
341
|
+
function normalizeBindingCodeMode(value) {
|
|
342
|
+
return value === "rebind" ? "rebind" : "bootstrap";
|
|
244
343
|
}
|
|
245
|
-
function
|
|
246
|
-
return value && isSessionWebSearchMode(value) ? value : null;
|
|
247
|
-
}
|
|
248
|
-
function normalizeBoolean(value, fallback) {
|
|
249
|
-
if (value == null)
|
|
250
|
-
return fallback;
|
|
251
|
-
return Number(value) !== 0;
|
|
252
|
-
}
|
|
253
|
-
function normalizeStringArray(value) {
|
|
344
|
+
function normalizeNonNegativeInteger(value) {
|
|
254
345
|
if (!value)
|
|
255
|
-
return
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
if (!Array.isArray(parsed))
|
|
259
|
-
return [];
|
|
260
|
-
return parsed.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
|
|
261
|
-
}
|
|
262
|
-
catch {
|
|
263
|
-
return [];
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
function normalizeOutputSchema(value) {
|
|
267
|
-
if (!value?.trim())
|
|
268
|
-
return null;
|
|
269
|
-
try {
|
|
270
|
-
const parsed = JSON.parse(value);
|
|
271
|
-
return isPlainObject(parsed) ? JSON.stringify(parsed) : null;
|
|
272
|
-
}
|
|
273
|
-
catch {
|
|
274
|
-
return null;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
function normalizeRuntimeStatus(value, activeTurnId) {
|
|
278
|
-
switch (value) {
|
|
279
|
-
case "idle":
|
|
280
|
-
case "preparing":
|
|
281
|
-
case "running":
|
|
282
|
-
case "failed":
|
|
283
|
-
return value;
|
|
284
|
-
default:
|
|
285
|
-
return activeTurnId ? "running" : "idle";
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
function parseStoredCodexInput(inputJson, fallbackText) {
|
|
289
|
-
if (!inputJson)
|
|
290
|
-
return fallbackText;
|
|
291
|
-
try {
|
|
292
|
-
const parsed = JSON.parse(inputJson);
|
|
293
|
-
return normalizeStoredCodexInput(parsed) ?? fallbackText;
|
|
294
|
-
}
|
|
295
|
-
catch {
|
|
296
|
-
return fallbackText;
|
|
297
|
-
}
|
|
346
|
+
return 0;
|
|
347
|
+
const parsed = Number(value);
|
|
348
|
+
return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : 0;
|
|
298
349
|
}
|
|
299
|
-
function
|
|
300
|
-
if (
|
|
301
|
-
return value;
|
|
302
|
-
if (!Array.isArray(value))
|
|
350
|
+
function normalizeOptionalUserId(value) {
|
|
351
|
+
if (value == null)
|
|
303
352
|
return null;
|
|
304
|
-
const
|
|
305
|
-
|
|
306
|
-
if (!isPlainObject(item))
|
|
307
|
-
return null;
|
|
308
|
-
if (item.type === "text" && typeof item.text === "string") {
|
|
309
|
-
items.push({ type: "text", text: item.text });
|
|
310
|
-
}
|
|
311
|
-
else if (item.type === "local_image" && typeof item.path === "string") {
|
|
312
|
-
items.push({ type: "local_image", path: item.path });
|
|
313
|
-
}
|
|
314
|
-
else {
|
|
315
|
-
return null;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
return items;
|
|
353
|
+
const parsed = Number(value);
|
|
354
|
+
return Number.isSafeInteger(parsed) ? parsed : null;
|
|
319
355
|
}
|
|
320
356
|
export function formatCodexInputPreview(input) {
|
|
321
357
|
if (typeof input === "string")
|
|
@@ -323,6 +359,3 @@ export function formatCodexInputPreview(input) {
|
|
|
323
359
|
const parts = input.map((item) => (item.type === "text" ? item.text : `[image: ${item.path}]`));
|
|
324
360
|
return parts.join(" ").trim() || "[image]";
|
|
325
361
|
}
|
|
326
|
-
function isPlainObject(value) {
|
|
327
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
328
|
-
}
|
|
@@ -11,6 +11,11 @@ export function escapeHtml(value) {
|
|
|
11
11
|
.replaceAll("<", "<")
|
|
12
12
|
.replaceAll(">", ">");
|
|
13
13
|
}
|
|
14
|
+
function escapeHtmlAttribute(value) {
|
|
15
|
+
return escapeHtml(value)
|
|
16
|
+
.replaceAll('"', """)
|
|
17
|
+
.replaceAll("'", "'");
|
|
18
|
+
}
|
|
14
19
|
export function renderMarkdownForTelegram(markdown) {
|
|
15
20
|
const tokens = md.parse(markdown, {});
|
|
16
21
|
const html = renderTokens(tokens).replace(/\n{3,}/g, "\n\n").trim();
|
|
@@ -95,6 +100,7 @@ function renderTokens(tokens) {
|
|
|
95
100
|
}
|
|
96
101
|
function renderInline(tokens) {
|
|
97
102
|
let out = "";
|
|
103
|
+
const linkStack = [];
|
|
98
104
|
for (const token of tokens) {
|
|
99
105
|
switch (token.type) {
|
|
100
106
|
case "text":
|
|
@@ -122,12 +128,18 @@ function renderInline(tokens) {
|
|
|
122
128
|
out += "</s>";
|
|
123
129
|
break;
|
|
124
130
|
case "link_open": {
|
|
125
|
-
const href = token.attrGet("href");
|
|
126
|
-
|
|
131
|
+
const href = sanitizeTelegramHref(token.attrGet("href"));
|
|
132
|
+
const opened = Boolean(href);
|
|
133
|
+
linkStack.push(opened);
|
|
134
|
+
if (href) {
|
|
135
|
+
out += `<a href="${escapeHtmlAttribute(href)}">`;
|
|
136
|
+
}
|
|
127
137
|
break;
|
|
128
138
|
}
|
|
129
139
|
case "link_close":
|
|
130
|
-
|
|
140
|
+
if (linkStack.pop()) {
|
|
141
|
+
out += "</a>";
|
|
142
|
+
}
|
|
131
143
|
break;
|
|
132
144
|
case "softbreak":
|
|
133
145
|
case "hardbreak":
|
|
@@ -144,3 +156,28 @@ function renderInline(tokens) {
|
|
|
144
156
|
}
|
|
145
157
|
return out;
|
|
146
158
|
}
|
|
159
|
+
function sanitizeTelegramHref(value) {
|
|
160
|
+
if (!value)
|
|
161
|
+
return null;
|
|
162
|
+
const href = value.trim();
|
|
163
|
+
if (!href)
|
|
164
|
+
return null;
|
|
165
|
+
try {
|
|
166
|
+
const url = new URL(href);
|
|
167
|
+
return isAllowedTelegramProtocol(url.protocol) ? url.toString() : null;
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function isAllowedTelegramProtocol(protocol) {
|
|
174
|
+
switch (protocol) {
|
|
175
|
+
case "http:":
|
|
176
|
+
case "https:":
|
|
177
|
+
case "mailto:":
|
|
178
|
+
case "tg:":
|
|
179
|
+
return true;
|
|
180
|
+
default:
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "telecodex",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Telegram bridge for local Codex.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
43
43
|
"dev": "NODE_OPTIONS=--disable-warning=ExperimentalWarning tsx src/cli.ts",
|
|
44
|
-
"build": "tsc -p tsconfig.json",
|
|
44
|
+
"build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json",
|
|
45
45
|
"prepack": "npm run build",
|
|
46
46
|
"start": "node --disable-warning=ExperimentalWarning dist/cli.js",
|
|
47
47
|
"check": "tsc -p tsconfig.json --noEmit",
|