kimaki 0.4.86 → 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/btw.js +111 -0
- 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-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/btw.ts +158 -0
- 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-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
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,
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// /btw command - Fork the current session with full context and send a new prompt.
|
|
2
|
+
// Unlike /fork, this does not replay past messages in Discord. It just creates
|
|
3
|
+
// a new thread, forks the entire session (no messageID), and immediately
|
|
4
|
+
// dispatches the user's prompt so the forked session starts working right away.
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
ChannelType,
|
|
8
|
+
ThreadAutoArchiveDuration,
|
|
9
|
+
type ThreadChannel,
|
|
10
|
+
MessageFlags,
|
|
11
|
+
} from 'discord.js'
|
|
12
|
+
import { getThreadSession, setThreadSession } from '../database.js'
|
|
13
|
+
import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
14
|
+
import {
|
|
15
|
+
resolveWorkingDirectory,
|
|
16
|
+
resolveTextChannel,
|
|
17
|
+
sendThreadMessage,
|
|
18
|
+
} from '../discord-utils.js'
|
|
19
|
+
import { getOrCreateRuntime } from '../session-handler/thread-session-runtime.js'
|
|
20
|
+
import { createLogger, LogPrefix } from '../logger.js'
|
|
21
|
+
import type { CommandContext } from './types.js'
|
|
22
|
+
|
|
23
|
+
const logger = createLogger(LogPrefix.FORK)
|
|
24
|
+
|
|
25
|
+
export async function handleBtwCommand({
|
|
26
|
+
command,
|
|
27
|
+
appId,
|
|
28
|
+
}: CommandContext): Promise<void> {
|
|
29
|
+
const channel = command.channel
|
|
30
|
+
|
|
31
|
+
if (!channel) {
|
|
32
|
+
await command.reply({
|
|
33
|
+
content: 'This command can only be used in a channel',
|
|
34
|
+
flags: MessageFlags.Ephemeral,
|
|
35
|
+
})
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const isThread = [
|
|
40
|
+
ChannelType.PublicThread,
|
|
41
|
+
ChannelType.PrivateThread,
|
|
42
|
+
ChannelType.AnnouncementThread,
|
|
43
|
+
].includes(channel.type)
|
|
44
|
+
|
|
45
|
+
if (!isThread) {
|
|
46
|
+
await command.reply({
|
|
47
|
+
content:
|
|
48
|
+
'This command can only be used in a thread with an active session',
|
|
49
|
+
flags: MessageFlags.Ephemeral,
|
|
50
|
+
})
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const prompt = command.options.getString('prompt', true)
|
|
55
|
+
|
|
56
|
+
const resolved = await resolveWorkingDirectory({
|
|
57
|
+
channel: channel as ThreadChannel,
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
if (!resolved) {
|
|
61
|
+
await command.reply({
|
|
62
|
+
content: 'Could not determine project directory for this channel',
|
|
63
|
+
flags: MessageFlags.Ephemeral,
|
|
64
|
+
})
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const { projectDirectory } = resolved
|
|
69
|
+
|
|
70
|
+
const sessionId = await getThreadSession(channel.id)
|
|
71
|
+
|
|
72
|
+
if (!sessionId) {
|
|
73
|
+
await command.reply({
|
|
74
|
+
content: 'No active session in this thread',
|
|
75
|
+
flags: MessageFlags.Ephemeral,
|
|
76
|
+
})
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
await command.deferReply({ flags: MessageFlags.Ephemeral })
|
|
81
|
+
|
|
82
|
+
const getClient = await initializeOpencodeForDirectory(projectDirectory)
|
|
83
|
+
if (getClient instanceof Error) {
|
|
84
|
+
await command.editReply({
|
|
85
|
+
content: `Failed to fork session: ${getClient.message}`,
|
|
86
|
+
})
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
// Fork the entire session (no messageID = fork at the latest point)
|
|
92
|
+
const forkResponse = await getClient().session.fork({
|
|
93
|
+
sessionID: sessionId,
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
if (!forkResponse.data) {
|
|
97
|
+
await command.editReply('Failed to fork session')
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const forkedSession = forkResponse.data
|
|
102
|
+
|
|
103
|
+
const textChannel = await resolveTextChannel(channel as ThreadChannel)
|
|
104
|
+
if (!textChannel) {
|
|
105
|
+
await command.editReply('Could not resolve parent text channel')
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const threadName = `btw: ${prompt}`.slice(0, 100)
|
|
110
|
+
const thread = await textChannel.threads.create({
|
|
111
|
+
name: threadName,
|
|
112
|
+
autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
|
|
113
|
+
reason: `btw fork from session ${sessionId}`,
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
// Claim the forked session immediately so external polling does not race
|
|
117
|
+
await setThreadSession(thread.id, forkedSession.id)
|
|
118
|
+
|
|
119
|
+
await thread.members.add(command.user.id)
|
|
120
|
+
|
|
121
|
+
logger.log(
|
|
122
|
+
`Created btw fork session ${forkedSession.id} in thread ${thread.id} from ${sessionId}`,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
// Short status message with prompt instead of replaying past messages
|
|
126
|
+
const sourceThreadLink = `<#${channel.id}>`
|
|
127
|
+
await sendThreadMessage(
|
|
128
|
+
thread,
|
|
129
|
+
`Reusing context from ${sourceThreadLink} to answer prompt...\n${prompt}`,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
// Create runtime and dispatch the prompt immediately
|
|
133
|
+
const runtime = getOrCreateRuntime({
|
|
134
|
+
threadId: thread.id,
|
|
135
|
+
thread,
|
|
136
|
+
projectDirectory,
|
|
137
|
+
sdkDirectory: projectDirectory,
|
|
138
|
+
channelId: textChannel.id,
|
|
139
|
+
appId,
|
|
140
|
+
})
|
|
141
|
+
await runtime.enqueueIncoming({
|
|
142
|
+
prompt,
|
|
143
|
+
userId: command.user.id,
|
|
144
|
+
username: command.user.displayName,
|
|
145
|
+
appId,
|
|
146
|
+
mode: 'opencode',
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
await command.editReply(
|
|
150
|
+
`Session forked! Continue in ${thread.toString()}`,
|
|
151
|
+
)
|
|
152
|
+
} catch (error) {
|
|
153
|
+
logger.error('Error in /btw:', error)
|
|
154
|
+
await command.editReply(
|
|
155
|
+
`Failed to fork session: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -249,13 +249,7 @@ export async function registerCommands({
|
|
|
249
249
|
})
|
|
250
250
|
.setDMPermission(false)
|
|
251
251
|
.toJSON(),
|
|
252
|
-
|
|
253
|
-
.setName('toggle-mention-mode')
|
|
254
|
-
.setDescription(
|
|
255
|
-
truncateCommandDescription('Toggle mention-only mode (bot only responds when @mentioned)'),
|
|
256
|
-
)
|
|
257
|
-
.setDMPermission(false)
|
|
258
|
-
.toJSON(),
|
|
252
|
+
|
|
259
253
|
new SlashCommandBuilder()
|
|
260
254
|
.setName('add-project')
|
|
261
255
|
.setDescription(
|
|
@@ -315,11 +309,7 @@ export async function registerCommands({
|
|
|
315
309
|
)
|
|
316
310
|
.setDMPermission(false)
|
|
317
311
|
.toJSON(),
|
|
318
|
-
|
|
319
|
-
.setName('stop')
|
|
320
|
-
.setDescription(truncateCommandDescription('Abort the current OpenCode request in this thread'))
|
|
321
|
-
.setDMPermission(false)
|
|
322
|
-
.toJSON(),
|
|
312
|
+
|
|
323
313
|
new SlashCommandBuilder()
|
|
324
314
|
.setName('share')
|
|
325
315
|
.setDescription(truncateCommandDescription('Share the current session as a public URL'))
|
|
@@ -335,6 +325,18 @@ export async function registerCommands({
|
|
|
335
325
|
.setDescription(truncateCommandDescription('Fork the session from a past user message'))
|
|
336
326
|
.setDMPermission(false)
|
|
337
327
|
.toJSON(),
|
|
328
|
+
new SlashCommandBuilder()
|
|
329
|
+
.setName('btw')
|
|
330
|
+
.setDescription(truncateCommandDescription('Ask something without polluting or blocking the current session'))
|
|
331
|
+
.addStringOption((option) => {
|
|
332
|
+
option
|
|
333
|
+
.setName('prompt')
|
|
334
|
+
.setDescription(truncateCommandDescription('The message to send in the forked session'))
|
|
335
|
+
.setRequired(true)
|
|
336
|
+
return option
|
|
337
|
+
})
|
|
338
|
+
.setDMPermission(false)
|
|
339
|
+
.toJSON(),
|
|
338
340
|
new SlashCommandBuilder()
|
|
339
341
|
.setName('model')
|
|
340
342
|
.setDescription(truncateCommandDescription('Set the preferred model for this channel or session'))
|
|
@@ -456,13 +458,7 @@ export async function registerCommands({
|
|
|
456
458
|
)
|
|
457
459
|
.setDMPermission(false)
|
|
458
460
|
.toJSON(),
|
|
459
|
-
|
|
460
|
-
.setName('memory-snapshot')
|
|
461
|
-
.setDescription(
|
|
462
|
-
truncateCommandDescription('Write a V8 heap snapshot to disk for memory debugging'),
|
|
463
|
-
)
|
|
464
|
-
.setDMPermission(false)
|
|
465
|
-
.toJSON(),
|
|
461
|
+
|
|
466
462
|
new SlashCommandBuilder()
|
|
467
463
|
.setName('upgrade-and-restart')
|
|
468
464
|
.setDescription(
|
|
@@ -494,10 +490,50 @@ export async function registerCommands({
|
|
|
494
490
|
.toJSON(),
|
|
495
491
|
]
|
|
496
492
|
|
|
497
|
-
//
|
|
493
|
+
// Dynamic commands are registered in priority order: agents → user commands → skills → MCP prompts.
|
|
494
|
+
// This ordering matters because we slice to MAX_DISCORD_COMMANDS (100) at the end,
|
|
495
|
+
// so lower-priority dynamic commands get trimmed first if the total exceeds the limit.
|
|
496
|
+
|
|
497
|
+
// 1. Agent-specific quick commands like /plan-agent, /build-agent
|
|
498
|
+
// Filter to primary/all mode agents (same as /agent command shows), excluding hidden agents
|
|
499
|
+
const primaryAgents = agents.filter(
|
|
500
|
+
(a) => (a.mode === 'primary' || a.mode === 'all') && !a.hidden,
|
|
501
|
+
)
|
|
502
|
+
for (const agent of primaryAgents) {
|
|
503
|
+
const sanitizedName = sanitizeAgentName(agent.name)
|
|
504
|
+
// Skip if sanitized name is empty or would create invalid command name
|
|
505
|
+
// Discord command names must start with a lowercase letter or number
|
|
506
|
+
if (!sanitizedName || !/^[a-z0-9]/.test(sanitizedName)) {
|
|
507
|
+
continue
|
|
508
|
+
}
|
|
509
|
+
// Truncate base name before appending suffix so the -agent suffix is never
|
|
510
|
+
// lost to Discord's 32-char command name limit.
|
|
511
|
+
const agentSuffix = '-agent'
|
|
512
|
+
const agentBaseName = sanitizedName.slice(0, 32 - agentSuffix.length)
|
|
513
|
+
const commandName = `${agentBaseName}${agentSuffix}`
|
|
514
|
+
const description = buildQuickAgentCommandDescription({
|
|
515
|
+
agentName: agent.name,
|
|
516
|
+
description: agent.description,
|
|
517
|
+
})
|
|
518
|
+
|
|
519
|
+
commands.push(
|
|
520
|
+
new SlashCommandBuilder()
|
|
521
|
+
.setName(commandName)
|
|
522
|
+
.setDescription(truncateCommandDescription(description))
|
|
523
|
+
.setDMPermission(false)
|
|
524
|
+
.toJSON(),
|
|
525
|
+
)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// 2. User-defined commands, skills, and MCP prompts (ordered by priority)
|
|
498
529
|
// Also populate registeredUserCommands in the store for /queue-command autocomplete
|
|
499
530
|
const newRegisteredCommands: RegisteredUserCommand[] = []
|
|
500
|
-
|
|
531
|
+
// Sort: regular commands first, then skills, then MCP prompts
|
|
532
|
+
const sourceOrder: Record<string, number> = { config: 0, skill: 1, mcp: 2 }
|
|
533
|
+
const sortedUserCommands = [...userCommands].sort((a, b) => {
|
|
534
|
+
return (sourceOrder[a.source || ''] ?? 0) - (sourceOrder[b.source || ''] ?? 0)
|
|
535
|
+
})
|
|
536
|
+
for (const cmd of sortedUserCommands) {
|
|
501
537
|
if (SKIP_USER_COMMANDS.includes(cmd.name)) {
|
|
502
538
|
continue
|
|
503
539
|
}
|
|
@@ -549,35 +585,14 @@ export async function registerCommands({
|
|
|
549
585
|
}
|
|
550
586
|
store.setState({ registeredUserCommands: newRegisteredCommands })
|
|
551
587
|
|
|
552
|
-
//
|
|
553
|
-
//
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
const sanitizedName = sanitizeAgentName(agent.name)
|
|
559
|
-
// Skip if sanitized name is empty or would create invalid command name
|
|
560
|
-
// Discord command names must start with a lowercase letter or number
|
|
561
|
-
if (!sanitizedName || !/^[a-z0-9]/.test(sanitizedName)) {
|
|
562
|
-
continue
|
|
563
|
-
}
|
|
564
|
-
// Truncate base name before appending suffix so the -agent suffix is never
|
|
565
|
-
// lost to Discord's 32-char command name limit.
|
|
566
|
-
const agentSuffix = '-agent'
|
|
567
|
-
const agentBaseName = sanitizedName.slice(0, 32 - agentSuffix.length)
|
|
568
|
-
const commandName = `${agentBaseName}${agentSuffix}`
|
|
569
|
-
const description = buildQuickAgentCommandDescription({
|
|
570
|
-
agentName: agent.name,
|
|
571
|
-
description: agent.description,
|
|
572
|
-
})
|
|
573
|
-
|
|
574
|
-
commands.push(
|
|
575
|
-
new SlashCommandBuilder()
|
|
576
|
-
.setName(commandName)
|
|
577
|
-
.setDescription(truncateCommandDescription(description))
|
|
578
|
-
.setDMPermission(false)
|
|
579
|
-
.toJSON(),
|
|
588
|
+
// Discord allows max 100 guild commands. Slice to stay within the limit,
|
|
589
|
+
// trimming lowest-priority dynamic commands (MCP prompts, then skills) first.
|
|
590
|
+
const MAX_DISCORD_COMMANDS = 100
|
|
591
|
+
if (commands.length > MAX_DISCORD_COMMANDS) {
|
|
592
|
+
cliLogger.warn(
|
|
593
|
+
`COMMANDS: ${commands.length} commands exceed Discord limit of ${MAX_DISCORD_COMMANDS}, truncating to ${MAX_DISCORD_COMMANDS}`,
|
|
580
594
|
)
|
|
595
|
+
commands.length = MAX_DISCORD_COMMANDS
|
|
581
596
|
}
|
|
582
597
|
|
|
583
598
|
const rest = createDiscordRest(token)
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
import { handleToggleWorktreesCommand } from './commands/worktree-settings.js'
|
|
24
24
|
import { handleWorktreesCommand } from './commands/worktrees.js'
|
|
25
25
|
import { handleTasksCommand } from './commands/tasks.js'
|
|
26
|
-
|
|
26
|
+
|
|
27
27
|
import {
|
|
28
28
|
handleResumeCommand,
|
|
29
29
|
handleResumeAutocomplete,
|
|
@@ -43,6 +43,7 @@ import { handleCompactCommand } from './commands/compact.js'
|
|
|
43
43
|
import { handleShareCommand } from './commands/share.js'
|
|
44
44
|
import { handleDiffCommand } from './commands/diff.js'
|
|
45
45
|
import { handleForkCommand, handleForkSelectMenu } from './commands/fork.js'
|
|
46
|
+
import { handleBtwCommand } from './commands/btw.js'
|
|
46
47
|
import {
|
|
47
48
|
handleModelCommand,
|
|
48
49
|
handleProviderSelectMenu,
|
|
@@ -93,7 +94,7 @@ import { handleRestartOpencodeServerCommand } from './commands/restart-opencode-
|
|
|
93
94
|
import { handleRunCommand } from './commands/run-command.js'
|
|
94
95
|
import { handleContextUsageCommand } from './commands/context-usage.js'
|
|
95
96
|
import { handleSessionIdCommand } from './commands/session-id.js'
|
|
96
|
-
|
|
97
|
+
|
|
97
98
|
import { handleUpgradeAndRestartCommand } from './commands/upgrade.js'
|
|
98
99
|
import { handleMcpCommand, handleMcpSelectMenu } from './commands/mcp.js'
|
|
99
100
|
import {
|
|
@@ -218,12 +219,6 @@ export function registerInteractionHandler({
|
|
|
218
219
|
})
|
|
219
220
|
return
|
|
220
221
|
|
|
221
|
-
case 'toggle-mention-mode':
|
|
222
|
-
await handleToggleMentionModeCommand({
|
|
223
|
-
command: interaction,
|
|
224
|
-
appId,
|
|
225
|
-
})
|
|
226
|
-
return
|
|
227
222
|
|
|
228
223
|
case 'resume':
|
|
229
224
|
await handleResumeCommand({ command: interaction, appId })
|
|
@@ -245,7 +240,6 @@ export function registerInteractionHandler({
|
|
|
245
240
|
return
|
|
246
241
|
|
|
247
242
|
case 'abort':
|
|
248
|
-
case 'stop':
|
|
249
243
|
await handleAbortCommand({ command: interaction, appId })
|
|
250
244
|
return
|
|
251
245
|
|
|
@@ -265,6 +259,10 @@ export function registerInteractionHandler({
|
|
|
265
259
|
await handleForkCommand(interaction)
|
|
266
260
|
return
|
|
267
261
|
|
|
262
|
+
case 'btw':
|
|
263
|
+
await handleBtwCommand({ command: interaction, appId })
|
|
264
|
+
return
|
|
265
|
+
|
|
268
266
|
case 'model':
|
|
269
267
|
await handleModelCommand({ interaction, appId })
|
|
270
268
|
return
|
|
@@ -328,12 +326,7 @@ export function registerInteractionHandler({
|
|
|
328
326
|
await handleSessionIdCommand({ command: interaction, appId })
|
|
329
327
|
return
|
|
330
328
|
|
|
331
|
-
|
|
332
|
-
await handleMemorySnapshotCommand({
|
|
333
|
-
command: interaction,
|
|
334
|
-
appId,
|
|
335
|
-
})
|
|
336
|
-
return
|
|
329
|
+
|
|
337
330
|
|
|
338
331
|
case 'upgrade-and-restart':
|
|
339
332
|
await handleUpgradeAndRestartCommand({
|
package/src/markdown.test.ts
CHANGED
|
@@ -222,6 +222,22 @@ test('generate markdown with system info', async () => {
|
|
|
222
222
|
|
|
223
223
|
|
|
224
224
|
*Completed in Xs*
|
|
225
|
+
|
|
226
|
+
### 🤖 Assistant (deterministic-v2)
|
|
227
|
+
|
|
228
|
+
**Started using deterministic-provider/deterministic-v2**
|
|
229
|
+
|
|
230
|
+
Hello! This is a deterministic markdown test response.
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
*Completed in Xs*
|
|
234
|
+
|
|
235
|
+
### 🤖 Assistant (deterministic-v2)
|
|
236
|
+
|
|
237
|
+
**Started using deterministic-provider/deterministic-v2**
|
|
238
|
+
|
|
239
|
+
Hello! This is a deterministic markdown test response.
|
|
240
|
+
|
|
225
241
|
"
|
|
226
242
|
`)
|
|
227
243
|
})
|
|
@@ -261,6 +277,22 @@ test('generate markdown without system info', async () => {
|
|
|
261
277
|
|
|
262
278
|
|
|
263
279
|
*Completed in Xs*
|
|
280
|
+
|
|
281
|
+
### 🤖 Assistant (deterministic-v2)
|
|
282
|
+
|
|
283
|
+
**Started using deterministic-provider/deterministic-v2**
|
|
284
|
+
|
|
285
|
+
Hello! This is a deterministic markdown test response.
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
*Completed in Xs*
|
|
289
|
+
|
|
290
|
+
### 🤖 Assistant (deterministic-v2)
|
|
291
|
+
|
|
292
|
+
**Started using deterministic-provider/deterministic-v2**
|
|
293
|
+
|
|
294
|
+
Hello! This is a deterministic markdown test response.
|
|
295
|
+
|
|
264
296
|
"
|
|
265
297
|
`)
|
|
266
298
|
})
|