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
@@ -3,8 +3,13 @@
3
3
  import { describe, expect, test } from 'vitest'
4
4
  import {
5
5
  buildSessionSearchSnippet,
6
+ collectSessionSearchMatches,
6
7
  findFirstSessionSearchHit,
8
+ parseSessionSearchDays,
7
9
  parseSessionSearchPattern,
10
+ resolveSessionSearchDirectories,
11
+ sessionSearchMinUpdated,
12
+ validateSessionSearchScope,
8
13
  } from './session-search.js'
9
14
 
10
15
  describe('session search helpers', () => {
@@ -47,4 +52,280 @@ describe('session search helpers', () => {
47
52
  expect(snippet.toUpperCase()).toContain(expectedSubstring.toUpperCase())
48
53
  })
49
54
  })
55
+
56
+ test('rejects --all combined with --project or --channel', () => {
57
+ expect(
58
+ validateSessionSearchScope({ all: true, project: '/tmp/a' }),
59
+ ).toBeInstanceOf(Error)
60
+ expect(
61
+ validateSessionSearchScope({ all: true, channel: '123' }),
62
+ ).toBeInstanceOf(Error)
63
+ expect(validateSessionSearchScope({ all: true })).toBeNull()
64
+ expect(
65
+ validateSessionSearchScope({ project: '/tmp/a', channel: '123' }),
66
+ ).toBeInstanceOf(Error)
67
+ })
68
+
69
+ test('resolves --all to every registered project directory', () => {
70
+ const resolved = resolveSessionSearchDirectories({
71
+ all: true,
72
+ registeredDirectories: ['/tmp/kimaki', '/tmp/website', '/tmp/kimaki'],
73
+ cwd: '/tmp/current',
74
+ })
75
+ expect(resolved).toEqual(['/tmp/kimaki', '/tmp/website'])
76
+ })
77
+
78
+ test('returns error when --all has no registered projects', () => {
79
+ const resolved = resolveSessionSearchDirectories({
80
+ all: true,
81
+ registeredDirectories: [],
82
+ cwd: '/tmp/current',
83
+ })
84
+ expect(resolved).toBeInstanceOf(Error)
85
+ })
86
+
87
+ test('defaults to cwd or an explicit project when --all is off', () => {
88
+ expect(
89
+ resolveSessionSearchDirectories({
90
+ all: false,
91
+ cwd: '/tmp/current',
92
+ registeredDirectories: ['/tmp/kimaki'],
93
+ }),
94
+ ).toEqual(['/tmp/current'])
95
+ expect(
96
+ resolveSessionSearchDirectories({
97
+ all: false,
98
+ explicitDirectory: '/tmp/website',
99
+ cwd: '/tmp/current',
100
+ registeredDirectories: ['/tmp/kimaki'],
101
+ }),
102
+ ).toEqual(['/tmp/website'])
103
+ })
104
+
105
+ test('defaults --days to 14 and treats 0 as all time', () => {
106
+ expect(parseSessionSearchDays(undefined)).toBe(14)
107
+ expect(parseSessionSearchDays('0')).toBe(0)
108
+ expect(parseSessionSearchDays('7')).toBe(7)
109
+ expect(parseSessionSearchDays('-1')).toBeInstanceOf(Error)
110
+ expect(parseSessionSearchDays('nope')).toBeInstanceOf(Error)
111
+ expect(sessionSearchMinUpdated({ days: 0, now: 1_000 })).toBeUndefined()
112
+ expect(sessionSearchMinUpdated({ days: 14, now: 1_000 })).toBe(
113
+ 1_000 - 14 * 24 * 60 * 60 * 1000,
114
+ )
115
+ })
116
+
117
+ test('skips sessions older than minUpdated without loading messages', async () => {
118
+ const parsed = parseSessionSearchPattern('auth timeout')
119
+ if (parsed instanceof Error) {
120
+ throw parsed
121
+ }
122
+
123
+ const now = 1_000_000_000_000
124
+ const minUpdated = now - 14 * 24 * 60 * 60 * 1000
125
+ const loaded: string[] = []
126
+ const result = await collectSessionSearchMatches({
127
+ sessions: [
128
+ {
129
+ id: 'ses_new',
130
+ title: 'new hit',
131
+ directory: '/tmp/kimaki',
132
+ updated: now,
133
+ },
134
+ {
135
+ id: 'ses_old',
136
+ title: 'old hit',
137
+ directory: '/tmp/website',
138
+ updated: minUpdated - 1,
139
+ },
140
+ ],
141
+ searchPattern: parsed,
142
+ sessionToThread: new Map(),
143
+ limit: 20,
144
+ minUpdated,
145
+ loadMessages: async (session) => {
146
+ loaded.push(session.id)
147
+ return [
148
+ {
149
+ info: { role: 'user' },
150
+ parts: [{ type: 'text', text: 'auth timeout', synthetic: false }],
151
+ },
152
+ ]
153
+ },
154
+ })
155
+
156
+ expect(loaded).toEqual(['ses_new'])
157
+ expect(result.scannedSessions).toBe(1)
158
+ expect(result.matches.map((match) => match.id)).toEqual(['ses_new'])
159
+ })
160
+
161
+ test('loads old matching sessions when minUpdated is unset', async () => {
162
+ const parsed = parseSessionSearchPattern('auth timeout')
163
+ if (parsed instanceof Error) {
164
+ throw parsed
165
+ }
166
+
167
+ const loaded: string[] = []
168
+ const result = await collectSessionSearchMatches({
169
+ sessions: [
170
+ {
171
+ id: 'ses_old',
172
+ title: 'old hit',
173
+ directory: '/tmp/website',
174
+ updated: 1,
175
+ },
176
+ ],
177
+ searchPattern: parsed,
178
+ sessionToThread: new Map(),
179
+ limit: 20,
180
+ loadMessages: async (session) => {
181
+ loaded.push(session.id)
182
+ return [
183
+ {
184
+ info: { role: 'user' },
185
+ parts: [{ type: 'text', text: 'auth timeout', synthetic: false }],
186
+ },
187
+ ]
188
+ },
189
+ })
190
+
191
+ expect(loaded).toEqual(['ses_old'])
192
+ expect(result.matches.map((match) => match.id)).toEqual(['ses_old'])
193
+ })
194
+
195
+ test('loads messages in parallel but keeps newest matches first', async () => {
196
+ const parsed = parseSessionSearchPattern('auth timeout')
197
+ if (parsed instanceof Error) {
198
+ throw parsed
199
+ }
200
+
201
+ let inFlight = 0
202
+ let maxInFlight = 0
203
+ const streamed: string[] = []
204
+ const result = await collectSessionSearchMatches({
205
+ sessions: [
206
+ {
207
+ id: 'ses_new',
208
+ title: 'new hit',
209
+ directory: '/tmp/kimaki',
210
+ updated: 3,
211
+ },
212
+ {
213
+ id: 'ses_mid',
214
+ title: 'mid hit',
215
+ directory: '/tmp/cli',
216
+ updated: 2,
217
+ },
218
+ {
219
+ id: 'ses_old',
220
+ title: 'old hit',
221
+ directory: '/tmp/website',
222
+ updated: 1,
223
+ },
224
+ ],
225
+ searchPattern: parsed,
226
+ sessionToThread: new Map(),
227
+ limit: 2,
228
+ concurrency: 3,
229
+ onMatch: (match) => {
230
+ streamed.push(match.id)
231
+ },
232
+ loadMessages: async (session) => {
233
+ inFlight++
234
+ maxInFlight = Math.max(maxInFlight, inFlight)
235
+ await new Promise((resolve) => {
236
+ setTimeout(resolve, session.id === 'ses_new' ? 40 : 5)
237
+ })
238
+ inFlight--
239
+ return [
240
+ {
241
+ info: { role: 'user' },
242
+ parts: [{ type: 'text', text: 'auth timeout', synthetic: false }],
243
+ },
244
+ ]
245
+ },
246
+ })
247
+
248
+ expect(maxInFlight).toBeGreaterThan(1)
249
+ expect(result.matches.map((match) => match.id)).toEqual([
250
+ 'ses_new',
251
+ 'ses_mid',
252
+ ])
253
+ expect(streamed).toEqual(['ses_new', 'ses_mid'])
254
+ })
255
+
256
+ test('collects newest matches across projects up to the global limit', async () => {
257
+ const parsed = parseSessionSearchPattern('auth timeout')
258
+ if (parsed instanceof Error) {
259
+ throw parsed
260
+ }
261
+
262
+ const result = await collectSessionSearchMatches({
263
+ sessions: [
264
+ {
265
+ id: 'ses_old',
266
+ title: 'old website hit',
267
+ directory: '/tmp/website',
268
+ updated: 1,
269
+ messages: [
270
+ {
271
+ info: { role: 'user' },
272
+ parts: [{ type: 'text', text: 'auth timeout on login', synthetic: false }],
273
+ },
274
+ ],
275
+ },
276
+ {
277
+ id: 'ses_new',
278
+ title: 'new kimaki hit',
279
+ directory: '/tmp/kimaki',
280
+ updated: 3,
281
+ messages: [
282
+ {
283
+ info: { role: 'assistant' },
284
+ parts: [{ type: 'text', text: 'fixed the auth timeout', synthetic: false }],
285
+ },
286
+ ],
287
+ },
288
+ {
289
+ id: 'ses_miss',
290
+ title: 'unrelated',
291
+ directory: '/tmp/cli',
292
+ updated: 4,
293
+ messages: [
294
+ {
295
+ info: { role: 'user' },
296
+ parts: [{ type: 'text', text: 'refactor logger', synthetic: false }],
297
+ },
298
+ ],
299
+ },
300
+ {
301
+ id: 'ses_mid',
302
+ title: 'mid cli hit',
303
+ directory: '/tmp/cli',
304
+ updated: 2,
305
+ messages: [
306
+ {
307
+ info: { role: 'user' },
308
+ parts: [{ type: 'text', text: 'auth timeout retry', synthetic: false }],
309
+ },
310
+ ],
311
+ },
312
+ ],
313
+ searchPattern: parsed,
314
+ sessionToThread: new Map([
315
+ ['ses_new', 'thread_new'],
316
+ ['ses_mid', 'thread_mid'],
317
+ ]),
318
+ limit: 2,
319
+ concurrency: 1,
320
+ })
321
+
322
+ expect(result.scannedSessions).toBe(3)
323
+ expect(result.matches.map((match) => match.id)).toEqual([
324
+ 'ses_new',
325
+ 'ses_mid',
326
+ ])
327
+ expect(result.matches[0]?.directory).toBe('/tmp/kimaki')
328
+ expect(result.matches[0]?.source).toBe('kimaki')
329
+ expect(result.matches[1]?.directory).toBe('/tmp/cli')
330
+ })
50
331
  })
@@ -1,8 +1,6 @@
1
1
  // Session search helpers for kimaki CLI commands.
2
2
  // Parses string/regex queries and builds readable snippets from matched content.
3
3
 
4
- import type { Part } from '@opencode-ai/sdk/v2'
5
-
6
4
  export type SessionSearchPattern =
7
5
  | {
8
6
  mode: 'literal'
@@ -20,6 +18,48 @@ export type SessionSearchHit = {
20
18
  length: number
21
19
  }
22
20
 
21
+ export type SessionSearchPart = {
22
+ type: string
23
+ synthetic?: boolean
24
+ text?: string
25
+ tool?: string
26
+ filename?: string
27
+ url?: string
28
+ state?: {
29
+ input?: unknown
30
+ status?: string
31
+ output?: unknown
32
+ error?: string
33
+ }
34
+ }
35
+
36
+ export type SessionSearchableMessage = {
37
+ info: { role: string }
38
+ parts: SessionSearchPart[]
39
+ }
40
+
41
+ export type SessionSearchableSession = {
42
+ id: string
43
+ title?: string
44
+ directory: string
45
+ updated: number
46
+ messages: SessionSearchableMessage[]
47
+ }
48
+
49
+ // Default window so search does not load years of old session messages.
50
+ export const SESSION_SEARCH_DEFAULT_DAYS = 14
51
+ export const SESSION_SEARCH_MESSAGE_CONCURRENCY = 4
52
+
53
+ export type SessionSearchMatch = {
54
+ id: string
55
+ title: string
56
+ directory: string
57
+ updated: string
58
+ source: 'kimaki' | 'opencode'
59
+ threadId: string | null
60
+ snippets: string[]
61
+ }
62
+
23
63
  export function parseSessionSearchPattern(
24
64
  query: string,
25
65
  ): SessionSearchPattern | Error {
@@ -120,21 +160,21 @@ function stringifyUnknown(value: unknown): string {
120
160
  }
121
161
  }
122
162
 
123
- export function getPartSearchTexts(part: Part): string[] {
163
+ export function getPartSearchTexts(part: SessionSearchPart): string[] {
124
164
  switch (part.type) {
125
165
  case 'text':
126
166
  return part.text ? [part.text] : []
127
167
  case 'reasoning':
128
168
  return part.text ? [part.text] : []
129
169
  case 'tool': {
130
- const inputText = stringifyUnknown(part.state.input)
170
+ const inputText = stringifyUnknown(part.state?.input)
131
171
  const outputText =
132
- part.state.status === 'completed'
172
+ part.state?.status === 'completed'
133
173
  ? stringifyUnknown(part.state.output)
134
- : part.state.status === 'error'
174
+ : part.state?.status === 'error'
135
175
  ? part.state.error || ''
136
176
  : ''
137
- return [`tool:${part.tool}`, inputText, outputText].filter((entry) => {
177
+ return [`tool:${part.tool || ''}`, inputText, outputText].filter((entry) => {
138
178
  return entry.trim().length > 0
139
179
  })
140
180
  }
@@ -146,3 +186,197 @@ export function getPartSearchTexts(part: Part): string[] {
146
186
  return []
147
187
  }
148
188
  }
189
+
190
+ export function validateSessionSearchScope({
191
+ all,
192
+ project,
193
+ channel,
194
+ }: {
195
+ all?: boolean
196
+ project?: string
197
+ channel?: string
198
+ }): Error | null {
199
+ if (all && (project || channel)) {
200
+ return new Error(
201
+ 'Use --all alone. Do not combine it with --project or --channel. Search one project with: kimaki session search "query" --project /path',
202
+ )
203
+ }
204
+ if (project && channel) {
205
+ return new Error('Use either --project or --channel, not both')
206
+ }
207
+ return null
208
+ }
209
+
210
+ export function parseSessionSearchDays(raw: string | undefined): number | Error {
211
+ if (raw === undefined) {
212
+ return SESSION_SEARCH_DEFAULT_DAYS
213
+ }
214
+ const parsed = Number.parseInt(raw, 10)
215
+ if (Number.isNaN(parsed) || parsed < 0 || String(parsed) !== raw.trim()) {
216
+ return new Error(
217
+ `Invalid --days value: ${raw}. Use a whole number of days, or 0 for all time.`,
218
+ )
219
+ }
220
+ return parsed
221
+ }
222
+
223
+ export function sessionSearchMinUpdated({
224
+ days,
225
+ now = Date.now(),
226
+ }: {
227
+ days: number
228
+ now?: number
229
+ }): number | undefined {
230
+ if (days === 0) {
231
+ return undefined
232
+ }
233
+ return now - days * 24 * 60 * 60 * 1000
234
+ }
235
+
236
+ export function resolveSessionSearchDirectories({
237
+ all,
238
+ registeredDirectories,
239
+ cwd,
240
+ explicitDirectory,
241
+ }: {
242
+ all: boolean
243
+ registeredDirectories: string[]
244
+ cwd: string
245
+ explicitDirectory?: string
246
+ }): string[] | Error {
247
+ if (!all) {
248
+ return [explicitDirectory || cwd]
249
+ }
250
+
251
+ const uniqueDirectories = [...new Set(registeredDirectories)]
252
+ if (uniqueDirectories.length === 0) {
253
+ return new Error(
254
+ 'No registered projects found. Add a project first with `kimaki project add`, or search one directory with --project.',
255
+ )
256
+ }
257
+ return uniqueDirectories
258
+ }
259
+
260
+ export function getSessionSearchSnippets({
261
+ messages,
262
+ searchPattern,
263
+ }: {
264
+ messages: SessionSearchableMessage[]
265
+ searchPattern: SessionSearchPattern
266
+ }): string[] {
267
+ return messages
268
+ .flatMap((message) => {
269
+ const rolePrefix =
270
+ message.info.role === 'assistant'
271
+ ? 'assistant'
272
+ : message.info.role === 'user'
273
+ ? 'user'
274
+ : 'message'
275
+
276
+ return message.parts
277
+ .filter((part) => !(part.type === 'text' && part.synthetic))
278
+ .flatMap((part) => {
279
+ return getPartSearchTexts(part).flatMap((text) => {
280
+ const hit = findFirstSessionSearchHit({
281
+ text,
282
+ searchPattern,
283
+ })
284
+ if (!hit) {
285
+ return []
286
+ }
287
+ const snippet = buildSessionSearchSnippet({ text, hit })
288
+ if (!snippet) {
289
+ return []
290
+ }
291
+ return [`${rolePrefix}: ${snippet}`]
292
+ })
293
+ })
294
+ })
295
+ .slice(0, 3)
296
+ }
297
+
298
+ export async function collectSessionSearchMatches({
299
+ sessions,
300
+ searchPattern,
301
+ sessionToThread,
302
+ limit,
303
+ minUpdated,
304
+ concurrency = SESSION_SEARCH_MESSAGE_CONCURRENCY,
305
+ loadMessages,
306
+ onMatch,
307
+ }: {
308
+ sessions: Array<
309
+ Omit<SessionSearchableSession, 'messages'> & {
310
+ messages?: SessionSearchableMessage[]
311
+ }
312
+ >
313
+ searchPattern: SessionSearchPattern
314
+ sessionToThread: Map<string, string>
315
+ limit: number
316
+ minUpdated?: number
317
+ concurrency?: number
318
+ loadMessages?: (session: {
319
+ id: string
320
+ directory: string
321
+ }) => Promise<SessionSearchableMessage[]>
322
+ onMatch?: (match: SessionSearchMatch) => void
323
+ }): Promise<{
324
+ matches: SessionSearchMatch[]
325
+ scannedSessions: number
326
+ }> {
327
+ const sortedSessions = [...sessions]
328
+ .sort((a, b) => {
329
+ return b.updated - a.updated
330
+ })
331
+ .filter((session) => {
332
+ return minUpdated === undefined || session.updated >= minUpdated
333
+ })
334
+ const matches: SessionSearchMatch[] = []
335
+ let scannedSessions = 0
336
+ const batchSize = Math.max(1, concurrency)
337
+
338
+ for (let offset = 0; offset < sortedSessions.length; offset += batchSize) {
339
+ if (matches.length >= limit) {
340
+ break
341
+ }
342
+ const batch = sortedSessions.slice(offset, offset + batchSize)
343
+ scannedSessions += batch.length
344
+ const loadedBatch = await Promise.all(
345
+ batch.map(async (session) => {
346
+ const messages = session.messages
347
+ ? session.messages
348
+ : loadMessages
349
+ ? await loadMessages(session)
350
+ : []
351
+ return { session, messages }
352
+ }),
353
+ )
354
+ for (const { session, messages } of loadedBatch) {
355
+ if (matches.length >= limit) {
356
+ break
357
+ }
358
+ const snippets = getSessionSearchSnippets({
359
+ messages,
360
+ searchPattern,
361
+ })
362
+ if (snippets.length === 0) {
363
+ continue
364
+ }
365
+
366
+ const threadId = sessionToThread.get(session.id)
367
+ const match: SessionSearchMatch = {
368
+ id: session.id,
369
+ title: session.title || 'Untitled Session',
370
+ directory: session.directory,
371
+ updated: new Date(session.updated).toISOString(),
372
+ source: threadId ? 'kimaki' : 'opencode',
373
+ threadId: threadId || null,
374
+ snippets,
375
+ }
376
+ matches.push(match)
377
+ onMatch?.(match)
378
+ }
379
+ }
380
+
381
+ return { matches, scannedSessions }
382
+ }