@otto-assistant/bridge 0.4.92

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 (483) hide show
  1. package/bin.js +2 -0
  2. package/dist/agent-model.e2e.test.js +755 -0
  3. package/dist/ai-tool-to-genai.js +233 -0
  4. package/dist/ai-tool-to-genai.test.js +267 -0
  5. package/dist/ai-tool.js +6 -0
  6. package/dist/anthropic-auth-plugin.js +728 -0
  7. package/dist/anthropic-auth-plugin.test.js +125 -0
  8. package/dist/anthropic-auth-state.js +231 -0
  9. package/dist/bin.js +90 -0
  10. package/dist/channel-management.js +227 -0
  11. package/dist/cli-parsing.test.js +137 -0
  12. package/dist/cli-send-thread.e2e.test.js +356 -0
  13. package/dist/cli.js +3276 -0
  14. package/dist/commands/abort.js +65 -0
  15. package/dist/commands/action-buttons.js +245 -0
  16. package/dist/commands/add-project.js +113 -0
  17. package/dist/commands/agent.js +335 -0
  18. package/dist/commands/ask-question.js +274 -0
  19. package/dist/commands/btw.js +116 -0
  20. package/dist/commands/compact.js +120 -0
  21. package/dist/commands/context-usage.js +140 -0
  22. package/dist/commands/create-new-project.js +130 -0
  23. package/dist/commands/diff.js +63 -0
  24. package/dist/commands/file-upload.js +275 -0
  25. package/dist/commands/fork.js +220 -0
  26. package/dist/commands/gemini-apikey.js +70 -0
  27. package/dist/commands/login.js +885 -0
  28. package/dist/commands/mcp.js +239 -0
  29. package/dist/commands/memory-snapshot.js +24 -0
  30. package/dist/commands/mention-mode.js +44 -0
  31. package/dist/commands/merge-worktree.js +159 -0
  32. package/dist/commands/model-variant.js +364 -0
  33. package/dist/commands/model.js +776 -0
  34. package/dist/commands/new-worktree.js +366 -0
  35. package/dist/commands/paginated-select.js +57 -0
  36. package/dist/commands/permissions.js +274 -0
  37. package/dist/commands/queue.js +206 -0
  38. package/dist/commands/remove-project.js +115 -0
  39. package/dist/commands/restart-opencode-server.js +127 -0
  40. package/dist/commands/resume.js +149 -0
  41. package/dist/commands/run-command.js +79 -0
  42. package/dist/commands/screenshare.js +303 -0
  43. package/dist/commands/screenshare.test.js +20 -0
  44. package/dist/commands/session-id.js +78 -0
  45. package/dist/commands/session.js +176 -0
  46. package/dist/commands/share.js +80 -0
  47. package/dist/commands/tasks.js +205 -0
  48. package/dist/commands/types.js +2 -0
  49. package/dist/commands/undo-redo.js +305 -0
  50. package/dist/commands/unset-model.js +138 -0
  51. package/dist/commands/upgrade.js +42 -0
  52. package/dist/commands/user-command.js +155 -0
  53. package/dist/commands/verbosity.js +125 -0
  54. package/dist/commands/worktree-settings.js +43 -0
  55. package/dist/commands/worktrees.js +410 -0
  56. package/dist/condense-memory.js +33 -0
  57. package/dist/config.js +94 -0
  58. package/dist/context-awareness-plugin.js +363 -0
  59. package/dist/context-awareness-plugin.test.js +124 -0
  60. package/dist/critique-utils.js +95 -0
  61. package/dist/database.js +1310 -0
  62. package/dist/db.js +251 -0
  63. package/dist/db.test.js +138 -0
  64. package/dist/debounce-timeout.js +28 -0
  65. package/dist/debounced-process-flush.js +77 -0
  66. package/dist/discord-bot.js +1008 -0
  67. package/dist/discord-command-registration.js +524 -0
  68. package/dist/discord-urls.js +81 -0
  69. package/dist/discord-utils.js +591 -0
  70. package/dist/discord-utils.test.js +134 -0
  71. package/dist/errors.js +157 -0
  72. package/dist/escape-backticks.test.js +429 -0
  73. package/dist/event-stream-real-capture.e2e.test.js +533 -0
  74. package/dist/eventsource-parser.test.js +327 -0
  75. package/dist/exec-async.js +26 -0
  76. package/dist/external-opencode-sync.js +480 -0
  77. package/dist/format-tables.js +302 -0
  78. package/dist/format-tables.test.js +308 -0
  79. package/dist/forum-sync/config.js +79 -0
  80. package/dist/forum-sync/discord-operations.js +154 -0
  81. package/dist/forum-sync/index.js +5 -0
  82. package/dist/forum-sync/markdown.js +113 -0
  83. package/dist/forum-sync/sync-to-discord.js +417 -0
  84. package/dist/forum-sync/sync-to-files.js +190 -0
  85. package/dist/forum-sync/types.js +53 -0
  86. package/dist/forum-sync/watchers.js +307 -0
  87. package/dist/gateway-proxy-reconnect.e2e.test.js +394 -0
  88. package/dist/gateway-proxy.e2e.test.js +483 -0
  89. package/dist/genai-worker-wrapper.js +111 -0
  90. package/dist/genai-worker.js +311 -0
  91. package/dist/genai.js +232 -0
  92. package/dist/generated/browser.js +17 -0
  93. package/dist/generated/client.js +37 -0
  94. package/dist/generated/commonInputTypes.js +10 -0
  95. package/dist/generated/enums.js +52 -0
  96. package/dist/generated/internal/class.js +49 -0
  97. package/dist/generated/internal/prismaNamespace.js +253 -0
  98. package/dist/generated/internal/prismaNamespaceBrowser.js +223 -0
  99. package/dist/generated/models/bot_api_keys.js +1 -0
  100. package/dist/generated/models/bot_tokens.js +1 -0
  101. package/dist/generated/models/channel_agents.js +1 -0
  102. package/dist/generated/models/channel_directories.js +1 -0
  103. package/dist/generated/models/channel_mention_mode.js +1 -0
  104. package/dist/generated/models/channel_models.js +1 -0
  105. package/dist/generated/models/channel_verbosity.js +1 -0
  106. package/dist/generated/models/channel_worktrees.js +1 -0
  107. package/dist/generated/models/forum_sync_configs.js +1 -0
  108. package/dist/generated/models/global_models.js +1 -0
  109. package/dist/generated/models/ipc_requests.js +1 -0
  110. package/dist/generated/models/part_messages.js +1 -0
  111. package/dist/generated/models/scheduled_tasks.js +1 -0
  112. package/dist/generated/models/session_agents.js +1 -0
  113. package/dist/generated/models/session_events.js +1 -0
  114. package/dist/generated/models/session_models.js +1 -0
  115. package/dist/generated/models/session_start_sources.js +1 -0
  116. package/dist/generated/models/thread_sessions.js +1 -0
  117. package/dist/generated/models/thread_worktrees.js +1 -0
  118. package/dist/generated/models.js +1 -0
  119. package/dist/heap-monitor.js +122 -0
  120. package/dist/hrana-server.js +263 -0
  121. package/dist/hrana-server.test.js +370 -0
  122. package/dist/html-actions.js +123 -0
  123. package/dist/html-actions.test.js +70 -0
  124. package/dist/html-components.js +117 -0
  125. package/dist/html-components.test.js +34 -0
  126. package/dist/image-optimizer-plugin.js +153 -0
  127. package/dist/image-utils.js +112 -0
  128. package/dist/interaction-handler.js +397 -0
  129. package/dist/ipc-polling.js +252 -0
  130. package/dist/ipc-tools-plugin.js +193 -0
  131. package/dist/kimaki-digital-twin.e2e.test.js +161 -0
  132. package/dist/kimaki-opencode-plugin-loading.e2e.test.js +87 -0
  133. package/dist/kimaki-opencode-plugin.js +17 -0
  134. package/dist/kimaki-opencode-plugin.test.js +98 -0
  135. package/dist/limit-heading-depth.js +25 -0
  136. package/dist/limit-heading-depth.test.js +105 -0
  137. package/dist/logger.js +165 -0
  138. package/dist/markdown.js +342 -0
  139. package/dist/markdown.test.js +257 -0
  140. package/dist/message-finish-field.e2e.test.js +165 -0
  141. package/dist/message-formatting.js +413 -0
  142. package/dist/message-formatting.test.js +73 -0
  143. package/dist/message-preprocessing.js +330 -0
  144. package/dist/onboarding-tutorial.js +172 -0
  145. package/dist/onboarding-welcome.js +37 -0
  146. package/dist/openai-realtime.js +224 -0
  147. package/dist/opencode-command-detection.js +65 -0
  148. package/dist/opencode-command-detection.test.js +240 -0
  149. package/dist/opencode-command.js +129 -0
  150. package/dist/opencode-command.test.js +48 -0
  151. package/dist/opencode-interrupt-plugin.js +361 -0
  152. package/dist/opencode-interrupt-plugin.test.js +458 -0
  153. package/dist/opencode.js +861 -0
  154. package/dist/otto/branding.js +22 -0
  155. package/dist/otto/index.js +21 -0
  156. package/dist/parse-permission-rules.test.js +117 -0
  157. package/dist/patch-text-parser.js +97 -0
  158. package/dist/plugin-logger.js +59 -0
  159. package/dist/privacy-sanitizer.js +105 -0
  160. package/dist/queue-advanced-abort.e2e.test.js +293 -0
  161. package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
  162. package/dist/queue-advanced-e2e-setup.js +786 -0
  163. package/dist/queue-advanced-footer.e2e.test.js +472 -0
  164. package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
  165. package/dist/queue-advanced-permissions-typing.e2e.test.js +180 -0
  166. package/dist/queue-advanced-question.e2e.test.js +261 -0
  167. package/dist/queue-advanced-typing-interrupt.e2e.test.js +114 -0
  168. package/dist/queue-advanced-typing.e2e.test.js +153 -0
  169. package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
  170. package/dist/queue-interrupt-drain.e2e.test.js +135 -0
  171. package/dist/queue-question-select-drain.e2e.test.js +120 -0
  172. package/dist/runtime-idle-sweeper.js +52 -0
  173. package/dist/runtime-lifecycle.e2e.test.js +508 -0
  174. package/dist/sentry.js +23 -0
  175. package/dist/session-handler/agent-utils.js +67 -0
  176. package/dist/session-handler/event-stream-state.js +420 -0
  177. package/dist/session-handler/event-stream-state.test.js +563 -0
  178. package/dist/session-handler/model-utils.js +124 -0
  179. package/dist/session-handler/opencode-session-event-log.js +94 -0
  180. package/dist/session-handler/thread-runtime-state.js +104 -0
  181. package/dist/session-handler/thread-session-runtime.js +3258 -0
  182. package/dist/session-handler.js +9 -0
  183. package/dist/session-search.js +100 -0
  184. package/dist/session-search.test.js +40 -0
  185. package/dist/session-title-rename.test.js +80 -0
  186. package/dist/startup-service.js +153 -0
  187. package/dist/startup-time.e2e.test.js +296 -0
  188. package/dist/store.js +17 -0
  189. package/dist/system-message.js +613 -0
  190. package/dist/system-message.test.js +602 -0
  191. package/dist/task-runner.js +295 -0
  192. package/dist/task-schedule.js +209 -0
  193. package/dist/task-schedule.test.js +71 -0
  194. package/dist/test-utils.js +299 -0
  195. package/dist/thinking-utils.js +35 -0
  196. package/dist/thread-message-queue.e2e.test.js +999 -0
  197. package/dist/tools.js +357 -0
  198. package/dist/undo-redo.e2e.test.js +161 -0
  199. package/dist/unnest-code-blocks.js +146 -0
  200. package/dist/unnest-code-blocks.test.js +673 -0
  201. package/dist/upgrade.js +114 -0
  202. package/dist/utils.js +144 -0
  203. package/dist/voice-attachment.js +34 -0
  204. package/dist/voice-handler.js +646 -0
  205. package/dist/voice-message.e2e.test.js +1021 -0
  206. package/dist/voice.js +447 -0
  207. package/dist/voice.test.js +235 -0
  208. package/dist/wait-session.js +94 -0
  209. package/dist/websockify.js +69 -0
  210. package/dist/worker-types.js +4 -0
  211. package/dist/worktree-lifecycle.e2e.test.js +308 -0
  212. package/dist/worktree-utils.js +3 -0
  213. package/dist/worktrees.js +929 -0
  214. package/dist/worktrees.test.js +189 -0
  215. package/dist/xml.js +92 -0
  216. package/dist/xml.test.js +32 -0
  217. package/package.json +98 -0
  218. package/schema.prisma +295 -0
  219. package/skills/batch/SKILL.md +87 -0
  220. package/skills/critique/SKILL.md +112 -0
  221. package/skills/egaki/SKILL.md +100 -0
  222. package/skills/errore/SKILL.md +647 -0
  223. package/skills/event-sourcing-state/SKILL.md +252 -0
  224. package/skills/gitchamber/SKILL.md +93 -0
  225. package/skills/goke/SKILL.md +644 -0
  226. package/skills/jitter/EDITOR.md +219 -0
  227. package/skills/jitter/EXPORT-INTERNALS.md +309 -0
  228. package/skills/jitter/SKILL.md +158 -0
  229. package/skills/jitter/jitter-clipboard.json +1042 -0
  230. package/skills/jitter/package.json +14 -0
  231. package/skills/jitter/tsconfig.json +15 -0
  232. package/skills/jitter/utils/actions.ts +212 -0
  233. package/skills/jitter/utils/export.ts +114 -0
  234. package/skills/jitter/utils/index.ts +141 -0
  235. package/skills/jitter/utils/snapshot.ts +154 -0
  236. package/skills/jitter/utils/traverse.ts +246 -0
  237. package/skills/jitter/utils/types.ts +279 -0
  238. package/skills/jitter/utils/wait.ts +133 -0
  239. package/skills/lintcn/SKILL.md +873 -0
  240. package/skills/new-skill/SKILL.md +211 -0
  241. package/skills/npm-package/SKILL.md +239 -0
  242. package/skills/playwriter/SKILL.md +35 -0
  243. package/skills/proxyman/SKILL.md +215 -0
  244. package/skills/security-review/SKILL.md +208 -0
  245. package/skills/simplify/SKILL.md +58 -0
  246. package/skills/spiceflow/SKILL.md +14 -0
  247. package/skills/termcast/SKILL.md +945 -0
  248. package/skills/tuistory/SKILL.md +250 -0
  249. package/skills/usecomputer/SKILL.md +264 -0
  250. package/skills/x-articles/SKILL.md +554 -0
  251. package/skills/zele/SKILL.md +112 -0
  252. package/skills/zustand-centralized-state/SKILL.md +1004 -0
  253. package/src/agent-model.e2e.test.ts +976 -0
  254. package/src/ai-tool-to-genai.test.ts +296 -0
  255. package/src/ai-tool-to-genai.ts +283 -0
  256. package/src/ai-tool.ts +39 -0
  257. package/src/anthropic-auth-plugin.test.ts +159 -0
  258. package/src/anthropic-auth-plugin.ts +861 -0
  259. package/src/anthropic-auth-state.ts +282 -0
  260. package/src/bin.ts +111 -0
  261. package/src/channel-management.ts +334 -0
  262. package/src/cli-parsing.test.ts +195 -0
  263. package/src/cli-send-thread.e2e.test.ts +464 -0
  264. package/src/cli.ts +4581 -0
  265. package/src/commands/abort.ts +89 -0
  266. package/src/commands/action-buttons.ts +364 -0
  267. package/src/commands/add-project.ts +149 -0
  268. package/src/commands/agent.ts +473 -0
  269. package/src/commands/ask-question.ts +390 -0
  270. package/src/commands/btw.ts +164 -0
  271. package/src/commands/compact.ts +157 -0
  272. package/src/commands/context-usage.ts +199 -0
  273. package/src/commands/create-new-project.ts +190 -0
  274. package/src/commands/diff.ts +91 -0
  275. package/src/commands/file-upload.ts +389 -0
  276. package/src/commands/fork.ts +321 -0
  277. package/src/commands/gemini-apikey.ts +104 -0
  278. package/src/commands/login.ts +1173 -0
  279. package/src/commands/mcp.ts +307 -0
  280. package/src/commands/memory-snapshot.ts +30 -0
  281. package/src/commands/mention-mode.ts +68 -0
  282. package/src/commands/merge-worktree.ts +223 -0
  283. package/src/commands/model-variant.ts +483 -0
  284. package/src/commands/model.ts +1053 -0
  285. package/src/commands/new-worktree.ts +510 -0
  286. package/src/commands/paginated-select.ts +81 -0
  287. package/src/commands/permissions.ts +397 -0
  288. package/src/commands/queue.ts +271 -0
  289. package/src/commands/remove-project.ts +155 -0
  290. package/src/commands/restart-opencode-server.ts +162 -0
  291. package/src/commands/resume.ts +230 -0
  292. package/src/commands/run-command.ts +123 -0
  293. package/src/commands/screenshare.test.ts +30 -0
  294. package/src/commands/screenshare.ts +366 -0
  295. package/src/commands/session-id.ts +109 -0
  296. package/src/commands/session.ts +227 -0
  297. package/src/commands/share.ts +106 -0
  298. package/src/commands/tasks.ts +293 -0
  299. package/src/commands/types.ts +25 -0
  300. package/src/commands/undo-redo.ts +386 -0
  301. package/src/commands/unset-model.ts +173 -0
  302. package/src/commands/upgrade.ts +52 -0
  303. package/src/commands/user-command.ts +198 -0
  304. package/src/commands/verbosity.ts +173 -0
  305. package/src/commands/worktree-settings.ts +70 -0
  306. package/src/commands/worktrees.ts +552 -0
  307. package/src/condense-memory.ts +36 -0
  308. package/src/config.ts +111 -0
  309. package/src/context-awareness-plugin.test.ts +142 -0
  310. package/src/context-awareness-plugin.ts +510 -0
  311. package/src/critique-utils.ts +139 -0
  312. package/src/database.ts +1876 -0
  313. package/src/db.test.ts +162 -0
  314. package/src/db.ts +286 -0
  315. package/src/debounce-timeout.ts +43 -0
  316. package/src/debounced-process-flush.ts +104 -0
  317. package/src/discord-bot.ts +1330 -0
  318. package/src/discord-command-registration.ts +693 -0
  319. package/src/discord-urls.ts +88 -0
  320. package/src/discord-utils.test.ts +153 -0
  321. package/src/discord-utils.ts +800 -0
  322. package/src/errors.ts +201 -0
  323. package/src/escape-backticks.test.ts +469 -0
  324. package/src/event-stream-real-capture.e2e.test.ts +692 -0
  325. package/src/eventsource-parser.test.ts +351 -0
  326. package/src/exec-async.ts +35 -0
  327. package/src/external-opencode-sync.ts +685 -0
  328. package/src/format-tables.test.ts +335 -0
  329. package/src/format-tables.ts +445 -0
  330. package/src/forum-sync/config.ts +92 -0
  331. package/src/forum-sync/discord-operations.ts +241 -0
  332. package/src/forum-sync/index.ts +9 -0
  333. package/src/forum-sync/markdown.ts +172 -0
  334. package/src/forum-sync/sync-to-discord.ts +595 -0
  335. package/src/forum-sync/sync-to-files.ts +294 -0
  336. package/src/forum-sync/types.ts +175 -0
  337. package/src/forum-sync/watchers.ts +454 -0
  338. package/src/gateway-proxy-reconnect.e2e.test.ts +523 -0
  339. package/src/gateway-proxy.e2e.test.ts +640 -0
  340. package/src/genai-worker-wrapper.ts +164 -0
  341. package/src/genai-worker.ts +386 -0
  342. package/src/genai.ts +321 -0
  343. package/src/generated/browser.ts +114 -0
  344. package/src/generated/client.ts +138 -0
  345. package/src/generated/commonInputTypes.ts +736 -0
  346. package/src/generated/enums.ts +88 -0
  347. package/src/generated/internal/class.ts +384 -0
  348. package/src/generated/internal/prismaNamespace.ts +2386 -0
  349. package/src/generated/internal/prismaNamespaceBrowser.ts +326 -0
  350. package/src/generated/models/bot_api_keys.ts +1288 -0
  351. package/src/generated/models/bot_tokens.ts +1656 -0
  352. package/src/generated/models/channel_agents.ts +1256 -0
  353. package/src/generated/models/channel_directories.ts +1859 -0
  354. package/src/generated/models/channel_mention_mode.ts +1300 -0
  355. package/src/generated/models/channel_models.ts +1288 -0
  356. package/src/generated/models/channel_verbosity.ts +1228 -0
  357. package/src/generated/models/channel_worktrees.ts +1300 -0
  358. package/src/generated/models/forum_sync_configs.ts +1452 -0
  359. package/src/generated/models/global_models.ts +1288 -0
  360. package/src/generated/models/ipc_requests.ts +1485 -0
  361. package/src/generated/models/part_messages.ts +1302 -0
  362. package/src/generated/models/scheduled_tasks.ts +2320 -0
  363. package/src/generated/models/session_agents.ts +1086 -0
  364. package/src/generated/models/session_events.ts +1439 -0
  365. package/src/generated/models/session_models.ts +1114 -0
  366. package/src/generated/models/session_start_sources.ts +1408 -0
  367. package/src/generated/models/thread_sessions.ts +1781 -0
  368. package/src/generated/models/thread_worktrees.ts +1356 -0
  369. package/src/generated/models.ts +30 -0
  370. package/src/heap-monitor.ts +152 -0
  371. package/src/hrana-server.test.ts +434 -0
  372. package/src/hrana-server.ts +314 -0
  373. package/src/html-actions.test.ts +87 -0
  374. package/src/html-actions.ts +174 -0
  375. package/src/html-components.test.ts +38 -0
  376. package/src/html-components.ts +181 -0
  377. package/src/image-optimizer-plugin.ts +194 -0
  378. package/src/image-utils.ts +149 -0
  379. package/src/interaction-handler.ts +576 -0
  380. package/src/ipc-polling.ts +326 -0
  381. package/src/ipc-tools-plugin.ts +236 -0
  382. package/src/kimaki-digital-twin.e2e.test.ts +199 -0
  383. package/src/kimaki-opencode-plugin-loading.e2e.test.ts +109 -0
  384. package/src/kimaki-opencode-plugin.test.ts +108 -0
  385. package/src/kimaki-opencode-plugin.ts +18 -0
  386. package/src/limit-heading-depth.test.ts +116 -0
  387. package/src/limit-heading-depth.ts +26 -0
  388. package/src/logger.ts +208 -0
  389. package/src/markdown.test.ts +308 -0
  390. package/src/markdown.ts +410 -0
  391. package/src/message-finish-field.e2e.test.ts +192 -0
  392. package/src/message-formatting.test.ts +81 -0
  393. package/src/message-formatting.ts +533 -0
  394. package/src/message-preprocessing.ts +455 -0
  395. package/src/onboarding-tutorial.ts +176 -0
  396. package/src/onboarding-welcome.ts +49 -0
  397. package/src/openai-realtime.ts +358 -0
  398. package/src/opencode-command-detection.test.ts +307 -0
  399. package/src/opencode-command-detection.ts +76 -0
  400. package/src/opencode-command.test.ts +70 -0
  401. package/src/opencode-command.ts +188 -0
  402. package/src/opencode-interrupt-plugin.test.ts +677 -0
  403. package/src/opencode-interrupt-plugin.ts +477 -0
  404. package/src/opencode.ts +1110 -0
  405. package/src/otto/branding.ts +23 -0
  406. package/src/otto/index.ts +22 -0
  407. package/src/parse-permission-rules.test.ts +127 -0
  408. package/src/patch-text-parser.ts +107 -0
  409. package/src/plugin-logger.ts +68 -0
  410. package/src/privacy-sanitizer.ts +142 -0
  411. package/src/queue-advanced-abort.e2e.test.ts +382 -0
  412. package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
  413. package/src/queue-advanced-e2e-setup.ts +873 -0
  414. package/src/queue-advanced-footer.e2e.test.ts +576 -0
  415. package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
  416. package/src/queue-advanced-permissions-typing.e2e.test.ts +245 -0
  417. package/src/queue-advanced-question.e2e.test.ts +316 -0
  418. package/src/queue-advanced-typing-interrupt.e2e.test.ts +146 -0
  419. package/src/queue-advanced-typing.e2e.test.ts +199 -0
  420. package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
  421. package/src/queue-interrupt-drain.e2e.test.ts +166 -0
  422. package/src/queue-question-select-drain.e2e.test.ts +152 -0
  423. package/src/runtime-idle-sweeper.ts +76 -0
  424. package/src/runtime-lifecycle.e2e.test.ts +641 -0
  425. package/src/schema.sql +173 -0
  426. package/src/sentry.ts +26 -0
  427. package/src/session-handler/agent-utils.ts +97 -0
  428. package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
  429. package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
  430. package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
  431. package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
  432. package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
  433. package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
  434. package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +21 -0
  435. package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
  436. package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
  437. package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
  438. package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
  439. package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
  440. package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
  441. package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
  442. package/src/session-handler/event-stream-state.test.ts +645 -0
  443. package/src/session-handler/event-stream-state.ts +608 -0
  444. package/src/session-handler/model-utils.ts +183 -0
  445. package/src/session-handler/opencode-session-event-log.ts +130 -0
  446. package/src/session-handler/thread-runtime-state.ts +212 -0
  447. package/src/session-handler/thread-session-runtime.ts +4281 -0
  448. package/src/session-handler.ts +15 -0
  449. package/src/session-search.test.ts +50 -0
  450. package/src/session-search.ts +148 -0
  451. package/src/session-title-rename.test.ts +112 -0
  452. package/src/startup-service.ts +200 -0
  453. package/src/startup-time.e2e.test.ts +373 -0
  454. package/src/store.ts +122 -0
  455. package/src/system-message.test.ts +612 -0
  456. package/src/system-message.ts +723 -0
  457. package/src/task-runner.ts +421 -0
  458. package/src/task-schedule.test.ts +84 -0
  459. package/src/task-schedule.ts +311 -0
  460. package/src/test-utils.ts +435 -0
  461. package/src/thinking-utils.ts +61 -0
  462. package/src/thread-message-queue.e2e.test.ts +1219 -0
  463. package/src/tools.ts +430 -0
  464. package/src/undici.d.ts +12 -0
  465. package/src/undo-redo.e2e.test.ts +209 -0
  466. package/src/unnest-code-blocks.test.ts +713 -0
  467. package/src/unnest-code-blocks.ts +185 -0
  468. package/src/upgrade.ts +127 -0
  469. package/src/utils.ts +212 -0
  470. package/src/voice-attachment.ts +51 -0
  471. package/src/voice-handler.ts +908 -0
  472. package/src/voice-message.e2e.test.ts +1255 -0
  473. package/src/voice.test.ts +281 -0
  474. package/src/voice.ts +627 -0
  475. package/src/wait-session.ts +147 -0
  476. package/src/websockify.ts +101 -0
  477. package/src/worker-types.ts +64 -0
  478. package/src/worktree-lifecycle.e2e.test.ts +391 -0
  479. package/src/worktree-utils.ts +4 -0
  480. package/src/worktrees.test.ts +223 -0
  481. package/src/worktrees.ts +1294 -0
  482. package/src/xml.test.ts +38 -0
  483. package/src/xml.ts +121 -0
@@ -0,0 +1,510 @@
1
+ // Worktree management command: /new-worktree
2
+ // Uses OpenCode SDK v2 to create worktrees with kimaki- prefix
3
+ // Creates thread immediately, then worktree in background so user can type
4
+
5
+ import {
6
+ ChannelType,
7
+ REST,
8
+ type TextChannel,
9
+ type ThreadChannel,
10
+ type Message,
11
+ } from 'discord.js'
12
+ import fs from 'node:fs'
13
+ import type { CommandContext } from './types.js'
14
+ import {
15
+ createPendingWorktree,
16
+ setWorktreeReady,
17
+ setWorktreeError,
18
+ getChannelDirectory,
19
+ getThreadWorktree,
20
+ } from '../database.js'
21
+ import {
22
+ SILENT_MESSAGE_FLAGS,
23
+ reactToThread,
24
+ resolveProjectDirectoryFromAutocomplete,
25
+ } from '../discord-utils.js'
26
+ import { createLogger, LogPrefix } from '../logger.js'
27
+ import { notifyError } from '../sentry.js'
28
+ import {
29
+ createWorktreeWithSubmodules,
30
+ execAsync,
31
+ listBranchesByLastCommit,
32
+ validateBranchRef,
33
+ } from '../worktrees.js'
34
+ import { WORKTREE_PREFIX } from './merge-worktree.js'
35
+ import type { AutocompleteContext } from './types.js'
36
+ import * as errore from 'errore'
37
+
38
+ const logger = createLogger(LogPrefix.WORKTREE)
39
+
40
+ /** Status message shown while a worktree is being created. */
41
+ export function worktreeCreatingMessage(worktreeName: string): string {
42
+ return `🌳 **Creating worktree: ${worktreeName}**\n⏳ Setting up...`
43
+ }
44
+
45
+ class WorktreeError extends Error {
46
+ constructor(message: string, options?: { cause?: unknown }) {
47
+ super(message, options)
48
+ this.name = 'WorktreeError'
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Format worktree name: lowercase, spaces to dashes, remove special chars, add opencode/kimaki- prefix.
54
+ * "My Feature" → "opencode/kimaki-my-feature"
55
+ * Returns empty string if no valid name can be extracted.
56
+ */
57
+ export function formatWorktreeName(name: string): string {
58
+ const formatted = name
59
+ .toLowerCase()
60
+ .trim()
61
+ .replace(/\s+/g, '-')
62
+ .replace(/[^a-z0-9-]/g, '')
63
+
64
+ if (!formatted) {
65
+ return ''
66
+ }
67
+ return `opencode/kimaki-${formatted}`
68
+ }
69
+
70
+ /**
71
+ * Derive worktree name from thread name.
72
+ * Handles existing "⬦ worktree: opencode/kimaki-name" format or uses thread name directly.
73
+ */
74
+ function deriveWorktreeNameFromThread(threadName: string): string {
75
+ // Handle existing "⬦ worktree: opencode/kimaki-name" format
76
+ const worktreeMatch = threadName.match(/worktree:\s*(.+)$/i)
77
+ const extractedName = worktreeMatch?.[1]?.trim()
78
+ if (extractedName) {
79
+ // If already has opencode/kimaki- prefix, return as is
80
+ if (extractedName.startsWith('opencode/kimaki-')) {
81
+ return extractedName
82
+ }
83
+ return formatWorktreeName(extractedName)
84
+ }
85
+ // Use thread name directly
86
+ return formatWorktreeName(threadName)
87
+ }
88
+
89
+ /**
90
+ * Get project directory from database.
91
+ */
92
+ async function getProjectDirectoryFromChannel(
93
+ channel: TextChannel,
94
+ ): Promise<string | WorktreeError> {
95
+ const channelConfig = await getChannelDirectory(channel.id)
96
+
97
+ if (!channelConfig) {
98
+ return new WorktreeError(
99
+ 'This channel is not configured with a project directory',
100
+ )
101
+ }
102
+
103
+ if (!fs.existsSync(channelConfig.directory)) {
104
+ return new WorktreeError(
105
+ `Directory does not exist: ${channelConfig.directory}`,
106
+ )
107
+ }
108
+
109
+ return channelConfig.directory
110
+ }
111
+
112
+ /**
113
+ * Create worktree and update the status message when done.
114
+ * Handles the full lifecycle: pending DB entry, git creation, DB ready/error,
115
+ * tree emoji reaction, and editing the status message.
116
+ *
117
+ * starterMessage is optional — if omitted, status edits are skipped (creation
118
+ * still proceeds). This keeps worktree creation independent of Discord message
119
+ * delivery, so a transient send failure never silently skips the worktree.
120
+ *
121
+ * Returns the worktree directory on success, or an Error on failure.
122
+ * Never throws — all internal errors are caught and returned as Error values.
123
+ */
124
+ export async function createWorktreeInBackground({
125
+ thread,
126
+ starterMessage,
127
+ worktreeName,
128
+ projectDirectory,
129
+ baseBranch,
130
+ rest,
131
+ }: {
132
+ thread: ThreadChannel
133
+ starterMessage?: Message
134
+ worktreeName: string
135
+ projectDirectory: string
136
+ baseBranch?: string
137
+ rest: REST
138
+ }): Promise<string | Error> {
139
+ return errore.tryAsync({
140
+ try: async () => {
141
+ logger.log(
142
+ `Creating worktree "${worktreeName}" for project ${projectDirectory}${baseBranch ? ` from ${baseBranch}` : ''}`,
143
+ )
144
+
145
+ await createPendingWorktree({
146
+ threadId: thread.id,
147
+ worktreeName,
148
+ projectDirectory,
149
+ })
150
+
151
+ // Serialize status message edits so onProgress can't overwrite the
152
+ // final success/error edit even if Discord's API is slow.
153
+ let editChain: Promise<void> = Promise.resolve()
154
+ const editStatus = (content: string) => {
155
+ editChain = editChain
156
+ .then(async () => {
157
+ await starterMessage?.edit(content)
158
+ })
159
+ .catch(() => {})
160
+ }
161
+
162
+ const worktreeResult = await createWorktreeWithSubmodules({
163
+ directory: projectDirectory,
164
+ name: worktreeName,
165
+ baseBranch,
166
+ onProgress: (phase) => {
167
+ editStatus(`🌳 **Worktree: ${worktreeName}**\n${phase}`)
168
+ },
169
+ })
170
+
171
+ if (worktreeResult instanceof Error) {
172
+ const errorMsg = worktreeResult.message
173
+ logger.error('[WORKTREE] Creation failed:', worktreeResult)
174
+ await setWorktreeError({ threadId: thread.id, errorMessage: errorMsg })
175
+ editStatus(`🌳 **Worktree: ${worktreeName}**\n❌ ${errorMsg}`)
176
+ await editChain
177
+ return worktreeResult
178
+ }
179
+
180
+ // Success - update database and edit starter message
181
+ await setWorktreeReady({
182
+ threadId: thread.id,
183
+ worktreeDirectory: worktreeResult.directory,
184
+ })
185
+
186
+ // React with tree emoji to mark as worktree thread
187
+ await reactToThread({
188
+ rest,
189
+ threadId: thread.id,
190
+ channelId: thread.parentId || undefined,
191
+ emoji: '🌳',
192
+ })
193
+
194
+ editStatus(
195
+ `🌳 **Worktree: ${worktreeName}**\n` +
196
+ `📁 \`${worktreeResult.directory}\`\n` +
197
+ `🌿 Branch: \`${worktreeResult.branch}\``,
198
+ )
199
+ await editChain
200
+
201
+ return worktreeResult.directory
202
+ },
203
+ catch: (e) => {
204
+ logger.error('[WORKTREE] Unexpected error in createWorktreeInBackground:', e)
205
+ return new Error(`Worktree creation failed: ${e instanceof Error ? e.message : String(e)}`, { cause: e })
206
+ },
207
+ })
208
+ }
209
+
210
+ async function findExistingWorktreePath({
211
+ projectDirectory,
212
+ worktreeName,
213
+ }: {
214
+ projectDirectory: string
215
+ worktreeName: string
216
+ }): Promise<string | undefined | Error> {
217
+ const listResult = await errore.tryAsync({
218
+ try: () =>
219
+ execAsync('git worktree list --porcelain', { cwd: projectDirectory }),
220
+ catch: (e) => new WorktreeError('Failed to list worktrees', { cause: e }),
221
+ })
222
+ if (errore.isError(listResult)) {
223
+ return listResult
224
+ }
225
+
226
+ const lines = listResult.stdout.split('\n')
227
+ let currentPath = ''
228
+ const branchRef = `refs/heads/${worktreeName}`
229
+
230
+ for (const line of lines) {
231
+ if (line.startsWith('worktree ')) {
232
+ currentPath = line.slice('worktree '.length)
233
+ continue
234
+ }
235
+ if (
236
+ line.startsWith('branch ') &&
237
+ line.slice('branch '.length) === branchRef
238
+ ) {
239
+ return currentPath || undefined
240
+ }
241
+ }
242
+
243
+ return undefined
244
+ }
245
+
246
+ export async function handleNewWorktreeCommand({
247
+ command,
248
+ }: CommandContext): Promise<void> {
249
+ await command.deferReply()
250
+
251
+ const channel = command.channel
252
+ if (!channel) {
253
+ await command.editReply('Cannot determine channel')
254
+ return
255
+ }
256
+
257
+ const isThread =
258
+ channel.type === ChannelType.PublicThread ||
259
+ channel.type === ChannelType.PrivateThread
260
+
261
+ // Handle command in existing thread - attach worktree to this thread
262
+ if (isThread) {
263
+ await handleWorktreeInThread({
264
+ command,
265
+ thread: channel as ThreadChannel,
266
+ })
267
+ return
268
+ }
269
+
270
+ // Handle command in text channel - create new thread with worktree (existing behavior)
271
+ if (channel.type !== ChannelType.GuildText) {
272
+ await command.editReply(
273
+ 'This command can only be used in text channels or threads',
274
+ )
275
+ return
276
+ }
277
+
278
+ const rawName = command.options.getString('name')
279
+ const rawBaseBranch = command.options.getString('base-branch') || undefined
280
+ if (!rawName) {
281
+ await command.editReply(
282
+ 'Name is required when creating a worktree from a text channel. Use `/new-worktree name:my-feature`',
283
+ )
284
+ return
285
+ }
286
+
287
+ const worktreeName = formatWorktreeName(rawName)
288
+ if (!worktreeName) {
289
+ await command.editReply(
290
+ 'Invalid worktree name. Please use letters, numbers, and spaces.',
291
+ )
292
+ return
293
+ }
294
+
295
+ const textChannel = channel as TextChannel
296
+
297
+ const projectDirectory = await getProjectDirectoryFromChannel(
298
+ textChannel,
299
+ )
300
+ if (errore.isError(projectDirectory)) {
301
+ await command.editReply(projectDirectory.message)
302
+ return
303
+ }
304
+
305
+ let baseBranch = rawBaseBranch
306
+ if (baseBranch) {
307
+ const validated = await validateBranchRef({
308
+ directory: projectDirectory,
309
+ ref: baseBranch,
310
+ })
311
+ if (validated instanceof Error) {
312
+ await command.editReply(`Invalid base branch: \`${baseBranch}\``)
313
+ return
314
+ }
315
+ baseBranch = validated
316
+ }
317
+
318
+ const existingWorktree = await findExistingWorktreePath({
319
+ projectDirectory,
320
+ worktreeName,
321
+ })
322
+ if (errore.isError(existingWorktree)) {
323
+ await command.editReply(existingWorktree.message)
324
+ return
325
+ }
326
+ if (existingWorktree) {
327
+ await command.editReply(
328
+ `Worktree \`${worktreeName}\` already exists at \`${existingWorktree}\``,
329
+ )
330
+ return
331
+ }
332
+
333
+ // Create thread immediately so user can start typing
334
+ const result = await errore.tryAsync({
335
+ try: async () => {
336
+ const starterMessage = await textChannel.send({
337
+ content: worktreeCreatingMessage(worktreeName),
338
+ flags: SILENT_MESSAGE_FLAGS,
339
+ })
340
+
341
+ const thread = await starterMessage.startThread({
342
+ name: `${WORKTREE_PREFIX}worktree: ${worktreeName}`,
343
+ autoArchiveDuration: 1440,
344
+ reason: 'Worktree session',
345
+ })
346
+
347
+ // Add user to thread so it appears in their sidebar
348
+ await thread.members.add(command.user.id)
349
+
350
+ return { thread, starterMessage }
351
+ },
352
+ catch: (e) => new WorktreeError('Failed to create thread', { cause: e }),
353
+ })
354
+
355
+ if (errore.isError(result)) {
356
+ logger.error('[NEW-WORKTREE] Error:', result.cause)
357
+ await command.editReply(result.message)
358
+ return
359
+ }
360
+
361
+ const { thread, starterMessage } = result
362
+
363
+ await command.editReply(`Creating worktree in ${thread.toString()}`)
364
+
365
+ // Create worktree in background (don't await)
366
+ createWorktreeInBackground({
367
+ thread,
368
+ starterMessage,
369
+ worktreeName,
370
+ projectDirectory,
371
+ baseBranch,
372
+ rest: command.client.rest,
373
+ }).catch((e) => {
374
+ logger.error('[NEW-WORKTREE] Background error:', e)
375
+ void notifyError(e, 'Background worktree creation failed')
376
+ })
377
+ }
378
+
379
+ /**
380
+ * Handle /new-worktree when called inside an existing thread.
381
+ * Attaches a worktree to the current thread, using thread name if no name provided.
382
+ */
383
+ async function handleWorktreeInThread({
384
+ command,
385
+ thread,
386
+ }: {
387
+ command: CommandContext['command']
388
+ thread: ThreadChannel
389
+ }): Promise<void> {
390
+ // Error if thread already has a worktree
391
+ if (await getThreadWorktree(thread.id)) {
392
+ await command.editReply('This thread already has a worktree attached.')
393
+ return
394
+ }
395
+
396
+ // Get worktree name from parameter or derive from thread name
397
+ const rawName = command.options.getString('name')
398
+ const rawBaseBranch = command.options.getString('base-branch') || undefined
399
+ const worktreeName = rawName
400
+ ? formatWorktreeName(rawName)
401
+ : deriveWorktreeNameFromThread(thread.name)
402
+
403
+ if (!worktreeName) {
404
+ await command.editReply(
405
+ 'Invalid worktree name. Please provide a name or rename the thread.',
406
+ )
407
+ return
408
+ }
409
+
410
+ // Get parent channel for project directory
411
+ const parent = thread.parent
412
+ if (!parent || parent.type !== ChannelType.GuildText) {
413
+ await command.editReply('Cannot determine parent channel')
414
+ return
415
+ }
416
+
417
+ const projectDirectory = await getProjectDirectoryFromChannel(
418
+ parent as TextChannel,
419
+ )
420
+ if (errore.isError(projectDirectory)) {
421
+ await command.editReply(projectDirectory.message)
422
+ return
423
+ }
424
+
425
+ let baseBranch = rawBaseBranch
426
+ if (baseBranch) {
427
+ const validated = await validateBranchRef({
428
+ directory: projectDirectory,
429
+ ref: baseBranch,
430
+ })
431
+ if (validated instanceof Error) {
432
+ await command.editReply(`Invalid base branch: \`${baseBranch}\``)
433
+ return
434
+ }
435
+ baseBranch = validated
436
+ }
437
+
438
+ const existingWorktreePath = await findExistingWorktreePath({
439
+ projectDirectory,
440
+ worktreeName,
441
+ })
442
+ if (errore.isError(existingWorktreePath)) {
443
+ await command.editReply(existingWorktreePath.message)
444
+ return
445
+ }
446
+ if (existingWorktreePath) {
447
+ await command.editReply(
448
+ `Worktree \`${worktreeName}\` already exists at \`${existingWorktreePath}\``,
449
+ )
450
+ return
451
+ }
452
+
453
+ // Send status message in thread
454
+ const statusMessage = await thread.send({
455
+ content: worktreeCreatingMessage(worktreeName),
456
+ flags: SILENT_MESSAGE_FLAGS,
457
+ })
458
+
459
+ await command.editReply(
460
+ `Creating worktree \`${worktreeName}\` for this thread...`,
461
+ )
462
+
463
+ createWorktreeInBackground({
464
+ thread,
465
+ starterMessage: statusMessage,
466
+ worktreeName,
467
+ projectDirectory,
468
+ baseBranch,
469
+ rest: command.client.rest,
470
+ }).catch((e) => {
471
+ logger.error('[NEW-WORKTREE] Background error:', e)
472
+ void notifyError(e, 'Background worktree creation failed (in-thread)')
473
+ })
474
+ }
475
+
476
+ /**
477
+ * Autocomplete handler for /new-worktree base-branch option.
478
+ * Lists local + remote branches sorted by most recent commit date.
479
+ */
480
+ export async function handleNewWorktreeAutocomplete({
481
+ interaction,
482
+ }: AutocompleteContext): Promise<void> {
483
+ try {
484
+ const focusedValue = interaction.options.getFocused()
485
+
486
+ // interaction.channel can be null when the channel isn't cached
487
+ // (common with gateway-proxy). Use channelId which is always available
488
+ // from the raw interaction payload.
489
+ const projectDirectory = await resolveProjectDirectoryFromAutocomplete(interaction)
490
+
491
+ if (!projectDirectory) {
492
+ await interaction.respond([])
493
+ return
494
+ }
495
+
496
+ const branches = await listBranchesByLastCommit({
497
+ directory: projectDirectory,
498
+ query: focusedValue,
499
+ })
500
+
501
+ await interaction.respond(
502
+ branches.map((name) => {
503
+ return { name, value: name }
504
+ }),
505
+ )
506
+ } catch (e) {
507
+ logger.error('[NEW-WORKTREE] Autocomplete error:', e)
508
+ await interaction.respond([]).catch(() => {})
509
+ }
510
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Reusable paginated select menu helpers for Discord StringSelectMenuBuilder.
3
+ * Discord caps select menus at 25 options. This module slices a full options
4
+ * list into pages of PAGE_SIZE real items and appends "← Previous page" /
5
+ * "Next page →" sentinel options so the user can navigate. Handlers detect
6
+ * sentinel values via parsePaginationValue() and re-render the same select
7
+ * with the new page — reusing the same customId, no new interaction handlers.
8
+ */
9
+
10
+ const NAV_PREFIX = '__page_nav:'
11
+
12
+ /** 23 real items per page, leaving room for up to 2 nav sentinels (prev + next). */
13
+ const PAGE_SIZE = 23
14
+
15
+ export type SelectOption = {
16
+ label: string
17
+ value: string
18
+ description?: string
19
+ }
20
+
21
+ /**
22
+ * Build the options array for a single page, with prev/next nav sentinels.
23
+ * If allOptions fits in 25 items, returns them all with no nav items.
24
+ */
25
+ export function buildPaginatedOptions({
26
+ allOptions,
27
+ page,
28
+ }: {
29
+ allOptions: SelectOption[]
30
+ page: number
31
+ }): { options: SelectOption[]; totalPages: number } {
32
+ // No pagination needed — everything fits in one Discord select
33
+ if (allOptions.length <= 25) {
34
+ return { options: allOptions, totalPages: 1 }
35
+ }
36
+
37
+ const totalPages = Math.ceil(allOptions.length / PAGE_SIZE)
38
+ const safePage = Math.max(0, Math.min(page, totalPages - 1))
39
+ const start = safePage * PAGE_SIZE
40
+ const slice = allOptions.slice(start, start + PAGE_SIZE)
41
+
42
+ const result: SelectOption[] = []
43
+
44
+ if (safePage > 0) {
45
+ result.push({
46
+ label: `← Previous page (${safePage}/${totalPages})`,
47
+ value: `${NAV_PREFIX}${safePage - 1}`,
48
+ description: 'Go to previous page',
49
+ })
50
+ }
51
+
52
+ result.push(...slice)
53
+
54
+ if (safePage < totalPages - 1) {
55
+ result.push({
56
+ label: `Next page → (${safePage + 2}/${totalPages})`,
57
+ value: `${NAV_PREFIX}${safePage + 1}`,
58
+ description: 'Go to next page',
59
+ })
60
+ }
61
+
62
+ return { options: result, totalPages }
63
+ }
64
+
65
+ /**
66
+ * Check if a selected value is a pagination nav sentinel.
67
+ * Returns the target page number if so, undefined otherwise.
68
+ */
69
+ export function parsePaginationValue(
70
+ value: string,
71
+ ): number | undefined {
72
+ if (!value.startsWith(NAV_PREFIX)) {
73
+ return undefined
74
+ }
75
+ const pageStr = value.slice(NAV_PREFIX.length)
76
+ const page = Number(pageStr)
77
+ if (Number.isNaN(page)) {
78
+ return undefined
79
+ }
80
+ return page
81
+ }