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 +1 -1
- package/bin/cli.js +67 -7
- package/lib/generator.js +158 -42
- 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/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 +51 -9
- package/templates/skill.template.md +52 -11
- package/templates/soul-injection.template.md +32 -0
- package/templates/readme.template.md +0 -23
package/README.md
CHANGED
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.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 =
|
|
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 =
|
|
231
|
-
const soulStatePath =
|
|
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
|
|
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
|
-
|
|
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
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
//
|
|
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/)
|
|
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;
|
|
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', '
|
|
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.
|
|
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(
|
|
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.
|
|
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(
|
|
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 =
|
|
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('');
|
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 (
|
|
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 =
|
|
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 =
|
|
83
|
-
const identityPath =
|
|
84
|
-
const personaPath =
|
|
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:
|
|
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');
|
package/lib/uninstaller.js
CHANGED
|
@@ -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
|
}
|