@cloudless-ai/node-agent 0.0.13 → 0.0.15
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 +6 -2
- package/bin/cloudless-node.js +255 -0
- package/package.json +1 -1
- package/platform/darwin-amd64/CloudlessMenuBar +0 -0
- package/platform/darwin-arm64/CloudlessMenuBar +0 -0
- package/scripts/common.js +1 -0
- package/scripts/postinstall.js +1 -1
- package/platform/darwin-amd64/cloudless-agent +0 -0
- package/platform/darwin-arm64/cloudless-agent +0 -0
- package/platform/linux-amd64/cloudless-agent +0 -0
- package/platform/linux-arm64/cloudless-agent +0 -0
- package/platform/windows-amd64/cloudless-agent.exe +0 -0
- package/platform/windows-arm64/cloudless-agent.exe +0 -0
package/README.md
CHANGED
|
@@ -19,10 +19,11 @@ During install, the script:
|
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
21
|
cloudless-node start
|
|
22
|
+
cloudless-node start --interactive
|
|
22
23
|
cloudless-node menubar
|
|
24
|
+
cloudless-node stop
|
|
23
25
|
cloudless-node set-api-key
|
|
24
|
-
cloudless-node configure
|
|
25
|
-
cloudless-node paths
|
|
26
|
+
cloudless-node configure
|
|
26
27
|
```
|
|
27
28
|
|
|
28
29
|
## Non-interactive install
|
|
@@ -35,4 +36,7 @@ CLOUDLESS_NODE_API_KEY=your_key CLOUDLESS_ROUTER_URL=http://router-host:8080 npm
|
|
|
35
36
|
|
|
36
37
|
- By default binaries are installed to `~/.cloudless/bin`.
|
|
37
38
|
- Configuration is written to `~/.cloudless/agent.yaml`.
|
|
39
|
+
- `cloudless-node start` launches the node + menubar in the background by default.
|
|
40
|
+
- Use `cloudless-node start --interactive` to keep it attached to your terminal.
|
|
38
41
|
- Installation has no GitHub release dependency; binaries are bundled in the npm tarball.
|
|
42
|
+
- For internet demos, set `router.advertise_address` in `~/.cloudless/agent.yaml` (or `CLOUDLESS_AGENT_ADVERTISE_ADDRESS`) to a public tunnel URL so router/dashboard can reach this node.
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { spawn, spawnSync } = require("node:child_process");
|
|
3
|
+
const fs = require("node:fs");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
AGENT_BINARY_NAME,
|
|
8
|
+
MENUBAR_BINARY_NAME,
|
|
9
|
+
BIN_DIR,
|
|
10
|
+
CLOUDLESS_ROOT,
|
|
11
|
+
promptSecret,
|
|
12
|
+
promptLine,
|
|
13
|
+
writeOrUpdateConfig,
|
|
14
|
+
CONFIG_PATH,
|
|
15
|
+
} = require("../scripts/common");
|
|
16
|
+
|
|
17
|
+
const agentBin = path.join(BIN_DIR, AGENT_BINARY_NAME);
|
|
18
|
+
const menubarBin = path.join(BIN_DIR, MENUBAR_BINARY_NAME);
|
|
19
|
+
const supervisorPidFile = path.join(CLOUDLESS_ROOT, "cloudless-node-supervisor.pid");
|
|
20
|
+
|
|
21
|
+
function withDefaultConfig(args) {
|
|
22
|
+
const hasConfigArg = args.includes("--config");
|
|
23
|
+
if (hasConfigArg) {
|
|
24
|
+
return args;
|
|
25
|
+
}
|
|
26
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
27
|
+
return args;
|
|
28
|
+
}
|
|
29
|
+
return [...args, "--config", CONFIG_PATH];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function checkBinary(bin) {
|
|
33
|
+
if (!fs.existsSync(bin)) {
|
|
34
|
+
process.stderr.write(
|
|
35
|
+
`cloudless-node: binary not found at ${bin}\n` +
|
|
36
|
+
`Re-run \`npm install -g @cloudless-ai/node-agent\` to reinstall.\n`
|
|
37
|
+
);
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function runBinary(bin, args) {
|
|
43
|
+
checkBinary(bin);
|
|
44
|
+
const result = spawnSync(bin, withDefaultConfig(args), { stdio: "inherit" });
|
|
45
|
+
if (result.error) {
|
|
46
|
+
process.stderr.write(`cloudless-node: ${result.error.message}\n`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
process.exit(result.status ?? 0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function stopProcess(pid) {
|
|
53
|
+
try {
|
|
54
|
+
process.kill(pid, "SIGTERM");
|
|
55
|
+
return true;
|
|
56
|
+
} catch {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function readSupervisorPid() {
|
|
62
|
+
if (!fs.existsSync(supervisorPidFile)) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const raw = fs.readFileSync(supervisorPidFile, "utf8").trim();
|
|
66
|
+
if (!raw) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const pid = Number(raw);
|
|
70
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
process.kill(pid, 0);
|
|
75
|
+
return pid;
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function writeSupervisorPid() {
|
|
82
|
+
fs.mkdirSync(path.dirname(supervisorPidFile), { recursive: true });
|
|
83
|
+
fs.writeFileSync(supervisorPidFile, `${process.pid}\n`, "utf8");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function clearSupervisorPid() {
|
|
87
|
+
try {
|
|
88
|
+
fs.unlinkSync(supervisorPidFile);
|
|
89
|
+
} catch {}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function runSupervisor(agentArgs) {
|
|
93
|
+
checkBinary(agentBin);
|
|
94
|
+
|
|
95
|
+
const isMac = process.platform === "darwin";
|
|
96
|
+
const startTime = Date.now();
|
|
97
|
+
|
|
98
|
+
const agent = spawn(agentBin, withDefaultConfig(agentArgs), { stdio: "inherit" });
|
|
99
|
+
|
|
100
|
+
let menubar = null;
|
|
101
|
+
if (isMac) {
|
|
102
|
+
if (fs.existsSync(menubarBin)) {
|
|
103
|
+
menubar = spawn(menubarBin, [], { stdio: "ignore" });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
writeSupervisorPid();
|
|
108
|
+
|
|
109
|
+
let shuttingDown = false;
|
|
110
|
+
function shutdown() {
|
|
111
|
+
if (shuttingDown) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
shuttingDown = true;
|
|
115
|
+
agent.kill();
|
|
116
|
+
if (menubar) menubar.kill();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
process.on("SIGINT", shutdown);
|
|
120
|
+
process.on("SIGTERM", shutdown);
|
|
121
|
+
process.on("exit", clearSupervisorPid);
|
|
122
|
+
|
|
123
|
+
agent.on("exit", (code) => {
|
|
124
|
+
const exitedQuickly = Date.now() - startTime < 5000;
|
|
125
|
+
if (menubar && code !== 0 && code !== null && exitedQuickly) {
|
|
126
|
+
process.stderr.write(
|
|
127
|
+
"cloudless-node: agent failed to start, keeping menubar attached to existing local agent if available\n"
|
|
128
|
+
);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (menubar) menubar.kill();
|
|
132
|
+
clearSupervisorPid();
|
|
133
|
+
process.exit(code ?? 0);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (menubar) {
|
|
137
|
+
menubar.on("exit", () => {
|
|
138
|
+
agent.kill();
|
|
139
|
+
clearSupervisorPid();
|
|
140
|
+
process.exit(0);
|
|
141
|
+
});
|
|
142
|
+
menubar.on("exit", (code) => {
|
|
143
|
+
if (code !== 0 && code !== null) {
|
|
144
|
+
process.stderr.write(`cloudless-node: menubar exited with code ${code}\n`);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function parseStartArgs(args) {
|
|
151
|
+
let interactive = false;
|
|
152
|
+
const agentArgs = [];
|
|
153
|
+
for (const arg of args) {
|
|
154
|
+
if (arg === "--interactive") {
|
|
155
|
+
interactive = true;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
agentArgs.push(arg);
|
|
159
|
+
}
|
|
160
|
+
return { interactive, agentArgs };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function startDetached(agentArgs) {
|
|
164
|
+
const existing = readSupervisorPid();
|
|
165
|
+
if (existing) {
|
|
166
|
+
process.stdout.write(`cloudless-node: already running (supervisor pid ${existing})\n`);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const child = spawn(process.execPath, [__filename, "run-supervised", ...agentArgs], {
|
|
170
|
+
detached: true,
|
|
171
|
+
stdio: "ignore",
|
|
172
|
+
});
|
|
173
|
+
child.unref();
|
|
174
|
+
process.stdout.write(`cloudless-node: started in background (supervisor pid ${child.pid})\n`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function stopSupervisor() {
|
|
178
|
+
const pid = readSupervisorPid();
|
|
179
|
+
if (!pid) {
|
|
180
|
+
process.stdout.write("cloudless-node: not running\n");
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (!stopProcess(pid)) {
|
|
184
|
+
clearSupervisorPid();
|
|
185
|
+
process.stdout.write("cloudless-node: not running\n");
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
process.stdout.write("cloudless-node: stop signal sent\n");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function configure() {
|
|
192
|
+
const defaultURL = "http://localhost:8080";
|
|
193
|
+
process.stdout.write("Cloudless AI node configuration\n\n");
|
|
194
|
+
|
|
195
|
+
const routerURL =
|
|
196
|
+
(await promptLine(`Router URL [${defaultURL}]: `)) || defaultURL;
|
|
197
|
+
process.stdout.write(
|
|
198
|
+
"API key: create a node-scoped key in the Cloudless AI dashboard (Admin → API Keys → node scope).\n"
|
|
199
|
+
);
|
|
200
|
+
const apiKey = await promptSecret("Router API key (hidden): ");
|
|
201
|
+
|
|
202
|
+
if (!apiKey) {
|
|
203
|
+
process.stderr.write("API key is required.\n");
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
writeOrUpdateConfig({ routerURL, token: apiKey });
|
|
208
|
+
process.stdout.write(`\nConfig saved to ${CONFIG_PATH}\n`);
|
|
209
|
+
process.stdout.write("Run `cloudless-node start` to launch the node agent.\n");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const [, , cmd, ...rest] = process.argv;
|
|
213
|
+
|
|
214
|
+
switch (cmd) {
|
|
215
|
+
case "configure":
|
|
216
|
+
case "set-api-key":
|
|
217
|
+
configure().catch((err) => {
|
|
218
|
+
process.stderr.write(`${err.message}\n`);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
});
|
|
221
|
+
break;
|
|
222
|
+
|
|
223
|
+
case "menubar":
|
|
224
|
+
case "start": {
|
|
225
|
+
const { interactive, agentArgs } = parseStartArgs(rest);
|
|
226
|
+
if (interactive) {
|
|
227
|
+
runSupervisor(agentArgs);
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
startDetached(agentArgs);
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
case "run-supervised":
|
|
235
|
+
runSupervisor(rest);
|
|
236
|
+
break;
|
|
237
|
+
|
|
238
|
+
case "stop":
|
|
239
|
+
stopSupervisor();
|
|
240
|
+
break;
|
|
241
|
+
|
|
242
|
+
case undefined: {
|
|
243
|
+
const { interactive, agentArgs } = parseStartArgs(rest);
|
|
244
|
+
if (interactive) {
|
|
245
|
+
runSupervisor(agentArgs);
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
startDetached(agentArgs);
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
default:
|
|
253
|
+
// Pass unknown subcommands/flags straight to the agent binary
|
|
254
|
+
runBinary(agentBin, [cmd, ...rest]);
|
|
255
|
+
}
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
package/scripts/common.js
CHANGED
|
@@ -112,6 +112,7 @@ function defaultAgentConfig(routerURL, token) {
|
|
|
112
112
|
discover_interval: "1m"
|
|
113
113
|
servers_file: "${HOME}/.cloudless/router-servers.json"
|
|
114
114
|
manual_servers: []
|
|
115
|
+
# advertise_address: "https://my-node-demo.example.com"
|
|
115
116
|
|
|
116
117
|
inference:
|
|
117
118
|
backend: "llamacpp"
|
package/scripts/postinstall.js
CHANGED
|
@@ -54,7 +54,7 @@ async function promptConfig() {
|
|
|
54
54
|
}
|
|
55
55
|
if (!apiKey) {
|
|
56
56
|
process.stdout.write("\nNo API key provided during install.\n");
|
|
57
|
-
process.stdout.write("Run `cloudless-node
|
|
57
|
+
process.stdout.write("Run `cloudless-node configure` to set up your router URL and API key.\n");
|
|
58
58
|
return;
|
|
59
59
|
}
|
|
60
60
|
writeOrUpdateConfig({ routerURL, token: apiKey });
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|