@brianli/kimaki 0.4.72-brianli.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/bin.js +2 -0
- package/dist/ai-tool-to-genai.js +233 -0
- package/dist/ai-tool-to-genai.test.js +267 -0
- package/dist/ai-tool.js +6 -0
- package/dist/bin.js +87 -0
- package/dist/bot-token.js +121 -0
- package/dist/bot-token.test.js +134 -0
- package/dist/channel-management.js +101 -0
- package/dist/cli-parsing.test.js +89 -0
- package/dist/cli.js +2529 -0
- package/dist/commands/abort.js +82 -0
- package/dist/commands/action-buttons.js +257 -0
- package/dist/commands/add-project.js +114 -0
- package/dist/commands/agent.js +291 -0
- package/dist/commands/ask-question.js +223 -0
- package/dist/commands/compact.js +120 -0
- package/dist/commands/context-usage.js +140 -0
- package/dist/commands/create-new-project.js +118 -0
- package/dist/commands/diff.js +128 -0
- package/dist/commands/file-upload.js +275 -0
- package/dist/commands/fork.js +217 -0
- package/dist/commands/gemini-apikey.js +70 -0
- package/dist/commands/login.js +490 -0
- package/dist/commands/mention-mode.js +51 -0
- package/dist/commands/merge-worktree.js +124 -0
- package/dist/commands/model.js +694 -0
- package/dist/commands/permissions.js +163 -0
- package/dist/commands/queue.js +217 -0
- package/dist/commands/remove-project.js +115 -0
- package/dist/commands/restart-opencode-server.js +116 -0
- package/dist/commands/resume.js +159 -0
- package/dist/commands/run-command.js +79 -0
- package/dist/commands/session-id.js +78 -0
- package/dist/commands/session.js +192 -0
- package/dist/commands/share.js +80 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/undo-redo.js +159 -0
- package/dist/commands/unset-model.js +152 -0
- package/dist/commands/upgrade.js +42 -0
- package/dist/commands/user-command.js +148 -0
- package/dist/commands/verbosity.js +60 -0
- package/dist/commands/worktree-settings.js +50 -0
- package/dist/commands/worktree.js +299 -0
- package/dist/condense-memory.js +33 -0
- package/dist/config.js +110 -0
- package/dist/database.js +1050 -0
- package/dist/db.js +159 -0
- package/dist/db.test.js +49 -0
- package/dist/discord-api.js +28 -0
- package/dist/discord-auth.js +231 -0
- package/dist/discord-auth.test.js +80 -0
- package/dist/discord-bot.js +997 -0
- package/dist/discord-utils.js +560 -0
- package/dist/discord-utils.test.js +115 -0
- package/dist/errors.js +167 -0
- package/dist/escape-backticks.test.js +429 -0
- package/dist/format-tables.js +122 -0
- package/dist/format-tables.test.js +199 -0
- package/dist/forum-sync/config.js +79 -0
- package/dist/forum-sync/discord-operations.js +154 -0
- package/dist/forum-sync/index.js +5 -0
- package/dist/forum-sync/markdown.js +117 -0
- package/dist/forum-sync/sync-to-discord.js +417 -0
- package/dist/forum-sync/sync-to-files.js +190 -0
- package/dist/forum-sync/types.js +53 -0
- package/dist/forum-sync/watchers.js +307 -0
- package/dist/gateway-consumer.js +232 -0
- package/dist/gateway-consumer.test.js +18 -0
- package/dist/genai-worker-wrapper.js +111 -0
- package/dist/genai-worker.js +311 -0
- package/dist/genai.js +232 -0
- package/dist/generated/browser.js +17 -0
- package/dist/generated/client.js +35 -0
- package/dist/generated/commonInputTypes.js +10 -0
- package/dist/generated/enums.js +30 -0
- package/dist/generated/internal/class.js +41 -0
- package/dist/generated/internal/prismaNamespace.js +239 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js +209 -0
- package/dist/generated/models/bot_api_keys.js +1 -0
- package/dist/generated/models/bot_tokens.js +1 -0
- package/dist/generated/models/channel_agents.js +1 -0
- package/dist/generated/models/channel_directories.js +1 -0
- package/dist/generated/models/channel_mention_mode.js +1 -0
- package/dist/generated/models/channel_models.js +1 -0
- package/dist/generated/models/channel_verbosity.js +1 -0
- package/dist/generated/models/channel_worktrees.js +1 -0
- package/dist/generated/models/forum_sync_configs.js +1 -0
- package/dist/generated/models/global_models.js +1 -0
- package/dist/generated/models/ipc_requests.js +1 -0
- package/dist/generated/models/part_messages.js +1 -0
- package/dist/generated/models/scheduled_tasks.js +1 -0
- package/dist/generated/models/session_agents.js +1 -0
- package/dist/generated/models/session_models.js +1 -0
- package/dist/generated/models/session_start_sources.js +1 -0
- package/dist/generated/models/thread_sessions.js +1 -0
- package/dist/generated/models/thread_worktrees.js +1 -0
- package/dist/generated/models.js +1 -0
- package/dist/heap-monitor.js +95 -0
- package/dist/hrana-server.js +416 -0
- package/dist/hrana-server.test.js +368 -0
- package/dist/image-utils.js +112 -0
- package/dist/interaction-handler.js +327 -0
- package/dist/ipc-polling.js +251 -0
- package/dist/kimaki-digital-twin.e2e.test.js +165 -0
- package/dist/limit-heading-depth.js +25 -0
- package/dist/limit-heading-depth.test.js +105 -0
- package/dist/logger.js +160 -0
- package/dist/markdown.js +342 -0
- package/dist/markdown.test.js +253 -0
- package/dist/message-formatting.js +433 -0
- package/dist/message-formatting.test.js +73 -0
- package/dist/openai-realtime.js +228 -0
- package/dist/opencode-plugin-loading.e2e.test.js +91 -0
- package/dist/opencode-plugin.js +536 -0
- package/dist/opencode-plugin.test.js +98 -0
- package/dist/opencode.js +409 -0
- package/dist/privacy-sanitizer.js +105 -0
- package/dist/runtime-mode.js +51 -0
- package/dist/runtime-mode.test.js +115 -0
- package/dist/sentry.js +127 -0
- package/dist/session-handler/state.js +151 -0
- package/dist/session-handler.js +1874 -0
- package/dist/session-search.js +100 -0
- package/dist/session-search.test.js +40 -0
- package/dist/startup-service.js +153 -0
- package/dist/system-message.js +499 -0
- package/dist/task-runner.js +282 -0
- package/dist/task-schedule.js +191 -0
- package/dist/task-schedule.test.js +71 -0
- package/dist/thinking-utils.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +781 -0
- package/dist/tools.js +359 -0
- package/dist/unnest-code-blocks.js +136 -0
- package/dist/unnest-code-blocks.test.js +641 -0
- package/dist/upgrade.js +114 -0
- package/dist/utils.js +109 -0
- package/dist/voice-handler.js +606 -0
- package/dist/voice.js +304 -0
- package/dist/voice.test.js +187 -0
- package/dist/wait-session.js +94 -0
- package/dist/worker-types.js +4 -0
- package/dist/worktree-utils.js +727 -0
- package/dist/xml.js +92 -0
- package/dist/xml.test.js +32 -0
- package/package.json +82 -0
- package/schema.prisma +246 -0
- package/skills/batch/SKILL.md +87 -0
- package/skills/critique/SKILL.md +129 -0
- package/skills/errore/SKILL.md +589 -0
- package/skills/goke/.prettierrc +5 -0
- package/skills/goke/CHANGELOG.md +40 -0
- package/skills/goke/LICENSE +21 -0
- package/skills/goke/README.md +666 -0
- package/skills/goke/SKILL.md +458 -0
- package/skills/goke/package.json +43 -0
- package/skills/goke/src/__test__/coerce.test.ts +411 -0
- package/skills/goke/src/__test__/index.test.ts +1798 -0
- package/skills/goke/src/__test__/types.test-d.ts +111 -0
- package/skills/goke/src/coerce.ts +547 -0
- package/skills/goke/src/goke.ts +1362 -0
- package/skills/goke/src/index.ts +16 -0
- package/skills/goke/src/mri.ts +164 -0
- package/skills/goke/tsconfig.json +15 -0
- package/skills/jitter/EDITOR.md +219 -0
- package/skills/jitter/EXPORT-INTERNALS.md +309 -0
- package/skills/jitter/SKILL.md +158 -0
- package/skills/jitter/jitter-clipboard.json +1042 -0
- package/skills/jitter/package.json +14 -0
- package/skills/jitter/tsconfig.json +15 -0
- package/skills/jitter/utils/actions.ts +212 -0
- package/skills/jitter/utils/export.ts +114 -0
- package/skills/jitter/utils/index.ts +141 -0
- package/skills/jitter/utils/snapshot.ts +154 -0
- package/skills/jitter/utils/traverse.ts +246 -0
- package/skills/jitter/utils/types.ts +279 -0
- package/skills/jitter/utils/wait.ts +133 -0
- package/skills/playwriter/SKILL.md +31 -0
- package/skills/security-review/SKILL.md +208 -0
- package/skills/simplify/SKILL.md +58 -0
- package/skills/termcast/SKILL.md +945 -0
- package/skills/tuistory/SKILL.md +250 -0
- package/skills/zustand-centralized-state/SKILL.md +582 -0
- package/src/__snapshots__/compact-session-context-no-system.md +35 -0
- package/src/__snapshots__/compact-session-context.md +41 -0
- package/src/__snapshots__/first-session-no-info.md +17 -0
- package/src/__snapshots__/first-session-with-info.md +23 -0
- package/src/__snapshots__/session-1.md +17 -0
- package/src/__snapshots__/session-2.md +5871 -0
- package/src/__snapshots__/session-3.md +17 -0
- package/src/__snapshots__/session-with-tools.md +5871 -0
- package/src/ai-tool-to-genai.test.ts +296 -0
- package/src/ai-tool-to-genai.ts +282 -0
- package/src/ai-tool.ts +39 -0
- package/src/bin.ts +108 -0
- package/src/bot-token.test.ts +171 -0
- package/src/bot-token.ts +159 -0
- package/src/channel-management.ts +172 -0
- package/src/cli-parsing.test.ts +132 -0
- package/src/cli.ts +3605 -0
- package/src/commands/abort.ts +112 -0
- package/src/commands/action-buttons.ts +376 -0
- package/src/commands/add-project.ts +152 -0
- package/src/commands/agent.ts +404 -0
- package/src/commands/ask-question.ts +330 -0
- package/src/commands/compact.ts +157 -0
- package/src/commands/context-usage.ts +199 -0
- package/src/commands/create-new-project.ts +179 -0
- package/src/commands/diff.ts +165 -0
- package/src/commands/file-upload.ts +389 -0
- package/src/commands/fork.ts +320 -0
- package/src/commands/gemini-apikey.ts +104 -0
- package/src/commands/login.ts +634 -0
- package/src/commands/mention-mode.ts +77 -0
- package/src/commands/merge-worktree.ts +177 -0
- package/src/commands/model.ts +961 -0
- package/src/commands/permissions.ts +261 -0
- package/src/commands/queue.ts +296 -0
- package/src/commands/remove-project.ts +155 -0
- package/src/commands/restart-opencode-server.ts +162 -0
- package/src/commands/resume.ts +242 -0
- package/src/commands/run-command.ts +123 -0
- package/src/commands/session-id.ts +109 -0
- package/src/commands/session.ts +250 -0
- package/src/commands/share.ts +106 -0
- package/src/commands/types.ts +25 -0
- package/src/commands/undo-redo.ts +221 -0
- package/src/commands/unset-model.ts +189 -0
- package/src/commands/upgrade.ts +52 -0
- package/src/commands/user-command.ts +193 -0
- package/src/commands/verbosity.ts +88 -0
- package/src/commands/worktree-settings.ts +79 -0
- package/src/commands/worktree.ts +431 -0
- package/src/condense-memory.ts +36 -0
- package/src/config.ts +148 -0
- package/src/database.ts +1530 -0
- package/src/db.test.ts +60 -0
- package/src/db.ts +190 -0
- package/src/discord-api.ts +35 -0
- package/src/discord-bot.ts +1316 -0
- package/src/discord-utils.test.ts +132 -0
- package/src/discord-utils.ts +767 -0
- package/src/errors.ts +213 -0
- package/src/escape-backticks.test.ts +469 -0
- package/src/format-tables.test.ts +223 -0
- package/src/format-tables.ts +145 -0
- package/src/forum-sync/config.ts +92 -0
- package/src/forum-sync/discord-operations.ts +241 -0
- package/src/forum-sync/index.ts +9 -0
- package/src/forum-sync/markdown.ts +176 -0
- package/src/forum-sync/sync-to-discord.ts +595 -0
- package/src/forum-sync/sync-to-files.ts +294 -0
- package/src/forum-sync/types.ts +175 -0
- package/src/forum-sync/watchers.ts +454 -0
- package/src/genai-worker-wrapper.ts +164 -0
- package/src/genai-worker.ts +386 -0
- package/src/genai.ts +321 -0
- package/src/generated/browser.ts +109 -0
- package/src/generated/client.ts +131 -0
- package/src/generated/commonInputTypes.ts +512 -0
- package/src/generated/enums.ts +46 -0
- package/src/generated/internal/class.ts +362 -0
- package/src/generated/internal/prismaNamespace.ts +2251 -0
- package/src/generated/internal/prismaNamespaceBrowser.ts +308 -0
- package/src/generated/models/bot_api_keys.ts +1288 -0
- package/src/generated/models/bot_tokens.ts +1577 -0
- package/src/generated/models/channel_agents.ts +1256 -0
- package/src/generated/models/channel_directories.ts +2104 -0
- package/src/generated/models/channel_mention_mode.ts +1300 -0
- package/src/generated/models/channel_models.ts +1288 -0
- package/src/generated/models/channel_verbosity.ts +1224 -0
- package/src/generated/models/channel_worktrees.ts +1308 -0
- package/src/generated/models/forum_sync_configs.ts +1452 -0
- package/src/generated/models/global_models.ts +1288 -0
- package/src/generated/models/ipc_requests.ts +1485 -0
- package/src/generated/models/part_messages.ts +1302 -0
- package/src/generated/models/scheduled_tasks.ts +2320 -0
- package/src/generated/models/session_agents.ts +1086 -0
- package/src/generated/models/session_models.ts +1114 -0
- package/src/generated/models/session_start_sources.ts +1408 -0
- package/src/generated/models/thread_sessions.ts +1599 -0
- package/src/generated/models/thread_worktrees.ts +1352 -0
- package/src/generated/models.ts +29 -0
- package/src/heap-monitor.ts +121 -0
- package/src/hrana-server.test.ts +428 -0
- package/src/hrana-server.ts +547 -0
- package/src/image-utils.ts +149 -0
- package/src/interaction-handler.ts +461 -0
- package/src/ipc-polling.ts +325 -0
- package/src/kimaki-digital-twin.e2e.test.ts +201 -0
- package/src/limit-heading-depth.test.ts +116 -0
- package/src/limit-heading-depth.ts +26 -0
- package/src/logger.ts +203 -0
- package/src/markdown.test.ts +360 -0
- package/src/markdown.ts +410 -0
- package/src/message-formatting.test.ts +81 -0
- package/src/message-formatting.ts +549 -0
- package/src/openai-realtime.ts +362 -0
- package/src/opencode-plugin-loading.e2e.test.ts +112 -0
- package/src/opencode-plugin.test.ts +108 -0
- package/src/opencode-plugin.ts +652 -0
- package/src/opencode.ts +554 -0
- package/src/privacy-sanitizer.ts +142 -0
- package/src/schema.sql +158 -0
- package/src/sentry.ts +137 -0
- package/src/session-handler/state.ts +232 -0
- package/src/session-handler.ts +2668 -0
- package/src/session-search.test.ts +50 -0
- package/src/session-search.ts +148 -0
- package/src/startup-service.ts +200 -0
- package/src/system-message.ts +568 -0
- package/src/task-runner.ts +425 -0
- package/src/task-schedule.test.ts +84 -0
- package/src/task-schedule.ts +287 -0
- package/src/thinking-utils.ts +61 -0
- package/src/thread-message-queue.e2e.test.ts +997 -0
- package/src/tools.ts +432 -0
- package/src/unnest-code-blocks.test.ts +679 -0
- package/src/unnest-code-blocks.ts +168 -0
- package/src/upgrade.ts +127 -0
- package/src/utils.ts +145 -0
- package/src/voice-handler.ts +852 -0
- package/src/voice.test.ts +219 -0
- package/src/voice.ts +444 -0
- package/src/wait-session.ts +147 -0
- package/src/worker-types.ts +64 -0
- package/src/worktree-utils.ts +988 -0
- package/src/xml.test.ts +38 -0
- package/src/xml.ts +121 -0
package/dist/opencode.js
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
// OpenCode server process manager.
|
|
2
|
+
// Spawns and maintains OpenCode API servers per project directory,
|
|
3
|
+
// handles automatic restarts on failure, and provides typed SDK clients.
|
|
4
|
+
// Uses errore for type-safe error handling.
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import net from 'node:net';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
import { createOpencodeClient, } from '@opencode-ai/sdk/v2';
|
|
13
|
+
import { getBotToken } from './bot-token.js';
|
|
14
|
+
import { getDataDir, getLockPort, getVerboseOpencodeServer, } from './config.js';
|
|
15
|
+
import { getHranaUrl } from './hrana-server.js';
|
|
16
|
+
import * as errore from 'errore';
|
|
17
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
18
|
+
import { notifyError } from './sentry.js';
|
|
19
|
+
import { DirectoryNotAccessibleError, ServerStartError, ServerNotReadyError, FetchError, } from './errors.js';
|
|
20
|
+
const opencodeLogger = createLogger(LogPrefix.OPENCODE);
|
|
21
|
+
const STARTUP_STDERR_TAIL_LIMIT = 30;
|
|
22
|
+
const STARTUP_STDERR_LINE_MAX_LENGTH = 120;
|
|
23
|
+
const STARTUP_ERROR_REASON_MAX_LENGTH = 1500;
|
|
24
|
+
const ANSI_ESCAPE_REGEX = /[\u001B\u009B][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g;
|
|
25
|
+
function truncateWithEllipsis({ value, maxLength, }) {
|
|
26
|
+
if (maxLength <= 3) {
|
|
27
|
+
return value.slice(0, maxLength);
|
|
28
|
+
}
|
|
29
|
+
if (value.length <= maxLength) {
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
return `${value.slice(0, maxLength - 3)}...`;
|
|
33
|
+
}
|
|
34
|
+
function stripAnsiCodes(value) {
|
|
35
|
+
return value.replaceAll(ANSI_ESCAPE_REGEX, '');
|
|
36
|
+
}
|
|
37
|
+
function splitOutputChunkLines(chunk) {
|
|
38
|
+
return chunk
|
|
39
|
+
.split(/\r?\n/g)
|
|
40
|
+
.map((line) => stripAnsiCodes(line).trim())
|
|
41
|
+
.filter((line) => line.length > 0);
|
|
42
|
+
}
|
|
43
|
+
function sanitizeForCodeFence(line) {
|
|
44
|
+
return line.replaceAll('```', '`\u200b``');
|
|
45
|
+
}
|
|
46
|
+
function pushStartupStderrTail({ stderrTail, chunk, }) {
|
|
47
|
+
const incomingLines = splitOutputChunkLines(chunk);
|
|
48
|
+
const truncatedLines = incomingLines.map((line) => {
|
|
49
|
+
const sanitizedLine = sanitizeForCodeFence(line);
|
|
50
|
+
return truncateWithEllipsis({
|
|
51
|
+
value: sanitizedLine,
|
|
52
|
+
maxLength: STARTUP_STDERR_LINE_MAX_LENGTH,
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
stderrTail.push(...truncatedLines);
|
|
56
|
+
if (stderrTail.length > STARTUP_STDERR_TAIL_LIMIT) {
|
|
57
|
+
stderrTail.splice(0, stderrTail.length - STARTUP_STDERR_TAIL_LIMIT);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function buildStartupTimeoutReason({ maxAttempts, stderrTail, }) {
|
|
61
|
+
const baseReason = `Server did not start after ${maxAttempts} seconds`;
|
|
62
|
+
if (stderrTail.length === 0) {
|
|
63
|
+
return baseReason;
|
|
64
|
+
}
|
|
65
|
+
const formatReason = ({ lines, omitted, }) => {
|
|
66
|
+
const omittedLine = omitted > 0
|
|
67
|
+
? `[... ${omitted} older stderr lines omitted to fit Discord ...]\n`
|
|
68
|
+
: '';
|
|
69
|
+
const stderrCodeBlock = `${omittedLine}${lines.join('\n')}`;
|
|
70
|
+
return `${baseReason}\nLast opencode stderr lines:\n\`\`\`text\n${stderrCodeBlock}\n\`\`\``;
|
|
71
|
+
};
|
|
72
|
+
let lines = [...stderrTail];
|
|
73
|
+
let omitted = 0;
|
|
74
|
+
let formattedReason = formatReason({ lines, omitted });
|
|
75
|
+
while (formattedReason.length > STARTUP_ERROR_REASON_MAX_LENGTH &&
|
|
76
|
+
lines.length > 0) {
|
|
77
|
+
lines = lines.slice(1);
|
|
78
|
+
omitted += 1;
|
|
79
|
+
formattedReason = formatReason({ lines, omitted });
|
|
80
|
+
}
|
|
81
|
+
return truncateWithEllipsis({
|
|
82
|
+
value: formattedReason,
|
|
83
|
+
maxLength: STARTUP_ERROR_REASON_MAX_LENGTH,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
const opencodeServers = new Map();
|
|
87
|
+
const serverRetryCount = new Map();
|
|
88
|
+
async function getOpenPort() {
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
const server = net.createServer();
|
|
91
|
+
server.listen(0, () => {
|
|
92
|
+
const address = server.address();
|
|
93
|
+
if (address && typeof address === 'object') {
|
|
94
|
+
const port = address.port;
|
|
95
|
+
server.close(() => {
|
|
96
|
+
resolve(port);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
reject(new Error('Failed to get port'));
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
server.on('error', reject);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
async function waitForServer({ port, maxAttempts = 30, startupStderrTail, }) {
|
|
107
|
+
const endpoint = `http://127.0.0.1:${port}/api/health`;
|
|
108
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
109
|
+
const response = await errore.tryAsync({
|
|
110
|
+
try: () => fetch(endpoint),
|
|
111
|
+
catch: (e) => new FetchError({ url: endpoint, cause: e }),
|
|
112
|
+
});
|
|
113
|
+
if (response instanceof Error) {
|
|
114
|
+
// Connection refused or other transient errors - continue polling
|
|
115
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (response.status < 500) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
const body = await response.text();
|
|
122
|
+
// Fatal errors that won't resolve with retrying
|
|
123
|
+
if (body.includes('BunInstallFailedError')) {
|
|
124
|
+
return new ServerStartError({ port, reason: body.slice(0, 200) });
|
|
125
|
+
}
|
|
126
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
127
|
+
}
|
|
128
|
+
return new ServerStartError({
|
|
129
|
+
port,
|
|
130
|
+
reason: buildStartupTimeoutReason({
|
|
131
|
+
maxAttempts,
|
|
132
|
+
stderrTail: startupStderrTail,
|
|
133
|
+
}),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Initialize OpenCode server for a directory.
|
|
138
|
+
* @param directory - The directory to run the server in (cwd)
|
|
139
|
+
* @param options.originalRepoDirectory - For worktrees: the original repo directory to allow access to
|
|
140
|
+
*/
|
|
141
|
+
export async function initializeOpencodeForDirectory(directory, options) {
|
|
142
|
+
const existing = opencodeServers.get(directory);
|
|
143
|
+
if (existing && !existing.process.killed) {
|
|
144
|
+
opencodeLogger.log(`Reusing existing server on port ${existing.port} for directory: ${directory}`);
|
|
145
|
+
return () => {
|
|
146
|
+
const entry = opencodeServers.get(directory);
|
|
147
|
+
if (!entry?.client) {
|
|
148
|
+
throw new ServerNotReadyError({ directory });
|
|
149
|
+
}
|
|
150
|
+
return entry.client;
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
// Verify directory exists and is accessible before spawning
|
|
154
|
+
const accessCheck = errore.tryFn({
|
|
155
|
+
try: () => {
|
|
156
|
+
fs.accessSync(directory, fs.constants.R_OK | fs.constants.X_OK);
|
|
157
|
+
},
|
|
158
|
+
catch: () => new DirectoryNotAccessibleError({ directory }),
|
|
159
|
+
});
|
|
160
|
+
if (accessCheck instanceof Error) {
|
|
161
|
+
return accessCheck;
|
|
162
|
+
}
|
|
163
|
+
const port = await getOpenPort();
|
|
164
|
+
const opencodeCommand = process.env.OPENCODE_PATH || 'opencode';
|
|
165
|
+
// Normalize path separators for cross-platform compatibility (Windows uses backslashes)
|
|
166
|
+
const tmpdir = os.tmpdir().replaceAll('\\', '/');
|
|
167
|
+
const originalRepo = options?.originalRepoDirectory?.replaceAll('\\', '/');
|
|
168
|
+
const normalizedDirectory = directory.replaceAll('\\', '/');
|
|
169
|
+
// Build external_directory permissions, optionally including original repo for worktrees.
|
|
170
|
+
const externalDirectoryPermissions = {
|
|
171
|
+
'*': 'ask',
|
|
172
|
+
'/tmp': 'allow',
|
|
173
|
+
'/tmp/*': 'allow',
|
|
174
|
+
'/private/tmp': 'allow',
|
|
175
|
+
'/private/tmp/*': 'allow',
|
|
176
|
+
[tmpdir]: 'allow',
|
|
177
|
+
[`${tmpdir}/*`]: 'allow',
|
|
178
|
+
[normalizedDirectory]: 'allow',
|
|
179
|
+
[`${normalizedDirectory}/*`]: 'allow',
|
|
180
|
+
};
|
|
181
|
+
// Allow ~/.config/opencode so the agent doesn't get permission prompts when
|
|
182
|
+
// it tries to read the global AGENTS.md or opencode config (the path is
|
|
183
|
+
// visible in the system prompt, so models sometimes try to read it).
|
|
184
|
+
const opencodeConfigDir = path
|
|
185
|
+
.join(os.homedir(), '.config', 'opencode')
|
|
186
|
+
.replaceAll('\\', '/');
|
|
187
|
+
externalDirectoryPermissions[opencodeConfigDir] = 'allow';
|
|
188
|
+
externalDirectoryPermissions[`${opencodeConfigDir}/*`] = 'allow';
|
|
189
|
+
if (originalRepo) {
|
|
190
|
+
externalDirectoryPermissions[originalRepo] = 'allow';
|
|
191
|
+
externalDirectoryPermissions[`${originalRepo}/*`] = 'allow';
|
|
192
|
+
}
|
|
193
|
+
// Get bot token for plugin to use Discord API (env first, DB fallback)
|
|
194
|
+
const kimakiBotToken = getBotToken()?.token;
|
|
195
|
+
const serveArgs = ['serve', '--port', port.toString()];
|
|
196
|
+
if (getVerboseOpencodeServer()) {
|
|
197
|
+
serveArgs.push('--print-logs', '--log-level', 'DEBUG');
|
|
198
|
+
}
|
|
199
|
+
const serverProcess = spawn(opencodeCommand, serveArgs, {
|
|
200
|
+
stdio: 'pipe',
|
|
201
|
+
detached: false,
|
|
202
|
+
cwd: directory,
|
|
203
|
+
shell: true, // Required for .cmd files on Windows
|
|
204
|
+
env: {
|
|
205
|
+
...process.env,
|
|
206
|
+
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
207
|
+
$schema: 'https://opencode.ai/config.json',
|
|
208
|
+
lsp: false,
|
|
209
|
+
formatter: false,
|
|
210
|
+
plugin: [new URL('../src/opencode-plugin.ts', import.meta.url).href],
|
|
211
|
+
permission: {
|
|
212
|
+
edit: 'allow',
|
|
213
|
+
bash: 'allow',
|
|
214
|
+
external_directory: externalDirectoryPermissions,
|
|
215
|
+
webfetch: 'allow',
|
|
216
|
+
},
|
|
217
|
+
agent: {
|
|
218
|
+
explore: {
|
|
219
|
+
permission: {
|
|
220
|
+
'*': 'deny',
|
|
221
|
+
grep: 'allow',
|
|
222
|
+
glob: 'allow',
|
|
223
|
+
list: 'allow',
|
|
224
|
+
read: {
|
|
225
|
+
'*': 'allow',
|
|
226
|
+
'*.env': 'deny',
|
|
227
|
+
'*.env.*': 'deny',
|
|
228
|
+
'*.env.example': 'allow',
|
|
229
|
+
},
|
|
230
|
+
webfetch: 'allow',
|
|
231
|
+
websearch: 'allow',
|
|
232
|
+
codesearch: 'allow',
|
|
233
|
+
external_directory: externalDirectoryPermissions,
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
skills: {
|
|
238
|
+
paths: [path.resolve(__dirname, '..', 'skills')],
|
|
239
|
+
},
|
|
240
|
+
}),
|
|
241
|
+
OPENCODE_PORT: port.toString(),
|
|
242
|
+
KIMAKI_DATA_DIR: getDataDir(),
|
|
243
|
+
KIMAKI_LOCK_PORT: getLockPort().toString(),
|
|
244
|
+
...(kimakiBotToken && { KIMAKI_BOT_TOKEN: kimakiBotToken }),
|
|
245
|
+
...(getHranaUrl() && { KIMAKI_DB_URL: getHranaUrl() }),
|
|
246
|
+
...(process.env.KIMAKI_SENTRY_DSN && {
|
|
247
|
+
KIMAKI_SENTRY_DSN: process.env.KIMAKI_SENTRY_DSN,
|
|
248
|
+
}),
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
// Buffer logs until we know if server started successfully.
|
|
252
|
+
// Once ready, switch to forwarding if --verbose-opencode-server is set.
|
|
253
|
+
const logBuffer = [];
|
|
254
|
+
const startupStderrTail = [];
|
|
255
|
+
let serverReady = false;
|
|
256
|
+
const shortDir = path.basename(directory);
|
|
257
|
+
logBuffer.push(`Spawned opencode serve --port ${port} in ${directory} (pid: ${serverProcess.pid})`);
|
|
258
|
+
serverProcess.stdout?.on('data', (data) => {
|
|
259
|
+
try {
|
|
260
|
+
const chunk = data.toString();
|
|
261
|
+
const lines = splitOutputChunkLines(chunk);
|
|
262
|
+
if (!serverReady) {
|
|
263
|
+
logBuffer.push(...lines.map((line) => `[stdout] ${line}`));
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (getVerboseOpencodeServer()) {
|
|
267
|
+
for (const line of lines) {
|
|
268
|
+
opencodeLogger.log(`[${shortDir}:${port}] ${line}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
catch (error) {
|
|
273
|
+
logBuffer.push(`Failed to process stdout startup logs: ${error}`);
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
serverProcess.stderr?.on('data', (data) => {
|
|
277
|
+
try {
|
|
278
|
+
const chunk = data.toString();
|
|
279
|
+
const lines = splitOutputChunkLines(chunk);
|
|
280
|
+
if (!serverReady) {
|
|
281
|
+
logBuffer.push(...lines.map((line) => `[stderr] ${line}`));
|
|
282
|
+
pushStartupStderrTail({ stderrTail: startupStderrTail, chunk });
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (getVerboseOpencodeServer()) {
|
|
286
|
+
for (const line of lines) {
|
|
287
|
+
opencodeLogger.error(`[${shortDir}:${port}] ${line}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
catch (error) {
|
|
292
|
+
logBuffer.push(`Failed to process stderr startup logs: ${error}`);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
serverProcess.on('error', (error) => {
|
|
296
|
+
logBuffer.push(`Failed to start server on port ${port}: ${error}`);
|
|
297
|
+
});
|
|
298
|
+
serverProcess.on('exit', (code) => {
|
|
299
|
+
opencodeLogger.log(`Opencode server on ${directory} exited with code:`, code);
|
|
300
|
+
// Capture init options before deleting the entry so auto-restart preserves
|
|
301
|
+
// worktree repo access.
|
|
302
|
+
const storedInitOptions = opencodeServers.get(directory)?.initOptions;
|
|
303
|
+
opencodeServers.delete(directory);
|
|
304
|
+
if (code !== 0) {
|
|
305
|
+
const retryCount = serverRetryCount.get(directory) || 0;
|
|
306
|
+
if (retryCount < 5) {
|
|
307
|
+
serverRetryCount.set(directory, retryCount + 1);
|
|
308
|
+
opencodeLogger.log(`Restarting server for directory: ${directory} (attempt ${retryCount + 1}/5)`);
|
|
309
|
+
initializeOpencodeForDirectory(directory, storedInitOptions).then((result) => {
|
|
310
|
+
if (result instanceof Error) {
|
|
311
|
+
opencodeLogger.error(`Failed to restart opencode server:`, result);
|
|
312
|
+
void notifyError(result, `OpenCode server restart failed for ${directory}`);
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
const crashError = new Error(`Server for ${directory} crashed too many times (5), not restarting`);
|
|
318
|
+
opencodeLogger.error(crashError.message);
|
|
319
|
+
void notifyError(crashError, `OpenCode server crash loop exhausted`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
serverRetryCount.delete(directory);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
const waitResult = await waitForServer({
|
|
327
|
+
port,
|
|
328
|
+
startupStderrTail,
|
|
329
|
+
});
|
|
330
|
+
if (waitResult instanceof Error) {
|
|
331
|
+
// Dump buffered logs on failure
|
|
332
|
+
opencodeLogger.error(`Server failed to start for ${directory}:`);
|
|
333
|
+
for (const line of logBuffer) {
|
|
334
|
+
opencodeLogger.error(` ${line}`);
|
|
335
|
+
}
|
|
336
|
+
return waitResult;
|
|
337
|
+
}
|
|
338
|
+
serverReady = true;
|
|
339
|
+
opencodeLogger.log(`Server ready on port ${port}`);
|
|
340
|
+
// When verbose mode is enabled, also dump startup logs so plugin loading
|
|
341
|
+
// errors and other startup output are visible in kimaki.log.
|
|
342
|
+
if (getVerboseOpencodeServer()) {
|
|
343
|
+
for (const line of logBuffer) {
|
|
344
|
+
opencodeLogger.log(`[${shortDir}:${port}:startup] ${line}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const baseUrl = `http://127.0.0.1:${port}`;
|
|
348
|
+
const fetchWithTimeout = (request) => fetch(request, {
|
|
349
|
+
// @ts-ignore
|
|
350
|
+
timeout: false,
|
|
351
|
+
});
|
|
352
|
+
const client = createOpencodeClient({
|
|
353
|
+
baseUrl,
|
|
354
|
+
fetch: fetchWithTimeout,
|
|
355
|
+
});
|
|
356
|
+
opencodeServers.set(directory, {
|
|
357
|
+
process: serverProcess,
|
|
358
|
+
client,
|
|
359
|
+
port,
|
|
360
|
+
initOptions: options,
|
|
361
|
+
});
|
|
362
|
+
return () => {
|
|
363
|
+
const entry = opencodeServers.get(directory);
|
|
364
|
+
if (!entry?.client) {
|
|
365
|
+
throw new ServerNotReadyError({ directory });
|
|
366
|
+
}
|
|
367
|
+
return entry.client;
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
export function getOpencodeServers() {
|
|
371
|
+
return opencodeServers;
|
|
372
|
+
}
|
|
373
|
+
export function getOpencodeServerPort(directory) {
|
|
374
|
+
const entry = opencodeServers.get(directory);
|
|
375
|
+
return entry?.port ?? null;
|
|
376
|
+
}
|
|
377
|
+
export function getOpencodeClient(directory) {
|
|
378
|
+
const entry = opencodeServers.get(directory);
|
|
379
|
+
return entry?.client ?? null;
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Restart the opencode server for a directory.
|
|
383
|
+
* Kills the existing process and reinitializes a new one.
|
|
384
|
+
* Used for resolving opencode state issues, refreshing auth, plugins, etc.
|
|
385
|
+
*/
|
|
386
|
+
export async function restartOpencodeServer(directory) {
|
|
387
|
+
const existing = opencodeServers.get(directory);
|
|
388
|
+
// Preserve init options (originalRepoDirectory) so the restarted
|
|
389
|
+
// server retains worktree access.
|
|
390
|
+
const storedInitOptions = existing?.initOptions;
|
|
391
|
+
if (existing) {
|
|
392
|
+
opencodeLogger.log(`Killing existing server for directory: ${directory} (pid: ${existing.process.pid})`);
|
|
393
|
+
// Reset retry count so the exit handler doesn't auto-restart
|
|
394
|
+
serverRetryCount.set(directory, 999);
|
|
395
|
+
existing.process.kill('SIGTERM');
|
|
396
|
+
opencodeServers.delete(directory);
|
|
397
|
+
// Give the process time to fully terminate
|
|
398
|
+
await new Promise((resolve) => {
|
|
399
|
+
setTimeout(resolve, 1000);
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
// Reset retry count for the fresh start
|
|
403
|
+
serverRetryCount.delete(directory);
|
|
404
|
+
const result = await initializeOpencodeForDirectory(directory, storedInitOptions);
|
|
405
|
+
if (result instanceof Error) {
|
|
406
|
+
return result;
|
|
407
|
+
}
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Sensitive data redaction helpers for logs and telemetry payloads.
|
|
2
|
+
// Redacts common secrets, identifiers, emails, and can optionally redact paths.
|
|
3
|
+
const CORE_SENSITIVE_REPLACEMENTS = [
|
|
4
|
+
{
|
|
5
|
+
pattern: /\bBearer\s+[A-Za-z0-9._-]{10,}\b/gi,
|
|
6
|
+
replacement: 'Bearer [REDACTED]',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
pattern: /\bsk-[A-Za-z0-9]{16,}\b/g,
|
|
10
|
+
replacement: '[REDACTED_OPENAI_KEY]',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
pattern: /\bAIza[0-9A-Za-z_-]{20,}\b/g,
|
|
14
|
+
replacement: '[REDACTED_GOOGLE_KEY]',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
pattern: /\bgh[pousr]_[A-Za-z0-9]{20,}\b/g,
|
|
18
|
+
replacement: '[REDACTED_GITHUB_TOKEN]',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
pattern: /([?&](?:token|api[_-]?key|key|secret|password|authorization)=)[^&\s]+/gi,
|
|
22
|
+
replacement: '$1[REDACTED]',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
pattern: /(\b(?:token|api[_-]?key|secret|password|authorization)\b\s*[:=]\s*")([^"]+)(")/gi,
|
|
26
|
+
replacement: '$1[REDACTED]$3',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
pattern: /(\b(?:token|api[_-]?key|secret|password|authorization)\b\s*[:=]\s*)([^\s,;]+)/gi,
|
|
30
|
+
replacement: '$1[REDACTED]',
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
const PATH_REPLACEMENTS = [
|
|
34
|
+
{
|
|
35
|
+
pattern: /\/(?:Users|home)\/[^/\s]+\/[^\s'"`)]*/g,
|
|
36
|
+
replacement: '[REDACTED_PATH]',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
pattern: /[A-Za-z]:\\[^\s'"`)]*/g,
|
|
40
|
+
replacement: '[REDACTED_PATH]',
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
export function sanitizeSensitiveText(value, { redactPaths = false } = {}) {
|
|
44
|
+
const replacements = redactPaths
|
|
45
|
+
? [...CORE_SENSITIVE_REPLACEMENTS, ...PATH_REPLACEMENTS]
|
|
46
|
+
: CORE_SENSITIVE_REPLACEMENTS;
|
|
47
|
+
return replacements.reduce((current, entry) => {
|
|
48
|
+
return current.replace(entry.pattern, entry.replacement);
|
|
49
|
+
}, value);
|
|
50
|
+
}
|
|
51
|
+
export function sanitizeUnknownValue(value, { depth = 0, seen = new WeakSet(), redactPaths = false, } = {}) {
|
|
52
|
+
if (depth > 8) {
|
|
53
|
+
return '[REDACTED_DEPTH_LIMIT]';
|
|
54
|
+
}
|
|
55
|
+
if (typeof value === 'string') {
|
|
56
|
+
return sanitizeSensitiveText(value, { redactPaths });
|
|
57
|
+
}
|
|
58
|
+
if (typeof value === 'number' ||
|
|
59
|
+
typeof value === 'boolean' ||
|
|
60
|
+
value === null ||
|
|
61
|
+
value === undefined) {
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
if (value instanceof Date) {
|
|
65
|
+
return value.toISOString();
|
|
66
|
+
}
|
|
67
|
+
if (value instanceof Error) {
|
|
68
|
+
const sanitizedStack = value.stack
|
|
69
|
+
? sanitizeSensitiveText(value.stack, { redactPaths })
|
|
70
|
+
: undefined;
|
|
71
|
+
return {
|
|
72
|
+
name: value.name,
|
|
73
|
+
message: sanitizeSensitiveText(value.message, { redactPaths }),
|
|
74
|
+
stack: sanitizedStack,
|
|
75
|
+
cause: sanitizeUnknownValue(value.cause, {
|
|
76
|
+
depth: depth + 1,
|
|
77
|
+
seen,
|
|
78
|
+
redactPaths,
|
|
79
|
+
}),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (Array.isArray(value)) {
|
|
83
|
+
return value.map((item) => {
|
|
84
|
+
return sanitizeUnknownValue(item, { depth: depth + 1, seen, redactPaths });
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (typeof value === 'object') {
|
|
88
|
+
if (seen.has(value)) {
|
|
89
|
+
return '[REDACTED_CIRCULAR]';
|
|
90
|
+
}
|
|
91
|
+
seen.add(value);
|
|
92
|
+
const sanitizedEntries = Object.entries(value).map(([key, entryValue]) => {
|
|
93
|
+
return [
|
|
94
|
+
key,
|
|
95
|
+
sanitizeUnknownValue(entryValue, {
|
|
96
|
+
depth: depth + 1,
|
|
97
|
+
seen,
|
|
98
|
+
redactPaths,
|
|
99
|
+
}),
|
|
100
|
+
];
|
|
101
|
+
});
|
|
102
|
+
return Object.fromEntries(sanitizedEntries);
|
|
103
|
+
}
|
|
104
|
+
return sanitizeSensitiveText(String(value), { redactPaths });
|
|
105
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export var Mode;
|
|
2
|
+
(function (Mode) {
|
|
3
|
+
Mode["Bot"] = "bot";
|
|
4
|
+
Mode["Auth"] = "auth";
|
|
5
|
+
})(Mode || (Mode = {}));
|
|
6
|
+
function trim(value) {
|
|
7
|
+
const cleaned = value?.trim();
|
|
8
|
+
return cleaned || undefined;
|
|
9
|
+
}
|
|
10
|
+
function resolveAuthMode(env) {
|
|
11
|
+
const privateKey = trim(env.KIMAKI_PRIVATE_KEY);
|
|
12
|
+
const guildId = trim(env.KIMAKI_GUILD_ID);
|
|
13
|
+
const appId = trim(env.KIMAKI_APP_ID);
|
|
14
|
+
if (!privateKey || !guildId || !appId) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
kind: Mode.Auth,
|
|
19
|
+
privateKey,
|
|
20
|
+
guildId,
|
|
21
|
+
appId,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function resolveRuntimeMode({ env, dbToken, dbAppId, }) {
|
|
25
|
+
const envToken = trim(env.KIMAKI_BOT_TOKEN);
|
|
26
|
+
if (envToken) {
|
|
27
|
+
return {
|
|
28
|
+
kind: Mode.Bot,
|
|
29
|
+
token: envToken,
|
|
30
|
+
source: 'env',
|
|
31
|
+
appId: undefined,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const authMode = resolveAuthMode(env);
|
|
35
|
+
if (authMode) {
|
|
36
|
+
return authMode;
|
|
37
|
+
}
|
|
38
|
+
const databaseToken = trim(dbToken);
|
|
39
|
+
if (databaseToken) {
|
|
40
|
+
return {
|
|
41
|
+
kind: Mode.Bot,
|
|
42
|
+
token: databaseToken,
|
|
43
|
+
source: 'database',
|
|
44
|
+
appId: trim(dbAppId),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
export function shouldRegisterCommandsLocally(mode) {
|
|
50
|
+
return mode.kind === Mode.Bot;
|
|
51
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { Mode, resolveRuntimeMode, shouldRegisterCommandsLocally, } from './runtime-mode.js';
|
|
3
|
+
function authEnv(overrides = {}) {
|
|
4
|
+
return {
|
|
5
|
+
KIMAKI_PRIVATE_KEY: 'private-key',
|
|
6
|
+
KIMAKI_GUILD_ID: 'guild-1',
|
|
7
|
+
KIMAKI_APP_ID: 'app-1',
|
|
8
|
+
...overrides,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
describe('resolveRuntimeMode', () => {
|
|
12
|
+
test('prefers bot mode when env token is present', () => {
|
|
13
|
+
const mode = resolveRuntimeMode({
|
|
14
|
+
env: {
|
|
15
|
+
...authEnv(),
|
|
16
|
+
KIMAKI_BOT_TOKEN: 'token-from-env',
|
|
17
|
+
},
|
|
18
|
+
dbToken: 'token-from-db',
|
|
19
|
+
dbAppId: 'db-app',
|
|
20
|
+
});
|
|
21
|
+
expect(mode).toEqual({
|
|
22
|
+
kind: Mode.Bot,
|
|
23
|
+
token: 'token-from-env',
|
|
24
|
+
source: 'env',
|
|
25
|
+
appId: undefined,
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
test('uses auth mode when auth env is complete and no env token', () => {
|
|
29
|
+
const mode = resolveRuntimeMode({
|
|
30
|
+
env: authEnv(),
|
|
31
|
+
dbToken: 'token-from-db',
|
|
32
|
+
dbAppId: 'db-app',
|
|
33
|
+
});
|
|
34
|
+
expect(mode).toEqual({
|
|
35
|
+
kind: Mode.Auth,
|
|
36
|
+
privateKey: 'private-key',
|
|
37
|
+
guildId: 'guild-1',
|
|
38
|
+
appId: 'app-1',
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
test('does not accept legacy app id aliases for auth mode', () => {
|
|
42
|
+
const mode = resolveRuntimeMode({
|
|
43
|
+
env: authEnv({
|
|
44
|
+
KIMAKI_APP_ID: undefined,
|
|
45
|
+
KIMAKI_APPLICATION_ID: 'app-from-kimaki-application-id',
|
|
46
|
+
}),
|
|
47
|
+
});
|
|
48
|
+
expect(mode).toBeNull();
|
|
49
|
+
});
|
|
50
|
+
test('does not accept legacy guild aliases for auth mode', () => {
|
|
51
|
+
const mode = resolveRuntimeMode({
|
|
52
|
+
env: authEnv({
|
|
53
|
+
KIMAKI_GUILD_ID: undefined,
|
|
54
|
+
KIMAKI_GUILD: 'guild-from-kimaki-guild',
|
|
55
|
+
}),
|
|
56
|
+
});
|
|
57
|
+
expect(mode).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
test('falls back to db bot mode when auth env is incomplete', () => {
|
|
60
|
+
const mode = resolveRuntimeMode({
|
|
61
|
+
env: authEnv({ KIMAKI_APP_ID: undefined }),
|
|
62
|
+
dbToken: 'token-from-db',
|
|
63
|
+
dbAppId: 'db-app',
|
|
64
|
+
});
|
|
65
|
+
expect(mode).toEqual({
|
|
66
|
+
kind: Mode.Bot,
|
|
67
|
+
token: 'token-from-db',
|
|
68
|
+
source: 'database',
|
|
69
|
+
appId: 'db-app',
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
test('returns null when no valid mode can be resolved', () => {
|
|
73
|
+
const mode = resolveRuntimeMode({
|
|
74
|
+
env: {
|
|
75
|
+
KIMAKI_PRIVATE_KEY: 'only-private-key',
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
expect(mode).toBeNull();
|
|
79
|
+
});
|
|
80
|
+
test('trims whitespace for env values', () => {
|
|
81
|
+
const mode = resolveRuntimeMode({
|
|
82
|
+
env: {
|
|
83
|
+
KIMAKI_PRIVATE_KEY: ' private-key ',
|
|
84
|
+
KIMAKI_GUILD_ID: ' guild-1 ',
|
|
85
|
+
KIMAKI_APP_ID: ' app-1 ',
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
expect(mode).toEqual({
|
|
89
|
+
kind: Mode.Auth,
|
|
90
|
+
privateKey: 'private-key',
|
|
91
|
+
guildId: 'guild-1',
|
|
92
|
+
appId: 'app-1',
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
describe('shouldRegisterCommandsLocally', () => {
|
|
97
|
+
test('returns true for bot mode', () => {
|
|
98
|
+
const mode = {
|
|
99
|
+
kind: Mode.Bot,
|
|
100
|
+
token: 'token',
|
|
101
|
+
source: 'env',
|
|
102
|
+
appId: undefined,
|
|
103
|
+
};
|
|
104
|
+
expect(shouldRegisterCommandsLocally(mode)).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
test('returns false for auth mode', () => {
|
|
107
|
+
const mode = {
|
|
108
|
+
kind: Mode.Auth,
|
|
109
|
+
privateKey: 'private-key',
|
|
110
|
+
guildId: 'guild',
|
|
111
|
+
appId: 'app',
|
|
112
|
+
};
|
|
113
|
+
expect(shouldRegisterCommandsLocally(mode)).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
});
|