kimaki 0.26.0 → 0.28.0

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 (302) hide show
  1. package/dist/agent-model.e2e.test.js +250 -164
  2. package/dist/anthropic-account-identity.js +9 -1
  3. package/dist/anthropic-auth-plugin.js +10 -13
  4. package/dist/anthropic-auth-plugin.test.js +17 -0
  5. package/dist/anthropic-auth-state.js +1 -0
  6. package/dist/bash-tool-schema-plugin.js +32 -0
  7. package/dist/bash-tool-schema-plugin.test.js +49 -0
  8. package/dist/bin.js +73 -19
  9. package/dist/cli-commands/maintenance.js +46 -7
  10. package/dist/cli-commands/project.js +11 -27
  11. package/dist/cli-commands/send.js +30 -1
  12. package/dist/cli-commands/session.js +322 -90
  13. package/dist/cli-commands/task.js +34 -15
  14. package/dist/cli-parsing.test.js +83 -4
  15. package/dist/cli-runner.js +44 -8
  16. package/dist/cli-runner.test.js +27 -0
  17. package/dist/cli-send-thread.e2e.test.js +9 -9
  18. package/dist/cli.js +32 -0
  19. package/dist/commands/abort.js +5 -1
  20. package/dist/commands/action-buttons.js +2 -1
  21. package/dist/commands/add-project.js +4 -7
  22. package/dist/commands/agent.js +164 -32
  23. package/dist/commands/ask-question.js +23 -33
  24. package/dist/commands/ask-question.test.js +43 -1
  25. package/dist/commands/btw.js +28 -6
  26. package/dist/commands/file-upload.js +2 -1
  27. package/dist/commands/fork-subagent.js +2 -4
  28. package/dist/commands/fork.js +4 -4
  29. package/dist/commands/last-sessions.js +17 -10
  30. package/dist/commands/login.js +6 -0
  31. package/dist/commands/merge-worktree.js +47 -14
  32. package/dist/commands/model-variant.js +8 -1
  33. package/dist/commands/model.js +16 -9
  34. package/dist/commands/multioauth.js +1 -0
  35. package/dist/commands/new-worktree.js +170 -102
  36. package/dist/commands/queue.js +3 -2
  37. package/dist/commands/resume.js +4 -4
  38. package/dist/commands/session.js +78 -63
  39. package/dist/commands/user-command.js +6 -8
  40. package/dist/commands/worktrees.js +5 -13
  41. package/dist/config.js +16 -1
  42. package/dist/cpu-profiler.js +211 -0
  43. package/dist/cpu-profiler.test.js +160 -0
  44. package/dist/database.js +152 -3
  45. package/dist/db.js +33 -0
  46. package/dist/db.test.js +267 -1
  47. package/dist/default-channel-provisioning.test.js +148 -0
  48. package/dist/discord-bot.js +517 -488
  49. package/dist/discord-command-registration.js +25 -10
  50. package/dist/discord-command-registration.test.js +17 -0
  51. package/dist/discord-utils.js +97 -5
  52. package/dist/discord-utils.test.js +151 -1
  53. package/dist/errors.js +10 -0
  54. package/dist/exec-async.js +17 -5
  55. package/dist/external-directory-permissions.e2e.test.js +11 -7
  56. package/dist/external-directory-project-deny.e2e.test.js +5 -3
  57. package/dist/external-opencode-sync.js +94 -17
  58. package/dist/external-opencode-sync.test.js +206 -0
  59. package/dist/extract-sdk-error-message.test.js +13 -0
  60. package/dist/file-edit-log.js +197 -0
  61. package/dist/file-edit-log.test.js +258 -0
  62. package/dist/format-tables.js +52 -0
  63. package/dist/format-tables.test.js +93 -0
  64. package/dist/gateway-proxy-reconnect.e2e.test.js +28 -8
  65. package/dist/gateway-proxy.e2e.test.js +18 -18
  66. package/dist/git-worktree-core.js +182 -17
  67. package/dist/interaction-handler.js +416 -398
  68. package/dist/ipc-tools-plugin.js +68 -3
  69. package/dist/kimaki-opencode-plugin-loading.e2e.test.js +122 -58
  70. package/dist/kimaki-opencode-plugin.js +9 -3
  71. package/dist/kimaki-workspace-adaptor.js +33 -10
  72. package/dist/logger.js +11 -1
  73. package/dist/markdown.js +3 -1
  74. package/dist/markdown.test.js +11 -3
  75. package/dist/message-formatting.js +181 -51
  76. package/dist/message-formatting.test.js +529 -22
  77. package/dist/message-preprocessing.js +105 -1
  78. package/dist/oauth-rotation-shared.js +10 -0
  79. package/dist/openai-auth-plugin.js +1 -0
  80. package/dist/openai-auth-state.js +1 -0
  81. package/dist/opencode-command.js +28 -0
  82. package/dist/opencode-command.test.js +109 -1
  83. package/dist/opencode-interrupt-plugin.js +4 -0
  84. package/dist/opencode-interrupt-plugin.test.js +24 -0
  85. package/dist/opencode.js +82 -15
  86. package/dist/project-add.test.js +62 -0
  87. package/dist/question-abort-clears-dropdown.e2e.test.js +89 -0
  88. package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +150 -0
  89. package/dist/question-text-before-dropdown.e2e.test.js +52 -0
  90. package/dist/queue-advanced-abort.e2e.test.js +8 -10
  91. package/dist/queue-advanced-action-buttons.e2e.test.js +8 -8
  92. package/dist/queue-advanced-e2e-setup.js +78 -5
  93. package/dist/queue-advanced-footer.e2e.test.js +60 -269
  94. package/dist/queue-advanced-model-switch.e2e.test.js +9 -11
  95. package/dist/queue-advanced-permissions-typing.e2e.test.js +15 -15
  96. package/dist/queue-advanced-question.e2e.test.js +32 -27
  97. package/dist/queue-advanced-typing-interrupt.e2e.test.js +10 -12
  98. package/dist/queue-advanced-typing.e2e.test.js +7 -9
  99. package/dist/queue-delete-message.e2e.test.js +3 -3
  100. package/dist/queue-drain-after-interactive-ui.e2e.test.js +5 -5
  101. package/dist/queue-interrupt-drain.e2e.test.js +122 -12
  102. package/dist/queue-question-select-drain.e2e.test.js +34 -132
  103. package/dist/runtime-lifecycle.e2e.test.js +21 -51
  104. package/dist/schema.js +35 -1
  105. package/dist/session-handler/event-stream-state.js +391 -50
  106. package/dist/session-handler/event-stream-state.test.js +736 -8
  107. package/dist/session-handler/model-utils.js +172 -5
  108. package/dist/session-handler/model-utils.test.js +445 -0
  109. package/dist/session-handler/opencode-session-event-log.js +30 -1
  110. package/dist/session-handler/thread-ingress.test.js +41 -0
  111. package/dist/session-handler/thread-runtime-state.js +2 -0
  112. package/dist/session-handler/thread-session-runtime-summary.test.js +121 -0
  113. package/dist/session-handler/thread-session-runtime.js +574 -299
  114. package/dist/session-search.js +123 -4
  115. package/dist/session-search.test.js +248 -1
  116. package/dist/session-sleep.test.js +224 -0
  117. package/dist/session-title-rename.test.js +37 -39
  118. package/dist/sleep.e2e.test.js +494 -0
  119. package/dist/startup-time.e2e.test.js +2 -1
  120. package/dist/store.js +3 -0
  121. package/dist/system-message.js +102 -16
  122. package/dist/system-message.test.js +192 -15
  123. package/dist/task-runner.js +96 -5
  124. package/dist/task-schedule.js +87 -1
  125. package/dist/task-schedule.test.js +149 -1
  126. package/dist/test-utils.js +17 -9
  127. package/dist/thinking-utils.js +10 -0
  128. package/dist/thinking-utils.test.js +35 -0
  129. package/dist/thread-message-queue.e2e.test.js +65 -213
  130. package/dist/undo-redo.e2e.test.js +17 -9
  131. package/dist/voice-attachment.js +23 -1
  132. package/dist/voice-handler.js +21 -4
  133. package/dist/voice-message.e2e.test.js +144 -30
  134. package/dist/voice-routing-input.e2e.test.js +195 -0
  135. package/dist/voice-routing-question.e2e.test.js +125 -0
  136. package/dist/voice.js +28 -8
  137. package/dist/voice.test.js +96 -30
  138. package/dist/worktree-clone-isolation.e2e.test.js +188 -0
  139. package/dist/worktree-lifecycle.e2e.test.js +55 -36
  140. package/dist/worktrees.js +226 -443
  141. package/dist/worktrees.test.js +435 -58
  142. package/dist/xai-auth-plugin.js +1 -0
  143. package/dist/xai-auth-state.js +1 -0
  144. package/package.json +8 -6
  145. package/skills/bump-holocron-consumers/SKILL.md +137 -0
  146. package/skills/critique/SKILL.md +4 -4
  147. package/skills/errore/SKILL.md +60 -218
  148. package/skills/goke/SKILL.md +150 -10
  149. package/skills/holocron/SKILL.md +154 -17
  150. package/skills/npm-package/SKILL.md +55 -35
  151. package/skills/profano/SKILL.md +17 -2
  152. package/skills/spiceflow/SKILL.md +39 -3
  153. package/skills/strada/SKILL.md +39 -0
  154. package/skills/zele/SKILL.md +19 -0
  155. package/src/agent-model.e2e.test.ts +287 -186
  156. package/src/anthropic-account-identity.ts +20 -1
  157. package/src/anthropic-auth-plugin.test.ts +19 -0
  158. package/src/anthropic-auth-plugin.ts +12 -16
  159. package/src/anthropic-auth-state.ts +1 -0
  160. package/src/bash-tool-schema-plugin.test.ts +55 -0
  161. package/src/bash-tool-schema-plugin.ts +46 -0
  162. package/src/bin.ts +78 -26
  163. package/src/cli-commands/maintenance.ts +59 -13
  164. package/src/cli-commands/project.ts +14 -35
  165. package/src/cli-commands/send.ts +31 -1
  166. package/src/cli-commands/session.ts +374 -114
  167. package/src/cli-commands/task.ts +34 -15
  168. package/src/cli-parsing.test.ts +103 -4
  169. package/src/cli-runner.test.ts +49 -0
  170. package/src/cli-runner.ts +44 -7
  171. package/src/cli-send-thread.e2e.test.ts +9 -11
  172. package/src/cli.ts +53 -0
  173. package/src/commands/abort.ts +6 -1
  174. package/src/commands/action-buttons.ts +2 -1
  175. package/src/commands/add-project.ts +4 -8
  176. package/src/commands/agent.ts +205 -29
  177. package/src/commands/ask-question.test.ts +48 -1
  178. package/src/commands/ask-question.ts +23 -33
  179. package/src/commands/btw.ts +40 -5
  180. package/src/commands/file-upload.ts +2 -1
  181. package/src/commands/fork-subagent.ts +2 -3
  182. package/src/commands/fork.ts +4 -3
  183. package/src/commands/last-sessions.ts +10 -4
  184. package/src/commands/login.ts +6 -0
  185. package/src/commands/merge-worktree.ts +54 -16
  186. package/src/commands/model-variant.ts +9 -1
  187. package/src/commands/model.ts +20 -9
  188. package/src/commands/multioauth.ts +1 -0
  189. package/src/commands/new-worktree.ts +138 -55
  190. package/src/commands/queue.ts +3 -2
  191. package/src/commands/resume.ts +4 -3
  192. package/src/commands/session.ts +94 -70
  193. package/src/commands/user-command.ts +6 -6
  194. package/src/commands/worktrees.ts +5 -12
  195. package/src/config.ts +18 -1
  196. package/src/cpu-profiler.test.ts +183 -0
  197. package/src/cpu-profiler.ts +293 -0
  198. package/src/database.ts +235 -3
  199. package/src/db.test.ts +293 -0
  200. package/src/db.ts +38 -0
  201. package/src/default-channel-provisioning.test.ts +155 -0
  202. package/src/discord-bot.ts +78 -36
  203. package/src/discord-command-registration.test.ts +20 -0
  204. package/src/discord-command-registration.ts +42 -12
  205. package/src/discord-utils.test.ts +176 -0
  206. package/src/discord-utils.ts +152 -7
  207. package/src/errors.ts +12 -0
  208. package/src/exec-async.ts +20 -11
  209. package/src/external-directory-permissions.e2e.test.ts +11 -7
  210. package/src/external-directory-project-deny.e2e.test.ts +5 -3
  211. package/src/external-opencode-sync.test.ts +248 -0
  212. package/src/external-opencode-sync.ts +122 -19
  213. package/src/extract-sdk-error-message.test.ts +18 -0
  214. package/src/file-edit-log.test.ts +287 -0
  215. package/src/file-edit-log.ts +283 -0
  216. package/src/format-tables.test.ts +101 -0
  217. package/src/format-tables.ts +75 -0
  218. package/src/gateway-proxy-reconnect.e2e.test.ts +38 -8
  219. package/src/gateway-proxy.e2e.test.ts +18 -17
  220. package/src/git-worktree-core.ts +247 -20
  221. package/src/interaction-handler.ts +36 -13
  222. package/src/ipc-tools-plugin.ts +82 -5
  223. package/src/kimaki-opencode-plugin-loading.e2e.test.ts +188 -97
  224. package/src/kimaki-opencode-plugin.ts +9 -3
  225. package/src/kimaki-workspace-adaptor.ts +40 -11
  226. package/src/logger.ts +14 -4
  227. package/src/markdown.test.ts +13 -3
  228. package/src/markdown.ts +4 -1
  229. package/src/message-formatting.test.ts +672 -25
  230. package/src/message-formatting.ts +242 -53
  231. package/src/message-preprocessing.ts +116 -1
  232. package/src/oauth-rotation-shared.ts +10 -0
  233. package/src/openai-auth-plugin.ts +1 -0
  234. package/src/openai-auth-state.ts +1 -0
  235. package/src/opencode-command.test.ts +140 -0
  236. package/src/opencode-command.ts +38 -0
  237. package/src/opencode-interrupt-plugin.test.ts +32 -0
  238. package/src/opencode-interrupt-plugin.ts +3 -0
  239. package/src/opencode.ts +115 -15
  240. package/src/project-add.test.ts +80 -0
  241. package/src/question-abort-clears-dropdown.e2e.test.ts +109 -0
  242. package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +172 -0
  243. package/src/question-text-before-dropdown.e2e.test.ts +67 -0
  244. package/src/queue-advanced-abort.e2e.test.ts +8 -9
  245. package/src/queue-advanced-action-buttons.e2e.test.ts +8 -8
  246. package/src/queue-advanced-e2e-setup.ts +83 -4
  247. package/src/queue-advanced-footer.e2e.test.ts +67 -315
  248. package/src/queue-advanced-model-switch.e2e.test.ts +9 -10
  249. package/src/queue-advanced-permissions-typing.e2e.test.ts +15 -15
  250. package/src/queue-advanced-question.e2e.test.ts +36 -30
  251. package/src/queue-advanced-typing-interrupt.e2e.test.ts +11 -11
  252. package/src/queue-advanced-typing.e2e.test.ts +8 -8
  253. package/src/queue-delete-message.e2e.test.ts +3 -3
  254. package/src/queue-drain-after-interactive-ui.e2e.test.ts +5 -5
  255. package/src/queue-interrupt-drain.e2e.test.ts +133 -12
  256. package/src/queue-question-select-drain.e2e.test.ts +36 -161
  257. package/src/runtime-lifecycle.e2e.test.ts +21 -54
  258. package/src/schema.sql +15 -2
  259. package/src/schema.ts +37 -1
  260. package/src/session-handler/event-stream-state.test.ts +790 -7
  261. package/src/session-handler/event-stream-state.ts +568 -53
  262. package/src/session-handler/model-utils.test.ts +537 -0
  263. package/src/session-handler/model-utils.ts +281 -5
  264. package/src/session-handler/opencode-session-event-log.ts +31 -1
  265. package/src/session-handler/thread-ingress.test.ts +54 -0
  266. package/src/session-handler/thread-runtime-state.ts +3 -0
  267. package/src/session-handler/thread-session-runtime-summary.test.ts +169 -0
  268. package/src/session-handler/thread-session-runtime.ts +712 -358
  269. package/src/session-search.test.ts +281 -0
  270. package/src/session-search.ts +241 -7
  271. package/src/session-sleep.test.ts +294 -0
  272. package/src/session-title-rename.test.ts +54 -48
  273. package/src/sleep.e2e.test.ts +565 -0
  274. package/src/startup-time.e2e.test.ts +5 -1
  275. package/src/store.ts +22 -0
  276. package/src/system-message.test.ts +226 -15
  277. package/src/system-message.ts +130 -16
  278. package/src/task-runner.ts +138 -3
  279. package/src/task-schedule.test.ts +170 -0
  280. package/src/task-schedule.ts +141 -1
  281. package/src/test-utils.ts +21 -10
  282. package/src/thinking-utils.test.ts +46 -0
  283. package/src/thinking-utils.ts +21 -0
  284. package/src/thread-message-queue.e2e.test.ts +65 -245
  285. package/src/undo-redo.e2e.test.ts +17 -9
  286. package/src/voice-attachment.ts +29 -1
  287. package/src/voice-handler.ts +23 -4
  288. package/src/voice-message.e2e.test.ts +149 -29
  289. package/src/voice-routing-input.e2e.test.ts +203 -0
  290. package/src/voice-routing-question.e2e.test.ts +131 -0
  291. package/src/voice.test.ts +107 -29
  292. package/src/voice.ts +34 -6
  293. package/src/worktree-clone-isolation.e2e.test.ts +213 -0
  294. package/src/worktree-lifecycle.e2e.test.ts +55 -35
  295. package/src/worktrees.test.ts +505 -64
  296. package/src/worktrees.ts +257 -603
  297. package/src/xai-auth-plugin.ts +1 -0
  298. package/src/xai-auth-state.ts +1 -0
  299. package/dist/message-finish-field.e2e.test.js +0 -169
  300. package/dist/subagent-rate-limit-plugin.js +0 -184
  301. package/src/message-finish-field.e2e.test.ts +0 -196
  302. package/src/subagent-rate-limit-plugin.ts +0 -228
@@ -1,9 +1,10 @@
1
- // OpenCode plugin that provides IPC-based tools for Discord interaction:
1
+ // OpenCode plugin that provides Discord and session tools:
2
2
  // - kimaki_file_upload: prompts the Discord user to upload files via native picker
3
3
  // - kimaki_action_buttons: shows clickable action buttons in the Discord thread
4
+ // - kimaki_sleep: persist a wake time so the session can sleep hours or days
4
5
  //
5
- // Tools communicate with the bot process via IPC rows in SQLite (the plugin
6
- // runs inside the OpenCode server process, not the bot process).
6
+ // IPC tools talk to the bot process via SQLite rows. Sleep writes session_sleeps
7
+ // directly. The plugin runs inside the OpenCode server process, not the bot.
7
8
  //
8
9
  // Exported from kimaki-opencode-plugin.ts — each export is treated as a separate
9
10
  // plugin by OpenCode's plugin loader.
@@ -180,6 +181,70 @@ const ipcToolsPlugin = async () => {
180
181
  return 'Action button request timed out';
181
182
  },
182
183
  }),
184
+ kimaki_sleep: tool({
185
+ description: dedent `
186
+ Sleep this session until a future time. Kimaki later posts a wake
187
+ message in this thread and the same session continues.
188
+ Use this to wait hours or days for CI, a deploy, a date, or any later event.
189
+ The sleep is stored in SQLite and survives bot restarts.
190
+
191
+ Pass either duration (2h, 30m, 1d) or until (UTC ISO ending with Z).
192
+ Do not pass both. You MUST call kimaki_sleep LAST, after ALL text.
193
+ Do not call more tools after it.
194
+
195
+ The tool result is not a wake. After it succeeds, write one short
196
+ waiting line and stop. Do not continue the waited work until a later
197
+ Discord message that starts with "Woke after sleeping until".
198
+ A new user message cancels the sleep. If you still need that later
199
+ wake after answering, call kimaki_sleep again with until set to the
200
+ original UTC time.
201
+ `,
202
+ args: {
203
+ duration: z
204
+ .string()
205
+ .optional()
206
+ .describe('Relative wait. Examples: 30s, 2h, 1d. Do not pass with until.'),
207
+ until: z
208
+ .string()
209
+ .optional()
210
+ .describe('UTC ISO date ending with Z. Example: 2026-08-20T09:00:00Z.'),
211
+ reason: z
212
+ .string()
213
+ .optional()
214
+ .describe('Why the session is sleeping. Shown when it wakes.'),
215
+ },
216
+ async execute({ duration, until, reason }, context) {
217
+ const { parseSleepWakeAt, formatSessionSleepWakeAt, formatSessionSleepToolOutput, } = await import('./task-schedule.js');
218
+ const wakeAt = parseSleepWakeAt({
219
+ duration,
220
+ until,
221
+ now: new Date(),
222
+ });
223
+ if (wakeAt instanceof Error) {
224
+ return wakeAt.message;
225
+ }
226
+ const { getThreadIdBySessionId, upsertSessionSleep } = await loadDatabaseModule();
227
+ // Only a check that a thread owns this session: subagents have none.
228
+ // The wake resolves the thread again at delivery time, so nothing
229
+ // here can go stale if /resume rebinds the session later.
230
+ const threadId = await getThreadIdBySessionId(context.sessionID);
231
+ if (!threadId) {
232
+ return 'sleep is only available in the main session';
233
+ }
234
+ await upsertSessionSleep({
235
+ sessionId: context.sessionID,
236
+ wakeAt,
237
+ reason,
238
+ });
239
+ // The title carries the resolved absolute time so Discord shows the
240
+ // real wake instant instead of a relative input like "2h".
241
+ const untilLabel = formatSessionSleepWakeAt(wakeAt);
242
+ return {
243
+ title: `until ${untilLabel}`,
244
+ output: formatSessionSleepToolOutput({ wakeAt, reason }),
245
+ };
246
+ },
247
+ }),
183
248
  },
184
249
  };
185
250
  };
@@ -1,56 +1,64 @@
1
1
  // E2e test for OpenCode plugin loading.
2
- // Spawns `opencode serve` directly with our plugin in OPENCODE_CONFIG_CONTENT,
3
- // waits for the health endpoint, then checks stderr for plugin errors.
4
- // No Discord infrastructure needed — just the OpenCode server process.
2
+ // Spawns one `opencode serve` with the same plugin list kimaki writes (its own
3
+ // plugin plus @subrouter/opencode), then checks stderr for load errors and
4
+ // hits the real HTTP API. One server for both plugins, because that is how
5
+ // they actually run, and because booting two costs seconds per file.
6
+ // No Discord infrastructure and no real provider APIs.
5
7
  import { spawn } from 'node:child_process';
6
8
  import fs from 'node:fs';
7
9
  import path from 'node:path';
8
- import { fileURLToPath } from 'node:url';
9
- import { test, expect } from 'vitest';
10
+ import { createRequire } from 'node:module';
11
+ import { pathToFileURL } from 'node:url';
12
+ import { afterAll, beforeAll, expect, test } from 'vitest';
13
+ import { PROVIDER_IDS } from '@subrouter/cli';
10
14
  import { resolveOpencodeCommand } from './opencode.js';
11
15
  import { getSpawnCommandAndArgs } from './opencode-command.js';
12
16
  import { chooseLockPort } from './test-utils.js';
13
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
- async function waitForHealth({ port, maxAttempts = 30, }) {
15
- for (let i = 0; i < maxAttempts; i++) {
16
- try {
17
- const response = await fetch(`http://127.0.0.1:${port}/api/health`);
18
- if (response.status < 500) {
19
- return true;
20
- }
21
- }
22
- catch {
23
- // connection refused, retry
24
- }
17
+ const require = createRequire(import.meta.url);
18
+ const port = chooseLockPort({ key: 'opencode-plugin-loading-e2e' });
19
+ const projectDir = path.resolve(process.cwd(), 'tmp', 'plugin-loading-e2e');
20
+ const subrouterHome = path.join(projectDir, 'subrouter-home');
21
+ const stderrLines = [];
22
+ let serverProcess;
23
+ async function api(routePath) {
24
+ const url = new URL(routePath, `http://127.0.0.1:${port}`);
25
+ url.searchParams.set('directory', projectDir);
26
+ const response = await fetch(url, { headers: { 'x-opencode-directory': projectDir } });
27
+ if (!response.ok)
28
+ throw new Error(`${routePath} returned ${response.status}`);
29
+ return (await response.json());
30
+ }
31
+ async function waitForHealth() {
32
+ for (let attempt = 0; attempt < 60; attempt++) {
33
+ const ok = await fetch(`http://127.0.0.1:${port}/api/health`)
34
+ .then((r) => r.status < 500)
35
+ .catch(() => false);
36
+ if (ok)
37
+ return true;
25
38
  await new Promise((resolve) => {
26
- setTimeout(resolve, 1000);
39
+ setTimeout(resolve, 500);
27
40
  });
28
41
  }
29
42
  return false;
30
43
  }
31
- test('opencode server loads plugin without errors', async () => {
32
- const projectDir = path.resolve(process.cwd(), 'tmp', 'plugin-loading-e2e');
33
- fs.mkdirSync(projectDir, { recursive: true });
34
- const port = chooseLockPort({ key: 'opencode-plugin-loading-e2e' });
35
- const pluginPath = new URL('../src/kimaki-opencode-plugin.ts', import.meta.url).href;
36
- const stderrLines = [];
37
- const isolatedOpencodeRoot = path.join(projectDir, 'opencode-test-home');
44
+ beforeAll(async () => {
45
+ fs.rmSync(projectDir, { recursive: true, force: true });
46
+ const opencodeRoot = path.join(projectDir, 'opencode-test-home');
38
47
  const xdgDirectories = {
39
- OPENCODE_CONFIG_DIR: path.join(isolatedOpencodeRoot, '.opencode-kimaki'),
40
- XDG_CONFIG_HOME: path.join(isolatedOpencodeRoot, '.config'),
41
- XDG_DATA_HOME: path.join(isolatedOpencodeRoot, '.local', 'share'),
42
- XDG_CACHE_HOME: path.join(isolatedOpencodeRoot, '.cache'),
43
- XDG_STATE_HOME: path.join(isolatedOpencodeRoot, '.local', 'state'),
48
+ OPENCODE_CONFIG_DIR: path.join(opencodeRoot, '.opencode-kimaki'),
49
+ XDG_CONFIG_HOME: path.join(opencodeRoot, '.config'),
50
+ XDG_DATA_HOME: path.join(opencodeRoot, '.local', 'share'),
51
+ XDG_CACHE_HOME: path.join(opencodeRoot, '.cache'),
52
+ XDG_STATE_HOME: path.join(opencodeRoot, '.local', 'state'),
44
53
  };
45
- fs.mkdirSync(isolatedOpencodeRoot, { recursive: true });
46
- Object.values(xdgDirectories).forEach((directory) => {
54
+ [...Object.values(xdgDirectories), subrouterHome].forEach((directory) => {
47
55
  fs.mkdirSync(directory, { recursive: true });
48
56
  });
49
- const { command, args, windowsVerbatimArguments, } = getSpawnCommandAndArgs({
57
+ const { command, args, windowsVerbatimArguments } = getSpawnCommandAndArgs({
50
58
  resolvedCommand: resolveOpencodeCommand(),
51
59
  baseArgs: ['serve', '--port', port.toString(), '--print-logs', '--log-level', 'DEBUG'],
52
60
  });
53
- const serverProcess = spawn(command, args, {
61
+ serverProcess = spawn(command, args, {
54
62
  stdio: 'pipe',
55
63
  cwd: projectDir,
56
64
  windowsVerbatimArguments,
@@ -60,35 +68,91 @@ test('opencode server loads plugin without errors', async () => {
60
68
  $schema: 'https://opencode.ai/config.json',
61
69
  lsp: false,
62
70
  formatter: false,
63
- plugin: [pluginPath],
71
+ plugin: [
72
+ new URL('../src/kimaki-opencode-plugin.ts', import.meta.url).href,
73
+ pathToFileURL(require.resolve('@subrouter/opencode')).href,
74
+ ],
64
75
  }),
65
- OPENCODE_TEST_HOME: isolatedOpencodeRoot,
76
+ OPENCODE_TEST_HOME: opencodeRoot,
77
+ SUBROUTER_HOME: subrouterHome,
78
+ SUBROUTER_MANUAL_OAUTH: '1',
66
79
  ...xdgDirectories,
67
80
  },
68
81
  });
69
82
  serverProcess.stderr?.on('data', (data) => {
70
83
  stderrLines.push(...data.toString().split('\n').filter(Boolean));
71
84
  });
72
- try {
73
- const healthy = await waitForHealth({ port });
74
- expect(healthy).toBe(true);
75
- // Check no plugin-related errors in stderr
76
- const pluginErrorPatterns = [
77
- /plugin.*error/i,
78
- /failed to load plugin/i,
79
- /cannot find module/i,
80
- /ERR_MODULE_NOT_FOUND/i,
81
- /plugin.*failed/i,
82
- /plugin.*crash/i,
83
- ];
84
- const errorLines = stderrLines.filter((line) => {
85
- return pluginErrorPatterns.some((pattern) => {
86
- return pattern.test(line);
87
- });
85
+ expect(await waitForHealth()).toBe(true);
86
+ }, 120_000);
87
+ afterAll(() => {
88
+ serverProcess?.kill('SIGTERM');
89
+ });
90
+ test('server loads both plugins without errors', () => {
91
+ const pluginErrorPatterns = [
92
+ /plugin.*error/i,
93
+ /failed to load plugin/i,
94
+ /cannot find module/i,
95
+ /ERR_MODULE_NOT_FOUND/i,
96
+ /plugin.*failed/i,
97
+ /plugin.*crash/i,
98
+ ];
99
+ const errorLines = stderrLines.filter((line) => {
100
+ return pluginErrorPatterns.some((pattern) => {
101
+ return pattern.test(line);
88
102
  });
89
- expect(errorLines).toEqual([]);
90
- }
91
- finally {
92
- serverProcess.kill('SIGTERM');
93
- }
94
- }, 60_000);
103
+ });
104
+ expect(errorLines).toEqual([]);
105
+ });
106
+ test('subrouter is a connected provider so /model accepts subrouter/default', async () => {
107
+ const providers = await api('/provider');
108
+ expect(providers.connected).toContain('subrouter');
109
+ const subrouter = providers.all.find((entry) => {
110
+ return entry.id === 'subrouter';
111
+ });
112
+ expect(subrouter?.name).toBe('subrouter.org');
113
+ expect(subrouter?.models.default).toMatchObject({
114
+ capabilities: {
115
+ attachment: false,
116
+ input: { image: false, pdf: false },
117
+ },
118
+ });
119
+ });
120
+ test('login offers subrouter and asks which subscription to add first', async () => {
121
+ const methods = await api('/provider/auth');
122
+ expect(methods.subrouter).toHaveLength(1);
123
+ expect(methods.subrouter?.[0]).toMatchObject({ type: 'oauth', label: 'Add a subscription' });
124
+ // Derived from subrouter, not hardcoded: it adds providers over time and a
125
+ // literal list here just breaks on the next one.
126
+ const prompt = methods.subrouter?.[0]?.prompts?.[0];
127
+ expect(prompt).toMatchObject({ type: 'select', key: 'provider' });
128
+ expect(prompt?.options?.map((option) => option.value)).toEqual([...PROVIDER_IDS]);
129
+ });
130
+ test('authorize and callback add an opencode-go subscription', async () => {
131
+ const url = new URL('/provider/subrouter/oauth/authorize', `http://127.0.0.1:${port}`);
132
+ url.searchParams.set('directory', projectDir);
133
+ const response = await fetch(url, {
134
+ method: 'POST',
135
+ headers: { 'Content-Type': 'application/json', 'x-opencode-directory': projectDir },
136
+ body: JSON.stringify({ method: 0, inputs: { provider: 'opencode-go' } }),
137
+ });
138
+ expect(response.ok).toBe(true);
139
+ const authorization = (await response.json());
140
+ expect(authorization.method).toBe('code');
141
+ expect(authorization.url).toBe('https://opencode.ai/auth');
142
+ expect(authorization.instructions).toContain('API key');
143
+ const callbackUrl = new URL('/provider/subrouter/oauth/callback', `http://127.0.0.1:${port}`);
144
+ callbackUrl.searchParams.set('directory', projectDir);
145
+ const callbackResponse = await fetch(callbackUrl, {
146
+ method: 'POST',
147
+ headers: {
148
+ 'Content-Type': 'application/json',
149
+ 'x-opencode-directory': projectDir,
150
+ },
151
+ body: JSON.stringify({ method: 0, code: 'zen-key-1' }),
152
+ });
153
+ expect(callbackResponse.ok).toBe(true);
154
+ const auth = JSON.parse(fs.readFileSync(path.join(subrouterHome, 'auth.json'), 'utf8'));
155
+ expect(auth.providers['opencode-go']?.accounts).toMatchObject([
156
+ { type: 'api', key: 'zen-key-1' },
157
+ ]);
158
+ });
@@ -4,22 +4,28 @@
4
4
  // initializer functions. OpenCode calls every export as a plugin.
5
5
  //
6
6
  // Plugins are split into focused modules:
7
- // - ipc-tools-plugin: file upload + action buttons (IPC-based Discord tools)
7
+ // - ipc-tools-plugin: file upload, action buttons, and session sleep
8
8
  // - context-awareness-plugin: branch, pwd, memory reminder, onboarding tutorial
9
9
  // - memory-overview-plugin: frozen MEMORY.md heading overview per session
10
10
  // - opencode-interrupt-plugin: interrupt queued messages at step boundaries
11
- // - subagent-rate-limit-plugin: aborts only task subagents after rate limits
12
11
  // - kitty-graphics-plugin: extract Kitty Graphics Protocol images from bash output
12
+ // - file-edit-log: record edit/write/apply_patch files per session
13
+ // - bash-tool-schema-plugin: add description and hasSideEffect to bash
13
14
  export { ipcToolsPlugin } from './ipc-tools-plugin.js';
14
15
  export { contextAwarenessPlugin } from './context-awareness-plugin.js';
15
16
  export { memoryOverviewPlugin } from './memory-overview-plugin.js';
16
17
  export { interruptOpencodeSessionOnUserMessage } from './opencode-interrupt-plugin.js';
18
+ // LEGACY per-provider account rotation. Superseded by @subrouter/opencode
19
+ // (registered separately in opencode.ts), which also fails over across
20
+ // providers. These stay because they own `anthropic/*` auth, which opencode
21
+ // itself does not provide. See anthropic-auth-plugin.ts for the full note.
17
22
  export { anthropicAuthPlugin } from './anthropic-auth-plugin.js';
18
23
  export { openaiRotationPlugin } from './openai-auth-plugin.js';
19
24
  export { xaiRotationPlugin } from './xai-auth-plugin.js';
20
25
  export { imageOptimizerPlugin } from './image-optimizer-plugin.js';
21
- export { subagentRateLimitPlugin } from './subagent-rate-limit-plugin.js';
22
26
  export { cacheDriftPlugin } from './cache-drift-plugin.js';
23
27
  export { kittyGraphicsPlugin } from 'kitty-graphics-agent';
24
28
  export { injectionGuardInternal as injectionGuard } from 'opencode-injection-guard';
25
29
  export { kimakiWorkspaceAdaptorPlugin } from './kimaki-workspace-adaptor.js';
30
+ export { fileEditTrackerPlugin } from './file-edit-log.js';
31
+ export { bashToolSchemaPlugin } from './bash-tool-schema-plugin.js';
@@ -7,7 +7,7 @@
7
7
  // Never use console.log/console.error — plugins must be silent.
8
8
  import crypto from 'node:crypto';
9
9
  import path from 'node:path';
10
- import { createWorktreeCore, removeWorktreeCore } from './git-worktree-core.js';
10
+ import { createWorktreeCore, KIMAKI_WORKTREE_ADAPTER_TYPE, removeWorktreeFromOwnRepository, } from './git-worktree-core.js';
11
11
  /**
12
12
  * Compute the on-disk directory for a managed worktree.
13
13
  * Mirrors getManagedWorktreeDirectory from worktrees.ts but reads KIMAKI_DATA_DIR
@@ -29,14 +29,36 @@ function computeWorktreeDirectory({ projectDirectory, branchName, }) {
29
29
  .replaceAll('/', '-');
30
30
  return path.join(dataDir, 'worktrees', projectHash, withoutPrefix);
31
31
  }
32
- function createKimakiWorktreeAdaptor(projectDirectory) {
32
+ function getWorktreeIdentity(info) {
33
+ if (!info.extra || typeof info.extra !== 'object') {
34
+ return new Error('Kimaki worktree identity is missing');
35
+ }
36
+ const identity = {};
37
+ Object.assign(identity, info.extra);
38
+ if (typeof identity.projectDirectory !== 'string' ||
39
+ !path.isAbsolute(identity.projectDirectory)) {
40
+ return new Error('Kimaki worktree project directory must be absolute');
41
+ }
42
+ if (typeof identity.baseCommit !== 'string' ||
43
+ !/^[0-9a-f]{40}$/i.test(identity.baseCommit)) {
44
+ return new Error('Kimaki worktree base commit must be a full commit SHA');
45
+ }
46
+ return {
47
+ projectDirectory: identity.projectDirectory,
48
+ baseCommit: identity.baseCommit,
49
+ };
50
+ }
51
+ function createKimakiWorktreeAdaptor() {
33
52
  return {
34
53
  name: 'Kimaki Worktree',
35
54
  description: 'Create a git worktree managed by Kimaki',
36
55
  configure(info) {
56
+ const identity = getWorktreeIdentity(info);
57
+ if (identity instanceof Error)
58
+ throw identity;
37
59
  const branchName = info.branch || info.name;
38
60
  const directory = computeWorktreeDirectory({
39
- projectDirectory,
61
+ projectDirectory: identity.projectDirectory,
40
62
  branchName,
41
63
  });
42
64
  if (directory instanceof Error) {
@@ -53,12 +75,14 @@ function createKimakiWorktreeAdaptor(projectDirectory) {
53
75
  if (!info.directory) {
54
76
  throw new Error('Workspace directory not set — configure() likely failed');
55
77
  }
56
- const baseBranch = info.extra?.baseBranch || undefined;
78
+ const identity = getWorktreeIdentity(info);
79
+ if (identity instanceof Error)
80
+ throw identity;
57
81
  const result = await createWorktreeCore({
58
- projectDirectory,
82
+ projectDirectory: identity.projectDirectory,
59
83
  targetDirectory: info.directory,
60
84
  branchName: info.branch || info.name,
61
- baseBranch,
85
+ baseCommit: identity.baseCommit,
62
86
  // Silent log — plugin must not write to stdout/stderr
63
87
  });
64
88
  if (result instanceof Error) {
@@ -68,8 +92,7 @@ function createKimakiWorktreeAdaptor(projectDirectory) {
68
92
  async remove(info) {
69
93
  if (!info.directory)
70
94
  return;
71
- const result = await removeWorktreeCore({
72
- projectDirectory,
95
+ const result = await removeWorktreeFromOwnRepository({
73
96
  worktreeDirectory: info.directory,
74
97
  branchName: info.branch || '',
75
98
  });
@@ -89,7 +112,7 @@ function createKimakiWorktreeAdaptor(projectDirectory) {
89
112
  * Plugin entrypoint — registers the kimaki-worktree adaptor.
90
113
  * Called by OpenCode's plugin loader.
91
114
  */
92
- export const kimakiWorkspaceAdaptorPlugin = async ({ directory, experimental_workspace, }) => {
93
- experimental_workspace.register('kimaki-worktree', createKimakiWorktreeAdaptor(directory));
115
+ export const kimakiWorkspaceAdaptorPlugin = async ({ experimental_workspace, }) => {
116
+ experimental_workspace.register(KIMAKI_WORKTREE_ADAPTER_TYPE, createKimakiWorktreeAdaptor());
94
117
  return {};
95
118
  };
package/dist/logger.js CHANGED
@@ -25,6 +25,7 @@ export const LogPrefix = {
25
25
  FORMATTING: 'FORMAT',
26
26
  GENAI: 'GENAI',
27
27
  HEAP: 'HEAP',
28
+ CPU: 'CPU',
28
29
  GENAI_WORKER: 'GENAI_W',
29
30
  INTERACTION: 'INTERACT',
30
31
  IPC: 'IPC',
@@ -95,7 +96,16 @@ function formatArg(arg) {
95
96
  }
96
97
  export function formatErrorWithStack(error) {
97
98
  if (error instanceof Error) {
98
- return sanitizeSensitiveText(error.stack ?? `${error.name}: ${error.message}`, { redactPaths: false });
99
+ const seen = new Set();
100
+ const parts = [];
101
+ let current = error;
102
+ while (current instanceof Error && !seen.has(current)) {
103
+ seen.add(current);
104
+ const formatted = sanitizeSensitiveText(current.stack ?? `${current.name}: ${current.message}`, { redactPaths: false });
105
+ parts.push(parts.length === 0 ? formatted : `Caused by: ${formatted}`);
106
+ current = current.cause;
107
+ }
108
+ return parts.join('\n');
99
109
  }
100
110
  if (typeof error === 'string') {
101
111
  return sanitizeSensitiveText(error, { redactPaths: false });
package/dist/markdown.js CHANGED
@@ -104,7 +104,9 @@ export class ShareMarkdown {
104
104
  }
105
105
  }
106
106
  else if (message.role === 'assistant') {
107
- lines.push(`### 🤖 Assistant (${message.modelID || 'unknown model'})`);
107
+ const modelId = [message.providerID, message.modelID].filter(Boolean).join('/') ||
108
+ 'unknown model';
109
+ lines.push(`### 🤖 Assistant (${modelId})`);
108
110
  lines.push('');
109
111
  // Filter and process parts
110
112
  const filteredParts = parts.filter((part) => {
@@ -102,6 +102,10 @@ beforeAll(async () => {
102
102
  await client.session.promptAsync({
103
103
  sessionID,
104
104
  directory: directories.projectDirectory,
105
+ model: {
106
+ providerID: 'deterministic-provider',
107
+ modelID: 'deterministic-v2',
108
+ },
105
109
  parts: [{ type: 'text', text: 'hello markdown test' }],
106
110
  });
107
111
  // Wait for assistant text parts to be fully written (not just message existence).
@@ -139,6 +143,10 @@ beforeAll(async () => {
139
143
  await client.session.promptAsync({
140
144
  sessionID: toolSessionID,
141
145
  directory: directories.projectDirectory,
146
+ model: {
147
+ providerID: 'deterministic-provider',
148
+ modelID: 'deterministic-v2',
149
+ },
142
150
  parts: [{ type: 'text', text: 'use a tool please' }],
143
151
  });
144
152
  // Wait for tool execution to complete
@@ -200,7 +208,7 @@ test('generate markdown with system info', async () => {
200
208
  expect(markdown).toContain('## Conversation');
201
209
  expect(markdown).toContain('### 👤 User');
202
210
  expect(markdown).toContain('hello markdown test');
203
- expect(markdown).toContain('### 🤖 Assistant');
211
+ expect(markdown).toContain('### 🤖 Assistant (deterministic-provider/deterministic-v2)');
204
212
  expect(markdown).toContain('Hello! This is a deterministic markdown test response.');
205
213
  expect(markdown).toContain('**Started using deterministic-provider/deterministic-v2**');
206
214
  const normalized = normalizeMarkdown(markdown);
@@ -220,7 +228,7 @@ test('generate markdown with system info', async () => {
220
228
  hello markdown test
221
229
 
222
230
 
223
- ### 🤖 Assistant (deterministic-v2)
231
+ ### 🤖 Assistant (deterministic-provider/deterministic-v2)
224
232
 
225
233
  **Started using deterministic-provider/deterministic-v2**
226
234
 
@@ -253,7 +261,7 @@ test('generate markdown without system info', async () => {
253
261
  hello markdown test
254
262
 
255
263
 
256
- ### 🤖 Assistant (deterministic-v2)
264
+ ### 🤖 Assistant (deterministic-provider/deterministic-v2)
257
265
 
258
266
  **Started using deterministic-provider/deterministic-v2**
259
267