volute 0.10.2 → 0.11.1
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 +2 -1
- package/dist/{agent-ECRX44DB.js → agent-JGO6N7IA.js} +2 -2
- package/dist/{agent-manager-4OCID725.js → agent-manager-B6EPBU45.js} +1 -1
- package/dist/chunk-25GGN3OV.js +175 -0
- package/dist/{chunk-M5AEQLB3.js → chunk-4QMF7SNL.js} +33 -19
- package/dist/{chunk-KR6WRAJ4.js → chunk-ANS3UV23.js} +34 -2
- package/dist/{chunk-FYQGANL6.js → chunk-GGLTM53H.js} +30 -57
- package/dist/cli.js +12 -8
- package/dist/daemon-restart-UCUNJ4AD.js +49 -0
- package/dist/daemon.js +11 -11
- package/dist/{down-4LIQG3CE.js → down-XV2OQJ7O.js} +3 -3
- package/dist/{package-BS2B432F.js → package-7NO4W4WX.js} +2 -2
- package/dist/{service-OW35VZ5G.js → service-U6SN5OZO.js} +15 -13
- package/dist/{setup-EDCCQ3X7.js → setup-GMZOD52B.js} +21 -6
- package/dist/status-CTWXP6UW.js +67 -0
- package/dist/{up-FCYL2IPZ.js → up-IY5M3Q35.js} +3 -1
- package/dist/{update-3TGXUTO2.js → update-BPKQX5OY.js} +75 -12
- package/package.json +2 -2
- package/dist/daemon-restart-7X72OXOW.js +0 -61
- package/dist/status-SIMKH3ZE.js +0 -63
package/README.md
CHANGED
|
@@ -36,6 +36,7 @@ One background process runs everything. `volute up` starts it; `volute down` sto
|
|
|
36
36
|
volute up # start (default port 4200)
|
|
37
37
|
volute up --port 8080 # custom port
|
|
38
38
|
volute down # stop all agents and shut down
|
|
39
|
+
volute status # check daemon status, version, and agents
|
|
39
40
|
```
|
|
40
41
|
|
|
41
42
|
The daemon handles agent lifecycle, crash recovery (auto-restarts after 3 seconds), connector processes, scheduled messages, and the web dashboard.
|
|
@@ -232,7 +233,7 @@ The container runs with per-agent user isolation enabled — each agent gets its
|
|
|
232
233
|
|
|
233
234
|
### Bare metal (Linux / Raspberry Pi)
|
|
234
235
|
|
|
235
|
-
One-liner install on a fresh Debian/Ubuntu
|
|
236
|
+
One-liner install on a fresh Linux system (Debian/Ubuntu, RHEL/Fedora, Arch, Alpine, SUSE):
|
|
236
237
|
|
|
237
238
|
```sh
|
|
238
239
|
curl -fsSL <install-url> | sudo bash
|
|
@@ -21,14 +21,14 @@ async function run(args) {
|
|
|
21
21
|
await import("./delete-BOTVU4YO.js").then((m) => m.run(args.slice(1)));
|
|
22
22
|
break;
|
|
23
23
|
case "list":
|
|
24
|
-
await import("./status-
|
|
24
|
+
await import("./status-CTWXP6UW.js").then((m) => m.run(args.slice(1)));
|
|
25
25
|
break;
|
|
26
26
|
case "status": {
|
|
27
27
|
const rest = args.slice(1);
|
|
28
28
|
if (!rest[0] && process.env.VOLUTE_AGENT) {
|
|
29
29
|
rest.unshift(process.env.VOLUTE_AGENT);
|
|
30
30
|
}
|
|
31
|
-
await import("./status-
|
|
31
|
+
await import("./status-CTWXP6UW.js").then((m) => m.run(rest));
|
|
32
32
|
break;
|
|
33
33
|
}
|
|
34
34
|
case "logs": {
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
execInherit
|
|
4
|
+
} from "./chunk-5C5JWR2L.js";
|
|
5
|
+
import {
|
|
6
|
+
voluteHome
|
|
7
|
+
} from "./chunk-DP2DX4WV.js";
|
|
8
|
+
|
|
9
|
+
// src/lib/service-mode.ts
|
|
10
|
+
import { execFileSync } from "child_process";
|
|
11
|
+
import { existsSync, readFileSync } from "fs";
|
|
12
|
+
import { homedir } from "os";
|
|
13
|
+
import { resolve } from "path";
|
|
14
|
+
var SYSTEM_SERVICE_PATH = "/etc/systemd/system/volute.service";
|
|
15
|
+
var USER_SYSTEMD_UNIT = resolve(homedir(), ".config", "systemd", "user", "volute.service");
|
|
16
|
+
var LAUNCHD_PLIST_LABEL = "com.volute.daemon";
|
|
17
|
+
var LAUNCHD_PLIST_PATH = resolve(
|
|
18
|
+
homedir(),
|
|
19
|
+
"Library",
|
|
20
|
+
"LaunchAgents",
|
|
21
|
+
`${LAUNCHD_PLIST_LABEL}.plist`
|
|
22
|
+
);
|
|
23
|
+
var HEALTH_POLL_TIMEOUT = 3e4;
|
|
24
|
+
var STOP_GRACE_TIMEOUT = 1e4;
|
|
25
|
+
var POLL_INTERVAL = 500;
|
|
26
|
+
function getServiceMode() {
|
|
27
|
+
if (existsSync(SYSTEM_SERVICE_PATH)) {
|
|
28
|
+
try {
|
|
29
|
+
execFileSync("systemctl", ["is-enabled", "--quiet", "volute"]);
|
|
30
|
+
return "system";
|
|
31
|
+
} catch {
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (existsSync(USER_SYSTEMD_UNIT)) {
|
|
35
|
+
try {
|
|
36
|
+
execFileSync("systemctl", ["--user", "is-enabled", "--quiet", "volute"]);
|
|
37
|
+
return "user-systemd";
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (process.platform === "darwin" && existsSync(LAUNCHD_PLIST_PATH)) {
|
|
42
|
+
return "user-launchd";
|
|
43
|
+
}
|
|
44
|
+
return "manual";
|
|
45
|
+
}
|
|
46
|
+
function getDaemonUrl(hostname, port) {
|
|
47
|
+
const url = new URL("http://localhost");
|
|
48
|
+
let h = hostname;
|
|
49
|
+
if (h === "0.0.0.0" || h === "::") h = "localhost";
|
|
50
|
+
else if (h.includes(":") && !h.startsWith("[")) h = `[${h}]`;
|
|
51
|
+
url.hostname = h;
|
|
52
|
+
url.port = String(port);
|
|
53
|
+
return url.origin;
|
|
54
|
+
}
|
|
55
|
+
async function pollHealth(hostname, port, timeout = HEALTH_POLL_TIMEOUT) {
|
|
56
|
+
const url = `${getDaemonUrl(hostname, port)}/api/health`;
|
|
57
|
+
const start = Date.now();
|
|
58
|
+
while (Date.now() - start < timeout) {
|
|
59
|
+
try {
|
|
60
|
+
const res = await fetch(url);
|
|
61
|
+
if (res.ok) {
|
|
62
|
+
const body = await res.json().catch(() => null);
|
|
63
|
+
if (body && body.ok) return true;
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL));
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
async function pollHealthDown(hostname, port, timeout = STOP_GRACE_TIMEOUT) {
|
|
72
|
+
const url = `${getDaemonUrl(hostname, port)}/api/health`;
|
|
73
|
+
const start = Date.now();
|
|
74
|
+
while (Date.now() - start < timeout) {
|
|
75
|
+
try {
|
|
76
|
+
const res = await fetch(url);
|
|
77
|
+
if (!res.ok) return true;
|
|
78
|
+
const body = await res.json().catch(() => null);
|
|
79
|
+
if (!body || !body.ok) return true;
|
|
80
|
+
} catch {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
await new Promise((r) => setTimeout(r, POLL_INTERVAL));
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
async function startService(mode) {
|
|
88
|
+
switch (mode) {
|
|
89
|
+
case "system":
|
|
90
|
+
await execInherit("sudo", ["systemctl", "start", "volute"]);
|
|
91
|
+
break;
|
|
92
|
+
case "user-systemd":
|
|
93
|
+
await execInherit("systemctl", ["--user", "start", "volute"]);
|
|
94
|
+
break;
|
|
95
|
+
case "user-launchd":
|
|
96
|
+
await execInherit("launchctl", ["load", LAUNCHD_PLIST_PATH]);
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function stopService(mode) {
|
|
101
|
+
switch (mode) {
|
|
102
|
+
case "system":
|
|
103
|
+
await execInherit("sudo", ["systemctl", "stop", "volute"]);
|
|
104
|
+
break;
|
|
105
|
+
case "user-systemd":
|
|
106
|
+
await execInherit("systemctl", ["--user", "stop", "volute"]);
|
|
107
|
+
break;
|
|
108
|
+
case "user-launchd":
|
|
109
|
+
await execInherit("launchctl", ["unload", LAUNCHD_PLIST_PATH]);
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function restartService(mode) {
|
|
114
|
+
switch (mode) {
|
|
115
|
+
case "system":
|
|
116
|
+
await execInherit("sudo", ["systemctl", "restart", "volute"]);
|
|
117
|
+
break;
|
|
118
|
+
case "user-systemd":
|
|
119
|
+
await execInherit("systemctl", ["--user", "restart", "volute"]);
|
|
120
|
+
break;
|
|
121
|
+
case "user-launchd":
|
|
122
|
+
try {
|
|
123
|
+
await execInherit("launchctl", ["unload", LAUNCHD_PLIST_PATH]);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
console.warn(
|
|
126
|
+
`Warning: launchctl unload failed: ${err instanceof Error ? err.message : err}`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
await execInherit("launchctl", ["load", LAUNCHD_PLIST_PATH]);
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function readDaemonConfig() {
|
|
134
|
+
const configPath = resolve(voluteHome(), "daemon.json");
|
|
135
|
+
if (!existsSync(configPath)) return { hostname: "127.0.0.1", port: 4200 };
|
|
136
|
+
try {
|
|
137
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
138
|
+
return {
|
|
139
|
+
hostname: config.hostname || "127.0.0.1",
|
|
140
|
+
port: config.port ?? 4200,
|
|
141
|
+
token: config.token
|
|
142
|
+
};
|
|
143
|
+
} catch {
|
|
144
|
+
console.error("Warning: could not read daemon config, using defaults.");
|
|
145
|
+
return { hostname: "127.0.0.1", port: 4200 };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function modeLabel(mode) {
|
|
149
|
+
switch (mode) {
|
|
150
|
+
case "system":
|
|
151
|
+
return "system service (systemd)";
|
|
152
|
+
case "user-systemd":
|
|
153
|
+
return "user service (systemd)";
|
|
154
|
+
case "user-launchd":
|
|
155
|
+
return "user service (launchd)";
|
|
156
|
+
case "manual":
|
|
157
|
+
return "manual";
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export {
|
|
162
|
+
SYSTEM_SERVICE_PATH,
|
|
163
|
+
USER_SYSTEMD_UNIT,
|
|
164
|
+
LAUNCHD_PLIST_LABEL,
|
|
165
|
+
LAUNCHD_PLIST_PATH,
|
|
166
|
+
getServiceMode,
|
|
167
|
+
getDaemonUrl,
|
|
168
|
+
pollHealth,
|
|
169
|
+
pollHealthDown,
|
|
170
|
+
startService,
|
|
171
|
+
stopService,
|
|
172
|
+
restartService,
|
|
173
|
+
readDaemonConfig,
|
|
174
|
+
modeLabel
|
|
175
|
+
};
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
getServiceMode,
|
|
4
|
+
modeLabel,
|
|
5
|
+
pollHealth,
|
|
6
|
+
startService
|
|
7
|
+
} from "./chunk-25GGN3OV.js";
|
|
2
8
|
import {
|
|
3
9
|
parseArgs
|
|
4
10
|
} from "./chunk-D424ZQGI.js";
|
|
@@ -7,7 +13,7 @@ import {
|
|
|
7
13
|
} from "./chunk-DP2DX4WV.js";
|
|
8
14
|
|
|
9
15
|
// src/commands/up.ts
|
|
10
|
-
import {
|
|
16
|
+
import { spawn } from "child_process";
|
|
11
17
|
import { existsSync, mkdirSync, openSync, readFileSync } from "fs";
|
|
12
18
|
import { dirname, resolve } from "path";
|
|
13
19
|
function readGlobalConfig() {
|
|
@@ -20,26 +26,31 @@ function readGlobalConfig() {
|
|
|
20
26
|
process.exit(1);
|
|
21
27
|
}
|
|
22
28
|
}
|
|
23
|
-
function isSystemdServiceEnabled() {
|
|
24
|
-
try {
|
|
25
|
-
execFileSync("systemctl", ["is-enabled", "--quiet", "volute"]);
|
|
26
|
-
return true;
|
|
27
|
-
} catch {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
29
|
async function run(args) {
|
|
32
30
|
const { flags } = parseArgs(args, {
|
|
33
31
|
port: { type: "number" },
|
|
34
32
|
host: { type: "string" },
|
|
35
33
|
foreground: { type: "boolean" }
|
|
36
34
|
});
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
console.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
35
|
+
const mode = getServiceMode();
|
|
36
|
+
if (!flags.foreground && mode !== "manual") {
|
|
37
|
+
console.log(`Starting volute (${modeLabel(mode)})...`);
|
|
38
|
+
try {
|
|
39
|
+
await startService(mode);
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.error(`Failed to start service: ${err instanceof Error ? err.message : err}`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
const config2 = readGlobalConfig();
|
|
45
|
+
const h = flags.host ?? config2.hostname ?? "127.0.0.1";
|
|
46
|
+
const p = flags.port ?? config2.port ?? 4200;
|
|
47
|
+
if (await pollHealth(h, p)) {
|
|
48
|
+
console.log(`Volute daemon running on ${h}:${p}`);
|
|
49
|
+
} else {
|
|
50
|
+
console.error("Service started but daemon did not become healthy within 30s.");
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
43
54
|
}
|
|
44
55
|
const config = readGlobalConfig();
|
|
45
56
|
const port = flags.port ?? config.port ?? 4200;
|
|
@@ -59,10 +70,13 @@ async function run(args) {
|
|
|
59
70
|
try {
|
|
60
71
|
const res = await fetch(`http://${pollHost}:${port}/api/health`);
|
|
61
72
|
if (res.ok) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
73
|
+
const body = await res.json().catch(() => null);
|
|
74
|
+
if (body && body.ok) {
|
|
75
|
+
console.error(
|
|
76
|
+
`Port ${port} is already in use by a Volute daemon. Use 'volute down' first, or kill the process on that port.`
|
|
77
|
+
);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
66
80
|
}
|
|
67
81
|
} catch {
|
|
68
82
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
applyIsolation
|
|
3
|
+
applyIsolation,
|
|
4
|
+
chownAgentDir,
|
|
5
|
+
isIsolationEnabled
|
|
4
6
|
} from "./chunk-46S7YHUB.js";
|
|
5
7
|
import {
|
|
6
8
|
loadMergedEnv
|
|
@@ -17,7 +19,7 @@ import {
|
|
|
17
19
|
|
|
18
20
|
// src/lib/agent-manager.ts
|
|
19
21
|
import { execFile, spawn } from "child_process";
|
|
20
|
-
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
22
|
+
import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, rmSync as rmSync2, symlinkSync, writeFileSync as writeFileSync2 } from "fs";
|
|
21
23
|
import { resolve } from "path";
|
|
22
24
|
import { promisify } from "util";
|
|
23
25
|
|
|
@@ -194,6 +196,36 @@ var AgentManager = class {
|
|
|
194
196
|
VOLUTE_AGENT_DIR: dir,
|
|
195
197
|
VOLUTE_AGENT_PORT: String(port)
|
|
196
198
|
};
|
|
199
|
+
if (isIsolationEnabled() && process.env.CLAUDE_CONFIG_DIR) {
|
|
200
|
+
const agentClaudeDir = resolve(dir, ".claude-config");
|
|
201
|
+
try {
|
|
202
|
+
mkdirSync(agentClaudeDir, { recursive: true });
|
|
203
|
+
} catch (err) {
|
|
204
|
+
throw new Error(
|
|
205
|
+
`Cannot start agent ${name}: failed to create config directory at ${agentClaudeDir}: ${err instanceof Error ? err.message : err}`
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
const sharedCreds = resolve(process.env.CLAUDE_CONFIG_DIR, ".credentials.json");
|
|
209
|
+
const agentCreds = resolve(agentClaudeDir, ".credentials.json");
|
|
210
|
+
if (existsSync3(sharedCreds)) {
|
|
211
|
+
if (!existsSync3(agentCreds)) {
|
|
212
|
+
try {
|
|
213
|
+
symlinkSync(sharedCreds, agentCreds);
|
|
214
|
+
} catch (err) {
|
|
215
|
+
console.error(
|
|
216
|
+
`[daemon] failed to symlink credentials for ${name}: ${err instanceof Error ? err.message : err}`
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
console.warn(
|
|
222
|
+
`[daemon] shared credentials not found at ${sharedCreds} for agent ${name}. Copy ~/.claude/.credentials.json to ${process.env.CLAUDE_CONFIG_DIR}/.credentials.json or set ANTHROPIC_API_KEY in the agent's environment.`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
const baseName2 = name.split("@", 2)[0];
|
|
226
|
+
chownAgentDir(agentClaudeDir, baseName2);
|
|
227
|
+
env.CLAUDE_CONFIG_DIR = agentClaudeDir;
|
|
228
|
+
}
|
|
197
229
|
const tsxBin = resolve(dir, "node_modules", ".bin", "tsx");
|
|
198
230
|
const spawnOpts = {
|
|
199
231
|
cwd: dir,
|
|
@@ -1,31 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
getServiceMode,
|
|
4
|
+
modeLabel,
|
|
5
|
+
pollHealthDown,
|
|
6
|
+
readDaemonConfig,
|
|
7
|
+
stopService
|
|
8
|
+
} from "./chunk-25GGN3OV.js";
|
|
5
9
|
import {
|
|
6
10
|
voluteHome
|
|
7
11
|
} from "./chunk-DP2DX4WV.js";
|
|
8
|
-
import {
|
|
9
|
-
getClient,
|
|
10
|
-
urlOf
|
|
11
|
-
} from "./chunk-4RQBJWQX.js";
|
|
12
12
|
|
|
13
13
|
// src/commands/down.ts
|
|
14
|
-
import { execFileSync } from "child_process";
|
|
15
14
|
import { existsSync, readFileSync, unlinkSync } from "fs";
|
|
16
15
|
import { resolve } from "path";
|
|
17
|
-
function isSystemdServiceEnabled() {
|
|
18
|
-
try {
|
|
19
|
-
execFileSync("systemctl", ["is-enabled", "--quiet", "volute"]);
|
|
20
|
-
return true;
|
|
21
|
-
} catch {
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
16
|
async function stopDaemon() {
|
|
26
|
-
if (isSystemdServiceEnabled()) {
|
|
27
|
-
return { stopped: false, reason: "systemd" };
|
|
28
|
-
}
|
|
29
17
|
const home = voluteHome();
|
|
30
18
|
const pidPath = resolve(home, "daemon.pid");
|
|
31
19
|
if (!existsSync(pidPath)) {
|
|
@@ -82,7 +70,7 @@ async function stopDaemon() {
|
|
|
82
70
|
console.error(
|
|
83
71
|
`Failed to send SIGTERM to daemon (pid ${pid}): ${e instanceof Error ? e.message : e}`
|
|
84
72
|
);
|
|
85
|
-
return { stopped: false, reason: "kill-failed"
|
|
73
|
+
return { stopped: false, reason: "kill-failed" };
|
|
86
74
|
}
|
|
87
75
|
}
|
|
88
76
|
const maxWait = 1e4;
|
|
@@ -116,49 +104,34 @@ async function stopDaemon() {
|
|
|
116
104
|
return { stopped: true, clean: false };
|
|
117
105
|
}
|
|
118
106
|
async function run(_args) {
|
|
119
|
-
const
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
let port = 4200;
|
|
128
|
-
if (existsSync(configPath)) {
|
|
129
|
-
try {
|
|
130
|
-
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
131
|
-
hostname = config.hostname || "localhost";
|
|
132
|
-
port = config.port ?? 4200;
|
|
133
|
-
} catch {
|
|
134
|
-
}
|
|
107
|
+
const mode = getServiceMode();
|
|
108
|
+
if (mode !== "manual") {
|
|
109
|
+
console.log(`Stopping volute (${modeLabel(mode)})...`);
|
|
110
|
+
try {
|
|
111
|
+
await stopService(mode);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.error(`Failed to stop service: ${err instanceof Error ? err.message : err}`);
|
|
114
|
+
process.exit(1);
|
|
135
115
|
}
|
|
136
|
-
|
|
137
|
-
if (hostname
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const maxWait = 1e4;
|
|
143
|
-
const start = Date.now();
|
|
144
|
-
while (Date.now() - start < maxWait) {
|
|
145
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
146
|
-
try {
|
|
147
|
-
await fetch(healthUrl);
|
|
148
|
-
} catch {
|
|
149
|
-
console.log("Daemon stopped.");
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
116
|
+
const { hostname, port } = readDaemonConfig();
|
|
117
|
+
if (await pollHealthDown(hostname, port)) {
|
|
118
|
+
console.log("Daemon stopped.");
|
|
119
|
+
} else {
|
|
120
|
+
console.error("Service stopped but daemon may still be responding.");
|
|
121
|
+
process.exit(1);
|
|
152
122
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const result = await stopDaemon();
|
|
126
|
+
if (result.stopped) return;
|
|
127
|
+
if (result.reason === "orphan") {
|
|
156
128
|
console.error(`Daemon appears to be running on port ${result.port} but PID file is missing.`);
|
|
157
129
|
console.error(`Kill the process manually: lsof -ti :${result.port} | xargs kill`);
|
|
158
|
-
|
|
159
|
-
|
|
130
|
+
process.exit(1);
|
|
131
|
+
} else if (result.reason === "kill-failed") {
|
|
132
|
+
process.exit(1);
|
|
160
133
|
}
|
|
161
|
-
|
|
134
|
+
console.log("Daemon is not running.");
|
|
162
135
|
}
|
|
163
136
|
|
|
164
137
|
export {
|
package/dist/cli.js
CHANGED
|
@@ -9,13 +9,13 @@ if (!process.env.VOLUTE_HOME) {
|
|
|
9
9
|
var command = process.argv[2];
|
|
10
10
|
var args = process.argv.slice(3);
|
|
11
11
|
if (command === "--version" || command === "-v") {
|
|
12
|
-
const { default: pkg } = await import("./package-
|
|
12
|
+
const { default: pkg } = await import("./package-7NO4W4WX.js");
|
|
13
13
|
console.log(pkg.version);
|
|
14
14
|
process.exit(0);
|
|
15
15
|
}
|
|
16
16
|
switch (command) {
|
|
17
17
|
case "agent":
|
|
18
|
-
await import("./agent-
|
|
18
|
+
await import("./agent-JGO6N7IA.js").then((m) => m.run(args));
|
|
19
19
|
break;
|
|
20
20
|
case "send":
|
|
21
21
|
await import("./send-X6OQGSD6.js").then((m) => m.run(args));
|
|
@@ -39,22 +39,25 @@ switch (command) {
|
|
|
39
39
|
await import("./env-CGORIKVF.js").then((m) => m.run(args));
|
|
40
40
|
break;
|
|
41
41
|
case "up":
|
|
42
|
-
await import("./up-
|
|
42
|
+
await import("./up-IY5M3Q35.js").then((m) => m.run(args));
|
|
43
43
|
break;
|
|
44
44
|
case "down":
|
|
45
|
-
await import("./down-
|
|
45
|
+
await import("./down-XV2OQJ7O.js").then((m) => m.run(args));
|
|
46
46
|
break;
|
|
47
47
|
case "restart":
|
|
48
|
-
await import("./daemon-restart-
|
|
48
|
+
await import("./daemon-restart-UCUNJ4AD.js").then((m) => m.run(args));
|
|
49
49
|
break;
|
|
50
50
|
case "setup":
|
|
51
|
-
await import("./setup-
|
|
51
|
+
await import("./setup-GMZOD52B.js").then((m) => m.run(args));
|
|
52
52
|
break;
|
|
53
53
|
case "service":
|
|
54
|
-
await import("./service-
|
|
54
|
+
await import("./service-U6SN5OZO.js").then((m) => m.run(args));
|
|
55
55
|
break;
|
|
56
56
|
case "update":
|
|
57
|
-
await import("./update-
|
|
57
|
+
await import("./update-BPKQX5OY.js").then((m) => m.run(args));
|
|
58
|
+
break;
|
|
59
|
+
case "status":
|
|
60
|
+
await import("./status-CTWXP6UW.js").then((m) => m.run(args));
|
|
58
61
|
break;
|
|
59
62
|
case "--help":
|
|
60
63
|
case "-h":
|
|
@@ -106,6 +109,7 @@ Commands:
|
|
|
106
109
|
volute setup uninstall [--force] Remove system service + isolation
|
|
107
110
|
|
|
108
111
|
volute update Update to latest version
|
|
112
|
+
volute status Show daemon status and agents
|
|
109
113
|
|
|
110
114
|
Options:
|
|
111
115
|
--version, -v Show version number
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
run
|
|
4
|
+
} from "./chunk-4QMF7SNL.js";
|
|
5
|
+
import {
|
|
6
|
+
stopDaemon
|
|
7
|
+
} from "./chunk-GGLTM53H.js";
|
|
8
|
+
import {
|
|
9
|
+
getServiceMode,
|
|
10
|
+
modeLabel,
|
|
11
|
+
pollHealth,
|
|
12
|
+
readDaemonConfig,
|
|
13
|
+
restartService
|
|
14
|
+
} from "./chunk-25GGN3OV.js";
|
|
15
|
+
import "./chunk-5C5JWR2L.js";
|
|
16
|
+
import "./chunk-D424ZQGI.js";
|
|
17
|
+
import "./chunk-DP2DX4WV.js";
|
|
18
|
+
import "./chunk-K3NQKI34.js";
|
|
19
|
+
|
|
20
|
+
// src/commands/daemon-restart.ts
|
|
21
|
+
async function run2(args) {
|
|
22
|
+
const mode = getServiceMode();
|
|
23
|
+
if (mode !== "manual") {
|
|
24
|
+
console.log(`Restarting volute (${modeLabel(mode)})...`);
|
|
25
|
+
try {
|
|
26
|
+
await restartService(mode);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.error(`Failed to restart service: ${err instanceof Error ? err.message : err}`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
const { hostname, port } = readDaemonConfig();
|
|
32
|
+
if (await pollHealth(hostname, port)) {
|
|
33
|
+
console.log("Daemon restarted.");
|
|
34
|
+
} else {
|
|
35
|
+
console.error("Service restarted but daemon did not become healthy within 30s.");
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const result = await stopDaemon();
|
|
41
|
+
if (!result.stopped && result.reason === "kill-failed") {
|
|
42
|
+
console.error("Cannot restart: failed to stop the running daemon.");
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
await run(args);
|
|
46
|
+
}
|
|
47
|
+
export {
|
|
48
|
+
run2 as run
|
|
49
|
+
};
|
package/dist/daemon.js
CHANGED
|
@@ -6,12 +6,7 @@ import {
|
|
|
6
6
|
initAgentManager,
|
|
7
7
|
loadJsonMap,
|
|
8
8
|
saveJsonMap
|
|
9
|
-
} from "./chunk-
|
|
10
|
-
import {
|
|
11
|
-
checkForUpdate,
|
|
12
|
-
checkForUpdateCached,
|
|
13
|
-
getCurrentVersion
|
|
14
|
-
} from "./chunk-RT6Y7AR3.js";
|
|
9
|
+
} from "./chunk-ANS3UV23.js";
|
|
15
10
|
import {
|
|
16
11
|
applyIsolation,
|
|
17
12
|
chownAgentDir,
|
|
@@ -21,10 +16,6 @@ import {
|
|
|
21
16
|
getAgentUserIds,
|
|
22
17
|
isIsolationEnabled
|
|
23
18
|
} from "./chunk-46S7YHUB.js";
|
|
24
|
-
import {
|
|
25
|
-
exec,
|
|
26
|
-
resolveVoluteBin
|
|
27
|
-
} from "./chunk-5C5JWR2L.js";
|
|
28
19
|
import {
|
|
29
20
|
findOpenClawSession,
|
|
30
21
|
importOpenClawConnectors,
|
|
@@ -44,6 +35,15 @@ import {
|
|
|
44
35
|
CHANNELS,
|
|
45
36
|
getChannelDriver
|
|
46
37
|
} from "./chunk-LIPPXNIE.js";
|
|
38
|
+
import {
|
|
39
|
+
exec,
|
|
40
|
+
resolveVoluteBin
|
|
41
|
+
} from "./chunk-5C5JWR2L.js";
|
|
42
|
+
import {
|
|
43
|
+
checkForUpdate,
|
|
44
|
+
checkForUpdateCached,
|
|
45
|
+
getCurrentVersion
|
|
46
|
+
} from "./chunk-RT6Y7AR3.js";
|
|
47
47
|
import "./chunk-D424ZQGI.js";
|
|
48
48
|
import {
|
|
49
49
|
slugify,
|
|
@@ -3501,7 +3501,7 @@ var app13 = new Hono13().post("/:name/chat", zValidator3("json", chatSchema), as
|
|
|
3501
3501
|
const participants = await getParticipants(conversationId);
|
|
3502
3502
|
const agentParticipants = participants.filter((p) => p.userType === "agent");
|
|
3503
3503
|
const participantNames = participants.map((p) => p.username);
|
|
3504
|
-
const { getAgentManager: getAgentManager2 } = await import("./agent-manager-
|
|
3504
|
+
const { getAgentManager: getAgentManager2 } = await import("./agent-manager-B6EPBU45.js");
|
|
3505
3505
|
const manager = getAgentManager2();
|
|
3506
3506
|
const runningAgents = agentParticipants.map((ap) => {
|
|
3507
3507
|
const agentKey = ap.username === baseName ? name : ap.username;
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import {
|
|
3
3
|
run,
|
|
4
4
|
stopDaemon
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
5
|
+
} from "./chunk-GGLTM53H.js";
|
|
6
|
+
import "./chunk-25GGN3OV.js";
|
|
7
|
+
import "./chunk-5C5JWR2L.js";
|
|
7
8
|
import "./chunk-DP2DX4WV.js";
|
|
8
|
-
import "./chunk-4RQBJWQX.js";
|
|
9
9
|
import "./chunk-K3NQKI34.js";
|
|
10
10
|
export {
|
|
11
11
|
run,
|
|
@@ -4,7 +4,7 @@ import "./chunk-K3NQKI34.js";
|
|
|
4
4
|
// package.json
|
|
5
5
|
var package_default = {
|
|
6
6
|
name: "volute",
|
|
7
|
-
version: "0.
|
|
7
|
+
version: "0.11.1",
|
|
8
8
|
description: "CLI for creating and managing self-modifying AI agents powered by the Claude Agent SDK",
|
|
9
9
|
type: "module",
|
|
10
10
|
license: "MIT",
|
|
@@ -20,7 +20,7 @@ var package_default = {
|
|
|
20
20
|
"anthropic"
|
|
21
21
|
],
|
|
22
22
|
engines: {
|
|
23
|
-
node: ">=
|
|
23
|
+
node: ">=24"
|
|
24
24
|
},
|
|
25
25
|
bin: {
|
|
26
26
|
volute: "dist/cli.js"
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
LAUNCHD_PLIST_LABEL,
|
|
4
|
+
LAUNCHD_PLIST_PATH,
|
|
5
|
+
SYSTEM_SERVICE_PATH,
|
|
6
|
+
USER_SYSTEMD_UNIT
|
|
7
|
+
} from "./chunk-25GGN3OV.js";
|
|
2
8
|
import {
|
|
3
9
|
resolveVoluteBin
|
|
4
10
|
} from "./chunk-5C5JWR2L.js";
|
|
5
11
|
import {
|
|
6
12
|
parseArgs
|
|
7
13
|
} from "./chunk-D424ZQGI.js";
|
|
14
|
+
import "./chunk-DP2DX4WV.js";
|
|
8
15
|
import "./chunk-K3NQKI34.js";
|
|
9
16
|
|
|
10
17
|
// src/commands/service.ts
|
|
@@ -15,7 +22,6 @@ import { resolve } from "path";
|
|
|
15
22
|
import { promisify } from "util";
|
|
16
23
|
var execFileAsync = promisify(execFile);
|
|
17
24
|
var HOST_RE = /^[a-zA-Z0-9.:_-]+$/;
|
|
18
|
-
var SYSTEM_SERVICE_PATH = "/etc/systemd/system/volute.service";
|
|
19
25
|
function validateHost(host) {
|
|
20
26
|
if (!HOST_RE.test(host)) {
|
|
21
27
|
throw new Error(`Invalid host: ${host}`);
|
|
@@ -24,8 +30,6 @@ function validateHost(host) {
|
|
|
24
30
|
function escapeXml(s) {
|
|
25
31
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
26
32
|
}
|
|
27
|
-
var PLIST_LABEL = "com.volute.daemon";
|
|
28
|
-
var plistPath = () => resolve(homedir(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
|
|
29
33
|
function generatePlist(voluteBin, port, host) {
|
|
30
34
|
const args = ["up", "--foreground"];
|
|
31
35
|
if (port != null) args.push("--port", String(port));
|
|
@@ -35,7 +39,7 @@ function generatePlist(voluteBin, port, host) {
|
|
|
35
39
|
<plist version="1.0">
|
|
36
40
|
<dict>
|
|
37
41
|
<key>Label</key>
|
|
38
|
-
<string>${
|
|
42
|
+
<string>${LAUNCHD_PLIST_LABEL}</string>
|
|
39
43
|
<key>ProgramArguments</key>
|
|
40
44
|
<array>
|
|
41
45
|
${[voluteBin, ...args].map((a) => `<string>${escapeXml(a)}</string>`).join("\n ")}
|
|
@@ -51,8 +55,6 @@ function generatePlist(voluteBin, port, host) {
|
|
|
51
55
|
</dict>
|
|
52
56
|
</plist>`;
|
|
53
57
|
}
|
|
54
|
-
var unitName = "volute.service";
|
|
55
|
-
var unitPath = () => resolve(homedir(), ".config", "systemd", "user", unitName);
|
|
56
58
|
function generateUnit(voluteBin, port, host) {
|
|
57
59
|
const args = ["up", "--foreground"];
|
|
58
60
|
if (port != null) args.push("--port", String(port));
|
|
@@ -76,7 +78,7 @@ async function install(port, host) {
|
|
|
76
78
|
const voluteBin = resolveVoluteBin();
|
|
77
79
|
const platform = process.platform;
|
|
78
80
|
if (platform === "darwin") {
|
|
79
|
-
const path =
|
|
81
|
+
const path = LAUNCHD_PLIST_PATH;
|
|
80
82
|
mkdirSync(resolve(homedir(), "Library", "LaunchAgents"), { recursive: true });
|
|
81
83
|
writeFileSync(path, generatePlist(voluteBin, port, host));
|
|
82
84
|
console.log(`Wrote ${path}`);
|
|
@@ -96,7 +98,7 @@ async function install(port, host) {
|
|
|
96
98
|
console.error("Use `volute setup` instead to install a system-level service.");
|
|
97
99
|
process.exit(1);
|
|
98
100
|
}
|
|
99
|
-
const path =
|
|
101
|
+
const path = USER_SYSTEMD_UNIT;
|
|
100
102
|
mkdirSync(resolve(homedir(), ".config", "systemd", "user"), { recursive: true });
|
|
101
103
|
writeFileSync(path, generateUnit(voluteBin, port, host));
|
|
102
104
|
console.log(`Wrote ${path}`);
|
|
@@ -110,7 +112,7 @@ async function install(port, host) {
|
|
|
110
112
|
async function uninstall() {
|
|
111
113
|
const platform = process.platform;
|
|
112
114
|
if (platform === "darwin") {
|
|
113
|
-
const path =
|
|
115
|
+
const path = LAUNCHD_PLIST_PATH;
|
|
114
116
|
if (existsSync(path)) {
|
|
115
117
|
try {
|
|
116
118
|
await execFileAsync("launchctl", ["unload", path]);
|
|
@@ -123,7 +125,7 @@ async function uninstall() {
|
|
|
123
125
|
console.log("Service not installed.");
|
|
124
126
|
}
|
|
125
127
|
} else if (platform === "linux") {
|
|
126
|
-
const path =
|
|
128
|
+
const path = USER_SYSTEMD_UNIT;
|
|
127
129
|
if (existsSync(path)) {
|
|
128
130
|
try {
|
|
129
131
|
await execFileAsync("systemctl", ["--user", "disable", "--now", "volute"]);
|
|
@@ -143,12 +145,12 @@ async function uninstall() {
|
|
|
143
145
|
async function status() {
|
|
144
146
|
const platform = process.platform;
|
|
145
147
|
if (platform === "darwin") {
|
|
146
|
-
if (!existsSync(
|
|
148
|
+
if (!existsSync(LAUNCHD_PLIST_PATH)) {
|
|
147
149
|
console.log("Service not installed.");
|
|
148
150
|
return;
|
|
149
151
|
}
|
|
150
152
|
try {
|
|
151
|
-
const { stdout } = await execFileAsync("launchctl", ["list",
|
|
153
|
+
const { stdout } = await execFileAsync("launchctl", ["list", LAUNCHD_PLIST_LABEL]);
|
|
152
154
|
console.log(stdout);
|
|
153
155
|
} catch {
|
|
154
156
|
console.log("Service installed but not currently loaded.");
|
|
@@ -171,7 +173,7 @@ async function status() {
|
|
|
171
173
|
}
|
|
172
174
|
return;
|
|
173
175
|
}
|
|
174
|
-
if (!existsSync(
|
|
176
|
+
if (!existsSync(USER_SYSTEMD_UNIT)) {
|
|
175
177
|
console.log("Service not installed.");
|
|
176
178
|
return;
|
|
177
179
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
SYSTEM_SERVICE_PATH
|
|
4
|
+
} from "./chunk-25GGN3OV.js";
|
|
2
5
|
import {
|
|
3
6
|
ensureVoluteGroup
|
|
4
7
|
} from "./chunk-46S7YHUB.js";
|
|
@@ -17,11 +20,11 @@ import { existsSync, mkdirSync, rmSync, unlinkSync, writeFileSync } from "fs";
|
|
|
17
20
|
import { homedir } from "os";
|
|
18
21
|
import { dirname } from "path";
|
|
19
22
|
var SERVICE_NAME = "volute.service";
|
|
20
|
-
var SERVICE_PATH = `/etc/systemd/system/${SERVICE_NAME}`;
|
|
21
23
|
var PROFILE_PATH = "/etc/profile.d/volute.sh";
|
|
22
24
|
var WRAPPER_PATH = "/usr/local/bin/volute";
|
|
23
25
|
var DATA_DIR = "/var/lib/volute";
|
|
24
26
|
var AGENTS_DIR = "/agents";
|
|
27
|
+
var CLAUDE_DIR = `${DATA_DIR}/.claude`;
|
|
25
28
|
var HOST_RE = /^[a-zA-Z0-9.:_-]+$/;
|
|
26
29
|
function validateHost(host) {
|
|
27
30
|
if (!HOST_RE.test(host)) {
|
|
@@ -59,6 +62,7 @@ function generateUnit(voluteBin, port, host) {
|
|
|
59
62
|
`Environment=VOLUTE_HOME=${DATA_DIR}`,
|
|
60
63
|
`Environment=VOLUTE_AGENTS_DIR=${AGENTS_DIR}`,
|
|
61
64
|
"Environment=VOLUTE_ISOLATION=user",
|
|
65
|
+
`Environment=CLAUDE_CONFIG_DIR=${CLAUDE_DIR}`,
|
|
62
66
|
"Restart=on-failure",
|
|
63
67
|
"RestartSec=5",
|
|
64
68
|
"ProtectSystem=true",
|
|
@@ -92,6 +96,10 @@ function install(port, host) {
|
|
|
92
96
|
console.log(`Created ${AGENTS_DIR}`);
|
|
93
97
|
ensureVoluteGroup({ force: true });
|
|
94
98
|
console.log("Ensured volute group exists");
|
|
99
|
+
mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
100
|
+
execFileSync("chown", ["root:volute", CLAUDE_DIR]);
|
|
101
|
+
execFileSync("chmod", ["750", CLAUDE_DIR]);
|
|
102
|
+
console.log(`Created ${CLAUDE_DIR}`);
|
|
95
103
|
execFileSync("chmod", ["755", DATA_DIR]);
|
|
96
104
|
execFileSync("chmod", ["755", AGENTS_DIR]);
|
|
97
105
|
console.log("Set permissions on directories");
|
|
@@ -113,13 +121,13 @@ exec "${voluteBin}" "$@"
|
|
|
113
121
|
writeFileSync(WRAPPER_PATH, wrapper, { mode: 493 });
|
|
114
122
|
console.log(`Wrote ${WRAPPER_PATH} (wrapper for ${voluteBin})`);
|
|
115
123
|
}
|
|
116
|
-
writeFileSync(
|
|
117
|
-
console.log(`Wrote ${
|
|
124
|
+
writeFileSync(SYSTEM_SERVICE_PATH, generateUnit(voluteBin, port, host ?? "0.0.0.0"));
|
|
125
|
+
console.log(`Wrote ${SYSTEM_SERVICE_PATH}`);
|
|
118
126
|
try {
|
|
119
127
|
execFileSync("systemctl", ["daemon-reload"]);
|
|
120
128
|
} catch (err) {
|
|
121
129
|
const e = err;
|
|
122
|
-
console.error(`Failed to reload systemd after writing ${
|
|
130
|
+
console.error(`Failed to reload systemd after writing ${SYSTEM_SERVICE_PATH}.`);
|
|
123
131
|
if (e.stderr) console.error(e.stderr.toString().trim());
|
|
124
132
|
console.error(
|
|
125
133
|
"Try running `systemctl daemon-reload` manually, then `systemctl enable --now volute`."
|
|
@@ -129,9 +137,16 @@ exec "${voluteBin}" "$@"
|
|
|
129
137
|
try {
|
|
130
138
|
execFileSync("systemctl", ["enable", "--now", SERVICE_NAME]);
|
|
131
139
|
console.log("Service installed, enabled, and started.");
|
|
140
|
+
console.log(
|
|
141
|
+
"Run `source /etc/profile.d/volute.sh` or start a new shell to use volute CLI commands."
|
|
142
|
+
);
|
|
132
143
|
console.log(`
|
|
133
144
|
Volute daemon is running. Data directory: ${DATA_DIR}`);
|
|
134
145
|
console.log("Use `systemctl status volute` to check status.");
|
|
146
|
+
console.log(
|
|
147
|
+
`
|
|
148
|
+
For agent-sdk agents, copy ~/.claude/.credentials.json to ${CLAUDE_DIR}/.credentials.json`
|
|
149
|
+
);
|
|
135
150
|
} catch (err) {
|
|
136
151
|
const e = err;
|
|
137
152
|
console.error("Service installed but failed to start.");
|
|
@@ -145,7 +160,7 @@ function uninstall(force) {
|
|
|
145
160
|
console.error("Error: volute setup uninstall must be run as root (use sudo).");
|
|
146
161
|
process.exit(1);
|
|
147
162
|
}
|
|
148
|
-
if (!existsSync(
|
|
163
|
+
if (!existsSync(SYSTEM_SERVICE_PATH)) {
|
|
149
164
|
console.log("Service not installed.");
|
|
150
165
|
return;
|
|
151
166
|
}
|
|
@@ -154,7 +169,7 @@ function uninstall(force) {
|
|
|
154
169
|
} catch {
|
|
155
170
|
console.warn("Warning: failed to disable service (may already be stopped)");
|
|
156
171
|
}
|
|
157
|
-
unlinkSync(
|
|
172
|
+
unlinkSync(SYSTEM_SERVICE_PATH);
|
|
158
173
|
if (existsSync(PROFILE_PATH)) unlinkSync(PROFILE_PATH);
|
|
159
174
|
if (existsSync(WRAPPER_PATH)) unlinkSync(WRAPPER_PATH);
|
|
160
175
|
try {
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
getDaemonUrl,
|
|
4
|
+
getServiceMode,
|
|
5
|
+
modeLabel,
|
|
6
|
+
readDaemonConfig
|
|
7
|
+
} from "./chunk-25GGN3OV.js";
|
|
8
|
+
import "./chunk-5C5JWR2L.js";
|
|
9
|
+
import {
|
|
10
|
+
checkForUpdate
|
|
11
|
+
} from "./chunk-RT6Y7AR3.js";
|
|
12
|
+
import "./chunk-DP2DX4WV.js";
|
|
13
|
+
import "./chunk-K3NQKI34.js";
|
|
14
|
+
|
|
15
|
+
// src/commands/status.ts
|
|
16
|
+
async function run(_args) {
|
|
17
|
+
const mode = getServiceMode();
|
|
18
|
+
console.log(`Mode: ${modeLabel(mode)}`);
|
|
19
|
+
const { hostname, port, token } = readDaemonConfig();
|
|
20
|
+
const baseUrl = getDaemonUrl(hostname, port);
|
|
21
|
+
let running = false;
|
|
22
|
+
let version;
|
|
23
|
+
try {
|
|
24
|
+
const res = await fetch(`${baseUrl}/api/health`);
|
|
25
|
+
if (res.ok) {
|
|
26
|
+
const body = await res.json();
|
|
27
|
+
if (body.ok) {
|
|
28
|
+
running = true;
|
|
29
|
+
version = body.version;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
}
|
|
34
|
+
if (!running) {
|
|
35
|
+
console.log("Status: not running");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
console.log(`Status: running on ${hostname}:${port}`);
|
|
39
|
+
if (version) console.log(`Version: ${version}`);
|
|
40
|
+
const update = await checkForUpdate();
|
|
41
|
+
if (update.updateAvailable) {
|
|
42
|
+
console.log(`Update available: ${update.current} \u2192 ${update.latest}`);
|
|
43
|
+
}
|
|
44
|
+
const headers = {};
|
|
45
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
46
|
+
headers.Origin = baseUrl;
|
|
47
|
+
try {
|
|
48
|
+
const res = await fetch(`${baseUrl}/api/agents`, { headers });
|
|
49
|
+
if (res.ok) {
|
|
50
|
+
const agents = await res.json();
|
|
51
|
+
if (agents.length > 0) {
|
|
52
|
+
console.log(`
|
|
53
|
+
Agents (${agents.length}):`);
|
|
54
|
+
for (const agent of agents) {
|
|
55
|
+
const status = agent.running ? "running" : "stopped";
|
|
56
|
+
console.log(` ${agent.name}: ${status}`);
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
console.log("\nNo agents configured.");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export {
|
|
66
|
+
run
|
|
67
|
+
};
|
|
@@ -1,30 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
getServiceMode,
|
|
4
|
+
modeLabel,
|
|
5
|
+
pollHealth,
|
|
6
|
+
readDaemonConfig,
|
|
7
|
+
restartService
|
|
8
|
+
} from "./chunk-25GGN3OV.js";
|
|
5
9
|
import {
|
|
10
|
+
exec,
|
|
6
11
|
execInherit,
|
|
7
12
|
resolveVoluteBin
|
|
8
13
|
} from "./chunk-5C5JWR2L.js";
|
|
14
|
+
import {
|
|
15
|
+
checkForUpdate
|
|
16
|
+
} from "./chunk-RT6Y7AR3.js";
|
|
9
17
|
import {
|
|
10
18
|
voluteHome
|
|
11
19
|
} from "./chunk-DP2DX4WV.js";
|
|
12
20
|
import "./chunk-K3NQKI34.js";
|
|
13
21
|
|
|
14
22
|
// src/commands/update.ts
|
|
15
|
-
import { execFileSync } from "child_process";
|
|
16
23
|
import { existsSync, readFileSync, unlinkSync } from "fs";
|
|
17
24
|
import { resolve } from "path";
|
|
18
25
|
async function run(_args) {
|
|
19
|
-
try {
|
|
20
|
-
execFileSync("systemctl", ["is-enabled", "--quiet", "volute"]);
|
|
21
|
-
console.error("Volute is managed by a systemd service.");
|
|
22
|
-
console.error("To update, run:");
|
|
23
|
-
console.error(" sudo npm install -g volute@latest");
|
|
24
|
-
console.error(" sudo systemctl restart volute");
|
|
25
|
-
process.exit(1);
|
|
26
|
-
} catch {
|
|
27
|
-
}
|
|
28
26
|
const result = await checkForUpdate();
|
|
29
27
|
if (result.checkFailed) {
|
|
30
28
|
console.error("Could not reach npm registry. Check your network connection and try again.");
|
|
@@ -38,6 +36,71 @@ async function run(_args) {
|
|
|
38
36
|
}
|
|
39
37
|
console.log(`
|
|
40
38
|
Updating volute ${result.current} \u2192 ${result.latest}...`);
|
|
39
|
+
const mode = getServiceMode();
|
|
40
|
+
if (mode === "system") {
|
|
41
|
+
let npmPath = "/usr/bin/npm";
|
|
42
|
+
if (!existsSync(npmPath)) {
|
|
43
|
+
try {
|
|
44
|
+
npmPath = (await exec("which", ["npm"])).trim();
|
|
45
|
+
} catch {
|
|
46
|
+
console.error("Could not find npm. Install npm and try again.");
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
await execInherit("sudo", [npmPath, "install", "-g", "volute@latest"]);
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.error(`
|
|
54
|
+
Update failed: ${err.message}`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
console.log("Restarting service...");
|
|
58
|
+
try {
|
|
59
|
+
await restartService(mode);
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error(`Failed to restart: ${err instanceof Error ? err.message : err}`);
|
|
62
|
+
console.error("Try: sudo systemctl restart volute");
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
{
|
|
66
|
+
const { hostname, port } = readDaemonConfig();
|
|
67
|
+
if (await pollHealth(hostname, port)) {
|
|
68
|
+
console.log(`
|
|
69
|
+
Updated to volute v${result.latest}`);
|
|
70
|
+
} else {
|
|
71
|
+
console.error("Service restarted but daemon did not become healthy.");
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (mode === "user-systemd" || mode === "user-launchd") {
|
|
78
|
+
try {
|
|
79
|
+
await execInherit("npm", ["install", "-g", "volute@latest"]);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
console.error(`
|
|
82
|
+
Update failed: ${err.message}`);
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
console.log(`Restarting service (${modeLabel(mode)})...`);
|
|
86
|
+
try {
|
|
87
|
+
await restartService(mode);
|
|
88
|
+
} catch (err) {
|
|
89
|
+
console.error(`Failed to restart: ${err instanceof Error ? err.message : err}`);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
{
|
|
93
|
+
const { hostname, port } = readDaemonConfig();
|
|
94
|
+
if (await pollHealth(hostname, port)) {
|
|
95
|
+
console.log(`
|
|
96
|
+
Updated to volute v${result.latest}`);
|
|
97
|
+
} else {
|
|
98
|
+
console.error("Service restarted but daemon did not become healthy.");
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
41
104
|
const home = voluteHome();
|
|
42
105
|
const pidPath = resolve(home, "daemon.pid");
|
|
43
106
|
const configPath = resolve(home, "daemon.json");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "volute",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "CLI for creating and managing self-modifying AI agents powered by the Claude Agent SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"anthropic"
|
|
17
17
|
],
|
|
18
18
|
"engines": {
|
|
19
|
-
"node": ">=
|
|
19
|
+
"node": ">=24"
|
|
20
20
|
},
|
|
21
21
|
"bin": {
|
|
22
22
|
"volute": "dist/cli.js"
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
run
|
|
4
|
-
} from "./chunk-M5AEQLB3.js";
|
|
5
|
-
import {
|
|
6
|
-
stopDaemon
|
|
7
|
-
} from "./chunk-FYQGANL6.js";
|
|
8
|
-
import "./chunk-D424ZQGI.js";
|
|
9
|
-
import {
|
|
10
|
-
daemonFetch
|
|
11
|
-
} from "./chunk-STOEJOJO.js";
|
|
12
|
-
import {
|
|
13
|
-
voluteHome
|
|
14
|
-
} from "./chunk-DP2DX4WV.js";
|
|
15
|
-
import {
|
|
16
|
-
getClient,
|
|
17
|
-
urlOf
|
|
18
|
-
} from "./chunk-4RQBJWQX.js";
|
|
19
|
-
import "./chunk-K3NQKI34.js";
|
|
20
|
-
|
|
21
|
-
// src/commands/daemon-restart.ts
|
|
22
|
-
import { readFileSync } from "fs";
|
|
23
|
-
import { resolve } from "path";
|
|
24
|
-
async function run2(args) {
|
|
25
|
-
const result = await stopDaemon();
|
|
26
|
-
if (!result.stopped && result.reason === "systemd") {
|
|
27
|
-
const client = getClient();
|
|
28
|
-
await daemonFetch(urlOf(client.api.system.restart.$url()), { method: "POST" });
|
|
29
|
-
const config = JSON.parse(readFileSync(resolve(voluteHome(), "daemon.json"), "utf-8"));
|
|
30
|
-
let hostname = config.hostname || "localhost";
|
|
31
|
-
if (hostname === "0.0.0.0") hostname = "127.0.0.1";
|
|
32
|
-
if (hostname === "::") hostname = "[::1]";
|
|
33
|
-
const url = new URL("http://localhost");
|
|
34
|
-
url.hostname = hostname;
|
|
35
|
-
url.port = String(config.port ?? 4200);
|
|
36
|
-
const healthUrl = `${url.origin}/api/health`;
|
|
37
|
-
const maxWait = 15e3;
|
|
38
|
-
const start = Date.now();
|
|
39
|
-
while (Date.now() - start < maxWait) {
|
|
40
|
-
try {
|
|
41
|
-
const res = await fetch(healthUrl);
|
|
42
|
-
if (res.ok) {
|
|
43
|
-
console.log("Daemon restarted.");
|
|
44
|
-
return;
|
|
45
|
-
}
|
|
46
|
-
} catch {
|
|
47
|
-
}
|
|
48
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
49
|
-
}
|
|
50
|
-
console.error("Daemon did not restart within 15s. Check logs.");
|
|
51
|
-
process.exit(1);
|
|
52
|
-
}
|
|
53
|
-
if (!result.stopped && result.reason === "kill-failed") {
|
|
54
|
-
console.error("Cannot restart: failed to stop the running daemon.");
|
|
55
|
-
process.exit(1);
|
|
56
|
-
}
|
|
57
|
-
await run(args);
|
|
58
|
-
}
|
|
59
|
-
export {
|
|
60
|
-
run2 as run
|
|
61
|
-
};
|
package/dist/status-SIMKH3ZE.js
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
daemonFetch
|
|
4
|
-
} from "./chunk-STOEJOJO.js";
|
|
5
|
-
import "./chunk-DP2DX4WV.js";
|
|
6
|
-
import {
|
|
7
|
-
getClient,
|
|
8
|
-
urlOf
|
|
9
|
-
} from "./chunk-4RQBJWQX.js";
|
|
10
|
-
import "./chunk-K3NQKI34.js";
|
|
11
|
-
|
|
12
|
-
// src/commands/status.ts
|
|
13
|
-
async function run(args) {
|
|
14
|
-
const name = args[0];
|
|
15
|
-
const client = getClient();
|
|
16
|
-
if (!name) {
|
|
17
|
-
const res2 = await daemonFetch(urlOf(client.api.agents.$url()));
|
|
18
|
-
if (!res2.ok) {
|
|
19
|
-
const data = await res2.json();
|
|
20
|
-
console.error(data.error ?? `Failed to get status: ${res2.status}`);
|
|
21
|
-
process.exit(1);
|
|
22
|
-
}
|
|
23
|
-
const agents = await res2.json();
|
|
24
|
-
if (agents.length === 0) {
|
|
25
|
-
console.log("No agents registered. Create one with: volute agent create <name>");
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
const nameW = Math.max(4, ...agents.map((a) => a.name.length));
|
|
29
|
-
const portW = Math.max(4, ...agents.map((a) => String(a.port).length));
|
|
30
|
-
console.log(`${"NAME".padEnd(nameW)} ${"PORT".padEnd(portW)} STATUS CONNECTORS`);
|
|
31
|
-
for (const agent2 of agents) {
|
|
32
|
-
const connected = agent2.channels.filter((ch) => ch.status === "connected").map((ch) => ch.name);
|
|
33
|
-
const connectors = connected.length > 0 ? connected.join(", ") : "-";
|
|
34
|
-
console.log(
|
|
35
|
-
`${agent2.name.padEnd(nameW)} ${String(agent2.port).padEnd(portW)} ${agent2.status.padEnd(8)} ${connectors}`
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
const res = await daemonFetch(urlOf(client.api.agents[":name"].$url({ param: { name } })));
|
|
41
|
-
if (!res.ok) {
|
|
42
|
-
const data = await res.json();
|
|
43
|
-
console.error(data.error || `Failed to get status for ${name}`);
|
|
44
|
-
process.exit(1);
|
|
45
|
-
}
|
|
46
|
-
const agent = await res.json();
|
|
47
|
-
console.log(`Agent: ${agent.name}`);
|
|
48
|
-
console.log(`Port: ${agent.port}`);
|
|
49
|
-
console.log(`Status: ${agent.status}`);
|
|
50
|
-
for (const ch of agent.channels) {
|
|
51
|
-
console.log(`${ch.name}: ${ch.status}`);
|
|
52
|
-
}
|
|
53
|
-
if (agent.variants && agent.variants.length > 0) {
|
|
54
|
-
console.log("");
|
|
55
|
-
console.log("Variants:");
|
|
56
|
-
for (const v of agent.variants) {
|
|
57
|
-
console.log(` ${v.name} port=${v.port} ${v.status}`);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
export {
|
|
62
|
-
run
|
|
63
|
-
};
|