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.
- package/README.md +29 -29
- package/dist/agent-Z2B6EFEQ.js +75 -0
- package/dist/{agent-manager-AUCKMGPR.js → agent-manager-PXBKA2GK.js} +4 -4
- package/dist/channel-MK5OK2SI.js +113 -0
- package/dist/chunk-5X7HGB6L.js +107 -0
- package/dist/{chunk-YGFIWIOF.js → chunk-7L4AN5D4.js} +1 -1
- package/dist/{chunk-VRVVQIYY.js → chunk-AZEL2IEK.js} +1 -1
- package/dist/chunk-B3R6L2GW.js +24 -0
- package/dist/{chunk-DNOXHLE5.js → chunk-HE67X4T6.js} +1 -1
- package/dist/{chunk-I6OHXCMV.js → chunk-MW2KFO3B.js} +47 -9
- package/dist/chunk-MXUCNIBG.js +168 -0
- package/dist/chunk-SMISE4SV.js +226 -0
- package/dist/{chunk-SOZA2TLP.js → chunk-UAVD2AHX.js} +1 -1
- package/dist/{chunk-3C2XR4IY.js → chunk-UX25Z2ND.js} +113 -107
- package/dist/{chunk-GSPKUPKU.js → chunk-XUA3JUFK.js} +2 -1
- package/dist/chunk-ZYGKG6VC.js +22 -0
- package/dist/cli.js +98 -75
- package/dist/connector-LYEMXQEV.js +157 -0
- package/dist/connectors/discord.js +104 -161
- package/dist/connectors/slack.js +179 -0
- package/dist/connectors/telegram.js +175 -0
- package/dist/conversation-ERXEQZTY.js +163 -0
- package/dist/create-RVCZN6HE.js +91 -0
- package/dist/{daemon-client-XR24PUJF.js → daemon-client-ZY6UUN2M.js} +2 -2
- package/dist/daemon.js +824 -252
- package/dist/{delete-GQ7JEK2S.js → delete-3QH7VYIN.js} +8 -9
- package/dist/{down-3OB6UVAJ.js → down-O7IFZLVJ.js} +1 -1
- package/dist/{env-JB27UAC3.js → env-4D4REPJF.js} +8 -5
- package/dist/{history-3VRUBGGV.js → history-OEONB53Z.js} +5 -5
- package/dist/{import-K4MP2GX7.js → import-MXJB2EII.js} +23 -8
- package/dist/{logs-NXFFGUKY.js → logs-DF342W4M.js} +2 -2
- package/dist/message-ADHWFHSI.js +32 -0
- package/dist/package-VQOE7JNH.js +89 -0
- package/dist/{schedule-4I5TYHFH.js → schedule-NAG6F463.js} +12 -7
- package/dist/send-66QMKRUH.js +75 -0
- package/dist/{setup-SRS7AUAA.js → setup-RPRRGG2F.js} +6 -6
- package/dist/{start-LDPMCMYT.js → start-TUOXDSFL.js} +3 -3
- package/dist/{status-MVSQG54T.js → status-A36EHRO4.js} +3 -3
- package/dist/{stop-5PZTZCLL.js → stop-AOJZLQ5X.js} +6 -7
- package/dist/{up-UT3IMKCA.js → up-7ILD7GU7.js} +2 -2
- package/dist/update-LPSIAWQ2.js +140 -0
- package/dist/update-check-Y33QDCFL.js +17 -0
- package/dist/{upgrade-CDKECCGN.js → upgrade-FX2TKJ2S.js} +16 -15
- package/dist/{variant-CVYM3EQG.js → variant-LAB67OC2.js} +17 -12
- package/dist/web-assets/assets/index-BbRmoxoA.js +308 -0
- package/dist/web-assets/index.html +2 -2
- package/drizzle/0003_clean_ego.sql +12 -0
- package/drizzle/meta/0003_snapshot.json +417 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +3 -1
- package/templates/_base/.init/.config/hooks/startup-context.sh +19 -1
- package/templates/_base/_skills/volute-agent/SKILL.md +112 -16
- package/templates/_base/home/.config/routes.json +10 -0
- package/templates/_base/home/VOLUTE.md +19 -28
- package/templates/_base/src/lib/file-handler.ts +46 -0
- package/templates/_base/src/lib/format-prefix.ts +1 -1
- package/templates/_base/src/lib/router.ts +327 -0
- package/templates/_base/src/lib/routing.ts +137 -0
- package/templates/_base/src/lib/types.ts +16 -3
- package/templates/_base/src/lib/volute-server.ts +20 -48
- package/templates/agent-sdk/.init/.config/routes.json +5 -0
- package/templates/agent-sdk/.init/CLAUDE.md +2 -2
- package/templates/agent-sdk/src/agent.ts +269 -82
- package/templates/agent-sdk/src/server.ts +19 -4
- package/templates/agent-sdk/volute-template.json +1 -1
- package/templates/pi/.init/.config/routes.json +5 -0
- package/templates/pi/.init/AGENTS.md +1 -1
- package/templates/pi/src/agent.ts +279 -58
- package/templates/pi/src/server.ts +15 -4
- package/templates/pi/volute-template.json +1 -1
- package/dist/channel-7FZ6D25H.js +0 -90
- package/dist/chunk-N4YNKR3Q.js +0 -90
- package/dist/connector-TVJULIRT.js +0 -96
- package/dist/create-BRG2DBWI.js +0 -79
- package/dist/send-UK3JBZIB.js +0 -53
- package/dist/web-assets/assets/index-BC5eSqbY.js +0 -296
- package/templates/_base/src/lib/sessions.ts +0 -71
- package/templates/agent-sdk/.init/.config/sessions.json +0 -4
- package/templates/agent-sdk/src/lib/agent-sessions.ts +0 -204
- package/templates/pi/.init/.config/sessions.json +0 -1
- package/templates/pi/src/lib/agent-sessions.ts +0 -210
- 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,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
|
-
|
|
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
|
|
115
|
+
return resolve2(voluteHome(), "variants.json");
|
|
12
116
|
}
|
|
13
117
|
function readAllVariants() {
|
|
14
118
|
const path = variantsPath();
|
|
15
|
-
if (!
|
|
119
|
+
if (!existsSync2(path)) return {};
|
|
16
120
|
try {
|
|
17
|
-
return JSON.parse(
|
|
121
|
+
return JSON.parse(readFileSync2(path, "utf-8"));
|
|
18
122
|
} catch {
|
|
19
123
|
return {};
|
|
20
124
|
}
|
|
21
125
|
}
|
|
22
126
|
function writeAllVariants(all) {
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
220
|
+
voluteHome,
|
|
215
221
|
ensureVoluteHome,
|
|
216
222
|
readRegistry,
|
|
217
223
|
validateAgentName,
|
|
@@ -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
|
+
};
|