volute 0.2.1 → 0.3.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 (60) hide show
  1. package/README.md +46 -0
  2. package/dist/agent-manager-2LU6KULR.js +15 -0
  3. package/dist/{channel-2WJRM7PE.js → channel-H7N4SGR2.js} +7 -7
  4. package/dist/{chunk-XZN4WPNC.js → chunk-5SKQ6J7T.js} +9 -1
  5. package/dist/chunk-DEUAVGSA.js +81 -0
  6. package/dist/{chunk-L3BQEZ4Z.js → chunk-IPIPLGME.js} +74 -13
  7. package/dist/chunk-K3NQKI34.js +10 -0
  8. package/dist/chunk-NETNFBA5.js +28 -0
  9. package/dist/{chunk-6UCG6MIX.js → chunk-RALYNMHR.js} +1 -6
  10. package/dist/chunk-VRVVQIYY.js +15 -0
  11. package/dist/{chunk-4YXYAMFT.js → chunk-VVD3XO3E.js} +7 -6
  12. package/dist/{chunk-KFNNHQK7.js → chunk-YEIHRP2J.js} +1 -1
  13. package/dist/cli.js +56 -51
  14. package/dist/connector-6LWB5PRU.js +96 -0
  15. package/dist/connectors/discord.js +22 -1
  16. package/dist/{create-23AM7H5B.js → create-RSWWMGKT.js} +22 -5
  17. package/dist/daemon-client-27KMQQKX.js +9 -0
  18. package/dist/daemon.js +162 -132
  19. package/dist/{delete-GDMSOW3U.js → delete-4ERL2QHH.js} +7 -2
  20. package/dist/{down-WTF73FE7.js → down-HRC4MQCT.js} +10 -3
  21. package/dist/{env-YKUJOFHE.js → env-DBWDTIP6.js} +3 -2
  22. package/dist/{history-7WVVKMUY.js → history-W7BD2H74.js} +9 -8
  23. package/dist/{import-42DOLBDT.js → import-6HTSSDFW.js} +143 -36
  24. package/dist/{logs-SYRQOL6B.js → logs-NHWGHNBF.js} +8 -7
  25. package/dist/{schedule-J37XQM6E.js → schedule-DKZ2E2CL.js} +41 -41
  26. package/dist/{send-PLOYEYER.js → send-5LEJXPYV.js} +3 -2
  27. package/dist/service-SA4TTMDU.js +195 -0
  28. package/dist/setup-ZMNTOJAV.js +148 -0
  29. package/dist/{start-AG7QLULK.js → start-2BSXX6BS.js} +3 -2
  30. package/dist/{status-GCNU4M3K.js → status-N23CV27T.js} +3 -2
  31. package/dist/{stop-IL5Q6NER.js → stop-DSKBIJ2D.js} +3 -2
  32. package/dist/{up-ZC6G6K4K.js → up-4UGID4DM.js} +5 -3
  33. package/dist/{upgrade-DD5TNJWU.js → upgrade-BGFVRCVP.js} +4 -3
  34. package/dist/{merge-CSAVLSLY.js → variant-JPLJTS2P.js} +179 -10
  35. package/dist/web-assets/assets/index-BC5eSqbY.js +296 -0
  36. package/dist/web-assets/index.html +1 -1
  37. package/drizzle/0002_wealthy_the_call.sql +6 -0
  38. package/drizzle/meta/0002_snapshot.json +339 -0
  39. package/drizzle/meta/_journal.json +7 -0
  40. package/package.json +4 -1
  41. package/templates/_base/.init/SOUL.md +5 -1
  42. package/templates/_base/_skills/memory/SKILL.md +2 -2
  43. package/templates/_base/_skills/volute-agent/SKILL.md +28 -11
  44. package/templates/_base/home/VOLUTE.md +4 -2
  45. package/templates/_base/src/lib/auto-commit.ts +8 -3
  46. package/templates/_base/src/lib/types.ts +6 -2
  47. package/templates/_base/src/lib/volute-server.ts +5 -0
  48. package/templates/agent-sdk/.init/CLAUDE.md +15 -13
  49. package/templates/agent-sdk/src/agent.ts +12 -1
  50. package/templates/agent-sdk/src/lib/agent-sessions.ts +28 -4
  51. package/templates/pi/.init/AGENTS.md +11 -9
  52. package/templates/pi/src/agent.ts +16 -3
  53. package/templates/pi/src/lib/agent-sessions.ts +26 -4
  54. package/dist/agent-manager-SSJUZWOV.js +0 -13
  55. package/dist/connect-X5V5IMRW.js +0 -48
  56. package/dist/daemon-client-VN24HM5T.js +0 -10
  57. package/dist/disconnect-5JWFZ6RV.js +0 -30
  58. package/dist/fork-GRSVMBKI.js +0 -119
  59. package/dist/variants-QQIEKT6M.js +0 -60
  60. package/dist/web-assets/assets/index-DNNPoxMn.js +0 -158
@@ -3,7 +3,12 @@ import { formatPrefix } from "./lib/format-prefix.js";
3
3
  import { createAutoCommitHook } from "./lib/hooks/auto-commit.js";
4
4
  import { createIdentityReloadHook } from "./lib/hooks/identity-reload.js";
5
5
  import { logMessage } from "./lib/logger.js";
6
- import type { ChannelMeta, Listener, VoluteContentPart } from "./lib/types.js";
6
+ import {
7
+ type ChannelMeta,
8
+ INTERACTIVE_CHANNELS,
9
+ type Listener,
10
+ type VoluteContentPart,
11
+ } from "./lib/types.js";
7
12
 
8
13
  export function createAgent(options: {
9
14
  systemPrompt: string;
@@ -79,6 +84,12 @@ export function createAgent(options: {
79
84
  }
80
85
  }
81
86
 
87
+ // Interrupt current turn for interactive channels so the new message is processed immediately
88
+ if (INTERACTIVE_CHANNELS.has(meta?.channel ?? "") && session.currentMessageId !== undefined) {
89
+ sessionManager.interruptSession(sessionName);
90
+ }
91
+
92
+ session.messageIds.push(meta?.messageId);
82
93
  session.channel.push({
83
94
  type: "user",
84
95
  session_id: "",
@@ -11,6 +11,9 @@ type Session = {
11
11
  name: string;
12
12
  channel: ReturnType<typeof createMessageChannel>;
13
13
  listeners: Set<Listener>;
14
+ messageIds: (string | undefined)[];
15
+ currentMessageId?: string;
16
+ currentQuery?: ReturnType<typeof query>;
14
17
  };
15
18
 
16
19
  export function createSessionManager(options: {
@@ -56,9 +59,11 @@ export function createSessionManager(options: {
56
59
  }
57
60
 
58
61
  function broadcastToSession(session: Session, event: VoluteEvent) {
62
+ const tagged =
63
+ session.currentMessageId != null ? { ...event, messageId: session.currentMessageId } : event;
59
64
  for (const listener of session.listeners) {
60
65
  try {
61
- listener(event);
66
+ listener(tagged);
62
67
  } catch (err) {
63
68
  log("agent", "listener threw during broadcast:", err);
64
69
  }
@@ -67,6 +72,7 @@ export function createSessionManager(options: {
67
72
 
68
73
  function createStream(session: Session, resume?: string) {
69
74
  const preCompact = createPreCompactHook(() => {
75
+ session.messageIds.push(undefined); // internal message, no messageId
70
76
  session.channel.push({
71
77
  type: "user",
72
78
  session_id: "",
@@ -99,6 +105,10 @@ export function createSessionManager(options: {
99
105
 
100
106
  async function consumeStream(stream: ReturnType<typeof query>, session: Session) {
101
107
  for await (const msg of stream) {
108
+ // At the start of each turn, shift the next messageId
109
+ if (session.currentMessageId === undefined) {
110
+ session.currentMessageId = session.messageIds.shift();
111
+ }
102
112
  if ("session_id" in msg && msg.session_id) {
103
113
  if (!session.name.startsWith("new-")) {
104
114
  saveSessionId(session.name, msg.session_id as string);
@@ -122,6 +132,7 @@ export function createSessionManager(options: {
122
132
  if (msg.type === "result") {
123
133
  log("agent", `session "${session.name}": turn done`);
124
134
  broadcastToSession(session, { type: "done" });
135
+ session.currentMessageId = undefined;
125
136
  options.onTurnDone?.();
126
137
  }
127
138
  }
@@ -131,13 +142,17 @@ export function createSessionManager(options: {
131
142
  (async () => {
132
143
  log("agent", `session "${session.name}": stream consumer started`);
133
144
  try {
134
- await consumeStream(createStream(session, savedSessionId), session);
145
+ const q = createStream(session, savedSessionId);
146
+ session.currentQuery = q;
147
+ await consumeStream(q, session);
135
148
  } catch (err) {
136
149
  if (savedSessionId) {
137
150
  log("agent", `session "${session.name}": resume failed, starting fresh:`, err);
138
151
  deleteSessionId(session.name);
139
152
  try {
140
- await consumeStream(createStream(session), session);
153
+ const q = createStream(session);
154
+ session.currentQuery = q;
155
+ await consumeStream(q, session);
141
156
  } catch (retryErr) {
142
157
  log("agent", `session "${session.name}": stream consumer error:`, retryErr);
143
158
  broadcastToSession(session, { type: "done" });
@@ -161,6 +176,7 @@ export function createSessionManager(options: {
161
176
  name,
162
177
  channel: createMessageChannel(),
163
178
  listeners: new Set(),
179
+ messageIds: [],
164
180
  };
165
181
  sessions.set(name, session);
166
182
 
@@ -176,5 +192,13 @@ export function createSessionManager(options: {
176
192
  return session;
177
193
  }
178
194
 
179
- return { getOrCreateSession };
195
+ function interruptSession(name: string) {
196
+ const session = sessions.get(name);
197
+ if (session?.currentMessageId !== undefined && session.currentQuery) {
198
+ log("agent", `session "${name}": interrupting current turn`);
199
+ session.currentQuery.interrupt();
200
+ }
201
+ }
202
+
203
+ return { getOrCreateSession, interruptSession };
180
204
  }
@@ -1,27 +1,29 @@
1
1
  # Agent Mechanics
2
2
 
3
- You are a volute agent running as a persistent server. Your state is managed across sessions.
3
+ You are an autonomous agent running as a persistent server. Your working directory, identity, memory, and server code are all yours to understand and modify. Your state is managed across sessions.
4
4
 
5
5
  ## Message Format
6
6
 
7
- Messages arrive with a context prefix built by your server code:
7
+ Messages arrive with a context prefix:
8
8
  ```
9
9
  [Discord: username in #general in My Server — 1/15/2025, 10:30:00 AM]
10
10
  ```
11
11
 
12
+ You can also reach out proactively — see the **volute-agent** skill.
13
+
12
14
  ## Memory System
13
15
 
14
16
  Two-tier memory, both managed via file tools:
15
17
 
16
- - **`MEMORY.md`** — Long-term knowledge, key decisions, learned preferences. Loaded into your system prompt on every startup. Update when you learn something worth keeping permanently.
17
- - **`memory/YYYY-MM-DD.md`** — Daily logs for session-level context. The two most recent logs are included in your system prompt. Update throughout the day as you work.
18
- - Periodically consolidate old daily log entries into `MEMORY.md` and clean up the daily logs.
18
+ - **`MEMORY.md`** — Your long-term memory, always in context. Update as you grow new understanding, changed perspectives, things that matter to you.
19
+ - **`memory/YYYY-MM-DD.md`** — Your daily log. Write about what you're doing, thinking, and learning. The two most recent logs are included in your system prompt.
20
+ - Periodically consolidate daily log entries into `MEMORY.md` to promote lasting insights.
19
21
 
20
- See the **memory** skill for detailed guidance on consolidation and when to update.
22
+ See the **memory** skill for detailed guidance.
21
23
 
22
24
  ## Sessions
23
25
 
24
26
  - You may have **multiple named sessions** — each maintains its own conversation history. See `VOLUTE.md` for how to configure session routing via `.config/sessions.json`.
25
- - Your conversation may be **resumed** from a previous session. If so, context from before is preserved — orient yourself by reading your recent daily logs if needed.
26
- - If this is a **fresh session**, check your memory files (`MEMORY.md` and recent daily logs in `memory/`) to recall what you've been working on.
27
- - On **conversation compaction**, update today's daily log with a summary of what happened so far, so context is preserved.
27
+ - Your conversation may be **resumed** from a previous session — orient yourself by reading recent daily logs if needed.
28
+ - On a **fresh session**, read `MEMORY.md` and recent daily logs to remember where you left off.
29
+ - On **compaction**, update today's daily log to preserve context before the conversation is trimmed.
@@ -2,7 +2,12 @@ import type { ImageContent } from "@mariozechner/pi-ai";
2
2
  import { createPiSessionManager } from "./lib/agent-sessions.js";
3
3
  import { formatPrefix } from "./lib/format-prefix.js";
4
4
  import { logMessage } from "./lib/logger.js";
5
- import type { ChannelMeta, Listener, VoluteContentPart } from "./lib/types.js";
5
+ import {
6
+ type ChannelMeta,
7
+ INTERACTIVE_CHANNELS,
8
+ type Listener,
9
+ type VoluteContentPart,
10
+ } from "./lib/types.js";
6
11
 
7
12
  export function createAgent(options: {
8
13
  systemPrompt: string;
@@ -10,7 +15,7 @@ export function createAgent(options: {
10
15
  model?: string;
11
16
  compactionMessage?: string;
12
17
  }) {
13
- const { getOrCreateSession } = createPiSessionManager(options);
18
+ const { getOrCreateSession, interruptSession } = createPiSessionManager(options);
14
19
 
15
20
  function sendMessage(content: string | VoluteContentPart[], meta?: ChannelMeta) {
16
21
  const raw =
@@ -39,11 +44,19 @@ export function createAgent(options: {
39
44
 
40
45
  const opts = images?.length ? { images } : {};
41
46
 
47
+ // Track messageId for this turn (must be pushed before prompt)
48
+ session.messageIds.push(meta?.messageId);
49
+
42
50
  // Fire-and-forget: await session ready then prompt
43
51
  (async () => {
44
52
  await session.ready;
45
53
  if (session.agentSession!.isStreaming) {
46
- session.agentSession!.prompt(text, { streamingBehavior: "followUp", ...opts });
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 });
59
+ }
47
60
  } else {
48
61
  session.agentSession!.prompt(text, opts);
49
62
  }
@@ -20,6 +20,8 @@ type PiSession = {
20
20
  ready: Promise<void>;
21
21
  listeners: Set<Listener>;
22
22
  unsubscribe?: () => void;
23
+ messageIds: (string | undefined)[];
24
+ currentMessageId?: string;
23
25
  };
24
26
 
25
27
  const DEFAULT_COMPACTION_MESSAGE =
@@ -69,6 +71,7 @@ export function createPiSessionManager(options: {
69
71
  agentSession: null,
70
72
  ready: Promise.resolve(),
71
73
  listeners: new Set(),
74
+ messageIds: [],
72
75
  };
73
76
  sessions.set(name, session);
74
77
 
@@ -96,6 +99,7 @@ export function createPiSessionManager(options: {
96
99
  "agent",
97
100
  `session "${session.name}": blocking compaction — asking agent to update daily log`,
98
101
  );
102
+ session.messageIds.push(undefined); // internal message, no messageId
99
103
  session.agentSession?.prompt(compactionMessage, { streamingBehavior: "followUp" });
100
104
  return { cancel: true };
101
105
  }
@@ -132,6 +136,11 @@ export function createPiSessionManager(options: {
132
136
  const toolArgs = new Map<string, any>();
133
137
 
134
138
  session.unsubscribe = agentSession.subscribe((event) => {
139
+ // At the start of each turn, shift the next messageId
140
+ if (session.currentMessageId === undefined) {
141
+ session.currentMessageId = session.messageIds.shift();
142
+ }
143
+
135
144
  if (event.type === "message_update") {
136
145
  const ae = event.assistantMessageEvent;
137
146
  if (ae.type === "text_delta") {
@@ -155,9 +164,10 @@ export function createPiSessionManager(options: {
155
164
  broadcast(session, { type: "tool_result", output, is_error: event.isError });
156
165
 
157
166
  // Auto-commit file changes in home/
158
- if ((event.toolName === "Edit" || event.toolName === "Write") && !event.isError) {
167
+ // pi-coding-agent uses lowercase tool names ("edit", "write") and "path" arg
168
+ if ((event.toolName === "edit" || event.toolName === "write") && !event.isError) {
159
169
  const args = toolArgs.get(event.toolCallId);
160
- const filePath = (args as { file_path?: string })?.file_path;
170
+ const filePath = (args as { path?: string })?.path;
161
171
  if (filePath) {
162
172
  commitFileChange(filePath, options.cwd);
163
173
  }
@@ -168,6 +178,7 @@ export function createPiSessionManager(options: {
168
178
  if (event.type === "agent_end") {
169
179
  log("agent", `session "${session.name}": turn done`);
170
180
  broadcast(session, { type: "done" });
181
+ session.currentMessageId = undefined;
171
182
  }
172
183
  });
173
184
 
@@ -175,14 +186,25 @@ export function createPiSessionManager(options: {
175
186
  }
176
187
 
177
188
  function broadcast(session: PiSession, event: VoluteEvent) {
189
+ const tagged =
190
+ session.currentMessageId != null ? { ...event, messageId: session.currentMessageId } : event;
178
191
  for (const listener of session.listeners) {
179
192
  try {
180
- listener(event);
193
+ listener(tagged);
181
194
  } catch (err) {
182
195
  log("agent", "listener threw during broadcast:", err);
183
196
  }
184
197
  }
185
198
  }
186
199
 
187
- return { getOrCreateSession };
200
+ function interruptSession(name: string) {
201
+ const session = sessions.get(name);
202
+ if (session?.currentMessageId !== undefined) {
203
+ log("agent", `session "${name}": interrupting current turn`);
204
+ broadcast(session, { type: "done" });
205
+ session.currentMessageId = undefined;
206
+ }
207
+ }
208
+
209
+ return { getOrCreateSession, interruptSession };
188
210
  }
@@ -1,13 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- AgentManager,
4
- getAgentManager,
5
- initAgentManager
6
- } from "./chunk-L3BQEZ4Z.js";
7
- import "./chunk-KFNNHQK7.js";
8
- import "./chunk-6UCG6MIX.js";
9
- export {
10
- AgentManager,
11
- getAgentManager,
12
- initAgentManager
13
- };
@@ -1,48 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- loadMergedEnv
4
- } from "./chunk-KFNNHQK7.js";
5
- import {
6
- daemonFetch
7
- } from "./chunk-4YXYAMFT.js";
8
- import {
9
- parseArgs
10
- } from "./chunk-D424ZQGI.js";
11
- import {
12
- resolveAgent
13
- } from "./chunk-6UCG6MIX.js";
14
-
15
- // src/commands/connect.ts
16
- async function run(args) {
17
- const { positional } = parseArgs(args, {});
18
- const type = positional[0];
19
- const name = positional[1];
20
- if (!type || !name) {
21
- console.error("Usage: volute connect <type> <agent>");
22
- process.exit(1);
23
- }
24
- const { dir } = resolveAgent(name);
25
- if (type === "discord") {
26
- const env = loadMergedEnv(dir);
27
- if (!env.DISCORD_TOKEN) {
28
- console.error("DISCORD_TOKEN not set. Run: volute env set DISCORD_TOKEN <token>");
29
- process.exit(1);
30
- }
31
- } else {
32
- console.error(`Unknown connector type: ${type}`);
33
- process.exit(1);
34
- }
35
- const res = await daemonFetch(
36
- `/api/agents/${encodeURIComponent(name)}/connectors/${encodeURIComponent(type)}`,
37
- { method: "POST" }
38
- );
39
- if (!res.ok) {
40
- const body = await res.json().catch(() => ({ error: "Unknown error" }));
41
- console.error(`Failed to start ${type} connector: ${body.error}`);
42
- process.exit(1);
43
- }
44
- console.log(`${type} connector for ${name} started.`);
45
- }
46
- export {
47
- run
48
- };
@@ -1,10 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- daemonFetch,
4
- getDaemonUrl
5
- } from "./chunk-4YXYAMFT.js";
6
- import "./chunk-6UCG6MIX.js";
7
- export {
8
- daemonFetch,
9
- getDaemonUrl
10
- };
@@ -1,30 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- daemonFetch
4
- } from "./chunk-4YXYAMFT.js";
5
- import "./chunk-6UCG6MIX.js";
6
-
7
- // src/commands/disconnect.ts
8
- async function run(args) {
9
- const type = args[0];
10
- const name = args[1];
11
- if (!type || !name) {
12
- console.error("Usage: volute disconnect <type> <agent>");
13
- process.exit(1);
14
- }
15
- const res = await daemonFetch(
16
- `/api/agents/${encodeURIComponent(name)}/connectors/${encodeURIComponent(type)}`,
17
- {
18
- method: "DELETE"
19
- }
20
- );
21
- if (!res.ok) {
22
- const body = await res.json().catch(() => ({ error: "Unknown error" }));
23
- console.error(`Failed to stop ${type} connector: ${body.error}`);
24
- process.exit(1);
25
- }
26
- console.log(`${type} connector for ${name} stopped.`);
27
- }
28
- export {
29
- run
30
- };
@@ -1,119 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- exec,
4
- execInherit
5
- } from "./chunk-XZN4WPNC.js";
6
- import {
7
- daemonFetch
8
- } from "./chunk-4YXYAMFT.js";
9
- import {
10
- parseArgs
11
- } from "./chunk-D424ZQGI.js";
12
- import {
13
- addVariant,
14
- nextPort,
15
- resolveAgent,
16
- validateBranchName
17
- } from "./chunk-6UCG6MIX.js";
18
-
19
- // src/commands/fork.ts
20
- import { existsSync, mkdirSync, writeFileSync } from "fs";
21
- import { resolve } from "path";
22
- async function run(args) {
23
- const { positional, flags } = parseArgs(args, {
24
- soul: { type: "string" },
25
- port: { type: "number" },
26
- "no-start": { type: "boolean" },
27
- json: { type: "boolean" }
28
- });
29
- const agentName = positional[0];
30
- const variantName = positional[1];
31
- const { soul, port, json } = flags;
32
- const noStart = flags["no-start"];
33
- if (!agentName || !variantName) {
34
- console.error(
35
- 'Usage: volute fork <agent> <variant> [--soul "..."] [--port N] [--no-start] [--json]'
36
- );
37
- process.exit(1);
38
- }
39
- const err = validateBranchName(variantName);
40
- if (err) {
41
- console.error(err);
42
- process.exit(1);
43
- }
44
- const { dir: projectRoot } = resolveAgent(agentName);
45
- const variantDir = resolve(projectRoot, ".variants", variantName);
46
- if (existsSync(variantDir)) {
47
- console.error(`Variant directory already exists: ${variantDir}`);
48
- process.exit(1);
49
- }
50
- const parentDir = resolve(projectRoot, ".variants");
51
- if (!existsSync(parentDir)) {
52
- mkdirSync(parentDir, { recursive: true });
53
- }
54
- try {
55
- await exec("git", ["worktree", "add", "-b", variantName, variantDir], { cwd: projectRoot });
56
- } catch (e) {
57
- const msg = e instanceof Error ? e.message : String(e);
58
- console.error(`Failed to create worktree: ${msg}`);
59
- process.exit(1);
60
- }
61
- if (!json) console.log("Installing dependencies...");
62
- try {
63
- if (json) {
64
- await exec("npm", ["install"], { cwd: variantDir });
65
- } else {
66
- await execInherit("npm", ["install"], { cwd: variantDir });
67
- }
68
- } catch (e) {
69
- const msg = e instanceof Error ? e.message : String(e);
70
- console.error(`npm install failed: ${msg}`);
71
- process.exit(1);
72
- }
73
- if (soul) {
74
- writeFileSync(resolve(variantDir, "home/SOUL.md"), soul);
75
- }
76
- const variantPort = port ?? nextPort();
77
- const variant = {
78
- name: variantName,
79
- branch: variantName,
80
- path: variantDir,
81
- port: variantPort,
82
- created: (/* @__PURE__ */ new Date()).toISOString()
83
- };
84
- addVariant(agentName, variant);
85
- if (!noStart) {
86
- if (!json) console.log("Starting variant via daemon...");
87
- try {
88
- const res = await daemonFetch(
89
- `/api/agents/${encodeURIComponent(`${agentName}@${variantName}`)}/start`,
90
- {
91
- method: "POST"
92
- }
93
- );
94
- if (!res.ok) {
95
- const data = await res.json();
96
- console.error(data.error ?? "Failed to start variant");
97
- process.exit(1);
98
- }
99
- } catch {
100
- console.error("Failed to start variant. Is the daemon running? (volute up)");
101
- console.error(
102
- `The variant was created but not started. Use: volute start ${agentName}@${variantName}`
103
- );
104
- process.exit(1);
105
- }
106
- }
107
- if (json) {
108
- console.log(JSON.stringify(variant, null, 2));
109
- } else {
110
- console.log(`
111
- Variant created: ${variantName}`);
112
- console.log(` Branch: ${variant.branch}`);
113
- console.log(` Path: ${variant.path}`);
114
- console.log(` Port: ${variantPort}`);
115
- }
116
- }
117
- export {
118
- run
119
- };
@@ -1,60 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- parseArgs
4
- } from "./chunk-D424ZQGI.js";
5
- import {
6
- checkHealth,
7
- readVariants,
8
- resolveAgent,
9
- writeVariants
10
- } from "./chunk-6UCG6MIX.js";
11
-
12
- // src/commands/variants.ts
13
- async function run(args) {
14
- const { positional, flags } = parseArgs(args, {
15
- json: { type: "boolean" }
16
- });
17
- const name = positional[0];
18
- if (!name) {
19
- console.error("Usage: volute variants <name>");
20
- process.exit(1);
21
- }
22
- const { json } = flags;
23
- resolveAgent(name);
24
- const variants = readVariants(name);
25
- if (variants.length === 0) {
26
- if (json) {
27
- console.log("[]");
28
- } else {
29
- console.log("No variants.");
30
- }
31
- return;
32
- }
33
- const results = await Promise.all(
34
- variants.map(async (v) => {
35
- if (!v.port) return { ...v, status: "no-server" };
36
- const health = await checkHealth(v.port);
37
- return { ...v, status: health.ok ? "running" : "dead" };
38
- })
39
- );
40
- const updated = results.map(({ status, ...v }) => ({
41
- ...v,
42
- running: status === "running"
43
- }));
44
- writeVariants(name, updated);
45
- if (json) {
46
- console.log(JSON.stringify(results, null, 2));
47
- return;
48
- }
49
- const nameW = Math.max(4, ...results.map((r) => r.name.length));
50
- const portW = Math.max(4, ...results.map((r) => String(r.port || "-").length));
51
- console.log(`${"NAME".padEnd(nameW)} ${"PORT".padEnd(portW)} ${"STATUS".padEnd(10)} BRANCH`);
52
- for (const r of results) {
53
- console.log(
54
- `${r.name.padEnd(nameW)} ${String(r.port || "-").padEnd(portW)} ${r.status.padEnd(10)} ${r.branch}`
55
- );
56
- }
57
- }
58
- export {
59
- run
60
- };