volute 0.3.1 → 0.4.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 +7 -7
- package/dist/{channel-7FZ6D25H.js → channel-DQ6UY7QB.js} +16 -39
- package/dist/chunk-5OCWMTVS.js +152 -0
- package/dist/chunk-MXUCNIBG.js +168 -0
- package/dist/{chunk-N4YNKR3Q.js → chunk-ZHCE4DPY.js} +20 -0
- package/dist/cli.js +29 -18
- package/dist/connector-DKDJTLYZ.js +152 -0
- package/dist/connectors/discord.js +102 -161
- package/dist/connectors/slack.js +170 -0
- package/dist/connectors/telegram.js +156 -0
- package/dist/daemon.js +262 -142
- package/dist/{import-K4MP2GX7.js → import-4CI2ZUTJ.js} +15 -0
- package/dist/package-Z2SFO2SV.js +89 -0
- package/dist/{send-UK3JBZIB.js → send-3U6OTKG7.js} +6 -2
- package/dist/web-assets/assets/{index-BC5eSqbY.js → index-NS621maO.js} +23 -23
- package/dist/web-assets/index.html +1 -1
- package/package.json +3 -1
- package/templates/_base/_skills/volute-agent/SKILL.md +3 -3
- package/templates/_base/home/VOLUTE.md +18 -6
- package/templates/_base/src/lib/file-handler.ts +46 -0
- package/templates/_base/src/lib/router.ts +180 -0
- package/templates/_base/src/lib/routing.ts +100 -0
- package/templates/_base/src/lib/types.ts +13 -2
- package/templates/_base/src/lib/volute-server.ts +20 -48
- package/templates/agent-sdk/src/agent.ts +268 -82
- package/templates/agent-sdk/src/server.ts +12 -3
- package/templates/pi/src/agent.ts +277 -58
- package/templates/pi/src/server.ts +15 -4
- package/dist/connector-TVJULIRT.js +0 -96
- package/templates/_base/src/lib/sessions.ts +0 -71
- package/templates/agent-sdk/src/lib/agent-sessions.ts +0 -204
- package/templates/pi/src/lib/agent-sessions.ts +0 -210
- package/dist/{create-BRG2DBWI.js → create-ILVOG75A.js} +3 -3
- package/dist/{delete-GQ7JEK2S.js → delete-55MXCEY5.js} +3 -3
- package/dist/{history-3VRUBGGV.js → history-BKG74I43.js} +3 -3
- package/dist/{schedule-4I5TYHFH.js → schedule-A35SH4HT.js} +3 -3
- package/dist/{setup-SRS7AUAA.js → setup-2FDVN7OF.js} +3 -3
- package/dist/{up-UT3IMKCA.js → up-F7TMTLRE.js} +0 -0
- package/dist/{upgrade-CDKECCGN.js → upgrade-6ZW2RD64.js} +3 -3
- package/dist/{variant-CVYM3EQG.js → variant-T64BKARF.js} +6 -6
|
@@ -1,74 +1,293 @@
|
|
|
1
1
|
import type { ImageContent } from "@mariozechner/pi-ai";
|
|
2
|
-
import {
|
|
3
|
-
import { formatPrefix } from "./lib/format-prefix.js";
|
|
4
|
-
import { logMessage } from "./lib/logger.js";
|
|
2
|
+
import { getModel, getModels } from "@mariozechner/pi-ai";
|
|
5
3
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
type
|
|
4
|
+
AuthStorage,
|
|
5
|
+
createAgentSession,
|
|
6
|
+
DefaultResourceLoader,
|
|
7
|
+
type ExtensionFactory,
|
|
8
|
+
ModelRegistry,
|
|
9
|
+
SessionManager,
|
|
10
|
+
SettingsManager,
|
|
11
|
+
} from "@mariozechner/pi-coding-agent";
|
|
12
|
+
import { commitFileChange } from "./lib/auto-commit.js";
|
|
13
|
+
import { log, logText, logThinking, logToolResult, logToolUse } from "./lib/logger.js";
|
|
14
|
+
import type {
|
|
15
|
+
HandlerMeta,
|
|
16
|
+
HandlerResolver,
|
|
17
|
+
Listener,
|
|
18
|
+
MessageHandler,
|
|
19
|
+
VoluteContentPart,
|
|
20
|
+
VoluteEvent,
|
|
10
21
|
} from "./lib/types.js";
|
|
11
22
|
|
|
23
|
+
type AgentSession = Awaited<ReturnType<typeof createAgentSession>>["session"];
|
|
24
|
+
|
|
25
|
+
type PiSession = {
|
|
26
|
+
name: string;
|
|
27
|
+
agentSession: AgentSession | null;
|
|
28
|
+
ready: Promise<void>;
|
|
29
|
+
listeners: Set<Listener>;
|
|
30
|
+
unsubscribe?: () => void;
|
|
31
|
+
messageIds: (string | undefined)[];
|
|
32
|
+
currentMessageId?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const DEFAULT_COMPACTION_MESSAGE =
|
|
36
|
+
"Your conversation is approaching its context limit. Please update today's journal entry to preserve important context before the conversation is compacted.";
|
|
37
|
+
|
|
38
|
+
function resolveModel(modelStr: string) {
|
|
39
|
+
const [provider, ...rest] = modelStr.split(":");
|
|
40
|
+
const modelId = rest.join(":");
|
|
41
|
+
|
|
42
|
+
// Try exact match first, then prefix match against available models
|
|
43
|
+
let model = getModel(provider as any, modelId as any);
|
|
44
|
+
if (!model) {
|
|
45
|
+
const available = getModels(provider as any);
|
|
46
|
+
const found = available.find((m) => m.id.startsWith(modelId));
|
|
47
|
+
if (found) model = found;
|
|
48
|
+
}
|
|
49
|
+
if (!model) {
|
|
50
|
+
const available = getModels(provider as any);
|
|
51
|
+
throw new Error(
|
|
52
|
+
`Model not found: ${modelStr}\nAvailable ${provider} models: ${available.map((m) => m.id).join(", ")}`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
return model;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function extractText(content: VoluteContentPart[]): string {
|
|
59
|
+
return content
|
|
60
|
+
.filter((p): p is { type: "text"; text: string } => p.type === "text")
|
|
61
|
+
.map((p) => p.text)
|
|
62
|
+
.join("\n");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function extractImages(content: VoluteContentPart[]): ImageContent[] {
|
|
66
|
+
return content
|
|
67
|
+
.filter((p): p is { type: "image"; media_type: string; data: string } => p.type === "image")
|
|
68
|
+
.map((p) => ({ type: "image" as const, mimeType: p.media_type, data: p.data }));
|
|
69
|
+
}
|
|
70
|
+
|
|
12
71
|
export function createAgent(options: {
|
|
13
72
|
systemPrompt: string;
|
|
14
73
|
cwd: string;
|
|
15
74
|
model?: string;
|
|
16
75
|
compactionMessage?: string;
|
|
17
|
-
}) {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
76
|
+
}): { resolve: HandlerResolver } {
|
|
77
|
+
const sessions = new Map<string, PiSession>();
|
|
78
|
+
const compactionMessage = options.compactionMessage ?? DEFAULT_COMPACTION_MESSAGE;
|
|
79
|
+
|
|
80
|
+
// Shared setup (created once)
|
|
81
|
+
const modelStr = options.model || process.env.PI_MODEL || "anthropic:claude-sonnet-4-20250514";
|
|
82
|
+
const model = resolveModel(modelStr);
|
|
83
|
+
const authStorage = new AuthStorage();
|
|
84
|
+
const modelRegistry = new ModelRegistry(authStorage);
|
|
85
|
+
|
|
86
|
+
// --- Session lifecycle ---
|
|
87
|
+
|
|
88
|
+
function getOrCreateSession(name: string): PiSession {
|
|
89
|
+
const existing = sessions.get(name);
|
|
90
|
+
if (existing) return existing;
|
|
91
|
+
|
|
92
|
+
const session: PiSession = {
|
|
93
|
+
name,
|
|
94
|
+
agentSession: null,
|
|
95
|
+
ready: Promise.resolve(),
|
|
96
|
+
listeners: new Set(),
|
|
97
|
+
messageIds: [],
|
|
98
|
+
};
|
|
99
|
+
sessions.set(name, session);
|
|
100
|
+
|
|
101
|
+
session.ready = initSession(session).catch((err) => {
|
|
102
|
+
log("agent", `session "${session.name}": init failed:`, err);
|
|
103
|
+
});
|
|
104
|
+
return session;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function initSession(session: PiSession) {
|
|
108
|
+
const isEphemeral = session.name.startsWith("new-");
|
|
109
|
+
|
|
110
|
+
const sessionManager = isEphemeral
|
|
111
|
+
? SessionManager.inMemory()
|
|
112
|
+
: SessionManager.continueRecent(options.cwd, `.volute/pi-sessions/${session.name}`);
|
|
113
|
+
|
|
114
|
+
log("agent", `session "${session.name}": ${isEphemeral ? "ephemeral" : "persistent"}`);
|
|
115
|
+
|
|
116
|
+
let compactBlocked = false;
|
|
117
|
+
const preCompactExtension: ExtensionFactory = (pi) => {
|
|
118
|
+
pi.on("session_before_compact", () => {
|
|
119
|
+
if (!compactBlocked) {
|
|
120
|
+
compactBlocked = true;
|
|
121
|
+
log(
|
|
122
|
+
"agent",
|
|
123
|
+
`session "${session.name}": blocking compaction — asking agent to update daily log`,
|
|
124
|
+
);
|
|
125
|
+
session.messageIds.push(undefined);
|
|
126
|
+
session.agentSession?.prompt(compactionMessage, { streamingBehavior: "followUp" });
|
|
127
|
+
return { cancel: true };
|
|
128
|
+
}
|
|
129
|
+
compactBlocked = false;
|
|
130
|
+
log("agent", `session "${session.name}": allowing compaction`);
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const settingsManager = SettingsManager.inMemory({
|
|
135
|
+
retry: { enabled: true, maxRetries: 3 },
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
139
|
+
cwd: options.cwd,
|
|
140
|
+
settingsManager,
|
|
141
|
+
systemPrompt: options.systemPrompt,
|
|
142
|
+
extensionFactories: [preCompactExtension],
|
|
143
|
+
});
|
|
144
|
+
await resourceLoader.reload();
|
|
145
|
+
|
|
146
|
+
const { session: agentSession } = await createAgentSession({
|
|
147
|
+
cwd: options.cwd,
|
|
148
|
+
model,
|
|
149
|
+
authStorage,
|
|
150
|
+
modelRegistry,
|
|
151
|
+
sessionManager,
|
|
152
|
+
settingsManager,
|
|
153
|
+
resourceLoader,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
session.agentSession = agentSession;
|
|
157
|
+
|
|
158
|
+
// Per-session event subscription
|
|
159
|
+
const toolArgs = new Map<string, any>();
|
|
160
|
+
|
|
161
|
+
session.unsubscribe = agentSession.subscribe((event) => {
|
|
162
|
+
if (session.currentMessageId === undefined) {
|
|
163
|
+
session.currentMessageId = session.messageIds.shift();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (event.type === "message_update") {
|
|
167
|
+
const ae = event.assistantMessageEvent;
|
|
168
|
+
if (ae.type === "text_delta") {
|
|
169
|
+
logText(ae.delta);
|
|
170
|
+
broadcast(session, { type: "text", content: ae.delta });
|
|
171
|
+
} else if (ae.type === "thinking_delta") {
|
|
172
|
+
logThinking(ae.delta);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (event.type === "tool_execution_start") {
|
|
177
|
+
toolArgs.set(event.toolCallId, event.args);
|
|
178
|
+
logToolUse(event.toolName, event.args);
|
|
179
|
+
broadcast(session, { type: "tool_use", name: event.toolName, input: event.args });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (event.type === "tool_execution_end") {
|
|
183
|
+
const output =
|
|
184
|
+
typeof event.result === "string" ? event.result : JSON.stringify(event.result);
|
|
185
|
+
logToolResult(event.toolName, output, event.isError);
|
|
186
|
+
broadcast(session, { type: "tool_result", output, is_error: event.isError });
|
|
187
|
+
|
|
188
|
+
// Auto-commit file changes in home/
|
|
189
|
+
if ((event.toolName === "edit" || event.toolName === "write") && !event.isError) {
|
|
190
|
+
const args = toolArgs.get(event.toolCallId);
|
|
191
|
+
const filePath = (args as { path?: string })?.path;
|
|
192
|
+
if (filePath) {
|
|
193
|
+
commitFileChange(filePath, options.cwd);
|
|
194
|
+
}
|
|
59
195
|
}
|
|
60
|
-
|
|
61
|
-
|
|
196
|
+
toolArgs.delete(event.toolCallId);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (event.type === "agent_end") {
|
|
200
|
+
log("agent", `session "${session.name}": turn done`);
|
|
201
|
+
broadcast(session, { type: "done" });
|
|
202
|
+
session.currentMessageId = undefined;
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
log("agent", `session "${session.name}": ready`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// --- Event broadcasting ---
|
|
210
|
+
|
|
211
|
+
function broadcast(session: PiSession, event: VoluteEvent) {
|
|
212
|
+
const tagged =
|
|
213
|
+
session.currentMessageId != null ? { ...event, messageId: session.currentMessageId } : event;
|
|
214
|
+
for (const listener of session.listeners) {
|
|
215
|
+
try {
|
|
216
|
+
listener(tagged);
|
|
217
|
+
} catch (err) {
|
|
218
|
+
log("agent", "listener threw during broadcast:", err);
|
|
62
219
|
}
|
|
63
|
-
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function interruptSession(name: string) {
|
|
224
|
+
const session = sessions.get(name);
|
|
225
|
+
if (session?.currentMessageId !== undefined) {
|
|
226
|
+
log("agent", `session "${name}": interrupting current turn`);
|
|
227
|
+
broadcast(session, { type: "done" });
|
|
228
|
+
session.currentMessageId = undefined;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// --- MessageHandler implementation ---
|
|
233
|
+
|
|
234
|
+
function createSessionHandler(sessionName: string): MessageHandler {
|
|
235
|
+
return {
|
|
236
|
+
handle(content: VoluteContentPart[], meta: HandlerMeta, listener: Listener): () => void {
|
|
237
|
+
const session = getOrCreateSession(sessionName);
|
|
238
|
+
|
|
239
|
+
// Filter listener to only receive events for this messageId
|
|
240
|
+
const filteredListener: Listener = (event) => {
|
|
241
|
+
if (event.messageId === meta.messageId) listener(event);
|
|
242
|
+
};
|
|
243
|
+
session.listeners.add(filteredListener);
|
|
244
|
+
|
|
245
|
+
// Track messageId (must be pushed before prompt)
|
|
246
|
+
session.messageIds.push(meta.messageId);
|
|
247
|
+
|
|
248
|
+
const text = extractText(content);
|
|
249
|
+
const images = extractImages(content);
|
|
250
|
+
const opts = images.length ? { images } : {};
|
|
251
|
+
|
|
252
|
+
// Fire-and-forget: await session ready then prompt
|
|
253
|
+
(async () => {
|
|
254
|
+
await session.ready;
|
|
255
|
+
if (session.agentSession!.isStreaming) {
|
|
256
|
+
if (meta.interrupt) {
|
|
257
|
+
interruptSession(sessionName);
|
|
258
|
+
session.agentSession!.prompt(text, { streamingBehavior: "steer", ...opts });
|
|
259
|
+
} else {
|
|
260
|
+
session.agentSession!.prompt(text, { streamingBehavior: "followUp", ...opts });
|
|
261
|
+
}
|
|
262
|
+
} else {
|
|
263
|
+
session.agentSession!.prompt(text, opts);
|
|
264
|
+
}
|
|
265
|
+
})().catch((err) => {
|
|
266
|
+
log("agent", `session "${sessionName}": prompt failed:`, err);
|
|
267
|
+
broadcast(session, { type: "done" });
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
return () => session.listeners.delete(filteredListener);
|
|
271
|
+
},
|
|
272
|
+
};
|
|
64
273
|
}
|
|
65
274
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
275
|
+
// --- HandlerResolver ---
|
|
276
|
+
|
|
277
|
+
const handlers = new Map<string, MessageHandler>();
|
|
278
|
+
|
|
279
|
+
function resolve(sessionName: string): MessageHandler {
|
|
280
|
+
// Ephemeral sessions get unique names — don't cache their handlers
|
|
281
|
+
if (sessionName.startsWith("new-")) {
|
|
282
|
+
return createSessionHandler(sessionName);
|
|
283
|
+
}
|
|
284
|
+
let handler = handlers.get(sessionName);
|
|
285
|
+
if (!handler) {
|
|
286
|
+
handler = createSessionHandler(sessionName);
|
|
287
|
+
handlers.set(sessionName, handler);
|
|
288
|
+
}
|
|
289
|
+
return handler;
|
|
71
290
|
}
|
|
72
291
|
|
|
73
|
-
return {
|
|
292
|
+
return { resolve };
|
|
74
293
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { createAgent } from "./agent.js";
|
|
3
|
+
import { createFileHandlerResolver } from "./lib/file-handler.js";
|
|
3
4
|
import { log } from "./lib/logger.js";
|
|
5
|
+
import { createRouter } from "./lib/router.js";
|
|
4
6
|
import {
|
|
5
7
|
handleMergeContext,
|
|
6
8
|
handleStartupContext,
|
|
@@ -26,21 +28,30 @@ const agent = createAgent({
|
|
|
26
28
|
compactionMessage: config.compactionMessage,
|
|
27
29
|
});
|
|
28
30
|
|
|
31
|
+
const router = createRouter({
|
|
32
|
+
configPath: resolve("home/.config/sessions.json"),
|
|
33
|
+
agentHandler: agent.resolve,
|
|
34
|
+
fileHandler: createFileHandlerResolver(resolve("home")),
|
|
35
|
+
});
|
|
36
|
+
|
|
29
37
|
const server = createVoluteServer({
|
|
30
|
-
|
|
38
|
+
router,
|
|
31
39
|
port,
|
|
32
40
|
name: pkg.name,
|
|
33
41
|
version: pkg.version,
|
|
34
|
-
sessionsConfigPath: resolve("home/.config/sessions.json"),
|
|
35
42
|
});
|
|
36
43
|
|
|
37
44
|
server.listen(port, async () => {
|
|
38
45
|
const addr = server.address();
|
|
39
46
|
const actualPort = typeof addr === "object" && addr ? addr.port : port;
|
|
40
47
|
log("server", `listening on :${actualPort}`);
|
|
41
|
-
const hasMerge = handleMergeContext((content) =>
|
|
48
|
+
const hasMerge = handleMergeContext((content) =>
|
|
49
|
+
router.route([{ type: "text", text: content }], { channel: "system" }),
|
|
50
|
+
);
|
|
42
51
|
if (!hasMerge) {
|
|
43
|
-
await handleStartupContext((content) =>
|
|
52
|
+
await handleStartupContext((content) =>
|
|
53
|
+
router.route([{ type: "text", text: content }], { channel: "system" }),
|
|
54
|
+
);
|
|
44
55
|
}
|
|
45
56
|
});
|
|
46
57
|
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
loadMergedEnv
|
|
4
|
-
} from "./chunk-DNOXHLE5.js";
|
|
5
|
-
import {
|
|
6
|
-
daemonFetch
|
|
7
|
-
} from "./chunk-YGFIWIOF.js";
|
|
8
|
-
import {
|
|
9
|
-
resolveAgentName
|
|
10
|
-
} from "./chunk-VRVVQIYY.js";
|
|
11
|
-
import {
|
|
12
|
-
parseArgs
|
|
13
|
-
} from "./chunk-D424ZQGI.js";
|
|
14
|
-
import {
|
|
15
|
-
resolveAgent
|
|
16
|
-
} from "./chunk-3C2XR4IY.js";
|
|
17
|
-
import "./chunk-K3NQKI34.js";
|
|
18
|
-
|
|
19
|
-
// src/commands/connector.ts
|
|
20
|
-
async function run(args) {
|
|
21
|
-
const subcommand = args[0];
|
|
22
|
-
switch (subcommand) {
|
|
23
|
-
case "connect":
|
|
24
|
-
await connectConnector(args.slice(1));
|
|
25
|
-
break;
|
|
26
|
-
case "disconnect":
|
|
27
|
-
await disconnectConnector(args.slice(1));
|
|
28
|
-
break;
|
|
29
|
-
default:
|
|
30
|
-
printUsage();
|
|
31
|
-
process.exit(subcommand ? 1 : 0);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
function printUsage() {
|
|
35
|
-
console.error(`Usage:
|
|
36
|
-
volute connector connect <type> [--agent <name>]
|
|
37
|
-
volute connector disconnect <type> [--agent <name>]`);
|
|
38
|
-
}
|
|
39
|
-
async function connectConnector(args) {
|
|
40
|
-
const { positional, flags } = parseArgs(args, {
|
|
41
|
-
agent: { type: "string" }
|
|
42
|
-
});
|
|
43
|
-
const agentName = resolveAgentName(flags);
|
|
44
|
-
const type = positional[0];
|
|
45
|
-
if (!type) {
|
|
46
|
-
console.error("Usage: volute connector connect <type> [--agent <name>]");
|
|
47
|
-
process.exit(1);
|
|
48
|
-
}
|
|
49
|
-
const { dir } = resolveAgent(agentName);
|
|
50
|
-
if (type === "discord") {
|
|
51
|
-
const env = loadMergedEnv(dir);
|
|
52
|
-
if (!env.DISCORD_TOKEN) {
|
|
53
|
-
console.error("DISCORD_TOKEN not set. Run: volute env set DISCORD_TOKEN <token>");
|
|
54
|
-
process.exit(1);
|
|
55
|
-
}
|
|
56
|
-
} else {
|
|
57
|
-
console.error(`Unknown connector type: ${type}`);
|
|
58
|
-
process.exit(1);
|
|
59
|
-
}
|
|
60
|
-
const res = await daemonFetch(
|
|
61
|
-
`/api/agents/${encodeURIComponent(agentName)}/connectors/${encodeURIComponent(type)}`,
|
|
62
|
-
{ method: "POST" }
|
|
63
|
-
);
|
|
64
|
-
if (!res.ok) {
|
|
65
|
-
const body = await res.json().catch(() => ({ error: "Unknown error" }));
|
|
66
|
-
console.error(`Failed to start ${type} connector: ${body.error}`);
|
|
67
|
-
process.exit(1);
|
|
68
|
-
}
|
|
69
|
-
console.log(`${type} connector for ${agentName} started.`);
|
|
70
|
-
}
|
|
71
|
-
async function disconnectConnector(args) {
|
|
72
|
-
const { positional, flags } = parseArgs(args, {
|
|
73
|
-
agent: { type: "string" }
|
|
74
|
-
});
|
|
75
|
-
const agentName = resolveAgentName(flags);
|
|
76
|
-
const type = positional[0];
|
|
77
|
-
if (!type) {
|
|
78
|
-
console.error("Usage: volute connector disconnect <type> [--agent <name>]");
|
|
79
|
-
process.exit(1);
|
|
80
|
-
}
|
|
81
|
-
const res = await daemonFetch(
|
|
82
|
-
`/api/agents/${encodeURIComponent(agentName)}/connectors/${encodeURIComponent(type)}`,
|
|
83
|
-
{
|
|
84
|
-
method: "DELETE"
|
|
85
|
-
}
|
|
86
|
-
);
|
|
87
|
-
if (!res.ok) {
|
|
88
|
-
const body = await res.json().catch(() => ({ error: "Unknown error" }));
|
|
89
|
-
console.error(`Failed to stop ${type} connector: ${body.error}`);
|
|
90
|
-
process.exit(1);
|
|
91
|
-
}
|
|
92
|
-
console.log(`${type} connector for ${agentName} stopped.`);
|
|
93
|
-
}
|
|
94
|
-
export {
|
|
95
|
-
run
|
|
96
|
-
};
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
|
|
3
|
-
export type SessionRule = {
|
|
4
|
-
session: string;
|
|
5
|
-
[key: string]: string; // all other keys are match criteria
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
export type SessionConfig = {
|
|
9
|
-
rules?: SessionRule[];
|
|
10
|
-
default?: string;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export function loadSessionConfig(configPath: string): SessionConfig {
|
|
14
|
-
try {
|
|
15
|
-
return JSON.parse(readFileSync(configPath, "utf-8"));
|
|
16
|
-
} catch {
|
|
17
|
-
return {};
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Match a glob-like pattern against a string.
|
|
23
|
-
* Supports only `*` as wildcard (matches any sequence of characters).
|
|
24
|
-
*/
|
|
25
|
-
function globMatch(pattern: string, value: string): boolean {
|
|
26
|
-
// Escape regex special chars except *, then replace * with .*
|
|
27
|
-
const regex = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
28
|
-
return new RegExp(`^${regex}$`).test(value);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Resolve which session a message should route to based on the config.
|
|
33
|
-
* Returns the session name (with template variables expanded, path-safe).
|
|
34
|
-
*/
|
|
35
|
-
export function resolveSession(
|
|
36
|
-
config: SessionConfig,
|
|
37
|
-
meta: { channel?: string; sender?: string },
|
|
38
|
-
): string {
|
|
39
|
-
const fallback = config.default ?? "main";
|
|
40
|
-
if (!config.rules) return fallback;
|
|
41
|
-
|
|
42
|
-
for (const rule of config.rules) {
|
|
43
|
-
if (ruleMatches(rule, meta)) {
|
|
44
|
-
return sanitizeSessionName(expandTemplate(rule.session, meta));
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return fallback;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const MATCH_KEYS = new Set(["channel", "sender"]);
|
|
52
|
-
|
|
53
|
-
function ruleMatches(rule: SessionRule, meta: { channel?: string; sender?: string }): boolean {
|
|
54
|
-
for (const [key, pattern] of Object.entries(rule)) {
|
|
55
|
-
if (key === "session") continue;
|
|
56
|
-
if (!MATCH_KEYS.has(key)) return false;
|
|
57
|
-
const value = meta[key as keyof typeof meta] ?? "";
|
|
58
|
-
if (!globMatch(pattern, value)) return false;
|
|
59
|
-
}
|
|
60
|
-
return true;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function expandTemplate(template: string, meta: { channel?: string; sender?: string }): string {
|
|
64
|
-
return template
|
|
65
|
-
.replace(/\$\{sender\}/g, meta.sender ?? "unknown")
|
|
66
|
-
.replace(/\$\{channel\}/g, meta.channel ?? "unknown");
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function sanitizeSessionName(name: string): string {
|
|
70
|
-
return name.replace(/\0/g, "").replace(/[/\\]/g, "-").replace(/\.\./g, "-").slice(0, 100);
|
|
71
|
-
}
|