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 +73 -82
- package/bin/cli.js +3 -3
- package/layers/skills/README.md +47 -18
- package/lib/generator.js +148 -5
- package/lib/installer.js +17 -28
- package/lib/switcher.js +22 -2
- package/lib/utils.js +117 -0
- package/package.json +1 -1
- package/presets/ai-girlfriend/manifest.json +14 -6
- package/presets/health-butler/manifest.json +16 -6
- package/presets/health-butler/persona.json +1 -1
- package/presets/life-assistant/manifest.json +16 -6
- package/presets/life-assistant/persona.json +1 -1
- package/presets/samantha/manifest.json +7 -5
- package/schemas/manifest.schema.json +70 -10
- package/schemas/skill/skill-declaration.spec.md +81 -3
- package/skills/open-persona/SKILL.md +23 -8
- package/templates/skill.template.md +53 -0
- package/templates/soul-injection.template.md +28 -0
package/README.md
CHANGED
|
@@ -1,32 +1,51 @@
|
|
|
1
|
-
# OpenPersona
|
|
1
|
+
# OpenPersona π¦
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The open framework for creating and orchestrating dynamic agent personas.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
##
|
|
7
|
+
## π Live Demo
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
14
|
-
npx openpersona create --preset ai-girlfriend --install
|
|
12
|
+
## Table of Contents
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 |
|
|
62
|
-
| **ai-girlfriend** | Luna β A 22-year-old pianist turned developer from coastal Oregon. | selfie, voice, music | Rich
|
|
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
|
-
|
|
84
|
+
`npx openpersona create --preset samantha` generates a self-contained skill pack:
|
|
69
85
|
|
|
70
86
|
```
|
|
71
87
|
persona-samantha/
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (
|
|
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 (
|
|
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.
|
|
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.
|
|
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;
|
package/layers/skills/README.md
CHANGED
|
@@ -1,33 +1,62 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Skills Layer
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
This directory holds **local skill definitions** β reusable skill packs that any persona can reference by name in its `manifest.json`.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Structure
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
##
|
|
28
|
+
## Adding a Skill
|
|
10
29
|
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
+
### Required: `skill.json`
|
|
17
37
|
|
|
18
38
|
```json
|
|
19
39
|
{
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
308
|
-
|
|
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
|
|
123
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
} catch
|
|
149
|
-
|
|
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
|
|