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.
Files changed (264) hide show
  1. package/LICENSE +21 -0
  2. package/dist/anthropic-auth-plugin.js +7 -0
  3. package/dist/cli.js +51 -7
  4. package/dist/commands/abort.js +5 -16
  5. package/dist/commands/action-buttons.js +3 -3
  6. package/dist/commands/add-project.js +1 -1
  7. package/dist/commands/ask-question.js +3 -3
  8. package/dist/commands/context-usage.js +1 -1
  9. package/dist/commands/create-new-project.js +1 -1
  10. package/dist/commands/fork.js +11 -8
  11. package/dist/commands/merge-worktree.js +1 -1
  12. package/dist/commands/new-worktree.js +63 -44
  13. package/dist/commands/remove-project.js +1 -1
  14. package/dist/commands/resume.js +11 -8
  15. package/dist/commands/screenshare.js +14 -6
  16. package/dist/commands/screenshare.test.js +20 -0
  17. package/dist/commands/session.js +1 -1
  18. package/dist/commands/undo-redo.js +91 -7
  19. package/dist/commands/user-command.js +1 -1
  20. package/dist/config.js +16 -1
  21. package/dist/database.js +53 -2
  22. package/dist/db.js +6 -0
  23. package/dist/discord-bot.js +48 -85
  24. package/dist/discord-command-registration.js +1 -1
  25. package/dist/external-opencode-sync.js +515 -0
  26. package/dist/external-opencode-sync.test.js +151 -0
  27. package/dist/gateway-proxy.e2e.test.js +8 -5
  28. package/dist/genai.js +1 -1
  29. package/dist/generated/enums.js +4 -0
  30. package/dist/generated/internal/class.js +4 -4
  31. package/dist/generated/internal/prismaNamespace.js +1 -0
  32. package/dist/generated/internal/prismaNamespaceBrowser.js +1 -0
  33. package/dist/generated/models/external_session_pending_prompts.js +1 -0
  34. package/dist/hrana-server.js +14 -285
  35. package/dist/hrana-server.test.js +4 -2
  36. package/dist/kimaki-opencode-plugin-loading.e2e.test.js +7 -0
  37. package/dist/kimaki-opencode-plugin.js +2 -0
  38. package/dist/kitty-graphics-parser.js +3 -0
  39. package/dist/kitty-graphics-parser.test.js +276 -0
  40. package/dist/kitty-graphics-plugin.js +3 -0
  41. package/dist/markdown.js +4 -4
  42. package/dist/markdown.test.js +1 -1
  43. package/dist/message-formatting.js +54 -15
  44. package/dist/onboarding-tutorial.js +1 -1
  45. package/dist/openai-realtime.js +9 -13
  46. package/dist/opencode.js +28 -5
  47. package/dist/queue-advanced-e2e-setup.js +89 -0
  48. package/dist/queue-advanced-permissions-typing.e2e.test.js +5 -5
  49. package/dist/queue-advanced-typing.e2e.test.js +9 -22
  50. package/dist/queue-question-select-drain.e2e.test.js +117 -0
  51. package/dist/session-handler/event-stream-state.js +101 -7
  52. package/dist/session-handler/event-stream-state.test.js +7 -3
  53. package/dist/session-handler/thread-session-runtime.js +120 -9
  54. package/dist/store.js +1 -0
  55. package/dist/system-message.js +22 -4
  56. package/dist/system-message.test.js +19 -0
  57. package/dist/task-runner.js +1 -1
  58. package/dist/thread-message-queue.e2e.test.js +8 -14
  59. package/dist/tools.js +1 -1
  60. package/dist/undo-redo.e2e.test.js +20 -25
  61. package/package.json +10 -6
  62. package/schema.prisma +6 -0
  63. package/skills/errore/SKILL.md +40 -13
  64. package/skills/goke/SKILL.md +12 -0
  65. package/skills/lintcn/SKILL.md +868 -0
  66. package/skills/npm-package/SKILL.md +1 -0
  67. package/skills/proxyman/SKILL.md +215 -0
  68. package/skills/spiceflow/SKILL.md +1 -1
  69. package/skills/usecomputer/SKILL.md +339 -0
  70. package/src/ai-tool-to-genai.ts +1 -0
  71. package/src/anthropic-auth-plugin.ts +7 -0
  72. package/src/cli.ts +59 -6
  73. package/src/commands/abort.ts +6 -16
  74. package/src/commands/action-buttons.ts +5 -1
  75. package/src/commands/add-project.ts +1 -1
  76. package/src/commands/ask-question.ts +5 -2
  77. package/src/commands/context-usage.ts +1 -1
  78. package/src/commands/create-new-project.ts +1 -1
  79. package/src/commands/fork.ts +12 -11
  80. package/src/commands/merge-worktree.ts +1 -1
  81. package/src/commands/new-worktree.ts +74 -55
  82. package/src/commands/remove-project.ts +1 -1
  83. package/src/commands/resume.ts +12 -10
  84. package/src/commands/screenshare.test.ts +30 -0
  85. package/src/commands/screenshare.ts +18 -6
  86. package/src/commands/session.ts +1 -1
  87. package/src/commands/undo-redo.ts +108 -10
  88. package/src/commands/user-command.ts +1 -1
  89. package/src/config.ts +19 -1
  90. package/src/database.ts +72 -3
  91. package/src/db.ts +8 -0
  92. package/src/discord-bot.ts +58 -93
  93. package/src/discord-command-registration.ts +1 -1
  94. package/src/external-opencode-sync.ts +729 -0
  95. package/src/gateway-proxy.e2e.test.ts +9 -5
  96. package/src/genai.ts +3 -3
  97. package/src/generated/commonInputTypes.ts +34 -0
  98. package/src/generated/enums.ts +8 -0
  99. package/src/generated/internal/class.ts +4 -4
  100. package/src/generated/internal/prismaNamespace.ts +8 -0
  101. package/src/generated/internal/prismaNamespaceBrowser.ts +1 -0
  102. package/src/generated/models/thread_sessions.ts +53 -1
  103. package/src/hrana-server.test.ts +8 -2
  104. package/src/hrana-server.ts +18 -390
  105. package/src/kimaki-opencode-plugin-loading.e2e.test.ts +7 -0
  106. package/src/kimaki-opencode-plugin.ts +2 -0
  107. package/src/markdown.test.ts +1 -1
  108. package/src/markdown.ts +4 -4
  109. package/src/message-formatting.ts +66 -17
  110. package/src/onboarding-tutorial.ts +1 -1
  111. package/src/openai-realtime.ts +6 -10
  112. package/src/opencode.ts +31 -7
  113. package/src/queue-advanced-e2e-setup.ts +92 -0
  114. package/src/queue-advanced-permissions-typing.e2e.test.ts +5 -5
  115. package/src/queue-advanced-typing.e2e.test.ts +9 -22
  116. package/src/queue-question-select-drain.e2e.test.ts +149 -0
  117. package/src/schema.sql +1 -0
  118. package/src/session-handler/event-stream-state.test.ts +7 -2
  119. package/src/session-handler/event-stream-state.ts +128 -7
  120. package/src/session-handler/thread-runtime-state.ts +5 -0
  121. package/src/session-handler/thread-session-runtime.ts +153 -11
  122. package/src/store.ts +8 -0
  123. package/src/system-message.ts +27 -4
  124. package/src/task-runner.ts +1 -1
  125. package/src/thread-message-queue.e2e.test.ts +8 -14
  126. package/src/tools.ts +1 -1
  127. package/src/undo-redo.e2e.test.ts +28 -26
  128. package/skills/jitter/node_modules/.bin/esbuild +0 -21
  129. package/skills/jitter/node_modules/.bin/tsc +0 -21
  130. package/skills/jitter/node_modules/.bin/tsserver +0 -21
  131. package/skills/jitter/node_modules/typescript/LICENSE.txt +0 -55
  132. package/skills/jitter/node_modules/typescript/README.md +0 -50
  133. package/skills/jitter/node_modules/typescript/SECURITY.md +0 -41
  134. package/skills/jitter/node_modules/typescript/ThirdPartyNoticeText.txt +0 -193
  135. package/skills/jitter/node_modules/typescript/bin/tsc +0 -2
  136. package/skills/jitter/node_modules/typescript/bin/tsserver +0 -2
  137. package/skills/jitter/node_modules/typescript/lib/_tsc.js +0 -133792
  138. package/skills/jitter/node_modules/typescript/lib/_tsserver.js +0 -659
  139. package/skills/jitter/node_modules/typescript/lib/_typingsInstaller.js +0 -222
  140. package/skills/jitter/node_modules/typescript/lib/cs/diagnosticMessages.generated.json +0 -2122
  141. package/skills/jitter/node_modules/typescript/lib/de/diagnosticMessages.generated.json +0 -2122
  142. package/skills/jitter/node_modules/typescript/lib/es/diagnosticMessages.generated.json +0 -2122
  143. package/skills/jitter/node_modules/typescript/lib/fr/diagnosticMessages.generated.json +0 -2122
  144. package/skills/jitter/node_modules/typescript/lib/it/diagnosticMessages.generated.json +0 -2122
  145. package/skills/jitter/node_modules/typescript/lib/ja/diagnosticMessages.generated.json +0 -2122
  146. package/skills/jitter/node_modules/typescript/lib/ko/diagnosticMessages.generated.json +0 -2122
  147. package/skills/jitter/node_modules/typescript/lib/lib.d.ts +0 -22
  148. package/skills/jitter/node_modules/typescript/lib/lib.decorators.d.ts +0 -384
  149. package/skills/jitter/node_modules/typescript/lib/lib.decorators.legacy.d.ts +0 -22
  150. package/skills/jitter/node_modules/typescript/lib/lib.dom.asynciterable.d.ts +0 -41
  151. package/skills/jitter/node_modules/typescript/lib/lib.dom.d.ts +0 -39429
  152. package/skills/jitter/node_modules/typescript/lib/lib.dom.iterable.d.ts +0 -571
  153. package/skills/jitter/node_modules/typescript/lib/lib.es2015.collection.d.ts +0 -147
  154. package/skills/jitter/node_modules/typescript/lib/lib.es2015.core.d.ts +0 -597
  155. package/skills/jitter/node_modules/typescript/lib/lib.es2015.d.ts +0 -28
  156. package/skills/jitter/node_modules/typescript/lib/lib.es2015.generator.d.ts +0 -77
  157. package/skills/jitter/node_modules/typescript/lib/lib.es2015.iterable.d.ts +0 -605
  158. package/skills/jitter/node_modules/typescript/lib/lib.es2015.promise.d.ts +0 -81
  159. package/skills/jitter/node_modules/typescript/lib/lib.es2015.proxy.d.ts +0 -128
  160. package/skills/jitter/node_modules/typescript/lib/lib.es2015.reflect.d.ts +0 -144
  161. package/skills/jitter/node_modules/typescript/lib/lib.es2015.symbol.d.ts +0 -46
  162. package/skills/jitter/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts +0 -326
  163. package/skills/jitter/node_modules/typescript/lib/lib.es2016.array.include.d.ts +0 -116
  164. package/skills/jitter/node_modules/typescript/lib/lib.es2016.d.ts +0 -21
  165. package/skills/jitter/node_modules/typescript/lib/lib.es2016.full.d.ts +0 -23
  166. package/skills/jitter/node_modules/typescript/lib/lib.es2016.intl.d.ts +0 -31
  167. package/skills/jitter/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts +0 -21
  168. package/skills/jitter/node_modules/typescript/lib/lib.es2017.d.ts +0 -26
  169. package/skills/jitter/node_modules/typescript/lib/lib.es2017.date.d.ts +0 -31
  170. package/skills/jitter/node_modules/typescript/lib/lib.es2017.full.d.ts +0 -23
  171. package/skills/jitter/node_modules/typescript/lib/lib.es2017.intl.d.ts +0 -44
  172. package/skills/jitter/node_modules/typescript/lib/lib.es2017.object.d.ts +0 -49
  173. package/skills/jitter/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts +0 -135
  174. package/skills/jitter/node_modules/typescript/lib/lib.es2017.string.d.ts +0 -45
  175. package/skills/jitter/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts +0 -53
  176. package/skills/jitter/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts +0 -77
  177. package/skills/jitter/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts +0 -53
  178. package/skills/jitter/node_modules/typescript/lib/lib.es2018.d.ts +0 -24
  179. package/skills/jitter/node_modules/typescript/lib/lib.es2018.full.d.ts +0 -24
  180. package/skills/jitter/node_modules/typescript/lib/lib.es2018.intl.d.ts +0 -83
  181. package/skills/jitter/node_modules/typescript/lib/lib.es2018.promise.d.ts +0 -30
  182. package/skills/jitter/node_modules/typescript/lib/lib.es2018.regexp.d.ts +0 -37
  183. package/skills/jitter/node_modules/typescript/lib/lib.es2019.array.d.ts +0 -79
  184. package/skills/jitter/node_modules/typescript/lib/lib.es2019.d.ts +0 -24
  185. package/skills/jitter/node_modules/typescript/lib/lib.es2019.full.d.ts +0 -24
  186. package/skills/jitter/node_modules/typescript/lib/lib.es2019.intl.d.ts +0 -23
  187. package/skills/jitter/node_modules/typescript/lib/lib.es2019.object.d.ts +0 -33
  188. package/skills/jitter/node_modules/typescript/lib/lib.es2019.string.d.ts +0 -37
  189. package/skills/jitter/node_modules/typescript/lib/lib.es2019.symbol.d.ts +0 -24
  190. package/skills/jitter/node_modules/typescript/lib/lib.es2020.bigint.d.ts +0 -765
  191. package/skills/jitter/node_modules/typescript/lib/lib.es2020.d.ts +0 -27
  192. package/skills/jitter/node_modules/typescript/lib/lib.es2020.date.d.ts +0 -42
  193. package/skills/jitter/node_modules/typescript/lib/lib.es2020.full.d.ts +0 -24
  194. package/skills/jitter/node_modules/typescript/lib/lib.es2020.intl.d.ts +0 -474
  195. package/skills/jitter/node_modules/typescript/lib/lib.es2020.number.d.ts +0 -28
  196. package/skills/jitter/node_modules/typescript/lib/lib.es2020.promise.d.ts +0 -47
  197. package/skills/jitter/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts +0 -99
  198. package/skills/jitter/node_modules/typescript/lib/lib.es2020.string.d.ts +0 -44
  199. package/skills/jitter/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts +0 -41
  200. package/skills/jitter/node_modules/typescript/lib/lib.es2021.d.ts +0 -23
  201. package/skills/jitter/node_modules/typescript/lib/lib.es2021.full.d.ts +0 -24
  202. package/skills/jitter/node_modules/typescript/lib/lib.es2021.intl.d.ts +0 -166
  203. package/skills/jitter/node_modules/typescript/lib/lib.es2021.promise.d.ts +0 -48
  204. package/skills/jitter/node_modules/typescript/lib/lib.es2021.string.d.ts +0 -33
  205. package/skills/jitter/node_modules/typescript/lib/lib.es2021.weakref.d.ts +0 -78
  206. package/skills/jitter/node_modules/typescript/lib/lib.es2022.array.d.ts +0 -121
  207. package/skills/jitter/node_modules/typescript/lib/lib.es2022.d.ts +0 -25
  208. package/skills/jitter/node_modules/typescript/lib/lib.es2022.error.d.ts +0 -75
  209. package/skills/jitter/node_modules/typescript/lib/lib.es2022.full.d.ts +0 -24
  210. package/skills/jitter/node_modules/typescript/lib/lib.es2022.intl.d.ts +0 -145
  211. package/skills/jitter/node_modules/typescript/lib/lib.es2022.object.d.ts +0 -26
  212. package/skills/jitter/node_modules/typescript/lib/lib.es2022.regexp.d.ts +0 -39
  213. package/skills/jitter/node_modules/typescript/lib/lib.es2022.string.d.ts +0 -25
  214. package/skills/jitter/node_modules/typescript/lib/lib.es2023.array.d.ts +0 -924
  215. package/skills/jitter/node_modules/typescript/lib/lib.es2023.collection.d.ts +0 -21
  216. package/skills/jitter/node_modules/typescript/lib/lib.es2023.d.ts +0 -22
  217. package/skills/jitter/node_modules/typescript/lib/lib.es2023.full.d.ts +0 -24
  218. package/skills/jitter/node_modules/typescript/lib/lib.es2023.intl.d.ts +0 -56
  219. package/skills/jitter/node_modules/typescript/lib/lib.es2024.arraybuffer.d.ts +0 -65
  220. package/skills/jitter/node_modules/typescript/lib/lib.es2024.collection.d.ts +0 -29
  221. package/skills/jitter/node_modules/typescript/lib/lib.es2024.d.ts +0 -26
  222. package/skills/jitter/node_modules/typescript/lib/lib.es2024.full.d.ts +0 -24
  223. package/skills/jitter/node_modules/typescript/lib/lib.es2024.object.d.ts +0 -29
  224. package/skills/jitter/node_modules/typescript/lib/lib.es2024.promise.d.ts +0 -35
  225. package/skills/jitter/node_modules/typescript/lib/lib.es2024.regexp.d.ts +0 -25
  226. package/skills/jitter/node_modules/typescript/lib/lib.es2024.sharedmemory.d.ts +0 -68
  227. package/skills/jitter/node_modules/typescript/lib/lib.es2024.string.d.ts +0 -29
  228. package/skills/jitter/node_modules/typescript/lib/lib.es5.d.ts +0 -4601
  229. package/skills/jitter/node_modules/typescript/lib/lib.es6.d.ts +0 -23
  230. package/skills/jitter/node_modules/typescript/lib/lib.esnext.array.d.ts +0 -35
  231. package/skills/jitter/node_modules/typescript/lib/lib.esnext.collection.d.ts +0 -96
  232. package/skills/jitter/node_modules/typescript/lib/lib.esnext.d.ts +0 -29
  233. package/skills/jitter/node_modules/typescript/lib/lib.esnext.decorators.d.ts +0 -28
  234. package/skills/jitter/node_modules/typescript/lib/lib.esnext.disposable.d.ts +0 -193
  235. package/skills/jitter/node_modules/typescript/lib/lib.esnext.error.d.ts +0 -24
  236. package/skills/jitter/node_modules/typescript/lib/lib.esnext.float16.d.ts +0 -443
  237. package/skills/jitter/node_modules/typescript/lib/lib.esnext.full.d.ts +0 -24
  238. package/skills/jitter/node_modules/typescript/lib/lib.esnext.intl.d.ts +0 -21
  239. package/skills/jitter/node_modules/typescript/lib/lib.esnext.iterator.d.ts +0 -148
  240. package/skills/jitter/node_modules/typescript/lib/lib.esnext.promise.d.ts +0 -34
  241. package/skills/jitter/node_modules/typescript/lib/lib.esnext.sharedmemory.d.ts +0 -25
  242. package/skills/jitter/node_modules/typescript/lib/lib.scripthost.d.ts +0 -322
  243. package/skills/jitter/node_modules/typescript/lib/lib.webworker.asynciterable.d.ts +0 -41
  244. package/skills/jitter/node_modules/typescript/lib/lib.webworker.d.ts +0 -13150
  245. package/skills/jitter/node_modules/typescript/lib/lib.webworker.importscripts.d.ts +0 -23
  246. package/skills/jitter/node_modules/typescript/lib/lib.webworker.iterable.d.ts +0 -340
  247. package/skills/jitter/node_modules/typescript/lib/pl/diagnosticMessages.generated.json +0 -2122
  248. package/skills/jitter/node_modules/typescript/lib/pt-br/diagnosticMessages.generated.json +0 -2122
  249. package/skills/jitter/node_modules/typescript/lib/ru/diagnosticMessages.generated.json +0 -2122
  250. package/skills/jitter/node_modules/typescript/lib/tr/diagnosticMessages.generated.json +0 -2122
  251. package/skills/jitter/node_modules/typescript/lib/tsc.js +0 -8
  252. package/skills/jitter/node_modules/typescript/lib/tsserver.js +0 -8
  253. package/skills/jitter/node_modules/typescript/lib/tsserverlibrary.d.ts +0 -17
  254. package/skills/jitter/node_modules/typescript/lib/tsserverlibrary.js +0 -21
  255. package/skills/jitter/node_modules/typescript/lib/typesMap.json +0 -497
  256. package/skills/jitter/node_modules/typescript/lib/typescript.d.ts +0 -11438
  257. package/skills/jitter/node_modules/typescript/lib/typescript.js +0 -200253
  258. package/skills/jitter/node_modules/typescript/lib/typingsInstaller.js +0 -8
  259. package/skills/jitter/node_modules/typescript/lib/watchGuard.js +0 -53
  260. package/skills/jitter/node_modules/typescript/lib/zh-cn/diagnosticMessages.generated.json +0 -2122
  261. package/skills/jitter/node_modules/typescript/lib/zh-tw/diagnosticMessages.generated.json +0 -2122
  262. package/skills/jitter/node_modules/typescript/node_modules/.bin/tsc +0 -21
  263. package/skills/jitter/node_modules/typescript/node_modules/.bin/tsserver +0 -21
  264. package/skills/jitter/node_modules/typescript/package.json +0 -120
@@ -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
- // Serves POST /v2/pipeline (Hrana v2 JSON), GET /v2, and GET /health.
6
- // The @libsql/client HTTP driver and @prisma/adapter-libsql connect here.
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
- const hranaHandler = createHranaHandler(database);
136
- // Combined handler: all control/data routes require the same service auth token.
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
- // Hrana routes: /health, /v2, /v2/pipeline
143
+ // Health check no auth required
161
144
  if (pathname === '/health') {
162
- hranaHandler(req, res);
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
- hranaHandler(req, res);
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
- // ── Value encoding/decoding ──────────────────────────────────────────────
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 { createHranaHandler } from './hrana-server.js';
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 srv = http.createServer(createHranaHandler(database));
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,3 @@
1
+ // Re-export from the kitty-graphics-agent package.
2
+ // The implementation lives in its own package/repo for reuse as a spec.
3
+ export { extractKittyGraphics } from 'kitty-graphics-agent/parser';
@@ -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": "red text 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
+ });
@@ -0,0 +1,3 @@
1
+ // Re-export from the kitty-graphics-agent package.
2
+ // The implementation lives in its own package/repo for reuse as a spec.
3
+ export { kittyGraphicsPlugin } from 'kitty-graphics-agent';