@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/README.md +93 -3
- package/dist/cli/index.js +832 -26
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +676 -11
- package/dist/lib/agents.d.ts +14 -1
- package/dist/lib/agents.d.ts.map +1 -1
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/daemon.d.ts.map +1 -1
- package/dist/lib/doctor.d.ts.map +1 -1
- package/dist/lib/imessage.d.ts +39 -0
- package/dist/lib/imessage.d.ts.map +1 -0
- package/dist/lib/router.d.ts.map +1 -1
- package/dist/lib/sessions.d.ts +48 -0
- package/dist/lib/sessions.d.ts.map +1 -0
- package/dist/lib/state.d.ts +8 -1
- package/dist/lib/state.d.ts.map +1 -1
- package/dist/lib/telegram.d.ts +1 -0
- package/dist/lib/telegram.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +584 -7
- package/dist/types.d.ts +73 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/architecture.md +69 -15
- package/docs/session-bridge-plan.md +204 -0
- package/package.json +1 -1
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 {
|
|
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(
|
|
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
|
|
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,213 @@ 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 tableColumns(db, table) {
|
|
5110
|
+
const rows = db.query(`pragma table_info(${table})`).all();
|
|
5111
|
+
return new Set(rows.map((row) => row.name).filter((name) => Boolean(name)));
|
|
5112
|
+
}
|
|
5113
|
+
function selectColumn(columns, table, column, alias) {
|
|
5114
|
+
return columns.has(column) ? `${table}.${column} as ${alias}` : `null as ${alias}`;
|
|
5115
|
+
}
|
|
5116
|
+
function normalizeIdentifier(value) {
|
|
5117
|
+
return value.trim().toLowerCase();
|
|
5118
|
+
}
|
|
5119
|
+
function valueMatchesConfigured(value, expected) {
|
|
5120
|
+
if (!value || !expected)
|
|
5121
|
+
return false;
|
|
5122
|
+
const normalizedValue = normalizeIdentifier(value);
|
|
5123
|
+
const normalizedExpected = normalizeIdentifier(expected);
|
|
5124
|
+
return normalizedValue === normalizedExpected || normalizedValue.endsWith(`:${normalizedExpected}`) || normalizedValue.endsWith(`;${normalizedExpected}`);
|
|
5125
|
+
}
|
|
5126
|
+
function rowMatchesAccount(channel, row) {
|
|
5127
|
+
if (!channel.account)
|
|
5128
|
+
return true;
|
|
5129
|
+
const rowCandidates = [row.account, row.accountGuid].filter(Boolean);
|
|
5130
|
+
const candidates = rowCandidates.length ? rowCandidates : [row.chatAccount].filter(Boolean);
|
|
5131
|
+
return candidates.some((value) => valueMatchesConfigured(value, channel.account));
|
|
5132
|
+
}
|
|
5133
|
+
function rowMatchesService(channel, row) {
|
|
5134
|
+
const expected = channel.serviceName || "iMessage";
|
|
5135
|
+
const candidates = row.service ? [row.service] : row.handleService ? [row.handleService] : row.chatService ? [row.chatService] : [];
|
|
5136
|
+
if (!candidates.length)
|
|
5137
|
+
return true;
|
|
5138
|
+
return candidates.some((value) => valueMatchesConfigured(value, expected));
|
|
5139
|
+
}
|
|
5140
|
+
function getIMessageMessages(channel, options = {}) {
|
|
5141
|
+
if ((channel.receiveMode || "disabled") !== "chat-db")
|
|
5142
|
+
return [];
|
|
5143
|
+
const db = new Database(getIMessageDbPath(channel), { readonly: true });
|
|
5144
|
+
try {
|
|
5145
|
+
const messageColumns = tableColumns(db, "message");
|
|
5146
|
+
const handleColumns = tableColumns(db, "handle");
|
|
5147
|
+
const chatColumns = tableColumns(db, "chat");
|
|
5148
|
+
const limit = options.limit || channel.pollLimit || 50;
|
|
5149
|
+
const scanLimit = Math.max(limit * 10, limit);
|
|
5150
|
+
const rows = db.query(`
|
|
5151
|
+
select
|
|
5152
|
+
message.ROWID as rowId,
|
|
5153
|
+
handle.id as handle,
|
|
5154
|
+
${selectColumn(messageColumns, "message", "account", "account")},
|
|
5155
|
+
${selectColumn(messageColumns, "message", "account_guid", "accountGuid")},
|
|
5156
|
+
${selectColumn(messageColumns, "message", "service", "service")},
|
|
5157
|
+
${selectColumn(handleColumns, "handle", "service", "handleService")},
|
|
5158
|
+
${selectColumn(chatColumns, "chat", "account_login", "chatAccount")},
|
|
5159
|
+
${selectColumn(chatColumns, "chat", "service_name", "chatService")},
|
|
5160
|
+
chat.guid as chatGuid,
|
|
5161
|
+
chat.display_name as displayName,
|
|
5162
|
+
message.text as text,
|
|
5163
|
+
message.date as date
|
|
5164
|
+
from message
|
|
5165
|
+
left join handle on message.handle_id = handle.ROWID
|
|
5166
|
+
left join chat_message_join on chat_message_join.message_id = message.ROWID
|
|
5167
|
+
left join chat on chat.ROWID = chat_message_join.chat_id
|
|
5168
|
+
where message.ROWID > ?
|
|
5169
|
+
and message.is_from_me = 0
|
|
5170
|
+
and message.text is not null
|
|
5171
|
+
order by message.ROWID asc
|
|
5172
|
+
limit ?
|
|
5173
|
+
`).all(options.afterRowId || 0, scanLimit);
|
|
5174
|
+
return rows.filter((row) => row.handle && row.text && imessageHandleAllowed(channel, row.handle) && rowMatchesAccount(channel, row) && rowMatchesService(channel, row)).slice(0, limit).map((row) => {
|
|
5175
|
+
const item = { rowId: row.rowId, handle: row.handle, text: row.text, date: row.date };
|
|
5176
|
+
if (row.account)
|
|
5177
|
+
item.account = row.account;
|
|
5178
|
+
if (row.accountGuid)
|
|
5179
|
+
item.accountGuid = row.accountGuid;
|
|
5180
|
+
if (row.service || row.handleService || row.chatService)
|
|
5181
|
+
item.service = row.service || row.handleService || row.chatService;
|
|
5182
|
+
if (row.chatGuid)
|
|
5183
|
+
item.chatGuid = row.chatGuid;
|
|
5184
|
+
if (row.displayName)
|
|
5185
|
+
item.displayName = row.displayName;
|
|
5186
|
+
return item;
|
|
5187
|
+
});
|
|
5188
|
+
} finally {
|
|
5189
|
+
db.close();
|
|
5190
|
+
}
|
|
5191
|
+
}
|
|
5192
|
+
function imessageRowToMessage(channelId, row) {
|
|
5193
|
+
return {
|
|
5194
|
+
id: `imessage:${row.rowId}`,
|
|
5195
|
+
channelId,
|
|
5196
|
+
chatId: row.chatGuid ? `chat:${row.chatGuid}` : row.handle,
|
|
5197
|
+
responseTargetId: row.chatGuid ? `chat:${row.chatGuid}` : row.handle,
|
|
5198
|
+
from: row.handle,
|
|
5199
|
+
text: row.text,
|
|
5200
|
+
receivedAt: imessageDateToIso(row.date),
|
|
5201
|
+
raw: row
|
|
5202
|
+
};
|
|
5203
|
+
}
|
|
5204
|
+
async function commandExists(command) {
|
|
5205
|
+
const proc = Bun.spawn(["sh", "-lc", `command -v ${command} >/dev/null 2>&1`], {
|
|
5206
|
+
stdout: "ignore",
|
|
5207
|
+
stderr: "ignore"
|
|
5208
|
+
});
|
|
5209
|
+
return await proc.exited === 0;
|
|
5210
|
+
}
|
|
5211
|
+
async function diagnoseIMessage(channel) {
|
|
5212
|
+
const checks = [];
|
|
5213
|
+
checks.push({
|
|
5214
|
+
name: `imessage-platform:${channel.id}`,
|
|
5215
|
+
ok: process.platform === "darwin",
|
|
5216
|
+
detail: process.platform === "darwin" ? "macOS" : `unsupported platform: ${process.platform}`
|
|
5217
|
+
});
|
|
5218
|
+
checks.push({
|
|
5219
|
+
name: `imessage-osascript:${channel.id}`,
|
|
5220
|
+
ok: await commandExists("osascript"),
|
|
5221
|
+
detail: "required for Messages send automation"
|
|
5222
|
+
});
|
|
5223
|
+
checks.push({
|
|
5224
|
+
name: `imessage-allowlist:${channel.id}`,
|
|
5225
|
+
ok: Boolean(channel.allowAllHandles || channel.allowedHandles?.length),
|
|
5226
|
+
detail: channel.allowAllHandles ? "allowAllHandles=true" : `${channel.allowedHandles?.length || 0} handle(s)`
|
|
5227
|
+
});
|
|
5228
|
+
if ((channel.receiveMode || "disabled") === "chat-db") {
|
|
5229
|
+
const path = getIMessageDbPath(channel);
|
|
5230
|
+
try {
|
|
5231
|
+
await access(path);
|
|
5232
|
+
checks.push({ name: `imessage-chat-db:${channel.id}`, ok: true, detail: path });
|
|
5233
|
+
} catch (err) {
|
|
5234
|
+
checks.push({
|
|
5235
|
+
name: `imessage-chat-db:${channel.id}`,
|
|
5236
|
+
ok: false,
|
|
5237
|
+
detail: `${path}: ${err instanceof Error ? err.message : String(err)}. Grant Full Disk Access to the terminal/daemon host or disable receive mode.`
|
|
5238
|
+
});
|
|
5239
|
+
}
|
|
5240
|
+
} else {
|
|
5241
|
+
checks.push({ name: `imessage-receive:${channel.id}`, ok: true, detail: "receiveMode=disabled" });
|
|
5242
|
+
}
|
|
5243
|
+
return checks;
|
|
5244
|
+
}
|
|
5245
|
+
|
|
5246
|
+
// src/lib/doctor.ts
|
|
4967
5247
|
function isNotFound2(err) {
|
|
4968
5248
|
return Boolean(err && typeof err === "object" && "code" in err && err.code === "ENOENT");
|
|
4969
5249
|
}
|
|
@@ -4991,7 +5271,7 @@ async function privateDirCheck(name, path) {
|
|
|
4991
5271
|
return { name, ok: false, detail: `${path}: ${err instanceof Error ? err.message : String(err)}` };
|
|
4992
5272
|
}
|
|
4993
5273
|
}
|
|
4994
|
-
async function
|
|
5274
|
+
async function commandExists2(command) {
|
|
4995
5275
|
const proc = Bun.spawn(["sh", "-lc", `command -v ${command} >/dev/null 2>&1`], {
|
|
4996
5276
|
stdout: "ignore",
|
|
4997
5277
|
stderr: "ignore"
|
|
@@ -5029,7 +5309,7 @@ async function doctor(configPath = defaultConfigPath(), statePath = defaultState
|
|
|
5029
5309
|
for (const command of ["bridge", "codewith", "claude", "aicopilot"]) {
|
|
5030
5310
|
checks.push({
|
|
5031
5311
|
name: `command:${command}`,
|
|
5032
|
-
ok: command === "bridge" ? true : await
|
|
5312
|
+
ok: command === "bridge" ? true : await commandExists2(command),
|
|
5033
5313
|
detail: command === "bridge" ? "current package" : undefined
|
|
5034
5314
|
});
|
|
5035
5315
|
}
|
|
@@ -5054,6 +5334,10 @@ async function doctor(configPath = defaultConfigPath(), statePath = defaultState
|
|
|
5054
5334
|
detail: `${route.fromChannel} -> ${route.toAgent}`
|
|
5055
5335
|
});
|
|
5056
5336
|
}
|
|
5337
|
+
const imessageChannels = Object.values(config.channels).filter((channel) => channel.kind === "imessage");
|
|
5338
|
+
for (const channel of imessageChannels) {
|
|
5339
|
+
checks.push(...await diagnoseIMessage(channel));
|
|
5340
|
+
}
|
|
5057
5341
|
return { ok: checks.every((check) => check.ok), configPath, checks };
|
|
5058
5342
|
}
|
|
5059
5343
|
// src/lib/router.ts
|
|
@@ -5064,6 +5348,9 @@ function matchingRoutes(config, message) {
|
|
|
5064
5348
|
if (channel?.kind === "telegram" && !telegramChatAllowed(channel, message.chatId)) {
|
|
5065
5349
|
return [];
|
|
5066
5350
|
}
|
|
5351
|
+
if (channel?.kind === "imessage" && !imessageHandleAllowed(channel, message.from || (message.chatId?.startsWith("chat:") ? undefined : message.chatId))) {
|
|
5352
|
+
return [];
|
|
5353
|
+
}
|
|
5067
5354
|
return config.routes.filter((route) => {
|
|
5068
5355
|
if (route.enabled === false)
|
|
5069
5356
|
return false;
|
|
@@ -5103,16 +5390,367 @@ async function routeMessage(config, message, options = {}) {
|
|
|
5103
5390
|
if (options.writeConsole !== false)
|
|
5104
5391
|
(options.writeConsole || console.log)(responseText);
|
|
5105
5392
|
deliveredResponse = true;
|
|
5393
|
+
} else if (responseText && channel?.kind === "imessage") {
|
|
5394
|
+
const handle = message.responseTargetId || message.chatId || message.from;
|
|
5395
|
+
const allowedIdentity = message.from || (handle?.startsWith("chat:") ? undefined : handle);
|
|
5396
|
+
if (handle && imessageHandleAllowed(channel, allowedIdentity)) {
|
|
5397
|
+
await sendIMessage(channel, handle, responseText, { allowChatTarget: handle.startsWith("chat:") });
|
|
5398
|
+
deliveredResponse = true;
|
|
5399
|
+
}
|
|
5106
5400
|
}
|
|
5107
5401
|
results.push({ route, agent, deliveredResponse });
|
|
5108
5402
|
}
|
|
5109
5403
|
return results;
|
|
5110
5404
|
}
|
|
5405
|
+
// src/lib/sessions.ts
|
|
5406
|
+
import { randomUUID } from "crypto";
|
|
5407
|
+
function nowIso() {
|
|
5408
|
+
return new Date().toISOString();
|
|
5409
|
+
}
|
|
5410
|
+
function newSessionId() {
|
|
5411
|
+
return `ses_${randomUUID()}`;
|
|
5412
|
+
}
|
|
5413
|
+
function normalizeConversationId(channel, conversation) {
|
|
5414
|
+
if (conversation.includes(":") && conversation.startsWith(`${channel.kind}:`))
|
|
5415
|
+
return conversation;
|
|
5416
|
+
if (channel.kind === "telegram")
|
|
5417
|
+
return `telegram:${channel.id}:${conversation}`;
|
|
5418
|
+
if (channel.kind === "imessage")
|
|
5419
|
+
return `imessage:${channel.id}:${conversation}`;
|
|
5420
|
+
return `${channel.kind}:${channel.id}:${conversation || "default"}`;
|
|
5421
|
+
}
|
|
5422
|
+
function messageConversationId(config, message) {
|
|
5423
|
+
const channel = config.channels[message.channelId];
|
|
5424
|
+
if (!channel)
|
|
5425
|
+
return;
|
|
5426
|
+
if (channel.kind === "telegram") {
|
|
5427
|
+
if (!message.chatId)
|
|
5428
|
+
return;
|
|
5429
|
+
return normalizeConversationId(channel, message.threadId ? `${message.chatId}:${message.threadId}` : message.chatId);
|
|
5430
|
+
}
|
|
5431
|
+
if (channel.kind === "imessage") {
|
|
5432
|
+
const conversation = message.chatId || message.from;
|
|
5433
|
+
return conversation ? normalizeConversationId(channel, conversation) : undefined;
|
|
5434
|
+
}
|
|
5435
|
+
return normalizeConversationId(channel, message.chatId || message.from || "default");
|
|
5436
|
+
}
|
|
5437
|
+
function bindingId(channelId, conversationId) {
|
|
5438
|
+
return `${channelId}::${conversationId}`;
|
|
5439
|
+
}
|
|
5440
|
+
function ledgerId(message) {
|
|
5441
|
+
return `${message.channelId}::${message.id}`;
|
|
5442
|
+
}
|
|
5443
|
+
function createBridgeSession(config, state, input) {
|
|
5444
|
+
const { agent, profile } = resolveAgent(config, input.agentId);
|
|
5445
|
+
const timestamp = nowIso();
|
|
5446
|
+
const session = {
|
|
5447
|
+
id: input.id || newSessionId(),
|
|
5448
|
+
agentId: agent.id,
|
|
5449
|
+
profileId: agent.profileId,
|
|
5450
|
+
cwd: input.cwd || agent.cwd || profile?.cwd,
|
|
5451
|
+
title: input.title,
|
|
5452
|
+
status: "active",
|
|
5453
|
+
createdAt: timestamp,
|
|
5454
|
+
updatedAt: timestamp,
|
|
5455
|
+
agentSession: createAgentSessionRef(config, agent.id)
|
|
5456
|
+
};
|
|
5457
|
+
state.sessions[session.id] = session;
|
|
5458
|
+
return session;
|
|
5459
|
+
}
|
|
5460
|
+
function getBridgeSession(state, sessionId) {
|
|
5461
|
+
const session = state.sessions[sessionId];
|
|
5462
|
+
if (!session)
|
|
5463
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
5464
|
+
return session;
|
|
5465
|
+
}
|
|
5466
|
+
function listBridgeSessions(state) {
|
|
5467
|
+
return Object.values(state.sessions).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
5468
|
+
}
|
|
5469
|
+
function updateBridgeSessionStatus(state, sessionId, status) {
|
|
5470
|
+
const session = getBridgeSession(state, sessionId);
|
|
5471
|
+
if (status === "closed")
|
|
5472
|
+
closeAgentSession(session);
|
|
5473
|
+
session.status = status;
|
|
5474
|
+
session.updatedAt = nowIso();
|
|
5475
|
+
return session;
|
|
5476
|
+
}
|
|
5477
|
+
function attachBridgeSession(config, state, input) {
|
|
5478
|
+
const channel = config.channels[input.channelId];
|
|
5479
|
+
if (!channel)
|
|
5480
|
+
throw new Error(`Channel not found: ${input.channelId}`);
|
|
5481
|
+
const session = getBridgeSession(state, input.sessionId);
|
|
5482
|
+
if (session.status === "closed")
|
|
5483
|
+
throw new Error(`Cannot attach closed session: ${session.id}`);
|
|
5484
|
+
const conversationId = normalizeConversationId(channel, input.conversation);
|
|
5485
|
+
const id = bindingId(channel.id, conversationId);
|
|
5486
|
+
const existing = state.bindings[id];
|
|
5487
|
+
const timestamp = nowIso();
|
|
5488
|
+
const binding = {
|
|
5489
|
+
id,
|
|
5490
|
+
channelId: channel.id,
|
|
5491
|
+
conversationId,
|
|
5492
|
+
activeSessionId: session.id,
|
|
5493
|
+
defaultSessionId: input.makeDefault ? session.id : existing?.defaultSessionId,
|
|
5494
|
+
createdAt: existing?.createdAt || timestamp,
|
|
5495
|
+
updatedAt: timestamp,
|
|
5496
|
+
authorization: input.authorization || existing?.authorization || (channel.kind === "telegram" ? { chatId: input.conversation.split(":")[0] } : undefined)
|
|
5497
|
+
};
|
|
5498
|
+
state.bindings[id] = binding;
|
|
5499
|
+
return binding;
|
|
5500
|
+
}
|
|
5501
|
+
function detachBridgeBinding(config, state, channelId, conversation) {
|
|
5502
|
+
const channel = config.channels[channelId];
|
|
5503
|
+
if (!channel)
|
|
5504
|
+
throw new Error(`Channel not found: ${channelId}`);
|
|
5505
|
+
const conversationId = normalizeConversationId(channel, conversation);
|
|
5506
|
+
const id = bindingId(channel.id, conversationId);
|
|
5507
|
+
const existing = state.bindings[id];
|
|
5508
|
+
delete state.bindings[id];
|
|
5509
|
+
return existing;
|
|
5510
|
+
}
|
|
5511
|
+
function findBridgeBinding(config, state, message) {
|
|
5512
|
+
const conversationId = messageConversationId(config, message);
|
|
5513
|
+
if (!conversationId)
|
|
5514
|
+
return;
|
|
5515
|
+
return state.bindings[bindingId(message.channelId, conversationId)];
|
|
5516
|
+
}
|
|
5517
|
+
function noSessionText(channelId, conversationId) {
|
|
5518
|
+
return [
|
|
5519
|
+
"No bridge session is attached to this conversation.",
|
|
5520
|
+
"Create and attach one locally:",
|
|
5521
|
+
"bridge sessions create --agent <agent-id>",
|
|
5522
|
+
`bridge sessions attach <session-id> --channel ${channelId}${conversationId ? ` --conversation ${conversationId}` : " --conversation <conversation-id>"}`
|
|
5523
|
+
].join(`
|
|
5524
|
+
`);
|
|
5525
|
+
}
|
|
5526
|
+
async function deliverResponse(config, message, text, options) {
|
|
5527
|
+
const channel = config.channels[message.channelId];
|
|
5528
|
+
if (!text || !channel || channel.enabled === false)
|
|
5529
|
+
return false;
|
|
5530
|
+
if (channel.kind === "telegram" && message.chatId) {
|
|
5531
|
+
if (!telegramChatAllowed(channel, message.chatId))
|
|
5532
|
+
return false;
|
|
5533
|
+
await (options.sendTelegram || sendTelegramMessage)(telegramToken(channel), message.chatId, text);
|
|
5534
|
+
return true;
|
|
5535
|
+
}
|
|
5536
|
+
if (channel.kind === "console") {
|
|
5537
|
+
if (options.writeConsole !== false)
|
|
5538
|
+
(options.writeConsole || console.log)(text);
|
|
5539
|
+
return true;
|
|
5540
|
+
}
|
|
5541
|
+
if (channel.kind === "imessage" && message.chatId) {
|
|
5542
|
+
const allowedIdentity = message.from || (message.chatId.startsWith("chat:") ? undefined : message.chatId);
|
|
5543
|
+
if (!imessageHandleAllowed(channel, allowedIdentity))
|
|
5544
|
+
return false;
|
|
5545
|
+
await sendIMessage(channel, message.responseTargetId || message.chatId, text, { allowChatTarget: Boolean(message.responseTargetId?.startsWith("chat:") || message.chatId.startsWith("chat:")) });
|
|
5546
|
+
return true;
|
|
5547
|
+
}
|
|
5548
|
+
return false;
|
|
5549
|
+
}
|
|
5550
|
+
async function deliverStoredResponse(config, state, binding, message, entry, options) {
|
|
5551
|
+
const session = getBridgeSession(state, binding.activeSessionId);
|
|
5552
|
+
const responseText = entry.responseText || "";
|
|
5553
|
+
const deliveredResponse = responseText ? await deliverResponse(config, message, responseText, options) : false;
|
|
5554
|
+
completeLedger(entry, "delivered", session.id);
|
|
5555
|
+
entry.deliveredResponse = deliveredResponse;
|
|
5556
|
+
return {
|
|
5557
|
+
kind: "session",
|
|
5558
|
+
session,
|
|
5559
|
+
binding,
|
|
5560
|
+
conversationId: binding.conversationId,
|
|
5561
|
+
deliveredResponse,
|
|
5562
|
+
status: responseText ? "delivered" : "no_output"
|
|
5563
|
+
};
|
|
5564
|
+
}
|
|
5565
|
+
function channelAuthorized(config, message) {
|
|
5566
|
+
const channel = config.channels[message.channelId];
|
|
5567
|
+
if (!channel || channel.enabled === false)
|
|
5568
|
+
return false;
|
|
5569
|
+
if (channel.kind === "telegram")
|
|
5570
|
+
return telegramChatAllowed(channel, message.chatId);
|
|
5571
|
+
if (channel.kind === "imessage")
|
|
5572
|
+
return imessageHandleAllowed(channel, message.from || (message.chatId?.startsWith("chat:") ? undefined : message.chatId));
|
|
5573
|
+
return true;
|
|
5574
|
+
}
|
|
5575
|
+
function bindingAuthorized(binding, message) {
|
|
5576
|
+
if (binding.authorization?.chatId && binding.authorization.chatId !== message.chatId)
|
|
5577
|
+
return false;
|
|
5578
|
+
if (binding.authorization?.from && binding.authorization.from !== message.from)
|
|
5579
|
+
return false;
|
|
5580
|
+
return true;
|
|
5581
|
+
}
|
|
5582
|
+
async function sendBridgeSessionMessage(config, state, sessionId, message, options = {}) {
|
|
5583
|
+
const session = getBridgeSession(state, sessionId);
|
|
5584
|
+
if (session.status === "paused")
|
|
5585
|
+
return { kind: "session", session, status: "paused", message: "Session is paused" };
|
|
5586
|
+
if (session.status === "closed")
|
|
5587
|
+
return { kind: "session", session, status: "closed", message: "Session is closed" };
|
|
5588
|
+
const agent = await sendAgentSessionMessage(config, session, message, { run: options.run });
|
|
5589
|
+
const timestamp = nowIso();
|
|
5590
|
+
session.lastMessageAt = timestamp;
|
|
5591
|
+
session.updatedAt = timestamp;
|
|
5592
|
+
if (session.agentSession)
|
|
5593
|
+
session.agentSession.updatedAt = timestamp;
|
|
5594
|
+
if (agent.timedOut || agent.exitCode !== null && agent.exitCode !== 0) {
|
|
5595
|
+
return {
|
|
5596
|
+
kind: "session",
|
|
5597
|
+
session,
|
|
5598
|
+
agent,
|
|
5599
|
+
deliveredResponse: false,
|
|
5600
|
+
status: "failed",
|
|
5601
|
+
message: agent.stderr.trim() || agent.stdout.trim() || (agent.timedOut ? "Agent timed out" : `Agent exited ${agent.exitCode}`)
|
|
5602
|
+
};
|
|
5603
|
+
}
|
|
5604
|
+
const responseText = agent.stdout.trim();
|
|
5605
|
+
await options.beforeDeliver?.(agent, responseText);
|
|
5606
|
+
const deliveredResponse = responseText ? await deliverResponse(config, message, responseText, options) : false;
|
|
5607
|
+
return {
|
|
5608
|
+
kind: "session",
|
|
5609
|
+
session,
|
|
5610
|
+
agent,
|
|
5611
|
+
deliveredResponse,
|
|
5612
|
+
status: responseText ? "delivered" : "no_output"
|
|
5613
|
+
};
|
|
5614
|
+
}
|
|
5615
|
+
async function routeSessionMessage(config, state, message, options = {}) {
|
|
5616
|
+
const channel = config.channels[message.channelId];
|
|
5617
|
+
if (!channel || channel.enabled === false) {
|
|
5618
|
+
return { kind: "session", status: "unauthorized", message: `Channel not enabled: ${message.channelId}` };
|
|
5619
|
+
}
|
|
5620
|
+
if (!channelAuthorized(config, message)) {
|
|
5621
|
+
return { kind: "session", status: "unauthorized", message: "Message is not authorized for this channel" };
|
|
5622
|
+
}
|
|
5623
|
+
const conversationId = messageConversationId(config, message);
|
|
5624
|
+
const binding = conversationId ? state.bindings[bindingId(message.channelId, conversationId)] : undefined;
|
|
5625
|
+
if (!binding) {
|
|
5626
|
+
const text = noSessionText(message.channelId, conversationId);
|
|
5627
|
+
if (options.respondOnNoSession !== false)
|
|
5628
|
+
await deliverResponse(config, message, text, options);
|
|
5629
|
+
return { kind: "session", conversationId, status: "no_session", message: text };
|
|
5630
|
+
}
|
|
5631
|
+
if (!bindingAuthorized(binding, message)) {
|
|
5632
|
+
return { kind: "session", binding, conversationId, status: "unauthorized", message: "Message does not match binding authorization" };
|
|
5633
|
+
}
|
|
5634
|
+
const result = await sendBridgeSessionMessage(config, state, binding.activeSessionId, message, options);
|
|
5635
|
+
return { ...result, binding, conversationId };
|
|
5636
|
+
}
|
|
5637
|
+
function beginLedger(state, message, conversationId) {
|
|
5638
|
+
const id = ledgerId(message);
|
|
5639
|
+
const existing = state.messageLedger[id];
|
|
5640
|
+
if (existing && ["delivered", "skipped", "unauthorized"].includes(existing.status)) {
|
|
5641
|
+
return { entry: existing, shouldProcess: false };
|
|
5642
|
+
}
|
|
5643
|
+
const timestamp = nowIso();
|
|
5644
|
+
const entry = existing || {
|
|
5645
|
+
id,
|
|
5646
|
+
channelId: message.channelId,
|
|
5647
|
+
messageId: message.id,
|
|
5648
|
+
conversationId,
|
|
5649
|
+
status: "processing",
|
|
5650
|
+
attempts: 0,
|
|
5651
|
+
firstSeenAt: timestamp,
|
|
5652
|
+
updatedAt: timestamp
|
|
5653
|
+
};
|
|
5654
|
+
if (entry.status !== "agent_completed")
|
|
5655
|
+
entry.status = "processing";
|
|
5656
|
+
entry.attempts += 1;
|
|
5657
|
+
entry.conversationId = conversationId || entry.conversationId;
|
|
5658
|
+
entry.updatedAt = timestamp;
|
|
5659
|
+
delete entry.error;
|
|
5660
|
+
state.messageLedger[id] = entry;
|
|
5661
|
+
return { entry, shouldProcess: true };
|
|
5662
|
+
}
|
|
5663
|
+
function completeLedger(entry, status, sessionId, error) {
|
|
5664
|
+
const timestamp = nowIso();
|
|
5665
|
+
entry.status = status;
|
|
5666
|
+
entry.sessionId = sessionId || entry.sessionId;
|
|
5667
|
+
entry.updatedAt = timestamp;
|
|
5668
|
+
if (["delivered", "skipped", "unauthorized"].includes(status))
|
|
5669
|
+
entry.terminalAt = timestamp;
|
|
5670
|
+
if (error)
|
|
5671
|
+
entry.error = error;
|
|
5672
|
+
return entry;
|
|
5673
|
+
}
|
|
5674
|
+
function recordAgentCompleted(entry, sessionId, agent, responseText) {
|
|
5675
|
+
const timestamp = nowIso();
|
|
5676
|
+
entry.status = "agent_completed";
|
|
5677
|
+
entry.sessionId = sessionId || entry.sessionId;
|
|
5678
|
+
entry.responseText = responseText;
|
|
5679
|
+
entry.agentExitCode = agent.exitCode;
|
|
5680
|
+
entry.agentTimedOut = agent.timedOut;
|
|
5681
|
+
entry.updatedAt = timestamp;
|
|
5682
|
+
delete entry.error;
|
|
5683
|
+
return entry;
|
|
5684
|
+
}
|
|
5685
|
+
async function dispatchMessageWithSessions(config, state, message, options = {}) {
|
|
5686
|
+
const conversationId = messageConversationId(config, message);
|
|
5687
|
+
const { entry, shouldProcess } = beginLedger(state, message, conversationId);
|
|
5688
|
+
if (!shouldProcess)
|
|
5689
|
+
return { message, ledger: entry };
|
|
5690
|
+
await options.persistState?.(state);
|
|
5691
|
+
try {
|
|
5692
|
+
const binding = conversationId ? state.bindings[bindingId(message.channelId, conversationId)] : undefined;
|
|
5693
|
+
if (binding) {
|
|
5694
|
+
if (!bindingAuthorized(binding, message)) {
|
|
5695
|
+
const session3 = {
|
|
5696
|
+
kind: "session",
|
|
5697
|
+
binding,
|
|
5698
|
+
conversationId,
|
|
5699
|
+
status: "unauthorized",
|
|
5700
|
+
message: "Message does not match binding authorization"
|
|
5701
|
+
};
|
|
5702
|
+
completeLedger(entry, "unauthorized");
|
|
5703
|
+
return { message, session: session3, ledger: entry };
|
|
5704
|
+
}
|
|
5705
|
+
if (entry.status === "agent_completed") {
|
|
5706
|
+
const session3 = await deliverStoredResponse(config, state, binding, message, entry, options);
|
|
5707
|
+
return { message, session: session3, ledger: entry };
|
|
5708
|
+
}
|
|
5709
|
+
const session2 = await routeSessionMessage(config, state, message, {
|
|
5710
|
+
...options,
|
|
5711
|
+
beforeDeliver: async (agent, responseText) => {
|
|
5712
|
+
recordAgentCompleted(entry, binding.activeSessionId, agent, responseText);
|
|
5713
|
+
await options.persistState?.(state);
|
|
5714
|
+
await options.beforeDeliver?.(agent, responseText);
|
|
5715
|
+
}
|
|
5716
|
+
});
|
|
5717
|
+
if (session2.status === "failed") {
|
|
5718
|
+
completeLedger(entry, "failed", session2.session?.id, session2.message);
|
|
5719
|
+
throw new Error(session2.message || "Agent session failed");
|
|
5720
|
+
}
|
|
5721
|
+
const terminal = session2.status === "unauthorized" ? "unauthorized" : session2.status === "delivered" || session2.status === "no_output" ? "delivered" : "skipped";
|
|
5722
|
+
completeLedger(entry, terminal, session2.session?.id);
|
|
5723
|
+
entry.deliveredResponse = session2.deliveredResponse;
|
|
5724
|
+
return { message, session: session2, ledger: entry };
|
|
5725
|
+
}
|
|
5726
|
+
if (options.fallbackToRoutes) {
|
|
5727
|
+
const routes = await routeMessage(config, message, options);
|
|
5728
|
+
if (routes.length) {
|
|
5729
|
+
completeLedger(entry, "delivered");
|
|
5730
|
+
return { message, routes, ledger: entry };
|
|
5731
|
+
}
|
|
5732
|
+
}
|
|
5733
|
+
const session = await routeSessionMessage(config, state, message, options);
|
|
5734
|
+
const status = session.status === "unauthorized" ? "unauthorized" : "skipped";
|
|
5735
|
+
completeLedger(entry, status, session.session?.id);
|
|
5736
|
+
return { message, session, ledger: entry };
|
|
5737
|
+
} catch (err) {
|
|
5738
|
+
const messageText = err instanceof Error ? err.message : String(err);
|
|
5739
|
+
if (entry.status === "agent_completed") {
|
|
5740
|
+
entry.error = messageText;
|
|
5741
|
+
entry.updatedAt = nowIso();
|
|
5742
|
+
} else {
|
|
5743
|
+
completeLedger(entry, "failed", undefined, messageText);
|
|
5744
|
+
}
|
|
5745
|
+
throw err;
|
|
5746
|
+
}
|
|
5747
|
+
}
|
|
5111
5748
|
export {
|
|
5112
5749
|
upsertRoute,
|
|
5113
5750
|
upsertProfile,
|
|
5114
5751
|
upsertChannel,
|
|
5115
5752
|
upsertAgent,
|
|
5753
|
+
updateBridgeSessionStatus,
|
|
5116
5754
|
uninstallDaemon,
|
|
5117
5755
|
telegramUpdateToMessage,
|
|
5118
5756
|
telegramToken,
|
|
@@ -5124,38 +5762,65 @@ export {
|
|
|
5124
5762
|
startProcessDaemon,
|
|
5125
5763
|
startInstalledDaemon,
|
|
5126
5764
|
sendTelegramMessage,
|
|
5765
|
+
sendIMessage,
|
|
5766
|
+
sendBridgeSessionMessage,
|
|
5767
|
+
sendAgentSessionMessage,
|
|
5127
5768
|
saveState,
|
|
5128
5769
|
saveConfig,
|
|
5129
5770
|
runAgent,
|
|
5771
|
+
routeSessionMessage,
|
|
5130
5772
|
routeMessage,
|
|
5773
|
+
resumeAgentSessionRef,
|
|
5131
5774
|
restartProcessDaemon,
|
|
5132
5775
|
restartInstalledDaemon,
|
|
5133
5776
|
resolveSupervisor,
|
|
5134
5777
|
resolveAgent,
|
|
5135
5778
|
requiredTelegramEnvVars,
|
|
5136
5779
|
renderSystemdUnit,
|
|
5780
|
+
renderSendIMessageScript,
|
|
5137
5781
|
renderLaunchdPlist,
|
|
5138
5782
|
redactConfig,
|
|
5139
5783
|
parseConfig,
|
|
5784
|
+
normalizeConversationId,
|
|
5785
|
+
messageConversationId,
|
|
5140
5786
|
matchingRoutes,
|
|
5141
5787
|
loadState,
|
|
5142
5788
|
loadConfig,
|
|
5789
|
+
listBridgeSessions,
|
|
5790
|
+
ledgerId,
|
|
5143
5791
|
installDaemon,
|
|
5792
|
+
imessageRowToMessage,
|
|
5793
|
+
imessageHandleAllowed,
|
|
5144
5794
|
homeDir,
|
|
5145
5795
|
getTelegramUpdates,
|
|
5796
|
+
getIMessageMessages,
|
|
5797
|
+
getIMessageDbPath,
|
|
5798
|
+
getBridgeSession,
|
|
5799
|
+
findBridgeBinding,
|
|
5146
5800
|
ensureDaemonDir,
|
|
5147
5801
|
ensureConfig,
|
|
5148
5802
|
emptyState,
|
|
5149
5803
|
emptyConfig,
|
|
5150
5804
|
doctor,
|
|
5805
|
+
dispatchMessageWithSessions,
|
|
5806
|
+
diagnoseIMessage,
|
|
5807
|
+
detachBridgeBinding,
|
|
5151
5808
|
defaultStatePath,
|
|
5809
|
+
defaultMessagesDbPath,
|
|
5152
5810
|
defaultDaemonDir,
|
|
5153
5811
|
defaultConfigPath,
|
|
5154
5812
|
daemonStatus,
|
|
5155
5813
|
daemonPaths,
|
|
5156
5814
|
daemonLogs,
|
|
5815
|
+
createBridgeSession,
|
|
5816
|
+
createAgentSessionRef,
|
|
5817
|
+
closeAgentSession,
|
|
5818
|
+
cancelAgentSession,
|
|
5157
5819
|
buildAgentCommand,
|
|
5158
5820
|
bridgeHome,
|
|
5821
|
+
bindingId,
|
|
5822
|
+
attachBridgeSession,
|
|
5823
|
+
STATE_SCHEMA_VERSION,
|
|
5159
5824
|
CONFIG_VERSION,
|
|
5160
5825
|
CHANNEL_KINDS,
|
|
5161
5826
|
AGENT_KINDS
|