kimaki 0.12.0 → 0.13.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/anthropic-auth-plugin.js +15 -15
- package/dist/anthropic-auth-state.js +1 -1
- package/dist/anthropic-auth-state.test.js +2 -2
- package/dist/btw-prefix-detection.js +13 -15
- package/dist/btw-prefix-detection.test.js +60 -30
- package/dist/channel-reference-permissions.e2e.test.js +2 -0
- package/dist/cli-parsing.test.js +1 -1
- package/dist/cli-runner.js +8 -2
- package/dist/cli.js +6 -1
- package/dist/commands/abort.js +1 -1
- package/dist/commands/compact.js +2 -5
- package/dist/commands/model-variant.js +1 -1
- package/dist/commands/model.js +1 -1
- package/dist/commands/new-worktree.js +107 -59
- package/dist/commands/restart-opencode-server.js +1 -1
- package/dist/commands/undo-redo.js +2 -2
- package/dist/commands/upgrade.js +1 -2
- package/dist/context-awareness-plugin.js +9 -4
- package/dist/discord-bot.js +80 -42
- package/dist/message-finish-field.e2e.test.js +1 -0
- package/dist/message-preprocessing.js +1 -1
- package/dist/openai-auth-plugin.js +16 -16
- package/dist/openai-auth-state.js +1 -1
- package/dist/opencode-command.js +25 -1
- package/dist/opencode-command.test.js +64 -2
- package/dist/opencode-interrupt-plugin.js +184 -329
- package/dist/opencode-interrupt-plugin.test.js +168 -362
- package/dist/opencode.js +22 -0
- package/dist/plugin-opencode-client.js +43 -0
- package/dist/queue-advanced-footer.e2e.test.js +8 -1
- package/dist/queue-advanced-model-switch.e2e.test.js +1 -1
- package/dist/queue-advanced-permissions-typing.e2e.test.js +1 -0
- package/dist/session-handler/agent-utils.js +9 -9
- package/dist/session-handler/event-stream-state.js +3 -1
- package/dist/session-handler/event-stream-state.test.js +67 -1
- package/dist/session-handler/thread-runtime-state.js +29 -0
- package/dist/session-handler/thread-session-runtime.js +51 -66
- package/dist/store.js +1 -0
- package/dist/subagent-rate-limit-plugin.js +12 -12
- package/dist/thread-message-queue.e2e.test.js +200 -21
- package/dist/undo-redo.e2e.test.js +1 -0
- package/dist/voice.js +3 -2
- package/dist/worktree-lifecycle.e2e.test.js +130 -50
- package/package.json +7 -7
- package/skills/holocron/SKILL.md +594 -0
- package/skills/sigillo/SKILL.md +4 -4
- package/skills/spiceflow/SKILL.md +12 -4
- package/skills/tuistory/SKILL.md +38 -2
- package/src/anthropic-auth-plugin.ts +17 -16
- package/src/anthropic-auth-state.test.ts +2 -2
- package/src/anthropic-auth-state.ts +4 -4
- package/src/btw-prefix-detection.test.ts +61 -30
- package/src/btw-prefix-detection.ts +15 -19
- package/src/channel-reference-permissions.e2e.test.ts +2 -0
- package/src/cli-parsing.test.ts +1 -1
- package/src/cli-runner.ts +8 -2
- package/src/cli.ts +12 -1
- package/src/commands/abort.ts +1 -1
- package/src/commands/compact.ts +2 -5
- package/src/commands/model-variant.ts +1 -1
- package/src/commands/model.ts +1 -1
- package/src/commands/new-worktree.ts +136 -81
- package/src/commands/restart-opencode-server.ts +1 -1
- package/src/commands/undo-redo.ts +2 -2
- package/src/commands/upgrade.ts +1 -2
- package/src/context-awareness-plugin.ts +15 -8
- package/src/discord-bot.ts +92 -41
- package/src/message-finish-field.e2e.test.ts +1 -0
- package/src/message-preprocessing.ts +1 -1
- package/src/openai-auth-plugin.ts +18 -17
- package/src/openai-auth-state.ts +4 -4
- package/src/opencode-command.test.ts +81 -1
- package/src/opencode-command.ts +26 -1
- package/src/opencode-interrupt-plugin.test.ts +201 -496
- package/src/opencode-interrupt-plugin.ts +205 -415
- package/src/opencode.ts +43 -0
- package/src/plugin-opencode-client.ts +60 -0
- package/src/queue-advanced-footer.e2e.test.ts +8 -1
- package/src/queue-advanced-model-switch.e2e.test.ts +1 -1
- package/src/queue-advanced-permissions-typing.e2e.test.ts +1 -0
- package/src/session-handler/agent-utils.ts +11 -11
- package/src/session-handler/event-stream-state.test.ts +72 -2
- package/src/session-handler/event-stream-state.ts +3 -1
- package/src/session-handler/thread-runtime-state.ts +35 -0
- package/src/session-handler/thread-session-runtime.ts +72 -84
- package/src/store.ts +8 -0
- package/src/subagent-rate-limit-plugin.ts +13 -12
- package/src/thread-message-queue.e2e.test.ts +229 -23
- package/src/undo-redo.e2e.test.ts +1 -0
- package/src/voice.ts +3 -2
- package/src/worktree-lifecycle.e2e.test.ts +138 -53
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* - https://github.com/badlogic/pi-mono/blob/main/packages/ai/src/providers/anthropic.ts
|
|
24
24
|
*/
|
|
25
25
|
import { appendToastSessionMarker } from "./plugin-logger.js";
|
|
26
|
+
import { createPluginClient } from "./plugin-opencode-client.js";
|
|
26
27
|
import { loadAccountStore, rememberAnthropicOAuth, rotateAnthropicAccount, saveAccountStore, setAnthropicAuth, shouldRotateAuth, upsertAccount, withAuthStateLock, } from "./anthropic-auth-state.js";
|
|
27
28
|
import { extractAnthropicAccountIdentity, } from "./anthropic-account-identity.js";
|
|
28
29
|
// PKCE (Proof Key for Code Exchange) using Web Crypto API.
|
|
@@ -766,7 +767,10 @@ async function getFreshOAuth(getAuth, client) {
|
|
|
766
767
|
pendingRefresh.delete(auth.refresh);
|
|
767
768
|
});
|
|
768
769
|
}
|
|
769
|
-
const AnthropicAuthPlugin = async ({
|
|
770
|
+
const AnthropicAuthPlugin = async ({ serverUrl, directory }) => {
|
|
771
|
+
// Build our own v2 client. The plugin-provided ctx.client (v1) does not
|
|
772
|
+
// reliably make REST calls from inside the plugin process.
|
|
773
|
+
const client = createPluginClient({ serverUrl, directory });
|
|
770
774
|
return {
|
|
771
775
|
"chat.headers": async (input, output) => {
|
|
772
776
|
if (input.model.providerID !== "anthropic") {
|
|
@@ -816,13 +820,11 @@ const AnthropicAuthPlugin = async ({ client }) => {
|
|
|
816
820
|
const rewritten = rewriteRequestPayload(originalBody, (msg) => {
|
|
817
821
|
client.tui
|
|
818
822
|
.showToast({
|
|
819
|
-
|
|
820
|
-
message:
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
variant: "error",
|
|
825
|
-
},
|
|
823
|
+
message: appendToastSessionMarker({
|
|
824
|
+
message: msg,
|
|
825
|
+
sessionId,
|
|
826
|
+
}),
|
|
827
|
+
variant: "error",
|
|
826
828
|
})
|
|
827
829
|
.catch(() => { });
|
|
828
830
|
});
|
|
@@ -859,13 +861,11 @@ const AnthropicAuthPlugin = async ({ client }) => {
|
|
|
859
861
|
// Show toast notification so Discord thread shows the rotation
|
|
860
862
|
client.tui
|
|
861
863
|
.showToast({
|
|
862
|
-
|
|
863
|
-
message:
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
variant: "info",
|
|
868
|
-
},
|
|
864
|
+
message: appendToastSessionMarker({
|
|
865
|
+
message: `Switching from account ${rotated.fromLabel} to account ${rotated.toLabel}`,
|
|
866
|
+
sessionId,
|
|
867
|
+
}),
|
|
868
|
+
variant: "info",
|
|
869
869
|
})
|
|
870
870
|
.catch(() => { });
|
|
871
871
|
const retryAuth = await getFreshOAuth(getAuth, client);
|
|
@@ -55,7 +55,7 @@ async function writeAnthropicAuthFile(auth) {
|
|
|
55
55
|
}
|
|
56
56
|
export async function setAnthropicAuth(auth, client) {
|
|
57
57
|
await writeAnthropicAuthFile(auth);
|
|
58
|
-
await client.auth.set({
|
|
58
|
+
await client.auth.set({ providerID: 'anthropic', auth });
|
|
59
59
|
}
|
|
60
60
|
// --- Current account ---
|
|
61
61
|
export async function getCurrentAnthropicAccount() {
|
|
@@ -97,8 +97,8 @@ describe('rotateAnthropicAccount', () => {
|
|
|
97
97
|
expect(authJson.anthropic?.refresh).toBe('refresh-second');
|
|
98
98
|
expect(authSetCalls).toEqual([
|
|
99
99
|
{
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
providerID: 'anthropic',
|
|
101
|
+
auth: {
|
|
102
102
|
type: 'oauth',
|
|
103
103
|
refresh: 'refresh-second',
|
|
104
104
|
access: 'access-second',
|
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
// Detects
|
|
2
|
-
//
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
// Detects `. btw` suffix at the end of a Discord message, identical pattern
|
|
2
|
+
// to the queue suffix. When present the suffix is stripped and the remaining
|
|
3
|
+
// message is forked to a new btw thread via /btw.
|
|
4
|
+
//
|
|
5
|
+
// Supported forms:
|
|
6
|
+
// - punctuation + btw: ". btw", "! btw", ". btw.", "!btw."
|
|
7
|
+
// - btw as its own final line: "text\nbtw"
|
|
8
|
+
// Non-matches: "btw fix this" (start only), "hello btw" (no punctuation)
|
|
9
|
+
const BTW_SUFFIX_RE = /(?:[.!?,;:])\s*btw\.?\s*$|\n\s*btw\.?\s*$/i;
|
|
10
|
+
export function extractBtwSuffix(content) {
|
|
11
|
+
if (!BTW_SUFFIX_RE.test(content)) {
|
|
12
|
+
return { prompt: content, forceBtw: false };
|
|
6
13
|
}
|
|
7
|
-
|
|
8
|
-
const match = content.match(/^\s*btw[.,;:!?\s]\s*([\s\S]+)$/i);
|
|
9
|
-
if (!match) {
|
|
10
|
-
return null;
|
|
11
|
-
}
|
|
12
|
-
const prompt = match[1]?.trim();
|
|
13
|
-
if (!prompt) {
|
|
14
|
-
return null;
|
|
15
|
-
}
|
|
16
|
-
return { prompt };
|
|
14
|
+
return { prompt: content.replace(BTW_SUFFIX_RE, '').trimEnd(), forceBtw: true };
|
|
17
15
|
}
|
|
@@ -1,63 +1,93 @@
|
|
|
1
1
|
import { describe, expect, test } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
describe('
|
|
4
|
-
test('matches
|
|
5
|
-
expect(
|
|
2
|
+
import { extractBtwSuffix } from './btw-prefix-detection.js';
|
|
3
|
+
describe('extractBtwSuffix', () => {
|
|
4
|
+
test('matches after period', () => {
|
|
5
|
+
expect(extractBtwSuffix('fix the bug. btw')).toMatchInlineSnapshot(`
|
|
6
6
|
{
|
|
7
|
-
"
|
|
7
|
+
"forceBtw": true,
|
|
8
|
+
"prompt": "fix the bug",
|
|
8
9
|
}
|
|
9
10
|
`);
|
|
10
11
|
});
|
|
11
|
-
test('matches
|
|
12
|
-
expect(
|
|
12
|
+
test('matches after exclamation', () => {
|
|
13
|
+
expect(extractBtwSuffix('done! btw')).toMatchInlineSnapshot(`
|
|
13
14
|
{
|
|
14
|
-
"
|
|
15
|
+
"forceBtw": true,
|
|
16
|
+
"prompt": "done",
|
|
15
17
|
}
|
|
16
18
|
`);
|
|
17
19
|
});
|
|
18
|
-
test('
|
|
19
|
-
expect(
|
|
20
|
+
test('matches after comma', () => {
|
|
21
|
+
expect(extractBtwSuffix('sure, btw')).toMatchInlineSnapshot(`
|
|
20
22
|
{
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
+
"forceBtw": true,
|
|
24
|
+
"prompt": "sure",
|
|
23
25
|
}
|
|
24
26
|
`);
|
|
25
27
|
});
|
|
26
|
-
test('matches
|
|
27
|
-
expect(
|
|
28
|
+
test('matches after newline', () => {
|
|
29
|
+
expect(extractBtwSuffix('fix the bug\nbtw')).toMatchInlineSnapshot(`
|
|
28
30
|
{
|
|
29
|
-
"
|
|
31
|
+
"forceBtw": true,
|
|
32
|
+
"prompt": "fix the bug",
|
|
30
33
|
}
|
|
31
34
|
`);
|
|
32
35
|
});
|
|
33
|
-
test('matches
|
|
34
|
-
expect(
|
|
36
|
+
test('matches with trailing dot', () => {
|
|
37
|
+
expect(extractBtwSuffix('fix the bug. btw.')).toMatchInlineSnapshot(`
|
|
35
38
|
{
|
|
36
|
-
"
|
|
39
|
+
"forceBtw": true,
|
|
40
|
+
"prompt": "fix the bug",
|
|
37
41
|
}
|
|
38
42
|
`);
|
|
39
43
|
});
|
|
40
|
-
test('
|
|
41
|
-
expect(
|
|
44
|
+
test('case insensitive', () => {
|
|
45
|
+
expect(extractBtwSuffix('done. BTW')).toMatchInlineSnapshot(`
|
|
42
46
|
{
|
|
43
|
-
"
|
|
47
|
+
"forceBtw": true,
|
|
48
|
+
"prompt": "done",
|
|
44
49
|
}
|
|
45
50
|
`);
|
|
46
51
|
});
|
|
47
|
-
test('
|
|
48
|
-
expect(
|
|
52
|
+
test('no space between punctuation and btw', () => {
|
|
53
|
+
expect(extractBtwSuffix('done.btw')).toMatchInlineSnapshot(`
|
|
49
54
|
{
|
|
50
|
-
"
|
|
55
|
+
"forceBtw": true,
|
|
56
|
+
"prompt": "done",
|
|
51
57
|
}
|
|
52
58
|
`);
|
|
53
59
|
});
|
|
54
|
-
test('does not match
|
|
55
|
-
expect(
|
|
60
|
+
test('does not match at start of message', () => {
|
|
61
|
+
expect(extractBtwSuffix('btw fix this')).toMatchInlineSnapshot(`
|
|
62
|
+
{
|
|
63
|
+
"forceBtw": false,
|
|
64
|
+
"prompt": "btw fix this",
|
|
65
|
+
}
|
|
66
|
+
`);
|
|
56
67
|
});
|
|
57
|
-
test('does not match mid-message', () => {
|
|
58
|
-
expect(
|
|
68
|
+
test('does not match mid-message without punctuation', () => {
|
|
69
|
+
expect(extractBtwSuffix('hello btw')).toMatchInlineSnapshot(`
|
|
70
|
+
{
|
|
71
|
+
"forceBtw": false,
|
|
72
|
+
"prompt": "hello btw",
|
|
73
|
+
}
|
|
74
|
+
`);
|
|
59
75
|
});
|
|
60
|
-
test('does not match empty
|
|
61
|
-
expect(
|
|
76
|
+
test('does not match empty content', () => {
|
|
77
|
+
expect(extractBtwSuffix('')).toMatchInlineSnapshot(`
|
|
78
|
+
{
|
|
79
|
+
"forceBtw": false,
|
|
80
|
+
"prompt": "",
|
|
81
|
+
}
|
|
82
|
+
`);
|
|
83
|
+
});
|
|
84
|
+
test('multiline message with btw at end', () => {
|
|
85
|
+
expect(extractBtwSuffix('first line\nsecond line. btw')).toMatchInlineSnapshot(`
|
|
86
|
+
{
|
|
87
|
+
"forceBtw": true,
|
|
88
|
+
"prompt": "first line
|
|
89
|
+
second line",
|
|
90
|
+
}
|
|
91
|
+
`);
|
|
62
92
|
});
|
|
63
93
|
});
|
|
@@ -71,12 +71,14 @@ describe('channel reference permissions', () => {
|
|
|
71
71
|
--- from: assistant (TestBot)
|
|
72
72
|
*using deterministic-provider/deterministic-v2*
|
|
73
73
|
⬥ reading referenced channel directory
|
|
74
|
+
┣ read *allowed.txt*
|
|
74
75
|
⬥ channel-reference-read-done
|
|
75
76
|
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
|
|
76
77
|
--- from: user (channel-reference-tester)
|
|
77
78
|
Use <#200000000000001022> CHANNEL_REFERENCE_PERMISSION_MARKER followup
|
|
78
79
|
--- from: assistant (TestBot)
|
|
79
80
|
⬥ reading referenced channel directory
|
|
81
|
+
┣ read *allowed.txt*
|
|
80
82
|
⬥ channel-reference-read-done
|
|
81
83
|
*project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
|
|
82
84
|
`);
|
package/dist/cli-parsing.test.js
CHANGED
|
@@ -15,7 +15,7 @@ async function parseWithGoke(argv) {
|
|
|
15
15
|
"cli.command('multioauth anthropic remove <indexOrEmail>', 'Remove stored Anthropic account')",
|
|
16
16
|
"cli.command('multioauth openai list', 'List stored OpenAI accounts')",
|
|
17
17
|
"cli.command('multioauth openai remove <indexOrEmail>', 'Remove stored OpenAI account')",
|
|
18
|
-
`const result = cli.parse(${JSON.stringify(argv)}, { run: false })`,
|
|
18
|
+
`const result = await cli.parse(${JSON.stringify(argv)}, { run: false })`,
|
|
19
19
|
'process.stdout.write(JSON.stringify({ args: result.args, options: result.options }))',
|
|
20
20
|
].join(';');
|
|
21
21
|
const { stdout } = await execAsync(`node --input-type=module -e ${JSON.stringify(script)}`, {
|
package/dist/cli-runner.js
CHANGED
|
@@ -111,7 +111,11 @@ export async function sendDiscordMessageWithOptionalAttachment({ channelId, prom
|
|
|
111
111
|
const discordMaxLength = 2000;
|
|
112
112
|
if (prompt.length <= discordMaxLength) {
|
|
113
113
|
return (await rest.post(Routes.channelMessages(channelId), {
|
|
114
|
-
body: {
|
|
114
|
+
body: {
|
|
115
|
+
content: prompt,
|
|
116
|
+
embeds,
|
|
117
|
+
allowed_mentions: { parse: store.getState().allowedMentions },
|
|
118
|
+
},
|
|
115
119
|
}));
|
|
116
120
|
}
|
|
117
121
|
const preview = prompt.slice(0, 100).replace(/\n/g, ' ');
|
|
@@ -158,6 +162,7 @@ export async function sendDiscordMessageWithOptionalAttachment({ channelId, prom
|
|
|
158
162
|
content: summaryContent,
|
|
159
163
|
attachments: [{ id: 0, filename: 'prompt.md' }],
|
|
160
164
|
embeds,
|
|
165
|
+
allowed_mentions: { parse: store.getState().allowedMentions },
|
|
161
166
|
}));
|
|
162
167
|
const buffer = fs.readFileSync(tmpFile);
|
|
163
168
|
formData.append('files[0]', new Blob([buffer], { type: 'text/markdown' }), 'prompt.md');
|
|
@@ -509,6 +514,7 @@ export async function ensureCommandAvailable({ name, envPathKey, installUnix, in
|
|
|
509
514
|
}
|
|
510
515
|
// Run opencode upgrade in the background so the user always has the latest version.
|
|
511
516
|
// Spawn caffeinate on macOS to prevent system sleep while bot is running.
|
|
517
|
+
// Uses -s to also prevent sleep on lid close (AC power only, not battery).
|
|
512
518
|
// Uses -w to watch the parent PID so caffeinate self-terminates if kimaki
|
|
513
519
|
// exits for any reason (SIGTERM, crash, process.exit, supervisor stop).
|
|
514
520
|
export function startCaffeinate() {
|
|
@@ -516,7 +522,7 @@ export function startCaffeinate() {
|
|
|
516
522
|
return;
|
|
517
523
|
}
|
|
518
524
|
try {
|
|
519
|
-
const proc = spawn('caffeinate', ['-
|
|
525
|
+
const proc = spawn('caffeinate', ['-s', '-w', String(process.pid)], {
|
|
520
526
|
stdio: 'ignore',
|
|
521
527
|
detached: false,
|
|
522
528
|
});
|
package/dist/cli.js
CHANGED
|
@@ -44,6 +44,10 @@ cli
|
|
|
44
44
|
.option('--no-sentry', 'Disable Sentry error reporting')
|
|
45
45
|
.option('--gateway', 'Force gateway mode (use the gateway Kimaki bot instead of a self-hosted bot)')
|
|
46
46
|
.option('--gateway-callback-url <url>', 'After gateway OAuth install, redirect to this URL instead of the default success page (appends ?guild_id=<id>)')
|
|
47
|
+
.option('--allow-mention <type>', z
|
|
48
|
+
.array(z.enum(['users', 'roles', 'everyone']))
|
|
49
|
+
.optional()
|
|
50
|
+
.describe('Which mention types the bot can trigger (users, roles, everyone). Repeatable. Default: users only.'))
|
|
47
51
|
.option('--enable-skill <name>', z
|
|
48
52
|
.array(z.string())
|
|
49
53
|
.optional()
|
|
@@ -140,6 +144,7 @@ cli
|
|
|
140
144
|
...(options.disableSync && { syncEnabled: false }),
|
|
141
145
|
...(enabledSkills.length > 0 && { enabledSkills }),
|
|
142
146
|
...(disabledSkills.length > 0 && { disabledSkills }),
|
|
147
|
+
...(options.allowMention && { allowedMentions: options.allowMention }),
|
|
143
148
|
});
|
|
144
149
|
if (enabledSkills.length > 0) {
|
|
145
150
|
cliLogger.log(`Skill whitelist enabled: only [${enabledSkills.join(', ')}] will be injected`);
|
|
@@ -202,4 +207,4 @@ cli.use(sessionCommands);
|
|
|
202
207
|
cli.use(maintenanceCommands);
|
|
203
208
|
cli.version(getCurrentVersion());
|
|
204
209
|
cli.help();
|
|
205
|
-
cli.parse();
|
|
210
|
+
void cli.parse();
|
package/dist/commands/abort.js
CHANGED
package/dist/commands/compact.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// /compact command - Trigger context compaction (summarization) for the current session.
|
|
2
2
|
import { ChannelType, MessageFlags, } from 'discord.js';
|
|
3
3
|
import { getThreadSession } from '../database.js';
|
|
4
|
-
import { initializeOpencodeForDirectory, getOpencodeClient, } from '../opencode.js';
|
|
4
|
+
import { initializeOpencodeForDirectory, getOpencodeClient, extractSdkErrorMessage, } from '../opencode.js';
|
|
5
5
|
import { resolveWorkingDirectory, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
|
|
6
6
|
import { createLogger, LogPrefix } from '../logger.js';
|
|
7
7
|
const logger = createLogger(LogPrefix.COMPACT);
|
|
@@ -97,10 +97,7 @@ export async function handleCompactCommand({ command, }) {
|
|
|
97
97
|
});
|
|
98
98
|
if (result.error) {
|
|
99
99
|
logger.error('[COMPACT] Error:', result.error);
|
|
100
|
-
const
|
|
101
|
-
const errorMessage = errorData && typeof errorData === 'object' && 'message' in errorData
|
|
102
|
-
? String(errorData.message || 'Unknown error')
|
|
103
|
-
: 'Unknown error';
|
|
100
|
+
const errorMessage = extractSdkErrorMessage(result.error);
|
|
104
101
|
await command.editReply({
|
|
105
102
|
content: `Failed to compact: ${errorMessage}`,
|
|
106
103
|
});
|
|
@@ -292,7 +292,7 @@ export async function handleVariantScopeSelectMenu(interaction) {
|
|
|
292
292
|
async function applyVariant({ interaction, context, variant, scope, contextHash, }) {
|
|
293
293
|
const modelId = context.modelId;
|
|
294
294
|
const variantSuffix = variant ? ` (${variant})` : '';
|
|
295
|
-
const agentTip = '\n_Tip: create [agent .md files](https://kimaki.dev/model-switching) in .opencode/agent/ for one-command model switching_';
|
|
295
|
+
const agentTip = '\n_Tip: create [agent .md files](https://kimaki.dev/docs/getting-started/model-switching) in .opencode/agent/ for one-command model switching_';
|
|
296
296
|
try {
|
|
297
297
|
if (scope === 'session') {
|
|
298
298
|
if (!context.sessionId) {
|
package/dist/commands/model.js
CHANGED
|
@@ -728,7 +728,7 @@ export async function handleModelScopeSelectMenu(interaction) {
|
|
|
728
728
|
const modelDisplay = modelId.split('/')[1] || modelId;
|
|
729
729
|
const variant = context.selectedVariant ?? null;
|
|
730
730
|
const variantSuffix = variant ? ` (${variant})` : '';
|
|
731
|
-
const agentTip = '\n_Tip: create [agent .md files](https://kimaki.dev/model-switching) in .opencode/agent/ for one-command model switching_';
|
|
731
|
+
const agentTip = '\n_Tip: create [agent .md files](https://kimaki.dev/docs/getting-started/model-switching) in .opencode/agent/ for one-command model switching_';
|
|
732
732
|
try {
|
|
733
733
|
if (selectedScope === 'session') {
|
|
734
734
|
if (!context.sessionId) {
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
// Creates thread immediately, then worktree in background so user can type
|
|
4
4
|
import { ChannelType, REST, } from 'discord.js';
|
|
5
5
|
import fs from 'node:fs';
|
|
6
|
-
import { createPendingWorktree, setWorktreeReady, setWorktreeError, getChannelDirectory,
|
|
7
|
-
import { SILENT_MESSAGE_FLAGS, reactToThread, resolveProjectDirectoryFromAutocomplete, } from '../discord-utils.js';
|
|
6
|
+
import { createPendingWorktree, setWorktreeReady, setWorktreeError, getChannelDirectory, getThreadSession, setThreadSession, } from '../database.js';
|
|
7
|
+
import { SILENT_MESSAGE_FLAGS, reactToThread, resolveProjectDirectoryFromAutocomplete, resolveTextChannel, sendThreadMessage, } from '../discord-utils.js';
|
|
8
8
|
import { createLogger, LogPrefix } from '../logger.js';
|
|
9
9
|
import { notifyError } from '../sentry.js';
|
|
10
10
|
import { createWorktreeWithSubmodules, execAsync, listBranchesByLastCommit, validateBranchRef, } from '../worktrees.js';
|
|
11
|
-
import {
|
|
11
|
+
import { getOrCreateRuntime } from '../session-handler/thread-session-runtime.js';
|
|
12
|
+
import { buildSessionPermissions, initializeOpencodeForDirectory, } from '../opencode.js';
|
|
12
13
|
import { WORKTREE_PREFIX } from './merge-worktree.js';
|
|
13
14
|
import * as errore from 'errore';
|
|
14
15
|
const logger = createLogger(LogPrefix.WORKTREE);
|
|
@@ -187,10 +188,6 @@ export async function createWorktreeInBackground({ thread, starterMessage, workt
|
|
|
187
188
|
threadId: thread.id,
|
|
188
189
|
worktreeDirectory: worktreeResult.directory,
|
|
189
190
|
});
|
|
190
|
-
await denyPreviousCheckoutForExistingSession({
|
|
191
|
-
threadId: thread.id,
|
|
192
|
-
projectDirectory,
|
|
193
|
-
});
|
|
194
191
|
// React with tree emoji to mark as worktree thread
|
|
195
192
|
await reactToThread({
|
|
196
193
|
rest,
|
|
@@ -210,41 +207,6 @@ export async function createWorktreeInBackground({ thread, starterMessage, workt
|
|
|
210
207
|
},
|
|
211
208
|
});
|
|
212
209
|
}
|
|
213
|
-
async function denyPreviousCheckoutForExistingSession({ threadId, projectDirectory, }) {
|
|
214
|
-
const sessionId = await getThreadSession(threadId);
|
|
215
|
-
if (!sessionId) {
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
const initializeResult = await initializeOpencodeForDirectory(projectDirectory);
|
|
219
|
-
if (initializeResult instanceof Error) {
|
|
220
|
-
logger.warn(`[WORKTREE] Failed to initialize OpenCode before denying previous checkout for thread ${threadId}: ${initializeResult.message}`);
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
const client = getOpencodeClient(projectDirectory);
|
|
224
|
-
if (!client) {
|
|
225
|
-
logger.warn(`[WORKTREE] Missing OpenCode client for previous checkout deny update in thread ${threadId}`);
|
|
226
|
-
return;
|
|
227
|
-
}
|
|
228
|
-
const updateResult = await errore.tryAsync({
|
|
229
|
-
try: async () => {
|
|
230
|
-
await client.session.update({
|
|
231
|
-
sessionID: sessionId,
|
|
232
|
-
permission: buildExternalDirectoryPermissionRules({
|
|
233
|
-
resolvedPattern: projectDirectory.replaceAll('\\', '/'),
|
|
234
|
-
action: 'deny',
|
|
235
|
-
}),
|
|
236
|
-
});
|
|
237
|
-
},
|
|
238
|
-
catch: (e) => new Error('Failed to deny previous checkout for existing session', {
|
|
239
|
-
cause: e,
|
|
240
|
-
}),
|
|
241
|
-
});
|
|
242
|
-
if (updateResult instanceof Error) {
|
|
243
|
-
logger.warn(`[WORKTREE] Failed to deny previous checkout for existing session in thread ${threadId}: ${updateResult.message}`);
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
logger.log(`[WORKTREE] Denied previous checkout for existing session ${sessionId} in thread ${threadId}`);
|
|
247
|
-
}
|
|
248
210
|
async function findExistingWorktreePath({ projectDirectory, worktreeName, }) {
|
|
249
211
|
const listResult = await errore.tryAsync({
|
|
250
212
|
try: () => execAsync('git worktree list --porcelain', { cwd: projectDirectory }),
|
|
@@ -268,7 +230,7 @@ async function findExistingWorktreePath({ projectDirectory, worktreeName, }) {
|
|
|
268
230
|
}
|
|
269
231
|
return undefined;
|
|
270
232
|
}
|
|
271
|
-
export async function handleNewWorktreeCommand({ command, }) {
|
|
233
|
+
export async function handleNewWorktreeCommand({ command, appId, }) {
|
|
272
234
|
await command.deferReply();
|
|
273
235
|
const channel = command.channel;
|
|
274
236
|
if (!channel) {
|
|
@@ -281,6 +243,7 @@ export async function handleNewWorktreeCommand({ command, }) {
|
|
|
281
243
|
await handleWorktreeInThread({
|
|
282
244
|
command,
|
|
283
245
|
thread: channel,
|
|
246
|
+
appId,
|
|
284
247
|
});
|
|
285
248
|
return;
|
|
286
249
|
}
|
|
@@ -351,7 +314,7 @@ export async function handleNewWorktreeCommand({ command, }) {
|
|
|
351
314
|
const { thread, starterMessage } = result;
|
|
352
315
|
await command.editReply(`Creating worktree in ${thread.toString()}`);
|
|
353
316
|
// Create worktree in background (don't await)
|
|
354
|
-
createWorktreeInBackground({
|
|
317
|
+
void createWorktreeInBackground({
|
|
355
318
|
thread,
|
|
356
319
|
starterMessage,
|
|
357
320
|
worktreeName,
|
|
@@ -365,14 +328,10 @@ export async function handleNewWorktreeCommand({ command, }) {
|
|
|
365
328
|
}
|
|
366
329
|
/**
|
|
367
330
|
* Handle /new-worktree when called inside an existing thread.
|
|
368
|
-
*
|
|
331
|
+
* Creates a separate worktree thread, using the source thread name if no name
|
|
332
|
+
* is provided. The source thread stays bound to its original directory.
|
|
369
333
|
*/
|
|
370
|
-
async function handleWorktreeInThread({ command, thread, }) {
|
|
371
|
-
// Error if thread already has a worktree
|
|
372
|
-
if (await getThreadWorktree(thread.id)) {
|
|
373
|
-
await command.editReply('This thread already has a worktree attached.');
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
334
|
+
async function handleWorktreeInThread({ command, thread, appId, }) {
|
|
376
335
|
// Get worktree name from parameter or derive from thread name
|
|
377
336
|
const rawName = command.options.getString('name');
|
|
378
337
|
const rawBaseBranch = command.options.getString('base-branch') || undefined;
|
|
@@ -414,20 +373,109 @@ async function handleWorktreeInThread({ command, thread, }) {
|
|
|
414
373
|
await command.editReply(`Worktree \`${worktreeName}\` already exists at \`${existingWorktreePath}\``);
|
|
415
374
|
return;
|
|
416
375
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
376
|
+
const textChannel = await resolveTextChannel(thread);
|
|
377
|
+
if (!textChannel) {
|
|
378
|
+
await command.editReply('Could not resolve parent text channel');
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
const threadResult = await errore.tryAsync({
|
|
382
|
+
try: async () => {
|
|
383
|
+
const worktreeThread = await textChannel.threads.create({
|
|
384
|
+
name: `${WORKTREE_PREFIX}worktree: ${worktreeName}`.slice(0, 100),
|
|
385
|
+
autoArchiveDuration: 1440,
|
|
386
|
+
reason: `Worktree fork from thread ${thread.id}`,
|
|
387
|
+
});
|
|
388
|
+
await worktreeThread.members.add(command.user.id);
|
|
389
|
+
const statusMessage = await worktreeThread.send({
|
|
390
|
+
content: worktreeCreatingMessage(worktreeName),
|
|
391
|
+
flags: SILENT_MESSAGE_FLAGS,
|
|
392
|
+
});
|
|
393
|
+
return { worktreeThread, statusMessage };
|
|
394
|
+
},
|
|
395
|
+
catch: (e) => new WorktreeError('Failed to create worktree thread', { cause: e }),
|
|
421
396
|
});
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
397
|
+
if (threadResult instanceof Error) {
|
|
398
|
+
await command.editReply(threadResult.message);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const { worktreeThread, statusMessage } = threadResult;
|
|
402
|
+
await command.editReply(`Creating worktree in ${worktreeThread.toString()}`);
|
|
403
|
+
void createWorktreeInBackground({
|
|
404
|
+
thread: worktreeThread,
|
|
425
405
|
starterMessage: statusMessage,
|
|
426
406
|
worktreeName,
|
|
427
407
|
projectDirectory,
|
|
428
408
|
baseBranch,
|
|
429
409
|
rest: command.client.rest,
|
|
430
|
-
})
|
|
410
|
+
})
|
|
411
|
+
.then(async (result) => {
|
|
412
|
+
if (result instanceof Error) {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
const sourceSessionId = await getThreadSession(thread.id);
|
|
416
|
+
if (!sourceSessionId) {
|
|
417
|
+
await sendThreadMessage(worktreeThread, 'Worktree is ready. Send a message here to start a fresh session in this checkout.');
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const getClient = await initializeOpencodeForDirectory(result, {
|
|
421
|
+
originalRepoDirectory: projectDirectory,
|
|
422
|
+
channelId: parent.id,
|
|
423
|
+
});
|
|
424
|
+
if (getClient instanceof Error) {
|
|
425
|
+
await sendThreadMessage(worktreeThread, `✗ Worktree is ready, but failed to initialize OpenCode for context reuse: ${getClient.message}`);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const forkResponse = await errore.tryAsync(() => {
|
|
429
|
+
return getClient().session.fork({
|
|
430
|
+
sessionID: sourceSessionId,
|
|
431
|
+
directory: result,
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
if (forkResponse instanceof Error) {
|
|
435
|
+
logger.error('[NEW-WORKTREE] Failed to fork session into worktree:', forkResponse);
|
|
436
|
+
void notifyError(forkResponse, 'Failed to fork session into worktree');
|
|
437
|
+
await sendThreadMessage(worktreeThread, `✗ Worktree is ready, but failed to reuse session context there: ${forkResponse.message}`);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const forkedSession = forkResponse.data;
|
|
441
|
+
if (!forkedSession) {
|
|
442
|
+
const error = new Error('OpenCode did not return a forked session');
|
|
443
|
+
logger.error('[NEW-WORKTREE] Failed to fork session into worktree:', error);
|
|
444
|
+
void notifyError(error, 'Failed to fork session into worktree');
|
|
445
|
+
await sendThreadMessage(worktreeThread, `✗ Worktree is ready, but failed to reuse session context there: ${error.message}`);
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const permissionResponse = await errore.tryAsync(() => {
|
|
449
|
+
return getClient().session.update({
|
|
450
|
+
sessionID: forkedSession.id,
|
|
451
|
+
directory: result,
|
|
452
|
+
permission: buildSessionPermissions({
|
|
453
|
+
directory: result,
|
|
454
|
+
originalRepoDirectory: projectDirectory,
|
|
455
|
+
}),
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
if (permissionResponse instanceof Error || permissionResponse.error) {
|
|
459
|
+
const error = permissionResponse instanceof Error
|
|
460
|
+
? permissionResponse
|
|
461
|
+
: new Error('OpenCode rejected forked session permission update');
|
|
462
|
+
logger.error('[NEW-WORKTREE] Failed to update forked session permissions:', error);
|
|
463
|
+
void notifyError(error, 'Failed to update forked session permissions');
|
|
464
|
+
await sendThreadMessage(worktreeThread, `✗ Worktree is ready, but failed to update forked session permissions: ${error.message}`);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
await setThreadSession(worktreeThread.id, forkedSession.id);
|
|
468
|
+
getOrCreateRuntime({
|
|
469
|
+
threadId: worktreeThread.id,
|
|
470
|
+
thread: worktreeThread,
|
|
471
|
+
projectDirectory,
|
|
472
|
+
sdkDirectory: result,
|
|
473
|
+
channelId: parent.id,
|
|
474
|
+
appId,
|
|
475
|
+
});
|
|
476
|
+
await sendThreadMessage(worktreeThread, `Reusing context from <#${thread.id}> in worktree session \`${forkedSession.id}\`.`);
|
|
477
|
+
})
|
|
478
|
+
.catch((e) => {
|
|
431
479
|
logger.error('[NEW-WORKTREE] Background error:', e);
|
|
432
480
|
void notifyError(e, 'Background worktree creation failed (in-thread)');
|
|
433
481
|
});
|
|
@@ -46,7 +46,7 @@ export async function handleRestartOpencodeServerCommand({ command, appId, }) {
|
|
|
46
46
|
}
|
|
47
47
|
const { projectDirectory } = resolved;
|
|
48
48
|
// Defer reply since restart may take a moment
|
|
49
|
-
await command.deferReply(
|
|
49
|
+
await command.deferReply();
|
|
50
50
|
// Dispose all runtimes for this directory/channel scope.
|
|
51
51
|
// disposeRuntimesForDirectory aborts active runs, kills listeners, and
|
|
52
52
|
// removes runtimes from the registry. Scoped by channelId so runtimes
|
|
@@ -58,7 +58,7 @@ export async function handleUndoCommand({ command, }) {
|
|
|
58
58
|
});
|
|
59
59
|
return;
|
|
60
60
|
}
|
|
61
|
-
await command.deferReply(
|
|
61
|
+
await command.deferReply();
|
|
62
62
|
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
63
63
|
if (getClient instanceof Error) {
|
|
64
64
|
await command.editReply(`Failed to undo: ${getClient.message}`);
|
|
@@ -209,7 +209,7 @@ export async function handleRedoCommand({ command, }) {
|
|
|
209
209
|
});
|
|
210
210
|
return;
|
|
211
211
|
}
|
|
212
|
-
await command.deferReply(
|
|
212
|
+
await command.deferReply();
|
|
213
213
|
const getClient = await initializeOpencodeForDirectory(projectDirectory);
|
|
214
214
|
if (getClient instanceof Error) {
|
|
215
215
|
await command.editReply(`Failed to redo: ${getClient.message}`);
|