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.
- package/dist/agent-model.e2e.test.js +250 -164
- package/dist/anthropic-account-identity.js +9 -1
- package/dist/anthropic-auth-plugin.js +10 -13
- package/dist/anthropic-auth-plugin.test.js +17 -0
- package/dist/anthropic-auth-state.js +1 -0
- package/dist/bash-tool-schema-plugin.js +32 -0
- package/dist/bash-tool-schema-plugin.test.js +49 -0
- package/dist/bin.js +73 -19
- package/dist/cli-commands/maintenance.js +46 -7
- package/dist/cli-commands/project.js +11 -27
- package/dist/cli-commands/send.js +30 -1
- package/dist/cli-commands/session.js +322 -90
- package/dist/cli-commands/task.js +34 -15
- package/dist/cli-parsing.test.js +83 -4
- package/dist/cli-runner.js +44 -8
- package/dist/cli-runner.test.js +27 -0
- package/dist/cli-send-thread.e2e.test.js +9 -9
- package/dist/cli.js +32 -0
- package/dist/commands/abort.js +5 -1
- package/dist/commands/action-buttons.js +2 -1
- package/dist/commands/add-project.js +4 -7
- package/dist/commands/agent.js +164 -32
- package/dist/commands/ask-question.js +23 -33
- package/dist/commands/ask-question.test.js +43 -1
- package/dist/commands/btw.js +28 -6
- package/dist/commands/file-upload.js +2 -1
- package/dist/commands/fork-subagent.js +2 -4
- package/dist/commands/fork.js +4 -4
- package/dist/commands/last-sessions.js +17 -10
- package/dist/commands/login.js +6 -0
- package/dist/commands/merge-worktree.js +47 -14
- package/dist/commands/model-variant.js +8 -1
- package/dist/commands/model.js +16 -9
- package/dist/commands/multioauth.js +1 -0
- package/dist/commands/new-worktree.js +170 -102
- package/dist/commands/queue.js +3 -2
- package/dist/commands/resume.js +4 -4
- package/dist/commands/session.js +78 -63
- package/dist/commands/user-command.js +6 -8
- package/dist/commands/worktrees.js +5 -13
- package/dist/config.js +16 -1
- package/dist/cpu-profiler.js +211 -0
- package/dist/cpu-profiler.test.js +160 -0
- package/dist/database.js +152 -3
- package/dist/db.js +33 -0
- package/dist/db.test.js +267 -1
- package/dist/default-channel-provisioning.test.js +148 -0
- package/dist/discord-bot.js +517 -488
- package/dist/discord-command-registration.js +25 -10
- package/dist/discord-command-registration.test.js +17 -0
- package/dist/discord-utils.js +97 -5
- package/dist/discord-utils.test.js +151 -1
- package/dist/errors.js +10 -0
- package/dist/exec-async.js +17 -5
- package/dist/external-directory-permissions.e2e.test.js +11 -7
- package/dist/external-directory-project-deny.e2e.test.js +5 -3
- package/dist/external-opencode-sync.js +94 -17
- package/dist/external-opencode-sync.test.js +206 -0
- package/dist/extract-sdk-error-message.test.js +13 -0
- package/dist/file-edit-log.js +197 -0
- package/dist/file-edit-log.test.js +258 -0
- package/dist/format-tables.js +52 -0
- package/dist/format-tables.test.js +93 -0
- package/dist/gateway-proxy-reconnect.e2e.test.js +28 -8
- package/dist/gateway-proxy.e2e.test.js +18 -18
- package/dist/git-worktree-core.js +182 -17
- package/dist/interaction-handler.js +416 -398
- package/dist/ipc-tools-plugin.js +68 -3
- package/dist/kimaki-opencode-plugin-loading.e2e.test.js +122 -58
- package/dist/kimaki-opencode-plugin.js +9 -3
- package/dist/kimaki-workspace-adaptor.js +33 -10
- package/dist/logger.js +11 -1
- package/dist/markdown.js +3 -1
- package/dist/markdown.test.js +11 -3
- package/dist/message-formatting.js +181 -51
- package/dist/message-formatting.test.js +529 -22
- package/dist/message-preprocessing.js +105 -1
- package/dist/oauth-rotation-shared.js +10 -0
- package/dist/openai-auth-plugin.js +1 -0
- package/dist/openai-auth-state.js +1 -0
- package/dist/opencode-command.js +28 -0
- package/dist/opencode-command.test.js +109 -1
- package/dist/opencode-interrupt-plugin.js +4 -0
- package/dist/opencode-interrupt-plugin.test.js +24 -0
- package/dist/opencode.js +82 -15
- package/dist/project-add.test.js +62 -0
- package/dist/question-abort-clears-dropdown.e2e.test.js +89 -0
- package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +150 -0
- package/dist/question-text-before-dropdown.e2e.test.js +52 -0
- package/dist/queue-advanced-abort.e2e.test.js +8 -10
- package/dist/queue-advanced-action-buttons.e2e.test.js +8 -8
- package/dist/queue-advanced-e2e-setup.js +78 -5
- package/dist/queue-advanced-footer.e2e.test.js +60 -269
- package/dist/queue-advanced-model-switch.e2e.test.js +9 -11
- package/dist/queue-advanced-permissions-typing.e2e.test.js +15 -15
- package/dist/queue-advanced-question.e2e.test.js +32 -27
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +10 -12
- package/dist/queue-advanced-typing.e2e.test.js +7 -9
- package/dist/queue-delete-message.e2e.test.js +3 -3
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +5 -5
- package/dist/queue-interrupt-drain.e2e.test.js +122 -12
- package/dist/queue-question-select-drain.e2e.test.js +34 -132
- package/dist/runtime-lifecycle.e2e.test.js +21 -51
- package/dist/schema.js +35 -1
- package/dist/session-handler/event-stream-state.js +391 -50
- package/dist/session-handler/event-stream-state.test.js +736 -8
- package/dist/session-handler/model-utils.js +172 -5
- package/dist/session-handler/model-utils.test.js +445 -0
- package/dist/session-handler/opencode-session-event-log.js +30 -1
- package/dist/session-handler/thread-ingress.test.js +41 -0
- package/dist/session-handler/thread-runtime-state.js +2 -0
- package/dist/session-handler/thread-session-runtime-summary.test.js +121 -0
- package/dist/session-handler/thread-session-runtime.js +574 -299
- package/dist/session-search.js +123 -4
- package/dist/session-search.test.js +248 -1
- package/dist/session-sleep.test.js +224 -0
- package/dist/session-title-rename.test.js +37 -39
- package/dist/sleep.e2e.test.js +494 -0
- package/dist/startup-time.e2e.test.js +2 -1
- package/dist/store.js +3 -0
- package/dist/system-message.js +102 -16
- package/dist/system-message.test.js +192 -15
- package/dist/task-runner.js +96 -5
- package/dist/task-schedule.js +87 -1
- package/dist/task-schedule.test.js +149 -1
- package/dist/test-utils.js +17 -9
- package/dist/thinking-utils.js +10 -0
- package/dist/thinking-utils.test.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +65 -213
- package/dist/undo-redo.e2e.test.js +17 -9
- package/dist/voice-attachment.js +23 -1
- package/dist/voice-handler.js +21 -4
- package/dist/voice-message.e2e.test.js +144 -30
- package/dist/voice-routing-input.e2e.test.js +195 -0
- package/dist/voice-routing-question.e2e.test.js +125 -0
- package/dist/voice.js +28 -8
- package/dist/voice.test.js +96 -30
- package/dist/worktree-clone-isolation.e2e.test.js +188 -0
- package/dist/worktree-lifecycle.e2e.test.js +55 -36
- package/dist/worktrees.js +226 -443
- package/dist/worktrees.test.js +435 -58
- package/dist/xai-auth-plugin.js +1 -0
- package/dist/xai-auth-state.js +1 -0
- package/package.json +8 -6
- package/skills/bump-holocron-consumers/SKILL.md +137 -0
- package/skills/critique/SKILL.md +4 -4
- package/skills/errore/SKILL.md +60 -218
- package/skills/goke/SKILL.md +150 -10
- package/skills/holocron/SKILL.md +154 -17
- package/skills/npm-package/SKILL.md +55 -35
- package/skills/profano/SKILL.md +17 -2
- package/skills/spiceflow/SKILL.md +39 -3
- package/skills/strada/SKILL.md +39 -0
- package/skills/zele/SKILL.md +19 -0
- package/src/agent-model.e2e.test.ts +287 -186
- package/src/anthropic-account-identity.ts +20 -1
- package/src/anthropic-auth-plugin.test.ts +19 -0
- package/src/anthropic-auth-plugin.ts +12 -16
- package/src/anthropic-auth-state.ts +1 -0
- package/src/bash-tool-schema-plugin.test.ts +55 -0
- package/src/bash-tool-schema-plugin.ts +46 -0
- package/src/bin.ts +78 -26
- package/src/cli-commands/maintenance.ts +59 -13
- package/src/cli-commands/project.ts +14 -35
- package/src/cli-commands/send.ts +31 -1
- package/src/cli-commands/session.ts +374 -114
- package/src/cli-commands/task.ts +34 -15
- package/src/cli-parsing.test.ts +103 -4
- package/src/cli-runner.test.ts +49 -0
- package/src/cli-runner.ts +44 -7
- package/src/cli-send-thread.e2e.test.ts +9 -11
- package/src/cli.ts +53 -0
- package/src/commands/abort.ts +6 -1
- package/src/commands/action-buttons.ts +2 -1
- package/src/commands/add-project.ts +4 -8
- package/src/commands/agent.ts +205 -29
- package/src/commands/ask-question.test.ts +48 -1
- package/src/commands/ask-question.ts +23 -33
- package/src/commands/btw.ts +40 -5
- package/src/commands/file-upload.ts +2 -1
- package/src/commands/fork-subagent.ts +2 -3
- package/src/commands/fork.ts +4 -3
- package/src/commands/last-sessions.ts +10 -4
- package/src/commands/login.ts +6 -0
- package/src/commands/merge-worktree.ts +54 -16
- package/src/commands/model-variant.ts +9 -1
- package/src/commands/model.ts +20 -9
- package/src/commands/multioauth.ts +1 -0
- package/src/commands/new-worktree.ts +138 -55
- package/src/commands/queue.ts +3 -2
- package/src/commands/resume.ts +4 -3
- package/src/commands/session.ts +94 -70
- package/src/commands/user-command.ts +6 -6
- package/src/commands/worktrees.ts +5 -12
- package/src/config.ts +18 -1
- package/src/cpu-profiler.test.ts +183 -0
- package/src/cpu-profiler.ts +293 -0
- package/src/database.ts +235 -3
- package/src/db.test.ts +293 -0
- package/src/db.ts +38 -0
- package/src/default-channel-provisioning.test.ts +155 -0
- package/src/discord-bot.ts +78 -36
- package/src/discord-command-registration.test.ts +20 -0
- package/src/discord-command-registration.ts +42 -12
- package/src/discord-utils.test.ts +176 -0
- package/src/discord-utils.ts +152 -7
- package/src/errors.ts +12 -0
- package/src/exec-async.ts +20 -11
- package/src/external-directory-permissions.e2e.test.ts +11 -7
- package/src/external-directory-project-deny.e2e.test.ts +5 -3
- package/src/external-opencode-sync.test.ts +248 -0
- package/src/external-opencode-sync.ts +122 -19
- package/src/extract-sdk-error-message.test.ts +18 -0
- package/src/file-edit-log.test.ts +287 -0
- package/src/file-edit-log.ts +283 -0
- package/src/format-tables.test.ts +101 -0
- package/src/format-tables.ts +75 -0
- package/src/gateway-proxy-reconnect.e2e.test.ts +38 -8
- package/src/gateway-proxy.e2e.test.ts +18 -17
- package/src/git-worktree-core.ts +247 -20
- package/src/interaction-handler.ts +36 -13
- package/src/ipc-tools-plugin.ts +82 -5
- package/src/kimaki-opencode-plugin-loading.e2e.test.ts +188 -97
- package/src/kimaki-opencode-plugin.ts +9 -3
- package/src/kimaki-workspace-adaptor.ts +40 -11
- package/src/logger.ts +14 -4
- package/src/markdown.test.ts +13 -3
- package/src/markdown.ts +4 -1
- package/src/message-formatting.test.ts +672 -25
- package/src/message-formatting.ts +242 -53
- package/src/message-preprocessing.ts +116 -1
- package/src/oauth-rotation-shared.ts +10 -0
- package/src/openai-auth-plugin.ts +1 -0
- package/src/openai-auth-state.ts +1 -0
- package/src/opencode-command.test.ts +140 -0
- package/src/opencode-command.ts +38 -0
- package/src/opencode-interrupt-plugin.test.ts +32 -0
- package/src/opencode-interrupt-plugin.ts +3 -0
- package/src/opencode.ts +115 -15
- package/src/project-add.test.ts +80 -0
- package/src/question-abort-clears-dropdown.e2e.test.ts +109 -0
- package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +172 -0
- package/src/question-text-before-dropdown.e2e.test.ts +67 -0
- package/src/queue-advanced-abort.e2e.test.ts +8 -9
- package/src/queue-advanced-action-buttons.e2e.test.ts +8 -8
- package/src/queue-advanced-e2e-setup.ts +83 -4
- package/src/queue-advanced-footer.e2e.test.ts +67 -315
- package/src/queue-advanced-model-switch.e2e.test.ts +9 -10
- package/src/queue-advanced-permissions-typing.e2e.test.ts +15 -15
- package/src/queue-advanced-question.e2e.test.ts +36 -30
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +11 -11
- package/src/queue-advanced-typing.e2e.test.ts +8 -8
- package/src/queue-delete-message.e2e.test.ts +3 -3
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +5 -5
- package/src/queue-interrupt-drain.e2e.test.ts +133 -12
- package/src/queue-question-select-drain.e2e.test.ts +36 -161
- package/src/runtime-lifecycle.e2e.test.ts +21 -54
- package/src/schema.sql +15 -2
- package/src/schema.ts +37 -1
- package/src/session-handler/event-stream-state.test.ts +790 -7
- package/src/session-handler/event-stream-state.ts +568 -53
- package/src/session-handler/model-utils.test.ts +537 -0
- package/src/session-handler/model-utils.ts +281 -5
- package/src/session-handler/opencode-session-event-log.ts +31 -1
- package/src/session-handler/thread-ingress.test.ts +54 -0
- package/src/session-handler/thread-runtime-state.ts +3 -0
- package/src/session-handler/thread-session-runtime-summary.test.ts +169 -0
- package/src/session-handler/thread-session-runtime.ts +712 -358
- package/src/session-search.test.ts +281 -0
- package/src/session-search.ts +241 -7
- package/src/session-sleep.test.ts +294 -0
- package/src/session-title-rename.test.ts +54 -48
- package/src/sleep.e2e.test.ts +565 -0
- package/src/startup-time.e2e.test.ts +5 -1
- package/src/store.ts +22 -0
- package/src/system-message.test.ts +226 -15
- package/src/system-message.ts +130 -16
- package/src/task-runner.ts +138 -3
- package/src/task-schedule.test.ts +170 -0
- package/src/task-schedule.ts +141 -1
- package/src/test-utils.ts +21 -10
- package/src/thinking-utils.test.ts +46 -0
- package/src/thinking-utils.ts +21 -0
- package/src/thread-message-queue.e2e.test.ts +65 -245
- package/src/undo-redo.e2e.test.ts +17 -9
- package/src/voice-attachment.ts +29 -1
- package/src/voice-handler.ts +23 -4
- package/src/voice-message.e2e.test.ts +149 -29
- package/src/voice-routing-input.e2e.test.ts +203 -0
- package/src/voice-routing-question.e2e.test.ts +131 -0
- package/src/voice.test.ts +107 -29
- package/src/voice.ts +34 -6
- package/src/worktree-clone-isolation.e2e.test.ts +213 -0
- package/src/worktree-lifecycle.e2e.test.ts +55 -35
- package/src/worktrees.test.ts +505 -64
- package/src/worktrees.ts +257 -603
- package/src/xai-auth-plugin.ts +1 -0
- package/src/xai-auth-state.ts +1 -0
- package/dist/message-finish-field.e2e.test.js +0 -169
- package/dist/subagent-rate-limit-plugin.js +0 -184
- package/src/message-finish-field.e2e.test.ts +0 -196
- 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
|
})
|
package/src/session-search.ts
CHANGED
|
@@ -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:
|
|
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
|
|
170
|
+
const inputText = stringifyUnknown(part.state?.input)
|
|
131
171
|
const outputText =
|
|
132
|
-
part.state
|
|
172
|
+
part.state?.status === 'completed'
|
|
133
173
|
? stringifyUnknown(part.state.output)
|
|
134
|
-
: part.state
|
|
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
|
+
}
|