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
package/dist/voice.js
CHANGED
|
@@ -181,16 +181,22 @@ function createWavHeader({ dataLength, sampleRate, numChannels, bitsPerSample, }
|
|
|
181
181
|
}
|
|
182
182
|
// Build the transcription tool schema dynamically so the agent field can
|
|
183
183
|
// use an enum constrained to the actual available agent names.
|
|
184
|
-
function buildTranscriptionTool({ agentNames, }) {
|
|
184
|
+
export function buildTranscriptionTool({ agentNames, canForkSession = false, }) {
|
|
185
185
|
const properties = {
|
|
186
186
|
transcription: {
|
|
187
187
|
type: 'string',
|
|
188
|
-
description: 'The final transcription of the audio.
|
|
188
|
+
description: 'The final transcription of the audio. If only a session routing instruction was spoken with no request, return an empty string. If audio is unclear, transcribe your best interpretation. If silent, too short to understand, or completely incomprehensible, use "[inaudible audio]".',
|
|
189
189
|
},
|
|
190
190
|
queueMessage: {
|
|
191
191
|
type: 'boolean',
|
|
192
192
|
description: 'Set to true ONLY if the user explicitly says "queue this message", "queue this", or similar phrasing indicating they want this message queued instead of sent immediately. If not mentioned, omit or set to false.',
|
|
193
193
|
},
|
|
194
|
+
sessionAction: {
|
|
195
|
+
type: 'string',
|
|
196
|
+
enum: canForkSession ? ['btw', 'new-session'] : ['new-session'],
|
|
197
|
+
description: 'Use "new-session" only when explicitly asked to create a new chat, session, or thread with no conversation history. Remove routing instructions from transcription. Omit for normal messages. Never combine with queueMessage.' +
|
|
198
|
+
(canForkSession ? ' Use "btw" only when explicitly asked to create a side chat or fork with current context.' : ''),
|
|
199
|
+
},
|
|
194
200
|
};
|
|
195
201
|
if (agentNames && agentNames.length > 0) {
|
|
196
202
|
properties['agent'] = {
|
|
@@ -213,7 +219,7 @@ function buildTranscriptionTool({ agentNames, }) {
|
|
|
213
219
|
/**
|
|
214
220
|
* Extract transcription result from doGenerate content array.
|
|
215
221
|
* Looks for a tool-call named 'transcriptionResult', falls back to text content.
|
|
216
|
-
* Returns
|
|
222
|
+
* Returns transcription text, queue/session routing, and optional agent selection.
|
|
217
223
|
*/
|
|
218
224
|
export function extractTranscription(content) {
|
|
219
225
|
const toolCall = content.find((c) => c.type === 'tool-call' && c.toolName === 'transcriptionResult');
|
|
@@ -226,13 +232,16 @@ export function extractTranscription(content) {
|
|
|
226
232
|
return {};
|
|
227
233
|
})();
|
|
228
234
|
const transcription = (typeof args.transcription === 'string' ? args.transcription : '').trim();
|
|
229
|
-
const
|
|
235
|
+
const sessionAction = args.sessionAction === 'btw' || args.sessionAction === 'new-session'
|
|
236
|
+
? args.sessionAction
|
|
237
|
+
: undefined;
|
|
238
|
+
const queueMessage = !sessionAction && args.queueMessage === true;
|
|
230
239
|
const agent = typeof args.agent === 'string' ? args.agent : undefined;
|
|
231
240
|
voiceLogger.log(`Transcription result received: "${transcription.slice(0, 100)}..."${queueMessage ? ' [QUEUE]' : ''}${agent ? ` [AGENT:${agent}]` : ''}`);
|
|
232
241
|
if (!transcription) {
|
|
233
242
|
return new EmptyTranscriptionError();
|
|
234
243
|
}
|
|
235
|
-
return { transcription, queueMessage, agent };
|
|
244
|
+
return { transcription, queueMessage, agent, sessionAction };
|
|
236
245
|
}
|
|
237
246
|
// Fall back to text content if no tool call
|
|
238
247
|
const textPart = content.find((c) => c.type === 'text');
|
|
@@ -247,8 +256,8 @@ export function extractTranscription(content) {
|
|
|
247
256
|
reason: 'Model did not produce a transcription',
|
|
248
257
|
});
|
|
249
258
|
}
|
|
250
|
-
async function runTranscriptionOnce({ model, prompt, audioBase64, mediaType, temperature, agentNames, provider, }) {
|
|
251
|
-
const tool = buildTranscriptionTool({ agentNames });
|
|
259
|
+
async function runTranscriptionOnce({ model, prompt, audioBase64, mediaType, temperature, agentNames, canForkSession, provider, }) {
|
|
260
|
+
const tool = buildTranscriptionTool({ agentNames, canForkSession });
|
|
252
261
|
const options = {
|
|
253
262
|
prompt: [
|
|
254
263
|
{
|
|
@@ -311,7 +320,7 @@ export function createTranscriptionModel({ apiKey, provider, }) {
|
|
|
311
320
|
const google = createGoogleGenerativeAI({ apiKey });
|
|
312
321
|
return google('gemini-2.5-flash');
|
|
313
322
|
}
|
|
314
|
-
export async function transcribeAudio({ audio, prompt, language, temperature, apiKey: apiKeyParam, model, provider, mediaType: mediaTypeParam, currentSessionContext, lastSessionContext, agents, }) {
|
|
323
|
+
export async function transcribeAudio({ audio, prompt, language, temperature, apiKey: apiKeyParam, model, provider, mediaType: mediaTypeParam, currentSessionContext, lastSessionContext, agents, canForkSession = false, }) {
|
|
315
324
|
const apiKey = apiKeyParam || process.env.OPENAI_API_KEY || process.env.GEMINI_API_KEY;
|
|
316
325
|
if (!model && !apiKey) {
|
|
317
326
|
return Promise.resolve(new ApiKeyMissingError({ service: 'OpenAI or Gemini' }));
|
|
@@ -412,6 +421,16 @@ This is a software development environment. The speaker is giving instructions t
|
|
|
412
421
|
- Example: "Queue this message. Fix the login bug in auth.ts" → transcription: "Fix the login bug in auth.ts", queueMessage: true
|
|
413
422
|
- If removing the queue phrase would leave empty content (user only said "queue this" with nothing else), keep the full spoken text as the transcription — never return an empty transcription.
|
|
414
423
|
- If no queue intent is detected, omit queueMessage or set it to false.
|
|
424
|
+
|
|
425
|
+
SESSION ROUTING:
|
|
426
|
+
- Only route when the user explicitly asks to create a new chat, session, or thread for the request. Set sessionAction to "new-session": a separate thread with NO conversation history.
|
|
427
|
+
${canForkSession ? '- If the user explicitly asks to create a side chat or fork with current context, set sessionAction to "btw" instead.' : '- Context-preserving side chats and forks are unavailable here. Preserve requests for them as spoken text without setting sessionAction.'}
|
|
428
|
+
- Remove these routing words from the transcription. Include only the actual request, not instructions about where to send it.
|
|
429
|
+
- Example: "Fix the login bug. Create this as a new chat session" -> transcription: "Fix the login bug", sessionAction: "new-session".
|
|
430
|
+
- Do not infer routing from conversational phrases, task content, or session context.
|
|
431
|
+
- If both routing and queueing are requested, sessionAction takes priority; set queueMessage to false. An explicit fresh-session request takes priority over a contextual fork.
|
|
432
|
+
- If there is no actual request after removing routing words, return an empty transcription and omit sessionAction. Never invent placeholder content.
|
|
433
|
+
- Otherwise omit sessionAction. Agent selection can be combined with either route.
|
|
415
434
|
${agents && agents.length > 0 ? `
|
|
416
435
|
AGENT SELECTION:
|
|
417
436
|
- Only set the agent field when the user explicitly says phrases like "use the X agent", "switch to X agent", "with the X agent", or similar phrasing that clearly names a specific agent to switch to.
|
|
@@ -449,6 +468,7 @@ Note: "critique" is a CLI tool for showing diffs in the browser.`;
|
|
|
449
468
|
mediaType,
|
|
450
469
|
temperature: temperature ?? 0.3,
|
|
451
470
|
agentNames: agentNames && agentNames.length > 0 ? agentNames : undefined,
|
|
471
|
+
canForkSession,
|
|
452
472
|
provider: resolvedProvider,
|
|
453
473
|
});
|
|
454
474
|
}
|
package/dist/voice.test.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { describe, test, expect } from 'vitest';
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
-
import { transcribeAudio, convertOggToWav, extractTranscription, normalizeAudioMediaType, getOpenAIAudioConversionStrategy, } from './voice.js';
|
|
6
|
+
import { transcribeAudio, convertOggToWav, extractTranscription, normalizeAudioMediaType, getOpenAIAudioConversionStrategy, buildTranscriptionTool, } from './voice.js';
|
|
7
7
|
import { getVoiceAttachmentMatchReason, isVoiceAttachment, } from './voice-attachment.js';
|
|
8
8
|
describe('audio media type routing', () => {
|
|
9
9
|
test('normalizes m4a aliases to audio/mp4', () => {
|
|
@@ -50,8 +50,86 @@ describe('voice attachment detection', () => {
|
|
|
50
50
|
]
|
|
51
51
|
`);
|
|
52
52
|
});
|
|
53
|
+
test('does not treat iOS and Discord videos as voice', () => {
|
|
54
|
+
expect([
|
|
55
|
+
getVoiceAttachmentMatchReason({
|
|
56
|
+
name: 'ScreenRecording_08-26-2026_11-10-29_1.mov',
|
|
57
|
+
contentType: 'video/quicktime',
|
|
58
|
+
duration: 12.4,
|
|
59
|
+
width: 1170,
|
|
60
|
+
height: 2532,
|
|
61
|
+
}),
|
|
62
|
+
getVoiceAttachmentMatchReason({
|
|
63
|
+
name: 'IMG_1234.MOV',
|
|
64
|
+
contentType: null,
|
|
65
|
+
duration: 8,
|
|
66
|
+
width: 1920,
|
|
67
|
+
height: 1080,
|
|
68
|
+
}),
|
|
69
|
+
getVoiceAttachmentMatchReason({
|
|
70
|
+
name: 'clip.mp4',
|
|
71
|
+
contentType: 'video/mp4',
|
|
72
|
+
duration: 4,
|
|
73
|
+
}),
|
|
74
|
+
getVoiceAttachmentMatchReason({
|
|
75
|
+
name: 'clip.mp4',
|
|
76
|
+
contentType: null,
|
|
77
|
+
}),
|
|
78
|
+
isVoiceAttachment({
|
|
79
|
+
name: 'memo.m4a',
|
|
80
|
+
contentType: null,
|
|
81
|
+
duration: 3.2,
|
|
82
|
+
}),
|
|
83
|
+
]).toMatchInlineSnapshot(`
|
|
84
|
+
[
|
|
85
|
+
null,
|
|
86
|
+
null,
|
|
87
|
+
null,
|
|
88
|
+
null,
|
|
89
|
+
true,
|
|
90
|
+
]
|
|
91
|
+
`);
|
|
92
|
+
});
|
|
53
93
|
});
|
|
54
94
|
describe('extractTranscription', () => {
|
|
95
|
+
test('only offers contextual routing when a source session exists', () => {
|
|
96
|
+
expect(buildTranscriptionTool({}).inputSchema).toMatchObject({
|
|
97
|
+
properties: { sessionAction: { enum: ['new-session'] } },
|
|
98
|
+
});
|
|
99
|
+
expect(buildTranscriptionTool({ canForkSession: true }).inputSchema).toMatchObject({
|
|
100
|
+
properties: { sessionAction: { enum: ['btw', 'new-session'] } },
|
|
101
|
+
});
|
|
102
|
+
expect(JSON.stringify(buildTranscriptionTool({ canForkSession: true })))
|
|
103
|
+
.not.toContain('by the way');
|
|
104
|
+
});
|
|
105
|
+
test.each(['btw', 'new-session'])('extracts %s routing without queueing', (sessionAction) => {
|
|
106
|
+
const result = extractTranscription([{
|
|
107
|
+
type: 'tool-call',
|
|
108
|
+
toolCallId: 'routing',
|
|
109
|
+
toolName: 'transcriptionResult',
|
|
110
|
+
input: JSON.stringify({
|
|
111
|
+
transcription: 'Explain the authentication flow',
|
|
112
|
+
sessionAction,
|
|
113
|
+
queueMessage: true,
|
|
114
|
+
agent: 'plan',
|
|
115
|
+
}),
|
|
116
|
+
}]);
|
|
117
|
+
expect(result).toEqual({
|
|
118
|
+
transcription: 'Explain the authentication flow',
|
|
119
|
+
sessionAction,
|
|
120
|
+
queueMessage: false,
|
|
121
|
+
agent: 'plan',
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
test('ignores unknown session routing', () => {
|
|
125
|
+
const result = extractTranscription([{
|
|
126
|
+
type: 'tool-call',
|
|
127
|
+
toolCallId: 'routing',
|
|
128
|
+
toolName: 'transcriptionResult',
|
|
129
|
+
input: JSON.stringify({ transcription: 'Keep working', sessionAction: 'invalid' }),
|
|
130
|
+
}]);
|
|
131
|
+
expect(result).toEqual({ transcription: 'Keep working', queueMessage: false, agent: undefined });
|
|
132
|
+
});
|
|
55
133
|
test('extracts transcription from tool call', () => {
|
|
56
134
|
const result = extractTranscription([
|
|
57
135
|
{
|
|
@@ -65,6 +143,7 @@ describe('extractTranscription', () => {
|
|
|
65
143
|
{
|
|
66
144
|
"agent": undefined,
|
|
67
145
|
"queueMessage": false,
|
|
146
|
+
"sessionAction": undefined,
|
|
68
147
|
"transcription": "hello world",
|
|
69
148
|
}
|
|
70
149
|
`);
|
|
@@ -85,6 +164,7 @@ describe('extractTranscription', () => {
|
|
|
85
164
|
{
|
|
86
165
|
"agent": undefined,
|
|
87
166
|
"queueMessage": true,
|
|
167
|
+
"sessionAction": undefined,
|
|
88
168
|
"transcription": "Fix the login bug in auth.ts",
|
|
89
169
|
}
|
|
90
170
|
`);
|
|
@@ -143,18 +223,17 @@ describe('extractTranscription', () => {
|
|
|
143
223
|
expect(result.message).toMatchInlineSnapshot(`"Transcription failed: Model did not produce a transcription"`);
|
|
144
224
|
});
|
|
145
225
|
});
|
|
146
|
-
describe('
|
|
226
|
+
const liveVoiceTest = describe.skipIf(process.env['KIMAKI_LIVE_VOICE_TESTS'] !== '1');
|
|
227
|
+
liveVoiceTest('transcribeAudio with real API', () => {
|
|
147
228
|
const audioPath = path.join(import.meta.dirname, '..', 'scripts', 'example-audio.mp3');
|
|
148
|
-
|
|
229
|
+
const oggPath = path.join(import.meta.dirname, '..', 'scripts', 'example-audio.ogg');
|
|
230
|
+
const geminiLiveTest = test.skipIf(!process.env.GEMINI_API_KEY || !fs.existsSync(audioPath));
|
|
231
|
+
const openAiLiveTest = test.skipIf(!process.env.OPENAI_API_KEY || !fs.existsSync(audioPath));
|
|
232
|
+
const openAiOggLiveTest = test.skipIf(!process.env.OPENAI_API_KEY || !fs.existsSync(oggPath));
|
|
233
|
+
geminiLiveTest('transcribes with Gemini', { timeout: 30_000 }, async () => {
|
|
149
234
|
const apiKey = process.env.GEMINI_API_KEY;
|
|
150
|
-
if (!apiKey)
|
|
151
|
-
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
if (!fs.existsSync(audioPath)) {
|
|
155
|
-
console.log('Skipping: example-audio.mp3 not found');
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
235
|
+
if (!apiKey)
|
|
236
|
+
throw new Error('GEMINI_API_KEY is required');
|
|
158
237
|
const audio = fs.readFileSync(audioPath);
|
|
159
238
|
const result = await transcribeAudio({
|
|
160
239
|
audio,
|
|
@@ -167,16 +246,10 @@ describe('transcribeAudio with real API', () => {
|
|
|
167
246
|
expect(transcription.length).toBeGreaterThan(0);
|
|
168
247
|
console.log('Gemini transcription:', result);
|
|
169
248
|
});
|
|
170
|
-
|
|
249
|
+
openAiLiveTest('transcribes with OpenAI', { timeout: 30_000 }, async () => {
|
|
171
250
|
const apiKey = process.env.OPENAI_API_KEY;
|
|
172
|
-
if (!apiKey)
|
|
173
|
-
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
if (!fs.existsSync(audioPath)) {
|
|
177
|
-
console.log('Skipping: example-audio.mp3 not found');
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
251
|
+
if (!apiKey)
|
|
252
|
+
throw new Error('OPENAI_API_KEY is required');
|
|
180
253
|
const audio = fs.readFileSync(audioPath);
|
|
181
254
|
const result = await transcribeAudio({
|
|
182
255
|
audio,
|
|
@@ -189,17 +262,10 @@ describe('transcribeAudio with real API', () => {
|
|
|
189
262
|
expect(transcription.length).toBeGreaterThan(0);
|
|
190
263
|
console.log('OpenAI transcription:', result);
|
|
191
264
|
});
|
|
192
|
-
|
|
265
|
+
openAiOggLiveTest('transcribes OGG with OpenAI (converts to WAV)', { timeout: 30_000 }, async () => {
|
|
193
266
|
const apiKey = process.env.OPENAI_API_KEY;
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
console.log('Skipping: OPENAI_API_KEY not set');
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
if (!fs.existsSync(oggPath)) {
|
|
200
|
-
console.log('Skipping: example-audio.ogg not found');
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
267
|
+
if (!apiKey)
|
|
268
|
+
throw new Error('OPENAI_API_KEY is required');
|
|
203
269
|
const audio = fs.readFileSync(oggPath);
|
|
204
270
|
const result = await transcribeAudio({
|
|
205
271
|
audio,
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// Verifies OpenCode workspace creation stays bound to the requested local clone.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { afterAll, beforeAll, expect, test } from 'vitest';
|
|
5
|
+
import { setDataDir } from './config.js';
|
|
6
|
+
import { tryWorkspaceCreate } from './commands/new-worktree.js';
|
|
7
|
+
import { initializeOpencodeForDirectory, stopOpencodeServer } from './opencode.js';
|
|
8
|
+
import { chooseLockPort } from './test-utils.js';
|
|
9
|
+
import { execAsync, getManagedWorktreeDirectory } from './worktrees.js';
|
|
10
|
+
import { KIMAKI_WORKTREE_ADAPTER_TYPE } from './git-worktree-core.js';
|
|
11
|
+
const WORKTREE_BRANCH = 'opencode/kimaki-clone-isolation';
|
|
12
|
+
const REJECTED_WORKTREE_BRANCH = 'opencode/kimaki-rejected-clone-isolation';
|
|
13
|
+
async function git({ cwd, args }) {
|
|
14
|
+
const result = await execAsync(`git ${args.map((arg) => JSON.stringify(arg)).join(' ')}`, {
|
|
15
|
+
cwd,
|
|
16
|
+
timeout: 60_000,
|
|
17
|
+
});
|
|
18
|
+
return result.stdout.trim();
|
|
19
|
+
}
|
|
20
|
+
let sandbox = '';
|
|
21
|
+
let requestedClone = '';
|
|
22
|
+
let otherClone = '';
|
|
23
|
+
beforeAll(async () => {
|
|
24
|
+
const root = path.resolve(process.cwd(), 'tmp');
|
|
25
|
+
fs.mkdirSync(root, { recursive: true });
|
|
26
|
+
sandbox = fs.mkdtempSync(path.join(root, 'worktree-clone-isolation-'));
|
|
27
|
+
const remote = path.join(sandbox, 'remote.git');
|
|
28
|
+
requestedClone = path.join(sandbox, 'holocron');
|
|
29
|
+
otherClone = path.join(sandbox, 'fumabase');
|
|
30
|
+
process.env['KIMAKI_LOCK_PORT'] = String(chooseLockPort({ key: 'worktree-clone-isolation' }));
|
|
31
|
+
setDataDir(path.join(sandbox, 'data'));
|
|
32
|
+
await git({ cwd: sandbox, args: ['init', '--bare', '-b', 'main', remote] });
|
|
33
|
+
await git({ cwd: sandbox, args: ['clone', remote, otherClone] });
|
|
34
|
+
await git({
|
|
35
|
+
cwd: otherClone,
|
|
36
|
+
args: ['config', 'user.email', 'kimaki-tests@example.com'],
|
|
37
|
+
});
|
|
38
|
+
await git({ cwd: otherClone, args: ['config', 'user.name', 'Kimaki Tests'] });
|
|
39
|
+
fs.writeFileSync(path.join(otherClone, 'preview.txt'), 'old preview\n');
|
|
40
|
+
await git({ cwd: otherClone, args: ['add', 'preview.txt'] });
|
|
41
|
+
await git({ cwd: otherClone, args: ['commit', '-m', 'old preview'] });
|
|
42
|
+
await git({ cwd: otherClone, args: ['push', 'origin', 'HEAD:main'] });
|
|
43
|
+
await git({ cwd: otherClone, args: ['switch', '-c', 'preview'] });
|
|
44
|
+
await git({ cwd: sandbox, args: ['clone', remote, requestedClone] });
|
|
45
|
+
await git({
|
|
46
|
+
cwd: requestedClone,
|
|
47
|
+
args: ['config', 'user.email', 'kimaki-tests@example.com'],
|
|
48
|
+
});
|
|
49
|
+
await git({ cwd: requestedClone, args: ['config', 'user.name', 'Kimaki Tests'] });
|
|
50
|
+
fs.mkdirSync(path.join(requestedClone, 'vite'));
|
|
51
|
+
fs.writeFileSync(path.join(requestedClone, 'vite', 'README.md'), 'new layout\n');
|
|
52
|
+
await git({ cwd: requestedClone, args: ['add', 'vite/README.md'] });
|
|
53
|
+
await git({ cwd: requestedClone, args: ['commit', '-m', 'add vite layout'] });
|
|
54
|
+
}, 20_000);
|
|
55
|
+
afterAll(async () => {
|
|
56
|
+
await stopOpencodeServer();
|
|
57
|
+
delete process.env['KIMAKI_LOCK_PORT'];
|
|
58
|
+
if (sandbox)
|
|
59
|
+
fs.rmSync(sandbox, { recursive: true, force: true });
|
|
60
|
+
});
|
|
61
|
+
test('creates a workspace from the exact requested clone and commit', async () => {
|
|
62
|
+
const requestedCommit = await git({
|
|
63
|
+
cwd: requestedClone,
|
|
64
|
+
args: ['rev-parse', 'HEAD^{commit}'],
|
|
65
|
+
});
|
|
66
|
+
const requestedCommonDirectory = path.resolve(requestedClone, await git({
|
|
67
|
+
cwd: requestedClone,
|
|
68
|
+
args: ['rev-parse', '--git-common-dir'],
|
|
69
|
+
}));
|
|
70
|
+
const requestedClientResult = await initializeOpencodeForDirectory(requestedClone);
|
|
71
|
+
if (requestedClientResult instanceof Error)
|
|
72
|
+
throw requestedClientResult;
|
|
73
|
+
const otherClientResult = await initializeOpencodeForDirectory(otherClone);
|
|
74
|
+
if (otherClientResult instanceof Error)
|
|
75
|
+
throw otherClientResult;
|
|
76
|
+
const requestedClient = requestedClientResult();
|
|
77
|
+
const otherClient = otherClientResult();
|
|
78
|
+
// Load the requested clone first, then overwrite the colliding OpenCode
|
|
79
|
+
// project adapter with the other clone before creating the workspace.
|
|
80
|
+
await requestedClient.config.get({ directory: requestedClone });
|
|
81
|
+
await otherClient.config.get({ directory: otherClone });
|
|
82
|
+
const response = await requestedClient.experimental.workspace.create({
|
|
83
|
+
directory: requestedClone,
|
|
84
|
+
type: KIMAKI_WORKTREE_ADAPTER_TYPE,
|
|
85
|
+
branch: WORKTREE_BRANCH,
|
|
86
|
+
extra: {
|
|
87
|
+
projectDirectory: requestedClone,
|
|
88
|
+
baseCommit: requestedCommit,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
if (response.error)
|
|
92
|
+
throw new Error(JSON.stringify(response.error));
|
|
93
|
+
const workspace = response.data;
|
|
94
|
+
if (!workspace)
|
|
95
|
+
throw new Error('OpenCode returned no workspace');
|
|
96
|
+
try {
|
|
97
|
+
const actualCommit = await git({
|
|
98
|
+
cwd: workspace.directory,
|
|
99
|
+
args: ['rev-parse', 'HEAD^{commit}'],
|
|
100
|
+
});
|
|
101
|
+
const actualCommonDirectory = path.resolve(workspace.directory, await git({
|
|
102
|
+
cwd: workspace.directory,
|
|
103
|
+
args: ['rev-parse', '--git-common-dir'],
|
|
104
|
+
}));
|
|
105
|
+
expect({
|
|
106
|
+
usesRequestedClone: path.resolve(actualCommonDirectory) === path.resolve(requestedCommonDirectory),
|
|
107
|
+
usesRequestedCommit: actualCommit === requestedCommit,
|
|
108
|
+
hasRequestedLayout: fs.existsSync(path.join(workspace.directory, 'vite', 'README.md')),
|
|
109
|
+
absentFromOtherClone: !(await git({
|
|
110
|
+
cwd: otherClone,
|
|
111
|
+
args: ['worktree', 'list', '--porcelain'],
|
|
112
|
+
})).includes(workspace.directory),
|
|
113
|
+
}).toMatchInlineSnapshot(`
|
|
114
|
+
{
|
|
115
|
+
"absentFromOtherClone": true,
|
|
116
|
+
"hasRequestedLayout": true,
|
|
117
|
+
"usesRequestedClone": true,
|
|
118
|
+
"usesRequestedCommit": true,
|
|
119
|
+
}
|
|
120
|
+
`);
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
await requestedClient.experimental.workspace.remove({
|
|
124
|
+
id: workspace.id,
|
|
125
|
+
directory: requestedClone,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}, 30_000);
|
|
129
|
+
test('removes workspace state when identity validation rejects creation', async () => {
|
|
130
|
+
const requestedCommit = await git({
|
|
131
|
+
cwd: requestedClone,
|
|
132
|
+
args: ['rev-parse', 'HEAD^{commit}'],
|
|
133
|
+
});
|
|
134
|
+
const result = await tryWorkspaceCreate({
|
|
135
|
+
worktreeName: REJECTED_WORKTREE_BRANCH,
|
|
136
|
+
projectDirectory: requestedClone,
|
|
137
|
+
baseCommit: requestedCommit.toUpperCase(),
|
|
138
|
+
});
|
|
139
|
+
expect(result).toBeInstanceOf(Error);
|
|
140
|
+
const clientResult = await initializeOpencodeForDirectory(requestedClone);
|
|
141
|
+
if (clientResult instanceof Error)
|
|
142
|
+
throw clientResult;
|
|
143
|
+
const client = clientResult();
|
|
144
|
+
const listResponse = await client.experimental.workspace.list({
|
|
145
|
+
directory: requestedClone,
|
|
146
|
+
});
|
|
147
|
+
if (listResponse.error)
|
|
148
|
+
throw new Error(JSON.stringify(listResponse.error));
|
|
149
|
+
const rejectedWorkspaces = (listResponse.data ?? []).filter((workspace) => {
|
|
150
|
+
return workspace.branch === REJECTED_WORKTREE_BRANCH;
|
|
151
|
+
});
|
|
152
|
+
try {
|
|
153
|
+
const managedDirectory = getManagedWorktreeDirectory({
|
|
154
|
+
directory: requestedClone,
|
|
155
|
+
name: REJECTED_WORKTREE_BRANCH,
|
|
156
|
+
});
|
|
157
|
+
const [requestedWorktrees, otherWorktrees] = await Promise.all([
|
|
158
|
+
git({ cwd: requestedClone, args: ['worktree', 'list', '--porcelain'] }),
|
|
159
|
+
git({ cwd: otherClone, args: ['worktree', 'list', '--porcelain'] }),
|
|
160
|
+
]);
|
|
161
|
+
const branchExists = await execAsync(`git show-ref --verify --quiet ${JSON.stringify(`refs/heads/${REJECTED_WORKTREE_BRANCH}`)}`, { cwd: requestedClone })
|
|
162
|
+
.then(() => true)
|
|
163
|
+
.catch(() => false);
|
|
164
|
+
expect({
|
|
165
|
+
workspaceRows: rejectedWorkspaces.length,
|
|
166
|
+
directoryExists: fs.existsSync(managedDirectory),
|
|
167
|
+
requestedCloneRegistration: requestedWorktrees.includes(managedDirectory),
|
|
168
|
+
otherCloneRegistration: otherWorktrees.includes(managedDirectory),
|
|
169
|
+
branchExists,
|
|
170
|
+
}).toMatchInlineSnapshot(`
|
|
171
|
+
{
|
|
172
|
+
"branchExists": false,
|
|
173
|
+
"directoryExists": false,
|
|
174
|
+
"otherCloneRegistration": false,
|
|
175
|
+
"requestedCloneRegistration": false,
|
|
176
|
+
"workspaceRows": 0,
|
|
177
|
+
}
|
|
178
|
+
`);
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
await Promise.all(rejectedWorkspaces.map((workspace) => {
|
|
182
|
+
return client.experimental.workspace.remove({
|
|
183
|
+
id: workspace.id,
|
|
184
|
+
directory: requestedClone,
|
|
185
|
+
});
|
|
186
|
+
}));
|
|
187
|
+
}
|
|
188
|
+
}, 30_000);
|