volute 0.3.1 → 0.5.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 (82) hide show
  1. package/README.md +29 -29
  2. package/dist/agent-Z2B6EFEQ.js +75 -0
  3. package/dist/{agent-manager-AUCKMGPR.js → agent-manager-PXBKA2GK.js} +4 -4
  4. package/dist/channel-MK5OK2SI.js +113 -0
  5. package/dist/chunk-5X7HGB6L.js +107 -0
  6. package/dist/{chunk-YGFIWIOF.js → chunk-7L4AN5D4.js} +1 -1
  7. package/dist/{chunk-VRVVQIYY.js → chunk-AZEL2IEK.js} +1 -1
  8. package/dist/chunk-B3R6L2GW.js +24 -0
  9. package/dist/{chunk-DNOXHLE5.js → chunk-HE67X4T6.js} +1 -1
  10. package/dist/{chunk-I6OHXCMV.js → chunk-MW2KFO3B.js} +47 -9
  11. package/dist/chunk-MXUCNIBG.js +168 -0
  12. package/dist/chunk-SMISE4SV.js +226 -0
  13. package/dist/{chunk-SOZA2TLP.js → chunk-UAVD2AHX.js} +1 -1
  14. package/dist/{chunk-3C2XR4IY.js → chunk-UX25Z2ND.js} +113 -107
  15. package/dist/{chunk-GSPKUPKU.js → chunk-XUA3JUFK.js} +2 -1
  16. package/dist/chunk-ZYGKG6VC.js +22 -0
  17. package/dist/cli.js +98 -75
  18. package/dist/connector-LYEMXQEV.js +157 -0
  19. package/dist/connectors/discord.js +104 -161
  20. package/dist/connectors/slack.js +179 -0
  21. package/dist/connectors/telegram.js +175 -0
  22. package/dist/conversation-ERXEQZTY.js +163 -0
  23. package/dist/create-RVCZN6HE.js +91 -0
  24. package/dist/{daemon-client-XR24PUJF.js → daemon-client-ZY6UUN2M.js} +2 -2
  25. package/dist/daemon.js +824 -252
  26. package/dist/{delete-GQ7JEK2S.js → delete-3QH7VYIN.js} +8 -9
  27. package/dist/{down-3OB6UVAJ.js → down-O7IFZLVJ.js} +1 -1
  28. package/dist/{env-JB27UAC3.js → env-4D4REPJF.js} +8 -5
  29. package/dist/{history-3VRUBGGV.js → history-OEONB53Z.js} +5 -5
  30. package/dist/{import-K4MP2GX7.js → import-MXJB2EII.js} +23 -8
  31. package/dist/{logs-NXFFGUKY.js → logs-DF342W4M.js} +2 -2
  32. package/dist/message-ADHWFHSI.js +32 -0
  33. package/dist/package-VQOE7JNH.js +89 -0
  34. package/dist/{schedule-4I5TYHFH.js → schedule-NAG6F463.js} +12 -7
  35. package/dist/send-66QMKRUH.js +75 -0
  36. package/dist/{setup-SRS7AUAA.js → setup-RPRRGG2F.js} +6 -6
  37. package/dist/{start-LDPMCMYT.js → start-TUOXDSFL.js} +3 -3
  38. package/dist/{status-MVSQG54T.js → status-A36EHRO4.js} +3 -3
  39. package/dist/{stop-5PZTZCLL.js → stop-AOJZLQ5X.js} +6 -7
  40. package/dist/{up-UT3IMKCA.js → up-7ILD7GU7.js} +2 -2
  41. package/dist/update-LPSIAWQ2.js +140 -0
  42. package/dist/update-check-Y33QDCFL.js +17 -0
  43. package/dist/{upgrade-CDKECCGN.js → upgrade-FX2TKJ2S.js} +16 -15
  44. package/dist/{variant-CVYM3EQG.js → variant-LAB67OC2.js} +17 -12
  45. package/dist/web-assets/assets/index-BbRmoxoA.js +308 -0
  46. package/dist/web-assets/index.html +2 -2
  47. package/drizzle/0003_clean_ego.sql +12 -0
  48. package/drizzle/meta/0003_snapshot.json +417 -0
  49. package/drizzle/meta/_journal.json +7 -0
  50. package/package.json +3 -1
  51. package/templates/_base/.init/.config/hooks/startup-context.sh +19 -1
  52. package/templates/_base/_skills/volute-agent/SKILL.md +112 -16
  53. package/templates/_base/home/.config/routes.json +10 -0
  54. package/templates/_base/home/VOLUTE.md +19 -28
  55. package/templates/_base/src/lib/file-handler.ts +46 -0
  56. package/templates/_base/src/lib/format-prefix.ts +1 -1
  57. package/templates/_base/src/lib/router.ts +327 -0
  58. package/templates/_base/src/lib/routing.ts +137 -0
  59. package/templates/_base/src/lib/types.ts +16 -3
  60. package/templates/_base/src/lib/volute-server.ts +20 -48
  61. package/templates/agent-sdk/.init/.config/routes.json +5 -0
  62. package/templates/agent-sdk/.init/CLAUDE.md +2 -2
  63. package/templates/agent-sdk/src/agent.ts +269 -82
  64. package/templates/agent-sdk/src/server.ts +19 -4
  65. package/templates/agent-sdk/volute-template.json +1 -1
  66. package/templates/pi/.init/.config/routes.json +5 -0
  67. package/templates/pi/.init/AGENTS.md +1 -1
  68. package/templates/pi/src/agent.ts +279 -58
  69. package/templates/pi/src/server.ts +15 -4
  70. package/templates/pi/volute-template.json +1 -1
  71. package/dist/channel-7FZ6D25H.js +0 -90
  72. package/dist/chunk-N4YNKR3Q.js +0 -90
  73. package/dist/connector-TVJULIRT.js +0 -96
  74. package/dist/create-BRG2DBWI.js +0 -79
  75. package/dist/send-UK3JBZIB.js +0 -53
  76. package/dist/web-assets/assets/index-BC5eSqbY.js +0 -296
  77. package/templates/_base/src/lib/sessions.ts +0 -71
  78. package/templates/agent-sdk/.init/.config/sessions.json +0 -4
  79. package/templates/agent-sdk/src/lib/agent-sessions.ts +0 -204
  80. package/templates/pi/.init/.config/sessions.json +0 -1
  81. package/templates/pi/src/lib/agent-sessions.ts +0 -210
  82. package/dist/{service-SA4TTMDU.js → service-HZNIDNJF.js} +3 -3
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/connectors/sdk.ts
4
+ import { existsSync, readFileSync } from "fs";
5
+ import { resolve } from "path";
6
+ function loadEnv() {
7
+ const agentPort = process.env.VOLUTE_AGENT_PORT;
8
+ const agentName = process.env.VOLUTE_AGENT_NAME;
9
+ if (!agentPort || !agentName) {
10
+ console.error("Missing required env vars: VOLUTE_AGENT_PORT, VOLUTE_AGENT_NAME");
11
+ process.exit(1);
12
+ }
13
+ const agentDir = process.env.VOLUTE_AGENT_DIR;
14
+ const daemonUrl = process.env.VOLUTE_DAEMON_URL;
15
+ const daemonToken = process.env.VOLUTE_DAEMON_TOKEN;
16
+ const baseUrl = daemonUrl ? `${daemonUrl}/api/agents/${encodeURIComponent(agentName)}` : `http://127.0.0.1:${agentPort}`;
17
+ return { agentPort, agentName, agentDir, baseUrl, daemonUrl, daemonToken };
18
+ }
19
+ function loadFollowedChannels(env, platform) {
20
+ if (!env.agentDir) return [];
21
+ const configPath = resolve(env.agentDir, "home/.config/volute.json");
22
+ if (!existsSync(configPath)) return [];
23
+ try {
24
+ const config = JSON.parse(readFileSync(configPath, "utf-8"));
25
+ const platformConfig = config[platform];
26
+ return platformConfig?.channels ?? platformConfig?.chats ?? [];
27
+ } catch (err) {
28
+ console.warn(`Failed to load agent config: ${err}`);
29
+ return [];
30
+ }
31
+ }
32
+ function splitMessage(text, maxLength) {
33
+ const chunks = [];
34
+ while (text.length > maxLength) {
35
+ let splitAt = text.lastIndexOf("\n", maxLength);
36
+ if (splitAt < maxLength / 2) splitAt = maxLength;
37
+ chunks.push(text.slice(0, splitAt));
38
+ text = text.slice(splitAt).replace(/^\n/, "");
39
+ }
40
+ if (text) chunks.push(text);
41
+ return chunks;
42
+ }
43
+ async function* readNdjson(body) {
44
+ const reader = body.getReader();
45
+ const decoder = new TextDecoder();
46
+ let buffer = "";
47
+ try {
48
+ while (true) {
49
+ const { done, value } = await reader.read();
50
+ if (done) break;
51
+ buffer += decoder.decode(value, { stream: true });
52
+ const lines = buffer.split("\n");
53
+ buffer = lines.pop() || "";
54
+ for (const line of lines) {
55
+ if (!line.trim()) continue;
56
+ try {
57
+ yield JSON.parse(line);
58
+ } catch {
59
+ console.warn(`ndjson: skipping invalid line: ${line.slice(0, 100)}`);
60
+ }
61
+ }
62
+ }
63
+ if (buffer.trim()) {
64
+ try {
65
+ yield JSON.parse(buffer);
66
+ } catch {
67
+ console.warn(`ndjson: skipping invalid line: ${buffer.slice(0, 100)}`);
68
+ }
69
+ }
70
+ } finally {
71
+ reader.releaseLock();
72
+ }
73
+ }
74
+ function getHeaders(env) {
75
+ const headers = { "Content-Type": "application/json" };
76
+ if (env.daemonUrl && env.daemonToken) {
77
+ headers.Authorization = `Bearer ${env.daemonToken}`;
78
+ headers.Origin = env.daemonUrl;
79
+ }
80
+ return headers;
81
+ }
82
+ function onShutdown(cleanup) {
83
+ const handler = () => {
84
+ Promise.resolve(cleanup()).then(
85
+ () => process.exit(0),
86
+ (err) => {
87
+ console.error(`Shutdown error: ${err}`);
88
+ process.exit(1);
89
+ }
90
+ );
91
+ };
92
+ process.on("SIGINT", handler);
93
+ process.on("SIGTERM", handler);
94
+ }
95
+ async function fireAndForget(env, payload) {
96
+ try {
97
+ const res = await fetch(`${env.baseUrl}/message`, {
98
+ method: "POST",
99
+ headers: getHeaders(env),
100
+ body: JSON.stringify(payload)
101
+ });
102
+ if (!res.ok) {
103
+ console.error(`fireAndForget: agent returned ${res.status}`);
104
+ }
105
+ if (res.body) {
106
+ const reader = res.body.getReader();
107
+ while (!(await reader.read()).done) {
108
+ }
109
+ }
110
+ } catch (err) {
111
+ console.error(`Failed to forward message: ${err}`);
112
+ }
113
+ }
114
+ async function handleAgentMessage(env, payload, handlers) {
115
+ try {
116
+ const res = await fetch(`${env.baseUrl}/message`, {
117
+ method: "POST",
118
+ headers: getHeaders(env),
119
+ body: JSON.stringify(payload)
120
+ });
121
+ if (!res.ok) {
122
+ const body = await res.text().catch(() => "");
123
+ console.error(`Agent returned ${res.status}: ${body}`);
124
+ await handlers.onError(`Error: agent returned ${res.status}`);
125
+ return;
126
+ }
127
+ if (!res.body) {
128
+ await handlers.onError("Error: no response from agent");
129
+ return;
130
+ }
131
+ let accumulated = "";
132
+ const pendingImages = [];
133
+ for await (const event of readNdjson(res.body)) {
134
+ if (event.type === "text") {
135
+ accumulated += event.content;
136
+ } else if (event.type === "image") {
137
+ pendingImages.push({ data: event.data, media_type: event.media_type });
138
+ } else if (event.type === "tool_use") {
139
+ const text2 = accumulated.trim();
140
+ accumulated = "";
141
+ const images2 = pendingImages.splice(0);
142
+ if (text2 || images2.length > 0) {
143
+ await handlers.onFlush(text2, images2);
144
+ }
145
+ } else if (event.type === "done") {
146
+ break;
147
+ }
148
+ }
149
+ const text = accumulated.trim();
150
+ const images = pendingImages.splice(0);
151
+ if (text || images.length > 0) {
152
+ await handlers.onFlush(text, images);
153
+ }
154
+ } catch (err) {
155
+ console.error(`Failed to reach agent at ${env.baseUrl}/message:`, err);
156
+ const errMsg = err instanceof TypeError && err.cause?.code === "ECONNREFUSED" ? "Agent is not running" : `Error: ${err}`;
157
+ await handlers.onError(errMsg);
158
+ }
159
+ }
160
+
161
+ export {
162
+ loadEnv,
163
+ loadFollowedChannels,
164
+ splitMessage,
165
+ onShutdown,
166
+ fireAndForget,
167
+ handleAgentMessage
168
+ };
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ voluteHome
4
+ } from "./chunk-UX25Z2ND.js";
5
+ import {
6
+ __export
7
+ } from "./chunk-K3NQKI34.js";
8
+
9
+ // src/lib/channels/discord.ts
10
+ var discord_exports = {};
11
+ __export(discord_exports, {
12
+ read: () => read,
13
+ send: () => send
14
+ });
15
+ var API_BASE = "https://discord.com/api/v10";
16
+ function requireToken(env) {
17
+ const token = env.DISCORD_TOKEN;
18
+ if (!token) throw new Error("DISCORD_TOKEN not set");
19
+ return token;
20
+ }
21
+ async function read(env, channelId, limit) {
22
+ const token = requireToken(env);
23
+ const res = await fetch(`${API_BASE}/channels/${channelId}/messages?limit=${limit}`, {
24
+ headers: { Authorization: `Bot ${token}` }
25
+ });
26
+ if (!res.ok) {
27
+ throw new Error(`Discord API error: ${res.status} ${res.statusText}`);
28
+ }
29
+ const messages = await res.json();
30
+ return messages.reverse().map((m) => `${m.author.username}: ${m.content}`).join("\n");
31
+ }
32
+ async function send(env, channelId, message) {
33
+ const token = requireToken(env);
34
+ const res = await fetch(`${API_BASE}/channels/${channelId}/messages`, {
35
+ method: "POST",
36
+ headers: {
37
+ Authorization: `Bot ${token}`,
38
+ "Content-Type": "application/json"
39
+ },
40
+ body: JSON.stringify({ content: message })
41
+ });
42
+ if (!res.ok) {
43
+ throw new Error(`Discord API error: ${res.status} ${res.statusText}`);
44
+ }
45
+ }
46
+
47
+ // src/lib/channels/slack.ts
48
+ var slack_exports = {};
49
+ __export(slack_exports, {
50
+ read: () => read2,
51
+ send: () => send2
52
+ });
53
+ var API_BASE2 = "https://slack.com/api";
54
+ function requireToken2(env) {
55
+ const token = env.SLACK_BOT_TOKEN;
56
+ if (!token) throw new Error("SLACK_BOT_TOKEN not set");
57
+ return token;
58
+ }
59
+ async function slackApi(token, method, body) {
60
+ const res = await fetch(`${API_BASE2}/${method}`, {
61
+ method: "POST",
62
+ headers: {
63
+ Authorization: `Bearer ${token}`,
64
+ "Content-Type": "application/json"
65
+ },
66
+ body: JSON.stringify(body)
67
+ });
68
+ if (!res.ok) {
69
+ throw new Error(`Slack API HTTP error: ${res.status} ${res.statusText}`);
70
+ }
71
+ const data = await res.json();
72
+ if (!data.ok) {
73
+ throw new Error(`Slack API error: ${data.error}`);
74
+ }
75
+ return data;
76
+ }
77
+ async function read2(env, channelId, limit) {
78
+ const token = requireToken2(env);
79
+ const data = await slackApi(token, "conversations.history", {
80
+ channel: channelId,
81
+ limit
82
+ });
83
+ return data.messages.reverse().map((m) => `${m.user ?? m.bot_id ?? "unknown"}: ${m.text}`).join("\n");
84
+ }
85
+ async function send2(env, channelId, message) {
86
+ const token = requireToken2(env);
87
+ await slackApi(token, "chat.postMessage", {
88
+ channel: channelId,
89
+ text: message
90
+ });
91
+ }
92
+
93
+ // src/lib/channels/telegram.ts
94
+ var telegram_exports = {};
95
+ __export(telegram_exports, {
96
+ read: () => read3,
97
+ send: () => send3
98
+ });
99
+ var API_BASE3 = "https://api.telegram.org";
100
+ function requireToken3(env) {
101
+ const token = env.TELEGRAM_BOT_TOKEN;
102
+ if (!token) throw new Error("TELEGRAM_BOT_TOKEN not set");
103
+ return token;
104
+ }
105
+ async function read3(_env, _channelId, _limit) {
106
+ throw new Error(
107
+ "Telegram Bot API does not support reading chat history. Use volute channel send instead."
108
+ );
109
+ }
110
+ async function send3(env, chatId, message) {
111
+ const token = requireToken3(env);
112
+ const res = await fetch(`${API_BASE3}/bot${token}/sendMessage`, {
113
+ method: "POST",
114
+ headers: { "Content-Type": "application/json" },
115
+ body: JSON.stringify({ chat_id: chatId, text: message })
116
+ });
117
+ if (!res.ok) {
118
+ const body = await res.text().catch(() => "");
119
+ throw new Error(`Telegram API error: ${res.status} ${body}`);
120
+ }
121
+ }
122
+
123
+ // src/lib/channels/volute.ts
124
+ var volute_exports = {};
125
+ __export(volute_exports, {
126
+ read: () => read4,
127
+ send: () => send4
128
+ });
129
+ import { existsSync, readFileSync } from "fs";
130
+ import { resolve } from "path";
131
+ function getDaemonConfig() {
132
+ const configPath = resolve(voluteHome(), "daemon.json");
133
+ if (!existsSync(configPath)) {
134
+ throw new Error("Volute daemon is not running");
135
+ }
136
+ let config;
137
+ try {
138
+ config = JSON.parse(readFileSync(configPath, "utf-8"));
139
+ } catch (err) {
140
+ throw new Error(`Failed to parse ${configPath}: ${err}`);
141
+ }
142
+ if (typeof config.port !== "number") {
143
+ throw new Error(`Invalid or missing port in ${configPath}`);
144
+ }
145
+ const url = new URL("http://localhost");
146
+ url.hostname = config.hostname || "localhost";
147
+ url.port = String(config.port);
148
+ return { url: url.origin, token: config.token };
149
+ }
150
+ async function read4(env, conversationId, limit) {
151
+ const agentName = env.VOLUTE_AGENT;
152
+ if (!agentName) throw new Error("VOLUTE_AGENT not set");
153
+ const { url, token } = getDaemonConfig();
154
+ const headers = { Origin: url };
155
+ if (token) headers.Authorization = `Bearer ${token}`;
156
+ const res = await fetch(
157
+ `${url}/api/agents/${encodeURIComponent(agentName)}/conversations/${encodeURIComponent(conversationId)}/messages`,
158
+ { headers }
159
+ );
160
+ if (!res.ok) {
161
+ throw new Error(`Failed to read conversation: ${res.status} ${res.statusText}`);
162
+ }
163
+ const messages = await res.json();
164
+ return messages.slice(-limit).map((m) => {
165
+ const text = Array.isArray(m.content) ? m.content.filter((b) => b.type === "text").map((b) => b.text).join("") : m.content;
166
+ return `${m.sender_name ?? m.role}: ${text}`;
167
+ }).join("\n");
168
+ }
169
+ async function send4(env, conversationId, message) {
170
+ const agentName = env.VOLUTE_AGENT;
171
+ if (!agentName) throw new Error("VOLUTE_AGENT not set");
172
+ const { url, token } = getDaemonConfig();
173
+ const headers = {
174
+ "Content-Type": "application/json",
175
+ Origin: url
176
+ };
177
+ if (token) headers.Authorization = `Bearer ${token}`;
178
+ const res = await fetch(`${url}/api/agents/${encodeURIComponent(agentName)}/chat`, {
179
+ method: "POST",
180
+ headers,
181
+ body: JSON.stringify({ message, conversationId, sender: agentName })
182
+ });
183
+ if (!res.ok) {
184
+ const data = await res.json().catch(() => ({}));
185
+ throw new Error(data.error ?? `Failed to send: ${res.status}`);
186
+ }
187
+ if (res.body) {
188
+ const reader = res.body.getReader();
189
+ while (true) {
190
+ const { done } = await reader.read();
191
+ if (done) break;
192
+ }
193
+ }
194
+ }
195
+
196
+ // src/lib/channels.ts
197
+ var CHANNELS = {
198
+ volute: { name: "volute", displayName: "Volute", showToolCalls: true, driver: volute_exports },
199
+ discord: {
200
+ name: "discord",
201
+ displayName: "Discord",
202
+ showToolCalls: false,
203
+ driver: discord_exports
204
+ },
205
+ slack: {
206
+ name: "slack",
207
+ displayName: "Slack",
208
+ showToolCalls: false,
209
+ driver: slack_exports
210
+ },
211
+ telegram: {
212
+ name: "telegram",
213
+ displayName: "Telegram",
214
+ showToolCalls: false,
215
+ driver: telegram_exports
216
+ },
217
+ system: { name: "system", displayName: "System", showToolCalls: false }
218
+ };
219
+ function getChannelDriver(platform) {
220
+ return CHANNELS[platform]?.driver ?? null;
221
+ }
222
+
223
+ export {
224
+ CHANNELS,
225
+ getChannelDriver
226
+ };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  validateAgentName
4
- } from "./chunk-3C2XR4IY.js";
4
+ } from "./chunk-UX25Z2ND.js";
5
5
 
6
6
  // src/lib/isolation.ts
7
7
  import { execFile, execFileSync } from "child_process";
@@ -1,27 +1,131 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/lib/variants.ts
4
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
4
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
5
+ import { resolve as resolve2 } from "path";
6
+
7
+ // src/lib/registry.ts
8
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
5
9
  import { homedir } from "os";
6
- import { resolve } from "path";
10
+ import { dirname, resolve } from "path";
11
+ import { fileURLToPath } from "url";
7
12
  function voluteHome() {
8
- return process.env.VOLUTE_HOME || resolve(homedir(), ".volute");
13
+ if (process.env.VOLUTE_HOME) return process.env.VOLUTE_HOME;
14
+ const dir = dirname(fileURLToPath(import.meta.url));
15
+ if (dir.endsWith("/src/lib")) {
16
+ throw new Error(
17
+ 'VOLUTE_HOME must be set when running from source. For tests, run via "npm test" or add "--import ./test/setup.ts".'
18
+ );
19
+ }
20
+ return resolve(homedir(), ".volute");
21
+ }
22
+ function ensureVoluteHome() {
23
+ mkdirSync(resolve(voluteHome(), "agents"), { recursive: true });
24
+ }
25
+ function readRegistry() {
26
+ const registryPath = resolve(voluteHome(), "agents.json");
27
+ if (!existsSync(registryPath)) return [];
28
+ try {
29
+ const entries = JSON.parse(readFileSync(registryPath, "utf-8"));
30
+ return entries.map((e) => ({ ...e, running: e.running ?? false }));
31
+ } catch {
32
+ return [];
33
+ }
34
+ }
35
+ function writeRegistry(entries) {
36
+ ensureVoluteHome();
37
+ const registryPath = resolve(voluteHome(), "agents.json");
38
+ const tmpPath = `${registryPath}.tmp`;
39
+ writeFileSync(tmpPath, `${JSON.stringify(entries, null, 2)}
40
+ `);
41
+ renameSync(tmpPath, registryPath);
42
+ }
43
+ var AGENT_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
44
+ var AGENT_NAME_MAX = 64;
45
+ function validateAgentName(name) {
46
+ if (!name) return "Agent name is required";
47
+ if (name.length > AGENT_NAME_MAX)
48
+ return `Agent name must be at most ${AGENT_NAME_MAX} characters`;
49
+ if (!AGENT_NAME_RE.test(name)) {
50
+ return "Agent name must start with alphanumeric and contain only alphanumeric, dots, dashes, or underscores";
51
+ }
52
+ return null;
53
+ }
54
+ function addAgent(name, port) {
55
+ const err = validateAgentName(name);
56
+ if (err) throw new Error(err);
57
+ const entries = readRegistry();
58
+ const filtered = entries.filter((e) => e.name !== name);
59
+ filtered.push({ name, port, created: (/* @__PURE__ */ new Date()).toISOString(), running: false });
60
+ writeRegistry(filtered);
9
61
  }
62
+ function removeAgent(name) {
63
+ const entries = readRegistry();
64
+ writeRegistry(entries.filter((e) => e.name !== name));
65
+ }
66
+ function setAgentRunning(name, running) {
67
+ const entries = readRegistry();
68
+ const entry = entries.find((e) => e.name === name);
69
+ if (entry) {
70
+ entry.running = running;
71
+ writeRegistry(entries);
72
+ }
73
+ }
74
+ function findAgent(name) {
75
+ return readRegistry().find((e) => e.name === name);
76
+ }
77
+ function agentDir(name) {
78
+ return resolve(voluteHome(), "agents", name);
79
+ }
80
+ function nextPort() {
81
+ const entries = readRegistry();
82
+ const usedPorts = new Set(entries.map((e) => e.port));
83
+ for (const entry of entries) {
84
+ for (const v of readVariants(entry.name)) {
85
+ if (v.port) usedPorts.add(v.port);
86
+ }
87
+ }
88
+ let port = 4100;
89
+ while (usedPorts.has(port)) port++;
90
+ if (port > 65535) throw new Error("No available ports \u2014 all ports 4100-65535 are allocated");
91
+ return port;
92
+ }
93
+ function resolveAgent(name) {
94
+ const [baseName, variantName] = name.split("@", 2);
95
+ const entry = findAgent(baseName);
96
+ if (!entry) {
97
+ throw new Error(`Unknown agent: ${baseName}`);
98
+ }
99
+ const dir = agentDir(baseName);
100
+ if (!existsSync(dir)) {
101
+ throw new Error(`Agent directory missing: ${dir}`);
102
+ }
103
+ if (variantName) {
104
+ const variant = findVariant(baseName, variantName);
105
+ if (!variant) {
106
+ throw new Error(`Unknown variant: ${variantName} (agent: ${baseName})`);
107
+ }
108
+ return { entry: { ...entry, port: variant.port }, dir: variant.path };
109
+ }
110
+ return { entry, dir };
111
+ }
112
+
113
+ // src/lib/variants.ts
10
114
  function variantsPath() {
11
- return resolve(voluteHome(), "variants.json");
115
+ return resolve2(voluteHome(), "variants.json");
12
116
  }
13
117
  function readAllVariants() {
14
118
  const path = variantsPath();
15
- if (!existsSync(path)) return {};
119
+ if (!existsSync2(path)) return {};
16
120
  try {
17
- return JSON.parse(readFileSync(path, "utf-8"));
121
+ return JSON.parse(readFileSync2(path, "utf-8"));
18
122
  } catch {
19
123
  return {};
20
124
  }
21
125
  }
22
126
  function writeAllVariants(all) {
23
- mkdirSync(voluteHome(), { recursive: true });
24
- writeFileSync(variantsPath(), `${JSON.stringify(all, null, 2)}
127
+ mkdirSync2(voluteHome(), { recursive: true });
128
+ writeFileSync2(variantsPath(), `${JSON.stringify(all, null, 2)}
25
129
  `);
26
130
  }
27
131
  function readVariants(agentName) {
@@ -102,104 +206,6 @@ function validateBranchName(branch) {
102
206
  return null;
103
207
  }
104
208
 
105
- // src/lib/registry.ts
106
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync, writeFileSync as writeFileSync2 } from "fs";
107
- import { homedir as homedir2 } from "os";
108
- import { resolve as resolve2 } from "path";
109
- function voluteHome2() {
110
- return process.env.VOLUTE_HOME || resolve2(homedir2(), ".volute");
111
- }
112
- function ensureVoluteHome() {
113
- mkdirSync2(resolve2(voluteHome2(), "agents"), { recursive: true });
114
- }
115
- function readRegistry() {
116
- const registryPath = resolve2(voluteHome2(), "agents.json");
117
- if (!existsSync2(registryPath)) return [];
118
- try {
119
- const entries = JSON.parse(readFileSync2(registryPath, "utf-8"));
120
- return entries.map((e) => ({ ...e, running: e.running ?? false }));
121
- } catch {
122
- return [];
123
- }
124
- }
125
- function writeRegistry(entries) {
126
- ensureVoluteHome();
127
- const registryPath = resolve2(voluteHome2(), "agents.json");
128
- const tmpPath = `${registryPath}.tmp`;
129
- writeFileSync2(tmpPath, `${JSON.stringify(entries, null, 2)}
130
- `);
131
- renameSync(tmpPath, registryPath);
132
- }
133
- var AGENT_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
134
- var AGENT_NAME_MAX = 64;
135
- function validateAgentName(name) {
136
- if (!name) return "Agent name is required";
137
- if (name.length > AGENT_NAME_MAX)
138
- return `Agent name must be at most ${AGENT_NAME_MAX} characters`;
139
- if (!AGENT_NAME_RE.test(name)) {
140
- return "Agent name must start with alphanumeric and contain only alphanumeric, dots, dashes, or underscores";
141
- }
142
- return null;
143
- }
144
- function addAgent(name, port) {
145
- const err = validateAgentName(name);
146
- if (err) throw new Error(err);
147
- const entries = readRegistry();
148
- const filtered = entries.filter((e) => e.name !== name);
149
- filtered.push({ name, port, created: (/* @__PURE__ */ new Date()).toISOString(), running: false });
150
- writeRegistry(filtered);
151
- }
152
- function removeAgent(name) {
153
- const entries = readRegistry();
154
- writeRegistry(entries.filter((e) => e.name !== name));
155
- }
156
- function setAgentRunning(name, running) {
157
- const entries = readRegistry();
158
- const entry = entries.find((e) => e.name === name);
159
- if (entry) {
160
- entry.running = running;
161
- writeRegistry(entries);
162
- }
163
- }
164
- function findAgent(name) {
165
- return readRegistry().find((e) => e.name === name);
166
- }
167
- function agentDir(name) {
168
- return resolve2(voluteHome2(), "agents", name);
169
- }
170
- function nextPort() {
171
- const entries = readRegistry();
172
- const usedPorts = new Set(entries.map((e) => e.port));
173
- for (const entry of entries) {
174
- for (const v of readVariants(entry.name)) {
175
- if (v.port) usedPorts.add(v.port);
176
- }
177
- }
178
- let port = 4100;
179
- while (usedPorts.has(port)) port++;
180
- if (port > 65535) throw new Error("No available ports \u2014 all ports 4100-65535 are allocated");
181
- return port;
182
- }
183
- function resolveAgent(name) {
184
- const [baseName, variantName] = name.split("@", 2);
185
- const entry = findAgent(baseName);
186
- if (!entry) {
187
- throw new Error(`Unknown agent: ${baseName}`);
188
- }
189
- const dir = agentDir(baseName);
190
- if (!existsSync2(dir)) {
191
- throw new Error(`Agent directory missing: ${dir}`);
192
- }
193
- if (variantName) {
194
- const variant = findVariant(baseName, variantName);
195
- if (!variant) {
196
- throw new Error(`Unknown variant: ${variantName} (agent: ${baseName})`);
197
- }
198
- return { entry: { ...entry, port: variant.port }, dir: variant.path };
199
- }
200
- return { entry, dir };
201
- }
202
-
203
209
  export {
204
210
  readVariants,
205
211
  writeVariants,
@@ -211,7 +217,7 @@ export {
211
217
  removeAllVariants,
212
218
  checkHealth,
213
219
  validateBranchName,
214
- voluteHome2 as voluteHome,
220
+ voluteHome,
215
221
  ensureVoluteHome,
216
222
  readRegistry,
217
223
  validateAgentName,
@@ -116,5 +116,6 @@ export {
116
116
  findTemplatesRoot,
117
117
  composeTemplate,
118
118
  copyTemplateToDir,
119
- applyInitFiles
119
+ applyInitFiles,
120
+ listFiles
120
121
  };
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/lib/read-stdin.ts
4
+ import { isatty } from "tty";
5
+ async function readStdin() {
6
+ if (isatty(0)) return void 0;
7
+ const chunks = [];
8
+ try {
9
+ for await (const chunk of process.stdin) {
10
+ chunks.push(chunk);
11
+ }
12
+ } catch (err) {
13
+ console.error(`Failed to read from stdin: ${err instanceof Error ? err.message : String(err)}`);
14
+ process.exit(1);
15
+ }
16
+ const text = Buffer.concat(chunks).toString().replace(/\r?\n$/, "");
17
+ return text || void 0;
18
+ }
19
+
20
+ export {
21
+ readStdin
22
+ };