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,15 +1,58 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { resolve as resolvePath } from "node:path";
|
|
3
|
+
import type { HookCallback } from "@anthropic-ai/claude-agent-sdk";
|
|
4
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
3
5
|
import { createAutoCommitHook } from "./lib/hooks/auto-commit.js";
|
|
4
6
|
import { createIdentityReloadHook } from "./lib/hooks/identity-reload.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
import { createPreCompactHook } from "./lib/hooks/pre-compact.js";
|
|
8
|
+
import { log, logText, logThinking, logToolUse } from "./lib/logger.js";
|
|
9
|
+
import { createMessageChannel } from "./lib/message-channel.js";
|
|
10
|
+
import type {
|
|
11
|
+
HandlerMeta,
|
|
12
|
+
HandlerResolver,
|
|
13
|
+
Listener,
|
|
14
|
+
MessageHandler,
|
|
15
|
+
VoluteContentPart,
|
|
16
|
+
VoluteEvent,
|
|
11
17
|
} from "./lib/types.js";
|
|
12
18
|
|
|
19
|
+
type SDKContent = (
|
|
20
|
+
| { type: "text"; text: string }
|
|
21
|
+
| {
|
|
22
|
+
type: "image";
|
|
23
|
+
source: {
|
|
24
|
+
type: "base64";
|
|
25
|
+
media_type: "image/jpeg" | "image/png" | "image/gif" | "image/webp";
|
|
26
|
+
data: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
)[];
|
|
30
|
+
|
|
31
|
+
type Session = {
|
|
32
|
+
name: string;
|
|
33
|
+
channel: ReturnType<typeof createMessageChannel>;
|
|
34
|
+
listeners: Set<Listener>;
|
|
35
|
+
messageIds: (string | undefined)[];
|
|
36
|
+
currentMessageId?: string;
|
|
37
|
+
currentQuery?: ReturnType<typeof query>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function toSDKContent(content: VoluteContentPart[]): SDKContent {
|
|
41
|
+
return content.map((part) => {
|
|
42
|
+
if (part.type === "text") {
|
|
43
|
+
return { type: "text" as const, text: part.text };
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
type: "image" as const,
|
|
47
|
+
source: {
|
|
48
|
+
type: "base64" as const,
|
|
49
|
+
media_type: part.media_type as "image/jpeg" | "image/png" | "image/gif" | "image/webp",
|
|
50
|
+
data: part.data,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
13
56
|
export function createAgent(options: {
|
|
14
57
|
systemPrompt: string;
|
|
15
58
|
cwd: string;
|
|
@@ -18,95 +61,238 @@ export function createAgent(options: {
|
|
|
18
61
|
sessionsDir: string;
|
|
19
62
|
compactionMessage?: string;
|
|
20
63
|
onIdentityReload?: () => Promise<void>;
|
|
21
|
-
}) {
|
|
64
|
+
}): { resolve: HandlerResolver; waitForCommits: () => Promise<void> } {
|
|
22
65
|
const autoCommit = createAutoCommitHook(options.cwd);
|
|
23
66
|
const identityReload = createIdentityReloadHook(options.cwd);
|
|
67
|
+
const postToolUseHooks: { matcher: string; hooks: HookCallback[] }[] = [
|
|
68
|
+
{ matcher: "Edit|Write", hooks: [autoCommit.hook, identityReload.hook] },
|
|
69
|
+
];
|
|
24
70
|
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
71
|
+
const sessions = new Map<string, Session>();
|
|
72
|
+
const compactionMessage =
|
|
73
|
+
options.compactionMessage ??
|
|
74
|
+
"Your conversation is approaching its context limit. Please update today's journal entry to preserve important context before the conversation is compacted.";
|
|
75
|
+
|
|
76
|
+
// --- Session persistence ---
|
|
77
|
+
|
|
78
|
+
function sessionFilePath(sessionName: string): string {
|
|
79
|
+
return resolvePath(options.sessionsDir, `${sessionName}.json`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function loadSessionId(sessionName: string): string | undefined {
|
|
83
|
+
try {
|
|
84
|
+
const data = JSON.parse(readFileSync(sessionFilePath(sessionName), "utf-8"));
|
|
85
|
+
return data.sessionId;
|
|
86
|
+
} catch (err: any) {
|
|
87
|
+
if (err?.code !== "ENOENT") {
|
|
88
|
+
log("agent", `failed to load session file for "${sessionName}":`, err);
|
|
36
89
|
}
|
|
37
|
-
|
|
38
|
-
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
39
93
|
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const text =
|
|
45
|
-
typeof content === "string"
|
|
46
|
-
? content
|
|
47
|
-
: content.map((p) => (p.type === "text" ? p.text : `[${p.type}]`)).join(" ");
|
|
48
|
-
logMessage("in", text, meta?.channel);
|
|
49
|
-
|
|
50
|
-
const time = new Date().toLocaleString();
|
|
51
|
-
const prefix = formatPrefix(meta, time);
|
|
52
|
-
|
|
53
|
-
let sdkContent: (
|
|
54
|
-
| { type: "text"; text: string }
|
|
55
|
-
| {
|
|
56
|
-
type: "image";
|
|
57
|
-
source: {
|
|
58
|
-
type: "base64";
|
|
59
|
-
media_type: "image/jpeg" | "image/png" | "image/gif" | "image/webp";
|
|
60
|
-
data: string;
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
)[];
|
|
94
|
+
function saveSessionId(sessionName: string, sessionId: string) {
|
|
95
|
+
mkdirSync(options.sessionsDir, { recursive: true });
|
|
96
|
+
writeFileSync(sessionFilePath(sessionName), JSON.stringify({ sessionId }));
|
|
97
|
+
}
|
|
64
98
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
99
|
+
function deleteSessionId(sessionName: string) {
|
|
100
|
+
try {
|
|
101
|
+
const path = sessionFilePath(sessionName);
|
|
102
|
+
if (existsSync(path)) unlinkSync(path);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
log("agent", `failed to delete session file for "${sessionName}":`, err);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// --- Event broadcasting ---
|
|
109
|
+
|
|
110
|
+
function broadcastToSession(session: Session, event: VoluteEvent) {
|
|
111
|
+
const tagged =
|
|
112
|
+
session.currentMessageId != null ? { ...event, messageId: session.currentMessageId } : event;
|
|
113
|
+
for (const listener of session.listeners) {
|
|
114
|
+
try {
|
|
115
|
+
listener(tagged);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
log("agent", "listener threw during broadcast:", err);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// --- SDK stream management ---
|
|
123
|
+
|
|
124
|
+
function createStream(session: Session, resume?: string) {
|
|
125
|
+
const preCompact = createPreCompactHook(() => {
|
|
126
|
+
session.messageIds.push(undefined);
|
|
127
|
+
session.channel.push({
|
|
128
|
+
type: "user",
|
|
129
|
+
session_id: "",
|
|
130
|
+
message: {
|
|
131
|
+
role: "user",
|
|
132
|
+
content: [{ type: "text", text: compactionMessage }],
|
|
133
|
+
},
|
|
134
|
+
parent_tool_use_id: null,
|
|
81
135
|
});
|
|
82
|
-
|
|
83
|
-
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return query({
|
|
139
|
+
prompt: session.channel.iterable,
|
|
140
|
+
options: {
|
|
141
|
+
systemPrompt: options.systemPrompt,
|
|
142
|
+
permissionMode: "bypassPermissions",
|
|
143
|
+
allowDangerouslySkipPermissions: true,
|
|
144
|
+
settingSources: ["project"],
|
|
145
|
+
cwd: options.cwd,
|
|
146
|
+
abortController: options.abortController,
|
|
147
|
+
model: options.model,
|
|
148
|
+
resume,
|
|
149
|
+
hooks: {
|
|
150
|
+
PostToolUse: postToolUseHooks,
|
|
151
|
+
PreCompact: [{ hooks: [preCompact.hook] }],
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function consumeStream(stream: ReturnType<typeof query>, session: Session) {
|
|
158
|
+
for await (const msg of stream) {
|
|
159
|
+
if (session.currentMessageId === undefined) {
|
|
160
|
+
session.currentMessageId = session.messageIds.shift();
|
|
161
|
+
}
|
|
162
|
+
if ("session_id" in msg && msg.session_id) {
|
|
163
|
+
if (!session.name.startsWith("new-")) {
|
|
164
|
+
saveSessionId(session.name, msg.session_id as string);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (msg.type === "assistant") {
|
|
168
|
+
for (const b of msg.message.content) {
|
|
169
|
+
if (b.type === "thinking" && "thinking" in b && b.thinking) {
|
|
170
|
+
logThinking(b.thinking as string);
|
|
171
|
+
} else if (b.type === "text") {
|
|
172
|
+
const text = (b as { text: string }).text;
|
|
173
|
+
logText(text);
|
|
174
|
+
broadcastToSession(session, { type: "text", content: text });
|
|
175
|
+
} else if (b.type === "tool_use") {
|
|
176
|
+
const tb = b as { name: string; input: unknown };
|
|
177
|
+
logToolUse(tb.name, tb.input);
|
|
178
|
+
broadcastToSession(session, { type: "tool_use", name: tb.name, input: tb.input });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (msg.type === "result") {
|
|
183
|
+
log("agent", `session "${session.name}": turn done`);
|
|
184
|
+
broadcastToSession(session, { type: "done" });
|
|
185
|
+
session.currentMessageId = undefined;
|
|
186
|
+
if (identityReload.needsReload()) {
|
|
187
|
+
options.onIdentityReload?.();
|
|
188
|
+
}
|
|
84
189
|
}
|
|
85
190
|
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function startSession(session: Session, savedSessionId?: string) {
|
|
194
|
+
(async () => {
|
|
195
|
+
log("agent", `session "${session.name}": stream consumer started`);
|
|
196
|
+
try {
|
|
197
|
+
const q = createStream(session, savedSessionId);
|
|
198
|
+
session.currentQuery = q;
|
|
199
|
+
await consumeStream(q, session);
|
|
200
|
+
} catch (err) {
|
|
201
|
+
if (savedSessionId) {
|
|
202
|
+
log("agent", `session "${session.name}": resume failed, starting fresh:`, err);
|
|
203
|
+
deleteSessionId(session.name);
|
|
204
|
+
try {
|
|
205
|
+
const q = createStream(session);
|
|
206
|
+
session.currentQuery = q;
|
|
207
|
+
await consumeStream(q, session);
|
|
208
|
+
} catch (retryErr) {
|
|
209
|
+
log("agent", `session "${session.name}": stream consumer error:`, retryErr);
|
|
210
|
+
broadcastToSession(session, { type: "done" });
|
|
211
|
+
sessions.delete(session.name);
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
log("agent", `session "${session.name}": stream consumer error:`, err);
|
|
215
|
+
broadcastToSession(session, { type: "done" });
|
|
216
|
+
sessions.delete(session.name);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
log("agent", `session "${session.name}": stream consumer ended`);
|
|
220
|
+
})();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function getOrCreateSession(name: string): Session {
|
|
224
|
+
const existing = sessions.get(name);
|
|
225
|
+
if (existing) return existing;
|
|
226
|
+
|
|
227
|
+
const session: Session = {
|
|
228
|
+
name,
|
|
229
|
+
channel: createMessageChannel(),
|
|
230
|
+
listeners: new Set(),
|
|
231
|
+
messageIds: [],
|
|
232
|
+
};
|
|
233
|
+
sessions.set(name, session);
|
|
86
234
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
235
|
+
const isEphemeral = name.startsWith("new-");
|
|
236
|
+
const savedSessionId = isEphemeral ? undefined : loadSessionId(name);
|
|
237
|
+
if (savedSessionId) {
|
|
238
|
+
log("agent", `session "${name}": resuming ${savedSessionId}`);
|
|
239
|
+
} else {
|
|
240
|
+
log("agent", `session "${name}": starting fresh`);
|
|
90
241
|
}
|
|
91
242
|
|
|
92
|
-
session
|
|
93
|
-
session
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
243
|
+
startSession(session, savedSessionId);
|
|
244
|
+
return session;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// --- MessageHandler implementation ---
|
|
248
|
+
|
|
249
|
+
function createSessionHandler(sessionName: string): MessageHandler {
|
|
250
|
+
return {
|
|
251
|
+
handle(content: VoluteContentPart[], meta: HandlerMeta, listener: Listener): () => void {
|
|
252
|
+
const session = getOrCreateSession(sessionName);
|
|
253
|
+
|
|
254
|
+
// Filter listener to only receive events for this messageId
|
|
255
|
+
const filteredListener: Listener = (event) => {
|
|
256
|
+
if (event.messageId === meta.messageId) listener(event);
|
|
257
|
+
};
|
|
258
|
+
session.listeners.add(filteredListener);
|
|
259
|
+
|
|
260
|
+
// Interrupt if requested and session is mid-turn
|
|
261
|
+
if (meta.interrupt && session.currentMessageId !== undefined && session.currentQuery) {
|
|
262
|
+
log("agent", `session "${sessionName}": interrupting current turn`);
|
|
263
|
+
session.currentQuery.interrupt();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Push message into SDK
|
|
267
|
+
session.messageIds.push(meta.messageId);
|
|
268
|
+
session.channel.push({
|
|
269
|
+
type: "user",
|
|
270
|
+
session_id: "",
|
|
271
|
+
message: { role: "user", content: toSDKContent(content) },
|
|
272
|
+
parent_tool_use_id: null,
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
return () => session.listeners.delete(filteredListener);
|
|
99
276
|
},
|
|
100
|
-
|
|
101
|
-
});
|
|
277
|
+
};
|
|
102
278
|
}
|
|
103
279
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
280
|
+
// --- HandlerResolver ---
|
|
281
|
+
|
|
282
|
+
const handlers = new Map<string, MessageHandler>();
|
|
283
|
+
|
|
284
|
+
function resolve(sessionName: string): MessageHandler {
|
|
285
|
+
// Ephemeral sessions get unique names — don't cache their handlers
|
|
286
|
+
if (sessionName.startsWith("new-")) {
|
|
287
|
+
return createSessionHandler(sessionName);
|
|
288
|
+
}
|
|
289
|
+
let handler = handlers.get(sessionName);
|
|
290
|
+
if (!handler) {
|
|
291
|
+
handler = createSessionHandler(sessionName);
|
|
292
|
+
handlers.set(sessionName, handler);
|
|
293
|
+
}
|
|
294
|
+
return handler;
|
|
109
295
|
}
|
|
110
296
|
|
|
111
|
-
return {
|
|
297
|
+
return { resolve, waitForCommits: autoCommit.waitForCommits };
|
|
112
298
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, renameSync } from "node:fs";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { createAgent } from "./agent.js";
|
|
4
|
+
import { createFileHandlerResolver } from "./lib/file-handler.js";
|
|
4
5
|
import { log } from "./lib/logger.js";
|
|
6
|
+
import { createRouter } from "./lib/router.js";
|
|
5
7
|
import {
|
|
6
8
|
handleMergeContext,
|
|
7
9
|
loadConfig,
|
|
@@ -44,19 +46,26 @@ const agent = createAgent({
|
|
|
44
46
|
},
|
|
45
47
|
});
|
|
46
48
|
|
|
49
|
+
const router = createRouter({
|
|
50
|
+
configPath: resolve("home/.config/sessions.json"),
|
|
51
|
+
agentHandler: agent.resolve,
|
|
52
|
+
fileHandler: createFileHandlerResolver(resolve("home")),
|
|
53
|
+
});
|
|
54
|
+
|
|
47
55
|
const server = createVoluteServer({
|
|
48
|
-
|
|
56
|
+
router,
|
|
49
57
|
port,
|
|
50
58
|
name: pkg.name,
|
|
51
59
|
version: pkg.version,
|
|
52
|
-
sessionsConfigPath: resolve("home/.config/sessions.json"),
|
|
53
60
|
});
|
|
54
61
|
|
|
55
62
|
server.listen(port, () => {
|
|
56
63
|
const addr = server.address();
|
|
57
64
|
const actualPort = typeof addr === "object" && addr ? addr.port : port;
|
|
58
65
|
log("server", `listening on :${actualPort}`);
|
|
59
|
-
handleMergeContext((content) =>
|
|
66
|
+
handleMergeContext((content) =>
|
|
67
|
+
router.route([{ type: "text", text: content }], { channel: "system" }),
|
|
68
|
+
);
|
|
60
69
|
});
|
|
61
70
|
|
|
62
71
|
setupShutdown();
|