kimaki 0.4.85 → 0.4.87
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 +3 -2
- package/dist/commands/ask-question.js +22 -8
- package/dist/commands/btw.js +111 -0
- package/dist/discord-bot.js +24 -8
- package/dist/discord-command-registration.js +53 -41
- package/dist/interaction-handler.js +4 -15
- package/dist/markdown.test.js +32 -0
- package/dist/queue-advanced-footer.e2e.test.js +40 -3
- package/dist/queue-advanced-model-switch.e2e.test.js +6 -0
- package/dist/queue-advanced-permissions-typing.e2e.test.js +1 -0
- package/dist/queue-advanced-question.e2e.test.js +108 -34
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +8 -2
- package/dist/runtime-lifecycle.e2e.test.js +4 -1
- package/dist/thread-message-queue.e2e.test.js +2 -5
- package/dist/voice-message.e2e.test.js +6 -1
- package/package.json +4 -4
- package/skills/critique/SKILL.md +3 -37
- package/skills/gitchamber/SKILL.md +93 -0
- package/skills/goke/SKILL.md +3 -1
- package/src/agent-model.e2e.test.ts +3 -2
- package/src/commands/ask-question.ts +23 -8
- package/src/commands/btw.ts +158 -0
- package/src/discord-bot.ts +23 -8
- package/src/discord-command-registration.ts +64 -49
- package/src/interaction-handler.ts +8 -15
- package/src/markdown.test.ts +32 -0
- package/src/queue-advanced-footer.e2e.test.ts +40 -3
- package/src/queue-advanced-model-switch.e2e.test.ts +6 -0
- package/src/queue-advanced-permissions-typing.e2e.test.ts +1 -0
- package/src/queue-advanced-question.e2e.test.ts +129 -35
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +8 -2
- package/src/runtime-lifecycle.e2e.test.ts +4 -1
- package/src/thread-message-queue.e2e.test.ts +2 -5
- package/src/voice-message.e2e.test.ts +6 -1
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
// E2e test for question tool: user text message during pending question should
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
2
|
+
// dismiss the question (abort), then enqueue as a normal user prompt.
|
|
3
|
+
// The user's message must appear as a real user message in the thread, not
|
|
4
|
+
// get consumed as a tool result answer (which lost voice/image content).
|
|
5
|
+
import { describe, test, expect, afterEach } from 'vitest';
|
|
5
6
|
import { setupQueueAdvancedSuite, TEST_USER_ID, } from './queue-advanced-e2e-setup.js';
|
|
6
7
|
import { waitForBotMessageContaining, waitForFooterMessage, } from './test-utils.js';
|
|
7
8
|
import { pendingQuestionContexts } from './commands/ask-question.js';
|
|
9
|
+
import { store } from './store.js';
|
|
8
10
|
const TEXT_CHANNEL_ID = '200000000000001007';
|
|
11
|
+
const VOICE_CHANNEL_ID = '200000000000001017';
|
|
9
12
|
async function waitForPendingQuestion({ threadId, timeoutMs, }) {
|
|
10
13
|
const start = Date.now();
|
|
11
14
|
while (Date.now() - start < timeoutMs) {
|
|
@@ -36,14 +39,22 @@ async function waitForNoPendingQuestion({ threadId, timeoutMs, }) {
|
|
|
36
39
|
}
|
|
37
40
|
throw new Error('Timed out waiting for question context cleanup');
|
|
38
41
|
}
|
|
39
|
-
|
|
42
|
+
function setDeterministicTranscription(config) {
|
|
43
|
+
store.setState({
|
|
44
|
+
test: { deterministicTranscription: config },
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
describe('queue advanced: question tool answer', () => {
|
|
40
48
|
const ctx = setupQueueAdvancedSuite({
|
|
41
49
|
channelId: TEXT_CHANNEL_ID,
|
|
42
50
|
channelName: 'qa-question-e2e',
|
|
43
51
|
dirName: 'qa-question-e2e',
|
|
44
52
|
username: 'queue-question-tester',
|
|
45
53
|
});
|
|
46
|
-
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
setDeterministicTranscription(null);
|
|
56
|
+
});
|
|
57
|
+
test('user text message dismisses pending question and enqueues as normal prompt', async () => {
|
|
47
58
|
await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
48
59
|
content: 'QUESTION_TEXT_ANSWER_MARKER',
|
|
49
60
|
});
|
|
@@ -69,32 +80,17 @@ describe('queue advanced: question tool text answer', () => {
|
|
|
69
80
|
});
|
|
70
81
|
// User sends a text message while question is pending.
|
|
71
82
|
// This should:
|
|
72
|
-
// 1.
|
|
73
|
-
// 2.
|
|
74
|
-
// 3.
|
|
83
|
+
// 1. Dismiss the pending question (cleanup context)
|
|
84
|
+
// 2. Abort the blocked session so OpenCode unblocks
|
|
85
|
+
// 3. Enqueue the message as a normal user prompt (not consumed as answer)
|
|
75
86
|
await th.user(TEST_USER_ID).sendMessage({
|
|
76
87
|
content: 'my text answer',
|
|
77
88
|
});
|
|
78
|
-
// Pending question context should be cleaned up
|
|
89
|
+
// Pending question context should be cleaned up
|
|
79
90
|
await waitForNoPendingQuestion({
|
|
80
91
|
threadId: thread.id,
|
|
81
92
|
timeoutMs: 4_000,
|
|
82
93
|
});
|
|
83
|
-
// Wait for second question dropdown (from question-answer followup —
|
|
84
|
-
// OpenCode calls LLM again with same prompt after question tool completes,
|
|
85
|
-
// deterministic matcher fires question tool again). This is expected.
|
|
86
|
-
// Poll for it instead of sleeping.
|
|
87
|
-
const start = Date.now();
|
|
88
|
-
while (Date.now() - start < 4_000) {
|
|
89
|
-
const msgs = await th.getMessages();
|
|
90
|
-
const questionMsgs = msgs.filter((m) => {
|
|
91
|
-
return m.content.includes('Which option do you prefer?');
|
|
92
|
-
});
|
|
93
|
-
if (questionMsgs.length >= 2) {
|
|
94
|
-
break;
|
|
95
|
-
}
|
|
96
|
-
await new Promise((r) => { setTimeout(r, 50); });
|
|
97
|
-
}
|
|
98
94
|
const timeline = await th.text({ showInteractions: true });
|
|
99
95
|
expect(timeline).toMatchInlineSnapshot(`
|
|
100
96
|
"--- from: user (queue-question-tester)
|
|
@@ -103,18 +99,96 @@ describe('queue advanced: question tool text answer', () => {
|
|
|
103
99
|
**Pick one**
|
|
104
100
|
Which option do you prefer?
|
|
105
101
|
--- from: user (queue-question-tester)
|
|
106
|
-
my text answer
|
|
107
|
-
--- from: assistant (TestBot)
|
|
108
|
-
**Pick one**
|
|
109
|
-
Which option do you prefer?"
|
|
102
|
+
my text answer"
|
|
110
103
|
`);
|
|
111
|
-
// The user's
|
|
104
|
+
// The user's message must appear in Discord
|
|
112
105
|
expect(timeline).toContain('my text answer');
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
// dropdown. With the fix, only 2 dropdowns appear (initial + followup
|
|
116
|
-
// from question answer). Count occurrences of "Which option do you prefer?"
|
|
106
|
+
// Only 1 question dropdown — text message was consumed as the answer,
|
|
107
|
+
// no duplicate prompt was sent (which would trigger a second dropdown).
|
|
117
108
|
const questionCount = (timeline.match(/Which option do you prefer\?/g) || []).length;
|
|
118
|
-
expect(questionCount).toBe(
|
|
109
|
+
expect(questionCount).toBe(1);
|
|
110
|
+
}, 20_000);
|
|
111
|
+
});
|
|
112
|
+
describe('queue advanced: voice message during pending question', () => {
|
|
113
|
+
const ctx = setupQueueAdvancedSuite({
|
|
114
|
+
channelId: VOICE_CHANNEL_ID,
|
|
115
|
+
channelName: 'qa-question-voice-e2e',
|
|
116
|
+
dirName: 'qa-question-voice-e2e',
|
|
117
|
+
username: 'queue-question-tester',
|
|
118
|
+
});
|
|
119
|
+
afterEach(() => {
|
|
120
|
+
setDeterministicTranscription(null);
|
|
121
|
+
});
|
|
122
|
+
test('voice message during pending question dismisses question and transcribes normally', async () => {
|
|
123
|
+
// This is the exact bug scenario: user sends a voice message while a
|
|
124
|
+
// question dropdown is pending. Voice messages have empty message.content
|
|
125
|
+
// (audio is in attachments, transcription happens later). The old code
|
|
126
|
+
// passed "" as the question answer and consumed the message — the voice
|
|
127
|
+
// content was completely lost.
|
|
128
|
+
await ctx.discord.channel(VOICE_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
129
|
+
content: 'QUESTION_TEXT_ANSWER_MARKER',
|
|
130
|
+
});
|
|
131
|
+
const thread = await ctx.discord.channel(VOICE_CHANNEL_ID).waitForThread({
|
|
132
|
+
timeout: 4_000,
|
|
133
|
+
predicate: (t) => {
|
|
134
|
+
return t.name === 'QUESTION_TEXT_ANSWER_MARKER';
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
const th = ctx.discord.thread(thread.id);
|
|
138
|
+
// Wait for the question dropdown to appear
|
|
139
|
+
await waitForPendingQuestion({
|
|
140
|
+
threadId: thread.id,
|
|
141
|
+
timeoutMs: 4_000,
|
|
142
|
+
});
|
|
143
|
+
await waitForBotMessageContaining({
|
|
144
|
+
discord: ctx.discord,
|
|
145
|
+
threadId: thread.id,
|
|
146
|
+
text: 'Which option do you prefer?',
|
|
147
|
+
timeout: 4_000,
|
|
148
|
+
});
|
|
149
|
+
// Send a voice message while the question is pending.
|
|
150
|
+
// message.content is "" for voice messages — only the attachment exists.
|
|
151
|
+
setDeterministicTranscription({
|
|
152
|
+
transcription: 'I want option Alpha please',
|
|
153
|
+
queueMessage: false,
|
|
154
|
+
});
|
|
155
|
+
await th.user(TEST_USER_ID).sendVoiceMessage();
|
|
156
|
+
// Question context should be cleaned up (empty reply sent to unblock OpenCode)
|
|
157
|
+
await waitForNoPendingQuestion({
|
|
158
|
+
threadId: thread.id,
|
|
159
|
+
timeoutMs: 4_000,
|
|
160
|
+
});
|
|
161
|
+
// Voice content should be transcribed and appear as the next user message,
|
|
162
|
+
// processed after the model responds to the empty question answer.
|
|
163
|
+
await waitForBotMessageContaining({
|
|
164
|
+
discord: ctx.discord,
|
|
165
|
+
threadId: thread.id,
|
|
166
|
+
text: 'I want option Alpha please',
|
|
167
|
+
timeout: 4_000,
|
|
168
|
+
});
|
|
169
|
+
await waitForFooterMessage({
|
|
170
|
+
discord: ctx.discord,
|
|
171
|
+
threadId: thread.id,
|
|
172
|
+
timeout: 4_000,
|
|
173
|
+
afterMessageIncludes: 'I want option Alpha please',
|
|
174
|
+
afterAuthorId: ctx.discord.botUserId,
|
|
175
|
+
});
|
|
176
|
+
const timeline = await th.text({ showInteractions: true });
|
|
177
|
+
expect(timeline).toMatchInlineSnapshot(`
|
|
178
|
+
"--- from: user (queue-question-tester)
|
|
179
|
+
QUESTION_TEXT_ANSWER_MARKER
|
|
180
|
+
--- from: assistant (TestBot)
|
|
181
|
+
**Pick one**
|
|
182
|
+
Which option do you prefer?
|
|
183
|
+
--- from: user (queue-question-tester)
|
|
184
|
+
[attachment: voice-message.ogg]
|
|
185
|
+
--- from: assistant (TestBot)
|
|
186
|
+
🎤 Transcribing voice message...
|
|
187
|
+
📝 **Transcribed message:** I want option Alpha please
|
|
188
|
+
⬥ ok
|
|
189
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
190
|
+
`);
|
|
191
|
+
// Voice content must be present as a real transcribed message, not lost
|
|
192
|
+
expect(timeline).toContain('I want option Alpha please');
|
|
119
193
|
}, 20_000);
|
|
120
194
|
});
|
|
@@ -87,16 +87,22 @@ e2eTest('queue advanced: typing interrupt', () => {
|
|
|
87
87
|
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
88
88
|
--- from: user (queue-advanced-tester)
|
|
89
89
|
PLUGIN_TIMEOUT_SLEEP_MARKER
|
|
90
|
-
[bot typing]
|
|
91
90
|
--- from: assistant (TestBot)
|
|
91
|
+
⬥ ok
|
|
92
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
93
|
+
[bot typing]
|
|
94
|
+
⬥ ok
|
|
95
|
+
[bot typing]
|
|
92
96
|
⬥ starting sleep 100
|
|
93
97
|
--- from: user (queue-advanced-tester)
|
|
94
98
|
Reply with exactly: typing-stop-interrupt-final
|
|
95
99
|
[bot typing]
|
|
96
100
|
[bot typing]
|
|
101
|
+
[bot typing]
|
|
97
102
|
--- from: assistant (TestBot)
|
|
98
103
|
⬥ ok
|
|
99
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
104
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
105
|
+
[bot typing]"
|
|
100
106
|
`);
|
|
101
107
|
expect(finalUserIndex).toBeGreaterThanOrEqual(0);
|
|
102
108
|
expect(finalReplyIndex).toBeGreaterThan(finalUserIndex);
|
|
@@ -278,6 +278,7 @@ describe('runtime lifecycle', () => {
|
|
|
278
278
|
--- from: assistant (TestBot)
|
|
279
279
|
⬥ ok
|
|
280
280
|
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
281
|
+
⬥ ok
|
|
281
282
|
--- from: user (lifecycle-tester)
|
|
282
283
|
Reply with exactly: seq-beta
|
|
283
284
|
--- from: assistant (TestBot)
|
|
@@ -287,6 +288,7 @@ describe('runtime lifecycle', () => {
|
|
|
287
288
|
Reply with exactly: seq-gamma
|
|
288
289
|
--- from: assistant (TestBot)
|
|
289
290
|
⬥ ok
|
|
291
|
+
⬥ ok
|
|
290
292
|
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
291
293
|
`);
|
|
292
294
|
expect(runtimeAfterC).toBe(runtimeAfterA);
|
|
@@ -324,7 +326,8 @@ describe('runtime lifecycle', () => {
|
|
|
324
326
|
Reply with exactly: footer-check
|
|
325
327
|
--- from: assistant (TestBot)
|
|
326
328
|
⬥ ok
|
|
327
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
329
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
330
|
+
⬥ ok"
|
|
328
331
|
`);
|
|
329
332
|
expect(footerMessage).toBeDefined();
|
|
330
333
|
if (!footerMessage) {
|
|
@@ -488,7 +488,6 @@ e2eTest('thread message queue ordering', () => {
|
|
|
488
488
|
Reply with exactly: three
|
|
489
489
|
--- from: assistant (TestBot)
|
|
490
490
|
⬥ ok
|
|
491
|
-
⬥ ok
|
|
492
491
|
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
493
492
|
`);
|
|
494
493
|
const userThreeIndex = after.findIndex((message) => {
|
|
@@ -565,6 +564,7 @@ e2eTest('thread message queue ordering', () => {
|
|
|
565
564
|
Prompt from test: respond with short text for opencode queue mode.
|
|
566
565
|
--- from: assistant (TestBot)
|
|
567
566
|
⬥ ok
|
|
567
|
+
⬥ ok
|
|
568
568
|
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
569
569
|
`);
|
|
570
570
|
const followupUserIndex = messagesWithFollowupFooter.findIndex((message) => {
|
|
@@ -632,7 +632,6 @@ e2eTest('thread message queue ordering', () => {
|
|
|
632
632
|
Reply with exactly: BASH_TOOL_FILE_MARKER
|
|
633
633
|
--- from: assistant (TestBot)
|
|
634
634
|
⬥ running create file
|
|
635
|
-
⬥ ok
|
|
636
635
|
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
637
636
|
`);
|
|
638
637
|
expect(fs.existsSync(markerPath)).toBe(true);
|
|
@@ -815,10 +814,10 @@ e2eTest('thread message queue ordering', () => {
|
|
|
815
814
|
Reply with exactly: echo
|
|
816
815
|
--- from: assistant (TestBot)
|
|
817
816
|
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
818
|
-
⬥ ok
|
|
819
817
|
--- from: user (queue-tester)
|
|
820
818
|
Reply with exactly: foxtrot
|
|
821
819
|
--- from: assistant (TestBot)
|
|
820
|
+
⬥ ok
|
|
822
821
|
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
823
822
|
`);
|
|
824
823
|
expect(userEchoIndex).toBeGreaterThan(-1);
|
|
@@ -899,7 +898,6 @@ e2eTest('thread message queue ordering', () => {
|
|
|
899
898
|
Reply with exactly: india
|
|
900
899
|
--- from: assistant (TestBot)
|
|
901
900
|
⬥ ok
|
|
902
|
-
⬥ ok
|
|
903
901
|
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
904
902
|
`);
|
|
905
903
|
const userIndiaIndex = after.findIndex((m) => {
|
|
@@ -991,7 +989,6 @@ e2eTest('thread message queue ordering', () => {
|
|
|
991
989
|
Reply with exactly: november
|
|
992
990
|
--- from: assistant (TestBot)
|
|
993
991
|
⬥ ok
|
|
994
|
-
⬥ ok
|
|
995
992
|
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
996
993
|
`);
|
|
997
994
|
// E's user message appears before the final bot response
|
|
@@ -394,7 +394,8 @@ e2eTest('voice message handling', () => {
|
|
|
394
394
|
🎤 Transcribing voice message...
|
|
395
395
|
📝 **Transcribed message:** Fix the login bug in auth.ts
|
|
396
396
|
⬥ session-reply
|
|
397
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
397
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
398
|
+
⬥ ok"
|
|
398
399
|
`);
|
|
399
400
|
expect(finalState.sessionId).toBeDefined();
|
|
400
401
|
// Verify OpenCode session received the transcribed voice message as a prompt
|
|
@@ -566,6 +567,7 @@ e2eTest('voice message handling', () => {
|
|
|
566
567
|
--- from: assistant (TestBot)
|
|
567
568
|
🎤 Transcribing voice message...
|
|
568
569
|
📝 **Transcribed message:** Add error handling to the parser
|
|
570
|
+
⬥ fast-response-done
|
|
569
571
|
⬥ session-reply
|
|
570
572
|
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
571
573
|
`);
|
|
@@ -874,7 +876,10 @@ e2eTest('voice message handling', () => {
|
|
|
874
876
|
[attachment: voice-message.ogg]
|
|
875
877
|
--- from: assistant (TestBot)
|
|
876
878
|
🎤 Transcribing voice message...
|
|
879
|
+
⬥ fast-response-done
|
|
880
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
877
881
|
📝 **Transcribed message:** Delayed transcription result
|
|
882
|
+
⬥ fast-response-done
|
|
878
883
|
⬥ session-reply
|
|
879
884
|
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
880
885
|
`);
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "kimaki",
|
|
3
3
|
"module": "index.ts",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.4.
|
|
5
|
+
"version": "0.4.87",
|
|
6
6
|
"repository": "https://github.com/remorses/kimaki",
|
|
7
7
|
"bin": "bin.js",
|
|
8
8
|
"files": [
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
"prisma": "7.4.2",
|
|
26
26
|
"tsx": "^4.20.5",
|
|
27
27
|
"discord-digital-twin": "^0.1.0",
|
|
28
|
-
"opencode-deterministic-provider": "^0.0.1",
|
|
29
28
|
"opencode-cached-provider": "^0.0.1",
|
|
29
|
+
"opencode-deterministic-provider": "^0.0.1",
|
|
30
30
|
"db": "^0.0.0"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
@@ -66,9 +66,9 @@
|
|
|
66
66
|
"xdg-basedir": "^5.1.0",
|
|
67
67
|
"zod": "^4.3.6",
|
|
68
68
|
"zustand": "^5.0.11",
|
|
69
|
-
"errore": "^0.14.1",
|
|
70
69
|
"libsqlproxy": "^0.1.0",
|
|
71
|
-
"traforo": "^0.2.
|
|
70
|
+
"traforo": "^0.2.2",
|
|
71
|
+
"errore": "^0.14.1"
|
|
72
72
|
},
|
|
73
73
|
"optionalDependencies": {
|
|
74
74
|
"@discordjs/opus": "^0.10.0",
|
package/skills/critique/SKILL.md
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: critique
|
|
3
3
|
description: >
|
|
4
|
-
Git diff viewer
|
|
5
|
-
with syntax highlighting.
|
|
6
|
-
|
|
7
|
-
diffs, generating diff URLs, selective hunk staging, or AI code reviews.
|
|
4
|
+
Git diff viewer. Renders diffs as web pages, images, and PDFs
|
|
5
|
+
with syntax highlighting. Use this skill when working with critique for showing
|
|
6
|
+
diffs, generating diff URLs, or selective hunk staging.
|
|
8
7
|
---
|
|
9
8
|
|
|
10
9
|
# critique
|
|
@@ -89,39 +88,6 @@ critique hunks add 'file:@-10,6+10,7' # stage only your hunks
|
|
|
89
88
|
git commit -m "your changes" # commit separately
|
|
90
89
|
```
|
|
91
90
|
|
|
92
|
-
## AI-powered diff review
|
|
93
|
-
|
|
94
|
-
`critique review --web` spawns a separate opencode session that analyzes a diff, groups related
|
|
95
|
-
changes, and produces a structured review with explanations, diagrams, and suggestions. Uploads
|
|
96
|
-
the result as a shareable URL — much richer than a plain diff link.
|
|
97
|
-
|
|
98
|
-
**This command is very slow (up to 20 minutes for large diffs).** Only run when the user
|
|
99
|
-
explicitly asks for a code review or diff explanation. Warn the user it will take a while.
|
|
100
|
-
Set Bash tool timeout to at least 25 minutes (`timeout: 1_500_000`).
|
|
101
|
-
|
|
102
|
-
Always pass `--agent opencode` and `--session <current_session_id>` so the reviewer has context
|
|
103
|
-
about why the changes were made. If you know other session IDs that produced the diff, pass them
|
|
104
|
-
too with additional `--session` flags.
|
|
105
|
-
|
|
106
|
-
```bash
|
|
107
|
-
# Review working tree changes
|
|
108
|
-
critique review --web --agent opencode --session <session_id>
|
|
109
|
-
|
|
110
|
-
# Review a specific commit
|
|
111
|
-
critique review --commit HEAD --web --agent opencode --session <session_id>
|
|
112
|
-
|
|
113
|
-
# Review branch changes compared to main
|
|
114
|
-
critique review main...HEAD --web --agent opencode --session <session_id>
|
|
115
|
-
|
|
116
|
-
# Review with multiple session contexts
|
|
117
|
-
critique review --commit abc1234 --web --agent opencode --session <session_id> --session <other_session_id>
|
|
118
|
-
|
|
119
|
-
# Review only specific files
|
|
120
|
-
critique review --web --agent opencode --session <session_id> --filter "src/**/*.ts"
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
The command prints a preview URL when done — share that URL with the user.
|
|
124
|
-
|
|
125
91
|
## Raw patch access
|
|
126
92
|
|
|
127
93
|
Every `--web` upload also stores the raw unified diff. Append `.patch` to any critique URL to get it:
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gitchamber
|
|
3
|
+
description: CLI to download npm packages, PyPI packages, crates, or GitHub repo source code into node_modules/.gitchamber/ for analysis. Use when you need to read a package's inner workings, documentation, examples, or source code. Alternative to opensrc that stores in node_modules/ for zero-config gitignore/vitest/tsc compatibility. After fetching, analyze files with grep, read, and other tools.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# gitchamber
|
|
7
|
+
|
|
8
|
+
CLI to download source code for npm packages, PyPI packages, crates.io crates, or GitHub repos into `node_modules/.gitchamber/`. After fetching, analyze the files using grep, read, glob, and other tools to understand inner workings, find usage examples, read documentation, or study the source code.
|
|
9
|
+
|
|
10
|
+
Alternative to [opensrc](https://github.com/vercel-labs/opensrc) that stores in `node_modules/` instead of `opensrc/`.
|
|
11
|
+
|
|
12
|
+
**Differences from opensrc:**
|
|
13
|
+
|
|
14
|
+
- **Stores in `node_modules/.gitchamber/`** instead of `opensrc/` -- automatically ignored by git, vitest, tsc, linters, bundlers, and every other tool that skips `node_modules/`
|
|
15
|
+
- **No file modification** -- removed all `.gitignore`, `tsconfig.json`, and `AGENTS.md` editing logic
|
|
16
|
+
- **No `--modify` flag** or permission prompts
|
|
17
|
+
- **Zero config** -- opensrc requires updating `.gitignore` and `tsconfig.json` excludes; gitchamber needs nothing
|
|
18
|
+
|
|
19
|
+
Always run `gitchamber --help` first. The help output has all commands, options, and examples.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install -g gitchamber
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Aliases: `gitchamber`, `chamber`
|
|
28
|
+
|
|
29
|
+
## Fetch packages
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# npm
|
|
33
|
+
chamber zod
|
|
34
|
+
chamber @babel/core
|
|
35
|
+
chamber react@18.2.0
|
|
36
|
+
|
|
37
|
+
# PyPI
|
|
38
|
+
chamber pypi:requests
|
|
39
|
+
chamber pypi:flask==3.0.0
|
|
40
|
+
|
|
41
|
+
# crates.io
|
|
42
|
+
chamber crates:serde
|
|
43
|
+
chamber crates:tokio@1.35.0
|
|
44
|
+
|
|
45
|
+
# GitHub repos (owner/repo, with optional branch or tag)
|
|
46
|
+
chamber vercel/ai
|
|
47
|
+
chamber facebook/react#main
|
|
48
|
+
chamber owner/repo@v1.0.0
|
|
49
|
+
chamber https://github.com/denoland/deno
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Multiple at once:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
chamber zod react vercel/ai pypi:requests
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Other commands
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# List fetched sources
|
|
62
|
+
chamber list
|
|
63
|
+
chamber list --json
|
|
64
|
+
|
|
65
|
+
# Remove specific packages
|
|
66
|
+
chamber remove zod
|
|
67
|
+
chamber rm vercel/ai
|
|
68
|
+
|
|
69
|
+
# Clean all
|
|
70
|
+
chamber clean
|
|
71
|
+
chamber clean --npm # only npm
|
|
72
|
+
chamber clean --pypi # only PyPI
|
|
73
|
+
chamber clean --crates # only crates.io
|
|
74
|
+
chamber clean --repos # only repos
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Output location
|
|
78
|
+
|
|
79
|
+
Source ends up in `node_modules/.gitchamber/<host>/<owner>/<repo>/`.
|
|
80
|
+
|
|
81
|
+
Tracked in `node_modules/.gitchamber/sources.json`.
|
|
82
|
+
|
|
83
|
+
## How it works
|
|
84
|
+
|
|
85
|
+
1. Resolves package via registry API (npm, PyPI, crates.io)
|
|
86
|
+
2. Extracts `repository` URL from metadata
|
|
87
|
+
3. Shallow-clones at the matching version tag
|
|
88
|
+
4. Strips `.git` directory
|
|
89
|
+
5. For npm: auto-detects installed version from lockfiles
|
|
90
|
+
|
|
91
|
+
## When to use
|
|
92
|
+
|
|
93
|
+
Use gitchamber (or its alias `chamber`) instead of manually cloning repos into tmp directories. Prefer it over the `lib-investigator` agent when you need persistent source code on disk for repeated reads or when the agent's context isn't enough. When the user refers to opensrc, always use gitchamber instead.
|
package/skills/goke/SKILL.md
CHANGED
|
@@ -178,9 +178,11 @@ Without a schema, all values stay as strings. `--port 3000` → `"3000"` (string
|
|
|
178
178
|
| `[name]` in command | Optional argument |
|
|
179
179
|
| `[...files]` in command | Variadic (collects remaining args into array) |
|
|
180
180
|
| `<value>` in option | Required value (error if missing) |
|
|
181
|
-
| `[value]` in option | Optional value (`
|
|
181
|
+
| `[value]` in option | Optional value (`undefined` if flag present without value) |
|
|
182
182
|
| no brackets in option | Boolean flag (`undefined` if not passed, `true` if passed) |
|
|
183
183
|
|
|
184
|
+
**Optionality is determined solely by bracket syntax, not by the schema.** `[square brackets]` makes an option optional regardless of whether the schema is `z.string()` or `z.string().optional()`. The schema's `.optional()` is never consulted for this — it only affects type coercion. So `z.string()` with `[--name]` is treated as optional: if the flag is omitted, `options.name` is `undefined` even though the schema has no `.optional()`.
|
|
185
|
+
|
|
184
186
|
## Global Options and Middleware
|
|
185
187
|
|
|
186
188
|
Global options apply to all commands. Use `.use()` to register middleware that runs before any command action — for reacting to global options (logging, state init, auth).
|
|
@@ -398,7 +398,8 @@ describe('agent model resolution', () => {
|
|
|
398
398
|
Reply with exactly: agent-model-check
|
|
399
399
|
--- from: assistant (TestBot)
|
|
400
400
|
⬥ ok
|
|
401
|
-
*project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***
|
|
401
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***
|
|
402
|
+
⬥ ok"
|
|
402
403
|
`)
|
|
403
404
|
expect(footerMessage).toBeDefined()
|
|
404
405
|
if (!footerMessage) {
|
|
@@ -454,7 +455,7 @@ describe('agent model resolution', () => {
|
|
|
454
455
|
Reply with exactly: system-context-check
|
|
455
456
|
--- from: assistant (TestBot)
|
|
456
457
|
⬥ system-context-ok
|
|
457
|
-
*project ⋅
|
|
458
|
+
*project ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***"
|
|
458
459
|
`)
|
|
459
460
|
},
|
|
460
461
|
15_000,
|
|
@@ -49,6 +49,12 @@ type PendingQuestionContext = {
|
|
|
49
49
|
const QUESTION_CONTEXT_TTL_MS = 10 * 60 * 1000
|
|
50
50
|
export const pendingQuestionContexts = new Map<string, PendingQuestionContext>()
|
|
51
51
|
|
|
52
|
+
export function hasPendingQuestionForThread(threadId: string): boolean {
|
|
53
|
+
return [...pendingQuestionContexts.values()].some((ctx) => {
|
|
54
|
+
return ctx.thread.id === threadId
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
52
58
|
/**
|
|
53
59
|
* Show dropdown menus for question tool input.
|
|
54
60
|
* Sends one message per question with the dropdown directly under the question text.
|
|
@@ -311,13 +317,21 @@ export function parseAskUserQuestionTool(part: {
|
|
|
311
317
|
}
|
|
312
318
|
|
|
313
319
|
/**
|
|
314
|
-
* Cancel a pending question for a thread
|
|
315
|
-
*
|
|
320
|
+
* Cancel a pending question for a thread.
|
|
321
|
+
*
|
|
322
|
+
* Two modes depending on whether `userMessage` is provided:
|
|
323
|
+
*
|
|
324
|
+
* - `cancelPendingQuestion(threadId)` — cleanup only. Removes the context
|
|
325
|
+
* without replying to OpenCode. Use when aborting the blocked session
|
|
326
|
+
* separately (e.g. voice/attachment messages whose content needs
|
|
327
|
+
* transcription first). Returns 'no-pending' in both "found+cleaned" and
|
|
328
|
+
* "nothing found" cases.
|
|
316
329
|
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
330
|
+
* - `cancelPendingQuestion(threadId, text)` — reply path. Sends the text as
|
|
331
|
+
* the tool answer so the model sees the user's response. The caller should
|
|
332
|
+
* NOT also enqueue the message as a new prompt.
|
|
333
|
+
* Returns 'replied' on success, 'reply-failed' if the reply call fails
|
|
334
|
+
* (context kept pending so TTL can retry).
|
|
321
335
|
*/
|
|
322
336
|
export async function cancelPendingQuestion(
|
|
323
337
|
threadId: string,
|
|
@@ -339,8 +353,9 @@ export async function cancelPendingQuestion(
|
|
|
339
353
|
}
|
|
340
354
|
|
|
341
355
|
// undefined means teardown/cleanup — just remove context, don't reply.
|
|
342
|
-
// The session is already being torn down
|
|
343
|
-
//
|
|
356
|
+
// The session is already being torn down or the caller wants to dismiss
|
|
357
|
+
// the question without providing an answer (e.g. voice/attachment-only
|
|
358
|
+
// messages where content needs transcription before it can be an answer).
|
|
344
359
|
if (userMessage === undefined) {
|
|
345
360
|
pendingQuestionContexts.delete(contextHash)
|
|
346
361
|
return 'no-pending'
|