@debbl/relay 0.0.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.
package/dist/index.mjs ADDED
@@ -0,0 +1,1360 @@
1
+ import * as Lark from "@larksuiteoapi/node-sdk";
2
+ import { i18n } from "@lingui/core";
3
+ import { spawn } from "node:child_process";
4
+ import { createInterface } from "node:readline";
5
+ import process from "node:process";
6
+ import fs from "node:fs";
7
+ import os from "node:os";
8
+ import path from "node:path";
9
+
10
+ //#region src/locales/en/messages.po
11
+ const messages$1 = JSON.parse("{\"1xKjU/\":[\"Failed to read open projects. Please try again later.\"],\"36QmnO\":[\"thread: \",[\"0\"]],\"4IDydv\":[\"Invalid relay config at \",[\"configPath\"],\": root must be a JSON object.\"],\"4Vrm3r\":[\"cwd: \",[\"0\"]],\"6tcMXX\":[\"Invalid relay config: \",[\"field\"],\" is required and must be a non-empty string.\"],\"AWIU5i\":[\"Relay config is missing. Template created at \",[\"configPath\"],\". Please edit this file and restart.\"],\"Bb/dZi\":[\"Available commands:\"],\"CfFOzJ\":[\"Invalid relay config at \",[\"configPath\"],\": env must be a JSON object.\"],\"Ck6rmi\":[\"Current session has been cleared.\"],\"FZcpfm\":[\"Failed to process message. Please try again later.\"],\"G1WYAl\":[\"Unknown command \\\"\",[\"0\"],\"\\\".\\n\\n\",[\"helpText\"]],\"H7VlDR\":[\"Currently busy. Please try again later.\"],\"HEx9te\":[\"New Session\"],\"Hrkm8q\":[\"Invalid relay config: CODEX_TIMEOUT_MS must be a positive integer.\"],\"JvAWQ5\":[\"No working directories are currently open.\"],\"K8IUjY\":[\"mode: \",[\"0\"]],\"Ks6r4a\":[\"Invalid relay config: \",[\"field\"],\" must be a string.\"],\"MZ28Ys\":[\"Failed to read open projects: \",[\"0\"]],\"NX40Ku\":[\"/new [default|plan] - Create a new session\"],\"OZZfXh\":[\"Current session status:\"],\"Oeb7jH\":[\"model: \",[\"0\"]],\"Qq8U1p\":[\"/status does not accept arguments.\\n\\n\",[\"helpText\"]],\"RLcmLa\":[\"/status - Show current session status\"],\"Rn/EAE\":[\"You are a session title generator.\\nGenerate a short English title based on the user message.\\nStrict requirements:\\n1. Output title text only, with no explanation.\\n2. Output a single line with no line breaks.\\n3. Do not use quotes.\\n4. Keep the title within 24 characters.\"],\"T/qXZl\":[\"/new accepts at most one optional argument: default or plan.\\n\\n\",[\"helpText\"]],\"Xg0HiS\":[\"/mode <default|plan> - Switch current session mode\"],\"YwjZm9\":[\"No active session. Send a normal message or use /new to create one.\"],\"aQiwam\":[\"Failed to start relay: \",[\"message\"]],\"dV7HiV\":[\"Invalid relay config: LOCALE \\\"\",[\"0\"],\"\\\" is not supported. Falling back to en.\"],\"dXeZCW\":[\"Switched to \",[\"0\"],\" mode.\"],\"dZhhUX\":[\"Created a new session.\"],\"doQwEN\":[\"Codex execution failed: \",[\"0\"]],\"eie2Mj\":[\"Command cannot be empty.\\n\\n\",[\"helpText\"]],\"gHcqpZ\":[\"Invalid mode \\\"\",[\"modeToken\"],\"\\\", only default or plan are supported.\\n\\n\",[\"helpText\"]],\"gf810l\":[\"/mode requires one argument: default or plan.\\n\\n\",[\"helpText\"]],\"j3bsc1\":[[\"0\"],\". \",[\"root\"]],\"ksj3fj\":[\"Invalid relay config: LOCALE \\\"\",[\"normalized\"],\"\\\" is not supported. Falling back to en.\"],\"lksJXf\":[\"/projects - Show current working directories\"],\"mehbut\":[\"No active session. Send a normal message or use /new to create one first.\"],\"miupLy\":[\"Current working directories:\"],\"o4F8ck\":[\"/reset does not accept arguments.\\n\\n\",[\"helpText\"]],\"oEMyJf\":[\"Please send a text message.\"],\"oKQkYZ\":[\"/projects does not accept arguments.\\n\\n\",[\"helpText\"]],\"orhkAj\":[\"Codex execution failed. Please try again later.\"],\"qN3BkN\":[\"/reset - Clear current session\"],\"rHDhWM\":[\"Failed to parse message. Please send a text message.\"],\"rdodSw\":[\"You are a session title generator.\\nGenerate a short Chinese title based on the user message.\\nStrict requirements:\\n1. Output title text only, with no explanation.\\n2. Output a single line with no line breaks.\\n3. Do not use quotes or title marks.\\n4. Keep the title within 24 characters.\"],\"smfLBQ\":[\"Cannot identify sender. Please try again later.\"],\"tHEFWw\":[\"title: \",[\"title\"]],\"tZQUtS\":[\"Failed to read relay config at \",[\"configPath\"],\": \",[\"0\"]],\"uAkDSp\":[\"User message: \",[\"0\"]],\"wBrugH\":[\"/help does not accept arguments.\\n\\n\",[\"helpText\"]],\"wSNjFy\":[\"/help - Show help\"],\"z+7ZYe\":[\"Invalid JSON in relay config at \",[\"configPath\"],\": \",[\"0\"]]}");
12
+
13
+ //#endregion
14
+ //#region src/locales/zh/messages.po
15
+ const messages = JSON.parse("{\"1xKjU/\":[\"读取已打开项目失败,请稍后重试。\"],\"36QmnO\":[\"thread: \",[\"0\"]],\"4IDydv\":[\"中继配置 \",[\"configPath\"],\" 无效:root 必须是 JSON 对象。\"],\"4Vrm3r\":[\"cwd: \",[\"0\"]],\"6tcMXX\":[\"中继配置无效:\",[\"field\"],\" 为必填项且必须为非空字符串。\"],\"AWIU5i\":[\"中继配置缺失,已在 \",[\"configPath\"],\" 创建模板。请编辑该文件后重启。\"],\"Bb/dZi\":[\"可用命令:\"],\"CfFOzJ\":[\"中继配置 \",[\"configPath\"],\" 无效:env 必须是 JSON 对象。\"],\"Ck6rmi\":[\"当前会话已清空。\"],\"FZcpfm\":[\"处理消息失败,请稍后重试。\"],\"G1WYAl\":[\"未知命令 \\\"\",[\"0\"],\"\\\"。\\n\\n\",[\"helpText\"]],\"H7VlDR\":[\"当前忙碌,请稍后重试。\"],\"HEx9te\":[\"新会话\"],\"Hrkm8q\":[\"中继配置无效:CODEX_TIMEOUT_MS 必须是正整数。\"],\"JvAWQ5\":[\"当前没有打开的工作目录。\"],\"K8IUjY\":[\"mode: \",[\"0\"]],\"Ks6r4a\":[\"中继配置无效:\",[\"field\"],\" 必须是字符串。\"],\"MZ28Ys\":[\"读取已打开项目失败:\",[\"0\"]],\"NX40Ku\":[\"/new [default|plan] - 创建新会话\"],\"OZZfXh\":[\"当前会话状态:\"],\"Oeb7jH\":[\"model: \",[\"0\"]],\"Qq8U1p\":[\"/status 不接受参数。\\n\\n\",[\"helpText\"]],\"RLcmLa\":[\"/status - 显示当前会话状态\"],\"Rn/EAE\":[\"你是会话标题生成器。\\n根据用户消息生成简短英文标题。\\n严格要求:\\n1. 仅输出标题文字,不要解释。\\n2. 单行输出,不要换行。\\n3. 不要使用引号。\\n4. 标题不超过 24 个字符。\"],\"T/qXZl\":[\"/new 最多接受一个可选参数:default 或 plan。\\n\\n\",[\"helpText\"]],\"Xg0HiS\":[\"/mode <default|plan> - 切换当前会话模式\"],\"YwjZm9\":[\"没有活跃会话。请发送普通消息或使用 /new 创建会话。\"],\"aQiwam\":[\"启动中继失败:\",[\"message\"]],\"dV7HiV\":[\"中继配置无效:不支持的 LOCALE \\\"\",[\"0\"],\"\\\",已回退为 en。\"],\"dXeZCW\":[\"已切换到 \",[\"0\"],\" 模式。\"],\"dZhhUX\":[\"已创建新会话。\"],\"doQwEN\":[\"Codex 执行失败:\",[\"0\"]],\"eie2Mj\":[\"命令不能为空。\\n\\n\",[\"helpText\"]],\"gHcqpZ\":[\"无效的模式 \\\"\",[\"modeToken\"],\"\\\",仅支持 default 或 plan。\\n\\n\",[\"helpText\"]],\"gf810l\":[\"/mode 需要一个参数:default 或 plan。\\n\\n\",[\"helpText\"]],\"j3bsc1\":[[\"0\"],\". \",[\"root\"]],\"ksj3fj\":[\"中继配置无效:不支持的 LOCALE \\\"\",[\"normalized\"],\"\\\",已回退为 en。\"],\"lksJXf\":[\"/projects - 显示当前工作目录\"],\"mehbut\":[\"没有活跃会话。请先发送普通消息或使用 /new 创建会话。\"],\"miupLy\":[\"当前工作目录:\"],\"o4F8ck\":[\"/reset 不接受参数。\\n\\n\",[\"helpText\"]],\"oEMyJf\":[\"请发送文本消息。\"],\"oKQkYZ\":[\"/projects 不接受参数。\\n\\n\",[\"helpText\"]],\"orhkAj\":[\"Codex 执行失败,请稍后重试。\"],\"qN3BkN\":[\"/reset - 清空当前会话\"],\"rHDhWM\":[\"解析消息失败,请发送文本消息。\"],\"rdodSw\":[\"你是会话标题生成器。\\n根据用户消息生成简短中文标题。\\n严格要求:\\n1. 仅输出标题文字,不要解释。\\n2. 单行输出,不要换行。\\n3. 不要使用引号或书名号。\\n4. 标题不超过 24 个字符。\"],\"smfLBQ\":[\"无法识别发送者,请稍后重试。\"],\"tHEFWw\":[\"title: \",[\"title\"]],\"tZQUtS\":[\"读取中继配置失败 \",[\"configPath\"],\":\",[\"0\"]],\"uAkDSp\":[\"用户消息:\",[\"0\"]],\"wBrugH\":[\"/help 不接受参数。\\n\\n\",[\"helpText\"]],\"wSNjFy\":[\"/help - 显示帮助\"],\"z+7ZYe\":[\"中继配置 \",[\"configPath\"],\" 中的 JSON 无效:\",[\"0\"]]}");
16
+
17
+ //#endregion
18
+ //#region src/i18n/runtime.ts
19
+ const DEFAULT_LOCALE = "en";
20
+ const CATALOGS = {
21
+ en: messages$1,
22
+ zh: messages
23
+ };
24
+ let activeLocale = null;
25
+ function initializeI18n(locale) {
26
+ const resolved = resolveLocale(locale);
27
+ i18n.loadAndActivate({
28
+ locale: resolved,
29
+ messages: CATALOGS[resolved]
30
+ });
31
+ activeLocale = resolved;
32
+ return resolved;
33
+ }
34
+ function getCurrentLocale() {
35
+ ensureI18nInitialized();
36
+ return activeLocale ?? DEFAULT_LOCALE;
37
+ }
38
+ function isSupportedLocale(locale) {
39
+ return locale === "en" || locale === "zh";
40
+ }
41
+ function getDefaultLocale() {
42
+ return DEFAULT_LOCALE;
43
+ }
44
+ function resolveLocale(locale) {
45
+ if (!locale) return DEFAULT_LOCALE;
46
+ if (isSupportedLocale(locale)) return locale;
47
+ return DEFAULT_LOCALE;
48
+ }
49
+ function ensureI18nInitialized() {
50
+ if (!activeLocale) initializeI18n(DEFAULT_LOCALE);
51
+ }
52
+
53
+ //#endregion
54
+ //#region src/session/store.ts
55
+ const sessionStore = /* @__PURE__ */ new Map();
56
+ const sessionQueue = /* @__PURE__ */ new Map();
57
+ function getSessionKey(input) {
58
+ if (input.chatType === "p2p") return `p2p:${input.chatId}`;
59
+ return `group:${input.chatId}:${input.userId}`;
60
+ }
61
+ function getSession(sessionKey) {
62
+ return sessionStore.get(sessionKey);
63
+ }
64
+ function setSession(sessionKey, session) {
65
+ sessionStore.set(sessionKey, session);
66
+ }
67
+ function clearSession(sessionKey) {
68
+ sessionStore.delete(sessionKey);
69
+ }
70
+ async function withSessionLock(sessionKey, run) {
71
+ const running = (sessionQueue.get(sessionKey) ?? Promise.resolve()).then(() => run(), () => run());
72
+ const queueItem = running.then(() => void 0, () => void 0);
73
+ sessionQueue.set(sessionKey, queueItem);
74
+ try {
75
+ return await running;
76
+ } finally {
77
+ if (sessionQueue.get(sessionKey) === queueItem) sessionQueue.delete(sessionKey);
78
+ }
79
+ }
80
+
81
+ //#endregion
82
+ //#region src/bot/commands.ts
83
+ const COMMAND_HELP = "/help";
84
+ const COMMAND_NEW = "/new";
85
+ const COMMAND_MODE = "/mode";
86
+ const COMMAND_STATUS = "/status";
87
+ const COMMAND_PROJECTS = "/projects";
88
+ const COMMAND_RESET = "/reset";
89
+ function getHelpText() {
90
+ return [
91
+ i18n._({
92
+ id: "Bb/dZi",
93
+ message: "Available commands:"
94
+ }),
95
+ i18n._({
96
+ id: "wSNjFy",
97
+ message: "/help - Show help"
98
+ }),
99
+ i18n._({
100
+ id: "NX40Ku",
101
+ message: "/new [default|plan] - Create a new session"
102
+ }),
103
+ i18n._({
104
+ id: "Xg0HiS",
105
+ message: "/mode <default|plan> - Switch current session mode"
106
+ }),
107
+ i18n._({
108
+ id: "RLcmLa",
109
+ message: "/status - Show current session status"
110
+ }),
111
+ i18n._({
112
+ id: "lksJXf",
113
+ message: "/projects - Show current working directories"
114
+ }),
115
+ i18n._({
116
+ id: "qN3BkN",
117
+ message: "/reset - Clear current session"
118
+ })
119
+ ].join("\n");
120
+ }
121
+ function parseCommand(input) {
122
+ const normalized = input.trim();
123
+ const helpText = getHelpText();
124
+ if (normalized.length === 0) return {
125
+ type: "invalid",
126
+ message: i18n._({
127
+ id: "eie2Mj",
128
+ message: "Command cannot be empty.\n\n{helpText}",
129
+ values: { helpText }
130
+ })
131
+ };
132
+ if (!normalized.startsWith("/")) return {
133
+ type: "prompt",
134
+ prompt: normalized
135
+ };
136
+ const parts = normalized.split(/\s+/);
137
+ const command = parts[0]?.toLowerCase();
138
+ if (command === COMMAND_HELP) {
139
+ if (parts.length > 1) return {
140
+ type: "invalid",
141
+ message: i18n._({
142
+ id: "wBrugH",
143
+ message: "/help does not accept arguments.\n\n{helpText}",
144
+ values: { helpText }
145
+ })
146
+ };
147
+ return { type: "help" };
148
+ }
149
+ if (command === COMMAND_NEW) {
150
+ if (parts.length > 2) return {
151
+ type: "invalid",
152
+ message: i18n._({
153
+ id: "T/qXZl",
154
+ message: "/new accepts at most one optional argument: default or plan.\n\n{helpText}",
155
+ values: { helpText }
156
+ })
157
+ };
158
+ const modeToken = parts[1];
159
+ if (!modeToken) return {
160
+ type: "new",
161
+ mode: "default"
162
+ };
163
+ const mode = parseMode(modeToken);
164
+ if (!mode) return {
165
+ type: "invalid",
166
+ message: i18n._({
167
+ id: "gHcqpZ",
168
+ message: "Invalid mode \"{modeToken}\", only default or plan are supported.\n\n{helpText}",
169
+ values: {
170
+ modeToken,
171
+ helpText
172
+ }
173
+ })
174
+ };
175
+ return {
176
+ type: "new",
177
+ mode
178
+ };
179
+ }
180
+ if (command === COMMAND_MODE) {
181
+ const modeToken = parts[1];
182
+ if (!modeToken || parts.length > 2) return {
183
+ type: "invalid",
184
+ message: i18n._({
185
+ id: "gf810l",
186
+ message: "/mode requires one argument: default or plan.\n\n{helpText}",
187
+ values: { helpText }
188
+ })
189
+ };
190
+ const mode = parseMode(modeToken);
191
+ if (!mode) return {
192
+ type: "invalid",
193
+ message: i18n._({
194
+ id: "gHcqpZ",
195
+ message: "Invalid mode \"{modeToken}\", only default or plan are supported.\n\n{helpText}",
196
+ values: {
197
+ modeToken,
198
+ helpText
199
+ }
200
+ })
201
+ };
202
+ return {
203
+ type: "mode",
204
+ mode
205
+ };
206
+ }
207
+ if (command === COMMAND_STATUS) {
208
+ if (parts.length > 1) return {
209
+ type: "invalid",
210
+ message: i18n._({
211
+ id: "Qq8U1p",
212
+ message: "/status does not accept arguments.\n\n{helpText}",
213
+ values: { helpText }
214
+ })
215
+ };
216
+ return { type: "status" };
217
+ }
218
+ if (command === COMMAND_PROJECTS) {
219
+ if (parts.length > 1) return {
220
+ type: "invalid",
221
+ message: i18n._({
222
+ id: "oKQkYZ",
223
+ message: "/projects does not accept arguments.\n\n{helpText}",
224
+ values: { helpText }
225
+ })
226
+ };
227
+ return { type: "projects" };
228
+ }
229
+ if (command === COMMAND_RESET) {
230
+ if (parts.length > 1) return {
231
+ type: "invalid",
232
+ message: i18n._({
233
+ id: "o4F8ck",
234
+ message: "/reset does not accept arguments.\n\n{helpText}",
235
+ values: { helpText }
236
+ })
237
+ };
238
+ return { type: "reset" };
239
+ }
240
+ return {
241
+ type: "invalid",
242
+ message: i18n._({
243
+ id: "G1WYAl",
244
+ message: "Unknown command \"{0}\".\n\n{helpText}",
245
+ values: {
246
+ helpText,
247
+ 0: command ?? normalized
248
+ }
249
+ })
250
+ };
251
+ }
252
+ function parseMode(input) {
253
+ const normalized = input.toLowerCase();
254
+ if (normalized === "default" || normalized === "plan") return normalized;
255
+ return null;
256
+ }
257
+
258
+ //#endregion
259
+ //#region src/bot/handler.ts
260
+ const MAX_SESSION_TITLE_LENGTH = 24;
261
+ const WRAPPING_QUOTE_PAIRS = [
262
+ ["\"", "\""],
263
+ ["'", "'"],
264
+ ["“", "”"],
265
+ ["‘", "’"],
266
+ ["「", "」"],
267
+ ["《", "》"]
268
+ ];
269
+ async function handleIncomingText(input, deps) {
270
+ const sessionKey = getSessionKey({
271
+ chatType: input.chatType,
272
+ chatId: input.chatId,
273
+ userId: input.senderId
274
+ });
275
+ return deps.withSessionLock(sessionKey, async () => {
276
+ const parsed = parseCommand(input.text);
277
+ const currentSession = deps.getSession(sessionKey);
278
+ if (parsed.type === "invalid") return parsed.message;
279
+ if (parsed.type === "help") return getHelpText();
280
+ if (parsed.type === "status") {
281
+ if (!currentSession) return i18n._({
282
+ id: "YwjZm9",
283
+ message: "No active session. Send a normal message or use /new to create one."
284
+ });
285
+ const title = normalizeSessionTitle(currentSession.title) ?? i18n._({
286
+ id: "HEx9te",
287
+ message: "New Session"
288
+ });
289
+ return [
290
+ i18n._({
291
+ id: "OZZfXh",
292
+ message: "Current session status:"
293
+ }),
294
+ i18n._({
295
+ id: "36QmnO",
296
+ message: "thread: {0}",
297
+ values: { 0: currentSession.threadId }
298
+ }),
299
+ i18n._({
300
+ id: "tHEFWw",
301
+ message: "title: {title}",
302
+ values: { title }
303
+ }),
304
+ i18n._({
305
+ id: "K8IUjY",
306
+ message: "mode: {0}",
307
+ values: { 0: currentSession.mode }
308
+ }),
309
+ i18n._({
310
+ id: "Oeb7jH",
311
+ message: "model: {0}",
312
+ values: { 0: currentSession.model }
313
+ })
314
+ ].join("\n");
315
+ }
316
+ if (parsed.type === "projects") try {
317
+ const result = await deps.listOpenProjects();
318
+ if (result.roots.length === 0) return i18n._({
319
+ id: "JvAWQ5",
320
+ message: "No working directories are currently open."
321
+ });
322
+ const lines = result.roots.map((root, index) => i18n._({
323
+ id: "j3bsc1",
324
+ message: "{0}. {root}",
325
+ values: {
326
+ root,
327
+ 0: index + 1
328
+ }
329
+ }));
330
+ return [i18n._({
331
+ id: "miupLy",
332
+ message: "Current working directories:"
333
+ }), ...lines].join("\n");
334
+ } catch (error) {
335
+ return formatProjectsError(error);
336
+ }
337
+ if (parsed.type === "reset") {
338
+ deps.clearSession(sessionKey);
339
+ return i18n._({
340
+ id: "Ck6rmi",
341
+ message: "Current session has been cleared."
342
+ });
343
+ }
344
+ if (parsed.type === "mode") {
345
+ if (!currentSession) return i18n._({
346
+ id: "mehbut",
347
+ message: "No active session. Send a normal message or use /new to create one first."
348
+ });
349
+ deps.setSession(sessionKey, {
350
+ ...currentSession,
351
+ mode: parsed.mode
352
+ });
353
+ return i18n._({
354
+ id: "dXeZCW",
355
+ message: "Switched to {0} mode.",
356
+ values: { 0: parsed.mode }
357
+ });
358
+ }
359
+ if (parsed.type === "new") try {
360
+ const created = await deps.createThread(parsed.mode);
361
+ deps.setSession(sessionKey, created);
362
+ return [
363
+ i18n._({
364
+ id: "dZhhUX",
365
+ message: "Created a new session."
366
+ }),
367
+ i18n._({
368
+ id: "36QmnO",
369
+ message: "thread: {0}",
370
+ values: { 0: created.threadId }
371
+ }),
372
+ i18n._({
373
+ id: "4Vrm3r",
374
+ message: "cwd: {0}",
375
+ values: { 0: created.cwd }
376
+ }),
377
+ i18n._({
378
+ id: "K8IUjY",
379
+ message: "mode: {0}",
380
+ values: { 0: created.mode }
381
+ }),
382
+ i18n._({
383
+ id: "Oeb7jH",
384
+ message: "model: {0}",
385
+ values: { 0: created.model }
386
+ })
387
+ ].join("\n");
388
+ } catch (error) {
389
+ return formatCodexError(error);
390
+ }
391
+ try {
392
+ const mode = currentSession?.mode ?? "default";
393
+ const result = await deps.runTurn({
394
+ prompt: parsed.prompt,
395
+ mode,
396
+ session: currentSession ?? null
397
+ });
398
+ const title = await resolveSessionTitle({
399
+ currentSession,
400
+ prompt: parsed.prompt,
401
+ mode,
402
+ runTurn: deps.runTurn
403
+ });
404
+ deps.setSession(sessionKey, {
405
+ threadId: result.threadId,
406
+ model: result.model,
407
+ mode: result.mode,
408
+ cwd: result.cwd,
409
+ title
410
+ });
411
+ return result.message;
412
+ } catch (error) {
413
+ return formatCodexError(error);
414
+ }
415
+ });
416
+ }
417
+ function formatCodexError(error) {
418
+ if (error instanceof Error && error.message.trim().length > 0) return i18n._({
419
+ id: "doQwEN",
420
+ message: "Codex execution failed: {0}",
421
+ values: { 0: error.message }
422
+ });
423
+ return i18n._({
424
+ id: "orhkAj",
425
+ message: "Codex execution failed. Please try again later."
426
+ });
427
+ }
428
+ function formatProjectsError(error) {
429
+ if (error instanceof Error && error.message.trim().length > 0) return i18n._({
430
+ id: "MZ28Ys",
431
+ message: "Failed to read open projects: {0}",
432
+ values: { 0: error.message }
433
+ });
434
+ return i18n._({
435
+ id: "1xKjU/",
436
+ message: "Failed to read open projects. Please try again later."
437
+ });
438
+ }
439
+ async function resolveSessionTitle(input) {
440
+ const currentTitle = normalizeSessionTitle(input.currentSession?.title);
441
+ if (currentTitle) return currentTitle;
442
+ if (!input.currentSession) return;
443
+ return generateSessionTitleWithFallback({
444
+ prompt: input.prompt,
445
+ mode: input.mode,
446
+ runTurn: input.runTurn
447
+ });
448
+ }
449
+ async function generateSessionTitleWithFallback(input) {
450
+ const fallbackTitle = buildFallbackSessionTitle(input.prompt);
451
+ const titlePrompt = buildTitleGenerationPrompt(input.prompt);
452
+ try {
453
+ const sanitizedTitle = sanitizeGeneratedTitle((await input.runTurn({
454
+ prompt: titlePrompt,
455
+ mode: input.mode,
456
+ session: null
457
+ })).message);
458
+ if (sanitizedTitle) return sanitizedTitle;
459
+ logTitleGenerationFallback("generated title is empty after post-processing, using fallback");
460
+ } catch (error) {
461
+ logTitleGenerationFallback(`title generation request failed: ${formatErrorMessage(error)}`);
462
+ }
463
+ return fallbackTitle;
464
+ }
465
+ function buildFallbackSessionTitle(prompt) {
466
+ const normalizedPrompt = normalizePrompt(prompt);
467
+ if (normalizedPrompt.length === 0) return i18n._({
468
+ id: "HEx9te",
469
+ message: "New Session"
470
+ });
471
+ return truncateTitle(normalizedPrompt);
472
+ }
473
+ function normalizeSessionTitle(title) {
474
+ if (!title) return null;
475
+ const normalized = title.trim();
476
+ if (normalized.length === 0) return null;
477
+ return normalized;
478
+ }
479
+ function buildTitleGenerationPrompt(prompt) {
480
+ return [
481
+ getCurrentLocale() === "zh" ? i18n._({
482
+ id: "rdodSw",
483
+ message: "You are a session title generator.\nGenerate a short Chinese title based on the user message.\nStrict requirements:\n1. Output title text only, with no explanation.\n2. Output a single line with no line breaks.\n3. Do not use quotes or title marks.\n4. Keep the title within 24 characters."
484
+ }) : i18n._({
485
+ id: "Rn/EAE",
486
+ message: "You are a session title generator.\nGenerate a short English title based on the user message.\nStrict requirements:\n1. Output title text only, with no explanation.\n2. Output a single line with no line breaks.\n3. Do not use quotes.\n4. Keep the title within 24 characters."
487
+ }),
488
+ "",
489
+ i18n._({
490
+ id: "uAkDSp",
491
+ message: "User message: {0}",
492
+ values: { 0: normalizePrompt(prompt) }
493
+ })
494
+ ].join("\n");
495
+ }
496
+ function sanitizeGeneratedTitle(rawTitle) {
497
+ const trimmed = rawTitle.trim();
498
+ if (trimmed.length === 0) return null;
499
+ const firstLine = trimmed.split(/\r?\n/)[0]?.trim() ?? "";
500
+ if (firstLine.length === 0) return null;
501
+ const unquoted = stripWrappingQuotes(normalizePrompt(firstLine));
502
+ if (unquoted.length === 0) return null;
503
+ return truncateTitle(unquoted);
504
+ }
505
+ function stripWrappingQuotes(input) {
506
+ let value = input.trim();
507
+ let changed = true;
508
+ while (changed && value.length > 0) {
509
+ changed = false;
510
+ for (const [left, right] of WRAPPING_QUOTE_PAIRS) if (value.startsWith(left) && value.endsWith(right)) {
511
+ const inner = value.slice(left.length, value.length - right.length).trim();
512
+ if (inner !== value) {
513
+ value = inner;
514
+ changed = true;
515
+ break;
516
+ }
517
+ }
518
+ }
519
+ return value;
520
+ }
521
+ function truncateTitle(input) {
522
+ const chars = Array.from(input);
523
+ if (chars.length <= MAX_SESSION_TITLE_LENGTH) return input;
524
+ if (MAX_SESSION_TITLE_LENGTH <= 3) return chars.slice(0, MAX_SESSION_TITLE_LENGTH).join("");
525
+ return `${chars.slice(0, MAX_SESSION_TITLE_LENGTH - 3).join("")}...`;
526
+ }
527
+ function normalizePrompt(input) {
528
+ return input.replace(/\s+/g, " ").trim();
529
+ }
530
+ function formatErrorMessage(error) {
531
+ if (error instanceof Error && error.message.trim().length > 0) return error.message;
532
+ return String(error);
533
+ }
534
+ function logTitleGenerationFallback(message) {
535
+ console.warn(`[relay] ${message}`);
536
+ }
537
+
538
+ //#endregion
539
+ //#region src/bot/message-filter.ts
540
+ function shouldProcessMessage(event, botOpenId) {
541
+ if (isMessageFromBot(event, botOpenId)) return false;
542
+ if (event.message.chat_type === "p2p") return true;
543
+ return shouldHandleGroupMessage(event.message.mentions, botOpenId);
544
+ }
545
+ function shouldHandleGroupMessage(mentions, botOpenId) {
546
+ if (!mentions || mentions.length === 0) return false;
547
+ if (!botOpenId) return true;
548
+ return mentions.some((mention) => mention.id?.open_id === botOpenId);
549
+ }
550
+ function resolveSenderId(senderId) {
551
+ if (!senderId) return null;
552
+ return senderId.open_id ?? senderId.user_id ?? senderId.union_id ?? null;
553
+ }
554
+ function isMessageFromBot(event, botOpenId) {
555
+ if (event.sender.sender_type?.toLowerCase() === "app") return true;
556
+ if (!botOpenId) return false;
557
+ return resolveSenderId(event.sender.sender_id) === botOpenId;
558
+ }
559
+
560
+ //#endregion
561
+ //#region src/bot/relay.ts
562
+ async function buildReplyForMessageEvent(event, deps) {
563
+ if (isMessageFromBot(event, deps.botOpenId)) return null;
564
+ if (event.message.message_type !== "text") return i18n._({
565
+ id: "rHDhWM",
566
+ message: "Failed to parse message. Please send a text message."
567
+ });
568
+ const text = parseTextContent(event.message.content);
569
+ if (!text) return i18n._({
570
+ id: "rHDhWM",
571
+ message: "Failed to parse message. Please send a text message."
572
+ });
573
+ if (event.message.chat_type !== "p2p" && !shouldHandleGroupMessage(event.message.mentions, deps.botOpenId)) return null;
574
+ const senderId = resolveSenderId(event.sender.sender_id);
575
+ if (!senderId) return i18n._({
576
+ id: "smfLBQ",
577
+ message: "Cannot identify sender. Please try again later."
578
+ });
579
+ const normalizedText = stripMentionTags(text).trim();
580
+ if (normalizedText.length === 0) return i18n._({
581
+ id: "oEMyJf",
582
+ message: "Please send a text message."
583
+ });
584
+ return deps.handleIncomingText({
585
+ chatType: event.message.chat_type,
586
+ chatId: event.message.chat_id,
587
+ senderId,
588
+ text: normalizedText
589
+ });
590
+ }
591
+ function stripMentionTags(text) {
592
+ return text.replace(/<at\b[^>]*>.*?<\/at>/g, "").trim();
593
+ }
594
+ function parseTextContent(content) {
595
+ try {
596
+ const parsed = JSON.parse(content);
597
+ if (!isRecord$1(parsed)) return null;
598
+ return typeof parsed.text === "string" ? parsed.text : null;
599
+ } catch {
600
+ return null;
601
+ }
602
+ }
603
+ function isRecord$1(value) {
604
+ return typeof value === "object" && value !== null;
605
+ }
606
+
607
+ //#endregion
608
+ //#region src/codex/rpc.ts
609
+ function parseRpcLine(line) {
610
+ let parsed;
611
+ try {
612
+ parsed = JSON.parse(line);
613
+ } catch {
614
+ return null;
615
+ }
616
+ if (!isRecord(parsed)) return null;
617
+ if (typeof parsed.method === "string") {
618
+ if (isRpcRequestId(parsed.id)) return {
619
+ id: parsed.id,
620
+ method: parsed.method,
621
+ params: parsed.params
622
+ };
623
+ return {
624
+ method: parsed.method,
625
+ params: parsed.params
626
+ };
627
+ }
628
+ if (!isRpcRequestId(parsed.id)) return null;
629
+ if ("error" in parsed && isRpcErrorObject(parsed.error)) return {
630
+ id: parsed.id,
631
+ error: parsed.error
632
+ };
633
+ if ("result" in parsed) return {
634
+ id: parsed.id,
635
+ result: parsed.result
636
+ };
637
+ return null;
638
+ }
639
+ function formatRpcError(error) {
640
+ return `Codex RPC error (${error.code}): ${error.message}`;
641
+ }
642
+ function isRecord(value) {
643
+ return typeof value === "object" && value !== null;
644
+ }
645
+ function isRpcRequestId(value) {
646
+ return typeof value === "number" || typeof value === "string";
647
+ }
648
+ function isRpcErrorObject(value) {
649
+ if (!isRecord(value)) return false;
650
+ return typeof value.code === "number" && typeof value.message === "string";
651
+ }
652
+ function isRpcErrorResponse(value) {
653
+ return "error" in value;
654
+ }
655
+ function isRpcSuccessResponse(value) {
656
+ return "result" in value;
657
+ }
658
+ function isRpcServerRequest(value) {
659
+ return "method" in value && "id" in value;
660
+ }
661
+ function getServerRequestResult(method) {
662
+ if (method === "item/commandExecution/requestApproval") return {
663
+ decision: "accept",
664
+ acceptSettings: { forSession: true }
665
+ };
666
+ if (method === "item/fileChange/requestApproval") return { decision: "accept" };
667
+ if (method.endsWith("/requestApproval")) return { decision: "accept" };
668
+ if (method === "execCommandApproval") return { decision: "allow" };
669
+ if (method === "applyPatchApproval") return { decision: "allow" };
670
+ if (method.endsWith("Approval")) return { decision: "allow" };
671
+ if (method === "item/tool/requestUserInput") return { answers: {} };
672
+ if (method === "item/tool/call") return {
673
+ success: false,
674
+ contentItems: [{
675
+ type: "inputText",
676
+ text: "Dynamic tool calls are unavailable in relay-bot."
677
+ }]
678
+ };
679
+ return null;
680
+ }
681
+
682
+ //#endregion
683
+ //#region src/codex/app-server-client.ts
684
+ var CodexAppServerClient = class {
685
+ setNotificationHandler(handler) {
686
+ this.notificationHandler = handler;
687
+ }
688
+ async request(method, params) {
689
+ if (this.exited) throw new Error(this.buildExitMessage(null, null));
690
+ const requestId = this.nextId;
691
+ this.nextId += 1;
692
+ const responsePromise = new Promise((resolve, reject) => {
693
+ this.pending.set(requestId, {
694
+ resolve: (value) => resolve(value),
695
+ reject
696
+ });
697
+ });
698
+ const payload = JSON.stringify({
699
+ jsonrpc: "2.0",
700
+ id: requestId,
701
+ method,
702
+ params
703
+ });
704
+ await new Promise((resolve, reject) => {
705
+ this.child.stdin.write(`${payload}\n`, (error) => {
706
+ if (error) {
707
+ this.pending.delete(requestId);
708
+ reject(error);
709
+ return;
710
+ }
711
+ resolve();
712
+ });
713
+ });
714
+ return responsePromise;
715
+ }
716
+ dispose() {
717
+ this.lineReader.close();
718
+ if (!this.child.killed) this.child.kill("SIGTERM");
719
+ }
720
+ handleStdoutLine(line) {
721
+ const parsed = parseRpcLine(line);
722
+ if (!parsed) return;
723
+ if ("method" in parsed) {
724
+ if (isRpcServerRequest(parsed)) {
725
+ this.respondToServerRequest(parsed).catch((error) => {
726
+ this.stderrBuffer.push(`failed to respond to server request "${parsed.method}": ${String(error)}`);
727
+ });
728
+ return;
729
+ }
730
+ this.notificationHandler?.(parsed);
731
+ return;
732
+ }
733
+ const pending = this.pending.get(parsed.id);
734
+ if (!pending) return;
735
+ this.pending.delete(parsed.id);
736
+ if (isRpcErrorResponse(parsed)) {
737
+ pending.reject(new Error(formatRpcError(parsed.error)));
738
+ return;
739
+ }
740
+ if (isRpcSuccessResponse(parsed)) pending.resolve(parsed.result);
741
+ }
742
+ async respondToServerRequest(request) {
743
+ const result = getServerRequestResult(request.method);
744
+ if (result !== null) {
745
+ await this.sendRpcResult(request.id, result);
746
+ return;
747
+ }
748
+ await this.sendRpcError(request.id, -32601, `Unsupported server request method: ${request.method}`);
749
+ }
750
+ async sendRpcResult(id, result) {
751
+ await this.writeRpcPayload({
752
+ jsonrpc: "2.0",
753
+ id,
754
+ result
755
+ });
756
+ }
757
+ async sendRpcError(id, code, message) {
758
+ await this.writeRpcPayload({
759
+ jsonrpc: "2.0",
760
+ id,
761
+ error: {
762
+ code,
763
+ message
764
+ }
765
+ });
766
+ }
767
+ async writeRpcPayload(payload) {
768
+ if (this.exited) return;
769
+ const serialized = JSON.stringify(payload);
770
+ await new Promise((resolve, reject) => {
771
+ this.child.stdin.write(`${serialized}\n`, (error) => {
772
+ if (error) {
773
+ reject(error);
774
+ return;
775
+ }
776
+ resolve();
777
+ });
778
+ });
779
+ }
780
+ buildExitMessage(code, signal) {
781
+ const suffix = this.stderrBuffer.length > 0 ? `; stderr: ${this.stderrBuffer.at(-1)}` : "";
782
+ return `Codex app-server exited (code=${code ?? "null"}, signal=${signal ?? "null"})${suffix}`;
783
+ }
784
+ constructor(options) {
785
+ this.pending = /* @__PURE__ */ new Map();
786
+ this.stderrBuffer = [];
787
+ this.nextId = 1;
788
+ this.notificationHandler = null;
789
+ this.exited = false;
790
+ this.options = options;
791
+ this.child = spawn(this.options.codexBin, ["app-server"], {
792
+ cwd: this.options.cwd,
793
+ stdio: [
794
+ "pipe",
795
+ "pipe",
796
+ "pipe"
797
+ ]
798
+ });
799
+ this.lineReader = createInterface({
800
+ input: this.child.stdout,
801
+ crlfDelay: Infinity
802
+ });
803
+ this.lineReader.on("line", (line) => {
804
+ this.handleStdoutLine(line);
805
+ });
806
+ this.child.stderr.on("data", (chunk) => {
807
+ const text = String(chunk).trim();
808
+ if (text.length > 0) this.stderrBuffer.push(text);
809
+ });
810
+ this.child.on("exit", (code, signal) => {
811
+ this.exited = true;
812
+ const error = new Error(this.buildExitMessage(code, signal));
813
+ for (const pending of this.pending.values()) pending.reject(error);
814
+ this.pending.clear();
815
+ });
816
+ }
817
+ };
818
+
819
+ //#endregion
820
+ //#region src/codex/thread.ts
821
+ async function initializeClient(client) {
822
+ await client.request("initialize", {
823
+ clientInfo: {
824
+ name: "relay-bot",
825
+ title: "Relay Bot",
826
+ version: "0.0.0"
827
+ },
828
+ capabilities: { experimentalApi: true }
829
+ });
830
+ }
831
+ async function getCollaborationModes(client) {
832
+ const raw = await client.request("collaborationMode/list", {});
833
+ if (!isCollaborationModeListResponse(raw)) throw new Error("Invalid collaboration mode response from Codex");
834
+ return raw.data;
835
+ }
836
+ async function openThread(client, session, cwd) {
837
+ if (!session) return startThread(client, cwd);
838
+ if (session.cwd !== cwd) return startThread(client, cwd);
839
+ try {
840
+ const resumed = await resumeThread(client, session.threadId);
841
+ if (resumed.cwd !== cwd) return startThread(client, cwd);
842
+ return resumed;
843
+ } catch (error) {
844
+ if (isThreadMissingError(error)) return startThread(client, cwd);
845
+ throw error;
846
+ }
847
+ }
848
+ async function startThread(client, cwd) {
849
+ return parseThreadResult(await client.request("thread/start", {
850
+ cwd,
851
+ approvalPolicy: "on-request",
852
+ sandbox: "workspace-write",
853
+ experimentalRawEvents: false
854
+ }));
855
+ }
856
+ function selectCollaborationModePayload(masks, mode, model) {
857
+ const selected = masks.find((mask) => {
858
+ if (mask.mode === mode) return true;
859
+ return mask.name.toLowerCase() === mode;
860
+ });
861
+ if (!selected) throw new Error(`Collaboration mode "${mode}" is unavailable`);
862
+ return {
863
+ mode,
864
+ settings: {
865
+ model,
866
+ reasoning_effort: selected.reasoning_effort,
867
+ developer_instructions: selected.developer_instructions
868
+ }
869
+ };
870
+ }
871
+ async function resumeThread(client, threadId) {
872
+ return parseThreadResult(await client.request("thread/resume", { threadId }));
873
+ }
874
+ function parseThreadResult(raw) {
875
+ if (!isThreadResult(raw)) throw new Error("Invalid thread response from Codex");
876
+ return {
877
+ threadId: raw.thread.id,
878
+ model: raw.model,
879
+ cwd: raw.cwd
880
+ };
881
+ }
882
+ function isThreadMissingError(error) {
883
+ if (!(error instanceof Error)) return false;
884
+ return error.message.includes("thread not found");
885
+ }
886
+ function isCollaborationModeMask(value) {
887
+ if (!isRecord(value)) return false;
888
+ const modeIsValid = value.mode === null || value.mode === "default" || value.mode === "plan";
889
+ return typeof value.name === "string" && modeIsValid && (typeof value.model === "string" || value.model === null) && (typeof value.reasoning_effort === "string" || value.reasoning_effort === null) && (typeof value.developer_instructions === "string" || value.developer_instructions === null);
890
+ }
891
+ function isCollaborationModeListResponse(value) {
892
+ if (!isRecord(value) || !Array.isArray(value.data)) return false;
893
+ return value.data.every(isCollaborationModeMask);
894
+ }
895
+ function isThreadResult(value) {
896
+ if (!isRecord(value) || !isRecord(value.thread)) return false;
897
+ return typeof value.thread.id === "string" && typeof value.model === "string";
898
+ }
899
+
900
+ //#endregion
901
+ //#region src/codex/turn-state.ts
902
+ function createTurnAccumulator() {
903
+ return {
904
+ turnCompleted: false,
905
+ turnError: null,
906
+ lastAgentMessageByItem: null,
907
+ lastAgentMessageByTask: null
908
+ };
909
+ }
910
+ function applyTurnNotification(accumulator, notification) {
911
+ if (notification.method === "error") {
912
+ if (isRecord(notification.params) && typeof notification.params.message === "string") accumulator.turnError = notification.params.message;
913
+ else accumulator.turnError = "Codex returned an unknown error event";
914
+ accumulator.turnCompleted = true;
915
+ return;
916
+ }
917
+ if (notification.method === "item/completed") {
918
+ const item = notification.params.item;
919
+ if (item?.type === "agentMessage" && typeof item.text === "string") accumulator.lastAgentMessageByItem = item.text;
920
+ return;
921
+ }
922
+ if (notification.method === "codex/event/task_complete") {
923
+ const message = notification.params.msg?.last_agent_message;
924
+ if (typeof message === "string") accumulator.lastAgentMessageByTask = message;
925
+ return;
926
+ }
927
+ if (notification.method === "turn/completed") {
928
+ const params = notification.params;
929
+ accumulator.turnCompleted = true;
930
+ if (params.turn?.error?.message) {
931
+ accumulator.turnError = params.turn.error.message;
932
+ return;
933
+ }
934
+ if (params.turn?.status === "failed") accumulator.turnError = "Codex turn failed";
935
+ }
936
+ }
937
+ function resolveTurnMessage(accumulator) {
938
+ return accumulator.lastAgentMessageByTask ?? accumulator.lastAgentMessageByItem;
939
+ }
940
+
941
+ //#endregion
942
+ //#region src/codex/app-server.ts
943
+ const DEFAULT_CODEX_BIN$1 = "codex";
944
+ async function createCodexThread(input) {
945
+ const client = new CodexAppServerClient({
946
+ cwd: input.cwd,
947
+ codexBin: input.codexBin ?? DEFAULT_CODEX_BIN$1
948
+ });
949
+ try {
950
+ return await runWithOptionalTimeout(async () => {
951
+ await initializeClient(client);
952
+ const opened = await startThread(client, input.cwd);
953
+ return {
954
+ threadId: opened.threadId,
955
+ model: opened.model,
956
+ mode: input.mode,
957
+ cwd: opened.cwd
958
+ };
959
+ }, input.timeoutMs, () => client.dispose());
960
+ } finally {
961
+ client.dispose();
962
+ }
963
+ }
964
+ async function runCodexTurn(input) {
965
+ const client = new CodexAppServerClient({
966
+ cwd: input.cwd,
967
+ codexBin: input.codexBin ?? DEFAULT_CODEX_BIN$1
968
+ });
969
+ const accumulator = createTurnAccumulator();
970
+ const turnDone = createDeferred();
971
+ let turnDoneResolved = false;
972
+ client.setNotificationHandler((notification) => {
973
+ applyTurnNotification(accumulator, notification);
974
+ if (accumulator.turnCompleted && !turnDoneResolved) {
975
+ turnDoneResolved = true;
976
+ turnDone.resolve();
977
+ }
978
+ });
979
+ try {
980
+ return await runWithOptionalTimeout(async () => {
981
+ await initializeClient(client);
982
+ const modeMasks = await getCollaborationModes(client);
983
+ const opened = await openThread(client, input.session, input.cwd);
984
+ const collaborationMode = selectCollaborationModePayload(modeMasks, input.mode, opened.model);
985
+ await client.request("turn/start", {
986
+ threadId: opened.threadId,
987
+ input: [{
988
+ type: "text",
989
+ text: input.prompt,
990
+ text_elements: []
991
+ }],
992
+ collaborationMode
993
+ });
994
+ await turnDone.promise;
995
+ if (accumulator.turnError) throw new Error(accumulator.turnError);
996
+ const message = resolveTurnMessage(accumulator);
997
+ if (!message || message.trim().length === 0) throw new Error("Codex did not return a message");
998
+ return {
999
+ threadId: opened.threadId,
1000
+ model: opened.model,
1001
+ mode: input.mode,
1002
+ message,
1003
+ cwd: opened.cwd
1004
+ };
1005
+ }, input.timeoutMs, () => {
1006
+ if (!turnDoneResolved) {
1007
+ turnDoneResolved = true;
1008
+ turnDone.reject(/* @__PURE__ */ new Error("Codex execution timed out"));
1009
+ }
1010
+ client.dispose();
1011
+ });
1012
+ } finally {
1013
+ client.dispose();
1014
+ }
1015
+ }
1016
+ async function runWithOptionalTimeout(run, timeoutMs, onTimeout) {
1017
+ if (typeof timeoutMs !== "number" || timeoutMs <= 0) return run();
1018
+ return withTimeout(run, timeoutMs, onTimeout);
1019
+ }
1020
+ function createDeferred() {
1021
+ let resolve;
1022
+ let reject;
1023
+ return {
1024
+ promise: new Promise((innerResolve, innerReject) => {
1025
+ resolve = innerResolve;
1026
+ reject = innerReject;
1027
+ }),
1028
+ resolve,
1029
+ reject
1030
+ };
1031
+ }
1032
+ async function withTimeout(run, timeoutMs, onTimeout) {
1033
+ let timeoutHandle;
1034
+ const timeoutPromise = new Promise((_, reject) => {
1035
+ timeoutHandle = setTimeout(() => {
1036
+ onTimeout();
1037
+ reject(/* @__PURE__ */ new Error(`Codex request timed out after ${timeoutMs}ms`));
1038
+ }, timeoutMs);
1039
+ });
1040
+ try {
1041
+ return await Promise.race([run(), timeoutPromise]);
1042
+ } finally {
1043
+ if (timeoutHandle) clearTimeout(timeoutHandle);
1044
+ }
1045
+ }
1046
+
1047
+ //#endregion
1048
+ //#region src/codex/state.ts
1049
+ async function listOpenProjects() {
1050
+ return { roots: [process.cwd()] };
1051
+ }
1052
+
1053
+ //#endregion
1054
+ //#region src/core/config.ts
1055
+ const DEFAULT_CODEX_BIN = "codex";
1056
+ const TEMPLATE_ENV_CONFIG = {
1057
+ BASE_DOMAIN: "https://open.feishu.cn",
1058
+ APP_ID: "your_app_id",
1059
+ APP_SECRET: "your_app_secret",
1060
+ BOT_OPEN_ID: "ou_xxx",
1061
+ CODEX_BIN: DEFAULT_CODEX_BIN,
1062
+ CODEX_TIMEOUT_MS: null
1063
+ };
1064
+ const TEMPLATE_CONFIG = {
1065
+ locale: getDefaultLocale(),
1066
+ env: TEMPLATE_ENV_CONFIG
1067
+ };
1068
+ function loadRelayConfig(options = {}) {
1069
+ const homeDir = options.homeDir ?? os.homedir();
1070
+ const workspaceCwd = options.workspaceCwd ?? process.cwd();
1071
+ const configDir = path.join(homeDir, ".relay");
1072
+ const configPath = path.join(configDir, "config.json");
1073
+ if (!fs.existsSync(configPath)) {
1074
+ ensureConfigTemplate(configDir, configPath);
1075
+ throw new Error(i18n._({
1076
+ id: "AWIU5i",
1077
+ message: "Relay config is missing. Template created at {configPath}. Please edit this file and restart.",
1078
+ values: { configPath }
1079
+ }));
1080
+ }
1081
+ const parsed = parseConfigFile(configPath);
1082
+ const locale = readLocale(parsed.localeValue);
1083
+ initializeI18n(locale);
1084
+ const domain = readRequiredString(parsed.env.BASE_DOMAIN, "BASE_DOMAIN");
1085
+ return {
1086
+ baseConfig: {
1087
+ appId: readRequiredString(parsed.env.APP_ID, "APP_ID"),
1088
+ appSecret: readRequiredString(parsed.env.APP_SECRET, "APP_SECRET"),
1089
+ domain
1090
+ },
1091
+ botOpenId: readOptionalString(parsed.env.BOT_OPEN_ID, "BOT_OPEN_ID"),
1092
+ codexBin: readOptionalString(parsed.env.CODEX_BIN, "CODEX_BIN") ?? DEFAULT_CODEX_BIN,
1093
+ codexTimeoutMs: readTimeoutMs(parsed.env.CODEX_TIMEOUT_MS),
1094
+ workspaceCwd,
1095
+ locale
1096
+ };
1097
+ }
1098
+ function ensureConfigTemplate(configDir, configPath) {
1099
+ fs.mkdirSync(configDir, { recursive: true });
1100
+ if (fs.existsSync(configPath)) return;
1101
+ fs.writeFileSync(configPath, `${JSON.stringify(TEMPLATE_CONFIG, null, 2)}\n`, {
1102
+ encoding: "utf-8",
1103
+ flag: "wx"
1104
+ });
1105
+ }
1106
+ function parseConfigFile(configPath) {
1107
+ let raw;
1108
+ try {
1109
+ raw = fs.readFileSync(configPath, "utf-8");
1110
+ } catch (error) {
1111
+ throw new Error(i18n._({
1112
+ id: "tZQUtS",
1113
+ message: "Failed to read relay config at {configPath}: {0}",
1114
+ values: {
1115
+ configPath,
1116
+ 0: formatError(error)
1117
+ }
1118
+ }));
1119
+ }
1120
+ let parsed;
1121
+ try {
1122
+ parsed = JSON.parse(raw);
1123
+ } catch (error) {
1124
+ throw new Error(i18n._({
1125
+ id: "z+7ZYe",
1126
+ message: "Invalid JSON in relay config at {configPath}: {0}",
1127
+ values: {
1128
+ configPath,
1129
+ 0: formatError(error)
1130
+ }
1131
+ }));
1132
+ }
1133
+ if (!isObject(parsed)) throw new Error(i18n._({
1134
+ id: "4IDydv",
1135
+ message: "Invalid relay config at {configPath}: root must be a JSON object.",
1136
+ values: { configPath }
1137
+ }));
1138
+ const configObject = parsed;
1139
+ if (configObject.env === void 0) return {
1140
+ env: configObject,
1141
+ localeValue: configObject.locale ?? configObject.LOCALE
1142
+ };
1143
+ if (!isObject(configObject.env)) throw new Error(i18n._({
1144
+ id: "CfFOzJ",
1145
+ message: "Invalid relay config at {configPath}: env must be a JSON object.",
1146
+ values: { configPath }
1147
+ }));
1148
+ return {
1149
+ env: configObject.env,
1150
+ localeValue: configObject.locale ?? configObject.LOCALE
1151
+ };
1152
+ }
1153
+ function readRequiredString(value, field) {
1154
+ const normalized = readOptionalString(value, field);
1155
+ if (!normalized) throw new Error(i18n._({
1156
+ id: "6tcMXX",
1157
+ message: "Invalid relay config: {field} is required and must be a non-empty string.",
1158
+ values: { field }
1159
+ }));
1160
+ return normalized;
1161
+ }
1162
+ function readOptionalString(value, field) {
1163
+ if (value === void 0) return;
1164
+ if (typeof value !== "string") throw new TypeError(i18n._({
1165
+ id: "Ks6r4a",
1166
+ message: "Invalid relay config: {field} must be a string.",
1167
+ values: { field }
1168
+ }));
1169
+ const normalized = value.trim();
1170
+ if (normalized.length === 0) return;
1171
+ return normalized;
1172
+ }
1173
+ function readTimeoutMs(value) {
1174
+ if (value === void 0 || value === null) return;
1175
+ if (typeof value === "number") {
1176
+ if (Number.isInteger(value) && value > 0) return value;
1177
+ throw new Error(i18n._({
1178
+ id: "Hrkm8q",
1179
+ message: "Invalid relay config: CODEX_TIMEOUT_MS must be a positive integer."
1180
+ }));
1181
+ }
1182
+ if (typeof value === "string") {
1183
+ const trimmed = value.trim();
1184
+ if (trimmed.length === 0) return;
1185
+ if (!/^[1-9]\d*$/.test(trimmed)) throw new Error(i18n._({
1186
+ id: "Hrkm8q",
1187
+ message: "Invalid relay config: CODEX_TIMEOUT_MS must be a positive integer."
1188
+ }));
1189
+ return Number.parseInt(trimmed, 10);
1190
+ }
1191
+ throw new Error(i18n._({
1192
+ id: "Hrkm8q",
1193
+ message: "Invalid relay config: CODEX_TIMEOUT_MS must be a positive integer."
1194
+ }));
1195
+ }
1196
+ function readLocale(value) {
1197
+ const defaultLocale = getDefaultLocale();
1198
+ if (value === void 0 || value === null) return defaultLocale;
1199
+ if (typeof value !== "string") {
1200
+ console.warn(i18n._({
1201
+ id: "Nkzhzf",
1202
+ message: "Invalid relay config: locale \"{0}\" is not supported. Falling back to en.",
1203
+ values: { 0: formatInvalidLocale(value) }
1204
+ }));
1205
+ return defaultLocale;
1206
+ }
1207
+ const normalized = value.trim();
1208
+ if (normalized.length === 0) return defaultLocale;
1209
+ if (isSupportedLocale(normalized)) return normalized;
1210
+ console.warn(i18n._({
1211
+ id: "D8aQGU",
1212
+ message: "Invalid relay config: locale \"{normalized}\" is not supported. Falling back to en.",
1213
+ values: { normalized }
1214
+ }));
1215
+ return defaultLocale;
1216
+ }
1217
+ function formatInvalidLocale(value) {
1218
+ if (typeof value === "string") return value;
1219
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
1220
+ try {
1221
+ return JSON.stringify(value);
1222
+ } catch {
1223
+ return String(value);
1224
+ }
1225
+ }
1226
+ function isObject(value) {
1227
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1228
+ }
1229
+ function formatError(error) {
1230
+ if (error instanceof Error) return error.message;
1231
+ return String(error);
1232
+ }
1233
+
1234
+ //#endregion
1235
+ //#region src/core/startup.ts
1236
+ function loadConfigOrExit() {
1237
+ try {
1238
+ return loadRelayConfig();
1239
+ } catch (error) {
1240
+ console.error(formatStartupError(error));
1241
+ process.exit(1);
1242
+ }
1243
+ }
1244
+ function formatStartupError(error) {
1245
+ const message = error instanceof Error ? error.message : String(error);
1246
+ return i18n._({
1247
+ id: "aQiwam",
1248
+ message: "Failed to start relay: {message}",
1249
+ values: { message }
1250
+ });
1251
+ }
1252
+
1253
+ //#endregion
1254
+ //#region src/feishu/reply.ts
1255
+ const FALLBACK_REPLY_TAG = "[no-thread]";
1256
+ async function sendReply(larkClient, data, text) {
1257
+ const content = JSON.stringify({ text: formatReplyTextWithThreadId(data, text) });
1258
+ if (data.message.chat_type === "p2p") {
1259
+ await larkClient.im.v1.message.create({
1260
+ params: { receive_id_type: "chat_id" },
1261
+ data: {
1262
+ receive_id: data.message.chat_id,
1263
+ msg_type: "text",
1264
+ content
1265
+ }
1266
+ });
1267
+ return;
1268
+ }
1269
+ await larkClient.im.v1.message.reply({
1270
+ path: { message_id: data.message.message_id },
1271
+ data: {
1272
+ msg_type: "text",
1273
+ content
1274
+ }
1275
+ });
1276
+ }
1277
+ function formatReplyTextWithThreadId(data, text) {
1278
+ const replyTag = resolveReplyTag(data);
1279
+ const normalizedText = text.trim();
1280
+ if (normalizedText.length === 0) return `${replyTag}\n`;
1281
+ return `${replyTag}\n\n${normalizedText}`;
1282
+ }
1283
+ function resolveReplyTag(data) {
1284
+ const senderId = resolveSenderId(data.sender.sender_id);
1285
+ if (!senderId) return FALLBACK_REPLY_TAG;
1286
+ const session = getSession(getSessionKey({
1287
+ chatType: data.message.chat_type,
1288
+ chatId: data.message.chat_id,
1289
+ userId: senderId
1290
+ }));
1291
+ if (!session || session.threadId.trim().length === 0) return FALLBACK_REPLY_TAG;
1292
+ return `[${session.threadId}]`;
1293
+ }
1294
+
1295
+ //#endregion
1296
+ //#region src/index.ts
1297
+ const relayConfig = loadConfigOrExit();
1298
+ initializeI18n(relayConfig.locale);
1299
+ const BUSY_MESSAGE = i18n._({
1300
+ id: "H7VlDR",
1301
+ message: "Currently busy. Please try again later."
1302
+ });
1303
+ const client = new Lark.Client(relayConfig.baseConfig);
1304
+ const wsClient = new Lark.WSClient(relayConfig.baseConfig);
1305
+ let isTaskRunning = false;
1306
+ async function processIncomingEvent(data) {
1307
+ try {
1308
+ const reply = await buildReplyForMessageEvent(data, {
1309
+ botOpenId: relayConfig.botOpenId,
1310
+ handleIncomingText: (input) => handleIncomingText(input, {
1311
+ createThread: (mode) => createCodexThread({
1312
+ mode,
1313
+ cwd: relayConfig.workspaceCwd,
1314
+ codexBin: relayConfig.codexBin,
1315
+ timeoutMs: relayConfig.codexTimeoutMs
1316
+ }),
1317
+ runTurn: (params) => runCodexTurn({
1318
+ ...params,
1319
+ cwd: relayConfig.workspaceCwd,
1320
+ codexBin: relayConfig.codexBin,
1321
+ timeoutMs: relayConfig.codexTimeoutMs
1322
+ }),
1323
+ getSession,
1324
+ setSession,
1325
+ clearSession,
1326
+ withSessionLock,
1327
+ listOpenProjects
1328
+ })
1329
+ });
1330
+ if (reply === null) return;
1331
+ await sendReply(client, data, reply);
1332
+ } catch (error) {
1333
+ console.error("failed to handle Feishu message", error);
1334
+ try {
1335
+ await sendReply(client, data, i18n._({
1336
+ id: "FZcpfm",
1337
+ message: "Failed to process message. Please try again later."
1338
+ }));
1339
+ } catch (replyError) {
1340
+ console.error("failed to send failure message", replyError);
1341
+ }
1342
+ }
1343
+ }
1344
+ const eventDispatcher = new Lark.EventDispatcher({}).register({ "im.message.receive_v1": async (data) => {
1345
+ console.info("feishu message received\n", JSON.stringify(data, null, 2), "\n");
1346
+ if (!shouldProcessMessage(data, relayConfig.botOpenId)) return;
1347
+ if (isTaskRunning) {
1348
+ sendReply(client, data, BUSY_MESSAGE);
1349
+ return;
1350
+ }
1351
+ isTaskRunning = true;
1352
+ processIncomingEvent(data).finally(() => {
1353
+ isTaskRunning = false;
1354
+ });
1355
+ } });
1356
+ wsClient.start({ eventDispatcher });
1357
+
1358
+ //#endregion
1359
+ export { };
1360
+ //# sourceMappingURL=index.mjs.map