kimaki 0.4.78 → 0.4.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/anthropic-auth-plugin.js +628 -0
- package/dist/channel-management.js +2 -2
- package/dist/cli.js +316 -129
- package/dist/commands/action-buttons.js +1 -1
- package/dist/commands/login.js +634 -277
- package/dist/commands/model.js +91 -6
- package/dist/commands/paginated-select.js +57 -0
- package/dist/commands/resume.js +2 -2
- package/dist/commands/tasks.js +205 -0
- package/dist/commands/undo-redo.js +80 -18
- package/dist/context-awareness-plugin.js +347 -0
- package/dist/database.js +103 -7
- package/dist/db.js +39 -1
- package/dist/discord-bot.js +42 -19
- package/dist/discord-urls.js +11 -0
- package/dist/discord-ws-proxy.js +350 -0
- package/dist/discord-ws-proxy.test.js +500 -0
- package/dist/errors.js +1 -1
- package/dist/gateway-session.js +163 -0
- package/dist/hrana-server.js +114 -4
- package/dist/interaction-handler.js +30 -7
- package/dist/ipc-tools-plugin.js +186 -0
- package/dist/message-preprocessing.js +56 -11
- package/dist/onboarding-welcome.js +1 -1
- package/dist/opencode-interrupt-plugin.js +133 -75
- package/dist/opencode-plugin.js +12 -389
- package/dist/opencode.js +59 -5
- package/dist/parse-permission-rules.test.js +117 -0
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
- package/dist/session-handler/thread-session-runtime.js +68 -29
- package/dist/startup-time.e2e.test.js +295 -0
- package/dist/store.js +1 -0
- package/dist/system-message.js +3 -1
- package/dist/task-runner.js +7 -3
- package/dist/task-schedule.js +12 -0
- package/dist/thread-message-queue.e2e.test.js +13 -1
- package/dist/undo-redo.e2e.test.js +166 -0
- package/dist/utils.js +4 -1
- package/dist/voice-attachment.js +34 -0
- package/dist/voice-handler.js +11 -9
- package/dist/voice-message.e2e.test.js +78 -0
- package/dist/voice.test.js +31 -0
- package/package.json +12 -7
- package/skills/egaki/SKILL.md +80 -15
- package/skills/errore/SKILL.md +13 -0
- package/skills/lintcn/SKILL.md +749 -0
- package/skills/npm-package/SKILL.md +17 -3
- package/skills/spiceflow/SKILL.md +14 -0
- package/skills/zele/SKILL.md +9 -0
- package/src/anthropic-auth-plugin.ts +732 -0
- package/src/channel-management.ts +2 -2
- package/src/cli.ts +354 -132
- package/src/commands/action-buttons.ts +1 -0
- package/src/commands/login.ts +836 -337
- package/src/commands/model.ts +102 -7
- package/src/commands/paginated-select.ts +81 -0
- package/src/commands/resume.ts +6 -1
- package/src/commands/tasks.ts +293 -0
- package/src/commands/undo-redo.ts +87 -20
- package/src/context-awareness-plugin.ts +469 -0
- package/src/database.ts +138 -7
- package/src/db.ts +40 -1
- package/src/discord-bot.ts +46 -19
- package/src/discord-urls.ts +12 -0
- package/src/errors.ts +1 -1
- package/src/hrana-server.ts +124 -3
- package/src/interaction-handler.ts +41 -9
- package/src/ipc-tools-plugin.ts +228 -0
- package/src/message-preprocessing.ts +82 -11
- package/src/onboarding-welcome.ts +1 -1
- package/src/opencode-interrupt-plugin.ts +164 -91
- package/src/opencode-plugin.ts +13 -483
- package/src/opencode.ts +60 -5
- package/src/parse-permission-rules.test.ts +127 -0
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
- package/src/session-handler/thread-runtime-state.ts +4 -1
- package/src/session-handler/thread-session-runtime.ts +82 -20
- package/src/startup-time.e2e.test.ts +372 -0
- package/src/store.ts +8 -0
- package/src/system-message.ts +10 -1
- package/src/task-runner.ts +9 -22
- package/src/task-schedule.ts +15 -0
- package/src/thread-message-queue.e2e.test.ts +14 -1
- package/src/undo-redo.e2e.test.ts +207 -0
- package/src/utils.ts +7 -0
- package/src/voice-attachment.ts +51 -0
- package/src/voice-handler.ts +15 -7
- package/src/voice-message.e2e.test.ts +95 -0
- package/src/voice.test.ts +36 -0
- package/src/onboarding-tutorial-plugin.ts +0 -93
package/src/database.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { getPrisma, closePrisma } from './db.js'
|
|
6
6
|
import type { Prisma, session_events, BotMode, VerbosityLevel, WorktreeStatus, ChannelType as PrismaChannelType } from './generated/client.js'
|
|
7
|
+
import crypto from 'node:crypto'
|
|
7
8
|
|
|
8
9
|
import { store } from './store.js'
|
|
9
10
|
import { createLogger, LogPrefix } from './logger.js'
|
|
@@ -209,6 +210,65 @@ export async function listScheduledTasks({
|
|
|
209
210
|
return rows.map((row) => toScheduledTask(row))
|
|
210
211
|
}
|
|
211
212
|
|
|
213
|
+
export async function getScheduledTask(
|
|
214
|
+
taskId: number,
|
|
215
|
+
): Promise<ScheduledTask | null> {
|
|
216
|
+
const prisma = await getPrisma()
|
|
217
|
+
const row = await prisma.scheduled_tasks.findUnique({
|
|
218
|
+
where: { id: taskId },
|
|
219
|
+
})
|
|
220
|
+
return row ? toScheduledTask(row) : null
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export async function updateScheduledTask({
|
|
224
|
+
taskId,
|
|
225
|
+
payloadJson,
|
|
226
|
+
promptPreview,
|
|
227
|
+
scheduleKind,
|
|
228
|
+
runAt,
|
|
229
|
+
cronExpr,
|
|
230
|
+
timezone,
|
|
231
|
+
nextRunAt,
|
|
232
|
+
}: {
|
|
233
|
+
taskId: number
|
|
234
|
+
payloadJson: string
|
|
235
|
+
promptPreview: string
|
|
236
|
+
scheduleKind?: ScheduledTaskScheduleKind
|
|
237
|
+
runAt?: Date | null
|
|
238
|
+
cronExpr?: string | null
|
|
239
|
+
timezone?: string | null
|
|
240
|
+
nextRunAt?: Date
|
|
241
|
+
}): Promise<boolean> {
|
|
242
|
+
const prisma = await getPrisma()
|
|
243
|
+
const data: Record<string, unknown> = {
|
|
244
|
+
payload_json: payloadJson,
|
|
245
|
+
prompt_preview: promptPreview,
|
|
246
|
+
}
|
|
247
|
+
if (scheduleKind !== undefined) {
|
|
248
|
+
data.schedule_kind = scheduleKind
|
|
249
|
+
}
|
|
250
|
+
if (runAt !== undefined) {
|
|
251
|
+
data.run_at = runAt
|
|
252
|
+
}
|
|
253
|
+
if (cronExpr !== undefined) {
|
|
254
|
+
data.cron_expr = cronExpr
|
|
255
|
+
}
|
|
256
|
+
if (timezone !== undefined) {
|
|
257
|
+
data.timezone = timezone
|
|
258
|
+
}
|
|
259
|
+
if (nextRunAt !== undefined) {
|
|
260
|
+
data.next_run_at = nextRunAt
|
|
261
|
+
}
|
|
262
|
+
const result = await prisma.scheduled_tasks.updateMany({
|
|
263
|
+
where: {
|
|
264
|
+
id: taskId,
|
|
265
|
+
status: 'planned',
|
|
266
|
+
},
|
|
267
|
+
data,
|
|
268
|
+
})
|
|
269
|
+
return result.count > 0
|
|
270
|
+
}
|
|
271
|
+
|
|
212
272
|
export async function cancelScheduledTask(taskId: number): Promise<boolean> {
|
|
213
273
|
const prisma = await getPrisma()
|
|
214
274
|
const result = await prisma.scheduled_tasks.updateMany({
|
|
@@ -1173,6 +1233,7 @@ export async function getBotTokenWithMode(): Promise<
|
|
|
1173
1233
|
| {
|
|
1174
1234
|
appId: string
|
|
1175
1235
|
token: string
|
|
1236
|
+
gatewayToken: string
|
|
1176
1237
|
mode: BotMode
|
|
1177
1238
|
clientId: string | null
|
|
1178
1239
|
clientSecret: string | null
|
|
@@ -1191,9 +1252,11 @@ export async function getBotTokenWithMode(): Promise<
|
|
|
1191
1252
|
if (!row) {
|
|
1192
1253
|
return undefined
|
|
1193
1254
|
}
|
|
1255
|
+
const gatewayToken = await ensureServiceAuthToken({ appId: row.app_id })
|
|
1256
|
+
const serviceParts = splitServiceAuthToken({ token: gatewayToken })
|
|
1194
1257
|
const mode: BotMode = row.bot_mode === 'gateway' ? 'gateway' : 'self_hosted'
|
|
1195
|
-
const token = (mode === 'gateway' &&
|
|
1196
|
-
?
|
|
1258
|
+
const token = (mode === 'gateway' && serviceParts)
|
|
1259
|
+
? gatewayToken
|
|
1197
1260
|
: row.token
|
|
1198
1261
|
// Always reset discordBaseUrl on every read so a mode switch within
|
|
1199
1262
|
// the same process (e.g. DB has gateway row but user proceeds self-hosted)
|
|
@@ -1201,27 +1264,90 @@ export async function getBotTokenWithMode(): Promise<
|
|
|
1201
1264
|
const discordBaseUrl = (mode === 'gateway' && row.proxy_url)
|
|
1202
1265
|
? row.proxy_url
|
|
1203
1266
|
: 'https://discord.com'
|
|
1204
|
-
store.setState({ discordBaseUrl })
|
|
1267
|
+
store.setState({ discordBaseUrl, gatewayToken })
|
|
1205
1268
|
return {
|
|
1206
1269
|
appId: row.app_id,
|
|
1207
1270
|
token,
|
|
1271
|
+
gatewayToken,
|
|
1208
1272
|
mode,
|
|
1209
|
-
clientId: row.client_id,
|
|
1210
|
-
clientSecret: row.client_secret,
|
|
1273
|
+
clientId: serviceParts?.clientId || row.client_id,
|
|
1274
|
+
clientSecret: serviceParts?.clientSecret || row.client_secret,
|
|
1211
1275
|
proxyUrl: row.proxy_url,
|
|
1212
1276
|
}
|
|
1213
1277
|
}
|
|
1214
1278
|
|
|
1279
|
+
function splitServiceAuthToken({ token }: { token: string }): { clientId: string; clientSecret: string } | null {
|
|
1280
|
+
const separatorIndex = token.indexOf(':')
|
|
1281
|
+
if (separatorIndex <= 0 || separatorIndex >= token.length - 1) {
|
|
1282
|
+
return null
|
|
1283
|
+
}
|
|
1284
|
+
return {
|
|
1285
|
+
clientId: token.slice(0, separatorIndex),
|
|
1286
|
+
clientSecret: token.slice(separatorIndex + 1),
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function createServiceCredentials(): { clientId: string; clientSecret: string } {
|
|
1291
|
+
return {
|
|
1292
|
+
clientId: crypto.randomUUID(),
|
|
1293
|
+
clientSecret: crypto.randomBytes(32).toString('hex'),
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
export async function ensureServiceAuthToken({
|
|
1298
|
+
appId,
|
|
1299
|
+
preferredGatewayToken,
|
|
1300
|
+
}: {
|
|
1301
|
+
appId: string
|
|
1302
|
+
preferredGatewayToken?: string
|
|
1303
|
+
}): Promise<string> {
|
|
1304
|
+
const prisma = await getPrisma()
|
|
1305
|
+
const row = await prisma.bot_tokens.findUnique({
|
|
1306
|
+
where: { app_id: appId },
|
|
1307
|
+
})
|
|
1308
|
+
if (!row) {
|
|
1309
|
+
throw new Error(`Bot token row not found for app_id ${appId}`)
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
const preferred = preferredGatewayToken
|
|
1313
|
+
? splitServiceAuthToken({ token: preferredGatewayToken })
|
|
1314
|
+
: null
|
|
1315
|
+
const existing = (row.client_id && row.client_secret)
|
|
1316
|
+
? { clientId: row.client_id, clientSecret: row.client_secret }
|
|
1317
|
+
: null
|
|
1318
|
+
const fromStoredToken = splitServiceAuthToken({ token: row.token })
|
|
1319
|
+
const resolved = preferred || existing || fromStoredToken || createServiceCredentials()
|
|
1320
|
+
|
|
1321
|
+
if (row.client_id !== resolved.clientId || row.client_secret !== resolved.clientSecret) {
|
|
1322
|
+
await prisma.bot_tokens.update({
|
|
1323
|
+
where: { app_id: appId },
|
|
1324
|
+
data: {
|
|
1325
|
+
client_id: resolved.clientId,
|
|
1326
|
+
client_secret: resolved.clientSecret,
|
|
1327
|
+
},
|
|
1328
|
+
})
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
return `${resolved.clientId}:${resolved.clientSecret}`
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1215
1334
|
/**
|
|
1216
1335
|
* Store a bot token.
|
|
1217
1336
|
*/
|
|
1218
1337
|
export async function setBotToken(appId: string, token: string): Promise<void> {
|
|
1219
1338
|
const prisma = await getPrisma()
|
|
1339
|
+
const generated = createServiceCredentials()
|
|
1220
1340
|
await prisma.bot_tokens.upsert({
|
|
1221
1341
|
where: { app_id: appId },
|
|
1222
|
-
create: {
|
|
1342
|
+
create: {
|
|
1343
|
+
app_id: appId,
|
|
1344
|
+
token,
|
|
1345
|
+
client_id: generated.clientId,
|
|
1346
|
+
client_secret: generated.clientSecret,
|
|
1347
|
+
},
|
|
1223
1348
|
update: { token },
|
|
1224
1349
|
})
|
|
1350
|
+
await ensureServiceAuthToken({ appId })
|
|
1225
1351
|
}
|
|
1226
1352
|
|
|
1227
1353
|
export type { BotMode }
|
|
@@ -1250,11 +1376,16 @@ export async function setBotMode({
|
|
|
1250
1376
|
client_secret: clientSecret ?? null,
|
|
1251
1377
|
proxy_url: proxyUrl ?? null,
|
|
1252
1378
|
}
|
|
1379
|
+
const createToken = (clientId && clientSecret) ? `${clientId}:${clientSecret}` : ''
|
|
1253
1380
|
await prisma.bot_tokens.upsert({
|
|
1254
1381
|
where: { app_id: appId },
|
|
1255
|
-
create: { app_id: appId, token:
|
|
1382
|
+
create: { app_id: appId, token: createToken, ...data },
|
|
1256
1383
|
update: data,
|
|
1257
1384
|
})
|
|
1385
|
+
await ensureServiceAuthToken({
|
|
1386
|
+
appId,
|
|
1387
|
+
preferredGatewayToken: (clientId && clientSecret) ? `${clientId}:${clientSecret}` : undefined,
|
|
1388
|
+
})
|
|
1258
1389
|
}
|
|
1259
1390
|
|
|
1260
1391
|
|
package/src/db.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import fs from 'node:fs'
|
|
6
6
|
import path from 'node:path'
|
|
7
|
+
import crypto from 'node:crypto'
|
|
7
8
|
import { PrismaLibSql } from '@prisma/adapter-libsql'
|
|
8
9
|
import { PrismaClient, Prisma } from './generated/client.js'
|
|
9
10
|
import { getDataDir } from './config.js'
|
|
@@ -60,6 +61,14 @@ function getDbUrl(): string {
|
|
|
60
61
|
return `file:${dbPath}`
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
function getDbAuthToken(): string | undefined {
|
|
65
|
+
const token = process.env.KIMAKI_DB_AUTH_TOKEN
|
|
66
|
+
if (!token) {
|
|
67
|
+
return undefined
|
|
68
|
+
}
|
|
69
|
+
return token
|
|
70
|
+
}
|
|
71
|
+
|
|
63
72
|
async function initializePrisma(): Promise<PrismaClient> {
|
|
64
73
|
const dbUrl = getDbUrl()
|
|
65
74
|
const isFileMode = dbUrl.startsWith('file:')
|
|
@@ -78,7 +87,11 @@ async function initializePrisma(): Promise<PrismaClient> {
|
|
|
78
87
|
|
|
79
88
|
dbLogger.log(`Opening database via: ${dbUrl}`)
|
|
80
89
|
|
|
81
|
-
const
|
|
90
|
+
const dbAuthToken = getDbAuthToken()
|
|
91
|
+
const adapter = new PrismaLibSql({
|
|
92
|
+
url: dbUrl,
|
|
93
|
+
...(dbAuthToken && { authToken: dbAuthToken }),
|
|
94
|
+
})
|
|
82
95
|
const prisma = new PrismaClient({ adapter })
|
|
83
96
|
|
|
84
97
|
try {
|
|
@@ -224,6 +237,32 @@ async function migrateSchema(prisma: PrismaClient): Promise<void> {
|
|
|
224
237
|
}
|
|
225
238
|
}
|
|
226
239
|
|
|
240
|
+
// Migration: ensure every bot row has service auth credentials.
|
|
241
|
+
// These credentials are used for local/internet control-plane auth.
|
|
242
|
+
try {
|
|
243
|
+
const botRows = await prisma.bot_tokens.findMany({
|
|
244
|
+
select: {
|
|
245
|
+
app_id: true,
|
|
246
|
+
client_id: true,
|
|
247
|
+
client_secret: true,
|
|
248
|
+
},
|
|
249
|
+
})
|
|
250
|
+
for (const botRow of botRows) {
|
|
251
|
+
if (botRow.client_id && botRow.client_secret) {
|
|
252
|
+
continue
|
|
253
|
+
}
|
|
254
|
+
await prisma.bot_tokens.update({
|
|
255
|
+
where: { app_id: botRow.app_id },
|
|
256
|
+
data: {
|
|
257
|
+
client_id: crypto.randomUUID(),
|
|
258
|
+
client_secret: crypto.randomBytes(32).toString('hex'),
|
|
259
|
+
},
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
} catch {
|
|
263
|
+
// Defensive migration only; ignore if table shape is not ready yet.
|
|
264
|
+
}
|
|
265
|
+
|
|
227
266
|
}
|
|
228
267
|
|
|
229
268
|
/**
|
package/src/discord-bot.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
initDatabase,
|
|
7
7
|
closeDatabase,
|
|
8
8
|
getThreadWorktree,
|
|
9
|
+
getThreadSession,
|
|
9
10
|
createPendingWorktree,
|
|
10
11
|
setWorktreeReady,
|
|
11
12
|
setWorktreeError,
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
splitMarkdownForDiscord,
|
|
27
28
|
sendThreadMessage,
|
|
28
29
|
SILENT_MESSAGE_FLAGS,
|
|
30
|
+
NOTIFY_MESSAGE_FLAGS,
|
|
29
31
|
reactToThread,
|
|
30
32
|
stripMentions,
|
|
31
33
|
hasKimakiBotPermission,
|
|
@@ -40,6 +42,7 @@ import {
|
|
|
40
42
|
getTextAttachments,
|
|
41
43
|
resolveMentions,
|
|
42
44
|
} from './message-formatting.js'
|
|
45
|
+
import { isVoiceAttachment } from './voice-attachment.js'
|
|
43
46
|
import {
|
|
44
47
|
preprocessExistingThreadMessage,
|
|
45
48
|
preprocessNewThreadMessage,
|
|
@@ -71,7 +74,7 @@ import {
|
|
|
71
74
|
import { runShellCommand } from './commands/run-command.js'
|
|
72
75
|
import { registerInteractionHandler } from './interaction-handler.js'
|
|
73
76
|
import { getDiscordRestApiUrl } from './discord-urls.js'
|
|
74
|
-
import { stopHranaServer } from './hrana-server.js'
|
|
77
|
+
import { markDiscordGatewayReady, stopHranaServer } from './hrana-server.js'
|
|
75
78
|
import { notifyError } from './sentry.js'
|
|
76
79
|
import { flushDebouncedProcessCallbacks } from './debounced-process-flush.js'
|
|
77
80
|
import { startRuntimeIdleSweeper } from './runtime-idle-sweeper.js'
|
|
@@ -276,6 +279,7 @@ export async function startDiscordBot({
|
|
|
276
279
|
}
|
|
277
280
|
|
|
278
281
|
voiceLogger.log('[READY] Bot is ready')
|
|
282
|
+
markDiscordGatewayReady()
|
|
279
283
|
|
|
280
284
|
registerInteractionHandler({ discordClient: c, appId: currentAppId })
|
|
281
285
|
registerVoiceStateHandler({ discordClient: c, appId: currentAppId })
|
|
@@ -299,7 +303,7 @@ export async function startDiscordBot({
|
|
|
299
303
|
}
|
|
300
304
|
})().catch((error) => {
|
|
301
305
|
discordLogger.warn(
|
|
302
|
-
`Background guild channel scan failed: ${error instanceof Error ? error.
|
|
306
|
+
`Background guild channel scan failed: ${error instanceof Error ? error.stack : String(error)}`,
|
|
303
307
|
)
|
|
304
308
|
})
|
|
305
309
|
}
|
|
@@ -409,6 +413,9 @@ export async function startDiscordBot({
|
|
|
409
413
|
const cliInjectedModel = isCliInjectedPrompt
|
|
410
414
|
? promptMarker?.model
|
|
411
415
|
: undefined
|
|
416
|
+
const cliInjectedPermissions = isCliInjectedPrompt
|
|
417
|
+
? promptMarker?.permissions
|
|
418
|
+
: undefined
|
|
412
419
|
|
|
413
420
|
// Always ignore our own messages (unless CLI-injected prompt above).
|
|
414
421
|
// Without this, assigning the Kimaki role to the bot itself would loop.
|
|
@@ -496,6 +503,19 @@ export async function startDiscordBot({
|
|
|
496
503
|
const thread = channel as ThreadChannel
|
|
497
504
|
discordLogger.log(`Message in thread ${thread.name} (${thread.id})`)
|
|
498
505
|
|
|
506
|
+
// Only respond in threads kimaki knows about (has a session row in DB)
|
|
507
|
+
// or where the bot is explicitly @mentioned. This prevents the bot from
|
|
508
|
+
// hijacking user-created threads in project channels. (GitHub #84)
|
|
509
|
+
const hasExistingSession = await getThreadSession(thread.id)
|
|
510
|
+
const botMentioned =
|
|
511
|
+
discordClient.user && message.mentions.has(discordClient.user.id)
|
|
512
|
+
if (!hasExistingSession && !botMentioned && !isCliInjectedPrompt) {
|
|
513
|
+
discordLogger.log(
|
|
514
|
+
`Ignoring thread ${thread.id}: no existing session and bot not mentioned`,
|
|
515
|
+
)
|
|
516
|
+
return
|
|
517
|
+
}
|
|
518
|
+
|
|
499
519
|
const parent = thread.parent as TextChannel | null
|
|
500
520
|
let projectDirectory: string | undefined
|
|
501
521
|
if (parent) {
|
|
@@ -518,7 +538,7 @@ export async function startDiscordBot({
|
|
|
518
538
|
if (worktreeInfo.status === 'error') {
|
|
519
539
|
await message.reply({
|
|
520
540
|
content: `❌ Worktree creation failed: ${(worktreeInfo.error_message || '').slice(0, 1900)}`,
|
|
521
|
-
flags:
|
|
541
|
+
flags: NOTIFY_MESSAGE_FLAGS,
|
|
522
542
|
})
|
|
523
543
|
return
|
|
524
544
|
}
|
|
@@ -536,7 +556,7 @@ export async function startDiscordBot({
|
|
|
536
556
|
discordLogger.error(`Directory does not exist: ${projectDirectory}`)
|
|
537
557
|
await message.reply({
|
|
538
558
|
content: `✗ Directory does not exist: ${JSON.stringify(projectDirectory).slice(0, 1900)}`,
|
|
539
|
-
flags:
|
|
559
|
+
flags: NOTIFY_MESSAGE_FLAGS,
|
|
540
560
|
})
|
|
541
561
|
return
|
|
542
562
|
}
|
|
@@ -563,8 +583,8 @@ export async function startDiscordBot({
|
|
|
563
583
|
}
|
|
564
584
|
}
|
|
565
585
|
|
|
566
|
-
const hasVoiceAttachment = message.attachments.some((
|
|
567
|
-
return
|
|
586
|
+
const hasVoiceAttachment = message.attachments.some((attachment) => {
|
|
587
|
+
return isVoiceAttachment(attachment)
|
|
568
588
|
})
|
|
569
589
|
|
|
570
590
|
if (!projectDirectory) {
|
|
@@ -622,6 +642,7 @@ export async function startDiscordBot({
|
|
|
622
642
|
appId: currentAppId,
|
|
623
643
|
agent: cliInjectedAgent,
|
|
624
644
|
model: cliInjectedModel,
|
|
645
|
+
permissions: cliInjectedPermissions,
|
|
625
646
|
sessionStartSource: sessionStartSource
|
|
626
647
|
? {
|
|
627
648
|
scheduleKind: sessionStartSource.scheduleKind,
|
|
@@ -687,7 +708,7 @@ export async function startDiscordBot({
|
|
|
687
708
|
discordLogger.error(`Directory does not exist: ${projectDirectory}`)
|
|
688
709
|
await message.reply({
|
|
689
710
|
content: `✗ Directory does not exist: ${JSON.stringify(projectDirectory).slice(0, 1900)}`,
|
|
690
|
-
flags:
|
|
711
|
+
flags: NOTIFY_MESSAGE_FLAGS,
|
|
691
712
|
})
|
|
692
713
|
return
|
|
693
714
|
}
|
|
@@ -708,9 +729,9 @@ export async function startDiscordBot({
|
|
|
708
729
|
}
|
|
709
730
|
}
|
|
710
731
|
|
|
711
|
-
const hasVoice = message.attachments.some((
|
|
712
|
-
|
|
713
|
-
)
|
|
732
|
+
const hasVoice = message.attachments.some((attachment) => {
|
|
733
|
+
return isVoiceAttachment(attachment)
|
|
734
|
+
})
|
|
714
735
|
|
|
715
736
|
const baseThreadName = hasVoice
|
|
716
737
|
? 'Voice Message'
|
|
@@ -767,7 +788,7 @@ export async function startDiscordBot({
|
|
|
767
788
|
})
|
|
768
789
|
await thread.send({
|
|
769
790
|
content: `⚠️ Failed to create worktree: ${errMsg}\nUsing main project directory instead.`,
|
|
770
|
-
flags:
|
|
791
|
+
flags: NOTIFY_MESSAGE_FLAGS,
|
|
771
792
|
})
|
|
772
793
|
} else {
|
|
773
794
|
await setWorktreeReady({
|
|
@@ -824,7 +845,7 @@ export async function startDiscordBot({
|
|
|
824
845
|
).slice(0, 1900)
|
|
825
846
|
await message.reply({
|
|
826
847
|
content: `Error: ${errMsg}`,
|
|
827
|
-
flags:
|
|
848
|
+
flags: NOTIFY_MESSAGE_FLAGS,
|
|
828
849
|
})
|
|
829
850
|
} catch (sendError) {
|
|
830
851
|
voiceLogger.error(
|
|
@@ -855,7 +876,7 @@ export async function startDiscordBot({
|
|
|
855
876
|
.catch((error) => {
|
|
856
877
|
discordLogger.warn(
|
|
857
878
|
`[THREAD_CREATE] Failed to fetch starter message for thread ${thread.id}:`,
|
|
858
|
-
error instanceof Error ? error.
|
|
879
|
+
error instanceof Error ? error.stack : String(error),
|
|
859
880
|
)
|
|
860
881
|
return null
|
|
861
882
|
})
|
|
@@ -915,7 +936,7 @@ export async function startDiscordBot({
|
|
|
915
936
|
)
|
|
916
937
|
await thread.send({
|
|
917
938
|
content: `✗ Directory does not exist: ${JSON.stringify(projectDirectory).slice(0, 1900)}`,
|
|
918
|
-
flags:
|
|
939
|
+
flags: NOTIFY_MESSAGE_FLAGS,
|
|
919
940
|
})
|
|
920
941
|
return
|
|
921
942
|
}
|
|
@@ -958,11 +979,11 @@ export async function startDiscordBot({
|
|
|
958
979
|
})
|
|
959
980
|
await (worktreeStatusMessage?.edit({
|
|
960
981
|
content: `⚠️ Failed to create worktree: ${worktreeResult.message}\nUsing main project directory instead.`,
|
|
961
|
-
flags:
|
|
982
|
+
flags: NOTIFY_MESSAGE_FLAGS,
|
|
962
983
|
}) ||
|
|
963
984
|
thread.send({
|
|
964
985
|
content: `⚠️ Failed to create worktree: ${worktreeResult.message}\nUsing main project directory instead.`,
|
|
965
|
-
flags:
|
|
986
|
+
flags: NOTIFY_MESSAGE_FLAGS,
|
|
966
987
|
}))
|
|
967
988
|
return projectDirectory
|
|
968
989
|
}
|
|
@@ -1013,6 +1034,7 @@ export async function startDiscordBot({
|
|
|
1013
1034
|
appId: currentAppId,
|
|
1014
1035
|
agent: marker.agent,
|
|
1015
1036
|
model: marker.model,
|
|
1037
|
+
permissions: marker.permissions,
|
|
1016
1038
|
mode: 'opencode',
|
|
1017
1039
|
sessionStartSource: botThreadStartSource
|
|
1018
1040
|
? {
|
|
@@ -1033,7 +1055,7 @@ export async function startDiscordBot({
|
|
|
1033
1055
|
).slice(0, 1900)
|
|
1034
1056
|
await thread.send({
|
|
1035
1057
|
content: `Error: ${errMsg}`,
|
|
1036
|
-
flags:
|
|
1058
|
+
flags: NOTIFY_MESSAGE_FLAGS,
|
|
1037
1059
|
})
|
|
1038
1060
|
} catch (sendError) {
|
|
1039
1061
|
voiceLogger.error(
|
|
@@ -1050,7 +1072,12 @@ export async function startDiscordBot({
|
|
|
1050
1072
|
disposeRuntime(thread.id)
|
|
1051
1073
|
})
|
|
1052
1074
|
|
|
1053
|
-
|
|
1075
|
+
// Skip login if the caller already connected the client (e.g. cli.ts logs in
|
|
1076
|
+
// before calling startDiscordBot). Calling login() again destroys the existing
|
|
1077
|
+
// WebSocket (close code 1000) and triggers a spurious ShardReconnecting event.
|
|
1078
|
+
if (!discordClient.isReady()) {
|
|
1079
|
+
await discordClient.login(token)
|
|
1080
|
+
}
|
|
1054
1081
|
|
|
1055
1082
|
startHeapMonitor()
|
|
1056
1083
|
const stopTaskRunner = startTaskRunner({ token })
|
|
@@ -1072,7 +1099,7 @@ export async function startDiscordBot({
|
|
|
1072
1099
|
await flushDebouncedProcessCallbacks().catch((error) => {
|
|
1073
1100
|
discordLogger.warn(
|
|
1074
1101
|
'Failed to flush debounced process callbacks:',
|
|
1075
|
-
error instanceof Error ? error.
|
|
1102
|
+
error instanceof Error ? error.stack : String(error),
|
|
1076
1103
|
)
|
|
1077
1104
|
})
|
|
1078
1105
|
|
package/src/discord-urls.ts
CHANGED
|
@@ -56,6 +56,18 @@ export function createDiscordRest(token: string): REST {
|
|
|
56
56
|
return new REST({ api: getDiscordRestApiUrl() }).setToken(token)
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Returns the internet-reachable base URL for this kimaki instance.
|
|
61
|
+
* When KIMAKI_INTERNET_REACHABLE_URL is set (e.g. "https://my-kimaki.fly.dev"),
|
|
62
|
+
* kimaki binds the hrana server to 0.0.0.0 and exposes a /kimaki/wake endpoint
|
|
63
|
+
* so the gateway-proxy can wake this instance. Discord traffic still flows
|
|
64
|
+
* through the normal path (gateway-proxy in gateway mode, direct in self-hosted).
|
|
65
|
+
* Returns null when not set (kimaki only reachable on localhost).
|
|
66
|
+
*/
|
|
67
|
+
export function getInternetReachableBaseUrl(): string | null {
|
|
68
|
+
return process.env['KIMAKI_INTERNET_REACHABLE_URL'] || null
|
|
69
|
+
}
|
|
70
|
+
|
|
59
71
|
/**
|
|
60
72
|
* Derive an HTTPS REST base URL from a WebSocket gateway URL.
|
|
61
73
|
* Swaps wss→https and ws→http. Used for gateway mode where the
|
package/src/errors.ts
CHANGED
|
@@ -160,7 +160,7 @@ export class NotFastForwardError extends createTaggedError({
|
|
|
160
160
|
export class ConflictingFilesError extends createTaggedError({
|
|
161
161
|
name: 'ConflictingFilesError',
|
|
162
162
|
message:
|
|
163
|
-
'Cannot merge: $target worktree has uncommitted changes in overlapping files',
|
|
163
|
+
'Cannot merge: $target worktree has uncommitted changes in overlapping files. Commit changes in main worktree first, then run `/merge-worktree` again.',
|
|
164
164
|
}) {}
|
|
165
165
|
|
|
166
166
|
export class PushError extends createTaggedError({
|