kimaki 0.26.0 → 0.28.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/dist/agent-model.e2e.test.js +250 -164
- package/dist/anthropic-account-identity.js +9 -1
- package/dist/anthropic-auth-plugin.js +10 -13
- package/dist/anthropic-auth-plugin.test.js +17 -0
- package/dist/anthropic-auth-state.js +1 -0
- package/dist/bash-tool-schema-plugin.js +32 -0
- package/dist/bash-tool-schema-plugin.test.js +49 -0
- package/dist/bin.js +73 -19
- package/dist/cli-commands/maintenance.js +46 -7
- package/dist/cli-commands/project.js +11 -27
- package/dist/cli-commands/send.js +30 -1
- package/dist/cli-commands/session.js +322 -90
- package/dist/cli-commands/task.js +34 -15
- package/dist/cli-parsing.test.js +83 -4
- package/dist/cli-runner.js +44 -8
- package/dist/cli-runner.test.js +27 -0
- package/dist/cli-send-thread.e2e.test.js +9 -9
- package/dist/cli.js +32 -0
- package/dist/commands/abort.js +5 -1
- package/dist/commands/action-buttons.js +2 -1
- package/dist/commands/add-project.js +4 -7
- package/dist/commands/agent.js +164 -32
- package/dist/commands/ask-question.js +23 -33
- package/dist/commands/ask-question.test.js +43 -1
- package/dist/commands/btw.js +28 -6
- package/dist/commands/file-upload.js +2 -1
- package/dist/commands/fork-subagent.js +2 -4
- package/dist/commands/fork.js +4 -4
- package/dist/commands/last-sessions.js +17 -10
- package/dist/commands/login.js +6 -0
- package/dist/commands/merge-worktree.js +47 -14
- package/dist/commands/model-variant.js +8 -1
- package/dist/commands/model.js +16 -9
- package/dist/commands/multioauth.js +1 -0
- package/dist/commands/new-worktree.js +170 -102
- package/dist/commands/queue.js +3 -2
- package/dist/commands/resume.js +4 -4
- package/dist/commands/session.js +78 -63
- package/dist/commands/user-command.js +6 -8
- package/dist/commands/worktrees.js +5 -13
- package/dist/config.js +16 -1
- package/dist/cpu-profiler.js +211 -0
- package/dist/cpu-profiler.test.js +160 -0
- package/dist/database.js +152 -3
- package/dist/db.js +33 -0
- package/dist/db.test.js +267 -1
- package/dist/default-channel-provisioning.test.js +148 -0
- package/dist/discord-bot.js +517 -488
- package/dist/discord-command-registration.js +25 -10
- package/dist/discord-command-registration.test.js +17 -0
- package/dist/discord-utils.js +97 -5
- package/dist/discord-utils.test.js +151 -1
- package/dist/errors.js +10 -0
- package/dist/exec-async.js +17 -5
- package/dist/external-directory-permissions.e2e.test.js +11 -7
- package/dist/external-directory-project-deny.e2e.test.js +5 -3
- package/dist/external-opencode-sync.js +94 -17
- package/dist/external-opencode-sync.test.js +206 -0
- package/dist/extract-sdk-error-message.test.js +13 -0
- package/dist/file-edit-log.js +197 -0
- package/dist/file-edit-log.test.js +258 -0
- package/dist/format-tables.js +52 -0
- package/dist/format-tables.test.js +93 -0
- package/dist/gateway-proxy-reconnect.e2e.test.js +28 -8
- package/dist/gateway-proxy.e2e.test.js +18 -18
- package/dist/git-worktree-core.js +182 -17
- package/dist/interaction-handler.js +416 -398
- package/dist/ipc-tools-plugin.js +68 -3
- package/dist/kimaki-opencode-plugin-loading.e2e.test.js +122 -58
- package/dist/kimaki-opencode-plugin.js +9 -3
- package/dist/kimaki-workspace-adaptor.js +33 -10
- package/dist/logger.js +11 -1
- package/dist/markdown.js +3 -1
- package/dist/markdown.test.js +11 -3
- package/dist/message-formatting.js +181 -51
- package/dist/message-formatting.test.js +529 -22
- package/dist/message-preprocessing.js +105 -1
- package/dist/oauth-rotation-shared.js +10 -0
- package/dist/openai-auth-plugin.js +1 -0
- package/dist/openai-auth-state.js +1 -0
- package/dist/opencode-command.js +28 -0
- package/dist/opencode-command.test.js +109 -1
- package/dist/opencode-interrupt-plugin.js +4 -0
- package/dist/opencode-interrupt-plugin.test.js +24 -0
- package/dist/opencode.js +82 -15
- package/dist/project-add.test.js +62 -0
- package/dist/question-abort-clears-dropdown.e2e.test.js +89 -0
- package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +150 -0
- package/dist/question-text-before-dropdown.e2e.test.js +52 -0
- package/dist/queue-advanced-abort.e2e.test.js +8 -10
- package/dist/queue-advanced-action-buttons.e2e.test.js +8 -8
- package/dist/queue-advanced-e2e-setup.js +78 -5
- package/dist/queue-advanced-footer.e2e.test.js +60 -269
- package/dist/queue-advanced-model-switch.e2e.test.js +9 -11
- package/dist/queue-advanced-permissions-typing.e2e.test.js +15 -15
- package/dist/queue-advanced-question.e2e.test.js +32 -27
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +10 -12
- package/dist/queue-advanced-typing.e2e.test.js +7 -9
- package/dist/queue-delete-message.e2e.test.js +3 -3
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +5 -5
- package/dist/queue-interrupt-drain.e2e.test.js +122 -12
- package/dist/queue-question-select-drain.e2e.test.js +34 -132
- package/dist/runtime-lifecycle.e2e.test.js +21 -51
- package/dist/schema.js +35 -1
- package/dist/session-handler/event-stream-state.js +391 -50
- package/dist/session-handler/event-stream-state.test.js +736 -8
- package/dist/session-handler/model-utils.js +172 -5
- package/dist/session-handler/model-utils.test.js +445 -0
- package/dist/session-handler/opencode-session-event-log.js +30 -1
- package/dist/session-handler/thread-ingress.test.js +41 -0
- package/dist/session-handler/thread-runtime-state.js +2 -0
- package/dist/session-handler/thread-session-runtime-summary.test.js +121 -0
- package/dist/session-handler/thread-session-runtime.js +574 -299
- package/dist/session-search.js +123 -4
- package/dist/session-search.test.js +248 -1
- package/dist/session-sleep.test.js +224 -0
- package/dist/session-title-rename.test.js +37 -39
- package/dist/sleep.e2e.test.js +494 -0
- package/dist/startup-time.e2e.test.js +2 -1
- package/dist/store.js +3 -0
- package/dist/system-message.js +102 -16
- package/dist/system-message.test.js +192 -15
- package/dist/task-runner.js +96 -5
- package/dist/task-schedule.js +87 -1
- package/dist/task-schedule.test.js +149 -1
- package/dist/test-utils.js +17 -9
- package/dist/thinking-utils.js +10 -0
- package/dist/thinking-utils.test.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +65 -213
- package/dist/undo-redo.e2e.test.js +17 -9
- package/dist/voice-attachment.js +23 -1
- package/dist/voice-handler.js +21 -4
- package/dist/voice-message.e2e.test.js +144 -30
- package/dist/voice-routing-input.e2e.test.js +195 -0
- package/dist/voice-routing-question.e2e.test.js +125 -0
- package/dist/voice.js +28 -8
- package/dist/voice.test.js +96 -30
- package/dist/worktree-clone-isolation.e2e.test.js +188 -0
- package/dist/worktree-lifecycle.e2e.test.js +55 -36
- package/dist/worktrees.js +226 -443
- package/dist/worktrees.test.js +435 -58
- package/dist/xai-auth-plugin.js +1 -0
- package/dist/xai-auth-state.js +1 -0
- package/package.json +8 -6
- package/skills/bump-holocron-consumers/SKILL.md +137 -0
- package/skills/critique/SKILL.md +4 -4
- package/skills/errore/SKILL.md +60 -218
- package/skills/goke/SKILL.md +150 -10
- package/skills/holocron/SKILL.md +154 -17
- package/skills/npm-package/SKILL.md +55 -35
- package/skills/profano/SKILL.md +17 -2
- package/skills/spiceflow/SKILL.md +39 -3
- package/skills/strada/SKILL.md +39 -0
- package/skills/zele/SKILL.md +19 -0
- package/src/agent-model.e2e.test.ts +287 -186
- package/src/anthropic-account-identity.ts +20 -1
- package/src/anthropic-auth-plugin.test.ts +19 -0
- package/src/anthropic-auth-plugin.ts +12 -16
- package/src/anthropic-auth-state.ts +1 -0
- package/src/bash-tool-schema-plugin.test.ts +55 -0
- package/src/bash-tool-schema-plugin.ts +46 -0
- package/src/bin.ts +78 -26
- package/src/cli-commands/maintenance.ts +59 -13
- package/src/cli-commands/project.ts +14 -35
- package/src/cli-commands/send.ts +31 -1
- package/src/cli-commands/session.ts +374 -114
- package/src/cli-commands/task.ts +34 -15
- package/src/cli-parsing.test.ts +103 -4
- package/src/cli-runner.test.ts +49 -0
- package/src/cli-runner.ts +44 -7
- package/src/cli-send-thread.e2e.test.ts +9 -11
- package/src/cli.ts +53 -0
- package/src/commands/abort.ts +6 -1
- package/src/commands/action-buttons.ts +2 -1
- package/src/commands/add-project.ts +4 -8
- package/src/commands/agent.ts +205 -29
- package/src/commands/ask-question.test.ts +48 -1
- package/src/commands/ask-question.ts +23 -33
- package/src/commands/btw.ts +40 -5
- package/src/commands/file-upload.ts +2 -1
- package/src/commands/fork-subagent.ts +2 -3
- package/src/commands/fork.ts +4 -3
- package/src/commands/last-sessions.ts +10 -4
- package/src/commands/login.ts +6 -0
- package/src/commands/merge-worktree.ts +54 -16
- package/src/commands/model-variant.ts +9 -1
- package/src/commands/model.ts +20 -9
- package/src/commands/multioauth.ts +1 -0
- package/src/commands/new-worktree.ts +138 -55
- package/src/commands/queue.ts +3 -2
- package/src/commands/resume.ts +4 -3
- package/src/commands/session.ts +94 -70
- package/src/commands/user-command.ts +6 -6
- package/src/commands/worktrees.ts +5 -12
- package/src/config.ts +18 -1
- package/src/cpu-profiler.test.ts +183 -0
- package/src/cpu-profiler.ts +293 -0
- package/src/database.ts +235 -3
- package/src/db.test.ts +293 -0
- package/src/db.ts +38 -0
- package/src/default-channel-provisioning.test.ts +155 -0
- package/src/discord-bot.ts +78 -36
- package/src/discord-command-registration.test.ts +20 -0
- package/src/discord-command-registration.ts +42 -12
- package/src/discord-utils.test.ts +176 -0
- package/src/discord-utils.ts +152 -7
- package/src/errors.ts +12 -0
- package/src/exec-async.ts +20 -11
- package/src/external-directory-permissions.e2e.test.ts +11 -7
- package/src/external-directory-project-deny.e2e.test.ts +5 -3
- package/src/external-opencode-sync.test.ts +248 -0
- package/src/external-opencode-sync.ts +122 -19
- package/src/extract-sdk-error-message.test.ts +18 -0
- package/src/file-edit-log.test.ts +287 -0
- package/src/file-edit-log.ts +283 -0
- package/src/format-tables.test.ts +101 -0
- package/src/format-tables.ts +75 -0
- package/src/gateway-proxy-reconnect.e2e.test.ts +38 -8
- package/src/gateway-proxy.e2e.test.ts +18 -17
- package/src/git-worktree-core.ts +247 -20
- package/src/interaction-handler.ts +36 -13
- package/src/ipc-tools-plugin.ts +82 -5
- package/src/kimaki-opencode-plugin-loading.e2e.test.ts +188 -97
- package/src/kimaki-opencode-plugin.ts +9 -3
- package/src/kimaki-workspace-adaptor.ts +40 -11
- package/src/logger.ts +14 -4
- package/src/markdown.test.ts +13 -3
- package/src/markdown.ts +4 -1
- package/src/message-formatting.test.ts +672 -25
- package/src/message-formatting.ts +242 -53
- package/src/message-preprocessing.ts +116 -1
- package/src/oauth-rotation-shared.ts +10 -0
- package/src/openai-auth-plugin.ts +1 -0
- package/src/openai-auth-state.ts +1 -0
- package/src/opencode-command.test.ts +140 -0
- package/src/opencode-command.ts +38 -0
- package/src/opencode-interrupt-plugin.test.ts +32 -0
- package/src/opencode-interrupt-plugin.ts +3 -0
- package/src/opencode.ts +115 -15
- package/src/project-add.test.ts +80 -0
- package/src/question-abort-clears-dropdown.e2e.test.ts +109 -0
- package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +172 -0
- package/src/question-text-before-dropdown.e2e.test.ts +67 -0
- package/src/queue-advanced-abort.e2e.test.ts +8 -9
- package/src/queue-advanced-action-buttons.e2e.test.ts +8 -8
- package/src/queue-advanced-e2e-setup.ts +83 -4
- package/src/queue-advanced-footer.e2e.test.ts +67 -315
- package/src/queue-advanced-model-switch.e2e.test.ts +9 -10
- package/src/queue-advanced-permissions-typing.e2e.test.ts +15 -15
- package/src/queue-advanced-question.e2e.test.ts +36 -30
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +11 -11
- package/src/queue-advanced-typing.e2e.test.ts +8 -8
- package/src/queue-delete-message.e2e.test.ts +3 -3
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +5 -5
- package/src/queue-interrupt-drain.e2e.test.ts +133 -12
- package/src/queue-question-select-drain.e2e.test.ts +36 -161
- package/src/runtime-lifecycle.e2e.test.ts +21 -54
- package/src/schema.sql +15 -2
- package/src/schema.ts +37 -1
- package/src/session-handler/event-stream-state.test.ts +790 -7
- package/src/session-handler/event-stream-state.ts +568 -53
- package/src/session-handler/model-utils.test.ts +537 -0
- package/src/session-handler/model-utils.ts +281 -5
- package/src/session-handler/opencode-session-event-log.ts +31 -1
- package/src/session-handler/thread-ingress.test.ts +54 -0
- package/src/session-handler/thread-runtime-state.ts +3 -0
- package/src/session-handler/thread-session-runtime-summary.test.ts +169 -0
- package/src/session-handler/thread-session-runtime.ts +712 -358
- package/src/session-search.test.ts +281 -0
- package/src/session-search.ts +241 -7
- package/src/session-sleep.test.ts +294 -0
- package/src/session-title-rename.test.ts +54 -48
- package/src/sleep.e2e.test.ts +565 -0
- package/src/startup-time.e2e.test.ts +5 -1
- package/src/store.ts +22 -0
- package/src/system-message.test.ts +226 -15
- package/src/system-message.ts +130 -16
- package/src/task-runner.ts +138 -3
- package/src/task-schedule.test.ts +170 -0
- package/src/task-schedule.ts +141 -1
- package/src/test-utils.ts +21 -10
- package/src/thinking-utils.test.ts +46 -0
- package/src/thinking-utils.ts +21 -0
- package/src/thread-message-queue.e2e.test.ts +65 -245
- package/src/undo-redo.e2e.test.ts +17 -9
- package/src/voice-attachment.ts +29 -1
- package/src/voice-handler.ts +23 -4
- package/src/voice-message.e2e.test.ts +149 -29
- package/src/voice-routing-input.e2e.test.ts +203 -0
- package/src/voice-routing-question.e2e.test.ts +131 -0
- package/src/voice.test.ts +107 -29
- package/src/voice.ts +34 -6
- package/src/worktree-clone-isolation.e2e.test.ts +213 -0
- package/src/worktree-lifecycle.e2e.test.ts +55 -35
- package/src/worktrees.test.ts +505 -64
- package/src/worktrees.ts +257 -603
- package/src/xai-auth-plugin.ts +1 -0
- package/src/xai-auth-state.ts +1 -0
- package/dist/message-finish-field.e2e.test.js +0 -169
- package/dist/subagent-rate-limit-plugin.js +0 -184
- package/src/message-finish-field.e2e.test.ts +0 -196
- package/src/subagent-rate-limit-plugin.ts +0 -228
|
@@ -1,24 +1,109 @@
|
|
|
1
1
|
// Message pre-processing pipeline for incoming Discord messages.
|
|
2
2
|
// Extracts prompt text, voice transcription, file/text attachments, and
|
|
3
3
|
// session context from a Discord Message before handing off to the runtime.
|
|
4
|
+
// Voice side/fresh-chat requests dispatch to separate threads instead.
|
|
4
5
|
//
|
|
5
6
|
// This module exists so discord-bot.ts stays a thin event router and the
|
|
6
7
|
// expensive async work (voice transcription, context fetch, attachment
|
|
7
8
|
// download) runs inside the runtime's serialized preprocessChain —
|
|
8
9
|
// preserving arrival order without a separate threadIngressQueue.
|
|
9
10
|
import { DiscordOperationError, OpenCodeSdkError } from './errors.js';
|
|
11
|
+
import { getOrCreateRuntime } from './session-handler/thread-session-runtime.js';
|
|
10
12
|
import { resolveMentions, getFileAttachments, getTextAttachments, } from './message-formatting.js';
|
|
11
13
|
import { processVoiceAttachment } from './voice-handler.js';
|
|
12
14
|
import { isVoiceAttachment } from './voice-attachment.js';
|
|
13
15
|
import { initializeOpencodeForDirectory } from './opencode.js';
|
|
14
16
|
import { getCompactSessionContext, getLastSessionId } from './markdown.js';
|
|
15
|
-
import { getThreadSession } from './database.js';
|
|
17
|
+
import { getThreadSession, getThreadWorktreeOrWorkspace } from './database.js';
|
|
18
|
+
import { resolveWorkingDirectory, resolveTextChannel, sendThreadMessage } from './discord-utils.js';
|
|
19
|
+
import { forkSessionToBtwThread } from './commands/btw.js';
|
|
20
|
+
import { createNewSessionThread } from './commands/session.js';
|
|
16
21
|
import * as errore from 'errore';
|
|
17
22
|
import { createLogger, LogPrefix } from './logger.js';
|
|
18
23
|
import { notifyError } from './sentry.js';
|
|
19
24
|
const logger = createLogger(LogPrefix.SESSION);
|
|
20
25
|
const voiceLogger = createLogger(LogPrefix.VOICE);
|
|
21
26
|
export const VOICE_MESSAGE_TRANSCRIPTION_PREFIX = 'Voice message transcription from Discord user:\n';
|
|
27
|
+
// Dispatch before returning a prompt so the source runtime never receives the side request.
|
|
28
|
+
async function routeVoiceSession({ voiceResult, message, thread, appId, }) {
|
|
29
|
+
if (!voiceResult?.sessionAction)
|
|
30
|
+
return false;
|
|
31
|
+
const resolved = await resolveWorkingDirectory({ channel: thread });
|
|
32
|
+
if (!resolved) {
|
|
33
|
+
await sendThreadMessage(thread, 'Could not determine project directory. Use /add-project to configure it, then retry.');
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
const images = await getFileAttachments(message);
|
|
37
|
+
const textAttachments = await getTextAttachments(message);
|
|
38
|
+
const caption = resolveMentions(message).trim();
|
|
39
|
+
const prompt = [caption, voiceResult.transcription, textAttachments]
|
|
40
|
+
.filter((part) => part?.trim())
|
|
41
|
+
.join('\n\n');
|
|
42
|
+
const modelPrompt = [caption, `${VOICE_MESSAGE_TRANSCRIPTION_PREFIX}${voiceResult.transcription}`, textAttachments]
|
|
43
|
+
.filter((part) => part?.trim())
|
|
44
|
+
.join('\n\n');
|
|
45
|
+
if (voiceResult.sessionAction === 'btw') {
|
|
46
|
+
const result = await forkSessionToBtwThread({
|
|
47
|
+
sourceThread: thread,
|
|
48
|
+
projectDirectory: resolved.projectDirectory,
|
|
49
|
+
sdkDirectory: resolved.workingDirectory,
|
|
50
|
+
prompt,
|
|
51
|
+
modelPrompt,
|
|
52
|
+
images,
|
|
53
|
+
agent: voiceResult.agent,
|
|
54
|
+
userId: message.author.id,
|
|
55
|
+
username: message.member?.displayName || message.author.displayName,
|
|
56
|
+
appId,
|
|
57
|
+
})
|
|
58
|
+
.catch((cause) => new DiscordOperationError({ operation: 'forkVoiceSession', cause }));
|
|
59
|
+
if (result instanceof Error) {
|
|
60
|
+
logger.error('Voice side chat failed:', result);
|
|
61
|
+
await sendThreadMessage(thread, `${result.message}. Start a session first, then retry with /btw.`);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
await sendThreadMessage(thread, `Session forked! Continue in ${result.thread.toString()}`);
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
const textChannel = await resolveTextChannel(thread);
|
|
68
|
+
if (!textChannel) {
|
|
69
|
+
await sendThreadMessage(thread, 'Could not resolve parent text channel. Retry /new-session in the project channel.');
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
const result = await createNewSessionThread({
|
|
73
|
+
textChannel,
|
|
74
|
+
projectDirectory: resolved.projectDirectory,
|
|
75
|
+
prompt,
|
|
76
|
+
userId: message.author.id,
|
|
77
|
+
sourceWorkspace: await getThreadWorktreeOrWorkspace(thread.id),
|
|
78
|
+
}).catch((cause) => new DiscordOperationError({ operation: 'createVoiceSessionThread', cause }));
|
|
79
|
+
if (result instanceof Error) {
|
|
80
|
+
logger.error('Voice new session failed:', result);
|
|
81
|
+
await sendThreadMessage(thread, `${result.message}. Retry with /new-session.`);
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
await sendThreadMessage(thread, `Created new session in ${result.toString()}`);
|
|
85
|
+
const runtime = getOrCreateRuntime({
|
|
86
|
+
threadId: result.id,
|
|
87
|
+
thread: result,
|
|
88
|
+
projectDirectory: resolved.projectDirectory,
|
|
89
|
+
sdkDirectory: resolved.workingDirectory,
|
|
90
|
+
channelId: textChannel.id,
|
|
91
|
+
appId,
|
|
92
|
+
});
|
|
93
|
+
await runtime.enqueueIncoming({
|
|
94
|
+
prompt: modelPrompt,
|
|
95
|
+
images,
|
|
96
|
+
agent: voiceResult.agent,
|
|
97
|
+
userId: message.author.id,
|
|
98
|
+
username: message.member?.displayName || message.author.displayName,
|
|
99
|
+
appId,
|
|
100
|
+
mode: 'opencode',
|
|
101
|
+
}).catch(async (error) => {
|
|
102
|
+
logger.error('Voice session dispatch failed:', error);
|
|
103
|
+
await sendThreadMessage(result, 'Could not send the request to OpenCode. Send your request again in this thread.');
|
|
104
|
+
});
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
22
107
|
/** Fetch available agents from OpenCode for voice transcription agent selection. */
|
|
23
108
|
async function fetchAvailableAgents(getClient, directory) {
|
|
24
109
|
if (getClient instanceof Error) {
|
|
@@ -170,7 +255,15 @@ export async function preprocessExistingThreadMessage({ message, thread, project
|
|
|
170
255
|
currentSessionContext,
|
|
171
256
|
lastSessionContext,
|
|
172
257
|
agents,
|
|
258
|
+
canForkSession: true,
|
|
173
259
|
});
|
|
260
|
+
if (voiceResult instanceof Error) {
|
|
261
|
+
voiceLogger.warn('Voice request not dispatched:', voiceResult);
|
|
262
|
+
return { prompt: '', mode: 'opencode', skip: true };
|
|
263
|
+
}
|
|
264
|
+
if (await routeVoiceSession({ voiceResult, message, thread, appId })) {
|
|
265
|
+
return { prompt: '', mode: 'opencode', skip: true };
|
|
266
|
+
}
|
|
174
267
|
if (voiceResult) {
|
|
175
268
|
messageContent = `${VOICE_MESSAGE_TRANSCRIPTION_PREFIX}${voiceResult.transcription}`;
|
|
176
269
|
}
|
|
@@ -231,6 +324,13 @@ export async function preprocessNewSessionMessage({ message, thread, projectDire
|
|
|
231
324
|
appId,
|
|
232
325
|
agents,
|
|
233
326
|
});
|
|
327
|
+
if (voiceResult instanceof Error) {
|
|
328
|
+
voiceLogger.warn('Voice request not dispatched:', voiceResult);
|
|
329
|
+
return { prompt: '', mode: 'opencode', skip: true };
|
|
330
|
+
}
|
|
331
|
+
if (await routeVoiceSession({ voiceResult, message, thread, appId })) {
|
|
332
|
+
return { prompt: '', mode: 'opencode', skip: true };
|
|
333
|
+
}
|
|
234
334
|
if (voiceResult) {
|
|
235
335
|
prompt = `${VOICE_MESSAGE_TRANSCRIPTION_PREFIX}${voiceResult.transcription}`;
|
|
236
336
|
}
|
|
@@ -304,6 +404,10 @@ export async function preprocessNewThreadMessage({ message, thread, projectDirec
|
|
|
304
404
|
appId,
|
|
305
405
|
agents,
|
|
306
406
|
});
|
|
407
|
+
if (voiceResult instanceof Error) {
|
|
408
|
+
voiceLogger.warn('Voice request not dispatched:', voiceResult);
|
|
409
|
+
return { prompt: '', mode: 'opencode', skip: true };
|
|
410
|
+
}
|
|
307
411
|
if (voiceResult) {
|
|
308
412
|
messageContent = `${VOICE_MESSAGE_TRANSCRIPTION_PREFIX}${voiceResult.transcription}`;
|
|
309
413
|
}
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LEGACY. Superseded by @subrouter/opencode, registered alongside the kimaki
|
|
3
|
+
* plugin in opencode.ts. This whole rotation path only cycles accounts inside
|
|
4
|
+
* one provider; subrouter also fails over to a different provider when every
|
|
5
|
+
* account of one is exhausted.
|
|
6
|
+
*
|
|
7
|
+
* Kept because opencode ships no Claude Pro/Max auth of its own, so deleting
|
|
8
|
+
* it would strand anyone still picking `anthropic/*` directly. To move to
|
|
9
|
+
* subrouter, log in again through /login and pick Subrouter.
|
|
10
|
+
*/
|
|
1
11
|
/**
|
|
2
12
|
* Shared utilities for multi-provider OAuth account rotation.
|
|
3
13
|
* Used by both anthropic-auth-state.ts and openai-auth-state.ts to avoid
|
package/dist/opencode-command.js
CHANGED
|
@@ -4,7 +4,35 @@
|
|
|
4
4
|
// and creates a stable `kimaki` shim for OpenCode child processes.
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
+
import { OpencodeIncompatibleVersionError } from './errors.js';
|
|
7
8
|
const WINDOWS_CMD_SHIM_REGEX = /\.(cmd|bat)$/i;
|
|
9
|
+
const OPENCODE_SEMVER_REGEX = /(\d+)\.(\d+)\.(\d+)/;
|
|
10
|
+
// Kimaki still talks to OpenCode 1.x. Flip this to 1 after the v2 port ships.
|
|
11
|
+
export const INCOMPATIBLE_OPENCODE_MAJOR_VERSION = 2;
|
|
12
|
+
export function parseOpencodeVersion(output) {
|
|
13
|
+
const match = output.match(OPENCODE_SEMVER_REGEX);
|
|
14
|
+
if (!match)
|
|
15
|
+
return null;
|
|
16
|
+
const raw = match[0];
|
|
17
|
+
const major = Number(match[1]);
|
|
18
|
+
const minor = Number(match[2]);
|
|
19
|
+
const patch = Number(match[3]);
|
|
20
|
+
if (!Number.isInteger(major) || !Number.isInteger(minor) || !Number.isInteger(patch)) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
return { major, minor, patch, raw };
|
|
24
|
+
}
|
|
25
|
+
export function isIncompatibleOpencodeMajor({ major, }) {
|
|
26
|
+
return major === INCOMPATIBLE_OPENCODE_MAJOR_VERSION;
|
|
27
|
+
}
|
|
28
|
+
export function getIncompatibleOpencodeVersionError(output) {
|
|
29
|
+
const parsed = parseOpencodeVersion(output);
|
|
30
|
+
if (!parsed)
|
|
31
|
+
return null;
|
|
32
|
+
if (!isIncompatibleOpencodeMajor(parsed))
|
|
33
|
+
return null;
|
|
34
|
+
return new OpencodeIncompatibleVersionError({ version: parsed.raw });
|
|
35
|
+
}
|
|
8
36
|
function quotePosixShellSegment(value) {
|
|
9
37
|
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
10
38
|
}
|
|
@@ -1,9 +1,58 @@
|
|
|
1
1
|
// Regression tests for Windows OpenCode command resolution and spawn args.
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
3
4
|
import os from 'node:os';
|
|
4
5
|
import path from 'node:path';
|
|
5
6
|
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
|
6
|
-
import { ensureKimakiCommandShim, getSpawnCommandAndArgs, sanitizeShimExecArgv, selectResolvedCommand, splitCommandLookupOutput, } from './opencode-command.js';
|
|
7
|
+
import { ensureKimakiCommandShim, getIncompatibleOpencodeVersionError, getSpawnCommandAndArgs, INCOMPATIBLE_OPENCODE_MAJOR_VERSION, isIncompatibleOpencodeMajor, parseOpencodeVersion, sanitizeShimExecArgv, selectResolvedCommand, splitCommandLookupOutput, } from './opencode-command.js';
|
|
8
|
+
import { OpencodeIncompatibleVersionError } from './errors.js';
|
|
9
|
+
describe('parseOpencodeVersion', () => {
|
|
10
|
+
test('extracts major.minor.patch from opencode --version output', () => {
|
|
11
|
+
expect(parseOpencodeVersion('1.2.3')).toEqual({
|
|
12
|
+
major: 1,
|
|
13
|
+
minor: 2,
|
|
14
|
+
patch: 3,
|
|
15
|
+
raw: '1.2.3',
|
|
16
|
+
});
|
|
17
|
+
expect(parseOpencodeVersion('opencode 2.0.0\n')).toEqual({
|
|
18
|
+
major: 2,
|
|
19
|
+
minor: 0,
|
|
20
|
+
patch: 0,
|
|
21
|
+
raw: '2.0.0',
|
|
22
|
+
});
|
|
23
|
+
expect(parseOpencodeVersion('v2.1.0-beta.1')).toEqual({
|
|
24
|
+
major: 2,
|
|
25
|
+
minor: 1,
|
|
26
|
+
patch: 0,
|
|
27
|
+
raw: '2.1.0',
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
test('returns null when output has no three-part version', () => {
|
|
31
|
+
expect(parseOpencodeVersion('')).toBeNull();
|
|
32
|
+
expect(parseOpencodeVersion('opencode')).toBeNull();
|
|
33
|
+
expect(parseOpencodeVersion('2.0')).toBeNull();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe('isIncompatibleOpencodeMajor', () => {
|
|
37
|
+
test('rejects the incompatible major and allows every other major', () => {
|
|
38
|
+
expect(INCOMPATIBLE_OPENCODE_MAJOR_VERSION).toBe(2);
|
|
39
|
+
expect(isIncompatibleOpencodeMajor({ major: 2 })).toBe(true);
|
|
40
|
+
expect(isIncompatibleOpencodeMajor({ major: 1 })).toBe(false);
|
|
41
|
+
expect(isIncompatibleOpencodeMajor({ major: 0 })).toBe(false);
|
|
42
|
+
expect(isIncompatibleOpencodeMajor({ major: 3 })).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
describe('getIncompatibleOpencodeVersionError', () => {
|
|
46
|
+
test('returns a tagged error for OpenCode 2.x', () => {
|
|
47
|
+
const error = getIncompatibleOpencodeVersionError('2.0.0');
|
|
48
|
+
expect(error).toBeInstanceOf(OpencodeIncompatibleVersionError);
|
|
49
|
+
expect(error?.message).toMatchInlineSnapshot(`"Kimaki is not compatible with OpenCode version 2.0.0. Install an OpenCode 1.x release."`);
|
|
50
|
+
});
|
|
51
|
+
test('allows 1.x and unparseable output', () => {
|
|
52
|
+
expect(getIncompatibleOpencodeVersionError('1.4.0')).toBeNull();
|
|
53
|
+
expect(getIncompatibleOpencodeVersionError('opencode')).toBeNull();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
7
56
|
describe('splitCommandLookupOutput', () => {
|
|
8
57
|
test('splits windows command lookup output into trimmed lines', () => {
|
|
9
58
|
expect(splitCommandLookupOutput('C:\\Program Files\\nodejs\\opencode\r\nC:\\Program Files\\nodejs\\opencode.cmd\r\n')).toEqual([
|
|
@@ -26,6 +75,65 @@ describe('selectResolvedCommand', () => {
|
|
|
26
75
|
})).toBe('/usr/local/bin/opencode');
|
|
27
76
|
});
|
|
28
77
|
});
|
|
78
|
+
describe('buildOpencodeServeArgs', () => {
|
|
79
|
+
test('always passes --hostname so opencode.json cannot bind 0.0.0.0', async () => {
|
|
80
|
+
const { buildOpencodeServeArgs } = await import('./opencode.js');
|
|
81
|
+
expect(buildOpencodeServeArgs({ port: 4096 })).toEqual([
|
|
82
|
+
'serve',
|
|
83
|
+
'--port',
|
|
84
|
+
'4096',
|
|
85
|
+
'--hostname',
|
|
86
|
+
'127.0.0.1',
|
|
87
|
+
'--print-logs',
|
|
88
|
+
'--log-level',
|
|
89
|
+
'WARN',
|
|
90
|
+
]);
|
|
91
|
+
});
|
|
92
|
+
test('passes --hostname when set', async () => {
|
|
93
|
+
const { buildOpencodeServeArgs } = await import('./opencode.js');
|
|
94
|
+
expect(buildOpencodeServeArgs({ port: 4096, hostname: '0.0.0.0' })).toEqual([
|
|
95
|
+
'serve',
|
|
96
|
+
'--port',
|
|
97
|
+
'4096',
|
|
98
|
+
'--hostname',
|
|
99
|
+
'0.0.0.0',
|
|
100
|
+
'--print-logs',
|
|
101
|
+
'--log-level',
|
|
102
|
+
'WARN',
|
|
103
|
+
]);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
describe('published runtime artifacts', () => {
|
|
107
|
+
test('lists @subrouter/opencode as a runtime dependency', () => {
|
|
108
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(import.meta.dirname, '../package.json'), 'utf8'));
|
|
109
|
+
expect(pkg.dependencies?.['@subrouter/opencode']).toMatch(/^(workspace:\^|\^)/);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
describe('resolveSubrouterPluginSpec', () => {
|
|
113
|
+
test('uses npm package identity in production for OpenCode deduplication', async () => {
|
|
114
|
+
const { resolveSubrouterPluginSpec } = await import('./opencode.js');
|
|
115
|
+
const require = createRequire(import.meta.url);
|
|
116
|
+
const packageJsonPath = require.resolve('@subrouter/opencode/package.json');
|
|
117
|
+
const version = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')).version;
|
|
118
|
+
expect(resolveSubrouterPluginSpec({ isDev: false })).toBe(`@subrouter/opencode@${version}`);
|
|
119
|
+
});
|
|
120
|
+
test('loads workspace source directly in development', async () => {
|
|
121
|
+
const { resolveSubrouterPluginSpec } = await import('./opencode.js');
|
|
122
|
+
expect(resolveSubrouterPluginSpec({ isDev: true })).toMatch(/^file:.*\/subrouter\/opencode\/dist\/index\.js$/);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
describe('publicOpencodeBindRequiresPassword', () => {
|
|
126
|
+
test('allows loopback without a password', async () => {
|
|
127
|
+
const { publicOpencodeBindRequiresPassword } = await import('./opencode.js');
|
|
128
|
+
expect(publicOpencodeBindRequiresPassword({ hostname: null })).toBe(false);
|
|
129
|
+
expect(publicOpencodeBindRequiresPassword({ hostname: '127.0.0.1' })).toBe(false);
|
|
130
|
+
expect(publicOpencodeBindRequiresPassword({ hostname: 'localhost' })).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
test('requires a password for 0.0.0.0', async () => {
|
|
133
|
+
const { publicOpencodeBindRequiresPassword } = await import('./opencode.js');
|
|
134
|
+
expect(publicOpencodeBindRequiresPassword({ hostname: '0.0.0.0' })).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
29
137
|
describe('getSpawnCommandAndArgs', () => {
|
|
30
138
|
test('wraps windows cmd shims through cmd.exe without double-quoting by node', () => {
|
|
31
139
|
expect(getSpawnCommandAndArgs({
|
|
@@ -216,6 +216,10 @@ const interruptOpencodeSessionOnUserMessage = async (ctx) => {
|
|
|
216
216
|
return;
|
|
217
217
|
if (output.parts.length === 0)
|
|
218
218
|
return;
|
|
219
|
+
// Hidden context notices are not user turns. Subrouter posts these while
|
|
220
|
+
// a session is already busy; aborting would cancel running subagents.
|
|
221
|
+
if (output.parts.every((part) => part.type === 'text' && part.ignored === true))
|
|
222
|
+
return;
|
|
219
223
|
const messageID = input.messageID || output.message.id;
|
|
220
224
|
if (!messageID)
|
|
221
225
|
return;
|
|
@@ -205,6 +205,30 @@ describe('interruptOpencodeSessionOnUserMessage', () => {
|
|
|
205
205
|
expect(stub.abortCalls).toEqual([]);
|
|
206
206
|
expect(stub.promptAsyncCalls).toEqual([]);
|
|
207
207
|
});
|
|
208
|
+
test('ignored context notices do not abort a busy session', async () => {
|
|
209
|
+
process.env['KIMAKI_INTERRUPT_STEP_TIMEOUT_MS'] = '20';
|
|
210
|
+
const { chatHook } = await requireHooks();
|
|
211
|
+
const sessionID = 'ses-oracle';
|
|
212
|
+
const messageID = 'msg-fallback-notice';
|
|
213
|
+
stub.setStatus(sessionID, { type: 'busy' });
|
|
214
|
+
await chatHook({ sessionID, messageID }, createChatOutput({
|
|
215
|
+
sessionID,
|
|
216
|
+
messageID,
|
|
217
|
+
parts: [
|
|
218
|
+
{
|
|
219
|
+
id: 'prt-fallback',
|
|
220
|
+
sessionID,
|
|
221
|
+
messageID,
|
|
222
|
+
type: 'text',
|
|
223
|
+
text: 'Subrouter: Using xai/grok-4.6 because openai/gpt-5.6-sol is rate limited.',
|
|
224
|
+
ignored: true,
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
}));
|
|
228
|
+
await delay({ ms: 80 });
|
|
229
|
+
expect(stub.abortCalls).toEqual([]);
|
|
230
|
+
expect(stub.promptAsyncCalls).toEqual([]);
|
|
231
|
+
});
|
|
208
232
|
test('replayed message does not schedule another interrupt (no abort loop)', async () => {
|
|
209
233
|
process.env['KIMAKI_INTERRUPT_STEP_TIMEOUT_MS'] = '20';
|
|
210
234
|
const { chatHook } = await requireHooks();
|
package/dist/opencode.js
CHANGED
|
@@ -40,18 +40,32 @@ import net from 'node:net';
|
|
|
40
40
|
import os from 'node:os';
|
|
41
41
|
import path from 'node:path';
|
|
42
42
|
import readline from 'node:readline';
|
|
43
|
-
import {
|
|
43
|
+
import { createRequire } from 'node:module';
|
|
44
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
44
45
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
45
46
|
import { createOpencodeClient, } from '@opencode-ai/sdk/v2';
|
|
46
47
|
import { restartGlobalEventListener, waitForGlobalEventListener, } from './session-handler/global-event-listener.js';
|
|
47
|
-
import { getDataDir, getLockPort, getRestrictExternalDirectories, } from './config.js';
|
|
48
|
+
import { getDataDir, getLockPort, getRestrictExternalDirectories, getOpencodeHostname, getOpencodePort, } from './config.js';
|
|
48
49
|
import { store } from './store.js';
|
|
49
50
|
import { getHranaUrl } from './hrana-server.js';
|
|
51
|
+
export function resolveSubrouterPluginSpec({ isDev }) {
|
|
52
|
+
const require = createRequire(import.meta.url);
|
|
53
|
+
const entry = require.resolve('@subrouter/opencode');
|
|
54
|
+
if (isDev)
|
|
55
|
+
return pathToFileURL(entry).href;
|
|
56
|
+
const packageJsonPath = require.resolve('@subrouter/opencode/package.json');
|
|
57
|
+
const version = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')).version;
|
|
58
|
+
if (typeof version !== 'string' || !version) {
|
|
59
|
+
throw new Error(`Missing @subrouter/opencode version in ${packageJsonPath}`);
|
|
60
|
+
}
|
|
61
|
+
return `@subrouter/opencode@${version}`;
|
|
62
|
+
}
|
|
50
63
|
import * as errore from 'errore';
|
|
51
64
|
import { createLogger, LogPrefix } from './logger.js';
|
|
52
65
|
import { notifyError } from './sentry.js';
|
|
53
|
-
import { DirectoryNotAccessibleError, ServerStartError, ServerNotReadyError, FetchError, } from './errors.js';
|
|
54
|
-
import { ensureKimakiCommandShim, getPathEnvKey, getSpawnCommandAndArgs, prependPathEntry, selectResolvedCommand, } from './opencode-command.js';
|
|
66
|
+
import { DirectoryNotAccessibleError, ServerStartError, ServerNotReadyError, FetchError, OpencodeIncompatibleVersionError, } from './errors.js';
|
|
67
|
+
import { ensureKimakiCommandShim, getIncompatibleOpencodeVersionError, getPathEnvKey, getSpawnCommandAndArgs, prependPathEntry, selectResolvedCommand, } from './opencode-command.js';
|
|
68
|
+
import { execAsync } from './exec-async.js';
|
|
55
69
|
import { computeSkillPermission } from './skill-filter.js';
|
|
56
70
|
const opencodeLogger = createLogger(LogPrefix.OPENCODE);
|
|
57
71
|
/**
|
|
@@ -66,6 +80,26 @@ export function getOpencodeServerAuthHeaders() {
|
|
|
66
80
|
const encoded = Buffer.from(`${username}:${serverPassword}`).toString('base64');
|
|
67
81
|
return { Authorization: `Basic ${encoded}` };
|
|
68
82
|
}
|
|
83
|
+
const LOOPBACK_HOSTNAMES = new Set(['127.0.0.1', 'localhost', '::1']);
|
|
84
|
+
export function publicOpencodeBindRequiresPassword({ hostname, }) {
|
|
85
|
+
if (!hostname)
|
|
86
|
+
return false;
|
|
87
|
+
return !LOOPBACK_HOSTNAMES.has(hostname);
|
|
88
|
+
}
|
|
89
|
+
// Always pass --hostname so opencode.json server.hostname / mdns cannot bind 0.0.0.0.
|
|
90
|
+
const DEFAULT_OPENCODE_HOSTNAME = '127.0.0.1';
|
|
91
|
+
export function buildOpencodeServeArgs({ port, hostname, }) {
|
|
92
|
+
return [
|
|
93
|
+
'serve',
|
|
94
|
+
'--port',
|
|
95
|
+
port.toString(),
|
|
96
|
+
'--hostname',
|
|
97
|
+
hostname || DEFAULT_OPENCODE_HOSTNAME,
|
|
98
|
+
'--print-logs',
|
|
99
|
+
'--log-level',
|
|
100
|
+
'WARN',
|
|
101
|
+
];
|
|
102
|
+
}
|
|
69
103
|
// Tracks directories that have been initialized, to avoid repeated log spam
|
|
70
104
|
// from the external sync polling loop.
|
|
71
105
|
const initializedDirectories = new Set();
|
|
@@ -89,6 +123,7 @@ export async function requestHealthcheck({ url, timeoutMs = 2000, }) {
|
|
|
89
123
|
method: 'GET',
|
|
90
124
|
headers: {
|
|
91
125
|
connection: 'close',
|
|
126
|
+
...getOpencodeServerAuthHeaders(),
|
|
92
127
|
},
|
|
93
128
|
}, (res) => {
|
|
94
129
|
const chunks = [];
|
|
@@ -337,6 +372,23 @@ export function resolveOpencodeCommand() {
|
|
|
337
372
|
opencodeLogger.log(`Resolved opencode binary: ${result}`);
|
|
338
373
|
return result;
|
|
339
374
|
}
|
|
375
|
+
export async function assertCompatibleOpencodeVersion({ resolvedCommand = resolveOpencodeCommand(), } = {}) {
|
|
376
|
+
const { command, args } = getSpawnCommandAndArgs({
|
|
377
|
+
resolvedCommand,
|
|
378
|
+
baseArgs: ['--version'],
|
|
379
|
+
});
|
|
380
|
+
const result = await execAsync({ command, args }, { timeout: 5000, encoding: 'utf8' }).catch((cause) => {
|
|
381
|
+
return new Error('Failed to read OpenCode version', { cause });
|
|
382
|
+
});
|
|
383
|
+
if (result instanceof Error) {
|
|
384
|
+
opencodeLogger.warn(result.message);
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
const incompatible = getIncompatibleOpencodeVersionError(`${result.stdout}\n${result.stderr}`);
|
|
388
|
+
if (incompatible)
|
|
389
|
+
return incompatible;
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
340
392
|
async function getOpenPort() {
|
|
341
393
|
return new Promise((resolve, reject) => {
|
|
342
394
|
const server = net.createServer();
|
|
@@ -462,6 +514,9 @@ async function ensureSingleServer({ directory, } = {}) {
|
|
|
462
514
|
singleServer = discovered;
|
|
463
515
|
return discovered;
|
|
464
516
|
}
|
|
517
|
+
const compatibility = await assertCompatibleOpencodeVersion();
|
|
518
|
+
if (compatibility instanceof Error)
|
|
519
|
+
return compatibility;
|
|
465
520
|
return startSingleServer({ directory: startupDirectory });
|
|
466
521
|
})();
|
|
467
522
|
try {
|
|
@@ -473,15 +528,17 @@ async function ensureSingleServer({ directory, } = {}) {
|
|
|
473
528
|
}
|
|
474
529
|
async function startSingleServer({ directory, } = {}) {
|
|
475
530
|
ensureProcessCleanupHandlersRegistered();
|
|
476
|
-
const
|
|
477
|
-
const
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
531
|
+
const configuredPort = getOpencodePort();
|
|
532
|
+
const port = configuredPort ?? (await getOpenPort());
|
|
533
|
+
const hostname = getOpencodeHostname() ?? DEFAULT_OPENCODE_HOSTNAME;
|
|
534
|
+
if (publicOpencodeBindRequiresPassword({ hostname }) &&
|
|
535
|
+
!process.env.OPENCODE_SERVER_PASSWORD) {
|
|
536
|
+
return new ServerStartError({
|
|
537
|
+
port,
|
|
538
|
+
reason: `OPENCODE_SERVER_PASSWORD is required when --opencode-hostname is ${hostname}`,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
const serveArgs = buildOpencodeServeArgs({ port, hostname });
|
|
485
542
|
const { command: spawnCommand, args: spawnArgs, windowsVerbatimArguments, } = getSpawnCommandAndArgs({
|
|
486
543
|
resolvedCommand: resolveOpencodeCommand(),
|
|
487
544
|
baseArgs: serveArgs,
|
|
@@ -552,6 +609,9 @@ async function startSingleServer({ directory, } = {}) {
|
|
|
552
609
|
formatter: false,
|
|
553
610
|
plugin: [
|
|
554
611
|
new URL(isDev ? './kimaki-opencode-plugin.ts' : './kimaki-opencode-plugin.js', import.meta.url).href,
|
|
612
|
+
// npm identity lets opencode dedupe a user-installed copy by package
|
|
613
|
+
// name. Development still loads this workspace's built package directly.
|
|
614
|
+
resolveSubrouterPluginSpec({ isDev }),
|
|
555
615
|
],
|
|
556
616
|
permission: {
|
|
557
617
|
edit: 'allow',
|
|
@@ -639,6 +699,8 @@ async function startSingleServer({ directory, } = {}) {
|
|
|
639
699
|
OPENCODE_CONFIG: opencodeConfigPath,
|
|
640
700
|
OPENCODE_PORT: port.toString(),
|
|
641
701
|
KIMAKI: '1',
|
|
702
|
+
// The browser is not on this machine, so no localhost callback fires.
|
|
703
|
+
SUBROUTER_MANUAL_OAUTH: '1',
|
|
642
704
|
OPENCODE_EXPERIMENTAL_WORKSPACES: 'true',
|
|
643
705
|
OPENCODE_ENABLE_EXA: '1',
|
|
644
706
|
KIMAKI_DATA_DIR: getDataDir(),
|
|
@@ -661,7 +723,7 @@ async function startSingleServer({ directory, } = {}) {
|
|
|
661
723
|
const logBuffer = [];
|
|
662
724
|
const startupStderrTail = [];
|
|
663
725
|
let serverReady = false;
|
|
664
|
-
logBuffer.push(`Spawned opencode
|
|
726
|
+
logBuffer.push(`Spawned opencode ${serveArgs.join(' ')} (pid: ${serverProcess.pid})`);
|
|
665
727
|
const stdoutReader = subscribeToProcessLogStream({
|
|
666
728
|
stream: serverProcess.stdout,
|
|
667
729
|
onLine: (line) => {
|
|
@@ -1058,7 +1120,9 @@ export function extractSdkErrorMessage(error) {
|
|
|
1058
1120
|
return 'Unknown OpenCode API error';
|
|
1059
1121
|
}
|
|
1060
1122
|
if (error.data?.message) {
|
|
1061
|
-
|
|
1123
|
+
const name = error.name ? `${error.name}: ` : '';
|
|
1124
|
+
const ref = error.data.ref ? ` (${error.data.ref})` : '';
|
|
1125
|
+
return `${name}${error.data.message}${ref}`;
|
|
1062
1126
|
}
|
|
1063
1127
|
if (error.message) {
|
|
1064
1128
|
return error.message;
|
|
@@ -1069,6 +1133,9 @@ export function extractSdkErrorMessage(error) {
|
|
|
1069
1133
|
if (error._tag) {
|
|
1070
1134
|
return error._tag;
|
|
1071
1135
|
}
|
|
1136
|
+
if (error.name) {
|
|
1137
|
+
return error.name;
|
|
1138
|
+
}
|
|
1072
1139
|
return 'Unknown OpenCode API error';
|
|
1073
1140
|
}
|
|
1074
1141
|
/**
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Tests that `kimaki project add` refuses a folder already registered locally.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
|
6
|
+
import { setDataDir } from './config.js';
|
|
7
|
+
import { closeDb } from './db.js';
|
|
8
|
+
import { findRegisteredTextChannelForDirectory, formatProjectAlreadyRegisteredError, initDatabase, setChannelDirectory, } from './database.js';
|
|
9
|
+
describe('project add duplicate directory', () => {
|
|
10
|
+
let tmpDir;
|
|
11
|
+
let projectDir;
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kimaki-project-add-'));
|
|
14
|
+
projectDir = path.join(tmpDir, 'repo');
|
|
15
|
+
fs.mkdirSync(projectDir);
|
|
16
|
+
setDataDir(path.join(tmpDir, 'data'));
|
|
17
|
+
await closeDb();
|
|
18
|
+
await initDatabase();
|
|
19
|
+
});
|
|
20
|
+
afterEach(async () => {
|
|
21
|
+
await closeDb();
|
|
22
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
test('returns the existing text channel for a registered folder', async () => {
|
|
25
|
+
await setChannelDirectory({
|
|
26
|
+
channelId: '111',
|
|
27
|
+
directory: projectDir,
|
|
28
|
+
channelType: 'text',
|
|
29
|
+
});
|
|
30
|
+
const existing = await findRegisteredTextChannelForDirectory(projectDir);
|
|
31
|
+
expect(existing).toMatchObject({
|
|
32
|
+
channel_id: '111',
|
|
33
|
+
directory: projectDir,
|
|
34
|
+
channel_type: 'text',
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
test('matches a symlink to the same registered folder', async () => {
|
|
38
|
+
await setChannelDirectory({
|
|
39
|
+
channelId: '222',
|
|
40
|
+
directory: fs.realpathSync(projectDir),
|
|
41
|
+
channelType: 'text',
|
|
42
|
+
});
|
|
43
|
+
const alias = path.join(tmpDir, 'alias');
|
|
44
|
+
fs.symlinkSync(projectDir, alias);
|
|
45
|
+
const existing = await findRegisteredTextChannelForDirectory(alias);
|
|
46
|
+
expect(existing?.channel_id).toBe('222');
|
|
47
|
+
});
|
|
48
|
+
test('returns null when the folder is not registered', async () => {
|
|
49
|
+
const existing = await findRegisteredTextChannelForDirectory(projectDir);
|
|
50
|
+
expect(existing).toBe(null);
|
|
51
|
+
});
|
|
52
|
+
test('error message tells the user how to remove the mapping', () => {
|
|
53
|
+
expect(formatProjectAlreadyRegisteredError({
|
|
54
|
+
channelId: '111',
|
|
55
|
+
directory: '/tmp/repo',
|
|
56
|
+
})).toMatchInlineSnapshot(`
|
|
57
|
+
"Channel already exists for this directory: /tmp/repo
|
|
58
|
+
Channel ID: 111
|
|
59
|
+
Remove the mapping first: kimaki project remove 111"
|
|
60
|
+
`);
|
|
61
|
+
});
|
|
62
|
+
});
|