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.
@@ -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 = 'ACNet-AI/OpenPersona';
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} "${tmpDir}" -- --depth 1`, { stdio: 'pipe' });
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
- execSync(`git commit -m "${commitMsg}"`, { cwd: tmpDir, stdio: 'pipe' });
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}:${branch} --title "${title}" --body "${body.replace(/"/g, '\\"')}"`,
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/ACNet-AI/OpenPersona/pulls');
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/ACNet-AI/OpenPersona.git');
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 ACNet-AI/OpenPersona');
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 ACNet-AI/OpenPersona');
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(npm:*)', 'Bash(npx:*)', 'Bash(openclaw:*)', 'Read', 'Write'];
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
- // Ensure soul-evolution if evolution.enabled
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.3.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
- if (cfg.apiKey) envDefaults.SUNO_API_KEY = cfg.apiKey;
272
- if (cfg.model) envDefaults.SUNO_MODEL = cfg.model;
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', 'faculties', 'soul-evolution', 'soul-state.template.json'),
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}`);
@@ -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 "${personaDir}" --slug "${slug}" --name "${personaName}" --version "${version}" --changelog "${changelog}" --tags openpersona,persona`,
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.3.0",
4
- "description": "Open four-layer agent framework — Soul/Body/Faculty/Skill. Create, manage, and orchestrate AI personas.",
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": "ACNet-AI",
19
+ "author": "acnlabs",
20
20
  "license": "MIT",
21
21
  "repository": {
22
22
  "type": "git",
23
- "url": "git+https://github.com/ACNet-AI/OpenPersona.git"
23
+ "url": "git+https://github.com/acnlabs/OpenPersona.git"
24
24
  },
25
- "homepage": "https://github.com/ACNet-AI/OpenPersona#readme",
25
+ "homepage": "https://github.com/acnlabs/OpenPersona#readme",
26
26
  "bugs": {
27
- "url": "https://github.com/ACNet-AI/OpenPersona/issues"
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
- "skill/",
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.3.0"
21
+ "frameworkVersion": "0.4.0"
23
22
  }
24
23
  }
@@ -16,6 +16,6 @@
16
16
  "allowedTools": ["Bash(npm:*)", "Bash(npx:*)", "Bash(openclaw:*)", "Read", "Write"],
17
17
  "meta": {
18
18
  "framework": "openpersona",
19
- "frameworkVersion": "0.3.0"
19
+ "frameworkVersion": "0.4.0"
20
20
  }
21
21
  }
@@ -16,6 +16,6 @@
16
16
  "allowedTools": ["Bash(npm:*)", "Bash(npx:*)", "Bash(openclaw:*)", "Read", "Write"],
17
17
  "meta": {
18
18
  "framework": "openpersona",
19
- "frameworkVersion": "0.3.0"
19
+ "frameworkVersion": "0.4.0"
20
20
  }
21
21
  }
@@ -13,8 +13,7 @@
13
13
  "stability": 0.4,
14
14
  "similarity_boost": 0.8
15
15
  },
16
- { "name": "music", "model": "V4_5ALL" },
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.3.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.