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 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
- try {
1524
- const absolutePath = path.resolve(directory || '.');
1525
- if (!fs.existsSync(absolutePath)) {
1526
- cliLogger.error(`Directory does not exist: ${absolutePath}`);
1527
- process.exit(EXIT_NO_RESTART);
1528
- }
1529
- // Initialize database
1530
- await initDatabase();
1531
- // Get bot token from env var or database
1532
- const envToken = process.env.KIMAKI_BOT_TOKEN;
1533
- let botToken;
1534
- let appId = options.appId;
1535
- if (envToken) {
1536
- botToken = envToken;
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
- if (botRow) {
1551
- botToken = botRow.token;
1552
- appId = appId || botRow.app_id;
1553
- }
1589
+ appId = botRow?.app_id;
1554
1590
  }
1555
- catch (e) {
1556
- cliLogger.error('Database error:', e instanceof Error ? e.message : String(e));
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
- if (!botToken) {
1560
- cliLogger.error('No bot token found. Set KIMAKI_BOT_TOKEN env var or run `kimaki` first to set up.');
1561
- process.exit(EXIT_NO_RESTART);
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
- if (!appId) {
1564
- cliLogger.error('App ID is required to create channels. Use --app-id or run `kimaki` first.');
1565
- process.exit(EXIT_NO_RESTART);
1604
+ catch (e) {
1605
+ cliLogger.error('Database error:', e instanceof Error ? e.message : String(e));
1566
1606
  }
1567
- cliLogger.log('Connecting to Discord...');
1568
- const client = await createDiscordClient();
1569
- await new Promise((resolve, reject) => {
1570
- client.once(Events.ClientReady, () => {
1571
- resolve();
1572
- });
1573
- client.once(Events.Error, reject);
1574
- client.login(botToken);
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
- cliLogger.log('Finding guild...');
1577
- // Find guild
1578
- let guild;
1579
- if (options.guild) {
1580
- const guildId = String(options.guild);
1581
- const foundGuild = client.guilds.cache.get(guildId);
1582
- if (!foundGuild) {
1583
- cliLogger.log('Guild not found');
1584
- cliLogger.error(`Guild not found: ${guildId}`);
1585
- client.destroy();
1586
- process.exit(EXIT_NO_RESTART);
1587
- }
1588
- guild = foundGuild;
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
- else {
1591
- // Auto-detect: prefer guild with existing channels for this bot, else first guild
1592
- const existingChannelId = await findChannelByAppId(appId);
1593
- if (existingChannelId) {
1594
- try {
1595
- const ch = await client.channels.fetch(existingChannelId);
1596
- if (ch && 'guild' in ch && ch.guild) {
1597
- guild = ch.guild;
1598
- }
1599
- else {
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
- catch (error) {
1604
- cliLogger.debug('Failed to fetch existing channel while selecting guild:', error instanceof Error ? error.message : String(error));
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
- else {
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
- // Check if channel already exists in this guild
1643
- cliLogger.log('Checking for existing channel...');
1644
- try {
1645
- const existingChannels = await findChannelsByDirectory({
1646
- directory: absolutePath,
1647
- channelType: 'text',
1648
- appId,
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
- catch (error) {
1665
- cliLogger.debug('Database lookup failed while checking existing channels:', error instanceof Error ? error.message : String(error));
1666
- }
1667
- cliLogger.log(`Creating channels in ${guild.name}...`);
1668
- const { textChannelId, voiceChannelId, channelName } = await createProjectChannels({
1669
- guild,
1670
- projectDirectory: absolutePath,
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
- client.destroy();
1675
- cliLogger.log('Channels created!');
1676
- const channelUrl = `https://discord.com/channels/${guild.id}/${textChannelId}`;
1677
- note(`Created channels for project:\n\n📝 Text: #${channelName}\n🔊 Voice: #${channelName}\n📁 Directory: ${absolutePath}\n\nURL: ${channelUrl}`, '✅ Success');
1678
- cliLogger.log(channelUrl);
1679
- process.exit(0);
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.error('Error:', error instanceof Error ? error.message : String(error));
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
- try {
1691
- await initDatabase();
1692
- const prisma = await getPrisma();
1693
- const channels = await prisma.channel_directories.findMany({
1694
- where: { channel_type: 'text' },
1695
- orderBy: { created_at: 'desc' },
1696
- });
1697
- if (options.json) {
1698
- const output = channels.map((ch) => ({
1699
- channel_id: ch.channel_id,
1700
- directory: ch.directory,
1701
- app_id: ch.app_id,
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
- catch (error) {
1722
- cliLogger.error('Error:', error instanceof Error ? error.message : String(error));
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
- try {
1731
- const sanitizedName = name
1732
- .toLowerCase()
1733
- .replace(/[^a-z0-9-]/g, '-')
1734
- .replace(/-+/g, '-')
1735
- .replace(/^-|-$/g, '')
1736
- .slice(0, 100);
1737
- if (!sanitizedName) {
1738
- cliLogger.error('Invalid project name');
1739
- process.exit(EXIT_NO_RESTART);
1740
- }
1741
- await initDatabase();
1742
- const botRow = await getBotToken();
1743
- if (!botRow) {
1744
- cliLogger.error('No bot configured. Run `kimaki` first.');
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
- const { app_id: appId, token: botToken } = botRow;
1748
- const projectsDir = getProjectsDir();
1749
- const projectDirectory = path.join(projectsDir, sanitizedName);
1750
- if (!fs.existsSync(projectsDir)) {
1751
- fs.mkdirSync(projectsDir, { recursive: true });
1752
- }
1753
- if (fs.existsSync(projectDirectory)) {
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
- fs.mkdirSync(projectDirectory, { recursive: true });
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')