@hopgoldy/agent-bridge 0.1.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,2019 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import process2 from "process";
5
+ import { Command } from "commander";
6
+
7
+ // src/config/prompt.ts
8
+ import { cancel, confirm as clackConfirm, isCancel, password, select as clackSelect, text } from "@clack/prompts";
9
+ function normalize(value) {
10
+ return value == null ? "" : String(value).trim();
11
+ }
12
+ function throwIfCancelled(value) {
13
+ if (isCancel(value)) {
14
+ cancel("Setup cancelled.");
15
+ throw new Error("Setup cancelled");
16
+ }
17
+ }
18
+ function createPromptContext() {
19
+ return {
20
+ async input(label, opts = {}) {
21
+ const { defaultValue, required = false, secret = false, validate } = opts;
22
+ const validateInput = (raw2) => {
23
+ const value = normalize(raw2) || normalize(defaultValue);
24
+ if (required && !value) {
25
+ return "This field is required.";
26
+ }
27
+ if (validate) {
28
+ return validate(value) ?? void 0;
29
+ }
30
+ return void 0;
31
+ };
32
+ const raw = secret ? await password({ message: label, validate: validateInput }) : await text({
33
+ message: label,
34
+ defaultValue: defaultValue || void 0,
35
+ placeholder: defaultValue,
36
+ validate: validateInput
37
+ });
38
+ throwIfCancelled(raw);
39
+ return normalize(raw) || normalize(defaultValue);
40
+ },
41
+ async select(label, options) {
42
+ if (options.length === 0) {
43
+ throw new Error("select() requires at least one option");
44
+ }
45
+ const value = await clackSelect({
46
+ message: label,
47
+ options: options.map((option) => ({ value: option.value, label: option.label }))
48
+ });
49
+ throwIfCancelled(value);
50
+ return value;
51
+ },
52
+ async confirm(label, defaultValue = false) {
53
+ const value = await clackConfirm({ message: label, initialValue: defaultValue });
54
+ throwIfCancelled(value);
55
+ return value;
56
+ },
57
+ close() {
58
+ }
59
+ };
60
+ }
61
+
62
+ // src/config/session-bindings.ts
63
+ import { mkdir, readFile, unlink, writeFile } from "fs/promises";
64
+ import os from "os";
65
+ import path from "path";
66
+ var BINDINGS_DIR = path.join(os.homedir(), ".config", "agent-bridge", "session-bindings");
67
+ function getSessionBindingStorePath(channelName) {
68
+ return path.join(BINDINGS_DIR, `${encodeURIComponent(channelName)}.json`);
69
+ }
70
+ async function removeSessionBindingStore(channelName) {
71
+ try {
72
+ await unlink(getSessionBindingStorePath(channelName));
73
+ } catch (error) {
74
+ if (error?.code !== "ENOENT") {
75
+ throw error;
76
+ }
77
+ }
78
+ }
79
+ function createFileSessionBindingStore(filePath) {
80
+ return {
81
+ async load() {
82
+ try {
83
+ const raw = await readFile(filePath, "utf8");
84
+ const parsed = JSON.parse(raw);
85
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
86
+ return {};
87
+ }
88
+ return parsed;
89
+ } catch (error) {
90
+ if (error?.code === "ENOENT") {
91
+ return {};
92
+ }
93
+ throw error;
94
+ }
95
+ },
96
+ async save(bindings) {
97
+ await mkdir(path.dirname(filePath), { recursive: true });
98
+ await writeFile(filePath, `${JSON.stringify(bindings, null, 2)}
99
+ `, "utf8");
100
+ }
101
+ };
102
+ }
103
+
104
+ // src/config/store.ts
105
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
106
+ import os2 from "os";
107
+ import path2 from "path";
108
+
109
+ // src/config/defaults.ts
110
+ var DEFAULTS = {
111
+ agentIdleTimeoutMs: 10 * 60 * 1e3
112
+ };
113
+
114
+ // src/config/store.ts
115
+ var CONFIG_DIR = path2.join(os2.homedir(), ".config", "agent-bridge");
116
+ var CONFIG_PATH = path2.join(CONFIG_DIR, "config.json");
117
+ function normalizeChannelConfig(channel) {
118
+ if (!channel.client || !channel.agent) {
119
+ throw new Error("Invalid channel config shape");
120
+ }
121
+ return channel;
122
+ }
123
+ function mergeDefaults(config = {}) {
124
+ const channels = Object.fromEntries(
125
+ Object.entries(config.channels ?? {}).map(([name, channel]) => [name, normalizeChannelConfig(channel)])
126
+ );
127
+ return {
128
+ channels,
129
+ defaults: {
130
+ agentIdleTimeoutMs: config.defaults?.agentIdleTimeoutMs ?? DEFAULTS.agentIdleTimeoutMs
131
+ }
132
+ };
133
+ }
134
+ function getConfigPath() {
135
+ return CONFIG_PATH;
136
+ }
137
+ async function ensureConfigDir() {
138
+ await mkdir2(CONFIG_DIR, { recursive: true });
139
+ }
140
+ async function loadConfig() {
141
+ await ensureConfigDir();
142
+ try {
143
+ const raw = await readFile2(CONFIG_PATH, "utf8");
144
+ return mergeDefaults(JSON.parse(raw));
145
+ } catch (error) {
146
+ if (error?.code === "ENOENT") {
147
+ return mergeDefaults();
148
+ }
149
+ throw error;
150
+ }
151
+ }
152
+ async function saveConfig(config) {
153
+ await ensureConfigDir();
154
+ const merged = mergeDefaults(config);
155
+ await writeFile2(CONFIG_PATH, `${JSON.stringify(merged, null, 2)}
156
+ `, "utf8");
157
+ }
158
+
159
+ // src/core/logger.ts
160
+ var LEVEL_ORDER = {
161
+ debug: 10,
162
+ info: 20,
163
+ warn: 30,
164
+ error: 40
165
+ };
166
+ function resolveMinLevel() {
167
+ const raw = (process.env.LOG_LEVEL ?? "info").toLowerCase();
168
+ return LEVEL_ORDER[raw] ?? LEVEL_ORDER.info;
169
+ }
170
+ function timestamp() {
171
+ return (/* @__PURE__ */ new Date()).toISOString();
172
+ }
173
+ function createLogger(scope) {
174
+ const prefix = () => `[${timestamp()}] [${scope}]`;
175
+ const log = (level, write) => {
176
+ return (...args) => {
177
+ if (LEVEL_ORDER[level] < resolveMinLevel()) return;
178
+ write(prefix(), ...args);
179
+ };
180
+ };
181
+ return {
182
+ debug: log("debug", console.debug),
183
+ info: log("info", console.log),
184
+ warn: log("warn", console.warn),
185
+ error: log("error", console.error)
186
+ };
187
+ }
188
+
189
+ // src/core/gateway-core.ts
190
+ var GatewayCore = class {
191
+ #imAdapter;
192
+ #agentModule;
193
+ #agentConfig;
194
+ #agentIdleTimeoutMs;
195
+ #bindingStore;
196
+ #logger = createLogger("core");
197
+ #clientToAgentSession = /* @__PURE__ */ new Map();
198
+ #agentRuntimes = /* @__PURE__ */ new Map();
199
+ #started = false;
200
+ constructor({ imAdapter, agentModule, agentConfig, agentIdleTimeoutMs, bindingStore }) {
201
+ this.#imAdapter = imAdapter;
202
+ this.#agentModule = agentModule;
203
+ this.#agentConfig = agentConfig;
204
+ this.#agentIdleTimeoutMs = agentIdleTimeoutMs;
205
+ this.#bindingStore = bindingStore;
206
+ }
207
+ async start() {
208
+ if (this.#started) return;
209
+ this.#started = true;
210
+ if (this.#bindingStore) {
211
+ const bindings = await this.#bindingStore.load();
212
+ for (const [clientSessionId, agentSessionId] of Object.entries(bindings)) {
213
+ this.#clientToAgentSession.set(clientSessionId, agentSessionId);
214
+ }
215
+ }
216
+ await this.#imAdapter.start(async (event) => {
217
+ try {
218
+ await this.#handleClientOutput(event);
219
+ } catch (error) {
220
+ this.#logger.error("failed to process client output event:", error);
221
+ }
222
+ });
223
+ }
224
+ async stop() {
225
+ if (!this.#started) return;
226
+ this.#started = false;
227
+ for (const runtime of [...this.#agentRuntimes.values()]) {
228
+ await this.#stopRuntime(runtime);
229
+ }
230
+ await this.#imAdapter.stop();
231
+ }
232
+ async #handleClientOutput(event) {
233
+ if (event.type === "command.session.new") {
234
+ await this.#handleSessionNew(event.clientSessionId);
235
+ return;
236
+ }
237
+ if (event.type === "command.session.compact") {
238
+ await this.#handleSessionCompact(event.clientSessionId);
239
+ return;
240
+ }
241
+ if (event.type === "command.session.stop") {
242
+ await this.#handleSessionStop(event.clientSessionId);
243
+ return;
244
+ }
245
+ await this.#handleUserMessage(event.clientSessionId, event.text);
246
+ }
247
+ async #handleUserMessage(clientSessionId, text2) {
248
+ const runtime = await this.#getOrCreateActiveRuntime(clientSessionId);
249
+ this.#touchRuntime(runtime);
250
+ await runtime.agentAdapter.input({
251
+ type: "user.message",
252
+ text: text2
253
+ });
254
+ }
255
+ async #handleSessionCompact(clientSessionId) {
256
+ const runtime = await this.#getActiveRuntime(clientSessionId);
257
+ if (!runtime) {
258
+ await this.#deliverClientInput({
259
+ type: "assistant.message",
260
+ clientSessionId,
261
+ text: "No active agent session to compact."
262
+ });
263
+ return;
264
+ }
265
+ this.#touchRuntime(runtime);
266
+ await runtime.agentAdapter.input({
267
+ type: "command.session.compact"
268
+ });
269
+ }
270
+ async #handleSessionStop(clientSessionId) {
271
+ const runtime = await this.#getActiveRuntime(clientSessionId);
272
+ if (!runtime) {
273
+ await this.#deliverClientInput({
274
+ type: "assistant.message",
275
+ clientSessionId,
276
+ text: "No active agent session to stop."
277
+ });
278
+ return;
279
+ }
280
+ this.#touchRuntime(runtime);
281
+ if (!runtime.agentAdapter.abort) {
282
+ await this.#deliverClientInput({
283
+ type: "assistant.message",
284
+ clientSessionId,
285
+ text: "This agent session cannot be stopped right now."
286
+ });
287
+ return;
288
+ }
289
+ if (!await runtime.agentAdapter.isBusy()) {
290
+ await this.#deliverClientInput({
291
+ type: "assistant.message",
292
+ clientSessionId,
293
+ text: "No active agent run to stop."
294
+ });
295
+ return;
296
+ }
297
+ await runtime.agentAdapter.abort();
298
+ }
299
+ async #handleSessionNew(clientSessionId) {
300
+ const previousAgentSessionId = this.#clientToAgentSession.get(clientSessionId);
301
+ if (previousAgentSessionId) {
302
+ const previousRuntime = this.#agentRuntimes.get(previousAgentSessionId);
303
+ if (previousRuntime) {
304
+ await this.#stopRuntime(previousRuntime);
305
+ }
306
+ }
307
+ const runtime = await this.#createRuntimeForClient(clientSessionId);
308
+ this.#bindClientToAgent(clientSessionId, runtime.agentSessionId);
309
+ await this.#deliverClientInput({
310
+ type: "assistant.message",
311
+ clientSessionId,
312
+ text: "Started a new session."
313
+ });
314
+ }
315
+ async #deliverClientInput(event) {
316
+ try {
317
+ await this.#imAdapter.input(event);
318
+ } catch (error) {
319
+ this.#logger.error("failed to deliver client input event:", error);
320
+ }
321
+ }
322
+ async #getActiveRuntime(clientSessionId) {
323
+ const agentSessionId = this.#clientToAgentSession.get(clientSessionId);
324
+ if (!agentSessionId) {
325
+ return null;
326
+ }
327
+ return this.#getOrRestoreRuntime(clientSessionId, agentSessionId);
328
+ }
329
+ async #getOrCreateActiveRuntime(clientSessionId) {
330
+ const existing = await this.#getActiveRuntime(clientSessionId);
331
+ if (existing) {
332
+ return existing;
333
+ }
334
+ const runtime = await this.#createRuntimeForClient(clientSessionId);
335
+ this.#bindClientToAgent(clientSessionId, runtime.agentSessionId);
336
+ return runtime;
337
+ }
338
+ async #getOrRestoreRuntime(clientSessionId, agentSessionId) {
339
+ const existing = this.#agentRuntimes.get(agentSessionId);
340
+ if (existing) {
341
+ this.#touchRuntime(existing);
342
+ return existing;
343
+ }
344
+ if (this.#agentModule.resumeAgentSession) {
345
+ const agentAdapter = await this.#agentModule.resumeAgentSession({
346
+ config: this.#agentConfig,
347
+ agentSessionId
348
+ });
349
+ return this.#startRuntime(clientSessionId, agentSessionId, agentAdapter);
350
+ }
351
+ const runtime = await this.#createRuntimeForClient(clientSessionId);
352
+ this.#bindClientToAgent(clientSessionId, runtime.agentSessionId);
353
+ return runtime;
354
+ }
355
+ async #createRuntimeForClient(clientSessionId) {
356
+ const { agentSessionId, agentAdapter } = await this.#agentModule.createAgentSession({
357
+ config: this.#agentConfig
358
+ });
359
+ return this.#startRuntime(clientSessionId, agentSessionId, agentAdapter);
360
+ }
361
+ async #startRuntime(clientSessionId, agentSessionId, agentAdapter) {
362
+ await agentAdapter.start(async (event) => {
363
+ await this.#handleAgentOutput(event);
364
+ });
365
+ const runtime = {
366
+ agentSessionId,
367
+ clientSessionId,
368
+ agentAdapter,
369
+ lastActiveAt: Date.now(),
370
+ idleTimer: null
371
+ };
372
+ this.#agentRuntimes.set(agentSessionId, runtime);
373
+ this.#touchRuntime(runtime);
374
+ return runtime;
375
+ }
376
+ #bindClientToAgent(clientSessionId, agentSessionId) {
377
+ this.#clientToAgentSession.set(clientSessionId, agentSessionId);
378
+ void this.#persistBindings();
379
+ }
380
+ async #persistBindings() {
381
+ if (!this.#bindingStore) {
382
+ return;
383
+ }
384
+ try {
385
+ await this.#bindingStore.save(Object.fromEntries(this.#clientToAgentSession));
386
+ } catch (error) {
387
+ this.#logger.error("failed to persist session bindings:", error);
388
+ }
389
+ }
390
+ async #handleAgentOutput(event) {
391
+ const agentSessionId = event.agentSessionId;
392
+ const runtime = this.#agentRuntimes.get(agentSessionId);
393
+ if (!runtime) {
394
+ this.#logger.info(`dropping output from released agent session ${agentSessionId}`);
395
+ return;
396
+ }
397
+ const clientSessionId = runtime.clientSessionId;
398
+ const activeAgentSessionId = this.#clientToAgentSession.get(clientSessionId);
399
+ if (activeAgentSessionId !== agentSessionId) {
400
+ this.#logger.info(
401
+ `dropping late output from inactive agent session ${agentSessionId} for client ${clientSessionId}`
402
+ );
403
+ return;
404
+ }
405
+ this.#touchRuntime(runtime);
406
+ if (this.#isToolRelatedEvent(event)) {
407
+ this.#logger.info("forwarding tool event from agent", {
408
+ type: event.type,
409
+ agentSessionId,
410
+ clientSessionId,
411
+ toolName: "toolName" in event ? event.toolName : void 0,
412
+ text: event.text
413
+ });
414
+ }
415
+ if (event.type === "assistant.message") {
416
+ await this.#deliverClientInput({
417
+ type: "assistant.message",
418
+ clientSessionId,
419
+ text: event.text,
420
+ attachments: event.attachments
421
+ });
422
+ return;
423
+ }
424
+ await this.#deliverClientInput({
425
+ ...event,
426
+ clientSessionId
427
+ });
428
+ }
429
+ #isToolRelatedEvent(event) {
430
+ return event.type === "assistant.tool.running" || event.type === "assistant.tool.done" || event.type === "assistant.tool.error" || event.type === "session.compacting";
431
+ }
432
+ #touchRuntime(runtime) {
433
+ runtime.lastActiveAt = Date.now();
434
+ this.#scheduleIdleRelease(runtime);
435
+ }
436
+ #scheduleIdleRelease(runtime) {
437
+ if (runtime.idleTimer) {
438
+ clearTimeout(runtime.idleTimer);
439
+ }
440
+ if (this.#agentIdleTimeoutMs <= 0) {
441
+ runtime.idleTimer = null;
442
+ return;
443
+ }
444
+ runtime.idleTimer = setTimeout(() => {
445
+ void this.#releaseIdleRuntime(runtime.agentSessionId);
446
+ }, this.#agentIdleTimeoutMs);
447
+ runtime.idleTimer.unref?.();
448
+ }
449
+ async #releaseIdleRuntime(agentSessionId) {
450
+ const runtime = this.#agentRuntimes.get(agentSessionId);
451
+ if (!runtime) {
452
+ return;
453
+ }
454
+ const idleForMs = Date.now() - runtime.lastActiveAt;
455
+ if (idleForMs < this.#agentIdleTimeoutMs) {
456
+ this.#scheduleIdleRelease(runtime);
457
+ return;
458
+ }
459
+ if (await runtime.agentAdapter.isBusy()) {
460
+ this.#scheduleIdleRelease(runtime);
461
+ return;
462
+ }
463
+ await this.#stopRuntime(runtime);
464
+ this.#logger.info(`released idle agent session ${agentSessionId}`);
465
+ }
466
+ async #stopRuntime(runtime) {
467
+ if (runtime.idleTimer) {
468
+ clearTimeout(runtime.idleTimer);
469
+ runtime.idleTimer = null;
470
+ }
471
+ try {
472
+ if (runtime.agentAdapter.abort && await runtime.agentAdapter.isBusy()) {
473
+ try {
474
+ await runtime.agentAdapter.abort();
475
+ } catch (error) {
476
+ this.#logger.error(`abort failed for ${runtime.agentSessionId}:`, error);
477
+ }
478
+ }
479
+ await runtime.agentAdapter.stop();
480
+ } finally {
481
+ this.#agentRuntimes.delete(runtime.agentSessionId);
482
+ }
483
+ }
484
+ };
485
+
486
+ // src/modules/agent/pi-coding-agent/index.ts
487
+ import { randomUUID } from "crypto";
488
+ import os5 from "os";
489
+ import path6 from "path";
490
+
491
+ // src/modules/agent/pi-coding-agent/media-marker.ts
492
+ import { existsSync, realpathSync } from "fs";
493
+ import path3 from "path";
494
+ import os3 from "os";
495
+ var IMAGE_EXTS = /* @__PURE__ */ new Set(["png", "jpg", "jpeg", "webp", "gif", "bmp"]);
496
+ var DELIVERABLE_EXTS = /* @__PURE__ */ new Set([
497
+ ...IMAGE_EXTS,
498
+ "pdf",
499
+ "txt",
500
+ "md",
501
+ "csv",
502
+ "zip",
503
+ "docx",
504
+ "xlsx",
505
+ "pptx",
506
+ "json"
507
+ ]);
508
+ var MEDIA_TAG_RE = /[`"']?MEDIA:\s*(`[^`\n]+`|"[^"\n]+"|'[^'\n]+'|(?:~\/|\/|[A-Za-z]:[/\\])[^\s`"']+\.\w+)[`"']?/gi;
509
+ function maskProtectedSpans(text2) {
510
+ const chars = [...text2];
511
+ const maskRange = (re) => {
512
+ for (const match of text2.matchAll(re)) {
513
+ const start = match.index ?? 0;
514
+ const end = start + match[0].length;
515
+ for (let i = start; i < end; i++) {
516
+ if (chars[i] !== "\n") chars[i] = " ";
517
+ }
518
+ }
519
+ };
520
+ maskRange(/```[\s\S]*?```/g);
521
+ maskRange(/`[^`\n]+`/g);
522
+ return chars.join("");
523
+ }
524
+ function stripQuotes(raw) {
525
+ const trimmed = raw.trim();
526
+ if (trimmed.length >= 2 && trimmed[0] === trimmed.at(-1) && "`\"'".includes(trimmed[0])) {
527
+ return trimmed.slice(1, -1).trim();
528
+ }
529
+ return trimmed;
530
+ }
531
+ function resolveDeliverablePath(rawPath) {
532
+ const stripped = stripQuotes(rawPath);
533
+ const expanded = stripped.startsWith("~") ? path3.join(os3.homedir(), stripped.slice(1)) : stripped;
534
+ const candidate = path3.resolve(expanded);
535
+ const ext = candidate.split(".").pop()?.toLowerCase() ?? "";
536
+ if (!DELIVERABLE_EXTS.has(ext)) return null;
537
+ if (!existsSync(candidate)) return null;
538
+ try {
539
+ return realpathSync(candidate);
540
+ } catch {
541
+ return null;
542
+ }
543
+ }
544
+ function extractMediaMarkers(rawText) {
545
+ const scanText = maskProtectedSpans(rawText);
546
+ const attachments = [];
547
+ const spansToStrip = [];
548
+ for (const match of scanText.matchAll(MEDIA_TAG_RE)) {
549
+ const resolved = resolveDeliverablePath(match[1]);
550
+ if (!resolved) continue;
551
+ const ext = resolved.split(".").pop()?.toLowerCase() ?? "";
552
+ attachments.push({
553
+ kind: IMAGE_EXTS.has(ext) ? "image" : "file",
554
+ filePath: resolved
555
+ });
556
+ const start = match.index ?? 0;
557
+ spansToStrip.push([start, start + match[0].length]);
558
+ }
559
+ let cleaned = rawText;
560
+ for (const [start, end] of spansToStrip.reverse()) {
561
+ cleaned = cleaned.slice(0, start) + cleaned.slice(end);
562
+ }
563
+ return { text: cleaned.trim(), attachments };
564
+ }
565
+
566
+ // src/modules/agent/pi-coding-agent/adapter/pi-rpc-client.ts
567
+ import { spawn } from "child_process";
568
+ import { mkdir as mkdir3 } from "fs/promises";
569
+ import os4 from "os";
570
+ import path5 from "path";
571
+ import { StringDecoder } from "string_decoder";
572
+
573
+ // src/modules/agent/pi-coding-agent/adapter/pi-extension-path.ts
574
+ import { existsSync as existsSync2 } from "fs";
575
+ import path4 from "path";
576
+ import { fileURLToPath } from "url";
577
+
578
+ // src/config/env.ts
579
+ function isDevelopmentEnv() {
580
+ return process.env.NODE_ENV === "development";
581
+ }
582
+
583
+ // src/modules/agent/pi-coding-agent/adapter/pi-extension-path.ts
584
+ var MEDIA_PROMPT_RELATIVE_PATH = path4.join(
585
+ "src",
586
+ "modules",
587
+ "agent",
588
+ "pi-coding-agent",
589
+ "adapter",
590
+ "media-prompt.ts"
591
+ );
592
+ function resolveMediaPromptExtensionPath() {
593
+ const selfDir = path4.dirname(fileURLToPath(import.meta.url));
594
+ let dir = selfDir;
595
+ for (let i = 0; i < 10; i++) {
596
+ if (existsSync2(path4.join(dir, "package.json"))) {
597
+ if (isDevelopmentEnv()) {
598
+ const sourceCandidate = path4.join(dir, MEDIA_PROMPT_RELATIVE_PATH);
599
+ if (existsSync2(sourceCandidate)) {
600
+ return sourceCandidate;
601
+ }
602
+ throw new Error(
603
+ `NODE_ENV=development but could not locate ${MEDIA_PROMPT_RELATIVE_PATH} relative to the agent-bridge package root`
604
+ );
605
+ }
606
+ const bundledCandidate = path4.join(selfDir, "media-prompt.js");
607
+ if (existsSync2(bundledCandidate)) {
608
+ return bundledCandidate;
609
+ }
610
+ throw new Error(
611
+ "NODE_ENV is not development, so agent-bridge expects a built dist/media-prompt.js next to the bundled runtime. Run npm run build before starting in production mode."
612
+ );
613
+ }
614
+ const parent = path4.dirname(dir);
615
+ if (parent === dir) break;
616
+ dir = parent;
617
+ }
618
+ throw new Error(
619
+ "Could not locate the agent-bridge package root while resolving the media-prompt extension path"
620
+ );
621
+ }
622
+
623
+ // src/modules/agent/pi-coding-agent/adapter/pi-rpc-client.ts
624
+ function serializeJsonLine(value) {
625
+ return `${JSON.stringify(value)}
626
+ `;
627
+ }
628
+ function attachStrictJsonlReader(stream, onLine) {
629
+ const decoder = new StringDecoder("utf8");
630
+ let buffer = "";
631
+ const onData = (chunk) => {
632
+ buffer += typeof chunk === "string" ? chunk : decoder.write(chunk);
633
+ while (true) {
634
+ const newlineIndex = buffer.indexOf("\n");
635
+ if (newlineIndex === -1) break;
636
+ let line = buffer.slice(0, newlineIndex);
637
+ buffer = buffer.slice(newlineIndex + 1);
638
+ if (line.endsWith("\r")) {
639
+ line = line.slice(0, -1);
640
+ }
641
+ if (line.length > 0) {
642
+ onLine(line);
643
+ }
644
+ }
645
+ };
646
+ const onEnd = () => {
647
+ buffer += decoder.end();
648
+ const tail = buffer.endsWith("\r") ? buffer.slice(0, -1) : buffer;
649
+ if (tail.length > 0) {
650
+ onLine(tail);
651
+ }
652
+ };
653
+ stream.on("data", onData);
654
+ stream.on("end", onEnd);
655
+ return () => {
656
+ stream.off("data", onData);
657
+ stream.off("end", onEnd);
658
+ };
659
+ }
660
+ function defaultSessionDir() {
661
+ return path5.join(os4.homedir(), ".config", "agent-bridge", "pi-sessions");
662
+ }
663
+ var PiRpcClient = class {
664
+ #options;
665
+ #logger;
666
+ #process = null;
667
+ #stderr = "";
668
+ #requestId = 0;
669
+ #pendingRequests = /* @__PURE__ */ new Map();
670
+ #detachStdoutReader = null;
671
+ #settledWaiters = [];
672
+ #started = false;
673
+ #stopping = false;
674
+ #exitError = null;
675
+ #eventListeners = /* @__PURE__ */ new Set();
676
+ constructor(options) {
677
+ this.#options = {
678
+ agentSessionId: options.agentSessionId,
679
+ piSessionId: options.piSessionId,
680
+ cwd: options.cwd ?? process.cwd(),
681
+ sessionDir: options.sessionDir ?? defaultSessionDir(),
682
+ bin: options.bin ?? "pi",
683
+ model: options.model,
684
+ extraArgs: options.extraArgs ?? []
685
+ };
686
+ this.#logger = options.logger ?? createLogger("pi-rpc");
687
+ }
688
+ onEvent(listener) {
689
+ this.#eventListeners.add(listener);
690
+ return () => {
691
+ this.#eventListeners.delete(listener);
692
+ };
693
+ }
694
+ async start() {
695
+ if (this.#started) {
696
+ throw new Error("PiRpcClient already started");
697
+ }
698
+ await mkdir3(this.#options.sessionDir, { recursive: true });
699
+ const args = [
700
+ "--mode",
701
+ "rpc",
702
+ "--session-id",
703
+ this.#options.piSessionId,
704
+ "--session-dir",
705
+ this.#options.sessionDir,
706
+ "--extension",
707
+ resolveMediaPromptExtensionPath(),
708
+ ...this.#options.model ? ["--model", this.#options.model] : [],
709
+ ...this.#options.extraArgs
710
+ ];
711
+ const child = spawn(this.#options.bin, args, {
712
+ cwd: this.#options.cwd,
713
+ env: process.env,
714
+ stdio: ["pipe", "pipe", "pipe"]
715
+ });
716
+ this.#logger.info(
717
+ `spawned pi process (pid=${child.pid} bin=${this.#options.bin} args=${args.join(" ")} cwd=${this.#options.cwd})`
718
+ );
719
+ this.#process = child;
720
+ this.#started = true;
721
+ this.#exitError = null;
722
+ child.stderr.on("data", (chunk) => {
723
+ this.#stderr += chunk.toString();
724
+ this.#logger.debug(`pi stderr: ${chunk.toString().trimEnd()}`);
725
+ });
726
+ child.once("error", (error) => {
727
+ const wrapped = new Error(`pi RPC process error: ${error.message}`);
728
+ this.#handleExitError(wrapped);
729
+ });
730
+ child.once("exit", (code, signal) => {
731
+ const detail = signal ? `signal ${signal}` : `code ${String(code)}`;
732
+ const stderr = this.#stderr.trim();
733
+ const message = stderr ? `pi RPC process exited (${detail}): ${stderr}` : `pi RPC process exited (${detail})`;
734
+ this.#handleExitError(new Error(message));
735
+ });
736
+ this.#detachStdoutReader = attachStrictJsonlReader(child.stdout, (line) => {
737
+ this.#handleLine(line);
738
+ });
739
+ await new Promise((resolve) => setTimeout(resolve, 100));
740
+ if (this.#exitError) {
741
+ throw this.#exitError;
742
+ }
743
+ const state = await this.getState();
744
+ if (!state.sessionName) {
745
+ await this.setSessionName(this.#options.agentSessionId);
746
+ }
747
+ }
748
+ async stop() {
749
+ const child = this.#process;
750
+ if (!child) return;
751
+ this.#stopping = true;
752
+ this.#detachStdoutReader?.();
753
+ this.#detachStdoutReader = null;
754
+ await new Promise((resolve) => {
755
+ let finished = false;
756
+ const done = () => {
757
+ if (finished) return;
758
+ finished = true;
759
+ resolve();
760
+ };
761
+ child.once("exit", () => done());
762
+ child.kill("SIGTERM");
763
+ setTimeout(() => {
764
+ if (child.exitCode === null && child.signalCode === null) {
765
+ this.#logger.warn("pi process did not exit after SIGTERM, sending SIGKILL");
766
+ child.kill("SIGKILL");
767
+ }
768
+ done();
769
+ }, 1e3);
770
+ });
771
+ this.#process = null;
772
+ this.#started = false;
773
+ this.#rejectPending(new Error("pi RPC client stopped"));
774
+ }
775
+ async prompt(message) {
776
+ await this.#send({ type: "prompt", message });
777
+ }
778
+ async abort() {
779
+ await this.#send({ type: "abort" });
780
+ }
781
+ async compact(customInstructions) {
782
+ const response = await this.#send({ type: "compact", customInstructions });
783
+ const data = response.data;
784
+ return data ?? {};
785
+ }
786
+ async getLastAssistantText() {
787
+ const response = await this.#send({ type: "get_last_assistant_text" });
788
+ const data = response.data;
789
+ return data?.text ?? null;
790
+ }
791
+ async getState() {
792
+ const response = await this.#send({ type: "get_state" });
793
+ return response.data ?? {};
794
+ }
795
+ async setSessionName(name) {
796
+ await this.#send({ type: "set_session_name", name });
797
+ }
798
+ waitForSettled(timeoutMs = 10 * 60 * 1e3) {
799
+ if (!this.#process) {
800
+ return Promise.reject(new Error("pi RPC client is not running"));
801
+ }
802
+ return new Promise((resolve, reject) => {
803
+ const timeout = setTimeout(() => {
804
+ reject(new Error(`Timed out waiting for pi agent to settle after ${timeoutMs}ms`));
805
+ }, timeoutMs);
806
+ this.#settledWaiters.push({
807
+ resolve: () => {
808
+ clearTimeout(timeout);
809
+ resolve();
810
+ },
811
+ reject: (error) => {
812
+ clearTimeout(timeout);
813
+ reject(error);
814
+ }
815
+ });
816
+ });
817
+ }
818
+ async #send(command) {
819
+ if (!this.#process?.stdin.writable) {
820
+ throw this.#exitError ?? new Error("pi RPC process is not writable");
821
+ }
822
+ const id = `req-${++this.#requestId}`;
823
+ const payload = { ...command, id };
824
+ this.#logger.debug(`sending command (id=${id} type=${command.type})`);
825
+ return new Promise((resolve, reject) => {
826
+ this.#pendingRequests.set(id, { resolve, reject });
827
+ this.#process.stdin.write(serializeJsonLine(payload), (error) => {
828
+ if (!error) return;
829
+ this.#pendingRequests.delete(id);
830
+ this.#logger.error(`failed to write command to pi RPC stdin (id=${id}):`, error);
831
+ reject(new Error(`Failed to write to pi RPC stdin: ${error.message}`));
832
+ });
833
+ }).then((response) => {
834
+ this.#logger.debug(`received response (id=${id} type=${command.type} success=${response.success})`);
835
+ if (!response.success) {
836
+ this.#logger.error(
837
+ `pi RPC command failed (id=${id} type=${response.command}): ${response.error ?? "unknown error"}`
838
+ );
839
+ throw new Error(response.error ?? `pi RPC command failed: ${response.command}`);
840
+ }
841
+ return response;
842
+ });
843
+ }
844
+ #handleLine(line) {
845
+ let payload;
846
+ try {
847
+ payload = JSON.parse(line);
848
+ } catch (error) {
849
+ this.#logger.error("failed to parse line:", error);
850
+ return;
851
+ }
852
+ if (payload.type === "response") {
853
+ const response = payload;
854
+ const id = response.id;
855
+ if (!id) {
856
+ this.#logger.warn("ignoring RPC response without id:", JSON.stringify(response).slice(0, 500));
857
+ return;
858
+ }
859
+ const pending = this.#pendingRequests.get(id);
860
+ if (!pending) {
861
+ this.#logger.warn(`ignoring RPC response with unknown id (id=${id})`);
862
+ return;
863
+ }
864
+ this.#pendingRequests.delete(id);
865
+ pending.resolve(response);
866
+ return;
867
+ }
868
+ const event = payload;
869
+ this.#logger.debug(`received event (type=${event.type})`);
870
+ for (const listener of this.#eventListeners) {
871
+ listener(event);
872
+ }
873
+ if (event.type === "agent_settled") {
874
+ const waiters = this.#settledWaiters.splice(0);
875
+ for (const waiter of waiters) {
876
+ waiter.resolve();
877
+ }
878
+ }
879
+ }
880
+ #handleExitError(error) {
881
+ if (this.#stopping) {
882
+ this.#logger.debug(`pi process exited during stop: ${error.message}`);
883
+ } else {
884
+ this.#logger.error(error.message);
885
+ }
886
+ this.#stopping = false;
887
+ this.#exitError = error;
888
+ this.#process = null;
889
+ this.#started = false;
890
+ this.#detachStdoutReader?.();
891
+ this.#detachStdoutReader = null;
892
+ this.#rejectPending(error);
893
+ }
894
+ #rejectPending(error) {
895
+ for (const [id, pending] of this.#pendingRequests) {
896
+ this.#pendingRequests.delete(id);
897
+ pending.reject(error);
898
+ }
899
+ const waiters = this.#settledWaiters.splice(0);
900
+ for (const waiter of waiters) {
901
+ waiter.reject(error);
902
+ }
903
+ }
904
+ };
905
+
906
+ // src/modules/agent/pi-coding-agent/adapter/pi-session-id.ts
907
+ import crypto from "crypto";
908
+ function trimToAlnumEdges(value) {
909
+ return value.replace(/^[^A-Za-z0-9]+/, "").replace(/[^A-Za-z0-9]+$/, "");
910
+ }
911
+ function toPiSessionId(agentSessionId) {
912
+ const normalized = trimToAlnumEdges(agentSessionId.replace(/[^A-Za-z0-9._-]+/g, "."));
913
+ if (normalized && /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/.test(normalized)) {
914
+ return normalized;
915
+ }
916
+ const digest = crypto.createHash("sha256").update(agentSessionId).digest("hex").slice(0, 24);
917
+ return `agent-${digest}`;
918
+ }
919
+
920
+ // src/modules/agent/pi-coding-agent/adapter/pi-coding-agent-adapter.ts
921
+ var PiCodingAgentAdapter = class {
922
+ #agentSessionId;
923
+ #piSessionId;
924
+ #cwd;
925
+ #sessionDir;
926
+ #bin;
927
+ #model;
928
+ #extraArgs;
929
+ #logger;
930
+ #client = null;
931
+ #onOutput = null;
932
+ #inputQueue = [];
933
+ #processing = false;
934
+ constructor({
935
+ agentSessionId,
936
+ cwd,
937
+ sessionDir,
938
+ bin,
939
+ model,
940
+ extraArgs,
941
+ logger: logger3
942
+ }) {
943
+ this.#agentSessionId = agentSessionId;
944
+ this.#piSessionId = toPiSessionId(agentSessionId);
945
+ this.#cwd = cwd ?? process.cwd();
946
+ this.#sessionDir = sessionDir;
947
+ this.#bin = bin ?? "pi";
948
+ this.#model = model;
949
+ this.#extraArgs = extraArgs ?? [];
950
+ this.#logger = logger3 ?? createLogger("pi-coding-agent");
951
+ }
952
+ async start(onOutput) {
953
+ this.#onOutput = onOutput;
954
+ this.#client = new PiRpcClient({
955
+ agentSessionId: this.#agentSessionId,
956
+ piSessionId: this.#piSessionId,
957
+ cwd: this.#cwd,
958
+ sessionDir: this.#sessionDir,
959
+ bin: this.#bin,
960
+ model: this.#model,
961
+ extraArgs: this.#extraArgs,
962
+ logger: this.#logger
963
+ });
964
+ this.#client.onEvent((rpcEvent) => {
965
+ void this.#handleRpcEvent(rpcEvent);
966
+ if (rpcEvent.type === "extension_error") {
967
+ this.#logger.error(`extension_error for ${this.#agentSessionId}:`, rpcEvent);
968
+ }
969
+ });
970
+ this.#logger.info(`starting agent instance (bin=${this.#bin} cwd=${this.#cwd})`);
971
+ await this.#client.start();
972
+ this.#logger.info(`session ${this.#agentSessionId} started (piSessionId=${this.#piSessionId})`);
973
+ }
974
+ async stop() {
975
+ this.#inputQueue.length = 0;
976
+ await this.#client?.stop();
977
+ this.#client = null;
978
+ this.#processing = false;
979
+ this.#onOutput = null;
980
+ this.#logger.info(`session ${this.#agentSessionId} stopped`);
981
+ }
982
+ async abort() {
983
+ this.#logger.info(`aborting agent turn (session=${this.#agentSessionId})`);
984
+ await this.#client?.abort();
985
+ }
986
+ async input(event) {
987
+ if (!this.#client || !this.#onOutput) {
988
+ throw new Error("PiCodingAgentAdapter is not started");
989
+ }
990
+ this.#inputQueue.push(event);
991
+ this.#logger.debug(
992
+ `input event queued (session=${this.#agentSessionId} type=${event.type} queueDepth=${this.#inputQueue.length})`
993
+ );
994
+ void this.#drainInputQueue();
995
+ }
996
+ async isBusy() {
997
+ return this.#processing || this.#inputQueue.length > 0;
998
+ }
999
+ async #drainInputQueue() {
1000
+ if (this.#processing) {
1001
+ return;
1002
+ }
1003
+ this.#processing = true;
1004
+ try {
1005
+ while (this.#client && this.#onOutput && this.#inputQueue.length > 0) {
1006
+ const event = this.#inputQueue.shift();
1007
+ if (!event) continue;
1008
+ await this.#processEvent(event);
1009
+ }
1010
+ } finally {
1011
+ this.#processing = false;
1012
+ }
1013
+ }
1014
+ async #processEvent(event) {
1015
+ if (!this.#client) {
1016
+ throw new Error("PiCodingAgentAdapter is not started");
1017
+ }
1018
+ try {
1019
+ if (event.type === "user.message") {
1020
+ this.#logger.info(`sending prompt to agent (session=${this.#agentSessionId})`);
1021
+ const startedAt = Date.now();
1022
+ await this.#emitProgress({
1023
+ type: "assistant.thinking",
1024
+ agentSessionId: this.#agentSessionId,
1025
+ text: "Processing request"
1026
+ });
1027
+ await this.#client.prompt(event.text);
1028
+ await this.#client.waitForSettled();
1029
+ const rawText = await this.#client.getLastAssistantText();
1030
+ const { text: text2, attachments } = extractMediaMarkers(rawText ?? "(pi returned no assistant text)");
1031
+ this.#logger.debug(
1032
+ `prompt settled (session=${this.#agentSessionId} durationMs=${Date.now() - startedAt} replyLength=${text2.length} attachments=${attachments.length})`
1033
+ );
1034
+ await this.#emitAssistant(text2, attachments);
1035
+ return;
1036
+ }
1037
+ this.#logger.info(`compacting context (session=${this.#agentSessionId})`);
1038
+ await this.#emitProgress({
1039
+ type: "session.compacting",
1040
+ agentSessionId: this.#agentSessionId,
1041
+ text: "Compacting context"
1042
+ });
1043
+ const result = await this.#client.compact();
1044
+ this.#logger.debug(
1045
+ `compact finished (session=${this.#agentSessionId} estimatedTokensAfter=${result.estimatedTokensAfter ?? "unknown"})`
1046
+ );
1047
+ const suffix = typeof result.estimatedTokensAfter === "number" ? ` Estimated tokens after: ${result.estimatedTokensAfter}.` : "";
1048
+ await this.#emitAssistant(`Context compacted.${suffix}`);
1049
+ } catch (error) {
1050
+ const message = error instanceof Error ? error.message : String(error);
1051
+ this.#logger.error(
1052
+ `event processing failed (session=${this.#agentSessionId} type=${event.type}):`,
1053
+ error
1054
+ );
1055
+ await this.#emitAssistant(`[pi-coding-agent error] ${message}`);
1056
+ }
1057
+ }
1058
+ async #emitAssistant(text2, attachments) {
1059
+ if (!this.#onOutput) {
1060
+ this.#logger.error(`dropped assistant output for stopped session ${this.#agentSessionId}`);
1061
+ return;
1062
+ }
1063
+ await this.#onOutput({
1064
+ type: "assistant.message",
1065
+ agentSessionId: this.#agentSessionId,
1066
+ text: text2,
1067
+ attachments
1068
+ });
1069
+ }
1070
+ async #emitProgress(event) {
1071
+ if (!this.#onOutput) {
1072
+ return;
1073
+ }
1074
+ await this.#onOutput(event);
1075
+ }
1076
+ async #handleRpcEvent(rpcEvent) {
1077
+ if (!this.#onOutput) {
1078
+ return;
1079
+ }
1080
+ if (rpcEvent.type === "tool_execution_start") {
1081
+ const toolName = typeof rpcEvent.toolName === "string" ? rpcEvent.toolName : "unknown";
1082
+ await this.#emitProgress({
1083
+ type: "assistant.tool.running",
1084
+ agentSessionId: this.#agentSessionId,
1085
+ toolName,
1086
+ text: `Running ${toolName}`
1087
+ });
1088
+ return;
1089
+ }
1090
+ if (rpcEvent.type === "tool_execution_end") {
1091
+ const toolName = typeof rpcEvent.toolName === "string" ? rpcEvent.toolName : "unknown";
1092
+ const isError = Boolean(rpcEvent.isError);
1093
+ await this.#emitProgress({
1094
+ type: isError ? "assistant.tool.error" : "assistant.tool.done",
1095
+ agentSessionId: this.#agentSessionId,
1096
+ toolName,
1097
+ text: isError ? `Failed ${toolName}` : `Finished ${toolName}`
1098
+ });
1099
+ }
1100
+ }
1101
+ };
1102
+
1103
+ // src/modules/agent/pi-coding-agent/index.ts
1104
+ var logger = createLogger("pi-coding-agent");
1105
+ function parseExtraArgs(raw) {
1106
+ if (!raw) return [];
1107
+ return raw.split(" ").map((part) => part.trim()).filter(Boolean);
1108
+ }
1109
+ function buildAdapter(config, agentSessionId) {
1110
+ return new PiCodingAgentAdapter({
1111
+ agentSessionId,
1112
+ cwd: process.cwd(),
1113
+ sessionDir: config.sessionDir ?? process.env.PI_SESSION_DIR ?? path6.join(os5.homedir(), ".config", "agent-bridge", "pi-sessions"),
1114
+ bin: config.bin ?? process.env.PI_BIN ?? "pi",
1115
+ model: config.model ?? process.env.PI_MODEL,
1116
+ extraArgs: config.extraArgs ?? parseExtraArgs(process.env.PI_RPC_EXTRA_ARGS)
1117
+ });
1118
+ }
1119
+ function createPiCodingAgentConfigCollector() {
1120
+ return {
1121
+ async collect(ctx) {
1122
+ const model = await ctx.input("Pi model (leave empty for pi default)");
1123
+ return model ? { model } : {};
1124
+ },
1125
+ validate(config) {
1126
+ if (config.model !== void 0 && !config.model.trim()) {
1127
+ throw new Error("Pi model must be non-empty when provided");
1128
+ }
1129
+ },
1130
+ summarize(config) {
1131
+ return `type=pi-coding-agent model=${config.model ?? "default"}`;
1132
+ }
1133
+ };
1134
+ }
1135
+ var piCodingAgentModule = {
1136
+ type: "pi-coding-agent",
1137
+ createConfigCollector: createPiCodingAgentConfigCollector,
1138
+ async createAgentSession({ config }) {
1139
+ const agentSessionId = `pi-coding-agent:${randomUUID()}`;
1140
+ logger.info(`creating agent session ${agentSessionId}`);
1141
+ return {
1142
+ agentSessionId,
1143
+ agentAdapter: buildAdapter(config, agentSessionId)
1144
+ };
1145
+ },
1146
+ async resumeAgentSession({ config, agentSessionId }) {
1147
+ logger.info(`resuming agent session ${agentSessionId}`);
1148
+ return buildAdapter(config, agentSessionId);
1149
+ }
1150
+ };
1151
+
1152
+ // src/modules/agent/index.ts
1153
+ var registry = /* @__PURE__ */ new Map([[piCodingAgentModule.type, piCodingAgentModule]]);
1154
+ function listAgentModules() {
1155
+ return [...registry.values()];
1156
+ }
1157
+ function getAgentModule(type) {
1158
+ return registry.get(type);
1159
+ }
1160
+ function getTypedAgentModule(config) {
1161
+ const module = registry.get(config.type);
1162
+ if (!module) {
1163
+ throw new Error(`Unsupported agent module type: ${config.type}`);
1164
+ }
1165
+ return module;
1166
+ }
1167
+
1168
+ // src/modules/client/feishu/adapter/feishu-client.ts
1169
+ import * as Lark from "@larksuiteoapi/node-sdk";
1170
+ import { existsSync as existsSync3, mkdirSync, writeFileSync } from "fs";
1171
+ import { basename, join } from "path";
1172
+ import { tmpdir } from "os";
1173
+ var DEDUP_TTL_MS = 12 * 60 * 60 * 1e3;
1174
+ var DEDUP_MAX_ENTRIES = 5e3;
1175
+ var MESSAGE_EXPIRY_MS = 30 * 60 * 1e3;
1176
+ var DEDUP_SWEEP_INTERVAL_MS = 5 * 60 * 1e3;
1177
+ var REACTION_TYPING = "Typing";
1178
+ function createChannel(config, logger3) {
1179
+ const domain = config.domain === "lark" ? Lark.Domain.Lark : Lark.Domain.Feishu;
1180
+ return Lark.createLarkChannel({
1181
+ appId: config.appId,
1182
+ appSecret: config.appSecret,
1183
+ domain,
1184
+ logger: {
1185
+ error: (...args) => logger3.error(...args),
1186
+ warn: (...args) => logger3.warn(...args),
1187
+ info: (...args) => logger3.info(...args),
1188
+ debug: (...args) => logger3.debug(...args),
1189
+ trace: (...args) => logger3.debug(...args)
1190
+ },
1191
+ loggerLevel: Lark.LoggerLevel.warn,
1192
+ safety: {
1193
+ dedup: {
1194
+ ttl: DEDUP_TTL_MS,
1195
+ maxEntries: DEDUP_MAX_ENTRIES,
1196
+ sweepIntervalMs: DEDUP_SWEEP_INTERVAL_MS
1197
+ },
1198
+ staleMessageWindowMs: MESSAGE_EXPIRY_MS
1199
+ },
1200
+ policy: {
1201
+ requireMention: false
1202
+ },
1203
+ webhook: {
1204
+ encryptKey: config.encryptKey,
1205
+ verificationToken: config.verificationToken
1206
+ }
1207
+ });
1208
+ }
1209
+ function createClient(config) {
1210
+ const domain = config.domain === "lark" ? Lark.Domain.Lark : Lark.Domain.Feishu;
1211
+ return new Lark.Client({
1212
+ appId: config.appId,
1213
+ appSecret: config.appSecret,
1214
+ domain
1215
+ });
1216
+ }
1217
+ function buildMarkdownCard(text2) {
1218
+ return JSON.stringify({
1219
+ schema: "2.0",
1220
+ body: {
1221
+ elements: [
1222
+ {
1223
+ tag: "markdown",
1224
+ content: text2
1225
+ }
1226
+ ]
1227
+ }
1228
+ });
1229
+ }
1230
+ function buildCardPayload(card) {
1231
+ return JSON.stringify(card);
1232
+ }
1233
+ var FeishuClient = class {
1234
+ #logger;
1235
+ #channel;
1236
+ #client;
1237
+ #onMessage = null;
1238
+ #unsubscribe = [];
1239
+ #typingReactionByChatId = /* @__PURE__ */ new Map();
1240
+ constructor(config, logger3 = createLogger("feishu")) {
1241
+ this.#logger = logger3;
1242
+ this.#channel = createChannel(config, logger3);
1243
+ this.#client = createClient(config);
1244
+ }
1245
+ setOnMessage(onMessage) {
1246
+ this.#onMessage = onMessage;
1247
+ }
1248
+ async connect() {
1249
+ this.#unsubscribe = [
1250
+ this.#channel.on("message", (message) => {
1251
+ void this.#handleMessage(message);
1252
+ }),
1253
+ this.#channel.on("reject", (event) => {
1254
+ this.#logger.debug(
1255
+ `channel rejected inbound message (reason=${event.reason} chatId=${event.chatId} messageId=${event.messageId})`
1256
+ );
1257
+ }),
1258
+ this.#channel.on("error", (error) => {
1259
+ this.#logger.error("channel error:", error);
1260
+ }),
1261
+ this.#channel.on("reconnecting", () => {
1262
+ this.#logger.info("channel reconnecting");
1263
+ }),
1264
+ this.#channel.on("reconnected", () => {
1265
+ this.#logger.info("channel reconnected");
1266
+ })
1267
+ ];
1268
+ this.#logger.info("starting channel connection");
1269
+ await this.#channel.connect();
1270
+ this.#logger.info("channel connected");
1271
+ }
1272
+ async disconnect() {
1273
+ for (const unsubscribe of this.#unsubscribe.splice(0)) {
1274
+ try {
1275
+ unsubscribe();
1276
+ } catch (error) {
1277
+ this.#logger.debug("ignored channel unsubscribe error:", error);
1278
+ }
1279
+ }
1280
+ await this.#channel.disconnect();
1281
+ }
1282
+ async sendText(chatId, text2, replyToMessageId) {
1283
+ await this.sendMarkdown(chatId, text2, replyToMessageId);
1284
+ }
1285
+ async sendMarkdown(chatId, text2, replyToMessageId) {
1286
+ const content = buildMarkdownCard(text2);
1287
+ await this.#sendInteractive(chatId, content, replyToMessageId);
1288
+ this.#logger.debug(
1289
+ `markdown message sent (chatId=${chatId} replyTo=${replyToMessageId ?? "none"} length=${text2.length})`
1290
+ );
1291
+ }
1292
+ async sendCard(chatId, card, replyToMessageId) {
1293
+ const content = buildCardPayload(card);
1294
+ const response = await this.#sendInteractive(chatId, content, replyToMessageId);
1295
+ return response.data?.message_id ?? null;
1296
+ }
1297
+ async updateCard(messageId, card) {
1298
+ await this.#client.im.message.patch({
1299
+ path: {
1300
+ message_id: messageId
1301
+ },
1302
+ data: {
1303
+ content: buildCardPayload(card)
1304
+ }
1305
+ });
1306
+ }
1307
+ /** Download an inbound image/file/audio/video resource to a local temp path. */
1308
+ async downloadResource(messageId, fileKey, resourceType, fileName) {
1309
+ try {
1310
+ const resp = await this.#client.im.messageResource.get({
1311
+ path: { message_id: messageId, file_key: fileKey },
1312
+ params: { type: resourceType }
1313
+ });
1314
+ if (!resp) return null;
1315
+ const ext = resourceType === "image" ? ".png" : resourceType === "audio" ? ".ogg" : "";
1316
+ const safeName = fileName && fileName.length > 0 ? fileName.replace(/[^a-zA-Z0-9._-]/g, "_") : `${fileKey}${ext}`;
1317
+ const dir = join(tmpdir(), "agent-bridge-feishu-media");
1318
+ if (!existsSync3(dir)) {
1319
+ mkdirSync(dir, { recursive: true });
1320
+ }
1321
+ const localPath = join(dir, `${Date.now()}-${safeName}`);
1322
+ if (typeof resp.writeFile === "function") {
1323
+ await resp.writeFile(localPath);
1324
+ return localPath;
1325
+ }
1326
+ if (typeof resp.getReadableStream === "function") {
1327
+ const chunks = [];
1328
+ for await (const chunk of resp.getReadableStream()) {
1329
+ chunks.push(Buffer.from(chunk));
1330
+ }
1331
+ writeFileSync(localPath, Buffer.concat(chunks));
1332
+ return localPath;
1333
+ }
1334
+ this.#logger.warn(`resource response had neither writeFile nor getReadableStream (fileKey=${fileKey})`);
1335
+ return null;
1336
+ } catch (error) {
1337
+ this.#logger.error(`failed to download resource (fileKey=${fileKey}):`, error);
1338
+ return null;
1339
+ }
1340
+ }
1341
+ /** Upload and send a local image/file as a native Feishu attachment. */
1342
+ async sendAttachment(chatId, attachment, replyToMessageId) {
1343
+ const input = attachment.kind === "image" ? { image: { source: attachment.filePath } } : {
1344
+ file: {
1345
+ source: attachment.filePath,
1346
+ fileName: attachment.fileName ?? basename(attachment.filePath)
1347
+ }
1348
+ };
1349
+ await this.#channel.send(chatId, input, replyToMessageId ? { replyTo: replyToMessageId } : void 0);
1350
+ }
1351
+ async #sendInteractive(chatId, content, replyToMessageId) {
1352
+ try {
1353
+ if (replyToMessageId) {
1354
+ return await this.#client.im.message.reply({
1355
+ path: {
1356
+ message_id: replyToMessageId
1357
+ },
1358
+ data: {
1359
+ content,
1360
+ msg_type: "interactive"
1361
+ }
1362
+ });
1363
+ }
1364
+ return await this.#client.im.message.create({
1365
+ params: {
1366
+ receive_id_type: "chat_id"
1367
+ },
1368
+ data: {
1369
+ receive_id: chatId,
1370
+ content,
1371
+ msg_type: "interactive"
1372
+ }
1373
+ });
1374
+ } catch (error) {
1375
+ if (replyToMessageId && (error?.code === 230011 || error?.code === 231003)) {
1376
+ this.#logger.warn(
1377
+ `reply target unavailable, falling back to create (chatId=${chatId} replyTo=${replyToMessageId})`
1378
+ );
1379
+ return await this.#client.im.message.create({
1380
+ params: {
1381
+ receive_id_type: "chat_id"
1382
+ },
1383
+ data: {
1384
+ receive_id: chatId,
1385
+ content,
1386
+ msg_type: "interactive"
1387
+ }
1388
+ });
1389
+ }
1390
+ throw error;
1391
+ }
1392
+ }
1393
+ async startTyping(chatId, messageId) {
1394
+ try {
1395
+ const response = await this.#client.im.messageReaction.create({
1396
+ path: {
1397
+ message_id: messageId
1398
+ },
1399
+ data: {
1400
+ reaction_type: {
1401
+ emoji_type: REACTION_TYPING
1402
+ }
1403
+ }
1404
+ });
1405
+ const reactionId = response.data?.reaction_id;
1406
+ if (reactionId) {
1407
+ this.#typingReactionByChatId.set(chatId, { messageId, reactionId });
1408
+ }
1409
+ } catch (error) {
1410
+ this.#logger.debug(`failed to add typing reaction (chatId=${chatId} messageId=${messageId})`, error);
1411
+ }
1412
+ }
1413
+ async stopTyping(chatId) {
1414
+ const entry = this.#typingReactionByChatId.get(chatId);
1415
+ if (!entry) {
1416
+ return;
1417
+ }
1418
+ this.#typingReactionByChatId.delete(chatId);
1419
+ try {
1420
+ await this.#client.im.messageReaction.delete({
1421
+ path: {
1422
+ message_id: entry.messageId,
1423
+ reaction_id: entry.reactionId
1424
+ }
1425
+ });
1426
+ } catch (error) {
1427
+ this.#logger.debug(
1428
+ `failed to remove typing reaction (chatId=${chatId} messageId=${entry.messageId})`,
1429
+ error
1430
+ );
1431
+ }
1432
+ }
1433
+ async #handleMessage(message) {
1434
+ if (!message.content && message.resources.length === 0) {
1435
+ this.#logger.debug(`dropping message with no content or resources (messageId=${message.messageId})`);
1436
+ return;
1437
+ }
1438
+ let text2 = message.content ?? "";
1439
+ for (const resource of message.resources) {
1440
+ if (resource.type === "sticker") continue;
1441
+ const localPath = await this.downloadResource(
1442
+ message.messageId,
1443
+ resource.fileKey,
1444
+ resource.type,
1445
+ resource.fileName
1446
+ );
1447
+ if (localPath) {
1448
+ text2 += `
1449
+ [Received ${resource.type}: ${localPath}]`;
1450
+ }
1451
+ }
1452
+ await this.#onMessage?.({
1453
+ chatId: message.chatId,
1454
+ chatType: message.chatType,
1455
+ messageId: message.messageId,
1456
+ text: text2,
1457
+ mentionedBot: message.mentionedBot,
1458
+ raw: message.raw
1459
+ });
1460
+ }
1461
+ };
1462
+
1463
+ // src/modules/client/feishu/adapter/feishu-session.ts
1464
+ function buildFeishuSessionId(chatType, chatId) {
1465
+ if (!chatId) {
1466
+ throw new Error("chatId is required");
1467
+ }
1468
+ if (chatType === "p2p" || chatType === "dm") {
1469
+ return `feishu:dm:${chatId}`;
1470
+ }
1471
+ if (chatType === "group") {
1472
+ return `feishu:group:${chatId}`;
1473
+ }
1474
+ throw new Error(`Unsupported Feishu chat type: ${chatType}`);
1475
+ }
1476
+ function parseFeishuSessionId(clientSessionId) {
1477
+ if (clientSessionId.startsWith("feishu:dm:")) {
1478
+ return { platform: "feishu", chatType: "dm", chatId: clientSessionId.slice("feishu:dm:".length) };
1479
+ }
1480
+ if (clientSessionId.startsWith("feishu:group:")) {
1481
+ return { platform: "feishu", chatType: "group", chatId: clientSessionId.slice("feishu:group:".length) };
1482
+ }
1483
+ throw new Error(`Unsupported clientSessionId: ${clientSessionId}`);
1484
+ }
1485
+
1486
+ // src/modules/client/feishu/adapter/feishu-im-adapter.ts
1487
+ var MAX_TEXT_CHUNK = 4e3;
1488
+ function chunkText(text2, maxLen) {
1489
+ if (text2.length <= maxLen) return [text2];
1490
+ const chunks = [];
1491
+ let remaining = text2;
1492
+ while (remaining.length > 0) {
1493
+ if (remaining.length <= maxLen) {
1494
+ chunks.push(remaining);
1495
+ break;
1496
+ }
1497
+ let splitPos = remaining.lastIndexOf("\n", maxLen);
1498
+ if (splitPos <= 0) {
1499
+ splitPos = remaining.lastIndexOf(" ", maxLen);
1500
+ }
1501
+ if (splitPos <= 0) {
1502
+ chunks.push(remaining.slice(0, maxLen));
1503
+ remaining = remaining.slice(maxLen);
1504
+ continue;
1505
+ }
1506
+ chunks.push(remaining.slice(0, splitPos + 1));
1507
+ remaining = remaining.slice(splitPos + 1);
1508
+ }
1509
+ return chunks.filter((chunk) => chunk.length > 0);
1510
+ }
1511
+ var FeishuIMAdapter = class _FeishuIMAdapter {
1512
+ #config;
1513
+ #logger;
1514
+ #onOutput = null;
1515
+ #client = null;
1516
+ #egressQueue = [];
1517
+ #processing = false;
1518
+ #lastInboundMessageIdBySession = /* @__PURE__ */ new Map();
1519
+ #progressStateBySession = /* @__PURE__ */ new Map();
1520
+ static buildProgressCard(lines, _status, collapsedCount = 0) {
1521
+ return {
1522
+ schema: "2.0",
1523
+ body: {
1524
+ elements: [
1525
+ {
1526
+ tag: "markdown",
1527
+ content: _FeishuIMAdapter.progressBody(lines, collapsedCount)
1528
+ }
1529
+ ]
1530
+ }
1531
+ };
1532
+ }
1533
+ static progressBody(lines, collapsedCount) {
1534
+ const contentLines = [];
1535
+ if (collapsedCount > 0) {
1536
+ contentLines.push(`- Collapsed ${collapsedCount} earlier updates.`);
1537
+ }
1538
+ if (lines.length > 0) {
1539
+ contentLines.push(...lines);
1540
+ }
1541
+ return contentLines.length > 0 ? contentLines.join("\n") : "No progress yet.";
1542
+ }
1543
+ async #notifySendFailure(chatId, error) {
1544
+ if (!this.#client) {
1545
+ return;
1546
+ }
1547
+ const message = error instanceof Error ? error.message : String(error);
1548
+ const text2 = `[agent-bridge error] Message delivery failed
1549
+
1550
+ ${message}`;
1551
+ try {
1552
+ await this.#client.sendText(chatId, text2);
1553
+ } catch (notifyError) {
1554
+ this.#logger.error("failed to notify send failure:", notifyError);
1555
+ }
1556
+ }
1557
+ constructor(config, logger3 = createLogger("feishu")) {
1558
+ this.#config = config;
1559
+ this.#logger = logger3;
1560
+ }
1561
+ async start(onOutput) {
1562
+ this.#onOutput = onOutput;
1563
+ this.#client = new FeishuClient(this.#config, this.#logger);
1564
+ this.#client.setOnMessage(async ({ chatId, chatType, text: text2, messageId, mentionedBot }) => {
1565
+ if (!this.#onOutput) {
1566
+ this.#logger.warn(`dropping inbound message, adapter not ready (chatId=${chatId})`);
1567
+ return;
1568
+ }
1569
+ const clientSessionId = buildFeishuSessionId(chatType, chatId);
1570
+ if (chatType === "group" && (this.#config.requireMentionInGroup ?? true) && !mentionedBot) {
1571
+ this.#logger.debug(
1572
+ `ignoring group message without bot mention (session=${clientSessionId} messageId=${messageId})`
1573
+ );
1574
+ return;
1575
+ }
1576
+ this.#lastInboundMessageIdBySession.set(clientSessionId, messageId);
1577
+ this.#resetProgressState(clientSessionId);
1578
+ await this.#client?.startTyping(chatId, messageId);
1579
+ const normalizedText = text2.trim();
1580
+ if (normalizedText === "/new") {
1581
+ this.#logger.info(`received command /new (session=${clientSessionId})`);
1582
+ await this.#onOutput({
1583
+ type: "command.session.new",
1584
+ clientSessionId
1585
+ });
1586
+ return;
1587
+ }
1588
+ if (normalizedText === "/compact") {
1589
+ this.#logger.info(`received command /compact (session=${clientSessionId})`);
1590
+ await this.#onOutput({
1591
+ type: "command.session.compact",
1592
+ clientSessionId
1593
+ });
1594
+ return;
1595
+ }
1596
+ if (normalizedText === "/stop") {
1597
+ this.#logger.info(`received command /stop (session=${clientSessionId})`);
1598
+ await this.#onOutput({
1599
+ type: "command.session.stop",
1600
+ clientSessionId
1601
+ });
1602
+ return;
1603
+ }
1604
+ this.#logger.info(`received user message (session=${clientSessionId}): ${normalizedText}`);
1605
+ await this.#onOutput({
1606
+ type: "user.message",
1607
+ clientSessionId,
1608
+ text: text2
1609
+ });
1610
+ });
1611
+ await this.#client.connect();
1612
+ this.#logger.info(`adapter started (domain=${this.#config.domain ?? "feishu"})`);
1613
+ }
1614
+ async stop() {
1615
+ this.#egressQueue.length = 0;
1616
+ if (this.#client) {
1617
+ await this.#client.disconnect();
1618
+ this.#client = null;
1619
+ }
1620
+ this.#processing = false;
1621
+ this.#onOutput = null;
1622
+ this.#logger.info("adapter stopped");
1623
+ }
1624
+ async input(event) {
1625
+ if (!this.#client) {
1626
+ throw new Error("FeishuIMAdapter is not started");
1627
+ }
1628
+ this.#egressQueue.push(event);
1629
+ this.#logger.debug(
1630
+ `egress event queued (session=${event.clientSessionId} queueDepth=${this.#egressQueue.length})`
1631
+ );
1632
+ void this.#drainEgressQueue();
1633
+ }
1634
+ async isBusy() {
1635
+ return this.#processing || this.#egressQueue.length > 0;
1636
+ }
1637
+ async #drainEgressQueue() {
1638
+ if (this.#processing) {
1639
+ return;
1640
+ }
1641
+ this.#processing = true;
1642
+ try {
1643
+ while (this.#client && this.#egressQueue.length > 0) {
1644
+ const event = this.#egressQueue.shift();
1645
+ if (!event) continue;
1646
+ try {
1647
+ const target = parseFeishuSessionId(event.clientSessionId);
1648
+ if (event.type !== "assistant.message") {
1649
+ await this.#handleProgressEvent(target.chatId, event);
1650
+ continue;
1651
+ }
1652
+ const replyToMessageId = this.#lastInboundMessageIdBySession.get(event.clientSessionId);
1653
+ this.#logger.info(`sending reply (session=${event.clientSessionId})`);
1654
+ if (event.text.trim().length > 0) {
1655
+ const chunks = chunkText(event.text, MAX_TEXT_CHUNK);
1656
+ for (const [index, chunk] of chunks.entries()) {
1657
+ await this.#client.sendText(target.chatId, chunk, index === 0 ? replyToMessageId : void 0);
1658
+ }
1659
+ }
1660
+ for (const attachment of event.attachments ?? []) {
1661
+ try {
1662
+ await this.#client.sendAttachment(target.chatId, attachment, replyToMessageId);
1663
+ } catch (attachmentError) {
1664
+ this.#logger.error("failed to send attachment:", attachmentError);
1665
+ await this.#notifySendFailure(target.chatId, attachmentError);
1666
+ }
1667
+ }
1668
+ await this.#client.stopTyping(target.chatId);
1669
+ this.#logger.debug(`reply sent (session=${event.clientSessionId})`);
1670
+ } catch (error) {
1671
+ this.#logger.error("failed to send egress event:", error);
1672
+ try {
1673
+ const target = parseFeishuSessionId(event.clientSessionId);
1674
+ await this.#client.stopTyping(target.chatId);
1675
+ await this.#notifySendFailure(target.chatId, error);
1676
+ } catch (notifyError) {
1677
+ this.#logger.error("failed to handle egress send failure:", notifyError);
1678
+ }
1679
+ }
1680
+ }
1681
+ } finally {
1682
+ this.#processing = false;
1683
+ }
1684
+ }
1685
+ async #handleProgressEvent(chatId, event) {
1686
+ if (!this.#client) {
1687
+ return;
1688
+ }
1689
+ if (!this.#shouldRenderProgressEvent(event)) {
1690
+ return;
1691
+ }
1692
+ const state = this.#progressStateBySession.get(event.clientSessionId) ?? {
1693
+ messageId: null,
1694
+ creating: false,
1695
+ lines: [],
1696
+ status: "running",
1697
+ turnId: 0,
1698
+ collapsedCount: 0
1699
+ };
1700
+ state.lines.push(this.#formatProgressLine(event));
1701
+ if (state.lines.length > 10) {
1702
+ state.collapsedCount += state.lines.length - 10;
1703
+ state.lines.splice(0, state.lines.length - 10);
1704
+ }
1705
+ state.status = this.#progressStatus(event);
1706
+ this.#progressStateBySession.set(event.clientSessionId, state);
1707
+ const card = _FeishuIMAdapter.buildProgressCard(state.lines, state.status, state.collapsedCount);
1708
+ if (state.messageId) {
1709
+ await this.#client.updateCard(state.messageId, card);
1710
+ return;
1711
+ }
1712
+ if (state.creating) {
1713
+ return;
1714
+ }
1715
+ state.creating = true;
1716
+ try {
1717
+ state.messageId = await this.#client.sendCard(
1718
+ chatId,
1719
+ card,
1720
+ this.#lastInboundMessageIdBySession.get(event.clientSessionId)
1721
+ );
1722
+ } finally {
1723
+ state.creating = false;
1724
+ }
1725
+ }
1726
+ #shouldRenderProgressEvent(event) {
1727
+ return event.type !== "assistant.thinking";
1728
+ }
1729
+ #resetProgressState(clientSessionId) {
1730
+ const previous = this.#progressStateBySession.get(clientSessionId);
1731
+ this.#progressStateBySession.set(clientSessionId, {
1732
+ messageId: null,
1733
+ creating: false,
1734
+ lines: [],
1735
+ status: "running",
1736
+ turnId: (previous?.turnId ?? 0) + 1,
1737
+ collapsedCount: 0
1738
+ });
1739
+ }
1740
+ #formatProgressLine(event) {
1741
+ switch (event.type) {
1742
+ case "assistant.thinking":
1743
+ return "";
1744
+ case "session.compacting":
1745
+ return `- Compacting session${event.text ? `: ${event.text}` : ""}`;
1746
+ case "assistant.tool.running":
1747
+ return `- Running ${event.toolName}`;
1748
+ case "assistant.tool.done":
1749
+ return `- Finished ${event.toolName}`;
1750
+ case "assistant.tool.error":
1751
+ return this.#formatToolErrorLine(event.toolName, event.text);
1752
+ }
1753
+ }
1754
+ #formatToolErrorLine(toolName, text2) {
1755
+ const normalizedText = text2?.trim();
1756
+ if (!normalizedText) {
1757
+ return `- ${this.#humanizeToolError(toolName)}`;
1758
+ }
1759
+ const lowerText = normalizedText.toLowerCase();
1760
+ const lowerToolName = toolName.toLowerCase();
1761
+ if (lowerText === lowerToolName || lowerText === `failed ${lowerToolName}`) {
1762
+ return `- ${this.#humanizeToolError(toolName)}`;
1763
+ }
1764
+ return `- ${this.#humanizeToolError(toolName)}: ${normalizedText}`;
1765
+ }
1766
+ #humanizeToolError(toolName) {
1767
+ return `Failed ${toolName}`;
1768
+ }
1769
+ #progressStatus(event) {
1770
+ switch (event.type) {
1771
+ case "assistant.tool.error":
1772
+ return "error";
1773
+ case "assistant.tool.done":
1774
+ return "done";
1775
+ default:
1776
+ return "running";
1777
+ }
1778
+ }
1779
+ };
1780
+
1781
+ // src/modules/client/feishu/index.ts
1782
+ function createFeishuConfigCollector() {
1783
+ return {
1784
+ async collect(ctx) {
1785
+ const appId = await ctx.input("Feishu App ID", {
1786
+ required: true,
1787
+ validate: (value) => value ? null : "App ID is required"
1788
+ });
1789
+ const appSecret = await ctx.input("Feishu App Secret", {
1790
+ required: true,
1791
+ secret: true,
1792
+ validate: (value) => value ? null : "App Secret is required"
1793
+ });
1794
+ const domain = await ctx.select("Feishu domain", [
1795
+ { label: "Feishu (default)", value: "feishu" },
1796
+ { label: "Lark", value: "lark" }
1797
+ ]);
1798
+ const requireMentionInGroup = await ctx.confirm("Require @mention in group chats", true);
1799
+ return {
1800
+ appId,
1801
+ appSecret,
1802
+ domain,
1803
+ requireMentionInGroup
1804
+ };
1805
+ },
1806
+ validate(config) {
1807
+ if (!config.appId.trim()) {
1808
+ throw new Error("Feishu appId is required");
1809
+ }
1810
+ if (!config.appSecret.trim()) {
1811
+ throw new Error("Feishu appSecret is required");
1812
+ }
1813
+ if (config.domain && !["feishu", "lark"].includes(config.domain)) {
1814
+ throw new Error("Feishu domain must be feishu or lark");
1815
+ }
1816
+ },
1817
+ summarize(config) {
1818
+ const masked = config.appId.length > 8 ? `${config.appId.slice(0, 4)}****${config.appId.slice(-4)}` : "****";
1819
+ return `type=feishu appId=${masked} domain=${config.domain ?? "feishu"} requireMentionInGroup=${config.requireMentionInGroup ?? true}`;
1820
+ }
1821
+ };
1822
+ }
1823
+ var feishuClientModule = {
1824
+ type: "feishu",
1825
+ createConfigCollector: createFeishuConfigCollector,
1826
+ createClientAdapter(config) {
1827
+ return new FeishuIMAdapter(config);
1828
+ }
1829
+ };
1830
+
1831
+ // src/modules/client/index.ts
1832
+ var registry2 = /* @__PURE__ */ new Map([
1833
+ [feishuClientModule.type, feishuClientModule]
1834
+ ]);
1835
+ function listClientModules() {
1836
+ return [...registry2.values()];
1837
+ }
1838
+ function getClientModule(type) {
1839
+ return registry2.get(type);
1840
+ }
1841
+ function getTypedClientModule(config) {
1842
+ const module = registry2.get(config.type);
1843
+ if (!module) {
1844
+ throw new Error(`Unsupported client module type: ${config.type}`);
1845
+ }
1846
+ return module;
1847
+ }
1848
+
1849
+ // src/core/channel-runner.ts
1850
+ var logger2 = createLogger("runner");
1851
+ async function runChannel({ channelName, channelConfig, defaults }) {
1852
+ const clientModule = getTypedClientModule(channelConfig.client);
1853
+ const agentModule = getTypedAgentModule(channelConfig.agent);
1854
+ const imAdapter = clientModule.createClientAdapter(channelConfig.client.config);
1855
+ const bindingStore = createFileSessionBindingStore(getSessionBindingStorePath(channelName));
1856
+ const core = new GatewayCore({
1857
+ imAdapter,
1858
+ agentModule,
1859
+ agentConfig: channelConfig.agent.config,
1860
+ agentIdleTimeoutMs: defaults.agentIdleTimeoutMs,
1861
+ bindingStore
1862
+ });
1863
+ await core.start();
1864
+ logger2.info(`channel ${channelName} started`);
1865
+ logger2.info("press Ctrl+C to stop");
1866
+ return {
1867
+ async stop() {
1868
+ await core.stop();
1869
+ logger2.info(`channel ${channelName} stopped`);
1870
+ }
1871
+ };
1872
+ }
1873
+
1874
+ // src/cli.ts
1875
+ async function selectModuleType(label, modules, ctx) {
1876
+ if (modules.length === 0) {
1877
+ throw new Error(`No modules available for ${label}`);
1878
+ }
1879
+ return ctx.select(
1880
+ label,
1881
+ modules.map((module) => ({
1882
+ label: module.type,
1883
+ value: module.type
1884
+ }))
1885
+ );
1886
+ }
1887
+ async function collectModuleConfig(module, ctx) {
1888
+ const collector = module.createConfigCollector?.();
1889
+ if (!collector) {
1890
+ return {};
1891
+ }
1892
+ const config = await collector.collect(ctx);
1893
+ await collector.validate(config);
1894
+ return config;
1895
+ }
1896
+ async function addChannel(config) {
1897
+ const ctx = createPromptContext();
1898
+ try {
1899
+ const name = await ctx.input("Channel name", {
1900
+ required: true,
1901
+ validate: (value) => {
1902
+ if (!value) return "Channel name is required";
1903
+ if (config.channels[value]) return "Channel name already exists";
1904
+ return null;
1905
+ }
1906
+ });
1907
+ const clientType = await selectModuleType("Select client module", listClientModules(), ctx);
1908
+ const clientModule = getClientModule(clientType);
1909
+ if (!clientModule) {
1910
+ throw new Error(`No client module for type: ${clientType}`);
1911
+ }
1912
+ const clientConfig = await collectModuleConfig(clientModule, ctx);
1913
+ const agentType = await selectModuleType("Select agent module", listAgentModules(), ctx);
1914
+ const agentModule = getAgentModule(agentType);
1915
+ if (!agentModule) {
1916
+ throw new Error(`No agent module for type: ${agentType}`);
1917
+ }
1918
+ const agentConfig = await collectModuleConfig(agentModule, ctx);
1919
+ config.channels[name] = {
1920
+ client: {
1921
+ type: clientType,
1922
+ config: clientConfig
1923
+ },
1924
+ agent: {
1925
+ type: agentType,
1926
+ config: agentConfig
1927
+ }
1928
+ };
1929
+ await saveConfig(config);
1930
+ console.log(`Saved channel ${name} to ${getConfigPath()}`);
1931
+ } finally {
1932
+ ctx.close();
1933
+ }
1934
+ }
1935
+ function summarizeClient(module, channel) {
1936
+ const summary = module?.createConfigCollector?.()?.summarize?.(channel.client.config);
1937
+ return summary ?? `type=${channel.client.type}`;
1938
+ }
1939
+ function summarizeAgent(module, channel) {
1940
+ const summary = module?.createConfigCollector?.()?.summarize?.(channel.agent.config);
1941
+ return summary ?? `type=${channel.agent.type}`;
1942
+ }
1943
+ async function listChannels() {
1944
+ const config = await loadConfig();
1945
+ const names = Object.keys(config.channels).sort();
1946
+ if (names.length === 0) {
1947
+ console.log("No channels configured.");
1948
+ return;
1949
+ }
1950
+ for (const name of names) {
1951
+ const channel = config.channels[name];
1952
+ const clientModule = getClientModule(channel.client.type);
1953
+ const agentModule = getAgentModule(channel.agent.type);
1954
+ const clientSummary = summarizeClient(clientModule, channel);
1955
+ const agentSummary = summarizeAgent(agentModule, channel);
1956
+ console.log(`${name} client(${clientSummary}) agent(${agentSummary})`);
1957
+ }
1958
+ }
1959
+ async function removeChannel(channelName) {
1960
+ const config = await loadConfig();
1961
+ if (!config.channels[channelName]) {
1962
+ throw new Error(`Unknown channel: ${channelName}`);
1963
+ }
1964
+ delete config.channels[channelName];
1965
+ await saveConfig(config);
1966
+ await removeSessionBindingStore(channelName);
1967
+ console.log(`Removed channel ${channelName}`);
1968
+ }
1969
+ async function startChannel(channelName) {
1970
+ const config = await loadConfig();
1971
+ const channelConfig = config.channels[channelName];
1972
+ if (!channelConfig) {
1973
+ throw new Error(`Unknown channel: ${channelName}`);
1974
+ }
1975
+ const runner = await runChannel({
1976
+ channelName,
1977
+ channelConfig,
1978
+ defaults: config.defaults
1979
+ });
1980
+ let stopping = false;
1981
+ const stop = async () => {
1982
+ if (stopping) return;
1983
+ stopping = true;
1984
+ await runner.stop();
1985
+ process2.exit(0);
1986
+ };
1987
+ process2.on("SIGINT", () => {
1988
+ void stop();
1989
+ });
1990
+ process2.on("SIGTERM", () => {
1991
+ void stop();
1992
+ });
1993
+ await new Promise(() => {
1994
+ });
1995
+ }
1996
+ async function runCli(argv = process2.argv) {
1997
+ const program = new Command();
1998
+ program.name("agent-bridge").description("IM to Pi bridge CLI").version("0.1.0");
1999
+ program.command("add").description("Interactively add a channel").action(async () => {
2000
+ const config = await loadConfig();
2001
+ await addChannel(config);
2002
+ });
2003
+ program.command("ls").description("List configured channels").action(async () => {
2004
+ await listChannels();
2005
+ });
2006
+ program.command("remove").description("Remove a channel").argument("<channel-name>").action(async (channelName) => {
2007
+ await removeChannel(channelName);
2008
+ });
2009
+ program.command("start").description("Start a configured channel").argument("<channel-name>").action(async (channelName) => {
2010
+ await startChannel(channelName);
2011
+ });
2012
+ await program.parseAsync(argv);
2013
+ }
2014
+
2015
+ // bin/agent-bridge.ts
2016
+ runCli().catch((error) => {
2017
+ console.error(error instanceof Error ? error.message : error);
2018
+ process.exit(1);
2019
+ });