kernelbot 1.0.1 → 1.0.3
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/README.md +20 -27
- package/bin/kernel.js +138 -150
- package/package.json +1 -1
- package/src/utils/config.js +89 -4
package/README.md
CHANGED
|
@@ -6,10 +6,10 @@ Send a message in Telegram, and KernelBot will read files, write code, run comma
|
|
|
6
6
|
|
|
7
7
|
## How It Works
|
|
8
8
|
|
|
9
|
-
```
|
|
9
|
+
```text
|
|
10
10
|
You (Telegram) → KernelBot → Claude Sonnet (Anthropic API)
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
↕
|
|
12
|
+
OS Tools (shell, files, directories)
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
KernelBot runs a **tool-use loop**: Claude decides which tools to call, KernelBot executes them on your OS, feeds results back, and Claude continues until the task is done. One message can trigger dozens of tool calls autonomously.
|
|
@@ -29,42 +29,35 @@ KernelBot runs a **tool-use loop**: Claude decides which tools to call, KernelBo
|
|
|
29
29
|
npm install -g kernelbot
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
This installs the `kernelbot` command globally.
|
|
33
|
-
|
|
34
32
|
## Quick Start
|
|
35
33
|
|
|
36
34
|
```bash
|
|
37
|
-
|
|
38
|
-
kernelbot init
|
|
39
|
-
|
|
40
|
-
# Verify everything works
|
|
41
|
-
kernelbot check
|
|
42
|
-
|
|
43
|
-
# Launch the bot
|
|
44
|
-
kernelbot start
|
|
35
|
+
kernelbot
|
|
45
36
|
```
|
|
46
37
|
|
|
47
|
-
|
|
38
|
+
That's it. On first run, KernelBot will:
|
|
48
39
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
| `kernelbot check` | Validate config and test API connections |
|
|
54
|
-
| `kernelbot init` | Interactive setup wizard |
|
|
40
|
+
1. Detect missing credentials and prompt for them
|
|
41
|
+
2. Save them to `~/.kernelbot/.env`
|
|
42
|
+
3. Verify API connections
|
|
43
|
+
4. Launch the Telegram bot
|
|
55
44
|
|
|
56
45
|
## Configuration
|
|
57
46
|
|
|
58
|
-
KernelBot
|
|
47
|
+
KernelBot auto-detects config from the current directory or `~/.kernelbot/`. Everything works with zero config — just provide your API keys when prompted.
|
|
59
48
|
|
|
60
|
-
###
|
|
49
|
+
### Environment Variables
|
|
61
50
|
|
|
62
|
-
|
|
51
|
+
Set these in `.env` or as system environment variables:
|
|
52
|
+
|
|
53
|
+
```text
|
|
63
54
|
ANTHROPIC_API_KEY=sk-ant-...
|
|
64
55
|
TELEGRAM_BOT_TOKEN=123456:ABC-DEF...
|
|
65
56
|
```
|
|
66
57
|
|
|
67
|
-
### `config.yaml`
|
|
58
|
+
### `config.yaml` (optional)
|
|
59
|
+
|
|
60
|
+
Drop a `config.yaml` in your working directory or `~/.kernelbot/` to customize behavior:
|
|
68
61
|
|
|
69
62
|
```yaml
|
|
70
63
|
bot:
|
|
@@ -102,10 +95,10 @@ conversation:
|
|
|
102
95
|
|
|
103
96
|
## Project Structure
|
|
104
97
|
|
|
105
|
-
```
|
|
98
|
+
```text
|
|
106
99
|
KernelBot/
|
|
107
100
|
├── bin/
|
|
108
|
-
│ └── kernel.js #
|
|
101
|
+
│ └── kernel.js # Entry point
|
|
109
102
|
├── src/
|
|
110
103
|
│ ├── agent.js # Sonnet tool-use loop
|
|
111
104
|
│ ├── bot.js # Telegram bot (polling, auth, message handling)
|
|
@@ -119,7 +112,7 @@ KernelBot/
|
|
|
119
112
|
│ │ ├── os.js # OS tool definitions + handlers
|
|
120
113
|
│ │ └── index.js # Tool registry + dispatcher
|
|
121
114
|
│ └── utils/
|
|
122
|
-
│ ├── config.js # Config loading (
|
|
115
|
+
│ ├── config.js # Config loading (auto-detect + prompt)
|
|
123
116
|
│ ├── display.js # CLI display (logo, spinners, banners)
|
|
124
117
|
│ └── logger.js # Winston logger
|
|
125
118
|
├── config.example.yaml
|
package/bin/kernel.js
CHANGED
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
process.removeAllListeners('warning');
|
|
5
5
|
process.on('warning', (w) => { if (w.name !== 'DeprecationWarning' || !w.message.includes('punycode')) console.warn(w); });
|
|
6
6
|
|
|
7
|
-
import { Command } from 'commander';
|
|
8
7
|
import { createInterface } from 'readline';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
8
|
+
import { readFileSync, existsSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { homedir } from 'os';
|
|
11
|
+
import chalk from 'chalk';
|
|
12
|
+
import { loadConfig, loadConfigInteractive } from '../src/utils/config.js';
|
|
11
13
|
import { createLogger, getLogger } from '../src/utils/logger.js';
|
|
12
14
|
import {
|
|
13
15
|
showLogo,
|
|
@@ -21,177 +23,163 @@ import { Agent } from '../src/agent.js';
|
|
|
21
23
|
import { startBot } from '../src/bot.js';
|
|
22
24
|
import Anthropic from '@anthropic-ai/sdk';
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
.
|
|
28
|
-
.
|
|
29
|
-
.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
showError('Startup checks failed. Fix the issues above and try again.');
|
|
66
|
-
process.exit(1);
|
|
26
|
+
function showMenu() {
|
|
27
|
+
console.log('');
|
|
28
|
+
console.log(chalk.bold(' What would you like to do?\n'));
|
|
29
|
+
console.log(` ${chalk.cyan('1.')} Start bot`);
|
|
30
|
+
console.log(` ${chalk.cyan('2.')} Check connections`);
|
|
31
|
+
console.log(` ${chalk.cyan('3.')} View logs`);
|
|
32
|
+
console.log(` ${chalk.cyan('4.')} View audit logs`);
|
|
33
|
+
console.log(` ${chalk.cyan('5.')} Exit`);
|
|
34
|
+
console.log('');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function ask(rl, question) {
|
|
38
|
+
return new Promise((res) => rl.question(question, res));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function viewLog(filename) {
|
|
42
|
+
const paths = [
|
|
43
|
+
join(process.cwd(), filename),
|
|
44
|
+
join(homedir(), '.kernelbot', filename),
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
for (const p of paths) {
|
|
48
|
+
if (existsSync(p)) {
|
|
49
|
+
const content = readFileSync(p, 'utf-8');
|
|
50
|
+
const lines = content.split('\n').filter(Boolean);
|
|
51
|
+
const recent = lines.slice(-30);
|
|
52
|
+
console.log(chalk.dim(`\n Showing last ${recent.length} entries from ${p}\n`));
|
|
53
|
+
for (const line of recent) {
|
|
54
|
+
try {
|
|
55
|
+
const entry = JSON.parse(line);
|
|
56
|
+
const time = entry.timestamp || '';
|
|
57
|
+
const level = entry.level || '';
|
|
58
|
+
const msg = entry.message || '';
|
|
59
|
+
const color = level === 'error' ? chalk.red : level === 'warn' ? chalk.yellow : chalk.dim;
|
|
60
|
+
console.log(` ${chalk.dim(time)} ${color(level)} ${msg}`);
|
|
61
|
+
} catch {
|
|
62
|
+
console.log(` ${line}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
console.log('');
|
|
66
|
+
return;
|
|
67
67
|
}
|
|
68
|
+
}
|
|
69
|
+
console.log(chalk.dim(`\n No ${filename} found yet.\n`));
|
|
70
|
+
}
|
|
68
71
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
startBot(config, agent);
|
|
73
|
-
showStartupComplete();
|
|
72
|
+
async function runCheck(config) {
|
|
73
|
+
await showStartupCheck('ANTHROPIC_API_KEY', async () => {
|
|
74
|
+
if (!config.anthropic.api_key) throw new Error('Not set');
|
|
74
75
|
});
|
|
75
76
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
.description('Run a one-off prompt through the agent (no Telegram)')
|
|
80
|
-
.argument('<prompt>', 'The prompt to send')
|
|
81
|
-
.action(async (prompt) => {
|
|
82
|
-
const config = loadConfig();
|
|
83
|
-
createLogger(config);
|
|
84
|
-
createAuditLogger();
|
|
85
|
-
|
|
86
|
-
if (!config.anthropic.api_key) {
|
|
87
|
-
showError('ANTHROPIC_API_KEY not set. Run `kernelbot init` first.');
|
|
88
|
-
process.exit(1);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const conversationManager = new ConversationManager(config);
|
|
92
|
-
const agent = new Agent({ config, conversationManager });
|
|
77
|
+
await showStartupCheck('TELEGRAM_BOT_TOKEN', async () => {
|
|
78
|
+
if (!config.telegram.bot_token) throw new Error('Not set');
|
|
79
|
+
});
|
|
93
80
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
81
|
+
await showStartupCheck('Anthropic API connection', async () => {
|
|
82
|
+
const client = new Anthropic({ apiKey: config.anthropic.api_key });
|
|
83
|
+
await client.messages.create({
|
|
84
|
+
model: config.anthropic.model,
|
|
85
|
+
max_tokens: 16,
|
|
86
|
+
messages: [{ role: 'user', content: 'ping' }],
|
|
97
87
|
});
|
|
98
|
-
|
|
99
|
-
console.log('\n' + reply);
|
|
100
88
|
});
|
|
101
89
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const config = loadConfig();
|
|
110
|
-
createLogger(config);
|
|
90
|
+
await showStartupCheck('Telegram Bot API', async () => {
|
|
91
|
+
const res = await fetch(
|
|
92
|
+
`https://api.telegram.org/bot${config.telegram.bot_token}/getMe`,
|
|
93
|
+
);
|
|
94
|
+
const data = await res.json();
|
|
95
|
+
if (!data.ok) throw new Error(data.description || 'Invalid token');
|
|
96
|
+
});
|
|
111
97
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
});
|
|
98
|
+
console.log(chalk.green('\n All checks passed.\n'));
|
|
99
|
+
}
|
|
115
100
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
101
|
+
async function startBotFlow(config) {
|
|
102
|
+
createAuditLogger();
|
|
103
|
+
const logger = getLogger();
|
|
119
104
|
|
|
120
|
-
|
|
121
|
-
if (!config.telegram.bot_token) throw new Error('Not set');
|
|
122
|
-
});
|
|
105
|
+
const checks = [];
|
|
123
106
|
|
|
124
|
-
|
|
107
|
+
checks.push(
|
|
108
|
+
await showStartupCheck('Anthropic API', async () => {
|
|
125
109
|
const client = new Anthropic({ apiKey: config.anthropic.api_key });
|
|
126
110
|
await client.messages.create({
|
|
127
111
|
model: config.anthropic.model,
|
|
128
112
|
max_tokens: 16,
|
|
129
113
|
messages: [{ role: 'user', content: 'ping' }],
|
|
130
114
|
});
|
|
131
|
-
})
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
132
117
|
|
|
118
|
+
checks.push(
|
|
133
119
|
await showStartupCheck('Telegram Bot API', async () => {
|
|
134
120
|
const res = await fetch(
|
|
135
121
|
`https://api.telegram.org/bot${config.telegram.bot_token}/getMe`,
|
|
136
122
|
);
|
|
137
123
|
const data = await res.json();
|
|
138
124
|
if (!data.ok) throw new Error(data.description || 'Invalid token');
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
writeFileSync('config.yaml', configContent);
|
|
125
|
+
}),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
if (checks.some((c) => !c)) {
|
|
129
|
+
showError('Startup failed. Fix the issues above and try again.');
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const conversationManager = new ConversationManager(config);
|
|
134
|
+
const agent = new Agent({ config, conversationManager });
|
|
135
|
+
|
|
136
|
+
startBot(config, agent);
|
|
137
|
+
showStartupComplete();
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function main() {
|
|
142
|
+
showLogo();
|
|
143
|
+
|
|
144
|
+
const config = await loadConfigInteractive();
|
|
145
|
+
createLogger(config);
|
|
146
|
+
|
|
147
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
148
|
+
|
|
149
|
+
let running = true;
|
|
150
|
+
while (running) {
|
|
151
|
+
showMenu();
|
|
152
|
+
const choice = await ask(rl, chalk.cyan(' > '));
|
|
153
|
+
|
|
154
|
+
switch (choice.trim()) {
|
|
155
|
+
case '1': {
|
|
156
|
+
rl.close();
|
|
157
|
+
const started = await startBotFlow(config);
|
|
158
|
+
if (!started) process.exit(1);
|
|
159
|
+
return; // bot is running, don't show menu again
|
|
160
|
+
}
|
|
161
|
+
case '2':
|
|
162
|
+
await runCheck(config);
|
|
163
|
+
break;
|
|
164
|
+
case '3':
|
|
165
|
+
viewLog('kernel.log');
|
|
166
|
+
break;
|
|
167
|
+
case '4':
|
|
168
|
+
viewLog('kernel-audit.log');
|
|
169
|
+
break;
|
|
170
|
+
case '5':
|
|
171
|
+
running = false;
|
|
172
|
+
break;
|
|
173
|
+
default:
|
|
174
|
+
console.log(chalk.dim(' Invalid choice.\n'));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
192
177
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
178
|
+
rl.close();
|
|
179
|
+
console.log(chalk.dim(' Goodbye.\n'));
|
|
180
|
+
}
|
|
196
181
|
|
|
197
|
-
|
|
182
|
+
main().catch((err) => {
|
|
183
|
+
showError(err.message);
|
|
184
|
+
process.exit(1);
|
|
185
|
+
});
|
package/package.json
CHANGED
package/src/utils/config.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { readFileSync, existsSync } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
|
+
import { createInterface } from 'readline';
|
|
4
5
|
import yaml from 'js-yaml';
|
|
5
6
|
import dotenv from 'dotenv';
|
|
7
|
+
import chalk from 'chalk';
|
|
6
8
|
|
|
7
9
|
const DEFAULTS = {
|
|
8
10
|
bot: {
|
|
@@ -54,18 +56,96 @@ function deepMerge(target, source) {
|
|
|
54
56
|
return result;
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
function getConfigDir() {
|
|
60
|
+
return join(homedir(), '.kernelbot');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getEnvPath() {
|
|
64
|
+
const cwdEnv = join(process.cwd(), '.env');
|
|
65
|
+
if (existsSync(cwdEnv)) return cwdEnv;
|
|
66
|
+
return join(getConfigDir(), '.env');
|
|
67
|
+
}
|
|
68
|
+
|
|
57
69
|
function findConfigFile() {
|
|
58
70
|
const cwdPath = join(process.cwd(), 'config.yaml');
|
|
59
71
|
if (existsSync(cwdPath)) return cwdPath;
|
|
60
72
|
|
|
61
|
-
const homePath = join(
|
|
73
|
+
const homePath = join(getConfigDir(), 'config.yaml');
|
|
62
74
|
if (existsSync(homePath)) return homePath;
|
|
63
75
|
|
|
64
76
|
return null;
|
|
65
77
|
}
|
|
66
78
|
|
|
79
|
+
function ask(rl, question) {
|
|
80
|
+
return new Promise((res) => rl.question(question, res));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function promptForMissing(config) {
|
|
84
|
+
const missing = [];
|
|
85
|
+
if (!config.anthropic.api_key) missing.push('ANTHROPIC_API_KEY');
|
|
86
|
+
if (!config.telegram.bot_token) missing.push('TELEGRAM_BOT_TOKEN');
|
|
87
|
+
|
|
88
|
+
if (missing.length === 0) return config;
|
|
89
|
+
|
|
90
|
+
console.log(chalk.yellow('\n Missing credentials detected. Let\'s set them up.\n'));
|
|
91
|
+
|
|
92
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
93
|
+
const mutableConfig = JSON.parse(JSON.stringify(config));
|
|
94
|
+
const envLines = [];
|
|
95
|
+
|
|
96
|
+
// Read existing .env if any
|
|
97
|
+
const envPath = getEnvPath();
|
|
98
|
+
let existingEnv = '';
|
|
99
|
+
if (existsSync(envPath)) {
|
|
100
|
+
existingEnv = readFileSync(envPath, 'utf-8');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!mutableConfig.anthropic.api_key) {
|
|
104
|
+
const key = await ask(rl, chalk.cyan(' Anthropic API key: '));
|
|
105
|
+
mutableConfig.anthropic.api_key = key.trim();
|
|
106
|
+
envLines.push(`ANTHROPIC_API_KEY=${key.trim()}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!mutableConfig.telegram.bot_token) {
|
|
110
|
+
const token = await ask(rl, chalk.cyan(' Telegram Bot Token: '));
|
|
111
|
+
mutableConfig.telegram.bot_token = token.trim();
|
|
112
|
+
envLines.push(`TELEGRAM_BOT_TOKEN=${token.trim()}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
rl.close();
|
|
116
|
+
|
|
117
|
+
// Save to ~/.kernelbot/.env so it persists globally
|
|
118
|
+
if (envLines.length > 0) {
|
|
119
|
+
const configDir = getConfigDir();
|
|
120
|
+
mkdirSync(configDir, { recursive: true });
|
|
121
|
+
const savePath = join(configDir, '.env');
|
|
122
|
+
|
|
123
|
+
// Merge with existing content
|
|
124
|
+
let content = existingEnv ? existingEnv.trimEnd() + '\n' : '';
|
|
125
|
+
for (const line of envLines) {
|
|
126
|
+
const key = line.split('=')[0];
|
|
127
|
+
// Replace if exists, append if not
|
|
128
|
+
const regex = new RegExp(`^${key}=.*$`, 'm');
|
|
129
|
+
if (regex.test(content)) {
|
|
130
|
+
content = content.replace(regex, line);
|
|
131
|
+
} else {
|
|
132
|
+
content += line + '\n';
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
writeFileSync(savePath, content);
|
|
136
|
+
console.log(chalk.dim(`\n Saved to ${savePath}\n`));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return mutableConfig;
|
|
140
|
+
}
|
|
141
|
+
|
|
67
142
|
export function loadConfig() {
|
|
143
|
+
// Load .env from CWD first, then from ~/.kernelbot/
|
|
68
144
|
dotenv.config();
|
|
145
|
+
const globalEnv = join(getConfigDir(), '.env');
|
|
146
|
+
if (existsSync(globalEnv)) {
|
|
147
|
+
dotenv.config({ path: globalEnv });
|
|
148
|
+
}
|
|
69
149
|
|
|
70
150
|
let fileConfig = {};
|
|
71
151
|
const configPath = findConfigFile();
|
|
@@ -84,5 +164,10 @@ export function loadConfig() {
|
|
|
84
164
|
config.telegram.bot_token = process.env.TELEGRAM_BOT_TOKEN;
|
|
85
165
|
}
|
|
86
166
|
|
|
87
|
-
return
|
|
167
|
+
return config;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function loadConfigInteractive() {
|
|
171
|
+
const config = loadConfig();
|
|
172
|
+
return await promptForMissing(config);
|
|
88
173
|
}
|