@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/README.md +79 -3
- package/dist/cli/index.js +786 -26
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +630 -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 +36 -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/cli/index.js
CHANGED
|
@@ -2064,7 +2064,7 @@ var require_commander = __commonJS((exports) => {
|
|
|
2064
2064
|
|
|
2065
2065
|
// src/cli/index.ts
|
|
2066
2066
|
import { readFileSync } from "fs";
|
|
2067
|
-
import { dirname as dirname4, join as
|
|
2067
|
+
import { dirname as dirname4, join as join5 } from "path";
|
|
2068
2068
|
import { fileURLToPath } from "url";
|
|
2069
2069
|
|
|
2070
2070
|
// node_modules/commander/esm.mjs
|
|
@@ -2103,6 +2103,11 @@ function mergeEnv(profile, agent) {
|
|
|
2103
2103
|
env["HOME"] = profile.home;
|
|
2104
2104
|
return Object.keys(env).length ? env : undefined;
|
|
2105
2105
|
}
|
|
2106
|
+
function compatibilityDetail(kind) {
|
|
2107
|
+
if (kind === "shell")
|
|
2108
|
+
return "shell command session; local bridge state is durable";
|
|
2109
|
+
return "compatibility mode: this adapter invokes the current CLI one message at a time until a stable create/send/resume API is wired";
|
|
2110
|
+
}
|
|
2106
2111
|
function resolveAgent(config, agentId) {
|
|
2107
2112
|
const agent = config.agents[agentId];
|
|
2108
2113
|
if (!agent)
|
|
@@ -2121,7 +2126,7 @@ function buildAgentCommand(config, agentId, input) {
|
|
|
2121
2126
|
const kind = agent.kind;
|
|
2122
2127
|
const command = agent.command || profile?.command;
|
|
2123
2128
|
const args = agent.args || profile?.args;
|
|
2124
|
-
const cwd = agent.cwd || profile?.cwd;
|
|
2129
|
+
const cwd = input.session?.cwd || agent.cwd || profile?.cwd;
|
|
2125
2130
|
const env = mergeEnv(profile, agent);
|
|
2126
2131
|
if (command) {
|
|
2127
2132
|
return { command: [command, ...renderCustomArgs(args, prompt)], cwd, env };
|
|
@@ -2143,6 +2148,32 @@ function buildAgentCommand(config, agentId, input) {
|
|
|
2143
2148
|
}
|
|
2144
2149
|
return { command: ["sh", "-lc", prompt], cwd, env };
|
|
2145
2150
|
}
|
|
2151
|
+
function createAgentSessionRef(config, agentId) {
|
|
2152
|
+
const { agent } = resolveAgent(config, agentId);
|
|
2153
|
+
const timestamp = new Date().toISOString();
|
|
2154
|
+
return {
|
|
2155
|
+
kind: agent.kind,
|
|
2156
|
+
mode: "compatibility",
|
|
2157
|
+
createdAt: timestamp,
|
|
2158
|
+
updatedAt: timestamp,
|
|
2159
|
+
detail: compatibilityDetail(agent.kind)
|
|
2160
|
+
};
|
|
2161
|
+
}
|
|
2162
|
+
function closeAgentSession(session) {
|
|
2163
|
+
return {
|
|
2164
|
+
supported: session.agentSession?.mode === "durable",
|
|
2165
|
+
ref: session.agentSession,
|
|
2166
|
+
detail: session.agentSession?.mode === "durable" ? "durable close is adapter-owned" : "compatibility close only updates bridge session state"
|
|
2167
|
+
};
|
|
2168
|
+
}
|
|
2169
|
+
async function sendAgentSessionMessage(config, session, message, options = {}) {
|
|
2170
|
+
const run = options.run || runAgent;
|
|
2171
|
+
return run(config, session.agentId, {
|
|
2172
|
+
message,
|
|
2173
|
+
route: { id: `session:${session.id}`, fromChannel: message.channelId, toAgent: session.agentId },
|
|
2174
|
+
session
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
2146
2177
|
async function runAgent(config, agentId, input) {
|
|
2147
2178
|
const { agent } = resolveAgent(config, agentId);
|
|
2148
2179
|
const built = buildAgentCommand(config, agentId, input);
|
|
@@ -6200,7 +6231,14 @@ var channelSchema = exports_external.discriminatedUnion("kind", [
|
|
|
6200
6231
|
kind: exports_external.literal("imessage"),
|
|
6201
6232
|
label: exports_external.string().optional(),
|
|
6202
6233
|
enabled: exports_external.boolean().optional(),
|
|
6203
|
-
account: exports_external.string().optional()
|
|
6234
|
+
account: exports_external.string().optional(),
|
|
6235
|
+
serviceName: exports_external.string().optional(),
|
|
6236
|
+
defaultHandle: exports_external.string().optional(),
|
|
6237
|
+
allowedHandles: exports_external.array(exports_external.string()).optional(),
|
|
6238
|
+
allowAllHandles: exports_external.boolean().optional(),
|
|
6239
|
+
receiveMode: exports_external.enum(["disabled", "chat-db"]).optional(),
|
|
6240
|
+
chatDbPath: exports_external.string().optional(),
|
|
6241
|
+
pollLimit: exports_external.number().int().positive().max(500).optional()
|
|
6204
6242
|
})
|
|
6205
6243
|
]);
|
|
6206
6244
|
var envSchema = exports_external.record(exports_external.string(), exports_external.string());
|
|
@@ -6313,19 +6351,35 @@ import { dirname as dirname3, join as join3, resolve } from "path";
|
|
|
6313
6351
|
// src/lib/state.ts
|
|
6314
6352
|
import { chmod as chmod2, mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
6315
6353
|
import { dirname as dirname2, join as join2 } from "path";
|
|
6354
|
+
var STATE_SCHEMA_VERSION = 2;
|
|
6316
6355
|
function defaultStatePath() {
|
|
6317
6356
|
return process.env["BRIDGE_STATE"] || join2(bridgeHome(), "state.json");
|
|
6318
6357
|
}
|
|
6319
6358
|
function emptyState() {
|
|
6320
|
-
return {
|
|
6359
|
+
return {
|
|
6360
|
+
schemaVersion: STATE_SCHEMA_VERSION,
|
|
6361
|
+
telegramOffsets: {},
|
|
6362
|
+
sessions: {},
|
|
6363
|
+
bindings: {},
|
|
6364
|
+
messageLedger: {},
|
|
6365
|
+
cursors: {}
|
|
6366
|
+
};
|
|
6367
|
+
}
|
|
6368
|
+
function normalizeState(value) {
|
|
6369
|
+
return {
|
|
6370
|
+
schemaVersion: STATE_SCHEMA_VERSION,
|
|
6371
|
+
telegramOffsets: value.telegramOffsets && typeof value.telegramOffsets === "object" ? value.telegramOffsets : {},
|
|
6372
|
+
sessions: value.sessions && typeof value.sessions === "object" ? value.sessions : {},
|
|
6373
|
+
bindings: value.bindings && typeof value.bindings === "object" ? value.bindings : {},
|
|
6374
|
+
messageLedger: value.messageLedger && typeof value.messageLedger === "object" ? value.messageLedger : {},
|
|
6375
|
+
cursors: value.cursors && typeof value.cursors === "object" ? value.cursors : {}
|
|
6376
|
+
};
|
|
6321
6377
|
}
|
|
6322
6378
|
async function loadState(statePath = defaultStatePath()) {
|
|
6323
6379
|
try {
|
|
6324
6380
|
const raw = await readFile2(statePath, "utf-8");
|
|
6325
6381
|
const parsed = JSON.parse(raw);
|
|
6326
|
-
return
|
|
6327
|
-
telegramOffsets: parsed.telegramOffsets && typeof parsed.telegramOffsets === "object" ? parsed.telegramOffsets : {}
|
|
6328
|
-
};
|
|
6382
|
+
return normalizeState(parsed);
|
|
6329
6383
|
} catch (err) {
|
|
6330
6384
|
if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") {
|
|
6331
6385
|
return emptyState();
|
|
@@ -6334,8 +6388,9 @@ async function loadState(statePath = defaultStatePath()) {
|
|
|
6334
6388
|
}
|
|
6335
6389
|
}
|
|
6336
6390
|
async function saveState(state, statePath = defaultStatePath()) {
|
|
6391
|
+
const normalized = normalizeState(state);
|
|
6337
6392
|
await mkdir2(dirname2(statePath), { recursive: true, mode: 448 });
|
|
6338
|
-
await writeFile2(statePath, `${JSON.stringify(
|
|
6393
|
+
await writeFile2(statePath, `${JSON.stringify(normalized, null, 2)}
|
|
6339
6394
|
`, { encoding: "utf-8", mode: 384 });
|
|
6340
6395
|
await chmod2(statePath, 384);
|
|
6341
6396
|
}
|
|
@@ -6422,6 +6477,7 @@ function telegramUpdateToMessage(channelId, update) {
|
|
|
6422
6477
|
channelId,
|
|
6423
6478
|
text,
|
|
6424
6479
|
chatId: String(chatId),
|
|
6480
|
+
threadId: update.message?.message_thread_id !== undefined ? String(update.message.message_thread_id) : undefined,
|
|
6425
6481
|
from: update.message?.from?.username || (update.message?.from?.id !== undefined ? String(update.message.from.id) : undefined),
|
|
6426
6482
|
receivedAt: update.message?.date ? new Date(update.message.date * 1000).toISOString() : new Date().toISOString(),
|
|
6427
6483
|
raw: update
|
|
@@ -6600,14 +6656,17 @@ function startCommand(options) {
|
|
|
6600
6656
|
function telegramChannels(config) {
|
|
6601
6657
|
return Object.values(config.channels).filter((channel) => channel.kind === "telegram" && channel.enabled !== false);
|
|
6602
6658
|
}
|
|
6659
|
+
function imessagePollChannels(config) {
|
|
6660
|
+
return Object.values(config.channels).filter((channel) => channel.kind === "imessage" && channel.enabled !== false && channel.receiveMode === "chat-db");
|
|
6661
|
+
}
|
|
6603
6662
|
function requiredTelegramEnvVars(config) {
|
|
6604
6663
|
return [...new Set(telegramChannels(config).map((channel) => channel.botTokenEnv || "TELEGRAM_BOT_TOKEN"))];
|
|
6605
6664
|
}
|
|
6606
6665
|
async function validateStartConfig(configPath) {
|
|
6607
6666
|
const config = await loadConfig(configPath);
|
|
6608
|
-
const channels = telegramChannels(config);
|
|
6667
|
+
const channels = [...telegramChannels(config), ...imessagePollChannels(config)];
|
|
6609
6668
|
if (!channels.length)
|
|
6610
|
-
throw new Error("No enabled
|
|
6669
|
+
throw new Error("No enabled pollable channels configured; add Telegram or iMessage receive before starting the daemon");
|
|
6611
6670
|
for (const envName of requiredTelegramEnvVars(config)) {
|
|
6612
6671
|
if (!process.env[envName])
|
|
6613
6672
|
throw new Error(`Missing Telegram bot token env var for daemon start: ${envName}`);
|
|
@@ -7009,6 +7068,167 @@ async function daemonLogs(options = {}) {
|
|
|
7009
7068
|
}
|
|
7010
7069
|
// src/lib/doctor.ts
|
|
7011
7070
|
import { stat as stat2 } from "fs/promises";
|
|
7071
|
+
|
|
7072
|
+
// src/lib/imessage.ts
|
|
7073
|
+
import { access } from "fs/promises";
|
|
7074
|
+
import { join as join4 } from "path";
|
|
7075
|
+
import { Database } from "bun:sqlite";
|
|
7076
|
+
function defaultMessagesDbPath() {
|
|
7077
|
+
return join4(homeDir(), "Library", "Messages", "chat.db");
|
|
7078
|
+
}
|
|
7079
|
+
function imessageHandleAllowed(channel, handle) {
|
|
7080
|
+
if (channel.allowAllHandles)
|
|
7081
|
+
return true;
|
|
7082
|
+
if (!channel.allowedHandles?.length)
|
|
7083
|
+
return false;
|
|
7084
|
+
return Boolean(handle && channel.allowedHandles.includes(handle));
|
|
7085
|
+
}
|
|
7086
|
+
function appleScriptString(value) {
|
|
7087
|
+
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"")}"`;
|
|
7088
|
+
}
|
|
7089
|
+
function renderSendIMessageScript(channel, handle, text) {
|
|
7090
|
+
const service = channel.serviceName || "iMessage";
|
|
7091
|
+
const serviceSelector = channel.account ? `1st service whose name = ${appleScriptString(service)} and account = ${appleScriptString(channel.account)}` : `1st service whose name = ${appleScriptString(service)}`;
|
|
7092
|
+
const targetLines = handle.startsWith("chat:") ? [
|
|
7093
|
+
`set targetChat to 1st chat whose id = ${appleScriptString(handle.slice("chat:".length))}`,
|
|
7094
|
+
`send ${appleScriptString(text)} to targetChat`
|
|
7095
|
+
] : [
|
|
7096
|
+
`set targetBuddy to buddy ${appleScriptString(handle)} of targetService`,
|
|
7097
|
+
`send ${appleScriptString(text)} to targetBuddy`
|
|
7098
|
+
];
|
|
7099
|
+
return [
|
|
7100
|
+
'tell application "Messages"',
|
|
7101
|
+
`set targetService to ${serviceSelector}`,
|
|
7102
|
+
...targetLines,
|
|
7103
|
+
"end tell"
|
|
7104
|
+
].join(`
|
|
7105
|
+
`);
|
|
7106
|
+
}
|
|
7107
|
+
async function defaultRun(command) {
|
|
7108
|
+
const proc = Bun.spawn(command, { stdout: "pipe", stderr: "pipe" });
|
|
7109
|
+
const [exitCode, stdout, stderr] = await Promise.all([
|
|
7110
|
+
proc.exited,
|
|
7111
|
+
new Response(proc.stdout).text(),
|
|
7112
|
+
new Response(proc.stderr).text()
|
|
7113
|
+
]);
|
|
7114
|
+
return { exitCode, stdout, stderr };
|
|
7115
|
+
}
|
|
7116
|
+
async function sendIMessage(channel, handle, text, options = {}) {
|
|
7117
|
+
if (!(options.allowChatTarget && handle.startsWith("chat:")) && !imessageHandleAllowed(channel, handle)) {
|
|
7118
|
+
throw new Error(`iMessage handle is not allowed for channel ${channel.id}: ${handle}`);
|
|
7119
|
+
}
|
|
7120
|
+
const script = renderSendIMessageScript(channel, handle, text);
|
|
7121
|
+
const result = await (options.run || defaultRun)(["osascript", "-e", script]);
|
|
7122
|
+
if (result.exitCode !== 0) {
|
|
7123
|
+
throw new Error(`iMessage send failed: ${result.stderr || result.stdout || `exit ${result.exitCode}`}`);
|
|
7124
|
+
}
|
|
7125
|
+
return { ok: true };
|
|
7126
|
+
}
|
|
7127
|
+
function imessageDateToIso(value) {
|
|
7128
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0)
|
|
7129
|
+
return new Date().toISOString();
|
|
7130
|
+
const appleEpochMs = Date.UTC(2001, 0, 1);
|
|
7131
|
+
if (value > 1000000000000000)
|
|
7132
|
+
return new Date(appleEpochMs + Math.floor(value / 1e6)).toISOString();
|
|
7133
|
+
if (value > 1e9)
|
|
7134
|
+
return new Date(appleEpochMs + value * 1000).toISOString();
|
|
7135
|
+
return new Date(appleEpochMs + value).toISOString();
|
|
7136
|
+
}
|
|
7137
|
+
function getIMessageDbPath(channel) {
|
|
7138
|
+
return channel.chatDbPath || defaultMessagesDbPath();
|
|
7139
|
+
}
|
|
7140
|
+
function getIMessageMessages(channel, options = {}) {
|
|
7141
|
+
if ((channel.receiveMode || "disabled") !== "chat-db")
|
|
7142
|
+
return [];
|
|
7143
|
+
const db = new Database(getIMessageDbPath(channel), { readonly: true });
|
|
7144
|
+
try {
|
|
7145
|
+
const limit = options.limit || channel.pollLimit || 50;
|
|
7146
|
+
const scanLimit = Math.max(limit * 10, limit);
|
|
7147
|
+
const rows = db.query(`
|
|
7148
|
+
select
|
|
7149
|
+
message.ROWID as rowId,
|
|
7150
|
+
handle.id as handle,
|
|
7151
|
+
chat.guid as chatGuid,
|
|
7152
|
+
chat.display_name as displayName,
|
|
7153
|
+
message.text as text,
|
|
7154
|
+
message.date as date
|
|
7155
|
+
from message
|
|
7156
|
+
left join handle on message.handle_id = handle.ROWID
|
|
7157
|
+
left join chat_message_join on chat_message_join.message_id = message.ROWID
|
|
7158
|
+
left join chat on chat.ROWID = chat_message_join.chat_id
|
|
7159
|
+
where message.ROWID > ?
|
|
7160
|
+
and message.is_from_me = 0
|
|
7161
|
+
and message.text is not null
|
|
7162
|
+
order by message.ROWID asc
|
|
7163
|
+
limit ?
|
|
7164
|
+
`).all(options.afterRowId || 0, scanLimit);
|
|
7165
|
+
return rows.filter((row) => row.handle && row.text && imessageHandleAllowed(channel, row.handle)).slice(0, limit).map((row) => {
|
|
7166
|
+
const item = { rowId: row.rowId, handle: row.handle, text: row.text, date: row.date };
|
|
7167
|
+
if (row.chatGuid)
|
|
7168
|
+
item.chatGuid = row.chatGuid;
|
|
7169
|
+
if (row.displayName)
|
|
7170
|
+
item.displayName = row.displayName;
|
|
7171
|
+
return item;
|
|
7172
|
+
});
|
|
7173
|
+
} finally {
|
|
7174
|
+
db.close();
|
|
7175
|
+
}
|
|
7176
|
+
}
|
|
7177
|
+
function imessageRowToMessage(channelId, row) {
|
|
7178
|
+
return {
|
|
7179
|
+
id: `imessage:${row.rowId}`,
|
|
7180
|
+
channelId,
|
|
7181
|
+
chatId: row.chatGuid ? `chat:${row.chatGuid}` : row.handle,
|
|
7182
|
+
responseTargetId: row.chatGuid ? `chat:${row.chatGuid}` : row.handle,
|
|
7183
|
+
from: row.handle,
|
|
7184
|
+
text: row.text,
|
|
7185
|
+
receivedAt: imessageDateToIso(row.date),
|
|
7186
|
+
raw: row
|
|
7187
|
+
};
|
|
7188
|
+
}
|
|
7189
|
+
async function commandExists(command) {
|
|
7190
|
+
const proc = Bun.spawn(["sh", "-lc", `command -v ${command} >/dev/null 2>&1`], {
|
|
7191
|
+
stdout: "ignore",
|
|
7192
|
+
stderr: "ignore"
|
|
7193
|
+
});
|
|
7194
|
+
return await proc.exited === 0;
|
|
7195
|
+
}
|
|
7196
|
+
async function diagnoseIMessage(channel) {
|
|
7197
|
+
const checks = [];
|
|
7198
|
+
checks.push({
|
|
7199
|
+
name: `imessage-platform:${channel.id}`,
|
|
7200
|
+
ok: process.platform === "darwin",
|
|
7201
|
+
detail: process.platform === "darwin" ? "macOS" : `unsupported platform: ${process.platform}`
|
|
7202
|
+
});
|
|
7203
|
+
checks.push({
|
|
7204
|
+
name: `imessage-osascript:${channel.id}`,
|
|
7205
|
+
ok: await commandExists("osascript"),
|
|
7206
|
+
detail: "required for Messages send automation"
|
|
7207
|
+
});
|
|
7208
|
+
checks.push({
|
|
7209
|
+
name: `imessage-allowlist:${channel.id}`,
|
|
7210
|
+
ok: Boolean(channel.allowAllHandles || channel.allowedHandles?.length),
|
|
7211
|
+
detail: channel.allowAllHandles ? "allowAllHandles=true" : `${channel.allowedHandles?.length || 0} handle(s)`
|
|
7212
|
+
});
|
|
7213
|
+
if ((channel.receiveMode || "disabled") === "chat-db") {
|
|
7214
|
+
const path = getIMessageDbPath(channel);
|
|
7215
|
+
try {
|
|
7216
|
+
await access(path);
|
|
7217
|
+
checks.push({ name: `imessage-chat-db:${channel.id}`, ok: true, detail: path });
|
|
7218
|
+
} catch (err) {
|
|
7219
|
+
checks.push({
|
|
7220
|
+
name: `imessage-chat-db:${channel.id}`,
|
|
7221
|
+
ok: false,
|
|
7222
|
+
detail: `${path}: ${err instanceof Error ? err.message : String(err)}. Grant Full Disk Access to the terminal/daemon host or disable receive mode.`
|
|
7223
|
+
});
|
|
7224
|
+
}
|
|
7225
|
+
} else {
|
|
7226
|
+
checks.push({ name: `imessage-receive:${channel.id}`, ok: true, detail: "receiveMode=disabled" });
|
|
7227
|
+
}
|
|
7228
|
+
return checks;
|
|
7229
|
+
}
|
|
7230
|
+
|
|
7231
|
+
// src/lib/doctor.ts
|
|
7012
7232
|
function isNotFound2(err) {
|
|
7013
7233
|
return Boolean(err && typeof err === "object" && "code" in err && err.code === "ENOENT");
|
|
7014
7234
|
}
|
|
@@ -7036,7 +7256,7 @@ async function privateDirCheck(name, path) {
|
|
|
7036
7256
|
return { name, ok: false, detail: `${path}: ${err instanceof Error ? err.message : String(err)}` };
|
|
7037
7257
|
}
|
|
7038
7258
|
}
|
|
7039
|
-
async function
|
|
7259
|
+
async function commandExists2(command) {
|
|
7040
7260
|
const proc = Bun.spawn(["sh", "-lc", `command -v ${command} >/dev/null 2>&1`], {
|
|
7041
7261
|
stdout: "ignore",
|
|
7042
7262
|
stderr: "ignore"
|
|
@@ -7074,7 +7294,7 @@ async function doctor(configPath = defaultConfigPath(), statePath = defaultState
|
|
|
7074
7294
|
for (const command of ["bridge", "codewith", "claude", "aicopilot"]) {
|
|
7075
7295
|
checks.push({
|
|
7076
7296
|
name: `command:${command}`,
|
|
7077
|
-
ok: command === "bridge" ? true : await
|
|
7297
|
+
ok: command === "bridge" ? true : await commandExists2(command),
|
|
7078
7298
|
detail: command === "bridge" ? "current package" : undefined
|
|
7079
7299
|
});
|
|
7080
7300
|
}
|
|
@@ -7099,6 +7319,10 @@ async function doctor(configPath = defaultConfigPath(), statePath = defaultState
|
|
|
7099
7319
|
detail: `${route.fromChannel} -> ${route.toAgent}`
|
|
7100
7320
|
});
|
|
7101
7321
|
}
|
|
7322
|
+
const imessageChannels = Object.values(config.channels).filter((channel) => channel.kind === "imessage");
|
|
7323
|
+
for (const channel of imessageChannels) {
|
|
7324
|
+
checks.push(...await diagnoseIMessage(channel));
|
|
7325
|
+
}
|
|
7102
7326
|
return { ok: checks.every((check) => check.ok), configPath, checks };
|
|
7103
7327
|
}
|
|
7104
7328
|
// src/lib/router.ts
|
|
@@ -7109,6 +7333,9 @@ function matchingRoutes(config, message) {
|
|
|
7109
7333
|
if (channel?.kind === "telegram" && !telegramChatAllowed(channel, message.chatId)) {
|
|
7110
7334
|
return [];
|
|
7111
7335
|
}
|
|
7336
|
+
if (channel?.kind === "imessage" && !imessageHandleAllowed(channel, message.from || (message.chatId?.startsWith("chat:") ? undefined : message.chatId))) {
|
|
7337
|
+
return [];
|
|
7338
|
+
}
|
|
7112
7339
|
return config.routes.filter((route) => {
|
|
7113
7340
|
if (route.enabled === false)
|
|
7114
7341
|
return false;
|
|
@@ -7148,15 +7375,359 @@ async function routeMessage(config, message, options = {}) {
|
|
|
7148
7375
|
if (options.writeConsole !== false)
|
|
7149
7376
|
(options.writeConsole || console.log)(responseText);
|
|
7150
7377
|
deliveredResponse = true;
|
|
7378
|
+
} else if (responseText && channel?.kind === "imessage") {
|
|
7379
|
+
const handle = message.responseTargetId || message.chatId || message.from;
|
|
7380
|
+
const allowedIdentity = message.from || (handle?.startsWith("chat:") ? undefined : handle);
|
|
7381
|
+
if (handle && imessageHandleAllowed(channel, allowedIdentity)) {
|
|
7382
|
+
await sendIMessage(channel, handle, responseText, { allowChatTarget: handle.startsWith("chat:") });
|
|
7383
|
+
deliveredResponse = true;
|
|
7384
|
+
}
|
|
7151
7385
|
}
|
|
7152
7386
|
results.push({ route, agent, deliveredResponse });
|
|
7153
7387
|
}
|
|
7154
7388
|
return results;
|
|
7155
7389
|
}
|
|
7390
|
+
// src/lib/sessions.ts
|
|
7391
|
+
import { randomUUID } from "crypto";
|
|
7392
|
+
function nowIso() {
|
|
7393
|
+
return new Date().toISOString();
|
|
7394
|
+
}
|
|
7395
|
+
function newSessionId() {
|
|
7396
|
+
return `ses_${randomUUID()}`;
|
|
7397
|
+
}
|
|
7398
|
+
function normalizeConversationId(channel, conversation) {
|
|
7399
|
+
if (conversation.includes(":") && conversation.startsWith(`${channel.kind}:`))
|
|
7400
|
+
return conversation;
|
|
7401
|
+
if (channel.kind === "telegram")
|
|
7402
|
+
return `telegram:${channel.id}:${conversation}`;
|
|
7403
|
+
if (channel.kind === "imessage")
|
|
7404
|
+
return `imessage:${channel.id}:${conversation}`;
|
|
7405
|
+
return `${channel.kind}:${channel.id}:${conversation || "default"}`;
|
|
7406
|
+
}
|
|
7407
|
+
function messageConversationId(config, message) {
|
|
7408
|
+
const channel = config.channels[message.channelId];
|
|
7409
|
+
if (!channel)
|
|
7410
|
+
return;
|
|
7411
|
+
if (channel.kind === "telegram") {
|
|
7412
|
+
if (!message.chatId)
|
|
7413
|
+
return;
|
|
7414
|
+
return normalizeConversationId(channel, message.threadId ? `${message.chatId}:${message.threadId}` : message.chatId);
|
|
7415
|
+
}
|
|
7416
|
+
if (channel.kind === "imessage") {
|
|
7417
|
+
const conversation = message.chatId || message.from;
|
|
7418
|
+
return conversation ? normalizeConversationId(channel, conversation) : undefined;
|
|
7419
|
+
}
|
|
7420
|
+
return normalizeConversationId(channel, message.chatId || message.from || "default");
|
|
7421
|
+
}
|
|
7422
|
+
function bindingId(channelId, conversationId) {
|
|
7423
|
+
return `${channelId}::${conversationId}`;
|
|
7424
|
+
}
|
|
7425
|
+
function ledgerId(message) {
|
|
7426
|
+
return `${message.channelId}::${message.id}`;
|
|
7427
|
+
}
|
|
7428
|
+
function createBridgeSession(config, state, input) {
|
|
7429
|
+
const { agent, profile } = resolveAgent(config, input.agentId);
|
|
7430
|
+
const timestamp = nowIso();
|
|
7431
|
+
const session = {
|
|
7432
|
+
id: input.id || newSessionId(),
|
|
7433
|
+
agentId: agent.id,
|
|
7434
|
+
profileId: agent.profileId,
|
|
7435
|
+
cwd: input.cwd || agent.cwd || profile?.cwd,
|
|
7436
|
+
title: input.title,
|
|
7437
|
+
status: "active",
|
|
7438
|
+
createdAt: timestamp,
|
|
7439
|
+
updatedAt: timestamp,
|
|
7440
|
+
agentSession: createAgentSessionRef(config, agent.id)
|
|
7441
|
+
};
|
|
7442
|
+
state.sessions[session.id] = session;
|
|
7443
|
+
return session;
|
|
7444
|
+
}
|
|
7445
|
+
function getBridgeSession(state, sessionId) {
|
|
7446
|
+
const session = state.sessions[sessionId];
|
|
7447
|
+
if (!session)
|
|
7448
|
+
throw new Error(`Session not found: ${sessionId}`);
|
|
7449
|
+
return session;
|
|
7450
|
+
}
|
|
7451
|
+
function listBridgeSessions(state) {
|
|
7452
|
+
return Object.values(state.sessions).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
7453
|
+
}
|
|
7454
|
+
function updateBridgeSessionStatus(state, sessionId, status) {
|
|
7455
|
+
const session = getBridgeSession(state, sessionId);
|
|
7456
|
+
if (status === "closed")
|
|
7457
|
+
closeAgentSession(session);
|
|
7458
|
+
session.status = status;
|
|
7459
|
+
session.updatedAt = nowIso();
|
|
7460
|
+
return session;
|
|
7461
|
+
}
|
|
7462
|
+
function attachBridgeSession(config, state, input) {
|
|
7463
|
+
const channel = config.channels[input.channelId];
|
|
7464
|
+
if (!channel)
|
|
7465
|
+
throw new Error(`Channel not found: ${input.channelId}`);
|
|
7466
|
+
const session = getBridgeSession(state, input.sessionId);
|
|
7467
|
+
if (session.status === "closed")
|
|
7468
|
+
throw new Error(`Cannot attach closed session: ${session.id}`);
|
|
7469
|
+
const conversationId = normalizeConversationId(channel, input.conversation);
|
|
7470
|
+
const id = bindingId(channel.id, conversationId);
|
|
7471
|
+
const existing = state.bindings[id];
|
|
7472
|
+
const timestamp = nowIso();
|
|
7473
|
+
const binding = {
|
|
7474
|
+
id,
|
|
7475
|
+
channelId: channel.id,
|
|
7476
|
+
conversationId,
|
|
7477
|
+
activeSessionId: session.id,
|
|
7478
|
+
defaultSessionId: input.makeDefault ? session.id : existing?.defaultSessionId,
|
|
7479
|
+
createdAt: existing?.createdAt || timestamp,
|
|
7480
|
+
updatedAt: timestamp,
|
|
7481
|
+
authorization: input.authorization || existing?.authorization || (channel.kind === "telegram" ? { chatId: input.conversation.split(":")[0] } : undefined)
|
|
7482
|
+
};
|
|
7483
|
+
state.bindings[id] = binding;
|
|
7484
|
+
return binding;
|
|
7485
|
+
}
|
|
7486
|
+
function detachBridgeBinding(config, state, channelId, conversation) {
|
|
7487
|
+
const channel = config.channels[channelId];
|
|
7488
|
+
if (!channel)
|
|
7489
|
+
throw new Error(`Channel not found: ${channelId}`);
|
|
7490
|
+
const conversationId = normalizeConversationId(channel, conversation);
|
|
7491
|
+
const id = bindingId(channel.id, conversationId);
|
|
7492
|
+
const existing = state.bindings[id];
|
|
7493
|
+
delete state.bindings[id];
|
|
7494
|
+
return existing;
|
|
7495
|
+
}
|
|
7496
|
+
function noSessionText(channelId, conversationId) {
|
|
7497
|
+
return [
|
|
7498
|
+
"No bridge session is attached to this conversation.",
|
|
7499
|
+
"Create and attach one locally:",
|
|
7500
|
+
"bridge sessions create --agent <agent-id>",
|
|
7501
|
+
`bridge sessions attach <session-id> --channel ${channelId}${conversationId ? ` --conversation ${conversationId}` : " --conversation <conversation-id>"}`
|
|
7502
|
+
].join(`
|
|
7503
|
+
`);
|
|
7504
|
+
}
|
|
7505
|
+
async function deliverResponse(config, message, text, options) {
|
|
7506
|
+
const channel = config.channels[message.channelId];
|
|
7507
|
+
if (!text || !channel || channel.enabled === false)
|
|
7508
|
+
return false;
|
|
7509
|
+
if (channel.kind === "telegram" && message.chatId) {
|
|
7510
|
+
if (!telegramChatAllowed(channel, message.chatId))
|
|
7511
|
+
return false;
|
|
7512
|
+
await (options.sendTelegram || sendTelegramMessage)(telegramToken(channel), message.chatId, text);
|
|
7513
|
+
return true;
|
|
7514
|
+
}
|
|
7515
|
+
if (channel.kind === "console") {
|
|
7516
|
+
if (options.writeConsole !== false)
|
|
7517
|
+
(options.writeConsole || console.log)(text);
|
|
7518
|
+
return true;
|
|
7519
|
+
}
|
|
7520
|
+
if (channel.kind === "imessage" && message.chatId) {
|
|
7521
|
+
const allowedIdentity = message.from || (message.chatId.startsWith("chat:") ? undefined : message.chatId);
|
|
7522
|
+
if (!imessageHandleAllowed(channel, allowedIdentity))
|
|
7523
|
+
return false;
|
|
7524
|
+
await sendIMessage(channel, message.responseTargetId || message.chatId, text, { allowChatTarget: Boolean(message.responseTargetId?.startsWith("chat:") || message.chatId.startsWith("chat:")) });
|
|
7525
|
+
return true;
|
|
7526
|
+
}
|
|
7527
|
+
return false;
|
|
7528
|
+
}
|
|
7529
|
+
async function deliverStoredResponse(config, state, binding, message, entry, options) {
|
|
7530
|
+
const session = getBridgeSession(state, binding.activeSessionId);
|
|
7531
|
+
const responseText = entry.responseText || "";
|
|
7532
|
+
const deliveredResponse = responseText ? await deliverResponse(config, message, responseText, options) : false;
|
|
7533
|
+
completeLedger(entry, "delivered", session.id);
|
|
7534
|
+
entry.deliveredResponse = deliveredResponse;
|
|
7535
|
+
return {
|
|
7536
|
+
kind: "session",
|
|
7537
|
+
session,
|
|
7538
|
+
binding,
|
|
7539
|
+
conversationId: binding.conversationId,
|
|
7540
|
+
deliveredResponse,
|
|
7541
|
+
status: responseText ? "delivered" : "no_output"
|
|
7542
|
+
};
|
|
7543
|
+
}
|
|
7544
|
+
function channelAuthorized(config, message) {
|
|
7545
|
+
const channel = config.channels[message.channelId];
|
|
7546
|
+
if (!channel || channel.enabled === false)
|
|
7547
|
+
return false;
|
|
7548
|
+
if (channel.kind === "telegram")
|
|
7549
|
+
return telegramChatAllowed(channel, message.chatId);
|
|
7550
|
+
if (channel.kind === "imessage")
|
|
7551
|
+
return imessageHandleAllowed(channel, message.from || (message.chatId?.startsWith("chat:") ? undefined : message.chatId));
|
|
7552
|
+
return true;
|
|
7553
|
+
}
|
|
7554
|
+
function bindingAuthorized(binding, message) {
|
|
7555
|
+
if (binding.authorization?.chatId && binding.authorization.chatId !== message.chatId)
|
|
7556
|
+
return false;
|
|
7557
|
+
if (binding.authorization?.from && binding.authorization.from !== message.from)
|
|
7558
|
+
return false;
|
|
7559
|
+
return true;
|
|
7560
|
+
}
|
|
7561
|
+
async function sendBridgeSessionMessage(config, state, sessionId, message, options = {}) {
|
|
7562
|
+
const session = getBridgeSession(state, sessionId);
|
|
7563
|
+
if (session.status === "paused")
|
|
7564
|
+
return { kind: "session", session, status: "paused", message: "Session is paused" };
|
|
7565
|
+
if (session.status === "closed")
|
|
7566
|
+
return { kind: "session", session, status: "closed", message: "Session is closed" };
|
|
7567
|
+
const agent = await sendAgentSessionMessage(config, session, message, { run: options.run });
|
|
7568
|
+
const timestamp = nowIso();
|
|
7569
|
+
session.lastMessageAt = timestamp;
|
|
7570
|
+
session.updatedAt = timestamp;
|
|
7571
|
+
if (session.agentSession)
|
|
7572
|
+
session.agentSession.updatedAt = timestamp;
|
|
7573
|
+
if (agent.timedOut || agent.exitCode !== null && agent.exitCode !== 0) {
|
|
7574
|
+
return {
|
|
7575
|
+
kind: "session",
|
|
7576
|
+
session,
|
|
7577
|
+
agent,
|
|
7578
|
+
deliveredResponse: false,
|
|
7579
|
+
status: "failed",
|
|
7580
|
+
message: agent.stderr.trim() || agent.stdout.trim() || (agent.timedOut ? "Agent timed out" : `Agent exited ${agent.exitCode}`)
|
|
7581
|
+
};
|
|
7582
|
+
}
|
|
7583
|
+
const responseText = agent.stdout.trim();
|
|
7584
|
+
await options.beforeDeliver?.(agent, responseText);
|
|
7585
|
+
const deliveredResponse = responseText ? await deliverResponse(config, message, responseText, options) : false;
|
|
7586
|
+
return {
|
|
7587
|
+
kind: "session",
|
|
7588
|
+
session,
|
|
7589
|
+
agent,
|
|
7590
|
+
deliveredResponse,
|
|
7591
|
+
status: responseText ? "delivered" : "no_output"
|
|
7592
|
+
};
|
|
7593
|
+
}
|
|
7594
|
+
async function routeSessionMessage(config, state, message, options = {}) {
|
|
7595
|
+
const channel = config.channels[message.channelId];
|
|
7596
|
+
if (!channel || channel.enabled === false) {
|
|
7597
|
+
return { kind: "session", status: "unauthorized", message: `Channel not enabled: ${message.channelId}` };
|
|
7598
|
+
}
|
|
7599
|
+
if (!channelAuthorized(config, message)) {
|
|
7600
|
+
return { kind: "session", status: "unauthorized", message: "Message is not authorized for this channel" };
|
|
7601
|
+
}
|
|
7602
|
+
const conversationId = messageConversationId(config, message);
|
|
7603
|
+
const binding = conversationId ? state.bindings[bindingId(message.channelId, conversationId)] : undefined;
|
|
7604
|
+
if (!binding) {
|
|
7605
|
+
const text = noSessionText(message.channelId, conversationId);
|
|
7606
|
+
if (options.respondOnNoSession !== false)
|
|
7607
|
+
await deliverResponse(config, message, text, options);
|
|
7608
|
+
return { kind: "session", conversationId, status: "no_session", message: text };
|
|
7609
|
+
}
|
|
7610
|
+
if (!bindingAuthorized(binding, message)) {
|
|
7611
|
+
return { kind: "session", binding, conversationId, status: "unauthorized", message: "Message does not match binding authorization" };
|
|
7612
|
+
}
|
|
7613
|
+
const result = await sendBridgeSessionMessage(config, state, binding.activeSessionId, message, options);
|
|
7614
|
+
return { ...result, binding, conversationId };
|
|
7615
|
+
}
|
|
7616
|
+
function beginLedger(state, message, conversationId) {
|
|
7617
|
+
const id = ledgerId(message);
|
|
7618
|
+
const existing = state.messageLedger[id];
|
|
7619
|
+
if (existing && ["delivered", "skipped", "unauthorized"].includes(existing.status)) {
|
|
7620
|
+
return { entry: existing, shouldProcess: false };
|
|
7621
|
+
}
|
|
7622
|
+
const timestamp = nowIso();
|
|
7623
|
+
const entry = existing || {
|
|
7624
|
+
id,
|
|
7625
|
+
channelId: message.channelId,
|
|
7626
|
+
messageId: message.id,
|
|
7627
|
+
conversationId,
|
|
7628
|
+
status: "processing",
|
|
7629
|
+
attempts: 0,
|
|
7630
|
+
firstSeenAt: timestamp,
|
|
7631
|
+
updatedAt: timestamp
|
|
7632
|
+
};
|
|
7633
|
+
if (entry.status !== "agent_completed")
|
|
7634
|
+
entry.status = "processing";
|
|
7635
|
+
entry.attempts += 1;
|
|
7636
|
+
entry.conversationId = conversationId || entry.conversationId;
|
|
7637
|
+
entry.updatedAt = timestamp;
|
|
7638
|
+
delete entry.error;
|
|
7639
|
+
state.messageLedger[id] = entry;
|
|
7640
|
+
return { entry, shouldProcess: true };
|
|
7641
|
+
}
|
|
7642
|
+
function completeLedger(entry, status, sessionId, error) {
|
|
7643
|
+
const timestamp = nowIso();
|
|
7644
|
+
entry.status = status;
|
|
7645
|
+
entry.sessionId = sessionId || entry.sessionId;
|
|
7646
|
+
entry.updatedAt = timestamp;
|
|
7647
|
+
if (["delivered", "skipped", "unauthorized"].includes(status))
|
|
7648
|
+
entry.terminalAt = timestamp;
|
|
7649
|
+
if (error)
|
|
7650
|
+
entry.error = error;
|
|
7651
|
+
return entry;
|
|
7652
|
+
}
|
|
7653
|
+
function recordAgentCompleted(entry, sessionId, agent, responseText) {
|
|
7654
|
+
const timestamp = nowIso();
|
|
7655
|
+
entry.status = "agent_completed";
|
|
7656
|
+
entry.sessionId = sessionId || entry.sessionId;
|
|
7657
|
+
entry.responseText = responseText;
|
|
7658
|
+
entry.agentExitCode = agent.exitCode;
|
|
7659
|
+
entry.agentTimedOut = agent.timedOut;
|
|
7660
|
+
entry.updatedAt = timestamp;
|
|
7661
|
+
delete entry.error;
|
|
7662
|
+
return entry;
|
|
7663
|
+
}
|
|
7664
|
+
async function dispatchMessageWithSessions(config, state, message, options = {}) {
|
|
7665
|
+
const conversationId = messageConversationId(config, message);
|
|
7666
|
+
const { entry, shouldProcess } = beginLedger(state, message, conversationId);
|
|
7667
|
+
if (!shouldProcess)
|
|
7668
|
+
return { message, ledger: entry };
|
|
7669
|
+
await options.persistState?.(state);
|
|
7670
|
+
try {
|
|
7671
|
+
const binding = conversationId ? state.bindings[bindingId(message.channelId, conversationId)] : undefined;
|
|
7672
|
+
if (binding) {
|
|
7673
|
+
if (!bindingAuthorized(binding, message)) {
|
|
7674
|
+
const session3 = {
|
|
7675
|
+
kind: "session",
|
|
7676
|
+
binding,
|
|
7677
|
+
conversationId,
|
|
7678
|
+
status: "unauthorized",
|
|
7679
|
+
message: "Message does not match binding authorization"
|
|
7680
|
+
};
|
|
7681
|
+
completeLedger(entry, "unauthorized");
|
|
7682
|
+
return { message, session: session3, ledger: entry };
|
|
7683
|
+
}
|
|
7684
|
+
if (entry.status === "agent_completed") {
|
|
7685
|
+
const session3 = await deliverStoredResponse(config, state, binding, message, entry, options);
|
|
7686
|
+
return { message, session: session3, ledger: entry };
|
|
7687
|
+
}
|
|
7688
|
+
const session2 = await routeSessionMessage(config, state, message, {
|
|
7689
|
+
...options,
|
|
7690
|
+
beforeDeliver: async (agent, responseText) => {
|
|
7691
|
+
recordAgentCompleted(entry, binding.activeSessionId, agent, responseText);
|
|
7692
|
+
await options.persistState?.(state);
|
|
7693
|
+
await options.beforeDeliver?.(agent, responseText);
|
|
7694
|
+
}
|
|
7695
|
+
});
|
|
7696
|
+
if (session2.status === "failed") {
|
|
7697
|
+
completeLedger(entry, "failed", session2.session?.id, session2.message);
|
|
7698
|
+
throw new Error(session2.message || "Agent session failed");
|
|
7699
|
+
}
|
|
7700
|
+
const terminal = session2.status === "unauthorized" ? "unauthorized" : session2.status === "delivered" || session2.status === "no_output" ? "delivered" : "skipped";
|
|
7701
|
+
completeLedger(entry, terminal, session2.session?.id);
|
|
7702
|
+
entry.deliveredResponse = session2.deliveredResponse;
|
|
7703
|
+
return { message, session: session2, ledger: entry };
|
|
7704
|
+
}
|
|
7705
|
+
if (options.fallbackToRoutes) {
|
|
7706
|
+
const routes = await routeMessage(config, message, options);
|
|
7707
|
+
if (routes.length) {
|
|
7708
|
+
completeLedger(entry, "delivered");
|
|
7709
|
+
return { message, routes, ledger: entry };
|
|
7710
|
+
}
|
|
7711
|
+
}
|
|
7712
|
+
const session = await routeSessionMessage(config, state, message, options);
|
|
7713
|
+
const status = session.status === "unauthorized" ? "unauthorized" : "skipped";
|
|
7714
|
+
completeLedger(entry, status, session.session?.id);
|
|
7715
|
+
return { message, session, ledger: entry };
|
|
7716
|
+
} catch (err) {
|
|
7717
|
+
const messageText = err instanceof Error ? err.message : String(err);
|
|
7718
|
+
if (entry.status === "agent_completed") {
|
|
7719
|
+
entry.error = messageText;
|
|
7720
|
+
entry.updatedAt = nowIso();
|
|
7721
|
+
} else {
|
|
7722
|
+
completeLedger(entry, "failed", undefined, messageText);
|
|
7723
|
+
}
|
|
7724
|
+
throw err;
|
|
7725
|
+
}
|
|
7726
|
+
}
|
|
7156
7727
|
// src/cli/index.ts
|
|
7157
7728
|
function version() {
|
|
7158
7729
|
try {
|
|
7159
|
-
const pkgPath =
|
|
7730
|
+
const pkgPath = join5(dirname4(fileURLToPath(import.meta.url)), "..", "..", "package.json");
|
|
7160
7731
|
return JSON.parse(readFileSync(pkgPath, "utf-8")).version || "0.0.0";
|
|
7161
7732
|
} catch {
|
|
7162
7733
|
return "0.0.0";
|
|
@@ -7198,11 +7769,11 @@ function printList(items) {
|
|
|
7198
7769
|
async function runServe(options) {
|
|
7199
7770
|
const config2 = await loadConfig(options.config);
|
|
7200
7771
|
const telegramChannels2 = Object.values(config2.channels).filter((channel) => channel.kind === "telegram" && channel.enabled !== false);
|
|
7772
|
+
const imessageChannels = Object.values(config2.channels).filter((channel) => channel.kind === "imessage" && channel.enabled !== false && channel.receiveMode === "chat-db");
|
|
7201
7773
|
const intervalMs = parseNonNegativeInt(options.interval || "1000", "--interval");
|
|
7202
|
-
if (!telegramChannels2.length)
|
|
7203
|
-
throw new Error("No enabled
|
|
7774
|
+
if (!telegramChannels2.length && !imessageChannels.length)
|
|
7775
|
+
throw new Error("No enabled pollable channels configured");
|
|
7204
7776
|
const statePath = options.state || defaultStatePath();
|
|
7205
|
-
const state2 = await loadState(statePath);
|
|
7206
7777
|
const errorCounts = new Map;
|
|
7207
7778
|
let stopping = false;
|
|
7208
7779
|
const stop = () => {
|
|
@@ -7213,20 +7784,79 @@ async function runServe(options) {
|
|
|
7213
7784
|
while (!stopping) {
|
|
7214
7785
|
for (const channel of telegramChannels2) {
|
|
7215
7786
|
try {
|
|
7787
|
+
const pollState = await loadState(statePath);
|
|
7216
7788
|
const updates = await getTelegramUpdates(telegramToken(channel), {
|
|
7217
|
-
offset:
|
|
7789
|
+
offset: pollState.telegramOffsets[channel.id],
|
|
7218
7790
|
timeoutSeconds: channel.pollTimeoutSeconds || 20
|
|
7219
7791
|
});
|
|
7220
7792
|
errorCounts.delete(channel.id);
|
|
7221
7793
|
for (const update of updates) {
|
|
7222
|
-
state2
|
|
7223
|
-
await saveState(state2, statePath);
|
|
7794
|
+
const state2 = await loadState(statePath);
|
|
7224
7795
|
const message = telegramUpdateToMessage(channel.id, update);
|
|
7225
|
-
if (
|
|
7226
|
-
|
|
7227
|
-
|
|
7228
|
-
|
|
7229
|
-
|
|
7796
|
+
if (message) {
|
|
7797
|
+
try {
|
|
7798
|
+
const results = await dispatchMessageWithSessions(config2, state2, message, {
|
|
7799
|
+
writeConsole: options.json ? false : undefined,
|
|
7800
|
+
fallbackToRoutes: true,
|
|
7801
|
+
persistState: async (nextState) => saveState(nextState, statePath)
|
|
7802
|
+
});
|
|
7803
|
+
if (results.ledger?.status === "failed" || results.ledger?.status === "processing" || results.ledger?.status === "agent_completed") {
|
|
7804
|
+
await saveState(state2, statePath);
|
|
7805
|
+
throw new Error(results.ledger.error || `Message ${message.id} did not reach a terminal state`);
|
|
7806
|
+
}
|
|
7807
|
+
state2.telegramOffsets[channel.id] = update.update_id + 1;
|
|
7808
|
+
await saveState(state2, statePath);
|
|
7809
|
+
if (options.json)
|
|
7810
|
+
asJson(results);
|
|
7811
|
+
} catch (err) {
|
|
7812
|
+
await saveState(state2, statePath);
|
|
7813
|
+
throw err;
|
|
7814
|
+
}
|
|
7815
|
+
} else {
|
|
7816
|
+
state2.telegramOffsets[channel.id] = update.update_id + 1;
|
|
7817
|
+
await saveState(state2, statePath);
|
|
7818
|
+
}
|
|
7819
|
+
}
|
|
7820
|
+
} catch (err) {
|
|
7821
|
+
if (options.once)
|
|
7822
|
+
throw err;
|
|
7823
|
+
const count = (errorCounts.get(channel.id) || 0) + 1;
|
|
7824
|
+
errorCounts.set(channel.id, count);
|
|
7825
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
7826
|
+
console.error(`[bridge] ${channel.id} poll failed (${count}): ${message}`);
|
|
7827
|
+
await Bun.sleep(Math.min(30000, Math.max(1000, intervalMs * Math.min(count, 30))));
|
|
7828
|
+
}
|
|
7829
|
+
}
|
|
7830
|
+
for (const channel of imessageChannels) {
|
|
7831
|
+
try {
|
|
7832
|
+
const pollState = await loadState(statePath);
|
|
7833
|
+
const cursorKey = `imessage:${channel.id}`;
|
|
7834
|
+
const rows = getIMessageMessages(channel, {
|
|
7835
|
+
afterRowId: Number(pollState.cursors[cursorKey] || 0),
|
|
7836
|
+
limit: channel.pollLimit || 50
|
|
7837
|
+
});
|
|
7838
|
+
errorCounts.delete(channel.id);
|
|
7839
|
+
for (const row of rows) {
|
|
7840
|
+
const state2 = await loadState(statePath);
|
|
7841
|
+
const message = imessageRowToMessage(channel.id, row);
|
|
7842
|
+
try {
|
|
7843
|
+
const results = await dispatchMessageWithSessions(config2, state2, message, {
|
|
7844
|
+
writeConsole: options.json ? false : undefined,
|
|
7845
|
+
fallbackToRoutes: true,
|
|
7846
|
+
persistState: async (nextState) => saveState(nextState, statePath)
|
|
7847
|
+
});
|
|
7848
|
+
if (results.ledger?.status === "failed" || results.ledger?.status === "processing" || results.ledger?.status === "agent_completed") {
|
|
7849
|
+
await saveState(state2, statePath);
|
|
7850
|
+
throw new Error(results.ledger.error || `Message ${message.id} did not reach a terminal state`);
|
|
7851
|
+
}
|
|
7852
|
+
state2.cursors[cursorKey] = row.rowId;
|
|
7853
|
+
await saveState(state2, statePath);
|
|
7854
|
+
if (options.json)
|
|
7855
|
+
asJson(results);
|
|
7856
|
+
} catch (err) {
|
|
7857
|
+
await saveState(state2, statePath);
|
|
7858
|
+
throw err;
|
|
7859
|
+
}
|
|
7230
7860
|
}
|
|
7231
7861
|
} catch (err) {
|
|
7232
7862
|
if (options.once)
|
|
@@ -7294,6 +7924,27 @@ channels.command("add-console").argument("<id>").description("Add a console chan
|
|
|
7294
7924
|
const config2 = await upsertChannel({ id, kind: "console", enabled: true }, options.config);
|
|
7295
7925
|
options.json ? asJson(config2.channels[id]) : console.log(`Added console channel ${id}`);
|
|
7296
7926
|
});
|
|
7927
|
+
channels.command("add-imessage").argument("<id>").description("Add a local macOS iMessage channel").option("--default-handle <handle>", "default iMessage handle for bridge send").option("--allowed-handles <handles>", "comma-separated allowed handles").option("--allow-all-handles", "explicitly allow every local iMessage handle").option("--account <account>", "Messages account selector for multi-account Macs").option("--service-name <name>", "Messages service name", "iMessage").option("--receive", "enable local Messages chat.db polling").option("--chat-db-path <path>", "override Messages chat.db path").option("--poll-limit <n>", "maximum rows per poll").option("-c, --config <path>", "config path", defaultConfigPath()).option("--json", "output JSON").action(async (id, options) => {
|
|
7928
|
+
const allowedHandles = splitCsv(options.allowedHandles);
|
|
7929
|
+
if (!allowedHandles?.length && !options.allowAllHandles) {
|
|
7930
|
+
throw new Error("iMessage channels require --allowed-handles or explicit --allow-all-handles");
|
|
7931
|
+
}
|
|
7932
|
+
const pollLimit = options.pollLimit ? Number.parseInt(options.pollLimit, 10) : undefined;
|
|
7933
|
+
const config2 = await upsertChannel({
|
|
7934
|
+
id,
|
|
7935
|
+
kind: "imessage",
|
|
7936
|
+
enabled: true,
|
|
7937
|
+
defaultHandle: options.defaultHandle,
|
|
7938
|
+
allowedHandles,
|
|
7939
|
+
allowAllHandles: Boolean(options.allowAllHandles),
|
|
7940
|
+
account: options.account,
|
|
7941
|
+
serviceName: options.serviceName,
|
|
7942
|
+
receiveMode: options.receive ? "chat-db" : "disabled",
|
|
7943
|
+
chatDbPath: options.chatDbPath,
|
|
7944
|
+
pollLimit
|
|
7945
|
+
}, options.config);
|
|
7946
|
+
options.json ? asJson(config2.channels[id]) : console.log(`Added imessage channel ${id}`);
|
|
7947
|
+
});
|
|
7297
7948
|
var profiles = program2.command("profiles").description("Manage reusable agent profiles");
|
|
7298
7949
|
profiles.command("list").option("-c, --config <path>", "config path", defaultConfigPath()).option("--json", "output JSON").action(async (options) => {
|
|
7299
7950
|
const config2 = await loadConfig(options.config);
|
|
@@ -7350,14 +8001,103 @@ routes.command("add").argument("<id>").requiredOption("--from <channel>", "sourc
|
|
|
7350
8001
|
}, options.config);
|
|
7351
8002
|
options.json ? asJson(config2.routes.find((route) => route.id === id)) : console.log(`Added route ${id}`);
|
|
7352
8003
|
});
|
|
8004
|
+
var sessions2 = program2.command("sessions").description("Manage durable bridge sessions and channel bindings");
|
|
8005
|
+
sessions2.command("list").description("List bridge sessions").option("--state <path>", "state path", defaultStatePath()).option("--json", "output JSON").action(async (options) => {
|
|
8006
|
+
const state2 = await loadState(options.state);
|
|
8007
|
+
const items = listBridgeSessions(state2);
|
|
8008
|
+
options.json ? asJson(items) : printList(items);
|
|
8009
|
+
});
|
|
8010
|
+
sessions2.command("show").argument("<id>").description("Show one bridge session").option("--state <path>", "state path", defaultStatePath()).option("--json", "output JSON").action(async (id, options) => {
|
|
8011
|
+
const state2 = await loadState(options.state);
|
|
8012
|
+
const session = getBridgeSession(state2, id);
|
|
8013
|
+
options.json ? asJson(session) : console.log(JSON.stringify(session, null, 2));
|
|
8014
|
+
});
|
|
8015
|
+
sessions2.command("create").description("Create a bridge-owned session for an agent").requiredOption("--agent <id>", "agent id").option("--id <id>", "explicit session id").option("--title <text>", "session title").option("--cwd <path>", "session working directory override").option("-c, --config <path>", "config path", defaultConfigPath()).option("--state <path>", "state path", defaultStatePath()).option("--json", "output JSON").action(async (options) => {
|
|
8016
|
+
const config2 = await loadConfig(options.config);
|
|
8017
|
+
const state2 = await loadState(options.state);
|
|
8018
|
+
const session = createBridgeSession(config2, state2, {
|
|
8019
|
+
id: options.id,
|
|
8020
|
+
agentId: options.agent,
|
|
8021
|
+
title: options.title,
|
|
8022
|
+
cwd: options.cwd
|
|
8023
|
+
});
|
|
8024
|
+
await saveState(state2, options.state);
|
|
8025
|
+
options.json ? asJson(session) : console.log(session.id);
|
|
8026
|
+
});
|
|
8027
|
+
async function attachSessionAction(sessionId, options) {
|
|
8028
|
+
const config2 = await loadConfig(options.config);
|
|
8029
|
+
const state2 = await loadState(options.state);
|
|
8030
|
+
const binding = attachBridgeSession(config2, state2, {
|
|
8031
|
+
sessionId,
|
|
8032
|
+
channelId: options.channel,
|
|
8033
|
+
conversation: options.conversation,
|
|
8034
|
+
makeDefault: Boolean(options.default)
|
|
8035
|
+
});
|
|
8036
|
+
await saveState(state2, options.state);
|
|
8037
|
+
options.json ? asJson(binding) : console.log(binding.id);
|
|
8038
|
+
}
|
|
8039
|
+
sessions2.command("attach").argument("<id>").description("Attach a session to a channel conversation").requiredOption("--channel <id>", "channel id").requiredOption("--conversation <id>", "external conversation id, such as a Telegram chat id").option("--default", "also make this the default session for the conversation").option("-c, --config <path>", "config path", defaultConfigPath()).option("--state <path>", "state path", defaultStatePath()).option("--json", "output JSON").action(attachSessionAction);
|
|
8040
|
+
sessions2.command("use").argument("<id>").description("Set the active session for a channel conversation").requiredOption("--channel <id>", "channel id").requiredOption("--conversation <id>", "external conversation id, such as a Telegram chat id").option("-c, --config <path>", "config path", defaultConfigPath()).option("--state <path>", "state path", defaultStatePath()).option("--json", "output JSON").action(async (id, options) => attachSessionAction(id, { ...options, default: true }));
|
|
8041
|
+
sessions2.command("detach").description("Detach the active session from a channel conversation").requiredOption("--channel <id>", "channel id").requiredOption("--conversation <id>", "external conversation id, such as a Telegram chat id").option("-c, --config <path>", "config path", defaultConfigPath()).option("--state <path>", "state path", defaultStatePath()).option("--json", "output JSON").action(async (options) => {
|
|
8042
|
+
const config2 = await loadConfig(options.config);
|
|
8043
|
+
const state2 = await loadState(options.state);
|
|
8044
|
+
const binding = detachBridgeBinding(config2, state2, options.channel, options.conversation);
|
|
8045
|
+
await saveState(state2, options.state);
|
|
8046
|
+
options.json ? asJson(binding || null) : console.log(binding ? `detached ${binding.id}` : "No binding.");
|
|
8047
|
+
});
|
|
8048
|
+
for (const status of ["pause", "resume", "close"]) {
|
|
8049
|
+
const nextStatus = status === "pause" ? "paused" : status === "resume" ? "active" : "closed";
|
|
8050
|
+
sessions2.command(status).argument("<id>").description(`${status} a bridge session`).option("--state <path>", "state path", defaultStatePath()).option("--json", "output JSON").action(async (id, options) => {
|
|
8051
|
+
const state2 = await loadState(options.state);
|
|
8052
|
+
const session = updateBridgeSessionStatus(state2, id, nextStatus);
|
|
8053
|
+
await saveState(state2, options.state);
|
|
8054
|
+
options.json ? asJson(session) : console.log(`${session.id} ${session.status}`);
|
|
8055
|
+
});
|
|
8056
|
+
}
|
|
8057
|
+
sessions2.command("send").argument("<id>").argument("<text...>").description("Send one message directly to a bridge session").option("-c, --config <path>", "config path", defaultConfigPath()).option("--state <path>", "state path", defaultStatePath()).option("--json", "output JSON").action(async (id, textParts, options) => {
|
|
8058
|
+
const config2 = await loadConfig(options.config);
|
|
8059
|
+
const state2 = await loadState(options.state);
|
|
8060
|
+
const message = {
|
|
8061
|
+
id: `cli:${Date.now()}`,
|
|
8062
|
+
channelId: "cli",
|
|
8063
|
+
text: textParts.join(" "),
|
|
8064
|
+
receivedAt: new Date().toISOString()
|
|
8065
|
+
};
|
|
8066
|
+
const result = await sendBridgeSessionMessage(config2, state2, id, message, { writeConsole: false });
|
|
8067
|
+
await saveState(state2, options.state);
|
|
8068
|
+
if (options.json)
|
|
8069
|
+
asJson(result);
|
|
8070
|
+
else
|
|
8071
|
+
process.stdout.write(result.agent?.stdout || result.agent?.stderr || result.message || "");
|
|
8072
|
+
process.exitCode = result.agent?.exitCode ?? 0;
|
|
8073
|
+
});
|
|
8074
|
+
sessions2.command("route-message").description("Route one synthetic message through session bindings").requiredOption("--channel <id>", "source channel id").requiredOption("--text <text>", "message text").option("--chat-id <id>", "chat id").option("--from <from>", "sender").option("--fallback-routes", "fall back to compatibility routes when no session is bound").option("-c, --config <path>", "config path", defaultConfigPath()).option("--state <path>", "state path", defaultStatePath()).option("--json", "output JSON").action(async (options) => {
|
|
8075
|
+
const config2 = await loadConfig(options.config);
|
|
8076
|
+
const state2 = await loadState(options.state);
|
|
8077
|
+
const result = await dispatchMessageWithSessions(config2, state2, {
|
|
8078
|
+
id: `cli:${Date.now()}`,
|
|
8079
|
+
channelId: options.channel,
|
|
8080
|
+
text: options.text,
|
|
8081
|
+
chatId: options.chatId,
|
|
8082
|
+
from: options.from,
|
|
8083
|
+
receivedAt: new Date().toISOString()
|
|
8084
|
+
}, {
|
|
8085
|
+
writeConsole: options.json ? false : undefined,
|
|
8086
|
+
fallbackToRoutes: Boolean(options.fallbackRoutes),
|
|
8087
|
+
persistState: async (nextState) => saveState(nextState, options.state)
|
|
8088
|
+
});
|
|
8089
|
+
await saveState(state2, options.state);
|
|
8090
|
+
options.json ? asJson(result) : printList(result.session ? [result.session] : result.routes || []);
|
|
8091
|
+
});
|
|
7353
8092
|
program2.command("send").argument("<channel>").argument("[chatId]").argument("[text...]").description("Send a message through a channel").option("-c, --config <path>", "config path", defaultConfigPath()).option("--json", "output JSON").action(async (channelId, chatId, textParts, options) => {
|
|
7354
8093
|
const config2 = await loadConfig(options.config);
|
|
7355
8094
|
const channel = config2.channels[channelId];
|
|
7356
8095
|
if (!channel)
|
|
7357
8096
|
throw new Error(`Channel not found: ${channelId}`);
|
|
7358
8097
|
let targetChat = chatId;
|
|
7359
|
-
|
|
7360
|
-
|
|
8098
|
+
const textArgParts = textParts;
|
|
8099
|
+
let text = textArgParts.join(" ");
|
|
8100
|
+
if (channel.kind === "console" && !text && targetChat) {
|
|
7361
8101
|
text = targetChat;
|
|
7362
8102
|
targetChat = undefined;
|
|
7363
8103
|
}
|
|
@@ -7379,6 +8119,26 @@ program2.command("send").argument("<channel>").argument("[chatId]").argument("[t
|
|
|
7379
8119
|
console.log(text);
|
|
7380
8120
|
return;
|
|
7381
8121
|
}
|
|
8122
|
+
if (channel.kind === "imessage") {
|
|
8123
|
+
if (!text && targetChat) {
|
|
8124
|
+
const looksLikeHandle = targetChat.startsWith("+") || targetChat.includes("@") || imessageHandleAllowed(channel, targetChat);
|
|
8125
|
+
if (looksLikeHandle)
|
|
8126
|
+
throw new Error("message text is required when an iMessage handle is provided");
|
|
8127
|
+
text = targetChat;
|
|
8128
|
+
targetChat = undefined;
|
|
8129
|
+
}
|
|
8130
|
+
targetChat = targetChat || channel.defaultHandle;
|
|
8131
|
+
if (!targetChat)
|
|
8132
|
+
throw new Error("chatId/handle argument or channel.defaultHandle is required");
|
|
8133
|
+
if (!text)
|
|
8134
|
+
throw new Error("message text is required");
|
|
8135
|
+
if (!imessageHandleAllowed(channel, targetChat)) {
|
|
8136
|
+
throw new Error(`iMessage handle ${targetChat} is not allowed for channel ${channel.id}`);
|
|
8137
|
+
}
|
|
8138
|
+
const result = await sendIMessage(channel, targetChat, text);
|
|
8139
|
+
options.json ? asJson(result) : console.log("sent");
|
|
8140
|
+
return;
|
|
8141
|
+
}
|
|
7382
8142
|
throw new Error(`Sending through ${channel.kind} is not implemented yet`);
|
|
7383
8143
|
});
|
|
7384
8144
|
program2.command("ask").argument("<agent>").argument("<text...>").description("Run one agent directly").option("-c, --config <path>", "config path", defaultConfigPath()).option("--json", "output JSON").action(async (agentId, textParts, options) => {
|