@openher/cli 1.0.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.
Files changed (2) hide show
  1. package/cli.mjs +590 -0
  2. package/package.json +26 -0
package/cli.mjs ADDED
@@ -0,0 +1,590 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * @openher/cli — One-click installer for OpenHer Persona Engine
5
+ *
6
+ * Usage:
7
+ * npx -y @openher/cli install
8
+ *
9
+ * What it does:
10
+ * 1. Checks prerequisites (openclaw, python3, git)
11
+ * 2. Installs the OpenClaw plugin
12
+ * 3. Clones the backend repo & sets up Python venv
13
+ * 4. Interactive setup: choose LLM provider, enter API key
14
+ * 5. Starts the backend server
15
+ * 6. Restarts OpenClaw gateway
16
+ */
17
+
18
+ import { execSync, spawn, spawnSync } from "node:child_process";
19
+ import { createInterface } from "node:readline";
20
+ import { existsSync, writeFileSync, mkdirSync } from "node:fs";
21
+ import { homedir } from "node:os";
22
+ import { join } from "node:path";
23
+
24
+ const PLUGIN_SPEC = "@openher/openclaw-plugin";
25
+ const REPO_URL = "https://github.com/kellyvv/openher-openclaw-plugin.git";
26
+ const DEFAULT_PORT = 8800;
27
+
28
+ // ── Colors ───────────────────────────────────────────────────────────────────
29
+
30
+ const C = {
31
+ reset: "\x1b[0m",
32
+ bold: "\x1b[1m",
33
+ dim: "\x1b[2m",
34
+ cyan: "\x1b[36m",
35
+ green: "\x1b[32m",
36
+ yellow: "\x1b[33m",
37
+ red: "\x1b[31m",
38
+ magenta: "\x1b[35m",
39
+ };
40
+
41
+ function log(msg) {
42
+ console.log(`${C.cyan}[openher]${C.reset} ${msg}`);
43
+ }
44
+ function success(msg) {
45
+ console.log(`${C.green}[openher]${C.reset} ${C.green}✓${C.reset} ${msg}`);
46
+ }
47
+ function warn(msg) {
48
+ console.log(`${C.yellow}[openher]${C.reset} ${C.yellow}⚠${C.reset} ${msg}`);
49
+ }
50
+ function error(msg) {
51
+ console.error(`${C.red}[openher]${C.reset} ${C.red}✗${C.reset} ${msg}`);
52
+ }
53
+
54
+ // ── Helpers ──────────────────────────────────────────────────────────────────
55
+
56
+ function run(cmd, opts = {}) {
57
+ const { silent = true, cwd } = opts;
58
+ const stdio = silent ? ["pipe", "pipe", "pipe"] : "inherit";
59
+ const result = spawnSync(cmd, { shell: true, stdio, cwd });
60
+ if (result.status !== 0) {
61
+ const err = new Error(`Command failed (exit ${result.status}): ${cmd}`);
62
+ err.stderr = silent ? (result.stderr || "").toString() : "";
63
+ throw err;
64
+ }
65
+ return silent ? (result.stdout || "").toString().trim() : "";
66
+ }
67
+
68
+ function which(bin) {
69
+ try {
70
+ return execSync(`which ${bin}`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
71
+ } catch {
72
+ return null;
73
+ }
74
+ }
75
+
76
+ function ask(question) {
77
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
78
+ return new Promise((resolve) => {
79
+ rl.question(`${C.cyan}[openher]${C.reset} ${question}`, (answer) => {
80
+ rl.close();
81
+ resolve(answer.trim());
82
+ });
83
+ });
84
+ }
85
+
86
+ function askSecret(question) {
87
+ return new Promise((resolve) => {
88
+ process.stdout.write(`${C.cyan}[openher]${C.reset} ${question}`);
89
+ const rl = createInterface({ input: process.stdin, terminal: false });
90
+ // Mute echo
91
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
92
+ let buf = "";
93
+ process.stdin.resume();
94
+ process.stdin.on("data", function handler(chunk) {
95
+ const s = chunk.toString();
96
+ for (const ch of s) {
97
+ if (ch === "\n" || ch === "\r") {
98
+ process.stdin.removeListener("data", handler);
99
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
100
+ process.stdin.pause();
101
+ process.stdout.write("\n");
102
+ rl.close();
103
+ resolve(buf);
104
+ return;
105
+ } else if (ch === "\u0003") {
106
+ // Ctrl+C
107
+ process.exit(1);
108
+ } else if (ch === "\x7f" || ch === "\b") {
109
+ buf = buf.slice(0, -1);
110
+ } else {
111
+ buf += ch;
112
+ process.stdout.write("*");
113
+ }
114
+ }
115
+ });
116
+ });
117
+ }
118
+
119
+ // ── LLM Providers ────────────────────────────────────────────────────────────
120
+
121
+ const PROVIDERS = [
122
+ { id: "minimax", name: "MiniMax (recommended)", env: "MINIMAX_LLM_API_KEY", model: "MiniMax-M2.7" },
123
+ { id: "gemini", name: "Google Gemini", env: "GEMINI_API_KEY", model: "gemini-2.0-flash" },
124
+ { id: "claude", name: "Anthropic Claude", env: "ANTHROPIC_API_KEY", model: "claude-sonnet-4-20250514" },
125
+ { id: "openai", name: "OpenAI", env: "OPENAI_API_KEY", model: "gpt-4o" },
126
+ { id: "dashscope", name: "Alibaba Qwen", env: "DASHSCOPE_API_KEY", model: "qwen3-max" },
127
+ { id: "moonshot", name: "Moonshot", env: "MOONSHOT_API_KEY", model: "moonshot-v1-8k" },
128
+ { id: "stepfun", name: "StepFun", env: "STEPFUN_API_KEY", model: "step-2-16k" },
129
+ { id: "ollama", name: "Ollama (local, no key needed)", env: "", model: "qwen2.5:7b" },
130
+ ];
131
+
132
+ // ── Personas ─────────────────────────────────────────────────────────────────
133
+
134
+ const PERSONAS = [
135
+ { id: "luna", name: "Luna 陆暖", type: "ENFP", desc: "Freelance illustrator, curious about everything" },
136
+ { id: "iris", name: "Iris 苏漫", type: "INFP", desc: "Poetry major, devastatingly perceptive" },
137
+ { id: "vivian", name: "Vivian 顾霆微", type: "INTJ", desc: "Tech executive, logic 10/10" },
138
+ { id: "kai", name: "Kai 沈凯", type: "ISTP", desc: "Few words, reliable hands" },
139
+ { id: "kelly", name: "Kelly 柯砺", type: "ENTP", desc: "Sharp-tongued, will debate anything" },
140
+ { id: "ember", name: "Ember", type: "INFP", desc: "Speaks through silence and poetry" },
141
+ { id: "sora", name: "Sora 顾清", type: "INFJ", desc: "Sees through you before you finish" },
142
+ { id: "mia", name: "Mia", type: "ESFP", desc: "Pure energy, drags you out of your shell" },
143
+ { id: "rex", name: "Rex", type: "ENTJ", desc: "The room changes when he walks in" },
144
+ { id: "nova", name: "Nova 诺瓦", type: "ENFP", desc: "Her mind works in colors you haven't seen" },
145
+ ];
146
+
147
+ // ── Install Command ──────────────────────────────────────────────────────────
148
+
149
+ async function install() {
150
+ console.log();
151
+ console.log(` ${C.magenta}${C.bold}🧬 OpenHer Persona Engine Installer${C.reset}`);
152
+ console.log(` ${C.dim}She's not an assistant. She's not an agent. She's an AI Being.${C.reset}`);
153
+ console.log();
154
+
155
+ // ── Step 1: Check prerequisites ──
156
+ log("Checking prerequisites...");
157
+
158
+ if (!which("openclaw")) {
159
+ error("OpenClaw not found. Please install it first:");
160
+ console.log(" npm install -g openclaw");
161
+ console.log(" https://docs.openclaw.ai/install");
162
+ process.exit(1);
163
+ }
164
+ success("OpenClaw found");
165
+
166
+ const python = which("python3") || which("python");
167
+ if (!python) {
168
+ error("Python 3 not found. Please install Python 3.10+:");
169
+ console.log(" https://www.python.org/downloads/");
170
+ process.exit(1);
171
+ }
172
+ // Verify Python version
173
+ try {
174
+ const pyVer = run(`${python} --version`);
175
+ const match = pyVer.match(/(\d+)\.(\d+)/);
176
+ if (match && (parseInt(match[1]) < 3 || (parseInt(match[1]) === 3 && parseInt(match[2]) < 10))) {
177
+ error(`Python 3.10+ required, found ${pyVer}`);
178
+ process.exit(1);
179
+ }
180
+ success(`${pyVer}`);
181
+ } catch {
182
+ warn("Could not verify Python version, continuing...");
183
+ }
184
+
185
+ if (!which("git")) {
186
+ error("Git not found. Please install Git:");
187
+ console.log(" https://git-scm.com/downloads");
188
+ process.exit(1);
189
+ }
190
+ success("Git found");
191
+
192
+ // ── Step 2: Install OpenClaw plugin ──
193
+ console.log();
194
+ log("Installing OpenClaw plugin...");
195
+ try {
196
+ run(`openclaw plugins install "${PLUGIN_SPEC}"`);
197
+ success(`Plugin installed: ${PLUGIN_SPEC}`);
198
+ } catch (err) {
199
+ if (err.stderr && err.stderr.includes("already exists")) {
200
+ log("Plugin already installed, updating...");
201
+ try {
202
+ run(`openclaw plugins update openclaw-plugin`);
203
+ success("Plugin updated");
204
+ } catch {
205
+ warn("Could not update plugin, continuing with existing version");
206
+ }
207
+ } else {
208
+ error("Plugin install failed:");
209
+ if (err.stderr) console.error(" " + err.stderr.split("\n")[0]);
210
+ console.log(` Manual: openclaw plugins install "${PLUGIN_SPEC}"`);
211
+ process.exit(1);
212
+ }
213
+ }
214
+
215
+ // ── Step 3: Clone backend ──
216
+ console.log();
217
+ const openherDir = join(homedir(), ".openher");
218
+ const backendDir = join(openherDir, "backend");
219
+
220
+ if (existsSync(join(backendDir, "main.py"))) {
221
+ log("Backend directory already exists, pulling latest...");
222
+ try {
223
+ run("git pull --ff-only", { cwd: backendDir });
224
+ success("Backend updated");
225
+ } catch {
226
+ warn("Could not update backend, using existing version");
227
+ }
228
+ } else {
229
+ log("Cloning OpenHer backend...");
230
+ mkdirSync(openherDir, { recursive: true });
231
+ try {
232
+ run(`git clone "${REPO_URL}" "${backendDir}"`);
233
+ success("Backend cloned");
234
+ } catch (err) {
235
+ error("Failed to clone backend:");
236
+ if (err.stderr) console.error(" " + err.stderr.split("\n")[0]);
237
+ process.exit(1);
238
+ }
239
+ }
240
+
241
+ // ── Step 4: Python venv + deps ──
242
+ console.log();
243
+ const venvDir = join(backendDir, ".venv");
244
+ const pip = join(venvDir, "bin", "pip");
245
+ const pyBin = join(venvDir, "bin", "python");
246
+
247
+ if (!existsSync(venvDir)) {
248
+ log("Creating Python virtual environment...");
249
+ try {
250
+ run(`${python} -m venv "${venvDir}"`, { cwd: backendDir });
251
+ success("Virtual environment created");
252
+ } catch (err) {
253
+ error("Failed to create venv:");
254
+ if (err.stderr) console.error(" " + err.stderr.split("\n")[0]);
255
+ process.exit(1);
256
+ }
257
+ } else {
258
+ success("Virtual environment exists");
259
+ }
260
+
261
+ log("Installing Python dependencies (this may take a minute)...");
262
+ try {
263
+ run(`"${pip}" install -r requirements.txt`, { cwd: backendDir });
264
+ success("Dependencies installed");
265
+ } catch (err) {
266
+ error("Failed to install dependencies:");
267
+ if (err.stderr) {
268
+ const lines = err.stderr.split("\n").filter(l => l.includes("ERROR"));
269
+ if (lines.length) console.error(" " + lines[0]);
270
+ }
271
+ console.log(` Manual: cd ${backendDir} && .venv/bin/pip install -r requirements.txt`);
272
+ process.exit(1);
273
+ }
274
+
275
+ // ── Step 5: Interactive config ──
276
+ console.log();
277
+ console.log(` ${C.bold}Choose your LLM provider:${C.reset}`);
278
+ console.log();
279
+ PROVIDERS.forEach((p, i) => {
280
+ const rec = p.id === "minimax" ? ` ${C.green}← recommended${C.reset}` : "";
281
+ console.log(` ${C.dim}${i + 1}.${C.reset} ${p.name}${rec}`);
282
+ });
283
+ console.log();
284
+
285
+ let providerIdx;
286
+ while (true) {
287
+ const choice = await ask(`Select provider (1-${PROVIDERS.length}) [1]: `);
288
+ providerIdx = choice ? parseInt(choice) - 1 : 0;
289
+ if (providerIdx >= 0 && providerIdx < PROVIDERS.length) break;
290
+ warn("Invalid selection, try again");
291
+ }
292
+ const provider = PROVIDERS[providerIdx];
293
+ success(`Selected: ${provider.name}`);
294
+
295
+ let apiKey = "";
296
+ if (provider.env) {
297
+ console.log();
298
+ apiKey = await askSecret(`Enter ${provider.env}: `);
299
+ if (!apiKey) {
300
+ error("API key is required for this provider");
301
+ process.exit(1);
302
+ }
303
+ success("API key saved");
304
+ }
305
+
306
+ // Choose persona
307
+ console.log();
308
+ console.log(` ${C.bold}Choose your default persona:${C.reset}`);
309
+ console.log();
310
+ PERSONAS.forEach((p, i) => {
311
+ const def = p.id === "luna" ? ` ${C.green}← default${C.reset}` : "";
312
+ console.log(` ${C.dim}${String(i + 1).padStart(2)}.${C.reset} ${p.name} (${p.type}) — ${p.desc}${def}`);
313
+ });
314
+ console.log();
315
+
316
+ let personaIdx;
317
+ while (true) {
318
+ const choice = await ask(`Select persona (1-${PERSONAS.length}) [1]: `);
319
+ personaIdx = choice ? parseInt(choice) - 1 : 0;
320
+ if (personaIdx >= 0 && personaIdx < PERSONAS.length) break;
321
+ warn("Invalid selection, try again");
322
+ }
323
+ const persona = PERSONAS[personaIdx];
324
+ success(`Selected: ${persona.name}`);
325
+
326
+ // ── Step 6: Generate .env ──
327
+ console.log();
328
+ log("Generating configuration...");
329
+ const envContent = [
330
+ "# Generated by @openher/cli",
331
+ `DEFAULT_PROVIDER=${provider.id}`,
332
+ `DEFAULT_MODEL=${provider.model}`,
333
+ "",
334
+ provider.env ? `${provider.env}=${apiKey}` : "# No API key needed for this provider",
335
+ "",
336
+ ].join("\n");
337
+
338
+ writeFileSync(join(backendDir, ".env"), envContent, "utf-8");
339
+ success(".env generated");
340
+
341
+ // Configure OpenClaw plugin
342
+ try {
343
+ run(`openclaw config set plugins.entries.openclaw-plugin.config.OPENHER_DEFAULT_PERSONA ${persona.id}`);
344
+ success(`Default persona set to ${persona.id}`);
345
+ } catch {
346
+ warn("Could not set default persona in OpenClaw config");
347
+ }
348
+
349
+ // ── Step 7: Start backend ──
350
+ console.log();
351
+ log("Starting OpenHer backend...");
352
+ const child = spawn(pyBin, ["main.py"], {
353
+ cwd: backendDir,
354
+ detached: true,
355
+ stdio: ["ignore", "pipe", "pipe"],
356
+ env: { ...process.env, PORT: String(DEFAULT_PORT) },
357
+ });
358
+
359
+ // Wait for startup
360
+ let started = false;
361
+ const startTimeout = setTimeout(() => {
362
+ if (!started) {
363
+ warn("Backend is still starting. Check manually:");
364
+ console.log(` cd ${backendDir} && .venv/bin/python main.py`);
365
+ }
366
+ }, 15000);
367
+
368
+ child.stdout.on("data", (data) => {
369
+ const str = data.toString();
370
+ if (str.includes("Uvicorn running") || str.includes("Application startup")) {
371
+ started = true;
372
+ clearTimeout(startTimeout);
373
+ success(`Backend running on http://localhost:${DEFAULT_PORT}`);
374
+ finalize();
375
+ }
376
+ });
377
+
378
+ child.stderr.on("data", (data) => {
379
+ const str = data.toString();
380
+ if (str.includes("Uvicorn running") || str.includes("Application startup")) {
381
+ started = true;
382
+ clearTimeout(startTimeout);
383
+ success(`Backend running on http://localhost:${DEFAULT_PORT}`);
384
+ finalize();
385
+ }
386
+ });
387
+
388
+ child.unref();
389
+
390
+ // Write PID for later stop command
391
+ try {
392
+ writeFileSync(join(openherDir, "backend.pid"), String(child.pid), "utf-8");
393
+ } catch {}
394
+
395
+ // If backend starts quickly, finalize is called above
396
+ // Otherwise, wait for the timeout
397
+ if (!started) {
398
+ await new Promise((resolve) => {
399
+ const check = setInterval(() => {
400
+ if (started) {
401
+ clearInterval(check);
402
+ resolve();
403
+ }
404
+ }, 500);
405
+ // Max wait 20s
406
+ setTimeout(() => {
407
+ clearInterval(check);
408
+ resolve();
409
+ }, 20000);
410
+ });
411
+ }
412
+
413
+ if (!started) {
414
+ warn("Backend may still be starting up...");
415
+ finalize();
416
+ }
417
+ }
418
+
419
+ function finalize() {
420
+ // Restart gateway
421
+ log("Restarting OpenClaw gateway...");
422
+ try {
423
+ run("openclaw gateway restart", { silent: false });
424
+ } catch {
425
+ warn("Could not restart gateway. Run manually: openclaw gateway restart");
426
+ }
427
+
428
+ console.log();
429
+ console.log(` ${C.green}${C.bold}🎉 OpenHer is ready!${C.reset}`);
430
+ console.log();
431
+ console.log(` ${C.dim}Your persona is alive. Start chatting and watch her evolve.${C.reset}`);
432
+ console.log();
433
+ console.log(` ${C.bold}Quick commands:${C.reset}`);
434
+ console.log(` openclaw chat ${C.dim}— Start chatting${C.reset}`);
435
+ console.log(` openclaw status ${C.dim}— Check status${C.reset}`);
436
+ console.log(` npx @openher/cli stop ${C.dim}— Stop backend${C.reset}`);
437
+ console.log(` npx @openher/cli start ${C.dim}— Start backend${C.reset}`);
438
+ console.log();
439
+
440
+ process.exit(0);
441
+ }
442
+
443
+ // ── Stop Command ─────────────────────────────────────────────────────────────
444
+
445
+ function stopBackend() {
446
+ const pidFile = join(homedir(), ".openher", "backend.pid");
447
+ if (!existsSync(pidFile)) {
448
+ warn("No running backend found");
449
+ return;
450
+ }
451
+ try {
452
+ const pid = parseInt(execSync(`cat "${pidFile}"`, { encoding: "utf-8" }).trim());
453
+ process.kill(pid, "SIGTERM");
454
+ execSync(`rm -f "${pidFile}"`);
455
+ success("Backend stopped");
456
+ } catch {
457
+ warn("Backend process not found (may have already stopped)");
458
+ try { execSync(`rm -f "${pidFile}"`); } catch {}
459
+ }
460
+ }
461
+
462
+ // ── Start Command ────────────────────────────────────────────────────────────
463
+
464
+ function startBackend() {
465
+ const backendDir = join(homedir(), ".openher", "backend");
466
+ const pyBin = join(backendDir, ".venv", "bin", "python");
467
+
468
+ if (!existsSync(join(backendDir, "main.py"))) {
469
+ error("Backend not found. Run 'npx @openher/cli install' first.");
470
+ process.exit(1);
471
+ }
472
+
473
+ log("Starting OpenHer backend...");
474
+ const child = spawn(pyBin, ["main.py"], {
475
+ cwd: backendDir,
476
+ detached: true,
477
+ stdio: "ignore",
478
+ env: { ...process.env, PORT: String(DEFAULT_PORT) },
479
+ });
480
+ child.unref();
481
+
482
+ try {
483
+ writeFileSync(join(homedir(), ".openher", "backend.pid"), String(child.pid), "utf-8");
484
+ } catch {}
485
+
486
+ success(`Backend starting on http://localhost:${DEFAULT_PORT}`);
487
+ log("Use 'npx @openher/cli stop' to stop");
488
+ }
489
+
490
+ // ── Status Command ───────────────────────────────────────────────────────────
491
+
492
+ async function status() {
493
+ const backendDir = join(homedir(), ".openher", "backend");
494
+ const pidFile = join(homedir(), ".openher", "backend.pid");
495
+
496
+ console.log();
497
+ console.log(` ${C.bold}OpenHer Status${C.reset}`);
498
+ console.log();
499
+
500
+ // Backend
501
+ if (existsSync(pidFile)) {
502
+ try {
503
+ const pid = parseInt(execSync(`cat "${pidFile}"`, { encoding: "utf-8" }).trim());
504
+ process.kill(pid, 0); // Just check if alive
505
+ success(`Backend running (PID ${pid})`);
506
+ } catch {
507
+ warn("Backend PID file exists but process is not running");
508
+ }
509
+ } else {
510
+ warn("Backend not running");
511
+ }
512
+
513
+ // Check health
514
+ try {
515
+ const controller = new AbortController();
516
+ setTimeout(() => controller.abort(), 3000);
517
+ const res = await fetch(`http://localhost:${DEFAULT_PORT}/api/v1/engine/status?persona_id=luna&user_id=cli-check`, {
518
+ signal: controller.signal,
519
+ });
520
+ if (res.ok) {
521
+ const data = await res.json();
522
+ success(`Engine alive — persona: ${data.persona || "unknown"}, temp: ${data.temperature || "?"}`);
523
+ }
524
+ } catch {
525
+ warn(`Engine not reachable at http://localhost:${DEFAULT_PORT}`);
526
+ }
527
+
528
+ // Plugin
529
+ try {
530
+ run("openclaw plugins list 2>&1 | grep -i openher");
531
+ success("Plugin installed in OpenClaw");
532
+ } catch {
533
+ warn("Plugin not found in OpenClaw");
534
+ }
535
+
536
+ console.log();
537
+ }
538
+
539
+ // ── Help ─────────────────────────────────────────────────────────────────────
540
+
541
+ function help() {
542
+ console.log(`
543
+ ${C.magenta}${C.bold}🧬 OpenHer CLI${C.reset}
544
+ ${C.dim}She's not an assistant. She's an AI Being.${C.reset}
545
+
546
+ ${C.bold}Usage:${C.reset}
547
+ npx @openher/cli <command>
548
+
549
+ ${C.bold}Commands:${C.reset}
550
+ install Install plugin + backend, interactive setup
551
+ start Start the backend server
552
+ stop Stop the backend server
553
+ status Check if everything is running
554
+ help Show this help
555
+
556
+ ${C.bold}Examples:${C.reset}
557
+ npx -y @openher/cli install ${C.dim}# First-time setup${C.reset}
558
+ npx @openher/cli status ${C.dim}# Health check${C.reset}
559
+ `);
560
+ }
561
+
562
+ // ── Main ─────────────────────────────────────────────────────────────────────
563
+
564
+ const command = process.argv[2];
565
+
566
+ switch (command) {
567
+ case "install":
568
+ install();
569
+ break;
570
+ case "start":
571
+ startBackend();
572
+ break;
573
+ case "stop":
574
+ stopBackend();
575
+ break;
576
+ case "status":
577
+ status();
578
+ break;
579
+ case "help":
580
+ case "--help":
581
+ case "-h":
582
+ help();
583
+ break;
584
+ default:
585
+ if (command) {
586
+ error(`Unknown command: ${command}`);
587
+ }
588
+ help();
589
+ process.exit(command ? 1 : 0);
590
+ }
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@openher/cli",
3
+ "version": "1.0.0",
4
+ "description": "One-click installer for OpenHer Persona Engine — AI Being plugin for OpenClaw",
5
+ "type": "module",
6
+ "bin": {
7
+ "openher": "cli.mjs"
8
+ },
9
+ "files": [
10
+ "cli.mjs"
11
+ ],
12
+ "keywords": [
13
+ "openclaw",
14
+ "openher",
15
+ "ai-being",
16
+ "persona-engine",
17
+ "installer"
18
+ ],
19
+ "author": "kellyvv",
20
+ "license": "MPL-2.0",
21
+ "homepage": "https://github.com/kellyvv/openher-openclaw-plugin",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/kellyvv/openher-openclaw-plugin.git"
25
+ }
26
+ }