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
|
@@ -12,11 +12,13 @@ import type {
|
|
|
12
12
|
SDKPartialAssistantMessage,
|
|
13
13
|
SDKAssistantMessage,
|
|
14
14
|
SDKResultMessage,
|
|
15
|
+
SDKUserMessage,
|
|
15
16
|
ModelUsage,
|
|
16
17
|
} from "@anthropic-ai/claude-agent-sdk";
|
|
17
18
|
import type { BetaRawContentBlockDeltaEvent } from "@anthropic-ai/sdk/resources/beta/messages/messages.mjs";
|
|
18
19
|
import { STREAM_INTERVAL } from "./constants.js";
|
|
19
20
|
import { log } from "../../util/log.js";
|
|
21
|
+
import { checkModelDrift } from "./model-drift.js";
|
|
20
22
|
|
|
21
23
|
// ── Stream state accumulator ────────────────────────────────────────────────
|
|
22
24
|
|
|
@@ -61,6 +63,29 @@ export type StreamState = {
|
|
|
61
63
|
* may have appeared in the same assistant message before the terminator.
|
|
62
64
|
*/
|
|
63
65
|
turnTerminated: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Per-token text chunks accumulated since the last throttled flush.
|
|
68
|
+
* Drained into a `text_delta` event when `processStreamDelta` decides
|
|
69
|
+
* the throttle interval has elapsed.
|
|
70
|
+
*/
|
|
71
|
+
unflushedTextDelta: string;
|
|
72
|
+
/**
|
|
73
|
+
* Same as `unflushedTextDelta` but for thinking-phase tokens — drained
|
|
74
|
+
* into a `reasoning` event. Tracked separately so a thinking burst
|
|
75
|
+
* doesn't poison the visible-text delta buffer.
|
|
76
|
+
*/
|
|
77
|
+
unflushedThinkingDelta: string;
|
|
78
|
+
/**
|
|
79
|
+
* Set when the SDK's final result message reported a failed turn —
|
|
80
|
+
* either an error subtype (`error_during_execution`, `error_max_turns`,
|
|
81
|
+
* …) or `is_error: true` on a `success` result. The latter is how API
|
|
82
|
+
* errors surface: the SDK converts them (usage limits, 429s, auth
|
|
83
|
+
* failures) into a synthetic assistant message and finishes the turn
|
|
84
|
+
* "successfully" with the error text in `result` — it never throws.
|
|
85
|
+
* Carries the best error text available so the handler can fail the
|
|
86
|
+
* turn with the REAL message instead of treating it as a normal reply.
|
|
87
|
+
*/
|
|
88
|
+
resultErrorText: string | undefined;
|
|
64
89
|
};
|
|
65
90
|
|
|
66
91
|
export function createStreamState(): StreamState {
|
|
@@ -80,6 +105,9 @@ export function createStreamState(): StreamState {
|
|
|
80
105
|
lastTrailingText: "",
|
|
81
106
|
deliveredTextNorms: [],
|
|
82
107
|
turnTerminated: false,
|
|
108
|
+
unflushedTextDelta: "",
|
|
109
|
+
unflushedThinkingDelta: "",
|
|
110
|
+
resultErrorText: undefined,
|
|
83
111
|
};
|
|
84
112
|
}
|
|
85
113
|
|
|
@@ -99,47 +127,75 @@ export function isAssistant(msg: SDKMessage): msg is SDKAssistantMessage {
|
|
|
99
127
|
return msg.type === "assistant";
|
|
100
128
|
}
|
|
101
129
|
|
|
130
|
+
export function isUserMessage(msg: SDKMessage): msg is SDKUserMessage {
|
|
131
|
+
return msg.type === "user" && "message" in msg;
|
|
132
|
+
}
|
|
133
|
+
|
|
102
134
|
export function isResult(msg: SDKMessage): msg is SDKResultMessage {
|
|
103
135
|
return msg.type === "result";
|
|
104
136
|
}
|
|
105
137
|
|
|
106
138
|
// ── Message processors ──────────────────────────────────────────────────────
|
|
107
139
|
|
|
140
|
+
/** Output of `processStreamDelta` when the throttle interval has elapsed. */
|
|
141
|
+
export type StreamDeltaEmit =
|
|
142
|
+
{ phase: "text"; text: string } | { phase: "thinking"; text: string };
|
|
143
|
+
|
|
108
144
|
/**
|
|
109
|
-
* Process a streaming delta event — accumulates
|
|
110
|
-
*
|
|
145
|
+
* Process a streaming delta event — accumulates per-token chunks
|
|
146
|
+
* into `state.currentBlockText` (text) / unflushed buffers, and
|
|
147
|
+
* returns the chunk to emit when the throttle interval has elapsed.
|
|
148
|
+
*
|
|
149
|
+
* Returns `null` when nothing should be emitted yet (either the delta
|
|
150
|
+
* wasn't a content-block-delta, or the throttle window is still open).
|
|
151
|
+
* Callers yield a `text_delta` / `reasoning` event with the returned
|
|
152
|
+
* `text`. The throttle keeps the event volume bounded — Telegram /
|
|
153
|
+
* terminal renderers re-accumulate, but at human-readable cadence.
|
|
111
154
|
*/
|
|
112
155
|
export function processStreamDelta(
|
|
113
156
|
msg: SDKPartialAssistantMessage,
|
|
114
157
|
state: StreamState,
|
|
115
|
-
|
|
116
|
-
): void {
|
|
117
|
-
if (!onStreamDelta) return;
|
|
118
|
-
|
|
158
|
+
): StreamDeltaEmit | null {
|
|
119
159
|
const event = msg.event;
|
|
120
|
-
if (event.type !== "content_block_delta") return;
|
|
160
|
+
if (event.type !== "content_block_delta") return null;
|
|
121
161
|
|
|
122
162
|
const deltaEvent = event as BetaRawContentBlockDeltaEvent;
|
|
123
163
|
const delta = deltaEvent.delta;
|
|
124
164
|
|
|
125
165
|
if (delta.type === "thinking_delta") {
|
|
166
|
+
// The SDK's typed thinking_delta carries `.thinking`; treat
|
|
167
|
+
// missing/non-string defensively so a schema drift can't crash
|
|
168
|
+
// the loop.
|
|
169
|
+
const chunk =
|
|
170
|
+
typeof (delta as { thinking?: unknown }).thinking === "string"
|
|
171
|
+
? (delta as { thinking: string }).thinking
|
|
172
|
+
: "";
|
|
173
|
+
if (chunk) state.unflushedThinkingDelta += chunk;
|
|
126
174
|
const now = Date.now();
|
|
127
175
|
if (now - state.lastStreamUpdate >= STREAM_INTERVAL) {
|
|
128
176
|
state.lastStreamUpdate = now;
|
|
129
|
-
|
|
177
|
+
const out = state.unflushedThinkingDelta;
|
|
178
|
+
state.unflushedThinkingDelta = "";
|
|
179
|
+
if (out.length > 0) return { phase: "thinking", text: out };
|
|
130
180
|
}
|
|
131
181
|
} else if (delta.type === "text_delta") {
|
|
132
182
|
state.currentBlockText += delta.text;
|
|
183
|
+
state.unflushedTextDelta += delta.text;
|
|
133
184
|
const now = Date.now();
|
|
134
185
|
if (now - state.lastStreamUpdate >= STREAM_INTERVAL) {
|
|
135
186
|
state.lastStreamUpdate = now;
|
|
136
|
-
|
|
187
|
+
const out = state.unflushedTextDelta;
|
|
188
|
+
state.unflushedTextDelta = "";
|
|
189
|
+
if (out.length > 0) return { phase: "text", text: out };
|
|
137
190
|
}
|
|
138
191
|
}
|
|
192
|
+
return null;
|
|
139
193
|
}
|
|
140
194
|
|
|
141
195
|
/** A tool call extracted from an assistant message. */
|
|
142
196
|
export type ToolCall = {
|
|
197
|
+
/** The SDK's tool_use block id — matches the later tool_result. */
|
|
198
|
+
id: string;
|
|
143
199
|
name: string;
|
|
144
200
|
input: Record<string, unknown>;
|
|
145
201
|
};
|
|
@@ -181,7 +237,7 @@ export function processAssistantMessage(
|
|
|
181
237
|
typeof block.input === "object" && block.input !== null
|
|
182
238
|
? (block.input as Record<string, unknown>)
|
|
183
239
|
: {};
|
|
184
|
-
tools.push({ name: block.name, input });
|
|
240
|
+
tools.push({ id: block.id, name: block.name, input });
|
|
185
241
|
// Text before this tool call is a progress message
|
|
186
242
|
if (blockText.trim()) {
|
|
187
243
|
progressTexts.push(blockText.trim());
|
|
@@ -201,6 +257,62 @@ export function processAssistantMessage(
|
|
|
201
257
|
return { progressTexts, tools, trailingText };
|
|
202
258
|
}
|
|
203
259
|
|
|
260
|
+
/** One completed tool execution, extracted from an SDK user message. */
|
|
261
|
+
export type ToolResultInfo = {
|
|
262
|
+
toolUseId: string;
|
|
263
|
+
/** Present when the tool failed — first ~500 chars of the error text. */
|
|
264
|
+
error?: string;
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Extract tool results from an SDK user message. Tool executions come
|
|
269
|
+
* back as `tool_result` content blocks (keyed by `tool_use_id`) on
|
|
270
|
+
* synthetic user messages — the missing half of the tool lifecycle:
|
|
271
|
+
* without these, a UI that shows a spinner on `tool_call` never gets
|
|
272
|
+
* the event that stops it.
|
|
273
|
+
*/
|
|
274
|
+
export function extractToolResults(msg: SDKUserMessage): ToolResultInfo[] {
|
|
275
|
+
const content = msg.message?.content;
|
|
276
|
+
if (!Array.isArray(content)) return [];
|
|
277
|
+
const results: ToolResultInfo[] = [];
|
|
278
|
+
for (const block of content) {
|
|
279
|
+
if (
|
|
280
|
+
!block ||
|
|
281
|
+
typeof block !== "object" ||
|
|
282
|
+
(block as { type?: string }).type !== "tool_result"
|
|
283
|
+
) {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const b = block as {
|
|
287
|
+
tool_use_id?: string;
|
|
288
|
+
is_error?: boolean;
|
|
289
|
+
content?: unknown;
|
|
290
|
+
};
|
|
291
|
+
if (typeof b.tool_use_id !== "string") continue;
|
|
292
|
+
let error: string | undefined;
|
|
293
|
+
if (b.is_error) {
|
|
294
|
+
const text =
|
|
295
|
+
typeof b.content === "string"
|
|
296
|
+
? b.content
|
|
297
|
+
: Array.isArray(b.content)
|
|
298
|
+
? b.content
|
|
299
|
+
.filter(
|
|
300
|
+
(c): c is { type: "text"; text: string } =>
|
|
301
|
+
!!c &&
|
|
302
|
+
typeof c === "object" &&
|
|
303
|
+
(c as { type?: string }).type === "text" &&
|
|
304
|
+
typeof (c as { text?: unknown }).text === "string",
|
|
305
|
+
)
|
|
306
|
+
.map((c) => c.text)
|
|
307
|
+
.join("\n")
|
|
308
|
+
: "";
|
|
309
|
+
error = (text || "tool failed").slice(0, 500);
|
|
310
|
+
}
|
|
311
|
+
results.push({ toolUseId: b.tool_use_id, error });
|
|
312
|
+
}
|
|
313
|
+
return results;
|
|
314
|
+
}
|
|
315
|
+
|
|
204
316
|
/**
|
|
205
317
|
* Process the final result message — extracts token counts, context info,
|
|
206
318
|
* and API call counts from the typed SDK result.
|
|
@@ -227,6 +339,11 @@ export function processResultMessage(
|
|
|
227
339
|
// and contains cumulative session totals per model — summing all entries
|
|
228
340
|
// double-counts when switching models mid-session.
|
|
229
341
|
const modelUsage: Record<string, ModelUsage> = msg.modelUsage;
|
|
342
|
+
// Drift check: modelUsage is keyed by the model that ACTUALLY served
|
|
343
|
+
// the turn. If none of the keys is (an alias of) the model we asked
|
|
344
|
+
// for, the API substituted — warn instead of burying it in the
|
|
345
|
+
// accounting line.
|
|
346
|
+
checkModelDrift(sdkModel, Object.keys(modelUsage ?? {}));
|
|
230
347
|
const mu = modelUsage[sdkModel] ?? Object.values(modelUsage).at(-1);
|
|
231
348
|
if (mu) {
|
|
232
349
|
state.sdkInputTokens = mu.inputTokens ?? 0;
|
|
@@ -253,41 +370,32 @@ export function processResultMessage(
|
|
|
253
370
|
) {
|
|
254
371
|
state.currentBlockText = msg.result;
|
|
255
372
|
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// ── Trailing-text fallback dedup ────────────────────────────────────────────
|
|
259
373
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
.
|
|
271
|
-
|
|
374
|
+
// Failed turn. Two shapes (see StreamState.resultErrorText):
|
|
375
|
+
// - error subtype: no `result` field, but `errors[]` carries diagnostics.
|
|
376
|
+
// - success subtype with is_error: `result` (and the trailing assistant
|
|
377
|
+
// text) IS the API error message, e.g. "You've hit your weekly limit
|
|
378
|
+
// · resets Jul 10, 9am". Prefer that text — it's the user-facing one.
|
|
379
|
+
if (msg.subtype !== "success") {
|
|
380
|
+
const diagnostics = (msg.errors ?? [])
|
|
381
|
+
.filter((e) => typeof e === "string" && !e.startsWith("[ede_diagnostic]"))
|
|
382
|
+
.join("; ")
|
|
383
|
+
.slice(0, 500);
|
|
384
|
+
state.resultErrorText =
|
|
385
|
+
state.lastTrailingText.trim() ||
|
|
386
|
+
diagnostics ||
|
|
387
|
+
`Claude SDK turn failed (${msg.subtype})`;
|
|
388
|
+
} else if (msg.is_error) {
|
|
389
|
+
state.resultErrorText =
|
|
390
|
+
(typeof msg.result === "string" && msg.result.trim()) ||
|
|
391
|
+
state.lastTrailingText.trim() ||
|
|
392
|
+
"Claude SDK turn failed (API error)";
|
|
393
|
+
}
|
|
272
394
|
}
|
|
273
395
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
* dropping short legitimate replies.
|
|
281
|
-
*/
|
|
282
|
-
export function isDuplicateOfDelivered(
|
|
283
|
-
candidate: string,
|
|
284
|
-
deliveredNorms: string[],
|
|
285
|
-
): boolean {
|
|
286
|
-
if (deliveredNorms.length === 0) return false;
|
|
287
|
-
const norm = normalizeForDedupe(candidate);
|
|
288
|
-
if (norm.length < MIN_DEDUP_LENGTH) return false;
|
|
289
|
-
return deliveredNorms.some(
|
|
290
|
-
(d) =>
|
|
291
|
-
d.length >= MIN_DEDUP_LENGTH && (norm.includes(d) || d.includes(norm)),
|
|
292
|
-
);
|
|
293
|
-
}
|
|
396
|
+
// ── Trailing-text fallback dedup ────────────────────────────────────────────
|
|
397
|
+
// Re-export from shared so legacy imports do not grow a second implementation.
|
|
398
|
+
export {
|
|
399
|
+
normalizeForDedupe,
|
|
400
|
+
isDuplicateOfDelivered,
|
|
401
|
+
} from "../shared/delivered-text.js";
|
|
@@ -23,10 +23,18 @@ export async function warmSession(chatId: string): Promise<void> {
|
|
|
23
23
|
|
|
24
24
|
const abort = new AbortController();
|
|
25
25
|
try {
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
// Snapshot the prompt under this chat's fresh session epoch. The
|
|
27
|
+
// first real turn finds the same snapshot (same epoch) and sends a
|
|
28
|
+
// byte-identical prompt — so the warm-up's cache write is actually
|
|
29
|
+
// reused instead of being invalidated by a re-timestamped rebuild.
|
|
30
|
+
const session = getSession(chatId);
|
|
31
|
+
const prepared = prepareSystemPrompt({
|
|
32
|
+
config: getConfig(),
|
|
33
|
+
previousTurns: session.turns,
|
|
34
|
+
chatId,
|
|
35
|
+
sessionEpoch: session.createdAt,
|
|
36
|
+
});
|
|
37
|
+
const { options } = buildSdkOptions(chatId, undefined, undefined, prepared);
|
|
30
38
|
|
|
31
39
|
// Streaming input mode: pass an async iterable that never yields a user message
|
|
32
40
|
const neverYield = async function* (): AsyncGenerator<never> {
|
|
@@ -69,7 +77,6 @@ export async function warmSession(chatId: string): Promise<void> {
|
|
|
69
77
|
} finally {
|
|
70
78
|
if (timeoutId !== undefined) clearTimeout(timeoutId);
|
|
71
79
|
}
|
|
72
|
-
const session = getSession(chatId);
|
|
73
80
|
if (ctx.maxTokens > 0) session.usage.contextWindow = ctx.maxTokens;
|
|
74
81
|
if (ctx.totalTokens > 0) session.usage.contextTokens = ctx.totalTokens;
|
|
75
82
|
log(
|
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
* The Codex CLI supports two distinct authentication modes, with
|
|
5
5
|
* materially different capabilities:
|
|
6
6
|
*
|
|
7
|
-
* - **API key** (`
|
|
8
|
-
* SDK's `Codex` constructor, or
|
|
9
|
-
* `~/.codex/auth.json`). Full model
|
|
10
|
-
*
|
|
7
|
+
* - **API key** (`CODEX_API_KEY` / `TALON_CODEX_KEY` env var,
|
|
8
|
+
* `codexApiKey` passed to the SDK's `Codex` constructor, or
|
|
9
|
+
* `OPENAI_API_KEY` stored in `~/.codex/auth.json`). Full model
|
|
10
|
+
* catalog access, billed via the configured OpenAI-compatible API.
|
|
11
|
+
* `gpt-5-codex` is the flagship on OpenAI's native endpoint.
|
|
11
12
|
*
|
|
12
13
|
* - **ChatGPT OAuth** (`~/.codex/auth.json` `auth_mode: "chatgpt"`
|
|
13
14
|
* after running `codex login`). Restricted to a subset of models —
|
|
@@ -22,12 +23,21 @@
|
|
|
22
23
|
*
|
|
23
24
|
* Detection order (each step short-circuits if it succeeds):
|
|
24
25
|
*
|
|
25
|
-
* 1. `
|
|
26
|
-
* 2. `
|
|
27
|
-
* 3.
|
|
26
|
+
* 1. `CODEX_API_KEY` env var → `"api-key"` (Codex CLI convention).
|
|
27
|
+
* 2. `TALON_CODEX_KEY` env var → `"api-key"` (Talon-scoped alias).
|
|
28
|
+
* 3. `codexApiKey` in Talon config → `"api-key"`.
|
|
29
|
+
* Any Codex-specific API key is paired with `openaiBaseUrl` when
|
|
30
|
+
* configured; the SDK maps that to Codex's `openai_base_url`
|
|
31
|
+
* override.
|
|
32
|
+
* 4. `~/.codex/auth.json` exists + `auth_mode` field is read → returns
|
|
28
33
|
* that value (`"chatgpt"` for OAuth, `"api-key"` if the JSON also
|
|
29
34
|
* has a non-null `OPENAI_API_KEY`).
|
|
30
|
-
*
|
|
35
|
+
* 5. `OPENAI_API_KEY` env var → `"api-key"` as a generic fallback.
|
|
36
|
+
* 6. `openaiApiKey` in Talon config → `"api-key"` as a legacy fallback.
|
|
37
|
+
* These shared OpenAI credentials are deliberately after the Codex
|
|
38
|
+
* auth file so other Talon backends can keep their credentials
|
|
39
|
+
* without hijacking a logged-in Codex CLI.
|
|
40
|
+
* 7. Nothing → `"none"`. First turn will fail with an auth error;
|
|
31
41
|
* caller emits a startup warning pointing to `codex login`.
|
|
32
42
|
*/
|
|
33
43
|
|
|
@@ -37,55 +47,157 @@ import { join } from "node:path";
|
|
|
37
47
|
/** Detected auth mode. */
|
|
38
48
|
export type CodexAuthMode = "api-key" | "chatgpt" | "none";
|
|
39
49
|
|
|
50
|
+
export type CodexApiKeySource =
|
|
51
|
+
| "env:CODEX_API_KEY"
|
|
52
|
+
| "env:TALON_CODEX_KEY"
|
|
53
|
+
| "config:codexApiKey"
|
|
54
|
+
| "env:OPENAI_API_KEY"
|
|
55
|
+
| "config:openaiApiKey";
|
|
56
|
+
|
|
40
57
|
/** Resolved auth state plus diagnostics for the startup log. */
|
|
41
58
|
export interface CodexAuthInfo {
|
|
42
59
|
mode: CodexAuthMode;
|
|
43
60
|
/** Where the credential was found, for logging. */
|
|
44
|
-
source:
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
61
|
+
source: CodexApiKeySource | "file:~/.codex/auth.json" | "missing";
|
|
62
|
+
/** API key to pass into the Codex SDK, when auth is explicit. */
|
|
63
|
+
apiKey?: string;
|
|
64
|
+
/** Base URL to pass into the Codex SDK alongside explicit API-key auth. */
|
|
65
|
+
baseUrl?: string;
|
|
49
66
|
/** Path to the auth file when present (for diagnostics). */
|
|
50
67
|
authFilePath?: string;
|
|
51
68
|
/** Whether the auth file (if present) parsed correctly. */
|
|
52
69
|
authFileParsed: boolean;
|
|
53
70
|
/** Raw parse error when the file existed but couldn't be parsed. */
|
|
54
71
|
parseError?: string;
|
|
72
|
+
/** Diagnostics about credential resolution. */
|
|
73
|
+
diagnostics: string[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface DetectCodexAuthInput {
|
|
77
|
+
codexApiKey?: string;
|
|
78
|
+
openaiApiKey?: string;
|
|
79
|
+
openaiBaseUrl?: string;
|
|
80
|
+
env?: NodeJS.ProcessEnv;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function normalizeCodexBaseUrl(
|
|
84
|
+
baseUrl: string | undefined,
|
|
85
|
+
): string | undefined {
|
|
86
|
+
const trimmed = baseUrl?.trim();
|
|
87
|
+
if (!trimmed) return undefined;
|
|
88
|
+
return trimmed;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function normalizeSecret(value: string | undefined): string | undefined {
|
|
92
|
+
const trimmed = value?.trim();
|
|
93
|
+
if (!trimmed) return undefined;
|
|
94
|
+
return trimmed;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function resolveCodexApiKey(input: DetectCodexAuthInput): {
|
|
98
|
+
apiKey?: string;
|
|
99
|
+
baseUrl?: string;
|
|
100
|
+
source?: CodexApiKeySource;
|
|
101
|
+
diagnostics: string[];
|
|
102
|
+
} {
|
|
103
|
+
const env = input.env ?? process.env;
|
|
104
|
+
const baseUrl = normalizeCodexBaseUrl(input.openaiBaseUrl);
|
|
105
|
+
const diagnostics: string[] = [];
|
|
106
|
+
|
|
107
|
+
const codexApiKeyEnv = normalizeSecret(env.CODEX_API_KEY);
|
|
108
|
+
if (codexApiKeyEnv) {
|
|
109
|
+
return {
|
|
110
|
+
apiKey: codexApiKeyEnv,
|
|
111
|
+
baseUrl,
|
|
112
|
+
source: "env:CODEX_API_KEY",
|
|
113
|
+
diagnostics,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const talonCodexKeyEnv = normalizeSecret(env.TALON_CODEX_KEY);
|
|
118
|
+
if (talonCodexKeyEnv) {
|
|
119
|
+
return {
|
|
120
|
+
apiKey: talonCodexKeyEnv,
|
|
121
|
+
baseUrl,
|
|
122
|
+
source: "env:TALON_CODEX_KEY",
|
|
123
|
+
diagnostics,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const configCodexApiKey = normalizeSecret(input.codexApiKey);
|
|
128
|
+
if (configCodexApiKey) {
|
|
129
|
+
return {
|
|
130
|
+
apiKey: configCodexApiKey,
|
|
131
|
+
baseUrl,
|
|
132
|
+
source: "config:codexApiKey",
|
|
133
|
+
diagnostics,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { diagnostics };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function resolveGenericOpenAiApiKey(input: DetectCodexAuthInput): {
|
|
141
|
+
apiKey?: string;
|
|
142
|
+
baseUrl?: string;
|
|
143
|
+
source?: Extract<
|
|
144
|
+
CodexApiKeySource,
|
|
145
|
+
"env:OPENAI_API_KEY" | "config:openaiApiKey"
|
|
146
|
+
>;
|
|
147
|
+
diagnostics: string[];
|
|
148
|
+
} {
|
|
149
|
+
const env = input.env ?? process.env;
|
|
150
|
+
const baseUrl = normalizeCodexBaseUrl(input.openaiBaseUrl);
|
|
151
|
+
const diagnostics: string[] = [];
|
|
152
|
+
|
|
153
|
+
const openAiApiKeyEnv = normalizeSecret(env.OPENAI_API_KEY);
|
|
154
|
+
if (openAiApiKeyEnv) {
|
|
155
|
+
return {
|
|
156
|
+
apiKey: openAiApiKeyEnv,
|
|
157
|
+
baseUrl,
|
|
158
|
+
source: "env:OPENAI_API_KEY",
|
|
159
|
+
diagnostics,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const configOpenAiApiKey = normalizeSecret(input.openaiApiKey);
|
|
164
|
+
if (configOpenAiApiKey) {
|
|
165
|
+
return {
|
|
166
|
+
apiKey: configOpenAiApiKey,
|
|
167
|
+
baseUrl,
|
|
168
|
+
source: "config:openaiApiKey",
|
|
169
|
+
diagnostics,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { diagnostics };
|
|
55
174
|
}
|
|
56
175
|
|
|
57
176
|
/**
|
|
58
177
|
* Detect the active Codex auth mode.
|
|
59
178
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* the config module. `envOverride` defaults to `process.env` so tests
|
|
63
|
-
* can substitute a frozen env map.
|
|
179
|
+
* All config values are passed in by the caller so this module doesn't
|
|
180
|
+
* depend on the config module.
|
|
64
181
|
*/
|
|
65
182
|
export function detectCodexAuth(
|
|
66
|
-
|
|
67
|
-
envOverride: NodeJS.ProcessEnv = process.env,
|
|
183
|
+
input: DetectCodexAuthInput = {},
|
|
68
184
|
): CodexAuthInfo {
|
|
69
|
-
|
|
70
|
-
|
|
185
|
+
const env = input.env ?? process.env;
|
|
186
|
+
const explicitKey = resolveCodexApiKey(input);
|
|
187
|
+
if (explicitKey.apiKey && explicitKey.source) {
|
|
71
188
|
return {
|
|
72
189
|
mode: "api-key",
|
|
73
|
-
source:
|
|
190
|
+
source: explicitKey.source,
|
|
191
|
+
apiKey: explicitKey.apiKey,
|
|
192
|
+
baseUrl: explicitKey.baseUrl,
|
|
74
193
|
authFileParsed: false,
|
|
194
|
+
diagnostics: explicitKey.diagnostics,
|
|
75
195
|
};
|
|
76
196
|
}
|
|
77
197
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
mode: "api-key",
|
|
82
|
-
source: "config:openaiApiKey",
|
|
83
|
-
authFileParsed: false,
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// 3. Codex CLI auth file (created by `codex login`).
|
|
88
|
-
const home = envOverride.HOME ?? envOverride.USERPROFILE;
|
|
198
|
+
let authFileResult: CodexAuthInfo | null = null;
|
|
199
|
+
// Codex CLI auth file (created by `codex login`).
|
|
200
|
+
const home = env.HOME ?? env.USERPROFILE;
|
|
89
201
|
if (home) {
|
|
90
202
|
const authFilePath = join(home, ".codex", "auth.json");
|
|
91
203
|
if (existsSync(authFilePath)) {
|
|
@@ -98,15 +210,18 @@ export function detectCodexAuth(
|
|
|
98
210
|
// The auth.json schema includes `OPENAI_API_KEY` as a top-level
|
|
99
211
|
// field even on ChatGPT-mode installs; when it's non-null it
|
|
100
212
|
// takes precedence (mirrors what the CLI itself does).
|
|
101
|
-
|
|
102
|
-
parsed.OPENAI_API_KEY &&
|
|
213
|
+
const fileApiKey =
|
|
103
214
|
typeof parsed.OPENAI_API_KEY === "string"
|
|
104
|
-
|
|
215
|
+
? normalizeSecret(parsed.OPENAI_API_KEY)
|
|
216
|
+
: undefined;
|
|
217
|
+
if (fileApiKey) {
|
|
105
218
|
return {
|
|
106
219
|
mode: "api-key",
|
|
107
220
|
source: "file:~/.codex/auth.json",
|
|
221
|
+
apiKey: fileApiKey,
|
|
108
222
|
authFilePath,
|
|
109
223
|
authFileParsed: true,
|
|
224
|
+
diagnostics: explicitKey.diagnostics,
|
|
110
225
|
};
|
|
111
226
|
}
|
|
112
227
|
if (parsed.auth_mode === "chatgpt") {
|
|
@@ -115,33 +230,53 @@ export function detectCodexAuth(
|
|
|
115
230
|
source: "file:~/.codex/auth.json",
|
|
116
231
|
authFilePath,
|
|
117
232
|
authFileParsed: true,
|
|
233
|
+
diagnostics: explicitKey.diagnostics,
|
|
118
234
|
};
|
|
119
235
|
}
|
|
120
236
|
// File parsed but doesn't carry a recognised mode — treat as
|
|
121
|
-
// missing
|
|
122
|
-
|
|
237
|
+
// missing for now; generic OpenAI fallback may still be usable.
|
|
238
|
+
authFileResult = {
|
|
123
239
|
mode: "none",
|
|
124
240
|
source: "missing",
|
|
125
241
|
authFilePath,
|
|
126
242
|
authFileParsed: true,
|
|
243
|
+
diagnostics: explicitKey.diagnostics,
|
|
127
244
|
};
|
|
128
245
|
} catch (err) {
|
|
129
|
-
|
|
246
|
+
authFileResult = {
|
|
130
247
|
mode: "none",
|
|
131
248
|
source: "missing",
|
|
132
249
|
authFilePath,
|
|
133
250
|
authFileParsed: false,
|
|
134
251
|
parseError: err instanceof Error ? err.message : String(err),
|
|
252
|
+
diagnostics: explicitKey.diagnostics,
|
|
135
253
|
};
|
|
136
254
|
}
|
|
137
255
|
}
|
|
138
256
|
}
|
|
139
257
|
|
|
140
|
-
|
|
258
|
+
const genericKey = resolveGenericOpenAiApiKey(input);
|
|
259
|
+
if (genericKey.apiKey && genericKey.source) {
|
|
260
|
+
return {
|
|
261
|
+
mode: "api-key",
|
|
262
|
+
source: genericKey.source,
|
|
263
|
+
apiKey: genericKey.apiKey,
|
|
264
|
+
baseUrl: genericKey.baseUrl,
|
|
265
|
+
authFileParsed: authFileResult?.authFileParsed ?? false,
|
|
266
|
+
authFilePath: authFileResult?.authFilePath,
|
|
267
|
+
parseError: authFileResult?.parseError,
|
|
268
|
+
diagnostics: [...explicitKey.diagnostics, ...genericKey.diagnostics],
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (authFileResult) return authFileResult;
|
|
273
|
+
|
|
274
|
+
// Nothing.
|
|
141
275
|
return {
|
|
142
276
|
mode: "none",
|
|
143
277
|
source: "missing",
|
|
144
278
|
authFileParsed: false,
|
|
279
|
+
diagnostics: explicitKey.diagnostics,
|
|
145
280
|
};
|
|
146
281
|
}
|
|
147
282
|
|
|
@@ -163,3 +298,44 @@ export function isChatGptModelMismatchError(message: string): boolean {
|
|
|
163
298
|
message,
|
|
164
299
|
);
|
|
165
300
|
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Detect the *silent* OAuth-incompat exit shape — the one that hit
|
|
304
|
+
* Pandario on 2026-05-20 at 23:13Z.
|
|
305
|
+
*
|
|
306
|
+
* On a free ChatGPT-OAuth credential the Codex CLI silently rejects
|
|
307
|
+
* most model strings (only `gpt-5.5` is verified working). Crucially,
|
|
308
|
+
* for some rejected models the CLI exits 1 *without* emitting a
|
|
309
|
+
* structured `error` or `turn.failed` event over the JSON stream, AND
|
|
310
|
+
* without writing the canonical
|
|
311
|
+
* `"not supported when using Codex with a ChatGPT account"` text to
|
|
312
|
+
* stderr. The only signal the SDK surfaces is:
|
|
313
|
+
*
|
|
314
|
+
* `Codex Exec exited with code 1: Reading prompt from stdin...\n`
|
|
315
|
+
*
|
|
316
|
+
* (`"Reading prompt from stdin..."` is the CLI's startup banner — it
|
|
317
|
+
* always prints that and then exits before the prompt even reaches
|
|
318
|
+
* the model.)
|
|
319
|
+
*
|
|
320
|
+
* The detector heuristic:
|
|
321
|
+
* - Error text contains `"Codex Exec exited"` (SDK's wrapper);
|
|
322
|
+
* - Error text contains `"Reading prompt from stdin"` (CLI banner);
|
|
323
|
+
* - Error text does NOT contain the explicit mismatch phrase (the
|
|
324
|
+
* other detector handles that case).
|
|
325
|
+
*
|
|
326
|
+
* Callers MUST additionally check `authInfo.mode === "chatgpt"` and
|
|
327
|
+
* that the model isn't already the OAuth default before falling back —
|
|
328
|
+
* a silent exit-1 on api-key auth is a different bug class (network,
|
|
329
|
+
* config error, etc.) and shouldn't be misclassified.
|
|
330
|
+
*
|
|
331
|
+
* Case-insensitive; whitespace-tolerant. Matches both `code 1` and
|
|
332
|
+
* `code 2` (the CLI has been observed using both).
|
|
333
|
+
*/
|
|
334
|
+
export function isSilentOAuthExitError(message: string): boolean {
|
|
335
|
+
if (!message) return false;
|
|
336
|
+
if (isChatGptModelMismatchError(message)) return false;
|
|
337
|
+
return (
|
|
338
|
+
/Codex\s+Exec\s+exited\s+with\s+code\s+\d+/i.test(message) &&
|
|
339
|
+
/Reading\s+prompt\s+from\s+stdin/i.test(message)
|
|
340
|
+
);
|
|
341
|
+
}
|