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/dist/cli.js
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
// Handles interactive setup, Discord OAuth, slash command registration,
|
|
4
4
|
// project channel creation, and launching the bot with opencode integration.
|
|
5
5
|
import { goke } from 'goke';
|
|
6
|
+
import { z } from 'zod';
|
|
6
7
|
import { intro, outro, text, password, note, cancel, isCancel, confirm, log, multiselect, select, spinner, } from '@clack/prompts';
|
|
7
8
|
import { deduplicateByKey, generateBotInstallUrl, generateDiscordInstallUrlForBot, KIMAKI_GATEWAY_APP_ID, KIMAKI_WEBSITE_URL, abbreviatePath, } from './utils.js';
|
|
8
9
|
import { getChannelsWithDescriptions, createDiscordClient, initDatabase, getChannelDirectory, startDiscordBot, initializeOpencodeForDirectory, ensureKimakiCategory, createProjectChannels, createDefaultKimakiChannel, } from './discord-bot.js';
|
|
9
|
-
import { getBotTokenWithMode, setBotToken, setBotMode, setChannelDirectory, findChannelsByDirectory, getThreadSession, getThreadIdBySessionId, getSessionEventSnapshot, getPrisma, createScheduledTask, listScheduledTasks, cancelScheduledTask, getSessionStartSourcesBySessionIds, } from './database.js';
|
|
10
|
+
import { getBotTokenWithMode, ensureServiceAuthToken, setBotToken, setBotMode, setChannelDirectory, findChannelsByDirectory, getThreadSession, getThreadIdBySessionId, getSessionEventSnapshot, getPrisma, createScheduledTask, listScheduledTasks, cancelScheduledTask, getScheduledTask, updateScheduledTask, getSessionStartSourcesBySessionIds, } from './database.js';
|
|
10
11
|
import { ShareMarkdown } from './markdown.js';
|
|
11
12
|
import { parseSessionSearchPattern, findFirstSessionSearchHit, buildSessionSearchSnippet, getPartSearchTexts, } from './session-search.js';
|
|
12
13
|
import { formatWorktreeName } from './commands/new-worktree.js';
|
|
@@ -16,7 +17,7 @@ import { buildOpencodeEventLogLine } from './session-handler/opencode-session-ev
|
|
|
16
17
|
import { selectResolvedCommand } from './opencode-command.js';
|
|
17
18
|
import yaml from 'js-yaml';
|
|
18
19
|
import { Events, ChannelType, ActivityType, Routes, SlashCommandBuilder, AttachmentBuilder, } from 'discord.js';
|
|
19
|
-
import { createDiscordRest, discordApiUrl, getDiscordRestApiUrl, getGatewayProxyRestBaseUrl } from './discord-urls.js';
|
|
20
|
+
import { createDiscordRest, discordApiUrl, getDiscordRestApiUrl, getGatewayProxyRestBaseUrl, getInternetReachableBaseUrl } from './discord-urls.js';
|
|
20
21
|
import crypto from 'node:crypto';
|
|
21
22
|
import path from 'node:path';
|
|
22
23
|
import fs from 'node:fs';
|
|
@@ -31,7 +32,7 @@ import { execAsync } from './worktrees.js';
|
|
|
31
32
|
import { backgroundUpgradeKimaki, upgrade, getCurrentVersion, } from './upgrade.js';
|
|
32
33
|
import { startHranaServer } from './hrana-server.js';
|
|
33
34
|
import { startIpcPolling, stopIpcPolling } from './ipc-polling.js';
|
|
34
|
-
import {
|
|
35
|
+
import { getPromptPreview, parseSendAtValue, parseScheduledTaskPayload, serializeScheduledTaskPayload, } from './task-schedule.js';
|
|
35
36
|
const cliLogger = createLogger(LogPrefix.CLI);
|
|
36
37
|
// Gateway bot mode constants.
|
|
37
38
|
// KIMAKI_GATEWAY_APP_ID is the Discord Application ID of the gateway bot.
|
|
@@ -128,7 +129,37 @@ async function sendDiscordMessageWithOptionalAttachment({ channelId, prompt, bot
|
|
|
128
129
|
fs.mkdirSync(tmpDir, { recursive: true });
|
|
129
130
|
}
|
|
130
131
|
const tmpFile = path.join(tmpDir, `prompt-${Date.now()}.md`);
|
|
131
|
-
|
|
132
|
+
// Wrap long lines so the file is readable in Discord's preview
|
|
133
|
+
// (Discord doesn't wrap text in file attachments)
|
|
134
|
+
const wrappedPrompt = prompt
|
|
135
|
+
.split('\n')
|
|
136
|
+
.flatMap((line) => {
|
|
137
|
+
if (line.length <= 120) {
|
|
138
|
+
return [line];
|
|
139
|
+
}
|
|
140
|
+
const wrapped = [];
|
|
141
|
+
let remaining = line;
|
|
142
|
+
const maxCol = 120;
|
|
143
|
+
// Only soft-break at a space if it's reasonably close to maxCol,
|
|
144
|
+
// otherwise hard-break to avoid tiny fragments from early spaces
|
|
145
|
+
const minSoftBreak = 90;
|
|
146
|
+
while (remaining.length > maxCol) {
|
|
147
|
+
const lastSpace = remaining.lastIndexOf(' ', maxCol);
|
|
148
|
+
const useSoftBreak = lastSpace >= minSoftBreak;
|
|
149
|
+
const breakAt = useSoftBreak ? lastSpace : maxCol;
|
|
150
|
+
wrapped.push(remaining.slice(0, breakAt));
|
|
151
|
+
// Only consume the separator space on soft breaks
|
|
152
|
+
remaining = useSoftBreak
|
|
153
|
+
? remaining.slice(breakAt + 1)
|
|
154
|
+
: remaining.slice(breakAt);
|
|
155
|
+
}
|
|
156
|
+
if (remaining.length > 0) {
|
|
157
|
+
wrapped.push(remaining);
|
|
158
|
+
}
|
|
159
|
+
return wrapped;
|
|
160
|
+
})
|
|
161
|
+
.join('\n');
|
|
162
|
+
fs.writeFileSync(tmpFile, wrappedPrompt);
|
|
132
163
|
try {
|
|
133
164
|
const formData = new FormData();
|
|
134
165
|
formData.append('payload_json', JSON.stringify({
|
|
@@ -336,7 +367,7 @@ async function ensureCommandAvailable({ name, envPathKey, installUnix, installWi
|
|
|
336
367
|
}
|
|
337
368
|
catch (error) {
|
|
338
369
|
cliLogger.log(`Failed to install ${name}`);
|
|
339
|
-
cliLogger.error('Installation error:', error instanceof Error ? error.
|
|
370
|
+
cliLogger.error('Installation error:', error instanceof Error ? error.stack : String(error));
|
|
340
371
|
process.exit(EXIT_NO_RESTART);
|
|
341
372
|
}
|
|
342
373
|
// After install, re-check PATH first (install script may have added it)
|
|
@@ -383,16 +414,18 @@ async function ensureCommandAvailable({ name, envPathKey, installUnix, installWi
|
|
|
383
414
|
}
|
|
384
415
|
// Run opencode upgrade in the background so the user always has the latest version.
|
|
385
416
|
// Spawn caffeinate on macOS to prevent system sleep while bot is running.
|
|
386
|
-
//
|
|
417
|
+
// Uses -w to watch the parent PID so caffeinate self-terminates if kimaki
|
|
418
|
+
// exits for any reason (SIGTERM, crash, process.exit, supervisor stop).
|
|
387
419
|
function startCaffeinate() {
|
|
388
420
|
if (process.platform !== 'darwin') {
|
|
389
421
|
return;
|
|
390
422
|
}
|
|
391
423
|
try {
|
|
392
|
-
const proc = spawn('caffeinate', ['-i'], {
|
|
424
|
+
const proc = spawn('caffeinate', ['-i', '-w', String(process.pid)], {
|
|
393
425
|
stdio: 'ignore',
|
|
394
426
|
detached: false,
|
|
395
427
|
});
|
|
428
|
+
proc.unref();
|
|
396
429
|
proc.on('error', (err) => {
|
|
397
430
|
cliLogger.warn('Failed to start caffeinate:', err.message);
|
|
398
431
|
});
|
|
@@ -455,18 +488,23 @@ async function deleteLegacyGlobalCommands({ rest, appId, commandNames, }) {
|
|
|
455
488
|
}
|
|
456
489
|
}
|
|
457
490
|
catch (error) {
|
|
458
|
-
cliLogger.warn(`COMMANDS: Could not clean legacy global commands: ${error instanceof Error ? error.
|
|
491
|
+
cliLogger.warn(`COMMANDS: Could not clean legacy global commands: ${error instanceof Error ? error.stack : String(error)}`);
|
|
459
492
|
}
|
|
460
493
|
}
|
|
494
|
+
// Discord slash command descriptions must be 1-100 chars.
|
|
495
|
+
// Truncate to 100 so @sapphire/shapeshift validation never throws.
|
|
496
|
+
function truncateCommandDescription(description) {
|
|
497
|
+
return description.slice(0, 100);
|
|
498
|
+
}
|
|
461
499
|
async function registerCommands({ token, appId, guildIds, userCommands = [], agents = [], }) {
|
|
462
500
|
const commands = [
|
|
463
501
|
new SlashCommandBuilder()
|
|
464
502
|
.setName('resume')
|
|
465
|
-
.setDescription('Resume an existing OpenCode session')
|
|
503
|
+
.setDescription(truncateCommandDescription('Resume an existing OpenCode session'))
|
|
466
504
|
.addStringOption((option) => {
|
|
467
505
|
option
|
|
468
506
|
.setName('session')
|
|
469
|
-
.setDescription('The session to resume')
|
|
507
|
+
.setDescription(truncateCommandDescription('The session to resume'))
|
|
470
508
|
.setRequired(true)
|
|
471
509
|
.setAutocomplete(true);
|
|
472
510
|
return option;
|
|
@@ -475,18 +513,18 @@ async function registerCommands({ token, appId, guildIds, userCommands = [], age
|
|
|
475
513
|
.toJSON(),
|
|
476
514
|
new SlashCommandBuilder()
|
|
477
515
|
.setName('new-session')
|
|
478
|
-
.setDescription('Start a new OpenCode session')
|
|
516
|
+
.setDescription(truncateCommandDescription('Start a new OpenCode session'))
|
|
479
517
|
.addStringOption((option) => {
|
|
480
518
|
option
|
|
481
519
|
.setName('prompt')
|
|
482
|
-
.setDescription('Prompt content for the session')
|
|
520
|
+
.setDescription(truncateCommandDescription('Prompt content for the session'))
|
|
483
521
|
.setRequired(true);
|
|
484
522
|
return option;
|
|
485
523
|
})
|
|
486
524
|
.addStringOption((option) => {
|
|
487
525
|
option
|
|
488
526
|
.setName('files')
|
|
489
|
-
.setDescription('Files to mention (comma or space separated; autocomplete)')
|
|
527
|
+
.setDescription(truncateCommandDescription('Files to mention (comma or space separated; autocomplete)'))
|
|
490
528
|
.setAutocomplete(true)
|
|
491
529
|
.setMaxLength(6000);
|
|
492
530
|
return option;
|
|
@@ -494,7 +532,7 @@ async function registerCommands({ token, appId, guildIds, userCommands = [], age
|
|
|
494
532
|
.addStringOption((option) => {
|
|
495
533
|
option
|
|
496
534
|
.setName('agent')
|
|
497
|
-
.setDescription('Agent to use for this session')
|
|
535
|
+
.setDescription(truncateCommandDescription('Agent to use for this session'))
|
|
498
536
|
.setAutocomplete(true);
|
|
499
537
|
return option;
|
|
500
538
|
})
|
|
@@ -502,18 +540,18 @@ async function registerCommands({ token, appId, guildIds, userCommands = [], age
|
|
|
502
540
|
.toJSON(),
|
|
503
541
|
new SlashCommandBuilder()
|
|
504
542
|
.setName('new-worktree')
|
|
505
|
-
.setDescription('Create a git worktree branch from origin/HEAD (or main). Optionally pick a base branch.')
|
|
543
|
+
.setDescription(truncateCommandDescription('Create a git worktree branch from origin/HEAD (or main). Optionally pick a base branch.'))
|
|
506
544
|
.addStringOption((option) => {
|
|
507
545
|
option
|
|
508
546
|
.setName('name')
|
|
509
|
-
.setDescription('Name for worktree (optional in threads - uses thread name)')
|
|
547
|
+
.setDescription(truncateCommandDescription('Name for worktree (optional in threads - uses thread name)'))
|
|
510
548
|
.setRequired(false);
|
|
511
549
|
return option;
|
|
512
550
|
})
|
|
513
551
|
.addStringOption((option) => {
|
|
514
552
|
option
|
|
515
553
|
.setName('base-branch')
|
|
516
|
-
.setDescription('Branch to create the worktree from (default: origin/HEAD or main)')
|
|
554
|
+
.setDescription(truncateCommandDescription('Branch to create the worktree from (default: origin/HEAD or main)'))
|
|
517
555
|
.setRequired(false)
|
|
518
556
|
.setAutocomplete(true);
|
|
519
557
|
return option;
|
|
@@ -522,11 +560,11 @@ async function registerCommands({ token, appId, guildIds, userCommands = [], age
|
|
|
522
560
|
.toJSON(),
|
|
523
561
|
new SlashCommandBuilder()
|
|
524
562
|
.setName('merge-worktree')
|
|
525
|
-
.setDescription('Squash-merge worktree into
|
|
563
|
+
.setDescription(truncateCommandDescription('Squash-merge worktree into default branch. Aborts if main has uncommitted changes.'))
|
|
526
564
|
.addStringOption((option) => {
|
|
527
565
|
option
|
|
528
566
|
.setName('target-branch')
|
|
529
|
-
.setDescription('Branch to merge into (default: origin/HEAD or main)')
|
|
567
|
+
.setDescription(truncateCommandDescription('Branch to merge into (default: origin/HEAD or main)'))
|
|
530
568
|
.setRequired(false)
|
|
531
569
|
.setAutocomplete(true);
|
|
532
570
|
return option;
|
|
@@ -535,26 +573,36 @@ async function registerCommands({ token, appId, guildIds, userCommands = [], age
|
|
|
535
573
|
.toJSON(),
|
|
536
574
|
new SlashCommandBuilder()
|
|
537
575
|
.setName('toggle-worktrees')
|
|
538
|
-
.setDescription('Toggle automatic git worktree creation for new sessions in this channel')
|
|
576
|
+
.setDescription(truncateCommandDescription('Toggle automatic git worktree creation for new sessions in this channel'))
|
|
539
577
|
.setDMPermission(false)
|
|
540
578
|
.toJSON(),
|
|
541
579
|
new SlashCommandBuilder()
|
|
542
580
|
.setName('worktrees')
|
|
543
|
-
.setDescription('List all active worktree sessions')
|
|
581
|
+
.setDescription(truncateCommandDescription('List all active worktree sessions'))
|
|
582
|
+
.setDMPermission(false)
|
|
583
|
+
.toJSON(),
|
|
584
|
+
new SlashCommandBuilder()
|
|
585
|
+
.setName('tasks')
|
|
586
|
+
.setDescription(truncateCommandDescription('List scheduled tasks created via send --send-at'))
|
|
587
|
+
.addBooleanOption((option) => {
|
|
588
|
+
return option
|
|
589
|
+
.setName('all')
|
|
590
|
+
.setDescription(truncateCommandDescription('Include completed, cancelled, and failed tasks'));
|
|
591
|
+
})
|
|
544
592
|
.setDMPermission(false)
|
|
545
593
|
.toJSON(),
|
|
546
594
|
new SlashCommandBuilder()
|
|
547
595
|
.setName('toggle-mention-mode')
|
|
548
|
-
.setDescription('Toggle mention-only mode (bot only responds when @mentioned)')
|
|
596
|
+
.setDescription(truncateCommandDescription('Toggle mention-only mode (bot only responds when @mentioned)'))
|
|
549
597
|
.setDMPermission(false)
|
|
550
598
|
.toJSON(),
|
|
551
599
|
new SlashCommandBuilder()
|
|
552
600
|
.setName('add-project')
|
|
553
|
-
.setDescription('Create Discord channels for a project. Use `npx kimaki project add` for unlisted projects')
|
|
601
|
+
.setDescription(truncateCommandDescription('Create Discord channels for a project. Use `npx kimaki project add` for unlisted projects'))
|
|
554
602
|
.addStringOption((option) => {
|
|
555
603
|
option
|
|
556
604
|
.setName('project')
|
|
557
|
-
.setDescription('Recent OpenCode projects. Use `npx kimaki project add` if not listed')
|
|
605
|
+
.setDescription(truncateCommandDescription('Recent OpenCode projects. Use `npx kimaki project add` if not listed'))
|
|
558
606
|
.setRequired(true)
|
|
559
607
|
.setAutocomplete(true);
|
|
560
608
|
return option;
|
|
@@ -563,11 +611,11 @@ async function registerCommands({ token, appId, guildIds, userCommands = [], age
|
|
|
563
611
|
.toJSON(),
|
|
564
612
|
new SlashCommandBuilder()
|
|
565
613
|
.setName('remove-project')
|
|
566
|
-
.setDescription('Remove Discord channels for a project')
|
|
614
|
+
.setDescription(truncateCommandDescription('Remove Discord channels for a project'))
|
|
567
615
|
.addStringOption((option) => {
|
|
568
616
|
option
|
|
569
617
|
.setName('project')
|
|
570
|
-
.setDescription('Select a project to remove')
|
|
618
|
+
.setDescription(truncateCommandDescription('Select a project to remove'))
|
|
571
619
|
.setRequired(true)
|
|
572
620
|
.setAutocomplete(true);
|
|
573
621
|
return option;
|
|
@@ -576,11 +624,11 @@ async function registerCommands({ token, appId, guildIds, userCommands = [], age
|
|
|
576
624
|
.toJSON(),
|
|
577
625
|
new SlashCommandBuilder()
|
|
578
626
|
.setName('create-new-project')
|
|
579
|
-
.setDescription('Create a new project folder, initialize git, and start a session')
|
|
627
|
+
.setDescription(truncateCommandDescription('Create a new project folder, initialize git, and start a session'))
|
|
580
628
|
.addStringOption((option) => {
|
|
581
629
|
option
|
|
582
630
|
.setName('name')
|
|
583
|
-
.setDescription('Name for the new project folder')
|
|
631
|
+
.setDescription(truncateCommandDescription('Name for the new project folder'))
|
|
584
632
|
.setRequired(true);
|
|
585
633
|
return option;
|
|
586
634
|
})
|
|
@@ -588,66 +636,66 @@ async function registerCommands({ token, appId, guildIds, userCommands = [], age
|
|
|
588
636
|
.toJSON(),
|
|
589
637
|
new SlashCommandBuilder()
|
|
590
638
|
.setName('abort')
|
|
591
|
-
.setDescription('Abort the current OpenCode request in this thread')
|
|
639
|
+
.setDescription(truncateCommandDescription('Abort the current OpenCode request in this thread'))
|
|
592
640
|
.setDMPermission(false)
|
|
593
641
|
.toJSON(),
|
|
594
642
|
new SlashCommandBuilder()
|
|
595
643
|
.setName('compact')
|
|
596
|
-
.setDescription('Compact the session context by summarizing conversation history')
|
|
644
|
+
.setDescription(truncateCommandDescription('Compact the session context by summarizing conversation history'))
|
|
597
645
|
.setDMPermission(false)
|
|
598
646
|
.toJSON(),
|
|
599
647
|
new SlashCommandBuilder()
|
|
600
648
|
.setName('stop')
|
|
601
|
-
.setDescription('Abort the current OpenCode request in this thread')
|
|
649
|
+
.setDescription(truncateCommandDescription('Abort the current OpenCode request in this thread'))
|
|
602
650
|
.setDMPermission(false)
|
|
603
651
|
.toJSON(),
|
|
604
652
|
new SlashCommandBuilder()
|
|
605
653
|
.setName('share')
|
|
606
|
-
.setDescription('Share the current session as a public URL')
|
|
654
|
+
.setDescription(truncateCommandDescription('Share the current session as a public URL'))
|
|
607
655
|
.setDMPermission(false)
|
|
608
656
|
.toJSON(),
|
|
609
657
|
new SlashCommandBuilder()
|
|
610
658
|
.setName('diff')
|
|
611
|
-
.setDescription('Show git diff as a shareable URL')
|
|
659
|
+
.setDescription(truncateCommandDescription('Show git diff as a shareable URL'))
|
|
612
660
|
.setDMPermission(false)
|
|
613
661
|
.toJSON(),
|
|
614
662
|
new SlashCommandBuilder()
|
|
615
663
|
.setName('fork')
|
|
616
|
-
.setDescription('Fork the session from a past user message')
|
|
664
|
+
.setDescription(truncateCommandDescription('Fork the session from a past user message'))
|
|
617
665
|
.setDMPermission(false)
|
|
618
666
|
.toJSON(),
|
|
619
667
|
new SlashCommandBuilder()
|
|
620
668
|
.setName('model')
|
|
621
|
-
.setDescription('Set the preferred model for this channel or session')
|
|
669
|
+
.setDescription(truncateCommandDescription('Set the preferred model for this channel or session'))
|
|
622
670
|
.setDMPermission(false)
|
|
623
671
|
.toJSON(),
|
|
624
672
|
new SlashCommandBuilder()
|
|
625
673
|
.setName('model-variant')
|
|
626
|
-
.setDescription('Quickly change the thinking level variant for the current model')
|
|
674
|
+
.setDescription(truncateCommandDescription('Quickly change the thinking level variant for the current model'))
|
|
627
675
|
.setDMPermission(false)
|
|
628
676
|
.toJSON(),
|
|
629
677
|
new SlashCommandBuilder()
|
|
630
678
|
.setName('unset-model-override')
|
|
631
|
-
.setDescription('Remove model override and use default instead')
|
|
679
|
+
.setDescription(truncateCommandDescription('Remove model override and use default instead'))
|
|
632
680
|
.setDMPermission(false)
|
|
633
681
|
.toJSON(),
|
|
634
682
|
new SlashCommandBuilder()
|
|
635
683
|
.setName('login')
|
|
636
|
-
.setDescription('Authenticate with an AI provider (OAuth or API key). Use this instead of /connect')
|
|
684
|
+
.setDescription(truncateCommandDescription('Authenticate with an AI provider (OAuth or API key). Use this instead of /connect'))
|
|
637
685
|
.setDMPermission(false)
|
|
638
686
|
.toJSON(),
|
|
639
687
|
new SlashCommandBuilder()
|
|
640
688
|
.setName('agent')
|
|
641
|
-
.setDescription('Set the preferred agent for this channel or session')
|
|
689
|
+
.setDescription(truncateCommandDescription('Set the preferred agent for this channel or session'))
|
|
642
690
|
.setDMPermission(false)
|
|
643
691
|
.toJSON(),
|
|
644
692
|
new SlashCommandBuilder()
|
|
645
693
|
.setName('queue')
|
|
646
|
-
.setDescription('Queue a message to be sent after the current response finishes')
|
|
694
|
+
.setDescription(truncateCommandDescription('Queue a message to be sent after the current response finishes'))
|
|
647
695
|
.addStringOption((option) => {
|
|
648
696
|
option
|
|
649
697
|
.setName('message')
|
|
650
|
-
.setDescription('The message to queue')
|
|
698
|
+
.setDescription(truncateCommandDescription('The message to queue'))
|
|
651
699
|
.setRequired(true);
|
|
652
700
|
return option;
|
|
653
701
|
})
|
|
@@ -655,16 +703,16 @@ async function registerCommands({ token, appId, guildIds, userCommands = [], age
|
|
|
655
703
|
.toJSON(),
|
|
656
704
|
new SlashCommandBuilder()
|
|
657
705
|
.setName('clear-queue')
|
|
658
|
-
.setDescription('Clear all queued messages in this thread')
|
|
706
|
+
.setDescription(truncateCommandDescription('Clear all queued messages in this thread'))
|
|
659
707
|
.setDMPermission(false)
|
|
660
708
|
.toJSON(),
|
|
661
709
|
new SlashCommandBuilder()
|
|
662
710
|
.setName('queue-command')
|
|
663
|
-
.setDescription('Queue a user command to run after the current response finishes')
|
|
711
|
+
.setDescription(truncateCommandDescription('Queue a user command to run after the current response finishes'))
|
|
664
712
|
.addStringOption((option) => {
|
|
665
713
|
option
|
|
666
714
|
.setName('command')
|
|
667
|
-
.setDescription('The command to run')
|
|
715
|
+
.setDescription(truncateCommandDescription('The command to run'))
|
|
668
716
|
.setRequired(true)
|
|
669
717
|
.setAutocomplete(true);
|
|
670
718
|
return option;
|
|
@@ -672,7 +720,7 @@ async function registerCommands({ token, appId, guildIds, userCommands = [], age
|
|
|
672
720
|
.addStringOption((option) => {
|
|
673
721
|
option
|
|
674
722
|
.setName('arguments')
|
|
675
|
-
.setDescription('Arguments to pass to the command')
|
|
723
|
+
.setDescription(truncateCommandDescription('Arguments to pass to the command'))
|
|
676
724
|
.setRequired(false);
|
|
677
725
|
return option;
|
|
678
726
|
})
|
|
@@ -680,31 +728,31 @@ async function registerCommands({ token, appId, guildIds, userCommands = [], age
|
|
|
680
728
|
.toJSON(),
|
|
681
729
|
new SlashCommandBuilder()
|
|
682
730
|
.setName('undo')
|
|
683
|
-
.setDescription('Undo the last assistant message (revert file changes)')
|
|
731
|
+
.setDescription(truncateCommandDescription('Undo the last assistant message (revert file changes)'))
|
|
684
732
|
.setDMPermission(false)
|
|
685
733
|
.toJSON(),
|
|
686
734
|
new SlashCommandBuilder()
|
|
687
735
|
.setName('redo')
|
|
688
|
-
.setDescription('Redo previously undone changes')
|
|
736
|
+
.setDescription(truncateCommandDescription('Redo previously undone changes'))
|
|
689
737
|
.setDMPermission(false)
|
|
690
738
|
.toJSON(),
|
|
691
739
|
new SlashCommandBuilder()
|
|
692
740
|
.setName('verbosity')
|
|
693
|
-
.setDescription('Set output verbosity for this channel')
|
|
741
|
+
.setDescription(truncateCommandDescription('Set output verbosity for this channel'))
|
|
694
742
|
.setDMPermission(false)
|
|
695
743
|
.toJSON(),
|
|
696
744
|
new SlashCommandBuilder()
|
|
697
745
|
.setName('restart-opencode-server')
|
|
698
|
-
.setDescription('Restart the shared opencode server (fixes state/auth/plugins)')
|
|
746
|
+
.setDescription(truncateCommandDescription('Restart the shared opencode server (fixes state/auth/plugins)'))
|
|
699
747
|
.setDMPermission(false)
|
|
700
748
|
.toJSON(),
|
|
701
749
|
new SlashCommandBuilder()
|
|
702
750
|
.setName('run-shell-command')
|
|
703
|
-
.setDescription('Run a shell command in the project directory. Tip: prefix messages with ! as shortcut')
|
|
751
|
+
.setDescription(truncateCommandDescription('Run a shell command in the project directory. Tip: prefix messages with ! as shortcut'))
|
|
704
752
|
.addStringOption((option) => {
|
|
705
753
|
option
|
|
706
754
|
.setName('command')
|
|
707
|
-
.setDescription('Command to run')
|
|
755
|
+
.setDescription(truncateCommandDescription('Command to run'))
|
|
708
756
|
.setRequired(true);
|
|
709
757
|
return option;
|
|
710
758
|
})
|
|
@@ -712,37 +760,37 @@ async function registerCommands({ token, appId, guildIds, userCommands = [], age
|
|
|
712
760
|
.toJSON(),
|
|
713
761
|
new SlashCommandBuilder()
|
|
714
762
|
.setName('context-usage')
|
|
715
|
-
.setDescription('Show token usage and context window percentage for this session')
|
|
763
|
+
.setDescription(truncateCommandDescription('Show token usage and context window percentage for this session'))
|
|
716
764
|
.setDMPermission(false)
|
|
717
765
|
.toJSON(),
|
|
718
766
|
new SlashCommandBuilder()
|
|
719
767
|
.setName('session-id')
|
|
720
|
-
.setDescription('Show current session ID and opencode attach command for this thread')
|
|
768
|
+
.setDescription(truncateCommandDescription('Show current session ID and opencode attach command for this thread'))
|
|
721
769
|
.setDMPermission(false)
|
|
722
770
|
.toJSON(),
|
|
723
771
|
new SlashCommandBuilder()
|
|
724
772
|
.setName('upgrade-and-restart')
|
|
725
|
-
.setDescription('Upgrade kimaki to the latest version and restart the bot')
|
|
773
|
+
.setDescription(truncateCommandDescription('Upgrade kimaki to the latest version and restart the bot'))
|
|
726
774
|
.setDMPermission(false)
|
|
727
775
|
.toJSON(),
|
|
728
776
|
new SlashCommandBuilder()
|
|
729
777
|
.setName('transcription-key')
|
|
730
|
-
.setDescription('Set API key for voice message transcription (OpenAI or Gemini)')
|
|
778
|
+
.setDescription(truncateCommandDescription('Set API key for voice message transcription (OpenAI or Gemini)'))
|
|
731
779
|
.setDMPermission(false)
|
|
732
780
|
.toJSON(),
|
|
733
781
|
new SlashCommandBuilder()
|
|
734
782
|
.setName('mcp')
|
|
735
|
-
.setDescription('List and manage MCP servers for this project')
|
|
783
|
+
.setDescription(truncateCommandDescription('List and manage MCP servers for this project'))
|
|
736
784
|
.setDMPermission(false)
|
|
737
785
|
.toJSON(),
|
|
738
786
|
new SlashCommandBuilder()
|
|
739
787
|
.setName('screenshare')
|
|
740
|
-
.setDescription('Start screen sharing via VNC tunnel (auto-stops after 1 hour)')
|
|
788
|
+
.setDescription(truncateCommandDescription('Start screen sharing via VNC tunnel (auto-stops after 1 hour)'))
|
|
741
789
|
.setDMPermission(false)
|
|
742
790
|
.toJSON(),
|
|
743
791
|
new SlashCommandBuilder()
|
|
744
792
|
.setName('screenshare-stop')
|
|
745
|
-
.setDescription('Stop screen sharing')
|
|
793
|
+
.setDescription(truncateCommandDescription('Stop screen sharing'))
|
|
746
794
|
.setDMPermission(false)
|
|
747
795
|
.toJSON(),
|
|
748
796
|
];
|
|
@@ -780,11 +828,11 @@ async function registerCommands({ token, appId, guildIds, userCommands = [], age
|
|
|
780
828
|
});
|
|
781
829
|
commands.push(new SlashCommandBuilder()
|
|
782
830
|
.setName(commandName)
|
|
783
|
-
.setDescription(description
|
|
831
|
+
.setDescription(truncateCommandDescription(description))
|
|
784
832
|
.addStringOption((option) => {
|
|
785
833
|
option
|
|
786
834
|
.setName('arguments')
|
|
787
|
-
.setDescription('Arguments to pass to the command')
|
|
835
|
+
.setDescription(truncateCommandDescription('Arguments to pass to the command'))
|
|
788
836
|
.setRequired(false);
|
|
789
837
|
return option;
|
|
790
838
|
})
|
|
@@ -813,7 +861,7 @@ async function registerCommands({ token, appId, guildIds, userCommands = [], age
|
|
|
813
861
|
});
|
|
814
862
|
commands.push(new SlashCommandBuilder()
|
|
815
863
|
.setName(commandName)
|
|
816
|
-
.setDescription(description)
|
|
864
|
+
.setDescription(truncateCommandDescription(description))
|
|
817
865
|
.setDMPermission(false)
|
|
818
866
|
.toJSON());
|
|
819
867
|
}
|
|
@@ -1005,7 +1053,7 @@ async function ensureDefaultChannelsWithWelcome({ guilds, discordClient, appId,
|
|
|
1005
1053
|
}
|
|
1006
1054
|
}
|
|
1007
1055
|
catch (error) {
|
|
1008
|
-
cliLogger.warn(`Failed to create default kimaki channel in ${guild.name}: ${error instanceof Error ? error.
|
|
1056
|
+
cliLogger.warn(`Failed to create default kimaki channel in ${guild.name}: ${error instanceof Error ? error.stack : String(error)}`);
|
|
1009
1057
|
}
|
|
1010
1058
|
}
|
|
1011
1059
|
return created;
|
|
@@ -1035,14 +1083,14 @@ async function backgroundInit({ currentDir, token, appId, guildIds, }) {
|
|
|
1035
1083
|
.command.list({ directory: currentDir })
|
|
1036
1084
|
.then((r) => r.data || [])
|
|
1037
1085
|
.catch((error) => {
|
|
1038
|
-
cliLogger.warn('Failed to load user commands during background init:', error instanceof Error ? error.
|
|
1086
|
+
cliLogger.warn('Failed to load user commands during background init:', error instanceof Error ? error.stack : String(error));
|
|
1039
1087
|
return [];
|
|
1040
1088
|
}),
|
|
1041
1089
|
getClient()
|
|
1042
1090
|
.app.agents({ directory: currentDir })
|
|
1043
1091
|
.then((r) => r.data || [])
|
|
1044
1092
|
.catch((error) => {
|
|
1045
|
-
cliLogger.warn('Failed to load agents during background init:', error instanceof Error ? error.
|
|
1093
|
+
cliLogger.warn('Failed to load agents during background init:', error instanceof Error ? error.stack : String(error));
|
|
1046
1094
|
return [];
|
|
1047
1095
|
}),
|
|
1048
1096
|
]);
|
|
@@ -1050,7 +1098,7 @@ async function backgroundInit({ currentDir, token, appId, guildIds, }) {
|
|
|
1050
1098
|
cliLogger.log('Slash commands registered!');
|
|
1051
1099
|
}
|
|
1052
1100
|
catch (error) {
|
|
1053
|
-
cliLogger.error('Background init failed:', error instanceof Error ? error.
|
|
1101
|
+
cliLogger.error('Background init failed:', error instanceof Error ? error.stack : String(error));
|
|
1054
1102
|
void notifyError(error, 'Background init failed');
|
|
1055
1103
|
}
|
|
1056
1104
|
}
|
|
@@ -1166,6 +1214,7 @@ async function resolveCredentials({ forceRestartOnboarding, forceGateway, gatewa
|
|
|
1166
1214
|
clientId,
|
|
1167
1215
|
clientSecret,
|
|
1168
1216
|
gatewayCallbackUrl,
|
|
1217
|
+
reachableUrl: getInternetReachableBaseUrl() || undefined,
|
|
1169
1218
|
});
|
|
1170
1219
|
if (oauthUrlResult instanceof Error) {
|
|
1171
1220
|
throw oauthUrlResult;
|
|
@@ -1319,32 +1368,35 @@ async function run({ restartOnboarding, addChannels, useWorktrees, enableVoiceCh
|
|
|
1319
1368
|
startCaffeinate();
|
|
1320
1369
|
const forceRestartOnboarding = Boolean(restartOnboarding);
|
|
1321
1370
|
const forceGateway = Boolean(gateway);
|
|
1322
|
-
// Step 0: Ensure required CLI tools are installed (OpenCode + Bun)
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
'
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1371
|
+
// Step 0: Ensure required CLI tools are installed (OpenCode + Bun).
|
|
1372
|
+
// Run checks in parallel since they're independent `which` calls.
|
|
1373
|
+
await Promise.all([
|
|
1374
|
+
ensureCommandAvailable({
|
|
1375
|
+
name: 'opencode',
|
|
1376
|
+
envPathKey: 'OPENCODE_PATH',
|
|
1377
|
+
installUnix: 'curl -fsSL https://opencode.ai/install | bash',
|
|
1378
|
+
installWindows: 'irm https://opencode.ai/install.ps1 | iex',
|
|
1379
|
+
possiblePathsUnix: [
|
|
1380
|
+
'~/.local/bin/opencode',
|
|
1381
|
+
'~/.opencode/bin/opencode',
|
|
1382
|
+
'/usr/local/bin/opencode',
|
|
1383
|
+
'/opt/opencode/bin/opencode',
|
|
1384
|
+
],
|
|
1385
|
+
possiblePathsWindows: [
|
|
1386
|
+
'~\\.local\\bin\\opencode.exe',
|
|
1387
|
+
'~\\AppData\\Local\\opencode\\opencode.exe',
|
|
1388
|
+
'~\\.opencode\\bin\\opencode.exe',
|
|
1389
|
+
],
|
|
1390
|
+
}),
|
|
1391
|
+
ensureCommandAvailable({
|
|
1392
|
+
name: 'bun',
|
|
1393
|
+
envPathKey: 'BUN_PATH',
|
|
1394
|
+
installUnix: 'curl -fsSL https://bun.sh/install | bash',
|
|
1395
|
+
installWindows: 'irm bun.sh/install.ps1 | iex',
|
|
1396
|
+
possiblePathsUnix: ['~/.bun/bin/bun', '/usr/local/bin/bun'],
|
|
1397
|
+
possiblePathsWindows: ['~\\.bun\\bin\\bun.exe'],
|
|
1398
|
+
}),
|
|
1399
|
+
]);
|
|
1348
1400
|
backgroundUpgradeKimaki();
|
|
1349
1401
|
// Start in-process Hrana server before database init. Required for the bot
|
|
1350
1402
|
// process because it serves as both the DB server and the single-instance
|
|
@@ -1352,6 +1404,7 @@ async function run({ restartOnboarding, addChannels, useWorktrees, enableVoiceCh
|
|
|
1352
1404
|
// don't work. CLI subcommands skip the server and use file: directly.
|
|
1353
1405
|
const hranaResult = await startHranaServer({
|
|
1354
1406
|
dbPath: path.join(getDataDir(), 'discord-sessions.db'),
|
|
1407
|
+
bindAll: getInternetReachableBaseUrl() !== null,
|
|
1355
1408
|
});
|
|
1356
1409
|
if (hranaResult instanceof Error) {
|
|
1357
1410
|
cliLogger.error('Failed to start hrana server:', hranaResult.message);
|
|
@@ -1364,6 +1417,13 @@ async function run({ restartOnboarding, addChannels, useWorktrees, enableVoiceCh
|
|
|
1364
1417
|
forceGateway,
|
|
1365
1418
|
gatewayCallbackUrl,
|
|
1366
1419
|
});
|
|
1420
|
+
const gatewayToken = await ensureServiceAuthToken({
|
|
1421
|
+
appId,
|
|
1422
|
+
preferredGatewayToken: isGatewayMode ? token : undefined,
|
|
1423
|
+
});
|
|
1424
|
+
// Always set service auth token so local and internet control-plane paths
|
|
1425
|
+
// share one auth model (/kimaki/wake and future service endpoints).
|
|
1426
|
+
store.setState({ gatewayToken });
|
|
1367
1427
|
// In gateway mode, ensure REST calls route through the gateway proxy.
|
|
1368
1428
|
// getBotTokenWithMode() sets this for saved-credential paths, but the fresh
|
|
1369
1429
|
// onboarding path returns directly without going through getBotTokenWithMode(),
|
|
@@ -1373,6 +1433,30 @@ async function run({ restartOnboarding, addChannels, useWorktrees, enableVoiceCh
|
|
|
1373
1433
|
if (isGatewayMode) {
|
|
1374
1434
|
store.setState({ discordBaseUrl: KIMAKI_GATEWAY_PROXY_REST_BASE_URL });
|
|
1375
1435
|
}
|
|
1436
|
+
// When KIMAKI_INTERNET_REACHABLE_URL is set, the hrana server exposes
|
|
1437
|
+
// a /kimaki/wake endpoint for the gateway-proxy to wake this instance and
|
|
1438
|
+
// wait until discord.js is connected. Keep Discord traffic on the normal
|
|
1439
|
+
// configured base URL (gateway-proxy in gateway mode).
|
|
1440
|
+
if (getInternetReachableBaseUrl()) {
|
|
1441
|
+
cliLogger.log('Internet-reachable mode: enabling /kimaki/wake endpoint on hrana server');
|
|
1442
|
+
}
|
|
1443
|
+
// Start OpenCode server as early as possible — non-blocking.
|
|
1444
|
+
// All dependencies are met (dataDir, lockPort, gatewayToken, hranaUrl set).
|
|
1445
|
+
// Runs in parallel with last_used_at update, skipChannelSetup check, and
|
|
1446
|
+
// Discord Gateway login so cold start is not blocked by OpenCode spawn.
|
|
1447
|
+
const currentDir = process.cwd();
|
|
1448
|
+
cliLogger.log('Starting OpenCode server...');
|
|
1449
|
+
const opencodePromise = initializeOpencodeForDirectory(currentDir).then((result) => {
|
|
1450
|
+
if (result instanceof Error) {
|
|
1451
|
+
throw new Error(result.message);
|
|
1452
|
+
}
|
|
1453
|
+
cliLogger.log('OpenCode server ready!');
|
|
1454
|
+
return result;
|
|
1455
|
+
});
|
|
1456
|
+
// Prevent unhandled rejection if OpenCode fails before backgroundInit
|
|
1457
|
+
// or the channel setup path awaits it. Errors are handled by the
|
|
1458
|
+
// respective consumers (backgroundInit catches, channel setup re-throws).
|
|
1459
|
+
opencodePromise.catch(() => { });
|
|
1376
1460
|
// Mark this bot as the most recently used so subcommands in separate
|
|
1377
1461
|
// processes (send, upload-to-discord, project list) pick the correct bot.
|
|
1378
1462
|
// getBotTokenWithMode() orders by last_used_at DESC as cross-process
|
|
@@ -1408,16 +1492,6 @@ async function run({ restartOnboarding, addChannels, useWorktrees, enableVoiceCh
|
|
|
1408
1492
|
}
|
|
1409
1493
|
return true;
|
|
1410
1494
|
})();
|
|
1411
|
-
// Start OpenCode server EARLY - let it initialize in parallel with Discord login.
|
|
1412
|
-
// This is the biggest startup bottleneck (can take 1-30 seconds to spawn and wait for ready)
|
|
1413
|
-
const currentDir = process.cwd();
|
|
1414
|
-
cliLogger.log('Starting OpenCode server...');
|
|
1415
|
-
const opencodePromise = initializeOpencodeForDirectory(currentDir).then((result) => {
|
|
1416
|
-
if (result instanceof Error) {
|
|
1417
|
-
throw new Error(result.message);
|
|
1418
|
-
}
|
|
1419
|
-
return result;
|
|
1420
|
-
});
|
|
1421
1495
|
cliLogger.log(`Connecting to ${getDiscordRestApiUrl()}...`);
|
|
1422
1496
|
const discordClient = await createDiscordClient();
|
|
1423
1497
|
const guilds = [];
|
|
@@ -1467,7 +1541,7 @@ async function run({ restartOnboarding, addChannels, useWorktrees, enableVoiceCh
|
|
|
1467
1541
|
}
|
|
1468
1542
|
catch (error) {
|
|
1469
1543
|
cliLogger.log('Failed to connect to Discord', discordClient.ws.gateway);
|
|
1470
|
-
cliLogger.error('Error: ' + (error instanceof Error ? error.
|
|
1544
|
+
cliLogger.error('Error: ' + (error instanceof Error ? error.stack : String(error)));
|
|
1471
1545
|
process.exit(EXIT_NO_RESTART);
|
|
1472
1546
|
}
|
|
1473
1547
|
await setBotToken(appId, token);
|
|
@@ -1518,7 +1592,7 @@ async function run({ restartOnboarding, addChannels, useWorktrees, enableVoiceCh
|
|
|
1518
1592
|
cliLogger.log(`Background channel sync completed for ${backgroundChannels.length} guild(s)`);
|
|
1519
1593
|
}
|
|
1520
1594
|
catch (error) {
|
|
1521
|
-
cliLogger.warn('Background channel sync failed:', error instanceof Error ? error.
|
|
1595
|
+
cliLogger.warn('Background channel sync failed:', error instanceof Error ? error.stack : String(error));
|
|
1522
1596
|
}
|
|
1523
1597
|
// Create default kimaki channel + welcome message in each guild.
|
|
1524
1598
|
// Runs after channel sync so existing channels are detected correctly.
|
|
@@ -1532,7 +1606,7 @@ async function run({ restartOnboarding, addChannels, useWorktrees, enableVoiceCh
|
|
|
1532
1606
|
});
|
|
1533
1607
|
}
|
|
1534
1608
|
catch (error) {
|
|
1535
|
-
cliLogger.warn('Background default channel creation failed:', error instanceof Error ? error.
|
|
1609
|
+
cliLogger.warn('Background default channel creation failed:', error instanceof Error ? error.stack : String(error));
|
|
1536
1610
|
}
|
|
1537
1611
|
})();
|
|
1538
1612
|
// Background: OpenCode init + slash command registration (non-blocking)
|
|
@@ -1563,7 +1637,6 @@ async function run({ restartOnboarding, addChannels, useWorktrees, enableVoiceCh
|
|
|
1563
1637
|
// Wait for OpenCode, fetch projects, show prompts, create channels if needed
|
|
1564
1638
|
cliLogger.log('Waiting for OpenCode server...');
|
|
1565
1639
|
const getClient = await opencodePromise;
|
|
1566
|
-
cliLogger.log('OpenCode server ready!');
|
|
1567
1640
|
cliLogger.log('Fetching OpenCode data...');
|
|
1568
1641
|
// Fetch projects, commands, and agents in parallel
|
|
1569
1642
|
const [projects, allUserCommands, allAgents] = await Promise.all([
|
|
@@ -1572,7 +1645,7 @@ async function run({ restartOnboarding, addChannels, useWorktrees, enableVoiceCh
|
|
|
1572
1645
|
.then((r) => r.data || [])
|
|
1573
1646
|
.catch((error) => {
|
|
1574
1647
|
cliLogger.log('Failed to fetch projects');
|
|
1575
|
-
cliLogger.error('Error:', error instanceof Error ? error.
|
|
1648
|
+
cliLogger.error('Error:', error instanceof Error ? error.stack : String(error));
|
|
1576
1649
|
discordClient.destroy();
|
|
1577
1650
|
process.exit(EXIT_NO_RESTART);
|
|
1578
1651
|
}),
|
|
@@ -1580,14 +1653,14 @@ async function run({ restartOnboarding, addChannels, useWorktrees, enableVoiceCh
|
|
|
1580
1653
|
.command.list({ directory: currentDir })
|
|
1581
1654
|
.then((r) => r.data || [])
|
|
1582
1655
|
.catch((error) => {
|
|
1583
|
-
cliLogger.warn('Failed to load user commands during setup:', error instanceof Error ? error.
|
|
1656
|
+
cliLogger.warn('Failed to load user commands during setup:', error instanceof Error ? error.stack : String(error));
|
|
1584
1657
|
return [];
|
|
1585
1658
|
}),
|
|
1586
1659
|
getClient()
|
|
1587
1660
|
.app.agents({ directory: currentDir })
|
|
1588
1661
|
.then((r) => r.data || [])
|
|
1589
1662
|
.catch((error) => {
|
|
1590
|
-
cliLogger.warn('Failed to load agents during setup:', error instanceof Error ? error.
|
|
1663
|
+
cliLogger.warn('Failed to load agents during setup:', error instanceof Error ? error.stack : String(error));
|
|
1591
1664
|
return [];
|
|
1592
1665
|
}),
|
|
1593
1666
|
]);
|
|
@@ -1703,7 +1776,7 @@ async function run({ restartOnboarding, addChannels, useWorktrees, enableVoiceCh
|
|
|
1703
1776
|
cliLogger.log('Slash commands registered!');
|
|
1704
1777
|
})
|
|
1705
1778
|
.catch((error) => {
|
|
1706
|
-
cliLogger.error('Failed to register slash commands:', error instanceof Error ? error.
|
|
1779
|
+
cliLogger.error('Failed to register slash commands:', error instanceof Error ? error.stack : String(error));
|
|
1707
1780
|
});
|
|
1708
1781
|
// Start bot after channel setup is complete so it doesn't handle
|
|
1709
1782
|
// messages/interactions while the user is still going through prompts.
|
|
@@ -1838,7 +1911,7 @@ cli
|
|
|
1838
1911
|
});
|
|
1839
1912
|
}
|
|
1840
1913
|
catch (error) {
|
|
1841
|
-
cliLogger.error('Error:', error instanceof Error ? error.
|
|
1914
|
+
cliLogger.error('Error:', error instanceof Error ? error.stack : String(error));
|
|
1842
1915
|
process.exit(EXIT_NO_RESTART);
|
|
1843
1916
|
}
|
|
1844
1917
|
});
|
|
@@ -1874,7 +1947,7 @@ cli
|
|
|
1874
1947
|
});
|
|
1875
1948
|
}
|
|
1876
1949
|
catch (error) {
|
|
1877
|
-
cliLogger.error('Error:', error instanceof Error ? error.
|
|
1950
|
+
cliLogger.error('Error:', error instanceof Error ? error.stack : String(error));
|
|
1878
1951
|
process.exit(EXIT_NO_RESTART);
|
|
1879
1952
|
}
|
|
1880
1953
|
});
|
|
@@ -1975,7 +2048,7 @@ cli
|
|
|
1975
2048
|
process.exit(0);
|
|
1976
2049
|
}
|
|
1977
2050
|
catch (error) {
|
|
1978
|
-
cliLogger.error('Error:', error instanceof Error ? error.
|
|
2051
|
+
cliLogger.error('Error:', error instanceof Error ? error.stack : String(error));
|
|
1979
2052
|
process.exit(EXIT_NO_RESTART);
|
|
1980
2053
|
}
|
|
1981
2054
|
});
|
|
@@ -2015,7 +2088,7 @@ cli
|
|
|
2015
2088
|
process.exit(0);
|
|
2016
2089
|
}
|
|
2017
2090
|
catch (error) {
|
|
2018
|
-
cliLogger.error('Error:', error instanceof Error ? error.
|
|
2091
|
+
cliLogger.error('Error:', error instanceof Error ? error.stack : String(error));
|
|
2019
2092
|
process.exit(EXIT_NO_RESTART);
|
|
2020
2093
|
}
|
|
2021
2094
|
});
|
|
@@ -2062,7 +2135,7 @@ cli
|
|
|
2062
2135
|
process.exit(0);
|
|
2063
2136
|
}
|
|
2064
2137
|
catch (error) {
|
|
2065
|
-
cliLogger.error('Error:', error instanceof Error ? error.
|
|
2138
|
+
cliLogger.error('Error:', error instanceof Error ? error.stack : String(error));
|
|
2066
2139
|
process.exit(EXIT_NO_RESTART);
|
|
2067
2140
|
}
|
|
2068
2141
|
});
|
|
@@ -2079,6 +2152,8 @@ cli
|
|
|
2079
2152
|
.option('-u, --user <username>', 'Discord username to add to thread')
|
|
2080
2153
|
.option('--agent <agent>', 'Agent to use for the session')
|
|
2081
2154
|
.option('--model <model>', 'Model to use (format: provider/model)')
|
|
2155
|
+
.option('--permission <rule>', z.array(z.string()).describe('Session permission rule (repeatable). Format: "tool:action" or "tool:pattern:action". ' +
|
|
2156
|
+
'Actions: allow, deny, ask. Examples: --permission "bash:deny" --permission "edit:deny"'))
|
|
2082
2157
|
.option('--send-at <schedule>', 'Schedule send for future (UTC ISO date/time ending in Z, or cron expression)')
|
|
2083
2158
|
.option('--thread <threadId>', 'Post prompt to an existing thread')
|
|
2084
2159
|
.option('--session <sessionId>', 'Post prompt to thread mapped to an existing session')
|
|
@@ -2119,10 +2194,12 @@ cli
|
|
|
2119
2194
|
if (!sendAt) {
|
|
2120
2195
|
return null;
|
|
2121
2196
|
}
|
|
2197
|
+
// Cron expressions use UTC so the schedule is consistent regardless of
|
|
2198
|
+
// which machine runs the bot. The system message tells the model to use UTC.
|
|
2122
2199
|
return parseSendAtValue({
|
|
2123
2200
|
value: sendAt,
|
|
2124
2201
|
now: new Date(),
|
|
2125
|
-
timezone:
|
|
2202
|
+
timezone: 'UTC',
|
|
2126
2203
|
});
|
|
2127
2204
|
})();
|
|
2128
2205
|
if (parsedSchedule instanceof Error) {
|
|
@@ -2238,7 +2315,7 @@ cli
|
|
|
2238
2315
|
}
|
|
2239
2316
|
}
|
|
2240
2317
|
catch (error) {
|
|
2241
|
-
cliLogger.debug('Failed to fetch existing channel while selecting guild:', error instanceof Error ? error.
|
|
2318
|
+
cliLogger.debug('Failed to fetch existing channel while selecting guild:', error instanceof Error ? error.stack : String(error));
|
|
2242
2319
|
}
|
|
2243
2320
|
}
|
|
2244
2321
|
// Fall back to first guild the bot is in
|
|
@@ -2306,6 +2383,7 @@ cli
|
|
|
2306
2383
|
model: options.model || null,
|
|
2307
2384
|
username: null,
|
|
2308
2385
|
userId: null,
|
|
2386
|
+
permissions: options.permission?.length ? options.permission : null,
|
|
2309
2387
|
};
|
|
2310
2388
|
const taskId = await createScheduledTask({
|
|
2311
2389
|
scheduleKind: parsedSchedule.scheduleKind,
|
|
@@ -2327,6 +2405,7 @@ cli
|
|
|
2327
2405
|
}
|
|
2328
2406
|
const threadPromptMarker = {
|
|
2329
2407
|
cliThreadPrompt: true,
|
|
2408
|
+
...(options.permission?.length ? { permissions: options.permission } : {}),
|
|
2330
2409
|
};
|
|
2331
2410
|
const promptEmbed = [
|
|
2332
2411
|
{
|
|
@@ -2417,6 +2496,7 @@ cli
|
|
|
2417
2496
|
model: options.model || null,
|
|
2418
2497
|
username: resolvedUser?.username || null,
|
|
2419
2498
|
userId: resolvedUser?.id || null,
|
|
2499
|
+
permissions: options.permission?.length ? options.permission : null,
|
|
2420
2500
|
};
|
|
2421
2501
|
const taskId = await createScheduledTask({
|
|
2422
2502
|
scheduleKind: parsedSchedule.scheduleKind,
|
|
@@ -2447,6 +2527,7 @@ cli
|
|
|
2447
2527
|
}),
|
|
2448
2528
|
...(options.agent && { agent: options.agent }),
|
|
2449
2529
|
...(options.model && { model: options.model }),
|
|
2530
|
+
...(options.permission?.length && { permissions: options.permission }),
|
|
2450
2531
|
};
|
|
2451
2532
|
const autoStartEmbed = embedMarker
|
|
2452
2533
|
? [{ color: 0x2b2d31, footer: { text: yaml.dump(embedMarker) } }]
|
|
@@ -2490,7 +2571,7 @@ cli
|
|
|
2490
2571
|
process.exit(0);
|
|
2491
2572
|
}
|
|
2492
2573
|
catch (error) {
|
|
2493
|
-
cliLogger.error('Error:', error instanceof Error ? error.
|
|
2574
|
+
cliLogger.error('Error:', error instanceof Error ? error.stack : String(error));
|
|
2494
2575
|
process.exit(EXIT_NO_RESTART);
|
|
2495
2576
|
}
|
|
2496
2577
|
});
|
|
@@ -2526,7 +2607,7 @@ cli
|
|
|
2526
2607
|
process.exit(0);
|
|
2527
2608
|
}
|
|
2528
2609
|
catch (error) {
|
|
2529
|
-
cliLogger.error('Error:', error instanceof Error ? error.
|
|
2610
|
+
cliLogger.error('Error:', error instanceof Error ? error.stack : String(error));
|
|
2530
2611
|
process.exit(EXIT_NO_RESTART);
|
|
2531
2612
|
}
|
|
2532
2613
|
});
|
|
@@ -2549,7 +2630,85 @@ cli
|
|
|
2549
2630
|
process.exit(0);
|
|
2550
2631
|
}
|
|
2551
2632
|
catch (error) {
|
|
2552
|
-
cliLogger.error('Error:', error instanceof Error ? error.
|
|
2633
|
+
cliLogger.error('Error:', error instanceof Error ? error.stack : String(error));
|
|
2634
|
+
process.exit(EXIT_NO_RESTART);
|
|
2635
|
+
}
|
|
2636
|
+
});
|
|
2637
|
+
cli
|
|
2638
|
+
.command('task edit <id>', 'Edit prompt or schedule of a planned task')
|
|
2639
|
+
.option('--prompt <prompt>', 'New prompt text')
|
|
2640
|
+
.option('--send-at <sendAt>', 'New schedule (UTC ISO date or cron expression)')
|
|
2641
|
+
.action(async (id, options) => {
|
|
2642
|
+
try {
|
|
2643
|
+
const trimmedPrompt = options.prompt === undefined ? undefined : options.prompt.trim();
|
|
2644
|
+
if (!trimmedPrompt && !options.sendAt) {
|
|
2645
|
+
cliLogger.error('Provide at least --prompt or --send-at');
|
|
2646
|
+
process.exit(EXIT_NO_RESTART);
|
|
2647
|
+
}
|
|
2648
|
+
if (trimmedPrompt !== undefined && trimmedPrompt.length === 0) {
|
|
2649
|
+
cliLogger.error('--prompt cannot be empty');
|
|
2650
|
+
process.exit(EXIT_NO_RESTART);
|
|
2651
|
+
}
|
|
2652
|
+
if (trimmedPrompt !== undefined && trimmedPrompt.length > 1900) {
|
|
2653
|
+
cliLogger.error('--prompt currently supports up to 1900 characters');
|
|
2654
|
+
process.exit(EXIT_NO_RESTART);
|
|
2655
|
+
}
|
|
2656
|
+
const taskId = Number.parseInt(id, 10);
|
|
2657
|
+
if (Number.isNaN(taskId) || taskId < 1) {
|
|
2658
|
+
cliLogger.error(`Invalid task ID: ${id}`);
|
|
2659
|
+
process.exit(EXIT_NO_RESTART);
|
|
2660
|
+
}
|
|
2661
|
+
await initDatabase();
|
|
2662
|
+
const task = await getScheduledTask(taskId);
|
|
2663
|
+
if (!task) {
|
|
2664
|
+
cliLogger.error(`Task ${taskId} not found`);
|
|
2665
|
+
process.exit(EXIT_NO_RESTART);
|
|
2666
|
+
}
|
|
2667
|
+
if (task.status !== 'planned') {
|
|
2668
|
+
cliLogger.error(`Task ${taskId} is ${task.status}, only planned tasks can be edited`);
|
|
2669
|
+
process.exit(EXIT_NO_RESTART);
|
|
2670
|
+
}
|
|
2671
|
+
const existingPayload = parseScheduledTaskPayload(task.payload_json);
|
|
2672
|
+
if (existingPayload instanceof Error) {
|
|
2673
|
+
cliLogger.error(`Failed to parse task payload: ${existingPayload.message}`);
|
|
2674
|
+
process.exit(EXIT_NO_RESTART);
|
|
2675
|
+
}
|
|
2676
|
+
const newPrompt = trimmedPrompt ?? existingPayload.prompt;
|
|
2677
|
+
const updatedPayload = {
|
|
2678
|
+
...existingPayload,
|
|
2679
|
+
prompt: newPrompt,
|
|
2680
|
+
};
|
|
2681
|
+
const updateData = {
|
|
2682
|
+
taskId,
|
|
2683
|
+
payloadJson: serializeScheduledTaskPayload(updatedPayload),
|
|
2684
|
+
promptPreview: getPromptPreview(newPrompt),
|
|
2685
|
+
};
|
|
2686
|
+
if (options.sendAt) {
|
|
2687
|
+
const parsed = parseSendAtValue({
|
|
2688
|
+
value: options.sendAt,
|
|
2689
|
+
now: new Date(),
|
|
2690
|
+
timezone: 'UTC',
|
|
2691
|
+
});
|
|
2692
|
+
if (parsed instanceof Error) {
|
|
2693
|
+
cliLogger.error(`Invalid --send-at: ${parsed.message}`);
|
|
2694
|
+
process.exit(EXIT_NO_RESTART);
|
|
2695
|
+
}
|
|
2696
|
+
updateData.scheduleKind = parsed.scheduleKind;
|
|
2697
|
+
updateData.runAt = parsed.runAt;
|
|
2698
|
+
updateData.cronExpr = parsed.cronExpr;
|
|
2699
|
+
updateData.timezone = parsed.timezone;
|
|
2700
|
+
updateData.nextRunAt = parsed.nextRunAt;
|
|
2701
|
+
}
|
|
2702
|
+
const updated = await updateScheduledTask(updateData);
|
|
2703
|
+
if (!updated) {
|
|
2704
|
+
cliLogger.error(`Task ${taskId} could not be updated (status may have changed)`);
|
|
2705
|
+
process.exit(EXIT_NO_RESTART);
|
|
2706
|
+
}
|
|
2707
|
+
cliLogger.log(`Updated task ${taskId}`);
|
|
2708
|
+
process.exit(0);
|
|
2709
|
+
}
|
|
2710
|
+
catch (error) {
|
|
2711
|
+
cliLogger.error('Error:', error instanceof Error ? error.stack : String(error));
|
|
2553
2712
|
process.exit(EXIT_NO_RESTART);
|
|
2554
2713
|
}
|
|
2555
2714
|
});
|
|
@@ -2613,7 +2772,7 @@ cli
|
|
|
2613
2772
|
}
|
|
2614
2773
|
}
|
|
2615
2774
|
catch (error) {
|
|
2616
|
-
cliLogger.debug('Failed to fetch existing channel while selecting guild:', error instanceof Error ? error.
|
|
2775
|
+
cliLogger.debug('Failed to fetch existing channel while selecting guild:', error instanceof Error ? error.stack : String(error));
|
|
2617
2776
|
let firstGuild = client.guilds.cache.first();
|
|
2618
2777
|
if (!firstGuild) {
|
|
2619
2778
|
// Cache might be empty, try fetching guilds from API
|
|
@@ -2668,12 +2827,12 @@ cli
|
|
|
2668
2827
|
}
|
|
2669
2828
|
}
|
|
2670
2829
|
catch (error) {
|
|
2671
|
-
cliLogger.debug(`Failed to fetch channel ${existingChannel.channel_id} while checking existing channels:`, error instanceof Error ? error.
|
|
2830
|
+
cliLogger.debug(`Failed to fetch channel ${existingChannel.channel_id} while checking existing channels:`, error instanceof Error ? error.stack : String(error));
|
|
2672
2831
|
}
|
|
2673
2832
|
}
|
|
2674
2833
|
}
|
|
2675
2834
|
catch (error) {
|
|
2676
|
-
cliLogger.debug('Database lookup failed while checking existing channels:', error instanceof Error ? error.
|
|
2835
|
+
cliLogger.debug('Database lookup failed while checking existing channels:', error instanceof Error ? error.stack : String(error));
|
|
2677
2836
|
}
|
|
2678
2837
|
cliLogger.log(`Creating channels in ${guild.name}...`);
|
|
2679
2838
|
const { textChannelId, voiceChannelId, channelName } = await createProjectChannels({
|
|
@@ -2914,7 +3073,7 @@ cli
|
|
|
2914
3073
|
process.exit(0);
|
|
2915
3074
|
}
|
|
2916
3075
|
catch (error) {
|
|
2917
|
-
cliLogger.error('Error:', error instanceof Error ? error.
|
|
3076
|
+
cliLogger.error('Error:', error instanceof Error ? error.stack : String(error));
|
|
2918
3077
|
process.exit(EXIT_NO_RESTART);
|
|
2919
3078
|
}
|
|
2920
3079
|
});
|
|
@@ -3040,7 +3199,7 @@ cli
|
|
|
3040
3199
|
process.exit(0);
|
|
3041
3200
|
}
|
|
3042
3201
|
catch (error) {
|
|
3043
|
-
cliLogger.error('Error:', error instanceof Error ? error.
|
|
3202
|
+
cliLogger.error('Error:', error instanceof Error ? error.stack : String(error));
|
|
3044
3203
|
process.exit(EXIT_NO_RESTART);
|
|
3045
3204
|
}
|
|
3046
3205
|
});
|
|
@@ -3103,7 +3262,7 @@ cli
|
|
|
3103
3262
|
process.exit(EXIT_NO_RESTART);
|
|
3104
3263
|
}
|
|
3105
3264
|
catch (error) {
|
|
3106
|
-
cliLogger.error('Error:', error instanceof Error ? error.
|
|
3265
|
+
cliLogger.error('Error:', error instanceof Error ? error.stack : String(error));
|
|
3107
3266
|
process.exit(EXIT_NO_RESTART);
|
|
3108
3267
|
}
|
|
3109
3268
|
});
|
|
@@ -3254,7 +3413,7 @@ cli
|
|
|
3254
3413
|
process.exit(0);
|
|
3255
3414
|
}
|
|
3256
3415
|
catch (error) {
|
|
3257
|
-
cliLogger.error('Error:', error instanceof Error ? error.
|
|
3416
|
+
cliLogger.error('Error:', error instanceof Error ? error.stack : String(error));
|
|
3258
3417
|
process.exit(EXIT_NO_RESTART);
|
|
3259
3418
|
}
|
|
3260
3419
|
});
|
|
@@ -3392,10 +3551,38 @@ cli
|
|
|
3392
3551
|
process.exit(0);
|
|
3393
3552
|
}
|
|
3394
3553
|
catch (error) {
|
|
3395
|
-
cliLogger.error('Error:', error instanceof Error ? error.
|
|
3554
|
+
cliLogger.error('Error:', error instanceof Error ? error.stack : String(error));
|
|
3396
3555
|
process.exit(EXIT_NO_RESTART);
|
|
3397
3556
|
}
|
|
3398
3557
|
});
|
|
3558
|
+
cli
|
|
3559
|
+
.command('session discord-url <sessionId>', 'Print the Discord thread URL for a session')
|
|
3560
|
+
.option('--json', 'Output as JSON')
|
|
3561
|
+
.action(async (sessionId, options) => {
|
|
3562
|
+
await initDatabase();
|
|
3563
|
+
const threadId = await getThreadIdBySessionId(sessionId);
|
|
3564
|
+
if (!threadId) {
|
|
3565
|
+
cliLogger.error(`No Discord thread found for session: ${sessionId}`);
|
|
3566
|
+
process.exit(EXIT_NO_RESTART);
|
|
3567
|
+
}
|
|
3568
|
+
const { token: botToken } = await resolveBotCredentials();
|
|
3569
|
+
const rest = createDiscordRest(botToken);
|
|
3570
|
+
const threadData = (await rest.get(Routes.channel(threadId)));
|
|
3571
|
+
const url = `https://discord.com/channels/${threadData.guild_id}/${threadData.id}`;
|
|
3572
|
+
if (options.json) {
|
|
3573
|
+
console.log(JSON.stringify({
|
|
3574
|
+
url,
|
|
3575
|
+
threadId: threadData.id,
|
|
3576
|
+
guildId: threadData.guild_id,
|
|
3577
|
+
sessionId,
|
|
3578
|
+
threadName: threadData.name,
|
|
3579
|
+
}));
|
|
3580
|
+
}
|
|
3581
|
+
else {
|
|
3582
|
+
console.log(url);
|
|
3583
|
+
}
|
|
3584
|
+
process.exit(0);
|
|
3585
|
+
});
|
|
3399
3586
|
cli
|
|
3400
3587
|
.command('upgrade', 'Upgrade kimaki to the latest version and restart the running bot')
|
|
3401
3588
|
.option('--skip-restart', 'Only upgrade, do not restart the running bot')
|
|
@@ -3425,7 +3612,7 @@ cli
|
|
|
3425
3612
|
process.exit(0);
|
|
3426
3613
|
}
|
|
3427
3614
|
catch (error) {
|
|
3428
|
-
cliLogger.error('Upgrade failed:', error instanceof Error ? error.
|
|
3615
|
+
cliLogger.error('Upgrade failed:', error instanceof Error ? error.stack : String(error));
|
|
3429
3616
|
process.exit(EXIT_NO_RESTART);
|
|
3430
3617
|
}
|
|
3431
3618
|
});
|
|
@@ -3495,7 +3682,7 @@ cli
|
|
|
3495
3682
|
process.exit(0);
|
|
3496
3683
|
}
|
|
3497
3684
|
catch (error) {
|
|
3498
|
-
cliLogger.error('Merge failed:', error instanceof Error ? error.
|
|
3685
|
+
cliLogger.error('Merge failed:', error instanceof Error ? error.stack : String(error));
|
|
3499
3686
|
process.exit(EXIT_NO_RESTART);
|
|
3500
3687
|
}
|
|
3501
3688
|
});
|