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,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Access control — DM whitelist, group admin-membership checks, unauthorized
|
|
3
|
+
* notifications, and DM-user tracking.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Bot, Context } from "grammy";
|
|
7
|
+
import { appendDailyLog } from "../../../storage/daily-log.js";
|
|
8
|
+
import { log, logWarn } from "../../../util/log.js";
|
|
9
|
+
import { getSenderName } from "./context.js";
|
|
10
|
+
import {
|
|
11
|
+
knownDmUsers,
|
|
12
|
+
KNOWN_DM_USERS_CAP,
|
|
13
|
+
accessConfig,
|
|
14
|
+
verifiedGroups,
|
|
15
|
+
MAX_VERIFIED_GROUPS,
|
|
16
|
+
VERIFIED_GROUP_TTL_MS,
|
|
17
|
+
unauthorizedCooldown,
|
|
18
|
+
UNAUTHORIZED_COOLDOWN_MS,
|
|
19
|
+
MAX_UNAUTHORIZED_COOLDOWNS,
|
|
20
|
+
} from "./state.js";
|
|
21
|
+
|
|
22
|
+
export function trackDmUser(
|
|
23
|
+
senderId: number,
|
|
24
|
+
senderName: string,
|
|
25
|
+
senderUsername?: string,
|
|
26
|
+
): void {
|
|
27
|
+
if (knownDmUsers.has(senderId)) return;
|
|
28
|
+
// Evict oldest 10% when cap reached (Set maintains insertion order)
|
|
29
|
+
if (knownDmUsers.size >= KNOWN_DM_USERS_CAP) {
|
|
30
|
+
const evictCount = Math.floor(KNOWN_DM_USERS_CAP * 0.1);
|
|
31
|
+
const iter = knownDmUsers.values();
|
|
32
|
+
for (let i = 0; i < evictCount; i++) {
|
|
33
|
+
knownDmUsers.delete(iter.next().value as number);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
knownDmUsers.add(senderId);
|
|
37
|
+
const tag = senderUsername ? ` (@${senderUsername})` : "";
|
|
38
|
+
log("users", `New DM user: ${senderName}${tag} [id:${senderId}]`);
|
|
39
|
+
appendDailyLog("System", `New DM user: ${senderName}${tag} [id:${senderId}]`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function setAccessControl(cfg: {
|
|
43
|
+
allowedUsers?: number[];
|
|
44
|
+
adminUserId?: number;
|
|
45
|
+
}): void {
|
|
46
|
+
accessConfig.allowedUserIds = cfg.allowedUsers?.length
|
|
47
|
+
? new Set(cfg.allowedUsers)
|
|
48
|
+
: null;
|
|
49
|
+
accessConfig.adminId = cfg.adminUserId ?? 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if a DM user is allowed. Returns true if no whitelist is set.
|
|
54
|
+
*/
|
|
55
|
+
function isDmAllowed(senderId: number | undefined): boolean {
|
|
56
|
+
if (!accessConfig.allowedUserIds) return true;
|
|
57
|
+
return senderId !== undefined && accessConfig.allowedUserIds.has(senderId);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check if the admin is a member of a group. Caches results for 10 minutes.
|
|
62
|
+
*/
|
|
63
|
+
async function isAdminInGroup(bot: Bot, chatId: number): Promise<boolean> {
|
|
64
|
+
if (!accessConfig.adminId) return true; // no admin configured, allow all groups
|
|
65
|
+
const cached = verifiedGroups.get(chatId);
|
|
66
|
+
if (cached !== undefined && cached.expiresAt > Date.now()) {
|
|
67
|
+
return cached.isMember;
|
|
68
|
+
}
|
|
69
|
+
// Expired or missing — delete stale entry
|
|
70
|
+
if (cached) verifiedGroups.delete(chatId);
|
|
71
|
+
|
|
72
|
+
// Prevent unbounded growth — evict expired entries first, then clear if still over
|
|
73
|
+
if (verifiedGroups.size >= MAX_VERIFIED_GROUPS) {
|
|
74
|
+
const now = Date.now();
|
|
75
|
+
for (const [k, v] of verifiedGroups) {
|
|
76
|
+
if (v.expiresAt <= now) verifiedGroups.delete(k);
|
|
77
|
+
}
|
|
78
|
+
if (verifiedGroups.size >= MAX_VERIFIED_GROUPS) verifiedGroups.clear();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const member = await bot.api.getChatMember(chatId, accessConfig.adminId);
|
|
83
|
+
const isMember = !["left", "kicked"].includes(member.status);
|
|
84
|
+
verifiedGroups.set(chatId, {
|
|
85
|
+
isMember,
|
|
86
|
+
expiresAt: Date.now() + VERIFIED_GROUP_TTL_MS,
|
|
87
|
+
});
|
|
88
|
+
return isMember;
|
|
89
|
+
} catch (err) {
|
|
90
|
+
logWarn(
|
|
91
|
+
"bot",
|
|
92
|
+
`isAdminInGroup check failed for chat ${chatId}: ${err instanceof Error ? err.message : err}`,
|
|
93
|
+
);
|
|
94
|
+
// API error (e.g. bot can't query members) — deny by default
|
|
95
|
+
verifiedGroups.set(chatId, {
|
|
96
|
+
isMember: false,
|
|
97
|
+
expiresAt: Date.now() + VERIFIED_GROUP_TTL_MS,
|
|
98
|
+
});
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function shouldHandleInGroup(ctx: Context): boolean {
|
|
104
|
+
if (!ctx.chat || !ctx.message) return false;
|
|
105
|
+
const isGroup = ctx.chat.type === "group" || ctx.chat.type === "supergroup";
|
|
106
|
+
if (!isGroup) return true;
|
|
107
|
+
const text = ctx.message.text || ctx.message.caption || "";
|
|
108
|
+
const botUser = ctx.me.username;
|
|
109
|
+
// Word-boundary match — @botname must not be followed by alphanumeric/underscore
|
|
110
|
+
const mentioned =
|
|
111
|
+
botUser && new RegExp(`@${botUser}(?![a-zA-Z0-9_])`, "i").test(text);
|
|
112
|
+
const repliedToBot = ctx.message.reply_to_message?.from?.id === ctx.me.id;
|
|
113
|
+
return !!(mentioned || repliedToBot);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Full access check: DM whitelist + group admin membership.
|
|
118
|
+
* Returns true if the message should be processed.
|
|
119
|
+
* Warns unauthorized users and notifies the admin.
|
|
120
|
+
*/
|
|
121
|
+
export async function isAccessAllowed(
|
|
122
|
+
ctx: Context,
|
|
123
|
+
bot: Bot,
|
|
124
|
+
): Promise<boolean> {
|
|
125
|
+
if (!ctx.chat) return false;
|
|
126
|
+
const isGroup = ctx.chat.type === "group" || ctx.chat.type === "supergroup";
|
|
127
|
+
|
|
128
|
+
if (!isGroup) {
|
|
129
|
+
if (isDmAllowed(ctx.from?.id)) return true;
|
|
130
|
+
await notifyUnauthorized(bot, ctx, "dm");
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (await isAdminInGroup(bot, ctx.chat.id)) return true;
|
|
135
|
+
await notifyUnauthorized(bot, ctx, "group");
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Maximum length of an unauthorized message body to retain in logs.
|
|
141
|
+
* Keeps abusive payloads (large pastes, attachment captions etc.) bounded
|
|
142
|
+
* while still preserving enough context to understand what was sent.
|
|
143
|
+
*/
|
|
144
|
+
const UNAUTHORIZED_BODY_MAX_LEN = 1024;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Best-effort preview of an unauthorized message for forensics.
|
|
148
|
+
*
|
|
149
|
+
* Returns the visible text payload (text or caption), a short tag for
|
|
150
|
+
* media-only messages (`[sticker: 🤖]`, `[photo]`, `[voice 14s]`, etc.),
|
|
151
|
+
* or `undefined` if there's nothing meaningful to capture (e.g. a service
|
|
152
|
+
* message or empty content).
|
|
153
|
+
*
|
|
154
|
+
* Truncated to UNAUTHORIZED_BODY_MAX_LEN to keep log lines bounded.
|
|
155
|
+
*/
|
|
156
|
+
export function extractUnauthorizedPreview(
|
|
157
|
+
message:
|
|
158
|
+
| {
|
|
159
|
+
text?: string;
|
|
160
|
+
caption?: string;
|
|
161
|
+
sticker?: { emoji?: string; set_name?: string };
|
|
162
|
+
photo?: unknown;
|
|
163
|
+
voice?: { duration?: number };
|
|
164
|
+
video?: unknown;
|
|
165
|
+
video_note?: unknown;
|
|
166
|
+
audio?: unknown;
|
|
167
|
+
animation?: unknown;
|
|
168
|
+
document?: { file_name?: string };
|
|
169
|
+
contact?: unknown;
|
|
170
|
+
location?: unknown;
|
|
171
|
+
poll?: { question?: string };
|
|
172
|
+
dice?: { emoji?: string };
|
|
173
|
+
}
|
|
174
|
+
| undefined,
|
|
175
|
+
): string | undefined {
|
|
176
|
+
if (!message) return undefined;
|
|
177
|
+
|
|
178
|
+
const text = message.text ?? message.caption;
|
|
179
|
+
if (typeof text === "string" && text.trim().length > 0) {
|
|
180
|
+
return text.length > UNAUTHORIZED_BODY_MAX_LEN
|
|
181
|
+
? `${text.slice(0, UNAUTHORIZED_BODY_MAX_LEN)}… [truncated]`
|
|
182
|
+
: text;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (message.sticker) {
|
|
186
|
+
const emoji = message.sticker.emoji ?? "?";
|
|
187
|
+
const set = message.sticker.set_name
|
|
188
|
+
? ` from ${message.sticker.set_name}`
|
|
189
|
+
: "";
|
|
190
|
+
return `[sticker: ${emoji}${set}]`;
|
|
191
|
+
}
|
|
192
|
+
if (message.photo) return "[photo]";
|
|
193
|
+
if (message.voice) {
|
|
194
|
+
const dur = message.voice.duration;
|
|
195
|
+
return dur ? `[voice ${dur}s]` : "[voice]";
|
|
196
|
+
}
|
|
197
|
+
if (message.video_note) return "[video note]";
|
|
198
|
+
if (message.video) return "[video]";
|
|
199
|
+
if (message.audio) return "[audio]";
|
|
200
|
+
if (message.animation) return "[animation]";
|
|
201
|
+
if (message.document) {
|
|
202
|
+
return message.document.file_name
|
|
203
|
+
? `[document: ${message.document.file_name}]`
|
|
204
|
+
: "[document]";
|
|
205
|
+
}
|
|
206
|
+
if (message.contact) return "[contact]";
|
|
207
|
+
if (message.location) return "[location]";
|
|
208
|
+
if (message.poll) {
|
|
209
|
+
return message.poll.question
|
|
210
|
+
? `[poll: ${message.poll.question}]`
|
|
211
|
+
: "[poll]";
|
|
212
|
+
}
|
|
213
|
+
if (message.dice) return `[dice: ${message.dice.emoji ?? "🎲"}]`;
|
|
214
|
+
|
|
215
|
+
return undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function notifyUnauthorized(
|
|
219
|
+
bot: Bot,
|
|
220
|
+
ctx: Context,
|
|
221
|
+
type: "dm" | "group",
|
|
222
|
+
): Promise<void> {
|
|
223
|
+
const sender = getSenderName(ctx.from);
|
|
224
|
+
const username = ctx.from?.username ? ` (@${ctx.from.username})` : "";
|
|
225
|
+
const userId = ctx.from?.id ?? "unknown";
|
|
226
|
+
|
|
227
|
+
// Capture message body BEFORE the cooldown check — every unauthorized
|
|
228
|
+
// attempt should be recorded for forensics, even if the user-facing
|
|
229
|
+
// warning + admin notification are suppressed by cooldown. Without
|
|
230
|
+
// this, follow-up DMs from a known social-engineering account vanish
|
|
231
|
+
// entirely from logs.
|
|
232
|
+
const body = extractUnauthorizedPreview(
|
|
233
|
+
ctx.message as Parameters<typeof extractUnauthorizedPreview>[0],
|
|
234
|
+
);
|
|
235
|
+
if (body) {
|
|
236
|
+
try {
|
|
237
|
+
appendDailyLog(
|
|
238
|
+
`⚠️ UNAUTHORIZED ${sender}${username} [id:${userId}]`,
|
|
239
|
+
body,
|
|
240
|
+
);
|
|
241
|
+
} catch {
|
|
242
|
+
/* daily log unavailable — fall through to talon.log */
|
|
243
|
+
}
|
|
244
|
+
logWarn(
|
|
245
|
+
"access",
|
|
246
|
+
`Unauthorized ${type} body from ${sender}${username} [id:${userId}]: ${body.slice(0, 200)}`,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const key = type === "dm" ? `dm:${ctx.from?.id}` : `group:${ctx.chat?.id}`;
|
|
251
|
+
const now = Date.now();
|
|
252
|
+
const lastWarned = unauthorizedCooldown.get(key);
|
|
253
|
+
if (lastWarned && now - lastWarned < UNAUTHORIZED_COOLDOWN_MS) return;
|
|
254
|
+
if (unauthorizedCooldown.size >= MAX_UNAUTHORIZED_COOLDOWNS) {
|
|
255
|
+
unauthorizedCooldown.clear();
|
|
256
|
+
}
|
|
257
|
+
unauthorizedCooldown.set(key, now);
|
|
258
|
+
|
|
259
|
+
// Warn the user
|
|
260
|
+
try {
|
|
261
|
+
await bot.api.sendMessage(
|
|
262
|
+
ctx.chat!.id,
|
|
263
|
+
"⚠️ Unauthorized access. This bot is private. This attempt has been reported to the bot owner.",
|
|
264
|
+
);
|
|
265
|
+
} catch {
|
|
266
|
+
/* can't send — ignore */
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Notify admin
|
|
270
|
+
if (accessConfig.adminId) {
|
|
271
|
+
const detail =
|
|
272
|
+
type === "dm"
|
|
273
|
+
? `🚨 Unauthorized DM from ${sender}${username} [id:${userId}]`
|
|
274
|
+
: `🚨 Unauthorized group access: "${(ctx.chat as { title?: string })?.title ?? ctx.chat!.id}" [id:${ctx.chat!.id}] by ${sender}${username}`;
|
|
275
|
+
const detailWithBody = body ? `${detail}\n\n${body.slice(0, 400)}` : detail;
|
|
276
|
+
try {
|
|
277
|
+
await bot.api.sendMessage(accessConfig.adminId, detailWithBody);
|
|
278
|
+
} catch {
|
|
279
|
+
/* admin unreachable — ignore */
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
logWarn(
|
|
284
|
+
"access",
|
|
285
|
+
`Unauthorized ${type}: ${sender}${username} [id:${userId}] in chat ${ctx.chat!.id}`,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message-context helpers — sender name, reply/forward context strings, and
|
|
3
|
+
* Telegram file downloads (with image magic-byte validation).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Bot } from "grammy";
|
|
7
|
+
import type { TalonConfig } from "../../../util/config.js";
|
|
8
|
+
import { writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
9
|
+
import { resolve } from "node:path";
|
|
10
|
+
import { logWarn } from "../../../util/log.js";
|
|
11
|
+
|
|
12
|
+
export function getSenderName(
|
|
13
|
+
from: { first_name?: string; last_name?: string } | undefined,
|
|
14
|
+
): string {
|
|
15
|
+
return (
|
|
16
|
+
[from?.first_name, from?.last_name].filter(Boolean).join(" ") || "User"
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getReplyContext(
|
|
21
|
+
replyMsg:
|
|
22
|
+
| {
|
|
23
|
+
message_id?: number;
|
|
24
|
+
from?: { id: number; first_name?: string; last_name?: string };
|
|
25
|
+
text?: string;
|
|
26
|
+
caption?: string;
|
|
27
|
+
photo?: unknown[];
|
|
28
|
+
document?: unknown;
|
|
29
|
+
video?: unknown;
|
|
30
|
+
voice?: unknown;
|
|
31
|
+
audio?: unknown;
|
|
32
|
+
sticker?: unknown;
|
|
33
|
+
animation?: unknown;
|
|
34
|
+
}
|
|
35
|
+
| undefined,
|
|
36
|
+
botId: number,
|
|
37
|
+
quote?: { text?: string; is_manual?: boolean } | undefined,
|
|
38
|
+
): string {
|
|
39
|
+
if (!replyMsg) return "";
|
|
40
|
+
|
|
41
|
+
const author =
|
|
42
|
+
replyMsg.from?.id === botId
|
|
43
|
+
? "bot"
|
|
44
|
+
: [replyMsg.from?.first_name, replyMsg.from?.last_name]
|
|
45
|
+
.filter(Boolean)
|
|
46
|
+
.join(" ") || "User";
|
|
47
|
+
const text = replyMsg.text || replyMsg.caption || "";
|
|
48
|
+
const msgIdTag = replyMsg.message_id ? ` msg_id:${replyMsg.message_id}` : "";
|
|
49
|
+
|
|
50
|
+
// Detect media type
|
|
51
|
+
const mediaType = replyMsg.photo
|
|
52
|
+
? "photo"
|
|
53
|
+
: replyMsg.video
|
|
54
|
+
? "video"
|
|
55
|
+
: replyMsg.document
|
|
56
|
+
? "document"
|
|
57
|
+
: replyMsg.voice
|
|
58
|
+
? "voice"
|
|
59
|
+
: replyMsg.audio
|
|
60
|
+
? "audio"
|
|
61
|
+
: replyMsg.sticker
|
|
62
|
+
? "sticker"
|
|
63
|
+
: replyMsg.animation
|
|
64
|
+
? "animation"
|
|
65
|
+
: null;
|
|
66
|
+
const mediaPart = mediaType ? ` [${mediaType}]` : "";
|
|
67
|
+
|
|
68
|
+
// Build context — always include if there's a message_id (even if no text)
|
|
69
|
+
const textPart = text ? `: "${text.slice(0, 500)}"` : "";
|
|
70
|
+
|
|
71
|
+
// Telegram lets users highlight a specific portion of a message when
|
|
72
|
+
// replying (Bot API 7.0+). When present, surface it explicitly so the model
|
|
73
|
+
// sees which part the user is pointing at, not just the whole replied-to
|
|
74
|
+
// message. `is_manual=false` means Telegram chose the snippet automatically
|
|
75
|
+
// (long messages) — still useful signal, so include either way.
|
|
76
|
+
const quoteText = quote?.text?.trim();
|
|
77
|
+
const quotePart = quoteText
|
|
78
|
+
? `\n[Quoted portion: "${quoteText.slice(0, 500)}"]`
|
|
79
|
+
: "";
|
|
80
|
+
|
|
81
|
+
if (!textPart && !mediaPart && !msgIdTag && !quotePart) return "";
|
|
82
|
+
|
|
83
|
+
return `[Replying to ${author}${textPart}${mediaPart}${msgIdTag}]${quotePart}\n\n`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* If the replied-to message contains a photo, download it and return a prompt
|
|
88
|
+
* line pointing to the saved file so the model can see it. Returns "" if no photo.
|
|
89
|
+
*/
|
|
90
|
+
export async function downloadReplyPhoto(
|
|
91
|
+
replyMsg:
|
|
92
|
+
| {
|
|
93
|
+
photo?: {
|
|
94
|
+
file_id: string;
|
|
95
|
+
file_unique_id: string;
|
|
96
|
+
width?: number;
|
|
97
|
+
height?: number;
|
|
98
|
+
}[];
|
|
99
|
+
}
|
|
100
|
+
| undefined,
|
|
101
|
+
bot: Bot,
|
|
102
|
+
config: TalonConfig,
|
|
103
|
+
): Promise<string> {
|
|
104
|
+
if (!replyMsg?.photo?.length) return "";
|
|
105
|
+
try {
|
|
106
|
+
// Pick the largest photo size (last in array)
|
|
107
|
+
const bestPhoto = replyMsg.photo[replyMsg.photo.length - 1];
|
|
108
|
+
const savedPath = await downloadTelegramFile(
|
|
109
|
+
bot,
|
|
110
|
+
config,
|
|
111
|
+
bestPhoto.file_id,
|
|
112
|
+
`reply_photo_${bestPhoto.file_unique_id}.jpg`,
|
|
113
|
+
);
|
|
114
|
+
return `[Replied-to message contains a photo saved to: ${savedPath} — read it to view]\n`;
|
|
115
|
+
} catch (err) {
|
|
116
|
+
logWarn(
|
|
117
|
+
"bot",
|
|
118
|
+
`Failed to download reply photo: ${err instanceof Error ? err.message : err}`,
|
|
119
|
+
);
|
|
120
|
+
return "";
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function getForwardContext(msg: {
|
|
125
|
+
forward_origin?: {
|
|
126
|
+
type: string;
|
|
127
|
+
sender_user?: { first_name?: string; last_name?: string };
|
|
128
|
+
sender_user_name?: string;
|
|
129
|
+
chat?: { title?: string };
|
|
130
|
+
};
|
|
131
|
+
}): string {
|
|
132
|
+
const origin = msg.forward_origin;
|
|
133
|
+
if (!origin) return "";
|
|
134
|
+
let from = "someone";
|
|
135
|
+
if (origin.type === "user" && origin.sender_user) {
|
|
136
|
+
from = [origin.sender_user.first_name, origin.sender_user.last_name]
|
|
137
|
+
.filter(Boolean)
|
|
138
|
+
.join(" ");
|
|
139
|
+
} else if (origin.type === "hidden_user" && origin.sender_user_name) {
|
|
140
|
+
from = origin.sender_user_name;
|
|
141
|
+
} else if (
|
|
142
|
+
(origin.type === "channel" || origin.type === "chat") &&
|
|
143
|
+
origin.chat
|
|
144
|
+
) {
|
|
145
|
+
from = origin.chat.title || "a chat";
|
|
146
|
+
}
|
|
147
|
+
return `[Forwarded from ${from}]\n`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export async function downloadTelegramFile(
|
|
151
|
+
bot: Bot,
|
|
152
|
+
config: TalonConfig,
|
|
153
|
+
fileId: string,
|
|
154
|
+
fileName: string,
|
|
155
|
+
): Promise<string> {
|
|
156
|
+
const file = await bot.api.getFile(fileId);
|
|
157
|
+
if (!file.file_path) throw new Error("Could not get file path from Telegram");
|
|
158
|
+
|
|
159
|
+
const url = `https://api.telegram.org/file/bot${config.botToken}/${file.file_path}`;
|
|
160
|
+
const resp = await fetch(url);
|
|
161
|
+
if (!resp.ok) throw new Error(`Download failed: ${resp.status}`);
|
|
162
|
+
|
|
163
|
+
// Guard against excessively large files (50MB limit)
|
|
164
|
+
const MAX_DOWNLOAD_BYTES = 50 * 1024 * 1024;
|
|
165
|
+
const contentLength = resp.headers.get("content-length");
|
|
166
|
+
if (contentLength && parseInt(contentLength, 10) > MAX_DOWNLOAD_BYTES) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`File too large (${Math.round(parseInt(contentLength, 10) / 1024 / 1024)}MB, max 50MB)`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const buffer = Buffer.from(await resp.arrayBuffer());
|
|
173
|
+
if (buffer.length === 0)
|
|
174
|
+
throw new Error("Downloaded file is empty (0 bytes)");
|
|
175
|
+
|
|
176
|
+
// Validate image files — prevent saving HTML/garbage as .jpg/.png
|
|
177
|
+
// (corrupt "images" poison the session permanently on resume)
|
|
178
|
+
const imageExts = [".jpg", ".jpeg", ".png", ".gif", ".webp"];
|
|
179
|
+
const isImageExt = imageExts.some((ext) =>
|
|
180
|
+
fileName.toLowerCase().endsWith(ext),
|
|
181
|
+
);
|
|
182
|
+
if (isImageExt) {
|
|
183
|
+
const m = buffer.subarray(0, 16);
|
|
184
|
+
const validImage =
|
|
185
|
+
(m[0] === 0xff && m[1] === 0xd8) || // JPEG
|
|
186
|
+
(m[0] === 0x89 && m[1] === 0x50 && m[2] === 0x4e && m[3] === 0x47) || // PNG
|
|
187
|
+
(m[0] === 0x47 && m[1] === 0x49 && m[2] === 0x46) || // GIF
|
|
188
|
+
(m[0] === 0x52 &&
|
|
189
|
+
m[1] === 0x49 &&
|
|
190
|
+
m[2] === 0x46 &&
|
|
191
|
+
m[3] === 0x46 &&
|
|
192
|
+
m[8] === 0x57 &&
|
|
193
|
+
m[9] === 0x45 &&
|
|
194
|
+
m[10] === 0x42 &&
|
|
195
|
+
m[11] === 0x50); // WebP
|
|
196
|
+
if (!validImage) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
`File "${fileName}" has image extension but invalid content — not saving to prevent session corruption`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const uploadsDir = resolve(config.workspace, "uploads");
|
|
204
|
+
if (!existsSync(uploadsDir)) mkdirSync(uploadsDir, { recursive: true });
|
|
205
|
+
|
|
206
|
+
const safeName = `${Date.now()}-${fileName.replace(/[^a-zA-Z0-9._-]/g, "_")}`;
|
|
207
|
+
const destPath = resolve(uploadsDir, safeName);
|
|
208
|
+
writeFileSync(destPath, buffer);
|
|
209
|
+
return destPath;
|
|
210
|
+
}
|