kimaki 0.13.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/channel-reference-permissions.e2e.test.js +2 -0
- package/dist/cli-parsing.test.js +1 -1
- package/dist/cli.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/context-awareness-plugin.js +9 -4
- package/dist/discord-bot.js +25 -32
- package/dist/message-finish-field.e2e.test.js +1 -0
- 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 -341
- package/dist/opencode-interrupt-plugin.test.js +168 -381
- 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/event-stream-state.js +3 -1
- package/dist/session-handler/event-stream-state.test.js +67 -1
- package/dist/session-handler/thread-session-runtime.js +11 -60
- package/dist/subagent-rate-limit-plugin.js +12 -12
- package/dist/thread-message-queue.e2e.test.js +2 -20
- 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 +176 -14
- 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/channel-reference-permissions.e2e.test.ts +2 -0
- package/src/cli-parsing.test.ts +1 -1
- package/src/cli.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/context-awareness-plugin.ts +15 -8
- package/src/discord-bot.ts +27 -32
- package/src/message-finish-field.e2e.test.ts +1 -0
- 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 -520
- package/src/opencode-interrupt-plugin.ts +206 -428
- 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/event-stream-state.test.ts +72 -2
- package/src/session-handler/event-stream-state.ts +3 -1
- package/src/session-handler/thread-session-runtime.ts +16 -78
- package/src/subagent-rate-limit-plugin.ts +13 -12
- package/src/thread-message-queue.e2e.test.ts +2 -22
- 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',
|
|
@@ -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.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/docs/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/docs/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
|
});
|
|
@@ -18,6 +18,7 @@ import crypto from 'node:crypto';
|
|
|
18
18
|
import * as errore from 'errore';
|
|
19
19
|
import { createPluginLogger, formatPluginErrorWithStack, setPluginLogFilePath, } from './plugin-logger.js';
|
|
20
20
|
import { setDataDir } from './config.js';
|
|
21
|
+
import { createPluginClient } from './plugin-opencode-client.js';
|
|
21
22
|
import { initSentry, notifyError } from './sentry.js';
|
|
22
23
|
import { execAsync } from './exec-async.js';
|
|
23
24
|
import { ONBOARDING_TUTORIAL_INSTRUCTIONS, TUTORIAL_WELCOME_TEXT, } from './onboarding-tutorial.js';
|
|
@@ -145,7 +146,7 @@ async function resolveGitState({ directory, }) {
|
|
|
145
146
|
async function resolveSessionDirectory({ client, sessionID, state, }) {
|
|
146
147
|
const previousDirectory = state.resolvedDirectory;
|
|
147
148
|
const result = await errore.tryAsync(() => {
|
|
148
|
-
return client.session.get({
|
|
149
|
+
return client.session.get({ sessionID });
|
|
149
150
|
});
|
|
150
151
|
if (result instanceof Error || !result.data?.directory) {
|
|
151
152
|
return {
|
|
@@ -160,13 +161,16 @@ async function resolveSessionDirectory({ client, sessionID, state, }) {
|
|
|
160
161
|
};
|
|
161
162
|
}
|
|
162
163
|
// ── Plugin ───────────────────────────────────────────────────────
|
|
163
|
-
const contextAwarenessPlugin = async ({ directory,
|
|
164
|
+
const contextAwarenessPlugin = async ({ directory, serverUrl }) => {
|
|
164
165
|
initSentry();
|
|
165
166
|
const dataDir = process.env.KIMAKI_DATA_DIR;
|
|
166
167
|
if (dataDir) {
|
|
167
168
|
setDataDir(dataDir);
|
|
168
169
|
setPluginLogFilePath(dataDir);
|
|
169
170
|
}
|
|
171
|
+
// Build our own v2 client. The plugin-provided ctx.client (v1) does not
|
|
172
|
+
// reliably make REST calls from inside the plugin process.
|
|
173
|
+
const client = createPluginClient({ serverUrl, directory });
|
|
170
174
|
// Single Map for all per-session state. One entry per session, one
|
|
171
175
|
// delete on cleanup — no parallel Maps that can drift out of sync.
|
|
172
176
|
const sessions = new Map();
|
|
@@ -225,8 +229,9 @@ const contextAwarenessPlugin = async ({ directory, client }) => {
|
|
|
225
229
|
const messageID = first.messageID;
|
|
226
230
|
const latestAssistantMessageResult = await errore.tryAsync(() => {
|
|
227
231
|
return client.session.messages({
|
|
228
|
-
|
|
229
|
-
|
|
232
|
+
sessionID,
|
|
233
|
+
directory,
|
|
234
|
+
limit: 20,
|
|
230
235
|
});
|
|
231
236
|
});
|
|
232
237
|
const latestAssistantMessage = latestAssistantMessageResult instanceof Error
|
package/dist/discord-bot.js
CHANGED
|
@@ -687,11 +687,21 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
|
|
|
687
687
|
rest: discordClient.rest,
|
|
688
688
|
});
|
|
689
689
|
}
|
|
690
|
+
const sessionDirectory = await (async () => {
|
|
691
|
+
if (!worktreePromise) {
|
|
692
|
+
return projectDirectory;
|
|
693
|
+
}
|
|
694
|
+
const result = await worktreePromise;
|
|
695
|
+
if (result instanceof Error) {
|
|
696
|
+
return projectDirectory;
|
|
697
|
+
}
|
|
698
|
+
return result;
|
|
699
|
+
})();
|
|
690
700
|
const channelRuntime = getOrCreateRuntime({
|
|
691
701
|
threadId: thread.id,
|
|
692
702
|
thread,
|
|
693
703
|
projectDirectory,
|
|
694
|
-
sdkDirectory:
|
|
704
|
+
sdkDirectory: sessionDirectory,
|
|
695
705
|
channelId: channel.id,
|
|
696
706
|
appId: currentAppId,
|
|
697
707
|
});
|
|
@@ -703,19 +713,6 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
|
|
|
703
713
|
sourceThreadId: thread.id,
|
|
704
714
|
appId: currentAppId,
|
|
705
715
|
preprocess: async () => {
|
|
706
|
-
// Wait for worktree creation + install before preprocessing.
|
|
707
|
-
// Follow-up messages queue behind this in the preprocess chain.
|
|
708
|
-
let sessionDirectory = projectDirectory;
|
|
709
|
-
if (worktreePromise) {
|
|
710
|
-
const result = await worktreePromise;
|
|
711
|
-
if (!(result instanceof Error)) {
|
|
712
|
-
sessionDirectory = result;
|
|
713
|
-
channelRuntime.handleDirectoryChanged({
|
|
714
|
-
oldDirectory: projectDirectory,
|
|
715
|
-
newDirectory: sessionDirectory,
|
|
716
|
-
});
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
716
|
return preprocessNewThreadMessage({
|
|
720
717
|
message,
|
|
721
718
|
thread,
|
|
@@ -925,11 +922,24 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
|
|
|
925
922
|
}
|
|
926
923
|
discordLogger.log(`[BOT_SESSION] Starting session for thread ${thread.id} with prompt: "${prompt.slice(0, 50)}..."`);
|
|
927
924
|
const botThreadStartSource = parseSessionStartSourceFromMarker(marker);
|
|
925
|
+
const sessionDirectory = await (async () => {
|
|
926
|
+
if (cwdDirectory) {
|
|
927
|
+
return cwdDirectory;
|
|
928
|
+
}
|
|
929
|
+
if (!worktreePromise) {
|
|
930
|
+
return projectDirectory;
|
|
931
|
+
}
|
|
932
|
+
const result = await worktreePromise;
|
|
933
|
+
if (result instanceof Error) {
|
|
934
|
+
return projectDirectory;
|
|
935
|
+
}
|
|
936
|
+
return result;
|
|
937
|
+
})();
|
|
928
938
|
const runtime = getOrCreateRuntime({
|
|
929
939
|
threadId: thread.id,
|
|
930
940
|
thread,
|
|
931
941
|
projectDirectory,
|
|
932
|
-
sdkDirectory:
|
|
942
|
+
sdkDirectory: sessionDirectory,
|
|
933
943
|
channelId: parent.id,
|
|
934
944
|
appId: currentAppId,
|
|
935
945
|
});
|
|
@@ -950,23 +960,6 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
|
|
|
950
960
|
}
|
|
951
961
|
: undefined,
|
|
952
962
|
preprocess: async () => {
|
|
953
|
-
// Wait for worktree creation + install before starting session.
|
|
954
|
-
if (worktreePromise) {
|
|
955
|
-
const result = await worktreePromise;
|
|
956
|
-
if (!(result instanceof Error)) {
|
|
957
|
-
runtime.handleDirectoryChanged({
|
|
958
|
-
oldDirectory: projectDirectory,
|
|
959
|
-
newDirectory: result,
|
|
960
|
-
});
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
// --cwd: switch sdkDirectory to the existing worktree path
|
|
964
|
-
if (cwdDirectory) {
|
|
965
|
-
runtime.handleDirectoryChanged({
|
|
966
|
-
oldDirectory: projectDirectory,
|
|
967
|
-
newDirectory: cwdDirectory,
|
|
968
|
-
});
|
|
969
|
-
}
|
|
970
963
|
const permissionRules = await getChannelReferencePermissionRules({
|
|
971
964
|
message: starterMessage,
|
|
972
965
|
});
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* Account management is done via `kimaki multioauth openai` CLI commands.
|
|
15
15
|
*/
|
|
16
16
|
import { createPluginLogger, appendToastSessionMarker } from './plugin-logger.js';
|
|
17
|
+
import { createPluginClient } from './plugin-opencode-client.js';
|
|
17
18
|
import { isRateLimitRetryMessage, isTokenRefreshError, isOAuthStored, readJson, authFilePath } from './oauth-rotation-shared.js';
|
|
18
19
|
import { detectAndRememberNewOpenAIAccount, loadOpenAIAccountStore, rotateOpenAIAccount, } from './openai-auth-state.js';
|
|
19
20
|
const log = createPluginLogger('openai-rotation');
|
|
@@ -30,7 +31,7 @@ function isRetryStatusEvent(event) {
|
|
|
30
31
|
// the last message in the session to find the model.
|
|
31
32
|
async function isOpenAISession(client, sessionID) {
|
|
32
33
|
try {
|
|
33
|
-
const res = await client.session.messages({
|
|
34
|
+
const res = await client.session.messages({ sessionID });
|
|
34
35
|
const lastMessage = res.data?.filter((m) => m.info).at(-1)?.info;
|
|
35
36
|
if (!lastMessage)
|
|
36
37
|
return false;
|
|
@@ -45,8 +46,11 @@ async function isOpenAISession(client, sessionID) {
|
|
|
45
46
|
// Throttle login detection to avoid spamming auth.json reads
|
|
46
47
|
let lastLoginCheckMs = 0;
|
|
47
48
|
const LOGIN_CHECK_INTERVAL_MS = 30_000;
|
|
48
|
-
const openaiRotationPlugin = async ({
|
|
49
|
+
const openaiRotationPlugin = async ({ serverUrl, directory }) => {
|
|
49
50
|
log.info('OpenAI rotation plugin loaded');
|
|
51
|
+
// Build our own v2 client. The plugin-provided ctx.client (v1) does not
|
|
52
|
+
// reliably make REST calls from inside the plugin process.
|
|
53
|
+
const client = createPluginClient({ serverUrl, directory });
|
|
50
54
|
return {
|
|
51
55
|
'chat.headers': async (input, output) => {
|
|
52
56
|
if (input.model.providerID !== 'openai')
|
|
@@ -69,13 +73,11 @@ const openaiRotationPlugin = async ({ client }) => {
|
|
|
69
73
|
const count = store?.accounts.length ?? 1;
|
|
70
74
|
client.tui
|
|
71
75
|
.showToast({
|
|
72
|
-
|
|
73
|
-
message:
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
variant: 'info',
|
|
78
|
-
},
|
|
76
|
+
message: appendToastSessionMarker({
|
|
77
|
+
message: `OpenAI account ${label} added to rotation pool (${count} account${count === 1 ? '' : 's'})`,
|
|
78
|
+
sessionId: event.properties.sessionID,
|
|
79
|
+
}),
|
|
80
|
+
variant: 'info',
|
|
79
81
|
})
|
|
80
82
|
.catch(() => { });
|
|
81
83
|
}
|
|
@@ -106,13 +108,11 @@ const openaiRotationPlugin = async ({ client }) => {
|
|
|
106
108
|
if (result) {
|
|
107
109
|
client.tui
|
|
108
110
|
.showToast({
|
|
109
|
-
|
|
110
|
-
message:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
variant: 'info',
|
|
115
|
-
},
|
|
111
|
+
message: appendToastSessionMarker({
|
|
112
|
+
message: `Switching OpenAI from ${result.fromLabel} to ${result.toLabel}`,
|
|
113
|
+
sessionId: sessionID,
|
|
114
|
+
}),
|
|
115
|
+
variant: 'info',
|
|
116
116
|
})
|
|
117
117
|
.catch(() => { });
|
|
118
118
|
}
|
|
@@ -92,7 +92,7 @@ async function writeOpenAIAuthFile(auth) {
|
|
|
92
92
|
}
|
|
93
93
|
export async function setOpenAIAuth(auth, client) {
|
|
94
94
|
await writeOpenAIAuthFile(auth);
|
|
95
|
-
await client.auth.set({
|
|
95
|
+
await client.auth.set({ providerID: 'openai', auth });
|
|
96
96
|
}
|
|
97
97
|
// --- Remember new login ---
|
|
98
98
|
export async function rememberOpenAIOAuth(auth, identity) {
|
package/dist/opencode-command.js
CHANGED
|
@@ -62,12 +62,36 @@ export function getSpawnCommandAndArgs({ resolvedCommand, baseArgs, platform, })
|
|
|
62
62
|
windowsVerbatimArguments: true,
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
|
+
// Remove flags from the parent process's execArgv that must not leak into the
|
|
66
|
+
// relocatable kimaki shim. The shim runs from arbitrary working directories
|
|
67
|
+
// (it is on PATH for opencode child processes), so a relative `--env-file=.env`
|
|
68
|
+
// would make node abort with ".env: not found" whenever the cwd has no .env.
|
|
69
|
+
// The shim does not need to re-load env files at all: the env vars the bot
|
|
70
|
+
// cares about are already in the inherited process environment. We strip both
|
|
71
|
+
// `--env-file`/`--env-file-if-exists` forms: `--env-file=value` (single arg)
|
|
72
|
+
// and `--env-file value` (two args).
|
|
73
|
+
export function sanitizeShimExecArgv(execArgv) {
|
|
74
|
+
const sanitized = [];
|
|
75
|
+
for (let index = 0; index < execArgv.length; index++) {
|
|
76
|
+
const arg = execArgv[index];
|
|
77
|
+
if (arg === '--env-file' || arg === '--env-file-if-exists') {
|
|
78
|
+
// Skip this flag and its separate value argument, if present.
|
|
79
|
+
index++;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (arg.startsWith('--env-file=') || arg.startsWith('--env-file-if-exists=')) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
sanitized.push(arg);
|
|
86
|
+
}
|
|
87
|
+
return sanitized;
|
|
88
|
+
}
|
|
65
89
|
export function ensureKimakiCommandShim({ dataDir, execPath, execArgv, entryScript, platform, }) {
|
|
66
90
|
const effectivePlatform = platform || process.platform;
|
|
67
91
|
const shimDirectory = path.join(dataDir, 'bin');
|
|
68
92
|
try {
|
|
69
93
|
fs.mkdirSync(shimDirectory, { recursive: true });
|
|
70
|
-
const launcherArgs = [...execArgv, entryScript];
|
|
94
|
+
const launcherArgs = [...sanitizeShimExecArgv(execArgv), entryScript];
|
|
71
95
|
if (effectivePlatform === 'win32') {
|
|
72
96
|
const shimPath = path.join(shimDirectory, 'kimaki.cmd');
|
|
73
97
|
const shimContent = [
|