@foxden-app/foxclaw 0.2.0

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.
Files changed (126) hide show
  1. package/.env.example +36 -0
  2. package/LICENSE +22 -0
  3. package/README.md +244 -0
  4. package/README_EN.md +244 -0
  5. package/dist/channels/bridge_messaging_router.d.ts +27 -0
  6. package/dist/channels/bridge_messaging_router.js +85 -0
  7. package/dist/channels/telegram/telegram_channel_adapter.d.ts +12 -0
  8. package/dist/channels/telegram/telegram_channel_adapter.js +21 -0
  9. package/dist/channels/telegram/telegram_messaging_port.d.ts +25 -0
  10. package/dist/channels/telegram/telegram_messaging_port.js +51 -0
  11. package/dist/channels/weixin/account_store.d.ts +15 -0
  12. package/dist/channels/weixin/account_store.js +54 -0
  13. package/dist/channels/weixin/ilink/aes_ecb.d.ts +3 -0
  14. package/dist/channels/weixin/ilink/aes_ecb.js +12 -0
  15. package/dist/channels/weixin/ilink/api.d.ts +44 -0
  16. package/dist/channels/weixin/ilink/api.js +187 -0
  17. package/dist/channels/weixin/ilink/cdn_upload.d.ts +11 -0
  18. package/dist/channels/weixin/ilink/cdn_upload.js +60 -0
  19. package/dist/channels/weixin/ilink/cdn_url.d.ts +7 -0
  20. package/dist/channels/weixin/ilink/cdn_url.js +7 -0
  21. package/dist/channels/weixin/ilink/constants.d.ts +7 -0
  22. package/dist/channels/weixin/ilink/constants.js +27 -0
  23. package/dist/channels/weixin/ilink/context.d.ts +13 -0
  24. package/dist/channels/weixin/ilink/context.js +13 -0
  25. package/dist/channels/weixin/ilink/login_qr.d.ts +34 -0
  26. package/dist/channels/weixin/ilink/login_qr.js +233 -0
  27. package/dist/channels/weixin/ilink/media_image.d.ts +11 -0
  28. package/dist/channels/weixin/ilink/media_image.js +44 -0
  29. package/dist/channels/weixin/ilink/mime.d.ts +3 -0
  30. package/dist/channels/weixin/ilink/mime.js +36 -0
  31. package/dist/channels/weixin/ilink/pic_decrypt.d.ts +2 -0
  32. package/dist/channels/weixin/ilink/pic_decrypt.js +56 -0
  33. package/dist/channels/weixin/ilink/random.d.ts +2 -0
  34. package/dist/channels/weixin/ilink/random.js +7 -0
  35. package/dist/channels/weixin/ilink/redact.d.ts +4 -0
  36. package/dist/channels/weixin/ilink/redact.js +34 -0
  37. package/dist/channels/weixin/ilink/runtime_attach.d.ts +3 -0
  38. package/dist/channels/weixin/ilink/runtime_attach.js +13 -0
  39. package/dist/channels/weixin/ilink/send.d.ts +21 -0
  40. package/dist/channels/weixin/ilink/send.js +108 -0
  41. package/dist/channels/weixin/ilink/session_guard.d.ts +6 -0
  42. package/dist/channels/weixin/ilink/session_guard.js +39 -0
  43. package/dist/channels/weixin/ilink/types.d.ts +155 -0
  44. package/dist/channels/weixin/ilink/types.js +10 -0
  45. package/dist/channels/weixin/ilink/upload.d.ts +15 -0
  46. package/dist/channels/weixin/ilink/upload.js +75 -0
  47. package/dist/channels/weixin/sync_buf_store.d.ts +3 -0
  48. package/dist/channels/weixin/sync_buf_store.js +19 -0
  49. package/dist/channels/weixin/weixin_channel_adapter.d.ts +18 -0
  50. package/dist/channels/weixin/weixin_channel_adapter.js +273 -0
  51. package/dist/channels/weixin/weixin_messaging_port.d.ts +29 -0
  52. package/dist/channels/weixin/weixin_messaging_port.js +113 -0
  53. package/dist/codex_app/client.d.ts +176 -0
  54. package/dist/codex_app/client.js +1230 -0
  55. package/dist/codex_app/deeplink.d.ts +7 -0
  56. package/dist/codex_app/deeplink.js +29 -0
  57. package/dist/codex_app/local_usage.d.ts +16 -0
  58. package/dist/codex_app/local_usage.js +123 -0
  59. package/dist/config.d.ts +44 -0
  60. package/dist/config.js +131 -0
  61. package/dist/controller/access.d.ts +11 -0
  62. package/dist/controller/access.js +33 -0
  63. package/dist/controller/activity.d.ts +62 -0
  64. package/dist/controller/activity.js +330 -0
  65. package/dist/controller/commands.d.ts +6 -0
  66. package/dist/controller/commands.js +17 -0
  67. package/dist/controller/controller.d.ts +326 -0
  68. package/dist/controller/controller.js +7503 -0
  69. package/dist/controller/observer.d.ts +16 -0
  70. package/dist/controller/observer.js +98 -0
  71. package/dist/controller/presentation.d.ts +80 -0
  72. package/dist/controller/presentation.js +568 -0
  73. package/dist/controller/service_tier.d.ts +9 -0
  74. package/dist/controller/service_tier.js +32 -0
  75. package/dist/controller/session_observer.d.ts +22 -0
  76. package/dist/controller/session_observer.js +259 -0
  77. package/dist/controller/status.d.ts +10 -0
  78. package/dist/controller/status.js +28 -0
  79. package/dist/core/bridge_scope.d.ts +18 -0
  80. package/dist/core/bridge_scope.js +46 -0
  81. package/dist/core/channel_port.d.ts +15 -0
  82. package/dist/core/channel_port.js +1 -0
  83. package/dist/i18n.d.ts +1108 -0
  84. package/dist/i18n.js +1154 -0
  85. package/dist/lock.d.ts +7 -0
  86. package/dist/lock.js +80 -0
  87. package/dist/logger.d.ts +12 -0
  88. package/dist/logger.js +57 -0
  89. package/dist/main.d.ts +2 -0
  90. package/dist/main.js +236 -0
  91. package/dist/runtime.d.ts +3 -0
  92. package/dist/runtime.js +14 -0
  93. package/dist/store/database.d.ts +79 -0
  94. package/dist/store/database.js +489 -0
  95. package/dist/store/migrate_bridge_scope.d.ts +6 -0
  96. package/dist/store/migrate_bridge_scope.js +59 -0
  97. package/dist/telegram/addressing.d.ts +33 -0
  98. package/dist/telegram/addressing.js +57 -0
  99. package/dist/telegram/api.d.ts +14 -0
  100. package/dist/telegram/api.js +89 -0
  101. package/dist/telegram/gateway.d.ts +76 -0
  102. package/dist/telegram/gateway.js +383 -0
  103. package/dist/telegram/media.d.ts +34 -0
  104. package/dist/telegram/media.js +180 -0
  105. package/dist/telegram/rendering.d.ts +10 -0
  106. package/dist/telegram/rendering.js +21 -0
  107. package/dist/telegram/scope.d.ts +6 -0
  108. package/dist/telegram/scope.js +24 -0
  109. package/dist/telegram/text.d.ts +7 -0
  110. package/dist/telegram/text.js +47 -0
  111. package/dist/types.d.ts +343 -0
  112. package/dist/types.js +1 -0
  113. package/docs/agent-assisted-install.md +84 -0
  114. package/docs/install-for-beginners.md +287 -0
  115. package/docs/troubleshooting.md +239 -0
  116. package/package.json +62 -0
  117. package/scripts/doctor.sh +3 -0
  118. package/scripts/launchd/install.sh +54 -0
  119. package/scripts/status.sh +3 -0
  120. package/scripts/systemd/install.sh +83 -0
  121. package/scripts/systemd/uninstall.sh +15 -0
  122. package/skills/foxclaw/SKILL.md +167 -0
  123. package/skills/foxclaw/agents/openai.yaml +4 -0
  124. package/skills/foxclaw/references/telegram-setup.md +93 -0
  125. package/skills/foxclaw/scripts/bootstrap_host.py +350 -0
  126. package/skills/foxclaw/scripts/bootstrap_remote.py +67 -0
@@ -0,0 +1,489 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+ import { DatabaseSync } from 'node:sqlite';
4
+ import { migrateLegacyBridgeScopeIds } from './migrate_bridge_scope.js';
5
+ export class BridgeStore {
6
+ db;
7
+ constructor(dbPath) {
8
+ fs.mkdirSync(path.dirname(dbPath), { recursive: true });
9
+ this.db = new DatabaseSync(dbPath);
10
+ this.db.exec(`
11
+ CREATE TABLE IF NOT EXISTS telegram_offsets (
12
+ bot_key TEXT PRIMARY KEY,
13
+ update_id INTEGER NOT NULL
14
+ );
15
+ CREATE TABLE IF NOT EXISTS chat_bindings (
16
+ chat_id TEXT PRIMARY KEY,
17
+ thread_id TEXT NOT NULL,
18
+ cwd TEXT,
19
+ updated_at INTEGER NOT NULL
20
+ );
21
+ CREATE TABLE IF NOT EXISTS chat_settings (
22
+ chat_id TEXT PRIMARY KEY,
23
+ model TEXT,
24
+ reasoning_effort TEXT,
25
+ locale TEXT,
26
+ access_preset TEXT,
27
+ collaboration_mode TEXT,
28
+ service_tier TEXT,
29
+ active_turn_message_mode TEXT,
30
+ updated_at INTEGER NOT NULL
31
+ );
32
+ CREATE TABLE IF NOT EXISTS thread_cache (
33
+ chat_id TEXT NOT NULL,
34
+ idx INTEGER NOT NULL,
35
+ thread_id TEXT NOT NULL,
36
+ name TEXT,
37
+ preview TEXT NOT NULL,
38
+ cwd TEXT,
39
+ model_provider TEXT,
40
+ status TEXT NOT NULL DEFAULT 'idle',
41
+ archived INTEGER NOT NULL DEFAULT 0,
42
+ updated_at INTEGER NOT NULL,
43
+ PRIMARY KEY (chat_id, idx)
44
+ );
45
+ CREATE TABLE IF NOT EXISTS pending_approvals (
46
+ local_id TEXT PRIMARY KEY,
47
+ server_request_id TEXT NOT NULL,
48
+ kind TEXT NOT NULL,
49
+ chat_id TEXT NOT NULL,
50
+ thread_id TEXT NOT NULL,
51
+ turn_id TEXT NOT NULL,
52
+ item_id TEXT NOT NULL,
53
+ approval_id TEXT,
54
+ reason TEXT,
55
+ command TEXT,
56
+ cwd TEXT,
57
+ payload_json TEXT,
58
+ message_id INTEGER,
59
+ created_at INTEGER NOT NULL,
60
+ resolved_at INTEGER
61
+ );
62
+ CREATE TABLE IF NOT EXISTS active_turn_previews (
63
+ turn_id TEXT PRIMARY KEY,
64
+ scope_id TEXT NOT NULL,
65
+ thread_id TEXT NOT NULL,
66
+ message_id INTEGER NOT NULL,
67
+ created_at INTEGER NOT NULL,
68
+ updated_at INTEGER NOT NULL
69
+ );
70
+ CREATE TABLE IF NOT EXISTS pending_user_inputs (
71
+ local_id TEXT PRIMARY KEY,
72
+ server_request_id TEXT NOT NULL,
73
+ chat_id TEXT NOT NULL,
74
+ thread_id TEXT NOT NULL,
75
+ turn_id TEXT,
76
+ item_id TEXT NOT NULL,
77
+ message_id INTEGER,
78
+ questions_json TEXT NOT NULL,
79
+ answers_json TEXT NOT NULL,
80
+ current_question_index INTEGER NOT NULL,
81
+ awaiting_free_text INTEGER NOT NULL DEFAULT 0,
82
+ status TEXT NOT NULL DEFAULT 'pending',
83
+ created_at INTEGER NOT NULL,
84
+ submitted_at INTEGER,
85
+ resolved_at INTEGER
86
+ );
87
+ CREATE TABLE IF NOT EXISTS pending_user_input_messages (
88
+ input_local_id TEXT NOT NULL,
89
+ question_index INTEGER NOT NULL,
90
+ message_id INTEGER NOT NULL,
91
+ message_kind TEXT NOT NULL,
92
+ created_at INTEGER NOT NULL,
93
+ PRIMARY KEY (input_local_id, question_index, message_kind)
94
+ );
95
+ CREATE TABLE IF NOT EXISTS audit_logs (
96
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
97
+ direction TEXT NOT NULL,
98
+ chat_id TEXT NOT NULL,
99
+ event_type TEXT NOT NULL,
100
+ summary TEXT NOT NULL,
101
+ created_at INTEGER NOT NULL
102
+ );
103
+ CREATE TABLE IF NOT EXISTS weixin_context_tokens (
104
+ scope_id TEXT PRIMARY KEY,
105
+ context_token TEXT NOT NULL,
106
+ updated_at INTEGER NOT NULL
107
+ );
108
+ CREATE TABLE IF NOT EXISTS codex_auth_candidates (
109
+ name TEXT PRIMARY KEY,
110
+ disabled INTEGER NOT NULL DEFAULT 0,
111
+ updated_at INTEGER NOT NULL
112
+ );
113
+ `);
114
+ this.ensureColumn('thread_cache', 'name', 'TEXT');
115
+ this.ensureColumn('thread_cache', 'model_provider', 'TEXT');
116
+ this.ensureColumn('thread_cache', 'status', "TEXT NOT NULL DEFAULT 'idle'");
117
+ this.ensureColumn('thread_cache', 'archived', 'INTEGER NOT NULL DEFAULT 0');
118
+ this.ensureColumn('chat_settings', 'locale', 'TEXT');
119
+ this.ensureColumn('chat_settings', 'access_preset', 'TEXT');
120
+ this.ensureColumn('chat_settings', 'collaboration_mode', 'TEXT');
121
+ this.ensureColumn('chat_settings', 'service_tier', 'TEXT');
122
+ this.ensureColumn('chat_settings', 'active_turn_message_mode', 'TEXT');
123
+ this.ensureColumn('pending_approvals', 'payload_json', 'TEXT');
124
+ this.ensureColumn('pending_user_inputs', 'status', "TEXT NOT NULL DEFAULT 'pending'");
125
+ this.ensureColumn('pending_user_inputs', 'submitted_at', 'INTEGER');
126
+ migrateLegacyBridgeScopeIds(this.db);
127
+ }
128
+ getTelegramOffset(botKey) {
129
+ const row = this.db.prepare('SELECT update_id FROM telegram_offsets WHERE bot_key = ?').get(botKey);
130
+ return row?.update_id ?? 0;
131
+ }
132
+ setTelegramOffset(botKey, updateId) {
133
+ this.db.prepare(`
134
+ INSERT INTO telegram_offsets (bot_key, update_id)
135
+ VALUES (?, ?)
136
+ ON CONFLICT(bot_key) DO UPDATE SET update_id = excluded.update_id
137
+ `).run(botKey, updateId);
138
+ }
139
+ getBinding(chatId) {
140
+ const row = this.db.prepare('SELECT chat_id, thread_id, cwd, updated_at FROM chat_bindings WHERE chat_id = ?').get(chatId);
141
+ if (!row)
142
+ return null;
143
+ return {
144
+ chatId: String(row.chat_id),
145
+ threadId: String(row.thread_id),
146
+ cwd: row.cwd === null ? null : String(row.cwd),
147
+ updatedAt: Number(row.updated_at)
148
+ };
149
+ }
150
+ setBinding(chatId, threadId, cwd) {
151
+ this.db.prepare(`
152
+ INSERT INTO chat_bindings (chat_id, thread_id, cwd, updated_at)
153
+ VALUES (?, ?, ?, ?)
154
+ ON CONFLICT(chat_id) DO UPDATE SET thread_id = excluded.thread_id, cwd = excluded.cwd, updated_at = excluded.updated_at
155
+ `).run(chatId, threadId, cwd, Date.now());
156
+ }
157
+ clearBinding(chatId) {
158
+ this.db.prepare('DELETE FROM chat_bindings WHERE chat_id = ?').run(chatId);
159
+ }
160
+ getChatSettings(chatId) {
161
+ const row = this.db.prepare('SELECT chat_id, model, reasoning_effort, locale, access_preset, collaboration_mode, service_tier, active_turn_message_mode, updated_at FROM chat_settings WHERE chat_id = ?').get(chatId);
162
+ if (!row)
163
+ return null;
164
+ return {
165
+ chatId: String(row.chat_id),
166
+ model: row.model === null ? null : String(row.model),
167
+ reasoningEffort: row.reasoning_effort === null ? null : String(row.reasoning_effort),
168
+ locale: row.locale === null ? null : String(row.locale),
169
+ accessPreset: row.access_preset === null ? null : String(row.access_preset),
170
+ collaborationMode: normalizeCollaborationMode(row.collaboration_mode),
171
+ serviceTier: row.service_tier === null ? null : String(row.service_tier),
172
+ activeTurnMessageMode: normalizeActiveTurnMessageMode(row.active_turn_message_mode),
173
+ updatedAt: Number(row.updated_at),
174
+ };
175
+ }
176
+ setChatSettings(chatId, model, reasoningEffort, locale) {
177
+ const current = this.getChatSettings(chatId);
178
+ const nextLocale = locale === undefined ? current?.locale ?? null : locale;
179
+ this.writeChatSettings(chatId, model, reasoningEffort, nextLocale, current?.accessPreset ?? null, current?.collaborationMode ?? null, current?.serviceTier ?? null, current?.activeTurnMessageMode ?? null);
180
+ }
181
+ setChatLocale(chatId, locale) {
182
+ const current = this.getChatSettings(chatId);
183
+ this.writeChatSettings(chatId, current?.model ?? null, current?.reasoningEffort ?? null, locale, current?.accessPreset ?? null, current?.collaborationMode ?? null, current?.serviceTier ?? null, current?.activeTurnMessageMode ?? null);
184
+ }
185
+ setChatAccessPreset(chatId, accessPreset) {
186
+ const current = this.getChatSettings(chatId);
187
+ this.writeChatSettings(chatId, current?.model ?? null, current?.reasoningEffort ?? null, current?.locale ?? null, accessPreset, current?.collaborationMode ?? null, current?.serviceTier ?? null, current?.activeTurnMessageMode ?? null);
188
+ }
189
+ setChatCollaborationMode(chatId, collaborationMode) {
190
+ const current = this.getChatSettings(chatId);
191
+ this.writeChatSettings(chatId, current?.model ?? null, current?.reasoningEffort ?? null, current?.locale ?? null, current?.accessPreset ?? null, collaborationMode, current?.serviceTier ?? null, current?.activeTurnMessageMode ?? null);
192
+ }
193
+ setChatServiceTier(chatId, serviceTier) {
194
+ const current = this.getChatSettings(chatId);
195
+ this.writeChatSettings(chatId, current?.model ?? null, current?.reasoningEffort ?? null, current?.locale ?? null, current?.accessPreset ?? null, current?.collaborationMode ?? null, serviceTier, current?.activeTurnMessageMode ?? null);
196
+ }
197
+ setChatActiveTurnMessageMode(chatId, activeTurnMessageMode) {
198
+ const current = this.getChatSettings(chatId);
199
+ this.writeChatSettings(chatId, current?.model ?? null, current?.reasoningEffort ?? null, current?.locale ?? null, current?.accessPreset ?? null, current?.collaborationMode ?? null, current?.serviceTier ?? null, activeTurnMessageMode);
200
+ }
201
+ findChatIdByThreadId(threadId) {
202
+ const row = this.db.prepare('SELECT chat_id FROM chat_bindings WHERE thread_id = ?').get(threadId);
203
+ return row ? String(row.chat_id) : null;
204
+ }
205
+ findAllChatIdsByThreadId(threadId) {
206
+ const rows = this.db.prepare('SELECT chat_id FROM chat_bindings WHERE thread_id = ? ORDER BY updated_at ASC').all(threadId);
207
+ return rows.map(row => String(row.chat_id));
208
+ }
209
+ countBindings() {
210
+ const row = this.db.prepare('SELECT COUNT(*) AS count FROM chat_bindings').get();
211
+ return Number(row.count);
212
+ }
213
+ cacheThreadList(chatId, threads) {
214
+ const deleteStmt = this.db.prepare('DELETE FROM thread_cache WHERE chat_id = ?');
215
+ const insertStmt = this.db.prepare(`
216
+ INSERT INTO thread_cache (chat_id, idx, thread_id, name, preview, cwd, model_provider, status, archived, updated_at)
217
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
218
+ `);
219
+ deleteStmt.run(chatId);
220
+ threads.forEach((thread, index) => {
221
+ const idx = typeof thread.listIndex === 'number' ? thread.listIndex : index + 1;
222
+ insertStmt.run(chatId, idx, thread.threadId, thread.name, thread.preview, thread.cwd, thread.modelProvider, thread.status, thread.archived ? 1 : 0, thread.updatedAt);
223
+ });
224
+ }
225
+ getCachedThread(chatId, index) {
226
+ const row = this.db.prepare(`
227
+ SELECT idx, thread_id, name, preview, cwd, model_provider, status, archived, updated_at
228
+ FROM thread_cache
229
+ WHERE chat_id = ? AND idx = ?
230
+ `).get(chatId, index);
231
+ if (!row)
232
+ return null;
233
+ return {
234
+ index: Number(row.idx),
235
+ threadId: String(row.thread_id),
236
+ name: row.name === null ? null : String(row.name),
237
+ preview: String(row.preview),
238
+ cwd: row.cwd === null ? null : String(row.cwd),
239
+ modelProvider: row.model_provider === null ? null : String(row.model_provider),
240
+ status: String(row.status),
241
+ archived: Boolean(row.archived),
242
+ updatedAt: Number(row.updated_at),
243
+ };
244
+ }
245
+ listCachedThreads(chatId) {
246
+ const rows = this.db.prepare(`
247
+ SELECT idx, thread_id, name, preview, cwd, model_provider, status, archived, updated_at
248
+ FROM thread_cache
249
+ WHERE chat_id = ?
250
+ ORDER BY idx ASC
251
+ `).all(chatId);
252
+ return rows.map((row) => ({
253
+ index: Number(row.idx),
254
+ threadId: String(row.thread_id),
255
+ name: row.name === null ? null : String(row.name),
256
+ preview: String(row.preview),
257
+ cwd: row.cwd === null ? null : String(row.cwd),
258
+ modelProvider: row.model_provider === null ? null : String(row.model_provider),
259
+ status: String(row.status),
260
+ archived: Boolean(row.archived),
261
+ updatedAt: Number(row.updated_at),
262
+ }));
263
+ }
264
+ savePendingApproval(record) {
265
+ this.db.prepare(`
266
+ INSERT INTO pending_approvals (
267
+ local_id, server_request_id, kind, chat_id, thread_id, turn_id, item_id, approval_id, reason, command, cwd, payload_json, message_id, created_at, resolved_at
268
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
269
+ `).run(record.localId, record.serverRequestId, record.kind, record.chatId, record.threadId, record.turnId, record.itemId, record.approvalId, record.reason, record.command, record.cwd, record.payloadJson, record.messageId, record.createdAt, record.resolvedAt);
270
+ }
271
+ updatePendingApprovalMessage(localId, messageId) {
272
+ this.db.prepare('UPDATE pending_approvals SET message_id = ? WHERE local_id = ?').run(messageId, localId);
273
+ }
274
+ getPendingApproval(localId) {
275
+ const row = this.db.prepare('SELECT * FROM pending_approvals WHERE local_id = ?').get(localId);
276
+ if (!row)
277
+ return null;
278
+ return this.mapApproval(row);
279
+ }
280
+ getPendingApprovalByServerRequestId(serverRequestId) {
281
+ const row = this.db.prepare(`
282
+ SELECT * FROM pending_approvals
283
+ WHERE server_request_id = ? AND resolved_at IS NULL
284
+ ORDER BY created_at DESC
285
+ LIMIT 1
286
+ `).get(serverRequestId);
287
+ if (!row)
288
+ return null;
289
+ return this.mapApproval(row);
290
+ }
291
+ markApprovalResolved(localId) {
292
+ this.db.prepare('UPDATE pending_approvals SET resolved_at = ? WHERE local_id = ?').run(Date.now(), localId);
293
+ }
294
+ countPendingApprovals() {
295
+ const row = this.db.prepare('SELECT COUNT(*) AS count FROM pending_approvals WHERE resolved_at IS NULL').get();
296
+ return Number(row.count);
297
+ }
298
+ saveActiveTurnPreview(record) {
299
+ const now = Date.now();
300
+ this.db.prepare('DELETE FROM active_turn_previews WHERE turn_id = ? OR scope_id = ?').run(record.turnId, record.scopeId);
301
+ this.db.prepare(`
302
+ INSERT INTO active_turn_previews (turn_id, scope_id, thread_id, message_id, created_at, updated_at)
303
+ VALUES (?, ?, ?, ?, ?, ?)
304
+ `).run(record.turnId, record.scopeId, record.threadId, record.messageId, now, now);
305
+ }
306
+ listActiveTurnPreviews() {
307
+ const rows = this.db.prepare(`
308
+ SELECT turn_id, scope_id, thread_id, message_id, created_at, updated_at
309
+ FROM active_turn_previews
310
+ ORDER BY created_at ASC
311
+ `).all();
312
+ return rows.map((row) => ({
313
+ turnId: String(row.turn_id),
314
+ scopeId: String(row.scope_id),
315
+ threadId: String(row.thread_id),
316
+ messageId: Number(row.message_id),
317
+ createdAt: Number(row.created_at),
318
+ updatedAt: Number(row.updated_at),
319
+ }));
320
+ }
321
+ removeActiveTurnPreview(turnId) {
322
+ this.db.prepare('DELETE FROM active_turn_previews WHERE turn_id = ?').run(turnId);
323
+ }
324
+ removeActiveTurnPreviewByMessage(scopeId, messageId) {
325
+ this.db.prepare('DELETE FROM active_turn_previews WHERE scope_id = ? AND message_id = ?').run(scopeId, messageId);
326
+ }
327
+ savePendingUserInput(record) {
328
+ this.db.prepare(`
329
+ INSERT INTO pending_user_inputs (
330
+ local_id, server_request_id, chat_id, thread_id, turn_id, item_id, message_id,
331
+ questions_json, answers_json, current_question_index, awaiting_free_text, status, created_at, submitted_at, resolved_at
332
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
333
+ ON CONFLICT(local_id) DO UPDATE SET
334
+ server_request_id = excluded.server_request_id,
335
+ chat_id = excluded.chat_id,
336
+ thread_id = excluded.thread_id,
337
+ turn_id = excluded.turn_id,
338
+ item_id = excluded.item_id,
339
+ message_id = excluded.message_id,
340
+ questions_json = excluded.questions_json,
341
+ answers_json = excluded.answers_json,
342
+ current_question_index = excluded.current_question_index,
343
+ awaiting_free_text = excluded.awaiting_free_text,
344
+ status = excluded.status,
345
+ created_at = excluded.created_at,
346
+ submitted_at = excluded.submitted_at,
347
+ resolved_at = excluded.resolved_at
348
+ `).run(record.localId, record.serverRequestId, record.chatId, record.threadId, record.turnId ?? '', record.itemId, record.messageId, record.questionsJson, record.answersJson, record.currentQuestionIndex, record.awaitingFreeText ? 1 : 0, record.status, record.createdAt, record.submittedAt, record.resolvedAt);
349
+ }
350
+ updatePendingUserInputMessage(localId, messageId) {
351
+ this.db.prepare('UPDATE pending_user_inputs SET message_id = ? WHERE local_id = ?').run(messageId, localId);
352
+ }
353
+ updatePendingUserInputAnswers(localId, answersJson, currentQuestionIndex, awaitingFreeText = false) {
354
+ this.db.prepare(`
355
+ UPDATE pending_user_inputs
356
+ SET answers_json = ?, current_question_index = ?, awaiting_free_text = ?
357
+ WHERE local_id = ?
358
+ `).run(answersJson, currentQuestionIndex, awaitingFreeText ? 1 : 0, localId);
359
+ }
360
+ markPendingUserInputSubmitted(localId) {
361
+ this.db.prepare(`
362
+ UPDATE pending_user_inputs
363
+ SET status = 'submitted', submitted_at = ?
364
+ WHERE local_id = ? AND resolved_at IS NULL
365
+ `).run(Date.now(), localId);
366
+ }
367
+ markPendingUserInputResolved(localId) {
368
+ this.db.prepare(`
369
+ UPDATE pending_user_inputs
370
+ SET status = 'resolved', resolved_at = ?
371
+ WHERE local_id = ?
372
+ `).run(Date.now(), localId);
373
+ }
374
+ markPendingUserInputInterrupted(localId) {
375
+ this.db.prepare(`
376
+ UPDATE pending_user_inputs
377
+ SET status = 'interrupted', resolved_at = ?
378
+ WHERE local_id = ?
379
+ `).run(Date.now(), localId);
380
+ }
381
+ listPendingUserInputs() {
382
+ const rows = this.db.prepare(`
383
+ SELECT local_id, server_request_id, chat_id, thread_id, turn_id, item_id, message_id,
384
+ questions_json, answers_json, current_question_index, awaiting_free_text, status, created_at, submitted_at, resolved_at
385
+ FROM pending_user_inputs
386
+ WHERE resolved_at IS NULL
387
+ ORDER BY created_at ASC
388
+ `).all();
389
+ return rows.map((row) => this.mapPendingUserInput(row));
390
+ }
391
+ countPendingUserInputs() {
392
+ const row = this.db.prepare('SELECT COUNT(*) AS count FROM pending_user_inputs WHERE resolved_at IS NULL').get();
393
+ return Number(row?.count ?? 0);
394
+ }
395
+ insertAudit(direction, chatId, eventType, summary) {
396
+ this.db.prepare('INSERT INTO audit_logs (direction, chat_id, event_type, summary, created_at) VALUES (?, ?, ?, ?, ?)').run(direction, chatId, eventType, summary, Date.now());
397
+ }
398
+ close() {
399
+ this.db.close();
400
+ }
401
+ mapApproval(row) {
402
+ return {
403
+ localId: String(row.local_id),
404
+ serverRequestId: String(row.server_request_id),
405
+ kind: row.kind === 'fileChange' ? 'fileChange' : row.kind === 'permissions' ? 'permissions' : 'command',
406
+ chatId: String(row.chat_id),
407
+ threadId: String(row.thread_id),
408
+ turnId: String(row.turn_id),
409
+ itemId: String(row.item_id),
410
+ approvalId: row.approval_id === null ? null : String(row.approval_id),
411
+ reason: row.reason === null ? null : String(row.reason),
412
+ command: row.command === null ? null : String(row.command),
413
+ cwd: row.cwd === null ? null : String(row.cwd),
414
+ payloadJson: row.payload_json === null ? null : String(row.payload_json),
415
+ messageId: row.message_id === null ? null : Number(row.message_id),
416
+ createdAt: Number(row.created_at),
417
+ resolvedAt: row.resolved_at === null ? null : Number(row.resolved_at)
418
+ };
419
+ }
420
+ mapPendingUserInput(row) {
421
+ return {
422
+ localId: String(row.local_id),
423
+ serverRequestId: String(row.server_request_id),
424
+ chatId: String(row.chat_id),
425
+ threadId: String(row.thread_id),
426
+ turnId: row.turn_id === null || String(row.turn_id) === '' ? null : String(row.turn_id),
427
+ itemId: String(row.item_id),
428
+ messageId: row.message_id === null ? null : Number(row.message_id),
429
+ questionsJson: String(row.questions_json),
430
+ answersJson: String(row.answers_json),
431
+ currentQuestionIndex: Number(row.current_question_index),
432
+ awaitingFreeText: Boolean(row.awaiting_free_text),
433
+ status: row.status === null ? 'pending' : String(row.status),
434
+ createdAt: Number(row.created_at),
435
+ submittedAt: row.submitted_at === null ? null : Number(row.submitted_at),
436
+ resolvedAt: row.resolved_at === null ? null : Number(row.resolved_at),
437
+ };
438
+ }
439
+ writeChatSettings(chatId, model, reasoningEffort, locale, accessPreset, collaborationMode, serviceTier, activeTurnMessageMode) {
440
+ this.db.prepare(`
441
+ INSERT INTO chat_settings (chat_id, model, reasoning_effort, locale, access_preset, collaboration_mode, service_tier, active_turn_message_mode, updated_at)
442
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
443
+ ON CONFLICT(chat_id) DO UPDATE SET
444
+ model = excluded.model,
445
+ reasoning_effort = excluded.reasoning_effort,
446
+ locale = excluded.locale,
447
+ access_preset = excluded.access_preset,
448
+ collaboration_mode = excluded.collaboration_mode,
449
+ service_tier = excluded.service_tier,
450
+ active_turn_message_mode = excluded.active_turn_message_mode,
451
+ updated_at = excluded.updated_at
452
+ `).run(chatId, model, reasoningEffort, locale, accessPreset, collaborationMode, serviceTier, activeTurnMessageMode, Date.now());
453
+ }
454
+ getWeixinContextToken(scopeId) {
455
+ const row = this.db.prepare('SELECT context_token FROM weixin_context_tokens WHERE scope_id = ?').get(scopeId);
456
+ return row ? String(row.context_token) : null;
457
+ }
458
+ setWeixinContextToken(scopeId, contextToken) {
459
+ this.db.prepare(`
460
+ INSERT INTO weixin_context_tokens (scope_id, context_token, updated_at)
461
+ VALUES (?, ?, ?)
462
+ ON CONFLICT(scope_id) DO UPDATE SET context_token = excluded.context_token, updated_at = excluded.updated_at
463
+ `).run(scopeId, contextToken, Date.now());
464
+ }
465
+ listDisabledCodexAuthCandidateNames() {
466
+ const rows = this.db.prepare('SELECT name FROM codex_auth_candidates WHERE disabled = 1').all();
467
+ return new Set(rows.map(row => String(row.name)));
468
+ }
469
+ setCodexAuthCandidateDisabled(name, disabled) {
470
+ this.db.prepare(`
471
+ INSERT INTO codex_auth_candidates (name, disabled, updated_at)
472
+ VALUES (?, ?, ?)
473
+ ON CONFLICT(name) DO UPDATE SET disabled = excluded.disabled, updated_at = excluded.updated_at
474
+ `).run(name, disabled ? 1 : 0, Date.now());
475
+ }
476
+ ensureColumn(table, column, definition) {
477
+ const columns = this.db.prepare(`PRAGMA table_info(${table})`).all();
478
+ if (columns.some(entry => entry.name === column)) {
479
+ return;
480
+ }
481
+ this.db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
482
+ }
483
+ }
484
+ function normalizeCollaborationMode(value) {
485
+ return value === 'default' || value === 'plan' ? value : null;
486
+ }
487
+ function normalizeActiveTurnMessageMode(value) {
488
+ return value === 'steer' || value === 'queue' ? value : null;
489
+ }
@@ -0,0 +1,6 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ /**
3
+ * One-time migration: prefix legacy Telegram-only keys so future channels (e.g. weixin) cannot collide.
4
+ * Idempotent: rows already prefixed are skipped.
5
+ */
6
+ export declare function migrateLegacyBridgeScopeIds(db: DatabaseSync): void;
@@ -0,0 +1,59 @@
1
+ import { BRIDGE_SCOPE_TELEGRAM_PREFIX, isBridgeScopedKey } from '../core/bridge_scope.js';
2
+ /**
3
+ * One-time migration: prefix legacy Telegram-only keys so future channels (e.g. weixin) cannot collide.
4
+ * Idempotent: rows already prefixed are skipped.
5
+ */
6
+ export function migrateLegacyBridgeScopeIds(db) {
7
+ const needsMigrate = (key) => !isBridgeScopedKey(key);
8
+ db.exec('BEGIN IMMEDIATE');
9
+ try {
10
+ const bindingRows = db.prepare('SELECT chat_id FROM chat_bindings').all();
11
+ for (const row of bindingRows) {
12
+ if (needsMigrate(row.chat_id)) {
13
+ db.prepare('UPDATE chat_bindings SET chat_id = ? WHERE chat_id = ?').run(`${BRIDGE_SCOPE_TELEGRAM_PREFIX}${row.chat_id}`, row.chat_id);
14
+ }
15
+ }
16
+ const settingsRows = db.prepare('SELECT chat_id FROM chat_settings').all();
17
+ for (const row of settingsRows) {
18
+ if (needsMigrate(row.chat_id)) {
19
+ db.prepare('UPDATE chat_settings SET chat_id = ? WHERE chat_id = ?').run(`${BRIDGE_SCOPE_TELEGRAM_PREFIX}${row.chat_id}`, row.chat_id);
20
+ }
21
+ }
22
+ const cacheRows = db.prepare('SELECT DISTINCT chat_id FROM thread_cache').all();
23
+ for (const row of cacheRows) {
24
+ if (needsMigrate(row.chat_id)) {
25
+ const next = `${BRIDGE_SCOPE_TELEGRAM_PREFIX}${row.chat_id}`;
26
+ db.prepare('UPDATE thread_cache SET chat_id = ? WHERE chat_id = ?').run(next, row.chat_id);
27
+ }
28
+ }
29
+ const approvalRows = db.prepare('SELECT local_id, chat_id FROM pending_approvals').all();
30
+ for (const row of approvalRows) {
31
+ if (needsMigrate(row.chat_id)) {
32
+ db.prepare('UPDATE pending_approvals SET chat_id = ? WHERE local_id = ?').run(`${BRIDGE_SCOPE_TELEGRAM_PREFIX}${row.chat_id}`, row.local_id);
33
+ }
34
+ }
35
+ const userInputRows = db.prepare('SELECT local_id, chat_id FROM pending_user_inputs').all();
36
+ for (const row of userInputRows) {
37
+ if (needsMigrate(row.chat_id)) {
38
+ db.prepare('UPDATE pending_user_inputs SET chat_id = ? WHERE local_id = ?').run(`${BRIDGE_SCOPE_TELEGRAM_PREFIX}${row.chat_id}`, row.local_id);
39
+ }
40
+ }
41
+ const auditRows = db.prepare('SELECT id, chat_id FROM audit_logs').all();
42
+ for (const row of auditRows) {
43
+ if (needsMigrate(row.chat_id)) {
44
+ db.prepare('UPDATE audit_logs SET chat_id = ? WHERE id = ?').run(`${BRIDGE_SCOPE_TELEGRAM_PREFIX}${row.chat_id}`, row.id);
45
+ }
46
+ }
47
+ const previewRows = db.prepare('SELECT turn_id, scope_id FROM active_turn_previews').all();
48
+ for (const row of previewRows) {
49
+ if (needsMigrate(row.scope_id)) {
50
+ db.prepare('UPDATE active_turn_previews SET scope_id = ? WHERE turn_id = ?').run(`${BRIDGE_SCOPE_TELEGRAM_PREFIX}${row.scope_id}`, row.turn_id);
51
+ }
52
+ }
53
+ db.exec('COMMIT');
54
+ }
55
+ catch (error) {
56
+ db.exec('ROLLBACK');
57
+ throw error;
58
+ }
59
+ }
@@ -0,0 +1,33 @@
1
+ import type { ParsedCommand } from '../controller/commands.js';
2
+ export interface TelegramMessageEntity {
3
+ type: string;
4
+ offset: number;
5
+ length: number;
6
+ }
7
+ interface ResolveTelegramAddressingParams {
8
+ text: string;
9
+ attachmentsCount: number;
10
+ entities: readonly TelegramMessageEntity[];
11
+ command: ParsedCommand | null;
12
+ botUsername: string | null;
13
+ isDefaultTopic: boolean;
14
+ replyToBot: boolean;
15
+ }
16
+ export type TelegramAddressingDecision = {
17
+ kind: 'ignore';
18
+ } | {
19
+ kind: 'command';
20
+ command: ParsedCommand;
21
+ } | {
22
+ kind: 'prompt';
23
+ text: string;
24
+ };
25
+ interface DefaultScopeParams {
26
+ chatType: string;
27
+ allowedChatId: string | null;
28
+ allowedTopicId: number | null;
29
+ topicId: number | null;
30
+ }
31
+ export declare function resolveTelegramAddressing(params: ResolveTelegramAddressingParams): TelegramAddressingDecision;
32
+ export declare function isDefaultTelegramScope(params: DefaultScopeParams): boolean;
33
+ export {};
@@ -0,0 +1,57 @@
1
+ export function resolveTelegramAddressing(params) {
2
+ const botUsername = normalizeBotUsername(params.botUsername);
3
+ if (params.command) {
4
+ if (isCommandAddressedToThisBot(params.command, botUsername, params.isDefaultTopic)) {
5
+ return { kind: 'command', command: params.command };
6
+ }
7
+ return { kind: 'ignore' };
8
+ }
9
+ const stripped = stripLeadingBotMention(params.text, params.entities, botUsername);
10
+ const explicitMention = stripped !== null;
11
+ const text = stripped ?? params.text;
12
+ const normalizedText = text.trim();
13
+ if (params.isDefaultTopic || params.replyToBot || explicitMention) {
14
+ if (normalizedText || params.attachmentsCount > 0) {
15
+ return { kind: 'prompt', text: normalizedText };
16
+ }
17
+ }
18
+ return { kind: 'ignore' };
19
+ }
20
+ export function isDefaultTelegramScope(params) {
21
+ if (params.chatType === 'private') {
22
+ return true;
23
+ }
24
+ if (params.allowedChatId === null) {
25
+ return false;
26
+ }
27
+ if (params.allowedTopicId === null) {
28
+ return true;
29
+ }
30
+ return params.topicId === params.allowedTopicId;
31
+ }
32
+ function normalizeBotUsername(botUsername) {
33
+ if (!botUsername)
34
+ return null;
35
+ return botUsername.trim().replace(/^@+/, '').toLowerCase() || null;
36
+ }
37
+ function isCommandAddressedToThisBot(command, botUsername, isDefaultTopic) {
38
+ if (command.targetBot === null) {
39
+ return isDefaultTopic;
40
+ }
41
+ if (!botUsername) {
42
+ return false;
43
+ }
44
+ return command.targetBot === botUsername;
45
+ }
46
+ function stripLeadingBotMention(text, entities, botUsername) {
47
+ if (!botUsername)
48
+ return null;
49
+ const mentionEntity = entities.find(entity => entity.type === 'mention' && entity.offset === 0);
50
+ if (!mentionEntity)
51
+ return null;
52
+ const mentionText = text.slice(mentionEntity.offset, mentionEntity.offset + mentionEntity.length);
53
+ if (mentionText.toLowerCase() !== `@${botUsername}`) {
54
+ return null;
55
+ }
56
+ return text.slice(mentionEntity.length).replace(/^[\s,:;-]+/, '');
57
+ }
@@ -0,0 +1,14 @@
1
+ export interface TelegramApiResult<T> {
2
+ ok: boolean;
3
+ result?: T;
4
+ description?: string;
5
+ }
6
+ export interface TelegramRemoteFile {
7
+ file_id: string;
8
+ file_unique_id: string;
9
+ file_size?: number;
10
+ file_path?: string;
11
+ }
12
+ export declare function callTelegramApi<T>(botToken: string, method: string, body: Record<string, unknown>): Promise<TelegramApiResult<T>>;
13
+ export declare function getTelegramFile(botToken: string, fileId: string): Promise<TelegramRemoteFile>;
14
+ export declare function downloadTelegramFile(botToken: string, remoteFilePath: string, destinationPath: string): Promise<number>;