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