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,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
  *
@@ -25,7 +26,7 @@
25
26
  import { appendToastSessionMarker } from "./plugin-logger.js";
26
27
  import { createPluginClient } from "./plugin-opencode-client.js";
27
28
  import { isPermanentOAuthRefreshFailure, loadAccountStore, rememberAnthropicOAuth, removeAccountByAuth, rotateAnthropicAccount, saveAccountStore, setAnthropicAuth, shouldRotateAuth, upsertAccount, withAuthStateLock, } from "./anthropic-auth-state.js";
28
- import { extractAnthropicAccountIdentity, } from "./anthropic-account-identity.js";
29
+ import { applyClaudeCodeRequestIdentity, extractAnthropicAccountIdentity, } from "./anthropic-account-identity.js";
29
30
  // PKCE (Proof Key for Code Exchange) using Web Crypto API.
30
31
  // Reference: https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/utils/oauth/pkce.ts
31
32
  function base64urlEncode(bytes) {
@@ -62,7 +63,6 @@ const CALLBACK_PATH = "/callback";
62
63
  const REDIRECT_URI = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
63
64
  const SCOPES = "org:create_api_key user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload";
64
65
  const OAUTH_TIMEOUT_MS = 5 * 60 * 1000;
65
- const CLAUDE_CODE_VERSION = "2.1.75";
66
66
  const CLAUDE_CODE_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude.";
67
67
  const OPENCODE_IDENTITY = "You are OpenCode, the best coding agent on the planet.";
68
68
  // Subagent prompts don't contain OPENCODE_IDENTITY; opencode appends this
@@ -242,13 +242,10 @@ async function fetchAnthropicAccountIdentity(accessToken) {
242
242
  for (const url of urls) {
243
243
  const responseText = await requestText(url, {
244
244
  method: "GET",
245
- headers: {
246
- Accept: "application/json",
247
- authorization: `Bearer ${accessToken}`,
248
- "user-agent": process.env.OPENCODE_ANTHROPIC_USER_AGENT ||
249
- `claude-cli/${CLAUDE_CODE_VERSION}`,
250
- "x-app": "cli",
251
- },
245
+ headers: Object.fromEntries(applyClaudeCodeRequestIdentity({
246
+ headers: new Headers({ Accept: "application/json" }),
247
+ accessToken,
248
+ }).entries()),
252
249
  }).catch(() => {
253
250
  return undefined;
254
251
  });
@@ -876,10 +873,10 @@ const AnthropicAuthPlugin = async ({ serverUrl, directory }) => {
876
873
  requestHeaders.set("accept", "application/json");
877
874
  requestHeaders.set("anthropic-beta", mergeBetas(requestHeaders.get("anthropic-beta"), betas));
878
875
  requestHeaders.set("anthropic-dangerous-direct-browser-access", "true");
879
- requestHeaders.set("authorization", `Bearer ${auth.access}`);
880
- requestHeaders.set("user-agent", process.env.OPENCODE_ANTHROPIC_USER_AGENT ||
881
- `claude-cli/${CLAUDE_CODE_VERSION}`);
882
- requestHeaders.set("x-app", "cli");
876
+ applyClaudeCodeRequestIdentity({
877
+ headers: requestHeaders,
878
+ accessToken: auth.access,
879
+ });
883
880
  requestHeaders.delete("x-api-key");
884
881
  return fetch(input, {
885
882
  ...(init ?? {}),
@@ -1,5 +1,6 @@
1
1
  // Tests Anthropic system prompt rewriting so project instructions survive OpenCode prompt layout changes.
2
2
  import { describe, expect, test } from 'vitest';
3
+ import { applyClaudeCodeRequestIdentity } from './anthropic-account-identity.js';
3
4
  import { replacer } from './anthropic-auth-plugin.js';
4
5
  async function transformSystem(systemText) {
5
6
  const plugin = await replacer({});
@@ -12,6 +13,22 @@ async function transformSystem(systemText) {
12
13
  }, output);
13
14
  return output.system.join('\n');
14
15
  }
16
+ describe('Anthropic OAuth request identity', () => {
17
+ test('applies the current external Claude Code client headers', () => {
18
+ const headers = applyClaudeCodeRequestIdentity({
19
+ headers: new Headers({ Accept: 'application/json' }),
20
+ accessToken: 'access-token',
21
+ });
22
+ expect(Object.fromEntries(headers.entries())).toMatchInlineSnapshot(`
23
+ {
24
+ "accept": "application/json",
25
+ "authorization": "Bearer access-token",
26
+ "user-agent": "claude-cli/2.1.257 (external, cli)",
27
+ "x-app": "cli",
28
+ }
29
+ `);
30
+ });
31
+ });
15
32
  describe('Anthropic system prompt rewriting', () => {
16
33
  test('preserves instructions when OpenCode places them before skills', async () => {
17
34
  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 account store and rotation.
3
4
  * Uses shared utilities from oauth-rotation-shared.ts for file locking,
@@ -0,0 +1,32 @@
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
+ import { z } from 'zod';
7
+ export const bashParameters = z.object({
8
+ command: z.string().describe('The command to execute'),
9
+ description: z
10
+ .string()
11
+ .describe('Short 5-10 word summary shown in Discord when the command is longer than 50 characters'),
12
+ hasSideEffect: z
13
+ .boolean()
14
+ .describe('True if the command writes files, modifies state, installs packages, or triggers external effects'),
15
+ workdir: z
16
+ .string()
17
+ .optional()
18
+ .describe('Working directory. Defaults to the current directory.'),
19
+ timeout: z.number().int().optional().describe('Optional timeout in milliseconds'),
20
+ });
21
+ export function extendBashToolDefinition(output) {
22
+ output.jsonSchema = z.toJSONSchema(bashParameters);
23
+ }
24
+ export const bashToolSchemaPlugin = async () => {
25
+ return {
26
+ 'tool.definition': async (input, output) => {
27
+ if (input.toolID !== 'bash')
28
+ return;
29
+ extendBashToolDefinition(output);
30
+ },
31
+ };
32
+ };
@@ -0,0 +1,49 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { extendBashToolDefinition, } from './bash-tool-schema-plugin.js';
3
+ describe('extendBashToolDefinition', () => {
4
+ test('keeps parameters and assigns jsonSchema from bashParameters', () => {
5
+ const parameters = { keep: true };
6
+ const output = {
7
+ description: 'Execute a shell command',
8
+ parameters,
9
+ };
10
+ extendBashToolDefinition(output);
11
+ expect(output.parameters).toBe(parameters);
12
+ expect(output.jsonSchema).toMatchInlineSnapshot(`
13
+ {
14
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
15
+ "additionalProperties": false,
16
+ "properties": {
17
+ "command": {
18
+ "description": "The command to execute",
19
+ "type": "string",
20
+ },
21
+ "description": {
22
+ "description": "Short 5-10 word summary shown in Discord when the command is longer than 50 characters",
23
+ "type": "string",
24
+ },
25
+ "hasSideEffect": {
26
+ "description": "True if the command writes files, modifies state, installs packages, or triggers external effects",
27
+ "type": "boolean",
28
+ },
29
+ "timeout": {
30
+ "description": "Optional timeout in milliseconds",
31
+ "maximum": 9007199254740991,
32
+ "minimum": -9007199254740991,
33
+ "type": "integer",
34
+ },
35
+ "workdir": {
36
+ "description": "Working directory. Defaults to the current directory.",
37
+ "type": "string",
38
+ },
39
+ },
40
+ "required": [
41
+ "command",
42
+ "description",
43
+ "hasSideEffect",
44
+ ],
45
+ "type": "object",
46
+ }
47
+ `);
48
+ });
49
+ });
package/dist/bin.js CHANGED
@@ -32,14 +32,57 @@ else {
32
32
  console.error('no subcommand detected. kimaki will automatically restart on crash');
33
33
  console.error();
34
34
  const EXIT_NO_RESTART = 64;
35
+ // Keep in sync with EXIT_TEMPFAIL in cli-runner.ts. Network-down login
36
+ // failures use this so a long outage cannot trip the crash-loop detector.
37
+ const EXIT_TEMPFAIL = 75;
35
38
  const MAX_RAPID_RESTARTS = 5;
36
39
  const RAPID_RESTART_WINDOW_MS = 60_000;
37
40
  const RESTART_DELAY_MS = 2_000;
41
+ const CHILD_EXIT_DEADLINE_MS = 15_000;
38
42
  const restartTimestamps = [];
43
+ let tempFailAttempts = 0;
39
44
  let child = null;
40
45
  // Track when we forwarded a termination signal so we don't restart after graceful shutdown
41
46
  let shutdownRequested = false;
47
+ let forceKillTimer = null;
48
+ let scheduledRestart = null;
49
+ function clearForceKillTimer() {
50
+ if (!forceKillTimer)
51
+ return;
52
+ clearTimeout(forceKillTimer);
53
+ forceKillTimer = null;
54
+ }
55
+ function clearScheduledRestart() {
56
+ if (!scheduledRestart)
57
+ return;
58
+ clearTimeout(scheduledRestart);
59
+ scheduledRestart = null;
60
+ }
61
+ function killChild(signal) {
62
+ const target = child;
63
+ if (!target) {
64
+ if (shutdownRequested) {
65
+ clearScheduledRestart();
66
+ process.exit(0);
67
+ }
68
+ return;
69
+ }
70
+ const sent = target.kill(signal);
71
+ if (!sent || forceKillTimer)
72
+ return;
73
+ forceKillTimer = setTimeout(() => {
74
+ forceKillTimer = null;
75
+ if (child !== target)
76
+ return;
77
+ console.error(`[kimaki] Child did not exit within ${CHILD_EXIT_DEADLINE_MS / 1000}s, force-killing it`);
78
+ target.kill('SIGKILL');
79
+ }, CHILD_EXIT_DEADLINE_MS);
80
+ forceKillTimer.unref();
81
+ }
42
82
  function start() {
83
+ if (shutdownRequested)
84
+ return;
85
+ scheduledRestart = null;
43
86
  if (!fs.existsSync(HEAP_SNAPSHOT_DIR)) {
44
87
  fs.mkdirSync(HEAP_SNAPSHOT_DIR, { recursive: true });
45
88
  }
@@ -48,32 +91,42 @@ else {
48
91
  `--diagnostic-dir=${HEAP_SNAPSHOT_DIR}`,
49
92
  ];
50
93
  const args = [...heapArgs, ...process.execArgv, ...process.argv.slice(1)];
51
- child = spawn(process.argv[0], args, {
94
+ const currentChild = spawn(process.argv[0], args, {
52
95
  stdio: 'inherit',
53
96
  env: { ...process.env, __KIMAKI_CHILD: '1' },
54
97
  });
55
- child.on('exit', (code, signal) => {
98
+ child = currentChild;
99
+ currentChild.on('exit', (code, signal) => {
100
+ if (child === currentChild)
101
+ child = null;
102
+ clearForceKillTimer();
56
103
  if (code === 0 || code === EXIT_NO_RESTART || shutdownRequested) {
57
104
  process.exit(code ?? 0);
58
105
  return;
59
106
  }
60
107
  const now = Date.now();
61
- restartTimestamps.push(now);
62
- while (restartTimestamps.length > 0 &&
63
- restartTimestamps[0] < now - RAPID_RESTART_WINDOW_MS) {
64
- restartTimestamps.shift();
65
- }
66
- if (restartTimestamps.length > MAX_RAPID_RESTARTS) {
67
- console.error(`[kimaki] Crash loop detected (${MAX_RAPID_RESTARTS} crashes in ${RAPID_RESTART_WINDOW_MS / 1000}s), exiting`);
68
- process.exit(1);
69
- return;
108
+ const isTempFail = code === EXIT_TEMPFAIL;
109
+ if (!isTempFail) {
110
+ restartTimestamps.push(now);
111
+ while (restartTimestamps.length > 0 &&
112
+ restartTimestamps[0] < now - RAPID_RESTART_WINDOW_MS) {
113
+ restartTimestamps.shift();
114
+ }
115
+ if (restartTimestamps.length > MAX_RAPID_RESTARTS) {
116
+ console.error(`[kimaki] Crash loop detected (${MAX_RAPID_RESTARTS} crashes in ${RAPID_RESTART_WINDOW_MS / 1000}s), exiting`);
117
+ process.exit(1);
118
+ return;
119
+ }
70
120
  }
71
121
  const reason = signal ? `signal ${signal}` : `code ${code}`;
72
122
  // Progressive backoff: 2s, 4s, 8s, 16s, capped at 30s.
73
123
  // Prevents hammering DNS/gateway during sustained network outages.
74
- const delay = Math.min(RESTART_DELAY_MS * 2 ** (restartTimestamps.length - 1), 30_000);
124
+ const backoffStep = isTempFail
125
+ ? Math.min(tempFailAttempts++, 4)
126
+ : restartTimestamps.length - 1;
127
+ const delay = Math.min(RESTART_DELAY_MS * 2 ** backoffStep, 30_000);
75
128
  console.error(`[kimaki] Process exited with ${reason}, restarting in ${(delay / 1000).toFixed(0)}s...`);
76
- setTimeout(start, delay);
129
+ scheduledRestart = setTimeout(start, delay);
77
130
  });
78
131
  }
79
132
  // Forward signals to child so graceful shutdown and heap snapshots work.
@@ -81,13 +134,14 @@ else {
81
134
  for (const sig of ['SIGTERM', 'SIGINT']) {
82
135
  process.on(sig, () => {
83
136
  shutdownRequested = true;
84
- child?.kill(sig);
85
- });
86
- }
87
- for (const sig of ['SIGUSR1', 'SIGUSR2']) {
88
- process.on(sig, () => {
89
- child?.kill(sig);
137
+ killChild(sig);
90
138
  });
91
139
  }
140
+ process.on('SIGUSR1', () => {
141
+ child?.kill('SIGUSR1');
142
+ });
143
+ process.on('SIGUSR2', () => {
144
+ killChild('SIGUSR2');
145
+ });
92
146
  start();
93
147
  }
@@ -60,13 +60,21 @@ cli
60
60
  }
61
61
  });
62
62
  cli
63
- .command('worktree merge', 'Merge worktree branch into default branch using worktrunk-style pipeline')
63
+ .command('merge-worktree', 'Merge the current worktree into a target branch')
64
64
  .option('-d, --directory <path>', 'Worktree directory (defaults to cwd)')
65
65
  .option('-m, --main-repo <path>', 'Main repository directory (auto-detected from worktree)')
66
66
  .option('-n, --name <name>', 'Worktree/branch name (auto-detected from branch)')
67
+ .option('--strategy [strategy]', z.enum(['rebase', 'squash'])
68
+ .default('rebase')
69
+ .describe('Keep commits or squash them into one'))
70
+ .option('--target-branch [branch]', z.string()
71
+ .optional()
72
+ .describe('Branch to merge into (defaults to origin/HEAD or main)'))
73
+ .option('--thread [threadId]', z.string().optional().describe('Discord thread to mark as merged'))
74
+ .option('--thread-name [name]', z.string().optional().describe('Discord thread name after merging'))
67
75
  .action(async (options) => {
68
76
  try {
69
- const { mergeWorktree } = await import('../worktrees.js');
77
+ const { formatMergeWorktreeError, mergeWorktree } = await import('../worktrees.js');
70
78
  const worktreeDir = path.resolve(options.directory || '.');
71
79
  // Auto-detect main repo: find the main worktree's toplevel.
72
80
  // For linked worktrees, --git-common-dir points to the shared .git,
@@ -77,14 +85,20 @@ cli
77
85
  if (!mainRepoDir) {
78
86
  try {
79
87
  // `git worktree list --porcelain` first line is always the main worktree
80
- const { stdout } = await execAsync(`git -C "${worktreeDir}" worktree list --porcelain`);
88
+ const { stdout } = await execAsync({
89
+ command: 'git',
90
+ args: ['-C', worktreeDir, 'worktree', 'list', '--porcelain'],
91
+ });
81
92
  const firstLine = stdout.split('\n')[0] || '';
82
93
  // Format: "worktree /path/to/main"
83
94
  mainRepoDir = firstLine.replace(/^worktree\s+/, '').trim();
84
95
  }
85
96
  catch {
86
97
  // Fallback: derive from git common dir
87
- const { stdout: commonDir } = await execAsync(`git -C "${worktreeDir}" rev-parse --git-common-dir`);
98
+ const { stdout: commonDir } = await execAsync({
99
+ command: 'git',
100
+ args: ['-C', worktreeDir, 'rev-parse', '--git-common-dir'],
101
+ });
88
102
  const resolved = path.isAbsolute(commonDir.trim())
89
103
  ? commonDir.trim()
90
104
  : path.resolve(worktreeDir, commonDir.trim());
@@ -95,7 +109,10 @@ cli
95
109
  let worktreeName = options.name;
96
110
  if (!worktreeName) {
97
111
  try {
98
- const { stdout } = await execAsync(`git -C "${worktreeDir}" symbolic-ref --short HEAD`);
112
+ const { stdout } = await execAsync({
113
+ command: 'git',
114
+ args: ['-C', worktreeDir, 'symbolic-ref', '--short', 'HEAD'],
115
+ });
99
116
  worktreeName = stdout.trim();
100
117
  }
101
118
  catch {
@@ -110,18 +127,40 @@ cli
110
127
  worktreeDir,
111
128
  mainRepoDir,
112
129
  worktreeName,
130
+ strategy: options.strategy,
131
+ targetBranch: options.targetBranch,
113
132
  onProgress: (msg) => {
114
133
  cliLogger.log(msg);
115
134
  },
116
135
  });
117
136
  if (result instanceof Error) {
118
- cliLogger.error(`Merge failed: ${result.message}`);
137
+ cliLogger.error(formatMergeWorktreeError(result, { maxLength: 8000 }));
119
138
  if (result instanceof RebaseConflictError) {
120
139
  cliLogger.log('Resolve the rebase conflicts, then run this command again.');
121
140
  }
122
141
  process.exit(1);
123
142
  }
124
- cliLogger.log(`Merged ${result.branchName} into ${result.defaultBranch} @ ${result.shortSha} (${result.commitCount} commit${result.commitCount === 1 ? '' : 's'})`);
143
+ const action = options.strategy === 'squash' ? 'Squashed' : 'Merged';
144
+ cliLogger.log(`${action} ${result.branchName} into ${result.defaultBranch} @ ${result.shortSha} (${result.commitCount} source commit${result.commitCount === 1 ? '' : 's'})`);
145
+ if (options.thread && options.threadName) {
146
+ const credentials = await resolveBotCredentials().catch((e) => {
147
+ return new Error('Failed to resolve Discord credentials', { cause: e });
148
+ });
149
+ if (credentials instanceof Error) {
150
+ cliLogger.warn(`Could not update the thread title: ${credentials.message}`);
151
+ }
152
+ else {
153
+ const rest = createDiscordRest(credentials.token);
154
+ const renameResult = await rest
155
+ .patch(Routes.channel(options.thread), {
156
+ body: { name: options.threadName },
157
+ })
158
+ .catch((e) => new Error('Discord rejected the thread rename', { cause: e }));
159
+ if (renameResult instanceof Error) {
160
+ cliLogger.warn(`Could not update the thread title: ${renameResult.message}`);
161
+ }
162
+ }
163
+ }
125
164
  process.exit(0);
126
165
  }
127
166
  catch (error) {
@@ -12,7 +12,7 @@ import { spawn, execSync } from 'node:child_process';
12
12
  import { createLogger, LogPrefix, initLogFile } from '../logger.js';
13
13
  import { createDiscordClient, initDatabase, getChannelDirectory, initializeOpencodeForDirectory, createProjectChannels } from '../discord-bot.js';
14
14
  import { getDefaultKimakiDirectory } from '../channel-management.js';
15
- import { getBotTokenWithMode, getThreadSession, getThreadIdBySessionId, getSessionEventSnapshot, getDb, createScheduledTask, listScheduledTasks, cancelScheduledTask, getScheduledTask, updateScheduledTask, getSessionStartSourcesBySessionIds, deleteChannelDirectoryById, findChannelsByDirectory } from '../database.js';
15
+ import { getBotTokenWithMode, getThreadSession, getThreadIdBySessionId, getSessionEventSnapshot, getDb, createScheduledTask, listScheduledTasks, cancelScheduledTask, getScheduledTask, updateScheduledTask, getSessionStartSourcesBySessionIds, deleteChannelDirectoryById, findChannelsByDirectory, findRegisteredTextChannelForDirectory, formatProjectAlreadyRegisteredError } from '../database.js';
16
16
  import { ShareMarkdown } from '../markdown.js';
17
17
  import { parseSessionSearchPattern, findFirstSessionSearchHit, buildSessionSearchSnippet, getPartSearchTexts } from '../session-search.js';
18
18
  import { formatWorktreeName, formatAutoWorktreeName } from '../commands/new-worktree.js';
@@ -39,8 +39,15 @@ cli
39
39
  cliLogger.error(`Directory does not exist: ${absolutePath}`);
40
40
  process.exit(EXIT_NO_RESTART);
41
41
  }
42
- // Initialize database
43
42
  await initDatabase();
43
+ const existingChannel = await findRegisteredTextChannelForDirectory(absolutePath);
44
+ if (existingChannel) {
45
+ cliLogger.error(formatProjectAlreadyRegisteredError({
46
+ channelId: existingChannel.channel_id,
47
+ directory: absolutePath,
48
+ }));
49
+ process.exit(EXIT_NO_RESTART);
50
+ }
44
51
  const { token: botToken, appId } = await resolveBotCredentials({
45
52
  appIdOverride: options.appId,
46
53
  });
@@ -62,30 +69,6 @@ cli
62
69
  });
63
70
  cliLogger.log('Finding guild...');
64
71
  const guild = await resolveGuildForProjectCommand({ client, guildIdOverride: options.guild });
65
- // Check if channel already exists in this guild
66
- cliLogger.log('Checking for existing channel...');
67
- try {
68
- const existingChannels = await findChannelsByDirectory({
69
- directory: absolutePath,
70
- channelType: 'text',
71
- });
72
- for (const existingChannel of existingChannels) {
73
- try {
74
- const ch = await client.channels.fetch(existingChannel.channel_id);
75
- if (ch && !ch.isDMBased() && ch.guild.id === guild.id) {
76
- void client.destroy();
77
- cliLogger.error(`Channel already exists for this directory in ${guild.name}. Channel ID: ${existingChannel.channel_id}`);
78
- process.exit(EXIT_NO_RESTART);
79
- }
80
- }
81
- catch (error) {
82
- cliLogger.debug(`Failed to fetch channel ${existingChannel.channel_id} while checking existing channels:`, error instanceof Error ? error.stack : String(error));
83
- }
84
- }
85
- }
86
- catch (error) {
87
- cliLogger.debug('Database lookup failed while checking existing channels:', error instanceof Error ? error.stack : String(error));
88
- }
89
72
  const { textChannelId, voiceChannelId, channelName } = await createProjectChannels({
90
73
  guild,
91
74
  projectDirectory: absolutePath,
@@ -131,7 +114,8 @@ cli
131
114
  }
132
115
  catch (error) {
133
116
  // Only mark as deleted for Unknown Channel (10003) or 404,
134
- // not transient errors like rate limits or 5xx
117
+ // not transient errors like rate limits or 5xx.
118
+ // Gateway-proxy must return Discord 404 for unknown channels, not 403.
135
119
  const code = error instanceof Error ? Reflect.get(error, 'code') : undefined;
136
120
  const status = error instanceof Error ? Reflect.get(error, 'status') : undefined;
137
121
  const isUnknownChannel = code === 10003 || status === 404;
@@ -21,6 +21,7 @@ import { ShareMarkdown } from '../markdown.js';
21
21
  import { parseSessionSearchPattern, findFirstSessionSearchHit, buildSessionSearchSnippet, getPartSearchTexts } from '../session-search.js';
22
22
  import { formatWorktreeName, formatAutoWorktreeName } from '../commands/new-worktree.js';
23
23
  import { WORKTREE_PREFIX } from '../commands/merge-worktree.js';
24
+ import { QUEUE_PREFIX } from '../message-formatting.js';
24
25
  import { buildOpencodeEventLogLine } from '../session-handler/opencode-session-event-log.js';
25
26
  import { createDiscordRest } from '../discord-urls.js';
26
27
  import { archiveThread, ensureThreadMember, uploadFilesToDiscord, stripMentions } from '../discord-utils.js';
@@ -29,6 +30,7 @@ import { execAsync, resolveSessionWorkingDirectory, isGitRepositoryRoot } from '
29
30
  import { upgrade, getCurrentVersion } from '../upgrade.js';
30
31
  import { getPromptPreview, parseSendAtValue, parseScheduledTaskPayload, serializeScheduledTaskPayload } from '../task-schedule.js';
31
32
  import { EXIT_NO_RESTART, formatMemberLookupUnavailableMessage, formatRelativeTime, formatTaskScheduleLine, isDiscordMemberLookupUnavailable, isGuildMemberSearchResult, isThreadChannelType, printDiscordInstallUrlAndExit, resolveBotCredentials, resolveDiscordUserOption, sendDiscordMessageWithOptionalAttachment, } from '../cli-runner.js';
33
+ import { validateCliModelOption } from '../session-handler/model-utils.js';
32
34
  const cliLogger = createLogger(LogPrefix.CLI);
33
35
  const cli = goke();
34
36
  cli
@@ -89,6 +91,13 @@ cli
89
91
  cliLogger.error('Prompt is required. Use --prompt <prompt>');
90
92
  process.exit(EXIT_NO_RESTART);
91
93
  }
94
+ const earlyModelCheck = await validateCliModelOption({
95
+ model: options.model,
96
+ });
97
+ if (earlyModelCheck instanceof Error) {
98
+ cliLogger.error(earlyModelCheck.message);
99
+ process.exit(EXIT_NO_RESTART);
100
+ }
92
101
  const filePaths = options.file?.length
93
102
  ? options.file.map((f) => path.resolve(f))
94
103
  : undefined;
@@ -335,6 +344,14 @@ cli
335
344
  // We only require it for features that genuinely need a local directory
336
345
  // (scheduled tasks and --wait).
337
346
  const channelConfig = await getChannelDirectory(threadData.parent_id);
347
+ const threadModelCheck = await validateCliModelOption({
348
+ model: options.model,
349
+ directory: channelConfig?.directory,
350
+ });
351
+ if (threadModelCheck instanceof Error) {
352
+ cliLogger.error(threadModelCheck.message);
353
+ process.exit(EXIT_NO_RESTART);
354
+ }
338
355
  // Guard early: fail before sending the message if a feature that
339
356
  // needs local project directory mapping is requested.
340
357
  if (!channelConfig && (parsedSchedule || options.wait)) {
@@ -378,6 +395,10 @@ cli
378
395
  }
379
396
  const threadPromptMarker = {
380
397
  start: true,
398
+ ...(threadTargetUser && {
399
+ userId: threadTargetUser.id,
400
+ ...(threadTargetUser.username && { username: threadTargetUser.username }),
401
+ }),
381
402
  ...(options.agent && { agent: options.agent }),
382
403
  ...(options.model && { model: options.model }),
383
404
  ...(options.permission?.length ? { permissions: options.permission } : {}),
@@ -393,7 +414,7 @@ cli
393
414
  // Prefix the prompt so it's clear who sent it (matches /queue format).
394
415
  // Use a newline between prefix and prompt so leading /command
395
416
  // detection can find the command on its own line.
396
- const prefixedPrompt = `» **kimaki-cli:**\n${prompt}`;
417
+ const prefixedPrompt = `${QUEUE_PREFIX}**kimaki-cli:**\n${prompt}`;
397
418
  if (threadTargetUser) {
398
419
  cliLogger.log(`Adding user ${threadTargetUser.username || threadTargetUser.id} to thread...`);
399
420
  const addMemberResult = await ensureThreadMember({
@@ -446,6 +467,14 @@ cli
446
467
  // (--send-at, --wait, --cwd).
447
468
  const channelConfig = await getChannelDirectory(channelData.id);
448
469
  const projectDirectory = channelConfig?.directory;
470
+ const channelModelCheck = await validateCliModelOption({
471
+ model: options.model,
472
+ directory: projectDirectory,
473
+ });
474
+ if (channelModelCheck instanceof Error) {
475
+ cliLogger.error(channelModelCheck.message);
476
+ process.exit(EXIT_NO_RESTART);
477
+ }
449
478
  // Features that require a local project directory mapping
450
479
  const needsProjectDirectory = Boolean(parsedSchedule || options.wait || options.cwd);
451
480
  if (!channelConfig && needsProjectDirectory) {