@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.
- package/cli.mjs +590 -0
- 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
|
+
}
|