@hasna/bridge 0.1.2 → 0.2.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.
package/dist/mcp/index.js CHANGED
@@ -4008,6 +4008,11 @@ function mergeEnv(profile, agent) {
4008
4008
  env["HOME"] = profile.home;
4009
4009
  return Object.keys(env).length ? env : undefined;
4010
4010
  }
4011
+ function compatibilityDetail(kind) {
4012
+ if (kind === "shell")
4013
+ return "shell command session; local bridge state is durable";
4014
+ return "compatibility mode: this adapter invokes the current CLI one message at a time until a stable create/send/resume API is wired";
4015
+ }
4011
4016
  function resolveAgent(config, agentId) {
4012
4017
  const agent = config.agents[agentId];
4013
4018
  if (!agent)
@@ -4026,7 +4031,7 @@ function buildAgentCommand(config, agentId, input) {
4026
4031
  const kind = agent.kind;
4027
4032
  const command = agent.command || profile?.command;
4028
4033
  const args = agent.args || profile?.args;
4029
- const cwd = agent.cwd || profile?.cwd;
4034
+ const cwd = input.session?.cwd || agent.cwd || profile?.cwd;
4030
4035
  const env = mergeEnv(profile, agent);
4031
4036
  if (command) {
4032
4037
  return { command: [command, ...renderCustomArgs(args, prompt)], cwd, env };
@@ -4048,6 +4053,25 @@ function buildAgentCommand(config, agentId, input) {
4048
4053
  }
4049
4054
  return { command: ["sh", "-lc", prompt], cwd, env };
4050
4055
  }
4056
+ function createAgentSessionRef(config, agentId) {
4057
+ const { agent } = resolveAgent(config, agentId);
4058
+ const timestamp = new Date().toISOString();
4059
+ return {
4060
+ kind: agent.kind,
4061
+ mode: "compatibility",
4062
+ createdAt: timestamp,
4063
+ updatedAt: timestamp,
4064
+ detail: compatibilityDetail(agent.kind)
4065
+ };
4066
+ }
4067
+ async function sendAgentSessionMessage(config, session, message, options = {}) {
4068
+ const run = options.run || runAgent;
4069
+ return run(config, session.agentId, {
4070
+ message,
4071
+ route: { id: `session:${session.id}`, fromChannel: message.channelId, toAgent: session.agentId },
4072
+ session
4073
+ });
4074
+ }
4051
4075
  async function runAgent(config, agentId, input) {
4052
4076
  const { agent } = resolveAgent(config, agentId);
4053
4077
  const built = buildAgentCommand(config, agentId, input);
@@ -4132,7 +4156,14 @@ var channelSchema = exports_external.discriminatedUnion("kind", [
4132
4156
  kind: exports_external.literal("imessage"),
4133
4157
  label: exports_external.string().optional(),
4134
4158
  enabled: exports_external.boolean().optional(),
4135
- account: exports_external.string().optional()
4159
+ account: exports_external.string().optional(),
4160
+ serviceName: exports_external.string().optional(),
4161
+ defaultHandle: exports_external.string().optional(),
4162
+ allowedHandles: exports_external.array(exports_external.string()).optional(),
4163
+ allowAllHandles: exports_external.boolean().optional(),
4164
+ receiveMode: exports_external.enum(["disabled", "chat-db"]).optional(),
4165
+ chatDbPath: exports_external.string().optional(),
4166
+ pollLimit: exports_external.number().int().positive().max(500).optional()
4136
4167
  })
4137
4168
  ]);
4138
4169
  var envSchema = exports_external.record(exports_external.string(), exports_external.string());
@@ -4226,14 +4257,55 @@ async function loadConfig(configPath = defaultConfigPath()) {
4226
4257
  }
4227
4258
  }
4228
4259
  // src/lib/daemon.ts
4229
- import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, rename, rm, rmdir, stat, writeFile as writeFile2 } from "fs/promises";
4260
+ import { chmod as chmod3, mkdir as mkdir3, readFile as readFile3, rename, rm, rmdir, stat, writeFile as writeFile3 } from "fs/promises";
4230
4261
  import { dirname as dirname2, join as join3, resolve } from "path";
4231
4262
 
4232
4263
  // src/lib/state.ts
4264
+ import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
4233
4265
  import { dirname, join as join2 } from "path";
4266
+ var STATE_SCHEMA_VERSION = 2;
4234
4267
  function defaultStatePath() {
4235
4268
  return process.env["BRIDGE_STATE"] || join2(bridgeHome(), "state.json");
4236
4269
  }
4270
+ function emptyState() {
4271
+ return {
4272
+ schemaVersion: STATE_SCHEMA_VERSION,
4273
+ telegramOffsets: {},
4274
+ sessions: {},
4275
+ bindings: {},
4276
+ messageLedger: {},
4277
+ cursors: {}
4278
+ };
4279
+ }
4280
+ function normalizeState(value) {
4281
+ return {
4282
+ schemaVersion: STATE_SCHEMA_VERSION,
4283
+ telegramOffsets: value.telegramOffsets && typeof value.telegramOffsets === "object" ? value.telegramOffsets : {},
4284
+ sessions: value.sessions && typeof value.sessions === "object" ? value.sessions : {},
4285
+ bindings: value.bindings && typeof value.bindings === "object" ? value.bindings : {},
4286
+ messageLedger: value.messageLedger && typeof value.messageLedger === "object" ? value.messageLedger : {},
4287
+ cursors: value.cursors && typeof value.cursors === "object" ? value.cursors : {}
4288
+ };
4289
+ }
4290
+ async function loadState(statePath = defaultStatePath()) {
4291
+ try {
4292
+ const raw = await readFile2(statePath, "utf-8");
4293
+ const parsed = JSON.parse(raw);
4294
+ return normalizeState(parsed);
4295
+ } catch (err) {
4296
+ if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
4297
+ return emptyState();
4298
+ }
4299
+ throw err;
4300
+ }
4301
+ }
4302
+ async function saveState(state, statePath = defaultStatePath()) {
4303
+ const normalized = normalizeState(state);
4304
+ await mkdir2(dirname(statePath), { recursive: true, mode: 448 });
4305
+ await writeFile2(statePath, `${JSON.stringify(normalized, null, 2)}
4306
+ `, { encoding: "utf-8", mode: 384 });
4307
+ await chmod2(statePath, 384);
4308
+ }
4237
4309
 
4238
4310
  // src/lib/telegram.ts
4239
4311
  var DEFAULT_TELEGRAM_API_BASE = "https://api.telegram.org";
@@ -4335,7 +4407,7 @@ async function fileExists(path) {
4335
4407
  }
4336
4408
  async function readMetadata(paths) {
4337
4409
  try {
4338
- return JSON.parse(await readFile2(paths.metadataFile, "utf-8"));
4410
+ return JSON.parse(await readFile3(paths.metadataFile, "utf-8"));
4339
4411
  } catch (err) {
4340
4412
  if (isNotFound(err))
4341
4413
  return;
@@ -4467,6 +4539,107 @@ async function daemonStatus(options = {}) {
4467
4539
  }
4468
4540
  // src/lib/doctor.ts
4469
4541
  import { stat as stat2 } from "fs/promises";
4542
+
4543
+ // src/lib/imessage.ts
4544
+ import { access } from "fs/promises";
4545
+ import { join as join4 } from "path";
4546
+ function defaultMessagesDbPath() {
4547
+ return join4(homeDir(), "Library", "Messages", "chat.db");
4548
+ }
4549
+ function imessageHandleAllowed(channel, handle) {
4550
+ if (channel.allowAllHandles)
4551
+ return true;
4552
+ if (!channel.allowedHandles?.length)
4553
+ return false;
4554
+ return Boolean(handle && channel.allowedHandles.includes(handle));
4555
+ }
4556
+ function appleScriptString(value) {
4557
+ return `"${value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"")}"`;
4558
+ }
4559
+ function renderSendIMessageScript(channel, handle, text) {
4560
+ const service = channel.serviceName || "iMessage";
4561
+ const serviceSelector = channel.account ? `1st service whose name = ${appleScriptString(service)} and account = ${appleScriptString(channel.account)}` : `1st service whose name = ${appleScriptString(service)}`;
4562
+ const targetLines = handle.startsWith("chat:") ? [
4563
+ `set targetChat to 1st chat whose id = ${appleScriptString(handle.slice("chat:".length))}`,
4564
+ `send ${appleScriptString(text)} to targetChat`
4565
+ ] : [
4566
+ `set targetBuddy to buddy ${appleScriptString(handle)} of targetService`,
4567
+ `send ${appleScriptString(text)} to targetBuddy`
4568
+ ];
4569
+ return [
4570
+ 'tell application "Messages"',
4571
+ `set targetService to ${serviceSelector}`,
4572
+ ...targetLines,
4573
+ "end tell"
4574
+ ].join(`
4575
+ `);
4576
+ }
4577
+ async function defaultRun(command) {
4578
+ const proc = Bun.spawn(command, { stdout: "pipe", stderr: "pipe" });
4579
+ const [exitCode, stdout, stderr] = await Promise.all([
4580
+ proc.exited,
4581
+ new Response(proc.stdout).text(),
4582
+ new Response(proc.stderr).text()
4583
+ ]);
4584
+ return { exitCode, stdout, stderr };
4585
+ }
4586
+ async function sendIMessage(channel, handle, text, options = {}) {
4587
+ if (!(options.allowChatTarget && handle.startsWith("chat:")) && !imessageHandleAllowed(channel, handle)) {
4588
+ throw new Error(`iMessage handle is not allowed for channel ${channel.id}: ${handle}`);
4589
+ }
4590
+ const script = renderSendIMessageScript(channel, handle, text);
4591
+ const result = await (options.run || defaultRun)(["osascript", "-e", script]);
4592
+ if (result.exitCode !== 0) {
4593
+ throw new Error(`iMessage send failed: ${result.stderr || result.stdout || `exit ${result.exitCode}`}`);
4594
+ }
4595
+ return { ok: true };
4596
+ }
4597
+ function getIMessageDbPath(channel) {
4598
+ return channel.chatDbPath || defaultMessagesDbPath();
4599
+ }
4600
+ async function commandExists(command) {
4601
+ const proc = Bun.spawn(["sh", "-lc", `command -v ${command} >/dev/null 2>&1`], {
4602
+ stdout: "ignore",
4603
+ stderr: "ignore"
4604
+ });
4605
+ return await proc.exited === 0;
4606
+ }
4607
+ async function diagnoseIMessage(channel) {
4608
+ const checks = [];
4609
+ checks.push({
4610
+ name: `imessage-platform:${channel.id}`,
4611
+ ok: process.platform === "darwin",
4612
+ detail: process.platform === "darwin" ? "macOS" : `unsupported platform: ${process.platform}`
4613
+ });
4614
+ checks.push({
4615
+ name: `imessage-osascript:${channel.id}`,
4616
+ ok: await commandExists("osascript"),
4617
+ detail: "required for Messages send automation"
4618
+ });
4619
+ checks.push({
4620
+ name: `imessage-allowlist:${channel.id}`,
4621
+ ok: Boolean(channel.allowAllHandles || channel.allowedHandles?.length),
4622
+ detail: channel.allowAllHandles ? "allowAllHandles=true" : `${channel.allowedHandles?.length || 0} handle(s)`
4623
+ });
4624
+ if ((channel.receiveMode || "disabled") === "chat-db") {
4625
+ const path = getIMessageDbPath(channel);
4626
+ try {
4627
+ await access(path);
4628
+ checks.push({ name: `imessage-chat-db:${channel.id}`, ok: true, detail: path });
4629
+ } catch (err) {
4630
+ checks.push({
4631
+ name: `imessage-chat-db:${channel.id}`,
4632
+ ok: false,
4633
+ detail: `${path}: ${err instanceof Error ? err.message : String(err)}. Grant Full Disk Access to the terminal/daemon host or disable receive mode.`
4634
+ });
4635
+ }
4636
+ } else {
4637
+ checks.push({ name: `imessage-receive:${channel.id}`, ok: true, detail: "receiveMode=disabled" });
4638
+ }
4639
+ return checks;
4640
+ }
4641
+
4642
+ // src/lib/doctor.ts
4470
4643
  function isNotFound2(err) {
4471
4644
  return Boolean(err && typeof err === "object" && "code" in err && err.code === "ENOENT");
4472
4645
  }
@@ -4494,7 +4667,7 @@ async function privateDirCheck(name, path) {
4494
4667
  return { name, ok: false, detail: `${path}: ${err instanceof Error ? err.message : String(err)}` };
4495
4668
  }
4496
4669
  }
4497
- async function commandExists(command) {
4670
+ async function commandExists2(command) {
4498
4671
  const proc = Bun.spawn(["sh", "-lc", `command -v ${command} >/dev/null 2>&1`], {
4499
4672
  stdout: "ignore",
4500
4673
  stderr: "ignore"
@@ -4532,7 +4705,7 @@ async function doctor(configPath = defaultConfigPath(), statePath = defaultState
4532
4705
  for (const command of ["bridge", "codewith", "claude", "aicopilot"]) {
4533
4706
  checks.push({
4534
4707
  name: `command:${command}`,
4535
- ok: command === "bridge" ? true : await commandExists(command),
4708
+ ok: command === "bridge" ? true : await commandExists2(command),
4536
4709
  detail: command === "bridge" ? "current package" : undefined
4537
4710
  });
4538
4711
  }
@@ -4557,6 +4730,10 @@ async function doctor(configPath = defaultConfigPath(), statePath = defaultState
4557
4730
  detail: `${route.fromChannel} -> ${route.toAgent}`
4558
4731
  });
4559
4732
  }
4733
+ const imessageChannels = Object.values(config.channels).filter((channel) => channel.kind === "imessage");
4734
+ for (const channel of imessageChannels) {
4735
+ checks.push(...await diagnoseIMessage(channel));
4736
+ }
4560
4737
  return { ok: checks.every((check) => check.ok), configPath, checks };
4561
4738
  }
4562
4739
  // src/lib/router.ts
@@ -4567,6 +4744,9 @@ function matchingRoutes(config, message) {
4567
4744
  if (channel?.kind === "telegram" && !telegramChatAllowed(channel, message.chatId)) {
4568
4745
  return [];
4569
4746
  }
4747
+ if (channel?.kind === "imessage" && !imessageHandleAllowed(channel, message.from || (message.chatId?.startsWith("chat:") ? undefined : message.chatId))) {
4748
+ return [];
4749
+ }
4570
4750
  return config.routes.filter((route) => {
4571
4751
  if (route.enabled === false)
4572
4752
  return false;
@@ -4606,19 +4786,416 @@ async function routeMessage(config, message, options = {}) {
4606
4786
  if (options.writeConsole !== false)
4607
4787
  (options.writeConsole || console.log)(responseText);
4608
4788
  deliveredResponse = true;
4789
+ } else if (responseText && channel?.kind === "imessage") {
4790
+ const handle = message.responseTargetId || message.chatId || message.from;
4791
+ const allowedIdentity = message.from || (handle?.startsWith("chat:") ? undefined : handle);
4792
+ if (handle && imessageHandleAllowed(channel, allowedIdentity)) {
4793
+ await sendIMessage(channel, handle, responseText, { allowChatTarget: handle.startsWith("chat:") });
4794
+ deliveredResponse = true;
4795
+ }
4609
4796
  }
4610
4797
  results.push({ route, agent, deliveredResponse });
4611
4798
  }
4612
4799
  return results;
4613
4800
  }
4801
+ // src/lib/sessions.ts
4802
+ import { randomUUID } from "crypto";
4803
+ function nowIso() {
4804
+ return new Date().toISOString();
4805
+ }
4806
+ function newSessionId() {
4807
+ return `ses_${randomUUID()}`;
4808
+ }
4809
+ function normalizeConversationId(channel, conversation) {
4810
+ if (conversation.includes(":") && conversation.startsWith(`${channel.kind}:`))
4811
+ return conversation;
4812
+ if (channel.kind === "telegram")
4813
+ return `telegram:${channel.id}:${conversation}`;
4814
+ if (channel.kind === "imessage")
4815
+ return `imessage:${channel.id}:${conversation}`;
4816
+ return `${channel.kind}:${channel.id}:${conversation || "default"}`;
4817
+ }
4818
+ function messageConversationId(config, message) {
4819
+ const channel = config.channels[message.channelId];
4820
+ if (!channel)
4821
+ return;
4822
+ if (channel.kind === "telegram") {
4823
+ if (!message.chatId)
4824
+ return;
4825
+ return normalizeConversationId(channel, message.threadId ? `${message.chatId}:${message.threadId}` : message.chatId);
4826
+ }
4827
+ if (channel.kind === "imessage") {
4828
+ const conversation = message.chatId || message.from;
4829
+ return conversation ? normalizeConversationId(channel, conversation) : undefined;
4830
+ }
4831
+ return normalizeConversationId(channel, message.chatId || message.from || "default");
4832
+ }
4833
+ function bindingId(channelId, conversationId) {
4834
+ return `${channelId}::${conversationId}`;
4835
+ }
4836
+ function ledgerId(message) {
4837
+ return `${message.channelId}::${message.id}`;
4838
+ }
4839
+ function createBridgeSession(config, state, input) {
4840
+ const { agent, profile } = resolveAgent(config, input.agentId);
4841
+ const timestamp = nowIso();
4842
+ const session = {
4843
+ id: input.id || newSessionId(),
4844
+ agentId: agent.id,
4845
+ profileId: agent.profileId,
4846
+ cwd: input.cwd || agent.cwd || profile?.cwd,
4847
+ title: input.title,
4848
+ status: "active",
4849
+ createdAt: timestamp,
4850
+ updatedAt: timestamp,
4851
+ agentSession: createAgentSessionRef(config, agent.id)
4852
+ };
4853
+ state.sessions[session.id] = session;
4854
+ return session;
4855
+ }
4856
+ function getBridgeSession(state, sessionId) {
4857
+ const session = state.sessions[sessionId];
4858
+ if (!session)
4859
+ throw new Error(`Session not found: ${sessionId}`);
4860
+ return session;
4861
+ }
4862
+ function listBridgeSessions(state) {
4863
+ return Object.values(state.sessions).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
4864
+ }
4865
+ function attachBridgeSession(config, state, input) {
4866
+ const channel = config.channels[input.channelId];
4867
+ if (!channel)
4868
+ throw new Error(`Channel not found: ${input.channelId}`);
4869
+ const session = getBridgeSession(state, input.sessionId);
4870
+ if (session.status === "closed")
4871
+ throw new Error(`Cannot attach closed session: ${session.id}`);
4872
+ const conversationId = normalizeConversationId(channel, input.conversation);
4873
+ const id = bindingId(channel.id, conversationId);
4874
+ const existing = state.bindings[id];
4875
+ const timestamp = nowIso();
4876
+ const binding = {
4877
+ id,
4878
+ channelId: channel.id,
4879
+ conversationId,
4880
+ activeSessionId: session.id,
4881
+ defaultSessionId: input.makeDefault ? session.id : existing?.defaultSessionId,
4882
+ createdAt: existing?.createdAt || timestamp,
4883
+ updatedAt: timestamp,
4884
+ authorization: input.authorization || existing?.authorization || (channel.kind === "telegram" ? { chatId: input.conversation.split(":")[0] } : undefined)
4885
+ };
4886
+ state.bindings[id] = binding;
4887
+ return binding;
4888
+ }
4889
+ function noSessionText(channelId, conversationId) {
4890
+ return [
4891
+ "No bridge session is attached to this conversation.",
4892
+ "Create and attach one locally:",
4893
+ "bridge sessions create --agent <agent-id>",
4894
+ `bridge sessions attach <session-id> --channel ${channelId}${conversationId ? ` --conversation ${conversationId}` : " --conversation <conversation-id>"}`
4895
+ ].join(`
4896
+ `);
4897
+ }
4898
+ async function deliverResponse(config, message, text, options) {
4899
+ const channel = config.channels[message.channelId];
4900
+ if (!text || !channel || channel.enabled === false)
4901
+ return false;
4902
+ if (channel.kind === "telegram" && message.chatId) {
4903
+ if (!telegramChatAllowed(channel, message.chatId))
4904
+ return false;
4905
+ await (options.sendTelegram || sendTelegramMessage)(telegramToken(channel), message.chatId, text);
4906
+ return true;
4907
+ }
4908
+ if (channel.kind === "console") {
4909
+ if (options.writeConsole !== false)
4910
+ (options.writeConsole || console.log)(text);
4911
+ return true;
4912
+ }
4913
+ if (channel.kind === "imessage" && message.chatId) {
4914
+ const allowedIdentity = message.from || (message.chatId.startsWith("chat:") ? undefined : message.chatId);
4915
+ if (!imessageHandleAllowed(channel, allowedIdentity))
4916
+ return false;
4917
+ await sendIMessage(channel, message.responseTargetId || message.chatId, text, { allowChatTarget: Boolean(message.responseTargetId?.startsWith("chat:") || message.chatId.startsWith("chat:")) });
4918
+ return true;
4919
+ }
4920
+ return false;
4921
+ }
4922
+ async function deliverStoredResponse(config, state, binding, message, entry, options) {
4923
+ const session = getBridgeSession(state, binding.activeSessionId);
4924
+ const responseText = entry.responseText || "";
4925
+ const deliveredResponse = responseText ? await deliverResponse(config, message, responseText, options) : false;
4926
+ completeLedger(entry, "delivered", session.id);
4927
+ entry.deliveredResponse = deliveredResponse;
4928
+ return {
4929
+ kind: "session",
4930
+ session,
4931
+ binding,
4932
+ conversationId: binding.conversationId,
4933
+ deliveredResponse,
4934
+ status: responseText ? "delivered" : "no_output"
4935
+ };
4936
+ }
4937
+ function channelAuthorized(config, message) {
4938
+ const channel = config.channels[message.channelId];
4939
+ if (!channel || channel.enabled === false)
4940
+ return false;
4941
+ if (channel.kind === "telegram")
4942
+ return telegramChatAllowed(channel, message.chatId);
4943
+ if (channel.kind === "imessage")
4944
+ return imessageHandleAllowed(channel, message.from || (message.chatId?.startsWith("chat:") ? undefined : message.chatId));
4945
+ return true;
4946
+ }
4947
+ function bindingAuthorized(binding, message) {
4948
+ if (binding.authorization?.chatId && binding.authorization.chatId !== message.chatId)
4949
+ return false;
4950
+ if (binding.authorization?.from && binding.authorization.from !== message.from)
4951
+ return false;
4952
+ return true;
4953
+ }
4954
+ async function sendBridgeSessionMessage(config, state, sessionId, message, options = {}) {
4955
+ const session = getBridgeSession(state, sessionId);
4956
+ if (session.status === "paused")
4957
+ return { kind: "session", session, status: "paused", message: "Session is paused" };
4958
+ if (session.status === "closed")
4959
+ return { kind: "session", session, status: "closed", message: "Session is closed" };
4960
+ const agent = await sendAgentSessionMessage(config, session, message, { run: options.run });
4961
+ const timestamp = nowIso();
4962
+ session.lastMessageAt = timestamp;
4963
+ session.updatedAt = timestamp;
4964
+ if (session.agentSession)
4965
+ session.agentSession.updatedAt = timestamp;
4966
+ if (agent.timedOut || agent.exitCode !== null && agent.exitCode !== 0) {
4967
+ return {
4968
+ kind: "session",
4969
+ session,
4970
+ agent,
4971
+ deliveredResponse: false,
4972
+ status: "failed",
4973
+ message: agent.stderr.trim() || agent.stdout.trim() || (agent.timedOut ? "Agent timed out" : `Agent exited ${agent.exitCode}`)
4974
+ };
4975
+ }
4976
+ const responseText = agent.stdout.trim();
4977
+ await options.beforeDeliver?.(agent, responseText);
4978
+ const deliveredResponse = responseText ? await deliverResponse(config, message, responseText, options) : false;
4979
+ return {
4980
+ kind: "session",
4981
+ session,
4982
+ agent,
4983
+ deliveredResponse,
4984
+ status: responseText ? "delivered" : "no_output"
4985
+ };
4986
+ }
4987
+ async function routeSessionMessage(config, state, message, options = {}) {
4988
+ const channel = config.channels[message.channelId];
4989
+ if (!channel || channel.enabled === false) {
4990
+ return { kind: "session", status: "unauthorized", message: `Channel not enabled: ${message.channelId}` };
4991
+ }
4992
+ if (!channelAuthorized(config, message)) {
4993
+ return { kind: "session", status: "unauthorized", message: "Message is not authorized for this channel" };
4994
+ }
4995
+ const conversationId = messageConversationId(config, message);
4996
+ const binding = conversationId ? state.bindings[bindingId(message.channelId, conversationId)] : undefined;
4997
+ if (!binding) {
4998
+ const text = noSessionText(message.channelId, conversationId);
4999
+ if (options.respondOnNoSession !== false)
5000
+ await deliverResponse(config, message, text, options);
5001
+ return { kind: "session", conversationId, status: "no_session", message: text };
5002
+ }
5003
+ if (!bindingAuthorized(binding, message)) {
5004
+ return { kind: "session", binding, conversationId, status: "unauthorized", message: "Message does not match binding authorization" };
5005
+ }
5006
+ const result = await sendBridgeSessionMessage(config, state, binding.activeSessionId, message, options);
5007
+ return { ...result, binding, conversationId };
5008
+ }
5009
+ function beginLedger(state, message, conversationId) {
5010
+ const id = ledgerId(message);
5011
+ const existing = state.messageLedger[id];
5012
+ if (existing && ["delivered", "skipped", "unauthorized"].includes(existing.status)) {
5013
+ return { entry: existing, shouldProcess: false };
5014
+ }
5015
+ const timestamp = nowIso();
5016
+ const entry = existing || {
5017
+ id,
5018
+ channelId: message.channelId,
5019
+ messageId: message.id,
5020
+ conversationId,
5021
+ status: "processing",
5022
+ attempts: 0,
5023
+ firstSeenAt: timestamp,
5024
+ updatedAt: timestamp
5025
+ };
5026
+ if (entry.status !== "agent_completed")
5027
+ entry.status = "processing";
5028
+ entry.attempts += 1;
5029
+ entry.conversationId = conversationId || entry.conversationId;
5030
+ entry.updatedAt = timestamp;
5031
+ delete entry.error;
5032
+ state.messageLedger[id] = entry;
5033
+ return { entry, shouldProcess: true };
5034
+ }
5035
+ function completeLedger(entry, status, sessionId, error) {
5036
+ const timestamp = nowIso();
5037
+ entry.status = status;
5038
+ entry.sessionId = sessionId || entry.sessionId;
5039
+ entry.updatedAt = timestamp;
5040
+ if (["delivered", "skipped", "unauthorized"].includes(status))
5041
+ entry.terminalAt = timestamp;
5042
+ if (error)
5043
+ entry.error = error;
5044
+ return entry;
5045
+ }
5046
+ function recordAgentCompleted(entry, sessionId, agent, responseText) {
5047
+ const timestamp = nowIso();
5048
+ entry.status = "agent_completed";
5049
+ entry.sessionId = sessionId || entry.sessionId;
5050
+ entry.responseText = responseText;
5051
+ entry.agentExitCode = agent.exitCode;
5052
+ entry.agentTimedOut = agent.timedOut;
5053
+ entry.updatedAt = timestamp;
5054
+ delete entry.error;
5055
+ return entry;
5056
+ }
5057
+ async function dispatchMessageWithSessions(config, state, message, options = {}) {
5058
+ const conversationId = messageConversationId(config, message);
5059
+ const { entry, shouldProcess } = beginLedger(state, message, conversationId);
5060
+ if (!shouldProcess)
5061
+ return { message, ledger: entry };
5062
+ await options.persistState?.(state);
5063
+ try {
5064
+ const binding = conversationId ? state.bindings[bindingId(message.channelId, conversationId)] : undefined;
5065
+ if (binding) {
5066
+ if (!bindingAuthorized(binding, message)) {
5067
+ const session3 = {
5068
+ kind: "session",
5069
+ binding,
5070
+ conversationId,
5071
+ status: "unauthorized",
5072
+ message: "Message does not match binding authorization"
5073
+ };
5074
+ completeLedger(entry, "unauthorized");
5075
+ return { message, session: session3, ledger: entry };
5076
+ }
5077
+ if (entry.status === "agent_completed") {
5078
+ const session3 = await deliverStoredResponse(config, state, binding, message, entry, options);
5079
+ return { message, session: session3, ledger: entry };
5080
+ }
5081
+ const session2 = await routeSessionMessage(config, state, message, {
5082
+ ...options,
5083
+ beforeDeliver: async (agent, responseText) => {
5084
+ recordAgentCompleted(entry, binding.activeSessionId, agent, responseText);
5085
+ await options.persistState?.(state);
5086
+ await options.beforeDeliver?.(agent, responseText);
5087
+ }
5088
+ });
5089
+ if (session2.status === "failed") {
5090
+ completeLedger(entry, "failed", session2.session?.id, session2.message);
5091
+ throw new Error(session2.message || "Agent session failed");
5092
+ }
5093
+ const terminal = session2.status === "unauthorized" ? "unauthorized" : session2.status === "delivered" || session2.status === "no_output" ? "delivered" : "skipped";
5094
+ completeLedger(entry, terminal, session2.session?.id);
5095
+ entry.deliveredResponse = session2.deliveredResponse;
5096
+ return { message, session: session2, ledger: entry };
5097
+ }
5098
+ if (options.fallbackToRoutes) {
5099
+ const routes = await routeMessage(config, message, options);
5100
+ if (routes.length) {
5101
+ completeLedger(entry, "delivered");
5102
+ return { message, routes, ledger: entry };
5103
+ }
5104
+ }
5105
+ const session = await routeSessionMessage(config, state, message, options);
5106
+ const status = session.status === "unauthorized" ? "unauthorized" : "skipped";
5107
+ completeLedger(entry, status, session.session?.id);
5108
+ return { message, session, ledger: entry };
5109
+ } catch (err) {
5110
+ const messageText = err instanceof Error ? err.message : String(err);
5111
+ if (entry.status === "agent_completed") {
5112
+ entry.error = messageText;
5113
+ entry.updatedAt = nowIso();
5114
+ } else {
5115
+ completeLedger(entry, "failed", undefined, messageText);
5116
+ }
5117
+ throw err;
5118
+ }
5119
+ }
4614
5120
  // src/mcp/index.ts
4615
5121
  function text(value) {
4616
5122
  return { content: [{ type: "text", text: typeof value === "string" ? value : JSON.stringify(value, null, 2) }] };
4617
5123
  }
4618
5124
  function buildServer() {
4619
- const server = new McpServer({ name: "bridge", version: "0.1.1" });
5125
+ const server = new McpServer({ name: "bridge", version: "0.2.1" });
4620
5126
  server.tool("bridge_status", {}, async () => text(await doctor()));
4621
5127
  server.tool("bridge_config", {}, async () => text(redactConfig(await loadConfig())));
5128
+ server.tool("bridge_session_list", {}, async () => text(listBridgeSessions(await loadState())));
5129
+ server.tool("bridge_session_status", { sessionId: exports_external.string() }, async (args) => text(getBridgeSession(await loadState(), args.sessionId)));
5130
+ server.tool("bridge_session_create", {
5131
+ agentId: exports_external.string(),
5132
+ title: exports_external.string().optional(),
5133
+ cwd: exports_external.string().optional()
5134
+ }, async (args) => {
5135
+ const config2 = await loadConfig();
5136
+ const state2 = await loadState();
5137
+ const session = createBridgeSession(config2, state2, { agentId: args.agentId, title: args.title, cwd: args.cwd });
5138
+ await saveState(state2);
5139
+ return text(session);
5140
+ });
5141
+ server.tool("bridge_session_attach", {
5142
+ sessionId: exports_external.string(),
5143
+ channelId: exports_external.string(),
5144
+ conversation: exports_external.string(),
5145
+ makeDefault: exports_external.boolean().optional()
5146
+ }, async (args) => {
5147
+ const config2 = await loadConfig();
5148
+ const state2 = await loadState();
5149
+ const binding = attachBridgeSession(config2, state2, {
5150
+ sessionId: args.sessionId,
5151
+ channelId: args.channelId,
5152
+ conversation: args.conversation,
5153
+ makeDefault: args.makeDefault
5154
+ });
5155
+ await saveState(state2);
5156
+ return text(binding);
5157
+ });
5158
+ server.tool("bridge_session_send", {
5159
+ sessionId: exports_external.string(),
5160
+ text: exports_external.string()
5161
+ }, async (args) => {
5162
+ const config2 = await loadConfig();
5163
+ const state2 = await loadState();
5164
+ const result = await sendBridgeSessionMessage(config2, state2, args.sessionId, {
5165
+ id: `mcp:${Date.now()}`,
5166
+ channelId: "mcp",
5167
+ text: args.text,
5168
+ receivedAt: new Date().toISOString()
5169
+ }, { writeConsole: false });
5170
+ await saveState(state2);
5171
+ return text(result);
5172
+ });
5173
+ server.tool("bridge_session_route_message", {
5174
+ channelId: exports_external.string(),
5175
+ text: exports_external.string(),
5176
+ chatId: exports_external.string().optional(),
5177
+ threadId: exports_external.string().optional(),
5178
+ from: exports_external.string().optional(),
5179
+ fallbackRoutes: exports_external.boolean().optional()
5180
+ }, async (args) => {
5181
+ const config2 = await loadConfig();
5182
+ const state2 = await loadState();
5183
+ const result = await dispatchMessageWithSessions(config2, state2, {
5184
+ id: `mcp:${Date.now()}`,
5185
+ channelId: args.channelId,
5186
+ text: args.text,
5187
+ chatId: args.chatId,
5188
+ threadId: args.threadId,
5189
+ from: args.from,
5190
+ receivedAt: new Date().toISOString()
5191
+ }, {
5192
+ writeConsole: false,
5193
+ fallbackToRoutes: Boolean(args.fallbackRoutes),
5194
+ persistState: async (nextState) => saveState(nextState)
5195
+ });
5196
+ await saveState(state2);
5197
+ return text(result);
5198
+ });
4622
5199
  server.tool("bridge_route_message", {
4623
5200
  channelId: exports_external.string(),
4624
5201
  text: exports_external.string(),