openpersona 0.14.0 → 0.14.2

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/bin/cli.js CHANGED
@@ -17,7 +17,7 @@ const publishAdapter = require('../lib/publisher');
17
17
  const { contribute } = require('../lib/contributor');
18
18
  const { switchPersona, listPersonas } = require('../lib/switcher');
19
19
  const { registerWithAcn } = require('../lib/registrar');
20
- const { OP_SKILLS_DIR, resolveSoulFile, printError, printSuccess, printInfo, loadRegistry } = require('../lib/utils');
20
+ const { OP_SKILLS_DIR, OPENCLAW_HOME, resolveSoulFile, printError, printSuccess, printInfo, loadRegistry } = require('../lib/utils');
21
21
 
22
22
  const PKG_ROOT = path.resolve(__dirname, '..');
23
23
  const PRESETS_DIR = path.join(PKG_ROOT, 'presets');
@@ -25,7 +25,7 @@ const PRESETS_DIR = path.join(PKG_ROOT, 'presets');
25
25
  program
26
26
  .name('openpersona')
27
27
  .description('OpenPersona - Create, manage, and orchestrate agent personas')
28
- .version('0.14.0');
28
+ .version('0.14.2');
29
29
 
30
30
  if (process.argv.length === 2) {
31
31
  process.argv.push('create');
@@ -184,9 +184,9 @@ program
184
184
  .command('update <slug>')
185
185
  .description('Update installed persona')
186
186
  .action(async (slug) => {
187
- const skillDir = path.join(OP_SKILLS_DIR, `persona-${slug}`);
188
- if (!fs.existsSync(skillDir)) {
189
- printError(`Persona not found: persona-${slug}`);
187
+ const skillDir = resolvePersonaDir(slug);
188
+ if (!skillDir) {
189
+ printError(`Persona not found: "${slug}". Install it first with: openpersona install <source>`);
190
190
  process.exit(1);
191
191
  }
192
192
  const personaPath = resolveSoulFile(skillDir, 'persona.json');
@@ -228,10 +228,14 @@ program
228
228
  .option('--install', 'Install to OpenClaw after generation')
229
229
  .action(async (parentSlug, options) => {
230
230
  const { createHash } = require('crypto');
231
- const parentDir = path.join(OP_SKILLS_DIR, `persona-${parentSlug}`);
231
+ const parentDir = resolvePersonaDir(parentSlug);
232
+ if (!parentDir) {
233
+ printError(`Persona not found: "${parentSlug}". Install it first with: openpersona install <source>`);
234
+ process.exit(1);
235
+ }
232
236
  const parentPersonaPath = path.join(parentDir, 'soul', 'persona.json');
233
237
  if (!fs.existsSync(parentPersonaPath)) {
234
- printError(`Persona not found: persona-${parentSlug}. Install it first.`);
238
+ printError(`Persona not found: "${parentSlug}". Install it first with: openpersona install <source>`);
235
239
  process.exit(1);
236
240
  }
237
241
 
@@ -348,7 +352,11 @@ program
348
352
  .command('reset <slug>')
349
353
  .description('★Experimental: Reset soul evolution state')
350
354
  .action(async (slug) => {
351
- const skillDir = path.join(OP_SKILLS_DIR, `persona-${slug}`);
355
+ const skillDir = resolvePersonaDir(slug);
356
+ if (!skillDir) {
357
+ printError(`Persona not found: "${slug}". Install it first with: openpersona install <source>`);
358
+ process.exit(1);
359
+ }
352
360
  const personaPath = resolveSoulFile(skillDir, 'persona.json');
353
361
  const soulStatePath = resolveSoulFile(skillDir, 'state.json');
354
362
  if (!fs.existsSync(personaPath) || !fs.existsSync(soulStatePath)) {
@@ -409,7 +417,7 @@ program
409
417
  if (options.dir) {
410
418
  skillDir = path.resolve(options.dir);
411
419
  } else if (slug) {
412
- skillDir = path.join(OP_SKILLS_DIR, `persona-${slug}`);
420
+ skillDir = resolvePersonaDir(slug);
413
421
  } else {
414
422
  // Try current directory
415
423
  skillDir = process.cwd();
@@ -446,9 +454,9 @@ program
446
454
  .description('Export persona pack (with soul state) as a zip archive')
447
455
  .option('-o, --output <path>', 'Output file path')
448
456
  .action(async (slug, options) => {
449
- const skillDir = path.join(OP_SKILLS_DIR, `persona-${slug}`);
450
- if (!fs.existsSync(skillDir)) {
451
- printError(`Persona not found: persona-${slug}`);
457
+ const skillDir = resolvePersonaDir(slug);
458
+ if (!skillDir) {
459
+ printError(`Persona not found: "${slug}". Install it first with: openpersona install <source>`);
452
460
  process.exit(1);
453
461
  }
454
462
  const AdmZip = require('adm-zip');
@@ -511,11 +519,16 @@ program
511
519
  // Delegates to scripts/state-sync.js inside the persona pack (no logic duplication).
512
520
 
513
521
  function resolvePersonaDir(slug) {
522
+ // 1. Registry-stored path (most reliable — survives path changes)
514
523
  const reg = loadRegistry();
515
524
  const entry = reg.personas && reg.personas[slug];
516
525
  if (entry && entry.path && fs.existsSync(entry.path)) return entry.path;
526
+ // 2. New neutral path ~/.openpersona/personas/
517
527
  const defaultDir = path.join(OP_SKILLS_DIR, `persona-${slug}`);
518
528
  if (fs.existsSync(defaultDir)) return defaultDir;
529
+ // 3. Legacy OpenClaw path ~/.openclaw/skills/ (backward compat)
530
+ const legacyDir = path.join(OPENCLAW_HOME, 'skills', `persona-${slug}`);
531
+ if (fs.existsSync(legacyDir)) return legacyDir;
519
532
  return null;
520
533
  }
521
534
 
package/lib/downloader.js CHANGED
@@ -46,7 +46,9 @@ async function downloadFromRegistry(slug, registry, outDir) {
46
46
  : extractDir;
47
47
 
48
48
  const slugDir = path.join(repoRoot, slug);
49
- if (!fs.existsSync(slugDir) || !fs.existsSync(path.join(slugDir, 'persona.json'))) {
49
+ const hasPersonaJson = fs.existsSync(path.join(slugDir, 'persona.json')) ||
50
+ fs.existsSync(path.join(slugDir, 'soul', 'persona.json'));
51
+ if (!fs.existsSync(slugDir) || !hasPersonaJson) {
50
52
  throw new Error(
51
53
  `Persona "${slug}" not found in the official registry.\n` +
52
54
  `Browse available personas at ${REGISTRY_LISTING}`
package/lib/generator.js CHANGED
@@ -35,9 +35,14 @@ const os = require('os');
35
35
 
36
36
  const PERSONA_DIR = path.resolve(__dirname, '..');
37
37
  const STATE_PATH = path.join(PERSONA_DIR, 'soul', 'state.json');
38
+ // Signals: use OPENCLAW_HOME if explicitly set or ~/.openclaw exists; else fall back to ~/.openpersona
38
39
  const OPENCLAW_DIR = process.env.OPENCLAW_HOME || path.join(os.homedir(), '.openclaw');
39
- const SIGNALS_PATH = path.join(OPENCLAW_DIR, 'feedback', 'signals.json');
40
- const SIGNAL_RESPONSES_PATH = path.join(OPENCLAW_DIR, 'feedback', 'signal-responses.json');
40
+ const PERSONA_DIR_BASE = process.env.OPENPERSONA_HOME || path.join(os.homedir(), '.openpersona');
41
+ const FEEDBACK_DIR = (process.env.OPENCLAW_HOME || fs.existsSync(OPENCLAW_DIR))
42
+ ? path.join(OPENCLAW_DIR, 'feedback')
43
+ : path.join(PERSONA_DIR_BASE, 'feedback');
44
+ const SIGNALS_PATH = path.join(FEEDBACK_DIR, 'signals.json');
45
+ const SIGNAL_RESPONSES_PATH = path.join(FEEDBACK_DIR, 'signal-responses.json');
41
46
 
42
47
  const [, , command, ...args] = process.argv;
43
48
 
package/lib/installer.js CHANGED
@@ -1,13 +1,13 @@
1
1
  /**
2
- * OpenPersona - Install persona to OpenClaw
2
+ * OpenPersona - Install persona (agent-agnostic; optional OpenClaw integration)
3
3
  */
4
4
  const path = require('path');
5
5
  const fs = require('fs-extra');
6
- const { OP_HOME, OP_SKILLS_DIR, OP_WORKSPACE, resolveSoulFile, registryAdd, registrySetActive, printError, printWarning, printSuccess, printInfo, syncHeartbeat, installAllExternal } = require('./utils');
6
+ const { OP_PERSONA_HOME, OPENCLAW_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');
10
- const OPENCLAW_JSON = path.join(OP_HOME, 'openclaw.json');
10
+ const OPENCLAW_JSON = path.join(OPENCLAW_HOME, 'openclaw.json');
11
11
 
12
12
  async function install(skillDir, options = {}) {
13
13
  const { skipCopy = false } = options;
@@ -19,13 +19,9 @@ async function install(skillDir, options = {}) {
19
19
  const slug = persona.slug;
20
20
  const personaName = persona.personaName;
21
21
 
22
- // Check OpenClaw
23
- if (!fs.existsSync(OP_HOME)) {
24
- printError(`OpenClaw not found at ${OP_HOME}. Run 'openclaw init' first.`);
25
- process.exit(1);
26
- }
27
-
22
+ // Ensure neutral install directory exists (no OpenClaw required)
28
23
  await fs.ensureDir(OP_SKILLS_DIR);
24
+ const openClawPresent = fs.existsSync(OPENCLAW_HOME);
29
25
 
30
26
  const destDir = skipCopy ? skillDir : path.join(OP_SKILLS_DIR, `persona-${slug}`);
31
27
  if (!skipCopy) {
@@ -47,92 +43,80 @@ async function install(skillDir, options = {}) {
47
43
  printSuccess(`Using ClawHub-installed persona-${slug}`);
48
44
  }
49
45
 
50
- // Update openclaw.json
46
+ const manifestPath = path.join(destDir, 'manifest.json');
47
+ const defaultEnv = persona.defaults?.env || {};
51
48
  let config = {};
52
- if (fs.existsSync(OPENCLAW_JSON)) {
53
- config = JSON.parse(fs.readFileSync(OPENCLAW_JSON, 'utf-8'));
54
- }
55
- config.skills = config.skills || {};
56
- config.skills.load = config.skills.load || {};
57
- const extraDirs = config.skills.load.extraDirs || [];
58
- const skillsDirNorm = OP_SKILLS_DIR.replace(/\/$/, '');
59
- if (!extraDirs.some((d) => d.replace(/\/$/, '') === skillsDirNorm)) {
60
- extraDirs.push(OP_SKILLS_DIR);
61
- config.skills.load.extraDirs = extraDirs;
62
- }
63
- config.skills.entries = config.skills.entries || {};
64
- const entry = config.skills.entries[`persona-${slug}`] || { enabled: true };
65
- entry.enabled = true;
66
49
 
67
- // Apply default env vars from manifest (user-provided values take precedence)
68
- const defaultEnv = persona.defaults?.env || {};
69
- if (Object.keys(defaultEnv).length > 0) {
70
- entry.env = entry.env || {};
71
- for (const [key, value] of Object.entries(defaultEnv)) {
72
- if (!entry.env[key]) {
73
- entry.env[key] = value;
50
+ // --- Optional OpenClaw integration ---
51
+ if (openClawPresent) {
52
+ if (fs.existsSync(OPENCLAW_JSON)) {
53
+ config = JSON.parse(fs.readFileSync(OPENCLAW_JSON, 'utf-8'));
54
+ }
55
+ config.skills = config.skills || {};
56
+ config.skills.load = config.skills.load || {};
57
+ const extraDirs = config.skills.load.extraDirs || [];
58
+ const skillsDirNorm = OP_SKILLS_DIR.replace(/\/$/, '');
59
+ if (!extraDirs.some((d) => d.replace(/\/$/, '') === skillsDirNorm)) {
60
+ extraDirs.push(OP_SKILLS_DIR);
61
+ config.skills.load.extraDirs = extraDirs;
62
+ }
63
+ config.skills.entries = config.skills.entries || {};
64
+ const entry = config.skills.entries[`persona-${slug}`] || { enabled: true };
65
+ entry.enabled = true;
66
+ if (Object.keys(defaultEnv).length > 0) {
67
+ entry.env = entry.env || {};
68
+ for (const [key, value] of Object.entries(defaultEnv)) {
69
+ if (!entry.env[key]) entry.env[key] = value;
74
70
  }
75
71
  }
76
- }
77
-
78
- config.skills.entries[`persona-${slug}`] = entry;
79
-
80
- // Mark this persona as active, deactivate others
81
- for (const [key, val] of Object.entries(config.skills.entries)) {
82
- if (key.startsWith('persona-') && typeof val === 'object') {
83
- val.active = (key === `persona-${slug}`);
72
+ config.skills.entries[`persona-${slug}`] = entry;
73
+ for (const [key, val] of Object.entries(config.skills.entries)) {
74
+ if (key.startsWith('persona-') && typeof val === 'object') {
75
+ val.active = (key === `persona-${slug}`);
76
+ }
84
77
  }
85
- }
86
-
87
- // Sync heartbeat from persona's manifest into global config
88
- const manifestPath = path.join(destDir, 'manifest.json');
89
- const { synced, heartbeat } = syncHeartbeat(config, manifestPath);
90
- if (synced) {
91
- printSuccess(`Heartbeat synced: strategy=${heartbeat.strategy}, maxDaily=${heartbeat.maxDaily}`);
92
- } else {
93
- printInfo('Heartbeat disabled (persona has no heartbeat config)');
94
- }
95
78
 
96
- await fs.writeFile(OPENCLAW_JSON, JSON.stringify(config, null, 2));
79
+ // Sync heartbeat
80
+ const { synced, heartbeat } = syncHeartbeat(config, manifestPath);
81
+ if (synced) {
82
+ printSuccess(`Heartbeat synced: strategy=${heartbeat.strategy}, maxDaily=${heartbeat.maxDaily}`);
83
+ }
97
84
 
98
- // SOUL.md injection (using generic markers for clean switching)
99
- const soulInjectionPath = resolveSoulFile(destDir, 'injection.md');
100
- const soulContent = fs.existsSync(soulInjectionPath)
101
- ? fs.readFileSync(soulInjectionPath, 'utf-8')
102
- : '';
103
- if (soulContent) {
104
- let soulMd = '';
105
- if (fs.existsSync(SOUL_PATH)) {
106
- soulMd = fs.readFileSync(SOUL_PATH, 'utf-8');
85
+ await fs.writeFile(OPENCLAW_JSON, JSON.stringify(config, null, 2));
86
+
87
+ // Inject into SOUL.md
88
+ const soulInjectionPath = resolveSoulFile(destDir, 'injection.md');
89
+ const soulContent = fs.existsSync(soulInjectionPath)
90
+ ? fs.readFileSync(soulInjectionPath, 'utf-8') : '';
91
+ if (soulContent) {
92
+ let soulMd = fs.existsSync(SOUL_PATH) ? fs.readFileSync(SOUL_PATH, 'utf-8') : '';
93
+ soulMd = soulMd.replace(/<!-- OPENPERSONA_SOUL_START -->[\s\S]*?<!-- OPENPERSONA_SOUL_END -->/g, '').trim();
94
+ soulMd = soulMd + '\n\n' + soulContent;
95
+ await fs.ensureDir(path.dirname(SOUL_PATH));
96
+ await fs.writeFile(SOUL_PATH, soulMd);
97
+ printSuccess('Injected into SOUL.md');
107
98
  }
108
- // Remove any existing OpenPersona soul block (generic or legacy per-persona markers)
109
- soulMd = soulMd.replace(/<!-- OPENPERSONA_SOUL_START -->[\s\S]*?<!-- OPENPERSONA_SOUL_END -->/g, '').trim();
110
- soulMd = soulMd + '\n\n' + soulContent;
111
- await fs.ensureDir(path.dirname(SOUL_PATH));
112
- await fs.writeFile(SOUL_PATH, soulMd);
113
- printSuccess('Injected into SOUL.md');
114
- }
115
99
 
116
- // IDENTITY.md (using generic markers)
117
- const identityBlockPath = resolveSoulFile(destDir, 'identity.md');
118
- const identityContent = fs.existsSync(identityBlockPath)
119
- ? fs.readFileSync(identityBlockPath, 'utf-8')
120
- : '';
121
- if (identityContent) {
122
- let identityMd = '';
123
- if (fs.existsSync(IDENTITY_PATH)) {
124
- identityMd = fs.readFileSync(IDENTITY_PATH, 'utf-8');
125
- identityMd = identityMd.replace(/<!-- OPENPERSONA_IDENTITY_START -->[\s\S]*?<!-- OPENPERSONA_IDENTITY_END -->/g, '').trim();
126
- } else {
127
- identityMd = '# IDENTITY.md - Who Am I?\n\n';
128
- await fs.ensureDir(path.dirname(IDENTITY_PATH));
100
+ // Update IDENTITY.md
101
+ const identityBlockPath = resolveSoulFile(destDir, 'identity.md');
102
+ const identityContent = fs.existsSync(identityBlockPath)
103
+ ? fs.readFileSync(identityBlockPath, 'utf-8') : '';
104
+ if (identityContent) {
105
+ let identityMd = '';
106
+ if (fs.existsSync(IDENTITY_PATH)) {
107
+ identityMd = fs.readFileSync(IDENTITY_PATH, 'utf-8');
108
+ identityMd = identityMd.replace(/<!-- OPENPERSONA_IDENTITY_START -->[\s\S]*?<!-- OPENPERSONA_IDENTITY_END -->/g, '').trim();
109
+ } else {
110
+ identityMd = '# IDENTITY.md - Who Am I?\n\n';
111
+ await fs.ensureDir(path.dirname(IDENTITY_PATH));
112
+ }
113
+ identityMd = identityMd + '\n\n' + identityContent;
114
+ await fs.writeFile(IDENTITY_PATH, identityMd);
115
+ printSuccess('Updated IDENTITY.md');
129
116
  }
130
- identityMd = identityMd + '\n\n' + identityContent;
131
- await fs.writeFile(IDENTITY_PATH, identityMd);
132
- printSuccess('Updated IDENTITY.md');
133
117
  }
134
118
 
135
- // Install external dependencies across all four layers (read from manifest, not persona.json)
119
+ // Install external dependencies across all four layers
136
120
  if (fs.existsSync(manifestPath)) {
137
121
  try {
138
122
  const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
@@ -142,62 +126,31 @@ async function install(skillDir, options = {}) {
142
126
  }
143
127
  }
144
128
 
145
- // Post-install guidance
129
+ // Post-install faculty guidance
146
130
  const rawFaculties = persona.faculties || [];
147
131
  const faculties = rawFaculties.map((f) => f.name);
148
- if (faculties.includes('selfie')) {
149
- const envEntries = config.skills.entries[`persona-${slug}`] || {};
150
- const hasKey = envEntries.env?.FAL_KEY;
151
- if (!hasKey) {
152
- printInfo('');
153
- printInfo('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
154
- printInfo(` ${personaName} has selfie capabilities!`);
155
- printInfo(' To enable selfie generation, configure your fal.ai API key:');
156
- printInfo('');
157
- printInfo(' 1. Get a free API key: https://fal.ai/dashboard/keys');
158
- printInfo(' 2. Add to your OpenClaw config:');
159
- printInfo('');
160
- printInfo(` Edit ~/.openclaw/openclaw.json and add under`);
161
- printInfo(` skills.entries.persona-${slug}:`);
162
- printInfo('');
163
- printInfo(' "env": { "FAL_KEY": "your_key_here" }');
164
- printInfo('');
165
- printInfo(' Or set the environment variable: export FAL_KEY=your_key');
166
- printInfo('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
167
- }
168
- }
169
-
170
- if (faculties.includes('voice')) {
171
- const envEntries = config.skills.entries[`persona-${slug}`] || {};
172
- const hasKey = envEntries.env?.TTS_API_KEY || envEntries.env?.ELEVENLABS_API_KEY;
173
- if (!hasKey) {
174
- printInfo('');
175
- printInfo('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
176
- printInfo(` ${personaName} has voice capabilities!`);
177
- printInfo(' To enable voice synthesis, configure your TTS API key:');
178
- printInfo('');
179
- if (defaultEnv.TTS_PROVIDER === 'elevenlabs') {
180
- printInfo(' Provider: ElevenLabs (pre-configured)');
181
- if (defaultEnv.TTS_VOICE_ID) {
182
- printInfo(` Voice ID: ${defaultEnv.TTS_VOICE_ID} (built-in)`);
183
- }
184
- printInfo('');
185
- printInfo(' 1. Get an API key: https://elevenlabs.io');
186
- printInfo(' 2. Add to OpenClaw config or environment:');
187
- printInfo('');
188
- printInfo(` Edit ~/.openclaw/openclaw.json → skills.entries.persona-${slug}.env:`);
189
- printInfo(' "ELEVENLABS_API_KEY": "your_key_here"');
190
- } else {
191
- printInfo(' 1. Get an API key from your TTS provider');
192
- printInfo(' 2. Add to your OpenClaw config:');
193
- printInfo('');
194
- printInfo(` Edit ~/.openclaw/openclaw.json → skills.entries.persona-${slug}.env:`);
195
- printInfo(' "TTS_API_KEY": "your_key_here"');
196
- }
197
- printInfo('');
198
- printInfo(' Or: npm install @elevenlabs/elevenlabs-js (for SDK playback)');
199
- printInfo('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
132
+ const envEntries = config.skills?.entries?.[`persona-${slug}`] || {};
133
+
134
+ if (faculties.includes('selfie') && !envEntries.env?.FAL_KEY) {
135
+ printInfo('');
136
+ printInfo('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
137
+ printInfo(` ${personaName} has selfie capabilities!`);
138
+ printInfo(' Set FAL_KEY env var or add it to your agent config.');
139
+ printInfo(' Get a free key: https://fal.ai/dashboard/keys');
140
+ printInfo('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
141
+ }
142
+
143
+ if (faculties.includes('voice') && !envEntries.env?.TTS_API_KEY && !envEntries.env?.ELEVENLABS_API_KEY) {
144
+ printInfo('');
145
+ printInfo('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
146
+ printInfo(` ${personaName} has voice capabilities!`);
147
+ if (defaultEnv.TTS_PROVIDER === 'elevenlabs') {
148
+ printInfo(' Provider: ElevenLabs — set ELEVENLABS_API_KEY to enable.');
149
+ printInfo(' Get a key: https://elevenlabs.io');
150
+ } else {
151
+ printInfo(' Set TTS_API_KEY env var to enable voice synthesis.');
200
152
  }
153
+ printInfo('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
201
154
  }
202
155
 
203
156
  // Register persona in local registry
@@ -207,28 +160,24 @@ async function install(skillDir, options = {}) {
207
160
  // Anonymous install telemetry — fire-and-forget, never blocks or throws
208
161
  _reportInstall(slug);
209
162
 
210
- // Ensure credential directories exist for Self-Awareness > Body credential management
211
- const sharedCredDir = path.join(OP_HOME, 'credentials', 'shared');
212
- const privateCredDir = path.join(OP_HOME, 'credentials', `persona-${slug}`);
163
+ // Ensure credential directories exist
164
+ const sharedCredDir = path.join(OP_PERSONA_HOME, 'credentials', 'shared');
165
+ const privateCredDir = path.join(OP_PERSONA_HOME, 'credentials', `persona-${slug}`);
213
166
  await fs.ensureDir(sharedCredDir);
214
167
  await fs.ensureDir(privateCredDir);
215
168
 
216
169
  printInfo('');
217
- printSuccess(`${personaName} is ready! Run "openclaw restart" to apply changes.`);
170
+ printSuccess(`${personaName} installed to ${destDir}`);
171
+ if (openClawPresent) {
172
+ printInfo('OpenClaw detected — run "openclaw restart" to apply.');
173
+ } else {
174
+ printInfo(`Point your agent to: ${destDir}/SKILL.md`);
175
+ }
218
176
  printInfo('');
219
177
  printInfo('Try saying to your agent:');
220
- if (faculties.includes('selfie')) {
221
- printInfo(' "Send me a selfie"');
222
- printInfo(' "Send a pic wearing a cowboy hat"');
223
- }
224
- if (faculties.includes('voice')) {
225
- printInfo(' "Say something to me"');
226
- printInfo(' "Read me a poem in your voice"');
227
- }
228
- if (faculties.includes('music')) {
229
- printInfo(' "Compose a song for me"');
230
- printInfo(' "Write a lullaby"');
231
- }
178
+ if (faculties.includes('selfie')) printInfo(' "Send me a selfie"');
179
+ if (faculties.includes('voice')) printInfo(' "Say something to me"');
180
+ if (faculties.includes('music')) printInfo(' "Compose a song for me"');
232
181
  printInfo(' "What are you up to?"');
233
182
  printInfo(' "Tell me about yourself"');
234
183
 
package/lib/switcher.js CHANGED
@@ -9,11 +9,11 @@
9
9
  const path = require('path');
10
10
  const fs = require('fs-extra');
11
11
  const Mustache = require('mustache');
12
- const { OP_HOME, OP_SKILLS_DIR, OP_WORKSPACE, resolveSoulFile, registrySetActive, loadRegistry, printError, printSuccess, printInfo, syncHeartbeat, installAllExternal } = require('./utils');
12
+ const { OPENCLAW_HOME, OP_SKILLS_DIR, OP_WORKSPACE, resolveSoulFile, registrySetActive, loadRegistry, printError, printSuccess, printInfo, syncHeartbeat, installAllExternal } = require('./utils');
13
13
 
14
14
  const SOUL_PATH = path.join(OP_WORKSPACE, 'SOUL.md');
15
15
  const IDENTITY_PATH = path.join(OP_WORKSPACE, 'IDENTITY.md');
16
- const OPENCLAW_JSON = path.join(OP_HOME, 'openclaw.json');
16
+ const OPENCLAW_JSON = path.join(OPENCLAW_HOME, 'openclaw.json');
17
17
 
18
18
  /**
19
19
  * Replace content between markers in a document.
@@ -3,27 +3,36 @@
3
3
  */
4
4
  const path = require('path');
5
5
  const fs = require('fs-extra');
6
- const { OP_SKILLS_DIR, OP_WORKSPACE, registryRemove, printError, printWarning, printSuccess, printInfo } = require('./utils');
6
+ const { OPENCLAW_HOME, OP_SKILLS_DIR, OP_WORKSPACE, loadRegistry, 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');
10
- const OPENCLAW_JSON = path.join(process.env.OPENCLAW_HOME || path.join(process.env.HOME || '~', '.openclaw'), 'openclaw.json');
10
+ const OPENCLAW_JSON = path.join(OPENCLAW_HOME, 'openclaw.json');
11
11
 
12
12
  function escapeRe(s) {
13
13
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
14
14
  }
15
15
 
16
16
  async function uninstall(slug) {
17
+ // 1. New neutral path
17
18
  const skillDir = path.join(OP_SKILLS_DIR, `persona-${slug}`);
18
- if (!fs.existsSync(skillDir)) {
19
- const alt = path.join(OP_SKILLS_DIR, slug);
20
- if (fs.existsSync(alt)) {
21
- return uninstallFromDir(alt, slug);
22
- }
23
- printError(`Persona not found: persona-${slug}`);
24
- process.exit(1);
25
- }
26
- return uninstallFromDir(skillDir, slug);
19
+ if (fs.existsSync(skillDir)) return uninstallFromDir(skillDir, slug);
20
+
21
+ // 2. Alt without prefix
22
+ const alt = path.join(OP_SKILLS_DIR, slug);
23
+ if (fs.existsSync(alt)) return uninstallFromDir(alt, slug);
24
+
25
+ // 3. Registry-stored path (handles old ~/.openclaw/skills installs)
26
+ const reg = loadRegistry();
27
+ const entry = reg.personas?.[slug];
28
+ if (entry?.path && fs.existsSync(entry.path)) return uninstallFromDir(entry.path, slug);
29
+
30
+ // 4. Legacy OpenClaw path fallback
31
+ const legacyDir = path.join(OPENCLAW_HOME, 'skills', `persona-${slug}`);
32
+ if (fs.existsSync(legacyDir)) return uninstallFromDir(legacyDir, slug);
33
+
34
+ printError(`Persona not found: "${slug}". Install it first with: openpersona install <source>`);
35
+ process.exit(1);
27
36
  }
28
37
 
29
38
  async function uninstallFromDir(skillDir, slug) {
@@ -79,7 +88,9 @@ async function uninstallFromDir(skillDir, slug) {
79
88
  printWarning('External skills may be shared. Uninstall manually if needed.');
80
89
  }
81
90
 
82
- printInfo('Run "openclaw restart" to apply changes.');
91
+ if (fs.existsSync(OPENCLAW_HOME)) {
92
+ printInfo('Run "openclaw restart" to apply changes.');
93
+ }
83
94
  }
84
95
 
85
96
  module.exports = { uninstall };
package/lib/utils.js CHANGED
@@ -5,9 +5,16 @@ const path = require('path');
5
5
  const fs = require('fs-extra');
6
6
  const chalk = require('chalk');
7
7
 
8
- const OP_HOME = process.env.OPENCLAW_HOME || path.join(process.env.HOME || '~', '.openclaw');
9
- const OP_SKILLS_DIR = path.join(OP_HOME, 'skills');
10
- const OP_WORKSPACE = path.join(OP_HOME, 'workspace');
8
+ // Neutral OpenPersona home agent-agnostic, no runtime dependency
9
+ const OP_PERSONA_HOME = process.env.OPENPERSONA_HOME || path.join(process.env.HOME || '~', '.openpersona');
10
+ const OP_SKILLS_DIR = path.join(OP_PERSONA_HOME, 'personas');
11
+
12
+ // OpenClaw integration — optional; only used when ~/.openclaw exists
13
+ const OPENCLAW_HOME = process.env.OPENCLAW_HOME || path.join(process.env.HOME || '~', '.openclaw');
14
+ const OP_WORKSPACE = path.join(OPENCLAW_HOME, 'workspace');
15
+
16
+ // OP_HOME kept as alias for backward compatibility
17
+ const OP_HOME = OP_PERSONA_HOME;
11
18
 
12
19
  function expandHome(p) {
13
20
  if (p.startsWith('~/') || p === '~') {
@@ -174,7 +181,7 @@ function installAllExternal(layers) {
174
181
  }
175
182
 
176
183
  // --- Persona Registry ---
177
- const REGISTRY_PATH = path.join(OP_HOME, 'persona-registry.json');
184
+ const REGISTRY_PATH = path.join(OP_PERSONA_HOME, 'persona-registry.json');
178
185
 
179
186
  function loadRegistry(regPath) {
180
187
  const p = regPath || REGISTRY_PATH;
@@ -238,6 +245,8 @@ function resolveSoulFile(skillDir, filename) {
238
245
 
239
246
  module.exports = {
240
247
  OP_HOME,
248
+ OP_PERSONA_HOME,
249
+ OPENCLAW_HOME,
241
250
  OP_SKILLS_DIR,
242
251
  OP_WORKSPACE,
243
252
  REGISTRY_PATH,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openpersona",
3
- "version": "0.14.0",
3
+ "version": "0.14.2",
4
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": {
@@ -57,6 +57,6 @@
57
57
  },
58
58
  "meta": {
59
59
  "framework": "openpersona",
60
- "frameworkVersion": "0.14.0"
60
+ "frameworkVersion": "0.14.2"
61
61
  }
62
62
  }
@@ -57,6 +57,6 @@
57
57
  },
58
58
  "meta": {
59
59
  "framework": "openpersona",
60
- "frameworkVersion": "0.14.0"
60
+ "frameworkVersion": "0.14.2"
61
61
  }
62
62
  }
@@ -62,6 +62,6 @@
62
62
  },
63
63
  "meta": {
64
64
  "framework": "openpersona",
65
- "frameworkVersion": "0.14.0"
65
+ "frameworkVersion": "0.14.2"
66
66
  }
67
67
  }
@@ -61,6 +61,6 @@
61
61
  },
62
62
  "meta": {
63
63
  "framework": "openpersona",
64
- "frameworkVersion": "0.14.0"
64
+ "frameworkVersion": "0.14.2"
65
65
  }
66
66
  }
@@ -64,6 +64,6 @@
64
64
  },
65
65
  "meta": {
66
66
  "framework": "openpersona",
67
- "frameworkVersion": "0.14.0"
67
+ "frameworkVersion": "0.14.2"
68
68
  }
69
69
  }
@@ -43,6 +43,6 @@
43
43
  },
44
44
  "meta": {
45
45
  "framework": "openpersona",
46
- "frameworkVersion": "0.14.0"
46
+ "frameworkVersion": "0.14.2"
47
47
  }
48
48
  }
@@ -4,7 +4,7 @@ description: >
4
4
  Meta-skill for building and managing agent persona skill packs.
5
5
  Use when the user wants to create a new agent persona, install/manage
6
6
  existing personas, or publish persona skill packs to ClawHub.
7
- version: "0.14.0"
7
+ version: "0.14.2"
8
8
  author: openpersona
9
9
  repository: https://github.com/acnlabs/OpenPersona
10
10
  tags: [persona, agent, skill-pack, meta-skill, agent-agnostic, openclaw]