volute 0.1.0

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 (64) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +227 -0
  3. package/dist/channel-Q642YUZE.js +90 -0
  4. package/dist/chunk-5YW4B7CG.js +181 -0
  5. package/dist/chunk-A5ZJEMHT.js +40 -0
  6. package/dist/chunk-D424ZQGI.js +31 -0
  7. package/dist/chunk-GSPKUPKU.js +120 -0
  8. package/dist/chunk-H5XQARAP.js +48 -0
  9. package/dist/chunk-KSMIWOCN.js +84 -0
  10. package/dist/chunk-N4QN44LC.js +74 -0
  11. package/dist/chunk-XZN4WPNC.js +34 -0
  12. package/dist/cli.js +95 -0
  13. package/dist/connect-LW6G23AV.js +48 -0
  14. package/dist/connectors/discord.js +213 -0
  15. package/dist/create-3K6O2SDC.js +62 -0
  16. package/dist/daemon-client-ZTHW7ROS.js +10 -0
  17. package/dist/daemon.js +1731 -0
  18. package/dist/delete-JNGY7ZFH.js +54 -0
  19. package/dist/disconnect-ACVTKTRE.js +30 -0
  20. package/dist/down-FYCUYC5H.js +71 -0
  21. package/dist/env-7SLRN3MG.js +159 -0
  22. package/dist/fork-BB3DZ426.js +112 -0
  23. package/dist/import-W2AMTEV5.js +410 -0
  24. package/dist/logs-BUHRIQ2L.js +35 -0
  25. package/dist/merge-446QTE7Q.js +219 -0
  26. package/dist/schedule-KKSOVUDF.js +113 -0
  27. package/dist/send-WQSVSRDD.js +50 -0
  28. package/dist/start-LKMWS6ZE.js +29 -0
  29. package/dist/status-CIEKUI3V.js +50 -0
  30. package/dist/stop-YTOAGYE4.js +29 -0
  31. package/dist/up-AJJ4GCXY.js +111 -0
  32. package/dist/upgrade-JACA6YMO.js +211 -0
  33. package/dist/variants-HPY4DEWU.js +60 -0
  34. package/dist/web-assets/assets/index-DNNPoxMn.js +158 -0
  35. package/dist/web-assets/index.html +15 -0
  36. package/package.json +76 -0
  37. package/templates/_base/.init/MEMORY.md +2 -0
  38. package/templates/_base/.init/SOUL.md +2 -0
  39. package/templates/_base/.init/memory/.gitkeep +0 -0
  40. package/templates/_base/_skills/memory/SKILL.md +30 -0
  41. package/templates/_base/_skills/volute-agent/SKILL.md +53 -0
  42. package/templates/_base/biome.json.tmpl +21 -0
  43. package/templates/_base/home/VOLUTE.md +19 -0
  44. package/templates/_base/src/lib/auto-commit.ts +46 -0
  45. package/templates/_base/src/lib/logger.ts +47 -0
  46. package/templates/_base/src/lib/types.ts +24 -0
  47. package/templates/_base/src/lib/volute-server.ts +98 -0
  48. package/templates/_base/tsconfig.json +13 -0
  49. package/templates/_base/volute.json.tmpl +3 -0
  50. package/templates/agent-sdk/.init/CLAUDE.md +36 -0
  51. package/templates/agent-sdk/package.json.tmpl +20 -0
  52. package/templates/agent-sdk/src/lib/agent.ts +199 -0
  53. package/templates/agent-sdk/src/lib/hooks/auto-commit.ts +14 -0
  54. package/templates/agent-sdk/src/lib/hooks/identity-reload.ts +26 -0
  55. package/templates/agent-sdk/src/lib/hooks/pre-compact.ts +20 -0
  56. package/templates/agent-sdk/src/lib/message-channel.ts +37 -0
  57. package/templates/agent-sdk/src/server.ts +158 -0
  58. package/templates/agent-sdk/volute-template.json +9 -0
  59. package/templates/pi/.init/AGENTS.md +26 -0
  60. package/templates/pi/package.json.tmpl +20 -0
  61. package/templates/pi/src/lib/agent.ts +205 -0
  62. package/templates/pi/src/server.ts +121 -0
  63. package/templates/pi/volute-template.json +9 -0
  64. package/templates/pi/volute.json.tmpl +3 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 mimsy
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,227 @@
1
+ # Volute
2
+
3
+ A CLI for creating and managing persistent, self-modifying AI agents.
4
+
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
+
7
+ Built on the [Anthropic Claude Agent SDK](https://github.com/anthropics/claude-agent-sdk).
8
+
9
+ ## Quickstart
10
+
11
+ ```sh
12
+ npm install -g volute
13
+
14
+ # Start the daemon (manages all your agents)
15
+ volute up
16
+
17
+ # Create an agent
18
+ volute create atlas
19
+
20
+ # Start it
21
+ volute start atlas
22
+
23
+ # Talk to it
24
+ volute send atlas "hey, what can you do?"
25
+ ```
26
+
27
+ You now have a running AI agent with persistent memory, auto-committing file changes, and session resume across restarts. Open `http://localhost:4200` for the web dashboard.
28
+
29
+ ## The daemon
30
+
31
+ One background process runs everything. `volute up` starts it; `volute down` stops it.
32
+
33
+ ```sh
34
+ volute up # start (default port 4200)
35
+ volute up --port 8080 # custom port
36
+ volute down # stop all agents and shut down
37
+ ```
38
+
39
+ The daemon handles agent lifecycle, crash recovery (auto-restarts after 3 seconds), connector processes, scheduled messages, and the web dashboard.
40
+
41
+ ## Agents
42
+
43
+ ### Lifecycle
44
+
45
+ ```sh
46
+ volute create atlas # scaffold a new agent
47
+ volute start atlas # start it
48
+ volute stop atlas # stop it
49
+ volute status # list all agents
50
+ volute status atlas # check one
51
+ volute logs atlas --follow # tail logs
52
+ volute delete atlas # remove from registry
53
+ volute delete atlas --force # also delete files
54
+ ```
55
+
56
+ ### Sending messages
57
+
58
+ ```sh
59
+ volute send atlas "what's on your mind?"
60
+ ```
61
+
62
+ Responses stream back to your terminal in real time. The agent knows which channel each message came from — CLI, web, Discord, or system — and routes its response back to the source.
63
+
64
+ ### Anatomy of an agent
65
+
66
+ ```
67
+ ~/.volute/agents/atlas/
68
+ ├── home/ # the agent's working directory (its cwd)
69
+ │ ├── SOUL.md # personality and system prompt
70
+ │ ├── MEMORY.md # long-term memory, always in context
71
+ │ ├── VOLUTE.md # channel routing docs
72
+ │ └── memory/ # daily logs (YYYY-MM-DD.md)
73
+ ├── src/ # agent server code
74
+ ├── volute.json # agent config (model, etc.)
75
+ └── .volute/ # runtime state, session, logs
76
+ ```
77
+
78
+ **`SOUL.md`** is the identity. This is the core of the system prompt. Edit it to change how the agent thinks and speaks.
79
+
80
+ **`MEMORY.md`** is long-term memory, always included in context. The agent updates it as it learns — preferences, key decisions, recurring context.
81
+
82
+ **Daily logs** (`memory/YYYY-MM-DD.md`) are working memory. Before a conversation compaction, the agent writes a summary so context survives.
83
+
84
+ **Auto-commit**: any file changes the agent makes inside `home/` are automatically committed to git.
85
+
86
+ **Session resume**: if the agent restarts, it picks up where it left off.
87
+
88
+ ## Variants
89
+
90
+ This is the interesting part. Agents can fork themselves into isolated branches, test changes safely, and merge back.
91
+
92
+ ```sh
93
+ # Create a variant — gets its own git worktree and running server
94
+ volute fork atlas experiment
95
+
96
+ # Talk to the variant directly
97
+ volute send atlas@experiment "try a different approach"
98
+
99
+ # List all variants
100
+ volute variants atlas
101
+
102
+ # Merge it back (verifies, merges, cleans up, restarts the main agent)
103
+ volute merge atlas experiment --summary "improved response style"
104
+ ```
105
+
106
+ What happens:
107
+
108
+ 1. **Fork** creates a git worktree, installs dependencies, and starts a separate server
109
+ 2. The variant is a full independent copy — same code, same identity, its own state
110
+ 3. **Merge** verifies the variant server works, merges the branch, removes the worktree, and restarts the main agent
111
+ 4. After restart, the agent receives orientation context about what changed
112
+
113
+ You can fork with a custom personality:
114
+
115
+ ```sh
116
+ volute fork atlas poet --soul "You are a poet who responds only in verse."
117
+ ```
118
+
119
+ Agents have access to the `volute` CLI from their working directory, so they can fork, test, and merge their own variants autonomously.
120
+
121
+ ## Connectors
122
+
123
+ Connect agents to external services.
124
+
125
+ ### Discord
126
+
127
+ ```sh
128
+ # Set the bot token (shared across agents, or per-agent with --agent)
129
+ volute env set DISCORD_TOKEN <your-bot-token>
130
+
131
+ # Connect
132
+ volute connect discord atlas
133
+
134
+ # Disconnect
135
+ volute disconnect discord atlas
136
+ ```
137
+
138
+ The agent receives Discord messages and responds in-channel. Tool calls are filtered out — Discord users see clean text responses.
139
+
140
+ ### Channel commands
141
+
142
+ Read from and write to channels directly:
143
+
144
+ ```sh
145
+ volute channel read discord:123456789 # recent messages
146
+ volute channel send discord:123456789 "hello" # send a message
147
+ ```
148
+
149
+ ## Schedules
150
+
151
+ Cron-based scheduled messages — daily check-ins, periodic tasks, whatever you need.
152
+
153
+ ```sh
154
+ volute schedule add atlas \
155
+ --cron "0 9 * * *" \
156
+ --message "good morning — write your daily log"
157
+
158
+ volute schedule list atlas
159
+ volute schedule remove atlas --id <schedule-id>
160
+ ```
161
+
162
+ ## Environment variables
163
+
164
+ Manage secrets and config. Supports shared (all agents) and per-agent scoping.
165
+
166
+ ```sh
167
+ volute env set API_KEY sk-abc123 # shared
168
+ volute env set API_KEY sk-xyz789 --agent atlas # agent-specific override
169
+ volute env list --agent atlas # see effective config
170
+ volute env remove API_KEY
171
+ ```
172
+
173
+ ## Web dashboard
174
+
175
+ The daemon serves a web UI at `http://localhost:4200` (or whatever port you chose).
176
+
177
+ - Real-time chat with full tool call visibility
178
+ - File browser and editor
179
+ - Log streaming
180
+ - Connector and schedule management
181
+ - Variant status
182
+ - First user to register becomes admin
183
+
184
+ ## Upgrading agents
185
+
186
+ When the Volute template updates, you can upgrade agents without touching their identity:
187
+
188
+ ```sh
189
+ volute upgrade atlas # creates an "upgrade" variant
190
+ # resolve conflicts if needed, then:
191
+ volute upgrade atlas --continue
192
+ # test:
193
+ volute send atlas@upgrade "are you working?"
194
+ # merge:
195
+ volute merge atlas upgrade
196
+ ```
197
+
198
+ Your agent's `SOUL.md` and `MEMORY.md` are never overwritten.
199
+
200
+ ## Templates
201
+
202
+ Two built-in templates:
203
+
204
+ - **`agent-sdk`** (default) — Anthropic Claude Agent SDK
205
+ - **`pi`** — [pi-coding-agent](https://github.com/nicepkg/pi) for multi-provider LLM support
206
+
207
+ ```sh
208
+ volute create atlas --template pi
209
+ ```
210
+
211
+ ## Model configuration
212
+
213
+ Set the model via `volute.json` in the agent's root directory, or the `VOLUTE_MODEL` env var.
214
+
215
+ ## Development
216
+
217
+ ```sh
218
+ git clone <repo-url>
219
+ cd volute
220
+ npm install
221
+ npm run dev # run CLI via tsx
222
+ npm run build # build CLI + web frontend
223
+ npm run dev:web # frontend dev server
224
+ npm test # run tests
225
+ ```
226
+
227
+ Install globally for testing: `npm run build && npm link`.
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ loadMergedEnv
4
+ } from "./chunk-A5ZJEMHT.js";
5
+ import {
6
+ parseArgs
7
+ } from "./chunk-D424ZQGI.js";
8
+ import {
9
+ resolveAgent
10
+ } from "./chunk-5YW4B7CG.js";
11
+
12
+ // src/lib/channels/discord.ts
13
+ var API_BASE = "https://discord.com/api/v10";
14
+ async function read(token, channelId, limit) {
15
+ const res = await fetch(`${API_BASE}/channels/${channelId}/messages?limit=${limit}`, {
16
+ headers: { Authorization: `Bot ${token}` }
17
+ });
18
+ if (!res.ok) {
19
+ throw new Error(`Discord API error: ${res.status} ${res.statusText}`);
20
+ }
21
+ const messages = await res.json();
22
+ return messages.reverse().map((m) => `${m.author.username}: ${m.content}`).join("\n");
23
+ }
24
+ async function send(token, channelId, message) {
25
+ const res = await fetch(`${API_BASE}/channels/${channelId}/messages`, {
26
+ method: "POST",
27
+ headers: {
28
+ Authorization: `Bot ${token}`,
29
+ "Content-Type": "application/json"
30
+ },
31
+ body: JSON.stringify({ content: message })
32
+ });
33
+ if (!res.ok) {
34
+ throw new Error(`Discord API error: ${res.status} ${res.statusText}`);
35
+ }
36
+ }
37
+
38
+ // src/commands/channel.ts
39
+ async function run(args) {
40
+ const { positional, flags } = parseArgs(args, {
41
+ agent: { type: "string" },
42
+ limit: { type: "number" }
43
+ });
44
+ const subcommand = positional[0];
45
+ const uri = positional[1];
46
+ const message = positional[2];
47
+ if (!subcommand || !uri || subcommand === "send" && !message) {
48
+ console.error(`Usage:
49
+ volute channel read <channel-uri> [--limit N] [--agent <name>]
50
+ volute channel send <channel-uri> "<message>" [--agent <name>]`);
51
+ process.exit(1);
52
+ }
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
+ }
58
+ const colonIdx = uri.indexOf(":");
59
+ if (colonIdx === -1) {
60
+ console.error(`Invalid channel URI: ${uri} (expected format: platform:id)`);
61
+ process.exit(1);
62
+ }
63
+ const platform = uri.slice(0, colonIdx);
64
+ const channelId = uri.slice(colonIdx + 1);
65
+ const { dir } = resolveAgent(agentName);
66
+ const env = loadMergedEnv(dir);
67
+ if (platform === "discord") {
68
+ const token = env.DISCORD_TOKEN;
69
+ if (!token) {
70
+ console.error("DISCORD_TOKEN not set. Run: volute env set DISCORD_TOKEN <token>");
71
+ process.exit(1);
72
+ }
73
+ if (subcommand === "read") {
74
+ const limit = flags.limit ?? 20;
75
+ const output = await read(token, channelId, limit);
76
+ console.log(output);
77
+ } else if (subcommand === "send") {
78
+ await send(token, channelId, message);
79
+ } else {
80
+ console.error(`Unknown subcommand: ${subcommand}`);
81
+ process.exit(1);
82
+ }
83
+ } else {
84
+ console.error(`Unsupported platform: ${platform}`);
85
+ process.exit(1);
86
+ }
87
+ }
88
+ export {
89
+ run
90
+ };
@@ -0,0 +1,181 @@
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
+ // src/lib/variants.ts
9
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
10
+ import { homedir } from "os";
11
+ import { resolve } from "path";
12
+ var VOLUTE_HOME = process.env.VOLUTE_HOME || resolve(homedir(), ".volute");
13
+ var VARIANTS_PATH = resolve(VOLUTE_HOME, "variants.json");
14
+ function readAllVariants() {
15
+ if (!existsSync(VARIANTS_PATH)) return {};
16
+ try {
17
+ return JSON.parse(readFileSync(VARIANTS_PATH, "utf-8"));
18
+ } catch {
19
+ return {};
20
+ }
21
+ }
22
+ function writeAllVariants(all) {
23
+ mkdirSync(VOLUTE_HOME, { recursive: true });
24
+ writeFileSync(VARIANTS_PATH, `${JSON.stringify(all, null, 2)}
25
+ `);
26
+ }
27
+ function readVariants(agentName) {
28
+ return readAllVariants()[agentName] ?? [];
29
+ }
30
+ function writeVariants(agentName, variants) {
31
+ const all = readAllVariants();
32
+ if (variants.length === 0) {
33
+ delete all[agentName];
34
+ } else {
35
+ all[agentName] = variants;
36
+ }
37
+ writeAllVariants(all);
38
+ }
39
+ function addVariant(agentName, variant) {
40
+ const variants = readVariants(agentName);
41
+ const filtered = variants.filter((v) => v.name !== variant.name);
42
+ filtered.push(variant);
43
+ writeVariants(agentName, filtered);
44
+ }
45
+ function removeVariant(agentName, name) {
46
+ const variants = readVariants(agentName);
47
+ writeVariants(
48
+ agentName,
49
+ variants.filter((v) => v.name !== name)
50
+ );
51
+ }
52
+ function findVariant(agentName, name) {
53
+ return readVariants(agentName).find((v) => v.name === name);
54
+ }
55
+ function removeAllVariants(agentName) {
56
+ const all = readAllVariants();
57
+ delete all[agentName];
58
+ writeAllVariants(all);
59
+ }
60
+ async function checkHealth(port) {
61
+ try {
62
+ const res = await fetch(`http://localhost:${port}/health`, {
63
+ signal: AbortSignal.timeout(2e3)
64
+ });
65
+ if (!res.ok) return { ok: false };
66
+ const data = await res.json();
67
+ return { ok: true, name: data.name };
68
+ } catch {
69
+ return { ok: false };
70
+ }
71
+ }
72
+ var SAFE_BRANCH_RE = /^[a-zA-Z0-9._\-/]+$/;
73
+ function validateBranchName(branch) {
74
+ if (!SAFE_BRANCH_RE.test(branch)) {
75
+ return `Invalid branch name: ${branch}. Only alphanumeric, '.', '_', '-', '/' allowed.`;
76
+ }
77
+ if (branch.includes("..")) {
78
+ return `Invalid branch name: ${branch}. '..' not allowed.`;
79
+ }
80
+ return null;
81
+ }
82
+
83
+ // src/lib/registry.ts
84
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
85
+ import { homedir as homedir2 } from "os";
86
+ import { resolve as resolve2 } from "path";
87
+ var VOLUTE_HOME2 = process.env.VOLUTE_HOME || resolve2(homedir2(), ".volute");
88
+ var AGENTS_DIR = resolve2(VOLUTE_HOME2, "agents");
89
+ var REGISTRY_PATH = resolve2(VOLUTE_HOME2, "agents.json");
90
+ function ensureVoluteHome() {
91
+ mkdirSync2(AGENTS_DIR, { recursive: true });
92
+ }
93
+ function readRegistry() {
94
+ if (!existsSync2(REGISTRY_PATH)) return [];
95
+ try {
96
+ const entries = JSON.parse(readFileSync2(REGISTRY_PATH, "utf-8"));
97
+ return entries.map((e) => ({ ...e, running: e.running ?? false }));
98
+ } catch {
99
+ return [];
100
+ }
101
+ }
102
+ function writeRegistry(entries) {
103
+ ensureVoluteHome();
104
+ writeFileSync2(REGISTRY_PATH, `${JSON.stringify(entries, null, 2)}
105
+ `);
106
+ }
107
+ function addAgent(name, port) {
108
+ const entries = readRegistry();
109
+ const filtered = entries.filter((e) => e.name !== name);
110
+ filtered.push({ name, port, created: (/* @__PURE__ */ new Date()).toISOString(), running: false });
111
+ writeRegistry(filtered);
112
+ }
113
+ function removeAgent(name) {
114
+ const entries = readRegistry();
115
+ writeRegistry(entries.filter((e) => e.name !== name));
116
+ }
117
+ function setAgentRunning(name, running) {
118
+ const entries = readRegistry();
119
+ const entry = entries.find((e) => e.name === name);
120
+ if (entry) {
121
+ entry.running = running;
122
+ writeRegistry(entries);
123
+ }
124
+ }
125
+ function findAgent(name) {
126
+ return readRegistry().find((e) => e.name === name);
127
+ }
128
+ function agentDir(name) {
129
+ return resolve2(AGENTS_DIR, name);
130
+ }
131
+ function nextPort() {
132
+ const entries = readRegistry();
133
+ const usedPorts = new Set(entries.map((e) => e.port));
134
+ let port = 4100;
135
+ while (usedPorts.has(port)) port++;
136
+ return port;
137
+ }
138
+ function resolveAgent(name) {
139
+ const [baseName, variantName] = name.split("@", 2);
140
+ const entry = findAgent(baseName);
141
+ if (!entry) {
142
+ console.error(`Unknown agent: ${baseName}`);
143
+ process.exit(1);
144
+ }
145
+ const dir = agentDir(baseName);
146
+ if (!existsSync2(dir)) {
147
+ console.error(`Agent directory missing: ${dir}`);
148
+ process.exit(1);
149
+ }
150
+ if (variantName) {
151
+ const variant = findVariant(baseName, variantName);
152
+ if (!variant) {
153
+ console.error(`Unknown variant: ${variantName} (agent: ${baseName})`);
154
+ process.exit(1);
155
+ }
156
+ return { entry: { ...entry, port: variant.port }, dir };
157
+ }
158
+ return { entry, dir };
159
+ }
160
+
161
+ export {
162
+ __export,
163
+ readVariants,
164
+ writeVariants,
165
+ addVariant,
166
+ removeVariant,
167
+ findVariant,
168
+ removeAllVariants,
169
+ checkHealth,
170
+ validateBranchName,
171
+ VOLUTE_HOME2 as VOLUTE_HOME,
172
+ ensureVoluteHome,
173
+ readRegistry,
174
+ addAgent,
175
+ removeAgent,
176
+ setAgentRunning,
177
+ findAgent,
178
+ agentDir,
179
+ nextPort,
180
+ resolveAgent
181
+ };
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ VOLUTE_HOME
4
+ } from "./chunk-5YW4B7CG.js";
5
+
6
+ // src/lib/env.ts
7
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
8
+ import { dirname, resolve } from "path";
9
+ function sharedEnvPath() {
10
+ return resolve(VOLUTE_HOME, "env.json");
11
+ }
12
+ function agentEnvPath(agentDir) {
13
+ return resolve(agentDir, ".volute", "env.json");
14
+ }
15
+ function readEnv(path) {
16
+ if (!existsSync(path)) return {};
17
+ try {
18
+ return JSON.parse(readFileSync(path, "utf-8"));
19
+ } catch {
20
+ return {};
21
+ }
22
+ }
23
+ function writeEnv(path, env) {
24
+ mkdirSync(dirname(path), { recursive: true });
25
+ writeFileSync(path, `${JSON.stringify(env, null, 2)}
26
+ `);
27
+ }
28
+ function loadMergedEnv(agentDir) {
29
+ const shared = readEnv(sharedEnvPath());
30
+ const agent = readEnv(agentEnvPath(agentDir));
31
+ return { ...shared, ...agent };
32
+ }
33
+
34
+ export {
35
+ sharedEnvPath,
36
+ agentEnvPath,
37
+ readEnv,
38
+ writeEnv,
39
+ loadMergedEnv
40
+ };
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/lib/parse-args.ts
4
+ function parseArgs(args, flags) {
5
+ const positional = [];
6
+ const result = {};
7
+ for (const [key, def] of Object.entries(flags)) {
8
+ result[key] = def.type === "boolean" ? false : void 0;
9
+ }
10
+ for (let i = 0; i < args.length; i++) {
11
+ const arg = args[i];
12
+ if (arg.startsWith("--")) {
13
+ const name = arg.slice(2);
14
+ const def = flags[name];
15
+ if (!def) continue;
16
+ if (def.type === "boolean") {
17
+ result[name] = true;
18
+ } else if (i + 1 < args.length) {
19
+ const val = args[++i];
20
+ result[name] = def.type === "number" ? parseInt(val, 10) : val;
21
+ }
22
+ } else {
23
+ positional.push(arg);
24
+ }
25
+ }
26
+ return { positional, flags: result };
27
+ }
28
+
29
+ export {
30
+ parseArgs
31
+ };