openvole 0.3.1 → 0.3.2

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 CHANGED
@@ -26,8 +26,11 @@ A fresh OpenVole installation has zero tools, zero skills, zero opinions. This i
26
26
  ```bash
27
27
  mkdir my-agent && cd my-agent
28
28
  npm init -y
29
- npm install openvole @openvole/paw-ollama @openvole/paw-memory @openvole/paw-dashboard
29
+ npm install openvole
30
30
  npx vole init
31
+ npx vole paw add @openvole/paw-ollama
32
+ npx vole paw add @openvole/paw-memory
33
+ npx vole paw add @openvole/paw-dashboard
31
34
  ```
32
35
 
33
36
  Edit `vole.config.json`:
@@ -91,23 +94,23 @@ curl -fsSL https://raw.githubusercontent.com/openvole/openvole/main/presets/full
91
94
  |
92
95
  v
93
96
  ┌─────────────────────────────────────────────────────────────┐
94
- │ VoleEngine
95
-
96
- │ Tool Registry ──── Skill Registry ──── Paw Registry
97
- │ | | |
97
+ │ VoleEngine
98
+
99
+ │ Tool Registry ──── Skill Registry ──── Paw Registry
100
+ │ | | |
98
101
  │ ┌────────────────────────────────────────────────┐ │
99
- │ │ Agent Loop (per task) │ │
100
- │ │ │ │
102
+ │ │ Agent Loop (per task) │ │
103
+ │ │ │ │
101
104
  │ │ PERCEIVE ─── THINK ─── ACT ─── OBSERVE │ │
102
- │ │ | | | | │ │
103
- │ │ Enrich Brain Execute Process │ │
104
- │ │ context plans tools results │ │
105
- │ │ │ │
106
- └─────────────────────────────────────────────────┘
107
-
108
- │ Task Queue ──── Scheduler ──── Message Bus
109
-
110
- └──────┬──────────┬──────────┬──────────┬──────────────────────┘
105
+ │ │ | | | | │ │
106
+ │ │ Enrich Brain Execute Process │ │
107
+ │ │ context plans tools results │ │
108
+ │ │ │ │
109
+ └────────────────────────────────────────────────┘
110
+
111
+ │ Task Queue ──── Scheduler ──── Message Bus
112
+
113
+ └──────┬──────────┬──────────┬──────────┬─────────────────────┘
111
114
  | | | |
112
115
  [Brain Paw] [Channel] [Tools] [In-Process]
113
116
  Ollama Telegram Browser Compact
@@ -141,7 +144,7 @@ Perceive → Think → Act → Observe → loop
141
144
  **Paws are tool providers.** They connect OpenVole to the outside world — APIs, databases, browsers, messaging platforms. Each Paw runs in an isolated subprocess with capability-based permissions.
142
145
 
143
146
  ```bash
144
- npm install @openvole/paw-telegram
147
+ npx vole paw add @openvole/paw-telegram
145
148
  ```
146
149
 
147
150
  ### Skills
@@ -312,7 +315,15 @@ When context grows too large, `paw-compact` summarizes old messages while preser
312
315
 
313
316
  ### Dashboard
314
317
 
315
- Real-time web UI at `localhost:3001` paws, tools, skills, tasks, schedules, live events.
318
+ Real-time web UI powered by `paw-dashboard`, another Paw you install like any other. Shows paws, tools, skills, tasks, schedules, and live events.
319
+
320
+ ```bash
321
+ npx vole paw add @openvole/paw-dashboard
322
+ ```
323
+
324
+ <p align="center">
325
+ <img src="https://raw.githubusercontent.com/openvole/openvole/main/assets/example/paw-dashboard/paw-dashboard.png" alt="OpenVole Dashboard" width="800">
326
+ </p>
316
327
 
317
328
  ## Official Paws (22)
318
329
 
@@ -362,7 +373,8 @@ All paws live in [PawHub](https://github.com/openvole/pawhub) and are installed
362
373
  Install from npm:
363
374
 
364
375
  ```bash
365
- npm install @openvole/paw-telegram @openvole/paw-browser
376
+ npx vole paw add @openvole/paw-telegram
377
+ npx vole paw add @openvole/paw-browser
366
378
  ```
367
379
 
368
380
  ## CLI
@@ -444,6 +456,37 @@ npx vole clawhub install summarize
444
456
  └── HEARTBEAT.md ← recurring job definitions
445
457
  ```
446
458
 
459
+ ## OpenVole vs OpenClaw
460
+
461
+ Both are open-source AI agent frameworks. Different philosophies, many shared concepts.
462
+
463
+ | | OpenVole | OpenClaw |
464
+ |---|---|---|
465
+ | **Philosophy** | Microkernel — empty core, everything is a plugin | Batteries-included — 25 built-in tools |
466
+ | **Core size** | ~60KB | ~8MB |
467
+ | **Skills** | SKILL.md (same format, compatible) | SKILL.md |
468
+ | **Skill marketplace** | ClawHub-compatible (`vole clawhub install`) | ClawHub (13K+ skills) |
469
+ | **Skill loading** | Progressive on-demand | Progressive on-demand |
470
+ | **Brain/LLM** | External Paw — core is LLM-ignorant | Configurable provider in core |
471
+ | **Brain options** | 5 (Ollama, Claude, OpenAI, Gemini, xAI) | Multi-provider with fallback chains |
472
+ | **Heartbeat** | HEARTBEAT.md + cron | HEARTBEAT.md + cron |
473
+ | **Memory** | Source-isolated (user/paw/heartbeat scoped) | Shared (no source isolation) |
474
+ | **Identity files** | BRAIN.md, SOUL.md, USER.md, AGENT.md | SOUL.md, USER.md, AGENTS.md |
475
+ | **MCP support** | Via Paw with auto-discovery + late registration | Native in core |
476
+ | **Channels** | 4 (Telegram, Slack, Discord, WhatsApp) | 20+ (WhatsApp, iMessage, Signal, etc.) |
477
+ | **Plugin isolation** | Subprocess sandbox + capability permissions | Optional Docker sandbox |
478
+ | **Tool profiles** | Per-source deny/allow lists | Channel sandboxing |
479
+ | **Scheduling** | Cron-based, persistent, Brain-initiated | Cron + heartbeat |
480
+ | **Sessions** | Per-session transcripts with auto-expiry | Built-in session keys |
481
+ | **Vault** | AES-256 encrypted, write-once, metadata | N/A (env vars) |
482
+ | **Dashboard** | Real-time web UI | Gateway web UI |
483
+ | **CLI** | `vole` (start/run/tool call/clawhub/skill) | `openclaw` |
484
+ | **Config** | Single JSON file | Single JSON file |
485
+
486
+ OpenVole is a newborn — a tiny vole just getting started. We share the same skill format, the same heartbeat pattern, and the same MCP ecosystem as OpenClaw. Skills written for one work on the other.
487
+
488
+ We're building something small, modular, and community-driven. If you like the microkernel approach — where every piece is a Paw you can swap, extend, or build yourself — come join us. Try it out, build a Paw, write a Skill, break things, and help this little vole grow.
489
+
447
490
  ## Philosophy
448
491
 
449
492
  > **If it connects to something, it's a Paw.**
package/dist/cli.js CHANGED
@@ -485,6 +485,19 @@ var init_task = __esm({
485
485
  }
486
486
  return false;
487
487
  }
488
+ /** Cancel all queued tasks (for shutdown) */
489
+ cancelAll() {
490
+ while (this.queue.length > 0) {
491
+ const task = this.queue.shift();
492
+ task.status = "cancelled";
493
+ task.completedAt = Date.now();
494
+ this.completed.push(task);
495
+ this.bus.emit("task:cancelled", { taskId: task.id });
496
+ }
497
+ for (const task of this.running.values()) {
498
+ task.status = "cancelled";
499
+ }
500
+ }
488
501
  /** Get all tasks (queued + running + completed) */
489
502
  list() {
490
503
  return [
@@ -612,7 +625,8 @@ var init_scheduler = __esm({
612
625
  if (persisted.length > 0) {
613
626
  logger4.info(`Restored ${persisted.length} schedule(s) from disk`);
614
627
  }
615
- } catch {
628
+ } catch (err) {
629
+ logger4.warn(`Could not restore schedules: ${err}`);
616
630
  }
617
631
  }
618
632
  /** Create or replace a recurring schedule */
@@ -655,8 +669,9 @@ var init_scheduler = __esm({
655
669
  createdAt
656
670
  }));
657
671
  }
658
- /** Clear all schedules (for shutdown) */
672
+ /** Clear all schedules (for shutdown). Disables persistence so the file is never overwritten. */
659
673
  clearAll() {
674
+ this.savePath = void 0;
660
675
  for (const entry of this.schedules.values()) {
661
676
  entry.job.stop();
662
677
  }
@@ -665,16 +680,29 @@ var init_scheduler = __esm({
665
680
  }
666
681
  /** Save schedules to disk */
667
682
  async persist() {
668
- if (!this.savePath) return;
683
+ const targetPath = this.savePath;
684
+ if (!targetPath) return;
669
685
  const toSave = Array.from(this.schedules.values()).filter((s) => s.id !== "__heartbeat__").map(({ id, input, cron, createdAt }) => ({
670
686
  id,
671
687
  input,
672
688
  cron,
673
689
  createdAt
674
690
  }));
691
+ if (toSave.length === 0) {
692
+ try {
693
+ const existing = await fs3.readFile(targetPath, "utf-8");
694
+ const parsed = JSON.parse(existing);
695
+ if (parsed.length > 0) {
696
+ logger4.warn(`Refusing to overwrite ${parsed.length} persisted schedule(s) with empty list`);
697
+ return;
698
+ }
699
+ } catch {
700
+ }
701
+ }
675
702
  try {
676
- await fs3.mkdir(path3.dirname(this.savePath), { recursive: true });
677
- await fs3.writeFile(this.savePath, JSON.stringify(toSave, null, 2) + "\n", "utf-8");
703
+ await fs3.mkdir(path3.dirname(targetPath), { recursive: true });
704
+ await fs3.writeFile(targetPath, JSON.stringify(toSave, null, 2) + "\n", "utf-8");
705
+ logger4.debug(`Persisted ${toSave.length} schedule(s) to disk`);
678
706
  } catch (err) {
679
707
  logger4.error(`Failed to persist schedules: ${err}`);
680
708
  }
@@ -2042,8 +2070,14 @@ var IpcTransport = class {
2042
2070
  this.handlers.clear();
2043
2071
  }
2044
2072
  send(message) {
2073
+ if (this.disposed) return;
2045
2074
  if (this.type === "ipc") {
2046
- this.childProcess.send?.(message);
2075
+ try {
2076
+ if (this.childProcess.connected) {
2077
+ this.childProcess.send?.(message);
2078
+ }
2079
+ } catch {
2080
+ }
2047
2081
  } else {
2048
2082
  const json = JSON.stringify(message);
2049
2083
  const header = `Content-Length: ${Buffer.byteLength(json)}\r
@@ -2876,6 +2910,7 @@ async function createEngine(projectRoot, options) {
2876
2910
  rateLimiter
2877
2911
  });
2878
2912
  });
2913
+ let shuttingDown = false;
2879
2914
  const engine = {
2880
2915
  bus,
2881
2916
  toolRegistry,
@@ -2886,10 +2921,12 @@ async function createEngine(projectRoot, options) {
2886
2921
  config,
2887
2922
  async start() {
2888
2923
  engineLogger.info("Starting OpenVole...");
2924
+ const headless = options?.headless ?? false;
2889
2925
  if (config.brain) {
2890
2926
  } else {
2891
2927
  engineLogger.info("No Brain Paw configured \u2014 Think step will be a no-op");
2892
2928
  }
2929
+ const headlessSkipPatterns = ["paw-dashboard", "paw-telegram", "paw-slack", "paw-discord", "paw-whatsapp"];
2893
2930
  const pawConfigs = config.paws.map(normalizePawConfig);
2894
2931
  const brainConfig = pawConfigs.find((p) => p.name === config.brain);
2895
2932
  const subprocessPaws = pawConfigs.filter(
@@ -2907,15 +2944,18 @@ async function createEngine(projectRoot, options) {
2907
2944
  pawRegistry.setBrain("");
2908
2945
  }
2909
2946
  }
2910
- for (const pawConfig of subprocessPaws) {
2911
- await pawRegistry.load(pawConfig);
2912
- }
2947
+ const pawsToLoad = headless ? subprocessPaws.filter((p) => !headlessSkipPatterns.some((pat) => p.name.includes(pat))) : subprocessPaws;
2948
+ await Promise.all(pawsToLoad.map((pawConfig) => pawRegistry.load(pawConfig)));
2913
2949
  for (const skillName of config.skills) {
2914
2950
  await skillRegistry.load(skillName);
2915
2951
  }
2916
2952
  skillRegistry.resolve();
2917
- await scheduler.restore();
2918
- if (config.heartbeat.enabled) {
2953
+ if (!headless) {
2954
+ await scheduler.restore();
2955
+ } else {
2956
+ await scheduler.loadFromDisk();
2957
+ }
2958
+ if (config.heartbeat.enabled && !headless) {
2919
2959
  const heartbeatMdPath = path10.resolve(projectRoot, ".openvole", "HEARTBEAT.md");
2920
2960
  const heartbeatCron = `*/${config.heartbeat.intervalMinutes} * * * *`;
2921
2961
  scheduler.add(
@@ -2946,8 +2986,11 @@ ${heartbeatContent}` : "Heartbeat wake-up. Check active skills and decide if any
2946
2986
  taskQueue.enqueue(input, source, sessionId ? { sessionId } : void 0);
2947
2987
  },
2948
2988
  async shutdown() {
2989
+ if (shuttingDown) return;
2990
+ shuttingDown = true;
2949
2991
  engineLogger.info("Shutting down...");
2950
2992
  scheduler.clearAll();
2993
+ taskQueue.cancelAll();
2951
2994
  for (const paw of pawRegistry.list()) {
2952
2995
  await pawRegistry.unload(paw.name);
2953
2996
  }
@@ -3097,7 +3140,7 @@ async function startInteractive(projectRoot) {
3097
3140
  process.on("SIGTERM", gracefulShutdown);
3098
3141
  }
3099
3142
  async function runSingle(projectRoot, input) {
3100
- const engine = await createEngine(projectRoot);
3143
+ const engine = await createEngine(projectRoot, { headless: true });
3101
3144
  await engine.start();
3102
3145
  engine.run(input);
3103
3146
  return new Promise((resolve7) => {
@@ -3837,7 +3880,14 @@ async function handleClawHubCommand(args, projectRoot) {
3837
3880
  logger15.info(` requires env: ${definition.requires.env.join(", ")}`);
3838
3881
  }
3839
3882
  } else {
3840
- logger15.warn(`Installed "${skillName}" but could not parse SKILL.md \u2014 add to vole.config.json manually`);
3883
+ const fsCheck = await import("fs/promises");
3884
+ const pluginJsonPath = path11.resolve(projectRoot, ".openvole", "skills", "clawhub", skillDir ?? skillName, "openclaw.plugin.json");
3885
+ try {
3886
+ await fsCheck.access(pluginJsonPath);
3887
+ logger15.error(`"${skillName}" is an OpenClaw plugin, not a skill. OpenVole does not support OpenClaw plugins \u2014 use Paws instead.`);
3888
+ } catch {
3889
+ logger15.warn(`Installed "${skillName}" but could not parse SKILL.md \u2014 add to vole.config.json manually`);
3890
+ }
3841
3891
  }
3842
3892
  break;
3843
3893
  }