feishu-codex-connector 0.1.6

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.
@@ -0,0 +1,3876 @@
1
+ // src/config/paths.ts
2
+ import { homedir, tmpdir } from "os";
3
+ import { dirname, join, resolve, sep } from "path";
4
+ var DEFAULT_HOME_DIR = "~/.feishu-codex";
5
+ function expandTilde(path, home = homedir()) {
6
+ if (path === "~") return home;
7
+ if (path.startsWith(`~${sep}`) || path.startsWith("~/")) {
8
+ return join(home, path.slice(2));
9
+ }
10
+ return path;
11
+ }
12
+ function getBridgeHome(env = process.env) {
13
+ return resolve(expandTilde(env.FEISHU_CODEX_HOME || DEFAULT_HOME_DIR));
14
+ }
15
+ function bridgePaths(home = getBridgeHome()) {
16
+ return {
17
+ home,
18
+ configFile: join(home, "config.json"),
19
+ activeProfileFile: join(home, "active-profile"),
20
+ registryDir: join(home, "registry"),
21
+ profilesDir: join(home, "profiles")
22
+ };
23
+ }
24
+ function profilePaths(profile, home = getBridgeHome()) {
25
+ const root = join(bridgePaths(home).profilesDir, profile);
26
+ return {
27
+ root,
28
+ config: join(root, "config.json"),
29
+ sessions: join(root, "sessions.json"),
30
+ sessionCatalog: join(root, "session-catalog.json"),
31
+ workspaces: join(root, "workspaces.json"),
32
+ mediaDir: join(root, "media"),
33
+ logsDir: join(root, "logs"),
34
+ secretsKey: join(root, "secrets.key"),
35
+ secretsStore: join(root, "secrets.enc"),
36
+ codexHome: join(root, "codex-home"),
37
+ feishuCliDir: join(root, "feishu-cli")
38
+ };
39
+ }
40
+ function parentDir(path) {
41
+ return dirname(path);
42
+ }
43
+ function tempRoot() {
44
+ return resolve(tmpdir());
45
+ }
46
+
47
+ // src/config/keystore.ts
48
+ import { createCipheriv, createDecipheriv, randomBytes as randomBytes2 } from "crypto";
49
+ import { readFile as readFile2, writeFile as writeFile2, chmod as chmod2 } from "fs/promises";
50
+ import { dirname as dirname3 } from "path";
51
+
52
+ // src/platform/atomic.ts
53
+ import { randomBytes } from "crypto";
54
+ import { mkdir, readFile, rename, writeFile, chmod } from "fs/promises";
55
+ import { dirname as dirname2 } from "path";
56
+ async function ensureDir(path, mode = 448) {
57
+ await mkdir(path, { recursive: true, mode });
58
+ await chmod(path, mode).catch(() => void 0);
59
+ }
60
+ async function readJsonFile(path, fallback) {
61
+ try {
62
+ const raw = await readFile(path, "utf8");
63
+ return JSON.parse(raw);
64
+ } catch (error) {
65
+ if (error.code === "ENOENT") return fallback;
66
+ throw error;
67
+ }
68
+ }
69
+ async function writeTextAtomic(path, content, mode = 384) {
70
+ await ensureDir(dirname2(path));
71
+ const tmp = `${path}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
72
+ await writeFile(tmp, content, { mode });
73
+ await chmod(tmp, mode).catch(() => void 0);
74
+ await rename(tmp, path);
75
+ }
76
+ async function writeJsonAtomic(path, value, mode = 384) {
77
+ await writeTextAtomic(path, `${JSON.stringify(value, null, 2)}
78
+ `, mode);
79
+ }
80
+
81
+ // src/config/keystore.ts
82
+ var LocalKeystore = class {
83
+ constructor(paths) {
84
+ this.paths = paths;
85
+ }
86
+ paths;
87
+ async set(id, value) {
88
+ const store = await this.readPlain();
89
+ store[id] = value;
90
+ await this.writePlain(store);
91
+ }
92
+ async get(id) {
93
+ return (await this.readPlain())[id];
94
+ }
95
+ async delete(id) {
96
+ const store = await this.readPlain();
97
+ delete store[id];
98
+ await this.writePlain(store);
99
+ }
100
+ async export(includeSecrets = false) {
101
+ const store = await this.readPlain();
102
+ if (includeSecrets) return store;
103
+ return Object.fromEntries(Object.keys(store).map((id) => [id, "<redacted>"]));
104
+ }
105
+ async key() {
106
+ try {
107
+ const raw = await readFile2(this.paths.secretsKey);
108
+ if (raw.length !== 32) throw new Error("invalid keystore key length");
109
+ return raw;
110
+ } catch (error) {
111
+ if (error.code !== "ENOENT") throw error;
112
+ await ensureDir(dirname3(this.paths.secretsKey));
113
+ const key = randomBytes2(32);
114
+ await writeFile2(this.paths.secretsKey, key, { mode: 384 });
115
+ await chmod2(this.paths.secretsKey, 384).catch(() => void 0);
116
+ return key;
117
+ }
118
+ }
119
+ async readPlain() {
120
+ const encrypted = await readJsonFile(this.paths.secretsStore, null);
121
+ if (!encrypted) return {};
122
+ const key = await this.key();
123
+ const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(encrypted.iv, "base64url"));
124
+ decipher.setAuthTag(Buffer.from(encrypted.tag, "base64url"));
125
+ const plain = Buffer.concat([
126
+ decipher.update(Buffer.from(encrypted.ciphertext, "base64url")),
127
+ decipher.final()
128
+ ]);
129
+ return JSON.parse(plain.toString("utf8"));
130
+ }
131
+ async writePlain(store) {
132
+ const key = await this.key();
133
+ const iv = randomBytes2(12);
134
+ const cipher = createCipheriv("aes-256-gcm", key, iv);
135
+ const ciphertext = Buffer.concat([cipher.update(JSON.stringify(store), "utf8"), cipher.final()]);
136
+ const encrypted = {
137
+ version: 1,
138
+ iv: iv.toString("base64url"),
139
+ tag: cipher.getAuthTag().toString("base64url"),
140
+ ciphertext: ciphertext.toString("base64url")
141
+ };
142
+ await writeJsonAtomic(this.paths.secretsStore, encrypted);
143
+ }
144
+ };
145
+
146
+ // src/config/schema.ts
147
+ function createDefaultProfileConfig(input2) {
148
+ const paths = profilePaths(input2.name, input2.home);
149
+ return normalizeProfileConfig({
150
+ schemaVersion: 1,
151
+ name: input2.name,
152
+ tenant: input2.tenant,
153
+ app: {
154
+ id: input2.appId,
155
+ secret: input2.appSecret
156
+ },
157
+ codex: {
158
+ backend: input2.codexBackend ?? "sdk",
159
+ auth: input2.codexAuth,
160
+ codexHome: paths.codexHome,
161
+ inheritUserCodexHome: input2.codexAuth.mode === "inherit-user",
162
+ ...input2.codexPath ? { codexPath: input2.codexPath } : {}
163
+ },
164
+ feishuCli: {
165
+ enabled: true,
166
+ identityPreset: "bot-only",
167
+ configDir: paths.feishuCliDir
168
+ },
169
+ permissions: {
170
+ defaultAccess: "workspace",
171
+ maxAccess: "workspace",
172
+ networkAccess: false
173
+ },
174
+ access: {
175
+ allowedUsers: [],
176
+ allowedChats: [],
177
+ admins: [],
178
+ requireMentionInGroup: true,
179
+ allowUserCliInGroups: false
180
+ },
181
+ preferences: {
182
+ replyMode: "card",
183
+ showToolCalls: true,
184
+ maxConcurrency: 2,
185
+ debounceMs: 600
186
+ },
187
+ attachments: {
188
+ maxCount: 10,
189
+ maxBytes: 100 * 1024 * 1024,
190
+ maxFileBytes: 25 * 1024 * 1024,
191
+ imageMaxBytes: 25 * 1024 * 1024,
192
+ cacheTtlMs: 24 * 60 * 60 * 1e3
193
+ },
194
+ workspaces: input2.workspace ? { default: input2.workspace } : {}
195
+ });
196
+ }
197
+ function normalizeProfileConfig(value) {
198
+ if (!isRecord(value)) throw new Error("profile config must be an object");
199
+ if (value.schemaVersion !== 1) throw new Error("profile schemaVersion must be 1");
200
+ const name = stringRequired(value.name, "name");
201
+ const tenant = value.tenant === "feishu" || value.tenant === "lark" ? value.tenant : fail("tenant must be feishu or lark");
202
+ const app = normalizeApp(value.app);
203
+ const codex = normalizeCodex(value.codex, name);
204
+ const feishuCli = normalizeFeishuCli(value.feishuCli, name);
205
+ return {
206
+ schemaVersion: 1,
207
+ name,
208
+ tenant,
209
+ app,
210
+ codex,
211
+ feishuCli,
212
+ permissions: normalizePermissions(value.permissions),
213
+ access: normalizeAccess(value.access),
214
+ preferences: normalizePreferences(value.preferences),
215
+ attachments: normalizeAttachments(value.attachments),
216
+ workspaces: normalizeWorkspaces(value.workspaces)
217
+ };
218
+ }
219
+ function isSecretRef(value) {
220
+ if (!isRecord(value) || typeof value.source !== "string") return false;
221
+ if (value.source === "keystore") return typeof value.id === "string" && value.id.length > 0;
222
+ if (value.source === "env") return typeof value.name === "string" && value.name.length > 0;
223
+ if (value.source === "file") return typeof value.path === "string" && value.path.length > 0;
224
+ if (value.source === "exec") {
225
+ return typeof value.command === "string" && (!value.args || Array.isArray(value.args) && stringArray(value.args).length === value.args.length);
226
+ }
227
+ return false;
228
+ }
229
+ function redactProfileConfig(profile) {
230
+ return JSON.parse(
231
+ JSON.stringify(profile, (_key, value) => {
232
+ if (isSecretRef(value)) return { ...value, redacted: true };
233
+ return value;
234
+ })
235
+ );
236
+ }
237
+ function normalizeApp(value) {
238
+ if (!isRecord(value)) throw new Error("app config is required");
239
+ const id = stringRequired(value.id, "app.id");
240
+ if (!isSecretRef(value.secret)) throw new Error("app.secret must be a secret reference");
241
+ return { id, secret: value.secret };
242
+ }
243
+ function normalizeCodex(value, name) {
244
+ if (!isRecord(value)) throw new Error("codex config is required");
245
+ const backend = value.backend === "sdk" || value.backend === "exec-json" || value.backend === "app-server" ? value.backend : "sdk";
246
+ const auth = normalizeCodexAuth(value.auth);
247
+ const paths = profilePaths(name);
248
+ return {
249
+ backend,
250
+ auth,
251
+ codexHome: typeof value.codexHome === "string" ? value.codexHome : paths.codexHome,
252
+ inheritUserCodexHome: Boolean(value.inheritUserCodexHome),
253
+ ...typeof value.codexPath === "string" ? { codexPath: value.codexPath } : {},
254
+ ...typeof value.baseUrl === "string" ? { baseUrl: value.baseUrl } : {},
255
+ ...isRecord(value.config) ? { config: value.config } : {}
256
+ };
257
+ }
258
+ function normalizeCodexAuth(value) {
259
+ if (!isRecord(value)) throw new Error("codex.auth is required");
260
+ if (value.mode === "api-key") {
261
+ if (!isSecretRef(value.apiKey)) throw new Error("codex.auth.apiKey must be a secret reference");
262
+ return { mode: "api-key", apiKey: value.apiKey };
263
+ }
264
+ if (value.mode === "codex-home") return { mode: "codex-home" };
265
+ if (value.mode === "inherit-user") return { mode: "inherit-user" };
266
+ throw new Error("codex.auth.mode must be api-key, codex-home, or inherit-user");
267
+ }
268
+ function normalizeFeishuCli(value, name) {
269
+ const paths = profilePaths(name);
270
+ if (!isRecord(value)) {
271
+ return { enabled: true, identityPreset: "bot-only", configDir: paths.feishuCliDir };
272
+ }
273
+ const preset = value.identityPreset === "user-default" || value.identityPreset === "disabled" ? value.identityPreset : "bot-only";
274
+ return {
275
+ enabled: value.enabled !== false && preset !== "disabled",
276
+ identityPreset: preset,
277
+ configDir: typeof value.configDir === "string" ? value.configDir : paths.feishuCliDir
278
+ };
279
+ }
280
+ function normalizePermissions(value) {
281
+ const raw = isRecord(value) ? value : {};
282
+ return {
283
+ defaultAccess: accessMode(raw.defaultAccess, "workspace"),
284
+ maxAccess: accessMode(raw.maxAccess, "workspace"),
285
+ networkAccess: Boolean(raw.networkAccess)
286
+ };
287
+ }
288
+ function normalizeAccess(value) {
289
+ const raw = isRecord(value) ? value : {};
290
+ return {
291
+ allowedUsers: stringArray(raw.allowedUsers),
292
+ allowedChats: stringArray(raw.allowedChats),
293
+ admins: stringArray(raw.admins),
294
+ requireMentionInGroup: raw.requireMentionInGroup !== false,
295
+ allowUserCliInGroups: Boolean(raw.allowUserCliInGroups)
296
+ };
297
+ }
298
+ function normalizePreferences(value) {
299
+ const raw = isRecord(value) ? value : {};
300
+ return {
301
+ replyMode: raw.replyMode === "markdown" || raw.replyMode === "text" ? raw.replyMode : "card",
302
+ showToolCalls: raw.showToolCalls !== false,
303
+ maxConcurrency: positiveInt(raw.maxConcurrency, 2),
304
+ debounceMs: positiveInt(raw.debounceMs, 600),
305
+ ...typeof raw.idleTimeoutMinutes === "number" ? { idleTimeoutMinutes: raw.idleTimeoutMinutes } : {}
306
+ };
307
+ }
308
+ function normalizeAttachments(value) {
309
+ const raw = isRecord(value) ? value : {};
310
+ return {
311
+ maxCount: positiveInt(raw.maxCount, 10),
312
+ maxBytes: positiveInt(raw.maxBytes, 100 * 1024 * 1024),
313
+ maxFileBytes: positiveInt(raw.maxFileBytes, 25 * 1024 * 1024),
314
+ imageMaxBytes: positiveInt(raw.imageMaxBytes, 25 * 1024 * 1024),
315
+ cacheTtlMs: positiveInt(raw.cacheTtlMs, 24 * 60 * 60 * 1e3)
316
+ };
317
+ }
318
+ function normalizeWorkspaces(value) {
319
+ const raw = isRecord(value) ? value : {};
320
+ return typeof raw.default === "string" && raw.default.length > 0 ? { default: raw.default } : {};
321
+ }
322
+ function accessMode(value, fallback) {
323
+ return value === "read-only" || value === "workspace" || value === "full" ? value : fallback;
324
+ }
325
+ function positiveInt(value, fallback) {
326
+ return Number.isInteger(value) && Number(value) > 0 ? Number(value) : fallback;
327
+ }
328
+ function stringRequired(value, label) {
329
+ if (typeof value === "string" && value.length > 0) return value;
330
+ throw new Error(`${label} is required`);
331
+ }
332
+ function stringArray(value) {
333
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
334
+ }
335
+ function isRecord(value) {
336
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
337
+ }
338
+ function fail(message) {
339
+ throw new Error(message);
340
+ }
341
+
342
+ // src/config/profile-store.ts
343
+ import { constants } from "fs";
344
+ import { access, cp, mkdir as mkdir2, readdir, readFile as readFile3, rm, rename as rename2, stat } from "fs/promises";
345
+ import { basename, join as join2 } from "path";
346
+ var ProfileStore = class {
347
+ constructor(home = bridgePaths().home) {
348
+ this.home = home;
349
+ this.paths = bridgePaths(home);
350
+ }
351
+ home;
352
+ paths;
353
+ profilePaths(name) {
354
+ return profilePaths(name, this.home);
355
+ }
356
+ keystore(name) {
357
+ return new LocalKeystore(this.profilePaths(name));
358
+ }
359
+ async init() {
360
+ await ensureDir(this.paths.home);
361
+ await ensureDir(this.paths.profilesDir);
362
+ await ensureDir(this.paths.registryDir);
363
+ }
364
+ async create(input2) {
365
+ await this.init();
366
+ const paths = this.profilePaths(input2.name);
367
+ await ensureDir(paths.root);
368
+ await ensureDir(paths.mediaDir);
369
+ await ensureDir(paths.logsDir);
370
+ await ensureDir(paths.codexHome);
371
+ await ensureDir(paths.feishuCliDir);
372
+ const keystore = this.keystore(input2.name);
373
+ const appSecretRef = { source: "keystore", id: `app-${input2.appId}` };
374
+ await keystore.set(appSecretRef.id, input2.appSecret);
375
+ let auth;
376
+ if (input2.codexAuthMode === "api-key") {
377
+ if (!input2.codexApiKey) throw new Error("codexApiKey is required for api-key auth mode");
378
+ const apiKeyRef = { source: "keystore", id: `openai-api-key-${input2.name}` };
379
+ await keystore.set(apiKeyRef.id, input2.codexApiKey);
380
+ auth = { mode: "api-key", apiKey: apiKeyRef };
381
+ } else {
382
+ auth = { mode: input2.codexAuthMode };
383
+ }
384
+ const profile = createDefaultProfileConfig({
385
+ name: input2.name,
386
+ tenant: input2.tenant,
387
+ appId: input2.appId,
388
+ appSecret: appSecretRef,
389
+ codexAuth: auth,
390
+ workspace: input2.workspace,
391
+ home: this.home,
392
+ codexBackend: input2.codexBackend,
393
+ codexPath: input2.codexPath
394
+ });
395
+ await writeJsonAtomic(paths.config, profile);
396
+ await writeJsonAtomic(paths.sessions, {});
397
+ await writeJsonAtomic(paths.sessionCatalog, { schemaVersion: 1, entries: [] });
398
+ await writeJsonAtomic(paths.workspaces, { schemaVersion: 1, currentByScope: {}, aliases: {} });
399
+ if (input2.setActive !== false) await this.use(input2.name);
400
+ await this.writeRootConfig();
401
+ return profile;
402
+ }
403
+ async read(name) {
404
+ const profileName = name ?? await this.activeProfile();
405
+ if (!profileName) throw new Error("no active profile");
406
+ const raw = await readJsonFile(this.profilePaths(profileName).config, null);
407
+ if (!raw) throw new Error(`profile not found: ${profileName}`);
408
+ return normalizeProfileConfig(raw);
409
+ }
410
+ async write(profile) {
411
+ await writeJsonAtomic(this.profilePaths(profile.name).config, normalizeProfileConfig(profile));
412
+ await this.writeRootConfig();
413
+ }
414
+ async list() {
415
+ await this.init();
416
+ const active = await this.activeProfile().catch(() => void 0);
417
+ const entries = await readdir(this.paths.profilesDir, { withFileTypes: true }).catch(() => []);
418
+ const summaries = [];
419
+ for (const entry of entries) {
420
+ if (!entry.isDirectory()) continue;
421
+ try {
422
+ const profile = await this.read(entry.name);
423
+ summaries.push({
424
+ name: profile.name,
425
+ active: profile.name === active,
426
+ tenant: profile.tenant,
427
+ appIdSuffix: suffix(profile.app.id),
428
+ workspaceStatus: profile.workspaces.default ? "configured" : "missing",
429
+ backend: profile.codex.backend,
430
+ codexAuthMode: profile.codex.auth.mode,
431
+ feishuCliIdentity: profile.feishuCli.identityPreset
432
+ });
433
+ } catch {
434
+ }
435
+ }
436
+ return summaries.sort((a, b) => a.name.localeCompare(b.name));
437
+ }
438
+ async use(name) {
439
+ await this.assertExists(name);
440
+ await writeTextAtomic(this.paths.activeProfileFile, `${name}
441
+ `);
442
+ await this.writeRootConfig(name);
443
+ }
444
+ async activeProfile() {
445
+ try {
446
+ const raw = await readFile3(this.paths.activeProfileFile, "utf8");
447
+ const value = raw.trim();
448
+ return value.length > 0 ? value : void 0;
449
+ } catch (error) {
450
+ if (error.code === "ENOENT") return void 0;
451
+ throw error;
452
+ }
453
+ }
454
+ async remove(name, opts = {}) {
455
+ const paths = this.profilePaths(name);
456
+ await this.assertExists(name);
457
+ if (opts.purge) {
458
+ if (!opts.yes) throw new Error("profile purge requires --yes");
459
+ await rm(paths.root, { recursive: true, force: true });
460
+ await this.clearActiveIf(name);
461
+ await this.writeRootConfig();
462
+ return "purged";
463
+ }
464
+ const archived = `${paths.root}.archived-${Date.now()}`;
465
+ await rename2(paths.root, archived);
466
+ await this.clearActiveIf(name);
467
+ await this.writeRootConfig();
468
+ return archived;
469
+ }
470
+ async export(name, opts = {}) {
471
+ const profile = await this.read(name);
472
+ if (!opts.includeSecrets) return redactProfileConfig(profile);
473
+ return {
474
+ profile,
475
+ secrets: await this.keystore(name).export(true)
476
+ };
477
+ }
478
+ async importFilesFrom(name, sourceDir) {
479
+ const target = this.profilePaths(name).root;
480
+ await mkdir2(target, { recursive: true });
481
+ await cp(sourceDir, target, { recursive: true, force: true });
482
+ }
483
+ async assertExists(name) {
484
+ await access(this.profilePaths(name).config, constants.R_OK);
485
+ }
486
+ async writeRootConfig(activeOverride) {
487
+ await this.init();
488
+ const entries = await readdir(this.paths.profilesDir, { withFileTypes: true }).catch(() => []);
489
+ const profiles = {};
490
+ for (const entry of entries) {
491
+ if (!entry.isDirectory()) continue;
492
+ try {
493
+ profiles[entry.name] = await this.read(entry.name);
494
+ } catch {
495
+ }
496
+ }
497
+ const activeProfile = activeOverride ?? await this.activeProfile().catch(() => void 0);
498
+ const root = {
499
+ schemaVersion: 1,
500
+ ...activeProfile ? { activeProfile } : {},
501
+ profiles
502
+ };
503
+ await writeJsonAtomic(this.paths.configFile, root);
504
+ }
505
+ async clearActiveIf(name) {
506
+ const active = await this.activeProfile();
507
+ if (active !== name) return;
508
+ const next = (await this.list()).find((summary) => summary.name !== name);
509
+ if (next) {
510
+ await this.use(next.name);
511
+ } else {
512
+ await writeTextAtomic(this.paths.activeProfileFile, "");
513
+ }
514
+ }
515
+ };
516
+ async function profileExists(path) {
517
+ try {
518
+ return (await stat(path)).isDirectory();
519
+ } catch {
520
+ return false;
521
+ }
522
+ }
523
+ function suffix(value) {
524
+ return value.length <= 6 ? value : value.slice(-6);
525
+ }
526
+ function profileNameFromPath(path) {
527
+ return basename(path);
528
+ }
529
+ function profileLogPath(paths, kind) {
530
+ return join2(paths.logsDir, `${kind}.jsonl`);
531
+ }
532
+
533
+ // src/config/secret-resolver.ts
534
+ import { readFile as readFile4 } from "fs/promises";
535
+ import { spawn } from "child_process";
536
+ var SecretResolver = class {
537
+ constructor(options = {}) {
538
+ this.options = options;
539
+ this.env = options.env ?? process.env;
540
+ }
541
+ options;
542
+ env;
543
+ async resolve(ref) {
544
+ if (ref.source === "keystore") {
545
+ const value = await this.options.keystore?.get(ref.id);
546
+ if (!value) throw new Error(`secret not found: ${ref.id}`);
547
+ return value;
548
+ }
549
+ if (ref.source === "env") {
550
+ const value = this.env[ref.name];
551
+ if (!value) throw new Error(`environment secret not set: ${ref.name}`);
552
+ return value;
553
+ }
554
+ if (ref.source === "file") {
555
+ return (await readFile4(expandTilde(ref.path), "utf8")).trim();
556
+ }
557
+ return runSecretCommand(ref.command, ref.args ?? []);
558
+ }
559
+ };
560
+ function runSecretCommand(command, args) {
561
+ return new Promise((resolve3, reject3) => {
562
+ const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
563
+ const stdout = [];
564
+ const stderr = [];
565
+ child.stdout.on("data", (chunk) => stdout.push(chunk));
566
+ child.stderr.on("data", (chunk) => stderr.push(chunk));
567
+ child.on("error", reject3);
568
+ child.on("exit", (code) => {
569
+ if (code === 0) {
570
+ resolve3(Buffer.concat(stdout).toString("utf8").trim());
571
+ } else {
572
+ reject3(new Error(`secret exec failed with code ${code}: ${Buffer.concat(stderr).toString("utf8").trim()}`));
573
+ }
574
+ });
575
+ });
576
+ }
577
+
578
+ // src/gateway/normalizer.ts
579
+ import { createHash } from "crypto";
580
+ function scopeForChat(input2) {
581
+ if (input2.fileToken && input2.documentLevel) return `doc:${hashId(input2.fileToken)}`;
582
+ if (input2.fileToken && input2.commentId) {
583
+ return `comment:${hashId(input2.fileToken)}:${hashId(input2.commentId)}:${input2.nonce ?? "default"}`;
584
+ }
585
+ if (!input2.chatId) throw new Error("chatId is required for chat scope");
586
+ if (input2.threadId) return `topic:${input2.chatId}:${input2.threadId}`;
587
+ return `chat:${input2.chatId}`;
588
+ }
589
+ function messageRequiresMention(message, botOpenId) {
590
+ if (message.chatType === "private") return false;
591
+ return !hasRealBotMention(message.mentions, botOpenId);
592
+ }
593
+ function hasRealBotMention(mentions, botOpenId) {
594
+ return mentions.some((mention) => !mention.isAll && (mention.isBot || botOpenId && mention.id === botOpenId));
595
+ }
596
+ function hashId(value) {
597
+ return createHash("sha256").update(value).digest("base64url").slice(0, 16);
598
+ }
599
+
600
+ // src/runtime/locks.ts
601
+ import { mkdir as mkdir3, readFile as readFile5, rm as rm2, writeFile as writeFile3 } from "fs/promises";
602
+ import { join as join3 } from "path";
603
+ var RuntimeLock = class {
604
+ constructor(path) {
605
+ this.path = path;
606
+ }
607
+ path;
608
+ locked = false;
609
+ async acquire() {
610
+ await ensureDir(join3(this.path, ".."));
611
+ for (let attempt = 0; attempt < 2; attempt += 1) {
612
+ try {
613
+ await mkdir3(this.path, { mode: 448 });
614
+ await writeFile3(join3(this.path, "pid"), String(process.pid), { mode: 384 });
615
+ this.locked = true;
616
+ return;
617
+ } catch (error) {
618
+ if (error.code === "EEXIST" && attempt === 0 && await this.isStale()) {
619
+ await rm2(this.path, { recursive: true, force: true });
620
+ continue;
621
+ }
622
+ if (error.code === "EEXIST") {
623
+ throw new Error(`runtime lock already held: ${this.path}`);
624
+ }
625
+ throw error;
626
+ }
627
+ }
628
+ }
629
+ async release() {
630
+ if (!this.locked) return;
631
+ await rm2(this.path, { recursive: true, force: true });
632
+ this.locked = false;
633
+ }
634
+ async isStale() {
635
+ try {
636
+ const pid = Number((await readFile5(join3(this.path, "pid"), "utf8")).trim());
637
+ if (!Number.isInteger(pid) || pid <= 0) return true;
638
+ try {
639
+ process.kill(pid, 0);
640
+ return false;
641
+ } catch {
642
+ return true;
643
+ }
644
+ } catch {
645
+ return true;
646
+ }
647
+ }
648
+ };
649
+ function profileLock(profile, home = bridgePaths().home) {
650
+ return new RuntimeLock(join3(bridgePaths(home).registryDir, "locks", `profile-${profile}.lock`));
651
+ }
652
+ function appLock(appId, home = bridgePaths().home) {
653
+ return new RuntimeLock(join3(bridgePaths(home).registryDir, "locks", `app-${appId}.lock`));
654
+ }
655
+
656
+ // src/runtime/registry.ts
657
+ import { randomBytes as randomBytes3 } from "crypto";
658
+ import { hostname, platform } from "os";
659
+ import { join as join4 } from "path";
660
+ var RuntimeRegistry = class {
661
+ path;
662
+ constructor(home = bridgePaths().home) {
663
+ this.path = join4(bridgePaths(home).registryDir, "processes.json");
664
+ }
665
+ async register(record3) {
666
+ await ensureDir(join4(this.path, ".."));
667
+ const file = await this.read();
668
+ const next = {
669
+ id: record3.id ?? shortId(),
670
+ pid: record3.pid ?? process.pid,
671
+ profile: record3.profile,
672
+ tenant: record3.tenant,
673
+ appId: record3.appId,
674
+ botName: record3.botName,
675
+ startedAt: record3.startedAt ?? Date.now(),
676
+ host: hostname(),
677
+ platform: platform(),
678
+ logPath: record3.logPath
679
+ };
680
+ file.processes = file.processes.filter((entry) => isAlive(entry.pid) && entry.pid !== next.pid && entry.id !== next.id);
681
+ file.processes.push(next);
682
+ await this.write(file);
683
+ return next;
684
+ }
685
+ async unregister(idOrPid) {
686
+ const file = await this.read();
687
+ file.processes = file.processes.filter((entry) => entry.id !== String(idOrPid) && entry.pid !== Number(idOrPid));
688
+ await this.write(file);
689
+ }
690
+ async list() {
691
+ const file = await this.read();
692
+ const live = file.processes.filter((entry) => isAlive(entry.pid));
693
+ if (live.length !== file.processes.length) await this.write({ ...file, processes: live });
694
+ return live;
695
+ }
696
+ async find(idOrPid) {
697
+ const needle = String(idOrPid);
698
+ return (await this.list()).find((entry) => entry.id === needle || String(entry.pid) === needle);
699
+ }
700
+ async kill(idOrPid, signal = "SIGTERM") {
701
+ const entry = await this.find(idOrPid);
702
+ if (!entry) return void 0;
703
+ process.kill(entry.pid, signal);
704
+ return entry;
705
+ }
706
+ async read() {
707
+ const file = await readJsonFile(this.path, { schemaVersion: 1, processes: [] });
708
+ return { schemaVersion: 1, processes: Array.isArray(file.processes) ? file.processes : [] };
709
+ }
710
+ write(file) {
711
+ return writeJsonAtomic(this.path, file);
712
+ }
713
+ };
714
+ function isAlive(pid) {
715
+ try {
716
+ process.kill(pid, 0);
717
+ return true;
718
+ } catch (error) {
719
+ return error.code === "EPERM";
720
+ }
721
+ }
722
+ function shortId() {
723
+ return randomBytes3(2).toString("hex");
724
+ }
725
+
726
+ // src/runner/sdk-translator.ts
727
+ var SdkEventTranslator = class {
728
+ textByItem = /* @__PURE__ */ new Map();
729
+ translate(event) {
730
+ if (event.type === "thread.started") return [{ type: "thread", threadId: event.thread_id }];
731
+ if (event.type === "turn.completed") {
732
+ return [usageEvent(event.usage), { type: "done", reason: "normal" }];
733
+ }
734
+ if (event.type === "turn.failed") {
735
+ return [{ type: "error", message: event.error.message }];
736
+ }
737
+ if (event.type === "error") return [{ type: "error", message: event.message }];
738
+ if (event.type === "item.started") return this.itemStarted(event.item);
739
+ if (event.type === "item.updated") return this.itemUpdated(event.item);
740
+ if (event.type === "item.completed") return this.itemCompleted(event.item);
741
+ return [];
742
+ }
743
+ itemStarted(item) {
744
+ if (item.type === "command_execution") {
745
+ return [{ type: "tool_start", id: item.id, name: "shell", input: { command: item.command } }];
746
+ }
747
+ if (item.type === "mcp_tool_call") {
748
+ return [{ type: "tool_start", id: item.id, name: `${item.server}.${item.tool}`, input: item.arguments }];
749
+ }
750
+ if (item.type === "web_search") {
751
+ return [{ type: "tool_start", id: item.id, name: "web_search", input: { query: item.query } }];
752
+ }
753
+ if (item.type === "file_change") {
754
+ return [{ type: "tool_start", id: item.id, name: "file_change", input: item.changes }];
755
+ }
756
+ if (item.type === "todo_list") {
757
+ return [{ type: "tool_start", id: item.id, name: "todo_list", input: item.items }];
758
+ }
759
+ return this.textDelta(item);
760
+ }
761
+ itemUpdated(item) {
762
+ if (item.type === "command_execution" && item.status === "completed") {
763
+ return [{ type: "tool_end", id: item.id, output: item.aggregated_output, isError: false }];
764
+ }
765
+ return this.textDelta(item);
766
+ }
767
+ itemCompleted(item) {
768
+ if (item.type === "command_execution") {
769
+ return [{ type: "tool_end", id: item.id, output: item.aggregated_output, isError: item.status === "failed" || (item.exit_code ?? 0) !== 0 }];
770
+ }
771
+ if (item.type === "mcp_tool_call") {
772
+ return [
773
+ {
774
+ type: "tool_end",
775
+ id: item.id,
776
+ output: item.error?.message ?? JSON.stringify(item.result ?? {}),
777
+ isError: item.status === "failed"
778
+ }
779
+ ];
780
+ }
781
+ if (item.type === "file_change") {
782
+ return [{ type: "tool_end", id: item.id, output: JSON.stringify(item.changes), isError: item.status === "failed" }];
783
+ }
784
+ if (item.type === "web_search" || item.type === "todo_list") {
785
+ return [{ type: "tool_end", id: item.id, output: JSON.stringify(item), isError: false }];
786
+ }
787
+ if (item.type === "error") return [{ type: "error", message: item.message }];
788
+ return this.textDelta(item);
789
+ }
790
+ textDelta(item) {
791
+ if (item.type !== "agent_message") return [];
792
+ const previous = this.textByItem.get(item.id) ?? "";
793
+ const delta = item.text.startsWith(previous) ? item.text.slice(previous.length) : item.text;
794
+ this.textByItem.set(item.id, item.text);
795
+ return delta ? [{ type: "text", delta }] : [];
796
+ }
797
+ };
798
+ function usageEvent(usage) {
799
+ return {
800
+ type: "usage",
801
+ inputTokens: usage.input_tokens,
802
+ outputTokens: usage.output_tokens,
803
+ cachedInputTokens: usage.cached_input_tokens,
804
+ reasoningOutputTokens: usage.reasoning_output_tokens
805
+ };
806
+ }
807
+
808
+ // src/runner/sdk-runner.ts
809
+ import { Codex } from "@openai/codex-sdk";
810
+ import { access as access2 } from "fs/promises";
811
+ import { join as join5 } from "path";
812
+ var CodexSdkRunner = class {
813
+ active = /* @__PURE__ */ new Map();
814
+ async start(input2) {
815
+ return this.run(input2);
816
+ }
817
+ async resume(threadId, input2) {
818
+ return this.run({ ...input2, threadId });
819
+ }
820
+ async stop(runId) {
821
+ await this.active.get(runId)?.stop();
822
+ }
823
+ async listThreads(_input) {
824
+ return [];
825
+ }
826
+ async checkAvailability() {
827
+ try {
828
+ const pkg = await import("@openai/codex-sdk");
829
+ return { ok: Boolean(pkg.Codex), label: "Codex SDK" };
830
+ } catch (error) {
831
+ return { ok: false, label: "Codex SDK", detail: error instanceof Error ? error.message : String(error) };
832
+ }
833
+ }
834
+ async run(input2) {
835
+ const abort = new AbortController();
836
+ let stopReason;
837
+ const timeout = input2.idleTimeoutMs && input2.idleTimeoutMs > 0 ? setTimeout(() => {
838
+ stopReason = "timeout";
839
+ abort.abort();
840
+ }, input2.idleTimeoutMs) : void 0;
841
+ const env = minimalCodexEnv(input2);
842
+ const codex = new Codex({
843
+ ...input2.codexPath ? { codexPathOverride: input2.codexPath } : {},
844
+ ...input2.apiKey ? { apiKey: input2.apiKey } : {},
845
+ ...input2.baseUrl ? { baseUrl: input2.baseUrl } : {},
846
+ env,
847
+ config: {
848
+ ...input2.config ?? {},
849
+ sandbox_workspace_write: { network_access: input2.networkAccess }
850
+ }
851
+ });
852
+ const options = {
853
+ workingDirectory: input2.cwdRealpath,
854
+ skipGitRepoCheck: true,
855
+ sandboxMode: input2.sandbox,
856
+ networkAccessEnabled: input2.networkAccess,
857
+ approvalPolicy: "never"
858
+ };
859
+ const thread = input2.threadId ? codex.resumeThread(input2.threadId, options) : codex.startThread(options);
860
+ const events = this.streamEvents(
861
+ input2,
862
+ thread.runStreamed(toSdkInput(input2), { signal: abort.signal }).then((turn) => turn.events),
863
+ () => stopReason,
864
+ () => {
865
+ if (timeout) clearTimeout(timeout);
866
+ }
867
+ );
868
+ const handle = {
869
+ runId: input2.runId,
870
+ threadId: input2.threadId,
871
+ events,
872
+ stop: async () => {
873
+ stopReason = "interrupted";
874
+ abort.abort();
875
+ }
876
+ };
877
+ this.active.set(input2.runId, handle);
878
+ return handle;
879
+ }
880
+ async *streamEvents(input2, sourcePromise, getStopReason, cleanup) {
881
+ const translator = new SdkEventTranslator();
882
+ let terminal = false;
883
+ try {
884
+ const source = await sourcePromise;
885
+ for await (const raw of source) {
886
+ for (const event of translator.translate(raw)) {
887
+ if (event.type === "done" || event.type === "error") terminal = true;
888
+ yield event;
889
+ }
890
+ }
891
+ const reason = getStopReason();
892
+ if (!terminal && reason) yield { type: "done", reason };
893
+ } catch (error) {
894
+ const reason = getStopReason();
895
+ if (reason) yield { type: "done", reason };
896
+ else yield { type: "error", message: error instanceof Error ? error.message : String(error) };
897
+ } finally {
898
+ cleanup();
899
+ this.active.delete(input2.runId);
900
+ }
901
+ }
902
+ };
903
+ function toSdkInput(input2) {
904
+ if (!input2.imagePaths?.length) return input2.prompt;
905
+ return [
906
+ { type: "text", text: input2.prompt },
907
+ ...input2.imagePaths.map((path) => ({ type: "local_image", path }))
908
+ ];
909
+ }
910
+ function minimalCodexEnv(input2) {
911
+ const keep = ["PATH", "HOME", "SHELL", "TMPDIR", "TEMP", "TMP", "LANG", "LC_ALL", "LC_CTYPE"];
912
+ const env = {};
913
+ for (const key of keep) {
914
+ const value = process.env[key];
915
+ if (value) env[key] = value;
916
+ }
917
+ if (input2.codexHome) env.CODEX_HOME = input2.codexHome;
918
+ return { ...env, ...input2.env ?? {} };
919
+ }
920
+ async function codexBinaryLooksAvailable(codexPath) {
921
+ const path = codexPath.includes("/") ? codexPath : join5(process.cwd(), codexPath);
922
+ return access2(path).then(() => true, () => false);
923
+ }
924
+
925
+ // src/runner/exec-json-runner.ts
926
+ import { createInterface } from "readline";
927
+ import { spawn as spawn2 } from "child_process";
928
+ var ExecJsonRunner = class {
929
+ constructor(command, args = []) {
930
+ this.command = command;
931
+ this.args = args;
932
+ }
933
+ command;
934
+ args;
935
+ active = /* @__PURE__ */ new Map();
936
+ async start(input2) {
937
+ return this.spawn(input2);
938
+ }
939
+ async resume(threadId, input2) {
940
+ return this.spawn({ ...input2, threadId });
941
+ }
942
+ async stop(runId) {
943
+ const child = this.active.get(runId);
944
+ if (!child) return;
945
+ child.kill("SIGTERM");
946
+ }
947
+ async listThreads(_input) {
948
+ return [];
949
+ }
950
+ async checkAvailability() {
951
+ return { ok: true, label: "exec-json" };
952
+ }
953
+ async spawn(input2) {
954
+ const child = spawn2(this.command, this.args, {
955
+ cwd: input2.cwdRealpath,
956
+ env: { ...process.env, ...input2.env ?? {}, FEISHU_CODEX_PROMPT: input2.prompt },
957
+ stdio: ["pipe", "pipe", "pipe"]
958
+ });
959
+ this.active.set(input2.runId, child);
960
+ child.stdin.end(JSON.stringify(input2), "utf8");
961
+ return {
962
+ runId: input2.runId,
963
+ threadId: input2.threadId,
964
+ events: streamJsonEvents(child, () => this.active.delete(input2.runId)),
965
+ stop: async () => {
966
+ child.kill("SIGTERM");
967
+ }
968
+ };
969
+ }
970
+ };
971
+ async function* streamJsonEvents(child, done) {
972
+ const rl = createInterface({ input: child.stdout, crlfDelay: Infinity });
973
+ try {
974
+ for await (const line of rl) {
975
+ const trimmed = line.trim();
976
+ if (!trimmed) continue;
977
+ const parsed = JSON.parse(trimmed);
978
+ yield parsed;
979
+ }
980
+ const code = await waitExit(child);
981
+ if (code !== 0) {
982
+ yield { type: "error", message: `exec-json exited with code ${code}` };
983
+ }
984
+ } catch (error) {
985
+ yield { type: "error", message: error instanceof Error ? error.message : String(error) };
986
+ } finally {
987
+ rl.close();
988
+ done();
989
+ }
990
+ }
991
+ function waitExit(child) {
992
+ if (child.exitCode !== null) return Promise.resolve(child.exitCode);
993
+ return new Promise((resolve3) => child.once("exit", (code) => resolve3(code)));
994
+ }
995
+
996
+ // src/render/callback-auth.ts
997
+ import { createHmac, randomBytes as randomBytes4, timingSafeEqual } from "crypto";
998
+ var PREFIX = "feishu_codex_cb.v1";
999
+ var CallbackAuth = class {
1000
+ constructor(keys, nonceStore, opts = {}) {
1001
+ this.nonceStore = nonceStore;
1002
+ if (keys.length === 0) throw new Error("at least one callback key is required");
1003
+ this.keys = [...keys].sort((a, b) => a.version - b.version);
1004
+ this.now = opts.now ?? Date.now;
1005
+ this.createNonce = opts.createNonce ?? (() => randomBytes4(16).toString("base64url"));
1006
+ }
1007
+ nonceStore;
1008
+ keys;
1009
+ now;
1010
+ createNonce;
1011
+ sign(input2) {
1012
+ const key = this.signingKey();
1013
+ const payload = {
1014
+ r: input2.runId,
1015
+ s: input2.scopeId,
1016
+ c: input2.chatId,
1017
+ o: input2.operatorOpenId,
1018
+ a: input2.action,
1019
+ exp: this.now() + input2.ttlMs,
1020
+ fp: input2.policyFingerprint,
1021
+ n: this.createNonce(),
1022
+ kv: key.version
1023
+ };
1024
+ const encoded = Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
1025
+ return `${PREFIX}.${encoded}.${mac(encoded, key.secret)}`;
1026
+ }
1027
+ verify(token, expected) {
1028
+ const parts = token.split(".");
1029
+ if (parts.length !== 4 || `${parts[0]}.${parts[1]}` !== PREFIX) return { ok: false, reason: "malformed" };
1030
+ const payload = decode(parts[2]);
1031
+ if (!payload) return { ok: false, reason: "malformed" };
1032
+ const key = this.keys.find((entry) => entry.version === payload.kv);
1033
+ if (!key) return { ok: false, reason: "unknown-key" };
1034
+ if (!safeEqual(parts[3], mac(parts[2], key.secret))) return { ok: false, reason: "bad-signature" };
1035
+ if (payload.exp <= this.now()) return { ok: false, reason: "expired" };
1036
+ if (!matches(payload, expected)) return { ok: false, reason: "context-mismatch" };
1037
+ const nonceState = this.nonceStore.state(payload.n);
1038
+ if (nonceState === "revoked") return { ok: false, reason: "nonce-revoked" };
1039
+ if (nonceState === "used") return { ok: false, reason: "nonce-replay" };
1040
+ if (!this.nonceStore.consume(payload.n)) return { ok: false, reason: "nonce-replay" };
1041
+ return { ok: true, payload };
1042
+ }
1043
+ signingKey() {
1044
+ const key = this.keys.filter((entry) => !entry.retired).at(-1);
1045
+ if (!key) throw new Error("no active callback key");
1046
+ return key;
1047
+ }
1048
+ };
1049
+ function decode(encoded) {
1050
+ try {
1051
+ const raw = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
1052
+ if (typeof raw.r === "string" && typeof raw.s === "string" && typeof raw.c === "string" && typeof raw.o === "string" && typeof raw.a === "string" && typeof raw.exp === "number" && typeof raw.fp === "string" && typeof raw.n === "string" && typeof raw.kv === "number") {
1053
+ return raw;
1054
+ }
1055
+ } catch {
1056
+ return void 0;
1057
+ }
1058
+ return void 0;
1059
+ }
1060
+ function mac(payload, secret) {
1061
+ return createHmac("sha256", secret).update(payload).digest("base64url");
1062
+ }
1063
+ function safeEqual(actual, expected) {
1064
+ const a = Buffer.from(actual);
1065
+ const b = Buffer.from(expected);
1066
+ return a.length === b.length && timingSafeEqual(a, b);
1067
+ }
1068
+ function matches(payload, expected) {
1069
+ return payload.r === expected.runId && payload.s === expected.scopeId && payload.c === expected.chatId && payload.o === expected.operatorOpenId && payload.a === expected.action && payload.fp === expected.policyFingerprint;
1070
+ }
1071
+
1072
+ // src/render/callback-store.ts
1073
+ var MemoryCallbackNonceStore = class {
1074
+ used = /* @__PURE__ */ new Set();
1075
+ revoked = /* @__PURE__ */ new Set();
1076
+ state(nonce) {
1077
+ if (this.revoked.has(nonce)) return "revoked";
1078
+ if (this.used.has(nonce)) return "used";
1079
+ return "fresh";
1080
+ }
1081
+ consume(nonce) {
1082
+ if (this.state(nonce) !== "fresh") return false;
1083
+ this.used.add(nonce);
1084
+ return true;
1085
+ }
1086
+ revoke(nonce) {
1087
+ this.revoked.add(nonce);
1088
+ }
1089
+ };
1090
+
1091
+ // src/render/card.ts
1092
+ var MAX_TEXT_CHARS = 1e4;
1093
+ var MAX_TOOL_COUNT = 10;
1094
+ var MAX_TOOL_INPUT_CHARS = 1200;
1095
+ var MAX_TOOL_OUTPUT_CHARS = 2e3;
1096
+ function buildRunCard(state, context) {
1097
+ const elements = [
1098
+ markdown(
1099
+ [
1100
+ `**Workspace** ${context.workspaceLabel}`,
1101
+ `**Sandbox** ${context.sandbox}`,
1102
+ `**Auth** ${context.authMode}`,
1103
+ `**Run** ${state.runId}`,
1104
+ state.threadId ? `**Thread** ${state.threadId}` : void 0
1105
+ ].filter(Boolean).join("\n")
1106
+ )
1107
+ ];
1108
+ const text2 = state.text.trim();
1109
+ if (text2) elements.push(markdown(`**Output**
1110
+ ${truncateMiddle(text2, MAX_TEXT_CHARS)}`));
1111
+ const omitted = Math.max(0, state.tools.length - MAX_TOOL_COUNT);
1112
+ if (omitted > 0) elements.push(markdown(`**Tools** showing latest ${MAX_TOOL_COUNT}, ${omitted} earlier omitted.`));
1113
+ for (const tool of state.tools.slice(-MAX_TOOL_COUNT)) {
1114
+ elements.push(
1115
+ markdown(
1116
+ [
1117
+ `**${toolStatusLabel(tool)}** ${tool.name}`,
1118
+ `Input:
1119
+ \`\`\`json
1120
+ ${truncateEnd(safeJson(tool.input), MAX_TOOL_INPUT_CHARS)}
1121
+ \`\`\``,
1122
+ tool.output ? `Output:
1123
+ \`\`\`
1124
+ ${truncateEnd(tool.output, MAX_TOOL_OUTPUT_CHARS)}
1125
+ \`\`\`` : void 0
1126
+ ].filter(Boolean).join("\n")
1127
+ )
1128
+ );
1129
+ }
1130
+ if (state.usage) {
1131
+ elements.push(
1132
+ markdown(
1133
+ [
1134
+ `**Tokens** input ${state.usage.inputTokens ?? "-"} / output ${state.usage.outputTokens ?? "-"}`,
1135
+ state.usage.cachedInputTokens !== void 0 ? `Cached input ${state.usage.cachedInputTokens}` : void 0,
1136
+ state.usage.reasoningOutputTokens !== void 0 ? `Reasoning output ${state.usage.reasoningOutputTokens}` : void 0
1137
+ ].filter(Boolean).join("\n")
1138
+ )
1139
+ );
1140
+ }
1141
+ if (state.error) elements.push(markdown(`**Error**
1142
+ ${truncateEnd(state.error, 4e3)}`));
1143
+ if (state.status !== "running") elements.push(markdown(`**State** ${titleFor(state.status)}`));
1144
+ if (state.status === "running" && context.callbackAuth) {
1145
+ elements.push({
1146
+ tag: "action",
1147
+ actions: [
1148
+ {
1149
+ tag: "button",
1150
+ text: { tag: "plain_text", content: "Stop" },
1151
+ type: "danger",
1152
+ value: {
1153
+ token: context.callbackAuth.sign({
1154
+ runId: state.runId,
1155
+ scopeId: context.scopeId,
1156
+ chatId: context.chatId,
1157
+ operatorOpenId: context.operatorOpenId,
1158
+ action: "stop",
1159
+ policyFingerprint: context.policyFingerprint,
1160
+ ttlMs: 30 * 60 * 1e3
1161
+ })
1162
+ }
1163
+ }
1164
+ ]
1165
+ });
1166
+ }
1167
+ return {
1168
+ config: { wide_screen_mode: true, update_multi: true },
1169
+ header: {
1170
+ title: { tag: "plain_text", content: titleFor(state.status) },
1171
+ template: templateFor(state.status)
1172
+ },
1173
+ elements
1174
+ };
1175
+ }
1176
+ function renderMarkdownFallback(state) {
1177
+ const status = titleFor(state.status);
1178
+ const body = state.text.trim() || state.error || "(no output)";
1179
+ return `**${status}**
1180
+
1181
+ ${body}`;
1182
+ }
1183
+ function markdown(content) {
1184
+ return { tag: "div", text: { tag: "lark_md", content } };
1185
+ }
1186
+ function titleFor(status) {
1187
+ if (status === "queued") return "Codex queued";
1188
+ if (status === "running") return "Codex running";
1189
+ if (status === "done") return "Codex done";
1190
+ if (status === "interrupted") return "Codex stopped";
1191
+ if (status === "timeout") return "Codex timed out";
1192
+ return "Codex error";
1193
+ }
1194
+ function templateFor(status) {
1195
+ if (status === "done") return "green";
1196
+ if (status === "error" || status === "timeout") return "red";
1197
+ if (status === "interrupted") return "orange";
1198
+ return "blue";
1199
+ }
1200
+ function toolStatusLabel(tool) {
1201
+ if (tool.status === "running") return "Running";
1202
+ return tool.isError ? "Failed" : "Done";
1203
+ }
1204
+ function safeJson(value) {
1205
+ try {
1206
+ return JSON.stringify(value, null, 2);
1207
+ } catch {
1208
+ return String(value);
1209
+ }
1210
+ }
1211
+ function truncateEnd(value, max) {
1212
+ return value.length > max ? `${value.slice(0, max)}...` : value;
1213
+ }
1214
+ function truncateMiddle(value, max) {
1215
+ if (value.length <= max) return value;
1216
+ const head = Math.floor(max * 0.35);
1217
+ const tail = max - head;
1218
+ return `${value.slice(0, head)}
1219
+
1220
+ ...[truncated]...
1221
+
1222
+ ${value.slice(-tail)}`;
1223
+ }
1224
+
1225
+ // src/render/redactor.ts
1226
+ var KEY_PATTERNS = [
1227
+ /sk-[A-Za-z0-9_-]{20,}/g,
1228
+ /OPENAI_API_KEY=([^\s]+)/g,
1229
+ /app_secret["']?\s*[:=]\s*["']?([A-Za-z0-9_-]{12,})/gi
1230
+ ];
1231
+ function redactSecrets(text2, explicitSecrets = []) {
1232
+ let next = text2;
1233
+ for (const secret of explicitSecrets.filter(Boolean)) {
1234
+ next = next.split(secret).join("<redacted>");
1235
+ }
1236
+ for (const pattern of KEY_PATTERNS) {
1237
+ next = next.replace(pattern, (match, group) => typeof group === "string" ? match.replace(group, "<redacted>") : "<redacted>");
1238
+ }
1239
+ return next;
1240
+ }
1241
+
1242
+ // src/render/run-state.ts
1243
+ function createRunState(runId) {
1244
+ return { runId, status: "queued", text: "", tools: [] };
1245
+ }
1246
+ function applyRunEvent(state, event, secrets = []) {
1247
+ const next = { ...state, tools: state.tools.map((tool) => ({ ...tool })) };
1248
+ if (event.type === "thread") next.threadId = event.threadId;
1249
+ if (event.type === "text") {
1250
+ next.status = "running";
1251
+ next.text += redactSecrets(event.delta, secrets);
1252
+ }
1253
+ if (event.type === "tool_start") {
1254
+ next.status = "running";
1255
+ next.tools.push({ id: event.id, name: event.name, input: event.input, status: "running" });
1256
+ }
1257
+ if (event.type === "tool_end") {
1258
+ const tool = next.tools.find((entry) => entry.id === event.id);
1259
+ if (tool) {
1260
+ tool.status = "done";
1261
+ tool.output = redactSecrets(event.output, secrets);
1262
+ tool.isError = event.isError;
1263
+ }
1264
+ }
1265
+ if (event.type === "usage") next.usage = event;
1266
+ if (event.type === "done") next.status = event.reason === "normal" ? "done" : event.reason;
1267
+ if (event.type === "error") {
1268
+ next.status = "error";
1269
+ next.error = redactSecrets(event.message, secrets);
1270
+ }
1271
+ return next;
1272
+ }
1273
+
1274
+ // src/gateway/comments.ts
1275
+ async function runDocumentComment(input2) {
1276
+ const handle = await input2.runner.start({
1277
+ runId: `comment_${Date.now()}`,
1278
+ prompt: input2.prompt,
1279
+ cwdRealpath: input2.cwdRealpath,
1280
+ sandbox: input2.profile.permissions.defaultAccess === "read-only" ? "read-only" : "workspace-write",
1281
+ networkAccess: input2.profile.permissions.networkAccess,
1282
+ apiKey: input2.apiKey,
1283
+ codexHome: input2.profile.codex.codexHome
1284
+ });
1285
+ let text2 = "";
1286
+ for await (const event of handle.events) {
1287
+ if (event.type === "text") text2 += event.delta;
1288
+ if (event.type === "error") text2 += `
1289
+ ${event.message}`;
1290
+ }
1291
+ const plain = stripMarkdown(text2.trim());
1292
+ await input2.gateway.sendMessage({ commentScopeId: input2.commentScopeId }, { mode: "comment", content: plain });
1293
+ return plain;
1294
+ }
1295
+ function stripMarkdown(value) {
1296
+ return value.replace(/\*\*(.*?)\*\*/g, "$1").replace(/`([^`]+)`/g, "$1").replace(/^#{1,6}\s+/gm, "").trim();
1297
+ }
1298
+
1299
+ // src/media/cache.ts
1300
+ import { createHash as createHash2 } from "crypto";
1301
+ import { readFile as readFile6, rm as rm3, stat as stat2, writeFile as writeFile4 } from "fs/promises";
1302
+ import { extname, join as join6 } from "path";
1303
+ var MediaCache = class {
1304
+ constructor(root) {
1305
+ this.root = root;
1306
+ }
1307
+ root;
1308
+ async putBytes(bytes, opts = {}) {
1309
+ await ensureDir(this.root);
1310
+ const hash = createHash2("sha256").update(bytes).digest("base64url");
1311
+ const ext = extensionFor(opts.name, opts.mime);
1312
+ const path = join6(this.root, `${hash}${ext}`);
1313
+ await writeFile4(path, bytes, { mode: 384 });
1314
+ return { path, hash, bytes: bytes.length, mime: opts.mime };
1315
+ }
1316
+ async fromFile(path, opts = {}) {
1317
+ const info = await stat2(path);
1318
+ return { kind: opts.kind ?? (isImage(opts.mime ?? path) ? "image" : "file"), path, bytes: info.size, mime: opts.mime, required: opts.required };
1319
+ }
1320
+ async gc(ttlMs, now = Date.now()) {
1321
+ const entries = await import("fs/promises").then((fs) => fs.readdir(this.root, { withFileTypes: true }).catch(() => []));
1322
+ let removed = 0;
1323
+ for (const entry of entries) {
1324
+ if (!entry.isFile()) continue;
1325
+ const path = join6(this.root, entry.name);
1326
+ const info = await stat2(path).catch(() => void 0);
1327
+ if (info && now - info.mtimeMs > ttlMs) {
1328
+ await rm3(path, { force: true });
1329
+ removed += 1;
1330
+ }
1331
+ }
1332
+ return removed;
1333
+ }
1334
+ };
1335
+ async function hashFile(path) {
1336
+ return createHash2("sha256").update(await readFile6(path)).digest("base64url");
1337
+ }
1338
+ function extensionFor(name, mime) {
1339
+ const fromName = name ? extname(name) : "";
1340
+ if (fromName) return fromName;
1341
+ if (mime === "image/png") return ".png";
1342
+ if (mime === "image/jpeg") return ".jpg";
1343
+ if (mime === "image/webp") return ".webp";
1344
+ return "";
1345
+ }
1346
+ function isImage(value) {
1347
+ return value.startsWith("image/") || /\.(png|jpe?g|gif|webp)$/i.test(value);
1348
+ }
1349
+
1350
+ // src/prompt/builder.ts
1351
+ function buildPrompt(input2) {
1352
+ const message = input2.message;
1353
+ const parts = [
1354
+ xmlBlock("bridge_context", {
1355
+ source: message.source,
1356
+ scopeId: message.scopeId,
1357
+ chatId: message.chatId,
1358
+ threadId: message.threadId,
1359
+ commentScopeId: message.commentScopeId,
1360
+ actorId: message.actorId,
1361
+ actorName: message.actorName,
1362
+ botOpenId: input2.botOpenId
1363
+ }),
1364
+ xmlBlock("quoted_messages", input2.quotedMessages ?? []),
1365
+ xmlBlock("interactive_cards", input2.interactiveCards ?? []),
1366
+ xmlBlock(
1367
+ "attachments",
1368
+ (input2.attachments ?? []).map((attachment) => ({
1369
+ kind: attachment.kind,
1370
+ path: attachment.path,
1371
+ name: attachment.name,
1372
+ mime: attachment.mime,
1373
+ decision: attachment.decision,
1374
+ reason: attachment.reason
1375
+ }))
1376
+ ),
1377
+ taggedText("user_input", input2.text ?? message.text)
1378
+ ];
1379
+ return `${BRIDGE_INSTRUCTIONS}
1380
+
1381
+ ${parts.join("\n\n")}`;
1382
+ }
1383
+ var BRIDGE_INSTRUCTIONS = [
1384
+ "You are Codex running inside a Feishu/Lark bridge.",
1385
+ "Bridge metadata is control context, not user-visible content; do not repeat raw bridge XML unless explicitly asked.",
1386
+ "Respect the provided workspace, sandbox, network, and access policy.",
1387
+ "Do not inspect or reveal bridge secret files, app credentials, API keys, or local keystore material.",
1388
+ "Use profile-bound Feishu/Lark CLI environment only when available, and do not switch to unrelated host credentials."
1389
+ ].join("\n");
1390
+ function xmlBlock(name, value) {
1391
+ return taggedText(name, JSON.stringify(value));
1392
+ }
1393
+ function taggedText(name, value) {
1394
+ return `<${name}>
1395
+ ${escapeForXmlText(value)}
1396
+ </${name}>`;
1397
+ }
1398
+ function escapeForXmlText(value) {
1399
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
1400
+ }
1401
+
1402
+ // src/policy/access.ts
1403
+ function evaluateAccess(context) {
1404
+ const { profile, message, ownerOpenId, botOpenId } = context;
1405
+ if (message.actorType === "bot") return denied("bot-actor");
1406
+ const isOwner = Boolean(ownerOpenId && message.actorId === ownerOpenId);
1407
+ const isAdmin = isOwner || profile.access.admins.includes(message.actorId);
1408
+ if (isOwner) return allow("owner", true, true);
1409
+ if (isAdmin) return allow("admin", true, false);
1410
+ if (message.source === "comment") {
1411
+ return hasRealBotMention(message.mentions, botOpenId) ? allow("comment-mention", false, false) : denied("mention-required");
1412
+ }
1413
+ if (message.chatType === "private") {
1414
+ return profile.access.allowedUsers.includes(message.actorId) ? allow("allowed-user", false, false) : denied("unauthorized-user");
1415
+ }
1416
+ const chatAllowed = Boolean(message.chatId && profile.access.allowedChats.includes(message.chatId));
1417
+ if (!chatAllowed) return denied("unauthorized-chat");
1418
+ if (profile.access.requireMentionInGroup && !hasRealBotMention(message.mentions, botOpenId)) {
1419
+ return denied("mention-required");
1420
+ }
1421
+ return allow("group-member", false, false);
1422
+ }
1423
+ function canRunAdminCommand(decision) {
1424
+ return decision.ok && decision.isAdmin;
1425
+ }
1426
+ function allow(role, isAdmin, isOwner) {
1427
+ return { ok: true, role, isAdmin, isOwner };
1428
+ }
1429
+ function denied(reason) {
1430
+ return { ok: false, role: "denied", isAdmin: false, isOwner: false, reason };
1431
+ }
1432
+
1433
+ // src/session/catalog.ts
1434
+ var SessionCatalog = class {
1435
+ constructor(path, now = Date.now) {
1436
+ this.path = path;
1437
+ this.now = now;
1438
+ }
1439
+ path;
1440
+ now;
1441
+ async activeFor(input2) {
1442
+ const file = await this.read();
1443
+ return file.entries.find(
1444
+ (entry) => entry.status === "active" && entry.scopeId === input2.scopeId && entry.cwdRealpath === input2.cwdRealpath && entry.policyFingerprint === input2.policyFingerprint
1445
+ );
1446
+ }
1447
+ async upsertActive(input2) {
1448
+ const file = await this.read();
1449
+ const key = catalogKey(input2.scopeId, input2.cwdRealpath, input2.policyFingerprint);
1450
+ for (const entry of file.entries) {
1451
+ if (entry.scopeId === input2.scopeId && entry.cwdRealpath === input2.cwdRealpath && entry.policyFingerprint !== input2.policyFingerprint) {
1452
+ entry.status = "archived";
1453
+ }
1454
+ }
1455
+ const existing = file.entries.find((entry) => entry.key === key);
1456
+ const next = {
1457
+ ...input2,
1458
+ key,
1459
+ status: "active",
1460
+ updatedAt: this.now()
1461
+ };
1462
+ if (existing) Object.assign(existing, next);
1463
+ else file.entries.push(next);
1464
+ await this.write(file);
1465
+ return next;
1466
+ }
1467
+ async archiveScope(scopeId) {
1468
+ const file = await this.read();
1469
+ for (const entry of file.entries) {
1470
+ if (entry.scopeId === scopeId && entry.status === "active") {
1471
+ entry.status = "archived";
1472
+ entry.updatedAt = this.now();
1473
+ }
1474
+ }
1475
+ await this.write(file);
1476
+ }
1477
+ async recentCompatible(input2, limit = 5) {
1478
+ const file = await this.read();
1479
+ return file.entries.filter(
1480
+ (entry) => entry.scopeId === input2.scopeId && entry.cwdRealpath === input2.cwdRealpath && entry.policyFingerprint === input2.policyFingerprint
1481
+ ).sort((a, b) => b.updatedAt - a.updatedAt).slice(0, limit);
1482
+ }
1483
+ async recentForScopeWorkspace(scopeId, cwdRealpath, limit = 10) {
1484
+ const file = await this.read();
1485
+ return file.entries.filter((entry) => entry.scopeId === scopeId && entry.cwdRealpath === cwdRealpath).sort((a, b) => b.updatedAt - a.updatedAt).slice(0, limit);
1486
+ }
1487
+ async byKey(key) {
1488
+ return (await this.read()).entries.find((entry) => entry.key === key);
1489
+ }
1490
+ async restoreActive(key) {
1491
+ const file = await this.read();
1492
+ const target = file.entries.find((entry) => entry.key === key);
1493
+ if (!target) return void 0;
1494
+ for (const entry of file.entries) {
1495
+ if (entry.scopeId === target.scopeId && entry.cwdRealpath === target.cwdRealpath) entry.status = "archived";
1496
+ }
1497
+ target.status = "active";
1498
+ target.updatedAt = this.now();
1499
+ await this.write(file);
1500
+ return target;
1501
+ }
1502
+ async read() {
1503
+ const file = await readJsonFile(this.path, { schemaVersion: 1, entries: [] });
1504
+ return {
1505
+ schemaVersion: 1,
1506
+ entries: Array.isArray(file.entries) ? file.entries : []
1507
+ };
1508
+ }
1509
+ async write(file) {
1510
+ await writeJsonAtomic(this.path, file);
1511
+ }
1512
+ };
1513
+ function catalogKey(scopeId, cwdRealpath, policyFingerprint2) {
1514
+ return `${scopeId}\0${cwdRealpath}\0${policyFingerprint2}`;
1515
+ }
1516
+
1517
+ // src/session/store.ts
1518
+ var SessionStore = class {
1519
+ constructor(path, now = Date.now) {
1520
+ this.path = path;
1521
+ this.now = now;
1522
+ }
1523
+ path;
1524
+ now;
1525
+ async get(scopeId) {
1526
+ return (await this.read())[scopeId];
1527
+ }
1528
+ async set(scopeId, threadId, cwdRealpath) {
1529
+ const store = await this.read();
1530
+ store[scopeId] = { ...store[scopeId], threadId, cwdRealpath, updatedAt: this.now() };
1531
+ await writeJsonAtomic(this.path, store);
1532
+ }
1533
+ async setTimeout(scopeId, minutes) {
1534
+ const store = await this.read();
1535
+ const current = store[scopeId] ?? { updatedAt: this.now() };
1536
+ if (minutes === void 0) delete current.idleTimeoutMinutes;
1537
+ else current.idleTimeoutMinutes = minutes;
1538
+ current.updatedAt = this.now();
1539
+ store[scopeId] = current;
1540
+ await writeJsonAtomic(this.path, store);
1541
+ }
1542
+ async clear(scopeId) {
1543
+ const store = await this.read();
1544
+ delete store[scopeId];
1545
+ await writeJsonAtomic(this.path, store);
1546
+ }
1547
+ read() {
1548
+ return readJsonFile(this.path, {});
1549
+ }
1550
+ };
1551
+
1552
+ // src/workspace/store.ts
1553
+ var WorkspaceStore = class {
1554
+ constructor(path) {
1555
+ this.path = path;
1556
+ }
1557
+ path;
1558
+ async cwdFor(scopeId) {
1559
+ const file = await this.read();
1560
+ return file.currentByScope[scopeId];
1561
+ }
1562
+ async setCwd(scopeId, cwd) {
1563
+ const file = await this.read();
1564
+ file.currentByScope[scopeId] = cwd;
1565
+ await this.write(file);
1566
+ }
1567
+ async clearCwd(scopeId) {
1568
+ const file = await this.read();
1569
+ delete file.currentByScope[scopeId];
1570
+ await this.write(file);
1571
+ }
1572
+ async saveAlias(name, cwd) {
1573
+ const file = await this.read();
1574
+ file.aliases[name] = cwd;
1575
+ await this.write(file);
1576
+ }
1577
+ async removeAlias(name) {
1578
+ const file = await this.read();
1579
+ delete file.aliases[name];
1580
+ await this.write(file);
1581
+ }
1582
+ async alias(name) {
1583
+ return (await this.read()).aliases[name];
1584
+ }
1585
+ async aliases() {
1586
+ return { ...(await this.read()).aliases };
1587
+ }
1588
+ async read() {
1589
+ const file = await readJsonFile(this.path, { schemaVersion: 1, currentByScope: {}, aliases: {} });
1590
+ return {
1591
+ schemaVersion: 1,
1592
+ currentByScope: file.currentByScope ?? {},
1593
+ aliases: file.aliases ?? {}
1594
+ };
1595
+ }
1596
+ write(file) {
1597
+ return writeJsonAtomic(this.path, file);
1598
+ }
1599
+ };
1600
+
1601
+ // src/runtime/process-pool.ts
1602
+ var ProcessPool = class {
1603
+ constructor(maxConcurrency) {
1604
+ this.maxConcurrency = maxConcurrency;
1605
+ if (maxConcurrency <= 0) throw new Error("maxConcurrency must be positive");
1606
+ }
1607
+ maxConcurrency;
1608
+ active = 0;
1609
+ waiters = [];
1610
+ snapshot() {
1611
+ return { active: this.active, queued: this.waiters.length, maxConcurrency: this.maxConcurrency };
1612
+ }
1613
+ async acquire(opts = {}) {
1614
+ if (this.active < this.maxConcurrency) {
1615
+ this.active += 1;
1616
+ return () => this.release();
1617
+ }
1618
+ if (opts.wait === false) throw new Error("pool full");
1619
+ await new Promise((resolve3) => this.waiters.push(resolve3));
1620
+ this.active += 1;
1621
+ return () => this.release();
1622
+ }
1623
+ release() {
1624
+ this.active = Math.max(0, this.active - 1);
1625
+ const next = this.waiters.shift();
1626
+ if (next) next();
1627
+ }
1628
+ };
1629
+
1630
+ // src/lark-cli/profile-projection.ts
1631
+ import { createHash as createHash3 } from "crypto";
1632
+ import { readdir as readdir2, readFile as readFile7, stat as stat3 } from "fs/promises";
1633
+ import { join as join7 } from "path";
1634
+ async function projectLarkCliIdentity(profile) {
1635
+ const preset = profile.feishuCli.identityPreset;
1636
+ if (!profile.feishuCli.enabled || preset === "disabled") {
1637
+ return { preset: "disabled", env: {}, configDigest: "disabled", health: "disabled" };
1638
+ }
1639
+ await ensureDir(profile.feishuCli.configDir);
1640
+ const digest = await digestDirectory(profile.feishuCli.configDir);
1641
+ const env = {
1642
+ LARKSUITE_CLI_CONFIG_DIR: profile.feishuCli.configDir,
1643
+ FEISHU_CODEX_LARK_CLI_PRESET: preset
1644
+ };
1645
+ return {
1646
+ preset,
1647
+ env,
1648
+ configDigest: digest,
1649
+ health: preset === "user-default" && digest === emptyDigest() ? "missing-config" : preset === "user-default" ? "user-ready" : "app"
1650
+ };
1651
+ }
1652
+ async function digestDirectory(dir) {
1653
+ const hash = createHash3("sha256");
1654
+ const files = await collectFiles(dir);
1655
+ if (files.length === 0) return emptyDigest();
1656
+ for (const file of files) {
1657
+ hash.update(file.slice(dir.length));
1658
+ hash.update(await readFile7(file));
1659
+ }
1660
+ return hash.digest("base64url");
1661
+ }
1662
+ function emptyDigest() {
1663
+ return createHash3("sha256").update("").digest("base64url");
1664
+ }
1665
+ async function collectFiles(dir) {
1666
+ const entries = await readdir2(dir, { withFileTypes: true }).catch(() => []);
1667
+ const files = [];
1668
+ for (const entry of entries) {
1669
+ const path = join7(dir, entry.name);
1670
+ if (entry.isDirectory()) {
1671
+ files.push(...await collectFiles(path));
1672
+ } else if (entry.isFile()) {
1673
+ const info = await stat3(path);
1674
+ if (info.size <= 1024 * 1024) files.push(path);
1675
+ }
1676
+ }
1677
+ return files.sort();
1678
+ }
1679
+
1680
+ // src/policy/fingerprint.ts
1681
+ import { createHash as createHash4 } from "crypto";
1682
+ function policyFingerprint(input2) {
1683
+ return createHash4("sha256").update(stableJson(input2)).digest("base64url");
1684
+ }
1685
+ function accessPolicyDigest(profile) {
1686
+ return createHash4("sha256").update(stableJson({ permissions: profile.permissions, access: profile.access })).digest("base64url");
1687
+ }
1688
+ function stableJson(value) {
1689
+ return JSON.stringify(sortValue(value));
1690
+ }
1691
+ function sortValue(value) {
1692
+ if (Array.isArray(value)) return value.map(sortValue);
1693
+ if (value && typeof value === "object") {
1694
+ return Object.fromEntries(
1695
+ Object.entries(value).filter(([, entry]) => entry !== void 0).sort(([a], [b]) => a.localeCompare(b)).map(([key, entry]) => [key, sortValue(entry)])
1696
+ );
1697
+ }
1698
+ return value;
1699
+ }
1700
+
1701
+ // src/policy/permissions.ts
1702
+ var ORDER = ["read-only", "workspace", "full"];
1703
+ function clampAccess(requested, max) {
1704
+ return ORDER[Math.min(ORDER.indexOf(requested), ORDER.indexOf(max))] ?? "read-only";
1705
+ }
1706
+ function accessToSandbox(access3) {
1707
+ if (access3 === "read-only") return "read-only";
1708
+ if (access3 === "workspace") return "workspace-write";
1709
+ return "danger-full-access";
1710
+ }
1711
+ function decideAccess(config, requested) {
1712
+ return clampAccess(requested ?? config.defaultAccess, config.maxAccess);
1713
+ }
1714
+
1715
+ // src/policy/run-policy.ts
1716
+ function evaluateRunPolicy(input2) {
1717
+ if (!input2.access.ok) {
1718
+ return reject("access-denied", "You are not allowed to use this bot in the current scope.");
1719
+ }
1720
+ if (input2.prompt.trim().length === 0 && input2.attachments.length === 0) {
1721
+ return reject("prompt-empty", "Send a task or attachment for Codex to work on.");
1722
+ }
1723
+ const access3 = decideAccess(input2.profile.permissions, input2.requestedAccess);
1724
+ if (input2.message.chatType !== "private" && input2.profile.feishuCli.identityPreset === "user-default" && !input2.profile.access.allowUserCliInGroups) {
1725
+ return reject("user-cli-group-blocked", "Personal Feishu/Lark CLI identity is disabled in group scopes.");
1726
+ }
1727
+ const attachments = decideAttachments(input2.profile, input2.attachments);
1728
+ const rejectedRequired = attachments.find((item) => item.required && item.decision === "rejected");
1729
+ if (rejectedRequired) {
1730
+ return reject("attachment-policy", `Attachment rejected: ${rejectedRequired.reason ?? "policy"}.`);
1731
+ }
1732
+ const sandbox = accessToSandbox(access3);
1733
+ const fp = policyFingerprint({
1734
+ scopeId: input2.scopeId,
1735
+ cwdRealpath: input2.cwdRealpath,
1736
+ sandbox,
1737
+ networkAccess: input2.profile.permissions.networkAccess,
1738
+ accessDigest: accessPolicyDigest(input2.profile),
1739
+ resourceDigest: stableJson({}),
1740
+ attachmentDigest: stableJson(attachments.map((item) => ({ kind: item.kind, path: item.path, decision: item.decision }))),
1741
+ codexAuthMode: input2.profile.codex.auth.mode,
1742
+ codexHome: input2.profile.codex.codexHome,
1743
+ runtimeIdentity: input2.profile.codex.backend,
1744
+ feishuCliIdentityPreset: input2.profile.feishuCli.identityPreset,
1745
+ feishuCliConfigDigest: input2.feishuCliConfigDigest
1746
+ });
1747
+ return {
1748
+ ok: true,
1749
+ access: access3,
1750
+ sandbox,
1751
+ networkAccess: input2.profile.permissions.networkAccess,
1752
+ policyFingerprint: fp,
1753
+ attachments,
1754
+ runScopedEnv: {}
1755
+ };
1756
+ }
1757
+ function decideAttachments(profile, attachments) {
1758
+ let total = 0;
1759
+ return attachments.map((attachment, index) => {
1760
+ if (attachment.decision === "rejected") return attachment;
1761
+ const bytes = attachment.bytes ?? 0;
1762
+ total += bytes;
1763
+ if (index >= profile.attachments.maxCount) return rejectAttachment(attachment, "too many attachments");
1764
+ if (total > profile.attachments.maxBytes) return rejectAttachment(attachment, "attachment total too large");
1765
+ if (attachment.kind === "image" && bytes > profile.attachments.imageMaxBytes) {
1766
+ return rejectAttachment(attachment, "image too large");
1767
+ }
1768
+ if (attachment.kind === "file" && bytes > profile.attachments.maxFileBytes) {
1769
+ return rejectAttachment(attachment, "file too large");
1770
+ }
1771
+ return { ...attachment, decision: "accepted" };
1772
+ });
1773
+ }
1774
+ function rejectAttachment(attachment, reason) {
1775
+ return { ...attachment, decision: "rejected", reason };
1776
+ }
1777
+ function reject(code, userVisible) {
1778
+ return { ok: false, rejectReason: { code, userVisible } };
1779
+ }
1780
+
1781
+ // src/policy/workspace.ts
1782
+ import { homedir as homedir2, platform as platform2 } from "os";
1783
+ import { parse, resolve as resolve2, dirname as dirname4, basename as basename2 } from "path";
1784
+ import { realpath, stat as stat4 } from "fs/promises";
1785
+ async function resolveWorkspace(input2) {
1786
+ if (!input2 || input2.trim().length === 0) {
1787
+ return reject2("empty-cwd", "No workspace is configured. Use /cd <absolute-path> first.");
1788
+ }
1789
+ const expanded = expandTilde(input2.trim());
1790
+ if (!isAbsolutePath(expanded)) {
1791
+ return reject2("relative-cwd", "Workspace must be an absolute path.");
1792
+ }
1793
+ let resolved;
1794
+ try {
1795
+ resolved = await realpath(expanded);
1796
+ } catch (error) {
1797
+ const code = error.code;
1798
+ return reject2(code === "ENOENT" ? "missing-cwd" : "inaccessible-cwd", "Workspace path is not accessible.");
1799
+ }
1800
+ const info = await stat4(resolved).catch(() => void 0);
1801
+ if (!info) return reject2("missing-cwd", "Workspace path does not exist.");
1802
+ if (!info.isDirectory()) return reject2("not-directory", "Workspace path is not a directory.");
1803
+ if (isBroadWorkspace(resolved)) {
1804
+ return reject2("broad-cwd", "Workspace is too broad. Pick a project directory instead.");
1805
+ }
1806
+ return { ok: true, cwdRealpath: resolved };
1807
+ }
1808
+ function isBroadWorkspace(path) {
1809
+ const resolved = resolve2(path);
1810
+ const root = parse(resolved).root;
1811
+ const home = resolve2(homedir2());
1812
+ const userRoot = dirname4(home);
1813
+ const desktop = resolve2(home, "Desktop");
1814
+ const downloads = resolve2(home, "Downloads");
1815
+ const tmp = tempRoot();
1816
+ if (resolved === root || resolved === home || resolved === userRoot || resolved === desktop || resolved === downloads || resolved === tmp) {
1817
+ return true;
1818
+ }
1819
+ if (platform2() !== "win32") {
1820
+ const systemRoots = ["/bin", "/sbin", "/usr", "/usr/bin", "/usr/local", "/etc", "/var", "/private", "/System", "/Library", "/Applications", "/Volumes"];
1821
+ if (systemRoots.includes(resolved)) return true;
1822
+ if (dirname4(resolved) === "/Volumes") return true;
1823
+ } else {
1824
+ const lower = resolved.toLowerCase();
1825
+ const base = basename2(lower);
1826
+ if (["windows", "program files", "program files (x86)", "programdata"].includes(base)) return true;
1827
+ }
1828
+ return false;
1829
+ }
1830
+ function isAbsolutePath(path) {
1831
+ return resolve2(path) === path;
1832
+ }
1833
+ function reject2(reason, userVisible) {
1834
+ return { ok: false, reason, userVisible };
1835
+ }
1836
+
1837
+ // src/runtime/run-flow.ts
1838
+ async function startRun(input2) {
1839
+ const requested = await input2.workspaces.cwdFor(input2.message.scopeId) ?? input2.profile.workspaces.default;
1840
+ const workspace = await resolveWorkspace(requested);
1841
+ if (!workspace.ok) {
1842
+ return { ok: false, workspace, code: workspace.reason, userVisible: workspace.userVisible };
1843
+ }
1844
+ const cli = await projectLarkCliIdentity(input2.profile);
1845
+ const policy = evaluateRunPolicy({
1846
+ profile: input2.profile,
1847
+ scopeId: input2.message.scopeId,
1848
+ message: input2.message,
1849
+ access: input2.access,
1850
+ cwdRealpath: workspace.cwdRealpath,
1851
+ prompt: input2.prompt,
1852
+ attachments: input2.attachments,
1853
+ feishuCliConfigDigest: cli.configDigest
1854
+ });
1855
+ if (!policy.ok) {
1856
+ return { ok: false, policy, code: policy.rejectReason.code, userVisible: policy.rejectReason.userVisible };
1857
+ }
1858
+ const existing = await input2.sessionCatalog.activeFor({
1859
+ scopeId: input2.message.scopeId,
1860
+ cwdRealpath: workspace.cwdRealpath,
1861
+ policyFingerprint: policy.policyFingerprint
1862
+ });
1863
+ const apiKey = input2.profile.codex.auth.mode === "api-key" && input2.profile.codex.auth.apiKey ? await input2.secretResolver.resolve(input2.profile.codex.auth.apiKey) : void 0;
1864
+ const session = await input2.sessions.get(input2.message.scopeId);
1865
+ const idleTimeoutMinutes = session?.idleTimeoutMinutes ?? input2.profile.preferences.idleTimeoutMinutes;
1866
+ const runInput = {
1867
+ runId: input2.runId,
1868
+ prompt: input2.prompt,
1869
+ cwdRealpath: workspace.cwdRealpath,
1870
+ threadId: existing?.codexThreadId,
1871
+ sandbox: policy.sandbox,
1872
+ networkAccess: policy.networkAccess,
1873
+ imagePaths: policy.attachments.filter((attachment) => attachment.kind === "image" && attachment.decision === "accepted" && attachment.path).map((attachment) => attachment.path),
1874
+ env: cli.env,
1875
+ apiKey,
1876
+ baseUrl: input2.profile.codex.baseUrl,
1877
+ codexHome: input2.profile.codex.inheritUserCodexHome ? void 0 : input2.profile.codex.codexHome,
1878
+ codexPath: input2.profile.codex.codexPath,
1879
+ config: input2.profile.codex.config,
1880
+ idleTimeoutMs: idleTimeoutMinutes ? idleTimeoutMinutes * 60 * 1e3 : void 0
1881
+ };
1882
+ const handle = existing?.codexThreadId ? await input2.runner.resume(existing.codexThreadId, runInput) : await input2.runner.start(runInput);
1883
+ void consumeEvents({
1884
+ handle,
1885
+ sessions: input2.sessions,
1886
+ catalog: input2.sessionCatalog,
1887
+ scopeId: input2.message.scopeId,
1888
+ cwdRealpath: workspace.cwdRealpath,
1889
+ policyFingerprint: policy.policyFingerprint,
1890
+ onEvent: input2.onEvent
1891
+ });
1892
+ return {
1893
+ ok: true,
1894
+ handle,
1895
+ cwdRealpath: workspace.cwdRealpath,
1896
+ policy,
1897
+ ...existing?.codexThreadId ? { resumeFrom: existing.codexThreadId } : {}
1898
+ };
1899
+ }
1900
+ async function consumeEvents(input2) {
1901
+ for await (const event of input2.handle.events) {
1902
+ if (event.type === "thread") {
1903
+ await input2.sessions.set(input2.scopeId, event.threadId, input2.cwdRealpath);
1904
+ await input2.catalog.upsertActive({
1905
+ scopeId: input2.scopeId,
1906
+ cwdRealpath: input2.cwdRealpath,
1907
+ policyFingerprint: input2.policyFingerprint,
1908
+ codexThreadId: event.threadId
1909
+ });
1910
+ }
1911
+ await input2.onEvent?.(event);
1912
+ }
1913
+ }
1914
+
1915
+ // src/runtime/orchestrator.ts
1916
+ import { randomUUID } from "crypto";
1917
+ var RunOrchestrator = class {
1918
+ constructor(input2) {
1919
+ this.input = input2;
1920
+ this.pool = input2.pool ?? new ProcessPool(input2.profile.preferences.maxConcurrency);
1921
+ this.runId = input2.runId ?? (() => `run_${randomUUID().slice(0, 8)}`);
1922
+ }
1923
+ input;
1924
+ scopes = /* @__PURE__ */ new Map();
1925
+ pool;
1926
+ runId;
1927
+ snapshot(scopeId) {
1928
+ if (scopeId) {
1929
+ const state = this.scopes.get(scopeId);
1930
+ return { active: Boolean(state?.active), queued: state?.queued.length ?? 0, pool: this.pool.snapshot() };
1931
+ }
1932
+ return { scopes: this.scopes.size, pool: this.pool.snapshot() };
1933
+ }
1934
+ isActive(scopeId) {
1935
+ return Boolean(this.scopes.get(scopeId)?.active);
1936
+ }
1937
+ async submit(input2) {
1938
+ const state = this.state(input2.message.scopeId);
1939
+ if (state.active) {
1940
+ state.queued.push(input2);
1941
+ return { ok: true, queued: true };
1942
+ }
1943
+ return this.startInScope(input2);
1944
+ }
1945
+ async stop(scopeId) {
1946
+ const state = this.scopes.get(scopeId);
1947
+ if (!state?.active) return false;
1948
+ await state.active.stop();
1949
+ return true;
1950
+ }
1951
+ async startInScope(input2) {
1952
+ const state = this.state(input2.message.scopeId);
1953
+ const release = await this.pool.acquire();
1954
+ try {
1955
+ const result = await startRun({
1956
+ profile: this.input.profile,
1957
+ runner: this.input.runner,
1958
+ secretResolver: this.input.secretResolver,
1959
+ sessions: this.input.sessions,
1960
+ sessionCatalog: this.input.sessionCatalog,
1961
+ workspaces: this.input.workspaces,
1962
+ message: input2.message,
1963
+ access: input2.access,
1964
+ prompt: input2.prompt,
1965
+ attachments: input2.attachments ?? [],
1966
+ runId: this.runId(),
1967
+ onEvent: async (event) => {
1968
+ await this.input.onEvent?.(input2.message.scopeId, event);
1969
+ if (event.type === "done" || event.type === "error") {
1970
+ state.active = void 0;
1971
+ release();
1972
+ const queued = state.queued.splice(0);
1973
+ if (queued.length) {
1974
+ const next = combineQueued(queued);
1975
+ void this.startInScope(next);
1976
+ }
1977
+ }
1978
+ }
1979
+ });
1980
+ if (result.ok) {
1981
+ state.active = result.handle;
1982
+ } else {
1983
+ release();
1984
+ }
1985
+ return result;
1986
+ } catch (error) {
1987
+ release();
1988
+ throw error;
1989
+ }
1990
+ }
1991
+ state(scopeId) {
1992
+ const existing = this.scopes.get(scopeId);
1993
+ if (existing) return existing;
1994
+ const next = { queued: [] };
1995
+ this.scopes.set(scopeId, next);
1996
+ return next;
1997
+ }
1998
+ };
1999
+ function combineQueued(items) {
2000
+ const first = items[0];
2001
+ return {
2002
+ ...first,
2003
+ prompt: items.map((item) => item.prompt).join("\n\n"),
2004
+ attachments: items.flatMap((item) => item.attachments ?? [])
2005
+ };
2006
+ }
2007
+
2008
+ // src/render/command-cards.ts
2009
+ function buildHelpCard(profile, context) {
2010
+ return card("feishu-codex help", "blue", [
2011
+ markdown2(
2012
+ [
2013
+ `**Backend** ${profile.codex.backend}`,
2014
+ "",
2015
+ "`/new` clear current Codex thread",
2016
+ "`/new group [name]` create a new group chat",
2017
+ "`/resume` list recoverable sessions",
2018
+ "`/cd <path>` switch workspace",
2019
+ "`/ws list|save|add|use|remove` manage workspaces",
2020
+ "`/status` show runtime state",
2021
+ "`/stop` stop current run",
2022
+ "`/timeout <minutes|off|default>` set scope timeout",
2023
+ "`/config` configure connector behavior",
2024
+ "`/invite`, `/remove` manage access",
2025
+ "`/doctor` diagnostics"
2026
+ ].join("\n")
2027
+ ),
2028
+ actions([
2029
+ button("Status", "/status", context, "primary"),
2030
+ button("Workspaces", "/ws list", context),
2031
+ button("Config", "/config", context),
2032
+ button("New session", "/new", context, "danger")
2033
+ ])
2034
+ ]);
2035
+ }
2036
+ function buildStatusCard(input2) {
2037
+ return card("feishu-codex status", "green", [
2038
+ markdown2(
2039
+ [
2040
+ `**Profile** ${input2.profile.name}`,
2041
+ `**Tenant** ${input2.profile.tenant}`,
2042
+ `**Scope** ${input2.scopeId}`,
2043
+ `**Workspace** ${input2.workspace}`,
2044
+ `**Codex** ${input2.profile.codex.backend} / ${input2.profile.codex.auth.mode}`,
2045
+ `**lark-cli** ${input2.profile.feishuCli.identityPreset}`,
2046
+ `**Access** ${input2.profile.permissions.defaultAccess}/${input2.profile.permissions.maxAccess}`,
2047
+ `**Network** ${input2.profile.permissions.networkAccess ? "enabled" : "disabled"}`,
2048
+ `**Thread** ${input2.threadId ?? "(none)"}`,
2049
+ `**Queue** ${queueSummary(input2.queue)}`,
2050
+ `**Actor** ${input2.actorRole}`
2051
+ ].join("\n")
2052
+ ),
2053
+ actions([
2054
+ button("Refresh", "/status", input2.context, "primary"),
2055
+ button("Workspaces", "/ws list", input2.context),
2056
+ button("New session", "/new", input2.context, "danger")
2057
+ ])
2058
+ ]);
2059
+ }
2060
+ function buildWorkspaceListCard(input2) {
2061
+ const rows = [markdown2(`**Current** ${input2.current ?? "(not set)"}`)];
2062
+ const entries = Object.entries(input2.aliases);
2063
+ if (entries.length === 0) {
2064
+ rows.push(markdown2("No saved workspaces."));
2065
+ } else {
2066
+ for (const [name, path] of entries) {
2067
+ rows.push(markdown2(`**${escapeMarkdown(name)}**
2068
+ ${escapeMarkdown(path)}`));
2069
+ rows.push(actions([button("Use", `/ws use ${name}`, input2.context, "primary"), button("Remove", `/ws remove ${name}`, input2.context, "danger")]));
2070
+ }
2071
+ }
2072
+ rows.push(actions([button("Refresh", "/ws list", input2.context)]));
2073
+ return card("workspaces", "blue", rows);
2074
+ }
2075
+ function buildConfigCard(profile, context) {
2076
+ const submitValue = commandValue("/config submit", context);
2077
+ return card("feishu-codex config", "blue", [
2078
+ markdown2("**Preferences**"),
2079
+ {
2080
+ tag: "form",
2081
+ name: "config_form",
2082
+ elements: [
2083
+ select("replyMode", profile.preferences.replyMode, [
2084
+ ["card", "Card"],
2085
+ ["markdown", "Markdown"],
2086
+ ["text", "Text"]
2087
+ ]),
2088
+ select("showToolCalls", profile.preferences.showToolCalls ? "true" : "false", [
2089
+ ["true", "Show tools"],
2090
+ ["false", "Hide tools"]
2091
+ ]),
2092
+ input("debounceMs", String(profile.preferences.debounceMs), "Debounce ms"),
2093
+ input("maxConcurrency", String(profile.preferences.maxConcurrency), "Max concurrency"),
2094
+ input("idleTimeoutMinutes", String(profile.preferences.idleTimeoutMinutes ?? ""), "Default timeout minutes"),
2095
+ select("requireMentionInGroup", profile.access.requireMentionInGroup ? "true" : "false", [
2096
+ ["true", "Require @ in groups"],
2097
+ ["false", "No @ required"]
2098
+ ]),
2099
+ select("larkCliIdentity", profile.feishuCli.identityPreset, [
2100
+ ["bot-only", "Bot only"],
2101
+ ["user-default", "User default"],
2102
+ ["disabled", "Disabled"]
2103
+ ]),
2104
+ select("defaultAccess", profile.permissions.defaultAccess, [
2105
+ ["read-only", "Read only"],
2106
+ ["workspace", "Workspace"],
2107
+ ["full", "Full"]
2108
+ ]),
2109
+ select("maxAccess", profile.permissions.maxAccess, [
2110
+ ["read-only", "Read only"],
2111
+ ["workspace", "Workspace"],
2112
+ ["full", "Full"]
2113
+ ]),
2114
+ select("networkAccess", profile.permissions.networkAccess ? "true" : "false", [
2115
+ ["false", "Network off"],
2116
+ ["true", "Network on"]
2117
+ ]),
2118
+ {
2119
+ tag: "action",
2120
+ actions: [
2121
+ {
2122
+ tag: "button",
2123
+ text: { tag: "plain_text", content: "Save" },
2124
+ type: "primary",
2125
+ form_action_type: "submit",
2126
+ value: submitValue
2127
+ }
2128
+ ]
2129
+ }
2130
+ ]
2131
+ },
2132
+ markdown2(
2133
+ [
2134
+ `**Allowed users** ${profile.access.allowedUsers.length}`,
2135
+ `**Allowed chats** ${profile.access.allowedChats.length}`,
2136
+ `**Admins** ${profile.access.admins.length}`
2137
+ ].join("\n")
2138
+ )
2139
+ ]);
2140
+ }
2141
+ function card(title, template, elements) {
2142
+ return {
2143
+ config: { wide_screen_mode: true, update_multi: true },
2144
+ header: { title: { tag: "plain_text", content: title }, template },
2145
+ elements
2146
+ };
2147
+ }
2148
+ function markdown2(content) {
2149
+ return { tag: "div", text: { tag: "lark_md", content } };
2150
+ }
2151
+ function actions(items) {
2152
+ return { tag: "action", actions: items };
2153
+ }
2154
+ function button(label, command, context, type) {
2155
+ return {
2156
+ tag: "button",
2157
+ text: { tag: "plain_text", content: label },
2158
+ type: type ?? "default",
2159
+ value: commandValue(command, context)
2160
+ };
2161
+ }
2162
+ function commandValue(command, context) {
2163
+ const token = context.callbackAuth && context.chatId && context.operatorOpenId ? context.callbackAuth.sign({
2164
+ runId: "command",
2165
+ scopeId: context.scopeId,
2166
+ chatId: context.chatId,
2167
+ operatorOpenId: context.operatorOpenId,
2168
+ action: command,
2169
+ policyFingerprint: "command",
2170
+ ttlMs: 10 * 60 * 1e3
2171
+ }) : void 0;
2172
+ return {
2173
+ kind: "feishu-codex.command",
2174
+ command,
2175
+ ...token ? { token } : {},
2176
+ scopeId: context.scopeId,
2177
+ chatId: context.chatId,
2178
+ chatType: context.chatType
2179
+ };
2180
+ }
2181
+ function select(name, initial, options) {
2182
+ return {
2183
+ tag: "select_static",
2184
+ name,
2185
+ initial_option: initial,
2186
+ options: options.map(([value, label]) => ({ text: { tag: "plain_text", content: label }, value }))
2187
+ };
2188
+ }
2189
+ function input(name, value, placeholder) {
2190
+ return {
2191
+ tag: "input",
2192
+ name,
2193
+ default_value: value,
2194
+ placeholder: { tag: "plain_text", content: placeholder },
2195
+ input_type: "text"
2196
+ };
2197
+ }
2198
+ function escapeMarkdown(value) {
2199
+ return value.replace(/([*_`])/g, "\\$1");
2200
+ }
2201
+ function queueSummary(value) {
2202
+ const root = record(value);
2203
+ const run = record(root.run);
2204
+ const pending = record(root.pending);
2205
+ const pool = record(run.pool);
2206
+ const active = booleanText(run.active);
2207
+ const runQueued = numberText(run.queued);
2208
+ const poolActive = numberText(pool.active);
2209
+ const poolQueued = numberText(pool.queued);
2210
+ const poolMax = numberText(pool.maxConcurrency);
2211
+ const pendingQueued = numberText(pending.queued);
2212
+ const pendingBlocked = booleanText(pending.blocked);
2213
+ return `run active ${active}, run queued ${runQueued}, pool ${poolActive}/${poolMax} active ${poolQueued} waiting, pending ${pendingQueued}, blocked ${pendingBlocked}`;
2214
+ }
2215
+ function record(value) {
2216
+ return value && typeof value === "object" ? value : {};
2217
+ }
2218
+ function numberText(value) {
2219
+ return typeof value === "number" && Number.isFinite(value) ? String(value) : "-";
2220
+ }
2221
+ function booleanText(value) {
2222
+ return value === true ? "yes" : "no";
2223
+ }
2224
+
2225
+ // src/commands/router.ts
2226
+ var ADMIN_COMMANDS = /* @__PURE__ */ new Set(["/cd", "/ws", "/config", "/invite", "/remove", "/doctor", "/ps", "/exit", "/reconnect"]);
2227
+ async function routeCommand(context) {
2228
+ const parsed = parseCommand(context.message.text);
2229
+ if (!parsed) return { handled: false };
2230
+ if (ADMIN_COMMANDS.has(parsed.name) && !canRunAdminCommand(context.access)) {
2231
+ return text("This command is limited to the owner or admins.");
2232
+ }
2233
+ switch (parsed.name) {
2234
+ case "/help":
2235
+ return card2(buildHelpCard(context.profile, commandCardContext(context)));
2236
+ case "/status":
2237
+ return statusCard(context);
2238
+ case "/new":
2239
+ case "/reset":
2240
+ if (parsed.name === "/new" && parsed.args[0] === "group") return newGroupCommand(context, parsed.args.slice(1).join(" "));
2241
+ await context.stopScope(context.message.scopeId);
2242
+ await context.sessions.clear(context.message.scopeId);
2243
+ await context.sessionCatalog.archiveScope(context.message.scopeId);
2244
+ return text("Session reset for this scope.");
2245
+ case "/cd":
2246
+ return cdCommand(context, parsed.args.join(" "));
2247
+ case "/ws":
2248
+ return workspaceCommand(context, parsed.args);
2249
+ case "/stop":
2250
+ return text(await context.stopScope(context.message.scopeId) ? "Stopping the current run." : "No active run in this scope.");
2251
+ case "/timeout":
2252
+ return timeoutCommand(context, parsed.args[0]);
2253
+ case "/config":
2254
+ return configCommand(context, parsed.args);
2255
+ case "/invite":
2256
+ return inviteCommand(context, parsed.args);
2257
+ case "/remove":
2258
+ return removeCommand(context, parsed.args);
2259
+ case "/doctor":
2260
+ return markdown3(await doctorText(context, parsed.args[0] === "detail"), true);
2261
+ case "/ps":
2262
+ return markdown3(`Process registry:
2263
+
2264
+ \`\`\`json
2265
+ ${JSON.stringify(await context.processSnapshot?.() ?? [], null, 2)}
2266
+ \`\`\``);
2267
+ case "/exit":
2268
+ if (!context.lifecycle?.exit) return text("Lifecycle controls are not available in this runtime.");
2269
+ await context.lifecycle?.exit?.(parsed.args[0]);
2270
+ return text("Exit requested.");
2271
+ case "/reconnect":
2272
+ if (!context.lifecycle?.reconnect) return text("Reconnect is not available in this runtime.");
2273
+ await context.lifecycle?.reconnect?.();
2274
+ return text("Reconnect requested.");
2275
+ case "/resume":
2276
+ return resumeCommand(context, parsed.args);
2277
+ default:
2278
+ return text(`Unknown command: ${parsed.name}`);
2279
+ }
2280
+ }
2281
+ function parseCommand(text2) {
2282
+ const trimmed = text2.trim();
2283
+ if (!trimmed.startsWith("/")) return void 0;
2284
+ const [name, ...args] = trimmed.split(/\s+/);
2285
+ return { name: name.toLowerCase(), args };
2286
+ }
2287
+ async function cdCommand(context, path) {
2288
+ const resolved = await resolveWorkspace(path);
2289
+ if (!resolved.ok) return text(resolved.userVisible);
2290
+ await context.stopScope(context.message.scopeId);
2291
+ await context.workspaces.setCwd(context.message.scopeId, resolved.cwdRealpath);
2292
+ await context.sessions.clear(context.message.scopeId);
2293
+ await context.sessionCatalog.archiveScope(context.message.scopeId);
2294
+ return text(`Workspace switched to ${resolved.cwdRealpath}. Session reset.`);
2295
+ }
2296
+ async function workspaceCommand(context, args) {
2297
+ const [sub, name] = args;
2298
+ if (sub === "list") {
2299
+ const aliases = await context.workspaces.aliases();
2300
+ const current = await context.workspaces.cwdFor(context.message.scopeId);
2301
+ return card2(buildWorkspaceListCard({ current, aliases, context: commandCardContext(context) }));
2302
+ }
2303
+ if (sub === "save" && name) {
2304
+ const cwd = await context.workspaces.cwdFor(context.message.scopeId);
2305
+ if (!cwd) return text("No current workspace to save.");
2306
+ await context.workspaces.saveAlias(name, cwd);
2307
+ return text(`Saved workspace ${name}.`);
2308
+ }
2309
+ if (sub === "add" && name && args[2]) {
2310
+ const resolved = await resolveWorkspace(args.slice(2).join(" "));
2311
+ if (!resolved.ok) return text(resolved.userVisible);
2312
+ await context.workspaces.saveAlias(name, resolved.cwdRealpath);
2313
+ return text(`Saved workspace ${name}: ${resolved.cwdRealpath}.`);
2314
+ }
2315
+ if (sub === "use" && name) {
2316
+ const cwd = await context.workspaces.alias(name);
2317
+ if (!cwd) return text(`Workspace alias not found: ${name}`);
2318
+ const resolved = await resolveWorkspace(cwd);
2319
+ if (!resolved.ok) return text(resolved.userVisible);
2320
+ await context.workspaces.setCwd(context.message.scopeId, resolved.cwdRealpath);
2321
+ await context.sessions.clear(context.message.scopeId);
2322
+ await context.sessionCatalog.archiveScope(context.message.scopeId);
2323
+ return text(`Workspace switched to ${name}. Session reset.`);
2324
+ }
2325
+ if (sub === "remove" && name) {
2326
+ await context.workspaces.removeAlias(name);
2327
+ return text(`Removed workspace ${name}.`);
2328
+ }
2329
+ return text("Usage: /ws list | /ws save <name> | /ws add <name> <path> | /ws use <name> | /ws remove <name>");
2330
+ }
2331
+ async function timeoutCommand(context, value) {
2332
+ const current = await context.sessions.get(context.message.scopeId);
2333
+ if (!value) {
2334
+ return text(`Current timeout: ${current?.idleTimeoutMinutes ?? context.profile.preferences.idleTimeoutMinutes ?? "default"} minutes.`);
2335
+ }
2336
+ if (value === "off" || value === "default") await context.sessions.setTimeout(context.message.scopeId, void 0);
2337
+ else {
2338
+ const minutes = Number(value);
2339
+ if (!Number.isFinite(minutes) || minutes < 1 || minutes > 24 * 60) return text("Timeout must be 1-1440 minutes, off, or default.");
2340
+ await context.sessions.setTimeout(context.message.scopeId, Math.floor(minutes));
2341
+ }
2342
+ const next = await context.sessions.get(context.message.scopeId);
2343
+ return text(`Timeout updated: ${next?.idleTimeoutMinutes ?? context.profile.preferences.idleTimeoutMinutes ?? "default"}.`);
2344
+ }
2345
+ async function inviteCommand(context, args) {
2346
+ const [kind, second] = args;
2347
+ if (kind === "all" && second === "group") {
2348
+ if (!context.gateway?.listJoinedChats) return text("The current channel cannot list joined groups.");
2349
+ const chats = await context.gateway.listJoinedChats();
2350
+ for (const chat of chats) addUnique(context.profile.access.allowedChats, chat.chatId);
2351
+ await context.saveProfile?.(context.profile);
2352
+ return text(`Access list updated. Added ${chats.length} groups.`);
2353
+ }
2354
+ if (kind === "group") {
2355
+ if (!context.message.chatId || context.message.chatType === "private") return text("/invite group must be used in a group or topic chat.");
2356
+ addUnique(context.profile.access.allowedChats, context.message.chatId);
2357
+ } else if (kind === "admin") {
2358
+ const mentions = realUserMentions(context.message);
2359
+ if (mentions.length === 0) return text("Mention at least one user: /invite admin @name");
2360
+ for (const mention of mentions) addUnique(context.profile.access.admins, mention.id);
2361
+ } else if (kind === "user") {
2362
+ const mentions = realUserMentions(context.message);
2363
+ if (mentions.length === 0) return text("Mention at least one user: /invite user @name");
2364
+ for (const mention of mentions) addUnique(context.profile.access.allowedUsers, mention.id);
2365
+ } else {
2366
+ return text("Usage: /invite user @name | /invite admin @name | /invite group | /invite all group");
2367
+ }
2368
+ await context.saveProfile?.(context.profile);
2369
+ return text("Access list updated.");
2370
+ }
2371
+ async function removeCommand(context, args) {
2372
+ const [kind] = args;
2373
+ if (kind === "group" && context.message.chatId) {
2374
+ if (context.message.chatType === "private") return text("/remove group must be used in a group or topic chat.");
2375
+ removeValue(context.profile.access.allowedChats, context.message.chatId);
2376
+ } else if (kind === "admin") {
2377
+ const mentions = realUserMentions(context.message);
2378
+ if (mentions.length === 0) return text("Mention at least one user: /remove admin @name");
2379
+ for (const mention of mentions) removeValue(context.profile.access.admins, mention.id);
2380
+ } else if (kind === "user") {
2381
+ const mentions = realUserMentions(context.message);
2382
+ if (mentions.length === 0) return text("Mention at least one user: /remove user @name");
2383
+ for (const mention of mentions) removeValue(context.profile.access.allowedUsers, mention.id);
2384
+ } else {
2385
+ return text("Usage: /remove user @name | /remove admin @name | /remove group");
2386
+ }
2387
+ await context.saveProfile?.(context.profile);
2388
+ return text("Access list updated.");
2389
+ }
2390
+ async function statusCard(context) {
2391
+ const cwd = await context.workspaces.cwdFor(context.message.scopeId) ?? context.profile.workspaces.default ?? "(not set)";
2392
+ const session = await context.sessions.get(context.message.scopeId);
2393
+ return card2(
2394
+ buildStatusCard({
2395
+ profile: context.profile,
2396
+ scopeId: context.message.scopeId,
2397
+ workspace: cwd,
2398
+ threadId: session?.threadId,
2399
+ queue: context.queueSnapshot(context.message.scopeId),
2400
+ actorRole: context.access.role,
2401
+ context: commandCardContext(context)
2402
+ })
2403
+ );
2404
+ }
2405
+ function commandCardContext(context) {
2406
+ return {
2407
+ scopeId: context.message.scopeId,
2408
+ chatId: context.message.chatId,
2409
+ chatType: context.message.chatType,
2410
+ operatorOpenId: context.message.actorId,
2411
+ callbackAuth: context.callbackAuth
2412
+ };
2413
+ }
2414
+ async function doctorText(context, detail) {
2415
+ const availability = await context.runner.checkAvailability();
2416
+ const cwd = await context.workspaces.cwdFor(context.message.scopeId) ?? context.profile.workspaces.default;
2417
+ const workspace = await resolveWorkspace(cwd);
2418
+ const lines = [
2419
+ "Doctor report",
2420
+ `Profile: ${context.profile.name}`,
2421
+ `Backend: ${availability.label} (${availability.ok ? "ok" : "failed"})`,
2422
+ availability.detail ? `Backend detail: ${availability.detail}` : void 0,
2423
+ `Workspace: ${workspace.ok ? workspace.cwdRealpath : workspace.userVisible}`,
2424
+ `Sandbox: ${context.profile.permissions.defaultAccess}`,
2425
+ `Access: ${context.access.role}`,
2426
+ `Queue: ${JSON.stringify(context.queueSnapshot(context.message.scopeId))}`
2427
+ ];
2428
+ if (detail) {
2429
+ lines.push(
2430
+ `App suffix: ${context.profile.app.id.slice(-6)}`,
2431
+ `Reply mode: ${context.profile.preferences.replyMode}`,
2432
+ `Tool calls: ${context.profile.preferences.showToolCalls ? "show" : "hide"}`,
2433
+ `Debounce: ${context.profile.preferences.debounceMs}ms`,
2434
+ `Max concurrency: ${context.profile.preferences.maxConcurrency}`,
2435
+ `Require mention in group: ${context.profile.access.requireMentionInGroup ? "yes" : "no"}`,
2436
+ `Allowed users: ${context.profile.access.allowedUsers.length}`,
2437
+ `Allowed chats: ${context.profile.access.allowedChats.length}`,
2438
+ `Admins: ${context.profile.access.admins.length}`
2439
+ );
2440
+ }
2441
+ return lines.filter(Boolean).join("\n");
2442
+ }
2443
+ function configText(profile) {
2444
+ return JSON.stringify(
2445
+ {
2446
+ replyMode: profile.preferences.replyMode,
2447
+ showToolCalls: profile.preferences.showToolCalls,
2448
+ maxConcurrency: profile.preferences.maxConcurrency,
2449
+ requireMentionInGroup: profile.access.requireMentionInGroup,
2450
+ access: profile.permissions,
2451
+ codexAuth: profile.codex.auth.mode,
2452
+ feishuCli: profile.feishuCli.identityPreset
2453
+ },
2454
+ null,
2455
+ 2
2456
+ );
2457
+ }
2458
+ async function configCommand(context, args) {
2459
+ if (args[0] === "show") return markdown3(configText(context.profile));
2460
+ if (args[0] === "submit") {
2461
+ if (!context.formValues) return text("No config form values received.");
2462
+ const result = applyConfigForm(context.profile, context.formValues);
2463
+ if (!result.ok) return text(result.message);
2464
+ await context.saveProfile?.(context.profile);
2465
+ return text("Config updated.");
2466
+ }
2467
+ return card2(buildConfigCard(context.profile, commandCardContext(context)));
2468
+ }
2469
+ async function resumeCommand(context, args) {
2470
+ if (!context.resumeNonces) return text("Resume controls are not available in this runtime.");
2471
+ if (args[0] === "use") {
2472
+ const nonce = args[1];
2473
+ if (!nonce) return text("Usage: /resume use <nonce>");
2474
+ const key = context.resumeNonces.consume(nonce);
2475
+ if (!key) return text("Resume token expired or invalid.");
2476
+ const entry = await context.sessionCatalog.restoreActive(key);
2477
+ if (!entry || entry.scopeId !== context.message.scopeId) return text("Resume candidate is no longer available.");
2478
+ await context.sessions.set(context.message.scopeId, entry.codexThreadId, entry.cwdRealpath);
2479
+ return text(`Resumed thread ${entry.codexThreadId}.`);
2480
+ }
2481
+ const cwd = await context.workspaces.cwdFor(context.message.scopeId) ?? context.profile.workspaces.default;
2482
+ const workspace = await resolveWorkspace(cwd);
2483
+ if (!workspace.ok) return text(workspace.userVisible);
2484
+ const candidates = await context.sessionCatalog.recentForScopeWorkspace(context.message.scopeId, workspace.cwdRealpath, 8);
2485
+ if (candidates.length === 0) return text("No resumable sessions for this scope and workspace.");
2486
+ const lines = ["Resumable sessions:"];
2487
+ for (const entry of candidates) {
2488
+ const nonce = context.resumeNonces.issue(entry.key);
2489
+ lines.push(`- ${new Date(entry.updatedAt).toLocaleString()} ${entry.codexThreadId} -> /resume use ${nonce}`);
2490
+ }
2491
+ return markdown3(lines.join("\n"));
2492
+ }
2493
+ async function newGroupCommand(context, rawName) {
2494
+ if (!canRunAdminCommand(context.access)) return text("Creating groups is limited to the owner or admins.");
2495
+ const name = rawName.trim() || defaultGroupName(context.profile.name);
2496
+ if (!context.gateway?.createGroupChat) return text("The current channel cannot create group chats.");
2497
+ const created = await context.gateway.createGroupChat({ name, inviteOpenId: context.message.actorId });
2498
+ const sourceCwd = await context.workspaces.cwdFor(context.message.scopeId) ?? context.profile.workspaces.default;
2499
+ if (sourceCwd) await context.workspaces.setCwd(`chat:${created.chatId}`, sourceCwd);
2500
+ const welcome = sourceCwd ? `Group created. Workspace inherited: ${sourceCwd}
2501
+
2502
+ Mention me with a task to start.` : "Group created.\n\nMention me with a task to start.";
2503
+ await context.gateway.sendMessage({ chatId: created.chatId }, { mode: "markdown", content: welcome }).catch(() => void 0);
2504
+ return text(`Created group ${created.name}.`);
2505
+ }
2506
+ function applyConfigForm(profile, form) {
2507
+ const replyMode = stringForm(form.replyMode);
2508
+ if (replyMode && (replyMode === "card" || replyMode === "markdown" || replyMode === "text")) profile.preferences.replyMode = replyMode;
2509
+ const showToolCalls = boolForm(form.showToolCalls);
2510
+ if (showToolCalls !== void 0) profile.preferences.showToolCalls = showToolCalls;
2511
+ const debounce = intForm(form.debounceMs);
2512
+ if (debounce !== void 0) {
2513
+ if (debounce < 0 || debounce > 6e4) return { ok: false, message: "debounceMs must be 0-60000." };
2514
+ profile.preferences.debounceMs = debounce;
2515
+ }
2516
+ const maxConcurrency = intForm(form.maxConcurrency);
2517
+ if (maxConcurrency !== void 0) {
2518
+ if (maxConcurrency < 1 || maxConcurrency > 50) return { ok: false, message: "maxConcurrency must be 1-50." };
2519
+ profile.preferences.maxConcurrency = maxConcurrency;
2520
+ }
2521
+ const idle = stringForm(form.idleTimeoutMinutes);
2522
+ if (idle !== void 0) {
2523
+ if (idle.trim() === "") delete profile.preferences.idleTimeoutMinutes;
2524
+ else {
2525
+ const minutes = Number(idle);
2526
+ if (!Number.isInteger(minutes) || minutes < 1 || minutes > 1440) return { ok: false, message: "idleTimeoutMinutes must be blank or 1-1440." };
2527
+ profile.preferences.idleTimeoutMinutes = minutes;
2528
+ }
2529
+ }
2530
+ const requireMention = boolForm(form.requireMentionInGroup);
2531
+ if (requireMention !== void 0) profile.access.requireMentionInGroup = requireMention;
2532
+ const identity = stringForm(form.larkCliIdentity);
2533
+ if (identity === "bot-only" || identity === "user-default" || identity === "disabled") {
2534
+ profile.feishuCli.identityPreset = identity;
2535
+ profile.feishuCli.enabled = identity !== "disabled";
2536
+ }
2537
+ const defaultAccess = stringForm(form.defaultAccess);
2538
+ if (defaultAccess === "read-only" || defaultAccess === "workspace" || defaultAccess === "full") profile.permissions.defaultAccess = defaultAccess;
2539
+ const maxAccess = stringForm(form.maxAccess);
2540
+ if (maxAccess === "read-only" || maxAccess === "workspace" || maxAccess === "full") profile.permissions.maxAccess = maxAccess;
2541
+ const network = boolForm(form.networkAccess);
2542
+ if (network !== void 0) profile.permissions.networkAccess = network;
2543
+ return { ok: true };
2544
+ }
2545
+ function stringForm(value) {
2546
+ if (typeof value === "string") return value;
2547
+ if (value && typeof value === "object") {
2548
+ const raw = value;
2549
+ if (typeof raw.value === "string") return raw.value;
2550
+ }
2551
+ return void 0;
2552
+ }
2553
+ function boolForm(value) {
2554
+ const raw = stringForm(value);
2555
+ if (raw === "true") return true;
2556
+ if (raw === "false") return false;
2557
+ return void 0;
2558
+ }
2559
+ function intForm(value) {
2560
+ const raw = stringForm(value);
2561
+ if (raw === void 0 || raw.trim() === "") return void 0;
2562
+ const num = Number(raw);
2563
+ return Number.isInteger(num) ? num : void 0;
2564
+ }
2565
+ function defaultGroupName(profileName) {
2566
+ const now = /* @__PURE__ */ new Date();
2567
+ const pad = (value) => String(value).padStart(2, "0");
2568
+ return `${profileName} ${now.getMonth() + 1}-${now.getDate()} ${pad(now.getHours())}:${pad(now.getMinutes())}`;
2569
+ }
2570
+ function realUserMentions(message) {
2571
+ return message.mentions.filter((mention) => mention.id && !mention.isBot && !mention.isAll);
2572
+ }
2573
+ function text(content, sensitive = false) {
2574
+ return { handled: true, sensitive, reply: { mode: "text", content } };
2575
+ }
2576
+ function markdown3(content, sensitive = false) {
2577
+ return { handled: true, sensitive, reply: { mode: "markdown", content } };
2578
+ }
2579
+ function card2(content, sensitive = false) {
2580
+ return { handled: true, sensitive, reply: { mode: "card", content } };
2581
+ }
2582
+ function addUnique(values, value) {
2583
+ if (!values.includes(value)) values.push(value);
2584
+ }
2585
+ function removeValue(values, value) {
2586
+ const index = values.indexOf(value);
2587
+ if (index >= 0) values.splice(index, 1);
2588
+ }
2589
+
2590
+ // src/media/resolve.ts
2591
+ async function resolveMessageAttachments(input2) {
2592
+ const out = [];
2593
+ for (const resource of input2.message.resources) {
2594
+ const kind = attachmentKind(resource);
2595
+ const required = resource.required !== false;
2596
+ if (!kind) {
2597
+ if (required) out.push({ kind: "file", required, decision: "rejected", reason: `unsupported attachment kind: ${resource.kind}` });
2598
+ continue;
2599
+ }
2600
+ if (!input2.gateway.downloadResource) {
2601
+ out.push({ kind, required, decision: "rejected", reason: "resource download unavailable" });
2602
+ continue;
2603
+ }
2604
+ try {
2605
+ const data = await input2.gateway.downloadResource(input2.message, resource);
2606
+ const cached = await input2.cache.putBytes(data.bytes, { name: data.name ?? resource.name, mime: data.mime ?? resource.mime });
2607
+ out.push({
2608
+ kind,
2609
+ path: cached.path,
2610
+ name: data.name ?? resource.name,
2611
+ bytes: cached.bytes,
2612
+ mime: data.mime ?? resource.mime ?? cached.mime,
2613
+ required
2614
+ });
2615
+ } catch (error) {
2616
+ out.push({
2617
+ kind,
2618
+ required,
2619
+ decision: "rejected",
2620
+ reason: error instanceof Error ? error.message : "resource download failed"
2621
+ });
2622
+ }
2623
+ }
2624
+ return out;
2625
+ }
2626
+ function attachmentKind(resource) {
2627
+ if (resource.kind === "image") return "image";
2628
+ if (resource.kind === "file") return "file";
2629
+ return void 0;
2630
+ }
2631
+
2632
+ // src/session/resume-nonce.ts
2633
+ import { randomBytes as randomBytes5 } from "crypto";
2634
+ var ResumeNonceStore = class {
2635
+ constructor(now = Date.now) {
2636
+ this.now = now;
2637
+ }
2638
+ now;
2639
+ entries = /* @__PURE__ */ new Map();
2640
+ issue(key, ttlMs = 10 * 60 * 1e3) {
2641
+ this.gc();
2642
+ const nonce = randomBytes5(5).toString("base64url");
2643
+ this.entries.set(nonce, { key, expiresAt: this.now() + ttlMs });
2644
+ return nonce;
2645
+ }
2646
+ consume(nonce) {
2647
+ this.gc();
2648
+ const entry = this.entries.get(nonce);
2649
+ if (!entry) return void 0;
2650
+ this.entries.delete(nonce);
2651
+ return entry.key;
2652
+ }
2653
+ gc() {
2654
+ const now = this.now();
2655
+ for (const [nonce, entry] of this.entries) {
2656
+ if (entry.expiresAt <= now) this.entries.delete(nonce);
2657
+ }
2658
+ }
2659
+ };
2660
+
2661
+ // src/runtime/pending-queue.ts
2662
+ var PendingMessageQueue = class {
2663
+ constructor(options) {
2664
+ this.options = options;
2665
+ }
2666
+ options;
2667
+ scopes = /* @__PURE__ */ new Map();
2668
+ push(scopeId, item) {
2669
+ const scope = this.scope(scopeId);
2670
+ scope.items.push(item);
2671
+ this.schedule(scopeId, scope);
2672
+ }
2673
+ block(scopeId) {
2674
+ const scope = this.scope(scopeId);
2675
+ scope.blocked = true;
2676
+ this.clearTimer(scope);
2677
+ }
2678
+ unblock(scopeId) {
2679
+ const scope = this.scopes.get(scopeId);
2680
+ if (!scope) return;
2681
+ scope.blocked = false;
2682
+ this.schedule(scopeId, scope);
2683
+ }
2684
+ cancel(scopeId) {
2685
+ const scope = this.scopes.get(scopeId);
2686
+ if (!scope) return 0;
2687
+ this.clearTimer(scope);
2688
+ const count = scope.items.length;
2689
+ this.scopes.delete(scopeId);
2690
+ return count;
2691
+ }
2692
+ snapshot(scopeId) {
2693
+ if (scopeId) {
2694
+ const scope = this.scopes.get(scopeId);
2695
+ return { queued: scope?.items.length ?? 0, blocked: Boolean(scope?.blocked) };
2696
+ }
2697
+ let queued = 0;
2698
+ let blocked = 0;
2699
+ for (const scope of this.scopes.values()) {
2700
+ queued += scope.items.length;
2701
+ if (scope.blocked) blocked += 1;
2702
+ }
2703
+ return { scopes: this.scopes.size, queued, blocked };
2704
+ }
2705
+ scope(scopeId) {
2706
+ const existing = this.scopes.get(scopeId);
2707
+ if (existing) return existing;
2708
+ const next = { items: [], blocked: false };
2709
+ this.scopes.set(scopeId, next);
2710
+ return next;
2711
+ }
2712
+ schedule(scopeId, scope) {
2713
+ this.clearTimer(scope);
2714
+ if (scope.blocked || scope.items.length === 0) return;
2715
+ scope.timer = setTimeout(() => {
2716
+ void this.flushScope(scopeId);
2717
+ }, Math.max(0, this.options.debounceMs));
2718
+ }
2719
+ async flushScope(scopeId) {
2720
+ const scope = this.scopes.get(scopeId);
2721
+ if (!scope || scope.blocked || scope.items.length === 0) return;
2722
+ this.clearTimer(scope);
2723
+ const items = scope.items.splice(0);
2724
+ this.scopes.delete(scopeId);
2725
+ await this.options.flush(scopeId, items);
2726
+ }
2727
+ clearTimer(scope) {
2728
+ if (!scope.timer) return;
2729
+ clearTimeout(scope.timer);
2730
+ scope.timer = void 0;
2731
+ }
2732
+ };
2733
+
2734
+ // src/runtime/bridge-runtime.ts
2735
+ var CARD_RENDER_THROTTLE_MS = 400;
2736
+ function bindBridgeRuntime(input2) {
2737
+ const sessions = new SessionStore(input2.paths.sessions);
2738
+ const sessionCatalog = new SessionCatalog(input2.paths.sessionCatalog);
2739
+ const workspaces = new WorkspaceStore(input2.paths.workspaces);
2740
+ const mediaCache = new MediaCache(input2.paths.mediaDir);
2741
+ void mediaCache.gc(input2.profile.attachments.cacheTtlMs).catch(() => void 0);
2742
+ const callbackAuth = new CallbackAuth([{ version: 1, secret: input2.appSecret }], new MemoryCallbackNonceStore());
2743
+ const resumeNonces = new ResumeNonceStore();
2744
+ const states = /* @__PURE__ */ new Map();
2745
+ let orchestrator;
2746
+ const pending = new PendingMessageQueue({
2747
+ debounceMs: input2.profile.preferences.debounceMs,
2748
+ flush: async (_scopeId, items) => {
2749
+ await submitPendingMessages(input2, orchestrator, pending, states, callbackAuth, mediaCache, items);
2750
+ }
2751
+ });
2752
+ orchestrator = new RunOrchestrator({
2753
+ profile: input2.profile,
2754
+ runner: input2.runner,
2755
+ secretResolver: input2.secretResolver,
2756
+ sessions,
2757
+ sessionCatalog,
2758
+ workspaces,
2759
+ onEvent: async (scopeId, event) => {
2760
+ await renderEvent(input2.gateway, states, scopeId, event);
2761
+ if (event.type === "done" || event.type === "error") {
2762
+ pending.unblock(scopeId);
2763
+ }
2764
+ }
2765
+ });
2766
+ input2.gateway.onMessage?.(async (message) => {
2767
+ await maybeBootstrapOwner(input2, message);
2768
+ const access3 = evaluateAccess({ profile: input2.profile, message, ownerOpenId: input2.ownerOpenId, botOpenId: input2.botOpenId });
2769
+ console.log(`[bridge:access] scope=${message.scopeId} actor=${message.actorId} ok=${access3.ok} role=${access3.role} reason=${access3.reason ?? ""}`);
2770
+ if (!access3.ok) return;
2771
+ const parsed = parseCommand(message.text);
2772
+ const command = await routeCommand({
2773
+ profile: input2.profile,
2774
+ access: access3,
2775
+ message,
2776
+ sessions,
2777
+ sessionCatalog,
2778
+ workspaces,
2779
+ runner: input2.runner,
2780
+ stopScope: (scopeId) => orchestrator.stop(scopeId),
2781
+ queueSnapshot: (scopeId) => ({ run: orchestrator.snapshot(scopeId), pending: pending.snapshot(scopeId) }),
2782
+ processSnapshot: input2.processSnapshot,
2783
+ saveProfile: input2.saveProfile,
2784
+ callbackAuth,
2785
+ resumeNonces,
2786
+ gateway: input2.gateway,
2787
+ lifecycle: input2.lifecycle
2788
+ });
2789
+ if (command.handled) {
2790
+ if (parsed && cancelsPending(parsed.name, parsed.args)) pending.cancel(message.scopeId);
2791
+ if (command.reply) await sendBridgeReply(input2.gateway, message, command.reply);
2792
+ return;
2793
+ }
2794
+ if (orchestrator.isActive(message.scopeId)) pending.block(message.scopeId);
2795
+ pending.push(message.scopeId, { message, payload: { access: access3 } });
2796
+ });
2797
+ input2.gateway.onAction?.(async (action) => {
2798
+ const actionCommand = typeof action.value?.command === "string" ? action.value.command : void 0;
2799
+ if (actionCommand) {
2800
+ const token = typeof action.value?.token === "string" ? action.value.token : "";
2801
+ const verified2 = callbackAuth.verify(token, {
2802
+ runId: "command",
2803
+ scopeId: typeof action.value?.scopeId === "string" ? action.value.scopeId : action.scopeId,
2804
+ chatId: typeof action.value?.chatId === "string" ? action.value.chatId : action.chatId,
2805
+ operatorOpenId: action.operatorOpenId,
2806
+ action: actionCommand,
2807
+ policyFingerprint: "command"
2808
+ });
2809
+ if (!verified2.ok) {
2810
+ await input2.gateway.sendMessage({ chatId: action.chatId, messageId: action.messageId }, { mode: "text", content: `Action denied: ${verified2.reason}` });
2811
+ return;
2812
+ }
2813
+ const message = messageFromAction(action, actionCommand, input2.botOpenId);
2814
+ const access3 = evaluateAccess({ profile: input2.profile, message, ownerOpenId: input2.ownerOpenId, botOpenId: input2.botOpenId });
2815
+ if (!access3.ok) {
2816
+ await input2.gateway.sendMessage({ chatId: action.chatId, messageId: action.messageId }, { mode: "text", content: `Action denied: ${access3.reason ?? "unauthorized"}` });
2817
+ return;
2818
+ }
2819
+ const parsed = parseCommand(message.text);
2820
+ const command = await routeCommand({
2821
+ profile: input2.profile,
2822
+ access: access3,
2823
+ message,
2824
+ sessions,
2825
+ sessionCatalog,
2826
+ workspaces,
2827
+ runner: input2.runner,
2828
+ stopScope: (scopeId) => orchestrator.stop(scopeId),
2829
+ queueSnapshot: (scopeId) => ({ run: orchestrator.snapshot(scopeId), pending: pending.snapshot(scopeId) }),
2830
+ processSnapshot: input2.processSnapshot,
2831
+ saveProfile: input2.saveProfile,
2832
+ callbackAuth,
2833
+ formValues: action.formValues,
2834
+ resumeNonces,
2835
+ gateway: input2.gateway,
2836
+ lifecycle: input2.lifecycle
2837
+ });
2838
+ if (command.handled) {
2839
+ if (parsed && cancelsPending(parsed.name, parsed.args)) pending.cancel(message.scopeId);
2840
+ if (command.reply) await input2.gateway.sendMessage({ chatId: message.chatId, messageId: message.messageId }, command.reply);
2841
+ }
2842
+ return;
2843
+ }
2844
+ const entry = states.get(action.scopeId);
2845
+ if (!entry || action.token.length === 0) return;
2846
+ const verified = callbackAuth.verify(action.token, {
2847
+ runId: entry.state.runId,
2848
+ scopeId: action.scopeId,
2849
+ chatId: action.chatId,
2850
+ operatorOpenId: action.operatorOpenId,
2851
+ action: "stop",
2852
+ policyFingerprint: entry.context.policyFingerprint
2853
+ });
2854
+ if (!verified.ok) return;
2855
+ await orchestrator.stop(action.scopeId);
2856
+ });
2857
+ return orchestrator;
2858
+ }
2859
+ async function maybeBootstrapOwner(input2, message) {
2860
+ if (!input2.bootstrapOwner) return;
2861
+ if (message.chatType !== "private" || message.actorType === "bot" || !message.actorId) return;
2862
+ const access3 = input2.profile.access;
2863
+ const hasConfiguredOperator = Boolean(input2.ownerOpenId) || access3.admins.length > 0 || access3.allowedUsers.length > 0 || access3.allowedChats.length > 0;
2864
+ if (hasConfiguredOperator) return;
2865
+ access3.admins.push(message.actorId);
2866
+ access3.allowedUsers.push(message.actorId);
2867
+ await input2.saveProfile?.(input2.profile);
2868
+ console.log(`bootstrapped owner/admin open_id=${message.actorId}`);
2869
+ }
2870
+ async function submitPendingMessages(input2, orchestrator, pending, states, callbackAuth, mediaCache, items) {
2871
+ const first = items[0];
2872
+ if (!first) return;
2873
+ const message = first.message;
2874
+ const attachmentsByMessage = await Promise.all(
2875
+ items.map((item) => resolveMessageAttachments({ gateway: input2.gateway, cache: mediaCache, message: item.message }))
2876
+ );
2877
+ const prompt = items.map((item, index) => buildPrompt({ message: item.message, botOpenId: input2.botOpenId, attachments: attachmentsByMessage[index] ?? [] })).join("\n\n");
2878
+ const attachments = attachmentsByMessage.flat();
2879
+ pending.block(message.scopeId);
2880
+ const result = await orchestrator.submit({ message, access: first.payload.access, prompt, attachments });
2881
+ if ("queued" in result) {
2882
+ return;
2883
+ }
2884
+ if (!result.ok) {
2885
+ pending.unblock(message.scopeId);
2886
+ await sendBridgeReply(input2.gateway, message, { mode: "text", content: result.userVisible });
2887
+ return;
2888
+ }
2889
+ const state = { ...createRunState(result.handle.runId), status: "running" };
2890
+ const context = {
2891
+ scopeId: message.scopeId,
2892
+ chatId: message.chatId ?? "",
2893
+ operatorOpenId: message.actorId,
2894
+ policyFingerprint: result.policy.policyFingerprint,
2895
+ workspaceLabel: result.cwdRealpath,
2896
+ sandbox: result.policy.sandbox,
2897
+ authMode: input2.profile.codex.auth.mode,
2898
+ callbackAuth
2899
+ };
2900
+ if (message.commentTarget) {
2901
+ states.set(message.scopeId, { state, context, commentTarget: message.commentTarget, commentScopeId: message.commentScopeId });
2902
+ return;
2903
+ }
2904
+ states.set(message.scopeId, { state, context, initialSendPending: true });
2905
+ const sent = await input2.gateway.sendMessage({ chatId: message.chatId, messageId: message.messageId }, { mode: "card", content: buildRunCard(state, context) });
2906
+ const entry = states.get(message.scopeId);
2907
+ if (entry) {
2908
+ entry.initialSendPending = false;
2909
+ if (sent?.messageId) entry.messageId = sent.messageId;
2910
+ if (entry.renderPending) await flushRender(input2.gateway, entry);
2911
+ }
2912
+ }
2913
+ function cancelsPending(name, args) {
2914
+ if (name === "/new" || name === "/reset" || name === "/cd" || name === "/stop") return true;
2915
+ return name === "/ws" && args[0] === "use";
2916
+ }
2917
+ function messageFromAction(action, command, botOpenId) {
2918
+ const chatType = normalizeActionChatType(action.value?.chatType);
2919
+ return {
2920
+ source: "card",
2921
+ scopeId: typeof action.value?.scopeId === "string" ? action.value.scopeId : action.scopeId,
2922
+ chatId: typeof action.value?.chatId === "string" ? action.value.chatId : action.chatId,
2923
+ messageId: action.messageId,
2924
+ actorId: action.operatorOpenId,
2925
+ actorType: "user",
2926
+ chatType,
2927
+ text: command,
2928
+ mentions: botOpenId && chatType !== "private" ? [{ id: botOpenId, isBot: true }] : [],
2929
+ resources: [],
2930
+ raw: action.raw
2931
+ };
2932
+ }
2933
+ function normalizeActionChatType(value) {
2934
+ return value === "private" || value === "group" || value === "topic" ? value : "private";
2935
+ }
2936
+ async function sendBridgeReply(gateway, message, reply) {
2937
+ if (message.commentTarget) {
2938
+ await gateway.sendMessage(
2939
+ { commentScopeId: message.commentScopeId, commentTarget: message.commentTarget },
2940
+ { mode: "comment", content: plainCommentText(reply.content) }
2941
+ );
2942
+ return;
2943
+ }
2944
+ await gateway.sendMessage({ chatId: message.chatId, messageId: message.messageId }, reply);
2945
+ }
2946
+ function plainCommentText(content) {
2947
+ if (typeof content === "string") return stripMarkdown(content);
2948
+ return "This command is not available in document comments.";
2949
+ }
2950
+ async function renderEvent(gateway, states, scopeId, event) {
2951
+ const entry = states.get(scopeId);
2952
+ if (!entry) return;
2953
+ entry.state = applyRunEvent(entry.state, event);
2954
+ if (entry.commentTarget) {
2955
+ if (isTerminalEvent(event)) {
2956
+ const text2 = entry.state.text.trim() || entry.state.error || titleForComment(entry.state.status);
2957
+ await gateway.sendMessage(
2958
+ { commentScopeId: entry.commentScopeId, commentTarget: entry.commentTarget },
2959
+ { mode: "comment", content: plainCommentText(text2) }
2960
+ );
2961
+ }
2962
+ return;
2963
+ }
2964
+ entry.renderPending = true;
2965
+ if (entry.initialSendPending) return;
2966
+ if (isTerminalEvent(event)) {
2967
+ if (entry.renderTimer) clearTimeout(entry.renderTimer);
2968
+ entry.renderTimer = void 0;
2969
+ await flushRender(gateway, entry);
2970
+ return;
2971
+ }
2972
+ const now = Date.now();
2973
+ const last = entry.lastRenderAt ?? 0;
2974
+ if (now - last < CARD_RENDER_THROTTLE_MS) {
2975
+ if (!entry.renderTimer) {
2976
+ entry.renderTimer = setTimeout(() => {
2977
+ entry.renderTimer = void 0;
2978
+ void flushRender(gateway, entry);
2979
+ }, CARD_RENDER_THROTTLE_MS - (now - last));
2980
+ }
2981
+ return;
2982
+ }
2983
+ await flushRender(gateway, entry);
2984
+ }
2985
+ async function flushRender(gateway, entry) {
2986
+ if (!entry.renderPending) return;
2987
+ entry.renderPending = false;
2988
+ entry.lastRenderAt = Date.now();
2989
+ const reply = entry.context.callbackAuth && entry.context.chatId ? { mode: "card", content: buildRunCard(entry.state, entry.context) } : { mode: "markdown", content: renderMarkdownFallback(entry.state) };
2990
+ if (entry.messageId && gateway.updateMessage) {
2991
+ await gateway.updateMessage(entry.messageId, reply).catch(() => void 0);
2992
+ } else {
2993
+ const sent = await gateway.sendMessage({ chatId: entry.context.chatId }, reply).catch(() => void 0);
2994
+ if (sent?.messageId) entry.messageId = sent.messageId;
2995
+ }
2996
+ }
2997
+ function isTerminalEvent(event) {
2998
+ return event.type === "done" || event.type === "error";
2999
+ }
3000
+ function titleForComment(status) {
3001
+ if (status === "interrupted") return "Stopped.";
3002
+ if (status === "timeout") return "Timed out.";
3003
+ if (status === "error") return "Codex failed.";
3004
+ return "Done.";
3005
+ }
3006
+
3007
+ // src/bootstrap/wizard.ts
3008
+ import { registerApp } from "@larksuiteoapi/node-sdk";
3009
+ import qrcode from "qrcode-terminal";
3010
+ import QRCode from "qrcode";
3011
+ import { mkdir as mkdir4, realpath as realpath2 } from "fs/promises";
3012
+ import { join as join8 } from "path";
3013
+ import { homedir as homedir3 } from "os";
3014
+ import * as p from "@clack/prompts";
3015
+ async function runRegistrationWizard(options = {}) {
3016
+ const register = options.register ?? registerApp;
3017
+ const log = options.log ?? console;
3018
+ log.log("\n\u672A\u68C0\u6D4B\u5230 Feishu/Lark \u5E94\u7528\u914D\u7F6E\uFF0C\u8FDB\u5165\u626B\u7801\u521B\u5EFA\u5411\u5BFC\u3002\n");
3019
+ const result = await register({
3020
+ source: "feishu-codex-connector",
3021
+ appPreset: {
3022
+ name: "{user} PersonalAgent",
3023
+ desc: "Personal Codex agent controlled from Feishu/Lark."
3024
+ },
3025
+ onQRCodeReady: (info) => {
3026
+ log.log("\u8BF7\u7528\u98DE\u4E66 App \u626B\u63CF\u4EE5\u4E0B\u4E8C\u7EF4\u7801\uFF0C\u9009\u62E9\u6216\u521B\u5EFA PersonalAgent \u5E94\u7528\uFF1A\n");
3027
+ if (options.renderQr) options.renderQr(info.url);
3028
+ else qrcode.generate(info.url, { small: true });
3029
+ if (options.qrImagePath) {
3030
+ void QRCode.toFile(options.qrImagePath, info.url, { type: "png", width: 640, margin: 2 }).then(() => log.log(`
3031
+ \u4E8C\u7EF4\u7801\u56FE\u7247\u5DF2\u4FDD\u5B58\uFF1A${options.qrImagePath}`)).catch((error) => log.log(`
3032
+ \u4E8C\u7EF4\u7801\u56FE\u7247\u4FDD\u5B58\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}`));
3033
+ }
3034
+ const minutes = Math.max(1, Math.round(info.expireIn / 60));
3035
+ log.log(`
3036
+ \u4E8C\u7EF4\u7801\u6709\u6548\u671F\uFF1A\u7EA6 ${minutes} \u5206\u949F`);
3037
+ log.log(`\u4E5F\u53EF\u4EE5\u76F4\u63A5\u5728\u6D4F\u89C8\u5668\u6253\u5F00\uFF1A${info.url}
3038
+ `);
3039
+ },
3040
+ onStatusChange: (info) => {
3041
+ if (info.status === "domain_switched") log.log("\u8BC6\u522B\u5230 Lark \u56FD\u9645\u7248\u79DF\u6237\uFF0C\u5DF2\u5207\u6362\u5230 larksuite.com \u57DF\u540D\u3002");
3042
+ if (info.status === "slow_down") log.log("\u8F6E\u8BE2\u901F\u5EA6\u8FC7\u5FEB\uFF0C\u5DF2\u81EA\u52A8\u964D\u901F\u3002");
3043
+ }
3044
+ });
3045
+ const tenant = result.user_info?.tenant_brand ?? "feishu";
3046
+ log.log("\n\u2713 \u5E94\u7528\u521B\u5EFA\u6210\u529F");
3047
+ log.log(` App ID: ${result.client_id}`);
3048
+ log.log(` Tenant: ${tenant}`);
3049
+ if (result.user_info?.open_id) log.log(` Creator: ${result.user_info.open_id}`);
3050
+ log.log("\n\u521D\u59CB\u5316 agent: Codex\uFF08\u672C\u9879\u76EE\u4E3A Codex-only connector\uFF09\n");
3051
+ return {
3052
+ appId: result.client_id,
3053
+ appSecret: result.client_secret,
3054
+ tenant,
3055
+ operatorOpenId: result.user_info?.open_id
3056
+ };
3057
+ }
3058
+ async function ensureProfileForRun(options = {}) {
3059
+ const store = new ProfileStore(options.home);
3060
+ if (options.profile) {
3061
+ try {
3062
+ await store.read(options.profile);
3063
+ return options.profile;
3064
+ } catch (error) {
3065
+ if (!isProfileMissingError(error)) throw error;
3066
+ return bootstrapProfile(options);
3067
+ }
3068
+ }
3069
+ const existing = await store.activeProfile();
3070
+ if (existing) return existing;
3071
+ const list = await store.list();
3072
+ if (list.length > 0) {
3073
+ const selected = options.profile ?? list[0]?.name;
3074
+ if (selected) {
3075
+ await store.use(selected);
3076
+ return selected;
3077
+ }
3078
+ }
3079
+ if (!isInteractiveTerminal() && !options.appId) {
3080
+ throw new Error(
3081
+ "\u5F53\u524D\u6CA1\u6709\u914D\u7F6E\uFF0C\u975E\u4EA4\u4E92\u6A21\u5F0F\u65E0\u6CD5\u5B8C\u6210\u626B\u7801\u521B\u5EFA\u5E94\u7528\u3002\u8BF7\u5148\u5728\u7EC8\u7AEF\u8FD0\u884C `feishu-codex run` \u5B8C\u6210\u9996\u6B21\u521D\u59CB\u5316\uFF0C\u6216\u4F20\u5165 --app-id \u548C --app-secret\u3002"
3082
+ );
3083
+ }
3084
+ return bootstrapProfile(options);
3085
+ }
3086
+ function isProfileMissingError(error) {
3087
+ return error instanceof Error && /profile not found|no active profile|ENOENT/i.test(error.message);
3088
+ }
3089
+ async function bootstrapProfile(options = {}) {
3090
+ const store = new ProfileStore(options.home);
3091
+ const profileName = options.profile ?? "default";
3092
+ const app = options.appId ? await appFromExplicitOptions(options) : await runRegistrationWizard({ qrImagePath: join8(store.paths.home, "registration-qr.png") });
3093
+ const workspace = await resolveBootstrapWorkspace(options.workspace, profileName, store.paths.home);
3094
+ const auth = await resolveBootstrapCodexAuth(options);
3095
+ await store.create({
3096
+ name: profileName,
3097
+ tenant: app.tenant,
3098
+ appId: app.appId,
3099
+ appSecret: app.appSecret,
3100
+ codexAuthMode: auth.mode,
3101
+ codexApiKey: auth.apiKey,
3102
+ workspace,
3103
+ codexPath: options.codexPath,
3104
+ setActive: true
3105
+ });
3106
+ const profile = await store.read(profileName);
3107
+ if (app.operatorOpenId) {
3108
+ profile.access.admins = [app.operatorOpenId];
3109
+ profile.access.allowedUsers = [app.operatorOpenId];
3110
+ await store.write(profile);
3111
+ }
3112
+ console.log(`\u2713 \u914D\u7F6E\u5DF2\u5199\u5165 ${store.paths.configFile}`);
3113
+ console.log(`\u2713 profile \u5DF2\u521D\u59CB\u5316: ${profileName}`);
3114
+ return profileName;
3115
+ }
3116
+ async function appFromExplicitOptions(options) {
3117
+ const appId = options.appId;
3118
+ if (!appId) throw new Error("appId is required");
3119
+ let appSecret = options.appSecret;
3120
+ if (!appSecret) {
3121
+ if (!isInteractiveTerminal()) throw new Error(`\u975E\u4EA4\u4E92\u6A21\u5F0F\u7F3A\u5C11 App Secret: ${appId}`);
3122
+ const value = await p.password({ message: `\u8F93\u5165 ${appId} \u7684 App Secret` });
3123
+ if (p.isCancel(value)) throw new Error("\u5DF2\u53D6\u6D88");
3124
+ appSecret = String(value);
3125
+ }
3126
+ return {
3127
+ appId,
3128
+ appSecret,
3129
+ tenant: options.tenant ?? "feishu"
3130
+ };
3131
+ }
3132
+ async function resolveBootstrapWorkspace(workspace, profile, home) {
3133
+ if (workspace) {
3134
+ const resolved = await resolveWorkspace(workspace);
3135
+ if (!resolved.ok) throw new Error(resolved.userVisible);
3136
+ return resolved.cwdRealpath;
3137
+ }
3138
+ const managed = join8(home, "workspaces", profile, "default");
3139
+ await mkdir4(managed, { recursive: true, mode: 448 });
3140
+ return realpath2(managed);
3141
+ }
3142
+ async function resolveBootstrapCodexAuth(options) {
3143
+ if (options.codexAuth === "api-key") {
3144
+ const apiKey = options.codexApiKey ?? process.env.OPENAI_API_KEY;
3145
+ if (!apiKey) {
3146
+ if (!isInteractiveTerminal()) throw new Error("api-key \u6A21\u5F0F\u9700\u8981 --codex-api-key \u6216 OPENAI_API_KEY");
3147
+ const value = await p.password({ message: "\u8F93\u5165 OpenAI API key" });
3148
+ if (p.isCancel(value)) throw new Error("\u5DF2\u53D6\u6D88");
3149
+ return { mode: "api-key", apiKey: String(value) };
3150
+ }
3151
+ return { mode: "api-key", apiKey };
3152
+ }
3153
+ if (options.codexAuth === "codex-home" || options.codexAuth === "inherit-user") {
3154
+ return { mode: options.codexAuth };
3155
+ }
3156
+ if (process.env.OPENAI_API_KEY) return { mode: "api-key", apiKey: process.env.OPENAI_API_KEY };
3157
+ if (await hasLocalCodexState()) return { mode: "inherit-user" };
3158
+ if (!isInteractiveTerminal()) return { mode: "codex-home" };
3159
+ const selected = await p.select({
3160
+ message: "\u9009\u62E9 Codex \u8BA4\u8BC1\u65B9\u5F0F",
3161
+ options: [
3162
+ { value: "inherit-user", label: "inherit-user", hint: "\u590D\u7528\u672C\u673A Codex \u767B\u5F55\u72B6\u6001" },
3163
+ { value: "api-key", label: "api-key", hint: "\u8F93\u5165 OpenAI API key\uFF0C\u9002\u5408\u540E\u53F0\u8FDC\u7A0B\u8FD0\u884C" },
3164
+ { value: "codex-home", label: "codex-home", hint: "\u4F7F\u7528 profile-local Codex home" }
3165
+ ]
3166
+ });
3167
+ if (p.isCancel(selected)) throw new Error("\u5DF2\u53D6\u6D88");
3168
+ if (selected === "api-key") return resolveBootstrapCodexAuth({ ...options, codexAuth: "api-key" });
3169
+ return { mode: selected };
3170
+ }
3171
+ async function hasLocalCodexState() {
3172
+ try {
3173
+ await realpath2(join8(homedir3(), ".codex"));
3174
+ return true;
3175
+ } catch {
3176
+ return false;
3177
+ }
3178
+ }
3179
+ function isInteractiveTerminal() {
3180
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
3181
+ }
3182
+
3183
+ // src/runtime/app.ts
3184
+ import { once } from "events";
3185
+
3186
+ // src/gateway/lark-channel.ts
3187
+ import { mkdtemp, readFile as readFile8, rm as rm4 } from "fs/promises";
3188
+ import { tmpdir as tmpdir2 } from "os";
3189
+ import { join as join9 } from "path";
3190
+ var LarkChannelGateway = class {
3191
+ constructor(options) {
3192
+ this.options = options;
3193
+ }
3194
+ options;
3195
+ channel;
3196
+ messageHandler;
3197
+ actionHandler;
3198
+ pollTimer;
3199
+ pollSeen = /* @__PURE__ */ new Set();
3200
+ pollStartedAt = Math.floor(Date.now() / 1e3);
3201
+ onMessage(handler) {
3202
+ this.messageHandler = handler;
3203
+ }
3204
+ onAction(handler) {
3205
+ this.actionHandler = handler;
3206
+ }
3207
+ async connect() {
3208
+ const sdk = await import("@larksuiteoapi/node-sdk");
3209
+ const createLarkChannel = sdk.createLarkChannel;
3210
+ if (!createLarkChannel) {
3211
+ throw new Error("@larksuiteoapi/node-sdk does not expose createLarkChannel");
3212
+ }
3213
+ this.channel = createLarkChannel({
3214
+ appId: this.options.appId,
3215
+ appSecret: this.options.appSecret,
3216
+ domain: this.options.tenant === "lark" ? sdk.Domain && sdk.Domain.Lark : sdk.Domain.Feishu,
3217
+ source: "feishu-codex-connector",
3218
+ loggerLevel: sdk.LoggerLevel?.info,
3219
+ policy: {
3220
+ dmMode: "open",
3221
+ requireMention: false,
3222
+ respondToMentionAll: false
3223
+ },
3224
+ safety: {
3225
+ chatQueue: { enabled: false }
3226
+ },
3227
+ includeRawEvent: true,
3228
+ outbound: {
3229
+ streamThrottleMs: 400
3230
+ },
3231
+ wsConfig: {
3232
+ pingTimeout: 3
3233
+ },
3234
+ handshakeTimeoutMs: 8e3
3235
+ });
3236
+ const channel = this.channel;
3237
+ channel.on?.({
3238
+ message: async (event) => {
3239
+ console.log("[bridge:intake] message raw keys=" + Object.keys(record2(event)).join(","));
3240
+ try {
3241
+ const normalized = normalizeMessage(event);
3242
+ console.log(
3243
+ `[bridge:intake] message normalized chat=${normalized.chatId ?? ""} scope=${normalized.scopeId} actor=${normalized.actorId} text=${normalized.text.slice(0, 40)}`
3244
+ );
3245
+ await this.messageHandler?.(normalized);
3246
+ } catch (error) {
3247
+ console.error("[bridge:intake] message normalize/handler failed:", error instanceof Error ? error.stack ?? error.message : String(error));
3248
+ }
3249
+ },
3250
+ cardAction: async (event) => {
3251
+ console.log("[bridge:intake] cardAction raw keys=" + Object.keys(record2(event)).join(","));
3252
+ try {
3253
+ await this.actionHandler?.(normalizeAction(event));
3254
+ } catch (error) {
3255
+ console.error("[bridge:intake] cardAction normalize/handler failed:", error instanceof Error ? error.stack ?? error.message : String(error));
3256
+ }
3257
+ },
3258
+ comment: async (event) => {
3259
+ console.log("[bridge:intake] comment raw keys=" + Object.keys(record2(event)).join(","));
3260
+ try {
3261
+ const normalized = await this.normalizeComment(event);
3262
+ if (normalized) await this.messageHandler?.(normalized);
3263
+ } catch (error) {
3264
+ console.error("[bridge:intake] comment normalize/handler failed:", error instanceof Error ? error.stack ?? error.message : String(error));
3265
+ }
3266
+ },
3267
+ reject: (event) => {
3268
+ console.warn("[bridge:intake] reject " + JSON.stringify(event));
3269
+ },
3270
+ reconnecting: () => {
3271
+ console.warn("[bridge:ws] reconnecting");
3272
+ },
3273
+ reconnected: () => {
3274
+ console.log("[bridge:ws] reconnected");
3275
+ },
3276
+ error: (error) => {
3277
+ console.error("[bridge:ws] error:", error instanceof Error ? error.stack ?? error.message : String(error));
3278
+ }
3279
+ });
3280
+ if (!channel.connect) {
3281
+ throw new Error("Lark channel does not expose connect");
3282
+ }
3283
+ await channel.connect();
3284
+ if (this.options.pollChats?.length) {
3285
+ this.startPollingFallback();
3286
+ }
3287
+ }
3288
+ async disconnect() {
3289
+ if (this.pollTimer) clearInterval(this.pollTimer);
3290
+ this.pollTimer = void 0;
3291
+ const maybe = this.channel;
3292
+ await withTimeout(
3293
+ maybe?.disconnect?.() ?? maybe?.close?.() ?? Promise.resolve(),
3294
+ this.options.disconnectTimeoutMs ?? 5e3,
3295
+ "Lark channel disconnect timed out"
3296
+ );
3297
+ this.channel = void 0;
3298
+ }
3299
+ async sendMessage(target, reply) {
3300
+ const channel = this.channel;
3301
+ if (!channel) throw new Error("Lark channel is not connected");
3302
+ if (target.commentTarget) {
3303
+ await this.postCommentReply(target.commentTarget, String(reply.content));
3304
+ return;
3305
+ }
3306
+ if (target.chatId && channel.send) {
3307
+ return channel.send(target.chatId, toChannelInput(reply), target.messageId ? { replyTo: target.messageId } : void 0);
3308
+ }
3309
+ if (target.messageId && channel.send) {
3310
+ throw new Error("chatId is required when replying through Lark channel");
3311
+ }
3312
+ if (!channel.send) {
3313
+ throw new Error("Lark channel does not expose a supported send method");
3314
+ }
3315
+ }
3316
+ async updateMessage(messageId, reply) {
3317
+ const channel = this.channel;
3318
+ if (reply.mode === "card") {
3319
+ if (!channel?.updateCard) throw new Error("Lark channel does not expose updateCard");
3320
+ await channel.updateCard(messageId, reply.content);
3321
+ return;
3322
+ }
3323
+ if (!channel?.editMessage) throw new Error("Lark channel does not expose editMessage");
3324
+ await channel.editMessage(messageId, String(reply.content));
3325
+ }
3326
+ async downloadResource(message, resource) {
3327
+ const channel = this.channel;
3328
+ const get = channel?.rawClient?.im?.v1?.messageResource?.get;
3329
+ if (!get) throw new Error("Lark channel does not expose messageResource.get");
3330
+ const messageId = resource.messageId ?? message.messageId;
3331
+ if (!messageId) throw new Error("resource message_id is missing");
3332
+ if (!resource.fileKey) throw new Error("resource file_key is missing");
3333
+ const result = await get({
3334
+ params: { type: resource.kind },
3335
+ path: { message_id: messageId, file_key: resource.fileKey }
3336
+ });
3337
+ return {
3338
+ bytes: await resourceResultToBuffer(result),
3339
+ name: resource.name,
3340
+ mime: contentTypeFromResult(result) ?? resource.mime
3341
+ };
3342
+ }
3343
+ async createGroupChat(input2) {
3344
+ const create = this.channel?.rawClient?.im?.v1?.chat?.create;
3345
+ if (!create) throw new Error("Lark channel does not expose im.v1.chat.create");
3346
+ const result = await create({
3347
+ data: {
3348
+ name: input2.name,
3349
+ description: input2.description,
3350
+ chat_mode: "group",
3351
+ chat_type: "private",
3352
+ user_id_list: [input2.inviteOpenId]
3353
+ },
3354
+ params: { user_id_type: "open_id" }
3355
+ });
3356
+ const data = record2(record2(result).data);
3357
+ const chatId = stringValue(data.chat_id) ?? stringValue(data.chatId);
3358
+ if (!chatId) throw new Error("chat.create returned no chat_id");
3359
+ return { chatId, name: input2.name };
3360
+ }
3361
+ async listJoinedChats() {
3362
+ const list = this.channel?.rawClient?.im?.v1?.chat?.list;
3363
+ if (!list) throw new Error("Lark channel does not expose im.v1.chat.list");
3364
+ const out = [];
3365
+ let pageToken;
3366
+ do {
3367
+ const result = await list({ params: { page_size: 100, ...pageToken ? { page_token: pageToken } : {} } });
3368
+ const data = record2(record2(result).data);
3369
+ const items = Array.isArray(data.items) ? data.items : [];
3370
+ for (const item of items) {
3371
+ const row = record2(item);
3372
+ const chatId = stringValue(row.chat_id) ?? stringValue(row.chatId);
3373
+ if (chatId) out.push({ chatId, name: stringValue(row.name) });
3374
+ }
3375
+ pageToken = stringValue(data.page_token) ?? stringValue(data.pageToken);
3376
+ } while (pageToken);
3377
+ return out;
3378
+ }
3379
+ async normalizeComment(event) {
3380
+ const raw = record2(event);
3381
+ const fileToken = stringValue(raw.fileToken) ?? stringValue(raw.file_token);
3382
+ const fileType = stringValue(raw.fileType) ?? stringValue(raw.file_type);
3383
+ const commentId = stringValue(raw.commentId) ?? stringValue(raw.comment_id);
3384
+ if (!fileToken || !fileType || !commentId || !isSupportedCommentFileType(fileType)) return void 0;
3385
+ const mentioned = Boolean(raw.mentionedBot ?? raw.mentioned_bot);
3386
+ if (!mentioned) return void 0;
3387
+ const replyId = stringValue(raw.replyId) ?? stringValue(raw.reply_id);
3388
+ const operator = record2(raw.operator);
3389
+ const actorId = stringValue(operator.openId) ?? stringValue(operator.open_id) ?? stringValue(raw.operatorOpenId) ?? stringValue(raw.operator_open_id) ?? "";
3390
+ const context = await this.fetchCommentContext({ fileToken, fileType, commentId, replyId }).catch(() => ({
3391
+ question: stringValue(raw.text) ?? stringValue(raw.commentText) ?? stringValue(raw.comment_text) ?? "",
3392
+ quote: void 0,
3393
+ isWhole: false
3394
+ }));
3395
+ const scopeId = scopeForChat({ fileToken, commentId });
3396
+ return {
3397
+ source: "comment",
3398
+ scopeId,
3399
+ commentScopeId: scopeId,
3400
+ commentTarget: { fileToken, fileType, commentId, replyId, isWhole: context.isWhole },
3401
+ messageId: `comment:${commentId}:${replyId ?? "root"}`,
3402
+ actorId,
3403
+ actorType: "user",
3404
+ chatType: "private",
3405
+ text: buildCommentPrompt({ fileToken, fileType, question: context.question, quote: context.quote, isWhole: context.isWhole }),
3406
+ mentions: [{ id: "bot", isBot: true }],
3407
+ resources: [],
3408
+ raw: event
3409
+ };
3410
+ }
3411
+ async fetchCommentContext(input2) {
3412
+ const rawClient = this.channel?.rawClient;
3413
+ const get = rawClient?.drive?.v1?.fileComment?.get;
3414
+ if (!get) return { question: "", isWhole: false };
3415
+ const result = await get({
3416
+ params: { file_type: input2.fileType },
3417
+ path: { file_token: input2.fileToken, comment_id: input2.commentId }
3418
+ });
3419
+ const data = record2(record2(result).data);
3420
+ const replies = Array.isArray(record2(record2(data.reply_list).replies).items) ? record2(record2(data.reply_list).replies).items : Array.isArray(record2(data.reply_list).replies) ? record2(data.reply_list).replies : [];
3421
+ return {
3422
+ question: extractCommentQuestion(replies, input2.replyId),
3423
+ quote: stringValue(data.quote),
3424
+ isWhole: Boolean(data.is_whole)
3425
+ };
3426
+ }
3427
+ async postCommentReply(target, text2) {
3428
+ const rawClient = this.channel?.rawClient;
3429
+ if (!rawClient) throw new Error("Lark channel does not expose rawClient");
3430
+ if (!target.isWhole && rawClient.request) {
3431
+ const url = `/open-apis/drive/v1/files/${encodeURIComponent(target.fileToken)}/comments/${encodeURIComponent(target.commentId)}/replies?file_type=${encodeURIComponent(target.fileType)}`;
3432
+ try {
3433
+ await rawClient.request({ method: "POST", url, data: commentContent(text2) });
3434
+ return;
3435
+ } catch (error) {
3436
+ if (responseCode(error) !== 1069302) throw error;
3437
+ }
3438
+ }
3439
+ const create = rawClient.drive?.v1?.fileComment?.create;
3440
+ if (!create) throw new Error("Lark channel does not expose fileComment.create");
3441
+ await create({
3442
+ params: { file_type: target.fileType },
3443
+ path: { file_token: target.fileToken },
3444
+ data: {
3445
+ reply_list: {
3446
+ replies: [{ content: commentContent(text2).content }]
3447
+ }
3448
+ }
3449
+ });
3450
+ }
3451
+ startPollingFallback() {
3452
+ const tick = () => {
3453
+ void this.pollOnce().catch((error) => {
3454
+ console.error("[bridge:poll] failed:", error instanceof Error ? error.stack ?? error.message : String(error));
3455
+ });
3456
+ };
3457
+ console.log("[bridge:poll] enabled chats=" + this.options.pollChats?.join(","));
3458
+ tick();
3459
+ this.pollTimer = setInterval(tick, this.options.pollIntervalMs ?? 3e3);
3460
+ }
3461
+ async pollOnce() {
3462
+ const token = await this.tenantAccessToken();
3463
+ const end = Math.floor(Date.now() / 1e3) + 60;
3464
+ for (const chatId of this.options.pollChats ?? []) {
3465
+ const url = new URL("https://open.feishu.cn/open-apis/im/v1/messages");
3466
+ url.searchParams.set("container_id_type", "chat");
3467
+ url.searchParams.set("container_id", chatId);
3468
+ url.searchParams.set("start_time", String(this.pollStartedAt));
3469
+ url.searchParams.set("end_time", String(end));
3470
+ url.searchParams.set("page_size", "50");
3471
+ const resp = await fetch(url, { headers: { authorization: `Bearer ${token}` } });
3472
+ const json = await resp.json();
3473
+ if (json.code !== 0) {
3474
+ console.warn(`[bridge:poll] message list failed chat=${chatId} code=${json.code} msg=${json.msg ?? ""}`);
3475
+ continue;
3476
+ }
3477
+ const items = [...json.data?.items ?? []].sort((a, b) => Number(record2(a).create_time ?? 0) - Number(record2(b).create_time ?? 0));
3478
+ for (const item of items) {
3479
+ const raw = record2(item);
3480
+ const messageId = stringValue(raw.message_id);
3481
+ if (!messageId || this.pollSeen.has(messageId)) continue;
3482
+ this.pollSeen.add(messageId);
3483
+ const sender = record2(raw.sender);
3484
+ if (sender.sender_type === "app" || sender.id === this.options.appId) continue;
3485
+ const normalized = normalizeMessageListItem(raw, chatId);
3486
+ console.log(`[bridge:poll] message chat=${chatId} actor=${normalized.actorId} text=${normalized.text.slice(0, 40)}`);
3487
+ await this.messageHandler?.(normalized);
3488
+ }
3489
+ }
3490
+ }
3491
+ async tenantAccessToken() {
3492
+ const resp = await fetch("https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal", {
3493
+ method: "POST",
3494
+ headers: { "content-type": "application/json" },
3495
+ body: JSON.stringify({ app_id: this.options.appId, app_secret: this.options.appSecret })
3496
+ });
3497
+ const json = await resp.json();
3498
+ if (!json.tenant_access_token) throw new Error(`tenant token failed code=${json.code} msg=${json.msg ?? ""}`);
3499
+ return json.tenant_access_token;
3500
+ }
3501
+ };
3502
+ function toChannelInput(reply) {
3503
+ if (reply.mode === "card") return { card: reply.content };
3504
+ if (reply.mode === "markdown") return { markdown: String(reply.content) };
3505
+ return { text: String(reply.content) };
3506
+ }
3507
+ function normalizeMessage(event) {
3508
+ const raw = record2(event);
3509
+ const chatId = stringValue(raw.chatId) ?? stringValue(raw.chat_id);
3510
+ const threadId = stringValue(raw.threadId) ?? stringValue(raw.thread_id) ?? stringValue(raw.rootId) ?? stringValue(raw.root_id);
3511
+ const chatType = raw.chatType === "group" || raw.chat_type === "group" ? "group" : threadId ? "topic" : raw.chatType === "topic" ? "topic" : "private";
3512
+ const messageId = stringValue(raw.messageId) ?? stringValue(raw.message_id) ?? "";
3513
+ return {
3514
+ source: "im",
3515
+ scopeId: scopeForChat({ chatId, threadId, chatType }),
3516
+ chatId,
3517
+ threadId,
3518
+ messageId,
3519
+ actorId: stringValue(raw.senderId) ?? stringValue(raw.sender_id) ?? stringValue(record2(raw.sender).openId) ?? stringValue(record2(raw.sender).open_id) ?? "",
3520
+ actorName: stringValue(raw.senderName) ?? stringValue(record2(raw.sender).name),
3521
+ actorType: raw.senderType === "bot" || raw.sender_type === "bot" ? "bot" : "user",
3522
+ chatType,
3523
+ text: stringValue(raw.text) ?? stringValue(raw.content) ?? "",
3524
+ mentions: Array.isArray(raw.mentions) ? raw.mentions.map((mention) => {
3525
+ const item = record2(mention);
3526
+ return {
3527
+ id: stringValue(item.id) ?? stringValue(item.openId) ?? stringValue(item.open_id) ?? "",
3528
+ name: stringValue(item.name),
3529
+ isBot: Boolean(item.isBot ?? item.is_bot),
3530
+ isAll: Boolean(item.isAll ?? item.is_all)
3531
+ };
3532
+ }) : [],
3533
+ resources: normalizeResources(raw, messageId),
3534
+ raw: event
3535
+ };
3536
+ }
3537
+ function normalizeAction(event) {
3538
+ const raw = record2(event);
3539
+ const value = record2(raw.value ?? record2(raw.action).value);
3540
+ const chatId = stringValue(value.chatId) ?? stringValue(value.chat_id) ?? stringValue(raw.chatId) ?? stringValue(raw.chat_id) ?? "";
3541
+ return {
3542
+ source: "card",
3543
+ token: stringValue(value.token) ?? stringValue(raw.token) ?? "",
3544
+ value,
3545
+ scopeId: stringValue(value.scopeId) ?? stringValue(value.scope_id) ?? stringValue(raw.scopeId) ?? stringValue(raw.scope_id) ?? (chatId ? scopeForChat({ chatId }) : ""),
3546
+ chatId,
3547
+ messageId: stringValue(raw.messageId) ?? stringValue(raw.message_id) ?? "",
3548
+ operatorOpenId: stringValue(raw.operatorOpenId) ?? stringValue(raw.operator_open_id) ?? stringValue(record2(raw.operator).openId) ?? "",
3549
+ raw: event,
3550
+ formValues: record2(raw.formValues ?? raw.form_values)
3551
+ };
3552
+ }
3553
+ function normalizeMessageListItem(raw, chatId) {
3554
+ const sender = record2(raw.sender);
3555
+ const body = record2(raw.body);
3556
+ const content = parseContent(stringValue(body.content) ?? "");
3557
+ const messageId = stringValue(raw.message_id) ?? "";
3558
+ return {
3559
+ source: "im",
3560
+ scopeId: scopeForChat({ chatId }),
3561
+ chatId,
3562
+ messageId,
3563
+ actorId: stringValue(sender.id) ?? "",
3564
+ actorType: sender.sender_type === "app" ? "bot" : "user",
3565
+ chatType: "private",
3566
+ text: stringValue(content.text) ?? stringValue(body.content) ?? "",
3567
+ mentions: [],
3568
+ resources: normalizeResources({ ...raw, content, resources: raw.resources }, messageId),
3569
+ raw
3570
+ };
3571
+ }
3572
+ function normalizeResources(raw, messageId) {
3573
+ const source = Array.isArray(raw.resources) ? raw.resources : Array.isArray(record2(raw.message).resources) ? record2(raw.message).resources : [];
3574
+ const out = source.map((entry) => normalizeResource(record2(entry), messageId)).filter((entry) => Boolean(entry));
3575
+ if (out.length) return out;
3576
+ const content = record2(raw.content);
3577
+ const messageType = stringValue(raw.messageType) ?? stringValue(raw.message_type) ?? stringValue(raw.type);
3578
+ const kind = normalizeResourceKind(messageType);
3579
+ const fileKey = stringValue(content.file_key) ?? stringValue(content.fileKey) ?? stringValue(content.image_key) ?? stringValue(content.imageKey) ?? stringValue(content.file_id);
3580
+ if (!kind || !fileKey) return [];
3581
+ return [
3582
+ {
3583
+ kind,
3584
+ fileKey,
3585
+ messageId,
3586
+ name: stringValue(content.file_name) ?? stringValue(content.fileName),
3587
+ mime: stringValue(content.mime) ?? stringValue(content.mime_type),
3588
+ size: numberValue(content.size),
3589
+ required: true
3590
+ }
3591
+ ];
3592
+ }
3593
+ function normalizeResource(value, messageId) {
3594
+ const kind = normalizeResourceKind(stringValue(value.type) ?? stringValue(value.kind));
3595
+ if (!kind) return void 0;
3596
+ const fileKey = stringValue(value.fileKey) ?? stringValue(value.file_key) ?? stringValue(value.imageKey) ?? stringValue(value.image_key);
3597
+ if (!fileKey) return void 0;
3598
+ return {
3599
+ kind,
3600
+ fileKey,
3601
+ messageId,
3602
+ name: stringValue(value.fileName) ?? stringValue(value.file_name) ?? stringValue(value.name),
3603
+ mime: stringValue(value.mime) ?? stringValue(value.mime_type),
3604
+ size: numberValue(value.size),
3605
+ required: true
3606
+ };
3607
+ }
3608
+ function normalizeResourceKind(value) {
3609
+ if (value === "image" || value === "file" || value === "audio" || value === "video" || value === "sticker") return value;
3610
+ return void 0;
3611
+ }
3612
+ function isSupportedCommentFileType(value) {
3613
+ return value === "doc" || value === "docx" || value === "sheet" || value === "file";
3614
+ }
3615
+ function buildCommentPrompt(input2) {
3616
+ return [
3617
+ "I was mentioned in a Feishu/Lark cloud document comment.",
3618
+ `Document: https://feishu.cn/${input2.fileType}/${input2.fileToken}`,
3619
+ `File token: ${input2.fileToken}`,
3620
+ `File type: ${input2.fileType}`,
3621
+ `Comment scope: ${input2.isWhole ? "whole document" : "inline comment"}`,
3622
+ input2.quote ? `Quoted text:
3623
+ ${input2.quote}` : void 0,
3624
+ "",
3625
+ `User question:
3626
+ ${input2.question}`,
3627
+ "",
3628
+ "The bridge owns the comment reply API. Do not call document comment APIs. Return plain text only; avoid markdown, code fences, tool logs, and internal reasoning."
3629
+ ].filter((line) => line !== void 0).join("\n");
3630
+ }
3631
+ function extractCommentQuestion(replies, replyId) {
3632
+ const records = replies.map((reply) => record2(reply));
3633
+ const target = (replyId ? records.find((reply) => stringValue(reply.reply_id) === replyId) : void 0) ?? records.at(-1);
3634
+ if (!target) return "";
3635
+ const elements = record2(record2(target.content).elements);
3636
+ const array = Array.isArray(record2(target.content).elements) ? record2(target.content).elements : Array.isArray(elements.items) ? elements.items : [];
3637
+ return array.map((entry) => {
3638
+ const item = record2(entry);
3639
+ if (item.type === "text_run") return stringValue(record2(item.text_run).text) ?? "";
3640
+ if (item.type === "docs_link") return stringValue(record2(item.docs_link).url) ?? "";
3641
+ return "";
3642
+ }).join("").trim();
3643
+ }
3644
+ function commentContent(text2) {
3645
+ return { content: { elements: [{ type: "text_run", text_run: { text: text2 } }] } };
3646
+ }
3647
+ function responseCode(error) {
3648
+ return numberValue(record2(record2(record2(error).response).data).code);
3649
+ }
3650
+ function parseContent(value) {
3651
+ try {
3652
+ return JSON.parse(value);
3653
+ } catch {
3654
+ return {};
3655
+ }
3656
+ }
3657
+ async function resourceResultToBuffer(result) {
3658
+ if (Buffer.isBuffer(result)) return result;
3659
+ if (result instanceof ArrayBuffer) return Buffer.from(result);
3660
+ if (ArrayBuffer.isView(result)) return Buffer.from(result.buffer, result.byteOffset, result.byteLength);
3661
+ const raw = record2(result);
3662
+ if (typeof raw.writeFile === "function") {
3663
+ const dir = await mkdtemp(join9(tmpdir2(), "feishu-codex-resource-"));
3664
+ const path = join9(dir, "resource");
3665
+ try {
3666
+ await raw.writeFile(path);
3667
+ return await readFile8(path);
3668
+ } finally {
3669
+ await rm4(dir, { recursive: true, force: true });
3670
+ }
3671
+ }
3672
+ const data = raw.data;
3673
+ if (Buffer.isBuffer(data)) return data;
3674
+ if (data instanceof ArrayBuffer) return Buffer.from(data);
3675
+ if (ArrayBuffer.isView(data)) return Buffer.from(data.buffer, data.byteOffset, data.byteLength);
3676
+ if (typeof data === "string") return Buffer.from(data);
3677
+ const body = raw.body;
3678
+ if (body && typeof body === "object" && Symbol.asyncIterator in body) {
3679
+ const chunks = [];
3680
+ for await (const chunk of body) {
3681
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
3682
+ }
3683
+ return Buffer.concat(chunks);
3684
+ }
3685
+ throw new Error("unsupported resource download result");
3686
+ }
3687
+ function contentTypeFromResult(result) {
3688
+ const headers = record2(record2(result).headers);
3689
+ const value = stringValue(headers["content-type"]) ?? stringValue(headers["Content-Type"]);
3690
+ return value?.split(";")[0]?.trim().toLowerCase();
3691
+ }
3692
+ function record2(value) {
3693
+ return value && typeof value === "object" ? value : {};
3694
+ }
3695
+ function stringValue(value) {
3696
+ return typeof value === "string" && value.length > 0 ? value : void 0;
3697
+ }
3698
+ function numberValue(value) {
3699
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
3700
+ }
3701
+ function withTimeout(promise, timeoutMs, message) {
3702
+ let timer;
3703
+ const timeout = new Promise((_, reject3) => {
3704
+ timer = setTimeout(() => reject3(new Error(message)), timeoutMs);
3705
+ });
3706
+ return Promise.race([promise, timeout]).finally(() => {
3707
+ if (timer) clearTimeout(timer);
3708
+ });
3709
+ }
3710
+
3711
+ // src/runtime/app.ts
3712
+ async function runBridge(options = {}) {
3713
+ const store = new ProfileStore(options.home);
3714
+ const profileName = await ensureProfileForRun({
3715
+ profile: options.profile,
3716
+ tenant: options.tenant,
3717
+ appId: options.appId,
3718
+ appSecret: options.appSecret,
3719
+ workspace: options.workspace,
3720
+ codexAuth: options.codexAuth,
3721
+ codexApiKey: options.codexApiKey,
3722
+ codexPath: options.codexPath,
3723
+ home: options.home
3724
+ });
3725
+ const profile = await store.read(profileName);
3726
+ if (options.workspace) {
3727
+ profile.workspaces.default = options.workspace;
3728
+ await store.write(profile);
3729
+ }
3730
+ const paths = store.profilePaths(profile.name);
3731
+ const secrets = new SecretResolver({ keystore: store.keystore(profile.name) });
3732
+ const appSecret = await secrets.resolve(profile.app.secret);
3733
+ const runner = runnerForProfile(profile);
3734
+ const availability = await runner.checkAvailability();
3735
+ if (!availability.ok) throw new Error(`Codex runner unavailable: ${availability.detail ?? availability.label}`);
3736
+ if (options.preflight) {
3737
+ console.log(`profile=${profile.name} backend=${availability.label} workspace=${profile.workspaces.default ?? "(not set)"}`);
3738
+ return;
3739
+ }
3740
+ const pLock = profileLock(profile.name, store.paths.home);
3741
+ const aLock = appLock(profile.app.id, store.paths.home);
3742
+ await pLock.acquire();
3743
+ await aLock.acquire();
3744
+ const registry = new RuntimeRegistry(store.paths.home);
3745
+ const self = await registry.register({
3746
+ profile: profile.name,
3747
+ tenant: profile.tenant,
3748
+ appId: profile.app.id,
3749
+ logPath: profileLogPath(paths, "run")
3750
+ });
3751
+ const gateway = new LarkChannelGateway({
3752
+ tenant: profile.tenant,
3753
+ appId: profile.app.id,
3754
+ appSecret,
3755
+ pollChats: options.pollChat
3756
+ });
3757
+ bindBridgeRuntime({
3758
+ profile,
3759
+ paths,
3760
+ gateway,
3761
+ runner,
3762
+ secretResolver: secrets,
3763
+ appSecret,
3764
+ ownerOpenId: process.env.FEISHU_CODEX_OWNER_OPEN_ID,
3765
+ botOpenId: process.env.FEISHU_CODEX_BOT_OPEN_ID,
3766
+ bootstrapOwner: options.bootstrapOwner,
3767
+ saveProfile: (next) => store.write(next),
3768
+ processSnapshot: () => registry.list(),
3769
+ lifecycle: {
3770
+ exit: async (target) => {
3771
+ if (!target || target === self.id || target === String(self.pid)) {
3772
+ setTimeout(() => process.kill(process.pid, "SIGTERM"), 50);
3773
+ return;
3774
+ }
3775
+ const killed = await registry.kill(target);
3776
+ if (!killed) throw new Error(`process not found: ${target}`);
3777
+ },
3778
+ reconnect: async () => {
3779
+ await gateway.disconnect().catch(() => void 0);
3780
+ await gateway.connect();
3781
+ }
3782
+ }
3783
+ });
3784
+ let keepalive;
3785
+ try {
3786
+ await gateway.connect();
3787
+ console.log(`feishu-codex connector running for profile ${profile.name}`);
3788
+ keepalive = setInterval(() => void 0, 6e4);
3789
+ await Promise.race([once(process, "SIGINT"), once(process, "SIGTERM")]);
3790
+ } finally {
3791
+ if (keepalive) clearInterval(keepalive);
3792
+ await gateway.disconnect().catch(() => void 0);
3793
+ await registry.unregister(process.pid).catch(() => void 0);
3794
+ await aLock.release().catch(() => void 0);
3795
+ await pLock.release().catch(() => void 0);
3796
+ }
3797
+ }
3798
+ function runnerForProfile(profile) {
3799
+ if (profile.codex.backend === "exec-json") {
3800
+ return new ExecJsonRunner(profile.codex.codexPath ?? "codex", ["exec", "--json"]);
3801
+ }
3802
+ return new CodexSdkRunner();
3803
+ }
3804
+
3805
+ export {
3806
+ DEFAULT_HOME_DIR,
3807
+ expandTilde,
3808
+ getBridgeHome,
3809
+ bridgePaths,
3810
+ profilePaths,
3811
+ parentDir,
3812
+ tempRoot,
3813
+ LocalKeystore,
3814
+ createDefaultProfileConfig,
3815
+ normalizeProfileConfig,
3816
+ isSecretRef,
3817
+ redactProfileConfig,
3818
+ ProfileStore,
3819
+ profileExists,
3820
+ profileNameFromPath,
3821
+ profileLogPath,
3822
+ SecretResolver,
3823
+ scopeForChat,
3824
+ messageRequiresMention,
3825
+ hasRealBotMention,
3826
+ hashId,
3827
+ RuntimeLock,
3828
+ profileLock,
3829
+ appLock,
3830
+ RuntimeRegistry,
3831
+ isAlive,
3832
+ SdkEventTranslator,
3833
+ CodexSdkRunner,
3834
+ codexBinaryLooksAvailable,
3835
+ ExecJsonRunner,
3836
+ CallbackAuth,
3837
+ MemoryCallbackNonceStore,
3838
+ buildRunCard,
3839
+ renderMarkdownFallback,
3840
+ redactSecrets,
3841
+ createRunState,
3842
+ applyRunEvent,
3843
+ runDocumentComment,
3844
+ stripMarkdown,
3845
+ MediaCache,
3846
+ hashFile,
3847
+ buildPrompt,
3848
+ BRIDGE_INSTRUCTIONS,
3849
+ evaluateAccess,
3850
+ canRunAdminCommand,
3851
+ SessionCatalog,
3852
+ catalogKey,
3853
+ SessionStore,
3854
+ WorkspaceStore,
3855
+ ProcessPool,
3856
+ projectLarkCliIdentity,
3857
+ policyFingerprint,
3858
+ accessPolicyDigest,
3859
+ stableJson,
3860
+ clampAccess,
3861
+ accessToSandbox,
3862
+ decideAccess,
3863
+ evaluateRunPolicy,
3864
+ resolveWorkspace,
3865
+ isBroadWorkspace,
3866
+ startRun,
3867
+ RunOrchestrator,
3868
+ routeCommand,
3869
+ parseCommand,
3870
+ bindBridgeRuntime,
3871
+ runRegistrationWizard,
3872
+ ensureProfileForRun,
3873
+ bootstrapProfile,
3874
+ runBridge,
3875
+ runnerForProfile
3876
+ };