volute 0.6.0 → 0.7.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.
@@ -18,7 +18,7 @@ async function run(args) {
18
18
  await import("./restart-O4ETYLJF.js").then((m) => m.run(args.slice(1)));
19
19
  break;
20
20
  case "delete":
21
- await import("./delete-2PH2CGDY.js").then((m) => m.run(args.slice(1)));
21
+ await import("./delete-UOU4AFQN.js").then((m) => m.run(args.slice(1)));
22
22
  break;
23
23
  case "list":
24
24
  await import("./status-QAJWXKMZ.js").then((m) => m.run(args.slice(1)));
@@ -3,7 +3,7 @@ import {
3
3
  AgentManager,
4
4
  getAgentManager,
5
5
  initAgentManager
6
- } from "./chunk-G6ZNGLUX.js";
6
+ } from "./chunk-62X577Y7.js";
7
7
  import "./chunk-H7AMDUIA.js";
8
8
  import "./chunk-W76KWE23.js";
9
9
  import "./chunk-UWHWAPGO.js";
@@ -285,11 +285,13 @@ var AgentManager = class {
285
285
  });
286
286
  this.stopping.delete(name);
287
287
  if (this.restartAttempts.delete(name)) this.saveCrashAttempts();
288
- const [baseName, variantName] = name.split("@", 2);
289
- if (variantName) {
290
- setVariantRunning(baseName, variantName, false);
291
- } else {
292
- setAgentRunning(name, false);
288
+ if (!this.shuttingDown) {
289
+ const [baseName, variantName] = name.split("@", 2);
290
+ if (variantName) {
291
+ setVariantRunning(baseName, variantName, false);
292
+ } else {
293
+ setAgentRunning(name, false);
294
+ }
293
295
  }
294
296
  console.error(`[daemon] stopped agent ${name}`);
295
297
  }
@@ -0,0 +1,265 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ voluteHome
4
+ } from "./chunk-UWHWAPGO.js";
5
+ import {
6
+ __export
7
+ } from "./chunk-K3NQKI34.js";
8
+
9
+ // src/lib/auth.ts
10
+ import { compareSync, hashSync } from "bcryptjs";
11
+ import { and, count, eq } from "drizzle-orm";
12
+
13
+ // src/lib/db.ts
14
+ import { chmodSync, existsSync } from "fs";
15
+ import { dirname, resolve } from "path";
16
+ import { fileURLToPath } from "url";
17
+ import { drizzle } from "drizzle-orm/libsql";
18
+ import { migrate } from "drizzle-orm/libsql/migrator";
19
+
20
+ // src/lib/schema.ts
21
+ var schema_exports = {};
22
+ __export(schema_exports, {
23
+ agentMessages: () => agentMessages,
24
+ conversationParticipants: () => conversationParticipants,
25
+ conversations: () => conversations,
26
+ messages: () => messages,
27
+ sessions: () => sessions,
28
+ users: () => users
29
+ });
30
+ import { sql } from "drizzle-orm";
31
+ import { index, integer, sqliteTable, text, uniqueIndex } from "drizzle-orm/sqlite-core";
32
+ var users = sqliteTable("users", {
33
+ id: integer("id").primaryKey({ autoIncrement: true }),
34
+ username: text("username").unique().notNull(),
35
+ password_hash: text("password_hash").notNull(),
36
+ role: text("role").notNull().default("pending"),
37
+ user_type: text("user_type").notNull().default("human"),
38
+ created_at: text("created_at").notNull().default(sql`(datetime('now'))`)
39
+ });
40
+ var conversations = sqliteTable(
41
+ "conversations",
42
+ {
43
+ id: text("id").primaryKey(),
44
+ agent_name: text("agent_name").notNull(),
45
+ channel: text("channel").notNull(),
46
+ user_id: integer("user_id").references(() => users.id),
47
+ title: text("title"),
48
+ created_at: text("created_at").notNull().default(sql`(datetime('now'))`),
49
+ updated_at: text("updated_at").notNull().default(sql`(datetime('now'))`)
50
+ },
51
+ (table) => [
52
+ index("idx_conversations_agent_name").on(table.agent_name),
53
+ index("idx_conversations_user_id").on(table.user_id),
54
+ index("idx_conversations_updated_at").on(table.updated_at)
55
+ ]
56
+ );
57
+ var agentMessages = sqliteTable(
58
+ "agent_messages",
59
+ {
60
+ id: integer("id").primaryKey({ autoIncrement: true }),
61
+ agent: text("agent").notNull(),
62
+ channel: text("channel").notNull(),
63
+ role: text("role").notNull(),
64
+ sender: text("sender"),
65
+ content: text("content").notNull(),
66
+ created_at: text("created_at").notNull().default(sql`(datetime('now'))`)
67
+ },
68
+ (table) => [
69
+ index("idx_agent_messages_agent").on(table.agent),
70
+ index("idx_agent_messages_channel").on(table.agent, table.channel)
71
+ ]
72
+ );
73
+ var conversationParticipants = sqliteTable(
74
+ "conversation_participants",
75
+ {
76
+ conversation_id: text("conversation_id").notNull().references(() => conversations.id, { onDelete: "cascade" }),
77
+ user_id: integer("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
78
+ role: text("role").notNull().default("member"),
79
+ joined_at: text("joined_at").notNull().default(sql`(datetime('now'))`)
80
+ },
81
+ (table) => [
82
+ uniqueIndex("idx_cp_unique").on(table.conversation_id, table.user_id),
83
+ index("idx_cp_user_id").on(table.user_id)
84
+ ]
85
+ );
86
+ var sessions = sqliteTable("sessions", {
87
+ id: text("id").primaryKey(),
88
+ userId: integer("user_id").references(() => users.id, { onDelete: "cascade" }).notNull(),
89
+ createdAt: integer("created_at").notNull()
90
+ });
91
+ var messages = sqliteTable(
92
+ "messages",
93
+ {
94
+ id: integer("id").primaryKey({ autoIncrement: true }),
95
+ conversation_id: text("conversation_id").notNull().references(() => conversations.id, { onDelete: "cascade" }),
96
+ role: text("role").notNull(),
97
+ sender_name: text("sender_name"),
98
+ content: text("content").notNull(),
99
+ created_at: text("created_at").notNull().default(sql`(datetime('now'))`)
100
+ },
101
+ (table) => [index("idx_messages_conversation_id").on(table.conversation_id)]
102
+ );
103
+
104
+ // src/lib/db.ts
105
+ var __dirname = dirname(fileURLToPath(import.meta.url));
106
+ var migrationsFolder = existsSync(resolve(__dirname, "../drizzle")) ? resolve(__dirname, "../drizzle") : resolve(__dirname, "../../drizzle");
107
+ var db = null;
108
+ async function getDb() {
109
+ if (db) return db;
110
+ const dbPath = process.env.VOLUTE_DB_PATH || resolve(voluteHome(), "volute.db");
111
+ db = drizzle({ connection: { url: `file:${dbPath}` }, schema: schema_exports });
112
+ await migrate(db, { migrationsFolder });
113
+ try {
114
+ chmodSync(dbPath, 384);
115
+ } catch (err) {
116
+ console.error(
117
+ `[volute] WARNING: Failed to restrict database file permissions on ${dbPath}:`,
118
+ err
119
+ );
120
+ }
121
+ return db;
122
+ }
123
+
124
+ // src/lib/auth.ts
125
+ async function createUser(username, password) {
126
+ const db2 = await getDb();
127
+ const hash = hashSync(password, 10);
128
+ const [{ value }] = await db2.select({ value: count() }).from(users).where(eq(users.user_type, "human"));
129
+ const role = value === 0 ? "admin" : "pending";
130
+ const [result] = await db2.insert(users).values({ username, password_hash: hash, role }).returning({
131
+ id: users.id,
132
+ username: users.username,
133
+ role: users.role,
134
+ user_type: users.user_type,
135
+ created_at: users.created_at
136
+ });
137
+ return result;
138
+ }
139
+ async function verifyUser(username, password) {
140
+ const db2 = await getDb();
141
+ const row = await db2.select().from(users).where(eq(users.username, username)).get();
142
+ if (!row) return null;
143
+ if (row.user_type === "agent") return null;
144
+ if (!compareSync(password, row.password_hash)) return null;
145
+ const { password_hash: _, ...user } = row;
146
+ return user;
147
+ }
148
+ async function getUser(id) {
149
+ const db2 = await getDb();
150
+ const row = await db2.select({
151
+ id: users.id,
152
+ username: users.username,
153
+ role: users.role,
154
+ user_type: users.user_type,
155
+ created_at: users.created_at
156
+ }).from(users).where(eq(users.id, id)).get();
157
+ return row ?? null;
158
+ }
159
+ async function getUserByUsername(username) {
160
+ const db2 = await getDb();
161
+ const row = await db2.select({
162
+ id: users.id,
163
+ username: users.username,
164
+ role: users.role,
165
+ user_type: users.user_type,
166
+ created_at: users.created_at
167
+ }).from(users).where(eq(users.username, username)).get();
168
+ return row ?? null;
169
+ }
170
+ async function listUsers() {
171
+ const db2 = await getDb();
172
+ return db2.select({
173
+ id: users.id,
174
+ username: users.username,
175
+ role: users.role,
176
+ user_type: users.user_type,
177
+ created_at: users.created_at
178
+ }).from(users).orderBy(users.created_at).all();
179
+ }
180
+ async function listPendingUsers() {
181
+ const db2 = await getDb();
182
+ return db2.select({
183
+ id: users.id,
184
+ username: users.username,
185
+ role: users.role,
186
+ user_type: users.user_type,
187
+ created_at: users.created_at
188
+ }).from(users).where(eq(users.role, "pending")).orderBy(users.created_at).all();
189
+ }
190
+ async function listUsersByType(userType) {
191
+ const db2 = await getDb();
192
+ return db2.select({
193
+ id: users.id,
194
+ username: users.username,
195
+ role: users.role,
196
+ user_type: users.user_type,
197
+ created_at: users.created_at
198
+ }).from(users).where(eq(users.user_type, userType)).orderBy(users.created_at).all();
199
+ }
200
+ async function getOrCreateAgentUser(agentName) {
201
+ const db2 = await getDb();
202
+ const existing = await db2.select({
203
+ id: users.id,
204
+ username: users.username,
205
+ role: users.role,
206
+ user_type: users.user_type,
207
+ created_at: users.created_at
208
+ }).from(users).where(and(eq(users.username, agentName), eq(users.user_type, "agent"))).get();
209
+ if (existing) return existing;
210
+ try {
211
+ const [result] = await db2.insert(users).values({
212
+ username: agentName,
213
+ password_hash: "!agent",
214
+ role: "agent",
215
+ user_type: "agent"
216
+ }).returning({
217
+ id: users.id,
218
+ username: users.username,
219
+ role: users.role,
220
+ user_type: users.user_type,
221
+ created_at: users.created_at
222
+ });
223
+ return result;
224
+ } catch (err) {
225
+ if (err instanceof Error && err.message.includes("UNIQUE constraint")) {
226
+ const retried = await db2.select({
227
+ id: users.id,
228
+ username: users.username,
229
+ role: users.role,
230
+ user_type: users.user_type,
231
+ created_at: users.created_at
232
+ }).from(users).where(and(eq(users.username, agentName), eq(users.user_type, "agent"))).get();
233
+ if (retried) return retried;
234
+ }
235
+ throw err;
236
+ }
237
+ }
238
+ async function deleteAgentUser(agentName) {
239
+ const db2 = await getDb();
240
+ await db2.delete(users).where(and(eq(users.username, agentName), eq(users.user_type, "agent")));
241
+ }
242
+ async function approveUser(id) {
243
+ const db2 = await getDb();
244
+ await db2.update(users).set({ role: "user" }).where(and(eq(users.id, id), eq(users.role, "pending")));
245
+ }
246
+
247
+ export {
248
+ users,
249
+ conversations,
250
+ agentMessages,
251
+ conversationParticipants,
252
+ sessions,
253
+ messages,
254
+ getDb,
255
+ createUser,
256
+ verifyUser,
257
+ getUser,
258
+ getUserByUsername,
259
+ listUsers,
260
+ listPendingUsers,
261
+ listUsersByType,
262
+ getOrCreateAgentUser,
263
+ deleteAgentUser,
264
+ approveUser
265
+ };
@@ -5,7 +5,6 @@ import {
5
5
  import {
6
6
  voluteHome
7
7
  } from "./chunk-UWHWAPGO.js";
8
- import "./chunk-K3NQKI34.js";
9
8
 
10
9
  // src/commands/up.ts
11
10
  import { spawn } from "child_process";
@@ -103,6 +102,7 @@ async function run(args) {
103
102
  console.error(`Check logs: ${logFile}`);
104
103
  process.exit(1);
105
104
  }
105
+
106
106
  export {
107
107
  readGlobalConfig,
108
108
  run
@@ -2,12 +2,11 @@
2
2
  import {
3
3
  voluteHome
4
4
  } from "./chunk-UWHWAPGO.js";
5
- import "./chunk-K3NQKI34.js";
6
5
 
7
6
  // src/commands/down.ts
8
7
  import { existsSync, readFileSync, unlinkSync } from "fs";
9
8
  import { resolve } from "path";
10
- async function run(_args) {
9
+ async function stopDaemon() {
11
10
  const home = voluteHome();
12
11
  const pidPath = resolve(home, "daemon.pid");
13
12
  if (!existsSync(pidPath)) {
@@ -28,16 +27,21 @@ async function run(_args) {
28
27
  url.port = String(port);
29
28
  const res = await fetch(`${url.origin}/api/health`);
30
29
  if (res.ok) {
31
- console.error(`Daemon appears to be running on port ${port} but PID file is missing.`);
32
- console.error(`Kill the process manually: lsof -ti :${port} | xargs kill`);
33
- process.exit(1);
30
+ return { stopped: false, reason: "orphan", port };
34
31
  }
35
32
  } catch {
36
33
  }
37
- console.error("Daemon is not running (no PID file found).");
38
- process.exit(1);
34
+ return { stopped: false, reason: "not-running" };
39
35
  }
40
36
  const pid = parseInt(readFileSync(pidPath, "utf-8").trim(), 10);
37
+ if (!Number.isInteger(pid) || pid <= 0) {
38
+ console.error(`Stale or corrupt PID file (${pidPath}), removing.`);
39
+ try {
40
+ unlinkSync(pidPath);
41
+ } catch {
42
+ }
43
+ return { stopped: false, reason: "not-running" };
44
+ }
41
45
  try {
42
46
  process.kill(pid, 0);
43
47
  } catch {
@@ -46,21 +50,28 @@ async function run(_args) {
46
50
  } catch {
47
51
  }
48
52
  console.log("Daemon was not running (cleaned up stale PID file).");
49
- return;
53
+ return { stopped: false, reason: "not-running" };
50
54
  }
51
55
  try {
52
56
  process.kill(-pid, "SIGTERM");
53
57
  console.log(`Sent SIGTERM to daemon group (pid ${pid})`);
54
58
  } catch {
55
- process.kill(pid, "SIGTERM");
56
- console.log(`Sent SIGTERM to daemon (pid ${pid})`);
59
+ try {
60
+ process.kill(pid, "SIGTERM");
61
+ console.log(`Sent SIGTERM to daemon (pid ${pid})`);
62
+ } catch (e) {
63
+ console.error(
64
+ `Failed to send SIGTERM to daemon (pid ${pid}): ${e instanceof Error ? e.message : e}`
65
+ );
66
+ return { stopped: false, reason: "kill-failed", port: pid };
67
+ }
57
68
  }
58
69
  const maxWait = 1e4;
59
70
  const start = Date.now();
60
71
  while (Date.now() - start < maxWait) {
61
72
  if (!existsSync(pidPath)) {
62
73
  console.log("Daemon stopped.");
63
- return;
74
+ return { stopped: true, clean: true };
64
75
  }
65
76
  await new Promise((r) => setTimeout(r, 200));
66
77
  }
@@ -69,11 +80,35 @@ async function run(_args) {
69
80
  } catch {
70
81
  try {
71
82
  process.kill(pid, "SIGKILL");
72
- } catch {
83
+ } catch (e) {
84
+ console.error(
85
+ `Failed to force-kill daemon (pid ${pid}): ${e instanceof Error ? e.message : e}`
86
+ );
87
+ console.error(`The daemon may still be running. Kill it manually: kill -9 ${pid}`);
88
+ return { stopped: false, reason: "kill-failed" };
73
89
  }
74
90
  }
91
+ try {
92
+ unlinkSync(pidPath);
93
+ } catch {
94
+ }
95
+ await new Promise((r) => setTimeout(r, 500));
75
96
  console.error("Daemon did not exit cleanly, sent SIGKILL.");
97
+ return { stopped: true, clean: false };
76
98
  }
99
+ async function run(_args) {
100
+ const result = await stopDaemon();
101
+ if (result.stopped) return;
102
+ if (result.reason === "orphan") {
103
+ console.error(`Daemon appears to be running on port ${result.port} but PID file is missing.`);
104
+ console.error(`Kill the process manually: lsof -ti :${result.port} | xargs kill`);
105
+ } else if (result.reason === "not-running") {
106
+ console.error("Daemon is not running (no PID file found).");
107
+ }
108
+ process.exit(1);
109
+ }
110
+
77
111
  export {
112
+ stopDaemon,
78
113
  run
79
114
  };
@@ -28,6 +28,7 @@ function writeCache(latest) {
28
28
  function getCurrentVersion() {
29
29
  const thisDir = new URL(".", import.meta.url).pathname;
30
30
  const candidates = [
31
+ resolve(thisDir, "../package.json"),
31
32
  resolve(thisDir, "../../package.json"),
32
33
  resolve(thisDir, "../../../package.json")
33
34
  ];
package/dist/cli.js CHANGED
@@ -9,13 +9,13 @@ if (!process.env.VOLUTE_HOME) {
9
9
  var command = process.argv[2];
10
10
  var args = process.argv.slice(3);
11
11
  if (command === "--version" || command === "-v") {
12
- const { default: pkg } = await import("./package-4DP4Y4UO.js");
12
+ const { default: pkg } = await import("./package-T2WAVJOU.js");
13
13
  console.log(pkg.version);
14
14
  process.exit(0);
15
15
  }
16
16
  switch (command) {
17
17
  case "agent":
18
- await import("./agent-X7GJLBLW.js").then((m) => m.run(args));
18
+ await import("./agent-7JF7MT73.js").then((m) => m.run(args));
19
19
  break;
20
20
  case "message":
21
21
  await import("./message-SCOQDR3P.js").then((m) => m.run(args));
@@ -36,10 +36,13 @@ switch (command) {
36
36
  await import("./env-7GLUJCWS.js").then((m) => m.run(args));
37
37
  break;
38
38
  case "up":
39
- await import("./up-CSX3ZUIU.js").then((m) => m.run(args));
39
+ await import("./up-RWZF6MLT.js").then((m) => m.run(args));
40
40
  break;
41
41
  case "down":
42
- await import("./down-FXWAN66A.js").then((m) => m.run(args));
42
+ await import("./down-AZVH5TCD.js").then((m) => m.run(args));
43
+ break;
44
+ case "restart":
45
+ await import("./daemon-restart-4HVEKYFY.js").then((m) => m.run(args));
43
46
  break;
44
47
  case "setup":
45
48
  await import("./setup-F4TCWVSP.js").then((m) => m.run(args));
@@ -48,7 +51,7 @@ switch (command) {
48
51
  await import("./service-HZNIDNJF.js").then((m) => m.run(args));
49
52
  break;
50
53
  case "update":
51
- await import("./update-XSIX3GGP.js").then((m) => m.run(args));
54
+ await import("./update-F7QWV2LB.js").then((m) => m.run(args));
52
55
  break;
53
56
  case "--help":
54
57
  case "-h":
@@ -92,6 +95,7 @@ Commands:
92
95
 
93
96
  volute up [--port N] Start the daemon (default: 4200)
94
97
  volute down Stop the daemon
98
+ volute restart [--port N] Restart the daemon
95
99
 
96
100
  volute service install [--port N] Install as system service (auto-start)
97
101
  volute service uninstall Remove system service
@@ -114,7 +118,7 @@ Run 'volute --help' for usage.`);
114
118
  process.exit(1);
115
119
  }
116
120
  if (command !== "update") {
117
- import("./update-check-5ZADDHCK.js").then((m) => m.checkForUpdate()).then((result) => {
121
+ import("./update-check-B4J6IEQ4.js").then((m) => m.checkForUpdate()).then((result) => {
118
122
  if (result.updateAvailable) {
119
123
  console.error(`
120
124
  Update available: ${result.current} \u2192 ${result.latest}`);
@@ -122,6 +122,7 @@ client.on(Events.MessageCreate, async (message) => {
122
122
  ...message.guild?.name ? { serverName: message.guild.name } : {},
123
123
  ...participantCount ? { participantCount } : {}
124
124
  };
125
+ reportTyping(env, channelKey, senderName, false);
125
126
  if (isFollowedChannel && !isMentioned) {
126
127
  await fireAndForget(env, payload);
127
128
  return;
@@ -130,7 +131,7 @@ client.on(Events.MessageCreate, async (message) => {
130
131
  });
131
132
  client.on(Events.TypingStart, (typing) => {
132
133
  if (typing.user.bot) return;
133
- const sender = typing.member?.displayName ?? typing.user.username ?? typing.user.id ?? "unknown";
134
+ const sender = typing.user.displayName || typing.user.username || typing.user.id || "unknown";
134
135
  const typingChannel = typing.guild ? `discord:${slugify(typing.guild.name)}/${slugify("name" in typing.channel ? String(typing.channel.name) : typing.channel.id)}` : `discord:@${slugify(typing.user.username ?? typing.user.id)}`;
135
136
  reportTyping(env, typingChannel, sender, true);
136
137
  });
@@ -138,10 +139,12 @@ async function handleDiscordMessage(message, payload) {
138
139
  const channel = message.channel;
139
140
  if (!("sendTyping" in channel)) return;
140
141
  const typingInterval = setInterval(() => {
141
- channel.sendTyping().catch(() => {
142
+ channel.sendTyping().catch((err) => {
143
+ console.warn(`[discord] sendTyping failed: ${err}`);
142
144
  });
143
145
  }, TYPING_INTERVAL_MS);
144
- channel.sendTyping().catch(() => {
146
+ channel.sendTyping().catch((err) => {
147
+ console.warn(`[discord] sendTyping failed: ${err}`);
145
148
  });
146
149
  let replied = false;
147
150
  try {
@@ -182,7 +185,8 @@ async function handleDiscordMessage(message, payload) {
182
185
  }
183
186
  },
184
187
  onError: async (msg) => {
185
- await message.reply(msg).catch(() => {
188
+ await message.reply(msg).catch((err) => {
189
+ console.error(`[discord] failed to send error reply: ${err}`);
186
190
  });
187
191
  }
188
192
  });
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ run
4
+ } from "./chunk-EG45HBSJ.js";
5
+ import {
6
+ stopDaemon
7
+ } from "./chunk-LLJNZPCU.js";
8
+ import "./chunk-D424ZQGI.js";
9
+ import "./chunk-UWHWAPGO.js";
10
+ import "./chunk-K3NQKI34.js";
11
+
12
+ // src/commands/daemon-restart.ts
13
+ async function run2(args) {
14
+ const result = await stopDaemon();
15
+ if (!result.stopped && result.reason === "kill-failed") {
16
+ console.error("Cannot restart: failed to stop the running daemon.");
17
+ process.exit(1);
18
+ }
19
+ await run(args);
20
+ }
21
+ export {
22
+ run2 as run
23
+ };