kimaki 0.9.0 → 0.9.1
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/cli-commands/user.js +9 -9
- package/dist/discord-bot.js +15 -4
- package/dist/onboarding-tutorial.js +1 -1
- package/dist/system-message.js +7 -7
- package/dist/system-message.test.js +27 -24
- package/dist/voice-message.e2e.test.js +7 -0
- package/dist/worktree-lifecycle.e2e.test.js +80 -4
- package/dist/worktrees.js +7 -0
- package/package.json +4 -4
- package/skills/parallel-security-review/SKILL.md +332 -0
- package/src/cli-commands/user.ts +9 -10
- package/src/discord-bot.ts +20 -4
- package/src/onboarding-tutorial.ts +1 -1
- package/src/system-message.test.ts +27 -24
- package/src/system-message.ts +7 -7
- package/src/voice-message.e2e.test.ts +8 -0
- package/src/worktree-lifecycle.e2e.test.ts +94 -3
- package/src/worktrees.ts +8 -0
|
@@ -89,25 +89,25 @@ cli
|
|
|
89
89
|
});
|
|
90
90
|
cli
|
|
91
91
|
.command('tunnel', 'Expose a local port via tunnel')
|
|
92
|
-
.option('-p, --port <port>', 'Local port to expose (
|
|
92
|
+
.option('-p, --port <port>', 'Local port to expose (optional when command output reveals one)')
|
|
93
93
|
.option('-t, --tunnel-id [id]', 'Custom tunnel ID (only for services safe to expose publicly; prefer random default)')
|
|
94
94
|
.option('-h, --host [host]', 'Local host (default: localhost)')
|
|
95
95
|
.option('-s, --server [url]', 'Tunnel server URL')
|
|
96
96
|
.option('-k, --kill', 'Kill any existing process on the port before starting')
|
|
97
97
|
.action(async (options) => {
|
|
98
|
-
const { runTunnel, parseCommandFromArgv
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
cliLogger.error(
|
|
98
|
+
const { runTunnel, parseCommandFromArgv } = await import('traforo/run-tunnel');
|
|
99
|
+
const { command } = parseCommandFromArgv(process.argv);
|
|
100
|
+
if (!options.port && command.length === 0) {
|
|
101
|
+
cliLogger.error('Error: --port is required unless a command is provided after --');
|
|
102
|
+
cliLogger.error(`\nUsage: kimaki tunnel [-- command]`);
|
|
103
|
+
cliLogger.error(` or: kimaki tunnel --port <port>`);
|
|
102
104
|
process.exit(EXIT_NO_RESTART);
|
|
103
105
|
}
|
|
104
|
-
const port = parseInt(options.port, 10);
|
|
105
|
-
if (
|
|
106
|
+
const port = options.port ? parseInt(options.port, 10) : undefined;
|
|
107
|
+
if (options.port && (!port || port < 1 || port > 65535)) {
|
|
106
108
|
cliLogger.error(`Error: Invalid port number: ${options.port}`);
|
|
107
109
|
process.exit(EXIT_NO_RESTART);
|
|
108
110
|
}
|
|
109
|
-
// Parse command after -- from argv
|
|
110
|
-
const { command } = parseCommandFromArgv(process.argv);
|
|
111
111
|
await runTunnel({
|
|
112
112
|
port,
|
|
113
113
|
tunnelId: options.tunnelId || undefined,
|
package/dist/discord-bot.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { initDatabase, closeDatabase, getThreadWorktree, getThreadSession, getChannelWorktreesEnabled, getChannelMentionMode, getChannelDirectory, getPrisma, cancelAllPendingIpcRequests, deleteChannelDirectoryById, createPendingWorktree, setWorktreeReady, } from './database.js';
|
|
5
5
|
import { stopOpencodeServer, } from './opencode.js';
|
|
6
6
|
import { formatAutoWorktreeName, createWorktreeInBackground, worktreeCreatingMessage } from './commands/new-worktree.js';
|
|
7
|
-
import { validateWorktreeDirectory, git } from './worktrees.js';
|
|
7
|
+
import { validateWorktreeDirectory, git, isGitRepositoryRoot } from './worktrees.js';
|
|
8
8
|
import { WORKTREE_PREFIX } from './commands/merge-worktree.js';
|
|
9
9
|
import { escapeBackticksInCodeBlocks, splitMarkdownForDiscord, sendThreadMessage, SILENT_MESSAGE_FLAGS, NOTIFY_MESSAGE_FLAGS, reactToThread, stripMentions, hasKimakiBotPermission, hasNoKimakiRole, } from './discord-utils.js';
|
|
10
10
|
import { getOpencodeSystemMessage, isInjectedPromptMarker, } from './system-message.js';
|
|
@@ -634,8 +634,16 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
|
|
|
634
634
|
: stripMentions(message.content || '')
|
|
635
635
|
.replace(/\s+/g, ' ')
|
|
636
636
|
.trim() || 'kimaki thread';
|
|
637
|
-
// Check if worktrees should be enabled (CLI flag OR channel setting)
|
|
638
|
-
|
|
637
|
+
// Check if worktrees should be enabled (CLI flag OR channel setting).
|
|
638
|
+
// Only create worktrees from the configured project directory when that
|
|
639
|
+
// directory is itself the git root. If the user registered a non-git
|
|
640
|
+
// workspace folder under a larger repo, git would create the worktree
|
|
641
|
+
// from the parent repo and strand follow-up messages on failure.
|
|
642
|
+
const wantsWorktrees = useWorktrees || (await getChannelWorktreesEnabled(channel.id));
|
|
643
|
+
const shouldUseWorktrees = wantsWorktrees && (await isGitRepositoryRoot(projectDirectory));
|
|
644
|
+
if (wantsWorktrees && !shouldUseWorktrees) {
|
|
645
|
+
discordLogger.warn(`[WORKTREE] Skipping automatic worktree for non-git project directory: ${projectDirectory}`);
|
|
646
|
+
}
|
|
639
647
|
// Add worktree prefix if worktrees are enabled
|
|
640
648
|
const threadName = shouldUseWorktrees
|
|
641
649
|
? `${WORKTREE_PREFIX}${baseThreadName}`
|
|
@@ -800,7 +808,7 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
|
|
|
800
808
|
// The runtime is created immediately so follow-up messages queue
|
|
801
809
|
// naturally; the worktree promise is awaited inside enqueueIncoming.
|
|
802
810
|
let worktreePromise;
|
|
803
|
-
if (marker.worktree) {
|
|
811
|
+
if (marker.worktree && (await isGitRepositoryRoot(projectDirectory))) {
|
|
804
812
|
discordLogger.log(`[BOT_SESSION] Creating worktree: ${marker.worktree}`);
|
|
805
813
|
const worktreeStatusMessage = await thread
|
|
806
814
|
.send({
|
|
@@ -816,6 +824,9 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
|
|
|
816
824
|
rest: discordClient.rest,
|
|
817
825
|
});
|
|
818
826
|
}
|
|
827
|
+
else if (marker.worktree) {
|
|
828
|
+
discordLogger.warn(`[BOT_SESSION] Skipping requested worktree for non-git project directory: ${projectDirectory}`);
|
|
829
|
+
}
|
|
819
830
|
// --cwd: reuse an existing worktree directory. Revalidate at bot-time
|
|
820
831
|
// (CLI validated at send-time but the path could become stale).
|
|
821
832
|
// Store in thread_worktrees as ready with origin=external so
|
|
@@ -132,7 +132,7 @@ Pick a random port between 3000-9000 to avoid conflicts:
|
|
|
132
132
|
|
|
133
133
|
${backticks}bash
|
|
134
134
|
PORT=$((RANDOM % 6000 + 3000))
|
|
135
|
-
bunx tuistory launch "PORT=$PORT kimaki tunnel
|
|
135
|
+
bunx tuistory launch "PORT=$PORT kimaki tunnel -- bun run server.ts" -s game-dev --cwd "$PWD"
|
|
136
136
|
${backticks}
|
|
137
137
|
|
|
138
138
|
Wait a moment, then get the tunnel URL:
|
package/dist/system-message.js
CHANGED
|
@@ -136,11 +136,11 @@ Use a tuistory session with a descriptive name like \`projectname-dev\` so you c
|
|
|
136
136
|
|
|
137
137
|
Use random tunnel IDs by default. Only pass \`-t\` when exposing a service that is safe to be publicly discoverable.
|
|
138
138
|
|
|
139
|
-
\`kimaki tunnel\` injects \`TRAFORO_URL\` into the child process. Prefer wiring your app to that URL so OAuth callbacks, webhook URLs, and absolute links use the public tunnel instead of localhost.
|
|
139
|
+
\`kimaki tunnel\` injects \`TRAFORO_URL\` into the child process. Prefer wiring your app to that URL so OAuth callbacks, webhook URLs, and absolute links use the public tunnel instead of localhost. The local port is detected from the child process output, so do not pass \`-p\` when launching a dev server command unless detection fails.
|
|
140
140
|
|
|
141
141
|
\`\`\`bash
|
|
142
142
|
# Start the dev server in a named background session
|
|
143
|
-
bunx tuistory launch "kimaki tunnel
|
|
143
|
+
bunx tuistory launch "kimaki tunnel -- pnpm dev" -s myapp-dev
|
|
144
144
|
|
|
145
145
|
# Wait until the dev server prints something useful, then inspect it
|
|
146
146
|
bunx tuistory -s myapp-dev wait "/ready|local|tunnel/i" --timeout 30000
|
|
@@ -149,7 +149,7 @@ bunx tuistory read -s myapp-dev
|
|
|
149
149
|
|
|
150
150
|
### passing the public URL to your app
|
|
151
151
|
|
|
152
|
-
If you launch the server command through \`kimaki tunnel -- ...\`, the local port is auto-detected from the child process logs in many common dev-server setups
|
|
152
|
+
If you launch the server command through \`kimaki tunnel -- ...\`, the local port is auto-detected from the child process logs in many common dev-server setups. Use \`--port\` only when the dev server does not print a detectable localhost URL or port line.
|
|
153
153
|
|
|
154
154
|
\`\`\`bash
|
|
155
155
|
# Your app can read process.env.TRAFORO_URL directly
|
|
@@ -176,13 +176,13 @@ bunx tuistory read -s myapp-dev
|
|
|
176
176
|
|
|
177
177
|
\`\`\`bash
|
|
178
178
|
# Next.js project
|
|
179
|
-
bunx tuistory launch "kimaki tunnel
|
|
179
|
+
bunx tuistory launch "kimaki tunnel -- pnpm dev" -s projectname-nextjs-dev
|
|
180
180
|
|
|
181
|
-
# Vite project
|
|
182
|
-
bunx tuistory launch "kimaki tunnel
|
|
181
|
+
# Vite project
|
|
182
|
+
bunx tuistory launch "kimaki tunnel -- pnpm dev" -s vite-dev
|
|
183
183
|
|
|
184
184
|
# Custom tunnel ID (only for intentionally public-safe services)
|
|
185
|
-
bunx tuistory launch "kimaki tunnel -
|
|
185
|
+
bunx tuistory launch "kimaki tunnel -t holocron -- pnpm dev" -s holocron-dev
|
|
186
186
|
\`\`\`
|
|
187
187
|
|
|
188
188
|
### stopping the dev server
|
|
@@ -87,23 +87,26 @@ describe('system-message', () => {
|
|
|
87
87
|
|
|
88
88
|
Only do this when the user explicitly asks to close or archive the thread, and only after your final message.
|
|
89
89
|
|
|
90
|
-
##
|
|
90
|
+
## discord user mentions
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
Prefer Discord user IDs for mentions. Discord bots cannot ping by @name; use \`<@userId>\` in message text or pass the ID to \`--user\`.
|
|
93
|
+
The current user's ID is available in the per-turn \`<discord-user ... user-id="..." />\` metadata.
|
|
94
|
+
|
|
95
|
+
To search for Discord users in a guild as a best-effort fallback, run:
|
|
93
96
|
|
|
94
97
|
kimaki user list --guild guild_123 --query "username"
|
|
95
98
|
|
|
96
|
-
This returns user IDs you can use for Discord mentions.
|
|
99
|
+
This returns user IDs you can use for Discord mentions. It can fail when Server Members Intent is disabled, so prefer IDs from existing Discord metadata or raw mentions when possible.
|
|
97
100
|
|
|
98
101
|
## starting new sessions from CLI
|
|
99
102
|
|
|
100
103
|
To start a new thread/session in this channel pro-grammatically, run:
|
|
101
104
|
|
|
102
|
-
kimaki send --channel chan_123 --prompt 'your prompt here' --agent <current_agent> --user '
|
|
105
|
+
kimaki send --channel chan_123 --prompt 'your prompt here' --agent <current_agent> --user '<discord-user-id>'
|
|
103
106
|
|
|
104
107
|
You can use this to "spawn" parallel helper sessions like teammates: start new threads with focused prompts, then come back and collect the results.
|
|
105
108
|
Prefer passing the current agent with \`--agent <current_agent>\` so spawned or scheduled sessions keep the same agent unless you are intentionally switching. Replace \`<current_agent>\` with the value from the per-turn \`Current agent\` reminder.
|
|
106
|
-
When writing \`kimaki send\` shell commands, use single quotes around \`--prompt\`, \`--user\`, \`--send-at\`, and other literal arguments so backticks inside prompts are not interpreted by the shell.
|
|
109
|
+
When writing \`kimaki send\` shell commands, use single quotes around \`--prompt\`, \`--user\`, \`--send-at\`, and other literal arguments so backticks inside prompts are not interpreted by the shell. Prefer \`--user '<discord-user-id>'\` over \`--user 'name'\` because name lookup depends on optional Server Members Intent.
|
|
107
110
|
|
|
108
111
|
IMPORTANT: NEVER use \`--worktree\` unless the user explicitly asks for a worktree. Default to creating normal threads without worktrees.
|
|
109
112
|
|
|
@@ -121,19 +124,19 @@ describe('system-message', () => {
|
|
|
121
124
|
|
|
122
125
|
Use --notify-only to create a notification thread without starting an AI session:
|
|
123
126
|
|
|
124
|
-
kimaki send --channel chan_123 --prompt 'User cancelled subscription' --notify-only --agent <current_agent> --user '
|
|
127
|
+
kimaki send --channel chan_123 --prompt 'User cancelled subscription' --notify-only --agent <current_agent> --user '<discord-user-id>'
|
|
125
128
|
|
|
126
|
-
Use --user to add a specific Discord user to the new thread:
|
|
129
|
+
Use --user with a Discord user ID or raw mention to add a specific Discord user to the new thread:
|
|
127
130
|
|
|
128
|
-
kimaki send --channel chan_123 --prompt 'Review the latest CI failure' --agent <current_agent> --user '
|
|
131
|
+
kimaki send --channel chan_123 --prompt 'Review the latest CI failure' --agent <current_agent> --user '<discord-user-id>'
|
|
129
132
|
|
|
130
133
|
Use --worktree to create a git worktree for the session (ONLY when the user explicitly asks for a worktree):
|
|
131
134
|
|
|
132
|
-
kimaki send --channel chan_123 --prompt 'Add dark mode support' --worktree dark-mode --agent <current_agent> --user '
|
|
135
|
+
kimaki send --channel chan_123 --prompt 'Add dark mode support' --worktree dark-mode --agent <current_agent> --user '<discord-user-id>'
|
|
133
136
|
|
|
134
137
|
Use --cwd to start a session in an existing git worktree directory (must be a worktree of the project):
|
|
135
138
|
|
|
136
|
-
kimaki send --channel chan_123 --prompt 'Continue work on feature' --cwd /path/to/existing-worktree --agent <current_agent> --user '
|
|
139
|
+
kimaki send --channel chan_123 --prompt 'Continue work on feature' --cwd /path/to/existing-worktree --agent <current_agent> --user '<discord-user-id>'
|
|
137
140
|
|
|
138
141
|
Important:
|
|
139
142
|
- NEVER use \`--worktree\` unless the user explicitly requests a worktree. Most tasks should use normal threads without worktrees.
|
|
@@ -144,7 +147,7 @@ describe('system-message', () => {
|
|
|
144
147
|
|
|
145
148
|
Use --agent to specify which agent to use for the session:
|
|
146
149
|
|
|
147
|
-
kimaki send --channel chan_123 --prompt 'Plan the refactor of the auth module' --agent plan --user '
|
|
150
|
+
kimaki send --channel chan_123 --prompt 'Plan the refactor of the auth module' --agent plan --user '<discord-user-id>'
|
|
148
151
|
|
|
149
152
|
|
|
150
153
|
Available agents:
|
|
@@ -156,7 +159,7 @@ describe('system-message', () => {
|
|
|
156
159
|
You can trigger registered opencode commands (slash commands, skills, MCP prompts) by starting the \`--prompt\` with \`/commandname\`:
|
|
157
160
|
|
|
158
161
|
kimaki send --thread <thread_id> --prompt '/review fix the auth module' --agent <current_agent>
|
|
159
|
-
kimaki send --channel chan_123 --prompt '/build-cmd update dependencies' --agent <current_agent> --user '
|
|
162
|
+
kimaki send --channel chan_123 --prompt '/build-cmd update dependencies' --agent <current_agent> --user '<discord-user-id>'
|
|
160
163
|
|
|
161
164
|
The command name must match a registered opencode command. If the command is not recognized, the prompt is sent as plain text to the model. This works for both new threads (\`--channel\`) and existing threads (\`--thread\`/\`--session\`).
|
|
162
165
|
|
|
@@ -172,8 +175,8 @@ describe('system-message', () => {
|
|
|
172
175
|
|
|
173
176
|
Use \`--send-at\` to schedule a one-time or recurring task:
|
|
174
177
|
|
|
175
|
-
kimaki send --channel chan_123 --prompt 'Reminder: review open PRs' --send-at '2026-03-01T09:00:00Z' --agent <current_agent> --user '
|
|
176
|
-
kimaki send --channel chan_123 --prompt 'Run weekly test suite and summarize failures' --send-at '0 9 * * 1' --agent <current_agent> --user '
|
|
178
|
+
kimaki send --channel chan_123 --prompt 'Reminder: review open PRs' --send-at '2026-03-01T09:00:00Z' --agent <current_agent> --user '<discord-user-id>'
|
|
179
|
+
kimaki send --channel chan_123 --prompt 'Run weekly test suite and summarize failures' --send-at '0 9 * * 1' --agent <current_agent> --user '<discord-user-id>'
|
|
177
180
|
|
|
178
181
|
ALL scheduling is in UTC. Dates must be UTC ISO format ending with \`Z\`. Cron expressions also fire in UTC (e.g. \`0 9 * * 1\` means 9:00 UTC every Monday).
|
|
179
182
|
When the user specifies a time without a timezone, ask them to confirm their timezone or the UTC equivalent. Never guess the user's timezone.
|
|
@@ -231,7 +234,7 @@ describe('system-message', () => {
|
|
|
231
234
|
When the user asks to "create a worktree" or "make a worktree", they mean you should use the kimaki CLI to create it. Do NOT use raw \`git worktree add\` commands. Instead use:
|
|
232
235
|
|
|
233
236
|
\`\`\`bash
|
|
234
|
-
kimaki send --channel chan_123 --prompt 'your task description' --worktree worktree-name --agent <current_agent> --user '
|
|
237
|
+
kimaki send --channel chan_123 --prompt 'your task description' --worktree worktree-name --agent <current_agent> --user '<discord-user-id>'
|
|
235
238
|
\`\`\`
|
|
236
239
|
|
|
237
240
|
This creates a new Discord thread with an isolated git worktree and starts a session in it. The worktree name should be kebab-case and descriptive of the task.
|
|
@@ -247,7 +250,7 @@ describe('system-message', () => {
|
|
|
247
250
|
Use \`--cwd\` to start a session in an existing git worktree directory instead of creating a new one:
|
|
248
251
|
|
|
249
252
|
\`\`\`bash
|
|
250
|
-
kimaki send --channel chan_123 --prompt 'Continue work on feature X' --cwd /path/to/existing-worktree --agent <current_agent> --user '
|
|
253
|
+
kimaki send --channel chan_123 --prompt 'Continue work on feature X' --cwd /path/to/existing-worktree --agent <current_agent> --user '<discord-user-id>'
|
|
251
254
|
\`\`\`
|
|
252
255
|
|
|
253
256
|
The path must be a git worktree of the project (validated via \`git worktree list\`). The session resolves to the correct project channel but uses the worktree as its working directory. Use \`--worktree\` to create a new worktree, \`--cwd\` to reuse an existing one.
|
|
@@ -261,7 +264,7 @@ describe('system-message', () => {
|
|
|
261
264
|
When you are approaching the **context window limit** or the user explicitly asks to **handoff to a new thread**, use the \`kimaki send\` command to start a fresh session with context:
|
|
262
265
|
|
|
263
266
|
\`\`\`bash
|
|
264
|
-
kimaki send --channel chan_123 --prompt 'Continuing from previous session: <summary of current task and state>' --agent <current_agent> --user '
|
|
267
|
+
kimaki send --channel chan_123 --prompt 'Continuing from previous session: <summary of current task and state>' --agent <current_agent> --user '<discord-user-id>'
|
|
265
268
|
\`\`\`
|
|
266
269
|
|
|
267
270
|
The command automatically handles long prompts (over 2000 chars) by sending them as file attachments.
|
|
@@ -494,11 +497,11 @@ describe('system-message', () => {
|
|
|
494
497
|
|
|
495
498
|
Use random tunnel IDs by default. Only pass \`-t\` when exposing a service that is safe to be publicly discoverable.
|
|
496
499
|
|
|
497
|
-
\`kimaki tunnel\` injects \`TRAFORO_URL\` into the child process. Prefer wiring your app to that URL so OAuth callbacks, webhook URLs, and absolute links use the public tunnel instead of localhost.
|
|
500
|
+
\`kimaki tunnel\` injects \`TRAFORO_URL\` into the child process. Prefer wiring your app to that URL so OAuth callbacks, webhook URLs, and absolute links use the public tunnel instead of localhost. The local port is detected from the child process output, so do not pass \`-p\` when launching a dev server command unless detection fails.
|
|
498
501
|
|
|
499
502
|
\`\`\`bash
|
|
500
503
|
# Start the dev server in a named background session
|
|
501
|
-
bunx tuistory launch "kimaki tunnel
|
|
504
|
+
bunx tuistory launch "kimaki tunnel -- pnpm dev" -s myapp-dev
|
|
502
505
|
|
|
503
506
|
# Wait until the dev server prints something useful, then inspect it
|
|
504
507
|
bunx tuistory -s myapp-dev wait "/ready|local|tunnel/i" --timeout 30000
|
|
@@ -507,7 +510,7 @@ describe('system-message', () => {
|
|
|
507
510
|
|
|
508
511
|
### passing the public URL to your app
|
|
509
512
|
|
|
510
|
-
If you launch the server command through \`kimaki tunnel -- ...\`, the local port is auto-detected from the child process logs in many common dev-server setups
|
|
513
|
+
If you launch the server command through \`kimaki tunnel -- ...\`, the local port is auto-detected from the child process logs in many common dev-server setups. Use \`--port\` only when the dev server does not print a detectable localhost URL or port line.
|
|
511
514
|
|
|
512
515
|
\`\`\`bash
|
|
513
516
|
# Your app can read process.env.TRAFORO_URL directly
|
|
@@ -534,13 +537,13 @@ describe('system-message', () => {
|
|
|
534
537
|
|
|
535
538
|
\`\`\`bash
|
|
536
539
|
# Next.js project
|
|
537
|
-
bunx tuistory launch "kimaki tunnel
|
|
540
|
+
bunx tuistory launch "kimaki tunnel -- pnpm dev" -s projectname-nextjs-dev
|
|
538
541
|
|
|
539
|
-
# Vite project
|
|
540
|
-
bunx tuistory launch "kimaki tunnel
|
|
542
|
+
# Vite project
|
|
543
|
+
bunx tuistory launch "kimaki tunnel -- pnpm dev" -s vite-dev
|
|
541
544
|
|
|
542
545
|
# Custom tunnel ID (only for intentionally public-safe services)
|
|
543
|
-
bunx tuistory launch "kimaki tunnel -
|
|
546
|
+
bunx tuistory launch "kimaki tunnel -t holocron -- pnpm dev" -s holocron-dev
|
|
544
547
|
\`\`\`
|
|
545
548
|
|
|
546
549
|
### stopping the dev server
|
|
@@ -689,6 +689,13 @@ e2eTest('voice message handling', () => {
|
|
|
689
689
|
transcription: 'Queue this task for later',
|
|
690
690
|
queueMessage: true,
|
|
691
691
|
});
|
|
692
|
+
await waitForBotMessageContaining({
|
|
693
|
+
discord,
|
|
694
|
+
threadId: thread.id,
|
|
695
|
+
userId: TEST_USER_ID,
|
|
696
|
+
text: 'using deterministic-provider/deterministic-v2',
|
|
697
|
+
timeout: 4_000,
|
|
698
|
+
});
|
|
692
699
|
await th.user(TEST_USER_ID).sendVoiceMessage();
|
|
693
700
|
// 3. Transcription should appear, followed by queue notification
|
|
694
701
|
await waitForBotMessageContaining({
|
|
@@ -17,13 +17,14 @@ import { setDataDir } from './config.js';
|
|
|
17
17
|
import { store } from './store.js';
|
|
18
18
|
import { startDiscordBot } from './discord-bot.js';
|
|
19
19
|
import { getRuntime } from './session-handler/thread-session-runtime.js';
|
|
20
|
-
import { setBotToken, initDatabase, closeDatabase, setChannelDirectory, setChannelVerbosity, } from './database.js';
|
|
20
|
+
import { setBotToken, initDatabase, closeDatabase, setChannelDirectory, setChannelVerbosity, setChannelWorktreesEnabled, } from './database.js';
|
|
21
21
|
import { startHranaServer, stopHranaServer } from './hrana-server.js';
|
|
22
22
|
import { initializeOpencodeForDirectory, stopOpencodeServer, } from './opencode.js';
|
|
23
23
|
import { chooseLockPort, cleanupTestSessions, waitForBotMessageContaining, waitForBotReplyAfterUserMessage, } from './test-utils.js';
|
|
24
24
|
import { execAsync } from './worktrees.js';
|
|
25
25
|
const TEST_USER_ID = '200000000000000901';
|
|
26
26
|
const TEXT_CHANNEL_ID = '200000000000000902';
|
|
27
|
+
const NON_GIT_CHANNEL_ID = '200000000000000903';
|
|
27
28
|
// Unique worktree name per run to avoid collisions with leftover worktrees
|
|
28
29
|
const WORKTREE_SUFFIX = Date.now().toString(36).slice(-6);
|
|
29
30
|
const WORKTREE_NAME = `wt-e2e-${WORKTREE_SUFFIX}`;
|
|
@@ -32,8 +33,10 @@ function createRunDirectories() {
|
|
|
32
33
|
fs.mkdirSync(root, { recursive: true });
|
|
33
34
|
const dataDir = fs.mkdtempSync(path.join(root, 'data-'));
|
|
34
35
|
const projectDirectory = path.join(root, 'project');
|
|
36
|
+
const nonGitDirectory = path.join(root, 'non-git-project');
|
|
35
37
|
fs.mkdirSync(projectDirectory, { recursive: true });
|
|
36
|
-
|
|
38
|
+
fs.mkdirSync(nonGitDirectory, { recursive: true });
|
|
39
|
+
return { root, dataDir, projectDirectory, nonGitDirectory };
|
|
37
40
|
}
|
|
38
41
|
function createDiscordJsClient({ restUrl }) {
|
|
39
42
|
return new Client({
|
|
@@ -77,7 +80,7 @@ function createDeterministicMatchers() {
|
|
|
77
80
|
priority: 10,
|
|
78
81
|
when: {
|
|
79
82
|
lastMessageRole: 'user',
|
|
80
|
-
|
|
83
|
+
latestUserTextIncludes: 'Reply with exactly:',
|
|
81
84
|
},
|
|
82
85
|
then: {
|
|
83
86
|
parts: [
|
|
@@ -122,6 +125,11 @@ describe('worktree lifecycle', () => {
|
|
|
122
125
|
name: 'worktree-e2e',
|
|
123
126
|
type: ChannelType.GuildText,
|
|
124
127
|
},
|
|
128
|
+
{
|
|
129
|
+
id: NON_GIT_CHANNEL_ID,
|
|
130
|
+
name: 'non-git-worktree-e2e',
|
|
131
|
+
type: ChannelType.GuildText,
|
|
132
|
+
},
|
|
125
133
|
],
|
|
126
134
|
users: [
|
|
127
135
|
{
|
|
@@ -146,6 +154,7 @@ describe('worktree lifecycle', () => {
|
|
|
146
154
|
},
|
|
147
155
|
});
|
|
148
156
|
fs.writeFileSync(path.join(directories.projectDirectory, 'opencode.json'), JSON.stringify(opencodeConfig, null, 2));
|
|
157
|
+
fs.writeFileSync(path.join(directories.nonGitDirectory, 'opencode.json'), JSON.stringify(opencodeConfig, null, 2));
|
|
149
158
|
// Initialize git repo after writing opencode.json so the initial commit
|
|
150
159
|
// includes it. Worktrees require at least one commit.
|
|
151
160
|
await initGitRepo(directories.projectDirectory);
|
|
@@ -162,7 +171,14 @@ describe('worktree lifecycle', () => {
|
|
|
162
171
|
directory: directories.projectDirectory,
|
|
163
172
|
channelType: 'text',
|
|
164
173
|
});
|
|
174
|
+
await setChannelDirectory({
|
|
175
|
+
channelId: NON_GIT_CHANNEL_ID,
|
|
176
|
+
directory: directories.nonGitDirectory,
|
|
177
|
+
channelType: 'text',
|
|
178
|
+
});
|
|
165
179
|
await setChannelVerbosity(TEXT_CHANNEL_ID, 'tools_and_text');
|
|
180
|
+
await setChannelVerbosity(NON_GIT_CHANNEL_ID, 'tools_and_text');
|
|
181
|
+
await setChannelWorktreesEnabled(NON_GIT_CHANNEL_ID, true);
|
|
166
182
|
botClient = createDiscordJsClient({ restUrl: discord.restUrl });
|
|
167
183
|
await startDiscordBot({
|
|
168
184
|
token: discord.botToken,
|
|
@@ -213,7 +229,11 @@ describe('worktree lifecycle', () => {
|
|
|
213
229
|
}
|
|
214
230
|
}).catch(() => { return; });
|
|
215
231
|
await execAsync(`git branch -D ${JSON.stringify(`opencode/kimaki-${WORKTREE_NAME}`)}`, { cwd: directories.projectDirectory }).catch(() => { return; });
|
|
216
|
-
fs.rmSync(directories.dataDir, {
|
|
232
|
+
fs.rmSync(directories.dataDir, {
|
|
233
|
+
recursive: true,
|
|
234
|
+
force: true,
|
|
235
|
+
maxRetries: 3,
|
|
236
|
+
});
|
|
217
237
|
}
|
|
218
238
|
}, 5_000);
|
|
219
239
|
test('session responds after /new-worktree switches sdkDirectory in existing thread', async () => {
|
|
@@ -308,4 +328,60 @@ describe('worktree lifecycle', () => {
|
|
|
308
328
|
const okCount = (text.match(/⬥ ok/g) || []).length;
|
|
309
329
|
expect(okCount).toBe(2);
|
|
310
330
|
}, 30_000);
|
|
331
|
+
test('auto-worktrees fall back to normal sessions outside git repositories', async () => {
|
|
332
|
+
await discord.channel(NON_GIT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
333
|
+
content: 'Reply with exactly: non-git-first',
|
|
334
|
+
});
|
|
335
|
+
const thread = await discord.channel(NON_GIT_CHANNEL_ID).waitForThread({
|
|
336
|
+
timeout: 4_000,
|
|
337
|
+
predicate: (t) => {
|
|
338
|
+
return Boolean(t.name?.includes('Reply with exactly: non-git-first'));
|
|
339
|
+
},
|
|
340
|
+
});
|
|
341
|
+
const th = discord.thread(thread.id);
|
|
342
|
+
await waitForBotReplyAfterUserMessage({
|
|
343
|
+
discord,
|
|
344
|
+
threadId: thread.id,
|
|
345
|
+
userId: TEST_USER_ID,
|
|
346
|
+
userMessageIncludes: 'non-git-first',
|
|
347
|
+
timeout: 4_000,
|
|
348
|
+
});
|
|
349
|
+
await th.user(TEST_USER_ID).sendMessage({
|
|
350
|
+
content: 'Reply with exactly: non-git-second',
|
|
351
|
+
});
|
|
352
|
+
await waitForBotMessageContaining({
|
|
353
|
+
discord,
|
|
354
|
+
threadId: thread.id,
|
|
355
|
+
userId: TEST_USER_ID,
|
|
356
|
+
text: '⬥ ok',
|
|
357
|
+
afterUserMessageIncludes: 'non-git-second',
|
|
358
|
+
timeout: 4_000,
|
|
359
|
+
});
|
|
360
|
+
let text = await th.text();
|
|
361
|
+
for (let attempt = 0; attempt < 40; attempt++) {
|
|
362
|
+
if ((text.match(/⬥ ok/g) || []).length >= 2) {
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
await new Promise((resolve) => {
|
|
366
|
+
setTimeout(resolve, 100);
|
|
367
|
+
});
|
|
368
|
+
text = await th.text();
|
|
369
|
+
}
|
|
370
|
+
expect(text).toMatchInlineSnapshot(`
|
|
371
|
+
"--- from: user (worktree-tester)
|
|
372
|
+
Reply with exactly: non-git-first
|
|
373
|
+
--- from: assistant (TestBot)
|
|
374
|
+
*using deterministic-provider/deterministic-v2*
|
|
375
|
+
--- from: user (worktree-tester)
|
|
376
|
+
Reply with exactly: non-git-second
|
|
377
|
+
--- from: assistant (TestBot)
|
|
378
|
+
⬥ ok
|
|
379
|
+
⬥ ok"
|
|
380
|
+
`);
|
|
381
|
+
expect(text).toContain('Reply with exactly: non-git-first');
|
|
382
|
+
expect(text).toContain('Reply with exactly: non-git-second');
|
|
383
|
+
expect(text).not.toContain('Worktree creation failed');
|
|
384
|
+
const okCount = (text.match(/⬥ ok/g) || []).length;
|
|
385
|
+
expect(okCount).toBe(2);
|
|
386
|
+
}, 20_000);
|
|
311
387
|
});
|
package/dist/worktrees.js
CHANGED
|
@@ -574,6 +574,13 @@ export async function isDirty(dir, opts) {
|
|
|
574
574
|
}
|
|
575
575
|
return status.length > 0;
|
|
576
576
|
}
|
|
577
|
+
export async function isGitRepositoryRoot(directory) {
|
|
578
|
+
const topLevel = await git(directory, 'rev-parse --show-toplevel');
|
|
579
|
+
if (topLevel instanceof Error) {
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
return path.resolve(topLevel) === path.resolve(directory);
|
|
583
|
+
}
|
|
577
584
|
async function getGitCommonDir(dir) {
|
|
578
585
|
const commonDir = await git(dir, 'rev-parse --git-common-dir');
|
|
579
586
|
if (commonDir instanceof Error) {
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "kimaki",
|
|
3
3
|
"module": "index.ts",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.9.
|
|
5
|
+
"version": "0.9.1",
|
|
6
6
|
"repository": "https://github.com/remorses/kimaki",
|
|
7
7
|
"bin": "bin.js",
|
|
8
8
|
"files": [
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"tsx": "^4.20.5",
|
|
27
27
|
"undici": "^8.0.2",
|
|
28
28
|
"discord-digital-twin": "^0.1.0",
|
|
29
|
-
"opencode-deterministic-provider": "^0.0.1",
|
|
30
29
|
"opencode-cached-provider": "^0.0.1",
|
|
30
|
+
"opencode-deterministic-provider": "^0.0.1",
|
|
31
31
|
"db": "^0.0.0"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
@@ -65,9 +65,9 @@
|
|
|
65
65
|
"zod": "^4.3.6",
|
|
66
66
|
"zustand": "^5.0.11",
|
|
67
67
|
"errore": "^0.14.1",
|
|
68
|
-
"opencode-injection-guard": "^0.2.1",
|
|
69
68
|
"libsqlproxy": "^0.1.0",
|
|
70
|
-
"traforo": "^0.5.0"
|
|
69
|
+
"traforo": "^0.5.0",
|
|
70
|
+
"opencode-injection-guard": "^0.2.1"
|
|
71
71
|
},
|
|
72
72
|
"optionalDependencies": {
|
|
73
73
|
"@snazzah/davey": "^0.1.10",
|