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 +112 -0
- package/npx/coral-server.js +85 -0
- package/npx/lib/cli-parser.js +70 -0
- package/npx/lib/config-manager.js +81 -0
- package/npx/lib/constants.js +25 -0
- package/npx/lib/runner.js +173 -0
- package/npx/lib/utils.js +30 -0
- package/npx/lib/wizard.js +196 -0
- package/package.json +30 -0
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
|
+
[](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
|
+
};
|
package/npx/lib/utils.js
ADDED
|
@@ -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
|
+
}
|