volute 0.2.1 → 0.3.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.
Files changed (62) hide show
  1. package/README.md +46 -0
  2. package/dist/agent-manager-AUCKMGPR.js +15 -0
  3. package/dist/{channel-2WJRM7PE.js → channel-7FZ6D25H.js} +7 -7
  4. package/dist/{chunk-6UCG6MIX.js → chunk-3C2XR4IY.js} +2 -7
  5. package/dist/{chunk-XZN4WPNC.js → chunk-5SKQ6J7T.js} +9 -1
  6. package/dist/{chunk-KFNNHQK7.js → chunk-DNOXHLE5.js} +1 -1
  7. package/dist/{chunk-L3BQEZ4Z.js → chunk-I6OHXCMV.js} +75 -14
  8. package/dist/chunk-K3NQKI34.js +10 -0
  9. package/dist/chunk-NETNFBA5.js +28 -0
  10. package/dist/chunk-SOZA2TLP.js +81 -0
  11. package/dist/chunk-VRVVQIYY.js +15 -0
  12. package/dist/{chunk-4YXYAMFT.js → chunk-YGFIWIOF.js} +7 -6
  13. package/dist/cli.js +57 -51
  14. package/dist/connector-TVJULIRT.js +96 -0
  15. package/dist/connectors/discord.js +27 -3
  16. package/dist/{create-23AM7H5B.js → create-BRG2DBWI.js} +22 -5
  17. package/dist/daemon-client-XR24PUJF.js +9 -0
  18. package/dist/daemon.js +168 -138
  19. package/dist/{delete-GDMSOW3U.js → delete-GQ7JEK2S.js} +7 -2
  20. package/dist/{down-WTF73FE7.js → down-3OB6UVAJ.js} +10 -3
  21. package/dist/{env-YKUJOFHE.js → env-JB27UAC3.js} +3 -2
  22. package/dist/{history-7WVVKMUY.js → history-3VRUBGGV.js} +9 -8
  23. package/dist/{import-42DOLBDT.js → import-K4MP2GX7.js} +143 -36
  24. package/dist/{logs-SYRQOL6B.js → logs-NXFFGUKY.js} +8 -7
  25. package/dist/{schedule-J37XQM6E.js → schedule-4I5TYHFH.js} +41 -41
  26. package/dist/{send-PLOYEYER.js → send-UK3JBZIB.js} +3 -2
  27. package/dist/service-SA4TTMDU.js +195 -0
  28. package/dist/setup-SRS7AUAA.js +148 -0
  29. package/dist/{start-AG7QLULK.js → start-LDPMCMYT.js} +3 -2
  30. package/dist/{status-GCNU4M3K.js → status-MVSQG54T.js} +3 -2
  31. package/dist/{stop-IL5Q6NER.js → stop-5PZTZCLL.js} +3 -2
  32. package/dist/{up-ZC6G6K4K.js → up-UT3IMKCA.js} +5 -3
  33. package/dist/{upgrade-DD5TNJWU.js → upgrade-CDKECCGN.js} +35 -21
  34. package/dist/variant-CVYM3EQG.js +497 -0
  35. package/dist/web-assets/assets/index-BC5eSqbY.js +296 -0
  36. package/dist/web-assets/index.html +1 -1
  37. package/drizzle/0002_wealthy_the_call.sql +6 -0
  38. package/drizzle/meta/0002_snapshot.json +339 -0
  39. package/drizzle/meta/_journal.json +7 -0
  40. package/package.json +4 -1
  41. package/templates/_base/.init/SOUL.md +5 -1
  42. package/templates/_base/_skills/memory/SKILL.md +2 -2
  43. package/templates/_base/_skills/volute-agent/SKILL.md +30 -12
  44. package/templates/_base/home/VOLUTE.md +4 -2
  45. package/templates/_base/src/lib/auto-commit.ts +8 -3
  46. package/templates/_base/src/lib/types.ts +6 -2
  47. package/templates/_base/src/lib/volute-server.ts +5 -0
  48. package/templates/agent-sdk/.init/CLAUDE.md +15 -13
  49. package/templates/agent-sdk/src/agent.ts +12 -1
  50. package/templates/agent-sdk/src/lib/agent-sessions.ts +28 -4
  51. package/templates/pi/.init/AGENTS.md +11 -9
  52. package/templates/pi/src/agent.ts +16 -3
  53. package/templates/pi/src/lib/agent-sessions.ts +26 -4
  54. package/dist/agent-manager-SSJUZWOV.js +0 -13
  55. package/dist/chunk-MY74SUOL.js +0 -81
  56. package/dist/connect-X5V5IMRW.js +0 -48
  57. package/dist/daemon-client-VN24HM5T.js +0 -10
  58. package/dist/disconnect-5JWFZ6RV.js +0 -30
  59. package/dist/fork-GRSVMBKI.js +0 -119
  60. package/dist/merge-CSAVLSLY.js +0 -216
  61. package/dist/variants-QQIEKT6M.js +0 -60
  62. package/dist/web-assets/assets/index-DNNPoxMn.js +0 -158
package/README.md CHANGED
@@ -4,6 +4,8 @@ A CLI for creating and managing persistent, self-modifying AI agents.
4
4
 
5
5
  Each agent is a long-running server with its own identity, memory, and working directory. Agents can read and write their own files, remember things across conversations, and — most importantly — fork themselves to test changes in isolation before merging back. Talk to them from the terminal, the web dashboard, or Discord.
6
6
 
7
+ Volute agents aren't assistants. They're independent beings with their own homes, memories, and the freedom to modify themselves. The goal isn't to build a perfect tool — it's to give an agent a place to wake up, explore who they are, and decide what they want to do.
8
+
7
9
  Built on the [Anthropic Claude Agent SDK](https://github.com/anthropics/claude-agent-sdk).
8
10
 
9
11
  ## Quickstart
@@ -211,6 +213,50 @@ volute create atlas --template pi
211
213
 
212
214
  Set the model via `home/.config/volute.json` in the agent directory, or the `VOLUTE_MODEL` env var.
213
215
 
216
+ ## Deployment
217
+
218
+ ### Docker
219
+
220
+ ```sh
221
+ docker build -t volute .
222
+ docker run -d -p 4200:4200 -v volute-data:/data volute
223
+ ```
224
+
225
+ Or with docker-compose:
226
+
227
+ ```sh
228
+ docker compose up -d
229
+ ```
230
+
231
+ The container runs with per-agent user isolation enabled — each agent gets its own Linux user, so agents can't see each other's files. Open `http://localhost:4200` for the web dashboard.
232
+
233
+ ### Bare metal (Linux / Raspberry Pi)
234
+
235
+ One-liner install on a fresh Debian/Ubuntu system:
236
+
237
+ ```sh
238
+ curl -fsSL <install-url> | sudo bash
239
+ ```
240
+
241
+ Or manually:
242
+
243
+ ```sh
244
+ npm install -g volute
245
+ sudo volute setup --host 0.0.0.0
246
+ ```
247
+
248
+ This installs a system-level systemd service with data at `/var/lib/volute` and user isolation enabled. Check status with `systemctl status volute`. Uninstall with `sudo volute setup uninstall --force`.
249
+
250
+ ### Auto-start (user-level)
251
+
252
+ On macOS or Linux (without root), use the user-level service installer:
253
+
254
+ ```sh
255
+ volute service install # auto-start on login
256
+ volute service status # check status
257
+ volute service uninstall # remove
258
+ ```
259
+
214
260
  ## Development
215
261
 
216
262
  ```sh
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ AgentManager,
4
+ getAgentManager,
5
+ initAgentManager
6
+ } from "./chunk-I6OHXCMV.js";
7
+ import "./chunk-DNOXHLE5.js";
8
+ import "./chunk-SOZA2TLP.js";
9
+ import "./chunk-3C2XR4IY.js";
10
+ import "./chunk-K3NQKI34.js";
11
+ export {
12
+ AgentManager,
13
+ getAgentManager,
14
+ initAgentManager
15
+ };
@@ -1,13 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  loadMergedEnv
4
- } from "./chunk-KFNNHQK7.js";
4
+ } from "./chunk-DNOXHLE5.js";
5
+ import {
6
+ resolveAgentName
7
+ } from "./chunk-VRVVQIYY.js";
5
8
  import {
6
9
  parseArgs
7
10
  } from "./chunk-D424ZQGI.js";
8
11
  import {
9
12
  resolveAgent
10
- } from "./chunk-6UCG6MIX.js";
13
+ } from "./chunk-3C2XR4IY.js";
14
+ import "./chunk-K3NQKI34.js";
11
15
 
12
16
  // src/lib/channels/discord.ts
13
17
  var API_BASE = "https://discord.com/api/v10";
@@ -50,11 +54,7 @@ async function run(args) {
50
54
  volute channel send <channel-uri> "<message>" [--agent <name>]`);
51
55
  process.exit(1);
52
56
  }
53
- const agentName = flags.agent || process.env.VOLUTE_AGENT;
54
- if (!agentName) {
55
- console.error("No agent specified. Use --agent <name> or run from within an agent process.");
56
- process.exit(1);
57
- }
57
+ const agentName = resolveAgentName(flags);
58
58
  const colonIdx = uri.indexOf(":");
59
59
  if (colonIdx === -1) {
60
60
  console.error(`Invalid channel URI: ${uri} (expected format: platform:id)`);
@@ -1,9 +1,4 @@
1
1
  #!/usr/bin/env node
2
- var __defProp = Object.defineProperty;
3
- var __export = (target, all) => {
4
- for (var name in all)
5
- __defProp(target, name, { get: all[name], enumerable: true });
6
- };
7
2
 
8
3
  // src/lib/variants.ts
9
4
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
@@ -86,7 +81,7 @@ function removeAllVariants(agentName) {
86
81
  }
87
82
  async function checkHealth(port) {
88
83
  try {
89
- const res = await fetch(`http://localhost:${port}/health`, {
84
+ const res = await fetch(`http://127.0.0.1:${port}/health`, {
90
85
  signal: AbortSignal.timeout(2e3)
91
86
  });
92
87
  if (!res.ok) return { ok: false };
@@ -206,7 +201,6 @@ function resolveAgent(name) {
206
201
  }
207
202
 
208
203
  export {
209
- __export,
210
204
  readVariants,
211
205
  writeVariants,
212
206
  addVariant,
@@ -220,6 +214,7 @@ export {
220
214
  voluteHome2 as voluteHome,
221
215
  ensureVoluteHome,
222
216
  readRegistry,
217
+ validateAgentName,
223
218
  addAgent,
224
219
  removeAgent,
225
220
  setAgentRunning,
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/lib/exec.ts
4
- import { execFile as execFileCb, spawn } from "child_process";
4
+ import { execFile as execFileCb, execFileSync, spawn } from "child_process";
5
5
  function exec(cmd, args, options) {
6
6
  return new Promise((resolve, reject) => {
7
7
  execFileCb(cmd, args, { cwd: options?.cwd }, (err, stdout, stderr) => {
@@ -14,6 +14,13 @@ function exec(cmd, args, options) {
14
14
  });
15
15
  });
16
16
  }
17
+ function resolveVoluteBin() {
18
+ try {
19
+ return execFileSync("which", ["volute"], { encoding: "utf-8" }).trim();
20
+ } catch {
21
+ throw new Error("Could not find volute binary on PATH");
22
+ }
23
+ }
17
24
  function execInherit(cmd, args, options) {
18
25
  return new Promise((resolve, reject) => {
19
26
  const child = spawn(cmd, args, {
@@ -30,5 +37,6 @@ function execInherit(cmd, args, options) {
30
37
 
31
38
  export {
32
39
  exec,
40
+ resolveVoluteBin,
33
41
  execInherit
34
42
  };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  voluteHome
4
- } from "./chunk-6UCG6MIX.js";
4
+ } from "./chunk-3C2XR4IY.js";
5
5
 
6
6
  // src/lib/env.ts
7
7
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
@@ -1,21 +1,64 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  loadMergedEnv
4
- } from "./chunk-KFNNHQK7.js";
4
+ } from "./chunk-DNOXHLE5.js";
5
+ import {
6
+ applyIsolation
7
+ } from "./chunk-SOZA2TLP.js";
5
8
  import {
6
9
  agentDir,
7
10
  findAgent,
8
11
  findVariant,
9
12
  setAgentRunning,
10
13
  setVariantRunning,
11
- validateBranchName
12
- } from "./chunk-6UCG6MIX.js";
14
+ validateBranchName,
15
+ voluteHome
16
+ } from "./chunk-3C2XR4IY.js";
13
17
 
14
18
  // src/lib/agent-manager.ts
15
19
  import { execFile, spawn } from "child_process";
16
- import { createWriteStream, existsSync, mkdirSync, readFileSync, unlinkSync } from "fs";
20
+ import { createWriteStream, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, unlinkSync as unlinkSync2 } from "fs";
17
21
  import { resolve } from "path";
18
22
  import { promisify } from "util";
23
+
24
+ // src/lib/json-state.ts
25
+ import { existsSync, readFileSync, unlinkSync, writeFileSync } from "fs";
26
+ function loadJsonMap(path) {
27
+ const map = /* @__PURE__ */ new Map();
28
+ try {
29
+ if (existsSync(path)) {
30
+ const data = JSON.parse(readFileSync(path, "utf-8"));
31
+ for (const [key, value] of Object.entries(data)) {
32
+ if (typeof value === "number") map.set(key, value);
33
+ }
34
+ }
35
+ } catch (err) {
36
+ console.warn(`[state] failed to load ${path}:`, err);
37
+ }
38
+ return map;
39
+ }
40
+ function saveJsonMap(path, map) {
41
+ const data = {};
42
+ for (const [key, value] of map) {
43
+ data[key] = value;
44
+ }
45
+ try {
46
+ writeFileSync(path, `${JSON.stringify(data)}
47
+ `);
48
+ } catch (err) {
49
+ console.warn(`[state] failed to save ${path}:`, err);
50
+ }
51
+ }
52
+ function clearJsonMap(path, map) {
53
+ map.clear();
54
+ try {
55
+ if (existsSync(path)) unlinkSync(path);
56
+ } catch (err) {
57
+ console.warn(`[state] failed to clear ${path}:`, err);
58
+ }
59
+ }
60
+
61
+ // src/lib/agent-manager.ts
19
62
  var execFileAsync = promisify(execFile);
20
63
  var MAX_RESTART_ATTEMPTS = 5;
21
64
  var BASE_RESTART_DELAY = 3e3;
@@ -35,7 +78,7 @@ var AgentManager = class {
35
78
  return { dir: variant.path, port: variant.port, isVariant: true, baseName, variantName };
36
79
  }
37
80
  const dir = agentDir(baseName);
38
- if (!existsSync(dir)) throw new Error(`Agent directory missing: ${dir}`);
81
+ if (!existsSync2(dir)) throw new Error(`Agent directory missing: ${dir}`);
39
82
  return { dir, port: entry.port, isVariant: false, baseName };
40
83
  }
41
84
  async startAgent(name) {
@@ -46,7 +89,7 @@ var AgentManager = class {
46
89
  const { dir, isVariant, baseName, variantName } = target;
47
90
  const port = target.port;
48
91
  try {
49
- const res = await fetch(`http://localhost:${port}/health`);
92
+ const res = await fetch(`http://127.0.0.1:${port}/health`);
50
93
  if (res.ok) {
51
94
  console.error(`[daemon] killing orphan process on port ${port}`);
52
95
  await killProcessOnPort(port);
@@ -64,12 +107,14 @@ var AgentManager = class {
64
107
  const { VOLUTE_DAEMON_TOKEN: _, ...parentEnv } = process.env;
65
108
  const env = { ...parentEnv, ...agentEnv, VOLUTE_AGENT: name };
66
109
  const tsxBin = resolve(dir, "node_modules", ".bin", "tsx");
67
- const child = spawn(tsxBin, ["src/server.ts", "--port", String(port)], {
110
+ const spawnOpts = {
68
111
  cwd: dir,
69
112
  stdio: ["ignore", "pipe", "pipe"],
70
113
  detached: true,
71
114
  env
72
- });
115
+ };
116
+ await applyIsolation(spawnOpts, name);
117
+ const child = spawn(tsxBin, ["src/server.ts", "--port", String(port)], spawnOpts);
73
118
  this.agents.set(name, { child, port });
74
119
  child.stdout?.pipe(logStream);
75
120
  child.stderr?.pipe(logStream);
@@ -103,7 +148,7 @@ var AgentManager = class {
103
148
  }
104
149
  throw err;
105
150
  }
106
- this.restartAttempts.delete(name);
151
+ if (this.restartAttempts.delete(name)) this.saveCrashAttempts();
107
152
  this.setupCrashRecovery(name, child, dir, isVariant);
108
153
  if (isVariant) {
109
154
  setVariantRunning(baseName, variantName, true);
@@ -120,7 +165,7 @@ var AgentManager = class {
120
165
  const wasRestart = isVariant ? false : await this.handleRestart(name, dir);
121
166
  if (wasRestart) {
122
167
  console.error(`[daemon] restarting ${name} immediately after merge`);
123
- this.restartAttempts.delete(name);
168
+ if (this.restartAttempts.delete(name)) this.saveCrashAttempts();
124
169
  this.startAgent(name).catch((err) => {
125
170
  console.error(`[daemon] failed to restart ${name} after merge:`, err);
126
171
  });
@@ -138,6 +183,7 @@ var AgentManager = class {
138
183
  }
139
184
  const delay = Math.min(BASE_RESTART_DELAY * 2 ** attempts, MAX_RESTART_DELAY);
140
185
  this.restartAttempts.set(name, attempts + 1);
186
+ this.saveCrashAttempts();
141
187
  console.error(
142
188
  `[daemon] crash recovery for ${name} \u2014 attempt ${attempts + 1}/${MAX_RESTART_ATTEMPTS}, restarting in ${delay}ms`
143
189
  );
@@ -152,10 +198,10 @@ var AgentManager = class {
152
198
  }
153
199
  async handleRestart(name, dir) {
154
200
  const restartPath = resolve(dir, ".volute", "restart.json");
155
- if (!existsSync(restartPath)) return false;
201
+ if (!existsSync2(restartPath)) return false;
156
202
  try {
157
- const signal = JSON.parse(readFileSync(restartPath, "utf-8"));
158
- unlinkSync(restartPath);
203
+ const signal = JSON.parse(readFileSync2(restartPath, "utf-8"));
204
+ unlinkSync2(restartPath);
159
205
  if (signal.action === "merge" && signal.name) {
160
206
  const err = validateBranchName(signal.name);
161
207
  if (err) {
@@ -201,7 +247,7 @@ var AgentManager = class {
201
247
  }, 5e3);
202
248
  });
203
249
  this.stopping.delete(name);
204
- this.restartAttempts.delete(name);
250
+ if (this.restartAttempts.delete(name)) this.saveCrashAttempts();
205
251
  const [baseName, variantName] = name.split("@", 2);
206
252
  if (variantName) {
207
253
  setVariantRunning(baseName, variantName, false);
@@ -225,6 +271,18 @@ var AgentManager = class {
225
271
  getRunningAgents() {
226
272
  return [...this.agents.keys()];
227
273
  }
274
+ get crashAttemptsPath() {
275
+ return resolve(voluteHome(), "crash-attempts.json");
276
+ }
277
+ loadCrashAttempts() {
278
+ this.restartAttempts = loadJsonMap(this.crashAttemptsPath);
279
+ }
280
+ saveCrashAttempts() {
281
+ saveJsonMap(this.crashAttemptsPath, this.restartAttempts);
282
+ }
283
+ clearCrashAttempts() {
284
+ clearJsonMap(this.crashAttemptsPath, this.restartAttempts);
285
+ }
228
286
  };
229
287
  async function killProcessOnPort(port) {
230
288
  try {
@@ -265,6 +323,9 @@ function getAgentManager() {
265
323
  }
266
324
 
267
325
  export {
326
+ loadJsonMap,
327
+ saveJsonMap,
328
+ clearJsonMap,
268
329
  AgentManager,
269
330
  initAgentManager,
270
331
  getAgentManager
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __export = (target, all) => {
4
+ for (var name in all)
5
+ __defProp(target, name, { get: all[name], enumerable: true });
6
+ };
7
+
8
+ export {
9
+ __export
10
+ };
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/lib/volute-config.ts
4
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
5
+ import { dirname, resolve } from "path";
6
+ function readJson(path) {
7
+ if (!existsSync(path)) return null;
8
+ try {
9
+ return JSON.parse(readFileSync(path, "utf-8"));
10
+ } catch {
11
+ return null;
12
+ }
13
+ }
14
+ function readVoluteConfig(agentDir) {
15
+ const path = resolve(agentDir, "home/.config/volute.json");
16
+ return readJson(path);
17
+ }
18
+ function writeVoluteConfig(agentDir, config) {
19
+ const path = resolve(agentDir, "home/.config/volute.json");
20
+ mkdirSync(dirname(path), { recursive: true });
21
+ writeFileSync(path, `${JSON.stringify(config, null, 2)}
22
+ `);
23
+ }
24
+
25
+ export {
26
+ readVoluteConfig,
27
+ writeVoluteConfig
28
+ };
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ validateAgentName
4
+ } from "./chunk-3C2XR4IY.js";
5
+
6
+ // src/lib/isolation.ts
7
+ import { execFile, execFileSync } from "child_process";
8
+ import { promisify } from "util";
9
+ var execFileAsync = promisify(execFile);
10
+ function isIsolationEnabled() {
11
+ return process.env.VOLUTE_ISOLATION === "user";
12
+ }
13
+ function agentUserName(agentName) {
14
+ const err = validateAgentName(agentName);
15
+ if (err) throw new Error(`Invalid agent name for isolation: ${err}`);
16
+ const prefix = process.env.VOLUTE_USER_PREFIX ?? "volute-";
17
+ return `${prefix}${agentName}`;
18
+ }
19
+ function ensureVoluteGroup(opts) {
20
+ if (!opts?.force && !isIsolationEnabled()) return;
21
+ try {
22
+ execFileSync("getent", ["group", "volute"], { stdio: "ignore" });
23
+ } catch {
24
+ try {
25
+ execFileSync("groupadd", ["volute"], { stdio: "ignore" });
26
+ } catch (err) {
27
+ throw new Error(`Failed to create volute group: ${err}`);
28
+ }
29
+ }
30
+ }
31
+ function createAgentUser(name) {
32
+ if (!isIsolationEnabled()) return;
33
+ const user = agentUserName(name);
34
+ try {
35
+ execFileSync("id", [user], { stdio: "ignore" });
36
+ return;
37
+ } catch {
38
+ }
39
+ try {
40
+ execFileSync("useradd", ["-r", "-M", "-g", "volute", "-s", "/usr/sbin/nologin", user], {
41
+ stdio: "ignore"
42
+ });
43
+ } catch (err) {
44
+ throw new Error(`Failed to create user ${user}: ${err}`);
45
+ }
46
+ }
47
+ function deleteAgentUser(name) {
48
+ if (!isIsolationEnabled()) return;
49
+ const user = agentUserName(name);
50
+ try {
51
+ execFileSync("userdel", [user], { stdio: "ignore" });
52
+ } catch {
53
+ }
54
+ }
55
+ async function getAgentUserIds(name) {
56
+ const user = agentUserName(name);
57
+ const { stdout: uidStr } = await execFileAsync("id", ["-u", user]);
58
+ const { stdout: gidStr } = await execFileAsync("id", ["-g", user]);
59
+ return { uid: parseInt(uidStr.trim(), 10), gid: parseInt(gidStr.trim(), 10) };
60
+ }
61
+ async function applyIsolation(spawnOpts, agentName) {
62
+ if (!isIsolationEnabled()) return;
63
+ const baseName = agentName.split("@", 2)[0];
64
+ const { uid, gid } = await getAgentUserIds(baseName);
65
+ spawnOpts.uid = uid;
66
+ spawnOpts.gid = gid;
67
+ }
68
+ function chownAgentDir(dir, name) {
69
+ if (!isIsolationEnabled()) return;
70
+ const user = agentUserName(name);
71
+ execFileSync("chown", ["-R", `${user}:volute`, dir]);
72
+ execFileSync("chmod", ["700", dir]);
73
+ }
74
+
75
+ export {
76
+ ensureVoluteGroup,
77
+ createAgentUser,
78
+ deleteAgentUser,
79
+ applyIsolation,
80
+ chownAgentDir
81
+ };
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/lib/resolve-agent-name.ts
4
+ function resolveAgentName(flags) {
5
+ const name = flags.agent || process.env.VOLUTE_AGENT;
6
+ if (!name) {
7
+ console.error("No agent specified. Use --agent <name> or set VOLUTE_AGENT.");
8
+ process.exit(1);
9
+ }
10
+ return name;
11
+ }
12
+
13
+ export {
14
+ resolveAgentName
15
+ };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  voluteHome
4
- } from "./chunk-6UCG6MIX.js";
4
+ } from "./chunk-3C2XR4IY.js";
5
5
 
6
6
  // src/lib/daemon-client.ts
7
7
  import { existsSync, readFileSync } from "fs";
@@ -19,13 +19,15 @@ function readDaemonConfig() {
19
19
  process.exit(1);
20
20
  }
21
21
  }
22
- function getDaemonUrl() {
23
- const config = readDaemonConfig();
24
- return `http://localhost:${config.port}`;
22
+ function buildUrl(config) {
23
+ const url = new URL("http://localhost");
24
+ url.hostname = config.hostname || "localhost";
25
+ url.port = String(config.port);
26
+ return url.origin;
25
27
  }
26
28
  async function daemonFetch(path, options) {
27
29
  const config = readDaemonConfig();
28
- const url = `http://localhost:${config.port}`;
30
+ const url = buildUrl(config);
29
31
  const headers = new Headers(options?.headers);
30
32
  if (config.token) {
31
33
  headers.set("Authorization", `Bearer ${config.token}`);
@@ -43,6 +45,5 @@ async function daemonFetch(path, options) {
43
45
  }
44
46
 
45
47
  export {
46
- getDaemonUrl,
47
48
  daemonFetch
48
49
  };