@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
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
let dbBotToken = null;
|
|
3
|
+
function toBase64(value) {
|
|
4
|
+
const normalized = value.replace(/-/g, '+').replace(/_/g, '/');
|
|
5
|
+
const padding = '='.repeat((4 - (normalized.length % 4)) % 4);
|
|
6
|
+
return `${normalized}${padding}`;
|
|
7
|
+
}
|
|
8
|
+
function resolveAuthModeConfig() {
|
|
9
|
+
const guildId = process.env.KIMAKI_GUILD_ID?.trim();
|
|
10
|
+
const privateKey = process.env.KIMAKI_PRIVATE_KEY?.trim();
|
|
11
|
+
const appId = process.env.KIMAKI_APP_ID?.trim();
|
|
12
|
+
if (!guildId || !privateKey || !appId) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
guildId,
|
|
17
|
+
privateKey,
|
|
18
|
+
appId,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function parsePrivateKey(privateKeyValue) {
|
|
22
|
+
if (privateKeyValue.includes('BEGIN PRIVATE KEY')) {
|
|
23
|
+
return crypto.createPrivateKey(privateKeyValue);
|
|
24
|
+
}
|
|
25
|
+
const candidates = [];
|
|
26
|
+
try {
|
|
27
|
+
candidates.push({
|
|
28
|
+
key: Buffer.from(toBase64(privateKeyValue), 'base64'),
|
|
29
|
+
format: 'der',
|
|
30
|
+
type: 'pkcs8',
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Ignore and continue to hex fallback.
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
candidates.push({
|
|
38
|
+
key: Buffer.from(privateKeyValue, 'hex'),
|
|
39
|
+
format: 'der',
|
|
40
|
+
type: 'pkcs8',
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// Ignore and continue.
|
|
45
|
+
}
|
|
46
|
+
for (const candidate of candidates) {
|
|
47
|
+
try {
|
|
48
|
+
return crypto.createPrivateKey(candidate);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Try next candidate.
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
throw new Error('Invalid KIMAKI_PRIVATE_KEY for auth mode');
|
|
55
|
+
}
|
|
56
|
+
function createAuthModeToken(config) {
|
|
57
|
+
const timestamp = Date.now();
|
|
58
|
+
const key = parsePrivateKey(config.privateKey);
|
|
59
|
+
const message = `${config.guildId}\n${timestamp}`;
|
|
60
|
+
const signature = crypto
|
|
61
|
+
.sign(null, Buffer.from(message, 'utf8'), key)
|
|
62
|
+
.toString('base64url');
|
|
63
|
+
const guildPart = Buffer.from(config.guildId, 'utf8').toString('base64');
|
|
64
|
+
return `${guildPart}.${timestamp}.${signature}`;
|
|
65
|
+
}
|
|
66
|
+
// Derive the Discord Application ID from a bot token.
|
|
67
|
+
// Discord bot tokens have the format: base64(userId).timestamp.hmac
|
|
68
|
+
// The first segment is the bot's user ID (= Application ID) base64-encoded.
|
|
69
|
+
export function appIdFromToken(token) {
|
|
70
|
+
const segment = token.split('.')[0];
|
|
71
|
+
if (!segment) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const decoded = Buffer.from(segment, 'base64').toString('utf8');
|
|
76
|
+
if (/^\d{17,20}$/.test(decoded)) {
|
|
77
|
+
return decoded;
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export function hydrateBotTokenCache(botToken) {
|
|
86
|
+
dbBotToken = botToken;
|
|
87
|
+
}
|
|
88
|
+
export function isAuthModeEnabled() {
|
|
89
|
+
return resolveAuthModeConfig() !== null;
|
|
90
|
+
}
|
|
91
|
+
export function getBotToken(options = {}) {
|
|
92
|
+
const { appIdOverride, preferEnv = true, allowDatabase = true } = options;
|
|
93
|
+
const authMode = resolveAuthModeConfig();
|
|
94
|
+
if (authMode) {
|
|
95
|
+
return {
|
|
96
|
+
token: createAuthModeToken(authMode),
|
|
97
|
+
appId: appIdOverride || authMode.appId,
|
|
98
|
+
source: 'auth',
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const envToken = process.env.KIMAKI_BOT_TOKEN;
|
|
102
|
+
if (preferEnv && envToken) {
|
|
103
|
+
return {
|
|
104
|
+
token: envToken,
|
|
105
|
+
appId: appIdOverride || appIdFromToken(envToken),
|
|
106
|
+
source: 'env',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
if (!allowDatabase) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
const botRow = dbBotToken;
|
|
113
|
+
if (!botRow) {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
token: botRow.token,
|
|
118
|
+
appId: appIdOverride || botRow.app_id,
|
|
119
|
+
source: 'db',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { afterAll, beforeEach, describe, expect, test } from 'vitest';
|
|
3
|
+
import { appIdFromToken, getBotToken, hydrateBotTokenCache, isAuthModeEnabled, } from './bot-token.js';
|
|
4
|
+
const ORIGINAL_BOT_TOKEN = process.env.KIMAKI_BOT_TOKEN;
|
|
5
|
+
const ORIGINAL_GUILD_ID = process.env.KIMAKI_GUILD_ID;
|
|
6
|
+
const ORIGINAL_PRIVATE_KEY = process.env.KIMAKI_PRIVATE_KEY;
|
|
7
|
+
const ORIGINAL_APP_ID = process.env.KIMAKI_APP_ID;
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
delete process.env.KIMAKI_BOT_TOKEN;
|
|
10
|
+
delete process.env.KIMAKI_GUILD_ID;
|
|
11
|
+
delete process.env.KIMAKI_PRIVATE_KEY;
|
|
12
|
+
delete process.env.KIMAKI_APP_ID;
|
|
13
|
+
hydrateBotTokenCache(null);
|
|
14
|
+
});
|
|
15
|
+
afterAll(() => {
|
|
16
|
+
process.env.KIMAKI_BOT_TOKEN = ORIGINAL_BOT_TOKEN;
|
|
17
|
+
process.env.KIMAKI_GUILD_ID = ORIGINAL_GUILD_ID;
|
|
18
|
+
process.env.KIMAKI_PRIVATE_KEY = ORIGINAL_PRIVATE_KEY;
|
|
19
|
+
process.env.KIMAKI_APP_ID = ORIGINAL_APP_ID;
|
|
20
|
+
});
|
|
21
|
+
describe('appIdFromToken', () => {
|
|
22
|
+
test('derives app id from valid token format', () => {
|
|
23
|
+
const token = 'MTQ3Njc0NTc2MzAwOTU5MzM2NQ.anything.anything';
|
|
24
|
+
expect(appIdFromToken(token)).toBe('1476745763009593365');
|
|
25
|
+
});
|
|
26
|
+
test('returns undefined for malformed tokens', () => {
|
|
27
|
+
expect(appIdFromToken('not-a-token')).toBeUndefined();
|
|
28
|
+
expect(appIdFromToken('')).toBeUndefined();
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
describe('getBotToken', () => {
|
|
32
|
+
test('prefers env token over db by default', () => {
|
|
33
|
+
process.env.KIMAKI_BOT_TOKEN =
|
|
34
|
+
'MTQ3Njc0NTc2MzAwOTU5MzM2NQ.env.payload';
|
|
35
|
+
hydrateBotTokenCache({ app_id: 'db-app', token: 'db-token' });
|
|
36
|
+
const resolved = getBotToken();
|
|
37
|
+
expect(resolved).toEqual({
|
|
38
|
+
token: 'MTQ3Njc0NTc2MzAwOTU5MzM2NQ.env.payload',
|
|
39
|
+
appId: '1476745763009593365',
|
|
40
|
+
source: 'env',
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
test('uses db token when env is absent', () => {
|
|
44
|
+
delete process.env.KIMAKI_BOT_TOKEN;
|
|
45
|
+
hydrateBotTokenCache({
|
|
46
|
+
app_id: '1476745763009593365',
|
|
47
|
+
token: 'db-token',
|
|
48
|
+
});
|
|
49
|
+
const resolved = getBotToken();
|
|
50
|
+
expect(resolved).toEqual({
|
|
51
|
+
token: 'db-token',
|
|
52
|
+
appId: '1476745763009593365',
|
|
53
|
+
source: 'db',
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
test('supports db-only lookup when preferEnv is false', () => {
|
|
57
|
+
process.env.KIMAKI_BOT_TOKEN = 'env-token';
|
|
58
|
+
hydrateBotTokenCache({
|
|
59
|
+
app_id: '1476745763009593365',
|
|
60
|
+
token: 'db-token',
|
|
61
|
+
});
|
|
62
|
+
const resolved = getBotToken({ preferEnv: false });
|
|
63
|
+
expect(resolved).toEqual({
|
|
64
|
+
token: 'db-token',
|
|
65
|
+
appId: '1476745763009593365',
|
|
66
|
+
source: 'db',
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
test('can skip db lookup entirely', () => {
|
|
70
|
+
delete process.env.KIMAKI_BOT_TOKEN;
|
|
71
|
+
hydrateBotTokenCache({ app_id: 'x', token: 'y' });
|
|
72
|
+
const resolved = getBotToken({ allowDatabase: false });
|
|
73
|
+
expect(resolved).toBeUndefined();
|
|
74
|
+
});
|
|
75
|
+
test('applies appId override consistently', () => {
|
|
76
|
+
process.env.KIMAKI_BOT_TOKEN = 'env-token';
|
|
77
|
+
const fromEnv = getBotToken({
|
|
78
|
+
appIdOverride: 'override-app-id',
|
|
79
|
+
allowDatabase: false,
|
|
80
|
+
});
|
|
81
|
+
expect(fromEnv?.appId).toBe('override-app-id');
|
|
82
|
+
delete process.env.KIMAKI_BOT_TOKEN;
|
|
83
|
+
hydrateBotTokenCache({
|
|
84
|
+
app_id: 'db-app',
|
|
85
|
+
token: 'db-token',
|
|
86
|
+
});
|
|
87
|
+
const fromDb = getBotToken({
|
|
88
|
+
appIdOverride: 'override-app-id',
|
|
89
|
+
});
|
|
90
|
+
expect(fromDb?.appId).toBe('override-app-id');
|
|
91
|
+
});
|
|
92
|
+
test('auth mode takes precedence over env and db token', () => {
|
|
93
|
+
const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519');
|
|
94
|
+
process.env.KIMAKI_GUILD_ID = '1477130736841658398';
|
|
95
|
+
process.env.KIMAKI_APP_ID = '1476745763009593365';
|
|
96
|
+
process.env.KIMAKI_PRIVATE_KEY = privateKey
|
|
97
|
+
.export({ format: 'pem', type: 'pkcs8' })
|
|
98
|
+
.toString();
|
|
99
|
+
process.env.KIMAKI_BOT_TOKEN = 'env-token';
|
|
100
|
+
hydrateBotTokenCache({
|
|
101
|
+
app_id: 'db-app',
|
|
102
|
+
token: 'db-token',
|
|
103
|
+
});
|
|
104
|
+
const resolved = getBotToken();
|
|
105
|
+
expect(isAuthModeEnabled()).toBe(true);
|
|
106
|
+
expect(resolved?.source).toBe('auth');
|
|
107
|
+
expect(resolved?.appId).toBe('1476745763009593365');
|
|
108
|
+
expect(resolved?.token.split('.')).toHaveLength(3);
|
|
109
|
+
const tokenParts = resolved.token.split('.');
|
|
110
|
+
expect(tokenParts).toHaveLength(3);
|
|
111
|
+
const guildPart = tokenParts[0];
|
|
112
|
+
const timestamp = tokenParts[1];
|
|
113
|
+
const signaturePart = tokenParts[2];
|
|
114
|
+
const decodedGuildId = Buffer.from(guildPart, 'base64').toString('utf8');
|
|
115
|
+
expect(decodedGuildId).toBe('1477130736841658398');
|
|
116
|
+
expect(Number.isNaN(Number.parseInt(timestamp, 10))).toBe(false);
|
|
117
|
+
const signature = Buffer.from(signaturePart, 'base64url');
|
|
118
|
+
const verified = crypto.verify(null, Buffer.from(`${decodedGuildId}\n${timestamp}`, 'utf8'), publicKey, signature);
|
|
119
|
+
expect(verified).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
test('auth mode requires all env vars', () => {
|
|
122
|
+
process.env.KIMAKI_GUILD_ID = '1477130736841658398';
|
|
123
|
+
delete process.env.KIMAKI_PRIVATE_KEY;
|
|
124
|
+
process.env.KIMAKI_APP_ID = '1476745763009593365';
|
|
125
|
+
process.env.KIMAKI_BOT_TOKEN = 'env-token';
|
|
126
|
+
const resolved = getBotToken();
|
|
127
|
+
expect(isAuthModeEnabled()).toBe(false);
|
|
128
|
+
expect(resolved).toEqual({
|
|
129
|
+
token: 'env-token',
|
|
130
|
+
appId: undefined,
|
|
131
|
+
source: 'env',
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Discord channel and category management.
|
|
2
|
+
// Creates and manages Kimaki project channels (text + voice pairs),
|
|
3
|
+
// extracts channel metadata from topic tags, and ensures category structure.
|
|
4
|
+
import { ChannelType, } from 'discord.js';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { getChannelDirectory, setChannelDirectory } from './database.js';
|
|
7
|
+
export async function ensureKimakiCategory(guild, botName) {
|
|
8
|
+
// Skip appending bot name if it's already "kimaki" to avoid "Kimaki kimaki"
|
|
9
|
+
const isKimakiBot = botName?.toLowerCase() === 'kimaki';
|
|
10
|
+
const categoryName = botName && !isKimakiBot ? `Kimaki ${botName}` : 'Kimaki';
|
|
11
|
+
const existingCategory = guild.channels.cache.find((channel) => {
|
|
12
|
+
if (channel.type !== ChannelType.GuildCategory) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
return channel.name.toLowerCase() === categoryName.toLowerCase();
|
|
16
|
+
});
|
|
17
|
+
if (existingCategory) {
|
|
18
|
+
return existingCategory;
|
|
19
|
+
}
|
|
20
|
+
return guild.channels.create({
|
|
21
|
+
name: categoryName,
|
|
22
|
+
type: ChannelType.GuildCategory,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export async function ensureKimakiAudioCategory(guild, botName) {
|
|
26
|
+
// Skip appending bot name if it's already "kimaki" to avoid "Kimaki Audio kimaki"
|
|
27
|
+
const isKimakiBot = botName?.toLowerCase() === 'kimaki';
|
|
28
|
+
const categoryName = botName && !isKimakiBot ? `Kimaki Audio ${botName}` : 'Kimaki Audio';
|
|
29
|
+
const existingCategory = guild.channels.cache.find((channel) => {
|
|
30
|
+
if (channel.type !== ChannelType.GuildCategory) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
return channel.name.toLowerCase() === categoryName.toLowerCase();
|
|
34
|
+
});
|
|
35
|
+
if (existingCategory) {
|
|
36
|
+
return existingCategory;
|
|
37
|
+
}
|
|
38
|
+
return guild.channels.create({
|
|
39
|
+
name: categoryName,
|
|
40
|
+
type: ChannelType.GuildCategory,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
export async function createProjectChannels({ guild, projectDirectory, appId, botName, enableVoiceChannels = false, }) {
|
|
44
|
+
const baseName = path.basename(projectDirectory);
|
|
45
|
+
const channelName = `${baseName}`
|
|
46
|
+
.toLowerCase()
|
|
47
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
48
|
+
.slice(0, 100);
|
|
49
|
+
const kimakiCategory = await ensureKimakiCategory(guild, botName);
|
|
50
|
+
const textChannel = await guild.channels.create({
|
|
51
|
+
name: channelName,
|
|
52
|
+
type: ChannelType.GuildText,
|
|
53
|
+
parent: kimakiCategory,
|
|
54
|
+
// Channel configuration is stored in SQLite, not in the topic
|
|
55
|
+
});
|
|
56
|
+
await setChannelDirectory({
|
|
57
|
+
channelId: textChannel.id,
|
|
58
|
+
directory: projectDirectory,
|
|
59
|
+
channelType: 'text',
|
|
60
|
+
appId,
|
|
61
|
+
});
|
|
62
|
+
let voiceChannelId = null;
|
|
63
|
+
if (enableVoiceChannels) {
|
|
64
|
+
const kimakiAudioCategory = await ensureKimakiAudioCategory(guild, botName);
|
|
65
|
+
const voiceChannel = await guild.channels.create({
|
|
66
|
+
name: channelName,
|
|
67
|
+
type: ChannelType.GuildVoice,
|
|
68
|
+
parent: kimakiAudioCategory,
|
|
69
|
+
});
|
|
70
|
+
await setChannelDirectory({
|
|
71
|
+
channelId: voiceChannel.id,
|
|
72
|
+
directory: projectDirectory,
|
|
73
|
+
channelType: 'voice',
|
|
74
|
+
appId,
|
|
75
|
+
});
|
|
76
|
+
voiceChannelId = voiceChannel.id;
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
textChannelId: textChannel.id,
|
|
80
|
+
voiceChannelId,
|
|
81
|
+
channelName,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
export async function getChannelsWithDescriptions(guild) {
|
|
85
|
+
const channels = [];
|
|
86
|
+
const textChannels = guild.channels.cache.filter((channel) => channel.isTextBased());
|
|
87
|
+
for (const channel of textChannels.values()) {
|
|
88
|
+
const textChannel = channel;
|
|
89
|
+
const description = textChannel.topic || null;
|
|
90
|
+
// Get channel config from database instead of parsing XML from topic
|
|
91
|
+
const channelConfig = await getChannelDirectory(textChannel.id);
|
|
92
|
+
channels.push({
|
|
93
|
+
id: textChannel.id,
|
|
94
|
+
name: textChannel.name,
|
|
95
|
+
description,
|
|
96
|
+
kimakiDirectory: channelConfig?.directory,
|
|
97
|
+
kimakiApp: channelConfig?.appId || undefined,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return channels;
|
|
101
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Regression tests for CLI argument parsing around Discord ID string preservation.
|
|
2
|
+
import { describe, expect, test } from 'vitest';
|
|
3
|
+
import { goke } from 'goke';
|
|
4
|
+
function createCliForIdParsing() {
|
|
5
|
+
const cli = goke('kimaki');
|
|
6
|
+
cli
|
|
7
|
+
.command('send', 'Send a message')
|
|
8
|
+
.option('-c, --channel <channelId>', 'Discord channel ID')
|
|
9
|
+
.option('--thread <threadId>', 'Thread ID')
|
|
10
|
+
.option('--session <sessionId>', 'Session ID')
|
|
11
|
+
.option('--send-at <schedule>', 'Schedule');
|
|
12
|
+
cli.command('session archive <threadId>', 'Archive a thread');
|
|
13
|
+
cli
|
|
14
|
+
.command('session search <query>', 'Search sessions')
|
|
15
|
+
.option('--channel <channelId>', 'Discord channel ID')
|
|
16
|
+
.option('--project <path>', 'Project path');
|
|
17
|
+
cli
|
|
18
|
+
.command('add-project', 'Add a project')
|
|
19
|
+
.option('-g, --guild <guildId>', 'Discord guild/server ID');
|
|
20
|
+
cli.command('task delete <id>', 'Delete task');
|
|
21
|
+
return cli;
|
|
22
|
+
}
|
|
23
|
+
describe('goke CLI ID parsing', () => {
|
|
24
|
+
test('keeps large Discord IDs as strings', () => {
|
|
25
|
+
const cli = createCliForIdParsing();
|
|
26
|
+
const channelId = '1234567890123456789';
|
|
27
|
+
const threadId = '9876543210987654321';
|
|
28
|
+
const sessionId = '1111222233334444555';
|
|
29
|
+
const channelResult = cli.parse(['node', 'kimaki', 'send', '--channel', channelId], {
|
|
30
|
+
run: false,
|
|
31
|
+
});
|
|
32
|
+
expect(channelResult.options.channel).toBe(channelId);
|
|
33
|
+
expect(typeof channelResult.options.channel).toBe('string');
|
|
34
|
+
const threadResult = cli.parse(['node', 'kimaki', 'send', '--thread', threadId], { run: false });
|
|
35
|
+
expect(threadResult.options.thread).toBe(threadId);
|
|
36
|
+
expect(typeof threadResult.options.thread).toBe('string');
|
|
37
|
+
const sessionResult = cli.parse(['node', 'kimaki', 'send', '--session', sessionId], {
|
|
38
|
+
run: false,
|
|
39
|
+
});
|
|
40
|
+
expect(sessionResult.options.session).toBe(sessionId);
|
|
41
|
+
expect(typeof sessionResult.options.session).toBe('string');
|
|
42
|
+
});
|
|
43
|
+
test('preserves leading zeros in Discord IDs', () => {
|
|
44
|
+
const cli = createCliForIdParsing();
|
|
45
|
+
const guildId = '001230045600789';
|
|
46
|
+
const result = cli.parse(['node', 'kimaki', 'add-project', '--guild', guildId], { run: false });
|
|
47
|
+
expect(result.options.guild).toBe(guildId);
|
|
48
|
+
expect(typeof result.options.guild).toBe('string');
|
|
49
|
+
});
|
|
50
|
+
test('keeps session archive thread ID as string', () => {
|
|
51
|
+
const cli = createCliForIdParsing();
|
|
52
|
+
const threadId = '0098765432109876543';
|
|
53
|
+
const result = cli.parse(['node', 'kimaki', 'session', 'archive', threadId], {
|
|
54
|
+
run: false,
|
|
55
|
+
});
|
|
56
|
+
expect(result.args[0]).toBe(threadId);
|
|
57
|
+
expect(typeof result.args[0]).toBe('string');
|
|
58
|
+
});
|
|
59
|
+
test('keeps session search regex and channel ID as strings', () => {
|
|
60
|
+
const cli = createCliForIdParsing();
|
|
61
|
+
const channelId = '0012345678901234567';
|
|
62
|
+
const query = '/error\\s+42/i';
|
|
63
|
+
const result = cli.parse(['node', 'kimaki', 'session', 'search', query, '--channel', channelId], {
|
|
64
|
+
run: false,
|
|
65
|
+
});
|
|
66
|
+
expect(result.args[0]).toBe(query);
|
|
67
|
+
expect(typeof result.args[0]).toBe('string');
|
|
68
|
+
expect(result.options.channel).toBe(channelId);
|
|
69
|
+
expect(typeof result.options.channel).toBe('string');
|
|
70
|
+
});
|
|
71
|
+
test('keeps --send-at cron string intact', () => {
|
|
72
|
+
const cli = createCliForIdParsing();
|
|
73
|
+
const cron = '0 9 * * 1';
|
|
74
|
+
const result = cli.parse(['node', 'kimaki', 'send', '--send-at', cron], {
|
|
75
|
+
run: false,
|
|
76
|
+
});
|
|
77
|
+
expect(result.options.sendAt).toBe(cron);
|
|
78
|
+
expect(typeof result.options.sendAt).toBe('string');
|
|
79
|
+
});
|
|
80
|
+
test('keeps task delete ID as string before validation', () => {
|
|
81
|
+
const cli = createCliForIdParsing();
|
|
82
|
+
const taskId = '0012345';
|
|
83
|
+
const result = cli.parse(['node', 'kimaki', 'task', 'delete', taskId], {
|
|
84
|
+
run: false,
|
|
85
|
+
});
|
|
86
|
+
expect(result.args[0]).toBe(taskId);
|
|
87
|
+
expect(typeof result.args[0]).toBe('string');
|
|
88
|
+
});
|
|
89
|
+
});
|