kimaki 0.4.50 → 0.4.51

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.
@@ -40,34 +40,38 @@ export async function ensureKimakiAudioCategory(guild, botName) {
40
40
  type: ChannelType.GuildCategory,
41
41
  });
42
42
  }
43
- export async function createProjectChannels({ guild, projectDirectory, appId, botName, }) {
43
+ export async function createProjectChannels({ guild, projectDirectory, appId, botName, enableVoiceChannels = false, }) {
44
44
  const baseName = path.basename(projectDirectory);
45
45
  const channelName = `${baseName}`
46
46
  .toLowerCase()
47
47
  .replace(/[^a-z0-9-]/g, '-')
48
48
  .slice(0, 100);
49
49
  const kimakiCategory = await ensureKimakiCategory(guild, botName);
50
- const kimakiAudioCategory = await ensureKimakiAudioCategory(guild, botName);
51
50
  const textChannel = await guild.channels.create({
52
51
  name: channelName,
53
52
  type: ChannelType.GuildText,
54
53
  parent: kimakiCategory,
55
54
  // Channel configuration is stored in SQLite, not in the topic
56
55
  });
57
- const voiceChannel = await guild.channels.create({
58
- name: channelName,
59
- type: ChannelType.GuildVoice,
60
- parent: kimakiAudioCategory,
61
- });
62
56
  getDatabase()
63
57
  .prepare('INSERT OR REPLACE INTO channel_directories (channel_id, directory, channel_type, app_id) VALUES (?, ?, ?, ?)')
64
58
  .run(textChannel.id, projectDirectory, 'text', appId);
65
- getDatabase()
66
- .prepare('INSERT OR REPLACE INTO channel_directories (channel_id, directory, channel_type, app_id) VALUES (?, ?, ?, ?)')
67
- .run(voiceChannel.id, projectDirectory, 'voice', appId);
59
+ let voiceChannelId = null;
60
+ if (enableVoiceChannels) {
61
+ const kimakiAudioCategory = await ensureKimakiAudioCategory(guild, botName);
62
+ const voiceChannel = await guild.channels.create({
63
+ name: channelName,
64
+ type: ChannelType.GuildVoice,
65
+ parent: kimakiAudioCategory,
66
+ });
67
+ getDatabase()
68
+ .prepare('INSERT OR REPLACE INTO channel_directories (channel_id, directory, channel_type, app_id) VALUES (?, ?, ?, ?)')
69
+ .run(voiceChannel.id, projectDirectory, 'voice', appId);
70
+ voiceChannelId = voiceChannel.id;
71
+ }
68
72
  return {
69
73
  textChannelId: textChannel.id,
70
- voiceChannelId: voiceChannel.id,
74
+ voiceChannelId,
71
75
  channelName,
72
76
  };
73
77
  }
package/dist/cli.js CHANGED
@@ -428,7 +428,7 @@ async function backgroundInit({ currentDir, token, appId, }) {
428
428
  cliLogger.error('Background init failed:', error instanceof Error ? error.message : String(error));
429
429
  }
430
430
  }
431
- async function run({ restart, addChannels, useWorktrees }) {
431
+ async function run({ restart, addChannels, useWorktrees, enableVoiceChannels }) {
432
432
  startCaffeinate();
433
433
  const forceSetup = Boolean(restart);
434
434
  intro('šŸ¤– Discord Bot Setup');
@@ -777,6 +777,7 @@ async function run({ restart, addChannels, useWorktrees }) {
777
777
  projectDirectory: project.worktree,
778
778
  appId,
779
779
  botName: discordClient.user?.username,
780
+ enableVoiceChannels,
780
781
  });
781
782
  createdChannels.push({
782
783
  name: channelName,
@@ -823,6 +824,7 @@ cli
823
824
  .option('--data-dir <path>', 'Data directory for config and database (default: ~/.kimaki)')
824
825
  .option('--install-url', 'Print the bot install URL and exit')
825
826
  .option('--use-worktrees', 'Create git worktrees for all new sessions started from channel messages')
827
+ .option('--enable-voice-channels', 'Create voice channels for projects (disabled by default)')
826
828
  .option('--verbosity <level>', 'Default verbosity for all channels (tools-and-text, text-and-essential-tools, or text-only)')
827
829
  .action(async (options) => {
828
830
  try {
@@ -859,6 +861,7 @@ cli
859
861
  addChannels: options.addChannels,
860
862
  dataDir: options.dataDir,
861
863
  useWorktrees: options.useWorktrees,
864
+ enableVoiceChannels: options.enableVoiceChannels,
862
865
  });
863
866
  }
864
867
  catch (error) {
@@ -52,7 +52,8 @@ export async function handleAddProjectCommand({ command, appId }) {
52
52
  appId,
53
53
  botName: command.client.user?.username,
54
54
  });
55
- await command.editReply(`āœ… Created channels for project:\nšŸ“ Text: <#${textChannelId}>\nšŸ”Š Voice: <#${voiceChannelId}>\nšŸ“ Directory: \`${directory}\``);
55
+ const voiceInfo = voiceChannelId ? `\nšŸ”Š Voice: <#${voiceChannelId}>` : '';
56
+ await command.editReply(`āœ… Created channels for project:\nšŸ“ Text: <#${textChannelId}>${voiceInfo}\nšŸ“ Directory: \`${directory}\``);
56
57
  logger.log(`Created channels for project ${channelName} at ${directory}`);
57
58
  }
58
59
  catch (error) {
@@ -83,7 +83,8 @@ export async function handleCreateNewProjectCommand({ command, appId, }) {
83
83
  }
84
84
  const { textChannelId, voiceChannelId, channelName, projectDirectory, sanitizedName } = result;
85
85
  const textChannel = (await guild.channels.fetch(textChannelId));
86
- await command.editReply(`āœ… Created new project **${sanitizedName}**\nšŸ“ Directory: \`${projectDirectory}\`\nšŸ“ Text: <#${textChannelId}>\nšŸ”Š Voice: <#${voiceChannelId}>\n_Starting session..._`);
86
+ const voiceInfo = voiceChannelId ? `\nšŸ”Š Voice: <#${voiceChannelId}>` : '';
87
+ await command.editReply(`āœ… Created new project **${sanitizedName}**\nšŸ“ Directory: \`${projectDirectory}\`\nšŸ“ Text: <#${textChannelId}>${voiceInfo}\n_Starting session..._`);
87
88
  const starterMessage = await textChannel.send({
88
89
  content: `šŸš€ **New project initialized**\nšŸ“ \`${projectDirectory}\``,
89
90
  flags: SILENT_MESSAGE_FLAGS,
@@ -117,6 +117,17 @@ export async function startDiscordBot({ token, appId, discordClient, useWorktree
117
117
  }
118
118
  }
119
119
  if (message.guild && message.member) {
120
+ // Check for "no-kimaki" role first - blocks user regardless of other permissions.
121
+ // This implements the "four-eyes principle": even owners must remove this role
122
+ // to use the bot, adding friction to prevent accidental usage.
123
+ const hasNoKimakiRole = message.member.roles.cache.some((role) => role.name.toLowerCase() === 'no-kimaki');
124
+ if (hasNoKimakiRole) {
125
+ await message.reply({
126
+ content: `You have the **no-kimaki** role which blocks bot access.\nRemove this role to use Kimaki.`,
127
+ flags: SILENT_MESSAGE_FLAGS,
128
+ });
129
+ return;
130
+ }
120
131
  const isOwner = message.member.id === message.guild.ownerId;
121
132
  const isAdmin = message.member.permissions.has(PermissionsBitField.Flags.Administrator);
122
133
  const canManageServer = message.member.permissions.has(PermissionsBitField.Flags.ManageGuild);
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "kimaki",
3
3
  "module": "index.ts",
4
4
  "type": "module",
5
- "version": "0.4.50",
5
+ "version": "0.4.51",
6
6
  "repository": "https://github.com/remorses/kimaki",
7
7
  "bin": "bin.js",
8
8
  "files": [
@@ -63,12 +63,14 @@ export async function createProjectChannels({
63
63
  projectDirectory,
64
64
  appId,
65
65
  botName,
66
+ enableVoiceChannels = false,
66
67
  }: {
67
68
  guild: Guild
68
69
  projectDirectory: string
69
70
  appId: string
70
71
  botName?: string
71
- }): Promise<{ textChannelId: string; voiceChannelId: string; channelName: string }> {
72
+ enableVoiceChannels?: boolean
73
+ }): Promise<{ textChannelId: string; voiceChannelId: string | null; channelName: string }> {
72
74
  const baseName = path.basename(projectDirectory)
73
75
  const channelName = `${baseName}`
74
76
  .toLowerCase()
@@ -76,7 +78,6 @@ export async function createProjectChannels({
76
78
  .slice(0, 100)
77
79
 
78
80
  const kimakiCategory = await ensureKimakiCategory(guild, botName)
79
- const kimakiAudioCategory = await ensureKimakiAudioCategory(guild, botName)
80
81
 
81
82
  const textChannel = await guild.channels.create({
82
83
  name: channelName,
@@ -85,27 +86,35 @@ export async function createProjectChannels({
85
86
  // Channel configuration is stored in SQLite, not in the topic
86
87
  })
87
88
 
88
- const voiceChannel = await guild.channels.create({
89
- name: channelName,
90
- type: ChannelType.GuildVoice,
91
- parent: kimakiAudioCategory,
92
- })
93
-
94
89
  getDatabase()
95
90
  .prepare(
96
91
  'INSERT OR REPLACE INTO channel_directories (channel_id, directory, channel_type, app_id) VALUES (?, ?, ?, ?)',
97
92
  )
98
93
  .run(textChannel.id, projectDirectory, 'text', appId)
99
94
 
100
- getDatabase()
101
- .prepare(
102
- 'INSERT OR REPLACE INTO channel_directories (channel_id, directory, channel_type, app_id) VALUES (?, ?, ?, ?)',
103
- )
104
- .run(voiceChannel.id, projectDirectory, 'voice', appId)
95
+ let voiceChannelId: string | null = null
96
+
97
+ if (enableVoiceChannels) {
98
+ const kimakiAudioCategory = await ensureKimakiAudioCategory(guild, botName)
99
+
100
+ const voiceChannel = await guild.channels.create({
101
+ name: channelName,
102
+ type: ChannelType.GuildVoice,
103
+ parent: kimakiAudioCategory,
104
+ })
105
+
106
+ getDatabase()
107
+ .prepare(
108
+ 'INSERT OR REPLACE INTO channel_directories (channel_id, directory, channel_type, app_id) VALUES (?, ?, ?, ?)',
109
+ )
110
+ .run(voiceChannel.id, projectDirectory, 'voice', appId)
111
+
112
+ voiceChannelId = voiceChannel.id
113
+ }
105
114
 
106
115
  return {
107
116
  textChannelId: textChannel.id,
108
- voiceChannelId: voiceChannel.id,
117
+ voiceChannelId,
109
118
  channelName,
110
119
  }
111
120
  }
package/src/cli.ts CHANGED
@@ -209,6 +209,7 @@ type CliOptions = {
209
209
  addChannels?: boolean
210
210
  dataDir?: string
211
211
  useWorktrees?: boolean
212
+ enableVoiceChannels?: boolean
212
213
  }
213
214
 
214
215
  // Commands to skip when registering user commands (reserved names)
@@ -595,7 +596,7 @@ async function backgroundInit({
595
596
  }
596
597
  }
597
598
 
598
- async function run({ restart, addChannels, useWorktrees }: CliOptions) {
599
+ async function run({ restart, addChannels, useWorktrees, enableVoiceChannels }: CliOptions) {
599
600
  startCaffeinate()
600
601
 
601
602
  const forceSetup = Boolean(restart)
@@ -1057,6 +1058,7 @@ async function run({ restart, addChannels, useWorktrees }: CliOptions) {
1057
1058
  projectDirectory: project.worktree,
1058
1059
  appId,
1059
1060
  botName: discordClient.user?.username,
1061
+ enableVoiceChannels,
1060
1062
  })
1061
1063
 
1062
1064
  createdChannels.push({
@@ -1123,6 +1125,7 @@ cli
1123
1125
  .option('--data-dir <path>', 'Data directory for config and database (default: ~/.kimaki)')
1124
1126
  .option('--install-url', 'Print the bot install URL and exit')
1125
1127
  .option('--use-worktrees', 'Create git worktrees for all new sessions started from channel messages')
1128
+ .option('--enable-voice-channels', 'Create voice channels for projects (disabled by default)')
1126
1129
  .option('--verbosity <level>', 'Default verbosity for all channels (tools-and-text, text-and-essential-tools, or text-only)')
1127
1130
  .action(
1128
1131
  async (options: {
@@ -1131,6 +1134,7 @@ cli
1131
1134
  dataDir?: string
1132
1135
  installUrl?: boolean
1133
1136
  useWorktrees?: boolean
1137
+ enableVoiceChannels?: boolean
1134
1138
  verbosity?: string
1135
1139
  }) => {
1136
1140
  try {
@@ -1172,6 +1176,7 @@ cli
1172
1176
  addChannels: options.addChannels,
1173
1177
  dataDir: options.dataDir,
1174
1178
  useWorktrees: options.useWorktrees,
1179
+ enableVoiceChannels: options.enableVoiceChannels,
1175
1180
  })
1176
1181
  } catch (error) {
1177
1182
  cliLogger.error('Unhandled error:', error instanceof Error ? error.message : String(error))
@@ -72,8 +72,9 @@ export async function handleAddProjectCommand({ command, appId }: CommandContext
72
72
  botName: command.client.user?.username,
73
73
  })
74
74
 
75
+ const voiceInfo = voiceChannelId ? `\nšŸ”Š Voice: <#${voiceChannelId}>` : ''
75
76
  await command.editReply(
76
- `āœ… Created channels for project:\nšŸ“ Text: <#${textChannelId}>\nšŸ”Š Voice: <#${voiceChannelId}>\nšŸ“ Directory: \`${directory}\``,
77
+ `āœ… Created channels for project:\nšŸ“ Text: <#${textChannelId}>${voiceInfo}\nšŸ“ Directory: \`${directory}\``,
77
78
  )
78
79
 
79
80
  logger.log(`Created channels for project ${channelName} at ${directory}`)
@@ -31,7 +31,7 @@ export async function createNewProject({
31
31
  botName?: string
32
32
  }): Promise<{
33
33
  textChannelId: string
34
- voiceChannelId: string
34
+ voiceChannelId: string | null
35
35
  channelName: string
36
36
  projectDirectory: string
37
37
  sanitizedName: string
@@ -124,8 +124,9 @@ export async function handleCreateNewProjectCommand({
124
124
  const { textChannelId, voiceChannelId, channelName, projectDirectory, sanitizedName } = result
125
125
  const textChannel = (await guild.channels.fetch(textChannelId)) as TextChannel
126
126
 
127
+ const voiceInfo = voiceChannelId ? `\nšŸ”Š Voice: <#${voiceChannelId}>` : ''
127
128
  await command.editReply(
128
- `āœ… Created new project **${sanitizedName}**\nšŸ“ Directory: \`${projectDirectory}\`\nšŸ“ Text: <#${textChannelId}>\nšŸ”Š Voice: <#${voiceChannelId}>\n_Starting session..._`,
129
+ `āœ… Created new project **${sanitizedName}**\nšŸ“ Directory: \`${projectDirectory}\`\nšŸ“ Text: <#${textChannelId}>${voiceInfo}\n_Starting session..._`,
129
130
  )
130
131
 
131
132
  const starterMessage = await textChannel.send({
@@ -193,6 +193,20 @@ export async function startDiscordBot({
193
193
  }
194
194
 
195
195
  if (message.guild && message.member) {
196
+ // Check for "no-kimaki" role first - blocks user regardless of other permissions.
197
+ // This implements the "four-eyes principle": even owners must remove this role
198
+ // to use the bot, adding friction to prevent accidental usage.
199
+ const hasNoKimakiRole = message.member.roles.cache.some(
200
+ (role) => role.name.toLowerCase() === 'no-kimaki',
201
+ )
202
+ if (hasNoKimakiRole) {
203
+ await message.reply({
204
+ content: `You have the **no-kimaki** role which blocks bot access.\nRemove this role to use Kimaki.`,
205
+ flags: SILENT_MESSAGE_FLAGS,
206
+ })
207
+ return
208
+ }
209
+
196
210
  const isOwner = message.member.id === message.guild.ownerId
197
211
  const isAdmin = message.member.permissions.has(PermissionsBitField.Flags.Administrator)
198
212
  const canManageServer = message.member.permissions.has(