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,6 +1,7 @@
1
1
  // Session inspection and archival terminal commands.
2
2
  import { goke } from 'goke';
3
3
  import { z } from 'zod';
4
+ import dedent from 'string-dedent';
4
5
  import { note } from '@clack/prompts';
5
6
  import YAML from 'yaml';
6
7
  import * as errore from 'errore';
@@ -11,14 +12,17 @@ import { fileURLToPath } from 'node:url';
11
12
  import { spawn, execSync } from 'node:child_process';
12
13
  import { createLogger, LogPrefix, initLogFile } from '../logger.js';
13
14
  import { createDiscordClient, initDatabase, getChannelDirectory, initializeOpencodeForDirectory, createProjectChannels } from '../discord-bot.js';
14
- 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';
15
16
  import { ShareMarkdown } from '../markdown.js';
16
- 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';
17
18
  import { formatWorktreeName, formatAutoWorktreeName } from '../commands/new-worktree.js';
19
+ import { formatTimeAgo } from '../commands/worktrees.js';
20
+ import { editorsForFile, loadFileEditEvents } from '../file-edit-log.js';
18
21
  import { WORKTREE_PREFIX } from '../commands/merge-worktree.js';
19
22
  import { buildOpencodeEventLogLine } from '../session-handler/opencode-session-event-log.js';
20
23
  import { createDiscordRest } from '../discord-urls.js';
21
24
  import { archiveThread, uploadFilesToDiscord, stripMentions } from '../discord-utils.js';
25
+ import { OpenCodeSdkError } from '../errors.js';
22
26
  import { setDataDir, setProjectsDir, getDataDir, getProjectsDir } from '../config.js';
23
27
  import { execAsync, validateWorktreeDirectory } from '../worktrees.js';
24
28
  import { upgrade, getCurrentVersion } from '../upgrade.js';
@@ -53,6 +57,8 @@ async function resolveSessionDirectoryFromDatabase({ sessionId, }) {
53
57
  cli
54
58
  .command('session list', 'List all OpenCode sessions, marking which were started via Kimaki')
55
59
  .option('--project <path>', 'Project directory to list sessions for (defaults to cwd)')
60
+ .option('--active', 'Only list active sessions; exits 1 when none remain')
61
+ .option('--exclude <sessionId>', 'Exclude one session ID from the results')
56
62
  .option('--json', 'Output as JSON')
57
63
  .action(async (options) => {
58
64
  try {
@@ -66,9 +72,32 @@ cli
66
72
  }
67
73
  const sessionsResponse = await getClient().session.list();
68
74
  const sessions = sessionsResponse.data || [];
69
- if (sessions.length === 0) {
70
- cliLogger.log('No sessions found');
71
- process.exit(0);
75
+ const statuses = await (async () => {
76
+ if (!options.active)
77
+ return null;
78
+ const response = await getClient().session.status({
79
+ directory: projectDirectory,
80
+ });
81
+ if (response.error) {
82
+ cliLogger.error('Failed to list active sessions');
83
+ process.exit(EXIT_NO_RESTART);
84
+ }
85
+ return response.data || {};
86
+ })();
87
+ const selectedSessions = sessions.filter((session) => {
88
+ if (session.id === options.exclude)
89
+ return false;
90
+ if (!options.active)
91
+ return true;
92
+ const status = statuses?.[session.id];
93
+ return Boolean(status && status.type !== 'idle');
94
+ });
95
+ if (selectedSessions.length === 0) {
96
+ if (options.json)
97
+ console.log('[]');
98
+ else
99
+ cliLogger.log(options.active ? 'No active sessions found' : 'No sessions found');
100
+ process.exit(options.active ? 1 : 0);
72
101
  }
73
102
  // Look up which sessions were started via kimaki (have a thread mapping)
74
103
  const db = await getDb();
@@ -78,7 +107,7 @@ cli
78
107
  const sessionToThread = new Map(threadSessions
79
108
  .filter((row) => row.session_id !== '')
80
109
  .map((row) => [row.session_id, row.thread_id]));
81
- const sessionStartSources = await getSessionStartSourcesBySessionIds(sessions.map((session) => session.id));
110
+ const sessionStartSources = await getSessionStartSourcesBySessionIds(selectedSessions.map((session) => session.id));
82
111
  const scheduleModeLabel = ({ scheduleKind, }) => {
83
112
  if (scheduleKind === 'at') {
84
113
  return 'delay';
@@ -86,7 +115,7 @@ cli
86
115
  return 'cron';
87
116
  };
88
117
  if (options.json) {
89
- const output = sessions.map((session) => {
118
+ const output = selectedSessions.map((session) => {
90
119
  const startSource = sessionStartSources.get(session.id);
91
120
  const startedBy = startSource
92
121
  ? `scheduled-${scheduleModeLabel({ scheduleKind: startSource.schedule_kind })}`
@@ -100,12 +129,13 @@ cli
100
129
  threadId: sessionToThread.get(session.id) || null,
101
130
  startedBy,
102
131
  scheduledTaskId: startSource?.scheduled_task_id || null,
132
+ status: options.active ? statuses?.[session.id]?.type || 'busy' : undefined,
103
133
  };
104
134
  });
105
135
  console.log(JSON.stringify(output, null, 2));
106
136
  process.exit(0);
107
137
  }
108
- for (const session of sessions) {
138
+ for (const session of selectedSessions) {
109
139
  const threadId = sessionToThread.get(session.id);
110
140
  const startSource = sessionStartSources.get(session.id);
111
141
  const source = threadId ? '(kimaki)' : '(opencode)';
@@ -114,7 +144,10 @@ cli
114
144
  : '';
115
145
  const updatedAt = new Date(session.time.updated).toISOString();
116
146
  const threadInfo = threadId ? ` | thread: ${threadId}` : '';
117
- console.log(`${session.id} | ${session.title || 'Untitled Session'} | ${session.directory} | ${updatedAt} | ${source}${threadInfo}${startedBy}`);
147
+ const statusInfo = options.active
148
+ ? ` | status: ${statuses?.[session.id]?.type || 'busy'}`
149
+ : '';
150
+ console.log(`${session.id} | ${session.title || 'Untitled Session'} | ${session.directory} | ${updatedAt} | ${source}${threadInfo}${startedBy}${statusInfo}`);
118
151
  }
119
152
  process.exit(0);
120
153
  }
@@ -123,6 +156,79 @@ cli
123
156
  process.exit(EXIT_NO_RESTART);
124
157
  }
125
158
  });
159
+ cli
160
+ .command('session editors <file>', dedent `
161
+ List sessions that last edited a file, newest first.
162
+
163
+ Use this before a commit in another session so the \`Session:\` line
164
+ uses the session that actually edited the file.
165
+ `)
166
+ .option('--json', 'Output as JSON')
167
+ .option('--limit <n>', z.number().default(20).describe('Max sessions to show'))
168
+ .example('kimaki session editors src/cli.ts')
169
+ .example('kimaki session editors src/cli.ts --json')
170
+ .action(async (file, options, { console, process }) => {
171
+ try {
172
+ const cwd = process.cwd;
173
+ const loaded = loadFileEditEvents({ dataDir: getDataDir() });
174
+ if (loaded instanceof Error) {
175
+ console.error(loaded.message);
176
+ process.exit(EXIT_NO_RESTART);
177
+ return;
178
+ }
179
+ const editors = editorsForFile({
180
+ events: loaded,
181
+ filePath: file,
182
+ cwd,
183
+ }).slice(0, options.limit);
184
+ if (editors.length === 0) {
185
+ console.error(`No recorded editors for ${path.resolve(cwd, file)}`);
186
+ process.exit(1);
187
+ return;
188
+ }
189
+ const titles = new Map();
190
+ try {
191
+ await initDatabase();
192
+ const db = await getDb();
193
+ const sessionRows = await db.query.thread_sessions.findMany({
194
+ columns: { session_id: true, last_synced_name: true },
195
+ where: { session_id: { in: editors.map((editor) => editor.sessionId) } },
196
+ orderBy: { updated_at: 'desc' },
197
+ });
198
+ for (const row of sessionRows) {
199
+ if (!titles.has(row.session_id) && row.last_synced_name) {
200
+ titles.set(row.session_id, row.last_synced_name);
201
+ }
202
+ }
203
+ }
204
+ catch (error) {
205
+ console.error(error instanceof Error ? error.message : String(error));
206
+ }
207
+ const rows = editors.map((editor) => {
208
+ const title = titles.get(editor.sessionId) || '-';
209
+ const editedAt = new Date(editor.at);
210
+ return {
211
+ sessionId: editor.sessionId,
212
+ title,
213
+ editedAt: editedAt.toISOString(),
214
+ ago: formatTimeAgo(editedAt),
215
+ };
216
+ });
217
+ if (options.json) {
218
+ console.log(JSON.stringify(rows, null, 2));
219
+ process.exit(0);
220
+ return;
221
+ }
222
+ for (const row of rows) {
223
+ console.log(`${row.sessionId} | ${row.title} | ${row.ago}`);
224
+ }
225
+ process.exit(0);
226
+ }
227
+ catch (error) {
228
+ console.error(error instanceof Error ? error.stack : String(error));
229
+ process.exit(EXIT_NO_RESTART);
230
+ }
231
+ });
126
232
  cli
127
233
  .command('session read <sessionId>', 'Read a session conversation as markdown (pipe to file to grep)')
128
234
  .option('--project <path>', 'Project directory (defaults to cwd)')
@@ -213,16 +319,26 @@ cli
213
319
  }
214
320
  });
215
321
  cli
216
- .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.`)
217
323
  .option('--project <path>', 'Project directory (defaults to cwd)')
218
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)`)
219
327
  .option('--limit <n>', 'Maximum matched sessions to return (default: 20)')
220
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')
221
332
  .action(async (query, options) => {
222
333
  try {
223
334
  await initDatabase();
224
- if (options.project && options.channel) {
225
- 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);
226
342
  process.exit(EXIT_NO_RESTART);
227
343
  }
228
344
  const limit = (() => {
@@ -237,7 +353,16 @@ cli
237
353
  cliLogger.error(limit.message);
238
354
  process.exit(EXIT_NO_RESTART);
239
355
  }
240
- 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
+ }
241
366
  if (options.channel) {
242
367
  const channelConfig = await getChannelDirectory(options.channel);
243
368
  if (!channelConfig) {
@@ -245,15 +370,45 @@ cli
245
370
  }
246
371
  return path.resolve(channelConfig.directory);
247
372
  }
248
- return path.resolve(options.project || '.');
373
+ if (options.project) {
374
+ return path.resolve(options.project);
375
+ }
376
+ return undefined;
249
377
  })();
250
- if (projectDirectoryResult instanceof Error) {
251
- cliLogger.error(projectDirectoryResult.message);
378
+ if (explicitDirectory instanceof Error) {
379
+ cliLogger.error(explicitDirectory.message);
252
380
  process.exit(EXIT_NO_RESTART);
253
381
  }
254
- const projectDirectory = projectDirectoryResult;
255
- if (!fs.existsSync(projectDirectory)) {
256
- cliLogger.error(`Directory does not exist: ${projectDirectory}`);
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}`);
408
+ process.exit(EXIT_NO_RESTART);
409
+ }
410
+ if (existingDirectories.length === 0) {
411
+ cliLogger.error('No searchable project directories found. Add a project with `kimaki project add`, or pass --project.');
257
412
  process.exit(EXIT_NO_RESTART);
258
413
  }
259
414
  const searchPattern = parseSessionSearchPattern(query);
@@ -261,15 +416,43 @@ cli
261
416
  cliLogger.error(searchPattern.message);
262
417
  process.exit(EXIT_NO_RESTART);
263
418
  }
264
- cliLogger.log('Connecting to OpenCode server...');
265
- const getClient = await initializeOpencodeForDirectory(projectDirectory);
266
- if (getClient instanceof Error) {
267
- cliLogger.error('Failed to connect to OpenCode:', getClient.message);
268
- 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
+ }
269
454
  }
270
- const sessionsResponse = await getClient().session.list();
271
- const sessions = sessionsResponse.data || [];
272
- if (sessions.length === 0) {
455
+ if (searchableSessions.length === 0) {
273
456
  cliLogger.log('No sessions found');
274
457
  process.exit(0);
275
458
  }
@@ -280,82 +463,62 @@ cli
280
463
  const sessionToThread = new Map(threadSessions
281
464
  .filter((row) => row.session_id !== '')
282
465
  .map((row) => [row.session_id, row.thread_id]));
283
- const sortedSessions = [...sessions].sort((a, b) => {
284
- return b.time.updated - a.time.updated;
285
- });
286
- const matchedSessions = [];
287
- let scannedSessions = 0;
288
- for (const session of sortedSessions) {
289
- scannedSessions++;
290
- const messagesResponse = await getClient().session.messages({
291
- 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}`);
292
476
  });
293
- const messages = messagesResponse.data || [];
294
- const snippets = messages
295
- .flatMap((message) => {
296
- const rolePrefix = message.info.role === 'assistant'
297
- ? 'assistant'
298
- : message.info.role === 'user'
299
- ? 'user'
300
- : 'message';
301
- return message.parts.filter((p) => !(p.type === 'text' && p.synthetic)).flatMap((part) => {
302
- return getPartSearchTexts(part).flatMap((text) => {
303
- const hit = findFirstSessionSearchHit({
304
- text,
305
- searchPattern,
306
- });
307
- if (!hit) {
308
- return [];
309
- }
310
- const snippet = buildSessionSearchSnippet({ text, hit });
311
- if (!snippet) {
312
- return [];
313
- }
314
- return [`${rolePrefix}: ${snippet}`];
315
- });
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,
316
501
  });
317
- })
318
- .slice(0, 3);
319
- if (snippets.length === 0) {
320
- continue;
321
- }
322
- const threadId = sessionToThread.get(session.id);
323
- matchedSessions.push({
324
- id: session.id,
325
- title: session.title || 'Untitled Session',
326
- directory: session.directory,
327
- updated: new Date(session.time.updated).toISOString(),
328
- source: threadId ? 'kimaki' : 'opencode',
329
- threadId: threadId || null,
330
- snippets,
331
- });
332
- if (matchedSessions.length >= limit) {
333
- break;
334
- }
335
- }
502
+ return messagesResponse.data || [];
503
+ },
504
+ });
336
505
  if (options.json) {
337
506
  console.log(JSON.stringify({
338
507
  query: searchPattern.raw,
339
508
  mode: searchPattern.mode,
340
- projectDirectory,
509
+ all: Boolean(options.all),
510
+ days,
511
+ projectDirectories: searchedDirectories,
341
512
  scannedSessions,
342
513
  matches: matchedSessions,
343
514
  }, null, 2));
344
515
  process.exit(0);
345
516
  }
346
517
  if (matchedSessions.length === 0) {
347
- 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}`);
348
520
  process.exit(0);
349
521
  }
350
- cliLogger.log(`Found ${matchedSessions.length} matching session(s) for ${searchPattern.raw} in ${projectDirectory}`);
351
- for (const match of matchedSessions) {
352
- const threadInfo = match.threadId ? ` | thread: ${match.threadId}` : '';
353
- console.log(`${match.id} | ${match.title} | ${match.updated} | ${match.source}${threadInfo}`);
354
- console.log(` Directory: ${match.directory}`);
355
- match.snippets.forEach((snippet) => {
356
- console.log(` - ${snippet}`);
357
- });
358
- }
359
522
  process.exit(0);
360
523
  }
361
524
  catch (error) {
@@ -430,7 +593,7 @@ cli
430
593
  process.exit(0);
431
594
  });
432
595
  cli
433
- .command('session archive [threadId]', 'Archive a Discord thread and stop its mapped OpenCode session')
596
+ .command('session archive [threadId]', 'Archive a Discord thread without stopping its mapped OpenCode session')
434
597
  .option('--session <sessionId>', 'Resolve thread from an OpenCode session ID')
435
598
  .action(async (threadIdArg, options) => {
436
599
  try {
@@ -544,6 +707,75 @@ cli
544
707
  process.exit(EXIT_NO_RESTART);
545
708
  }
546
709
  });
710
+ cli
711
+ .command('session title <title>', 'Update the OpenCode session title. Discord thread name follows automatically.')
712
+ .option('--session <sessionId>', 'OpenCode session ID')
713
+ .option('--thread <threadId>', 'Discord thread ID')
714
+ .example("kimaki session title 'Fix queue draining' --session ses_xxx")
715
+ .action(async (title, options) => {
716
+ try {
717
+ await initDatabase();
718
+ const trimmedTitle = title.trim();
719
+ if (!trimmedTitle) {
720
+ cliLogger.error('Title must not be empty');
721
+ process.exit(EXIT_NO_RESTART);
722
+ }
723
+ if (options.session && options.thread) {
724
+ cliLogger.error('Use either --session or --thread, not both');
725
+ process.exit(EXIT_NO_RESTART);
726
+ }
727
+ if (!options.session && !options.thread) {
728
+ cliLogger.error('Provide --session <sessionId> or --thread <threadId>');
729
+ process.exit(EXIT_NO_RESTART);
730
+ }
731
+ const sessionId = await (async () => {
732
+ if (options.session)
733
+ return options.session;
734
+ const threadId = options.thread;
735
+ if (!threadId)
736
+ return null;
737
+ return getThreadSession(threadId);
738
+ })();
739
+ if (!sessionId) {
740
+ cliLogger.error(options.thread
741
+ ? `No OpenCode session found for thread: ${options.thread}`
742
+ : 'Provide --session <sessionId> or --thread <threadId>');
743
+ process.exit(EXIT_NO_RESTART);
744
+ }
745
+ const directory = await resolveSessionDirectoryFromDatabase({
746
+ sessionId,
747
+ });
748
+ if (directory instanceof Error) {
749
+ cliLogger.error(directory.message);
750
+ process.exit(EXIT_NO_RESTART);
751
+ }
752
+ const serverResult = await initializeOpencodeForDirectory(directory);
753
+ if (serverResult instanceof Error) {
754
+ cliLogger.error(`Failed to initialize OpenCode: ${serverResult.message}`);
755
+ process.exit(EXIT_NO_RESTART);
756
+ }
757
+ const updateResult = await serverResult()
758
+ .session.update({
759
+ sessionID: sessionId,
760
+ title: trimmedTitle,
761
+ })
762
+ .catch((e) => new OpenCodeSdkError({ operation: 'session.update', cause: e }));
763
+ if (updateResult instanceof Error) {
764
+ cliLogger.error(updateResult.message);
765
+ process.exit(EXIT_NO_RESTART);
766
+ }
767
+ if (updateResult.error) {
768
+ cliLogger.error('OpenCode rejected the session title update');
769
+ process.exit(EXIT_NO_RESTART);
770
+ }
771
+ note(`Updated OpenCode title: ${trimmedTitle}`, 'Title updated');
772
+ process.exit(0);
773
+ }
774
+ catch (error) {
775
+ cliLogger.error('Error:', error instanceof Error ? error.stack : String(error));
776
+ process.exit(EXIT_NO_RESTART);
777
+ }
778
+ });
547
779
  cli
548
780
  .command('session discord-url <sessionId>', 'Print the Discord thread URL for a session')
549
781
  .option('--json', 'Output as JSON')
@@ -22,8 +22,9 @@ 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
+ import { validateCliModelOption } from '../session-handler/model-utils.js';
27
28
  const cliLogger = createLogger(LogPrefix.CLI);
28
29
  const cli = goke();
29
30
  cli
@@ -134,7 +135,7 @@ cli
134
135
  .option('--model <model>', 'Model for the scheduled session, format provider/model (empty string clears)')
135
136
  .option('--pre-run <command>', 'New pre-run command (empty string clears)')
136
137
  .option('--allow-concurrency <enabled>', z.enum(['true', 'false']).describe('Allow concurrent sessions: true or false'))
137
- .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)')
138
139
  .action(async (id, options) => {
139
140
  try {
140
141
  const trimmedPrompt = options.prompt === undefined ? undefined : options.prompt.trim();
@@ -142,9 +143,10 @@ cli
142
143
  const hasModel = options.model !== undefined;
143
144
  const hasPreRun = options.preRun !== undefined;
144
145
  const hasAllowConcurrency = options.allowConcurrency !== undefined;
146
+ const hasUser = options.user !== undefined;
145
147
  if (!trimmedPrompt &&
146
148
  !options.sendAt &&
147
- !options.user &&
149
+ !hasUser &&
148
150
  !hasAgent &&
149
151
  !hasModel &&
150
152
  !hasPreRun &&
@@ -180,7 +182,7 @@ cli
180
182
  cliLogger.error(`Failed to parse task payload: ${existingPayload.message}`);
181
183
  process.exit(EXIT_NO_RESTART);
182
184
  }
183
- const resolvedUser = options.user
185
+ const resolvedUser = hasUser && options.user.trim()
184
186
  ? await resolveTaskUser({
185
187
  user: options.user,
186
188
  channelId: task.channel_id,
@@ -191,23 +193,38 @@ cli
191
193
  process.exit(EXIT_NO_RESTART);
192
194
  }
193
195
  const newPrompt = trimmedPrompt ?? existingPayload.prompt;
196
+ const nextModel = hasModel ? options.model.trim() || null : existingPayload.model;
197
+ if (nextModel) {
198
+ const modelCheck = await validateCliModelOption({
199
+ model: nextModel,
200
+ directory: task.project_directory ?? undefined,
201
+ });
202
+ if (modelCheck instanceof Error) {
203
+ cliLogger.error(modelCheck.message);
204
+ process.exit(EXIT_NO_RESTART);
205
+ }
206
+ }
194
207
  // Match send --model/--agent: empty string clears the override (null).
195
208
  const updatedPayload = {
196
209
  ...existingPayload,
197
210
  prompt: newPrompt,
198
- ...(resolvedUser
199
- ? { userId: resolvedUser.id, username: resolvedUser.username || null }
200
- : {}),
201
211
  ...(hasAgent ? { agent: options.agent.trim() || null } : {}),
202
- ...(hasModel ? { model: options.model.trim() || null } : {}),
212
+ ...(hasModel ? { model: nextModel } : {}),
203
213
  ...(hasPreRun ? { preRunCommand: options.preRun.trim() || null } : {}),
204
214
  ...(hasAllowConcurrency
205
215
  ? { allowConcurrency: options.allowConcurrency === 'true' }
206
216
  : {}),
207
217
  };
218
+ const payloadWithUser = hasUser
219
+ ? applyScheduledTaskUserEdit({
220
+ payload: updatedPayload,
221
+ userOption: options.user,
222
+ resolvedUser,
223
+ })
224
+ : updatedPayload;
208
225
  const updateData = {
209
226
  taskId,
210
- payloadJson: serializeScheduledTaskPayload(updatedPayload),
227
+ payloadJson: serializeScheduledTaskPayload(payloadWithUser),
211
228
  promptPreview: getPromptPreview(newPrompt),
212
229
  };
213
230
  if (options.sendAt) {
@@ -233,19 +250,21 @@ cli
233
250
  process.exit(EXIT_NO_RESTART);
234
251
  }
235
252
  const parts = [`Updated task ${taskId}`];
236
- if (resolvedUser) {
237
- 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');
238
257
  }
239
258
  if (hasAgent) {
240
- parts.push(`agent=${updatedPayload.agent || '-'}`);
259
+ parts.push(`agent=${payloadWithUser.agent || '-'}`);
241
260
  }
242
261
  if (hasModel) {
243
- parts.push(`model=${updatedPayload.model || '-'}`);
262
+ parts.push(`model=${payloadWithUser.model || '-'}`);
244
263
  }
245
264
  if (hasPreRun)
246
- parts.push(`preRun=${updatedPayload.preRunCommand || '-'}`);
265
+ parts.push(`preRun=${payloadWithUser.preRunCommand || '-'}`);
247
266
  if (hasAllowConcurrency) {
248
- parts.push(`allowConcurrency=${updatedPayload.allowConcurrency}`);
267
+ parts.push(`allowConcurrency=${payloadWithUser.allowConcurrency}`);
249
268
  }
250
269
  cliLogger.log(parts.join(' | '));
251
270
  process.exit(0);