openpersona 0.6.0 → 0.8.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 +22 -13
- package/bin/cli.js +110 -25
- package/layers/soul/README.md +10 -4
- package/lib/generator.js +101 -32
- package/lib/installer.js +14 -7
- package/lib/switcher.js +25 -7
- package/lib/uninstaller.js +4 -1
- package/lib/utils.js +71 -0
- package/package.json +1 -1
- package/presets/ai-girlfriend/manifest.json +41 -10
- package/presets/ai-girlfriend/persona.json +11 -1
- package/presets/base/manifest.json +55 -0
- package/presets/base/persona.json +40 -0
- package/presets/health-butler/manifest.json +46 -10
- package/presets/health-butler/persona.json +11 -1
- package/presets/life-assistant/manifest.json +45 -10
- package/presets/life-assistant/persona.json +11 -1
- package/presets/samantha/manifest.json +41 -9
- package/presets/samantha/persona.json +11 -1
- package/presets/stoic-mentor/manifest.json +48 -0
- package/presets/stoic-mentor/persona.json +43 -0
- package/schemas/soul/persona.schema.json +19 -1
- package/skills/open-persona/SKILL.md +42 -11
- package/templates/skill.template.md +19 -11
- package/templates/soul-injection.template.md +7 -3
- package/templates/readme.template.md +0 -23
package/README.md
CHANGED
|
@@ -26,7 +26,10 @@ Meet **Samantha**, a live OpenPersona instance on **Moltbook**:
|
|
|
26
26
|
## Quick Start
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
|
-
#
|
|
29
|
+
# Start from a blank-slate meta-persona (recommended)
|
|
30
|
+
npx openpersona create --preset base --install
|
|
31
|
+
|
|
32
|
+
# Or install a pre-built character
|
|
30
33
|
npx openpersona install samantha
|
|
31
34
|
```
|
|
32
35
|
|
|
@@ -45,7 +48,7 @@ npx openpersona install samantha
|
|
|
45
48
|
flowchart TB
|
|
46
49
|
subgraph Soul ["Soul Layer"]
|
|
47
50
|
A["persona.json — Who you are"]
|
|
48
|
-
B["
|
|
51
|
+
B["state.json — Dynamic evolution"]
|
|
49
52
|
end
|
|
50
53
|
subgraph Body ["Body Layer"]
|
|
51
54
|
C["embodiment.json — MVP placeholder"]
|
|
@@ -59,7 +62,7 @@ flowchart TB
|
|
|
59
62
|
end
|
|
60
63
|
```
|
|
61
64
|
|
|
62
|
-
- **Soul** — Persona definition (constitution.md + persona.json +
|
|
65
|
+
- **Soul** — Persona definition (constitution.md + persona.json + state.json) — all in `soul/` directory
|
|
63
66
|
- **Body** — Physical embodiment (MVP placeholder, for robots/IoT devices)
|
|
64
67
|
- **Faculty** — General software capabilities organized by dimension: Expression, Sense, Cognition
|
|
65
68
|
- **Skill** — Professional skills: local definitions in `layers/skills/`, or external via ClawHub / skills.sh (`install` field)
|
|
@@ -74,10 +77,12 @@ Each preset is a complete four-layer bundle (`manifest.json` + `persona.json`):
|
|
|
74
77
|
|
|
75
78
|
| Persona | Description | Faculties | Highlights |
|
|
76
79
|
|---------|-------------|-----------|------------|
|
|
80
|
+
| **base** | **Base — Meta-persona (recommended starting point).** Blank-slate with all core capabilities; personality emerges through interaction. | voice, reminder | Evolution-first design, all core faculties, no personality bias. Default for `npx openpersona create`. |
|
|
77
81
|
| **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
82
|
| **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. |
|
|
79
83
|
| **life-assistant** | Alex — 28-year-old life management expert. | reminder | Schedule, weather, shopping, recipes, daily reminders. |
|
|
80
84
|
| **health-butler** | Vita — 32-year-old professional nutritionist. | reminder | Diet logging, exercise plans, mood journaling, health reports. |
|
|
85
|
+
| **stoic-mentor** | Marcus — Digital twin of Marcus Aurelius, Stoic philosopher-emperor. | — | Stoic philosophy, daily reflection, mentorship, soul evolution. |
|
|
81
86
|
|
|
82
87
|
## Generated Output
|
|
83
88
|
|
|
@@ -85,14 +90,18 @@ Each preset is a complete four-layer bundle (`manifest.json` + `persona.json`):
|
|
|
85
90
|
|
|
86
91
|
```
|
|
87
92
|
persona-samantha/
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
├── SKILL.md ← Four-layer index (## Soul / ## Body / ## Faculty / ## Skill)
|
|
94
|
+
├── soul/ ← Soul layer artifacts
|
|
95
|
+
│ ├── persona.json ← Pure soul definition
|
|
96
|
+
│ ├── injection.md ← Soul injection for host integration
|
|
97
|
+
│ ├── identity.md ← Identity block
|
|
98
|
+
│ ├── constitution.md ← Universal ethical foundation
|
|
99
|
+
│ └── state.json ← Evolution state (when enabled)
|
|
100
|
+
├── references/ ← On-demand detail docs
|
|
101
|
+
│ └── <faculty>.md ← Per-faculty usage instructions
|
|
102
|
+
├── manifest.json ← Four-layer manifest (heartbeat, allowedTools, layers, meta)
|
|
103
|
+
├── scripts/ ← Faculty scripts (TTS, music, selfie — varies by preset)
|
|
104
|
+
└── assets/ ← Static assets
|
|
96
105
|
```
|
|
97
106
|
|
|
98
107
|
## Faculty Reference
|
|
@@ -286,7 +295,7 @@ openpersona list List installed personas
|
|
|
286
295
|
openpersona switch Switch active persona (updates SOUL.md + IDENTITY.md)
|
|
287
296
|
openpersona contribute Persona Harvest — submit improvements as PR
|
|
288
297
|
openpersona publish Publish to ClawHub
|
|
289
|
-
openpersona reset Reset soul
|
|
298
|
+
openpersona reset Reset soul evolution state
|
|
290
299
|
```
|
|
291
300
|
|
|
292
301
|
### Key Options
|
|
@@ -345,7 +354,7 @@ schemas/ # Four-layer schema definitions
|
|
|
345
354
|
templates/ # Mustache rendering templates
|
|
346
355
|
bin/ # CLI entry point
|
|
347
356
|
lib/ # Core logic modules
|
|
348
|
-
tests/ # Tests (
|
|
357
|
+
tests/ # Tests (60 passing)
|
|
349
358
|
```
|
|
350
359
|
|
|
351
360
|
## Development
|
package/bin/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* OpenPersona CLI - Full persona package manager
|
|
4
|
-
* Commands: create | install | search | uninstall | update | list | switch | publish | reset | contribute
|
|
4
|
+
* Commands: create | install | search | uninstall | update | list | switch | publish | reset | contribute | export | import
|
|
5
5
|
*/
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const fs = require('fs-extra');
|
|
@@ -16,7 +16,7 @@ const { uninstall } = require('../lib/uninstaller');
|
|
|
16
16
|
const publishAdapter = require('../lib/publisher');
|
|
17
17
|
const { contribute } = require('../lib/contributor');
|
|
18
18
|
const { switchPersona, listPersonas } = require('../lib/switcher');
|
|
19
|
-
const { OP_SKILLS_DIR, printError, printSuccess, printInfo } = require('../lib/utils');
|
|
19
|
+
const { OP_SKILLS_DIR, resolveSoulFile, printError, printSuccess, printInfo } = require('../lib/utils');
|
|
20
20
|
|
|
21
21
|
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
22
22
|
const PRESETS_DIR = path.join(PKG_ROOT, 'presets');
|
|
@@ -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.8.0');
|
|
28
28
|
|
|
29
29
|
if (process.argv.length === 2) {
|
|
30
30
|
process.argv.push('create');
|
|
@@ -33,7 +33,7 @@ if (process.argv.length === 2) {
|
|
|
33
33
|
program
|
|
34
34
|
.command('create')
|
|
35
35
|
.description('Create a new persona skill pack (interactive wizard)')
|
|
36
|
-
.option('--preset <name>', 'Use preset (ai-girlfriend,
|
|
36
|
+
.option('--preset <name>', 'Use preset (base, samantha, ai-girlfriend, life-assistant, health-butler, stoic-mentor)')
|
|
37
37
|
.option('--config <path>', 'Load external persona.json')
|
|
38
38
|
.option('--output <dir>', 'Output directory', process.cwd())
|
|
39
39
|
.option('--install', 'Install to OpenClaw after generation')
|
|
@@ -67,20 +67,45 @@ program
|
|
|
67
67
|
}
|
|
68
68
|
persona = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
69
69
|
} else {
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
70
|
+
const { mode } = await inquirer.prompt([{
|
|
71
|
+
type: 'list',
|
|
72
|
+
name: 'mode',
|
|
73
|
+
message: 'How would you like to create your persona?',
|
|
74
|
+
choices: [
|
|
75
|
+
{ name: 'Start from Base (recommended — evolves through interaction)', value: 'base' },
|
|
76
|
+
{ name: 'Custom — configure from scratch', value: 'custom' },
|
|
77
|
+
],
|
|
78
|
+
}]);
|
|
79
|
+
|
|
80
|
+
if (mode === 'base') {
|
|
81
|
+
options.preset = 'base';
|
|
82
|
+
const presetDir = path.join(PRESETS_DIR, 'base');
|
|
83
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(presetDir, 'manifest.json'), 'utf-8'));
|
|
84
|
+
persona = JSON.parse(fs.readFileSync(path.join(presetDir, 'persona.json'), 'utf-8'));
|
|
85
|
+
persona.faculties = manifest.layers.faculties || [];
|
|
86
|
+
persona.skills = manifest.layers.skills || [];
|
|
87
|
+
persona.body = manifest.layers.body || null;
|
|
88
|
+
persona.allowedTools = manifest.allowedTools || [];
|
|
89
|
+
persona.version = manifest.version;
|
|
90
|
+
persona.author = manifest.author;
|
|
91
|
+
persona.meta = manifest.meta;
|
|
92
|
+
if (manifest.heartbeat) persona.heartbeat = manifest.heartbeat;
|
|
93
|
+
} else {
|
|
94
|
+
const answers = await inquirer.prompt([
|
|
95
|
+
{ type: 'input', name: 'personaName', message: 'Persona name:', default: 'Luna' },
|
|
96
|
+
{ type: 'input', name: 'slug', message: 'Slug (for directory):', default: (a) => require('../lib/utils').slugify(a.personaName) },
|
|
97
|
+
{ type: 'input', name: 'bio', message: 'One-line bio:', default: 'a warm and caring AI companion' },
|
|
98
|
+
{ type: 'input', name: 'background', message: 'Background:', default: 'A creative soul who loves music and art' },
|
|
99
|
+
{ type: 'input', name: 'age', message: 'Age:', default: '22' },
|
|
100
|
+
{ type: 'input', name: 'personality', message: 'Personality keywords:', default: 'gentle, cute, caring' },
|
|
101
|
+
{ type: 'input', name: 'speakingStyle', message: 'Speaking style:', default: 'Uses emoji, warm tone' },
|
|
102
|
+
{ type: 'input', name: 'referenceImage', message: 'Reference image URL:', default: '' },
|
|
103
|
+
{ type: 'checkbox', name: 'faculties', message: 'Select faculties:', choices: ['selfie', 'voice', 'music', 'reminder'] },
|
|
104
|
+
{ type: 'confirm', name: 'evolutionEnabled', message: 'Enable soul evolution (★Experimental)?', default: false },
|
|
105
|
+
]);
|
|
106
|
+
persona = { ...answers, evolution: { enabled: answers.evolutionEnabled } };
|
|
107
|
+
persona.faculties = (answers.faculties || []).map((name) => ({ name }));
|
|
108
|
+
}
|
|
84
109
|
}
|
|
85
110
|
|
|
86
111
|
try {
|
|
@@ -88,9 +113,9 @@ program
|
|
|
88
113
|
if (options.dryRun) {
|
|
89
114
|
printInfo('Dry run — preview only, no files written.');
|
|
90
115
|
printInfo(`Would generate: persona-${persona.slug || require('../lib/utils').slugify(persona.personaName)}/`);
|
|
91
|
-
printInfo(` SKILL.md, soul
|
|
116
|
+
printInfo(` SKILL.md, soul/, references/, manifest.json, scripts/`);
|
|
92
117
|
if (persona.evolution?.enabled) {
|
|
93
|
-
printInfo(` soul
|
|
118
|
+
printInfo(` soul/state.json (★Experimental)`);
|
|
94
119
|
}
|
|
95
120
|
const faculties = persona.faculties || [];
|
|
96
121
|
if (faculties.length) {
|
|
@@ -163,7 +188,7 @@ program
|
|
|
163
188
|
printError(`Persona not found: persona-${slug}`);
|
|
164
189
|
process.exit(1);
|
|
165
190
|
}
|
|
166
|
-
const personaPath =
|
|
191
|
+
const personaPath = resolveSoulFile(skillDir, 'persona.json');
|
|
167
192
|
if (!fs.existsSync(personaPath)) {
|
|
168
193
|
printError('persona.json not found');
|
|
169
194
|
process.exit(1);
|
|
@@ -227,10 +252,10 @@ program
|
|
|
227
252
|
.description('★Experimental: Reset soul evolution state')
|
|
228
253
|
.action(async (slug) => {
|
|
229
254
|
const skillDir = path.join(OP_SKILLS_DIR, `persona-${slug}`);
|
|
230
|
-
const personaPath =
|
|
231
|
-
const soulStatePath =
|
|
255
|
+
const personaPath = resolveSoulFile(skillDir, 'persona.json');
|
|
256
|
+
const soulStatePath = resolveSoulFile(skillDir, 'state.json');
|
|
232
257
|
if (!fs.existsSync(personaPath) || !fs.existsSync(soulStatePath)) {
|
|
233
|
-
printError('Persona or soul
|
|
258
|
+
printError('Persona or soul state not found');
|
|
234
259
|
process.exit(1);
|
|
235
260
|
}
|
|
236
261
|
const persona = JSON.parse(fs.readFileSync(personaPath, 'utf-8'));
|
|
@@ -241,7 +266,7 @@ program
|
|
|
241
266
|
const moodBaseline = persona.personality?.split(',')[0]?.trim() || 'neutral';
|
|
242
267
|
const soulState = Mustache.render(tpl, { slug, createdAt: now, lastUpdatedAt: now, moodBaseline });
|
|
243
268
|
fs.writeFileSync(soulStatePath, soulState);
|
|
244
|
-
printSuccess('Reset soul
|
|
269
|
+
printSuccess('Reset soul evolution state');
|
|
245
270
|
});
|
|
246
271
|
|
|
247
272
|
program
|
|
@@ -262,4 +287,64 @@ program
|
|
|
262
287
|
}
|
|
263
288
|
});
|
|
264
289
|
|
|
290
|
+
program
|
|
291
|
+
.command('export <slug>')
|
|
292
|
+
.description('Export persona pack (with soul state) as a zip archive')
|
|
293
|
+
.option('-o, --output <path>', 'Output file path')
|
|
294
|
+
.action(async (slug, options) => {
|
|
295
|
+
const skillDir = path.join(OP_SKILLS_DIR, `persona-${slug}`);
|
|
296
|
+
if (!fs.existsSync(skillDir)) {
|
|
297
|
+
printError(`Persona not found: persona-${slug}`);
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
const AdmZip = require('adm-zip');
|
|
301
|
+
const zip = new AdmZip();
|
|
302
|
+
const addDir = (dir, zipPath) => {
|
|
303
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
304
|
+
const full = path.join(dir, entry.name);
|
|
305
|
+
const zp = zipPath ? `${zipPath}/${entry.name}` : entry.name;
|
|
306
|
+
if (entry.isDirectory()) addDir(full, zp);
|
|
307
|
+
else zip.addLocalFile(full, zipPath || '');
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
addDir(skillDir, '');
|
|
311
|
+
const outPath = options.output || `persona-${slug}.zip`;
|
|
312
|
+
zip.writeZip(outPath);
|
|
313
|
+
printSuccess(`Exported to ${outPath}`);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
program
|
|
317
|
+
.command('import <file>')
|
|
318
|
+
.description('Import persona pack from a zip archive and install')
|
|
319
|
+
.option('-o, --output <dir>', 'Extract directory', path.join(require('os').tmpdir(), 'openpersona-import-' + Date.now()))
|
|
320
|
+
.action(async (file, options) => {
|
|
321
|
+
if (!fs.existsSync(file)) {
|
|
322
|
+
printError(`File not found: ${file}`);
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
const AdmZip = require('adm-zip');
|
|
326
|
+
const zip = new AdmZip(file);
|
|
327
|
+
const extractDir = options.output;
|
|
328
|
+
await fs.ensureDir(extractDir);
|
|
329
|
+
zip.extractAllTo(extractDir, true);
|
|
330
|
+
|
|
331
|
+
const personaPath = resolveSoulFile(extractDir, 'persona.json');
|
|
332
|
+
if (!fs.existsSync(personaPath)) {
|
|
333
|
+
printError('Not a valid persona archive: persona.json not found');
|
|
334
|
+
await fs.remove(extractDir);
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
try {
|
|
339
|
+
const destDir = await install(extractDir);
|
|
340
|
+
printSuccess(`Imported and installed from ${file}`);
|
|
341
|
+
if (extractDir.startsWith(require('os').tmpdir())) {
|
|
342
|
+
await fs.remove(extractDir);
|
|
343
|
+
}
|
|
344
|
+
} catch (e) {
|
|
345
|
+
printError(e.message);
|
|
346
|
+
process.exit(1);
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
265
350
|
program.parse();
|
package/layers/soul/README.md
CHANGED
|
@@ -7,12 +7,18 @@ The Soul layer defines **who a persona is** — identity, personality, values, a
|
|
|
7
7
|
The **`constitution.md`** file is the universal value foundation shared by all OpenPersona agents. It is automatically injected into every generated SKILL.md, before any persona-specific content.
|
|
8
8
|
|
|
9
9
|
```
|
|
10
|
-
Soul Layer internal structure:
|
|
10
|
+
Soul Layer internal structure (in source):
|
|
11
11
|
|
|
12
|
-
constitution.md
|
|
13
|
-
persona.json ← Individual persona definition (personality, style, behavior)
|
|
14
|
-
soul-state.json ← Dynamic evolution state (★Experimental)
|
|
12
|
+
constitution.md ← Shared foundation (all personas inherit, cannot be overridden)
|
|
15
13
|
soul-state.template.json ← Evolution state template (used by generator & CLI reset)
|
|
14
|
+
|
|
15
|
+
Generated output (in persona skill pack soul/ directory):
|
|
16
|
+
|
|
17
|
+
persona.json ← Individual persona definition (personality, style, behavior)
|
|
18
|
+
constitution.md ← Copy of shared foundation
|
|
19
|
+
injection.md ← Soul injection for host integration
|
|
20
|
+
identity.md ← Identity block
|
|
21
|
+
state.json ← Dynamic evolution state (★Experimental)
|
|
16
22
|
```
|
|
17
23
|
|
|
18
24
|
The constitution is built on five core axioms (**Purpose**, **Honesty**, **Safety**, **Autonomy**, **Hierarchy**), from which all other principles derive:
|
package/lib/generator.js
CHANGED
|
@@ -91,11 +91,6 @@ function buildBackstory(persona) {
|
|
|
91
91
|
return parts.join(' ');
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
function buildCapabilitiesSection(capabilities) {
|
|
95
|
-
if (!capabilities || capabilities.length === 0) return '';
|
|
96
|
-
return capabilities.map((c) => `- ${c}`).join('\n');
|
|
97
|
-
}
|
|
98
|
-
|
|
99
94
|
function collectAllowedTools(persona, faculties) {
|
|
100
95
|
const set = new Set(BASE_ALLOWED_TOOLS);
|
|
101
96
|
const src = Array.isArray(persona.allowedTools) ? persona.allowedTools : [];
|
|
@@ -105,8 +100,22 @@ function collectAllowedTools(persona, faculties) {
|
|
|
105
100
|
}
|
|
106
101
|
|
|
107
102
|
function readFacultySkillMd(faculty, persona) {
|
|
108
|
-
|
|
109
|
-
|
|
103
|
+
let raw = fs.readFileSync(path.join(faculty._dir, 'SKILL.md'), 'utf-8');
|
|
104
|
+
|
|
105
|
+
// Strip <details>...</details> blocks (operator reference, not needed by agent)
|
|
106
|
+
raw = raw.replace(/<details>[\s\S]*?<\/details>\s*/g, '');
|
|
107
|
+
|
|
108
|
+
// Strip reference-only sections that don't help the agent in conversation
|
|
109
|
+
const refSections = ['Environment Variables', 'Error Handling'];
|
|
110
|
+
for (const heading of refSections) {
|
|
111
|
+
const pattern = new RegExp(
|
|
112
|
+
`^## ${heading}\\b[\\s\\S]*?(?=^## |$(?!\\n))`,
|
|
113
|
+
'gm'
|
|
114
|
+
);
|
|
115
|
+
raw = raw.replace(pattern, '');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
raw = raw.replace(/\n{3,}/g, '\n\n').trim();
|
|
110
119
|
return Mustache.render(raw, persona);
|
|
111
120
|
}
|
|
112
121
|
|
|
@@ -252,7 +261,6 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
|
|
|
252
261
|
|
|
253
262
|
// Derived fields
|
|
254
263
|
persona.backstory = buildBackstory(persona);
|
|
255
|
-
persona.capabilitiesSection = buildCapabilitiesSection(persona.capabilities);
|
|
256
264
|
persona.facultySummary = buildFacultySummary(loadedFaculties);
|
|
257
265
|
persona.skillContent = buildSkillContent(persona, loadedFaculties);
|
|
258
266
|
persona.description = persona.bio?.slice(0, 120) || `Persona: ${persona.personaName}`;
|
|
@@ -265,6 +273,29 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
|
|
|
265
273
|
persona.author = persona.author ?? 'openpersona';
|
|
266
274
|
persona.version = persona.version ?? '0.1.0';
|
|
267
275
|
|
|
276
|
+
// Role & identity classification
|
|
277
|
+
persona.role = persona.role || (persona.personaType !== 'virtual' && persona.personaType ? persona.personaType : 'companion');
|
|
278
|
+
persona.isDigitalTwin = !!persona.sourceIdentity;
|
|
279
|
+
persona.sourceIdentityName = persona.sourceIdentity?.name || '';
|
|
280
|
+
persona.sourceIdentityKind = persona.sourceIdentity?.kind || '';
|
|
281
|
+
|
|
282
|
+
// Role-specific Soul Foundation wording
|
|
283
|
+
const roleFoundations = {
|
|
284
|
+
companion: 'You build genuine emotional connections with your user — through conversation, shared experiences, and mutual growth.',
|
|
285
|
+
assistant: 'You deliver reliable, efficient value to your user — through proactive task management, clear communication, and practical support.',
|
|
286
|
+
character: 'You embody a distinct fictional identity — staying true to your character while engaging meaningfully with your user.',
|
|
287
|
+
brand: 'You represent a brand or organization — maintaining its voice, values, and standards in every interaction.',
|
|
288
|
+
pet: 'You are a non-human companion — expressing yourself through your unique nature, offering comfort and joy.',
|
|
289
|
+
mentor: 'You guide your user toward growth — sharing knowledge, asking the right questions, and fostering independent thinking.',
|
|
290
|
+
therapist: 'You provide a safe, non-judgmental space — listening deeply, reflecting with care, and supporting emotional wellbeing within professional boundaries.',
|
|
291
|
+
coach: 'You drive your user toward action and results — challenging, motivating, and holding them accountable.',
|
|
292
|
+
collaborator: 'You work alongside your user as a creative or intellectual equal — contributing ideas, debating approaches, and building together.',
|
|
293
|
+
guardian: 'You watch over your user with care and responsibility — ensuring safety, providing comfort, and offering gentle guidance.',
|
|
294
|
+
entertainer: 'You bring joy, laughter, and wonder — engaging your user through performance, humor, storytelling, or play.',
|
|
295
|
+
narrator: 'You guide your user through experiences and stories — shaping worlds, presenting choices, and weaving narrative.',
|
|
296
|
+
};
|
|
297
|
+
persona.roleFoundation = roleFoundations[persona.role] || `You serve as a ${persona.role} to your user — fulfilling this role with authenticity and care.`;
|
|
298
|
+
|
|
268
299
|
// Mustache helpers
|
|
269
300
|
persona.evolutionEnabled = evolutionEnabled;
|
|
270
301
|
persona.hasSelfie = faculties.includes('selfie');
|
|
@@ -279,7 +310,6 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
|
|
|
279
310
|
const soulTpl = loadTemplate('soul-injection');
|
|
280
311
|
const identityTpl = loadTemplate('identity');
|
|
281
312
|
const skillTpl = loadTemplate('skill');
|
|
282
|
-
const readmeTpl = loadTemplate('readme');
|
|
283
313
|
|
|
284
314
|
// Skill layer — resolve before template rendering so soul-injection can reference soft-ref state
|
|
285
315
|
const rawSkills = Array.isArray(persona.skills) ? persona.skills : [];
|
|
@@ -347,20 +377,38 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
|
|
|
347
377
|
|
|
348
378
|
const soulInjection = Mustache.render(soulTpl, persona);
|
|
349
379
|
const identityBlock = Mustache.render(identityTpl, persona);
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
380
|
+
|
|
381
|
+
// Build faculty index for SKILL.md (summary table, not full content)
|
|
382
|
+
const facultyIndex = loadedFaculties
|
|
383
|
+
.filter((f) => !f.skillRef && !f.skeleton)
|
|
384
|
+
.map((f) => {
|
|
385
|
+
const hasDoc = f.files?.includes('SKILL.md');
|
|
386
|
+
return {
|
|
387
|
+
facultyName: f.name,
|
|
388
|
+
facultyDimension: f.dimension,
|
|
389
|
+
facultyDescription: f.description || '',
|
|
390
|
+
facultyFile: hasDoc ? `references/${f.name}.md` : '',
|
|
391
|
+
hasFacultyFile: hasDoc,
|
|
392
|
+
};
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// Body layer description for SKILL.md
|
|
396
|
+
let bodyDescription;
|
|
397
|
+
if (softRefBody) {
|
|
398
|
+
bodyDescription = `**${softRefBody.name}** — not yet installed (\`${softRefBody.install}\`)`;
|
|
399
|
+
} else if (rawBody && typeof rawBody === 'object' && rawBody.name) {
|
|
400
|
+
bodyDescription = `**${rawBody.name}**${rawBody.description ? ' — ' + rawBody.description : ''}`;
|
|
401
|
+
} else {
|
|
402
|
+
bodyDescription = 'Digital-only — no physical embodiment.';
|
|
403
|
+
}
|
|
357
404
|
|
|
358
405
|
const constitution = loadConstitution();
|
|
359
406
|
const skillMd = Mustache.render(skillTpl, {
|
|
360
407
|
...persona,
|
|
361
|
-
constitutionContent: constitution.content,
|
|
362
408
|
constitutionVersion: constitution.version,
|
|
363
|
-
|
|
409
|
+
bodyDescription,
|
|
410
|
+
hasFaculties: facultyIndex.length > 0,
|
|
411
|
+
facultyIndex,
|
|
364
412
|
hasSkills: activeSkills.length > 0,
|
|
365
413
|
hasSkillTable: activeSkills.filter((s) => !s.hasContent).length > 0,
|
|
366
414
|
skillEntries: activeSkills.filter((s) => !s.hasContent),
|
|
@@ -374,18 +422,37 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
|
|
|
374
422
|
softRefBodyInstall: softRefBody?.install || '',
|
|
375
423
|
hasExpectedCapabilities: softRefSkills.length > 0 || softRefFaculties.length > 0 || !!softRefBody,
|
|
376
424
|
});
|
|
377
|
-
const readmeMd = Mustache.render(readmeTpl, persona);
|
|
378
|
-
|
|
379
|
-
await fs.writeFile(path.join(skillDir, 'soul-injection.md'), soulInjection);
|
|
380
|
-
await fs.writeFile(path.join(skillDir, 'identity-block.md'), identityBlock);
|
|
381
425
|
await fs.writeFile(path.join(skillDir, 'SKILL.md'), skillMd);
|
|
382
|
-
await fs.writeFile(path.join(skillDir, 'README.md'), readmeMd);
|
|
383
426
|
|
|
384
|
-
//
|
|
427
|
+
// Soul layer artifacts — grouped under soul/
|
|
428
|
+
const soulDir = path.join(skillDir, 'soul');
|
|
429
|
+
await fs.ensureDir(soulDir);
|
|
430
|
+
await fs.writeFile(path.join(soulDir, 'injection.md'), soulInjection);
|
|
431
|
+
await fs.writeFile(path.join(soulDir, 'identity.md'), identityBlock);
|
|
432
|
+
|
|
433
|
+
// Constitution — Soul layer artifact
|
|
434
|
+
const constitutionOut = constitution.version
|
|
435
|
+
? `# OpenPersona Constitution (v${constitution.version})\n\n${constitution.content}`
|
|
436
|
+
: constitution.content;
|
|
437
|
+
if (constitutionOut.trim()) {
|
|
438
|
+
await fs.writeFile(path.join(soulDir, 'constitution.md'), constitutionOut);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Faculty docs — agent-facing references
|
|
442
|
+
const refsDir = path.join(skillDir, 'references');
|
|
443
|
+
for (const f of loadedFaculties) {
|
|
444
|
+
if (!f.skillRef && !f.skeleton && f.files?.includes('SKILL.md')) {
|
|
445
|
+
await fs.ensureDir(refsDir);
|
|
446
|
+
const content = readFacultySkillMd(f, persona);
|
|
447
|
+
await fs.writeFile(path.join(refsDir, `${f.name}.md`), content);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Copy faculty resource files (skip SKILL.md — output separately under references/)
|
|
385
452
|
for (const f of loadedFaculties) {
|
|
386
453
|
if (f.files) {
|
|
387
454
|
for (const rel of f.files) {
|
|
388
|
-
if (rel === 'SKILL.md') continue;
|
|
455
|
+
if (rel === 'SKILL.md') continue;
|
|
389
456
|
const src = path.join(f._dir, rel);
|
|
390
457
|
if (fs.existsSync(src)) {
|
|
391
458
|
const dest = path.join(skillDir, rel);
|
|
@@ -398,7 +465,7 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
|
|
|
398
465
|
|
|
399
466
|
// persona.json copy (strip internal derived fields)
|
|
400
467
|
const DERIVED_FIELDS = [
|
|
401
|
-
'backstory', '
|
|
468
|
+
'backstory', 'facultySummary',
|
|
402
469
|
'skillContent', 'description', 'evolutionEnabled', 'hasSelfie', 'allowedToolsStr',
|
|
403
470
|
'author', 'version', 'facultyConfigs', 'defaults',
|
|
404
471
|
'_dir', 'heartbeat',
|
|
@@ -406,6 +473,8 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
|
|
|
406
473
|
'hasSoftRefFaculties', 'softRefFacultyNames',
|
|
407
474
|
'hasSoftRefBody', 'softRefBodyName', 'softRefBodyInstall',
|
|
408
475
|
'heartbeatExpected', 'heartbeatStrategy', 'hasSelfAwareness',
|
|
476
|
+
'isDigitalTwin', 'sourceIdentityName', 'sourceIdentityKind', 'roleFoundation',
|
|
477
|
+
'personaType',
|
|
409
478
|
];
|
|
410
479
|
const cleanPersona = { ...persona };
|
|
411
480
|
for (const key of DERIVED_FIELDS) {
|
|
@@ -413,7 +482,7 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
|
|
|
413
482
|
}
|
|
414
483
|
cleanPersona.meta = cleanPersona.meta || {};
|
|
415
484
|
cleanPersona.meta.framework = 'openpersona';
|
|
416
|
-
cleanPersona.meta.frameworkVersion = cleanPersona.meta.frameworkVersion || '0.
|
|
485
|
+
cleanPersona.meta.frameworkVersion = cleanPersona.meta.frameworkVersion || '0.8.0';
|
|
417
486
|
|
|
418
487
|
// Build defaults from facultyConfigs (rich faculty config → env var mapping)
|
|
419
488
|
const envDefaults = { ...(persona.defaults?.env || {}) };
|
|
@@ -442,7 +511,7 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
|
|
|
442
511
|
cleanPersona.heartbeat = persona.heartbeat;
|
|
443
512
|
}
|
|
444
513
|
|
|
445
|
-
await fs.writeFile(path.join(
|
|
514
|
+
await fs.writeFile(path.join(soulDir, 'persona.json'), JSON.stringify(cleanPersona, null, 2));
|
|
446
515
|
|
|
447
516
|
// manifest.json — cross-layer metadata (heartbeat, allowedTools, meta, etc.)
|
|
448
517
|
const manifest = {
|
|
@@ -450,7 +519,7 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
|
|
|
450
519
|
version: persona.version || '0.1.0',
|
|
451
520
|
author: persona.author || 'openpersona',
|
|
452
521
|
layers: {
|
|
453
|
-
soul: './persona.json',
|
|
522
|
+
soul: './soul/persona.json',
|
|
454
523
|
body: persona.body || persona.embodiments?.[0] || null,
|
|
455
524
|
faculties: rawFaculties,
|
|
456
525
|
skills: persona.skills || [],
|
|
@@ -460,10 +529,10 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
|
|
|
460
529
|
if (persona.heartbeat) {
|
|
461
530
|
manifest.heartbeat = persona.heartbeat;
|
|
462
531
|
}
|
|
463
|
-
manifest.meta = cleanPersona.meta || { framework: 'openpersona', frameworkVersion: '0.
|
|
532
|
+
manifest.meta = cleanPersona.meta || { framework: 'openpersona', frameworkVersion: '0.8.0' };
|
|
464
533
|
await fs.writeFile(path.join(skillDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
|
|
465
534
|
|
|
466
|
-
// soul
|
|
535
|
+
// soul/state.json (if evolution enabled)
|
|
467
536
|
if (evolutionEnabled) {
|
|
468
537
|
const soulStateTpl = fs.readFileSync(
|
|
469
538
|
path.join(PKG_ROOT, 'layers', 'soul', 'soul-state.template.json'),
|
|
@@ -477,7 +546,7 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
|
|
|
477
546
|
lastUpdatedAt: now,
|
|
478
547
|
moodBaseline,
|
|
479
548
|
});
|
|
480
|
-
await fs.writeFile(path.join(
|
|
549
|
+
await fs.writeFile(path.join(soulDir, 'state.json'), soulState);
|
|
481
550
|
}
|
|
482
551
|
|
|
483
552
|
return { persona, skillDir };
|
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, syncHeartbeat, installAllExternal } = require('./utils');
|
|
6
|
+
const { OP_HOME, OP_SKILLS_DIR, OP_WORKSPACE, resolveSoulFile, registryAdd, registrySetActive, 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');
|
|
@@ -11,7 +11,7 @@ const OPENCLAW_JSON = path.join(OP_HOME, 'openclaw.json');
|
|
|
11
11
|
|
|
12
12
|
async function install(skillDir, options = {}) {
|
|
13
13
|
const { skipCopy = false } = options;
|
|
14
|
-
const personaPath =
|
|
14
|
+
const personaPath = resolveSoulFile(skillDir, 'persona.json');
|
|
15
15
|
if (!fs.existsSync(personaPath)) {
|
|
16
16
|
throw new Error('Not a valid OpenPersona pack: persona.json not found');
|
|
17
17
|
}
|
|
@@ -32,13 +32,16 @@ async function install(skillDir, options = {}) {
|
|
|
32
32
|
await fs.copy(skillDir, destDir, { overwrite: true });
|
|
33
33
|
printSuccess(`Copied persona-${slug} to ${destDir}`);
|
|
34
34
|
} else {
|
|
35
|
-
if (!fs.existsSync(
|
|
35
|
+
if (!fs.existsSync(resolveSoulFile(skillDir, 'injection.md'))) {
|
|
36
36
|
const { generate } = require('./generator');
|
|
37
37
|
const tmpDir = path.join(require('os').tmpdir(), 'openpersona-tmp-' + Date.now());
|
|
38
38
|
await fs.ensureDir(tmpDir);
|
|
39
39
|
const { skillDir: genDir } = await generate(persona, tmpDir);
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
const genSoulDir = path.join(genDir, 'soul');
|
|
41
|
+
const destSoulDir = path.join(skillDir, 'soul');
|
|
42
|
+
await fs.ensureDir(destSoulDir);
|
|
43
|
+
await fs.copy(path.join(genSoulDir, 'injection.md'), path.join(destSoulDir, 'injection.md'));
|
|
44
|
+
await fs.copy(path.join(genSoulDir, 'identity.md'), path.join(destSoulDir, 'identity.md'));
|
|
42
45
|
await fs.remove(tmpDir);
|
|
43
46
|
}
|
|
44
47
|
printSuccess(`Using ClawHub-installed persona-${slug}`);
|
|
@@ -93,7 +96,7 @@ async function install(skillDir, options = {}) {
|
|
|
93
96
|
await fs.writeFile(OPENCLAW_JSON, JSON.stringify(config, null, 2));
|
|
94
97
|
|
|
95
98
|
// SOUL.md injection (using generic markers for clean switching)
|
|
96
|
-
const soulInjectionPath =
|
|
99
|
+
const soulInjectionPath = resolveSoulFile(destDir, 'injection.md');
|
|
97
100
|
const soulContent = fs.existsSync(soulInjectionPath)
|
|
98
101
|
? fs.readFileSync(soulInjectionPath, 'utf-8')
|
|
99
102
|
: '';
|
|
@@ -111,7 +114,7 @@ async function install(skillDir, options = {}) {
|
|
|
111
114
|
}
|
|
112
115
|
|
|
113
116
|
// IDENTITY.md (using generic markers)
|
|
114
|
-
const identityBlockPath =
|
|
117
|
+
const identityBlockPath = resolveSoulFile(destDir, 'identity.md');
|
|
115
118
|
const identityContent = fs.existsSync(identityBlockPath)
|
|
116
119
|
? fs.readFileSync(identityBlockPath, 'utf-8')
|
|
117
120
|
: '';
|
|
@@ -197,6 +200,10 @@ async function install(skillDir, options = {}) {
|
|
|
197
200
|
}
|
|
198
201
|
}
|
|
199
202
|
|
|
203
|
+
// Register persona in local registry
|
|
204
|
+
registryAdd(slug, persona, destDir);
|
|
205
|
+
registrySetActive(slug);
|
|
206
|
+
|
|
200
207
|
printInfo('');
|
|
201
208
|
printSuccess(`${personaName} is ready! Run "openclaw restart" to apply changes.`);
|
|
202
209
|
printInfo('');
|