kimaki 0.4.76 → 0.4.78
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/adapter-rest-boundary.test.js +34 -0
- package/dist/agent-model.e2e.test.js +2 -20
- package/dist/cli.js +50 -13
- package/dist/commands/channel-ref.js +16 -0
- package/dist/commands/diff.js +20 -85
- package/dist/commands/merge-worktree.js +5 -17
- package/dist/commands/new-worktree.js +5 -9
- package/dist/commands/permissions.js +77 -11
- package/dist/commands/resume.js +5 -9
- package/dist/commands/screenshare.js +295 -0
- package/dist/commands/session.js +6 -17
- package/dist/critique-utils.js +95 -0
- package/dist/diff-patch-plugin.js +314 -0
- package/dist/discord-bot.js +19 -14
- package/dist/discord-js-import-boundary.test.js +62 -0
- package/dist/discord-utils.js +44 -0
- package/dist/event-stream-real-capture.e2e.test.js +2 -20
- package/dist/gateway-proxy.e2e.test.js +2 -5
- package/dist/generated/cloudflare/browser.js +17 -0
- package/dist/generated/cloudflare/client.js +34 -0
- package/dist/generated/cloudflare/commonInputTypes.js +10 -0
- package/dist/generated/cloudflare/enums.js +48 -0
- package/dist/generated/cloudflare/internal/class.js +47 -0
- package/dist/generated/cloudflare/internal/prismaNamespace.js +252 -0
- package/dist/generated/cloudflare/internal/prismaNamespaceBrowser.js +222 -0
- package/dist/generated/cloudflare/internal/query_compiler_fast_bg.js +135 -0
- package/dist/generated/cloudflare/models/bot_api_keys.js +1 -0
- package/dist/generated/cloudflare/models/bot_tokens.js +1 -0
- package/dist/generated/cloudflare/models/channel_agents.js +1 -0
- package/dist/generated/cloudflare/models/channel_directories.js +1 -0
- package/dist/generated/cloudflare/models/channel_mention_mode.js +1 -0
- package/dist/generated/cloudflare/models/channel_models.js +1 -0
- package/dist/generated/cloudflare/models/channel_verbosity.js +1 -0
- package/dist/generated/cloudflare/models/channel_worktrees.js +1 -0
- package/dist/generated/cloudflare/models/forum_sync_configs.js +1 -0
- package/dist/generated/cloudflare/models/global_models.js +1 -0
- package/dist/generated/cloudflare/models/ipc_requests.js +1 -0
- package/dist/generated/cloudflare/models/part_messages.js +1 -0
- package/dist/generated/cloudflare/models/scheduled_tasks.js +1 -0
- package/dist/generated/cloudflare/models/session_agents.js +1 -0
- package/dist/generated/cloudflare/models/session_events.js +1 -0
- package/dist/generated/cloudflare/models/session_models.js +1 -0
- package/dist/generated/cloudflare/models/session_start_sources.js +1 -0
- package/dist/generated/cloudflare/models/thread_sessions.js +1 -0
- package/dist/generated/cloudflare/models/thread_worktrees.js +1 -0
- package/dist/generated/cloudflare/models.js +1 -0
- package/dist/generated/node/browser.js +17 -0
- package/dist/generated/node/client.js +37 -0
- package/dist/generated/node/commonInputTypes.js +10 -0
- package/dist/generated/node/enums.js +48 -0
- package/dist/generated/node/internal/class.js +49 -0
- package/dist/generated/node/internal/prismaNamespace.js +252 -0
- package/dist/generated/node/internal/prismaNamespaceBrowser.js +222 -0
- package/dist/generated/node/models/bot_api_keys.js +1 -0
- package/dist/generated/node/models/bot_tokens.js +1 -0
- package/dist/generated/node/models/channel_agents.js +1 -0
- package/dist/generated/node/models/channel_directories.js +1 -0
- package/dist/generated/node/models/channel_mention_mode.js +1 -0
- package/dist/generated/node/models/channel_models.js +1 -0
- package/dist/generated/node/models/channel_verbosity.js +1 -0
- package/dist/generated/node/models/channel_worktrees.js +1 -0
- package/dist/generated/node/models/forum_sync_configs.js +1 -0
- package/dist/generated/node/models/global_models.js +1 -0
- package/dist/generated/node/models/ipc_requests.js +1 -0
- package/dist/generated/node/models/part_messages.js +1 -0
- package/dist/generated/node/models/scheduled_tasks.js +1 -0
- package/dist/generated/node/models/session_agents.js +1 -0
- package/dist/generated/node/models/session_events.js +1 -0
- package/dist/generated/node/models/session_models.js +1 -0
- package/dist/generated/node/models/session_start_sources.js +1 -0
- package/dist/generated/node/models/thread_sessions.js +1 -0
- package/dist/generated/node/models/thread_worktrees.js +1 -0
- package/dist/generated/node/models.js +1 -0
- package/dist/interaction-handler.js +10 -0
- package/dist/kimaki-digital-twin.e2e.test.js +2 -20
- package/dist/message-flags-boundary.test.js +54 -0
- package/dist/message-formatting.js +3 -62
- package/dist/onboarding-tutorial-plugin.js +1 -1
- package/dist/opencode-command.js +129 -0
- package/dist/opencode-command.test.js +48 -0
- package/dist/opencode-interrupt-plugin.js +19 -1
- package/dist/opencode-interrupt-plugin.test.js +0 -5
- package/dist/opencode-plugin-loading.e2e.test.js +9 -20
- package/dist/opencode-plugin.js +4 -4
- package/dist/opencode.js +150 -27
- package/dist/patch-text-parser.js +97 -0
- package/dist/platform/components-v2.js +20 -0
- package/dist/platform/discord-adapter.js +1440 -0
- package/dist/platform/discord-routes.js +31 -0
- package/dist/platform/message-flags.js +8 -0
- package/dist/platform/platform-value.js +41 -0
- package/dist/platform/slack-adapter.js +872 -0
- package/dist/platform/slack-markdown.js +169 -0
- package/dist/platform/types.js +4 -0
- package/dist/queue-advanced-e2e-setup.js +265 -0
- package/dist/queue-advanced-footer.e2e.test.js +173 -0
- package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
- package/dist/queue-advanced-permissions-typing.e2e.test.js +73 -1
- package/dist/runtime-lifecycle.e2e.test.js +2 -20
- package/dist/session-handler/event-stream-state.js +5 -0
- package/dist/session-handler/event-stream-state.test.js +6 -2
- package/dist/session-handler/thread-session-runtime.js +32 -2
- package/dist/system-message.js +26 -23
- package/dist/test-utils.js +16 -0
- package/dist/thread-message-queue.e2e.test.js +2 -20
- package/dist/utils.js +3 -1
- package/dist/voice-message.e2e.test.js +2 -20
- package/dist/voice.js +122 -9
- package/dist/voice.test.js +17 -2
- package/dist/websockify.js +69 -0
- package/dist/worktree-lifecycle.e2e.test.js +308 -0
- package/package.json +4 -2
- package/skills/critique/SKILL.md +17 -0
- package/skills/egaki/SKILL.md +35 -0
- package/skills/event-sourcing-state/SKILL.md +252 -0
- package/skills/goke/SKILL.md +1 -0
- package/skills/npm-package/SKILL.md +21 -2
- package/skills/playwriter/SKILL.md +1 -1
- package/skills/x-articles/SKILL.md +554 -0
- package/src/agent-model.e2e.test.ts +4 -19
- package/src/cli.ts +60 -13
- package/src/commands/diff.ts +25 -99
- package/src/commands/merge-worktree.ts +5 -21
- package/src/commands/new-worktree.ts +5 -11
- package/src/commands/permissions.ts +100 -15
- package/src/commands/resume.ts +5 -12
- package/src/commands/screenshare.ts +354 -0
- package/src/commands/session.ts +6 -23
- package/src/critique-utils.ts +139 -0
- package/src/discord-bot.ts +20 -15
- package/src/discord-utils.ts +53 -0
- package/src/event-stream-real-capture.e2e.test.ts +4 -20
- package/src/gateway-proxy.e2e.test.ts +2 -5
- package/src/interaction-handler.ts +15 -0
- package/src/kimaki-digital-twin.e2e.test.ts +2 -21
- package/src/message-formatting.ts +3 -68
- package/src/onboarding-tutorial-plugin.ts +1 -1
- package/src/opencode-command.test.ts +70 -0
- package/src/opencode-command.ts +188 -0
- package/src/opencode-interrupt-plugin.test.ts +0 -5
- package/src/opencode-interrupt-plugin.ts +34 -1
- package/src/opencode-plugin-loading.e2e.test.ts +25 -35
- package/src/opencode-plugin.ts +5 -4
- package/src/opencode.ts +199 -32
- package/src/patch-text-parser.ts +107 -0
- package/src/queue-advanced-e2e-setup.ts +273 -0
- package/src/queue-advanced-footer.e2e.test.ts +211 -0
- package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
- package/src/queue-advanced-permissions-typing.e2e.test.ts +92 -0
- package/src/runtime-lifecycle.e2e.test.ts +4 -19
- package/src/session-handler/event-stream-state.test.ts +6 -2
- package/src/session-handler/event-stream-state.ts +5 -0
- package/src/session-handler/thread-session-runtime.ts +45 -2
- package/src/system-message.ts +26 -23
- package/src/test-utils.ts +17 -0
- package/src/thread-message-queue.e2e.test.ts +2 -20
- package/src/utils.ts +3 -1
- package/src/voice-message.e2e.test.ts +3 -20
- package/src/voice.test.ts +26 -2
- package/src/voice.ts +147 -9
- package/src/websockify.ts +101 -0
- package/src/worktree-lifecycle.e2e.test.ts +391 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Guardrail test to keep REST helpers out of adapter-consumer runtime files.
|
|
2
|
+
// CLI/task-runner/discord-utils must use platform adapter interfaces instead of
|
|
3
|
+
// direct createDiscordRest/discordRoutes/discordApiUrl imports.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { describe, expect, test } from 'vitest';
|
|
7
|
+
const TARGET_FILES = [
|
|
8
|
+
'src/cli.ts',
|
|
9
|
+
'src/task-runner.ts',
|
|
10
|
+
'src/discord-utils.ts',
|
|
11
|
+
];
|
|
12
|
+
const PROJECT_ROOT = path.resolve(import.meta.dirname, '..');
|
|
13
|
+
describe('adapter rest boundary', () => {
|
|
14
|
+
test('forbids direct REST helper imports in migrated files', () => {
|
|
15
|
+
const violations = TARGET_FILES.flatMap((relativePath) => {
|
|
16
|
+
const filePath = path.join(PROJECT_ROOT, relativePath);
|
|
17
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
18
|
+
const matches = [
|
|
19
|
+
/\bcreateDiscordRest\b/.test(content)
|
|
20
|
+
? 'createDiscordRest'
|
|
21
|
+
: null,
|
|
22
|
+
/\bdiscordRoutes\b/.test(content) ? 'discordRoutes' : null,
|
|
23
|
+
/\bdiscordApiUrl\b/.test(content) ? 'discordApiUrl' : null,
|
|
24
|
+
].filter((match) => {
|
|
25
|
+
return Boolean(match);
|
|
26
|
+
});
|
|
27
|
+
if (matches.length === 0) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
return [`${relativePath}: ${matches.join(', ')}`];
|
|
31
|
+
});
|
|
32
|
+
expect(violations).toMatchInlineSnapshot(`[]`);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
// Uses opencode-deterministic-provider (no real LLM calls).
|
|
11
11
|
// Poll timeouts: 4s max, 100ms interval.
|
|
12
12
|
import fs from 'node:fs';
|
|
13
|
-
import net from 'node:net';
|
|
14
13
|
import path from 'node:path';
|
|
15
14
|
import url from 'node:url';
|
|
16
15
|
import { describe, beforeAll, afterAll, test, expect, } from 'vitest';
|
|
@@ -24,7 +23,7 @@ import { setBotToken, initDatabase, closeDatabase, setChannelDirectory, setChann
|
|
|
24
23
|
import { getPrisma } from './db.js';
|
|
25
24
|
import { startHranaServer, stopHranaServer } from './hrana-server.js';
|
|
26
25
|
import { initializeOpencodeForDirectory, stopOpencodeServer } from './opencode.js';
|
|
27
|
-
import { cleanupTestSessions, waitForBotMessageContaining, waitForFooterMessage, } from './test-utils.js';
|
|
26
|
+
import { chooseLockPort, cleanupTestSessions, waitForBotMessageContaining, waitForFooterMessage, } from './test-utils.js';
|
|
28
27
|
import { buildQuickAgentCommandDescription } from './commands/agent.js';
|
|
29
28
|
const TEST_USER_ID = '200000000000000920';
|
|
30
29
|
const TEXT_CHANNEL_ID = '200000000000000921';
|
|
@@ -41,23 +40,6 @@ function createRunDirectories() {
|
|
|
41
40
|
fs.mkdirSync(projectDirectory, { recursive: true });
|
|
42
41
|
return { root, dataDir, projectDirectory };
|
|
43
42
|
}
|
|
44
|
-
function chooseLockPort() {
|
|
45
|
-
return new Promise((resolve, reject) => {
|
|
46
|
-
const server = net.createServer();
|
|
47
|
-
server.listen(0, () => {
|
|
48
|
-
const address = server.address();
|
|
49
|
-
if (!address || typeof address === 'string') {
|
|
50
|
-
server.close();
|
|
51
|
-
reject(new Error('Failed to resolve lock port'));
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
const port = address.port;
|
|
55
|
-
server.close(() => {
|
|
56
|
-
resolve(port);
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
43
|
function createDiscordJsClient({ restUrl }) {
|
|
62
44
|
return new Client({
|
|
63
45
|
intents: [
|
|
@@ -158,7 +140,7 @@ describe('agent model resolution', () => {
|
|
|
158
140
|
beforeAll(async () => {
|
|
159
141
|
testStartTime = Date.now();
|
|
160
142
|
directories = createRunDirectories();
|
|
161
|
-
const lockPort =
|
|
143
|
+
const lockPort = chooseLockPort({ key: TEXT_CHANNEL_ID });
|
|
162
144
|
process.env['KIMAKI_LOCK_PORT'] = String(lockPort);
|
|
163
145
|
setDataDir(directories.dataDir);
|
|
164
146
|
previousDefaultVerbosity = store.getState().defaultVerbosity;
|
package/dist/cli.js
CHANGED
|
@@ -13,6 +13,7 @@ import { formatWorktreeName } from './commands/new-worktree.js';
|
|
|
13
13
|
import { WORKTREE_PREFIX } from './commands/merge-worktree.js';
|
|
14
14
|
import { sendWelcomeMessage } from './onboarding-welcome.js';
|
|
15
15
|
import { buildOpencodeEventLogLine } from './session-handler/opencode-session-event-log.js';
|
|
16
|
+
import { selectResolvedCommand } from './opencode-command.js';
|
|
16
17
|
import yaml from 'js-yaml';
|
|
17
18
|
import { Events, ChannelType, ActivityType, Routes, SlashCommandBuilder, AttachmentBuilder, } from 'discord.js';
|
|
18
19
|
import { createDiscordRest, discordApiUrl, getDiscordRestApiUrl, getGatewayProxyRestBaseUrl } from './discord-urls.js';
|
|
@@ -342,7 +343,11 @@ async function ensureCommandAvailable({ name, envPathKey, installUnix, installWi
|
|
|
342
343
|
const foundInPath = await execAsync(`${whichCmd} ${name}`, {
|
|
343
344
|
env: process.env,
|
|
344
345
|
}).then((result) => {
|
|
345
|
-
|
|
346
|
+
const resolved = selectResolvedCommand({
|
|
347
|
+
output: result.stdout,
|
|
348
|
+
isWindows,
|
|
349
|
+
});
|
|
350
|
+
return resolved || '';
|
|
346
351
|
}, () => {
|
|
347
352
|
return '';
|
|
348
353
|
});
|
|
@@ -730,6 +735,16 @@ async function registerCommands({ token, appId, guildIds, userCommands = [], age
|
|
|
730
735
|
.setDescription('List and manage MCP servers for this project')
|
|
731
736
|
.setDMPermission(false)
|
|
732
737
|
.toJSON(),
|
|
738
|
+
new SlashCommandBuilder()
|
|
739
|
+
.setName('screenshare')
|
|
740
|
+
.setDescription('Start screen sharing via VNC tunnel (auto-stops after 1 hour)')
|
|
741
|
+
.setDMPermission(false)
|
|
742
|
+
.toJSON(),
|
|
743
|
+
new SlashCommandBuilder()
|
|
744
|
+
.setName('screenshare-stop')
|
|
745
|
+
.setDescription('Stop screen sharing')
|
|
746
|
+
.setDMPermission(false)
|
|
747
|
+
.toJSON(),
|
|
733
748
|
];
|
|
734
749
|
// Add user-defined commands with source-based suffixes (-cmd / -skill)
|
|
735
750
|
// Also populate registeredUserCommands in the store for /queue-command autocomplete
|
|
@@ -1173,7 +1188,7 @@ async function resolveCredentials({ forceRestartOnboarding, forceGateway, gatewa
|
|
|
1173
1188
|
emitJsonEvent({ type: 'install_url', url: oauthUrl });
|
|
1174
1189
|
}
|
|
1175
1190
|
// Poll until the user installs the bot in a Discord server.
|
|
1176
|
-
//
|
|
1191
|
+
// 100 attempts x 3s = 5 minutes timeout.
|
|
1177
1192
|
const s = isInteractive ? spinner() : undefined;
|
|
1178
1193
|
s?.start('Waiting for a Discord server with the bot installed...');
|
|
1179
1194
|
const pollUrl = new URL('/api/onboarding/status', KIMAKI_WEBSITE_URL);
|
|
@@ -1181,9 +1196,9 @@ async function resolveCredentials({ forceRestartOnboarding, forceGateway, gatewa
|
|
|
1181
1196
|
pollUrl.searchParams.set('secret', clientSecret);
|
|
1182
1197
|
let guildId;
|
|
1183
1198
|
let installerDiscordUserId;
|
|
1184
|
-
for (let attempt = 0; attempt <
|
|
1199
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
1185
1200
|
await new Promise((resolve) => {
|
|
1186
|
-
setTimeout(resolve,
|
|
1201
|
+
setTimeout(resolve, 3000);
|
|
1187
1202
|
});
|
|
1188
1203
|
// Progressive hints for interactive users who may be stuck
|
|
1189
1204
|
if (isInteractive) {
|
|
@@ -1217,9 +1232,9 @@ async function resolveCredentials({ forceRestartOnboarding, forceGateway, gatewa
|
|
|
1217
1232
|
s?.stop('Authorization timed out');
|
|
1218
1233
|
}
|
|
1219
1234
|
else {
|
|
1220
|
-
emitJsonEvent({ type: 'error', message: 'Authorization timed out after
|
|
1235
|
+
emitJsonEvent({ type: 'error', message: 'Authorization timed out after 5 minutes' });
|
|
1221
1236
|
}
|
|
1222
|
-
cliLogger.error('Bot authorization timed out after
|
|
1237
|
+
cliLogger.error('Bot authorization timed out after 5 minutes. Please try again.');
|
|
1223
1238
|
process.exit(EXIT_NO_RESTART);
|
|
1224
1239
|
}
|
|
1225
1240
|
if (isInteractive) {
|
|
@@ -1507,13 +1522,18 @@ async function run({ restartOnboarding, addChannels, useWorktrees, enableVoiceCh
|
|
|
1507
1522
|
}
|
|
1508
1523
|
// Create default kimaki channel + welcome message in each guild.
|
|
1509
1524
|
// Runs after channel sync so existing channels are detected correctly.
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1525
|
+
try {
|
|
1526
|
+
await ensureDefaultChannelsWithWelcome({
|
|
1527
|
+
guilds,
|
|
1528
|
+
discordClient,
|
|
1529
|
+
appId,
|
|
1530
|
+
isGatewayMode,
|
|
1531
|
+
installerDiscordUserId,
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
catch (error) {
|
|
1535
|
+
cliLogger.warn('Background default channel creation failed:', error instanceof Error ? error.message : String(error));
|
|
1536
|
+
}
|
|
1517
1537
|
})();
|
|
1518
1538
|
// Background: OpenCode init + slash command registration (non-blocking)
|
|
1519
1539
|
void backgroundInit({
|
|
@@ -2927,6 +2947,23 @@ cli
|
|
|
2927
2947
|
command: command.length > 0 ? command : undefined,
|
|
2928
2948
|
});
|
|
2929
2949
|
});
|
|
2950
|
+
cli
|
|
2951
|
+
.command('screenshare', 'Share your screen via VNC tunnel. Auto-stops after 1 hour. Runs until Ctrl+C. Use tmux to run in background.')
|
|
2952
|
+
.action(async () => {
|
|
2953
|
+
const { startScreenshare } = await import('./commands/screenshare.js');
|
|
2954
|
+
try {
|
|
2955
|
+
const session = await startScreenshare({
|
|
2956
|
+
sessionKey: 'cli',
|
|
2957
|
+
startedBy: 'cli',
|
|
2958
|
+
});
|
|
2959
|
+
cliLogger.log(`Screen sharing started: ${session.noVncUrl}`);
|
|
2960
|
+
cliLogger.log('Press Ctrl+C to stop');
|
|
2961
|
+
}
|
|
2962
|
+
catch (err) {
|
|
2963
|
+
cliLogger.error('Failed to start screen share:', err instanceof Error ? err.message : String(err));
|
|
2964
|
+
process.exit(EXIT_NO_RESTART);
|
|
2965
|
+
}
|
|
2966
|
+
});
|
|
2930
2967
|
cli
|
|
2931
2968
|
.command('sqlitedb', 'Show the location of the SQLite database file')
|
|
2932
2969
|
.action(() => {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Helpers for working with normalized platform channel references in commands.
|
|
2
|
+
export function isThreadChannel(channel) {
|
|
3
|
+
return channel?.kind === 'thread';
|
|
4
|
+
}
|
|
5
|
+
export function isTextChannel(channel) {
|
|
6
|
+
return channel?.kind === 'text';
|
|
7
|
+
}
|
|
8
|
+
export function getRootChannelId(channel) {
|
|
9
|
+
if (!channel) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
if (channel.kind === 'thread') {
|
|
13
|
+
return channel.parentId || channel.id;
|
|
14
|
+
}
|
|
15
|
+
return channel.id;
|
|
16
|
+
}
|
package/dist/commands/diff.js
CHANGED
|
@@ -3,7 +3,7 @@ import { ChannelType, EmbedBuilder, MessageFlags, } from 'discord.js';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { resolveWorkingDirectory, SILENT_MESSAGE_FLAGS, } from '../discord-utils.js';
|
|
5
5
|
import { createLogger, LogPrefix } from '../logger.js';
|
|
6
|
-
import {
|
|
6
|
+
import { uploadGitDiffViaCritique } from '../critique-utils.js';
|
|
7
7
|
const logger = createLogger(LogPrefix.DIFF);
|
|
8
8
|
export async function handleDiffCommand({ command, }) {
|
|
9
9
|
const channel = command.channel;
|
|
@@ -39,90 +39,25 @@ export async function handleDiffCommand({ command, }) {
|
|
|
39
39
|
}
|
|
40
40
|
const { workingDirectory } = resolved;
|
|
41
41
|
await command.deferReply({ flags: SILENT_MESSAGE_FLAGS });
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const lines = output.trim().split('\n');
|
|
52
|
-
const jsonLine = lines[lines.length - 1];
|
|
53
|
-
if (!jsonLine) {
|
|
54
|
-
await command.editReply({
|
|
55
|
-
content: 'No changes to show',
|
|
56
|
-
});
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
let result;
|
|
60
|
-
try {
|
|
61
|
-
result = JSON.parse(jsonLine);
|
|
62
|
-
}
|
|
63
|
-
catch {
|
|
64
|
-
// Fallback: try to find URL in output
|
|
65
|
-
const urlMatch = output.match(/https?:\/\/critique\.work\/[^\s]+/);
|
|
66
|
-
if (urlMatch) {
|
|
67
|
-
await command.editReply({
|
|
68
|
-
content: `[diff](${urlMatch[0]})`,
|
|
69
|
-
});
|
|
70
|
-
logger.log(`Diff shared: ${urlMatch[0]}`);
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
await command.editReply({
|
|
74
|
-
content: 'No changes to show',
|
|
75
|
-
});
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
if (result.error || !result.url || !result.id) {
|
|
79
|
-
await command.editReply({
|
|
80
|
-
content: result.error || 'No changes to show',
|
|
81
|
-
});
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
const imageUrl = `https://critique.work/og/${result.id}.png`;
|
|
85
|
-
const embed = new EmbedBuilder()
|
|
86
|
-
.setTitle(title)
|
|
87
|
-
.setURL(result.url)
|
|
88
|
-
.setImage(imageUrl);
|
|
89
|
-
await command.editReply({
|
|
90
|
-
embeds: [embed],
|
|
91
|
-
});
|
|
92
|
-
logger.log(`Diff shared: ${result.url}`);
|
|
42
|
+
const projectName = path.basename(workingDirectory);
|
|
43
|
+
const title = `${projectName}: Discord /diff`;
|
|
44
|
+
const result = await uploadGitDiffViaCritique({
|
|
45
|
+
title,
|
|
46
|
+
cwd: workingDirectory,
|
|
47
|
+
});
|
|
48
|
+
if (!result) {
|
|
49
|
+
await command.editReply({ content: 'No changes to show' });
|
|
50
|
+
return;
|
|
93
51
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const execError = error;
|
|
98
|
-
const output = execError.stdout || execError.stderr || '';
|
|
99
|
-
// Check if critique output JSON even on error
|
|
100
|
-
const lines = output.trim().split('\n');
|
|
101
|
-
const jsonLine = lines[lines.length - 1];
|
|
102
|
-
if (jsonLine) {
|
|
103
|
-
try {
|
|
104
|
-
const result = JSON.parse(jsonLine);
|
|
105
|
-
if (result.error) {
|
|
106
|
-
await command.editReply({
|
|
107
|
-
content: result.error,
|
|
108
|
-
});
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
catch {
|
|
113
|
-
// not JSON, continue to generic error
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
// Check for common errors
|
|
117
|
-
const message = execError.message || 'Unknown error';
|
|
118
|
-
if (message.includes('command not found') || message.includes('ENOENT')) {
|
|
119
|
-
await command.editReply({
|
|
120
|
-
content: 'bunx/critique not available',
|
|
121
|
-
});
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
await command.editReply({
|
|
125
|
-
content: `Failed to generate diff: ${message.slice(0, 200)}`,
|
|
126
|
-
});
|
|
52
|
+
if (result.error || !result.url) {
|
|
53
|
+
await command.editReply({ content: result.error || 'No changes to show' });
|
|
54
|
+
return;
|
|
127
55
|
}
|
|
56
|
+
const imageUrl = `https://critique.work/og/${result.id}.png`;
|
|
57
|
+
const embed = new EmbedBuilder()
|
|
58
|
+
.setTitle(title)
|
|
59
|
+
.setURL(result.url)
|
|
60
|
+
.setImage(imageUrl);
|
|
61
|
+
await command.editReply({ embeds: [embed] });
|
|
62
|
+
logger.log(`Diff shared: ${result.url}`);
|
|
128
63
|
}
|
|
@@ -6,7 +6,7 @@ import { getThreadWorktree, getThreadSession, getChannelDirectory, } from '../da
|
|
|
6
6
|
import { createLogger, LogPrefix } from '../logger.js';
|
|
7
7
|
import { notifyError } from '../sentry.js';
|
|
8
8
|
import { mergeWorktree, listBranchesByLastCommit, validateBranchRef } from '../worktrees.js';
|
|
9
|
-
import { sendThreadMessage, resolveWorkingDirectory,
|
|
9
|
+
import { sendThreadMessage, resolveWorkingDirectory, resolveProjectDirectoryFromAutocomplete, } from '../discord-utils.js';
|
|
10
10
|
import { getOrCreateRuntime, } from '../session-handler/thread-session-runtime.js';
|
|
11
11
|
import { RebaseConflictError, DirtyWorktreeError } from '../errors.js';
|
|
12
12
|
const logger = createLogger(LogPrefix.WORKTREE);
|
|
@@ -131,22 +131,10 @@ export async function handleMergeWorktreeCommand({ command, appId, }) {
|
|
|
131
131
|
export async function handleMergeWorktreeAutocomplete({ interaction, }) {
|
|
132
132
|
try {
|
|
133
133
|
const focusedValue = interaction.options.getFocused();
|
|
134
|
-
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
if (worktreeInfo?.project_directory) {
|
|
139
|
-
projectDirectory = worktreeInfo.project_directory;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
// Fallback: resolve from parent channel
|
|
143
|
-
if (!projectDirectory && interaction.channel) {
|
|
144
|
-
const textChannel = await resolveTextChannel(interaction.channel);
|
|
145
|
-
if (textChannel) {
|
|
146
|
-
const channelConfig = await getChannelDirectory(textChannel.id);
|
|
147
|
-
projectDirectory = channelConfig?.directory;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
134
|
+
// interaction.channel can be null when the channel isn't cached
|
|
135
|
+
// (common with gateway-proxy). Use channelId which is always available
|
|
136
|
+
// from the raw interaction payload.
|
|
137
|
+
const projectDirectory = await resolveProjectDirectoryFromAutocomplete(interaction);
|
|
150
138
|
if (!projectDirectory) {
|
|
151
139
|
await interaction.respond([]);
|
|
152
140
|
return;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { ChannelType, REST, } from 'discord.js';
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import { createPendingWorktree, setWorktreeReady, setWorktreeError, getChannelDirectory, getThreadWorktree, } from '../database.js';
|
|
7
|
-
import { SILENT_MESSAGE_FLAGS, reactToThread,
|
|
7
|
+
import { SILENT_MESSAGE_FLAGS, reactToThread, resolveProjectDirectoryFromAutocomplete, } 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';
|
|
@@ -313,14 +313,10 @@ async function handleWorktreeInThread({ command, thread, }) {
|
|
|
313
313
|
export async function handleNewWorktreeAutocomplete({ interaction, }) {
|
|
314
314
|
try {
|
|
315
315
|
const focusedValue = interaction.options.getFocused();
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const channelConfig = await getChannelDirectory(textChannel.id);
|
|
321
|
-
projectDirectory = channelConfig?.directory;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
316
|
+
// interaction.channel can be null when the channel isn't cached
|
|
317
|
+
// (common with gateway-proxy). Use channelId which is always available
|
|
318
|
+
// from the raw interaction payload.
|
|
319
|
+
const projectDirectory = await resolveProjectDirectoryFromAutocomplete(interaction);
|
|
324
320
|
if (!projectDirectory) {
|
|
325
321
|
await interaction.respond([]);
|
|
326
322
|
return;
|
|
@@ -118,9 +118,83 @@ export async function showPermissionButtons({ thread, permission, directory, per
|
|
|
118
118
|
components: [actionRow],
|
|
119
119
|
flags: NOTIFY_MESSAGE_FLAGS | MessageFlags.SuppressEmbeds,
|
|
120
120
|
});
|
|
121
|
+
context.messageId = permissionMessage.id;
|
|
121
122
|
logger.log(`Showed permission buttons for ${permission.id}`);
|
|
122
123
|
return { messageId: permissionMessage.id, contextHash };
|
|
123
124
|
}
|
|
125
|
+
function updatePermissionMessage({ context, status, }) {
|
|
126
|
+
if (!context.messageId) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
context.thread.messages
|
|
130
|
+
.fetch(context.messageId)
|
|
131
|
+
.then((message) => {
|
|
132
|
+
const patternStr = compactPermissionPatterns(context.permission.patterns).join(', ');
|
|
133
|
+
const externalDirLine = context.permission.permission === 'external_directory'
|
|
134
|
+
? 'Agent is accessing files outside the project. [Learn more](https://opencode.ai/docs/permissions/#external-directories)\n'
|
|
135
|
+
: '';
|
|
136
|
+
return message.edit({
|
|
137
|
+
content: `⚠️ **Permission Required**\n` +
|
|
138
|
+
`**Type:** \`${context.permission.permission}\`\n` +
|
|
139
|
+
externalDirLine +
|
|
140
|
+
(patternStr ? `**Pattern:** \`${patternStr}\`\n` : '') +
|
|
141
|
+
status,
|
|
142
|
+
components: [],
|
|
143
|
+
});
|
|
144
|
+
})
|
|
145
|
+
.catch((error) => {
|
|
146
|
+
logger.error('Failed to update permission message:', error);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
export async function cancelPendingPermission(threadId) {
|
|
150
|
+
const contexts = Array.from(pendingPermissionContexts.values()).filter((context) => {
|
|
151
|
+
return context.thread.id === threadId;
|
|
152
|
+
});
|
|
153
|
+
if (contexts.length === 0) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
let cancelledCount = 0;
|
|
157
|
+
for (const context of contexts) {
|
|
158
|
+
const pendingContext = takePendingPermissionContext(context.contextHash);
|
|
159
|
+
if (!pendingContext) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
const client = getOpencodeClient(pendingContext.directory);
|
|
163
|
+
if (!client) {
|
|
164
|
+
pendingPermissionContexts.set(pendingContext.contextHash, pendingContext);
|
|
165
|
+
logger.error('Failed to dismiss pending permission: OpenCode server not found');
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const requestIds = pendingContext.requestIds.length > 0
|
|
169
|
+
? pendingContext.requestIds
|
|
170
|
+
: [pendingContext.permission.id];
|
|
171
|
+
const result = await Promise.all(requestIds.map((requestId) => {
|
|
172
|
+
return client.permission.reply({
|
|
173
|
+
requestID: requestId,
|
|
174
|
+
directory: pendingContext.permissionDirectory,
|
|
175
|
+
reply: 'reject',
|
|
176
|
+
});
|
|
177
|
+
})).then(() => {
|
|
178
|
+
return 'ok';
|
|
179
|
+
}).catch((error) => {
|
|
180
|
+
pendingPermissionContexts.set(pendingContext.contextHash, pendingContext);
|
|
181
|
+
logger.error('Failed to dismiss pending permission:', error);
|
|
182
|
+
return 'error';
|
|
183
|
+
});
|
|
184
|
+
if (result === 'error') {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
updatePermissionMessage({
|
|
188
|
+
context: pendingContext,
|
|
189
|
+
status: '_Permission dismissed - user sent a new message._',
|
|
190
|
+
});
|
|
191
|
+
cancelledCount++;
|
|
192
|
+
}
|
|
193
|
+
if (cancelledCount > 0) {
|
|
194
|
+
logger.log(`Dismissed ${cancelledCount} pending permission request(s) for thread ${threadId}`);
|
|
195
|
+
}
|
|
196
|
+
return cancelledCount > 0;
|
|
197
|
+
}
|
|
124
198
|
/**
|
|
125
199
|
* Handle button click for permission.
|
|
126
200
|
*/
|
|
@@ -166,17 +240,9 @@ export async function handlePermissionButton(interaction) {
|
|
|
166
240
|
return '❌ Permission **rejected**';
|
|
167
241
|
}
|
|
168
242
|
})();
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
: '';
|
|
173
|
-
await interaction.editReply({
|
|
174
|
-
content: `⚠️ **Permission Required**\n` +
|
|
175
|
-
`**Type:** \`${context.permission.permission}\`\n` +
|
|
176
|
-
externalDirLine +
|
|
177
|
-
(patternStr ? `**Pattern:** \`${patternStr}\`\n` : '') +
|
|
178
|
-
resultText,
|
|
179
|
-
components: [], // Remove the buttons
|
|
243
|
+
updatePermissionMessage({
|
|
244
|
+
context,
|
|
245
|
+
status: resultText,
|
|
180
246
|
});
|
|
181
247
|
logger.log(`Permission ${context.permission.id} ${response} (${requestIds.length} request(s))`);
|
|
182
248
|
}
|
package/dist/commands/resume.js
CHANGED
|
@@ -3,7 +3,7 @@ import { ChannelType, ThreadAutoArchiveDuration, } from 'discord.js';
|
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import { getChannelDirectory, setThreadSession, setPartMessagesBatch, getAllThreadSessionIds, } from '../database.js';
|
|
5
5
|
import { initializeOpencodeForDirectory } from '../opencode.js';
|
|
6
|
-
import { sendThreadMessage,
|
|
6
|
+
import { sendThreadMessage, resolveProjectDirectoryFromAutocomplete } from '../discord-utils.js';
|
|
7
7
|
import { collectLastAssistantParts } from '../message-formatting.js';
|
|
8
8
|
import { createLogger, LogPrefix } from '../logger.js';
|
|
9
9
|
import * as errore from 'errore';
|
|
@@ -100,14 +100,10 @@ export async function handleResumeCommand({ command, }) {
|
|
|
100
100
|
}
|
|
101
101
|
export async function handleResumeAutocomplete({ interaction, }) {
|
|
102
102
|
const focusedValue = interaction.options.getFocused();
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const channelConfig = await getChannelDirectory(textChannel.id);
|
|
108
|
-
projectDirectory = channelConfig?.directory;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
103
|
+
// interaction.channel can be null when the channel isn't cached
|
|
104
|
+
// (common with gateway-proxy). Use channelId which is always available
|
|
105
|
+
// from the raw interaction payload.
|
|
106
|
+
const projectDirectory = await resolveProjectDirectoryFromAutocomplete(interaction);
|
|
111
107
|
if (!projectDirectory) {
|
|
112
108
|
await interaction.respond([]);
|
|
113
109
|
return;
|