kimaki 0.27.0 → 0.29.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 (246) hide show
  1. package/dist/agent-model.e2e.test.js +247 -64
  2. package/dist/anthropic-account-identity.js +8 -1
  3. package/dist/anthropic-auth-plugin.js +9 -13
  4. package/dist/anthropic-auth-plugin.test.js +17 -0
  5. package/dist/bash-tool-schema-plugin.js +50 -0
  6. package/dist/bash-tool-schema-plugin.test.js +68 -0
  7. package/dist/channel-management.js +113 -51
  8. package/dist/cli-commands/misc.js +4 -1
  9. package/dist/cli-commands/project.js +17 -30
  10. package/dist/cli-commands/send.js +6 -1
  11. package/dist/cli-commands/session.js +223 -115
  12. package/dist/cli-commands/task.js +21 -14
  13. package/dist/cli-commands/thread.js +63 -0
  14. package/dist/cli-parsing.test.js +24 -1
  15. package/dist/cli-runner.js +16 -4
  16. package/dist/cli-send-thread.e2e.test.js +9 -9
  17. package/dist/cli.js +2 -0
  18. package/dist/commands/action-buttons.js +2 -1
  19. package/dist/commands/add-project.js +4 -7
  20. package/dist/commands/agent.js +158 -24
  21. package/dist/commands/ask-question.js +2 -1
  22. package/dist/commands/btw.js +28 -6
  23. package/dist/commands/file-upload.js +2 -1
  24. package/dist/commands/fork-subagent.js +2 -4
  25. package/dist/commands/fork.js +4 -4
  26. package/dist/commands/last-sessions.js +17 -10
  27. package/dist/commands/merge-worktree.js +13 -6
  28. package/dist/commands/model-variant.js +3 -2
  29. package/dist/commands/model.js +4 -3
  30. package/dist/commands/queue.js +82 -33
  31. package/dist/commands/queue.test.js +15 -0
  32. package/dist/commands/restart-opencode-server.js +7 -1
  33. package/dist/commands/resume.js +4 -4
  34. package/dist/commands/session-id.js +5 -5
  35. package/dist/commands/session.js +78 -63
  36. package/dist/cpu-profiler.js +211 -0
  37. package/dist/cpu-profiler.test.js +160 -0
  38. package/dist/database.js +104 -0
  39. package/dist/db.js +45 -0
  40. package/dist/db.test.js +196 -1
  41. package/dist/default-channel-provisioning.test.js +149 -0
  42. package/dist/discord-bot.js +509 -476
  43. package/dist/discord-command-registration.js +17 -10
  44. package/dist/discord-command-registration.test.js +17 -0
  45. package/dist/discord-utils.js +84 -0
  46. package/dist/discord-utils.test.js +135 -1
  47. package/dist/errors.js +5 -0
  48. package/dist/external-directory-permissions.e2e.test.js +11 -7
  49. package/dist/external-directory-project-deny.e2e.test.js +5 -3
  50. package/dist/external-opencode-sync.js +94 -17
  51. package/dist/external-opencode-sync.test.js +206 -0
  52. package/dist/extract-sdk-error-message.test.js +13 -0
  53. package/dist/gateway-proxy.e2e.test.js +18 -18
  54. package/dist/guild-categories.test.js +225 -0
  55. package/dist/interaction-handler.js +421 -399
  56. package/dist/ipc-tools-plugin.js +12 -7
  57. package/dist/kimaki-opencode-plugin-loading.e2e.test.js +2 -2
  58. package/dist/kimaki-opencode-plugin.js +4 -0
  59. package/dist/kimaki-opencode-plugin.test.js +28 -0
  60. package/dist/list-channel-threads.js +163 -0
  61. package/dist/list-channel-threads.test.js +80 -0
  62. package/dist/logger.js +1 -0
  63. package/dist/message-formatting.js +156 -44
  64. package/dist/message-formatting.test.js +562 -13
  65. package/dist/message-preprocessing.js +105 -1
  66. package/dist/opencode-command.js +28 -0
  67. package/dist/opencode-command.test.js +49 -1
  68. package/dist/opencode-interrupt-plugin.js +4 -0
  69. package/dist/opencode-interrupt-plugin.test.js +24 -0
  70. package/dist/opencode.js +29 -3
  71. package/dist/project-add.test.js +62 -0
  72. package/dist/question-abort-clears-dropdown.e2e.test.js +1 -1
  73. package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +150 -0
  74. package/dist/question-text-before-dropdown.e2e.test.js +5 -5
  75. package/dist/queue-advanced-abort.e2e.test.js +8 -10
  76. package/dist/queue-advanced-action-buttons.e2e.test.js +8 -8
  77. package/dist/queue-advanced-footer.e2e.test.js +29 -32
  78. package/dist/queue-advanced-model-switch.e2e.test.js +9 -11
  79. package/dist/queue-advanced-permissions-typing.e2e.test.js +9 -7
  80. package/dist/queue-advanced-question.e2e.test.js +2 -2
  81. package/dist/queue-advanced-typing-interrupt.e2e.test.js +10 -12
  82. package/dist/queue-advanced-typing.e2e.test.js +7 -9
  83. package/dist/queue-delete-message.e2e.test.js +3 -3
  84. package/dist/queue-drain-after-interactive-ui.e2e.test.js +5 -5
  85. package/dist/queue-interrupt-drain.e2e.test.js +20 -20
  86. package/dist/queue-question-select-drain.e2e.test.js +7 -7
  87. package/dist/runtime-lifecycle.e2e.test.js +16 -19
  88. package/dist/schema.js +22 -1
  89. package/dist/session-handler/event-stream-state.js +176 -4
  90. package/dist/session-handler/event-stream-state.test.js +545 -1
  91. package/dist/session-handler/model-utils.js +16 -3
  92. package/dist/session-handler/model-utils.test.js +103 -3
  93. package/dist/session-handler/opencode-session-event-log.js +30 -1
  94. package/dist/session-handler/thread-ingress.test.js +41 -0
  95. package/dist/session-handler/thread-runtime-state.js +6 -0
  96. package/dist/session-handler/thread-session-runtime-summary.test.js +121 -0
  97. package/dist/session-handler/thread-session-runtime.js +642 -196
  98. package/dist/session-search.js +123 -4
  99. package/dist/session-search.test.js +248 -1
  100. package/dist/session-title-rename.test.js +30 -0
  101. package/dist/sleep.e2e.test.js +14 -11
  102. package/dist/system-message.js +100 -25
  103. package/dist/system-message.test.js +167 -23
  104. package/dist/task-id-plugin.js +16 -0
  105. package/dist/task-runner.js +2 -1
  106. package/dist/task-schedule.js +32 -2
  107. package/dist/task-schedule.test.js +79 -1
  108. package/dist/test-utils.js +17 -9
  109. package/dist/thinking-utils.js +10 -0
  110. package/dist/thinking-utils.test.js +35 -0
  111. package/dist/thread-message-queue.e2e.test.js +65 -54
  112. package/dist/undo-redo.e2e.test.js +17 -9
  113. package/dist/voice-handler.js +21 -4
  114. package/dist/voice-message.e2e.test.js +144 -30
  115. package/dist/voice-routing-input.e2e.test.js +195 -0
  116. package/dist/voice-routing-question.e2e.test.js +125 -0
  117. package/dist/voice.js +28 -8
  118. package/dist/voice.test.js +41 -1
  119. package/dist/worktree-lifecycle.e2e.test.js +30 -30
  120. package/package.json +9 -9
  121. package/skills/bump-holocron-consumers/SKILL.md +137 -0
  122. package/skills/critique/SKILL.md +4 -4
  123. package/skills/errore/SKILL.md +56 -232
  124. package/skills/holocron/SKILL.md +117 -17
  125. package/skills/strada/SKILL.md +61 -232
  126. package/src/agent-model.e2e.test.ts +293 -66
  127. package/src/anthropic-account-identity.ts +19 -1
  128. package/src/anthropic-auth-plugin.test.ts +19 -0
  129. package/src/anthropic-auth-plugin.ts +11 -16
  130. package/src/bash-tool-schema-plugin.test.ts +84 -0
  131. package/src/bash-tool-schema-plugin.ts +77 -0
  132. package/src/channel-management.ts +144 -65
  133. package/src/cli-commands/misc.ts +4 -1
  134. package/src/cli-commands/project.ts +22 -38
  135. package/src/cli-commands/send.ts +6 -1
  136. package/src/cli-commands/session.ts +294 -139
  137. package/src/cli-commands/task.ts +22 -15
  138. package/src/cli-commands/thread.ts +86 -0
  139. package/src/cli-parsing.test.ts +34 -1
  140. package/src/cli-runner.ts +16 -3
  141. package/src/cli-send-thread.e2e.test.ts +9 -11
  142. package/src/cli.ts +2 -0
  143. package/src/commands/action-buttons.ts +2 -1
  144. package/src/commands/add-project.ts +4 -8
  145. package/src/commands/agent.ts +199 -23
  146. package/src/commands/ask-question.ts +2 -1
  147. package/src/commands/btw.ts +40 -5
  148. package/src/commands/file-upload.ts +2 -1
  149. package/src/commands/fork-subagent.ts +2 -3
  150. package/src/commands/fork.ts +4 -3
  151. package/src/commands/last-sessions.ts +10 -4
  152. package/src/commands/merge-worktree.ts +13 -6
  153. package/src/commands/model-variant.ts +3 -2
  154. package/src/commands/model.ts +4 -3
  155. package/src/commands/queue.test.ts +19 -0
  156. package/src/commands/queue.ts +109 -42
  157. package/src/commands/restart-opencode-server.ts +12 -1
  158. package/src/commands/resume.ts +4 -3
  159. package/src/commands/session-id.ts +5 -5
  160. package/src/commands/session.ts +94 -70
  161. package/src/cpu-profiler.test.ts +183 -0
  162. package/src/cpu-profiler.ts +293 -0
  163. package/src/database.ts +154 -0
  164. package/src/db.test.ts +217 -0
  165. package/src/db.ts +50 -0
  166. package/src/default-channel-provisioning.test.ts +156 -0
  167. package/src/discord-bot.ts +54 -9
  168. package/src/discord-command-registration.test.ts +20 -0
  169. package/src/discord-command-registration.ts +30 -12
  170. package/src/discord-utils.test.ts +157 -0
  171. package/src/discord-utils.ts +131 -1
  172. package/src/errors.ts +7 -0
  173. package/src/external-directory-permissions.e2e.test.ts +11 -7
  174. package/src/external-directory-project-deny.e2e.test.ts +5 -3
  175. package/src/external-opencode-sync.test.ts +248 -0
  176. package/src/external-opencode-sync.ts +122 -19
  177. package/src/extract-sdk-error-message.test.ts +18 -0
  178. package/src/gateway-proxy.e2e.test.ts +18 -17
  179. package/src/guild-categories.test.ts +260 -0
  180. package/src/interaction-handler.ts +43 -13
  181. package/src/ipc-tools-plugin.ts +16 -9
  182. package/src/kimaki-opencode-plugin-loading.e2e.test.ts +5 -5
  183. package/src/kimaki-opencode-plugin.test.ts +34 -0
  184. package/src/kimaki-opencode-plugin.ts +4 -0
  185. package/src/list-channel-threads.test.ts +90 -0
  186. package/src/list-channel-threads.ts +252 -0
  187. package/src/logger.ts +1 -0
  188. package/src/message-formatting.test.ts +690 -13
  189. package/src/message-formatting.ts +218 -46
  190. package/src/message-preprocessing.ts +116 -1
  191. package/src/opencode-command.test.ts +59 -0
  192. package/src/opencode-command.ts +38 -0
  193. package/src/opencode-interrupt-plugin.test.ts +32 -0
  194. package/src/opencode-interrupt-plugin.ts +3 -0
  195. package/src/opencode.ts +44 -4
  196. package/src/project-add.test.ts +80 -0
  197. package/src/question-abort-clears-dropdown.e2e.test.ts +1 -1
  198. package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +172 -0
  199. package/src/question-text-before-dropdown.e2e.test.ts +5 -5
  200. package/src/queue-advanced-abort.e2e.test.ts +8 -9
  201. package/src/queue-advanced-action-buttons.e2e.test.ts +8 -8
  202. package/src/queue-advanced-footer.e2e.test.ts +30 -31
  203. package/src/queue-advanced-model-switch.e2e.test.ts +9 -10
  204. package/src/queue-advanced-permissions-typing.e2e.test.ts +9 -7
  205. package/src/queue-advanced-question.e2e.test.ts +2 -2
  206. package/src/queue-advanced-typing-interrupt.e2e.test.ts +11 -11
  207. package/src/queue-advanced-typing.e2e.test.ts +8 -8
  208. package/src/queue-delete-message.e2e.test.ts +3 -3
  209. package/src/queue-drain-after-interactive-ui.e2e.test.ts +5 -5
  210. package/src/queue-interrupt-drain.e2e.test.ts +20 -20
  211. package/src/queue-question-select-drain.e2e.test.ts +7 -7
  212. package/src/runtime-lifecycle.e2e.test.ts +16 -18
  213. package/src/schema.sql +17 -1
  214. package/src/schema.ts +24 -1
  215. package/src/session-handler/event-stream-state.test.ts +579 -0
  216. package/src/session-handler/event-stream-state.ts +235 -4
  217. package/src/session-handler/model-utils.test.ts +125 -3
  218. package/src/session-handler/model-utils.ts +36 -4
  219. package/src/session-handler/opencode-session-event-log.ts +31 -1
  220. package/src/session-handler/thread-ingress.test.ts +54 -0
  221. package/src/session-handler/thread-runtime-state.ts +8 -0
  222. package/src/session-handler/thread-session-runtime-summary.test.ts +169 -0
  223. package/src/session-handler/thread-session-runtime.ts +797 -229
  224. package/src/session-search.test.ts +281 -0
  225. package/src/session-search.ts +241 -7
  226. package/src/session-title-rename.test.ts +45 -0
  227. package/src/sleep.e2e.test.ts +14 -11
  228. package/src/store.ts +1 -0
  229. package/src/system-message.test.ts +191 -23
  230. package/src/system-message.ts +124 -23
  231. package/src/task-id-plugin.ts +17 -0
  232. package/src/task-runner.ts +2 -1
  233. package/src/task-schedule.test.ts +94 -0
  234. package/src/task-schedule.ts +47 -2
  235. package/src/test-utils.ts +21 -10
  236. package/src/thinking-utils.test.ts +46 -0
  237. package/src/thinking-utils.ts +21 -0
  238. package/src/thread-message-queue.e2e.test.ts +65 -53
  239. package/src/undo-redo.e2e.test.ts +17 -9
  240. package/src/voice-handler.ts +23 -4
  241. package/src/voice-message.e2e.test.ts +149 -29
  242. package/src/voice-routing-input.e2e.test.ts +203 -0
  243. package/src/voice-routing-question.e2e.test.ts +131 -0
  244. package/src/voice.test.ts +44 -0
  245. package/src/voice.ts +34 -6
  246. package/src/worktree-lifecycle.e2e.test.ts +30 -30
@@ -0,0 +1,68 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { extendBashToolDefinition, injectKimakiSessionEnv, KIMAKI_SESSION_ID_ENV, resolveUploadToDiscordSessionId, } 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
+ });
50
+ describe('upload-to-discord session targeting', () => {
51
+ test('bash env uses the live OpenCode session, not a copied --session flag', () => {
52
+ const env = {};
53
+ injectKimakiSessionEnv({ sessionID: 'ses_child', env });
54
+ expect(env).toEqual({ [KIMAKI_SESSION_ID_ENV]: 'ses_child' });
55
+ expect(resolveUploadToDiscordSessionId({
56
+ flagSessionId: 'ses_parent',
57
+ envSessionId: env[KIMAKI_SESSION_ID_ENV],
58
+ })).toBe('ses_child');
59
+ });
60
+ test('falls back to --session when bash is not inside an OpenCode session', () => {
61
+ expect(resolveUploadToDiscordSessionId({
62
+ flagSessionId: 'ses_parent',
63
+ })).toBe('ses_parent');
64
+ });
65
+ test('returns undefined when neither live session nor flag is set', () => {
66
+ expect(resolveUploadToDiscordSessionId({})).toBeUndefined();
67
+ });
68
+ });
@@ -4,7 +4,7 @@
4
4
  import { ChannelType, } from 'discord.js';
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
- import { getChannelDirectory, setChannelDirectory, findChannelsByDirectory, listTrackedTextChannels, } from './database.js';
7
+ import { getChannelDirectory, setChannelDirectory, findChannelsByDirectory, listTrackedTextChannels, getGuildCategories, setGuildCategoryId, } from './database.js';
8
8
  import { getProjectsDir } from './config.js';
9
9
  import { execAsync } from './worktrees.js';
10
10
  import { createLogger, LogPrefix } from './logger.js';
@@ -39,41 +39,117 @@ async function trackProjectRegistered({ projectKind, source, }) {
39
39
  trackEvent('project_registered', props);
40
40
  }
41
41
  const logger = createLogger(LogPrefix.CHANNEL);
42
- export async function ensureKimakiCategory(guild, botName) {
43
- // Skip appending bot name if it's already "kimaki" to avoid "Kimaki kimaki"
42
+ function defaultCategoryName(kind, botName) {
44
43
  const isKimakiBot = botName?.toLowerCase() === 'kimaki';
45
- const categoryName = botName && !isKimakiBot ? `Kimaki ${botName}` : 'Kimaki';
46
- const existingCategory = guild.channels.cache.find((channel) => {
47
- if (channel.type !== ChannelType.GuildCategory) {
48
- return false;
44
+ if (kind === 'audio') {
45
+ return botName && !isKimakiBot ? `Kimaki Audio ${botName}` : 'Kimaki Audio';
46
+ }
47
+ return botName && !isKimakiBot ? `Kimaki ${botName}` : 'Kimaki';
48
+ }
49
+ function defaultKimakiChannelName({ botName, isGatewayMode, }) {
50
+ if (isGatewayMode || !botName)
51
+ return 'kimaki';
52
+ const sanitized = botName
53
+ .toLowerCase()
54
+ .replace(/[^a-z0-9-]/g, '-')
55
+ .replace(/-+/g, '-')
56
+ .replace(/^-|-$/g, '');
57
+ if (!sanitized || sanitized === 'kimaki')
58
+ return 'kimaki';
59
+ return `kimaki-${sanitized}`.slice(0, 100);
60
+ }
61
+ const categoryEnsures = new Map();
62
+ function ensureCategorySerialized({ key, run, }) {
63
+ const existing = categoryEnsures.get(key);
64
+ if (existing)
65
+ return existing;
66
+ const promise = run().finally(() => {
67
+ if (categoryEnsures.get(key) === promise) {
68
+ categoryEnsures.delete(key);
49
69
  }
50
- return channel.name.toLowerCase() === categoryName.toLowerCase();
51
70
  });
52
- if (existingCategory) {
53
- return existingCategory;
71
+ categoryEnsures.set(key, promise);
72
+ return promise;
73
+ }
74
+ function isUnknownDiscordChannel(error) {
75
+ const code = error instanceof Error ? Reflect.get(error, 'code') : undefined;
76
+ const status = error instanceof Error ? Reflect.get(error, 'status') : undefined;
77
+ return code === 10003 || status === 404;
78
+ }
79
+ function isCategoryChannel(channel) {
80
+ return channel?.type === ChannelType.GuildCategory;
81
+ }
82
+ async function fetchCategoryById(guild, categoryId) {
83
+ const cached = guild.channels.cache.get(categoryId);
84
+ if (isCategoryChannel(cached))
85
+ return cached;
86
+ try {
87
+ const fetched = await guild.channels.fetch(categoryId);
88
+ return isCategoryChannel(fetched) ? fetched : null;
89
+ }
90
+ catch (error) {
91
+ if (isUnknownDiscordChannel(error))
92
+ return null;
93
+ throw error;
54
94
  }
55
- return guild.channels.create({
56
- name: categoryName,
57
- type: ChannelType.GuildCategory,
58
- });
59
95
  }
60
- export async function ensureKimakiAudioCategory(guild, botName) {
61
- // Skip appending bot name if it's already "kimaki" to avoid "Kimaki Audio kimaki"
62
- const isKimakiBot = botName?.toLowerCase() === 'kimaki';
63
- const categoryName = botName && !isKimakiBot ? `Kimaki Audio ${botName}` : 'Kimaki Audio';
64
- const existingCategory = guild.channels.cache.find((channel) => {
65
- if (channel.type !== ChannelType.GuildCategory) {
66
- return false;
67
- }
68
- return channel.name.toLowerCase() === categoryName.toLowerCase();
96
+ async function adoptParentFromTrackedChannels({ guild, channelType, }) {
97
+ const mappings = await findChannelsByDirectory({ channelType });
98
+ const channels = await guild.channels.fetch();
99
+ for (const row of mappings) {
100
+ if (row.guild_id && row.guild_id !== guild.id)
101
+ continue;
102
+ const channel = channels.get(row.channel_id);
103
+ if (!channel?.parentId)
104
+ continue;
105
+ const parent = await fetchCategoryById(guild, channel.parentId);
106
+ if (parent)
107
+ return parent;
108
+ }
109
+ return null;
110
+ }
111
+ async function resolveKimakiCategory({ guild, kind, botName, }) {
112
+ const stored = await getGuildCategories(guild.id);
113
+ const storedId = kind === 'audio' ? stored?.audio_category_id : stored?.category_id;
114
+ if (storedId) {
115
+ const existing = await fetchCategoryById(guild, storedId);
116
+ if (existing)
117
+ return existing;
118
+ }
119
+ const adopted = await adoptParentFromTrackedChannels({
120
+ guild,
121
+ channelType: kind === 'audio' ? 'voice' : 'text',
69
122
  });
70
- if (existingCategory) {
71
- return existingCategory;
123
+ if (adopted) {
124
+ await setGuildCategoryId({
125
+ guildId: guild.id,
126
+ kind,
127
+ categoryId: adopted.id,
128
+ });
129
+ return adopted;
72
130
  }
73
- return guild.channels.create({
74
- name: categoryName,
131
+ const created = await guild.channels.create({
132
+ name: defaultCategoryName(kind, botName),
75
133
  type: ChannelType.GuildCategory,
76
134
  });
135
+ await setGuildCategoryId({
136
+ guildId: guild.id,
137
+ kind,
138
+ categoryId: created.id,
139
+ });
140
+ return created;
141
+ }
142
+ export function ensureKimakiCategory(guild, botName) {
143
+ return ensureCategorySerialized({
144
+ key: `${guild.id}:text`,
145
+ run: () => resolveKimakiCategory({ guild, kind: 'text', botName }),
146
+ });
147
+ }
148
+ export function ensureKimakiAudioCategory(guild, botName) {
149
+ return ensureCategorySerialized({
150
+ key: `${guild.id}:audio`,
151
+ run: () => resolveKimakiCategory({ guild, kind: 'audio', botName }),
152
+ });
77
153
  }
78
154
  export async function createProjectChannels({ guild, projectDirectory, botName, enableVoiceChannels = false, analyticsSource = 'cli', }) {
79
155
  const baseName = path.basename(projectDirectory);
@@ -92,6 +168,7 @@ export async function createProjectChannels({ guild, projectDirectory, botName,
92
168
  channelId: textChannel.id,
93
169
  directory: projectDirectory,
94
170
  channelType: 'text',
171
+ guildId: guild.id,
95
172
  });
96
173
  await trackProjectRegistered({
97
174
  projectKind: 'user',
@@ -109,6 +186,7 @@ export async function createProjectChannels({ guild, projectDirectory, botName,
109
186
  channelId: voiceChannel.id,
110
187
  directory: projectDirectory,
111
188
  channelType: 'voice',
189
+ guildId: guild.id,
112
190
  });
113
191
  voiceChannelId = voiceChannel.id;
114
192
  }
@@ -158,8 +236,9 @@ const DEFAULT_CHANNEL_TOPIC = 'General channel for misc tasks with Kimaki. Not c
158
236
  * Directory is ~/.kimaki/projects/kimaki, git-initialized with a .gitignore.
159
237
  *
160
238
  * Idempotency: checks the database for an existing channel mapped to the
161
- * kimaki projects directory. Also scans guild channels by name+category
162
- * as a fallback for channels created before DB mapping existed.
239
+ * kimaki projects directory. Also scans this machine's category for the
240
+ * exact default channel name as a fallback for channels created before
241
+ * DB mapping existed.
163
242
  */
164
243
  export async function createDefaultKimakiChannel({ guild, botName, appId, isGatewayMode, }) {
165
244
  const projectDirectory = getDefaultKimakiDirectory();
@@ -209,11 +288,9 @@ export async function createDefaultKimakiChannel({ guild, botName, appId, isGate
209
288
  logger.log(`Default kimaki channel was previously provisioned for guild ${guild.name} (${guild.id}) as ${staleForThisGuild.channel_id}, but no longer exists. Skipping recreation.`);
210
289
  return null;
211
290
  }
212
- // 2. Fallback: detect existing channel by name+category.
213
- // If a "kimaki" channel already exists in the guild but is NOT in our local
214
- // DB, it was likely created by another kimaki instance (different machine).
215
- // Do NOT adopt it — just skip channel creation entirely to avoid both
216
- // instances fighting over the same channel.
291
+ // 2. Fallback: detect an existing default channel in THIS machine's group.
292
+ // A #kimaki channel in another machine's group is ignored.
293
+ const channelName = defaultKimakiChannelName({ botName, isGatewayMode });
217
294
  const kimakiCategory = await ensureKimakiCategory(guild, botName);
218
295
  const existingByName = guild.channels.cache.find((ch) => {
219
296
  if (ch.type !== ChannelType.GuildText) {
@@ -222,10 +299,10 @@ export async function createDefaultKimakiChannel({ guild, botName, appId, isGate
222
299
  if (ch.parentId !== kimakiCategory.id) {
223
300
  return false;
224
301
  }
225
- return ch.name === 'kimaki' || ch.name.startsWith('kimaki-');
302
+ return ch.name === channelName;
226
303
  });
227
304
  if (existingByName) {
228
- logger.log(`Found existing default kimaki channel by name: ${existingByName.id}, but it is not in our DB — skipping (likely owned by another kimaki instance)`);
305
+ logger.log(`Found existing default kimaki channel by name: ${existingByName.id}. Skipping recreation.`);
229
306
  return null;
230
307
  }
231
308
  // Git init — gracefully skip if git is not installed
@@ -244,21 +321,6 @@ export async function createDefaultKimakiChannel({ guild, botName, appId, isGate
244
321
  if (!fs.existsSync(gitignorePath)) {
245
322
  fs.writeFileSync(gitignorePath, DEFAULT_GITIGNORE);
246
323
  }
247
- // Channel name: "kimaki-{botName}" for self-hosted, "kimaki" for gateway
248
- const channelName = (() => {
249
- if (isGatewayMode || !botName) {
250
- return 'kimaki';
251
- }
252
- const sanitized = botName
253
- .toLowerCase()
254
- .replace(/[^a-z0-9-]/g, '-')
255
- .replace(/-+/g, '-')
256
- .replace(/^-|-$/g, '');
257
- if (!sanitized || sanitized === 'kimaki') {
258
- return 'kimaki';
259
- }
260
- return `kimaki-${sanitized}`.slice(0, 100);
261
- })();
262
324
  const textChannel = await guild.channels.create({
263
325
  name: channelName,
264
326
  type: ChannelType.GuildText,
@@ -24,6 +24,7 @@ import { execAsync, validateWorktreeDirectory } from '../worktrees.js';
24
24
  import { upgrade, getCurrentVersion } from '../upgrade.js';
25
25
  import { getPromptPreview, parseSendAtValue, parseScheduledTaskPayload, serializeScheduledTaskPayload } from '../task-schedule.js';
26
26
  import { EXIT_NO_RESTART, formatMemberLookupUnavailableMessage, formatRelativeTime, formatTaskScheduleLine, isDiscordMemberLookupUnavailable, isGuildMemberSearchResult, isThreadChannelType, printDiscordInstallUrlAndExit, resolveBotCredentials, resolveDiscordUserOption, sendDiscordMessageWithOptionalAttachment, } from '../cli-runner.js';
27
+ import { resolveUploadToDiscordSessionId } from '../bash-tool-schema-plugin.js';
27
28
  const cliLogger = createLogger(LogPrefix.CLI);
28
29
  const cli = goke();
29
30
  cli
@@ -31,7 +32,9 @@ cli
31
32
  .option('-s, --session <sessionId>', 'OpenCode session ID')
32
33
  .action(async (files, options) => {
33
34
  try {
34
- const { session: sessionId } = options;
35
+ const sessionId = resolveUploadToDiscordSessionId({
36
+ flagSessionId: options.session,
37
+ });
35
38
  if (!sessionId) {
36
39
  cliLogger.error('Session ID is required. Use --session <sessionId>');
37
40
  process.exit(EXIT_NO_RESTART);
@@ -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, listGuildCategoryIds } 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,
@@ -105,7 +88,7 @@ cli
105
88
  cli
106
89
  .command('project list', 'List all registered projects with their Discord channels')
107
90
  .option('--json', 'Output as JSON')
108
- .option('--all', 'Include remote projects from other machines (scans Kimaki category in Discord)')
91
+ .option('--all', 'Include remote projects from other machines (scans Kimaki groups in Discord)')
109
92
  .option('-g, --guild <guildId>', 'Discord guild/server ID to scan (used with --all when no local projects exist)')
110
93
  .option('--prune', 'Remove stale entries whose Discord channel no longer exists')
111
94
  .action(async (options) => {
@@ -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;
@@ -174,12 +158,15 @@ cli
174
158
  process.exit(EXIT_NO_RESTART);
175
159
  }
176
160
  let guildScanFailures = 0;
161
+ const storedCategoryIds = new Set(await listGuildCategoryIds());
177
162
  for (const guildId of uniqueGuildIds) {
178
163
  try {
179
164
  const guildChannels = (await rest.get(Routes.guildChannels(guildId)));
180
- // Find Kimaki category channels (type 4 = GuildCategory)
165
+ // Kimaki groups: name still starts with Kimaki, or this machine stored the id
166
+ // after a rename. Other machines' renamed groups cannot be found by name.
181
167
  const kimakiCategoryIds = new Set(guildChannels
182
- .filter((ch) => ch.type === 4 && /^kimaki(\s|$)/i.test(ch.name))
168
+ .filter((ch) => ch.type === 4 &&
169
+ (/^kimaki(\s|$)/i.test(ch.name) || storedCategoryIds.has(ch.id)))
183
170
  .map((ch) => ch.id));
184
171
  // Find text channels (type 0) in Kimaki categories that are not in our local DB
185
172
  for (const ch of guildChannels) {
@@ -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';
@@ -394,6 +395,10 @@ cli
394
395
  }
395
396
  const threadPromptMarker = {
396
397
  start: true,
398
+ ...(threadTargetUser && {
399
+ userId: threadTargetUser.id,
400
+ ...(threadTargetUser.username && { username: threadTargetUser.username }),
401
+ }),
397
402
  ...(options.agent && { agent: options.agent }),
398
403
  ...(options.model && { model: options.model }),
399
404
  ...(options.permission?.length ? { permissions: options.permission } : {}),
@@ -409,7 +414,7 @@ cli
409
414
  // Prefix the prompt so it's clear who sent it (matches /queue format).
410
415
  // Use a newline between prefix and prompt so leading /command
411
416
  // detection can find the command on its own line.
412
- const prefixedPrompt = `» **kimaki-cli:**\n${prompt}`;
417
+ const prefixedPrompt = `${QUEUE_PREFIX}**kimaki-cli:**\n${prompt}`;
413
418
  if (threadTargetUser) {
414
419
  cliLogger.log(`Adding user ${threadTargetUser.username || threadTargetUser.id} to thread...`);
415
420
  const addMemberResult = await ensureThreadMember({