kimaki 0.25.0 → 0.26.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/agent-model.e2e.test.js +135 -3
- package/dist/analytics.test.js +30 -1
- package/dist/cli-commands/send.js +13 -3
- package/dist/cli-commands/task.js +26 -7
- package/dist/cli-runner.js +66 -70
- package/dist/cli-runner.test.js +10 -1
- package/dist/commands/agent.js +87 -74
- package/dist/commands/model.js +18 -0
- package/dist/commands/multioauth.js +169 -1
- package/dist/commands/tasks.js +6 -0
- package/dist/commands/unset-model.js +5 -20
- package/dist/database.js +65 -0
- package/dist/discord-bot.js +7 -1
- package/dist/ipc-tools-plugin.js +4 -3
- package/dist/kimaki-opencode-plugin.js +2 -0
- package/dist/message-formatting.js +84 -5
- package/dist/message-formatting.test.js +160 -2
- package/dist/oauth-rotation-shared.js +7 -1
- package/dist/openai-auth-state.js +20 -0
- package/dist/schema.js +19 -0
- package/dist/session-handler/event-stream-state.js +147 -0
- package/dist/session-handler/event-stream-state.test.js +437 -1
- package/dist/session-handler/thread-session-runtime.js +90 -37
- package/dist/subagent-rate-limit-plugin.js +13 -2
- package/dist/system-message.js +18 -4
- package/dist/system-message.test.js +28 -4
- package/dist/task-runner.js +143 -12
- package/dist/task-schedule.js +12 -0
- package/dist/task-schedule.test.js +149 -2
- package/dist/xai-auth-plugin.js +237 -0
- package/dist/xai-auth-state.js +233 -0
- package/dist/xai-auth-state.test.js +183 -0
- package/package.json +6 -6
- package/skills/profano/SKILL.md +5 -3
- package/skills/spiceflow/SKILL.md +24 -1
- package/src/agent-model.e2e.test.ts +172 -4
- package/src/analytics.test.ts +30 -1
- package/src/analytics.ts +1 -0
- package/src/cli-commands/send.ts +22 -3
- package/src/cli-commands/task.ts +32 -19
- package/src/cli-runner.test.ts +16 -1
- package/src/cli-runner.ts +88 -89
- package/src/commands/agent.ts +120 -84
- package/src/commands/model.ts +25 -0
- package/src/commands/multioauth.ts +195 -2
- package/src/commands/tasks.ts +6 -0
- package/src/commands/unset-model.ts +5 -21
- package/src/database.ts +124 -0
- package/src/discord-bot.ts +7 -1
- package/src/ipc-tools-plugin.ts +4 -3
- package/src/kimaki-opencode-plugin.ts +2 -0
- package/src/message-formatting.test.ts +197 -2
- package/src/message-formatting.ts +98 -5
- package/src/oauth-rotation-shared.ts +6 -1
- package/src/openai-auth-state.ts +19 -0
- package/src/schema.sql +15 -0
- package/src/schema.ts +20 -0
- package/src/session-handler/event-stream-state.test.ts +490 -0
- package/src/session-handler/event-stream-state.ts +210 -0
- package/src/session-handler/model-utils.ts +1 -0
- package/src/session-handler/thread-session-runtime.ts +105 -44
- package/src/subagent-rate-limit-plugin.ts +12 -2
- package/src/system-message.test.ts +29 -4
- package/src/system-message.ts +20 -4
- package/src/task-runner.ts +168 -11
- package/src/task-schedule.test.ts +172 -2
- package/src/task-schedule.ts +23 -2
- package/src/xai-auth-plugin.ts +302 -0
- package/src/xai-auth-state.test.ts +224 -0
- package/src/xai-auth-state.ts +293 -0
package/dist/commands/agent.js
CHANGED
|
@@ -9,7 +9,7 @@ import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
|
9
9
|
import { resolveTextChannel, resolveWorkingDirectory, getKimakiMetadata, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
|
|
10
10
|
import { getOrCreateRuntime } from '../session-handler/thread-session-runtime.js';
|
|
11
11
|
import { createLogger, LogPrefix } from '../logger.js';
|
|
12
|
-
import { getCurrentModelInfo } from './model.js';
|
|
12
|
+
import { formatModelSource, getCurrentModelInfo, } from './model.js';
|
|
13
13
|
import { isGitRepositoryRoot } from '../worktrees.js';
|
|
14
14
|
import { formatAutoWorktreeName, createWorktreeInBackground, worktreeCreatingMessage, } from './new-worktree.js';
|
|
15
15
|
import { WORKTREE_PREFIX } from './merge-worktree.js';
|
|
@@ -100,7 +100,7 @@ async function resolveQuickAgentNameFromInteraction({ command, }) {
|
|
|
100
100
|
* Resolve the context for an agent command (directory, channel, session).
|
|
101
101
|
* Returns null if the command cannot be executed in this context.
|
|
102
102
|
*/
|
|
103
|
-
export async function resolveAgentCommandContext({ interaction, }) {
|
|
103
|
+
export async function resolveAgentCommandContext({ interaction, appId, }) {
|
|
104
104
|
const channel = interaction.channel;
|
|
105
105
|
if (!channel) {
|
|
106
106
|
await interaction.editReply({
|
|
@@ -113,39 +113,35 @@ export async function resolveAgentCommandContext({ interaction, }) {
|
|
|
113
113
|
ChannelType.PrivateThread,
|
|
114
114
|
ChannelType.AnnouncementThread,
|
|
115
115
|
].includes(channel.type);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
let sessionId;
|
|
119
|
-
if (isThread) {
|
|
120
|
-
const thread = channel;
|
|
121
|
-
const textChannel = await resolveTextChannel(thread);
|
|
122
|
-
const metadata = await getKimakiMetadata(textChannel);
|
|
123
|
-
projectDirectory = metadata.projectDirectory;
|
|
124
|
-
targetChannelId = textChannel?.id || channel.id;
|
|
125
|
-
sessionId = await getThreadSession(thread.id);
|
|
126
|
-
}
|
|
127
|
-
else if (channel.type === ChannelType.GuildText) {
|
|
128
|
-
const metadata = await getKimakiMetadata(channel);
|
|
129
|
-
projectDirectory = metadata.projectDirectory;
|
|
130
|
-
targetChannelId = channel.id;
|
|
131
|
-
}
|
|
132
|
-
else {
|
|
116
|
+
if (!isThread &&
|
|
117
|
+
channel.type !== ChannelType.GuildText) {
|
|
133
118
|
await interaction.editReply({
|
|
134
119
|
content: 'This command can only be used in text channels or threads',
|
|
135
120
|
});
|
|
136
121
|
return null;
|
|
137
122
|
}
|
|
138
|
-
|
|
123
|
+
const resolved = await resolveWorkingDirectory({
|
|
124
|
+
channel: channel,
|
|
125
|
+
});
|
|
126
|
+
if (!resolved) {
|
|
139
127
|
await interaction.editReply({
|
|
140
128
|
content: 'This channel is not configured with a project directory',
|
|
141
129
|
});
|
|
142
130
|
return null;
|
|
143
131
|
}
|
|
132
|
+
const textChannel = isThread
|
|
133
|
+
? await resolveTextChannel(channel)
|
|
134
|
+
: undefined;
|
|
135
|
+
const sessionId = isThread
|
|
136
|
+
? await getThreadSession(channel.id)
|
|
137
|
+
: undefined;
|
|
144
138
|
return {
|
|
145
|
-
dir: projectDirectory,
|
|
146
|
-
|
|
139
|
+
dir: resolved.projectDirectory,
|
|
140
|
+
workingDirectory: resolved.workingDirectory,
|
|
141
|
+
channelId: textChannel?.id || channel.id,
|
|
147
142
|
sessionId,
|
|
148
143
|
isThread,
|
|
144
|
+
appId,
|
|
149
145
|
};
|
|
150
146
|
}
|
|
151
147
|
/**
|
|
@@ -165,6 +161,42 @@ export async function setAgentForContext({ context, agentName, }) {
|
|
|
165
161
|
agentLogger.log(`Set agent ${agentName} for channel ${context.channelId}`);
|
|
166
162
|
}
|
|
167
163
|
}
|
|
164
|
+
function formatAgentModelLine(modelInfo) {
|
|
165
|
+
if (modelInfo.type === 'none')
|
|
166
|
+
return '';
|
|
167
|
+
const source = formatModelSource({
|
|
168
|
+
type: modelInfo.type,
|
|
169
|
+
agentName: modelInfo.type === 'agent' ? modelInfo.agentName : undefined,
|
|
170
|
+
});
|
|
171
|
+
const line = `\nModel: *${modelInfo.model}* (${source})`;
|
|
172
|
+
if (modelInfo.type !== 'session' && modelInfo.type !== 'channel') {
|
|
173
|
+
return line;
|
|
174
|
+
}
|
|
175
|
+
return `${line}\nThis model comes from a ${modelInfo.type} override. Use /model and press Clear override if you want this agent's model.`;
|
|
176
|
+
}
|
|
177
|
+
function formatAgentPreferenceReply({ agentName, previousAgentName, modelInfo, scope, }) {
|
|
178
|
+
const sameAgent = previousAgentName === agentName;
|
|
179
|
+
const verb = sameAgent ? 'Using' : 'Switched to';
|
|
180
|
+
const previousText = !sameAgent && previousAgentName ? ` (was **${previousAgentName}**)` : '';
|
|
181
|
+
const modelText = formatAgentModelLine(modelInfo);
|
|
182
|
+
if (scope === 'session') {
|
|
183
|
+
return `${verb} **${agentName}** agent for this session${previousText}${modelText}\nThe agent will change on the next message.`;
|
|
184
|
+
}
|
|
185
|
+
return `${verb} **${agentName}** agent for this channel${previousText}${modelText}\nAll new sessions will use this agent.`;
|
|
186
|
+
}
|
|
187
|
+
async function resolveAgentModelInfo({ context, agentName, }) {
|
|
188
|
+
const getClient = await initializeOpencodeForDirectory(context.dir);
|
|
189
|
+
if (getClient instanceof Error)
|
|
190
|
+
return { type: 'none' };
|
|
191
|
+
return getCurrentModelInfo({
|
|
192
|
+
sessionId: context.sessionId,
|
|
193
|
+
channelId: context.channelId,
|
|
194
|
+
appId: context.appId,
|
|
195
|
+
agentPreference: agentName,
|
|
196
|
+
getClient,
|
|
197
|
+
directory: context.workingDirectory,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
168
200
|
export async function handleAgentCommand({ interaction, appId, }) {
|
|
169
201
|
await interaction.deferReply();
|
|
170
202
|
const context = await resolveAgentCommandContext({ interaction, appId });
|
|
@@ -178,7 +210,7 @@ export async function handleAgentCommand({ interaction, appId, }) {
|
|
|
178
210
|
return;
|
|
179
211
|
}
|
|
180
212
|
const agentsResponse = await getClient().app.agents({
|
|
181
|
-
directory: context.
|
|
213
|
+
directory: context.workingDirectory,
|
|
182
214
|
});
|
|
183
215
|
if (!agentsResponse.data || agentsResponse.data.length === 0) {
|
|
184
216
|
await interaction.editReply({ content: 'No agents available' });
|
|
@@ -259,19 +291,26 @@ export async function handleAgentSelectMenu(interaction) {
|
|
|
259
291
|
return;
|
|
260
292
|
}
|
|
261
293
|
try {
|
|
294
|
+
const previousAgent = await getCurrentAgentInfo({
|
|
295
|
+
sessionId: context.sessionId,
|
|
296
|
+
channelId: context.channelId,
|
|
297
|
+
});
|
|
298
|
+
const previousAgentName = previousAgent.type !== 'none' ? previousAgent.agent : undefined;
|
|
262
299
|
await setAgentForContext({ context, agentName: selectedAgent });
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
300
|
+
const modelInfo = await resolveAgentModelInfo({
|
|
301
|
+
context,
|
|
302
|
+
agentName: selectedAgent,
|
|
303
|
+
});
|
|
304
|
+
const scope = context.isThread && context.sessionId ? 'session' : 'channel';
|
|
305
|
+
await interaction.editReply({
|
|
306
|
+
content: formatAgentPreferenceReply({
|
|
307
|
+
agentName: selectedAgent,
|
|
308
|
+
previousAgentName,
|
|
309
|
+
modelInfo,
|
|
310
|
+
scope,
|
|
311
|
+
}),
|
|
312
|
+
components: [],
|
|
313
|
+
});
|
|
275
314
|
pendingAgentContexts.delete(contextHash);
|
|
276
315
|
}
|
|
277
316
|
catch (error) {
|
|
@@ -317,46 +356,20 @@ export async function handleQuickAgentCommand({ command, appId, }) {
|
|
|
317
356
|
channelId: context.channelId,
|
|
318
357
|
});
|
|
319
358
|
const previousAgentName = previousAgent.type !== 'none' ? previousAgent.agent : undefined;
|
|
320
|
-
if (previousAgentName === resolvedAgentName) {
|
|
321
|
-
await command.editReply({
|
|
322
|
-
content: `Already using **${resolvedAgentName}** agent`,
|
|
323
|
-
});
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
// Set the agent preference in DB for this context.
|
|
327
359
|
await setAgentForContext({ context, agentName: resolvedAgentName });
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
:
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
sessionId: context.sessionId,
|
|
342
|
-
channelId: context.channelId,
|
|
343
|
-
appId,
|
|
344
|
-
agentPreference: resolvedAgentName,
|
|
345
|
-
getClient,
|
|
346
|
-
directory: context.dir,
|
|
347
|
-
});
|
|
348
|
-
})();
|
|
349
|
-
const modelText = modelInfo.type === 'none' ? '' : `\nModel: *${modelInfo.model}*`;
|
|
350
|
-
if (context.isThread && context.sessionId) {
|
|
351
|
-
await command.editReply({
|
|
352
|
-
content: `Switched to **${resolvedAgentName}** agent for this session${previousText}${modelText}\nThe agent will change on the next message.`,
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
else {
|
|
356
|
-
await command.editReply({
|
|
357
|
-
content: `Switched to **${resolvedAgentName}** agent for this channel${previousText}${modelText}\nAll new sessions will use this agent.`,
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
+
const modelInfo = await resolveAgentModelInfo({
|
|
361
|
+
context,
|
|
362
|
+
agentName: resolvedAgentName,
|
|
363
|
+
});
|
|
364
|
+
const scope = context.isThread && context.sessionId ? 'session' : 'channel';
|
|
365
|
+
await command.editReply({
|
|
366
|
+
content: formatAgentPreferenceReply({
|
|
367
|
+
agentName: resolvedAgentName,
|
|
368
|
+
previousAgentName,
|
|
369
|
+
modelInfo,
|
|
370
|
+
scope,
|
|
371
|
+
}),
|
|
372
|
+
});
|
|
360
373
|
}
|
|
361
374
|
catch (error) {
|
|
362
375
|
agentLogger.error('Error in quick agent command:', error);
|
package/dist/commands/model.js
CHANGED
|
@@ -37,6 +37,24 @@ function setModelContext(contextHash, context) {
|
|
|
37
37
|
pendingModelContexts.delete(contextHash);
|
|
38
38
|
}, MODEL_CONTEXT_TTL_MS).unref();
|
|
39
39
|
}
|
|
40
|
+
export function formatModelSource({ type, agentName, }) {
|
|
41
|
+
switch (type) {
|
|
42
|
+
case 'session':
|
|
43
|
+
return 'session override';
|
|
44
|
+
case 'agent':
|
|
45
|
+
return `agent "${agentName}"`;
|
|
46
|
+
case 'channel':
|
|
47
|
+
return 'channel override';
|
|
48
|
+
case 'global':
|
|
49
|
+
return 'global default';
|
|
50
|
+
case 'opencode-config':
|
|
51
|
+
case 'opencode-recent':
|
|
52
|
+
case 'opencode-provider-default':
|
|
53
|
+
return 'opencode default';
|
|
54
|
+
default:
|
|
55
|
+
return 'none';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
40
58
|
function parseModelId(modelString) {
|
|
41
59
|
const [providerID, ...modelParts] = modelString.split('/');
|
|
42
60
|
const modelID = modelParts.join('/');
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* CLI commands for multi-provider OAuth account management.
|
|
3
3
|
* Mounted via goke .use() in cli.ts under the `multioauth` namespace.
|
|
4
|
-
* Manages Anthropic and
|
|
4
|
+
* Manages Anthropic, OpenAI, and xAI OAuth account rotation pools.
|
|
5
5
|
*/
|
|
6
6
|
import { goke } from 'goke';
|
|
7
7
|
import { accountLabel as anthropicAccountLabel, accountsFilePath as anthropicAccountsFilePath, authFilePath, getCurrentAnthropicAccount, loadAccountStore as loadAnthropicAccountStore, removeAccount as removeAnthropicAccount, } from '../anthropic-auth-state.js';
|
|
8
8
|
import { accountLabel as openaiAccountLabel, openaiAccountsFilePath, getCurrentOpenAIAccount, loadOpenAIAccountStore, removeOpenAIAccount, saveOpenAIAccountStore, } from '../openai-auth-state.js';
|
|
9
|
+
import { accountLabel as xaiAccountLabel, xaiAccountsFilePath, getCurrentXAIAccount, loadXAIAccountStore, removeXAIAccount, saveXAIAccountStore, } from '../xai-auth-state.js';
|
|
9
10
|
const EXIT_NO_RESTART = 64;
|
|
10
11
|
function resolveAccountIndex(indexOrEmail, accounts) {
|
|
11
12
|
const value = Number(indexOrEmail);
|
|
@@ -25,6 +26,7 @@ multioauth
|
|
|
25
26
|
.action(async () => {
|
|
26
27
|
const anthropicStore = await loadAnthropicAccountStore();
|
|
27
28
|
const openaiStore = await loadOpenAIAccountStore();
|
|
29
|
+
const xaiStore = await loadXAIAccountStore();
|
|
28
30
|
console.log('Anthropic OAuth accounts:');
|
|
29
31
|
if (anthropicStore.accounts.length === 0) {
|
|
30
32
|
console.log(' (none)');
|
|
@@ -46,6 +48,17 @@ multioauth
|
|
|
46
48
|
console.log(` ${active} ${index + 1}. ${openaiAccountLabel(account)}`);
|
|
47
49
|
});
|
|
48
50
|
}
|
|
51
|
+
console.log('');
|
|
52
|
+
console.log('xAI OAuth accounts:');
|
|
53
|
+
if (xaiStore.accounts.length === 0) {
|
|
54
|
+
console.log(' (none)');
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
xaiStore.accounts.forEach((account, index) => {
|
|
58
|
+
const active = index === xaiStore.activeIndex ? '*' : ' ';
|
|
59
|
+
console.log(` ${active} ${index + 1}. ${xaiAccountLabel(account)}`);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
49
62
|
process.exit(0);
|
|
50
63
|
});
|
|
51
64
|
// --- Anthropic subcommands ---
|
|
@@ -198,6 +211,9 @@ multioauth
|
|
|
198
211
|
const json = (await response.json());
|
|
199
212
|
accessToken = json.access_token;
|
|
200
213
|
account.access = accessToken;
|
|
214
|
+
// OpenAI can rotate refresh tokens; store the new one if returned
|
|
215
|
+
if (json.refresh_token)
|
|
216
|
+
account.refresh = json.refresh_token;
|
|
201
217
|
account.expires = Date.now() + (json.expires_in ?? 3600) * 1000;
|
|
202
218
|
}
|
|
203
219
|
catch {
|
|
@@ -253,4 +269,156 @@ multioauth
|
|
|
253
269
|
await saveOpenAIAccountStore(store);
|
|
254
270
|
process.exit(0);
|
|
255
271
|
});
|
|
272
|
+
// --- xAI subcommands ---
|
|
273
|
+
multioauth
|
|
274
|
+
.command('multioauth xai list', 'List stored xAI OAuth accounts used for automatic rotation')
|
|
275
|
+
.action(async () => {
|
|
276
|
+
const store = await loadXAIAccountStore();
|
|
277
|
+
console.log(`Store: ${xaiAccountsFilePath()}`);
|
|
278
|
+
if (store.accounts.length === 0) {
|
|
279
|
+
console.log('No xAI OAuth accounts configured.');
|
|
280
|
+
process.exit(0);
|
|
281
|
+
}
|
|
282
|
+
store.accounts.forEach((account, index) => {
|
|
283
|
+
const active = index === store.activeIndex ? '*' : ' ';
|
|
284
|
+
console.log(`${active} ${index + 1}. ${xaiAccountLabel(account)}`);
|
|
285
|
+
});
|
|
286
|
+
process.exit(0);
|
|
287
|
+
});
|
|
288
|
+
multioauth
|
|
289
|
+
.command('multioauth xai current', 'Show the current xAI OAuth account being used')
|
|
290
|
+
.action(async () => {
|
|
291
|
+
const current = await getCurrentXAIAccount();
|
|
292
|
+
console.log(`Store: ${xaiAccountsFilePath()}`);
|
|
293
|
+
console.log(`Auth: ${authFilePath()}`);
|
|
294
|
+
if (!current) {
|
|
295
|
+
console.log('No active xAI OAuth account configured.');
|
|
296
|
+
process.exit(0);
|
|
297
|
+
}
|
|
298
|
+
const lines = [];
|
|
299
|
+
lines.push(`Current: ${xaiAccountLabel(current.account || current.auth, current.index)}`);
|
|
300
|
+
if (current.account?.email) {
|
|
301
|
+
lines.push(`Email: ${current.account.email}`);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
lines.push('Email: unavailable');
|
|
305
|
+
}
|
|
306
|
+
if (current.account?.accountId) {
|
|
307
|
+
lines.push(`Account ID: ${current.account.accountId}`);
|
|
308
|
+
}
|
|
309
|
+
if (!current.account) {
|
|
310
|
+
lines.push('Rotation pool entry: not found');
|
|
311
|
+
}
|
|
312
|
+
console.log(lines.join('\n'));
|
|
313
|
+
process.exit(0);
|
|
314
|
+
});
|
|
315
|
+
multioauth
|
|
316
|
+
.command('multioauth xai remove <indexOrEmail>', 'Remove an xAI OAuth account from the rotation pool')
|
|
317
|
+
.action(async (indexOrEmail) => {
|
|
318
|
+
const store = await loadXAIAccountStore();
|
|
319
|
+
const resolvedIndex = resolveAccountIndex(indexOrEmail, store.accounts);
|
|
320
|
+
if (resolvedIndex < 0) {
|
|
321
|
+
console.error('Usage: kimaki multioauth xai remove <index-or-email>');
|
|
322
|
+
process.exit(EXIT_NO_RESTART);
|
|
323
|
+
}
|
|
324
|
+
const removed = store.accounts[resolvedIndex];
|
|
325
|
+
await removeXAIAccount(resolvedIndex);
|
|
326
|
+
console.log(`Removed xAI account ${removed ? xaiAccountLabel(removed, resolvedIndex) : indexOrEmail}`);
|
|
327
|
+
process.exit(0);
|
|
328
|
+
});
|
|
329
|
+
multioauth
|
|
330
|
+
.command('multioauth xai check', 'Test all xAI OAuth accounts for usage limits')
|
|
331
|
+
.action(async () => {
|
|
332
|
+
const store = await loadXAIAccountStore();
|
|
333
|
+
if (store.accounts.length === 0) {
|
|
334
|
+
console.log('No xAI OAuth accounts configured.');
|
|
335
|
+
process.exit(0);
|
|
336
|
+
}
|
|
337
|
+
console.log('Checking usage limits for all xAI accounts...\n');
|
|
338
|
+
for (let i = 0; i < store.accounts.length; i++) {
|
|
339
|
+
const account = store.accounts[i];
|
|
340
|
+
if (!account)
|
|
341
|
+
continue;
|
|
342
|
+
const marker = i === store.activeIndex ? '*' : ' ';
|
|
343
|
+
const label = account.email ?? account.accountId ?? 'unknown';
|
|
344
|
+
process.stdout.write(`${marker} ${i + 1}. ${label}: `);
|
|
345
|
+
// Refresh token if expired
|
|
346
|
+
let accessToken = account.access;
|
|
347
|
+
if (account.expires < Date.now()) {
|
|
348
|
+
try {
|
|
349
|
+
const response = await fetch('https://auth.x.ai/oauth2/token', {
|
|
350
|
+
method: 'POST',
|
|
351
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
352
|
+
body: new URLSearchParams({
|
|
353
|
+
grant_type: 'refresh_token',
|
|
354
|
+
refresh_token: account.refresh,
|
|
355
|
+
client_id: 'b1a00492-073a-47ea-816f-4c329264a828',
|
|
356
|
+
}).toString(),
|
|
357
|
+
});
|
|
358
|
+
if (!response.ok) {
|
|
359
|
+
console.log('ERROR - Token expired, refresh failed');
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
const json = (await response.json());
|
|
363
|
+
accessToken = json.access_token;
|
|
364
|
+
account.access = accessToken;
|
|
365
|
+
// xAI can rotate refresh tokens; store the new one if returned
|
|
366
|
+
if (json.refresh_token)
|
|
367
|
+
account.refresh = json.refresh_token;
|
|
368
|
+
account.expires = Date.now() + (json.expires_in ?? 3600) * 1000;
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
console.log('ERROR - Token expired, refresh failed');
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
try {
|
|
376
|
+
const response = await fetch('https://api.x.ai/v1/chat/completions', {
|
|
377
|
+
method: 'POST',
|
|
378
|
+
headers: {
|
|
379
|
+
'Content-Type': 'application/json',
|
|
380
|
+
Authorization: `Bearer ${accessToken}`,
|
|
381
|
+
},
|
|
382
|
+
body: JSON.stringify({
|
|
383
|
+
model: 'grok-3-mini',
|
|
384
|
+
max_tokens: 1,
|
|
385
|
+
messages: [{ role: 'user', content: 'hi' }],
|
|
386
|
+
}),
|
|
387
|
+
});
|
|
388
|
+
if (response.ok) {
|
|
389
|
+
console.log('OK');
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
const text = await response.text();
|
|
393
|
+
const isLimited = text.includes('usage limit') ||
|
|
394
|
+
text.includes('rate limit') ||
|
|
395
|
+
text.includes('usage_limit') ||
|
|
396
|
+
text.includes('balance exhausted') ||
|
|
397
|
+
response.status === 429 ||
|
|
398
|
+
response.status === 402;
|
|
399
|
+
if (isLimited) {
|
|
400
|
+
let resetInfo;
|
|
401
|
+
try {
|
|
402
|
+
const json = JSON.parse(text);
|
|
403
|
+
const match = json.error?.message?.match(/try again (after|in) ([^.]+)/i);
|
|
404
|
+
if (match)
|
|
405
|
+
resetInfo = match[2];
|
|
406
|
+
}
|
|
407
|
+
catch { }
|
|
408
|
+
console.log(`LIMITED${resetInfo ? ` (resets ${resetInfo})` : ''}`);
|
|
409
|
+
}
|
|
410
|
+
else if (response.status === 401) {
|
|
411
|
+
console.log('ERROR - Auth failed (token invalid)');
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
console.log(`ERROR - ${response.status}: ${text.slice(0, 100)}`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
catch (err) {
|
|
418
|
+
console.log(`ERROR - ${err instanceof Error ? err.message : String(err)}`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
await saveXAIAccountStore(store);
|
|
422
|
+
process.exit(0);
|
|
423
|
+
});
|
|
256
424
|
export default multioauth;
|
package/dist/commands/tasks.js
CHANGED
|
@@ -222,6 +222,12 @@ async function handleRunTaskAction({ interaction, taskId, showAll, }) {
|
|
|
222
222
|
if (result.kind === 'skipped') {
|
|
223
223
|
return `Task #${taskId} is already running or was claimed elsewhere.`;
|
|
224
224
|
}
|
|
225
|
+
if (result.kind === 'condition-not-met') {
|
|
226
|
+
return `Task #${taskId} pre-run condition did not pass.`;
|
|
227
|
+
}
|
|
228
|
+
if (result.kind === 'concurrency-blocked') {
|
|
229
|
+
return `Task #${taskId} still has an active session.`;
|
|
230
|
+
}
|
|
225
231
|
if (result.kind === 'failed') {
|
|
226
232
|
return `Task #${taskId} failed: ${result.error.message}`;
|
|
227
233
|
}
|
|
@@ -8,27 +8,9 @@ import * as schema from '../schema.js';
|
|
|
8
8
|
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
9
9
|
import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js';
|
|
10
10
|
import { getRuntime } from '../session-handler/thread-session-runtime.js';
|
|
11
|
-
import { getCurrentModelInfo } from './model.js';
|
|
11
|
+
import { formatModelSource, getCurrentModelInfo } from './model.js';
|
|
12
12
|
import { createLogger, LogPrefix } from '../logger.js';
|
|
13
13
|
const unsetModelLogger = createLogger(LogPrefix.MODEL);
|
|
14
|
-
function formatModelSource(type, agentName) {
|
|
15
|
-
switch (type) {
|
|
16
|
-
case 'session':
|
|
17
|
-
return 'session override';
|
|
18
|
-
case 'agent':
|
|
19
|
-
return `agent "${agentName}"`;
|
|
20
|
-
case 'channel':
|
|
21
|
-
return 'channel override';
|
|
22
|
-
case 'global':
|
|
23
|
-
return 'global default';
|
|
24
|
-
case 'opencode-config':
|
|
25
|
-
case 'opencode-recent':
|
|
26
|
-
case 'opencode-provider-default':
|
|
27
|
-
return 'opencode default';
|
|
28
|
-
default:
|
|
29
|
-
return 'none';
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
14
|
/**
|
|
33
15
|
* Clear the nearest model override for the current channel/thread.
|
|
34
16
|
* In thread: clears session override if exists, otherwise channel override.
|
|
@@ -110,7 +92,10 @@ export async function clearModelOverride({ channel, appId, editReply, }) {
|
|
|
110
92
|
newModelText =
|
|
111
93
|
newModelInfo.type === 'none'
|
|
112
94
|
? 'none'
|
|
113
|
-
: `\`${newModelInfo.model}\` (${formatModelSource(
|
|
95
|
+
: `\`${newModelInfo.model}\` (${formatModelSource({
|
|
96
|
+
type: newModelInfo.type,
|
|
97
|
+
agentName,
|
|
98
|
+
})})`;
|
|
114
99
|
}
|
|
115
100
|
// Check if there's a running request and abort+retry with new model (only for session changes in threads)
|
|
116
101
|
let retried = false;
|
package/dist/database.js
CHANGED
|
@@ -95,6 +95,71 @@ export async function claimScheduledTaskRunning({ taskId, startedAt }) {
|
|
|
95
95
|
.returning({ id: schema.scheduled_tasks.id });
|
|
96
96
|
return countRows(rows) > 0;
|
|
97
97
|
}
|
|
98
|
+
export async function releaseScheduledTaskClaim(taskId) {
|
|
99
|
+
const db = await getDb();
|
|
100
|
+
await db.update(schema.scheduled_tasks)
|
|
101
|
+
.set({ status: 'planned', running_started_at: null })
|
|
102
|
+
.where(orm.and(orm.eq(schema.scheduled_tasks.id, taskId), orm.eq(schema.scheduled_tasks.status, 'running')));
|
|
103
|
+
}
|
|
104
|
+
export async function createScheduledTaskRun({ taskId, startedAt, }) {
|
|
105
|
+
const db = await getDb();
|
|
106
|
+
const [run] = await db.insert(schema.scheduled_task_runs)
|
|
107
|
+
.values({ scheduled_task_id: taskId, started_at: startedAt })
|
|
108
|
+
.returning({ id: schema.scheduled_task_runs.id });
|
|
109
|
+
if (!run)
|
|
110
|
+
throw new Error(`Failed to create run for scheduled task ${taskId}`);
|
|
111
|
+
return run.id;
|
|
112
|
+
}
|
|
113
|
+
export async function getActiveScheduledTaskRuns(taskId) {
|
|
114
|
+
const db = await getDb();
|
|
115
|
+
return db.query.scheduled_task_runs.findMany({
|
|
116
|
+
where: {
|
|
117
|
+
scheduled_task_id: taskId,
|
|
118
|
+
status: { in: ['pending', 'running'] },
|
|
119
|
+
},
|
|
120
|
+
orderBy: { started_at: 'desc' },
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
export async function setScheduledTaskRunThread({ runId, threadId, }) {
|
|
124
|
+
const db = await getDb();
|
|
125
|
+
await db.update(schema.scheduled_task_runs)
|
|
126
|
+
.set({ thread_id: threadId })
|
|
127
|
+
.where(orm.eq(schema.scheduled_task_runs.id, runId));
|
|
128
|
+
}
|
|
129
|
+
export async function startScheduledTaskRunSession({ runId, sessionId, projectDirectory, }) {
|
|
130
|
+
const db = await getDb();
|
|
131
|
+
await db.update(schema.scheduled_task_runs)
|
|
132
|
+
.set({ status: 'running', session_id: sessionId, project_directory: projectDirectory })
|
|
133
|
+
.where(orm.and(orm.eq(schema.scheduled_task_runs.id, runId), orm.eq(schema.scheduled_task_runs.status, 'pending')));
|
|
134
|
+
}
|
|
135
|
+
export async function completeScheduledTaskRunsForSession(sessionId) {
|
|
136
|
+
const db = await getDb();
|
|
137
|
+
const rows = await db.update(schema.scheduled_task_runs)
|
|
138
|
+
.set({ status: 'completed', completed_at: new Date() })
|
|
139
|
+
.where(orm.and(orm.eq(schema.scheduled_task_runs.session_id, sessionId), orm.inArray(schema.scheduled_task_runs.status, ['pending', 'running'])))
|
|
140
|
+
.returning({ id: schema.scheduled_task_runs.id });
|
|
141
|
+
return rows.length;
|
|
142
|
+
}
|
|
143
|
+
export async function failScheduledTaskRunsForSession({ sessionId, error, }) {
|
|
144
|
+
const db = await getDb();
|
|
145
|
+
const rows = await db.update(schema.scheduled_task_runs)
|
|
146
|
+
.set({ status: 'failed', completed_at: new Date(), error })
|
|
147
|
+
.where(orm.and(orm.eq(schema.scheduled_task_runs.session_id, sessionId), orm.inArray(schema.scheduled_task_runs.status, ['pending', 'running'])))
|
|
148
|
+
.returning({ id: schema.scheduled_task_runs.id });
|
|
149
|
+
return rows.length;
|
|
150
|
+
}
|
|
151
|
+
export async function failScheduledTaskRun({ runId, error, }) {
|
|
152
|
+
const db = await getDb();
|
|
153
|
+
await db.update(schema.scheduled_task_runs)
|
|
154
|
+
.set({ status: 'failed', completed_at: new Date(), error })
|
|
155
|
+
.where(orm.eq(schema.scheduled_task_runs.id, runId));
|
|
156
|
+
}
|
|
157
|
+
export async function finishScheduledTaskRun({ runId, status, }) {
|
|
158
|
+
const db = await getDb();
|
|
159
|
+
await db.update(schema.scheduled_task_runs)
|
|
160
|
+
.set({ status, completed_at: new Date() })
|
|
161
|
+
.where(orm.eq(schema.scheduled_task_runs.id, runId));
|
|
162
|
+
}
|
|
98
163
|
export async function recoverStaleRunningScheduledTasks({ staleBefore }) {
|
|
99
164
|
const db = await getDb();
|
|
100
165
|
const rows = await db.update(schema.scheduled_tasks)
|
package/dist/discord-bot.js
CHANGED
|
@@ -142,11 +142,15 @@ function parseSessionStartSourceFromMarker(marker) {
|
|
|
142
142
|
if (typeof marker.scheduledTaskId !== 'number' ||
|
|
143
143
|
!Number.isInteger(marker.scheduledTaskId) ||
|
|
144
144
|
marker.scheduledTaskId < 1) {
|
|
145
|
-
return {
|
|
145
|
+
return {
|
|
146
|
+
scheduleKind: marker.scheduledKind,
|
|
147
|
+
scheduledTaskRunId: marker.scheduledTaskRunId,
|
|
148
|
+
};
|
|
146
149
|
}
|
|
147
150
|
return {
|
|
148
151
|
scheduleKind: marker.scheduledKind,
|
|
149
152
|
scheduledTaskId: marker.scheduledTaskId,
|
|
153
|
+
scheduledTaskRunId: marker.scheduledTaskRunId,
|
|
150
154
|
};
|
|
151
155
|
}
|
|
152
156
|
export async function createDiscordClient() {
|
|
@@ -632,6 +636,7 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
|
|
|
632
636
|
? {
|
|
633
637
|
scheduleKind: sessionStartSource.scheduleKind,
|
|
634
638
|
scheduledTaskId: sessionStartSource.scheduledTaskId,
|
|
639
|
+
scheduledTaskRunId: sessionStartSource.scheduledTaskRunId,
|
|
635
640
|
}
|
|
636
641
|
: undefined,
|
|
637
642
|
preprocess: () => {
|
|
@@ -1085,6 +1090,7 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
|
|
|
1085
1090
|
? {
|
|
1086
1091
|
scheduleKind: botThreadStartSource.scheduleKind,
|
|
1087
1092
|
scheduledTaskId: botThreadStartSource.scheduledTaskId,
|
|
1093
|
+
scheduledTaskRunId: botThreadStartSource.scheduledTaskRunId,
|
|
1088
1094
|
}
|
|
1089
1095
|
: undefined,
|
|
1090
1096
|
preprocess: async () => {
|
package/dist/ipc-tools-plugin.js
CHANGED
|
@@ -56,7 +56,7 @@ const ipcToolsPlugin = async () => {
|
|
|
56
56
|
'The user sees a button, clicks it, and gets a file upload dialog. ' +
|
|
57
57
|
'Returns the local file paths of downloaded files in the project directory. ' +
|
|
58
58
|
'Use this when you need the user to provide files (images, documents, configs, etc.). ' +
|
|
59
|
-
'
|
|
59
|
+
'You MUST call kimaki_file_upload LAST, after ALL text. NEVER call it before your text.',
|
|
60
60
|
args: {
|
|
61
61
|
prompt: z
|
|
62
62
|
.string()
|
|
@@ -115,8 +115,9 @@ const ipcToolsPlugin = async () => {
|
|
|
115
115
|
Use this when the user can respond by clicking one of up to 3 buttons.
|
|
116
116
|
Prefer a single button whenever possible.
|
|
117
117
|
Default color is white (same visual style as permission deny button).
|
|
118
|
-
If you need more than 3 options, use
|
|
119
|
-
|
|
118
|
+
If you need more than 3 options, use \`question\` instead.
|
|
119
|
+
You MUST call kimaki_action_buttons LAST, after ALL text.
|
|
120
|
+
NEVER call kimaki_action_buttons before your text.
|
|
120
121
|
|
|
121
122
|
Examples:
|
|
122
123
|
- buttons: [{"label":"Yes, proceed"}]
|
|
@@ -15,6 +15,8 @@ export { contextAwarenessPlugin } from './context-awareness-plugin.js';
|
|
|
15
15
|
export { memoryOverviewPlugin } from './memory-overview-plugin.js';
|
|
16
16
|
export { interruptOpencodeSessionOnUserMessage } from './opencode-interrupt-plugin.js';
|
|
17
17
|
export { anthropicAuthPlugin } from './anthropic-auth-plugin.js';
|
|
18
|
+
export { openaiRotationPlugin } from './openai-auth-plugin.js';
|
|
19
|
+
export { xaiRotationPlugin } from './xai-auth-plugin.js';
|
|
18
20
|
export { imageOptimizerPlugin } from './image-optimizer-plugin.js';
|
|
19
21
|
export { subagentRateLimitPlugin } from './subagent-rate-limit-plugin.js';
|
|
20
22
|
export { cacheDriftPlugin } from './cache-drift-plugin.js';
|