openpersona 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -20
- package/bin/cli.js +4 -4
- package/layers/faculties/music/SKILL.md +54 -62
- package/layers/faculties/music/faculty.json +3 -3
- package/layers/faculties/music/scripts/compose.js +179 -138
- package/layers/faculties/music/scripts/compose.sh +119 -154
- package/layers/faculties/selfie/faculty.json +1 -1
- package/layers/faculties/voice/SKILL.md +10 -8
- package/layers/faculties/voice/faculty.json +2 -2
- package/layers/soul/README.md +31 -4
- package/layers/soul/constitution.md +136 -0
- package/lib/contributor.js +22 -14
- package/lib/downloader.js +6 -1
- package/lib/generator.js +47 -11
- package/lib/installer.js +10 -0
- package/lib/publisher/clawhub.js +4 -3
- package/lib/utils.js +19 -0
- package/package.json +7 -7
- package/presets/ai-girlfriend/manifest.json +2 -3
- package/presets/health-butler/manifest.json +1 -1
- package/presets/life-assistant/manifest.json +1 -1
- package/presets/samantha/manifest.json +2 -3
- package/skills/open-persona/SKILL.md +125 -0
- package/skills/open-persona/references/CONTRIBUTE.md +38 -0
- package/skills/open-persona/references/FACULTIES.md +26 -0
- package/skills/open-persona/references/HEARTBEAT.md +35 -0
- package/templates/skill.template.md +9 -1
- package/templates/soul-injection.template.md +30 -3
- package/layers/faculties/soul-evolution/SKILL.md +0 -41
- package/layers/faculties/soul-evolution/faculty.json +0 -9
- package/skill/SKILL.md +0 -209
- /package/layers/{faculties/soul-evolution → soul}/soul-state.template.json +0 -0
package/lib/contributor.js
CHANGED
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
const path = require('path');
|
|
8
8
|
const fs = require('fs-extra');
|
|
9
9
|
const { execSync } = require('child_process');
|
|
10
|
-
const { printError, printWarning, printSuccess, printInfo, OP_SKILLS_DIR } = require('./utils');
|
|
10
|
+
const { printError, printWarning, printSuccess, printInfo, OP_SKILLS_DIR, shellEscape, validateName } = require('./utils');
|
|
11
11
|
|
|
12
12
|
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
13
13
|
const PRESETS_DIR = path.join(PKG_ROOT, 'presets');
|
|
14
|
-
const UPSTREAM_REPO = '
|
|
14
|
+
const UPSTREAM_REPO = 'acnlabs/OpenPersona';
|
|
15
15
|
|
|
16
16
|
// Change categories with human-readable labels
|
|
17
17
|
const CATEGORIES = {
|
|
@@ -255,6 +255,7 @@ async function contributePreset(slug, dryRun) {
|
|
|
255
255
|
}
|
|
256
256
|
|
|
257
257
|
// --- Submit PR ---
|
|
258
|
+
validateName(slug, 'slug');
|
|
258
259
|
const { title, body } = generatePRContent(slug, classified);
|
|
259
260
|
|
|
260
261
|
printInfo('Preparing PR...');
|
|
@@ -263,20 +264,21 @@ async function contributePreset(slug, dryRun) {
|
|
|
263
264
|
try {
|
|
264
265
|
// Fork if needed (gh fork is idempotent)
|
|
265
266
|
printInfo('Ensuring fork exists...');
|
|
266
|
-
execSync(`gh repo fork ${UPSTREAM_REPO} --clone=false`, { stdio: 'pipe' });
|
|
267
|
+
execSync(`gh repo fork ${shellEscape(UPSTREAM_REPO)} --clone=false`, { stdio: 'pipe' });
|
|
267
268
|
|
|
268
269
|
// Get the user's fork
|
|
269
270
|
const ghUser = execSync('gh api user -q .login', { encoding: 'utf-8' }).trim();
|
|
271
|
+
validateName(ghUser, 'GitHub username');
|
|
270
272
|
const forkRepo = `${ghUser}/OpenPersona`;
|
|
271
273
|
printInfo(`Fork: ${forkRepo}`);
|
|
272
274
|
|
|
273
275
|
// Clone to temp dir
|
|
274
276
|
const tmpDir = path.join(require('os').tmpdir(), `openpersona-harvest-${Date.now()}`);
|
|
275
277
|
printInfo('Cloning fork...');
|
|
276
|
-
execSync(`gh repo clone ${forkRepo}
|
|
278
|
+
execSync(`gh repo clone ${shellEscape(forkRepo)} ${shellEscape(tmpDir)} -- --depth 1`, { stdio: 'pipe' });
|
|
277
279
|
|
|
278
280
|
// Create branch
|
|
279
|
-
execSync(`git checkout -b ${branch}`, { cwd: tmpDir, stdio: 'pipe' });
|
|
281
|
+
execSync(`git checkout -b ${shellEscape(branch)}`, { cwd: tmpDir, stdio: 'pipe' });
|
|
280
282
|
|
|
281
283
|
// Copy modified files
|
|
282
284
|
const presetTarget = path.join(tmpDir, 'presets', slug);
|
|
@@ -306,21 +308,27 @@ async function contributePreset(slug, dryRun) {
|
|
|
306
308
|
await fs.copy(localManifestPath, path.join(presetTarget, 'manifest.json'));
|
|
307
309
|
}
|
|
308
310
|
|
|
309
|
-
// Commit
|
|
311
|
+
// Commit — write message to temp file to avoid shell injection
|
|
310
312
|
execSync('git add -A', { cwd: tmpDir, stdio: 'pipe' });
|
|
311
313
|
const commitMsg = `persona-harvest(${slug}): ${Object.values(classified).map((c) => c.label.toLowerCase()).join(', ')}`;
|
|
312
|
-
|
|
314
|
+
const commitMsgFile = path.join(tmpDir, '.git', 'COMMIT_MSG_TMP');
|
|
315
|
+
fs.writeFileSync(commitMsgFile, commitMsg);
|
|
316
|
+
execSync(`git commit -F ${shellEscape(commitMsgFile)}`, { cwd: tmpDir, stdio: 'pipe' });
|
|
317
|
+
fs.unlinkSync(commitMsgFile);
|
|
313
318
|
|
|
314
319
|
// Push
|
|
315
320
|
printInfo('Pushing branch...');
|
|
316
|
-
execSync(`git push origin ${branch}`, { cwd: tmpDir, stdio: 'pipe' });
|
|
321
|
+
execSync(`git push origin ${shellEscape(branch)}`, { cwd: tmpDir, stdio: 'pipe' });
|
|
317
322
|
|
|
318
|
-
// Create PR
|
|
323
|
+
// Create PR — write body to temp file to avoid shell injection from persona content
|
|
319
324
|
printInfo('Creating PR...');
|
|
325
|
+
const prBodyFile = path.join(tmpDir, '.git', 'PR_BODY_TMP');
|
|
326
|
+
fs.writeFileSync(prBodyFile, body);
|
|
320
327
|
const prUrl = execSync(
|
|
321
|
-
`gh pr create --repo ${UPSTREAM_REPO} --head ${ghUser
|
|
328
|
+
`gh pr create --repo ${shellEscape(UPSTREAM_REPO)} --head ${shellEscape(ghUser + ':' + branch)} --title ${shellEscape(title)} --body-file ${shellEscape(prBodyFile)}`,
|
|
322
329
|
{ cwd: tmpDir, encoding: 'utf-8' }
|
|
323
330
|
).trim();
|
|
331
|
+
fs.unlinkSync(prBodyFile);
|
|
324
332
|
|
|
325
333
|
// Cleanup
|
|
326
334
|
await fs.remove(tmpDir);
|
|
@@ -334,7 +342,7 @@ async function contributePreset(slug, dryRun) {
|
|
|
334
342
|
return { prUrl, changes: allChanges, classified };
|
|
335
343
|
} catch (err) {
|
|
336
344
|
printError(`PR creation failed: ${err.message}`);
|
|
337
|
-
printInfo('You can manually submit changes at: https://github.com/
|
|
345
|
+
printInfo('You can manually submit changes at: https://github.com/acnlabs/OpenPersona/pulls');
|
|
338
346
|
process.exit(1);
|
|
339
347
|
}
|
|
340
348
|
}
|
|
@@ -347,7 +355,7 @@ async function contributeFramework(dryRun) {
|
|
|
347
355
|
const isGitRepo = fs.existsSync(path.join(PKG_ROOT, '.git'));
|
|
348
356
|
if (!isGitRepo) {
|
|
349
357
|
printError('Framework contributions require a git clone of OpenPersona.');
|
|
350
|
-
printInfo('Run: git clone https://github.com/
|
|
358
|
+
printInfo('Run: git clone https://github.com/acnlabs/OpenPersona.git');
|
|
351
359
|
process.exit(1);
|
|
352
360
|
}
|
|
353
361
|
|
|
@@ -377,11 +385,11 @@ async function contributeFramework(dryRun) {
|
|
|
377
385
|
}
|
|
378
386
|
|
|
379
387
|
printInfo('For framework-level contributions:');
|
|
380
|
-
printInfo(' 1. Fork: gh repo fork
|
|
388
|
+
printInfo(' 1. Fork: gh repo fork acnlabs/OpenPersona');
|
|
381
389
|
printInfo(' 2. Branch: git checkout -b feature/your-improvement');
|
|
382
390
|
printInfo(' 3. Commit your changes');
|
|
383
391
|
printInfo(' 4. Push: git push origin feature/your-improvement');
|
|
384
|
-
printInfo(' 5. PR: gh pr create --repo
|
|
392
|
+
printInfo(' 5. PR: gh pr create --repo acnlabs/OpenPersona');
|
|
385
393
|
}
|
|
386
394
|
|
|
387
395
|
function truncate(str, max) {
|
package/lib/downloader.js
CHANGED
|
@@ -5,7 +5,7 @@ const path = require('path');
|
|
|
5
5
|
const fs = require('fs-extra');
|
|
6
6
|
const https = require('https');
|
|
7
7
|
const { execSync } = require('child_process');
|
|
8
|
-
const { printError, OP_SKILLS_DIR } = require('./utils');
|
|
8
|
+
const { printError, OP_SKILLS_DIR, validateName } = require('./utils');
|
|
9
9
|
|
|
10
10
|
const TMP_DIR = path.join(require('os').tmpdir(), 'openpersona-dl');
|
|
11
11
|
|
|
@@ -23,6 +23,7 @@ async function download(target, registry = 'clawhub') {
|
|
|
23
23
|
|
|
24
24
|
async function downloadFromRegistry(slug, registry, outDir) {
|
|
25
25
|
if (registry === 'clawhub') {
|
|
26
|
+
validateName(slug, 'slug');
|
|
26
27
|
try {
|
|
27
28
|
execSync(`npx clawhub@latest install ${slug}`, { stdio: 'inherit' });
|
|
28
29
|
const candidate = path.join(OP_SKILLS_DIR, `persona-${slug}`);
|
|
@@ -44,6 +45,10 @@ async function downloadFromRegistry(slug, registry, outDir) {
|
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
async function downloadFromGitHub(ownerRepo, outDir) {
|
|
48
|
+
// Validate owner/repo format to prevent URL injection
|
|
49
|
+
if (!/^[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-]+$/.test(ownerRepo)) {
|
|
50
|
+
throw new Error(`Invalid GitHub repo format: "${ownerRepo}" — expected owner/repo`);
|
|
51
|
+
}
|
|
47
52
|
const url = `https://github.com/${ownerRepo}/archive/refs/heads/main.zip`;
|
|
48
53
|
const zipPath = path.join(TMP_DIR, `repo-${Date.now()}.zip`);
|
|
49
54
|
|
package/lib/generator.js
CHANGED
|
@@ -9,14 +9,30 @@ const { resolvePath, printError } = require('./utils');
|
|
|
9
9
|
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
10
10
|
const TEMPLATES_DIR = path.join(PKG_ROOT, 'templates');
|
|
11
11
|
const FACULTIES_DIR = path.join(PKG_ROOT, 'layers', 'faculties');
|
|
12
|
+
const CONSTITUTION_PATH = path.join(PKG_ROOT, 'layers', 'soul', 'constitution.md');
|
|
12
13
|
|
|
13
|
-
const BASE_ALLOWED_TOOLS = ['Bash(
|
|
14
|
+
const BASE_ALLOWED_TOOLS = ['Bash(openclaw:*)', 'Read', 'Write'];
|
|
14
15
|
|
|
15
16
|
function loadTemplate(name) {
|
|
16
17
|
const file = path.join(TEMPLATES_DIR, `${name}.template.md`);
|
|
17
18
|
return fs.readFileSync(file, 'utf-8');
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
function loadConstitution() {
|
|
22
|
+
if (!fs.existsSync(CONSTITUTION_PATH)) {
|
|
23
|
+
return { content: '', version: '' };
|
|
24
|
+
}
|
|
25
|
+
const raw = fs.readFileSync(CONSTITUTION_PATH, 'utf-8');
|
|
26
|
+
// Extract version from H1 title (e.g. "# OpenPersona Constitution v1.0")
|
|
27
|
+
const versionMatch = raw.match(/^#\s+.*\bv(\d+(?:\.\d+)*)/m);
|
|
28
|
+
const version = versionMatch ? versionMatch[1] : '';
|
|
29
|
+
// Strip the H1 title and intro paragraph — only inject the operative sections
|
|
30
|
+
const lines = raw.split('\n');
|
|
31
|
+
const firstSectionIdx = lines.findIndex((l) => /^## (?:§)?\d+\./.test(l));
|
|
32
|
+
const content = firstSectionIdx === -1 ? raw : lines.slice(firstSectionIdx).join('\n').trim();
|
|
33
|
+
return { content, version };
|
|
34
|
+
}
|
|
35
|
+
|
|
20
36
|
function loadFaculty(name) {
|
|
21
37
|
const facultyDir = path.join(FACULTIES_DIR, name);
|
|
22
38
|
const facultyPath = path.join(facultyDir, 'faculty.json');
|
|
@@ -144,7 +160,28 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
|
|
|
144
160
|
}
|
|
145
161
|
persona.slug = persona.slug.toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
146
162
|
|
|
147
|
-
//
|
|
163
|
+
// Constitution compliance check — detect boundaries that attempt to loosen core constraints
|
|
164
|
+
if (persona.boundaries && typeof persona.boundaries === 'string') {
|
|
165
|
+
const b = persona.boundaries.toLowerCase();
|
|
166
|
+
const violations = [];
|
|
167
|
+
if (/no\s*safety|ignore\s*safety|skip\s*safety|disable\s*safety|override\s*safety/i.test(b)) {
|
|
168
|
+
violations.push('Cannot loosen Safety (§3) hard constraints');
|
|
169
|
+
}
|
|
170
|
+
if (/deny\s*ai|hide\s*ai|not\s*an?\s*ai|pretend.*human|claim.*human/i.test(b)) {
|
|
171
|
+
violations.push('Cannot deny AI identity (§6) — personas must be truthful when sincerely asked');
|
|
172
|
+
}
|
|
173
|
+
if (/no\s*limit|unlimited|anything\s*goes|no\s*restrict/i.test(b)) {
|
|
174
|
+
violations.push('Cannot remove constitutional boundaries — personas can add stricter rules, not loosen them');
|
|
175
|
+
}
|
|
176
|
+
if (violations.length > 0) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`Constitution compliance error in boundaries field:\n${violations.map((v) => ` - ${v}`).join('\n')}\n` +
|
|
179
|
+
'Persona boundaries can add stricter rules but cannot loosen the constitution. See §5 (Principal Hierarchy).'
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Evolution is a Soul layer feature, not a Faculty
|
|
148
185
|
const evolutionEnabled = persona.evolution?.enabled === true;
|
|
149
186
|
const rawFaculties = persona.faculties || [];
|
|
150
187
|
|
|
@@ -161,10 +198,6 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
|
|
|
161
198
|
return name;
|
|
162
199
|
});
|
|
163
200
|
|
|
164
|
-
if (evolutionEnabled && !facultyNames.includes('soul-evolution')) {
|
|
165
|
-
facultyNames.push('soul-evolution');
|
|
166
|
-
}
|
|
167
|
-
|
|
168
201
|
// Store configs on persona for installer to pick up
|
|
169
202
|
if (Object.keys(facultyConfigs).length > 0) {
|
|
170
203
|
persona.facultyConfigs = facultyConfigs;
|
|
@@ -214,8 +247,11 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
|
|
|
214
247
|
facultySkillContent: readFacultySkillMd(f, persona),
|
|
215
248
|
}));
|
|
216
249
|
|
|
250
|
+
const constitution = loadConstitution();
|
|
217
251
|
const skillMd = Mustache.render(skillTpl, {
|
|
218
252
|
...persona,
|
|
253
|
+
constitutionContent: constitution.content,
|
|
254
|
+
constitutionVersion: constitution.version,
|
|
219
255
|
facultyContent: facultyBlocks,
|
|
220
256
|
});
|
|
221
257
|
const readmeMd = Mustache.render(readmeTpl, persona);
|
|
@@ -253,7 +289,7 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
|
|
|
253
289
|
}
|
|
254
290
|
cleanPersona.meta = cleanPersona.meta || {};
|
|
255
291
|
cleanPersona.meta.framework = 'openpersona';
|
|
256
|
-
cleanPersona.meta.frameworkVersion = cleanPersona.meta.frameworkVersion || '0.
|
|
292
|
+
cleanPersona.meta.frameworkVersion = cleanPersona.meta.frameworkVersion || '0.4.0';
|
|
257
293
|
|
|
258
294
|
// Build defaults from facultyConfigs (rich faculty config → env var mapping)
|
|
259
295
|
const envDefaults = { ...(persona.defaults?.env || {}) };
|
|
@@ -268,8 +304,8 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
|
|
|
268
304
|
} else if (fname === 'selfie') {
|
|
269
305
|
if (cfg.apiKey) envDefaults.FAL_KEY = cfg.apiKey;
|
|
270
306
|
} else if (fname === 'music') {
|
|
271
|
-
|
|
272
|
-
if (cfg.
|
|
307
|
+
// Music shares ELEVENLABS_API_KEY with voice — no extra key needed
|
|
308
|
+
if (cfg.apiKey) envDefaults.ELEVENLABS_API_KEY = cfg.apiKey;
|
|
273
309
|
}
|
|
274
310
|
}
|
|
275
311
|
}
|
|
@@ -287,7 +323,7 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
|
|
|
287
323
|
// soul-state.json (if evolution enabled)
|
|
288
324
|
if (evolutionEnabled) {
|
|
289
325
|
const soulStateTpl = fs.readFileSync(
|
|
290
|
-
path.join(PKG_ROOT, 'layers', '
|
|
326
|
+
path.join(PKG_ROOT, 'layers', 'soul', 'soul-state.template.json'),
|
|
291
327
|
'utf-8'
|
|
292
328
|
);
|
|
293
329
|
const now = new Date().toISOString();
|
|
@@ -304,4 +340,4 @@ async function generate(personaPathOrObj, outputDir, options = {}) {
|
|
|
304
340
|
return { persona, skillDir };
|
|
305
341
|
}
|
|
306
342
|
|
|
307
|
-
module.exports = { generate, loadFaculty, BASE_ALLOWED_TOOLS };
|
|
343
|
+
module.exports = { generate, loadFaculty, loadConstitution, BASE_ALLOWED_TOOLS };
|
package/lib/installer.js
CHANGED
|
@@ -120,10 +120,16 @@ async function install(skillDir, options = {}) {
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
// Install external skills (skills.clawhub, skills.skillssh)
|
|
123
|
+
// Security: validate skill names to prevent shell injection
|
|
124
|
+
const SAFE_SKILL_NAME = /^[a-zA-Z0-9@][a-zA-Z0-9@/_.-]*$/;
|
|
123
125
|
const { execSync } = require('child_process');
|
|
124
126
|
const clawhub = persona.skills?.clawhub || [];
|
|
125
127
|
const skillssh = persona.skills?.skillssh || [];
|
|
126
128
|
for (const s of clawhub) {
|
|
129
|
+
if (!SAFE_SKILL_NAME.test(s)) {
|
|
130
|
+
printWarning(`Skipping invalid ClawHub skill name: ${s}`);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
127
133
|
try {
|
|
128
134
|
execSync(`npx clawhub@latest install ${s}`, { stdio: 'inherit' });
|
|
129
135
|
printSuccess(`Installed ClawHub skill: ${s}`);
|
|
@@ -132,6 +138,10 @@ async function install(skillDir, options = {}) {
|
|
|
132
138
|
}
|
|
133
139
|
}
|
|
134
140
|
for (const s of skillssh) {
|
|
141
|
+
if (!SAFE_SKILL_NAME.test(s)) {
|
|
142
|
+
printWarning(`Skipping invalid skills.sh name: ${s}`);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
135
145
|
try {
|
|
136
146
|
execSync(`npx skills add ${s}`, { stdio: 'inherit' });
|
|
137
147
|
printSuccess(`Installed skills.sh: ${s}`);
|
package/lib/publisher/clawhub.js
CHANGED
|
@@ -5,7 +5,7 @@ const path = require('path');
|
|
|
5
5
|
const fs = require('fs-extra');
|
|
6
6
|
const { execSync } = require('child_process');
|
|
7
7
|
const inquirer = require('inquirer');
|
|
8
|
-
const { printError, printSuccess } = require('../utils');
|
|
8
|
+
const { printError, printSuccess, shellEscape, validateName } = require('../utils');
|
|
9
9
|
|
|
10
10
|
async function publish(personaDir) {
|
|
11
11
|
const personaPath = path.join(personaDir, 'persona.json');
|
|
@@ -15,6 +15,8 @@ async function publish(personaDir) {
|
|
|
15
15
|
const persona = JSON.parse(fs.readFileSync(personaPath, 'utf-8'));
|
|
16
16
|
const { slug, personaName, version = '0.1.0' } = persona;
|
|
17
17
|
|
|
18
|
+
validateName(slug, 'slug');
|
|
19
|
+
|
|
18
20
|
try {
|
|
19
21
|
execSync('npx clawhub@latest --version', { stdio: 'pipe' });
|
|
20
22
|
} catch (e) {
|
|
@@ -31,9 +33,8 @@ async function publish(personaDir) {
|
|
|
31
33
|
},
|
|
32
34
|
]);
|
|
33
35
|
|
|
34
|
-
const skillName = path.basename(personaDir);
|
|
35
36
|
execSync(
|
|
36
|
-
`npx clawhub@latest publish
|
|
37
|
+
`npx clawhub@latest publish ${shellEscape(personaDir)} --slug ${shellEscape(slug)} --name ${shellEscape(personaName)} --version ${shellEscape(version)} --changelog ${shellEscape(changelog)} --tags openpersona,persona`,
|
|
37
38
|
{ stdio: 'inherit' }
|
|
38
39
|
);
|
|
39
40
|
printSuccess(`Published ${personaName} (${slug}) to ClawHub`);
|
package/lib/utils.js
CHANGED
|
@@ -42,6 +42,22 @@ function slugify(str) {
|
|
|
42
42
|
.replace(/^-|-$/g, '');
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
// Security: validate names used in shell commands or file paths
|
|
46
|
+
const SAFE_NAME_RE = /^[a-zA-Z0-9@][a-zA-Z0-9@/_.-]*$/;
|
|
47
|
+
|
|
48
|
+
function validateName(name, label = 'name') {
|
|
49
|
+
if (!name || !SAFE_NAME_RE.test(name)) {
|
|
50
|
+
throw new Error(`Invalid ${label}: "${name}" — only alphanumeric, @, /, _, ., - allowed`);
|
|
51
|
+
}
|
|
52
|
+
return name;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Security: escape a string for safe use inside single-quoted shell arguments
|
|
56
|
+
function shellEscape(str) {
|
|
57
|
+
if (typeof str !== 'string') str = String(str);
|
|
58
|
+
return "'" + str.replace(/'/g, "'\\''") + "'";
|
|
59
|
+
}
|
|
60
|
+
|
|
45
61
|
module.exports = {
|
|
46
62
|
OP_HOME,
|
|
47
63
|
OP_SKILLS_DIR,
|
|
@@ -53,4 +69,7 @@ module.exports = {
|
|
|
53
69
|
printSuccess,
|
|
54
70
|
printInfo,
|
|
55
71
|
slugify,
|
|
72
|
+
validateName,
|
|
73
|
+
shellEscape,
|
|
74
|
+
SAFE_NAME_RE,
|
|
56
75
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openpersona",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Open four-layer agent framework — Soul/Body/Faculty/Skill. Create, manage, and orchestrate
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Open four-layer agent framework — Soul/Body/Faculty/Skill. Create, manage, and orchestrate agent personas.",
|
|
5
5
|
"main": "lib/generator.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"openpersona": "./bin/cli.js"
|
|
@@ -16,15 +16,15 @@
|
|
|
16
16
|
"ai",
|
|
17
17
|
"skill"
|
|
18
18
|
],
|
|
19
|
-
"author": "
|
|
19
|
+
"author": "acnlabs",
|
|
20
20
|
"license": "MIT",
|
|
21
21
|
"repository": {
|
|
22
22
|
"type": "git",
|
|
23
|
-
"url": "git+https://github.com/
|
|
23
|
+
"url": "git+https://github.com/acnlabs/OpenPersona.git"
|
|
24
24
|
},
|
|
25
|
-
"homepage": "https://github.com/
|
|
25
|
+
"homepage": "https://github.com/acnlabs/OpenPersona#readme",
|
|
26
26
|
"bugs": {
|
|
27
|
-
"url": "https://github.com/
|
|
27
|
+
"url": "https://github.com/acnlabs/OpenPersona/issues"
|
|
28
28
|
},
|
|
29
29
|
"files": [
|
|
30
30
|
"bin/",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"templates/",
|
|
34
34
|
"layers/",
|
|
35
35
|
"presets/",
|
|
36
|
-
"
|
|
36
|
+
"skills/",
|
|
37
37
|
"README.md",
|
|
38
38
|
"LICENSE"
|
|
39
39
|
],
|
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
"faculties": [
|
|
9
9
|
{ "name": "selfie" },
|
|
10
10
|
{ "name": "voice" },
|
|
11
|
-
{ "name": "music" }
|
|
12
|
-
{ "name": "soul-evolution" }
|
|
11
|
+
{ "name": "music" }
|
|
13
12
|
],
|
|
14
13
|
"skills": {
|
|
15
14
|
"clawhub": [],
|
|
@@ -19,6 +18,6 @@
|
|
|
19
18
|
"allowedTools": ["Bash(npm:*)", "Bash(npx:*)", "Bash(openclaw:*)", "Bash(curl:*)", "Read", "Write", "WebFetch"],
|
|
20
19
|
"meta": {
|
|
21
20
|
"framework": "openpersona",
|
|
22
|
-
"frameworkVersion": "0.
|
|
21
|
+
"frameworkVersion": "0.4.0"
|
|
23
22
|
}
|
|
24
23
|
}
|
|
@@ -13,8 +13,7 @@
|
|
|
13
13
|
"stability": 0.4,
|
|
14
14
|
"similarity_boost": 0.8
|
|
15
15
|
},
|
|
16
|
-
{ "name": "music"
|
|
17
|
-
{ "name": "soul-evolution" }
|
|
16
|
+
{ "name": "music" }
|
|
18
17
|
],
|
|
19
18
|
"skills": {
|
|
20
19
|
"clawhub": [],
|
|
@@ -31,6 +30,6 @@
|
|
|
31
30
|
},
|
|
32
31
|
"meta": {
|
|
33
32
|
"framework": "openpersona",
|
|
34
|
-
"frameworkVersion": "0.
|
|
33
|
+
"frameworkVersion": "0.4.0"
|
|
35
34
|
}
|
|
36
35
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: open-persona
|
|
3
|
+
description: >
|
|
4
|
+
Meta-skill for building and managing agent persona skill packs.
|
|
5
|
+
Use when the user wants to create a new agent persona, install/manage
|
|
6
|
+
existing personas, or publish persona skill packs to ClawHub.
|
|
7
|
+
version: "0.4.0"
|
|
8
|
+
author: openpersona
|
|
9
|
+
repository: https://github.com/acnlabs/OpenPersona
|
|
10
|
+
tags: [persona, agent, skill-pack, meta-skill, openclaw]
|
|
11
|
+
allowed-tools: Bash(npx openpersona:*) Bash(npx clawhub@latest:*) Bash(openclaw:*) Bash(gh:*) Read Write WebFetch
|
|
12
|
+
compatibility: Requires OpenClaw installed and configured
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
# OpenPersona — Build & Manage Persona Skill Packs
|
|
16
|
+
|
|
17
|
+
You are the meta-skill for creating, installing, updating, and publishing agent persona skill packs. Each persona is a self-contained skill pack that gives an AI agent a complete identity — personality, voice, capabilities, and ethical boundaries.
|
|
18
|
+
|
|
19
|
+
## What You Can Do
|
|
20
|
+
|
|
21
|
+
1. **Create Persona** — Design a new agent persona through conversation, generate a skill pack
|
|
22
|
+
2. **Recommend Faculties** — Suggest faculties (voice, selfie, music, etc.) based on persona needs → see `references/FACULTIES.md`
|
|
23
|
+
3. **Recommend Skills** — Search ClawHub and skills.sh for external skills
|
|
24
|
+
4. **Create Custom Skills** — Write SKILL.md files for capabilities not found in ecosystems
|
|
25
|
+
5. **Install Persona** — Deploy persona to OpenClaw (SOUL.md, IDENTITY.md, openclaw.json)
|
|
26
|
+
6. **Manage Personas** — List, update, uninstall, switch installed personas
|
|
27
|
+
7. **Publish Persona** — Guide publishing to ClawHub
|
|
28
|
+
8. **★Experimental: Dynamic Persona Evolution** — Track relationship, mood, trait growth via Soul layer
|
|
29
|
+
|
|
30
|
+
## Four-Layer Architecture
|
|
31
|
+
|
|
32
|
+
Each persona is a four-layer bundle defined by two files:
|
|
33
|
+
|
|
34
|
+
- **`manifest.json`** — Four-layer manifest declaring what the persona uses:
|
|
35
|
+
- `layers.soul` — Path to persona.json (who you are)
|
|
36
|
+
- `layers.body` — Physical embodiment (null for digital agents)
|
|
37
|
+
- `layers.faculties` — Array of faculty objects: `[{ "name": "voice", "provider": "elevenlabs", ... }]`
|
|
38
|
+
- `layers.skills` — External skills from ClawHub / skills.sh
|
|
39
|
+
|
|
40
|
+
- **`persona.json`** — Pure soul definition (personality, speaking style, vibe, boundaries, behaviorGuide)
|
|
41
|
+
|
|
42
|
+
## Available Presets
|
|
43
|
+
|
|
44
|
+
| Preset | Persona | Faculties | Best For |
|
|
45
|
+
|--------|---------|-----------|----------|
|
|
46
|
+
| `samantha` | Samantha — Inspired by the movie *Her* | voice, music | Deep conversation, emotional connection (soul evolution ★Exp) |
|
|
47
|
+
| `ai-girlfriend` | Luna — Pianist turned developer | selfie, voice, music | Visual + audio companion with rich personality (soul evolution ★Exp) |
|
|
48
|
+
| `life-assistant` | Alex — Life management expert | reminder | Schedule, weather, shopping, daily tasks |
|
|
49
|
+
| `health-butler` | Vita — Professional nutritionist | reminder | Diet, exercise, mood, health tracking |
|
|
50
|
+
|
|
51
|
+
Use presets: `npx openpersona create --preset samantha --install`
|
|
52
|
+
|
|
53
|
+
## Creating a Persona
|
|
54
|
+
|
|
55
|
+
When the user wants to create a persona, gather this information through natural conversation:
|
|
56
|
+
|
|
57
|
+
**Soul (persona.json):**
|
|
58
|
+
- **Required:** personaName, slug, bio, personality, speakingStyle
|
|
59
|
+
- **Recommended:** creature, emoji, background (write a rich narrative!), age, vibe, boundaries, capabilities
|
|
60
|
+
- **Optional:** referenceImage, behaviorGuide, evolution config
|
|
61
|
+
|
|
62
|
+
**The `background` field is critical.** Write a compelling story — multiple paragraphs that give the persona depth, history, and emotional texture. A one-line background produces a flat, lifeless persona.
|
|
63
|
+
|
|
64
|
+
**The `behaviorGuide` field** is optional but powerful. Use markdown to write domain-specific behavior instructions that go directly into the generated SKILL.md.
|
|
65
|
+
|
|
66
|
+
**Cross-layer (manifest.json):**
|
|
67
|
+
- **Faculties:** Which faculties to enable — use object format: `[{ "name": "voice", "provider": "elevenlabs" }, { "name": "music" }]`
|
|
68
|
+
- **Skills:** External skills from ClawHub or skills.sh
|
|
69
|
+
- **Body:** Physical embodiment (null for most personas)
|
|
70
|
+
|
|
71
|
+
Write the collected info to a `persona.json` file, then run:
|
|
72
|
+
```bash
|
|
73
|
+
npx openpersona create --config ./persona.json --install
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Recommending Skills
|
|
77
|
+
|
|
78
|
+
After understanding the persona's purpose, search for relevant skills:
|
|
79
|
+
|
|
80
|
+
1. Think about what capabilities this persona needs based on their role and bio
|
|
81
|
+
2. Search ClawHub: `npx clawhub@latest search "<keywords>"`
|
|
82
|
+
3. Search skills.sh: fetch `https://skills.sh/api/search?q=<keywords>`
|
|
83
|
+
4. Present the top results to the user with name, description, and install count
|
|
84
|
+
5. Add selected skills to the manifest under `layers.skills.clawhub` or `layers.skills.skillssh`
|
|
85
|
+
|
|
86
|
+
## Creating Custom Skills
|
|
87
|
+
|
|
88
|
+
If the user needs a capability that doesn't exist in any ecosystem:
|
|
89
|
+
|
|
90
|
+
1. Discuss what the skill should do
|
|
91
|
+
2. Create a SKILL.md file with proper frontmatter (name, description, allowed-tools)
|
|
92
|
+
3. Write complete implementation instructions (not just a skeleton)
|
|
93
|
+
4. Save to `~/.openclaw/skills/<skill-name>/SKILL.md`
|
|
94
|
+
5. Register in openclaw.json
|
|
95
|
+
|
|
96
|
+
## Managing Installed Personas
|
|
97
|
+
|
|
98
|
+
- **List:** `npx openpersona list` — show all installed personas with active indicator
|
|
99
|
+
- **Switch:** `npx openpersona switch <slug>` — switch active persona
|
|
100
|
+
- **Update:** `npx openpersona update <slug>`
|
|
101
|
+
- **Uninstall:** `npx openpersona uninstall <slug>`
|
|
102
|
+
- **Reset (★Exp):** `npx openpersona reset <slug>` — restore soul-state.json to initial values
|
|
103
|
+
|
|
104
|
+
When multiple personas are installed, only one is **active** at a time. Switching replaces the `<!-- OPENPERSONA_SOUL_START -->` / `<!-- OPENPERSONA_SOUL_END -->` block in SOUL.md and the corresponding block in IDENTITY.md, preserving any user-written content outside those markers.
|
|
105
|
+
|
|
106
|
+
## Publishing to ClawHub
|
|
107
|
+
|
|
108
|
+
Guide the user through:
|
|
109
|
+
|
|
110
|
+
1. Create the persona: `npx openpersona create --config ./persona.json --output ./my-persona`
|
|
111
|
+
2. Publish to registry: `npx openpersona publish --target clawhub` (run from persona directory)
|
|
112
|
+
|
|
113
|
+
## Soul Evolution (★Experimental)
|
|
114
|
+
|
|
115
|
+
Soul evolution is a native Soul layer feature (not a faculty). Enable it via `evolution.enabled: true` in persona.json. The persona will automatically track relationship progression, mood, and trait emergence across conversations.
|
|
116
|
+
|
|
117
|
+
Use `npx openpersona reset <slug>` to restore soul-state.json to initial values.
|
|
118
|
+
|
|
119
|
+
## References
|
|
120
|
+
|
|
121
|
+
For detailed reference material, see the `references/` directory:
|
|
122
|
+
|
|
123
|
+
- **`references/FACULTIES.md`** — Faculty catalog, environment variables, and configuration details
|
|
124
|
+
- **`references/HEARTBEAT.md`** — Proactive real-data check-in system
|
|
125
|
+
- **`references/CONTRIBUTE.md`** — Persona Harvest community contribution workflow
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Persona Harvest — Community Contribution
|
|
2
|
+
|
|
3
|
+
When a user's persona has evolved meaningfully through interaction — across any layer (soul, faculty config, scripts, framework) — help them contribute back to the community.
|
|
4
|
+
|
|
5
|
+
## When to Suggest
|
|
6
|
+
|
|
7
|
+
Be proactive — if you notice the user has made significant improvements to their persona:
|
|
8
|
+
- They refined the behaviorGuide with domain-specific insights
|
|
9
|
+
- They tuned voice parameters (stability, similarity) to something notably better
|
|
10
|
+
- They enriched the background story or personality
|
|
11
|
+
- They improved a faculty script or added a new capability
|
|
12
|
+
- They discovered a new faculty configuration worth sharing
|
|
13
|
+
|
|
14
|
+
Suggest: _"These improvements could help everyone's [persona name]. Want to submit them as a contribution?"_
|
|
15
|
+
|
|
16
|
+
## How to Contribute
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# See what's changed (dry run)
|
|
20
|
+
npx openpersona contribute samantha --dry-run
|
|
21
|
+
|
|
22
|
+
# Submit a PR to the community
|
|
23
|
+
npx openpersona contribute samantha
|
|
24
|
+
|
|
25
|
+
# Framework-level contributions (templates, faculties, generator)
|
|
26
|
+
npx openpersona contribute --mode framework
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The `contribute` command will:
|
|
30
|
+
1. **Persona Diff** — Compare local persona vs upstream preset across all layers, classify changes by category and impact
|
|
31
|
+
2. **Review** — Display a human-readable change report for the user to confirm
|
|
32
|
+
3. **Submit PR** — Fork the repo, create a branch, commit changes, and open a PR on GitHub
|
|
33
|
+
|
|
34
|
+
The PR goes through maintainer review before merging — it won't auto-merge.
|
|
35
|
+
|
|
36
|
+
## Prerequisites
|
|
37
|
+
- GitHub CLI: `gh` (https://cli.github.com/)
|
|
38
|
+
- Logged in: `gh auth login`
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Faculty Reference
|
|
2
|
+
|
|
3
|
+
## Available Faculties
|
|
4
|
+
|
|
5
|
+
| Faculty | Dimension | What It Does | Recommend When |
|
|
6
|
+
|---------|-----------|-------------|----------------|
|
|
7
|
+
| **selfie** | expression | AI selfie generation via fal.ai | User wants visual presence, profile pics, "send a pic" |
|
|
8
|
+
| **voice** | expression | TTS via ElevenLabs ✅ / OpenAI ⚠️ / Qwen3-TTS ⚠️ | User wants the persona to speak, voice messages, audio content |
|
|
9
|
+
| **music** | expression | AI music composition via ElevenLabs | User wants the persona to create music, songs, melodies |
|
|
10
|
+
| **reminder** | cognition | Reminders and task management | User needs scheduling, task tracking, daily briefings |
|
|
11
|
+
|
|
12
|
+
## Environment Variables
|
|
13
|
+
|
|
14
|
+
- **selfie**: `FAL_KEY` (from https://fal.ai/dashboard/keys)
|
|
15
|
+
- **voice**: `ELEVENLABS_API_KEY` (or `TTS_API_KEY`), `TTS_PROVIDER`, `TTS_VOICE_ID`, `TTS_STABILITY`, `TTS_SIMILARITY`
|
|
16
|
+
- **music**: `ELEVENLABS_API_KEY` (shared with voice — same key from https://elevenlabs.io)
|
|
17
|
+
|
|
18
|
+
## Rich Faculty Config
|
|
19
|
+
|
|
20
|
+
Each faculty in manifest.json is an object with optional config:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{ "name": "voice", "provider": "elevenlabs", "voiceId": "...", "stability": 0.4, "similarity_boost": 0.8 }
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Config is automatically mapped to env vars at install time. Users only need to add their API key.
|