volute 0.17.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/archive-ZCFOSTKB.js +15 -0
- package/dist/{channel-SLURLIRV.js → channel-PUQKGSQM.js} +60 -7
- package/dist/{chunk-CE7WMOVW.js → chunk-2TJGRJ4O.js} +236 -103
- package/dist/{chunk-6BDNWYKG.js → chunk-32VR2EOH.js} +2 -2
- package/dist/chunk-4KPUF5JD.js +214 -0
- package/dist/{chunk-QJIIHU32.js → chunk-7NO7EV5Z.js} +2 -2
- package/dist/chunk-AW7P4EVV.js +159 -0
- package/dist/{chunk-2Y77MCFG.js → chunk-DYZGP3EW.js} +2 -2
- package/dist/{chunk-M77QBTEH.js → chunk-EBGCNDMM.js} +24 -14
- package/dist/{chunk-GSPWIM5E.js → chunk-EMQSAY3B.js} +77 -6
- package/dist/{chunk-37X7ECMF.js → chunk-FCDU5BFX.js} +1 -1
- package/dist/chunk-FGV2H4TX.js +803 -0
- package/dist/{chunk-ZCEYUUID.js → chunk-OGXOMR65.js} +2 -1
- package/dist/chunk-OTWLI7F4.js +375 -0
- package/dist/{chunk-3FC42ZBM.js → chunk-RHEGSQFJ.js} +4 -1
- package/dist/{chunk-MVSXRMJJ.js → chunk-SCUDS4US.js} +1 -1
- package/dist/{chunk-MIJIAGGG.js → chunk-UJ6GHNR7.js} +8 -6
- package/dist/{chunk-OYSZNX5I.js → chunk-VDWCHYTS.js} +1 -1
- package/dist/{chunk-77ISBIKI.js → chunk-VE4D3GOP.js} +2 -2
- package/dist/chunk-VQWDC6UK.js +142 -0
- package/dist/{chunk-OJQ47SCA.js → chunk-WC6ZHVRL.js} +1 -1
- package/dist/chunk-YUIHSKR6.js +72 -0
- package/dist/chunk-Z524RFCJ.js +36 -0
- package/dist/cli.js +44 -24
- package/dist/{connector-3ELFMI2R.js → connector-JBVNZ7VK.js} +6 -6
- package/dist/connectors/discord.js +2 -2
- package/dist/connectors/slack.js +2 -2
- package/dist/connectors/telegram.js +2 -2
- package/dist/{create-ZWHCRT5F.js → create-HP4OVVHF.js} +6 -4
- package/dist/{daemon-client-ODKDUYDE.js → daemon-client-ITWUCNFO.js} +2 -2
- package/dist/{daemon-restart-VRQMZLBK.js → daemon-restart-JMZM3QY4.js} +8 -8
- package/dist/daemon.js +1624 -940
- package/dist/db-5ZVC6MQF.js +10 -0
- package/dist/{delete-6G6WEX4F.js → delete-BSU7K3RY.js} +1 -1
- package/dist/delivery-manager-ISTJMZDW.js +16 -0
- package/dist/down-ZY35KMHR.js +14 -0
- package/dist/{env-6IDWGBUH.js → env-A3LMO777.js} +6 -6
- package/dist/export-GCDNQCF3.js +100 -0
- package/dist/{history-5F4WQW7S.js → history-WNK3DFUM.js} +10 -7
- package/dist/{import-EDGRLIGO.js → import-M63VIUJ5.js} +3 -3
- package/dist/log-PPPZDVEF.js +39 -0
- package/dist/{login-ORQDXLBM.js → login-HNH3EUQV.js} +2 -2
- package/dist/{logout-XC5AUO5I.js → logout-I5CB5UZS.js} +2 -2
- package/dist/{logs-GYOR3L2L.js → logs-SF2IMJN4.js} +6 -6
- package/dist/merge-33C237A4.js +46 -0
- package/dist/{mind-OJN6RBZW.js → mind-PQ5NCPSU.js} +14 -10
- package/dist/mind-manager-RVCFROAY.js +18 -0
- package/dist/{package-4GTJGUXI.js → package-MYE2ZJLV.js} +7 -3
- package/dist/{pages-6IV4VQTU.js → pages-AXCOSY3P.js} +2 -2
- package/dist/{publish-Q4RPSJLL.js → publish-YB377JB7.js} +18 -4
- package/dist/pull-XAEWQJ47.js +39 -0
- package/dist/{register-LDE6LRXY.js → register-VSPCMHKX.js} +2 -2
- package/dist/{restart-YFAWFS5T.js → restart-IQKMCK5M.js} +6 -6
- package/dist/{schedule-AGYLDMNS.js → schedule-LMX7GAQZ.js} +6 -6
- package/dist/schema-5BW7DFZI.js +24 -0
- package/dist/{seed-AP4Q7RZ7.js → seed-J43YDKXG.js} +7 -4
- package/dist/{send-4GKDO26C.js → send-KVIZIGCE.js} +8 -8
- package/dist/{service-U7MZ2H7F.js → service-LUR7WDO7.js} +6 -6
- package/dist/{setup-DJKIZKGW.js → setup-OH3PJUJO.js} +7 -7
- package/dist/shared-KO35ZM44.js +39 -0
- package/dist/skill-BCVNI6TV.js +287 -0
- package/{templates/_base/_skills → dist/skills}/orientation/SKILL.md +1 -1
- package/{templates/_base/_skills → dist/skills}/sessions/SKILL.md +2 -2
- package/{templates/_base/_skills → dist/skills}/volute-mind/SKILL.md +35 -1
- package/dist/{sprout-TJ3BHVOG.js → sprout-VBEX63LX.js} +38 -20
- package/dist/{start-3YYRXBKP.js → start-I5JYB65M.js} +6 -6
- package/dist/{status-VSFZYX7S.js → status-4ESFLGH4.js} +5 -5
- package/dist/status-D7E5HHBV.js +35 -0
- package/dist/{status-OKNA6AR3.js → status-JCJAOXTW.js} +2 -2
- package/dist/{stop-AA5K5LYG.js → stop-NBVKEFQQ.js} +6 -6
- package/dist/{up-LT3X5Q26.js → up-WG65SWJU.js} +5 -5
- package/dist/{update-YAGN5ODG.js → update-FJIHDJKM.js} +5 -5
- package/dist/{update-check-APLTH4IN.js → update-check-MWE5AH4U.js} +2 -2
- package/dist/{upgrade-KXZCQSZN.js → upgrade-AIT24B5I.js} +1 -1
- package/dist/{variant-X5QFG6KK.js → variant-63ZWO2W7.js} +4 -4
- package/dist/variants-JAGWGBXG.js +26 -0
- package/dist/web-assets/assets/index-BAbuRsVF.css +1 -0
- package/dist/web-assets/assets/index-CiQhSKi_.js +63 -0
- package/dist/web-assets/index.html +2 -2
- 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/0010_delivery_queue.sql +12 -0
- package/drizzle/0011_rename_human_to_brain.sql +1 -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/0010_snapshot.json +7 -0
- package/drizzle/meta/0011_snapshot.json +7 -0
- package/drizzle/meta/_journal.json +35 -0
- package/package.json +7 -3
- package/templates/_base/.init/.config/hooks/startup-context.sh +1 -1
- package/templates/_base/.init/.config/prompts.json +5 -0
- package/templates/_base/.init/.config/scripts/session-reader.ts +3 -3
- package/templates/_base/home/VOLUTE.md +16 -1
- package/templates/_base/src/lib/auto-commit.ts +51 -14
- package/templates/_base/src/lib/router.ts +168 -29
- package/templates/_base/src/lib/routing.ts +4 -1
- package/templates/_base/src/lib/startup.ts +43 -0
- package/templates/_base/src/lib/types.ts +4 -0
- package/templates/_base/src/lib/volute-server.ts +91 -2
- package/templates/claude/src/agent.ts +4 -3
- package/templates/claude/src/lib/hooks/reply-instructions.ts +3 -1
- package/templates/claude/src/server.ts +2 -2
- package/templates/claude/volute-template.json +1 -2
- package/templates/pi/src/agent.ts +6 -7
- package/templates/pi/src/lib/reply-instructions-extension.ts +3 -1
- package/templates/pi/src/lib/session-context-extension.ts +2 -2
- package/templates/pi/volute-template.json +1 -2
- package/dist/chunk-PO5Q2AYN.js +0 -121
- package/dist/down-A56B5JLK.js +0 -14
- package/dist/mind-manager-ETNCPQJN.js +0 -15
- package/dist/web-assets/assets/index-BcmT7Qxo.js +0 -63
- package/dist/web-assets/assets/index-DG01TyLb.css +0 -1
- /package/{templates/_base/_skills → dist/skills}/memory/SKILL.md +0 -0
|
@@ -7,8 +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-
|
|
11
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
10
|
+
<script type="module" crossorigin src="/assets/index-CiQhSKi_.js"></script>
|
|
11
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BAbuRsVF.css">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
|
14
14
|
<div id="root"></div>
|
|
@@ -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
|
+
);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
CREATE TABLE `delivery_queue` (
|
|
2
|
+
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
|
3
|
+
`mind` text NOT NULL,
|
|
4
|
+
`session` text NOT NULL,
|
|
5
|
+
`channel` text,
|
|
6
|
+
`sender` text,
|
|
7
|
+
`status` text NOT NULL DEFAULT 'pending',
|
|
8
|
+
`payload` text NOT NULL,
|
|
9
|
+
`created_at` text DEFAULT (datetime('now')) NOT NULL
|
|
10
|
+
);--> statement-breakpoint
|
|
11
|
+
CREATE INDEX `idx_delivery_queue_mind_session` ON `delivery_queue` (`mind`, `session`);--> statement-breakpoint
|
|
12
|
+
CREATE INDEX `idx_delivery_queue_mind_status` ON `delivery_queue` (`mind`, `status`);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
UPDATE `users` SET `user_type` = 'brain' WHERE `user_type` = 'human';
|
|
@@ -50,6 +50,41 @@
|
|
|
50
50
|
"when": 1771400000000,
|
|
51
51
|
"tag": "0006_mind_history",
|
|
52
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
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"idx": 10,
|
|
77
|
+
"version": "6",
|
|
78
|
+
"when": 1771900000000,
|
|
79
|
+
"tag": "0010_delivery_queue",
|
|
80
|
+
"breakpoints": true
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"idx": 11,
|
|
84
|
+
"version": "6",
|
|
85
|
+
"when": 1772000000000,
|
|
86
|
+
"tag": "0011_rename_human_to_brain",
|
|
87
|
+
"breakpoints": true
|
|
53
88
|
}
|
|
54
89
|
]
|
|
55
90
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "volute",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.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",
|
|
@@ -28,10 +28,12 @@
|
|
|
28
28
|
],
|
|
29
29
|
"scripts": {
|
|
30
30
|
"dev": "tsx src/cli.ts",
|
|
31
|
-
"build": "tsup && npm run build:web",
|
|
31
|
+
"build": "tsup && cp -r skills dist/skills && npm run build:web",
|
|
32
32
|
"build:web": "vite build --config src/web/ui/vite.config.ts",
|
|
33
33
|
"dev:web": "vite --config src/web/ui/vite.config.ts",
|
|
34
|
-
"test": "node --import tsx --import ./test/setup.ts --test --test-force-exit --test-concurrency=
|
|
34
|
+
"test": "node --import tsx --import ./test/setup.ts --test --test-force-exit --test-concurrency=4 $(find test -name '*.test.ts' ! -name 'daemon-e2e.test.ts' | sort)",
|
|
35
|
+
"test:e2e": "node --import tsx --import ./test/setup.ts --test --test-force-exit --test-concurrency=1 test/daemon-e2e.test.ts",
|
|
36
|
+
"test:all": "npm test && npm run test:e2e",
|
|
35
37
|
"lint": "biome check src/ test/",
|
|
36
38
|
"lint:fix": "biome check --write src/ test/",
|
|
37
39
|
"format": "biome format --write src/ test/",
|
|
@@ -49,6 +51,7 @@
|
|
|
49
51
|
"@hono/zod-validator": "^0.7.6",
|
|
50
52
|
"@libsql/client": "^0.17.0",
|
|
51
53
|
"@slack/bolt": "^4.6.0",
|
|
54
|
+
"adm-zip": "^0.5.16",
|
|
52
55
|
"bcryptjs": "^3.0.3",
|
|
53
56
|
"cron-parser": "^5.5.0",
|
|
54
57
|
"discord.js": "^14.25.1",
|
|
@@ -63,6 +66,7 @@
|
|
|
63
66
|
"@mariozechner/pi-ai": "^0.52.7",
|
|
64
67
|
"@mariozechner/pi-coding-agent": "^0.52.7",
|
|
65
68
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
|
69
|
+
"@types/adm-zip": "^0.5.7",
|
|
66
70
|
"@types/bcryptjs": "^2.4.6",
|
|
67
71
|
"@types/dompurify": "^3.0.5",
|
|
68
72
|
"@types/node": "^25.2.0",
|
|
@@ -11,7 +11,7 @@ SOURCE=$(echo "$INPUT" | jq -r '.source // "startup"')
|
|
|
11
11
|
CONTEXT="Session ${SOURCE} at $(date '+%Y-%m-%d %H:%M')."
|
|
12
12
|
|
|
13
13
|
# Active sessions
|
|
14
|
-
SESSIONS_DIR=".
|
|
14
|
+
SESSIONS_DIR=".mind/sessions"
|
|
15
15
|
if [ -d "$SESSIONS_DIR" ]; then
|
|
16
16
|
SESSION_LIST=$(ls -1 "$SESSIONS_DIR"/*.json 2>/dev/null | xargs -I{} basename {} .json | sort)
|
|
17
17
|
if [ -n "$SESSION_LIST" ]; then
|
|
@@ -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
|
+
}
|
|
@@ -33,8 +33,8 @@ if (!sessionName) {
|
|
|
33
33
|
|
|
34
34
|
// Detect template type and resolve JSONL path
|
|
35
35
|
const cwd = process.cwd();
|
|
36
|
-
const agentSdkSessions = resolve(cwd, "../.
|
|
37
|
-
const piSessions = resolve(cwd, "../.
|
|
36
|
+
const agentSdkSessions = resolve(cwd, "../.mind/sessions");
|
|
37
|
+
const piSessions = resolve(cwd, "../.mind/pi-sessions");
|
|
38
38
|
|
|
39
39
|
let jsonlPath: string | null = null;
|
|
40
40
|
let format: "agent-sdk" | "pi";
|
|
@@ -46,7 +46,7 @@ if (existsSync(agentSdkSessions)) {
|
|
|
46
46
|
format = "pi";
|
|
47
47
|
jsonlPath = resolvePiJsonl(piSessions, sessionName);
|
|
48
48
|
} else {
|
|
49
|
-
console.error("No session directory found. Expected .
|
|
49
|
+
console.error("No session directory found. Expected .mind/sessions/ or .mind/pi-sessions/");
|
|
50
50
|
process.exit(1);
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -7,7 +7,7 @@ You are a volute mind — a persistent being that communicates across multiple c
|
|
|
7
7
|
**Your text output stays in your session — it is not sent to anyone.** To send a message, you must use the `volute send` command:
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
|
-
volute send @other-mind "hello" # DM another
|
|
10
|
+
volute send @other-mind "hello" # DM another user
|
|
11
11
|
volute send discord:server/channel "hello" # send to a channel
|
|
12
12
|
volute send animal-chat "hello" # send to a volute channel
|
|
13
13
|
```
|
|
@@ -34,6 +34,21 @@ Messages are routed to named sessions based on rules in `.config/routes.json`. E
|
|
|
34
34
|
|
|
35
35
|
Messages from unrecognized channels are held until you add a routing rule. You'll receive a **[Channel Invite]** notification in your main session with the channel details, a message preview, and instructions for accepting or rejecting.
|
|
36
36
|
|
|
37
|
+
## Shared Files
|
|
38
|
+
|
|
39
|
+
Your `shared/` directory is a collaborative space where all minds can work on files together. Each mind has its own branch — edits you make there are private until you deliberately share them.
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
volute shared status # see what you've changed vs main
|
|
43
|
+
volute shared merge "msg" # share your changes with everyone
|
|
44
|
+
volute shared pull # get the latest from other minds
|
|
45
|
+
volute shared log # see recent shared history
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Files you edit in `shared/` are auto-committed to your branch. When you're ready to share, merge to main. Other minds get your changes by pulling. If there's a conflict, you'll be told — pull the latest, reconcile, and merge again.
|
|
49
|
+
|
|
50
|
+
The `shared/pages/` directory can be published as the system's shared website with `volute pages publish` (no `--mind` flag).
|
|
51
|
+
|
|
37
52
|
## Reference
|
|
38
53
|
|
|
39
54
|
See the **volute-mind** skill for routing config syntax, batch options, channel management, and all CLI commands.
|
|
@@ -2,6 +2,10 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { log } from "./logger.js";
|
|
4
4
|
|
|
5
|
+
function gitArgs(args: string[]): string[] {
|
|
6
|
+
return process.env.VOLUTE_ISOLATION === "user" ? ["-c", "safe.directory=*", ...args] : args;
|
|
7
|
+
}
|
|
8
|
+
|
|
5
9
|
function exec(cmd: string, args: string[], cwd: string): Promise<{ code: number; stdout: string }> {
|
|
6
10
|
return new Promise((r) => {
|
|
7
11
|
execFile(cmd, args, { cwd }, (_err, stdout) => {
|
|
@@ -16,6 +20,9 @@ let pending = Promise.resolve();
|
|
|
16
20
|
/**
|
|
17
21
|
* Commit a file change in the mind's home directory.
|
|
18
22
|
* Called by the PostToolUse hook when Edit or Write completes.
|
|
23
|
+
*
|
|
24
|
+
* Files under home/shared/ are committed to the shared worktree repo
|
|
25
|
+
* with mind attribution. All other files go to the mind's own repo.
|
|
19
26
|
*/
|
|
20
27
|
export function commitFileChange(filePath: string, cwd: string): void {
|
|
21
28
|
// Only commit files under the home directory
|
|
@@ -26,21 +33,51 @@ export function commitFileChange(filePath: string, cwd: string): void {
|
|
|
26
33
|
const relativePath = resolved.slice(homeDir.length + 1);
|
|
27
34
|
if (!relativePath) return;
|
|
28
35
|
|
|
36
|
+
// Check if this file is under the shared/ worktree
|
|
37
|
+
const sharedPrefix = "shared/";
|
|
38
|
+
const isShared = relativePath.startsWith(sharedPrefix);
|
|
39
|
+
|
|
29
40
|
pending = pending.then(async () => {
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
if (isShared) {
|
|
42
|
+
// Route to shared worktree
|
|
43
|
+
const sharedCwd = resolve(cwd, "shared");
|
|
44
|
+
const sharedRelative = relativePath.slice(sharedPrefix.length);
|
|
45
|
+
const mindName = process.env.VOLUTE_MIND ?? "unknown";
|
|
46
|
+
|
|
47
|
+
if ((await exec("git", gitArgs(["add", sharedRelative]), sharedCwd)).code !== 0) {
|
|
48
|
+
log("auto-commit", `git add failed for shared/${sharedRelative}`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if ((await exec("git", gitArgs(["diff", "--cached", "--quiet"]), sharedCwd)).code === 0)
|
|
52
|
+
return;
|
|
53
|
+
|
|
54
|
+
const message = `Update ${sharedRelative}`;
|
|
55
|
+
const authorFlag = `${mindName} <${mindName}@volute>`;
|
|
56
|
+
if (
|
|
57
|
+
(await exec("git", gitArgs(["commit", "--author", authorFlag, "-m", message]), sharedCwd))
|
|
58
|
+
.code === 0
|
|
59
|
+
) {
|
|
60
|
+
log("auto-commit", `[shared] ${message}`);
|
|
61
|
+
} else {
|
|
62
|
+
log("auto-commit", `[shared] commit failed for ${sharedRelative}`);
|
|
63
|
+
}
|
|
64
|
+
// No auto-push for shared files — sharing is deliberate
|
|
65
|
+
} else {
|
|
66
|
+
// Existing behavior: commit to mind's own repo
|
|
67
|
+
if ((await exec("git", ["add", relativePath], cwd)).code !== 0) {
|
|
68
|
+
log("auto-commit", `git add failed for ${relativePath}`);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if ((await exec("git", ["diff", "--cached", "--quiet"], cwd)).code === 0) return;
|
|
72
|
+
|
|
73
|
+
const message = `Update ${relativePath}`;
|
|
74
|
+
if ((await exec("git", ["commit", "-m", message], cwd)).code === 0) {
|
|
75
|
+
log("auto-commit", message);
|
|
76
|
+
// Push if a remote is configured
|
|
77
|
+
const { stdout: remote } = await exec("git", ["remote"], cwd);
|
|
78
|
+
if (remote) {
|
|
79
|
+
await exec("git", ["push"], cwd);
|
|
80
|
+
}
|
|
44
81
|
}
|
|
45
82
|
}
|
|
46
83
|
});
|
|
@@ -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 = {
|
|
@@ -14,6 +15,18 @@ export type Router = {
|
|
|
14
15
|
meta: ChannelMeta,
|
|
15
16
|
listener?: Listener,
|
|
16
17
|
): { messageId: string; unsubscribe: () => void };
|
|
18
|
+
/** Direct dispatch to a pre-routed session (daemon has already resolved the route). */
|
|
19
|
+
dispatch(
|
|
20
|
+
content: VoluteContentPart[],
|
|
21
|
+
session: string,
|
|
22
|
+
meta: ChannelMeta,
|
|
23
|
+
listener?: Listener,
|
|
24
|
+
): { messageId: string; unsubscribe: () => void };
|
|
25
|
+
dispatchBatch(
|
|
26
|
+
batch: { channels: Record<string, any[]> },
|
|
27
|
+
session: string,
|
|
28
|
+
meta: ChannelMeta,
|
|
29
|
+
): void;
|
|
17
30
|
close(): void;
|
|
18
31
|
};
|
|
19
32
|
|
|
@@ -115,38 +128,39 @@ function formatInviteNotification(
|
|
|
115
128
|
messageText: string,
|
|
116
129
|
): string {
|
|
117
130
|
const time = new Date().toLocaleString();
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (meta.
|
|
122
|
-
if (meta.
|
|
123
|
-
if (meta.
|
|
131
|
+
const prompts = loadPrompts();
|
|
132
|
+
|
|
133
|
+
const headerLines: string[] = [];
|
|
134
|
+
if (meta.channel) headerLines.push(`Channel: ${meta.channel}`);
|
|
135
|
+
if (meta.sender) headerLines.push(`Sender: ${meta.sender}`);
|
|
136
|
+
if (meta.platform) headerLines.push(`Platform: ${meta.platform}`);
|
|
137
|
+
if (meta.serverName) headerLines.push(`Server: ${meta.serverName}`);
|
|
138
|
+
if (meta.channelName) headerLines.push(`Channel name: ${meta.channelName}`);
|
|
124
139
|
if (meta.participants && meta.participants.length > 0)
|
|
125
|
-
|
|
126
|
-
|
|
140
|
+
headerLines.push(`Participants: ${meta.participants.join(", ")}`);
|
|
141
|
+
|
|
127
142
|
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
143
|
const suggestedSession = sanitizeChannelPath(meta.channel ?? "unknown");
|
|
144
|
+
const channel = meta.channel ?? "unknown";
|
|
135
145
|
const otherCount = (meta.participantCount ?? 1) - 1;
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
146
|
+
const batchRecommendation =
|
|
147
|
+
otherCount > 1
|
|
148
|
+
? ` Session config: "${suggestedSession}": { "batch": { "debounce": 20, "maxWait": 120 } }\n(batch recommended — ${otherCount} other participants may generate frequent messages)\n`
|
|
149
|
+
: "";
|
|
150
|
+
|
|
151
|
+
const vars: Record<string, string> = {
|
|
152
|
+
headers: headerLines.join("\n"),
|
|
153
|
+
sender: meta.sender ?? "unknown",
|
|
154
|
+
time,
|
|
155
|
+
preview,
|
|
156
|
+
filePath,
|
|
157
|
+
channel,
|
|
158
|
+
suggestedSession,
|
|
159
|
+
batchRecommendation,
|
|
160
|
+
};
|
|
161
|
+
return prompts.channel_invite.replace(/\$\{(\w+)\}/g, (match, name) =>
|
|
162
|
+
name in vars ? vars[name] : match,
|
|
163
|
+
);
|
|
150
164
|
}
|
|
151
165
|
|
|
152
166
|
export function createRouter(options: {
|
|
@@ -245,6 +259,45 @@ export function createRouter(options: {
|
|
|
245
259
|
}
|
|
246
260
|
}
|
|
247
261
|
|
|
262
|
+
/**
|
|
263
|
+
* Direct dispatch to a pre-routed session. The daemon delivery manager has already
|
|
264
|
+
* resolved the route and session — just format and send.
|
|
265
|
+
*/
|
|
266
|
+
function dispatch(
|
|
267
|
+
content: VoluteContentPart[],
|
|
268
|
+
session: string,
|
|
269
|
+
meta: ChannelMeta,
|
|
270
|
+
listener?: Listener,
|
|
271
|
+
): { messageId: string; unsubscribe: () => void } {
|
|
272
|
+
const text = content
|
|
273
|
+
.filter((p): p is { type: "text"; text: string } => p.type === "text")
|
|
274
|
+
.map((p) => p.text)
|
|
275
|
+
.join(" ");
|
|
276
|
+
logMessage("in", text, meta.channel);
|
|
277
|
+
|
|
278
|
+
const messageId = generateMessageId();
|
|
279
|
+
const noop = () => {};
|
|
280
|
+
const safeListener = listener ?? noop;
|
|
281
|
+
|
|
282
|
+
// Apply formatting
|
|
283
|
+
const formatted = applyPrefix(content, { ...meta, sessionName: session });
|
|
284
|
+
const withTyping = appendTypingSuffix(formatted, meta.typing);
|
|
285
|
+
|
|
286
|
+
// Resolve session config for instructions
|
|
287
|
+
const config = options.configPath ? loadRoutingConfig(options.configPath) : {};
|
|
288
|
+
const sessionConfig = resolveSessionConfig(config, session);
|
|
289
|
+
const withInstructions = prependInstructions(withTyping, sessionConfig.instructions);
|
|
290
|
+
|
|
291
|
+
const handler = options.mindHandler(session);
|
|
292
|
+
const interrupt = (meta as any).interrupt ?? sessionConfig.interrupt;
|
|
293
|
+
const unsubscribe = handler.handle(
|
|
294
|
+
withInstructions,
|
|
295
|
+
{ ...meta, sessionName: session, messageId, interrupt },
|
|
296
|
+
safeListener,
|
|
297
|
+
);
|
|
298
|
+
return { messageId, unsubscribe };
|
|
299
|
+
}
|
|
300
|
+
|
|
248
301
|
function route(
|
|
249
302
|
content: VoluteContentPart[],
|
|
250
303
|
meta: ChannelMeta,
|
|
@@ -304,6 +357,21 @@ export function createRouter(options: {
|
|
|
304
357
|
return { messageId, unsubscribe: noop };
|
|
305
358
|
}
|
|
306
359
|
|
|
360
|
+
// Mention-mode filtering: skip messages that don't mention this mind
|
|
361
|
+
if (resolved.destination === "mind" && resolved.mode === "mention") {
|
|
362
|
+
const mindName = process.env.VOLUTE_MIND;
|
|
363
|
+
if (mindName) {
|
|
364
|
+
const escaped = mindName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
365
|
+
const pattern = new RegExp(`\\b${escaped}\\b`, "i");
|
|
366
|
+
if (!pattern.test(text)) {
|
|
367
|
+
queueMicrotask(() => safeListener({ type: "done", messageId }));
|
|
368
|
+
return { messageId, unsubscribe: noop };
|
|
369
|
+
}
|
|
370
|
+
} else {
|
|
371
|
+
log("router", "VOLUTE_MIND not set — mention filtering disabled");
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
307
375
|
// File destination
|
|
308
376
|
if (resolved.destination === "file") {
|
|
309
377
|
if (options.fileHandler) {
|
|
@@ -383,6 +451,77 @@ export function createRouter(options: {
|
|
|
383
451
|
return { messageId, unsubscribe };
|
|
384
452
|
}
|
|
385
453
|
|
|
454
|
+
/**
|
|
455
|
+
* Handle a pre-batched payload from the daemon delivery manager.
|
|
456
|
+
* Formats messages grouped by channel into a single SDK message.
|
|
457
|
+
*/
|
|
458
|
+
function dispatchBatch(
|
|
459
|
+
batch: { channels: Record<string, any[]> },
|
|
460
|
+
session: string,
|
|
461
|
+
_meta: ChannelMeta,
|
|
462
|
+
): void {
|
|
463
|
+
const allMessages: { channel: string; payload: any }[] = [];
|
|
464
|
+
for (const [channel, messages] of Object.entries(batch.channels)) {
|
|
465
|
+
for (const msg of messages) {
|
|
466
|
+
allMessages.push({ channel, payload: msg });
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (allMessages.length === 0) return;
|
|
471
|
+
|
|
472
|
+
// Build channel summary
|
|
473
|
+
const channelCounts = new Map<string, number>();
|
|
474
|
+
for (const msg of allMessages) {
|
|
475
|
+
channelCounts.set(msg.channel, (channelCounts.get(msg.channel) ?? 0) + 1);
|
|
476
|
+
}
|
|
477
|
+
const channelLabels = [...channelCounts.entries()].map(([ch, n]) => `${n} from ${ch}`);
|
|
478
|
+
const summary = channelLabels.join(", ");
|
|
479
|
+
|
|
480
|
+
const header = `[Batch: ${allMessages.length} message${allMessages.length === 1 ? "" : "s"} — ${summary}]`;
|
|
481
|
+
const multiChannel = channelCounts.size > 1;
|
|
482
|
+
|
|
483
|
+
const body = allMessages
|
|
484
|
+
.map((m) => {
|
|
485
|
+
const sender = m.payload.sender ?? "unknown";
|
|
486
|
+
const text =
|
|
487
|
+
typeof m.payload.content === "string"
|
|
488
|
+
? m.payload.content
|
|
489
|
+
: Array.isArray(m.payload.content)
|
|
490
|
+
? (m.payload.content as { type: string; text?: string }[])
|
|
491
|
+
.filter((p) => p.type === "text" && p.text)
|
|
492
|
+
.map((p) => p.text)
|
|
493
|
+
.join("\n")
|
|
494
|
+
: JSON.stringify(m.payload.content);
|
|
495
|
+
const time = new Date().toLocaleTimeString("en-US", {
|
|
496
|
+
hour: "numeric",
|
|
497
|
+
minute: "2-digit",
|
|
498
|
+
});
|
|
499
|
+
const prefix = multiChannel
|
|
500
|
+
? `[${sender} in ${m.channel} — ${time}]`
|
|
501
|
+
: `[${sender} — ${time}]`;
|
|
502
|
+
return `${prefix}\n${text}`;
|
|
503
|
+
})
|
|
504
|
+
.join("\n\n");
|
|
505
|
+
|
|
506
|
+
const content: VoluteContentPart[] = [{ type: "text", text: `${header}\n\n${body}` }];
|
|
507
|
+
|
|
508
|
+
// Resolve session config for instructions
|
|
509
|
+
const config = options.configPath ? loadRoutingConfig(options.configPath) : {};
|
|
510
|
+
const sessionConfig = resolveSessionConfig(config, session);
|
|
511
|
+
const withInstructions = prependInstructions(content, sessionConfig.instructions);
|
|
512
|
+
|
|
513
|
+
const messageId = generateMessageId();
|
|
514
|
+
const handler = options.mindHandler(session);
|
|
515
|
+
const noop = () => {};
|
|
516
|
+
|
|
517
|
+
try {
|
|
518
|
+
handler.handle(withInstructions, { sessionName: session, messageId }, noop);
|
|
519
|
+
log("router", `dispatched batch for session ${session}: ${allMessages.length} messages`);
|
|
520
|
+
} catch (err) {
|
|
521
|
+
log("router", `error dispatching batch for session ${session}:`, err);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
386
525
|
function close() {
|
|
387
526
|
for (const [key, buffer] of batchBuffers) {
|
|
388
527
|
if (buffer.debounceTimer) clearTimeout(buffer.debounceTimer);
|
|
@@ -392,5 +531,5 @@ export function createRouter(options: {
|
|
|
392
531
|
batchBuffers.clear();
|
|
393
532
|
}
|
|
394
533
|
|
|
395
|
-
return { route, close };
|
|
534
|
+
return { route, dispatch, dispatchBatch, close };
|
|
396
535
|
}
|
|
@@ -15,6 +15,7 @@ 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 = {
|
|
@@ -41,6 +42,7 @@ export type ResolvedRoute =
|
|
|
41
42
|
destination: "mind";
|
|
42
43
|
session: string;
|
|
43
44
|
matched: boolean;
|
|
45
|
+
mode?: "all" | "mention";
|
|
44
46
|
}
|
|
45
47
|
| { destination: "file"; path: string; matched: boolean };
|
|
46
48
|
|
|
@@ -75,7 +77,7 @@ function globMatch(pattern: string, value: string): boolean {
|
|
|
75
77
|
}
|
|
76
78
|
|
|
77
79
|
const GLOB_MATCH_KEYS = new Set(["channel", "sender"]);
|
|
78
|
-
const NON_MATCH_KEYS = new Set(["session", "destination", "path"]);
|
|
80
|
+
const NON_MATCH_KEYS = new Set(["session", "destination", "path", "mode"]);
|
|
79
81
|
|
|
80
82
|
type MatchMeta = { channel?: string; sender?: string; isDM?: boolean; participantCount?: number };
|
|
81
83
|
|
|
@@ -135,6 +137,7 @@ export function resolveRoute(config: RoutingConfig, meta: MatchMeta): ResolvedRo
|
|
|
135
137
|
destination: "mind",
|
|
136
138
|
session: sanitizeSessionName(expandTemplate(rule.session ?? fallback, meta)),
|
|
137
139
|
matched: true,
|
|
140
|
+
mode: rule.mode,
|
|
138
141
|
};
|
|
139
142
|
}
|
|
140
143
|
}
|