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.
Files changed (34) hide show
  1. package/dist/agent-model.e2e.test.js +3 -2
  2. package/dist/commands/ask-question.js +22 -8
  3. package/dist/commands/btw.js +111 -0
  4. package/dist/discord-bot.js +24 -8
  5. package/dist/discord-command-registration.js +53 -41
  6. package/dist/interaction-handler.js +4 -15
  7. package/dist/markdown.test.js +32 -0
  8. package/dist/queue-advanced-footer.e2e.test.js +40 -3
  9. package/dist/queue-advanced-model-switch.e2e.test.js +6 -0
  10. package/dist/queue-advanced-permissions-typing.e2e.test.js +1 -0
  11. package/dist/queue-advanced-question.e2e.test.js +108 -34
  12. package/dist/queue-advanced-typing-interrupt.e2e.test.js +8 -2
  13. package/dist/runtime-lifecycle.e2e.test.js +4 -1
  14. package/dist/thread-message-queue.e2e.test.js +2 -5
  15. package/dist/voice-message.e2e.test.js +6 -1
  16. package/package.json +4 -4
  17. package/skills/critique/SKILL.md +3 -37
  18. package/skills/gitchamber/SKILL.md +93 -0
  19. package/skills/goke/SKILL.md +3 -1
  20. package/src/agent-model.e2e.test.ts +3 -2
  21. package/src/commands/ask-question.ts +23 -8
  22. package/src/commands/btw.ts +158 -0
  23. package/src/discord-bot.ts +23 -8
  24. package/src/discord-command-registration.ts +64 -49
  25. package/src/interaction-handler.ts +8 -15
  26. package/src/markdown.test.ts +32 -0
  27. package/src/queue-advanced-footer.e2e.test.ts +40 -3
  28. package/src/queue-advanced-model-switch.e2e.test.ts +6 -0
  29. package/src/queue-advanced-permissions-typing.e2e.test.ts +1 -0
  30. package/src/queue-advanced-question.e2e.test.ts +129 -35
  31. package/src/queue-advanced-typing-interrupt.e2e.test.ts +8 -2
  32. package/src/runtime-lifecycle.e2e.test.ts +4 -1
  33. package/src/thread-message-queue.e2e.test.ts +2 -5
  34. 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
- // be consumed as the answer and NOT also sent as a duplicate promptAsync.
3
- // Reproduces the bug from commit a4dfb01 where the same message was sent twice.
4
- import { describe, test, expect } from 'vitest';
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
- describe('queue advanced: question tool text answer', () => {
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
- test('user text message answers pending question without sending duplicate prompt', async () => {
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. Answer the question via cancelPendingQuestion (consumed as answer)
73
- // 2. NOT also send as a new promptAsync (the fix)
74
- // 3. Clean up the pending question context
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 after answer
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 "my text answer" message must appear in the thread
104
+ // The user's message must appear in Discord
112
105
  expect(timeline).toContain('my text answer');
113
- // Key regression assertion: without the fix, the user's text message
114
- // is ALSO sent as a duplicate promptAsync which triggers a THIRD question
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(2);
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.85",
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.0"
70
+ "traforo": "^0.2.2",
71
+ "errore": "^0.14.1"
72
72
  },
73
73
  "optionalDependencies": {
74
74
  "@discordjs/opus": "^0.10.0",
@@ -1,10 +1,9 @@
1
1
  ---
2
2
  name: critique
3
3
  description: >
4
- Git diff viewer and AI reviewer. Renders diffs as web pages, images, and PDFs
5
- with syntax highlighting. Also provides AI-powered diff reviews via
6
- `critique review --web`. Use this skill when working with critique for showing
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.
@@ -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 (`true` if flag present without 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 ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***"
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 (e.g., when user sends a new message).
315
- * Sends the user's message as the answer to OpenCode so the model sees their actual response.
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
- * Returns 'replied' if the question was answered successfully (caller should NOT
318
- * enqueue the user message as a new prompt it was consumed as the answer).
319
- * Returns 'reply-failed' if reply failed (context kept pending so TTL can retry).
320
- * Returns 'no-pending' if no question was pending for this thread.
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. Empty string '' is a valid
343
- // user message (attachment-only, voice, etc.) and must still go through.
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'