Skip to content

Commit 9fbe317

Browse files
committed
feat(openclaw): add npm install surface
1 parent b20e108 commit 9fbe317

7 files changed

Lines changed: 361 additions & 6 deletions

File tree

reflexio/integrations/openclaw/README.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,29 @@ silently when the backend is unreachable.
1414

1515
## Quick install
1616

17+
Guided Reflexio setup:
18+
1719
```bash
1820
reflexio setup openclaw
1921
```
2022

21-
This walks you through:
23+
Or install the OpenClaw plugin from npm:
24+
25+
```bash
26+
npx openclaw-smart install
27+
```
28+
29+
The unscoped `openclaw-smart` npm package is a thin `npx` alias around the
30+
scoped plugin package, `@reflexioai/openclaw-smart`.
31+
32+
If your OpenClaw version supports npm package specs in its plugin installer,
33+
you can also use the native installer directly:
34+
35+
```bash
36+
openclaw plugins install @reflexioai/openclaw-smart
37+
```
38+
39+
The guided `reflexio setup openclaw` command walks you through:
2240

2341
1. Picking an LLM provider and storage backend (SQLite by default).
2442
2. Writing `OPENCLAW_BIN` + `OPENCLAW_SMART_USE_LOCAL_CLI=1` to
@@ -32,7 +50,9 @@ This walks you through:
3250
the plugin loaded.
3351

3452
`reflexio setup openclaw --uninstall [--purge]` reverses everything;
35-
`--repair` re-runs only the first-run installer.
53+
`--repair` re-runs only the first-run installer. The npm wrapper exposes
54+
the same maintenance operations as `openclaw-smart uninstall [--purge]`
55+
and `openclaw-smart repair`.
3656

3757
## How it works
3858

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/usr/bin/env node
2+
// Unscoped npm alias for `npx openclaw-smart install`.
3+
4+
import { spawnSync } from "node:child_process";
5+
import { createRequire } from "node:module";
6+
7+
const require = createRequire(import.meta.url);
8+
const cli = require.resolve("@reflexioai/openclaw-smart/scripts/npm-cli.js");
9+
const result = spawnSync(process.execPath, [cli, ...process.argv.slice(2)], {
10+
stdio: "inherit",
11+
});
12+
13+
process.exit(result.status ?? 1);
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "openclaw-smart",
3+
"version": "0.1.0",
4+
"description": "npx installer alias for @reflexioai/openclaw-smart.",
5+
"type": "module",
6+
"bin": {
7+
"openclaw-smart": "./bin/openclaw-smart.js"
8+
},
9+
"files": [
10+
"bin/**/*.js"
11+
],
12+
"dependencies": {
13+
"@reflexioai/openclaw-smart": "0.1.0"
14+
}
15+
}
Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,38 @@
11
# openclaw-smart
22

3-
openClaw plugin: cross-session memory via local reflexio backend.
3+
openClaw plugin: cross-session memory via a local Reflexio backend.
44

5-
See ../README.md for usage. See `docs/superpowers/specs/2026-05-19-openclaw-smart-design.md` in the parent repo for design.
5+
## Install
6+
7+
```bash
8+
npx openclaw-smart install
9+
```
10+
11+
The unscoped `openclaw-smart` package is a thin alias for this scoped plugin
12+
package, `@reflexioai/openclaw-smart`.
13+
14+
If your OpenClaw version supports npm package specs in `plugins install`, you
15+
can install the plugin package directly:
16+
17+
```bash
18+
openclaw plugins install @reflexioai/openclaw-smart
19+
```
20+
21+
For users who already have Reflexio installed, the guided setup remains:
22+
23+
```bash
24+
reflexio setup openclaw
25+
```
26+
27+
## Commands
28+
29+
```bash
30+
openclaw-smart install
31+
openclaw-smart repair
32+
openclaw-smart uninstall
33+
openclaw-smart uninstall --purge
34+
```
35+
36+
`install` registers `reflexio-openclaw-smart` with OpenClaw, enables typed hook
37+
access, writes `OPENCLAW_BIN` to `~/.reflexio/.env`, warms Python dependencies,
38+
and verifies the plugin is loaded.

reflexio/integrations/openclaw/plugin/package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
{
2-
"name": "openclaw-smart",
2+
"name": "@reflexioai/openclaw-smart",
33
"version": "0.1.0",
44
"description": "Self-improving openClaw plugin — learns from corrections across sessions via reflexio.",
5-
"private": true,
65
"type": "module",
6+
"bin": {
7+
"openclaw-smart": "./scripts/npm-cli.js"
8+
},
79
"scripts": {
810
"build": "tsc -p tsconfig.build.json",
11+
"prepack": "npm run build",
912
"test": "vitest run",
1013
"test:watch": "vitest",
1114
"typecheck": "tsc --noEmit"
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
#!/usr/bin/env node
2+
// npm-facing installer for openclaw-smart.
3+
//
4+
// This intentionally mirrors the core non-interactive install steps from
5+
// `reflexio setup openclaw` so npm and Python installs do not drift.
6+
7+
import { spawnSync } from "node:child_process";
8+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
9+
import { homedir } from "node:os";
10+
import { dirname, join, resolve } from "node:path";
11+
import { fileURLToPath } from "node:url";
12+
13+
const PLUGIN_ID = "reflexio-openclaw-smart";
14+
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
15+
const PLUGIN_ROOT = resolve(SCRIPT_DIR, "..");
16+
const REFLEXIO_DIR = join(homedir(), ".reflexio");
17+
const REFLEXIO_ENV = join(REFLEXIO_DIR, ".env");
18+
const STALE_EXTENSION_DIR = join(homedir(), ".openclaw", "extensions", PLUGIN_ID);
19+
20+
function usage() {
21+
console.log(`openclaw-smart
22+
23+
Usage:
24+
openclaw-smart install
25+
openclaw-smart uninstall [--purge]
26+
openclaw-smart repair
27+
28+
Install registers the bundled OpenClaw plugin, enables typed hook access,
29+
writes OPENCLAW_BIN to ~/.reflexio/.env, warms dependencies, and verifies
30+
that OpenClaw loaded the plugin.`);
31+
}
32+
33+
function fail(message, code = 1) {
34+
console.error(`openclaw-smart: ${message}`);
35+
process.exit(code);
36+
}
37+
38+
function run(argv, opts = {}) {
39+
const result = spawnSync(argv[0], argv.slice(1), {
40+
encoding: "utf8",
41+
stdio: opts.stdio ?? "pipe",
42+
...opts,
43+
});
44+
if (result.error) {
45+
return {
46+
status: 1,
47+
stdout: result.stdout ?? "",
48+
stderr: result.error.message,
49+
};
50+
}
51+
return {
52+
status: result.status ?? 1,
53+
stdout: result.stdout ?? "",
54+
stderr: result.stderr ?? "",
55+
};
56+
}
57+
58+
function findExecutable(name) {
59+
const candidates = [];
60+
if (process.env.PATH) {
61+
for (const dir of process.env.PATH.split(process.platform === "win32" ? ";" : ":")) {
62+
if (!dir) continue;
63+
candidates.push(join(dir, name));
64+
if (process.platform === "win32") candidates.push(join(dir, `${name}.cmd`));
65+
if (process.platform === "win32") candidates.push(join(dir, `${name}.exe`));
66+
}
67+
}
68+
for (const candidate of candidates) {
69+
if (existsSync(candidate)) return candidate;
70+
}
71+
return null;
72+
}
73+
74+
function resolveOpenClawBin() {
75+
const configured = process.env.OPENCLAW_BIN;
76+
if (configured && existsSync(configured)) return configured;
77+
return findExecutable("openclaw");
78+
}
79+
80+
function shellQuote(value) {
81+
return `"${String(value).replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
82+
}
83+
84+
function upsertEnv(envPath, updates) {
85+
mkdirSync(dirname(envPath), { recursive: true });
86+
const existing = existsSync(envPath) ? readFileSync(envPath, "utf8").split(/\r?\n/) : [];
87+
const remaining = existing.filter((line) => {
88+
const trimmed = line.trim();
89+
if (!trimmed || trimmed.startsWith("#")) return true;
90+
return !Object.keys(updates).some((key) => trimmed.startsWith(`${key}=`));
91+
});
92+
for (const [key, value] of Object.entries(updates)) {
93+
remaining.push(`${key}=${shellQuote(value)}`);
94+
}
95+
writeFileSync(envPath, `${remaining.filter((line) => line.length > 0).join("\n")}\n`);
96+
}
97+
98+
function removeEnvKeys(envPath, keys) {
99+
if (!existsSync(envPath)) return;
100+
const kept = readFileSync(envPath, "utf8")
101+
.split(/\r?\n/)
102+
.filter((line) => !keys.some((key) => line.trim().startsWith(`${key}=`)))
103+
.filter((line) => line.length > 0);
104+
writeFileSync(envPath, kept.length ? `${kept.join("\n")}\n` : "");
105+
}
106+
107+
function ensurePluginRoot() {
108+
if (!existsSync(join(PLUGIN_ROOT, "openclaw.plugin.json"))) {
109+
fail(`plugin root is incomplete: ${PLUGIN_ROOT}`);
110+
}
111+
}
112+
113+
function runOpenClaw(cli, args, opts = {}) {
114+
return run([cli, ...args], opts);
115+
}
116+
117+
function printCommandFailure(label, result) {
118+
const detail = (result.stderr || result.stdout || "").trim();
119+
console.error(`openclaw-smart: ${label} failed${detail ? `: ${detail}` : ""}`);
120+
}
121+
122+
function runSmartInstall() {
123+
const script = join(PLUGIN_ROOT, "scripts", "smart-install.sh");
124+
if (!existsSync(script)) {
125+
console.warn(`openclaw-smart: ${script} missing; skipping dependency warmup`);
126+
return;
127+
}
128+
const result = run(["bash", script], { stdio: "inherit" });
129+
if (result.status !== 0) {
130+
console.warn(
131+
`openclaw-smart: smart-install.sh exited ${result.status}; first session may bootstrap dependencies`,
132+
);
133+
}
134+
}
135+
136+
function inspectLoaded(cli) {
137+
const result = runOpenClaw(cli, ["plugins", "inspect", PLUGIN_ID]);
138+
return result.status === 0 && /Status:\s*loaded\b/.test(result.stdout);
139+
}
140+
141+
function install() {
142+
ensurePluginRoot();
143+
const cli = resolveOpenClawBin();
144+
if (!cli) fail("openclaw CLI not found. Install OpenClaw first or set OPENCLAW_BIN.");
145+
146+
upsertEnv(REFLEXIO_ENV, {
147+
OPENCLAW_BIN: cli,
148+
OPENCLAW_SMART_USE_LOCAL_CLI: "1",
149+
});
150+
151+
runOpenClaw(cli, ["plugins", "uninstall", "--force", PLUGIN_ID]);
152+
rmSync(STALE_EXTENSION_DIR, { recursive: true, force: true });
153+
154+
const installResult = runOpenClaw(cli, ["plugins", "install", PLUGIN_ROOT]);
155+
if (installResult.status !== 0) {
156+
printCommandFailure("plugins install", installResult);
157+
process.exit(1);
158+
}
159+
160+
const enableResult = runOpenClaw(cli, ["plugins", "enable", PLUGIN_ID]);
161+
if (enableResult.status !== 0) {
162+
printCommandFailure("plugins enable", enableResult);
163+
process.exit(1);
164+
}
165+
166+
const accessResult = runOpenClaw(cli, [
167+
"config",
168+
"set",
169+
`plugins.entries.${PLUGIN_ID}.hooks.allowConversationAccess`,
170+
"true",
171+
]);
172+
if (accessResult.status !== 0) {
173+
printCommandFailure("config set allowConversationAccess", accessResult);
174+
process.exit(1);
175+
}
176+
177+
runSmartInstall();
178+
runOpenClaw(cli, ["gateway", "restart"]);
179+
180+
if (!inspectLoaded(cli)) {
181+
fail(`plugin not loaded; check 'openclaw plugins inspect ${PLUGIN_ID}'`);
182+
}
183+
console.log("openclaw-smart installed and registered.");
184+
}
185+
186+
function uninstall({ purge = false } = {}) {
187+
const cli = resolveOpenClawBin();
188+
if (cli) {
189+
runOpenClaw(cli, ["plugins", "disable", PLUGIN_ID]);
190+
runOpenClaw(cli, ["plugins", "uninstall", "--force", PLUGIN_ID]);
191+
} else {
192+
console.warn("openclaw-smart: openclaw CLI not found; skipping plugin removal");
193+
}
194+
removeEnvKeys(REFLEXIO_ENV, ["OPENCLAW_BIN", "OPENCLAW_SMART_USE_LOCAL_CLI"]);
195+
if (purge) rmSync(join(homedir(), ".openclaw-smart"), { recursive: true, force: true });
196+
console.log("openclaw-smart uninstalled.");
197+
}
198+
199+
function repair() {
200+
runSmartInstall();
201+
console.log("openclaw-smart repair complete.");
202+
}
203+
204+
const [command, ...args] = process.argv.slice(2);
205+
if (!command || command === "-h" || command === "--help") {
206+
usage();
207+
process.exit(0);
208+
}
209+
if (command === "install") install();
210+
else if (command === "uninstall") uninstall({ purge: args.includes("--purge") });
211+
else if (command === "repair") repair();
212+
else {
213+
usage();
214+
fail(`unknown command: ${command}`);
215+
}

0 commit comments

Comments
 (0)