volute 0.17.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.
Files changed (38) hide show
  1. package/dist/{chunk-CE7WMOVW.js → chunk-AYB7XAWO.js} +323 -25
  2. package/dist/{chunk-MIJIAGGG.js → chunk-FW5API7X.js} +7 -5
  3. package/dist/{chunk-3FC42ZBM.js → chunk-GK4E7LM7.js} +3 -0
  4. package/dist/cli.js +18 -6
  5. package/dist/connectors/discord.js +1 -1
  6. package/dist/connectors/slack.js +1 -1
  7. package/dist/connectors/telegram.js +1 -1
  8. package/dist/{daemon-restart-VRQMZLBK.js → daemon-restart-2HVTHZAT.js} +1 -1
  9. package/dist/daemon.js +1080 -432
  10. package/dist/{history-5F4WQW7S.js → history-YUEKTJ2N.js} +4 -1
  11. package/dist/{mind-manager-ETNCPQJN.js → mind-manager-Z7O7PN2O.js} +1 -1
  12. package/dist/{package-4GTJGUXI.js → package-OKLFO7UY.js} +3 -1
  13. package/dist/{send-4GKDO26C.js → send-BNDTLUPM.js} +2 -2
  14. package/dist/skill-2Y42P4JY.js +287 -0
  15. package/dist/{up-LT3X5Q26.js → up-7B3BWF2U.js} +1 -1
  16. package/dist/web-assets/assets/index-CtiimdWK.css +1 -0
  17. package/dist/web-assets/assets/index-kt1_EcuO.js +63 -0
  18. package/dist/web-assets/index.html +2 -2
  19. package/drizzle/0007_system_prompts.sql +5 -0
  20. package/drizzle/0008_volute_channels.sql +24 -0
  21. package/drizzle/0009_shared_skills.sql +9 -0
  22. package/drizzle/meta/0007_snapshot.json +7 -0
  23. package/drizzle/meta/0008_snapshot.json +7 -0
  24. package/drizzle/meta/0009_snapshot.json +7 -0
  25. package/drizzle/meta/_journal.json +21 -0
  26. package/package.json +3 -1
  27. package/templates/_base/.init/.config/prompts.json +5 -0
  28. package/templates/_base/_skills/volute-mind/SKILL.md +17 -1
  29. package/templates/_base/src/lib/router.ts +45 -28
  30. package/templates/_base/src/lib/routing.ts +4 -1
  31. package/templates/_base/src/lib/startup.ts +43 -0
  32. package/templates/claude/src/agent.ts +4 -3
  33. package/templates/claude/src/lib/hooks/reply-instructions.ts +3 -1
  34. package/templates/pi/src/agent.ts +5 -6
  35. package/templates/pi/src/lib/reply-instructions-extension.ts +3 -1
  36. package/dist/web-assets/assets/index-BcmT7Qxo.js +0 -63
  37. package/dist/web-assets/assets/index-DG01TyLb.css +0 -1
  38. /package/dist/{chunk-77ISBIKI.js → chunk-6DVBMLVN.js} +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-BcmT7Qxo.js"></script>
11
- <link rel="stylesheet" crossorigin href="/assets/index-DG01TyLb.css">
10
+ <script type="module" crossorigin src="/assets/index-kt1_EcuO.js"></script>
11
+ <link rel="stylesheet" crossorigin href="/assets/index-CtiimdWK.css">
12
12
  </head>
13
13
  <body>
14
14
  <div id="root"></div>
@@ -0,0 +1,5 @@
1
+ CREATE TABLE `system_prompts` (
2
+ `key` text PRIMARY KEY NOT NULL,
3
+ `content` text NOT NULL,
4
+ `updated_at` text DEFAULT (datetime('now')) NOT NULL
5
+ );
@@ -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,7 @@
1
+ {
2
+ "id": "0007_system_prompts",
3
+ "prevId": "0006_mind_history",
4
+ "version": "6",
5
+ "dialect": "sqlite",
6
+ "tables": {}
7
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "id": "0008_volute_channels",
3
+ "prevId": "0007_system_prompts",
4
+ "version": "6",
5
+ "dialect": "sqlite",
6
+ "tables": {}
7
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "id": "0009_shared_skills",
3
+ "prevId": "0008_volute_channels",
4
+ "version": "6",
5
+ "dialect": "sqlite",
6
+ "tables": {}
7
+ }
@@ -50,6 +50,27 @@
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
53
74
  }
54
75
  ]
55
76
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "volute",
3
- "version": "0.17.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",
@@ -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",
@@ -63,6 +64,7 @@
63
64
  "@mariozechner/pi-ai": "^0.52.7",
64
65
  "@mariozechner/pi-coding-agent": "^0.52.7",
65
66
  "@sveltejs/vite-plugin-svelte": "^6.2.4",
67
+ "@types/adm-zip": "^0.5.7",
66
68
  "@types/bcryptjs": "^2.4.6",
67
69
  "@types/dompurify": "^3.0.5",
68
70
  "@types/node": "^25.2.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 message flow. 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".
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
@@ -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.
@@ -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 lines = ["[Channel Invite]"];
119
- if (meta.channel) lines.push(`Channel: ${meta.channel}`);
120
- if (meta.sender) lines.push(`Sender: ${meta.sender}`);
121
- if (meta.platform) lines.push(`Platform: ${meta.platform}`);
122
- if (meta.serverName) lines.push(`Server: ${meta.serverName}`);
123
- if (meta.channelName) lines.push(`Channel name: ${meta.channelName}`);
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
- lines.push(`Participants: ${meta.participants.join(", ")}`);
126
- lines.push("");
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
- if (otherCount > 1) {
137
- lines.push(` Rule: { "channel": "${meta.channel}", "session": "${suggestedSession}" }`);
138
- lines.push(
139
- ` Session config: "${suggestedSession}": { "batch": { "debounce": 20, "maxWait": 120 } }`,
140
- );
141
- lines.push(
142
- `(batch recommended — ${otherCount} other participants may generate frequent messages)`,
143
- );
144
- } else {
145
- lines.push(` Rule: { "channel": "${meta.channel}", "session": "${suggestedSession}" }`);
146
- }
147
- lines.push(`To respond, use: volute send ${meta.channel ?? "unknown"} "your message"`);
148
- lines.push(`To reject, delete ${filePath}`);
149
- return lines.join("\n");
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: {
@@ -304,6 +306,21 @@ export function createRouter(options: {
304
306
  return { messageId, unsubscribe: noop };
305
307
  }
306
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
+
307
324
  // File destination
308
325
  if (resolved.destination === "file") {
309
326
  if (options.fileHandler) {
@@ -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
  }
@@ -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");
@@ -9,6 +9,7 @@ import { createSessionContextHook } from "./lib/hooks/session-context.js";
9
9
  import { log } from "./lib/logger.js";
10
10
  import { createMessageChannel } from "./lib/message-channel.js";
11
11
  import { createSessionStore } from "./lib/session-store.js";
12
+ import { loadPrompts } from "./lib/startup.js";
12
13
  import { consumeStream } from "./lib/stream-consumer.js";
13
14
  import type {
14
15
  HandlerMeta,
@@ -47,10 +48,10 @@ export function createMind(options: {
47
48
  ];
48
49
 
49
50
  const sessions = new Map<string, Session>();
50
- const today = new Date().toISOString().slice(0, 10);
51
+ const prompts = loadPrompts();
52
+ const today = new Date().toLocaleDateString("en-CA");
51
53
  const compactionMessage =
52
- options.compactionMessage ??
53
- `Context is getting long — compaction is about to summarize this conversation. Before that happens, save anything important to files (MEMORY.md, memory/journal/${today}.md, etc.) since those survive compaction. Focus on: decisions made, open tasks, and anything you'd need to pick up where you left off.`;
54
+ options.compactionMessage ?? prompts.compaction_warning.replace("${date}", today);
54
55
 
55
56
  // --- Event broadcasting ---
56
57
 
@@ -1,7 +1,9 @@
1
1
  import type { HookCallback } from "@anthropic-ai/claude-agent-sdk";
2
+ import { loadPrompts } from "../startup.js";
2
3
 
3
4
  export function createReplyInstructionsHook(messageChannels: Map<string, string>) {
4
5
  let fired = false;
6
+ const prompts = loadPrompts();
5
7
 
6
8
  const hook: HookCallback = async () => {
7
9
  if (fired) return {};
@@ -14,7 +16,7 @@ export function createReplyInstructionsHook(messageChannels: Map<string, string>
14
16
  return {
15
17
  hookSpecificOutput: {
16
18
  hookEventName: "UserPromptSubmit" as const,
17
- additionalContext: `To reply to this message, use: volute send ${channel} "your message"`,
19
+ additionalContext: prompts.reply_instructions.replace(/\$\{channel\}/g, channel),
18
20
  },
19
21
  };
20
22
  };
@@ -13,6 +13,7 @@ import { log } from "./lib/logger.js";
13
13
  import { createReplyInstructionsExtension } from "./lib/reply-instructions-extension.js";
14
14
  import { resolveModel } from "./lib/resolve-model.js";
15
15
  import { createSessionContextExtension } from "./lib/session-context-extension.js";
16
+ import { loadPrompts } from "./lib/startup.js";
16
17
  import type {
17
18
  HandlerMeta,
18
19
  HandlerResolver,
@@ -35,11 +36,6 @@ type PiSession = {
35
36
  messageChannels: Map<string, string>;
36
37
  };
37
38
 
38
- function defaultCompactionMessage(): string {
39
- const today = new Date().toISOString().slice(0, 10);
40
- return `Context is getting long — compaction is about to summarize this conversation. Before that happens, save anything important to files (MEMORY.md, memory/journal/${today}.md, etc.) since those survive compaction. Focus on: decisions made, open tasks, and anything you'd need to pick up where you left off.`;
41
- }
42
-
43
39
  export function createMind(options: {
44
40
  systemPrompt: string;
45
41
  cwd: string;
@@ -48,7 +44,10 @@ export function createMind(options: {
48
44
  compactionMessage?: string;
49
45
  }): { resolve: HandlerResolver } {
50
46
  const sessions = new Map<string, PiSession>();
51
- const compactionMessage = options.compactionMessage ?? defaultCompactionMessage();
47
+ const prompts = loadPrompts();
48
+ const today = new Date().toLocaleDateString("en-CA");
49
+ const compactionMessage =
50
+ options.compactionMessage ?? prompts.compaction_warning.replace("${date}", today);
52
51
 
53
52
  // Shared setup (created once)
54
53
  const modelStr = options.model || process.env.PI_MODEL || "anthropic:claude-sonnet-4-20250514";
@@ -1,8 +1,10 @@
1
1
  import type { ExtensionFactory } from "@mariozechner/pi-coding-agent";
2
+ import { loadPrompts } from "./startup.js";
2
3
 
3
4
  export function createReplyInstructionsExtension(
4
5
  messageChannels: Map<string, string>,
5
6
  ): ExtensionFactory {
7
+ const prompts = loadPrompts();
6
8
  return (pi) => {
7
9
  let fired = false;
8
10
  pi.on("before_agent_start", () => {
@@ -16,7 +18,7 @@ export function createReplyInstructionsExtension(
16
18
  return {
17
19
  message: {
18
20
  customType: "reply-instructions",
19
- content: `To reply to this message, use: volute send ${channel} "your message"`,
21
+ content: prompts.reply_instructions.replace(/\$\{channel\}/g, channel),
20
22
  display: true,
21
23
  },
22
24
  };