talon-agent 5.14.0 → 5.18.2
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/LICENSE +202 -21
- package/LICENSE-MIT +21 -0
- package/NOTICE +16 -0
- package/README.md +14 -7
- package/package.json +6 -4
- package/prompts/system/heartbeat-agent.md +1 -1
- package/src/backend/agy/factory.ts +3 -0
- package/src/backend/agy/mcp/config.ts +14 -2
- package/src/backend/claude-sdk/factory.ts +3 -0
- package/src/backend/claude-sdk/options.ts +24 -3
- package/src/backend/codex/factory.ts +3 -0
- package/src/backend/codex/init.ts +4 -0
- package/src/backend/codex/mcp-config.ts +10 -0
- package/src/backend/codex/oauth-incompat.ts +1 -1
- package/src/backend/codex/token-usage.ts +2 -2
- package/src/backend/openai-agents/factory.ts +3 -0
- package/src/backend/openai-agents/mcp-pool.ts +4 -0
- package/src/backend/remote-server/factory.ts +3 -0
- package/src/backend/remote-server/mcp.ts +3 -0
- package/src/backend/runtime/prompt/prompt-format.ts +3 -3
- package/src/bootstrap.ts +8 -0
- package/src/cli/commands/backup.ts +61 -5
- package/src/cli/commands/mesh.ts +133 -0
- package/src/cli/config.ts +3 -1
- package/src/cli/daemon-api.ts +22 -0
- package/src/cli/index.ts +6 -0
- package/src/cli/install-sources.ts +40 -5
- package/src/cli/plugin.ts +10 -0
- package/src/cli/setup.ts +45 -4
- package/src/cli/skill.ts +3 -0
- package/src/core/agent-runtime/backend-registry.ts +16 -0
- package/src/core/backup/archive/crypt.ts +429 -0
- package/src/core/backup/archive/manifest-auth.ts +98 -0
- package/src/core/backup/passphrase.ts +130 -0
- package/src/core/backup/plan.ts +66 -13
- package/src/core/backup/restore-guard.ts +101 -0
- package/src/core/backup/restore.ts +249 -32
- package/src/core/backup/snapshot.ts +418 -60
- package/src/core/backup/sources/plugins.ts +223 -0
- package/src/core/backup/sources/relocate.ts +136 -0
- package/src/core/backup/sources/sessions.ts +198 -0
- package/src/core/backup/store.ts +3 -1
- package/src/core/backup/types.ts +66 -0
- package/src/core/backup/upload.ts +66 -6
- package/src/core/config/index.ts +140 -8
- package/src/core/daemon/control.ts +9 -0
- package/src/core/daemon/discovery.ts +7 -0
- package/src/core/engine/backend-router/headroom.ts +29 -5
- package/src/core/engine/backend-router/usage.ts +6 -2
- package/src/core/engine/gateway-actions/fetch-url/guard.ts +201 -0
- package/src/core/engine/gateway-actions/{fetch-url.ts → fetch-url/index.ts} +60 -32
- package/src/core/engine/gateway-actions/index.ts +4 -2
- package/src/core/engine/gateway-actions/native/index.ts +24 -0
- package/src/core/engine/gateway-actions/whatsapp-account.ts +1 -1
- package/src/core/engine/gateway-auth.ts +164 -0
- package/src/core/engine/gateway-routes.ts +100 -5
- package/src/core/engine/gateway.ts +12 -4
- package/src/core/mcp-hub/guest-scope.ts +170 -29
- package/src/core/mcp-hub/index.ts +33 -16
- package/src/core/mcp-hub/talon-server.ts +71 -14
- package/src/core/mesh/credentials/admin.ts +146 -0
- package/src/core/mesh/credentials/index.ts +19 -0
- package/src/core/mesh/credentials/store.ts +443 -0
- package/src/core/mesh/credentials/token.ts +45 -0
- package/src/core/mesh/credentials/types.ts +83 -0
- package/src/core/mesh/devices/service.ts +58 -6
- package/src/core/mesh/links/bridge-links.ts +46 -4
- package/src/core/mesh/links/node-binaries.ts +1 -1
- package/src/core/mesh/links/node-provision.ts +8 -1
- package/src/core/models/active-model.ts +1 -1
- package/src/core/plugin/loader.ts +4 -0
- package/src/core/plugin/mcp.ts +4 -0
- package/src/core/tools/bridge.ts +2 -1
- package/src/core/types.ts +13 -0
- package/src/core/weaver/weaver.ts +47 -15
- package/src/frontend/discord/callbacks/components/agent-buttons.ts +1 -0
- package/src/frontend/discord/handlers/delivery.ts +3 -0
- package/src/frontend/discord/handlers/queue.ts +1 -0
- package/src/frontend/native/bridge/auth-guard.ts +277 -0
- package/src/frontend/native/bridge/auth.ts +84 -1
- package/src/frontend/native/bridge/credentials/claims.ts +51 -0
- package/src/frontend/native/bridge/credentials/principal.ts +200 -0
- package/src/frontend/native/bridge/credentials/upgrade.ts +129 -0
- package/src/frontend/native/bridge/routes/auth.ts +37 -0
- package/src/frontend/native/bridge/routes/chats.ts +20 -2
- package/src/frontend/native/bridge/routes/host.ts +11 -1
- package/src/frontend/native/bridge/routes/index.ts +2 -0
- package/src/frontend/native/bridge/routes/mesh.ts +72 -15
- package/src/frontend/native/bridge/routes/table.ts +73 -51
- package/src/frontend/native/bridge/server.ts +266 -83
- package/src/frontend/native/index.ts +54 -3
- package/src/frontend/native/turn/turn.ts +2 -0
- package/src/frontend/teams/turn.ts +1 -0
- package/src/frontend/telegram/actions/outgoing-log.ts +70 -0
- package/src/frontend/telegram/actions/send.ts +4 -0
- package/src/frontend/telegram/admin.ts +20 -0
- package/src/frontend/telegram/commands/admin.ts +1 -1
- package/src/frontend/telegram/commands/state.ts +7 -9
- package/src/frontend/telegram/handlers/access.ts +31 -10
- package/src/frontend/telegram/handlers/delivery.ts +14 -1
- package/src/frontend/telegram/handlers/group-access.ts +50 -0
- package/src/frontend/telegram/handlers/messages.ts +1 -0
- package/src/frontend/telegram/handlers/queue.ts +14 -0
- package/src/frontend/telegram/handlers/state.ts +2 -2
- package/src/frontend/telegram/index.ts +32 -9
- package/src/frontend/telegram/middleware.ts +2 -2
- package/src/frontend/telegram/polling/poll-deadline.ts +52 -0
- package/src/frontend/telegram/{stale-command.ts → polling/stale-command.ts} +1 -1
- package/src/frontend/telegram/{update-offset.ts → polling/update-offset.ts} +1 -1
- package/src/frontend/telegram/userbot.ts +103 -10
- package/src/frontend/terminal/index.ts +8 -1
- package/src/frontend/whatsapp/commands.ts +3 -3
- package/src/frontend/whatsapp/messages/inbound.ts +1 -0
- package/src/plugins/playwright/index.ts +1 -1
- package/src/storage/backup/index.ts +1 -1
- package/src/storage/db.ts +23 -0
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import { TelegramClient, Api } from "telegram";
|
|
12
12
|
import { StringSession } from "telegram/sessions/index.js";
|
|
13
|
+
import { outgoingText } from "./actions/outgoing-log.js";
|
|
13
14
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
14
15
|
import { resolve, dirname } from "node:path";
|
|
15
16
|
import writeFileAtomic from "write-file-atomic";
|
|
@@ -127,6 +128,29 @@ export async function disconnectUserClient(): Promise<void> {
|
|
|
127
128
|
// ── Connection monitoring ────────────────────────────────────────────────────
|
|
128
129
|
|
|
129
130
|
const CHECK_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
131
|
+
/**
|
|
132
|
+
* Upper bound for any single network step in the monitor. GramJS's
|
|
133
|
+
* `connect()` can hang forever on a half-open socket; without a bound the
|
|
134
|
+
* `reconnecting` latch never clears and every later tick bails out, leaving
|
|
135
|
+
* the userbot dead until the daemon restarts.
|
|
136
|
+
*/
|
|
137
|
+
const RECONNECT_STEP_TIMEOUT_MS = 30_000;
|
|
138
|
+
|
|
139
|
+
/** Reject if `p` doesn't settle within `ms`. Exported for tests. */
|
|
140
|
+
export function withTimeout<T>(
|
|
141
|
+
p: Promise<T>,
|
|
142
|
+
ms: number,
|
|
143
|
+
what: string,
|
|
144
|
+
): Promise<T> {
|
|
145
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
146
|
+
const timeout = new Promise<never>((_, reject) => {
|
|
147
|
+
timer = setTimeout(
|
|
148
|
+
() => reject(new Error(`${what} timed out after ${ms}ms`)),
|
|
149
|
+
ms,
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
return Promise.race([p, timeout]).finally(() => clearTimeout(timer));
|
|
153
|
+
}
|
|
130
154
|
|
|
131
155
|
let reconnecting = false;
|
|
132
156
|
// Bumped by stopConnectionMonitor so a tick whose awaits straddle a shutdown
|
|
@@ -150,7 +174,11 @@ function startConnectionMonitor(): void {
|
|
|
150
174
|
if (client.connected) {
|
|
151
175
|
try {
|
|
152
176
|
// A full API round-trip; throws if the socket is silently dead.
|
|
153
|
-
await
|
|
177
|
+
await withTimeout(
|
|
178
|
+
client.getMe(),
|
|
179
|
+
RECONNECT_STEP_TIMEOUT_MS,
|
|
180
|
+
"liveness probe",
|
|
181
|
+
);
|
|
154
182
|
return; // genuinely alive
|
|
155
183
|
} catch {
|
|
156
184
|
if (gen !== monitorGeneration) return; // monitor stopped mid-probe
|
|
@@ -162,8 +190,18 @@ function startConnectionMonitor(): void {
|
|
|
162
190
|
reconnecting = true;
|
|
163
191
|
logWarn("userbot", "Connection lost, attempting reconnect...");
|
|
164
192
|
try {
|
|
165
|
-
await
|
|
166
|
-
|
|
193
|
+
await withTimeout(
|
|
194
|
+
client.connect(),
|
|
195
|
+
RECONNECT_STEP_TIMEOUT_MS,
|
|
196
|
+
"reconnect",
|
|
197
|
+
);
|
|
198
|
+
if (
|
|
199
|
+
await withTimeout(
|
|
200
|
+
client.isUserAuthorized(),
|
|
201
|
+
RECONNECT_STEP_TIMEOUT_MS,
|
|
202
|
+
"auth check",
|
|
203
|
+
)
|
|
204
|
+
) {
|
|
167
205
|
log("userbot", "Reconnected successfully.");
|
|
168
206
|
} else {
|
|
169
207
|
logWarn("userbot", "Reconnected but not authorized.");
|
|
@@ -174,7 +212,11 @@ function startConnectionMonitor(): void {
|
|
|
174
212
|
// Try a full re-init on next check
|
|
175
213
|
if (storedApiId && storedApiHash) {
|
|
176
214
|
try {
|
|
215
|
+
// Drop the wedged client without awaiting it — its socket may be
|
|
216
|
+
// the thing that's hung.
|
|
217
|
+
const stale = client;
|
|
177
218
|
client = null;
|
|
219
|
+
void stale?.disconnect().catch(() => {});
|
|
178
220
|
let sessionString = "";
|
|
179
221
|
if (existsSync(SESSION_FILE)) {
|
|
180
222
|
sessionString = readFileSync(SESSION_FILE, "utf-8").trim();
|
|
@@ -183,8 +225,18 @@ function startConnectionMonitor(): void {
|
|
|
183
225
|
client = new TelegramClient(session, storedApiId, storedApiHash, {
|
|
184
226
|
connectionRetries: 5,
|
|
185
227
|
});
|
|
186
|
-
await
|
|
187
|
-
|
|
228
|
+
await withTimeout(
|
|
229
|
+
client.connect(),
|
|
230
|
+
RECONNECT_STEP_TIMEOUT_MS,
|
|
231
|
+
"re-init connect",
|
|
232
|
+
);
|
|
233
|
+
if (
|
|
234
|
+
await withTimeout(
|
|
235
|
+
client.isUserAuthorized(),
|
|
236
|
+
RECONNECT_STEP_TIMEOUT_MS,
|
|
237
|
+
"re-init auth check",
|
|
238
|
+
)
|
|
239
|
+
) {
|
|
188
240
|
log("userbot", "Full re-init reconnect succeeded.");
|
|
189
241
|
}
|
|
190
242
|
} catch (retryErr) {
|
|
@@ -230,7 +282,7 @@ export async function searchMessages(params: {
|
|
|
230
282
|
m.sender && "firstName" in m.sender
|
|
231
283
|
? [m.sender.firstName, m.sender.lastName].filter(Boolean).join(" ")
|
|
232
284
|
: "Unknown";
|
|
233
|
-
return `[msg:${m.id} ${date}] ${sender}: ${m
|
|
285
|
+
return `[msg:${m.id} ${date}] ${sender}: ${messageBody(m)}`;
|
|
234
286
|
})
|
|
235
287
|
.join("\n");
|
|
236
288
|
} catch (err) {
|
|
@@ -238,6 +290,47 @@ export async function searchMessages(params: {
|
|
|
238
290
|
}
|
|
239
291
|
}
|
|
240
292
|
|
|
293
|
+
/**
|
|
294
|
+
* Render one message's body for the history readers.
|
|
295
|
+
*
|
|
296
|
+
* Rich Messages (Bot API 10.2) reach this client as `messageMediaUnsupported`
|
|
297
|
+
* with no text, because gramjs negotiates an older MTProto layer — so every
|
|
298
|
+
* message the bot itself sent reads back as "(media)" and the agent cannot see
|
|
299
|
+
* its own half of the conversation. When the text is one of ours, splice it
|
|
300
|
+
* back in from the outgoing log; when it is not, say what happened rather than
|
|
301
|
+
* calling it media.
|
|
302
|
+
*/
|
|
303
|
+
export function messageBody(m: {
|
|
304
|
+
id: number;
|
|
305
|
+
text?: string;
|
|
306
|
+
media?: { className: string };
|
|
307
|
+
}): string {
|
|
308
|
+
if (m.text) return m.text;
|
|
309
|
+
if (m.media?.className === "MessageMediaUnsupported") {
|
|
310
|
+
const own = outgoingText(m.id);
|
|
311
|
+
if (own) return own;
|
|
312
|
+
return "(rich message — not renderable by this client)";
|
|
313
|
+
}
|
|
314
|
+
return "(media)";
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Media tag for the history line, suppressed for rich messages we resolved. */
|
|
318
|
+
export function mediaTagFor(m: {
|
|
319
|
+
id: number;
|
|
320
|
+
text?: string;
|
|
321
|
+
media?: { className: string };
|
|
322
|
+
}): string {
|
|
323
|
+
if (!m.media) return "";
|
|
324
|
+
if (
|
|
325
|
+
m.media.className === "MessageMediaUnsupported" &&
|
|
326
|
+
!m.text &&
|
|
327
|
+
outgoingText(m.id)
|
|
328
|
+
) {
|
|
329
|
+
return "";
|
|
330
|
+
}
|
|
331
|
+
return ` [${m.media.className}]`;
|
|
332
|
+
}
|
|
333
|
+
|
|
241
334
|
/** Get message history from a chat. Supports going back in time via offsetDate or offsetId. */
|
|
242
335
|
export async function getHistory(params: {
|
|
243
336
|
chatId: number | string;
|
|
@@ -278,8 +371,8 @@ export async function getHistory(params: {
|
|
|
278
371
|
const replyTag = m.replyTo?.replyToMsgId
|
|
279
372
|
? ` (reply to msg:${m.replyTo.replyToMsgId})`
|
|
280
373
|
: "";
|
|
281
|
-
const mediaTag = m
|
|
282
|
-
return `[msg:${m.id} ${date}] ${sender}${replyTag}${mediaTag}: ${m
|
|
374
|
+
const mediaTag = mediaTagFor(m);
|
|
375
|
+
return `[msg:${m.id} ${date}] ${sender}${replyTag}${mediaTag}: ${messageBody(m)}`;
|
|
283
376
|
})
|
|
284
377
|
.join("\n");
|
|
285
378
|
} catch (err) {
|
|
@@ -415,8 +508,8 @@ export async function getMessage(params: {
|
|
|
415
508
|
const replyTag = m.replyTo?.replyToMsgId
|
|
416
509
|
? `\nReply to: msg:${m.replyTo.replyToMsgId}`
|
|
417
510
|
: "";
|
|
418
|
-
const mediaTag = m
|
|
419
|
-
return `[msg:${m.id} ${date}] ${sender}${replyTag}${mediaTag}\n${m
|
|
511
|
+
const mediaTag = mediaTagFor(m) ? `\nMedia: ${m.media?.className}` : "";
|
|
512
|
+
return `[msg:${m.id} ${date}] ${sender}${replyTag}${mediaTag}\n${messageBody(m)}`;
|
|
420
513
|
} catch (err) {
|
|
421
514
|
return `Failed: ${err instanceof Error ? err.message : err}`;
|
|
422
515
|
}
|
|
@@ -33,9 +33,16 @@ import {
|
|
|
33
33
|
type CommandContext,
|
|
34
34
|
} from "./commands.js";
|
|
35
35
|
import { buildCacheDisplay } from "../presentation/status-context.js";
|
|
36
|
+
import { LOCAL_OPERATOR_SENDER } from "../../core/mcp-hub/guest-scope.js";
|
|
36
37
|
|
|
37
38
|
// ── State ────────────────────────────────────────────────────────────────────
|
|
38
39
|
|
|
40
|
+
/** The terminal is driven by the operator at the keyboard. */
|
|
41
|
+
const TERMINAL_SENDER = {
|
|
42
|
+
senderName: "User",
|
|
43
|
+
senderKeys: [LOCAL_OPERATOR_SENDER],
|
|
44
|
+
} as const;
|
|
45
|
+
|
|
39
46
|
let terminalChatId = "";
|
|
40
47
|
let terminalNumericId = 0;
|
|
41
48
|
|
|
@@ -224,7 +231,7 @@ export function createTerminalFrontend(
|
|
|
224
231
|
chatId: terminalChatId,
|
|
225
232
|
numericChatId: terminalNumericId,
|
|
226
233
|
prompt: text,
|
|
227
|
-
|
|
234
|
+
...TERMINAL_SENDER,
|
|
228
235
|
isGroup: false,
|
|
229
236
|
source: "message",
|
|
230
237
|
onEvent: async (event) => {
|
|
@@ -101,14 +101,14 @@ function isMutating(cmd: WhatsAppCommand): boolean {
|
|
|
101
101
|
* May this sender change the chat's settings? A DM sender already passed
|
|
102
102
|
* the allowlist. In a group, the same allowlist is the admin rule — the
|
|
103
103
|
* people in `allowedJids` are the operator's, everyone else may read
|
|
104
|
-
* settings but not change them. With no allowlist at all
|
|
105
|
-
*
|
|
104
|
+
* settings but not change them. With no allowlist at all nobody in a group
|
|
105
|
+
* may change them: an empty list never means everyone.
|
|
106
106
|
*/
|
|
107
107
|
export function canChangeSettings(
|
|
108
108
|
runtime: WhatsAppRuntime,
|
|
109
109
|
inbound: Pick<CommandInbound, "isGroup" | "identity">,
|
|
110
110
|
): boolean {
|
|
111
|
-
if (!inbound.isGroup
|
|
111
|
+
if (!inbound.isGroup) return true;
|
|
112
112
|
return identityAllowed(inbound.identity, runtime.allowedDms);
|
|
113
113
|
}
|
|
114
114
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* "playwright": {
|
|
16
16
|
* "enabled": true,
|
|
17
17
|
* "browser": "firefox",
|
|
18
|
-
* "endpointFile": "/home/
|
|
18
|
+
* "endpointFile": "/home/ada/camoufox-endpoint.txt"
|
|
19
19
|
* }
|
|
20
20
|
*
|
|
21
21
|
* VERSION COUPLING (endpoint mode): the WebSocket handshake requires the
|
|
@@ -22,7 +22,7 @@ import * as repo from "./repo.js";
|
|
|
22
22
|
* handle stays inside storage/ (`db-handle-stays-in-storage`), and a
|
|
23
23
|
* snapshot of the database is a storage concern with a storage API.
|
|
24
24
|
*/
|
|
25
|
-
export { snapshotDatabase } from "../db.js";
|
|
25
|
+
export { snapshotDatabase, snapshotSqliteFile } from "../db.js";
|
|
26
26
|
|
|
27
27
|
export type { BackupRecord, BackupRemoteRecord } from "./repo.js";
|
|
28
28
|
import type { BackupRecord, BackupRemoteRecord } from "./repo.js";
|
package/src/storage/db.ts
CHANGED
|
@@ -58,6 +58,11 @@ const sqliteModule = (await import(
|
|
|
58
58
|
IS_BUN ? "bun:sqlite" : "node:sqlite"
|
|
59
59
|
)) as Record<string, new (path: string) => SqlDatabase>;
|
|
60
60
|
const Database = IS_BUN ? sqliteModule.Database : sqliteModule.DatabaseSync;
|
|
61
|
+
/** The same constructor, with the options bag both runtimes accept. */
|
|
62
|
+
const ReadOnlyDatabase = Database as unknown as new (
|
|
63
|
+
path: string,
|
|
64
|
+
options: Record<string, boolean>,
|
|
65
|
+
) => SqlDatabase;
|
|
61
66
|
|
|
62
67
|
let db: SqlDatabase | null = null;
|
|
63
68
|
|
|
@@ -236,6 +241,24 @@ export function snapshotDatabase(destPath: string): void {
|
|
|
236
241
|
database.prepare(dbSql.vacuumInto).run(destPath);
|
|
237
242
|
}
|
|
238
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Consistent copy of ANOTHER program's SQLite file (a backend's session
|
|
246
|
+
* store) — the same `VACUUM INTO` as {@link snapshotDatabase}, through a
|
|
247
|
+
* read-only handle so the owner's database and WAL are never written.
|
|
248
|
+
*/
|
|
249
|
+
export function snapshotSqliteFile(sourcePath: string, destPath: string): void {
|
|
250
|
+
const foreign = new ReadOnlyDatabase(
|
|
251
|
+
sourcePath,
|
|
252
|
+
IS_BUN ? { readonly: true } : { readOnly: true },
|
|
253
|
+
);
|
|
254
|
+
try {
|
|
255
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
256
|
+
foreign.prepare(dbSql.vacuumInto).run(destPath);
|
|
257
|
+
} finally {
|
|
258
|
+
foreign.close();
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
239
262
|
export function closeDatabase(): void {
|
|
240
263
|
if (!db) return;
|
|
241
264
|
try {
|