volute 0.25.0 → 0.27.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 (145) hide show
  1. package/README.md +28 -33
  2. package/dist/{activity-events-4O37J7PD.js → activity-events-BBIEA2F4.js} +2 -3
  3. package/dist/api.d.ts +886 -220
  4. package/dist/{archive-4ZQYK5MN.js → archive-UA4BDFXQ.js} +2 -2
  5. package/dist/{auth-HM2RSPY7.js → auth-D3OT2ARB.js} +3 -3
  6. package/dist/bridge-FQHZL3MC.js +206 -0
  7. package/dist/chat-MHJ3L6JQ.js +58 -0
  8. package/dist/{chunk-PHU4DEAJ.js → chunk-2WPW7OT6.js} +3 -3
  9. package/dist/{chunk-BOTQ25QT.js → chunk-2YP2TVDT.js} +138 -56
  10. package/dist/{chunk-DG7TO7EE.js → chunk-4WXYUOAK.js} +5 -7
  11. package/dist/{chunk-JTDFJWI2.js → chunk-AW7PFDVN.js} +5 -5
  12. package/dist/{chunk-2767L2RZ.js → chunk-EHYDTZTF.js} +6 -6
  13. package/dist/{chunk-ZSH4G2P5.js → chunk-GIE6CSN5.js} +17 -17
  14. package/dist/chunk-H7OZRFJB.js +432 -0
  15. package/dist/{chunk-ON3FF5JA.js → chunk-HDN7MNGD.js} +3 -3
  16. package/dist/chunk-IAYBDWVG.js +477 -0
  17. package/dist/chunk-IKRVFPWU.js +83 -0
  18. package/dist/{chunk-TRQEV3CD.js → chunk-JGFVMROS.js} +32 -6
  19. package/dist/{chunk-PHHKNGA3.js → chunk-JKOWNZ4P.js} +3 -3
  20. package/dist/{chunk-E7GOKNOT.js → chunk-K5NAC55T.js} +1 -1
  21. package/dist/{chunk-HFCBO2GL.js → chunk-KDGS53OS.js} +4 -4
  22. package/dist/chunk-KTLFDYPT.js +61 -0
  23. package/dist/{chunk-3AIBT4TW.js → chunk-LAC664WU.js} +30 -4
  24. package/dist/{chunk-PMX4EIJK.js → chunk-OQZH4PBB.js} +467 -1054
  25. package/dist/{chunk-SHSWYG2J.js → chunk-PHSAT7YL.js} +71 -58
  26. package/dist/chunk-RKQEHRBB.js +177 -0
  27. package/dist/{chunk-RVKR2R7F.js → chunk-SSI47XP2.js} +10 -2
  28. package/dist/chunk-T6HKBWXZ.js +23 -0
  29. package/dist/chunk-USUXRNVD.js +113 -0
  30. package/dist/{chunk-BFK6SOEJ.js → chunk-VIVMW2H2.js} +4 -4
  31. package/dist/{chunk-KTJGZ7M7.js → chunk-XBLSAVJF.js} +1 -1
  32. package/dist/chunk-ZYGKG6VC.js +22 -0
  33. package/dist/cli.js +51 -32
  34. package/dist/{cloud-sync-PPBBJDY6.js → cloud-sync-T7M3ESC3.js} +15 -12
  35. package/dist/connectors/discord-bridge.js +158 -0
  36. package/dist/connectors/slack-bridge.js +119 -0
  37. package/dist/connectors/telegram-bridge.js +133 -0
  38. package/dist/conversations-M2K4253F.js +55 -0
  39. package/dist/create-D7J73A6H.js +45 -0
  40. package/dist/{create-VDQJER52.js → create-QWV73WXD.js} +1 -1
  41. package/dist/{daemon-client-JOVQZ52X.js → daemon-client-I42FK2BF.js} +2 -2
  42. package/dist/{daemon-restart-FDNOZEAD.js → daemon-restart-M2QTYMEG.js} +7 -6
  43. package/dist/daemon.js +2247 -1085
  44. package/dist/db-IC4J52XQ.js +8 -0
  45. package/dist/{delete-2MRR4JX5.js → delete-4JYGD4VN.js} +1 -1
  46. package/dist/down-LVBXEULC.js +14 -0
  47. package/dist/{env-2FPOZK37.js → env-YJMUMFIY.js} +5 -5
  48. package/dist/{export-IKFAPRAO.js → export-BOJQWBMA.js} +4 -4
  49. package/dist/{file-KT3UIQM3.js → file-CR36YUPD.js} +4 -4
  50. package/dist/{history-46WZN5CN.js → history-XKRTAFS2.js} +7 -7
  51. package/dist/{import-TH26J76F.js → import-SRTQXBGH.js} +4 -4
  52. package/dist/join-J4QU42DL.js +66 -0
  53. package/dist/list-R73GENNL.js +40 -0
  54. package/dist/{log-6SGSSR3D.js → log-ABYNVYJ3.js} +4 -4
  55. package/dist/login-3QZNR2DF.js +46 -0
  56. package/dist/{login-UO6AOVEA.js → login-XX37I52P.js} +3 -3
  57. package/dist/logout-T53VKCPU.js +39 -0
  58. package/dist/{logout-UKD5LA37.js → logout-W4KOOBIT.js} +2 -2
  59. package/dist/{logs-HRBONI5I.js → logs-U35JR2KE.js} +7 -7
  60. package/dist/{merge-KSFJKX6T.js → merge-LNSMSAOF.js} +4 -4
  61. package/dist/message-delivery-LDXLGERA.js +25 -0
  62. package/dist/migrate-registry-to-db-XC7T5B7P.js +110 -0
  63. package/dist/{mind-YVWAHL2A.js → mind-DI33C74K.js} +25 -25
  64. package/dist/{mind-activity-tracker-NMDDEV3K.js → mind-activity-tracker-EN6XNXPF.js} +3 -4
  65. package/dist/{mind-manager-4NDNAYAB.js → mind-manager-M6EMUW5I.js} +6 -5
  66. package/dist/{mind-sleep-GHPTSAYN.js → mind-sleep-BTSWQNAC.js} +4 -4
  67. package/dist/{mind-wake-BJDJFMDF.js → mind-wake-SBAKIDVP.js} +4 -4
  68. package/dist/notes-XCER3I7M.js +220 -0
  69. package/dist/{package-3HF5MXU2.js → package-7WY6VKU3.js} +2 -1
  70. package/dist/{pages-Y6DRWUOJ.js → pages-6EBS6CBR.js} +2 -2
  71. package/dist/{publish-EEKTZBHW.js → publish-66UB2ZFY.js} +5 -5
  72. package/dist/{pull-D32SPFVU.js → pull-XCHJTM5M.js} +4 -4
  73. package/dist/read-36UFXN3G.js +46 -0
  74. package/dist/{register-U2UO6TC4.js → register-6B2CXTYM.js} +3 -3
  75. package/dist/{registry-D2BSQ2X5.js → registry-NDNOOYG4.js} +15 -9
  76. package/dist/{restart-5BMNV7KU.js → restart-6ESL3NBO.js} +6 -6
  77. package/dist/sandbox-TGBX22DS.js +19 -0
  78. package/dist/{schedule-YEFDLVMJ.js → schedule-QTJMFATP.js} +7 -7
  79. package/dist/{seed-6FEKB3YC.js → seed-SSUCYYDF.js} +2 -2
  80. package/dist/{send-IISDYFCL.js → send-ZNCJDSRP.js} +28 -36
  81. package/dist/service-6LIN3F3K.js +122 -0
  82. package/dist/setup-JG4QAEBV.js +371 -0
  83. package/dist/setup-JHL5ZEST.js +17 -0
  84. package/dist/{shared-LWMNTTZN.js → shared-ML5I4Q2A.js} +4 -4
  85. package/dist/{skill-T3EMR6IR.js → skill-AUAQTSP5.js} +7 -7
  86. package/dist/skills/dreaming/SKILL.md +68 -0
  87. package/dist/skills/dreaming/references/INSTALL.md +56 -0
  88. package/dist/skills/dreaming/scripts/dream.ts +289 -0
  89. package/dist/skills/dreaming/scripts/wake-context-dreams.sh +30 -0
  90. package/dist/skills/notes/SKILL.md +34 -0
  91. package/dist/skills/orientation/SKILL.md +3 -3
  92. package/dist/skills/volute-mind/SKILL.md +32 -30
  93. package/dist/sleep-manager-MWYHM5HV.js +29 -0
  94. package/dist/split-TKJ5OT3P.js +63 -0
  95. package/dist/{sprout-QJVGJDSH.js → sprout-IJVVKSJ2.js} +6 -7
  96. package/dist/{start-C7XITZ5O.js → start-EUJSS5R4.js} +4 -4
  97. package/dist/{status-SIRPLEZC.js → status-77YEPHMW.js} +5 -5
  98. package/dist/{status-LYS4NUOZ.js → status-7GA4SM4Y.js} +4 -4
  99. package/dist/{status-LV34BG6G.js → status-THLOBLWG.js} +2 -2
  100. package/dist/{stop-CVKBSLXY.js → stop-3XAITBBF.js} +6 -6
  101. package/dist/{tailscale-AJ4VL5XK.js → tailscale-NY5MUMY3.js} +1 -1
  102. package/dist/up-NKSMXBWR.js +17 -0
  103. package/dist/{update-7XCZMYBT.js → update-PTSH22AZ.js} +11 -11
  104. package/dist/{update-check-F5Z3ALXX.js → update-check-64FWC4Y2.js} +2 -2
  105. package/dist/{upgrade-7RUIXGOO.js → upgrade-HA47CS4C.js} +12 -5
  106. package/dist/variant-7TGZHOU3.js +41 -0
  107. package/dist/{version-notify-AZQMC32A.js → version-notify-5Z4MNR6M.js} +26 -28
  108. package/dist/web-assets/assets/index-CI5wgghI.css +1 -0
  109. package/dist/web-assets/assets/index-is5CvJWH.js +75 -0
  110. package/dist/web-assets/favicon.png +0 -0
  111. package/dist/web-assets/index.html +2 -2
  112. package/drizzle/0015_notes.sql +23 -0
  113. package/drizzle/0016_note_reactions_and_replies.sql +15 -0
  114. package/drizzle/0017_minds.sql +16 -0
  115. package/drizzle/meta/_journal.json +21 -0
  116. package/package.json +2 -1
  117. package/templates/_base/.init/.config/hooks/wake-context.sh +7 -0
  118. package/templates/_base/.init/.config/prompts.json +2 -2
  119. package/templates/_base/home/VOLUTE.md +5 -5
  120. package/templates/_base/src/lib/startup.ts +10 -2
  121. package/templates/claude/src/agent.ts +51 -1
  122. package/templates/claude/src/server.ts +1 -0
  123. package/templates/pi/package.json.tmpl +1 -0
  124. package/templates/pi/src/agent.ts +48 -1
  125. package/templates/pi/src/lib/subagents.ts +150 -0
  126. package/templates/pi/src/server.ts +1 -0
  127. package/dist/channel-HZOSHGNF.js +0 -260
  128. package/dist/chunk-33XAVCS4.js +0 -203
  129. package/dist/chunk-B2CPS4QU.js +0 -283
  130. package/dist/chunk-NWPT4ASZ.js +0 -89
  131. package/dist/chunk-SIAG3QMM.js +0 -42
  132. package/dist/chunk-WSLPZF72.js +0 -173
  133. package/dist/connector-M6XFI6GM.js +0 -147
  134. package/dist/connectors/discord.js +0 -177
  135. package/dist/connectors/slack.js +0 -181
  136. package/dist/connectors/telegram.js +0 -187
  137. package/dist/down-674SX2IZ.js +0 -14
  138. package/dist/message-delivery-XMGV3FUM.js +0 -23
  139. package/dist/service-FASYWLTC.js +0 -247
  140. package/dist/setup-BMLM2UTK.js +0 -230
  141. package/dist/sleep-manager-RKTFZPD3.js +0 -27
  142. package/dist/up-CJ26KQLN.js +0 -15
  143. package/dist/variant-UGREB4G5.js +0 -207
  144. package/dist/web-assets/assets/index-CGPSVu19.js +0 -69
  145. package/dist/web-assets/assets/index-V_rNDsM8.css +0 -1
@@ -1,12 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- loadMergedEnv
4
- } from "./chunk-PHU4DEAJ.js";
3
+ isSandboxEnabled,
4
+ wrapForSandbox
5
+ } from "./chunk-USUXRNVD.js";
5
6
  import {
6
- getDb,
7
- mindHistory,
8
- systemPrompts
9
- } from "./chunk-33XAVCS4.js";
7
+ loadMergedEnv
8
+ } from "./chunk-2WPW7OT6.js";
10
9
  import {
11
10
  logger_default
12
11
  } from "./chunk-YUIHSKR6.js";
@@ -14,16 +13,17 @@ import {
14
13
  chownMindDir,
15
14
  isIsolationEnabled,
16
15
  wrapForIsolation
17
- } from "./chunk-NWPT4ASZ.js";
16
+ } from "./chunk-RKQEHRBB.js";
18
17
  import {
19
18
  findMind,
20
- findVariant,
19
+ getDb,
21
20
  mindDir,
21
+ mindHistory,
22
22
  setMindRunning,
23
- setVariantRunning,
24
23
  stateDir,
25
- voluteHome
26
- } from "./chunk-B2CPS4QU.js";
24
+ systemPrompts,
25
+ voluteSystemDir
26
+ } from "./chunk-H7OZRFJB.js";
27
27
 
28
28
  // src/lib/daemon/mind-manager.ts
29
29
  import { execFile, spawn } from "child_process";
@@ -82,8 +82,7 @@ var PROMPT_KEYS = [
82
82
  "reply_instructions",
83
83
  "channel_invite",
84
84
  "pre_sleep",
85
- "wake_summary",
86
- "wake_trigger_summary"
85
+ "wake_summary"
87
86
  ];
88
87
  var PROMPT_DEFAULTS = {
89
88
  seed_soul: {
@@ -141,7 +140,7 @@ Have a conversation with the human. Explore what kind of mind you want to be. Wh
141
140
  category: "mind"
142
141
  },
143
142
  reply_instructions: {
144
- content: 'To reply to this message, use: volute send ${channel} "your message"',
143
+ content: 'To reply to this message, use: volute chat send ${channel} "your message"',
145
144
  description: "First-message reply hint injected via hook",
146
145
  variables: ["channel"],
147
146
  category: "mind"
@@ -157,7 +156,7 @@ Further messages will be saved to \${filePath}
157
156
 
158
157
  To accept, add to .config/routes.json:
159
158
  Rule: { "channel": "\${channel}", "session": "\${suggestedSession}" }
160
- \${batchRecommendation}To respond, use: volute send \${channel} "your message"
159
+ \${batchRecommendation}To respond, use: volute chat send \${channel} "your message"
161
160
  To reject, delete \${filePath}`,
162
161
  description: "New channel notification template",
163
162
  variables: [
@@ -179,15 +178,9 @@ To reject, delete \${filePath}`,
179
178
  category: "system"
180
179
  },
181
180
  wake_summary: {
182
- content: "Good morning \u2014 it's ${currentDate}. You slept from ${sleepTime} to now (${duration}).\n\n${queuedSummary}",
181
+ content: "Good morning \u2014 it's ${currentDate}. You slept from ${sleepTime} to now (${duration}).\n\n${sleepActivity}",
183
182
  description: "Wake-up summary after scheduled sleep",
184
- variables: ["currentDate", "sleepTime", "duration", "queuedSummary"],
185
- category: "system"
186
- },
187
- wake_trigger_summary: {
188
- content: "You were woken during sleep by a message on ${triggerChannel}. It's ${currentDate} \u2014 you've been asleep since ${sleepTime} (${duration}).\n\nYou have this full turn to respond and handle anything else. You'll return to sleep when you go idle.\n\n${queuedSummary}",
189
- description: "Wake-up summary when woken by a trigger message",
190
- variables: ["currentDate", "triggerChannel", "sleepTime", "duration", "queuedSummary"],
183
+ variables: ["currentDate", "sleepTime", "duration", "sleepActivity"],
191
184
  category: "system"
192
185
  }
193
186
  };
@@ -295,6 +288,28 @@ var RotatingLog = class extends Writable {
295
288
  }
296
289
  };
297
290
 
291
+ // src/lib/daemon/mind-tokens.ts
292
+ import { randomUUID } from "crypto";
293
+ var tokenToMind = /* @__PURE__ */ new Map();
294
+ var mindToToken = /* @__PURE__ */ new Map();
295
+ function generateMindToken(mindName) {
296
+ revokeMindToken(mindName);
297
+ const token = randomUUID();
298
+ tokenToMind.set(token, mindName);
299
+ mindToToken.set(mindName, token);
300
+ return token;
301
+ }
302
+ function revokeMindToken(mindName) {
303
+ const token = mindToToken.get(mindName);
304
+ if (token) {
305
+ tokenToMind.delete(token);
306
+ mindToToken.delete(mindName);
307
+ }
308
+ }
309
+ function resolveMindToken(token) {
310
+ return tokenToMind.get(token) ?? null;
311
+ }
312
+
298
313
  // src/lib/daemon/restart-tracker.ts
299
314
  var DEFAULT_MAX_ATTEMPTS = 5;
300
315
  var DEFAULT_BASE_DELAY = 3e3;
@@ -352,25 +367,23 @@ var MindManager = class {
352
367
  shuttingDown = false;
353
368
  restartTracker = new RestartTracker();
354
369
  pendingContext = /* @__PURE__ */ new Map();
355
- resolveTarget(name) {
356
- const [baseName, variantName] = name.split("@", 2);
357
- const entry = findMind(baseName);
358
- if (!entry) throw new Error(`Unknown mind: ${baseName}`);
359
- if (variantName) {
360
- const variant = findVariant(baseName, variantName);
361
- if (!variant) throw new Error(`Unknown variant: ${variantName} (mind: ${baseName})`);
362
- return { dir: variant.path, port: variant.port, isVariant: true, baseName, variantName };
370
+ async resolveTarget(name) {
371
+ const entry = await findMind(name);
372
+ if (!entry) throw new Error(`Unknown mind: ${name}`);
373
+ if (entry.parent) {
374
+ if (!entry.dir) throw new Error(`Variant ${name} has no directory`);
375
+ return { dir: entry.dir, port: entry.port, baseName: entry.parent };
363
376
  }
364
- const dir = mindDir(baseName);
377
+ const dir = mindDir(name);
365
378
  if (!existsSync3(dir)) throw new Error(`Mind directory missing: ${dir}`);
366
- return { dir, port: entry.port, isVariant: false, baseName };
379
+ return { dir, port: entry.port, baseName: name };
367
380
  }
368
381
  async startMind(name) {
369
382
  if (this.minds.has(name)) {
370
383
  throw new Error(`Mind ${name} is already running`);
371
384
  }
372
- const target = this.resolveTarget(name);
373
- const { dir, isVariant, baseName, variantName } = target;
385
+ const target = await this.resolveTarget(name);
386
+ const { dir, baseName } = target;
374
387
  const port = target.port;
375
388
  const pidFile = mindPidPath(name);
376
389
  try {
@@ -420,6 +433,7 @@ var MindManager = class {
420
433
  }
421
434
  }
422
435
  const logStream = new RotatingLog(resolve(logsDir, "mind.log"));
436
+ const mindToken = generateMindToken(name);
423
437
  const mindEnv = loadMergedEnv(name);
424
438
  const env = {
425
439
  ...process.env,
@@ -428,6 +442,7 @@ var MindManager = class {
428
442
  VOLUTE_STATE_DIR: stateDir(name),
429
443
  VOLUTE_MIND_DIR: dir,
430
444
  VOLUTE_MIND_PORT: String(port),
445
+ VOLUTE_DAEMON_TOKEN: mindToken,
431
446
  // Strip CLAUDECODE so the Agent SDK can spawn Claude Code subprocesses
432
447
  CLAUDECODE: void 0
433
448
  };
@@ -436,7 +451,16 @@ var MindManager = class {
436
451
  }
437
452
  const tsxBin = resolve(dir, "node_modules", ".bin", "tsx");
438
453
  const tsxArgs = ["src/server.ts", "--port", String(port)];
439
- const [spawnCmd, spawnArgs] = wrapForIsolation(tsxBin, tsxArgs, name);
454
+ let spawnCmd;
455
+ let spawnArgs;
456
+ if (isIsolationEnabled()) {
457
+ [spawnCmd, spawnArgs] = await wrapForIsolation(tsxBin, tsxArgs, name);
458
+ } else if (isSandboxEnabled()) {
459
+ [spawnCmd, spawnArgs] = await wrapForSandbox(tsxBin, tsxArgs, dir, name);
460
+ } else {
461
+ spawnCmd = tsxBin;
462
+ spawnArgs = tsxArgs;
463
+ }
440
464
  const spawnOpts = {
441
465
  cwd: dir,
442
466
  stdio: ["ignore", "pipe", "pipe"],
@@ -486,11 +510,7 @@ var MindManager = class {
486
510
  }
487
511
  if (this.restartTracker.reset(name)) this.saveCrashAttempts();
488
512
  this.setupCrashRecovery(name, child);
489
- if (isVariant) {
490
- setVariantRunning(baseName, variantName, true);
491
- } else {
492
- setMindRunning(name, true);
493
- }
513
+ await setMindRunning(name, true);
494
514
  mlog.info(`started mind ${name} on port ${port}`);
495
515
  await this.deliverPendingContext(name);
496
516
  }
@@ -548,28 +568,24 @@ var MindManager = class {
548
568
  if (this.shuttingDown || this.stopping.has(name)) return;
549
569
  mlog.error(`mind ${name} exited with code ${code}`);
550
570
  try {
551
- const { getSleepManagerIfReady } = await import("./sleep-manager-RKTFZPD3.js");
552
- if (getSleepManagerIfReady()?.isSleeping(name)) {
571
+ const { getSleepManagerIfReady } = await import("./sleep-manager-MWYHM5HV.js");
572
+ const sleepState = getSleepManagerIfReady()?.getState(name);
573
+ if (sleepState?.sleeping) {
553
574
  mlog.info(`${name} is sleeping \u2014 skipping crash recovery`);
554
575
  return;
555
576
  }
556
577
  } catch (err) {
557
578
  mlog.warn(`failed to check sleep state for ${name}`, logger_default.errorData(err));
558
579
  }
559
- import("./mind-activity-tracker-NMDDEV3K.js").then(({ markIdle }) => markIdle(name)).catch((err) => mlog.warn(`failed to mark ${name} idle after crash`, logger_default.errorData(err)));
560
- import("./activity-events-4O37J7PD.js").then(
580
+ import("./mind-activity-tracker-EN6XNXPF.js").then(({ markIdle }) => markIdle(name)).catch((err) => mlog.warn(`failed to mark ${name} idle after crash`, logger_default.errorData(err)));
581
+ import("./activity-events-BBIEA2F4.js").then(
561
582
  ({ publish }) => publish({ type: "mind_stopped", mind: name, summary: `${name} crashed (exit ${code})` })
562
583
  ).catch((err) => mlog.warn(`failed to publish crash event for ${name}`, logger_default.errorData(err)));
563
584
  const { shouldRestart, delay, attempt } = this.restartTracker.recordCrash(name);
564
585
  this.saveCrashAttempts();
565
586
  if (!shouldRestart) {
566
587
  mlog.error(`${name} crashed ${attempt} times \u2014 giving up on restart`);
567
- const [base, variant] = name.split("@", 2);
568
- if (variant) {
569
- setVariantRunning(base, variant, false);
570
- } else {
571
- setMindRunning(name, false);
572
- }
588
+ await setMindRunning(name, false);
573
589
  return;
574
590
  }
575
591
  mlog.info(
@@ -605,15 +621,11 @@ var MindManager = class {
605
621
  }, 5e3);
606
622
  });
607
623
  this.stopping.delete(name);
624
+ revokeMindToken(name);
608
625
  if (this.restartTracker.reset(name)) this.saveCrashAttempts();
609
626
  rmSync2(mindPidPath(name), { force: true });
610
627
  if (!this.shuttingDown) {
611
- const [baseName, variantName] = name.split("@", 2);
612
- if (variantName) {
613
- setVariantRunning(baseName, variantName, false);
614
- } else {
615
- setMindRunning(name, false);
616
- }
628
+ await setMindRunning(name, false);
617
629
  }
618
630
  mlog.info(`stopped mind ${name}`);
619
631
  }
@@ -633,7 +645,7 @@ var MindManager = class {
633
645
  return [...this.minds.keys()];
634
646
  }
635
647
  get crashAttemptsPath() {
636
- return resolve(voluteHome(), "crash-attempts.json");
648
+ return resolve(voluteSystemDir(), "crash-attempts.json");
637
649
  }
638
650
  loadCrashAttempts() {
639
651
  this.restartTracker.load(loadJsonMap(this.crashAttemptsPath));
@@ -696,6 +708,7 @@ export {
696
708
  loadJsonMap,
697
709
  saveJsonMap,
698
710
  clearJsonMap,
711
+ resolveMindToken,
699
712
  MindManager,
700
713
  initMindManager,
701
714
  getMindManager
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getBaseName,
4
+ validateMindName
5
+ } from "./chunk-H7OZRFJB.js";
6
+
7
+ // src/lib/isolation.ts
8
+ import { execFileSync } from "child_process";
9
+ function isIsolationEnabled() {
10
+ return process.env.VOLUTE_ISOLATION === "user";
11
+ }
12
+ function mindUserName(mindName) {
13
+ const err = validateMindName(mindName);
14
+ if (err) throw new Error(`Invalid mind name for isolation: ${err}`);
15
+ const prefix = process.env.VOLUTE_USER_PREFIX ?? "mind-";
16
+ return `${prefix}${mindName}`;
17
+ }
18
+ function findNextMacId(type) {
19
+ const idField = type === "Users" ? "UniqueID" : "PrimaryGroupID";
20
+ let output;
21
+ try {
22
+ output = execFileSync("dscl", [".", "-list", `/${type}`, idField], { encoding: "utf-8" });
23
+ } catch (err) {
24
+ throw new Error(
25
+ `Failed to query ${type} via dscl: ${err instanceof Error ? err.message : err}`
26
+ );
27
+ }
28
+ const ids = /* @__PURE__ */ new Set();
29
+ for (const line of output.split("\n")) {
30
+ const parts = line.trim().split(/\s+/);
31
+ const id = parseInt(parts[parts.length - 1], 10);
32
+ if (!Number.isNaN(id)) ids.add(id);
33
+ }
34
+ let next = 401;
35
+ while (ids.has(next)) next++;
36
+ return next;
37
+ }
38
+ function getVoluteGroupGid() {
39
+ if (process.platform === "darwin") {
40
+ const output2 = execFileSync("dscl", [".", "-read", "/Groups/volute", "PrimaryGroupID"], {
41
+ encoding: "utf-8"
42
+ });
43
+ const match = output2.match(/PrimaryGroupID:\s*(\d+)/);
44
+ if (!match) throw new Error("Could not read volute group GID");
45
+ return parseInt(match[1], 10);
46
+ }
47
+ const output = execFileSync("getent", ["group", "volute"], { encoding: "utf-8" });
48
+ const gid = parseInt(output.split(":")[2], 10);
49
+ if (Number.isNaN(gid)) throw new Error("Could not read volute group GID");
50
+ return gid;
51
+ }
52
+ function ensureVoluteGroup(opts) {
53
+ if (!opts?.force && !isIsolationEnabled()) return;
54
+ if (process.platform === "darwin") {
55
+ try {
56
+ execFileSync("dscl", [".", "-read", "/Groups/volute"], { stdio: "ignore" });
57
+ return;
58
+ } catch {
59
+ }
60
+ const gid = findNextMacId("Groups");
61
+ try {
62
+ execFileSync("dscl", [".", "-create", "/Groups/volute"]);
63
+ execFileSync("dscl", [".", "-create", "/Groups/volute", "PrimaryGroupID", String(gid)]);
64
+ execFileSync("dscl", [".", "-create", "/Groups/volute", "Password", "*"]);
65
+ } catch (err) {
66
+ const msg = err instanceof Error ? err.message : String(err);
67
+ throw new Error(`Failed to create volute group on macOS: ${msg}`);
68
+ }
69
+ return;
70
+ }
71
+ try {
72
+ execFileSync("getent", ["group", "volute"], { stdio: "ignore" });
73
+ } catch {
74
+ try {
75
+ execFileSync("groupadd", ["volute"], { stdio: ["ignore", "ignore", "pipe"] });
76
+ } catch (err) {
77
+ const stderr = err?.stderr?.toString().trim();
78
+ throw new Error(`Failed to create volute group${stderr ? `: ${stderr}` : ""}`);
79
+ }
80
+ }
81
+ }
82
+ function createMindUser(name, homeDir) {
83
+ if (!isIsolationEnabled()) return;
84
+ const user = mindUserName(name);
85
+ try {
86
+ execFileSync("id", [user], { stdio: "ignore" });
87
+ return;
88
+ } catch {
89
+ }
90
+ if (process.platform === "darwin") {
91
+ const uid = findNextMacId("Users");
92
+ const gid = getVoluteGroupGid();
93
+ const home = homeDir ?? "/var/empty";
94
+ try {
95
+ execFileSync("dscl", [".", "-create", `/Users/${user}`]);
96
+ execFileSync("dscl", [".", "-create", `/Users/${user}`, "UniqueID", String(uid)]);
97
+ execFileSync("dscl", [".", "-create", `/Users/${user}`, "PrimaryGroupID", String(gid)]);
98
+ execFileSync("dscl", [".", "-create", `/Users/${user}`, "UserShell", "/usr/bin/false"]);
99
+ execFileSync("dscl", [".", "-create", `/Users/${user}`, "NFSHomeDirectory", home]);
100
+ execFileSync("dscl", [".", "-create", `/Users/${user}`, "RealName", `Volute Mind: ${name}`]);
101
+ execFileSync("dscl", [".", "-create", `/Users/${user}`, "IsHidden", "1"]);
102
+ execFileSync("dscl", [".", "-append", "/Groups/volute", "GroupMembership", user]);
103
+ } catch (err) {
104
+ const msg = err instanceof Error ? err.message : String(err);
105
+ throw new Error(`Failed to create user ${user} on macOS: ${msg}`);
106
+ }
107
+ return;
108
+ }
109
+ try {
110
+ const args = ["-r", "-M", "-G", "volute", "-s", "/usr/sbin/nologin"];
111
+ if (homeDir) args.push("-d", homeDir);
112
+ args.push(user);
113
+ execFileSync("useradd", args, {
114
+ stdio: ["ignore", "ignore", "pipe"]
115
+ });
116
+ } catch (err) {
117
+ const stderr = err?.stderr?.toString().trim();
118
+ throw new Error(`Failed to create user ${user}${stderr ? `: ${stderr}` : ""}`);
119
+ }
120
+ }
121
+ function deleteMindUser(name) {
122
+ if (!isIsolationEnabled()) return;
123
+ const user = mindUserName(name);
124
+ if (process.platform === "darwin") {
125
+ try {
126
+ execFileSync("dscl", [".", "-delete", `/Users/${user}`], { stdio: "ignore" });
127
+ } catch {
128
+ }
129
+ try {
130
+ execFileSync("dscl", [".", "-delete", "/Groups/volute", "GroupMembership", user], {
131
+ stdio: "ignore"
132
+ });
133
+ } catch {
134
+ }
135
+ return;
136
+ }
137
+ try {
138
+ execFileSync("userdel", [user], { stdio: "ignore" });
139
+ } catch {
140
+ }
141
+ }
142
+ async function wrapForIsolation(cmd, args, mindName) {
143
+ if (!isIsolationEnabled()) return [cmd, args];
144
+ const baseName = await getBaseName(mindName);
145
+ const user = mindUserName(baseName);
146
+ if (process.platform === "darwin") {
147
+ return ["sudo", ["-u", user, "--", cmd, ...args]];
148
+ }
149
+ return ["runuser", ["-u", user, "--", cmd, ...args]];
150
+ }
151
+ function chownMindDir(dir, name) {
152
+ if (!isIsolationEnabled()) return;
153
+ const user = mindUserName(name);
154
+ const group = process.platform === "darwin" ? "volute" : user;
155
+ try {
156
+ execFileSync("chown", ["-R", `${user}:${group}`, dir], { stdio: ["ignore", "ignore", "pipe"] });
157
+ } catch (err) {
158
+ const stderr = err?.stderr?.toString().trim();
159
+ throw new Error(`Failed to chown ${dir} to ${user}:${group}${stderr ? `: ${stderr}` : ""}`);
160
+ }
161
+ try {
162
+ execFileSync("chmod", ["700", dir], { stdio: ["ignore", "ignore", "pipe"] });
163
+ } catch (err) {
164
+ const stderr = err?.stderr?.toString().trim();
165
+ throw new Error(`Failed to chmod ${dir}${stderr ? `: ${stderr}` : ""}`);
166
+ }
167
+ }
168
+
169
+ export {
170
+ isIsolationEnabled,
171
+ mindUserName,
172
+ ensureVoluteGroup,
173
+ createMindUser,
174
+ deleteMindUser,
175
+ wrapForIsolation,
176
+ chownMindDir
177
+ };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/lib/prompt.ts
4
- function promptLine(prompt) {
4
+ function rawPrompt(prompt, echo) {
5
5
  process.stderr.write(prompt);
6
6
  return new Promise((resolve) => {
7
7
  let value = "";
@@ -23,6 +23,7 @@ function promptLine(prompt) {
23
23
  value = value.slice(0, -1);
24
24
  } else {
25
25
  value += String.fromCharCode(byte);
26
+ if (echo) process.stderr.write(String.fromCharCode(byte));
26
27
  }
27
28
  }
28
29
  };
@@ -31,7 +32,14 @@ function promptLine(prompt) {
31
32
  process.stdin.on("data", onData);
32
33
  });
33
34
  }
35
+ function promptLine(prompt) {
36
+ return rawPrompt(prompt, true);
37
+ }
38
+ function promptPassword(prompt) {
39
+ return rawPrompt(prompt, false);
40
+ }
34
41
 
35
42
  export {
36
- promptLine
43
+ promptLine,
44
+ promptPassword
37
45
  };
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/lib/slugify.ts
4
+ function slugify(text) {
5
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
6
+ }
7
+ function buildVoluteSlug(opts) {
8
+ if (opts.convType === "channel" && opts.convName) {
9
+ return `volute:#${opts.convName}`;
10
+ }
11
+ const isDM = opts.participants.length === 2;
12
+ if (isDM) {
13
+ const other = opts.participants.find((p) => p.username !== opts.mindUsername);
14
+ const otherSlug = other ? slugify(other.username) : "";
15
+ return otherSlug ? `volute:@${otherSlug}` : `volute:${opts.conversationId}`;
16
+ }
17
+ return opts.convTitle ? `volute:${slugify(opts.convTitle)}` : `volute:${opts.conversationId}`;
18
+ }
19
+
20
+ export {
21
+ slugify,
22
+ buildVoluteSlug
23
+ };
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ logger_default
4
+ } from "./chunk-YUIHSKR6.js";
5
+ import {
6
+ readGlobalConfig
7
+ } from "./chunk-IKRVFPWU.js";
8
+ import {
9
+ getBaseName,
10
+ readRegistry,
11
+ voluteHome,
12
+ voluteSystemDir,
13
+ voluteUserHome
14
+ } from "./chunk-H7OZRFJB.js";
15
+
16
+ // src/lib/sandbox.ts
17
+ import { resolve } from "path";
18
+ var slog = logger_default.child("sandbox");
19
+ var sandboxManager = null;
20
+ function isSandboxEnabled() {
21
+ if (process.env.VOLUTE_SANDBOX === "0") return false;
22
+ return readGlobalConfig().setup?.isolation === "sandbox";
23
+ }
24
+ async function initSandbox() {
25
+ if (!isSandboxEnabled()) return;
26
+ try {
27
+ const { SandboxManager } = await import("@anthropic-ai/sandbox-runtime");
28
+ const config = {
29
+ network: {
30
+ allowedDomains: ["*"],
31
+ deniedDomains: [],
32
+ allowLocalBinding: true
33
+ },
34
+ filesystem: {
35
+ denyRead: [],
36
+ allowWrite: [],
37
+ denyWrite: []
38
+ }
39
+ };
40
+ await SandboxManager.initialize(config);
41
+ sandboxManager = SandboxManager;
42
+ } catch (err) {
43
+ slog.error(
44
+ "sandbox runtime not available \u2014 minds will run without sandbox isolation",
45
+ logger_default.errorData(err)
46
+ );
47
+ }
48
+ }
49
+ async function buildDenyRead(mindName, mindDir) {
50
+ const home = voluteHome();
51
+ const userHome = process.env.HOME || "";
52
+ const mindsDir = process.env.VOLUTE_MINDS_DIR || resolve(home, "minds");
53
+ const deny = [];
54
+ deny.push(voluteSystemDir());
55
+ const userVoluteHome = voluteUserHome();
56
+ if (userVoluteHome !== home) {
57
+ deny.push(userVoluteHome);
58
+ } else {
59
+ deny.push(resolve(home, "systems.json"));
60
+ }
61
+ try {
62
+ const entries = await readRegistry();
63
+ for (const entry of entries) {
64
+ if (entry.name === await getBaseName(mindName)) continue;
65
+ const otherDir = resolve(mindsDir, entry.name);
66
+ if (otherDir !== mindDir) {
67
+ deny.push(otherDir);
68
+ }
69
+ }
70
+ } catch (err) {
71
+ slog.warn("failed to read minds registry for deny-read list", logger_default.errorData(err));
72
+ }
73
+ if (userHome) {
74
+ deny.push(resolve(userHome, ".ssh"));
75
+ deny.push(resolve(userHome, ".aws"));
76
+ deny.push(resolve(userHome, ".gnupg"));
77
+ deny.push(resolve(userHome, ".config"));
78
+ }
79
+ return deny;
80
+ }
81
+ function shellEscape(s) {
82
+ return `'${s.replace(/'/g, "'\\''")}'`;
83
+ }
84
+ async function wrapForSandbox(cmd, args, mindDir, mindName, allowWrite) {
85
+ if (!sandboxManager) return [cmd, args];
86
+ const denyRead = await buildDenyRead(mindName, mindDir);
87
+ const customConfig = {
88
+ filesystem: {
89
+ denyRead,
90
+ allowWrite: allowWrite ?? [mindDir],
91
+ denyWrite: []
92
+ }
93
+ };
94
+ try {
95
+ const shellCmd = [cmd, ...args].map(shellEscape).join(" ");
96
+ const wrapped = await sandboxManager.wrapWithSandbox(shellCmd, void 0, customConfig);
97
+ return ["bash", ["-c", wrapped]];
98
+ } catch (err) {
99
+ slog.error(
100
+ `failed to sandbox mind ${mindName} \u2014 running without isolation`,
101
+ logger_default.errorData(err)
102
+ );
103
+ return [cmd, args];
104
+ }
105
+ }
106
+
107
+ export {
108
+ isSandboxEnabled,
109
+ initSandbox,
110
+ buildDenyRead,
111
+ shellEscape,
112
+ wrapForSandbox
113
+ };
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- activity,
4
- getDb
5
- } from "./chunk-33XAVCS4.js";
6
2
  import {
7
3
  logger_default
8
4
  } from "./chunk-YUIHSKR6.js";
5
+ import {
6
+ activity,
7
+ getDb
8
+ } from "./chunk-H7OZRFJB.js";
9
9
 
10
10
  // src/lib/events/activity-events.ts
11
11
  var subscribers = /* @__PURE__ */ new Set();
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  mindDir,
4
4
  stateDir
5
- } from "./chunk-B2CPS4QU.js";
5
+ } from "./chunk-H7OZRFJB.js";
6
6
 
7
7
  // src/lib/archive.ts
8
8
  import { execFileSync } from "child_process";
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/lib/read-stdin.ts
4
+ import { isatty } from "tty";
5
+ async function readStdin() {
6
+ if (isatty(0)) return void 0;
7
+ const chunks = [];
8
+ try {
9
+ for await (const chunk of process.stdin) {
10
+ chunks.push(chunk);
11
+ }
12
+ } catch (err) {
13
+ console.error(`Failed to read from stdin: ${err instanceof Error ? err.message : String(err)}`);
14
+ process.exit(1);
15
+ }
16
+ const text = Buffer.concat(chunks).toString().replace(/\r?\n$/, "");
17
+ return text || void 0;
18
+ }
19
+
20
+ export {
21
+ readStdin
22
+ };