volute 0.7.0 → 0.8.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 (77) hide show
  1. package/README.md +16 -14
  2. package/dist/{agent-7JF7MT73.js → agent-YORVRB6I.js} +10 -10
  3. package/dist/{agent-manager-IMZ7ZMBF.js → agent-manager-CMMH5KQQ.js} +4 -4
  4. package/dist/{channel-SMCNOIVQ.js → channel-RDGHBFSI.js} +16 -56
  5. package/dist/{chunk-JR4UXCTO.js → chunk-23L3MKEV.js} +1 -1
  6. package/dist/{chunk-5SKQ6J7T.js → chunk-5C5JWR2L.js} +15 -7
  7. package/dist/{chunk-UWHWAPGO.js → chunk-DP2DX4WV.js} +9 -1
  8. package/dist/{chunk-7ACDT3P2.js → chunk-ECPQXRLB.js} +1 -2
  9. package/dist/{chunk-LLJNZPCU.js → chunk-HZ5LTOEJ.js} +1 -1
  10. package/dist/{chunk-W76KWE23.js → chunk-IQXBMFZG.js} +6 -4
  11. package/dist/{chunk-ZZOOTYXK.js → chunk-LIPPXNIE.js} +60 -74
  12. package/dist/{chunk-BX7KI4S3.js → chunk-N6MLQ26B.js} +23 -96
  13. package/dist/{chunk-H7AMDUIA.js → chunk-QF22MYDJ.js} +6 -5
  14. package/dist/{chunk-NKXULRSW.js → chunk-RT6Y7AR3.js} +1 -1
  15. package/dist/{chunk-62X577Y7.js → chunk-W6TMWYU3.js} +126 -73
  16. package/dist/{chunk-EG45HBSJ.js → chunk-XSJ27WEM.js} +1 -1
  17. package/dist/cli.js +22 -20
  18. package/dist/{connector-Y7JPNROO.js → connector-ZP6MEFF4.js} +3 -3
  19. package/dist/connectors/discord.js +18 -59
  20. package/dist/connectors/slack.js +21 -38
  21. package/dist/connectors/telegram.js +31 -49
  22. package/dist/{create-G525LWEA.js → create-HGJHLABX.js} +22 -17
  23. package/dist/{daemon-client-442IV43D.js → daemon-client-54J3EIZD.js} +2 -2
  24. package/dist/{daemon-restart-4HVEKYFY.js → daemon-restart-CPBLMMRI.js} +3 -3
  25. package/dist/daemon.js +342 -402
  26. package/dist/{delete-UOU4AFQN.js → delete-45TGQC4N.js} +10 -5
  27. package/dist/{down-AZVH5TCD.js → down-O4EWZTVA.js} +2 -2
  28. package/dist/{env-7GLUJCWS.js → env-KMNYGVZ2.js} +7 -9
  29. package/dist/{history-H72ZUIBN.js → history-PXJVYLVY.js} +2 -2
  30. package/dist/{import-AVKQJDYC.js → import-CNEDF3TD.js} +6 -6
  31. package/dist/{logs-EDGK26AK.js → logs-TZB3MTLZ.js} +5 -4
  32. package/dist/{package-T2WAVJOU.js → package-RJSONENE.js} +1 -1
  33. package/dist/{restart-O4ETYLJF.js → restart-KVH3TK5N.js} +2 -2
  34. package/dist/{schedule-S6QVC5ON.js → schedule-HCUCBNQI.js} +2 -2
  35. package/dist/send-BNC2S5BY.js +162 -0
  36. package/dist/{service-HZNIDNJF.js → service-XCADRKIS.js} +8 -1
  37. package/dist/{setup-F4TCWVSP.js → setup-32KH5KLN.js} +85 -26
  38. package/dist/{start-VHQ7LNWM.js → start-QU73YTJW.js} +2 -2
  39. package/dist/{status-QAJWXKMZ.js → status-Q6ZQJXNI.js} +2 -2
  40. package/dist/{stop-CAGCT5NI.js → stop-N7U5N6A7.js} +2 -2
  41. package/dist/{up-RWZF6MLT.js → up-V6EAA7OZ.js} +2 -2
  42. package/dist/{update-F7QWV2LB.js → update-EUCZ7XGG.js} +3 -3
  43. package/dist/{update-check-B4J6IEQ4.js → update-check-SM4244SU.js} +2 -2
  44. package/dist/{upgrade-YXKPWDRU.js → upgrade-CZF6PN7Y.js} +4 -4
  45. package/dist/{variant-4Z6W3PP6.js → variant-RKXPN5DH.js} +20 -46
  46. package/dist/web-assets/assets/index-D-3zx6vs.js +307 -0
  47. package/dist/web-assets/index.html +1 -1
  48. package/drizzle/0004_magical_silverclaw.sql +1 -0
  49. package/drizzle/meta/0004_snapshot.json +410 -0
  50. package/drizzle/meta/_journal.json +7 -0
  51. package/package.json +1 -1
  52. package/templates/_base/_skills/volute-agent/SKILL.md +32 -16
  53. package/templates/_base/home/.config/routes.json +4 -8
  54. package/templates/_base/home/VOLUTE.md +16 -14
  55. package/templates/_base/src/lib/auto-reply.ts +38 -0
  56. package/templates/_base/src/lib/daemon-client.ts +53 -0
  57. package/templates/_base/src/lib/router.ts +66 -14
  58. package/templates/_base/src/lib/routing.ts +48 -9
  59. package/templates/_base/src/lib/startup.ts +1 -25
  60. package/templates/_base/src/lib/types.ts +2 -1
  61. package/templates/_base/src/lib/volute-server.ts +29 -14
  62. package/templates/agent-sdk/src/agent.ts +53 -111
  63. package/templates/agent-sdk/src/lib/content.ts +41 -0
  64. package/templates/agent-sdk/src/lib/session-store.ts +43 -0
  65. package/templates/agent-sdk/src/lib/stream-consumer.ts +66 -0
  66. package/templates/agent-sdk/src/server.ts +5 -13
  67. package/templates/pi/.init/AGENTS.md +5 -5
  68. package/templates/pi/src/agent.ts +32 -84
  69. package/templates/pi/src/lib/content.ts +15 -0
  70. package/templates/pi/src/lib/event-handler.ts +74 -0
  71. package/templates/pi/src/lib/resolve-model.ts +21 -0
  72. package/templates/pi/src/server.ts +3 -7
  73. package/dist/chunk-B3R6L2GW.js +0 -24
  74. package/dist/chunk-ZYGKG6VC.js +0 -22
  75. package/dist/message-SCOQDR3P.js +0 -32
  76. package/dist/send-G7PE4DOJ.js +0 -72
  77. package/dist/web-assets/assets/index-B1CqjUYD.js +0 -308
@@ -4,19 +4,20 @@ import {
4
4
  } from "./chunk-AZEL2IEK.js";
5
5
  import {
6
6
  deleteAgentUser as deleteAgentUser2
7
- } from "./chunk-7ACDT3P2.js";
7
+ } from "./chunk-ECPQXRLB.js";
8
8
  import {
9
9
  parseArgs
10
10
  } from "./chunk-D424ZQGI.js";
11
11
  import {
12
12
  deleteAgentUser
13
- } from "./chunk-W76KWE23.js";
13
+ } from "./chunk-IQXBMFZG.js";
14
14
  import {
15
15
  agentDir,
16
16
  findAgent,
17
17
  removeAgent,
18
- removeAllVariants
19
- } from "./chunk-UWHWAPGO.js";
18
+ removeAllVariants,
19
+ stateDir
20
+ } from "./chunk-DP2DX4WV.js";
20
21
  import "./chunk-K3NQKI34.js";
21
22
 
22
23
  // src/commands/delete.ts
@@ -32,7 +33,7 @@ async function run(args) {
32
33
  process.exit(1);
33
34
  }
34
35
  try {
35
- const { daemonFetch } = await import("./daemon-client-442IV43D.js");
36
+ const { daemonFetch } = await import("./daemon-client-54J3EIZD.js");
36
37
  const res = await daemonFetch(`/api/agents/${encodeURIComponent(name)}/stop`, {
37
38
  method: "POST"
38
39
  });
@@ -46,6 +47,10 @@ async function run(args) {
46
47
  removeAgent(name);
47
48
  await deleteAgentUser2(name);
48
49
  console.log(`Removed ${name} from registry.`);
50
+ const state = stateDir(name);
51
+ if (existsSync(state)) {
52
+ rmSync(state, { recursive: true, force: true });
53
+ }
49
54
  if (existsSync(dir)) {
50
55
  if (!flags.force) {
51
56
  console.log(`Directory: ${dir}`);
@@ -2,8 +2,8 @@
2
2
  import {
3
3
  run,
4
4
  stopDaemon
5
- } from "./chunk-LLJNZPCU.js";
6
- import "./chunk-UWHWAPGO.js";
5
+ } from "./chunk-HZ5LTOEJ.js";
6
+ import "./chunk-DP2DX4WV.js";
7
7
  import "./chunk-K3NQKI34.js";
8
8
  export {
9
9
  run,
@@ -5,20 +5,20 @@ import {
5
5
  readEnv,
6
6
  sharedEnvPath,
7
7
  writeEnv
8
- } from "./chunk-H7AMDUIA.js";
8
+ } from "./chunk-QF22MYDJ.js";
9
9
  import {
10
10
  parseArgs
11
11
  } from "./chunk-D424ZQGI.js";
12
12
  import {
13
- resolveAgent
14
- } from "./chunk-UWHWAPGO.js";
13
+ findAgent
14
+ } from "./chunk-DP2DX4WV.js";
15
15
  import "./chunk-K3NQKI34.js";
16
16
 
17
17
  // src/commands/env.ts
18
18
  function getEnvPath(agentName) {
19
19
  if (agentName) {
20
- const { dir } = resolveAgent(agentName);
21
- return agentEnvPath(dir);
20
+ if (!findAgent(agentName)) throw new Error(`Unknown agent: ${agentName}`);
21
+ return agentEnvPath(agentName);
22
22
  }
23
23
  return sharedEnvPath();
24
24
  }
@@ -85,8 +85,7 @@ async function run(args) {
85
85
  process.exit(1);
86
86
  }
87
87
  if (flags.agent) {
88
- const { dir } = resolveAgent(flags.agent);
89
- const merged = loadMergedEnv(dir);
88
+ const merged = loadMergedEnv(flags.agent);
90
89
  if (key in merged) {
91
90
  console.log(merged[key]);
92
91
  } else {
@@ -106,9 +105,8 @@ async function run(args) {
106
105
  }
107
106
  case "list": {
108
107
  if (flags.agent) {
109
- const { dir } = resolveAgent(flags.agent);
110
108
  const shared = readEnv(sharedEnvPath());
111
- const agent = readEnv(agentEnvPath(dir));
109
+ const agent = readEnv(agentEnvPath(flags.agent));
112
110
  const allKeys = /* @__PURE__ */ new Set([...Object.keys(shared), ...Object.keys(agent)]);
113
111
  if (allKeys.size === 0) {
114
112
  console.log("No environment variables set.");
@@ -7,8 +7,8 @@ import {
7
7
  } from "./chunk-D424ZQGI.js";
8
8
  import {
9
9
  daemonFetch
10
- } from "./chunk-JR4UXCTO.js";
11
- import "./chunk-UWHWAPGO.js";
10
+ } from "./chunk-23L3MKEV.js";
11
+ import "./chunk-DP2DX4WV.js";
12
12
  import "./chunk-K3NQKI34.js";
13
13
 
14
14
  // src/commands/history.ts
@@ -7,7 +7,7 @@ import {
7
7
  agentEnvPath,
8
8
  readEnv,
9
9
  writeEnv
10
- } from "./chunk-H7AMDUIA.js";
10
+ } from "./chunk-QF22MYDJ.js";
11
11
  import {
12
12
  composeTemplate,
13
13
  copyTemplateToDir,
@@ -19,13 +19,13 @@ import {
19
19
  import {
20
20
  exec,
21
21
  execInherit
22
- } from "./chunk-5SKQ6J7T.js";
22
+ } from "./chunk-5C5JWR2L.js";
23
23
  import {
24
24
  addAgent,
25
25
  agentDir,
26
26
  ensureVoluteHome,
27
27
  nextPort
28
- } from "./chunk-UWHWAPGO.js";
28
+ } from "./chunk-DP2DX4WV.js";
29
29
  import "./chunk-K3NQKI34.js";
30
30
 
31
31
  // src/commands/import.ts
@@ -390,7 +390,7 @@ ${user.trimEnd()}
390
390
  console.warn(`Session import not supported for template: ${template}`);
391
391
  }
392
392
  }
393
- importOpenClawConnectors(dest);
393
+ importOpenClawConnectors(name, dest);
394
394
  console.log(`
395
395
  Imported agent: ${name} (port ${port})`);
396
396
  console.log(`
@@ -475,7 +475,7 @@ function importPiSession(sessionFile, agentDirPath) {
475
475
  `);
476
476
  console.log(`Imported session (${lines.length} entries)`);
477
477
  }
478
- function importOpenClawConnectors(agentDirPath) {
478
+ function importOpenClawConnectors(agentName, agentDirPath) {
479
479
  const configPath = resolve3(homedir2(), ".openclaw/openclaw.json");
480
480
  if (!existsSync(configPath)) return;
481
481
  let config;
@@ -487,7 +487,7 @@ function importOpenClawConnectors(agentDirPath) {
487
487
  }
488
488
  const discord = config.channels?.discord;
489
489
  if (!discord?.enabled || !discord.token) return;
490
- const envPath = agentEnvPath(agentDirPath);
490
+ const envPath = agentEnvPath(agentName);
491
491
  const env = readEnv(envPath);
492
492
  env.DISCORD_TOKEN = discord.token;
493
493
  writeEnv(envPath, env);
@@ -6,8 +6,9 @@ import {
6
6
  parseArgs
7
7
  } from "./chunk-D424ZQGI.js";
8
8
  import {
9
- resolveAgent
10
- } from "./chunk-UWHWAPGO.js";
9
+ resolveAgent,
10
+ stateDir
11
+ } from "./chunk-DP2DX4WV.js";
11
12
  import "./chunk-K3NQKI34.js";
12
13
 
13
14
  // src/commands/logs.ts
@@ -21,8 +22,8 @@ async function run(args) {
21
22
  n: { type: "number" }
22
23
  });
23
24
  const name = resolveAgentName(flags);
24
- const { dir } = resolveAgent(name);
25
- const logFile = resolve(dir, ".volute", "logs", "agent.log");
25
+ resolveAgent(name);
26
+ const logFile = resolve(stateDir(name), "logs", "agent.log");
26
27
  if (!existsSync(logFile)) {
27
28
  console.error(`No log file found. Has ${name} been started?`);
28
29
  process.exit(1);
@@ -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.0",
7
+ version: "0.8.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",
@@ -4,10 +4,10 @@ import {
4
4
  } from "./chunk-AZEL2IEK.js";
5
5
  import {
6
6
  daemonFetch
7
- } from "./chunk-JR4UXCTO.js";
7
+ } from "./chunk-23L3MKEV.js";
8
8
  import {
9
9
  resolveAgent
10
- } from "./chunk-UWHWAPGO.js";
10
+ } from "./chunk-DP2DX4WV.js";
11
11
  import "./chunk-K3NQKI34.js";
12
12
 
13
13
  // src/commands/restart.ts
@@ -7,8 +7,8 @@ import {
7
7
  } from "./chunk-D424ZQGI.js";
8
8
  import {
9
9
  daemonFetch
10
- } from "./chunk-JR4UXCTO.js";
11
- import "./chunk-UWHWAPGO.js";
10
+ } from "./chunk-23L3MKEV.js";
11
+ import "./chunk-DP2DX4WV.js";
12
12
  import "./chunk-K3NQKI34.js";
13
13
 
14
14
  // src/commands/schedule.ts
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ resolveAgentName
4
+ } from "./chunk-AZEL2IEK.js";
5
+ import {
6
+ getChannelDriver
7
+ } from "./chunk-LIPPXNIE.js";
8
+ import {
9
+ loadMergedEnv
10
+ } from "./chunk-QF22MYDJ.js";
11
+ import "./chunk-N6MLQ26B.js";
12
+ import {
13
+ parseArgs
14
+ } from "./chunk-D424ZQGI.js";
15
+ import {
16
+ daemonFetch
17
+ } from "./chunk-23L3MKEV.js";
18
+ import {
19
+ resolveAgent
20
+ } from "./chunk-DP2DX4WV.js";
21
+ import "./chunk-K3NQKI34.js";
22
+
23
+ // src/commands/send.ts
24
+ import { userInfo } from "os";
25
+
26
+ // src/lib/parse-target.ts
27
+ function parseTarget(target) {
28
+ const colonIdx = target.indexOf(":");
29
+ if (colonIdx !== -1) {
30
+ const platform = target.slice(0, colonIdx);
31
+ const identifier = target.slice(colonIdx + 1);
32
+ return {
33
+ platform,
34
+ identifier,
35
+ uri: target,
36
+ isDM: identifier.startsWith("@")
37
+ };
38
+ }
39
+ if (target.startsWith("@")) {
40
+ return {
41
+ platform: "volute",
42
+ identifier: target,
43
+ uri: `volute:${target}`,
44
+ isDM: true
45
+ };
46
+ }
47
+ return {
48
+ platform: "volute",
49
+ identifier: target,
50
+ uri: `volute:${target}`,
51
+ isDM: false
52
+ };
53
+ }
54
+
55
+ // src/lib/read-stdin.ts
56
+ import { isatty } from "tty";
57
+ async function readStdin() {
58
+ if (isatty(0)) return void 0;
59
+ const chunks = [];
60
+ try {
61
+ for await (const chunk of process.stdin) {
62
+ chunks.push(chunk);
63
+ }
64
+ } catch (err) {
65
+ console.error(`Failed to read from stdin: ${err instanceof Error ? err.message : String(err)}`);
66
+ process.exit(1);
67
+ }
68
+ const text = Buffer.concat(chunks).toString().replace(/\r?\n$/, "");
69
+ return text || void 0;
70
+ }
71
+
72
+ // src/commands/send.ts
73
+ async function run(args) {
74
+ const { positional, flags } = parseArgs(args, {
75
+ agent: { type: "string" }
76
+ });
77
+ const target = positional[0];
78
+ const message = positional[1] ?? await readStdin();
79
+ if (!target || !message) {
80
+ console.error('Usage: volute send <target> "<message>" [--agent <name>]');
81
+ console.error(' echo "message" | volute send <target> [--agent <name>]');
82
+ console.error("");
83
+ console.error("Examples:");
84
+ console.error(' volute send @other-agent "hello"');
85
+ console.error(' volute send animal-chat "hello everyone"');
86
+ console.error(' volute send discord:server/channel "hello"');
87
+ process.exit(1);
88
+ }
89
+ const parsed = parseTarget(target);
90
+ const driver = getChannelDriver(parsed.platform);
91
+ if (!driver) {
92
+ console.error(`No driver for platform: ${parsed.platform}`);
93
+ process.exit(1);
94
+ }
95
+ let channelUri = parsed.uri;
96
+ if (parsed.isDM && parsed.platform === "volute") {
97
+ const targetName = parsed.identifier.slice(1);
98
+ const agentSelf = process.env.VOLUTE_AGENT;
99
+ const sender = agentSelf || userInfo().username;
100
+ if (!driver.createConversation) {
101
+ console.error("Volute driver does not support creating conversations");
102
+ process.exit(1);
103
+ }
104
+ const env = {
105
+ VOLUTE_AGENT: targetName,
106
+ VOLUTE_SENDER: sender
107
+ };
108
+ try {
109
+ channelUri = await driver.createConversation(env, [sender]);
110
+ } catch (err) {
111
+ console.error(err instanceof Error ? err.message : String(err));
112
+ process.exit(1);
113
+ }
114
+ try {
115
+ await driver.send(env, channelUri, message);
116
+ console.log("Message sent.");
117
+ } catch (err) {
118
+ console.error(err instanceof Error ? err.message : String(err));
119
+ process.exit(1);
120
+ }
121
+ if (agentSelf) {
122
+ try {
123
+ await daemonFetch(`/api/agents/${encodeURIComponent(agentSelf)}/history`, {
124
+ method: "POST",
125
+ headers: { "Content-Type": "application/json" },
126
+ body: JSON.stringify({ channel: channelUri, content: message })
127
+ });
128
+ } catch (err) {
129
+ console.error(`Failed to persist to history: ${err instanceof Error ? err.message : err}`);
130
+ }
131
+ }
132
+ } else {
133
+ const agentName = resolveAgentName(flags);
134
+ const { dir } = resolveAgent(agentName);
135
+ const env = {
136
+ ...loadMergedEnv(agentName),
137
+ VOLUTE_AGENT: agentName,
138
+ VOLUTE_AGENT_DIR: dir
139
+ };
140
+ try {
141
+ await driver.send(env, channelUri, message);
142
+ console.log("Message sent.");
143
+ } catch (err) {
144
+ console.error(err instanceof Error ? err.message : String(err));
145
+ process.exit(1);
146
+ }
147
+ if (process.env.VOLUTE_AGENT) {
148
+ try {
149
+ await daemonFetch(`/api/agents/${encodeURIComponent(agentName)}/history`, {
150
+ method: "POST",
151
+ headers: { "Content-Type": "application/json" },
152
+ body: JSON.stringify({ channel: channelUri, content: message })
153
+ });
154
+ } catch (err) {
155
+ console.error(`Failed to persist to history: ${err instanceof Error ? err.message : err}`);
156
+ }
157
+ }
158
+ }
159
+ }
160
+ export {
161
+ run
162
+ };
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-D424ZQGI.js";
5
5
  import {
6
6
  resolveVoluteBin
7
- } from "./chunk-5SKQ6J7T.js";
7
+ } from "./chunk-5C5JWR2L.js";
8
8
  import "./chunk-K3NQKI34.js";
9
9
 
10
10
  // src/commands/service.ts
@@ -82,6 +82,13 @@ async function install(port, host) {
82
82
  await execFileAsync("launchctl", ["load", path]);
83
83
  console.log("Service installed and loaded. Volute daemon will start on login.");
84
84
  } else if (platform === "linux") {
85
+ if (process.getuid?.() === 0) {
86
+ console.error(
87
+ "Error: `volute service install` uses systemd user services, which don't work as root."
88
+ );
89
+ console.error("Use `volute setup` instead to install a system-level service.");
90
+ process.exit(1);
91
+ }
85
92
  const path = unitPath();
86
93
  mkdirSync(resolve(homedir(), ".config", "systemd", "user"), { recursive: true });
87
94
  writeFileSync(path, generateUnit(voluteBin, port, host));
@@ -4,44 +4,73 @@ import {
4
4
  } from "./chunk-D424ZQGI.js";
5
5
  import {
6
6
  ensureVoluteGroup
7
- } from "./chunk-W76KWE23.js";
7
+ } from "./chunk-IQXBMFZG.js";
8
8
  import {
9
9
  resolveVoluteBin
10
- } from "./chunk-5SKQ6J7T.js";
11
- import "./chunk-UWHWAPGO.js";
10
+ } from "./chunk-5C5JWR2L.js";
11
+ import "./chunk-DP2DX4WV.js";
12
12
  import "./chunk-K3NQKI34.js";
13
13
 
14
14
  // src/commands/setup.ts
15
15
  import { execFileSync } from "child_process";
16
16
  import { existsSync, mkdirSync, rmSync, unlinkSync, writeFileSync } from "fs";
17
+ import { homedir } from "os";
18
+ import { dirname } from "path";
17
19
  var SERVICE_NAME = "volute.service";
18
20
  var SERVICE_PATH = `/etc/systemd/system/${SERVICE_NAME}`;
19
21
  var DATA_DIR = "/var/lib/volute";
22
+ var AGENTS_DIR = "/agents";
20
23
  var HOST_RE = /^[a-zA-Z0-9.:_-]+$/;
21
24
  function validateHost(host) {
22
25
  if (!HOST_RE.test(host)) {
23
26
  throw new Error(`Invalid host: ${host}`);
24
27
  }
25
28
  }
29
+ function buildServicePath(voluteBin) {
30
+ const binDir = dirname(voluteBin);
31
+ const standardPaths = [
32
+ "/usr/local/sbin",
33
+ "/usr/local/bin",
34
+ "/usr/sbin",
35
+ "/usr/bin",
36
+ "/sbin",
37
+ "/bin"
38
+ ];
39
+ const parts = standardPaths.includes(binDir) ? standardPaths : [binDir, ...standardPaths];
40
+ return parts.join(":");
41
+ }
26
42
  function generateUnit(voluteBin, port, host) {
27
43
  const args = ["up", "--foreground"];
28
44
  if (port != null) args.push("--port", String(port));
29
45
  if (host) args.push("--host", host);
30
- return `[Unit]
31
- Description=Volute Agent Manager
32
- After=network.target
33
-
34
- [Service]
35
- Type=exec
36
- ExecStart=${voluteBin} ${args.join(" ")}
37
- Environment=VOLUTE_HOME=${DATA_DIR}
38
- Environment=VOLUTE_ISOLATION=user
39
- Restart=on-failure
40
- RestartSec=5
41
-
42
- [Install]
43
- WantedBy=multi-user.target
44
- `;
46
+ const home = homedir();
47
+ const binUnderHome = voluteBin.startsWith(`${home}/`);
48
+ const lines = [
49
+ "[Unit]",
50
+ "Description=Volute Agent Manager",
51
+ "After=network.target",
52
+ "",
53
+ "[Service]",
54
+ "Type=exec",
55
+ `ExecStart=${voluteBin} ${args.join(" ")}`,
56
+ `Environment=PATH=${buildServicePath(voluteBin)}`,
57
+ `Environment=VOLUTE_HOME=${DATA_DIR}`,
58
+ `Environment=VOLUTE_AGENTS_DIR=${AGENTS_DIR}`,
59
+ "Environment=VOLUTE_ISOLATION=user",
60
+ "Restart=on-failure",
61
+ "RestartSec=5",
62
+ "ProtectSystem=strict",
63
+ `ReadWritePaths=${DATA_DIR} ${AGENTS_DIR}`,
64
+ "PrivateTmp=yes"
65
+ ];
66
+ if (!binUnderHome) {
67
+ lines.push("ProtectHome=yes");
68
+ } else {
69
+ console.warn(`Warning: ProtectHome=yes omitted because volute binary is under ${home}.`);
70
+ console.warn("Consider installing Node.js system-wide for stronger sandboxing.");
71
+ }
72
+ lines.push("RestrictSUIDSGID=yes", "", "[Install]", "WantedBy=multi-user.target", "");
73
+ return lines.join("\n");
45
74
  }
46
75
  function install(port, host) {
47
76
  if (host) validateHost(host);
@@ -57,18 +86,39 @@ function install(port, host) {
57
86
  const voluteBin = resolveVoluteBin();
58
87
  mkdirSync(DATA_DIR, { recursive: true });
59
88
  console.log(`Created ${DATA_DIR}`);
89
+ mkdirSync(AGENTS_DIR, { recursive: true });
90
+ console.log(`Created ${AGENTS_DIR}`);
60
91
  ensureVoluteGroup({ force: true });
61
92
  console.log("Ensured volute group exists");
62
93
  execFileSync("chmod", ["755", DATA_DIR]);
63
- console.log("Set permissions on data directory");
94
+ execFileSync("chmod", ["755", AGENTS_DIR]);
95
+ console.log("Set permissions on directories");
64
96
  writeFileSync(SERVICE_PATH, generateUnit(voluteBin, port, host ?? "0.0.0.0"));
65
97
  console.log(`Wrote ${SERVICE_PATH}`);
66
- execFileSync("systemctl", ["daemon-reload"]);
67
- execFileSync("systemctl", ["enable", "--now", SERVICE_NAME]);
68
- console.log("Service installed, enabled, and started.");
69
- console.log(`
98
+ try {
99
+ execFileSync("systemctl", ["daemon-reload"]);
100
+ } catch (err) {
101
+ const e = err;
102
+ console.error(`Failed to reload systemd after writing ${SERVICE_PATH}.`);
103
+ if (e.stderr) console.error(e.stderr.toString().trim());
104
+ console.error(
105
+ "Try running `systemctl daemon-reload` manually, then `systemctl enable --now volute`."
106
+ );
107
+ process.exit(1);
108
+ }
109
+ try {
110
+ execFileSync("systemctl", ["enable", "--now", SERVICE_NAME]);
111
+ console.log("Service installed, enabled, and started.");
112
+ console.log(`
70
113
  Volute daemon is running. Data directory: ${DATA_DIR}`);
71
- console.log("Use `systemctl status volute` to check status.");
114
+ console.log("Use `systemctl status volute` to check status.");
115
+ } catch (err) {
116
+ const e = err;
117
+ console.error("Service installed but failed to start.");
118
+ if (e.stderr) console.error(e.stderr.toString().trim());
119
+ console.error("Check `journalctl -xeu volute.service` for details.");
120
+ process.exit(1);
121
+ }
72
122
  }
73
123
  function uninstall(force) {
74
124
  if (process.getuid?.() !== 0) {
@@ -97,10 +147,15 @@ function uninstall(force) {
97
147
  const members = output.split(":")[3]?.trim();
98
148
  if (members) {
99
149
  for (const user of members.split(",")) {
150
+ const u = user.trim();
100
151
  try {
101
- execFileSync("userdel", [user.trim()], { stdio: "ignore" });
152
+ execFileSync("userdel", [u], { stdio: "ignore" });
153
+ } catch {
154
+ console.warn(`Warning: failed to remove user ${u}`);
155
+ }
156
+ try {
157
+ execFileSync("groupdel", [u], { stdio: "ignore" });
102
158
  } catch {
103
- console.warn(`Warning: failed to remove user ${user.trim()}`);
104
159
  }
105
160
  }
106
161
  }
@@ -110,6 +165,10 @@ function uninstall(force) {
110
165
  rmSync(DATA_DIR, { recursive: true, force: true });
111
166
  console.log(`Deleted ${DATA_DIR}`);
112
167
  }
168
+ if (existsSync(AGENTS_DIR)) {
169
+ rmSync(AGENTS_DIR, { recursive: true, force: true });
170
+ console.log(`Deleted ${AGENTS_DIR}`);
171
+ }
113
172
  try {
114
173
  execFileSync("groupdel", ["volute"], { stdio: "ignore" });
115
174
  console.log("Removed volute group");
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  daemonFetch
4
- } from "./chunk-JR4UXCTO.js";
4
+ } from "./chunk-23L3MKEV.js";
5
5
  import {
6
6
  resolveAgent
7
- } from "./chunk-UWHWAPGO.js";
7
+ } from "./chunk-DP2DX4WV.js";
8
8
  import "./chunk-K3NQKI34.js";
9
9
 
10
10
  // src/commands/start.ts
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  daemonFetch
4
- } from "./chunk-JR4UXCTO.js";
5
- import "./chunk-UWHWAPGO.js";
4
+ } from "./chunk-23L3MKEV.js";
5
+ import "./chunk-DP2DX4WV.js";
6
6
  import "./chunk-K3NQKI34.js";
7
7
 
8
8
  // src/commands/status.ts
@@ -4,10 +4,10 @@ import {
4
4
  } from "./chunk-AZEL2IEK.js";
5
5
  import {
6
6
  daemonFetch
7
- } from "./chunk-JR4UXCTO.js";
7
+ } from "./chunk-23L3MKEV.js";
8
8
  import {
9
9
  resolveAgent
10
- } from "./chunk-UWHWAPGO.js";
10
+ } from "./chunk-DP2DX4WV.js";
11
11
  import "./chunk-K3NQKI34.js";
12
12
 
13
13
  // src/commands/stop.ts
@@ -2,9 +2,9 @@
2
2
  import {
3
3
  readGlobalConfig,
4
4
  run
5
- } from "./chunk-EG45HBSJ.js";
5
+ } from "./chunk-XSJ27WEM.js";
6
6
  import "./chunk-D424ZQGI.js";
7
- import "./chunk-UWHWAPGO.js";
7
+ import "./chunk-DP2DX4WV.js";
8
8
  import "./chunk-K3NQKI34.js";
9
9
  export {
10
10
  readGlobalConfig,
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  checkForUpdate
4
- } from "./chunk-NKXULRSW.js";
4
+ } from "./chunk-RT6Y7AR3.js";
5
5
  import {
6
6
  execInherit,
7
7
  resolveVoluteBin
8
- } from "./chunk-5SKQ6J7T.js";
8
+ } from "./chunk-5C5JWR2L.js";
9
9
  import {
10
10
  voluteHome
11
- } from "./chunk-UWHWAPGO.js";
11
+ } from "./chunk-DP2DX4WV.js";
12
12
  import "./chunk-K3NQKI34.js";
13
13
 
14
14
  // src/commands/update.ts
@@ -5,8 +5,8 @@ import {
5
5
  fetchLatestVersion,
6
6
  getCurrentVersion,
7
7
  isNewer
8
- } from "./chunk-NKXULRSW.js";
9
- import "./chunk-UWHWAPGO.js";
8
+ } from "./chunk-RT6Y7AR3.js";
9
+ import "./chunk-DP2DX4WV.js";
10
10
  import "./chunk-K3NQKI34.js";
11
11
  export {
12
12
  checkForUpdate,