talon-agent 4.3.1 → 4.3.3

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.
Files changed (133) hide show
  1. package/package.json +1 -1
  2. package/src/app.ts +1 -1
  3. package/src/backend/claude-sdk/doctor.ts +1 -1
  4. package/src/backend/codex/doctor.ts +1 -1
  5. package/src/backend/openai-agents/doctor.ts +1 -1
  6. package/src/bootstrap.ts +3 -3
  7. package/src/cli/doctor.ts +1 -1
  8. package/src/cli/skill.ts +2 -2
  9. package/src/core/agent-runtime/backend-registry.ts +1 -1
  10. package/src/core/auth/expiry-monitor.ts +1 -1
  11. package/src/core/background/cron-spec.ts +1 -1
  12. package/src/core/background/cron.ts +1 -1
  13. package/src/core/background/heartbeat/agent.ts +1 -1
  14. package/src/core/background/triggers/command.ts +1 -1
  15. package/src/core/background/triggers/exit.ts +1 -1
  16. package/src/core/background/triggers/output.ts +1 -1
  17. package/src/core/background/triggers/resume.ts +1 -1
  18. package/src/core/background/triggers/spawn.ts +1 -1
  19. package/src/core/{doctor.ts → doctor/index.ts} +11 -11
  20. package/src/core/engine/gateway-actions/cron.ts +2 -2
  21. package/src/core/engine/gateway-actions/goals.ts +2 -2
  22. package/src/core/engine/gateway-actions/scripts.ts +1 -1
  23. package/src/core/engine/gateway-actions/skills.ts +1 -1
  24. package/src/core/engine/gateway-actions/triggers.ts +2 -2
  25. package/src/core/engine/gateway-actions/{shared.ts → validation.ts} +2 -2
  26. package/src/core/{notify.ts → frontend-runtime/admin-notify.ts} +1 -1
  27. package/src/core/plugin/native-runtimes.ts +1 -1
  28. package/src/core/prompt/assemble.ts +2 -2
  29. package/src/core/scripts/runner.ts +1 -1
  30. package/src/core/soul/forgetting.ts +2 -2
  31. package/src/core/soul/salience.ts +8 -4
  32. package/src/frontend/discord/actions/{shared.ts → buttons.ts} +3 -56
  33. package/src/frontend/discord/actions/channels.ts +34 -0
  34. package/src/frontend/discord/actions/chat-info.ts +1 -1
  35. package/src/frontend/discord/actions/index.ts +1 -1
  36. package/src/frontend/discord/actions/media.ts +1 -1
  37. package/src/frontend/discord/actions/messaging.ts +4 -2
  38. package/src/frontend/discord/actions/try-action.ts +24 -0
  39. package/src/frontend/discord/admin.ts +2 -2
  40. package/src/frontend/discord/callbacks/components/settings.ts +1 -1
  41. package/src/frontend/discord/callbacks/modals.ts +1 -1
  42. package/src/frontend/discord/commands/admin.ts +3 -3
  43. package/src/frontend/discord/commands/info.ts +2 -2
  44. package/src/frontend/discord/commands/{shared.ts → interaction.ts} +2 -1
  45. package/src/frontend/discord/commands/router.ts +1 -1
  46. package/src/frontend/discord/commands/session.ts +2 -2
  47. package/src/frontend/discord/commands/settings.ts +2 -2
  48. package/src/frontend/discord/{helpers.ts → render.ts} +3 -2
  49. package/src/frontend/native/extensions.ts +1 -1
  50. package/src/frontend/teams/chat-discovery.ts +69 -0
  51. package/src/frontend/teams/commands.ts +141 -0
  52. package/src/frontend/teams/index.ts +24 -350
  53. package/src/frontend/teams/outbound.ts +44 -0
  54. package/src/frontend/teams/poll.ts +101 -0
  55. package/src/frontend/teams/runtime.ts +51 -0
  56. package/src/frontend/teams/turn.ts +97 -0
  57. package/src/frontend/telegram/actions/index.ts +1 -1
  58. package/src/frontend/telegram/actions/messaging.ts +1 -1
  59. package/src/frontend/telegram/actions/moderation/chat.ts +63 -0
  60. package/src/frontend/telegram/actions/moderation/index.ts +74 -0
  61. package/src/frontend/telegram/actions/moderation/members.ts +77 -0
  62. package/src/frontend/telegram/actions/moderation/permissions.ts +69 -0
  63. package/src/frontend/telegram/actions/moderation/topics.ts +48 -0
  64. package/src/frontend/telegram/actions/moderation/types.ts +16 -0
  65. package/src/frontend/telegram/actions/shared.ts +4 -4
  66. package/src/frontend/telegram/admin/background.ts +66 -0
  67. package/src/frontend/telegram/admin/chunked-reply.ts +20 -0
  68. package/src/frontend/telegram/admin/health.ts +81 -0
  69. package/src/frontend/telegram/admin/sessions.ts +105 -0
  70. package/src/frontend/telegram/admin.ts +56 -261
  71. package/src/frontend/telegram/callbacks/model/backend.ts +156 -0
  72. package/src/frontend/telegram/callbacks/model/control.ts +29 -0
  73. package/src/frontend/telegram/callbacks/model/nav.ts +54 -0
  74. package/src/frontend/telegram/callbacks/model/select.ts +100 -0
  75. package/src/frontend/telegram/callbacks/model/types.ts +39 -0
  76. package/src/frontend/telegram/callbacks/model/views.ts +116 -0
  77. package/src/frontend/telegram/callbacks/model.ts +61 -331
  78. package/src/frontend/telegram/commands/admin.ts +108 -77
  79. package/src/frontend/telegram/commands/settings.ts +65 -43
  80. package/src/frontend/telegram/commands/whatsapp-pairing.ts +1 -1
  81. package/src/frontend/telegram/helpers/diagnostics.ts +1 -1
  82. package/src/frontend/telegram/middleware.ts +166 -201
  83. package/src/frontend/telegram/sticker-library.ts +2 -2
  84. package/src/frontend/terminal/builtins/context.ts +184 -0
  85. package/src/frontend/terminal/builtins/help.ts +35 -0
  86. package/src/frontend/terminal/builtins/index.ts +23 -0
  87. package/src/frontend/terminal/builtins/model.ts +198 -0
  88. package/src/frontend/terminal/builtins/session.ts +101 -0
  89. package/src/frontend/terminal/builtins/status.ts +202 -0
  90. package/src/frontend/terminal/command-registry.ts +80 -0
  91. package/src/frontend/terminal/commands.ts +11 -674
  92. package/src/frontend/terminal/input/history.ts +64 -0
  93. package/src/frontend/terminal/input/keys.ts +143 -0
  94. package/src/frontend/terminal/input/parts.ts +33 -0
  95. package/src/frontend/terminal/input/paste.ts +53 -0
  96. package/src/frontend/terminal/input/render.ts +42 -0
  97. package/src/frontend/terminal/input/state.ts +60 -0
  98. package/src/frontend/terminal/input.ts +27 -342
  99. package/src/frontend/whatsapp/actions/account.ts +3 -6
  100. package/src/frontend/whatsapp/actions/chat-info.ts +2 -1
  101. package/src/frontend/whatsapp/actions/coerce.ts +30 -0
  102. package/src/frontend/whatsapp/actions/history.ts +1 -1
  103. package/src/frontend/whatsapp/actions/media-upload.ts +82 -0
  104. package/src/frontend/whatsapp/actions/media.ts +3 -8
  105. package/src/frontend/whatsapp/actions/messaging.ts +2 -7
  106. package/src/frontend/whatsapp/actions/moderation.ts +2 -1
  107. package/src/frontend/whatsapp/actions/{shared.ts → send.ts} +4 -124
  108. package/src/frontend/whatsapp/actions/try-action.ts +24 -0
  109. package/src/frontend/whatsapp/commands.ts +1 -1
  110. package/src/frontend/whatsapp/connection.ts +1 -1
  111. package/src/frontend/whatsapp/inbound.ts +1 -1
  112. package/src/frontend/whatsapp/index.ts +2 -2
  113. package/src/frontend/whatsapp/pairing-service.ts +1 -1
  114. package/src/plugins/github/provision.ts +1 -1
  115. package/src/plugins/mempalace/provision.ts +1 -1
  116. package/src/plugins/playwright/provision.ts +1 -1
  117. package/src/storage/repositories/cron-repo.ts +1 -1
  118. package/src/storage/repositories/goals-repo.ts +1 -1
  119. package/src/storage/repositories/scripts-repo.ts +1 -1
  120. package/src/storage/repositories/triggers-repo.ts +1 -1
  121. package/src/storage/sql/schema.sql +1 -1
  122. package/src/storage/sql/statements.generated.ts +2 -2
  123. package/src/storage/sql/triggers.sql +1 -1
  124. package/src/frontend/telegram/actions/moderation.ts +0 -255
  125. /package/src/core/{doctor-types.ts → doctor/types.ts} +0 -0
  126. /package/src/core/{pairing-broker.ts → frontend-runtime/pairing-broker.ts} +0 -0
  127. /package/src/storage/{cron-store.ts → cron.ts} +0 -0
  128. /package/src/storage/{goal-store.ts → goals.ts} +0 -0
  129. /package/src/storage/{scheduled-store.ts → scheduled.ts} +0 -0
  130. /package/src/storage/{script-store.ts → scripts.ts} +0 -0
  131. /package/src/storage/{skill-store.ts → skills.ts} +0 -0
  132. /package/src/storage/{sticker-store.ts → stickers.ts} +0 -0
  133. /package/src/storage/{trigger-store.ts → triggers.ts} +0 -0
@@ -1,111 +1,17 @@
1
1
  /**
2
- * Shared helpers for the WhatsApp action handlers: uniform error
3
- * capture, media-source resolution, quoted-reply resolution, and the
4
- * JID coercion the member-facing actions need.
2
+ * Outbound WhatsApp sends: quoted-reply resolution, the serialized
3
+ * time-bounded socket queue, and the content/text senders that remember
4
+ * what went out so later tool calls and history can address it.
5
5
  */
6
6
 
7
- import { existsSync, statSync } from "node:fs";
8
- import { basename, extname } from "node:path";
9
7
  import type { AnyMessageContent, WAMessage, WASocket } from "baileys";
10
- import { expandFsPath } from "../../../util/fs-path.js";
11
- import { log, logError } from "../../../util/log.js";
8
+ import { log } from "../../../util/log.js";
12
9
  import type { ActionResult } from "../../../core/types.js";
13
10
  import { toWhatsAppChunks } from "../formatting.js";
14
11
  import { lookupMessage, rememberMessage } from "../message-store.js";
15
12
  import { pushMessage, type HistoryMessage } from "../../../storage/history.js";
16
13
  import type { WhatsAppChatInfo } from "../registry.js";
17
14
 
18
- /** WhatsApp's own ceiling for a media upload. */
19
- const MAX_MEDIA_BYTES = 64 * 1024 * 1024;
20
-
21
- /**
22
- * Run an action, converting a throw into a structured failure. WhatsApp
23
- * errors arrive as Boom objects whose message is the useful part; the
24
- * model gets that text so it can adapt rather than retry blindly.
25
- */
26
- export async function tryAction(
27
- label: string,
28
- fn: () => Promise<ActionResult>,
29
- ): Promise<ActionResult> {
30
- try {
31
- return await fn();
32
- } catch (err) {
33
- const msg = err instanceof Error ? err.message : String(err);
34
- logError("whatsapp", `${label} failed: ${msg}`);
35
- return { ok: false, error: `${label}: ${msg}` };
36
- }
37
- }
38
-
39
- /** Baileys accepts a Buffer, a stream, or `{ url }` for local paths and HTTP. */
40
- export type MediaUpload = { url: string };
41
-
42
- /**
43
- * Resolve one media input to something Baileys can upload. Two sources:
44
- * a public URL (WhatsApp's uploader fetches it) or a workspace file path
45
- * (streamed from disk). `file_id` is a Telegram concept with no WhatsApp
46
- * equivalent — say so instead of failing obscurely.
47
- */
48
- export function resolveMediaUpload(
49
- src: { file_path?: unknown; url?: unknown; file_id?: unknown },
50
- label: string,
51
- ): { media: MediaUpload; fileName: string } | { error: string } {
52
- if (src.url) {
53
- const url = String(src.url);
54
- return {
55
- media: { url },
56
- fileName: basename(new URL(url).pathname) || "file",
57
- };
58
- }
59
- if (src.file_id) {
60
- return {
61
- error:
62
- `${label}: WhatsApp has no file_id — re-send by url (public) or ` +
63
- `file_path (workspace file)`,
64
- };
65
- }
66
- if (!src.file_path) {
67
- return {
68
- error: `${label}: provide file_path (workspace file) or url (public)`,
69
- };
70
- }
71
- const filePath = expandFsPath(String(src.file_path));
72
- if (!existsSync(filePath)) {
73
- return {
74
- error: `File not found: ${filePath} — check the workspace path, or send by url instead`,
75
- };
76
- }
77
- if (statSync(filePath).size > MAX_MEDIA_BYTES) {
78
- return { error: `${label}: file exceeds WhatsApp's 64MB limit` };
79
- }
80
- return { media: { url: filePath }, fileName: basename(filePath) };
81
- }
82
-
83
- /** Extension → mimetype for the document/audio paths that require one. */
84
- const MIME_BY_EXT: Record<string, string> = {
85
- ".pdf": "application/pdf",
86
- ".txt": "text/plain",
87
- ".md": "text/markdown",
88
- ".json": "application/json",
89
- ".csv": "text/csv",
90
- ".zip": "application/zip",
91
- ".png": "image/png",
92
- ".jpg": "image/jpeg",
93
- ".jpeg": "image/jpeg",
94
- ".gif": "image/gif",
95
- ".webp": "image/webp",
96
- ".mp4": "video/mp4",
97
- ".mov": "video/quicktime",
98
- ".mp3": "audio/mpeg",
99
- ".m4a": "audio/mp4",
100
- ".ogg": "audio/ogg",
101
- ".opus": "audio/ogg; codecs=opus",
102
- ".wav": "audio/wav",
103
- };
104
-
105
- export function guessMimetype(fileName: string, fallback: string): string {
106
- return MIME_BY_EXT[extname(fileName).toLowerCase()] ?? fallback;
107
- }
108
-
109
15
  /**
110
16
  * Resolve a `reply_to` message id into the quoted message Baileys wants.
111
17
  * An unknown id quotes nothing rather than failing the send — the reply
@@ -292,29 +198,3 @@ export async function sendText(
292
198
  );
293
199
  return first ?? { ok: true };
294
200
  }
295
-
296
- /**
297
- * Coerce a user reference the model supplied — bare number, JID, or the
298
- * numeric id from a member listing — into a WhatsApp user JID.
299
- */
300
- export function toUserJid(value: unknown): string | null {
301
- const raw = String(value ?? "").trim();
302
- if (!raw) return null;
303
- if (raw.includes("@")) return raw;
304
- const digits = raw.replace(/[^0-9]/g, "");
305
- return digits ? `${digits}@s.whatsapp.net` : null;
306
- }
307
-
308
- /**
309
- * Pull the about text out of a `fetchStatus` result. Baileys has
310
- * returned both a bare object and a one-element array across versions,
311
- * and the payload itself nests `status` either as a string or as
312
- * `{ status }` — accept all four shapes rather than guess one.
313
- */
314
- export function readStatusText(result: unknown): string | undefined {
315
- const entry = (Array.isArray(result) ? result[0] : result) as
316
- { status?: string | { status?: string } } | undefined;
317
- return typeof entry?.status === "string"
318
- ? entry.status
319
- : entry?.status?.status;
320
- }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Run a WhatsApp action, converting a throw into a structured failure.
3
+ */
4
+
5
+ import { logError } from "../../../util/log.js";
6
+ import type { ActionResult } from "../../../core/types.js";
7
+
8
+ /**
9
+ * Run an action, converting a throw into a structured failure. WhatsApp
10
+ * errors arrive as Boom objects whose message is the useful part; the
11
+ * model gets that text so it can adapt rather than retry blindly.
12
+ */
13
+ export async function tryAction(
14
+ label: string,
15
+ fn: () => Promise<ActionResult>,
16
+ ): Promise<ActionResult> {
17
+ try {
18
+ return await fn();
19
+ } catch (err) {
20
+ const msg = err instanceof Error ? err.message : String(err);
21
+ logError("whatsapp", `${label} failed: ${msg}`);
22
+ return { ok: false, error: `${label}: ${msg}` };
23
+ }
24
+ }
@@ -42,7 +42,7 @@ import {
42
42
  type ModelCommandDeps,
43
43
  type ModelOverview,
44
44
  } from "../shared/model-commands.js";
45
- import { sendText } from "./actions/shared.js";
45
+ import { sendText } from "./actions/send.js";
46
46
  import { identityAllowed, type Identity } from "./identity.js";
47
47
  import type { WhatsAppChatInfo } from "./registry.js";
48
48
  import type { WhatsAppRuntime } from "./runtime.js";
@@ -11,7 +11,7 @@ import makeWASocket, { type AuthenticationState, type WASocket } from "baileys";
11
11
  import qrcode from "qrcode-terminal";
12
12
  import { log, logError, logWarn } from "../../util/log.js";
13
13
  import { dirs } from "../../util/paths.js";
14
- import { notifyAdmin } from "../../core/notify.js";
14
+ import { notifyAdmin } from "../../core/frontend-runtime/admin-notify.js";
15
15
  import { useAtomicAuthState } from "./auth-state.js";
16
16
  import { bareId } from "./identity.js";
17
17
  import { handleInbound } from "./inbound.js";
@@ -18,7 +18,7 @@ import {
18
18
  } from "../../util/watchdog.js";
19
19
  import { isAddressedToSelf, isGroupAllowed } from "./access.js";
20
20
  import { handleWhatsAppCommand } from "./commands.js";
21
- import { sendText } from "./actions/shared.js";
21
+ import { sendText } from "./actions/send.js";
22
22
  import {
23
23
  bareId,
24
24
  canonicalId,
@@ -25,14 +25,14 @@ import type { Gateway } from "../../core/engine/gateway.js";
25
25
  import { log, logError, logWarn } from "../../util/log.js";
26
26
  import { maxMsgIdForChatPrefix } from "../../storage/history.js";
27
27
  import { createWhatsAppActionHandler } from "./actions/index.js";
28
- import { sendText, setWhatsAppBotName } from "./actions/shared.js";
28
+ import { sendText, setWhatsAppBotName } from "./actions/send.js";
29
29
  import { seedMessageStore } from "./message-store.js";
30
30
  import {
31
31
  maxWhatsAppMsgId,
32
32
  pruneWhatsAppMessages,
33
33
  } from "../../storage/whatsapp-messages.js";
34
34
  import { flushAuthWrites } from "./auth-state.js";
35
- import { registerPairingProvider } from "../../core/pairing-broker.js";
35
+ import { registerPairingProvider } from "../../core/frontend-runtime/pairing-broker.js";
36
36
  import { beginPairingAttempt } from "./pairing-service.js";
37
37
  import { runConnectionLoop } from "./connection.js";
38
38
  import { lookupWhatsAppChat, type WhatsAppChatInfo } from "./registry.js";
@@ -16,7 +16,7 @@ import QRCode from "qrcode";
16
16
  import type {
17
17
  PairingAttempt,
18
18
  PairingOutcome,
19
- } from "../../core/pairing-broker.js";
19
+ } from "../../core/frontend-runtime/pairing-broker.js";
20
20
  import { log, logError } from "../../util/log.js";
21
21
  import { dirs } from "../../util/paths.js";
22
22
  import { useAtomicAuthState, flushAuthWrites } from "./auth-state.js";
@@ -9,7 +9,7 @@
9
9
  */
10
10
 
11
11
  import { join } from "node:path";
12
- import type { DoctorCheck } from "../../core/doctor.js";
12
+ import type { DoctorCheck } from "../../core/doctor/index.js";
13
13
  import {
14
14
  failDetail,
15
15
  loadProvisionState,
@@ -23,7 +23,7 @@
23
23
  import { existsSync } from "node:fs";
24
24
  import { homedir } from "node:os";
25
25
  import { dirname, join, resolve } from "node:path";
26
- import type { DoctorCheck } from "../../core/doctor.js";
26
+ import type { DoctorCheck } from "../../core/doctor/index.js";
27
27
  import {
28
28
  compareVersions,
29
29
  expandHome,
@@ -16,7 +16,7 @@
16
16
  import { existsSync, readFileSync, readdirSync } from "node:fs";
17
17
  import { homedir } from "node:os";
18
18
  import { dirname, join, resolve } from "node:path";
19
- import type { DoctorCheck } from "../../core/doctor.js";
19
+ import type { DoctorCheck } from "../../core/doctor/index.js";
20
20
  import {
21
21
  failDetail,
22
22
  loadProvisionState,
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Cron repository — executes the statements in sql/cron.sql against
3
3
  * the `cron_jobs` table; no SQL text lives here. The public store
4
- * (storage/cron-store.ts) holds the domain API and validation; this
4
+ * (storage/cron.ts) holds the domain API and validation; this
5
5
  * module owns statement execution and the row↔domain mapping.
6
6
  */
7
7
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Goals repository — executes the statements in sql/goals.sql against
3
3
  * the `goals` table; no SQL text lives here. The public store
4
- * (storage/goal-store.ts) holds the domain API and validation; this
4
+ * (storage/goals.ts) holds the domain API and validation; this
5
5
  * module owns statement execution and the row↔domain mapping.
6
6
  */
7
7
 
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Scripts repository — executes the statements in sql/scripts.sql
3
3
  * against the `scripts` table; no SQL text lives here. The public
4
- * store (storage/script-store.ts) holds the domain API, validation,
4
+ * store (storage/scripts.ts) holds the domain API, validation,
5
5
  * and the on-disk script files; this module owns statement execution
6
6
  * and the row↔domain mapping.
7
7
  */
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Triggers repository — executes the statements in sql/triggers.sql
3
3
  * against the `triggers` table; no SQL text lives here. The public
4
- * store (storage/trigger-store.ts) holds the domain API, validation
4
+ * store (storage/triggers.ts) holds the domain API, validation
5
5
  * and the on-disk script/log handling; this module owns statement
6
6
  * execution and the row↔domain mapping.
7
7
  */
@@ -140,7 +140,7 @@ CREATE INDEX IF NOT EXISTS idx_goals_chat_status ON goals(chat_id, status, updat
140
140
  CREATE INDEX IF NOT EXISTS idx_goals_status ON goals(status, updated_at DESC);
141
141
 
142
142
  -- Agent-authored scripts. Metadata rows only: the script body lives on
143
- -- disk under ~/.talon/workspace/scripts/ (mirroring the trigger-store
143
+ -- disk under ~/.talon/workspace/scripts/ (mirroring the triggers store
144
144
  -- split) so the agent can also Read/Edit a script as a normal workspace
145
145
  -- file. Scripts are global capabilities, not chat data — no chat_id
146
146
  -- column. `name` is the lookup key; UNIQUE enforces one per name.
@@ -145,7 +145,7 @@ CREATE INDEX IF NOT EXISTS idx_goals_chat_status ON goals(chat_id, status, updat
145
145
  CREATE INDEX IF NOT EXISTS idx_goals_status ON goals(status, updated_at DESC);
146
146
 
147
147
  -- Agent-authored scripts. Metadata rows only: the script body lives on
148
- -- disk under ~/.talon/workspace/scripts/ (mirroring the trigger-store
148
+ -- disk under ~/.talon/workspace/scripts/ (mirroring the triggers store
149
149
  -- split) so the agent can also Read/Edit a script as a normal workspace
150
150
  -- file. Scripts are global capabilities, not chat data — no chat_id
151
151
  -- column. \`name\` is the lookup key; UNIQUE enforces one per name.
@@ -561,7 +561,7 @@ FROM triggers ORDER BY created_at`,
561
561
  SET status = 'terminated',
562
562
  pid = NULL,
563
563
  ended_at = COALESCE(ended_at, ?),
564
- -- Literal must match RESTART_KILL_ERROR in storage/trigger-store.ts.
564
+ -- Literal must match RESTART_KILL_ERROR in storage/triggers.ts.
565
565
  last_error = COALESCE(last_error, 'Talon restarted while trigger was running')
566
566
  WHERE status IN ('running', 'pending') AND persistent = 0
567
567
 
@@ -56,7 +56,7 @@ UPDATE triggers
56
56
  SET status = 'terminated',
57
57
  pid = NULL,
58
58
  ended_at = COALESCE(ended_at, ?),
59
- -- Literal must match RESTART_KILL_ERROR in storage/trigger-store.ts.
59
+ -- Literal must match RESTART_KILL_ERROR in storage/triggers.ts.
60
60
  last_error = COALESCE(last_error, 'Talon restarted while trigger was running')
61
61
  WHERE status IN ('running', 'pending') AND persistent = 0
62
62
 
@@ -1,255 +0,0 @@
1
- /**
2
- * Moderation actions — the `moderate` tool's member/chat/topic operations,
3
- * plus profile-photo lookup.
4
- *
5
- * Handlers call the Bot API directly and let Telegram's own errors surface
6
- * (missing admin rights come back as descriptive 400s the model can read);
7
- * pre-checking rights here would just race the real check.
8
- */
9
-
10
- import type { ChatPermissions } from "grammy/types";
11
- import { toPositiveId } from "./shared.js";
12
- import { resolveMediaInput } from "./media.js";
13
- import { clearJoinRequest, listJoinRequests } from "../join-requests.js";
14
- import type { TelegramActionHandlers } from "./types.js";
15
-
16
- /** Seconds-from-now → Telegram until_date, honouring "omit = forever". */
17
- function untilDate(minutes: unknown): number | undefined {
18
- const n = Number(minutes);
19
- if (!Number.isFinite(n) || n <= 0) return undefined;
20
- return Math.floor(Date.now() / 1000) + Math.round(n * 60);
21
- }
22
-
23
- /**
24
- * Friendly permission names → ChatPermissions. `send_media` fans out to all
25
- * the per-kind media flags; raw `can_*` keys pass through untouched so the
26
- * full API surface stays reachable.
27
- */
28
- function toChatPermissions(raw: Record<string, boolean>): ChatPermissions {
29
- const out: Record<string, boolean> = {};
30
- const alias: Record<string, string[]> = {
31
- send_messages: ["can_send_messages"],
32
- send_media: [
33
- "can_send_audios",
34
- "can_send_documents",
35
- "can_send_photos",
36
- "can_send_videos",
37
- "can_send_video_notes",
38
- "can_send_voice_notes",
39
- ],
40
- send_polls: ["can_send_polls"],
41
- send_other: ["can_send_other_messages"],
42
- web_previews: ["can_add_web_page_previews"],
43
- change_info: ["can_change_info"],
44
- invite_users: ["can_invite_users"],
45
- pin_messages: ["can_pin_messages"],
46
- manage_topics: ["can_manage_topics"],
47
- };
48
- for (const [key, value] of Object.entries(raw)) {
49
- if (typeof value !== "boolean") continue;
50
- for (const target of alias[key] ?? (key.startsWith("can_") ? [key] : [])) {
51
- out[target] = value;
52
- }
53
- }
54
- return out;
55
- }
56
-
57
- /** Everything a mute takes away; unmute grants it back. */
58
- const FULL_MEMBER_PERMISSIONS: ChatPermissions = toChatPermissions({
59
- send_messages: true,
60
- send_media: true,
61
- send_polls: true,
62
- send_other: true,
63
- web_previews: true,
64
- });
65
-
66
- /** The workaday admin kit `promote` grants; demote sets all of these false. */
67
- const DEFAULT_ADMIN_RIGHTS = {
68
- can_manage_chat: true,
69
- can_delete_messages: true,
70
- can_restrict_members: true,
71
- can_pin_messages: true,
72
- can_invite_users: true,
73
- can_change_info: true,
74
- can_manage_topics: true,
75
- can_manage_video_chats: true,
76
- };
77
-
78
- export const moderationHandlers: TelegramActionHandlers = {
79
- moderate: async (body, chatId, ctx) => {
80
- const { bot, InputFileClass } = ctx;
81
- const op = String(body.op ?? "");
82
- const userId = toPositiveId(body.user_id);
83
- const needsUser = [
84
- "ban",
85
- "unban",
86
- "mute",
87
- "unmute",
88
- "promote",
89
- "demote",
90
- "set_admin_title",
91
- "approve_join_request",
92
- "decline_join_request",
93
- ];
94
- if (needsUser.includes(op) && userId === undefined)
95
- return { ok: false, error: `${op}: user_id is required` };
96
-
97
- switch (op) {
98
- case "ban":
99
- await bot.api.banChatMember(chatId, userId!, {
100
- until_date: untilDate(body.minutes),
101
- revoke_messages: body.delete_messages === true || undefined,
102
- });
103
- return { ok: true };
104
- case "unban":
105
- // only_if_banned keeps this from kicking a current member.
106
- await bot.api.unbanChatMember(chatId, userId!, {
107
- only_if_banned: true,
108
- });
109
- return { ok: true };
110
- case "mute":
111
- await bot.api.restrictChatMember(
112
- chatId,
113
- userId!,
114
- { can_send_messages: false },
115
- { until_date: untilDate(body.minutes) },
116
- );
117
- return { ok: true };
118
- case "unmute":
119
- await bot.api.restrictChatMember(
120
- chatId,
121
- userId!,
122
- FULL_MEMBER_PERMISSIONS,
123
- );
124
- return { ok: true };
125
- case "promote":
126
- await bot.api.promoteChatMember(chatId, userId!, DEFAULT_ADMIN_RIGHTS);
127
- return { ok: true };
128
- case "demote": {
129
- const revoked = Object.fromEntries(
130
- Object.keys(DEFAULT_ADMIN_RIGHTS).map((k) => [k, false]),
131
- );
132
- await bot.api.promoteChatMember(chatId, userId!, revoked);
133
- return { ok: true };
134
- }
135
- case "set_admin_title":
136
- await bot.api.setChatAdministratorCustomTitle(
137
- chatId,
138
- userId!,
139
- String(body.title ?? ""),
140
- );
141
- return { ok: true };
142
- case "set_permissions": {
143
- const raw = body.permissions;
144
- if (!raw || typeof raw !== "object")
145
- return { ok: false, error: "set_permissions: permissions required" };
146
- await bot.api.setChatPermissions(
147
- chatId,
148
- toChatPermissions(raw as Record<string, boolean>),
149
- );
150
- return { ok: true };
151
- }
152
- case "create_invite_link": {
153
- const expireMin = Number(body.expire_minutes);
154
- const link = await bot.api.createChatInviteLink(chatId, {
155
- name: body.title ? String(body.title) : undefined,
156
- expire_date:
157
- Number.isFinite(expireMin) && expireMin > 0
158
- ? Math.floor(Date.now() / 1000) + Math.round(expireMin * 60)
159
- : undefined,
160
- member_limit: toPositiveId(body.member_limit),
161
- });
162
- return { ok: true, invite_link: link.invite_link };
163
- }
164
- case "revoke_invite_link": {
165
- if (!body.link)
166
- return { ok: false, error: "revoke_invite_link: link required" };
167
- await bot.api.revokeChatInviteLink(chatId, String(body.link));
168
- return { ok: true };
169
- }
170
- case "approve_join_request":
171
- await bot.api.approveChatJoinRequest(chatId, userId!);
172
- clearJoinRequest(chatId, userId!);
173
- return { ok: true };
174
- case "decline_join_request":
175
- await bot.api.declineChatJoinRequest(chatId, userId!);
176
- clearJoinRequest(chatId, userId!);
177
- return { ok: true };
178
- case "list_join_requests":
179
- return { ok: true, requests: listJoinRequests(chatId) };
180
- case "set_chat_photo": {
181
- const resolved = resolveMediaInput(
182
- body,
183
- "set_chat_photo",
184
- InputFileClass,
185
- );
186
- if ("error" in resolved) return { ok: false, error: resolved.error };
187
- if (typeof resolved.file === "string")
188
- return {
189
- ok: false,
190
- error: "set_chat_photo needs a local file_path (no url/file_id)",
191
- };
192
- await bot.api.setChatPhoto(chatId, resolved.file);
193
- return { ok: true };
194
- }
195
- case "delete_chat_photo":
196
- await bot.api.deleteChatPhoto(chatId);
197
- return { ok: true };
198
- case "unpin_all":
199
- await bot.api.unpinAllChatMessages(chatId);
200
- return { ok: true };
201
- case "leave_chat":
202
- await bot.api.leaveChat(chatId);
203
- return { ok: true };
204
- case "create_topic": {
205
- if (!body.title)
206
- return { ok: false, error: "create_topic: title required" };
207
- const topic = await bot.api.createForumTopic(
208
- chatId,
209
- String(body.title),
210
- );
211
- return { ok: true, thread_id: topic.message_thread_id };
212
- }
213
- case "edit_topic": {
214
- const threadId = toPositiveId(body.thread_id);
215
- if (threadId === undefined)
216
- return { ok: false, error: "edit_topic: thread_id required" };
217
- await bot.api.editForumTopic(chatId, threadId, {
218
- name: body.title ? String(body.title) : undefined,
219
- });
220
- return { ok: true };
221
- }
222
- case "close_topic":
223
- case "reopen_topic":
224
- case "delete_topic": {
225
- const threadId = toPositiveId(body.thread_id);
226
- if (threadId === undefined)
227
- return { ok: false, error: `${op}: thread_id required` };
228
- if (op === "close_topic")
229
- await bot.api.closeForumTopic(chatId, threadId);
230
- else if (op === "reopen_topic")
231
- await bot.api.reopenForumTopic(chatId, threadId);
232
- else await bot.api.deleteForumTopic(chatId, threadId);
233
- return { ok: true };
234
- }
235
- default:
236
- return { ok: false, error: `Unknown moderation op: ${op}` };
237
- }
238
- },
239
-
240
- get_user_profile_photos: async (body, _chatId, { bot }) => {
241
- const userId = toPositiveId(body.user_id);
242
- if (userId === undefined)
243
- return { ok: false, error: "user_id is required" };
244
- const limit = toPositiveId(body.limit) ?? 5;
245
- const photos = await bot.api.getUserProfilePhotos(userId, {
246
- limit: Math.min(limit, 100),
247
- });
248
- return {
249
- ok: true,
250
- total: photos.total_count,
251
- // Largest size of each photo; its file_id sends/downloads like media.
252
- file_ids: photos.photos.map((sizes) => sizes[sizes.length - 1].file_id),
253
- };
254
- },
255
- };
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes