@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,368 @@
1
+ import fs from 'node:fs';
2
+ import http from 'node:http';
3
+ import path from 'node:path';
4
+ import crypto from 'node:crypto';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { describe, test, expect, afterAll } from 'vitest';
7
+ import Database from 'libsql';
8
+ import { PrismaLibSql } from '@prisma/adapter-libsql';
9
+ import { PrismaClient } from './generated/client.js';
10
+ import { createHranaHandler } from './hrana-server.js';
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = path.dirname(__filename);
13
+ async function migrateSchema(prisma) {
14
+ const schemaPath = path.join(__dirname, '../src/schema.sql');
15
+ const sql = fs.readFileSync(schemaPath, 'utf-8');
16
+ const statements = sql
17
+ .split(';')
18
+ .map((s) => s
19
+ .split('\n')
20
+ .filter((line) => !line.trimStart().startsWith('--'))
21
+ .join('\n')
22
+ .trim())
23
+ .filter((s) => s.length > 0 &&
24
+ !/^CREATE\s+TABLE\s+["']?sqlite_sequence["']?\s*\(/i.test(s))
25
+ .map((s) => s
26
+ .replace(/^CREATE\s+UNIQUE\s+INDEX\b(?!\s+IF)/i, 'CREATE UNIQUE INDEX IF NOT EXISTS')
27
+ .replace(/^CREATE\s+INDEX\b(?!\s+IF)/i, 'CREATE INDEX IF NOT EXISTS'));
28
+ for (const statement of statements) {
29
+ await prisma.$executeRawUnsafe(statement);
30
+ }
31
+ }
32
+ describe('hrana-server', () => {
33
+ let testServer = null;
34
+ let testDb = null;
35
+ let prisma = null;
36
+ const dbPath = path.join(process.cwd(), `tmp/test-hrana-${crypto.randomUUID().slice(0, 8)}.db`);
37
+ afterAll(async () => {
38
+ if (prisma)
39
+ await prisma.$disconnect();
40
+ if (testServer)
41
+ await new Promise((resolve) => {
42
+ testServer.close(() => {
43
+ resolve();
44
+ });
45
+ });
46
+ if (testDb)
47
+ testDb.close();
48
+ try {
49
+ fs.unlinkSync(dbPath);
50
+ }
51
+ catch (e) {
52
+ console.warn('cleanup:', dbPath, e.message);
53
+ }
54
+ try {
55
+ fs.unlinkSync(dbPath + '-wal');
56
+ }
57
+ catch (e) {
58
+ console.warn('cleanup:', dbPath + '-wal', e.message);
59
+ }
60
+ try {
61
+ fs.unlinkSync(dbPath + '-shm');
62
+ }
63
+ catch (e) {
64
+ console.warn('cleanup:', dbPath + '-shm', e.message);
65
+ }
66
+ });
67
+ test('prisma CRUD through hrana server', async () => {
68
+ fs.mkdirSync(path.dirname(dbPath), { recursive: true });
69
+ const database = new Database(dbPath);
70
+ database.exec('PRAGMA journal_mode = WAL');
71
+ database.exec('PRAGMA busy_timeout = 5000');
72
+ testDb = database;
73
+ const port = 10000 + Math.floor(Math.random() * 50000);
74
+ await new Promise((resolve, reject) => {
75
+ const srv = http.createServer(createHranaHandler(database));
76
+ srv.on('error', reject);
77
+ srv.listen(port, '127.0.0.1', () => {
78
+ testServer = srv;
79
+ resolve();
80
+ });
81
+ });
82
+ const adapter = new PrismaLibSql({ url: `http://127.0.0.1:${port}` });
83
+ prisma = new PrismaClient({ adapter });
84
+ await migrateSchema(prisma);
85
+ // Create
86
+ const created = await prisma.thread_sessions.create({
87
+ data: {
88
+ thread_id: 'hrana-test-thread',
89
+ session_id: 'hrana-test-session',
90
+ },
91
+ });
92
+ expect(created.thread_id).toMatchInlineSnapshot(`"hrana-test-thread"`);
93
+ expect(created.session_id).toMatchInlineSnapshot(`"hrana-test-session"`);
94
+ // Read
95
+ const found = await prisma.thread_sessions.findUnique({
96
+ where: { thread_id: 'hrana-test-thread' },
97
+ });
98
+ expect(found?.session_id).toMatchInlineSnapshot(`"hrana-test-session"`);
99
+ // Update
100
+ await prisma.thread_sessions.update({
101
+ where: { thread_id: 'hrana-test-thread' },
102
+ data: { session_id: 'updated-session' },
103
+ });
104
+ const updated = await prisma.thread_sessions.findUnique({
105
+ where: { thread_id: 'hrana-test-thread' },
106
+ });
107
+ expect(updated?.session_id).toMatchInlineSnapshot(`"updated-session"`);
108
+ // Delete
109
+ await prisma.thread_sessions.delete({
110
+ where: { thread_id: 'hrana-test-thread' },
111
+ });
112
+ const deleted = await prisma.thread_sessions.findUnique({
113
+ where: { thread_id: 'hrana-test-thread' },
114
+ });
115
+ expect(deleted).toBeNull();
116
+ }, 30_000);
117
+ test('$executeRawUnsafe works for PRAGMAs', async () => {
118
+ if (!prisma)
119
+ throw new Error('prisma not initialized');
120
+ const result = await prisma.$executeRawUnsafe('PRAGMA journal_mode');
121
+ expect(typeof result).toBe('number');
122
+ });
123
+ test('batch transaction via Prisma $transaction', async () => {
124
+ if (!prisma)
125
+ throw new Error('prisma not initialized');
126
+ const [s1, s2] = await prisma.$transaction([
127
+ prisma.thread_sessions.create({
128
+ data: { thread_id: 'batch-1', session_id: 'sess-1' },
129
+ }),
130
+ prisma.thread_sessions.create({
131
+ data: { thread_id: 'batch-2', session_id: 'sess-2' },
132
+ }),
133
+ ]);
134
+ expect(s1.thread_id).toMatchInlineSnapshot(`"batch-1"`);
135
+ expect(s2.thread_id).toMatchInlineSnapshot(`"batch-2"`);
136
+ const count = await prisma.thread_sessions.count({
137
+ where: { thread_id: { in: ['batch-1', 'batch-2'] } },
138
+ });
139
+ expect(count).toBe(2);
140
+ await prisma.thread_sessions.deleteMany({
141
+ where: { thread_id: { in: ['batch-1', 'batch-2'] } },
142
+ });
143
+ }, 30_000);
144
+ test('schema migration DDL via $executeRawUnsafe', async () => {
145
+ if (!prisma)
146
+ throw new Error('prisma not initialized');
147
+ // CREATE TABLE IF NOT EXISTS is idempotent — running migrateSchema again
148
+ // should not throw even though tables already exist.
149
+ await migrateSchema(prisma);
150
+ // Verify DDL actually created the tables by querying sqlite_master
151
+ const tables = await prisma.$queryRawUnsafe(`SELECT name FROM sqlite_master WHERE type='table' ORDER BY name`);
152
+ const tableNames = tables.map((t) => t.name);
153
+ expect(tableNames).toContain('thread_sessions');
154
+ expect(tableNames).toContain('ipc_requests');
155
+ expect(tableNames).toContain('scheduled_tasks');
156
+ // Also verify indexes were created
157
+ const indexes = await prisma.$queryRawUnsafe(`SELECT name FROM sqlite_master WHERE type='index' AND name LIKE '%idx%' ORDER BY name`);
158
+ const indexNames = indexes.map((i) => i.name);
159
+ expect(indexNames).toContain('ipc_requests_status_created_at_idx');
160
+ expect(indexNames).toContain('scheduled_tasks_status_next_run_at_idx');
161
+ // Test CREATE INDEX IF NOT EXISTS is also idempotent
162
+ await prisma.$executeRawUnsafe(`CREATE INDEX IF NOT EXISTS "ipc_requests_status_created_at_idx" ON "ipc_requests"("status", "created_at")`);
163
+ });
164
+ test('concurrent queries via Promise.all', async () => {
165
+ if (!prisma)
166
+ throw new Error('prisma not initialized');
167
+ // Seed some data for concurrent reads
168
+ const threads = Array.from({ length: 5 }, (_, i) => ({
169
+ thread_id: `concurrent-${i}`,
170
+ session_id: `sess-concurrent-${i}`,
171
+ }));
172
+ for (const t of threads) {
173
+ await prisma.thread_sessions.create({ data: t });
174
+ }
175
+ // Simulate kimaki's pattern of parallel Prisma queries
176
+ const [allThreads, count, single, filtered] = await Promise.all([
177
+ prisma.thread_sessions.findMany({
178
+ where: { thread_id: { startsWith: 'concurrent-' } },
179
+ orderBy: { thread_id: 'asc' },
180
+ }),
181
+ prisma.thread_sessions.count({
182
+ where: { thread_id: { startsWith: 'concurrent-' } },
183
+ }),
184
+ prisma.thread_sessions.findUnique({
185
+ where: { thread_id: 'concurrent-2' },
186
+ }),
187
+ prisma.thread_sessions.findMany({
188
+ where: { thread_id: { in: ['concurrent-0', 'concurrent-4'] } },
189
+ orderBy: { thread_id: 'asc' },
190
+ }),
191
+ ]);
192
+ expect(allThreads.length).toBe(5);
193
+ expect(count).toBe(5);
194
+ expect(single?.session_id).toMatchInlineSnapshot(`"sess-concurrent-2"`);
195
+ expect(filtered.map((f) => f.thread_id)).toMatchInlineSnapshot(`
196
+ [
197
+ "concurrent-0",
198
+ "concurrent-4",
199
+ ]
200
+ `);
201
+ // Cleanup
202
+ await prisma.thread_sessions.deleteMany({
203
+ where: { thread_id: { startsWith: 'concurrent-' } },
204
+ });
205
+ }, 30_000);
206
+ test('$queryRawUnsafe for PRAGMAs that return values', async () => {
207
+ if (!prisma)
208
+ throw new Error('prisma not initialized');
209
+ // PRAGMA that returns a value — journal_mode should be WAL
210
+ const journalMode = await prisma.$queryRawUnsafe('PRAGMA journal_mode');
211
+ expect(journalMode[0]?.journal_mode).toMatchInlineSnapshot(`"wal"`);
212
+ // PRAGMA busy_timeout returns the current timeout value
213
+ const busyTimeout = await prisma.$queryRawUnsafe('PRAGMA busy_timeout');
214
+ expect(busyTimeout[0]?.busy_timeout).toMatchInlineSnapshot(`undefined`);
215
+ // PRAGMA table_info returns column metadata
216
+ const tableInfo = await prisma.$queryRawUnsafe(`PRAGMA table_info('ipc_requests')`);
217
+ const colNames = tableInfo.map((c) => c.name);
218
+ expect(colNames).toMatchInlineSnapshot(`
219
+ [
220
+ "id",
221
+ "type",
222
+ "session_id",
223
+ "thread_id",
224
+ "payload",
225
+ "response",
226
+ "status",
227
+ "created_at",
228
+ "updated_at",
229
+ ]
230
+ `);
231
+ });
232
+ test('updateMany with complex WHERE using in operator', async () => {
233
+ if (!prisma)
234
+ throw new Error('prisma not initialized');
235
+ // Seed: create a thread + multiple IPC requests in different statuses
236
+ // (mirrors kimaki's cancelAllPendingIpcRequests pattern)
237
+ await prisma.thread_sessions.create({
238
+ data: { thread_id: 'ipc-test-thread', session_id: 'ipc-test-session' },
239
+ });
240
+ const statuses = ['pending', 'pending', 'processing', 'completed'];
241
+ for (let i = 0; i < statuses.length; i++) {
242
+ await prisma.ipc_requests.create({
243
+ data: {
244
+ id: `ipc-req-${i}`,
245
+ type: 'file_upload',
246
+ session_id: 'ipc-test-session',
247
+ thread_id: 'ipc-test-thread',
248
+ payload: JSON.stringify({ prompt: `test-${i}` }),
249
+ status: statuses[i],
250
+ },
251
+ });
252
+ }
253
+ // updateMany with WHERE status IN ['pending', 'processing']
254
+ const result = await prisma.ipc_requests.updateMany({
255
+ where: { status: { in: ['pending', 'processing'] } },
256
+ data: {
257
+ status: 'cancelled',
258
+ response: JSON.stringify({ error: 'Bot shutting down' }),
259
+ },
260
+ });
261
+ expect(result.count).toBe(3);
262
+ // Verify: only 'completed' row is untouched
263
+ const remaining = await prisma.ipc_requests.findMany({
264
+ where: { thread_id: 'ipc-test-thread' },
265
+ orderBy: { id: 'asc' },
266
+ select: { id: true, status: true },
267
+ });
268
+ expect(remaining).toMatchInlineSnapshot(`
269
+ [
270
+ {
271
+ "id": "ipc-req-0",
272
+ "status": "cancelled",
273
+ },
274
+ {
275
+ "id": "ipc-req-1",
276
+ "status": "cancelled",
277
+ },
278
+ {
279
+ "id": "ipc-req-2",
280
+ "status": "cancelled",
281
+ },
282
+ {
283
+ "id": "ipc-req-3",
284
+ "status": "completed",
285
+ },
286
+ ]
287
+ `);
288
+ // Cleanup
289
+ await prisma.ipc_requests.deleteMany({
290
+ where: { thread_id: 'ipc-test-thread' },
291
+ });
292
+ await prisma.thread_sessions.delete({
293
+ where: { thread_id: 'ipc-test-thread' },
294
+ });
295
+ }, 30_000);
296
+ test('interactive $transaction (callback form)', async () => {
297
+ if (!prisma)
298
+ throw new Error('prisma not initialized');
299
+ // Interactive transaction: reads and writes within the same tx callback.
300
+ // This exercises BEGIN/queries/COMMIT across multiple hrana pipeline
301
+ // requests with batons (stream continuity).
302
+ const result = await prisma.$transaction(async (tx) => {
303
+ await tx.thread_sessions.create({
304
+ data: { thread_id: 'tx-interactive-1', session_id: 'sess-tx-1' },
305
+ });
306
+ await tx.thread_sessions.create({
307
+ data: { thread_id: 'tx-interactive-2', session_id: 'sess-tx-2' },
308
+ });
309
+ // Read inside the same transaction — should see uncommitted rows
310
+ const count = await tx.thread_sessions.count({
311
+ where: { thread_id: { startsWith: 'tx-interactive-' } },
312
+ });
313
+ // Conditional write based on read
314
+ if (count === 2) {
315
+ await tx.thread_sessions.update({
316
+ where: { thread_id: 'tx-interactive-1' },
317
+ data: { session_id: 'sess-tx-1-updated' },
318
+ });
319
+ }
320
+ return tx.thread_sessions.findMany({
321
+ where: { thread_id: { startsWith: 'tx-interactive-' } },
322
+ orderBy: { thread_id: 'asc' },
323
+ select: { thread_id: true, session_id: true },
324
+ });
325
+ });
326
+ expect(result).toMatchInlineSnapshot(`
327
+ [
328
+ {
329
+ "session_id": "sess-tx-1-updated",
330
+ "thread_id": "tx-interactive-1",
331
+ },
332
+ {
333
+ "session_id": "sess-tx-2",
334
+ "thread_id": "tx-interactive-2",
335
+ },
336
+ ]
337
+ `);
338
+ // Verify committed outside transaction
339
+ const outside = await prisma.thread_sessions.count({
340
+ where: { thread_id: { startsWith: 'tx-interactive-' } },
341
+ });
342
+ expect(outside).toBe(2);
343
+ // Cleanup
344
+ await prisma.thread_sessions.deleteMany({
345
+ where: { thread_id: { startsWith: 'tx-interactive-' } },
346
+ });
347
+ }, 30_000);
348
+ test('interactive $transaction rolls back on error', async () => {
349
+ if (!prisma)
350
+ throw new Error('prisma not initialized');
351
+ // Verify rollback: if the callback throws, no rows should be committed
352
+ const txError = await prisma
353
+ .$transaction(async (tx) => {
354
+ await tx.thread_sessions.create({
355
+ data: { thread_id: 'tx-rollback-1', session_id: 'sess-rollback' },
356
+ });
357
+ throw new Error('intentional rollback');
358
+ })
359
+ .catch((e) => e);
360
+ expect(txError).toBeInstanceOf(Error);
361
+ expect(txError.message).toContain('intentional rollback');
362
+ // Row should NOT exist — transaction was rolled back
363
+ const ghost = await prisma.thread_sessions.findUnique({
364
+ where: { thread_id: 'tx-rollback-1' },
365
+ });
366
+ expect(ghost).toBeNull();
367
+ }, 30_000);
368
+ });
@@ -0,0 +1,112 @@
1
+ // Image processing utilities for Discord attachments.
2
+ // Uses sharp (optional) to resize large images and heic-convert (optional) for HEIC support.
3
+ // Falls back gracefully if dependencies are not available.
4
+ import { createLogger, LogPrefix } from './logger.js';
5
+ const logger = createLogger(LogPrefix.FORMATTING);
6
+ const MAX_DIMENSION = 1500;
7
+ const HEIC_MIME_TYPES = [
8
+ 'image/heic',
9
+ 'image/heif',
10
+ 'image/heic-sequence',
11
+ 'image/heif-sequence',
12
+ ];
13
+ let sharpModule = undefined;
14
+ let heicConvertModule = undefined;
15
+ async function tryLoadSharp() {
16
+ if (sharpModule !== undefined) {
17
+ return sharpModule;
18
+ }
19
+ try {
20
+ sharpModule = (await import('sharp')).default;
21
+ logger.log('sharp loaded successfully');
22
+ return sharpModule;
23
+ }
24
+ catch {
25
+ logger.log('sharp not available, images will be sent at original size');
26
+ sharpModule = null;
27
+ return null;
28
+ }
29
+ }
30
+ async function tryLoadHeicConvert() {
31
+ if (heicConvertModule !== undefined) {
32
+ return heicConvertModule;
33
+ }
34
+ try {
35
+ const mod = await import('heic-convert');
36
+ heicConvertModule = mod.default;
37
+ logger.log('heic-convert loaded successfully');
38
+ return heicConvertModule;
39
+ }
40
+ catch {
41
+ logger.log('heic-convert not available, HEIC images will be sent as-is');
42
+ heicConvertModule = null;
43
+ return null;
44
+ }
45
+ }
46
+ function isHeicMime(mime) {
47
+ return HEIC_MIME_TYPES.includes(mime.toLowerCase());
48
+ }
49
+ export async function processImage(buffer, mime) {
50
+ // Skip non-images (PDFs, etc.)
51
+ if (!mime.startsWith('image/')) {
52
+ return { buffer, mime };
53
+ }
54
+ let workingBuffer = buffer;
55
+ let workingMime = mime;
56
+ // Handle HEIC conversion first (before sharp, since sharp doesn't support HEIC)
57
+ if (isHeicMime(mime)) {
58
+ const heicConvert = await tryLoadHeicConvert();
59
+ if (heicConvert) {
60
+ try {
61
+ const outputArrayBuffer = await heicConvert({
62
+ buffer: workingBuffer.buffer.slice(workingBuffer.byteOffset, workingBuffer.byteOffset + workingBuffer.byteLength),
63
+ format: 'JPEG',
64
+ quality: 0.85,
65
+ });
66
+ workingBuffer = Buffer.from(outputArrayBuffer);
67
+ workingMime = 'image/jpeg';
68
+ logger.log(`Converted HEIC to JPEG (${buffer.length} → ${workingBuffer.length} bytes)`);
69
+ }
70
+ catch (error) {
71
+ logger.error('Failed to convert HEIC, sending original:', error);
72
+ return { buffer, mime };
73
+ }
74
+ }
75
+ else {
76
+ // No heic-convert available, return original (LLM might not support it)
77
+ logger.log('HEIC image detected but heic-convert not available, sending as-is');
78
+ return { buffer, mime };
79
+ }
80
+ }
81
+ // Now process with sharp (resize + ensure JPEG output)
82
+ const sharp = await tryLoadSharp();
83
+ if (!sharp) {
84
+ return { buffer: workingBuffer, mime: workingMime };
85
+ }
86
+ try {
87
+ const image = sharp(workingBuffer);
88
+ const metadata = await image.metadata();
89
+ const { width, height } = metadata;
90
+ const needsResize = width && height && (width > MAX_DIMENSION || height > MAX_DIMENSION);
91
+ if (!needsResize) {
92
+ // Still convert to JPEG for consistency (unless already JPEG from HEIC conversion)
93
+ const outputBuffer = await image.jpeg({ quality: 85 }).toBuffer();
94
+ logger.log(`Converted image to JPEG: ${width}x${height} (${outputBuffer.length} bytes)`);
95
+ return { buffer: outputBuffer, mime: 'image/jpeg' };
96
+ }
97
+ // Resize and convert to JPEG
98
+ const outputBuffer = await image
99
+ .resize(MAX_DIMENSION, MAX_DIMENSION, {
100
+ fit: 'inside',
101
+ withoutEnlargement: true,
102
+ })
103
+ .jpeg({ quality: 85 })
104
+ .toBuffer();
105
+ logger.log(`Resized image: ${width}x${height} → max ${MAX_DIMENSION}px (${outputBuffer.length} bytes)`);
106
+ return { buffer: outputBuffer, mime: 'image/jpeg' };
107
+ }
108
+ catch (error) {
109
+ logger.error('Failed to process image with sharp, using working buffer:', error);
110
+ return { buffer: workingBuffer, mime: workingMime };
111
+ }
112
+ }