talon-agent 1.12.0 → 1.34.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 +111 -52
- package/package.json +39 -12
- package/prompts/README.md +112 -0
- package/prompts/base.md +12 -12
- package/prompts/discord.md +10 -47
- package/prompts/heartbeat.md +23 -8
- package/prompts/identity.md +12 -22
- package/prompts/native.md +23 -0
- package/prompts/system/contract-text-or-tools.md +8 -0
- package/prompts/system/contract-text-preferred.md +5 -0
- package/prompts/system/contract-tool-only.md +10 -0
- package/prompts/system/cron.md +3 -0
- package/prompts/system/daily-memory.md +3 -0
- package/prompts/system/goals.md +7 -0
- package/prompts/system/heartbeat-agent.md +44 -0
- package/prompts/system/persistent-memory.md +8 -0
- package/prompts/system/skills.md +9 -0
- package/prompts/system/triggers.md +5 -0
- package/prompts/system/workspace.md +9 -0
- package/prompts/teams.md +4 -14
- package/prompts/telegram.md +17 -89
- package/src/app.ts +278 -0
- package/src/backend/claude-sdk/constants.ts +22 -10
- package/src/backend/claude-sdk/factory.ts +79 -22
- package/src/backend/claude-sdk/handler.ts +431 -160
- package/src/backend/claude-sdk/index.ts +6 -2
- package/src/backend/claude-sdk/mcp-ready.ts +54 -0
- package/src/backend/claude-sdk/model-drift.ts +65 -0
- package/src/backend/claude-sdk/model-provider.ts +12 -6
- package/src/backend/claude-sdk/models/convert.ts +169 -0
- package/src/backend/claude-sdk/models/discovery.ts +203 -0
- package/src/backend/claude-sdk/models/index.ts +18 -0
- package/src/backend/claude-sdk/models/parsing.ts +264 -0
- package/src/backend/claude-sdk/models/static.ts +45 -0
- package/src/backend/claude-sdk/one-shot.ts +19 -13
- package/src/backend/claude-sdk/options.ts +228 -104
- package/src/backend/claude-sdk/state.ts +1 -1
- package/src/backend/claude-sdk/stream.ts +153 -45
- package/src/backend/claude-sdk/warm.ts +12 -5
- package/src/backend/codex/auth.ts +216 -40
- package/src/backend/codex/constants.ts +63 -22
- package/src/backend/codex/discovery.ts +427 -0
- package/src/backend/codex/factory.ts +63 -33
- package/src/backend/codex/handler/events.ts +239 -0
- package/src/backend/codex/handler/index.ts +15 -0
- package/src/backend/codex/handler/message.ts +669 -0
- package/src/backend/codex/handler/state.ts +14 -0
- package/src/backend/codex/handler/usage.ts +68 -0
- package/src/backend/codex/index.ts +2 -2
- package/src/backend/codex/init.ts +92 -11
- package/src/backend/codex/mcp-config.ts +89 -62
- package/src/backend/codex/models.ts +305 -54
- package/src/backend/codex/oauth-incompat.ts +302 -0
- package/src/backend/codex/one-shot.ts +91 -8
- package/src/backend/codex/state.ts +54 -3
- package/src/backend/codex/token-usage.ts +434 -0
- package/src/backend/kilo/factory.ts +60 -35
- package/src/backend/kilo/handler/index.ts +13 -0
- package/src/backend/kilo/handler/message.ts +317 -0
- package/src/backend/kilo/handler/state.ts +14 -0
- package/src/backend/kilo/handler/turn.ts +321 -0
- package/src/backend/kilo/index.ts +5 -5
- package/src/backend/kilo/model-provider.ts +26 -168
- package/src/backend/kilo/models/index.ts +52 -0
- package/src/backend/kilo/one-shot.ts +20 -142
- package/src/backend/kilo/server.ts +14 -21
- package/src/backend/openai-agents/builtins.ts +215 -114
- package/src/backend/openai-agents/constants.ts +41 -15
- package/src/backend/openai-agents/discovery.ts +332 -0
- package/src/backend/openai-agents/factory.ts +66 -28
- package/src/backend/openai-agents/handler/events.ts +170 -0
- package/src/backend/openai-agents/handler/index.ts +13 -0
- package/src/backend/openai-agents/handler/message.ts +527 -0
- package/src/backend/openai-agents/handler/state.ts +13 -0
- package/src/backend/openai-agents/index.ts +22 -4
- package/src/backend/openai-agents/init.ts +51 -88
- package/src/backend/openai-agents/mcp-pool.ts +252 -0
- package/src/backend/openai-agents/models.ts +118 -11
- package/src/backend/openai-agents/session.ts +277 -0
- package/src/backend/openai-agents/state.ts +82 -11
- package/src/backend/opencode/factory.ts +54 -25
- package/src/backend/opencode/handler/index.ts +13 -0
- package/src/backend/opencode/handler/message.ts +325 -0
- package/src/backend/opencode/handler/state.ts +14 -0
- package/src/backend/opencode/handler/turn.ts +304 -0
- package/src/backend/opencode/index.ts +2 -2
- package/src/backend/opencode/model-provider.ts +26 -168
- package/src/backend/opencode/models/index.ts +51 -0
- package/src/backend/opencode/one-shot.ts +20 -137
- package/src/backend/opencode/server.ts +12 -11
- package/src/backend/remote-server/client.ts +13 -6
- package/src/backend/remote-server/events.ts +59 -9
- package/src/backend/remote-server/lifecycle.ts +3 -1
- package/src/backend/remote-server/mcp.ts +52 -19
- package/src/backend/remote-server/model-catalog/catalog.ts +270 -0
- package/src/backend/remote-server/model-catalog/index.ts +100 -0
- package/src/backend/remote-server/model-catalog/presentation.ts +298 -0
- package/src/backend/remote-server/model-catalog/provider.ts +193 -0
- package/src/backend/remote-server/model-catalog/resolve.ts +225 -0
- package/src/backend/remote-server/model-catalog/types.ts +136 -0
- package/src/backend/remote-server/one-shot.ts +225 -0
- package/src/backend/remote-server/session-helpers.ts +3 -4
- package/src/backend/remote-server/state.ts +1 -1
- package/src/backend/shared/delivery-contract.ts +161 -0
- package/src/backend/shared/delivery.ts +49 -0
- package/src/backend/shared/flow-violation.ts +38 -7
- package/src/backend/shared/frontends.ts +66 -0
- package/src/backend/shared/handle-retry.ts +244 -0
- package/src/backend/shared/handler-to-events.ts +186 -0
- package/src/backend/shared/handler-types.ts +57 -0
- package/src/backend/shared/index.ts +51 -5
- package/src/backend/shared/metrics.ts +199 -0
- package/src/backend/shared/model-retry.ts +1 -1
- package/src/backend/shared/prompt-format.ts +69 -0
- package/src/backend/shared/session-name.ts +1 -1
- package/src/backend/shared/stream-state.ts +36 -2
- package/src/backend/shared/system-prompt.ts +200 -23
- package/src/bootstrap.ts +338 -38
- package/src/cli/chat.ts +49 -0
- package/src/cli/config-view.ts +100 -0
- package/src/cli/config.ts +105 -0
- package/src/cli/context.ts +23 -0
- package/src/cli/daemon.ts +95 -0
- package/src/cli/doctor.ts +53 -0
- package/src/cli/index.ts +139 -0
- package/src/cli/logs.ts +60 -0
- package/src/cli/menu.ts +104 -0
- package/src/cli/setup.ts +455 -0
- package/src/cli/status.ts +75 -0
- package/src/cli.ts +20 -1211
- package/src/core/agent-runtime/README.md +173 -0
- package/src/{backend/registry.ts → core/agent-runtime/backend-registry.ts} +15 -6
- package/src/core/agent-runtime/capabilities.ts +330 -0
- package/src/core/agent-runtime/contract-tests.ts +378 -0
- package/src/core/agent-runtime/events.ts +291 -0
- package/src/core/agent-runtime/index.ts +62 -0
- package/src/core/agent-runtime/model-ref.ts +130 -0
- package/src/core/background/cron.ts +469 -0
- package/src/core/{dream.ts → background/dream.ts} +118 -53
- package/src/core/background/heartbeat/agent.ts +333 -0
- package/src/core/background/heartbeat/index.ts +25 -0
- package/src/core/background/heartbeat/scheduler.ts +211 -0
- package/src/core/background/heartbeat/state.ts +153 -0
- package/src/core/background/index.ts +21 -0
- package/src/core/background/isolated-agent.ts +115 -0
- package/src/core/background/job-health.ts +140 -0
- package/src/core/background/job-oneshot.ts +165 -0
- package/src/core/background/job-prompt.ts +54 -0
- package/src/core/{pulse.ts → background/pulse.ts} +6 -5
- package/src/core/background/triggers/command.ts +60 -0
- package/src/core/background/triggers/exit.ts +202 -0
- package/src/core/background/triggers/index.ts +38 -0
- package/src/core/background/triggers/output.ts +142 -0
- package/src/core/background/triggers/resume.ts +127 -0
- package/src/core/background/triggers/spawn.ts +282 -0
- package/src/core/background/triggers/state.ts +52 -0
- package/src/core/constants.ts +57 -28
- package/src/core/daemon/control.ts +229 -0
- package/src/core/daemon/discovery.ts +169 -0
- package/src/core/daemon/pidfile.ts +98 -0
- package/src/core/doctor.ts +459 -0
- package/src/core/engine/backend-controller/holders.ts +17 -0
- package/src/core/engine/backend-controller/index.ts +71 -0
- package/src/core/engine/backend-controller/legacy.ts +111 -0
- package/src/core/engine/backend-controller/pool.ts +296 -0
- package/src/core/engine/backend-controller/rebind.ts +205 -0
- package/src/core/engine/backend-controller/state.ts +150 -0
- package/src/core/engine/backend-controller/types.ts +51 -0
- package/src/core/engine/dispatcher.ts +68 -0
- package/src/core/engine/gateway-actions/cron.ts +518 -0
- package/src/core/engine/gateway-actions/fetch-url.ts +120 -0
- package/src/core/engine/gateway-actions/goals.ts +178 -0
- package/src/core/engine/gateway-actions/history.ts +53 -0
- package/src/core/engine/gateway-actions/index.ts +60 -0
- package/src/core/engine/gateway-actions/models.ts +120 -0
- package/src/core/engine/gateway-actions/plugins.ts +83 -0
- package/src/core/engine/gateway-actions/scripts.ts +127 -0
- package/src/core/engine/gateway-actions/shared.ts +66 -0
- package/src/core/engine/gateway-actions/skills.ts +120 -0
- package/src/core/engine/gateway-actions/triggers.ts +237 -0
- package/src/core/engine/gateway-actions/types.ts +18 -0
- package/src/core/{gateway.ts → engine/gateway.ts} +223 -60
- package/src/core/engine/index.ts +20 -0
- package/src/core/engine/model-audit.ts +121 -0
- package/src/core/errors.ts +68 -6
- package/src/core/mcp-hub/children.ts +254 -0
- package/src/core/mcp-hub/index.ts +335 -0
- package/src/core/mcp-hub/proxy-server.ts +45 -0
- package/src/core/mcp-hub/talon-server.ts +71 -0
- package/src/core/memory/retrieval.ts +90 -0
- package/src/core/models/active-model.ts +441 -0
- package/src/core/{models.ts → models/catalog.ts} +6 -0
- package/src/core/models/index.ts +17 -0
- package/src/core/models/reasoning-levels.ts +57 -0
- package/src/core/plugin/actions.ts +32 -0
- package/src/core/plugin/builtins.ts +162 -0
- package/src/core/plugin/index.ts +37 -0
- package/src/core/plugin/loader.ts +289 -0
- package/src/core/plugin/mcp.ts +86 -0
- package/src/core/plugin/registry.ts +124 -0
- package/src/core/plugin/types.ts +126 -0
- package/src/core/prompt/assemble.ts +273 -0
- package/src/core/prompt/disk-prompts.ts +41 -0
- package/src/core/prompt/embed.ts +107 -0
- package/src/core/prompt/embedded-prompts.ts +85 -0
- package/src/core/prompt/index.ts +30 -0
- package/src/core/prompt/templates.ts +124 -0
- package/src/core/prompt/workspace-listing.ts +91 -0
- package/src/core/scripting/lua-runner.ts +210 -0
- package/src/core/scripts/runner.ts +125 -0
- package/src/core/soul/README.md +110 -0
- package/src/core/soul/RESEARCH.md +98 -0
- package/src/core/soul/associative.ts +98 -0
- package/src/core/soul/centrality.ts +98 -0
- package/src/core/soul/cluster.ts +83 -0
- package/src/core/soul/compiler.ts +207 -0
- package/src/core/soul/consolidate.ts +179 -0
- package/src/core/soul/critic.ts +159 -0
- package/src/core/soul/dag.ts +265 -0
- package/src/core/soul/delta.ts +123 -0
- package/src/core/soul/drift.ts +99 -0
- package/src/core/soul/embedder.ts +129 -0
- package/src/core/soul/emergent-critic.ts +96 -0
- package/src/core/soul/forgetting.ts +131 -0
- package/src/core/soul/governance.ts +93 -0
- package/src/core/soul/hash.ts +97 -0
- package/src/core/soul/hdc.ts +154 -0
- package/src/core/soul/index.ts +140 -0
- package/src/core/soul/kernel.ts +540 -0
- package/src/core/soul/lattice.ts +103 -0
- package/src/core/soul/lens.ts +110 -0
- package/src/core/soul/projector.ts +240 -0
- package/src/core/soul/reflect.ts +170 -0
- package/src/core/soul/reflex.ts +164 -0
- package/src/core/soul/retrieve.ts +146 -0
- package/src/core/soul/salience.ts +142 -0
- package/src/core/soul/service.ts +204 -0
- package/src/core/soul/settings.ts +47 -0
- package/src/core/soul/signals.ts +117 -0
- package/src/core/soul/talon-embedder.ts +80 -0
- package/src/core/soul/taps.ts +199 -0
- package/src/core/soul/types.ts +298 -0
- package/src/core/soul/valence.ts +83 -0
- package/src/core/tools/goals.ts +117 -0
- package/src/core/tools/index.ts +10 -0
- package/src/core/tools/mcp-env.ts +94 -0
- package/src/core/tools/mcp-server.ts +27 -4
- package/src/core/tools/messaging.ts +122 -29
- package/src/core/tools/models.ts +39 -0
- package/src/core/tools/scheduling.ts +133 -14
- package/src/core/tools/schemas.ts +16 -4
- package/src/core/tools/scripts.ts +78 -0
- package/src/core/tools/skills.ts +90 -0
- package/src/core/tools/stickers.ts +1 -1
- package/src/core/tools/triggers.ts +19 -3
- package/src/core/tools/types.ts +6 -6
- package/src/core/types.ts +62 -112
- package/src/core/update/self-update.ts +269 -0
- package/src/core/weaver/index.ts +10 -0
- package/src/core/weaver/loom.ts +125 -0
- package/src/core/weaver/thread-session.ts +51 -0
- package/src/core/weaver/thread.ts +196 -0
- package/src/core/weaver/weaver.ts +357 -0
- package/src/frontend/discord/actions/chat-info.ts +268 -0
- package/src/frontend/discord/actions/index.ts +77 -0
- package/src/frontend/discord/actions/media.ts +190 -0
- package/src/frontend/discord/actions/messaging.ts +277 -0
- package/src/frontend/discord/actions/shared.ts +100 -0
- package/src/frontend/discord/actions/types.ts +33 -0
- package/src/frontend/discord/admin.ts +13 -23
- package/src/frontend/discord/callbacks/autocomplete.ts +55 -0
- package/src/frontend/discord/{callbacks.ts → callbacks/components.ts} +150 -190
- package/src/frontend/discord/callbacks/index.ts +14 -0
- package/src/frontend/discord/callbacks/modals.ts +76 -0
- package/src/frontend/discord/callbacks/shared.ts +22 -0
- package/src/frontend/discord/commands/admin.ts +88 -0
- package/src/frontend/discord/commands/definitions.ts +200 -0
- package/src/frontend/discord/commands/index.ts +19 -0
- package/src/frontend/discord/commands/info.ts +106 -0
- package/src/frontend/discord/commands/router.ts +149 -0
- package/src/frontend/discord/commands/session.ts +89 -0
- package/src/frontend/discord/commands/settings.ts +405 -0
- package/src/frontend/discord/commands/shared.ts +50 -0
- package/src/frontend/discord/formatting.ts +8 -34
- package/src/frontend/discord/handlers/access.ts +248 -0
- package/src/frontend/discord/handlers/context.ts +161 -0
- package/src/frontend/discord/handlers/delivery.ts +94 -0
- package/src/frontend/discord/handlers/index.ts +34 -0
- package/src/frontend/discord/handlers/messages.ts +120 -0
- package/src/frontend/discord/handlers/queue.ts +135 -0
- package/src/frontend/discord/handlers/registry.ts +29 -0
- package/src/frontend/discord/handlers/state.ts +94 -0
- package/src/frontend/discord/helpers.ts +46 -57
- package/src/frontend/discord/index.ts +10 -7
- package/src/frontend/discord/middleware.ts +2 -2
- package/src/frontend/native/actions.ts +133 -0
- package/src/frontend/native/chats.ts +146 -0
- package/src/frontend/native/discovery.ts +50 -0
- package/src/frontend/native/index.ts +1227 -0
- package/src/frontend/native/logs.ts +95 -0
- package/src/frontend/native/protocol.ts +253 -0
- package/src/frontend/native/server.ts +612 -0
- package/src/frontend/native/settings.ts +205 -0
- package/src/frontend/native/turn-meta.ts +43 -0
- package/src/frontend/shared/format.ts +51 -0
- package/src/frontend/shared/reasoning-levels.ts +46 -0
- package/src/frontend/shared/session-status.ts +189 -0
- package/src/frontend/shared/status-context.ts +125 -0
- package/src/frontend/teams/actions.ts +7 -5
- package/src/frontend/teams/formatting.ts +7 -18
- package/src/frontend/teams/graph.ts +2 -1
- package/src/frontend/teams/index.ts +76 -54
- package/src/frontend/telegram/actions/chat-info.ts +323 -0
- package/src/frontend/telegram/actions/index.ts +68 -0
- package/src/frontend/telegram/actions/media.ts +224 -0
- package/src/frontend/telegram/actions/messaging.ts +295 -0
- package/src/frontend/telegram/actions/shared.ts +58 -0
- package/src/frontend/telegram/actions/types.ts +31 -0
- package/src/frontend/telegram/admin.ts +12 -22
- package/src/frontend/telegram/callbacks/effort.ts +68 -0
- package/src/frontend/telegram/callbacks/index.ts +68 -0
- package/src/frontend/telegram/callbacks/model.ts +311 -0
- package/src/frontend/telegram/callbacks/pulse.ts +50 -0
- package/src/frontend/telegram/callbacks/settings.ts +177 -0
- package/src/frontend/telegram/callbacks/shared.ts +71 -0
- package/src/frontend/telegram/commands/admin.ts +226 -0
- package/src/frontend/telegram/commands/definitions.ts +60 -0
- package/src/frontend/telegram/commands/index.ts +39 -0
- package/src/frontend/telegram/commands/info.ts +125 -0
- package/src/frontend/telegram/commands/session.ts +77 -0
- package/src/frontend/telegram/commands/settings.ts +345 -0
- package/src/frontend/telegram/commands/state.ts +34 -0
- package/src/frontend/telegram/formatting.ts +14 -25
- package/src/frontend/telegram/handlers/access.ts +287 -0
- package/src/frontend/telegram/handlers/context.ts +210 -0
- package/src/frontend/telegram/handlers/delivery.ts +236 -0
- package/src/frontend/telegram/handlers/index.ts +39 -0
- package/src/frontend/telegram/handlers/messages.ts +477 -0
- package/src/frontend/telegram/handlers/queue.ts +241 -0
- package/src/frontend/telegram/handlers/state.ts +75 -0
- package/src/frontend/telegram/helpers/diagnostics.ts +188 -0
- package/src/frontend/telegram/helpers/format.ts +41 -0
- package/src/frontend/telegram/helpers/index.ts +13 -0
- package/src/frontend/telegram/{helpers.ts → helpers/menu.ts} +209 -210
- package/src/frontend/telegram/index.ts +24 -27
- package/src/frontend/telegram/middleware.ts +16 -3
- package/src/frontend/telegram/model-callbacks.ts +14 -0
- package/src/frontend/telegram/model-menu.ts +317 -0
- package/src/frontend/telegram/sticker-library.ts +178 -0
- package/src/frontend/telegram/userbot.ts +2 -44
- package/src/frontend/terminal/commands.ts +47 -41
- package/src/frontend/terminal/index.ts +46 -31
- package/src/frontend/terminal/input.ts +5 -0
- package/src/frontend/terminal/renderer.ts +4 -2
- package/src/index.ts +13 -171
- package/src/login.ts +3 -2
- package/src/native/blake3-wasm-bytes.ts +7 -0
- package/src/native/blake3.ts +234 -0
- package/src/native/htmlents-wasm-bytes.ts +7 -0
- package/src/native/htmlents.ts +56 -0
- package/src/native/prelude.d.mts +168 -0
- package/src/native/prelude.mjs +1561 -0
- package/src/native/registry.ts +142 -0
- package/src/native/runtime.ts +164 -0
- package/src/native/scheduler-core/gleam.d.mts +2 -0
- package/src/native/scheduler-core/gleam.mjs +1 -0
- package/src/native/scheduler-core/scheduler_core.d.mts +76 -0
- package/src/native/scheduler-core/scheduler_core.mjs +252 -0
- package/src/native/scheduler-core.ts +124 -0
- package/src/native/sqlguard-wasm-bytes.ts +7 -0
- package/src/native/sqlguard.ts +107 -0
- package/src/native/strsim-wasm-bytes.ts +7 -0
- package/src/native/strsim.ts +98 -0
- package/src/native/textops-wasm-bytes.ts +7 -0
- package/src/native/textops.ts +83 -0
- package/src/native/warden.ts +287 -0
- package/src/plugins/github/index.ts +1 -1
- package/src/plugins/mempalace/index.ts +1 -1
- package/src/plugins/playwright/index.ts +1 -1
- package/src/storage/chat-settings.ts +335 -85
- package/src/storage/cron-store.ts +266 -109
- package/src/storage/daily-log.ts +15 -3
- package/src/storage/db.ts +206 -0
- package/src/storage/goal-store.ts +186 -0
- package/src/storage/history.ts +137 -188
- package/src/storage/kv.ts +52 -0
- package/src/storage/legacy-import.ts +47 -0
- package/src/storage/media-index.ts +118 -61
- package/src/storage/repositories/chat-settings-repo.ts +60 -0
- package/src/storage/repositories/cron-repo.ts +128 -0
- package/src/storage/repositories/goals-repo.ts +122 -0
- package/src/storage/repositories/history-repo.ts +207 -0
- package/src/storage/repositories/kv-repo.ts +25 -0
- package/src/storage/repositories/media-index-repo.ts +147 -0
- package/src/storage/repositories/scripts-repo.ts +81 -0
- package/src/storage/repositories/sessions-repo.ts +129 -0
- package/src/storage/repositories/triggers-repo.ts +158 -0
- package/src/storage/repositories/turn-meta-repo.ts +32 -0
- package/src/storage/scheduled-store.ts +84 -0
- package/src/storage/script-store.ts +198 -0
- package/src/storage/sessions.ts +227 -102
- package/src/storage/skill-store.ts +325 -0
- package/src/storage/sql/README.md +18 -0
- package/src/storage/sql/chat-settings.sql +11 -0
- package/src/storage/sql/cron.sql +40 -0
- package/src/storage/sql/db.sql +11 -0
- package/src/storage/sql/embed.ts +118 -0
- package/src/storage/sql/goals.sql +41 -0
- package/src/storage/sql/history.sql +83 -0
- package/src/storage/sql/kv.sql +11 -0
- package/src/storage/sql/media-index.sql +45 -0
- package/src/storage/sql/schema.sql +233 -0
- package/src/storage/sql/scripts.sql +28 -0
- package/src/storage/sql/sessions.sql +22 -0
- package/src/storage/sql/statements.generated.ts +500 -0
- package/src/storage/sql/triggers.sql +69 -0
- package/src/storage/sql/turn-meta.sql +24 -0
- package/src/storage/sticker-store.ts +141 -0
- package/src/storage/trigger-store.ts +104 -126
- package/src/storage/turn-meta.ts +118 -0
- package/src/util/chat-id.ts +30 -0
- package/src/util/config.ts +313 -195
- package/src/util/fs-path.ts +32 -0
- package/src/util/log.ts +53 -31
- package/src/util/mcp-launcher.ts +351 -33
- package/src/util/paths.ts +45 -17
- package/src/util/tail-file.ts +24 -0
- package/src/util/workspace.ts +161 -10
- package/tsconfig.json +1 -0
- package/src/backend/claude-sdk/models.ts +0 -501
- package/src/backend/codex/handler.ts +0 -595
- package/src/backend/kilo/handler.ts +0 -707
- package/src/backend/kilo/models.ts +0 -762
- package/src/backend/openai-agents/handler.ts +0 -539
- package/src/backend/openai-agents/mcp.ts +0 -139
- package/src/backend/opencode/handler.ts +0 -663
- package/src/backend/opencode/models.ts +0 -741
- package/src/core/cron.ts +0 -184
- package/src/core/dispatcher.ts +0 -144
- package/src/core/gateway-actions.ts +0 -603
- package/src/core/heartbeat.ts +0 -575
- package/src/core/plugin.ts +0 -776
- package/src/core/triggers.ts +0 -640
- package/src/frontend/discord/actions.ts +0 -729
- package/src/frontend/discord/commands.ts +0 -1036
- package/src/frontend/discord/handlers.ts +0 -798
- package/src/frontend/telegram/actions.ts +0 -731
- package/src/frontend/telegram/callbacks.ts +0 -419
- package/src/frontend/telegram/commands.ts +0 -642
- package/src/frontend/telegram/handlers.ts +0 -1352
- package/src/util/mcp-launcher.mjs +0 -140
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media actions — sending files/photos/videos/animations/voice/audio,
|
|
3
|
+
* stickers, polls, locations, contacts, and dice.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
7
|
+
import { basename } from "node:path";
|
|
8
|
+
import { expandFsPath } from "../../../util/fs-path.js";
|
|
9
|
+
import { markdownToTelegramHtml } from "../formatting.js";
|
|
10
|
+
import { withRetry } from "../../../core/engine/gateway.js";
|
|
11
|
+
import { resolveStickerByEmoji } from "../sticker-library.js";
|
|
12
|
+
import { replyParams } from "./shared.js";
|
|
13
|
+
import type { TelegramActionHandlers } from "./types.js";
|
|
14
|
+
|
|
15
|
+
const sendMediaFile: TelegramActionHandlers[string] = async (
|
|
16
|
+
body,
|
|
17
|
+
chatId,
|
|
18
|
+
{ bot, InputFileClass, gateway },
|
|
19
|
+
) => {
|
|
20
|
+
// Routed for send_file / send_photo / send_video / send_animation /
|
|
21
|
+
// send_voice / send_audio — branch on the action name in the body.
|
|
22
|
+
const action = String(body.action);
|
|
23
|
+
const caption = body.caption
|
|
24
|
+
? markdownToTelegramHtml(String(body.caption))
|
|
25
|
+
: undefined;
|
|
26
|
+
const captionParseMode = caption ? ("HTML" as const) : undefined;
|
|
27
|
+
gateway.incrementMessages(chatId);
|
|
28
|
+
// Three media sources, in priority order: a public URL or a Telegram
|
|
29
|
+
// file_id (both passed to the Bot API as a plain string — Telegram
|
|
30
|
+
// fetches/reuses server-side, no local bytes involved), else a
|
|
31
|
+
// workspace file path uploaded as multipart.
|
|
32
|
+
const remote = body.url ?? body.file_id;
|
|
33
|
+
let file: string | import("grammy").InputFile;
|
|
34
|
+
if (remote) {
|
|
35
|
+
file = String(remote);
|
|
36
|
+
} else {
|
|
37
|
+
// Fail with guidance the model can act on, not a raw ENOENT: a
|
|
38
|
+
// mistyped path is the most common media-send error, and naming
|
|
39
|
+
// the alternatives (url / file_id) teaches the recovery path.
|
|
40
|
+
if (!body.file_path)
|
|
41
|
+
return {
|
|
42
|
+
ok: false,
|
|
43
|
+
error: `${action}: provide file_path (workspace file), url (public), or file_id (seen in chat)`,
|
|
44
|
+
};
|
|
45
|
+
const filePath = expandFsPath(String(body.file_path));
|
|
46
|
+
if (!existsSync(filePath))
|
|
47
|
+
return {
|
|
48
|
+
ok: false,
|
|
49
|
+
error: `File not found: ${filePath} — check the workspace path, or send by url/file_id instead`,
|
|
50
|
+
};
|
|
51
|
+
const stat = statSync(filePath);
|
|
52
|
+
if (stat.size > 49 * 1024 * 1024)
|
|
53
|
+
return { ok: false, error: "File too large (max 49MB)" };
|
|
54
|
+
const data = readFileSync(filePath);
|
|
55
|
+
file = new InputFileClass(data, basename(filePath));
|
|
56
|
+
}
|
|
57
|
+
const rp = replyParams(body);
|
|
58
|
+
let sent;
|
|
59
|
+
switch (action) {
|
|
60
|
+
case "send_file":
|
|
61
|
+
sent = await withRetry(() =>
|
|
62
|
+
bot.api.sendDocument(chatId, file, {
|
|
63
|
+
caption,
|
|
64
|
+
parse_mode: captionParseMode,
|
|
65
|
+
reply_parameters: rp,
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
break;
|
|
69
|
+
case "send_photo":
|
|
70
|
+
sent = await withRetry(() =>
|
|
71
|
+
bot.api.sendPhoto(chatId, file, {
|
|
72
|
+
caption,
|
|
73
|
+
parse_mode: captionParseMode,
|
|
74
|
+
reply_parameters: rp,
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
break;
|
|
78
|
+
case "send_video":
|
|
79
|
+
sent = await withRetry(() =>
|
|
80
|
+
bot.api.sendVideo(chatId, file, {
|
|
81
|
+
caption,
|
|
82
|
+
parse_mode: captionParseMode,
|
|
83
|
+
reply_parameters: rp,
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
break;
|
|
87
|
+
case "send_animation":
|
|
88
|
+
sent = await withRetry(() =>
|
|
89
|
+
bot.api.sendAnimation(chatId, file, {
|
|
90
|
+
caption,
|
|
91
|
+
parse_mode: captionParseMode,
|
|
92
|
+
reply_parameters: rp,
|
|
93
|
+
}),
|
|
94
|
+
);
|
|
95
|
+
break;
|
|
96
|
+
case "send_audio":
|
|
97
|
+
sent = await withRetry(() =>
|
|
98
|
+
bot.api.sendAudio(chatId, file, {
|
|
99
|
+
caption,
|
|
100
|
+
parse_mode: captionParseMode,
|
|
101
|
+
reply_parameters: rp,
|
|
102
|
+
title: body.title as string | undefined,
|
|
103
|
+
performer: body.performer as string | undefined,
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
break;
|
|
107
|
+
default:
|
|
108
|
+
sent = await withRetry(() =>
|
|
109
|
+
bot.api.sendVoice(chatId, file, {
|
|
110
|
+
caption,
|
|
111
|
+
parse_mode: captionParseMode,
|
|
112
|
+
reply_parameters: rp,
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
return { ok: true, message_id: sent.message_id };
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export const mediaHandlers: TelegramActionHandlers = {
|
|
121
|
+
send_file: sendMediaFile,
|
|
122
|
+
send_photo: sendMediaFile,
|
|
123
|
+
send_video: sendMediaFile,
|
|
124
|
+
send_animation: sendMediaFile,
|
|
125
|
+
send_voice: sendMediaFile,
|
|
126
|
+
send_audio: sendMediaFile,
|
|
127
|
+
|
|
128
|
+
send_sticker: async (body, chatId, { bot, gateway }) => {
|
|
129
|
+
// Three addressing modes: a concrete file_id, a public URL of a
|
|
130
|
+
// .webp (Telegram fetches it server-side, like other media), or an
|
|
131
|
+
// emoji resolved against the saved sticker library (optionally
|
|
132
|
+
// pinned to one pack via set_name) — the low-friction path the
|
|
133
|
+
// prompt teaches.
|
|
134
|
+
let fileId = body.file_id
|
|
135
|
+
? String(body.file_id)
|
|
136
|
+
: body.url
|
|
137
|
+
? String(body.url)
|
|
138
|
+
: "";
|
|
139
|
+
if (!fileId && body.emoji) {
|
|
140
|
+
const resolved = await resolveStickerByEmoji(
|
|
141
|
+
bot,
|
|
142
|
+
String(body.emoji),
|
|
143
|
+
body.set_name ? String(body.set_name) : undefined,
|
|
144
|
+
);
|
|
145
|
+
if (!resolved) {
|
|
146
|
+
return {
|
|
147
|
+
ok: false,
|
|
148
|
+
error: `No saved sticker matches ${String(body.emoji)}${body.set_name ? ` in pack "${String(body.set_name)}"` : ""} — check the sticker library, or save a pack with save_sticker_pack.`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
fileId = resolved.fileId;
|
|
152
|
+
}
|
|
153
|
+
if (!fileId)
|
|
154
|
+
return { ok: false, error: "Required: file_id, url, or emoji" };
|
|
155
|
+
gateway.incrementMessages(chatId);
|
|
156
|
+
const sent = await bot.api.sendSticker(chatId, fileId, {
|
|
157
|
+
reply_parameters: replyParams(body),
|
|
158
|
+
});
|
|
159
|
+
return { ok: true, message_id: sent.message_id };
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
send_poll: async (body, chatId, { bot, gateway }) => {
|
|
163
|
+
gateway.incrementMessages(chatId);
|
|
164
|
+
const sent = await bot.api.sendPoll(
|
|
165
|
+
chatId,
|
|
166
|
+
String(body.question ?? ""),
|
|
167
|
+
((body.options as string[]) ?? []).map((o) => ({ text: o })),
|
|
168
|
+
{
|
|
169
|
+
is_anonymous: body.is_anonymous as boolean | undefined,
|
|
170
|
+
allows_multiple_answers: body.allows_multiple_answers as
|
|
171
|
+
boolean | undefined,
|
|
172
|
+
type: body.type as "regular" | "quiz" | undefined,
|
|
173
|
+
correct_option_ids:
|
|
174
|
+
body.correct_option_id != null
|
|
175
|
+
? [body.correct_option_id as number]
|
|
176
|
+
: undefined,
|
|
177
|
+
explanation: body.explanation as string | undefined,
|
|
178
|
+
reply_parameters: replyParams(body),
|
|
179
|
+
},
|
|
180
|
+
);
|
|
181
|
+
return { ok: true, message_id: sent.message_id };
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
send_location: async (body, chatId, { bot, gateway }) => {
|
|
185
|
+
gateway.incrementMessages(chatId);
|
|
186
|
+
const sent = await bot.api.sendLocation(
|
|
187
|
+
chatId,
|
|
188
|
+
Number(body.latitude),
|
|
189
|
+
Number(body.longitude),
|
|
190
|
+
{ reply_parameters: replyParams(body) },
|
|
191
|
+
);
|
|
192
|
+
return { ok: true, message_id: sent.message_id };
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
send_contact: async (body, chatId, { bot, gateway }) => {
|
|
196
|
+
gateway.incrementMessages(chatId);
|
|
197
|
+
const sent = await bot.api.sendContact(
|
|
198
|
+
chatId,
|
|
199
|
+
String(body.phone_number),
|
|
200
|
+
String(body.first_name),
|
|
201
|
+
{
|
|
202
|
+
last_name: body.last_name as string | undefined,
|
|
203
|
+
reply_parameters: replyParams(body),
|
|
204
|
+
},
|
|
205
|
+
);
|
|
206
|
+
return { ok: true, message_id: sent.message_id };
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
send_dice: async (body, chatId, { bot, gateway }) => {
|
|
210
|
+
gateway.incrementMessages(chatId);
|
|
211
|
+
const sent = await bot.api.sendDice(
|
|
212
|
+
chatId,
|
|
213
|
+
(body.emoji as string) || "🎲",
|
|
214
|
+
{
|
|
215
|
+
reply_parameters: replyParams(body),
|
|
216
|
+
},
|
|
217
|
+
);
|
|
218
|
+
return {
|
|
219
|
+
ok: true,
|
|
220
|
+
message_id: sent.message_id,
|
|
221
|
+
value: sent.dice?.value,
|
|
222
|
+
};
|
|
223
|
+
},
|
|
224
|
+
};
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Messaging actions — send/reply/edit/delete/pin/forward/copy/react,
|
|
3
|
+
* inline-button messages, chat actions, and scheduled messages.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Bot } from "grammy";
|
|
7
|
+
import { markdownToTelegramHtml } from "../formatting.js";
|
|
8
|
+
import { withRetry } from "../../../core/engine/gateway.js";
|
|
9
|
+
import { log, logError, logWarn } from "../../../util/log.js";
|
|
10
|
+
import {
|
|
11
|
+
saveScheduled,
|
|
12
|
+
deleteScheduled,
|
|
13
|
+
listScheduled,
|
|
14
|
+
listScheduledForChat,
|
|
15
|
+
MAX_OVERDUE_MS,
|
|
16
|
+
type ScheduledMessage,
|
|
17
|
+
} from "../../../storage/scheduled-store.js";
|
|
18
|
+
import { sendText, toPositiveId } from "./shared.js";
|
|
19
|
+
import { TELEGRAM_MAX_TEXT, type TelegramActionHandlers } from "./types.js";
|
|
20
|
+
|
|
21
|
+
// ── Scheduled sends (persistent) ─────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
/** Longest schedulable delay: 24h. Timers re-arm from the store on boot. */
|
|
24
|
+
const MAX_DELAY_SEC = 24 * 60 * 60;
|
|
25
|
+
|
|
26
|
+
/** Deliver one scheduled entry (shared by the live timer and restore). */
|
|
27
|
+
async function fireScheduled(bot: Bot, entry: ScheduledMessage): Promise<void> {
|
|
28
|
+
const chatId = Number(entry.chatId);
|
|
29
|
+
if (entry.rows) {
|
|
30
|
+
const keyboard = entry.rows.map((row) =>
|
|
31
|
+
row.map((btn) =>
|
|
32
|
+
btn.url
|
|
33
|
+
? { text: btn.text, url: btn.url }
|
|
34
|
+
: { text: btn.text, callback_data: btn.callback_data ?? btn.text },
|
|
35
|
+
),
|
|
36
|
+
);
|
|
37
|
+
await bot.api.sendMessage(chatId, markdownToTelegramHtml(entry.text), {
|
|
38
|
+
parse_mode: "HTML",
|
|
39
|
+
reply_markup: { inline_keyboard: keyboard },
|
|
40
|
+
reply_parameters: entry.replyTo
|
|
41
|
+
? { message_id: entry.replyTo }
|
|
42
|
+
: undefined,
|
|
43
|
+
});
|
|
44
|
+
} else {
|
|
45
|
+
await sendText(bot, chatId, entry.text, entry.replyTo);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Arm a timer for a stored entry; fires, then cleans up store + map. */
|
|
50
|
+
function armScheduled(
|
|
51
|
+
bot: Bot,
|
|
52
|
+
entry: ScheduledMessage,
|
|
53
|
+
timers: Map<string, ReturnType<typeof setTimeout>>,
|
|
54
|
+
): void {
|
|
55
|
+
const delayMs = Math.max(0, entry.fireAt - Date.now());
|
|
56
|
+
const timer = setTimeout(async () => {
|
|
57
|
+
try {
|
|
58
|
+
await fireScheduled(bot, entry);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
logError("bot", `Scheduled message failed (chat=${entry.chatId})`, err);
|
|
61
|
+
}
|
|
62
|
+
deleteScheduled(entry.id);
|
|
63
|
+
timers.delete(entry.id);
|
|
64
|
+
}, delayMs);
|
|
65
|
+
timers.set(entry.id, timer);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Re-arm persisted scheduled sends after a restart. Called once when
|
|
70
|
+
* the action handler is created. Overdue entries fire immediately —
|
|
71
|
+
* late beats never — unless they are stale past MAX_OVERDUE_MS, which
|
|
72
|
+
* drops them (a day-late reminder is noise, not delivery).
|
|
73
|
+
*/
|
|
74
|
+
export function restoreScheduledMessages(
|
|
75
|
+
bot: Bot,
|
|
76
|
+
timers: Map<string, ReturnType<typeof setTimeout>>,
|
|
77
|
+
): void {
|
|
78
|
+
const entries = listScheduled("telegram");
|
|
79
|
+
if (entries.length === 0) return;
|
|
80
|
+
let restored = 0;
|
|
81
|
+
let dropped = 0;
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
if (Date.now() - entry.fireAt > MAX_OVERDUE_MS) {
|
|
84
|
+
deleteScheduled(entry.id);
|
|
85
|
+
dropped++;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
armScheduled(bot, entry, timers);
|
|
89
|
+
restored++;
|
|
90
|
+
}
|
|
91
|
+
log(
|
|
92
|
+
"bot",
|
|
93
|
+
`Restored ${restored} scheduled message(s) from store${dropped > 0 ? `, dropped ${dropped} stale` : ""}`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const messagingHandlers: TelegramActionHandlers = {
|
|
98
|
+
send_message: async (body, chatId, { bot, gateway }) => {
|
|
99
|
+
const text = String(body.text ?? "");
|
|
100
|
+
const replyTo = toPositiveId(body.reply_to_message_id);
|
|
101
|
+
gateway.incrementMessages(chatId);
|
|
102
|
+
const msgId = await withRetry(() => sendText(bot, chatId, text, replyTo));
|
|
103
|
+
return { ok: true, message_id: msgId };
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
reply_to: async (body, chatId, { bot, gateway }) => {
|
|
107
|
+
const msgId = Number(body.message_id);
|
|
108
|
+
gateway.incrementMessages(chatId);
|
|
109
|
+
const sentId = await withRetry(() =>
|
|
110
|
+
sendText(bot, chatId, String(body.text ?? ""), msgId),
|
|
111
|
+
);
|
|
112
|
+
return { ok: true, message_id: sentId };
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
react: async (body, chatId, { bot, gateway }) => {
|
|
116
|
+
gateway.incrementMessages(chatId);
|
|
117
|
+
const emoji = String(body.emoji ?? "👍");
|
|
118
|
+
try {
|
|
119
|
+
await withRetry(() =>
|
|
120
|
+
bot.api.setMessageReaction(chatId, Number(body.message_id), [
|
|
121
|
+
{ type: "emoji", emoji: emoji as "👍" },
|
|
122
|
+
]),
|
|
123
|
+
);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
logWarn(
|
|
126
|
+
"bot",
|
|
127
|
+
`Custom emoji reaction failed, falling back to 👍: ${err instanceof Error ? err.message : err}`,
|
|
128
|
+
);
|
|
129
|
+
try {
|
|
130
|
+
await bot.api.setMessageReaction(chatId, Number(body.message_id), [
|
|
131
|
+
{ type: "emoji", emoji: "👍" },
|
|
132
|
+
]);
|
|
133
|
+
} catch (e) {
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
error: `Reaction failed: ${e instanceof Error ? e.message : e}`,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return { ok: true };
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
edit_message: async (body, chatId, { bot }) => {
|
|
144
|
+
const text = String(body.text ?? "");
|
|
145
|
+
if (text.length > TELEGRAM_MAX_TEXT)
|
|
146
|
+
return {
|
|
147
|
+
ok: false,
|
|
148
|
+
error: `Text too long (max ${TELEGRAM_MAX_TEXT})`,
|
|
149
|
+
};
|
|
150
|
+
const html = markdownToTelegramHtml(text);
|
|
151
|
+
await withRetry(async () => {
|
|
152
|
+
try {
|
|
153
|
+
await bot.api.editMessageText(chatId, Number(body.message_id), html, {
|
|
154
|
+
parse_mode: "HTML",
|
|
155
|
+
});
|
|
156
|
+
} catch {
|
|
157
|
+
await bot.api.editMessageText(chatId, Number(body.message_id), text);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
return { ok: true };
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
delete_message: async (body, chatId, { bot }) => {
|
|
164
|
+
await bot.api.deleteMessage(chatId, Number(body.message_id));
|
|
165
|
+
return { ok: true };
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
pin_message: async (body, chatId, { bot }) => {
|
|
169
|
+
await bot.api.pinChatMessage(chatId, Number(body.message_id));
|
|
170
|
+
return { ok: true };
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
unpin_message: async (body, chatId, { bot }) => {
|
|
174
|
+
await bot.api.unpinChatMessage(
|
|
175
|
+
chatId,
|
|
176
|
+
body.message_id ? Number(body.message_id) : undefined,
|
|
177
|
+
);
|
|
178
|
+
return { ok: true };
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
forward_message: async (body, chatId, { bot }) => {
|
|
182
|
+
if (body.to_chat_id && Number(body.to_chat_id) !== chatId)
|
|
183
|
+
return { ok: false, error: "Cross-chat forwarding not allowed." };
|
|
184
|
+
const sent = await bot.api.forwardMessage(
|
|
185
|
+
chatId,
|
|
186
|
+
chatId,
|
|
187
|
+
Number(body.message_id),
|
|
188
|
+
);
|
|
189
|
+
return { ok: true, message_id: sent.message_id };
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
copy_message: async (body, chatId, { bot }) => {
|
|
193
|
+
const sent = await bot.api.copyMessage(
|
|
194
|
+
chatId,
|
|
195
|
+
chatId,
|
|
196
|
+
Number(body.message_id),
|
|
197
|
+
);
|
|
198
|
+
return { ok: true, message_id: sent.message_id };
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
send_chat_action: async (body, chatId, { bot }) => {
|
|
202
|
+
await bot.api.sendChatAction(
|
|
203
|
+
chatId,
|
|
204
|
+
String(body.chat_action ?? "typing") as "typing",
|
|
205
|
+
);
|
|
206
|
+
return { ok: true };
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
send_message_with_buttons: async (body, chatId, { bot, gateway }) => {
|
|
210
|
+
const text = String(body.text ?? "");
|
|
211
|
+
if (text.length > TELEGRAM_MAX_TEXT)
|
|
212
|
+
return { ok: false, error: `Text too long` };
|
|
213
|
+
const html = markdownToTelegramHtml(text);
|
|
214
|
+
const rows = body.rows as Array<
|
|
215
|
+
Array<{ text: string; url?: string; callback_data?: string }>
|
|
216
|
+
>;
|
|
217
|
+
gateway.incrementMessages(chatId);
|
|
218
|
+
const keyboard = rows.map((row) =>
|
|
219
|
+
row.map((btn) =>
|
|
220
|
+
btn.url
|
|
221
|
+
? { text: btn.text, url: btn.url }
|
|
222
|
+
: {
|
|
223
|
+
text: btn.text,
|
|
224
|
+
callback_data: btn.callback_data ?? btn.text,
|
|
225
|
+
},
|
|
226
|
+
),
|
|
227
|
+
);
|
|
228
|
+
try {
|
|
229
|
+
const sent = await bot.api.sendMessage(chatId, html, {
|
|
230
|
+
parse_mode: "HTML",
|
|
231
|
+
reply_markup: { inline_keyboard: keyboard },
|
|
232
|
+
});
|
|
233
|
+
return { ok: true, message_id: sent.message_id };
|
|
234
|
+
} catch {
|
|
235
|
+
const sent = await bot.api.sendMessage(chatId, text, {
|
|
236
|
+
reply_markup: { inline_keyboard: keyboard },
|
|
237
|
+
});
|
|
238
|
+
return { ok: true, message_id: sent.message_id };
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
schedule_message: (body, chatId, { bot, scheduledMessages }) => {
|
|
243
|
+
const text = String(body.text ?? "");
|
|
244
|
+
const replyTo = toPositiveId(body.reply_to_message_id);
|
|
245
|
+
const rows = body.rows as ScheduledMessage["rows"];
|
|
246
|
+
const delaySec = Math.max(
|
|
247
|
+
1,
|
|
248
|
+
Math.min(MAX_DELAY_SEC, Number(body.delay_seconds ?? 60)),
|
|
249
|
+
);
|
|
250
|
+
const entry: ScheduledMessage = {
|
|
251
|
+
id: `sched_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
252
|
+
frontend: "telegram",
|
|
253
|
+
chatId: String(chatId),
|
|
254
|
+
text,
|
|
255
|
+
fireAt: Date.now() + delaySec * 1000,
|
|
256
|
+
createdAt: Date.now(),
|
|
257
|
+
replyTo,
|
|
258
|
+
rows,
|
|
259
|
+
};
|
|
260
|
+
// Persist before arming: if we crash between the two, the restore
|
|
261
|
+
// path delivers it; the reverse order could lose it forever.
|
|
262
|
+
saveScheduled(entry);
|
|
263
|
+
armScheduled(bot, entry, scheduledMessages);
|
|
264
|
+
return { ok: true, schedule_id: entry.id, delay_seconds: delaySec };
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
cancel_scheduled: (body, _chatId, { scheduledMessages }) => {
|
|
268
|
+
const id = String(body.schedule_id ?? "");
|
|
269
|
+
const timer = scheduledMessages.get(id);
|
|
270
|
+
if (timer) {
|
|
271
|
+
clearTimeout(timer);
|
|
272
|
+
scheduledMessages.delete(id);
|
|
273
|
+
}
|
|
274
|
+
// The store is the source of truth — an entry can exist without a
|
|
275
|
+
// live timer (scheduled before a restart, not yet re-armed here).
|
|
276
|
+
const existed = deleteScheduled(id) || Boolean(timer);
|
|
277
|
+
return existed
|
|
278
|
+
? { ok: true, cancelled: true }
|
|
279
|
+
: { ok: false, error: "Schedule not found" };
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
list_scheduled: (body, chatId) => {
|
|
283
|
+
const pending = listScheduledForChat("telegram", String(chatId)).map(
|
|
284
|
+
(e) => ({
|
|
285
|
+
schedule_id: e.id,
|
|
286
|
+
fires_in_seconds: Math.max(
|
|
287
|
+
0,
|
|
288
|
+
Math.round((e.fireAt - Date.now()) / 1000),
|
|
289
|
+
),
|
|
290
|
+
text: e.text.length > 200 ? `${e.text.slice(0, 200)}…` : e.text,
|
|
291
|
+
}),
|
|
292
|
+
);
|
|
293
|
+
return { ok: true, scheduled: pending, count: pending.length };
|
|
294
|
+
},
|
|
295
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for Telegram action handlers: reply-parameter extraction and
|
|
3
|
+
* the HTML-with-plain-fallback text sender.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Bot } from "grammy";
|
|
7
|
+
import { markdownToTelegramHtml } from "../formatting.js";
|
|
8
|
+
import { logWarn } from "../../../util/log.js";
|
|
9
|
+
import { TELEGRAM_MAX_TEXT } from "./types.js";
|
|
10
|
+
|
|
11
|
+
export function replyParams(
|
|
12
|
+
body: Record<string, unknown>,
|
|
13
|
+
): { message_id: number } | undefined {
|
|
14
|
+
const replyTo = toPositiveId(body.reply_to ?? body.reply_to_message_id);
|
|
15
|
+
return replyTo !== undefined ? { message_id: replyTo } : undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Telegram/GramJS APIs expect numeric IDs. `snowflakeOrIdSchema` normalizes IDs
|
|
20
|
+
* to digit-strings (so 17-19 digit Discord snowflakes survive validation), so a
|
|
21
|
+
* Telegram message/reply/offset ID can arrive here as a string. Coerce it back
|
|
22
|
+
* to a positive integer: Telegram IDs are well within 2^53, so this is lossless.
|
|
23
|
+
* Returns undefined for missing / non-positive / non-integer values.
|
|
24
|
+
*/
|
|
25
|
+
export function toPositiveId(v: unknown): number | undefined {
|
|
26
|
+
const n = typeof v === "number" ? v : typeof v === "string" ? Number(v) : NaN;
|
|
27
|
+
return Number.isInteger(n) && n > 0 ? n : undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function sendText(
|
|
31
|
+
bot: Bot,
|
|
32
|
+
chatId: number,
|
|
33
|
+
text: string,
|
|
34
|
+
replyTo?: number,
|
|
35
|
+
): Promise<number> {
|
|
36
|
+
if (text.length > TELEGRAM_MAX_TEXT) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Message too long (${text.length} chars, max ${TELEGRAM_MAX_TEXT}).`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
const html = markdownToTelegramHtml(text);
|
|
42
|
+
try {
|
|
43
|
+
const sent = await bot.api.sendMessage(chatId, html, {
|
|
44
|
+
parse_mode: "HTML",
|
|
45
|
+
reply_parameters: replyTo ? { message_id: replyTo } : undefined,
|
|
46
|
+
});
|
|
47
|
+
return sent.message_id;
|
|
48
|
+
} catch (err) {
|
|
49
|
+
logWarn(
|
|
50
|
+
"bot",
|
|
51
|
+
`sendText with parse_mode=HTML failed; retrying without parse_mode (chat=${chatId}): ${err instanceof Error ? err.message : err}`,
|
|
52
|
+
);
|
|
53
|
+
const sent = await bot.api.sendMessage(chatId, text, {
|
|
54
|
+
reply_parameters: replyTo ? { message_id: replyTo } : undefined,
|
|
55
|
+
});
|
|
56
|
+
return sent.message_id;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram action-handler types.
|
|
3
|
+
*
|
|
4
|
+
* Each domain module exports a `TelegramActionHandlers` map keyed by action
|
|
5
|
+
* name. Handlers receive the request body, the numeric chat id, and a shared
|
|
6
|
+
* `TelegramActionContext` carrying the bound bot, the InputFile constructor,
|
|
7
|
+
* the bot token, the gateway, and the per-handler `scheduledMessages` timers.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Bot, InputFile as GrammyInputFile } from "grammy";
|
|
11
|
+
import type { Gateway } from "../../../core/engine/gateway.js";
|
|
12
|
+
import type { ActionResult } from "../../../core/types.js";
|
|
13
|
+
|
|
14
|
+
export interface TelegramActionContext {
|
|
15
|
+
bot: Bot;
|
|
16
|
+
InputFileClass: typeof GrammyInputFile;
|
|
17
|
+
botToken: string;
|
|
18
|
+
gateway: Gateway;
|
|
19
|
+
/** Active scheduled-message timers, keyed by schedule id (for cancellation). */
|
|
20
|
+
scheduledMessages: Map<string, ReturnType<typeof setTimeout>>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type TelegramActionHandler = (
|
|
24
|
+
body: Record<string, unknown>,
|
|
25
|
+
chatId: number,
|
|
26
|
+
ctx: TelegramActionContext,
|
|
27
|
+
) => Promise<ActionResult | null> | ActionResult | null;
|
|
28
|
+
|
|
29
|
+
export type TelegramActionHandlers = Record<string, TelegramActionHandler>;
|
|
30
|
+
|
|
31
|
+
export const TELEGRAM_MAX_TEXT = 4096;
|
|
@@ -6,18 +6,20 @@ import type { Bot, Context } from "grammy";
|
|
|
6
6
|
import { readFileSync } from "node:fs";
|
|
7
7
|
import type { TalonConfig } from "../../util/config.js";
|
|
8
8
|
import { files, dirs } from "../../util/paths.js";
|
|
9
|
+
import { tailFile } from "../../util/tail-file.js";
|
|
9
10
|
import { escapeHtml } from "./formatting.js";
|
|
10
11
|
import { resetSession, getAllSessions } from "../../storage/sessions.js";
|
|
11
12
|
import { clearHistory } from "../../storage/history.js";
|
|
12
13
|
import { getChatSettings } from "../../storage/chat-settings.js";
|
|
13
14
|
import {
|
|
14
15
|
getAllCronJobs,
|
|
15
|
-
|
|
16
|
+
describeSchedule,
|
|
17
|
+
nextRunAt,
|
|
16
18
|
} from "../../storage/cron-store.js";
|
|
17
|
-
import { getActiveCount } from "../../core/dispatcher.js";
|
|
18
|
-
import { getPulseStatus } from "../../core/pulse.js";
|
|
19
|
+
import { getActiveCount } from "../../core/engine/dispatcher.js";
|
|
20
|
+
import { getPulseStatus } from "../../core/background/pulse.js";
|
|
19
21
|
import { getHealthStatus, getRecentErrors } from "../../util/watchdog.js";
|
|
20
|
-
import { formatDuration, formatModelLabel } from "./helpers.js";
|
|
22
|
+
import { formatDuration, formatModelLabel } from "./helpers/index.js";
|
|
21
23
|
|
|
22
24
|
export async function handleAdminCommand(
|
|
23
25
|
ctx: Context,
|
|
@@ -91,6 +93,7 @@ export async function handleAdminCommand(
|
|
|
91
93
|
try {
|
|
92
94
|
await bot.api.sendMessage(id, text);
|
|
93
95
|
sent++;
|
|
96
|
+
await new Promise((r) => setTimeout(r, 40));
|
|
94
97
|
} catch {
|
|
95
98
|
failed++;
|
|
96
99
|
}
|
|
@@ -116,20 +119,7 @@ export async function handleAdminCommand(
|
|
|
116
119
|
case "logs": {
|
|
117
120
|
const logPath = files.log;
|
|
118
121
|
try {
|
|
119
|
-
const
|
|
120
|
-
await import("node:fs");
|
|
121
|
-
const stat = statSync(logPath);
|
|
122
|
-
const size = Math.min(8192, stat.size);
|
|
123
|
-
const buf = Buffer.alloc(size);
|
|
124
|
-
const fd = openSync(logPath, "r");
|
|
125
|
-
readSync(fd, buf, 0, size, Math.max(0, stat.size - size));
|
|
126
|
-
closeSync(fd);
|
|
127
|
-
const lines = buf
|
|
128
|
-
.toString("utf-8")
|
|
129
|
-
.trim()
|
|
130
|
-
.split("\n")
|
|
131
|
-
.slice(-20)
|
|
132
|
-
.join("\n");
|
|
122
|
+
const lines = tailFile(logPath);
|
|
133
123
|
await ctx.reply(`<pre>${escapeHtml(lines.slice(0, 3800))}</pre>`, {
|
|
134
124
|
parse_mode: "HTML",
|
|
135
125
|
});
|
|
@@ -187,14 +177,14 @@ export async function handleAdminCommand(
|
|
|
187
177
|
return;
|
|
188
178
|
}
|
|
189
179
|
const lines = jobs.map((j) => {
|
|
190
|
-
const
|
|
180
|
+
const nextMs = nextRunAt(j);
|
|
191
181
|
const last = j.lastRunAt
|
|
192
182
|
? new Date(j.lastRunAt).toISOString().slice(0, 16).replace("T", " ")
|
|
193
183
|
: "never";
|
|
194
|
-
const next =
|
|
195
|
-
? new Date(
|
|
184
|
+
const next = nextMs
|
|
185
|
+
? new Date(nextMs).toISOString().slice(0, 16).replace("T", " ")
|
|
196
186
|
: "?";
|
|
197
|
-
return `${j.enabled ? "\u2713" : "\u2717"} <b>${escapeHtml(j.name)}</b>\n <code>${j
|
|
187
|
+
return `${j.enabled ? "\u2713" : "\u2717"} <b>${escapeHtml(j.name)}</b>\n <code>${escapeHtml(describeSchedule(j))}</code> | ${j.type} | runs: ${j.runCount} | last: ${last} | next: ${next}`;
|
|
198
188
|
});
|
|
199
189
|
await ctx.reply(
|
|
200
190
|
`<b>Cron Jobs (${jobs.length})</b>\n\n` + lines.join("\n\n"),
|