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,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Access control — DM whitelist, guild/channel allowlists, admin checks,
|
|
3
|
+
* mention/respond gating, unauthorized notifications, DM-user tracking, and
|
|
4
|
+
* per-user rate limiting.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Client, Message } from "discord.js";
|
|
8
|
+
import { ChannelType } from "discord.js";
|
|
9
|
+
import { appendDailyLog } from "../../../storage/daily-log.js";
|
|
10
|
+
import { log, logWarn, logDebug } from "../../../util/log.js";
|
|
11
|
+
import { escapeMarkdown } from "../formatting.js";
|
|
12
|
+
import {
|
|
13
|
+
accessState,
|
|
14
|
+
knownDmUsers,
|
|
15
|
+
KNOWN_DM_USERS_CAP,
|
|
16
|
+
unauthorizedCooldown,
|
|
17
|
+
UNAUTHORIZED_COOLDOWN_MS,
|
|
18
|
+
MAX_UNAUTHORIZED_COOLDOWNS,
|
|
19
|
+
userMessageTimestamps,
|
|
20
|
+
RATE_LIMIT_WINDOW_MS,
|
|
21
|
+
RATE_LIMIT_MAX_MESSAGES,
|
|
22
|
+
type AccessConfig,
|
|
23
|
+
} from "./state.js";
|
|
24
|
+
|
|
25
|
+
export function setAccessControl(cfg: {
|
|
26
|
+
allowedUsers: string[];
|
|
27
|
+
allowedGuilds: string[];
|
|
28
|
+
allowedChannels: string[];
|
|
29
|
+
adminUserIds: string[];
|
|
30
|
+
respondMode: "mention" | "channel";
|
|
31
|
+
}): void {
|
|
32
|
+
accessState.access = {
|
|
33
|
+
allowedUsers: new Set(cfg.allowedUsers),
|
|
34
|
+
allowedGuilds: new Set(cfg.allowedGuilds),
|
|
35
|
+
allowedChannels: new Set(cfg.allowedChannels),
|
|
36
|
+
adminUserIds: new Set(cfg.adminUserIds),
|
|
37
|
+
respondMode: cfg.respondMode,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function isAdmin(userId: string): boolean {
|
|
42
|
+
return accessState.access.adminUserIds.has(userId);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getAccessSnapshot(): AccessConfig {
|
|
46
|
+
return accessState.access;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function trackDmUser(
|
|
50
|
+
senderId: string,
|
|
51
|
+
senderName: string,
|
|
52
|
+
tag?: string,
|
|
53
|
+
): void {
|
|
54
|
+
if (knownDmUsers.has(senderId)) return;
|
|
55
|
+
if (knownDmUsers.size >= KNOWN_DM_USERS_CAP) {
|
|
56
|
+
const evictCount = Math.floor(KNOWN_DM_USERS_CAP * 0.1);
|
|
57
|
+
const iter = knownDmUsers.values();
|
|
58
|
+
for (let i = 0; i < evictCount; i++) {
|
|
59
|
+
knownDmUsers.delete(iter.next().value as string);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
knownDmUsers.add(senderId);
|
|
63
|
+
const tagStr = tag ? ` (${tag})` : "";
|
|
64
|
+
log("users", `New DM user: ${senderName}${tagStr} [id:${senderId}]`);
|
|
65
|
+
appendDailyLog(
|
|
66
|
+
"System",
|
|
67
|
+
`New DM user: ${senderName}${tagStr} [id:${senderId}]`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function notifyUnauthorized(
|
|
72
|
+
client: Client,
|
|
73
|
+
msg: Message,
|
|
74
|
+
type: "dm" | "guild" | "channel",
|
|
75
|
+
): Promise<void> {
|
|
76
|
+
const userId = msg.author.id;
|
|
77
|
+
const guildId = msg.guildId ?? "";
|
|
78
|
+
const channelId = msg.channelId;
|
|
79
|
+
const key =
|
|
80
|
+
type === "dm"
|
|
81
|
+
? `dm:${userId}`
|
|
82
|
+
: type === "guild"
|
|
83
|
+
? `guild:${guildId}`
|
|
84
|
+
: `channel:${channelId}`;
|
|
85
|
+
const now = Date.now();
|
|
86
|
+
const last = unauthorizedCooldown.get(key);
|
|
87
|
+
if (last && now - last < UNAUTHORIZED_COOLDOWN_MS) return;
|
|
88
|
+
if (unauthorizedCooldown.size >= MAX_UNAUTHORIZED_COOLDOWNS) {
|
|
89
|
+
unauthorizedCooldown.clear();
|
|
90
|
+
}
|
|
91
|
+
unauthorizedCooldown.set(key, now);
|
|
92
|
+
|
|
93
|
+
const senderTag = msg.author.username ? ` (@${msg.author.username})` : "";
|
|
94
|
+
const senderName = msg.author.globalName || msg.author.username || "User";
|
|
95
|
+
|
|
96
|
+
// Warn the user
|
|
97
|
+
try {
|
|
98
|
+
if (msg.channel.isSendable()) {
|
|
99
|
+
await msg.channel.send({
|
|
100
|
+
content:
|
|
101
|
+
"⚠️ Unauthorized access. This bot is private. This attempt has been reported to the bot owner.",
|
|
102
|
+
allowedMentions: { parse: [] },
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
/* can't send — ignore */
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Notify admins via DM. User-supplied names (sender, guild, channel) are
|
|
110
|
+
// escaped so a nick containing ** or || can't deform the admin's message.
|
|
111
|
+
const safeSender = escapeMarkdown(`${senderName}${senderTag}`);
|
|
112
|
+
const safeGuildName = escapeMarkdown(msg.guild?.name ?? guildId);
|
|
113
|
+
const safeChannelName = escapeMarkdown(
|
|
114
|
+
(msg.channel as { name?: string }).name ?? channelId,
|
|
115
|
+
);
|
|
116
|
+
for (const adminId of accessState.access.adminUserIds) {
|
|
117
|
+
try {
|
|
118
|
+
const admin = await client.users.fetch(adminId);
|
|
119
|
+
const detail =
|
|
120
|
+
type === "dm"
|
|
121
|
+
? `🚨 Unauthorized DM from ${safeSender} [id:${userId}]`
|
|
122
|
+
: type === "guild"
|
|
123
|
+
? `🚨 Unauthorized guild access: "${safeGuildName}" [id:${guildId}] by ${safeSender}`
|
|
124
|
+
: `🚨 Unauthorized channel access: #${safeChannelName} in "${safeGuildName}" by ${safeSender}`;
|
|
125
|
+
await admin.send({ content: detail, allowedMentions: { parse: [] } });
|
|
126
|
+
} catch {
|
|
127
|
+
/* admin unreachable */
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
logWarn(
|
|
132
|
+
"access",
|
|
133
|
+
`Unauthorized ${type}: ${senderName}${senderTag} [id:${userId}] guild=${guildId} channel=${channelId}`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Check if a DM user is allowed. */
|
|
138
|
+
function isDmAllowed(userId: string): boolean {
|
|
139
|
+
return accessState.access.allowedUsers.has(userId);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Full access check for plain messages. Returns true if message should be processed.
|
|
144
|
+
* Notifies user + admins on rejection (with cooldown).
|
|
145
|
+
*/
|
|
146
|
+
export async function isAccessAllowed(
|
|
147
|
+
client: Client,
|
|
148
|
+
msg: Message,
|
|
149
|
+
): Promise<boolean> {
|
|
150
|
+
if (msg.channel.type === ChannelType.DM) {
|
|
151
|
+
if (isDmAllowed(msg.author.id)) return true;
|
|
152
|
+
await notifyUnauthorized(client, msg, "dm");
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
// Guild context
|
|
156
|
+
const guildId = msg.guildId;
|
|
157
|
+
if (!guildId) return false; // unknown — deny
|
|
158
|
+
|
|
159
|
+
if (!accessState.access.allowedGuilds.has(guildId)) {
|
|
160
|
+
await notifyUnauthorized(client, msg, "guild");
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
if (
|
|
164
|
+
accessState.access.allowedChannels.size > 0 &&
|
|
165
|
+
!accessState.access.allowedChannels.has(msg.channelId)
|
|
166
|
+
) {
|
|
167
|
+
await notifyUnauthorized(client, msg, "channel");
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* For interactions (slash commands, buttons): synchronous predicate without
|
|
175
|
+
* side effects, since the interaction has its own ephemeral reply path.
|
|
176
|
+
*/
|
|
177
|
+
export function isInteractionAllowed(
|
|
178
|
+
inGuild: boolean,
|
|
179
|
+
userId: string,
|
|
180
|
+
guildId: string | null,
|
|
181
|
+
channelId: string | null,
|
|
182
|
+
): { ok: true } | { ok: false; reason: string } {
|
|
183
|
+
if (!inGuild) {
|
|
184
|
+
if (isDmAllowed(userId)) return { ok: true };
|
|
185
|
+
return { ok: false, reason: "DM access not authorized." };
|
|
186
|
+
}
|
|
187
|
+
if (!guildId)
|
|
188
|
+
return { ok: false, reason: "Could not determine guild context." };
|
|
189
|
+
if (!accessState.access.allowedGuilds.has(guildId))
|
|
190
|
+
return { ok: false, reason: "This server is not authorized." };
|
|
191
|
+
if (
|
|
192
|
+
accessState.access.allowedChannels.size > 0 &&
|
|
193
|
+
channelId &&
|
|
194
|
+
!accessState.access.allowedChannels.has(channelId)
|
|
195
|
+
)
|
|
196
|
+
return { ok: false, reason: "This channel is not authorized." };
|
|
197
|
+
return { ok: true };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Whether a guild message should activate the bot.
|
|
202
|
+
* - mode "mention": only if @bot or reply-to-bot
|
|
203
|
+
* - mode "channel": any message inside an allowedChannels channel (whitelist
|
|
204
|
+
* must be set; otherwise we fall back to mention to avoid being noisy).
|
|
205
|
+
*/
|
|
206
|
+
export function shouldHandleInGuild(client: Client, msg: Message): boolean {
|
|
207
|
+
if (msg.channel.type === ChannelType.DM) return true;
|
|
208
|
+
|
|
209
|
+
const botId = client.user?.id;
|
|
210
|
+
const mentioned = botId ? msg.mentions.users.has(botId) : false;
|
|
211
|
+
const repliedToBot =
|
|
212
|
+
msg.reference?.messageId && msg.mentions.repliedUser?.id === botId;
|
|
213
|
+
|
|
214
|
+
if (
|
|
215
|
+
accessState.access.respondMode === "channel" &&
|
|
216
|
+
accessState.access.allowedChannels.size > 0
|
|
217
|
+
) {
|
|
218
|
+
return accessState.access.allowedChannels.has(msg.channelId);
|
|
219
|
+
}
|
|
220
|
+
return Boolean(mentioned || repliedToBot);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function isUserRateLimited(senderId: string): boolean {
|
|
224
|
+
const now = Date.now();
|
|
225
|
+
let timestamps = userMessageTimestamps.get(senderId);
|
|
226
|
+
if (!timestamps) {
|
|
227
|
+
timestamps = [];
|
|
228
|
+
userMessageTimestamps.set(senderId, timestamps);
|
|
229
|
+
}
|
|
230
|
+
while (timestamps.length > 0 && timestamps[0] < now - RATE_LIMIT_WINDOW_MS) {
|
|
231
|
+
timestamps.shift();
|
|
232
|
+
}
|
|
233
|
+
if (timestamps.length >= RATE_LIMIT_MAX_MESSAGES) {
|
|
234
|
+
logDebug("bot", `Rate-limited user ${senderId}`);
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
timestamps.push(now);
|
|
238
|
+
if (userMessageTimestamps.size > 5_000) {
|
|
239
|
+
const cutoff = now - 10 * 60_000;
|
|
240
|
+
for (const [userId, ts] of userMessageTimestamps) {
|
|
241
|
+
if (ts.length === 0 || ts[ts.length - 1] < cutoff) {
|
|
242
|
+
userMessageTimestamps.delete(userId);
|
|
243
|
+
}
|
|
244
|
+
if (userMessageTimestamps.size <= 2_500) break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return false;
|
|
248
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message-context helpers — sender name, reply context, attachment download +
|
|
3
|
+
* classification, and the chunked sender used by both replies and tool calls.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
7
|
+
import { resolve } from "node:path";
|
|
8
|
+
import type { Message, Attachment, TextBasedChannel } from "discord.js";
|
|
9
|
+
import {
|
|
10
|
+
splitMessage,
|
|
11
|
+
suppressMentions,
|
|
12
|
+
DISCORD_MAX_TEXT,
|
|
13
|
+
} from "../formatting.js";
|
|
14
|
+
|
|
15
|
+
export function getSenderName(msg: Message): string {
|
|
16
|
+
return (
|
|
17
|
+
(msg.member?.displayName || msg.author.globalName || msg.author.username) ??
|
|
18
|
+
"User"
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function buildReplyContext(
|
|
23
|
+
msg: Message,
|
|
24
|
+
botId: string | undefined,
|
|
25
|
+
): string {
|
|
26
|
+
const ref = msg.reference;
|
|
27
|
+
if (!ref || !ref.messageId) return "";
|
|
28
|
+
// We don't always have the message cached. Discord.js provides
|
|
29
|
+
// msg.fetchReference() but that's async; we keep this synchronous and rely
|
|
30
|
+
// on the partial info already available via mentions.repliedUser.
|
|
31
|
+
const replied = msg.mentions.repliedUser;
|
|
32
|
+
if (!replied) return `[Replying to msg_id:${ref.messageId}]\n\n`;
|
|
33
|
+
const author =
|
|
34
|
+
replied.id === botId
|
|
35
|
+
? "bot"
|
|
36
|
+
: replied.globalName || replied.username || "User";
|
|
37
|
+
return `[Replying to ${author} msg_id:${ref.messageId}]\n\n`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ── Attachment download ──────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
const ATTACHMENT_MAX_BYTES = 50 * 1024 * 1024;
|
|
43
|
+
|
|
44
|
+
export async function downloadAttachment(
|
|
45
|
+
attachment: Attachment,
|
|
46
|
+
workspace: string,
|
|
47
|
+
): Promise<string> {
|
|
48
|
+
if (attachment.size && attachment.size > ATTACHMENT_MAX_BYTES) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`File too large (${Math.round(attachment.size / 1024 / 1024)}MB, max ${ATTACHMENT_MAX_BYTES / 1024 / 1024}MB).`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
const resp = await fetch(attachment.url);
|
|
54
|
+
if (!resp.ok) throw new Error(`Download failed: ${resp.status}`);
|
|
55
|
+
const buffer = Buffer.from(await resp.arrayBuffer());
|
|
56
|
+
if (buffer.length === 0) throw new Error("Downloaded file is empty.");
|
|
57
|
+
|
|
58
|
+
const safeName = (attachment.name || `att_${attachment.id}`).replace(
|
|
59
|
+
/[^a-zA-Z0-9._-]/g,
|
|
60
|
+
"_",
|
|
61
|
+
);
|
|
62
|
+
const uploadsDir = resolve(workspace, "uploads");
|
|
63
|
+
if (!existsSync(uploadsDir)) mkdirSync(uploadsDir, { recursive: true });
|
|
64
|
+
const dest = resolve(uploadsDir, `${Date.now()}-${safeName}`);
|
|
65
|
+
writeFileSync(dest, buffer);
|
|
66
|
+
return dest;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function classifyAttachment(att: Attachment): {
|
|
70
|
+
type: "photo" | "document" | "voice" | "video" | "audio" | "animation";
|
|
71
|
+
promptLines: (savedPath: string) => string[];
|
|
72
|
+
} {
|
|
73
|
+
const ct = att.contentType ?? "";
|
|
74
|
+
const name = (att.name ?? "").toLowerCase();
|
|
75
|
+
|
|
76
|
+
if (ct.startsWith("image/") || /\.(png|jpe?g|webp|gif|bmp)$/.test(name)) {
|
|
77
|
+
if (ct === "image/gif" || name.endsWith(".gif")) {
|
|
78
|
+
return {
|
|
79
|
+
type: "animation",
|
|
80
|
+
promptLines: (p) => [
|
|
81
|
+
`User sent a GIF: "${att.name ?? att.id}".`,
|
|
82
|
+
`Saved to: ${p}`,
|
|
83
|
+
],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
type: "photo",
|
|
88
|
+
promptLines: (p) => [
|
|
89
|
+
`User sent an image saved to: ${p}`,
|
|
90
|
+
"Read this file to view it. If you need to reference this image in future turns, re-read the file — image data does not persist between turns.",
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
if (ct.startsWith("video/") || /\.(mp4|mov|webm|mkv)$/.test(name)) {
|
|
95
|
+
return {
|
|
96
|
+
type: "video",
|
|
97
|
+
promptLines: (p) => [
|
|
98
|
+
`User sent a video: "${att.name ?? att.id}".`,
|
|
99
|
+
`Saved to: ${p}`,
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (
|
|
104
|
+
ct === "audio/ogg" ||
|
|
105
|
+
/voice/.test(name) ||
|
|
106
|
+
(att.waveform != null && ct.startsWith("audio/"))
|
|
107
|
+
) {
|
|
108
|
+
return {
|
|
109
|
+
type: "voice",
|
|
110
|
+
promptLines: (p) => [
|
|
111
|
+
`User sent a voice message (${att.duration ?? "?"}s).`,
|
|
112
|
+
`Audio saved to: ${p}. You cannot transcribe audio — acknowledge it and respond based on context.`,
|
|
113
|
+
],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (ct.startsWith("audio/") || /\.(mp3|wav|flac|m4a|opus)$/.test(name)) {
|
|
117
|
+
return {
|
|
118
|
+
type: "audio",
|
|
119
|
+
promptLines: (p) => [
|
|
120
|
+
`User sent an audio file: "${att.name ?? att.id}".`,
|
|
121
|
+
`Saved to: ${p}`,
|
|
122
|
+
],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
type: "document",
|
|
127
|
+
promptLines: (p) => [
|
|
128
|
+
`User sent a document: "${att.name ?? att.id}" (${ct || "unknown"}).`,
|
|
129
|
+
`Saved to: ${p}`,
|
|
130
|
+
"Read and process this file.",
|
|
131
|
+
],
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ── Send helpers ─────────────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
export async function sendChunked(
|
|
138
|
+
channel: TextBasedChannel,
|
|
139
|
+
text: string,
|
|
140
|
+
replyToId?: string,
|
|
141
|
+
): Promise<string[]> {
|
|
142
|
+
const ids: string[] = [];
|
|
143
|
+
if (!text.trim()) return ids;
|
|
144
|
+
const chunks = splitMessage(suppressMentions(text), DISCORD_MAX_TEXT);
|
|
145
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
146
|
+
const chunk = chunks[i];
|
|
147
|
+
if (!channel.isSendable()) continue;
|
|
148
|
+
// Let errors propagate so withRetry can classify + retry transient failures.
|
|
149
|
+
// Previously we swallowed all errors here, making the outer withRetry dead code.
|
|
150
|
+
const sent = await channel.send({
|
|
151
|
+
content: chunk,
|
|
152
|
+
allowedMentions: { parse: [] },
|
|
153
|
+
reply:
|
|
154
|
+
i === 0 && replyToId
|
|
155
|
+
? { messageReference: replyToId, failIfNotExists: false }
|
|
156
|
+
: undefined,
|
|
157
|
+
});
|
|
158
|
+
ids.push(sent.id);
|
|
159
|
+
}
|
|
160
|
+
return ids;
|
|
161
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response delivery — runs the agent and delivers each text block as a
|
|
3
|
+
* discrete (chunked) Discord message. Discord has no draft-edit streaming, so
|
|
4
|
+
* blocks are sent as they arrive.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { TextBasedChannel } from "discord.js";
|
|
8
|
+
import type { TalonConfig } from "../../../util/config.js";
|
|
9
|
+
import { execute } from "../../../core/engine/dispatcher.js";
|
|
10
|
+
import { toolInputToRecord } from "../../../core/agent-runtime/events.js";
|
|
11
|
+
import { appendDailyLogResponse } from "../../../storage/daily-log.js";
|
|
12
|
+
import { log } from "../../../util/log.js";
|
|
13
|
+
import { trackDmUser } from "./access.js";
|
|
14
|
+
import { sendChunked } from "./context.js";
|
|
15
|
+
|
|
16
|
+
export type ProcessAndReplyParams = {
|
|
17
|
+
config: TalonConfig;
|
|
18
|
+
chatId: string;
|
|
19
|
+
numericChatId: number;
|
|
20
|
+
replyToId: string;
|
|
21
|
+
messageId: string;
|
|
22
|
+
numericMessageId: number;
|
|
23
|
+
prompt: string;
|
|
24
|
+
senderName: string;
|
|
25
|
+
isGroup: boolean;
|
|
26
|
+
senderUsername?: string;
|
|
27
|
+
senderId: string;
|
|
28
|
+
channel: TextBasedChannel;
|
|
29
|
+
chatTitle?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export async function processAndReply(p: ProcessAndReplyParams): Promise<void> {
|
|
33
|
+
const sentTextBlock = { value: false };
|
|
34
|
+
let firstChunkReplyTo: string | undefined = p.replyToId;
|
|
35
|
+
|
|
36
|
+
// Discord doesn't support draft message edits, so we use onTextBlock to
|
|
37
|
+
// deliver each text block as a discrete message (chunked if necessary).
|
|
38
|
+
const onTextBlock = async (text: string) => {
|
|
39
|
+
if (!text.trim()) return;
|
|
40
|
+
const ids = await sendChunked(p.channel, text, firstChunkReplyTo);
|
|
41
|
+
if (ids.length > 0) sentTextBlock.value = true;
|
|
42
|
+
// Subsequent blocks should not reply to the original — too noisy.
|
|
43
|
+
firstChunkReplyTo = undefined;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
if (!p.isGroup && p.senderId) {
|
|
47
|
+
trackDmUser(p.senderId, p.senderName, p.senderUsername);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const result = await execute({
|
|
51
|
+
chatId: p.chatId,
|
|
52
|
+
numericChatId: p.numericChatId,
|
|
53
|
+
prompt: p.prompt,
|
|
54
|
+
senderName: p.senderName,
|
|
55
|
+
isGroup: p.isGroup,
|
|
56
|
+
// Use the real Discord snowflake string, not the hashed numeric.
|
|
57
|
+
// The hash collides with Telegram-style 32-bit IDs and Discord's API
|
|
58
|
+
// rejects it as "Unknown Message" when the model tries to react/edit.
|
|
59
|
+
messageId: p.messageId,
|
|
60
|
+
source: "message",
|
|
61
|
+
onEvent: async (event) => {
|
|
62
|
+
switch (event.type) {
|
|
63
|
+
case "assistant_message":
|
|
64
|
+
// Throw on delivery failure — the dispatcher rejects the ack.
|
|
65
|
+
await onTextBlock(event.text);
|
|
66
|
+
break;
|
|
67
|
+
case "tool_call": {
|
|
68
|
+
const input = toolInputToRecord(event.name, event.input);
|
|
69
|
+
if (
|
|
70
|
+
event.name === "send" &&
|
|
71
|
+
input.type === "text" &&
|
|
72
|
+
typeof input.text === "string"
|
|
73
|
+
) {
|
|
74
|
+
appendDailyLogResponse("Talon", input.text, {
|
|
75
|
+
chatTitle: p.chatTitle,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (
|
|
85
|
+
result.bridgeMessageCount === 0 &&
|
|
86
|
+
!sentTextBlock.value &&
|
|
87
|
+
result.text?.trim()
|
|
88
|
+
) {
|
|
89
|
+
log(
|
|
90
|
+
"bot",
|
|
91
|
+
`Suppressed fallback text (${result.text.length} chars) — no send tool used`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Discord message handlers — mirrors the telegram handlers/ layout.
|
|
3
|
+
*
|
|
4
|
+
* - `state` — shared maps/sets (chat registry, access config, queues,
|
|
5
|
+
* rate-limit windows, cooldowns)
|
|
6
|
+
* - `registry` — numeric/string chatId → Discord channel info accessors
|
|
7
|
+
* - `access` — DM/guild/channel allowlists, admin checks, mention gating,
|
|
8
|
+
* unauthorized handling, rate limiting
|
|
9
|
+
* - `context` — sender/reply context, attachment download/classify,
|
|
10
|
+
* chunked send
|
|
11
|
+
* - `delivery` — the agent run + reply pipeline (processAndReply)
|
|
12
|
+
* - `queue` — per-chat debounce queue
|
|
13
|
+
* - `messages` — the messageCreate handler (handleMessage)
|
|
14
|
+
*
|
|
15
|
+
* Re-exports the same public surface the old single-file module exposed.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export type { DiscordChatInfo } from "./registry.js";
|
|
19
|
+
export {
|
|
20
|
+
registerDiscordChat,
|
|
21
|
+
lookupDiscordChat,
|
|
22
|
+
lookupDiscordChatByString,
|
|
23
|
+
} from "./registry.js";
|
|
24
|
+
export {
|
|
25
|
+
setAccessControl,
|
|
26
|
+
isAdmin,
|
|
27
|
+
getAccessSnapshot,
|
|
28
|
+
isAccessAllowed,
|
|
29
|
+
isInteractionAllowed,
|
|
30
|
+
shouldHandleInGuild,
|
|
31
|
+
isUserRateLimited,
|
|
32
|
+
} from "./access.js";
|
|
33
|
+
export { getSenderName, sendChunked } from "./context.js";
|
|
34
|
+
export { handleMessage } from "./messages.js";
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public message handler — the messageCreate entry point. Filters bots/self,
|
|
3
|
+
* applies access + rate limits, downloads attachments, builds the prompt, and
|
|
4
|
+
* enqueues for debounced processing.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Client, Message } from "discord.js";
|
|
8
|
+
import { ChannelType } from "discord.js";
|
|
9
|
+
import type { TalonConfig } from "../../../util/config.js";
|
|
10
|
+
import { setMessageFilePath } from "../../../storage/history.js";
|
|
11
|
+
import { addMedia } from "../../../storage/media-index.js";
|
|
12
|
+
import { deriveNumericChatId } from "../../../util/chat-id.js";
|
|
13
|
+
import { logError } from "../../../util/log.js";
|
|
14
|
+
import {
|
|
15
|
+
shouldHandleInGuild,
|
|
16
|
+
isAccessAllowed,
|
|
17
|
+
isUserRateLimited,
|
|
18
|
+
} from "./access.js";
|
|
19
|
+
import {
|
|
20
|
+
getSenderName,
|
|
21
|
+
buildReplyContext,
|
|
22
|
+
downloadAttachment,
|
|
23
|
+
classifyAttachment,
|
|
24
|
+
} from "./context.js";
|
|
25
|
+
import { registerDiscordChat } from "./registry.js";
|
|
26
|
+
import { enqueueMessage } from "./queue.js";
|
|
27
|
+
|
|
28
|
+
export async function handleMessage(
|
|
29
|
+
client: Client,
|
|
30
|
+
msg: Message,
|
|
31
|
+
config: TalonConfig,
|
|
32
|
+
): Promise<void> {
|
|
33
|
+
// Ignore bots, system messages, and our own messages
|
|
34
|
+
if (msg.author.bot || msg.system) return;
|
|
35
|
+
if (msg.author.id === client.user?.id) return;
|
|
36
|
+
|
|
37
|
+
if (!shouldHandleInGuild(client, msg)) return;
|
|
38
|
+
if (!(await isAccessAllowed(client, msg))) return;
|
|
39
|
+
if (isUserRateLimited(msg.author.id)) return;
|
|
40
|
+
|
|
41
|
+
const isGroup = msg.channel.type !== ChannelType.DM;
|
|
42
|
+
const chatId = isGroup
|
|
43
|
+
? `discord_guild_${msg.guildId}_${msg.channelId}`
|
|
44
|
+
: `discord_dm_${msg.author.id}`;
|
|
45
|
+
const numericChatId = deriveNumericChatId(chatId);
|
|
46
|
+
const numericMessageId = deriveNumericChatId(msg.id);
|
|
47
|
+
const sender = getSenderName(msg);
|
|
48
|
+
const senderUsername = msg.author.username;
|
|
49
|
+
const channel = msg.channel;
|
|
50
|
+
const chatTitle = isGroup
|
|
51
|
+
? `${msg.guild?.name ?? msg.guildId} #${(msg.channel as { name?: string }).name ?? msg.channelId}`
|
|
52
|
+
: undefined;
|
|
53
|
+
|
|
54
|
+
// Strip the leading bot mention from the text — common when users invoke the bot.
|
|
55
|
+
const botId = client.user?.id;
|
|
56
|
+
const mentionPattern = botId ? new RegExp(`<@!?${botId}>`, "g") : null;
|
|
57
|
+
const cleanedContent = mentionPattern
|
|
58
|
+
? msg.content.replace(mentionPattern, "").trim()
|
|
59
|
+
: msg.content.trim();
|
|
60
|
+
|
|
61
|
+
// Build prompt parts
|
|
62
|
+
const replyCtx = buildReplyContext(msg, botId);
|
|
63
|
+
const promptParts: string[] = [replyCtx];
|
|
64
|
+
|
|
65
|
+
// Handle attachments — download each, register in media index/history, add to prompt.
|
|
66
|
+
if (msg.attachments.size > 0) {
|
|
67
|
+
for (const att of msg.attachments.values()) {
|
|
68
|
+
try {
|
|
69
|
+
const savedPath = await downloadAttachment(att, config.workspace);
|
|
70
|
+
const cls = classifyAttachment(att);
|
|
71
|
+
setMessageFilePath(chatId, numericMessageId, savedPath);
|
|
72
|
+
addMedia({
|
|
73
|
+
chatId,
|
|
74
|
+
msgId: numericMessageId,
|
|
75
|
+
senderName: sender,
|
|
76
|
+
type: cls.type,
|
|
77
|
+
filePath: savedPath,
|
|
78
|
+
caption: cleanedContent || undefined,
|
|
79
|
+
timestamp: Date.now(),
|
|
80
|
+
});
|
|
81
|
+
promptParts.push(...cls.promptLines(savedPath));
|
|
82
|
+
} catch (err) {
|
|
83
|
+
logError(
|
|
84
|
+
"bot",
|
|
85
|
+
`[${chatId}] attachment "${att.name}" download failed: ${err instanceof Error ? err.message : err}`,
|
|
86
|
+
);
|
|
87
|
+
promptParts.push(
|
|
88
|
+
`[Attachment "${att.name ?? "file"}" failed to download: ${err instanceof Error ? err.message : err}]`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (cleanedContent) promptParts.push(`Caption: ${cleanedContent}`);
|
|
93
|
+
} else if (cleanedContent) {
|
|
94
|
+
promptParts.push(cleanedContent);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const prompt = promptParts.filter(Boolean).join("\n");
|
|
98
|
+
if (!prompt.trim()) return;
|
|
99
|
+
|
|
100
|
+
registerDiscordChat({
|
|
101
|
+
channelId: msg.channelId,
|
|
102
|
+
guildId: msg.guildId,
|
|
103
|
+
userId: isGroup ? null : msg.author.id,
|
|
104
|
+
numericChatId,
|
|
105
|
+
chatId,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
enqueueMessage(config, chatId, numericChatId, {
|
|
109
|
+
prompt,
|
|
110
|
+
replyToId: msg.id,
|
|
111
|
+
messageId: msg.id,
|
|
112
|
+
numericMessageId,
|
|
113
|
+
senderName: sender,
|
|
114
|
+
senderUsername,
|
|
115
|
+
senderId: msg.author.id,
|
|
116
|
+
isGroup,
|
|
117
|
+
channel,
|
|
118
|
+
chatTitle,
|
|
119
|
+
});
|
|
120
|
+
}
|