kimaki 0.4.82 → 0.4.84
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/LICENSE +21 -0
- package/dist/anthropic-auth-plugin.js +7 -0
- package/dist/cli.js +51 -7
- package/dist/commands/abort.js +5 -16
- package/dist/commands/action-buttons.js +3 -3
- package/dist/commands/add-project.js +1 -1
- package/dist/commands/ask-question.js +3 -3
- package/dist/commands/context-usage.js +1 -1
- package/dist/commands/create-new-project.js +1 -1
- package/dist/commands/fork.js +11 -8
- package/dist/commands/merge-worktree.js +1 -1
- package/dist/commands/new-worktree.js +63 -44
- package/dist/commands/remove-project.js +1 -1
- package/dist/commands/resume.js +11 -8
- package/dist/commands/screenshare.js +14 -6
- package/dist/commands/screenshare.test.js +20 -0
- package/dist/commands/session.js +1 -1
- package/dist/commands/undo-redo.js +91 -7
- package/dist/commands/user-command.js +1 -1
- package/dist/config.js +16 -1
- package/dist/database.js +53 -2
- package/dist/db.js +6 -0
- package/dist/discord-bot.js +48 -85
- package/dist/discord-command-registration.js +1 -1
- package/dist/external-opencode-sync.js +515 -0
- package/dist/external-opencode-sync.test.js +151 -0
- package/dist/gateway-proxy.e2e.test.js +8 -5
- package/dist/genai.js +1 -1
- package/dist/generated/enums.js +4 -0
- package/dist/generated/internal/class.js +4 -4
- package/dist/generated/internal/prismaNamespace.js +1 -0
- package/dist/generated/internal/prismaNamespaceBrowser.js +1 -0
- package/dist/generated/models/external_session_pending_prompts.js +1 -0
- package/dist/hrana-server.js +14 -285
- package/dist/hrana-server.test.js +4 -2
- package/dist/kimaki-opencode-plugin-loading.e2e.test.js +7 -0
- package/dist/kimaki-opencode-plugin.js +2 -0
- package/dist/kitty-graphics-parser.js +3 -0
- package/dist/kitty-graphics-parser.test.js +276 -0
- package/dist/kitty-graphics-plugin.js +3 -0
- package/dist/markdown.js +4 -4
- package/dist/markdown.test.js +1 -1
- package/dist/message-formatting.js +54 -15
- package/dist/onboarding-tutorial.js +1 -1
- package/dist/openai-realtime.js +9 -13
- package/dist/opencode.js +28 -5
- package/dist/queue-advanced-e2e-setup.js +89 -0
- package/dist/queue-advanced-permissions-typing.e2e.test.js +5 -5
- package/dist/queue-advanced-typing.e2e.test.js +9 -22
- package/dist/queue-question-select-drain.e2e.test.js +117 -0
- package/dist/session-handler/event-stream-state.js +101 -7
- package/dist/session-handler/event-stream-state.test.js +7 -3
- package/dist/session-handler/thread-session-runtime.js +120 -9
- package/dist/store.js +1 -0
- package/dist/system-message.js +22 -4
- package/dist/system-message.test.js +19 -0
- package/dist/task-runner.js +1 -1
- package/dist/thread-message-queue.e2e.test.js +8 -14
- package/dist/tools.js +1 -1
- package/dist/undo-redo.e2e.test.js +20 -25
- package/package.json +10 -6
- package/schema.prisma +6 -0
- package/skills/errore/SKILL.md +40 -13
- package/skills/goke/SKILL.md +12 -0
- package/skills/lintcn/SKILL.md +868 -0
- package/skills/npm-package/SKILL.md +1 -0
- package/skills/proxyman/SKILL.md +215 -0
- package/skills/spiceflow/SKILL.md +1 -1
- package/skills/usecomputer/SKILL.md +339 -0
- package/src/ai-tool-to-genai.ts +1 -0
- package/src/anthropic-auth-plugin.ts +7 -0
- package/src/cli.ts +59 -6
- package/src/commands/abort.ts +6 -16
- package/src/commands/action-buttons.ts +5 -1
- package/src/commands/add-project.ts +1 -1
- package/src/commands/ask-question.ts +5 -2
- package/src/commands/context-usage.ts +1 -1
- package/src/commands/create-new-project.ts +1 -1
- package/src/commands/fork.ts +12 -11
- package/src/commands/merge-worktree.ts +1 -1
- package/src/commands/new-worktree.ts +74 -55
- package/src/commands/remove-project.ts +1 -1
- package/src/commands/resume.ts +12 -10
- package/src/commands/screenshare.test.ts +30 -0
- package/src/commands/screenshare.ts +18 -6
- package/src/commands/session.ts +1 -1
- package/src/commands/undo-redo.ts +108 -10
- package/src/commands/user-command.ts +1 -1
- package/src/config.ts +19 -1
- package/src/database.ts +72 -3
- package/src/db.ts +8 -0
- package/src/discord-bot.ts +58 -93
- package/src/discord-command-registration.ts +1 -1
- package/src/external-opencode-sync.ts +729 -0
- package/src/gateway-proxy.e2e.test.ts +9 -5
- package/src/genai.ts +3 -3
- package/src/generated/commonInputTypes.ts +34 -0
- package/src/generated/enums.ts +8 -0
- package/src/generated/internal/class.ts +4 -4
- package/src/generated/internal/prismaNamespace.ts +8 -0
- package/src/generated/internal/prismaNamespaceBrowser.ts +1 -0
- package/src/generated/models/thread_sessions.ts +53 -1
- package/src/hrana-server.test.ts +8 -2
- package/src/hrana-server.ts +18 -390
- package/src/kimaki-opencode-plugin-loading.e2e.test.ts +7 -0
- package/src/kimaki-opencode-plugin.ts +2 -0
- package/src/markdown.test.ts +1 -1
- package/src/markdown.ts +4 -4
- package/src/message-formatting.ts +66 -17
- package/src/onboarding-tutorial.ts +1 -1
- package/src/openai-realtime.ts +6 -10
- package/src/opencode.ts +31 -7
- package/src/queue-advanced-e2e-setup.ts +92 -0
- package/src/queue-advanced-permissions-typing.e2e.test.ts +5 -5
- package/src/queue-advanced-typing.e2e.test.ts +9 -22
- package/src/queue-question-select-drain.e2e.test.ts +149 -0
- package/src/schema.sql +1 -0
- package/src/session-handler/event-stream-state.test.ts +7 -2
- package/src/session-handler/event-stream-state.ts +128 -7
- package/src/session-handler/thread-runtime-state.ts +5 -0
- package/src/session-handler/thread-session-runtime.ts +153 -11
- package/src/store.ts +8 -0
- package/src/system-message.ts +27 -4
- package/src/task-runner.ts +1 -1
- package/src/thread-message-queue.e2e.test.ts +8 -14
- package/src/tools.ts +1 -1
- package/src/undo-redo.e2e.test.ts +28 -26
- package/skills/jitter/node_modules/.bin/esbuild +0 -21
- package/skills/jitter/node_modules/.bin/tsc +0 -21
- package/skills/jitter/node_modules/.bin/tsserver +0 -21
- package/skills/jitter/node_modules/typescript/LICENSE.txt +0 -55
- package/skills/jitter/node_modules/typescript/README.md +0 -50
- package/skills/jitter/node_modules/typescript/SECURITY.md +0 -41
- package/skills/jitter/node_modules/typescript/ThirdPartyNoticeText.txt +0 -193
- package/skills/jitter/node_modules/typescript/bin/tsc +0 -2
- package/skills/jitter/node_modules/typescript/bin/tsserver +0 -2
- package/skills/jitter/node_modules/typescript/lib/_tsc.js +0 -133792
- package/skills/jitter/node_modules/typescript/lib/_tsserver.js +0 -659
- package/skills/jitter/node_modules/typescript/lib/_typingsInstaller.js +0 -222
- package/skills/jitter/node_modules/typescript/lib/cs/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/lib/de/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/lib/es/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/lib/fr/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/lib/it/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/lib/ja/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/lib/ko/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/lib/lib.d.ts +0 -22
- package/skills/jitter/node_modules/typescript/lib/lib.decorators.d.ts +0 -384
- package/skills/jitter/node_modules/typescript/lib/lib.decorators.legacy.d.ts +0 -22
- package/skills/jitter/node_modules/typescript/lib/lib.dom.asynciterable.d.ts +0 -41
- package/skills/jitter/node_modules/typescript/lib/lib.dom.d.ts +0 -39429
- package/skills/jitter/node_modules/typescript/lib/lib.dom.iterable.d.ts +0 -571
- package/skills/jitter/node_modules/typescript/lib/lib.es2015.collection.d.ts +0 -147
- package/skills/jitter/node_modules/typescript/lib/lib.es2015.core.d.ts +0 -597
- package/skills/jitter/node_modules/typescript/lib/lib.es2015.d.ts +0 -28
- package/skills/jitter/node_modules/typescript/lib/lib.es2015.generator.d.ts +0 -77
- package/skills/jitter/node_modules/typescript/lib/lib.es2015.iterable.d.ts +0 -605
- package/skills/jitter/node_modules/typescript/lib/lib.es2015.promise.d.ts +0 -81
- package/skills/jitter/node_modules/typescript/lib/lib.es2015.proxy.d.ts +0 -128
- package/skills/jitter/node_modules/typescript/lib/lib.es2015.reflect.d.ts +0 -144
- package/skills/jitter/node_modules/typescript/lib/lib.es2015.symbol.d.ts +0 -46
- package/skills/jitter/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts +0 -326
- package/skills/jitter/node_modules/typescript/lib/lib.es2016.array.include.d.ts +0 -116
- package/skills/jitter/node_modules/typescript/lib/lib.es2016.d.ts +0 -21
- package/skills/jitter/node_modules/typescript/lib/lib.es2016.full.d.ts +0 -23
- package/skills/jitter/node_modules/typescript/lib/lib.es2016.intl.d.ts +0 -31
- package/skills/jitter/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts +0 -21
- package/skills/jitter/node_modules/typescript/lib/lib.es2017.d.ts +0 -26
- package/skills/jitter/node_modules/typescript/lib/lib.es2017.date.d.ts +0 -31
- package/skills/jitter/node_modules/typescript/lib/lib.es2017.full.d.ts +0 -23
- package/skills/jitter/node_modules/typescript/lib/lib.es2017.intl.d.ts +0 -44
- package/skills/jitter/node_modules/typescript/lib/lib.es2017.object.d.ts +0 -49
- package/skills/jitter/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts +0 -135
- package/skills/jitter/node_modules/typescript/lib/lib.es2017.string.d.ts +0 -45
- package/skills/jitter/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts +0 -53
- package/skills/jitter/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts +0 -77
- package/skills/jitter/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts +0 -53
- package/skills/jitter/node_modules/typescript/lib/lib.es2018.d.ts +0 -24
- package/skills/jitter/node_modules/typescript/lib/lib.es2018.full.d.ts +0 -24
- package/skills/jitter/node_modules/typescript/lib/lib.es2018.intl.d.ts +0 -83
- package/skills/jitter/node_modules/typescript/lib/lib.es2018.promise.d.ts +0 -30
- package/skills/jitter/node_modules/typescript/lib/lib.es2018.regexp.d.ts +0 -37
- package/skills/jitter/node_modules/typescript/lib/lib.es2019.array.d.ts +0 -79
- package/skills/jitter/node_modules/typescript/lib/lib.es2019.d.ts +0 -24
- package/skills/jitter/node_modules/typescript/lib/lib.es2019.full.d.ts +0 -24
- package/skills/jitter/node_modules/typescript/lib/lib.es2019.intl.d.ts +0 -23
- package/skills/jitter/node_modules/typescript/lib/lib.es2019.object.d.ts +0 -33
- package/skills/jitter/node_modules/typescript/lib/lib.es2019.string.d.ts +0 -37
- package/skills/jitter/node_modules/typescript/lib/lib.es2019.symbol.d.ts +0 -24
- package/skills/jitter/node_modules/typescript/lib/lib.es2020.bigint.d.ts +0 -765
- package/skills/jitter/node_modules/typescript/lib/lib.es2020.d.ts +0 -27
- package/skills/jitter/node_modules/typescript/lib/lib.es2020.date.d.ts +0 -42
- package/skills/jitter/node_modules/typescript/lib/lib.es2020.full.d.ts +0 -24
- package/skills/jitter/node_modules/typescript/lib/lib.es2020.intl.d.ts +0 -474
- package/skills/jitter/node_modules/typescript/lib/lib.es2020.number.d.ts +0 -28
- package/skills/jitter/node_modules/typescript/lib/lib.es2020.promise.d.ts +0 -47
- package/skills/jitter/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts +0 -99
- package/skills/jitter/node_modules/typescript/lib/lib.es2020.string.d.ts +0 -44
- package/skills/jitter/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts +0 -41
- package/skills/jitter/node_modules/typescript/lib/lib.es2021.d.ts +0 -23
- package/skills/jitter/node_modules/typescript/lib/lib.es2021.full.d.ts +0 -24
- package/skills/jitter/node_modules/typescript/lib/lib.es2021.intl.d.ts +0 -166
- package/skills/jitter/node_modules/typescript/lib/lib.es2021.promise.d.ts +0 -48
- package/skills/jitter/node_modules/typescript/lib/lib.es2021.string.d.ts +0 -33
- package/skills/jitter/node_modules/typescript/lib/lib.es2021.weakref.d.ts +0 -78
- package/skills/jitter/node_modules/typescript/lib/lib.es2022.array.d.ts +0 -121
- package/skills/jitter/node_modules/typescript/lib/lib.es2022.d.ts +0 -25
- package/skills/jitter/node_modules/typescript/lib/lib.es2022.error.d.ts +0 -75
- package/skills/jitter/node_modules/typescript/lib/lib.es2022.full.d.ts +0 -24
- package/skills/jitter/node_modules/typescript/lib/lib.es2022.intl.d.ts +0 -145
- package/skills/jitter/node_modules/typescript/lib/lib.es2022.object.d.ts +0 -26
- package/skills/jitter/node_modules/typescript/lib/lib.es2022.regexp.d.ts +0 -39
- package/skills/jitter/node_modules/typescript/lib/lib.es2022.string.d.ts +0 -25
- package/skills/jitter/node_modules/typescript/lib/lib.es2023.array.d.ts +0 -924
- package/skills/jitter/node_modules/typescript/lib/lib.es2023.collection.d.ts +0 -21
- package/skills/jitter/node_modules/typescript/lib/lib.es2023.d.ts +0 -22
- package/skills/jitter/node_modules/typescript/lib/lib.es2023.full.d.ts +0 -24
- package/skills/jitter/node_modules/typescript/lib/lib.es2023.intl.d.ts +0 -56
- package/skills/jitter/node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts +0 -65
- package/skills/jitter/node_modules/typescript/lib/lib.es2024.collection.d.ts +0 -29
- package/skills/jitter/node_modules/typescript/lib/lib.es2024.d.ts +0 -26
- package/skills/jitter/node_modules/typescript/lib/lib.es2024.full.d.ts +0 -24
- package/skills/jitter/node_modules/typescript/lib/lib.es2024.object.d.ts +0 -29
- package/skills/jitter/node_modules/typescript/lib/lib.es2024.promise.d.ts +0 -35
- package/skills/jitter/node_modules/typescript/lib/lib.es2024.regexp.d.ts +0 -25
- package/skills/jitter/node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts +0 -68
- package/skills/jitter/node_modules/typescript/lib/lib.es2024.string.d.ts +0 -29
- package/skills/jitter/node_modules/typescript/lib/lib.es5.d.ts +0 -4601
- package/skills/jitter/node_modules/typescript/lib/lib.es6.d.ts +0 -23
- package/skills/jitter/node_modules/typescript/lib/lib.esnext.array.d.ts +0 -35
- package/skills/jitter/node_modules/typescript/lib/lib.esnext.collection.d.ts +0 -96
- package/skills/jitter/node_modules/typescript/lib/lib.esnext.d.ts +0 -29
- package/skills/jitter/node_modules/typescript/lib/lib.esnext.decorators.d.ts +0 -28
- package/skills/jitter/node_modules/typescript/lib/lib.esnext.disposable.d.ts +0 -193
- package/skills/jitter/node_modules/typescript/lib/lib.esnext.error.d.ts +0 -24
- package/skills/jitter/node_modules/typescript/lib/lib.esnext.float16.d.ts +0 -443
- package/skills/jitter/node_modules/typescript/lib/lib.esnext.full.d.ts +0 -24
- package/skills/jitter/node_modules/typescript/lib/lib.esnext.intl.d.ts +0 -21
- package/skills/jitter/node_modules/typescript/lib/lib.esnext.iterator.d.ts +0 -148
- package/skills/jitter/node_modules/typescript/lib/lib.esnext.promise.d.ts +0 -34
- package/skills/jitter/node_modules/typescript/lib/lib.esnext.sharedmemory.d.ts +0 -25
- package/skills/jitter/node_modules/typescript/lib/lib.scripthost.d.ts +0 -322
- package/skills/jitter/node_modules/typescript/lib/lib.webworker.asynciterable.d.ts +0 -41
- package/skills/jitter/node_modules/typescript/lib/lib.webworker.d.ts +0 -13150
- package/skills/jitter/node_modules/typescript/lib/lib.webworker.importscripts.d.ts +0 -23
- package/skills/jitter/node_modules/typescript/lib/lib.webworker.iterable.d.ts +0 -340
- package/skills/jitter/node_modules/typescript/lib/pl/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/lib/pt-br/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/lib/ru/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/lib/tr/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/lib/tsc.js +0 -8
- package/skills/jitter/node_modules/typescript/lib/tsserver.js +0 -8
- package/skills/jitter/node_modules/typescript/lib/tsserverlibrary.d.ts +0 -17
- package/skills/jitter/node_modules/typescript/lib/tsserverlibrary.js +0 -21
- package/skills/jitter/node_modules/typescript/lib/typesMap.json +0 -497
- package/skills/jitter/node_modules/typescript/lib/typescript.d.ts +0 -11438
- package/skills/jitter/node_modules/typescript/lib/typescript.js +0 -200253
- package/skills/jitter/node_modules/typescript/lib/typingsInstaller.js +0 -8
- package/skills/jitter/node_modules/typescript/lib/watchGuard.js +0 -53
- package/skills/jitter/node_modules/typescript/lib/zh-cn/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/lib/zh-tw/diagnosticMessages.generated.json +0 -2122
- package/skills/jitter/node_modules/typescript/node_modules/.bin/tsc +0 -21
- package/skills/jitter/node_modules/typescript/node_modules/.bin/tsserver +0 -21
- package/skills/jitter/node_modules/typescript/package.json +0 -120
package/dist/hrana-server.js
CHANGED
|
@@ -2,38 +2,19 @@
|
|
|
2
2
|
// Backed by the `libsql` npm package (better-sqlite3 API).
|
|
3
3
|
// Binds to the fixed lock port for single-instance enforcement.
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
5
|
+
// Protocol logic is implemented in the `libsqlproxy` package.
|
|
6
|
+
// This file handles: server lifecycle, single-instance enforcement,
|
|
7
|
+
// auth, and kimaki-specific endpoints (/kimaki/wake, /health).
|
|
7
8
|
//
|
|
8
9
|
// Hrana v2 protocol spec ("Hrana over HTTP"):
|
|
9
10
|
// https://github.com/tursodatabase/libsql/blob/main/docs/HTTP_V2_SPEC.md
|
|
10
|
-
//
|
|
11
|
-
// The protocol exposes stateful streams over HTTP. Each stream corresponds
|
|
12
|
-
// to a SQLite connection. Requests on the same stream are tied together
|
|
13
|
-
// via a "baton" — the server returns a baton in every response, and the
|
|
14
|
-
// client includes it in the next request. Stream-scoped state includes
|
|
15
|
-
// SQL text cached via store_sql (referenced by sql_id in later stmts).
|
|
16
|
-
//
|
|
17
|
-
// Request types implemented:
|
|
18
|
-
// execute — run a single SQL statement, return cols/rows/changes
|
|
19
|
-
// batch — run multiple steps with conditional execution (ok/not/and/or)
|
|
20
|
-
// sequence — split raw SQL by semicolons, execute each (no results)
|
|
21
|
-
// store_sql — cache SQL text under a numeric sql_id for the stream
|
|
22
|
-
// close_sql — remove a cached sql_id
|
|
23
|
-
// close — close the stream (baton becomes null)
|
|
24
|
-
//
|
|
25
|
-
// Value encoding (SQLite → Hrana JSON):
|
|
26
|
-
// INTEGER → {"type":"integer","value":"42"} (string, not number)
|
|
27
|
-
// REAL → {"type":"float","value":3.14}
|
|
28
|
-
// TEXT → {"type":"text","value":"hello"}
|
|
29
|
-
// BLOB → {"type":"blob","base64":"..."}
|
|
30
|
-
// NULL → {"type":"null"}
|
|
31
11
|
import fs from 'node:fs';
|
|
32
12
|
import http from 'node:http';
|
|
33
13
|
import path from 'node:path';
|
|
34
14
|
import crypto from 'node:crypto';
|
|
35
15
|
import Database from 'libsql';
|
|
36
16
|
import * as errore from 'errore';
|
|
17
|
+
import { createLibsqlHandler, createLibsqlNodeHandler, libsqlExecutor, } from 'libsqlproxy';
|
|
37
18
|
import { createLogger, LogPrefix } from './logger.js';
|
|
38
19
|
import { ServerStartError, FetchError } from './errors.js';
|
|
39
20
|
import { getLockPort } from './config.js';
|
|
@@ -132,8 +113,10 @@ export async function startHranaServer({ dbPath, bindAll = false, }) {
|
|
|
132
113
|
database.exec('PRAGMA journal_mode = WAL');
|
|
133
114
|
database.exec('PRAGMA busy_timeout = 5000');
|
|
134
115
|
db = database;
|
|
135
|
-
|
|
136
|
-
|
|
116
|
+
// Create the Hrana handler using libsqlproxy
|
|
117
|
+
const hranaFetchHandler = createLibsqlHandler(libsqlExecutor(database));
|
|
118
|
+
const hranaNodeHandler = createLibsqlNodeHandler(hranaFetchHandler);
|
|
119
|
+
// Combined handler: kimaki-specific endpoints + hrana protocol
|
|
137
120
|
const handler = async (req, res) => {
|
|
138
121
|
const pathname = new URL(req.url || '/', 'http://localhost').pathname;
|
|
139
122
|
if (pathname === '/kimaki/wake') {
|
|
@@ -157,18 +140,20 @@ export async function startHranaServer({ dbPath, bindAll = false, }) {
|
|
|
157
140
|
res.end(JSON.stringify({ ready: true }));
|
|
158
141
|
return;
|
|
159
142
|
}
|
|
160
|
-
//
|
|
143
|
+
// Health check — no auth required
|
|
161
144
|
if (pathname === '/health') {
|
|
162
|
-
|
|
145
|
+
res.writeHead(200, { 'content-type': 'application/json' });
|
|
146
|
+
res.end(JSON.stringify({ status: 'ok', pid: process.pid }));
|
|
163
147
|
return;
|
|
164
148
|
}
|
|
149
|
+
// Hrana routes: /v2, /v2/pipeline — require auth
|
|
165
150
|
if (pathname === '/v2' || pathname === '/v2/pipeline') {
|
|
166
151
|
if (!isAuthorizedRequest(req)) {
|
|
167
152
|
res.writeHead(401, { 'content-type': 'application/json' });
|
|
168
153
|
res.end(JSON.stringify({ error: 'unauthorized' }));
|
|
169
154
|
return;
|
|
170
155
|
}
|
|
171
|
-
|
|
156
|
+
hranaNodeHandler(req, res);
|
|
172
157
|
return;
|
|
173
158
|
}
|
|
174
159
|
res.writeHead(404);
|
|
@@ -220,263 +205,7 @@ export async function stopHranaServer() {
|
|
|
220
205
|
readyWaiters = [];
|
|
221
206
|
hranaLogger.log('Hrana server stopped');
|
|
222
207
|
}
|
|
223
|
-
// ──
|
|
224
|
-
function encodeValue(val) {
|
|
225
|
-
if (val === null || val === undefined)
|
|
226
|
-
return { type: 'null' };
|
|
227
|
-
if (typeof val === 'bigint')
|
|
228
|
-
return { type: 'integer', value: val.toString() };
|
|
229
|
-
if (typeof val === 'number') {
|
|
230
|
-
if (Number.isInteger(val))
|
|
231
|
-
return { type: 'integer', value: val.toString() };
|
|
232
|
-
return { type: 'float', value: val };
|
|
233
|
-
}
|
|
234
|
-
if (typeof val === 'string')
|
|
235
|
-
return { type: 'text', value: val };
|
|
236
|
-
if (Buffer.isBuffer(val))
|
|
237
|
-
return { type: 'blob', base64: val.toString('base64') };
|
|
238
|
-
if (val instanceof Uint8Array)
|
|
239
|
-
return { type: 'blob', base64: Buffer.from(val).toString('base64') };
|
|
240
|
-
return { type: 'text', value: String(val) };
|
|
241
|
-
}
|
|
242
|
-
function decodeValue(val) {
|
|
243
|
-
if (val.type === 'null')
|
|
244
|
-
return null;
|
|
245
|
-
if (val.type === 'integer') {
|
|
246
|
-
const n = Number(val.value);
|
|
247
|
-
return Number.isSafeInteger(n) ? n : BigInt(val.value);
|
|
248
|
-
}
|
|
249
|
-
if (val.type === 'float')
|
|
250
|
-
return val.value;
|
|
251
|
-
if (val.type === 'text')
|
|
252
|
-
return val.value;
|
|
253
|
-
if (val.type === 'blob')
|
|
254
|
-
return Buffer.from(val.base64, 'base64');
|
|
255
|
-
return null;
|
|
256
|
-
}
|
|
257
|
-
// ── Statement execution ──────────────────────────────────────────────────
|
|
258
|
-
// SqliteError from libsql has a `code` property but catch gives Error.
|
|
259
|
-
function getSqliteErrorCode(err) {
|
|
260
|
-
return err.code ?? 'SQLITE_ERROR';
|
|
261
|
-
}
|
|
262
|
-
function resolveStmtSql(stmt, sqlStore) {
|
|
263
|
-
if (stmt.sql != null)
|
|
264
|
-
return stmt.sql;
|
|
265
|
-
if (stmt.sql_id != null)
|
|
266
|
-
return sqlStore.get(stmt.sql_id) ?? '';
|
|
267
|
-
return '';
|
|
268
|
-
}
|
|
269
|
-
function bindParams(stmt) {
|
|
270
|
-
if (stmt.named_args && stmt.named_args.length > 0) {
|
|
271
|
-
const named = {};
|
|
272
|
-
for (const na of stmt.named_args) {
|
|
273
|
-
named[na.name] = decodeValue(na.value);
|
|
274
|
-
}
|
|
275
|
-
return [named];
|
|
276
|
-
}
|
|
277
|
-
return (stmt.args ?? []).map(decodeValue);
|
|
278
|
-
}
|
|
279
|
-
function executeStmt(database, stmt, sqlStore) {
|
|
280
|
-
const sql = resolveStmtSql(stmt, sqlStore);
|
|
281
|
-
const prepared = database.prepare(sql);
|
|
282
|
-
const params = bindParams(stmt);
|
|
283
|
-
if (prepared.reader) {
|
|
284
|
-
const cols = prepared.columns();
|
|
285
|
-
const rows = prepared.all(...params);
|
|
286
|
-
return {
|
|
287
|
-
cols: cols.map((c) => ({ name: c.name, decltype: c.type })),
|
|
288
|
-
rows: rows.map((row) => cols.map((c) => encodeValue(row[c.name]))),
|
|
289
|
-
affected_row_count: 0,
|
|
290
|
-
last_insert_rowid: null,
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
const result = prepared.run(...params);
|
|
294
|
-
return {
|
|
295
|
-
cols: [],
|
|
296
|
-
rows: [],
|
|
297
|
-
affected_row_count: result.changes,
|
|
298
|
-
last_insert_rowid: result.lastInsertRowid != null ? result.lastInsertRowid.toString() : null,
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
// ── Batch condition evaluation ───────────────────────────────────────────
|
|
302
|
-
function evaluateCondition(cond, stepResults, stepErrors) {
|
|
303
|
-
if (!cond)
|
|
304
|
-
return true;
|
|
305
|
-
if (cond.type === 'ok')
|
|
306
|
-
return stepErrors[cond.step] === null && stepResults[cond.step] !== null;
|
|
307
|
-
if (cond.type === 'not')
|
|
308
|
-
return !evaluateCondition(cond.cond, stepResults, stepErrors);
|
|
309
|
-
if (cond.type === 'and')
|
|
310
|
-
return (cond.conds ?? []).every((c) => evaluateCondition(c, stepResults, stepErrors));
|
|
311
|
-
if (cond.type === 'or')
|
|
312
|
-
return (cond.conds ?? []).some((c) => evaluateCondition(c, stepResults, stepErrors));
|
|
313
|
-
return true;
|
|
314
|
-
}
|
|
315
|
-
// ── Request handlers ─────────────────────────────────────────────────────
|
|
316
|
-
function handleExecute(database, req, sqlStore) {
|
|
317
|
-
if (!req.stmt)
|
|
318
|
-
return {
|
|
319
|
-
type: 'error',
|
|
320
|
-
error: { message: 'Missing stmt', code: 'HRANA_PROTO_ERROR' },
|
|
321
|
-
};
|
|
322
|
-
const result = errore.try({
|
|
323
|
-
try: () => executeStmt(database, req.stmt, sqlStore),
|
|
324
|
-
catch: (e) => e,
|
|
325
|
-
});
|
|
326
|
-
if (result instanceof Error) {
|
|
327
|
-
return {
|
|
328
|
-
type: 'error',
|
|
329
|
-
error: { message: result.message, code: getSqliteErrorCode(result) },
|
|
330
|
-
};
|
|
331
|
-
}
|
|
332
|
-
return { type: 'ok', response: { type: 'execute', result } };
|
|
333
|
-
}
|
|
334
|
-
function handleBatch(database, req, sqlStore) {
|
|
335
|
-
const steps = req.batch?.steps ?? [];
|
|
336
|
-
const stepResults = [];
|
|
337
|
-
const stepErrors = [];
|
|
338
|
-
for (const step of steps) {
|
|
339
|
-
if (!evaluateCondition(step.condition, stepResults, stepErrors)) {
|
|
340
|
-
stepResults.push(null);
|
|
341
|
-
stepErrors.push(null);
|
|
342
|
-
continue;
|
|
343
|
-
}
|
|
344
|
-
const result = errore.try({
|
|
345
|
-
try: () => executeStmt(database, step.stmt, sqlStore),
|
|
346
|
-
catch: (e) => e,
|
|
347
|
-
});
|
|
348
|
-
if (result instanceof Error) {
|
|
349
|
-
stepResults.push(null);
|
|
350
|
-
stepErrors.push({
|
|
351
|
-
message: result.message,
|
|
352
|
-
code: getSqliteErrorCode(result),
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
else {
|
|
356
|
-
stepResults.push(result);
|
|
357
|
-
stepErrors.push(null);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
return {
|
|
361
|
-
type: 'ok',
|
|
362
|
-
response: {
|
|
363
|
-
type: 'batch',
|
|
364
|
-
result: { step_results: stepResults, step_errors: stepErrors },
|
|
365
|
-
},
|
|
366
|
-
};
|
|
367
|
-
}
|
|
368
|
-
function handleSequence(database, req, sqlStore) {
|
|
369
|
-
const sql = req.sql ?? (req.sql_id != null ? sqlStore.get(req.sql_id) : null);
|
|
370
|
-
if (!sql)
|
|
371
|
-
return { type: 'ok', response: { type: 'sequence' } };
|
|
372
|
-
const result = errore.try({
|
|
373
|
-
try: () => {
|
|
374
|
-
database.exec(sql);
|
|
375
|
-
},
|
|
376
|
-
catch: (e) => e,
|
|
377
|
-
});
|
|
378
|
-
if (result instanceof Error) {
|
|
379
|
-
return {
|
|
380
|
-
type: 'error',
|
|
381
|
-
error: { message: result.message, code: getSqliteErrorCode(result) },
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
return { type: 'ok', response: { type: 'sequence' } };
|
|
385
|
-
}
|
|
386
|
-
function processRequest(database, req, sqlStore) {
|
|
387
|
-
if (req.type === 'execute')
|
|
388
|
-
return handleExecute(database, req, sqlStore);
|
|
389
|
-
if (req.type === 'batch')
|
|
390
|
-
return handleBatch(database, req, sqlStore);
|
|
391
|
-
if (req.type === 'sequence')
|
|
392
|
-
return handleSequence(database, req, sqlStore);
|
|
393
|
-
if (req.type === 'close')
|
|
394
|
-
return { type: 'ok', response: { type: 'close' } };
|
|
395
|
-
if (req.type === 'store_sql') {
|
|
396
|
-
if (req.sql_id != null && req.sql != null)
|
|
397
|
-
sqlStore.set(req.sql_id, req.sql);
|
|
398
|
-
return { type: 'ok', response: { type: 'store_sql' } };
|
|
399
|
-
}
|
|
400
|
-
if (req.type === 'close_sql') {
|
|
401
|
-
if (req.sql_id != null)
|
|
402
|
-
sqlStore.delete(req.sql_id);
|
|
403
|
-
return { type: 'ok', response: { type: 'close_sql' } };
|
|
404
|
-
}
|
|
405
|
-
return {
|
|
406
|
-
type: 'error',
|
|
407
|
-
error: {
|
|
408
|
-
message: `Unknown request type: ${req.type}`,
|
|
409
|
-
code: 'HRANA_PROTO_ERROR',
|
|
410
|
-
},
|
|
411
|
-
};
|
|
412
|
-
}
|
|
413
|
-
// ── HTTP handler ─────────────────────────────────────────────────────────
|
|
414
|
-
// @libsql/client HTTP driver uses batons to keep streams alive across
|
|
415
|
-
// pipeline requests (needed for interactive transactions). Each stream has
|
|
416
|
-
// its own SQL store for store_sql/close_sql scoping.
|
|
417
|
-
let batonCounter = 0;
|
|
418
|
-
const streamStores = new Map();
|
|
419
|
-
export function createHranaHandler(database) {
|
|
420
|
-
return (req, res) => {
|
|
421
|
-
const requestUrl = new URL(req.url || '/', 'http://127.0.0.1');
|
|
422
|
-
if (req.method === 'GET' && requestUrl.pathname === '/health') {
|
|
423
|
-
res.writeHead(200, { 'content-type': 'application/json' });
|
|
424
|
-
res.end(JSON.stringify({ status: 'ok', pid: process.pid }));
|
|
425
|
-
return;
|
|
426
|
-
}
|
|
427
|
-
if (req.method === 'GET' && requestUrl.pathname === '/v2') {
|
|
428
|
-
res.writeHead(200, { 'content-type': 'application/json' });
|
|
429
|
-
res.end('{"version":"hrana-v2"}');
|
|
430
|
-
return;
|
|
431
|
-
}
|
|
432
|
-
if (req.method === 'POST' && requestUrl.pathname === '/v2/pipeline') {
|
|
433
|
-
const chunks = [];
|
|
434
|
-
let aborted = false;
|
|
435
|
-
req.on('error', () => {
|
|
436
|
-
aborted = true;
|
|
437
|
-
res.destroy();
|
|
438
|
-
});
|
|
439
|
-
req.on('data', (chunk) => {
|
|
440
|
-
chunks.push(chunk);
|
|
441
|
-
});
|
|
442
|
-
req.on('end', () => {
|
|
443
|
-
if (aborted)
|
|
444
|
-
return;
|
|
445
|
-
const parseResult = errore.try({
|
|
446
|
-
try: () => JSON.parse(Buffer.concat(chunks).toString()),
|
|
447
|
-
catch: (e) => e,
|
|
448
|
-
});
|
|
449
|
-
if (parseResult instanceof Error) {
|
|
450
|
-
res.writeHead(400, { 'content-type': 'application/json' });
|
|
451
|
-
res.end(JSON.stringify({
|
|
452
|
-
error: {
|
|
453
|
-
message: parseResult.message,
|
|
454
|
-
code: 'HRANA_PROTO_ERROR',
|
|
455
|
-
},
|
|
456
|
-
}));
|
|
457
|
-
return;
|
|
458
|
-
}
|
|
459
|
-
// Resolve or create per-stream SQL store keyed by baton
|
|
460
|
-
const incoming = parseResult.baton;
|
|
461
|
-
const sqlStore = (incoming ? streamStores.get(incoming) : undefined) ??
|
|
462
|
-
new Map();
|
|
463
|
-
if (incoming)
|
|
464
|
-
streamStores.delete(incoming);
|
|
465
|
-
const results = (parseResult.requests ?? []).map((r) => processRequest(database, r, sqlStore));
|
|
466
|
-
const hasClose = (parseResult.requests ?? []).some((r) => r.type === 'close');
|
|
467
|
-
const baton = hasClose ? null : `b${++batonCounter}`;
|
|
468
|
-
if (baton)
|
|
469
|
-
streamStores.set(baton, sqlStore);
|
|
470
|
-
res.writeHead(200, { 'content-type': 'application/json' });
|
|
471
|
-
res.end(JSON.stringify({ baton, base_url: null, results }));
|
|
472
|
-
});
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
res.writeHead(404);
|
|
476
|
-
res.end();
|
|
477
|
-
};
|
|
478
|
-
}
|
|
479
|
-
// ── Single-instance enforcement ──────────────────────────────────────────
|
|
208
|
+
// ── Single-instance enforcement ──────────────────────────────────────
|
|
480
209
|
/**
|
|
481
210
|
* Evict a previous kimaki instance on the lock port.
|
|
482
211
|
* Fetches /health to get the running process PID, then kills it directly.
|
|
@@ -7,7 +7,7 @@ import { describe, test, expect, afterAll } from 'vitest';
|
|
|
7
7
|
import Database from 'libsql';
|
|
8
8
|
import { PrismaLibSql } from '@prisma/adapter-libsql';
|
|
9
9
|
import { PrismaClient } from './generated/client.js';
|
|
10
|
-
import {
|
|
10
|
+
import { createLibsqlHandler, createLibsqlNodeHandler, libsqlExecutor, } from 'libsqlproxy';
|
|
11
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
12
|
const __dirname = path.dirname(__filename);
|
|
13
13
|
async function migrateSchema(prisma) {
|
|
@@ -72,7 +72,9 @@ describe('hrana-server', () => {
|
|
|
72
72
|
testDb = database;
|
|
73
73
|
const port = 10000 + Math.floor(Math.random() * 50000);
|
|
74
74
|
await new Promise((resolve, reject) => {
|
|
75
|
-
const
|
|
75
|
+
const hranaFetchHandler = createLibsqlHandler(libsqlExecutor(database));
|
|
76
|
+
const hranaNodeHandler = createLibsqlNodeHandler(hranaFetchHandler);
|
|
77
|
+
const srv = http.createServer(hranaNodeHandler);
|
|
76
78
|
srv.on('error', reject);
|
|
77
79
|
srv.listen(port, '127.0.0.1', () => {
|
|
78
80
|
testServer = srv;
|
|
@@ -34,6 +34,7 @@ test('opencode server loads plugin without errors', async () => {
|
|
|
34
34
|
const port = chooseLockPort({ key: 'opencode-plugin-loading-e2e' });
|
|
35
35
|
const pluginPath = new URL('../src/kimaki-opencode-plugin.ts', import.meta.url).href;
|
|
36
36
|
const stderrLines = [];
|
|
37
|
+
const isolatedOpencodeRoot = path.join(projectDir, 'opencode-test-home');
|
|
37
38
|
const { command, args, windowsVerbatimArguments, } = getSpawnCommandAndArgs({
|
|
38
39
|
resolvedCommand: resolveOpencodeCommand(),
|
|
39
40
|
baseArgs: ['serve', '--port', port.toString(), '--print-logs', '--log-level', 'DEBUG'],
|
|
@@ -50,6 +51,12 @@ test('opencode server loads plugin without errors', async () => {
|
|
|
50
51
|
formatter: false,
|
|
51
52
|
plugin: [pluginPath],
|
|
52
53
|
}),
|
|
54
|
+
OPENCODE_TEST_HOME: isolatedOpencodeRoot,
|
|
55
|
+
OPENCODE_CONFIG_DIR: path.join(isolatedOpencodeRoot, '.opencode-kimaki'),
|
|
56
|
+
XDG_CONFIG_HOME: path.join(isolatedOpencodeRoot, '.config'),
|
|
57
|
+
XDG_DATA_HOME: path.join(isolatedOpencodeRoot, '.local', 'share'),
|
|
58
|
+
XDG_CACHE_HOME: path.join(isolatedOpencodeRoot, '.cache'),
|
|
59
|
+
XDG_STATE_HOME: path.join(isolatedOpencodeRoot, '.local', 'state'),
|
|
53
60
|
},
|
|
54
61
|
});
|
|
55
62
|
serverProcess.stderr?.on('data', (data) => {
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
// - ipc-tools-plugin: file upload + action buttons (IPC-based Discord tools)
|
|
8
8
|
// - context-awareness-plugin: branch, pwd, memory, time gap, onboarding tutorial
|
|
9
9
|
// - opencode-interrupt-plugin: interrupt queued messages at step boundaries
|
|
10
|
+
// - kitty-graphics-plugin: extract Kitty Graphics Protocol images from bash output
|
|
10
11
|
export { ipcToolsPlugin } from './ipc-tools-plugin.js';
|
|
11
12
|
export { contextAwarenessPlugin } from './context-awareness-plugin.js';
|
|
12
13
|
export { interruptOpencodeSessionOnUserMessage } from './opencode-interrupt-plugin.js';
|
|
13
14
|
export { anthropicAuthPlugin } from './anthropic-auth-plugin.js';
|
|
15
|
+
export { kittyGraphicsPlugin } from 'kitty-graphics-agent';
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { extractKittyGraphics } from './kitty-graphics-parser.js';
|
|
3
|
+
// Helper to build a Kitty Graphics escape sequence
|
|
4
|
+
function kittySeq(controlData, payload) {
|
|
5
|
+
return `\x1b_G${controlData};${payload}\x1b\\`;
|
|
6
|
+
}
|
|
7
|
+
// Small valid base64 PNG header (not a real PNG, just for testing)
|
|
8
|
+
const FAKE_PNG_B64 = 'iVBORw0KGgoAAAANSUhEUg==';
|
|
9
|
+
const FAKE_PNG_B64_CHUNK1 = 'iVBORw0KGg';
|
|
10
|
+
const FAKE_PNG_B64_CHUNK2 = 'oAAAANSUhEUg==';
|
|
11
|
+
describe('extractKittyGraphics', () => {
|
|
12
|
+
test('no escape sequences returns input unchanged', () => {
|
|
13
|
+
const result = extractKittyGraphics('hello world');
|
|
14
|
+
expect(result).toMatchInlineSnapshot(`
|
|
15
|
+
{
|
|
16
|
+
"cleanedOutput": "hello world",
|
|
17
|
+
"images": [],
|
|
18
|
+
}
|
|
19
|
+
`);
|
|
20
|
+
});
|
|
21
|
+
test('empty string', () => {
|
|
22
|
+
const result = extractKittyGraphics('');
|
|
23
|
+
expect(result).toMatchInlineSnapshot(`
|
|
24
|
+
{
|
|
25
|
+
"cleanedOutput": "",
|
|
26
|
+
"images": [],
|
|
27
|
+
}
|
|
28
|
+
`);
|
|
29
|
+
});
|
|
30
|
+
test('single PNG image (f=100, non-chunked)', () => {
|
|
31
|
+
const input = `before${kittySeq('f=100', FAKE_PNG_B64)}after`;
|
|
32
|
+
const result = extractKittyGraphics(input);
|
|
33
|
+
expect(result).toMatchInlineSnapshot(`
|
|
34
|
+
{
|
|
35
|
+
"cleanedOutput": "beforeafter",
|
|
36
|
+
"images": [
|
|
37
|
+
{
|
|
38
|
+
"data": "iVBORw0KGgoAAAANSUhEUg==",
|
|
39
|
+
"mime": "image/png",
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
}
|
|
43
|
+
`);
|
|
44
|
+
});
|
|
45
|
+
test('PNG image with transmit+display action (a=T)', () => {
|
|
46
|
+
const input = kittySeq('a=T,f=100', FAKE_PNG_B64);
|
|
47
|
+
const result = extractKittyGraphics(input);
|
|
48
|
+
expect(result).toMatchInlineSnapshot(`
|
|
49
|
+
{
|
|
50
|
+
"cleanedOutput": "",
|
|
51
|
+
"images": [
|
|
52
|
+
{
|
|
53
|
+
"data": "iVBORw0KGgoAAAANSUhEUg==",
|
|
54
|
+
"mime": "image/png",
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
}
|
|
58
|
+
`);
|
|
59
|
+
});
|
|
60
|
+
test('PNG image with width and height', () => {
|
|
61
|
+
const input = kittySeq('f=100,s=640,v=480', FAKE_PNG_B64);
|
|
62
|
+
const result = extractKittyGraphics(input);
|
|
63
|
+
expect(result).toMatchInlineSnapshot(`
|
|
64
|
+
{
|
|
65
|
+
"cleanedOutput": "",
|
|
66
|
+
"images": [
|
|
67
|
+
{
|
|
68
|
+
"data": "iVBORw0KGgoAAAANSUhEUg==",
|
|
69
|
+
"height": 480,
|
|
70
|
+
"mime": "image/png",
|
|
71
|
+
"width": 640,
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
}
|
|
75
|
+
`);
|
|
76
|
+
});
|
|
77
|
+
test('chunked PNG image (m=1 then m=0)', () => {
|
|
78
|
+
const chunk1 = kittySeq('a=T,f=100,m=1', FAKE_PNG_B64_CHUNK1);
|
|
79
|
+
const chunk2 = kittySeq('m=0', FAKE_PNG_B64_CHUNK2);
|
|
80
|
+
const input = `start${chunk1}${chunk2}end`;
|
|
81
|
+
const result = extractKittyGraphics(input);
|
|
82
|
+
expect(result).toMatchInlineSnapshot(`
|
|
83
|
+
{
|
|
84
|
+
"cleanedOutput": "startend",
|
|
85
|
+
"images": [
|
|
86
|
+
{
|
|
87
|
+
"data": "iVBORw0KGgoAAAANSUhEUg==",
|
|
88
|
+
"mime": "image/png",
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
}
|
|
92
|
+
`);
|
|
93
|
+
});
|
|
94
|
+
test('three-chunk PNG image', () => {
|
|
95
|
+
const chunk1 = kittySeq('a=T,f=100,s=100,v=50,m=1', 'AAAA');
|
|
96
|
+
const chunk2 = kittySeq('m=1', 'BBBB');
|
|
97
|
+
const chunk3 = kittySeq('m=0', 'CCCC');
|
|
98
|
+
const input = `${chunk1}${chunk2}${chunk3}`;
|
|
99
|
+
const result = extractKittyGraphics(input);
|
|
100
|
+
expect(result).toMatchInlineSnapshot(`
|
|
101
|
+
{
|
|
102
|
+
"cleanedOutput": "",
|
|
103
|
+
"images": [
|
|
104
|
+
{
|
|
105
|
+
"data": "AAAABBBBCCCC",
|
|
106
|
+
"height": 50,
|
|
107
|
+
"mime": "image/png",
|
|
108
|
+
"width": 100,
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
}
|
|
112
|
+
`);
|
|
113
|
+
});
|
|
114
|
+
test('RGBA image (f=32) is stripped but not extracted', () => {
|
|
115
|
+
const input = `text${kittySeq('f=32,s=10,v=10', 'RGBA_DATA')}more`;
|
|
116
|
+
const result = extractKittyGraphics(input);
|
|
117
|
+
expect(result).toMatchInlineSnapshot(`
|
|
118
|
+
{
|
|
119
|
+
"cleanedOutput": "textmore",
|
|
120
|
+
"images": [],
|
|
121
|
+
}
|
|
122
|
+
`);
|
|
123
|
+
});
|
|
124
|
+
test('RGB image (f=24) is stripped but not extracted', () => {
|
|
125
|
+
const input = `x${kittySeq('f=24,s=5,v=5', 'RGB_DATA')}y`;
|
|
126
|
+
const result = extractKittyGraphics(input);
|
|
127
|
+
expect(result).toMatchInlineSnapshot(`
|
|
128
|
+
{
|
|
129
|
+
"cleanedOutput": "xy",
|
|
130
|
+
"images": [],
|
|
131
|
+
}
|
|
132
|
+
`);
|
|
133
|
+
});
|
|
134
|
+
test('default format (no f= key, defaults to 32) is stripped but not extracted', () => {
|
|
135
|
+
const input = `a${kittySeq('s=10,v=10', 'SOME_DATA')}b`;
|
|
136
|
+
const result = extractKittyGraphics(input);
|
|
137
|
+
expect(result).toMatchInlineSnapshot(`
|
|
138
|
+
{
|
|
139
|
+
"cleanedOutput": "ab",
|
|
140
|
+
"images": [],
|
|
141
|
+
}
|
|
142
|
+
`);
|
|
143
|
+
});
|
|
144
|
+
test('multiple images in one output', () => {
|
|
145
|
+
const img1 = kittySeq('f=100', 'IMAGE_ONE');
|
|
146
|
+
const img2 = kittySeq('f=100,s=200,v=100', 'IMAGE_TWO');
|
|
147
|
+
const input = `first${img1}middle${img2}last`;
|
|
148
|
+
const result = extractKittyGraphics(input);
|
|
149
|
+
expect(result).toMatchInlineSnapshot(`
|
|
150
|
+
{
|
|
151
|
+
"cleanedOutput": "firstmiddlelast",
|
|
152
|
+
"images": [
|
|
153
|
+
{
|
|
154
|
+
"data": "IMAGE_ONE",
|
|
155
|
+
"mime": "image/png",
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"data": "IMAGE_TWO",
|
|
159
|
+
"height": 100,
|
|
160
|
+
"mime": "image/png",
|
|
161
|
+
"width": 200,
|
|
162
|
+
},
|
|
163
|
+
],
|
|
164
|
+
}
|
|
165
|
+
`);
|
|
166
|
+
});
|
|
167
|
+
test('mixed PNG and non-PNG: only PNG extracted', () => {
|
|
168
|
+
const pngImg = kittySeq('f=100', 'PNG_DATA');
|
|
169
|
+
const rgbaImg = kittySeq('f=32,s=10,v=10', 'RGBA_DATA');
|
|
170
|
+
const input = `${pngImg}between${rgbaImg}`;
|
|
171
|
+
const result = extractKittyGraphics(input);
|
|
172
|
+
expect(result).toMatchInlineSnapshot(`
|
|
173
|
+
{
|
|
174
|
+
"cleanedOutput": "between",
|
|
175
|
+
"images": [
|
|
176
|
+
{
|
|
177
|
+
"data": "PNG_DATA",
|
|
178
|
+
"mime": "image/png",
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
}
|
|
182
|
+
`);
|
|
183
|
+
});
|
|
184
|
+
test('file-based transmission (t=f) is stripped but not extracted', () => {
|
|
185
|
+
// t=f means the payload is a file path, not inline data
|
|
186
|
+
const input = `x${kittySeq('f=100,t=f', 'L3RtcC9pbWFnZS5wbmc=')}y`;
|
|
187
|
+
const result = extractKittyGraphics(input);
|
|
188
|
+
expect(result).toMatchInlineSnapshot(`
|
|
189
|
+
{
|
|
190
|
+
"cleanedOutput": "xy",
|
|
191
|
+
"images": [],
|
|
192
|
+
}
|
|
193
|
+
`);
|
|
194
|
+
});
|
|
195
|
+
test('image with suppress response (q=2)', () => {
|
|
196
|
+
const input = kittySeq('f=100,q=2', FAKE_PNG_B64);
|
|
197
|
+
const result = extractKittyGraphics(input);
|
|
198
|
+
expect(result).toMatchInlineSnapshot(`
|
|
199
|
+
{
|
|
200
|
+
"cleanedOutput": "",
|
|
201
|
+
"images": [
|
|
202
|
+
{
|
|
203
|
+
"data": "iVBORw0KGgoAAAANSUhEUg==",
|
|
204
|
+
"mime": "image/png",
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
}
|
|
208
|
+
`);
|
|
209
|
+
});
|
|
210
|
+
test('incomplete escape sequence at end is left in output', () => {
|
|
211
|
+
// Missing ST terminator
|
|
212
|
+
const input = `hello\x1b_Gf=100;${FAKE_PNG_B64}`;
|
|
213
|
+
const result = extractKittyGraphics(input);
|
|
214
|
+
expect(result).toMatchInlineSnapshot(`
|
|
215
|
+
{
|
|
216
|
+
"cleanedOutput": "hello_Gf=100;iVBORw0KGgoAAAANSUhEUg==",
|
|
217
|
+
"images": [],
|
|
218
|
+
}
|
|
219
|
+
`);
|
|
220
|
+
});
|
|
221
|
+
test('text with ANSI color codes is not affected', () => {
|
|
222
|
+
const input = '\x1b[31mred text\x1b[0m normal';
|
|
223
|
+
const result = extractKittyGraphics(input);
|
|
224
|
+
expect(result).toMatchInlineSnapshot(`
|
|
225
|
+
{
|
|
226
|
+
"cleanedOutput": "[31mred text[0m normal",
|
|
227
|
+
"images": [],
|
|
228
|
+
}
|
|
229
|
+
`);
|
|
230
|
+
});
|
|
231
|
+
test('delete command (a=d) is stripped, no image extracted', () => {
|
|
232
|
+
const input = `before\x1b_Ga=d\x1b\\after`;
|
|
233
|
+
const result = extractKittyGraphics(input);
|
|
234
|
+
expect(result).toMatchInlineSnapshot(`
|
|
235
|
+
{
|
|
236
|
+
"cleanedOutput": "beforeafter",
|
|
237
|
+
"images": [],
|
|
238
|
+
}
|
|
239
|
+
`);
|
|
240
|
+
});
|
|
241
|
+
test('escape sequence with no payload', () => {
|
|
242
|
+
const input = `x\x1b_Ga=d,d=a;\x1b\\y`;
|
|
243
|
+
const result = extractKittyGraphics(input);
|
|
244
|
+
expect(result).toMatchInlineSnapshot(`
|
|
245
|
+
{
|
|
246
|
+
"cleanedOutput": "xy",
|
|
247
|
+
"images": [],
|
|
248
|
+
}
|
|
249
|
+
`);
|
|
250
|
+
});
|
|
251
|
+
test('real-world: text with command output mixed with kitty image', () => {
|
|
252
|
+
const lines = [
|
|
253
|
+
'$ kitten icat image.png',
|
|
254
|
+
kittySeq('a=T,f=100,q=2,m=1', FAKE_PNG_B64_CHUNK1),
|
|
255
|
+
kittySeq('m=0', FAKE_PNG_B64_CHUNK2),
|
|
256
|
+
'$ echo done',
|
|
257
|
+
'done',
|
|
258
|
+
].join('\n');
|
|
259
|
+
const result = extractKittyGraphics(lines);
|
|
260
|
+
expect(result.cleanedOutput).toMatchInlineSnapshot(`
|
|
261
|
+
"$ kitten icat image.png
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
$ echo done
|
|
265
|
+
done"
|
|
266
|
+
`);
|
|
267
|
+
expect(result.images).toMatchInlineSnapshot(`
|
|
268
|
+
[
|
|
269
|
+
{
|
|
270
|
+
"data": "iVBORw0KGgoAAAANSUhEUg==",
|
|
271
|
+
"mime": "image/png",
|
|
272
|
+
},
|
|
273
|
+
]
|
|
274
|
+
`);
|
|
275
|
+
});
|
|
276
|
+
});
|