kimaki 0.4.60 → 0.4.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +312 -226
- package/dist/commands/agent.js +20 -20
- package/dist/commands/context-usage.js +143 -0
- package/dist/commands/queue.js +122 -1
- package/dist/config.js +1 -0
- package/dist/interaction-handler.js +11 -1
- package/dist/opencode-plugin.js +1 -1
- package/dist/session-handler.js +127 -57
- package/dist/system-message.js +22 -1
- package/dist/wait-session.js +79 -0
- package/package.json +1 -1
- package/src/cli.ts +362 -253
- package/src/commands/agent.ts +22 -26
- package/src/commands/context-usage.ts +178 -0
- package/src/commands/queue.ts +145 -2
- package/src/config.ts +5 -0
- package/src/interaction-handler.ts +14 -1
- package/src/opencode-plugin.ts +1 -1
- package/src/session-handler.ts +158 -62
- package/src/system-message.ts +22 -1
- package/src/wait-session.ts +120 -0
package/dist/cli.js
CHANGED
|
@@ -366,6 +366,7 @@ async function startLockServer() {
|
|
|
366
366
|
}
|
|
367
367
|
// Commands to skip when registering user commands (reserved names)
|
|
368
368
|
const SKIP_USER_COMMANDS = ['init'];
|
|
369
|
+
import { registeredUserCommands } from './config.js';
|
|
369
370
|
async function registerCommands({ token, appId, userCommands = [], agents = [], }) {
|
|
370
371
|
const commands = [
|
|
371
372
|
new SlashCommandBuilder()
|
|
@@ -531,6 +532,26 @@ async function registerCommands({ token, appId, userCommands = [], agents = [],
|
|
|
531
532
|
.setDescription('Clear all queued messages in this thread')
|
|
532
533
|
.setDMPermission(false)
|
|
533
534
|
.toJSON(),
|
|
535
|
+
new SlashCommandBuilder()
|
|
536
|
+
.setName('queue-command')
|
|
537
|
+
.setDescription('Queue a user command to run after the current response finishes')
|
|
538
|
+
.addStringOption((option) => {
|
|
539
|
+
option
|
|
540
|
+
.setName('command')
|
|
541
|
+
.setDescription('The command to run')
|
|
542
|
+
.setRequired(true)
|
|
543
|
+
.setAutocomplete(true);
|
|
544
|
+
return option;
|
|
545
|
+
})
|
|
546
|
+
.addStringOption((option) => {
|
|
547
|
+
option
|
|
548
|
+
.setName('arguments')
|
|
549
|
+
.setDescription('Arguments to pass to the command')
|
|
550
|
+
.setRequired(false);
|
|
551
|
+
return option;
|
|
552
|
+
})
|
|
553
|
+
.setDMPermission(false)
|
|
554
|
+
.toJSON(),
|
|
534
555
|
new SlashCommandBuilder()
|
|
535
556
|
.setName('undo')
|
|
536
557
|
.setDescription('Undo the last assistant message (revert file changes)')
|
|
@@ -571,6 +592,11 @@ async function registerCommands({ token, appId, userCommands = [], agents = [],
|
|
|
571
592
|
})
|
|
572
593
|
.setDMPermission(false)
|
|
573
594
|
.toJSON(),
|
|
595
|
+
new SlashCommandBuilder()
|
|
596
|
+
.setName('context-usage')
|
|
597
|
+
.setDescription('Show token usage and context window percentage for this session')
|
|
598
|
+
.setDMPermission(false)
|
|
599
|
+
.toJSON(),
|
|
574
600
|
new SlashCommandBuilder()
|
|
575
601
|
.setName('upgrade-and-restart')
|
|
576
602
|
.setDescription('Upgrade kimaki to the latest version and restart the bot')
|
|
@@ -578,6 +604,8 @@ async function registerCommands({ token, appId, userCommands = [], agents = [],
|
|
|
578
604
|
.toJSON(),
|
|
579
605
|
];
|
|
580
606
|
// Add user-defined commands with -cmd suffix
|
|
607
|
+
// Also populate registeredUserCommands for /queue-command autocomplete
|
|
608
|
+
registeredUserCommands.length = 0;
|
|
581
609
|
for (const cmd of userCommands) {
|
|
582
610
|
if (SKIP_USER_COMMANDS.includes(cmd.name)) {
|
|
583
611
|
continue;
|
|
@@ -587,6 +615,7 @@ async function registerCommands({ token, appId, userCommands = [], agents = [],
|
|
|
587
615
|
const sanitizedName = cmd.name.toLowerCase().replace(/:/g, '-');
|
|
588
616
|
const commandName = `${sanitizedName}-cmd`;
|
|
589
617
|
const description = cmd.description || `Run /${cmd.name} command`;
|
|
618
|
+
registeredUserCommands.push({ name: cmd.name, description });
|
|
590
619
|
commands.push(new SlashCommandBuilder()
|
|
591
620
|
.setName(commandName.slice(0, 32)) // Discord limits to 32 chars
|
|
592
621
|
.setDescription(description.slice(0, 100)) // Discord limits to 100 chars
|
|
@@ -1184,6 +1213,7 @@ cli
|
|
|
1184
1213
|
.option('--model <model>', 'Model to use (format: provider/model)')
|
|
1185
1214
|
.option('--thread <threadId>', 'Post prompt to an existing thread')
|
|
1186
1215
|
.option('--session <sessionId>', 'Post prompt to thread mapped to an existing session')
|
|
1216
|
+
.option('--wait', 'Wait for session to complete, then print session text to stdout')
|
|
1187
1217
|
.action(async (options) => {
|
|
1188
1218
|
try {
|
|
1189
1219
|
let { channel: channelId, prompt, name, appId: optionAppId, notifyOnly, thread: threadId, session: sessionId, } = options;
|
|
@@ -1209,6 +1239,10 @@ cli
|
|
|
1209
1239
|
cliLogger.error('Cannot use --worktree with --notify-only');
|
|
1210
1240
|
process.exit(EXIT_NO_RESTART);
|
|
1211
1241
|
}
|
|
1242
|
+
if (options.wait && notifyOnly) {
|
|
1243
|
+
cliLogger.error('Cannot use --wait with --notify-only');
|
|
1244
|
+
process.exit(EXIT_NO_RESTART);
|
|
1245
|
+
}
|
|
1212
1246
|
if (existingThreadMode) {
|
|
1213
1247
|
const incompatibleFlags = [];
|
|
1214
1248
|
if (notifyOnly) {
|
|
@@ -1403,9 +1437,11 @@ cli
|
|
|
1403
1437
|
}
|
|
1404
1438
|
const threadPromptMarker = { cliThreadPrompt: true };
|
|
1405
1439
|
const promptEmbed = [{ color: 0x2b2d31, footer: { text: yaml.dump(threadPromptMarker) } }];
|
|
1440
|
+
// Prefix the prompt so it's clear who sent it (matches /queue format)
|
|
1441
|
+
const prefixedPrompt = `» **kimaki-cli:** ${prompt}`;
|
|
1406
1442
|
await sendDiscordMessageWithOptionalAttachment({
|
|
1407
1443
|
channelId: targetThreadId,
|
|
1408
|
-
prompt,
|
|
1444
|
+
prompt: prefixedPrompt,
|
|
1409
1445
|
botToken,
|
|
1410
1446
|
embeds: promptEmbed,
|
|
1411
1447
|
rest,
|
|
@@ -1413,6 +1449,13 @@ cli
|
|
|
1413
1449
|
const threadUrl = `https://discord.com/channels/${threadData.guild_id}/${threadData.id}`;
|
|
1414
1450
|
note(`Prompt sent to thread: ${threadData.name}\n\nURL: ${threadUrl}`, '✅ Message Sent');
|
|
1415
1451
|
cliLogger.log(threadUrl);
|
|
1452
|
+
if (options.wait) {
|
|
1453
|
+
const { waitAndOutputSession } = await import('./wait-session.js');
|
|
1454
|
+
await waitAndOutputSession({
|
|
1455
|
+
threadId: targetThreadId,
|
|
1456
|
+
projectDirectory: channelConfig.directory,
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1416
1459
|
process.exit(0);
|
|
1417
1460
|
}
|
|
1418
1461
|
cliLogger.log('Fetching channel info...');
|
|
@@ -1507,6 +1550,13 @@ cli
|
|
|
1507
1550
|
: `Thread: ${threadData.name}\nDirectory: ${projectDirectory}${worktreeNote}\n\nThe running bot will pick this up and start the session.\n\nURL: ${threadUrl}`;
|
|
1508
1551
|
note(successMessage, '✅ Thread Created');
|
|
1509
1552
|
cliLogger.log(threadUrl);
|
|
1553
|
+
if (options.wait) {
|
|
1554
|
+
const { waitAndOutputSession } = await import('./wait-session.js');
|
|
1555
|
+
await waitAndOutputSession({
|
|
1556
|
+
threadId: threadData.id,
|
|
1557
|
+
projectDirectory,
|
|
1558
|
+
});
|
|
1559
|
+
}
|
|
1510
1560
|
process.exit(0);
|
|
1511
1561
|
}
|
|
1512
1562
|
catch (error) {
|
|
@@ -1520,107 +1570,87 @@ cli
|
|
|
1520
1570
|
.option('-g, --guild <guildId>', 'Discord guild/server ID (auto-detects if bot is in only one server)')
|
|
1521
1571
|
.option('-a, --app-id <appId>', 'Bot application ID (reads from database if available)')
|
|
1522
1572
|
.action(async (directory, options) => {
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
if (!appId) {
|
|
1538
|
-
try {
|
|
1539
|
-
const botRow = await getBotToken();
|
|
1540
|
-
appId = botRow?.app_id;
|
|
1541
|
-
}
|
|
1542
|
-
catch (error) {
|
|
1543
|
-
cliLogger.debug('Database lookup failed while resolving app ID:', error instanceof Error ? error.message : String(error));
|
|
1544
|
-
}
|
|
1545
|
-
}
|
|
1546
|
-
}
|
|
1547
|
-
else {
|
|
1573
|
+
const absolutePath = path.resolve(directory || '.');
|
|
1574
|
+
if (!fs.existsSync(absolutePath)) {
|
|
1575
|
+
cliLogger.error(`Directory does not exist: ${absolutePath}`);
|
|
1576
|
+
process.exit(EXIT_NO_RESTART);
|
|
1577
|
+
}
|
|
1578
|
+
// Initialize database
|
|
1579
|
+
await initDatabase();
|
|
1580
|
+
// Get bot token from env var or database
|
|
1581
|
+
const envToken = process.env.KIMAKI_BOT_TOKEN;
|
|
1582
|
+
let botToken;
|
|
1583
|
+
let appId = options.appId;
|
|
1584
|
+
if (envToken) {
|
|
1585
|
+
botToken = envToken;
|
|
1586
|
+
if (!appId) {
|
|
1548
1587
|
try {
|
|
1549
1588
|
const botRow = await getBotToken();
|
|
1550
|
-
|
|
1551
|
-
botToken = botRow.token;
|
|
1552
|
-
appId = appId || botRow.app_id;
|
|
1553
|
-
}
|
|
1589
|
+
appId = botRow?.app_id;
|
|
1554
1590
|
}
|
|
1555
|
-
catch (
|
|
1556
|
-
cliLogger.
|
|
1591
|
+
catch (error) {
|
|
1592
|
+
cliLogger.debug('Database lookup failed while resolving app ID:', error instanceof Error ? error.message : String(error));
|
|
1557
1593
|
}
|
|
1558
1594
|
}
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1595
|
+
}
|
|
1596
|
+
else {
|
|
1597
|
+
try {
|
|
1598
|
+
const botRow = await getBotToken();
|
|
1599
|
+
if (botRow) {
|
|
1600
|
+
botToken = botRow.token;
|
|
1601
|
+
appId = appId || botRow.app_id;
|
|
1602
|
+
}
|
|
1562
1603
|
}
|
|
1563
|
-
|
|
1564
|
-
cliLogger.error('
|
|
1565
|
-
process.exit(EXIT_NO_RESTART);
|
|
1604
|
+
catch (e) {
|
|
1605
|
+
cliLogger.error('Database error:', e instanceof Error ? e.message : String(e));
|
|
1566
1606
|
}
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1607
|
+
}
|
|
1608
|
+
if (!botToken) {
|
|
1609
|
+
cliLogger.error('No bot token found. Set KIMAKI_BOT_TOKEN env var or run `kimaki` first to set up.');
|
|
1610
|
+
process.exit(EXIT_NO_RESTART);
|
|
1611
|
+
}
|
|
1612
|
+
if (!appId) {
|
|
1613
|
+
cliLogger.error('App ID is required to create channels. Use --app-id or run `kimaki` first.');
|
|
1614
|
+
process.exit(EXIT_NO_RESTART);
|
|
1615
|
+
}
|
|
1616
|
+
cliLogger.log('Connecting to Discord...');
|
|
1617
|
+
const client = await createDiscordClient();
|
|
1618
|
+
await new Promise((resolve, reject) => {
|
|
1619
|
+
client.once(Events.ClientReady, () => {
|
|
1620
|
+
resolve();
|
|
1575
1621
|
});
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1622
|
+
client.once(Events.Error, reject);
|
|
1623
|
+
client.login(botToken);
|
|
1624
|
+
});
|
|
1625
|
+
cliLogger.log('Finding guild...');
|
|
1626
|
+
// Find guild
|
|
1627
|
+
let guild;
|
|
1628
|
+
if (options.guild) {
|
|
1629
|
+
const guildId = String(options.guild);
|
|
1630
|
+
const foundGuild = client.guilds.cache.get(guildId);
|
|
1631
|
+
if (!foundGuild) {
|
|
1632
|
+
cliLogger.log('Guild not found');
|
|
1633
|
+
cliLogger.error(`Guild not found: ${guildId}`);
|
|
1634
|
+
client.destroy();
|
|
1635
|
+
process.exit(EXIT_NO_RESTART);
|
|
1589
1636
|
}
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
throw new Error('Channel has no guild');
|
|
1601
|
-
}
|
|
1637
|
+
guild = foundGuild;
|
|
1638
|
+
}
|
|
1639
|
+
else {
|
|
1640
|
+
// Auto-detect: prefer guild with existing channels for this bot, else first guild
|
|
1641
|
+
const existingChannelId = await findChannelByAppId(appId);
|
|
1642
|
+
if (existingChannelId) {
|
|
1643
|
+
try {
|
|
1644
|
+
const ch = await client.channels.fetch(existingChannelId);
|
|
1645
|
+
if (ch && 'guild' in ch && ch.guild) {
|
|
1646
|
+
guild = ch.guild;
|
|
1602
1647
|
}
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
let firstGuild = client.guilds.cache.first();
|
|
1606
|
-
if (!firstGuild) {
|
|
1607
|
-
// Cache might be empty, try fetching guilds from API
|
|
1608
|
-
const fetched = await client.guilds.fetch();
|
|
1609
|
-
const firstOAuth2Guild = fetched.first();
|
|
1610
|
-
if (firstOAuth2Guild) {
|
|
1611
|
-
firstGuild = await client.guilds.fetch(firstOAuth2Guild.id);
|
|
1612
|
-
}
|
|
1613
|
-
}
|
|
1614
|
-
if (!firstGuild) {
|
|
1615
|
-
cliLogger.log('No guild found');
|
|
1616
|
-
cliLogger.error('No guild found. Add the bot to a server first.');
|
|
1617
|
-
client.destroy();
|
|
1618
|
-
process.exit(EXIT_NO_RESTART);
|
|
1619
|
-
}
|
|
1620
|
-
guild = firstGuild;
|
|
1648
|
+
else {
|
|
1649
|
+
throw new Error('Channel has no guild');
|
|
1621
1650
|
}
|
|
1622
1651
|
}
|
|
1623
|
-
|
|
1652
|
+
catch (error) {
|
|
1653
|
+
cliLogger.debug('Failed to fetch existing channel while selecting guild:', error instanceof Error ? error.message : String(error));
|
|
1624
1654
|
let firstGuild = client.guilds.cache.first();
|
|
1625
1655
|
if (!firstGuild) {
|
|
1626
1656
|
// Cache might be empty, try fetching guilds from API
|
|
@@ -1639,169 +1669,225 @@ cli
|
|
|
1639
1669
|
guild = firstGuild;
|
|
1640
1670
|
}
|
|
1641
1671
|
}
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
for (const existingChannel of existingChannels) {
|
|
1651
|
-
try {
|
|
1652
|
-
const ch = await client.channels.fetch(existingChannel.channel_id);
|
|
1653
|
-
if (ch && 'guild' in ch && ch.guild?.id === guild.id) {
|
|
1654
|
-
client.destroy();
|
|
1655
|
-
cliLogger.error(`Channel already exists for this directory in ${guild.name}. Channel ID: ${existingChannel.channel_id}`);
|
|
1656
|
-
process.exit(EXIT_NO_RESTART);
|
|
1657
|
-
}
|
|
1658
|
-
}
|
|
1659
|
-
catch (error) {
|
|
1660
|
-
cliLogger.debug(`Failed to fetch channel ${existingChannel.channel_id} while checking existing channels:`, error instanceof Error ? error.message : String(error));
|
|
1672
|
+
else {
|
|
1673
|
+
let firstGuild = client.guilds.cache.first();
|
|
1674
|
+
if (!firstGuild) {
|
|
1675
|
+
// Cache might be empty, try fetching guilds from API
|
|
1676
|
+
const fetched = await client.guilds.fetch();
|
|
1677
|
+
const firstOAuth2Guild = fetched.first();
|
|
1678
|
+
if (firstOAuth2Guild) {
|
|
1679
|
+
firstGuild = await client.guilds.fetch(firstOAuth2Guild.id);
|
|
1661
1680
|
}
|
|
1662
1681
|
}
|
|
1682
|
+
if (!firstGuild) {
|
|
1683
|
+
cliLogger.log('No guild found');
|
|
1684
|
+
cliLogger.error('No guild found. Add the bot to a server first.');
|
|
1685
|
+
client.destroy();
|
|
1686
|
+
process.exit(EXIT_NO_RESTART);
|
|
1687
|
+
}
|
|
1688
|
+
guild = firstGuild;
|
|
1663
1689
|
}
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
const
|
|
1669
|
-
|
|
1670
|
-
|
|
1690
|
+
}
|
|
1691
|
+
// Check if channel already exists in this guild
|
|
1692
|
+
cliLogger.log('Checking for existing channel...');
|
|
1693
|
+
try {
|
|
1694
|
+
const existingChannels = await findChannelsByDirectory({
|
|
1695
|
+
directory: absolutePath,
|
|
1696
|
+
channelType: 'text',
|
|
1671
1697
|
appId,
|
|
1672
|
-
botName: client.user?.username,
|
|
1673
1698
|
});
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1699
|
+
for (const existingChannel of existingChannels) {
|
|
1700
|
+
try {
|
|
1701
|
+
const ch = await client.channels.fetch(existingChannel.channel_id);
|
|
1702
|
+
if (ch && 'guild' in ch && ch.guild?.id === guild.id) {
|
|
1703
|
+
client.destroy();
|
|
1704
|
+
cliLogger.error(`Channel already exists for this directory in ${guild.name}. Channel ID: ${existingChannel.channel_id}`);
|
|
1705
|
+
process.exit(EXIT_NO_RESTART);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
catch (error) {
|
|
1709
|
+
cliLogger.debug(`Failed to fetch channel ${existingChannel.channel_id} while checking existing channels:`, error instanceof Error ? error.message : String(error));
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1680
1712
|
}
|
|
1681
1713
|
catch (error) {
|
|
1682
|
-
cliLogger.
|
|
1683
|
-
process.exit(EXIT_NO_RESTART);
|
|
1714
|
+
cliLogger.debug('Database lookup failed while checking existing channels:', error instanceof Error ? error.message : String(error));
|
|
1684
1715
|
}
|
|
1716
|
+
cliLogger.log(`Creating channels in ${guild.name}...`);
|
|
1717
|
+
const { textChannelId, voiceChannelId, channelName } = await createProjectChannels({
|
|
1718
|
+
guild,
|
|
1719
|
+
projectDirectory: absolutePath,
|
|
1720
|
+
appId,
|
|
1721
|
+
botName: client.user?.username,
|
|
1722
|
+
});
|
|
1723
|
+
client.destroy();
|
|
1724
|
+
cliLogger.log('Channels created!');
|
|
1725
|
+
const channelUrl = `https://discord.com/channels/${guild.id}/${textChannelId}`;
|
|
1726
|
+
note(`Created channels for project:\n\n📝 Text: #${channelName}\n🔊 Voice: #${channelName}\n📁 Directory: ${absolutePath}\n\nURL: ${channelUrl}`, '✅ Success');
|
|
1727
|
+
cliLogger.log(channelUrl);
|
|
1728
|
+
process.exit(0);
|
|
1685
1729
|
});
|
|
1686
1730
|
cli
|
|
1687
1731
|
.command('project list', 'List all registered projects with their Discord channels')
|
|
1688
1732
|
.option('--json', 'Output as JSON')
|
|
1689
1733
|
.action(async (options) => {
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
console.log(JSON.stringify(output, null, 2));
|
|
1704
|
-
process.exit(0);
|
|
1705
|
-
}
|
|
1706
|
-
if (channels.length === 0) {
|
|
1707
|
-
cliLogger.log('No projects registered');
|
|
1708
|
-
process.exit(0);
|
|
1709
|
-
}
|
|
1710
|
-
for (const ch of channels) {
|
|
1711
|
-
const name = path.basename(ch.directory);
|
|
1712
|
-
console.log(`\n📁 ${name}`);
|
|
1713
|
-
console.log(` Directory: ${ch.directory}`);
|
|
1714
|
-
console.log(` Channel ID: ${ch.channel_id}`);
|
|
1715
|
-
if (ch.app_id) {
|
|
1716
|
-
console.log(` Bot App ID: ${ch.app_id}`);
|
|
1717
|
-
}
|
|
1718
|
-
}
|
|
1734
|
+
await initDatabase();
|
|
1735
|
+
const prisma = await getPrisma();
|
|
1736
|
+
const channels = await prisma.channel_directories.findMany({
|
|
1737
|
+
where: { channel_type: 'text' },
|
|
1738
|
+
orderBy: { created_at: 'desc' },
|
|
1739
|
+
});
|
|
1740
|
+
if (options.json) {
|
|
1741
|
+
const output = channels.map((ch) => ({
|
|
1742
|
+
channel_id: ch.channel_id,
|
|
1743
|
+
directory: ch.directory,
|
|
1744
|
+
app_id: ch.app_id,
|
|
1745
|
+
}));
|
|
1746
|
+
console.log(JSON.stringify(output, null, 2));
|
|
1719
1747
|
process.exit(0);
|
|
1720
1748
|
}
|
|
1721
|
-
|
|
1722
|
-
cliLogger.
|
|
1749
|
+
if (channels.length === 0) {
|
|
1750
|
+
cliLogger.log('No projects registered');
|
|
1751
|
+
process.exit(0);
|
|
1752
|
+
}
|
|
1753
|
+
for (const ch of channels) {
|
|
1754
|
+
const name = path.basename(ch.directory);
|
|
1755
|
+
console.log(`\n📁 ${name}`);
|
|
1756
|
+
console.log(` Directory: ${ch.directory}`);
|
|
1757
|
+
console.log(` Channel ID: ${ch.channel_id}`);
|
|
1758
|
+
if (ch.app_id) {
|
|
1759
|
+
console.log(` Bot App ID: ${ch.app_id}`);
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
process.exit(0);
|
|
1763
|
+
});
|
|
1764
|
+
cli
|
|
1765
|
+
.command('project open-in-discord', 'Open the current project channel in Discord')
|
|
1766
|
+
.action(async () => {
|
|
1767
|
+
await initDatabase();
|
|
1768
|
+
const botRow = await getBotToken();
|
|
1769
|
+
if (!botRow) {
|
|
1770
|
+
cliLogger.error('No bot configured. Run `kimaki` first.');
|
|
1723
1771
|
process.exit(EXIT_NO_RESTART);
|
|
1724
1772
|
}
|
|
1773
|
+
const { app_id: appId, token: botToken } = botRow;
|
|
1774
|
+
const absolutePath = path.resolve('.');
|
|
1775
|
+
// Walk up parent directories to find a matching channel
|
|
1776
|
+
const findChannelForPath = async (dirPath) => {
|
|
1777
|
+
const withAppId = appId ? await findChannelsByDirectory({ directory: dirPath, channelType: 'text', appId }) : [];
|
|
1778
|
+
if (withAppId.length > 0) {
|
|
1779
|
+
return withAppId[0];
|
|
1780
|
+
}
|
|
1781
|
+
const withoutAppId = await findChannelsByDirectory({ directory: dirPath, channelType: 'text' });
|
|
1782
|
+
return withoutAppId[0];
|
|
1783
|
+
};
|
|
1784
|
+
let existingChannel;
|
|
1785
|
+
let searchPath = absolutePath;
|
|
1786
|
+
do {
|
|
1787
|
+
existingChannel = await findChannelForPath(searchPath);
|
|
1788
|
+
if (existingChannel) {
|
|
1789
|
+
break;
|
|
1790
|
+
}
|
|
1791
|
+
const parent = path.dirname(searchPath);
|
|
1792
|
+
if (parent === searchPath) {
|
|
1793
|
+
break;
|
|
1794
|
+
}
|
|
1795
|
+
searchPath = parent;
|
|
1796
|
+
} while (true);
|
|
1797
|
+
if (!existingChannel) {
|
|
1798
|
+
cliLogger.error(`No project channel found for ${absolutePath}`);
|
|
1799
|
+
process.exit(EXIT_NO_RESTART);
|
|
1800
|
+
}
|
|
1801
|
+
// Fetch channel from Discord to get guild_id
|
|
1802
|
+
const rest = new REST().setToken(botToken);
|
|
1803
|
+
const channelData = (await rest.get(Routes.channel(existingChannel.channel_id)));
|
|
1804
|
+
const channelUrl = `https://discord.com/channels/${channelData.guild_id}/${channelData.id}`;
|
|
1805
|
+
cliLogger.log(channelUrl);
|
|
1806
|
+
// Open in browser if running in a TTY
|
|
1807
|
+
if (process.stdout.isTTY) {
|
|
1808
|
+
if (process.platform === 'win32') {
|
|
1809
|
+
spawn('cmd', ['/c', 'start', '', channelUrl], { detached: true, stdio: 'ignore' }).unref();
|
|
1810
|
+
}
|
|
1811
|
+
else {
|
|
1812
|
+
const openCmd = process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
1813
|
+
spawn(openCmd, [channelUrl], { detached: true, stdio: 'ignore' }).unref();
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
process.exit(0);
|
|
1725
1817
|
});
|
|
1726
1818
|
cli
|
|
1727
1819
|
.command('project create <name>', 'Create a new project folder with git and Discord channels')
|
|
1728
1820
|
.option('-g, --guild <guildId>', 'Discord guild ID')
|
|
1729
1821
|
.action(async (name, options) => {
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1822
|
+
const sanitizedName = name
|
|
1823
|
+
.toLowerCase()
|
|
1824
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
1825
|
+
.replace(/-+/g, '-')
|
|
1826
|
+
.replace(/^-|-$/g, '')
|
|
1827
|
+
.slice(0, 100);
|
|
1828
|
+
if (!sanitizedName) {
|
|
1829
|
+
cliLogger.error('Invalid project name');
|
|
1830
|
+
process.exit(EXIT_NO_RESTART);
|
|
1831
|
+
}
|
|
1832
|
+
await initDatabase();
|
|
1833
|
+
const botRow = await getBotToken();
|
|
1834
|
+
if (!botRow) {
|
|
1835
|
+
cliLogger.error('No bot configured. Run `kimaki` first.');
|
|
1836
|
+
process.exit(EXIT_NO_RESTART);
|
|
1837
|
+
}
|
|
1838
|
+
const { app_id: appId, token: botToken } = botRow;
|
|
1839
|
+
const projectsDir = getProjectsDir();
|
|
1840
|
+
const projectDirectory = path.join(projectsDir, sanitizedName);
|
|
1841
|
+
if (!fs.existsSync(projectsDir)) {
|
|
1842
|
+
fs.mkdirSync(projectsDir, { recursive: true });
|
|
1843
|
+
}
|
|
1844
|
+
if (fs.existsSync(projectDirectory)) {
|
|
1845
|
+
cliLogger.error(`Directory already exists: ${projectDirectory}`);
|
|
1846
|
+
process.exit(EXIT_NO_RESTART);
|
|
1847
|
+
}
|
|
1848
|
+
fs.mkdirSync(projectDirectory, { recursive: true });
|
|
1849
|
+
cliLogger.log(`Created: ${projectDirectory}`);
|
|
1850
|
+
execSync('git init', { cwd: projectDirectory, stdio: 'pipe' });
|
|
1851
|
+
cliLogger.log('Initialized git');
|
|
1852
|
+
cliLogger.log('Connecting to Discord...');
|
|
1853
|
+
const client = await createDiscordClient();
|
|
1854
|
+
await new Promise((resolve, reject) => {
|
|
1855
|
+
client.once(Events.ClientReady, () => {
|
|
1856
|
+
resolve();
|
|
1857
|
+
});
|
|
1858
|
+
client.once(Events.Error, reject);
|
|
1859
|
+
client.login(botToken).catch(reject);
|
|
1860
|
+
});
|
|
1861
|
+
let guild;
|
|
1862
|
+
if (options.guild) {
|
|
1863
|
+
const found = client.guilds.cache.get(options.guild);
|
|
1864
|
+
if (!found) {
|
|
1865
|
+
cliLogger.error(`Guild not found: ${options.guild}`);
|
|
1866
|
+
client.destroy();
|
|
1745
1867
|
process.exit(EXIT_NO_RESTART);
|
|
1746
1868
|
}
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
cliLogger.error(`Directory already exists: ${projectDirectory}`);
|
|
1869
|
+
guild = found;
|
|
1870
|
+
}
|
|
1871
|
+
else {
|
|
1872
|
+
const first = client.guilds.cache.first();
|
|
1873
|
+
if (!first) {
|
|
1874
|
+
cliLogger.error('No guild found. Add the bot to a server first.');
|
|
1875
|
+
client.destroy();
|
|
1755
1876
|
process.exit(EXIT_NO_RESTART);
|
|
1756
1877
|
}
|
|
1757
|
-
|
|
1758
|
-
cliLogger.log(`Created: ${projectDirectory}`);
|
|
1759
|
-
execSync('git init', { cwd: projectDirectory, stdio: 'pipe' });
|
|
1760
|
-
cliLogger.log('Initialized git');
|
|
1761
|
-
cliLogger.log('Connecting to Discord...');
|
|
1762
|
-
const client = await createDiscordClient();
|
|
1763
|
-
await new Promise((resolve, reject) => {
|
|
1764
|
-
client.once(Events.ClientReady, () => {
|
|
1765
|
-
resolve();
|
|
1766
|
-
});
|
|
1767
|
-
client.once(Events.Error, reject);
|
|
1768
|
-
client.login(botToken).catch(reject);
|
|
1769
|
-
});
|
|
1770
|
-
let guild;
|
|
1771
|
-
if (options.guild) {
|
|
1772
|
-
const found = client.guilds.cache.get(options.guild);
|
|
1773
|
-
if (!found) {
|
|
1774
|
-
cliLogger.error(`Guild not found: ${options.guild}`);
|
|
1775
|
-
client.destroy();
|
|
1776
|
-
process.exit(EXIT_NO_RESTART);
|
|
1777
|
-
}
|
|
1778
|
-
guild = found;
|
|
1779
|
-
}
|
|
1780
|
-
else {
|
|
1781
|
-
const first = client.guilds.cache.first();
|
|
1782
|
-
if (!first) {
|
|
1783
|
-
cliLogger.error('No guild found. Add the bot to a server first.');
|
|
1784
|
-
client.destroy();
|
|
1785
|
-
process.exit(EXIT_NO_RESTART);
|
|
1786
|
-
}
|
|
1787
|
-
guild = first;
|
|
1788
|
-
}
|
|
1789
|
-
const { textChannelId, channelName } = await createProjectChannels({
|
|
1790
|
-
guild,
|
|
1791
|
-
projectDirectory,
|
|
1792
|
-
appId,
|
|
1793
|
-
botName: client.user?.username,
|
|
1794
|
-
});
|
|
1795
|
-
client.destroy();
|
|
1796
|
-
const channelUrl = `https://discord.com/channels/${guild.id}/${textChannelId}`;
|
|
1797
|
-
note(`Created project: ${sanitizedName}\n\nDirectory: ${projectDirectory}\nChannel: #${channelName}\nURL: ${channelUrl}`, '✅ Success');
|
|
1798
|
-
cliLogger.log(channelUrl);
|
|
1799
|
-
process.exit(0);
|
|
1800
|
-
}
|
|
1801
|
-
catch (error) {
|
|
1802
|
-
cliLogger.error('Error:', error instanceof Error ? error.message : String(error));
|
|
1803
|
-
process.exit(EXIT_NO_RESTART);
|
|
1878
|
+
guild = first;
|
|
1804
1879
|
}
|
|
1880
|
+
const { textChannelId, channelName } = await createProjectChannels({
|
|
1881
|
+
guild,
|
|
1882
|
+
projectDirectory,
|
|
1883
|
+
appId,
|
|
1884
|
+
botName: client.user?.username,
|
|
1885
|
+
});
|
|
1886
|
+
client.destroy();
|
|
1887
|
+
const channelUrl = `https://discord.com/channels/${guild.id}/${textChannelId}`;
|
|
1888
|
+
note(`Created project: ${sanitizedName}\n\nDirectory: ${projectDirectory}\nChannel: #${channelName}\nURL: ${channelUrl}`, '✅ Success');
|
|
1889
|
+
cliLogger.log(channelUrl);
|
|
1890
|
+
process.exit(0);
|
|
1805
1891
|
});
|
|
1806
1892
|
cli
|
|
1807
1893
|
.command('tunnel', 'Expose a local port via tunnel')
|