@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,29 @@
1
+
2
+ /* !!! This is code generated by Prisma. Do not edit directly. !!! */
3
+ /* eslint-disable */
4
+ // biome-ignore-all lint: generated file
5
+ // @ts-nocheck
6
+ /*
7
+ * This is a barrel export file for all models and their related types.
8
+ *
9
+ * 🟢 You can import this file directly.
10
+ */
11
+ export type * from './models/thread_sessions.js'
12
+ export type * from './models/part_messages.js'
13
+ export type * from './models/bot_tokens.js'
14
+ export type * from './models/channel_directories.js'
15
+ export type * from './models/bot_api_keys.js'
16
+ export type * from './models/thread_worktrees.js'
17
+ export type * from './models/channel_models.js'
18
+ export type * from './models/session_models.js'
19
+ export type * from './models/channel_agents.js'
20
+ export type * from './models/session_agents.js'
21
+ export type * from './models/channel_worktrees.js'
22
+ export type * from './models/channel_verbosity.js'
23
+ export type * from './models/channel_mention_mode.js'
24
+ export type * from './models/global_models.js'
25
+ export type * from './models/scheduled_tasks.js'
26
+ export type * from './models/session_start_sources.js'
27
+ export type * from './models/forum_sync_configs.js'
28
+ export type * from './models/ipc_requests.js'
29
+ export type * from './commonInputTypes.js'
@@ -0,0 +1,121 @@
1
+ // Heap memory monitor and snapshot writer.
2
+ // Periodically checks V8 heap usage and writes .heapsnapshot files to ~/.kimaki/heap-snapshots/
3
+ // when memory usage is high. Also exposes writeHeapSnapshot() for on-demand snapshots via SIGUSR1.
4
+ //
5
+ // Threshold: 85% heap used -> write snapshot for debugging
6
+
7
+ import v8 from 'node:v8'
8
+ import fs from 'node:fs'
9
+ import path from 'node:path'
10
+ import { getDataDir } from './config.js'
11
+ import { createLogger, LogPrefix } from './logger.js'
12
+
13
+ const logger = createLogger(LogPrefix.HEAP)
14
+
15
+ const SNAPSHOT_THRESHOLD = 0.85
16
+ const CHECK_INTERVAL_MS = 30_000
17
+ // After writing a snapshot, wait at least 5 minutes before writing another
18
+ const SNAPSHOT_COOLDOWN_MS = 5 * 60 * 1000
19
+
20
+ let lastSnapshotTime = 0
21
+ let monitorInterval: ReturnType<typeof setInterval> | null = null
22
+
23
+ function getHeapSnapshotDir(): string {
24
+ return path.join(getDataDir(), 'heap-snapshots')
25
+ }
26
+
27
+ function ensureSnapshotDir(): string {
28
+ const dir = getHeapSnapshotDir()
29
+ if (!fs.existsSync(dir)) {
30
+ fs.mkdirSync(dir, { recursive: true })
31
+ }
32
+ return dir
33
+ }
34
+
35
+ function getHeapStats(): { usedMB: number; limitMB: number; ratio: number } {
36
+ const stats = v8.getHeapStatistics()
37
+ const usedMB = stats.used_heap_size / 1024 / 1024
38
+ const limitMB = stats.heap_size_limit / 1024 / 1024
39
+ const ratio = stats.used_heap_size / stats.heap_size_limit
40
+ return { usedMB, limitMB, ratio }
41
+ }
42
+
43
+ /**
44
+ * Write a V8 heap snapshot to ~/.kimaki/heap-snapshots/.
45
+ * Filename includes ISO date and current heap size for easy identification.
46
+ * Returns the snapshot file path.
47
+ */
48
+ export function writeHeapSnapshot(): string {
49
+ const dir = ensureSnapshotDir()
50
+ const { usedMB, limitMB, ratio } = getHeapStats()
51
+ const pct = (ratio * 100).toFixed(1)
52
+
53
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-')
54
+ const filename = `heap-${timestamp}-${Math.round(usedMB)}MB.heapsnapshot`
55
+ const filepath = path.join(dir, filename)
56
+
57
+ logger.log(
58
+ `Writing heap snapshot (${Math.round(usedMB)}MB / ${Math.round(limitMB)}MB, ${pct}%)`,
59
+ )
60
+ v8.writeHeapSnapshot(filepath)
61
+ logger.log(`Snapshot saved: ${filepath}`)
62
+
63
+ return filepath
64
+ }
65
+
66
+ function checkHeapUsage(): void {
67
+ const { usedMB, limitMB, ratio } = getHeapStats()
68
+ const pct = (ratio * 100).toFixed(1)
69
+
70
+ if (ratio >= SNAPSHOT_THRESHOLD) {
71
+ logger.warn(
72
+ `Heap at ${pct}% (${Math.round(usedMB)}MB / ${Math.round(limitMB)}MB) - exceeds snapshot threshold (${SNAPSHOT_THRESHOLD * 100}%)`,
73
+ )
74
+
75
+ const now = Date.now()
76
+ if (now - lastSnapshotTime >= SNAPSHOT_COOLDOWN_MS) {
77
+ lastSnapshotTime = now
78
+ try {
79
+ writeHeapSnapshot()
80
+ } catch (e) {
81
+ logger.error(
82
+ 'Failed to write heap snapshot:',
83
+ e instanceof Error ? e.message : String(e),
84
+ )
85
+ }
86
+ } else {
87
+ logger.log('Snapshot cooldown active, skipping')
88
+ }
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Start the periodic heap usage monitor.
94
+ * Checks every 30s and writes snapshots when threshold is exceeded.
95
+ */
96
+ export function startHeapMonitor(): void {
97
+ if (monitorInterval) {
98
+ return
99
+ }
100
+
101
+ // Ensure the snapshot directory exists so V8's --diagnostic-dir has a valid target.
102
+ // Also needed for our own writeHeapSnapshot() calls.
103
+ ensureSnapshotDir()
104
+
105
+ const { usedMB, limitMB, ratio } = getHeapStats()
106
+ logger.log(
107
+ `Heap monitor started (${Math.round(usedMB)}MB / ${Math.round(limitMB)}MB, ${(ratio * 100).toFixed(1)}%) - ` +
108
+ `snapshot at ${SNAPSHOT_THRESHOLD * 100}%`,
109
+ )
110
+
111
+ monitorInterval = setInterval(checkHeapUsage, CHECK_INTERVAL_MS)
112
+ // Don't prevent process exit
113
+ monitorInterval.unref()
114
+ }
115
+
116
+ export function stopHeapMonitor(): void {
117
+ if (monitorInterval) {
118
+ clearInterval(monitorInterval)
119
+ monitorInterval = null
120
+ }
121
+ }
@@ -0,0 +1,428 @@
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
+
12
+ const __filename = fileURLToPath(import.meta.url)
13
+ const __dirname = path.dirname(__filename)
14
+
15
+ async function migrateSchema(prisma: PrismaClient) {
16
+ const schemaPath = path.join(__dirname, '../src/schema.sql')
17
+ const sql = fs.readFileSync(schemaPath, 'utf-8')
18
+ const statements = sql
19
+ .split(';')
20
+ .map((s) =>
21
+ s
22
+ .split('\n')
23
+ .filter((line) => !line.trimStart().startsWith('--'))
24
+ .join('\n')
25
+ .trim(),
26
+ )
27
+ .filter(
28
+ (s) =>
29
+ s.length > 0 &&
30
+ !/^CREATE\s+TABLE\s+["']?sqlite_sequence["']?\s*\(/i.test(s),
31
+ )
32
+ .map((s) =>
33
+ s
34
+ .replace(
35
+ /^CREATE\s+UNIQUE\s+INDEX\b(?!\s+IF)/i,
36
+ 'CREATE UNIQUE INDEX IF NOT EXISTS',
37
+ )
38
+ .replace(/^CREATE\s+INDEX\b(?!\s+IF)/i, 'CREATE INDEX IF NOT EXISTS'),
39
+ )
40
+ for (const statement of statements) {
41
+ await prisma.$executeRawUnsafe(statement)
42
+ }
43
+ }
44
+
45
+ describe('hrana-server', () => {
46
+ let testServer: http.Server | null = null
47
+ let testDb: Database.Database | null = null
48
+ let prisma: PrismaClient | null = null
49
+ const dbPath = path.join(
50
+ process.cwd(),
51
+ `tmp/test-hrana-${crypto.randomUUID().slice(0, 8)}.db`,
52
+ )
53
+
54
+ afterAll(async () => {
55
+ if (prisma) await prisma.$disconnect()
56
+ if (testServer)
57
+ await new Promise<void>((resolve) => {
58
+ testServer!.close(() => {
59
+ resolve()
60
+ })
61
+ })
62
+ if (testDb) testDb.close()
63
+ try {
64
+ fs.unlinkSync(dbPath)
65
+ } catch (e) {
66
+ console.warn('cleanup:', dbPath, (e as Error).message)
67
+ }
68
+ try {
69
+ fs.unlinkSync(dbPath + '-wal')
70
+ } catch (e) {
71
+ console.warn('cleanup:', dbPath + '-wal', (e as Error).message)
72
+ }
73
+ try {
74
+ fs.unlinkSync(dbPath + '-shm')
75
+ } catch (e) {
76
+ console.warn('cleanup:', dbPath + '-shm', (e as Error).message)
77
+ }
78
+ })
79
+
80
+ test('prisma CRUD through hrana server', async () => {
81
+ fs.mkdirSync(path.dirname(dbPath), { recursive: true })
82
+
83
+ const database = new Database(dbPath)
84
+ database.exec('PRAGMA journal_mode = WAL')
85
+ database.exec('PRAGMA busy_timeout = 5000')
86
+ testDb = database
87
+
88
+ const port = 10000 + Math.floor(Math.random() * 50000)
89
+ await new Promise<void>((resolve, reject) => {
90
+ const srv = http.createServer(createHranaHandler(database))
91
+ srv.on('error', reject)
92
+ srv.listen(port, '127.0.0.1', () => {
93
+ testServer = srv
94
+ resolve()
95
+ })
96
+ })
97
+
98
+ const adapter = new PrismaLibSql({ url: `http://127.0.0.1:${port}` })
99
+ prisma = new PrismaClient({ adapter })
100
+ await migrateSchema(prisma)
101
+
102
+ // Create
103
+ const created = await prisma.thread_sessions.create({
104
+ data: {
105
+ thread_id: 'hrana-test-thread',
106
+ session_id: 'hrana-test-session',
107
+ },
108
+ })
109
+ expect(created.thread_id).toMatchInlineSnapshot(`"hrana-test-thread"`)
110
+ expect(created.session_id).toMatchInlineSnapshot(`"hrana-test-session"`)
111
+
112
+ // Read
113
+ const found = await prisma.thread_sessions.findUnique({
114
+ where: { thread_id: 'hrana-test-thread' },
115
+ })
116
+ expect(found?.session_id).toMatchInlineSnapshot(`"hrana-test-session"`)
117
+
118
+ // Update
119
+ await prisma.thread_sessions.update({
120
+ where: { thread_id: 'hrana-test-thread' },
121
+ data: { session_id: 'updated-session' },
122
+ })
123
+ const updated = await prisma.thread_sessions.findUnique({
124
+ where: { thread_id: 'hrana-test-thread' },
125
+ })
126
+ expect(updated?.session_id).toMatchInlineSnapshot(`"updated-session"`)
127
+
128
+ // Delete
129
+ await prisma.thread_sessions.delete({
130
+ where: { thread_id: 'hrana-test-thread' },
131
+ })
132
+ const deleted = await prisma.thread_sessions.findUnique({
133
+ where: { thread_id: 'hrana-test-thread' },
134
+ })
135
+ expect(deleted).toBeNull()
136
+ }, 30_000)
137
+
138
+ test('$executeRawUnsafe works for PRAGMAs', async () => {
139
+ if (!prisma) throw new Error('prisma not initialized')
140
+ const result = await prisma.$executeRawUnsafe('PRAGMA journal_mode')
141
+ expect(typeof result).toBe('number')
142
+ })
143
+
144
+ test('batch transaction via Prisma $transaction', async () => {
145
+ if (!prisma) throw new Error('prisma not initialized')
146
+
147
+ const [s1, s2] = await prisma.$transaction([
148
+ prisma.thread_sessions.create({
149
+ data: { thread_id: 'batch-1', session_id: 'sess-1' },
150
+ }),
151
+ prisma.thread_sessions.create({
152
+ data: { thread_id: 'batch-2', session_id: 'sess-2' },
153
+ }),
154
+ ])
155
+ expect(s1.thread_id).toMatchInlineSnapshot(`"batch-1"`)
156
+ expect(s2.thread_id).toMatchInlineSnapshot(`"batch-2"`)
157
+
158
+ const count = await prisma.thread_sessions.count({
159
+ where: { thread_id: { in: ['batch-1', 'batch-2'] } },
160
+ })
161
+ expect(count).toBe(2)
162
+
163
+ await prisma.thread_sessions.deleteMany({
164
+ where: { thread_id: { in: ['batch-1', 'batch-2'] } },
165
+ })
166
+ }, 30_000)
167
+
168
+ test('schema migration DDL via $executeRawUnsafe', async () => {
169
+ if (!prisma) throw new Error('prisma not initialized')
170
+
171
+ // CREATE TABLE IF NOT EXISTS is idempotent — running migrateSchema again
172
+ // should not throw even though tables already exist.
173
+ await migrateSchema(prisma)
174
+
175
+ // Verify DDL actually created the tables by querying sqlite_master
176
+ const tables = await prisma.$queryRawUnsafe<Array<{ name: string }>>(
177
+ `SELECT name FROM sqlite_master WHERE type='table' ORDER BY name`,
178
+ )
179
+ const tableNames = tables.map((t) => t.name)
180
+ expect(tableNames).toContain('thread_sessions')
181
+ expect(tableNames).toContain('ipc_requests')
182
+ expect(tableNames).toContain('scheduled_tasks')
183
+
184
+ // Also verify indexes were created
185
+ const indexes = await prisma.$queryRawUnsafe<Array<{ name: string }>>(
186
+ `SELECT name FROM sqlite_master WHERE type='index' AND name LIKE '%idx%' ORDER BY name`,
187
+ )
188
+ const indexNames = indexes.map((i) => i.name)
189
+ expect(indexNames).toContain('ipc_requests_status_created_at_idx')
190
+ expect(indexNames).toContain('scheduled_tasks_status_next_run_at_idx')
191
+
192
+ // Test CREATE INDEX IF NOT EXISTS is also idempotent
193
+ await prisma.$executeRawUnsafe(
194
+ `CREATE INDEX IF NOT EXISTS "ipc_requests_status_created_at_idx" ON "ipc_requests"("status", "created_at")`,
195
+ )
196
+ })
197
+
198
+ test('concurrent queries via Promise.all', async () => {
199
+ if (!prisma) throw new Error('prisma not initialized')
200
+
201
+ // Seed some data for concurrent reads
202
+ const threads = Array.from({ length: 5 }, (_, i) => ({
203
+ thread_id: `concurrent-${i}`,
204
+ session_id: `sess-concurrent-${i}`,
205
+ }))
206
+ for (const t of threads) {
207
+ await prisma.thread_sessions.create({ data: t })
208
+ }
209
+
210
+ // Simulate kimaki's pattern of parallel Prisma queries
211
+ const [allThreads, count, single, filtered] = await Promise.all([
212
+ prisma.thread_sessions.findMany({
213
+ where: { thread_id: { startsWith: 'concurrent-' } },
214
+ orderBy: { thread_id: 'asc' },
215
+ }),
216
+ prisma.thread_sessions.count({
217
+ where: { thread_id: { startsWith: 'concurrent-' } },
218
+ }),
219
+ prisma.thread_sessions.findUnique({
220
+ where: { thread_id: 'concurrent-2' },
221
+ }),
222
+ prisma.thread_sessions.findMany({
223
+ where: { thread_id: { in: ['concurrent-0', 'concurrent-4'] } },
224
+ orderBy: { thread_id: 'asc' },
225
+ }),
226
+ ])
227
+
228
+ expect(allThreads.length).toBe(5)
229
+ expect(count).toBe(5)
230
+ expect(single?.session_id).toMatchInlineSnapshot(`"sess-concurrent-2"`)
231
+ expect(filtered.map((f) => f.thread_id)).toMatchInlineSnapshot(`
232
+ [
233
+ "concurrent-0",
234
+ "concurrent-4",
235
+ ]
236
+ `)
237
+
238
+ // Cleanup
239
+ await prisma.thread_sessions.deleteMany({
240
+ where: { thread_id: { startsWith: 'concurrent-' } },
241
+ })
242
+ }, 30_000)
243
+
244
+ test('$queryRawUnsafe for PRAGMAs that return values', async () => {
245
+ if (!prisma) throw new Error('prisma not initialized')
246
+
247
+ // PRAGMA that returns a value — journal_mode should be WAL
248
+ const journalMode = await prisma.$queryRawUnsafe<
249
+ Array<{ journal_mode: string }>
250
+ >('PRAGMA journal_mode')
251
+ expect(journalMode[0]?.journal_mode).toMatchInlineSnapshot(`"wal"`)
252
+
253
+ // PRAGMA busy_timeout returns the current timeout value
254
+ const busyTimeout = await prisma.$queryRawUnsafe<
255
+ Array<{ busy_timeout: number }>
256
+ >('PRAGMA busy_timeout')
257
+ expect(busyTimeout[0]?.busy_timeout).toMatchInlineSnapshot(`undefined`)
258
+
259
+ // PRAGMA table_info returns column metadata
260
+ const tableInfo = await prisma.$queryRawUnsafe<
261
+ Array<{ name: string; type: string }>
262
+ >(`PRAGMA table_info('ipc_requests')`)
263
+ const colNames = tableInfo.map((c) => c.name)
264
+ expect(colNames).toMatchInlineSnapshot(`
265
+ [
266
+ "id",
267
+ "type",
268
+ "session_id",
269
+ "thread_id",
270
+ "payload",
271
+ "response",
272
+ "status",
273
+ "created_at",
274
+ "updated_at",
275
+ ]
276
+ `)
277
+ })
278
+
279
+ test('updateMany with complex WHERE using in operator', async () => {
280
+ if (!prisma) throw new Error('prisma not initialized')
281
+
282
+ // Seed: create a thread + multiple IPC requests in different statuses
283
+ // (mirrors kimaki's cancelAllPendingIpcRequests pattern)
284
+ await prisma.thread_sessions.create({
285
+ data: { thread_id: 'ipc-test-thread', session_id: 'ipc-test-session' },
286
+ })
287
+ const statuses = ['pending', 'pending', 'processing', 'completed'] as const
288
+ for (let i = 0; i < statuses.length; i++) {
289
+ await prisma.ipc_requests.create({
290
+ data: {
291
+ id: `ipc-req-${i}`,
292
+ type: 'file_upload',
293
+ session_id: 'ipc-test-session',
294
+ thread_id: 'ipc-test-thread',
295
+ payload: JSON.stringify({ prompt: `test-${i}` }),
296
+ status: statuses[i],
297
+ },
298
+ })
299
+ }
300
+
301
+ // updateMany with WHERE status IN ['pending', 'processing']
302
+ const result = await prisma.ipc_requests.updateMany({
303
+ where: { status: { in: ['pending', 'processing'] } },
304
+ data: {
305
+ status: 'cancelled',
306
+ response: JSON.stringify({ error: 'Bot shutting down' }),
307
+ },
308
+ })
309
+ expect(result.count).toBe(3)
310
+
311
+ // Verify: only 'completed' row is untouched
312
+ const remaining = await prisma.ipc_requests.findMany({
313
+ where: { thread_id: 'ipc-test-thread' },
314
+ orderBy: { id: 'asc' },
315
+ select: { id: true, status: true },
316
+ })
317
+ expect(remaining).toMatchInlineSnapshot(`
318
+ [
319
+ {
320
+ "id": "ipc-req-0",
321
+ "status": "cancelled",
322
+ },
323
+ {
324
+ "id": "ipc-req-1",
325
+ "status": "cancelled",
326
+ },
327
+ {
328
+ "id": "ipc-req-2",
329
+ "status": "cancelled",
330
+ },
331
+ {
332
+ "id": "ipc-req-3",
333
+ "status": "completed",
334
+ },
335
+ ]
336
+ `)
337
+
338
+ // Cleanup
339
+ await prisma.ipc_requests.deleteMany({
340
+ where: { thread_id: 'ipc-test-thread' },
341
+ })
342
+ await prisma.thread_sessions.delete({
343
+ where: { thread_id: 'ipc-test-thread' },
344
+ })
345
+ }, 30_000)
346
+
347
+ test('interactive $transaction (callback form)', async () => {
348
+ if (!prisma) throw new Error('prisma not initialized')
349
+
350
+ // Interactive transaction: reads and writes within the same tx callback.
351
+ // This exercises BEGIN/queries/COMMIT across multiple hrana pipeline
352
+ // requests with batons (stream continuity).
353
+ const result = await prisma.$transaction(async (tx) => {
354
+ await tx.thread_sessions.create({
355
+ data: { thread_id: 'tx-interactive-1', session_id: 'sess-tx-1' },
356
+ })
357
+ await tx.thread_sessions.create({
358
+ data: { thread_id: 'tx-interactive-2', session_id: 'sess-tx-2' },
359
+ })
360
+
361
+ // Read inside the same transaction — should see uncommitted rows
362
+ const count = await tx.thread_sessions.count({
363
+ where: { thread_id: { startsWith: 'tx-interactive-' } },
364
+ })
365
+
366
+ // Conditional write based on read
367
+ if (count === 2) {
368
+ await tx.thread_sessions.update({
369
+ where: { thread_id: 'tx-interactive-1' },
370
+ data: { session_id: 'sess-tx-1-updated' },
371
+ })
372
+ }
373
+
374
+ return tx.thread_sessions.findMany({
375
+ where: { thread_id: { startsWith: 'tx-interactive-' } },
376
+ orderBy: { thread_id: 'asc' },
377
+ select: { thread_id: true, session_id: true },
378
+ })
379
+ })
380
+
381
+ expect(result).toMatchInlineSnapshot(`
382
+ [
383
+ {
384
+ "session_id": "sess-tx-1-updated",
385
+ "thread_id": "tx-interactive-1",
386
+ },
387
+ {
388
+ "session_id": "sess-tx-2",
389
+ "thread_id": "tx-interactive-2",
390
+ },
391
+ ]
392
+ `)
393
+
394
+ // Verify committed outside transaction
395
+ const outside = await prisma.thread_sessions.count({
396
+ where: { thread_id: { startsWith: 'tx-interactive-' } },
397
+ })
398
+ expect(outside).toBe(2)
399
+
400
+ // Cleanup
401
+ await prisma.thread_sessions.deleteMany({
402
+ where: { thread_id: { startsWith: 'tx-interactive-' } },
403
+ })
404
+ }, 30_000)
405
+
406
+ test('interactive $transaction rolls back on error', async () => {
407
+ if (!prisma) throw new Error('prisma not initialized')
408
+
409
+ // Verify rollback: if the callback throws, no rows should be committed
410
+ const txError = await prisma
411
+ .$transaction(async (tx) => {
412
+ await tx.thread_sessions.create({
413
+ data: { thread_id: 'tx-rollback-1', session_id: 'sess-rollback' },
414
+ })
415
+ throw new Error('intentional rollback')
416
+ })
417
+ .catch((e: Error) => e)
418
+
419
+ expect(txError).toBeInstanceOf(Error)
420
+ expect((txError as Error).message).toContain('intentional rollback')
421
+
422
+ // Row should NOT exist — transaction was rolled back
423
+ const ghost = await prisma.thread_sessions.findUnique({
424
+ where: { thread_id: 'tx-rollback-1' },
425
+ })
426
+ expect(ghost).toBeNull()
427
+ }, 30_000)
428
+ })