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
package/src/cli.ts CHANGED
@@ -55,6 +55,7 @@ import {
55
55
  getScheduledTask,
56
56
  updateScheduledTask,
57
57
  getSessionStartSourcesBySessionIds,
58
+ deleteChannelDirectoryById,
58
59
  } from './database.js'
59
60
  import { ShareMarkdown } from './markdown.js'
60
61
  import {
@@ -102,6 +103,7 @@ import { spawn, execSync, type ExecSyncOptions } from 'node:child_process'
102
103
 
103
104
  import {
104
105
  setDataDir,
106
+ setProjectsDir,
105
107
  getDataDir,
106
108
  getProjectsDir,
107
109
  } from './config.js'
@@ -1834,6 +1836,10 @@ cli
1834
1836
  '--data-dir <path>',
1835
1837
  'Data directory for config and database (default: ~/.kimaki)',
1836
1838
  )
1839
+ .option(
1840
+ '--projects-dir <path>',
1841
+ 'Directory where new projects are created (default: <data-dir>/projects)',
1842
+ )
1837
1843
  .option('--install-url', 'Print the bot install URL and exit')
1838
1844
  .option(
1839
1845
  '--use-worktrees',
@@ -1877,6 +1883,7 @@ cli
1877
1883
  restartOnboarding?: boolean
1878
1884
  addChannels?: boolean
1879
1885
  dataDir?: string
1886
+ projectsDir?: string
1880
1887
  installUrl?: boolean
1881
1888
  useWorktrees?: boolean
1882
1889
  enableVoiceChannels?: boolean
@@ -1908,6 +1915,11 @@ cli
1908
1915
  cliLogger.log(`Using data directory: ${getDataDir()}`)
1909
1916
  }
1910
1917
 
1918
+ if (options.projectsDir) {
1919
+ setProjectsDir(options.projectsDir)
1920
+ cliLogger.log(`Using projects directory: ${getProjectsDir()}`)
1921
+ }
1922
+
1911
1923
  // Initialize file logging to <dataDir>/kimaki.log
1912
1924
  initLogFile(getDataDir())
1913
1925
 
@@ -3336,7 +3348,8 @@ cli
3336
3348
  'List all registered projects with their Discord channels',
3337
3349
  )
3338
3350
  .option('--json', 'Output as JSON')
3339
- .action(async (options: { json?: boolean }) => {
3351
+ .option('--prune', 'Remove stale entries whose Discord channel no longer exists')
3352
+ .action(async (options: { json?: boolean; prune?: boolean }) => {
3340
3353
  await initDatabase()
3341
3354
 
3342
3355
  const prisma = await getPrisma()
@@ -3357,26 +3370,62 @@ cli
3357
3370
  const enriched = await Promise.all(
3358
3371
  channels.map(async (ch) => {
3359
3372
  let channelName = ''
3373
+ let deleted = false
3360
3374
  if (rest) {
3361
3375
  try {
3362
3376
  const data = (await rest.get(Routes.channel(ch.channel_id))) as {
3363
3377
  name?: string
3364
3378
  }
3365
3379
  channelName = data.name || ''
3366
- } catch {
3367
- // Channel may have been deleted from Discord
3380
+ } catch (error) {
3381
+ // Only mark as deleted for Unknown Channel (10003) or 404,
3382
+ // not transient errors like rate limits or 5xx
3383
+ const isUnknownChannel =
3384
+ error instanceof Error &&
3385
+ 'code' in error &&
3386
+ 'status' in error &&
3387
+ ((error as { code: number | string }).code === 10003 ||
3388
+ (error as { status: number }).status === 404)
3389
+ deleted = isUnknownChannel
3368
3390
  }
3369
3391
  }
3370
- return { ...ch, channelName }
3392
+ return { ...ch, channelName, deleted }
3371
3393
  }),
3372
3394
  )
3373
3395
 
3396
+ // Prune stale entries if requested
3397
+ if (options.prune) {
3398
+ const stale = enriched.filter((ch) => {
3399
+ return ch.deleted
3400
+ })
3401
+ if (stale.length === 0) {
3402
+ cliLogger.log('No stale channels to prune')
3403
+ } else {
3404
+ for (const ch of stale) {
3405
+ await deleteChannelDirectoryById(ch.channel_id)
3406
+ cliLogger.log(`Pruned stale channel ${ch.channel_id} (${path.basename(ch.directory)})`)
3407
+ }
3408
+ cliLogger.log(`Pruned ${stale.length} stale channel(s)`)
3409
+ }
3410
+ // Re-filter to only show live entries after pruning
3411
+ const live = enriched.filter((ch) => {
3412
+ return !ch.deleted
3413
+ })
3414
+ if (live.length === 0) {
3415
+ cliLogger.log('No projects registered')
3416
+ process.exit(0)
3417
+ }
3418
+ enriched.length = 0
3419
+ enriched.push(...live)
3420
+ }
3421
+
3374
3422
  if (options.json) {
3375
3423
  const output = enriched.map((ch) => ({
3376
3424
  channel_id: ch.channel_id,
3377
3425
  channel_name: ch.channelName,
3378
3426
  directory: ch.directory,
3379
3427
  folder_name: path.basename(ch.directory),
3428
+ deleted: ch.deleted,
3380
3429
  }))
3381
3430
  console.log(JSON.stringify(output, null, 2))
3382
3431
  process.exit(0)
@@ -3384,8 +3433,9 @@ cli
3384
3433
 
3385
3434
  for (const ch of enriched) {
3386
3435
  const folderName = path.basename(ch.directory)
3436
+ const deletedTag = ch.deleted ? ' (deleted from Discord)' : ''
3387
3437
  const channelLabel = ch.channelName ? `#${ch.channelName}` : ch.channel_id
3388
- console.log(`\n${channelLabel}`)
3438
+ console.log(`\n${channelLabel}${deletedTag}`)
3389
3439
  console.log(` Folder: ${folderName}`)
3390
3440
  console.log(` Directory: ${ch.directory}`)
3391
3441
  console.log(` Channel ID: ${ch.channel_id}`)
@@ -3642,12 +3692,14 @@ cli
3642
3692
  )
3643
3693
  .option('-h, --host [host]', 'Local host (default: localhost)')
3644
3694
  .option('-s, --server [url]', 'Tunnel server URL')
3695
+ .option('-k, --kill', 'Kill any existing process on the port before starting')
3645
3696
  .action(
3646
3697
  async (options: {
3647
3698
  port?: string
3648
3699
  tunnelId?: string
3649
3700
  host?: string
3650
3701
  server?: string
3702
+ kill?: boolean
3651
3703
  }) => {
3652
3704
  const { runTunnel, parseCommandFromArgv, CLI_NAME } = await import(
3653
3705
  'traforo/run-tunnel'
@@ -3675,6 +3727,7 @@ cli
3675
3727
  baseDomain: 'kimaki.xyz',
3676
3728
  serverUrl: options.server,
3677
3729
  command: command.length > 0 ? command : undefined,
3730
+ kill: options.kill,
3678
3731
  })
3679
3732
  },
3680
3733
  )
@@ -3682,7 +3735,7 @@ cli
3682
3735
  cli
3683
3736
  .command(
3684
3737
  'screenshare',
3685
- 'Share your screen via VNC tunnel. Auto-stops after 1 hour. Runs until Ctrl+C. Use tmux to run in background.',
3738
+ 'Share your screen via VNC tunnel. Auto-stops after 30 minutes. Runs until Ctrl+C. Use tmux to run in background.',
3686
3739
  )
3687
3740
  .action(async () => {
3688
3741
  const { startScreenshare } = await import(
@@ -46,15 +46,14 @@ export async function handleAbortCommand({
46
46
  return
47
47
  }
48
48
 
49
+ await command.deferReply({ flags: SILENT_MESSAGE_FLAGS })
50
+
49
51
  const resolved = await resolveWorkingDirectory({
50
52
  channel: channel as TextChannel | ThreadChannel,
51
53
  })
52
54
 
53
55
  if (!resolved) {
54
- await command.reply({
55
- content: 'Could not determine project directory for this channel',
56
- flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
57
- })
56
+ await command.editReply('Could not determine project directory for this channel')
58
57
  return
59
58
  }
60
59
 
@@ -63,10 +62,7 @@ export async function handleAbortCommand({
63
62
  const sessionId = await getThreadSession(channel.id)
64
63
 
65
64
  if (!sessionId) {
66
- await command.reply({
67
- content: 'No active session in this thread',
68
- flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
69
- })
65
+ await command.editReply('No active session in this thread')
70
66
  return
71
67
  }
72
68
 
@@ -78,10 +74,7 @@ export async function handleAbortCommand({
78
74
  // No runtime but session exists — fall back to direct API abort
79
75
  const getClient = await initializeOpencodeForDirectory(projectDirectory)
80
76
  if (getClient instanceof Error) {
81
- await command.reply({
82
- content: `Failed to abort: ${getClient.message}`,
83
- flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
84
- })
77
+ await command.editReply(`Failed to abort: ${getClient.message}`)
85
78
  return
86
79
  }
87
80
  try {
@@ -91,9 +84,6 @@ export async function handleAbortCommand({
91
84
  }
92
85
  }
93
86
 
94
- await command.reply({
95
- content: `Request **aborted**`,
96
- flags: SILENT_MESSAGE_FLAGS,
97
- })
87
+ await command.editReply('Request **aborted**')
98
88
  logger.log(`Session ${sessionId} aborted by user`)
99
89
  }
@@ -14,6 +14,7 @@ import crypto from 'node:crypto'
14
14
  import { getThreadSession } from '../database.js'
15
15
  import {
16
16
  NOTIFY_MESSAGE_FLAGS,
17
+ SILENT_MESSAGE_FLAGS,
17
18
  resolveWorkingDirectory,
18
19
  sendThreadMessage,
19
20
  } from '../discord-utils.js'
@@ -185,11 +186,14 @@ export async function showActionButtons({
185
186
  sessionId,
186
187
  directory,
187
188
  buttons,
189
+ silent,
188
190
  }: {
189
191
  thread: ThreadChannel
190
192
  sessionId: string
191
193
  directory: string
192
194
  buttons: ActionButtonOption[]
195
+ /** Suppress notification when queue has pending items */
196
+ silent?: boolean
193
197
  }): Promise<void> {
194
198
  const safeButtons = buttons
195
199
  .slice(0, 3)
@@ -242,7 +246,7 @@ export async function showActionButtons({
242
246
  const message = await thread.send({
243
247
  content: '**Action Required**',
244
248
  components: [row],
245
- flags: NOTIFY_MESSAGE_FLAGS,
249
+ flags: silent ? SILENT_MESSAGE_FLAGS : NOTIFY_MESSAGE_FLAGS,
246
250
  })
247
251
 
248
252
  context.messageId = message.id
@@ -18,7 +18,7 @@ const logger = createLogger(LogPrefix.ADD_PROJECT)
18
18
  export async function handleAddProjectCommand({
19
19
  command,
20
20
  }: CommandContext): Promise<void> {
21
- await command.deferReply({ ephemeral: false })
21
+ await command.deferReply()
22
22
 
23
23
  const projectId = command.options.getString('project', true)
24
24
  const guild = command.guild
@@ -10,7 +10,7 @@ import {
10
10
  MessageFlags,
11
11
  } from 'discord.js'
12
12
  import crypto from 'node:crypto'
13
- import { sendThreadMessage, NOTIFY_MESSAGE_FLAGS } from '../discord-utils.js'
13
+ import { sendThreadMessage, NOTIFY_MESSAGE_FLAGS, SILENT_MESSAGE_FLAGS } from '../discord-utils.js'
14
14
  import { getOpencodeClient } from '../opencode.js'
15
15
  import { createLogger, LogPrefix } from '../logger.js'
16
16
 
@@ -59,12 +59,15 @@ export async function showAskUserQuestionDropdowns({
59
59
  directory,
60
60
  requestId,
61
61
  input,
62
+ silent,
62
63
  }: {
63
64
  thread: ThreadChannel
64
65
  sessionId: string
65
66
  directory: string
66
67
  requestId: string // OpenCode question request ID
67
68
  input: AskUserQuestionInput
69
+ /** Suppress notification when queue has pending items */
70
+ silent?: boolean
68
71
  }): Promise<void> {
69
72
  const contextHash = crypto.randomBytes(8).toString('hex')
70
73
 
@@ -144,7 +147,7 @@ export async function showAskUserQuestionDropdowns({
144
147
  await thread.send({
145
148
  content: `**${(q.header || '').slice(0, 200)}**\n${q.question.slice(0, 1700)}`,
146
149
  components: [actionRow],
147
- flags: NOTIFY_MESSAGE_FLAGS,
150
+ flags: silent ? SILENT_MESSAGE_FLAGS : NOTIFY_MESSAGE_FLAGS,
148
151
  })
149
152
  }
150
153
 
@@ -117,7 +117,7 @@ export async function handleContextUsageCommand({
117
117
  if (m.info.role !== 'assistant') {
118
118
  return false
119
119
  }
120
- if (!('tokens' in m.info) || !m.info.tokens) {
120
+ if (!m.info.tokens) {
121
121
  return false
122
122
  }
123
123
  return getTokenTotal(m.info.tokens) > 0
@@ -90,7 +90,7 @@ export async function handleCreateNewProjectCommand({
90
90
  command,
91
91
  appId,
92
92
  }: CommandContext): Promise<void> {
93
- await command.deferReply({ ephemeral: false })
93
+ await command.deferReply()
94
94
 
95
95
  const projectName = command.options.getString('name', true)
96
96
  const guild = command.guild
@@ -21,7 +21,7 @@ import {
21
21
  resolveTextChannel,
22
22
  sendThreadMessage,
23
23
  } from '../discord-utils.js'
24
- import { collectLastAssistantParts } from '../message-formatting.js'
24
+ import { collectSessionChunks, batchChunksForDiscord } from '../message-formatting.js'
25
25
  import { createLogger, LogPrefix } from '../logger.js'
26
26
  import * as errore from 'errore'
27
27
 
@@ -200,7 +200,7 @@ export async function handleForkSelectMenu(
200
200
  return
201
201
  }
202
202
 
203
- await interaction.deferReply({ ephemeral: false })
203
+ await interaction.deferReply()
204
204
 
205
205
  const threadChannel = interaction.channel
206
206
  if (!threadChannel) {
@@ -265,11 +265,13 @@ export async function handleForkSelectMenu(
265
265
  reason: `Forked from session ${sessionId}`,
266
266
  })
267
267
 
268
+ // Claim the forked session immediately so external polling does not race
269
+ // and create a duplicate Sync thread before the rest of this setup runs.
270
+ await setThreadSession(thread.id, forkedSession.id)
271
+
268
272
  // Add user to thread so it appears in their sidebar
269
273
  await thread.members.add(interaction.user.id)
270
274
 
271
- await setThreadSession(thread.id, forkedSession.id)
272
-
273
275
  sessionLogger.log(
274
276
  `Created forked session ${forkedSession.id} in thread ${thread.id}`,
275
277
  )
@@ -285,16 +287,15 @@ export async function handleForkSelectMenu(
285
287
  })
286
288
 
287
289
  if (messagesResponse.data) {
288
- const { partIds, content } = collectLastAssistantParts({
290
+ const { chunks } = collectSessionChunks({
289
291
  messages: messagesResponse.data,
292
+ limit: 30,
290
293
  })
291
-
292
- if (content.trim()) {
293
- const discordMessage = await sendThreadMessage(thread, content)
294
-
295
- // Store part-message mappings atomically
294
+ const batched = batchChunksForDiscord(chunks)
295
+ for (const batch of batched) {
296
+ const discordMessage = await sendThreadMessage(thread, batch.content)
296
297
  await setPartMessagesBatch(
297
- partIds.map((partId) => ({
298
+ batch.partIds.map((partId) => ({
298
299
  partId,
299
300
  messageId: discordMessage.id,
300
301
  threadId: thread.id,
@@ -92,7 +92,7 @@ export async function handleMergeWorktreeCommand({
92
92
  command,
93
93
  appId,
94
94
  }: CommandContext): Promise<void> {
95
- await command.deferReply({ ephemeral: false })
95
+ await command.deferReply()
96
96
 
97
97
  const channel = command.channel
98
98
  if (!channel || !channel.isThread()) {
@@ -37,6 +37,11 @@ import * as errore from 'errore'
37
37
 
38
38
  const logger = createLogger(LogPrefix.WORKTREE)
39
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
+
40
45
  class WorktreeError extends Error {
41
46
  constructor(message: string, options?: { cause?: unknown }) {
42
47
  super(message, options)
@@ -105,9 +110,18 @@ async function getProjectDirectoryFromChannel(
105
110
  }
106
111
 
107
112
  /**
108
- * Create worktree in background and update starter message when done.
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.
109
123
  */
110
- async function createWorktreeInBackground({
124
+ export async function createWorktreeInBackground({
111
125
  thread,
112
126
  starterMessage,
113
127
  worktreeName,
@@ -116,50 +130,69 @@ async function createWorktreeInBackground({
116
130
  rest,
117
131
  }: {
118
132
  thread: ThreadChannel
119
- starterMessage: Message
133
+ starterMessage?: Message
120
134
  worktreeName: string
121
135
  projectDirectory: string
122
136
  baseBranch?: string
123
137
  rest: REST
124
- }): Promise<void> {
125
- logger.log(
126
- `Creating worktree "${worktreeName}" for project ${projectDirectory}${baseBranch ? ` from ${baseBranch}` : ''}`,
127
- )
128
- const worktreeResult = await createWorktreeWithSubmodules({
129
- directory: projectDirectory,
130
- name: worktreeName,
131
- baseBranch,
132
- })
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
+ })
133
150
 
134
- if (worktreeResult instanceof Error) {
135
- const errorMsg = worktreeResult.message
136
- logger.error('[NEW-WORKTREE] Error:', worktreeResult)
137
- await setWorktreeError({ threadId: thread.id, errorMessage: errorMsg })
138
- await starterMessage.edit(
139
- `🌳 **Worktree: ${worktreeName}**\nāŒ ${errorMsg}`,
140
- )
141
- return
142
- }
151
+ const worktreeResult = await createWorktreeWithSubmodules({
152
+ directory: projectDirectory,
153
+ name: worktreeName,
154
+ baseBranch,
155
+ })
143
156
 
144
- // Success - update database and edit starter message
145
- await setWorktreeReady({
146
- threadId: thread.id,
147
- worktreeDirectory: worktreeResult.directory,
148
- })
157
+ if (worktreeResult instanceof Error) {
158
+ const errorMsg = worktreeResult.message
159
+ logger.error('[WORKTREE] Creation failed:', worktreeResult)
160
+ await setWorktreeError({ threadId: thread.id, errorMessage: errorMsg })
161
+ await starterMessage
162
+ ?.edit(`🌳 **Worktree: ${worktreeName}**\nāŒ ${errorMsg}`)
163
+ .catch(() => {})
164
+ return worktreeResult
165
+ }
166
+
167
+ // Success - update database and edit starter message
168
+ await setWorktreeReady({
169
+ threadId: thread.id,
170
+ worktreeDirectory: worktreeResult.directory,
171
+ })
149
172
 
150
- // React with tree emoji to mark as worktree thread
151
- await reactToThread({
152
- rest,
153
- threadId: thread.id,
154
- channelId: thread.parentId || undefined,
155
- emoji: '🌳',
156
- })
173
+ // React with tree emoji to mark as worktree thread
174
+ await reactToThread({
175
+ rest,
176
+ threadId: thread.id,
177
+ channelId: thread.parentId || undefined,
178
+ emoji: '🌳',
179
+ })
157
180
 
158
- await starterMessage.edit(
159
- `🌳 **Worktree: ${worktreeName}**\n` +
160
- `šŸ“ \`${worktreeResult.directory}\`\n` +
161
- `🌿 Branch: \`${worktreeResult.branch}\``,
162
- )
181
+ await starterMessage
182
+ ?.edit(
183
+ `🌳 **Worktree: ${worktreeName}**\n` +
184
+ `šŸ“ \`${worktreeResult.directory}\`\n` +
185
+ `🌿 Branch: \`${worktreeResult.branch}\``,
186
+ )
187
+ .catch(() => {})
188
+
189
+ return worktreeResult.directory
190
+ },
191
+ catch: (e) => {
192
+ logger.error('[WORKTREE] Unexpected error in createWorktreeInBackground:', e)
193
+ return new Error(`Worktree creation failed: ${e instanceof Error ? e.message : String(e)}`, { cause: e })
194
+ },
195
+ })
163
196
  }
164
197
 
165
198
  async function findExistingWorktreePath({
@@ -201,7 +234,7 @@ async function findExistingWorktreePath({
201
234
  export async function handleNewWorktreeCommand({
202
235
  command,
203
236
  }: CommandContext): Promise<void> {
204
- await command.deferReply({ ephemeral: false })
237
+ await command.deferReply()
205
238
 
206
239
  const channel = command.channel
207
240
  if (!channel) {
@@ -289,7 +322,7 @@ export async function handleNewWorktreeCommand({
289
322
  const result = await errore.tryAsync({
290
323
  try: async () => {
291
324
  const starterMessage = await textChannel.send({
292
- content: `🌳 **Creating worktree: ${worktreeName}**\nā³ Setting up...`,
325
+ content: worktreeCreatingMessage(worktreeName),
293
326
  flags: SILENT_MESSAGE_FLAGS,
294
327
  })
295
328
 
@@ -315,13 +348,6 @@ export async function handleNewWorktreeCommand({
315
348
 
316
349
  const { thread, starterMessage } = result
317
350
 
318
- // Store pending worktree in database
319
- await createPendingWorktree({
320
- threadId: thread.id,
321
- worktreeName,
322
- projectDirectory,
323
- })
324
-
325
351
  await command.editReply(`Creating worktree in ${thread.toString()}`)
326
352
 
327
353
  // Create worktree in background (don't await)
@@ -412,16 +438,9 @@ async function handleWorktreeInThread({
412
438
  return
413
439
  }
414
440
 
415
- // Store pending worktree in database for this existing thread
416
- await createPendingWorktree({
417
- threadId: thread.id,
418
- worktreeName,
419
- projectDirectory,
420
- })
421
-
422
441
  // Send status message in thread
423
442
  const statusMessage = await thread.send({
424
- content: `🌳 **Creating worktree: ${worktreeName}**\nā³ Setting up...`,
443
+ content: worktreeCreatingMessage(worktreeName),
425
444
  flags: SILENT_MESSAGE_FLAGS,
426
445
  })
427
446
 
@@ -17,7 +17,7 @@ export async function handleRemoveProjectCommand({
17
17
  command,
18
18
  appId,
19
19
  }: CommandContext): Promise<void> {
20
- await command.deferReply({ ephemeral: false })
20
+ await command.deferReply()
21
21
 
22
22
  const directory = command.options.getString('project', true)
23
23
  const guild = command.guild
@@ -20,7 +20,7 @@ import {
20
20
  resolveProjectDirectoryFromAutocomplete,
21
21
  NOTIFY_MESSAGE_FLAGS,
22
22
  } from '../discord-utils.js'
23
- import { collectLastAssistantParts } from '../message-formatting.js'
23
+ import { collectSessionChunks, batchChunksForDiscord } from '../message-formatting.js'
24
24
  import { createLogger, LogPrefix } from '../logger.js'
25
25
  import * as errore from 'errore'
26
26
 
@@ -29,7 +29,7 @@ const logger = createLogger(LogPrefix.RESUME)
29
29
  export async function handleResumeCommand({
30
30
  command,
31
31
  }: CommandContext): Promise<void> {
32
- await command.deferReply({ ephemeral: false })
32
+ await command.deferReply()
33
33
 
34
34
  const sessionId = command.options.getString('session', true)
35
35
  const channel = command.channel
@@ -95,11 +95,13 @@ export async function handleResumeCommand({
95
95
  reason: `Resuming session ${sessionId}`,
96
96
  })
97
97
 
98
+ // Claim the resumed session immediately so external polling does not race
99
+ // and create a duplicate Sync thread before the rest of this setup runs.
100
+ await setThreadSession(thread.id, sessionId)
101
+
98
102
  // Add user to thread so it appears in their sidebar
99
103
  await thread.members.add(command.user.id)
100
104
 
101
- await setThreadSession(thread.id, sessionId)
102
-
103
105
  logger.log(`[RESUME] Created thread ${thread.id} for session ${sessionId}`)
104
106
 
105
107
  const messagesResponse = await getClient().session.messages({
@@ -122,8 +124,9 @@ export async function handleResumeCommand({
122
124
  )
123
125
 
124
126
  try {
125
- const { partIds, content, skippedCount } = collectLastAssistantParts({
127
+ const { chunks, skippedCount } = collectSessionChunks({
126
128
  messages,
129
+ limit: 30,
127
130
  })
128
131
 
129
132
  if (skippedCount > 0) {
@@ -133,12 +136,11 @@ export async function handleResumeCommand({
133
136
  )
134
137
  }
135
138
 
136
- if (content.trim()) {
137
- const discordMessage = await sendThreadMessage(thread, content)
138
-
139
- // Store part-message mappings atomically
139
+ const batched = batchChunksForDiscord(chunks)
140
+ for (const batch of batched) {
141
+ const discordMessage = await sendThreadMessage(thread, batch.content)
140
142
  await setPartMessagesBatch(
141
- partIds.map((partId) => ({
143
+ batch.partIds.map((partId) => ({
142
144
  partId,
143
145
  messageId: discordMessage.id,
144
146
  threadId: thread.id,
@@ -0,0 +1,30 @@
1
+ import { describe, expect, test } from 'vitest'
2
+ import { buildNoVncUrl, createScreenshareTunnelId } from './screenshare.js'
3
+
4
+ describe('screenshare security defaults', () => {
5
+ test('generates a 128-bit tunnel id', () => {
6
+ const ids = new Set(
7
+ Array.from({ length: 32 }, () => {
8
+ return createScreenshareTunnelId()
9
+ }),
10
+ )
11
+
12
+ expect(ids.size).toBe(32)
13
+ for (const id of ids) {
14
+ expect(id).toMatch(/^[0-9a-f]{32}$/)
15
+ }
16
+ })
17
+
18
+ test('builds a secure noVNC URL', () => {
19
+ const url = new URL(
20
+ buildNoVncUrl({ tunnelHost: '0123456789abcdef-tunnel.kimaki.xyz' }),
21
+ )
22
+
23
+ expect(url.origin).toBe('https://novnc.com')
24
+ expect(url.searchParams.get('host')).toBe(
25
+ '0123456789abcdef-tunnel.kimaki.xyz',
26
+ )
27
+ expect(url.searchParams.get('port')).toBe('443')
28
+ expect(url.searchParams.get('encrypt')).toBe('1')
29
+ })
30
+ })