@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,228 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
|
2
|
+
/* istanbul ignore file */
|
|
3
|
+
// @ts-nocheck
|
|
4
|
+
import { RealtimeClient } from '@openai/realtime-api-beta';
|
|
5
|
+
import { writeFile } from 'fs';
|
|
6
|
+
import { createLogger, LogPrefix } from './logger.js';
|
|
7
|
+
const openaiLogger = createLogger(LogPrefix.OPENAI);
|
|
8
|
+
const audioParts = [];
|
|
9
|
+
function saveBinaryFile(fileName, content) {
|
|
10
|
+
writeFile(fileName, content, 'utf8', (err) => {
|
|
11
|
+
if (err) {
|
|
12
|
+
openaiLogger.error(`Error writing file ${fileName}:`, err);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
openaiLogger.log(`Appending stream content to file ${fileName}.`);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function convertToWav(rawData, mimeType) {
|
|
19
|
+
const options = parseMimeType(mimeType);
|
|
20
|
+
const dataLength = rawData.reduce((a, b) => a + b.length, 0);
|
|
21
|
+
const wavHeader = createWavHeader(dataLength, options);
|
|
22
|
+
const buffer = Buffer.concat(rawData);
|
|
23
|
+
return Buffer.concat([wavHeader, buffer]);
|
|
24
|
+
}
|
|
25
|
+
function parseMimeType(mimeType) {
|
|
26
|
+
const [fileType, ...params] = mimeType.split(';').map((s) => s.trim());
|
|
27
|
+
const [_, format] = fileType?.split('/') || [];
|
|
28
|
+
const options = {
|
|
29
|
+
numChannels: 1,
|
|
30
|
+
bitsPerSample: 16,
|
|
31
|
+
};
|
|
32
|
+
if (format && format.startsWith('L')) {
|
|
33
|
+
const bits = parseInt(format.slice(1), 10);
|
|
34
|
+
if (!isNaN(bits)) {
|
|
35
|
+
options.bitsPerSample = bits;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
for (const param of params) {
|
|
39
|
+
const [key, value] = param.split('=').map((s) => s.trim());
|
|
40
|
+
if (key === 'rate') {
|
|
41
|
+
options.sampleRate = parseInt(value || '', 10);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return options;
|
|
45
|
+
}
|
|
46
|
+
function createWavHeader(dataLength, options) {
|
|
47
|
+
const { numChannels, sampleRate, bitsPerSample } = options;
|
|
48
|
+
// http://soundfile.sapp.org/doc/WaveFormat
|
|
49
|
+
const byteRate = (sampleRate * numChannels * bitsPerSample) / 8;
|
|
50
|
+
const blockAlign = (numChannels * bitsPerSample) / 8;
|
|
51
|
+
const buffer = Buffer.alloc(44);
|
|
52
|
+
buffer.write('RIFF', 0); // ChunkID
|
|
53
|
+
buffer.writeUInt32LE(36 + dataLength, 4); // ChunkSize
|
|
54
|
+
buffer.write('WAVE', 8); // Format
|
|
55
|
+
buffer.write('fmt ', 12); // Subchunk1ID
|
|
56
|
+
buffer.writeUInt32LE(16, 16); // Subchunk1Size (PCM)
|
|
57
|
+
buffer.writeUInt16LE(1, 20); // AudioFormat (1 = PCM)
|
|
58
|
+
buffer.writeUInt16LE(numChannels, 22); // NumChannels
|
|
59
|
+
buffer.writeUInt32LE(sampleRate, 24); // SampleRate
|
|
60
|
+
buffer.writeUInt32LE(byteRate, 28); // ByteRate
|
|
61
|
+
buffer.writeUInt16LE(blockAlign, 32); // BlockAlign
|
|
62
|
+
buffer.writeUInt16LE(bitsPerSample, 34); // BitsPerSample
|
|
63
|
+
buffer.write('data', 36); // Subchunk2ID
|
|
64
|
+
buffer.writeUInt32LE(dataLength, 40); // Subchunk2Size
|
|
65
|
+
return buffer;
|
|
66
|
+
}
|
|
67
|
+
function defaultAudioChunkHandler({ data, mimeType, }) {
|
|
68
|
+
audioParts.push(data);
|
|
69
|
+
const fileName = 'audio.wav';
|
|
70
|
+
const buffer = convertToWav(audioParts, mimeType);
|
|
71
|
+
saveBinaryFile(fileName, buffer);
|
|
72
|
+
}
|
|
73
|
+
export async function startGenAiSession({ onAssistantAudioChunk, onAssistantStartSpeaking, onAssistantStopSpeaking, onAssistantInterruptSpeaking, systemMessage, tools, } = {}) {
|
|
74
|
+
if (!process.env.OPENAI_API_KEY) {
|
|
75
|
+
throw new Error('OPENAI_API_KEY environment variable is required');
|
|
76
|
+
}
|
|
77
|
+
const client = new RealtimeClient({
|
|
78
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
79
|
+
});
|
|
80
|
+
const audioChunkHandler = onAssistantAudioChunk || defaultAudioChunkHandler;
|
|
81
|
+
let isAssistantSpeaking = false;
|
|
82
|
+
// Configure session with 24kHz sample rate
|
|
83
|
+
client.updateSession({
|
|
84
|
+
instructions: systemMessage || '',
|
|
85
|
+
voice: 'alloy',
|
|
86
|
+
input_audio_format: 'pcm16',
|
|
87
|
+
output_audio_format: 'pcm16',
|
|
88
|
+
input_audio_transcription: { model: 'whisper-1' },
|
|
89
|
+
turn_detection: { type: 'server_vad' },
|
|
90
|
+
modalities: ['text', 'audio'],
|
|
91
|
+
temperature: 0.8,
|
|
92
|
+
});
|
|
93
|
+
// Add tools if provided
|
|
94
|
+
if (tools) {
|
|
95
|
+
for (const [name, tool] of Object.entries(tools)) {
|
|
96
|
+
// Convert AI SDK tool to OpenAI Realtime format
|
|
97
|
+
// The tool.inputSchema is a Zod schema, we need to convert it to JSON Schema
|
|
98
|
+
let parameters = {
|
|
99
|
+
type: 'object',
|
|
100
|
+
properties: {},
|
|
101
|
+
required: [],
|
|
102
|
+
};
|
|
103
|
+
// If the tool has a Zod schema, we can try to extract basic structure
|
|
104
|
+
// For now, we'll use a simple placeholder
|
|
105
|
+
if (tool.description?.includes('session')) {
|
|
106
|
+
parameters = {
|
|
107
|
+
type: 'object',
|
|
108
|
+
properties: {
|
|
109
|
+
sessionId: { type: 'string', description: 'The session ID' },
|
|
110
|
+
message: { type: 'string', description: 'The message text' },
|
|
111
|
+
},
|
|
112
|
+
required: ['sessionId'],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
client.addTool({
|
|
116
|
+
type: 'function',
|
|
117
|
+
name,
|
|
118
|
+
description: tool.description || '',
|
|
119
|
+
parameters,
|
|
120
|
+
}, async (params) => {
|
|
121
|
+
try {
|
|
122
|
+
if (!tool.execute || typeof tool.execute !== 'function') {
|
|
123
|
+
return { error: 'Tool execute function not found' };
|
|
124
|
+
}
|
|
125
|
+
// Call the execute function with params
|
|
126
|
+
// The Tool type from 'ai' expects (input, options) but we need to handle this safely
|
|
127
|
+
const result = await tool.execute(params, {
|
|
128
|
+
abortSignal: new AbortController().signal,
|
|
129
|
+
toolCallId: '',
|
|
130
|
+
messages: [],
|
|
131
|
+
});
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
openaiLogger.error(`Tool ${name} execution error:`, error);
|
|
136
|
+
return { error: String(error) };
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// Set up event handlers
|
|
142
|
+
client.on('conversation.item.created', ({ item }) => {
|
|
143
|
+
if ('role' in item &&
|
|
144
|
+
item.role === 'assistant' &&
|
|
145
|
+
item.type === 'message') {
|
|
146
|
+
// Check if this is the first audio content
|
|
147
|
+
const hasAudio = 'content' in item &&
|
|
148
|
+
Array.isArray(item.content) &&
|
|
149
|
+
item.content.some((c) => 'type' in c && c.type === 'audio');
|
|
150
|
+
if (hasAudio && !isAssistantSpeaking && onAssistantStartSpeaking) {
|
|
151
|
+
isAssistantSpeaking = true;
|
|
152
|
+
onAssistantStartSpeaking();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
client.on('conversation.updated', ({ item, delta, }) => {
|
|
157
|
+
// Handle audio chunks
|
|
158
|
+
if (delta?.audio && 'role' in item && item.role === 'assistant') {
|
|
159
|
+
if (!isAssistantSpeaking && onAssistantStartSpeaking) {
|
|
160
|
+
isAssistantSpeaking = true;
|
|
161
|
+
onAssistantStartSpeaking();
|
|
162
|
+
}
|
|
163
|
+
// OpenAI provides audio as Int16Array or base64
|
|
164
|
+
let audioBuffer;
|
|
165
|
+
if (delta.audio instanceof Int16Array) {
|
|
166
|
+
audioBuffer = Buffer.from(delta.audio.buffer);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
// Assume base64 string
|
|
170
|
+
audioBuffer = Buffer.from(delta.audio, 'base64');
|
|
171
|
+
}
|
|
172
|
+
// OpenAI uses 24kHz PCM16 format
|
|
173
|
+
audioChunkHandler({
|
|
174
|
+
data: audioBuffer,
|
|
175
|
+
mimeType: 'audio/pcm;rate=24000',
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
// Handle transcriptions
|
|
179
|
+
if (delta?.transcript) {
|
|
180
|
+
if ('role' in item) {
|
|
181
|
+
if (item.role === 'user') {
|
|
182
|
+
openaiLogger.log('User transcription:', delta.transcript);
|
|
183
|
+
}
|
|
184
|
+
else if (item.role === 'assistant') {
|
|
185
|
+
openaiLogger.log('Assistant transcription:', delta.transcript);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
client.on('conversation.item.completed', ({ item }) => {
|
|
191
|
+
if ('role' in item &&
|
|
192
|
+
item.role === 'assistant' &&
|
|
193
|
+
isAssistantSpeaking &&
|
|
194
|
+
onAssistantStopSpeaking) {
|
|
195
|
+
isAssistantSpeaking = false;
|
|
196
|
+
onAssistantStopSpeaking();
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
client.on('conversation.interrupted', () => {
|
|
200
|
+
openaiLogger.log('Assistant was interrupted');
|
|
201
|
+
if (isAssistantSpeaking && onAssistantInterruptSpeaking) {
|
|
202
|
+
isAssistantSpeaking = false;
|
|
203
|
+
onAssistantInterruptSpeaking();
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
// Connect to the Realtime API
|
|
207
|
+
await client.connect();
|
|
208
|
+
const sessionResult = {
|
|
209
|
+
session: {
|
|
210
|
+
send: (audioData) => {
|
|
211
|
+
// Convert ArrayBuffer to Int16Array for OpenAI
|
|
212
|
+
const int16Data = new Int16Array(audioData);
|
|
213
|
+
client.appendInputAudio(int16Data);
|
|
214
|
+
},
|
|
215
|
+
sendText: (text) => {
|
|
216
|
+
// Send text message to OpenAI
|
|
217
|
+
client.sendUserMessageContent([{ type: 'input_text', text }]);
|
|
218
|
+
},
|
|
219
|
+
close: () => {
|
|
220
|
+
client.disconnect();
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
stop: () => {
|
|
224
|
+
client.disconnect();
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
return sessionResult;
|
|
228
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// E2e test for OpenCode plugin loading.
|
|
2
|
+
// Spawns `opencode serve` directly with our plugin in OPENCODE_CONFIG_CONTENT,
|
|
3
|
+
// waits for the health endpoint, then checks stderr for plugin errors.
|
|
4
|
+
// No Discord infrastructure needed — just the OpenCode server process.
|
|
5
|
+
import { spawn } from 'node:child_process';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import net from 'node:net';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { test, expect } from 'vitest';
|
|
11
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
async function getOpenPort() {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const server = net.createServer();
|
|
15
|
+
server.listen(0, () => {
|
|
16
|
+
const address = server.address();
|
|
17
|
+
if (address && typeof address === 'object') {
|
|
18
|
+
server.close(() => {
|
|
19
|
+
resolve(address.port);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
reject(new Error('Failed to get port'));
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
server.on('error', reject);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
async function waitForHealth({ port, maxAttempts = 30, }) {
|
|
30
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
31
|
+
try {
|
|
32
|
+
const response = await fetch(`http://127.0.0.1:${port}/api/health`);
|
|
33
|
+
if (response.status < 500) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// connection refused, retry
|
|
39
|
+
}
|
|
40
|
+
await new Promise((resolve) => {
|
|
41
|
+
setTimeout(resolve, 1000);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
test('opencode server loads plugin without errors', async () => {
|
|
47
|
+
const projectDir = path.resolve(process.cwd(), 'tmp', 'plugin-loading-e2e');
|
|
48
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
49
|
+
const port = await getOpenPort();
|
|
50
|
+
const pluginPath = new URL('../src/opencode-plugin.ts', import.meta.url).href;
|
|
51
|
+
const stderrLines = [];
|
|
52
|
+
const serverProcess = spawn(process.env.OPENCODE_PATH || 'opencode', ['serve', '--port', port.toString(), '--print-logs', '--log-level', 'DEBUG'], {
|
|
53
|
+
stdio: 'pipe',
|
|
54
|
+
cwd: projectDir,
|
|
55
|
+
shell: true,
|
|
56
|
+
env: {
|
|
57
|
+
...process.env,
|
|
58
|
+
OPENCODE_CONFIG_CONTENT: JSON.stringify({
|
|
59
|
+
$schema: 'https://opencode.ai/config.json',
|
|
60
|
+
lsp: false,
|
|
61
|
+
formatter: false,
|
|
62
|
+
plugin: [pluginPath],
|
|
63
|
+
}),
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
serverProcess.stderr?.on('data', (data) => {
|
|
67
|
+
stderrLines.push(...data.toString().split('\n').filter(Boolean));
|
|
68
|
+
});
|
|
69
|
+
try {
|
|
70
|
+
const healthy = await waitForHealth({ port });
|
|
71
|
+
expect(healthy).toBe(true);
|
|
72
|
+
// Check no plugin-related errors in stderr
|
|
73
|
+
const pluginErrorPatterns = [
|
|
74
|
+
/plugin.*error/i,
|
|
75
|
+
/failed to load plugin/i,
|
|
76
|
+
/cannot find module/i,
|
|
77
|
+
/ERR_MODULE_NOT_FOUND/i,
|
|
78
|
+
/plugin.*failed/i,
|
|
79
|
+
/plugin.*crash/i,
|
|
80
|
+
];
|
|
81
|
+
const errorLines = stderrLines.filter((line) => {
|
|
82
|
+
return pluginErrorPatterns.some((pattern) => {
|
|
83
|
+
return pattern.test(line);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
expect(errorLines).toEqual([]);
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
serverProcess.kill('SIGTERM');
|
|
90
|
+
}
|
|
91
|
+
}, 60_000);
|