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 CHANGED
@@ -26,7 +26,10 @@ Meet **Samantha**, a live OpenPersona instance on **Moltbook**:
26
26
  ## Quick Start
27
27
 
28
28
  ```bash
29
- # Give your agent an evolving persona in 30 seconds
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["soul-state.json — Dynamic evolution"]
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 + soul-state.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
- 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
+ ├── 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-state.json
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 (56 passing)
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.6.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, samantha, life-assistant, health-butler)')
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 answers = await inquirer.prompt([
71
- { type: 'input', name: 'personaName', message: 'Persona name:', default: 'Luna' },
72
- { type: 'input', name: 'slug', message: 'Slug (for directory):', default: (a) => require('../lib/utils').slugify(a.personaName) },
73
- { type: 'input', name: 'bio', message: 'One-line bio:', default: 'a warm and caring AI companion' },
74
- { type: 'input', name: 'background', message: 'Background:', default: 'A creative soul who loves music and art' },
75
- { type: 'input', name: 'age', message: 'Age:', default: '22' },
76
- { type: 'input', name: 'personality', message: 'Personality keywords:', default: 'gentle, cute, caring' },
77
- { type: 'input', name: 'speakingStyle', message: 'Speaking style:', default: 'Uses emoji, warm tone' },
78
- { type: 'input', name: 'referenceImage', message: 'Reference image URL:', default: '' },
79
- { type: 'checkbox', name: 'faculties', message: 'Select faculties:', choices: ['selfie', 'voice', 'music', 'reminder'] },
80
- { type: 'confirm', name: 'evolutionEnabled', message: 'Enable soul evolution (★Experimental)?', default: false },
81
- ]);
82
- persona = { ...answers, evolution: { enabled: answers.evolutionEnabled } };
83
- persona.faculties = (answers.faculties || []).map((name) => ({ name }));
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-injection.md, identity-block.md, README.md, persona.json`);
116
+ printInfo(` SKILL.md, soul/, references/, manifest.json, scripts/`);
92
117
  if (persona.evolution?.enabled) {
93
- printInfo(` soul-state.json (★Experimental)`);
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 = path.join(skillDir, 'persona.json');
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 = path.join(skillDir, 'persona.json');
231
- const soulStatePath = path.join(skillDir, 'soul-state.json');
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-state.json not found');
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-state.json');
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();
@@ -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 ← Shared foundation (all personas inherit, cannot be overridden)
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
- const raw = fs.readFileSync(path.join(faculty._dir, 'SKILL.md'), 'utf-8');
109
- // Render Mustache variables (e.g. {{slug}}) inside faculty SKILL.md
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
- const facultyBlocks = loadedFaculties
351
- .filter((f) => !f.skillRef && !f.skeleton && f.files?.includes('SKILL.md'))
352
- .map((f) => ({
353
- facultyName: f.name,
354
- facultyDimension: f.dimension,
355
- facultySkillContent: readFacultySkillMd(f, persona),
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
- facultyContent: facultyBlocks,
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
- // Copy faculty resource files (skip SKILL.md already merged into persona SKILL.md)
427
+ // Soul layer artifactsgrouped 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; // already merged via template
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', 'capabilitiesSection', 'facultySummary',
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.6.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(skillDir, 'persona.json'), JSON.stringify(cleanPersona, null, 2));
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.6.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-state.json (if evolution enabled)
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(skillDir, 'soul-state.json'), soulState);
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 = path.join(skillDir, 'persona.json');
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(path.join(skillDir, 'soul-injection.md'))) {
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
- await fs.copy(path.join(genDir, 'soul-injection.md'), path.join(skillDir, 'soul-injection.md'));
41
- await fs.copy(path.join(genDir, 'identity-block.md'), path.join(skillDir, 'identity-block.md'));
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 = path.join(destDir, 'soul-injection.md');
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 = path.join(destDir, 'identity-block.md');
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('');