@brianli/kimaki 0.4.72-brianli.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin.js +2 -0
- package/dist/ai-tool-to-genai.js +233 -0
- package/dist/ai-tool-to-genai.test.js +267 -0
- package/dist/ai-tool.js +6 -0
- package/dist/bin.js +87 -0
- package/dist/bot-token.js +121 -0
- package/dist/bot-token.test.js +134 -0
- package/dist/channel-management.js +101 -0
- package/dist/cli-parsing.test.js +89 -0
- package/dist/cli.js +2529 -0
- package/dist/commands/abort.js +82 -0
- package/dist/commands/action-buttons.js +257 -0
- package/dist/commands/add-project.js +114 -0
- package/dist/commands/agent.js +291 -0
- package/dist/commands/ask-question.js +223 -0
- package/dist/commands/compact.js +120 -0
- package/dist/commands/context-usage.js +140 -0
- package/dist/commands/create-new-project.js +118 -0
- package/dist/commands/diff.js +128 -0
- package/dist/commands/file-upload.js +275 -0
- package/dist/commands/fork.js +217 -0
- package/dist/commands/gemini-apikey.js +70 -0
- package/dist/commands/login.js +490 -0
- package/dist/commands/mention-mode.js +51 -0
- package/dist/commands/merge-worktree.js +124 -0
- package/dist/commands/model.js +694 -0
- package/dist/commands/permissions.js +163 -0
- package/dist/commands/queue.js +217 -0
- package/dist/commands/remove-project.js +115 -0
- package/dist/commands/restart-opencode-server.js +116 -0
- package/dist/commands/resume.js +159 -0
- package/dist/commands/run-command.js +79 -0
- package/dist/commands/session-id.js +78 -0
- package/dist/commands/session.js +192 -0
- package/dist/commands/share.js +80 -0
- package/dist/commands/types.js +2 -0
- package/dist/commands/undo-redo.js +159 -0
- package/dist/commands/unset-model.js +152 -0
- package/dist/commands/upgrade.js +42 -0
- package/dist/commands/user-command.js +148 -0
- package/dist/commands/verbosity.js +60 -0
- package/dist/commands/worktree-settings.js +50 -0
- package/dist/commands/worktree.js +299 -0
- package/dist/condense-memory.js +33 -0
- package/dist/config.js +110 -0
- package/dist/database.js +1050 -0
- package/dist/db.js +159 -0
- package/dist/db.test.js +49 -0
- package/dist/discord-api.js +28 -0
- package/dist/discord-auth.js +231 -0
- package/dist/discord-auth.test.js +80 -0
- package/dist/discord-bot.js +997 -0
- package/dist/discord-utils.js +560 -0
- package/dist/discord-utils.test.js +115 -0
- package/dist/errors.js +167 -0
- package/dist/escape-backticks.test.js +429 -0
- package/dist/format-tables.js +122 -0
- package/dist/format-tables.test.js +199 -0
- package/dist/forum-sync/config.js +79 -0
- package/dist/forum-sync/discord-operations.js +154 -0
- package/dist/forum-sync/index.js +5 -0
- package/dist/forum-sync/markdown.js +117 -0
- package/dist/forum-sync/sync-to-discord.js +417 -0
- package/dist/forum-sync/sync-to-files.js +190 -0
- package/dist/forum-sync/types.js +53 -0
- package/dist/forum-sync/watchers.js +307 -0
- package/dist/gateway-consumer.js +232 -0
- package/dist/gateway-consumer.test.js +18 -0
- package/dist/genai-worker-wrapper.js +111 -0
- package/dist/genai-worker.js +311 -0
- package/dist/genai.js +232 -0
- package/dist/generated/browser.js +17 -0
- package/dist/generated/client.js +35 -0
- package/dist/generated/commonInputTypes.js +10 -0
- package/dist/generated/enums.js +30 -0
- package/dist/generated/internal/class.js +41 -0
- package/dist/generated/internal/prismaNamespace.js +239 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js +209 -0
- package/dist/generated/models/bot_api_keys.js +1 -0
- package/dist/generated/models/bot_tokens.js +1 -0
- package/dist/generated/models/channel_agents.js +1 -0
- package/dist/generated/models/channel_directories.js +1 -0
- package/dist/generated/models/channel_mention_mode.js +1 -0
- package/dist/generated/models/channel_models.js +1 -0
- package/dist/generated/models/channel_verbosity.js +1 -0
- package/dist/generated/models/channel_worktrees.js +1 -0
- package/dist/generated/models/forum_sync_configs.js +1 -0
- package/dist/generated/models/global_models.js +1 -0
- package/dist/generated/models/ipc_requests.js +1 -0
- package/dist/generated/models/part_messages.js +1 -0
- package/dist/generated/models/scheduled_tasks.js +1 -0
- package/dist/generated/models/session_agents.js +1 -0
- package/dist/generated/models/session_models.js +1 -0
- package/dist/generated/models/session_start_sources.js +1 -0
- package/dist/generated/models/thread_sessions.js +1 -0
- package/dist/generated/models/thread_worktrees.js +1 -0
- package/dist/generated/models.js +1 -0
- package/dist/heap-monitor.js +95 -0
- package/dist/hrana-server.js +416 -0
- package/dist/hrana-server.test.js +368 -0
- package/dist/image-utils.js +112 -0
- package/dist/interaction-handler.js +327 -0
- package/dist/ipc-polling.js +251 -0
- package/dist/kimaki-digital-twin.e2e.test.js +165 -0
- package/dist/limit-heading-depth.js +25 -0
- package/dist/limit-heading-depth.test.js +105 -0
- package/dist/logger.js +160 -0
- package/dist/markdown.js +342 -0
- package/dist/markdown.test.js +253 -0
- package/dist/message-formatting.js +433 -0
- package/dist/message-formatting.test.js +73 -0
- package/dist/openai-realtime.js +228 -0
- package/dist/opencode-plugin-loading.e2e.test.js +91 -0
- package/dist/opencode-plugin.js +536 -0
- package/dist/opencode-plugin.test.js +98 -0
- package/dist/opencode.js +409 -0
- package/dist/privacy-sanitizer.js +105 -0
- package/dist/runtime-mode.js +51 -0
- package/dist/runtime-mode.test.js +115 -0
- package/dist/sentry.js +127 -0
- package/dist/session-handler/state.js +151 -0
- package/dist/session-handler.js +1874 -0
- package/dist/session-search.js +100 -0
- package/dist/session-search.test.js +40 -0
- package/dist/startup-service.js +153 -0
- package/dist/system-message.js +499 -0
- package/dist/task-runner.js +282 -0
- package/dist/task-schedule.js +191 -0
- package/dist/task-schedule.test.js +71 -0
- package/dist/thinking-utils.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +781 -0
- package/dist/tools.js +359 -0
- package/dist/unnest-code-blocks.js +136 -0
- package/dist/unnest-code-blocks.test.js +641 -0
- package/dist/upgrade.js +114 -0
- package/dist/utils.js +109 -0
- package/dist/voice-handler.js +606 -0
- package/dist/voice.js +304 -0
- package/dist/voice.test.js +187 -0
- package/dist/wait-session.js +94 -0
- package/dist/worker-types.js +4 -0
- package/dist/worktree-utils.js +727 -0
- package/dist/xml.js +92 -0
- package/dist/xml.test.js +32 -0
- package/package.json +82 -0
- package/schema.prisma +246 -0
- package/skills/batch/SKILL.md +87 -0
- package/skills/critique/SKILL.md +129 -0
- package/skills/errore/SKILL.md +589 -0
- package/skills/goke/.prettierrc +5 -0
- package/skills/goke/CHANGELOG.md +40 -0
- package/skills/goke/LICENSE +21 -0
- package/skills/goke/README.md +666 -0
- package/skills/goke/SKILL.md +458 -0
- package/skills/goke/package.json +43 -0
- package/skills/goke/src/__test__/coerce.test.ts +411 -0
- package/skills/goke/src/__test__/index.test.ts +1798 -0
- package/skills/goke/src/__test__/types.test-d.ts +111 -0
- package/skills/goke/src/coerce.ts +547 -0
- package/skills/goke/src/goke.ts +1362 -0
- package/skills/goke/src/index.ts +16 -0
- package/skills/goke/src/mri.ts +164 -0
- package/skills/goke/tsconfig.json +15 -0
- package/skills/jitter/EDITOR.md +219 -0
- package/skills/jitter/EXPORT-INTERNALS.md +309 -0
- package/skills/jitter/SKILL.md +158 -0
- package/skills/jitter/jitter-clipboard.json +1042 -0
- package/skills/jitter/package.json +14 -0
- package/skills/jitter/tsconfig.json +15 -0
- package/skills/jitter/utils/actions.ts +212 -0
- package/skills/jitter/utils/export.ts +114 -0
- package/skills/jitter/utils/index.ts +141 -0
- package/skills/jitter/utils/snapshot.ts +154 -0
- package/skills/jitter/utils/traverse.ts +246 -0
- package/skills/jitter/utils/types.ts +279 -0
- package/skills/jitter/utils/wait.ts +133 -0
- package/skills/playwriter/SKILL.md +31 -0
- package/skills/security-review/SKILL.md +208 -0
- package/skills/simplify/SKILL.md +58 -0
- package/skills/termcast/SKILL.md +945 -0
- package/skills/tuistory/SKILL.md +250 -0
- package/skills/zustand-centralized-state/SKILL.md +582 -0
- package/src/__snapshots__/compact-session-context-no-system.md +35 -0
- package/src/__snapshots__/compact-session-context.md +41 -0
- package/src/__snapshots__/first-session-no-info.md +17 -0
- package/src/__snapshots__/first-session-with-info.md +23 -0
- package/src/__snapshots__/session-1.md +17 -0
- package/src/__snapshots__/session-2.md +5871 -0
- package/src/__snapshots__/session-3.md +17 -0
- package/src/__snapshots__/session-with-tools.md +5871 -0
- package/src/ai-tool-to-genai.test.ts +296 -0
- package/src/ai-tool-to-genai.ts +282 -0
- package/src/ai-tool.ts +39 -0
- package/src/bin.ts +108 -0
- package/src/bot-token.test.ts +171 -0
- package/src/bot-token.ts +159 -0
- package/src/channel-management.ts +172 -0
- package/src/cli-parsing.test.ts +132 -0
- package/src/cli.ts +3605 -0
- package/src/commands/abort.ts +112 -0
- package/src/commands/action-buttons.ts +376 -0
- package/src/commands/add-project.ts +152 -0
- package/src/commands/agent.ts +404 -0
- package/src/commands/ask-question.ts +330 -0
- package/src/commands/compact.ts +157 -0
- package/src/commands/context-usage.ts +199 -0
- package/src/commands/create-new-project.ts +179 -0
- package/src/commands/diff.ts +165 -0
- package/src/commands/file-upload.ts +389 -0
- package/src/commands/fork.ts +320 -0
- package/src/commands/gemini-apikey.ts +104 -0
- package/src/commands/login.ts +634 -0
- package/src/commands/mention-mode.ts +77 -0
- package/src/commands/merge-worktree.ts +177 -0
- package/src/commands/model.ts +961 -0
- package/src/commands/permissions.ts +261 -0
- package/src/commands/queue.ts +296 -0
- package/src/commands/remove-project.ts +155 -0
- package/src/commands/restart-opencode-server.ts +162 -0
- package/src/commands/resume.ts +242 -0
- package/src/commands/run-command.ts +123 -0
- package/src/commands/session-id.ts +109 -0
- package/src/commands/session.ts +250 -0
- package/src/commands/share.ts +106 -0
- package/src/commands/types.ts +25 -0
- package/src/commands/undo-redo.ts +221 -0
- package/src/commands/unset-model.ts +189 -0
- package/src/commands/upgrade.ts +52 -0
- package/src/commands/user-command.ts +193 -0
- package/src/commands/verbosity.ts +88 -0
- package/src/commands/worktree-settings.ts +79 -0
- package/src/commands/worktree.ts +431 -0
- package/src/condense-memory.ts +36 -0
- package/src/config.ts +148 -0
- package/src/database.ts +1530 -0
- package/src/db.test.ts +60 -0
- package/src/db.ts +190 -0
- package/src/discord-api.ts +35 -0
- package/src/discord-bot.ts +1316 -0
- package/src/discord-utils.test.ts +132 -0
- package/src/discord-utils.ts +767 -0
- package/src/errors.ts +213 -0
- package/src/escape-backticks.test.ts +469 -0
- package/src/format-tables.test.ts +223 -0
- package/src/format-tables.ts +145 -0
- package/src/forum-sync/config.ts +92 -0
- package/src/forum-sync/discord-operations.ts +241 -0
- package/src/forum-sync/index.ts +9 -0
- package/src/forum-sync/markdown.ts +176 -0
- package/src/forum-sync/sync-to-discord.ts +595 -0
- package/src/forum-sync/sync-to-files.ts +294 -0
- package/src/forum-sync/types.ts +175 -0
- package/src/forum-sync/watchers.ts +454 -0
- package/src/genai-worker-wrapper.ts +164 -0
- package/src/genai-worker.ts +386 -0
- package/src/genai.ts +321 -0
- package/src/generated/browser.ts +109 -0
- package/src/generated/client.ts +131 -0
- package/src/generated/commonInputTypes.ts +512 -0
- package/src/generated/enums.ts +46 -0
- package/src/generated/internal/class.ts +362 -0
- package/src/generated/internal/prismaNamespace.ts +2251 -0
- package/src/generated/internal/prismaNamespaceBrowser.ts +308 -0
- package/src/generated/models/bot_api_keys.ts +1288 -0
- package/src/generated/models/bot_tokens.ts +1577 -0
- package/src/generated/models/channel_agents.ts +1256 -0
- package/src/generated/models/channel_directories.ts +2104 -0
- package/src/generated/models/channel_mention_mode.ts +1300 -0
- package/src/generated/models/channel_models.ts +1288 -0
- package/src/generated/models/channel_verbosity.ts +1224 -0
- package/src/generated/models/channel_worktrees.ts +1308 -0
- package/src/generated/models/forum_sync_configs.ts +1452 -0
- package/src/generated/models/global_models.ts +1288 -0
- package/src/generated/models/ipc_requests.ts +1485 -0
- package/src/generated/models/part_messages.ts +1302 -0
- package/src/generated/models/scheduled_tasks.ts +2320 -0
- package/src/generated/models/session_agents.ts +1086 -0
- package/src/generated/models/session_models.ts +1114 -0
- package/src/generated/models/session_start_sources.ts +1408 -0
- package/src/generated/models/thread_sessions.ts +1599 -0
- package/src/generated/models/thread_worktrees.ts +1352 -0
- package/src/generated/models.ts +29 -0
- package/src/heap-monitor.ts +121 -0
- package/src/hrana-server.test.ts +428 -0
- package/src/hrana-server.ts +547 -0
- package/src/image-utils.ts +149 -0
- package/src/interaction-handler.ts +461 -0
- package/src/ipc-polling.ts +325 -0
- package/src/kimaki-digital-twin.e2e.test.ts +201 -0
- package/src/limit-heading-depth.test.ts +116 -0
- package/src/limit-heading-depth.ts +26 -0
- package/src/logger.ts +203 -0
- package/src/markdown.test.ts +360 -0
- package/src/markdown.ts +410 -0
- package/src/message-formatting.test.ts +81 -0
- package/src/message-formatting.ts +549 -0
- package/src/openai-realtime.ts +362 -0
- package/src/opencode-plugin-loading.e2e.test.ts +112 -0
- package/src/opencode-plugin.test.ts +108 -0
- package/src/opencode-plugin.ts +652 -0
- package/src/opencode.ts +554 -0
- package/src/privacy-sanitizer.ts +142 -0
- package/src/schema.sql +158 -0
- package/src/sentry.ts +137 -0
- package/src/session-handler/state.ts +232 -0
- package/src/session-handler.ts +2668 -0
- package/src/session-search.test.ts +50 -0
- package/src/session-search.ts +148 -0
- package/src/startup-service.ts +200 -0
- package/src/system-message.ts +568 -0
- package/src/task-runner.ts +425 -0
- package/src/task-schedule.test.ts +84 -0
- package/src/task-schedule.ts +287 -0
- package/src/thinking-utils.ts +61 -0
- package/src/thread-message-queue.e2e.test.ts +997 -0
- package/src/tools.ts +432 -0
- package/src/unnest-code-blocks.test.ts +679 -0
- package/src/unnest-code-blocks.ts +168 -0
- package/src/upgrade.ts +127 -0
- package/src/utils.ts +145 -0
- package/src/voice-handler.ts +852 -0
- package/src/voice.test.ts +219 -0
- package/src/voice.ts +444 -0
- package/src/wait-session.ts +147 -0
- package/src/worker-types.ts +64 -0
- package/src/worktree-utils.ts +988 -0
- package/src/xml.test.ts +38 -0
- package/src/xml.ts +121 -0
|
@@ -0,0 +1,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
|
+
}
|