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
package/dist/cli.js
CHANGED
|
@@ -1,105 +1,128 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { resolve } from "path";
|
|
6
|
+
if (!process.env.VOLUTE_HOME) {
|
|
7
|
+
process.env.VOLUTE_HOME = resolve(homedir(), ".volute");
|
|
8
|
+
}
|
|
4
9
|
var command = process.argv[2];
|
|
5
10
|
var args = process.argv.slice(3);
|
|
11
|
+
if (command === "--version" || command === "-v") {
|
|
12
|
+
const { default: pkg } = await import("./package-VQOE7JNH.js");
|
|
13
|
+
console.log(pkg.version);
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
6
16
|
switch (command) {
|
|
7
|
-
case "
|
|
8
|
-
await import("./
|
|
9
|
-
break;
|
|
10
|
-
case "start":
|
|
11
|
-
await import("./start-LDPMCMYT.js").then((m) => m.run(args));
|
|
12
|
-
break;
|
|
13
|
-
case "stop":
|
|
14
|
-
await import("./stop-5PZTZCLL.js").then((m) => m.run(args));
|
|
15
|
-
break;
|
|
16
|
-
case "logs":
|
|
17
|
-
await import("./logs-NXFFGUKY.js").then((m) => m.run(args));
|
|
17
|
+
case "agent":
|
|
18
|
+
await import("./agent-Z2B6EFEQ.js").then((m) => m.run(args));
|
|
18
19
|
break;
|
|
19
|
-
case "
|
|
20
|
-
await import("./
|
|
20
|
+
case "message":
|
|
21
|
+
await import("./message-ADHWFHSI.js").then((m) => m.run(args));
|
|
21
22
|
break;
|
|
22
23
|
case "variant":
|
|
23
|
-
await import("./variant-
|
|
24
|
-
break;
|
|
25
|
-
case "send":
|
|
26
|
-
await import("./send-UK3JBZIB.js").then((m) => m.run(args));
|
|
27
|
-
break;
|
|
28
|
-
case "import":
|
|
29
|
-
await import("./import-K4MP2GX7.js").then((m) => m.run(args));
|
|
30
|
-
break;
|
|
31
|
-
case "delete":
|
|
32
|
-
await import("./delete-GQ7JEK2S.js").then((m) => m.run(args));
|
|
33
|
-
break;
|
|
34
|
-
case "env":
|
|
35
|
-
await import("./env-JB27UAC3.js").then((m) => m.run(args));
|
|
24
|
+
await import("./variant-LAB67OC2.js").then((m) => m.run(args));
|
|
36
25
|
break;
|
|
37
26
|
case "connector":
|
|
38
|
-
await import("./connector-
|
|
27
|
+
await import("./connector-LYEMXQEV.js").then((m) => m.run(args));
|
|
39
28
|
break;
|
|
40
29
|
case "channel":
|
|
41
|
-
await import("./channel-
|
|
30
|
+
await import("./channel-MK5OK2SI.js").then((m) => m.run(args));
|
|
31
|
+
break;
|
|
32
|
+
case "schedule":
|
|
33
|
+
await import("./schedule-NAG6F463.js").then((m) => m.run(args));
|
|
34
|
+
break;
|
|
35
|
+
case "conversation":
|
|
36
|
+
await import("./conversation-ERXEQZTY.js").then((m) => m.run(args));
|
|
42
37
|
break;
|
|
43
|
-
case "
|
|
44
|
-
await import("./
|
|
38
|
+
case "env":
|
|
39
|
+
await import("./env-4D4REPJF.js").then((m) => m.run(args));
|
|
45
40
|
break;
|
|
46
41
|
case "up":
|
|
47
|
-
await import("./up-
|
|
42
|
+
await import("./up-7ILD7GU7.js").then((m) => m.run(args));
|
|
48
43
|
break;
|
|
49
44
|
case "down":
|
|
50
|
-
await import("./down-
|
|
51
|
-
break;
|
|
52
|
-
case "schedule":
|
|
53
|
-
await import("./schedule-4I5TYHFH.js").then((m) => m.run(args));
|
|
45
|
+
await import("./down-O7IFZLVJ.js").then((m) => m.run(args));
|
|
54
46
|
break;
|
|
55
|
-
case "
|
|
56
|
-
await import("./
|
|
47
|
+
case "setup":
|
|
48
|
+
await import("./setup-RPRRGG2F.js").then((m) => m.run(args));
|
|
57
49
|
break;
|
|
58
50
|
case "service":
|
|
59
|
-
await import("./service-
|
|
51
|
+
await import("./service-HZNIDNJF.js").then((m) => m.run(args));
|
|
60
52
|
break;
|
|
61
|
-
case "
|
|
62
|
-
await import("./
|
|
53
|
+
case "update":
|
|
54
|
+
await import("./update-LPSIAWQ2.js").then((m) => m.run(args));
|
|
63
55
|
break;
|
|
64
|
-
|
|
56
|
+
case "--help":
|
|
57
|
+
case "-h":
|
|
58
|
+
case void 0:
|
|
65
59
|
console.log(`volute \u2014 create and manage AI agents
|
|
66
60
|
|
|
67
61
|
Commands:
|
|
68
|
-
volute create <name>
|
|
69
|
-
volute start <name>
|
|
70
|
-
volute stop <name>
|
|
71
|
-
volute
|
|
72
|
-
volute
|
|
73
|
-
volute
|
|
74
|
-
volute
|
|
75
|
-
volute
|
|
76
|
-
volute
|
|
77
|
-
|
|
78
|
-
volute
|
|
79
|
-
volute
|
|
80
|
-
|
|
81
|
-
volute
|
|
82
|
-
volute
|
|
83
|
-
volute
|
|
84
|
-
volute
|
|
85
|
-
|
|
86
|
-
volute
|
|
87
|
-
volute
|
|
88
|
-
|
|
89
|
-
volute
|
|
90
|
-
volute
|
|
91
|
-
|
|
92
|
-
volute
|
|
93
|
-
volute
|
|
94
|
-
volute
|
|
95
|
-
|
|
96
|
-
volute
|
|
62
|
+
volute agent create <name> Create a new agent
|
|
63
|
+
volute agent start <name> Start an agent (daemonized)
|
|
64
|
+
volute agent stop <name> Stop an agent
|
|
65
|
+
volute agent delete <name> [--force] Delete an agent (--force removes files)
|
|
66
|
+
volute agent list List all agents
|
|
67
|
+
volute agent status <name> Check agent status
|
|
68
|
+
volute agent logs <name> [--follow] Tail agent logs
|
|
69
|
+
volute agent upgrade <name> Upgrade agent to latest template
|
|
70
|
+
volute agent import <path> Import an OpenClaw workspace
|
|
71
|
+
|
|
72
|
+
volute message send <name> "<msg>" Send a message to an agent
|
|
73
|
+
volute message history [--agent <name>] View message history
|
|
74
|
+
|
|
75
|
+
volute variant create <name> Create a variant (worktree + server)
|
|
76
|
+
volute variant list List variants for an agent
|
|
77
|
+
volute variant merge <name> Merge a variant back
|
|
78
|
+
volute variant delete <name> Delete a variant
|
|
79
|
+
|
|
80
|
+
volute connector connect <type> Enable a connector for an agent
|
|
81
|
+
volute connector disconnect <type> Disable a connector for an agent
|
|
82
|
+
|
|
83
|
+
volute channel read <uri> Read recent messages from a channel
|
|
84
|
+
volute channel send <uri> "<msg>" Send a message to a channel
|
|
85
|
+
|
|
86
|
+
volute schedule list List schedules for an agent
|
|
87
|
+
volute schedule add ... Add a cron schedule
|
|
88
|
+
volute schedule remove ... Remove a schedule
|
|
89
|
+
|
|
90
|
+
volute conversation create ... Create a group conversation
|
|
91
|
+
volute conversation list List conversations
|
|
92
|
+
volute conversation send <id> "<msg>" Send a message to a conversation
|
|
97
93
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
94
|
+
volute env <set|get|list|remove> Manage environment variables
|
|
95
|
+
|
|
96
|
+
volute up [--port N] Start the daemon (default: 4200)
|
|
97
|
+
volute down Stop the daemon
|
|
98
|
+
|
|
99
|
+
volute service install [--port N] Install as system service (auto-start)
|
|
100
|
+
volute service uninstall Remove system service
|
|
101
|
+
volute service status Check service status
|
|
102
|
+
volute setup [--port N] [--host H] Install system service with user isolation
|
|
103
|
+
volute setup uninstall [--force] Remove system service + isolation
|
|
104
|
+
|
|
105
|
+
volute update Update to latest version
|
|
106
|
+
|
|
107
|
+
Options:
|
|
108
|
+
--version, -v Show version number
|
|
109
|
+
--help, -h Show this help message
|
|
110
|
+
|
|
111
|
+
Agent-scoped commands (variant, connector, schedule, channel, conversation, message history)
|
|
112
|
+
use --agent <name> or VOLUTE_AGENT env var to identify the agent.`);
|
|
113
|
+
break;
|
|
114
|
+
default:
|
|
115
|
+
console.error(`Unknown command: ${command}
|
|
116
|
+
Run 'volute --help' for usage.`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
if (command !== "update") {
|
|
120
|
+
import("./update-check-Y33QDCFL.js").then((m) => m.checkForUpdate()).then((result) => {
|
|
121
|
+
if (result.updateAvailable) {
|
|
101
122
|
console.error(`
|
|
102
|
-
|
|
103
|
-
|
|
123
|
+
Update available: ${result.current} \u2192 ${result.latest}`);
|
|
124
|
+
console.error(" Run `volute update` to update\n");
|
|
104
125
|
}
|
|
126
|
+
}).catch(() => {
|
|
127
|
+
});
|
|
105
128
|
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
resolveAgentName
|
|
4
|
+
} from "./chunk-AZEL2IEK.js";
|
|
5
|
+
import {
|
|
6
|
+
agentEnvPath,
|
|
7
|
+
readEnv,
|
|
8
|
+
writeEnv
|
|
9
|
+
} from "./chunk-HE67X4T6.js";
|
|
10
|
+
import {
|
|
11
|
+
parseArgs
|
|
12
|
+
} from "./chunk-D424ZQGI.js";
|
|
13
|
+
import {
|
|
14
|
+
daemonFetch
|
|
15
|
+
} from "./chunk-7L4AN5D4.js";
|
|
16
|
+
import {
|
|
17
|
+
agentDir
|
|
18
|
+
} from "./chunk-UX25Z2ND.js";
|
|
19
|
+
import "./chunk-K3NQKI34.js";
|
|
20
|
+
|
|
21
|
+
// src/commands/connector.ts
|
|
22
|
+
async function run(args) {
|
|
23
|
+
const subcommand = args[0];
|
|
24
|
+
switch (subcommand) {
|
|
25
|
+
case "connect":
|
|
26
|
+
await connectConnector(args.slice(1));
|
|
27
|
+
break;
|
|
28
|
+
case "disconnect":
|
|
29
|
+
await disconnectConnector(args.slice(1));
|
|
30
|
+
break;
|
|
31
|
+
case "--help":
|
|
32
|
+
case "-h":
|
|
33
|
+
case void 0:
|
|
34
|
+
printUsage();
|
|
35
|
+
break;
|
|
36
|
+
default:
|
|
37
|
+
printUsage();
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function printUsage() {
|
|
42
|
+
console.log(`Usage:
|
|
43
|
+
volute connector connect <type> [--agent <name>]
|
|
44
|
+
volute connector disconnect <type> [--agent <name>]`);
|
|
45
|
+
}
|
|
46
|
+
async function promptValue(key, description) {
|
|
47
|
+
process.stderr.write(`${description}
|
|
48
|
+
Enter value for ${key}: `);
|
|
49
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
let value = "";
|
|
52
|
+
const onData = (buf) => {
|
|
53
|
+
for (const byte of buf) {
|
|
54
|
+
if (byte === 3) {
|
|
55
|
+
process.stderr.write("\n");
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
if (byte === 13 || byte === 10) {
|
|
59
|
+
process.stderr.write("\n");
|
|
60
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
61
|
+
process.stdin.removeListener("data", onData);
|
|
62
|
+
process.stdin.pause();
|
|
63
|
+
resolve(value);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (byte === 127 || byte === 8) {
|
|
67
|
+
value = value.slice(0, -1);
|
|
68
|
+
} else {
|
|
69
|
+
value += String.fromCharCode(byte);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
process.stdin.resume();
|
|
74
|
+
process.stdin.on("data", onData);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
async function connectConnector(args) {
|
|
78
|
+
const { positional, flags } = parseArgs(args, {
|
|
79
|
+
agent: { type: "string" }
|
|
80
|
+
});
|
|
81
|
+
const agentName = resolveAgentName(flags);
|
|
82
|
+
const type = positional[0];
|
|
83
|
+
if (!type) {
|
|
84
|
+
console.error("Usage: volute connector connect <type> [--agent <name>]");
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
const url = `/api/agents/${encodeURIComponent(agentName)}/connectors/${encodeURIComponent(type)}`;
|
|
88
|
+
let res = await daemonFetch(url, { method: "POST" });
|
|
89
|
+
if (!res.ok) {
|
|
90
|
+
const body = await res.json().catch(() => ({ error: "Unknown error" }));
|
|
91
|
+
if (body.error === "missing_env" && "missing" in body) {
|
|
92
|
+
const { missing, connectorName } = body;
|
|
93
|
+
if (!process.stdin.isTTY) {
|
|
94
|
+
console.error(`Missing required environment variables for ${connectorName}:`);
|
|
95
|
+
for (const v of missing) {
|
|
96
|
+
console.error(` ${v.name} \u2014 ${v.description}`);
|
|
97
|
+
}
|
|
98
|
+
console.error(`
|
|
99
|
+
Set them with: volute env set <KEY> --agent ${agentName}`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
console.error(`${connectorName} connector requires some environment variables.
|
|
103
|
+
`);
|
|
104
|
+
const dir = agentDir(agentName);
|
|
105
|
+
const envPath = agentEnvPath(dir);
|
|
106
|
+
const env = readEnv(envPath);
|
|
107
|
+
for (const v of missing) {
|
|
108
|
+
const value = await promptValue(v.name, v.description);
|
|
109
|
+
if (!value) {
|
|
110
|
+
console.error(`No value provided for ${v.name}. Aborting.`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
env[v.name] = value;
|
|
114
|
+
}
|
|
115
|
+
writeEnv(envPath, env);
|
|
116
|
+
console.log("Environment variables saved.\n");
|
|
117
|
+
res = await daemonFetch(url, { method: "POST" });
|
|
118
|
+
if (!res.ok) {
|
|
119
|
+
const retryBody = await res.json().catch(() => ({ error: "Unknown error" }));
|
|
120
|
+
console.error(
|
|
121
|
+
`Failed to start ${type} connector: ${retryBody.error}`
|
|
122
|
+
);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
console.error(`Failed to start ${type} connector: ${body.error}`);
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
console.log(`${type} connector for ${agentName} started.`);
|
|
131
|
+
}
|
|
132
|
+
async function disconnectConnector(args) {
|
|
133
|
+
const { positional, flags } = parseArgs(args, {
|
|
134
|
+
agent: { type: "string" }
|
|
135
|
+
});
|
|
136
|
+
const agentName = resolveAgentName(flags);
|
|
137
|
+
const type = positional[0];
|
|
138
|
+
if (!type) {
|
|
139
|
+
console.error("Usage: volute connector disconnect <type> [--agent <name>]");
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
const res = await daemonFetch(
|
|
143
|
+
`/api/agents/${encodeURIComponent(agentName)}/connectors/${encodeURIComponent(type)}`,
|
|
144
|
+
{
|
|
145
|
+
method: "DELETE"
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
if (!res.ok) {
|
|
149
|
+
const body = await res.json().catch(() => ({ error: "Unknown error" }));
|
|
150
|
+
console.error(`Failed to stop ${type} connector: ${body.error}`);
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
console.log(`${type} connector for ${agentName} stopped.`);
|
|
154
|
+
}
|
|
155
|
+
export {
|
|
156
|
+
run
|
|
157
|
+
};
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
fireAndForget,
|
|
4
|
+
handleAgentMessage,
|
|
5
|
+
loadEnv,
|
|
6
|
+
loadFollowedChannels,
|
|
7
|
+
splitMessage
|
|
8
|
+
} from "../chunk-MXUCNIBG.js";
|
|
9
|
+
import "../chunk-K3NQKI34.js";
|
|
2
10
|
|
|
3
11
|
// src/connectors/discord.ts
|
|
4
12
|
import {
|
|
5
13
|
AttachmentBuilder,
|
|
14
|
+
ChannelType,
|
|
6
15
|
Client,
|
|
7
16
|
Events,
|
|
8
17
|
GatewayIntentBits,
|
|
@@ -10,21 +19,14 @@ import {
|
|
|
10
19
|
} from "discord.js";
|
|
11
20
|
var DISCORD_MAX_LENGTH = 2e3;
|
|
12
21
|
var TYPING_INTERVAL_MS = 8e3;
|
|
13
|
-
var
|
|
14
|
-
var agentName = process.env.VOLUTE_AGENT_NAME;
|
|
22
|
+
var env = loadEnv();
|
|
15
23
|
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
24
|
if (!token) {
|
|
21
25
|
console.error("Missing required env var: DISCORD_TOKEN");
|
|
22
26
|
process.exit(1);
|
|
23
27
|
}
|
|
24
|
-
var
|
|
25
|
-
var
|
|
26
|
-
var daemonToken = process.env.VOLUTE_DAEMON_TOKEN;
|
|
27
|
-
var baseUrl = daemonUrl ? `${daemonUrl}/api/agents/${encodeURIComponent(agentName)}` : `http://127.0.0.1:${agentPort}`;
|
|
28
|
+
var followedChannelNames = loadFollowedChannels(env, "discord");
|
|
29
|
+
var followedChannelIds = /* @__PURE__ */ new Set();
|
|
28
30
|
var client = new Client({
|
|
29
31
|
intents: [
|
|
30
32
|
GatewayIntentBits.Guilds,
|
|
@@ -42,13 +44,28 @@ process.on("SIGINT", shutdown);
|
|
|
42
44
|
process.on("SIGTERM", shutdown);
|
|
43
45
|
client.once(Events.ClientReady, (c) => {
|
|
44
46
|
console.log(`Connected to Discord as ${c.user.tag}`);
|
|
45
|
-
console.log(`Bridging to agent: ${agentName} via ${baseUrl}/message`);
|
|
47
|
+
console.log(`Bridging to agent: ${env.agentName} via ${env.baseUrl}/message`);
|
|
48
|
+
if (followedChannelNames.length > 0) {
|
|
49
|
+
for (const guild of c.guilds.cache.values()) {
|
|
50
|
+
for (const ch of guild.channels.cache.values()) {
|
|
51
|
+
if (ch.type !== ChannelType.GuildText) continue;
|
|
52
|
+
if (followedChannelNames.includes(ch.name)) {
|
|
53
|
+
followedChannelIds.add(ch.id);
|
|
54
|
+
console.log(`Following #${ch.name} (${ch.id}) in ${guild.name}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (followedChannelIds.size === 0) {
|
|
59
|
+
console.warn(`No channels found matching: ${followedChannelNames.join(", ")}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
46
62
|
});
|
|
47
63
|
client.on(Events.MessageCreate, async (message) => {
|
|
48
64
|
if (message.author.bot) return;
|
|
49
65
|
const isDM = !message.guild;
|
|
50
66
|
const isMentioned = !isDM && message.mentions.has(client.user);
|
|
51
|
-
|
|
67
|
+
const isFollowedChannel = !isDM && followedChannelIds.has(message.channelId);
|
|
68
|
+
if (!isDM && !isMentioned && !isFollowedChannel) return;
|
|
52
69
|
let text = message.content;
|
|
53
70
|
if (isMentioned) {
|
|
54
71
|
text = text.replace(new RegExp(`<@!?${client.user.id}>`, "g"), "").trim();
|
|
@@ -70,8 +87,82 @@ client.on(Events.MessageCreate, async (message) => {
|
|
|
70
87
|
}
|
|
71
88
|
}
|
|
72
89
|
if (content.length === 0) return;
|
|
73
|
-
|
|
90
|
+
const senderName = message.author.displayName || message.author.username;
|
|
91
|
+
const channelKey = `discord:${message.channelId}`;
|
|
92
|
+
const channelName = !isDM && "name" in message.channel ? message.channel.name : void 0;
|
|
93
|
+
const participantCount = isDM ? 2 : message.guild?.memberCount;
|
|
94
|
+
const payload = {
|
|
95
|
+
content,
|
|
96
|
+
channel: channelKey,
|
|
97
|
+
sender: senderName,
|
|
98
|
+
platform: "Discord",
|
|
99
|
+
...isDM ? { isDM: true } : {},
|
|
100
|
+
...channelName ? { channelName } : {},
|
|
101
|
+
...message.guild?.name ? { serverName: message.guild.name } : {},
|
|
102
|
+
...participantCount ? { participantCount } : {}
|
|
103
|
+
};
|
|
104
|
+
if (isFollowedChannel && !isMentioned) {
|
|
105
|
+
await fireAndForget(env, payload);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
await handleDiscordMessage(message, payload);
|
|
74
109
|
});
|
|
110
|
+
async function handleDiscordMessage(message, payload) {
|
|
111
|
+
const channel = message.channel;
|
|
112
|
+
if (!("sendTyping" in channel)) return;
|
|
113
|
+
const typingInterval = setInterval(() => {
|
|
114
|
+
channel.sendTyping().catch(() => {
|
|
115
|
+
});
|
|
116
|
+
}, TYPING_INTERVAL_MS);
|
|
117
|
+
channel.sendTyping().catch(() => {
|
|
118
|
+
});
|
|
119
|
+
let replied = false;
|
|
120
|
+
try {
|
|
121
|
+
await handleAgentMessage(env, payload, {
|
|
122
|
+
onFlush: async (text, images) => {
|
|
123
|
+
if (!text && images.length === 0) return;
|
|
124
|
+
const chunks = text ? splitMessage(text, DISCORD_MAX_LENGTH) : [];
|
|
125
|
+
const imageFiles = images.map((img, i) => {
|
|
126
|
+
const ext = img.media_type.split("/")[1] || "png";
|
|
127
|
+
return new AttachmentBuilder(Buffer.from(img.data, "base64"), {
|
|
128
|
+
name: `image-${i}.${ext}`
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
if (chunks.length === 0 && imageFiles.length > 0) {
|
|
132
|
+
const sendFn = replied ? channel.send.bind(channel) : message.reply.bind(message);
|
|
133
|
+
await sendFn({ content: "\u200B", files: imageFiles }).catch((err) => {
|
|
134
|
+
console.error(`Failed to send message: ${err}`);
|
|
135
|
+
});
|
|
136
|
+
replied = true;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
140
|
+
const isLast = i === chunks.length - 1;
|
|
141
|
+
const opts = {
|
|
142
|
+
content: chunks[i]
|
|
143
|
+
};
|
|
144
|
+
if (isLast && imageFiles.length > 0) opts.files = imageFiles;
|
|
145
|
+
try {
|
|
146
|
+
if (!replied) {
|
|
147
|
+
await message.reply(opts);
|
|
148
|
+
replied = true;
|
|
149
|
+
} else {
|
|
150
|
+
await channel.send(opts);
|
|
151
|
+
}
|
|
152
|
+
} catch (err) {
|
|
153
|
+
console.error(`Failed to send message: ${err}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
onError: async (msg) => {
|
|
158
|
+
await message.reply(msg).catch(() => {
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
} finally {
|
|
163
|
+
clearInterval(typingInterval);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
75
166
|
async function loginWithRetry() {
|
|
76
167
|
try {
|
|
77
168
|
await client.login(token);
|
|
@@ -94,151 +185,3 @@ loginWithRetry().catch((err) => {
|
|
|
94
185
|
console.error("Failed to connect to Discord:", err);
|
|
95
186
|
process.exit(1);
|
|
96
187
|
});
|
|
97
|
-
function splitMessage(text) {
|
|
98
|
-
const chunks = [];
|
|
99
|
-
while (text.length > DISCORD_MAX_LENGTH) {
|
|
100
|
-
let splitAt = text.lastIndexOf("\n", DISCORD_MAX_LENGTH);
|
|
101
|
-
if (splitAt < DISCORD_MAX_LENGTH / 2) splitAt = DISCORD_MAX_LENGTH;
|
|
102
|
-
chunks.push(text.slice(0, splitAt));
|
|
103
|
-
text = text.slice(splitAt).replace(/^\n/, "");
|
|
104
|
-
}
|
|
105
|
-
if (text) chunks.push(text);
|
|
106
|
-
return chunks;
|
|
107
|
-
}
|
|
108
|
-
async function* readNdjson(body) {
|
|
109
|
-
const reader = body.getReader();
|
|
110
|
-
const decoder = new TextDecoder();
|
|
111
|
-
let buffer = "";
|
|
112
|
-
try {
|
|
113
|
-
while (true) {
|
|
114
|
-
const { done, value } = await reader.read();
|
|
115
|
-
if (done) break;
|
|
116
|
-
buffer += decoder.decode(value, { stream: true });
|
|
117
|
-
const lines = buffer.split("\n");
|
|
118
|
-
buffer = lines.pop() || "";
|
|
119
|
-
for (const line of lines) {
|
|
120
|
-
if (!line.trim()) continue;
|
|
121
|
-
try {
|
|
122
|
-
yield JSON.parse(line);
|
|
123
|
-
} catch {
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
if (buffer.trim()) {
|
|
128
|
-
try {
|
|
129
|
-
yield JSON.parse(buffer);
|
|
130
|
-
} catch {
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
} finally {
|
|
134
|
-
reader.releaseLock();
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
async function handleAgentRequest(message, content) {
|
|
138
|
-
const channel = message.channel;
|
|
139
|
-
if (!("sendTyping" in channel)) return;
|
|
140
|
-
const typingInterval = setInterval(() => {
|
|
141
|
-
channel.sendTyping().catch(() => {
|
|
142
|
-
});
|
|
143
|
-
}, TYPING_INTERVAL_MS);
|
|
144
|
-
channel.sendTyping().catch(() => {
|
|
145
|
-
});
|
|
146
|
-
let accumulated = "";
|
|
147
|
-
const pendingImages = [];
|
|
148
|
-
let replied = false;
|
|
149
|
-
async function flush() {
|
|
150
|
-
const text = accumulated.trim();
|
|
151
|
-
accumulated = "";
|
|
152
|
-
if (!text && pendingImages.length === 0) return;
|
|
153
|
-
const chunks = text ? splitMessage(text) : [];
|
|
154
|
-
const imageFiles = pendingImages.splice(0).map((img, i) => {
|
|
155
|
-
const ext = img.media_type.split("/")[1] || "png";
|
|
156
|
-
return new AttachmentBuilder(Buffer.from(img.data, "base64"), {
|
|
157
|
-
name: `image-${i}.${ext}`
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
if (chunks.length === 0 && imageFiles.length > 0) {
|
|
161
|
-
const sendFn = replied ? channel.send.bind(channel) : message.reply.bind(message);
|
|
162
|
-
await sendFn({ content: "\u200B", files: imageFiles }).catch((err) => {
|
|
163
|
-
console.error(`Failed to send message: ${err}`);
|
|
164
|
-
});
|
|
165
|
-
replied = true;
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
for (let i = 0; i < chunks.length; i++) {
|
|
169
|
-
const isLast = i === chunks.length - 1;
|
|
170
|
-
const opts = {
|
|
171
|
-
content: chunks[i]
|
|
172
|
-
};
|
|
173
|
-
if (isLast && imageFiles.length > 0) opts.files = imageFiles;
|
|
174
|
-
try {
|
|
175
|
-
if (!replied) {
|
|
176
|
-
await message.reply(opts);
|
|
177
|
-
replied = true;
|
|
178
|
-
} else {
|
|
179
|
-
await channel.send(opts);
|
|
180
|
-
}
|
|
181
|
-
} catch (err) {
|
|
182
|
-
console.error(`Failed to send message: ${err}`);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
const senderName = message.author.displayName || message.author.username;
|
|
187
|
-
const channelKey = `discord:${message.channelId}`;
|
|
188
|
-
const isDM = !message.guild;
|
|
189
|
-
const channelName = !isDM && "name" in message.channel ? message.channel.name : null;
|
|
190
|
-
try {
|
|
191
|
-
const headers = { "Content-Type": "application/json" };
|
|
192
|
-
if (daemonUrl && daemonToken) {
|
|
193
|
-
headers.Authorization = `Bearer ${daemonToken}`;
|
|
194
|
-
headers.Origin = daemonUrl;
|
|
195
|
-
}
|
|
196
|
-
const res = await fetch(`${baseUrl}/message`, {
|
|
197
|
-
method: "POST",
|
|
198
|
-
headers,
|
|
199
|
-
body: JSON.stringify({
|
|
200
|
-
content,
|
|
201
|
-
channel: channelKey,
|
|
202
|
-
sender: senderName,
|
|
203
|
-
platform: "Discord",
|
|
204
|
-
...isDM ? { isDM: true } : {},
|
|
205
|
-
...channelName ? { channelName } : {},
|
|
206
|
-
...message.guild?.name ? { guildName: message.guild.name } : {}
|
|
207
|
-
})
|
|
208
|
-
});
|
|
209
|
-
if (!res.ok) {
|
|
210
|
-
const body = await res.text().catch(() => "");
|
|
211
|
-
console.error(`Agent returned ${res.status}: ${body}`);
|
|
212
|
-
await message.reply(`Error: agent returned ${res.status}`);
|
|
213
|
-
clearInterval(typingInterval);
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
if (!res.body) {
|
|
217
|
-
await message.reply("Error: no response from agent");
|
|
218
|
-
clearInterval(typingInterval);
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
for await (const event of readNdjson(res.body)) {
|
|
222
|
-
if (event.type === "text") {
|
|
223
|
-
accumulated += event.content;
|
|
224
|
-
} else if (event.type === "image") {
|
|
225
|
-
pendingImages.push({
|
|
226
|
-
data: event.data,
|
|
227
|
-
media_type: event.media_type
|
|
228
|
-
});
|
|
229
|
-
} else if (event.type === "tool_use") {
|
|
230
|
-
await flush();
|
|
231
|
-
} else if (event.type === "done") {
|
|
232
|
-
break;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
await flush();
|
|
236
|
-
} catch (err) {
|
|
237
|
-
console.error(`Failed to reach agent at ${baseUrl}/message:`, err);
|
|
238
|
-
const errMsg = err instanceof TypeError && err.cause?.code === "ECONNREFUSED" ? "Agent is not running" : `Error: ${err}`;
|
|
239
|
-
await message.reply(errMsg).catch(() => {
|
|
240
|
-
});
|
|
241
|
-
} finally {
|
|
242
|
-
clearInterval(typingInterval);
|
|
243
|
-
}
|
|
244
|
-
}
|