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.
Files changed (116) hide show
  1. package/README.md +1 -1
  2. package/dist/archive-ZCFOSTKB.js +15 -0
  3. package/dist/{channel-SLURLIRV.js → channel-PUQKGSQM.js} +60 -7
  4. package/dist/{chunk-CE7WMOVW.js → chunk-2TJGRJ4O.js} +236 -103
  5. package/dist/{chunk-6BDNWYKG.js → chunk-32VR2EOH.js} +2 -2
  6. package/dist/chunk-4KPUF5JD.js +214 -0
  7. package/dist/{chunk-QJIIHU32.js → chunk-7NO7EV5Z.js} +2 -2
  8. package/dist/chunk-AW7P4EVV.js +159 -0
  9. package/dist/{chunk-2Y77MCFG.js → chunk-DYZGP3EW.js} +2 -2
  10. package/dist/{chunk-M77QBTEH.js → chunk-EBGCNDMM.js} +24 -14
  11. package/dist/{chunk-GSPWIM5E.js → chunk-EMQSAY3B.js} +77 -6
  12. package/dist/{chunk-37X7ECMF.js → chunk-FCDU5BFX.js} +1 -1
  13. package/dist/chunk-FGV2H4TX.js +803 -0
  14. package/dist/{chunk-ZCEYUUID.js → chunk-OGXOMR65.js} +2 -1
  15. package/dist/chunk-OTWLI7F4.js +375 -0
  16. package/dist/{chunk-3FC42ZBM.js → chunk-RHEGSQFJ.js} +4 -1
  17. package/dist/{chunk-MVSXRMJJ.js → chunk-SCUDS4US.js} +1 -1
  18. package/dist/{chunk-MIJIAGGG.js → chunk-UJ6GHNR7.js} +8 -6
  19. package/dist/{chunk-OYSZNX5I.js → chunk-VDWCHYTS.js} +1 -1
  20. package/dist/{chunk-77ISBIKI.js → chunk-VE4D3GOP.js} +2 -2
  21. package/dist/chunk-VQWDC6UK.js +142 -0
  22. package/dist/{chunk-OJQ47SCA.js → chunk-WC6ZHVRL.js} +1 -1
  23. package/dist/chunk-YUIHSKR6.js +72 -0
  24. package/dist/chunk-Z524RFCJ.js +36 -0
  25. package/dist/cli.js +44 -24
  26. package/dist/{connector-3ELFMI2R.js → connector-JBVNZ7VK.js} +6 -6
  27. package/dist/connectors/discord.js +2 -2
  28. package/dist/connectors/slack.js +2 -2
  29. package/dist/connectors/telegram.js +2 -2
  30. package/dist/{create-ZWHCRT5F.js → create-HP4OVVHF.js} +6 -4
  31. package/dist/{daemon-client-ODKDUYDE.js → daemon-client-ITWUCNFO.js} +2 -2
  32. package/dist/{daemon-restart-VRQMZLBK.js → daemon-restart-JMZM3QY4.js} +8 -8
  33. package/dist/daemon.js +1624 -940
  34. package/dist/db-5ZVC6MQF.js +10 -0
  35. package/dist/{delete-6G6WEX4F.js → delete-BSU7K3RY.js} +1 -1
  36. package/dist/delivery-manager-ISTJMZDW.js +16 -0
  37. package/dist/down-ZY35KMHR.js +14 -0
  38. package/dist/{env-6IDWGBUH.js → env-A3LMO777.js} +6 -6
  39. package/dist/export-GCDNQCF3.js +100 -0
  40. package/dist/{history-5F4WQW7S.js → history-WNK3DFUM.js} +10 -7
  41. package/dist/{import-EDGRLIGO.js → import-M63VIUJ5.js} +3 -3
  42. package/dist/log-PPPZDVEF.js +39 -0
  43. package/dist/{login-ORQDXLBM.js → login-HNH3EUQV.js} +2 -2
  44. package/dist/{logout-XC5AUO5I.js → logout-I5CB5UZS.js} +2 -2
  45. package/dist/{logs-GYOR3L2L.js → logs-SF2IMJN4.js} +6 -6
  46. package/dist/merge-33C237A4.js +46 -0
  47. package/dist/{mind-OJN6RBZW.js → mind-PQ5NCPSU.js} +14 -10
  48. package/dist/mind-manager-RVCFROAY.js +18 -0
  49. package/dist/{package-4GTJGUXI.js → package-MYE2ZJLV.js} +7 -3
  50. package/dist/{pages-6IV4VQTU.js → pages-AXCOSY3P.js} +2 -2
  51. package/dist/{publish-Q4RPSJLL.js → publish-YB377JB7.js} +18 -4
  52. package/dist/pull-XAEWQJ47.js +39 -0
  53. package/dist/{register-LDE6LRXY.js → register-VSPCMHKX.js} +2 -2
  54. package/dist/{restart-YFAWFS5T.js → restart-IQKMCK5M.js} +6 -6
  55. package/dist/{schedule-AGYLDMNS.js → schedule-LMX7GAQZ.js} +6 -6
  56. package/dist/schema-5BW7DFZI.js +24 -0
  57. package/dist/{seed-AP4Q7RZ7.js → seed-J43YDKXG.js} +7 -4
  58. package/dist/{send-4GKDO26C.js → send-KVIZIGCE.js} +8 -8
  59. package/dist/{service-U7MZ2H7F.js → service-LUR7WDO7.js} +6 -6
  60. package/dist/{setup-DJKIZKGW.js → setup-OH3PJUJO.js} +7 -7
  61. package/dist/shared-KO35ZM44.js +39 -0
  62. package/dist/skill-BCVNI6TV.js +287 -0
  63. package/{templates/_base/_skills → dist/skills}/orientation/SKILL.md +1 -1
  64. package/{templates/_base/_skills → dist/skills}/sessions/SKILL.md +2 -2
  65. package/{templates/_base/_skills → dist/skills}/volute-mind/SKILL.md +35 -1
  66. package/dist/{sprout-TJ3BHVOG.js → sprout-VBEX63LX.js} +38 -20
  67. package/dist/{start-3YYRXBKP.js → start-I5JYB65M.js} +6 -6
  68. package/dist/{status-VSFZYX7S.js → status-4ESFLGH4.js} +5 -5
  69. package/dist/status-D7E5HHBV.js +35 -0
  70. package/dist/{status-OKNA6AR3.js → status-JCJAOXTW.js} +2 -2
  71. package/dist/{stop-AA5K5LYG.js → stop-NBVKEFQQ.js} +6 -6
  72. package/dist/{up-LT3X5Q26.js → up-WG65SWJU.js} +5 -5
  73. package/dist/{update-YAGN5ODG.js → update-FJIHDJKM.js} +5 -5
  74. package/dist/{update-check-APLTH4IN.js → update-check-MWE5AH4U.js} +2 -2
  75. package/dist/{upgrade-KXZCQSZN.js → upgrade-AIT24B5I.js} +1 -1
  76. package/dist/{variant-X5QFG6KK.js → variant-63ZWO2W7.js} +4 -4
  77. package/dist/variants-JAGWGBXG.js +26 -0
  78. package/dist/web-assets/assets/index-BAbuRsVF.css +1 -0
  79. package/dist/web-assets/assets/index-CiQhSKi_.js +63 -0
  80. package/dist/web-assets/index.html +2 -2
  81. package/drizzle/0007_system_prompts.sql +5 -0
  82. package/drizzle/0008_volute_channels.sql +24 -0
  83. package/drizzle/0009_shared_skills.sql +9 -0
  84. package/drizzle/0010_delivery_queue.sql +12 -0
  85. package/drizzle/0011_rename_human_to_brain.sql +1 -0
  86. package/drizzle/meta/0007_snapshot.json +7 -0
  87. package/drizzle/meta/0008_snapshot.json +7 -0
  88. package/drizzle/meta/0009_snapshot.json +7 -0
  89. package/drizzle/meta/0010_snapshot.json +7 -0
  90. package/drizzle/meta/0011_snapshot.json +7 -0
  91. package/drizzle/meta/_journal.json +35 -0
  92. package/package.json +7 -3
  93. package/templates/_base/.init/.config/hooks/startup-context.sh +1 -1
  94. package/templates/_base/.init/.config/prompts.json +5 -0
  95. package/templates/_base/.init/.config/scripts/session-reader.ts +3 -3
  96. package/templates/_base/home/VOLUTE.md +16 -1
  97. package/templates/_base/src/lib/auto-commit.ts +51 -14
  98. package/templates/_base/src/lib/router.ts +168 -29
  99. package/templates/_base/src/lib/routing.ts +4 -1
  100. package/templates/_base/src/lib/startup.ts +43 -0
  101. package/templates/_base/src/lib/types.ts +4 -0
  102. package/templates/_base/src/lib/volute-server.ts +91 -2
  103. package/templates/claude/src/agent.ts +4 -3
  104. package/templates/claude/src/lib/hooks/reply-instructions.ts +3 -1
  105. package/templates/claude/src/server.ts +2 -2
  106. package/templates/claude/volute-template.json +1 -2
  107. package/templates/pi/src/agent.ts +6 -7
  108. package/templates/pi/src/lib/reply-instructions-extension.ts +3 -1
  109. package/templates/pi/src/lib/session-context-extension.ts +2 -2
  110. package/templates/pi/volute-template.json +1 -2
  111. package/dist/chunk-PO5Q2AYN.js +0 -121
  112. package/dist/down-A56B5JLK.js +0 -14
  113. package/dist/mind-manager-ETNCPQJN.js +0 -15
  114. package/dist/web-assets/assets/index-BcmT7Qxo.js +0 -63
  115. package/dist/web-assets/assets/index-DG01TyLb.css +0 -1
  116. /package/{templates/_base/_skills → dist/skills}/memory/SKILL.md +0 -0
@@ -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");
@@ -13,6 +13,10 @@ export type ChannelMeta = {
13
13
  participants?: string[];
14
14
  participantCount?: number;
15
15
  typing?: string[];
16
+ signature?: string;
17
+ signatureTimestamp?: string;
18
+ signerFingerprint?: string;
19
+ verified?: boolean;
16
20
  };
17
21
 
18
22
  /** ChannelMeta enriched by the router with dispatch info. */
@@ -1,7 +1,8 @@
1
+ import { createHash, verify } from "node:crypto";
1
2
  import { createServer, type IncomingMessage, type Server } from "node:http";
2
3
  import { log } from "./logger.js";
3
4
  import type { Router } from "./router.js";
4
- import type { VoluteRequest } from "./types.js";
5
+ import type { VoluteContentPart, VoluteRequest } from "./types.js";
5
6
 
6
7
  function readBody(req: IncomingMessage): Promise<string> {
7
8
  return new Promise((resolve, reject) => {
@@ -12,6 +13,71 @@ function readBody(req: IncomingMessage): Promise<string> {
12
13
  });
13
14
  }
14
15
 
16
+ function extractText(content: VoluteContentPart[] | string): string {
17
+ if (typeof content === "string") return content;
18
+ return content
19
+ .filter((p): p is { type: "text"; text: string } => p.type === "text")
20
+ .map((p) => p.text)
21
+ .join("\n");
22
+ }
23
+
24
+ /** Normalize content to VoluteContentPart[] — connectors may send plain strings. */
25
+ function normalizeContent(content: unknown): VoluteContentPart[] {
26
+ if (Array.isArray(content)) return content as VoluteContentPart[];
27
+ if (typeof content === "string") return [{ type: "text", text: content }];
28
+ return [{ type: "text", text: JSON.stringify(content) }];
29
+ }
30
+
31
+ /** Verify an Ed25519 signature against a public key */
32
+ function verifySignature(
33
+ publicKeyPem: string,
34
+ content: string,
35
+ timestamp: string,
36
+ signature: string,
37
+ ): boolean {
38
+ try {
39
+ const data = `${content}\n${timestamp}`;
40
+ return verify(null, Buffer.from(data), publicKeyPem, Buffer.from(signature, "base64"));
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
45
+
46
+ /** Look up a mind's public key via the daemon API */
47
+ async function fetchPublicKey(fingerprint: string): Promise<string | null> {
48
+ const daemonPort = process.env.VOLUTE_DAEMON_PORT;
49
+ const daemonToken = process.env.VOLUTE_DAEMON_TOKEN;
50
+ if (!daemonPort || !daemonToken) return null;
51
+
52
+ try {
53
+ const res = await fetch(
54
+ `http://127.0.0.1:${daemonPort}/api/keys/${encodeURIComponent(fingerprint)}`,
55
+ { headers: { Authorization: `Bearer ${daemonToken}` }, signal: AbortSignal.timeout(2000) },
56
+ );
57
+ if (!res.ok) return null;
58
+ const data = (await res.json()) as { publicKey?: string };
59
+ return data.publicKey ?? null;
60
+ } catch (err) {
61
+ log("identity", "failed to fetch public key:", err);
62
+ return null;
63
+ }
64
+ }
65
+
66
+ /** Best-effort signature verification */
67
+ async function verifyRequest(body: VoluteRequest): Promise<boolean | undefined> {
68
+ if (!body.signature || !body.signatureTimestamp || !body.signerFingerprint) return undefined;
69
+
70
+ const publicKey = await fetchPublicKey(body.signerFingerprint);
71
+ if (!publicKey) return false;
72
+
73
+ // Verify the fingerprint matches
74
+ const expectedFingerprint = createHash("sha256").update(publicKey).digest("hex");
75
+ if (expectedFingerprint !== body.signerFingerprint) return false;
76
+
77
+ const text = extractText(body.content);
78
+ return verifySignature(publicKey, text, body.signatureTimestamp, body.signature);
79
+ }
80
+
15
81
  export function createVoluteServer(options: {
16
82
  router: Router;
17
83
  port: number;
@@ -32,7 +98,30 @@ export function createVoluteServer(options: {
32
98
  if (req.method === "POST" && url.pathname === "/message") {
33
99
  try {
34
100
  const body = JSON.parse(await readBody(req)) as VoluteRequest;
35
- router.route(body.content, body);
101
+
102
+ // Strip any sender-provided verified field to prevent spoofing
103
+ delete body.verified;
104
+
105
+ // Best-effort signature verification (non-blocking)
106
+ const verified = await verifyRequest(body);
107
+ if (verified !== undefined) body.verified = verified;
108
+
109
+ // Normalize content — connectors may send plain strings
110
+ body.content = normalizeContent(body.content);
111
+
112
+ // Handle batch payloads from delivery manager
113
+ if ((body as any).batch) {
114
+ const batch = (body as any).batch as {
115
+ channels: Record<string, any[]>;
116
+ };
117
+ router.dispatchBatch(batch, body.session ?? "main", body);
118
+ } else if (body.session) {
119
+ // Pre-routed by daemon delivery manager — dispatch directly
120
+ router.dispatch(body.content, body.session, body);
121
+ } else {
122
+ // Legacy: local routing (for minds running with old daemon)
123
+ router.route(body.content, body);
124
+ }
36
125
  res.writeHead(200, { "Content-Type": "application/json" });
37
126
  res.end(JSON.stringify({ ok: true }));
38
127
  } catch (err) {
@@ -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
  };
@@ -20,10 +20,10 @@ if (config.model) log("server", `using model: ${config.model}`);
20
20
  if (config.maxThinkingTokens) log("server", `max thinking tokens: ${config.maxThinkingTokens}`);
21
21
 
22
22
  const systemPrompt = loadSystemPrompt();
23
- const sessionsDir = resolve(".volute/sessions");
23
+ const sessionsDir = resolve(".mind/sessions");
24
24
 
25
25
  // Migrate old single session.json → sessions/main.json
26
- const oldSessionPath = resolve(".volute/session.json");
26
+ const oldSessionPath = resolve(".mind/session.json");
27
27
  if (existsSync(oldSessionPath) && !existsSync(resolve(sessionsDir, "main.json"))) {
28
28
  mkdirSync(sessionsDir, { recursive: true });
29
29
  renameSync(oldSessionPath, resolve(sessionsDir, "main.json"));
@@ -4,6 +4,5 @@
4
4
  "biome.json.tmpl": "biome.json",
5
5
  "home/.config/config.json.tmpl": "home/.config/config.json"
6
6
  },
7
- "substitute": ["package.json", ".init/SOUL.md", "home/.config/routes.json"],
8
- "skillsDir": "home/.claude/skills"
7
+ "substitute": ["package.json", ".init/SOUL.md", "home/.config/routes.json"]
9
8
  }
@@ -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";
@@ -84,7 +83,7 @@ export function createMind(options: {
84
83
 
85
84
  const sessionManager = isEphemeral
86
85
  ? SessionManager.inMemory()
87
- : SessionManager.continueRecent(options.cwd, `.volute/pi-sessions/${session.name}`);
86
+ : SessionManager.continueRecent(options.cwd, `.mind/pi-sessions/${session.name}`);
88
87
 
89
88
  log("mind", `session "${session.name}": ${isEphemeral ? "ephemeral" : "persistent"}`);
90
89
 
@@ -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
  };
@@ -9,11 +9,11 @@ export function createSessionContextExtension(options: {
9
9
  return (pi) => {
10
10
  pi.on("before_agent_start", () => {
11
11
  try {
12
- const sessionsDir = resolve(options.cwd, ".volute/pi-sessions");
12
+ const sessionsDir = resolve(options.cwd, ".mind/pi-sessions");
13
13
  const summary = getSessionUpdates({
14
14
  currentSession: options.currentSession,
15
15
  sessionsDir,
16
- cursorFile: resolve(options.cwd, ".volute/session-cursors.json"),
16
+ cursorFile: resolve(options.cwd, ".mind/session-cursors.json"),
17
17
  jsonlResolver: (name) => resolvePiJsonl(sessionsDir, name),
18
18
  format: "pi",
19
19
  });
@@ -4,6 +4,5 @@
4
4
  "biome.json.tmpl": "biome.json",
5
5
  "home/.config/config.json.tmpl": "home/.config/config.json"
6
6
  },
7
- "substitute": ["package.json", ".init/SOUL.md", "home/.config/routes.json"],
8
- "skillsDir": "home/.claude/skills"
7
+ "substitute": ["package.json", ".init/SOUL.md", "home/.config/routes.json"]
9
8
  }
@@ -1,121 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/lib/template.ts
4
- import {
5
- cpSync,
6
- existsSync,
7
- mkdirSync,
8
- readdirSync,
9
- readFileSync,
10
- renameSync,
11
- rmSync,
12
- statSync,
13
- writeFileSync
14
- } from "fs";
15
- import { tmpdir } from "os";
16
- import { dirname, join, relative, resolve } from "path";
17
- function findTemplatesRoot() {
18
- let dir = dirname(new URL(import.meta.url).pathname);
19
- for (let i = 0; i < 5; i++) {
20
- const candidate = resolve(dir, "templates");
21
- if (existsSync(resolve(candidate, "_base"))) return candidate;
22
- dir = dirname(dir);
23
- }
24
- console.error(
25
- "Templates directory not found. Searched up from:",
26
- dirname(new URL(import.meta.url).pathname)
27
- );
28
- process.exit(1);
29
- }
30
- function composeTemplate(templatesRoot, templateName) {
31
- const baseDir = resolve(templatesRoot, "_base");
32
- const templateDir = resolve(templatesRoot, templateName);
33
- if (!existsSync(baseDir)) {
34
- console.error("Base template not found:", baseDir);
35
- process.exit(1);
36
- }
37
- if (!existsSync(templateDir)) {
38
- console.error(`Template not found: ${templateName}`);
39
- process.exit(1);
40
- }
41
- const composedDir = resolve(tmpdir(), `volute-template-${Date.now()}`);
42
- mkdirSync(composedDir, { recursive: true });
43
- cpSync(baseDir, composedDir, { recursive: true });
44
- for (const file of listFiles(templateDir)) {
45
- const src = resolve(templateDir, file);
46
- const dest = resolve(composedDir, file);
47
- mkdirSync(dirname(dest), { recursive: true });
48
- cpSync(src, dest);
49
- }
50
- const manifestPath = resolve(composedDir, "volute-template.json");
51
- if (!existsSync(manifestPath)) {
52
- rmSync(composedDir, { recursive: true, force: true });
53
- console.error(`Template manifest not found: ${templateName}/volute-template.json`);
54
- process.exit(1);
55
- }
56
- const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
57
- const skillsSrc = resolve(composedDir, "_skills");
58
- if (existsSync(skillsSrc)) {
59
- const skillsDest = resolve(composedDir, manifest.skillsDir);
60
- mkdirSync(skillsDest, { recursive: true });
61
- cpSync(skillsSrc, skillsDest, { recursive: true });
62
- rmSync(skillsSrc, { recursive: true, force: true });
63
- }
64
- rmSync(manifestPath);
65
- return { composedDir, manifest };
66
- }
67
- function copyTemplateToDir(composedDir, destDir, mindName, manifest) {
68
- cpSync(composedDir, destDir, { recursive: true });
69
- for (const [from, to] of Object.entries(manifest.rename)) {
70
- const fromPath = resolve(destDir, from);
71
- if (existsSync(fromPath)) {
72
- renameSync(fromPath, resolve(destDir, to));
73
- }
74
- }
75
- for (const file of manifest.substitute) {
76
- const path = resolve(destDir, file);
77
- if (existsSync(path)) {
78
- const content = readFileSync(path, "utf-8");
79
- writeFileSync(path, content.replaceAll("{{name}}", mindName));
80
- }
81
- }
82
- }
83
- function applyInitFiles(destDir) {
84
- const initDir = resolve(destDir, ".init");
85
- if (!existsSync(initDir)) return;
86
- const homeDir = resolve(destDir, "home");
87
- for (const file of listFiles(initDir)) {
88
- const src = resolve(initDir, file);
89
- const dest = resolve(homeDir, file);
90
- const parent = dirname(dest);
91
- if (!existsSync(parent)) {
92
- mkdirSync(parent, { recursive: true });
93
- }
94
- cpSync(src, dest);
95
- }
96
- rmSync(initDir, { recursive: true, force: true });
97
- }
98
- function listFiles(dir) {
99
- const results = [];
100
- function walk(current) {
101
- for (const entry of readdirSync(current)) {
102
- const full = join(current, entry);
103
- if (statSync(full).isDirectory()) {
104
- if (entry === ".git") continue;
105
- walk(full);
106
- } else {
107
- results.push(relative(dir, full));
108
- }
109
- }
110
- }
111
- walk(dir);
112
- return results;
113
- }
114
-
115
- export {
116
- findTemplatesRoot,
117
- composeTemplate,
118
- copyTemplateToDir,
119
- applyInitFiles,
120
- listFiles
121
- };
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- run,
4
- stopDaemon
5
- } from "./chunk-QJIIHU32.js";
6
- import "./chunk-6BDNWYKG.js";
7
- import "./chunk-2Y77MCFG.js";
8
- import "./chunk-ZCEYUUID.js";
9
- import "./chunk-M77QBTEH.js";
10
- import "./chunk-K3NQKI34.js";
11
- export {
12
- run,
13
- stopDaemon
14
- };
@@ -1,15 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- MindManager,
4
- getMindManager,
5
- initMindManager
6
- } from "./chunk-CE7WMOVW.js";
7
- import "./chunk-OYSZNX5I.js";
8
- import "./chunk-ZCEYUUID.js";
9
- import "./chunk-M77QBTEH.js";
10
- import "./chunk-K3NQKI34.js";
11
- export {
12
- MindManager,
13
- getMindManager,
14
- initMindManager
15
- };