onkol 0.1.0 → 0.3.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.
Files changed (2) hide show
  1. package/dist/cli/index.js +340 -169
  2. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -16,18 +16,135 @@ program
16
16
  .name('onkol')
17
17
  .description('Decentralized on-call agent system')
18
18
  .version('0.1.0');
19
+ function loadCheckpoint(homeDir) {
20
+ const checkpointPath = resolve(homeDir, '.onkol-setup-checkpoint.json');
21
+ if (existsSync(checkpointPath)) {
22
+ try {
23
+ return JSON.parse(readFileSync(checkpointPath, 'utf-8'));
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ }
29
+ return null;
30
+ }
31
+ function saveCheckpoint(homeDir, checkpoint) {
32
+ writeFileSync(resolve(homeDir, '.onkol-setup-checkpoint.json'), JSON.stringify(checkpoint, null, 2));
33
+ }
34
+ function clearCheckpoint(homeDir) {
35
+ const p = resolve(homeDir, '.onkol-setup-checkpoint.json');
36
+ if (existsSync(p)) {
37
+ const { unlinkSync } = require('fs');
38
+ unlinkSync(p);
39
+ }
40
+ }
41
+ function markStep(homeDir, checkpoint, step) {
42
+ checkpoint.completed.push(step);
43
+ saveCheckpoint(homeDir, checkpoint);
44
+ }
45
+ function checkDependencies() {
46
+ console.log(chalk.bold('Checking dependencies...\n'));
47
+ const deps = [
48
+ {
49
+ name: 'claude',
50
+ check: 'claude --version',
51
+ installHint: 'Install Claude Code: https://docs.anthropic.com/en/docs/claude-code/getting-started',
52
+ required: true,
53
+ },
54
+ {
55
+ name: 'bun',
56
+ check: 'bun --version',
57
+ installHint: 'Install Bun: curl -fsSL https://bun.sh/install | bash',
58
+ required: true,
59
+ },
60
+ {
61
+ name: 'tmux',
62
+ check: 'tmux -V',
63
+ installHint: 'Install tmux:\n Ubuntu/Debian: sudo apt install tmux\n RHEL/CentOS: sudo yum install tmux\n Arch: sudo pacman -S tmux\n macOS: brew install tmux',
64
+ required: true,
65
+ },
66
+ {
67
+ name: 'jq',
68
+ check: 'jq --version',
69
+ installHint: 'Install jq:\n Ubuntu/Debian: sudo apt install jq\n RHEL/CentOS: sudo yum install jq\n Arch: sudo pacman -S jq\n macOS: brew install jq',
70
+ required: true,
71
+ },
72
+ {
73
+ name: 'curl',
74
+ check: 'curl --version',
75
+ installHint: 'Install curl:\n Ubuntu/Debian: sudo apt install curl\n RHEL/CentOS: sudo yum install curl',
76
+ required: true,
77
+ },
78
+ ];
79
+ const missing = [];
80
+ for (const dep of deps) {
81
+ try {
82
+ execSync(dep.check, { stdio: 'pipe' });
83
+ console.log(chalk.green(` ✓ ${dep.name}`));
84
+ }
85
+ catch {
86
+ console.log(chalk.red(` ✗ ${dep.name} — not found`));
87
+ missing.push(dep);
88
+ }
89
+ }
90
+ if (missing.length > 0) {
91
+ console.log(chalk.red(`\nMissing ${missing.length} required dependencies:\n`));
92
+ for (const dep of missing) {
93
+ console.log(chalk.yellow(` ${dep.name}:`));
94
+ console.log(chalk.gray(` ${dep.installHint}\n`));
95
+ }
96
+ console.log(chalk.red('Install the missing dependencies and run `npx onkol setup` again.'));
97
+ process.exit(1);
98
+ }
99
+ console.log(chalk.green('\n All dependencies found.\n'));
100
+ }
19
101
  program
20
102
  .command('setup')
21
103
  .description('Set up an Onkol node on this VM')
22
104
  .action(async () => {
23
105
  console.log(chalk.bold('\nWelcome to Onkol Setup\n'));
106
+ // Check all dependencies before doing anything
107
+ checkDependencies();
24
108
  const homeDir = process.env.HOME || '/root';
25
- const answers = await runSetupPrompts(homeDir);
109
+ let answers;
110
+ let checkpoint;
111
+ // Check for existing checkpoint
112
+ const existing = loadCheckpoint(homeDir);
113
+ if (existing) {
114
+ const { resume } = await (await import('inquirer')).default.prompt([{
115
+ type: 'list',
116
+ name: 'resume',
117
+ message: `Found a previous setup attempt (${existing.completed.length} steps completed). What do you want to do?`,
118
+ choices: [
119
+ { name: `Resume from where it left off (node: ${existing.answers.nodeName})`, value: 'resume' },
120
+ { name: 'Start fresh', value: 'fresh' },
121
+ ],
122
+ }]);
123
+ if (resume === 'resume') {
124
+ answers = existing.answers;
125
+ checkpoint = existing;
126
+ console.log(chalk.green(`Resuming setup for "${answers.nodeName}". Skipping ${checkpoint.completed.length} completed steps.\n`));
127
+ }
128
+ else {
129
+ answers = await runSetupPrompts(homeDir);
130
+ checkpoint = { answers, completed: [] };
131
+ saveCheckpoint(homeDir, checkpoint);
132
+ }
133
+ }
134
+ else {
135
+ answers = await runSetupPrompts(homeDir);
136
+ checkpoint = { answers, completed: [] };
137
+ saveCheckpoint(homeDir, checkpoint);
138
+ }
26
139
  const dir = resolve(answers.installDir);
140
+ const skip = (step) => checkpoint.completed.includes(step);
27
141
  // Create directory structure
28
- console.log(chalk.gray('Creating directories...'));
29
- for (const sub of ['knowledge', 'workers', 'workers/.archive', 'scripts', 'plugins/discord-filtered', '.claude']) {
30
- mkdirSync(resolve(dir, sub), { recursive: true });
142
+ if (!skip('directories')) {
143
+ console.log(chalk.gray('Creating directories...'));
144
+ for (const sub of ['knowledge', 'workers', 'workers/.archive', 'scripts', 'plugins/discord-filtered', '.claude']) {
145
+ mkdirSync(resolve(dir, sub), { recursive: true });
146
+ }
147
+ markStep(homeDir, checkpoint, 'directories');
31
148
  }
32
149
  // Build allowed users list from Discord user ID prompt
33
150
  const user = process.env.USER || 'root';
@@ -36,182 +153,206 @@ program
36
153
  allowedUsers.push(answers.discordUserId.trim());
37
154
  }
38
155
  // --- CRITICAL: Create Discord category and orchestrator channel ---
39
- console.log(chalk.gray('Creating Discord category and channel...'));
40
- let category;
41
- let orchChannel;
42
- try {
43
- category = await createCategory(answers.botToken, answers.guildId, answers.nodeName);
44
- orchChannel = await createChannel(answers.botToken, answers.guildId, 'orchestrator', category.id);
156
+ let categoryId = checkpoint.categoryId || '';
157
+ let orchChannelId = checkpoint.orchChannelId || '';
158
+ if (!skip('discord')) {
159
+ console.log(chalk.gray('Creating Discord category and channel...'));
160
+ try {
161
+ const category = await createCategory(answers.botToken, answers.guildId, answers.nodeName);
162
+ const orchChannel = await createChannel(answers.botToken, answers.guildId, 'orchestrator', category.id);
163
+ categoryId = category.id;
164
+ orchChannelId = orchChannel.id;
165
+ checkpoint.categoryId = categoryId;
166
+ checkpoint.orchChannelId = orchChannelId;
167
+ markStep(homeDir, checkpoint, 'discord');
168
+ }
169
+ catch (err) {
170
+ console.error(chalk.red(`\nFATAL: Could not create Discord category/channel.`));
171
+ console.error(chalk.red(`${err instanceof Error ? err.message : err}`));
172
+ console.error(chalk.red('\nCheck that:'));
173
+ console.error(chalk.red(' 1. Your bot token is correct'));
174
+ console.error(chalk.red(' 2. Your server (guild) ID is correct'));
175
+ console.error(chalk.red(' 3. The bot has been invited to the server with "Manage Channels" permission'));
176
+ console.error(chalk.yellow('\nYour answers have been saved. Fix the issue and run `npx onkol setup` again to resume.'));
177
+ process.exit(1);
178
+ }
179
+ console.log(chalk.green('✓ Discord category and #orchestrator channel created'));
45
180
  }
46
- catch (err) {
47
- console.error(chalk.red(`\nFATAL: Could not create Discord category/channel.`));
48
- console.error(chalk.red(`${err instanceof Error ? err.message : err}`));
49
- console.error(chalk.red('\nCheck that:'));
50
- console.error(chalk.red(' 1. Your bot token is correct'));
51
- console.error(chalk.red(' 2. Your server (guild) ID is correct'));
52
- console.error(chalk.red(' 3. The bot has been invited to the server with "Manage Channels" permission'));
53
- process.exit(1);
181
+ else {
182
+ console.log(chalk.gray(' Discord category already created, skipping'));
54
183
  }
55
- console.log(chalk.green('✓ Discord category and #orchestrator channel created'));
56
184
  // Write config.json
57
- const config = {
58
- nodeName: answers.nodeName,
59
- botToken: answers.botToken,
60
- guildId: answers.guildId,
61
- categoryId: category.id,
62
- orchestratorChannelId: orchChannel.id,
63
- allowedUsers,
64
- maxWorkers: 3,
65
- installDir: dir,
66
- plugins: answers.plugins,
67
- };
68
- writeFileSync(resolve(dir, 'config.json'), JSON.stringify(config, null, 2), { mode: 0o600 });
69
- // Handle registry
70
- if (answers.registryMode === 'import' && answers.registryPath) {
71
- copyFileSync(answers.registryPath, resolve(dir, 'registry.json'));
72
- }
73
- else if (answers.registryMode !== 'prompt') {
74
- writeFileSync(resolve(dir, 'registry.json'), '{}');
75
- }
76
- // Handle services
77
- let servicesMd = '# Services\n\nNo services configured yet.\n';
78
- if (answers.serviceMode === 'auto') {
79
- console.log(chalk.gray('Discovering services...'));
80
- const services = discoverServices();
81
- servicesMd = formatServicesMarkdown(services);
82
- console.log(chalk.green(`Found ${services.length} services.`));
83
- }
84
- else if (answers.serviceMode === 'import' && answers.serviceSummaryPath) {
85
- servicesMd = readFileSync(answers.serviceSummaryPath, 'utf-8');
86
- }
87
- if (answers.serviceMode !== 'prompt') {
88
- writeFileSync(resolve(dir, 'services.md'), servicesMd);
185
+ if (!skip('config')) {
186
+ const config = {
187
+ nodeName: answers.nodeName,
188
+ botToken: answers.botToken,
189
+ guildId: answers.guildId,
190
+ categoryId,
191
+ orchestratorChannelId: orchChannelId,
192
+ allowedUsers,
193
+ maxWorkers: 3,
194
+ installDir: dir,
195
+ plugins: answers.plugins,
196
+ };
197
+ writeFileSync(resolve(dir, 'config.json'), JSON.stringify(config, null, 2), { mode: 0o600 });
198
+ markStep(homeDir, checkpoint, 'config');
89
199
  }
90
- // Generate CLAUDE.md
91
- const claudeMd = renderOrchestratorClaude({ nodeName: answers.nodeName, maxWorkers: 3 });
92
- writeFileSync(resolve(dir, 'CLAUDE.md'), claudeMd);
93
- // Generate .claude/settings.json
94
- const settings = renderSettings({ bashLogPath: resolve(dir, 'bash-log.txt') });
95
- writeFileSync(resolve(dir, '.claude/settings.json'), settings);
96
- // Write orchestrator .mcp.json
97
- const pluginPath = resolve(dir, 'plugins/discord-filtered/index.ts');
98
- const mcpJson = {
99
- mcpServers: {
100
- 'discord-filtered': {
101
- command: 'bun',
102
- args: [pluginPath],
103
- env: {
104
- DISCORD_BOT_TOKEN: answers.botToken,
105
- DISCORD_CHANNEL_ID: orchChannel.id,
106
- DISCORD_ALLOWED_USERS: JSON.stringify(allowedUsers),
200
+ // Write files (registry, services, CLAUDE.md, settings, mcp.json, state)
201
+ if (!skip('files')) {
202
+ // Handle registry
203
+ if (answers.registryMode === 'import' && answers.registryPath) {
204
+ copyFileSync(answers.registryPath, resolve(dir, 'registry.json'));
205
+ }
206
+ else if (answers.registryMode !== 'prompt') {
207
+ writeFileSync(resolve(dir, 'registry.json'), '{}');
208
+ }
209
+ // Handle services
210
+ let servicesMd = '# Services\n\nNo services configured yet.\n';
211
+ if (answers.serviceMode === 'auto') {
212
+ console.log(chalk.gray('Discovering services...'));
213
+ const services = discoverServices();
214
+ servicesMd = formatServicesMarkdown(services);
215
+ console.log(chalk.green(`Found ${services.length} services.`));
216
+ }
217
+ else if (answers.serviceMode === 'import' && answers.serviceSummaryPath) {
218
+ servicesMd = readFileSync(answers.serviceSummaryPath, 'utf-8');
219
+ }
220
+ if (answers.serviceMode !== 'prompt') {
221
+ writeFileSync(resolve(dir, 'services.md'), servicesMd);
222
+ }
223
+ // Generate CLAUDE.md, settings, mcp.json, state files
224
+ writeFileSync(resolve(dir, 'CLAUDE.md'), renderOrchestratorClaude({ nodeName: answers.nodeName, maxWorkers: 3 }));
225
+ writeFileSync(resolve(dir, '.claude/settings.json'), renderSettings({ bashLogPath: resolve(dir, 'bash-log.txt') }));
226
+ const pluginPath = resolve(dir, 'plugins/discord-filtered/index.ts');
227
+ const mcpJson = {
228
+ mcpServers: {
229
+ 'discord-filtered': {
230
+ command: 'bun',
231
+ args: [pluginPath],
232
+ env: {
233
+ DISCORD_BOT_TOKEN: answers.botToken,
234
+ DISCORD_CHANNEL_ID: orchChannelId,
235
+ DISCORD_ALLOWED_USERS: JSON.stringify(allowedUsers),
236
+ },
107
237
  },
108
238
  },
109
- },
110
- };
111
- writeFileSync(resolve(dir, '.mcp.json'), JSON.stringify(mcpJson, null, 2));
112
- // Initialize tracking and knowledge index
113
- writeFileSync(resolve(dir, 'workers/tracking.json'), '[]');
114
- writeFileSync(resolve(dir, 'knowledge/index.json'), '[]');
115
- writeFileSync(resolve(dir, 'state.md'), '');
116
- // Pre-accept Claude Code trust dialog for the onkol directory
117
- console.log(chalk.gray('Configuring Claude Code trust...'));
118
- const claudeJsonPath = resolve(homeDir, '.claude/.claude.json');
119
- try {
120
- const claudeJson = existsSync(claudeJsonPath) ? JSON.parse(readFileSync(claudeJsonPath, 'utf-8')) : {};
121
- if (!claudeJson.projects)
122
- claudeJson.projects = {};
123
- claudeJson.projects[dir] = {
124
- ...(claudeJson.projects[dir] || {}),
125
- allowedTools: [],
126
- hasTrustDialogAccepted: true,
127
239
  };
128
- writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2));
129
- console.log(chalk.green('✓ Claude Code trust pre-accepted for ' + dir));
130
- }
131
- catch {
132
- console.log(chalk.yellow('⚠ Could not pre-accept trust dialog. You may need to accept it manually on first run.'));
133
- }
134
- // Handle setup prompts
135
- const pendingPrompts = [];
136
- if (answers.registryPrompt) {
137
- pendingPrompts.push({ target: 'registry.json', prompt: answers.registryPrompt, status: 'pending' });
138
- }
139
- if (answers.servicesPrompt) {
140
- pendingPrompts.push({ target: 'services.md', prompt: answers.servicesPrompt, status: 'pending' });
141
- }
142
- if (answers.claudeMdPrompt) {
143
- pendingPrompts.push({ target: 'CLAUDE.md', prompt: answers.claudeMdPrompt, status: 'pending' });
240
+ writeFileSync(resolve(dir, '.mcp.json'), JSON.stringify(mcpJson, null, 2));
241
+ if (!existsSync(resolve(dir, 'workers/tracking.json')))
242
+ writeFileSync(resolve(dir, 'workers/tracking.json'), '[]');
243
+ if (!existsSync(resolve(dir, 'knowledge/index.json')))
244
+ writeFileSync(resolve(dir, 'knowledge/index.json'), '[]');
245
+ if (!existsSync(resolve(dir, 'state.md')))
246
+ writeFileSync(resolve(dir, 'state.md'), '');
247
+ // Pre-accept Claude Code trust
248
+ console.log(chalk.gray('Configuring Claude Code trust...'));
249
+ const claudeJsonPath = resolve(homeDir, '.claude/.claude.json');
250
+ try {
251
+ const claudeJson = existsSync(claudeJsonPath) ? JSON.parse(readFileSync(claudeJsonPath, 'utf-8')) : {};
252
+ if (!claudeJson.projects)
253
+ claudeJson.projects = {};
254
+ claudeJson.projects[dir] = { ...(claudeJson.projects[dir] || {}), allowedTools: [], hasTrustDialogAccepted: true };
255
+ writeFileSync(claudeJsonPath, JSON.stringify(claudeJson, null, 2));
256
+ console.log(chalk.green('✓ Claude Code trust pre-accepted'));
257
+ }
258
+ catch {
259
+ console.log(chalk.yellow('⚠ Could not pre-accept trust dialog.'));
260
+ }
261
+ // Handle setup prompts
262
+ const pendingPrompts = [];
263
+ if (answers.registryPrompt)
264
+ pendingPrompts.push({ target: 'registry.json', prompt: answers.registryPrompt, status: 'pending' });
265
+ if (answers.servicesPrompt)
266
+ pendingPrompts.push({ target: 'services.md', prompt: answers.servicesPrompt, status: 'pending' });
267
+ if (answers.claudeMdPrompt)
268
+ pendingPrompts.push({ target: 'CLAUDE.md', prompt: answers.claudeMdPrompt, status: 'pending' });
269
+ if (pendingPrompts.length > 0) {
270
+ writeFileSync(resolve(dir, 'setup-prompts.json'), JSON.stringify({ pending: pendingPrompts }, null, 2));
271
+ }
272
+ markStep(homeDir, checkpoint, 'files');
144
273
  }
145
- if (pendingPrompts.length > 0) {
146
- writeFileSync(resolve(dir, 'setup-prompts.json'), JSON.stringify({ pending: pendingPrompts }, null, 2));
274
+ else {
275
+ console.log(chalk.gray(' Config files already written, skipping'));
147
276
  }
148
277
  // --- CRITICAL: Copy scripts ---
149
278
  const requiredScripts = ['spawn-worker.sh', 'dissolve-worker.sh', 'list-workers.sh', 'check-worker.sh', 'healthcheck.sh', 'start-orchestrator.sh'];
150
279
  const scriptsSource = resolve(__dirname, '../../scripts');
151
- console.log(chalk.gray('Copying scripts...'));
152
- if (!existsSync(scriptsSource)) {
153
- console.error(chalk.red(`\nFATAL: Scripts directory not found at ${scriptsSource}`));
154
- console.error(chalk.red('The onkol package appears to be corrupted. Reinstall with: npm install -g onkol'));
155
- process.exit(1);
280
+ if (skip('scripts')) {
281
+ console.log(chalk.gray(' Scripts already installed, skipping'));
156
282
  }
157
- for (const script of requiredScripts) {
158
- const src = resolve(scriptsSource, script);
159
- const dst = resolve(dir, 'scripts', script);
160
- if (!existsSync(src)) {
161
- console.error(chalk.red(`\nFATAL: Required script not found: ${src}`));
283
+ else {
284
+ console.log(chalk.gray('Copying scripts...'));
285
+ if (!existsSync(scriptsSource)) {
286
+ console.error(chalk.red(`\nFATAL: Scripts directory not found at ${scriptsSource}`));
287
+ console.error(chalk.red('The onkol package appears to be corrupted. Reinstall with: npm install -g onkol'));
162
288
  process.exit(1);
163
289
  }
164
- copyFileSync(src, dst);
165
- execSync(`chmod +x "${dst}"`);
290
+ for (const script of requiredScripts) {
291
+ const src = resolve(scriptsSource, script);
292
+ const dst = resolve(dir, 'scripts', script);
293
+ if (!existsSync(src)) {
294
+ console.error(chalk.red(`\nFATAL: Required script not found: ${src}`));
295
+ process.exit(1);
296
+ }
297
+ copyFileSync(src, dst);
298
+ execSync(`chmod +x "${dst}"`);
299
+ }
300
+ console.log(chalk.green(`✓ ${requiredScripts.length} scripts installed`));
301
+ markStep(homeDir, checkpoint, 'scripts');
166
302
  }
167
- console.log(chalk.green(`✓ ${requiredScripts.length} scripts installed`));
168
303
  // --- CRITICAL: Copy plugin source ---
169
- // Look for .ts source files first (for bun), fall back to .js compiled files
170
304
  const pluginFiles = ['index', 'mcp-server', 'discord-client', 'message-batcher'];
171
305
  const pluginSourceDir = resolve(__dirname, '../plugin');
172
306
  const projectSrcDir = resolve(__dirname, '../../src/plugin');
173
- console.log(chalk.gray('Installing discord-filtered plugin...'));
174
- let pluginCopied = 0;
175
- for (const base of pluginFiles) {
176
- const dst = resolve(dir, 'plugins/discord-filtered', `${base}.ts`);
177
- // Try .ts from project src first, then .ts from dist, then .js from dist
178
- const candidates = [
179
- resolve(projectSrcDir, `${base}.ts`),
180
- resolve(pluginSourceDir, `${base}.ts`),
181
- resolve(pluginSourceDir, `${base}.js`),
182
- ];
183
- const found = candidates.find(c => existsSync(c));
184
- if (found) {
185
- copyFileSync(found, found.endsWith('.js') ? resolve(dir, 'plugins/discord-filtered', `${base}.js`) : dst);
186
- pluginCopied++;
187
- }
307
+ if (skip('plugin')) {
308
+ console.log(chalk.gray(' Plugin already installed, skipping'));
188
309
  }
189
- if (pluginCopied < pluginFiles.length) {
190
- console.error(chalk.red(`\nFATAL: Only ${pluginCopied}/${pluginFiles.length} plugin files found.`));
191
- console.error(chalk.red(`Searched in:\n ${projectSrcDir}\n ${pluginSourceDir}`));
192
- console.error(chalk.red('The onkol package appears to be corrupted. Reinstall with: npm install -g onkol'));
193
- process.exit(1);
194
- }
195
- // Create plugin package.json and install deps
196
- const pluginPkgJson = {
197
- name: 'discord-filtered',
198
- version: '0.1.0',
199
- private: true,
200
- dependencies: {
201
- '@modelcontextprotocol/sdk': '^1.0.0',
202
- 'discord.js': '^14.0.0',
203
- },
204
- };
205
- writeFileSync(resolve(dir, 'plugins/discord-filtered/package.json'), JSON.stringify(pluginPkgJson, null, 2));
206
- console.log(chalk.gray('Installing plugin dependencies (bun install)...'));
207
- try {
208
- execSync('bun install', { cwd: resolve(dir, 'plugins/discord-filtered'), stdio: 'pipe' });
209
- console.log(chalk.green(`✓ Plugin installed with ${pluginCopied} files + dependencies`));
210
- }
211
- catch {
212
- console.error(chalk.red('\nFATAL: Failed to install plugin dependencies.'));
213
- console.error(chalk.red('Is bun installed? Install with: curl -fsSL https://bun.sh/install | bash'));
214
- process.exit(1);
310
+ else {
311
+ console.log(chalk.gray('Installing discord-filtered plugin...'));
312
+ let pluginCopied = 0;
313
+ for (const base of pluginFiles) {
314
+ const dst = resolve(dir, 'plugins/discord-filtered', `${base}.ts`);
315
+ // Try .ts from project src first, then .ts from dist, then .js from dist
316
+ const candidates = [
317
+ resolve(projectSrcDir, `${base}.ts`),
318
+ resolve(pluginSourceDir, `${base}.ts`),
319
+ resolve(pluginSourceDir, `${base}.js`),
320
+ ];
321
+ const found = candidates.find(c => existsSync(c));
322
+ if (found) {
323
+ copyFileSync(found, found.endsWith('.js') ? resolve(dir, 'plugins/discord-filtered', `${base}.js`) : dst);
324
+ pluginCopied++;
325
+ }
326
+ }
327
+ if (pluginCopied < pluginFiles.length) {
328
+ console.error(chalk.red(`\nFATAL: Only ${pluginCopied}/${pluginFiles.length} plugin files found.`));
329
+ console.error(chalk.red(`Searched in:\n ${projectSrcDir}\n ${pluginSourceDir}`));
330
+ console.error(chalk.red('The onkol package appears to be corrupted. Reinstall with: npm install -g onkol'));
331
+ process.exit(1);
332
+ }
333
+ // Create plugin package.json and install deps
334
+ const pluginPkgJson = {
335
+ name: 'discord-filtered',
336
+ version: '0.1.0',
337
+ private: true,
338
+ dependencies: {
339
+ '@modelcontextprotocol/sdk': '^1.0.0',
340
+ 'discord.js': '^14.0.0',
341
+ },
342
+ };
343
+ writeFileSync(resolve(dir, 'plugins/discord-filtered/package.json'), JSON.stringify(pluginPkgJson, null, 2));
344
+ console.log(chalk.gray('Installing plugin dependencies (bun install)...'));
345
+ try {
346
+ execSync('bun install', { cwd: resolve(dir, 'plugins/discord-filtered'), stdio: 'pipe' });
347
+ console.log(chalk.green(`✓ Plugin installed with ${pluginCopied} files + dependencies`));
348
+ }
349
+ catch {
350
+ console.error(chalk.red('\nFATAL: Failed to install plugin dependencies.'));
351
+ console.error(chalk.red('Is bun installed? Install with: curl -fsSL https://bun.sh/install | bash'));
352
+ console.error(chalk.yellow('\nYour progress has been saved. Fix the issue and run `npx onkol setup` again to resume.'));
353
+ process.exit(1);
354
+ }
355
+ markStep(homeDir, checkpoint, 'plugin');
215
356
  }
216
357
  // Install systemd service
217
358
  const systemdUnit = generateSystemdUnit(answers.nodeName, user, dir);
@@ -282,22 +423,52 @@ program
282
423
  console.log(chalk.yellow(` You'll need to set up periodic health checks manually.`));
283
424
  }
284
425
  // Report pending setup prompts
285
- if (pendingPrompts.length > 0) {
286
- console.log(chalk.cyan('\nPending setup prompts saved. On first boot, the orchestrator will:'));
287
- for (const p of pendingPrompts) {
288
- console.log(chalk.cyan(` - Generate ${p.target} from your ${p.target === 'CLAUDE.md' ? 'description' : 'prompt'}`));
426
+ const setupPromptsPath = resolve(dir, 'setup-prompts.json');
427
+ if (existsSync(setupPromptsPath)) {
428
+ try {
429
+ const sp = JSON.parse(readFileSync(setupPromptsPath, 'utf-8'));
430
+ const pending = (sp.pending || []).filter((p) => p.status === 'pending');
431
+ if (pending.length > 0) {
432
+ console.log(chalk.cyan('\nPending setup prompts saved. On first boot, the orchestrator will:'));
433
+ for (const p of pending) {
434
+ console.log(chalk.cyan(` - Generate ${p.target} from your ${p.target === 'CLAUDE.md' ? 'description' : 'prompt'}`));
435
+ }
436
+ }
289
437
  }
438
+ catch { /* ignore */ }
290
439
  }
291
- // Start orchestrator
440
+ // Start orchestrator — try systemctl first (so service shows active), fall back to script
292
441
  console.log(chalk.gray('\nStarting orchestrator...'));
442
+ let started = false;
293
443
  try {
294
- execSync(`bash "${resolve(dir, 'scripts/start-orchestrator.sh')}"`, { stdio: 'pipe' });
295
- console.log(chalk.green(`✓ Orchestrator started in tmux session "onkol-${answers.nodeName}"`));
444
+ execSync(`sudo systemctl start onkol-${answers.nodeName}`, { stdio: 'pipe' });
445
+ // Wait for tmux session to appear
446
+ for (let i = 0; i < 10; i++) {
447
+ try {
448
+ execSync(`tmux has-session -t onkol-${answers.nodeName}`, { stdio: 'pipe' });
449
+ started = true;
450
+ break;
451
+ }
452
+ catch { /* not ready yet */ }
453
+ execSync('sleep 1', { stdio: 'pipe' });
454
+ }
455
+ if (started) {
456
+ console.log(chalk.green(`✓ Orchestrator started via systemd (tmux session "onkol-${answers.nodeName}")`));
457
+ }
296
458
  }
297
- catch (err) {
298
- console.log(chalk.yellow(`⚠ Could not start orchestrator automatically.`));
299
- console.log(chalk.yellow(` Start manually: ${dir}/scripts/start-orchestrator.sh`));
459
+ catch { /* systemctl start failed, try direct */ }
460
+ if (!started) {
461
+ try {
462
+ execSync(`bash "${resolve(dir, 'scripts/start-orchestrator.sh')}"`, { stdio: 'pipe' });
463
+ console.log(chalk.green(`✓ Orchestrator started in tmux session "onkol-${answers.nodeName}"`));
464
+ }
465
+ catch {
466
+ console.log(chalk.yellow(`⚠ Could not start orchestrator automatically.`));
467
+ console.log(chalk.yellow(` Start manually: ${dir}/scripts/start-orchestrator.sh`));
468
+ }
300
469
  }
470
+ // Setup complete — clear checkpoint
471
+ clearCheckpoint(homeDir);
301
472
  // Done
302
473
  console.log(chalk.green.bold(`\n✓ Onkol node "${answers.nodeName}" is live!`));
303
474
  console.log(chalk.green(`✓ Discord category "${answers.nodeName}" created with #orchestrator channel`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "onkol",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Decentralized on-call agent system powered by Claude Code",
5
5
  "type": "module",
6
6
  "bin": {