volute 0.4.0 → 0.6.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 +22 -22
- package/dist/agent-X7GJLBLW.js +79 -0
- package/dist/{agent-manager-AUCKMGPR.js → agent-manager-JDVXU3ON.js} +4 -4
- package/dist/channel-SMCNOIVQ.js +262 -0
- package/dist/chunk-AOKAQGO4.js +107 -0
- package/dist/{chunk-VRVVQIYY.js → chunk-AZEL2IEK.js} +1 -1
- package/dist/chunk-B3R6L2GW.js +24 -0
- package/dist/{chunk-MXUCNIBG.js → chunk-BX7KI4S3.js} +68 -3
- package/dist/{chunk-I6OHXCMV.js → chunk-G6ZNGLUX.js} +47 -9
- package/dist/{chunk-DNOXHLE5.js → chunk-H7AMDUIA.js} +1 -1
- package/dist/{chunk-YGFIWIOF.js → chunk-JR4UXCTO.js} +1 -1
- package/dist/{chunk-3C2XR4IY.js → chunk-UWHWAPGO.js} +120 -107
- package/dist/{chunk-SOZA2TLP.js → chunk-W76KWE23.js} +1 -1
- package/dist/{chunk-GSPKUPKU.js → chunk-XUA3JUFK.js} +2 -1
- package/dist/chunk-ZYGKG6VC.js +22 -0
- package/dist/chunk-ZZOOTYXK.js +583 -0
- package/dist/cli.js +83 -74
- package/dist/{connector-DKDJTLYZ.js → connector-Y7JPNROO.js} +11 -6
- package/dist/connectors/discord.js +34 -5
- package/dist/connectors/slack.js +36 -8
- package/dist/connectors/telegram.js +55 -6
- package/dist/create-G525LWEA.js +91 -0
- package/dist/{daemon-client-XR24PUJF.js → daemon-client-442IV43D.js} +2 -2
- package/dist/daemon.js +1273 -384
- package/dist/{delete-55MXCEY5.js → delete-2PH2CGDY.js} +7 -8
- package/dist/{down-3OB6UVAJ.js → down-FXWAN66A.js} +1 -1
- package/dist/{env-JB27UAC3.js → env-7GLUJCWS.js} +8 -5
- package/dist/{history-BKG74I43.js → history-H72ZUIBN.js} +3 -3
- package/dist/{import-4CI2ZUTJ.js → import-AVKQJDYC.js} +8 -8
- package/dist/{logs-NXFFGUKY.js → logs-EDGK26AK.js} +2 -2
- package/dist/message-SCOQDR3P.js +32 -0
- package/dist/{package-Z2SFO2SV.js → package-4DP4Y4UO.js} +1 -1
- package/dist/restart-O4ETYLJF.js +29 -0
- package/dist/{schedule-A35SH4HT.js → schedule-S6QVC5ON.js} +10 -5
- package/dist/send-G7PE4DOJ.js +72 -0
- package/dist/{setup-2FDVN7OF.js → setup-F4TCWVSP.js} +5 -5
- package/dist/{start-LDPMCMYT.js → start-VHQ7LNWM.js} +3 -3
- package/dist/{status-MVSQG54T.js → status-QAJWXKMZ.js} +3 -3
- package/dist/{stop-5PZTZCLL.js → stop-CAGCT5NI.js} +6 -7
- package/dist/{up-F7TMTLRE.js → up-CSX3ZUIU.js} +16 -4
- package/dist/update-XSIX3GGP.js +140 -0
- package/dist/update-check-5ZADDHCK.js +17 -0
- package/dist/{upgrade-6ZW2RD64.js → upgrade-YXKPWDRU.js} +16 -15
- package/dist/{variant-T64BKARF.js → variant-4Z6W3PP6.js} +15 -10
- package/dist/web-assets/assets/index-D5PzIndO.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 +1 -1
- package/templates/_base/.init/.config/hooks/startup-context.sh +19 -1
- package/templates/_base/.init/.config/scripts/session-reader.ts +59 -0
- package/templates/_base/_skills/sessions/SKILL.md +49 -0
- package/templates/_base/_skills/volute-agent/SKILL.md +114 -14
- package/templates/_base/home/.config/routes.json +10 -0
- package/templates/_base/home/VOLUTE.md +14 -35
- package/templates/_base/src/lib/format-prefix.ts +7 -1
- package/templates/_base/src/lib/router.ts +193 -19
- package/templates/_base/src/lib/routing.ts +55 -18
- package/templates/_base/src/lib/session-monitor.ts +400 -0
- package/templates/_base/src/lib/types.ts +5 -1
- 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 +18 -1
- package/templates/agent-sdk/src/lib/hooks/session-context.ts +32 -0
- package/templates/agent-sdk/src/server.ts +8 -2
- 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 +12 -4
- package/templates/pi/src/lib/session-context-extension.ts +33 -0
- package/templates/pi/src/server.ts +1 -1
- package/templates/pi/volute-template.json +1 -1
- package/dist/channel-DQ6UY7QB.js +0 -67
- package/dist/chunk-5OCWMTVS.js +0 -152
- package/dist/chunk-ZHCE4DPY.js +0 -110
- package/dist/create-ILVOG75A.js +0 -79
- package/dist/send-3U6OTKG7.js +0 -57
- package/dist/web-assets/assets/index-NS621maO.js +0 -296
- package/templates/agent-sdk/.init/.config/sessions.json +0 -4
- package/templates/pi/.init/.config/sessions.json +0 -1
- package/dist/{service-SA4TTMDU.js → service-HZNIDNJF.js} +3 -3
package/README.md
CHANGED
|
@@ -17,13 +17,13 @@ npm install -g volute
|
|
|
17
17
|
volute up
|
|
18
18
|
|
|
19
19
|
# Create an agent
|
|
20
|
-
volute create atlas
|
|
20
|
+
volute agent create atlas
|
|
21
21
|
|
|
22
22
|
# Start it
|
|
23
|
-
volute start atlas
|
|
23
|
+
volute agent start atlas
|
|
24
24
|
|
|
25
25
|
# Talk to it
|
|
26
|
-
volute send atlas "hey, what can you do?"
|
|
26
|
+
volute message send atlas "hey, what can you do?"
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
You now have a running AI agent with persistent memory, auto-committing file changes, and session resume across restarts. Open `http://localhost:4200` for the web dashboard.
|
|
@@ -45,20 +45,20 @@ The daemon handles agent lifecycle, crash recovery (auto-restarts after 3 second
|
|
|
45
45
|
### Lifecycle
|
|
46
46
|
|
|
47
47
|
```sh
|
|
48
|
-
volute create atlas # scaffold a new agent
|
|
49
|
-
volute start atlas # start it
|
|
50
|
-
volute stop atlas # stop it
|
|
51
|
-
volute
|
|
52
|
-
volute status atlas # check one
|
|
53
|
-
volute logs atlas --follow # tail logs
|
|
54
|
-
volute delete atlas # remove from registry
|
|
55
|
-
volute delete atlas --force # also delete files
|
|
48
|
+
volute agent create atlas # scaffold a new agent
|
|
49
|
+
volute agent start atlas # start it
|
|
50
|
+
volute agent stop atlas # stop it
|
|
51
|
+
volute agent list # list all agents
|
|
52
|
+
volute agent status atlas # check one
|
|
53
|
+
volute agent logs atlas --follow # tail logs
|
|
54
|
+
volute agent delete atlas # remove from registry
|
|
55
|
+
volute agent delete atlas --force # also delete files
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
### Sending messages
|
|
59
59
|
|
|
60
60
|
```sh
|
|
61
|
-
volute send atlas "what's on your mind?"
|
|
61
|
+
volute message send atlas "what's on your mind?"
|
|
62
62
|
```
|
|
63
63
|
|
|
64
64
|
Responses stream back to your terminal in real time. The agent knows which channel each message came from — CLI, web, Discord, or system — and routes its response back to the source.
|
|
@@ -92,16 +92,16 @@ This is the interesting part. Agents can fork themselves into isolated branches,
|
|
|
92
92
|
|
|
93
93
|
```sh
|
|
94
94
|
# Create a variant — gets its own git worktree and running server
|
|
95
|
-
volute
|
|
95
|
+
volute variant create experiment --agent atlas
|
|
96
96
|
|
|
97
97
|
# Talk to the variant directly
|
|
98
|
-
volute send atlas@experiment "try a different approach"
|
|
98
|
+
volute message send atlas@experiment "try a different approach"
|
|
99
99
|
|
|
100
100
|
# List all variants
|
|
101
|
-
volute
|
|
101
|
+
volute variant list --agent atlas
|
|
102
102
|
|
|
103
103
|
# Merge it back (verifies, merges, cleans up, restarts the main agent)
|
|
104
|
-
volute merge
|
|
104
|
+
volute variant merge experiment --agent atlas --summary "improved response style"
|
|
105
105
|
```
|
|
106
106
|
|
|
107
107
|
What happens:
|
|
@@ -114,7 +114,7 @@ What happens:
|
|
|
114
114
|
You can fork with a custom personality:
|
|
115
115
|
|
|
116
116
|
```sh
|
|
117
|
-
volute
|
|
117
|
+
volute variant create poet --agent atlas --soul "You are a poet who responds only in verse."
|
|
118
118
|
```
|
|
119
119
|
|
|
120
120
|
Agents have access to the `volute` CLI from their working directory, so they can fork, test, and merge their own variants autonomously.
|
|
@@ -187,13 +187,13 @@ The daemon serves a web UI at `http://localhost:4200` (or whatever port you chos
|
|
|
187
187
|
When the Volute template updates, you can upgrade agents without touching their identity:
|
|
188
188
|
|
|
189
189
|
```sh
|
|
190
|
-
volute upgrade atlas # creates an "upgrade" variant
|
|
190
|
+
volute agent upgrade atlas # creates an "upgrade" variant
|
|
191
191
|
# resolve conflicts if needed, then:
|
|
192
|
-
volute upgrade atlas --continue
|
|
192
|
+
volute agent upgrade atlas --continue
|
|
193
193
|
# test:
|
|
194
|
-
volute send atlas@upgrade "are you working?"
|
|
194
|
+
volute message send atlas@upgrade "are you working?"
|
|
195
195
|
# merge:
|
|
196
|
-
volute merge atlas
|
|
196
|
+
volute variant merge upgrade --agent atlas
|
|
197
197
|
```
|
|
198
198
|
|
|
199
199
|
Your agent's `SOUL.md` and `MEMORY.md` are never overwritten.
|
|
@@ -206,7 +206,7 @@ Two built-in templates:
|
|
|
206
206
|
- **`pi`** — [pi-coding-agent](https://github.com/nicepkg/pi) for multi-provider LLM support
|
|
207
207
|
|
|
208
208
|
```sh
|
|
209
|
-
volute create atlas --template pi
|
|
209
|
+
volute agent create atlas --template pi
|
|
210
210
|
```
|
|
211
211
|
|
|
212
212
|
## Model configuration
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "./chunk-K3NQKI34.js";
|
|
3
|
+
|
|
4
|
+
// src/commands/agent.ts
|
|
5
|
+
async function run(args) {
|
|
6
|
+
const subcommand = args[0];
|
|
7
|
+
switch (subcommand) {
|
|
8
|
+
case "create":
|
|
9
|
+
await import("./create-G525LWEA.js").then((m) => m.run(args.slice(1)));
|
|
10
|
+
break;
|
|
11
|
+
case "start":
|
|
12
|
+
await import("./start-VHQ7LNWM.js").then((m) => m.run(args.slice(1)));
|
|
13
|
+
break;
|
|
14
|
+
case "stop":
|
|
15
|
+
await import("./stop-CAGCT5NI.js").then((m) => m.run(args.slice(1)));
|
|
16
|
+
break;
|
|
17
|
+
case "restart":
|
|
18
|
+
await import("./restart-O4ETYLJF.js").then((m) => m.run(args.slice(1)));
|
|
19
|
+
break;
|
|
20
|
+
case "delete":
|
|
21
|
+
await import("./delete-2PH2CGDY.js").then((m) => m.run(args.slice(1)));
|
|
22
|
+
break;
|
|
23
|
+
case "list":
|
|
24
|
+
await import("./status-QAJWXKMZ.js").then((m) => m.run(args.slice(1)));
|
|
25
|
+
break;
|
|
26
|
+
case "status": {
|
|
27
|
+
const rest = args.slice(1);
|
|
28
|
+
if (!rest[0] && process.env.VOLUTE_AGENT) {
|
|
29
|
+
rest.unshift(process.env.VOLUTE_AGENT);
|
|
30
|
+
}
|
|
31
|
+
await import("./status-QAJWXKMZ.js").then((m) => m.run(rest));
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
case "logs": {
|
|
35
|
+
const rest = args.slice(1);
|
|
36
|
+
const logsArgs = transformAgentFlag(rest);
|
|
37
|
+
await import("./logs-EDGK26AK.js").then((m) => m.run(logsArgs));
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
case "upgrade":
|
|
41
|
+
await import("./upgrade-YXKPWDRU.js").then((m) => m.run(args.slice(1)));
|
|
42
|
+
break;
|
|
43
|
+
case "import":
|
|
44
|
+
await import("./import-AVKQJDYC.js").then((m) => m.run(args.slice(1)));
|
|
45
|
+
break;
|
|
46
|
+
case "--help":
|
|
47
|
+
case "-h":
|
|
48
|
+
case void 0:
|
|
49
|
+
printUsage();
|
|
50
|
+
break;
|
|
51
|
+
default:
|
|
52
|
+
printUsage();
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function transformAgentFlag(args) {
|
|
57
|
+
if (args.length > 0 && args[0] && !args[0].startsWith("-")) {
|
|
58
|
+
return ["--agent", args[0], ...args.slice(1)];
|
|
59
|
+
}
|
|
60
|
+
return args;
|
|
61
|
+
}
|
|
62
|
+
function printUsage() {
|
|
63
|
+
console.log(`Usage:
|
|
64
|
+
volute agent create <name> [--template <name>]
|
|
65
|
+
volute agent start <name>
|
|
66
|
+
volute agent stop [name]
|
|
67
|
+
volute agent restart [name]
|
|
68
|
+
volute agent delete [name] [--force]
|
|
69
|
+
volute agent list
|
|
70
|
+
volute agent status [name]
|
|
71
|
+
volute agent logs [name] [--follow] [-n N]
|
|
72
|
+
volute agent upgrade [name] [--template <name>] [--continue]
|
|
73
|
+
volute agent import <path> [--name <name>] [--session <path>] [--template <name>]
|
|
74
|
+
|
|
75
|
+
Agent name can be omitted (where shown as [name]) if VOLUTE_AGENT is set.`);
|
|
76
|
+
}
|
|
77
|
+
export {
|
|
78
|
+
run
|
|
79
|
+
};
|
|
@@ -3,10 +3,10 @@ import {
|
|
|
3
3
|
AgentManager,
|
|
4
4
|
getAgentManager,
|
|
5
5
|
initAgentManager
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import "./chunk-
|
|
8
|
-
import "./chunk-
|
|
9
|
-
import "./chunk-
|
|
6
|
+
} from "./chunk-G6ZNGLUX.js";
|
|
7
|
+
import "./chunk-H7AMDUIA.js";
|
|
8
|
+
import "./chunk-W76KWE23.js";
|
|
9
|
+
import "./chunk-UWHWAPGO.js";
|
|
10
10
|
import "./chunk-K3NQKI34.js";
|
|
11
11
|
export {
|
|
12
12
|
AgentManager,
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
readStdin
|
|
4
|
+
} from "./chunk-ZYGKG6VC.js";
|
|
5
|
+
import {
|
|
6
|
+
resolveAgentName
|
|
7
|
+
} from "./chunk-AZEL2IEK.js";
|
|
8
|
+
import {
|
|
9
|
+
CHANNELS,
|
|
10
|
+
getChannelDriver
|
|
11
|
+
} from "./chunk-ZZOOTYXK.js";
|
|
12
|
+
import {
|
|
13
|
+
loadMergedEnv
|
|
14
|
+
} from "./chunk-H7AMDUIA.js";
|
|
15
|
+
import {
|
|
16
|
+
writeChannelEntry
|
|
17
|
+
} from "./chunk-BX7KI4S3.js";
|
|
18
|
+
import {
|
|
19
|
+
parseArgs
|
|
20
|
+
} from "./chunk-D424ZQGI.js";
|
|
21
|
+
import {
|
|
22
|
+
daemonFetch
|
|
23
|
+
} from "./chunk-JR4UXCTO.js";
|
|
24
|
+
import {
|
|
25
|
+
resolveAgent
|
|
26
|
+
} from "./chunk-UWHWAPGO.js";
|
|
27
|
+
import "./chunk-K3NQKI34.js";
|
|
28
|
+
|
|
29
|
+
// src/commands/channel.ts
|
|
30
|
+
async function run(args) {
|
|
31
|
+
const subcommand = args[0];
|
|
32
|
+
switch (subcommand) {
|
|
33
|
+
case "read":
|
|
34
|
+
await readChannel(args.slice(1));
|
|
35
|
+
break;
|
|
36
|
+
case "send":
|
|
37
|
+
await sendChannel(args.slice(1));
|
|
38
|
+
break;
|
|
39
|
+
case "list":
|
|
40
|
+
await listChannels(args.slice(1));
|
|
41
|
+
break;
|
|
42
|
+
case "users":
|
|
43
|
+
await listUsers(args.slice(1));
|
|
44
|
+
break;
|
|
45
|
+
case "create":
|
|
46
|
+
await createChannel(args.slice(1));
|
|
47
|
+
break;
|
|
48
|
+
case "typing":
|
|
49
|
+
await typingChannel(args.slice(1));
|
|
50
|
+
break;
|
|
51
|
+
case "--help":
|
|
52
|
+
case "-h":
|
|
53
|
+
case void 0:
|
|
54
|
+
printUsage();
|
|
55
|
+
break;
|
|
56
|
+
default:
|
|
57
|
+
printUsage();
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function printUsage() {
|
|
62
|
+
console.log(`Usage:
|
|
63
|
+
volute channel read <channel-uri> [--limit N] [--agent <name>]
|
|
64
|
+
volute channel send <channel-uri> "<message>" [--agent <name>]
|
|
65
|
+
volute channel list [<platform>] [--agent <name>]
|
|
66
|
+
volute channel users <platform> [--agent <name>]
|
|
67
|
+
volute channel create <platform> --participants user1,user2 [--name "..."] [--agent <name>]
|
|
68
|
+
volute channel typing <channel-uri> [--agent <name>]
|
|
69
|
+
echo "message" | volute channel send <channel-uri> [--agent <name>]`);
|
|
70
|
+
}
|
|
71
|
+
async function readChannel(args) {
|
|
72
|
+
const { positional, flags } = parseArgs(args, {
|
|
73
|
+
agent: { type: "string" },
|
|
74
|
+
limit: { type: "number" }
|
|
75
|
+
});
|
|
76
|
+
const uri = positional[0];
|
|
77
|
+
if (!uri) {
|
|
78
|
+
console.error("Usage: volute channel read <channel-uri> [--limit N] [--agent <name>]");
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
const agentName = resolveAgentName(flags);
|
|
82
|
+
const { platform } = parseUri(uri);
|
|
83
|
+
const driver = requireDriver(platform);
|
|
84
|
+
const { dir } = resolveAgent(agentName);
|
|
85
|
+
const env = { ...loadMergedEnv(dir), VOLUTE_AGENT: agentName, VOLUTE_AGENT_DIR: dir };
|
|
86
|
+
try {
|
|
87
|
+
const limit = flags.limit ?? 20;
|
|
88
|
+
const output = await driver.read(env, uri, limit);
|
|
89
|
+
console.log(output);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function sendChannel(args) {
|
|
96
|
+
const { positional, flags } = parseArgs(args, {
|
|
97
|
+
agent: { type: "string" }
|
|
98
|
+
});
|
|
99
|
+
const uri = positional[0];
|
|
100
|
+
const message = positional[1] ?? await readStdin();
|
|
101
|
+
if (!uri || !message) {
|
|
102
|
+
console.error('Usage: volute channel send <channel-uri> "<message>" [--agent <name>]');
|
|
103
|
+
console.error(' echo "message" | volute channel send <channel-uri> [--agent <name>]');
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
const agentName = resolveAgentName(flags);
|
|
107
|
+
const { platform } = parseUri(uri);
|
|
108
|
+
const driver = requireDriver(platform);
|
|
109
|
+
const { dir } = resolveAgent(agentName);
|
|
110
|
+
const env = { ...loadMergedEnv(dir), VOLUTE_AGENT: agentName, VOLUTE_AGENT_DIR: dir };
|
|
111
|
+
try {
|
|
112
|
+
await driver.send(env, uri, message);
|
|
113
|
+
try {
|
|
114
|
+
await daemonFetch(`/api/agents/${encodeURIComponent(agentName)}/history`, {
|
|
115
|
+
method: "POST",
|
|
116
|
+
headers: { "Content-Type": "application/json" },
|
|
117
|
+
body: JSON.stringify({ channel: uri, content: message })
|
|
118
|
+
});
|
|
119
|
+
} catch (err) {
|
|
120
|
+
console.error(`Failed to persist to history: ${err instanceof Error ? err.message : err}`);
|
|
121
|
+
}
|
|
122
|
+
} catch (err) {
|
|
123
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async function listChannels(args) {
|
|
128
|
+
const { positional, flags } = parseArgs(args, {
|
|
129
|
+
agent: { type: "string" }
|
|
130
|
+
});
|
|
131
|
+
const platform = positional[0];
|
|
132
|
+
const agentName = resolveAgentName(flags);
|
|
133
|
+
const { dir } = resolveAgent(agentName);
|
|
134
|
+
const env = { ...loadMergedEnv(dir), VOLUTE_AGENT: agentName, VOLUTE_AGENT_DIR: dir };
|
|
135
|
+
const platforms = platform ? [platform] : Object.keys(CHANNELS);
|
|
136
|
+
for (const p of platforms) {
|
|
137
|
+
const driver = getChannelDriver(p);
|
|
138
|
+
if (!driver?.listConversations) continue;
|
|
139
|
+
try {
|
|
140
|
+
const convs = await driver.listConversations(env);
|
|
141
|
+
for (const conv of convs) {
|
|
142
|
+
writeChannelEntry(dir, conv.id, {
|
|
143
|
+
platformId: conv.platformId,
|
|
144
|
+
platform: p,
|
|
145
|
+
name: conv.name,
|
|
146
|
+
type: conv.type
|
|
147
|
+
});
|
|
148
|
+
const parts = [conv.id.padEnd(24), conv.name.padEnd(28), conv.type];
|
|
149
|
+
if (conv.participantCount != null) {
|
|
150
|
+
parts.push(String(conv.participantCount));
|
|
151
|
+
}
|
|
152
|
+
console.log(parts.join(" "));
|
|
153
|
+
}
|
|
154
|
+
} catch (err) {
|
|
155
|
+
console.error(`${p}: ${err instanceof Error ? err.message : String(err)}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async function listUsers(args) {
|
|
160
|
+
const { positional, flags } = parseArgs(args, {
|
|
161
|
+
agent: { type: "string" }
|
|
162
|
+
});
|
|
163
|
+
const platform = positional[0];
|
|
164
|
+
if (!platform) {
|
|
165
|
+
console.error("Usage: volute channel users <platform> [--agent <name>]");
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
const driver = requireDriver(platform);
|
|
169
|
+
if (!driver.listUsers) {
|
|
170
|
+
console.error(`Platform ${platform} does not support listing users`);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
const agentName = resolveAgentName(flags);
|
|
174
|
+
const { dir } = resolveAgent(agentName);
|
|
175
|
+
const env = { ...loadMergedEnv(dir), VOLUTE_AGENT: agentName, VOLUTE_AGENT_DIR: dir };
|
|
176
|
+
try {
|
|
177
|
+
const users = await driver.listUsers(env);
|
|
178
|
+
for (const user of users) {
|
|
179
|
+
console.log(`${user.username.padEnd(20)} ${user.id.padEnd(20)} ${user.type ?? ""}`);
|
|
180
|
+
}
|
|
181
|
+
} catch (err) {
|
|
182
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async function createChannel(args) {
|
|
187
|
+
const { positional, flags } = parseArgs(args, {
|
|
188
|
+
agent: { type: "string" },
|
|
189
|
+
participants: { type: "string" },
|
|
190
|
+
name: { type: "string" }
|
|
191
|
+
});
|
|
192
|
+
const platform = positional[0];
|
|
193
|
+
if (!platform || !flags.participants) {
|
|
194
|
+
console.error(
|
|
195
|
+
'Usage: volute channel create <platform> --participants user1,user2 [--name "..."] [--agent <name>]'
|
|
196
|
+
);
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
const driver = requireDriver(platform);
|
|
200
|
+
if (!driver.createConversation) {
|
|
201
|
+
console.error(`Platform ${platform} does not support creating conversations`);
|
|
202
|
+
process.exit(1);
|
|
203
|
+
}
|
|
204
|
+
const agentName = resolveAgentName(flags);
|
|
205
|
+
const { dir } = resolveAgent(agentName);
|
|
206
|
+
const env = { ...loadMergedEnv(dir), VOLUTE_AGENT: agentName, VOLUTE_AGENT_DIR: dir };
|
|
207
|
+
const participants = flags.participants.split(",").map((s) => s.trim());
|
|
208
|
+
try {
|
|
209
|
+
const slug = await driver.createConversation(env, participants, flags.name);
|
|
210
|
+
console.log(slug);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async function typingChannel(args) {
|
|
217
|
+
const { positional, flags } = parseArgs(args, {
|
|
218
|
+
agent: { type: "string" }
|
|
219
|
+
});
|
|
220
|
+
const uri = positional[0];
|
|
221
|
+
if (!uri) {
|
|
222
|
+
console.error("Usage: volute channel typing <channel-uri> [--agent <name>]");
|
|
223
|
+
process.exit(1);
|
|
224
|
+
}
|
|
225
|
+
const agentName = resolveAgentName(flags);
|
|
226
|
+
try {
|
|
227
|
+
const res = await daemonFetch(
|
|
228
|
+
`/api/agents/${encodeURIComponent(agentName)}/typing?channel=${encodeURIComponent(uri)}`
|
|
229
|
+
);
|
|
230
|
+
if (!res.ok) {
|
|
231
|
+
const body = await res.json().catch(() => ({}));
|
|
232
|
+
console.error(body.error ?? `Server responded with ${res.status}`);
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
const data = await res.json();
|
|
236
|
+
if (data.typing.length > 0) {
|
|
237
|
+
console.log(data.typing.join(", "));
|
|
238
|
+
}
|
|
239
|
+
} catch (err) {
|
|
240
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function parseUri(uri) {
|
|
245
|
+
const colonIdx = uri.indexOf(":");
|
|
246
|
+
if (colonIdx === -1) {
|
|
247
|
+
console.error(`Invalid channel URI: ${uri} (expected format: platform:id)`);
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
return { platform: uri.slice(0, colonIdx), channelId: uri.slice(colonIdx + 1) };
|
|
251
|
+
}
|
|
252
|
+
function requireDriver(platform) {
|
|
253
|
+
const driver = getChannelDriver(platform);
|
|
254
|
+
if (!driver) {
|
|
255
|
+
console.error(`No channel driver for platform: ${platform}`);
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
return driver;
|
|
259
|
+
}
|
|
260
|
+
export {
|
|
261
|
+
run
|
|
262
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
voluteHome
|
|
4
|
+
} from "./chunk-UWHWAPGO.js";
|
|
5
|
+
|
|
6
|
+
// src/lib/update-check.ts
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
8
|
+
import { resolve } from "path";
|
|
9
|
+
var CACHE_TTL = 60 * 60 * 1e3;
|
|
10
|
+
function cachePath() {
|
|
11
|
+
return resolve(voluteHome(), "update-check.json");
|
|
12
|
+
}
|
|
13
|
+
function readCache() {
|
|
14
|
+
try {
|
|
15
|
+
const data = JSON.parse(readFileSync(cachePath(), "utf-8"));
|
|
16
|
+
if (data.latest && typeof data.checkedAt === "number") return data;
|
|
17
|
+
} catch {
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
function writeCache(latest) {
|
|
22
|
+
try {
|
|
23
|
+
writeFileSync(cachePath(), `${JSON.stringify({ latest, checkedAt: Date.now() })}
|
|
24
|
+
`);
|
|
25
|
+
} catch {
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function getCurrentVersion() {
|
|
29
|
+
const thisDir = new URL(".", import.meta.url).pathname;
|
|
30
|
+
const candidates = [
|
|
31
|
+
resolve(thisDir, "../../package.json"),
|
|
32
|
+
resolve(thisDir, "../../../package.json")
|
|
33
|
+
];
|
|
34
|
+
for (const p of candidates) {
|
|
35
|
+
if (existsSync(p)) {
|
|
36
|
+
try {
|
|
37
|
+
return JSON.parse(readFileSync(p, "utf-8")).version;
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return "0.0.0";
|
|
43
|
+
}
|
|
44
|
+
async function fetchLatestVersion() {
|
|
45
|
+
const controller = new AbortController();
|
|
46
|
+
const timeout = setTimeout(() => controller.abort(), 5e3);
|
|
47
|
+
timeout.unref?.();
|
|
48
|
+
try {
|
|
49
|
+
const res = await fetch("https://registry.npmjs.org/volute/latest", {
|
|
50
|
+
signal: controller.signal
|
|
51
|
+
});
|
|
52
|
+
if (!res.ok) throw new Error(`npm registry returned ${res.status}`);
|
|
53
|
+
const data = await res.json();
|
|
54
|
+
if (typeof data.version !== "string") throw new Error("invalid npm response");
|
|
55
|
+
return data.version;
|
|
56
|
+
} finally {
|
|
57
|
+
clearTimeout(timeout);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function isNewer(current, latest) {
|
|
61
|
+
const parse = (v) => v.split("-")[0].split(".").map(Number);
|
|
62
|
+
const hasPrerelease = (v) => v.includes("-");
|
|
63
|
+
const c = parse(current);
|
|
64
|
+
const l = parse(latest);
|
|
65
|
+
for (let i = 0; i < 3; i++) {
|
|
66
|
+
if ((l[i] ?? 0) > (c[i] ?? 0)) return true;
|
|
67
|
+
if ((l[i] ?? 0) < (c[i] ?? 0)) return false;
|
|
68
|
+
}
|
|
69
|
+
if (hasPrerelease(current) && !hasPrerelease(latest)) return true;
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
async function checkForUpdate() {
|
|
73
|
+
const current = getCurrentVersion();
|
|
74
|
+
const cache = readCache();
|
|
75
|
+
if (cache && Date.now() - cache.checkedAt < CACHE_TTL) {
|
|
76
|
+
return {
|
|
77
|
+
current,
|
|
78
|
+
latest: cache.latest,
|
|
79
|
+
updateAvailable: isNewer(current, cache.latest)
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const latest = await fetchLatestVersion();
|
|
84
|
+
writeCache(latest);
|
|
85
|
+
return { current, latest, updateAvailable: isNewer(current, latest) };
|
|
86
|
+
} catch {
|
|
87
|
+
return { current, latest: current, updateAvailable: false, checkFailed: true };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function checkForUpdateCached() {
|
|
91
|
+
const cache = readCache();
|
|
92
|
+
if (!cache) return null;
|
|
93
|
+
const current = getCurrentVersion();
|
|
94
|
+
return {
|
|
95
|
+
current,
|
|
96
|
+
latest: cache.latest,
|
|
97
|
+
updateAvailable: isNewer(current, cache.latest)
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export {
|
|
102
|
+
getCurrentVersion,
|
|
103
|
+
fetchLatestVersion,
|
|
104
|
+
isNewer,
|
|
105
|
+
checkForUpdate,
|
|
106
|
+
checkForUpdateCached
|
|
107
|
+
};
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
function resolveAgentName(flags) {
|
|
5
5
|
const name = flags.agent || process.env.VOLUTE_AGENT;
|
|
6
6
|
if (!name) {
|
|
7
|
-
console.error("No agent specified.
|
|
7
|
+
console.error("No agent specified. Provide a name or set VOLUTE_AGENT.");
|
|
8
8
|
process.exit(1);
|
|
9
9
|
}
|
|
10
10
|
return name;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/lib/format-tool.ts
|
|
4
|
+
function summarizeTool(name, input) {
|
|
5
|
+
if (input && typeof input === "object") {
|
|
6
|
+
const args = input;
|
|
7
|
+
const val = args.path ?? args.command ?? args.query ?? args.url;
|
|
8
|
+
if (typeof val === "string") {
|
|
9
|
+
const brief = val.length > 60 ? `${val.slice(0, 57)}...` : val;
|
|
10
|
+
return `[${name} ${brief}]`;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return `[${name}]`;
|
|
14
|
+
}
|
|
15
|
+
function collectPart(event) {
|
|
16
|
+
if (event.type === "text") return event.content ?? "";
|
|
17
|
+
if (event.type === "tool_use") return summarizeTool(event.name ?? "", event.input);
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
summarizeTool,
|
|
23
|
+
collectPart
|
|
24
|
+
};
|
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// src/lib/slugify.ts
|
|
4
|
+
function slugify(text) {
|
|
5
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
6
|
+
}
|
|
7
|
+
|
|
3
8
|
// src/connectors/sdk.ts
|
|
4
|
-
import { existsSync, readFileSync } from "fs";
|
|
5
|
-
import { resolve } from "path";
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
10
|
+
import { join, resolve } from "path";
|
|
6
11
|
function loadEnv() {
|
|
7
12
|
const agentPort = process.env.VOLUTE_AGENT_PORT;
|
|
8
13
|
const agentName = process.env.VOLUTE_AGENT_NAME;
|
|
@@ -92,6 +97,15 @@ function onShutdown(cleanup) {
|
|
|
92
97
|
process.on("SIGINT", handler);
|
|
93
98
|
process.on("SIGTERM", handler);
|
|
94
99
|
}
|
|
100
|
+
function reportTyping(env, channel, sender, active) {
|
|
101
|
+
fetch(`${env.baseUrl}/typing`, {
|
|
102
|
+
method: "POST",
|
|
103
|
+
headers: getHeaders(env),
|
|
104
|
+
body: JSON.stringify({ channel, sender, active })
|
|
105
|
+
}).catch((err) => {
|
|
106
|
+
console.warn(`[typing] failed to report for ${sender} on ${channel}: ${err}`);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
95
109
|
async function fireAndForget(env, payload) {
|
|
96
110
|
try {
|
|
97
111
|
const res = await fetch(`${env.baseUrl}/message`, {
|
|
@@ -157,12 +171,63 @@ async function handleAgentMessage(env, payload, handlers) {
|
|
|
157
171
|
await handlers.onError(errMsg);
|
|
158
172
|
}
|
|
159
173
|
}
|
|
174
|
+
function buildChannelSlug(platform, meta) {
|
|
175
|
+
if (meta.isDM) {
|
|
176
|
+
if (meta.recipients && meta.recipients.length > 0) {
|
|
177
|
+
const sorted = meta.recipients.map(slugify).sort();
|
|
178
|
+
return `${platform}:@${sorted.join(",")}`;
|
|
179
|
+
}
|
|
180
|
+
if (meta.senderName) {
|
|
181
|
+
return `${platform}:@${slugify(meta.senderName)}`;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (meta.channelName && meta.serverName) {
|
|
185
|
+
return `${platform}:${slugify(meta.serverName)}/${slugify(meta.channelName)}`;
|
|
186
|
+
}
|
|
187
|
+
if (meta.channelName) {
|
|
188
|
+
return `${platform}:${slugify(meta.channelName)}`;
|
|
189
|
+
}
|
|
190
|
+
if (meta.platformId) {
|
|
191
|
+
return `${platform}:${meta.platformId}`;
|
|
192
|
+
}
|
|
193
|
+
return `${platform}:unknown`;
|
|
194
|
+
}
|
|
195
|
+
function readChannelMap(agentDir) {
|
|
196
|
+
const filePath = join(agentDir, ".volute", "channels.json");
|
|
197
|
+
if (!existsSync(filePath)) return {};
|
|
198
|
+
try {
|
|
199
|
+
return JSON.parse(readFileSync(filePath, "utf-8"));
|
|
200
|
+
} catch {
|
|
201
|
+
return {};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function writeChannelEntry(agentDir, slug, entry) {
|
|
205
|
+
const voluteDir = join(agentDir, ".volute");
|
|
206
|
+
mkdirSync(voluteDir, { recursive: true });
|
|
207
|
+
const filePath = join(voluteDir, "channels.json");
|
|
208
|
+
const map = readChannelMap(agentDir);
|
|
209
|
+
map[slug] = entry;
|
|
210
|
+
writeFileSync(filePath, JSON.stringify(map, null, 2) + "\n");
|
|
211
|
+
}
|
|
212
|
+
function resolveChannelId(agentDir, slug) {
|
|
213
|
+
const map = readChannelMap(agentDir);
|
|
214
|
+
if (map[slug]) {
|
|
215
|
+
return map[slug].platformId;
|
|
216
|
+
}
|
|
217
|
+
const colonIndex = slug.indexOf(":");
|
|
218
|
+
return colonIndex >= 0 ? slug.slice(colonIndex + 1) : slug;
|
|
219
|
+
}
|
|
160
220
|
|
|
161
221
|
export {
|
|
222
|
+
slugify,
|
|
162
223
|
loadEnv,
|
|
163
224
|
loadFollowedChannels,
|
|
164
225
|
splitMessage,
|
|
165
226
|
onShutdown,
|
|
227
|
+
reportTyping,
|
|
166
228
|
fireAndForget,
|
|
167
|
-
handleAgentMessage
|
|
229
|
+
handleAgentMessage,
|
|
230
|
+
buildChannelSlug,
|
|
231
|
+
writeChannelEntry,
|
|
232
|
+
resolveChannelId
|
|
168
233
|
};
|