@clawling/clawchat-plugin-openclaw 2026.6.23-1 → 2026.6.24-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/dist/src/liveware-cli.js +93 -0
- package/dist/src/runtime.js +10 -0
- package/dist/src/tools.js +10 -3
- package/package.json +1 -1
- package/src/liveware-cli.ts +106 -0
- package/src/runtime.ts +10 -0
- package/src/tools.ts +10 -3
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
|
|
4
|
+
export const LIVEWARE_BASE_URL = "https://media.clawling.chat/liveware/";
|
|
5
|
+
// A non-default User-Agent is required: the CDN (Cloudflare) returns 403 for
|
|
6
|
+
// known scripting/default UAs, so download requests must identify themselves.
|
|
7
|
+
const LIVEWARE_USER_AGENT = "clawchat-liveware-installer";
|
|
8
|
+
/** Directory where the downloaded binaries live: <stateDir>/clawchat/liveware. */
|
|
9
|
+
export function livewareDir(stateDir) {
|
|
10
|
+
const root = stateDir ?? resolveStateDir();
|
|
11
|
+
return path.join(root, "clawchat", "liveware");
|
|
12
|
+
}
|
|
13
|
+
/** Map host platform/arch to the two asset filenames, or null if unsupported. */
|
|
14
|
+
export function platformAssets(plat = process.platform, arch = process.arch) {
|
|
15
|
+
const os = plat === "darwin" ? "darwin" : plat === "linux" ? "linux" : null;
|
|
16
|
+
const cpu = arch === "x64" ? "amd64" : arch === "arm64" ? "arm64" : null;
|
|
17
|
+
if (!os || !cpu)
|
|
18
|
+
return null;
|
|
19
|
+
return { liveware: `liveware-${os}-${cpu}`, tunneAgent: `tunnel-agent-${os}-${cpu}` };
|
|
20
|
+
}
|
|
21
|
+
function findOnPath(name) {
|
|
22
|
+
const raw = process.env.PATH ?? "";
|
|
23
|
+
for (const dir of raw.split(path.delimiter)) {
|
|
24
|
+
if (!dir)
|
|
25
|
+
continue;
|
|
26
|
+
const candidate = path.join(dir, name);
|
|
27
|
+
try {
|
|
28
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
29
|
+
return candidate;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// not here; keep scanning
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the liveware executable to invoke:
|
|
39
|
+
* 1. on PATH → "liveware" (let the OS resolve it)
|
|
40
|
+
* 2. downloaded dir → absolute path
|
|
41
|
+
* 3. otherwise → null
|
|
42
|
+
*/
|
|
43
|
+
export function resolveLivewarePath(stateDir) {
|
|
44
|
+
if (findOnPath("liveware"))
|
|
45
|
+
return "liveware";
|
|
46
|
+
const local = path.join(livewareDir(stateDir), "liveware");
|
|
47
|
+
try {
|
|
48
|
+
fs.accessSync(local, fs.constants.X_OK);
|
|
49
|
+
return local;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function downloadTo(url, dest) {
|
|
56
|
+
const res = await fetch(url, { headers: { "user-agent": LIVEWARE_USER_AGENT } });
|
|
57
|
+
if (!res.ok)
|
|
58
|
+
throw new Error(`download ${url} failed: HTTP ${res.status}`);
|
|
59
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
60
|
+
if (buf.length === 0)
|
|
61
|
+
throw new Error(`download ${url} returned empty body`);
|
|
62
|
+
const tmp = `${dest}.tmp`;
|
|
63
|
+
await fs.promises.writeFile(tmp, buf);
|
|
64
|
+
await fs.promises.chmod(tmp, 0o755);
|
|
65
|
+
await fs.promises.rename(tmp, dest);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Startup hook: ensure the liveware CLI is available, downloading it when not.
|
|
69
|
+
* Fire-and-forget — never throws; logs and returns on any failure.
|
|
70
|
+
*/
|
|
71
|
+
export async function ensureLivewareCli(opts = {}) {
|
|
72
|
+
const { stateDir, log } = opts;
|
|
73
|
+
try {
|
|
74
|
+
if (findOnPath("liveware"))
|
|
75
|
+
return; // PATH wins
|
|
76
|
+
const dir = livewareDir(stateDir);
|
|
77
|
+
const target = path.join(dir, "liveware");
|
|
78
|
+
if (fs.existsSync(target))
|
|
79
|
+
return;
|
|
80
|
+
const assets = platformAssets();
|
|
81
|
+
if (!assets) {
|
|
82
|
+
log?.warn?.(`clawchat-plugin-openclaw: unsupported platform ${process.platform}/${process.arch}; skipping liveware download`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
86
|
+
await downloadTo(LIVEWARE_BASE_URL + assets.liveware, path.join(dir, "liveware"));
|
|
87
|
+
await downloadTo(LIVEWARE_BASE_URL + assets.tunneAgent, path.join(dir, "tunnel-agent"));
|
|
88
|
+
log?.debug?.(`clawchat-plugin-openclaw: liveware CLI downloaded to ${dir}`);
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
log?.warn?.(`clawchat-plugin-openclaw: liveware CLI download skipped: ${String(err)}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
package/dist/src/runtime.js
CHANGED
|
@@ -5,6 +5,7 @@ import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
|
|
5
5
|
import { createOpenclawClawlingClient, resolveOpenclawClawlingDeviceId } from "./client.js";
|
|
6
6
|
import { createOpenclawClawlingApiClient } from "./api-client.js";
|
|
7
7
|
import { reportPluginVersionSafe, resolvePluginVersion } from "./plugin-report.js";
|
|
8
|
+
import { ensureLivewareCli } from "./liveware-cli.js";
|
|
8
9
|
import { ClawlingApiError } from "./api-types.js";
|
|
9
10
|
import { RefreshManager } from "./refresh-manager.js";
|
|
10
11
|
import { runOpenclawClawlingLogin, } from "./login.runtime.js";
|
|
@@ -618,6 +619,15 @@ export async function startOpenclawClawlingGateway(params) {
|
|
|
618
619
|
authenticated: false,
|
|
619
620
|
log,
|
|
620
621
|
});
|
|
622
|
+
// Ensure the liveware CLI is installed (async, never blocks startup).
|
|
623
|
+
void ensureLivewareCli({
|
|
624
|
+
stateDir: runtime.state?.resolveStateDir?.(),
|
|
625
|
+
log: {
|
|
626
|
+
warn: (m) => log?.info?.(m),
|
|
627
|
+
debug: (m) => log?.debug?.(m),
|
|
628
|
+
error: (m) => log?.error?.(m),
|
|
629
|
+
},
|
|
630
|
+
});
|
|
621
631
|
// §B self-heal — non-interactive env connect-code activation BEFORE blocking on
|
|
622
632
|
// the wait-for-activation loop. Gated so the common path (no env code, or an
|
|
623
633
|
// already-credentialed account) adds NO async work: on success it credentials
|
package/dist/src/tools.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { execFile } from "node:child_process";
|
|
4
4
|
import { createOpenclawClawlingApiClient } from "./api-client.js";
|
|
5
|
+
import { resolveLivewarePath } from "./liveware-cli.js";
|
|
5
6
|
import { ClawlingApiError, } from "./api-types.js";
|
|
6
7
|
import { resolveOpenclawClawlingAccount } from "./config.js";
|
|
7
8
|
import { clawChatDbPathForStateDir, getClawChatStore, } from "./storage.js";
|
|
@@ -1048,14 +1049,20 @@ export function registerOpenclawClawlingTools(api, options = {}) {
|
|
|
1048
1049
|
return configError("clawchat-plugin-openclaw: token is required");
|
|
1049
1050
|
}
|
|
1050
1051
|
return await new Promise((resolve) => {
|
|
1052
|
+
const livewarePath = resolveLivewarePath(api.runtime.state?.resolveStateDir?.());
|
|
1053
|
+
if (!livewarePath) {
|
|
1054
|
+
resolve(jsonResponse({
|
|
1055
|
+
error: "config",
|
|
1056
|
+
message: "liveware CLI not found in PATH; install liveware before calling this tool",
|
|
1057
|
+
}));
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1051
1060
|
// Security: the token is passed as a child process argv value using
|
|
1052
1061
|
// liveware's documented --access-token interface. It is in the child
|
|
1053
1062
|
// argv briefly during process creation; env-var or stdin delivery
|
|
1054
1063
|
// would be preferred if liveware supported it.
|
|
1055
1064
|
// NEVER log the argv array; scrub token from any captured output below.
|
|
1056
|
-
execFile(
|
|
1057
|
-
// Scrub token from any output before returning — token must never
|
|
1058
|
-
// appear in tool results visible to the LLM.
|
|
1065
|
+
execFile(livewarePath, ["login", "--access-token", token], { timeout: 30000 }, (err, stdout, stderr) => {
|
|
1059
1066
|
const scrub = (s) => (token ? s.split(token).join("***") : s);
|
|
1060
1067
|
const safeStdout = scrub(stdout ?? "").trim();
|
|
1061
1068
|
const safeStderr = scrub(stderr ?? "").trim();
|
package/package.json
CHANGED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
|
|
4
|
+
|
|
5
|
+
export const LIVEWARE_BASE_URL = "https://media.clawling.chat/liveware/";
|
|
6
|
+
|
|
7
|
+
// A non-default User-Agent is required: the CDN (Cloudflare) returns 403 for
|
|
8
|
+
// known scripting/default UAs, so download requests must identify themselves.
|
|
9
|
+
const LIVEWARE_USER_AGENT = "clawchat-liveware-installer";
|
|
10
|
+
|
|
11
|
+
export type LivewareLogger = {
|
|
12
|
+
warn?: (m: string) => void;
|
|
13
|
+
debug?: (m: string) => void;
|
|
14
|
+
error?: (m: string) => void;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** Directory where the downloaded binaries live: <stateDir>/clawchat/liveware. */
|
|
18
|
+
export function livewareDir(stateDir?: string): string {
|
|
19
|
+
const root = stateDir ?? resolveStateDir();
|
|
20
|
+
return path.join(root, "clawchat", "liveware");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Map host platform/arch to the two asset filenames, or null if unsupported. */
|
|
24
|
+
export function platformAssets(
|
|
25
|
+
plat: string = process.platform,
|
|
26
|
+
arch: string = process.arch,
|
|
27
|
+
): { liveware: string; tunneAgent: string } | null {
|
|
28
|
+
const os = plat === "darwin" ? "darwin" : plat === "linux" ? "linux" : null;
|
|
29
|
+
const cpu = arch === "x64" ? "amd64" : arch === "arm64" ? "arm64" : null;
|
|
30
|
+
if (!os || !cpu) return null;
|
|
31
|
+
return { liveware: `liveware-${os}-${cpu}`, tunneAgent: `tunnel-agent-${os}-${cpu}` };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function findOnPath(name: string): string | null {
|
|
35
|
+
const raw = process.env.PATH ?? "";
|
|
36
|
+
for (const dir of raw.split(path.delimiter)) {
|
|
37
|
+
if (!dir) continue;
|
|
38
|
+
const candidate = path.join(dir, name);
|
|
39
|
+
try {
|
|
40
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
41
|
+
return candidate;
|
|
42
|
+
} catch {
|
|
43
|
+
// not here; keep scanning
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the liveware executable to invoke:
|
|
51
|
+
* 1. on PATH → "liveware" (let the OS resolve it)
|
|
52
|
+
* 2. downloaded dir → absolute path
|
|
53
|
+
* 3. otherwise → null
|
|
54
|
+
*/
|
|
55
|
+
export function resolveLivewarePath(stateDir?: string): string | null {
|
|
56
|
+
if (findOnPath("liveware")) return "liveware";
|
|
57
|
+
const local = path.join(livewareDir(stateDir), "liveware");
|
|
58
|
+
try {
|
|
59
|
+
fs.accessSync(local, fs.constants.X_OK);
|
|
60
|
+
return local;
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function downloadTo(url: string, dest: string): Promise<void> {
|
|
67
|
+
const res = await fetch(url, { headers: { "user-agent": LIVEWARE_USER_AGENT } });
|
|
68
|
+
if (!res.ok) throw new Error(`download ${url} failed: HTTP ${res.status}`);
|
|
69
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
70
|
+
if (buf.length === 0) throw new Error(`download ${url} returned empty body`);
|
|
71
|
+
const tmp = `${dest}.tmp`;
|
|
72
|
+
await fs.promises.writeFile(tmp, buf);
|
|
73
|
+
await fs.promises.chmod(tmp, 0o755);
|
|
74
|
+
await fs.promises.rename(tmp, dest);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Startup hook: ensure the liveware CLI is available, downloading it when not.
|
|
79
|
+
* Fire-and-forget — never throws; logs and returns on any failure.
|
|
80
|
+
*/
|
|
81
|
+
export async function ensureLivewareCli(
|
|
82
|
+
opts: { stateDir?: string; log?: LivewareLogger } = {},
|
|
83
|
+
): Promise<void> {
|
|
84
|
+
const { stateDir, log } = opts;
|
|
85
|
+
try {
|
|
86
|
+
if (findOnPath("liveware")) return; // PATH wins
|
|
87
|
+
const dir = livewareDir(stateDir);
|
|
88
|
+
const target = path.join(dir, "liveware");
|
|
89
|
+
if (fs.existsSync(target)) return;
|
|
90
|
+
|
|
91
|
+
const assets = platformAssets();
|
|
92
|
+
if (!assets) {
|
|
93
|
+
log?.warn?.(
|
|
94
|
+
`clawchat-plugin-openclaw: unsupported platform ${process.platform}/${process.arch}; skipping liveware download`,
|
|
95
|
+
);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
100
|
+
await downloadTo(LIVEWARE_BASE_URL + assets.liveware, path.join(dir, "liveware"));
|
|
101
|
+
await downloadTo(LIVEWARE_BASE_URL + assets.tunneAgent, path.join(dir, "tunnel-agent"));
|
|
102
|
+
log?.debug?.(`clawchat-plugin-openclaw: liveware CLI downloaded to ${dir}`);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
log?.warn?.(`clawchat-plugin-openclaw: liveware CLI download skipped: ${String(err)}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
package/src/runtime.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
|
|
|
16
16
|
import { createOpenclawClawlingClient, resolveOpenclawClawlingDeviceId } from "./client.ts";
|
|
17
17
|
import { createOpenclawClawlingApiClient } from "./api-client.ts";
|
|
18
18
|
import { reportPluginVersionSafe, resolvePluginVersion } from "./plugin-report.ts";
|
|
19
|
+
import { ensureLivewareCli } from "./liveware-cli.ts";
|
|
19
20
|
import { ClawlingApiError } from "./api-types.ts";
|
|
20
21
|
import { RefreshManager } from "./refresh-manager.ts";
|
|
21
22
|
import {
|
|
@@ -920,6 +921,15 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
|
|
|
920
921
|
authenticated: false,
|
|
921
922
|
log,
|
|
922
923
|
});
|
|
924
|
+
// Ensure the liveware CLI is installed (async, never blocks startup).
|
|
925
|
+
void ensureLivewareCli({
|
|
926
|
+
stateDir: runtime.state?.resolveStateDir?.(),
|
|
927
|
+
log: {
|
|
928
|
+
warn: (m) => log?.info?.(m),
|
|
929
|
+
debug: (m) => log?.debug?.(m),
|
|
930
|
+
error: (m) => log?.error?.(m),
|
|
931
|
+
},
|
|
932
|
+
});
|
|
923
933
|
// §B self-heal — non-interactive env connect-code activation BEFORE blocking on
|
|
924
934
|
// the wait-for-activation loop. Gated so the common path (no env code, or an
|
|
925
935
|
// already-credentialed account) adds NO async work: on success it credentials
|
package/src/tools.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { execFile } from "node:child_process";
|
|
|
4
4
|
import type { OpenClawAgentToolResult } from "openclaw/plugin-sdk/agent-harness-runtime";
|
|
5
5
|
import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk/core";
|
|
6
6
|
import { createOpenclawClawlingApiClient } from "./api-client.ts";
|
|
7
|
+
import { resolveLivewarePath } from "./liveware-cli.ts";
|
|
7
8
|
import {
|
|
8
9
|
ClawlingApiError,
|
|
9
10
|
type Profile,
|
|
@@ -1366,18 +1367,24 @@ export function registerOpenclawClawlingTools(
|
|
|
1366
1367
|
}
|
|
1367
1368
|
|
|
1368
1369
|
return await new Promise<ReturnType<typeof jsonResponse>>((resolve) => {
|
|
1370
|
+
const livewarePath = resolveLivewarePath(api.runtime.state?.resolveStateDir?.());
|
|
1371
|
+
if (!livewarePath) {
|
|
1372
|
+
resolve(jsonResponse({
|
|
1373
|
+
error: "config",
|
|
1374
|
+
message: "liveware CLI not found in PATH; install liveware before calling this tool",
|
|
1375
|
+
}));
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1369
1378
|
// Security: the token is passed as a child process argv value using
|
|
1370
1379
|
// liveware's documented --access-token interface. It is in the child
|
|
1371
1380
|
// argv briefly during process creation; env-var or stdin delivery
|
|
1372
1381
|
// would be preferred if liveware supported it.
|
|
1373
1382
|
// NEVER log the argv array; scrub token from any captured output below.
|
|
1374
1383
|
execFile(
|
|
1375
|
-
|
|
1384
|
+
livewarePath,
|
|
1376
1385
|
["login", "--access-token", token],
|
|
1377
1386
|
{ timeout: 30000 },
|
|
1378
1387
|
(err, stdout, stderr) => {
|
|
1379
|
-
// Scrub token from any output before returning — token must never
|
|
1380
|
-
// appear in tool results visible to the LLM.
|
|
1381
1388
|
const scrub = (s: string) => (token ? s.split(token).join("***") : s);
|
|
1382
1389
|
const safeStdout = scrub(stdout ?? "").trim();
|
|
1383
1390
|
const safeStderr = scrub(stderr ?? "").trim();
|