@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,95 @@
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
+ import v8 from 'node:v8';
7
+ import fs from 'node:fs';
8
+ import path from 'node:path';
9
+ import { getDataDir } from './config.js';
10
+ import { createLogger, LogPrefix } from './logger.js';
11
+ const logger = createLogger(LogPrefix.HEAP);
12
+ const SNAPSHOT_THRESHOLD = 0.85;
13
+ const CHECK_INTERVAL_MS = 30_000;
14
+ // After writing a snapshot, wait at least 5 minutes before writing another
15
+ const SNAPSHOT_COOLDOWN_MS = 5 * 60 * 1000;
16
+ let lastSnapshotTime = 0;
17
+ let monitorInterval = null;
18
+ function getHeapSnapshotDir() {
19
+ return path.join(getDataDir(), 'heap-snapshots');
20
+ }
21
+ function ensureSnapshotDir() {
22
+ const dir = getHeapSnapshotDir();
23
+ if (!fs.existsSync(dir)) {
24
+ fs.mkdirSync(dir, { recursive: true });
25
+ }
26
+ return dir;
27
+ }
28
+ function getHeapStats() {
29
+ const stats = v8.getHeapStatistics();
30
+ const usedMB = stats.used_heap_size / 1024 / 1024;
31
+ const limitMB = stats.heap_size_limit / 1024 / 1024;
32
+ const ratio = stats.used_heap_size / stats.heap_size_limit;
33
+ return { usedMB, limitMB, ratio };
34
+ }
35
+ /**
36
+ * Write a V8 heap snapshot to ~/.kimaki/heap-snapshots/.
37
+ * Filename includes ISO date and current heap size for easy identification.
38
+ * Returns the snapshot file path.
39
+ */
40
+ export function writeHeapSnapshot() {
41
+ const dir = ensureSnapshotDir();
42
+ const { usedMB, limitMB, ratio } = getHeapStats();
43
+ const pct = (ratio * 100).toFixed(1);
44
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
45
+ const filename = `heap-${timestamp}-${Math.round(usedMB)}MB.heapsnapshot`;
46
+ const filepath = path.join(dir, filename);
47
+ logger.log(`Writing heap snapshot (${Math.round(usedMB)}MB / ${Math.round(limitMB)}MB, ${pct}%)`);
48
+ v8.writeHeapSnapshot(filepath);
49
+ logger.log(`Snapshot saved: ${filepath}`);
50
+ return filepath;
51
+ }
52
+ function checkHeapUsage() {
53
+ const { usedMB, limitMB, ratio } = getHeapStats();
54
+ const pct = (ratio * 100).toFixed(1);
55
+ if (ratio >= SNAPSHOT_THRESHOLD) {
56
+ logger.warn(`Heap at ${pct}% (${Math.round(usedMB)}MB / ${Math.round(limitMB)}MB) - exceeds snapshot threshold (${SNAPSHOT_THRESHOLD * 100}%)`);
57
+ const now = Date.now();
58
+ if (now - lastSnapshotTime >= SNAPSHOT_COOLDOWN_MS) {
59
+ lastSnapshotTime = now;
60
+ try {
61
+ writeHeapSnapshot();
62
+ }
63
+ catch (e) {
64
+ logger.error('Failed to write heap snapshot:', e instanceof Error ? e.message : String(e));
65
+ }
66
+ }
67
+ else {
68
+ logger.log('Snapshot cooldown active, skipping');
69
+ }
70
+ }
71
+ }
72
+ /**
73
+ * Start the periodic heap usage monitor.
74
+ * Checks every 30s and writes snapshots when threshold is exceeded.
75
+ */
76
+ export function startHeapMonitor() {
77
+ if (monitorInterval) {
78
+ return;
79
+ }
80
+ // Ensure the snapshot directory exists so V8's --diagnostic-dir has a valid target.
81
+ // Also needed for our own writeHeapSnapshot() calls.
82
+ ensureSnapshotDir();
83
+ const { usedMB, limitMB, ratio } = getHeapStats();
84
+ logger.log(`Heap monitor started (${Math.round(usedMB)}MB / ${Math.round(limitMB)}MB, ${(ratio * 100).toFixed(1)}%) - ` +
85
+ `snapshot at ${SNAPSHOT_THRESHOLD * 100}%`);
86
+ monitorInterval = setInterval(checkHeapUsage, CHECK_INTERVAL_MS);
87
+ // Don't prevent process exit
88
+ monitorInterval.unref();
89
+ }
90
+ export function stopHeapMonitor() {
91
+ if (monitorInterval) {
92
+ clearInterval(monitorInterval);
93
+ monitorInterval = null;
94
+ }
95
+ }
@@ -0,0 +1,416 @@
1
+ // In-process HTTP server speaking the Hrana v2 protocol.
2
+ // Replaces the sqld child process (39MB Rust binary) with a lightweight
3
+ // server backed by the `libsql` npm package (better-sqlite3 API).
4
+ // Binds to the fixed lock port for single-instance enforcement.
5
+ //
6
+ // Serves POST /v2/pipeline (Hrana v2 JSON), GET /v2, and GET /health.
7
+ // The @libsql/client HTTP driver and @prisma/adapter-libsql connect here.
8
+ //
9
+ // Hrana v2 protocol spec ("Hrana over HTTP"):
10
+ // https://github.com/tursodatabase/libsql/blob/main/docs/HTTP_V2_SPEC.md
11
+ //
12
+ // The protocol exposes stateful streams over HTTP. Each stream corresponds
13
+ // to a SQLite connection. Requests on the same stream are tied together
14
+ // via a "baton" — the server returns a baton in every response, and the
15
+ // client includes it in the next request. Stream-scoped state includes
16
+ // SQL text cached via store_sql (referenced by sql_id in later stmts).
17
+ //
18
+ // Request types implemented:
19
+ // execute — run a single SQL statement, return cols/rows/changes
20
+ // batch — run multiple steps with conditional execution (ok/not/and/or)
21
+ // sequence — split raw SQL by semicolons, execute each (no results)
22
+ // store_sql — cache SQL text under a numeric sql_id for the stream
23
+ // close_sql — remove a cached sql_id
24
+ // close — close the stream (baton becomes null)
25
+ //
26
+ // Value encoding (SQLite → Hrana JSON):
27
+ // INTEGER → {"type":"integer","value":"42"} (string, not number)
28
+ // REAL → {"type":"float","value":3.14}
29
+ // TEXT → {"type":"text","value":"hello"}
30
+ // BLOB → {"type":"blob","base64":"..."}
31
+ // NULL → {"type":"null"}
32
+ import fs from 'node:fs';
33
+ import http from 'node:http';
34
+ import path from 'node:path';
35
+ import Database from 'libsql';
36
+ import * as errore from 'errore';
37
+ import { createLogger, LogPrefix } from './logger.js';
38
+ import { ServerStartError, FetchError } from './errors.js';
39
+ import { getLockPort } from './config.js';
40
+ const hranaLogger = createLogger(LogPrefix.DB);
41
+ let db = null;
42
+ let server = null;
43
+ let hranaUrl = null;
44
+ /**
45
+ * Get the Hrana HTTP URL for injecting into plugin child processes.
46
+ * Returns null if the server hasn't been started yet.
47
+ * Only used for KIMAKI_DB_URL env var in opencode.ts — the bot process
48
+ * itself always uses direct file: access via Prisma.
49
+ */
50
+ export function getHranaUrl() {
51
+ return hranaUrl;
52
+ }
53
+ /**
54
+ * Start the in-process Hrana v2 server on the fixed lock port.
55
+ * Handles single-instance enforcement: if the port is occupied, kills the
56
+ * existing process first.
57
+ */
58
+ export async function startHranaServer({ dbPath }) {
59
+ if (server && db && hranaUrl)
60
+ return hranaUrl;
61
+ const port = getLockPort();
62
+ fs.mkdirSync(path.dirname(dbPath), { recursive: true });
63
+ await evictExistingInstance({ port });
64
+ hranaLogger.log(`Starting hrana server on 127.0.0.1:${port} with db: ${dbPath}`);
65
+ const database = new Database(dbPath);
66
+ database.exec('PRAGMA journal_mode = WAL');
67
+ database.exec('PRAGMA busy_timeout = 5000');
68
+ db = database;
69
+ const handler = createHranaHandler(database);
70
+ const started = await new Promise((resolve) => {
71
+ const srv = http.createServer(handler);
72
+ srv.on('error', (err) => {
73
+ resolve(new ServerStartError({
74
+ port,
75
+ reason: err.code === 'EADDRINUSE'
76
+ ? `Port ${port} still in use after eviction`
77
+ : err.message,
78
+ }));
79
+ });
80
+ srv.listen(port, '127.0.0.1', () => {
81
+ server = srv;
82
+ resolve(true);
83
+ });
84
+ });
85
+ if (started instanceof Error) {
86
+ database.close();
87
+ db = null;
88
+ return started;
89
+ }
90
+ hranaUrl = `http://127.0.0.1:${port}`;
91
+ hranaLogger.log(`Hrana server ready at ${hranaUrl}`);
92
+ return hranaUrl;
93
+ }
94
+ /**
95
+ * Stop the Hrana server and close the database.
96
+ */
97
+ export async function stopHranaServer() {
98
+ if (server) {
99
+ hranaLogger.log('Stopping hrana server...');
100
+ await new Promise((resolve) => {
101
+ server.close(() => {
102
+ resolve();
103
+ });
104
+ });
105
+ server = null;
106
+ }
107
+ if (db) {
108
+ db.close();
109
+ db = null;
110
+ }
111
+ hranaUrl = null;
112
+ hranaLogger.log('Hrana server stopped');
113
+ }
114
+ // ── Value encoding/decoding ──────────────────────────────────────────────
115
+ function encodeValue(val) {
116
+ if (val === null || val === undefined)
117
+ return { type: 'null' };
118
+ if (typeof val === 'bigint')
119
+ return { type: 'integer', value: val.toString() };
120
+ if (typeof val === 'number') {
121
+ if (Number.isInteger(val))
122
+ return { type: 'integer', value: val.toString() };
123
+ return { type: 'float', value: val };
124
+ }
125
+ if (typeof val === 'string')
126
+ return { type: 'text', value: val };
127
+ if (Buffer.isBuffer(val))
128
+ return { type: 'blob', base64: val.toString('base64') };
129
+ if (val instanceof Uint8Array)
130
+ return { type: 'blob', base64: Buffer.from(val).toString('base64') };
131
+ return { type: 'text', value: String(val) };
132
+ }
133
+ function decodeValue(val) {
134
+ if (val.type === 'null')
135
+ return null;
136
+ if (val.type === 'integer') {
137
+ const n = Number(val.value);
138
+ return Number.isSafeInteger(n) ? n : BigInt(val.value);
139
+ }
140
+ if (val.type === 'float')
141
+ return val.value;
142
+ if (val.type === 'text')
143
+ return val.value;
144
+ if (val.type === 'blob')
145
+ return Buffer.from(val.base64, 'base64');
146
+ return null;
147
+ }
148
+ // ── Statement execution ──────────────────────────────────────────────────
149
+ // SqliteError from libsql has a `code` property but catch gives Error.
150
+ function getSqliteErrorCode(err) {
151
+ return err.code ?? 'SQLITE_ERROR';
152
+ }
153
+ function resolveStmtSql(stmt, sqlStore) {
154
+ if (stmt.sql != null)
155
+ return stmt.sql;
156
+ if (stmt.sql_id != null)
157
+ return sqlStore.get(stmt.sql_id) ?? '';
158
+ return '';
159
+ }
160
+ function bindParams(stmt) {
161
+ if (stmt.named_args && stmt.named_args.length > 0) {
162
+ const named = {};
163
+ for (const na of stmt.named_args) {
164
+ named[na.name] = decodeValue(na.value);
165
+ }
166
+ return [named];
167
+ }
168
+ return (stmt.args ?? []).map(decodeValue);
169
+ }
170
+ function executeStmt(database, stmt, sqlStore) {
171
+ const sql = resolveStmtSql(stmt, sqlStore);
172
+ const prepared = database.prepare(sql);
173
+ const params = bindParams(stmt);
174
+ if (prepared.reader) {
175
+ const cols = prepared.columns();
176
+ const rows = prepared.all(...params);
177
+ return {
178
+ cols: cols.map((c) => ({ name: c.name, decltype: c.type })),
179
+ rows: rows.map((row) => cols.map((c) => encodeValue(row[c.name]))),
180
+ affected_row_count: 0,
181
+ last_insert_rowid: null,
182
+ };
183
+ }
184
+ const result = prepared.run(...params);
185
+ return {
186
+ cols: [],
187
+ rows: [],
188
+ affected_row_count: result.changes,
189
+ last_insert_rowid: result.lastInsertRowid != null ? result.lastInsertRowid.toString() : null,
190
+ };
191
+ }
192
+ // ── Batch condition evaluation ───────────────────────────────────────────
193
+ function evaluateCondition(cond, stepResults, stepErrors) {
194
+ if (!cond)
195
+ return true;
196
+ if (cond.type === 'ok')
197
+ return stepErrors[cond.step] === null && stepResults[cond.step] !== null;
198
+ if (cond.type === 'not')
199
+ return !evaluateCondition(cond.cond, stepResults, stepErrors);
200
+ if (cond.type === 'and')
201
+ return (cond.conds ?? []).every((c) => evaluateCondition(c, stepResults, stepErrors));
202
+ if (cond.type === 'or')
203
+ return (cond.conds ?? []).some((c) => evaluateCondition(c, stepResults, stepErrors));
204
+ return true;
205
+ }
206
+ // ── Request handlers ─────────────────────────────────────────────────────
207
+ function handleExecute(database, req, sqlStore) {
208
+ if (!req.stmt)
209
+ return {
210
+ type: 'error',
211
+ error: { message: 'Missing stmt', code: 'HRANA_PROTO_ERROR' },
212
+ };
213
+ const result = errore.try({
214
+ try: () => executeStmt(database, req.stmt, sqlStore),
215
+ catch: (e) => e,
216
+ });
217
+ if (result instanceof Error) {
218
+ return {
219
+ type: 'error',
220
+ error: { message: result.message, code: getSqliteErrorCode(result) },
221
+ };
222
+ }
223
+ return { type: 'ok', response: { type: 'execute', result } };
224
+ }
225
+ function handleBatch(database, req, sqlStore) {
226
+ const steps = req.batch?.steps ?? [];
227
+ const stepResults = [];
228
+ const stepErrors = [];
229
+ for (const step of steps) {
230
+ if (!evaluateCondition(step.condition, stepResults, stepErrors)) {
231
+ stepResults.push(null);
232
+ stepErrors.push(null);
233
+ continue;
234
+ }
235
+ const result = errore.try({
236
+ try: () => executeStmt(database, step.stmt, sqlStore),
237
+ catch: (e) => e,
238
+ });
239
+ if (result instanceof Error) {
240
+ stepResults.push(null);
241
+ stepErrors.push({
242
+ message: result.message,
243
+ code: getSqliteErrorCode(result),
244
+ });
245
+ }
246
+ else {
247
+ stepResults.push(result);
248
+ stepErrors.push(null);
249
+ }
250
+ }
251
+ return {
252
+ type: 'ok',
253
+ response: {
254
+ type: 'batch',
255
+ result: { step_results: stepResults, step_errors: stepErrors },
256
+ },
257
+ };
258
+ }
259
+ function handleSequence(database, req, sqlStore) {
260
+ const sql = req.sql ?? (req.sql_id != null ? sqlStore.get(req.sql_id) : null);
261
+ if (!sql)
262
+ return { type: 'ok', response: { type: 'sequence' } };
263
+ const result = errore.try({
264
+ try: () => {
265
+ database.exec(sql);
266
+ },
267
+ catch: (e) => e,
268
+ });
269
+ if (result instanceof Error) {
270
+ return {
271
+ type: 'error',
272
+ error: { message: result.message, code: getSqliteErrorCode(result) },
273
+ };
274
+ }
275
+ return { type: 'ok', response: { type: 'sequence' } };
276
+ }
277
+ function processRequest(database, req, sqlStore) {
278
+ if (req.type === 'execute')
279
+ return handleExecute(database, req, sqlStore);
280
+ if (req.type === 'batch')
281
+ return handleBatch(database, req, sqlStore);
282
+ if (req.type === 'sequence')
283
+ return handleSequence(database, req, sqlStore);
284
+ if (req.type === 'close')
285
+ return { type: 'ok', response: { type: 'close' } };
286
+ if (req.type === 'store_sql') {
287
+ if (req.sql_id != null && req.sql != null)
288
+ sqlStore.set(req.sql_id, req.sql);
289
+ return { type: 'ok', response: { type: 'store_sql' } };
290
+ }
291
+ if (req.type === 'close_sql') {
292
+ if (req.sql_id != null)
293
+ sqlStore.delete(req.sql_id);
294
+ return { type: 'ok', response: { type: 'close_sql' } };
295
+ }
296
+ return {
297
+ type: 'error',
298
+ error: {
299
+ message: `Unknown request type: ${req.type}`,
300
+ code: 'HRANA_PROTO_ERROR',
301
+ },
302
+ };
303
+ }
304
+ // ── HTTP handler ─────────────────────────────────────────────────────────
305
+ // @libsql/client HTTP driver uses batons to keep streams alive across
306
+ // pipeline requests (needed for interactive transactions). Each stream has
307
+ // its own SQL store for store_sql/close_sql scoping.
308
+ let batonCounter = 0;
309
+ const streamStores = new Map();
310
+ export function createHranaHandler(database) {
311
+ return (req, res) => {
312
+ if (req.method === 'GET' && req.url === '/health') {
313
+ res.writeHead(200, { 'content-type': 'application/json' });
314
+ res.end(JSON.stringify({ status: 'ok', pid: process.pid }));
315
+ return;
316
+ }
317
+ if (req.method === 'GET' && req.url === '/v2') {
318
+ res.writeHead(200, { 'content-type': 'application/json' });
319
+ res.end('{"version":"hrana-v2"}');
320
+ return;
321
+ }
322
+ if (req.method === 'POST' && req.url === '/v2/pipeline') {
323
+ const chunks = [];
324
+ let aborted = false;
325
+ req.on('error', () => {
326
+ aborted = true;
327
+ res.destroy();
328
+ });
329
+ req.on('data', (chunk) => {
330
+ chunks.push(chunk);
331
+ });
332
+ req.on('end', () => {
333
+ if (aborted)
334
+ return;
335
+ const parseResult = errore.try({
336
+ try: () => JSON.parse(Buffer.concat(chunks).toString()),
337
+ catch: (e) => e,
338
+ });
339
+ if (parseResult instanceof Error) {
340
+ res.writeHead(400, { 'content-type': 'application/json' });
341
+ res.end(JSON.stringify({
342
+ error: {
343
+ message: parseResult.message,
344
+ code: 'HRANA_PROTO_ERROR',
345
+ },
346
+ }));
347
+ return;
348
+ }
349
+ // Resolve or create per-stream SQL store keyed by baton
350
+ const incoming = parseResult.baton;
351
+ const sqlStore = (incoming ? streamStores.get(incoming) : undefined) ??
352
+ new Map();
353
+ if (incoming)
354
+ streamStores.delete(incoming);
355
+ const results = (parseResult.requests ?? []).map((r) => processRequest(database, r, sqlStore));
356
+ const hasClose = (parseResult.requests ?? []).some((r) => r.type === 'close');
357
+ const baton = hasClose ? null : `b${++batonCounter}`;
358
+ if (baton)
359
+ streamStores.set(baton, sqlStore);
360
+ res.writeHead(200, { 'content-type': 'application/json' });
361
+ res.end(JSON.stringify({ baton, base_url: null, results }));
362
+ });
363
+ return;
364
+ }
365
+ res.writeHead(404);
366
+ res.end();
367
+ };
368
+ }
369
+ // ── Single-instance enforcement ──────────────────────────────────────────
370
+ /**
371
+ * Evict a previous kimaki instance on the lock port.
372
+ * Fetches /health to get the running process PID, then kills it directly.
373
+ * No lsof/netstat/spawnSync needed — the PID comes from the health response.
374
+ */
375
+ export async function evictExistingInstance({ port }) {
376
+ const url = `http://127.0.0.1:${port}/health`;
377
+ const probe = await fetch(url, { signal: AbortSignal.timeout(1000) }).catch((e) => new FetchError({ url, cause: e }));
378
+ if (probe instanceof Error)
379
+ return;
380
+ const body = await probe.json().catch((e) => new FetchError({ url, cause: e }));
381
+ if (body instanceof Error)
382
+ return;
383
+ const targetPid = body.pid;
384
+ if (!targetPid || targetPid === process.pid)
385
+ return;
386
+ hranaLogger.log(`Evicting existing kimaki process (PID: ${targetPid}) on port ${port}`);
387
+ const killResult = errore.try({
388
+ try: () => {
389
+ process.kill(targetPid, 'SIGTERM');
390
+ },
391
+ catch: (e) => e,
392
+ });
393
+ if (killResult instanceof Error) {
394
+ hranaLogger.log(`Failed to kill PID ${targetPid}: ${killResult.message}`);
395
+ return;
396
+ }
397
+ await new Promise((resolve) => {
398
+ setTimeout(resolve, 1000);
399
+ });
400
+ // Verify it's gone — if still alive, escalate to SIGKILL
401
+ const secondProbe = await fetch(url, {
402
+ signal: AbortSignal.timeout(500),
403
+ }).catch((e) => new FetchError({ url, cause: e }));
404
+ if (secondProbe instanceof Error)
405
+ return;
406
+ hranaLogger.log(`PID ${targetPid} still alive after SIGTERM, sending SIGKILL`);
407
+ errore.try({
408
+ try: () => {
409
+ process.kill(targetPid, 'SIGKILL');
410
+ },
411
+ catch: (e) => e,
412
+ });
413
+ await new Promise((resolve) => {
414
+ setTimeout(resolve, 1000);
415
+ });
416
+ }