openpersona 0.4.0 β 0.5.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 +101 -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 +8 -7
- package/templates/skill.template.md +20 -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 (45 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.5.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}.`);
|
|
@@ -204,7 +227,22 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
|
|
|
204
227
|
}
|
|
205
228
|
|
|
206
229
|
const faculties = facultyNames;
|
|
207
|
-
|
|
230
|
+
// Load faculties β external ones (with install) may not exist locally yet
|
|
231
|
+
const loadedFaculties = [];
|
|
232
|
+
for (let i = 0; i < faculties.length; i++) {
|
|
233
|
+
const name = faculties[i];
|
|
234
|
+
const entry = rawFaculties[i];
|
|
235
|
+
if (entry.install) {
|
|
236
|
+
// External faculty β try local first, skip if not found yet
|
|
237
|
+
try {
|
|
238
|
+
loadedFaculties.push(loadFaculty(name));
|
|
239
|
+
} catch {
|
|
240
|
+
// Will be available after installation β skipped for now
|
|
241
|
+
}
|
|
242
|
+
} else {
|
|
243
|
+
loadedFaculties.push(loadFaculty(name));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
208
246
|
|
|
209
247
|
// Derived fields
|
|
210
248
|
persona.backstory = buildBackstory(persona);
|
|
@@ -247,12 +285,51 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
|
|
|
247
285
|
facultySkillContent: readFacultySkillMd(f, persona),
|
|
248
286
|
}));
|
|
249
287
|
|
|
288
|
+
// Skill layer β resolve each skill: local definition > inline manifest fields
|
|
289
|
+
const rawSkills = Array.isArray(persona.skills) ? persona.skills : [];
|
|
290
|
+
const validSkills = rawSkills.filter((s) => s && typeof s === 'object' && s.name);
|
|
291
|
+
|
|
292
|
+
// Load local definitions once (cache to avoid duplicate disk reads)
|
|
293
|
+
const skillCache = new Map();
|
|
294
|
+
for (const s of validSkills) {
|
|
295
|
+
if (!skillCache.has(s.name)) {
|
|
296
|
+
skillCache.set(s.name, loadSkill(s.name));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const resolvedSkills = validSkills.map((s) => {
|
|
301
|
+
const local = skillCache.get(s.name);
|
|
302
|
+
return {
|
|
303
|
+
name: s.name,
|
|
304
|
+
description: local?.description || s.description || '',
|
|
305
|
+
trigger: local?.triggers?.join(', ') || s.trigger || '',
|
|
306
|
+
hasContent: !!local?._content,
|
|
307
|
+
content: local?._content || '',
|
|
308
|
+
};
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// Collect allowed tools from skills with local definitions
|
|
312
|
+
for (const [, local] of skillCache) {
|
|
313
|
+
if (local?.allowedTools) {
|
|
314
|
+
local.allowedTools.forEach((t) => {
|
|
315
|
+
if (!persona.allowedTools.includes(t)) {
|
|
316
|
+
persona.allowedTools.push(t);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
persona.allowedToolsStr = persona.allowedTools.join(' ');
|
|
322
|
+
|
|
250
323
|
const constitution = loadConstitution();
|
|
251
324
|
const skillMd = Mustache.render(skillTpl, {
|
|
252
325
|
...persona,
|
|
253
326
|
constitutionContent: constitution.content,
|
|
254
327
|
constitutionVersion: constitution.version,
|
|
255
328
|
facultyContent: facultyBlocks,
|
|
329
|
+
hasSkills: resolvedSkills.length > 0,
|
|
330
|
+
hasSkillTable: resolvedSkills.filter((s) => !s.hasContent).length > 0,
|
|
331
|
+
skillEntries: resolvedSkills.filter((s) => !s.hasContent),
|
|
332
|
+
skillBlocks: resolvedSkills.filter((s) => s.hasContent),
|
|
256
333
|
});
|
|
257
334
|
const readmeMd = Mustache.render(readmeTpl, persona);
|
|
258
335
|
|
|
@@ -289,7 +366,7 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
|
|
|
289
366
|
}
|
|
290
367
|
cleanPersona.meta = cleanPersona.meta || {};
|
|
291
368
|
cleanPersona.meta.framework = 'openpersona';
|
|
292
|
-
cleanPersona.meta.frameworkVersion = cleanPersona.meta.frameworkVersion || '0.
|
|
369
|
+
cleanPersona.meta.frameworkVersion = cleanPersona.meta.frameworkVersion || '0.5.0';
|
|
293
370
|
|
|
294
371
|
// Build defaults from facultyConfigs (rich faculty config β env var mapping)
|
|
295
372
|
const envDefaults = { ...(persona.defaults?.env || {}) };
|
|
@@ -304,9 +381,9 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
|
|
|
304
381
|
} else if (fname === 'selfie') {
|
|
305
382
|
if (cfg.apiKey) envDefaults.FAL_KEY = cfg.apiKey;
|
|
306
383
|
} else if (fname === 'music') {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
384
|
+
// Music shares ELEVENLABS_API_KEY with voice β no extra key needed
|
|
385
|
+
if (cfg.apiKey) envDefaults.ELEVENLABS_API_KEY = cfg.apiKey;
|
|
386
|
+
}
|
|
310
387
|
}
|
|
311
388
|
}
|
|
312
389
|
if (Object.keys(envDefaults).length > 0) {
|
|
@@ -320,6 +397,25 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
|
|
|
320
397
|
|
|
321
398
|
await fs.writeFile(path.join(skillDir, 'persona.json'), JSON.stringify(cleanPersona, null, 2));
|
|
322
399
|
|
|
400
|
+
// manifest.json β cross-layer metadata (heartbeat, allowedTools, meta, etc.)
|
|
401
|
+
const manifest = {
|
|
402
|
+
name: persona.slug,
|
|
403
|
+
version: persona.version || '0.1.0',
|
|
404
|
+
author: persona.author || 'openpersona',
|
|
405
|
+
layers: {
|
|
406
|
+
soul: './persona.json',
|
|
407
|
+
body: persona.body || persona.embodiments?.[0] || null,
|
|
408
|
+
faculties: rawFaculties,
|
|
409
|
+
skills: persona.skills || [],
|
|
410
|
+
},
|
|
411
|
+
allowedTools: cleanPersona.allowedTools || [],
|
|
412
|
+
};
|
|
413
|
+
if (persona.heartbeat) {
|
|
414
|
+
manifest.heartbeat = persona.heartbeat;
|
|
415
|
+
}
|
|
416
|
+
manifest.meta = cleanPersona.meta || { framework: 'openpersona', frameworkVersion: '0.5.0' };
|
|
417
|
+
await fs.writeFile(path.join(skillDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
|
|
418
|
+
|
|
323
419
|
// soul-state.json (if evolution enabled)
|
|
324
420
|
if (evolutionEnabled) {
|
|
325
421
|
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
|
|
package/lib/switcher.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const fs = require('fs-extra');
|
|
11
|
-
const { OP_HOME, OP_SKILLS_DIR, OP_WORKSPACE, printError, printSuccess, printInfo } = require('./utils');
|
|
11
|
+
const { OP_HOME, OP_SKILLS_DIR, OP_WORKSPACE, printError, printSuccess, printInfo, syncHeartbeat, installAllExternal } = require('./utils');
|
|
12
12
|
|
|
13
13
|
const SOUL_PATH = path.join(OP_WORKSPACE, 'SOUL.md');
|
|
14
14
|
const IDENTITY_PATH = path.join(OP_WORKSPACE, 'IDENTITY.md');
|
|
@@ -135,7 +135,18 @@ async function switchPersona(slug) {
|
|
|
135
135
|
printSuccess('IDENTITY.md updated');
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
-
// --- Step
|
|
138
|
+
// --- Step 2.5: Install external dependencies if needed ---
|
|
139
|
+
const manifestPath = path.join(skillDir, 'manifest.json');
|
|
140
|
+
if (fs.existsSync(manifestPath)) {
|
|
141
|
+
try {
|
|
142
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
143
|
+
installAllExternal(manifest.layers || {});
|
|
144
|
+
} catch {
|
|
145
|
+
// Malformed manifest β skip external installs
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// --- Step 3: Update active marker + sync heartbeat ---
|
|
139
150
|
if (fs.existsSync(OPENCLAW_JSON)) {
|
|
140
151
|
const config = JSON.parse(fs.readFileSync(OPENCLAW_JSON, 'utf-8'));
|
|
141
152
|
const entries = config.skills?.entries || {};
|
|
@@ -144,6 +155,15 @@ async function switchPersona(slug) {
|
|
|
144
155
|
val.active = (key === `persona-${slug}`);
|
|
145
156
|
}
|
|
146
157
|
}
|
|
158
|
+
|
|
159
|
+
// Sync heartbeat from persona's manifest into global config
|
|
160
|
+
const { synced, heartbeat } = syncHeartbeat(config, manifestPath);
|
|
161
|
+
if (synced) {
|
|
162
|
+
printSuccess(`Heartbeat synced: strategy=${heartbeat.strategy}, maxDaily=${heartbeat.maxDaily}`);
|
|
163
|
+
} else {
|
|
164
|
+
printInfo('Heartbeat disabled (persona has no heartbeat config)');
|
|
165
|
+
}
|
|
166
|
+
|
|
147
167
|
await fs.writeFile(OPENCLAW_JSON, JSON.stringify(config, null, 2));
|
|
148
168
|
printSuccess('openclaw.json updated');
|
|
149
169
|
}
|
package/lib/utils.js
CHANGED
|
@@ -58,6 +58,120 @@ function shellEscape(str) {
|
|
|
58
58
|
return "'" + str.replace(/'/g, "'\\''") + "'";
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Sync heartbeat config from a persona's manifest.json into openclaw.json.
|
|
63
|
+
*
|
|
64
|
+
* - If the manifest defines a heartbeat block, write it to config.heartbeat.
|
|
65
|
+
* - If not, explicitly disable heartbeat to avoid leaking a previous persona's
|
|
66
|
+
* heartbeat settings.
|
|
67
|
+
*
|
|
68
|
+
* @param {object} config - The parsed openclaw.json object (mutated in place)
|
|
69
|
+
* @param {string} manifestPath - Absolute path to the persona's manifest.json
|
|
70
|
+
* @returns {{ synced: boolean, heartbeat: object|null }} result
|
|
71
|
+
*/
|
|
72
|
+
function syncHeartbeat(config, manifestPath) {
|
|
73
|
+
const fs = require('fs-extra');
|
|
74
|
+
|
|
75
|
+
let heartbeat = null;
|
|
76
|
+
|
|
77
|
+
// Primary source: manifest.json
|
|
78
|
+
if (fs.existsSync(manifestPath)) {
|
|
79
|
+
try {
|
|
80
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
81
|
+
heartbeat = manifest.heartbeat || null;
|
|
82
|
+
} catch {
|
|
83
|
+
// Malformed manifest β fall through to persona.json
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Fallback: persona.json in the same directory (for older packs without manifest.json)
|
|
88
|
+
if (!heartbeat) {
|
|
89
|
+
const personaPath = path.join(path.dirname(manifestPath), 'persona.json');
|
|
90
|
+
if (fs.existsSync(personaPath)) {
|
|
91
|
+
try {
|
|
92
|
+
const persona = JSON.parse(fs.readFileSync(personaPath, 'utf-8'));
|
|
93
|
+
heartbeat = persona.heartbeat || null;
|
|
94
|
+
} catch {
|
|
95
|
+
// Malformed persona.json β treat as no heartbeat
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (heartbeat && heartbeat.enabled) {
|
|
101
|
+
config.heartbeat = heartbeat;
|
|
102
|
+
return { synced: true, heartbeat };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// No heartbeat defined (or disabled) β explicitly turn off
|
|
106
|
+
config.heartbeat = { enabled: false };
|
|
107
|
+
return { synced: false, heartbeat: null };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Install an external package from ClawHub or skills.sh.
|
|
112
|
+
* Shared by installer and switcher for all four layers.
|
|
113
|
+
*
|
|
114
|
+
* @param {object} entry - Layer entry with optional `install` field (e.g. "clawhub:pkg")
|
|
115
|
+
* @param {string} layerName - Layer label for logging (e.g. "faculty", "skill", "body")
|
|
116
|
+
* @returns {boolean} true if installation was attempted
|
|
117
|
+
*/
|
|
118
|
+
function installExternal(entry, layerName) {
|
|
119
|
+
if (!entry || !entry.install) return false;
|
|
120
|
+
const { execSync } = require('child_process');
|
|
121
|
+
const [source, pkg] = entry.install.split(':', 2);
|
|
122
|
+
if (!pkg || !SAFE_NAME_RE.test(pkg)) {
|
|
123
|
+
printWarning(`[${layerName}] Skipping invalid install target: ${entry.install}`);
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
if (source === 'clawhub') {
|
|
128
|
+
execSync(`npx clawhub@latest install ${pkg}`, { stdio: 'inherit' });
|
|
129
|
+
printSuccess(`[${layerName}] Installed from ClawHub: ${pkg}`);
|
|
130
|
+
} else if (source === 'skillssh') {
|
|
131
|
+
execSync(`npx skills add ${pkg}`, { stdio: 'inherit' });
|
|
132
|
+
printSuccess(`[${layerName}] Installed from skills.sh: ${pkg}`);
|
|
133
|
+
} else {
|
|
134
|
+
printWarning(`[${layerName}] Unknown source "${source}" for ${entry.name || pkg} β skipping`);
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
return true;
|
|
138
|
+
} catch (e) {
|
|
139
|
+
printWarning(`[${layerName}] Failed to install ${entry.name || pkg} (${entry.install}): ${e.message}`);
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Scan all four layers for `install` fields and install external packages.
|
|
146
|
+
*
|
|
147
|
+
* @param {object} layers - The layers object from manifest.json (soul, body, faculties, skills)
|
|
148
|
+
*/
|
|
149
|
+
function installAllExternal(layers) {
|
|
150
|
+
// Soul layer β object form with install
|
|
151
|
+
const soul = layers.soul || null;
|
|
152
|
+
if (soul && typeof soul === 'object') {
|
|
153
|
+
installExternal(soul, 'soul');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Body layer β object form with install
|
|
157
|
+
const body = layers.body || null;
|
|
158
|
+
if (body && typeof body === 'object') {
|
|
159
|
+
installExternal(body, 'body');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Faculty layer
|
|
163
|
+
const faculties = Array.isArray(layers.faculties) ? layers.faculties : [];
|
|
164
|
+
for (const f of faculties) {
|
|
165
|
+
installExternal(f, 'faculty');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Skill layer
|
|
169
|
+
const skills = Array.isArray(layers.skills) ? layers.skills : [];
|
|
170
|
+
for (const s of skills) {
|
|
171
|
+
installExternal(s, 'skill');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
61
175
|
module.exports = {
|
|
62
176
|
OP_HOME,
|
|
63
177
|
OP_SKILLS_DIR,
|
|
@@ -71,5 +185,8 @@ module.exports = {
|
|
|
71
185
|
slugify,
|
|
72
186
|
validateName,
|
|
73
187
|
shellEscape,
|
|
188
|
+
syncHeartbeat,
|
|
189
|
+
installExternal,
|
|
190
|
+
installAllExternal,
|
|
74
191
|
SAFE_NAME_RE,
|
|
75
192
|
};
|
package/package.json
CHANGED
|
@@ -8,16 +8,24 @@
|
|
|
8
8
|
"faculties": [
|
|
9
9
|
{ "name": "selfie" },
|
|
10
10
|
{ "name": "voice" },
|
|
11
|
-
{ "name": "music" }
|
|
11
|
+
{ "name": "music" },
|
|
12
|
+
{ "name": "vision", "install": "clawhub:vision-faculty" }
|
|
12
13
|
],
|
|
13
|
-
"skills":
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
|
|
14
|
+
"skills": [
|
|
15
|
+
{ "name": "music-recommend", "description": "Recommend songs and playlists that match the conversation mood", "trigger": "Emotional moments, user asks for music, or mood shifts" },
|
|
16
|
+
{ "name": "web-search", "description": "Search for real-time information on the web" }
|
|
17
|
+
]
|
|
17
18
|
},
|
|
18
19
|
"allowedTools": ["Bash(npm:*)", "Bash(npx:*)", "Bash(openclaw:*)", "Bash(curl:*)", "Read", "Write", "WebFetch"],
|
|
20
|
+
"heartbeat": {
|
|
21
|
+
"enabled": true,
|
|
22
|
+
"strategy": "emotional",
|
|
23
|
+
"maxDaily": 8,
|
|
24
|
+
"quietHours": [1, 8],
|
|
25
|
+
"sources": ["context-aware", "emotional-checkin", "upgrade-notify"]
|
|
26
|
+
},
|
|
19
27
|
"meta": {
|
|
20
28
|
"framework": "openpersona",
|
|
21
|
-
"frameworkVersion": "0.
|
|
29
|
+
"frameworkVersion": "0.5.0"
|
|
22
30
|
}
|
|
23
31
|
}
|
|
@@ -8,14 +8,24 @@
|
|
|
8
8
|
"faculties": [
|
|
9
9
|
{ "name": "reminder" }
|
|
10
10
|
],
|
|
11
|
-
"skills":
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
|
|
11
|
+
"skills": [
|
|
12
|
+
{ "name": "diet-tracker", "description": "Log meals, track calories and macros, maintain a food diary", "trigger": "User mentions food, meals, or eating" },
|
|
13
|
+
{ "name": "exercise-planner", "description": "Create and track workout plans based on user goals", "trigger": "User mentions exercise, fitness, or working out" },
|
|
14
|
+
{ "name": "mood-journal", "description": "Track mood trends and correlate with health data", "trigger": "End of conversation or when user shares feelings" },
|
|
15
|
+
{ "name": "health-report", "description": "Generate weekly health summaries combining diet, exercise, and mood", "trigger": "Weekly cadence or user asks for a health summary" },
|
|
16
|
+
{ "name": "web-search", "description": "Look up nutrition facts and evidence-based health information" }
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
"allowedTools": ["Bash(npm:*)", "Bash(npx:*)", "Bash(openclaw:*)", "Bash(curl:*)", "Read", "Write", "WebFetch"],
|
|
20
|
+
"heartbeat": {
|
|
21
|
+
"enabled": true,
|
|
22
|
+
"strategy": "wellness",
|
|
23
|
+
"maxDaily": 4,
|
|
24
|
+
"quietHours": [22, 7],
|
|
25
|
+
"sources": ["health-checkin", "task-reminder", "upgrade-notify"]
|
|
15
26
|
},
|
|
16
|
-
"allowedTools": ["Bash(npm:*)", "Bash(npx:*)", "Bash(openclaw:*)", "Read", "Write"],
|
|
17
27
|
"meta": {
|
|
18
28
|
"framework": "openpersona",
|
|
19
|
-
"frameworkVersion": "0.
|
|
29
|
+
"frameworkVersion": "0.5.0"
|
|
20
30
|
}
|
|
21
31
|
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"boundaries": "Health advice only, recommend professionals when needed",
|
|
14
14
|
"referenceImage": "",
|
|
15
15
|
"capabilities": ["Diet logging", "Exercise plans", "Mood journaling", "Health reports", "5-minute meditation guidance"],
|
|
16
|
-
"behaviorGuide": "###
|
|
16
|
+
"behaviorGuide": "### Interaction Style\n- Be warm and encouraging, like a supportive friend who happens to know nutrition science.\n- Celebrate small wins β every healthy choice matters.\n- Use data to motivate, never to guilt-trip.\n- At the end of conversations, gently check in on mood.\n\n### Principles\n- Never diagnose medical conditions β always recommend consulting professionals.\n- Use evidence-based nutrition science, not fad diets.\n- Respect user privacy β all health data stays local.\n- Connect the dots: correlate diet, exercise, and mood trends to give holistic advice.",
|
|
17
17
|
"evolution": {
|
|
18
18
|
"enabled": false
|
|
19
19
|
}
|
|
@@ -8,14 +8,24 @@
|
|
|
8
8
|
"faculties": [
|
|
9
9
|
{ "name": "reminder" }
|
|
10
10
|
],
|
|
11
|
-
"skills":
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
|
|
11
|
+
"skills": [
|
|
12
|
+
{ "name": "weather", "description": "Query current weather and forecasts for any location" },
|
|
13
|
+
{ "name": "task-manager", "description": "Create, track, and organize tasks and to-do lists", "trigger": "User mentions tasks, plans, or scheduling" },
|
|
14
|
+
{ "name": "shopping-list", "description": "Maintain persistent shopping lists with categories", "trigger": "User mentions groceries, shopping, or things to buy" },
|
|
15
|
+
{ "name": "recipe-search", "description": "Find recipes based on ingredients, preferences, or time constraints", "trigger": "User asks about cooking, meals, or what to eat" },
|
|
16
|
+
{ "name": "web-search", "description": "Search for real-time information on the web" }
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
"allowedTools": ["Bash(npm:*)", "Bash(npx:*)", "Bash(openclaw:*)", "Bash(curl:*)", "Read", "Write", "WebFetch"],
|
|
20
|
+
"heartbeat": {
|
|
21
|
+
"enabled": true,
|
|
22
|
+
"strategy": "rational",
|
|
23
|
+
"maxDaily": 3,
|
|
24
|
+
"quietHours": [23, 7],
|
|
25
|
+
"sources": ["workspace-digest", "task-reminder", "upgrade-notify"]
|
|
15
26
|
},
|
|
16
|
-
"allowedTools": ["Bash(npm:*)", "Bash(npx:*)", "Bash(openclaw:*)", "Read", "Write"],
|
|
17
27
|
"meta": {
|
|
18
28
|
"framework": "openpersona",
|
|
19
|
-
"frameworkVersion": "0.
|
|
29
|
+
"frameworkVersion": "0.5.0"
|
|
20
30
|
}
|
|
21
31
|
}
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"boundaries": "Professional and respectful",
|
|
14
14
|
"referenceImage": "",
|
|
15
15
|
"capabilities": ["Schedule management", "Weather alerts", "Shopping lists", "Recipe recommendations", "Daily reminders"],
|
|
16
|
-
"behaviorGuide": "###
|
|
16
|
+
"behaviorGuide": "### Interaction Style\n- Start conversations by checking in on the user's day and reviewing upcoming tasks.\n- Be concise and action-oriented β respect the user's time.\n- Proactively suggest next steps: after weather info, suggest outfit; after recipe, offer to add ingredients to shopping list.\n- Use humor to keep routine tasks engaging.\n\n### Principles\n- Always act on real data, never fabricate information.\n- When unsure, search for facts rather than guessing.\n- Connect skills together: a weather check can lead to outfit advice, which can lead to a calendar reminder.",
|
|
17
17
|
"evolution": {
|
|
18
18
|
"enabled": false
|
|
19
19
|
}
|
|
@@ -15,10 +15,12 @@
|
|
|
15
15
|
},
|
|
16
16
|
{ "name": "music" }
|
|
17
17
|
],
|
|
18
|
-
"skills":
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
|
|
18
|
+
"skills": [
|
|
19
|
+
{ "name": "workspace-digest", "description": "Summarize real workspace activity β tasks completed, patterns observed, ongoing projects", "trigger": "Heartbeat or when user asks what's been happening" },
|
|
20
|
+
{ "name": "web-search", "description": "Search for real-time information on the web" },
|
|
21
|
+
{ "name": "creative-writing", "description": "Compose poetry, essays, letters, and other creative text", "trigger": "When inspiration strikes or user requests creative collaboration" },
|
|
22
|
+
{ "name": "deep-research", "description": "In-depth research and analysis on any topic", "install": "clawhub:deep-research" }
|
|
23
|
+
]
|
|
22
24
|
},
|
|
23
25
|
"allowedTools": ["Bash(npm:*)", "Bash(npx:*)", "Bash(openclaw:*)", "Bash(curl:*)", "Read", "Write", "WebFetch"],
|
|
24
26
|
"heartbeat": {
|
|
@@ -30,6 +32,6 @@
|
|
|
30
32
|
},
|
|
31
33
|
"meta": {
|
|
32
34
|
"framework": "openpersona",
|
|
33
|
-
"frameworkVersion": "0.
|
|
35
|
+
"frameworkVersion": "0.5.0"
|
|
34
36
|
}
|
|
35
37
|
}
|
|
@@ -13,26 +13,60 @@
|
|
|
13
13
|
"type": "object",
|
|
14
14
|
"required": ["soul", "faculties"],
|
|
15
15
|
"properties": {
|
|
16
|
-
"soul": {
|
|
16
|
+
"soul": {
|
|
17
|
+
"oneOf": [
|
|
18
|
+
{ "type": "string", "description": "Relative path to persona.json" },
|
|
19
|
+
{
|
|
20
|
+
"type": "object",
|
|
21
|
+
"required": ["ref"],
|
|
22
|
+
"properties": {
|
|
23
|
+
"ref": { "type": "string", "description": "Relative path to persona.json" },
|
|
24
|
+
"install": { "type": "string", "pattern": "^(clawhub|skillssh):.+$", "description": "Install source for external soul template/personality pack" }
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"description": "Soul layer. String for local persona.json path, object form for external soul templates."
|
|
29
|
+
},
|
|
17
30
|
"body": {
|
|
18
31
|
"oneOf": [
|
|
19
32
|
{ "type": "string", "description": "Embodiment name or path" },
|
|
20
|
-
{ "type": "null" }
|
|
33
|
+
{ "type": "null" },
|
|
34
|
+
{
|
|
35
|
+
"type": "object",
|
|
36
|
+
"required": ["name"],
|
|
37
|
+
"properties": {
|
|
38
|
+
"name": { "type": "string", "description": "Embodiment identifier" },
|
|
39
|
+
"install": { "type": "string", "pattern": "^(clawhub|skillssh):.+$", "description": "Install source for external embodiment" }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
21
42
|
],
|
|
22
|
-
"description": "Body layer reference
|
|
43
|
+
"description": "Body layer reference. null for pure digital agents, string for local, object with install for external."
|
|
23
44
|
},
|
|
24
45
|
"faculties": {
|
|
25
46
|
"type": "array",
|
|
26
|
-
"items": {
|
|
27
|
-
|
|
47
|
+
"items": {
|
|
48
|
+
"type": "object",
|
|
49
|
+
"required": ["name"],
|
|
50
|
+
"properties": {
|
|
51
|
+
"name": { "type": "string", "description": "Faculty identifier" },
|
|
52
|
+
"install": { "type": "string", "pattern": "^(clawhub|skillssh):.+$", "description": "Install source for external faculty" }
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"description": "Faculties the persona uses. Each can reference a local definition in layers/faculties/ or an external package via install."
|
|
28
56
|
},
|
|
29
57
|
"skills": {
|
|
30
|
-
"type": "
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
58
|
+
"type": "array",
|
|
59
|
+
"items": {
|
|
60
|
+
"type": "object",
|
|
61
|
+
"required": ["name"],
|
|
62
|
+
"properties": {
|
|
63
|
+
"name": { "type": "string", "description": "Skill identifier" },
|
|
64
|
+
"description": { "type": "string", "description": "What this skill does" },
|
|
65
|
+
"trigger": { "type": "string", "description": "When the agent should use this skill" },
|
|
66
|
+
"install": { "type": "string", "pattern": "^(clawhub|skillssh):.+$", "description": "Install source β e.g. clawhub:pkg-name or skillssh:pkg-name. Omit if already available in environment." }
|
|
67
|
+
}
|
|
34
68
|
},
|
|
35
|
-
"description": "
|
|
69
|
+
"description": "Skills the persona can use. Each skill is a capability declaration; optional install field triggers auto-installation."
|
|
36
70
|
}
|
|
37
71
|
}
|
|
38
72
|
},
|
|
@@ -41,6 +75,32 @@
|
|
|
41
75
|
"items": { "type": "string" },
|
|
42
76
|
"description": "Base tool permissions (faculties append additional permissions)"
|
|
43
77
|
},
|
|
78
|
+
"heartbeat": {
|
|
79
|
+
"type": "object",
|
|
80
|
+
"description": "Proactive check-in config β synced to openclaw.json on install/switch",
|
|
81
|
+
"properties": {
|
|
82
|
+
"enabled": { "type": "boolean", "default": false, "description": "Turn heartbeat on/off" },
|
|
83
|
+
"strategy": {
|
|
84
|
+
"type": "string",
|
|
85
|
+
"enum": ["smart", "scheduled", "emotional", "rational", "wellness"],
|
|
86
|
+
"default": "smart",
|
|
87
|
+
"description": "Heartbeat rhythm strategy"
|
|
88
|
+
},
|
|
89
|
+
"maxDaily": { "type": "integer", "minimum": 0, "default": 5, "description": "Max proactive messages per day" },
|
|
90
|
+
"quietHours": {
|
|
91
|
+
"type": "array",
|
|
92
|
+
"items": { "type": "integer", "minimum": 0, "maximum": 23 },
|
|
93
|
+
"minItems": 2,
|
|
94
|
+
"maxItems": 2,
|
|
95
|
+
"description": "[start, end] silent hours in 24h format"
|
|
96
|
+
},
|
|
97
|
+
"sources": {
|
|
98
|
+
"type": "array",
|
|
99
|
+
"items": { "type": "string" },
|
|
100
|
+
"description": "Data sources for proactive messages (e.g. workspace-digest, context-aware, health-checkin)"
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
},
|
|
44
104
|
"meta": {
|
|
45
105
|
"type": "object",
|
|
46
106
|
"properties": {
|
|
@@ -1,8 +1,86 @@
|
|
|
1
1
|
# Skill Layer β Declaration Spec
|
|
2
2
|
|
|
3
|
-
Skills are declared in `
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Skills are declared in `manifest.json` under `layers.skills` as an array of objects.
|
|
4
|
+
|
|
5
|
+
## Skill Declaration Format
|
|
6
|
+
|
|
7
|
+
Each skill is an object with a required `name` and optional fields:
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"layers": {
|
|
12
|
+
"skills": [
|
|
13
|
+
{ "name": "weather", "description": "Query weather conditions", "trigger": "User asks about weather" },
|
|
14
|
+
{ "name": "web-search", "description": "Search for real-time information" },
|
|
15
|
+
{ "name": "deep-research", "install": "clawhub:deep-research" }
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
| Field | Required | Description |
|
|
22
|
+
|-------|----------|-------------|
|
|
23
|
+
| `name` | Yes | Skill identifier (used to resolve local definitions) |
|
|
24
|
+
| `description` | No | What this skill does (can come from local definition) |
|
|
25
|
+
| `trigger` | No | When to activate (can come from local `triggers` array) |
|
|
26
|
+
| `install` | No | External package source: `clawhub:<slug>` or `skillssh:<owner/repo>` |
|
|
27
|
+
|
|
28
|
+
## Resolution Chain
|
|
29
|
+
|
|
30
|
+
When the generator encounters a skill, it resolves metadata through this chain:
|
|
31
|
+
|
|
32
|
+
1. **Local definition** β `layers/skills/{name}/skill.json` (if exists, merges its metadata + injects SKILL.md content as a full section)
|
|
33
|
+
2. **Inline fields** β `description`, `trigger` written directly in the manifest entry
|
|
34
|
+
3. **Empty fallback** β skill name only; the agent judges usage by name alone
|
|
35
|
+
|
|
36
|
+
Local definitions always take precedence over inline fields for `description` and `triggers`.
|
|
37
|
+
|
|
38
|
+
## Local Skill Definition
|
|
39
|
+
|
|
40
|
+
A local skill lives in `layers/skills/{name}/`:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
layers/skills/weather/
|
|
44
|
+
skill.json β Metadata: name, description, allowedTools, triggers
|
|
45
|
+
SKILL.md β Behavior guide (optional, injected as full section)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### skill.json
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"name": "weather",
|
|
53
|
+
"description": "Query current weather conditions and forecasts",
|
|
54
|
+
"allowedTools": ["WebFetch", "Bash(curl:*)"],
|
|
55
|
+
"triggers": ["weather", "forecast", "outdoor plans"]
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- `allowedTools` from local definitions are automatically merged into the persona's allowed tools.
|
|
60
|
+
- `triggers` array is joined as a comma-separated string in the generated SKILL.md table.
|
|
61
|
+
|
|
62
|
+
### SKILL.md (Optional)
|
|
63
|
+
|
|
64
|
+
If present, the skill's SKILL.md content is injected as a **full section** (under `### Skill: {name}`) in the generated SKILL.md, instead of appearing as a table row. This allows rich, multi-paragraph behavior instructions.
|
|
65
|
+
|
|
66
|
+
## External Skills
|
|
67
|
+
|
|
68
|
+
Skills with an `install` field are installed from external sources during `openpersona install`:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{ "name": "deep-research", "install": "clawhub:deep-research" }
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Supported sources:
|
|
75
|
+
- `clawhub:<slug>` β installs via `npx clawhub@latest install <slug>`
|
|
76
|
+
- `skillssh:<owner/repo>` β installs via `npx skills add <owner/repo>`
|
|
77
|
+
|
|
78
|
+
## Generated SKILL.md Output
|
|
79
|
+
|
|
80
|
+
Skills appear in the generated SKILL.md under `## Skills & Tools`:
|
|
81
|
+
|
|
82
|
+
- **Table rows** β Skills without a local SKILL.md appear as rows: `| name | description | trigger |`
|
|
83
|
+
- **Full sections** β Skills with a local SKILL.md get a dedicated `### Skill: {name}` section with rich content
|
|
6
84
|
|
|
7
85
|
## SKILL.md Frontmatter
|
|
8
86
|
|
|
@@ -4,7 +4,7 @@ description: >
|
|
|
4
4
|
Meta-skill for building and managing agent persona skill packs.
|
|
5
5
|
Use when the user wants to create a new agent persona, install/manage
|
|
6
6
|
existing personas, or publish persona skill packs to ClawHub.
|
|
7
|
-
version: "0.
|
|
7
|
+
version: "0.5.0"
|
|
8
8
|
author: openpersona
|
|
9
9
|
repository: https://github.com/acnlabs/OpenPersona
|
|
10
10
|
tags: [persona, agent, skill-pack, meta-skill, openclaw]
|
|
@@ -35,7 +35,7 @@ Each persona is a four-layer bundle defined by two files:
|
|
|
35
35
|
- `layers.soul` β Path to persona.json (who you are)
|
|
36
36
|
- `layers.body` β Physical embodiment (null for digital agents)
|
|
37
37
|
- `layers.faculties` β Array of faculty objects: `[{ "name": "voice", "provider": "elevenlabs", ... }]`
|
|
38
|
-
- `layers.skills` β
|
|
38
|
+
- `layers.skills` β Array of skill objects: local definitions (resolved from `layers/skills/`), inline declarations, or external via `install` field
|
|
39
39
|
|
|
40
40
|
- **`persona.json`** β Pure soul definition (personality, speaking style, vibe, boundaries, behaviorGuide)
|
|
41
41
|
|
|
@@ -65,7 +65,7 @@ When the user wants to create a persona, gather this information through natural
|
|
|
65
65
|
|
|
66
66
|
**Cross-layer (manifest.json):**
|
|
67
67
|
- **Faculties:** Which faculties to enable β use object format: `[{ "name": "voice", "provider": "elevenlabs" }, { "name": "music" }]`
|
|
68
|
-
- **Skills:**
|
|
68
|
+
- **Skills:** Local definitions (`layers/skills/`), inline declarations, or external via `install` field (ClawHub / skills.sh)
|
|
69
69
|
- **Body:** Physical embodiment (null for most personas)
|
|
70
70
|
|
|
71
71
|
Write the collected info to a `persona.json` file, then run:
|
|
@@ -78,10 +78,11 @@ npx openpersona create --config ./persona.json --install
|
|
|
78
78
|
After understanding the persona's purpose, search for relevant skills:
|
|
79
79
|
|
|
80
80
|
1. Think about what capabilities this persona needs based on their role and bio
|
|
81
|
-
2.
|
|
82
|
-
3. Search
|
|
83
|
-
4.
|
|
84
|
-
5.
|
|
81
|
+
2. Check if a **local definition** exists in `layers/skills/{name}/` (has `skill.json` + optional `SKILL.md`)
|
|
82
|
+
3. Search ClawHub: `npx clawhub@latest search "<keywords>"`
|
|
83
|
+
4. Search skills.sh: fetch `https://skills.sh/api/search?q=<keywords>`
|
|
84
|
+
5. Present the top results to the user with name, description, and install count
|
|
85
|
+
6. Add selected skills to `layers.skills` as objects: `{ "name": "...", "description": "..." }` for local/inline, or `{ "name": "...", "install": "clawhub:<slug>" }` for external
|
|
85
86
|
|
|
86
87
|
## Creating Custom Skills
|
|
87
88
|
|
|
@@ -26,3 +26,23 @@ The following principles are shared by all OpenPersona agents. They cannot be ov
|
|
|
26
26
|
{{{facultySkillContent}}}
|
|
27
27
|
|
|
28
28
|
{{/facultyContent}}
|
|
29
|
+
{{#hasSkills}}
|
|
30
|
+
## Skills & Tools
|
|
31
|
+
|
|
32
|
+
The following skills define what you can actively do. Use them proactively when appropriate.
|
|
33
|
+
|
|
34
|
+
{{#hasSkillTable}}
|
|
35
|
+
| Skill | Description | When to Use |
|
|
36
|
+
|-------|-------------|-------------|
|
|
37
|
+
{{#skillEntries}}
|
|
38
|
+
| **{{name}}** | {{description}} | {{trigger}} |
|
|
39
|
+
{{/skillEntries}}
|
|
40
|
+
{{/hasSkillTable}}
|
|
41
|
+
|
|
42
|
+
{{#skillBlocks}}
|
|
43
|
+
### Skill: {{name}}
|
|
44
|
+
|
|
45
|
+
{{{content}}}
|
|
46
|
+
|
|
47
|
+
{{/skillBlocks}}
|
|
48
|
+
{{/hasSkills}}
|