coralos-dev 1.1.0-SNAPSHOT-11

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 ADDED
@@ -0,0 +1,112 @@
1
+ <img width="3320" height="1788" alt="image" src="https://github.com/user-attachments/assets/193afcea-8065-4549-9220-b5402e42a6dd" />
2
+
3
+
4
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/Coral-Protocol/coral-server)
5
+
6
+ > [!WARNING]
7
+ >
8
+ > This readme and connected documentation are a work in progress.
9
+
10
+
11
+ <br/>
12
+ <div align="center">
13
+
14
+ # Quickstart
15
+ **See [our docs](https://docs.coralos.ai/welcome)** for a quickstart and introduction to Coral!
16
+
17
+ **[How to Run](#how-to-run)** ┃ **[Configuration](#configuration)** ┃ **[Contributing](#contribution-guidelines)**
18
+
19
+ </div>
20
+ <br/>
21
+
22
+ ## How to Run
23
+
24
+ ### Using npx
25
+
26
+ If you have Node.js installed, you can run the server directly using `npx`:
27
+ ```bash
28
+ npx coral-server@1.1.0 start --auth.keys=dev
29
+ ```
30
+
31
+ You can also force the server to run from source code (cloning/updating the repository in `~/.coral/source`) by using the `--from-source` flag as the first argument:
32
+ ```bash
33
+ npx coral-server --from-source start --auth.keys=dev
34
+ ```
35
+
36
+ ### Using Gradle
37
+
38
+ Clone this repository, and in that folder run:
39
+ ```bash
40
+ ./gradlew run
41
+ ```
42
+
43
+ ### Using Docker
44
+
45
+ A coral-server docker image is available on ghcr.io:
46
+
47
+ ```bash
48
+ docker run \
49
+ -p 5555:5555 \
50
+ -e CONFIG_FILE_PATH=/config/config.toml
51
+ -v /path/to/your/config.toml:/config/config.toml
52
+ -v /var/run/docker.sock:/var/run/docker.sock # docker in docker
53
+ ghcr.io/coral-protocol/coral-server
54
+ ```
55
+
56
+ > [!WARNING]
57
+ > The Coral Server docker image is *very* minimal, which means the executable runtime will **not** work. All agents must use the Docker runtime, which means you **must** give your server container access to your host's docker socket.
58
+ >
59
+ > See [here](https://docs.coralprotocol.org/setup/coral-server-applications#docker-recommended) for more information on giving your docker container access to Docker.
60
+
61
+
62
+ ## Configuration
63
+
64
+ Authentication keys are required to be configured in a `config.toml` file, e.g.:
65
+
66
+ ```toml
67
+ [auth]
68
+ keys = ["my-auth-key"]
69
+ ```
70
+
71
+ The server must be given an environment variable `CONFIG_FILE_PATH` pointing to the location of the config file.
72
+ Alternatively, you can provide configuration via command-line arguments. For example, to set the authentication keys to `["dev"]`, run the server with:
73
+
74
+ ```bash
75
+ ./gradlew run --args="--auth.keys=dev"
76
+ ```
77
+
78
+ ### Command-line Property Mapping
79
+
80
+ The server uses [Hoplite](https://github.com/sksamuel/hoplite) for configuration. When using command-line arguments:
81
+
82
+ - **Nested Properties:** Use dot notation to reach nested configuration blocks. For example, the `[auth]` block's `keys` property becomes `--auth.keys`.
83
+ - **Collections (Lists/Sets):** Provide multiple values separated by commas. For example, `--auth.keys=key1,key2` will be parsed into a set containing both keys.
84
+ - **Data Types:** Simple types (strings, booleans, numbers) are automatically converted. Booleans can be set as `--some.flag=true`.
85
+
86
+ Examples:
87
+
88
+ | TOML Config | Command-line Argument |
89
+ | :--- |:---------------------------------------|
90
+ | `[auth]`<br/>`keys = ["a", "b"]` | `--auth.keys=a,b` |
91
+ | `[network]`<br/>`bindPort = 8080` | `--network.bind-port=8080` |
92
+ | `[registry]`<br/>`includeDebugAgents = true` | `--registry.include-debug-agents=true` |
93
+
94
+ (camel-case forms for still work for command-line args)
95
+
96
+ ## Contribution Guidelines
97
+
98
+ We welcome contributions! Email us at [hello@coralos.ai](mailto:hello@coralos.au) or join our Discord [here](https://discord.gg/rMQc2uWXhj) to connect with the developer team. Feel free to open issues or submit pull requests.
99
+
100
+ Thanks for checking out the project, we hope you like it!
101
+
102
+ ### Development
103
+ IntelliJ IDEA is recommended for development. The project uses Gradle as the build system.
104
+
105
+ To clone and import the project:
106
+ Go to File > New > Project from Version Control > Git.
107
+ enter `git@github.com:Coral-Protocol/coral-server.git`
108
+ Click Clone.
109
+
110
+ ### Running from IntelliJ IDEA
111
+ You can click the play button next to the main method in the `Main.kt` file to run the server directly from IntelliJ IDEA.
112
+
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const { parseCliArgs, printUsage } = require('./lib/cli-parser');
5
+ const { runServer, runFromSource } = require('./lib/runner');
6
+ const { runSetupWizard } = require('./lib/wizard');
7
+ const {
8
+ ensureConfigProfileDir,
9
+ getConfigProfilePath,
10
+ generateDefaultConfig
11
+ } = require('./lib/config-manager');
12
+
13
+ async function main() {
14
+ const parsed = parseCliArgs(process.argv);
15
+ const { command, subcommand, subcommandArgs, cliFlags, serverArgs } = parsed;
16
+
17
+ // Legacy support: no subcommand, print usage
18
+ if (!command) {
19
+ printUsage();
20
+ process.exit(0);
21
+ }
22
+
23
+ // Legacy --from-source=true as first arg (backward compat)
24
+ if (command === '--from-source=true' || (command && command.startsWith('--'))) {
25
+ // Legacy mode: treat all args as server args
26
+ let args = process.argv.slice(2);
27
+ let forceFromSource = false;
28
+ if (args[0] === '--from-source=true') {
29
+ forceFromSource = true;
30
+ args = args.slice(1);
31
+ }
32
+ if (forceFromSource) {
33
+ await runFromSource(args);
34
+ } else {
35
+ await runServer(args, null, false);
36
+ }
37
+ return;
38
+ }
39
+
40
+ if (command !== 'server') {
41
+ console.error(`Unknown command: ${command}`);
42
+ printUsage();
43
+ process.exit(1);
44
+ }
45
+
46
+ const configProfile = cliFlags['config-profile'] || null;
47
+ const forceFromSource = cliFlags['from-source'] === true || cliFlags['from-source'] === 'true';
48
+
49
+ switch (subcommand) {
50
+ case 'start':
51
+ await runServer(serverArgs, configProfile, forceFromSource);
52
+ break;
53
+
54
+ case 'configure': {
55
+ const profileName = subcommandArgs[0] || configProfile;
56
+ if (!profileName) {
57
+ console.error('Error: Please specify a profile name.');
58
+ console.log('Usage: npx coralos-dev server configure <profile-name>');
59
+ process.exit(1);
60
+ }
61
+ ensureConfigProfileDir(profileName);
62
+ const profilePath = getConfigProfilePath(profileName);
63
+ if (!fs.existsSync(profilePath)) {
64
+ fs.writeFileSync(profilePath, generateDefaultConfig());
65
+ console.log(`File created at ${profilePath}`);
66
+ }
67
+ await runSetupWizard(profileName, { hasAuthKeysArg: false, isStartCommand: false });
68
+ break;
69
+ }
70
+
71
+ default:
72
+ if (!subcommand) {
73
+ console.error('Error: Please specify a subcommand (start, configure).');
74
+ } else {
75
+ console.error(`Unknown subcommand: ${subcommand}`);
76
+ }
77
+ printUsage();
78
+ process.exit(1);
79
+ }
80
+ }
81
+
82
+ main().catch(err => {
83
+ console.error(err);
84
+ process.exit(1);
85
+ });
@@ -0,0 +1,70 @@
1
+ const { pkg } = require('./constants');
2
+
3
+ function parseCliArgs(argv) {
4
+ const rawArgs = argv.slice(2);
5
+
6
+ // Split on -- separator: args before are CLI args, args after are server args
7
+ const doubleDashIndex = rawArgs.indexOf('--');
8
+ let cliArgs, serverArgs;
9
+
10
+ if (doubleDashIndex >= 0) {
11
+ cliArgs = rawArgs.slice(0, doubleDashIndex);
12
+ serverArgs = rawArgs.slice(doubleDashIndex + 1);
13
+ } else {
14
+ cliArgs = rawArgs;
15
+ serverArgs = [];
16
+ }
17
+
18
+ // Extract subcommand (e.g., "server start", "server configure")
19
+ let command = null;
20
+ let subcommand = null;
21
+ let subcommandArgs = [];
22
+ let cliFlags = {};
23
+
24
+ const positional = [];
25
+ for (const arg of cliArgs) {
26
+ if (arg.startsWith('--')) {
27
+ const eqIndex = arg.indexOf('=');
28
+ if (eqIndex >= 0) {
29
+ const key = arg.substring(2, eqIndex);
30
+ const value = arg.substring(eqIndex + 1);
31
+ cliFlags[key] = value;
32
+ } else {
33
+ cliFlags[arg.substring(2)] = true;
34
+ }
35
+ } else {
36
+ positional.push(arg);
37
+ }
38
+ }
39
+
40
+ command = positional[0] || null;
41
+ subcommand = positional[1] || null;
42
+ subcommandArgs = positional.slice(2);
43
+
44
+ return { command, subcommand, subcommandArgs, cliFlags, serverArgs };
45
+ }
46
+
47
+ function printUsage() {
48
+ const version = pkg.version;
49
+ console.log(`coralos-dev v${version} - Coral Server CLI\n`);
50
+ console.log('Usage:');
51
+ console.log(' npx coralos-dev server start [--config-profile=<name>] [--from-source] [-- <server-args...>]');
52
+ console.log(' npx coralos-dev server configure <profile-name>');
53
+ console.log('');
54
+ console.log('Commands:');
55
+ console.log(' server start Start the Coral server');
56
+ console.log(' server configure Run the setup wizard for a config profile');
57
+ console.log('');
58
+ console.log('Options:');
59
+ console.log(' --config-profile=<name> Use a named config profile from ~/.coral/config-profiles/');
60
+ console.log(' --from-source Build and run from source code');
61
+ console.log('');
62
+ console.log('Examples:');
63
+ console.log(` npx coralos-dev@${version} server start --config-profile=dev -- --auth.keys=dev`);
64
+ console.log(` npx coralos-dev@${version} server configure dev`);
65
+ }
66
+
67
+ module.exports = {
68
+ parseCliArgs,
69
+ printUsage,
70
+ };
@@ -0,0 +1,81 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const { CONFIG_PROFILES_DIR, pkg } = require('./constants');
4
+
5
+ function getProfileDir(profileName) {
6
+ return path.join(CONFIG_PROFILES_DIR, profileName);
7
+ }
8
+
9
+ function getConfigProfilePath(profileName) {
10
+ return path.join(getProfileDir(profileName), `${profileName}-coral-server-config.toml`);
11
+ }
12
+
13
+ function ensureConfigProfileDir(profileName) {
14
+ const profileDir = getProfileDir(profileName);
15
+ if (!fs.existsSync(profileDir)) {
16
+ fs.mkdirSync(profileDir, { recursive: true });
17
+ }
18
+ }
19
+
20
+ function configProfileExists(profileName) {
21
+ return fs.existsSync(getConfigProfilePath(profileName));
22
+ }
23
+
24
+ function generateDefaultConfig() {
25
+ const templatePath = path.join(__dirname, '..', '..', 'default-dev-config.toml');
26
+ if (!fs.existsSync(templatePath)) {
27
+ // Fallback if template missing
28
+ return `# Coral Server Configuration Profile\n# Template not found at ${templatePath}\n`;
29
+ }
30
+
31
+ let content = fs.readFileSync(templatePath, 'utf8');
32
+
33
+ // Remove the "reference" message (line 10 in original)
34
+ content = content.replace(/^# This file is a reference.*?\n/m, '');
35
+ // Also remove the next few lines that are part of that note if they exist
36
+ content = content.replace(/^# NOTE: Agent-specific configuration lives in each agent's own.*?\n/m, '');
37
+ content = content.replace(/^# coral-agent.toml\..*?\n/m, '');
38
+
39
+ const header = `# Coral Server Configuration Profile
40
+ # Generated by coralos-dev v${pkg.version}
41
+ # Documentation: https://docs.coralos.ai/
42
+
43
+ `;
44
+ return header + content;
45
+ }
46
+
47
+ function buildConfigFromWizardResults(providers, authKey) {
48
+ let config = generateDefaultConfig();
49
+
50
+ // Add auth keys if provided
51
+ if (authKey) {
52
+ const authRegex = /# \[auth\]\n# keys = \["some-secure-password"\]/m;
53
+ if (authRegex.test(config)) {
54
+ config = config.replace(authRegex, `[auth]\nkeys = ["${authKey}"]`);
55
+ } else {
56
+ config += `\n[auth]\nkeys = ["${authKey}"]\n`;
57
+ }
58
+ }
59
+
60
+ // Add providers
61
+ config += '\n# --- Configured LLM Providers ---\n';
62
+ config += '[llm-proxy.providers]\n';
63
+ for (const p of providers) {
64
+ if (p.working) {
65
+ config += `[llm-proxy.providers.${p.id}]\napiKey = "${p.apiKey}"\n\n`;
66
+ } else if (p.apiKey) {
67
+ config += `# [llm-proxy.providers.${p.id}]\n# apiKey = "${p.apiKey}"\n\n`;
68
+ }
69
+ }
70
+
71
+ return config;
72
+ }
73
+
74
+ module.exports = {
75
+ getProfileDir,
76
+ getConfigProfilePath,
77
+ ensureConfigProfileDir,
78
+ configProfileExists,
79
+ generateDefaultConfig,
80
+ buildConfigFromWizardResults,
81
+ };
@@ -0,0 +1,25 @@
1
+ const path = require('path');
2
+ const os = require('os');
3
+ const fs = require('fs');
4
+
5
+ const pkgPath = path.join(__dirname, '..', '..', 'package.json');
6
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
7
+
8
+ const CORAL_HOME = path.join(os.homedir(), '.coral');
9
+ const CONFIG_PROFILES_DIR = path.join(CORAL_HOME, 'config-profiles');
10
+
11
+ const LLM_PROVIDERS = [
12
+ { id: 'openai', name: 'OpenAI', envVar: 'OPENAI_API_KEY', prefix: 'sk-' },
13
+ { id: 'anthropic', name: 'Anthropic', envVar: 'ANTHROPIC_API_KEY', prefix: 'sk-ant-' },
14
+ { id: 'openrouter', name: 'OpenRouter', envVar: 'OPENROUTER_API_KEY', prefix: 'sk-or-' },
15
+ ];
16
+
17
+ const IS_WINDOWS = process.platform === 'win32';
18
+
19
+ module.exports = {
20
+ pkg,
21
+ CORAL_HOME,
22
+ CONFIG_PROFILES_DIR,
23
+ LLM_PROVIDERS,
24
+ IS_WINDOWS,
25
+ };
@@ -0,0 +1,173 @@
1
+ const { spawn, spawnSync } = require('child_process');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const { pkg, IS_WINDOWS } = require('./constants');
6
+ const { getJavaVersion, askQuestion } = require('./utils');
7
+ const {
8
+ getConfigProfilePath,
9
+ configProfileExists,
10
+ ensureConfigProfileDir,
11
+ generateDefaultConfig
12
+ } = require('./config-manager');
13
+ const { handleFirstRun } = require('./wizard');
14
+
15
+ async function runFromSource(args) {
16
+ const sourceDir = path.join(os.homedir(), '.coral', 'source');
17
+ const repoUrl = pkg.repository && pkg.repository.url ? pkg.repository.url.replace(/^git\+/, '') : null;
18
+ const version = pkg.version;
19
+ const gitHead = pkg.gitHead;
20
+
21
+ if (!fs.existsSync(sourceDir)) {
22
+ fs.mkdirSync(sourceDir, { recursive: true });
23
+ }
24
+
25
+ if (repoUrl) {
26
+ try {
27
+ const hasGit = spawnSync('git', ['--version']).status === 0;
28
+ if (hasGit) {
29
+ const isGit = spawnSync('git', ['rev-parse', '--is-inside-work-tree'], { cwd: sourceDir }).status === 0;
30
+ const isSnapshot = version.includes('SNAPSHOT');
31
+ const target = !isSnapshot ? (gitHead || `v${version}`) : 'master';
32
+
33
+ if (!isGit) {
34
+ console.log(`Initializing git repository in ${sourceDir} and fetching ${target}...`);
35
+ spawnSync('git', ['init'], { cwd: sourceDir, stdio: 'inherit' });
36
+ spawnSync('git', ['remote', 'add', 'origin', repoUrl], { cwd: sourceDir, stdio: 'inherit' });
37
+ }
38
+
39
+ console.log(`Fetching ${target} in ${sourceDir}...`);
40
+ const fetch = spawnSync('git', ['fetch', 'origin', target, '--depth', '1'], { cwd: sourceDir, stdio: 'inherit' });
41
+ if (fetch.status === 0) {
42
+ spawnSync('git', ['checkout', 'FETCH_HEAD', '--force'], { cwd: sourceDir, stdio: 'inherit' });
43
+ } else if (isSnapshot) {
44
+ const fetchMain = spawnSync('git', ['fetch', 'origin', 'main', '--depth', '1'], { cwd: sourceDir, stdio: 'inherit' });
45
+ if (fetchMain.status === 0) {
46
+ spawnSync('git', ['checkout', 'FETCH_HEAD', '--force'], { cwd: sourceDir, stdio: 'inherit' });
47
+ }
48
+ }
49
+ }
50
+ } catch (e) {
51
+ console.warn('Git operation failed, continuing with existing source in ' + sourceDir + ':', e.message);
52
+ }
53
+ }
54
+
55
+ const gradlewPath = path.join(sourceDir, IS_WINDOWS ? 'gradlew.bat' : 'gradlew');
56
+ const gradlePropsPath = path.join(sourceDir, 'gradle.properties');
57
+
58
+ if (fs.existsSync(gradlePropsPath)) {
59
+ let props = fs.readFileSync(gradlePropsPath, 'utf8');
60
+ let modified = false;
61
+ if (!props.includes('org.gradle.java.installations.auto-detect=true')) {
62
+ props += '\norg.gradle.java.installations.auto-detect=true';
63
+ modified = true;
64
+ }
65
+ if (!props.includes('org.gradle.java.installations.auto-download=true')) {
66
+ props += '\norg.gradle.java.installations.auto-download=true';
67
+ modified = true;
68
+ }
69
+ if (modified) {
70
+ fs.writeFileSync(gradlePropsPath, props);
71
+ console.log('Added portable Gradle properties to ' + gradlePropsPath);
72
+ }
73
+ }
74
+
75
+ if (!fs.existsSync(gradlewPath)) {
76
+ console.error('Error: gradlew not found in ' + sourceDir);
77
+ process.exit(1);
78
+ }
79
+
80
+ console.log('Running from source code using gradlew run...');
81
+ const child = spawn(gradlewPath, ['run', '--args=' + args.join(' ')], {
82
+ cwd: sourceDir,
83
+ stdio: 'inherit',
84
+ shell: IS_WINDOWS
85
+ });
86
+
87
+ child.on('exit', (code) => {
88
+ process.exit(code !== null ? code : 1);
89
+ });
90
+
91
+ const killChild = () => {
92
+ if (child.pid) child.kill('SIGINT');
93
+ };
94
+
95
+ process.on('SIGINT', killChild);
96
+ process.on('SIGTERM', killChild);
97
+ }
98
+
99
+ async function runServer(serverArgs, configProfile, forceFromSource) {
100
+ // Check if --auth.keys is in serverArgs
101
+ const hasAuthKeysArg = serverArgs.some(arg => arg.startsWith('--auth.keys=') || arg.includes('.auth.keys='));
102
+
103
+ // If config profile specified, add CONFIG_FILE_PATH
104
+ if (configProfile) {
105
+ const profilePath = getConfigProfilePath(configProfile);
106
+
107
+ if (!configProfileExists(configProfile)) {
108
+ if (process.stdin.isTTY) {
109
+ await handleFirstRun(configProfile, { hasAuthKeysArg, isStartCommand: true });
110
+ } else {
111
+ ensureConfigProfileDir(configProfile);
112
+ fs.writeFileSync(profilePath, generateDefaultConfig());
113
+ console.log(`File created at ${profilePath}`);
114
+ }
115
+ }
116
+
117
+ // Set the config file path environment variable for the server
118
+ process.env.CONFIG_FILE_PATH = profilePath;
119
+ }
120
+
121
+ if (forceFromSource) {
122
+ await runFromSource(serverArgs);
123
+ return;
124
+ }
125
+
126
+ const javaVersion = getJavaVersion();
127
+ const jarPath = path.join(__dirname, '..', '..', 'coral-server.jar');
128
+
129
+ if (javaVersion !== 24 && process.stdin.isTTY) {
130
+ console.log(`\nJava 24 is recommended, but version ${javaVersion || 'unknown'} was detected.`);
131
+ const answer = await askQuestion('Would you like to try running from source code? This will clone/update the repo in ~/.coral/source and use Gradle to run. (y/N): ');
132
+
133
+ if (answer.toLowerCase() === 'y') {
134
+ await runFromSource(serverArgs);
135
+ return;
136
+ }
137
+ }
138
+
139
+ // Fallback to java -jar
140
+ if (!fs.existsSync(jarPath)) {
141
+ console.error('Error: "java" command not found or version mismatch, and coral-server.jar not found at ' + jarPath);
142
+ process.exit(1);
143
+ }
144
+
145
+ const child = spawn('java', ['-jar', jarPath, ...serverArgs], {
146
+ stdio: 'inherit'
147
+ });
148
+
149
+ child.on('error', (err) => {
150
+ if (err.code === 'ENOENT') {
151
+ console.error('Error: "java" command not found. Please install Java (JRE or JDK, version 24 or later recommended).');
152
+ } else {
153
+ console.error('Error starting java:', err.message);
154
+ }
155
+ process.exit(1);
156
+ });
157
+
158
+ child.on('exit', (code) => {
159
+ process.exit(code !== null ? code : 1);
160
+ });
161
+
162
+ const killChild = () => {
163
+ if (child.pid) child.kill('SIGINT');
164
+ };
165
+
166
+ process.on('SIGINT', killChild);
167
+ process.on('SIGTERM', killChild);
168
+ }
169
+
170
+ module.exports = {
171
+ runFromSource,
172
+ runServer,
173
+ };
@@ -0,0 +1,30 @@
1
+ const { spawnSync } = require('child_process');
2
+ const readline = require('readline');
3
+
4
+ function getJavaVersion() {
5
+ try {
6
+ const result = spawnSync('java', ['-version'], { encoding: 'utf8' });
7
+ const output = result.stderr || result.stdout;
8
+ if (!output) return null;
9
+ const match = output.match(/(?:version|openjdk version) "(\d+)/i);
10
+ return match ? parseInt(match[1]) : null;
11
+ } catch (e) {
12
+ return null;
13
+ }
14
+ }
15
+
16
+ function askQuestion(query) {
17
+ const rl = readline.createInterface({
18
+ input: process.stdin,
19
+ output: process.stdout,
20
+ });
21
+ return new Promise(resolve => rl.question(query, ans => {
22
+ rl.close();
23
+ resolve(ans.trim());
24
+ }));
25
+ }
26
+
27
+ module.exports = {
28
+ getJavaVersion,
29
+ askQuestion,
30
+ };
@@ -0,0 +1,196 @@
1
+ const { spawnSync } = require('child_process');
2
+ const fs = require('fs');
3
+ const { pkg, LLM_PROVIDERS, IS_WINDOWS } = require('./constants');
4
+ const { askQuestion } = require('./utils');
5
+ const {
6
+ getConfigProfilePath,
7
+ ensureConfigProfileDir,
8
+ generateDefaultConfig,
9
+ buildConfigFromWizardResults
10
+ } = require('./config-manager');
11
+
12
+ async function testLlmProvider(providerId, apiKey) {
13
+ try {
14
+ if (providerId === 'openai') {
15
+ const result = spawnSync('curl', [
16
+ '-s', '-o', '/dev/null', '-w', '%{http_code}',
17
+ '-H', `Authorization: Bearer ${apiKey}`,
18
+ 'https://api.openai.com/v1/models',
19
+ ], { encoding: 'utf8', timeout: 15000 });
20
+ return result.stdout && result.stdout.trim() === '200';
21
+ } else if (providerId === 'anthropic') {
22
+ const result = spawnSync('curl', [
23
+ '-s', '-o', '/dev/null', '-w', '%{http_code}',
24
+ '-H', `x-api-key: ${apiKey}`,
25
+ '-H', 'anthropic-version: 2023-06-01',
26
+ 'https://api.anthropic.com/v1/models',
27
+ ], { encoding: 'utf8', timeout: 15000 });
28
+ const code = result.stdout ? result.stdout.trim() : '';
29
+ return code === '200';
30
+ } else if (providerId === 'openrouter') {
31
+ const result = spawnSync('curl', [
32
+ '-s', '-o', '/dev/null', '-w', '%{http_code}',
33
+ '-H', `Authorization: Bearer ${apiKey}`,
34
+ 'https://openrouter.ai/api/v1/models',
35
+ ], { encoding: 'utf8', timeout: 15000 });
36
+ return result.stdout && result.stdout.trim() === '200';
37
+ }
38
+ } catch (e) {
39
+ return false;
40
+ }
41
+ return false;
42
+ }
43
+
44
+ async function runSetupWizard(profileName, options = {}) {
45
+ const { hasAuthKeysArg = false, isStartCommand = false } = options;
46
+ const profilePath = getConfigProfilePath(profileName);
47
+ const version = pkg.version;
48
+
49
+ console.log(`\nWelcome! You can run through this wizard at any time by running this command:`);
50
+ console.log(` npx coralos-dev@${version} server configure ${profileName}\n`);
51
+
52
+ let authKey = null;
53
+ if (!hasAuthKeysArg) {
54
+ const prompt = isStartCommand ?
55
+ 'Enter an API key for your Coral Server (required for auth, press Enter to skip): ' :
56
+ 'Enter an API key for your Coral Server (optional, press Enter to skip): ';
57
+ authKey = await askQuestion(prompt);
58
+
59
+ if (!authKey && isStartCommand) {
60
+ console.log('\nContinuing without a key. You might need to provide one later via --auth.keys\n');
61
+ }
62
+ }
63
+
64
+ // Step 1: Coral Cloud API key
65
+ console.log('First, lets setup the LLM proxy.');
66
+ console.log('Please visit https://coralcloud.ai/account and create an API key.');
67
+ console.log('This will be used for running third party agents, and in the near future for LLM inference.\n');
68
+
69
+ const coralApiKey = await askQuestion('Enter your Coral Cloud API key (or press Enter to skip): ');
70
+
71
+ if (coralApiKey) {
72
+ try {
73
+ // Try to use keytar if available
74
+ const keytar = require('keytar');
75
+ await keytar.setPassword('coralos-dev', 'coral-cloud-api-key', coralApiKey);
76
+ console.log('API key saved securely in system keychain.\n');
77
+ } catch (e) {
78
+ console.log('Note: Could not save to system keychain (keytar not available).');
79
+ console.log('Please set it as an environment variable next time: export CORAL_CLOUD_API_KEY=your-key\n');
80
+ }
81
+ }
82
+
83
+ // Step 2: LLM providers
84
+ console.log('Please choose the LLM providers that you\'d like to use:\n');
85
+
86
+ const configuredProviders = [];
87
+
88
+ for (const provider of LLM_PROVIDERS) {
89
+ const apiKey = await askQuestion(`Enter API key for ${provider.name} (or press Enter to skip and set later): `);
90
+
91
+ if (!apiKey) {
92
+ console.log(` Skipped ${provider.name}.\n`);
93
+ continue;
94
+ }
95
+
96
+ console.log(` Testing ${provider.name}...`);
97
+ const works = await testLlmProvider(provider.id, apiKey);
98
+
99
+ if (works) {
100
+ console.log(` ✓ ${provider.name} is working!\n`);
101
+ configuredProviders.push({ id: provider.id, apiKey, working: true });
102
+ } else {
103
+ console.log(` ✗ ${provider.name} test failed.\n`);
104
+ configuredProviders.push({ id: provider.id, apiKey, working: false });
105
+ }
106
+ }
107
+
108
+ // Check if any working providers
109
+ const workingCount = configuredProviders.filter(p => p.working).length;
110
+ const failedProviders = configuredProviders.filter(p => !p.working);
111
+
112
+ if (failedProviders.length > 0) {
113
+ console.log('\nThe following providers failed validation:');
114
+ for (const p of failedProviders) {
115
+ const provider = LLM_PROVIDERS.find(lp => lp.id === p.id);
116
+ console.log(` - ${provider.name}`);
117
+ }
118
+
119
+ if (workingCount === 0) {
120
+ console.log('\n⚠ No working LLM providers configured. It is highly recommended to have at least 1 working provider.');
121
+ }
122
+
123
+ const fixAnswer = await askQuestion('\nWould you like to re-enter keys for failed providers? (y/N): ');
124
+
125
+ if (fixAnswer.toLowerCase() === 'y') {
126
+ for (const p of failedProviders) {
127
+ const provider = LLM_PROVIDERS.find(lp => lp.id === p.id);
128
+ const newKey = await askQuestion(`Enter new API key for ${provider.name} (or press Enter to keep current): `);
129
+
130
+ if (newKey) {
131
+ console.log(` Testing ${provider.name}...`);
132
+ const works = await testLlmProvider(p.id, newKey);
133
+ if (works) {
134
+ console.log(` ✓ ${provider.name} is now working!\n`);
135
+ p.apiKey = newKey;
136
+ p.working = true;
137
+ } else {
138
+ console.log(` ✗ ${provider.name} still failing. The key will be commented out in config.\n`);
139
+ p.apiKey = newKey;
140
+ }
141
+ }
142
+ }
143
+ } else {
144
+ console.log('Non-working providers will be commented out in the config file.');
145
+ }
146
+ }
147
+
148
+ // Write config
149
+ const config = buildConfigFromWizardResults(configuredProviders, authKey);
150
+ fs.writeFileSync(profilePath, config);
151
+ console.log(`\nConfiguration saved to ${profilePath}`);
152
+ }
153
+
154
+ async function handleFirstRun(profileName, options = {}) {
155
+ const profilePath = getConfigProfilePath(profileName);
156
+
157
+ ensureConfigProfileDir(profileName);
158
+ fs.writeFileSync(profilePath, generateDefaultConfig());
159
+ console.log(`File created at ${profilePath}\n`);
160
+
161
+ console.log('What next?');
162
+ console.log('1) Go through setup wizard? (recommended)');
163
+ console.log('2) Edit config with $EDITOR');
164
+ console.log('3) Continue to run the server with empty config');
165
+ console.log('4) Exit');
166
+
167
+ const choice = await askQuestion('\nChoose an option [1-4]: ');
168
+
169
+ switch (choice) {
170
+ case '1':
171
+ await runSetupWizard(profileName, options);
172
+ return true;
173
+ case '2': {
174
+ const editor = process.env.EDITOR || process.env.VISUAL || (IS_WINDOWS ? 'notepad' : 'vi');
175
+ const result = spawnSync(editor, [profilePath], { stdio: 'inherit' });
176
+ if (result.status !== 0) {
177
+ console.error('Editor exited with non-zero status.');
178
+ }
179
+ return true;
180
+ }
181
+ case '3':
182
+ return true;
183
+ case '4':
184
+ process.exit(0);
185
+ break;
186
+ default:
187
+ console.log('Invalid choice, continuing with empty config.');
188
+ return true;
189
+ }
190
+ }
191
+
192
+ module.exports = {
193
+ testLlmProvider,
194
+ runSetupWizard,
195
+ handleFirstRun,
196
+ };
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "coralos-dev",
3
+ "version": "1.1.0-SNAPSHOT-11",
4
+ "description": "Coral Server - Open Infrastructure Connecting the Internet of AI Agents",
5
+ "bin": {
6
+ "coralos-dev": "npx/coral-server.js"
7
+ },
8
+ "files": [
9
+ "npx/",
10
+ "bin/coral-server.jar"
11
+ ],
12
+ "engines": {
13
+ "node": ">=18"
14
+ },
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/Coral-Protocol/coral-server.git"
18
+ },
19
+ "keywords": [
20
+ "ai",
21
+ "agents",
22
+ "server",
23
+ "coral"
24
+ ],
25
+ "author": "Coral Protocol",
26
+ "license": "UNLICENSED",
27
+ "dependencies": {
28
+ "keytar": "^7.9.0"
29
+ }
30
+ }