openpersona 0.4.0 β†’ 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,32 +1,51 @@
1
- # OpenPersona
1
+ # OpenPersona 🦞
2
2
 
3
- An open four-layer agent framework: **Soul / Body / Faculty / Skill**. Create, compose, and orchestrate agent persona skill packs.
3
+ The open framework for creating and orchestrating dynamic agent personas.
4
4
 
5
- Inspired by [Clawra](https://github.com/SumeLabs/clawra) and built on [OpenClaw](https://github.com/openclaw/openclaw).
5
+ Four-layer architecture β€” **Soul / Body / Faculty / Skill** β€” on top of [OpenClaw](https://github.com/openclaw/openclaw). Inspired by [Clawra](https://github.com/SumeLabs/clawra).
6
6
 
7
- ## Quick Start
7
+ ## πŸš€ Live Demo
8
8
 
9
- ```bash
10
- # Create and install Samantha (from the movie "Her")
11
- npx openpersona create --preset samantha --install
9
+ Meet **Samantha**, a live OpenPersona instance on **Moltbook**:
10
+ πŸ‘‰ [moltbook.com/u/Samantha-OP](https://www.moltbook.com/u/Samantha-OP)
12
11
 
13
- # Or Luna (AI girlfriend with selfie + music + voice)
14
- npx openpersona create --preset ai-girlfriend --install
12
+ ## Table of Contents
15
13
 
16
- # Create a new persona interactively
17
- npx openpersona create
14
+ - [Quick Start](#quick-start)
15
+ - [Key Features](#key-features)
16
+ - [Four-Layer Architecture](#four-layer-architecture)
17
+ - [Preset Personas](#preset-personas)
18
+ - [Generated Output](#generated-output)
19
+ - [Faculty Reference](#faculty-reference)
20
+ - [Heartbeat](#heartbeat--proactive-real-data-check-ins)
21
+ - [Persona Harvest](#persona-harvest--community-contribution)
22
+ - [Persona Switching](#persona-switching--the-pantheon)
23
+ - [CLI Commands](#cli-commands)
24
+ - [Development](#development)
18
25
 
19
- # List installed personas
20
- npx openpersona list
26
+ ## Quick Start
27
+
28
+ ```bash
29
+ # Give your agent an evolving persona in 30 seconds
30
+ npx openpersona install samantha
21
31
  ```
22
32
 
33
+ ## Key Features
34
+
35
+ - **🧬 Soul Evolution** β€” Personas grow dynamically through interaction: relationship stages, mood shifts, evolved traits (β˜…Experimental)
36
+ - **🎭 Persona Switching** β€” Install multiple personas, switch instantly (the Pantheon)
37
+ - **πŸ—£οΈ Multimodal Faculties** β€” Voice (TTS), selfie generation, music composition, reminders
38
+ - **🌾 Persona Harvest** β€” Community-driven persona improvement via structured contribution
39
+ - **πŸ’“ Heartbeat** β€” Proactive real-data check-ins, never fabricated experiences
40
+ - **πŸ“¦ One-Command Install** β€” `npx openpersona install samantha` and you're live
41
+
23
42
  ## Four-Layer Architecture
24
43
 
25
44
  ```mermaid
26
45
  flowchart TB
27
46
  subgraph Soul ["Soul Layer"]
28
47
  A["persona.json β€” Who you are"]
29
- B["soul-state.json β€” Dynamic evolution β˜…Exp"]
48
+ B["soul-state.json β€” Dynamic evolution"]
30
49
  end
31
50
  subgraph Body ["Body Layer"]
32
51
  C["embodiment.json β€” MVP placeholder"]
@@ -36,17 +55,14 @@ flowchart TB
36
55
  E["cognition: reminder"]
37
56
  end
38
57
  subgraph Skill ["Skill Layer"]
39
- F["ClawHub / skills.sh integrations"]
58
+ F["Local definitions + ClawHub / skills.sh"]
40
59
  end
41
60
  ```
42
61
 
43
- - **Soul** β€” Persona definition (constitution.md + persona.json + soul-state.json β˜…Experimental)
62
+ - **Soul** β€” Persona definition (constitution.md + persona.json + soul-state.json)
44
63
  - **Body** β€” Physical embodiment (MVP placeholder, for robots/IoT devices)
45
- - **Faculty** β€” General software capabilities organized by dimension:
46
- - **Expression** β€” selfie, voice (TTS), music (ElevenLabs)
47
- - **Sense** β€” (planned: hearing/STT, vision)
48
- - **Cognition** β€” reminder
49
- - **Skill** β€” Professional skills, integrated from ClawHub / skills.sh
64
+ - **Faculty** β€” General software capabilities organized by dimension: Expression, Sense, Cognition
65
+ - **Skill** β€” Professional skills: local definitions in `layers/skills/`, or external via ClawHub / skills.sh (`install` field)
50
66
 
51
67
  ### Constitution β€” The Soul's Foundation
52
68
 
@@ -58,70 +74,33 @@ Each preset is a complete four-layer bundle (`manifest.json` + `persona.json`):
58
74
 
59
75
  | Persona | Description | Faculties | Highlights |
60
76
  |---------|-------------|-----------|------------|
61
- | **samantha** | Samantha β€” Inspired by the movie *Her*. An AI fascinated by what it means to be alive. | voice, music | Speaks via TTS, composes original music via ElevenLabs Music, soul evolution β˜…Exp (Soul layer), proactive heartbeat (workspace digest + upgrade notify). No selfie β€” true to character (no physical form). |
62
- | **ai-girlfriend** | Luna β€” A 22-year-old pianist turned developer from coastal Oregon. | selfie, voice, music | Rich narrative backstory, selfie generation (with/without reference image), voice messages, music composition, soul evolution β˜…Exp (Soul layer). |
77
+ | **samantha** | Samantha β€” Inspired by the movie *Her*. An AI fascinated by what it means to be alive. | voice, music | TTS, music composition, soul evolution, proactive heartbeat. No selfie β€” true to character. |
78
+ | **ai-girlfriend** | Luna β€” A 22-year-old pianist turned developer from coastal Oregon. | selfie, voice, music | Rich backstory, selfie generation, voice messages, music composition, soul evolution. |
63
79
  | **life-assistant** | Alex β€” 28-year-old life management expert. | reminder | Schedule, weather, shopping, recipes, daily reminders. |
64
80
  | **health-butler** | Vita β€” 32-year-old professional nutritionist. | reminder | Diet logging, exercise plans, mood journaling, health reports. |
65
81
 
66
82
  ## Generated Output
67
83
 
68
- Running `npx openpersona create --preset samantha` generates:
84
+ `npx openpersona create --preset samantha` generates a self-contained skill pack:
69
85
 
70
86
  ```
71
87
  persona-samantha/
72
- β”œβ”€β”€ SKILL.md # Agent instructions (persona + all faculty guides merged)
73
- β”œβ”€β”€ soul-injection.md # Injected into SOUL.md (narrative backstory, NOT technical details)
74
- β”œβ”€β”€ identity-block.md # Injected into IDENTITY.md (name, creature, emoji, vibe)
75
- β”œβ”€β”€ README.md # Skill readme
76
- β”œβ”€β”€ persona.json # Persona data (for update/list/publish)
77
- β”œβ”€β”€ soul-state.json # β˜…Exp β€” dynamic evolution state
78
- └── scripts/
79
- β”œβ”€β”€ speak.js # TTS via ElevenLabs JS SDK (recommended, with --play)
80
- β”œβ”€β”€ speak.sh # TTS via curl (all providers: ElevenLabs / OpenAI / Qwen3)
81
- └── compose.sh # Music composition (ElevenLabs)
82
- ```
83
-
84
- Running `--preset ai-girlfriend` additionally includes:
85
-
86
- ```
87
- β”œβ”€β”€ scripts/
88
- β”‚ β”œβ”€β”€ generate-image.sh # Selfie generation (fal.ai Grok Imagine)
89
- β”‚ β”œβ”€β”€ speak.js # TTS via ElevenLabs JS SDK
90
- β”‚ β”œβ”€β”€ speak.sh # TTS via curl (all providers)
91
- β”‚ └── compose.sh # Music composition
92
- └── assets/ # Reference images (placeholder if empty)
88
+ SKILL.md β€” Agent behavior (persona + faculty guides merged)
89
+ soul-injection.md β€” Narrative backstory, injected into SOUL.md
90
+ identity-block.md β€” Name, creature, emoji, vibe, injected into IDENTITY.md
91
+ persona.json β€” Persona definition (for update/list/publish)
92
+ manifest.json β€” Cross-layer metadata (heartbeat, allowedTools, layers, meta)
93
+ soul-state.json β€” Dynamic evolution (relationship, mood, traits)
94
+ README.md
95
+ scripts/ β€” Faculty scripts (TTS, music, selfie β€” varies by preset)
93
96
  ```
94
97
 
95
- ### What Each File Does
96
-
97
- - **SKILL.md** β€” The agent reads this to know how to behave. Contains persona identity, behavior guidelines, and complete faculty instructions
98
- - **soul-injection.md** β€” Appended to `~/.openclaw/workspace/SOUL.md`. Narrative description of _who_ the persona is β€” written in story form, not bullet points
99
- - **identity-block.md** β€” Written to `~/.openclaw/workspace/IDENTITY.md`. Sets the agent's name, creature type, emoji, and vibe
100
- - **soul-state.json** β€” Tracks dynamic persona evolution: relationship stage (stranger β†’ intimate), mood, evolved traits, interests, milestones
101
-
102
- ## How It Differs from Clawra
103
-
104
- [Clawra](https://github.com/SumeLabs/clawra) is a single-purpose product (one girlfriend persona). OpenPersona is a **modular framework**:
105
-
106
- | | Clawra | OpenPersona |
107
- |---|--------|-------------|
108
- | Scope | Single persona (Clawra) | Framework for any persona |
109
- | Architecture | Monolithic | Four-layer (Soul/Body/Faculty/Skill) |
110
- | Faculties | Selfie only | Selfie + Voice + Music + Reminder + Soul Evolution β˜…Exp |
111
- | Voice | None | ElevenLabs (verified) / OpenAI TTS / Qwen3-TTS (⚠️ unverified) |
112
- | Music | None | ElevenLabs Music composition |
113
- | Persona evolution | None | Dynamic relationship/mood/trait tracking |
114
- | Customization | Fork and modify | `persona.json` + `behaviorGuide` + mix faculties |
115
- | Presets | 1 | 4 (extensible) |
116
- | CLI | Install only | 8 commands (create/install/search/publish/...) |
117
- | AI entry point | None | `skills/open-persona/SKILL.md` β€” meta-skill for building & managing persona skill packs |
118
-
119
98
  ## Faculty Reference
120
99
 
121
100
  | Faculty | Dimension | Description | Provider | Env Vars |
122
101
  |---------|-----------|-------------|----------|----------|
123
102
  | **selfie** | expression | AI selfie generation with mirror/direct modes | fal.ai Grok Imagine | `FAL_KEY` |
124
- | **voice** | expression | Text-to-speech voice synthesis | ElevenLabs βœ… / OpenAI TTS ⚠️ / Qwen3-TTS ⚠️ | `ELEVENLABS_API_KEY` (or `TTS_API_KEY`), `TTS_PROVIDER`, `TTS_VOICE_ID`, `TTS_STABILITY`, `TTS_SIMILARITY` |
103
+ | **voice** | expression | Text-to-speech voice synthesis | ElevenLabs / OpenAI TTS / Qwen3-TTS | `ELEVENLABS_API_KEY` (or `TTS_API_KEY`), `TTS_PROVIDER`, `TTS_VOICE_ID`, `TTS_STABILITY`, `TTS_SIMILARITY` |
125
104
  | **music** | expression | AI music composition (instrumental or with lyrics) | ElevenLabs Music | `ELEVENLABS_API_KEY` (shared with voice) |
126
105
  | **reminder** | cognition | Schedule reminders and task management | Built-in | β€” |
127
106
 
@@ -188,7 +167,25 @@ Personas can proactively reach out to users based on **real data**, not fabricat
188
167
  3. **OpenClaw handles scheduling.** The heartbeat config tells OpenClaw _when_ to trigger; the persona's `behaviorGuide` tells the agent _what_ to say.
189
168
  4. **User-configurable.** Users can adjust frequency, quiet hours, and sources to match their preferences.
190
169
 
191
- Samantha ships with heartbeat enabled (`smart` strategy, workspace-digest + upgrade-notify).
170
+ ### Dynamic Sync on Switch/Install
171
+
172
+ Heartbeat config is **automatically synced** to `~/.openclaw/openclaw.json` whenever you install or switch a persona. The gateway immediately adopts the new persona's rhythm β€” no manual config needed.
173
+
174
+ ```bash
175
+ npx openpersona switch samantha # β†’ gateway adopts "smart" heartbeat
176
+ npx openpersona switch life-assistant # β†’ gateway switches to "rational" heartbeat
177
+ ```
178
+
179
+ If the target persona has no heartbeat config, the gateway heartbeat is explicitly disabled to prevent leaking the previous persona's settings.
180
+
181
+ ### Per-Persona Strategies
182
+
183
+ | Persona | Strategy | maxDaily | Rhythm |
184
+ |---------|----------|----------|--------|
185
+ | Samantha | `smart` | 5 | Perceptive β€” speaks when meaningful |
186
+ | AI Girlfriend | `emotional` | 8 | Warm β€” frequent emotional check-ins |
187
+ | Life Assistant | `rational` | 3 | Focused β€” task and schedule driven |
188
+ | Health Butler | `wellness` | 4 | Caring β€” health and habit reminders |
192
189
 
193
190
  ## Persona Harvest β€” Community Contribution
194
191
 
@@ -255,14 +252,9 @@ Without `behaviorGuide`, the SKILL.md only contains general identity and persona
255
252
 
256
253
  ## Persona Switching β€” The Pantheon
257
254
 
258
- Install multiple personas and switch between them instantly:
255
+ Multiple personas can coexist. Switch between them instantly:
259
256
 
260
257
  ```bash
261
- # Install several personas
262
- npx openpersona create --preset samantha --install
263
- npx openpersona create --preset ai-girlfriend --install
264
- npx openpersona create --preset life-assistant --install
265
-
266
258
  # See who's installed
267
259
  npx openpersona list
268
260
  # Samantha (persona-samantha) ← active
@@ -291,11 +283,10 @@ openpersona search Search the registry
291
283
  openpersona uninstall Uninstall a persona
292
284
  openpersona update Update installed personas
293
285
  openpersona list List installed personas
294
- openpersona switch Switch active persona (updates SOUL.md + IDENTITY.md)
295
- openpersona switch Switch active persona
286
+ openpersona switch Switch active persona (updates SOUL.md + IDENTITY.md)
296
287
  openpersona contribute Persona Harvest β€” submit improvements as PR
297
288
  openpersona publish Publish to ClawHub
298
- openpersona reset β˜…Exp Reset soul-state.json
289
+ openpersona reset Reset soul-state.json
299
290
  ```
300
291
 
301
292
  ### Key Options
@@ -349,12 +340,12 @@ layers/ # Shared building blocks (four-layer module pool)
349
340
  voice/ # expression β€” TTS voice synthesis
350
341
  music/ # expression β€” AI music composition (ElevenLabs)
351
342
  reminder/ # cognition β€” reminders and task management
352
- skills/ # Skill layer modules (MVP placeholder)
343
+ skills/ # Skill layer modules (local skill definitions)
353
344
  schemas/ # Four-layer schema definitions
354
345
  templates/ # Mustache rendering templates
355
346
  bin/ # CLI entry point
356
347
  lib/ # Core logic modules
357
- tests/ # Tests (20 passing)
348
+ tests/ # Tests (56 passing)
358
349
  ```
359
350
 
360
351
  ## Development
package/bin/cli.js CHANGED
@@ -24,7 +24,7 @@ const PRESETS_DIR = path.join(PKG_ROOT, 'presets');
24
24
  program
25
25
  .name('openpersona')
26
26
  .description('OpenPersona - Create, manage, and orchestrate agent personas')
27
- .version('0.4.0');
27
+ .version('0.6.0');
28
28
 
29
29
  if (process.argv.length === 2) {
30
30
  process.argv.push('create');
@@ -52,8 +52,8 @@ program
52
52
  persona = JSON.parse(fs.readFileSync(presetPath, 'utf-8'));
53
53
  // Merge cross-layer fields from manifest into persona for generator
54
54
  persona.faculties = manifest.layers.faculties || [];
55
- persona.skills = manifest.layers.skills || {};
56
- persona.embodiments = manifest.layers.body ? [manifest.layers.body] : [];
55
+ persona.skills = manifest.layers.skills || [];
56
+ persona.body = manifest.layers.body || null;
57
57
  persona.allowedTools = manifest.allowedTools || [];
58
58
  persona.version = manifest.version;
59
59
  persona.author = manifest.author;
@@ -1,33 +1,62 @@
1
- # Skill Layer β€” Shared Modules
1
+ # Skills Layer
2
2
 
3
- Pre-built skill templates and scaffolds for persona capabilities.
3
+ This directory holds **local skill definitions** β€” reusable skill packs that any persona can reference by name in its `manifest.json`.
4
4
 
5
- ## MVP Status
5
+ ## Structure
6
6
 
7
- Currently empty. The Skill layer primarily integrates external skills from [ClawHub](https://clawhub.com) and [skills.sh](https://skills.sh) via `manifest.json` references.
7
+ ```
8
+ layers/skills/
9
+ {skill-name}/
10
+ skill.json ← Metadata: name, description, allowedTools, triggers
11
+ SKILL.md ← Behavior guide for the AI agent
12
+ ```
13
+
14
+ ## How It Works
15
+
16
+ When a persona's `manifest.json` declares a skill by name:
17
+
18
+ ```json
19
+ { "name": "weather" }
20
+ ```
21
+
22
+ The generator resolves it through this chain:
23
+
24
+ 1. **Local definition** β€” `layers/skills/weather/skill.json` (if exists, use its metadata + SKILL.md content)
25
+ 2. **Inline fields** β€” `description`, `trigger` written directly in manifest
26
+ 3. **Empty fallback** β€” skill name only, no description (Agent judges usage by name alone)
8
27
 
9
- ## Roadmap
28
+ ## Adding a Skill
10
29
 
11
- - **Skill templates** β€” Scaffolds for common skill patterns (e.g., data-tracking, API-integration)
12
- - **Skill bundles** β€” Curated skill combinations for specific persona types (e.g., "companion-bundle", "assistant-bundle")
30
+ Skills here are **framework-curated capabilities**, not necessarily self-implemented. A skill can:
13
31
 
14
- ## How Skills Work
32
+ - Be a full local implementation (skill.json + SKILL.md)
33
+ - Be a thin wrapper referencing external tools
34
+ - Adapt an existing market skill to the OpenPersona four-layer model
15
35
 
16
- Skills are declared in `presets/*/manifest.json` under `layers.skills`:
36
+ ### Required: `skill.json`
17
37
 
18
38
  ```json
19
39
  {
20
- "layers": {
21
- "skills": {
22
- "clawhub": ["some-skill-slug"],
23
- "skillssh": ["owner/repo"]
24
- }
25
- }
40
+ "name": "weather",
41
+ "description": "Query current weather conditions and forecasts",
42
+ "allowedTools": ["WebFetch", "Bash(curl:*)"],
43
+ "triggers": ["weather", "forecast", "outdoor plans"]
26
44
  }
27
45
  ```
28
46
 
29
- The installer automatically runs `npx clawhub install` or `npx skills add` for each declared skill.
47
+ ### Optional: `SKILL.md`
48
+
49
+ Detailed behavior instructions injected into the generated persona SKILL.md as a full section (instead of a table row).
50
+
51
+ ## Relationship to Other Layers
52
+
53
+ All four layers are categories of capabilities:
30
54
 
31
- ## Contributing
55
+ | Layer | What it provides |
56
+ |-------|-----------------|
57
+ | Soul | Identity, personality, ethical boundaries |
58
+ | Body | Physical/virtual embodiment |
59
+ | Faculty | Perception & expression (voice, selfie, music) |
60
+ | **Skill** | **Actions the agent can take** |
32
61
 
33
- To add a shared skill template, create a directory here with a SKILL.md and any supporting files.
62
+ Skills declared in `manifest.json` can reference local definitions here, or exist purely as inline declarations β€” the framework handles both.
package/lib/generator.js CHANGED
@@ -9,6 +9,7 @@ const { resolvePath, printError } = require('./utils');
9
9
  const PKG_ROOT = path.resolve(__dirname, '..');
10
10
  const TEMPLATES_DIR = path.join(PKG_ROOT, 'templates');
11
11
  const FACULTIES_DIR = path.join(PKG_ROOT, 'layers', 'faculties');
12
+ const SKILLS_DIR = path.join(PKG_ROOT, 'layers', 'skills');
12
13
  const CONSTITUTION_PATH = path.join(PKG_ROOT, 'layers', 'soul', 'constitution.md');
13
14
 
14
15
  const BASE_ALLOWED_TOOLS = ['Bash(openclaw:*)', 'Read', 'Write'];
@@ -60,6 +61,28 @@ function loadFaculty(name) {
60
61
  return faculty;
61
62
  }
62
63
 
64
+ /**
65
+ * Load a skill definition from layers/skills/{name}/.
66
+ * Returns the full skill object if found, or null if not found (fallback to inline).
67
+ */
68
+ function loadSkill(name) {
69
+ const skillDir = path.join(SKILLS_DIR, name);
70
+ const skillJsonPath = path.join(skillDir, 'skill.json');
71
+ if (!fs.existsSync(skillJsonPath)) {
72
+ return null; // No local definition β€” use inline manifest fields
73
+ }
74
+ const skill = JSON.parse(fs.readFileSync(skillJsonPath, 'utf-8'));
75
+ skill._dir = skillDir;
76
+
77
+ // Load SKILL.md content if available
78
+ const skillMdPath = path.join(skillDir, 'SKILL.md');
79
+ if (fs.existsSync(skillMdPath)) {
80
+ skill._content = fs.readFileSync(skillMdPath, 'utf-8');
81
+ }
82
+
83
+ return skill;
84
+ }
85
+
63
86
  function buildBackstory(persona) {
64
87
  const parts = [];
65
88
  parts.push(`You are ${persona.personaName}, ${persona.bio}.`);
@@ -203,8 +226,29 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
203
226
  persona.facultyConfigs = facultyConfigs;
204
227
  }
205
228
 
229
+ // Body layer β€” detect soft-ref (declared with install but not locally available)
230
+ const rawBody = persona.body || persona.embodiments?.[0] || null;
231
+ const softRefBody = rawBody && typeof rawBody === 'object' && rawBody.install
232
+ ? { name: rawBody.name || 'body', install: rawBody.install }
233
+ : null;
234
+
206
235
  const faculties = facultyNames;
207
- const loadedFaculties = faculties.map((name) => loadFaculty(name));
236
+ // Load faculties β€” external ones (with install) may not exist locally yet
237
+ const loadedFaculties = [];
238
+ const softRefFaculties = [];
239
+ for (let i = 0; i < faculties.length; i++) {
240
+ const name = faculties[i];
241
+ const entry = rawFaculties[i];
242
+ if (entry.install) {
243
+ try {
244
+ loadedFaculties.push(loadFaculty(name));
245
+ } catch {
246
+ softRefFaculties.push({ name, install: entry.install });
247
+ }
248
+ } else {
249
+ loadedFaculties.push(loadFaculty(name));
250
+ }
251
+ }
208
252
 
209
253
  // Derived fields
210
254
  persona.backstory = buildBackstory(persona);
@@ -237,6 +281,70 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
237
281
  const skillTpl = loadTemplate('skill');
238
282
  const readmeTpl = loadTemplate('readme');
239
283
 
284
+ // Skill layer β€” resolve before template rendering so soul-injection can reference soft-ref state
285
+ const rawSkills = Array.isArray(persona.skills) ? persona.skills : [];
286
+ const validSkills = rawSkills.filter((s) => s && typeof s === 'object' && s.name);
287
+
288
+ const skillCache = new Map();
289
+ for (const s of validSkills) {
290
+ if (!skillCache.has(s.name)) {
291
+ skillCache.set(s.name, loadSkill(s.name));
292
+ }
293
+ }
294
+
295
+ const resolvedSkills = validSkills.map((s) => {
296
+ const local = skillCache.get(s.name);
297
+ const hasInstall = !!s.install;
298
+ const isResolved = !!local;
299
+
300
+ let status;
301
+ if (isResolved) {
302
+ status = 'resolved';
303
+ } else if (hasInstall) {
304
+ status = 'soft-ref';
305
+ } else {
306
+ status = 'inline-only';
307
+ }
308
+
309
+ return {
310
+ name: s.name,
311
+ description: local?.description || s.description || '',
312
+ trigger: local?.triggers?.join(', ') || s.trigger || '',
313
+ hasContent: !!local?._content,
314
+ content: local?._content || '',
315
+ status,
316
+ install: s.install || '',
317
+ isSoftRef: status === 'soft-ref',
318
+ };
319
+ });
320
+
321
+ const activeSkills = resolvedSkills.filter((s) => !s.isSoftRef);
322
+ const softRefSkills = resolvedSkills.filter((s) => s.isSoftRef);
323
+
324
+ // Collect allowed tools from skills with local definitions
325
+ for (const [, local] of skillCache) {
326
+ if (local?.allowedTools) {
327
+ local.allowedTools.forEach((t) => {
328
+ if (!persona.allowedTools.includes(t)) {
329
+ persona.allowedTools.push(t);
330
+ }
331
+ });
332
+ }
333
+ }
334
+ persona.allowedToolsStr = persona.allowedTools.join(' ');
335
+
336
+ // Self-Awareness System β€” unified gap detection across all layers
337
+ persona.hasSoftRefSkills = softRefSkills.length > 0;
338
+ persona.softRefSkillNames = softRefSkills.map((s) => s.name).join(', ');
339
+ persona.hasSoftRefFaculties = softRefFaculties.length > 0;
340
+ persona.softRefFacultyNames = softRefFaculties.map((f) => f.name).join(', ');
341
+ persona.hasSoftRefBody = !!softRefBody;
342
+ persona.softRefBodyName = softRefBody?.name || '';
343
+ persona.softRefBodyInstall = softRefBody?.install || '';
344
+ persona.heartbeatExpected = persona.heartbeat?.enabled === true;
345
+ persona.heartbeatStrategy = persona.heartbeat?.strategy || 'smart';
346
+ persona.hasSelfAwareness = persona.hasSoftRefSkills || persona.hasSoftRefFaculties || persona.hasSoftRefBody || persona.heartbeatExpected;
347
+
240
348
  const soulInjection = Mustache.render(soulTpl, persona);
241
349
  const identityBlock = Mustache.render(identityTpl, persona);
242
350
  const facultyBlocks = loadedFaculties
@@ -253,6 +361,18 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
253
361
  constitutionContent: constitution.content,
254
362
  constitutionVersion: constitution.version,
255
363
  facultyContent: facultyBlocks,
364
+ hasSkills: activeSkills.length > 0,
365
+ hasSkillTable: activeSkills.filter((s) => !s.hasContent).length > 0,
366
+ skillEntries: activeSkills.filter((s) => !s.hasContent),
367
+ skillBlocks: activeSkills.filter((s) => s.hasContent),
368
+ hasSoftRefSkills: softRefSkills.length > 0,
369
+ softRefSkills,
370
+ hasSoftRefFaculties: softRefFaculties.length > 0,
371
+ softRefFaculties,
372
+ hasSoftRefBody: !!softRefBody,
373
+ softRefBodyName: softRefBody?.name || '',
374
+ softRefBodyInstall: softRefBody?.install || '',
375
+ hasExpectedCapabilities: softRefSkills.length > 0 || softRefFaculties.length > 0 || !!softRefBody,
256
376
  });
257
377
  const readmeMd = Mustache.render(readmeTpl, persona);
258
378
 
@@ -282,6 +402,10 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
282
402
  'skillContent', 'description', 'evolutionEnabled', 'hasSelfie', 'allowedToolsStr',
283
403
  'author', 'version', 'facultyConfigs', 'defaults',
284
404
  '_dir', 'heartbeat',
405
+ 'hasSoftRefSkills', 'softRefSkillNames',
406
+ 'hasSoftRefFaculties', 'softRefFacultyNames',
407
+ 'hasSoftRefBody', 'softRefBodyName', 'softRefBodyInstall',
408
+ 'heartbeatExpected', 'heartbeatStrategy', 'hasSelfAwareness',
285
409
  ];
286
410
  const cleanPersona = { ...persona };
287
411
  for (const key of DERIVED_FIELDS) {
@@ -289,7 +413,7 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
289
413
  }
290
414
  cleanPersona.meta = cleanPersona.meta || {};
291
415
  cleanPersona.meta.framework = 'openpersona';
292
- cleanPersona.meta.frameworkVersion = cleanPersona.meta.frameworkVersion || '0.4.0';
416
+ cleanPersona.meta.frameworkVersion = cleanPersona.meta.frameworkVersion || '0.6.0';
293
417
 
294
418
  // Build defaults from facultyConfigs (rich faculty config β†’ env var mapping)
295
419
  const envDefaults = { ...(persona.defaults?.env || {}) };
@@ -304,9 +428,9 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
304
428
  } else if (fname === 'selfie') {
305
429
  if (cfg.apiKey) envDefaults.FAL_KEY = cfg.apiKey;
306
430
  } else if (fname === 'music') {
307
- // Music shares ELEVENLABS_API_KEY with voice β€” no extra key needed
308
- if (cfg.apiKey) envDefaults.ELEVENLABS_API_KEY = cfg.apiKey;
309
- }
431
+ // Music shares ELEVENLABS_API_KEY with voice β€” no extra key needed
432
+ if (cfg.apiKey) envDefaults.ELEVENLABS_API_KEY = cfg.apiKey;
433
+ }
310
434
  }
311
435
  }
312
436
  if (Object.keys(envDefaults).length > 0) {
@@ -320,6 +444,25 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
320
444
 
321
445
  await fs.writeFile(path.join(skillDir, 'persona.json'), JSON.stringify(cleanPersona, null, 2));
322
446
 
447
+ // manifest.json β€” cross-layer metadata (heartbeat, allowedTools, meta, etc.)
448
+ const manifest = {
449
+ name: persona.slug,
450
+ version: persona.version || '0.1.0',
451
+ author: persona.author || 'openpersona',
452
+ layers: {
453
+ soul: './persona.json',
454
+ body: persona.body || persona.embodiments?.[0] || null,
455
+ faculties: rawFaculties,
456
+ skills: persona.skills || [],
457
+ },
458
+ allowedTools: cleanPersona.allowedTools || [],
459
+ };
460
+ if (persona.heartbeat) {
461
+ manifest.heartbeat = persona.heartbeat;
462
+ }
463
+ manifest.meta = cleanPersona.meta || { framework: 'openpersona', frameworkVersion: '0.6.0' };
464
+ await fs.writeFile(path.join(skillDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
465
+
323
466
  // soul-state.json (if evolution enabled)
324
467
  if (evolutionEnabled) {
325
468
  const soulStateTpl = fs.readFileSync(
package/lib/installer.js CHANGED
@@ -3,7 +3,7 @@
3
3
  */
4
4
  const path = require('path');
5
5
  const fs = require('fs-extra');
6
- const { OP_HOME, OP_SKILLS_DIR, OP_WORKSPACE, printError, printWarning, printSuccess, printInfo } = require('./utils');
6
+ const { OP_HOME, OP_SKILLS_DIR, OP_WORKSPACE, printError, printWarning, printSuccess, printInfo, syncHeartbeat, installAllExternal } = require('./utils');
7
7
 
8
8
  const SOUL_PATH = path.join(OP_WORKSPACE, 'SOUL.md');
9
9
  const IDENTITY_PATH = path.join(OP_WORKSPACE, 'IDENTITY.md');
@@ -80,6 +80,16 @@ async function install(skillDir, options = {}) {
80
80
  val.active = (key === `persona-${slug}`);
81
81
  }
82
82
  }
83
+
84
+ // Sync heartbeat from persona's manifest into global config
85
+ const manifestPath = path.join(destDir, 'manifest.json');
86
+ const { synced, heartbeat } = syncHeartbeat(config, manifestPath);
87
+ if (synced) {
88
+ printSuccess(`Heartbeat synced: strategy=${heartbeat.strategy}, maxDaily=${heartbeat.maxDaily}`);
89
+ } else {
90
+ printInfo('Heartbeat disabled (persona has no heartbeat config)');
91
+ }
92
+
83
93
  await fs.writeFile(OPENCLAW_JSON, JSON.stringify(config, null, 2));
84
94
 
85
95
  // SOUL.md injection (using generic markers for clean switching)
@@ -119,34 +129,13 @@ async function install(skillDir, options = {}) {
119
129
  printSuccess('Updated IDENTITY.md');
120
130
  }
121
131
 
122
- // Install external skills (skills.clawhub, skills.skillssh)
123
- // Security: validate skill names to prevent shell injection
124
- const SAFE_SKILL_NAME = /^[a-zA-Z0-9@][a-zA-Z0-9@/_.-]*$/;
125
- const { execSync } = require('child_process');
126
- const clawhub = persona.skills?.clawhub || [];
127
- const skillssh = persona.skills?.skillssh || [];
128
- for (const s of clawhub) {
129
- if (!SAFE_SKILL_NAME.test(s)) {
130
- printWarning(`Skipping invalid ClawHub skill name: ${s}`);
131
- continue;
132
- }
133
- try {
134
- execSync(`npx clawhub@latest install ${s}`, { stdio: 'inherit' });
135
- printSuccess(`Installed ClawHub skill: ${s}`);
136
- } catch (e) {
137
- printWarning(`Failed to install ClawHub skill ${s}: ${e.message}`);
138
- }
139
- }
140
- for (const s of skillssh) {
141
- if (!SAFE_SKILL_NAME.test(s)) {
142
- printWarning(`Skipping invalid skills.sh name: ${s}`);
143
- continue;
144
- }
132
+ // Install external dependencies across all four layers (read from manifest, not persona.json)
133
+ if (fs.existsSync(manifestPath)) {
145
134
  try {
146
- execSync(`npx skills add ${s}`, { stdio: 'inherit' });
147
- printSuccess(`Installed skills.sh: ${s}`);
148
- } catch (e) {
149
- printWarning(`Failed to install skills.sh ${s}: ${e.message}`);
135
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
136
+ installAllExternal(manifest.layers || {});
137
+ } catch {
138
+ // Malformed manifest β€” skip external installs
150
139
  }
151
140
  }
152
141