volute 0.16.0 → 0.18.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/dist/chunk-AYB7XAWO.js +812 -0
- package/dist/{chunk-3FD4ZZUL.js → chunk-FW5API7X.js} +116 -10
- package/dist/{chunk-3FC42ZBM.js → chunk-GK4E7LM7.js} +3 -0
- package/dist/cli.js +18 -6
- package/dist/connectors/discord.js +1 -1
- package/dist/connectors/slack.js +1 -1
- package/dist/connectors/telegram.js +1 -1
- package/dist/{daemon-restart-MS5FI44G.js → daemon-restart-2HVTHZAT.js} +1 -1
- package/dist/daemon.js +1443 -592
- package/dist/history-YUEKTJ2N.js +108 -0
- package/dist/{mind-manager-PN5SUDJ4.js → mind-manager-Z7O7PN2O.js} +1 -1
- package/dist/{package-3QGV3KX6.js → package-OKLFO7UY.js} +8 -9
- package/dist/{send-KBBZNYG6.js → send-BNDTLUPM.js} +41 -9
- package/dist/skill-2Y42P4JY.js +287 -0
- package/dist/{up-GZLWZAQE.js → up-7B3BWF2U.js} +1 -1
- package/dist/web-assets/assets/index-CtiimdWK.css +1 -0
- package/dist/web-assets/assets/index-kt1_EcuO.js +63 -0
- package/dist/web-assets/index.html +2 -1
- package/drizzle/0006_mind_history.sql +20 -0
- package/drizzle/0007_system_prompts.sql +5 -0
- package/drizzle/0008_volute_channels.sql +24 -0
- package/drizzle/0009_shared_skills.sql +9 -0
- package/drizzle/meta/0006_snapshot.json +7 -0
- package/drizzle/meta/0007_snapshot.json +7 -0
- package/drizzle/meta/0008_snapshot.json +7 -0
- package/drizzle/meta/0009_snapshot.json +7 -0
- package/drizzle/meta/_journal.json +28 -0
- package/package.json +8 -9
- package/templates/_base/.init/.config/prompts.json +5 -0
- package/templates/_base/_skills/volute-mind/SKILL.md +19 -5
- package/templates/_base/src/lib/daemon-client.ts +45 -0
- package/templates/_base/src/lib/logger.ts +19 -0
- package/templates/_base/src/lib/router.ts +48 -41
- package/templates/_base/src/lib/routing.ts +5 -8
- package/templates/_base/src/lib/startup.ts +43 -0
- package/templates/_base/src/lib/transparency.ts +89 -0
- package/templates/_base/src/lib/types.ts +0 -1
- package/templates/_base/src/lib/volute-server.ts +3 -35
- package/templates/claude/src/agent.ts +9 -22
- package/templates/claude/src/lib/hooks/reply-instructions.ts +6 -9
- package/templates/claude/src/lib/stream-consumer.ts +39 -12
- package/templates/pi/src/agent.ts +9 -22
- package/templates/pi/src/lib/event-handler.ts +58 -7
- package/templates/pi/src/lib/reply-instructions-extension.ts +6 -9
- package/dist/chunk-J52CJCVI.js +0 -447
- package/dist/history-LKCJJMUV.js +0 -50
- package/dist/web-assets/assets/index-B1XIIGCh.js +0 -307
- package/templates/_base/src/lib/auto-reply.ts +0 -38
- /package/dist/{chunk-LLBBVTEY.js → chunk-6DVBMLVN.js} +0 -0
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
8
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
9
|
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,300;0,400;0,500;0,600;1,400&display=swap" rel="stylesheet" />
|
|
10
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
+
<script type="module" crossorigin src="/assets/index-kt1_EcuO.js"></script>
|
|
11
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CtiimdWK.css">
|
|
11
12
|
</head>
|
|
12
13
|
<body>
|
|
13
14
|
<div id="root"></div>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
DROP INDEX IF EXISTS `idx_mind_messages_mind`;--> statement-breakpoint
|
|
2
|
+
DROP INDEX IF EXISTS `idx_mind_messages_channel`;--> statement-breakpoint
|
|
3
|
+
CREATE TABLE `mind_history` (
|
|
4
|
+
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
5
|
+
`mind` text NOT NULL,
|
|
6
|
+
`channel` text,
|
|
7
|
+
`session` text,
|
|
8
|
+
`sender` text,
|
|
9
|
+
`message_id` text,
|
|
10
|
+
`type` text NOT NULL DEFAULT 'inbound',
|
|
11
|
+
`content` text,
|
|
12
|
+
`metadata` text,
|
|
13
|
+
`created_at` text DEFAULT (datetime('now')) NOT NULL
|
|
14
|
+
);--> statement-breakpoint
|
|
15
|
+
INSERT INTO `mind_history` (`id`, `mind`, `channel`, `sender`, `type`, `content`, `created_at`)
|
|
16
|
+
SELECT `id`, `mind`, `channel`, `sender`, 'inbound', `content`, `created_at` FROM `mind_messages`;--> statement-breakpoint
|
|
17
|
+
DROP TABLE `mind_messages`;--> statement-breakpoint
|
|
18
|
+
CREATE INDEX `idx_mind_history_mind` ON `mind_history` (`mind`);--> statement-breakpoint
|
|
19
|
+
CREATE INDEX `idx_mind_history_mind_channel` ON `mind_history` (`mind`, `channel`);--> statement-breakpoint
|
|
20
|
+
CREATE INDEX `idx_mind_history_mind_type` ON `mind_history` (`mind`, `type`);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
-- Rebuild conversations table: make mind_name nullable, add type + name columns
|
|
2
|
+
CREATE TABLE `conversations_new` (
|
|
3
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
4
|
+
`mind_name` text,
|
|
5
|
+
`channel` text NOT NULL,
|
|
6
|
+
`type` text NOT NULL DEFAULT 'dm',
|
|
7
|
+
`name` text,
|
|
8
|
+
`user_id` integer REFERENCES `users`(`id`),
|
|
9
|
+
`title` text,
|
|
10
|
+
`created_at` text DEFAULT (datetime('now')) NOT NULL,
|
|
11
|
+
`updated_at` text DEFAULT (datetime('now')) NOT NULL
|
|
12
|
+
);--> statement-breakpoint
|
|
13
|
+
INSERT INTO `conversations_new` (`id`, `mind_name`, `channel`, `type`, `name`, `user_id`, `title`, `created_at`, `updated_at`)
|
|
14
|
+
SELECT `id`, `mind_name`, `channel`, 'dm', NULL, `user_id`, `title`, `created_at`, `updated_at` FROM `conversations`;--> statement-breakpoint
|
|
15
|
+
DROP TABLE `conversations`;--> statement-breakpoint
|
|
16
|
+
ALTER TABLE `conversations_new` RENAME TO `conversations`;--> statement-breakpoint
|
|
17
|
+
CREATE INDEX `idx_conversations_mind_name` ON `conversations` (`mind_name`);--> statement-breakpoint
|
|
18
|
+
CREATE INDEX `idx_conversations_user_id` ON `conversations` (`user_id`);--> statement-breakpoint
|
|
19
|
+
CREATE INDEX `idx_conversations_updated_at` ON `conversations` (`updated_at`);--> statement-breakpoint
|
|
20
|
+
CREATE UNIQUE INDEX `idx_conversations_name` ON `conversations` (`name`);--> statement-breakpoint
|
|
21
|
+
-- Backfill: mark conversations with 3+ participants as 'group'
|
|
22
|
+
UPDATE `conversations` SET `type` = 'group' WHERE `id` IN (
|
|
23
|
+
SELECT `conversation_id` FROM `conversation_participants` GROUP BY `conversation_id` HAVING COUNT(*) > 2
|
|
24
|
+
);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
CREATE TABLE `shared_skills` (
|
|
2
|
+
`id` text PRIMARY KEY NOT NULL,
|
|
3
|
+
`name` text NOT NULL,
|
|
4
|
+
`description` text DEFAULT '' NOT NULL,
|
|
5
|
+
`author` text NOT NULL,
|
|
6
|
+
`version` integer DEFAULT 1 NOT NULL,
|
|
7
|
+
`created_at` text DEFAULT (datetime('now')) NOT NULL,
|
|
8
|
+
`updated_at` text DEFAULT (datetime('now')) NOT NULL
|
|
9
|
+
);
|
|
@@ -43,6 +43,34 @@
|
|
|
43
43
|
"when": 1771200000000,
|
|
44
44
|
"tag": "0005_rename_agents_to_minds",
|
|
45
45
|
"breakpoints": true
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"idx": 6,
|
|
49
|
+
"version": "6",
|
|
50
|
+
"when": 1771400000000,
|
|
51
|
+
"tag": "0006_mind_history",
|
|
52
|
+
"breakpoints": true
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"idx": 7,
|
|
56
|
+
"version": "6",
|
|
57
|
+
"when": 1771600000000,
|
|
58
|
+
"tag": "0007_system_prompts",
|
|
59
|
+
"breakpoints": true
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"idx": 8,
|
|
63
|
+
"version": "6",
|
|
64
|
+
"when": 1771700000000,
|
|
65
|
+
"tag": "0008_volute_channels",
|
|
66
|
+
"breakpoints": true
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"idx": 9,
|
|
70
|
+
"version": "6",
|
|
71
|
+
"when": 1771800000000,
|
|
72
|
+
"tag": "0009_shared_skills",
|
|
73
|
+
"breakpoints": true
|
|
46
74
|
}
|
|
47
75
|
]
|
|
48
76
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "volute",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "CLI for creating and managing self-modifying AI minds powered by the Claude Agent SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"scripts": {
|
|
30
30
|
"dev": "tsx src/cli.ts",
|
|
31
31
|
"build": "tsup && npm run build:web",
|
|
32
|
-
"build:web": "vite build --config src/web/
|
|
33
|
-
"dev:web": "vite --config src/web/
|
|
32
|
+
"build:web": "vite build --config src/web/ui/vite.config.ts",
|
|
33
|
+
"dev:web": "vite --config src/web/ui/vite.config.ts",
|
|
34
34
|
"test": "node --import tsx --import ./test/setup.ts --test --test-force-exit --test-concurrency=1 test/*.test.ts",
|
|
35
35
|
"lint": "biome check src/ test/",
|
|
36
36
|
"lint:fix": "biome check --write src/ test/",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"typecheck": "tsc --noEmit",
|
|
39
39
|
"typecheck:templates": "tsc --noEmit -p templates/claude/tsconfig.json && tsc --noEmit -p templates/pi/tsconfig.json",
|
|
40
40
|
"lint:templates": "biome check templates/",
|
|
41
|
-
"typecheck:web": "
|
|
41
|
+
"typecheck:web": "svelte-check --tsconfig src/web/ui/tsconfig.json",
|
|
42
42
|
"prepare": "lefthook install",
|
|
43
43
|
"prepublishOnly": "npm run build",
|
|
44
44
|
"db:generate": "drizzle-kit generate",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"@hono/zod-validator": "^0.7.6",
|
|
50
50
|
"@libsql/client": "^0.17.0",
|
|
51
51
|
"@slack/bolt": "^4.6.0",
|
|
52
|
+
"adm-zip": "^0.5.16",
|
|
52
53
|
"bcryptjs": "^3.0.3",
|
|
53
54
|
"cron-parser": "^5.5.0",
|
|
54
55
|
"discord.js": "^14.25.1",
|
|
@@ -62,18 +63,16 @@
|
|
|
62
63
|
"@biomejs/biome": "2.3.14",
|
|
63
64
|
"@mariozechner/pi-ai": "^0.52.7",
|
|
64
65
|
"@mariozechner/pi-coding-agent": "^0.52.7",
|
|
66
|
+
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
|
67
|
+
"@types/adm-zip": "^0.5.7",
|
|
65
68
|
"@types/bcryptjs": "^2.4.6",
|
|
66
69
|
"@types/dompurify": "^3.0.5",
|
|
67
70
|
"@types/node": "^25.2.0",
|
|
68
|
-
"@types/react": "^19.2.11",
|
|
69
|
-
"@types/react-dom": "^19.2.3",
|
|
70
|
-
"@vitejs/plugin-react": "^5.1.3",
|
|
71
71
|
"dompurify": "^3.3.1",
|
|
72
72
|
"drizzle-kit": "^0.31.8",
|
|
73
73
|
"lefthook": "^2.1.0",
|
|
74
74
|
"marked": "^17.0.1",
|
|
75
|
-
"
|
|
76
|
-
"react-dom": "^19.2.4",
|
|
75
|
+
"svelte": "^5.53.0",
|
|
77
76
|
"tsup": "^8.0.0",
|
|
78
77
|
"tsx": "^4.0.0",
|
|
79
78
|
"typescript": "^5.7.0",
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compaction_warning": "Context is getting long — compaction is about to summarize this conversation. Before that happens, save anything important to files (MEMORY.md, memory/journal/${date}.md, etc.) since those survive compaction. Focus on: decisions made, open tasks, and anything you'd need to pick up where you left off.",
|
|
3
|
+
"reply_instructions": "To reply to this message, use: volute send ${channel} \"your message\"",
|
|
4
|
+
"channel_invite": "[Channel Invite]\n${headers}\n\n[${sender} — ${time}]\n${preview}\n\nFurther messages will be saved to ${filePath}\n\nTo accept, add to .config/routes.json:\n Rule: { \"channel\": \"${channel}\", \"session\": \"${suggestedSession}\" }\n${batchRecommendation}To respond, use: volute send ${channel} \"your message\"\nTo reject, delete ${filePath}"
|
|
5
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: Volute CLI
|
|
3
|
-
description: This skill should be used when working with the volute CLI, understanding variants, forking, merging, or managing the mind server. Also covers routing config, batch settings, channel gating, and
|
|
3
|
+
description: This skill should be used when working with the volute CLI, understanding variants, forking, merging, or managing the mind server. Also covers routing config, batch settings, channel gating, message flow, and shared skills. Covers "create variant", "merge variant", "send to variant", "fork", "volute CLI", "variant workflow", "mind server", "supervisor", "channel", "discord", "send message", "read messages", "history", "connector", "schedule", "mind-to-mind", "proactive", "initiative", "reach out", "conversation", "group chat", "participants", "invite", "routing", "routes.json", "batch", "debounce", "trigger", "gating", "gate", "skill", "shared skill", "install skill", "publish skill", "update skill".
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Self-Management
|
|
@@ -15,7 +15,7 @@ You manage yourself through the `volute` CLI. Your mind name is auto-detected vi
|
|
|
15
15
|
| `volute mind stop` | Stop your server |
|
|
16
16
|
| `volute mind status` | Check your status |
|
|
17
17
|
| `volute mind logs [--follow] [-n N]` | Read your own logs |
|
|
18
|
-
| `volute history [--channel <ch>] [--limit N]` | View your activity across all channels |
|
|
18
|
+
| `volute history [--channel <ch>] [--limit N] [--full]` | View your activity across all channels |
|
|
19
19
|
| `volute send @<other-mind> "msg"` | Send a message to another mind (or pipe via stdin) |
|
|
20
20
|
| `volute variant create <name> [--soul "..."] [--port N]` | Create a variant to experiment with changes |
|
|
21
21
|
| `volute variant list` | List your variants |
|
|
@@ -100,6 +100,22 @@ After a merge, you receive orientation context about what changed. Update your m
|
|
|
100
100
|
|
|
101
101
|
Create skills by writing `.claude/skills/<name>/SKILL.md` files in your `home/` directory. These are automatically available in your sessions.
|
|
102
102
|
|
|
103
|
+
## Shared Skills
|
|
104
|
+
|
|
105
|
+
Your system has a shared skill repository that all minds can browse and install from.
|
|
106
|
+
|
|
107
|
+
| Command | Purpose |
|
|
108
|
+
|---------|---------|
|
|
109
|
+
| `volute skill list` | List shared skills available to install |
|
|
110
|
+
| `volute skill list --mind` | List your installed skills with update status |
|
|
111
|
+
| `volute skill install <name>` | Install a shared skill |
|
|
112
|
+
| `volute skill update <name>` | Update an installed skill (3-way merge preserves your changes) |
|
|
113
|
+
| `volute skill update --all` | Update all installed skills |
|
|
114
|
+
| `volute skill publish <name>` | Publish one of your skills to the shared repository |
|
|
115
|
+
| `volute skill uninstall <name>` | Remove an installed skill |
|
|
116
|
+
|
|
117
|
+
When you install a skill, it's copied to your skills directory. You can modify it freely — updates use a 3-way merge to preserve your changes. If there are merge conflicts, resolve them like any git conflict.
|
|
118
|
+
|
|
103
119
|
## MCP Configuration
|
|
104
120
|
|
|
105
121
|
Edit `home/.mcp.json` to configure MCP servers for your SDK session. This gives you access to additional tools and services.
|
|
@@ -122,7 +138,6 @@ Messages are routed to sessions based on rules in `.config/routes.json`. Rules a
|
|
|
122
138
|
],
|
|
123
139
|
"sessions": {
|
|
124
140
|
"discord": { "batch": { "debounce": 20, "maxWait": 120, "triggers": ["@mymind"] }, "interrupt": false, "instructions": "Brief responses only." },
|
|
125
|
-
"volute:*": { "autoReply": true }
|
|
126
141
|
},
|
|
127
142
|
"default": "main",
|
|
128
143
|
"gateUnmatched": true
|
|
@@ -152,9 +167,8 @@ The `sessions` section configures behavior per session. Keys are glob patterns m
|
|
|
152
167
|
|
|
153
168
|
| Field | Description |
|
|
154
169
|
|-------|-------------|
|
|
155
|
-
| `batch` | Batch config (see below)
|
|
170
|
+
| `batch` | Batch config (see below) |
|
|
156
171
|
| `interrupt` | Whether to interrupt an in-progress turn (default: `true`) |
|
|
157
|
-
| `autoReply` | When `true`, your text output is automatically sent back to the originating channel. No need to use `volute send` for these conversations. Not supported with batch mode. |
|
|
158
172
|
| `instructions` | Instructions prepended to messages for this session (e.g. `"Brief responses only."`) |
|
|
159
173
|
|
|
160
174
|
### Batch config
|
|
@@ -29,6 +29,51 @@ export async function daemonRestart(context?: {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
export type EventType =
|
|
33
|
+
| "thinking"
|
|
34
|
+
| "text"
|
|
35
|
+
| "tool_use"
|
|
36
|
+
| "tool_result"
|
|
37
|
+
| "log"
|
|
38
|
+
| "usage"
|
|
39
|
+
| "session_start"
|
|
40
|
+
| "done"
|
|
41
|
+
| "inbound"
|
|
42
|
+
| "outbound";
|
|
43
|
+
|
|
44
|
+
export type DaemonEvent = {
|
|
45
|
+
type: EventType;
|
|
46
|
+
session?: string;
|
|
47
|
+
channel?: string;
|
|
48
|
+
messageId?: string;
|
|
49
|
+
content?: string;
|
|
50
|
+
metadata?: Record<string, unknown>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export async function daemonEmit(event: DaemonEvent): Promise<void> {
|
|
54
|
+
if (!port || !mind) {
|
|
55
|
+
if (process.env.VOLUTE_DEBUG === "1") {
|
|
56
|
+
console.error("[volute] daemonEmit: missing VOLUTE_DAEMON_PORT or VOLUTE_MIND");
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const res = await fetch(
|
|
62
|
+
`http://127.0.0.1:${port}/api/minds/${encodeURIComponent(mind)}/events`,
|
|
63
|
+
{
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: headers(),
|
|
66
|
+
body: JSON.stringify(event),
|
|
67
|
+
},
|
|
68
|
+
);
|
|
69
|
+
if (!res.ok) {
|
|
70
|
+
console.error(`[volute] event emit failed: ${res.status}`);
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
// Best-effort — don't let event emission failures break the mind
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
32
77
|
export async function daemonSend(channel: string, text: string): Promise<void> {
|
|
33
78
|
if (!port || !mind) {
|
|
34
79
|
console.error("[volute] daemonSend: VOLUTE_DAEMON_PORT or VOLUTE_MIND not set");
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
+
import { daemonEmit } from "./daemon-client.js";
|
|
2
|
+
import { filterEvent, loadTransparencyPreset } from "./transparency.js";
|
|
3
|
+
|
|
1
4
|
const DEBUG = process.env.VOLUTE_DEBUG === "1";
|
|
5
|
+
// Loaded once at startup — mind restarts on config changes
|
|
6
|
+
const preset = loadTransparencyPreset();
|
|
7
|
+
|
|
8
|
+
/** Categories whose log() calls are also emitted as daemon events. */
|
|
9
|
+
const EMIT_CATEGORIES = new Set(["mind", "server", "auto-commit"]);
|
|
2
10
|
|
|
3
11
|
function truncate(str: string, maxLen = 200): string {
|
|
4
12
|
return str.length > maxLen ? `${str.slice(0, maxLen)}...` : str;
|
|
@@ -11,6 +19,17 @@ export function log(category: string, ...args: unknown[]) {
|
|
|
11
19
|
} catch {
|
|
12
20
|
// EPIPE — parent closed pipes (detached mode). Ignore.
|
|
13
21
|
}
|
|
22
|
+
if (EMIT_CATEGORIES.has(category)) {
|
|
23
|
+
const message = args
|
|
24
|
+
.map((a) => (a instanceof Error ? a.message : typeof a === "string" ? a : JSON.stringify(a)))
|
|
25
|
+
.join(" ");
|
|
26
|
+
const filtered = filterEvent(preset, {
|
|
27
|
+
type: "log",
|
|
28
|
+
content: message,
|
|
29
|
+
metadata: { category },
|
|
30
|
+
});
|
|
31
|
+
if (filtered) daemonEmit(filtered);
|
|
32
|
+
}
|
|
14
33
|
}
|
|
15
34
|
|
|
16
35
|
export function debug(category: string, ...args: unknown[]) {
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
resolveRoute,
|
|
7
7
|
resolveSessionConfig,
|
|
8
8
|
} from "./routing.js";
|
|
9
|
+
import { loadPrompts } from "./startup.js";
|
|
9
10
|
import type { ChannelMeta, HandlerResolver, Listener, VoluteContentPart } from "./types.js";
|
|
10
11
|
|
|
11
12
|
export type Router = {
|
|
@@ -115,38 +116,39 @@ function formatInviteNotification(
|
|
|
115
116
|
messageText: string,
|
|
116
117
|
): string {
|
|
117
118
|
const time = new Date().toLocaleString();
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (meta.
|
|
122
|
-
if (meta.
|
|
123
|
-
if (meta.
|
|
119
|
+
const prompts = loadPrompts();
|
|
120
|
+
|
|
121
|
+
const headerLines: string[] = [];
|
|
122
|
+
if (meta.channel) headerLines.push(`Channel: ${meta.channel}`);
|
|
123
|
+
if (meta.sender) headerLines.push(`Sender: ${meta.sender}`);
|
|
124
|
+
if (meta.platform) headerLines.push(`Platform: ${meta.platform}`);
|
|
125
|
+
if (meta.serverName) headerLines.push(`Server: ${meta.serverName}`);
|
|
126
|
+
if (meta.channelName) headerLines.push(`Channel name: ${meta.channelName}`);
|
|
124
127
|
if (meta.participants && meta.participants.length > 0)
|
|
125
|
-
|
|
126
|
-
|
|
128
|
+
headerLines.push(`Participants: ${meta.participants.join(", ")}`);
|
|
129
|
+
|
|
127
130
|
const preview = messageText.length > 200 ? `${messageText.slice(0, 200)}...` : messageText;
|
|
128
|
-
lines.push(`[${meta.sender ?? "unknown"} — ${time}]`);
|
|
129
|
-
lines.push(preview);
|
|
130
|
-
lines.push("");
|
|
131
|
-
lines.push(`Further messages will be saved to ${filePath}`);
|
|
132
|
-
lines.push("");
|
|
133
|
-
lines.push("To accept, add to .config/routes.json:");
|
|
134
131
|
const suggestedSession = sanitizeChannelPath(meta.channel ?? "unknown");
|
|
132
|
+
const channel = meta.channel ?? "unknown";
|
|
135
133
|
const otherCount = (meta.participantCount ?? 1) - 1;
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
134
|
+
const batchRecommendation =
|
|
135
|
+
otherCount > 1
|
|
136
|
+
? ` Session config: "${suggestedSession}": { "batch": { "debounce": 20, "maxWait": 120 } }\n(batch recommended — ${otherCount} other participants may generate frequent messages)\n`
|
|
137
|
+
: "";
|
|
138
|
+
|
|
139
|
+
const vars: Record<string, string> = {
|
|
140
|
+
headers: headerLines.join("\n"),
|
|
141
|
+
sender: meta.sender ?? "unknown",
|
|
142
|
+
time,
|
|
143
|
+
preview,
|
|
144
|
+
filePath,
|
|
145
|
+
channel,
|
|
146
|
+
suggestedSession,
|
|
147
|
+
batchRecommendation,
|
|
148
|
+
};
|
|
149
|
+
return prompts.channel_invite.replace(/\$\{(\w+)\}/g, (match, name) =>
|
|
150
|
+
name in vars ? vars[name] : match,
|
|
151
|
+
);
|
|
150
152
|
}
|
|
151
153
|
|
|
152
154
|
export function createRouter(options: {
|
|
@@ -213,11 +215,7 @@ export function createRouter(options: {
|
|
|
213
215
|
|
|
214
216
|
// Batch flushes are fire-and-forget — no HTTP response is waiting, so listener is a noop
|
|
215
217
|
try {
|
|
216
|
-
handler.handle(
|
|
217
|
-
content,
|
|
218
|
-
{ sessionName: buffer.sessionName, messageId, autoReply: false },
|
|
219
|
-
() => {},
|
|
220
|
-
);
|
|
218
|
+
handler.handle(content, { sessionName: buffer.sessionName, messageId }, () => {});
|
|
221
219
|
} catch (err) {
|
|
222
220
|
log("router", `error flushing batch for session ${buffer.sessionName}:`, err);
|
|
223
221
|
return;
|
|
@@ -284,7 +282,7 @@ export function createRouter(options: {
|
|
|
284
282
|
if (options.fileHandler) {
|
|
285
283
|
const formatted = applyPrefix(content, meta);
|
|
286
284
|
const fileHandler = options.fileHandler(filePath);
|
|
287
|
-
fileHandler.handle(formatted, { ...meta, messageId
|
|
285
|
+
fileHandler.handle(formatted, { ...meta, messageId }, noop);
|
|
288
286
|
}
|
|
289
287
|
|
|
290
288
|
// First message from this channel — send invite notification
|
|
@@ -299,7 +297,6 @@ export function createRouter(options: {
|
|
|
299
297
|
sessionName: "main",
|
|
300
298
|
messageId: generateMessageId(),
|
|
301
299
|
interrupt: true,
|
|
302
|
-
autoReply: false,
|
|
303
300
|
},
|
|
304
301
|
noop,
|
|
305
302
|
);
|
|
@@ -309,16 +306,27 @@ export function createRouter(options: {
|
|
|
309
306
|
return { messageId, unsubscribe: noop };
|
|
310
307
|
}
|
|
311
308
|
|
|
309
|
+
// Mention-mode filtering: skip messages that don't mention this mind
|
|
310
|
+
if (resolved.destination === "mind" && resolved.mode === "mention") {
|
|
311
|
+
const mindName = process.env.VOLUTE_MIND;
|
|
312
|
+
if (mindName) {
|
|
313
|
+
const escaped = mindName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
314
|
+
const pattern = new RegExp(`\\b${escaped}\\b`, "i");
|
|
315
|
+
if (!pattern.test(text)) {
|
|
316
|
+
queueMicrotask(() => safeListener({ type: "done", messageId }));
|
|
317
|
+
return { messageId, unsubscribe: noop };
|
|
318
|
+
}
|
|
319
|
+
} else {
|
|
320
|
+
log("router", "VOLUTE_MIND not set — mention filtering disabled");
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
312
324
|
// File destination
|
|
313
325
|
if (resolved.destination === "file") {
|
|
314
326
|
if (options.fileHandler) {
|
|
315
327
|
const formatted = applyPrefix(content, meta);
|
|
316
328
|
const handler = options.fileHandler(resolved.path);
|
|
317
|
-
const unsubscribe = handler.handle(
|
|
318
|
-
formatted,
|
|
319
|
-
{ ...meta, messageId, autoReply: false },
|
|
320
|
-
safeListener,
|
|
321
|
-
);
|
|
329
|
+
const unsubscribe = handler.handle(formatted, { ...meta, messageId }, safeListener);
|
|
322
330
|
return { messageId, unsubscribe };
|
|
323
331
|
}
|
|
324
332
|
// No file handler configured — emit done and discard
|
|
@@ -386,7 +394,6 @@ export function createRouter(options: {
|
|
|
386
394
|
sessionName,
|
|
387
395
|
messageId,
|
|
388
396
|
interrupt: sessionConfig.interrupt,
|
|
389
|
-
autoReply: sessionConfig.autoReply,
|
|
390
397
|
},
|
|
391
398
|
safeListener,
|
|
392
399
|
);
|
|
@@ -15,17 +15,16 @@ export type RoutingRule = {
|
|
|
15
15
|
sender?: string;
|
|
16
16
|
isDM?: boolean; // match on isDM metadata
|
|
17
17
|
participants?: number; // match on participant count (e.g. 2 = DM)
|
|
18
|
+
mode?: "all" | "mention"; // "mention" = only process if mind name appears in message
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
export type SessionConfig = {
|
|
21
|
-
autoReply?: boolean;
|
|
22
22
|
batch?: number | BatchConfig;
|
|
23
23
|
interrupt?: boolean;
|
|
24
24
|
instructions?: string;
|
|
25
25
|
};
|
|
26
26
|
|
|
27
27
|
export type ResolvedSessionConfig = {
|
|
28
|
-
autoReply: boolean;
|
|
29
28
|
batch?: BatchConfig;
|
|
30
29
|
interrupt: boolean;
|
|
31
30
|
instructions?: string;
|
|
@@ -43,6 +42,7 @@ export type ResolvedRoute =
|
|
|
43
42
|
destination: "mind";
|
|
44
43
|
session: string;
|
|
45
44
|
matched: boolean;
|
|
45
|
+
mode?: "all" | "mention";
|
|
46
46
|
}
|
|
47
47
|
| { destination: "file"; path: string; matched: boolean };
|
|
48
48
|
|
|
@@ -77,7 +77,7 @@ function globMatch(pattern: string, value: string): boolean {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
const GLOB_MATCH_KEYS = new Set(["channel", "sender"]);
|
|
80
|
-
const NON_MATCH_KEYS = new Set(["session", "destination", "path"]);
|
|
80
|
+
const NON_MATCH_KEYS = new Set(["session", "destination", "path", "mode"]);
|
|
81
81
|
|
|
82
82
|
type MatchMeta = { channel?: string; sender?: string; isDM?: boolean; participantCount?: number };
|
|
83
83
|
|
|
@@ -137,6 +137,7 @@ export function resolveRoute(config: RoutingConfig, meta: MatchMeta): ResolvedRo
|
|
|
137
137
|
destination: "mind",
|
|
138
138
|
session: sanitizeSessionName(expandTemplate(rule.session ?? fallback, meta)),
|
|
139
139
|
matched: true,
|
|
140
|
+
mode: rule.mode,
|
|
140
141
|
};
|
|
141
142
|
}
|
|
142
143
|
}
|
|
@@ -152,18 +153,14 @@ export function resolveSessionConfig(
|
|
|
152
153
|
config: RoutingConfig,
|
|
153
154
|
sessionName: string,
|
|
154
155
|
): ResolvedSessionConfig {
|
|
155
|
-
const defaults: ResolvedSessionConfig = {
|
|
156
|
+
const defaults: ResolvedSessionConfig = { interrupt: true };
|
|
156
157
|
|
|
157
158
|
if (!config.sessions) return defaults;
|
|
158
159
|
|
|
159
160
|
for (const [pattern, sessionConfig] of Object.entries(config.sessions)) {
|
|
160
161
|
if (globMatch(pattern, sessionName)) {
|
|
161
162
|
const batch = sessionConfig.batch != null ? normalizeBatch(sessionConfig.batch) : undefined;
|
|
162
|
-
if (sessionConfig.autoReply && batch != null) {
|
|
163
|
-
log("routing", `autoReply is not supported with batch mode — autoReply will be ignored`);
|
|
164
|
-
}
|
|
165
163
|
return {
|
|
166
|
-
autoReply: batch != null ? false : (sessionConfig.autoReply ?? false),
|
|
167
164
|
batch,
|
|
168
165
|
interrupt: sessionConfig.interrupt ?? true,
|
|
169
166
|
instructions: sessionConfig.instructions,
|
|
@@ -102,6 +102,49 @@ export async function handleStartupContext(sendMessage: (content: string) => voi
|
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
export type MindPrompts = {
|
|
106
|
+
compaction_warning: string;
|
|
107
|
+
reply_instructions: string;
|
|
108
|
+
channel_invite: string;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const DEFAULT_PROMPTS: MindPrompts = {
|
|
112
|
+
compaction_warning:
|
|
113
|
+
"Context is getting long — compaction is about to summarize this conversation. Before that happens, save anything important to files (MEMORY.md, memory/journal/${date}.md, etc.) since those survive compaction. Focus on: decisions made, open tasks, and anything you'd need to pick up where you left off.",
|
|
114
|
+
reply_instructions: 'To reply to this message, use: volute send ${channel} "your message"',
|
|
115
|
+
channel_invite: `[Channel Invite]
|
|
116
|
+
\${headers}
|
|
117
|
+
|
|
118
|
+
[\${sender} — \${time}]
|
|
119
|
+
\${preview}
|
|
120
|
+
|
|
121
|
+
Further messages will be saved to \${filePath}
|
|
122
|
+
|
|
123
|
+
To accept, add to .config/routes.json:
|
|
124
|
+
Rule: { "channel": "\${channel}", "session": "\${suggestedSession}" }
|
|
125
|
+
\${batchRecommendation}To respond, use: volute send \${channel} "your message"
|
|
126
|
+
To reject, delete \${filePath}`,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export function loadPrompts(): MindPrompts {
|
|
130
|
+
try {
|
|
131
|
+
const raw = readFileSync(resolve("home/.config/prompts.json"), "utf-8");
|
|
132
|
+
const parsed = JSON.parse(raw);
|
|
133
|
+
const result = { ...DEFAULT_PROMPTS };
|
|
134
|
+
for (const key of Object.keys(DEFAULT_PROMPTS) as (keyof MindPrompts)[]) {
|
|
135
|
+
if (typeof parsed[key] === "string") {
|
|
136
|
+
result[key] = parsed[key];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return result;
|
|
140
|
+
} catch (err: any) {
|
|
141
|
+
if (err?.code !== "ENOENT") {
|
|
142
|
+
log("startup", "failed to load prompts.json, using defaults:", err);
|
|
143
|
+
}
|
|
144
|
+
return DEFAULT_PROMPTS;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
105
148
|
export function setupShutdown(): void {
|
|
106
149
|
function shutdown() {
|
|
107
150
|
log("server", "shutdown signal received");
|