@bryceli/openclaw 0.1.0 → 0.1.1

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/README.md CHANGED
@@ -1,45 +1,39 @@
1
- # OpenClaw
1
+ # OpenClaw Lightweight Wrapper
2
2
 
3
- This project installs `openclaw` locally with npm and keeps all OpenClaw state inside this project.
3
+ This package is a lightweight wrapper around `openclaw`.
4
4
 
5
- ## What is isolated
5
+ ## Why this version is lighter
6
6
 
7
- - OpenClaw is installed in local `node_modules`
8
- - config is stored in `.openclaw-home/openclaw.json`
9
- - secrets are stored in `.openclaw-home/.env`
10
- - workspace is stored in `.openclaw-home/workspace`
7
+ - `npm install @bryceli/openclaw` only installs the wrapper
8
+ - the heavy upstream `openclaw` runtime is installed later, on demand
9
+ - runtime files are stored in the current project under `.openclaw-runtime`
10
+ - config files are stored in the current project under `.openclaw-home`
11
11
 
12
- ## Package name
12
+ ## Current-project isolation
13
13
 
14
- This publishable wrapper package is intended to be published as `@bryceli/openclaw`.
14
+ Everything is project-local:
15
+
16
+ - `.openclaw-runtime`
17
+ - `.openclaw-home/openclaw.json`
18
+ - `.openclaw-home/.env`
19
+ - `.openclaw-home/workspace`
15
20
 
16
21
  ## Quick start
17
22
 
18
23
  ```bash
19
- npm install
20
- npm run setup
21
- npm run gateway
24
+ npx @bryceli/openclaw
22
25
  ```
23
26
 
24
- ## Useful commands
27
+ Or explicitly:
25
28
 
26
29
  ```bash
27
- npm run openclaw -- --version
28
- npm run dashboard
29
- npm run models:list
30
+ npx @bryceli/openclaw setup
31
+ npx @bryceli/openclaw gateway
32
+ npx @bryceli/openclaw dashboard
30
33
  ```
31
34
 
32
- ## Interface types
33
-
34
- - OpenAI Compatible (最常用)
35
- - Anthropic Native (原生)
36
- - OpenAI Responses (新接口)
37
- - Google Gemini (原生)
38
- - Ollama Native (本地原生)
39
-
40
35
  ## Notes
41
36
 
42
- - This project is only for OpenClaw.
43
- - It does not configure Claude Code.
44
- - It does not configure OpenCode.
45
- - All paths are project-local through `OPENCLAW_HOME`.
37
+ - The wrapper defaults to `setup` when no subcommand is given.
38
+ - The OpenClaw runtime is installed lazily when you first run setup or gateway-related commands.
39
+ - The UI keeps short Chinese annotations, but the package itself stays lightweight.
package/index.js ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ import "./scripts/cli.mjs";
package/package.json CHANGED
@@ -1,25 +1,33 @@
1
- {
1
+ {
2
2
  "name": "@bryceli/openclaw",
3
- "version": "0.1.0",
4
- "description": "Project-local OpenClaw installer and model configurator.",
3
+ "version": "0.1.1",
4
+ "description": "Lightweight OpenClaw wrapper with project-local runtime install.",
5
5
  "type": "module",
6
+ "bin": {
7
+ "bryce-openclaw": "index.js"
8
+ },
6
9
  "publishConfig": {
7
10
  "access": "public"
8
11
  },
12
+ "files": [
13
+ "index.js",
14
+ "README.md",
15
+ "scripts"
16
+ ],
9
17
  "scripts": {
10
- "setup": "node scripts/setup-openclaw.mjs",
11
- "openclaw": "node scripts/run-openclaw.mjs",
12
- "gateway": "node scripts/run-openclaw.mjs gateway run",
13
- "dashboard": "node scripts/run-openclaw.mjs dashboard",
14
- "models:list": "node scripts/run-openclaw.mjs models list",
15
- "models:test": "node scripts/run-openclaw.mjs models test"
18
+ "setup": "node index.js setup",
19
+ "openclaw": "node index.js openclaw",
20
+ "gateway": "node index.js gateway",
21
+ "dashboard": "node index.js dashboard",
22
+ "models:list": "node index.js models:list",
23
+ "models:test": "node index.js models:test",
24
+ "runtime:install": "node index.js runtime:install"
16
25
  },
17
26
  "engines": {
18
27
  "node": ">=22"
19
28
  },
20
29
  "dependencies": {
21
30
  "@clack/prompts": "^0.10.1",
22
- "openclaw": "latest",
23
31
  "picocolors": "^1.1.1"
24
32
  }
25
- }
33
+ }
@@ -0,0 +1,65 @@
1
+ import pc from "picocolors";
2
+ import { ensureRuntimeInstalled } from "./runtime-install.mjs";
3
+ import { runOpenClaw } from "./run-openclaw.mjs";
4
+ import { runSetup } from "./setup-openclaw.mjs";
5
+
6
+ const command = process.argv[2] ?? "setup";
7
+ const rest = process.argv.slice(3);
8
+
9
+ try {
10
+ switch (command) {
11
+ case "setup":
12
+ await runSetup();
13
+ break;
14
+ case "runtime:install":
15
+ await ensureRuntimeInstalled();
16
+ console.log(pc.green("OpenClaw runtime installed for the current project."));
17
+ break;
18
+ case "gateway":
19
+ await runOpenClaw(["gateway", "run", ...rest]);
20
+ break;
21
+ case "dashboard":
22
+ await runOpenClaw(["dashboard", ...rest]);
23
+ break;
24
+ case "models:list":
25
+ await runOpenClaw(["models", "list", ...rest]);
26
+ break;
27
+ case "models:test":
28
+ await runOpenClaw(["models", "test", ...rest]);
29
+ break;
30
+ case "openclaw":
31
+ await runOpenClaw(rest);
32
+ break;
33
+ case "help":
34
+ case "--help":
35
+ case "-h":
36
+ printHelp();
37
+ break;
38
+ default:
39
+ await runOpenClaw([command, ...rest]);
40
+ break;
41
+ }
42
+ } catch (error) {
43
+ const message = error instanceof Error ? error.message : String(error);
44
+ console.error(pc.red(message));
45
+ process.exit(1);
46
+ }
47
+
48
+ function printHelp() {
49
+ console.log(
50
+ [
51
+ "Usage:",
52
+ " npx @bryceli/openclaw",
53
+ " npx @bryceli/openclaw setup",
54
+ " npx @bryceli/openclaw gateway",
55
+ " npx @bryceli/openclaw dashboard",
56
+ " npx @bryceli/openclaw models:list",
57
+ " npx @bryceli/openclaw models:test",
58
+ "",
59
+ "Notes:",
60
+ " setup installs only a small wrapper first.",
61
+ " The heavy OpenClaw runtime is installed lazily into ./.openclaw-runtime when needed.",
62
+ " All config is stored in the current project via ./.openclaw-home."
63
+ ].join("\n")
64
+ );
65
+ }
@@ -0,0 +1,25 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ const __filename = fileURLToPath(import.meta.url);
5
+ const __dirname = path.dirname(__filename);
6
+
7
+ export function getPackageRoot() {
8
+ return path.resolve(__dirname, "..");
9
+ }
10
+
11
+ export function getProjectRoot() {
12
+ return process.cwd();
13
+ }
14
+
15
+ export function getOpenClawHome(projectRoot = getProjectRoot()) {
16
+ return path.join(projectRoot, ".openclaw-home");
17
+ }
18
+
19
+ export function getRuntimeRoot(projectRoot = getProjectRoot()) {
20
+ return path.join(projectRoot, ".openclaw-runtime");
21
+ }
22
+
23
+ export function getRuntimePackageJson(projectRoot = getProjectRoot()) {
24
+ return path.join(getRuntimeRoot(projectRoot), "package.json");
25
+ }
@@ -1,29 +1,34 @@
1
- import { spawn } from "node:child_process";
2
- import path from "node:path";
3
- import { fileURLToPath } from "node:url";
1
+ import { spawn } from "node:child_process";
2
+ import { ensureRuntimeInstalled } from "./runtime-install.mjs";
3
+ import { getOpenClawHome, getProjectRoot } from "./paths.mjs";
4
4
 
5
- const __filename = fileURLToPath(import.meta.url);
6
- const __dirname = path.dirname(__filename);
7
- const projectRoot = path.resolve(__dirname, "..");
8
- const openclawHome = path.join(projectRoot, ".openclaw-home");
5
+ export async function runOpenClaw(args = process.argv.slice(2)) {
6
+ const projectRoot = getProjectRoot();
7
+ const openclawHome = getOpenClawHome(projectRoot);
8
+ const { runtimeBin } = await ensureRuntimeInstalled({ quiet: false });
9
9
 
10
- const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
11
- const args = ["exec", "--", "openclaw", ...process.argv.slice(2)];
10
+ await new Promise((resolve, reject) => {
11
+ const child = spawn(runtimeBin, args, {
12
+ cwd: projectRoot,
13
+ env: {
14
+ ...process.env,
15
+ OPENCLAW_HOME: openclawHome
16
+ },
17
+ stdio: "inherit"
18
+ });
12
19
 
13
- const child = spawn(npmCommand, args, {
14
- cwd: projectRoot,
15
- env: {
16
- ...process.env,
17
- OPENCLAW_HOME: openclawHome
18
- },
19
- stdio: "inherit"
20
- });
20
+ child.on("exit", (code, signal) => {
21
+ if (signal) {
22
+ reject(new Error(`OpenClaw exited via signal ${signal}`));
23
+ return;
24
+ }
21
25
 
22
- child.on("exit", (code, signal) => {
23
- if (signal) {
24
- process.kill(process.pid, signal);
25
- return;
26
- }
26
+ if ((code ?? 0) !== 0) {
27
+ reject(new Error(`OpenClaw exited with code ${code ?? 0}`));
28
+ return;
29
+ }
27
30
 
28
- process.exit(code ?? 0);
29
- });
31
+ resolve();
32
+ });
33
+ });
34
+ }
@@ -0,0 +1,85 @@
1
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
2
+ import { spawn } from "node:child_process";
3
+ import path from "node:path";
4
+ import pc from "picocolors";
5
+ import { getProjectRoot, getRuntimePackageJson, getRuntimeRoot } from "./paths.mjs";
6
+
7
+ const RUNTIME_PACKAGE_JSON = {
8
+ name: "openclaw-runtime-local",
9
+ private: true,
10
+ version: "0.0.0",
11
+ description: "Project-local OpenClaw runtime installed by @bryceli/openclaw",
12
+ type: "module"
13
+ };
14
+
15
+ export async function ensureRuntimeInstalled({ quiet = false } = {}) {
16
+ const projectRoot = getProjectRoot();
17
+ const runtimeRoot = getRuntimeRoot(projectRoot);
18
+ const runtimePackageJson = getRuntimePackageJson(projectRoot);
19
+ const runtimeBin = getRuntimeExecutable(projectRoot);
20
+
21
+ if (existsSync(runtimeBin)) {
22
+ return { projectRoot, runtimeRoot, runtimeBin };
23
+ }
24
+
25
+ mkdirSync(runtimeRoot, { recursive: true });
26
+ if (!existsSync(runtimePackageJson)) {
27
+ writeFileSync(runtimePackageJson, `${JSON.stringify(RUNTIME_PACKAGE_JSON, null, 2)}\n`, "utf8");
28
+ }
29
+
30
+ if (!quiet) {
31
+ console.log(pc.cyan("Installing OpenClaw runtime into the current project (.openclaw-runtime)..."));
32
+ }
33
+
34
+ await runNpmInstall(runtimeRoot);
35
+
36
+ if (!existsSync(runtimeBin)) {
37
+ throw new Error("OpenClaw runtime install finished but the executable was not found.");
38
+ }
39
+
40
+ return { projectRoot, runtimeRoot, runtimeBin };
41
+ }
42
+
43
+ export function getRuntimeExecutable(projectRoot = getProjectRoot()) {
44
+ const binName = process.platform === "win32" ? "openclaw.cmd" : "openclaw";
45
+ return path.join(getRuntimeRoot(projectRoot), "node_modules", ".bin", binName);
46
+ }
47
+
48
+ function runNpmInstall(runtimeRoot) {
49
+ const npmCommand = process.platform === "win32" ? "npm.cmd" : "npm";
50
+
51
+ return new Promise((resolve, reject) => {
52
+ const child = spawn(npmCommand, ["install", "--no-save", "openclaw@latest"], {
53
+ cwd: runtimeRoot,
54
+ env: withNodePath(process.env),
55
+ stdio: "inherit"
56
+ });
57
+
58
+ child.on("exit", (code, signal) => {
59
+ if (signal) {
60
+ reject(new Error(`npm install terminated by signal ${signal}`));
61
+ return;
62
+ }
63
+
64
+ if (code !== 0) {
65
+ reject(new Error(`npm install exited with code ${code}`));
66
+ return;
67
+ }
68
+
69
+ resolve();
70
+ });
71
+ });
72
+ }
73
+
74
+ function withNodePath(env) {
75
+ const nextEnv = { ...env };
76
+ const nodeDir = process.execPath ? path.dirname(process.execPath) : "";
77
+ if (!nodeDir) {
78
+ return nextEnv;
79
+ }
80
+
81
+ const delimiter = process.platform === "win32" ? ";" : ":";
82
+ nextEnv.Path = nextEnv.Path ? `${nodeDir}${delimiter}${nextEnv.Path}` : nodeDir;
83
+ nextEnv.PATH = nextEnv.PATH ? `${nodeDir}${delimiter}${nextEnv.PATH}` : nodeDir;
84
+ return nextEnv;
85
+ }
@@ -1,13 +1,12 @@
1
- import * as p from "@clack/prompts";
1
+ import * as p from "@clack/prompts";
2
2
  import pc from "picocolors";
3
3
  import { mkdirSync, writeFileSync } from "node:fs";
4
4
  import path from "node:path";
5
- import { fileURLToPath } from "node:url";
5
+ import { ensureRuntimeInstalled } from "./runtime-install.mjs";
6
+ import { getOpenClawHome, getProjectRoot } from "./paths.mjs";
6
7
 
7
- const __filename = fileURLToPath(import.meta.url);
8
- const __dirname = path.dirname(__filename);
9
- const projectRoot = path.resolve(__dirname, "..");
10
- const openclawHome = path.join(projectRoot, ".openclaw-home");
8
+ const projectRoot = getProjectRoot();
9
+ const openclawHome = getOpenClawHome(projectRoot);
11
10
  const workspaceDir = path.join(openclawHome, "workspace");
12
11
  const configPath = path.join(openclawHome, "openclaw.json");
13
12
  const envPath = path.join(openclawHome, ".env");
@@ -45,12 +44,27 @@ const INTERFACE_TYPES = [
45
44
  }
46
45
  ];
47
46
 
48
- await main();
47
+ export async function runSetup() {
48
+ await main();
49
+ }
49
50
 
50
51
  async function main() {
51
52
  console.clear();
52
53
  p.intro(pc.bgBlue(pc.white(" OpenClaw Local Setup (安装器) ")));
53
54
 
55
+ const installRuntimeAnswer = await p.confirm({
56
+ message: "Install the heavy OpenClaw runtime now? (现在安装真正的 OpenClaw 运行时)",
57
+ initialValue: true
58
+ });
59
+
60
+ if (p.isCancel(installRuntimeAnswer)) {
61
+ exitCancelled();
62
+ }
63
+
64
+ if (installRuntimeAnswer) {
65
+ await ensureRuntimeInstalled();
66
+ }
67
+
54
68
  const interfaceType = await p.select({
55
69
  message: "Select the API interface type (选择接口类型)",
56
70
  options: INTERFACE_TYPES.map((item) => ({
@@ -70,7 +84,7 @@ async function main() {
70
84
  throw new Error(`Unsupported interface type: ${interfaceType}`);
71
85
  }
72
86
 
73
- const suggestedBaseUrl = await getSuggestedBaseUrl(interfaceConfig);
87
+ const suggestedBaseUrl = interfaceConfig.defaultBaseUrl;
74
88
  const baseUrl = await promptRequiredText({
75
89
  message: "Enter the API base URL (接口地址)",
76
90
  placeholder: suggestedBaseUrl,
@@ -106,17 +120,9 @@ async function main() {
106
120
  });
107
121
 
108
122
  mkdirSync(workspaceDir, { recursive: true });
109
- writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
123
+ writeFileSync(configPath, `${JSON.stringify(config.file, null, 2)}\n`, "utf8");
110
124
  writeFileSync(envPath, buildEnvFile(config.envEntries), "utf8");
111
125
 
112
- const notes = [];
113
- if (interfaceType === "openai-responses" && !isOfficialOpenAiBaseUrl(baseUrl)) {
114
- notes.push("Custom Responses endpoints are written using OpenClaw's practical openai-completions compatibility mode.");
115
- }
116
- if (interfaceType === "ollama-native") {
117
- notes.push("Ollama uses native API mode with no /v1 suffix so tool calling stays reliable.");
118
- }
119
-
120
126
  let outro = `${pc.green("OpenClaw project setup complete")}\n\n`;
121
127
  outro += `${pc.cyan("Project")}: ${projectRoot}\n`;
122
128
  outro += `${pc.cyan("OPENCLAW_HOME")}: ${openclawHome}\n`;
@@ -125,21 +131,9 @@ async function main() {
125
131
  outro += `${pc.cyan("Primary model")}: ${config.primaryModelRef}\n`;
126
132
  outro += `${pc.cyan("Fallback model")}: ${config.fallbackModelRef}\n`;
127
133
 
128
- if (notes.length) {
129
- outro += `\n${pc.yellow("Notes")}:\n- ${notes.join("\n- ")}`;
130
- }
131
-
132
134
  p.outro(outro);
133
135
  }
134
136
 
135
- async function getSuggestedBaseUrl(interfaceConfig) {
136
- if (interfaceConfig.value === "ollama-native") {
137
- return "http://127.0.0.1:11434";
138
- }
139
-
140
- return interfaceConfig.defaultBaseUrl;
141
- }
142
-
143
137
  function buildOpenClawConfig({ interfaceType, baseUrl, apiKey, primaryModelId, fallbackModelId }) {
144
138
  const providerId = getProviderId(interfaceType);
145
139
  const envEntries = buildEnvEntries(interfaceType, apiKey);
@@ -158,40 +152,33 @@ function buildOpenClawConfig({ interfaceType, baseUrl, apiKey, primaryModelId, f
158
152
  }
159
153
  }));
160
154
 
161
- const config = {
162
- agents: {
163
- defaults: {
164
- workspace: workspaceDir,
165
- model: {
166
- primary: `${providerId}/${primaryModelId}`,
167
- fallbacks:
168
- fallbackModelId && fallbackModelId !== primaryModelId
169
- ? [`${providerId}/${fallbackModelId}`]
170
- : []
171
- },
172
- models: Object.fromEntries(
173
- modelRefs.map((ref, index) => [
174
- ref,
175
- { alias: index === 0 ? "Primary (主模型)" : "Fallback (备用模型)" }
176
- ])
177
- )
155
+ return {
156
+ file: {
157
+ agents: {
158
+ defaults: {
159
+ workspace: workspaceDir,
160
+ model: {
161
+ primary: `${providerId}/${primaryModelId}`,
162
+ fallbacks:
163
+ fallbackModelId && fallbackModelId !== primaryModelId
164
+ ? [`${providerId}/${fallbackModelId}`]
165
+ : []
166
+ },
167
+ models: Object.fromEntries(
168
+ modelRefs.map((ref, index) => [
169
+ ref,
170
+ { alias: index === 0 ? "Primary (主模型)" : "Fallback (备用模型)" }
171
+ ])
172
+ )
173
+ }
174
+ },
175
+ models: {
176
+ mode: "merge",
177
+ providers: {
178
+ [providerId]: buildProviderConfig({ interfaceType, baseUrl, providerModels })
179
+ }
178
180
  }
179
181
  },
180
- models: {
181
- mode: "merge",
182
- providers: {
183
- [providerId]: buildProviderConfig({
184
- providerId,
185
- interfaceType,
186
- baseUrl,
187
- providerModels
188
- })
189
- }
190
- }
191
- };
192
-
193
- return {
194
- ...config,
195
182
  envEntries,
196
183
  primaryModelRef: `${providerId}/${primaryModelId}`,
197
184
  fallbackModelRef:
@@ -201,7 +188,7 @@ function buildOpenClawConfig({ interfaceType, baseUrl, apiKey, primaryModelId, f
201
188
  };
202
189
  }
203
190
 
204
- function buildProviderConfig({ providerId, interfaceType, baseUrl, providerModels }) {
191
+ function buildProviderConfig({ interfaceType, baseUrl, providerModels }) {
205
192
  if (interfaceType === "google-gemini") {
206
193
  return {
207
194
  baseUrl,
@@ -243,12 +230,6 @@ function buildEnvEntries(interfaceType, apiKey) {
243
230
  };
244
231
  }
245
232
 
246
- if (interfaceType === "google-gemini") {
247
- return {
248
- OPENCLAW_MODEL_API_KEY: apiKey
249
- };
250
- }
251
-
252
233
  return {
253
234
  OPENCLAW_MODEL_API_KEY: apiKey
254
235
  };
@@ -257,15 +238,11 @@ function buildEnvEntries(interfaceType, apiKey) {
257
238
  function buildEnvFile(entries) {
258
239
  const lines = Object.entries(entries)
259
240
  .filter(([, value]) => typeof value === "string" && value.length > 0)
260
- .map(([key, value]) => `${key}=${escapeEnvValue(value)}`);
241
+ .map(([key, value]) => `${key}=${JSON.stringify(String(value))}`);
261
242
 
262
243
  return `${lines.join("\n")}\n`;
263
244
  }
264
245
 
265
- function escapeEnvValue(value) {
266
- return JSON.stringify(String(value));
267
- }
268
-
269
246
  function getProviderId(interfaceType) {
270
247
  switch (interfaceType) {
271
248
  case "anthropic-native":
@@ -332,7 +309,7 @@ async function tryDetectModelId(baseUrl, interfaceType) {
332
309
  return tagId.trim();
333
310
  }
334
311
  } catch {
335
- // Best-effort only.
312
+ // Best effort only.
336
313
  }
337
314
  }
338
315
 
@@ -348,10 +325,6 @@ function stripTrailingSlash(value) {
348
325
  return String(value ?? "").replace(/\/+$/, "");
349
326
  }
350
327
 
351
- function isOfficialOpenAiBaseUrl(value) {
352
- return /https:\/\/api\.openai\.com\/?v1?$/i.test(stripTrailingSlash(value));
353
- }
354
-
355
328
  async function promptRequiredText({ message, placeholder, initialValue }) {
356
329
  const answer = await p.text({
357
330
  message,
@@ -390,4 +363,4 @@ async function promptOptionalText({ message, placeholder, initialValue }) {
390
363
  function exitCancelled() {
391
364
  p.cancel("Operation cancelled");
392
365
  process.exit(0);
393
- }
366
+ }