im-code-agent 0.0.0-alpha.1

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,3452 @@
1
+ import { access, copyFile, mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
2
+ import { spawn, spawnSync } from "node:child_process";
3
+ import { randomUUID } from "node:crypto";
4
+ import { constants } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import { dirname, isAbsolute, resolve } from "node:path";
7
+ import { createInterface } from "node:readline/promises";
8
+ import { fileURLToPath } from "node:url";
9
+ import lark from "@larksuiteoapi/node-sdk";
10
+ //#region src/agent/agent-adapter.ts
11
+ function resolveAgentCommand(config, agentType) {
12
+ const command = config.agents[agentType];
13
+ if (!command) throw new Error(`Agent not configured: ${agentType}`);
14
+ return command;
15
+ }
16
+ //#endregion
17
+ //#region src/agent/agent-process.ts
18
+ const ACP_TOOL_CALL_STANDARD_FIELDS = [
19
+ "toolCallId",
20
+ "kind",
21
+ "status",
22
+ "title",
23
+ "content",
24
+ "content[].type",
25
+ "locations",
26
+ "locations[].path",
27
+ "locations[].line",
28
+ "rawInput",
29
+ "rawOutput",
30
+ "_meta"
31
+ ];
32
+ const ACP_REQUEST_PERMISSION_STANDARD_FIELDS = [
33
+ "sessionId",
34
+ "toolCall",
35
+ "toolCall.toolCallId",
36
+ "toolCall.kind",
37
+ "toolCall.status",
38
+ "toolCall.title",
39
+ "toolCall.content",
40
+ "toolCall.locations",
41
+ "toolCall.rawInput",
42
+ "toolCall.rawOutput",
43
+ "toolCall._meta",
44
+ "options",
45
+ "options[].optionId",
46
+ "options[].name",
47
+ "options[].kind",
48
+ "options[]._meta",
49
+ "_meta"
50
+ ];
51
+ function collectFieldPaths(value, basePath = "") {
52
+ const paths = /* @__PURE__ */ new Set();
53
+ const walk = (node, path) => {
54
+ if (node === null || node === void 0) return;
55
+ if (Array.isArray(node)) {
56
+ if (path) paths.add(`${path}[]`);
57
+ for (const item of node) walk(item, path ? `${path}[]` : "[]");
58
+ return;
59
+ }
60
+ if (typeof node !== "object") return;
61
+ for (const [key, child] of Object.entries(node)) {
62
+ const nextPath = path ? `${path}.${key}` : key;
63
+ paths.add(nextPath);
64
+ walk(child, nextPath);
65
+ }
66
+ };
67
+ walk(value, basePath);
68
+ return Array.from(paths).sort();
69
+ }
70
+ function summarizePermissionRequest(params, rawParams) {
71
+ return {
72
+ topLevelKeys: rawParams && typeof rawParams === "object" ? Object.keys(rawParams) : [],
73
+ fieldPaths: collectFieldPaths(rawParams),
74
+ standardFields: ACP_REQUEST_PERMISSION_STANDARD_FIELDS,
75
+ sessionId: params.sessionId,
76
+ optionsCount: params.options.length,
77
+ options: params.options.map((item) => ({
78
+ optionId: item.id,
79
+ name: item.name,
80
+ kind: item.kind
81
+ }))
82
+ };
83
+ }
84
+ function normalizePermissionParams(raw) {
85
+ if (!raw || typeof raw !== "object") return {};
86
+ const obj = raw;
87
+ const sessionId = typeof obj.sessionId === "string" ? obj.sessionId : void 0;
88
+ const toolCall = obj.toolCall;
89
+ const options = ((Array.isArray(obj.options) ? obj.options : void 0) ?? []).map((item) => {
90
+ if (!item || typeof item !== "object") return;
91
+ const opt = item;
92
+ const optionId = typeof opt.optionId === "string" ? opt.optionId : void 0;
93
+ const name = typeof opt.name === "string" ? opt.name : void 0;
94
+ const kind = typeof opt.kind === "string" ? opt.kind : void 0;
95
+ if (!optionId || !name || !kind) return;
96
+ return {
97
+ id: optionId,
98
+ name,
99
+ kind
100
+ };
101
+ }).filter((item) => Boolean(item));
102
+ if (!sessionId || !toolCall) return {};
103
+ return { params: {
104
+ sessionId,
105
+ toolCall,
106
+ options
107
+ } };
108
+ }
109
+ var AgentProcessError = class extends Error {
110
+ constructor(code, message) {
111
+ super(message);
112
+ this.code = code;
113
+ this.name = "AgentProcessError";
114
+ }
115
+ };
116
+ var AgentProcess = class {
117
+ #children = /* @__PURE__ */ new Map();
118
+ #defaultRequestTimeoutMs = 3e4;
119
+ #promptIdleTimeoutMs = 12e4;
120
+ #toolUpdateLogDedup = /* @__PURE__ */ new Set();
121
+ constructor(config, logger, onEvent, onPermissionRequest) {
122
+ this.config = config;
123
+ this.logger = logger;
124
+ this.onEvent = onEvent;
125
+ this.onPermissionRequest = onPermissionRequest;
126
+ }
127
+ async checkHealth(agent) {
128
+ const command = resolveAgentCommand(this.config, agent);
129
+ const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
130
+ const notes = [];
131
+ const commandAvailable = await this.isCommandAvailable(command.command);
132
+ if (!commandAvailable) notes.push(`Command is not accessible: ${command.command}`);
133
+ let cliVersion;
134
+ if (agent === "codex") {
135
+ const result = spawnSync("codex", ["--version"], { encoding: "utf8" });
136
+ if (result.status === 0) cliVersion = result.stdout.trim();
137
+ else notes.push("codex CLI is installed but `codex --version` did not exit cleanly");
138
+ }
139
+ const hasStoredAuth = spawnSync("test", ["-f", `${process.env.HOME}/.codex/auth.json`], { stdio: "ignore" }).status === 0;
140
+ if (!hasStoredAuth) notes.push("No ~/.codex/auth.json detected");
141
+ let status = "ready";
142
+ if (!commandAvailable) status = "unavailable";
143
+ else if (!hasStoredAuth) status = "degraded";
144
+ return {
145
+ agent,
146
+ status,
147
+ commandAvailable,
148
+ cliVersion,
149
+ hasStoredAuth,
150
+ notes,
151
+ checkedAt
152
+ };
153
+ }
154
+ async start(options) {
155
+ if (this.#children.has(options.taskId)) throw new Error(`Task is already running: ${options.taskId}`);
156
+ const command = resolveAgentCommand(this.config, options.agent);
157
+ const spawnArgs = [...command.args ?? [], ...options.runtimeArgs ?? []];
158
+ const health = await this.checkHealth(options.agent);
159
+ if (!health.commandAvailable) throw new AgentProcessError("agent_command_unavailable", health.notes.join("; "));
160
+ const child = spawn(command.command, spawnArgs, {
161
+ cwd: options.cwd,
162
+ env: {
163
+ ...process.env,
164
+ ...command.env
165
+ },
166
+ stdio: "pipe"
167
+ });
168
+ const agentChild = {
169
+ child,
170
+ nextRequestId: 1,
171
+ pendingRequests: /* @__PURE__ */ new Map(),
172
+ stdoutBuffer: "",
173
+ stderrChunks: []
174
+ };
175
+ this.#children.set(options.taskId, agentChild);
176
+ child.stdout.on("data", (chunk) => {
177
+ this.handleStdout(options.taskId, chunk.toString());
178
+ });
179
+ child.stderr.on("data", (chunk) => {
180
+ agentChild.stderrChunks.push(chunk.toString());
181
+ this.logger.warn("agent stderr", {
182
+ taskId: options.taskId,
183
+ chunk: chunk.toString()
184
+ });
185
+ });
186
+ child.on("spawn", () => {
187
+ this.logger.info("agent process spawned", {
188
+ taskId: options.taskId,
189
+ agent: options.agent,
190
+ command: command.command,
191
+ args: spawnArgs,
192
+ cwd: options.cwd
193
+ });
194
+ });
195
+ child.on("error", (error) => {
196
+ this.rejectAllPendingRequests(options.taskId, `agent process spawn failed: ${error.message}`);
197
+ this.#children.delete(options.taskId);
198
+ this.clearToolUpdateLogCache(options.taskId);
199
+ this.logger.error("agent process error", {
200
+ taskId: options.taskId,
201
+ error: error.message
202
+ });
203
+ });
204
+ child.on("exit", (code, signal) => {
205
+ this.rejectAllPendingRequests(options.taskId, "agent process exited");
206
+ this.#children.delete(options.taskId);
207
+ this.clearToolUpdateLogCache(options.taskId);
208
+ this.logger.info("agent process exited", {
209
+ taskId: options.taskId,
210
+ code,
211
+ signal
212
+ });
213
+ });
214
+ const initialization = await this.initialize(options.taskId);
215
+ agentChild.initialization = initialization;
216
+ let sessionId;
217
+ let loadResult;
218
+ if (options.resumeSessionId) try {
219
+ loadResult = await this.loadSession(options.taskId, options.resumeSessionId, options.cwd);
220
+ sessionId = options.resumeSessionId;
221
+ this.logger.info("agent session resumed", {
222
+ taskId: options.taskId,
223
+ sessionId,
224
+ cwd: options.cwd
225
+ });
226
+ } catch (error) {
227
+ this.logger.warn("agent session resume failed, fallback to new session", {
228
+ taskId: options.taskId,
229
+ resumeSessionId: options.resumeSessionId,
230
+ error: error instanceof Error ? error.message : String(error)
231
+ });
232
+ const session = await this.newSession(options.taskId, options.cwd);
233
+ loadResult = session;
234
+ sessionId = session.sessionId;
235
+ }
236
+ else {
237
+ const session = await this.newSession(options.taskId, options.cwd);
238
+ loadResult = session;
239
+ sessionId = session.sessionId;
240
+ }
241
+ agentChild.sessionId = sessionId;
242
+ this.logger.info("agent initialized", {
243
+ taskId: options.taskId,
244
+ protocolVersion: initialization.protocolVersion,
245
+ agentName: initialization.agentInfo?.name,
246
+ agentVersion: initialization.agentInfo?.version,
247
+ sessionId
248
+ });
249
+ return {
250
+ initialization,
251
+ sessionId,
252
+ models: loadResult.models
253
+ };
254
+ }
255
+ async isCommandAvailable(command) {
256
+ if (command.includes("/")) try {
257
+ await access(command);
258
+ return true;
259
+ } catch {
260
+ return false;
261
+ }
262
+ return spawnSync("which", [command], { stdio: "ignore" }).status === 0;
263
+ }
264
+ async stop(taskId) {
265
+ const agentChild = this.#children.get(taskId);
266
+ if (!agentChild) return;
267
+ this.rejectAllPendingRequests(taskId, "agent process stopped");
268
+ agentChild.child.kill("SIGTERM");
269
+ this.#children.delete(taskId);
270
+ this.clearToolUpdateLogCache(taskId);
271
+ }
272
+ async initialize(taskId) {
273
+ return this.sendRequest(taskId, "initialize", {
274
+ protocolVersion: 1,
275
+ clientCapabilities: {
276
+ fs: {
277
+ readTextFile: false,
278
+ writeTextFile: false
279
+ },
280
+ terminal: false
281
+ }
282
+ });
283
+ }
284
+ async prompt(taskId, promptText) {
285
+ const agentChild = this.getChild(taskId);
286
+ if (!agentChild.sessionId) throw new Error(`Agent session not initialized for task: ${taskId}`);
287
+ const params = {
288
+ sessionId: agentChild.sessionId,
289
+ prompt: [{
290
+ type: "text",
291
+ text: promptText
292
+ }]
293
+ };
294
+ return this.sendRequest(taskId, "session/prompt", params);
295
+ }
296
+ async setSessionModel(taskId, modelId) {
297
+ const agentChild = this.getChild(taskId);
298
+ if (!agentChild.sessionId) throw new Error(`Agent session not initialized for task: ${taskId}`);
299
+ const params = {
300
+ sessionId: agentChild.sessionId,
301
+ modelId
302
+ };
303
+ await this.sendRequest(taskId, "session/set_model", params);
304
+ }
305
+ async newSession(taskId, cwd) {
306
+ const params = {
307
+ cwd,
308
+ mcpServers: []
309
+ };
310
+ try {
311
+ return await this.sendRequest(taskId, "session/new", params);
312
+ } catch (error) {
313
+ throw this.mapRequestError(taskId, "session/new", error);
314
+ }
315
+ }
316
+ async loadSession(taskId, sessionId, cwd) {
317
+ const params = {
318
+ sessionId,
319
+ cwd,
320
+ mcpServers: []
321
+ };
322
+ try {
323
+ return await this.sendRequest(taskId, "session/load", params);
324
+ } catch (error) {
325
+ throw this.mapRequestError(taskId, "session/load", error);
326
+ }
327
+ }
328
+ async sendRequest(taskId, method, params) {
329
+ const agentChild = this.getChild(taskId);
330
+ const id = agentChild.nextRequestId++;
331
+ const payload = {
332
+ jsonrpc: "2.0",
333
+ id,
334
+ method,
335
+ ...params === void 0 ? {} : { params }
336
+ };
337
+ return new Promise((resolve, reject) => {
338
+ const timeoutMs = method === "session/prompt" ? this.#promptIdleTimeoutMs : this.#defaultRequestTimeoutMs;
339
+ const timer = this.createRequestTimer(taskId, id, method, timeoutMs);
340
+ agentChild.pendingRequests.set(id, {
341
+ method,
342
+ timer,
343
+ timeoutMs,
344
+ resolve: (value) => resolve(value),
345
+ reject
346
+ });
347
+ this.writeMessage(taskId, payload);
348
+ });
349
+ }
350
+ writeMessage(taskId, message) {
351
+ this.getChild(taskId).child.stdin.write(`${JSON.stringify(message)}\n`);
352
+ this.logger.info("agent request sent", {
353
+ taskId,
354
+ method: message.method,
355
+ hasId: "id" in message
356
+ });
357
+ }
358
+ handleStdout(taskId, chunk) {
359
+ const agentChild = this.getChild(taskId);
360
+ agentChild.stdoutBuffer += chunk;
361
+ const lines = agentChild.stdoutBuffer.split("\n");
362
+ agentChild.stdoutBuffer = lines.pop() ?? "";
363
+ for (const rawLine of lines) {
364
+ const line = rawLine.trim();
365
+ if (!line) continue;
366
+ this.handleMessage(taskId, line).catch((error) => {
367
+ this.logger.warn("agent message handling failed", {
368
+ taskId,
369
+ error: error instanceof Error ? error.message : String(error)
370
+ });
371
+ });
372
+ }
373
+ }
374
+ async handleMessage(taskId, line) {
375
+ let message;
376
+ try {
377
+ message = JSON.parse(line);
378
+ } catch (error) {
379
+ this.logger.warn("agent emitted non-json stdout", {
380
+ taskId,
381
+ line,
382
+ error: error instanceof Error ? error.message : String(error)
383
+ });
384
+ return;
385
+ }
386
+ if ("id" in message && "result" in message) {
387
+ this.handleSuccess(taskId, message);
388
+ return;
389
+ }
390
+ if ("id" in message && "error" in message) {
391
+ this.handleFailure(taskId, message);
392
+ return;
393
+ }
394
+ if ("id" in message && "method" in message && !("result" in message) && !("error" in message)) {
395
+ await this.handleInboundRequest(taskId, message);
396
+ return;
397
+ }
398
+ if ("method" in message && message.method === "session/update") {
399
+ this.handleSessionUpdate(taskId, message);
400
+ return;
401
+ }
402
+ this.logger.info("agent notification received", {
403
+ taskId,
404
+ method: message.method
405
+ });
406
+ }
407
+ async handleInboundRequest(taskId, message) {
408
+ if (message.method !== "request_permission" && message.method !== "session/request_permission") {
409
+ this.logger.warn("agent inbound request is not supported", {
410
+ taskId,
411
+ method: message.method,
412
+ id: message.id
413
+ });
414
+ this.writeSuccess(taskId, message.id, { outcome: { outcome: "cancelled" } });
415
+ return;
416
+ }
417
+ if (!this.onPermissionRequest) {
418
+ this.logger.warn("permission request handler is not configured", {
419
+ taskId,
420
+ id: message.id
421
+ });
422
+ this.writeSuccess(taskId, message.id, { outcome: { outcome: "cancelled" } });
423
+ return;
424
+ }
425
+ const params = normalizePermissionParams(message.params).params;
426
+ if (!params) {
427
+ this.logger.warn("permission request params missing", {
428
+ taskId,
429
+ id: message.id,
430
+ rawFieldPaths: collectFieldPaths(message.params)
431
+ });
432
+ this.writeSuccess(taskId, message.id, { outcome: { outcome: "cancelled" } });
433
+ return;
434
+ }
435
+ this.logger.info("permission request payload", {
436
+ taskId,
437
+ id: message.id,
438
+ method: message.method,
439
+ permission: summarizePermissionRequest(params, message.params),
440
+ toolCallFieldPaths: collectFieldPaths(params.toolCall),
441
+ toolCallStandardFields: ACP_TOOL_CALL_STANDARD_FIELDS
442
+ });
443
+ let outcome;
444
+ try {
445
+ outcome = await this.onPermissionRequest({
446
+ taskId,
447
+ params,
448
+ emitApprovalRequested: (request) => {
449
+ this.onEvent?.({
450
+ type: "agent.approval_requested",
451
+ taskId,
452
+ request
453
+ });
454
+ }
455
+ });
456
+ } catch (error) {
457
+ this.logger.error("permission request handler failed", {
458
+ taskId,
459
+ id: message.id,
460
+ error: error instanceof Error ? error.message : String(error)
461
+ });
462
+ this.writeSuccess(taskId, message.id, { outcome: { outcome: "cancelled" } });
463
+ return;
464
+ }
465
+ if (outcome.decision) this.onEvent?.({
466
+ type: "agent.approval_resolved",
467
+ taskId,
468
+ decision: outcome.decision
469
+ });
470
+ if (outcome.status !== "approved") {
471
+ if (outcome.optionId) this.writeSuccess(taskId, message.id, { outcome: {
472
+ outcome: "selected",
473
+ optionId: outcome.optionId
474
+ } });
475
+ else this.writeSuccess(taskId, message.id, { outcome: { outcome: "cancelled" } });
476
+ return;
477
+ }
478
+ this.writeSuccess(taskId, message.id, { outcome: {
479
+ outcome: "selected",
480
+ optionId: outcome.optionId
481
+ } });
482
+ }
483
+ writeSuccess(taskId, id, result) {
484
+ const agentChild = this.#children.get(taskId);
485
+ if (!agentChild) {
486
+ this.logger.warn("agent response dropped: task is no longer running", {
487
+ taskId,
488
+ id
489
+ });
490
+ return;
491
+ }
492
+ const response = {
493
+ jsonrpc: "2.0",
494
+ id,
495
+ result
496
+ };
497
+ agentChild.child.stdin.write(`${JSON.stringify(response)}\n`);
498
+ this.logger.info("agent response sent", {
499
+ taskId,
500
+ id
501
+ });
502
+ }
503
+ handleSuccess(taskId, message) {
504
+ const agentChild = this.getChild(taskId);
505
+ const pending = agentChild.pendingRequests.get(message.id);
506
+ if (!pending) {
507
+ this.logger.warn("agent response without pending request", {
508
+ taskId,
509
+ id: message.id
510
+ });
511
+ return;
512
+ }
513
+ agentChild.pendingRequests.delete(message.id);
514
+ clearTimeout(pending.timer);
515
+ pending.resolve(message.result);
516
+ }
517
+ handleFailure(taskId, message) {
518
+ const agentChild = this.getChild(taskId);
519
+ if (message.id === null) {
520
+ this.logger.error("agent emitted rpc error without request id", {
521
+ taskId,
522
+ code: message.error.code,
523
+ message: message.error.message
524
+ });
525
+ return;
526
+ }
527
+ const pending = agentChild.pendingRequests.get(message.id);
528
+ if (!pending) {
529
+ this.logger.warn("agent error without pending request", {
530
+ taskId,
531
+ id: message.id,
532
+ code: message.error.code,
533
+ message: message.error.message
534
+ });
535
+ return;
536
+ }
537
+ agentChild.pendingRequests.delete(message.id);
538
+ clearTimeout(pending.timer);
539
+ pending.reject(new AgentProcessError("agent_protocol_error", `ACP request failed for ${pending.method}: ${message.error.message} (${message.error.code})`));
540
+ }
541
+ handleSessionUpdate(taskId, message) {
542
+ this.refreshPromptTimeout(taskId);
543
+ const update = message.params?.update;
544
+ if (update?.sessionUpdate === "agent_message_chunk" && update.content?.type === "text") this.onEvent?.({
545
+ type: "agent.output",
546
+ taskId,
547
+ text: update.content.text
548
+ });
549
+ const processText = this.formatProcessUpdate(update);
550
+ if (processText) this.onEvent?.({
551
+ type: "agent.output",
552
+ taskId,
553
+ text: processText
554
+ });
555
+ if (update?.sessionUpdate?.includes("tool")) {
556
+ const toolUpdate = this.toToolInvocation(update);
557
+ this.onEvent?.({
558
+ type: "agent.tool_update",
559
+ taskId,
560
+ update: toolUpdate
561
+ });
562
+ if (this.shouldLogToolUpdate(taskId, toolUpdate)) this.logger.info("agent tool update", {
563
+ taskId,
564
+ sessionId: message.params?.sessionId,
565
+ updateType: update.sessionUpdate,
566
+ tool: {
567
+ toolCallId: toolUpdate.toolCallId,
568
+ toolName: toolUpdate.toolName,
569
+ toolNameSource: toolUpdate.toolNameSource,
570
+ kind: toolUpdate.kind,
571
+ status: toolUpdate.status
572
+ },
573
+ fieldPaths: toolUpdate.fieldPaths,
574
+ standardFields: ACP_TOOL_CALL_STANDARD_FIELDS
575
+ });
576
+ }
577
+ if (update?.sessionUpdate && update.sessionUpdate !== "agent_message_chunk" && update.sessionUpdate !== "usage_update") this.logger.info("agent session update received", {
578
+ taskId,
579
+ sessionId: message.params?.sessionId,
580
+ updateType: update.sessionUpdate,
581
+ contentType: update?.content?.type
582
+ });
583
+ }
584
+ formatProcessUpdate(update) {
585
+ if (!update) return;
586
+ if (update.sessionUpdate === "agent_message_chunk") return;
587
+ if (update.sessionUpdate === "usage_update") return;
588
+ if (update.sessionUpdate === "available_commands_update") return;
589
+ if (update.sessionUpdate.includes("tool")) return this.formatToolUpdate(update);
590
+ const summary = this.summarizeUpdate(update);
591
+ return `\n[${update.sessionUpdate}]${summary ? ` ${summary}` : ""}\n`;
592
+ }
593
+ formatToolUpdate(update) {
594
+ const summary = this.toToolInvocation(update);
595
+ const toolName = summary.toolName ?? "工具调用";
596
+ const status = this.readStringField(update, [
597
+ "status",
598
+ "state",
599
+ "phase"
600
+ ]);
601
+ const command = summary.command;
602
+ const path = summary.path;
603
+ const error = this.readStringField(update, ["error", "message"]);
604
+ const query = summary.query;
605
+ const url = summary.url;
606
+ const lines = [`\n[tool] ${toolName}${status ? ` (${status})` : ""}`];
607
+ if (query) lines.push(`query: ${this.truncate(query, 160)}`);
608
+ if (url) lines.push(`url: ${this.truncate(url, 200)}`);
609
+ if (command) lines.push(`cmd: ${this.truncate(command, 160)}`);
610
+ if (path) lines.push(`path: ${this.truncate(path, 120)}`);
611
+ if (error) lines.push(`error: ${this.truncate(error, 160)}`);
612
+ lines.push("");
613
+ return lines.join("\n");
614
+ }
615
+ toToolInvocation(update) {
616
+ const command = this.readStringField(update, [
617
+ "command",
618
+ "cmd",
619
+ "shellCommand"
620
+ ]) ?? this.findStringByKeyPattern(update, /(cmd|command|shell)/i);
621
+ const path = this.readStringField(update, [
622
+ "path",
623
+ "cwd",
624
+ "targetPath"
625
+ ]) ?? this.findStringByKeyPattern(update, /(cwd|path|target)/i);
626
+ const query = this.readStringField(update, [
627
+ "query",
628
+ "q",
629
+ "searchQuery",
630
+ "keyword"
631
+ ]) ?? this.findStringByKeyPattern(update, /(query|keyword)\b/i);
632
+ const url = this.readStringField(update, [
633
+ "url",
634
+ "uri",
635
+ "link"
636
+ ]) ?? this.findStringByKeyPattern(update, /(url|uri|link)\b/i);
637
+ const toolNameResult = this.extractToolName(update);
638
+ const status = this.readStringField(update, [
639
+ "status",
640
+ "state",
641
+ "phase"
642
+ ]);
643
+ const id = this.readStringField(update, [
644
+ "id",
645
+ "toolCallId",
646
+ "tool_call_id"
647
+ ]) ?? this.findStringByKeyPattern(update, /\b(id|tool.*id|call.*id)\b/i);
648
+ const title = this.readStringField(update, ["title"]);
649
+ const kind = this.readStringField(update, ["kind"]);
650
+ const error = this.readStringField(update, ["error", "message"]);
651
+ return {
652
+ updateType: update.sessionUpdate,
653
+ toolCallId: id ?? "unknown",
654
+ toolName: toolNameResult.name,
655
+ toolNameSource: toolNameResult.source,
656
+ kind: kind ?? "unknown",
657
+ title,
658
+ status: status ?? "unknown",
659
+ command,
660
+ path,
661
+ query,
662
+ url,
663
+ error,
664
+ fieldPaths: collectFieldPaths(update)
665
+ };
666
+ }
667
+ extractToolName(update) {
668
+ const fromActionType = this.extractToolNameFromActionType(update);
669
+ if (fromActionType) return {
670
+ name: fromActionType,
671
+ source: "action_type"
672
+ };
673
+ const fromTitle = this.extractToolNameFromTitle(update);
674
+ if (fromTitle) return {
675
+ name: fromTitle,
676
+ source: "title"
677
+ };
678
+ const direct = this.readStringField(update, [
679
+ "toolName",
680
+ "tool_name",
681
+ "tool",
682
+ "method",
683
+ "function"
684
+ ]) ?? this.findStringByKeyPattern(update, /(tool.*name|tool|method|function|action)/i);
685
+ if (direct && !this.isGenericToolValue(direct)) return {
686
+ name: direct,
687
+ source: "direct_field"
688
+ };
689
+ const knownTool = this.findKnownToolName(update);
690
+ if (knownTool) return {
691
+ name: knownTool,
692
+ source: "known_pattern"
693
+ };
694
+ const command = this.readStringField(update, [
695
+ "command",
696
+ "cmd",
697
+ "shellCommand"
698
+ ]) ?? this.findStringByKeyPattern(update, /(cmd|command|shell)/i);
699
+ if (command) return {
700
+ name: command.split(/\s+/)[0] ?? "unknown",
701
+ source: "command"
702
+ };
703
+ return {
704
+ name: "unknown",
705
+ source: "fallback"
706
+ };
707
+ }
708
+ extractToolNameFromActionType(update) {
709
+ const actionType = this.readStringField(update, ["actionType", "action_type"]) ?? this.findStringByKeyPattern(update, /action.*type/i);
710
+ if (!actionType) return;
711
+ const normalized = actionType.trim().toLowerCase();
712
+ if (normalized === "search") return "web.search_query";
713
+ if (normalized === "open_page") return "web.open";
714
+ if (normalized === "find_text") return "web.find";
715
+ if (normalized === "screenshot") return "web.screenshot";
716
+ return `web.${normalized}`;
717
+ }
718
+ extractToolNameFromTitle(update) {
719
+ const title = this.readStringField(update, ["title"]);
720
+ if (!title) return;
721
+ const normalized = title.toLowerCase();
722
+ if (normalized.includes("searching the web") || normalized.includes("searching for:")) return "web.search_query";
723
+ if (normalized.includes("opening:") || normalized === "open page") return "web.open";
724
+ if (normalized.startsWith("finding:")) return "web.find";
725
+ }
726
+ summarizeUpdate(update) {
727
+ const content = update.content;
728
+ if (content?.type === "text") return content.text;
729
+ if (content?.type === "image") return `[image:${content.mimeType}]`;
730
+ const { sessionUpdate: _sessionUpdate, content: _content, availableCommands: _availableCommands, used: _used, size: _size, ...compact } = update;
731
+ const text = JSON.stringify(compact);
732
+ if (!text || text === "{}") return "";
733
+ return text.length > 300 ? `${text.slice(0, 300)}...` : text;
734
+ }
735
+ readStringField(update, keys) {
736
+ const dict = update;
737
+ for (const key of keys) {
738
+ const value = dict[key];
739
+ if (typeof value === "string" && value.trim()) return value.trim();
740
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
741
+ if (value && typeof value === "object") {
742
+ const nested = value;
743
+ for (const nestedKey of [
744
+ "name",
745
+ "text",
746
+ "command",
747
+ "path",
748
+ "message"
749
+ ]) {
750
+ const nestedValue = nested[nestedKey];
751
+ if (typeof nestedValue === "string" && nestedValue.trim()) return nestedValue.trim();
752
+ }
753
+ }
754
+ }
755
+ }
756
+ findStringByKeyPattern(value, pattern, depth = 0) {
757
+ if (depth > 6) return;
758
+ for (const [key, item] of Object.entries(value)) {
759
+ if (typeof item === "string" && item.trim() && pattern.test(key)) return item.trim();
760
+ if (item && typeof item === "object") {
761
+ if (Array.isArray(item)) {
762
+ for (const entry of item) if (entry && typeof entry === "object") {
763
+ const nested = this.findStringByKeyPattern(entry, pattern, depth + 1);
764
+ if (nested) return nested;
765
+ }
766
+ continue;
767
+ }
768
+ const nested = this.findStringByKeyPattern(item, pattern, depth + 1);
769
+ if (nested) return nested;
770
+ }
771
+ }
772
+ }
773
+ isGenericToolValue(value) {
774
+ const normalized = value.trim().toLowerCase();
775
+ return /^ws_[a-z0-9]+$/.test(normalized) || normalized === "tool" || normalized === "tools" || normalized === "in_progress" || normalized === "completed" || normalized === "running" || normalized === "status";
776
+ }
777
+ findKnownToolName(value, depth = 0) {
778
+ if (depth > 6) return;
779
+ const known = [
780
+ "search_query",
781
+ "web_search",
782
+ "image_query",
783
+ "open",
784
+ "click",
785
+ "find",
786
+ "screenshot",
787
+ "finance",
788
+ "weather",
789
+ "sports",
790
+ "time"
791
+ ];
792
+ for (const item of Object.values(value)) {
793
+ if (typeof item === "string") {
794
+ const normalized = item.trim().toLowerCase();
795
+ if (known.includes(normalized)) return normalized;
796
+ }
797
+ if (item && typeof item === "object") {
798
+ if (Array.isArray(item)) {
799
+ for (const entry of item) if (entry && typeof entry === "object") {
800
+ const nested = this.findKnownToolName(entry, depth + 1);
801
+ if (nested) return nested;
802
+ }
803
+ continue;
804
+ }
805
+ const nested = this.findKnownToolName(item, depth + 1);
806
+ if (nested) return nested;
807
+ }
808
+ }
809
+ }
810
+ shouldLogToolUpdate(taskId, update) {
811
+ const key = `${taskId}:${update.toolCallId}:${update.status}:${update.updateType}`;
812
+ if (this.#toolUpdateLogDedup.has(key)) return false;
813
+ this.#toolUpdateLogDedup.add(key);
814
+ return true;
815
+ }
816
+ clearToolUpdateLogCache(taskId) {
817
+ for (const key of this.#toolUpdateLogDedup) if (key.startsWith(`${taskId}:`)) this.#toolUpdateLogDedup.delete(key);
818
+ }
819
+ truncate(text, maxLen) {
820
+ return text.length > maxLen ? `${text.slice(0, maxLen)}...` : text;
821
+ }
822
+ getChild(taskId) {
823
+ const agentChild = this.#children.get(taskId);
824
+ if (!agentChild) throw new Error(`Agent process not found for task: ${taskId}`);
825
+ return agentChild;
826
+ }
827
+ createRequestTimer(taskId, requestId, method, timeoutMs) {
828
+ return setTimeout(() => {
829
+ const agentChild = this.#children.get(taskId);
830
+ const pending = agentChild?.pendingRequests.get(requestId);
831
+ if (!pending) return;
832
+ agentChild?.pendingRequests.delete(requestId);
833
+ pending.reject(new AgentProcessError("agent_session_timeout", `ACP request timed out for ${method} after ${timeoutMs}ms`));
834
+ }, timeoutMs);
835
+ }
836
+ refreshPromptTimeout(taskId) {
837
+ const agentChild = this.#children.get(taskId);
838
+ if (!agentChild) return;
839
+ for (const [requestId, pending] of agentChild.pendingRequests.entries()) {
840
+ if (pending.method !== "session/prompt") continue;
841
+ clearTimeout(pending.timer);
842
+ pending.timer = this.createRequestTimer(taskId, requestId, pending.method, pending.timeoutMs);
843
+ }
844
+ }
845
+ rejectAllPendingRequests(taskId, reason) {
846
+ const agentChild = this.#children.get(taskId);
847
+ if (!agentChild) return;
848
+ for (const [id, pending] of agentChild.pendingRequests.entries()) {
849
+ clearTimeout(pending.timer);
850
+ pending.reject(new AgentProcessError("agent_session_start_failed", `ACP request interrupted for ${pending.method}: ${reason}`));
851
+ agentChild.pendingRequests.delete(id);
852
+ }
853
+ }
854
+ mapRequestError(taskId, method, error) {
855
+ if (error instanceof AgentProcessError) {
856
+ if (error.code === "agent_session_timeout") {
857
+ const stderr = this.getChild(taskId).stderrChunks.join("\n");
858
+ if (!(spawnSync("test", ["-f", `${process.env.HOME}/.codex/auth.json`], { stdio: "ignore" }).status === 0)) return new AgentProcessError("agent_auth_missing", "Codex authentication is missing. Please complete local Codex login first.");
859
+ return new AgentProcessError("agent_session_timeout", stderr ? `codex-acp did not finish ${method} within timeout. stderr: ${stderr}` : `codex-acp did not finish ${method} within timeout`);
860
+ }
861
+ return error;
862
+ }
863
+ return new AgentProcessError("agent_session_start_failed", error instanceof Error ? error.message : String(error));
864
+ }
865
+ };
866
+ //#endregion
867
+ //#region src/approval/approval-store.ts
868
+ var ApprovalStore = class {
869
+ #snapshots = /* @__PURE__ */ new Map();
870
+ #waiters = /* @__PURE__ */ new Map();
871
+ set(request) {
872
+ this.#snapshots.set(request.id, {
873
+ request,
874
+ status: "pending"
875
+ });
876
+ }
877
+ get(requestId) {
878
+ return this.#snapshots.get(requestId);
879
+ }
880
+ delete(requestId) {
881
+ const waiter = this.#waiters.get(requestId);
882
+ if (waiter) {
883
+ clearTimeout(waiter.timer);
884
+ this.#waiters.delete(requestId);
885
+ }
886
+ this.#snapshots.delete(requestId);
887
+ }
888
+ awaitDecision(requestId, timeoutMs) {
889
+ const snapshot = this.#snapshots.get(requestId);
890
+ if (!snapshot) return Promise.reject(/* @__PURE__ */ new Error(`Approval request not found: ${requestId}`));
891
+ if (snapshot.status === "approved" && snapshot.decision) return Promise.resolve({
892
+ status: "approved",
893
+ request: snapshot.request,
894
+ decision: snapshot.decision
895
+ });
896
+ if (snapshot.status === "rejected") return Promise.resolve({
897
+ status: "rejected",
898
+ request: snapshot.request,
899
+ decision: snapshot.decision
900
+ });
901
+ if (snapshot.status === "expired") return Promise.resolve({
902
+ status: "expired",
903
+ request: snapshot.request
904
+ });
905
+ return new Promise((resolve) => {
906
+ const timer = setTimeout(() => {
907
+ const current = this.#snapshots.get(requestId);
908
+ if (!current || current.status !== "pending") return;
909
+ current.status = "expired";
910
+ current.resolvedAt = (/* @__PURE__ */ new Date()).toISOString();
911
+ this.#snapshots.set(requestId, current);
912
+ this.#waiters.delete(requestId);
913
+ resolve({
914
+ status: "expired",
915
+ request: current.request
916
+ });
917
+ }, timeoutMs);
918
+ this.#waiters.set(requestId, {
919
+ resolve,
920
+ timer
921
+ });
922
+ });
923
+ }
924
+ resolve(decision) {
925
+ const snapshot = this.#snapshots.get(decision.requestId);
926
+ if (!snapshot) return { accepted: false };
927
+ if (snapshot.status !== "pending") return {
928
+ accepted: false,
929
+ snapshot
930
+ };
931
+ snapshot.status = decision.decision === "approved" ? "approved" : "rejected";
932
+ snapshot.decision = decision;
933
+ snapshot.resolvedAt = (/* @__PURE__ */ new Date()).toISOString();
934
+ this.#snapshots.set(decision.requestId, snapshot);
935
+ const waiter = this.#waiters.get(decision.requestId);
936
+ if (waiter) {
937
+ clearTimeout(waiter.timer);
938
+ this.#waiters.delete(decision.requestId);
939
+ waiter.resolve(snapshot.status === "approved" ? {
940
+ status: "approved",
941
+ request: snapshot.request,
942
+ decision
943
+ } : {
944
+ status: "rejected",
945
+ request: snapshot.request,
946
+ decision
947
+ });
948
+ }
949
+ return {
950
+ accepted: true,
951
+ snapshot
952
+ };
953
+ }
954
+ };
955
+ //#endregion
956
+ //#region src/approval/approval-gateway.ts
957
+ var ApprovalGateway = class {
958
+ #resolutionListeners = /* @__PURE__ */ new Set();
959
+ #sessionAllowAll = /* @__PURE__ */ new Set();
960
+ constructor(store, logger) {
961
+ this.store = store;
962
+ this.logger = logger;
963
+ }
964
+ request(request) {
965
+ this.store.set(request);
966
+ this.logger.info("approval request stored", {
967
+ requestId: request.id,
968
+ taskId: request.taskId,
969
+ kind: request.kind
970
+ });
971
+ }
972
+ async requestAndWait(request, timeoutMs) {
973
+ this.request(request);
974
+ const result = await this.store.awaitDecision(request.id, timeoutMs);
975
+ if (result.status === "expired") {
976
+ const snapshot = this.store.get(request.id);
977
+ if (snapshot) {
978
+ this.emitResolution(snapshot);
979
+ this.store.delete(request.id);
980
+ }
981
+ }
982
+ return result;
983
+ }
984
+ resolve(decision) {
985
+ const result = this.store.resolve(decision);
986
+ if (!result.snapshot) {
987
+ this.logger.warn("approval request not found", {
988
+ requestId: decision.requestId,
989
+ taskId: decision.taskId
990
+ });
991
+ return;
992
+ }
993
+ if (!result.accepted) {
994
+ this.logger.info("approval request resolve ignored", {
995
+ requestId: decision.requestId,
996
+ taskId: decision.taskId,
997
+ decision: decision.decision,
998
+ status: result.snapshot.status
999
+ });
1000
+ return result.snapshot;
1001
+ }
1002
+ if (decision.decision === "approved" && decision.comment === "approved-for-session") this.#sessionAllowAll.add(decision.taskId);
1003
+ this.logger.info("approval request resolved", {
1004
+ requestId: decision.requestId,
1005
+ taskId: decision.taskId,
1006
+ decision: decision.decision
1007
+ });
1008
+ this.emitResolution(result.snapshot);
1009
+ this.store.delete(decision.requestId);
1010
+ return result.snapshot;
1011
+ }
1012
+ isSessionAllowAll(taskId) {
1013
+ return this.#sessionAllowAll.has(taskId);
1014
+ }
1015
+ onResolved(listener) {
1016
+ this.#resolutionListeners.add(listener);
1017
+ return () => {
1018
+ this.#resolutionListeners.delete(listener);
1019
+ };
1020
+ }
1021
+ emitResolution(snapshot) {
1022
+ for (const listener of this.#resolutionListeners) listener(snapshot);
1023
+ }
1024
+ };
1025
+ //#endregion
1026
+ //#region src/policy/policy-engine.ts
1027
+ function evaluatePolicy(input) {
1028
+ const { kind, hasSessionAllowAll } = input;
1029
+ if (hasSessionAllowAll) return { type: "allow" };
1030
+ return {
1031
+ type: "ask",
1032
+ reason: `Approval required for ${kind}`
1033
+ };
1034
+ }
1035
+ //#endregion
1036
+ //#region src/approval/permission-handler.ts
1037
+ const APPROVAL_TIMEOUT_MS = 12e4;
1038
+ function findWorkspaceByTask(taskId, taskRunner, workspaces) {
1039
+ const task = taskRunner.getTask(taskId);
1040
+ if (!task) return workspaces[0];
1041
+ return workspaces.find((item) => item.id === task.workspaceId) ?? workspaces[0];
1042
+ }
1043
+ function extractTargetPath(params) {
1044
+ const fromLocation = params.toolCall.locations?.[0]?.path;
1045
+ if (fromLocation) return fromLocation;
1046
+ const rawInput = params.toolCall.rawInput;
1047
+ if (rawInput && typeof rawInput === "object") {
1048
+ const candidate = rawInput.path;
1049
+ if (typeof candidate === "string" && candidate.trim()) return candidate;
1050
+ }
1051
+ }
1052
+ function extractCommand(params) {
1053
+ const rawInput = params.toolCall.rawInput;
1054
+ if (typeof rawInput === "string" && rawInput.trim()) return rawInput.trim();
1055
+ if (rawInput && typeof rawInput === "object") for (const key of [
1056
+ "command",
1057
+ "cmd",
1058
+ "shellCommand",
1059
+ "input",
1060
+ "prompt"
1061
+ ]) {
1062
+ const value = rawInput[key];
1063
+ if (typeof value === "string" && value.trim()) return value.trim();
1064
+ }
1065
+ const content = params.toolCall.content;
1066
+ if (content && content.length > 0) {
1067
+ for (const item of content) if (item.type === "text" && typeof item.text === "string" && item.text.trim()) return item.text.trim();
1068
+ }
1069
+ }
1070
+ function inferApprovalKind(params) {
1071
+ const kind = (params.toolCall.kind ?? "").toLowerCase();
1072
+ const title = (params.toolCall.title ?? "").toLowerCase();
1073
+ const cmd = (extractCommand(params) ?? "").toLowerCase();
1074
+ if (/network|http|https|curl|wget/.test(kind) || /network|http|https|curl|wget/.test(title) || /curl|wget/.test(cmd)) return "network";
1075
+ if (/edit|patch|write|file/.test(kind) || /edit|patch|write|apply/.test(title)) return "write";
1076
+ if (/read|view|list/.test(kind) || /read|view|list/.test(title)) return "read";
1077
+ if (/cat\s|ls\s|find\s|rg\s/.test(cmd)) return "read";
1078
+ return "exec";
1079
+ }
1080
+ function chooseAllowOption(options, preferSession) {
1081
+ if (options.length === 0) return;
1082
+ if (preferSession) {
1083
+ const sessionOption = options.find((item) => {
1084
+ const id = item.id.toLowerCase();
1085
+ return id === "approved-for-session" || id === "approve-for-session" || id === "always";
1086
+ });
1087
+ if (sessionOption) return sessionOption.id;
1088
+ }
1089
+ const explicitApproved = options.find((item) => {
1090
+ const id = item.id.toLowerCase();
1091
+ return id === "approved" || id === "approve" || id === "allow" || id === "yes";
1092
+ });
1093
+ if (explicitApproved) return explicitApproved.id;
1094
+ const allow = options.find((item) => item.kind.toLowerCase().startsWith("allow"));
1095
+ if (allow) return allow.id;
1096
+ return options[0]?.id;
1097
+ }
1098
+ function chooseRejectOption(options) {
1099
+ for (const candidate of [
1100
+ "abort",
1101
+ "rejected",
1102
+ "reject",
1103
+ "cancel"
1104
+ ]) {
1105
+ const hit = options.find((item) => item.id === candidate);
1106
+ if (hit) return hit.id;
1107
+ }
1108
+ return options.find((item) => item.kind.startsWith("reject"))?.id;
1109
+ }
1110
+ function buildApprovalRequest(taskId, kind, workspace, params) {
1111
+ const now = /* @__PURE__ */ new Date();
1112
+ const command = extractCommand(params);
1113
+ const target = extractTargetPath(params);
1114
+ const title = params.toolCall.title ?? `Permission required: ${kind}`;
1115
+ return {
1116
+ id: randomUUID(),
1117
+ taskId,
1118
+ kind,
1119
+ title,
1120
+ cwd: workspace.cwd,
1121
+ target,
1122
+ command,
1123
+ reason: title,
1124
+ riskLevel: kind === "read" ? "low" : kind === "write" ? "medium" : "high",
1125
+ createdAt: now.toISOString(),
1126
+ expiresAt: new Date(now.getTime() + APPROVAL_TIMEOUT_MS).toISOString()
1127
+ };
1128
+ }
1129
+ function buildDeniedWorkspaceOutcome(taskId, params) {
1130
+ const fallbackRequest = {
1131
+ id: randomUUID(),
1132
+ taskId,
1133
+ kind: "exec",
1134
+ title: "Permission denied: workspace not found",
1135
+ cwd: process.cwd(),
1136
+ reason: "workspace_not_found",
1137
+ riskLevel: "high",
1138
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1139
+ expiresAt: new Date(Date.now() + APPROVAL_TIMEOUT_MS).toISOString()
1140
+ };
1141
+ return {
1142
+ status: "denied",
1143
+ optionId: chooseRejectOption(params.options),
1144
+ approvalRequest: fallbackRequest,
1145
+ decision: {
1146
+ requestId: fallbackRequest.id,
1147
+ taskId,
1148
+ decision: "rejected",
1149
+ comment: "workspace_not_found",
1150
+ decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
1151
+ decidedBy: "policy-engine"
1152
+ }
1153
+ };
1154
+ }
1155
+ function createPermissionRequestHandler(args) {
1156
+ const { workspaces, approvalGateway, getTaskRunner } = args;
1157
+ return async ({ taskId, params, emitApprovalRequested }) => {
1158
+ const taskRunner = getTaskRunner();
1159
+ const workspace = findWorkspaceByTask(taskId, taskRunner, workspaces);
1160
+ if (!workspace) return buildDeniedWorkspaceOutcome(taskId, params);
1161
+ const kind = inferApprovalKind(params);
1162
+ const request = buildApprovalRequest(taskId, kind, workspace, params);
1163
+ const policy = evaluatePolicy({
1164
+ kind,
1165
+ workspace,
1166
+ hasSessionAllowAll: approvalGateway.isSessionAllowAll(taskId) || taskRunner.isTaskFullAccess(taskId)
1167
+ });
1168
+ if (policy.type === "allow") {
1169
+ const optionId = chooseAllowOption(params.options, true);
1170
+ if (!optionId) return {
1171
+ status: "denied",
1172
+ optionId: chooseRejectOption(params.options),
1173
+ approvalRequest: request,
1174
+ decision: {
1175
+ requestId: request.id,
1176
+ taskId,
1177
+ decision: "rejected",
1178
+ comment: "allow_option_not_found",
1179
+ decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
1180
+ decidedBy: "policy-engine"
1181
+ }
1182
+ };
1183
+ return {
1184
+ status: "approved",
1185
+ optionId,
1186
+ approvalRequest: request,
1187
+ decision: {
1188
+ requestId: request.id,
1189
+ taskId,
1190
+ decision: "approved",
1191
+ decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
1192
+ decidedBy: "policy-engine",
1193
+ comment: optionId === "approved-for-session" ? "approved-for-session" : "auto-allow"
1194
+ }
1195
+ };
1196
+ }
1197
+ if (policy.type === "deny") return {
1198
+ status: "denied",
1199
+ optionId: chooseRejectOption(params.options),
1200
+ approvalRequest: request,
1201
+ decision: {
1202
+ requestId: request.id,
1203
+ taskId,
1204
+ decision: "rejected",
1205
+ comment: policy.reason,
1206
+ decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
1207
+ decidedBy: "policy-engine"
1208
+ }
1209
+ };
1210
+ emitApprovalRequested(request);
1211
+ const awaited = await approvalGateway.requestAndWait(request, APPROVAL_TIMEOUT_MS);
1212
+ if (awaited.status === "approved") {
1213
+ const optionId = chooseAllowOption(params.options, awaited.decision.comment === "approved-for-session");
1214
+ if (!optionId) return {
1215
+ status: "denied",
1216
+ optionId: chooseRejectOption(params.options),
1217
+ approvalRequest: request,
1218
+ decision: {
1219
+ requestId: request.id,
1220
+ taskId,
1221
+ decision: "rejected",
1222
+ comment: "allow_option_not_found",
1223
+ decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
1224
+ decidedBy: "bridge"
1225
+ }
1226
+ };
1227
+ return {
1228
+ status: "approved",
1229
+ optionId,
1230
+ approvalRequest: request,
1231
+ decision: awaited.decision
1232
+ };
1233
+ }
1234
+ if (awaited.status === "rejected") return {
1235
+ status: "rejected",
1236
+ optionId: chooseRejectOption(params.options),
1237
+ approvalRequest: request,
1238
+ decision: awaited.decision
1239
+ };
1240
+ return {
1241
+ status: "expired",
1242
+ optionId: chooseRejectOption(params.options),
1243
+ approvalRequest: request,
1244
+ decision: {
1245
+ requestId: request.id,
1246
+ taskId,
1247
+ decision: "rejected",
1248
+ comment: "approval_timeout",
1249
+ decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
1250
+ decidedBy: "system-timeout"
1251
+ }
1252
+ };
1253
+ };
1254
+ }
1255
+ //#endregion
1256
+ //#region src/config/load-config.ts
1257
+ const CODEX_ACP_BIN = fileURLToPath(import.meta.resolve("@zed-industries/codex-acp/bin/codex-acp.js"));
1258
+ const DEFAULT_CONFIG_ENV_PATH = resolve(homedir(), ".im-code-agent", "config.env");
1259
+ const DEFAULT_CONFIG_ENV_CONTENT = `# Bridge 基础配置
1260
+
1261
+ # 飞书应用凭据
1262
+ FEISHU_APP_ID=
1263
+ FEISHU_APP_SECRET=
1264
+
1265
+ # 可选:true 时默认 Full Access
1266
+ YOLO_MODE=false
1267
+
1268
+ # 可选:默认工作目录,不填则使用 bridge 启动目录
1269
+ WORKSPACE_DEFAULT_CWD=
1270
+ `;
1271
+ function parseEnvFile(content) {
1272
+ const result = {};
1273
+ for (const rawLine of content.split("\n")) {
1274
+ const line = rawLine.trim();
1275
+ if (!line || line.startsWith("#")) continue;
1276
+ const equalIndex = line.indexOf("=");
1277
+ if (equalIndex <= 0) continue;
1278
+ const key = line.slice(0, equalIndex).trim();
1279
+ let value = line.slice(equalIndex + 1).trim();
1280
+ if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
1281
+ result[key] = value;
1282
+ }
1283
+ return result;
1284
+ }
1285
+ function stringifyEnvValue(value) {
1286
+ return /[\s#"'`]/.test(value) ? JSON.stringify(value) : value;
1287
+ }
1288
+ function upsertEnvValue(content, key, value) {
1289
+ const lines = content.split("\n");
1290
+ const rendered = `${key}=${stringifyEnvValue(value)}`;
1291
+ const lineIndex = lines.findIndex((line) => line.trimStart().startsWith(`${key}=`));
1292
+ if (lineIndex >= 0) lines[lineIndex] = rendered;
1293
+ else {
1294
+ if (lines.length > 0 && lines.at(-1) !== "") lines.push("");
1295
+ lines.push(rendered);
1296
+ }
1297
+ return lines.join("\n");
1298
+ }
1299
+ function normalizeCredential(value) {
1300
+ const trimmed = value?.trim();
1301
+ if (!trimmed) return;
1302
+ if (trimmed === "cli_xxx" || trimmed === "xxx") return;
1303
+ return trimmed;
1304
+ }
1305
+ async function resolveNodeCommand() {
1306
+ if (await fileExists(process.execPath)) return process.execPath;
1307
+ return "node";
1308
+ }
1309
+ async function isDirectory(dirPath) {
1310
+ try {
1311
+ return (await stat(dirPath)).isDirectory();
1312
+ } catch {
1313
+ return false;
1314
+ }
1315
+ }
1316
+ async function loadBridgeEnvFile() {
1317
+ await ensureDefaultConfigEnvFile(resolveBridgeEnvPath());
1318
+ const configuredPath = process.env.BRIDGE_ENV_PATH;
1319
+ const envPaths = configuredPath ? [resolve(configuredPath)] : [
1320
+ DEFAULT_CONFIG_ENV_PATH,
1321
+ resolve(process.cwd(), "bridge.env"),
1322
+ resolve(process.cwd(), ".env")
1323
+ ];
1324
+ for (const envPath of envPaths) try {
1325
+ return parseEnvFile(await readFile(envPath, "utf8"));
1326
+ } catch {
1327
+ continue;
1328
+ }
1329
+ return {};
1330
+ }
1331
+ function resolveBridgeEnvPath() {
1332
+ const configuredPath = process.env.BRIDGE_ENV_PATH;
1333
+ return configuredPath ? resolve(configuredPath) : DEFAULT_CONFIG_ENV_PATH;
1334
+ }
1335
+ async function fileExists(filePath) {
1336
+ try {
1337
+ await access(filePath, constants.F_OK);
1338
+ return true;
1339
+ } catch {
1340
+ return false;
1341
+ }
1342
+ }
1343
+ async function ensureDefaultConfigEnvFile(configEnvPath) {
1344
+ await createConfigEnvIfMissing(configEnvPath, process.cwd());
1345
+ }
1346
+ async function createConfigEnvIfMissing(targetPath, baseDir) {
1347
+ if (await fileExists(targetPath)) return;
1348
+ await mkdir(dirname(targetPath), { recursive: true });
1349
+ const configTemplatePath = resolve(baseDir, "config.env.example");
1350
+ if (await fileExists(configTemplatePath)) {
1351
+ await copyFile(configTemplatePath, targetPath);
1352
+ return;
1353
+ }
1354
+ await writeFile(targetPath, DEFAULT_CONFIG_ENV_CONTENT, "utf8");
1355
+ }
1356
+ async function promptForFeishuCredentials(envPath) {
1357
+ if (!process.stdin.isTTY || !process.stdout.isTTY) throw new Error(`未检测到飞书配置,请在 ${envPath} 中填写 FEISHU_APP_ID 和 FEISHU_APP_SECRET,或通过环境变量注入后再启动。`);
1358
+ console.warn([
1359
+ "未检测到有效的飞书配置。",
1360
+ `将引导你写入 ${envPath}。`,
1361
+ "直接回车可跳过,但当前启动会直接退出。"
1362
+ ].join(" "));
1363
+ const readline = createInterface({
1364
+ input: process.stdin,
1365
+ output: process.stdout
1366
+ });
1367
+ try {
1368
+ const appId = normalizeCredential(await readline.question("请输入飞书 App ID: "));
1369
+ const appSecret = normalizeCredential(await readline.question("请输入飞书 App Secret: "));
1370
+ if (!appId || !appSecret) throw new Error("缺少飞书配置,启动已取消。请填写 FEISHU_APP_ID 和 FEISHU_APP_SECRET 后重试。");
1371
+ const nextContent = upsertEnvValue(upsertEnvValue(await readFile(envPath, "utf8").catch(() => DEFAULT_CONFIG_ENV_CONTENT), "FEISHU_APP_ID", appId), "FEISHU_APP_SECRET", appSecret);
1372
+ await writeFile(envPath, nextContent.endsWith("\n") ? nextContent : `${nextContent}\n`, "utf8");
1373
+ process.env.FEISHU_APP_ID = appId;
1374
+ process.env.FEISHU_APP_SECRET = appSecret;
1375
+ console.warn(`飞书配置已写入 ${envPath}。`);
1376
+ } finally {
1377
+ readline.close();
1378
+ }
1379
+ }
1380
+ async function loadConfig() {
1381
+ const nodeCommand = await resolveNodeCommand();
1382
+ const envPath = resolveBridgeEnvPath();
1383
+ let fileEnv = await loadBridgeEnvFile();
1384
+ const getValue = (key) => process.env[key] ?? fileEnv[key];
1385
+ let appId = normalizeCredential(getValue("FEISHU_APP_ID"));
1386
+ let appSecret = normalizeCredential(getValue("FEISHU_APP_SECRET"));
1387
+ if (!appId || !appSecret) {
1388
+ await promptForFeishuCredentials(envPath);
1389
+ fileEnv = await loadBridgeEnvFile();
1390
+ appId = normalizeCredential(process.env.FEISHU_APP_ID ?? fileEnv.FEISHU_APP_ID);
1391
+ appSecret = normalizeCredential(process.env.FEISHU_APP_SECRET ?? fileEnv.FEISHU_APP_SECRET);
1392
+ }
1393
+ if (!appId || !appSecret) throw new Error(`缺少飞书配置,请在 ${envPath} 中填写 FEISHU_APP_ID 和 FEISHU_APP_SECRET 后重试。`);
1394
+ const yoloMode = getValue("YOLO_MODE")?.trim().toLowerCase() === "true";
1395
+ const workspaceDefaultCwd = getValue("WORKSPACE_DEFAULT_CWD")?.trim();
1396
+ const resolvedDefaultCwd = workspaceDefaultCwd ? resolve(workspaceDefaultCwd) : process.cwd();
1397
+ const defaultCwd = await isDirectory(resolvedDefaultCwd) ? resolvedDefaultCwd : process.cwd();
1398
+ return {
1399
+ feishu: {
1400
+ appId,
1401
+ appSecret
1402
+ },
1403
+ yoloMode,
1404
+ agents: { codex: {
1405
+ command: nodeCommand,
1406
+ args: [CODEX_ACP_BIN]
1407
+ } },
1408
+ workspaces: [{
1409
+ id: "local-default",
1410
+ name: "Local Default",
1411
+ cwd: defaultCwd
1412
+ }]
1413
+ };
1414
+ }
1415
+ //#endregion
1416
+ //#region src/feishu/approval-card-policy.ts
1417
+ function shouldPatchApprovalSummary(decision) {
1418
+ return decision.comment !== "approval_timeout";
1419
+ }
1420
+ //#endregion
1421
+ //#region src/feishu/command-router.ts
1422
+ function parseContent(content) {
1423
+ try {
1424
+ return JSON.parse(content);
1425
+ } catch {
1426
+ return {};
1427
+ }
1428
+ }
1429
+ async function parseUserCommand(rawPrompt, currentCwd) {
1430
+ if (rawPrompt === "/help") return { type: "help" };
1431
+ if (rawPrompt === "/status") return { type: "status" };
1432
+ if (rawPrompt === "/stop" || rawPrompt === "/interrupt") return { type: "stop" };
1433
+ if (rawPrompt === "/perm") return { type: "show-access" };
1434
+ if (rawPrompt === "/model") return { type: "model" };
1435
+ if (rawPrompt.startsWith("/model ")) return {
1436
+ type: "model",
1437
+ model: rawPrompt.slice(7).trim() || void 0
1438
+ };
1439
+ if (rawPrompt === "/new") return { type: "new" };
1440
+ if (rawPrompt.startsWith("/new ")) {
1441
+ const inputPath = rawPrompt.slice(5).trim();
1442
+ if (!inputPath) return { type: "new" };
1443
+ return {
1444
+ type: "new",
1445
+ cwd: await resolveValidatedCwd(inputPath, currentCwd)
1446
+ };
1447
+ }
1448
+ return {
1449
+ type: "prompt",
1450
+ prompt: rawPrompt
1451
+ };
1452
+ }
1453
+ function parseCardActionValue(data) {
1454
+ const payload = data.action?.value ?? data.event?.action?.value;
1455
+ if (!payload) return null;
1456
+ let value = payload;
1457
+ if (typeof value === "string") try {
1458
+ value = JSON.parse(value);
1459
+ } catch {
1460
+ return null;
1461
+ }
1462
+ if (!value || typeof value !== "object") return null;
1463
+ const item = value;
1464
+ if (item.type === "access" && typeof item.cardId === "string" && typeof item.chatId === "string" && (item.action === "set" || item.action === "clear")) {
1465
+ if (item.action === "clear") return {
1466
+ type: "access",
1467
+ cardId: item.cardId,
1468
+ chatId: item.chatId,
1469
+ action: "clear"
1470
+ };
1471
+ if (item.mode === "standard" || item.mode === "full-access") return {
1472
+ type: "access",
1473
+ cardId: item.cardId,
1474
+ chatId: item.chatId,
1475
+ action: "set",
1476
+ mode: item.mode
1477
+ };
1478
+ return null;
1479
+ }
1480
+ if (item.type === "model" && typeof item.cardId === "string" && typeof item.chatId === "string" && typeof item.model === "string") return {
1481
+ type: "model",
1482
+ cardId: item.cardId,
1483
+ chatId: item.chatId,
1484
+ model: item.model
1485
+ };
1486
+ if (typeof item.requestId === "string" && typeof item.taskId === "string" && (item.decision === "approved" || item.decision === "rejected")) return {
1487
+ type: "approval",
1488
+ requestId: item.requestId,
1489
+ taskId: item.taskId,
1490
+ decision: item.decision,
1491
+ comment: typeof item.comment === "string" ? item.comment : void 0
1492
+ };
1493
+ return null;
1494
+ }
1495
+ async function resolveValidatedCwd(inputPath, baseCwd) {
1496
+ const candidate = isAbsolute(inputPath) ? inputPath : resolve(baseCwd, inputPath);
1497
+ const dirStat = await stat(candidate).catch(() => void 0);
1498
+ if (!dirStat || !dirStat.isDirectory()) throw new Error(`路径不存在或不是目录:${candidate}`);
1499
+ return resolve(candidate);
1500
+ }
1501
+ //#endregion
1502
+ //#region src/feishu/card-renderer.ts
1503
+ var TaskCardStreamer = class {
1504
+ #segments = [];
1505
+ #tools = /* @__PURE__ */ new Map();
1506
+ #status = "running";
1507
+ #summary = "执行中";
1508
+ #lastPatchedAt = 0;
1509
+ #pendingPatchTimer;
1510
+ #pendingPatchResolvers = [];
1511
+ #patchQueue = Promise.resolve();
1512
+ #cardMessageId;
1513
+ constructor(deps) {
1514
+ this.deps = deps;
1515
+ }
1516
+ handleOutputChunk(chunk) {
1517
+ const toolUpdate = this.deps.renderer.parseToolChunk(chunk);
1518
+ if (toolUpdate) {
1519
+ this.upsertTool(toolUpdate);
1520
+ this.enqueuePatch(false);
1521
+ return;
1522
+ }
1523
+ const last = this.#segments[this.#segments.length - 1];
1524
+ if (last?.type === "text") last.content += chunk;
1525
+ else this.#segments.push({
1526
+ type: "text",
1527
+ content: chunk
1528
+ });
1529
+ this.enqueuePatch(false);
1530
+ }
1531
+ handleToolUpdate(update) {
1532
+ const toolUpdate = this.deps.renderer.toToolView(update);
1533
+ if (!toolUpdate) return;
1534
+ this.upsertTool(toolUpdate);
1535
+ this.enqueuePatch(false);
1536
+ }
1537
+ markFailed(error) {
1538
+ this.#status = "failed";
1539
+ this.#summary = error;
1540
+ this.enqueuePatch(true);
1541
+ }
1542
+ markCompleted(summary) {
1543
+ this.#status = "completed";
1544
+ this.#summary = summary ?? "执行完成";
1545
+ this.enqueuePatch(true);
1546
+ }
1547
+ async finalize() {
1548
+ this.enqueuePatch(true);
1549
+ if (this.#pendingPatchTimer) await new Promise((resolve) => {
1550
+ this.#pendingPatchResolvers.push(resolve);
1551
+ });
1552
+ await this.#patchQueue;
1553
+ if (!this.#cardMessageId) await this.ensureCardMessage();
1554
+ }
1555
+ upsertTool(toolUpdate) {
1556
+ const prev = this.#tools.get(toolUpdate.id) ?? { id: toolUpdate.id };
1557
+ this.#tools.set(toolUpdate.id, {
1558
+ ...prev,
1559
+ ...toolUpdate
1560
+ });
1561
+ if (!this.#segments.some((segment) => segment.type === "tool" && segment.id === toolUpdate.id)) this.#segments.push({
1562
+ type: "tool",
1563
+ id: toolUpdate.id
1564
+ });
1565
+ }
1566
+ enqueuePatch(force) {
1567
+ const now = Date.now();
1568
+ const wait = force ? 0 : Math.max(0, this.deps.patchMinIntervalMs - (now - this.#lastPatchedAt));
1569
+ if (!force && this.#pendingPatchTimer) return;
1570
+ if (this.#pendingPatchTimer) {
1571
+ clearTimeout(this.#pendingPatchTimer);
1572
+ this.#pendingPatchTimer = void 0;
1573
+ }
1574
+ this.#pendingPatchTimer = setTimeout(() => {
1575
+ this.#pendingPatchTimer = void 0;
1576
+ this.#lastPatchedAt = Date.now();
1577
+ while (this.#pendingPatchResolvers.length > 0) this.#pendingPatchResolvers.shift()?.();
1578
+ this.#patchQueue = this.#patchQueue.then(async () => {
1579
+ const messageId = await this.ensureCardMessage();
1580
+ if (!messageId) return;
1581
+ await this.deps.patchCard(messageId, this.deps.renderer.buildTaskCard({
1582
+ status: this.#status,
1583
+ segments: this.#segments,
1584
+ tools: this.#tools,
1585
+ summary: this.#summary
1586
+ }));
1587
+ }).catch((error) => {
1588
+ this.deps.logger.warn("feishu patch card failed", { error: error instanceof Error ? error.message : String(error) });
1589
+ });
1590
+ }, wait);
1591
+ }
1592
+ async ensureCardMessage() {
1593
+ if (this.#cardMessageId) return this.#cardMessageId;
1594
+ if (this.#status === "running" && this.#segments.length === 0) return;
1595
+ this.#cardMessageId = await this.deps.sendCard(this.deps.chatId, this.deps.renderer.buildTaskCard({
1596
+ status: this.#status,
1597
+ segments: this.#segments,
1598
+ tools: this.#tools,
1599
+ summary: this.#summary
1600
+ }));
1601
+ return this.#cardMessageId;
1602
+ }
1603
+ };
1604
+ var FeishuCardRenderer = class {
1605
+ buildTaskCard(params) {
1606
+ const footerElement = params.status === "running" ? {
1607
+ tag: "markdown",
1608
+ content: " ",
1609
+ icon: {
1610
+ tag: "custom_icon",
1611
+ img_key: "img_v3_02vb_496bec09-4b43-4773-ad6b-0cdd103cd2bg",
1612
+ size: "16px 16px"
1613
+ },
1614
+ element_id: "loading_icon"
1615
+ } : params.status === "completed" ? {
1616
+ tag: "div",
1617
+ text: {
1618
+ tag: "plain_text",
1619
+ content: "已完成",
1620
+ text_size: "cus-0"
1621
+ }
1622
+ } : {
1623
+ tag: "div",
1624
+ text: {
1625
+ tag: "plain_text",
1626
+ content: `执行失败:${params.summary}`,
1627
+ text_size: "cus-0"
1628
+ }
1629
+ };
1630
+ const elements = this.buildContentElements(params.segments, params.tools);
1631
+ if (elements.length === 0) elements.push({
1632
+ tag: "markdown",
1633
+ content: " "
1634
+ });
1635
+ elements.push(footerElement);
1636
+ return {
1637
+ schema: "2.0",
1638
+ config: {
1639
+ update_multi: true,
1640
+ width_mode: "fill",
1641
+ streaming_mode: true,
1642
+ style: {
1643
+ text_size: {
1644
+ "cus-0": {
1645
+ default: "notation",
1646
+ pc: "notation",
1647
+ mobile: "notation"
1648
+ },
1649
+ "cus-1": {
1650
+ default: "small",
1651
+ pc: "small",
1652
+ mobile: "small"
1653
+ }
1654
+ },
1655
+ color: {
1656
+ foot_gray: {
1657
+ light_mode: "rgba(100,106,115,1)",
1658
+ dark_mode: "rgba(182,188,196,1)"
1659
+ },
1660
+ tool_meta_gray: {
1661
+ light_mode: "rgba(100,106,115,1)",
1662
+ dark_mode: "rgba(182,188,196,1)"
1663
+ }
1664
+ }
1665
+ },
1666
+ summary: { content: params.status === "running" ? "生成中" : params.status === "completed" ? "已完成" : `执行失败:${params.summary}` }
1667
+ },
1668
+ body: {
1669
+ direction: "vertical",
1670
+ vertical_spacing: "6px",
1671
+ padding: "10px 12px 10px 12px",
1672
+ elements
1673
+ }
1674
+ };
1675
+ }
1676
+ buildApprovalSummaryCard(title, decision) {
1677
+ return {
1678
+ schema: "2.0",
1679
+ config: { update_multi: true },
1680
+ body: {
1681
+ direction: "vertical",
1682
+ padding: "10px 12px 10px 12px",
1683
+ elements: [
1684
+ {
1685
+ tag: "markdown",
1686
+ content: `**${this.escapeCardMarkdown(title)}**`
1687
+ },
1688
+ {
1689
+ tag: "markdown",
1690
+ content: `request: ${this.escapeCardMarkdown(decision.requestId)}`
1691
+ },
1692
+ {
1693
+ tag: "markdown",
1694
+ content: `时间: ${this.escapeCardMarkdown(decision.decidedAt)}`
1695
+ },
1696
+ {
1697
+ tag: "markdown",
1698
+ content: `操作者: ${this.escapeCardMarkdown(decision.decidedBy)}`
1699
+ }
1700
+ ]
1701
+ }
1702
+ };
1703
+ }
1704
+ buildAccessModeCard(params) {
1705
+ const currentMode = params.overrideMode ?? params.defaultMode;
1706
+ const overrideText = params.overrideMode ? this.formatAccessMode(params.overrideMode) : "继承默认";
1707
+ const sourceText = params.overrideMode ? "会话临时覆盖" : "全局默认";
1708
+ const fullActive = currentMode === "full-access";
1709
+ const standardActive = currentMode === "standard" && Boolean(params.overrideMode);
1710
+ const inheritActive = !params.overrideMode;
1711
+ return {
1712
+ schema: "2.0",
1713
+ config: {
1714
+ update_multi: true,
1715
+ width_mode: "fill",
1716
+ style: {
1717
+ text_size: { "cus-0": {
1718
+ default: "notation",
1719
+ pc: "notation",
1720
+ mobile: "notation"
1721
+ } },
1722
+ color: { foot_gray: {
1723
+ light_mode: "rgba(100,106,115,1)",
1724
+ dark_mode: "rgba(182,188,196,1)"
1725
+ } }
1726
+ }
1727
+ },
1728
+ body: {
1729
+ direction: "vertical",
1730
+ padding: "10px 12px 10px 12px",
1731
+ elements: [
1732
+ {
1733
+ tag: "markdown",
1734
+ content: "**权限模式**"
1735
+ },
1736
+ {
1737
+ tag: "markdown",
1738
+ content: `全局默认: ${this.escapeCardMarkdown(this.formatAccessMode(params.defaultMode))}`
1739
+ },
1740
+ {
1741
+ tag: "markdown",
1742
+ content: `本会话覆盖: ${this.escapeCardMarkdown(overrideText)}${inheritActive ? " ✅" : ""}`
1743
+ },
1744
+ {
1745
+ tag: "markdown",
1746
+ content: `当前生效: ${this.escapeCardMarkdown(this.formatAccessMode(currentMode))}(${this.escapeCardMarkdown(sourceText)}) ✅`
1747
+ },
1748
+ ...params.readonly ? [{
1749
+ tag: "markdown",
1750
+ content: params.readonlyReason ?? "本卡已应用变更,按钮已锁定。"
1751
+ }] : [{
1752
+ tag: "column_set",
1753
+ columns: [
1754
+ this.buildActionButton(fullActive ? "本会话 Full Access(当前)" : "本会话 Full Access", fullActive ? "primary" : "default", {
1755
+ type: "access",
1756
+ cardId: params.cardId,
1757
+ chatId: params.chatId,
1758
+ action: "set",
1759
+ mode: "full-access"
1760
+ }),
1761
+ this.buildActionButton(standardActive ? "本会话 Standard(当前)" : "本会话 Standard", standardActive ? "primary" : "default", {
1762
+ type: "access",
1763
+ cardId: params.cardId,
1764
+ chatId: params.chatId,
1765
+ action: "set",
1766
+ mode: "standard"
1767
+ }),
1768
+ this.buildActionButton(inheritActive ? "恢复默认(当前)" : "恢复默认", inheritActive ? "primary" : "default", {
1769
+ type: "access",
1770
+ cardId: params.cardId,
1771
+ chatId: params.chatId,
1772
+ action: "clear"
1773
+ })
1774
+ ]
1775
+ }],
1776
+ {
1777
+ tag: "div",
1778
+ text: {
1779
+ tag: "plain_text",
1780
+ content: "提示:会话覆盖仅当前会话有效,切换/重置会话后恢复默认。",
1781
+ text_size: "cus-0"
1782
+ }
1783
+ }
1784
+ ]
1785
+ }
1786
+ };
1787
+ }
1788
+ buildModelCard(params) {
1789
+ const modelRows = [];
1790
+ for (let index = 0; index < params.models.length; index += 3) {
1791
+ const items = params.models.slice(index, index + 3);
1792
+ modelRows.push({
1793
+ tag: "column_set",
1794
+ columns: items.map((model) => this.buildActionButton(model.id === params.currentModel ? `${model.name}(当前)` : model.name, model.id === params.currentModel ? "primary" : "default", {
1795
+ type: "model",
1796
+ cardId: params.cardId,
1797
+ chatId: params.chatId,
1798
+ model: model.id
1799
+ }))
1800
+ });
1801
+ }
1802
+ return {
1803
+ schema: "2.0",
1804
+ config: {
1805
+ update_multi: true,
1806
+ width_mode: "fill"
1807
+ },
1808
+ body: {
1809
+ direction: "vertical",
1810
+ padding: "10px 12px 10px 12px",
1811
+ elements: [
1812
+ {
1813
+ tag: "markdown",
1814
+ content: "**模型切换**"
1815
+ },
1816
+ {
1817
+ tag: "markdown",
1818
+ content: `当前模型: ${this.escapeCardMarkdown(params.currentModel ?? "未设置")}`
1819
+ },
1820
+ {
1821
+ tag: "markdown",
1822
+ content: `可选模型: ${this.escapeCardMarkdown(params.models.map((item) => item.id).join(", "))}`
1823
+ },
1824
+ ...params.readonly ? [{
1825
+ tag: "markdown",
1826
+ content: params.readonlyReason ?? "本卡已应用变更,按钮已锁定。"
1827
+ }] : [...modelRows],
1828
+ {
1829
+ tag: "div",
1830
+ text: {
1831
+ tag: "plain_text",
1832
+ content: "提示:切换模型会重置当前会话,后续消息使用新模型。",
1833
+ text_size: "cus-0"
1834
+ }
1835
+ }
1836
+ ]
1837
+ }
1838
+ };
1839
+ }
1840
+ buildApprovalCard(request, status, decision) {
1841
+ const statusText = status === "pending" ? "待审批" : status === "approved" ? "已批准" : status === "rejected" ? "已拒绝" : "已超时";
1842
+ const elements = [
1843
+ {
1844
+ tag: "markdown",
1845
+ content: `**状态:${this.escapeCardMarkdown(statusText)}**`
1846
+ },
1847
+ {
1848
+ tag: "markdown",
1849
+ content: `类型: ${this.escapeCardMarkdown(request.kind)} | 风险: ${this.escapeCardMarkdown(request.riskLevel)}`
1850
+ },
1851
+ {
1852
+ tag: "markdown",
1853
+ content: `标题: ${this.escapeCardMarkdown(request.title)}`
1854
+ },
1855
+ {
1856
+ tag: "markdown",
1857
+ content: `工作目录: ${this.escapeCardMarkdown(request.cwd)}`
1858
+ }
1859
+ ];
1860
+ if (request.command) elements.push({
1861
+ tag: "markdown",
1862
+ content: `命令: \`${this.escapeCardMarkdown(this.shorten(request.command, 140))}\``
1863
+ });
1864
+ if (request.target) elements.push({
1865
+ tag: "markdown",
1866
+ content: `目标: ${this.escapeCardMarkdown(request.target)}`
1867
+ });
1868
+ if (status === "pending") elements.push({
1869
+ tag: "column_set",
1870
+ columns: [
1871
+ this.buildApprovalButton("批准", "primary", {
1872
+ requestId: request.id,
1873
+ taskId: request.taskId,
1874
+ decision: "approved"
1875
+ }),
1876
+ this.buildApprovalButton("本会话允许", "default", {
1877
+ requestId: request.id,
1878
+ taskId: request.taskId,
1879
+ decision: "approved",
1880
+ comment: "approved-for-session"
1881
+ }),
1882
+ this.buildApprovalButton("拒绝", "danger", {
1883
+ requestId: request.id,
1884
+ taskId: request.taskId,
1885
+ decision: "rejected"
1886
+ })
1887
+ ]
1888
+ });
1889
+ else if (decision) elements.push({
1890
+ tag: "markdown",
1891
+ content: `操作人: ${this.escapeCardMarkdown(decision.decidedBy)}\\n时间: ${this.escapeCardMarkdown(decision.decidedAt)}`
1892
+ });
1893
+ return {
1894
+ schema: "2.0",
1895
+ config: {
1896
+ update_multi: true,
1897
+ width_mode: "fill"
1898
+ },
1899
+ body: {
1900
+ direction: "vertical",
1901
+ padding: "10px 12px 10px 12px",
1902
+ elements
1903
+ }
1904
+ };
1905
+ }
1906
+ parseToolChunk(chunk) {
1907
+ const lines = chunk.split("\n").map((line) => line.trim()).filter(Boolean);
1908
+ if (lines.length === 0 || !lines[0]?.startsWith("[tool]")) return;
1909
+ const headMatch = lines[0].match(/^\[tool\]\s+(.+?)(?:\s+\(([^)]+)\))?$/);
1910
+ if (!headMatch?.[1]) return;
1911
+ const tool = {
1912
+ id: headMatch[1].trim(),
1913
+ status: headMatch[2]?.trim()
1914
+ };
1915
+ for (const line of lines.slice(1)) {
1916
+ if (line.startsWith("cmd:")) {
1917
+ tool.cmd = line.slice(4).trim();
1918
+ continue;
1919
+ }
1920
+ if (line.startsWith("path:")) {
1921
+ tool.path = line.slice(5).trim();
1922
+ continue;
1923
+ }
1924
+ if (line.startsWith("error:")) tool.error = line.slice(6).trim();
1925
+ }
1926
+ return tool;
1927
+ }
1928
+ toToolView(update) {
1929
+ if (!update.toolCallId) return;
1930
+ const cmd = update.command ?? update.query ?? update.url ?? update.title ?? update.toolName;
1931
+ return {
1932
+ id: update.toolCallId,
1933
+ name: update.toolName,
1934
+ status: update.status,
1935
+ cmd,
1936
+ query: update.query,
1937
+ url: update.url,
1938
+ path: update.path,
1939
+ error: update.error
1940
+ };
1941
+ }
1942
+ buildContentElements(segments, tools) {
1943
+ const elements = [];
1944
+ const maxToolCards = 12;
1945
+ let renderedToolCards = 0;
1946
+ let hiddenToolCards = 0;
1947
+ let lastToolPath;
1948
+ let lastToolSignature;
1949
+ let index = 0;
1950
+ while (index < segments.length) {
1951
+ const segment = segments[index];
1952
+ if (!segment) break;
1953
+ if (segment.type === "text") {
1954
+ if (!segment.content) {
1955
+ index += 1;
1956
+ continue;
1957
+ }
1958
+ elements.push({
1959
+ tag: "markdown",
1960
+ content: this.escapeCardMarkdown(segment.content)
1961
+ });
1962
+ index += 1;
1963
+ continue;
1964
+ }
1965
+ const toolCards = [];
1966
+ while (index < segments.length && segments[index]?.type === "tool") {
1967
+ const current = segments[index];
1968
+ const tool = tools.get(current.id);
1969
+ if (!tool) {
1970
+ index += 1;
1971
+ continue;
1972
+ }
1973
+ const statusText = this.formatToolStatus(tool.status);
1974
+ const statusBadge = this.getToolStatusBadge(tool.status);
1975
+ const cmdText = tool.cmd ? this.shorten(tool.cmd, 140) : "(无命令)";
1976
+ const pathText = tool.path ? this.shorten(tool.path, 90) : void 0;
1977
+ const errorText = tool.error ? this.shorten(tool.error, 160) : void 0;
1978
+ const title = tool.name ?? this.getToolDisplayTitle(tool.cmd);
1979
+ const signature = `${statusText}|${cmdText}|${pathText ?? ""}|${errorText ?? ""}`;
1980
+ if (signature === lastToolSignature) {
1981
+ index += 1;
1982
+ continue;
1983
+ }
1984
+ lastToolSignature = signature;
1985
+ if (renderedToolCards >= maxToolCards) {
1986
+ hiddenToolCards += 1;
1987
+ index += 1;
1988
+ continue;
1989
+ }
1990
+ const cardElements = [
1991
+ {
1992
+ tag: "div",
1993
+ text: {
1994
+ tag: "plain_text",
1995
+ content: `${statusBadge} ${title}`
1996
+ }
1997
+ },
1998
+ {
1999
+ tag: "div",
2000
+ text: {
2001
+ tag: "plain_text",
2002
+ content: cmdText
2003
+ }
2004
+ },
2005
+ {
2006
+ tag: "markdown",
2007
+ content: `<font color='grey'>状态:${this.escapeCardMarkdown(statusText)}</font>`
2008
+ }
2009
+ ];
2010
+ if (pathText && pathText !== lastToolPath) {
2011
+ cardElements.push({
2012
+ tag: "markdown",
2013
+ content: `<font color='grey'>↳ ${this.escapeCardMarkdown(pathText)}</font>`
2014
+ });
2015
+ lastToolPath = pathText;
2016
+ }
2017
+ if (errorText) cardElements.push({
2018
+ tag: "div",
2019
+ text: {
2020
+ tag: "plain_text",
2021
+ content: `❗ ${errorText}`,
2022
+ text_size: "cus-1"
2023
+ }
2024
+ });
2025
+ toolCards.push({
2026
+ tag: "interactive_container",
2027
+ has_border: true,
2028
+ corner_radius: "10px",
2029
+ padding: "8px 10px 8px 10px",
2030
+ margin: "4px 0 4px 0",
2031
+ elements: cardElements
2032
+ });
2033
+ renderedToolCards += 1;
2034
+ index += 1;
2035
+ }
2036
+ if (toolCards.length > 0) elements.push({
2037
+ tag: "collapsible_panel",
2038
+ header: { title: {
2039
+ tag: "plain_text",
2040
+ content: `🛠️ 工具调用(${toolCards.length})`
2041
+ } },
2042
+ elements: toolCards
2043
+ });
2044
+ }
2045
+ if (hiddenToolCards > 0) elements.push({
2046
+ tag: "markdown",
2047
+ content: `…已折叠 ${hiddenToolCards} 条工具调用`
2048
+ });
2049
+ return elements;
2050
+ }
2051
+ buildApprovalButton(label, type, value) {
2052
+ return {
2053
+ tag: "column",
2054
+ width: "weighted",
2055
+ weight: 1,
2056
+ elements: [{
2057
+ tag: "button",
2058
+ type,
2059
+ text: {
2060
+ tag: "plain_text",
2061
+ content: label
2062
+ },
2063
+ behaviors: [{
2064
+ type: "callback",
2065
+ value
2066
+ }]
2067
+ }]
2068
+ };
2069
+ }
2070
+ buildActionButton(label, type, value) {
2071
+ return {
2072
+ tag: "column",
2073
+ width: "weighted",
2074
+ weight: 1,
2075
+ elements: [{
2076
+ tag: "button",
2077
+ type,
2078
+ text: {
2079
+ tag: "plain_text",
2080
+ content: label
2081
+ },
2082
+ behaviors: [{
2083
+ type: "callback",
2084
+ value
2085
+ }]
2086
+ }]
2087
+ };
2088
+ }
2089
+ formatToolStatus(status) {
2090
+ if (!status) return "处理中";
2091
+ if (status === "in_progress") return "进行中";
2092
+ if (status === "completed") return "已完成";
2093
+ if (status === "failed") return "失败";
2094
+ return status;
2095
+ }
2096
+ getToolStatusBadge(_status) {
2097
+ return "🛠️";
2098
+ }
2099
+ getToolDisplayTitle(cmd) {
2100
+ if (!cmd) return "步骤";
2101
+ const head = cmd.trim().split(/\s+/, 1)[0]?.toLowerCase();
2102
+ if (!head) return "步骤";
2103
+ if (head === "cat" || head === "sed" || head === "head" || head === "tail") return "读取";
2104
+ if (head === "ls" || head === "find" || head === "rg" || head === "grep") return "检索";
2105
+ if (head === "git") return "Git";
2106
+ if (head === "npm" || head === "pnpm" || head === "vp") return "命令";
2107
+ return "步骤";
2108
+ }
2109
+ formatAccessMode(mode) {
2110
+ return mode === "full-access" ? "Full Access" : "Standard";
2111
+ }
2112
+ shorten(text, maxLen) {
2113
+ if (text.length <= maxLen) return text;
2114
+ return `${text.slice(0, maxLen)}...`;
2115
+ }
2116
+ escapeCardMarkdown(text) {
2117
+ return text.replace(/\r/g, "").replace(/\\/g, "\\\\").replace(/`/g, "\\`");
2118
+ }
2119
+ };
2120
+ //#endregion
2121
+ //#region src/feishu/command-handler.ts
2122
+ var FeishuCommandHandler = class {
2123
+ constructor(deps) {
2124
+ this.deps = deps;
2125
+ }
2126
+ async handle(params) {
2127
+ const { chatId, workspace, command } = params;
2128
+ if (command.type === "prompt") return {
2129
+ type: "prompt",
2130
+ prompt: command.prompt
2131
+ };
2132
+ if (command.type === "help") {
2133
+ await this.deps.messageClient.sendText(chatId, [
2134
+ "支持指令:",
2135
+ "/help - 展示指令说明",
2136
+ "/new [path] - 新建会话,可选切换工作目录",
2137
+ "/model - 查看并切换当前模型",
2138
+ "/model <name> - 直接切换到指定模型",
2139
+ "/status - 查看当前会话状态",
2140
+ "/stop - 打断当前执行中的任务",
2141
+ "/perm - 查看并切换权限模式"
2142
+ ].join("\n"));
2143
+ return { type: "handled" };
2144
+ }
2145
+ if (command.type === "new") {
2146
+ const next = await this.deps.sessionController.startNewConversation({
2147
+ chatId,
2148
+ workspace,
2149
+ agent: "codex",
2150
+ cwd: command.cwd
2151
+ }, this.deps.taskRunner);
2152
+ await this.deps.messageClient.sendText(chatId, `已切换到新会话,session_id: ${next.sessionId}\n工作目录:${next.workspace.cwd}`);
2153
+ return { type: "handled" };
2154
+ }
2155
+ if (command.type === "status") {
2156
+ await this.deps.messageClient.sendText(chatId, [
2157
+ `session_id: ${this.deps.sessionController.getBridgeSessionId(chatId) ?? "未创建"}`,
2158
+ `codex_session_id: ${this.deps.sessionController.getResumeSessionId(chatId) ?? "未创建"}`,
2159
+ `工具目录(cwd): ${workspace.cwd}`,
2160
+ `当前模型: ${this.deps.sessionController.getModel(chatId) ?? "未设置"}`,
2161
+ `权限模式: ${this.deps.sessionController.getAccessMode(chatId)}`
2162
+ ].join("\n"));
2163
+ return { type: "handled" };
2164
+ }
2165
+ if (command.type === "stop") {
2166
+ const interrupted = await this.deps.sessionController.interrupt(chatId, this.deps.taskRunner);
2167
+ await this.deps.messageClient.sendText(chatId, interrupted ? "已打断当前任务。" : "当前没有执行中的任务。");
2168
+ return { type: "handled" };
2169
+ }
2170
+ if (command.type === "show-access") {
2171
+ await this.deps.cardActionHandler.sendPermissionCard(chatId);
2172
+ return { type: "handled" };
2173
+ }
2174
+ if (command.type === "model") {
2175
+ if (!command.model) {
2176
+ if (this.deps.sessionController.getAvailableModels(chatId).length === 0) await this.deps.sessionController.startNewConversation({
2177
+ chatId,
2178
+ workspace,
2179
+ agent: "codex"
2180
+ }, this.deps.taskRunner);
2181
+ await this.deps.cardActionHandler.sendModelCard(chatId);
2182
+ return { type: "handled" };
2183
+ }
2184
+ if (this.deps.sessionController.getAvailableModels(chatId).length === 0) await this.deps.sessionController.startNewConversation({
2185
+ chatId,
2186
+ workspace,
2187
+ agent: "codex"
2188
+ }, this.deps.taskRunner);
2189
+ const resolvedModels = this.deps.sessionController.getAvailableModels(chatId);
2190
+ if (!resolvedModels.some((item) => item.id === command.model)) {
2191
+ await this.deps.messageClient.sendText(chatId, `不支持的模型:${command.model}\n可用模型:${resolvedModels.map((item) => item.id).join(", ")}`);
2192
+ return { type: "handled" };
2193
+ }
2194
+ const changed = await this.deps.sessionController.setModel(chatId, command.model, this.deps.taskRunner);
2195
+ await this.deps.messageClient.sendText(chatId, changed ? `已切换模型:${this.deps.sessionController.getModel(chatId)}` : `当前模型已是:${this.deps.sessionController.getModel(chatId)}`);
2196
+ return { type: "handled" };
2197
+ }
2198
+ return { type: "handled" };
2199
+ }
2200
+ };
2201
+ //#endregion
2202
+ //#region src/feishu/feishu-event-dispatcher.ts
2203
+ function buildFeishuEventDispatcher(deps) {
2204
+ const dispatcher = new lark.EventDispatcher({}).register({ "im.message.receive_v1": async (data) => {
2205
+ await deps.onMessageReceived(data);
2206
+ } });
2207
+ registerLoosely(dispatcher, { "card.action.trigger": async (data) => {
2208
+ return await deps.onCardAction(data) ?? {};
2209
+ } });
2210
+ return dispatcher;
2211
+ }
2212
+ function registerLoosely(dispatcher, handles) {
2213
+ dispatcher.register(handles);
2214
+ }
2215
+ //#endregion
2216
+ //#region src/feishu/message-entry-queue.ts
2217
+ var MessageEntryQueue = class {
2218
+ #processingMessageIds = /* @__PURE__ */ new Set();
2219
+ #handledMessageIds = /* @__PURE__ */ new Map();
2220
+ #chatQueues = /* @__PURE__ */ new Map();
2221
+ constructor(dedupeTtlMs = 600 * 1e3) {
2222
+ this.dedupeTtlMs = dedupeTtlMs;
2223
+ }
2224
+ gcHandledMessageIds() {
2225
+ const now = Date.now();
2226
+ for (const [messageId, timestamp] of this.#handledMessageIds.entries()) if (now - timestamp > this.dedupeTtlMs) this.#handledMessageIds.delete(messageId);
2227
+ }
2228
+ tryBegin(messageId) {
2229
+ if (this.#handledMessageIds.has(messageId) || this.#processingMessageIds.has(messageId)) return false;
2230
+ this.#processingMessageIds.add(messageId);
2231
+ return true;
2232
+ }
2233
+ runInChatQueue(chatId, messageId, task) {
2234
+ if (!this.tryBegin(messageId)) return;
2235
+ const queue = (this.#chatQueues.get(chatId) ?? Promise.resolve()).catch(() => {}).then(async () => {
2236
+ await task();
2237
+ });
2238
+ this.#chatQueues.set(chatId, queue);
2239
+ queue.finally(() => {
2240
+ this.complete(messageId);
2241
+ if (this.#chatQueues.get(chatId) === queue) this.#chatQueues.delete(chatId);
2242
+ }).catch(() => void 0);
2243
+ return queue;
2244
+ }
2245
+ complete(messageId) {
2246
+ this.#processingMessageIds.delete(messageId);
2247
+ this.#handledMessageIds.set(messageId, Date.now());
2248
+ }
2249
+ };
2250
+ //#endregion
2251
+ //#region src/feishu/feishu-card-action-handler.ts
2252
+ var FeishuCardActionHandler = class {
2253
+ #permissionCardMessages = /* @__PURE__ */ new Map();
2254
+ #permissionCardRevisions = /* @__PURE__ */ new Map();
2255
+ #permissionCardUpdatedAt = /* @__PURE__ */ new Map();
2256
+ #handledPermissionCardIds = /* @__PURE__ */ new Map();
2257
+ #modelCardMessages = /* @__PURE__ */ new Map();
2258
+ #modelCardUpdatedAt = /* @__PURE__ */ new Map();
2259
+ #handledModelCardIds = /* @__PURE__ */ new Map();
2260
+ #handledCardActionEventIds = /* @__PURE__ */ new Map();
2261
+ constructor(deps) {
2262
+ this.deps = deps;
2263
+ }
2264
+ async sendPermissionCard(chatId) {
2265
+ this.gcPermissionCardState();
2266
+ const cardId = randomUUID();
2267
+ const version = 1;
2268
+ this.#permissionCardRevisions.set(cardId, version);
2269
+ const card = this.deps.cardRenderer.buildAccessModeCard({
2270
+ cardId,
2271
+ chatId,
2272
+ defaultMode: this.deps.sessionController.getDefaultAccessMode(),
2273
+ overrideMode: this.deps.sessionController.getAccessOverride(chatId),
2274
+ readonly: false
2275
+ });
2276
+ const messageId = await this.deps.messageClient.sendCard(chatId, card);
2277
+ this.#permissionCardMessages.set(cardId, messageId);
2278
+ this.#permissionCardUpdatedAt.set(cardId, Date.now());
2279
+ this.deps.logger.info("permission card sent", {
2280
+ chatId,
2281
+ cardId,
2282
+ version,
2283
+ messageId,
2284
+ defaultMode: this.deps.sessionController.getDefaultAccessMode(),
2285
+ overrideMode: this.deps.sessionController.getAccessOverride(chatId),
2286
+ effectiveMode: this.deps.sessionController.getAccessMode(chatId)
2287
+ });
2288
+ }
2289
+ async sendModelCard(chatId) {
2290
+ this.gcModelCardState();
2291
+ const cardId = randomUUID();
2292
+ const card = this.deps.cardRenderer.buildModelCard({
2293
+ cardId,
2294
+ chatId,
2295
+ currentModel: this.deps.sessionController.getModel(chatId),
2296
+ models: this.deps.sessionController.getAvailableModels(chatId),
2297
+ readonly: false
2298
+ });
2299
+ const messageId = await this.deps.messageClient.sendCard(chatId, card);
2300
+ this.#modelCardMessages.set(cardId, messageId);
2301
+ this.#modelCardUpdatedAt.set(cardId, Date.now());
2302
+ this.deps.logger.info("model card sent", {
2303
+ chatId,
2304
+ cardId,
2305
+ messageId,
2306
+ currentModel: this.deps.sessionController.getModel(chatId),
2307
+ models: this.deps.sessionController.getAvailableModels(chatId).map((item) => item.id)
2308
+ });
2309
+ }
2310
+ async handleCardAction(data) {
2311
+ this.deps.logger.info("card action received", {
2312
+ topLevelKeys: Object.keys(data),
2313
+ eventId: this.extractCardActionEventId(data),
2314
+ actionMessageId: this.extractActionMessageId(data)
2315
+ });
2316
+ this.gcHandledCardActionEventIds();
2317
+ const eventId = this.extractCardActionEventId(data);
2318
+ if (eventId && this.#handledCardActionEventIds.has(eventId)) {
2319
+ this.deps.logger.info("card action ignored: duplicated event", { eventId });
2320
+ return;
2321
+ }
2322
+ if (eventId) this.#handledCardActionEventIds.set(eventId, Date.now());
2323
+ const action = parseCardActionValue(data);
2324
+ if (!action) {
2325
+ this.deps.logger.warn("card action ignored: invalid payload");
2326
+ return;
2327
+ }
2328
+ if (action.type === "access") return this.handleAccessAction(action, data);
2329
+ if (action.type === "model") return this.handleModelAction(action, data);
2330
+ const decision = {
2331
+ requestId: action.requestId,
2332
+ taskId: action.taskId,
2333
+ decision: action.decision,
2334
+ comment: action.comment,
2335
+ decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
2336
+ decidedBy: this.extractOperatorId(data)
2337
+ };
2338
+ this.deps.approvalGateway.resolve(decision);
2339
+ this.deps.onApprovalResolved(decision);
2340
+ }
2341
+ async handleModelAction(action, data) {
2342
+ this.gcHandledModelCardIds();
2343
+ const chatId = action.chatId;
2344
+ const actionMessageId = this.extractActionMessageId(data);
2345
+ const availableModels = this.deps.sessionController.getAvailableModels(chatId);
2346
+ if (availableModels.length > 0 && !availableModels.some((item) => item.id === action.model)) {
2347
+ this.deps.logger.warn("model card action ignored: unsupported model", {
2348
+ chatId,
2349
+ cardId: action.cardId,
2350
+ model: action.model
2351
+ });
2352
+ return;
2353
+ }
2354
+ if (this.#handledModelCardIds.has(action.cardId)) {
2355
+ this.deps.logger.info("model card action ignored: card already locked", {
2356
+ chatId,
2357
+ cardId: action.cardId,
2358
+ messageId: actionMessageId
2359
+ });
2360
+ return;
2361
+ }
2362
+ this.#handledModelCardIds.set(action.cardId, Date.now());
2363
+ try {
2364
+ await this.deps.sessionController.setModel(chatId, action.model, this.deps.taskRunner);
2365
+ this.deps.logger.info("model card action applied", {
2366
+ chatId,
2367
+ cardId: action.cardId,
2368
+ model: action.model
2369
+ });
2370
+ } catch (error) {
2371
+ this.#handledModelCardIds.delete(action.cardId);
2372
+ this.deps.logger.warn("model card action failed, unlock card", {
2373
+ chatId,
2374
+ cardId: action.cardId,
2375
+ error: error instanceof Error ? error.message : String(error)
2376
+ });
2377
+ throw error;
2378
+ }
2379
+ const card = await this.updateModelCard(chatId, actionMessageId, action.cardId, true, false);
2380
+ if (!card) return;
2381
+ return { card: {
2382
+ type: "raw",
2383
+ data: card
2384
+ } };
2385
+ }
2386
+ async handleAccessAction(action, data) {
2387
+ this.gcHandledPermissionCardIds();
2388
+ const chatId = action.chatId;
2389
+ const actionMessageId = this.extractActionMessageId(data);
2390
+ this.deps.logger.info("permission card action parsed", {
2391
+ chatId,
2392
+ cardId: action.cardId,
2393
+ action: action.action,
2394
+ mode: action.action === "set" ? action.mode : void 0,
2395
+ actionMessageId
2396
+ });
2397
+ if (this.#handledPermissionCardIds.has(action.cardId)) {
2398
+ this.deps.logger.info("permission card action ignored: card already locked", {
2399
+ chatId,
2400
+ messageId: actionMessageId,
2401
+ cardId: action.cardId
2402
+ });
2403
+ return;
2404
+ }
2405
+ this.#handledPermissionCardIds.set(action.cardId, Date.now());
2406
+ try {
2407
+ if (action.action === "clear") await this.deps.sessionController.clearAccessOverride(chatId, this.deps.taskRunner);
2408
+ else await this.deps.sessionController.setAccessMode(chatId, action.mode, this.deps.taskRunner);
2409
+ this.deps.logger.info("permission card action applied", {
2410
+ chatId,
2411
+ cardId: action.cardId,
2412
+ effectiveMode: this.deps.sessionController.getAccessMode(chatId)
2413
+ });
2414
+ } catch (error) {
2415
+ this.#handledPermissionCardIds.delete(action.cardId);
2416
+ this.deps.logger.warn("permission card action failed, unlock card", {
2417
+ chatId,
2418
+ cardId: action.cardId,
2419
+ error: error instanceof Error ? error.message : String(error)
2420
+ });
2421
+ throw error;
2422
+ }
2423
+ const card = await this.updatePermissionCard(chatId, actionMessageId, action.cardId, true, void 0, false);
2424
+ if (!card) {
2425
+ this.deps.logger.warn("permission card action applied but card update skipped", {
2426
+ chatId,
2427
+ cardId: action.cardId,
2428
+ actionMessageId
2429
+ });
2430
+ return;
2431
+ }
2432
+ return { card: {
2433
+ type: "raw",
2434
+ data: card
2435
+ } };
2436
+ }
2437
+ async updatePermissionCard(chatId, messageId, cardId, lockAfterPatch = false, readonlyReason, patch = true) {
2438
+ this.gcPermissionCardState();
2439
+ const resolvedCardId = cardId ?? randomUUID();
2440
+ const nextRevision = (this.#permissionCardRevisions.get(resolvedCardId) ?? 1) + 1;
2441
+ this.#permissionCardRevisions.set(resolvedCardId, nextRevision);
2442
+ this.#permissionCardUpdatedAt.set(resolvedCardId, Date.now());
2443
+ const card = this.deps.cardRenderer.buildAccessModeCard({
2444
+ cardId: resolvedCardId,
2445
+ chatId,
2446
+ defaultMode: this.deps.sessionController.getDefaultAccessMode(),
2447
+ overrideMode: this.deps.sessionController.getAccessOverride(chatId),
2448
+ readonly: lockAfterPatch,
2449
+ readonlyReason
2450
+ });
2451
+ const fromActionMessageId = messageId;
2452
+ const fromCardMap = this.#permissionCardMessages.get(resolvedCardId);
2453
+ const targetMessageId = fromActionMessageId ?? fromCardMap;
2454
+ if (!targetMessageId) {
2455
+ this.deps.logger.warn("permission card update skipped: message id not found", {
2456
+ chatId,
2457
+ cardId: resolvedCardId
2458
+ });
2459
+ return;
2460
+ }
2461
+ this.deps.logger.info("permission card patch start", {
2462
+ chatId,
2463
+ cardId: resolvedCardId,
2464
+ version: nextRevision,
2465
+ targetMessageId,
2466
+ targetSource: fromActionMessageId ? "action-message-id" : "card-id-map",
2467
+ lockAfterPatch,
2468
+ overrideMode: this.deps.sessionController.getAccessOverride(chatId),
2469
+ effectiveMode: this.deps.sessionController.getAccessMode(chatId)
2470
+ });
2471
+ try {
2472
+ this.#permissionCardMessages.set(resolvedCardId, targetMessageId);
2473
+ if (patch) {
2474
+ await this.deps.messageClient.patchCard(targetMessageId, card);
2475
+ this.deps.logger.info("permission card patch done", {
2476
+ chatId,
2477
+ cardId: resolvedCardId,
2478
+ version: nextRevision,
2479
+ targetMessageId,
2480
+ lockAfterPatch
2481
+ });
2482
+ } else this.deps.logger.info("permission card patch skipped: callback response mode", {
2483
+ chatId,
2484
+ cardId: resolvedCardId,
2485
+ version: nextRevision,
2486
+ targetMessageId,
2487
+ lockAfterPatch
2488
+ });
2489
+ return card;
2490
+ } catch (error) {
2491
+ this.deps.logger.warn("patch permission card failed", {
2492
+ chatId,
2493
+ messageId: targetMessageId,
2494
+ error: error instanceof Error ? error.message : String(error)
2495
+ });
2496
+ return;
2497
+ }
2498
+ }
2499
+ async updateModelCard(chatId, messageId, cardId, lockAfterPatch = false, patch = true) {
2500
+ this.gcModelCardState();
2501
+ const resolvedCardId = cardId ?? randomUUID();
2502
+ this.#modelCardUpdatedAt.set(resolvedCardId, Date.now());
2503
+ const card = this.deps.cardRenderer.buildModelCard({
2504
+ cardId: resolvedCardId,
2505
+ chatId,
2506
+ currentModel: this.deps.sessionController.getModel(chatId),
2507
+ models: this.deps.sessionController.getAvailableModels(chatId),
2508
+ readonly: lockAfterPatch
2509
+ });
2510
+ const targetMessageId = messageId ?? this.#modelCardMessages.get(resolvedCardId);
2511
+ if (!targetMessageId) {
2512
+ this.deps.logger.warn("model card update skipped: message id not found", {
2513
+ chatId,
2514
+ cardId: resolvedCardId
2515
+ });
2516
+ return;
2517
+ }
2518
+ try {
2519
+ this.#modelCardMessages.set(resolvedCardId, targetMessageId);
2520
+ if (patch) await this.deps.messageClient.patchCard(targetMessageId, card);
2521
+ return card;
2522
+ } catch (error) {
2523
+ this.deps.logger.warn("patch model card failed", {
2524
+ chatId,
2525
+ messageId: targetMessageId,
2526
+ error: error instanceof Error ? error.message : String(error)
2527
+ });
2528
+ return;
2529
+ }
2530
+ }
2531
+ extractOperatorId(data) {
2532
+ const operator = data.operator && typeof data.operator === "object" ? data.operator : void 0;
2533
+ const operatorId = operator?.operator_id && typeof operator.operator_id === "object" ? operator.operator_id : void 0;
2534
+ if (typeof operatorId?.open_id === "string") return operatorId.open_id;
2535
+ if (typeof operatorId?.user_id === "string") return operatorId.user_id;
2536
+ return "feishu-user";
2537
+ }
2538
+ extractActionMessageId(data) {
2539
+ const topLevel = typeof data.open_message_id === "string" ? data.open_message_id : void 0;
2540
+ if (topLevel) return topLevel;
2541
+ const eventObj = data.event && typeof data.event === "object" ? data.event : void 0;
2542
+ if (eventObj) {
2543
+ if (typeof eventObj.open_message_id === "string") return eventObj.open_message_id;
2544
+ const contextObj = eventObj.context && typeof eventObj.context === "object" ? eventObj.context : void 0;
2545
+ if (contextObj && typeof contextObj.open_message_id === "string") return contextObj.open_message_id;
2546
+ const messageObj = eventObj.message && typeof eventObj.message === "object" ? eventObj.message : void 0;
2547
+ if (messageObj && typeof messageObj.message_id === "string") return messageObj.message_id;
2548
+ }
2549
+ const messageObj = data.message && typeof data.message === "object" ? data.message : void 0;
2550
+ if (messageObj && typeof messageObj.message_id === "string") return messageObj.message_id;
2551
+ }
2552
+ extractCardActionEventId(data) {
2553
+ if (typeof data.event_id === "string") return data.event_id;
2554
+ const headerObj = data.header && typeof data.header === "object" ? data.header : void 0;
2555
+ if (headerObj && typeof headerObj.event_id === "string") return headerObj.event_id;
2556
+ const eventObj = data.event && typeof data.event === "object" ? data.event : void 0;
2557
+ if (eventObj && typeof eventObj.event_id === "string") return eventObj.event_id;
2558
+ }
2559
+ gcHandledCardActionEventIds() {
2560
+ const expireMs = 6e4;
2561
+ const now = Date.now();
2562
+ for (const [eventId, ts] of this.#handledCardActionEventIds.entries()) if (now - ts > expireMs) this.#handledCardActionEventIds.delete(eventId);
2563
+ }
2564
+ gcHandledPermissionCardIds() {
2565
+ const expireMs = 10 * 6e4;
2566
+ const now = Date.now();
2567
+ for (const [cardId, ts] of this.#handledPermissionCardIds.entries()) if (now - ts > expireMs) this.#handledPermissionCardIds.delete(cardId);
2568
+ }
2569
+ gcHandledModelCardIds() {
2570
+ const expireMs = 10 * 6e4;
2571
+ const now = Date.now();
2572
+ for (const [cardId, ts] of this.#handledModelCardIds.entries()) if (now - ts > expireMs) this.#handledModelCardIds.delete(cardId);
2573
+ }
2574
+ gcPermissionCardState() {
2575
+ const expireMs = 1440 * 60 * 1e3;
2576
+ const now = Date.now();
2577
+ for (const [cardId, ts] of this.#permissionCardUpdatedAt.entries()) if (now - ts > expireMs) {
2578
+ this.#permissionCardUpdatedAt.delete(cardId);
2579
+ this.#permissionCardRevisions.delete(cardId);
2580
+ this.#permissionCardMessages.delete(cardId);
2581
+ }
2582
+ }
2583
+ gcModelCardState() {
2584
+ const expireMs = 1440 * 60 * 1e3;
2585
+ const now = Date.now();
2586
+ for (const [cardId, ts] of this.#modelCardUpdatedAt.entries()) if (now - ts > expireMs) {
2587
+ this.#modelCardUpdatedAt.delete(cardId);
2588
+ this.#modelCardMessages.delete(cardId);
2589
+ }
2590
+ }
2591
+ };
2592
+ //#endregion
2593
+ //#region src/feishu/feishu-message-client.ts
2594
+ var FeishuMessageClient = class {
2595
+ constructor(client, logger) {
2596
+ this.client = client;
2597
+ this.logger = logger;
2598
+ }
2599
+ async sendText(chatId, text) {
2600
+ await this.client.im.v1.message.create({
2601
+ params: { receive_id_type: "chat_id" },
2602
+ data: {
2603
+ receive_id: chatId,
2604
+ msg_type: "text",
2605
+ content: JSON.stringify({ text })
2606
+ }
2607
+ });
2608
+ }
2609
+ async sendCard(chatId, card) {
2610
+ const messageId = (await this.client.im.v1.message.create({
2611
+ params: { receive_id_type: "chat_id" },
2612
+ data: {
2613
+ receive_id: chatId,
2614
+ msg_type: "interactive",
2615
+ content: JSON.stringify(card)
2616
+ }
2617
+ })).data?.message_id;
2618
+ if (!messageId) throw new Error("failed to create card message: missing message_id");
2619
+ return messageId;
2620
+ }
2621
+ async patchCard(messageId, card) {
2622
+ await this.client.im.v1.message.patch({
2623
+ path: { message_id: messageId },
2624
+ data: { content: JSON.stringify(card) }
2625
+ });
2626
+ }
2627
+ async addTypingReaction(messageId) {
2628
+ try {
2629
+ return (await this.client.im.v1.messageReaction.create({
2630
+ path: { message_id: messageId },
2631
+ data: { reaction_type: { emoji_type: "Typing" } }
2632
+ })).data?.reaction_id;
2633
+ } catch (error) {
2634
+ this.logger.warn("add typing reaction failed", {
2635
+ messageId,
2636
+ error: error instanceof Error ? error.message : String(error)
2637
+ });
2638
+ return;
2639
+ }
2640
+ }
2641
+ async removeTypingReaction(messageId, reactionId) {
2642
+ await this.client.im.v1.messageReaction.delete({ path: {
2643
+ message_id: messageId,
2644
+ reaction_id: reactionId
2645
+ } });
2646
+ }
2647
+ };
2648
+ //#endregion
2649
+ //#region src/session/session-state-store.ts
2650
+ var FileSessionStateStore = class {
2651
+ constructor(filePath = resolve(homedir(), ".im-code-agent", "chat-state.json")) {
2652
+ this.filePath = filePath;
2653
+ }
2654
+ async loadState() {
2655
+ const raw = await readFile(this.filePath, "utf8").catch(() => void 0);
2656
+ if (!raw) return {
2657
+ chatCwds: /* @__PURE__ */ new Map(),
2658
+ chatBridgeSessionIds: /* @__PURE__ */ new Map(),
2659
+ chatSessionIds: /* @__PURE__ */ new Map()
2660
+ };
2661
+ const parsed = JSON.parse(raw);
2662
+ return {
2663
+ chatCwds: new Map(Object.entries(parsed.chatCwds ?? {})),
2664
+ chatBridgeSessionIds: new Map(Object.entries(parsed.chatBridgeSessionIds ?? {})),
2665
+ chatSessionIds: new Map(Object.entries(parsed.chatSessionIds ?? {}))
2666
+ };
2667
+ }
2668
+ async saveState(state) {
2669
+ const payload = {
2670
+ chatCwds: Object.fromEntries(state.chatCwds.entries()),
2671
+ chatBridgeSessionIds: Object.fromEntries(state.chatBridgeSessionIds.entries()),
2672
+ chatSessionIds: Object.fromEntries(state.chatSessionIds.entries())
2673
+ };
2674
+ await mkdir(dirname(this.filePath), { recursive: true });
2675
+ const tmpPath = `${this.filePath}.tmp`;
2676
+ await writeFile(tmpPath, JSON.stringify(payload, null, 2), "utf8");
2677
+ await rename(tmpPath, this.filePath);
2678
+ }
2679
+ };
2680
+ //#endregion
2681
+ //#region src/session/session-manager.ts
2682
+ var SessionManager = class {
2683
+ #tasks = /* @__PURE__ */ new Map();
2684
+ createTask(input, workspace) {
2685
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2686
+ const task = {
2687
+ id: crypto.randomUUID(),
2688
+ workspaceId: workspace.id,
2689
+ agent: input.agent,
2690
+ prompt: input.prompt,
2691
+ cwd: workspace.cwd,
2692
+ status: "pending",
2693
+ createdAt: now
2694
+ };
2695
+ this.#tasks.set(task.id, task);
2696
+ return task;
2697
+ }
2698
+ getTask(taskId) {
2699
+ return this.#tasks.get(taskId);
2700
+ }
2701
+ updateTaskStatus(taskId, status) {
2702
+ const task = this.#tasks.get(taskId);
2703
+ if (!task) return;
2704
+ const updatedTask = {
2705
+ ...task,
2706
+ status,
2707
+ startedAt: status === "running" ? (/* @__PURE__ */ new Date()).toISOString() : task.startedAt,
2708
+ endedAt: status === "completed" || status === "failed" || status === "cancelled" ? (/* @__PURE__ */ new Date()).toISOString() : task.endedAt
2709
+ };
2710
+ this.#tasks.set(taskId, updatedTask);
2711
+ return updatedTask;
2712
+ }
2713
+ };
2714
+ //#endregion
2715
+ //#region src/session/task-runner.ts
2716
+ function sameRuntimeArgs(left, right) {
2717
+ if (left.length !== right.length) return false;
2718
+ for (let index = 0; index < left.length; index += 1) if (left[index] !== right[index]) return false;
2719
+ return true;
2720
+ }
2721
+ var TaskRunner = class {
2722
+ #eventLog = /* @__PURE__ */ new Map();
2723
+ #eventListeners = /* @__PURE__ */ new Map();
2724
+ #conversations = /* @__PURE__ */ new Map();
2725
+ #runningConversations = /* @__PURE__ */ new Set();
2726
+ #taskRuntimeArgs = /* @__PURE__ */ new Map();
2727
+ constructor(sessionManager, agentProcess, logger) {
2728
+ this.sessionManager = sessionManager;
2729
+ this.agentProcess = agentProcess;
2730
+ this.logger = logger;
2731
+ }
2732
+ async startTask(input, workspace, options) {
2733
+ const task = this.sessionManager.createTask(input, workspace);
2734
+ this.#taskRuntimeArgs.set(task.id, options?.runtimeArgs ?? []);
2735
+ this.#eventLog.set(task.id, []);
2736
+ if (options?.onEvent) this.#eventListeners.set(task.id, options.onEvent);
2737
+ this.sessionManager.updateTaskStatus(task.id, "running");
2738
+ try {
2739
+ const { initialization, sessionId } = await this.agentProcess.start({
2740
+ taskId: task.id,
2741
+ agent: task.agent,
2742
+ cwd: task.cwd,
2743
+ runtimeArgs: options?.runtimeArgs
2744
+ });
2745
+ this.recordEvent(task.id, {
2746
+ type: "task.started",
2747
+ taskId: task.id,
2748
+ workspaceId: task.workspaceId,
2749
+ agent: task.agent,
2750
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2751
+ });
2752
+ const promptResult = await this.agentProcess.prompt(task.id, task.prompt);
2753
+ this.logger.info("task started", {
2754
+ taskId: task.id,
2755
+ workspaceId: task.workspaceId,
2756
+ agent: task.agent,
2757
+ protocolVersion: initialization.protocolVersion,
2758
+ sessionId,
2759
+ stopReason: promptResult.stopReason
2760
+ });
2761
+ this.recordEvent(task.id, {
2762
+ type: "task.completed",
2763
+ taskId: task.id,
2764
+ summary: promptResult.stopReason,
2765
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2766
+ });
2767
+ return {
2768
+ taskId: task.id,
2769
+ events: this.flushEvents(task.id)
2770
+ };
2771
+ } catch (error) {
2772
+ const taskError = error instanceof AgentProcessError ? error : new AgentProcessError("agent_session_start_failed", error instanceof Error ? error.message : String(error));
2773
+ this.sessionManager.updateTaskStatus(task.id, "failed");
2774
+ this.logger.error("task failed before prompt completed", {
2775
+ taskId: task.id,
2776
+ code: taskError.code,
2777
+ error: taskError.message
2778
+ });
2779
+ this.recordEvent(task.id, {
2780
+ type: "task.failed",
2781
+ taskId: task.id,
2782
+ code: taskError.code,
2783
+ error: taskError.message,
2784
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2785
+ });
2786
+ return {
2787
+ taskId: task.id,
2788
+ events: this.flushEvents(task.id)
2789
+ };
2790
+ }
2791
+ }
2792
+ async startConversationTask(conversationId, input, workspace, options) {
2793
+ const state = await this.ensureConversation(conversationId, workspace, input.agent, {
2794
+ resumeSessionId: options?.resumeSessionId,
2795
+ runtimeArgs: options?.runtimeArgs
2796
+ }, input.prompt);
2797
+ return this.runConversationPrompt(conversationId, state, input.prompt, options);
2798
+ }
2799
+ async ensureConversation(conversationId, workspace, agent, options, promptForTask) {
2800
+ let state = this.#conversations.get(conversationId);
2801
+ if (state && (state.workspaceId !== workspace.id || state.agent !== agent || state.cwd !== workspace.cwd || !sameRuntimeArgs(state.runtimeArgs, options?.runtimeArgs ?? []))) {
2802
+ await this.resetConversation(conversationId);
2803
+ state = void 0;
2804
+ }
2805
+ if (state) return state;
2806
+ const task = this.sessionManager.createTask({
2807
+ workspaceId: workspace.id,
2808
+ agent,
2809
+ prompt: promptForTask ?? ""
2810
+ }, workspace);
2811
+ const started = await this.agentProcess.start({
2812
+ taskId: task.id,
2813
+ agent,
2814
+ cwd: workspace.cwd,
2815
+ resumeSessionId: options?.resumeSessionId,
2816
+ runtimeArgs: options?.runtimeArgs
2817
+ });
2818
+ state = {
2819
+ taskId: task.id,
2820
+ workspaceId: workspace.id,
2821
+ agent,
2822
+ cwd: workspace.cwd,
2823
+ sessionId: started.sessionId,
2824
+ runtimeArgs: options?.runtimeArgs ?? [],
2825
+ models: started.models
2826
+ };
2827
+ this.#taskRuntimeArgs.set(task.id, state.runtimeArgs);
2828
+ this.#conversations.set(conversationId, state);
2829
+ this.logger.info("conversation created", {
2830
+ conversationId,
2831
+ taskId: state.taskId,
2832
+ workspaceId: state.workspaceId,
2833
+ agent: state.agent,
2834
+ sessionId: started.sessionId,
2835
+ resumed: Boolean(options?.resumeSessionId && started.sessionId === options.resumeSessionId)
2836
+ });
2837
+ return state;
2838
+ }
2839
+ async runConversationPrompt(conversationId, state, prompt, options) {
2840
+ this.#runningConversations.add(conversationId);
2841
+ const result = await this.runPrompt(state.taskId, state.workspaceId, state.agent, prompt, options).finally(() => {
2842
+ this.#runningConversations.delete(conversationId);
2843
+ });
2844
+ if (result.events.some((event) => event.type === "task.failed")) await this.resetConversation(conversationId);
2845
+ return {
2846
+ ...result,
2847
+ sessionId: state.sessionId,
2848
+ models: state.models
2849
+ };
2850
+ }
2851
+ async resetConversation(conversationId) {
2852
+ const state = this.#conversations.get(conversationId);
2853
+ if (!state) return false;
2854
+ this.#conversations.delete(conversationId);
2855
+ this.#taskRuntimeArgs.delete(state.taskId);
2856
+ this.sessionManager.updateTaskStatus(state.taskId, "cancelled");
2857
+ await this.agentProcess.stop(state.taskId);
2858
+ this.logger.info("conversation reset", {
2859
+ conversationId,
2860
+ taskId: state.taskId
2861
+ });
2862
+ return true;
2863
+ }
2864
+ isConversationRunning(conversationId) {
2865
+ return this.#runningConversations.has(conversationId);
2866
+ }
2867
+ hasConversation(conversationId) {
2868
+ return this.#conversations.has(conversationId);
2869
+ }
2870
+ async setConversationModel(conversationId, modelId) {
2871
+ const state = this.#conversations.get(conversationId);
2872
+ if (!state) return false;
2873
+ await this.agentProcess.setSessionModel(state.taskId, modelId);
2874
+ if (state.models) state.models = {
2875
+ ...state.models,
2876
+ currentModelId: modelId
2877
+ };
2878
+ return true;
2879
+ }
2880
+ getConversationModels(conversationId) {
2881
+ return this.#conversations.get(conversationId)?.models;
2882
+ }
2883
+ getTask(taskId) {
2884
+ return this.sessionManager.getTask(taskId);
2885
+ }
2886
+ isTaskFullAccess(taskId) {
2887
+ const args = this.#taskRuntimeArgs.get(taskId) ?? [];
2888
+ return this.hasApprovalPolicyNever(args);
2889
+ }
2890
+ handleAgentEvent(event) {
2891
+ if (event.type === "agent.output") {
2892
+ const bridgeEvent = {
2893
+ type: "task.output",
2894
+ taskId: event.taskId,
2895
+ stream: "agent",
2896
+ chunk: event.text,
2897
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2898
+ };
2899
+ this.recordEvent(event.taskId, bridgeEvent);
2900
+ return bridgeEvent;
2901
+ }
2902
+ if (event.type === "agent.approval_requested") {
2903
+ const bridgeEvent = {
2904
+ type: "task.approval_requested",
2905
+ taskId: event.taskId,
2906
+ request: event.request,
2907
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2908
+ };
2909
+ this.recordEvent(event.taskId, bridgeEvent);
2910
+ return bridgeEvent;
2911
+ }
2912
+ if (event.type === "agent.approval_resolved") {
2913
+ const bridgeEvent = {
2914
+ type: "task.approval_resolved",
2915
+ taskId: event.taskId,
2916
+ decision: event.decision,
2917
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2918
+ };
2919
+ this.recordEvent(event.taskId, bridgeEvent);
2920
+ return bridgeEvent;
2921
+ }
2922
+ if (event.type === "agent.tool_update") {
2923
+ const bridgeEvent = {
2924
+ type: "task.tool_update",
2925
+ taskId: event.taskId,
2926
+ update: event.update,
2927
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2928
+ };
2929
+ this.recordEvent(event.taskId, bridgeEvent);
2930
+ return bridgeEvent;
2931
+ }
2932
+ }
2933
+ recordEvent(taskId, event) {
2934
+ const events = this.#eventLog.get(taskId);
2935
+ if (!events) this.#eventLog.set(taskId, [event]);
2936
+ else events.push(event);
2937
+ const listener = this.#eventListeners.get(taskId);
2938
+ if (listener) listener(event);
2939
+ }
2940
+ async runPrompt(taskId, workspaceId, agent, prompt, options) {
2941
+ this.#eventLog.set(taskId, []);
2942
+ if (options?.onEvent) this.#eventListeners.set(taskId, options.onEvent);
2943
+ this.sessionManager.updateTaskStatus(taskId, "running");
2944
+ this.recordEvent(taskId, {
2945
+ type: "task.started",
2946
+ taskId,
2947
+ workspaceId,
2948
+ agent,
2949
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2950
+ });
2951
+ try {
2952
+ const promptResult = await this.agentProcess.prompt(taskId, prompt);
2953
+ this.recordEvent(taskId, {
2954
+ type: "task.completed",
2955
+ taskId,
2956
+ summary: promptResult.stopReason,
2957
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2958
+ });
2959
+ } catch (error) {
2960
+ const taskError = error instanceof AgentProcessError ? error : new AgentProcessError("agent_session_start_failed", error instanceof Error ? error.message : String(error));
2961
+ this.sessionManager.updateTaskStatus(taskId, "failed");
2962
+ this.logger.error("task failed before prompt completed", {
2963
+ taskId,
2964
+ code: taskError.code,
2965
+ error: taskError.message
2966
+ });
2967
+ this.recordEvent(taskId, {
2968
+ type: "task.failed",
2969
+ taskId,
2970
+ code: taskError.code,
2971
+ error: taskError.message,
2972
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2973
+ });
2974
+ }
2975
+ return {
2976
+ taskId,
2977
+ events: this.flushEvents(taskId)
2978
+ };
2979
+ }
2980
+ flushEvents(taskId) {
2981
+ const events = this.#eventLog.get(taskId) ?? [];
2982
+ this.#eventLog.delete(taskId);
2983
+ this.#eventListeners.delete(taskId);
2984
+ return events;
2985
+ }
2986
+ hasApprovalPolicyNever(args) {
2987
+ for (let index = 0; index < args.length - 1; index += 1) if (args[index] === "-c" && args[index + 1] === "approval_policy=\"never\"") return true;
2988
+ return false;
2989
+ }
2990
+ };
2991
+ //#endregion
2992
+ //#region src/feishu/session-controller.ts
2993
+ var FeishuSessionController = class {
2994
+ #chatCwds = /* @__PURE__ */ new Map();
2995
+ #chatBridgeSessionIds = /* @__PURE__ */ new Map();
2996
+ #chatSessionIds = /* @__PURE__ */ new Map();
2997
+ #chatAccessModes = /* @__PURE__ */ new Map();
2998
+ #chatCurrentModelIds = /* @__PURE__ */ new Map();
2999
+ #chatAvailableModels = /* @__PURE__ */ new Map();
3000
+ #defaultAccessMode;
3001
+ constructor(workspaces, defaultAccessMode, stateStore = new FileSessionStateStore()) {
3002
+ this.workspaces = workspaces;
3003
+ this.stateStore = stateStore;
3004
+ this.#defaultAccessMode = defaultAccessMode;
3005
+ }
3006
+ async restore() {
3007
+ const persisted = await this.stateStore.loadState();
3008
+ let sanitized = false;
3009
+ const fallbackCwd = this.workspaces[0]?.cwd;
3010
+ for (const [chatId, cwd] of persisted.chatCwds.entries()) if (await this.isDirectory(cwd)) this.#chatCwds.set(chatId, cwd);
3011
+ else if (fallbackCwd) {
3012
+ this.#chatCwds.set(chatId, fallbackCwd);
3013
+ sanitized = true;
3014
+ }
3015
+ for (const [chatId, bridgeSessionId] of persisted.chatBridgeSessionIds.entries()) this.#chatBridgeSessionIds.set(chatId, bridgeSessionId);
3016
+ for (const [chatId, sessionId] of persisted.chatSessionIds.entries()) this.#chatSessionIds.set(chatId, sessionId);
3017
+ if (sanitized) await this.persist();
3018
+ return {
3019
+ persistedChats: this.#chatCwds.size,
3020
+ persistedSessions: this.#chatSessionIds.size
3021
+ };
3022
+ }
3023
+ async resolveValidatedWorkspace(chatId) {
3024
+ const workspace = this.resolveWorkspace(chatId);
3025
+ if (!workspace) return;
3026
+ if (await this.isDirectory(workspace.cwd)) return workspace;
3027
+ const fallback = this.workspaces[0];
3028
+ if (!fallback) return workspace;
3029
+ await this.setChatCwd(chatId, fallback.cwd);
3030
+ await this.clearSession(chatId);
3031
+ return {
3032
+ ...fallback,
3033
+ cwd: fallback.cwd
3034
+ };
3035
+ }
3036
+ getResumeSessionId(chatId) {
3037
+ return this.#chatSessionIds.get(chatId);
3038
+ }
3039
+ getBridgeSessionId(chatId) {
3040
+ return this.#chatBridgeSessionIds.get(chatId);
3041
+ }
3042
+ async setSessionId(chatId, sessionId) {
3043
+ if (!sessionId) {
3044
+ this.#chatBridgeSessionIds.delete(chatId);
3045
+ this.#chatSessionIds.delete(chatId);
3046
+ } else {
3047
+ this.#chatBridgeSessionIds.set(chatId, randomUUID());
3048
+ this.#chatSessionIds.set(chatId, sessionId);
3049
+ }
3050
+ await this.persist();
3051
+ }
3052
+ async clearSession(chatId) {
3053
+ this.#chatBridgeSessionIds.delete(chatId);
3054
+ this.#chatSessionIds.delete(chatId);
3055
+ this.#chatCurrentModelIds.delete(chatId);
3056
+ this.#chatAvailableModels.delete(chatId);
3057
+ await this.persist();
3058
+ }
3059
+ getAccessMode(chatId) {
3060
+ return this.#chatAccessModes.get(chatId) ?? this.#defaultAccessMode;
3061
+ }
3062
+ getDefaultAccessMode() {
3063
+ return this.#defaultAccessMode;
3064
+ }
3065
+ getAccessOverride(chatId) {
3066
+ return this.#chatAccessModes.get(chatId);
3067
+ }
3068
+ getModel(chatId) {
3069
+ return this.#chatCurrentModelIds.get(chatId);
3070
+ }
3071
+ async setModel(chatId, model, taskRunner) {
3072
+ if (!taskRunner.hasConversation(chatId)) return false;
3073
+ if (this.getModel(chatId) === model) return false;
3074
+ if (!await taskRunner.setConversationModel(chatId, model)) return false;
3075
+ const models = taskRunner.getConversationModels(chatId);
3076
+ this.updateModelState(chatId, models);
3077
+ return true;
3078
+ }
3079
+ getAvailableModels(chatId) {
3080
+ return this.#chatAvailableModels.get(chatId) ?? [];
3081
+ }
3082
+ updateModelState(chatId, state) {
3083
+ if (!state) return;
3084
+ this.#chatCurrentModelIds.set(chatId, state.currentModelId);
3085
+ this.#chatAvailableModels.set(chatId, state.availableModels);
3086
+ }
3087
+ async setAccessMode(chatId, mode, taskRunner) {
3088
+ if (this.getAccessMode(chatId) === mode) return false;
3089
+ if (mode === this.#defaultAccessMode) this.#chatAccessModes.delete(chatId);
3090
+ else this.#chatAccessModes.set(chatId, mode);
3091
+ await this.resetConversation(chatId, taskRunner, { keepAccessOverride: true });
3092
+ return true;
3093
+ }
3094
+ async clearAccessOverride(chatId, taskRunner) {
3095
+ if (!this.#chatAccessModes.has(chatId)) return false;
3096
+ this.#chatAccessModes.delete(chatId);
3097
+ await this.resetConversation(chatId, taskRunner);
3098
+ return true;
3099
+ }
3100
+ async interrupt(chatId, taskRunner) {
3101
+ if (!taskRunner.isConversationRunning(chatId)) return false;
3102
+ await this.resetConversation(chatId, taskRunner, { keepAccessOverride: true });
3103
+ return true;
3104
+ }
3105
+ async resetConversation(chatId, taskRunner, options) {
3106
+ await taskRunner.resetConversation(chatId);
3107
+ this.#chatBridgeSessionIds.delete(chatId);
3108
+ this.#chatSessionIds.delete(chatId);
3109
+ this.#chatCurrentModelIds.delete(chatId);
3110
+ this.#chatAvailableModels.delete(chatId);
3111
+ if (!options?.keepAccessOverride) this.#chatAccessModes.delete(chatId);
3112
+ await this.persist();
3113
+ }
3114
+ async startNewConversation(options, taskRunner) {
3115
+ await this.resetConversation(options.chatId, taskRunner);
3116
+ if (options.cwd) await this.setChatCwd(options.chatId, options.cwd);
3117
+ else await this.setChatCwd(options.chatId, options.workspace.cwd);
3118
+ const nextWorkspace = {
3119
+ ...options.workspace,
3120
+ cwd: options.cwd ?? options.workspace.cwd
3121
+ };
3122
+ const conversation = await taskRunner.ensureConversation(options.chatId, nextWorkspace, options.agent);
3123
+ this.updateModelState(options.chatId, conversation.models);
3124
+ this.#chatBridgeSessionIds.set(options.chatId, randomUUID());
3125
+ this.#chatSessionIds.set(options.chatId, conversation.sessionId);
3126
+ await this.persist();
3127
+ return {
3128
+ sessionId: conversation.sessionId,
3129
+ workspace: nextWorkspace
3130
+ };
3131
+ }
3132
+ resolveWorkspace(chatId) {
3133
+ const baseWorkspace = this.workspaces[0];
3134
+ if (!baseWorkspace) return;
3135
+ const cwd = this.#chatCwds.get(chatId) ?? baseWorkspace.cwd;
3136
+ return {
3137
+ ...baseWorkspace,
3138
+ cwd
3139
+ };
3140
+ }
3141
+ async setChatCwd(chatId, cwd) {
3142
+ this.#chatCwds.set(chatId, cwd);
3143
+ await this.persist();
3144
+ }
3145
+ async isDirectory(path) {
3146
+ const dirStat = await stat(path).catch(() => void 0);
3147
+ return Boolean(dirStat?.isDirectory());
3148
+ }
3149
+ async persist() {
3150
+ await this.stateStore.saveState({
3151
+ chatCwds: this.#chatCwds,
3152
+ chatBridgeSessionIds: this.#chatBridgeSessionIds,
3153
+ chatSessionIds: this.#chatSessionIds
3154
+ });
3155
+ }
3156
+ };
3157
+ //#endregion
3158
+ //#region src/feishu/feishu-gateway.ts
3159
+ function buildCodexRuntimeArgs(mode) {
3160
+ const args = [];
3161
+ if (mode === "full-access") args.push("-c", "approval_policy=\"never\"", "-c", "sandbox_mode=\"danger-full-access\"");
3162
+ return args;
3163
+ }
3164
+ var FeishuGateway = class {
3165
+ #wsClient;
3166
+ #eventDispatcher;
3167
+ #messageQueue = new MessageEntryQueue();
3168
+ #sessionController;
3169
+ #cardRenderer = new FeishuCardRenderer();
3170
+ #messageClient;
3171
+ #cardActionHandler;
3172
+ #commandHandler;
3173
+ #approvalCards = /* @__PURE__ */ new Map();
3174
+ #patchMinIntervalMs = 280;
3175
+ constructor(config, workspaces, taskRunner, approvalGateway, logger, yoloMode = false) {
3176
+ this.taskRunner = taskRunner;
3177
+ this.approvalGateway = approvalGateway;
3178
+ this.logger = logger;
3179
+ this.#sessionController = new FeishuSessionController(workspaces, yoloMode ? "full-access" : "standard");
3180
+ this.#messageClient = new FeishuMessageClient(new lark.Client({
3181
+ appId: config.appId,
3182
+ appSecret: config.appSecret
3183
+ }), this.logger);
3184
+ this.#cardActionHandler = new FeishuCardActionHandler({
3185
+ sessionController: this.#sessionController,
3186
+ taskRunner: this.taskRunner,
3187
+ approvalGateway: this.approvalGateway,
3188
+ messageClient: this.#messageClient,
3189
+ cardRenderer: this.#cardRenderer,
3190
+ logger: this.logger,
3191
+ onApprovalResolved: (decision) => {
3192
+ this.patchApprovalCardByDecision(decision);
3193
+ }
3194
+ });
3195
+ this.#commandHandler = new FeishuCommandHandler({
3196
+ sessionController: this.#sessionController,
3197
+ cardActionHandler: this.#cardActionHandler,
3198
+ messageClient: this.#messageClient,
3199
+ taskRunner: this.taskRunner
3200
+ });
3201
+ this.#wsClient = new lark.WSClient({
3202
+ appId: config.appId,
3203
+ appSecret: config.appSecret,
3204
+ loggerLevel: lark.LoggerLevel.info
3205
+ });
3206
+ this.#eventDispatcher = buildFeishuEventDispatcher({
3207
+ onMessageReceived: async (data) => {
3208
+ await this.handleIncomingMessage(data);
3209
+ },
3210
+ onCardAction: async (data) => {
3211
+ return this.#cardActionHandler.handleCardAction(data ?? {});
3212
+ }
3213
+ });
3214
+ this.approvalGateway.onResolved((snapshot) => {
3215
+ this.patchApprovalCard(snapshot);
3216
+ });
3217
+ }
3218
+ async start() {
3219
+ const restored = await this.#sessionController.restore();
3220
+ await this.#wsClient.start({ eventDispatcher: this.#eventDispatcher });
3221
+ this.logger.info("feishu gateway started", restored);
3222
+ }
3223
+ async handleIncomingMessage(data) {
3224
+ this.#messageQueue.gcHandledMessageIds();
3225
+ const messageId = data.message.message_id;
3226
+ const queue = this.#messageQueue.runInChatQueue(data.message.chat_id, messageId, async () => {
3227
+ await this.processMessage(data);
3228
+ });
3229
+ if (!queue) {
3230
+ this.logger.info("feishu duplicate message ignored", { messageId });
3231
+ return;
3232
+ }
3233
+ queue.catch((error) => {
3234
+ this.logger.error("feishu message process failed", {
3235
+ messageId,
3236
+ error: error instanceof Error ? error.message : String(error)
3237
+ });
3238
+ });
3239
+ }
3240
+ async processMessage(data) {
3241
+ if (data.sender.sender_type !== "user") return;
3242
+ const chatId = data.message.chat_id;
3243
+ if (data.message.message_type !== "text") {
3244
+ await this.#messageClient.sendText(chatId, "只支持文本消息。");
3245
+ return;
3246
+ }
3247
+ const workspace = await this.#sessionController.resolveValidatedWorkspace(chatId);
3248
+ if (!workspace) {
3249
+ await this.#messageClient.sendText(chatId, "未配置可用工作区。");
3250
+ return;
3251
+ }
3252
+ const rawPrompt = (parseContent(data.message.content).text ?? "").trim();
3253
+ if (!rawPrompt) {
3254
+ await this.#messageClient.sendText(chatId, "消息内容为空。");
3255
+ return;
3256
+ }
3257
+ let command;
3258
+ try {
3259
+ command = await parseUserCommand(rawPrompt, workspace.cwd);
3260
+ } catch (error) {
3261
+ await this.#messageClient.sendText(chatId, `命令解析失败:${error instanceof Error ? error.message : String(error)}`);
3262
+ return;
3263
+ }
3264
+ const result = await this.#commandHandler.handle({
3265
+ chatId,
3266
+ workspace,
3267
+ command
3268
+ });
3269
+ if (result.type === "handled") return;
3270
+ await this.runConversationTask({
3271
+ chatId,
3272
+ messageId: data.message.message_id,
3273
+ workspace,
3274
+ prompt: result.prompt
3275
+ });
3276
+ }
3277
+ async runConversationTask(params) {
3278
+ const streamer = new TaskCardStreamer({
3279
+ chatId: params.chatId,
3280
+ logger: this.logger,
3281
+ renderer: this.#cardRenderer,
3282
+ sendCard: async (chatId, card) => this.#messageClient.sendCard(chatId, card),
3283
+ patchCard: async (messageId, card) => this.#messageClient.patchCard(messageId, card),
3284
+ patchMinIntervalMs: this.#patchMinIntervalMs
3285
+ });
3286
+ let typingReactionId;
3287
+ try {
3288
+ typingReactionId = await this.#messageClient.addTypingReaction(params.messageId);
3289
+ const result = await this.taskRunner.startConversationTask(params.chatId, {
3290
+ workspaceId: params.workspace.id,
3291
+ agent: "codex",
3292
+ prompt: params.prompt
3293
+ }, params.workspace, {
3294
+ runtimeArgs: buildCodexRuntimeArgs(this.#sessionController.getAccessMode(params.chatId)),
3295
+ resumeSessionId: this.#sessionController.getResumeSessionId(params.chatId),
3296
+ onEvent: (event) => {
3297
+ this.onTaskEvent(params.chatId, event, streamer);
3298
+ }
3299
+ });
3300
+ if (result.sessionId && this.#sessionController.getResumeSessionId(params.chatId) !== result.sessionId) await this.#sessionController.setSessionId(params.chatId, result.sessionId);
3301
+ this.#sessionController.updateModelState(params.chatId, result.models);
3302
+ const failed = result.events.find((event) => event.type === "task.failed");
3303
+ const completed = result.events.find((event) => event.type === "task.completed");
3304
+ if (failed) streamer.markFailed(failed.error);
3305
+ else if (completed) streamer.markCompleted(completed.summary);
3306
+ await streamer.finalize();
3307
+ } catch (error) {
3308
+ const errorMessage = error instanceof Error ? error.message : String(error);
3309
+ this.logger.error("run conversation task failed", {
3310
+ chatId: params.chatId,
3311
+ messageId: params.messageId,
3312
+ error: errorMessage
3313
+ });
3314
+ streamer.markFailed(errorMessage);
3315
+ await streamer.finalize().catch((finalizeError) => {
3316
+ this.logger.error("finalize failed after task error", {
3317
+ chatId: params.chatId,
3318
+ messageId: params.messageId,
3319
+ error: finalizeError instanceof Error ? finalizeError.message : String(finalizeError)
3320
+ });
3321
+ });
3322
+ } finally {
3323
+ if (typingReactionId) await this.#messageClient.removeTypingReaction(params.messageId, typingReactionId).catch(() => {});
3324
+ }
3325
+ }
3326
+ onTaskEvent(chatId, event, streamer) {
3327
+ if (event.type === "task.output") {
3328
+ streamer.handleOutputChunk(event.chunk);
3329
+ return;
3330
+ }
3331
+ if (event.type === "task.tool_update") {
3332
+ streamer.handleToolUpdate(event.update);
3333
+ return;
3334
+ }
3335
+ if (event.type === "task.approval_requested") {
3336
+ this.sendApprovalCard(chatId, event.request).catch((error) => {
3337
+ this.logger.error("send approval card failed", {
3338
+ taskId: event.taskId,
3339
+ requestId: event.request.id,
3340
+ error: error instanceof Error ? error.message : String(error)
3341
+ });
3342
+ });
3343
+ return;
3344
+ }
3345
+ if (event.type === "task.approval_resolved") {
3346
+ if (shouldPatchApprovalSummary(event.decision)) this.patchApprovalCardByDecision(event.decision);
3347
+ return;
3348
+ }
3349
+ if (event.type === "task.failed") {
3350
+ streamer.markFailed(event.error);
3351
+ return;
3352
+ }
3353
+ if (event.type === "task.completed") streamer.markCompleted(event.summary);
3354
+ }
3355
+ async sendApprovalCard(chatId, request) {
3356
+ this.gcApprovalCards();
3357
+ const messageId = await this.#messageClient.sendCard(chatId, this.#cardRenderer.buildApprovalCard(request, "pending"));
3358
+ this.#approvalCards.set(request.id, {
3359
+ chatId,
3360
+ messageId,
3361
+ updatedAtMs: Date.now()
3362
+ });
3363
+ }
3364
+ async patchApprovalCard(snapshot) {
3365
+ this.gcApprovalCards();
3366
+ const binding = this.#approvalCards.get(snapshot.request.id);
3367
+ if (!binding) return;
3368
+ binding.updatedAtMs = Date.now();
3369
+ await this.#messageClient.patchCard(binding.messageId, this.#cardRenderer.buildApprovalCard(snapshot.request, snapshot.status, snapshot.decision)).catch((error) => {
3370
+ this.logger.warn("patch approval card failed", {
3371
+ requestId: snapshot.request.id,
3372
+ messageId: binding.messageId,
3373
+ error: error instanceof Error ? error.message : String(error)
3374
+ });
3375
+ });
3376
+ }
3377
+ async patchApprovalCardByDecision(decision) {
3378
+ this.gcApprovalCards();
3379
+ const binding = this.#approvalCards.get(decision.requestId);
3380
+ if (!binding) return;
3381
+ binding.updatedAtMs = Date.now();
3382
+ const title = decision.decision === "approved" ? "已批准" : "已拒绝";
3383
+ await this.#messageClient.patchCard(binding.messageId, this.#cardRenderer.buildApprovalSummaryCard(title, decision)).catch((error) => {
3384
+ this.logger.warn("patch approval card failed", {
3385
+ requestId: decision.requestId,
3386
+ messageId: binding.messageId,
3387
+ error: error instanceof Error ? error.message : String(error)
3388
+ });
3389
+ });
3390
+ }
3391
+ gcApprovalCards() {
3392
+ const ttlMs = 1440 * 60 * 1e3;
3393
+ const now = Date.now();
3394
+ for (const [requestId, binding] of this.#approvalCards.entries()) if (now - binding.updatedAtMs > ttlMs) this.#approvalCards.delete(requestId);
3395
+ }
3396
+ };
3397
+ //#endregion
3398
+ //#region src/utils/logger.ts
3399
+ function log(level, message, context) {
3400
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
3401
+ const levelLabel = level.toUpperCase();
3402
+ const contextText = context ? ` ${formatContext(context)}` : "";
3403
+ console.log(`[${timestamp}] ${levelLabel} ${message}${contextText}`);
3404
+ }
3405
+ function formatContext(context) {
3406
+ return Object.entries(context).map(([key, value]) => `${key}=${stringifyValue(value)}`).join(" ");
3407
+ }
3408
+ function stringifyValue(value) {
3409
+ if (typeof value === "string") return value;
3410
+ if (value === void 0) return "undefined";
3411
+ if (value === null) return "null";
3412
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
3413
+ try {
3414
+ return JSON.stringify(value);
3415
+ } catch {
3416
+ return "[unserializable]";
3417
+ }
3418
+ }
3419
+ function createLogger() {
3420
+ return {
3421
+ info: (message, context) => log("info", message, context),
3422
+ warn: (message, context) => log("warn", message, context),
3423
+ error: (message, context) => log("error", message, context)
3424
+ };
3425
+ }
3426
+ //#endregion
3427
+ //#region src/index.ts
3428
+ async function startBridge() {
3429
+ const config = await loadConfig();
3430
+ const logger = createLogger();
3431
+ const approvalGateway = new ApprovalGateway(new ApprovalStore(), logger);
3432
+ const sessionManager = new SessionManager();
3433
+ let taskRunner;
3434
+ taskRunner = new TaskRunner(sessionManager, new AgentProcess(config, logger, (event) => {
3435
+ const bridgeEvent = taskRunner.handleAgentEvent(event);
3436
+ if (!bridgeEvent) return;
3437
+ logger.info("bridge event emitted", bridgeEvent);
3438
+ }, createPermissionRequestHandler({
3439
+ workspaces: config.workspaces,
3440
+ approvalGateway,
3441
+ getTaskRunner: () => taskRunner
3442
+ })), logger);
3443
+ const feishuGateway = config.feishu ? new FeishuGateway(config.feishu, config.workspaces, taskRunner, approvalGateway, logger, config.yoloMode) : void 0;
3444
+ if (feishuGateway) await feishuGateway.start();
3445
+ logger.info("bridge started", {
3446
+ workspaceCount: config.workspaces.length,
3447
+ feishuEnabled: Boolean(feishuGateway),
3448
+ yoloMode: config.yoloMode
3449
+ });
3450
+ }
3451
+ //#endregion
3452
+ export { startBridge as t };