@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.
Files changed (328) hide show
  1. package/bin.js +2 -0
  2. package/dist/ai-tool-to-genai.js +233 -0
  3. package/dist/ai-tool-to-genai.test.js +267 -0
  4. package/dist/ai-tool.js +6 -0
  5. package/dist/bin.js +87 -0
  6. package/dist/bot-token.js +121 -0
  7. package/dist/bot-token.test.js +134 -0
  8. package/dist/channel-management.js +101 -0
  9. package/dist/cli-parsing.test.js +89 -0
  10. package/dist/cli.js +2529 -0
  11. package/dist/commands/abort.js +82 -0
  12. package/dist/commands/action-buttons.js +257 -0
  13. package/dist/commands/add-project.js +114 -0
  14. package/dist/commands/agent.js +291 -0
  15. package/dist/commands/ask-question.js +223 -0
  16. package/dist/commands/compact.js +120 -0
  17. package/dist/commands/context-usage.js +140 -0
  18. package/dist/commands/create-new-project.js +118 -0
  19. package/dist/commands/diff.js +128 -0
  20. package/dist/commands/file-upload.js +275 -0
  21. package/dist/commands/fork.js +217 -0
  22. package/dist/commands/gemini-apikey.js +70 -0
  23. package/dist/commands/login.js +490 -0
  24. package/dist/commands/mention-mode.js +51 -0
  25. package/dist/commands/merge-worktree.js +124 -0
  26. package/dist/commands/model.js +694 -0
  27. package/dist/commands/permissions.js +163 -0
  28. package/dist/commands/queue.js +217 -0
  29. package/dist/commands/remove-project.js +115 -0
  30. package/dist/commands/restart-opencode-server.js +116 -0
  31. package/dist/commands/resume.js +159 -0
  32. package/dist/commands/run-command.js +79 -0
  33. package/dist/commands/session-id.js +78 -0
  34. package/dist/commands/session.js +192 -0
  35. package/dist/commands/share.js +80 -0
  36. package/dist/commands/types.js +2 -0
  37. package/dist/commands/undo-redo.js +159 -0
  38. package/dist/commands/unset-model.js +152 -0
  39. package/dist/commands/upgrade.js +42 -0
  40. package/dist/commands/user-command.js +148 -0
  41. package/dist/commands/verbosity.js +60 -0
  42. package/dist/commands/worktree-settings.js +50 -0
  43. package/dist/commands/worktree.js +299 -0
  44. package/dist/condense-memory.js +33 -0
  45. package/dist/config.js +110 -0
  46. package/dist/database.js +1050 -0
  47. package/dist/db.js +159 -0
  48. package/dist/db.test.js +49 -0
  49. package/dist/discord-api.js +28 -0
  50. package/dist/discord-auth.js +231 -0
  51. package/dist/discord-auth.test.js +80 -0
  52. package/dist/discord-bot.js +997 -0
  53. package/dist/discord-utils.js +560 -0
  54. package/dist/discord-utils.test.js +115 -0
  55. package/dist/errors.js +167 -0
  56. package/dist/escape-backticks.test.js +429 -0
  57. package/dist/format-tables.js +122 -0
  58. package/dist/format-tables.test.js +199 -0
  59. package/dist/forum-sync/config.js +79 -0
  60. package/dist/forum-sync/discord-operations.js +154 -0
  61. package/dist/forum-sync/index.js +5 -0
  62. package/dist/forum-sync/markdown.js +117 -0
  63. package/dist/forum-sync/sync-to-discord.js +417 -0
  64. package/dist/forum-sync/sync-to-files.js +190 -0
  65. package/dist/forum-sync/types.js +53 -0
  66. package/dist/forum-sync/watchers.js +307 -0
  67. package/dist/gateway-consumer.js +232 -0
  68. package/dist/gateway-consumer.test.js +18 -0
  69. package/dist/genai-worker-wrapper.js +111 -0
  70. package/dist/genai-worker.js +311 -0
  71. package/dist/genai.js +232 -0
  72. package/dist/generated/browser.js +17 -0
  73. package/dist/generated/client.js +35 -0
  74. package/dist/generated/commonInputTypes.js +10 -0
  75. package/dist/generated/enums.js +30 -0
  76. package/dist/generated/internal/class.js +41 -0
  77. package/dist/generated/internal/prismaNamespace.js +239 -0
  78. package/dist/generated/internal/prismaNamespaceBrowser.js +209 -0
  79. package/dist/generated/models/bot_api_keys.js +1 -0
  80. package/dist/generated/models/bot_tokens.js +1 -0
  81. package/dist/generated/models/channel_agents.js +1 -0
  82. package/dist/generated/models/channel_directories.js +1 -0
  83. package/dist/generated/models/channel_mention_mode.js +1 -0
  84. package/dist/generated/models/channel_models.js +1 -0
  85. package/dist/generated/models/channel_verbosity.js +1 -0
  86. package/dist/generated/models/channel_worktrees.js +1 -0
  87. package/dist/generated/models/forum_sync_configs.js +1 -0
  88. package/dist/generated/models/global_models.js +1 -0
  89. package/dist/generated/models/ipc_requests.js +1 -0
  90. package/dist/generated/models/part_messages.js +1 -0
  91. package/dist/generated/models/scheduled_tasks.js +1 -0
  92. package/dist/generated/models/session_agents.js +1 -0
  93. package/dist/generated/models/session_models.js +1 -0
  94. package/dist/generated/models/session_start_sources.js +1 -0
  95. package/dist/generated/models/thread_sessions.js +1 -0
  96. package/dist/generated/models/thread_worktrees.js +1 -0
  97. package/dist/generated/models.js +1 -0
  98. package/dist/heap-monitor.js +95 -0
  99. package/dist/hrana-server.js +416 -0
  100. package/dist/hrana-server.test.js +368 -0
  101. package/dist/image-utils.js +112 -0
  102. package/dist/interaction-handler.js +327 -0
  103. package/dist/ipc-polling.js +251 -0
  104. package/dist/kimaki-digital-twin.e2e.test.js +165 -0
  105. package/dist/limit-heading-depth.js +25 -0
  106. package/dist/limit-heading-depth.test.js +105 -0
  107. package/dist/logger.js +160 -0
  108. package/dist/markdown.js +342 -0
  109. package/dist/markdown.test.js +253 -0
  110. package/dist/message-formatting.js +433 -0
  111. package/dist/message-formatting.test.js +73 -0
  112. package/dist/openai-realtime.js +228 -0
  113. package/dist/opencode-plugin-loading.e2e.test.js +91 -0
  114. package/dist/opencode-plugin.js +536 -0
  115. package/dist/opencode-plugin.test.js +98 -0
  116. package/dist/opencode.js +409 -0
  117. package/dist/privacy-sanitizer.js +105 -0
  118. package/dist/runtime-mode.js +51 -0
  119. package/dist/runtime-mode.test.js +115 -0
  120. package/dist/sentry.js +127 -0
  121. package/dist/session-handler/state.js +151 -0
  122. package/dist/session-handler.js +1874 -0
  123. package/dist/session-search.js +100 -0
  124. package/dist/session-search.test.js +40 -0
  125. package/dist/startup-service.js +153 -0
  126. package/dist/system-message.js +499 -0
  127. package/dist/task-runner.js +282 -0
  128. package/dist/task-schedule.js +191 -0
  129. package/dist/task-schedule.test.js +71 -0
  130. package/dist/thinking-utils.js +35 -0
  131. package/dist/thread-message-queue.e2e.test.js +781 -0
  132. package/dist/tools.js +359 -0
  133. package/dist/unnest-code-blocks.js +136 -0
  134. package/dist/unnest-code-blocks.test.js +641 -0
  135. package/dist/upgrade.js +114 -0
  136. package/dist/utils.js +109 -0
  137. package/dist/voice-handler.js +606 -0
  138. package/dist/voice.js +304 -0
  139. package/dist/voice.test.js +187 -0
  140. package/dist/wait-session.js +94 -0
  141. package/dist/worker-types.js +4 -0
  142. package/dist/worktree-utils.js +727 -0
  143. package/dist/xml.js +92 -0
  144. package/dist/xml.test.js +32 -0
  145. package/package.json +82 -0
  146. package/schema.prisma +246 -0
  147. package/skills/batch/SKILL.md +87 -0
  148. package/skills/critique/SKILL.md +129 -0
  149. package/skills/errore/SKILL.md +589 -0
  150. package/skills/goke/.prettierrc +5 -0
  151. package/skills/goke/CHANGELOG.md +40 -0
  152. package/skills/goke/LICENSE +21 -0
  153. package/skills/goke/README.md +666 -0
  154. package/skills/goke/SKILL.md +458 -0
  155. package/skills/goke/package.json +43 -0
  156. package/skills/goke/src/__test__/coerce.test.ts +411 -0
  157. package/skills/goke/src/__test__/index.test.ts +1798 -0
  158. package/skills/goke/src/__test__/types.test-d.ts +111 -0
  159. package/skills/goke/src/coerce.ts +547 -0
  160. package/skills/goke/src/goke.ts +1362 -0
  161. package/skills/goke/src/index.ts +16 -0
  162. package/skills/goke/src/mri.ts +164 -0
  163. package/skills/goke/tsconfig.json +15 -0
  164. package/skills/jitter/EDITOR.md +219 -0
  165. package/skills/jitter/EXPORT-INTERNALS.md +309 -0
  166. package/skills/jitter/SKILL.md +158 -0
  167. package/skills/jitter/jitter-clipboard.json +1042 -0
  168. package/skills/jitter/package.json +14 -0
  169. package/skills/jitter/tsconfig.json +15 -0
  170. package/skills/jitter/utils/actions.ts +212 -0
  171. package/skills/jitter/utils/export.ts +114 -0
  172. package/skills/jitter/utils/index.ts +141 -0
  173. package/skills/jitter/utils/snapshot.ts +154 -0
  174. package/skills/jitter/utils/traverse.ts +246 -0
  175. package/skills/jitter/utils/types.ts +279 -0
  176. package/skills/jitter/utils/wait.ts +133 -0
  177. package/skills/playwriter/SKILL.md +31 -0
  178. package/skills/security-review/SKILL.md +208 -0
  179. package/skills/simplify/SKILL.md +58 -0
  180. package/skills/termcast/SKILL.md +945 -0
  181. package/skills/tuistory/SKILL.md +250 -0
  182. package/skills/zustand-centralized-state/SKILL.md +582 -0
  183. package/src/__snapshots__/compact-session-context-no-system.md +35 -0
  184. package/src/__snapshots__/compact-session-context.md +41 -0
  185. package/src/__snapshots__/first-session-no-info.md +17 -0
  186. package/src/__snapshots__/first-session-with-info.md +23 -0
  187. package/src/__snapshots__/session-1.md +17 -0
  188. package/src/__snapshots__/session-2.md +5871 -0
  189. package/src/__snapshots__/session-3.md +17 -0
  190. package/src/__snapshots__/session-with-tools.md +5871 -0
  191. package/src/ai-tool-to-genai.test.ts +296 -0
  192. package/src/ai-tool-to-genai.ts +282 -0
  193. package/src/ai-tool.ts +39 -0
  194. package/src/bin.ts +108 -0
  195. package/src/bot-token.test.ts +171 -0
  196. package/src/bot-token.ts +159 -0
  197. package/src/channel-management.ts +172 -0
  198. package/src/cli-parsing.test.ts +132 -0
  199. package/src/cli.ts +3605 -0
  200. package/src/commands/abort.ts +112 -0
  201. package/src/commands/action-buttons.ts +376 -0
  202. package/src/commands/add-project.ts +152 -0
  203. package/src/commands/agent.ts +404 -0
  204. package/src/commands/ask-question.ts +330 -0
  205. package/src/commands/compact.ts +157 -0
  206. package/src/commands/context-usage.ts +199 -0
  207. package/src/commands/create-new-project.ts +179 -0
  208. package/src/commands/diff.ts +165 -0
  209. package/src/commands/file-upload.ts +389 -0
  210. package/src/commands/fork.ts +320 -0
  211. package/src/commands/gemini-apikey.ts +104 -0
  212. package/src/commands/login.ts +634 -0
  213. package/src/commands/mention-mode.ts +77 -0
  214. package/src/commands/merge-worktree.ts +177 -0
  215. package/src/commands/model.ts +961 -0
  216. package/src/commands/permissions.ts +261 -0
  217. package/src/commands/queue.ts +296 -0
  218. package/src/commands/remove-project.ts +155 -0
  219. package/src/commands/restart-opencode-server.ts +162 -0
  220. package/src/commands/resume.ts +242 -0
  221. package/src/commands/run-command.ts +123 -0
  222. package/src/commands/session-id.ts +109 -0
  223. package/src/commands/session.ts +250 -0
  224. package/src/commands/share.ts +106 -0
  225. package/src/commands/types.ts +25 -0
  226. package/src/commands/undo-redo.ts +221 -0
  227. package/src/commands/unset-model.ts +189 -0
  228. package/src/commands/upgrade.ts +52 -0
  229. package/src/commands/user-command.ts +193 -0
  230. package/src/commands/verbosity.ts +88 -0
  231. package/src/commands/worktree-settings.ts +79 -0
  232. package/src/commands/worktree.ts +431 -0
  233. package/src/condense-memory.ts +36 -0
  234. package/src/config.ts +148 -0
  235. package/src/database.ts +1530 -0
  236. package/src/db.test.ts +60 -0
  237. package/src/db.ts +190 -0
  238. package/src/discord-api.ts +35 -0
  239. package/src/discord-bot.ts +1316 -0
  240. package/src/discord-utils.test.ts +132 -0
  241. package/src/discord-utils.ts +767 -0
  242. package/src/errors.ts +213 -0
  243. package/src/escape-backticks.test.ts +469 -0
  244. package/src/format-tables.test.ts +223 -0
  245. package/src/format-tables.ts +145 -0
  246. package/src/forum-sync/config.ts +92 -0
  247. package/src/forum-sync/discord-operations.ts +241 -0
  248. package/src/forum-sync/index.ts +9 -0
  249. package/src/forum-sync/markdown.ts +176 -0
  250. package/src/forum-sync/sync-to-discord.ts +595 -0
  251. package/src/forum-sync/sync-to-files.ts +294 -0
  252. package/src/forum-sync/types.ts +175 -0
  253. package/src/forum-sync/watchers.ts +454 -0
  254. package/src/genai-worker-wrapper.ts +164 -0
  255. package/src/genai-worker.ts +386 -0
  256. package/src/genai.ts +321 -0
  257. package/src/generated/browser.ts +109 -0
  258. package/src/generated/client.ts +131 -0
  259. package/src/generated/commonInputTypes.ts +512 -0
  260. package/src/generated/enums.ts +46 -0
  261. package/src/generated/internal/class.ts +362 -0
  262. package/src/generated/internal/prismaNamespace.ts +2251 -0
  263. package/src/generated/internal/prismaNamespaceBrowser.ts +308 -0
  264. package/src/generated/models/bot_api_keys.ts +1288 -0
  265. package/src/generated/models/bot_tokens.ts +1577 -0
  266. package/src/generated/models/channel_agents.ts +1256 -0
  267. package/src/generated/models/channel_directories.ts +2104 -0
  268. package/src/generated/models/channel_mention_mode.ts +1300 -0
  269. package/src/generated/models/channel_models.ts +1288 -0
  270. package/src/generated/models/channel_verbosity.ts +1224 -0
  271. package/src/generated/models/channel_worktrees.ts +1308 -0
  272. package/src/generated/models/forum_sync_configs.ts +1452 -0
  273. package/src/generated/models/global_models.ts +1288 -0
  274. package/src/generated/models/ipc_requests.ts +1485 -0
  275. package/src/generated/models/part_messages.ts +1302 -0
  276. package/src/generated/models/scheduled_tasks.ts +2320 -0
  277. package/src/generated/models/session_agents.ts +1086 -0
  278. package/src/generated/models/session_models.ts +1114 -0
  279. package/src/generated/models/session_start_sources.ts +1408 -0
  280. package/src/generated/models/thread_sessions.ts +1599 -0
  281. package/src/generated/models/thread_worktrees.ts +1352 -0
  282. package/src/generated/models.ts +29 -0
  283. package/src/heap-monitor.ts +121 -0
  284. package/src/hrana-server.test.ts +428 -0
  285. package/src/hrana-server.ts +547 -0
  286. package/src/image-utils.ts +149 -0
  287. package/src/interaction-handler.ts +461 -0
  288. package/src/ipc-polling.ts +325 -0
  289. package/src/kimaki-digital-twin.e2e.test.ts +201 -0
  290. package/src/limit-heading-depth.test.ts +116 -0
  291. package/src/limit-heading-depth.ts +26 -0
  292. package/src/logger.ts +203 -0
  293. package/src/markdown.test.ts +360 -0
  294. package/src/markdown.ts +410 -0
  295. package/src/message-formatting.test.ts +81 -0
  296. package/src/message-formatting.ts +549 -0
  297. package/src/openai-realtime.ts +362 -0
  298. package/src/opencode-plugin-loading.e2e.test.ts +112 -0
  299. package/src/opencode-plugin.test.ts +108 -0
  300. package/src/opencode-plugin.ts +652 -0
  301. package/src/opencode.ts +554 -0
  302. package/src/privacy-sanitizer.ts +142 -0
  303. package/src/schema.sql +158 -0
  304. package/src/sentry.ts +137 -0
  305. package/src/session-handler/state.ts +232 -0
  306. package/src/session-handler.ts +2668 -0
  307. package/src/session-search.test.ts +50 -0
  308. package/src/session-search.ts +148 -0
  309. package/src/startup-service.ts +200 -0
  310. package/src/system-message.ts +568 -0
  311. package/src/task-runner.ts +425 -0
  312. package/src/task-schedule.test.ts +84 -0
  313. package/src/task-schedule.ts +287 -0
  314. package/src/thinking-utils.ts +61 -0
  315. package/src/thread-message-queue.e2e.test.ts +997 -0
  316. package/src/tools.ts +432 -0
  317. package/src/unnest-code-blocks.test.ts +679 -0
  318. package/src/unnest-code-blocks.ts +168 -0
  319. package/src/upgrade.ts +127 -0
  320. package/src/utils.ts +145 -0
  321. package/src/voice-handler.ts +852 -0
  322. package/src/voice.test.ts +219 -0
  323. package/src/voice.ts +444 -0
  324. package/src/wait-session.ts +147 -0
  325. package/src/worker-types.ts +64 -0
  326. package/src/worktree-utils.ts +988 -0
  327. package/src/xml.test.ts +38 -0
  328. package/src/xml.ts +121 -0
@@ -0,0 +1,362 @@
1
+ /* eslint-disable @typescript-eslint/ban-ts-comment */
2
+ /* istanbul ignore file */
3
+ // @ts-nocheck
4
+
5
+ import { RealtimeClient } from '@openai/realtime-api-beta'
6
+ import { writeFile } from 'fs'
7
+ import { createLogger, LogPrefix } from './logger.js'
8
+
9
+ const openaiLogger = createLogger(LogPrefix.OPENAI)
10
+
11
+ // Export the session type for reuse
12
+ export interface OpenAIRealtimeSession {
13
+ send: (audioData: ArrayBuffer) => void
14
+ sendText: (text: string) => void
15
+ close: () => void
16
+ }
17
+
18
+ // Type definitions based on @openai/realtime-api-beta
19
+ interface ConversationItem {
20
+ id: string
21
+ object: string
22
+ type: 'message' | 'function_call' | 'function_call_output'
23
+ status: 'in_progress' | 'completed' | 'incomplete'
24
+ role?: 'user' | 'assistant' | 'system'
25
+ content?: Array<{
26
+ type: string
27
+ text?: string
28
+ audio?: string
29
+ transcript?: string | null
30
+ }>
31
+ formatted: {
32
+ audio?: Int16Array
33
+ text?: string
34
+ transcript?: string
35
+ tool?: {
36
+ type: 'function'
37
+ name: string
38
+ call_id: string
39
+ arguments: string
40
+ }
41
+ output?: string
42
+ }
43
+ }
44
+
45
+ interface ConversationEventDelta {
46
+ audio?: Int16Array
47
+ text?: string
48
+ transcript?: string
49
+ arguments?: string
50
+ }
51
+
52
+ const audioParts: Buffer[] = []
53
+
54
+ function saveBinaryFile(fileName: string, content: Buffer) {
55
+ writeFile(fileName, content, 'utf8', (err) => {
56
+ if (err) {
57
+ openaiLogger.error(`Error writing file ${fileName}:`, err)
58
+ return
59
+ }
60
+ openaiLogger.log(`Appending stream content to file ${fileName}.`)
61
+ })
62
+ }
63
+
64
+ interface WavConversionOptions {
65
+ numChannels: number
66
+ sampleRate: number
67
+ bitsPerSample: number
68
+ }
69
+
70
+ function convertToWav(rawData: Buffer[], mimeType: string) {
71
+ const options = parseMimeType(mimeType)
72
+ const dataLength = rawData.reduce((a, b) => a + b.length, 0)
73
+ const wavHeader = createWavHeader(dataLength, options)
74
+ const buffer = Buffer.concat(rawData)
75
+
76
+ return Buffer.concat([wavHeader, buffer])
77
+ }
78
+
79
+ function parseMimeType(mimeType: string) {
80
+ const [fileType, ...params] = mimeType.split(';').map((s) => s.trim())
81
+ const [_, format] = fileType?.split('/') || []
82
+
83
+ const options: Partial<WavConversionOptions> = {
84
+ numChannels: 1,
85
+ bitsPerSample: 16,
86
+ }
87
+
88
+ if (format && format.startsWith('L')) {
89
+ const bits = parseInt(format.slice(1), 10)
90
+ if (!isNaN(bits)) {
91
+ options.bitsPerSample = bits
92
+ }
93
+ }
94
+
95
+ for (const param of params) {
96
+ const [key, value] = param.split('=').map((s) => s.trim())
97
+ if (key === 'rate') {
98
+ options.sampleRate = parseInt(value || '', 10)
99
+ }
100
+ }
101
+
102
+ return options as WavConversionOptions
103
+ }
104
+
105
+ function createWavHeader(dataLength: number, options: WavConversionOptions) {
106
+ const { numChannels, sampleRate, bitsPerSample } = options
107
+
108
+ // http://soundfile.sapp.org/doc/WaveFormat
109
+
110
+ const byteRate = (sampleRate * numChannels * bitsPerSample) / 8
111
+ const blockAlign = (numChannels * bitsPerSample) / 8
112
+ const buffer = Buffer.alloc(44)
113
+
114
+ buffer.write('RIFF', 0) // ChunkID
115
+ buffer.writeUInt32LE(36 + dataLength, 4) // ChunkSize
116
+ buffer.write('WAVE', 8) // Format
117
+ buffer.write('fmt ', 12) // Subchunk1ID
118
+ buffer.writeUInt32LE(16, 16) // Subchunk1Size (PCM)
119
+ buffer.writeUInt16LE(1, 20) // AudioFormat (1 = PCM)
120
+ buffer.writeUInt16LE(numChannels, 22) // NumChannels
121
+ buffer.writeUInt32LE(sampleRate, 24) // SampleRate
122
+ buffer.writeUInt32LE(byteRate, 28) // ByteRate
123
+ buffer.writeUInt16LE(blockAlign, 32) // BlockAlign
124
+ buffer.writeUInt16LE(bitsPerSample, 34) // BitsPerSample
125
+ buffer.write('data', 36) // Subchunk2ID
126
+ buffer.writeUInt32LE(dataLength, 40) // Subchunk2Size
127
+
128
+ return buffer
129
+ }
130
+
131
+ function defaultAudioChunkHandler({
132
+ data,
133
+ mimeType,
134
+ }: {
135
+ data: Buffer
136
+ mimeType: string
137
+ }) {
138
+ audioParts.push(data)
139
+ const fileName = 'audio.wav'
140
+ const buffer = convertToWav(audioParts, mimeType)
141
+ saveBinaryFile(fileName, buffer)
142
+ }
143
+
144
+ export interface GenAISessionResult {
145
+ session: OpenAIRealtimeSession
146
+ stop: () => void
147
+ }
148
+
149
+ export async function startGenAiSession({
150
+ onAssistantAudioChunk,
151
+ onAssistantStartSpeaking,
152
+ onAssistantStopSpeaking,
153
+ onAssistantInterruptSpeaking,
154
+ systemMessage,
155
+ tools,
156
+ }: {
157
+ onAssistantAudioChunk?: (args: { data: Buffer; mimeType: string }) => void
158
+ onAssistantStartSpeaking?: () => void
159
+ onAssistantStopSpeaking?: () => void
160
+ onAssistantInterruptSpeaking?: () => void
161
+ systemMessage?: string
162
+ // Accept tools but use structural typing to avoid variance issues
163
+ tools?: Record<
164
+ string,
165
+ {
166
+ description?: string
167
+ inputSchema?: unknown
168
+ execute?: Function
169
+ }
170
+ >
171
+ } = {}): Promise<GenAISessionResult> {
172
+ if (!process.env.OPENAI_API_KEY) {
173
+ throw new Error('OPENAI_API_KEY environment variable is required')
174
+ }
175
+
176
+ const client = new RealtimeClient({
177
+ apiKey: process.env.OPENAI_API_KEY,
178
+ })
179
+
180
+ const audioChunkHandler = onAssistantAudioChunk || defaultAudioChunkHandler
181
+ let isAssistantSpeaking = false
182
+
183
+ // Configure session with 24kHz sample rate
184
+ client.updateSession({
185
+ instructions: systemMessage || '',
186
+ voice: 'alloy',
187
+ input_audio_format: 'pcm16',
188
+ output_audio_format: 'pcm16',
189
+ input_audio_transcription: { model: 'whisper-1' },
190
+ turn_detection: { type: 'server_vad' },
191
+ modalities: ['text', 'audio'],
192
+ temperature: 0.8,
193
+ })
194
+
195
+ // Add tools if provided
196
+ if (tools) {
197
+ for (const [name, tool] of Object.entries(tools)) {
198
+ // Convert AI SDK tool to OpenAI Realtime format
199
+ // The tool.inputSchema is a Zod schema, we need to convert it to JSON Schema
200
+ let parameters: Record<string, unknown> = {
201
+ type: 'object',
202
+ properties: {},
203
+ required: [],
204
+ }
205
+
206
+ // If the tool has a Zod schema, we can try to extract basic structure
207
+ // For now, we'll use a simple placeholder
208
+ if (tool.description?.includes('session')) {
209
+ parameters = {
210
+ type: 'object',
211
+ properties: {
212
+ sessionId: { type: 'string', description: 'The session ID' },
213
+ message: { type: 'string', description: 'The message text' },
214
+ },
215
+ required: ['sessionId'],
216
+ }
217
+ }
218
+
219
+ client.addTool(
220
+ {
221
+ type: 'function',
222
+ name,
223
+ description: tool.description || '',
224
+ parameters,
225
+ },
226
+ async (params: Record<string, unknown>) => {
227
+ try {
228
+ if (!tool.execute || typeof tool.execute !== 'function') {
229
+ return { error: 'Tool execute function not found' }
230
+ }
231
+ // Call the execute function with params
232
+ // The Tool type from 'ai' expects (input, options) but we need to handle this safely
233
+ const result = await tool.execute(params, {
234
+ abortSignal: new AbortController().signal,
235
+ toolCallId: '',
236
+ messages: [],
237
+ })
238
+ return result
239
+ } catch (error) {
240
+ openaiLogger.error(`Tool ${name} execution error:`, error)
241
+ return { error: String(error) }
242
+ }
243
+ },
244
+ )
245
+ }
246
+ }
247
+
248
+ // Set up event handlers
249
+ client.on(
250
+ 'conversation.item.created',
251
+ ({ item }: { item: ConversationItem }) => {
252
+ if (
253
+ 'role' in item &&
254
+ item.role === 'assistant' &&
255
+ item.type === 'message'
256
+ ) {
257
+ // Check if this is the first audio content
258
+ const hasAudio =
259
+ 'content' in item &&
260
+ Array.isArray(item.content) &&
261
+ item.content.some((c) => 'type' in c && c.type === 'audio')
262
+ if (hasAudio && !isAssistantSpeaking && onAssistantStartSpeaking) {
263
+ isAssistantSpeaking = true
264
+ onAssistantStartSpeaking()
265
+ }
266
+ }
267
+ },
268
+ )
269
+
270
+ client.on(
271
+ 'conversation.updated',
272
+ ({
273
+ item,
274
+ delta,
275
+ }: {
276
+ item: ConversationItem
277
+ delta: ConversationEventDelta | null
278
+ }) => {
279
+ // Handle audio chunks
280
+ if (delta?.audio && 'role' in item && item.role === 'assistant') {
281
+ if (!isAssistantSpeaking && onAssistantStartSpeaking) {
282
+ isAssistantSpeaking = true
283
+ onAssistantStartSpeaking()
284
+ }
285
+
286
+ // OpenAI provides audio as Int16Array or base64
287
+ let audioBuffer: Buffer
288
+ if (delta.audio instanceof Int16Array) {
289
+ audioBuffer = Buffer.from(delta.audio.buffer)
290
+ } else {
291
+ // Assume base64 string
292
+ audioBuffer = Buffer.from(delta.audio, 'base64')
293
+ }
294
+
295
+ // OpenAI uses 24kHz PCM16 format
296
+ audioChunkHandler({
297
+ data: audioBuffer,
298
+ mimeType: 'audio/pcm;rate=24000',
299
+ })
300
+ }
301
+
302
+ // Handle transcriptions
303
+ if (delta?.transcript) {
304
+ if ('role' in item) {
305
+ if (item.role === 'user') {
306
+ openaiLogger.log('User transcription:', delta.transcript)
307
+ } else if (item.role === 'assistant') {
308
+ openaiLogger.log('Assistant transcription:', delta.transcript)
309
+ }
310
+ }
311
+ }
312
+ },
313
+ )
314
+
315
+ client.on(
316
+ 'conversation.item.completed',
317
+ ({ item }: { item: ConversationItem }) => {
318
+ if (
319
+ 'role' in item &&
320
+ item.role === 'assistant' &&
321
+ isAssistantSpeaking &&
322
+ onAssistantStopSpeaking
323
+ ) {
324
+ isAssistantSpeaking = false
325
+ onAssistantStopSpeaking()
326
+ }
327
+ },
328
+ )
329
+
330
+ client.on('conversation.interrupted', () => {
331
+ openaiLogger.log('Assistant was interrupted')
332
+ if (isAssistantSpeaking && onAssistantInterruptSpeaking) {
333
+ isAssistantSpeaking = false
334
+ onAssistantInterruptSpeaking()
335
+ }
336
+ })
337
+
338
+ // Connect to the Realtime API
339
+ await client.connect()
340
+
341
+ const sessionResult: GenAISessionResult = {
342
+ session: {
343
+ send: (audioData: ArrayBuffer) => {
344
+ // Convert ArrayBuffer to Int16Array for OpenAI
345
+ const int16Data = new Int16Array(audioData)
346
+ client.appendInputAudio(int16Data)
347
+ },
348
+ sendText: (text: string) => {
349
+ // Send text message to OpenAI
350
+ client.sendUserMessageContent([{ type: 'input_text', text }])
351
+ },
352
+ close: () => {
353
+ client.disconnect()
354
+ },
355
+ },
356
+ stop: () => {
357
+ client.disconnect()
358
+ },
359
+ }
360
+
361
+ return sessionResult
362
+ }
@@ -0,0 +1,112 @@
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
+
6
+ import { spawn, type ChildProcess } from 'node:child_process'
7
+ import fs from 'node:fs'
8
+ import net from 'node:net'
9
+ import path from 'node:path'
10
+ import { fileURLToPath } from 'node:url'
11
+ import { test, expect } from 'vitest'
12
+
13
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
14
+
15
+ async function getOpenPort(): Promise<number> {
16
+ return new Promise((resolve, reject) => {
17
+ const server = net.createServer()
18
+ server.listen(0, () => {
19
+ const address = server.address()
20
+ if (address && typeof address === 'object') {
21
+ server.close(() => {
22
+ resolve(address.port)
23
+ })
24
+ } else {
25
+ reject(new Error('Failed to get port'))
26
+ }
27
+ })
28
+ server.on('error', reject)
29
+ })
30
+ }
31
+
32
+ async function waitForHealth({
33
+ port,
34
+ maxAttempts = 30,
35
+ }: {
36
+ port: number
37
+ maxAttempts?: number
38
+ }): Promise<boolean> {
39
+ for (let i = 0; i < maxAttempts; i++) {
40
+ try {
41
+ const response = await fetch(`http://127.0.0.1:${port}/api/health`)
42
+ if (response.status < 500) {
43
+ return true
44
+ }
45
+ } catch {
46
+ // connection refused, retry
47
+ }
48
+ await new Promise((resolve) => {
49
+ setTimeout(resolve, 1000)
50
+ })
51
+ }
52
+ return false
53
+ }
54
+
55
+ test(
56
+ 'opencode server loads plugin without errors',
57
+ async () => {
58
+ const projectDir = path.resolve(process.cwd(), 'tmp', 'plugin-loading-e2e')
59
+ fs.mkdirSync(projectDir, { recursive: true })
60
+
61
+ const port = await getOpenPort()
62
+ const pluginPath = new URL('../src/opencode-plugin.ts', import.meta.url).href
63
+ const stderrLines: string[] = []
64
+
65
+ const serverProcess: ChildProcess = spawn(
66
+ process.env.OPENCODE_PATH || 'opencode',
67
+ ['serve', '--port', port.toString(), '--print-logs', '--log-level', 'DEBUG'],
68
+ {
69
+ stdio: 'pipe',
70
+ cwd: projectDir,
71
+ shell: true,
72
+ env: {
73
+ ...process.env,
74
+ OPENCODE_CONFIG_CONTENT: JSON.stringify({
75
+ $schema: 'https://opencode.ai/config.json',
76
+ lsp: false,
77
+ formatter: false,
78
+ plugin: [pluginPath],
79
+ }),
80
+ },
81
+ },
82
+ )
83
+
84
+ serverProcess.stderr?.on('data', (data) => {
85
+ stderrLines.push(...data.toString().split('\n').filter(Boolean))
86
+ })
87
+
88
+ try {
89
+ const healthy = await waitForHealth({ port })
90
+ expect(healthy).toBe(true)
91
+
92
+ // Check no plugin-related errors in stderr
93
+ const pluginErrorPatterns = [
94
+ /plugin.*error/i,
95
+ /failed to load plugin/i,
96
+ /cannot find module/i,
97
+ /ERR_MODULE_NOT_FOUND/i,
98
+ /plugin.*failed/i,
99
+ /plugin.*crash/i,
100
+ ]
101
+ const errorLines = stderrLines.filter((line) => {
102
+ return pluginErrorPatterns.some((pattern) => {
103
+ return pattern.test(line)
104
+ })
105
+ })
106
+ expect(errorLines).toEqual([])
107
+ } finally {
108
+ serverProcess.kill('SIGTERM')
109
+ }
110
+ },
111
+ 60_000,
112
+ )
@@ -0,0 +1,108 @@
1
+ import { test, expect, describe } from 'vitest'
2
+ import { condenseMemoryMd } from './condense-memory.js'
3
+
4
+ describe('condenseMemoryMd', () => {
5
+ test('multiple headings with body content', () => {
6
+ const content = [
7
+ '# Project Overview',
8
+ '',
9
+ 'This is a big project with many things.',
10
+ 'It does X, Y, and Z.',
11
+ '',
12
+ '## Auth Architecture',
13
+ '',
14
+ 'JWT tokens with 15min expiry.',
15
+ 'Refresh tokens in httpOnly cookies.',
16
+ 'Session stored in Redis.',
17
+ '',
18
+ '## User Preferences',
19
+ '',
20
+ '- kebab-case filenames',
21
+ '- errore-style errors',
22
+ '- no emojis',
23
+ '',
24
+ '### API Conventions',
25
+ '',
26
+ 'All routes return { data, error }.',
27
+ 'Use spiceflow for the server.',
28
+ '',
29
+ ].join('\n')
30
+
31
+ expect(condenseMemoryMd(content)).toMatchInlineSnapshot(`
32
+ "1: # Project Overview
33
+ ...
34
+ 6: ## Auth Architecture
35
+ ...
36
+ 12: ## User Preferences
37
+ ...
38
+ 18: ### API Conventions
39
+ ..."
40
+ `)
41
+ })
42
+
43
+ test('body text before first heading', () => {
44
+ const content = [
45
+ 'Some preamble notes.',
46
+ '',
47
+ '# First Heading',
48
+ '',
49
+ 'Content here.',
50
+ '',
51
+ ].join('\n')
52
+
53
+ expect(condenseMemoryMd(content)).toMatchInlineSnapshot(`
54
+ "...
55
+ 3: # First Heading
56
+ ..."
57
+ `)
58
+ })
59
+
60
+ test('no headings at all', () => {
61
+ const content = 'Just some notes.\nMore notes.\n'
62
+ expect(condenseMemoryMd(content)).toMatchInlineSnapshot(`"..."`)
63
+ })
64
+
65
+ test('empty content', () => {
66
+ expect(condenseMemoryMd('')).toMatchInlineSnapshot(`""`)
67
+ })
68
+
69
+ test('consecutive headings without body', () => {
70
+ const content = [
71
+ '# H1',
72
+ '## H2',
73
+ '### H3',
74
+ '',
75
+ 'Some body.',
76
+ '',
77
+ ].join('\n')
78
+
79
+ expect(condenseMemoryMd(content)).toMatchInlineSnapshot(`
80
+ "1: # H1
81
+ 2: ## H2
82
+ 3: ### H3
83
+ ..."
84
+ `)
85
+ })
86
+
87
+ test('heading with code block body', () => {
88
+ const content = [
89
+ '# Config',
90
+ '',
91
+ '```json',
92
+ '{ "key": "value" }',
93
+ '```',
94
+ '',
95
+ '## Notes',
96
+ '',
97
+ 'Some text.',
98
+ '',
99
+ ].join('\n')
100
+
101
+ expect(condenseMemoryMd(content)).toMatchInlineSnapshot(`
102
+ "1: # Config
103
+ ...
104
+ 7: ## Notes
105
+ ..."
106
+ `)
107
+ })
108
+ })