kimaki 0.1.4 → 0.2.0

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/tools.js CHANGED
@@ -9,12 +9,14 @@ import { formatDistanceToNow } from 'date-fns';
9
9
  import { ShareMarkdown } from './markdown.js';
10
10
  import pc from 'picocolors';
11
11
  import { initializeOpencodeForDirectory } from './discordBot.js';
12
- export async function getTools({ onMessageCompleted, directory, }) {
12
+ export async function getTools({ onMessageCompleted, onAllSessionsCompleted, directory, }) {
13
13
  const getClient = await initializeOpencodeForDirectory(directory);
14
14
  const client = getClient();
15
15
  const markdownRenderer = new ShareMarkdown(client);
16
16
  const providersResponse = await client.config.providers({});
17
17
  const providers = providersResponse.data?.providers || [];
18
+ // Track all active OpenCode sessions
19
+ const activeSessions = new Set();
18
20
  // Helper: get last assistant model for a session (non-summary)
19
21
  const getSessionModel = async (sessionId) => {
20
22
  const res = await getClient().session.messages({ path: { id: sessionId } });
@@ -41,6 +43,9 @@ export async function getTools({ onMessageCompleted, directory, }) {
41
43
  }),
42
44
  execute: async ({ sessionId, message }) => {
43
45
  const sessionModel = await getSessionModel(sessionId);
46
+ // Track this session as active
47
+ activeSessions.add(sessionId);
48
+ toolsLogger.log(`Session ${sessionId} started, ${activeSessions.size} active sessions`);
44
49
  // do not await
45
50
  getClient()
46
51
  .session.prompt({
@@ -61,6 +66,14 @@ export async function getTools({ onMessageCompleted, directory, }) {
61
66
  data: response.data,
62
67
  markdown,
63
68
  });
69
+ // Remove from active sessions
70
+ activeSessions.delete(sessionId);
71
+ toolsLogger.log(`Session ${sessionId} completed, ${activeSessions.size} active sessions remaining`);
72
+ // Check if all sessions are complete
73
+ if (activeSessions.size === 0) {
74
+ toolsLogger.log('All sessions completed');
75
+ onAllSessionsCompleted?.();
76
+ }
64
77
  })
65
78
  .catch((error) => {
66
79
  onMessageCompleted?.({
@@ -68,6 +81,14 @@ export async function getTools({ onMessageCompleted, directory, }) {
68
81
  messageId: '',
69
82
  error,
70
83
  });
84
+ // Remove from active sessions even on error
85
+ activeSessions.delete(sessionId);
86
+ toolsLogger.log(`Session ${sessionId} failed, ${activeSessions.size} active sessions remaining`);
87
+ // Check if all sessions are complete
88
+ if (activeSessions.size === 0) {
89
+ toolsLogger.log('All sessions completed');
90
+ onAllSessionsCompleted?.();
91
+ }
71
92
  });
72
93
  return {
73
94
  success: true,
@@ -108,32 +129,52 @@ export async function getTools({ onMessageCompleted, directory, }) {
108
129
  if (!session.data) {
109
130
  throw new Error('Failed to create session');
110
131
  }
132
+ const newSessionId = session.data.id;
133
+ // Track this session as active
134
+ activeSessions.add(newSessionId);
135
+ toolsLogger.log(`New session ${newSessionId} created, ${activeSessions.size} active sessions`);
111
136
  // do not await
112
137
  getClient()
113
138
  .session.prompt({
114
- path: { id: session.data.id },
139
+ path: { id: newSessionId },
115
140
  body: {
116
141
  parts: [{ type: 'text', text: message }],
117
142
  },
118
143
  })
119
144
  .then(async (response) => {
120
145
  const markdown = await markdownRenderer.generate({
121
- sessionID: session.data.id,
146
+ sessionID: newSessionId,
122
147
  lastAssistantOnly: true,
123
148
  });
124
149
  onMessageCompleted?.({
125
- sessionId: session.data.id,
150
+ sessionId: newSessionId,
126
151
  messageId: '',
127
152
  data: response.data,
128
153
  markdown,
129
154
  });
155
+ // Remove from active sessions
156
+ activeSessions.delete(newSessionId);
157
+ toolsLogger.log(`Session ${newSessionId} completed, ${activeSessions.size} active sessions remaining`);
158
+ // Check if all sessions are complete
159
+ if (activeSessions.size === 0) {
160
+ toolsLogger.log('All sessions completed');
161
+ onAllSessionsCompleted?.();
162
+ }
130
163
  })
131
164
  .catch((error) => {
132
165
  onMessageCompleted?.({
133
- sessionId: session.data.id,
166
+ sessionId: newSessionId,
134
167
  messageId: '',
135
168
  error,
136
169
  });
170
+ // Remove from active sessions even on error
171
+ activeSessions.delete(newSessionId);
172
+ toolsLogger.log(`Session ${newSessionId} failed, ${activeSessions.size} active sessions remaining`);
173
+ // Check if all sessions are complete
174
+ if (activeSessions.size === 0) {
175
+ toolsLogger.log('All sessions completed');
176
+ onAllSessionsCompleted?.();
177
+ }
137
178
  });
138
179
  return {
139
180
  success: true,
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "kimaki",
3
3
  "module": "index.ts",
4
4
  "type": "module",
5
- "version": "0.1.4",
5
+ "version": "0.2.0",
6
6
  "repository": "https://github.com/remorses/kimaki",
7
7
  "bin": "bin.js",
8
8
  "files": [
package/src/cli.ts CHANGED
@@ -35,6 +35,7 @@ import {
35
35
  import path from 'node:path'
36
36
  import fs from 'node:fs'
37
37
  import { createLogger } from './logger.js'
38
+ import { spawnSync, execSync, type ExecSyncOptions } from 'node:child_process'
38
39
 
39
40
  const cliLogger = createLogger('CLI')
40
41
  const cli = cac('kimaki')
@@ -142,6 +143,73 @@ async function run({ restart, addChannels }: CliOptions) {
142
143
 
143
144
  intro('🤖 Discord Bot Setup')
144
145
 
146
+ // Step 0: Check if OpenCode CLI is available
147
+ const opencodeCheck = spawnSync('which', ['opencode'], { shell: true })
148
+
149
+ if (opencodeCheck.status !== 0) {
150
+ note(
151
+ 'OpenCode CLI is required but not found in your PATH.',
152
+ '⚠️ OpenCode Not Found',
153
+ )
154
+
155
+ const shouldInstall = await confirm({
156
+ message: 'Would you like to install OpenCode right now?',
157
+ })
158
+
159
+ if (isCancel(shouldInstall) || !shouldInstall) {
160
+ cancel('OpenCode CLI is required to run this bot')
161
+ process.exit(0)
162
+ }
163
+
164
+ const s = spinner()
165
+ s.start('Installing OpenCode CLI...')
166
+
167
+ try {
168
+ execSync('curl -fsSL https://opencode.ai/install | bash', {
169
+ stdio: 'inherit',
170
+ shell: '/bin/bash',
171
+ })
172
+ s.stop('OpenCode CLI installed successfully!')
173
+
174
+ // The install script adds opencode to PATH via shell configuration
175
+ // For the current process, we need to check common installation paths
176
+ const possiblePaths = [
177
+ `${process.env.HOME}/.local/bin/opencode`,
178
+ `${process.env.HOME}/.opencode/bin/opencode`,
179
+ '/usr/local/bin/opencode',
180
+ '/opt/opencode/bin/opencode',
181
+ ]
182
+
183
+ const installedPath = possiblePaths.find((p) => {
184
+ try {
185
+ fs.accessSync(p, fs.constants.F_OK)
186
+ return true
187
+ } catch {
188
+ return false
189
+ }
190
+ })
191
+
192
+ if (!installedPath) {
193
+ note(
194
+ 'OpenCode was installed but may not be available in this session.\n' +
195
+ 'Please restart your terminal and run this command again.',
196
+ '⚠️ Restart Required',
197
+ )
198
+ process.exit(0)
199
+ }
200
+
201
+ // For subsequent spawn calls in this session, we can use the full path
202
+ process.env.OPENCODE_PATH = installedPath
203
+ } catch (error) {
204
+ s.stop('Failed to install OpenCode CLI')
205
+ cliLogger.error(
206
+ 'Installation error:',
207
+ error instanceof Error ? error.message : String(error),
208
+ )
209
+ process.exit(EXIT_NO_RESTART)
210
+ }
211
+ }
212
+
145
213
  const db = getDatabase()
146
214
  let appId: string
147
215
  let token: string
@@ -377,7 +445,7 @@ async function run({ restart, addChannels }: CliOptions) {
377
445
  let projects: Project[] = []
378
446
 
379
447
  try {
380
- const projectsResponse = await getClient().project.list()
448
+ const projectsResponse = await getClient().project.list({})
381
449
  if (!projectsResponse.data) {
382
450
  throw new Error('Failed to fetch projects')
383
451
  }
@@ -436,22 +504,24 @@ async function run({ restart, addChannels }: CliOptions) {
436
504
 
437
505
  if (guilds.length === 1) {
438
506
  targetGuild = guilds[0]!
507
+ note(`Using server: ${targetGuild.name}`, 'Server Selected')
439
508
  } else {
440
- const guildId = await text({
441
- message: 'Enter the Discord server ID to create channels in:',
442
- placeholder: guilds[0]?.id,
443
- validate(value) {
444
- if (!value) return 'Server ID is required'
445
- if (!guilds.find((g) => g.id === value)) return 'Invalid server ID'
446
- },
509
+ const guildSelection = await multiselect({
510
+ message: 'Select a Discord server to create channels in:',
511
+ options: guilds.map((guild) => ({
512
+ value: guild.id,
513
+ label: `${guild.name} (${guild.memberCount} members)`,
514
+ })),
515
+ required: true,
516
+ maxItems: 1,
447
517
  })
448
518
 
449
- if (isCancel(guildId)) {
519
+ if (isCancel(guildSelection)) {
450
520
  cancel('Setup cancelled')
451
521
  process.exit(0)
452
522
  }
453
523
 
454
- targetGuild = guilds.find((g) => g.id === guildId)!
524
+ targetGuild = guilds.find((g) => g.id === guildSelection[0])!
455
525
  }
456
526
 
457
527
  s.start('Creating Discord channels...')