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
package/src/cli.ts
CHANGED
|
@@ -64,6 +64,7 @@ import { WORKTREE_PREFIX } from './commands/merge-worktree.js'
|
|
|
64
64
|
import type { ThreadStartMarker } from './system-message.js'
|
|
65
65
|
import { sendWelcomeMessage } from './onboarding-welcome.js'
|
|
66
66
|
import { buildOpencodeEventLogLine } from './session-handler/opencode-session-event-log.js'
|
|
67
|
+
import { selectResolvedCommand } from './opencode-command.js'
|
|
67
68
|
import yaml from 'js-yaml'
|
|
68
69
|
import type {
|
|
69
70
|
OpencodeClient,
|
|
@@ -552,7 +553,11 @@ async function ensureCommandAvailable({
|
|
|
552
553
|
env: process.env,
|
|
553
554
|
}).then(
|
|
554
555
|
(result) => {
|
|
555
|
-
|
|
556
|
+
const resolved = selectResolvedCommand({
|
|
557
|
+
output: result.stdout,
|
|
558
|
+
isWindows,
|
|
559
|
+
})
|
|
560
|
+
return resolved || ''
|
|
556
561
|
},
|
|
557
562
|
() => {
|
|
558
563
|
return ''
|
|
@@ -1084,6 +1089,16 @@ async function registerCommands({
|
|
|
1084
1089
|
.setDescription('List and manage MCP servers for this project')
|
|
1085
1090
|
.setDMPermission(false)
|
|
1086
1091
|
.toJSON(),
|
|
1092
|
+
new SlashCommandBuilder()
|
|
1093
|
+
.setName('screenshare')
|
|
1094
|
+
.setDescription('Start screen sharing via VNC tunnel (auto-stops after 1 hour)')
|
|
1095
|
+
.setDMPermission(false)
|
|
1096
|
+
.toJSON(),
|
|
1097
|
+
new SlashCommandBuilder()
|
|
1098
|
+
.setName('screenshare-stop')
|
|
1099
|
+
.setDescription('Stop screen sharing')
|
|
1100
|
+
.setDMPermission(false)
|
|
1101
|
+
.toJSON(),
|
|
1087
1102
|
]
|
|
1088
1103
|
|
|
1089
1104
|
// Add user-defined commands with source-based suffixes (-cmd / -skill)
|
|
@@ -1702,7 +1717,7 @@ async function resolveCredentials({
|
|
|
1702
1717
|
}
|
|
1703
1718
|
|
|
1704
1719
|
// Poll until the user installs the bot in a Discord server.
|
|
1705
|
-
//
|
|
1720
|
+
// 100 attempts x 3s = 5 minutes timeout.
|
|
1706
1721
|
const s = isInteractive ? spinner() : undefined
|
|
1707
1722
|
s?.start('Waiting for a Discord server with the bot installed...')
|
|
1708
1723
|
|
|
@@ -1712,9 +1727,9 @@ async function resolveCredentials({
|
|
|
1712
1727
|
|
|
1713
1728
|
let guildId: string | undefined
|
|
1714
1729
|
let installerDiscordUserId: string | undefined
|
|
1715
|
-
for (let attempt = 0; attempt <
|
|
1730
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
1716
1731
|
await new Promise((resolve) => {
|
|
1717
|
-
setTimeout(resolve,
|
|
1732
|
+
setTimeout(resolve, 3000)
|
|
1718
1733
|
})
|
|
1719
1734
|
|
|
1720
1735
|
// Progressive hints for interactive users who may be stuck
|
|
@@ -1756,10 +1771,10 @@ async function resolveCredentials({
|
|
|
1756
1771
|
if (isInteractive) {
|
|
1757
1772
|
s?.stop('Authorization timed out')
|
|
1758
1773
|
} else {
|
|
1759
|
-
emitJsonEvent({ type: 'error', message: 'Authorization timed out after
|
|
1774
|
+
emitJsonEvent({ type: 'error', message: 'Authorization timed out after 5 minutes' })
|
|
1760
1775
|
}
|
|
1761
1776
|
cliLogger.error(
|
|
1762
|
-
'Bot authorization timed out after
|
|
1777
|
+
'Bot authorization timed out after 5 minutes. Please try again.',
|
|
1763
1778
|
)
|
|
1764
1779
|
process.exit(EXIT_NO_RESTART)
|
|
1765
1780
|
}
|
|
@@ -2117,13 +2132,20 @@ async function run({
|
|
|
2117
2132
|
|
|
2118
2133
|
// Create default kimaki channel + welcome message in each guild.
|
|
2119
2134
|
// Runs after channel sync so existing channels are detected correctly.
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2135
|
+
try {
|
|
2136
|
+
await ensureDefaultChannelsWithWelcome({
|
|
2137
|
+
guilds,
|
|
2138
|
+
discordClient,
|
|
2139
|
+
appId,
|
|
2140
|
+
isGatewayMode,
|
|
2141
|
+
installerDiscordUserId,
|
|
2142
|
+
})
|
|
2143
|
+
} catch (error) {
|
|
2144
|
+
cliLogger.warn(
|
|
2145
|
+
'Background default channel creation failed:',
|
|
2146
|
+
error instanceof Error ? error.message : String(error),
|
|
2147
|
+
)
|
|
2148
|
+
}
|
|
2127
2149
|
})()
|
|
2128
2150
|
|
|
2129
2151
|
// Background: OpenCode init + slash command registration (non-blocking)
|
|
@@ -4126,6 +4148,31 @@ cli
|
|
|
4126
4148
|
},
|
|
4127
4149
|
)
|
|
4128
4150
|
|
|
4151
|
+
cli
|
|
4152
|
+
.command(
|
|
4153
|
+
'screenshare',
|
|
4154
|
+
'Share your screen via VNC tunnel. Auto-stops after 1 hour. Runs until Ctrl+C. Use tmux to run in background.',
|
|
4155
|
+
)
|
|
4156
|
+
.action(async () => {
|
|
4157
|
+
const { startScreenshare } = await import(
|
|
4158
|
+
'./commands/screenshare.js'
|
|
4159
|
+
)
|
|
4160
|
+
try {
|
|
4161
|
+
const session = await startScreenshare({
|
|
4162
|
+
sessionKey: 'cli',
|
|
4163
|
+
startedBy: 'cli',
|
|
4164
|
+
})
|
|
4165
|
+
cliLogger.log(`Screen sharing started: ${session.noVncUrl}`)
|
|
4166
|
+
cliLogger.log('Press Ctrl+C to stop')
|
|
4167
|
+
} catch (err) {
|
|
4168
|
+
cliLogger.error(
|
|
4169
|
+
'Failed to start screen share:',
|
|
4170
|
+
err instanceof Error ? err.message : String(err),
|
|
4171
|
+
)
|
|
4172
|
+
process.exit(EXIT_NO_RESTART)
|
|
4173
|
+
}
|
|
4174
|
+
})
|
|
4175
|
+
|
|
4129
4176
|
cli
|
|
4130
4177
|
.command('sqlitedb', 'Show the location of the SQLite database file')
|
|
4131
4178
|
.action(() => {
|
package/src/commands/diff.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
SILENT_MESSAGE_FLAGS,
|
|
15
15
|
} from '../discord-utils.js'
|
|
16
16
|
import { createLogger, LogPrefix } from '../logger.js'
|
|
17
|
-
import {
|
|
17
|
+
import { uploadGitDiffViaCritique } from '../critique-utils.js'
|
|
18
18
|
|
|
19
19
|
const logger = createLogger(LogPrefix.DIFF)
|
|
20
20
|
|
|
@@ -63,103 +63,29 @@ export async function handleDiffCommand({
|
|
|
63
63
|
|
|
64
64
|
await command.deferReply({ flags: SILENT_MESSAGE_FLAGS })
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
// critique --json outputs JSON on the last line: {"url":"...","id":"..."} or {"error":"..."}
|
|
78
|
-
const output = stdout || stderr
|
|
79
|
-
const lines = output.trim().split('\n')
|
|
80
|
-
const jsonLine = lines[lines.length - 1]
|
|
81
|
-
if (!jsonLine) {
|
|
82
|
-
await command.editReply({
|
|
83
|
-
content: 'No changes to show',
|
|
84
|
-
})
|
|
85
|
-
return
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
let result: { url?: string; id?: string; error?: string }
|
|
89
|
-
try {
|
|
90
|
-
result = JSON.parse(jsonLine)
|
|
91
|
-
} catch {
|
|
92
|
-
// Fallback: try to find URL in output
|
|
93
|
-
const urlMatch = output.match(/https?:\/\/critique\.work\/[^\s]+/)
|
|
94
|
-
if (urlMatch) {
|
|
95
|
-
await command.editReply({
|
|
96
|
-
content: `[diff](${urlMatch[0]})`,
|
|
97
|
-
})
|
|
98
|
-
logger.log(`Diff shared: ${urlMatch[0]}`)
|
|
99
|
-
return
|
|
100
|
-
}
|
|
101
|
-
await command.editReply({
|
|
102
|
-
content: 'No changes to show',
|
|
103
|
-
})
|
|
104
|
-
return
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (result.error || !result.url || !result.id) {
|
|
108
|
-
await command.editReply({
|
|
109
|
-
content: result.error || 'No changes to show',
|
|
110
|
-
})
|
|
111
|
-
return
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const imageUrl = `https://critique.work/og/${result.id}.png`
|
|
115
|
-
const embed = new EmbedBuilder()
|
|
116
|
-
.setTitle(title)
|
|
117
|
-
.setURL(result.url)
|
|
118
|
-
.setImage(imageUrl)
|
|
119
|
-
|
|
120
|
-
await command.editReply({
|
|
121
|
-
embeds: [embed],
|
|
122
|
-
})
|
|
123
|
-
logger.log(`Diff shared: ${result.url}`)
|
|
124
|
-
} catch (error) {
|
|
125
|
-
logger.error('[DIFF] Error:', error)
|
|
126
|
-
|
|
127
|
-
// exec error includes stdout/stderr - try to parse JSON from it
|
|
128
|
-
const execError = error as {
|
|
129
|
-
stdout?: string
|
|
130
|
-
stderr?: string
|
|
131
|
-
message?: string
|
|
132
|
-
}
|
|
133
|
-
const output = execError.stdout || execError.stderr || ''
|
|
134
|
-
|
|
135
|
-
// Check if critique output JSON even on error
|
|
136
|
-
const lines = output.trim().split('\n')
|
|
137
|
-
const jsonLine = lines[lines.length - 1]
|
|
138
|
-
if (jsonLine) {
|
|
139
|
-
try {
|
|
140
|
-
const result = JSON.parse(jsonLine) as { error?: string }
|
|
141
|
-
if (result.error) {
|
|
142
|
-
await command.editReply({
|
|
143
|
-
content: result.error,
|
|
144
|
-
})
|
|
145
|
-
return
|
|
146
|
-
}
|
|
147
|
-
} catch {
|
|
148
|
-
// not JSON, continue to generic error
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Check for common errors
|
|
153
|
-
const message = execError.message || 'Unknown error'
|
|
154
|
-
if (message.includes('command not found') || message.includes('ENOENT')) {
|
|
155
|
-
await command.editReply({
|
|
156
|
-
content: 'bunx/critique not available',
|
|
157
|
-
})
|
|
158
|
-
return
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
await command.editReply({
|
|
162
|
-
content: `Failed to generate diff: ${message.slice(0, 200)}`,
|
|
163
|
-
})
|
|
66
|
+
const projectName = path.basename(workingDirectory)
|
|
67
|
+
const title = `${projectName}: Discord /diff`
|
|
68
|
+
const result = await uploadGitDiffViaCritique({
|
|
69
|
+
title,
|
|
70
|
+
cwd: workingDirectory,
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
if (!result) {
|
|
74
|
+
await command.editReply({ content: 'No changes to show' })
|
|
75
|
+
return
|
|
164
76
|
}
|
|
77
|
+
|
|
78
|
+
if (result.error || !result.url) {
|
|
79
|
+
await command.editReply({ content: result.error || 'No changes to show' })
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const imageUrl = `https://critique.work/og/${result.id}.png`
|
|
84
|
+
const embed = new EmbedBuilder()
|
|
85
|
+
.setTitle(title)
|
|
86
|
+
.setURL(result.url)
|
|
87
|
+
.setImage(imageUrl)
|
|
88
|
+
|
|
89
|
+
await command.editReply({ embeds: [embed] })
|
|
90
|
+
logger.log(`Diff shared: ${result.url}`)
|
|
165
91
|
}
|
|
@@ -15,7 +15,7 @@ import { mergeWorktree, listBranchesByLastCommit, validateBranchRef } from '../w
|
|
|
15
15
|
import {
|
|
16
16
|
sendThreadMessage,
|
|
17
17
|
resolveWorkingDirectory,
|
|
18
|
-
|
|
18
|
+
resolveProjectDirectoryFromAutocomplete,
|
|
19
19
|
} from '../discord-utils.js'
|
|
20
20
|
import {
|
|
21
21
|
getOrCreateRuntime,
|
|
@@ -189,26 +189,10 @@ export async function handleMergeWorktreeAutocomplete({
|
|
|
189
189
|
try {
|
|
190
190
|
const focusedValue = interaction.options.getFocused()
|
|
191
191
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
//
|
|
195
|
-
|
|
196
|
-
const worktreeInfo = await getThreadWorktree(interaction.channel.id)
|
|
197
|
-
if (worktreeInfo?.project_directory) {
|
|
198
|
-
projectDirectory = worktreeInfo.project_directory
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Fallback: resolve from parent channel
|
|
203
|
-
if (!projectDirectory && interaction.channel) {
|
|
204
|
-
const textChannel = await resolveTextChannel(
|
|
205
|
-
interaction.channel as TextChannel | ThreadChannel | null,
|
|
206
|
-
)
|
|
207
|
-
if (textChannel) {
|
|
208
|
-
const channelConfig = await getChannelDirectory(textChannel.id)
|
|
209
|
-
projectDirectory = channelConfig?.directory
|
|
210
|
-
}
|
|
211
|
-
}
|
|
192
|
+
// interaction.channel can be null when the channel isn't cached
|
|
193
|
+
// (common with gateway-proxy). Use channelId which is always available
|
|
194
|
+
// from the raw interaction payload.
|
|
195
|
+
const projectDirectory = await resolveProjectDirectoryFromAutocomplete(interaction)
|
|
212
196
|
|
|
213
197
|
if (!projectDirectory) {
|
|
214
198
|
await interaction.respond([])
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
import {
|
|
22
22
|
SILENT_MESSAGE_FLAGS,
|
|
23
23
|
reactToThread,
|
|
24
|
-
|
|
24
|
+
resolveProjectDirectoryFromAutocomplete,
|
|
25
25
|
} from '../discord-utils.js'
|
|
26
26
|
import { createLogger, LogPrefix } from '../logger.js'
|
|
27
27
|
import { notifyError } from '../sentry.js'
|
|
@@ -452,16 +452,10 @@ export async function handleNewWorktreeAutocomplete({
|
|
|
452
452
|
try {
|
|
453
453
|
const focusedValue = interaction.options.getFocused()
|
|
454
454
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
)
|
|
460
|
-
if (textChannel) {
|
|
461
|
-
const channelConfig = await getChannelDirectory(textChannel.id)
|
|
462
|
-
projectDirectory = channelConfig?.directory
|
|
463
|
-
}
|
|
464
|
-
}
|
|
455
|
+
// interaction.channel can be null when the channel isn't cached
|
|
456
|
+
// (common with gateway-proxy). Use channelId which is always available
|
|
457
|
+
// from the raw interaction payload.
|
|
458
|
+
const projectDirectory = await resolveProjectDirectoryFromAutocomplete(interaction)
|
|
465
459
|
|
|
466
460
|
if (!projectDirectory) {
|
|
467
461
|
await interaction.respond([])
|
|
@@ -70,6 +70,7 @@ type PendingPermissionContext = {
|
|
|
70
70
|
permissionDirectory: string
|
|
71
71
|
thread: ThreadChannel
|
|
72
72
|
contextHash: string
|
|
73
|
+
messageId?: string
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
// Store pending permission contexts by hash.
|
|
@@ -189,11 +190,107 @@ export async function showPermissionButtons({
|
|
|
189
190
|
flags: NOTIFY_MESSAGE_FLAGS | MessageFlags.SuppressEmbeds,
|
|
190
191
|
})
|
|
191
192
|
|
|
193
|
+
context.messageId = permissionMessage.id
|
|
194
|
+
|
|
192
195
|
logger.log(`Showed permission buttons for ${permission.id}`)
|
|
193
196
|
|
|
194
197
|
return { messageId: permissionMessage.id, contextHash }
|
|
195
198
|
}
|
|
196
199
|
|
|
200
|
+
function updatePermissionMessage({
|
|
201
|
+
context,
|
|
202
|
+
status,
|
|
203
|
+
}: {
|
|
204
|
+
context: PendingPermissionContext
|
|
205
|
+
status: string
|
|
206
|
+
}): void {
|
|
207
|
+
if (!context.messageId) {
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
context.thread.messages
|
|
211
|
+
.fetch(context.messageId)
|
|
212
|
+
.then((message) => {
|
|
213
|
+
const patternStr = compactPermissionPatterns(context.permission.patterns).join(', ')
|
|
214
|
+
const externalDirLine =
|
|
215
|
+
context.permission.permission === 'external_directory'
|
|
216
|
+
? 'Agent is accessing files outside the project. [Learn more](https://opencode.ai/docs/permissions/#external-directories)\n'
|
|
217
|
+
: ''
|
|
218
|
+
return message.edit({
|
|
219
|
+
content:
|
|
220
|
+
`⚠️ **Permission Required**\n` +
|
|
221
|
+
`**Type:** \`${context.permission.permission}\`\n` +
|
|
222
|
+
externalDirLine +
|
|
223
|
+
(patternStr ? `**Pattern:** \`${patternStr}\`\n` : '') +
|
|
224
|
+
status,
|
|
225
|
+
components: [],
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
.catch((error) => {
|
|
229
|
+
logger.error('Failed to update permission message:', error)
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export async function cancelPendingPermission(threadId: string): Promise<boolean> {
|
|
234
|
+
const contexts = Array.from(pendingPermissionContexts.values()).filter((context) => {
|
|
235
|
+
return context.thread.id === threadId
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
if (contexts.length === 0) {
|
|
239
|
+
return false
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let cancelledCount = 0
|
|
243
|
+
for (const context of contexts) {
|
|
244
|
+
const pendingContext = takePendingPermissionContext(context.contextHash)
|
|
245
|
+
if (!pendingContext) {
|
|
246
|
+
continue
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const client = getOpencodeClient(pendingContext.directory)
|
|
250
|
+
if (!client) {
|
|
251
|
+
pendingPermissionContexts.set(pendingContext.contextHash, pendingContext)
|
|
252
|
+
logger.error('Failed to dismiss pending permission: OpenCode server not found')
|
|
253
|
+
continue
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const requestIds = pendingContext.requestIds.length > 0
|
|
257
|
+
? pendingContext.requestIds
|
|
258
|
+
: [pendingContext.permission.id]
|
|
259
|
+
|
|
260
|
+
const result = await Promise.all(
|
|
261
|
+
requestIds.map((requestId) => {
|
|
262
|
+
return client.permission.reply({
|
|
263
|
+
requestID: requestId,
|
|
264
|
+
directory: pendingContext.permissionDirectory,
|
|
265
|
+
reply: 'reject',
|
|
266
|
+
})
|
|
267
|
+
}),
|
|
268
|
+
).then(() => {
|
|
269
|
+
return 'ok' as const
|
|
270
|
+
}).catch((error) => {
|
|
271
|
+
pendingPermissionContexts.set(pendingContext.contextHash, pendingContext)
|
|
272
|
+
logger.error('Failed to dismiss pending permission:', error)
|
|
273
|
+
return 'error' as const
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
if (result === 'error') {
|
|
277
|
+
continue
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
updatePermissionMessage({
|
|
281
|
+
context: pendingContext,
|
|
282
|
+
status: '_Permission dismissed - user sent a new message._',
|
|
283
|
+
})
|
|
284
|
+
cancelledCount++
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (cancelledCount > 0) {
|
|
288
|
+
logger.log(`Dismissed ${cancelledCount} pending permission request(s) for thread ${threadId}`)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return cancelledCount > 0
|
|
292
|
+
}
|
|
293
|
+
|
|
197
294
|
/**
|
|
198
295
|
* Handle button click for permission.
|
|
199
296
|
*/
|
|
@@ -256,21 +353,9 @@ export async function handlePermissionButton(
|
|
|
256
353
|
}
|
|
257
354
|
})()
|
|
258
355
|
|
|
259
|
-
|
|
260
|
-
context
|
|
261
|
-
|
|
262
|
-
const externalDirLine =
|
|
263
|
-
context.permission.permission === 'external_directory'
|
|
264
|
-
? `Agent is accessing files outside the project. [Learn more](https://opencode.ai/docs/permissions/#external-directories)\n`
|
|
265
|
-
: ''
|
|
266
|
-
await interaction.editReply({
|
|
267
|
-
content:
|
|
268
|
-
`⚠️ **Permission Required**\n` +
|
|
269
|
-
`**Type:** \`${context.permission.permission}\`\n` +
|
|
270
|
-
externalDirLine +
|
|
271
|
-
(patternStr ? `**Pattern:** \`${patternStr}\`\n` : '') +
|
|
272
|
-
resultText,
|
|
273
|
-
components: [], // Remove the buttons
|
|
356
|
+
updatePermissionMessage({
|
|
357
|
+
context,
|
|
358
|
+
status: resultText,
|
|
274
359
|
})
|
|
275
360
|
|
|
276
361
|
logger.log(
|
package/src/commands/resume.ts
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
getAllThreadSessionIds,
|
|
16
16
|
} from '../database.js'
|
|
17
17
|
import { initializeOpencodeForDirectory } from '../opencode.js'
|
|
18
|
-
import { sendThreadMessage,
|
|
18
|
+
import { sendThreadMessage, resolveProjectDirectoryFromAutocomplete } from '../discord-utils.js'
|
|
19
19
|
import { collectLastAssistantParts } from '../message-formatting.js'
|
|
20
20
|
import { createLogger, LogPrefix } from '../logger.js'
|
|
21
21
|
import * as errore from 'errore'
|
|
@@ -168,17 +168,10 @@ export async function handleResumeAutocomplete({
|
|
|
168
168
|
}: AutocompleteContext): Promise<void> {
|
|
169
169
|
const focusedValue = interaction.options.getFocused()
|
|
170
170
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
interaction.channel as TextChannel | ThreadChannel | null,
|
|
176
|
-
)
|
|
177
|
-
if (textChannel) {
|
|
178
|
-
const channelConfig = await getChannelDirectory(textChannel.id)
|
|
179
|
-
projectDirectory = channelConfig?.directory
|
|
180
|
-
}
|
|
181
|
-
}
|
|
171
|
+
// interaction.channel can be null when the channel isn't cached
|
|
172
|
+
// (common with gateway-proxy). Use channelId which is always available
|
|
173
|
+
// from the raw interaction payload.
|
|
174
|
+
const projectDirectory = await resolveProjectDirectoryFromAutocomplete(interaction)
|
|
182
175
|
|
|
183
176
|
if (!projectDirectory) {
|
|
184
177
|
await interaction.respond([])
|