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,120 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/lib/template.ts
4
+ import {
5
+ cpSync,
6
+ existsSync,
7
+ mkdirSync,
8
+ readdirSync,
9
+ readFileSync,
10
+ renameSync,
11
+ rmSync,
12
+ statSync,
13
+ writeFileSync
14
+ } from "fs";
15
+ import { tmpdir } from "os";
16
+ import { dirname, join, relative, resolve } from "path";
17
+ function findTemplatesRoot() {
18
+ let dir = dirname(new URL(import.meta.url).pathname);
19
+ for (let i = 0; i < 5; i++) {
20
+ const candidate = resolve(dir, "templates");
21
+ if (existsSync(resolve(candidate, "_base"))) return candidate;
22
+ dir = dirname(dir);
23
+ }
24
+ console.error(
25
+ "Templates directory not found. Searched up from:",
26
+ dirname(new URL(import.meta.url).pathname)
27
+ );
28
+ process.exit(1);
29
+ }
30
+ function composeTemplate(templatesRoot, templateName) {
31
+ const baseDir = resolve(templatesRoot, "_base");
32
+ const templateDir = resolve(templatesRoot, templateName);
33
+ if (!existsSync(baseDir)) {
34
+ console.error("Base template not found:", baseDir);
35
+ process.exit(1);
36
+ }
37
+ if (!existsSync(templateDir)) {
38
+ console.error(`Template not found: ${templateName}`);
39
+ process.exit(1);
40
+ }
41
+ const composedDir = resolve(tmpdir(), `volute-template-${Date.now()}`);
42
+ mkdirSync(composedDir, { recursive: true });
43
+ cpSync(baseDir, composedDir, { recursive: true });
44
+ for (const file of listFiles(templateDir)) {
45
+ const src = resolve(templateDir, file);
46
+ const dest = resolve(composedDir, file);
47
+ mkdirSync(dirname(dest), { recursive: true });
48
+ cpSync(src, dest);
49
+ }
50
+ const manifestPath = resolve(composedDir, "volute-template.json");
51
+ if (!existsSync(manifestPath)) {
52
+ rmSync(composedDir, { recursive: true, force: true });
53
+ console.error(`Template manifest not found: ${templateName}/volute-template.json`);
54
+ process.exit(1);
55
+ }
56
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
57
+ const skillsSrc = resolve(composedDir, "_skills");
58
+ if (existsSync(skillsSrc)) {
59
+ const skillsDest = resolve(composedDir, manifest.skillsDir);
60
+ mkdirSync(skillsDest, { recursive: true });
61
+ cpSync(skillsSrc, skillsDest, { recursive: true });
62
+ rmSync(skillsSrc, { recursive: true, force: true });
63
+ }
64
+ rmSync(manifestPath);
65
+ return { composedDir, manifest };
66
+ }
67
+ function copyTemplateToDir(composedDir, destDir, agentName, manifest) {
68
+ cpSync(composedDir, destDir, { recursive: true });
69
+ for (const [from, to] of Object.entries(manifest.rename)) {
70
+ const fromPath = resolve(destDir, from);
71
+ if (existsSync(fromPath)) {
72
+ renameSync(fromPath, resolve(destDir, to));
73
+ }
74
+ }
75
+ for (const file of manifest.substitute) {
76
+ const path = resolve(destDir, file);
77
+ if (existsSync(path)) {
78
+ const content = readFileSync(path, "utf-8");
79
+ writeFileSync(path, content.replaceAll("{{name}}", agentName));
80
+ }
81
+ }
82
+ }
83
+ function applyInitFiles(destDir) {
84
+ const initDir = resolve(destDir, ".init");
85
+ if (!existsSync(initDir)) return;
86
+ const homeDir = resolve(destDir, "home");
87
+ for (const file of listFiles(initDir)) {
88
+ const src = resolve(initDir, file);
89
+ const dest = resolve(homeDir, file);
90
+ const parent = dirname(dest);
91
+ if (!existsSync(parent)) {
92
+ mkdirSync(parent, { recursive: true });
93
+ }
94
+ cpSync(src, dest);
95
+ }
96
+ rmSync(initDir, { recursive: true, force: true });
97
+ }
98
+ function listFiles(dir) {
99
+ const results = [];
100
+ function walk(current) {
101
+ for (const entry of readdirSync(current)) {
102
+ const full = join(current, entry);
103
+ if (statSync(full).isDirectory()) {
104
+ if (entry === ".git") continue;
105
+ walk(full);
106
+ } else {
107
+ results.push(relative(dir, full));
108
+ }
109
+ }
110
+ }
111
+ walk(dir);
112
+ return results;
113
+ }
114
+
115
+ export {
116
+ findTemplatesRoot,
117
+ composeTemplate,
118
+ copyTemplateToDir,
119
+ applyInitFiles
120
+ };
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ VOLUTE_HOME
4
+ } from "./chunk-5YW4B7CG.js";
5
+
6
+ // src/lib/daemon-client.ts
7
+ import { existsSync, readFileSync } from "fs";
8
+ import { resolve } from "path";
9
+ function readDaemonConfig() {
10
+ const configPath = resolve(VOLUTE_HOME, "daemon.json");
11
+ if (!existsSync(configPath)) {
12
+ console.error("Volute is not running. Start with: volute up");
13
+ process.exit(1);
14
+ }
15
+ try {
16
+ return JSON.parse(readFileSync(configPath, "utf-8"));
17
+ } catch {
18
+ console.error("Volute is not running. Start with: volute up");
19
+ process.exit(1);
20
+ }
21
+ }
22
+ function getDaemonUrl() {
23
+ const config = readDaemonConfig();
24
+ return `http://localhost:${config.port}`;
25
+ }
26
+ async function daemonFetch(path, options) {
27
+ const config = readDaemonConfig();
28
+ const url = `http://localhost:${config.port}`;
29
+ const headers = new Headers(options?.headers);
30
+ if (config.token) {
31
+ headers.set("Authorization", `Bearer ${config.token}`);
32
+ }
33
+ headers.set("Origin", url);
34
+ try {
35
+ return await fetch(`${url}${path}`, { ...options, headers });
36
+ } catch (err) {
37
+ if (err instanceof TypeError && err.cause?.code === "ECONNREFUSED") {
38
+ console.error("Volute is not running. Start with: volute up");
39
+ process.exit(1);
40
+ }
41
+ throw err;
42
+ }
43
+ }
44
+
45
+ export {
46
+ getDaemonUrl,
47
+ daemonFetch
48
+ };
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/lib/log-buffer.ts
4
+ var LogBuffer = class {
5
+ entries = [];
6
+ maxSize = 1e3;
7
+ subscribers = /* @__PURE__ */ new Set();
8
+ append(entry) {
9
+ this.entries.push(entry);
10
+ if (this.entries.length > this.maxSize) {
11
+ this.entries.shift();
12
+ }
13
+ for (const sub of this.subscribers) {
14
+ sub(entry);
15
+ }
16
+ }
17
+ getEntries() {
18
+ return [...this.entries];
19
+ }
20
+ subscribe(fn) {
21
+ this.subscribers.add(fn);
22
+ return () => this.subscribers.delete(fn);
23
+ }
24
+ };
25
+ var logBuffer = new LogBuffer();
26
+
27
+ // src/lib/logger.ts
28
+ function write(level, msg, data) {
29
+ const entry = {
30
+ level,
31
+ msg,
32
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
33
+ ...data ? { data } : {}
34
+ };
35
+ const line = JSON.stringify(entry);
36
+ process.stderr.write(`${line}
37
+ `);
38
+ logBuffer.append(entry);
39
+ }
40
+ var log = {
41
+ info: (msg, data) => write("info", msg, data),
42
+ warn: (msg, data) => write("warn", msg, data),
43
+ error: (msg, data) => write("error", msg, data)
44
+ };
45
+ var logger_default = log;
46
+
47
+ // src/lib/ndjson.ts
48
+ async function* readNdjson(body) {
49
+ const reader = body.getReader();
50
+ const decoder = new TextDecoder();
51
+ let buffer = "";
52
+ try {
53
+ while (true) {
54
+ const { done, value } = await reader.read();
55
+ if (done) break;
56
+ buffer += decoder.decode(value, { stream: true });
57
+ const lines = buffer.split("\n");
58
+ buffer = lines.pop() || "";
59
+ for (const line of lines) {
60
+ if (!line.trim()) continue;
61
+ try {
62
+ yield JSON.parse(line);
63
+ } catch {
64
+ logger_default.warn("ndjson: skipping invalid line", { line: line.slice(0, 100) });
65
+ }
66
+ }
67
+ }
68
+ if (buffer.trim()) {
69
+ try {
70
+ yield JSON.parse(buffer);
71
+ } catch {
72
+ logger_default.warn("ndjson: skipping invalid line", { line: buffer.slice(0, 100) });
73
+ }
74
+ }
75
+ } finally {
76
+ reader.releaseLock();
77
+ }
78
+ }
79
+
80
+ export {
81
+ logBuffer,
82
+ logger_default,
83
+ readNdjson
84
+ };
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/lib/spawn-server.ts
4
+ import { spawn } from "child_process";
5
+ import { resolve } from "path";
6
+ function tsxBin(cwd) {
7
+ return resolve(cwd, "node_modules", ".bin", "tsx");
8
+ }
9
+ function spawnServer(cwd, port, options) {
10
+ if (options?.detached) {
11
+ return spawnDetached(cwd, port);
12
+ }
13
+ return spawnAttached(cwd, port);
14
+ }
15
+ function spawnAttached(cwd, port) {
16
+ const child = spawn(tsxBin(cwd), ["src/server.ts", "--port", String(port)], {
17
+ cwd,
18
+ stdio: ["ignore", "pipe", "pipe"]
19
+ });
20
+ return new Promise((resolve2) => {
21
+ const timeout = setTimeout(() => resolve2(null), 3e4);
22
+ function checkOutput(data) {
23
+ const match = data.toString().match(/listening on :(\d+)/);
24
+ if (match) {
25
+ clearTimeout(timeout);
26
+ resolve2({ child, actualPort: parseInt(match[1], 10) });
27
+ }
28
+ }
29
+ child.stdout?.on("data", checkOutput);
30
+ child.stderr?.on("data", checkOutput);
31
+ child.on("error", () => {
32
+ clearTimeout(timeout);
33
+ resolve2(null);
34
+ });
35
+ child.on("exit", () => {
36
+ clearTimeout(timeout);
37
+ resolve2(null);
38
+ });
39
+ });
40
+ }
41
+ function spawnDetached(cwd, port) {
42
+ const child = spawn(tsxBin(cwd), ["src/server.ts", "--port", String(port)], {
43
+ cwd,
44
+ stdio: ["ignore", "pipe", "pipe"],
45
+ detached: true
46
+ });
47
+ return new Promise((resolve2) => {
48
+ const timeout = setTimeout(() => resolve2(null), 3e4);
49
+ function checkOutput(data) {
50
+ const match = data.toString().match(/listening on :(\d+)/);
51
+ if (match) {
52
+ clearTimeout(timeout);
53
+ child.stdout?.destroy();
54
+ child.stderr?.destroy();
55
+ child.unref();
56
+ resolve2({ child, actualPort: parseInt(match[1], 10) });
57
+ }
58
+ }
59
+ child.stdout?.on("data", checkOutput);
60
+ child.stderr?.on("data", checkOutput);
61
+ child.on("error", () => {
62
+ clearTimeout(timeout);
63
+ resolve2(null);
64
+ });
65
+ child.on("exit", () => {
66
+ clearTimeout(timeout);
67
+ resolve2(null);
68
+ });
69
+ });
70
+ }
71
+
72
+ export {
73
+ spawnServer
74
+ };
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/lib/exec.ts
4
+ import { execFile as execFileCb, spawn } from "child_process";
5
+ function exec(cmd, args, options) {
6
+ return new Promise((resolve, reject) => {
7
+ execFileCb(cmd, args, { cwd: options?.cwd }, (err, stdout, stderr) => {
8
+ if (err) {
9
+ err.stderr = stderr;
10
+ reject(err);
11
+ } else {
12
+ resolve(stdout);
13
+ }
14
+ });
15
+ });
16
+ }
17
+ function execInherit(cmd, args, options) {
18
+ return new Promise((resolve, reject) => {
19
+ const child = spawn(cmd, args, {
20
+ cwd: options?.cwd,
21
+ stdio: "inherit"
22
+ });
23
+ child.on("error", reject);
24
+ child.on("close", (code) => {
25
+ if (code === 0) resolve();
26
+ else reject(new Error(`${cmd} ${args.join(" ")} exited with code ${code}`));
27
+ });
28
+ });
29
+ }
30
+
31
+ export {
32
+ exec,
33
+ execInherit
34
+ };
package/dist/cli.js ADDED
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ var command = process.argv[2];
5
+ var args = process.argv.slice(3);
6
+ switch (command) {
7
+ case "create":
8
+ await import("./create-3K6O2SDC.js").then((m) => m.run(args));
9
+ break;
10
+ case "start":
11
+ await import("./start-LKMWS6ZE.js").then((m) => m.run(args));
12
+ break;
13
+ case "stop":
14
+ await import("./stop-YTOAGYE4.js").then((m) => m.run(args));
15
+ break;
16
+ case "logs":
17
+ await import("./logs-BUHRIQ2L.js").then((m) => m.run(args));
18
+ break;
19
+ case "status":
20
+ await import("./status-CIEKUI3V.js").then((m) => m.run(args));
21
+ break;
22
+ case "fork":
23
+ await import("./fork-BB3DZ426.js").then((m) => m.run(args));
24
+ break;
25
+ case "variants":
26
+ await import("./variants-HPY4DEWU.js").then((m) => m.run(args));
27
+ break;
28
+ case "send":
29
+ await import("./send-WQSVSRDD.js").then((m) => m.run(args));
30
+ break;
31
+ case "merge":
32
+ await import("./merge-446QTE7Q.js").then((m) => m.run(args));
33
+ break;
34
+ case "import":
35
+ await import("./import-W2AMTEV5.js").then((m) => m.run(args));
36
+ break;
37
+ case "delete":
38
+ await import("./delete-JNGY7ZFH.js").then((m) => m.run(args));
39
+ break;
40
+ case "env":
41
+ await import("./env-7SLRN3MG.js").then((m) => m.run(args));
42
+ break;
43
+ case "connect":
44
+ await import("./connect-LW6G23AV.js").then((m) => m.run(args));
45
+ break;
46
+ case "disconnect":
47
+ await import("./disconnect-ACVTKTRE.js").then((m) => m.run(args));
48
+ break;
49
+ case "channel":
50
+ await import("./channel-Q642YUZE.js").then((m) => m.run(args));
51
+ break;
52
+ case "upgrade":
53
+ await import("./upgrade-JACA6YMO.js").then((m) => m.run(args));
54
+ break;
55
+ case "up":
56
+ await import("./up-AJJ4GCXY.js").then((m) => m.run(args));
57
+ break;
58
+ case "down":
59
+ await import("./down-FYCUYC5H.js").then((m) => m.run(args));
60
+ break;
61
+ case "schedule":
62
+ await import("./schedule-KKSOVUDF.js").then((m) => m.run(args));
63
+ break;
64
+ default:
65
+ console.log(`volute \u2014 create and manage AI agents
66
+
67
+ Commands:
68
+ volute create <name> Create a new agent
69
+ volute start <name> Start an agent (daemonized)
70
+ volute stop <name> Stop an agent
71
+ volute status [<name>] Check agent status (or list all)
72
+ volute logs <name> Tail agent logs
73
+ volute send <name> "<msg>" Send a message to an agent
74
+ volute fork <name> <variant> Create a variant (worktree + server)
75
+ volute variants <name> List variants for an agent
76
+ volute merge <name> <variant> Merge a variant back
77
+ volute import <path> Import an OpenClaw workspace
78
+ volute env <set|get|list|remove> Manage environment variables
79
+ volute connect <type> <name> Enable a connector for an agent
80
+ volute disconnect <type> <name> Disable a connector for an agent
81
+ volute channel read <uri> Read recent messages from a channel
82
+ volute channel send <uri> "<msg>" Send a message to a channel
83
+ volute schedule list <agent> List schedules for an agent
84
+ volute schedule add <agent> ... Add a cron schedule
85
+ volute schedule remove <agent> ... Remove a schedule
86
+ volute up [--port N] Start the daemon (default: 4200)
87
+ volute down Stop the daemon
88
+ volute upgrade <name> Upgrade agent to latest template
89
+ volute delete <name> [--force] Delete an agent (--force removes files)`);
90
+ if (command) {
91
+ console.error(`
92
+ Unknown command: ${command}`);
93
+ process.exit(1);
94
+ }
95
+ }
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ loadMergedEnv
4
+ } from "./chunk-A5ZJEMHT.js";
5
+ import {
6
+ daemonFetch
7
+ } from "./chunk-H5XQARAP.js";
8
+ import {
9
+ parseArgs
10
+ } from "./chunk-D424ZQGI.js";
11
+ import {
12
+ resolveAgent
13
+ } from "./chunk-5YW4B7CG.js";
14
+
15
+ // src/commands/connect.ts
16
+ async function run(args) {
17
+ const { positional } = parseArgs(args, {});
18
+ const type = positional[0];
19
+ const name = positional[1];
20
+ if (!type || !name) {
21
+ console.error("Usage: volute connect <type> <agent>");
22
+ process.exit(1);
23
+ }
24
+ const { dir } = resolveAgent(name);
25
+ if (type === "discord") {
26
+ const env = loadMergedEnv(dir);
27
+ if (!env.DISCORD_TOKEN) {
28
+ console.error("DISCORD_TOKEN not set. Run: volute env set DISCORD_TOKEN <token>");
29
+ process.exit(1);
30
+ }
31
+ } else {
32
+ console.error(`Unknown connector type: ${type}`);
33
+ process.exit(1);
34
+ }
35
+ const res = await daemonFetch(
36
+ `/api/agents/${encodeURIComponent(name)}/connectors/${encodeURIComponent(type)}`,
37
+ { method: "POST" }
38
+ );
39
+ if (!res.ok) {
40
+ const body = await res.json().catch(() => ({ error: "Unknown error" }));
41
+ console.error(`Failed to start ${type} connector: ${body.error}`);
42
+ process.exit(1);
43
+ }
44
+ console.log(`${type} connector for ${name} started.`);
45
+ }
46
+ export {
47
+ run
48
+ };