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
@@ -0,0 +1,213 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/connectors/discord.ts
4
+ import {
5
+ AttachmentBuilder,
6
+ Client,
7
+ Events,
8
+ GatewayIntentBits,
9
+ Partials
10
+ } from "discord.js";
11
+ var DISCORD_MAX_LENGTH = 2e3;
12
+ var TYPING_INTERVAL_MS = 8e3;
13
+ var agentPort = process.env.VOLUTE_AGENT_PORT;
14
+ var agentName = process.env.VOLUTE_AGENT_NAME;
15
+ var token = process.env.DISCORD_TOKEN;
16
+ if (!agentPort || !agentName) {
17
+ console.error("Missing required env vars: VOLUTE_AGENT_PORT, VOLUTE_AGENT_NAME");
18
+ process.exit(1);
19
+ }
20
+ if (!token) {
21
+ console.error("Missing required env var: DISCORD_TOKEN");
22
+ process.exit(1);
23
+ }
24
+ var guildId = process.env.DISCORD_GUILD_ID;
25
+ var baseUrl = `http://localhost:${agentPort}`;
26
+ var client = new Client({
27
+ intents: [
28
+ GatewayIntentBits.Guilds,
29
+ GatewayIntentBits.GuildMessages,
30
+ GatewayIntentBits.MessageContent,
31
+ GatewayIntentBits.DirectMessages
32
+ ],
33
+ partials: [Partials.Channel]
34
+ });
35
+ function shutdown() {
36
+ client.destroy();
37
+ process.exit(0);
38
+ }
39
+ process.on("SIGINT", shutdown);
40
+ process.on("SIGTERM", shutdown);
41
+ client.once(Events.ClientReady, (c) => {
42
+ console.log(`Connected to Discord as ${c.user.tag}`);
43
+ console.log(`Bridging to agent: ${agentName} (port ${agentPort})`);
44
+ });
45
+ client.on(Events.MessageCreate, async (message) => {
46
+ if (message.author.bot) return;
47
+ const isDM = !message.guild;
48
+ const isMentioned = !isDM && message.mentions.has(client.user);
49
+ if (!isDM && !isMentioned) return;
50
+ let text = message.content;
51
+ if (isMentioned) {
52
+ text = text.replace(new RegExp(`<@!?${client.user.id}>`, "g"), "").trim();
53
+ }
54
+ const content = [];
55
+ if (text) content.push({ type: "text", text });
56
+ for (const attachment of message.attachments.values()) {
57
+ if (!attachment.contentType?.startsWith("image/")) continue;
58
+ try {
59
+ const res = await fetch(attachment.url);
60
+ const buffer = Buffer.from(await res.arrayBuffer());
61
+ content.push({
62
+ type: "image",
63
+ media_type: attachment.contentType,
64
+ data: buffer.toString("base64")
65
+ });
66
+ } catch (err) {
67
+ console.error(`Failed to download attachment: ${err}`);
68
+ }
69
+ }
70
+ if (content.length === 0) return;
71
+ await handleAgentRequest(message, content);
72
+ });
73
+ client.login(token);
74
+ function splitMessage(text) {
75
+ const chunks = [];
76
+ while (text.length > DISCORD_MAX_LENGTH) {
77
+ let splitAt = text.lastIndexOf("\n", DISCORD_MAX_LENGTH);
78
+ if (splitAt < DISCORD_MAX_LENGTH / 2) splitAt = DISCORD_MAX_LENGTH;
79
+ chunks.push(text.slice(0, splitAt));
80
+ text = text.slice(splitAt).replace(/^\n/, "");
81
+ }
82
+ if (text) chunks.push(text);
83
+ return chunks;
84
+ }
85
+ async function* readNdjson(body) {
86
+ const reader = body.getReader();
87
+ const decoder = new TextDecoder();
88
+ let buffer = "";
89
+ try {
90
+ while (true) {
91
+ const { done, value } = await reader.read();
92
+ if (done) break;
93
+ buffer += decoder.decode(value, { stream: true });
94
+ const lines = buffer.split("\n");
95
+ buffer = lines.pop() || "";
96
+ for (const line of lines) {
97
+ if (!line.trim()) continue;
98
+ try {
99
+ yield JSON.parse(line);
100
+ } catch {
101
+ }
102
+ }
103
+ }
104
+ if (buffer.trim()) {
105
+ try {
106
+ yield JSON.parse(buffer);
107
+ } catch {
108
+ }
109
+ }
110
+ } finally {
111
+ reader.releaseLock();
112
+ }
113
+ }
114
+ async function handleAgentRequest(message, content) {
115
+ const channel = message.channel;
116
+ if (!("sendTyping" in channel)) return;
117
+ const typingInterval = setInterval(() => {
118
+ channel.sendTyping().catch(() => {
119
+ });
120
+ }, TYPING_INTERVAL_MS);
121
+ channel.sendTyping().catch(() => {
122
+ });
123
+ let accumulated = "";
124
+ const pendingImages = [];
125
+ let replied = false;
126
+ async function flush() {
127
+ const text = accumulated.trim();
128
+ accumulated = "";
129
+ if (!text && pendingImages.length === 0) return;
130
+ const chunks = text ? splitMessage(text) : [];
131
+ const imageFiles = pendingImages.splice(0).map((img, i) => {
132
+ const ext = img.media_type.split("/")[1] || "png";
133
+ return new AttachmentBuilder(Buffer.from(img.data, "base64"), {
134
+ name: `image-${i}.${ext}`
135
+ });
136
+ });
137
+ if (chunks.length === 0 && imageFiles.length > 0) {
138
+ const sendFn = replied ? channel.send.bind(channel) : message.reply.bind(message);
139
+ await sendFn({ content: "\u200B", files: imageFiles }).catch((err) => {
140
+ console.error(`Failed to send message: ${err}`);
141
+ });
142
+ replied = true;
143
+ return;
144
+ }
145
+ for (let i = 0; i < chunks.length; i++) {
146
+ const isLast = i === chunks.length - 1;
147
+ const opts = {
148
+ content: chunks[i]
149
+ };
150
+ if (isLast && imageFiles.length > 0) opts.files = imageFiles;
151
+ try {
152
+ if (!replied) {
153
+ await message.reply(opts);
154
+ replied = true;
155
+ } else {
156
+ await channel.send(opts);
157
+ }
158
+ } catch (err) {
159
+ console.error(`Failed to send message: ${err}`);
160
+ }
161
+ }
162
+ }
163
+ const senderName = message.author.displayName || message.author.username;
164
+ const channelKey = `discord:${message.channelId}`;
165
+ const isDM = !message.guild;
166
+ const channelName = !isDM && "name" in message.channel ? message.channel.name : null;
167
+ try {
168
+ const res = await fetch(`${baseUrl}/message`, {
169
+ method: "POST",
170
+ headers: { "Content-Type": "application/json" },
171
+ body: JSON.stringify({
172
+ content,
173
+ channel: channelKey,
174
+ sender: senderName,
175
+ platform: "Discord",
176
+ ...isDM ? { isDM: true } : {},
177
+ ...channelName ? { channelName } : {},
178
+ ...message.guild?.name ? { guildName: message.guild.name } : {}
179
+ })
180
+ });
181
+ if (!res.ok) {
182
+ await message.reply(`Error: agent returned ${res.status}`);
183
+ clearInterval(typingInterval);
184
+ return;
185
+ }
186
+ if (!res.body) {
187
+ await message.reply("Error: no response from agent");
188
+ clearInterval(typingInterval);
189
+ return;
190
+ }
191
+ for await (const event of readNdjson(res.body)) {
192
+ if (event.type === "text") {
193
+ accumulated += event.content;
194
+ } else if (event.type === "image") {
195
+ pendingImages.push({
196
+ data: event.data,
197
+ media_type: event.media_type
198
+ });
199
+ } else if (event.type === "tool_use") {
200
+ await flush();
201
+ } else if (event.type === "done") {
202
+ break;
203
+ }
204
+ }
205
+ await flush();
206
+ } catch (err) {
207
+ const errMsg = err instanceof TypeError && err.cause?.code === "ECONNREFUSED" ? "Agent is not running" : `Error: ${err}`;
208
+ await message.reply(errMsg).catch(() => {
209
+ });
210
+ } finally {
211
+ clearInterval(typingInterval);
212
+ }
213
+ }
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ applyInitFiles,
4
+ composeTemplate,
5
+ copyTemplateToDir,
6
+ findTemplatesRoot
7
+ } from "./chunk-GSPKUPKU.js";
8
+ import {
9
+ exec,
10
+ execInherit
11
+ } from "./chunk-XZN4WPNC.js";
12
+ import {
13
+ parseArgs
14
+ } from "./chunk-D424ZQGI.js";
15
+ import {
16
+ addAgent,
17
+ agentDir,
18
+ ensureVoluteHome,
19
+ nextPort
20
+ } from "./chunk-5YW4B7CG.js";
21
+
22
+ // src/commands/create.ts
23
+ import { existsSync, rmSync } from "fs";
24
+ async function run(args) {
25
+ const { positional, flags } = parseArgs(args, {
26
+ template: { type: "string" }
27
+ });
28
+ const name = positional[0];
29
+ const template = flags.template ?? "agent-sdk";
30
+ if (!name) {
31
+ console.error("Usage: volute create <name> [--template <name>]");
32
+ process.exit(1);
33
+ }
34
+ ensureVoluteHome();
35
+ const dest = agentDir(name);
36
+ if (existsSync(dest)) {
37
+ console.error(`Agent already exists: ${name}`);
38
+ process.exit(1);
39
+ }
40
+ const templatesRoot = findTemplatesRoot();
41
+ const { composedDir, manifest } = composeTemplate(templatesRoot, template);
42
+ try {
43
+ copyTemplateToDir(composedDir, dest, name, manifest);
44
+ applyInitFiles(dest);
45
+ } finally {
46
+ rmSync(composedDir, { recursive: true, force: true });
47
+ }
48
+ const port = nextPort();
49
+ addAgent(name, port);
50
+ console.log("Installing dependencies...");
51
+ await execInherit("npm", ["install"], { cwd: dest });
52
+ await exec("git", ["init"], { cwd: dest });
53
+ await exec("git", ["add", "-A"], { cwd: dest });
54
+ await exec("git", ["commit", "-m", "initial commit"], { cwd: dest });
55
+ console.log(`
56
+ Created agent: ${name} (port ${port})`);
57
+ console.log(`
58
+ volute start ${name}`);
59
+ }
60
+ export {
61
+ run
62
+ };
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ daemonFetch,
4
+ getDaemonUrl
5
+ } from "./chunk-H5XQARAP.js";
6
+ import "./chunk-5YW4B7CG.js";
7
+ export {
8
+ daemonFetch,
9
+ getDaemonUrl
10
+ };