kimaki 0.13.0 → 0.14.0
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 +19 -1
- package/dist/commands/action-buttons.js +2 -0
- package/dist/commands/ask-question.js +2 -0
- package/dist/commands/compact.js +2 -5
- package/dist/commands/file-upload.js +5 -1
- package/dist/commands/model-variant.js +22 -17
- package/dist/commands/model.js +42 -14
- package/dist/commands/new-worktree.js +107 -59
- package/dist/commands/permissions.js +13 -3
- package/dist/config.js +8 -0
- package/dist/context-awareness-plugin.js +9 -4
- package/dist/discord-bot.js +50 -35
- 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 +192 -343
- package/dist/opencode-interrupt-plugin.test.js +168 -381
- package/dist/opencode.js +44 -0
- package/dist/plugin-opencode-client.js +43 -0
- package/dist/queue-advanced-action-buttons.e2e.test.js +1 -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/queue-question-select-drain.e2e.test.js +2 -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/global-event-listener.js +179 -0
- package/dist/session-handler/thread-runtime-state.js +0 -1
- package/dist/session-handler/thread-session-runtime.js +33 -220
- package/dist/store.js +1 -0
- package/dist/subagent-rate-limit-plugin.js +12 -12
- package/dist/system-message.js +4 -4
- package/dist/system-message.test.js +5 -3
- package/dist/thread-message-queue.e2e.test.js +6 -22
- package/dist/undo-redo.e2e.test.js +1 -0
- package/dist/voice-message.e2e.test.js +1 -1
- package/dist/voice.js +3 -2
- package/dist/worktree-lifecycle.e2e.test.js +130 -50
- package/package.json +6 -6
- package/skills/holocron/SKILL.md +192 -14
- package/skills/new-skill/SKILL.md +7 -7
- package/skills/sigillo/SKILL.md +4 -4
- package/skills/spiceflow/SKILL.md +12 -4
- package/skills/strada/SKILL.md +236 -0
- package/skills/termcast/SKILL.md +2 -0
- 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 +25 -1
- package/src/commands/action-buttons.ts +6 -0
- package/src/commands/ask-question.ts +6 -0
- package/src/commands/compact.ts +2 -5
- package/src/commands/file-upload.ts +9 -1
- package/src/commands/model-variant.ts +22 -17
- package/src/commands/model.ts +53 -15
- package/src/commands/new-worktree.ts +136 -81
- package/src/commands/permissions.ts +14 -3
- package/src/config.ts +9 -0
- package/src/context-awareness-plugin.ts +15 -8
- package/src/discord-bot.ts +63 -37
- 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 +213 -429
- package/src/opencode.ts +67 -0
- package/src/plugin-opencode-client.ts +60 -0
- package/src/queue-advanced-action-buttons.e2e.test.ts +1 -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/queue-question-select-drain.e2e.test.ts +2 -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/global-event-listener.ts +224 -0
- package/src/session-handler/thread-runtime-state.ts +0 -8
- package/src/session-handler/thread-session-runtime.ts +41 -276
- package/src/store.ts +10 -0
- package/src/subagent-rate-limit-plugin.ts +13 -12
- package/src/system-message.test.ts +5 -3
- package/src/system-message.ts +8 -4
- package/src/thread-message-queue.e2e.test.ts +6 -24
- package/src/undo-redo.e2e.test.ts +1 -0
- package/src/voice-message.e2e.test.ts +1 -1
- package/src/voice.ts +3 -2
- package/src/worktree-lifecycle.e2e.test.ts +138 -53
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { ButtonBuilder, ButtonStyle, ActionRowBuilder, MessageFlags, } from 'discord.js';
|
|
5
5
|
import crypto from 'node:crypto';
|
|
6
6
|
import { getOpencodeClient } from '../opencode.js';
|
|
7
|
+
import { getPermissionTimeoutMs } from '../config.js';
|
|
7
8
|
import { NOTIFY_MESSAGE_FLAGS } from '../discord-utils.js';
|
|
8
9
|
import { createLogger, LogPrefix } from '../logger.js';
|
|
9
10
|
const logger = createLogger(LogPrefix.PERMISSIONS);
|
|
@@ -59,7 +60,7 @@ export function compactPermissionPatterns(patterns) {
|
|
|
59
60
|
}
|
|
60
61
|
// Store pending permission contexts by hash.
|
|
61
62
|
// TTL prevents unbounded growth if user never clicks a permission button.
|
|
62
|
-
|
|
63
|
+
// Configurable via --permission-timeout-minutes CLI flag (default: 10 minutes).
|
|
63
64
|
export const pendingPermissionContexts = new Map();
|
|
64
65
|
// Atomic take: removes context from Map and returns it. Only the first caller
|
|
65
66
|
// (TTL expiry or button click) wins, preventing duplicate permission replies.
|
|
@@ -90,6 +91,9 @@ export async function showPermissionButtons({ thread, permission, directory, per
|
|
|
90
91
|
// Auto-reject on TTL expiry so the OpenCode session doesn't hang forever
|
|
91
92
|
// waiting for a permission reply that will never come. Uses atomic take
|
|
92
93
|
// so only one of TTL-expiry or button-click can win.
|
|
94
|
+
// With continue_loop_on_deny enabled in opencode config, the model sees
|
|
95
|
+
// this as a tool error and continues (tries alternatives or explains).
|
|
96
|
+
const ttlMs = getPermissionTimeoutMs();
|
|
93
97
|
setTimeout(async () => {
|
|
94
98
|
const ctx = takePendingPermissionContext(contextHash);
|
|
95
99
|
if (!ctx) {
|
|
@@ -100,21 +104,27 @@ export async function showPermissionButtons({ thread, permission, directory, per
|
|
|
100
104
|
const requestIds = ctx.requestIds.length > 0
|
|
101
105
|
? ctx.requestIds
|
|
102
106
|
: [ctx.permission.id];
|
|
107
|
+
const userId = ctx.thread.ownerId;
|
|
108
|
+
const timeoutFeedback = `Permission timed out — the user did not respond. They are probably away and not watching the session. ` +
|
|
109
|
+
`If this tool call is necessary for the core goal of this session, stop and mention the user with <@${userId}> asking them to grant permission. ` +
|
|
110
|
+
`If not, continue normally — work around it, skip the tool, or use an alternative approach.`;
|
|
103
111
|
await Promise.all(requestIds.map((requestId) => {
|
|
104
112
|
return client.permission.reply({
|
|
105
113
|
requestID: requestId,
|
|
106
114
|
directory: ctx.permissionDirectory,
|
|
107
115
|
reply: 'reject',
|
|
116
|
+
message: timeoutFeedback,
|
|
108
117
|
});
|
|
109
118
|
})).catch((error) => {
|
|
110
119
|
logger.error('Failed to auto-reject expired permission:', error);
|
|
111
120
|
});
|
|
121
|
+
const minutes = Math.round(ttlMs / 60_000);
|
|
112
122
|
updatePermissionMessage({
|
|
113
123
|
context: ctx,
|
|
114
|
-
status:
|
|
124
|
+
status: `_Permission expired after ${minutes} minute${minutes !== 1 ? 's' : ''} and was rejected._`,
|
|
115
125
|
});
|
|
116
126
|
}
|
|
117
|
-
},
|
|
127
|
+
}, ttlMs).unref();
|
|
118
128
|
const patternStr = compactPermissionPatterns(permission.patterns).join(', ');
|
|
119
129
|
// Build 3 buttons for permission actions
|
|
120
130
|
const acceptButton = new ButtonBuilder()
|
package/dist/config.js
CHANGED
|
@@ -62,6 +62,14 @@ export function setProjectsDir(dir) {
|
|
|
62
62
|
}
|
|
63
63
|
store.setState({ projectsDir: resolvedDir });
|
|
64
64
|
}
|
|
65
|
+
/**
|
|
66
|
+
* Get the permission button timeout in milliseconds.
|
|
67
|
+
* How long permission buttons remain active before auto-rejecting.
|
|
68
|
+
* Defaults to 10 minutes (600000ms).
|
|
69
|
+
*/
|
|
70
|
+
export function getPermissionTimeoutMs() {
|
|
71
|
+
return store.getState().permissionTimeoutMs;
|
|
72
|
+
}
|
|
65
73
|
const DEFAULT_LOCK_PORT = 29988;
|
|
66
74
|
/**
|
|
67
75
|
* Derive a lock port from the data directory path.
|
|
@@ -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
|
@@ -569,7 +569,7 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
|
|
|
569
569
|
});
|
|
570
570
|
// Notify when a voice message was queued instead of sent immediately
|
|
571
571
|
if (enqueueResult.queued && enqueueResult.position) {
|
|
572
|
-
await sendThreadMessage(thread, `Queued at position ${enqueueResult.position}`);
|
|
572
|
+
await sendThreadMessage(thread, `Queued at position ${enqueueResult.position}. Edit your message to update it in queue`);
|
|
573
573
|
}
|
|
574
574
|
}
|
|
575
575
|
if (channel.type === ChannelType.GuildText) {
|
|
@@ -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,
|
|
@@ -780,8 +777,16 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
|
|
|
780
777
|
// If the edit removed the queue suffix, remove the item from the queue.
|
|
781
778
|
// If the suffix is still present, update the prompt.
|
|
782
779
|
const result = runtime.updateQueuedMessage(message.id, forceQueue ? prompt : '');
|
|
783
|
-
if (result.found) {
|
|
784
|
-
|
|
780
|
+
if (result.found && channel.isThread()) {
|
|
781
|
+
const displayName = message.member?.displayName ?? message.author.displayName;
|
|
782
|
+
if (result.removed) {
|
|
783
|
+
discordLogger.log(`[MESSAGE_EDIT] Removed queued message ${message.id} in thread ${channel.id}`);
|
|
784
|
+
await sendThreadMessage(channel, `⬦ **${displayName}** removed message from queue`);
|
|
785
|
+
}
|
|
786
|
+
else {
|
|
787
|
+
discordLogger.log(`[MESSAGE_EDIT] Updated queued message ${message.id} in thread ${channel.id}`);
|
|
788
|
+
await sendThreadMessage(channel, `⬦ **${displayName}** edited queued message`);
|
|
789
|
+
}
|
|
785
790
|
}
|
|
786
791
|
}
|
|
787
792
|
catch (error) {
|
|
@@ -925,11 +930,24 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
|
|
|
925
930
|
}
|
|
926
931
|
discordLogger.log(`[BOT_SESSION] Starting session for thread ${thread.id} with prompt: "${prompt.slice(0, 50)}..."`);
|
|
927
932
|
const botThreadStartSource = parseSessionStartSourceFromMarker(marker);
|
|
933
|
+
const sessionDirectory = await (async () => {
|
|
934
|
+
if (cwdDirectory) {
|
|
935
|
+
return cwdDirectory;
|
|
936
|
+
}
|
|
937
|
+
if (!worktreePromise) {
|
|
938
|
+
return projectDirectory;
|
|
939
|
+
}
|
|
940
|
+
const result = await worktreePromise;
|
|
941
|
+
if (result instanceof Error) {
|
|
942
|
+
return projectDirectory;
|
|
943
|
+
}
|
|
944
|
+
return result;
|
|
945
|
+
})();
|
|
928
946
|
const runtime = getOrCreateRuntime({
|
|
929
947
|
threadId: thread.id,
|
|
930
948
|
thread,
|
|
931
949
|
projectDirectory,
|
|
932
|
-
sdkDirectory:
|
|
950
|
+
sdkDirectory: sessionDirectory,
|
|
933
951
|
channelId: parent.id,
|
|
934
952
|
appId: currentAppId,
|
|
935
953
|
});
|
|
@@ -950,23 +968,6 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
|
|
|
950
968
|
}
|
|
951
969
|
: undefined,
|
|
952
970
|
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
971
|
const permissionRules = await getChannelReferencePermissionRules({
|
|
971
972
|
message: starterMessage,
|
|
972
973
|
});
|
|
@@ -1017,6 +1018,20 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
|
|
|
1017
1018
|
startHeapMonitor();
|
|
1018
1019
|
const stopTaskRunner = startTaskRunner({ token });
|
|
1019
1020
|
const stopRuntimeIdleSweeper = startRuntimeIdleSweeper();
|
|
1021
|
+
// Prevent discord.js from permanently killing the REST token on 401.
|
|
1022
|
+
// @discordjs/rest calls setToken(null) whenever it receives a 401 response.
|
|
1023
|
+
// The gateway proxy now returns 503 for stale-DB rejections (not 401), but
|
|
1024
|
+
// this guard stays as defense-in-depth for any other transient 401 source.
|
|
1025
|
+
// Allows null through when Client.destroy() is running (it sets client.token
|
|
1026
|
+
// = null before calling rest.setToken(null)).
|
|
1027
|
+
const originalSetToken = discordClient.rest.setToken.bind(discordClient.rest);
|
|
1028
|
+
discordClient.rest.setToken = (newToken) => {
|
|
1029
|
+
if (!newToken && discordClient.token !== null) {
|
|
1030
|
+
discordLogger.warn('[REST] Blocked token nullification from 401 response');
|
|
1031
|
+
return discordClient.rest;
|
|
1032
|
+
}
|
|
1033
|
+
return originalSetToken(newToken);
|
|
1034
|
+
};
|
|
1020
1035
|
const handleShutdown = async (signal, { skipExit = false } = {}) => {
|
|
1021
1036
|
discordLogger.log(`Received ${signal}, cleaning up...`);
|
|
1022
1037
|
if (global.shuttingDown) {
|
|
@@ -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 = [
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// Regression tests for Windows OpenCode command resolution and spawn args.
|
|
2
|
-
import
|
|
3
|
-
import
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
|
6
|
+
import { ensureKimakiCommandShim, getSpawnCommandAndArgs, sanitizeShimExecArgv, selectResolvedCommand, splitCommandLookupOutput, } from './opencode-command.js';
|
|
4
7
|
describe('splitCommandLookupOutput', () => {
|
|
5
8
|
test('splits windows command lookup output into trimmed lines', () => {
|
|
6
9
|
expect(splitCommandLookupOutput('C:\\Program Files\\nodejs\\opencode\r\nC:\\Program Files\\nodejs\\opencode.cmd\r\n')).toEqual([
|
|
@@ -46,3 +49,62 @@ describe('getSpawnCommandAndArgs', () => {
|
|
|
46
49
|
});
|
|
47
50
|
});
|
|
48
51
|
});
|
|
52
|
+
describe('sanitizeShimExecArgv', () => {
|
|
53
|
+
test('strips --env-file=value single-arg form', () => {
|
|
54
|
+
expect(sanitizeShimExecArgv([
|
|
55
|
+
'--require',
|
|
56
|
+
'/abs/tsx/preflight.cjs',
|
|
57
|
+
'--env-file=.env',
|
|
58
|
+
'--import',
|
|
59
|
+
'file:///abs/tsx/loader.mjs',
|
|
60
|
+
])).toEqual([
|
|
61
|
+
'--require',
|
|
62
|
+
'/abs/tsx/preflight.cjs',
|
|
63
|
+
'--import',
|
|
64
|
+
'file:///abs/tsx/loader.mjs',
|
|
65
|
+
]);
|
|
66
|
+
});
|
|
67
|
+
test('strips --env-file value two-arg form and its value', () => {
|
|
68
|
+
expect(sanitizeShimExecArgv(['--env-file', '.env', '--require', '/abs/preflight.cjs'])).toEqual(['--require', '/abs/preflight.cjs']);
|
|
69
|
+
});
|
|
70
|
+
test('strips --env-file-if-exists in both forms', () => {
|
|
71
|
+
expect(sanitizeShimExecArgv([
|
|
72
|
+
'--env-file-if-exists=.env',
|
|
73
|
+
'--env-file-if-exists',
|
|
74
|
+
'/abs/.env',
|
|
75
|
+
'--enable-source-maps',
|
|
76
|
+
])).toEqual(['--enable-source-maps']);
|
|
77
|
+
});
|
|
78
|
+
test('leaves unrelated flags untouched', () => {
|
|
79
|
+
expect(sanitizeShimExecArgv(['--enable-source-maps', '--max-old-space-size=4096'])).toEqual(['--enable-source-maps', '--max-old-space-size=4096']);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
describe('ensureKimakiCommandShim', () => {
|
|
83
|
+
let tempDir;
|
|
84
|
+
beforeEach(() => {
|
|
85
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kimaki-shim-test-'));
|
|
86
|
+
});
|
|
87
|
+
afterEach(() => {
|
|
88
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
89
|
+
});
|
|
90
|
+
test('generated posix shim does not contain a relative --env-file flag', () => {
|
|
91
|
+
const result = ensureKimakiCommandShim({
|
|
92
|
+
dataDir: tempDir,
|
|
93
|
+
execPath: '/usr/bin/node',
|
|
94
|
+
execArgv: [
|
|
95
|
+
'--require',
|
|
96
|
+
'/abs/tsx/preflight.cjs',
|
|
97
|
+
'--env-file=.env',
|
|
98
|
+
'--import',
|
|
99
|
+
'file:///abs/tsx/loader.mjs',
|
|
100
|
+
],
|
|
101
|
+
entryScript: '/abs/cli/src/cli',
|
|
102
|
+
platform: 'linux',
|
|
103
|
+
});
|
|
104
|
+
expect(result).not.toBeInstanceOf(Error);
|
|
105
|
+
const shimContent = fs.readFileSync(path.join(tempDir, 'bin', 'kimaki'), 'utf8');
|
|
106
|
+
expect(shimContent).not.toContain('--env-file');
|
|
107
|
+
expect(shimContent).toContain('/abs/tsx/preflight.cjs');
|
|
108
|
+
expect(shimContent).toContain('/abs/cli/src/cli');
|
|
109
|
+
});
|
|
110
|
+
});
|