@shuyhere/takotako 0.0.1
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/CONTRIBUTING.md +84 -0
- package/LICENSE +21 -0
- package/README.md +171 -0
- package/dist/agents/communication.d.ts +48 -0
- package/dist/agents/communication.d.ts.map +1 -0
- package/dist/agents/communication.js +123 -0
- package/dist/agents/communication.js.map +1 -0
- package/dist/agents/config.d.ts +52 -0
- package/dist/agents/config.d.ts.map +1 -0
- package/dist/agents/config.js +65 -0
- package/dist/agents/config.js.map +1 -0
- package/dist/agents/model-catalog.d.ts +49 -0
- package/dist/agents/model-catalog.d.ts.map +1 -0
- package/dist/agents/model-catalog.js +79 -0
- package/dist/agents/model-catalog.js.map +1 -0
- package/dist/agents/registry.d.ts +71 -0
- package/dist/agents/registry.d.ts.map +1 -0
- package/dist/agents/registry.js +297 -0
- package/dist/agents/registry.js.map +1 -0
- package/dist/agents/roles.d.ts +79 -0
- package/dist/agents/roles.d.ts.map +1 -0
- package/dist/agents/roles.js +174 -0
- package/dist/agents/roles.js.map +1 -0
- package/dist/agents/subagent.d.ts +124 -0
- package/dist/agents/subagent.d.ts.map +1 -0
- package/dist/agents/subagent.js +352 -0
- package/dist/agents/subagent.js.map +1 -0
- package/dist/agents/templates.d.ts +18 -0
- package/dist/agents/templates.d.ts.map +1 -0
- package/dist/agents/templates.js +341 -0
- package/dist/agents/templates.js.map +1 -0
- package/dist/agents/thread-binding.d.ts +77 -0
- package/dist/agents/thread-binding.d.ts.map +1 -0
- package/dist/agents/thread-binding.js +167 -0
- package/dist/agents/thread-binding.js.map +1 -0
- package/dist/auth/agent-profiles.d.ts +46 -0
- package/dist/auth/agent-profiles.d.ts.map +1 -0
- package/dist/auth/agent-profiles.js +97 -0
- package/dist/auth/agent-profiles.js.map +1 -0
- package/dist/auth/allow-from.d.ts +27 -0
- package/dist/auth/allow-from.d.ts.map +1 -0
- package/dist/auth/allow-from.js +118 -0
- package/dist/auth/allow-from.js.map +1 -0
- package/dist/auth/oauth.d.ts +66 -0
- package/dist/auth/oauth.d.ts.map +1 -0
- package/dist/auth/oauth.js +253 -0
- package/dist/auth/oauth.js.map +1 -0
- package/dist/auth/storage.d.ts +69 -0
- package/dist/auth/storage.d.ts.map +1 -0
- package/dist/auth/storage.js +157 -0
- package/dist/auth/storage.js.map +1 -0
- package/dist/cache/file-cache.d.ts +68 -0
- package/dist/cache/file-cache.d.ts.map +1 -0
- package/dist/cache/file-cache.js +176 -0
- package/dist/cache/file-cache.js.map +1 -0
- package/dist/cache/manager.d.ts +69 -0
- package/dist/cache/manager.d.ts.map +1 -0
- package/dist/cache/manager.js +117 -0
- package/dist/cache/manager.js.map +1 -0
- package/dist/cache/symbol-index.d.ts +75 -0
- package/dist/cache/symbol-index.d.ts.map +1 -0
- package/dist/cache/symbol-index.js +267 -0
- package/dist/cache/symbol-index.js.map +1 -0
- package/dist/cache/tool-cache.d.ts +75 -0
- package/dist/cache/tool-cache.d.ts.map +1 -0
- package/dist/cache/tool-cache.js +173 -0
- package/dist/cache/tool-cache.js.map +1 -0
- package/dist/channels/channel.d.ts +156 -0
- package/dist/channels/channel.d.ts.map +1 -0
- package/dist/channels/channel.js +25 -0
- package/dist/channels/channel.js.map +1 -0
- package/dist/channels/cli.d.ts +35 -0
- package/dist/channels/cli.d.ts.map +1 -0
- package/dist/channels/cli.js +94 -0
- package/dist/channels/cli.js.map +1 -0
- package/dist/channels/delivery-queue.d.ts +31 -0
- package/dist/channels/delivery-queue.d.ts.map +1 -0
- package/dist/channels/delivery-queue.js +127 -0
- package/dist/channels/delivery-queue.js.map +1 -0
- package/dist/channels/discord.d.ts +124 -0
- package/dist/channels/discord.d.ts.map +1 -0
- package/dist/channels/discord.js +664 -0
- package/dist/channels/discord.js.map +1 -0
- package/dist/channels/retry.d.ts +31 -0
- package/dist/channels/retry.d.ts.map +1 -0
- package/dist/channels/retry.js +94 -0
- package/dist/channels/retry.js.map +1 -0
- package/dist/channels/telegram.d.ts +69 -0
- package/dist/channels/telegram.d.ts.map +1 -0
- package/dist/channels/telegram.js +499 -0
- package/dist/channels/telegram.js.map +1 -0
- package/dist/channels/tui.d.ts +42 -0
- package/dist/channels/tui.d.ts.map +1 -0
- package/dist/channels/tui.js +126 -0
- package/dist/channels/tui.js.map +1 -0
- package/dist/cli/acp.d.ts +10 -0
- package/dist/cli/acp.d.ts.map +1 -0
- package/dist/cli/acp.js +69 -0
- package/dist/cli/acp.js.map +1 -0
- package/dist/cli/audit.d.ts +11 -0
- package/dist/cli/audit.d.ts.map +1 -0
- package/dist/cli/audit.js +55 -0
- package/dist/cli/audit.js.map +1 -0
- package/dist/cli/cache.d.ts +10 -0
- package/dist/cli/cache.d.ts.map +1 -0
- package/dist/cli/cache.js +77 -0
- package/dist/cli/cache.js.map +1 -0
- package/dist/cli/config.d.ts +5 -0
- package/dist/cli/config.d.ts.map +1 -0
- package/dist/cli/config.js +168 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/cron.d.ts +5 -0
- package/dist/cli/cron.d.ts.map +1 -0
- package/dist/cli/cron.js +192 -0
- package/dist/cli/cron.js.map +1 -0
- package/dist/cli/extensions.d.ts +5 -0
- package/dist/cli/extensions.d.ts.map +1 -0
- package/dist/cli/extensions.js +53 -0
- package/dist/cli/extensions.js.map +1 -0
- package/dist/cli/logs.d.ts +5 -0
- package/dist/cli/logs.d.ts.map +1 -0
- package/dist/cli/logs.js +49 -0
- package/dist/cli/logs.js.map +1 -0
- package/dist/cli/memory.d.ts +5 -0
- package/dist/cli/memory.d.ts.map +1 -0
- package/dist/cli/memory.js +78 -0
- package/dist/cli/memory.js.map +1 -0
- package/dist/cli/message.d.ts +5 -0
- package/dist/cli/message.d.ts.map +1 -0
- package/dist/cli/message.js +69 -0
- package/dist/cli/message.js.map +1 -0
- package/dist/cli/service.d.ts +14 -0
- package/dist/cli/service.d.ts.map +1 -0
- package/dist/cli/service.js +181 -0
- package/dist/cli/service.js.map +1 -0
- package/dist/cli/symphony.d.ts +5 -0
- package/dist/cli/symphony.d.ts.map +1 -0
- package/dist/cli/symphony.js +114 -0
- package/dist/cli/symphony.js.map +1 -0
- package/dist/cli/update.d.ts +5 -0
- package/dist/cli/update.d.ts.map +1 -0
- package/dist/cli/update.js +48 -0
- package/dist/cli/update.js.map +1 -0
- package/dist/commands/channel-setup.d.ts +31 -0
- package/dist/commands/channel-setup.d.ts.map +1 -0
- package/dist/commands/channel-setup.js +138 -0
- package/dist/commands/channel-setup.js.map +1 -0
- package/dist/commands/dispatch.d.ts +48 -0
- package/dist/commands/dispatch.d.ts.map +1 -0
- package/dist/commands/dispatch.js +68 -0
- package/dist/commands/dispatch.js.map +1 -0
- package/dist/commands/model-picker.d.ts +16 -0
- package/dist/commands/model-picker.d.ts.map +1 -0
- package/dist/commands/model-picker.js +120 -0
- package/dist/commands/model-picker.js.map +1 -0
- package/dist/commands/parser.d.ts +32 -0
- package/dist/commands/parser.d.ts.map +1 -0
- package/dist/commands/parser.js +39 -0
- package/dist/commands/parser.js.map +1 -0
- package/dist/commands/registry.d.ts +76 -0
- package/dist/commands/registry.d.ts.map +1 -0
- package/dist/commands/registry.js +351 -0
- package/dist/commands/registry.js.map +1 -0
- package/dist/commands/skill-commands.d.ts +35 -0
- package/dist/commands/skill-commands.d.ts.map +1 -0
- package/dist/commands/skill-commands.js +61 -0
- package/dist/commands/skill-commands.js.map +1 -0
- package/dist/config/resolve.d.ts +25 -0
- package/dist/config/resolve.d.ts.map +1 -0
- package/dist/config/resolve.js +289 -0
- package/dist/config/resolve.js.map +1 -0
- package/dist/config/schema.d.ts +520 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +123 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/core/agent-loop.d.ts +137 -0
- package/dist/core/agent-loop.d.ts.map +1 -0
- package/dist/core/agent-loop.js +700 -0
- package/dist/core/agent-loop.js.map +1 -0
- package/dist/core/audit.d.ts +87 -0
- package/dist/core/audit.d.ts.map +1 -0
- package/dist/core/audit.js +224 -0
- package/dist/core/audit.js.map +1 -0
- package/dist/core/bootstrap.d.ts +23 -0
- package/dist/core/bootstrap.d.ts.map +1 -0
- package/dist/core/bootstrap.js +162 -0
- package/dist/core/bootstrap.js.map +1 -0
- package/dist/core/context.d.ts +44 -0
- package/dist/core/context.d.ts.map +1 -0
- package/dist/core/context.js +65 -0
- package/dist/core/context.js.map +1 -0
- package/dist/core/cron.d.ts +111 -0
- package/dist/core/cron.d.ts.map +1 -0
- package/dist/core/cron.js +284 -0
- package/dist/core/cron.js.map +1 -0
- package/dist/core/exec-approvals.d.ts +50 -0
- package/dist/core/exec-approvals.d.ts.map +1 -0
- package/dist/core/exec-approvals.js +187 -0
- package/dist/core/exec-approvals.js.map +1 -0
- package/dist/core/heartbeat.d.ts +71 -0
- package/dist/core/heartbeat.d.ts.map +1 -0
- package/dist/core/heartbeat.js +214 -0
- package/dist/core/heartbeat.js.map +1 -0
- package/dist/core/message-queue.d.ts +60 -0
- package/dist/core/message-queue.d.ts.map +1 -0
- package/dist/core/message-queue.js +182 -0
- package/dist/core/message-queue.js.map +1 -0
- package/dist/core/network-policy.d.ts +39 -0
- package/dist/core/network-policy.d.ts.map +1 -0
- package/dist/core/network-policy.js +121 -0
- package/dist/core/network-policy.js.map +1 -0
- package/dist/core/progress.d.ts +48 -0
- package/dist/core/progress.d.ts.map +1 -0
- package/dist/core/progress.js +81 -0
- package/dist/core/progress.js.map +1 -0
- package/dist/core/prompt.d.ts +105 -0
- package/dist/core/prompt.d.ts.map +1 -0
- package/dist/core/prompt.js +411 -0
- package/dist/core/prompt.js.map +1 -0
- package/dist/core/pruning.d.ts +40 -0
- package/dist/core/pruning.d.ts.map +1 -0
- package/dist/core/pruning.js +165 -0
- package/dist/core/pruning.js.map +1 -0
- package/dist/core/rate-limiter.d.ts +64 -0
- package/dist/core/rate-limiter.d.ts.map +1 -0
- package/dist/core/rate-limiter.js +142 -0
- package/dist/core/rate-limiter.js.map +1 -0
- package/dist/core/reactions.d.ts +31 -0
- package/dist/core/reactions.d.ts.map +1 -0
- package/dist/core/reactions.js +67 -0
- package/dist/core/reactions.js.map +1 -0
- package/dist/core/retry-queue.d.ts +56 -0
- package/dist/core/retry-queue.d.ts.map +1 -0
- package/dist/core/retry-queue.js +106 -0
- package/dist/core/retry-queue.js.map +1 -0
- package/dist/core/sanitizer.d.ts +38 -0
- package/dist/core/sanitizer.d.ts.map +1 -0
- package/dist/core/sanitizer.js +181 -0
- package/dist/core/sanitizer.js.map +1 -0
- package/dist/core/secret-scanner.d.ts +39 -0
- package/dist/core/secret-scanner.d.ts.map +1 -0
- package/dist/core/secret-scanner.js +96 -0
- package/dist/core/secret-scanner.js.map +1 -0
- package/dist/core/secrets.d.ts +38 -0
- package/dist/core/secrets.d.ts.map +1 -0
- package/dist/core/secrets.js +137 -0
- package/dist/core/secrets.js.map +1 -0
- package/dist/core/security.d.ts +58 -0
- package/dist/core/security.d.ts.map +1 -0
- package/dist/core/security.js +120 -0
- package/dist/core/security.js.map +1 -0
- package/dist/core/self-awareness.d.ts +19 -0
- package/dist/core/self-awareness.d.ts.map +1 -0
- package/dist/core/self-awareness.js +124 -0
- package/dist/core/self-awareness.js.map +1 -0
- package/dist/core/session-init.d.ts +34 -0
- package/dist/core/session-init.d.ts.map +1 -0
- package/dist/core/session-init.js +68 -0
- package/dist/core/session-init.js.map +1 -0
- package/dist/core/streaming.d.ts +82 -0
- package/dist/core/streaming.d.ts.map +1 -0
- package/dist/core/streaming.js +264 -0
- package/dist/core/streaming.js.map +1 -0
- package/dist/core/symphony/orchestrator.d.ts +61 -0
- package/dist/core/symphony/orchestrator.d.ts.map +1 -0
- package/dist/core/symphony/orchestrator.js +476 -0
- package/dist/core/symphony/orchestrator.js.map +1 -0
- package/dist/core/symphony/status.d.ts +11 -0
- package/dist/core/symphony/status.d.ts.map +1 -0
- package/dist/core/symphony/status.js +133 -0
- package/dist/core/symphony/status.js.map +1 -0
- package/dist/core/symphony/types.d.ts +84 -0
- package/dist/core/symphony/types.d.ts.map +1 -0
- package/dist/core/symphony/types.js +5 -0
- package/dist/core/symphony/types.js.map +1 -0
- package/dist/core/symphony/workflow.d.ts +18 -0
- package/dist/core/symphony/workflow.d.ts.map +1 -0
- package/dist/core/symphony/workflow.js +149 -0
- package/dist/core/symphony/workflow.js.map +1 -0
- package/dist/core/symphony/workspace.d.ts +24 -0
- package/dist/core/symphony/workspace.d.ts.map +1 -0
- package/dist/core/symphony/workspace.js +94 -0
- package/dist/core/symphony/workspace.js.map +1 -0
- package/dist/core/thinking.d.ts +27 -0
- package/dist/core/thinking.d.ts.map +1 -0
- package/dist/core/thinking.js +83 -0
- package/dist/core/thinking.js.map +1 -0
- package/dist/core/thread-bindings.d.ts +47 -0
- package/dist/core/thread-bindings.d.ts.map +1 -0
- package/dist/core/thread-bindings.js +94 -0
- package/dist/core/thread-bindings.js.map +1 -0
- package/dist/core/timezone.d.ts +28 -0
- package/dist/core/timezone.d.ts.map +1 -0
- package/dist/core/timezone.js +72 -0
- package/dist/core/timezone.js.map +1 -0
- package/dist/core/tool-loop-detector.d.ts +41 -0
- package/dist/core/tool-loop-detector.d.ts.map +1 -0
- package/dist/core/tool-loop-detector.js +83 -0
- package/dist/core/tool-loop-detector.js.map +1 -0
- package/dist/core/tool-validator.d.ts +44 -0
- package/dist/core/tool-validator.d.ts.map +1 -0
- package/dist/core/tool-validator.js +175 -0
- package/dist/core/tool-validator.js.map +1 -0
- package/dist/core/typing.d.ts +25 -0
- package/dist/core/typing.d.ts.map +1 -0
- package/dist/core/typing.js +48 -0
- package/dist/core/typing.js.map +1 -0
- package/dist/core/usage-tracker.d.ts +66 -0
- package/dist/core/usage-tracker.d.ts.map +1 -0
- package/dist/core/usage-tracker.js +163 -0
- package/dist/core/usage-tracker.js.map +1 -0
- package/dist/daemon/commands.d.ts +16 -0
- package/dist/daemon/commands.d.ts.map +1 -0
- package/dist/daemon/commands.js +445 -0
- package/dist/daemon/commands.js.map +1 -0
- package/dist/daemon/pid.d.ts +30 -0
- package/dist/daemon/pid.d.ts.map +1 -0
- package/dist/daemon/pid.js +62 -0
- package/dist/daemon/pid.js.map +1 -0
- package/dist/doctor/checks/browser.d.ts +9 -0
- package/dist/doctor/checks/browser.d.ts.map +1 -0
- package/dist/doctor/checks/browser.js +54 -0
- package/dist/doctor/checks/browser.js.map +1 -0
- package/dist/doctor/checks/channels.d.ts +9 -0
- package/dist/doctor/checks/channels.d.ts.map +1 -0
- package/dist/doctor/checks/channels.js +90 -0
- package/dist/doctor/checks/channels.js.map +1 -0
- package/dist/doctor/checks/config.d.ts +10 -0
- package/dist/doctor/checks/config.d.ts.map +1 -0
- package/dist/doctor/checks/config.js +89 -0
- package/dist/doctor/checks/config.js.map +1 -0
- package/dist/doctor/checks/memory.d.ts +10 -0
- package/dist/doctor/checks/memory.d.ts.map +1 -0
- package/dist/doctor/checks/memory.js +82 -0
- package/dist/doctor/checks/memory.js.map +1 -0
- package/dist/doctor/checks/permissions.d.ts +9 -0
- package/dist/doctor/checks/permissions.d.ts.map +1 -0
- package/dist/doctor/checks/permissions.js +53 -0
- package/dist/doctor/checks/permissions.js.map +1 -0
- package/dist/doctor/checks/providers.d.ts +10 -0
- package/dist/doctor/checks/providers.d.ts.map +1 -0
- package/dist/doctor/checks/providers.js +93 -0
- package/dist/doctor/checks/providers.js.map +1 -0
- package/dist/doctor/checks/sessions.d.ts +10 -0
- package/dist/doctor/checks/sessions.d.ts.map +1 -0
- package/dist/doctor/checks/sessions.js +86 -0
- package/dist/doctor/checks/sessions.js.map +1 -0
- package/dist/doctor/doctor.d.ts +35 -0
- package/dist/doctor/doctor.d.ts.map +1 -0
- package/dist/doctor/doctor.js +51 -0
- package/dist/doctor/doctor.js.map +1 -0
- package/dist/doctor/repairs.d.ts +14 -0
- package/dist/doctor/repairs.d.ts.map +1 -0
- package/dist/doctor/repairs.js +34 -0
- package/dist/doctor/repairs.js.map +1 -0
- package/dist/gateway/compaction.d.ts +63 -0
- package/dist/gateway/compaction.d.ts.map +1 -0
- package/dist/gateway/compaction.js +235 -0
- package/dist/gateway/compaction.js.map +1 -0
- package/dist/gateway/gateway.d.ts +94 -0
- package/dist/gateway/gateway.d.ts.map +1 -0
- package/dist/gateway/gateway.js +466 -0
- package/dist/gateway/gateway.js.map +1 -0
- package/dist/gateway/lock.d.ts +24 -0
- package/dist/gateway/lock.d.ts.map +1 -0
- package/dist/gateway/lock.js +88 -0
- package/dist/gateway/lock.js.map +1 -0
- package/dist/gateway/protocol.d.ts +117 -0
- package/dist/gateway/protocol.d.ts.map +1 -0
- package/dist/gateway/protocol.js +5 -0
- package/dist/gateway/protocol.js.map +1 -0
- package/dist/gateway/session.d.ts +123 -0
- package/dist/gateway/session.d.ts.map +1 -0
- package/dist/gateway/session.js +573 -0
- package/dist/gateway/session.js.map +1 -0
- package/dist/hooks/hooks.d.ts +18 -0
- package/dist/hooks/hooks.d.ts.map +1 -0
- package/dist/hooks/hooks.js +45 -0
- package/dist/hooks/hooks.js.map +1 -0
- package/dist/hooks/types.d.ts +112 -0
- package/dist/hooks/types.d.ts.map +1 -0
- package/dist/hooks/types.js +23 -0
- package/dist/hooks/types.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2900 -0
- package/dist/index.js.map +1 -0
- package/dist/media/storage.d.ts +25 -0
- package/dist/media/storage.d.ts.map +1 -0
- package/dist/media/storage.js +97 -0
- package/dist/media/storage.js.map +1 -0
- package/dist/memory/embeddings.d.ts +46 -0
- package/dist/memory/embeddings.d.ts.map +1 -0
- package/dist/memory/embeddings.js +118 -0
- package/dist/memory/embeddings.js.map +1 -0
- package/dist/memory/hybrid.d.ts +35 -0
- package/dist/memory/hybrid.d.ts.map +1 -0
- package/dist/memory/hybrid.js +156 -0
- package/dist/memory/hybrid.js.map +1 -0
- package/dist/memory/markdown.d.ts +48 -0
- package/dist/memory/markdown.d.ts.map +1 -0
- package/dist/memory/markdown.js +228 -0
- package/dist/memory/markdown.js.map +1 -0
- package/dist/memory/store.d.ts +88 -0
- package/dist/memory/store.d.ts.map +1 -0
- package/dist/memory/store.js +21 -0
- package/dist/memory/store.js.map +1 -0
- package/dist/memory/vector.d.ts +24 -0
- package/dist/memory/vector.d.ts.map +1 -0
- package/dist/memory/vector.js +63 -0
- package/dist/memory/vector.js.map +1 -0
- package/dist/mods/mod.d.ts +100 -0
- package/dist/mods/mod.d.ts.map +1 -0
- package/dist/mods/mod.js +242 -0
- package/dist/mods/mod.js.map +1 -0
- package/dist/onboard/channels.d.ts +12 -0
- package/dist/onboard/channels.d.ts.map +1 -0
- package/dist/onboard/channels.js +283 -0
- package/dist/onboard/channels.js.map +1 -0
- package/dist/onboard/models.d.ts +13 -0
- package/dist/onboard/models.d.ts.map +1 -0
- package/dist/onboard/models.js +491 -0
- package/dist/onboard/models.js.map +1 -0
- package/dist/onboard/onboard.d.ts +12 -0
- package/dist/onboard/onboard.d.ts.map +1 -0
- package/dist/onboard/onboard.js +1137 -0
- package/dist/onboard/onboard.js.map +1 -0
- package/dist/providers/anthropic.d.ts +83 -0
- package/dist/providers/anthropic.d.ts.map +1 -0
- package/dist/providers/anthropic.js +583 -0
- package/dist/providers/anthropic.js.map +1 -0
- package/dist/providers/failover.d.ts +46 -0
- package/dist/providers/failover.d.ts.map +1 -0
- package/dist/providers/failover.js +149 -0
- package/dist/providers/failover.js.map +1 -0
- package/dist/providers/litellm.d.ts +38 -0
- package/dist/providers/litellm.d.ts.map +1 -0
- package/dist/providers/litellm.js +349 -0
- package/dist/providers/litellm.js.map +1 -0
- package/dist/providers/openai.d.ts +28 -0
- package/dist/providers/openai.d.ts.map +1 -0
- package/dist/providers/openai.js +321 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/providers/prompt-cache.d.ts +50 -0
- package/dist/providers/prompt-cache.d.ts.map +1 -0
- package/dist/providers/prompt-cache.js +96 -0
- package/dist/providers/prompt-cache.js.map +1 -0
- package/dist/providers/provider.d.ts +173 -0
- package/dist/providers/provider.d.ts.map +1 -0
- package/dist/providers/provider.js +22 -0
- package/dist/providers/provider.js.map +1 -0
- package/dist/sandbox/config.d.ts +42 -0
- package/dist/sandbox/config.d.ts.map +1 -0
- package/dist/sandbox/config.js +20 -0
- package/dist/sandbox/config.js.map +1 -0
- package/dist/sandbox/container.d.ts +71 -0
- package/dist/sandbox/container.d.ts.map +1 -0
- package/dist/sandbox/container.js +193 -0
- package/dist/sandbox/container.js.map +1 -0
- package/dist/sandbox/sandbox.d.ts +82 -0
- package/dist/sandbox/sandbox.d.ts.map +1 -0
- package/dist/sandbox/sandbox.js +176 -0
- package/dist/sandbox/sandbox.js.map +1 -0
- package/dist/skills/channel-loader.d.ts +18 -0
- package/dist/skills/channel-loader.d.ts.map +1 -0
- package/dist/skills/channel-loader.js +35 -0
- package/dist/skills/channel-loader.js.map +1 -0
- package/dist/skills/extension-loader.d.ts +15 -0
- package/dist/skills/extension-loader.d.ts.map +1 -0
- package/dist/skills/extension-loader.js +63 -0
- package/dist/skills/extension-loader.js.map +1 -0
- package/dist/skills/extension-registry.d.ts +32 -0
- package/dist/skills/extension-registry.d.ts.map +1 -0
- package/dist/skills/extension-registry.js +57 -0
- package/dist/skills/extension-registry.js.map +1 -0
- package/dist/skills/extensions.d.ts +91 -0
- package/dist/skills/extensions.d.ts.map +1 -0
- package/dist/skills/extensions.js +14 -0
- package/dist/skills/extensions.js.map +1 -0
- package/dist/skills/loader.d.ts +64 -0
- package/dist/skills/loader.d.ts.map +1 -0
- package/dist/skills/loader.js +382 -0
- package/dist/skills/loader.js.map +1 -0
- package/dist/skills/marketplace.d.ts +56 -0
- package/dist/skills/marketplace.d.ts.map +1 -0
- package/dist/skills/marketplace.js +183 -0
- package/dist/skills/marketplace.js.map +1 -0
- package/dist/skills/types.d.ts +94 -0
- package/dist/skills/types.d.ts.map +1 -0
- package/dist/skills/types.js +9 -0
- package/dist/skills/types.js.map +1 -0
- package/dist/tools/acp-sessions.d.ts +89 -0
- package/dist/tools/acp-sessions.d.ts.map +1 -0
- package/dist/tools/acp-sessions.js +391 -0
- package/dist/tools/acp-sessions.js.map +1 -0
- package/dist/tools/acp.d.ts +18 -0
- package/dist/tools/acp.d.ts.map +1 -0
- package/dist/tools/acp.js +102 -0
- package/dist/tools/acp.js.map +1 -0
- package/dist/tools/agent-tools.d.ts +24 -0
- package/dist/tools/agent-tools.d.ts.map +1 -0
- package/dist/tools/agent-tools.js +611 -0
- package/dist/tools/agent-tools.js.map +1 -0
- package/dist/tools/browser.d.ts +26 -0
- package/dist/tools/browser.d.ts.map +1 -0
- package/dist/tools/browser.js +242 -0
- package/dist/tools/browser.js.map +1 -0
- package/dist/tools/comms.d.ts +8 -0
- package/dist/tools/comms.d.ts.map +1 -0
- package/dist/tools/comms.js +39 -0
- package/dist/tools/comms.js.map +1 -0
- package/dist/tools/cron-tools.d.ts +9 -0
- package/dist/tools/cron-tools.d.ts.map +1 -0
- package/dist/tools/cron-tools.js +117 -0
- package/dist/tools/cron-tools.js.map +1 -0
- package/dist/tools/exec-safety.d.ts +71 -0
- package/dist/tools/exec-safety.d.ts.map +1 -0
- package/dist/tools/exec-safety.js +141 -0
- package/dist/tools/exec-safety.js.map +1 -0
- package/dist/tools/exec.d.ts +24 -0
- package/dist/tools/exec.d.ts.map +1 -0
- package/dist/tools/exec.js +191 -0
- package/dist/tools/exec.js.map +1 -0
- package/dist/tools/fs.d.ts +15 -0
- package/dist/tools/fs.d.ts.map +1 -0
- package/dist/tools/fs.js +249 -0
- package/dist/tools/fs.js.map +1 -0
- package/dist/tools/git.d.ts +9 -0
- package/dist/tools/git.d.ts.map +1 -0
- package/dist/tools/git.js +56 -0
- package/dist/tools/git.js.map +1 -0
- package/dist/tools/image.d.ts +15 -0
- package/dist/tools/image.d.ts.map +1 -0
- package/dist/tools/image.js +106 -0
- package/dist/tools/image.js.map +1 -0
- package/dist/tools/introspect.d.ts +22 -0
- package/dist/tools/introspect.d.ts.map +1 -0
- package/dist/tools/introspect.js +223 -0
- package/dist/tools/introspect.js.map +1 -0
- package/dist/tools/memory.d.ts +11 -0
- package/dist/tools/memory.d.ts.map +1 -0
- package/dist/tools/memory.js +101 -0
- package/dist/tools/memory.js.map +1 -0
- package/dist/tools/message.d.ts +24 -0
- package/dist/tools/message.d.ts.map +1 -0
- package/dist/tools/message.js +205 -0
- package/dist/tools/message.js.map +1 -0
- package/dist/tools/model.d.ts +14 -0
- package/dist/tools/model.d.ts.map +1 -0
- package/dist/tools/model.js +62 -0
- package/dist/tools/model.js.map +1 -0
- package/dist/tools/policy.d.ts +101 -0
- package/dist/tools/policy.d.ts.map +1 -0
- package/dist/tools/policy.js +168 -0
- package/dist/tools/policy.js.map +1 -0
- package/dist/tools/registry.d.ts +52 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +154 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/search.d.ts +10 -0
- package/dist/tools/search.d.ts.map +1 -0
- package/dist/tools/search.js +78 -0
- package/dist/tools/search.js.map +1 -0
- package/dist/tools/session.d.ts +13 -0
- package/dist/tools/session.d.ts.map +1 -0
- package/dist/tools/session.js +142 -0
- package/dist/tools/session.js.map +1 -0
- package/dist/tools/spawn.d.ts +10 -0
- package/dist/tools/spawn.d.ts.map +1 -0
- package/dist/tools/spawn.js +72 -0
- package/dist/tools/spawn.js.map +1 -0
- package/dist/tools/symphony.d.ts +12 -0
- package/dist/tools/symphony.d.ts.map +1 -0
- package/dist/tools/symphony.js +142 -0
- package/dist/tools/symphony.js.map +1 -0
- package/dist/tools/system-tools.d.ts +11 -0
- package/dist/tools/system-tools.d.ts.map +1 -0
- package/dist/tools/system-tools.js +39 -0
- package/dist/tools/system-tools.js.map +1 -0
- package/dist/tools/tool.d.ts +119 -0
- package/dist/tools/tool.d.ts.map +1 -0
- package/dist/tools/tool.js +29 -0
- package/dist/tools/tool.js.map +1 -0
- package/dist/tools/web.d.ts +10 -0
- package/dist/tools/web.d.ts.map +1 -0
- package/dist/tools/web.js +105 -0
- package/dist/tools/web.js.map +1 -0
- package/dist/tui/App.d.ts +43 -0
- package/dist/tui/App.d.ts.map +1 -0
- package/dist/tui/App.js +265 -0
- package/dist/tui/App.js.map +1 -0
- package/dist/tui/bridge.d.ts +40 -0
- package/dist/tui/bridge.d.ts.map +1 -0
- package/dist/tui/bridge.js +29 -0
- package/dist/tui/bridge.js.map +1 -0
- package/dist/tui/components/Header.d.ts +14 -0
- package/dist/tui/components/Header.d.ts.map +1 -0
- package/dist/tui/components/Header.js +7 -0
- package/dist/tui/components/Header.js.map +1 -0
- package/dist/tui/components/InputBar.d.ts +10 -0
- package/dist/tui/components/InputBar.d.ts.map +1 -0
- package/dist/tui/components/InputBar.js +121 -0
- package/dist/tui/components/InputBar.js.map +1 -0
- package/dist/tui/components/MessageList.d.ts +18 -0
- package/dist/tui/components/MessageList.d.ts.map +1 -0
- package/dist/tui/components/MessageList.js +34 -0
- package/dist/tui/components/MessageList.js.map +1 -0
- package/dist/tui/components/Spinner.d.ts +9 -0
- package/dist/tui/components/Spinner.d.ts.map +1 -0
- package/dist/tui/components/Spinner.js +18 -0
- package/dist/tui/components/Spinner.js.map +1 -0
- package/dist/tui/components/StatusBar.d.ts +16 -0
- package/dist/tui/components/StatusBar.d.ts.map +1 -0
- package/dist/tui/components/StatusBar.js +15 -0
- package/dist/tui/components/StatusBar.js.map +1 -0
- package/dist/tui/components/ToolCallBox.d.ts +12 -0
- package/dist/tui/components/ToolCallBox.d.ts.map +1 -0
- package/dist/tui/components/ToolCallBox.js +12 -0
- package/dist/tui/components/ToolCallBox.js.map +1 -0
- package/dist/tui/theme.d.ts +58 -0
- package/dist/tui/theme.d.ts.map +1 -0
- package/dist/tui/theme.js +80 -0
- package/dist/tui/theme.js.map +1 -0
- package/dist/utils/logger.d.ts +16 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +70 -0
- package/dist/utils/logger.js.map +1 -0
- package/docs/DEVELOPMENT.md +74 -0
- package/docs/INSTALL.md +161 -0
- package/docs/USAGE.md +94 -0
- package/docs/architecture.md +128 -0
- package/docs/channels.md +140 -0
- package/docs/configuration.md +209 -0
- package/docs/io-system.md +430 -0
- package/docs/providers.md +99 -0
- package/docs/skill-channels.md +113 -0
- package/docs/skills.md +246 -0
- package/package.json +89 -0
- package/skills/acp-router/SKILL.md +41 -0
- package/skills/acp-router/tools/acp-router.mjs +239 -0
- package/skills/find-skills/SKILL.md +133 -0
- package/skills/security-audit/SKILL.md +181 -0
- package/skills/security-audit/audit.sh +67 -0
- package/skills/skill-creator/SKILL.md +479 -0
- package/skills/skill-security-audit/.clawhub/origin.json +7 -0
- package/skills/skill-security-audit/SKILL.md +196 -0
- package/skills/skill-security-audit/_meta.json +6 -0
- package/skills/skill-security-audit/references/prompt-injection-patterns.md +276 -0
- package/skills/skill-security-audit/references/vulnerability-patterns.md +348 -0
- package/skills/symphony/README.md +53 -0
- package/skills/symphony/SKILL.md +75 -0
- package/skills/symphony/tools/symphony-orchestrator.ts +8 -0
- package/tako.example.json +33 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2900 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Tako 🐙 — Agent-as-CPU OS: minimal core + pluggable skill arms.
|
|
4
|
+
*
|
|
5
|
+
* CLI entry point with subcommands:
|
|
6
|
+
* tako Start interactive agent (alias for `tako start`)
|
|
7
|
+
* tako start Start the agent in foreground (default)
|
|
8
|
+
* tako start -d Start as background daemon
|
|
9
|
+
* tako stop Stop the daemon
|
|
10
|
+
* tako restart Restart the daemon
|
|
11
|
+
* tako status Show daemon & runtime status
|
|
12
|
+
* tako tui Attach TUI to running daemon
|
|
13
|
+
* tako dev Development mode (watch + auto-restart)
|
|
14
|
+
* tako onboard Interactive first-time setup wizard
|
|
15
|
+
* tako doctor Run health checks
|
|
16
|
+
* tako models Model management (list, set, auth, status)
|
|
17
|
+
* tako channels Channel management (list, add, remove, status)
|
|
18
|
+
* tako skills list List discovered skills
|
|
19
|
+
* tako skills install <name> Install a skill from the ecosystem
|
|
20
|
+
* tako skills info <name> Show skill details
|
|
21
|
+
* tako agents Agent management (list, add, remove, info)
|
|
22
|
+
* tako sessions Session management (list, history)
|
|
23
|
+
* tako version Print version
|
|
24
|
+
* tako help Show help
|
|
25
|
+
*/
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
import { homedir } from 'node:os';
|
|
28
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
29
|
+
import { readFile, writeFile, rename } from 'node:fs/promises';
|
|
30
|
+
import { resolveConfig, hasConfig } from './config/resolve.js';
|
|
31
|
+
import { runOnboard } from './onboard/onboard.js';
|
|
32
|
+
import { runModels } from './onboard/models.js';
|
|
33
|
+
import { runChannels } from './onboard/channels.js';
|
|
34
|
+
import { runStartDaemon, runStop, runRestart, runStatus, runTui, runDev } from './daemon/commands.js';
|
|
35
|
+
import { writePidFile, removePidFile } from './daemon/pid.js';
|
|
36
|
+
import { CLIChannel } from './channels/cli.js';
|
|
37
|
+
import { TUIChannel } from './channels/tui.js';
|
|
38
|
+
import { DiscordChannel } from './channels/discord.js';
|
|
39
|
+
import { TelegramChannel } from './channels/telegram.js';
|
|
40
|
+
import { AnthropicProvider } from './providers/anthropic.js';
|
|
41
|
+
import { OpenAIProvider } from './providers/openai.js';
|
|
42
|
+
import { LiteLLMProvider } from './providers/litellm.js';
|
|
43
|
+
import { FailoverProvider } from './providers/failover.js';
|
|
44
|
+
import { RetryQueue } from './core/retry-queue.js';
|
|
45
|
+
import { MessageQueue } from './core/message-queue.js';
|
|
46
|
+
import { ToolRegistry } from './tools/registry.js';
|
|
47
|
+
import { ToolPolicy } from './tools/policy.js';
|
|
48
|
+
import { configureExecSafety } from './tools/exec.js';
|
|
49
|
+
import { fsTools } from './tools/fs.js';
|
|
50
|
+
import { searchTools } from './tools/search.js';
|
|
51
|
+
import { execTools } from './tools/exec.js';
|
|
52
|
+
import { webTools } from './tools/web.js';
|
|
53
|
+
import { createModelTool } from './tools/model.js';
|
|
54
|
+
import { imageTools } from './tools/image.js';
|
|
55
|
+
import { gitTools } from './tools/git.js';
|
|
56
|
+
import { AgentLoop } from './core/agent-loop.js';
|
|
57
|
+
import { PromptBuilder } from './core/prompt.js';
|
|
58
|
+
import { ContextManager } from './core/context.js';
|
|
59
|
+
import { SessionManager } from './gateway/session.js';
|
|
60
|
+
import { Gateway } from './gateway/gateway.js';
|
|
61
|
+
import { TakoHookSystem } from './hooks/hooks.js';
|
|
62
|
+
import { HybridMemoryStore } from './memory/hybrid.js';
|
|
63
|
+
import { createEmbeddingProvider } from './memory/embeddings.js';
|
|
64
|
+
import { createMemoryTools } from './tools/memory.js';
|
|
65
|
+
import { createSessionTools } from './tools/session.js';
|
|
66
|
+
import { createBrowserTools } from './tools/browser.js';
|
|
67
|
+
import { SkillLoader } from './skills/loader.js';
|
|
68
|
+
import { loadChannelFromSkill } from './skills/channel-loader.js';
|
|
69
|
+
import { bootstrapWorkspace, ensureDailyMemory } from './core/bootstrap.js';
|
|
70
|
+
import { Doctor } from './doctor/doctor.js';
|
|
71
|
+
import { checkConfig } from './doctor/checks/config.js';
|
|
72
|
+
import { checkProviders } from './doctor/checks/providers.js';
|
|
73
|
+
import { checkChannels } from './doctor/checks/channels.js';
|
|
74
|
+
import { checkMemory } from './doctor/checks/memory.js';
|
|
75
|
+
import { checkSessions } from './doctor/checks/sessions.js';
|
|
76
|
+
import { checkPermissions } from './doctor/checks/permissions.js';
|
|
77
|
+
import { checkBrowser } from './doctor/checks/browser.js';
|
|
78
|
+
import { SandboxManager } from './sandbox/sandbox.js';
|
|
79
|
+
import { DockerContainer } from './sandbox/container.js';
|
|
80
|
+
import { AgentRegistry } from './agents/registry.js';
|
|
81
|
+
import { resolveAgentForChannel } from './agents/config.js';
|
|
82
|
+
import { SubAgentOrchestrator } from './agents/subagent.js';
|
|
83
|
+
import { createAgentTools } from './tools/agent-tools.js';
|
|
84
|
+
import { createMessageTools } from './tools/message.js';
|
|
85
|
+
import { ThreadBindingManager } from './core/thread-bindings.js';
|
|
86
|
+
import { CommandRegistry } from './commands/registry.js';
|
|
87
|
+
import { buildSkillCommands } from './commands/skill-commands.js';
|
|
88
|
+
import { showModelPicker } from './commands/model-picker.js';
|
|
89
|
+
import { installFileLogger } from './utils/logger.js';
|
|
90
|
+
import { createIntrospectTools } from './tools/introspect.js';
|
|
91
|
+
import { handleSetupCommand, handleAgentSelect, handleChannelTypeButton, handleModalSubmit, } from './commands/channel-setup.js';
|
|
92
|
+
import { isUserAllowed, createAllowFromTools } from './auth/allow-from.js';
|
|
93
|
+
import { DeliveryQueue } from './channels/delivery-queue.js';
|
|
94
|
+
import { initMediaStorage, persistAttachments } from './media/storage.js';
|
|
95
|
+
import { runConfig } from './cli/config.js';
|
|
96
|
+
import { runCron } from './cli/cron.js';
|
|
97
|
+
import { runMemory } from './cli/memory.js';
|
|
98
|
+
import { runLogs } from './cli/logs.js';
|
|
99
|
+
import { runMessage } from './cli/message.js';
|
|
100
|
+
import { runUpdate } from './cli/update.js';
|
|
101
|
+
import { runService } from './cli/service.js';
|
|
102
|
+
import { runCache } from './cli/cache.js';
|
|
103
|
+
import { runAudit } from './cli/audit.js';
|
|
104
|
+
import { runAcp } from './cli/acp.js';
|
|
105
|
+
import { runExtensions } from './cli/extensions.js';
|
|
106
|
+
import { initSecurity } from './core/security.js';
|
|
107
|
+
import { CacheManager } from './cache/manager.js';
|
|
108
|
+
import { setFsCacheManager } from './tools/fs.js';
|
|
109
|
+
import { setExecCacheManager } from './tools/exec.js';
|
|
110
|
+
import { setImageProvider } from './tools/image.js';
|
|
111
|
+
import { runSymphony } from './cli/symphony.js';
|
|
112
|
+
import { ExtensionRegistry } from './skills/extension-registry.js';
|
|
113
|
+
import { loadExtension, getSkillsWithExtension } from './skills/extension-loader.js';
|
|
114
|
+
const VERSION = '0.0.1';
|
|
115
|
+
async function main() {
|
|
116
|
+
// Prevent crashes from killing the entire process
|
|
117
|
+
process.on('uncaughtException', (error) => {
|
|
118
|
+
console.error('[tako] Uncaught exception:', error.message);
|
|
119
|
+
console.error(error.stack);
|
|
120
|
+
// Don't exit — let the process continue serving other requests
|
|
121
|
+
});
|
|
122
|
+
process.on('unhandledRejection', (reason) => {
|
|
123
|
+
console.error('[tako] Unhandled rejection:', reason);
|
|
124
|
+
// Don't exit — log and continue
|
|
125
|
+
});
|
|
126
|
+
const args = process.argv.slice(2);
|
|
127
|
+
const command = args[0] ?? 'start';
|
|
128
|
+
switch (command) {
|
|
129
|
+
case 'start':
|
|
130
|
+
// If no config exists, prompt to run onboard first
|
|
131
|
+
if (!hasConfig()) {
|
|
132
|
+
console.log('No tako.json found. Run `tako onboard` to set up Tako first.\n');
|
|
133
|
+
await runOnboard();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
// Daemon mode: fork to background
|
|
137
|
+
if (args.includes('--daemon') || args.includes('-d')) {
|
|
138
|
+
await runStartDaemon();
|
|
139
|
+
}
|
|
140
|
+
else if (args.includes('--background')) {
|
|
141
|
+
// Internal: forked child runs the actual server
|
|
142
|
+
await runStart();
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
await runStart();
|
|
146
|
+
}
|
|
147
|
+
break;
|
|
148
|
+
case 'stop':
|
|
149
|
+
await runStop();
|
|
150
|
+
break;
|
|
151
|
+
case 'restart':
|
|
152
|
+
await runRestart();
|
|
153
|
+
break;
|
|
154
|
+
case 'tui':
|
|
155
|
+
case 'chat':
|
|
156
|
+
await runTui();
|
|
157
|
+
break;
|
|
158
|
+
case 'dev':
|
|
159
|
+
await runDev();
|
|
160
|
+
break;
|
|
161
|
+
case 'onboard':
|
|
162
|
+
case 'setup':
|
|
163
|
+
case 'configure':
|
|
164
|
+
await runOnboard();
|
|
165
|
+
break;
|
|
166
|
+
case 'models':
|
|
167
|
+
await runModels(args.slice(1));
|
|
168
|
+
break;
|
|
169
|
+
case 'channels':
|
|
170
|
+
await runChannels(args.slice(1));
|
|
171
|
+
break;
|
|
172
|
+
case 'mod':
|
|
173
|
+
case 'mods':
|
|
174
|
+
await runMod(args.slice(1));
|
|
175
|
+
break;
|
|
176
|
+
case 'doctor':
|
|
177
|
+
await runDoctor(args.slice(1));
|
|
178
|
+
break;
|
|
179
|
+
case 'skills':
|
|
180
|
+
await runSkills(args.slice(1));
|
|
181
|
+
break;
|
|
182
|
+
case 'sandbox':
|
|
183
|
+
await runSandbox(args.slice(1));
|
|
184
|
+
break;
|
|
185
|
+
case 'agents':
|
|
186
|
+
await runAgents(args.slice(1));
|
|
187
|
+
break;
|
|
188
|
+
case 'sessions':
|
|
189
|
+
await runSessions(args.slice(1));
|
|
190
|
+
break;
|
|
191
|
+
case 'extensions':
|
|
192
|
+
await runExtensions(args.slice(1));
|
|
193
|
+
break;
|
|
194
|
+
case 'symphony':
|
|
195
|
+
await runSymphony(args.slice(1));
|
|
196
|
+
break;
|
|
197
|
+
case 'status':
|
|
198
|
+
await runStatus();
|
|
199
|
+
break;
|
|
200
|
+
case 'nuke':
|
|
201
|
+
await runNuke(args.slice(1));
|
|
202
|
+
break;
|
|
203
|
+
case 'config':
|
|
204
|
+
await runConfig(args.slice(1));
|
|
205
|
+
break;
|
|
206
|
+
case 'cache':
|
|
207
|
+
await runCache(args.slice(1));
|
|
208
|
+
break;
|
|
209
|
+
case 'audit':
|
|
210
|
+
await runAudit(args.slice(1));
|
|
211
|
+
break;
|
|
212
|
+
case 'acp':
|
|
213
|
+
await runAcp(args.slice(1));
|
|
214
|
+
break;
|
|
215
|
+
case 'cron':
|
|
216
|
+
await runCron(args.slice(1));
|
|
217
|
+
break;
|
|
218
|
+
case 'memory':
|
|
219
|
+
await runMemory(args.slice(1));
|
|
220
|
+
break;
|
|
221
|
+
case 'logs':
|
|
222
|
+
case 'log':
|
|
223
|
+
await runLogs(args.slice(1));
|
|
224
|
+
break;
|
|
225
|
+
case 'message':
|
|
226
|
+
case 'msg':
|
|
227
|
+
await runMessage(args.slice(1));
|
|
228
|
+
break;
|
|
229
|
+
case 'update':
|
|
230
|
+
await runUpdate(args.slice(1));
|
|
231
|
+
break;
|
|
232
|
+
case 'service':
|
|
233
|
+
await runService(args.slice(1));
|
|
234
|
+
break;
|
|
235
|
+
case 'version':
|
|
236
|
+
case '--version':
|
|
237
|
+
case '-v':
|
|
238
|
+
console.log(`tako v${VERSION}`);
|
|
239
|
+
break;
|
|
240
|
+
case 'help':
|
|
241
|
+
case '--help':
|
|
242
|
+
case '-h':
|
|
243
|
+
printHelp();
|
|
244
|
+
break;
|
|
245
|
+
default:
|
|
246
|
+
console.error(`Unknown command: ${command}`);
|
|
247
|
+
printHelp();
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
function printHelp() {
|
|
252
|
+
console.log(`Tako 🐙 — Agent-as-CPU OS
|
|
253
|
+
|
|
254
|
+
Usage: tako [command] [options]
|
|
255
|
+
|
|
256
|
+
Commands:
|
|
257
|
+
start Start Tako (foreground, default)
|
|
258
|
+
start -d Start Tako as background daemon
|
|
259
|
+
stop Stop the daemon
|
|
260
|
+
restart Restart the daemon
|
|
261
|
+
status Show daemon status & runtime info
|
|
262
|
+
tui Attach TUI to running daemon
|
|
263
|
+
dev Development mode (watch + auto-restart)
|
|
264
|
+
onboard Interactive first-time setup wizard
|
|
265
|
+
|
|
266
|
+
config file Print active config file path
|
|
267
|
+
config get <path> Get config value by dot path
|
|
268
|
+
config set <p> <v> Set config value by dot path
|
|
269
|
+
config unset <p> Remove config value
|
|
270
|
+
config validate Validate config against schema
|
|
271
|
+
|
|
272
|
+
models list List available models
|
|
273
|
+
models set <model> Set active model
|
|
274
|
+
models auth login Interactive provider auth
|
|
275
|
+
models status Show current model & provider
|
|
276
|
+
models refresh Re-fetch models from providers
|
|
277
|
+
models fallbacks Show/set fallback model chain
|
|
278
|
+
models aliases Manage model aliases
|
|
279
|
+
|
|
280
|
+
channels list List configured channels
|
|
281
|
+
channels add Add a channel (discord, telegram)
|
|
282
|
+
channels remove Remove a channel
|
|
283
|
+
channels status Show channel connection status
|
|
284
|
+
|
|
285
|
+
agents list List all configured agents
|
|
286
|
+
agents add <name> Create a new agent with workspace
|
|
287
|
+
agents remove <n> Remove an agent
|
|
288
|
+
agents info <name> Show agent details
|
|
289
|
+
agents bind Bind agent to a channel
|
|
290
|
+
agents unbind Unbind agent from a channel
|
|
291
|
+
agents bindings List all agent-channel bindings
|
|
292
|
+
agents set-identity Update agent display name/emoji
|
|
293
|
+
|
|
294
|
+
sessions list List all active sessions
|
|
295
|
+
sessions history Show session history
|
|
296
|
+
sessions inspect Show full session metadata
|
|
297
|
+
sessions compact Compact a session
|
|
298
|
+
sessions clear Archive a session
|
|
299
|
+
|
|
300
|
+
cron list List all cron jobs
|
|
301
|
+
cron add Add a scheduled job
|
|
302
|
+
cron remove <id> Remove a cron job
|
|
303
|
+
cron enable/disable Toggle a cron job
|
|
304
|
+
cron run <id> Run a job immediately
|
|
305
|
+
cron runs Show cron run history
|
|
306
|
+
|
|
307
|
+
memory search <q> Search memory files
|
|
308
|
+
memory status Show memory index status
|
|
309
|
+
|
|
310
|
+
logs View today's log (--lines N, --follow, --grep)
|
|
311
|
+
|
|
312
|
+
message send Send message to a channel
|
|
313
|
+
message broadcast Broadcast to all channels
|
|
314
|
+
|
|
315
|
+
skills list List discovered skills
|
|
316
|
+
skills install <n> Install a skill
|
|
317
|
+
skills info <name> Show skill details
|
|
318
|
+
skills check Check skill readiness
|
|
319
|
+
skills audit <n> Security audit a skill
|
|
320
|
+
|
|
321
|
+
symphony start Start project orchestrator
|
|
322
|
+
symphony stop Stop orchestrator
|
|
323
|
+
symphony status Show orchestrator dashboard
|
|
324
|
+
symphony history Show completed runs
|
|
325
|
+
symphony config Show current config
|
|
326
|
+
|
|
327
|
+
sandbox status Show sandbox status
|
|
328
|
+
sandbox explain <t> Explain tool permissions
|
|
329
|
+
sandbox cleanup Remove sandbox containers
|
|
330
|
+
|
|
331
|
+
update check Check for new version
|
|
332
|
+
update install Self-update via npm
|
|
333
|
+
|
|
334
|
+
service install Install systemd user service
|
|
335
|
+
service uninstall Remove systemd user service
|
|
336
|
+
service start Start systemd service
|
|
337
|
+
service stop Stop systemd service
|
|
338
|
+
service restart Restart systemd service
|
|
339
|
+
service status Show service status
|
|
340
|
+
service logs Show service logs
|
|
341
|
+
|
|
342
|
+
doctor Run health checks
|
|
343
|
+
version Print version
|
|
344
|
+
help Show this help
|
|
345
|
+
|
|
346
|
+
Daemon examples:
|
|
347
|
+
tako start -d Start as background daemon
|
|
348
|
+
tako tui Attach TUI to running daemon
|
|
349
|
+
tako stop Stop the daemon
|
|
350
|
+
tako restart Restart the daemon
|
|
351
|
+
tako dev Watch mode for development
|
|
352
|
+
|
|
353
|
+
Auth examples:
|
|
354
|
+
tako models auth login --provider anthropic API key or setup-token
|
|
355
|
+
tako models auth login --provider openai API key or OAuth
|
|
356
|
+
tako models auth login --provider openai-codex OAuth flow
|
|
357
|
+
tako models auth status Check all providers
|
|
358
|
+
tako models auth logout --provider anthropic Remove stored auth
|
|
359
|
+
|
|
360
|
+
Config resolution:
|
|
361
|
+
1. --config <path> (explicit override)
|
|
362
|
+
2. ~/.tako/tako.json (user home)
|
|
363
|
+
|
|
364
|
+
Examples:
|
|
365
|
+
tako onboard First-time setup
|
|
366
|
+
tako Start interactive agent
|
|
367
|
+
tako models set anthropic/claude-opus-4-6
|
|
368
|
+
tako channels add discord
|
|
369
|
+
tako doctor Check system health
|
|
370
|
+
`);
|
|
371
|
+
}
|
|
372
|
+
// ─── tako start ──────────────────────────────────────────────────────
|
|
373
|
+
async function runStart() {
|
|
374
|
+
// Install file logger early so all console output is captured
|
|
375
|
+
installFileLogger();
|
|
376
|
+
const config = await resolveConfig();
|
|
377
|
+
// Bootstrap workspace
|
|
378
|
+
await bootstrapWorkspace(config.memory.workspace);
|
|
379
|
+
await ensureDailyMemory(config.memory.workspace);
|
|
380
|
+
// Initialize security modules
|
|
381
|
+
initSecurity(config.security, config.memory.workspace);
|
|
382
|
+
// Initialize cache
|
|
383
|
+
const cacheManager = new CacheManager(config.cache);
|
|
384
|
+
cacheManager.startAutoClean();
|
|
385
|
+
setFsCacheManager(cacheManager);
|
|
386
|
+
setExecCacheManager(cacheManager);
|
|
387
|
+
// Initialize subsystems
|
|
388
|
+
const hooks = new TakoHookSystem();
|
|
389
|
+
const sessions = new SessionManager();
|
|
390
|
+
// Thread bindings (Discord thread → sub-agent session routing)
|
|
391
|
+
const { homedir } = await import('node:os');
|
|
392
|
+
const threadBindings = new ThreadBindingManager(join(homedir(), '.tako', 'thread-bindings.json'));
|
|
393
|
+
await threadBindings.load();
|
|
394
|
+
// Memory
|
|
395
|
+
const embeddingProvider = createEmbeddingProvider(config.memory.embeddings);
|
|
396
|
+
const memoryStore = new HybridMemoryStore(config.memory.workspace, embeddingProvider ?? undefined);
|
|
397
|
+
await memoryStore.initialize();
|
|
398
|
+
const promptBuilder = new PromptBuilder(config.memory.workspace);
|
|
399
|
+
promptBuilder.setSandboxInfo(config.sandbox.mode, config.sandbox.workspaceAccess);
|
|
400
|
+
const contextManager = new ContextManager();
|
|
401
|
+
// Provider
|
|
402
|
+
const [providerName] = config.providers.primary.split('/');
|
|
403
|
+
let provider;
|
|
404
|
+
let resolvedProviderLabel = config.providers.primary;
|
|
405
|
+
switch (providerName) {
|
|
406
|
+
case 'anthropic':
|
|
407
|
+
provider = new AnthropicProvider();
|
|
408
|
+
break;
|
|
409
|
+
case 'openai':
|
|
410
|
+
provider = new OpenAIProvider();
|
|
411
|
+
break;
|
|
412
|
+
case 'litellm':
|
|
413
|
+
if (config.providers.litellm?.baseUrl) {
|
|
414
|
+
provider = LiteLLMProvider.fromConfig(config.providers.litellm);
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
console.error('[litellm] ✗ No LiteLLM endpoint configured!');
|
|
418
|
+
console.error('[litellm] Your primary model is litellm/* but no baseUrl is set.');
|
|
419
|
+
console.error('[litellm] Run `tako onboard` and configure LiteLLM, or switch provider.');
|
|
420
|
+
console.error('[litellm] Falling back to Anthropic provider.');
|
|
421
|
+
provider = new AnthropicProvider();
|
|
422
|
+
resolvedProviderLabel = `anthropic (fallback — litellm misconfigured)`;
|
|
423
|
+
}
|
|
424
|
+
break;
|
|
425
|
+
default:
|
|
426
|
+
provider = new AnthropicProvider();
|
|
427
|
+
resolvedProviderLabel = `anthropic (fallback — unknown provider '${providerName}')`;
|
|
428
|
+
}
|
|
429
|
+
// Wrap provider in FailoverProvider for automatic fallback
|
|
430
|
+
const fallbackChain = [config.providers.primary, ...(config.providers.fallback ?? [])];
|
|
431
|
+
const providerMap = new Map();
|
|
432
|
+
providerMap.set(providerName, provider);
|
|
433
|
+
// Create additional provider instances for fallback models
|
|
434
|
+
for (const ref of fallbackChain) {
|
|
435
|
+
const [pid] = ref.split('/');
|
|
436
|
+
if (!providerMap.has(pid)) {
|
|
437
|
+
switch (pid) {
|
|
438
|
+
case 'anthropic':
|
|
439
|
+
providerMap.set(pid, new AnthropicProvider());
|
|
440
|
+
break;
|
|
441
|
+
case 'openai':
|
|
442
|
+
providerMap.set(pid, new OpenAIProvider());
|
|
443
|
+
break;
|
|
444
|
+
case 'litellm':
|
|
445
|
+
if (config.providers.litellm?.baseUrl) {
|
|
446
|
+
providerMap.set(pid, LiteLLMProvider.fromConfig(config.providers.litellm));
|
|
447
|
+
}
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
const failoverProvider = new FailoverProvider({
|
|
453
|
+
providers: providerMap,
|
|
454
|
+
chain: fallbackChain,
|
|
455
|
+
cooldownMs: (config.providers.cooldownSeconds ?? 60) * 1000,
|
|
456
|
+
});
|
|
457
|
+
// Wire image tool to use the active provider for vision API
|
|
458
|
+
setImageProvider(failoverProvider, config.providers.primary);
|
|
459
|
+
// Sandbox manager
|
|
460
|
+
const sandboxManager = new SandboxManager(config.sandbox);
|
|
461
|
+
const sandboxActive = config.sandbox.mode !== 'off';
|
|
462
|
+
if (sandboxActive) {
|
|
463
|
+
const dockerOk = await sandboxManager.checkDocker();
|
|
464
|
+
if (!dockerOk) {
|
|
465
|
+
console.warn('[tako] Warning: Sandbox enabled but Docker is not available. Falling back to host execution.');
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
// Tool policy
|
|
469
|
+
const toolPolicy = new ToolPolicy({
|
|
470
|
+
profile: config.tools.profile,
|
|
471
|
+
allow: config.tools.allow,
|
|
472
|
+
deny: config.tools.deny,
|
|
473
|
+
sandbox: config.tools.sandbox,
|
|
474
|
+
exec: config.tools.exec ? {
|
|
475
|
+
security: config.tools.exec.security,
|
|
476
|
+
allowlist: config.tools.exec.allowlist,
|
|
477
|
+
timeout: config.tools.exec.timeout,
|
|
478
|
+
maxOutputSize: config.tools.exec.maxOutputSize,
|
|
479
|
+
} : undefined,
|
|
480
|
+
});
|
|
481
|
+
// Exec safety
|
|
482
|
+
configureExecSafety({
|
|
483
|
+
workspaceRoot: config.memory.workspace,
|
|
484
|
+
workDir: process.cwd(),
|
|
485
|
+
maxTimeout: config.tools.exec?.timeout ?? 120_000,
|
|
486
|
+
defaultTimeout: 30_000,
|
|
487
|
+
maxOutputSize: config.tools.exec?.maxOutputSize ?? 1024 * 1024,
|
|
488
|
+
});
|
|
489
|
+
// Tool registry
|
|
490
|
+
const toolRegistry = new ToolRegistry();
|
|
491
|
+
toolRegistry.setProfile(config.tools.profile);
|
|
492
|
+
toolRegistry.setDenyList(config.tools.deny);
|
|
493
|
+
if (config.tools.allow)
|
|
494
|
+
toolRegistry.setAllowList(config.tools.allow);
|
|
495
|
+
toolRegistry.setToolPolicy(toolPolicy);
|
|
496
|
+
// Register kernel tools
|
|
497
|
+
toolRegistry.registerAll(fsTools);
|
|
498
|
+
toolRegistry.registerAll(searchTools);
|
|
499
|
+
toolRegistry.registerAll(execTools);
|
|
500
|
+
toolRegistry.registerAll(webTools);
|
|
501
|
+
toolRegistry.registerAll(createBrowserTools({
|
|
502
|
+
enabled: config.tools.browser?.enabled ?? true,
|
|
503
|
+
headless: config.tools.browser?.headless ?? true,
|
|
504
|
+
idleTimeoutMs: config.tools.browser?.idleTimeoutMs ?? 300_000,
|
|
505
|
+
}));
|
|
506
|
+
toolRegistry.registerAll(imageTools);
|
|
507
|
+
toolRegistry.registerAll(gitTools);
|
|
508
|
+
toolRegistry.registerAll(createMemoryTools(memoryStore));
|
|
509
|
+
toolRegistry.registerAll(createSessionTools(sessions));
|
|
510
|
+
toolRegistry.registerAll(createAllowFromTools());
|
|
511
|
+
// Symphony tools (project orchestration)
|
|
512
|
+
const { symphonyTools } = await import('./tools/symphony.js');
|
|
513
|
+
toolRegistry.registerAll(symphonyTools);
|
|
514
|
+
// System tools (restart, etc.)
|
|
515
|
+
const { registerSystemTools } = await import('./tools/system-tools.js');
|
|
516
|
+
registerSystemTools(toolRegistry, {
|
|
517
|
+
gatewayPort: config.gateway.port,
|
|
518
|
+
gatewayBind: config.gateway.bind,
|
|
519
|
+
});
|
|
520
|
+
// ─── Agent registry ────────────────────────────────────────────────
|
|
521
|
+
const agentRegistry = new AgentRegistry(config.agents, config.providers.primary);
|
|
522
|
+
await agentRegistry.loadDynamic();
|
|
523
|
+
await agentRegistry.initialize();
|
|
524
|
+
// Enable per-agent session persistence (each agent stores sessions in its own dir)
|
|
525
|
+
const agentSessionDirs = new Map();
|
|
526
|
+
for (const agent of agentRegistry.list()) {
|
|
527
|
+
agentSessionDirs.set(agent.id, agent.sessionDir);
|
|
528
|
+
}
|
|
529
|
+
await sessions.enablePersistence(agentSessionDirs);
|
|
530
|
+
// Load skills — dirs come from config (already resolved/expanded)
|
|
531
|
+
const skillLoader = new SkillLoader(config.skills.dirs);
|
|
532
|
+
const skillManifests = await skillLoader.discover();
|
|
533
|
+
for (const manifest of skillManifests) {
|
|
534
|
+
const loaded = await skillLoader.load(manifest);
|
|
535
|
+
skillLoader.registerTools(loaded, toolRegistry);
|
|
536
|
+
skillLoader.registerHooks(loaded, hooks);
|
|
537
|
+
promptBuilder.addSkillInstructions(loaded.instructions);
|
|
538
|
+
}
|
|
539
|
+
// Build initial skill command specs (used by agent-loop dispatch + channel slash registration)
|
|
540
|
+
const skillCommandSpecs = buildSkillCommands(skillLoader.getAll());
|
|
541
|
+
// Hot reload — re-register tools, hooks, AND slash commands on skill changes
|
|
542
|
+
skillLoader.startWatching(async (reloadedSkills) => {
|
|
543
|
+
for (const skill of reloadedSkills) {
|
|
544
|
+
skillLoader.registerTools(skill, toolRegistry);
|
|
545
|
+
skillLoader.registerHooks(skill, hooks);
|
|
546
|
+
}
|
|
547
|
+
console.log(`[tako] Skills reloaded: ${reloadedSkills.length} skills`);
|
|
548
|
+
// Re-build and re-register skill commands with Discord
|
|
549
|
+
try {
|
|
550
|
+
const rebuiltSpecs = buildSkillCommands(reloadedSkills);
|
|
551
|
+
skillCommandSpecs.splice(0, skillCommandSpecs.length, ...rebuiltSpecs);
|
|
552
|
+
for (const dc of discordChannels) {
|
|
553
|
+
await dc.registerSkillCommands(skillCommandSpecs, async (commandName, channelId, author, guildId) => {
|
|
554
|
+
const agentId = dc.agentId ?? resolveAgentForChannel(agentRegistry.list(), 'discord', channelId);
|
|
555
|
+
return handleSlashCommand(commandName, channelId, author, agentId, dc);
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
if (discordChannels.length > 0) {
|
|
559
|
+
console.log(`[tako] Re-registered ${skillCommandSpecs.length} skill commands with Discord (${discordChannels.length} bot(s))`);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
catch (err) {
|
|
563
|
+
console.error('[tako] Failed to re-register skill commands:', err instanceof Error ? err.message : err);
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
// Retry queue for failed messages (all fallbacks exhausted)
|
|
567
|
+
const retryQueue = new RetryQueue(config.retryQueue);
|
|
568
|
+
// Message queue for batching rapid inbound messages
|
|
569
|
+
// The processor callback is wired after the agent loop is created (see below).
|
|
570
|
+
let messageQueueProcessor = null;
|
|
571
|
+
const messageQueue = new MessageQueue(config.queue, async (sessionId, messages) => {
|
|
572
|
+
if (messageQueueProcessor)
|
|
573
|
+
await messageQueueProcessor(sessionId, messages);
|
|
574
|
+
});
|
|
575
|
+
// Typing indicators + reaction feedback
|
|
576
|
+
const { TypingManager } = await import('./core/typing.js');
|
|
577
|
+
const { ReactionManager } = await import('./core/reactions.js');
|
|
578
|
+
const typingManager = new TypingManager(config.typing ?? { enabled: true, intervalMs: 5000 });
|
|
579
|
+
const reactionManager = new ReactionManager(config.reactions ?? { enabled: true, reactions: {
|
|
580
|
+
received: '👋', processing: '💭', completed: '👍', failed: '❌', retrying: '🔄', thinking: '🧐',
|
|
581
|
+
} });
|
|
582
|
+
// Agent loop with skill loader for dynamic injection
|
|
583
|
+
const agentLoop = new AgentLoop({ provider: failoverProvider, toolRegistry, promptBuilder, contextManager, hooks, skillLoader, skillCommandSpecs, model: config.providers.primary, workspaceRoot: config.memory.workspace, retryQueue, typingManager, reactionManager, streamingConfig: config.agent.streaming }, {
|
|
584
|
+
timeout: config.agent.timeout,
|
|
585
|
+
...(config.agent.maxOutputChars != null && { maxOutputChars: config.agent.maxOutputChars }),
|
|
586
|
+
...(config.agent.maxTurns != null && { maxTurns: config.agent.maxTurns }),
|
|
587
|
+
...(config.agent.maxToolCalls != null && { maxToolCalls: config.agent.maxToolCalls }),
|
|
588
|
+
...(config.agent.maxTokens != null && { maxTokens: config.agent.maxTokens }),
|
|
589
|
+
});
|
|
590
|
+
// Set retry runner — re-invokes agent loop for a session
|
|
591
|
+
retryQueue.setRunner(async (sessionId, userMessage) => {
|
|
592
|
+
const session = sessions.get(sessionId);
|
|
593
|
+
if (!session)
|
|
594
|
+
throw new Error(`Session ${sessionId} not found for retry`);
|
|
595
|
+
let result = '';
|
|
596
|
+
for await (const chunk of agentLoop.run(session, userMessage)) {
|
|
597
|
+
result += chunk;
|
|
598
|
+
}
|
|
599
|
+
return result;
|
|
600
|
+
});
|
|
601
|
+
// Per-agent loops: each agent gets its own PromptBuilder (workspace) but shares
|
|
602
|
+
// the same provider (auth), toolRegistry, contextManager, and hooks.
|
|
603
|
+
// Agents with per-agent skill dirs get their own SkillLoader; otherwise they
|
|
604
|
+
// share the global skillLoader and skillCommandSpecs.
|
|
605
|
+
const agentLoops = new Map();
|
|
606
|
+
// Track per-agent skill command specs for channel slash-command registration.
|
|
607
|
+
const agentSkillCommandSpecsMap = new Map();
|
|
608
|
+
for (const agent of agentRegistry.list()) {
|
|
609
|
+
if (agent.isMain)
|
|
610
|
+
continue;
|
|
611
|
+
const agentPromptBuilder = new PromptBuilder(agent.workspace);
|
|
612
|
+
agentPromptBuilder.setSandboxInfo(config.sandbox.mode, config.sandbox.workspaceAccess);
|
|
613
|
+
const agentModel = agent.model ?? config.providers.primary;
|
|
614
|
+
// Build per-agent skill loader when agent has extra skill dirs
|
|
615
|
+
let agentSkillLoader = skillLoader;
|
|
616
|
+
let agentSkillCommandSpecs = skillCommandSpecs;
|
|
617
|
+
if (agent.skills?.dirs && agent.skills.dirs.length > 0) {
|
|
618
|
+
const agentSkillDirs = [...config.skills.dirs, ...agent.skills.dirs];
|
|
619
|
+
agentSkillLoader = new SkillLoader(agentSkillDirs);
|
|
620
|
+
const agentSkillManifests = await agentSkillLoader.discover();
|
|
621
|
+
for (const manifest of agentSkillManifests) {
|
|
622
|
+
const loaded = await agentSkillLoader.load(manifest);
|
|
623
|
+
agentSkillLoader.registerTools(loaded, toolRegistry);
|
|
624
|
+
agentSkillLoader.registerHooks(loaded, hooks);
|
|
625
|
+
}
|
|
626
|
+
agentSkillCommandSpecs = buildSkillCommands(agentSkillLoader.getAll());
|
|
627
|
+
console.log(`[tako] Agent "${agent.id}" using ${agentSkillDirs.length} skill dir(s), ${agentSkillCommandSpecs.length} skill command(s)`);
|
|
628
|
+
}
|
|
629
|
+
// Store per-agent specs so channel setup below can use them
|
|
630
|
+
agentSkillCommandSpecsMap.set(agent.id, agentSkillCommandSpecs);
|
|
631
|
+
const loop = new AgentLoop({
|
|
632
|
+
provider: failoverProvider,
|
|
633
|
+
toolRegistry,
|
|
634
|
+
promptBuilder: agentPromptBuilder,
|
|
635
|
+
contextManager,
|
|
636
|
+
hooks,
|
|
637
|
+
skillLoader: agentSkillLoader,
|
|
638
|
+
skillCommandSpecs: agentSkillCommandSpecs,
|
|
639
|
+
model: agentModel,
|
|
640
|
+
workspaceRoot: agent.workspace,
|
|
641
|
+
agentId: agent.id,
|
|
642
|
+
agentRole: agent.role,
|
|
643
|
+
retryQueue,
|
|
644
|
+
typingManager,
|
|
645
|
+
reactionManager,
|
|
646
|
+
streamingConfig: config.agent.streaming,
|
|
647
|
+
}, {
|
|
648
|
+
timeout: config.agent.timeout,
|
|
649
|
+
...(config.agent.maxOutputChars != null && { maxOutputChars: config.agent.maxOutputChars }),
|
|
650
|
+
...(config.agent.maxTurns != null && { maxTurns: config.agent.maxTurns }),
|
|
651
|
+
...(config.agent.maxToolCalls != null && { maxToolCalls: config.agent.maxToolCalls }),
|
|
652
|
+
...(config.agent.maxTokens != null && { maxTokens: config.agent.maxTokens }),
|
|
653
|
+
});
|
|
654
|
+
agentLoops.set(agent.id, loop);
|
|
655
|
+
}
|
|
656
|
+
/** Get the correct AgentLoop for a given agentId. */
|
|
657
|
+
function getAgentLoop(agentId) {
|
|
658
|
+
if (agentId && agentLoops.has(agentId))
|
|
659
|
+
return agentLoops.get(agentId);
|
|
660
|
+
return agentLoop;
|
|
661
|
+
}
|
|
662
|
+
// Register set_model tool
|
|
663
|
+
toolRegistry.register(createModelTool({
|
|
664
|
+
setModel: (ref) => {
|
|
665
|
+
agentLoop.setModel(ref);
|
|
666
|
+
config.providers.primary = ref;
|
|
667
|
+
import('./config/resolve.js').then(m => m.patchConfig({ providers: { primary: ref } })).catch(() => { });
|
|
668
|
+
},
|
|
669
|
+
getModel: () => agentLoop.getModel(),
|
|
670
|
+
}));
|
|
671
|
+
// ─── Sub-agent orchestrator ────────────────────────────────────────
|
|
672
|
+
const subAgentOrchestrator = new SubAgentOrchestrator(sessions, agentLoop);
|
|
673
|
+
// Notify parent sessions when sub-agents complete — deliver through channels
|
|
674
|
+
subAgentOrchestrator.onCompletion(async (event) => {
|
|
675
|
+
const parentSession = sessions.get(event.parentSessionId);
|
|
676
|
+
if (!parentSession)
|
|
677
|
+
return;
|
|
678
|
+
const statusEmoji = event.status === 'completed' ? '👍' : event.status === 'timeout' ? '⏱' : '❌';
|
|
679
|
+
const label = event.runId.slice(0, 8);
|
|
680
|
+
const summary = event.status === 'completed'
|
|
681
|
+
? (event.result ?? '').slice(0, 1000)
|
|
682
|
+
: (event.error ?? 'Unknown error');
|
|
683
|
+
const announcement = `${statusEmoji} Sub-agent \`${label}\` ${event.status}\n\n${summary}`;
|
|
684
|
+
// Add to session messages
|
|
685
|
+
sessions.addMessage(event.parentSessionId, {
|
|
686
|
+
role: 'system',
|
|
687
|
+
content: announcement,
|
|
688
|
+
});
|
|
689
|
+
// Deliver through the channel that created this session
|
|
690
|
+
const channelType = parentSession.metadata.channelType;
|
|
691
|
+
const channelTarget = parentSession.metadata.channelTarget;
|
|
692
|
+
if (channelType && channelTarget) {
|
|
693
|
+
const channel = channels.find((ch) => ch.id === channelType);
|
|
694
|
+
if (channel) {
|
|
695
|
+
try {
|
|
696
|
+
await channel.send({ content: announcement, target: channelTarget });
|
|
697
|
+
}
|
|
698
|
+
catch (err) {
|
|
699
|
+
console.error(`[subagent] Failed to deliver completion to ${channelType}: ${err instanceof Error ? err.message : err}`);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
// Also output to CLI/TUI if that's the parent channel
|
|
704
|
+
if (!channelType || channelType === 'cli' || channelType === 'tui') {
|
|
705
|
+
console.log(`\n${announcement}\n`);
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
// Register agent tools (agents_list, sessions_spawn, sessions_history, subagents)
|
|
709
|
+
toolRegistry.registerAll(createAgentTools({
|
|
710
|
+
registry: agentRegistry,
|
|
711
|
+
orchestrator: subAgentOrchestrator,
|
|
712
|
+
sessions,
|
|
713
|
+
threadBindings,
|
|
714
|
+
}));
|
|
715
|
+
// ─── Command registry ────────────────────────────────────────────
|
|
716
|
+
const startTime = Date.now();
|
|
717
|
+
const defaultModel = config.providers.primary;
|
|
718
|
+
const commandRegistry = new CommandRegistry({
|
|
719
|
+
getModel: () => agentLoop.getModel(),
|
|
720
|
+
setModel: (ref) => {
|
|
721
|
+
agentLoop.setModel(ref);
|
|
722
|
+
config.providers.primary = ref;
|
|
723
|
+
// Persist to tako.json so it survives restart
|
|
724
|
+
import('./config/resolve.js').then(m => m.patchConfig({ providers: { primary: ref } })).catch(() => { });
|
|
725
|
+
},
|
|
726
|
+
getDefaultModel: () => defaultModel,
|
|
727
|
+
getFallbackModels: () => config.providers.fallback ?? [],
|
|
728
|
+
listAgents: () => agentRegistry.list().map((a) => ({
|
|
729
|
+
id: a.id,
|
|
730
|
+
description: a.description,
|
|
731
|
+
role: a.role,
|
|
732
|
+
})),
|
|
733
|
+
compactSession: (sessionId, keepLast) => sessions.compact(sessionId, keepLast),
|
|
734
|
+
resetSession: (sessionId) => sessions.resetSession(sessionId),
|
|
735
|
+
estimateTokens: (session) => contextManager.estimateTokens(session.messages),
|
|
736
|
+
startTime,
|
|
737
|
+
getWorkspaceRoot: () => config.memory.workspace,
|
|
738
|
+
getSessionCount: () => sessions.list().length,
|
|
739
|
+
getChannelNames: () => channels.map(ch => ch.id),
|
|
740
|
+
getSkillCount: () => skillManifests.length,
|
|
741
|
+
getToolCount: () => toolRegistry.getActiveTools().length,
|
|
742
|
+
getQueueMode: () => messageQueue.getConfig().mode,
|
|
743
|
+
setQueueMode: (mode) => messageQueue.setMode(mode),
|
|
744
|
+
getQueueStatus: () => messageQueue.status(),
|
|
745
|
+
});
|
|
746
|
+
// ─── Multi-channel routing ────────────────────────────────────────
|
|
747
|
+
function getSession(msg, channel) {
|
|
748
|
+
// If the channel has a bound agentId, use it directly; otherwise resolve from bindings
|
|
749
|
+
const channelType = msg.channelId.split(':')[0] ?? 'cli';
|
|
750
|
+
const channelTarget = msg.channelId.includes(':')
|
|
751
|
+
? msg.channelId.split(':').slice(1).join(':')
|
|
752
|
+
: msg.channelId;
|
|
753
|
+
// Check thread bindings first — if this message is in a bound thread,
|
|
754
|
+
// route to the sub-agent session instead of normal routing.
|
|
755
|
+
const binding = threadBindings.getBinding(channelTarget);
|
|
756
|
+
if (binding) {
|
|
757
|
+
threadBindings.touch(channelTarget);
|
|
758
|
+
const session = sessions.getOrCreate(binding.sessionKey, {
|
|
759
|
+
name: `${binding.agentId}/thread:${channelTarget}`,
|
|
760
|
+
metadata: {
|
|
761
|
+
agentId: binding.agentId,
|
|
762
|
+
channelType,
|
|
763
|
+
channelTarget,
|
|
764
|
+
authorId: msg.author.id,
|
|
765
|
+
threadBinding: true,
|
|
766
|
+
},
|
|
767
|
+
});
|
|
768
|
+
// Attach runtime-only ref (not persisted)
|
|
769
|
+
session.metadata.channelRef = channel;
|
|
770
|
+
return session;
|
|
771
|
+
}
|
|
772
|
+
const guildId = msg.author.meta?.guildId;
|
|
773
|
+
const agentId = channel?.agentId ?? resolveAgentForChannel(agentRegistry.list(), channelType, channelTarget, guildId);
|
|
774
|
+
// Build structured session key matching reference runtime's format:
|
|
775
|
+
// agent:<agentId>:<platform>:<type>:<target>
|
|
776
|
+
let key;
|
|
777
|
+
const chatType = msg.author.meta?.chatType;
|
|
778
|
+
if (channelType === 'discord') {
|
|
779
|
+
const guildId = msg.author.meta?.guildId;
|
|
780
|
+
if (guildId) {
|
|
781
|
+
key = `agent:${agentId}:discord:channel:${channelTarget}`;
|
|
782
|
+
}
|
|
783
|
+
else {
|
|
784
|
+
// No guild = DM
|
|
785
|
+
key = `agent:${agentId}:discord:dm:${msg.author.id}`;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
else if (channelType === 'telegram') {
|
|
789
|
+
if (chatType === 'private') {
|
|
790
|
+
key = `agent:${agentId}:telegram:dm:${channelTarget}`;
|
|
791
|
+
}
|
|
792
|
+
else {
|
|
793
|
+
key = `agent:${agentId}:telegram:group:${channelTarget}`;
|
|
794
|
+
}
|
|
795
|
+
// Telegram topic: separate session per forum topic
|
|
796
|
+
if (msg.threadId) {
|
|
797
|
+
key += `:topic:${msg.threadId}`;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
else if (channelType === 'cli') {
|
|
801
|
+
key = `agent:${agentId}:cli:main`;
|
|
802
|
+
}
|
|
803
|
+
else {
|
|
804
|
+
key = `agent:${agentId}:${msg.channelId}`;
|
|
805
|
+
}
|
|
806
|
+
const session = sessions.getOrCreate(key, {
|
|
807
|
+
name: `${agentId}/${msg.channelId}/${msg.author.name}`,
|
|
808
|
+
metadata: {
|
|
809
|
+
agentId, channelType, channelTarget, authorId: msg.author.id,
|
|
810
|
+
...(msg.threadId ? { threadId: msg.threadId } : {}),
|
|
811
|
+
},
|
|
812
|
+
});
|
|
813
|
+
// Attach runtime-only ref (not persisted — stripped in appendToJSONL)
|
|
814
|
+
session.metadata.channelRef = channel;
|
|
815
|
+
return session;
|
|
816
|
+
}
|
|
817
|
+
function wireChannel(channel) {
|
|
818
|
+
deliveryQueue.registerChannel(channel);
|
|
819
|
+
channel.onMessage(async (msg) => {
|
|
820
|
+
try {
|
|
821
|
+
await hooks.emit('message_received', {
|
|
822
|
+
event: 'message_received',
|
|
823
|
+
data: { channelId: msg.channelId, authorId: msg.author.id, content: msg.content },
|
|
824
|
+
timestamp: Date.now(),
|
|
825
|
+
});
|
|
826
|
+
if (channel.id === 'cli' && (msg.content === '/quit' || msg.content === '/exit')) {
|
|
827
|
+
await shutdown();
|
|
828
|
+
process.exit(0);
|
|
829
|
+
}
|
|
830
|
+
// ─── AllowFrom ACL check ─────────────────────────────────────
|
|
831
|
+
const aclAgentId = channel.agentId ?? 'main';
|
|
832
|
+
const aclChannel = channel.id;
|
|
833
|
+
if (aclChannel !== 'cli' && aclChannel !== 'tui') {
|
|
834
|
+
const allowed = await isUserAllowed(aclChannel, aclAgentId, msg.author.id);
|
|
835
|
+
if (!allowed)
|
|
836
|
+
return; // silently ignore
|
|
837
|
+
}
|
|
838
|
+
const session = getSession(msg, channel);
|
|
839
|
+
// Update per-message runtime metadata used by typing/reactions/rate-limits
|
|
840
|
+
session.metadata.channelId = msg.channelId;
|
|
841
|
+
session.metadata.channelRef = channel;
|
|
842
|
+
session.metadata.channelTarget = msg.channelId.includes(':')
|
|
843
|
+
? msg.channelId.split(':').slice(1).join(':')
|
|
844
|
+
: msg.channelId;
|
|
845
|
+
session.metadata.authorId = msg.author.id;
|
|
846
|
+
session.metadata.authorName = msg.author.name;
|
|
847
|
+
session.metadata.messageId = msg.id;
|
|
848
|
+
// Extract platform-specific target for typing/reactions
|
|
849
|
+
const target = session.metadata.channelTarget;
|
|
850
|
+
// ─── First-time channel intro (only on genuine first contact, not restarts) ──
|
|
851
|
+
// Track introduced channels persistently so we never re-intro after restart.
|
|
852
|
+
if (session.isNew && channel.id !== 'cli' && channel.id !== 'tui') {
|
|
853
|
+
const introKey = `${channel.agentId ?? 'main'}:${msg.channelId}`;
|
|
854
|
+
if (!introducedChannels.has(introKey)) {
|
|
855
|
+
introducedChannels.add(introKey);
|
|
856
|
+
saveIntroducedChannels();
|
|
857
|
+
const agentName = channel.agentId ?? 'Tako';
|
|
858
|
+
const intro = `👋 **${agentName}** is now active in this channel! Type \`/help\` for commands, or just @mention me to chat.`;
|
|
859
|
+
try {
|
|
860
|
+
await channel.send({ target, content: intro });
|
|
861
|
+
}
|
|
862
|
+
catch { /* may fail if no send permission */ }
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
// ─── Slash command handling (local, no LLM) ──────────────────
|
|
866
|
+
if (msg.content.trim().startsWith('/')) {
|
|
867
|
+
const channelType = msg.channelId.split(':')[0] ?? 'cli';
|
|
868
|
+
const channelTarget = msg.channelId.includes(':')
|
|
869
|
+
? msg.channelId.split(':').slice(1).join(':')
|
|
870
|
+
: msg.channelId;
|
|
871
|
+
const agentId = channel.agentId ?? resolveAgentForChannel(agentRegistry.list(), channelType, channelTarget);
|
|
872
|
+
// reference runtime-like command reactions: received -> processing -> done/failed
|
|
873
|
+
if (channel.addReaction)
|
|
874
|
+
channel.addReaction(target, msg.id, '👋').catch(() => { });
|
|
875
|
+
if (channel.addReaction)
|
|
876
|
+
channel.addReaction(target, msg.id, '🧐').catch(() => { });
|
|
877
|
+
if (channel.removeReaction)
|
|
878
|
+
channel.removeReaction(target, msg.id, '👋').catch(() => { });
|
|
879
|
+
try {
|
|
880
|
+
const cmdResult = await commandRegistry.handle(msg.content, {
|
|
881
|
+
channelId: msg.channelId,
|
|
882
|
+
authorId: msg.author.id,
|
|
883
|
+
authorName: msg.author.name,
|
|
884
|
+
session,
|
|
885
|
+
agentId,
|
|
886
|
+
});
|
|
887
|
+
if (cmdResult) {
|
|
888
|
+
if (channel.id === 'cli') {
|
|
889
|
+
process.stdout.write(cmdResult + '\n');
|
|
890
|
+
}
|
|
891
|
+
else {
|
|
892
|
+
await channel.send({ target, content: cmdResult, replyTo: msg.id });
|
|
893
|
+
}
|
|
894
|
+
if (channel.removeReaction)
|
|
895
|
+
channel.removeReaction(target, msg.id, '🧐').catch(() => { });
|
|
896
|
+
if (channel.addReaction)
|
|
897
|
+
channel.addReaction(target, msg.id, '👍').catch(() => { });
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
// Unknown command (not handled by registry)
|
|
901
|
+
if (channel.removeReaction)
|
|
902
|
+
channel.removeReaction(target, msg.id, '🧐').catch(() => { });
|
|
903
|
+
if (channel.addReaction)
|
|
904
|
+
channel.addReaction(target, msg.id, '🤷').catch(() => { });
|
|
905
|
+
}
|
|
906
|
+
catch (err) {
|
|
907
|
+
if (channel.removeReaction)
|
|
908
|
+
channel.removeReaction(target, msg.id, '🧐').catch(() => { });
|
|
909
|
+
if (channel.addReaction)
|
|
910
|
+
channel.addReaction(target, msg.id, '😅').catch(() => { });
|
|
911
|
+
throw err;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
// ─── Message queue: collect/debounce rapid messages ─────────
|
|
915
|
+
if (messageQueue.getConfig().mode !== 'off' && channel.id !== 'cli' && channel.id !== 'tui') {
|
|
916
|
+
const queued = messageQueue.enqueue(session.id, {
|
|
917
|
+
content: msg.content,
|
|
918
|
+
channelId: msg.channelId,
|
|
919
|
+
authorId: msg.author.id,
|
|
920
|
+
timestamp: Date.now(),
|
|
921
|
+
messageId: msg.id,
|
|
922
|
+
});
|
|
923
|
+
if (queued) {
|
|
924
|
+
// Immediate feedback while waiting for queue flush
|
|
925
|
+
if (channel.sendTyping)
|
|
926
|
+
channel.sendTyping(target).catch(() => { });
|
|
927
|
+
if (channel.addReaction)
|
|
928
|
+
channel.addReaction(target, msg.id, '💭').catch(() => { });
|
|
929
|
+
return; // message was queued, will be batch-processed later
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
// ─── Typing indicator setup ──────────────────────────────────
|
|
933
|
+
const typingMode = config.agent.typingMode ?? 'instant';
|
|
934
|
+
const typingIntervalMs = (config.agent.typingIntervalSeconds ?? 6) * 1000;
|
|
935
|
+
let typingInterval = null;
|
|
936
|
+
if (typingMode === 'instant' && channel.sendTyping) {
|
|
937
|
+
channel.sendTyping(target).catch(() => { });
|
|
938
|
+
typingInterval = setInterval(() => {
|
|
939
|
+
channel.sendTyping(target).catch(() => { });
|
|
940
|
+
}, typingIntervalMs);
|
|
941
|
+
}
|
|
942
|
+
// ─── Reaction feedback: react with 🤔 while processing ──────
|
|
943
|
+
if (channel.addReaction) {
|
|
944
|
+
channel.addReaction(target, msg.id, '🧐').catch(() => { });
|
|
945
|
+
}
|
|
946
|
+
let response = '';
|
|
947
|
+
let hadError = false;
|
|
948
|
+
// Prepend sender context so the agent knows who it's talking to
|
|
949
|
+
const senderPrefix = channel.id !== 'cli' && msg.author?.name
|
|
950
|
+
? `[From: ${msg.author.name}]\n`
|
|
951
|
+
: '';
|
|
952
|
+
const userMessage = senderPrefix + msg.content;
|
|
953
|
+
// Use the correct agent loop — per-agent loops have their own PromptBuilder
|
|
954
|
+
// (workspace/identity) but share the same provider (auth/API keys).
|
|
955
|
+
const activeLoop = getAgentLoop(channel.agentId ?? session.metadata?.agentId);
|
|
956
|
+
try {
|
|
957
|
+
// Set active channel for typing/reactions
|
|
958
|
+
activeLoop.setChannel(channel);
|
|
959
|
+
// Persist inbound media attachments locally
|
|
960
|
+
const attachments = msg.attachments?.length
|
|
961
|
+
? await persistAttachments(msg.attachments)
|
|
962
|
+
: msg.attachments;
|
|
963
|
+
for await (const chunk of activeLoop.run(session, userMessage, attachments)) {
|
|
964
|
+
if (channel.id === 'cli') {
|
|
965
|
+
process.stdout.write(chunk);
|
|
966
|
+
}
|
|
967
|
+
response += chunk;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
catch (err) {
|
|
971
|
+
hadError = true;
|
|
972
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
973
|
+
console.error(`[tako] Error: ${errMsg}`);
|
|
974
|
+
// Auto-fallback: if model not found (404), try fallback chain or reset to default
|
|
975
|
+
const is404 = errMsg.includes('404') || errMsg.includes('not_found');
|
|
976
|
+
if (is404 && !response) {
|
|
977
|
+
const currentModel = activeLoop.getModel();
|
|
978
|
+
const fallbacks = config.providers.fallback ?? [];
|
|
979
|
+
const nextFallback = fallbacks.find(f => f !== currentModel);
|
|
980
|
+
if (nextFallback) {
|
|
981
|
+
activeLoop.setModel(nextFallback);
|
|
982
|
+
response = `⚠️ Model \`${currentModel}\` not found. Auto-switched to fallback: \`${nextFallback}\`\n\nPlease resend your message, or use \`/model default\` to reset.`;
|
|
983
|
+
}
|
|
984
|
+
else {
|
|
985
|
+
activeLoop.setModel(defaultModel);
|
|
986
|
+
response = `⚠️ Model \`${currentModel}\` not found. Reset to default: \`${defaultModel}\`\n\nPlease resend your message.`;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
else if (!response) {
|
|
990
|
+
response = `⚠️ Error: ${errMsg.slice(0, 500)}`;
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
// ─── Send remaining text, then clean up ──────
|
|
994
|
+
if (typingInterval) {
|
|
995
|
+
clearInterval(typingInterval);
|
|
996
|
+
typingInterval = null;
|
|
997
|
+
}
|
|
998
|
+
if (channel.id === 'cli') {
|
|
999
|
+
if (response && !response.endsWith('\n')) {
|
|
1000
|
+
process.stdout.write('\n');
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
else if (channel.id === 'tui') {
|
|
1004
|
+
if (response) {
|
|
1005
|
+
await channel.send({ target: msg.channelId, content: response, replyTo: msg.id });
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
else if (response.trim()) {
|
|
1009
|
+
hooks.emit('message_sending', {
|
|
1010
|
+
event: 'message_sending',
|
|
1011
|
+
data: { channelId: msg.channelId, content: response },
|
|
1012
|
+
timestamp: Date.now(),
|
|
1013
|
+
}).catch(() => { });
|
|
1014
|
+
const outMsg = { target, content: response.trim(), replyTo: msg.id };
|
|
1015
|
+
try {
|
|
1016
|
+
await channel.send(outMsg);
|
|
1017
|
+
}
|
|
1018
|
+
catch (sendErr) {
|
|
1019
|
+
await deliveryQueue.enqueue(channel.id, outMsg, sendErr instanceof Error ? sendErr.message : String(sendErr));
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
else {
|
|
1023
|
+
// Empty response — fallback
|
|
1024
|
+
console.error(`[${channel.id}] Empty response for: "${msg.content.slice(0, 50)}" (session ${session.id}, msgs: ${session.messages.length})`);
|
|
1025
|
+
const fallbackMsg = { target, content: '🤔 I processed your message but had nothing to say. Try rephrasing?', replyTo: msg.id };
|
|
1026
|
+
try {
|
|
1027
|
+
await channel.send(fallbackMsg);
|
|
1028
|
+
}
|
|
1029
|
+
catch (sendErr) {
|
|
1030
|
+
await deliveryQueue.enqueue(channel.id, fallbackMsg, sendErr instanceof Error ? sendErr.message : String(sendErr));
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
// ─── Persist session to disk (agent loop pushes directly to session.messages) ──
|
|
1034
|
+
sessions.markSessionDirty(session.id);
|
|
1035
|
+
// ─── Reaction cleanup AFTER messages are sent ──────
|
|
1036
|
+
if (channel.removeReaction) {
|
|
1037
|
+
channel.removeReaction(target, msg.id, '🧐').catch(() => { });
|
|
1038
|
+
}
|
|
1039
|
+
if (channel.addReaction) {
|
|
1040
|
+
channel.addReaction(target, msg.id, hadError ? '😅' : '👍').catch(() => { });
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
catch (outerErr) {
|
|
1044
|
+
// Per-message error isolation: log and continue, don't kill the process
|
|
1045
|
+
const errMsg = outerErr instanceof Error ? outerErr.message : String(outerErr);
|
|
1046
|
+
console.error(`[tako] Error processing message in ${channel.id}: ${errMsg}`);
|
|
1047
|
+
if (outerErr instanceof Error && outerErr.stack) {
|
|
1048
|
+
console.error(outerErr.stack);
|
|
1049
|
+
}
|
|
1050
|
+
// Try to send error reaction if possible
|
|
1051
|
+
if (channel.addReaction) {
|
|
1052
|
+
channel.addReaction(msg.channelId.includes(':') ? msg.channelId.split(':').slice(1).join(':') : msg.channelId, msg.id, '😅').catch(() => { });
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
// ─── Message queue processor ────────────────────────────────────
|
|
1058
|
+
// Wire the processor callback now that wireChannel, sessions, and agentLoop exist.
|
|
1059
|
+
messageQueueProcessor = async (sessionId, messages) => {
|
|
1060
|
+
const session = sessions.get(sessionId);
|
|
1061
|
+
if (!session) {
|
|
1062
|
+
console.warn(`[message-queue] Session ${sessionId} not found, dropping ${messages.length} messages`);
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
const merged = MessageQueue.mergeMessages(messages);
|
|
1066
|
+
const channelRef = session.metadata?.channelRef;
|
|
1067
|
+
if (!channelRef) {
|
|
1068
|
+
console.warn(`[message-queue] No channel ref for session ${sessionId}, processing without channel`);
|
|
1069
|
+
}
|
|
1070
|
+
const activeLoop = getAgentLoop(session.metadata?.agentId);
|
|
1071
|
+
// Ensure loop has channel reference + latest message metadata for typing/reactions
|
|
1072
|
+
if (channelRef) {
|
|
1073
|
+
activeLoop.setChannel(channelRef);
|
|
1074
|
+
session.metadata.channelRef = channelRef;
|
|
1075
|
+
}
|
|
1076
|
+
const lastMsgId = messages[messages.length - 1]?.messageId;
|
|
1077
|
+
if (lastMsgId) {
|
|
1078
|
+
session.metadata.messageId = lastMsgId;
|
|
1079
|
+
}
|
|
1080
|
+
// Determine target for sending response
|
|
1081
|
+
const channelTarget = session.metadata?.channelTarget ?? '';
|
|
1082
|
+
const target = channelTarget;
|
|
1083
|
+
// Prepend sender context for merged messages
|
|
1084
|
+
const userMessage = merged;
|
|
1085
|
+
let response = '';
|
|
1086
|
+
let hadError = false;
|
|
1087
|
+
try {
|
|
1088
|
+
for await (const chunk of activeLoop.run(session, userMessage)) {
|
|
1089
|
+
response += chunk;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
catch (err) {
|
|
1093
|
+
hadError = true;
|
|
1094
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1095
|
+
console.error(`[message-queue] Error processing batch for session ${sessionId}: ${errMsg}`);
|
|
1096
|
+
if (!response)
|
|
1097
|
+
response = `⚠️ Error: ${errMsg.slice(0, 500)}`;
|
|
1098
|
+
}
|
|
1099
|
+
// Send response through the channel
|
|
1100
|
+
if (channelRef && target && response.trim()) {
|
|
1101
|
+
const lastMsgId = messages[messages.length - 1].messageId;
|
|
1102
|
+
try {
|
|
1103
|
+
await channelRef.send({ target, content: response.trim(), replyTo: lastMsgId });
|
|
1104
|
+
}
|
|
1105
|
+
catch (sendErr) {
|
|
1106
|
+
console.error(`[message-queue] Send error:`, sendErr instanceof Error ? sendErr.message : sendErr);
|
|
1107
|
+
}
|
|
1108
|
+
// Queue reaction lifecycle: ⏳ -> ✅/⚠️
|
|
1109
|
+
if (lastMsgId) {
|
|
1110
|
+
if (channelRef.removeReaction)
|
|
1111
|
+
channelRef.removeReaction(target, lastMsgId, '💭').catch(() => { });
|
|
1112
|
+
if (channelRef.addReaction)
|
|
1113
|
+
channelRef.addReaction(target, lastMsgId, hadError ? '😅' : '👍').catch(() => { });
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
sessions.markSessionDirty(sessionId);
|
|
1117
|
+
};
|
|
1118
|
+
// ─── Media storage ────────────────────────────────────────────────
|
|
1119
|
+
await initMediaStorage();
|
|
1120
|
+
// ─── Delivery queue ───────────────────────────────────────────────
|
|
1121
|
+
const deliveryQueue = new DeliveryQueue();
|
|
1122
|
+
await deliveryQueue.start();
|
|
1123
|
+
// ─── Initialize channels ──────────────────────────────────────────
|
|
1124
|
+
const channels = [];
|
|
1125
|
+
let discordChannel;
|
|
1126
|
+
const discordChannels = [];
|
|
1127
|
+
let telegramChannel;
|
|
1128
|
+
// Track which channels have received an intro (persistent across restarts)
|
|
1129
|
+
const introFilePath = join(homedir(), '.tako', 'introduced-channels.json');
|
|
1130
|
+
const introducedChannels = new Set();
|
|
1131
|
+
try {
|
|
1132
|
+
const raw = readFileSync(introFilePath, 'utf-8');
|
|
1133
|
+
const arr = JSON.parse(raw);
|
|
1134
|
+
if (Array.isArray(arr))
|
|
1135
|
+
arr.forEach((k) => introducedChannels.add(k));
|
|
1136
|
+
}
|
|
1137
|
+
catch { /* no file yet */ }
|
|
1138
|
+
function saveIntroducedChannels() {
|
|
1139
|
+
try {
|
|
1140
|
+
writeFileSync(introFilePath, JSON.stringify([...introducedChannels]), 'utf-8');
|
|
1141
|
+
}
|
|
1142
|
+
catch { /* non-critical */ }
|
|
1143
|
+
}
|
|
1144
|
+
const useTui = process.argv.includes('--tui') && process.stdout.isTTY;
|
|
1145
|
+
// Build available models list from config (primary + fallbacks + litellm + provider models)
|
|
1146
|
+
const availableModels = [config.providers.primary];
|
|
1147
|
+
if (config.providers.fallback) {
|
|
1148
|
+
for (const fb of config.providers.fallback) {
|
|
1149
|
+
if (!availableModels.includes(fb))
|
|
1150
|
+
availableModels.push(fb);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
if (config.providers.litellm?.models) {
|
|
1154
|
+
for (const m of config.providers.litellm.models) {
|
|
1155
|
+
const ref = `litellm/${m}`;
|
|
1156
|
+
if (!availableModels.includes(ref))
|
|
1157
|
+
availableModels.push(ref);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
// Add known provider models from registered providers
|
|
1161
|
+
for (const prov of [provider]) {
|
|
1162
|
+
for (const m of prov.models()) {
|
|
1163
|
+
const ref = `${m.provider}/${m.id}`;
|
|
1164
|
+
if (!availableModels.includes(ref))
|
|
1165
|
+
availableModels.push(ref);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
if (useTui) {
|
|
1169
|
+
const tui = new TUIChannel({
|
|
1170
|
+
version: VERSION,
|
|
1171
|
+
model: config.providers.primary,
|
|
1172
|
+
toolCount: toolRegistry.getAllTools().length,
|
|
1173
|
+
skillCount: skillManifests.length,
|
|
1174
|
+
toolProfile: config.tools.profile,
|
|
1175
|
+
memoryStatus: embeddingProvider ? 'hybrid' : 'BM25-only',
|
|
1176
|
+
availableModels,
|
|
1177
|
+
agents: agentRegistry.list().map((a) => ({
|
|
1178
|
+
id: a.id,
|
|
1179
|
+
description: a.description,
|
|
1180
|
+
role: a.role,
|
|
1181
|
+
isMain: a.isMain,
|
|
1182
|
+
})),
|
|
1183
|
+
onModelSwitch: (modelRef) => {
|
|
1184
|
+
// Update the agent loop's model at runtime
|
|
1185
|
+
agentLoop.setModel(modelRef);
|
|
1186
|
+
config.providers.primary = modelRef;
|
|
1187
|
+
},
|
|
1188
|
+
onAgentSwitch: (agentId) => {
|
|
1189
|
+
const agent = agentRegistry.get(agentId);
|
|
1190
|
+
if (agent) {
|
|
1191
|
+
// Switch workspace — prompt builder reads SOUL.md, AGENTS.md, etc. from here
|
|
1192
|
+
promptBuilder.setWorkspace(agent.workspace);
|
|
1193
|
+
// Switch model if agent has a different one
|
|
1194
|
+
if (agent.model && agent.model !== agentLoop.getModel()) {
|
|
1195
|
+
agentLoop.setModel(agent.model);
|
|
1196
|
+
}
|
|
1197
|
+
// Update working dir for tools
|
|
1198
|
+
if (agent.workspace) {
|
|
1199
|
+
promptBuilder.setWorkingDir(agent.workspace);
|
|
1200
|
+
}
|
|
1201
|
+
console.log(`[tako] Switched to agent: ${agentId} (role=${agent.role}, workspace=${agent.workspace})`);
|
|
1202
|
+
}
|
|
1203
|
+
},
|
|
1204
|
+
});
|
|
1205
|
+
channels.push(tui);
|
|
1206
|
+
wireChannel(tui);
|
|
1207
|
+
// Hook tool calls to show in TUI with proper colors
|
|
1208
|
+
hooks.on('before_tool_call', (event) => {
|
|
1209
|
+
const tuiBridge = globalThis.__takoTui;
|
|
1210
|
+
if (tuiBridge) {
|
|
1211
|
+
tuiBridge.addMessage({
|
|
1212
|
+
id: crypto.randomUUID(),
|
|
1213
|
+
role: 'tool',
|
|
1214
|
+
content: `Running...`,
|
|
1215
|
+
toolName: event.data.toolName,
|
|
1216
|
+
timestamp: new Date().toISOString(),
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
});
|
|
1220
|
+
hooks.on('after_tool_call', (event) => {
|
|
1221
|
+
const tuiBridge = globalThis.__takoTui;
|
|
1222
|
+
if (tuiBridge) {
|
|
1223
|
+
const result = event.data.result;
|
|
1224
|
+
const output = result.output?.slice(0, 200) ?? '';
|
|
1225
|
+
const icon = result.success ? '[✓]' : '[✗]';
|
|
1226
|
+
tuiBridge.addMessage({
|
|
1227
|
+
id: crypto.randomUUID(),
|
|
1228
|
+
role: 'tool',
|
|
1229
|
+
content: `${icon} ${output}${output.length >= 200 ? '...' : ''}`,
|
|
1230
|
+
toolName: event.data.toolName,
|
|
1231
|
+
timestamp: new Date().toISOString(),
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
else {
|
|
1237
|
+
const cli = new CLIChannel(config.channels.cli);
|
|
1238
|
+
channels.push(cli);
|
|
1239
|
+
wireChannel(cli);
|
|
1240
|
+
}
|
|
1241
|
+
// If other channels exist, don't let CLI stdin close kill the process
|
|
1242
|
+
if (config.channels.discord?.token || config.channels.telegram?.token) {
|
|
1243
|
+
process.env['TAKO_KEEP_ALIVE'] = '1';
|
|
1244
|
+
}
|
|
1245
|
+
const isSkillSlashCommand = (name) => skillCommandSpecs.some((s) => s.name === name);
|
|
1246
|
+
const handleSlashCommand = async (commandName, channelId, author, agentId, boundChannel) => {
|
|
1247
|
+
const channelKey = `discord:${channelId}`;
|
|
1248
|
+
const sessionKey = `agent:${agentId}:${channelKey}`;
|
|
1249
|
+
const session = sessions.getOrCreate(sessionKey, {
|
|
1250
|
+
name: `${agentId}/${channelKey}/${author.name}`,
|
|
1251
|
+
metadata: { agentId, channelType: 'discord', channelTarget: channelId, authorId: author.id },
|
|
1252
|
+
});
|
|
1253
|
+
const cmdResult = await commandRegistry.handle('/' + commandName, {
|
|
1254
|
+
channelId: channelKey,
|
|
1255
|
+
authorId: author.id,
|
|
1256
|
+
authorName: author.name,
|
|
1257
|
+
session,
|
|
1258
|
+
agentId,
|
|
1259
|
+
});
|
|
1260
|
+
if (cmdResult !== null)
|
|
1261
|
+
return cmdResult;
|
|
1262
|
+
// If this slash command came from a user-invocable skill, run it through AgentLoop
|
|
1263
|
+
if (!isSkillSlashCommand(commandName))
|
|
1264
|
+
return null;
|
|
1265
|
+
const activeLoop = getAgentLoop(agentId);
|
|
1266
|
+
activeLoop.setChannel(boundChannel);
|
|
1267
|
+
let response = '';
|
|
1268
|
+
for await (const chunk of activeLoop.run(session, `/${commandName}`)) {
|
|
1269
|
+
response += chunk;
|
|
1270
|
+
}
|
|
1271
|
+
return response || 'Done.';
|
|
1272
|
+
};
|
|
1273
|
+
// Build native command list from the command registry
|
|
1274
|
+
const nativeCommandList = [
|
|
1275
|
+
...commandRegistry.list(),
|
|
1276
|
+
{ name: 'setup', description: 'Configure agent channels (Discord/Telegram)' },
|
|
1277
|
+
];
|
|
1278
|
+
if (config.channels.discord?.token) {
|
|
1279
|
+
discordChannel = new DiscordChannel({
|
|
1280
|
+
token: config.channels.discord.token,
|
|
1281
|
+
guilds: config.channels.discord.guilds,
|
|
1282
|
+
});
|
|
1283
|
+
// Register native Discord slash commands
|
|
1284
|
+
discordChannel.setSlashCommands(nativeCommandList, async (commandName, channelId, author, guildId) => {
|
|
1285
|
+
const agentId = resolveAgentForChannel(agentRegistry.list(), 'discord', channelId);
|
|
1286
|
+
return handleSlashCommand(commandName, channelId, author, agentId, discordChannel);
|
|
1287
|
+
});
|
|
1288
|
+
// Merge user-invocable skills into slash commands before connect (single registration on ready)
|
|
1289
|
+
await discordChannel.registerSkillCommands(skillCommandSpecs, async (commandName, channelId, author, guildId) => {
|
|
1290
|
+
const agentId = resolveAgentForChannel(agentRegistry.list(), 'discord', channelId);
|
|
1291
|
+
return handleSlashCommand(commandName, channelId, author, agentId, discordChannel);
|
|
1292
|
+
});
|
|
1293
|
+
// Register interactive model picker for Discord /model command
|
|
1294
|
+
discordChannel.setInteractiveHandler('model', async (interaction) => {
|
|
1295
|
+
// Build provider → models map from all known providers
|
|
1296
|
+
const providerModelsMap = {};
|
|
1297
|
+
// Anthropic models (always available)
|
|
1298
|
+
const anthropicProvider = new AnthropicProvider();
|
|
1299
|
+
providerModelsMap['anthropic'] = anthropicProvider.models().map((m) => m.id);
|
|
1300
|
+
// OpenAI models (always available)
|
|
1301
|
+
const openaiProvider = new OpenAIProvider();
|
|
1302
|
+
providerModelsMap['openai'] = openaiProvider.models().map((m) => m.id);
|
|
1303
|
+
// LiteLLM models (from config or active provider)
|
|
1304
|
+
if (config.providers.litellm?.baseUrl) {
|
|
1305
|
+
const litellmModels = config.providers.litellm.models ?? [];
|
|
1306
|
+
if (litellmModels.length > 0) {
|
|
1307
|
+
providerModelsMap['litellm'] = litellmModels;
|
|
1308
|
+
}
|
|
1309
|
+
else if (provider.id === 'litellm') {
|
|
1310
|
+
providerModelsMap['litellm'] = provider.models().map((m) => m.id);
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
const providers = Object.keys(providerModelsMap);
|
|
1314
|
+
if (providers.length === 0)
|
|
1315
|
+
return false;
|
|
1316
|
+
await showModelPicker(interaction, {
|
|
1317
|
+
getModel: () => agentLoop.getModel(),
|
|
1318
|
+
setModel: (ref) => {
|
|
1319
|
+
agentLoop.setModel(ref);
|
|
1320
|
+
config.providers.primary = ref;
|
|
1321
|
+
import('./config/resolve.js').then(m => m.patchConfig({ providers: { primary: ref } })).catch(() => { });
|
|
1322
|
+
},
|
|
1323
|
+
getDefaultModel: () => defaultModel,
|
|
1324
|
+
getProviders: () => providers,
|
|
1325
|
+
getModelsForProvider: (p) => providerModelsMap[p] ?? [],
|
|
1326
|
+
});
|
|
1327
|
+
return true;
|
|
1328
|
+
});
|
|
1329
|
+
// Register interactive /setup command for channel configuration
|
|
1330
|
+
const setupDeps = {
|
|
1331
|
+
listAgents: () => agentRegistry.list().map((a) => ({ id: a.id, description: a.description })),
|
|
1332
|
+
saveChannelConfig: (agentId, channelType, cfg) => agentRegistry.saveChannelConfig(agentId, channelType, cfg),
|
|
1333
|
+
};
|
|
1334
|
+
discordChannel.setInteractiveHandler('setup', async (interaction) => {
|
|
1335
|
+
await handleSetupCommand(interaction, setupDeps);
|
|
1336
|
+
return true;
|
|
1337
|
+
});
|
|
1338
|
+
discordChannel.onSelectMenu(async (interaction) => {
|
|
1339
|
+
if (interaction.customId === 'setup_agent_select') {
|
|
1340
|
+
await handleAgentSelect(interaction);
|
|
1341
|
+
return true;
|
|
1342
|
+
}
|
|
1343
|
+
return false;
|
|
1344
|
+
});
|
|
1345
|
+
discordChannel.onButton(async (interaction) => {
|
|
1346
|
+
if (interaction.customId.startsWith('setup_type_') || interaction.customId === 'setup_cancel') {
|
|
1347
|
+
await handleChannelTypeButton(interaction);
|
|
1348
|
+
return true;
|
|
1349
|
+
}
|
|
1350
|
+
return false;
|
|
1351
|
+
});
|
|
1352
|
+
discordChannel.onModalSubmit(async (interaction) => {
|
|
1353
|
+
if (interaction.customId.startsWith('setup_modal_')) {
|
|
1354
|
+
await handleModalSubmit(interaction, setupDeps);
|
|
1355
|
+
return true;
|
|
1356
|
+
}
|
|
1357
|
+
return false;
|
|
1358
|
+
});
|
|
1359
|
+
discordChannels.push(discordChannel);
|
|
1360
|
+
channels.push(discordChannel);
|
|
1361
|
+
wireChannel(discordChannel);
|
|
1362
|
+
}
|
|
1363
|
+
if (config.channels.telegram?.token) {
|
|
1364
|
+
telegramChannel = new TelegramChannel({
|
|
1365
|
+
token: config.channels.telegram.token,
|
|
1366
|
+
allowedUsers: config.channels.telegram.allowedUsers,
|
|
1367
|
+
});
|
|
1368
|
+
// Register native Telegram command handlers
|
|
1369
|
+
telegramChannel.setCommands(nativeCommandList, async (commandName, chatId, author) => {
|
|
1370
|
+
const channelKey = `telegram:${chatId}`;
|
|
1371
|
+
const agentId = resolveAgentForChannel(agentRegistry.list(), 'telegram', chatId);
|
|
1372
|
+
const sessionKey = `agent:${agentId}:${channelKey}`;
|
|
1373
|
+
const session = sessions.getOrCreate(sessionKey, {
|
|
1374
|
+
name: `${agentId}/${channelKey}/${author.name}`,
|
|
1375
|
+
metadata: { agentId, channelType: 'telegram', channelTarget: chatId, authorId: author.id },
|
|
1376
|
+
});
|
|
1377
|
+
return commandRegistry.handle('/' + commandName, {
|
|
1378
|
+
channelId: channelKey,
|
|
1379
|
+
authorId: author.id,
|
|
1380
|
+
authorName: author.name,
|
|
1381
|
+
session,
|
|
1382
|
+
agentId,
|
|
1383
|
+
});
|
|
1384
|
+
});
|
|
1385
|
+
channels.push(telegramChannel);
|
|
1386
|
+
wireChannel(telegramChannel);
|
|
1387
|
+
}
|
|
1388
|
+
// ─── Per-agent channel setup ─────────────────────────────────────
|
|
1389
|
+
// Scan all non-main agents for channels.json and create separate
|
|
1390
|
+
// Discord/Telegram client instances bound to each agent.
|
|
1391
|
+
for (const agent of agentRegistry.list()) {
|
|
1392
|
+
if (agent.isMain)
|
|
1393
|
+
continue;
|
|
1394
|
+
const channelConfig = await agentRegistry.loadChannelConfig(agent.id);
|
|
1395
|
+
if (!channelConfig)
|
|
1396
|
+
continue;
|
|
1397
|
+
// Discord
|
|
1398
|
+
const discord = channelConfig.discord;
|
|
1399
|
+
if (discord?.enabled && discord?.token) {
|
|
1400
|
+
const agentDiscord = new DiscordChannel({
|
|
1401
|
+
token: discord.token,
|
|
1402
|
+
guilds: discord.guilds,
|
|
1403
|
+
});
|
|
1404
|
+
agentDiscord.agentId = agent.id;
|
|
1405
|
+
// Register slash commands for this agent's bot too
|
|
1406
|
+
agentDiscord.setSlashCommands(nativeCommandList, async (commandName, channelId, author, guildId) => {
|
|
1407
|
+
return handleSlashCommand(commandName, channelId, author, agent.id, agentDiscord);
|
|
1408
|
+
});
|
|
1409
|
+
// Merge user-invocable skill commands before connect (use agent-specific specs if available)
|
|
1410
|
+
const agentSpecificSkillSpecs = agentSkillCommandSpecsMap.get(agent.id) ?? skillCommandSpecs;
|
|
1411
|
+
await agentDiscord.registerSkillCommands(agentSpecificSkillSpecs, async (commandName, channelId, author, guildId) => {
|
|
1412
|
+
return handleSlashCommand(commandName, channelId, author, agent.id, agentDiscord);
|
|
1413
|
+
});
|
|
1414
|
+
discordChannels.push(agentDiscord);
|
|
1415
|
+
channels.push(agentDiscord);
|
|
1416
|
+
wireChannel(agentDiscord);
|
|
1417
|
+
console.log(`[tako] Agent "${agent.id}" Discord channel configured`);
|
|
1418
|
+
}
|
|
1419
|
+
// Telegram
|
|
1420
|
+
const telegram = channelConfig.telegram;
|
|
1421
|
+
if (telegram?.enabled && telegram?.token) {
|
|
1422
|
+
const agentTelegram = new TelegramChannel({
|
|
1423
|
+
token: telegram.token,
|
|
1424
|
+
allowedUsers: telegram.allowedUsers,
|
|
1425
|
+
});
|
|
1426
|
+
agentTelegram.agentId = agent.id;
|
|
1427
|
+
// Register commands for this agent's Telegram bot
|
|
1428
|
+
agentTelegram.setCommands(nativeCommandList, async (commandName, chatId, author) => {
|
|
1429
|
+
const channelKey = `telegram:${chatId}`;
|
|
1430
|
+
const sessionKey = `agent:${agent.id}:${channelKey}`;
|
|
1431
|
+
const session = sessions.getOrCreate(sessionKey, {
|
|
1432
|
+
name: `${agent.id}/${channelKey}/${author.name}`,
|
|
1433
|
+
metadata: { agentId: agent.id, channelType: 'telegram', channelTarget: chatId, authorId: author.id },
|
|
1434
|
+
});
|
|
1435
|
+
return commandRegistry.handle('/' + commandName, {
|
|
1436
|
+
channelId: channelKey,
|
|
1437
|
+
authorId: author.id,
|
|
1438
|
+
authorName: author.name,
|
|
1439
|
+
session,
|
|
1440
|
+
agentId: agent.id,
|
|
1441
|
+
});
|
|
1442
|
+
});
|
|
1443
|
+
channels.push(agentTelegram);
|
|
1444
|
+
wireChannel(agentTelegram);
|
|
1445
|
+
console.log(`[tako] Agent "${agent.id}" Telegram channel configured`);
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
// ─── Skill-provided channels ─────────────────────────────────────
|
|
1449
|
+
// Load and register channels provided by skills (plugin pattern).
|
|
1450
|
+
const loadedSkills = skillLoader.getAll();
|
|
1451
|
+
for (const skill of loadedSkills) {
|
|
1452
|
+
if (skill.manifest.hasChannel) {
|
|
1453
|
+
const channelConfig = config.skillChannels?.[skill.manifest.name] ?? {};
|
|
1454
|
+
const channel = await loadChannelFromSkill(skill, channelConfig);
|
|
1455
|
+
if (channel) {
|
|
1456
|
+
channels.push(channel);
|
|
1457
|
+
wireChannel(channel);
|
|
1458
|
+
console.log(`[tako] Loaded skill channel: ${channel.id} (from skill: ${skill.manifest.name})`);
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
// ─── Skill extensions (unified subsystem plugins) ──────────────────
|
|
1463
|
+
const extensionRegistry = new ExtensionRegistry();
|
|
1464
|
+
// Load provider extensions
|
|
1465
|
+
for (const skill of getSkillsWithExtension(loadedSkills, 'provider')) {
|
|
1466
|
+
const providerConfig = config.skillExtensions?.[skill.manifest.name]?.provider ?? {};
|
|
1467
|
+
const provider = await loadExtension(skill, 'provider', providerConfig);
|
|
1468
|
+
if (provider) {
|
|
1469
|
+
extensionRegistry.register('provider', skill.manifest.name, provider);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
// Load memory extensions
|
|
1473
|
+
for (const skill of getSkillsWithExtension(loadedSkills, 'memory')) {
|
|
1474
|
+
const memConfig = config.skillExtensions?.[skill.manifest.name]?.memory ?? {};
|
|
1475
|
+
const memStore = await loadExtension(skill, 'memory', memConfig);
|
|
1476
|
+
if (memStore) {
|
|
1477
|
+
extensionRegistry.register('memory', skill.manifest.name, memStore);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
// Load channel extensions (via unified loader — supplements legacy hasChannel)
|
|
1481
|
+
for (const skill of getSkillsWithExtension(loadedSkills, 'channel')) {
|
|
1482
|
+
if (skill.manifest.hasChannel)
|
|
1483
|
+
continue; // Already loaded above via legacy path
|
|
1484
|
+
const chConfig = config.skillExtensions?.[skill.manifest.name]?.channel ?? {};
|
|
1485
|
+
const channel = await loadExtension(skill, 'channel', chConfig);
|
|
1486
|
+
if (channel) {
|
|
1487
|
+
extensionRegistry.register('channel', skill.manifest.name, channel);
|
|
1488
|
+
channels.push(channel);
|
|
1489
|
+
wireChannel(channel);
|
|
1490
|
+
console.log(`[tako] Loaded extension channel: ${channel.id} (from skill: ${skill.manifest.name})`);
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
// Load network extensions
|
|
1494
|
+
for (const skill of getSkillsWithExtension(loadedSkills, 'network')) {
|
|
1495
|
+
const netConfig = config.skillExtensions?.[skill.manifest.name]?.network ?? {};
|
|
1496
|
+
const adapter = await loadExtension(skill, 'network', netConfig);
|
|
1497
|
+
if (adapter) {
|
|
1498
|
+
extensionRegistry.register('network', skill.manifest.name, adapter);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
// Register message tools (channel management: send, create/delete channels, threads, react)
|
|
1502
|
+
toolRegistry.registerAll(createMessageTools({
|
|
1503
|
+
discord: discordChannel,
|
|
1504
|
+
telegram: telegramChannel,
|
|
1505
|
+
}));
|
|
1506
|
+
// Register introspection tools (tako_status, tako_config, tako_logs, session_transcript)
|
|
1507
|
+
toolRegistry.registerAll(createIntrospectTools({
|
|
1508
|
+
config,
|
|
1509
|
+
sessions,
|
|
1510
|
+
startTime,
|
|
1511
|
+
channels,
|
|
1512
|
+
agentIds: agentRegistry.list().map((a) => a.id),
|
|
1513
|
+
skillCount: skillManifests.length,
|
|
1514
|
+
version: VERSION,
|
|
1515
|
+
}));
|
|
1516
|
+
// ─── Start Gateway ────────────────────────────────────────────────
|
|
1517
|
+
// Allow env override for gateway bind (needed for Docker: bind 0.0.0.0 inside container)
|
|
1518
|
+
if (process.env['TAKO_GATEWAY_BIND']) {
|
|
1519
|
+
config.gateway.bind = process.env['TAKO_GATEWAY_BIND'];
|
|
1520
|
+
}
|
|
1521
|
+
if (process.env['TAKO_GATEWAY_PORT']) {
|
|
1522
|
+
config.gateway.port = parseInt(process.env['TAKO_GATEWAY_PORT'], 10);
|
|
1523
|
+
}
|
|
1524
|
+
const gateway = new Gateway(config.gateway, { sessions, agentLoop, hooks, sandboxManager, retryQueue });
|
|
1525
|
+
await gateway.start();
|
|
1526
|
+
// Set status info for TUI clients
|
|
1527
|
+
gateway.setStatusInfo({
|
|
1528
|
+
model: config.providers.primary,
|
|
1529
|
+
tools: toolRegistry.getActiveTools().length,
|
|
1530
|
+
skills: skillManifests.length,
|
|
1531
|
+
channels: channels.map((c) => c.id),
|
|
1532
|
+
});
|
|
1533
|
+
// Write PID file for daemon management
|
|
1534
|
+
await writePidFile({
|
|
1535
|
+
pid: process.pid,
|
|
1536
|
+
startedAt: new Date().toISOString(),
|
|
1537
|
+
port: config.gateway.port,
|
|
1538
|
+
bind: config.gateway.bind,
|
|
1539
|
+
configPath: config._configPath,
|
|
1540
|
+
});
|
|
1541
|
+
// ─── SIGUSR1 — Graceful config reload ──────────────────────────
|
|
1542
|
+
process.on('SIGUSR1', async () => {
|
|
1543
|
+
console.log('[tako] Received SIGUSR1 — reloading config...');
|
|
1544
|
+
try {
|
|
1545
|
+
const newConfig = await resolveConfig();
|
|
1546
|
+
// Update model
|
|
1547
|
+
if (newConfig.providers.primary !== config.providers.primary) {
|
|
1548
|
+
agentLoop.setModel(newConfig.providers.primary);
|
|
1549
|
+
config.providers.primary = newConfig.providers.primary;
|
|
1550
|
+
console.log(`[tako] Model updated to: ${newConfig.providers.primary}`);
|
|
1551
|
+
}
|
|
1552
|
+
// Update tool profile
|
|
1553
|
+
if (newConfig.tools.profile !== config.tools.profile) {
|
|
1554
|
+
toolRegistry.setProfile(newConfig.tools.profile);
|
|
1555
|
+
config.tools.profile = newConfig.tools.profile;
|
|
1556
|
+
console.log(`[tako] Tool profile updated to: ${newConfig.tools.profile}`);
|
|
1557
|
+
}
|
|
1558
|
+
// Reload skills
|
|
1559
|
+
const newManifests = await skillLoader.discover();
|
|
1560
|
+
for (const manifest of newManifests) {
|
|
1561
|
+
const loaded = await skillLoader.load(manifest);
|
|
1562
|
+
skillLoader.registerTools(loaded, toolRegistry);
|
|
1563
|
+
skillLoader.registerHooks(loaded, hooks);
|
|
1564
|
+
}
|
|
1565
|
+
console.log(`[tako] Config reload complete. Skills: ${newManifests.length}`);
|
|
1566
|
+
// Re-register skill commands with Discord after reload
|
|
1567
|
+
if (discordChannels.length > 0) {
|
|
1568
|
+
try {
|
|
1569
|
+
const loadedAfterReload = skillLoader.getAll();
|
|
1570
|
+
const rebuiltSpecs = buildSkillCommands(loadedAfterReload);
|
|
1571
|
+
skillCommandSpecs.splice(0, skillCommandSpecs.length, ...rebuiltSpecs);
|
|
1572
|
+
for (const dc of discordChannels) {
|
|
1573
|
+
await dc.registerSkillCommands(skillCommandSpecs, async (commandName, channelId, author, guildId) => {
|
|
1574
|
+
const agentId = dc.agentId ?? resolveAgentForChannel(agentRegistry.list(), 'discord', channelId);
|
|
1575
|
+
return handleSlashCommand(commandName, channelId, author, agentId, dc);
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
console.log(`[tako] Re-registered ${skillCommandSpecs.length} skill commands with Discord (${discordChannels.length} bot(s))`);
|
|
1579
|
+
}
|
|
1580
|
+
catch (err) {
|
|
1581
|
+
console.error('[tako] Failed to re-register commands:', err instanceof Error ? err.message : err);
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
// Update gateway status info
|
|
1585
|
+
gateway.setStatusInfo({
|
|
1586
|
+
model: config.providers.primary,
|
|
1587
|
+
tools: toolRegistry.getActiveTools().length,
|
|
1588
|
+
skills: newManifests.length,
|
|
1589
|
+
channels: channels.map((c) => c.id),
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
catch (err) {
|
|
1593
|
+
console.error('[tako] Config reload failed:', err instanceof Error ? err.message : err);
|
|
1594
|
+
}
|
|
1595
|
+
});
|
|
1596
|
+
// ─── Cron Scheduler ────────────────────────────────────────────────
|
|
1597
|
+
const { CronScheduler } = await import('./core/cron.js');
|
|
1598
|
+
const { createCronTools } = await import('./tools/cron-tools.js');
|
|
1599
|
+
const cronScheduler = new CronScheduler();
|
|
1600
|
+
cronScheduler.setHandlers({
|
|
1601
|
+
agentTurn: async (message, model) => {
|
|
1602
|
+
const cronSession = sessions.create({ name: 'cron', metadata: { isCron: true } });
|
|
1603
|
+
let response = '';
|
|
1604
|
+
for await (const chunk of agentLoop.run(cronSession, message)) {
|
|
1605
|
+
response += chunk;
|
|
1606
|
+
}
|
|
1607
|
+
return response;
|
|
1608
|
+
},
|
|
1609
|
+
systemEvent: (text) => {
|
|
1610
|
+
// Inject into main session
|
|
1611
|
+
const mainSession = sessions.get('main') ?? sessions.create({ name: 'main' });
|
|
1612
|
+
sessions.addMessage(mainSession.id, { role: 'system', content: text });
|
|
1613
|
+
},
|
|
1614
|
+
delivery: (result, delivery) => {
|
|
1615
|
+
if (delivery.mode === 'announce' && delivery.channel) {
|
|
1616
|
+
const ch = channels.find((c) => c.id === delivery.channel || c.id.startsWith(delivery.channel));
|
|
1617
|
+
if (ch) {
|
|
1618
|
+
ch.send({ target: delivery.to ?? '', content: `📋 **${result.jobName}**\n${result.response.slice(0, 1500)}` });
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
},
|
|
1622
|
+
});
|
|
1623
|
+
toolRegistry.registerAll(createCronTools(cronScheduler));
|
|
1624
|
+
await cronScheduler.start();
|
|
1625
|
+
// ─── Session idle sweep ──────────────────────────────────────────
|
|
1626
|
+
// Every 2 minutes, check for sessions idle > 24h and archive them.
|
|
1627
|
+
// Files stay on disk — only removed from active maps.
|
|
1628
|
+
const idleSweepTimer = setInterval(async () => {
|
|
1629
|
+
const expired = sessions.sweepIdle();
|
|
1630
|
+
for (const session of expired) {
|
|
1631
|
+
const channelType = session.metadata.channelType;
|
|
1632
|
+
const target = session.metadata.channelTarget;
|
|
1633
|
+
if (channelType && target) {
|
|
1634
|
+
const channel = channels.find((ch) => ch.id === channelType);
|
|
1635
|
+
if (channel) {
|
|
1636
|
+
await channel.send({
|
|
1637
|
+
target,
|
|
1638
|
+
content: '⚙️ Session ended automatically after 24h of inactivity.',
|
|
1639
|
+
}).catch(() => { });
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
sessions.archiveSession(session.id);
|
|
1643
|
+
}
|
|
1644
|
+
if (expired.length > 0) {
|
|
1645
|
+
console.log(`[tako] Archived ${expired.length} idle session(s)`);
|
|
1646
|
+
}
|
|
1647
|
+
// Sweep expired thread bindings (24h idle)
|
|
1648
|
+
const expiredBindings = threadBindings.sweepExpired();
|
|
1649
|
+
for (const binding of expiredBindings) {
|
|
1650
|
+
const discordCh = channels.find((ch) => ch.id === 'discord');
|
|
1651
|
+
if (discordCh) {
|
|
1652
|
+
await discordCh.send({
|
|
1653
|
+
target: binding.threadId,
|
|
1654
|
+
content: '⚙️ Session ended automatically after 24h of inactivity. Messages here will no longer be routed.',
|
|
1655
|
+
}).catch(() => { });
|
|
1656
|
+
// Archive the thread
|
|
1657
|
+
if ('archiveThread' in discordCh && typeof discordCh.archiveThread === 'function') {
|
|
1658
|
+
await discordCh.archiveThread(binding.threadId).catch(() => { });
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
if (expiredBindings.length > 0) {
|
|
1663
|
+
await threadBindings.save();
|
|
1664
|
+
console.log(`[tako] Swept ${expiredBindings.length} expired thread binding(s)`);
|
|
1665
|
+
}
|
|
1666
|
+
}, 120_000);
|
|
1667
|
+
// ─── Daily 4 AM session rotation ──────────────────────────────────
|
|
1668
|
+
// Start fresh sessions every day at 4:00 AM local time.
|
|
1669
|
+
// Old session files stay on disk for history.
|
|
1670
|
+
let rotationTimeout = null;
|
|
1671
|
+
function scheduleNextRotation() {
|
|
1672
|
+
const now = new Date();
|
|
1673
|
+
const next4am = new Date(now);
|
|
1674
|
+
next4am.setHours(4, 0, 0, 0);
|
|
1675
|
+
if (next4am <= now) {
|
|
1676
|
+
next4am.setDate(next4am.getDate() + 1);
|
|
1677
|
+
}
|
|
1678
|
+
const delay = next4am.getTime() - now.getTime();
|
|
1679
|
+
console.log(`[tako] Next session rotation at 4:00 AM (in ${Math.round(delay / 60000)}min)`);
|
|
1680
|
+
rotationTimeout = setTimeout(async () => {
|
|
1681
|
+
console.log('[tako] Running daily 4 AM session rotation...');
|
|
1682
|
+
try {
|
|
1683
|
+
const result = await sessions.rotateAllSessions();
|
|
1684
|
+
console.log(`[tako] Rotated: ${result.archived.length} archived, ${result.created.length} created`);
|
|
1685
|
+
}
|
|
1686
|
+
catch (err) {
|
|
1687
|
+
console.error('[tako] Rotation error:', err instanceof Error ? err.message : err);
|
|
1688
|
+
}
|
|
1689
|
+
scheduleNextRotation();
|
|
1690
|
+
}, delay);
|
|
1691
|
+
}
|
|
1692
|
+
scheduleNextRotation();
|
|
1693
|
+
// ─── Shutdown ─────────────────────────────────────────────────────
|
|
1694
|
+
const blockingIds = new Set(['cli', 'tui']);
|
|
1695
|
+
async function shutdown() {
|
|
1696
|
+
console.log('\n[tako] Shutting down...');
|
|
1697
|
+
// Log shutdown — don't broadcast to channels (too noisy on restarts)
|
|
1698
|
+
console.log('⚙️ Tako going offline.');
|
|
1699
|
+
clearInterval(idleSweepTimer);
|
|
1700
|
+
if (rotationTimeout)
|
|
1701
|
+
clearTimeout(rotationTimeout);
|
|
1702
|
+
messageQueue.clear();
|
|
1703
|
+
await threadBindings.save();
|
|
1704
|
+
cronScheduler.stop();
|
|
1705
|
+
skillLoader.stopWatching();
|
|
1706
|
+
deliveryQueue.stop();
|
|
1707
|
+
for (const ch of channels) {
|
|
1708
|
+
await ch.disconnect().catch(() => { });
|
|
1709
|
+
}
|
|
1710
|
+
await gateway.stop();
|
|
1711
|
+
await sandboxManager.shutdown();
|
|
1712
|
+
await sessions.shutdown();
|
|
1713
|
+
await removePidFile();
|
|
1714
|
+
}
|
|
1715
|
+
process.on('SIGINT', async () => { await shutdown(); process.exit(0); });
|
|
1716
|
+
process.on('SIGTERM', async () => { await shutdown(); process.exit(0); });
|
|
1717
|
+
// ─── Print startup banner ─────────────────────────────────────────
|
|
1718
|
+
const embeddingStatus = embeddingProvider ? 'vector+BM25' : 'BM25-only';
|
|
1719
|
+
const channelNames = channels.map((c) => c.id).join(', ');
|
|
1720
|
+
const loadedSkillNames = skillLoader.getAll().map((s) => s.manifest.name);
|
|
1721
|
+
// TUI has its own header — skip the text banner
|
|
1722
|
+
if (!useTui) {
|
|
1723
|
+
console.log(`Tako 🐙 v${VERSION}`);
|
|
1724
|
+
console.log(`Provider: ${resolvedProviderLabel}`);
|
|
1725
|
+
console.log(`Tools: ${toolRegistry.getActiveTools().length} active (profile: ${config.tools.profile})`);
|
|
1726
|
+
console.log(`Memory: ${embeddingStatus}`);
|
|
1727
|
+
console.log(`Skills: ${loadedSkillNames.length} loaded (${loadedSkillNames.join(', ') || 'none'})`);
|
|
1728
|
+
console.log(`Channels: ${channelNames}`);
|
|
1729
|
+
console.log(`Sandbox: ${config.sandbox.mode}${config.sandbox.mode !== 'off' ? ` (scope: ${config.sandbox.scope}, workspace: ${config.sandbox.workspaceAccess})` : ''}`);
|
|
1730
|
+
console.log(`Agents: ${agentRegistry.list().length} registered (${agentRegistry.list().map((a) => a.id).join(', ')})`);
|
|
1731
|
+
console.log(`Gateway: ws://${config.gateway.bind}:${config.gateway.port}`);
|
|
1732
|
+
console.log('Type /quit to exit.\n');
|
|
1733
|
+
}
|
|
1734
|
+
// Connect channels (CLI/TUI last since they block on input)
|
|
1735
|
+
for (const ch of channels) {
|
|
1736
|
+
if (!blockingIds.has(ch.id)) {
|
|
1737
|
+
try {
|
|
1738
|
+
await ch.connect();
|
|
1739
|
+
}
|
|
1740
|
+
catch (err) {
|
|
1741
|
+
console.error(`[${ch.id}] ✗ Failed to connect: ${err instanceof Error ? err.message : err}`);
|
|
1742
|
+
console.error(`[${ch.id}] Check your token/config with \`tako onboard\``);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
// Helper: send a system message to all connected non-blocking channels
|
|
1747
|
+
async function broadcastToChannels(text, includeAgentChannels = false) {
|
|
1748
|
+
for (const ch of channels) {
|
|
1749
|
+
if (blockingIds.has(ch.id))
|
|
1750
|
+
continue;
|
|
1751
|
+
if (!includeAgentChannels && ch.agentId)
|
|
1752
|
+
continue;
|
|
1753
|
+
try {
|
|
1754
|
+
if (ch.broadcast) {
|
|
1755
|
+
await ch.broadcast(text);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
catch { /* channel may not be connected yet */ }
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
// Wait a moment for channels to fully connect (Discord ClientReady, etc.)
|
|
1762
|
+
const hasExternalChannels = channels.some((ch) => !blockingIds.has(ch.id));
|
|
1763
|
+
if (hasExternalChannels) {
|
|
1764
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
1765
|
+
}
|
|
1766
|
+
// Log startup — don't broadcast to channels (too noisy on restarts)
|
|
1767
|
+
console.log(`🐙 Tako online — model: ${config.providers.primary}`);
|
|
1768
|
+
// Deliver restart note if one exists (from a prior system_restart call)
|
|
1769
|
+
try {
|
|
1770
|
+
const { readFileSync, unlinkSync } = await import('node:fs');
|
|
1771
|
+
const { homedir } = await import('node:os');
|
|
1772
|
+
const restartNotePath = join(homedir(), '.tako', 'restart-note.json');
|
|
1773
|
+
const raw = readFileSync(restartNotePath, 'utf-8');
|
|
1774
|
+
const restartNote = JSON.parse(raw);
|
|
1775
|
+
unlinkSync(restartNotePath);
|
|
1776
|
+
const noteText = `⚙️ ${restartNote.note}`;
|
|
1777
|
+
console.log(`[tako] Post-restart: ${restartNote.note}`);
|
|
1778
|
+
// Deliver to connected non-blocking channels (Discord, Telegram)
|
|
1779
|
+
await broadcastToChannels(noteText);
|
|
1780
|
+
}
|
|
1781
|
+
catch { /* no restart note, normal boot */ }
|
|
1782
|
+
// Connect the blocking channel (CLI or TUI) last
|
|
1783
|
+
const blockingChannel = channels.find((ch) => blockingIds.has(ch.id));
|
|
1784
|
+
if (blockingChannel) {
|
|
1785
|
+
await blockingChannel.connect();
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
// ─── tako start --daemon ─────────────────────────────────────────────
|
|
1789
|
+
// ─── tako stop ──────────────────────────────────────────────────────
|
|
1790
|
+
// ─── tako restart ───────────────────────────────────────────────────
|
|
1791
|
+
// ─── tako tui ───────────────────────────────────────────────────────
|
|
1792
|
+
// ─── tako dev ───────────────────────────────────────────────────────
|
|
1793
|
+
// ─── tako doctor ─────────────────────────────────────────────────────
|
|
1794
|
+
async function runNuke(args) {
|
|
1795
|
+
const { homedir } = await import('node:os');
|
|
1796
|
+
const { rm, readdir } = await import('node:fs/promises');
|
|
1797
|
+
const { join } = await import('node:path');
|
|
1798
|
+
const readline = await import('node:readline');
|
|
1799
|
+
const takoDir = join(homedir(), '.tako');
|
|
1800
|
+
console.log('');
|
|
1801
|
+
console.log(' ⚠️ ⚠️ ⚠️ TAKO NUKE ⚠️ ⚠️ ⚠️');
|
|
1802
|
+
console.log('');
|
|
1803
|
+
console.log(' This will PERMANENTLY DELETE:');
|
|
1804
|
+
console.log('');
|
|
1805
|
+
// Show what exists
|
|
1806
|
+
const targets = [];
|
|
1807
|
+
const checks = [
|
|
1808
|
+
{ name: 'Config', path: join(takoDir, 'tako.json'), description: 'tako.json (provider, channel, agent config)' },
|
|
1809
|
+
{ name: 'Auth', path: join(takoDir, 'auth'), description: 'auth/ (API keys, OAuth tokens)' },
|
|
1810
|
+
{ name: 'Workspace', path: join(takoDir, 'workspace'), description: 'workspace/ (SOUL.md, AGENTS.md, memory, files)' },
|
|
1811
|
+
{ name: 'Agents', path: join(takoDir, 'agents'), description: 'agents/ (all agent configs and state)' },
|
|
1812
|
+
{ name: 'Sessions', path: join(takoDir, 'sessions'), description: 'sessions/ (conversation history)' },
|
|
1813
|
+
{ name: 'Mods', path: join(takoDir, 'mods'), description: 'mods/ (installed mods and their workspaces)' },
|
|
1814
|
+
{ name: 'Cron', path: join(takoDir, 'cron'), description: 'cron/ (scheduled jobs)' },
|
|
1815
|
+
{ name: 'PID', path: join(takoDir, 'tako.pid'), description: 'tako.pid (daemon PID file)' },
|
|
1816
|
+
];
|
|
1817
|
+
const { existsSync } = await import('node:fs');
|
|
1818
|
+
for (const check of checks) {
|
|
1819
|
+
if (existsSync(check.path)) {
|
|
1820
|
+
targets.push(check);
|
|
1821
|
+
console.log(` ✗ ${check.description}`);
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
if (targets.length === 0) {
|
|
1825
|
+
console.log(' (nothing found — ~/.tako/ is already clean)');
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
console.log('');
|
|
1829
|
+
console.log(` Location: ${takoDir}`);
|
|
1830
|
+
console.log('');
|
|
1831
|
+
// Triple confirmation
|
|
1832
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1833
|
+
const ask = (q) => new Promise((res) => rl.question(q, res));
|
|
1834
|
+
const answer1 = await ask(' Type "nuke" to confirm: ');
|
|
1835
|
+
if (answer1.trim().toLowerCase() !== 'nuke') {
|
|
1836
|
+
console.log(' Cancelled.');
|
|
1837
|
+
rl.close();
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
const answer2 = await ask(' Are you SURE? This cannot be undone. Type "yes i am sure": ');
|
|
1841
|
+
if (answer2.trim().toLowerCase() !== 'yes i am sure') {
|
|
1842
|
+
console.log(' Cancelled.');
|
|
1843
|
+
rl.close();
|
|
1844
|
+
return;
|
|
1845
|
+
}
|
|
1846
|
+
rl.close();
|
|
1847
|
+
console.log('');
|
|
1848
|
+
console.log(' Nuking...');
|
|
1849
|
+
// Stop daemon first if running
|
|
1850
|
+
try {
|
|
1851
|
+
const { getDaemonStatus, removePidFile } = await import('./daemon/pid.js');
|
|
1852
|
+
const status = await getDaemonStatus();
|
|
1853
|
+
if (status.running && status.info) {
|
|
1854
|
+
console.log(` Stopping daemon (PID: ${status.info.pid})...`);
|
|
1855
|
+
process.kill(status.info.pid, 'SIGTERM');
|
|
1856
|
+
await new Promise((r) => setTimeout(r, 2000));
|
|
1857
|
+
}
|
|
1858
|
+
await removePidFile();
|
|
1859
|
+
}
|
|
1860
|
+
catch { /* not running */ }
|
|
1861
|
+
// Delete everything
|
|
1862
|
+
for (const target of targets) {
|
|
1863
|
+
try {
|
|
1864
|
+
await rm(target.path, { recursive: true, force: true });
|
|
1865
|
+
console.log(` ✓ Deleted ${target.name}`);
|
|
1866
|
+
}
|
|
1867
|
+
catch (err) {
|
|
1868
|
+
console.error(` ✗ Failed to delete ${target.name}: ${err instanceof Error ? err.message : err}`);
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
console.log('');
|
|
1872
|
+
console.log(' 🐙 Tako has been reset to factory defaults.');
|
|
1873
|
+
console.log(' Run `tako onboard` to set up again.');
|
|
1874
|
+
console.log('');
|
|
1875
|
+
}
|
|
1876
|
+
async function runMod(args) {
|
|
1877
|
+
const { ModManager } = await import('./mods/mod.js');
|
|
1878
|
+
const mods = new ModManager();
|
|
1879
|
+
const sub = args[0];
|
|
1880
|
+
switch (sub) {
|
|
1881
|
+
case 'list':
|
|
1882
|
+
case 'ls': {
|
|
1883
|
+
const all = await mods.list();
|
|
1884
|
+
const active = await mods.getActive();
|
|
1885
|
+
console.log(`Active: ${active}\n`);
|
|
1886
|
+
if (all.length === 0) {
|
|
1887
|
+
console.log('No mods installed.');
|
|
1888
|
+
console.log(' tako mod create <name> "description" Create a new mod');
|
|
1889
|
+
console.log(' tako mod install <path|git-url> Install a mod');
|
|
1890
|
+
}
|
|
1891
|
+
else {
|
|
1892
|
+
for (const mod of all) {
|
|
1893
|
+
const marker = mod.isActive ? ' ← active' : '';
|
|
1894
|
+
console.log(` ${mod.name} v${mod.manifest.version}${marker}`);
|
|
1895
|
+
if (mod.manifest.description)
|
|
1896
|
+
console.log(` ${mod.manifest.description}`);
|
|
1897
|
+
if (mod.manifest.author)
|
|
1898
|
+
console.log(` by ${mod.manifest.author}`);
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
break;
|
|
1902
|
+
}
|
|
1903
|
+
case 'use':
|
|
1904
|
+
case 'switch': {
|
|
1905
|
+
const name = args[1];
|
|
1906
|
+
if (!name) {
|
|
1907
|
+
console.log('Usage: tako mod use <name|main>');
|
|
1908
|
+
process.exit(1);
|
|
1909
|
+
}
|
|
1910
|
+
const result = await mods.use(name);
|
|
1911
|
+
console.log(result.message);
|
|
1912
|
+
if (!result.success)
|
|
1913
|
+
process.exit(1);
|
|
1914
|
+
break;
|
|
1915
|
+
}
|
|
1916
|
+
case 'install':
|
|
1917
|
+
case 'add': {
|
|
1918
|
+
const source = args[1];
|
|
1919
|
+
if (!source) {
|
|
1920
|
+
console.log('Usage: tako mod install <path|git-url>');
|
|
1921
|
+
process.exit(1);
|
|
1922
|
+
}
|
|
1923
|
+
const result = source.includes('://') || source.endsWith('.git')
|
|
1924
|
+
? await mods.installFromGit(source)
|
|
1925
|
+
: await mods.install(source);
|
|
1926
|
+
console.log(result.message);
|
|
1927
|
+
if (!result.success)
|
|
1928
|
+
process.exit(1);
|
|
1929
|
+
break;
|
|
1930
|
+
}
|
|
1931
|
+
case 'create':
|
|
1932
|
+
case 'new': {
|
|
1933
|
+
const name = args[1];
|
|
1934
|
+
const desc = args.slice(2).join(' ') || 'A Tako mod';
|
|
1935
|
+
if (!name) {
|
|
1936
|
+
console.log('Usage: tako mod create <name> [description]');
|
|
1937
|
+
process.exit(1);
|
|
1938
|
+
}
|
|
1939
|
+
const result = await mods.create(name, desc);
|
|
1940
|
+
console.log(result.message);
|
|
1941
|
+
break;
|
|
1942
|
+
}
|
|
1943
|
+
case 'remove':
|
|
1944
|
+
case 'rm': {
|
|
1945
|
+
const name = args[1];
|
|
1946
|
+
if (!name) {
|
|
1947
|
+
console.log('Usage: tako mod remove <name>');
|
|
1948
|
+
process.exit(1);
|
|
1949
|
+
}
|
|
1950
|
+
const result = await mods.remove(name);
|
|
1951
|
+
console.log(result.message);
|
|
1952
|
+
if (!result.success)
|
|
1953
|
+
process.exit(1);
|
|
1954
|
+
break;
|
|
1955
|
+
}
|
|
1956
|
+
case 'info': {
|
|
1957
|
+
const name = args[1] ?? await mods.getActive();
|
|
1958
|
+
const all = await mods.list();
|
|
1959
|
+
const mod = all.find((m) => m.name === name);
|
|
1960
|
+
if (!mod) {
|
|
1961
|
+
console.log(`Mod "${name}" not found.`);
|
|
1962
|
+
process.exit(1);
|
|
1963
|
+
}
|
|
1964
|
+
console.log(`${mod.manifest.name} v${mod.manifest.version}`);
|
|
1965
|
+
if (mod.manifest.description)
|
|
1966
|
+
console.log(`Description: ${mod.manifest.description}`);
|
|
1967
|
+
if (mod.manifest.author)
|
|
1968
|
+
console.log(`Author: ${mod.manifest.author}`);
|
|
1969
|
+
if (mod.manifest.source)
|
|
1970
|
+
console.log(`Source: ${mod.manifest.source}`);
|
|
1971
|
+
if (mod.manifest.tags?.length)
|
|
1972
|
+
console.log(`Tags: ${mod.manifest.tags.join(', ')}`);
|
|
1973
|
+
console.log(`Path: ${mod.path}`);
|
|
1974
|
+
console.log(`Active: ${mod.isActive}`);
|
|
1975
|
+
if (mod.config.provider)
|
|
1976
|
+
console.log(`Provider: ${mod.config.provider}`);
|
|
1977
|
+
break;
|
|
1978
|
+
}
|
|
1979
|
+
default:
|
|
1980
|
+
console.log('Tako Mod Hub 🐙\n');
|
|
1981
|
+
console.log('Usage: tako mod <command>\n');
|
|
1982
|
+
console.log('Commands:');
|
|
1983
|
+
console.log(' list List installed mods');
|
|
1984
|
+
console.log(' use <name|main> Switch to a mod (or back to main)');
|
|
1985
|
+
console.log(' install <path|git-url> Install a mod from local dir or git');
|
|
1986
|
+
console.log(' create <name> [desc] Create a new empty mod');
|
|
1987
|
+
console.log(' remove <name> Remove an installed mod');
|
|
1988
|
+
console.log(' info [name] Show mod details');
|
|
1989
|
+
console.log('');
|
|
1990
|
+
console.log('Mods are stored at: ~/.tako/mods/');
|
|
1991
|
+
console.log('');
|
|
1992
|
+
console.log('A mod packages: identity (SOUL.md), skills, workspace templates,');
|
|
1993
|
+
console.log('and config overrides — everything except your API keys and bot tokens.');
|
|
1994
|
+
console.log('');
|
|
1995
|
+
console.log('⚠️ After switching mods, restart Tako and reconnect channels if needed.');
|
|
1996
|
+
break;
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
async function runDoctor(args) {
|
|
2000
|
+
const config = await resolveConfig();
|
|
2001
|
+
const doctor = new Doctor();
|
|
2002
|
+
doctor.addCheck(checkConfig);
|
|
2003
|
+
doctor.addCheck(checkProviders);
|
|
2004
|
+
doctor.addCheck(checkChannels);
|
|
2005
|
+
doctor.addCheck(checkMemory);
|
|
2006
|
+
doctor.addCheck(checkSessions);
|
|
2007
|
+
doctor.addCheck(checkPermissions);
|
|
2008
|
+
doctor.addCheck(checkBrowser);
|
|
2009
|
+
console.log('Tako Doctor — running health checks...\n');
|
|
2010
|
+
const results = await doctor.run(config, {
|
|
2011
|
+
autoRepair: args.includes('--yes') || args.includes('-y'),
|
|
2012
|
+
deep: args.includes('--deep'),
|
|
2013
|
+
});
|
|
2014
|
+
doctor.printResults(results);
|
|
2015
|
+
const hasErrors = results.some((r) => r.status === 'error');
|
|
2016
|
+
process.exit(hasErrors ? 1 : 0);
|
|
2017
|
+
}
|
|
2018
|
+
// ─── tako skills ─────────────────────────────────────────────────────
|
|
2019
|
+
async function runSkills(args) {
|
|
2020
|
+
const subcommand = args[0];
|
|
2021
|
+
if (!subcommand || subcommand === 'list') {
|
|
2022
|
+
await skillsList();
|
|
2023
|
+
}
|
|
2024
|
+
else if (subcommand === 'install') {
|
|
2025
|
+
const name = args[1];
|
|
2026
|
+
if (!name) {
|
|
2027
|
+
console.error('Usage: tako skills install <name>');
|
|
2028
|
+
console.error(' Example: tako skills install vercel-labs/agent-skills@find-skills');
|
|
2029
|
+
process.exit(1);
|
|
2030
|
+
}
|
|
2031
|
+
await skillsInstall(name);
|
|
2032
|
+
}
|
|
2033
|
+
else if (subcommand === 'info') {
|
|
2034
|
+
const name = args[1];
|
|
2035
|
+
if (!name) {
|
|
2036
|
+
console.error('Usage: tako skills info <name>');
|
|
2037
|
+
process.exit(1);
|
|
2038
|
+
}
|
|
2039
|
+
await skillsInfo(name);
|
|
2040
|
+
}
|
|
2041
|
+
else if (subcommand === 'check') {
|
|
2042
|
+
await skillsCheck();
|
|
2043
|
+
}
|
|
2044
|
+
else if (subcommand === 'audit') {
|
|
2045
|
+
const name = args[1];
|
|
2046
|
+
if (!name) {
|
|
2047
|
+
console.error('Usage: tako skills audit <name>');
|
|
2048
|
+
process.exit(1);
|
|
2049
|
+
}
|
|
2050
|
+
await skillsAudit(name);
|
|
2051
|
+
}
|
|
2052
|
+
else if (subcommand === 'search') {
|
|
2053
|
+
const query = args.slice(1).join(' ');
|
|
2054
|
+
if (!query) {
|
|
2055
|
+
console.error('Usage: tako skills search <query>');
|
|
2056
|
+
process.exit(1);
|
|
2057
|
+
}
|
|
2058
|
+
const { SkillMarketplace } = await import('./skills/marketplace.js');
|
|
2059
|
+
const marketplace = new SkillMarketplace();
|
|
2060
|
+
const results = await marketplace.search(query);
|
|
2061
|
+
if (results.length === 0) {
|
|
2062
|
+
console.log('No skills found matching your query.');
|
|
2063
|
+
return;
|
|
2064
|
+
}
|
|
2065
|
+
console.log(`Found ${results.length} skill(s):\n`);
|
|
2066
|
+
for (const r of results) {
|
|
2067
|
+
console.log(` ${r.fullName} (${r.stars} stars)`);
|
|
2068
|
+
console.log(` ${r.description || '(no description)'}`);
|
|
2069
|
+
console.log(` Install: tako skills install ${r.fullName}`);
|
|
2070
|
+
console.log();
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
else if (subcommand === 'update') {
|
|
2074
|
+
const name = args[1];
|
|
2075
|
+
const { SkillMarketplace } = await import('./skills/marketplace.js');
|
|
2076
|
+
const marketplace = new SkillMarketplace();
|
|
2077
|
+
const updated = await marketplace.update(name);
|
|
2078
|
+
console.log(`Updated ${updated.length} skill(s):`);
|
|
2079
|
+
for (const s of updated) {
|
|
2080
|
+
console.log(` ${s.name} → ${s.version ?? 'latest'}`);
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
else if (subcommand === 'remove' || subcommand === 'rm') {
|
|
2084
|
+
const name = args[1];
|
|
2085
|
+
if (!name) {
|
|
2086
|
+
console.error('Usage: tako skills remove <name>');
|
|
2087
|
+
process.exit(1);
|
|
2088
|
+
}
|
|
2089
|
+
const { SkillMarketplace } = await import('./skills/marketplace.js');
|
|
2090
|
+
const marketplace = new SkillMarketplace();
|
|
2091
|
+
const removed = await marketplace.remove(name);
|
|
2092
|
+
if (removed) {
|
|
2093
|
+
console.log(`Removed skill: ${name}`);
|
|
2094
|
+
}
|
|
2095
|
+
else {
|
|
2096
|
+
console.error(`Skill not found: ${name}`);
|
|
2097
|
+
process.exit(1);
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
else {
|
|
2101
|
+
console.error(`Unknown skills subcommand: ${subcommand}`);
|
|
2102
|
+
console.error('Available: list, install, search, update, remove, info, check, audit');
|
|
2103
|
+
process.exit(1);
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
async function skillsList() {
|
|
2107
|
+
const config = await resolveConfig();
|
|
2108
|
+
const loader = new SkillLoader(config.skills.dirs);
|
|
2109
|
+
const manifests = await loader.discover();
|
|
2110
|
+
if (manifests.length === 0) {
|
|
2111
|
+
console.log('No skills installed.');
|
|
2112
|
+
console.log('\nInstall skills with: tako skills install <name>');
|
|
2113
|
+
console.log('Browse available skills at: https://skills.sh/');
|
|
2114
|
+
return;
|
|
2115
|
+
}
|
|
2116
|
+
console.log(`Discovered ${manifests.length} skill(s):\n`);
|
|
2117
|
+
for (const m of manifests) {
|
|
2118
|
+
const triggers = m.triggers
|
|
2119
|
+
? m.triggers.map((t) => t.type === 'keyword' ? t.value : t.type).join(', ')
|
|
2120
|
+
: 'always';
|
|
2121
|
+
console.log(` ${m.name} (v${m.version})`);
|
|
2122
|
+
console.log(` ${m.description.slice(0, 80)}${m.description.length > 80 ? '...' : ''}`);
|
|
2123
|
+
console.log(` Triggers: ${triggers}`);
|
|
2124
|
+
console.log(` Path: ${m.rootDir}`);
|
|
2125
|
+
console.log();
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
async function skillsInstall(nameOrRef) {
|
|
2129
|
+
const { execSync } = await import('node:child_process');
|
|
2130
|
+
console.log(`Installing skill: ${nameOrRef}...`);
|
|
2131
|
+
try {
|
|
2132
|
+
execSync(`npx skills add ${nameOrRef} -y`, { stdio: 'inherit', cwd: process.cwd() });
|
|
2133
|
+
console.log('\nSkill installed. Run `tako skills list` to verify.');
|
|
2134
|
+
}
|
|
2135
|
+
catch {
|
|
2136
|
+
console.error('\nFailed to install skill. Make sure `npx skills` is available.');
|
|
2137
|
+
console.error('You can also manually create a skill directory in ./skills/ with a SKILL.md file.');
|
|
2138
|
+
process.exit(1);
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
async function skillsInfo(name) {
|
|
2142
|
+
const config = await resolveConfig();
|
|
2143
|
+
const loader = new SkillLoader(config.skills.dirs);
|
|
2144
|
+
const manifests = await loader.discover();
|
|
2145
|
+
const manifest = manifests.find((m) => m.name === name);
|
|
2146
|
+
if (!manifest) {
|
|
2147
|
+
console.error(`Skill not found: ${name}`);
|
|
2148
|
+
console.error(`Available skills: ${manifests.map((m) => m.name).join(', ') || 'none'}`);
|
|
2149
|
+
process.exit(1);
|
|
2150
|
+
}
|
|
2151
|
+
const loaded = await loader.load(manifest);
|
|
2152
|
+
console.log(`Skill: ${manifest.name}`);
|
|
2153
|
+
console.log(`Version: ${manifest.version}`);
|
|
2154
|
+
if (manifest.author)
|
|
2155
|
+
console.log(`Author: ${manifest.author}`);
|
|
2156
|
+
console.log(`Description: ${manifest.description}`);
|
|
2157
|
+
console.log(`Path: ${manifest.rootDir}`);
|
|
2158
|
+
console.log(`SKILL.md: ${manifest.skillPath}`);
|
|
2159
|
+
if (manifest.triggers && manifest.triggers.length > 0) {
|
|
2160
|
+
console.log(`\nTriggers:`);
|
|
2161
|
+
for (const t of manifest.triggers) {
|
|
2162
|
+
console.log(` - ${t.type}${t.value ? `: ${t.value}` : ''}`);
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
if (loaded.tools.length > 0) {
|
|
2166
|
+
console.log(`\nTools (${loaded.tools.length}):`);
|
|
2167
|
+
for (const t of loaded.tools) {
|
|
2168
|
+
console.log(` - ${t.name}: ${t.description}`);
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
const preview = loaded.instructions.slice(0, 500);
|
|
2172
|
+
console.log(`\nInstructions (${loaded.instructions.length} chars):`);
|
|
2173
|
+
console.log(preview + (loaded.instructions.length > 500 ? '\n ...' : ''));
|
|
2174
|
+
}
|
|
2175
|
+
async function skillsCheck() {
|
|
2176
|
+
const config = await resolveConfig();
|
|
2177
|
+
const loader = new SkillLoader(config.skills.dirs);
|
|
2178
|
+
const manifests = await loader.discover();
|
|
2179
|
+
if (manifests.length === 0) {
|
|
2180
|
+
console.log('No skills discovered.');
|
|
2181
|
+
return;
|
|
2182
|
+
}
|
|
2183
|
+
console.log(`Checking ${manifests.length} skill(s):\n`);
|
|
2184
|
+
let ready = 0;
|
|
2185
|
+
let failed = 0;
|
|
2186
|
+
for (const m of manifests) {
|
|
2187
|
+
try {
|
|
2188
|
+
const loaded = await loader.load(m);
|
|
2189
|
+
console.log(` ✓ ${m.name} (v${m.version}) — ${loaded.tools.length} tool(s)`);
|
|
2190
|
+
ready++;
|
|
2191
|
+
}
|
|
2192
|
+
catch (err) {
|
|
2193
|
+
console.log(` ✗ ${m.name} (v${m.version}) — ${err instanceof Error ? err.message : 'load failed'}`);
|
|
2194
|
+
failed++;
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
console.log(`\n${ready} ready, ${failed} failed`);
|
|
2198
|
+
}
|
|
2199
|
+
async function skillsAudit(name) {
|
|
2200
|
+
const config = await resolveConfig();
|
|
2201
|
+
const loader = new SkillLoader(config.skills.dirs);
|
|
2202
|
+
const manifests = await loader.discover();
|
|
2203
|
+
const manifest = manifests.find((m) => m.name === name);
|
|
2204
|
+
if (!manifest) {
|
|
2205
|
+
console.error(`Skill not found: ${name}`);
|
|
2206
|
+
console.error(`Available skills: ${manifests.map((m) => m.name).join(', ') || 'none'}`);
|
|
2207
|
+
process.exit(1);
|
|
2208
|
+
}
|
|
2209
|
+
const loaded = await loader.load(manifest);
|
|
2210
|
+
console.log(`Security Audit: ${manifest.name} (v${manifest.version})\n`);
|
|
2211
|
+
console.log(`Author: ${manifest.author ?? 'unknown'}`);
|
|
2212
|
+
console.log(`Path: ${manifest.rootDir}`);
|
|
2213
|
+
// Tools analysis
|
|
2214
|
+
console.log(`\nTools (${loaded.tools.length}):`);
|
|
2215
|
+
for (const t of loaded.tools) {
|
|
2216
|
+
const params = t.parameters ? Object.keys(t.parameters.properties ?? {}).join(', ') : 'none';
|
|
2217
|
+
console.log(` ${t.name}: ${t.description}`);
|
|
2218
|
+
console.log(` Parameters: ${params}`);
|
|
2219
|
+
}
|
|
2220
|
+
// Triggers analysis
|
|
2221
|
+
if (manifest.triggers && manifest.triggers.length > 0) {
|
|
2222
|
+
console.log(`\nTriggers (${manifest.triggers.length}):`);
|
|
2223
|
+
for (const t of manifest.triggers) {
|
|
2224
|
+
console.log(` - ${t.type}${t.value ? `: ${t.value}` : ''}`);
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
else {
|
|
2228
|
+
console.log('\nTriggers: always active (no triggers defined)');
|
|
2229
|
+
}
|
|
2230
|
+
// Instruction size
|
|
2231
|
+
console.log(`\nInstruction size: ${loaded.instructions.length} chars`);
|
|
2232
|
+
// Warnings
|
|
2233
|
+
const warnings = [];
|
|
2234
|
+
if (!manifest.author)
|
|
2235
|
+
warnings.push('No author specified');
|
|
2236
|
+
if (!manifest.triggers || manifest.triggers.length === 0)
|
|
2237
|
+
warnings.push('Always active (no trigger gating)');
|
|
2238
|
+
if (loaded.instructions.length > 10000)
|
|
2239
|
+
warnings.push(`Large instructions (${loaded.instructions.length} chars may impact context)`);
|
|
2240
|
+
if (loaded.tools.length > 5)
|
|
2241
|
+
warnings.push(`Many tools (${loaded.tools.length}) — consider splitting`);
|
|
2242
|
+
if (warnings.length > 0) {
|
|
2243
|
+
console.log(`\nWarnings:`);
|
|
2244
|
+
for (const w of warnings) {
|
|
2245
|
+
console.log(` ⚠ ${w}`);
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
else {
|
|
2249
|
+
console.log('\nNo warnings.');
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
// ─── tako sandbox ────────────────────────────────────────────────────
|
|
2253
|
+
async function runSandbox(args) {
|
|
2254
|
+
const subcommand = args[0] ?? 'status';
|
|
2255
|
+
const config = await resolveConfig();
|
|
2256
|
+
switch (subcommand) {
|
|
2257
|
+
case 'status': {
|
|
2258
|
+
const manager = new SandboxManager(config.sandbox);
|
|
2259
|
+
const status = await manager.getStatus();
|
|
2260
|
+
console.log('Tako Sandbox Status\n');
|
|
2261
|
+
console.log(`Mode: ${status.mode}`);
|
|
2262
|
+
console.log(`Docker: ${status.dockerAvailable ? 'available' : 'NOT available'}`);
|
|
2263
|
+
console.log(`Scope: ${config.sandbox.scope}`);
|
|
2264
|
+
console.log(`Workspace access: ${config.sandbox.workspaceAccess}`);
|
|
2265
|
+
console.log(`Image: ${config.sandbox.docker?.image ?? 'tako-sandbox:bookworm-slim'}`);
|
|
2266
|
+
console.log(`Network: ${config.sandbox.docker?.network ?? 'none'}`);
|
|
2267
|
+
if (status.dockerAvailable) {
|
|
2268
|
+
const containers = await DockerContainer.listSandboxContainers();
|
|
2269
|
+
if (containers.length > 0) {
|
|
2270
|
+
console.log(`\nActive sandbox containers (${containers.length}):`);
|
|
2271
|
+
for (const c of containers) {
|
|
2272
|
+
console.log(` ${c.id} ${c.name} (${c.running ? 'running' : 'stopped'})`);
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
else {
|
|
2276
|
+
console.log('\nNo active sandbox containers.');
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
// Exec policy
|
|
2280
|
+
if (config.tools.exec) {
|
|
2281
|
+
console.log(`\nExec policy:`);
|
|
2282
|
+
console.log(` Security: ${config.tools.exec.security}`);
|
|
2283
|
+
if (config.tools.exec.allowlist) {
|
|
2284
|
+
console.log(` Allowlist: ${config.tools.exec.allowlist.length} patterns`);
|
|
2285
|
+
}
|
|
2286
|
+
if (config.tools.exec.timeout) {
|
|
2287
|
+
console.log(` Timeout: ${config.tools.exec.timeout}ms`);
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
else {
|
|
2291
|
+
console.log(`\nExec policy: full (no restrictions)`);
|
|
2292
|
+
}
|
|
2293
|
+
break;
|
|
2294
|
+
}
|
|
2295
|
+
case 'explain': {
|
|
2296
|
+
const toolName = args[1];
|
|
2297
|
+
if (!toolName) {
|
|
2298
|
+
console.error('Usage: tako sandbox explain <tool-name>');
|
|
2299
|
+
console.error(' Example: tako sandbox explain exec');
|
|
2300
|
+
process.exit(1);
|
|
2301
|
+
}
|
|
2302
|
+
// Sandbox explanation
|
|
2303
|
+
const manager = new SandboxManager(config.sandbox);
|
|
2304
|
+
console.log(manager.explain(toolName, true));
|
|
2305
|
+
console.log();
|
|
2306
|
+
// Tool policy explanation
|
|
2307
|
+
const toolPolicy = new ToolPolicy({
|
|
2308
|
+
profile: config.tools.profile,
|
|
2309
|
+
allow: config.tools.allow,
|
|
2310
|
+
deny: config.tools.deny,
|
|
2311
|
+
sandbox: config.tools.sandbox,
|
|
2312
|
+
exec: config.tools.exec ? {
|
|
2313
|
+
security: config.tools.exec.security,
|
|
2314
|
+
allowlist: config.tools.exec.allowlist,
|
|
2315
|
+
timeout: config.tools.exec.timeout,
|
|
2316
|
+
maxOutputSize: config.tools.exec.maxOutputSize,
|
|
2317
|
+
} : undefined,
|
|
2318
|
+
});
|
|
2319
|
+
console.log('Tool Policy:');
|
|
2320
|
+
console.log(toolPolicy.explain(toolName, config.sandbox.mode !== 'off'));
|
|
2321
|
+
break;
|
|
2322
|
+
}
|
|
2323
|
+
case 'cleanup': {
|
|
2324
|
+
const dockerOk = await DockerContainer.isDockerAvailable();
|
|
2325
|
+
if (!dockerOk) {
|
|
2326
|
+
console.log('Docker is not available. Nothing to clean up.');
|
|
2327
|
+
return;
|
|
2328
|
+
}
|
|
2329
|
+
const count = await DockerContainer.cleanupAll();
|
|
2330
|
+
console.log(`Removed ${count} sandbox container(s).`);
|
|
2331
|
+
break;
|
|
2332
|
+
}
|
|
2333
|
+
default:
|
|
2334
|
+
console.error(`Unknown sandbox subcommand: ${subcommand}`);
|
|
2335
|
+
console.error('Available: status, explain <tool>, cleanup');
|
|
2336
|
+
process.exit(1);
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
// ─── tako agents ─────────────────────────────────────────────────────
|
|
2340
|
+
async function runAgents(args) {
|
|
2341
|
+
const config = await resolveConfig();
|
|
2342
|
+
const registry = new AgentRegistry(config.agents, config.providers.primary);
|
|
2343
|
+
await registry.loadDynamic();
|
|
2344
|
+
const subcommand = args[0] ?? 'list';
|
|
2345
|
+
switch (subcommand) {
|
|
2346
|
+
case 'list': {
|
|
2347
|
+
const showBindings = args.includes('--bindings');
|
|
2348
|
+
const agents = registry.list();
|
|
2349
|
+
if (agents.length === 0) {
|
|
2350
|
+
console.log('No agents configured (only default "main" agent).');
|
|
2351
|
+
return;
|
|
2352
|
+
}
|
|
2353
|
+
console.log(`Agents (${agents.length}):\n`);
|
|
2354
|
+
for (const agent of agents) {
|
|
2355
|
+
console.log(` ${agent.id}${agent.isMain ? ' (main)' : ''}`);
|
|
2356
|
+
console.log(` Workspace: ${agent.workspace}`);
|
|
2357
|
+
console.log(` Model: ${agent.model}`);
|
|
2358
|
+
if (agent.description)
|
|
2359
|
+
console.log(` Description: ${agent.description}`);
|
|
2360
|
+
if (agent.canSpawn.length > 0)
|
|
2361
|
+
console.log(` Can spawn: ${agent.canSpawn.join(', ')}`);
|
|
2362
|
+
if (showBindings && Object.keys(agent.bindings).length > 0) {
|
|
2363
|
+
console.log(` Bindings: ${JSON.stringify(agent.bindings)}`);
|
|
2364
|
+
}
|
|
2365
|
+
console.log();
|
|
2366
|
+
}
|
|
2367
|
+
break;
|
|
2368
|
+
}
|
|
2369
|
+
case 'add': {
|
|
2370
|
+
const nameArg = args[1];
|
|
2371
|
+
const hasFlags = args.some((a) => a.startsWith('--'));
|
|
2372
|
+
const isInteractive = !nameArg && !hasFlags;
|
|
2373
|
+
let agentName;
|
|
2374
|
+
let workspace;
|
|
2375
|
+
let model;
|
|
2376
|
+
let description;
|
|
2377
|
+
let discordChannels;
|
|
2378
|
+
let telegramUsers;
|
|
2379
|
+
if (isInteractive) {
|
|
2380
|
+
// ─── Interactive wizard ────────────────────────────────────
|
|
2381
|
+
const p = await import('@clack/prompts');
|
|
2382
|
+
p.intro('Tako 🐙 — New Agent Setup');
|
|
2383
|
+
const nameResult = await p.text({
|
|
2384
|
+
message: 'Agent name (lowercase, hyphens ok)',
|
|
2385
|
+
placeholder: 'code-agent',
|
|
2386
|
+
validate: (v) => {
|
|
2387
|
+
if (!v)
|
|
2388
|
+
return 'Name is required';
|
|
2389
|
+
if (!/^[a-z][a-z0-9-]*$/.test(v))
|
|
2390
|
+
return 'Use lowercase letters, numbers, and hyphens';
|
|
2391
|
+
if (v === 'main')
|
|
2392
|
+
return '"main" is reserved';
|
|
2393
|
+
if (registry.has(v))
|
|
2394
|
+
return `Agent "${v}" already exists`;
|
|
2395
|
+
return undefined;
|
|
2396
|
+
},
|
|
2397
|
+
});
|
|
2398
|
+
if (p.isCancel(nameResult)) {
|
|
2399
|
+
p.cancel('Cancelled.');
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2402
|
+
agentName = nameResult;
|
|
2403
|
+
const descResult = await p.text({
|
|
2404
|
+
message: 'Description (what does this agent do?)',
|
|
2405
|
+
placeholder: 'Handles code review and refactoring tasks',
|
|
2406
|
+
});
|
|
2407
|
+
if (p.isCancel(descResult)) {
|
|
2408
|
+
p.cancel('Cancelled.');
|
|
2409
|
+
return;
|
|
2410
|
+
}
|
|
2411
|
+
description = descResult || undefined;
|
|
2412
|
+
const wsResult = await p.text({
|
|
2413
|
+
message: 'Workspace path',
|
|
2414
|
+
placeholder: `~/.tako/workspace-${agentName}`,
|
|
2415
|
+
defaultValue: `~/.tako/workspace-${agentName}`,
|
|
2416
|
+
});
|
|
2417
|
+
if (p.isCancel(wsResult)) {
|
|
2418
|
+
p.cancel('Cancelled.');
|
|
2419
|
+
return;
|
|
2420
|
+
}
|
|
2421
|
+
workspace = wsResult || `~/.tako/workspace-${agentName}`;
|
|
2422
|
+
const modelResult = await p.select({
|
|
2423
|
+
message: 'Model',
|
|
2424
|
+
options: [
|
|
2425
|
+
{ value: '', label: `Inherit from main (${config.providers.primary})`, hint: 'recommended' },
|
|
2426
|
+
{ value: 'anthropic/claude-sonnet-4-6', label: 'claude-sonnet-4-6', hint: 'fast, balanced' },
|
|
2427
|
+
{ value: 'anthropic/claude-opus-4-6', label: 'claude-opus-4-6', hint: 'powerful, slower' },
|
|
2428
|
+
{ value: 'anthropic/claude-haiku-4-5', label: 'claude-haiku-4-5', hint: 'fastest, cheapest' },
|
|
2429
|
+
],
|
|
2430
|
+
});
|
|
2431
|
+
if (p.isCancel(modelResult)) {
|
|
2432
|
+
p.cancel('Cancelled.');
|
|
2433
|
+
return;
|
|
2434
|
+
}
|
|
2435
|
+
model = modelResult || undefined;
|
|
2436
|
+
// Channel bindings
|
|
2437
|
+
const bindResult = await p.confirm({
|
|
2438
|
+
message: 'Set up channel bindings? (route specific channels to this agent)',
|
|
2439
|
+
initialValue: false,
|
|
2440
|
+
});
|
|
2441
|
+
if (p.isCancel(bindResult)) {
|
|
2442
|
+
p.cancel('Cancelled.');
|
|
2443
|
+
return;
|
|
2444
|
+
}
|
|
2445
|
+
if (bindResult) {
|
|
2446
|
+
if (config.channels.discord?.token) {
|
|
2447
|
+
const dcResult = await p.text({
|
|
2448
|
+
message: 'Discord channel names (comma-separated, or empty to skip)',
|
|
2449
|
+
placeholder: 'coding, code-review',
|
|
2450
|
+
});
|
|
2451
|
+
if (p.isCancel(dcResult)) {
|
|
2452
|
+
p.cancel('Cancelled.');
|
|
2453
|
+
return;
|
|
2454
|
+
}
|
|
2455
|
+
if (dcResult) {
|
|
2456
|
+
discordChannels = dcResult.split(',').map((s) => s.trim()).filter(Boolean);
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
if (config.channels.telegram?.token) {
|
|
2460
|
+
const tgResult = await p.text({
|
|
2461
|
+
message: 'Telegram user IDs to route (comma-separated, or empty to skip)',
|
|
2462
|
+
placeholder: '123456789',
|
|
2463
|
+
});
|
|
2464
|
+
if (p.isCancel(tgResult)) {
|
|
2465
|
+
p.cancel('Cancelled.');
|
|
2466
|
+
return;
|
|
2467
|
+
}
|
|
2468
|
+
if (tgResult) {
|
|
2469
|
+
telegramUsers = tgResult.split(',').map((s) => s.trim()).filter(Boolean);
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
// Confirmation
|
|
2474
|
+
p.note([
|
|
2475
|
+
`Name: ${agentName}`,
|
|
2476
|
+
`Description: ${description || '(none)'}`,
|
|
2477
|
+
`Workspace: ${workspace}`,
|
|
2478
|
+
`Model: ${model || `inherit (${config.providers.primary})`}`,
|
|
2479
|
+
discordChannels ? `Discord: ${discordChannels.join(', ')}` : null,
|
|
2480
|
+
telegramUsers ? `Telegram: ${telegramUsers.join(', ')}` : null,
|
|
2481
|
+
].filter(Boolean).join('\n'), 'New Agent');
|
|
2482
|
+
const confirmResult = await p.confirm({ message: 'Create this agent?', initialValue: true });
|
|
2483
|
+
if (p.isCancel(confirmResult) || !confirmResult) {
|
|
2484
|
+
p.cancel('Cancelled.');
|
|
2485
|
+
return;
|
|
2486
|
+
}
|
|
2487
|
+
}
|
|
2488
|
+
else {
|
|
2489
|
+
// ─── Non-interactive (flags) ──────────────────────────────
|
|
2490
|
+
if (!nameArg) {
|
|
2491
|
+
console.error('Usage: tako agents add <name> [--workspace <path>] [--model <model>] [--description <desc>]');
|
|
2492
|
+
console.error(' tako agents add (interactive wizard)');
|
|
2493
|
+
process.exit(1);
|
|
2494
|
+
}
|
|
2495
|
+
agentName = nameArg;
|
|
2496
|
+
const workspaceIdx = args.indexOf('--workspace');
|
|
2497
|
+
const modelIdx = args.indexOf('--model');
|
|
2498
|
+
const descIdx = args.indexOf('--description');
|
|
2499
|
+
const discordIdx = args.indexOf('--discord-channels');
|
|
2500
|
+
const telegramIdx = args.indexOf('--telegram-users');
|
|
2501
|
+
workspace = workspaceIdx >= 0 ? args[workspaceIdx + 1] : undefined;
|
|
2502
|
+
model = modelIdx >= 0 ? args[modelIdx + 1] : undefined;
|
|
2503
|
+
description = descIdx >= 0 ? args[descIdx + 1] : undefined;
|
|
2504
|
+
if (discordIdx >= 0 && args[discordIdx + 1]) {
|
|
2505
|
+
discordChannels = args[discordIdx + 1].split(',').map((s) => s.trim());
|
|
2506
|
+
}
|
|
2507
|
+
if (telegramIdx >= 0 && args[telegramIdx + 1]) {
|
|
2508
|
+
telegramUsers = args[telegramIdx + 1].split(',').map((s) => s.trim());
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
// Build bindings
|
|
2512
|
+
const bindings = {};
|
|
2513
|
+
if (discordChannels && discordChannels.length > 0) {
|
|
2514
|
+
bindings.discord = { channels: discordChannels };
|
|
2515
|
+
}
|
|
2516
|
+
if (telegramUsers && telegramUsers.length > 0) {
|
|
2517
|
+
bindings.telegram = { users: telegramUsers };
|
|
2518
|
+
}
|
|
2519
|
+
try {
|
|
2520
|
+
const agent = await registry.add({
|
|
2521
|
+
id: agentName,
|
|
2522
|
+
workspace,
|
|
2523
|
+
model: model ? { primary: model } : undefined,
|
|
2524
|
+
description,
|
|
2525
|
+
bindings: Object.keys(bindings).length > 0 ? bindings : undefined,
|
|
2526
|
+
});
|
|
2527
|
+
console.log(`\nAgent created: ${agent.id}`);
|
|
2528
|
+
console.log(` Workspace: ${agent.workspace}`);
|
|
2529
|
+
console.log(` State dir: ${agent.stateDir}`);
|
|
2530
|
+
console.log(` Sessions: ${agent.sessionDir}`);
|
|
2531
|
+
console.log(` Model: ${agent.model}`);
|
|
2532
|
+
if (agent.description)
|
|
2533
|
+
console.log(` Description: ${agent.description}`);
|
|
2534
|
+
if (Object.keys(agent.bindings).length > 0) {
|
|
2535
|
+
console.log(` Bindings: ${JSON.stringify(agent.bindings)}`);
|
|
2536
|
+
}
|
|
2537
|
+
console.log(`\nWorkspace files created:`);
|
|
2538
|
+
console.log(` AGENTS.md — Operating instructions`);
|
|
2539
|
+
console.log(` SOUL.md — Personality & values`);
|
|
2540
|
+
console.log(` IDENTITY.md — Name, capabilities`);
|
|
2541
|
+
console.log(` USER.md — User profile (empty)`);
|
|
2542
|
+
console.log(` TOOLS.md — Tool learnings (empty)`);
|
|
2543
|
+
console.log(` HEARTBEAT.md — Status update behavior`);
|
|
2544
|
+
console.log(` BOOTSTRAP.md — First-run ritual`);
|
|
2545
|
+
console.log(` memory/MEMORY.md — Long-term memory`);
|
|
2546
|
+
}
|
|
2547
|
+
catch (err) {
|
|
2548
|
+
console.error(`Failed to create agent: ${err instanceof Error ? err.message : err}`);
|
|
2549
|
+
process.exit(1);
|
|
2550
|
+
}
|
|
2551
|
+
break;
|
|
2552
|
+
}
|
|
2553
|
+
case 'remove': {
|
|
2554
|
+
const name = args[1];
|
|
2555
|
+
if (!name) {
|
|
2556
|
+
console.error('Usage: tako agents remove <name>');
|
|
2557
|
+
process.exit(1);
|
|
2558
|
+
}
|
|
2559
|
+
try {
|
|
2560
|
+
const removed = await registry.remove(name);
|
|
2561
|
+
if (removed) {
|
|
2562
|
+
console.log(`Agent removed: ${name}`);
|
|
2563
|
+
console.log('Note: agent workspace was preserved (only state directory was removed).');
|
|
2564
|
+
}
|
|
2565
|
+
else {
|
|
2566
|
+
console.error(`Agent not found: ${name}`);
|
|
2567
|
+
process.exit(1);
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
catch (err) {
|
|
2571
|
+
console.error(`Failed to remove agent: ${err instanceof Error ? err.message : err}`);
|
|
2572
|
+
process.exit(1);
|
|
2573
|
+
}
|
|
2574
|
+
break;
|
|
2575
|
+
}
|
|
2576
|
+
case 'info': {
|
|
2577
|
+
const name = args[1];
|
|
2578
|
+
if (!name) {
|
|
2579
|
+
console.error('Usage: tako agents info <name>');
|
|
2580
|
+
process.exit(1);
|
|
2581
|
+
}
|
|
2582
|
+
const info = registry.info(name);
|
|
2583
|
+
if (!info) {
|
|
2584
|
+
console.error(`Agent not found: ${name}`);
|
|
2585
|
+
console.error(`Available agents: ${registry.list().map((a) => a.id).join(', ')}`);
|
|
2586
|
+
process.exit(1);
|
|
2587
|
+
}
|
|
2588
|
+
console.log(`Agent: ${info.id}`);
|
|
2589
|
+
console.log(JSON.stringify(info, null, 2));
|
|
2590
|
+
break;
|
|
2591
|
+
}
|
|
2592
|
+
case 'bind': {
|
|
2593
|
+
const agentId = args[1];
|
|
2594
|
+
const channel = args.includes('--channel') ? args[args.indexOf('--channel') + 1] : undefined;
|
|
2595
|
+
const target = args.includes('--target') ? args[args.indexOf('--target') + 1] : undefined;
|
|
2596
|
+
if (!agentId || !channel || !target) {
|
|
2597
|
+
console.error('Usage: tako agents bind <agentId> --channel <discord|telegram> --target <channelId>');
|
|
2598
|
+
process.exit(1);
|
|
2599
|
+
}
|
|
2600
|
+
const agent = registry.get(agentId);
|
|
2601
|
+
if (!agent) {
|
|
2602
|
+
console.error(`Agent not found: ${agentId}`);
|
|
2603
|
+
process.exit(1);
|
|
2604
|
+
}
|
|
2605
|
+
// Update bindings
|
|
2606
|
+
const bindings = { ...agent.bindings };
|
|
2607
|
+
if (channel === 'discord') {
|
|
2608
|
+
const existing = bindings.discord?.channels ?? [];
|
|
2609
|
+
if (!existing.includes(target)) {
|
|
2610
|
+
bindings.discord = { channels: [...existing, target] };
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
else if (channel === 'telegram') {
|
|
2614
|
+
const existing = bindings.telegram?.users ?? [];
|
|
2615
|
+
if (!existing.includes(target)) {
|
|
2616
|
+
bindings.telegram = { users: [...existing, target] };
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
else {
|
|
2620
|
+
console.error(`Unknown channel type: ${channel}. Use "discord" or "telegram".`);
|
|
2621
|
+
process.exit(1);
|
|
2622
|
+
}
|
|
2623
|
+
// Persist to agent.json
|
|
2624
|
+
const agentJsonPath = join(homedir(), '.tako', 'agents', agentId, 'agent.json');
|
|
2625
|
+
if (existsSync(agentJsonPath)) {
|
|
2626
|
+
const raw = await readFile(agentJsonPath, 'utf-8');
|
|
2627
|
+
const entry = JSON.parse(raw);
|
|
2628
|
+
entry.bindings = bindings;
|
|
2629
|
+
await writeFile(agentJsonPath, JSON.stringify(entry, null, 2), 'utf-8');
|
|
2630
|
+
}
|
|
2631
|
+
console.log(`Bound ${channel}/${target} → agent ${agentId}`);
|
|
2632
|
+
break;
|
|
2633
|
+
}
|
|
2634
|
+
case 'unbind': {
|
|
2635
|
+
const agentId = args[1];
|
|
2636
|
+
const channel = args.includes('--channel') ? args[args.indexOf('--channel') + 1] : undefined;
|
|
2637
|
+
const target = args.includes('--target') ? args[args.indexOf('--target') + 1] : undefined;
|
|
2638
|
+
if (!agentId || !channel || !target) {
|
|
2639
|
+
console.error('Usage: tako agents unbind <agentId> --channel <discord|telegram> --target <channelId>');
|
|
2640
|
+
process.exit(1);
|
|
2641
|
+
}
|
|
2642
|
+
const agent = registry.get(agentId);
|
|
2643
|
+
if (!agent) {
|
|
2644
|
+
console.error(`Agent not found: ${agentId}`);
|
|
2645
|
+
process.exit(1);
|
|
2646
|
+
}
|
|
2647
|
+
const bindings = { ...agent.bindings };
|
|
2648
|
+
if (channel === 'discord' && bindings.discord) {
|
|
2649
|
+
bindings.discord.channels = bindings.discord.channels.filter((c) => c !== target);
|
|
2650
|
+
}
|
|
2651
|
+
else if (channel === 'telegram' && bindings.telegram) {
|
|
2652
|
+
bindings.telegram.users = (bindings.telegram.users ?? []).filter((u) => u !== target);
|
|
2653
|
+
}
|
|
2654
|
+
const agentJsonPath = join(homedir(), '.tako', 'agents', agentId, 'agent.json');
|
|
2655
|
+
if (existsSync(agentJsonPath)) {
|
|
2656
|
+
const raw = await readFile(agentJsonPath, 'utf-8');
|
|
2657
|
+
const entry = JSON.parse(raw);
|
|
2658
|
+
entry.bindings = bindings;
|
|
2659
|
+
await writeFile(agentJsonPath, JSON.stringify(entry, null, 2), 'utf-8');
|
|
2660
|
+
}
|
|
2661
|
+
console.log(`Unbound ${channel}/${target} from agent ${agentId}`);
|
|
2662
|
+
break;
|
|
2663
|
+
}
|
|
2664
|
+
case 'bindings': {
|
|
2665
|
+
const agents = registry.list();
|
|
2666
|
+
let hasBindings = false;
|
|
2667
|
+
console.log('Agent Bindings:\n');
|
|
2668
|
+
for (const agent of agents) {
|
|
2669
|
+
const b = agent.bindings;
|
|
2670
|
+
if (!b || (Object.keys(b).length === 0))
|
|
2671
|
+
continue;
|
|
2672
|
+
hasBindings = true;
|
|
2673
|
+
console.log(` ${agent.id}:`);
|
|
2674
|
+
if (b.discord?.channels?.length) {
|
|
2675
|
+
console.log(` Discord: ${b.discord.channels.join(', ')}`);
|
|
2676
|
+
}
|
|
2677
|
+
if (b.telegram?.users?.length) {
|
|
2678
|
+
console.log(` Telegram users: ${b.telegram.users.join(', ')}`);
|
|
2679
|
+
}
|
|
2680
|
+
if (b.telegram?.groups?.length) {
|
|
2681
|
+
console.log(` Telegram groups: ${b.telegram.groups.join(', ')}`);
|
|
2682
|
+
}
|
|
2683
|
+
if (b.cli) {
|
|
2684
|
+
console.log(` CLI: bound`);
|
|
2685
|
+
}
|
|
2686
|
+
console.log();
|
|
2687
|
+
}
|
|
2688
|
+
if (!hasBindings) {
|
|
2689
|
+
console.log(' No bindings configured.');
|
|
2690
|
+
console.log('\n Add bindings with: tako agents bind <agentId> --channel discord --target <channelId>');
|
|
2691
|
+
}
|
|
2692
|
+
break;
|
|
2693
|
+
}
|
|
2694
|
+
case 'set-identity': {
|
|
2695
|
+
const agentId = args[1];
|
|
2696
|
+
if (!agentId) {
|
|
2697
|
+
console.error('Usage: tako agents set-identity <agentId> --name <name> [--emoji <emoji>]');
|
|
2698
|
+
process.exit(1);
|
|
2699
|
+
}
|
|
2700
|
+
const agent = registry.get(agentId);
|
|
2701
|
+
if (!agent) {
|
|
2702
|
+
console.error(`Agent not found: ${agentId}`);
|
|
2703
|
+
process.exit(1);
|
|
2704
|
+
}
|
|
2705
|
+
const nameArg = args.includes('--name') ? args[args.indexOf('--name') + 1] : undefined;
|
|
2706
|
+
const emojiArg = args.includes('--emoji') ? args[args.indexOf('--emoji') + 1] : undefined;
|
|
2707
|
+
if (!nameArg && !emojiArg) {
|
|
2708
|
+
console.error('Provide at least --name or --emoji');
|
|
2709
|
+
process.exit(1);
|
|
2710
|
+
}
|
|
2711
|
+
const agentJsonPath = join(homedir(), '.tako', 'agents', agentId, 'agent.json');
|
|
2712
|
+
let entry = {};
|
|
2713
|
+
if (existsSync(agentJsonPath)) {
|
|
2714
|
+
const raw = await readFile(agentJsonPath, 'utf-8');
|
|
2715
|
+
entry = JSON.parse(raw);
|
|
2716
|
+
}
|
|
2717
|
+
if (nameArg)
|
|
2718
|
+
entry.displayName = nameArg;
|
|
2719
|
+
if (emojiArg)
|
|
2720
|
+
entry.emoji = emojiArg;
|
|
2721
|
+
await writeFile(agentJsonPath, JSON.stringify(entry, null, 2), 'utf-8');
|
|
2722
|
+
console.log(`Updated identity for agent ${agentId}:`);
|
|
2723
|
+
if (nameArg)
|
|
2724
|
+
console.log(` Name: ${nameArg}`);
|
|
2725
|
+
if (emojiArg)
|
|
2726
|
+
console.log(` Emoji: ${emojiArg}`);
|
|
2727
|
+
break;
|
|
2728
|
+
}
|
|
2729
|
+
default:
|
|
2730
|
+
console.error(`Unknown agents subcommand: ${subcommand}`);
|
|
2731
|
+
console.error('Available: list, add, remove, info, bind, unbind, bindings, set-identity');
|
|
2732
|
+
process.exit(1);
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
// ─── tako sessions ──────────────────────────────────────────────────
|
|
2736
|
+
async function runSessions(args) {
|
|
2737
|
+
const config = await resolveConfig();
|
|
2738
|
+
const sessions = new SessionManager();
|
|
2739
|
+
const agentRegistry = new AgentRegistry(config.agents, config.providers.primary);
|
|
2740
|
+
await agentRegistry.loadDynamic();
|
|
2741
|
+
const agentSessionDirs = new Map();
|
|
2742
|
+
for (const agent of agentRegistry.list()) {
|
|
2743
|
+
agentSessionDirs.set(agent.id, agent.sessionDir);
|
|
2744
|
+
}
|
|
2745
|
+
await sessions.enablePersistence(agentSessionDirs);
|
|
2746
|
+
const subcommand = args[0] ?? 'list';
|
|
2747
|
+
switch (subcommand) {
|
|
2748
|
+
case 'list': {
|
|
2749
|
+
const agentFilter = args.includes('--agent') ? args[args.indexOf('--agent') + 1] : undefined;
|
|
2750
|
+
let allSessions = sessions.list();
|
|
2751
|
+
if (agentFilter) {
|
|
2752
|
+
allSessions = allSessions.filter((s) => (s.metadata.agentId ?? 'main') === agentFilter);
|
|
2753
|
+
}
|
|
2754
|
+
// Sort by most recent
|
|
2755
|
+
allSessions.sort((a, b) => b.lastActiveAt.getTime() - a.lastActiveAt.getTime());
|
|
2756
|
+
if (allSessions.length === 0) {
|
|
2757
|
+
console.log('No sessions found.');
|
|
2758
|
+
sessions.stopPersistence();
|
|
2759
|
+
return;
|
|
2760
|
+
}
|
|
2761
|
+
console.log(`Sessions (${allSessions.length}):\n`);
|
|
2762
|
+
for (const s of allSessions.slice(0, 50)) {
|
|
2763
|
+
const agentId = s.metadata.agentId ?? 'main';
|
|
2764
|
+
const isSubAgent = s.metadata.isSubAgent ? ' [sub-agent]' : '';
|
|
2765
|
+
console.log(` ${s.id.slice(0, 8)} ${s.name} (${agentId}${isSubAgent})`);
|
|
2766
|
+
console.log(` Messages: ${s.messages.length} Last active: ${s.lastActiveAt.toISOString()}`);
|
|
2767
|
+
}
|
|
2768
|
+
break;
|
|
2769
|
+
}
|
|
2770
|
+
case 'history': {
|
|
2771
|
+
const sessionId = args[1];
|
|
2772
|
+
if (!sessionId) {
|
|
2773
|
+
console.error('Usage: tako sessions history <session-id> [--limit <n>]');
|
|
2774
|
+
process.exit(1);
|
|
2775
|
+
}
|
|
2776
|
+
// Support partial session ID match
|
|
2777
|
+
let session = sessions.get(sessionId);
|
|
2778
|
+
if (!session) {
|
|
2779
|
+
const match = sessions.list().find((s) => s.id.startsWith(sessionId));
|
|
2780
|
+
if (match)
|
|
2781
|
+
session = match;
|
|
2782
|
+
}
|
|
2783
|
+
if (!session) {
|
|
2784
|
+
console.error(`Session not found: ${sessionId}`);
|
|
2785
|
+
sessions.stopPersistence();
|
|
2786
|
+
process.exit(1);
|
|
2787
|
+
}
|
|
2788
|
+
const limitIdx = args.indexOf('--limit');
|
|
2789
|
+
const limit = limitIdx >= 0 ? parseInt(args[limitIdx + 1] ?? '20', 10) : 20;
|
|
2790
|
+
const messages = session.messages.slice(-limit);
|
|
2791
|
+
console.log(`Session: ${session.name} (${session.id})`);
|
|
2792
|
+
console.log(`Total messages: ${session.messages.length}\n`);
|
|
2793
|
+
for (const msg of messages) {
|
|
2794
|
+
const content = typeof msg.content === 'string'
|
|
2795
|
+
? msg.content.slice(0, 200)
|
|
2796
|
+
: '[complex content]';
|
|
2797
|
+
console.log(` [${msg.role}] ${content}`);
|
|
2798
|
+
}
|
|
2799
|
+
break;
|
|
2800
|
+
}
|
|
2801
|
+
case 'inspect': {
|
|
2802
|
+
const sessionId = args[1];
|
|
2803
|
+
if (!sessionId) {
|
|
2804
|
+
console.error('Usage: tako sessions inspect <session-id>');
|
|
2805
|
+
process.exit(1);
|
|
2806
|
+
}
|
|
2807
|
+
let session = sessions.get(sessionId);
|
|
2808
|
+
if (!session) {
|
|
2809
|
+
const match = sessions.list().find((s) => s.id.startsWith(sessionId));
|
|
2810
|
+
if (match)
|
|
2811
|
+
session = match;
|
|
2812
|
+
}
|
|
2813
|
+
if (!session) {
|
|
2814
|
+
console.error(`Session not found: ${sessionId}`);
|
|
2815
|
+
sessions.stopPersistence();
|
|
2816
|
+
process.exit(1);
|
|
2817
|
+
}
|
|
2818
|
+
console.log(`Session: ${session.name}`);
|
|
2819
|
+
console.log(` ID: ${session.id}`);
|
|
2820
|
+
console.log(` Created: ${session.createdAt.toISOString()}`);
|
|
2821
|
+
console.log(` Last active: ${session.lastActiveAt.toISOString()}`);
|
|
2822
|
+
console.log(` Messages: ${session.messages.length}`);
|
|
2823
|
+
console.log(` Agent: ${session.metadata.agentId ?? 'main'}`);
|
|
2824
|
+
if (session.metadata.isSubAgent)
|
|
2825
|
+
console.log(` Sub-agent: yes`);
|
|
2826
|
+
if (Object.keys(session.metadata).length > 0) {
|
|
2827
|
+
console.log(` Metadata: ${JSON.stringify(session.metadata, null, 2)}`);
|
|
2828
|
+
}
|
|
2829
|
+
// Show last 10 messages
|
|
2830
|
+
const recent = session.messages.slice(-10);
|
|
2831
|
+
if (recent.length > 0) {
|
|
2832
|
+
console.log(`\nRecent messages (last ${recent.length}):\n`);
|
|
2833
|
+
for (const msg of recent) {
|
|
2834
|
+
const content = typeof msg.content === 'string'
|
|
2835
|
+
? msg.content.slice(0, 300)
|
|
2836
|
+
: '[complex content]';
|
|
2837
|
+
console.log(` [${msg.role}] ${content}`);
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
break;
|
|
2841
|
+
}
|
|
2842
|
+
case 'compact': {
|
|
2843
|
+
const sessionId = args[1];
|
|
2844
|
+
if (!sessionId) {
|
|
2845
|
+
console.error('Usage: tako sessions compact <session-id>');
|
|
2846
|
+
process.exit(1);
|
|
2847
|
+
}
|
|
2848
|
+
console.log('Session compaction requires the daemon to be running.');
|
|
2849
|
+
console.log('Use the /compact command inside an active session instead.');
|
|
2850
|
+
console.log('Or start Tako and the auto-compaction will handle it based on your config.');
|
|
2851
|
+
break;
|
|
2852
|
+
}
|
|
2853
|
+
case 'clear': {
|
|
2854
|
+
const sessionId = args[1];
|
|
2855
|
+
if (!sessionId) {
|
|
2856
|
+
console.error('Usage: tako sessions clear <session-id>');
|
|
2857
|
+
process.exit(1);
|
|
2858
|
+
}
|
|
2859
|
+
let session = sessions.get(sessionId);
|
|
2860
|
+
if (!session) {
|
|
2861
|
+
const match = sessions.list().find((s) => s.id.startsWith(sessionId));
|
|
2862
|
+
if (match)
|
|
2863
|
+
session = match;
|
|
2864
|
+
}
|
|
2865
|
+
if (!session) {
|
|
2866
|
+
console.error(`Session not found: ${sessionId}`);
|
|
2867
|
+
sessions.stopPersistence();
|
|
2868
|
+
process.exit(1);
|
|
2869
|
+
}
|
|
2870
|
+
// Archive by renaming the session file
|
|
2871
|
+
const agentId = session.metadata.agentId ?? 'main';
|
|
2872
|
+
const agentDir = agentSessionDirs.get(agentId);
|
|
2873
|
+
if (agentDir) {
|
|
2874
|
+
const sessionFile = join(agentDir, `${session.id}.jsonl`);
|
|
2875
|
+
const archiveFile = join(agentDir, `${session.id}.jsonl.archived`);
|
|
2876
|
+
if (existsSync(sessionFile)) {
|
|
2877
|
+
await rename(sessionFile, archiveFile);
|
|
2878
|
+
console.log(`Session ${session.id.slice(0, 8)} archived.`);
|
|
2879
|
+
console.log(` File moved to: ${archiveFile}`);
|
|
2880
|
+
}
|
|
2881
|
+
else {
|
|
2882
|
+
console.log('Session file not found on disk (may be in-memory only).');
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
break;
|
|
2886
|
+
}
|
|
2887
|
+
default:
|
|
2888
|
+
console.error(`Unknown sessions subcommand: ${subcommand}`);
|
|
2889
|
+
console.error('Available: list, history, inspect, compact, clear');
|
|
2890
|
+
process.exit(1);
|
|
2891
|
+
}
|
|
2892
|
+
sessions.stopPersistence();
|
|
2893
|
+
}
|
|
2894
|
+
// ─── tako status ─────────────────────────────────────────────────────
|
|
2895
|
+
// ─── Entry ───────────────────────────────────────────────────────────
|
|
2896
|
+
main().catch((err) => {
|
|
2897
|
+
console.error('Fatal:', err);
|
|
2898
|
+
process.exit(1);
|
|
2899
|
+
});
|
|
2900
|
+
//# sourceMappingURL=index.js.map
|