kimaki 0.27.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 (221) 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 +32 -0
  6. package/dist/bash-tool-schema-plugin.test.js +49 -0
  7. package/dist/cli-commands/project.js +11 -27
  8. package/dist/cli-commands/send.js +6 -1
  9. package/dist/cli-commands/session.js +139 -82
  10. package/dist/cli-commands/task.js +21 -14
  11. package/dist/cli-parsing.test.js +24 -1
  12. package/dist/cli-runner.js +16 -4
  13. package/dist/cli-send-thread.e2e.test.js +9 -9
  14. package/dist/commands/action-buttons.js +2 -1
  15. package/dist/commands/add-project.js +4 -7
  16. package/dist/commands/agent.js +158 -24
  17. package/dist/commands/ask-question.js +2 -1
  18. package/dist/commands/btw.js +28 -6
  19. package/dist/commands/file-upload.js +2 -1
  20. package/dist/commands/fork-subagent.js +2 -4
  21. package/dist/commands/fork.js +4 -4
  22. package/dist/commands/last-sessions.js +17 -10
  23. package/dist/commands/merge-worktree.js +13 -6
  24. package/dist/commands/model-variant.js +3 -2
  25. package/dist/commands/model.js +4 -3
  26. package/dist/commands/queue.js +3 -2
  27. package/dist/commands/resume.js +4 -4
  28. package/dist/commands/session.js +78 -63
  29. package/dist/cpu-profiler.js +211 -0
  30. package/dist/cpu-profiler.test.js +160 -0
  31. package/dist/database.js +28 -0
  32. package/dist/db.js +11 -0
  33. package/dist/db.test.js +100 -0
  34. package/dist/default-channel-provisioning.test.js +148 -0
  35. package/dist/discord-bot.js +501 -474
  36. package/dist/discord-command-registration.js +16 -9
  37. package/dist/discord-command-registration.test.js +17 -0
  38. package/dist/discord-utils.js +84 -0
  39. package/dist/discord-utils.test.js +135 -1
  40. package/dist/errors.js +5 -0
  41. package/dist/external-directory-permissions.e2e.test.js +11 -7
  42. package/dist/external-directory-project-deny.e2e.test.js +5 -3
  43. package/dist/external-opencode-sync.js +94 -17
  44. package/dist/external-opencode-sync.test.js +206 -0
  45. package/dist/extract-sdk-error-message.test.js +13 -0
  46. package/dist/gateway-proxy.e2e.test.js +18 -18
  47. package/dist/interaction-handler.js +416 -398
  48. package/dist/ipc-tools-plugin.js +12 -7
  49. package/dist/kimaki-opencode-plugin-loading.e2e.test.js +2 -2
  50. package/dist/kimaki-opencode-plugin.js +2 -0
  51. package/dist/logger.js +1 -0
  52. package/dist/message-formatting.js +163 -44
  53. package/dist/message-formatting.test.js +473 -13
  54. package/dist/message-preprocessing.js +105 -1
  55. package/dist/opencode-command.js +28 -0
  56. package/dist/opencode-command.test.js +49 -1
  57. package/dist/opencode-interrupt-plugin.js +4 -0
  58. package/dist/opencode-interrupt-plugin.test.js +24 -0
  59. package/dist/opencode.js +29 -3
  60. package/dist/project-add.test.js +62 -0
  61. package/dist/question-abort-clears-dropdown.e2e.test.js +1 -1
  62. package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +150 -0
  63. package/dist/question-text-before-dropdown.e2e.test.js +5 -5
  64. package/dist/queue-advanced-abort.e2e.test.js +8 -10
  65. package/dist/queue-advanced-action-buttons.e2e.test.js +8 -8
  66. package/dist/queue-advanced-footer.e2e.test.js +29 -32
  67. package/dist/queue-advanced-model-switch.e2e.test.js +9 -11
  68. package/dist/queue-advanced-permissions-typing.e2e.test.js +9 -7
  69. package/dist/queue-advanced-question.e2e.test.js +2 -2
  70. package/dist/queue-advanced-typing-interrupt.e2e.test.js +10 -12
  71. package/dist/queue-advanced-typing.e2e.test.js +7 -9
  72. package/dist/queue-delete-message.e2e.test.js +3 -3
  73. package/dist/queue-drain-after-interactive-ui.e2e.test.js +5 -5
  74. package/dist/queue-interrupt-drain.e2e.test.js +20 -20
  75. package/dist/queue-question-select-drain.e2e.test.js +7 -7
  76. package/dist/runtime-lifecycle.e2e.test.js +16 -19
  77. package/dist/schema.js +1 -1
  78. package/dist/session-handler/event-stream-state.js +124 -0
  79. package/dist/session-handler/event-stream-state.test.js +322 -1
  80. package/dist/session-handler/model-utils.js +16 -3
  81. package/dist/session-handler/model-utils.test.js +103 -3
  82. package/dist/session-handler/opencode-session-event-log.js +30 -1
  83. package/dist/session-handler/thread-ingress.test.js +41 -0
  84. package/dist/session-handler/thread-runtime-state.js +2 -0
  85. package/dist/session-handler/thread-session-runtime-summary.test.js +121 -0
  86. package/dist/session-handler/thread-session-runtime.js +340 -135
  87. package/dist/session-search.js +123 -4
  88. package/dist/session-search.test.js +248 -1
  89. package/dist/session-title-rename.test.js +30 -0
  90. package/dist/sleep.e2e.test.js +14 -11
  91. package/dist/system-message.js +45 -12
  92. package/dist/system-message.test.js +117 -10
  93. package/dist/task-runner.js +2 -1
  94. package/dist/task-schedule.js +32 -2
  95. package/dist/task-schedule.test.js +79 -1
  96. package/dist/test-utils.js +17 -9
  97. package/dist/thinking-utils.js +10 -0
  98. package/dist/thinking-utils.test.js +35 -0
  99. package/dist/thread-message-queue.e2e.test.js +65 -54
  100. package/dist/undo-redo.e2e.test.js +17 -9
  101. package/dist/voice-handler.js +21 -4
  102. package/dist/voice-message.e2e.test.js +144 -30
  103. package/dist/voice-routing-input.e2e.test.js +195 -0
  104. package/dist/voice-routing-question.e2e.test.js +125 -0
  105. package/dist/voice.js +28 -8
  106. package/dist/voice.test.js +41 -1
  107. package/dist/worktree-lifecycle.e2e.test.js +30 -30
  108. package/package.json +8 -8
  109. package/skills/bump-holocron-consumers/SKILL.md +137 -0
  110. package/skills/critique/SKILL.md +4 -4
  111. package/skills/errore/SKILL.md +56 -232
  112. package/skills/holocron/SKILL.md +117 -17
  113. package/src/agent-model.e2e.test.ts +293 -66
  114. package/src/anthropic-account-identity.ts +19 -1
  115. package/src/anthropic-auth-plugin.test.ts +19 -0
  116. package/src/anthropic-auth-plugin.ts +11 -16
  117. package/src/bash-tool-schema-plugin.test.ts +55 -0
  118. package/src/bash-tool-schema-plugin.ts +46 -0
  119. package/src/cli-commands/project.ts +14 -35
  120. package/src/cli-commands/send.ts +6 -1
  121. package/src/cli-commands/session.ts +171 -105
  122. package/src/cli-commands/task.ts +22 -15
  123. package/src/cli-parsing.test.ts +34 -1
  124. package/src/cli-runner.ts +16 -3
  125. package/src/cli-send-thread.e2e.test.ts +9 -11
  126. package/src/commands/action-buttons.ts +2 -1
  127. package/src/commands/add-project.ts +4 -8
  128. package/src/commands/agent.ts +199 -23
  129. package/src/commands/ask-question.ts +2 -1
  130. package/src/commands/btw.ts +40 -5
  131. package/src/commands/file-upload.ts +2 -1
  132. package/src/commands/fork-subagent.ts +2 -3
  133. package/src/commands/fork.ts +4 -3
  134. package/src/commands/last-sessions.ts +10 -4
  135. package/src/commands/merge-worktree.ts +13 -6
  136. package/src/commands/model-variant.ts +3 -2
  137. package/src/commands/model.ts +4 -3
  138. package/src/commands/queue.ts +3 -2
  139. package/src/commands/resume.ts +4 -3
  140. package/src/commands/session.ts +94 -70
  141. package/src/cpu-profiler.test.ts +183 -0
  142. package/src/cpu-profiler.ts +293 -0
  143. package/src/database.ts +40 -0
  144. package/src/db.test.ts +105 -0
  145. package/src/db.ts +12 -0
  146. package/src/default-channel-provisioning.test.ts +155 -0
  147. package/src/discord-bot.ts +43 -7
  148. package/src/discord-command-registration.test.ts +20 -0
  149. package/src/discord-command-registration.ts +27 -11
  150. package/src/discord-utils.test.ts +157 -0
  151. package/src/discord-utils.ts +131 -1
  152. package/src/errors.ts +7 -0
  153. package/src/external-directory-permissions.e2e.test.ts +11 -7
  154. package/src/external-directory-project-deny.e2e.test.ts +5 -3
  155. package/src/external-opencode-sync.test.ts +248 -0
  156. package/src/external-opencode-sync.ts +122 -19
  157. package/src/extract-sdk-error-message.test.ts +18 -0
  158. package/src/gateway-proxy.e2e.test.ts +18 -17
  159. package/src/interaction-handler.ts +36 -13
  160. package/src/ipc-tools-plugin.ts +16 -9
  161. package/src/kimaki-opencode-plugin-loading.e2e.test.ts +5 -5
  162. package/src/kimaki-opencode-plugin.ts +2 -0
  163. package/src/logger.ts +1 -0
  164. package/src/message-formatting.test.ts +586 -13
  165. package/src/message-formatting.ts +223 -46
  166. package/src/message-preprocessing.ts +116 -1
  167. package/src/opencode-command.test.ts +59 -0
  168. package/src/opencode-command.ts +38 -0
  169. package/src/opencode-interrupt-plugin.test.ts +32 -0
  170. package/src/opencode-interrupt-plugin.ts +3 -0
  171. package/src/opencode.ts +44 -4
  172. package/src/project-add.test.ts +80 -0
  173. package/src/question-abort-clears-dropdown.e2e.test.ts +1 -1
  174. package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +172 -0
  175. package/src/question-text-before-dropdown.e2e.test.ts +5 -5
  176. package/src/queue-advanced-abort.e2e.test.ts +8 -9
  177. package/src/queue-advanced-action-buttons.e2e.test.ts +8 -8
  178. package/src/queue-advanced-footer.e2e.test.ts +30 -31
  179. package/src/queue-advanced-model-switch.e2e.test.ts +9 -10
  180. package/src/queue-advanced-permissions-typing.e2e.test.ts +9 -7
  181. package/src/queue-advanced-question.e2e.test.ts +2 -2
  182. package/src/queue-advanced-typing-interrupt.e2e.test.ts +11 -11
  183. package/src/queue-advanced-typing.e2e.test.ts +8 -8
  184. package/src/queue-delete-message.e2e.test.ts +3 -3
  185. package/src/queue-drain-after-interactive-ui.e2e.test.ts +5 -5
  186. package/src/queue-interrupt-drain.e2e.test.ts +20 -20
  187. package/src/queue-question-select-drain.e2e.test.ts +7 -7
  188. package/src/runtime-lifecycle.e2e.test.ts +16 -18
  189. package/src/schema.sql +1 -1
  190. package/src/schema.ts +1 -1
  191. package/src/session-handler/event-stream-state.test.ts +353 -0
  192. package/src/session-handler/event-stream-state.ts +179 -0
  193. package/src/session-handler/model-utils.test.ts +125 -3
  194. package/src/session-handler/model-utils.ts +36 -4
  195. package/src/session-handler/opencode-session-event-log.ts +31 -1
  196. package/src/session-handler/thread-ingress.test.ts +54 -0
  197. package/src/session-handler/thread-runtime-state.ts +3 -0
  198. package/src/session-handler/thread-session-runtime-summary.test.ts +169 -0
  199. package/src/session-handler/thread-session-runtime.ts +427 -158
  200. package/src/session-search.test.ts +281 -0
  201. package/src/session-search.ts +241 -7
  202. package/src/session-title-rename.test.ts +45 -0
  203. package/src/sleep.e2e.test.ts +14 -11
  204. package/src/store.ts +1 -0
  205. package/src/system-message.test.ts +141 -10
  206. package/src/system-message.ts +66 -11
  207. package/src/task-runner.ts +2 -1
  208. package/src/task-schedule.test.ts +94 -0
  209. package/src/task-schedule.ts +47 -2
  210. package/src/test-utils.ts +21 -10
  211. package/src/thinking-utils.test.ts +46 -0
  212. package/src/thinking-utils.ts +21 -0
  213. package/src/thread-message-queue.e2e.test.ts +65 -53
  214. package/src/undo-redo.e2e.test.ts +17 -9
  215. package/src/voice-handler.ts +23 -4
  216. package/src/voice-message.e2e.test.ts +149 -29
  217. package/src/voice-routing-input.e2e.test.ts +203 -0
  218. package/src/voice-routing-question.e2e.test.ts +131 -0
  219. package/src/voice.test.ts +44 -0
  220. package/src/voice.ts +34 -6
  221. package/src/worktree-lifecycle.e2e.test.ts +30 -30
@@ -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
+ });
@@ -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';
@@ -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({
@@ -12,9 +12,9 @@ import { fileURLToPath } from 'node:url';
12
12
  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
- import { getBotTokenWithMode, getThreadSession, getThreadIdBySessionId, getSessionEventSnapshot, getDb, createScheduledTask, listScheduledTasks, cancelScheduledTask, getScheduledTask, updateScheduledTask, getSessionStartSourcesBySessionIds, deleteChannelDirectoryById, findChannelsByDirectory, getThreadWorktreeOrWorkspace } from '../database.js';
15
+ import { getBotTokenWithMode, getThreadSession, getThreadIdBySessionId, getSessionEventSnapshot, getDb, createScheduledTask, listScheduledTasks, cancelScheduledTask, getScheduledTask, updateScheduledTask, getSessionStartSourcesBySessionIds, deleteChannelDirectoryById, findChannelsByDirectory, getThreadWorktreeOrWorkspace, getAllTextChannelDirectories } from '../database.js';
16
16
  import { ShareMarkdown } from '../markdown.js';
17
- import { parseSessionSearchPattern, findFirstSessionSearchHit, buildSessionSearchSnippet, getPartSearchTexts } from '../session-search.js';
17
+ import { parseSessionSearchPattern, collectSessionSearchMatches, validateSessionSearchScope, resolveSessionSearchDirectories, parseSessionSearchDays, sessionSearchMinUpdated, SESSION_SEARCH_DEFAULT_DAYS } from '../session-search.js';
18
18
  import { formatWorktreeName, formatAutoWorktreeName } from '../commands/new-worktree.js';
19
19
  import { formatTimeAgo } from '../commands/worktrees.js';
20
20
  import { editorsForFile, loadFileEditEvents } from '../file-edit-log.js';
@@ -319,16 +319,26 @@ cli
319
319
  }
320
320
  });
321
321
  cli
322
- .command('session search <query>', 'Search past sessions for text or /regex/flags in the selected project')
322
+ .command('session search <query>', `Search past sessions for text or /regex/flags. Defaults to the last ${SESSION_SEARCH_DEFAULT_DAYS} days; use --days 0 for all time. Add --all for every locally registered project.`)
323
323
  .option('--project <path>', 'Project directory (defaults to cwd)')
324
324
  .option('--channel <channelId>', 'Resolve project from a Discord channel ID')
325
+ .option('--all', 'Search every locally registered project')
326
+ .option('--days <n>', `Only search sessions updated in the last n days (default: ${SESSION_SEARCH_DEFAULT_DAYS}; 0 = all time)`)
325
327
  .option('--limit <n>', 'Maximum matched sessions to return (default: 20)')
326
328
  .option('--json', 'Output as JSON')
329
+ .example('kimaki session search "auth timeout"')
330
+ .example('kimaki session search "auth timeout" --days 0')
331
+ .example('kimaki session search "auth timeout" --all')
327
332
  .action(async (query, options) => {
328
333
  try {
329
334
  await initDatabase();
330
- if (options.project && options.channel) {
331
- cliLogger.error('Use either --project or --channel, not both');
335
+ const scopeError = validateSessionSearchScope({
336
+ all: options.all,
337
+ project: options.project,
338
+ channel: options.channel,
339
+ });
340
+ if (scopeError) {
341
+ cliLogger.error(scopeError.message);
332
342
  process.exit(EXIT_NO_RESTART);
333
343
  }
334
344
  const limit = (() => {
@@ -343,7 +353,16 @@ cli
343
353
  cliLogger.error(limit.message);
344
354
  process.exit(EXIT_NO_RESTART);
345
355
  }
346
- const projectDirectoryResult = await (async () => {
356
+ const days = parseSessionSearchDays(typeof options.days === 'string' ? options.days : undefined);
357
+ if (days instanceof Error) {
358
+ cliLogger.error(days.message);
359
+ process.exit(EXIT_NO_RESTART);
360
+ }
361
+ const minUpdated = sessionSearchMinUpdated({ days });
362
+ const explicitDirectory = await (async () => {
363
+ if (options.all) {
364
+ return undefined;
365
+ }
347
366
  if (options.channel) {
348
367
  const channelConfig = await getChannelDirectory(options.channel);
349
368
  if (!channelConfig) {
@@ -351,15 +370,45 @@ cli
351
370
  }
352
371
  return path.resolve(channelConfig.directory);
353
372
  }
354
- return path.resolve(options.project || '.');
373
+ if (options.project) {
374
+ return path.resolve(options.project);
375
+ }
376
+ return undefined;
355
377
  })();
356
- if (projectDirectoryResult instanceof Error) {
357
- cliLogger.error(projectDirectoryResult.message);
378
+ if (explicitDirectory instanceof Error) {
379
+ cliLogger.error(explicitDirectory.message);
380
+ process.exit(EXIT_NO_RESTART);
381
+ }
382
+ const registeredDirectories = options.all
383
+ ? (await getAllTextChannelDirectories()).map((directory) => {
384
+ return path.resolve(directory);
385
+ })
386
+ : [];
387
+ const projectDirectories = resolveSessionSearchDirectories({
388
+ all: Boolean(options.all),
389
+ registeredDirectories,
390
+ cwd: path.resolve('.'),
391
+ explicitDirectory,
392
+ });
393
+ if (projectDirectories instanceof Error) {
394
+ cliLogger.error(projectDirectories.message);
395
+ process.exit(EXIT_NO_RESTART);
396
+ }
397
+ const existingDirectories = [];
398
+ for (const directory of projectDirectories) {
399
+ if (fs.existsSync(directory)) {
400
+ existingDirectories.push(directory);
401
+ continue;
402
+ }
403
+ if (options.all) {
404
+ cliLogger.warn(`Skipping missing directory: ${directory}`);
405
+ continue;
406
+ }
407
+ cliLogger.error(`Directory does not exist: ${directory}`);
358
408
  process.exit(EXIT_NO_RESTART);
359
409
  }
360
- const projectDirectory = projectDirectoryResult;
361
- if (!fs.existsSync(projectDirectory)) {
362
- cliLogger.error(`Directory does not exist: ${projectDirectory}`);
410
+ if (existingDirectories.length === 0) {
411
+ cliLogger.error('No searchable project directories found. Add a project with `kimaki project add`, or pass --project.');
363
412
  process.exit(EXIT_NO_RESTART);
364
413
  }
365
414
  const searchPattern = parseSessionSearchPattern(query);
@@ -367,15 +416,43 @@ cli
367
416
  cliLogger.error(searchPattern.message);
368
417
  process.exit(EXIT_NO_RESTART);
369
418
  }
370
- cliLogger.log('Connecting to OpenCode server...');
371
- const getClient = await initializeOpencodeForDirectory(projectDirectory);
372
- if (getClient instanceof Error) {
373
- cliLogger.error('Failed to connect to OpenCode:', getClient.message);
374
- process.exit(EXIT_NO_RESTART);
419
+ const clientsBySessionId = new Map();
420
+ const searchedDirectories = [];
421
+ const searchableSessions = [];
422
+ const listedDirectories = await Promise.all(existingDirectories.map(async (projectDirectory) => {
423
+ cliLogger.log(`Connecting to OpenCode server for ${projectDirectory}...`);
424
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
425
+ if (getClient instanceof Error) {
426
+ return { projectDirectory, getClient, sessions: [] };
427
+ }
428
+ const sessionsResponse = await getClient().session.list();
429
+ return {
430
+ projectDirectory,
431
+ getClient,
432
+ sessions: sessionsResponse.data || [],
433
+ };
434
+ }));
435
+ for (const listed of listedDirectories) {
436
+ if (listed.getClient instanceof Error) {
437
+ if (options.all) {
438
+ cliLogger.warn(`Skipping ${listed.projectDirectory}: failed to connect to OpenCode: ${listed.getClient.message}`);
439
+ continue;
440
+ }
441
+ cliLogger.error('Failed to connect to OpenCode:', listed.getClient.message);
442
+ process.exit(EXIT_NO_RESTART);
443
+ }
444
+ searchedDirectories.push(listed.projectDirectory);
445
+ for (const session of listed.sessions) {
446
+ clientsBySessionId.set(session.id, listed.getClient);
447
+ searchableSessions.push({
448
+ id: session.id,
449
+ title: session.title || 'Untitled Session',
450
+ directory: session.directory || listed.projectDirectory,
451
+ updated: session.time.updated,
452
+ });
453
+ }
375
454
  }
376
- const sessionsResponse = await getClient().session.list();
377
- const sessions = sessionsResponse.data || [];
378
- if (sessions.length === 0) {
455
+ if (searchableSessions.length === 0) {
379
456
  cliLogger.log('No sessions found');
380
457
  process.exit(0);
381
458
  }
@@ -386,82 +463,62 @@ cli
386
463
  const sessionToThread = new Map(threadSessions
387
464
  .filter((row) => row.session_id !== '')
388
465
  .map((row) => [row.session_id, row.thread_id]));
389
- const sortedSessions = [...sessions].sort((a, b) => {
390
- return b.time.updated - a.time.updated;
391
- });
392
- const matchedSessions = [];
393
- let scannedSessions = 0;
394
- for (const session of sortedSessions) {
395
- scannedSessions++;
396
- const messagesResponse = await getClient().session.messages({
397
- sessionID: session.id,
466
+ const scopeLabel = options.all
467
+ ? `${searchedDirectories.length} project(s)`
468
+ : searchedDirectories[0] || path.resolve('.');
469
+ const daysLabel = days === 0 ? 'all time' : `the last ${days} day${days === 1 ? '' : 's'}`;
470
+ const printMatch = (match) => {
471
+ const threadInfo = match.threadId ? ` | thread: ${match.threadId}` : '';
472
+ console.log(`${match.id} | ${match.title} | ${match.updated} | ${match.source}${threadInfo}`);
473
+ console.log(` Directory: ${match.directory}`);
474
+ match.snippets.forEach((snippet) => {
475
+ console.log(` - ${snippet}`);
398
476
  });
399
- const messages = messagesResponse.data || [];
400
- const snippets = messages
401
- .flatMap((message) => {
402
- const rolePrefix = message.info.role === 'assistant'
403
- ? 'assistant'
404
- : message.info.role === 'user'
405
- ? 'user'
406
- : 'message';
407
- return message.parts.filter((p) => !(p.type === 'text' && p.synthetic)).flatMap((part) => {
408
- return getPartSearchTexts(part).flatMap((text) => {
409
- const hit = findFirstSessionSearchHit({
410
- text,
411
- searchPattern,
412
- });
413
- if (!hit) {
414
- return [];
415
- }
416
- const snippet = buildSessionSearchSnippet({ text, hit });
417
- if (!snippet) {
418
- return [];
419
- }
420
- return [`${rolePrefix}: ${snippet}`];
421
- });
477
+ };
478
+ let printedHeader = false;
479
+ const { matches: matchedSessions, scannedSessions } = await collectSessionSearchMatches({
480
+ sessions: searchableSessions,
481
+ searchPattern,
482
+ sessionToThread,
483
+ limit,
484
+ minUpdated,
485
+ onMatch: options.json
486
+ ? undefined
487
+ : (match) => {
488
+ if (!printedHeader) {
489
+ printedHeader = true;
490
+ cliLogger.log(`Found matching session(s) for ${searchPattern.raw} in ${scopeLabel} (${daysLabel})`);
491
+ }
492
+ printMatch(match);
493
+ },
494
+ loadMessages: async (session) => {
495
+ const getClient = clientsBySessionId.get(session.id);
496
+ if (!getClient) {
497
+ return [];
498
+ }
499
+ const messagesResponse = await getClient().session.messages({
500
+ sessionID: session.id,
422
501
  });
423
- })
424
- .slice(0, 3);
425
- if (snippets.length === 0) {
426
- continue;
427
- }
428
- const threadId = sessionToThread.get(session.id);
429
- matchedSessions.push({
430
- id: session.id,
431
- title: session.title || 'Untitled Session',
432
- directory: session.directory,
433
- updated: new Date(session.time.updated).toISOString(),
434
- source: threadId ? 'kimaki' : 'opencode',
435
- threadId: threadId || null,
436
- snippets,
437
- });
438
- if (matchedSessions.length >= limit) {
439
- break;
440
- }
441
- }
502
+ return messagesResponse.data || [];
503
+ },
504
+ });
442
505
  if (options.json) {
443
506
  console.log(JSON.stringify({
444
507
  query: searchPattern.raw,
445
508
  mode: searchPattern.mode,
446
- projectDirectory,
509
+ all: Boolean(options.all),
510
+ days,
511
+ projectDirectories: searchedDirectories,
447
512
  scannedSessions,
448
513
  matches: matchedSessions,
449
514
  }, null, 2));
450
515
  process.exit(0);
451
516
  }
452
517
  if (matchedSessions.length === 0) {
453
- cliLogger.log(`No matches found for ${searchPattern.raw} in ${projectDirectory} (${scannedSessions} sessions scanned)`);
518
+ const cutoffHint = days === 0 ? '' : '. Use --days 0 to search all time';
519
+ cliLogger.log(`No matches found for ${searchPattern.raw} in ${scopeLabel} (${scannedSessions} sessions scanned, ${daysLabel})${cutoffHint}`);
454
520
  process.exit(0);
455
521
  }
456
- cliLogger.log(`Found ${matchedSessions.length} matching session(s) for ${searchPattern.raw} in ${projectDirectory}`);
457
- for (const match of matchedSessions) {
458
- const threadInfo = match.threadId ? ` | thread: ${match.threadId}` : '';
459
- console.log(`${match.id} | ${match.title} | ${match.updated} | ${match.source}${threadInfo}`);
460
- console.log(` Directory: ${match.directory}`);
461
- match.snippets.forEach((snippet) => {
462
- console.log(` - ${snippet}`);
463
- });
464
- }
465
522
  process.exit(0);
466
523
  }
467
524
  catch (error) {
@@ -22,7 +22,7 @@ import { archiveThread, uploadFilesToDiscord, stripMentions } from '../discord-u
22
22
  import { setDataDir, setProjectsDir, getDataDir, getProjectsDir } from '../config.js';
23
23
  import { execAsync, validateWorktreeDirectory } from '../worktrees.js';
24
24
  import { upgrade, getCurrentVersion } from '../upgrade.js';
25
- import { getPromptPreview, parseSendAtValue, parseScheduledTaskPayload, serializeScheduledTaskPayload } from '../task-schedule.js';
25
+ import { applyScheduledTaskUserEdit, getPromptPreview, parseSendAtValue, parseScheduledTaskPayload, serializeScheduledTaskPayload } from '../task-schedule.js';
26
26
  import { EXIT_NO_RESTART, formatMemberLookupUnavailableMessage, formatRelativeTime, formatTaskScheduleLine, getDiscordUserIdFromUserOption, isDiscordMemberLookupUnavailable, isGuildMemberSearchResult, isThreadChannelType, printDiscordInstallUrlAndExit, resolveBotCredentials, resolveDiscordUserOption, sendDiscordMessageWithOptionalAttachment, } from '../cli-runner.js';
27
27
  import { validateCliModelOption } from '../session-handler/model-utils.js';
28
28
  const cliLogger = createLogger(LogPrefix.CLI);
@@ -135,7 +135,7 @@ cli
135
135
  .option('--model <model>', 'Model for the scheduled session, format provider/model (empty string clears)')
136
136
  .option('--pre-run <command>', 'New pre-run command (empty string clears)')
137
137
  .option('--allow-concurrency <enabled>', z.enum(['true', 'false']).describe('Allow concurrent sessions: true or false'))
138
- .option('-u, --user <user>', 'Discord user ID, mention, or username added to the task thread')
138
+ .option('-u, --user <user>', 'Discord user ID, mention, or username added to the task thread (empty string clears)')
139
139
  .action(async (id, options) => {
140
140
  try {
141
141
  const trimmedPrompt = options.prompt === undefined ? undefined : options.prompt.trim();
@@ -143,9 +143,10 @@ cli
143
143
  const hasModel = options.model !== undefined;
144
144
  const hasPreRun = options.preRun !== undefined;
145
145
  const hasAllowConcurrency = options.allowConcurrency !== undefined;
146
+ const hasUser = options.user !== undefined;
146
147
  if (!trimmedPrompt &&
147
148
  !options.sendAt &&
148
- !options.user &&
149
+ !hasUser &&
149
150
  !hasAgent &&
150
151
  !hasModel &&
151
152
  !hasPreRun &&
@@ -181,7 +182,7 @@ cli
181
182
  cliLogger.error(`Failed to parse task payload: ${existingPayload.message}`);
182
183
  process.exit(EXIT_NO_RESTART);
183
184
  }
184
- const resolvedUser = options.user
185
+ const resolvedUser = hasUser && options.user.trim()
185
186
  ? await resolveTaskUser({
186
187
  user: options.user,
187
188
  channelId: task.channel_id,
@@ -207,9 +208,6 @@ cli
207
208
  const updatedPayload = {
208
209
  ...existingPayload,
209
210
  prompt: newPrompt,
210
- ...(resolvedUser
211
- ? { userId: resolvedUser.id, username: resolvedUser.username || null }
212
- : {}),
213
211
  ...(hasAgent ? { agent: options.agent.trim() || null } : {}),
214
212
  ...(hasModel ? { model: nextModel } : {}),
215
213
  ...(hasPreRun ? { preRunCommand: options.preRun.trim() || null } : {}),
@@ -217,9 +215,16 @@ cli
217
215
  ? { allowConcurrency: options.allowConcurrency === 'true' }
218
216
  : {}),
219
217
  };
218
+ const payloadWithUser = hasUser
219
+ ? applyScheduledTaskUserEdit({
220
+ payload: updatedPayload,
221
+ userOption: options.user,
222
+ resolvedUser,
223
+ })
224
+ : updatedPayload;
220
225
  const updateData = {
221
226
  taskId,
222
- payloadJson: serializeScheduledTaskPayload(updatedPayload),
227
+ payloadJson: serializeScheduledTaskPayload(payloadWithUser),
223
228
  promptPreview: getPromptPreview(newPrompt),
224
229
  };
225
230
  if (options.sendAt) {
@@ -245,19 +250,21 @@ cli
245
250
  process.exit(EXIT_NO_RESTART);
246
251
  }
247
252
  const parts = [`Updated task ${taskId}`];
248
- if (resolvedUser) {
249
- parts.push(`user ${resolvedUser.username || resolvedUser.id} will be added to the thread`);
253
+ if (hasUser) {
254
+ parts.push(payloadWithUser.userId
255
+ ? `user ${payloadWithUser.username || payloadWithUser.userId} will be added to the thread`
256
+ : 'user cleared; nobody will be added to the thread');
250
257
  }
251
258
  if (hasAgent) {
252
- parts.push(`agent=${updatedPayload.agent || '-'}`);
259
+ parts.push(`agent=${payloadWithUser.agent || '-'}`);
253
260
  }
254
261
  if (hasModel) {
255
- parts.push(`model=${updatedPayload.model || '-'}`);
262
+ parts.push(`model=${payloadWithUser.model || '-'}`);
256
263
  }
257
264
  if (hasPreRun)
258
- parts.push(`preRun=${updatedPayload.preRunCommand || '-'}`);
265
+ parts.push(`preRun=${payloadWithUser.preRunCommand || '-'}`);
259
266
  if (hasAllowConcurrency) {
260
- parts.push(`allowConcurrency=${updatedPayload.allowConcurrency}`);
267
+ parts.push(`allowConcurrency=${payloadWithUser.allowConcurrency}`);
261
268
  }
262
269
  cliLogger.log(parts.join(' | '));
263
270
  process.exit(0);
@@ -9,10 +9,11 @@ async function parseWithGoke(argv) {
9
9
  "cli.command('send', 'Send a message').option('-c, --channel <channelId>', 'Discord channel ID').option('--thread <threadId>', 'Thread ID').option('--session <sessionId>', 'Session ID').option('--send-at <schedule>', 'Schedule')",
10
10
  "cli.command('session archive <threadId>', 'Archive a thread')",
11
11
  "cli.command('session title <title>', 'Update title').option('--session <sessionId>', 'Session ID').option('--thread <threadId>', 'Thread ID')",
12
- "cli.command('session search <query>', 'Search sessions').option('--channel <channelId>', 'Discord channel ID').option('--project <path>', 'Project path')",
12
+ "cli.command('session search <query>', 'Search sessions').option('--channel <channelId>', 'Discord channel ID').option('--project <path>', 'Project path').option('--all', 'Search all registered projects').option('--days <n>', 'Only search recent sessions')",
13
13
  "cli.command('session export-events-jsonl', 'Export in-memory events to JSONL').option('--session <sessionId>', 'Session ID').option('--out <file>', 'Output path')",
14
14
  "cli.command('add-project', 'Add a project').option('-g, --guild <guildId>', 'Discord guild/server ID')",
15
15
  "cli.command('task delete <id>', 'Delete task')",
16
+ "cli.command('task edit <id>', 'Edit task').option('-u, --user <user>', 'Discord user')",
16
17
  "cli.command('multioauth anthropic list', 'List stored Anthropic accounts')",
17
18
  "cli.command('multioauth anthropic remove <indexOrEmail>', 'Remove stored Anthropic account')",
18
19
  "cli.command('multioauth openai list', 'List stored OpenAI accounts')",
@@ -112,6 +113,15 @@ describe('goke CLI ID parsing', () => {
112
113
  expect(result.options.channel).toBe(channelId);
113
114
  expect(typeof result.options.channel).toBe('string');
114
115
  });
116
+ test('parses session search --all as a boolean', async () => {
117
+ const result = await parseWithGoke(['node', 'kimaki', 'session', 'search', 'auth timeout', '--all']);
118
+ expect(result.args[0]).toBe('auth timeout');
119
+ expect(result.options.all).toBe(true);
120
+ });
121
+ test('parses session search --days as a string', async () => {
122
+ const result = await parseWithGoke(['node', 'kimaki', 'session', 'search', 'auth timeout', '--days', '0']);
123
+ expect(result.options.days).toBe('0');
124
+ });
115
125
  test('keeps session export options as strings', async () => {
116
126
  const sessionId = '001111222233334444';
117
127
  const outPath = './tmp/session-events.jsonl';
@@ -142,6 +152,19 @@ describe('goke CLI ID parsing', () => {
142
152
  expect(result.args[0]).toBe(taskId);
143
153
  expect(typeof result.args[0]).toBe('string');
144
154
  });
155
+ test('parses empty --user on task edit so the stored user can be cleared', async () => {
156
+ const result = await parseWithGoke([
157
+ 'node',
158
+ 'kimaki',
159
+ 'task',
160
+ 'edit',
161
+ '11',
162
+ '--user',
163
+ '',
164
+ ]);
165
+ expect(result.args[0]).toBe('11');
166
+ expect(result.options.user).toBe('');
167
+ });
145
168
  test('multioauth account remove parses index and email as strings', async () => {
146
169
  const indexResult = await parseWithGoke(['node', 'kimaki', 'multioauth', 'anthropic', 'remove', '2']);
147
170
  const emailResult = await parseWithGoke(['node', 'kimaki', 'multioauth', 'openai', 'remove', 'user@example.com']);
@@ -2,7 +2,7 @@
2
2
  // Keeps cli.ts focused on command composition while preserving the bot onboarding flow.
3
3
  import { intro, outro, text, password, note, cancel, isCancel, confirm, log, multiselect, select, spinner, } from '@clack/prompts';
4
4
  import { deduplicateByKey, generateBotInstallUrl, generateDiscordInstallUrlForBot, KIMAKI_GATEWAY_APP_ID, KIMAKI_WEBSITE_URL, abbreviatePath, } from './utils.js';
5
- import { getChannelsWithDescriptions, createDiscordClient, initDatabase, getChannelDirectory, startDiscordBot, initializeOpencodeForDirectory, createProjectChannels, createDefaultKimakiChannel, } from './discord-bot.js';
5
+ import { getChannelsWithDescriptions, createDiscordClient, initDatabase, getChannelDirectory, startDiscordBot, initializeOpencodeForDirectory, assertCompatibleOpencodeVersion, createProjectChannels, createDefaultKimakiChannel, } from './discord-bot.js';
6
6
  import { getBotTokenWithMode, ensureServiceAuthToken, setBotToken, setBotMode, setChannelDirectory, findChannelsByDirectory, getDb, } from './database.js';
7
7
  import * as orm from 'drizzle-orm';
8
8
  import * as dbSchema from './schema.js';
@@ -767,16 +767,23 @@ export function showReadyMessage({ kimakiChannels, createdChannels, }) {
767
767
  note('Leave this process running to keep the bot active.\n\nIf you close this process or restart your machine, run `npx kimaki` again to start the bot.', '⚠️ Keep Running');
768
768
  }
769
769
  /**
770
- * Create the default kimaki channel in each guild and send a welcome message.
770
+ * Create default channels in locally configured guilds, or proxy-authorized gateway guilds.
771
771
  * Idempotent: skips guilds that already have the channel.
772
772
  * Extracted so both the interactive and headless startup paths share the same logic.
773
773
  */
774
774
  export async function ensureDefaultChannelsWithWelcome({ guilds, discordClient, appId, isGatewayMode, installerDiscordUserId, }) {
775
775
  if (process.env['KIMAKI_NO_DEFAULT_CHANNEL'] === '1')
776
776
  return [];
777
+ const localMappings = isGatewayMode ? [] : await findChannelsByDirectory({});
777
778
  const created = [];
778
779
  for (const guild of guilds) {
779
780
  try {
781
+ if (!isGatewayMode) {
782
+ // Match live channel IDs: older local mappings have no guild_id.
783
+ const channels = await guild.channels.fetch();
784
+ if (!localMappings.some((row) => channels.has(row.channel_id)))
785
+ continue;
786
+ }
780
787
  const result = await createDefaultKimakiChannel({
781
788
  guild,
782
789
  botName: discordClient.user?.username,
@@ -1148,6 +1155,11 @@ export async function run({ restartOnboarding, addChannels, useWorktrees, enable
1148
1155
  possiblePathsWindows: ['~\\.bun\\bin\\bun.exe'],
1149
1156
  }),
1150
1157
  ]);
1158
+ const opencodeVersionCheck = await assertCompatibleOpencodeVersion();
1159
+ if (opencodeVersionCheck instanceof Error) {
1160
+ cliLogger.error(opencodeVersionCheck.message);
1161
+ process.exit(EXIT_NO_RESTART);
1162
+ }
1151
1163
  if (store.getState().autoUpgradeEnabled) {
1152
1164
  void backgroundUpgradeKimaki();
1153
1165
  }
@@ -1351,7 +1363,7 @@ export async function run({ restartOnboarding, addChannels, useWorktrees, enable
1351
1363
  catch (error) {
1352
1364
  cliLogger.warn('Background channel sync failed:', error instanceof Error ? error.stack : String(error));
1353
1365
  }
1354
- // Create default kimaki channel + welcome message in each guild.
1366
+ // Create default channels only in locally configured or gateway-authorized guilds.
1355
1367
  // Runs after channel sync so existing channels are detected correctly.
1356
1368
  try {
1357
1369
  await ensureDefaultChannelsWithWelcome({
@@ -1506,7 +1518,7 @@ export async function run({ restartOnboarding, addChannels, useWorktrees, enable
1506
1518
  }
1507
1519
  }
1508
1520
  // Create default kimaki channel for general-purpose tasks.
1509
- // Runs for every guild the bot is in, idempotent (skips if already exists).
1521
+ // Only locally configured or gateway-authorized guilds are eligible.
1510
1522
  const defaultChannelResults = await ensureDefaultChannelsWithWelcome({
1511
1523
  guilds,
1512
1524
  discordClient,