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,4 +1,23 @@
1
- // Helpers for extracting and normalizing Anthropic OAuth account identity.
1
+ // LEGACY rotation, superseded by @subrouter/opencode. See oauth-rotation-shared.ts.
2
+ // Helpers for Anthropic OAuth account and Claude Code client identity.
3
+
4
+ const CLAUDE_CODE_USER_AGENT = 'claude-cli/2.1.257 (external, cli)'
5
+
6
+ export function applyClaudeCodeRequestIdentity({
7
+ headers,
8
+ accessToken,
9
+ }: {
10
+ headers: Headers
11
+ accessToken: string
12
+ }) {
13
+ headers.set('authorization', `Bearer ${accessToken}`)
14
+ headers.set(
15
+ 'user-agent',
16
+ process.env.OPENCODE_ANTHROPIC_USER_AGENT || CLAUDE_CODE_USER_AGENT,
17
+ )
18
+ headers.set('x-app', 'cli')
19
+ return headers
20
+ }
2
21
 
3
22
  export type AnthropicAccountIdentity = {
4
23
  email?: string
@@ -1,6 +1,7 @@
1
1
  // Tests Anthropic system prompt rewriting so project instructions survive OpenCode prompt layout changes.
2
2
 
3
3
  import { describe, expect, test } from 'vitest'
4
+ import { applyClaudeCodeRequestIdentity } from './anthropic-account-identity.js'
4
5
  import { replacer } from './anthropic-auth-plugin.js'
5
6
 
6
7
  async function transformSystem(systemText: string) {
@@ -18,6 +19,24 @@ async function transformSystem(systemText: string) {
18
19
  return output.system.join('\n')
19
20
  }
20
21
 
22
+ describe('Anthropic OAuth request identity', () => {
23
+ test('applies the current external Claude Code client headers', () => {
24
+ const headers = applyClaudeCodeRequestIdentity({
25
+ headers: new Headers({ Accept: 'application/json' }),
26
+ accessToken: 'access-token',
27
+ })
28
+
29
+ expect(Object.fromEntries(headers.entries())).toMatchInlineSnapshot(`
30
+ {
31
+ "accept": "application/json",
32
+ "authorization": "Bearer access-token",
33
+ "user-agent": "claude-cli/2.1.257 (external, cli)",
34
+ "x-app": "cli",
35
+ }
36
+ `)
37
+ })
38
+ })
39
+
21
40
  describe('Anthropic system prompt rewriting', () => {
22
41
  test('preserves instructions when OpenCode places them before skills', async () => {
23
42
  const transformed = await transformSystem(`You are OpenCode, the best coding agent on the planet.
@@ -1,3 +1,4 @@
1
+ // LEGACY rotation, superseded by @subrouter/opencode. See oauth-rotation-shared.ts.
1
2
  /**
2
3
  * Anthropic OAuth authentication plugin for OpenCode.
3
4
  *
@@ -41,6 +42,7 @@ import {
41
42
  withAuthStateLock,
42
43
  } from "./anthropic-auth-state.js";
43
44
  import {
45
+ applyClaudeCodeRequestIdentity,
44
46
  extractAnthropicAccountIdentity,
45
47
  type AnthropicAccountIdentity,
46
48
  } from "./anthropic-account-identity.js";
@@ -90,7 +92,6 @@ const REDIRECT_URI = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
90
92
  const SCOPES =
91
93
  "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
92
94
  const OAUTH_TIMEOUT_MS = 5 * 60 * 1000;
93
- const CLAUDE_CODE_VERSION = "2.1.75";
94
95
  const CLAUDE_CODE_IDENTITY =
95
96
  "You are Claude Code, Anthropic's official CLI for Claude.";
96
97
 
@@ -344,14 +345,12 @@ async function fetchAnthropicAccountIdentity(accessToken: string) {
344
345
  for (const url of urls) {
345
346
  const responseText = await requestText(url, {
346
347
  method: "GET",
347
- headers: {
348
- Accept: "application/json",
349
- authorization: `Bearer ${accessToken}`,
350
- "user-agent":
351
- process.env.OPENCODE_ANTHROPIC_USER_AGENT ||
352
- `claude-cli/${CLAUDE_CODE_VERSION}`,
353
- "x-app": "cli",
354
- },
348
+ headers: Object.fromEntries(
349
+ applyClaudeCodeRequestIdentity({
350
+ headers: new Headers({ Accept: "application/json" }),
351
+ accessToken,
352
+ }).entries(),
353
+ ),
355
354
  }).catch(() => {
356
355
  return undefined;
357
356
  });
@@ -1087,13 +1086,10 @@ const AnthropicAuthPlugin: Plugin = async ({ serverUrl, directory }) => {
1087
1086
  "anthropic-dangerous-direct-browser-access",
1088
1087
  "true",
1089
1088
  );
1090
- requestHeaders.set("authorization", `Bearer ${auth.access}`);
1091
- requestHeaders.set(
1092
- "user-agent",
1093
- process.env.OPENCODE_ANTHROPIC_USER_AGENT ||
1094
- `claude-cli/${CLAUDE_CODE_VERSION}`,
1095
- );
1096
- requestHeaders.set("x-app", "cli");
1089
+ applyClaudeCodeRequestIdentity({
1090
+ headers: requestHeaders,
1091
+ accessToken: auth.access,
1092
+ });
1097
1093
  requestHeaders.delete("x-api-key");
1098
1094
 
1099
1095
  return fetch(input, {
@@ -1,3 +1,4 @@
1
+ // LEGACY rotation, superseded by @subrouter/opencode. See oauth-rotation-shared.ts.
1
2
  /**
2
3
  * Anthropic OAuth account store and rotation.
3
4
  * Uses shared utilities from oauth-rotation-shared.ts for file locking,
@@ -0,0 +1,55 @@
1
+ import { describe, expect, test } from 'vitest'
2
+ import {
3
+ extendBashToolDefinition,
4
+ type ToolDefinitionOutput,
5
+ } from './bash-tool-schema-plugin.js'
6
+
7
+ describe('extendBashToolDefinition', () => {
8
+ test('keeps parameters and assigns jsonSchema from bashParameters', () => {
9
+ const parameters = { keep: true }
10
+ const output: ToolDefinitionOutput = {
11
+ description: 'Execute a shell command',
12
+ parameters,
13
+ }
14
+
15
+ extendBashToolDefinition(output)
16
+
17
+ expect(output.parameters).toBe(parameters)
18
+ expect(output.jsonSchema).toMatchInlineSnapshot(`
19
+ {
20
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
21
+ "additionalProperties": false,
22
+ "properties": {
23
+ "command": {
24
+ "description": "The command to execute",
25
+ "type": "string",
26
+ },
27
+ "description": {
28
+ "description": "Short 5-10 word summary shown in Discord when the command is longer than 50 characters",
29
+ "type": "string",
30
+ },
31
+ "hasSideEffect": {
32
+ "description": "True if the command writes files, modifies state, installs packages, or triggers external effects",
33
+ "type": "boolean",
34
+ },
35
+ "timeout": {
36
+ "description": "Optional timeout in milliseconds",
37
+ "maximum": 9007199254740991,
38
+ "minimum": -9007199254740991,
39
+ "type": "integer",
40
+ },
41
+ "workdir": {
42
+ "description": "Working directory. Defaults to the current directory.",
43
+ "type": "string",
44
+ },
45
+ },
46
+ "required": [
47
+ "command",
48
+ "description",
49
+ "hasSideEffect",
50
+ ],
51
+ "type": "object",
52
+ }
53
+ `)
54
+ })
55
+ })
@@ -0,0 +1,46 @@
1
+ // Adds description and hasSideEffect to the built-in bash tool schema.
2
+ // Uses tool.definition so OpenCode keeps the real bash execute.
3
+ // Assign jsonSchema only. Leave parameters as the Effect Schema used to decode
4
+ // args before execute. Extra fields stay in the model-facing schema and in
5
+ // Discord input; they never reach the shell.
6
+
7
+ import type { Plugin } from '@opencode-ai/plugin'
8
+ import { z } from 'zod'
9
+
10
+ export const bashParameters = z.object({
11
+ command: z.string().describe('The command to execute'),
12
+ description: z
13
+ .string()
14
+ .describe(
15
+ 'Short 5-10 word summary shown in Discord when the command is longer than 50 characters',
16
+ ),
17
+ hasSideEffect: z
18
+ .boolean()
19
+ .describe(
20
+ 'True if the command writes files, modifies state, installs packages, or triggers external effects',
21
+ ),
22
+ workdir: z
23
+ .string()
24
+ .optional()
25
+ .describe('Working directory. Defaults to the current directory.'),
26
+ timeout: z.number().int().optional().describe('Optional timeout in milliseconds'),
27
+ })
28
+
29
+ export type ToolDefinitionOutput = {
30
+ description: string
31
+ parameters: unknown
32
+ jsonSchema?: unknown
33
+ }
34
+
35
+ export function extendBashToolDefinition(output: ToolDefinitionOutput) {
36
+ output.jsonSchema = z.toJSONSchema(bashParameters)
37
+ }
38
+
39
+ export const bashToolSchemaPlugin: Plugin = async () => {
40
+ return {
41
+ 'tool.definition': async (input, output) => {
42
+ if (input.toolID !== 'bash') return
43
+ extendBashToolDefinition(output)
44
+ },
45
+ }
46
+ }
package/src/bin.ts CHANGED
@@ -35,16 +35,61 @@ if (process.env.__KIMAKI_CHILD || isSubcommand || isHelpFlag) {
35
35
  console.error('no subcommand detected. kimaki will automatically restart on crash')
36
36
  console.error()
37
37
  const EXIT_NO_RESTART = 64
38
+ // Keep in sync with EXIT_TEMPFAIL in cli-runner.ts. Network-down login
39
+ // failures use this so a long outage cannot trip the crash-loop detector.
40
+ const EXIT_TEMPFAIL = 75
38
41
  const MAX_RAPID_RESTARTS = 5
39
42
  const RAPID_RESTART_WINDOW_MS = 60_000
40
43
  const RESTART_DELAY_MS = 2_000
41
44
 
45
+ const CHILD_EXIT_DEADLINE_MS = 15_000
42
46
  const restartTimestamps: number[] = []
47
+ let tempFailAttempts = 0
43
48
  let child: ReturnType<typeof spawn> | null = null
44
49
  // Track when we forwarded a termination signal so we don't restart after graceful shutdown
45
50
  let shutdownRequested = false
51
+ let forceKillTimer: ReturnType<typeof setTimeout> | null = null
52
+ let scheduledRestart: ReturnType<typeof setTimeout> | null = null
53
+
54
+ function clearForceKillTimer() {
55
+ if (!forceKillTimer) return
56
+ clearTimeout(forceKillTimer)
57
+ forceKillTimer = null
58
+ }
59
+
60
+ function clearScheduledRestart() {
61
+ if (!scheduledRestart) return
62
+ clearTimeout(scheduledRestart)
63
+ scheduledRestart = null
64
+ }
65
+
66
+ function killChild(signal: NodeJS.Signals) {
67
+ const target = child
68
+ if (!target) {
69
+ if (shutdownRequested) {
70
+ clearScheduledRestart()
71
+ process.exit(0)
72
+ }
73
+ return
74
+ }
75
+
76
+ const sent = target.kill(signal)
77
+ if (!sent || forceKillTimer) return
78
+
79
+ forceKillTimer = setTimeout(() => {
80
+ forceKillTimer = null
81
+ if (child !== target) return
82
+ console.error(
83
+ `[kimaki] Child did not exit within ${CHILD_EXIT_DEADLINE_MS / 1000}s, force-killing it`,
84
+ )
85
+ target.kill('SIGKILL')
86
+ }, CHILD_EXIT_DEADLINE_MS)
87
+ forceKillTimer.unref()
88
+ }
46
89
 
47
90
  function start() {
91
+ if (shutdownRequested) return
92
+ scheduledRestart = null
48
93
  if (!fs.existsSync(HEAP_SNAPSHOT_DIR)) {
49
94
  fs.mkdirSync(HEAP_SNAPSHOT_DIR, { recursive: true })
50
95
  }
@@ -53,7 +98,7 @@ if (process.env.__KIMAKI_CHILD || isSubcommand || isHelpFlag) {
53
98
  `--diagnostic-dir=${HEAP_SNAPSHOT_DIR}`,
54
99
  ]
55
100
  const args = [...heapArgs, ...process.execArgv, ...process.argv.slice(1)]
56
- child = spawn(
101
+ const currentChild = spawn(
57
102
  process.argv[0]!,
58
103
  args,
59
104
  {
@@ -61,41 +106,47 @@ if (process.env.__KIMAKI_CHILD || isSubcommand || isHelpFlag) {
61
106
  env: { ...process.env, __KIMAKI_CHILD: '1' },
62
107
  },
63
108
  )
109
+ child = currentChild
64
110
 
65
- child.on('exit', (code, signal) => {
111
+ currentChild.on('exit', (code, signal) => {
112
+ if (child === currentChild) child = null
113
+ clearForceKillTimer()
66
114
  if (code === 0 || code === EXIT_NO_RESTART || shutdownRequested) {
67
115
  process.exit(code ?? 0)
68
116
  return
69
117
  }
70
118
 
71
119
  const now = Date.now()
72
- restartTimestamps.push(now)
73
- while (
74
- restartTimestamps.length > 0 &&
75
- restartTimestamps[0]! < now - RAPID_RESTART_WINDOW_MS
76
- ) {
77
- restartTimestamps.shift()
78
- }
120
+ const isTempFail = code === EXIT_TEMPFAIL
121
+ if (!isTempFail) {
122
+ restartTimestamps.push(now)
123
+ while (
124
+ restartTimestamps.length > 0 &&
125
+ restartTimestamps[0]! < now - RAPID_RESTART_WINDOW_MS
126
+ ) {
127
+ restartTimestamps.shift()
128
+ }
79
129
 
80
- if (restartTimestamps.length > MAX_RAPID_RESTARTS) {
81
- console.error(
82
- `[kimaki] Crash loop detected (${MAX_RAPID_RESTARTS} crashes in ${RAPID_RESTART_WINDOW_MS / 1000}s), exiting`,
83
- )
84
- process.exit(1)
85
- return
130
+ if (restartTimestamps.length > MAX_RAPID_RESTARTS) {
131
+ console.error(
132
+ `[kimaki] Crash loop detected (${MAX_RAPID_RESTARTS} crashes in ${RAPID_RESTART_WINDOW_MS / 1000}s), exiting`,
133
+ )
134
+ process.exit(1)
135
+ return
136
+ }
86
137
  }
87
138
 
88
139
  const reason = signal ? `signal ${signal}` : `code ${code}`
89
140
  // Progressive backoff: 2s, 4s, 8s, 16s, capped at 30s.
90
141
  // Prevents hammering DNS/gateway during sustained network outages.
91
- const delay = Math.min(
92
- RESTART_DELAY_MS * 2 ** (restartTimestamps.length - 1),
93
- 30_000,
94
- )
142
+ const backoffStep = isTempFail
143
+ ? Math.min(tempFailAttempts++, 4)
144
+ : restartTimestamps.length - 1
145
+ const delay = Math.min(RESTART_DELAY_MS * 2 ** backoffStep, 30_000)
95
146
  console.error(
96
147
  `[kimaki] Process exited with ${reason}, restarting in ${(delay / 1000).toFixed(0)}s...`,
97
148
  )
98
- setTimeout(start, delay)
149
+ scheduledRestart = setTimeout(start, delay)
99
150
  })
100
151
  }
101
152
 
@@ -104,14 +155,15 @@ if (process.env.__KIMAKI_CHILD || isSubcommand || isHelpFlag) {
104
155
  for (const sig of ['SIGTERM', 'SIGINT'] as const) {
105
156
  process.on(sig, () => {
106
157
  shutdownRequested = true
107
- child?.kill(sig)
108
- })
109
- }
110
- for (const sig of ['SIGUSR1', 'SIGUSR2'] as const) {
111
- process.on(sig, () => {
112
- child?.kill(sig)
158
+ killChild(sig)
113
159
  })
114
160
  }
161
+ process.on('SIGUSR1', () => {
162
+ child?.kill('SIGUSR1')
163
+ })
164
+ process.on('SIGUSR2', () => {
165
+ killChild('SIGUSR2')
166
+ })
115
167
 
116
168
  start()
117
169
  }
@@ -87,8 +87,8 @@ cli
87
87
 
88
88
  cli
89
89
  .command(
90
- 'worktree merge',
91
- 'Merge worktree branch into default branch using worktrunk-style pipeline',
90
+ 'merge-worktree',
91
+ 'Merge the current worktree into a target branch',
92
92
  )
93
93
  .option('-d, --directory <path>', 'Worktree directory (defaults to cwd)')
94
94
  .option(
@@ -99,14 +99,30 @@ cli
99
99
  '-n, --name <name>',
100
100
  'Worktree/branch name (auto-detected from branch)',
101
101
  )
102
+ .option(
103
+ '--strategy [strategy]',
104
+ z.enum(['rebase', 'squash'])
105
+ .default('rebase')
106
+ .describe('Keep commits or squash them into one'),
107
+ )
108
+ .option(
109
+ '--target-branch [branch]',
110
+ z.string()
111
+ .optional()
112
+ .describe('Branch to merge into (defaults to origin/HEAD or main)'),
113
+ )
114
+ .option(
115
+ '--thread [threadId]',
116
+ z.string().optional().describe('Discord thread to mark as merged'),
117
+ )
118
+ .option(
119
+ '--thread-name [name]',
120
+ z.string().optional().describe('Discord thread name after merging'),
121
+ )
102
122
  .action(
103
- async (options: {
104
- directory?: string
105
- mainRepo?: string
106
- name?: string
107
- }) => {
123
+ async (options) => {
108
124
  try {
109
- const { mergeWorktree } = await import('../worktrees.js')
125
+ const { formatMergeWorktreeError, mergeWorktree } = await import('../worktrees.js')
110
126
  const worktreeDir = path.resolve(options.directory || '.')
111
127
 
112
128
  // Auto-detect main repo: find the main worktree's toplevel.
@@ -119,7 +135,10 @@ cli
119
135
  try {
120
136
  // `git worktree list --porcelain` first line is always the main worktree
121
137
  const { stdout } = await execAsync(
122
- `git -C "${worktreeDir}" worktree list --porcelain`,
138
+ {
139
+ command: 'git',
140
+ args: ['-C', worktreeDir, 'worktree', 'list', '--porcelain'],
141
+ },
123
142
  )
124
143
  const firstLine = stdout.split('\n')[0] || ''
125
144
  // Format: "worktree /path/to/main"
@@ -127,7 +146,10 @@ cli
127
146
  } catch {
128
147
  // Fallback: derive from git common dir
129
148
  const { stdout: commonDir } = await execAsync(
130
- `git -C "${worktreeDir}" rev-parse --git-common-dir`,
149
+ {
150
+ command: 'git',
151
+ args: ['-C', worktreeDir, 'rev-parse', '--git-common-dir'],
152
+ },
131
153
  )
132
154
  const resolved = path.isAbsolute(commonDir.trim())
133
155
  ? commonDir.trim()
@@ -141,7 +163,10 @@ cli
141
163
  if (!worktreeName) {
142
164
  try {
143
165
  const { stdout } = await execAsync(
144
- `git -C "${worktreeDir}" symbolic-ref --short HEAD`,
166
+ {
167
+ command: 'git',
168
+ args: ['-C', worktreeDir, 'symbolic-ref', '--short', 'HEAD'],
169
+ },
145
170
  )
146
171
  worktreeName = stdout.trim()
147
172
  } catch {
@@ -159,13 +184,15 @@ cli
159
184
  worktreeDir,
160
185
  mainRepoDir,
161
186
  worktreeName,
187
+ strategy: options.strategy,
188
+ targetBranch: options.targetBranch,
162
189
  onProgress: (msg) => {
163
190
  cliLogger.log(msg)
164
191
  },
165
192
  })
166
193
 
167
194
  if (result instanceof Error) {
168
- cliLogger.error(`Merge failed: ${result.message}`)
195
+ cliLogger.error(formatMergeWorktreeError(result, { maxLength: 8000 }))
169
196
  if (result instanceof RebaseConflictError) {
170
197
  cliLogger.log(
171
198
  'Resolve the rebase conflicts, then run this command again.',
@@ -174,9 +201,28 @@ cli
174
201
  process.exit(1)
175
202
  }
176
203
 
204
+ const action = options.strategy === 'squash' ? 'Squashed' : 'Merged'
177
205
  cliLogger.log(
178
- `Merged ${result.branchName} into ${result.defaultBranch} @ ${result.shortSha} (${result.commitCount} commit${result.commitCount === 1 ? '' : 's'})`,
206
+ `${action} ${result.branchName} into ${result.defaultBranch} @ ${result.shortSha} (${result.commitCount} source commit${result.commitCount === 1 ? '' : 's'})`,
179
207
  )
208
+ if (options.thread && options.threadName) {
209
+ const credentials = await resolveBotCredentials().catch((e) => {
210
+ return new Error('Failed to resolve Discord credentials', { cause: e })
211
+ })
212
+ if (credentials instanceof Error) {
213
+ cliLogger.warn(`Could not update the thread title: ${credentials.message}`)
214
+ } else {
215
+ const rest = createDiscordRest(credentials.token)
216
+ const renameResult = await rest
217
+ .patch(Routes.channel(options.thread), {
218
+ body: { name: options.threadName },
219
+ })
220
+ .catch((e) => new Error('Discord rejected the thread rename', { cause: e }))
221
+ if (renameResult instanceof Error) {
222
+ cliLogger.warn(`Could not update the thread title: ${renameResult.message}`)
223
+ }
224
+ }
225
+ }
180
226
  process.exit(0)
181
227
  } catch (error) {
182
228
  cliLogger.error(
@@ -13,7 +13,7 @@ import { spawn, execSync } from 'node:child_process'
13
13
  import { createLogger, LogPrefix, initLogFile } from '../logger.js'
14
14
  import { createDiscordClient, initDatabase, getChannelDirectory, initializeOpencodeForDirectory, createProjectChannels } from '../discord-bot.js'
15
15
  import { getDefaultKimakiDirectory } from '../channel-management.js'
16
- import { getBotTokenWithMode, getThreadSession, getThreadIdBySessionId, getSessionEventSnapshot, getDb, createScheduledTask, listScheduledTasks, cancelScheduledTask, getScheduledTask, updateScheduledTask, getSessionStartSourcesBySessionIds, deleteChannelDirectoryById, findChannelsByDirectory } from '../database.js'
16
+ import { getBotTokenWithMode, getThreadSession, getThreadIdBySessionId, getSessionEventSnapshot, getDb, createScheduledTask, listScheduledTasks, cancelScheduledTask, getScheduledTask, updateScheduledTask, getSessionStartSourcesBySessionIds, deleteChannelDirectoryById, findChannelsByDirectory, findRegisteredTextChannelForDirectory, formatProjectAlreadyRegisteredError } from '../database.js'
17
17
  import { ShareMarkdown } from '../markdown.js'
18
18
  import { parseSessionSearchPattern, findFirstSessionSearchHit, buildSessionSearchSnippet, getPartSearchTexts } from '../session-search.js'
19
19
  import { formatWorktreeName, formatAutoWorktreeName } from '../commands/new-worktree.js'
@@ -77,9 +77,19 @@ cli
77
77
  process.exit(EXIT_NO_RESTART)
78
78
  }
79
79
 
80
- // Initialize database
81
80
  await initDatabase()
82
81
 
82
+ const existingChannel = await findRegisteredTextChannelForDirectory(absolutePath)
83
+ if (existingChannel) {
84
+ cliLogger.error(
85
+ formatProjectAlreadyRegisteredError({
86
+ channelId: existingChannel.channel_id,
87
+ directory: absolutePath,
88
+ }),
89
+ )
90
+ process.exit(EXIT_NO_RESTART)
91
+ }
92
+
83
93
  const { token: botToken, appId } = await resolveBotCredentials({
84
94
  appIdOverride: options.appId,
85
95
  })
@@ -109,38 +119,6 @@ cli
109
119
 
110
120
  const guild = await resolveGuildForProjectCommand({ client, guildIdOverride: options.guild })
111
121
 
112
- // Check if channel already exists in this guild
113
- cliLogger.log('Checking for existing channel...')
114
- try {
115
- const existingChannels = await findChannelsByDirectory({
116
- directory: absolutePath,
117
- channelType: 'text',
118
- })
119
-
120
- for (const existingChannel of existingChannels) {
121
- try {
122
- const ch = await client.channels.fetch(existingChannel.channel_id)
123
- if (ch && !ch.isDMBased() && ch.guild.id === guild.id) {
124
- void client.destroy()
125
- cliLogger.error(
126
- `Channel already exists for this directory in ${guild.name}. Channel ID: ${existingChannel.channel_id}`,
127
- )
128
- process.exit(EXIT_NO_RESTART)
129
- }
130
- } catch (error) {
131
- cliLogger.debug(
132
- `Failed to fetch channel ${existingChannel.channel_id} while checking existing channels:`,
133
- error instanceof Error ? error.stack : String(error),
134
- )
135
- }
136
- }
137
- } catch (error) {
138
- cliLogger.debug(
139
- 'Database lookup failed while checking existing channels:',
140
- error instanceof Error ? error.stack : String(error),
141
- )
142
- }
143
-
144
122
  const { textChannelId, voiceChannelId, channelName } =
145
123
  await createProjectChannels({
146
124
  guild,
@@ -207,7 +185,8 @@ cli
207
185
  guildId = data.guild_id || ''
208
186
  } catch (error) {
209
187
  // Only mark as deleted for Unknown Channel (10003) or 404,
210
- // not transient errors like rate limits or 5xx
188
+ // not transient errors like rate limits or 5xx.
189
+ // Gateway-proxy must return Discord 404 for unknown channels, not 403.
211
190
  const code = error instanceof Error ? Reflect.get(error, 'code') : undefined
212
191
  const status = error instanceof Error ? Reflect.get(error, 'status') : undefined
213
192
  const isUnknownChannel = code === 10003 || status === 404
@@ -26,6 +26,7 @@ import { ShareMarkdown } from '../markdown.js'
26
26
  import { parseSessionSearchPattern, findFirstSessionSearchHit, buildSessionSearchSnippet, getPartSearchTexts } from '../session-search.js'
27
27
  import { formatWorktreeName, formatAutoWorktreeName } from '../commands/new-worktree.js'
28
28
  import { WORKTREE_PREFIX } from '../commands/merge-worktree.js'
29
+ import { QUEUE_PREFIX } from '../message-formatting.js'
29
30
  import type { ThreadStartMarker } from '../system-message.js'
30
31
  import { buildOpencodeEventLogLine } from '../session-handler/opencode-session-event-log.js'
31
32
  import { createDiscordRest } from '../discord-urls.js'
@@ -47,6 +48,7 @@ import {
47
48
  resolveDiscordUserOption,
48
49
  sendDiscordMessageWithOptionalAttachment,
49
50
  } from '../cli-runner.js'
51
+ import { validateCliModelOption } from '../session-handler/model-utils.js'
50
52
 
51
53
  const cliLogger = createLogger(LogPrefix.CLI)
52
54
  const cli = goke()
@@ -178,6 +180,14 @@ cli
178
180
  process.exit(EXIT_NO_RESTART)
179
181
  }
180
182
 
183
+ const earlyModelCheck = await validateCliModelOption({
184
+ model: options.model,
185
+ })
186
+ if (earlyModelCheck instanceof Error) {
187
+ cliLogger.error(earlyModelCheck.message)
188
+ process.exit(EXIT_NO_RESTART)
189
+ }
190
+
181
191
  const filePaths = options.file?.length
182
192
  ? options.file.map((f: string) => path.resolve(f))
183
193
  : undefined
@@ -485,6 +495,14 @@ cli
485
495
  // We only require it for features that genuinely need a local directory
486
496
  // (scheduled tasks and --wait).
487
497
  const channelConfig = await getChannelDirectory(threadData.parent_id)
498
+ const threadModelCheck = await validateCliModelOption({
499
+ model: options.model,
500
+ directory: channelConfig?.directory,
501
+ })
502
+ if (threadModelCheck instanceof Error) {
503
+ cliLogger.error(threadModelCheck.message)
504
+ process.exit(EXIT_NO_RESTART)
505
+ }
488
506
 
489
507
  // Guard early: fail before sending the message if a feature that
490
508
  // needs local project directory mapping is requested.
@@ -537,6 +555,10 @@ cli
537
555
 
538
556
  const threadPromptMarker: ThreadStartMarker = {
539
557
  start: true,
558
+ ...(threadTargetUser && {
559
+ userId: threadTargetUser.id,
560
+ ...(threadTargetUser.username && { username: threadTargetUser.username }),
561
+ }),
540
562
  ...(options.agent && { agent: options.agent }),
541
563
  ...(options.model && { model: options.model }),
542
564
  ...(options.permission?.length ? { permissions: options.permission } : {}),
@@ -553,7 +575,7 @@ cli
553
575
  // Prefix the prompt so it's clear who sent it (matches /queue format).
554
576
  // Use a newline between prefix and prompt so leading /command
555
577
  // detection can find the command on its own line.
556
- const prefixedPrompt = `» **kimaki-cli:**\n${prompt}`
578
+ const prefixedPrompt = `${QUEUE_PREFIX}**kimaki-cli:**\n${prompt}`
557
579
 
558
580
  if (threadTargetUser) {
559
581
  cliLogger.log(
@@ -624,6 +646,14 @@ cli
624
646
  // (--send-at, --wait, --cwd).
625
647
  const channelConfig = await getChannelDirectory(channelData.id)
626
648
  const projectDirectory = channelConfig?.directory
649
+ const channelModelCheck = await validateCliModelOption({
650
+ model: options.model,
651
+ directory: projectDirectory,
652
+ })
653
+ if (channelModelCheck instanceof Error) {
654
+ cliLogger.error(channelModelCheck.message)
655
+ process.exit(EXIT_NO_RESTART)
656
+ }
627
657
 
628
658
  // Features that require a local project directory mapping
629
659
  const needsProjectDirectory = Boolean(parsedSchedule || options.wait || options.cwd)