kimaki 0.14.0 → 0.16.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/cli-commands/send.js +46 -15
- package/dist/cli-commands/session.js +53 -11
- package/dist/cli-runner.js +29 -3
- package/dist/commands/abort.js +9 -6
- package/dist/commands/add-dir.js +1 -1
- package/dist/commands/btw.js +27 -16
- package/dist/commands/compact.js +1 -1
- package/dist/commands/context-usage.js +16 -9
- package/dist/commands/fork.js +9 -11
- package/dist/commands/login.js +2 -9
- package/dist/commands/new-worktree.js +116 -129
- package/dist/commands/permissions.js +10 -6
- package/dist/commands/remove-project.js +5 -9
- package/dist/commands/undo-redo.js +17 -9
- package/dist/context-awareness-plugin.js +135 -147
- package/dist/discord-bot.js +38 -16
- package/dist/discord-command-registration.js +8 -0
- package/dist/discord-utils.js +23 -36
- package/dist/errors.js +48 -30
- package/dist/external-opencode-sync.js +2 -4
- package/dist/forum-sync/markdown.js +1 -4
- package/dist/genai-worker.js +3 -5
- package/dist/hrana-server.js +5 -8
- package/dist/html-components.js +2 -4
- package/dist/ipc-polling.js +11 -18
- package/dist/markdown.js +95 -100
- package/dist/memory-overview-plugin.js +45 -50
- package/dist/message-formatting.js +4 -9
- package/dist/message-preprocessing.js +5 -6
- package/dist/openai-auth-plugin.js +1 -0
- package/dist/opencode.js +50 -62
- package/dist/plugin-logger.js +37 -0
- package/dist/plugin-opencode-client.js +11 -0
- package/dist/session-handler/agent-utils.js +3 -4
- package/dist/session-handler/global-event-listener.js +8 -7
- package/dist/session-handler/model-utils.js +7 -10
- package/dist/session-handler/opencode-session-event-log.js +5 -7
- package/dist/session-handler/thread-session-runtime.js +163 -229
- package/dist/skill-filter.js +12 -0
- package/dist/skill-filter.test.js +22 -1
- package/dist/subagent-rate-limit-plugin.js +18 -20
- package/dist/system-message.js +10 -1
- package/dist/system-message.test.js +10 -1
- package/dist/task-runner.js +4 -8
- package/dist/task-schedule.js +21 -34
- package/dist/thread-message-queue.e2e.test.js +3 -0
- package/dist/voice-handler.js +10 -17
- package/dist/voice.js +8 -13
- package/dist/worktrees.js +40 -76
- package/package.json +8 -8
- package/skills/holocron/SKILL.md +8 -0
- package/src/cli-commands/send.ts +50 -15
- package/src/cli-commands/session.ts +66 -7
- package/src/cli-runner.ts +32 -2
- package/src/commands/abort.ts +9 -6
- package/src/commands/add-dir.ts +1 -1
- package/src/commands/btw.ts +34 -24
- package/src/commands/compact.ts +1 -1
- package/src/commands/context-usage.ts +18 -9
- package/src/commands/fork.ts +7 -6
- package/src/commands/login.ts +2 -8
- package/src/commands/new-worktree.ts +65 -85
- package/src/commands/permissions.ts +9 -8
- package/src/commands/remove-project.ts +6 -9
- package/src/commands/undo-redo.ts +17 -9
- package/src/context-awareness-plugin.ts +25 -38
- package/src/discord-bot.ts +46 -18
- package/src/discord-command-registration.ts +11 -0
- package/src/discord-utils.ts +16 -29
- package/src/errors.ts +49 -30
- package/src/external-opencode-sync.ts +2 -6
- package/src/forum-sync/markdown.ts +4 -4
- package/src/genai-worker.ts +4 -5
- package/src/hrana-server.ts +4 -4
- package/src/html-components.ts +2 -6
- package/src/ipc-polling.ts +9 -10
- package/src/markdown.ts +103 -110
- package/src/memory-overview-plugin.ts +9 -13
- package/src/message-formatting.ts +5 -9
- package/src/message-preprocessing.ts +5 -6
- package/src/openai-auth-plugin.ts +1 -0
- package/src/opencode.ts +34 -38
- package/src/plugin-logger.ts +55 -0
- package/src/plugin-opencode-client.ts +11 -0
- package/src/queue-advanced-e2e-setup.ts +3 -0
- package/src/session-handler/agent-utils.ts +4 -4
- package/src/session-handler/global-event-listener.ts +9 -7
- package/src/session-handler/model-utils.ts +7 -12
- package/src/session-handler/opencode-session-event-log.ts +6 -7
- package/src/session-handler/thread-session-runtime.ts +173 -232
- package/src/skill-filter.test.ts +28 -1
- package/src/skill-filter.ts +21 -0
- package/src/subagent-rate-limit-plugin.ts +6 -7
- package/src/system-message.test.ts +10 -1
- package/src/system-message.ts +10 -1
- package/src/task-runner.ts +4 -12
- package/src/task-schedule.ts +16 -24
- package/src/thread-message-queue.e2e.test.ts +4 -0
- package/src/voice-handler.ts +9 -16
- package/src/voice.ts +7 -12
- package/src/worktrees.ts +52 -106
|
@@ -138,12 +138,6 @@ cli
|
|
|
138
138
|
if (options.user) {
|
|
139
139
|
incompatibleFlags.push('--user');
|
|
140
140
|
}
|
|
141
|
-
if (!sendAt && options.agent) {
|
|
142
|
-
incompatibleFlags.push('--agent');
|
|
143
|
-
}
|
|
144
|
-
if (!sendAt && options.model) {
|
|
145
|
-
incompatibleFlags.push('--model');
|
|
146
|
-
}
|
|
147
141
|
if (incompatibleFlags.length > 0) {
|
|
148
142
|
cliLogger.error(`Incompatible options with --thread/--session: ${incompatibleFlags.join(', ')}`);
|
|
149
143
|
process.exit(EXIT_NO_RESTART);
|
|
@@ -313,6 +307,8 @@ cli
|
|
|
313
307
|
}
|
|
314
308
|
const threadPromptMarker = {
|
|
315
309
|
start: true,
|
|
310
|
+
...(options.agent && { agent: options.agent }),
|
|
311
|
+
...(options.model && { model: options.model }),
|
|
316
312
|
...(options.permission?.length ? { permissions: options.permission } : {}),
|
|
317
313
|
...(options.injectionGuard?.length ? { injectionGuardPatterns: options.injectionGuard } : {}),
|
|
318
314
|
};
|
|
@@ -334,8 +330,12 @@ cli
|
|
|
334
330
|
rest,
|
|
335
331
|
});
|
|
336
332
|
const threadUrl = `https://discord.com/channels/${threadData.guild_id}/${threadData.id}`;
|
|
337
|
-
|
|
338
|
-
|
|
333
|
+
const existingSessionId = sessionId || await getThreadSession(targetThreadId);
|
|
334
|
+
const sessionLine = existingSessionId ? `Session: ${existingSessionId}\n` : '';
|
|
335
|
+
note(`Prompt sent to thread: ${threadData.name}\n${sessionLine}\nURL: ${threadUrl}`, '✅ Message Sent');
|
|
336
|
+
if (existingSessionId)
|
|
337
|
+
process.stdout.write(`Session: ${existingSessionId}\n`);
|
|
338
|
+
process.stdout.write(`${threadUrl}\n`);
|
|
339
339
|
if (options.wait) {
|
|
340
340
|
const { waitAndOutputSession } = await import('../wait-session.js');
|
|
341
341
|
await waitAndOutputSession({
|
|
@@ -353,16 +353,18 @@ cli
|
|
|
353
353
|
// Get channel info to extract directory from topic
|
|
354
354
|
const channelData = (await rest.get(Routes.channel(channelId)));
|
|
355
355
|
const channelConfig = await getChannelDirectory(channelData.id);
|
|
356
|
-
if (!channelConfig) {
|
|
356
|
+
if (!channelConfig && !notifyOnly) {
|
|
357
357
|
cliLogger.log('Channel not configured');
|
|
358
358
|
throw new Error(`Channel #${channelData.name} is not configured with a project directory. Run the bot first to sync channel data.`);
|
|
359
359
|
}
|
|
360
|
-
const projectDirectory = channelConfig
|
|
360
|
+
const projectDirectory = channelConfig?.directory;
|
|
361
361
|
// Validate --cwd is inside the project or an existing git worktree.
|
|
362
362
|
let resolvedCwd;
|
|
363
363
|
if (options.cwd) {
|
|
364
|
+
// projectDirectory is guaranteed here: --cwd is incompatible with --notify-only,
|
|
365
|
+
// and non-notify sends already require channelConfig above.
|
|
364
366
|
const cwdResult = await resolveSessionWorkingDirectory({
|
|
365
|
-
projectDirectory,
|
|
367
|
+
projectDirectory: projectDirectory,
|
|
366
368
|
candidatePath: options.cwd,
|
|
367
369
|
});
|
|
368
370
|
if (cwdResult instanceof Error) {
|
|
@@ -455,7 +457,16 @@ cli
|
|
|
455
457
|
botToken,
|
|
456
458
|
embeds: autoStartEmbed,
|
|
457
459
|
rest,
|
|
460
|
+
splitInsteadOfAttach: notifyOnly,
|
|
458
461
|
});
|
|
462
|
+
// For notify-only on non-project channels, just post the message without
|
|
463
|
+
// creating a thread. There's no session to start, so a thread is unnecessary.
|
|
464
|
+
if (notifyOnly && !channelConfig) {
|
|
465
|
+
const messageUrl = `https://discord.com/channels/${channelData.guild_id}/${channelId}/${starterMessage.id}`;
|
|
466
|
+
note(`Channel: #${channelData.name}\n\nMessage sent.\n\nURL: ${messageUrl}`, '✅ Message Sent');
|
|
467
|
+
process.stdout.write(`${messageUrl}\n`);
|
|
468
|
+
process.exit(0);
|
|
469
|
+
}
|
|
459
470
|
cliLogger.log('Creating thread...');
|
|
460
471
|
const threadData = (await rest.post(Routes.threads(channelId, starterMessage.id), {
|
|
461
472
|
body: {
|
|
@@ -470,21 +481,41 @@ cli
|
|
|
470
481
|
await rest.put(Routes.threadMembers(threadData.id, resolvedUser.id));
|
|
471
482
|
}
|
|
472
483
|
const threadUrl = `https://discord.com/channels/${channelData.guild_id}/${threadData.id}`;
|
|
484
|
+
// Poll for session ID if the bot is expected to auto-start (not --notify-only).
|
|
485
|
+
// The bot picks up the thread and creates a session asynchronously;
|
|
486
|
+
// we wait briefly so the caller can reference the session immediately.
|
|
487
|
+
let newSessionId;
|
|
488
|
+
if (!notifyOnly) {
|
|
489
|
+
const { waitForSessionId } = await import('../wait-session.js');
|
|
490
|
+
newSessionId = await waitForSessionId({
|
|
491
|
+
threadId: threadData.id,
|
|
492
|
+
timeoutMs: 15_000,
|
|
493
|
+
}).catch((e) => {
|
|
494
|
+
cliLogger.warn(`Could not resolve session ID: ${e instanceof Error ? e.message : String(e)}`);
|
|
495
|
+
return undefined;
|
|
496
|
+
});
|
|
497
|
+
}
|
|
473
498
|
const worktreeNote = worktreeName
|
|
474
499
|
? `\nWorktree: ${worktreeName} (will be created by bot)`
|
|
475
500
|
: resolvedCwd
|
|
476
501
|
? `\nWorking directory: ${resolvedCwd}`
|
|
477
502
|
: '';
|
|
503
|
+
const sessionLine = newSessionId ? `\nSession: ${newSessionId}` : '';
|
|
504
|
+
const directoryLine = projectDirectory ? `\nDirectory: ${projectDirectory}` : '';
|
|
478
505
|
const successMessage = notifyOnly
|
|
479
|
-
? `Thread: ${threadData.name}
|
|
480
|
-
: `Thread: ${threadData.name}
|
|
506
|
+
? `Thread: ${threadData.name}${directoryLine}\n\nNotification created. Reply to start a session.\n\nURL: ${threadUrl}`
|
|
507
|
+
: `Thread: ${threadData.name}${directoryLine}${worktreeNote}${sessionLine}\n\nThe running bot will pick this up and start the session.\n\nURL: ${threadUrl}`;
|
|
481
508
|
note(successMessage, '✅ Thread Created');
|
|
482
|
-
|
|
509
|
+
if (newSessionId)
|
|
510
|
+
process.stdout.write(`Session: ${newSessionId}\n`);
|
|
511
|
+
process.stdout.write(`${threadUrl}\n`);
|
|
483
512
|
if (options.wait) {
|
|
513
|
+
// projectDirectory is guaranteed here: --wait is incompatible with --notify-only,
|
|
514
|
+
// and non-notify sends already require channelConfig above.
|
|
484
515
|
const { waitAndOutputSession } = await import('../wait-session.js');
|
|
485
516
|
await waitAndOutputSession({
|
|
486
517
|
threadId: threadData.id,
|
|
487
|
-
projectDirectory,
|
|
518
|
+
projectDirectory: projectDirectory,
|
|
488
519
|
waitStartedAtMs,
|
|
489
520
|
});
|
|
490
521
|
}
|
|
@@ -166,9 +166,8 @@ cli
|
|
|
166
166
|
const dir = project.worktree;
|
|
167
167
|
cliLogger.log(`Trying project: ${dir}`);
|
|
168
168
|
const otherClient = await initializeOpencodeForDirectory(dir);
|
|
169
|
-
if (otherClient instanceof Error)
|
|
169
|
+
if (otherClient instanceof Error)
|
|
170
170
|
continue;
|
|
171
|
-
}
|
|
172
171
|
const otherMarkdown = new ShareMarkdown(otherClient());
|
|
173
172
|
const otherResult = await otherMarkdown.generate({
|
|
174
173
|
sessionID: sessionId,
|
|
@@ -387,15 +386,12 @@ cli
|
|
|
387
386
|
process.exit(EXIT_NO_RESTART);
|
|
388
387
|
}
|
|
389
388
|
const parsedRows = rows.flatMap((row) => {
|
|
390
|
-
const parsed = errore.try({
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
cause: error,
|
|
397
|
-
});
|
|
398
|
-
},
|
|
389
|
+
const parsed = errore.try(() => {
|
|
390
|
+
return JSON.parse(row.event_json);
|
|
391
|
+
}, (error) => {
|
|
392
|
+
return new Error('Failed to parse persisted event JSON', {
|
|
393
|
+
cause: error,
|
|
394
|
+
});
|
|
399
395
|
});
|
|
400
396
|
if (parsed instanceof Error) {
|
|
401
397
|
cliLogger.warn(`Skipping invalid persisted event row ${row.id}: ${parsed.message}`);
|
|
@@ -499,6 +495,52 @@ cli
|
|
|
499
495
|
process.exit(EXIT_NO_RESTART);
|
|
500
496
|
}
|
|
501
497
|
});
|
|
498
|
+
cli
|
|
499
|
+
.command('session abort <sessionId>', 'Abort a running session without archiving the thread')
|
|
500
|
+
.action(async (sessionId) => {
|
|
501
|
+
try {
|
|
502
|
+
await initDatabase();
|
|
503
|
+
const { token: botToken } = await resolveBotCredentials();
|
|
504
|
+
const rest = createDiscordRest(botToken);
|
|
505
|
+
// Try to resolve the project directory for the OpenCode abort call
|
|
506
|
+
const directory = await resolveSessionDirectoryFromDatabase({ sessionId });
|
|
507
|
+
if (directory instanceof Error) {
|
|
508
|
+
cliLogger.error(directory.message);
|
|
509
|
+
process.exit(EXIT_NO_RESTART);
|
|
510
|
+
}
|
|
511
|
+
const serverResult = await initializeOpencodeForDirectory(directory);
|
|
512
|
+
if (serverResult instanceof Error) {
|
|
513
|
+
cliLogger.error(`Failed to initialize OpenCode: ${serverResult.message}`);
|
|
514
|
+
process.exit(EXIT_NO_RESTART);
|
|
515
|
+
}
|
|
516
|
+
const client = serverResult();
|
|
517
|
+
// Don't pass directory — the server resolves sessions by ID regardless
|
|
518
|
+
// of the x-opencode-directory header, matching archiveThread's pattern.
|
|
519
|
+
// This avoids issues when --cwd was used (session directory != project directory).
|
|
520
|
+
const abortResult = await client.session.abort({
|
|
521
|
+
sessionID: sessionId,
|
|
522
|
+
}).catch((e) => new Error('Failed to abort session', { cause: e }));
|
|
523
|
+
if (abortResult instanceof Error) {
|
|
524
|
+
cliLogger.error(abortResult.message);
|
|
525
|
+
process.exit(EXIT_NO_RESTART);
|
|
526
|
+
}
|
|
527
|
+
// Post a message in the Discord thread so it's clear why the session stopped
|
|
528
|
+
const threadId = await getThreadIdBySessionId(sessionId);
|
|
529
|
+
if (threadId) {
|
|
530
|
+
await rest.post(Routes.channelMessages(threadId), {
|
|
531
|
+
body: { content: 'Session aborted via CLI' },
|
|
532
|
+
}).catch((e) => {
|
|
533
|
+
cliLogger.warn(`Could not post abort message to thread: ${e instanceof Error ? e.message : String(e)}`);
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
note(`Aborted session: ${sessionId}${threadId ? `\nThread ID: ${threadId}` : ''}`, '✅ Aborted');
|
|
537
|
+
process.exit(0);
|
|
538
|
+
}
|
|
539
|
+
catch (error) {
|
|
540
|
+
cliLogger.error('Error:', error instanceof Error ? error.stack : String(error));
|
|
541
|
+
process.exit(EXIT_NO_RESTART);
|
|
542
|
+
}
|
|
543
|
+
});
|
|
502
544
|
cli
|
|
503
545
|
.command('session discord-url <sessionId>', 'Print the Discord thread URL for a session')
|
|
504
546
|
.option('--json', 'Output as JSON')
|
package/dist/cli-runner.js
CHANGED
|
@@ -107,7 +107,7 @@ export function isThreadChannelType(type) {
|
|
|
107
107
|
ChannelType.AnnouncementThread,
|
|
108
108
|
].includes(type);
|
|
109
109
|
}
|
|
110
|
-
export async function sendDiscordMessageWithOptionalAttachment({ channelId, prompt, botToken, embeds, rest, }) {
|
|
110
|
+
export async function sendDiscordMessageWithOptionalAttachment({ channelId, prompt, botToken, embeds, rest, splitInsteadOfAttach, }) {
|
|
111
111
|
const discordMaxLength = 2000;
|
|
112
112
|
if (prompt.length <= discordMaxLength) {
|
|
113
113
|
return (await rest.post(Routes.channelMessages(channelId), {
|
|
@@ -118,6 +118,33 @@ export async function sendDiscordMessageWithOptionalAttachment({ channelId, prom
|
|
|
118
118
|
},
|
|
119
119
|
}));
|
|
120
120
|
}
|
|
121
|
+
if (splitInsteadOfAttach) {
|
|
122
|
+
const { splitMarkdownForDiscord } = await import('./discord-utils.js');
|
|
123
|
+
const chunks = splitMarkdownForDiscord({
|
|
124
|
+
content: prompt,
|
|
125
|
+
maxLength: discordMaxLength,
|
|
126
|
+
});
|
|
127
|
+
let firstMessage;
|
|
128
|
+
for (let chunk of chunks) {
|
|
129
|
+
if (!chunk?.trim())
|
|
130
|
+
continue;
|
|
131
|
+
// Safety net: hard-truncate if splitting still produced an oversized chunk
|
|
132
|
+
if (chunk.length > discordMaxLength) {
|
|
133
|
+
chunk = chunk.slice(0, discordMaxLength - 4) + '...';
|
|
134
|
+
}
|
|
135
|
+
const message = (await rest.post(Routes.channelMessages(channelId), {
|
|
136
|
+
body: {
|
|
137
|
+
content: chunk,
|
|
138
|
+
// Only attach embeds to the first message
|
|
139
|
+
...(firstMessage ? {} : { embeds }),
|
|
140
|
+
allowed_mentions: { parse: store.getState().allowedMentions },
|
|
141
|
+
},
|
|
142
|
+
}));
|
|
143
|
+
if (!firstMessage)
|
|
144
|
+
firstMessage = message;
|
|
145
|
+
}
|
|
146
|
+
return firstMessage;
|
|
147
|
+
}
|
|
121
148
|
const preview = prompt.slice(0, 100).replace(/\n/g, ' ');
|
|
122
149
|
const summaryContent = `Prompt attached as file (${prompt.length} chars)\n\n> ${preview}...`;
|
|
123
150
|
const tmpDir = path.join(process.cwd(), 'tmp');
|
|
@@ -759,8 +786,7 @@ export async function resolveCredentials({ forceRestartOnboarding, forceGateway,
|
|
|
759
786
|
options: [
|
|
760
787
|
{
|
|
761
788
|
value: 'gateway',
|
|
762
|
-
|
|
763
|
-
label: 'Gateway (pre-built Kimaki bot, currently disabled because of Discord verification process. will be re-enabled soon)',
|
|
789
|
+
label: 'Gateway (pre-built Kimaki bot, no setup needed)',
|
|
764
790
|
},
|
|
765
791
|
{
|
|
766
792
|
value: 'self_hosted',
|
package/dist/commands/abort.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// /abort command - Abort the current OpenCode request in this thread.
|
|
2
2
|
import { ChannelType, MessageFlags, } from 'discord.js';
|
|
3
3
|
import { getThreadSession } from '../database.js';
|
|
4
|
-
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
4
|
+
import { getOpencodeClient, initializeOpencodeForDirectory } from '../opencode.js';
|
|
5
5
|
import { resolveWorkingDirectory, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
|
|
6
6
|
import { getRuntime } from '../session-handler/thread-session-runtime.js';
|
|
7
7
|
import { createLogger, LogPrefix } from '../logger.js';
|
|
@@ -35,7 +35,7 @@ export async function handleAbortCommand({ command, }) {
|
|
|
35
35
|
await command.editReply('Could not determine project directory for this channel');
|
|
36
36
|
return;
|
|
37
37
|
}
|
|
38
|
-
const { projectDirectory } = resolved;
|
|
38
|
+
const { projectDirectory, workingDirectory } = resolved;
|
|
39
39
|
const sessionId = await getThreadSession(channel.id);
|
|
40
40
|
if (!sessionId) {
|
|
41
41
|
await command.editReply('No active session in this thread');
|
|
@@ -48,13 +48,16 @@ export async function handleAbortCommand({ command, }) {
|
|
|
48
48
|
}
|
|
49
49
|
else {
|
|
50
50
|
// No runtime but session exists — fall back to direct API abort
|
|
51
|
-
const
|
|
52
|
-
if (
|
|
53
|
-
await command.editReply(`Failed to abort: ${
|
|
51
|
+
const serverResult = await initializeOpencodeForDirectory(projectDirectory);
|
|
52
|
+
if (serverResult instanceof Error) {
|
|
53
|
+
await command.editReply(`Failed to abort: ${serverResult.message}`);
|
|
54
54
|
return;
|
|
55
55
|
}
|
|
56
56
|
try {
|
|
57
|
-
|
|
57
|
+
const client = getOpencodeClient(workingDirectory);
|
|
58
|
+
if (client) {
|
|
59
|
+
await client.session.abort({ sessionID: sessionId, directory: workingDirectory });
|
|
60
|
+
}
|
|
58
61
|
}
|
|
59
62
|
catch (error) {
|
|
60
63
|
logger.error('[ABORT] API abort failed:', error);
|
package/dist/commands/add-dir.js
CHANGED
|
@@ -132,7 +132,7 @@ export async function handleAddDirCommand({ command, }) {
|
|
|
132
132
|
await command.editReply(`Failed to update session permissions: ${getClient.message}`);
|
|
133
133
|
return;
|
|
134
134
|
}
|
|
135
|
-
const client = getOpencodeClient(resolvedDirectories.
|
|
135
|
+
const client = getOpencodeClient(resolvedDirectories.workingDirectory);
|
|
136
136
|
if (!client) {
|
|
137
137
|
await command.editReply('Failed to get OpenCode client');
|
|
138
138
|
return;
|
package/dist/commands/btw.js
CHANGED
|
@@ -10,41 +10,52 @@ import { createLogger, LogPrefix } from '../logger.js';
|
|
|
10
10
|
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
11
11
|
const logger = createLogger(LogPrefix.FORK);
|
|
12
12
|
export async function forkSessionToBtwThread({ sourceThread, projectDirectory, prompt, userId, username, appId, }) {
|
|
13
|
-
|
|
13
|
+
// Parallelize: session lookup + opencode init + parent channel resolve are independent
|
|
14
|
+
const [sessionId, getClientResult, textChannel] = await Promise.all([
|
|
15
|
+
getThreadSession(sourceThread.id),
|
|
16
|
+
initializeOpencodeForDirectory(projectDirectory),
|
|
17
|
+
resolveTextChannel(sourceThread),
|
|
18
|
+
]);
|
|
14
19
|
if (!sessionId) {
|
|
15
20
|
return new Error('No active session in this thread');
|
|
16
21
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
cause: getClient,
|
|
22
|
+
if (getClientResult instanceof Error) {
|
|
23
|
+
return new Error(`Failed to fork session: ${getClientResult.message}`, {
|
|
24
|
+
cause: getClientResult,
|
|
21
25
|
});
|
|
22
26
|
}
|
|
23
|
-
const forkResponse = await getClient().session.fork({
|
|
24
|
-
sessionID: sessionId,
|
|
25
|
-
});
|
|
26
|
-
if (!forkResponse.data) {
|
|
27
|
-
return new Error('Failed to fork session');
|
|
28
|
-
}
|
|
29
|
-
const textChannel = await resolveTextChannel(sourceThread);
|
|
30
27
|
if (!textChannel) {
|
|
31
28
|
return new Error('Could not resolve parent text channel');
|
|
32
29
|
}
|
|
30
|
+
// Fork must succeed before creating the Discord thread to avoid orphan threads
|
|
31
|
+
const forkResponse = await getClientResult().session.fork({ sessionID: sessionId });
|
|
32
|
+
if (!forkResponse.data) {
|
|
33
|
+
return new Error('Failed to fork session');
|
|
34
|
+
}
|
|
33
35
|
const forkedSession = forkResponse.data;
|
|
34
36
|
const thread = await textChannel.threads.create({
|
|
35
37
|
name: `btw: ${prompt}`.slice(0, 100),
|
|
36
38
|
autoArchiveDuration: ThreadAutoArchiveDuration.OneDay,
|
|
37
39
|
reason: `btw fork from session ${sessionId}`,
|
|
38
40
|
});
|
|
41
|
+
// DB mapping must complete before user-visible actions so the thread is routable
|
|
39
42
|
await setThreadSession(thread.id, forkedSession.id);
|
|
40
|
-
|
|
41
|
-
logger.log(`Created btw fork session ${forkedSession.id} in thread ${thread.id} from ${sessionId}`);
|
|
43
|
+
// Parallelize: member add and status message are independent best-effort actions
|
|
42
44
|
const sourceThreadLink = `<#${sourceThread.id}>`;
|
|
43
|
-
await
|
|
45
|
+
await Promise.all([
|
|
46
|
+
thread.members.add(userId).catch(() => { }),
|
|
47
|
+
sendThreadMessage(thread, `Reusing context from ${sourceThreadLink} to answer prompt...\n${prompt}`),
|
|
48
|
+
]);
|
|
49
|
+
logger.log(`Created btw fork session ${forkedSession.id} in thread ${thread.id} from source thread ${sourceThread.id} (session ${sessionId})`);
|
|
44
50
|
const wrappedPrompt = [
|
|
45
51
|
`The user asked a side question while you were working on another task.`,
|
|
46
52
|
`This is a forked session whose ONLY goal is to answer this question.`,
|
|
47
|
-
`Do NOT continue, resume, or reference the previous task. Only answer the question below
|
|
53
|
+
`Do NOT continue, resume, or reference the previous task. Only answer the question below.`,
|
|
54
|
+
``,
|
|
55
|
+
`Parent session: ${sessionId} (thread <#${sourceThread.id}>)`,
|
|
56
|
+
`If the user asks you to send a message or follow-up to the parent session, use:`,
|
|
57
|
+
` kimaki send --session ${sessionId} --prompt 'your message here'`,
|
|
58
|
+
``,
|
|
48
59
|
prompt,
|
|
49
60
|
].join('\n');
|
|
50
61
|
const runtime = getOrCreateRuntime({
|
package/dist/commands/compact.js
CHANGED
|
@@ -54,7 +54,7 @@ export async function handleCompactCommand({ command, }) {
|
|
|
54
54
|
});
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
|
-
const client = getOpencodeClient(
|
|
57
|
+
const client = getOpencodeClient(workingDirectory);
|
|
58
58
|
if (!client) {
|
|
59
59
|
await command.reply({
|
|
60
60
|
content: 'Failed to get OpenCode client',
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// /context-usage command - Show token usage and context window percentage for the current session.
|
|
2
2
|
import { ChannelType, MessageFlags, } from 'discord.js';
|
|
3
|
+
import { OpenCodeSdkError } from '../errors.js';
|
|
3
4
|
import { getThreadSession } from '../database.js';
|
|
4
|
-
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
5
|
+
import { getOpencodeClient, initializeOpencodeForDirectory } from '../opencode.js';
|
|
5
6
|
import { resolveWorkingDirectory, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
|
|
6
7
|
import { createLogger, LogPrefix } from '../logger.js';
|
|
7
|
-
import * as errore from 'errore';
|
|
8
8
|
const logger = createLogger(LogPrefix.SESSION);
|
|
9
9
|
function getTokenTotal({ input, output, reasoning, cache, }) {
|
|
10
10
|
return input + output + reasoning + cache.read + cache.write;
|
|
@@ -49,17 +49,25 @@ export async function handleContextUsageCommand({ command, }) {
|
|
|
49
49
|
});
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
|
-
const
|
|
53
|
-
if (
|
|
52
|
+
const serverResult = await initializeOpencodeForDirectory(projectDirectory);
|
|
53
|
+
if (serverResult instanceof Error) {
|
|
54
54
|
await command.reply({
|
|
55
|
-
content: `Failed to get context usage: ${
|
|
55
|
+
content: `Failed to get context usage: ${serverResult.message}`,
|
|
56
|
+
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
57
|
+
});
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const client = getOpencodeClient(workingDirectory);
|
|
61
|
+
if (!client) {
|
|
62
|
+
await command.reply({
|
|
63
|
+
content: 'Failed to get OpenCode client',
|
|
56
64
|
flags: MessageFlags.Ephemeral | SILENT_MESSAGE_FLAGS,
|
|
57
65
|
});
|
|
58
66
|
return;
|
|
59
67
|
}
|
|
60
68
|
await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
|
|
61
69
|
try {
|
|
62
|
-
const messagesResponse = await
|
|
70
|
+
const messagesResponse = await client.session.messages({
|
|
63
71
|
sessionID: sessionId,
|
|
64
72
|
directory: workingDirectory,
|
|
65
73
|
});
|
|
@@ -98,9 +106,8 @@ export async function handleContextUsageCommand({ command, }) {
|
|
|
98
106
|
}, 0);
|
|
99
107
|
// Fetch model context limit from provider API
|
|
100
108
|
let contextLimit;
|
|
101
|
-
const providersResult = await
|
|
102
|
-
|
|
103
|
-
});
|
|
109
|
+
const providersResult = await client.provider.list({ directory: workingDirectory })
|
|
110
|
+
.catch((e) => new OpenCodeSdkError({ operation: 'provider.list', cause: e }));
|
|
104
111
|
if (providersResult instanceof Error) {
|
|
105
112
|
logger.error('[CONTEXT-USAGE] Failed to fetch provider info:', providersResult);
|
|
106
113
|
}
|
package/dist/commands/fork.js
CHANGED
|
@@ -27,15 +27,12 @@ function getThreadChannel(channel) {
|
|
|
27
27
|
}
|
|
28
28
|
function parsePersistedEventRows({ rows, }) {
|
|
29
29
|
return rows.flatMap((row) => {
|
|
30
|
-
const parsed = errore.try({
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
cause: error,
|
|
37
|
-
});
|
|
38
|
-
},
|
|
30
|
+
const parsed = errore.try(() => {
|
|
31
|
+
return JSON.parse(row.event_json);
|
|
32
|
+
}, (error) => {
|
|
33
|
+
return new Error('Failed to parse persisted event JSON', {
|
|
34
|
+
cause: error,
|
|
35
|
+
});
|
|
39
36
|
});
|
|
40
37
|
if (parsed instanceof Error) {
|
|
41
38
|
forkLogger.warn(`[fork] Skipping invalid persisted event row ${row.id}: ${parsed.message}`);
|
|
@@ -230,8 +227,9 @@ export async function handleForkSelectMenu(interaction) {
|
|
|
230
227
|
await setThreadSession(thread.id, forkedSession.id);
|
|
231
228
|
// Add user to thread so it appears in their sidebar
|
|
232
229
|
await thread.members.add(interaction.user.id);
|
|
233
|
-
|
|
234
|
-
|
|
230
|
+
const sourceThreadLink = `<#${threadChannel.id}>`;
|
|
231
|
+
sessionLogger.log(`Created forked session ${forkedSession.id} in thread ${thread.id} from source thread ${threadChannel.id} (session ${sessionId})`);
|
|
232
|
+
await sendThreadMessage(thread, `**Forked session created!**\nFrom: ${sourceThreadLink} (\`${sessionId}\`)\nNew session: \`${forkedSession.id}\``);
|
|
235
233
|
// Fetch and display the last assistant messages from the forked session
|
|
236
234
|
const messagesResponse = await getClient().session.messages({
|
|
237
235
|
sessionID: forkedSession.id,
|
package/dist/commands/login.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// login_text:<hash> — text prompt modal submission
|
|
11
11
|
import { ChatInputCommandInteraction, StringSelectMenuInteraction, StringSelectMenuBuilder, ActionRowBuilder, ModalBuilder, TextInputBuilder, TextInputStyle, ModalSubmitInteraction, ButtonBuilder, ButtonStyle, ChannelType, MessageFlags, } from 'discord.js';
|
|
12
12
|
import crypto from 'node:crypto';
|
|
13
|
-
import { initializeOpencodeForDirectory, getOpencodeServerPort, } from '../opencode.js';
|
|
13
|
+
import { initializeOpencodeForDirectory, getOpencodeServerPort, getOpencodeServerAuthHeaders, } from '../opencode.js';
|
|
14
14
|
import { resolveTextChannel, getKimakiMetadata } from '../discord-utils.js';
|
|
15
15
|
import { createLogger, LogPrefix } from '../logger.js';
|
|
16
16
|
import { buildPaginatedOptions, parsePaginationValue } from './paginated-select.js';
|
|
@@ -784,18 +784,11 @@ async function startOAuthFlow(interaction, ctx, hash) {
|
|
|
784
784
|
const hasInputs = Object.keys(ctx.inputs).length > 0;
|
|
785
785
|
const authorizeUrl = new URL(`/provider/${encodeURIComponent(ctx.providerId)}/oauth/authorize`, `http://127.0.0.1:${port}`);
|
|
786
786
|
authorizeUrl.searchParams.set('directory', ctx.dir);
|
|
787
|
-
// Include basic auth if OPENCODE_SERVER_PASSWORD is set,
|
|
788
|
-
// matching the opencode server's optional basicAuth middleware.
|
|
789
787
|
const fetchHeaders = {
|
|
790
788
|
'Content-Type': 'application/json',
|
|
791
789
|
'x-opencode-directory': ctx.dir,
|
|
790
|
+
...getOpencodeServerAuthHeaders(),
|
|
792
791
|
};
|
|
793
|
-
const serverPassword = process.env.OPENCODE_SERVER_PASSWORD;
|
|
794
|
-
if (serverPassword) {
|
|
795
|
-
const username = process.env.OPENCODE_SERVER_USERNAME || 'opencode';
|
|
796
|
-
fetchHeaders['Authorization'] =
|
|
797
|
-
`Basic ${Buffer.from(`${username}:${serverPassword}`).toString('base64')}`;
|
|
798
|
-
}
|
|
799
792
|
const authorizeRes = await fetch(authorizeUrl, {
|
|
800
793
|
method: 'POST',
|
|
801
794
|
headers: fetchHeaders,
|