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.
Files changed (82) hide show
  1. package/README.md +29 -29
  2. package/dist/agent-Z2B6EFEQ.js +75 -0
  3. package/dist/{agent-manager-AUCKMGPR.js → agent-manager-PXBKA2GK.js} +4 -4
  4. package/dist/channel-MK5OK2SI.js +113 -0
  5. package/dist/chunk-5X7HGB6L.js +107 -0
  6. package/dist/{chunk-YGFIWIOF.js → chunk-7L4AN5D4.js} +1 -1
  7. package/dist/{chunk-VRVVQIYY.js → chunk-AZEL2IEK.js} +1 -1
  8. package/dist/chunk-B3R6L2GW.js +24 -0
  9. package/dist/{chunk-DNOXHLE5.js → chunk-HE67X4T6.js} +1 -1
  10. package/dist/{chunk-I6OHXCMV.js → chunk-MW2KFO3B.js} +47 -9
  11. package/dist/chunk-MXUCNIBG.js +168 -0
  12. package/dist/chunk-SMISE4SV.js +226 -0
  13. package/dist/{chunk-SOZA2TLP.js → chunk-UAVD2AHX.js} +1 -1
  14. package/dist/{chunk-3C2XR4IY.js → chunk-UX25Z2ND.js} +113 -107
  15. package/dist/{chunk-GSPKUPKU.js → chunk-XUA3JUFK.js} +2 -1
  16. package/dist/chunk-ZYGKG6VC.js +22 -0
  17. package/dist/cli.js +98 -75
  18. package/dist/connector-LYEMXQEV.js +157 -0
  19. package/dist/connectors/discord.js +104 -161
  20. package/dist/connectors/slack.js +179 -0
  21. package/dist/connectors/telegram.js +175 -0
  22. package/dist/conversation-ERXEQZTY.js +163 -0
  23. package/dist/create-RVCZN6HE.js +91 -0
  24. package/dist/{daemon-client-XR24PUJF.js → daemon-client-ZY6UUN2M.js} +2 -2
  25. package/dist/daemon.js +824 -252
  26. package/dist/{delete-GQ7JEK2S.js → delete-3QH7VYIN.js} +8 -9
  27. package/dist/{down-3OB6UVAJ.js → down-O7IFZLVJ.js} +1 -1
  28. package/dist/{env-JB27UAC3.js → env-4D4REPJF.js} +8 -5
  29. package/dist/{history-3VRUBGGV.js → history-OEONB53Z.js} +5 -5
  30. package/dist/{import-K4MP2GX7.js → import-MXJB2EII.js} +23 -8
  31. package/dist/{logs-NXFFGUKY.js → logs-DF342W4M.js} +2 -2
  32. package/dist/message-ADHWFHSI.js +32 -0
  33. package/dist/package-VQOE7JNH.js +89 -0
  34. package/dist/{schedule-4I5TYHFH.js → schedule-NAG6F463.js} +12 -7
  35. package/dist/send-66QMKRUH.js +75 -0
  36. package/dist/{setup-SRS7AUAA.js → setup-RPRRGG2F.js} +6 -6
  37. package/dist/{start-LDPMCMYT.js → start-TUOXDSFL.js} +3 -3
  38. package/dist/{status-MVSQG54T.js → status-A36EHRO4.js} +3 -3
  39. package/dist/{stop-5PZTZCLL.js → stop-AOJZLQ5X.js} +6 -7
  40. package/dist/{up-UT3IMKCA.js → up-7ILD7GU7.js} +2 -2
  41. package/dist/update-LPSIAWQ2.js +140 -0
  42. package/dist/update-check-Y33QDCFL.js +17 -0
  43. package/dist/{upgrade-CDKECCGN.js → upgrade-FX2TKJ2S.js} +16 -15
  44. package/dist/{variant-CVYM3EQG.js → variant-LAB67OC2.js} +17 -12
  45. package/dist/web-assets/assets/index-BbRmoxoA.js +308 -0
  46. package/dist/web-assets/index.html +2 -2
  47. package/drizzle/0003_clean_ego.sql +12 -0
  48. package/drizzle/meta/0003_snapshot.json +417 -0
  49. package/drizzle/meta/_journal.json +7 -0
  50. package/package.json +3 -1
  51. package/templates/_base/.init/.config/hooks/startup-context.sh +19 -1
  52. package/templates/_base/_skills/volute-agent/SKILL.md +112 -16
  53. package/templates/_base/home/.config/routes.json +10 -0
  54. package/templates/_base/home/VOLUTE.md +19 -28
  55. package/templates/_base/src/lib/file-handler.ts +46 -0
  56. package/templates/_base/src/lib/format-prefix.ts +1 -1
  57. package/templates/_base/src/lib/router.ts +327 -0
  58. package/templates/_base/src/lib/routing.ts +137 -0
  59. package/templates/_base/src/lib/types.ts +16 -3
  60. package/templates/_base/src/lib/volute-server.ts +20 -48
  61. package/templates/agent-sdk/.init/.config/routes.json +5 -0
  62. package/templates/agent-sdk/.init/CLAUDE.md +2 -2
  63. package/templates/agent-sdk/src/agent.ts +269 -82
  64. package/templates/agent-sdk/src/server.ts +19 -4
  65. package/templates/agent-sdk/volute-template.json +1 -1
  66. package/templates/pi/.init/.config/routes.json +5 -0
  67. package/templates/pi/.init/AGENTS.md +1 -1
  68. package/templates/pi/src/agent.ts +279 -58
  69. package/templates/pi/src/server.ts +15 -4
  70. package/templates/pi/volute-template.json +1 -1
  71. package/dist/channel-7FZ6D25H.js +0 -90
  72. package/dist/chunk-N4YNKR3Q.js +0 -90
  73. package/dist/connector-TVJULIRT.js +0 -96
  74. package/dist/create-BRG2DBWI.js +0 -79
  75. package/dist/send-UK3JBZIB.js +0 -53
  76. package/dist/web-assets/assets/index-BC5eSqbY.js +0 -296
  77. package/templates/_base/src/lib/sessions.ts +0 -71
  78. package/templates/agent-sdk/.init/.config/sessions.json +0 -4
  79. package/templates/agent-sdk/src/lib/agent-sessions.ts +0 -204
  80. package/templates/pi/.init/.config/sessions.json +0 -1
  81. package/templates/pi/src/lib/agent-sessions.ts +0 -210
  82. package/dist/{service-SA4TTMDU.js → service-HZNIDNJF.js} +3 -3
@@ -1,74 +1,295 @@
1
1
  import type { ImageContent } from "@mariozechner/pi-ai";
2
- import { createPiSessionManager } from "./lib/agent-sessions.js";
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
- type ChannelMeta,
7
- INTERACTIVE_CHANNELS,
8
- type Listener,
9
- type VoluteContentPart,
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
+ function defaultCompactionMessage(): string {
36
+ const today = new Date().toISOString().slice(0, 10);
37
+ return `Context is getting long — compaction is about to summarize this conversation. Before that happens, save anything important to files (MEMORY.md, memory/journal/${today}.md, etc.) since those survive compaction. Focus on: decisions made, open tasks, and anything you'd need to pick up where you left off.`;
38
+ }
39
+
40
+ function resolveModel(modelStr: string) {
41
+ const [provider, ...rest] = modelStr.split(":");
42
+ const modelId = rest.join(":");
43
+
44
+ // Try exact match first, then prefix match against available models
45
+ let model = getModel(provider as any, modelId as any);
46
+ if (!model) {
47
+ const available = getModels(provider as any);
48
+ const found = available.find((m) => m.id.startsWith(modelId));
49
+ if (found) model = found;
50
+ }
51
+ if (!model) {
52
+ const available = getModels(provider as any);
53
+ throw new Error(
54
+ `Model not found: ${modelStr}\nAvailable ${provider} models: ${available.map((m) => m.id).join(", ")}`,
55
+ );
56
+ }
57
+ return model;
58
+ }
59
+
60
+ function extractText(content: VoluteContentPart[]): string {
61
+ return content
62
+ .filter((p): p is { type: "text"; text: string } => p.type === "text")
63
+ .map((p) => p.text)
64
+ .join("\n");
65
+ }
66
+
67
+ function extractImages(content: VoluteContentPart[]): ImageContent[] {
68
+ return content
69
+ .filter((p): p is { type: "image"; media_type: string; data: string } => p.type === "image")
70
+ .map((p) => ({ type: "image" as const, mimeType: p.media_type, data: p.data }));
71
+ }
72
+
12
73
  export function createAgent(options: {
13
74
  systemPrompt: string;
14
75
  cwd: string;
15
76
  model?: string;
16
77
  compactionMessage?: string;
17
- }) {
18
- const { getOrCreateSession, interruptSession } = createPiSessionManager(options);
19
-
20
- function sendMessage(content: string | VoluteContentPart[], meta?: ChannelMeta) {
21
- const raw =
22
- typeof content === "string"
23
- ? content
24
- : content
25
- .filter((p) => p.type === "text")
26
- .map((p) => (p as { text: string }).text)
27
- .join("\n");
28
- logMessage("in", raw, meta?.channel);
29
-
30
- const sessionName = meta?.sessionName ?? "main";
31
- const session = getOrCreateSession(sessionName);
32
-
33
- // Build context prefix from channel metadata
34
- const prefix = formatPrefix(meta, new Date().toLocaleString());
35
- const text = prefix + raw;
36
-
37
- // Convert image parts to pi-ai ImageContent format
38
- const images: ImageContent[] | undefined =
39
- typeof content === "string"
40
- ? undefined
41
- : content
42
- .filter((p) => p.type === "image")
43
- .map((p) => ({ type: "image" as const, mimeType: p.media_type, data: p.data }));
44
-
45
- const opts = images?.length ? { images } : {};
46
-
47
- // Track messageId for this turn (must be pushed before prompt)
48
- session.messageIds.push(meta?.messageId);
49
-
50
- // Fire-and-forget: await session ready then prompt
51
- (async () => {
52
- await session.ready;
53
- if (session.agentSession!.isStreaming) {
54
- if (INTERACTIVE_CHANNELS.has(meta?.channel ?? "")) {
55
- interruptSession(sessionName);
56
- session.agentSession!.prompt(text, { streamingBehavior: "steer", ...opts });
57
- } else {
58
- session.agentSession!.prompt(text, { streamingBehavior: "followUp", ...opts });
78
+ }): { resolve: HandlerResolver } {
79
+ const sessions = new Map<string, PiSession>();
80
+ const compactionMessage = options.compactionMessage ?? defaultCompactionMessage();
81
+
82
+ // Shared setup (created once)
83
+ const modelStr = options.model || process.env.PI_MODEL || "anthropic:claude-sonnet-4-20250514";
84
+ const model = resolveModel(modelStr);
85
+ const authStorage = new AuthStorage();
86
+ const modelRegistry = new ModelRegistry(authStorage);
87
+
88
+ // --- Session lifecycle ---
89
+
90
+ function getOrCreateSession(name: string): PiSession {
91
+ const existing = sessions.get(name);
92
+ if (existing) return existing;
93
+
94
+ const session: PiSession = {
95
+ name,
96
+ agentSession: null,
97
+ ready: Promise.resolve(),
98
+ listeners: new Set(),
99
+ messageIds: [],
100
+ };
101
+ sessions.set(name, session);
102
+
103
+ session.ready = initSession(session).catch((err) => {
104
+ log("agent", `session "${session.name}": init failed:`, err);
105
+ });
106
+ return session;
107
+ }
108
+
109
+ async function initSession(session: PiSession) {
110
+ const isEphemeral = session.name.startsWith("new-");
111
+
112
+ const sessionManager = isEphemeral
113
+ ? SessionManager.inMemory()
114
+ : SessionManager.continueRecent(options.cwd, `.volute/pi-sessions/${session.name}`);
115
+
116
+ log("agent", `session "${session.name}": ${isEphemeral ? "ephemeral" : "persistent"}`);
117
+
118
+ let compactBlocked = false;
119
+ const preCompactExtension: ExtensionFactory = (pi) => {
120
+ pi.on("session_before_compact", () => {
121
+ if (!compactBlocked) {
122
+ compactBlocked = true;
123
+ log(
124
+ "agent",
125
+ `session "${session.name}": blocking compaction — asking agent to update daily log`,
126
+ );
127
+ session.messageIds.push(undefined);
128
+ session.agentSession?.prompt(compactionMessage, { streamingBehavior: "followUp" });
129
+ return { cancel: true };
59
130
  }
60
- } else {
61
- session.agentSession!.prompt(text, opts);
131
+ compactBlocked = false;
132
+ log("agent", `session "${session.name}": allowing compaction`);
133
+ });
134
+ };
135
+
136
+ const settingsManager = SettingsManager.inMemory({
137
+ retry: { enabled: true, maxRetries: 3 },
138
+ });
139
+
140
+ const resourceLoader = new DefaultResourceLoader({
141
+ cwd: options.cwd,
142
+ settingsManager,
143
+ systemPrompt: options.systemPrompt,
144
+ extensionFactories: [preCompactExtension],
145
+ });
146
+ await resourceLoader.reload();
147
+
148
+ const { session: agentSession } = await createAgentSession({
149
+ cwd: options.cwd,
150
+ model,
151
+ authStorage,
152
+ modelRegistry,
153
+ sessionManager,
154
+ settingsManager,
155
+ resourceLoader,
156
+ });
157
+
158
+ session.agentSession = agentSession;
159
+
160
+ // Per-session event subscription
161
+ const toolArgs = new Map<string, any>();
162
+
163
+ session.unsubscribe = agentSession.subscribe((event) => {
164
+ if (session.currentMessageId === undefined) {
165
+ session.currentMessageId = session.messageIds.shift();
62
166
  }
63
- })();
167
+
168
+ if (event.type === "message_update") {
169
+ const ae = event.assistantMessageEvent;
170
+ if (ae.type === "text_delta") {
171
+ logText(ae.delta);
172
+ broadcast(session, { type: "text", content: ae.delta });
173
+ } else if (ae.type === "thinking_delta") {
174
+ logThinking(ae.delta);
175
+ }
176
+ }
177
+
178
+ if (event.type === "tool_execution_start") {
179
+ toolArgs.set(event.toolCallId, event.args);
180
+ logToolUse(event.toolName, event.args);
181
+ broadcast(session, { type: "tool_use", name: event.toolName, input: event.args });
182
+ }
183
+
184
+ if (event.type === "tool_execution_end") {
185
+ const output =
186
+ typeof event.result === "string" ? event.result : JSON.stringify(event.result);
187
+ logToolResult(event.toolName, output, event.isError);
188
+ broadcast(session, { type: "tool_result", output, is_error: event.isError });
189
+
190
+ // Auto-commit file changes in home/
191
+ if ((event.toolName === "edit" || event.toolName === "write") && !event.isError) {
192
+ const args = toolArgs.get(event.toolCallId);
193
+ const filePath = (args as { path?: string })?.path;
194
+ if (filePath) {
195
+ commitFileChange(filePath, options.cwd);
196
+ }
197
+ }
198
+ toolArgs.delete(event.toolCallId);
199
+ }
200
+
201
+ if (event.type === "agent_end") {
202
+ log("agent", `session "${session.name}": turn done`);
203
+ broadcast(session, { type: "done" });
204
+ session.currentMessageId = undefined;
205
+ }
206
+ });
207
+
208
+ log("agent", `session "${session.name}": ready`);
209
+ }
210
+
211
+ // --- Event broadcasting ---
212
+
213
+ function broadcast(session: PiSession, event: VoluteEvent) {
214
+ const tagged =
215
+ session.currentMessageId != null ? { ...event, messageId: session.currentMessageId } : event;
216
+ for (const listener of session.listeners) {
217
+ try {
218
+ listener(tagged);
219
+ } catch (err) {
220
+ log("agent", "listener threw during broadcast:", err);
221
+ }
222
+ }
223
+ }
224
+
225
+ function interruptSession(name: string) {
226
+ const session = sessions.get(name);
227
+ if (session?.currentMessageId !== undefined) {
228
+ log("agent", `session "${name}": interrupting current turn`);
229
+ broadcast(session, { type: "done" });
230
+ session.currentMessageId = undefined;
231
+ }
232
+ }
233
+
234
+ // --- MessageHandler implementation ---
235
+
236
+ function createSessionHandler(sessionName: string): MessageHandler {
237
+ return {
238
+ handle(content: VoluteContentPart[], meta: HandlerMeta, listener: Listener): () => void {
239
+ const session = getOrCreateSession(sessionName);
240
+
241
+ // Filter listener to only receive events for this messageId
242
+ const filteredListener: Listener = (event) => {
243
+ if (event.messageId === meta.messageId) listener(event);
244
+ };
245
+ session.listeners.add(filteredListener);
246
+
247
+ // Track messageId (must be pushed before prompt)
248
+ session.messageIds.push(meta.messageId);
249
+
250
+ const text = extractText(content);
251
+ const images = extractImages(content);
252
+ const opts = images.length ? { images } : {};
253
+
254
+ // Fire-and-forget: await session ready then prompt
255
+ (async () => {
256
+ await session.ready;
257
+ if (session.agentSession!.isStreaming) {
258
+ if (meta.interrupt) {
259
+ interruptSession(sessionName);
260
+ session.agentSession!.prompt(text, { streamingBehavior: "steer", ...opts });
261
+ } else {
262
+ session.agentSession!.prompt(text, { streamingBehavior: "followUp", ...opts });
263
+ }
264
+ } else {
265
+ session.agentSession!.prompt(text, opts);
266
+ }
267
+ })().catch((err) => {
268
+ log("agent", `session "${sessionName}": prompt failed:`, err);
269
+ broadcast(session, { type: "done" });
270
+ });
271
+
272
+ return () => session.listeners.delete(filteredListener);
273
+ },
274
+ };
64
275
  }
65
276
 
66
- function onMessage(listener: Listener, sessionName?: string): () => void {
67
- const name = sessionName ?? "main";
68
- const session = getOrCreateSession(name);
69
- session.listeners.add(listener);
70
- return () => session.listeners.delete(listener);
277
+ // --- HandlerResolver ---
278
+
279
+ const handlers = new Map<string, MessageHandler>();
280
+
281
+ function resolve(sessionName: string): MessageHandler {
282
+ // Ephemeral sessions get unique names — don't cache their handlers
283
+ if (sessionName.startsWith("new-")) {
284
+ return createSessionHandler(sessionName);
285
+ }
286
+ let handler = handlers.get(sessionName);
287
+ if (!handler) {
288
+ handler = createSessionHandler(sessionName);
289
+ handlers.set(sessionName, handler);
290
+ }
291
+ return handler;
71
292
  }
72
293
 
73
- return { sendMessage, onMessage };
294
+ return { resolve };
74
295
  }
@@ -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/routes.json"),
33
+ agentHandler: agent.resolve,
34
+ fileHandler: createFileHandlerResolver(resolve("home")),
35
+ });
36
+
29
37
  const server = createVoluteServer({
30
- agent,
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) => agent.sendMessage(content));
48
+ const hasMerge = handleMergeContext((content) =>
49
+ router.route([{ type: "text", text: content }], { channel: "system" }),
50
+ );
42
51
  if (!hasMerge) {
43
- await handleStartupContext((content) => agent.sendMessage(content));
52
+ await handleStartupContext((content) =>
53
+ router.route([{ type: "text", text: content }], { channel: "system" }),
54
+ );
44
55
  }
45
56
  });
46
57
 
@@ -4,6 +4,6 @@
4
4
  "biome.json.tmpl": "biome.json",
5
5
  "home/.config/volute.json.tmpl": "home/.config/volute.json"
6
6
  },
7
- "substitute": ["package.json", ".init/SOUL.md"],
7
+ "substitute": ["package.json", ".init/SOUL.md", "home/.config/routes.json"],
8
8
  "skillsDir": "home/.claude/skills"
9
9
  }
@@ -1,90 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- loadMergedEnv
4
- } from "./chunk-DNOXHLE5.js";
5
- import {
6
- resolveAgentName
7
- } from "./chunk-VRVVQIYY.js";
8
- import {
9
- parseArgs
10
- } from "./chunk-D424ZQGI.js";
11
- import {
12
- resolveAgent
13
- } from "./chunk-3C2XR4IY.js";
14
- import "./chunk-K3NQKI34.js";
15
-
16
- // src/lib/channels/discord.ts
17
- var API_BASE = "https://discord.com/api/v10";
18
- async function read(token, channelId, limit) {
19
- const res = await fetch(`${API_BASE}/channels/${channelId}/messages?limit=${limit}`, {
20
- headers: { Authorization: `Bot ${token}` }
21
- });
22
- if (!res.ok) {
23
- throw new Error(`Discord API error: ${res.status} ${res.statusText}`);
24
- }
25
- const messages = await res.json();
26
- return messages.reverse().map((m) => `${m.author.username}: ${m.content}`).join("\n");
27
- }
28
- async function send(token, channelId, message) {
29
- const res = await fetch(`${API_BASE}/channels/${channelId}/messages`, {
30
- method: "POST",
31
- headers: {
32
- Authorization: `Bot ${token}`,
33
- "Content-Type": "application/json"
34
- },
35
- body: JSON.stringify({ content: message })
36
- });
37
- if (!res.ok) {
38
- throw new Error(`Discord API error: ${res.status} ${res.statusText}`);
39
- }
40
- }
41
-
42
- // src/commands/channel.ts
43
- async function run(args) {
44
- const { positional, flags } = parseArgs(args, {
45
- agent: { type: "string" },
46
- limit: { type: "number" }
47
- });
48
- const subcommand = positional[0];
49
- const uri = positional[1];
50
- const message = positional[2];
51
- if (!subcommand || !uri || subcommand === "send" && !message) {
52
- console.error(`Usage:
53
- volute channel read <channel-uri> [--limit N] [--agent <name>]
54
- volute channel send <channel-uri> "<message>" [--agent <name>]`);
55
- process.exit(1);
56
- }
57
- const agentName = resolveAgentName(flags);
58
- const colonIdx = uri.indexOf(":");
59
- if (colonIdx === -1) {
60
- console.error(`Invalid channel URI: ${uri} (expected format: platform:id)`);
61
- process.exit(1);
62
- }
63
- const platform = uri.slice(0, colonIdx);
64
- const channelId = uri.slice(colonIdx + 1);
65
- const { dir } = resolveAgent(agentName);
66
- const env = loadMergedEnv(dir);
67
- if (platform === "discord") {
68
- const token = env.DISCORD_TOKEN;
69
- if (!token) {
70
- console.error("DISCORD_TOKEN not set. Run: volute env set DISCORD_TOKEN <token>");
71
- process.exit(1);
72
- }
73
- if (subcommand === "read") {
74
- const limit = flags.limit ?? 20;
75
- const output = await read(token, channelId, limit);
76
- console.log(output);
77
- } else if (subcommand === "send") {
78
- await send(token, channelId, message);
79
- } else {
80
- console.error(`Unknown subcommand: ${subcommand}`);
81
- process.exit(1);
82
- }
83
- } else {
84
- console.error(`Unsupported platform: ${platform}`);
85
- process.exit(1);
86
- }
87
- }
88
- export {
89
- run
90
- };
@@ -1,90 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/lib/log-buffer.ts
4
- var LogBuffer = class {
5
- entries = [];
6
- maxSize = 1e3;
7
- subscribers = /* @__PURE__ */ new Set();
8
- append(entry) {
9
- this.entries.push(entry);
10
- if (this.entries.length > this.maxSize) {
11
- this.entries.shift();
12
- }
13
- for (const sub of this.subscribers) {
14
- sub(entry);
15
- }
16
- }
17
- getEntries() {
18
- return [...this.entries];
19
- }
20
- subscribe(fn) {
21
- this.subscribers.add(fn);
22
- return () => this.subscribers.delete(fn);
23
- }
24
- };
25
- var logBuffer = new LogBuffer();
26
-
27
- // src/lib/logger.ts
28
- function write(level, msg, data) {
29
- const entry = {
30
- level,
31
- msg,
32
- ts: (/* @__PURE__ */ new Date()).toISOString(),
33
- ...data ? { data } : {}
34
- };
35
- const line = JSON.stringify(entry);
36
- process.stderr.write(`${line}
37
- `);
38
- logBuffer.append(entry);
39
- }
40
- var log = {
41
- info: (msg, data) => write("info", msg, data),
42
- warn: (msg, data) => write("warn", msg, data),
43
- error: (msg, data) => write("error", msg, data)
44
- };
45
- var logger_default = log;
46
-
47
- // src/lib/ndjson.ts
48
- var MAX_BUFFER_SIZE = 1e6;
49
- async function* readNdjson(body) {
50
- const reader = body.getReader();
51
- const decoder = new TextDecoder();
52
- let buffer = "";
53
- try {
54
- while (true) {
55
- const { done, value } = await reader.read();
56
- if (done) break;
57
- buffer += decoder.decode(value, { stream: true });
58
- if (buffer.length > MAX_BUFFER_SIZE) {
59
- logger_default.warn("ndjson: buffer exceeded 1MB, resetting");
60
- buffer = "";
61
- continue;
62
- }
63
- const lines = buffer.split("\n");
64
- buffer = lines.pop() || "";
65
- for (const line of lines) {
66
- if (!line.trim()) continue;
67
- try {
68
- yield JSON.parse(line);
69
- } catch {
70
- logger_default.warn("ndjson: skipping invalid line", { line: line.slice(0, 100) });
71
- }
72
- }
73
- }
74
- if (buffer.trim()) {
75
- try {
76
- yield JSON.parse(buffer);
77
- } catch {
78
- logger_default.warn("ndjson: skipping invalid line", { line: buffer.slice(0, 100) });
79
- }
80
- }
81
- } finally {
82
- reader.releaseLock();
83
- }
84
- }
85
-
86
- export {
87
- logBuffer,
88
- logger_default,
89
- readNdjson
90
- };
@@ -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
- };