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.
- package/dist/cli/index.js +340 -169
- 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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
40
|
-
let
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
console.
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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(
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
146
|
-
|
|
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
|
-
|
|
152
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
console.error(chalk.red(
|
|
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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
174
|
-
|
|
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
|
-
|
|
190
|
-
console.
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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(`
|
|
295
|
-
|
|
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
|
|
298
|
-
|
|
299
|
-
|
|
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`));
|