@hasna/bridge 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -32,6 +32,11 @@ function mergeEnv(profile, agent) {
32
32
  env["HOME"] = profile.home;
33
33
  return Object.keys(env).length ? env : undefined;
34
34
  }
35
+ function compatibilityDetail(kind) {
36
+ if (kind === "shell")
37
+ return "shell command session; local bridge state is durable";
38
+ return "compatibility mode: this adapter invokes the current CLI one message at a time until a stable create/send/resume API is wired";
39
+ }
35
40
  function resolveAgent(config, agentId) {
36
41
  const agent = config.agents[agentId];
37
42
  if (!agent)
@@ -50,7 +55,7 @@ function buildAgentCommand(config, agentId, input) {
50
55
  const kind = agent.kind;
51
56
  const command = agent.command || profile?.command;
52
57
  const args = agent.args || profile?.args;
53
- const cwd = agent.cwd || profile?.cwd;
58
+ const cwd = input.session?.cwd || agent.cwd || profile?.cwd;
54
59
  const env = mergeEnv(profile, agent);
55
60
  if (command) {
56
61
  return { command: [command, ...renderCustomArgs(args, prompt)], cwd, env };
@@ -72,6 +77,46 @@ function buildAgentCommand(config, agentId, input) {
72
77
  }
73
78
  return { command: ["sh", "-lc", prompt], cwd, env };
74
79
  }
80
+ function createAgentSessionRef(config, agentId) {
81
+ const { agent } = resolveAgent(config, agentId);
82
+ const timestamp = new Date().toISOString();
83
+ return {
84
+ kind: agent.kind,
85
+ mode: "compatibility",
86
+ createdAt: timestamp,
87
+ updatedAt: timestamp,
88
+ detail: compatibilityDetail(agent.kind)
89
+ };
90
+ }
91
+ function resumeAgentSessionRef(session) {
92
+ return {
93
+ supported: session.agentSession?.mode === "durable",
94
+ ref: session.agentSession,
95
+ detail: session.agentSession?.mode === "durable" ? "durable agent session ref is available" : "compatibility sessions do not expose agent-side resume; bridge binding state is still durable"
96
+ };
97
+ }
98
+ function cancelAgentSession(session) {
99
+ return {
100
+ supported: false,
101
+ ref: session.agentSession,
102
+ detail: `cancel is not implemented for ${session.agentSession?.kind || "unknown"} ${session.agentSession?.mode || "compatibility"} sessions`
103
+ };
104
+ }
105
+ function closeAgentSession(session) {
106
+ return {
107
+ supported: session.agentSession?.mode === "durable",
108
+ ref: session.agentSession,
109
+ detail: session.agentSession?.mode === "durable" ? "durable close is adapter-owned" : "compatibility close only updates bridge session state"
110
+ };
111
+ }
112
+ async function sendAgentSessionMessage(config, session, message, options = {}) {
113
+ const run = options.run || runAgent;
114
+ return run(config, session.agentId, {
115
+ message,
116
+ route: { id: `session:${session.id}`, fromChannel: message.channelId, toAgent: session.agentId },
117
+ session
118
+ });
119
+ }
75
120
  async function runAgent(config, agentId, input) {
76
121
  const { agent } = resolveAgent(config, agentId);
77
122
  const built = buildAgentCommand(config, agentId, input);
@@ -4130,7 +4175,14 @@ var channelSchema = exports_external.discriminatedUnion("kind", [
4130
4175
  kind: exports_external.literal("imessage"),
4131
4176
  label: exports_external.string().optional(),
4132
4177
  enabled: exports_external.boolean().optional(),
4133
- account: exports_external.string().optional()
4178
+ account: exports_external.string().optional(),
4179
+ serviceName: exports_external.string().optional(),
4180
+ defaultHandle: exports_external.string().optional(),
4181
+ allowedHandles: exports_external.array(exports_external.string()).optional(),
4182
+ allowAllHandles: exports_external.boolean().optional(),
4183
+ receiveMode: exports_external.enum(["disabled", "chat-db"]).optional(),
4184
+ chatDbPath: exports_external.string().optional(),
4185
+ pollLimit: exports_external.number().int().positive().max(500).optional()
4134
4186
  })
4135
4187
  ]);
4136
4188
  var envSchema = exports_external.record(exports_external.string(), exports_external.string());
@@ -4268,19 +4320,35 @@ import { dirname as dirname3, join as join3, resolve } from "path";
4268
4320
  // src/lib/state.ts
4269
4321
  import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
4270
4322
  import { dirname as dirname2, join as join2 } from "path";
4323
+ var STATE_SCHEMA_VERSION = 2;
4271
4324
  function defaultStatePath() {
4272
4325
  return process.env["BRIDGE_STATE"] || join2(bridgeHome(), "state.json");
4273
4326
  }
4274
4327
  function emptyState() {
4275
- return { telegramOffsets: {} };
4328
+ return {
4329
+ schemaVersion: STATE_SCHEMA_VERSION,
4330
+ telegramOffsets: {},
4331
+ sessions: {},
4332
+ bindings: {},
4333
+ messageLedger: {},
4334
+ cursors: {}
4335
+ };
4336
+ }
4337
+ function normalizeState(value) {
4338
+ return {
4339
+ schemaVersion: STATE_SCHEMA_VERSION,
4340
+ telegramOffsets: value.telegramOffsets && typeof value.telegramOffsets === "object" ? value.telegramOffsets : {},
4341
+ sessions: value.sessions && typeof value.sessions === "object" ? value.sessions : {},
4342
+ bindings: value.bindings && typeof value.bindings === "object" ? value.bindings : {},
4343
+ messageLedger: value.messageLedger && typeof value.messageLedger === "object" ? value.messageLedger : {},
4344
+ cursors: value.cursors && typeof value.cursors === "object" ? value.cursors : {}
4345
+ };
4276
4346
  }
4277
4347
  async function loadState(statePath = defaultStatePath()) {
4278
4348
  try {
4279
4349
  const raw = await readFile2(statePath, "utf-8");
4280
4350
  const parsed = JSON.parse(raw);
4281
- return {
4282
- telegramOffsets: parsed.telegramOffsets && typeof parsed.telegramOffsets === "object" ? parsed.telegramOffsets : {}
4283
- };
4351
+ return normalizeState(parsed);
4284
4352
  } catch (err) {
4285
4353
  if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
4286
4354
  return emptyState();
@@ -4289,8 +4357,9 @@ async function loadState(statePath = defaultStatePath()) {
4289
4357
  }
4290
4358
  }
4291
4359
  async function saveState(state, statePath = defaultStatePath()) {
4360
+ const normalized = normalizeState(state);
4292
4361
  await mkdir2(dirname2(statePath), { recursive: true, mode: 448 });
4293
- await writeFile2(statePath, `${JSON.stringify(state, null, 2)}
4362
+ await writeFile2(statePath, `${JSON.stringify(normalized, null, 2)}
4294
4363
  `, { encoding: "utf-8", mode: 384 });
4295
4364
  await chmod2(statePath, 384);
4296
4365
  }
@@ -4377,6 +4446,7 @@ function telegramUpdateToMessage(channelId, update) {
4377
4446
  channelId,
4378
4447
  text,
4379
4448
  chatId: String(chatId),
4449
+ threadId: update.message?.message_thread_id !== undefined ? String(update.message.message_thread_id) : undefined,
4380
4450
  from: update.message?.from?.username || (update.message?.from?.id !== undefined ? String(update.message.from.id) : undefined),
4381
4451
  receivedAt: update.message?.date ? new Date(update.message.date * 1000).toISOString() : new Date().toISOString(),
4382
4452
  raw: update
@@ -4555,14 +4625,17 @@ function startCommand(options) {
4555
4625
  function telegramChannels(config) {
4556
4626
  return Object.values(config.channels).filter((channel) => channel.kind === "telegram" && channel.enabled !== false);
4557
4627
  }
4628
+ function imessagePollChannels(config) {
4629
+ return Object.values(config.channels).filter((channel) => channel.kind === "imessage" && channel.enabled !== false && channel.receiveMode === "chat-db");
4630
+ }
4558
4631
  function requiredTelegramEnvVars(config) {
4559
4632
  return [...new Set(telegramChannels(config).map((channel) => channel.botTokenEnv || "TELEGRAM_BOT_TOKEN"))];
4560
4633
  }
4561
4634
  async function validateStartConfig(configPath) {
4562
4635
  const config = await loadConfig(configPath);
4563
- const channels = telegramChannels(config);
4636
+ const channels = [...telegramChannels(config), ...imessagePollChannels(config)];
4564
4637
  if (!channels.length)
4565
- throw new Error("No enabled Telegram channels configured; add one before starting the daemon");
4638
+ throw new Error("No enabled pollable channels configured; add Telegram or iMessage receive before starting the daemon");
4566
4639
  for (const envName of requiredTelegramEnvVars(config)) {
4567
4640
  if (!process.env[envName])
4568
4641
  throw new Error(`Missing Telegram bot token env var for daemon start: ${envName}`);
@@ -4964,6 +5037,167 @@ async function daemonLogs(options = {}) {
4964
5037
  }
4965
5038
  // src/lib/doctor.ts
4966
5039
  import { stat as stat2 } from "fs/promises";
5040
+
5041
+ // src/lib/imessage.ts
5042
+ import { access } from "fs/promises";
5043
+ import { join as join4 } from "path";
5044
+ import { Database } from "bun:sqlite";
5045
+ function defaultMessagesDbPath() {
5046
+ return join4(homeDir(), "Library", "Messages", "chat.db");
5047
+ }
5048
+ function imessageHandleAllowed(channel, handle) {
5049
+ if (channel.allowAllHandles)
5050
+ return true;
5051
+ if (!channel.allowedHandles?.length)
5052
+ return false;
5053
+ return Boolean(handle && channel.allowedHandles.includes(handle));
5054
+ }
5055
+ function appleScriptString(value) {
5056
+ return `"${value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"")}"`;
5057
+ }
5058
+ function renderSendIMessageScript(channel, handle, text) {
5059
+ const service = channel.serviceName || "iMessage";
5060
+ const serviceSelector = channel.account ? `1st service whose name = ${appleScriptString(service)} and account = ${appleScriptString(channel.account)}` : `1st service whose name = ${appleScriptString(service)}`;
5061
+ const targetLines = handle.startsWith("chat:") ? [
5062
+ `set targetChat to 1st chat whose id = ${appleScriptString(handle.slice("chat:".length))}`,
5063
+ `send ${appleScriptString(text)} to targetChat`
5064
+ ] : [
5065
+ `set targetBuddy to buddy ${appleScriptString(handle)} of targetService`,
5066
+ `send ${appleScriptString(text)} to targetBuddy`
5067
+ ];
5068
+ return [
5069
+ 'tell application "Messages"',
5070
+ `set targetService to ${serviceSelector}`,
5071
+ ...targetLines,
5072
+ "end tell"
5073
+ ].join(`
5074
+ `);
5075
+ }
5076
+ async function defaultRun(command) {
5077
+ const proc = Bun.spawn(command, { stdout: "pipe", stderr: "pipe" });
5078
+ const [exitCode, stdout, stderr] = await Promise.all([
5079
+ proc.exited,
5080
+ new Response(proc.stdout).text(),
5081
+ new Response(proc.stderr).text()
5082
+ ]);
5083
+ return { exitCode, stdout, stderr };
5084
+ }
5085
+ async function sendIMessage(channel, handle, text, options = {}) {
5086
+ if (!(options.allowChatTarget && handle.startsWith("chat:")) && !imessageHandleAllowed(channel, handle)) {
5087
+ throw new Error(`iMessage handle is not allowed for channel ${channel.id}: ${handle}`);
5088
+ }
5089
+ const script = renderSendIMessageScript(channel, handle, text);
5090
+ const result = await (options.run || defaultRun)(["osascript", "-e", script]);
5091
+ if (result.exitCode !== 0) {
5092
+ throw new Error(`iMessage send failed: ${result.stderr || result.stdout || `exit ${result.exitCode}`}`);
5093
+ }
5094
+ return { ok: true };
5095
+ }
5096
+ function imessageDateToIso(value) {
5097
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0)
5098
+ return new Date().toISOString();
5099
+ const appleEpochMs = Date.UTC(2001, 0, 1);
5100
+ if (value > 1000000000000000)
5101
+ return new Date(appleEpochMs + Math.floor(value / 1e6)).toISOString();
5102
+ if (value > 1e9)
5103
+ return new Date(appleEpochMs + value * 1000).toISOString();
5104
+ return new Date(appleEpochMs + value).toISOString();
5105
+ }
5106
+ function getIMessageDbPath(channel) {
5107
+ return channel.chatDbPath || defaultMessagesDbPath();
5108
+ }
5109
+ function getIMessageMessages(channel, options = {}) {
5110
+ if ((channel.receiveMode || "disabled") !== "chat-db")
5111
+ return [];
5112
+ const db = new Database(getIMessageDbPath(channel), { readonly: true });
5113
+ try {
5114
+ const limit = options.limit || channel.pollLimit || 50;
5115
+ const scanLimit = Math.max(limit * 10, limit);
5116
+ const rows = db.query(`
5117
+ select
5118
+ message.ROWID as rowId,
5119
+ handle.id as handle,
5120
+ chat.guid as chatGuid,
5121
+ chat.display_name as displayName,
5122
+ message.text as text,
5123
+ message.date as date
5124
+ from message
5125
+ left join handle on message.handle_id = handle.ROWID
5126
+ left join chat_message_join on chat_message_join.message_id = message.ROWID
5127
+ left join chat on chat.ROWID = chat_message_join.chat_id
5128
+ where message.ROWID > ?
5129
+ and message.is_from_me = 0
5130
+ and message.text is not null
5131
+ order by message.ROWID asc
5132
+ limit ?
5133
+ `).all(options.afterRowId || 0, scanLimit);
5134
+ return rows.filter((row) => row.handle && row.text && imessageHandleAllowed(channel, row.handle)).slice(0, limit).map((row) => {
5135
+ const item = { rowId: row.rowId, handle: row.handle, text: row.text, date: row.date };
5136
+ if (row.chatGuid)
5137
+ item.chatGuid = row.chatGuid;
5138
+ if (row.displayName)
5139
+ item.displayName = row.displayName;
5140
+ return item;
5141
+ });
5142
+ } finally {
5143
+ db.close();
5144
+ }
5145
+ }
5146
+ function imessageRowToMessage(channelId, row) {
5147
+ return {
5148
+ id: `imessage:${row.rowId}`,
5149
+ channelId,
5150
+ chatId: row.chatGuid ? `chat:${row.chatGuid}` : row.handle,
5151
+ responseTargetId: row.chatGuid ? `chat:${row.chatGuid}` : row.handle,
5152
+ from: row.handle,
5153
+ text: row.text,
5154
+ receivedAt: imessageDateToIso(row.date),
5155
+ raw: row
5156
+ };
5157
+ }
5158
+ async function commandExists(command) {
5159
+ const proc = Bun.spawn(["sh", "-lc", `command -v ${command} >/dev/null 2>&1`], {
5160
+ stdout: "ignore",
5161
+ stderr: "ignore"
5162
+ });
5163
+ return await proc.exited === 0;
5164
+ }
5165
+ async function diagnoseIMessage(channel) {
5166
+ const checks = [];
5167
+ checks.push({
5168
+ name: `imessage-platform:${channel.id}`,
5169
+ ok: process.platform === "darwin",
5170
+ detail: process.platform === "darwin" ? "macOS" : `unsupported platform: ${process.platform}`
5171
+ });
5172
+ checks.push({
5173
+ name: `imessage-osascript:${channel.id}`,
5174
+ ok: await commandExists("osascript"),
5175
+ detail: "required for Messages send automation"
5176
+ });
5177
+ checks.push({
5178
+ name: `imessage-allowlist:${channel.id}`,
5179
+ ok: Boolean(channel.allowAllHandles || channel.allowedHandles?.length),
5180
+ detail: channel.allowAllHandles ? "allowAllHandles=true" : `${channel.allowedHandles?.length || 0} handle(s)`
5181
+ });
5182
+ if ((channel.receiveMode || "disabled") === "chat-db") {
5183
+ const path = getIMessageDbPath(channel);
5184
+ try {
5185
+ await access(path);
5186
+ checks.push({ name: `imessage-chat-db:${channel.id}`, ok: true, detail: path });
5187
+ } catch (err) {
5188
+ checks.push({
5189
+ name: `imessage-chat-db:${channel.id}`,
5190
+ ok: false,
5191
+ detail: `${path}: ${err instanceof Error ? err.message : String(err)}. Grant Full Disk Access to the terminal/daemon host or disable receive mode.`
5192
+ });
5193
+ }
5194
+ } else {
5195
+ checks.push({ name: `imessage-receive:${channel.id}`, ok: true, detail: "receiveMode=disabled" });
5196
+ }
5197
+ return checks;
5198
+ }
5199
+
5200
+ // src/lib/doctor.ts
4967
5201
  function isNotFound2(err) {
4968
5202
  return Boolean(err && typeof err === "object" && "code" in err && err.code === "ENOENT");
4969
5203
  }
@@ -4991,7 +5225,7 @@ async function privateDirCheck(name, path) {
4991
5225
  return { name, ok: false, detail: `${path}: ${err instanceof Error ? err.message : String(err)}` };
4992
5226
  }
4993
5227
  }
4994
- async function commandExists(command) {
5228
+ async function commandExists2(command) {
4995
5229
  const proc = Bun.spawn(["sh", "-lc", `command -v ${command} >/dev/null 2>&1`], {
4996
5230
  stdout: "ignore",
4997
5231
  stderr: "ignore"
@@ -5029,7 +5263,7 @@ async function doctor(configPath = defaultConfigPath(), statePath = defaultState
5029
5263
  for (const command of ["bridge", "codewith", "claude", "aicopilot"]) {
5030
5264
  checks.push({
5031
5265
  name: `command:${command}`,
5032
- ok: command === "bridge" ? true : await commandExists(command),
5266
+ ok: command === "bridge" ? true : await commandExists2(command),
5033
5267
  detail: command === "bridge" ? "current package" : undefined
5034
5268
  });
5035
5269
  }
@@ -5054,6 +5288,10 @@ async function doctor(configPath = defaultConfigPath(), statePath = defaultState
5054
5288
  detail: `${route.fromChannel} -> ${route.toAgent}`
5055
5289
  });
5056
5290
  }
5291
+ const imessageChannels = Object.values(config.channels).filter((channel) => channel.kind === "imessage");
5292
+ for (const channel of imessageChannels) {
5293
+ checks.push(...await diagnoseIMessage(channel));
5294
+ }
5057
5295
  return { ok: checks.every((check) => check.ok), configPath, checks };
5058
5296
  }
5059
5297
  // src/lib/router.ts
@@ -5064,6 +5302,9 @@ function matchingRoutes(config, message) {
5064
5302
  if (channel?.kind === "telegram" && !telegramChatAllowed(channel, message.chatId)) {
5065
5303
  return [];
5066
5304
  }
5305
+ if (channel?.kind === "imessage" && !imessageHandleAllowed(channel, message.from || (message.chatId?.startsWith("chat:") ? undefined : message.chatId))) {
5306
+ return [];
5307
+ }
5067
5308
  return config.routes.filter((route) => {
5068
5309
  if (route.enabled === false)
5069
5310
  return false;
@@ -5103,16 +5344,367 @@ async function routeMessage(config, message, options = {}) {
5103
5344
  if (options.writeConsole !== false)
5104
5345
  (options.writeConsole || console.log)(responseText);
5105
5346
  deliveredResponse = true;
5347
+ } else if (responseText && channel?.kind === "imessage") {
5348
+ const handle = message.responseTargetId || message.chatId || message.from;
5349
+ const allowedIdentity = message.from || (handle?.startsWith("chat:") ? undefined : handle);
5350
+ if (handle && imessageHandleAllowed(channel, allowedIdentity)) {
5351
+ await sendIMessage(channel, handle, responseText, { allowChatTarget: handle.startsWith("chat:") });
5352
+ deliveredResponse = true;
5353
+ }
5106
5354
  }
5107
5355
  results.push({ route, agent, deliveredResponse });
5108
5356
  }
5109
5357
  return results;
5110
5358
  }
5359
+ // src/lib/sessions.ts
5360
+ import { randomUUID } from "crypto";
5361
+ function nowIso() {
5362
+ return new Date().toISOString();
5363
+ }
5364
+ function newSessionId() {
5365
+ return `ses_${randomUUID()}`;
5366
+ }
5367
+ function normalizeConversationId(channel, conversation) {
5368
+ if (conversation.includes(":") && conversation.startsWith(`${channel.kind}:`))
5369
+ return conversation;
5370
+ if (channel.kind === "telegram")
5371
+ return `telegram:${channel.id}:${conversation}`;
5372
+ if (channel.kind === "imessage")
5373
+ return `imessage:${channel.id}:${conversation}`;
5374
+ return `${channel.kind}:${channel.id}:${conversation || "default"}`;
5375
+ }
5376
+ function messageConversationId(config, message) {
5377
+ const channel = config.channels[message.channelId];
5378
+ if (!channel)
5379
+ return;
5380
+ if (channel.kind === "telegram") {
5381
+ if (!message.chatId)
5382
+ return;
5383
+ return normalizeConversationId(channel, message.threadId ? `${message.chatId}:${message.threadId}` : message.chatId);
5384
+ }
5385
+ if (channel.kind === "imessage") {
5386
+ const conversation = message.chatId || message.from;
5387
+ return conversation ? normalizeConversationId(channel, conversation) : undefined;
5388
+ }
5389
+ return normalizeConversationId(channel, message.chatId || message.from || "default");
5390
+ }
5391
+ function bindingId(channelId, conversationId) {
5392
+ return `${channelId}::${conversationId}`;
5393
+ }
5394
+ function ledgerId(message) {
5395
+ return `${message.channelId}::${message.id}`;
5396
+ }
5397
+ function createBridgeSession(config, state, input) {
5398
+ const { agent, profile } = resolveAgent(config, input.agentId);
5399
+ const timestamp = nowIso();
5400
+ const session = {
5401
+ id: input.id || newSessionId(),
5402
+ agentId: agent.id,
5403
+ profileId: agent.profileId,
5404
+ cwd: input.cwd || agent.cwd || profile?.cwd,
5405
+ title: input.title,
5406
+ status: "active",
5407
+ createdAt: timestamp,
5408
+ updatedAt: timestamp,
5409
+ agentSession: createAgentSessionRef(config, agent.id)
5410
+ };
5411
+ state.sessions[session.id] = session;
5412
+ return session;
5413
+ }
5414
+ function getBridgeSession(state, sessionId) {
5415
+ const session = state.sessions[sessionId];
5416
+ if (!session)
5417
+ throw new Error(`Session not found: ${sessionId}`);
5418
+ return session;
5419
+ }
5420
+ function listBridgeSessions(state) {
5421
+ return Object.values(state.sessions).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
5422
+ }
5423
+ function updateBridgeSessionStatus(state, sessionId, status) {
5424
+ const session = getBridgeSession(state, sessionId);
5425
+ if (status === "closed")
5426
+ closeAgentSession(session);
5427
+ session.status = status;
5428
+ session.updatedAt = nowIso();
5429
+ return session;
5430
+ }
5431
+ function attachBridgeSession(config, state, input) {
5432
+ const channel = config.channels[input.channelId];
5433
+ if (!channel)
5434
+ throw new Error(`Channel not found: ${input.channelId}`);
5435
+ const session = getBridgeSession(state, input.sessionId);
5436
+ if (session.status === "closed")
5437
+ throw new Error(`Cannot attach closed session: ${session.id}`);
5438
+ const conversationId = normalizeConversationId(channel, input.conversation);
5439
+ const id = bindingId(channel.id, conversationId);
5440
+ const existing = state.bindings[id];
5441
+ const timestamp = nowIso();
5442
+ const binding = {
5443
+ id,
5444
+ channelId: channel.id,
5445
+ conversationId,
5446
+ activeSessionId: session.id,
5447
+ defaultSessionId: input.makeDefault ? session.id : existing?.defaultSessionId,
5448
+ createdAt: existing?.createdAt || timestamp,
5449
+ updatedAt: timestamp,
5450
+ authorization: input.authorization || existing?.authorization || (channel.kind === "telegram" ? { chatId: input.conversation.split(":")[0] } : undefined)
5451
+ };
5452
+ state.bindings[id] = binding;
5453
+ return binding;
5454
+ }
5455
+ function detachBridgeBinding(config, state, channelId, conversation) {
5456
+ const channel = config.channels[channelId];
5457
+ if (!channel)
5458
+ throw new Error(`Channel not found: ${channelId}`);
5459
+ const conversationId = normalizeConversationId(channel, conversation);
5460
+ const id = bindingId(channel.id, conversationId);
5461
+ const existing = state.bindings[id];
5462
+ delete state.bindings[id];
5463
+ return existing;
5464
+ }
5465
+ function findBridgeBinding(config, state, message) {
5466
+ const conversationId = messageConversationId(config, message);
5467
+ if (!conversationId)
5468
+ return;
5469
+ return state.bindings[bindingId(message.channelId, conversationId)];
5470
+ }
5471
+ function noSessionText(channelId, conversationId) {
5472
+ return [
5473
+ "No bridge session is attached to this conversation.",
5474
+ "Create and attach one locally:",
5475
+ "bridge sessions create --agent <agent-id>",
5476
+ `bridge sessions attach <session-id> --channel ${channelId}${conversationId ? ` --conversation ${conversationId}` : " --conversation <conversation-id>"}`
5477
+ ].join(`
5478
+ `);
5479
+ }
5480
+ async function deliverResponse(config, message, text, options) {
5481
+ const channel = config.channels[message.channelId];
5482
+ if (!text || !channel || channel.enabled === false)
5483
+ return false;
5484
+ if (channel.kind === "telegram" && message.chatId) {
5485
+ if (!telegramChatAllowed(channel, message.chatId))
5486
+ return false;
5487
+ await (options.sendTelegram || sendTelegramMessage)(telegramToken(channel), message.chatId, text);
5488
+ return true;
5489
+ }
5490
+ if (channel.kind === "console") {
5491
+ if (options.writeConsole !== false)
5492
+ (options.writeConsole || console.log)(text);
5493
+ return true;
5494
+ }
5495
+ if (channel.kind === "imessage" && message.chatId) {
5496
+ const allowedIdentity = message.from || (message.chatId.startsWith("chat:") ? undefined : message.chatId);
5497
+ if (!imessageHandleAllowed(channel, allowedIdentity))
5498
+ return false;
5499
+ await sendIMessage(channel, message.responseTargetId || message.chatId, text, { allowChatTarget: Boolean(message.responseTargetId?.startsWith("chat:") || message.chatId.startsWith("chat:")) });
5500
+ return true;
5501
+ }
5502
+ return false;
5503
+ }
5504
+ async function deliverStoredResponse(config, state, binding, message, entry, options) {
5505
+ const session = getBridgeSession(state, binding.activeSessionId);
5506
+ const responseText = entry.responseText || "";
5507
+ const deliveredResponse = responseText ? await deliverResponse(config, message, responseText, options) : false;
5508
+ completeLedger(entry, "delivered", session.id);
5509
+ entry.deliveredResponse = deliveredResponse;
5510
+ return {
5511
+ kind: "session",
5512
+ session,
5513
+ binding,
5514
+ conversationId: binding.conversationId,
5515
+ deliveredResponse,
5516
+ status: responseText ? "delivered" : "no_output"
5517
+ };
5518
+ }
5519
+ function channelAuthorized(config, message) {
5520
+ const channel = config.channels[message.channelId];
5521
+ if (!channel || channel.enabled === false)
5522
+ return false;
5523
+ if (channel.kind === "telegram")
5524
+ return telegramChatAllowed(channel, message.chatId);
5525
+ if (channel.kind === "imessage")
5526
+ return imessageHandleAllowed(channel, message.from || (message.chatId?.startsWith("chat:") ? undefined : message.chatId));
5527
+ return true;
5528
+ }
5529
+ function bindingAuthorized(binding, message) {
5530
+ if (binding.authorization?.chatId && binding.authorization.chatId !== message.chatId)
5531
+ return false;
5532
+ if (binding.authorization?.from && binding.authorization.from !== message.from)
5533
+ return false;
5534
+ return true;
5535
+ }
5536
+ async function sendBridgeSessionMessage(config, state, sessionId, message, options = {}) {
5537
+ const session = getBridgeSession(state, sessionId);
5538
+ if (session.status === "paused")
5539
+ return { kind: "session", session, status: "paused", message: "Session is paused" };
5540
+ if (session.status === "closed")
5541
+ return { kind: "session", session, status: "closed", message: "Session is closed" };
5542
+ const agent = await sendAgentSessionMessage(config, session, message, { run: options.run });
5543
+ const timestamp = nowIso();
5544
+ session.lastMessageAt = timestamp;
5545
+ session.updatedAt = timestamp;
5546
+ if (session.agentSession)
5547
+ session.agentSession.updatedAt = timestamp;
5548
+ if (agent.timedOut || agent.exitCode !== null && agent.exitCode !== 0) {
5549
+ return {
5550
+ kind: "session",
5551
+ session,
5552
+ agent,
5553
+ deliveredResponse: false,
5554
+ status: "failed",
5555
+ message: agent.stderr.trim() || agent.stdout.trim() || (agent.timedOut ? "Agent timed out" : `Agent exited ${agent.exitCode}`)
5556
+ };
5557
+ }
5558
+ const responseText = agent.stdout.trim();
5559
+ await options.beforeDeliver?.(agent, responseText);
5560
+ const deliveredResponse = responseText ? await deliverResponse(config, message, responseText, options) : false;
5561
+ return {
5562
+ kind: "session",
5563
+ session,
5564
+ agent,
5565
+ deliveredResponse,
5566
+ status: responseText ? "delivered" : "no_output"
5567
+ };
5568
+ }
5569
+ async function routeSessionMessage(config, state, message, options = {}) {
5570
+ const channel = config.channels[message.channelId];
5571
+ if (!channel || channel.enabled === false) {
5572
+ return { kind: "session", status: "unauthorized", message: `Channel not enabled: ${message.channelId}` };
5573
+ }
5574
+ if (!channelAuthorized(config, message)) {
5575
+ return { kind: "session", status: "unauthorized", message: "Message is not authorized for this channel" };
5576
+ }
5577
+ const conversationId = messageConversationId(config, message);
5578
+ const binding = conversationId ? state.bindings[bindingId(message.channelId, conversationId)] : undefined;
5579
+ if (!binding) {
5580
+ const text = noSessionText(message.channelId, conversationId);
5581
+ if (options.respondOnNoSession !== false)
5582
+ await deliverResponse(config, message, text, options);
5583
+ return { kind: "session", conversationId, status: "no_session", message: text };
5584
+ }
5585
+ if (!bindingAuthorized(binding, message)) {
5586
+ return { kind: "session", binding, conversationId, status: "unauthorized", message: "Message does not match binding authorization" };
5587
+ }
5588
+ const result = await sendBridgeSessionMessage(config, state, binding.activeSessionId, message, options);
5589
+ return { ...result, binding, conversationId };
5590
+ }
5591
+ function beginLedger(state, message, conversationId) {
5592
+ const id = ledgerId(message);
5593
+ const existing = state.messageLedger[id];
5594
+ if (existing && ["delivered", "skipped", "unauthorized"].includes(existing.status)) {
5595
+ return { entry: existing, shouldProcess: false };
5596
+ }
5597
+ const timestamp = nowIso();
5598
+ const entry = existing || {
5599
+ id,
5600
+ channelId: message.channelId,
5601
+ messageId: message.id,
5602
+ conversationId,
5603
+ status: "processing",
5604
+ attempts: 0,
5605
+ firstSeenAt: timestamp,
5606
+ updatedAt: timestamp
5607
+ };
5608
+ if (entry.status !== "agent_completed")
5609
+ entry.status = "processing";
5610
+ entry.attempts += 1;
5611
+ entry.conversationId = conversationId || entry.conversationId;
5612
+ entry.updatedAt = timestamp;
5613
+ delete entry.error;
5614
+ state.messageLedger[id] = entry;
5615
+ return { entry, shouldProcess: true };
5616
+ }
5617
+ function completeLedger(entry, status, sessionId, error) {
5618
+ const timestamp = nowIso();
5619
+ entry.status = status;
5620
+ entry.sessionId = sessionId || entry.sessionId;
5621
+ entry.updatedAt = timestamp;
5622
+ if (["delivered", "skipped", "unauthorized"].includes(status))
5623
+ entry.terminalAt = timestamp;
5624
+ if (error)
5625
+ entry.error = error;
5626
+ return entry;
5627
+ }
5628
+ function recordAgentCompleted(entry, sessionId, agent, responseText) {
5629
+ const timestamp = nowIso();
5630
+ entry.status = "agent_completed";
5631
+ entry.sessionId = sessionId || entry.sessionId;
5632
+ entry.responseText = responseText;
5633
+ entry.agentExitCode = agent.exitCode;
5634
+ entry.agentTimedOut = agent.timedOut;
5635
+ entry.updatedAt = timestamp;
5636
+ delete entry.error;
5637
+ return entry;
5638
+ }
5639
+ async function dispatchMessageWithSessions(config, state, message, options = {}) {
5640
+ const conversationId = messageConversationId(config, message);
5641
+ const { entry, shouldProcess } = beginLedger(state, message, conversationId);
5642
+ if (!shouldProcess)
5643
+ return { message, ledger: entry };
5644
+ await options.persistState?.(state);
5645
+ try {
5646
+ const binding = conversationId ? state.bindings[bindingId(message.channelId, conversationId)] : undefined;
5647
+ if (binding) {
5648
+ if (!bindingAuthorized(binding, message)) {
5649
+ const session3 = {
5650
+ kind: "session",
5651
+ binding,
5652
+ conversationId,
5653
+ status: "unauthorized",
5654
+ message: "Message does not match binding authorization"
5655
+ };
5656
+ completeLedger(entry, "unauthorized");
5657
+ return { message, session: session3, ledger: entry };
5658
+ }
5659
+ if (entry.status === "agent_completed") {
5660
+ const session3 = await deliverStoredResponse(config, state, binding, message, entry, options);
5661
+ return { message, session: session3, ledger: entry };
5662
+ }
5663
+ const session2 = await routeSessionMessage(config, state, message, {
5664
+ ...options,
5665
+ beforeDeliver: async (agent, responseText) => {
5666
+ recordAgentCompleted(entry, binding.activeSessionId, agent, responseText);
5667
+ await options.persistState?.(state);
5668
+ await options.beforeDeliver?.(agent, responseText);
5669
+ }
5670
+ });
5671
+ if (session2.status === "failed") {
5672
+ completeLedger(entry, "failed", session2.session?.id, session2.message);
5673
+ throw new Error(session2.message || "Agent session failed");
5674
+ }
5675
+ const terminal = session2.status === "unauthorized" ? "unauthorized" : session2.status === "delivered" || session2.status === "no_output" ? "delivered" : "skipped";
5676
+ completeLedger(entry, terminal, session2.session?.id);
5677
+ entry.deliveredResponse = session2.deliveredResponse;
5678
+ return { message, session: session2, ledger: entry };
5679
+ }
5680
+ if (options.fallbackToRoutes) {
5681
+ const routes = await routeMessage(config, message, options);
5682
+ if (routes.length) {
5683
+ completeLedger(entry, "delivered");
5684
+ return { message, routes, ledger: entry };
5685
+ }
5686
+ }
5687
+ const session = await routeSessionMessage(config, state, message, options);
5688
+ const status = session.status === "unauthorized" ? "unauthorized" : "skipped";
5689
+ completeLedger(entry, status, session.session?.id);
5690
+ return { message, session, ledger: entry };
5691
+ } catch (err) {
5692
+ const messageText = err instanceof Error ? err.message : String(err);
5693
+ if (entry.status === "agent_completed") {
5694
+ entry.error = messageText;
5695
+ entry.updatedAt = nowIso();
5696
+ } else {
5697
+ completeLedger(entry, "failed", undefined, messageText);
5698
+ }
5699
+ throw err;
5700
+ }
5701
+ }
5111
5702
  export {
5112
5703
  upsertRoute,
5113
5704
  upsertProfile,
5114
5705
  upsertChannel,
5115
5706
  upsertAgent,
5707
+ updateBridgeSessionStatus,
5116
5708
  uninstallDaemon,
5117
5709
  telegramUpdateToMessage,
5118
5710
  telegramToken,
@@ -5124,38 +5716,65 @@ export {
5124
5716
  startProcessDaemon,
5125
5717
  startInstalledDaemon,
5126
5718
  sendTelegramMessage,
5719
+ sendIMessage,
5720
+ sendBridgeSessionMessage,
5721
+ sendAgentSessionMessage,
5127
5722
  saveState,
5128
5723
  saveConfig,
5129
5724
  runAgent,
5725
+ routeSessionMessage,
5130
5726
  routeMessage,
5727
+ resumeAgentSessionRef,
5131
5728
  restartProcessDaemon,
5132
5729
  restartInstalledDaemon,
5133
5730
  resolveSupervisor,
5134
5731
  resolveAgent,
5135
5732
  requiredTelegramEnvVars,
5136
5733
  renderSystemdUnit,
5734
+ renderSendIMessageScript,
5137
5735
  renderLaunchdPlist,
5138
5736
  redactConfig,
5139
5737
  parseConfig,
5738
+ normalizeConversationId,
5739
+ messageConversationId,
5140
5740
  matchingRoutes,
5141
5741
  loadState,
5142
5742
  loadConfig,
5743
+ listBridgeSessions,
5744
+ ledgerId,
5143
5745
  installDaemon,
5746
+ imessageRowToMessage,
5747
+ imessageHandleAllowed,
5144
5748
  homeDir,
5145
5749
  getTelegramUpdates,
5750
+ getIMessageMessages,
5751
+ getIMessageDbPath,
5752
+ getBridgeSession,
5753
+ findBridgeBinding,
5146
5754
  ensureDaemonDir,
5147
5755
  ensureConfig,
5148
5756
  emptyState,
5149
5757
  emptyConfig,
5150
5758
  doctor,
5759
+ dispatchMessageWithSessions,
5760
+ diagnoseIMessage,
5761
+ detachBridgeBinding,
5151
5762
  defaultStatePath,
5763
+ defaultMessagesDbPath,
5152
5764
  defaultDaemonDir,
5153
5765
  defaultConfigPath,
5154
5766
  daemonStatus,
5155
5767
  daemonPaths,
5156
5768
  daemonLogs,
5769
+ createBridgeSession,
5770
+ createAgentSessionRef,
5771
+ closeAgentSession,
5772
+ cancelAgentSession,
5157
5773
  buildAgentCommand,
5158
5774
  bridgeHome,
5775
+ bindingId,
5776
+ attachBridgeSession,
5777
+ STATE_SCHEMA_VERSION,
5159
5778
  CONFIG_VERSION,
5160
5779
  CHANNEL_KINDS,
5161
5780
  AGENT_KINDS