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.
- package/LICENSE +21 -0
- package/README.md +227 -0
- package/dist/channel-Q642YUZE.js +90 -0
- package/dist/chunk-5YW4B7CG.js +181 -0
- package/dist/chunk-A5ZJEMHT.js +40 -0
- package/dist/chunk-D424ZQGI.js +31 -0
- package/dist/chunk-GSPKUPKU.js +120 -0
- package/dist/chunk-H5XQARAP.js +48 -0
- package/dist/chunk-KSMIWOCN.js +84 -0
- package/dist/chunk-N4QN44LC.js +74 -0
- package/dist/chunk-XZN4WPNC.js +34 -0
- package/dist/cli.js +95 -0
- package/dist/connect-LW6G23AV.js +48 -0
- package/dist/connectors/discord.js +213 -0
- package/dist/create-3K6O2SDC.js +62 -0
- package/dist/daemon-client-ZTHW7ROS.js +10 -0
- package/dist/daemon.js +1731 -0
- package/dist/delete-JNGY7ZFH.js +54 -0
- package/dist/disconnect-ACVTKTRE.js +30 -0
- package/dist/down-FYCUYC5H.js +71 -0
- package/dist/env-7SLRN3MG.js +159 -0
- package/dist/fork-BB3DZ426.js +112 -0
- package/dist/import-W2AMTEV5.js +410 -0
- package/dist/logs-BUHRIQ2L.js +35 -0
- package/dist/merge-446QTE7Q.js +219 -0
- package/dist/schedule-KKSOVUDF.js +113 -0
- package/dist/send-WQSVSRDD.js +50 -0
- package/dist/start-LKMWS6ZE.js +29 -0
- package/dist/status-CIEKUI3V.js +50 -0
- package/dist/stop-YTOAGYE4.js +29 -0
- package/dist/up-AJJ4GCXY.js +111 -0
- package/dist/upgrade-JACA6YMO.js +211 -0
- package/dist/variants-HPY4DEWU.js +60 -0
- package/dist/web-assets/assets/index-DNNPoxMn.js +158 -0
- package/dist/web-assets/index.html +15 -0
- package/package.json +76 -0
- package/templates/_base/.init/MEMORY.md +2 -0
- package/templates/_base/.init/SOUL.md +2 -0
- package/templates/_base/.init/memory/.gitkeep +0 -0
- package/templates/_base/_skills/memory/SKILL.md +30 -0
- package/templates/_base/_skills/volute-agent/SKILL.md +53 -0
- package/templates/_base/biome.json.tmpl +21 -0
- package/templates/_base/home/VOLUTE.md +19 -0
- package/templates/_base/src/lib/auto-commit.ts +46 -0
- package/templates/_base/src/lib/logger.ts +47 -0
- package/templates/_base/src/lib/types.ts +24 -0
- package/templates/_base/src/lib/volute-server.ts +98 -0
- package/templates/_base/tsconfig.json +13 -0
- package/templates/_base/volute.json.tmpl +3 -0
- package/templates/agent-sdk/.init/CLAUDE.md +36 -0
- package/templates/agent-sdk/package.json.tmpl +20 -0
- package/templates/agent-sdk/src/lib/agent.ts +199 -0
- package/templates/agent-sdk/src/lib/hooks/auto-commit.ts +14 -0
- package/templates/agent-sdk/src/lib/hooks/identity-reload.ts +26 -0
- package/templates/agent-sdk/src/lib/hooks/pre-compact.ts +20 -0
- package/templates/agent-sdk/src/lib/message-channel.ts +37 -0
- package/templates/agent-sdk/src/server.ts +158 -0
- package/templates/agent-sdk/volute-template.json +9 -0
- package/templates/pi/.init/AGENTS.md +26 -0
- package/templates/pi/package.json.tmpl +20 -0
- package/templates/pi/src/lib/agent.ts +205 -0
- package/templates/pi/src/server.ts +121 -0
- package/templates/pi/volute-template.json +9 -0
- 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
|
+
};
|