discoclaw 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.context/README.md +42 -0
- package/.context/architecture.md +58 -0
- package/.context/bot-setup.md +24 -0
- package/.context/dev.md +230 -0
- package/.context/discord.md +144 -0
- package/.context/memory.md +257 -0
- package/.context/ops.md +59 -0
- package/.context/pa-safety.md +47 -0
- package/.context/pa.md +118 -0
- package/.context/project.md +43 -0
- package/.context/runtime.md +253 -0
- package/.context/tasks.md +71 -0
- package/.context/tools.md +75 -0
- package/.env.example +88 -0
- package/.env.example.full +378 -0
- package/LICENSE +21 -0
- package/README.md +220 -0
- package/dist/beads/auto-tag.js +2 -0
- package/dist/beads/auto-tag.test.js +62 -0
- package/dist/beads/bd-cli.js +9 -0
- package/dist/beads/bd-cli.test.js +495 -0
- package/dist/beads/bead-hooks-cli.js +149 -0
- package/dist/beads/bead-sync-cli.js +5 -0
- package/dist/beads/bead-sync-cli.test.js +72 -0
- package/dist/beads/bead-sync-coordinator.js +4 -0
- package/dist/beads/bead-sync-coordinator.test.js +239 -0
- package/dist/beads/bead-sync-watcher.js +2 -0
- package/dist/beads/bead-sync-watcher.test.js +96 -0
- package/dist/beads/bead-sync.js +7 -0
- package/dist/beads/bead-sync.test.js +876 -0
- package/dist/beads/bead-thread-cache.js +8 -0
- package/dist/beads/bead-thread-cache.test.js +91 -0
- package/dist/beads/discord-sync.js +18 -0
- package/dist/beads/discord-sync.test.js +782 -0
- package/dist/beads/find-bead-by-thread.test.js +36 -0
- package/dist/beads/forum-guard.js +2 -0
- package/dist/beads/forum-guard.test.js +204 -0
- package/dist/beads/initialize.js +3 -0
- package/dist/beads/initialize.test.js +304 -0
- package/dist/beads/types.js +10 -0
- package/dist/cli/daemon-installer.js +225 -0
- package/dist/cli/daemon-installer.test.js +289 -0
- package/dist/cli/index.js +42 -0
- package/dist/cli/init-wizard.js +374 -0
- package/dist/cli/init-wizard.test.js +191 -0
- package/dist/config.js +385 -0
- package/dist/config.test.js +589 -0
- package/dist/cron/auto-tag.js +100 -0
- package/dist/cron/auto-tag.test.js +91 -0
- package/dist/cron/cadence.js +74 -0
- package/dist/cron/cadence.test.js +53 -0
- package/dist/cron/cron-sync-coordinator.js +66 -0
- package/dist/cron/cron-sync-coordinator.test.js +118 -0
- package/dist/cron/cron-sync.js +165 -0
- package/dist/cron/cron-sync.test.js +228 -0
- package/dist/cron/cron-tag-map-watcher.js +128 -0
- package/dist/cron/cron-tag-map-watcher.test.js +155 -0
- package/dist/cron/default-timezone.js +23 -0
- package/dist/cron/default-timezone.test.js +30 -0
- package/dist/cron/discord-sync.js +205 -0
- package/dist/cron/discord-sync.test.js +353 -0
- package/dist/cron/executor.js +303 -0
- package/dist/cron/executor.test.js +614 -0
- package/dist/cron/forum-sync.js +347 -0
- package/dist/cron/forum-sync.test.js +539 -0
- package/dist/cron/job-lock.js +164 -0
- package/dist/cron/job-lock.test.js +178 -0
- package/dist/cron/parser.js +68 -0
- package/dist/cron/parser.test.js +115 -0
- package/dist/cron/run-control.js +24 -0
- package/dist/cron/run-control.test.js +27 -0
- package/dist/cron/run-stats.js +265 -0
- package/dist/cron/run-stats.test.js +160 -0
- package/dist/cron/scheduler.js +97 -0
- package/dist/cron/scheduler.test.js +112 -0
- package/dist/cron/tag-map.js +47 -0
- package/dist/cron/tag-map.test.js +64 -0
- package/dist/cron/types.js +1 -0
- package/dist/discoclaw-plan-format.test.js +137 -0
- package/dist/discoclaw-recipe-format.test.js +137 -0
- package/dist/discord/abort-registry.js +70 -0
- package/dist/discord/action-categories.js +36 -0
- package/dist/discord/action-types.js +1 -0
- package/dist/discord/action-utils.js +58 -0
- package/dist/discord/action-utils.test.js +58 -0
- package/dist/discord/actions-beads.js +1 -0
- package/dist/discord/actions-beads.test.js +372 -0
- package/dist/discord/actions-bot-profile.js +107 -0
- package/dist/discord/actions-bot-profile.test.js +138 -0
- package/dist/discord/actions-channels.js +427 -0
- package/dist/discord/actions-channels.test.js +697 -0
- package/dist/discord/actions-config.js +173 -0
- package/dist/discord/actions-config.test.js +322 -0
- package/dist/discord/actions-crons.js +586 -0
- package/dist/discord/actions-crons.test.js +499 -0
- package/dist/discord/actions-defer.js +60 -0
- package/dist/discord/actions-defer.test.js +134 -0
- package/dist/discord/actions-forge.js +134 -0
- package/dist/discord/actions-forge.test.js +206 -0
- package/dist/discord/actions-guild.js +301 -0
- package/dist/discord/actions-guild.test.js +386 -0
- package/dist/discord/actions-memory.js +106 -0
- package/dist/discord/actions-memory.test.js +248 -0
- package/dist/discord/actions-messaging.js +401 -0
- package/dist/discord/actions-messaging.test.js +738 -0
- package/dist/discord/actions-moderation.js +65 -0
- package/dist/discord/actions-moderation.test.js +88 -0
- package/dist/discord/actions-plan.js +445 -0
- package/dist/discord/actions-plan.test.js +610 -0
- package/dist/discord/actions-poll.js +38 -0
- package/dist/discord/actions-poll.test.js +93 -0
- package/dist/discord/actions-tasks.js +3 -0
- package/dist/discord/actions-tasks.test.js +418 -0
- package/dist/discord/actions.js +600 -0
- package/dist/discord/actions.test.js +522 -0
- package/dist/discord/allowed-mentions.js +3 -0
- package/dist/discord/allowed-mentions.test.js +17 -0
- package/dist/discord/allowlist.js +29 -0
- package/dist/discord/allowlist.test.js +24 -0
- package/dist/discord/audit-handler.js +191 -0
- package/dist/discord/audit-handler.test.js +361 -0
- package/dist/discord/bot.js +141 -0
- package/dist/discord/channel-context.js +181 -0
- package/dist/discord/defer-scheduler.js +45 -0
- package/dist/discord/destructive-confirmation.js +128 -0
- package/dist/discord/destructive-confirmation.test.js +49 -0
- package/dist/discord/discord-plan-auto-implement.test.js +18 -0
- package/dist/discord/durable-memory.js +145 -0
- package/dist/discord/durable-memory.test.js +281 -0
- package/dist/discord/durable-write-queue.js +4 -0
- package/dist/discord/file-download.js +308 -0
- package/dist/discord/file-download.test.js +303 -0
- package/dist/discord/forge-audit-verdict.js +140 -0
- package/dist/discord/forge-auto-implement.js +80 -0
- package/dist/discord/forge-auto-implement.test.js +110 -0
- package/dist/discord/forge-commands.js +698 -0
- package/dist/discord/forge-commands.test.js +1606 -0
- package/dist/discord/forge-plan-registry.js +68 -0
- package/dist/discord/forge-plan-registry.test.js +127 -0
- package/dist/discord/forum-count-sync.js +130 -0
- package/dist/discord/forum-count-sync.test.js +200 -0
- package/dist/discord/health-command.js +98 -0
- package/dist/discord/health-command.test.js +195 -0
- package/dist/discord/help-command.js +22 -0
- package/dist/discord/help-command.test.js +49 -0
- package/dist/discord/image-download.js +201 -0
- package/dist/discord/image-download.test.js +499 -0
- package/dist/discord/inflight-replies.js +228 -0
- package/dist/discord/inflight-replies.test.js +295 -0
- package/dist/discord/json-extract.js +110 -0
- package/dist/discord/keyed-queue.js +22 -0
- package/dist/discord/memory-commands.js +85 -0
- package/dist/discord/memory-commands.test.js +159 -0
- package/dist/discord/memory-timing.integration.test.js +159 -0
- package/dist/discord/message-coordinator.js +2347 -0
- package/dist/discord/message-coordinator.onboarding.test.js +183 -0
- package/dist/discord/message-coordinator.plan-run.test.js +264 -0
- package/dist/discord/message-history.js +53 -0
- package/dist/discord/message-history.test.js +95 -0
- package/dist/discord/models-command.js +59 -0
- package/dist/discord/models-command.test.js +150 -0
- package/dist/discord/nickname.test.js +76 -0
- package/dist/discord/onboarding-completion.js +55 -0
- package/dist/discord/onboarding-completion.test.js +176 -0
- package/dist/discord/output-common.js +178 -0
- package/dist/discord/output-common.test.js +198 -0
- package/dist/discord/output-utils.js +156 -0
- package/dist/discord/parse-identity-name.test.js +129 -0
- package/dist/discord/plan-commands.js +612 -0
- package/dist/discord/plan-commands.test.js +1622 -0
- package/dist/discord/plan-manager.js +1491 -0
- package/dist/discord/plan-manager.test.js +2380 -0
- package/dist/discord/plan-parser.js +110 -0
- package/dist/discord/plan-parser.test.js +63 -0
- package/dist/discord/plan-run-phase-start.js +20 -0
- package/dist/discord/plan-run-phase-start.test.js +29 -0
- package/dist/discord/platform-message.js +45 -0
- package/dist/discord/platform-message.test.js +110 -0
- package/dist/discord/prompt-common.js +240 -0
- package/dist/discord/prompt-common.test.js +423 -0
- package/dist/discord/reaction-handler.js +691 -0
- package/dist/discord/reaction-handler.test.js +1574 -0
- package/dist/discord/reaction-prompts.js +118 -0
- package/dist/discord/reaction-prompts.test.js +253 -0
- package/dist/discord/reply-reference.js +66 -0
- package/dist/discord/reply-reference.test.js +125 -0
- package/dist/discord/restart-command.js +143 -0
- package/dist/discord/restart-command.test.js +196 -0
- package/dist/discord/runtime-utils.js +43 -0
- package/dist/discord/runtime-utils.test.js +112 -0
- package/dist/discord/session-key.js +7 -0
- package/dist/discord/session-key.test.js +13 -0
- package/dist/discord/shortterm-memory.js +166 -0
- package/dist/discord/shortterm-memory.test.js +345 -0
- package/dist/discord/shutdown-context.js +122 -0
- package/dist/discord/shutdown-context.test.js +279 -0
- package/dist/discord/startup-profile.test.js +214 -0
- package/dist/discord/status-channel.js +190 -0
- package/dist/discord/status-channel.test.js +282 -0
- package/dist/discord/status-command.js +206 -0
- package/dist/discord/status-command.test.js +341 -0
- package/dist/discord/streaming-progress.js +107 -0
- package/dist/discord/streaming-progress.test.js +93 -0
- package/dist/discord/summarizer.js +89 -0
- package/dist/discord/summarizer.test.js +245 -0
- package/dist/discord/system-bootstrap.js +396 -0
- package/dist/discord/system-bootstrap.test.js +724 -0
- package/dist/discord/thread-context.js +169 -0
- package/dist/discord/thread-context.test.js +386 -0
- package/dist/discord/tool-aware-queue.js +116 -0
- package/dist/discord/tool-aware-queue.test.js +180 -0
- package/dist/discord/update-command.js +127 -0
- package/dist/discord/update-command.test.js +275 -0
- package/dist/discord/user-errors.js +40 -0
- package/dist/discord/user-errors.test.js +31 -0
- package/dist/discord/user-turn-to-durable.js +111 -0
- package/dist/discord/user-turn-to-durable.test.js +273 -0
- package/dist/discord-followup.test.js +677 -0
- package/dist/discord.channel-context.test.js +95 -0
- package/dist/discord.fail-closed.test.js +199 -0
- package/dist/discord.health-command.integration.test.js +140 -0
- package/dist/discord.js +190 -0
- package/dist/discord.prompt-context.test.js +1431 -0
- package/dist/discord.render.test.js +621 -0
- package/dist/discord.status-wiring.test.js +187 -0
- package/dist/engine/claudeCli.js +137 -0
- package/dist/engine/types.js +1 -0
- package/dist/group-queue.js +25 -0
- package/dist/health/credential-check.js +175 -0
- package/dist/health/credential-check.test.js +401 -0
- package/dist/health/startup-healing.js +139 -0
- package/dist/health/startup-healing.test.js +298 -0
- package/dist/identity.js +36 -0
- package/dist/index.js +1378 -0
- package/dist/logging/logger-like.js +1 -0
- package/dist/observability/memory-sampler.js +51 -0
- package/dist/observability/memory-sampler.test.js +93 -0
- package/dist/observability/metrics.js +88 -0
- package/dist/observability/metrics.test.js +42 -0
- package/dist/onboarding/onboarding-flow.js +246 -0
- package/dist/onboarding/onboarding-flow.test.js +238 -0
- package/dist/onboarding/onboarding-writer.js +102 -0
- package/dist/onboarding/onboarding-writer.test.js +143 -0
- package/dist/pidlock.js +187 -0
- package/dist/pidlock.test.js +128 -0
- package/dist/pipeline/engine.js +206 -0
- package/dist/pipeline/engine.test.js +771 -0
- package/dist/root-policy.js +21 -0
- package/dist/root-policy.test.js +55 -0
- package/dist/runtime/claude-code-cli.js +35 -0
- package/dist/runtime/claude-code-cli.test.js +1199 -0
- package/dist/runtime/cli-adapter.js +584 -0
- package/dist/runtime/cli-output-parsers.js +108 -0
- package/dist/runtime/cli-shared.js +96 -0
- package/dist/runtime/cli-shared.test.js +104 -0
- package/dist/runtime/cli-strategy.js +6 -0
- package/dist/runtime/codex-cli.js +16 -0
- package/dist/runtime/codex-cli.test.js +862 -0
- package/dist/runtime/concurrency-limit.js +80 -0
- package/dist/runtime/concurrency-limit.test.js +137 -0
- package/dist/runtime/gemini-cli.js +16 -0
- package/dist/runtime/gemini-cli.test.js +413 -0
- package/dist/runtime/long-running-process.js +415 -0
- package/dist/runtime/long-running-process.test.js +318 -0
- package/dist/runtime/model-smoke-helpers.js +160 -0
- package/dist/runtime/model-smoke.test.js +194 -0
- package/dist/runtime/model-tiers.js +33 -0
- package/dist/runtime/model-tiers.test.js +65 -0
- package/dist/runtime/openai-auth.js +151 -0
- package/dist/runtime/openai-auth.test.js +361 -0
- package/dist/runtime/openai-compat.js +178 -0
- package/dist/runtime/openai-compat.test.js +449 -0
- package/dist/runtime/process-pool.js +93 -0
- package/dist/runtime/process-pool.test.js +148 -0
- package/dist/runtime/registry.js +15 -0
- package/dist/runtime/registry.test.js +47 -0
- package/dist/runtime/session-scanner.js +186 -0
- package/dist/runtime/session-scanner.test.js +257 -0
- package/dist/runtime/strategies/claude-strategy.js +193 -0
- package/dist/runtime/strategies/codex-strategy.js +161 -0
- package/dist/runtime/strategies/gemini-strategy.js +64 -0
- package/dist/runtime/strategies/template-strategy.js +85 -0
- package/dist/runtime/tool-capabilities.js +27 -0
- package/dist/runtime/tool-capabilities.test.js +24 -0
- package/dist/runtime/tool-labels.js +48 -0
- package/dist/runtime/types.js +2 -0
- package/dist/sessionManager.js +47 -0
- package/dist/sessions.js +18 -0
- package/dist/tasks/architecture-contract.js +33 -0
- package/dist/tasks/architecture-contract.test.js +90 -0
- package/dist/tasks/auto-tag.js +50 -0
- package/dist/tasks/auto-tag.test.js +64 -0
- package/dist/tasks/bd-cli.js +164 -0
- package/dist/tasks/bd-cli.test.js +359 -0
- package/dist/tasks/bead-sync.js +1 -0
- package/dist/tasks/context-summary.js +27 -0
- package/dist/tasks/discord-sync.js +3 -0
- package/dist/tasks/discord-sync.test.js +685 -0
- package/dist/tasks/discord-types.js +4 -0
- package/dist/tasks/find-task-by-thread.test.js +36 -0
- package/dist/tasks/forum-guard.js +81 -0
- package/dist/tasks/forum-guard.test.js +192 -0
- package/dist/tasks/initialize.js +77 -0
- package/dist/tasks/initialize.test.js +263 -0
- package/dist/tasks/logger-types.js +1 -0
- package/dist/tasks/metrics-types.js +3 -0
- package/dist/tasks/migrate.js +33 -0
- package/dist/tasks/migrate.test.js +156 -0
- package/dist/tasks/path-defaults.js +67 -0
- package/dist/tasks/path-defaults.test.js +73 -0
- package/dist/tasks/runtime-types.js +1 -0
- package/dist/tasks/service.js +33 -0
- package/dist/tasks/service.test.js +51 -0
- package/dist/tasks/store.js +238 -0
- package/dist/tasks/store.test.js +417 -0
- package/dist/tasks/sync-context.js +1 -0
- package/dist/tasks/sync-contract.js +24 -0
- package/dist/tasks/sync-contract.test.js +25 -0
- package/dist/tasks/sync-coordinator-metrics.js +41 -0
- package/dist/tasks/sync-coordinator-retries.js +71 -0
- package/dist/tasks/sync-coordinator.js +96 -0
- package/dist/tasks/sync-coordinator.test.js +501 -0
- package/dist/tasks/sync-types.js +1 -0
- package/dist/tasks/sync-watcher.js +27 -0
- package/dist/tasks/sync-watcher.test.js +92 -0
- package/dist/tasks/tag-map.js +36 -0
- package/dist/tasks/tag-map.test.js +54 -0
- package/dist/tasks/task-action-contract.js +16 -0
- package/dist/tasks/task-action-contract.test.js +16 -0
- package/dist/tasks/task-action-executor.js +18 -0
- package/dist/tasks/task-action-executor.test.js +420 -0
- package/dist/tasks/task-action-mutation-helpers.js +17 -0
- package/dist/tasks/task-action-mutations.js +151 -0
- package/dist/tasks/task-action-prompt.js +62 -0
- package/dist/tasks/task-action-read-ops.js +73 -0
- package/dist/tasks/task-action-runner-types.js +1 -0
- package/dist/tasks/task-action-thread-sync.js +82 -0
- package/dist/tasks/task-actions.js +3 -0
- package/dist/tasks/task-cli.js +227 -0
- package/dist/tasks/task-context.js +1 -0
- package/dist/tasks/task-lifecycle.js +46 -0
- package/dist/tasks/task-lifecycle.test.js +35 -0
- package/dist/tasks/task-sync-apply-plan.js +95 -0
- package/dist/tasks/task-sync-apply-types.js +12 -0
- package/dist/tasks/task-sync-apply.js +319 -0
- package/dist/tasks/task-sync-cli.js +89 -0
- package/dist/tasks/task-sync-cli.test.js +70 -0
- package/dist/tasks/task-sync-engine.js +88 -0
- package/dist/tasks/task-sync-engine.test.js +934 -0
- package/dist/tasks/task-sync-phase-apply.js +171 -0
- package/dist/tasks/task-sync-pipeline.js +2 -0
- package/dist/tasks/task-sync-pipeline.test.js +265 -0
- package/dist/tasks/task-sync-reconcile-plan.js +182 -0
- package/dist/tasks/task-sync-reconcile.js +144 -0
- package/dist/tasks/task-sync.js +56 -0
- package/dist/tasks/task-sync.test.js +86 -0
- package/dist/tasks/thread-cache.js +42 -0
- package/dist/tasks/thread-cache.test.js +89 -0
- package/dist/tasks/thread-contracts.test.js +711 -0
- package/dist/tasks/thread-forum-ops.js +68 -0
- package/dist/tasks/thread-helpers.js +86 -0
- package/dist/tasks/thread-helpers.test.js +33 -0
- package/dist/tasks/thread-lifecycle-ops.js +144 -0
- package/dist/tasks/thread-ops-shared.js +21 -0
- package/dist/tasks/thread-ops.js +2 -0
- package/dist/tasks/types.js +20 -0
- package/dist/tasks/types.test.js +60 -0
- package/dist/test-setup.js +11 -0
- package/dist/test-setup.test.js +42 -0
- package/dist/transport/types.js +1 -0
- package/dist/validate.js +41 -0
- package/dist/validate.test.js +94 -0
- package/dist/version.js +15 -0
- package/dist/version.test.js +31 -0
- package/dist/webhook/server.js +199 -0
- package/dist/webhook/server.test.js +460 -0
- package/dist/workspace-bootstrap.js +135 -0
- package/dist/workspace-bootstrap.test.js +514 -0
- package/dist/workspace-permissions.js +134 -0
- package/dist/workspace-permissions.test.js +181 -0
- package/package.json +74 -0
- package/scripts/cron/cron-tag-map.json +9 -0
- package/scripts/tasks/tag-map.json +10 -0
- package/systemd/discoclaw.service +19 -0
- package/templates/recipes/integration.discoclaw-recipe.md +171 -0
- package/templates/workspace/AGENTS.md +217 -0
- package/templates/workspace/BOOTSTRAP.md +1 -0
- package/templates/workspace/HEARTBEAT.md +10 -0
- package/templates/workspace/IDENTITY.md +16 -0
- package/templates/workspace/MEMORY.md +24 -0
- package/templates/workspace/SOUL.md +52 -0
- package/templates/workspace/TOOLS.md +304 -0
- package/templates/workspace/USER.md +37 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { createMessageCreateHandler } from './discord.js';
|
|
3
|
+
function makeQueue() {
|
|
4
|
+
return {
|
|
5
|
+
run: vi.fn(async (_key, fn) => fn()),
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
function makeMsg(overrides = {}) {
|
|
9
|
+
const replyObj = { edit: vi.fn(async () => { }) };
|
|
10
|
+
return {
|
|
11
|
+
author: { id: '123', bot: false, displayName: 'User', username: 'user' },
|
|
12
|
+
guildId: 'guild',
|
|
13
|
+
channelId: 'chan',
|
|
14
|
+
channel: { send: vi.fn(async () => { }), isThread: () => false, name: 'general' },
|
|
15
|
+
content: 'hello',
|
|
16
|
+
reply: vi.fn(async () => replyObj),
|
|
17
|
+
id: 'msg1',
|
|
18
|
+
...overrides,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function baseParams(runtimeOverride) {
|
|
22
|
+
return {
|
|
23
|
+
allowUserIds: new Set(['123']),
|
|
24
|
+
botDisplayName: 'TestBot',
|
|
25
|
+
runtime: runtimeOverride,
|
|
26
|
+
sessionManager: { getOrCreate: vi.fn(async () => 'sess') },
|
|
27
|
+
workspaceCwd: '/tmp',
|
|
28
|
+
projectCwd: '/tmp',
|
|
29
|
+
groupsDir: '/tmp',
|
|
30
|
+
useGroupDirCwd: false,
|
|
31
|
+
runtimeModel: 'opus',
|
|
32
|
+
runtimeTools: [],
|
|
33
|
+
runtimeTimeoutMs: 1000,
|
|
34
|
+
requireChannelContext: false,
|
|
35
|
+
autoIndexChannelContext: false,
|
|
36
|
+
autoJoinThreads: false,
|
|
37
|
+
useRuntimeSessions: true,
|
|
38
|
+
discordActionsEnabled: false,
|
|
39
|
+
discordActionsChannels: true,
|
|
40
|
+
discordActionsMessaging: false,
|
|
41
|
+
discordActionsGuild: false,
|
|
42
|
+
discordActionsModeration: false,
|
|
43
|
+
discordActionsPolls: false,
|
|
44
|
+
discordActionsTasks: false,
|
|
45
|
+
discordActionsBotProfile: false,
|
|
46
|
+
messageHistoryBudget: 0,
|
|
47
|
+
summaryEnabled: false,
|
|
48
|
+
summaryModel: 'haiku',
|
|
49
|
+
summaryMaxChars: 2000,
|
|
50
|
+
summaryEveryNTurns: 5,
|
|
51
|
+
summaryDataDir: '/tmp/summaries',
|
|
52
|
+
summaryToDurableEnabled: false,
|
|
53
|
+
shortTermMemoryEnabled: false,
|
|
54
|
+
shortTermDataDir: '/tmp/shortterm',
|
|
55
|
+
shortTermMaxEntries: 20,
|
|
56
|
+
shortTermMaxAgeMs: 21600000,
|
|
57
|
+
shortTermInjectMaxChars: 1000,
|
|
58
|
+
durableMemoryEnabled: false,
|
|
59
|
+
durableDataDir: '/tmp/durable',
|
|
60
|
+
durableInjectMaxChars: 2000,
|
|
61
|
+
durableMaxItems: 200,
|
|
62
|
+
memoryCommandsEnabled: false,
|
|
63
|
+
actionFollowupDepth: 0,
|
|
64
|
+
reactionHandlerEnabled: false,
|
|
65
|
+
reactionRemoveHandlerEnabled: false,
|
|
66
|
+
reactionMaxAgeMs: 86400000,
|
|
67
|
+
streamStallWarningMs: 0,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function mockStatus() {
|
|
71
|
+
return {
|
|
72
|
+
online: vi.fn(async () => { }),
|
|
73
|
+
offline: vi.fn(async () => { }),
|
|
74
|
+
runtimeError: vi.fn(async () => { }),
|
|
75
|
+
handlerError: vi.fn(async () => { }),
|
|
76
|
+
actionFailed: vi.fn(async () => { }),
|
|
77
|
+
taskSyncComplete: vi.fn(async () => { }),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
describe('status wiring in message handler', () => {
|
|
81
|
+
it('calls runtimeError when runtime emits an error event', async () => {
|
|
82
|
+
const runtime = {
|
|
83
|
+
invoke: async function* () {
|
|
84
|
+
yield { type: 'error', message: 'timed out' };
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
const status = mockStatus();
|
|
88
|
+
const statusRef = { current: status };
|
|
89
|
+
const handler = createMessageCreateHandler(baseParams(runtime), makeQueue(), statusRef);
|
|
90
|
+
await handler(makeMsg());
|
|
91
|
+
expect(status.runtimeError).toHaveBeenCalledOnce();
|
|
92
|
+
expect(status.runtimeError).toHaveBeenCalledWith(expect.objectContaining({ sessionKey: expect.any(String) }), 'timed out');
|
|
93
|
+
});
|
|
94
|
+
it('calls handlerError when an exception is thrown', async () => {
|
|
95
|
+
const runtime = {
|
|
96
|
+
invoke: async function* () {
|
|
97
|
+
throw new Error('kaboom');
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
const status = mockStatus();
|
|
101
|
+
const statusRef = { current: status };
|
|
102
|
+
const handler = createMessageCreateHandler(baseParams(runtime), makeQueue(), statusRef);
|
|
103
|
+
await handler(makeMsg());
|
|
104
|
+
expect(status.handlerError).toHaveBeenCalledOnce();
|
|
105
|
+
expect(status.handlerError).toHaveBeenCalledWith(expect.objectContaining({ sessionKey: expect.any(String) }), expect.any(Error));
|
|
106
|
+
});
|
|
107
|
+
it('cleans up streaming keepalive timer when invoke throws', async () => {
|
|
108
|
+
vi.useFakeTimers();
|
|
109
|
+
const setIntervalSpy = vi.spyOn(globalThis, 'setInterval');
|
|
110
|
+
const clearIntervalSpy = vi.spyOn(globalThis, 'clearInterval');
|
|
111
|
+
try {
|
|
112
|
+
const runtime = {
|
|
113
|
+
invoke: async function* () {
|
|
114
|
+
throw new Error('kaboom');
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
const status = mockStatus();
|
|
118
|
+
const statusRef = { current: status };
|
|
119
|
+
const handler = createMessageCreateHandler(baseParams(runtime), makeQueue(), statusRef);
|
|
120
|
+
await handler(makeMsg());
|
|
121
|
+
expect(setIntervalSpy).toHaveBeenCalled();
|
|
122
|
+
const keepaliveHandle = setIntervalSpy.mock.results.find((r) => r.type === 'return')?.value;
|
|
123
|
+
expect(keepaliveHandle).toBeDefined();
|
|
124
|
+
expect(clearIntervalSpy).toHaveBeenCalledWith(keepaliveHandle);
|
|
125
|
+
expect(status.handlerError).toHaveBeenCalledOnce();
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
setIntervalSpy.mockRestore();
|
|
129
|
+
clearIntervalSpy.mockRestore();
|
|
130
|
+
vi.useRealTimers();
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
it('does not call status methods when statusRef.current is null', async () => {
|
|
134
|
+
const runtime = {
|
|
135
|
+
invoke: async function* () {
|
|
136
|
+
yield { type: 'error', message: 'oops' };
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
const statusRef = { current: null };
|
|
140
|
+
const handler = createMessageCreateHandler(baseParams(runtime), makeQueue(), statusRef);
|
|
141
|
+
// Should not throw even though status is null.
|
|
142
|
+
await expect(handler(makeMsg())).resolves.toBeUndefined();
|
|
143
|
+
});
|
|
144
|
+
it('does not call status methods when statusRef is omitted', async () => {
|
|
145
|
+
const runtime = {
|
|
146
|
+
invoke: async function* () {
|
|
147
|
+
yield { type: 'error', message: 'oops' };
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
const handler = createMessageCreateHandler(baseParams(runtime), makeQueue());
|
|
151
|
+
await expect(handler(makeMsg())).resolves.toBeUndefined();
|
|
152
|
+
});
|
|
153
|
+
it('swallows 50083 (thread archived) without calling handlerError', async () => {
|
|
154
|
+
const runtime = {
|
|
155
|
+
invoke: async function* () {
|
|
156
|
+
yield { type: 'text_final', text: 'Done' };
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
const status = mockStatus();
|
|
160
|
+
const statusRef = { current: status };
|
|
161
|
+
const handler = createMessageCreateHandler(baseParams(runtime), makeQueue(), statusRef);
|
|
162
|
+
// Make reply.edit throw a Discord 50083 "Thread is archived" error.
|
|
163
|
+
const err50083 = Object.assign(new Error('Thread is archived'), { code: 50083 });
|
|
164
|
+
const replyObj = { edit: vi.fn().mockRejectedValue(err50083), delete: vi.fn(async () => { }) };
|
|
165
|
+
const msg = makeMsg();
|
|
166
|
+
msg.reply = vi.fn(async () => replyObj);
|
|
167
|
+
await handler(msg);
|
|
168
|
+
expect(status.handlerError).not.toHaveBeenCalled();
|
|
169
|
+
});
|
|
170
|
+
it('still calls handlerError for non-50083 Discord errors', async () => {
|
|
171
|
+
const runtime = {
|
|
172
|
+
invoke: async function* () {
|
|
173
|
+
yield { type: 'text_final', text: 'Done' };
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
const status = mockStatus();
|
|
177
|
+
const statusRef = { current: status };
|
|
178
|
+
const handler = createMessageCreateHandler(baseParams(runtime), makeQueue(), statusRef);
|
|
179
|
+
// A different Discord error (e.g. Missing Permissions = 50013).
|
|
180
|
+
const err50013 = Object.assign(new Error('Missing Permissions'), { code: 50013 });
|
|
181
|
+
const replyObj = { edit: vi.fn().mockRejectedValue(err50013) };
|
|
182
|
+
const msg = makeMsg();
|
|
183
|
+
msg.reply = vi.fn(async () => replyObj);
|
|
184
|
+
await handler(msg);
|
|
185
|
+
expect(status.handlerError).toHaveBeenCalledOnce();
|
|
186
|
+
});
|
|
187
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
function extractTextFromUnknownEvent(evt) {
|
|
3
|
+
if (!evt || typeof evt !== 'object')
|
|
4
|
+
return null;
|
|
5
|
+
const anyEvt = evt;
|
|
6
|
+
const candidates = [
|
|
7
|
+
anyEvt.text,
|
|
8
|
+
anyEvt.delta,
|
|
9
|
+
anyEvt.content,
|
|
10
|
+
// Sometimes nested.
|
|
11
|
+
(anyEvt.data && typeof anyEvt.data === 'object') ? anyEvt.data.text : undefined,
|
|
12
|
+
];
|
|
13
|
+
for (const c of candidates) {
|
|
14
|
+
if (typeof c === 'string' && c.length > 0)
|
|
15
|
+
return c;
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
function* textAsChunks(text) {
|
|
20
|
+
if (!text)
|
|
21
|
+
return;
|
|
22
|
+
yield { type: 'text_final', text };
|
|
23
|
+
yield { type: 'done' };
|
|
24
|
+
}
|
|
25
|
+
export function createClaudeCliRuntime(opts) {
|
|
26
|
+
const capabilities = new Set([
|
|
27
|
+
'streaming_text',
|
|
28
|
+
'sessions',
|
|
29
|
+
'workspace_instructions',
|
|
30
|
+
'tools_exec',
|
|
31
|
+
'tools_fs',
|
|
32
|
+
'tools_web',
|
|
33
|
+
'mcp',
|
|
34
|
+
]);
|
|
35
|
+
async function* invoke(params) {
|
|
36
|
+
const args = ['-p', '--model', params.model];
|
|
37
|
+
if (opts.dangerouslySkipPermissions) {
|
|
38
|
+
args.push('--dangerously-skip-permissions');
|
|
39
|
+
}
|
|
40
|
+
if (params.sessionId) {
|
|
41
|
+
args.push('--session-id', params.sessionId);
|
|
42
|
+
}
|
|
43
|
+
if (opts.outputFormat) {
|
|
44
|
+
args.push('--output-format', opts.outputFormat);
|
|
45
|
+
}
|
|
46
|
+
if (opts.outputFormat === 'stream-json') {
|
|
47
|
+
args.push('--include-partial-messages');
|
|
48
|
+
}
|
|
49
|
+
// Tool flags are runtime-specific; keep optional and configurable.
|
|
50
|
+
if (params.tools && params.tools.length > 0) {
|
|
51
|
+
// `--tools` accepts a comma-separated list for built-in tools.
|
|
52
|
+
// We keep this simple; if we need finer control, add --allowedTools/--disallowedTools.
|
|
53
|
+
args.push('--tools', params.tools.join(','));
|
|
54
|
+
}
|
|
55
|
+
args.push(params.prompt);
|
|
56
|
+
const subprocess = execa(opts.claudeBin, args, {
|
|
57
|
+
cwd: params.cwd,
|
|
58
|
+
timeout: params.timeoutMs,
|
|
59
|
+
reject: false,
|
|
60
|
+
stdout: 'pipe',
|
|
61
|
+
stderr: 'pipe',
|
|
62
|
+
});
|
|
63
|
+
if (!subprocess.stdout) {
|
|
64
|
+
yield { type: 'error', message: 'claude: missing stdout stream' };
|
|
65
|
+
yield { type: 'done' };
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (opts.outputFormat === 'text') {
|
|
69
|
+
const { stdout, stderr, exitCode } = await subprocess;
|
|
70
|
+
if (exitCode !== 0) {
|
|
71
|
+
const msg = (stderr || stdout || `claude exit ${exitCode}`).trim();
|
|
72
|
+
yield { type: 'error', message: msg };
|
|
73
|
+
yield { type: 'done' };
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
yield* textAsChunks(stdout.trimEnd());
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
// stream-json: parse line-delimited JSON events.
|
|
80
|
+
let buffered = '';
|
|
81
|
+
subprocess.stdout.on('data', (chunk) => {
|
|
82
|
+
buffered += String(chunk);
|
|
83
|
+
});
|
|
84
|
+
// Consume progressively while the process runs.
|
|
85
|
+
while (subprocess.exitCode == null) {
|
|
86
|
+
await new Promise((r) => setTimeout(r, 75));
|
|
87
|
+
const lines = buffered.split(/\r?\n/);
|
|
88
|
+
buffered = lines.pop() ?? '';
|
|
89
|
+
for (const line of lines) {
|
|
90
|
+
const trimmed = line.trim();
|
|
91
|
+
if (!trimmed)
|
|
92
|
+
continue;
|
|
93
|
+
try {
|
|
94
|
+
const evt = JSON.parse(trimmed);
|
|
95
|
+
const text = extractTextFromUnknownEvent(evt);
|
|
96
|
+
if (text) {
|
|
97
|
+
yield { type: 'text_delta', text };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// If CLI prints non-JSON noise, treat it as text.
|
|
102
|
+
yield { type: 'text_delta', text: trimmed };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const { stdout, stderr, exitCode } = await subprocess;
|
|
107
|
+
if (exitCode !== 0) {
|
|
108
|
+
yield { type: 'error', message: (stderr || stdout || `claude exit ${exitCode}`).trim() };
|
|
109
|
+
yield { type: 'done' };
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// Flush any trailing buffered line.
|
|
113
|
+
const tail = buffered.trim();
|
|
114
|
+
if (tail) {
|
|
115
|
+
try {
|
|
116
|
+
const evt = JSON.parse(tail);
|
|
117
|
+
const text = extractTextFromUnknownEvent(evt);
|
|
118
|
+
if (text)
|
|
119
|
+
yield { type: 'text_delta', text };
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
yield { type: 'text_delta', text: tail };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Provide a final merged text as well, based on what the CLI returned.
|
|
126
|
+
// (This makes Discord output stable even if the event model changes.)
|
|
127
|
+
if (stdout.trim()) {
|
|
128
|
+
yield { type: 'text_final', text: stdout.trimEnd() };
|
|
129
|
+
}
|
|
130
|
+
yield { type: 'done' };
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
id: 'claude_code',
|
|
134
|
+
capabilities,
|
|
135
|
+
invoke,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export class KeyedQueue {
|
|
2
|
+
tails = new Map();
|
|
3
|
+
size() {
|
|
4
|
+
return this.tails.size;
|
|
5
|
+
}
|
|
6
|
+
async run(key, fn) {
|
|
7
|
+
const prev = this.tails.get(key) ?? Promise.resolve();
|
|
8
|
+
let release;
|
|
9
|
+
const current = new Promise((r) => {
|
|
10
|
+
release = r;
|
|
11
|
+
});
|
|
12
|
+
const tail = prev.then(() => current, () => current);
|
|
13
|
+
this.tails.set(key, tail);
|
|
14
|
+
await prev;
|
|
15
|
+
try {
|
|
16
|
+
return await fn();
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
release();
|
|
20
|
+
if (this.tails.get(key) === tail) {
|
|
21
|
+
this.tails.delete(key);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
const DISCORD_API_BASE = 'https://discord.com/api/v10';
|
|
3
|
+
const OPENAI_API_DEFAULT_BASE = 'https://api.openai.com/v1';
|
|
4
|
+
const OPENROUTER_API_DEFAULT_BASE = 'https://openrouter.ai/api/v1';
|
|
5
|
+
// Credentials in this set gate core bot functionality; failure is surfaced prominently.
|
|
6
|
+
const CRITICAL = new Set(['discord-token']);
|
|
7
|
+
/**
|
|
8
|
+
* Validate the Discord bot token by calling GET /users/@me.
|
|
9
|
+
* Always resolves — returns a 'fail' result on network error instead of throwing.
|
|
10
|
+
*/
|
|
11
|
+
export async function checkDiscordToken(token) {
|
|
12
|
+
const name = 'discord-token';
|
|
13
|
+
try {
|
|
14
|
+
const res = await fetch(`${DISCORD_API_BASE}/users/@me`, {
|
|
15
|
+
headers: { Authorization: `Bot ${token}` },
|
|
16
|
+
});
|
|
17
|
+
if (res.ok) {
|
|
18
|
+
return { name, status: 'ok' };
|
|
19
|
+
}
|
|
20
|
+
if (res.status === 401) {
|
|
21
|
+
return { name, status: 'fail', message: 'invalid or revoked token (401)' };
|
|
22
|
+
}
|
|
23
|
+
return { name, status: 'fail', message: `unexpected status ${res.status}` };
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
27
|
+
return { name, status: 'fail', message: `network error: ${msg}` };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Validate the OpenAI API key by calling GET /models on the configured base URL.
|
|
32
|
+
* Returns 'skip' when no key is configured.
|
|
33
|
+
* Always resolves — returns a 'fail' result on network error instead of throwing.
|
|
34
|
+
*/
|
|
35
|
+
export async function checkOpenAiKey(opts) {
|
|
36
|
+
const name = 'openai-key';
|
|
37
|
+
const { apiKey, baseUrl } = opts;
|
|
38
|
+
if (!apiKey) {
|
|
39
|
+
return { name, status: 'skip' };
|
|
40
|
+
}
|
|
41
|
+
const base = (baseUrl ?? OPENAI_API_DEFAULT_BASE).replace(/\/$/, '');
|
|
42
|
+
try {
|
|
43
|
+
const res = await fetch(`${base}/models`, {
|
|
44
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
45
|
+
});
|
|
46
|
+
if (res.ok) {
|
|
47
|
+
return { name, status: 'ok' };
|
|
48
|
+
}
|
|
49
|
+
if (res.status === 401) {
|
|
50
|
+
return { name, status: 'fail', message: 'invalid or expired key (401)' };
|
|
51
|
+
}
|
|
52
|
+
if (res.status === 403) {
|
|
53
|
+
return { name, status: 'fail', message: 'key lacks required permissions (403)' };
|
|
54
|
+
}
|
|
55
|
+
return { name, status: 'fail', message: `unexpected status ${res.status}` };
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
59
|
+
return { name, status: 'fail', message: `network error: ${msg}` };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Validate the OpenRouter API key by calling GET /models on the configured base URL.
|
|
64
|
+
* Returns 'skip' when no key is configured.
|
|
65
|
+
* Always resolves — returns a 'fail' result on network error instead of throwing.
|
|
66
|
+
*/
|
|
67
|
+
export async function checkOpenRouterKey(opts) {
|
|
68
|
+
const name = 'openrouter-key';
|
|
69
|
+
const { apiKey, baseUrl } = opts;
|
|
70
|
+
if (!apiKey) {
|
|
71
|
+
return { name, status: 'skip' };
|
|
72
|
+
}
|
|
73
|
+
const base = (baseUrl ?? OPENROUTER_API_DEFAULT_BASE).replace(/\/$/, '');
|
|
74
|
+
try {
|
|
75
|
+
const res = await fetch(`${base}/models`, {
|
|
76
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
77
|
+
});
|
|
78
|
+
if (res.ok) {
|
|
79
|
+
return { name, status: 'ok' };
|
|
80
|
+
}
|
|
81
|
+
if (res.status === 401) {
|
|
82
|
+
return { name, status: 'fail', message: 'invalid or expired key (401)' };
|
|
83
|
+
}
|
|
84
|
+
if (res.status === 403) {
|
|
85
|
+
return { name, status: 'fail', message: 'key lacks required permissions (403)' };
|
|
86
|
+
}
|
|
87
|
+
return { name, status: 'fail', message: `unexpected status ${res.status}` };
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
91
|
+
return { name, status: 'fail', message: `network error: ${msg}` };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Check that the workspace path exists and is read/write accessible.
|
|
96
|
+
* Returns 'skip' if no path is provided.
|
|
97
|
+
* Always resolves — returns a 'fail' result on access error instead of throwing.
|
|
98
|
+
*/
|
|
99
|
+
export async function checkWorkspacePath(workspacePath) {
|
|
100
|
+
const name = 'workspace-path';
|
|
101
|
+
if (!workspacePath) {
|
|
102
|
+
return { name, status: 'skip' };
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
await fs.access(workspacePath, fs.constants.R_OK | fs.constants.W_OK);
|
|
106
|
+
return { name, status: 'ok' };
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
110
|
+
return { name, status: 'fail', message: `workspace not accessible: ${msg}` };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Validate that a status channel is configured (either a name or Discord snowflake ID).
|
|
115
|
+
* Returns 'skip' if no channel is configured.
|
|
116
|
+
*/
|
|
117
|
+
export function checkStatusChannel(channelId) {
|
|
118
|
+
const name = 'status-channel';
|
|
119
|
+
if (!channelId) {
|
|
120
|
+
return { name, status: 'skip', message: 'not configured' };
|
|
121
|
+
}
|
|
122
|
+
return { name, status: 'ok' };
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Run all credential checks concurrently and return a structured report.
|
|
126
|
+
* Never throws — individual validators are responsible for their own error handling.
|
|
127
|
+
*
|
|
128
|
+
* When `activeProviders` is provided, the OpenAI key check is only run if
|
|
129
|
+
* `'openai'` is in the set; otherwise the result is omitted from the report
|
|
130
|
+
* entirely. The OpenRouter key check is only run if `'openrouter'` is in the
|
|
131
|
+
* set; otherwise the result is omitted from the report entirely. The Discord
|
|
132
|
+
* token check always runs. If `activeProviders` is omitted, the current
|
|
133
|
+
* behavior is preserved (OpenAI check runs as normal).
|
|
134
|
+
*/
|
|
135
|
+
export async function runCredentialChecks(opts) {
|
|
136
|
+
const runOpenAi = opts.activeProviders === undefined || opts.activeProviders.has('openai');
|
|
137
|
+
const runOpenRouter = opts.activeProviders !== undefined && opts.activeProviders.has('openrouter');
|
|
138
|
+
const [discordResult, openaiResult, openrouterResult, workspaceResult, statusResult] = await Promise.all([
|
|
139
|
+
checkDiscordToken(opts.token),
|
|
140
|
+
runOpenAi
|
|
141
|
+
? checkOpenAiKey({ apiKey: opts.openaiApiKey, baseUrl: opts.openaiBaseUrl })
|
|
142
|
+
: null,
|
|
143
|
+
runOpenRouter
|
|
144
|
+
? checkOpenRouterKey({ apiKey: opts.openrouterApiKey, baseUrl: opts.openrouterBaseUrl })
|
|
145
|
+
: null,
|
|
146
|
+
checkWorkspacePath(opts.workspacePath),
|
|
147
|
+
Promise.resolve(checkStatusChannel(opts.statusChannelId)),
|
|
148
|
+
]);
|
|
149
|
+
const results = [discordResult];
|
|
150
|
+
if (openaiResult !== null)
|
|
151
|
+
results.push(openaiResult);
|
|
152
|
+
if (openrouterResult !== null)
|
|
153
|
+
results.push(openrouterResult);
|
|
154
|
+
results.push(workspaceResult, statusResult);
|
|
155
|
+
const criticalFailures = results
|
|
156
|
+
.filter((r) => r.status === 'fail' && CRITICAL.has(r.name))
|
|
157
|
+
.map((r) => r.name);
|
|
158
|
+
const allOk = results.every((r) => r.status === 'ok' || r.status === 'skip');
|
|
159
|
+
return { results, criticalFailures, allOk };
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Format a credential check report into a compact, single-line string
|
|
163
|
+
* suitable for inclusion in the boot report posted to the status channel.
|
|
164
|
+
*
|
|
165
|
+
* Example: "discord-token: ok, openai-key: FAIL (invalid or expired key (401))"
|
|
166
|
+
*/
|
|
167
|
+
export function formatCredentialReport(report) {
|
|
168
|
+
return report.results
|
|
169
|
+
.map((r) => {
|
|
170
|
+
const tag = r.status === 'ok' ? 'ok' : r.status === 'skip' ? 'skip' : 'FAIL';
|
|
171
|
+
const detail = r.message ? ` (${r.message})` : '';
|
|
172
|
+
return `${r.name}: ${tag}${detail}`;
|
|
173
|
+
})
|
|
174
|
+
.join(', ');
|
|
175
|
+
}
|