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'
@@ -12,15 +13,18 @@ import { fileURLToPath } from 'node:url'
12
13
  import { spawn, execSync } from 'node:child_process'
13
14
  import { createLogger, LogPrefix, initLogFile } from '../logger.js'
14
15
  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'
16
+ import { getBotTokenWithMode, getThreadSession, getThreadIdBySessionId, getSessionEventSnapshot, getDb, createScheduledTask, listScheduledTasks, cancelScheduledTask, getScheduledTask, updateScheduledTask, getSessionStartSourcesBySessionIds, deleteChannelDirectoryById, findChannelsByDirectory, getThreadWorktreeOrWorkspace, getAllTextChannelDirectories } from '../database.js'
16
17
  import { ShareMarkdown } from '../markdown.js'
17
- import { parseSessionSearchPattern, findFirstSessionSearchHit, buildSessionSearchSnippet, getPartSearchTexts } from '../session-search.js'
18
+ import { parseSessionSearchPattern, collectSessionSearchMatches, validateSessionSearchScope, resolveSessionSearchDirectories, parseSessionSearchDays, sessionSearchMinUpdated, SESSION_SEARCH_DEFAULT_DAYS, type SessionSearchMatch } from '../session-search.js'
18
19
  import { formatWorktreeName, formatAutoWorktreeName } from '../commands/new-worktree.js'
20
+ import { formatTimeAgo } from '../commands/worktrees.js'
21
+ import { editorsForFile, loadFileEditEvents } from '../file-edit-log.js'
19
22
  import { WORKTREE_PREFIX } from '../commands/merge-worktree.js'
20
23
  import type { ThreadStartMarker } from '../system-message.js'
21
24
  import { buildOpencodeEventLogLine } from '../session-handler/opencode-session-event-log.js'
22
25
  import { createDiscordRest } from '../discord-urls.js'
23
26
  import { archiveThread, uploadFilesToDiscord, stripMentions } from '../discord-utils.js'
27
+ import { OpenCodeSdkError } from '../errors.js'
24
28
  import { setDataDir, setProjectsDir, getDataDir, getProjectsDir } from '../config.js'
25
29
  import { execAsync, validateWorktreeDirectory } from '../worktrees.js'
26
30
  import { upgrade, getCurrentVersion } from '../upgrade.js'
@@ -90,8 +94,10 @@ cli
90
94
  '--project <path>',
91
95
  'Project directory to list sessions for (defaults to cwd)',
92
96
  )
97
+ .option('--active', 'Only list active sessions; exits 1 when none remain')
98
+ .option('--exclude <sessionId>', 'Exclude one session ID from the results')
93
99
  .option('--json', 'Output as JSON')
94
- .action(async (options: { project?: string; json?: boolean }) => {
100
+ .action(async (options) => {
95
101
  try {
96
102
  const projectDirectory = path.resolve(options.project || '.')
97
103
 
@@ -106,10 +112,28 @@ cli
106
112
 
107
113
  const sessionsResponse = await getClient().session.list()
108
114
  const sessions = sessionsResponse.data || []
115
+ const statuses = await (async () => {
116
+ if (!options.active) return null
117
+ const response = await getClient().session.status({
118
+ directory: projectDirectory,
119
+ })
120
+ if (response.error) {
121
+ cliLogger.error('Failed to list active sessions')
122
+ process.exit(EXIT_NO_RESTART)
123
+ }
124
+ return response.data || {}
125
+ })()
126
+ const selectedSessions = sessions.filter((session) => {
127
+ if (session.id === options.exclude) return false
128
+ if (!options.active) return true
129
+ const status = statuses?.[session.id]
130
+ return Boolean(status && status.type !== 'idle')
131
+ })
109
132
 
110
- if (sessions.length === 0) {
111
- cliLogger.log('No sessions found')
112
- process.exit(0)
133
+ if (selectedSessions.length === 0) {
134
+ if (options.json) console.log('[]')
135
+ else cliLogger.log(options.active ? 'No active sessions found' : 'No sessions found')
136
+ process.exit(options.active ? 1 : 0)
113
137
  }
114
138
 
115
139
  // Look up which sessions were started via kimaki (have a thread mapping)
@@ -123,7 +147,7 @@ cli
123
147
  .map((row) => [row.session_id, row.thread_id]),
124
148
  )
125
149
  const sessionStartSources = await getSessionStartSourcesBySessionIds(
126
- sessions.map((session) => session.id),
150
+ selectedSessions.map((session) => session.id),
127
151
  )
128
152
 
129
153
  const scheduleModeLabel = ({
@@ -138,7 +162,7 @@ cli
138
162
  }
139
163
 
140
164
  if (options.json) {
141
- const output = sessions.map((session) => {
165
+ const output = selectedSessions.map((session) => {
142
166
  const startSource = sessionStartSources.get(session.id)
143
167
  const startedBy = startSource
144
168
  ? `scheduled-${scheduleModeLabel({ scheduleKind: startSource.schedule_kind })}`
@@ -152,13 +176,14 @@ cli
152
176
  threadId: sessionToThread.get(session.id) || null,
153
177
  startedBy,
154
178
  scheduledTaskId: startSource?.scheduled_task_id || null,
179
+ status: options.active ? statuses?.[session.id]?.type || 'busy' : undefined,
155
180
  }
156
181
  })
157
182
  console.log(JSON.stringify(output, null, 2))
158
183
  process.exit(0)
159
184
  }
160
185
 
161
- for (const session of sessions) {
186
+ for (const session of selectedSessions) {
162
187
  const threadId = sessionToThread.get(session.id)
163
188
  const startSource = sessionStartSources.get(session.id)
164
189
  const source = threadId ? '(kimaki)' : '(opencode)'
@@ -167,8 +192,11 @@ cli
167
192
  : ''
168
193
  const updatedAt = new Date(session.time.updated).toISOString()
169
194
  const threadInfo = threadId ? ` | thread: ${threadId}` : ''
195
+ const statusInfo = options.active
196
+ ? ` | status: ${statuses?.[session.id]?.type || 'busy'}`
197
+ : ''
170
198
  console.log(
171
- `${session.id} | ${session.title || 'Untitled Session'} | ${session.directory} | ${updatedAt} | ${source}${threadInfo}${startedBy}`,
199
+ `${session.id} | ${session.title || 'Untitled Session'} | ${session.directory} | ${updatedAt} | ${source}${threadInfo}${startedBy}${statusInfo}`,
172
200
  )
173
201
  }
174
202
 
@@ -182,6 +210,89 @@ cli
182
210
  }
183
211
  })
184
212
 
213
+ cli
214
+ .command(
215
+ 'session editors <file>',
216
+ dedent`
217
+ List sessions that last edited a file, newest first.
218
+
219
+ Use this before a commit in another session so the \`Session:\` line
220
+ uses the session that actually edited the file.
221
+ `,
222
+ )
223
+ .option('--json', 'Output as JSON')
224
+ .option(
225
+ '--limit <n>',
226
+ z.number().default(20).describe('Max sessions to show'),
227
+ )
228
+ .example('kimaki session editors src/cli.ts')
229
+ .example('kimaki session editors src/cli.ts --json')
230
+ .action(async (file, options, { console, process }) => {
231
+ try {
232
+ const cwd = process.cwd
233
+ const loaded = loadFileEditEvents({ dataDir: getDataDir() })
234
+ if (loaded instanceof Error) {
235
+ console.error(loaded.message)
236
+ process.exit(EXIT_NO_RESTART)
237
+ return
238
+ }
239
+
240
+ const editors = editorsForFile({
241
+ events: loaded,
242
+ filePath: file,
243
+ cwd,
244
+ }).slice(0, options.limit)
245
+ if (editors.length === 0) {
246
+ console.error(`No recorded editors for ${path.resolve(cwd, file)}`)
247
+ process.exit(1)
248
+ return
249
+ }
250
+
251
+ const titles = new Map<string, string>()
252
+ try {
253
+ await initDatabase()
254
+ const db = await getDb()
255
+ const sessionRows = await db.query.thread_sessions.findMany({
256
+ columns: { session_id: true, last_synced_name: true },
257
+ where: { session_id: { in: editors.map((editor) => editor.sessionId) } },
258
+ orderBy: { updated_at: 'desc' },
259
+ })
260
+ for (const row of sessionRows) {
261
+ if (!titles.has(row.session_id) && row.last_synced_name) {
262
+ titles.set(row.session_id, row.last_synced_name)
263
+ }
264
+ }
265
+ } catch (error) {
266
+ console.error(error instanceof Error ? error.message : String(error))
267
+ }
268
+
269
+ const rows = editors.map((editor) => {
270
+ const title = titles.get(editor.sessionId) || '-'
271
+ const editedAt = new Date(editor.at)
272
+ return {
273
+ sessionId: editor.sessionId,
274
+ title,
275
+ editedAt: editedAt.toISOString(),
276
+ ago: formatTimeAgo(editedAt),
277
+ }
278
+ })
279
+
280
+ if (options.json) {
281
+ console.log(JSON.stringify(rows, null, 2))
282
+ process.exit(0)
283
+ return
284
+ }
285
+
286
+ for (const row of rows) {
287
+ console.log(`${row.sessionId} | ${row.title} | ${row.ago}`)
288
+ }
289
+ process.exit(0)
290
+ } catch (error) {
291
+ console.error(error instanceof Error ? error.stack : String(error))
292
+ process.exit(EXIT_NO_RESTART)
293
+ }
294
+ })
295
+
185
296
  cli
186
297
  .command(
187
298
  'session read <sessionId>',
@@ -293,18 +404,31 @@ cli
293
404
  cli
294
405
  .command(
295
406
  'session search <query>',
296
- 'Search past sessions for text or /regex/flags in the selected project',
407
+ `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.`,
297
408
  )
298
409
  .option('--project <path>', 'Project directory (defaults to cwd)')
299
410
  .option('--channel <channelId>', 'Resolve project from a Discord channel ID')
411
+ .option('--all', 'Search every locally registered project')
412
+ .option(
413
+ '--days <n>',
414
+ `Only search sessions updated in the last n days (default: ${SESSION_SEARCH_DEFAULT_DAYS}; 0 = all time)`,
415
+ )
300
416
  .option('--limit <n>', 'Maximum matched sessions to return (default: 20)')
301
417
  .option('--json', 'Output as JSON')
418
+ .example('kimaki session search "auth timeout"')
419
+ .example('kimaki session search "auth timeout" --days 0')
420
+ .example('kimaki session search "auth timeout" --all')
302
421
  .action(async (query, options) => {
303
422
  try {
304
423
  await initDatabase()
305
424
 
306
- if (options.project && options.channel) {
307
- cliLogger.error('Use either --project or --channel, not both')
425
+ const scopeError = validateSessionSearchScope({
426
+ all: options.all,
427
+ project: options.project,
428
+ channel: options.channel,
429
+ })
430
+ if (scopeError) {
431
+ cliLogger.error(scopeError.message)
308
432
  process.exit(EXIT_NO_RESTART)
309
433
  }
310
434
 
@@ -323,9 +447,19 @@ cli
323
447
  process.exit(EXIT_NO_RESTART)
324
448
  }
325
449
 
326
- const projectDirectoryResult = await (async (): Promise<
327
- string | Error
328
- > => {
450
+ const days = parseSessionSearchDays(
451
+ typeof options.days === 'string' ? options.days : undefined,
452
+ )
453
+ if (days instanceof Error) {
454
+ cliLogger.error(days.message)
455
+ process.exit(EXIT_NO_RESTART)
456
+ }
457
+ const minUpdated = sessionSearchMinUpdated({ days })
458
+
459
+ const explicitDirectory = await (async (): Promise<string | Error | undefined> => {
460
+ if (options.all) {
461
+ return undefined
462
+ }
329
463
  if (options.channel) {
330
464
  const channelConfig = await getChannelDirectory(options.channel)
331
465
  if (!channelConfig) {
@@ -335,17 +469,50 @@ cli
335
469
  }
336
470
  return path.resolve(channelConfig.directory)
337
471
  }
338
- return path.resolve(options.project || '.')
472
+ if (options.project) {
473
+ return path.resolve(options.project)
474
+ }
475
+ return undefined
339
476
  })()
340
477
 
341
- if (projectDirectoryResult instanceof Error) {
342
- cliLogger.error(projectDirectoryResult.message)
478
+ if (explicitDirectory instanceof Error) {
479
+ cliLogger.error(explicitDirectory.message)
343
480
  process.exit(EXIT_NO_RESTART)
344
481
  }
345
482
 
346
- const projectDirectory = projectDirectoryResult
347
- if (!fs.existsSync(projectDirectory)) {
348
- cliLogger.error(`Directory does not exist: ${projectDirectory}`)
483
+ const registeredDirectories = options.all
484
+ ? (await getAllTextChannelDirectories()).map((directory) => {
485
+ return path.resolve(directory)
486
+ })
487
+ : []
488
+ const projectDirectories = resolveSessionSearchDirectories({
489
+ all: Boolean(options.all),
490
+ registeredDirectories,
491
+ cwd: path.resolve('.'),
492
+ explicitDirectory,
493
+ })
494
+ if (projectDirectories instanceof Error) {
495
+ cliLogger.error(projectDirectories.message)
496
+ process.exit(EXIT_NO_RESTART)
497
+ }
498
+
499
+ const existingDirectories: string[] = []
500
+ for (const directory of projectDirectories) {
501
+ if (fs.existsSync(directory)) {
502
+ existingDirectories.push(directory)
503
+ continue
504
+ }
505
+ if (options.all) {
506
+ cliLogger.warn(`Skipping missing directory: ${directory}`)
507
+ continue
508
+ }
509
+ cliLogger.error(`Directory does not exist: ${directory}`)
510
+ process.exit(EXIT_NO_RESTART)
511
+ }
512
+ if (existingDirectories.length === 0) {
513
+ cliLogger.error(
514
+ 'No searchable project directories found. Add a project with `kimaki project add`, or pass --project.',
515
+ )
349
516
  process.exit(EXIT_NO_RESTART)
350
517
  }
351
518
 
@@ -355,16 +522,61 @@ cli
355
522
  process.exit(EXIT_NO_RESTART)
356
523
  }
357
524
 
358
- cliLogger.log('Connecting to OpenCode server...')
359
- const getClient = await initializeOpencodeForDirectory(projectDirectory)
360
- if (getClient instanceof Error) {
361
- cliLogger.error('Failed to connect to OpenCode:', getClient.message)
362
- process.exit(EXIT_NO_RESTART)
525
+ type OpencodeGetClient = Exclude<
526
+ Awaited<ReturnType<typeof initializeOpencodeForDirectory>>,
527
+ Error
528
+ >
529
+ const clientsBySessionId = new Map<string, OpencodeGetClient>()
530
+ const searchedDirectories: string[] = []
531
+ const searchableSessions: Array<{
532
+ id: string
533
+ title: string
534
+ directory: string
535
+ updated: number
536
+ }> = []
537
+
538
+ const listedDirectories = await Promise.all(
539
+ existingDirectories.map(async (projectDirectory) => {
540
+ cliLogger.log(`Connecting to OpenCode server for ${projectDirectory}...`)
541
+ const getClient = await initializeOpencodeForDirectory(projectDirectory)
542
+ if (getClient instanceof Error) {
543
+ return { projectDirectory, getClient, sessions: [] }
544
+ }
545
+ const sessionsResponse = await getClient().session.list()
546
+ return {
547
+ projectDirectory,
548
+ getClient,
549
+ sessions: sessionsResponse.data || [],
550
+ }
551
+ }),
552
+ )
553
+ for (const listed of listedDirectories) {
554
+ if (listed.getClient instanceof Error) {
555
+ if (options.all) {
556
+ cliLogger.warn(
557
+ `Skipping ${listed.projectDirectory}: failed to connect to OpenCode: ${listed.getClient.message}`,
558
+ )
559
+ continue
560
+ }
561
+ cliLogger.error(
562
+ 'Failed to connect to OpenCode:',
563
+ listed.getClient.message,
564
+ )
565
+ process.exit(EXIT_NO_RESTART)
566
+ }
567
+ searchedDirectories.push(listed.projectDirectory)
568
+ for (const session of listed.sessions) {
569
+ clientsBySessionId.set(session.id, listed.getClient)
570
+ searchableSessions.push({
571
+ id: session.id,
572
+ title: session.title || 'Untitled Session',
573
+ directory: session.directory || listed.projectDirectory,
574
+ updated: session.time.updated,
575
+ })
576
+ }
363
577
  }
364
578
 
365
- const sessionsResponse = await getClient().session.list()
366
- const sessions = sessionsResponse.data || []
367
- if (sessions.length === 0) {
579
+ if (searchableSessions.length === 0) {
368
580
  cliLogger.log('No sessions found')
369
581
  process.exit(0)
370
582
  }
@@ -379,84 +591,62 @@ cli
379
591
  .map((row) => [row.session_id, row.thread_id]),
380
592
  )
381
593
 
382
- const sortedSessions = [...sessions].sort((a, b) => {
383
- return b.time.updated - a.time.updated
384
- })
385
-
386
- const matchedSessions: Array<{
387
- id: string
388
- title: string
389
- directory: string
390
- updated: string
391
- source: 'kimaki' | 'opencode'
392
- threadId: string | null
393
- snippets: string[]
394
- }> = []
395
-
396
- let scannedSessions = 0
397
-
398
- for (const session of sortedSessions) {
399
- scannedSessions++
400
- const messagesResponse = await getClient().session.messages({
401
- sessionID: session.id,
594
+ const scopeLabel = options.all
595
+ ? `${searchedDirectories.length} project(s)`
596
+ : searchedDirectories[0] || path.resolve('.')
597
+ const daysLabel =
598
+ days === 0 ? 'all time' : `the last ${days} day${days === 1 ? '' : 's'}`
599
+ const printMatch = (match: SessionSearchMatch) => {
600
+ const threadInfo = match.threadId ? ` | thread: ${match.threadId}` : ''
601
+ console.log(
602
+ `${match.id} | ${match.title} | ${match.updated} | ${match.source}${threadInfo}`,
603
+ )
604
+ console.log(` Directory: ${match.directory}`)
605
+ match.snippets.forEach((snippet) => {
606
+ console.log(` - ${snippet}`)
402
607
  })
403
- const messages = messagesResponse.data || []
404
-
405
- const snippets = messages
406
- .flatMap((message) => {
407
- const rolePrefix =
408
- message.info.role === 'assistant'
409
- ? 'assistant'
410
- : message.info.role === 'user'
411
- ? 'user'
412
- : 'message'
413
-
414
- return message.parts.filter((p) => !(p.type === 'text' && p.synthetic)).flatMap((part) => {
415
- return getPartSearchTexts(part).flatMap((text) => {
416
- const hit = findFirstSessionSearchHit({
417
- text,
418
- searchPattern,
419
- })
420
- if (!hit) {
421
- return []
422
- }
423
- const snippet = buildSessionSearchSnippet({ text, hit })
424
- if (!snippet) {
425
- return []
608
+ }
609
+
610
+ let printedHeader = false
611
+ const { matches: matchedSessions, scannedSessions } =
612
+ await collectSessionSearchMatches({
613
+ sessions: searchableSessions,
614
+ searchPattern,
615
+ sessionToThread,
616
+ limit,
617
+ minUpdated,
618
+ onMatch: options.json
619
+ ? undefined
620
+ : (match) => {
621
+ if (!printedHeader) {
622
+ printedHeader = true
623
+ cliLogger.log(
624
+ `Found matching session(s) for ${searchPattern.raw} in ${scopeLabel} (${daysLabel})`,
625
+ )
426
626
  }
427
- return [`${rolePrefix}: ${snippet}`]
428
- })
627
+ printMatch(match)
628
+ },
629
+ loadMessages: async (session) => {
630
+ const getClient = clientsBySessionId.get(session.id)
631
+ if (!getClient) {
632
+ return []
633
+ }
634
+ const messagesResponse = await getClient().session.messages({
635
+ sessionID: session.id,
429
636
  })
430
- })
431
- .slice(0, 3)
432
-
433
- if (snippets.length === 0) {
434
- continue
435
- }
436
-
437
- const threadId = sessionToThread.get(session.id)
438
- matchedSessions.push({
439
- id: session.id,
440
- title: session.title || 'Untitled Session',
441
- directory: session.directory,
442
- updated: new Date(session.time.updated).toISOString(),
443
- source: threadId ? 'kimaki' : 'opencode',
444
- threadId: threadId || null,
445
- snippets,
637
+ return messagesResponse.data || []
638
+ },
446
639
  })
447
640
 
448
- if (matchedSessions.length >= limit) {
449
- break
450
- }
451
- }
452
-
453
641
  if (options.json) {
454
642
  console.log(
455
643
  JSON.stringify(
456
644
  {
457
645
  query: searchPattern.raw,
458
646
  mode: searchPattern.mode,
459
- projectDirectory,
647
+ all: Boolean(options.all),
648
+ days,
649
+ projectDirectories: searchedDirectories,
460
650
  scannedSessions,
461
651
  matches: matchedSessions,
462
652
  },
@@ -468,27 +658,14 @@ cli
468
658
  }
469
659
 
470
660
  if (matchedSessions.length === 0) {
661
+ const cutoffHint =
662
+ days === 0 ? '' : '. Use --days 0 to search all time'
471
663
  cliLogger.log(
472
- `No matches found for ${searchPattern.raw} in ${projectDirectory} (${scannedSessions} sessions scanned)`,
664
+ `No matches found for ${searchPattern.raw} in ${scopeLabel} (${scannedSessions} sessions scanned, ${daysLabel})${cutoffHint}`,
473
665
  )
474
666
  process.exit(0)
475
667
  }
476
668
 
477
- cliLogger.log(
478
- `Found ${matchedSessions.length} matching session(s) for ${searchPattern.raw} in ${projectDirectory}`,
479
- )
480
-
481
- for (const match of matchedSessions) {
482
- const threadInfo = match.threadId ? ` | thread: ${match.threadId}` : ''
483
- console.log(
484
- `${match.id} | ${match.title} | ${match.updated} | ${match.source}${threadInfo}`,
485
- )
486
- console.log(` Directory: ${match.directory}`)
487
- match.snippets.forEach((snippet) => {
488
- console.log(` - ${snippet}`)
489
- })
490
- }
491
-
492
669
  process.exit(0)
493
670
  } catch (error) {
494
671
  cliLogger.error(
@@ -600,7 +777,7 @@ cli
600
777
  cli
601
778
  .command(
602
779
  'session archive [threadId]',
603
- 'Archive a Discord thread and stop its mapped OpenCode session',
780
+ 'Archive a Discord thread without stopping its mapped OpenCode session',
604
781
  )
605
782
  .option('--session <sessionId>', 'Resolve thread from an OpenCode session ID')
606
783
  .action(async (threadIdArg: string | undefined, options: { session?: string }) => {
@@ -753,6 +930,89 @@ cli
753
930
  }
754
931
  })
755
932
 
933
+ cli
934
+ .command(
935
+ 'session title <title>',
936
+ 'Update the OpenCode session title. Discord thread name follows automatically.',
937
+ )
938
+ .option('--session <sessionId>', 'OpenCode session ID')
939
+ .option('--thread <threadId>', 'Discord thread ID')
940
+ .example("kimaki session title 'Fix queue draining' --session ses_xxx")
941
+ .action(async (title, options) => {
942
+ try {
943
+ await initDatabase()
944
+
945
+ const trimmedTitle = title.trim()
946
+ if (!trimmedTitle) {
947
+ cliLogger.error('Title must not be empty')
948
+ process.exit(EXIT_NO_RESTART)
949
+ }
950
+ if (options.session && options.thread) {
951
+ cliLogger.error('Use either --session or --thread, not both')
952
+ process.exit(EXIT_NO_RESTART)
953
+ }
954
+ if (!options.session && !options.thread) {
955
+ cliLogger.error('Provide --session <sessionId> or --thread <threadId>')
956
+ process.exit(EXIT_NO_RESTART)
957
+ }
958
+
959
+ const sessionId = await (async () => {
960
+ if (options.session) return options.session
961
+ const threadId = options.thread
962
+ if (!threadId) return null
963
+ return getThreadSession(threadId)
964
+ })()
965
+ if (!sessionId) {
966
+ cliLogger.error(
967
+ options.thread
968
+ ? `No OpenCode session found for thread: ${options.thread}`
969
+ : 'Provide --session <sessionId> or --thread <threadId>',
970
+ )
971
+ process.exit(EXIT_NO_RESTART)
972
+ }
973
+
974
+ const directory = await resolveSessionDirectoryFromDatabase({
975
+ sessionId,
976
+ })
977
+ if (directory instanceof Error) {
978
+ cliLogger.error(directory.message)
979
+ process.exit(EXIT_NO_RESTART)
980
+ }
981
+
982
+ const serverResult = await initializeOpencodeForDirectory(directory)
983
+ if (serverResult instanceof Error) {
984
+ cliLogger.error(`Failed to initialize OpenCode: ${serverResult.message}`)
985
+ process.exit(EXIT_NO_RESTART)
986
+ }
987
+
988
+ const updateResult = await serverResult()
989
+ .session.update({
990
+ sessionID: sessionId,
991
+ title: trimmedTitle,
992
+ })
993
+ .catch((e) =>
994
+ new OpenCodeSdkError({ operation: 'session.update', cause: e }),
995
+ )
996
+ if (updateResult instanceof Error) {
997
+ cliLogger.error(updateResult.message)
998
+ process.exit(EXIT_NO_RESTART)
999
+ }
1000
+ if (updateResult.error) {
1001
+ cliLogger.error('OpenCode rejected the session title update')
1002
+ process.exit(EXIT_NO_RESTART)
1003
+ }
1004
+
1005
+ note(`Updated OpenCode title: ${trimmedTitle}`, 'Title updated')
1006
+ process.exit(0)
1007
+ } catch (error) {
1008
+ cliLogger.error(
1009
+ 'Error:',
1010
+ error instanceof Error ? error.stack : String(error),
1011
+ )
1012
+ process.exit(EXIT_NO_RESTART)
1013
+ }
1014
+ })
1015
+
756
1016
  cli
757
1017
  .command(
758
1018
  'session discord-url <sessionId>',