openpersona 0.5.0 → 0.7.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
@@ -345,7 +345,7 @@ schemas/ # Four-layer schema definitions
345
345
  templates/ # Mustache rendering templates
346
346
  bin/ # CLI entry point
347
347
  lib/ # Core logic modules
348
- tests/ # Tests (45 passing)
348
+ tests/ # Tests (60 passing)
349
349
  ```
350
350
 
351
351
  ## 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.5.0');
27
+ .version('0.7.0');
28
28
 
29
29
  if (process.argv.length === 2) {
30
30
  process.argv.push('create');
@@ -163,7 +163,7 @@ program
163
163
  printError(`Persona not found: persona-${slug}`);
164
164
  process.exit(1);
165
165
  }
166
- const personaPath = path.join(skillDir, 'persona.json');
166
+ const personaPath = resolveSoulFile(skillDir, 'persona.json');
167
167
  if (!fs.existsSync(personaPath)) {
168
168
  printError('persona.json not found');
169
169
  process.exit(1);
@@ -227,10 +227,10 @@ program
227
227
  .description('★Experimental: Reset soul evolution state')
228
228
  .action(async (slug) => {
229
229
  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');
230
+ const personaPath = resolveSoulFile(skillDir, 'persona.json');
231
+ const soulStatePath = resolveSoulFile(skillDir, 'state.json');
232
232
  if (!fs.existsSync(personaPath) || !fs.existsSync(soulStatePath)) {
233
- printError('Persona or soul-state.json not found');
233
+ printError('Persona or soul state not found');
234
234
  process.exit(1);
235
235
  }
236
236
  const persona = JSON.parse(fs.readFileSync(personaPath, 'utf-8'));
@@ -262,4 +262,64 @@ program
262
262
  }
263
263
  });
264
264
 
265
+ program
266
+ .command('export <slug>')
267
+ .description('Export persona pack (with soul state) as a zip archive')
268
+ .option('-o, --output <path>', 'Output file path')
269
+ .action(async (slug, options) => {
270
+ const skillDir = path.join(OP_SKILLS_DIR, `persona-${slug}`);
271
+ if (!fs.existsSync(skillDir)) {
272
+ printError(`Persona not found: persona-${slug}`);
273
+ process.exit(1);
274
+ }
275
+ const AdmZip = require('adm-zip');
276
+ const zip = new AdmZip();
277
+ const addDir = (dir, zipPath) => {
278
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
279
+ const full = path.join(dir, entry.name);
280
+ const zp = zipPath ? `${zipPath}/${entry.name}` : entry.name;
281
+ if (entry.isDirectory()) addDir(full, zp);
282
+ else zip.addLocalFile(full, zipPath || '');
283
+ }
284
+ };
285
+ addDir(skillDir, '');
286
+ const outPath = options.output || `persona-${slug}.zip`;
287
+ zip.writeZip(outPath);
288
+ printSuccess(`Exported to ${outPath}`);
289
+ });
290
+
291
+ program
292
+ .command('import <file>')
293
+ .description('Import persona pack from a zip archive and install')
294
+ .option('-o, --output <dir>', 'Extract directory', path.join(require('os').tmpdir(), 'openpersona-import-' + Date.now()))
295
+ .action(async (file, options) => {
296
+ if (!fs.existsSync(file)) {
297
+ printError(`File not found: ${file}`);
298
+ process.exit(1);
299
+ }
300
+ const AdmZip = require('adm-zip');
301
+ const zip = new AdmZip(file);
302
+ const extractDir = options.output;
303
+ await fs.ensureDir(extractDir);
304
+ zip.extractAllTo(extractDir, true);
305
+
306
+ const personaPath = resolveSoulFile(extractDir, 'persona.json');
307
+ if (!fs.existsSync(personaPath)) {
308
+ printError('Not a valid persona archive: persona.json not found');
309
+ await fs.remove(extractDir);
310
+ process.exit(1);
311
+ }
312
+
313
+ try {
314
+ const destDir = await install(extractDir);
315
+ printSuccess(`Imported and installed from ${file}`);
316
+ if (extractDir.startsWith(require('os').tmpdir())) {
317
+ await fs.remove(extractDir);
318
+ }
319
+ } catch (e) {
320
+ printError(e.message);
321
+ process.exit(1);
322
+ }
323
+ });
324
+
265
325
  program.parse();
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
 
@@ -226,18 +235,24 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
226
235
  persona.facultyConfigs = facultyConfigs;
227
236
  }
228
237
 
238
+ // Body layer — detect soft-ref (declared with install but not locally available)
239
+ const rawBody = persona.body || persona.embodiments?.[0] || null;
240
+ const softRefBody = rawBody && typeof rawBody === 'object' && rawBody.install
241
+ ? { name: rawBody.name || 'body', install: rawBody.install }
242
+ : null;
243
+
229
244
  const faculties = facultyNames;
230
245
  // Load faculties — external ones (with install) may not exist locally yet
231
246
  const loadedFaculties = [];
247
+ const softRefFaculties = [];
232
248
  for (let i = 0; i < faculties.length; i++) {
233
249
  const name = faculties[i];
234
250
  const entry = rawFaculties[i];
235
251
  if (entry.install) {
236
- // External faculty — try local first, skip if not found yet
237
252
  try {
238
253
  loadedFaculties.push(loadFaculty(name));
239
254
  } catch {
240
- // Will be available after installation — skipped for now
255
+ softRefFaculties.push({ name, install: entry.install });
241
256
  }
242
257
  } else {
243
258
  loadedFaculties.push(loadFaculty(name));
@@ -246,7 +261,6 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
246
261
 
247
262
  // Derived fields
248
263
  persona.backstory = buildBackstory(persona);
249
- persona.capabilitiesSection = buildCapabilitiesSection(persona.capabilities);
250
264
  persona.facultySummary = buildFacultySummary(loadedFaculties);
251
265
  persona.skillContent = buildSkillContent(persona, loadedFaculties);
252
266
  persona.description = persona.bio?.slice(0, 120) || `Persona: ${persona.personaName}`;
@@ -259,6 +273,29 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
259
273
  persona.author = persona.author ?? 'openpersona';
260
274
  persona.version = persona.version ?? '0.1.0';
261
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
+
262
299
  // Mustache helpers
263
300
  persona.evolutionEnabled = evolutionEnabled;
264
301
  persona.hasSelfie = faculties.includes('selfie');
@@ -273,23 +310,11 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
273
310
  const soulTpl = loadTemplate('soul-injection');
274
311
  const identityTpl = loadTemplate('identity');
275
312
  const skillTpl = loadTemplate('skill');
276
- const readmeTpl = loadTemplate('readme');
277
313
 
278
- const soulInjection = Mustache.render(soulTpl, persona);
279
- const identityBlock = Mustache.render(identityTpl, persona);
280
- const facultyBlocks = loadedFaculties
281
- .filter((f) => !f.skillRef && !f.skeleton && f.files?.includes('SKILL.md'))
282
- .map((f) => ({
283
- facultyName: f.name,
284
- facultyDimension: f.dimension,
285
- facultySkillContent: readFacultySkillMd(f, persona),
286
- }));
287
-
288
- // Skill layer — resolve each skill: local definition > inline manifest fields
314
+ // Skill layer resolve before template rendering so soul-injection can reference soft-ref state
289
315
  const rawSkills = Array.isArray(persona.skills) ? persona.skills : [];
290
316
  const validSkills = rawSkills.filter((s) => s && typeof s === 'object' && s.name);
291
317
 
292
- // Load local definitions once (cache to avoid duplicate disk reads)
293
318
  const skillCache = new Map();
294
319
  for (const s of validSkills) {
295
320
  if (!skillCache.has(s.name)) {
@@ -299,15 +324,33 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
299
324
 
300
325
  const resolvedSkills = validSkills.map((s) => {
301
326
  const local = skillCache.get(s.name);
327
+ const hasInstall = !!s.install;
328
+ const isResolved = !!local;
329
+
330
+ let status;
331
+ if (isResolved) {
332
+ status = 'resolved';
333
+ } else if (hasInstall) {
334
+ status = 'soft-ref';
335
+ } else {
336
+ status = 'inline-only';
337
+ }
338
+
302
339
  return {
303
340
  name: s.name,
304
341
  description: local?.description || s.description || '',
305
342
  trigger: local?.triggers?.join(', ') || s.trigger || '',
306
343
  hasContent: !!local?._content,
307
344
  content: local?._content || '',
345
+ status,
346
+ install: s.install || '',
347
+ isSoftRef: status === 'soft-ref',
308
348
  };
309
349
  });
310
350
 
351
+ const activeSkills = resolvedSkills.filter((s) => !s.isSoftRef);
352
+ const softRefSkills = resolvedSkills.filter((s) => s.isSoftRef);
353
+
311
354
  // Collect allowed tools from skills with local definitions
312
355
  for (const [, local] of skillCache) {
313
356
  if (local?.allowedTools) {
@@ -320,29 +363,96 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
320
363
  }
321
364
  persona.allowedToolsStr = persona.allowedTools.join(' ');
322
365
 
366
+ // Self-Awareness System — unified gap detection across all layers
367
+ persona.hasSoftRefSkills = softRefSkills.length > 0;
368
+ persona.softRefSkillNames = softRefSkills.map((s) => s.name).join(', ');
369
+ persona.hasSoftRefFaculties = softRefFaculties.length > 0;
370
+ persona.softRefFacultyNames = softRefFaculties.map((f) => f.name).join(', ');
371
+ persona.hasSoftRefBody = !!softRefBody;
372
+ persona.softRefBodyName = softRefBody?.name || '';
373
+ persona.softRefBodyInstall = softRefBody?.install || '';
374
+ persona.heartbeatExpected = persona.heartbeat?.enabled === true;
375
+ persona.heartbeatStrategy = persona.heartbeat?.strategy || 'smart';
376
+ persona.hasSelfAwareness = persona.hasSoftRefSkills || persona.hasSoftRefFaculties || persona.hasSoftRefBody || persona.heartbeatExpected;
377
+
378
+ const soulInjection = Mustache.render(soulTpl, persona);
379
+ const identityBlock = Mustache.render(identityTpl, persona);
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
+ }
404
+
323
405
  const constitution = loadConstitution();
324
406
  const skillMd = Mustache.render(skillTpl, {
325
407
  ...persona,
326
- constitutionContent: constitution.content,
327
408
  constitutionVersion: constitution.version,
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),
409
+ bodyDescription,
410
+ hasFaculties: facultyIndex.length > 0,
411
+ facultyIndex,
412
+ hasSkills: activeSkills.length > 0,
413
+ hasSkillTable: activeSkills.filter((s) => !s.hasContent).length > 0,
414
+ skillEntries: activeSkills.filter((s) => !s.hasContent),
415
+ skillBlocks: activeSkills.filter((s) => s.hasContent),
416
+ hasSoftRefSkills: softRefSkills.length > 0,
417
+ softRefSkills,
418
+ hasSoftRefFaculties: softRefFaculties.length > 0,
419
+ softRefFaculties,
420
+ hasSoftRefBody: !!softRefBody,
421
+ softRefBodyName: softRefBody?.name || '',
422
+ softRefBodyInstall: softRefBody?.install || '',
423
+ hasExpectedCapabilities: softRefSkills.length > 0 || softRefFaculties.length > 0 || !!softRefBody,
333
424
  });
334
- const readmeMd = Mustache.render(readmeTpl, persona);
335
-
336
- await fs.writeFile(path.join(skillDir, 'soul-injection.md'), soulInjection);
337
- await fs.writeFile(path.join(skillDir, 'identity-block.md'), identityBlock);
338
425
  await fs.writeFile(path.join(skillDir, 'SKILL.md'), skillMd);
339
- await fs.writeFile(path.join(skillDir, 'README.md'), readmeMd);
340
426
 
341
- // 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/)
342
452
  for (const f of loadedFaculties) {
343
453
  if (f.files) {
344
454
  for (const rel of f.files) {
345
- if (rel === 'SKILL.md') continue; // already merged via template
455
+ if (rel === 'SKILL.md') continue;
346
456
  const src = path.join(f._dir, rel);
347
457
  if (fs.existsSync(src)) {
348
458
  const dest = path.join(skillDir, rel);
@@ -355,10 +465,16 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
355
465
 
356
466
  // persona.json copy (strip internal derived fields)
357
467
  const DERIVED_FIELDS = [
358
- 'backstory', 'capabilitiesSection', 'facultySummary',
468
+ 'backstory', 'facultySummary',
359
469
  'skillContent', 'description', 'evolutionEnabled', 'hasSelfie', 'allowedToolsStr',
360
470
  'author', 'version', 'facultyConfigs', 'defaults',
361
471
  '_dir', 'heartbeat',
472
+ 'hasSoftRefSkills', 'softRefSkillNames',
473
+ 'hasSoftRefFaculties', 'softRefFacultyNames',
474
+ 'hasSoftRefBody', 'softRefBodyName', 'softRefBodyInstall',
475
+ 'heartbeatExpected', 'heartbeatStrategy', 'hasSelfAwareness',
476
+ 'isDigitalTwin', 'sourceIdentityName', 'sourceIdentityKind', 'roleFoundation',
477
+ 'personaType',
362
478
  ];
363
479
  const cleanPersona = { ...persona };
364
480
  for (const key of DERIVED_FIELDS) {
@@ -366,7 +482,7 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
366
482
  }
367
483
  cleanPersona.meta = cleanPersona.meta || {};
368
484
  cleanPersona.meta.framework = 'openpersona';
369
- cleanPersona.meta.frameworkVersion = cleanPersona.meta.frameworkVersion || '0.5.0';
485
+ cleanPersona.meta.frameworkVersion = cleanPersona.meta.frameworkVersion || '0.7.0';
370
486
 
371
487
  // Build defaults from facultyConfigs (rich faculty config → env var mapping)
372
488
  const envDefaults = { ...(persona.defaults?.env || {}) };
@@ -395,7 +511,7 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
395
511
  cleanPersona.heartbeat = persona.heartbeat;
396
512
  }
397
513
 
398
- 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));
399
515
 
400
516
  // manifest.json — cross-layer metadata (heartbeat, allowedTools, meta, etc.)
401
517
  const manifest = {
@@ -403,7 +519,7 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
403
519
  version: persona.version || '0.1.0',
404
520
  author: persona.author || 'openpersona',
405
521
  layers: {
406
- soul: './persona.json',
522
+ soul: './soul/persona.json',
407
523
  body: persona.body || persona.embodiments?.[0] || null,
408
524
  faculties: rawFaculties,
409
525
  skills: persona.skills || [],
@@ -413,7 +529,7 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
413
529
  if (persona.heartbeat) {
414
530
  manifest.heartbeat = persona.heartbeat;
415
531
  }
416
- manifest.meta = cleanPersona.meta || { framework: 'openpersona', frameworkVersion: '0.5.0' };
532
+ manifest.meta = cleanPersona.meta || { framework: 'openpersona', frameworkVersion: '0.6.0' };
417
533
  await fs.writeFile(path.join(skillDir, 'manifest.json'), JSON.stringify(manifest, null, 2));
418
534
 
419
535
  // soul-state.json (if evolution enabled)
@@ -430,7 +546,7 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
430
546
  lastUpdatedAt: now,
431
547
  moodBaseline,
432
548
  });
433
- await fs.writeFile(path.join(skillDir, 'soul-state.json'), soulState);
549
+ await fs.writeFile(path.join(soulDir, 'state.json'), soulState);
434
550
  }
435
551
 
436
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('');
package/lib/switcher.js CHANGED
@@ -2,13 +2,13 @@
2
2
  * OpenPersona - Persona Switcher
3
3
  *
4
4
  * Three atomic operations:
5
- * 1. Read target persona resources (identity-block.md, soul-injection.md)
5
+ * 1. Read target persona resources (soul/injection.md, soul/identity.md)
6
6
  * 2. Sync workspace (IDENTITY.md, SOUL.md) — replace only the OpenPersona block
7
7
  * 3. Update active marker in openclaw.json
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, syncHeartbeat, installAllExternal } = require('./utils');
11
+ const { OP_HOME, OP_SKILLS_DIR, OP_WORKSPACE, resolveSoulFile, registrySetActive, loadRegistry, 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');
@@ -33,8 +33,23 @@ function escapeRe(s) {
33
33
 
34
34
  /**
35
35
  * List all installed personas with active status.
36
+ * Primary source: persona-registry.json; fallback: openclaw.json scan.
36
37
  */
37
38
  async function listPersonas() {
39
+ const reg = loadRegistry();
40
+ if (Object.keys(reg.personas).length > 0) {
41
+ return Object.values(reg.personas).map((e) => ({
42
+ slug: e.slug,
43
+ personaName: e.personaName,
44
+ active: e.active === true,
45
+ enabled: true,
46
+ role: e.role || 'companion',
47
+ installedAt: e.installedAt,
48
+ lastActiveAt: e.lastActiveAt,
49
+ }));
50
+ }
51
+
52
+ // Fallback: scan openclaw.json (pre-registry installs)
38
53
  if (!fs.existsSync(OPENCLAW_JSON)) return [];
39
54
 
40
55
  const config = JSON.parse(fs.readFileSync(OPENCLAW_JSON, 'utf-8'));
@@ -45,7 +60,7 @@ async function listPersonas() {
45
60
  if (!key.startsWith('persona-')) continue;
46
61
  const slug = key.replace('persona-', '');
47
62
  const skillDir = path.join(OP_SKILLS_DIR, key);
48
- const personaPath = path.join(skillDir, 'persona.json');
63
+ const personaPath = resolveSoulFile(skillDir, 'persona.json');
49
64
 
50
65
  let personaName = slug;
51
66
  if (fs.existsSync(personaPath)) {
@@ -79,12 +94,12 @@ async function switchPersona(slug) {
79
94
  process.exit(1);
80
95
  }
81
96
 
82
- const soulPath = path.join(skillDir, 'soul-injection.md');
83
- const identityPath = path.join(skillDir, 'identity-block.md');
84
- const personaPath = path.join(skillDir, 'persona.json');
97
+ const soulPath = resolveSoulFile(skillDir, 'injection.md');
98
+ const identityPath = resolveSoulFile(skillDir, 'identity.md');
99
+ const personaPath = resolveSoulFile(skillDir, 'persona.json');
85
100
 
86
101
  if (!fs.existsSync(personaPath)) {
87
- printError(`Not a valid persona pack: ${skillDir}/persona.json not found`);
102
+ printError(`Not a valid persona pack: persona.json not found in ${skillDir}`);
88
103
  process.exit(1);
89
104
  }
90
105
 
@@ -168,6 +183,9 @@ async function switchPersona(slug) {
168
183
  printSuccess('openclaw.json updated');
169
184
  }
170
185
 
186
+ // Update persona registry
187
+ registrySetActive(slug);
188
+
171
189
  // --- Step 4: Optional greeting via OpenClaw messaging ---
172
190
  try {
173
191
  const { execSync } = require('child_process');
@@ -3,7 +3,7 @@
3
3
  */
4
4
  const path = require('path');
5
5
  const fs = require('fs-extra');
6
- const { OP_SKILLS_DIR, OP_WORKSPACE, printError, printWarning, printSuccess, printInfo } = require('./utils');
6
+ const { OP_SKILLS_DIR, OP_WORKSPACE, registryRemove, printError, printWarning, printSuccess, printInfo } = 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');
@@ -72,6 +72,9 @@ async function uninstallFromDir(skillDir, slug) {
72
72
  await fs.remove(skillDir);
73
73
  printSuccess(`Removed ${skillDir}`);
74
74
 
75
+ // Remove from persona registry
76
+ registryRemove(slug);
77
+
75
78
  if ((persona.skills?.clawhub?.length || persona.skills?.skillssh?.length) > 0) {
76
79
  printWarning('External skills may be shared. Uninstall manually if needed.');
77
80
  }