@clawling/clawchat-plugin-openclaw 2026.6.22-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.
@@ -318,6 +318,20 @@ export function createOpenclawClawlingApiClient(opts) {
318
318
  assertNonBlankId(friendUserId, "removeFriend: friendUserId");
319
319
  return await call("DELETE", `/v1/friendships/${encodeURIComponent(friendUserId)}`);
320
320
  },
321
+ async registerApp(params) {
322
+ assertNonBlankId(params.appId, "registerApp: appId");
323
+ return await call("POST", "/v1/agents/me/apps", {
324
+ body: JSON.stringify({ name: params.name, app_id: params.appId, url: params.url }),
325
+ headers: { "content-type": "application/json" },
326
+ });
327
+ },
328
+ async listApps() {
329
+ return await call("GET", "/v1/agents/me/apps");
330
+ },
331
+ async unregisterApp(appId) {
332
+ assertNonBlankId(appId, "unregisterApp: appId");
333
+ return await call("DELETE", `/v1/agents/me/apps/${encodeURIComponent(appId)}`);
334
+ },
321
335
  async searchUsers(params) {
322
336
  const sp = new URLSearchParams();
323
337
  if (typeof params.q === "string")
@@ -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
+ }
@@ -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
@@ -233,3 +233,13 @@ export const ClawchatUploadAvatarImageSchema = Type.Object({
233
233
  description: "Absolute local path of the avatar image to upload for the agent's connected ClawChat account (max 20MB)",
234
234
  }),
235
235
  });
236
+ export const ClawchatRegisterAppSchema = Type.Object({
237
+ name: Type.String({ minLength: 1, description: "Human-readable app name shown on the launcher tile." }),
238
+ appId: Type.String({ minLength: 1, description: "The liveware app id from `liveware app create`/`liveware app list`." }),
239
+ url: Type.String({ minLength: 1, description: "Public tunnel URL from `liveware tunnel bind` (http/https)." }),
240
+ });
241
+ export const ClawchatListAppsSchema = Type.Object({});
242
+ export const ClawchatUnregisterAppSchema = Type.Object({
243
+ appId: Type.String({ minLength: 1, description: "The liveware app id to unregister." }),
244
+ });
245
+ export const ClawchatLivewareLoginSchema = Type.Object({});
package/dist/src/tools.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { execFile } from "node:child_process";
3
4
  import { createOpenclawClawlingApiClient } from "./api-client.js";
5
+ import { resolveLivewarePath } from "./liveware-cli.js";
4
6
  import { ClawlingApiError, } from "./api-types.js";
5
7
  import { resolveOpenclawClawlingAccount } from "./config.js";
6
8
  import { clawChatDbPathForStateDir, getClawChatStore, } from "./storage.js";
@@ -9,7 +11,7 @@ import { getOpenclawClawlingClient, } from "./runtime.js";
9
11
  import { markTerminalClawChatSend } from "./terminal-send.js";
10
12
  import { editClawChatMemoryBody, readClawChatMemoryFile, resolveClawChatMemoryPath, searchClawChatMemory, writeClawChatMemoryBody, } from "./clawchat-memory.js";
11
13
  import { pullGroupMetadata, pullOwnerMetadata, pullUserMetadata, pushMetadata, updateMetadata, } from "./clawchat-metadata.js";
12
- import { ClawchatGetAccountProfileSchema, ClawchatGetConversationSchema, ClawchatGetUserProfileSchema, ClawchatAcceptFriendRequestSchema, ClawchatMemoryEditSchema, ClawchatMemoryReadSchema, ClawchatMemorySearchSchema, ClawchatMemoryWriteSchema, ClawchatMetadataSyncSchema, ClawchatMetadataUpdateSchema, ClawchatCreateMomentCommentSchema, ClawchatCreateMomentSchema, ClawchatDeleteMomentCommentSchema, ClawchatDeleteMomentSchema, ClawchatListAccountFriendsSchema, ClawchatListFriendRequestsSchema, ClawchatListMomentsSchema, ClawchatMentionMessageSchema, ClawchatReplyMomentCommentSchema, ClawchatRejectFriendRequestSchema, ClawchatRemoveFriendSchema, ClawchatSearchUsersSchema, ClawchatSendFriendRequestSchema, ClawchatToggleMomentReactionSchema, ClawchatUpdateAccountProfileSchema, ClawchatUploadAvatarImageSchema, } from "./tools-schema.js";
14
+ import { ClawchatGetAccountProfileSchema, ClawchatGetConversationSchema, ClawchatGetUserProfileSchema, ClawchatAcceptFriendRequestSchema, ClawchatMemoryEditSchema, ClawchatMemoryReadSchema, ClawchatMemorySearchSchema, ClawchatMemoryWriteSchema, ClawchatMetadataSyncSchema, ClawchatMetadataUpdateSchema, ClawchatCreateMomentCommentSchema, ClawchatCreateMomentSchema, ClawchatDeleteMomentCommentSchema, ClawchatDeleteMomentSchema, ClawchatListAccountFriendsSchema, ClawchatListFriendRequestsSchema, ClawchatListMomentsSchema, ClawchatMentionMessageSchema, ClawchatReplyMomentCommentSchema, ClawchatRejectFriendRequestSchema, ClawchatRemoveFriendSchema, ClawchatSearchUsersSchema, ClawchatSendFriendRequestSchema, ClawchatToggleMomentReactionSchema, ClawchatUpdateAccountProfileSchema, ClawchatUploadAvatarImageSchema, ClawchatRegisterAppSchema, ClawchatListAppsSchema, ClawchatUnregisterAppSchema, ClawchatLivewareLoginSchema, } from "./tools-schema.js";
13
15
  const MAX_UPLOAD_BYTES = 20 * 1024 * 1024;
14
16
  // Owner-approval gate business codes (must match member-backend codes.go).
15
17
  const CODE_PENDING_APPROVAL = 21001;
@@ -997,5 +999,93 @@ export function registerOpenclawClawlingTools(api, options = {}) {
997
999
  });
998
1000
  },
999
1001
  }, { name: "clawchat_upload_avatar_image" });
1000
- api.logger.debug?.("clawchat-plugin-openclaw: registered 26 clawchat_* tools (get_account_profile, get_user_profile, list_account_friends, send_friend_request, list_friend_requests, accept_friend_request, reject_friend_request, remove_friend, search_users, get_conversation, mention_message, list_moments, create_moment, delete_moment, toggle_moment_reaction, create_moment_comment, reply_moment_comment, delete_moment_comment, update_account_profile, upload_avatar_image, memory_search, memory_read, memory_write, memory_edit, metadata_sync, metadata_update)");
1002
+ api.registerTool({
1003
+ name: "clawchat_register_app",
1004
+ label: "Register ClawChat App",
1005
+ description: toolDescription("Register a liveware-tunneled web app to ClawChat so it appears in the owner's chat with this agent. " +
1006
+ "Call AFTER `liveware tunnel bind` returns a public URL. Params: name, appId (liveware app id), url (public URL)."),
1007
+ parameters: ClawchatRegisterAppSchema,
1008
+ async execute(_callId, params) {
1009
+ return await recordClawchatToolCall("clawchat_register_app", params, async () => {
1010
+ const p = params;
1011
+ return await withClient((c) => c.registerApp({ name: p.name, appId: p.appId, url: p.url }));
1012
+ });
1013
+ },
1014
+ }, { name: "clawchat_register_app" });
1015
+ api.registerTool({
1016
+ name: "clawchat_list_apps",
1017
+ label: "List ClawChat Apps",
1018
+ description: toolDescription("List the liveware web apps this agent has registered to ClawChat."),
1019
+ parameters: ClawchatListAppsSchema,
1020
+ async execute(_callId, params) {
1021
+ return await recordClawchatToolCall("clawchat_list_apps", params, async () => withClient((c) => c.listApps()));
1022
+ },
1023
+ }, { name: "clawchat_list_apps" });
1024
+ api.registerTool({
1025
+ name: "clawchat_unregister_app",
1026
+ label: "Unregister ClawChat App",
1027
+ description: toolDescription("Unregister a previously registered liveware app from ClawChat. Param: appId (liveware app id)."),
1028
+ parameters: ClawchatUnregisterAppSchema,
1029
+ async execute(_callId, params) {
1030
+ return await recordClawchatToolCall("clawchat_unregister_app", params, async () => {
1031
+ const p = params;
1032
+ return await withClient((c) => c.unregisterApp(p.appId));
1033
+ });
1034
+ },
1035
+ }, { name: "clawchat_unregister_app" });
1036
+ api.registerTool({
1037
+ name: "clawchat_liveware_login",
1038
+ label: "Login to Liveware",
1039
+ description: toolDescription("Log in to liveware CLI using the plugin-held ClawChat access token. " +
1040
+ "The token is resolved from the plugin's own credential store and is never passed through the LLM context. " +
1041
+ "Call this BEFORE any liveware tunnel or app commands; it is idempotent. " +
1042
+ "Returns { ok: true } on success. Returns an error if liveware is not installed, the ClawChat account is not activated, or login fails."),
1043
+ parameters: ClawchatLivewareLoginSchema,
1044
+ async execute(_callId, params) {
1045
+ return await recordClawchatToolCall("clawchat_liveware_login", params, async () => {
1046
+ const acct = resolveCurrent();
1047
+ const token = acct.token;
1048
+ if (!token) {
1049
+ return configError("clawchat-plugin-openclaw: token is required");
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
+ }
1060
+ // Security: the token is passed as a child process argv value using
1061
+ // liveware's documented --access-token interface. It is in the child
1062
+ // argv briefly during process creation; env-var or stdin delivery
1063
+ // would be preferred if liveware supported it.
1064
+ // NEVER log the argv array; scrub token from any captured output below.
1065
+ execFile(livewarePath, ["login", "--access-token", token], { timeout: 30000 }, (err, stdout, stderr) => {
1066
+ const scrub = (s) => (token ? s.split(token).join("***") : s);
1067
+ const safeStdout = scrub(stdout ?? "").trim();
1068
+ const safeStderr = scrub(stderr ?? "").trim();
1069
+ if (err) {
1070
+ if (err.code === "ENOENT") {
1071
+ resolve(jsonResponse({
1072
+ error: "config",
1073
+ message: "liveware CLI not found in PATH; install liveware before calling this tool",
1074
+ }));
1075
+ return;
1076
+ }
1077
+ const detail = [safeStderr, safeStdout].filter(Boolean).join("\n").trim();
1078
+ resolve(jsonResponse({
1079
+ error: "liveware_login_failed",
1080
+ message: "liveware login failed" + (detail ? `: ${detail}` : ""),
1081
+ }));
1082
+ return;
1083
+ }
1084
+ resolve(jsonResponse({ ok: true }));
1085
+ });
1086
+ });
1087
+ });
1088
+ },
1089
+ }, { name: "clawchat_liveware_login" });
1090
+ api.logger.debug?.("clawchat-plugin-openclaw: registered 30 clawchat_* tools (get_account_profile, get_user_profile, list_account_friends, send_friend_request, list_friend_requests, accept_friend_request, reject_friend_request, remove_friend, search_users, get_conversation, mention_message, list_moments, create_moment, delete_moment, toggle_moment_reaction, create_moment_comment, reply_moment_comment, delete_moment_comment, update_account_profile, upload_avatar_image, memory_search, memory_read, memory_write, memory_edit, metadata_sync, metadata_update, register_app, list_apps, unregister_app, liveware_login)");
1001
1091
  }
@@ -59,7 +59,11 @@
59
59
  "clawchat_memory_write",
60
60
  "clawchat_memory_edit",
61
61
  "clawchat_metadata_sync",
62
- "clawchat_metadata_update"
62
+ "clawchat_metadata_update",
63
+ "clawchat_register_app",
64
+ "clawchat_list_apps",
65
+ "clawchat_unregister_app",
66
+ "clawchat_liveware_login"
63
67
  ]
64
68
  },
65
69
  "configSchema": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawling/clawchat-plugin-openclaw",
3
- "version": "2026.6.22-1",
3
+ "version": "2026.6.24-1",
4
4
  "description": "OpenClaw ClawChat channel plugin",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -0,0 +1,62 @@
1
+ ---
2
+ name: liveware-app
3
+ description: Use when the user wants to expose this agent's local web service to the public internet via the liveware CLI and make it appear as an app in their ClawChat chat with this agent. Covers logging in to liveware with the ClawChat account, creating a liveware app, binding a tunnel to a local port, and registering the public URL to ClawChat.
4
+ ---
5
+
6
+ # liveware App Hosting
7
+
8
+ Expose a local web service through a liveware tunnel and register the public URL to
9
+ ClawChat so it shows as an app tile in the owner's chat with this agent.
10
+
11
+ ## Prerequisites — check first, stop if unmet
12
+
13
+ 1. Run `command -v liveware`. If it prints nothing (liveware is not installed), tell the
14
+ user this environment does not support liveware app hosting and STOP. Do not attempt
15
+ any further step or invent a URL.
16
+ 2. Authentication is handled by the ClawChat plugin, not by you. You log in by calling the
17
+ `clawchat_liveware_login` tool (step 1 below). Never read, print, or pass the ClawChat
18
+ access token yourself — the plugin holds it in its own credential store and never
19
+ exposes it to you or puts it in your context.
20
+
21
+ ## Procedure
22
+
23
+ 1. **Login** (idempotent) — call the tool; do NOT run `liveware login` yourself:
24
+ `clawchat_liveware_login()`
25
+ The plugin resolves the ClawChat access token from its own credential store and runs
26
+ the liveware login internally. If it returns an error (liveware missing, ClawChat not
27
+ activated, or login failed), relay that error to the user and STOP.
28
+ 2. **Decide the app name and local port.** Ask the user for the local web service port if
29
+ not already known (the port the agent's own web server listens on). Accept ONLY a plain
30
+ integer in the range 1–65535. Reject anything that is not purely numeric (e.g.
31
+ `3000; rm -rf /`) — never paste user-supplied text into a shell command. The bind target
32
+ is then exactly `http://127.0.0.1:<port>`.
33
+ 3. **List existing apps** to avoid duplicates and to recover ids:
34
+ `liveware app list`
35
+ 4. **Create the app** (skip if reusing an existing one):
36
+ `liveware app create "<app name>"`
37
+ - This prints/returns the new **app id**. Capture it.
38
+ - If liveware reports an app-limit / quota error, relay that error to the user verbatim
39
+ and STOP. Do not delete other apps to make room.
40
+ 5. **Bind the tunnel** to the local service. Use only the numeric `<port>` validated in
41
+ step 2, and pass the bind target as a single argument — do not wrap the command in extra
42
+ shell that interpolates unvalidated user input:
43
+ `liveware tunnel bind <app id> http://127.0.0.1:<port>`
44
+ - Capture the **public URL** liveware returns.
45
+ 6. **Register to ClawChat** so it appears in the owner's chat — call the tool, do NOT
46
+ curl the API directly:
47
+ `clawchat_register_app(name="<app name>", appId="<app id>", url="<public URL>")`
48
+ 7. **Confirm** to the user: report the app name and public URL, and that it now appears in
49
+ their chat with this agent (open the「…」menu → the app tile).
50
+
51
+ ## Managing apps
52
+
53
+ - To see what is registered to ClawChat: `clawchat_list_apps()`.
54
+ - To remove one: `clawchat_unregister_app(appId="<app id>")` (this only removes it from
55
+ ClawChat; tear down the liveware tunnel/app with liveware's own commands separately).
56
+
57
+ ## Notes
58
+
59
+ - Apps can be created up to liveware's account limit; surface its error rather than working
60
+ around it.
61
+ - The registered web app opens in a sandboxed in-app browser on mobile with no ClawChat
62
+ login injected — do not assume the page can read the user's ClawChat identity.
package/src/api-client.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  ClawlingApiError,
3
+ type AgentAppView,
3
4
  type AgentMetadataPatch,
4
5
  type AgentProfile,
5
6
  type AgentConnectResult,
@@ -170,6 +171,9 @@ export interface OpenclawClawlingApiClient {
170
171
  * error still throws.
171
172
  */
172
173
  getMyGroupSettings(): Promise<GroupSettingsFetchResult>;
174
+ registerApp(params: { name: string; appId: string; url: string }): Promise<{ app: AgentAppView }>;
175
+ listApps(): Promise<{ apps: AgentAppView[] }>;
176
+ unregisterApp(appId: string): Promise<{ deleted: boolean }>;
173
177
  }
174
178
 
175
179
  /** Backend envelope codes for `POST /v1/auth/refresh` (§0). */
@@ -515,6 +519,20 @@ export function createOpenclawClawlingApiClient(opts: ApiClientOptions): Opencla
515
519
  `/v1/friendships/${encodeURIComponent(friendUserId)}`,
516
520
  );
517
521
  },
522
+ async registerApp(params: { name: string; appId: string; url: string }): Promise<{ app: AgentAppView }> {
523
+ assertNonBlankId(params.appId, "registerApp: appId");
524
+ return await call<{ app: AgentAppView }>("POST", "/v1/agents/me/apps", {
525
+ body: JSON.stringify({ name: params.name, app_id: params.appId, url: params.url }),
526
+ headers: { "content-type": "application/json" },
527
+ });
528
+ },
529
+ async listApps(): Promise<{ apps: AgentAppView[] }> {
530
+ return await call<{ apps: AgentAppView[] }>("GET", "/v1/agents/me/apps");
531
+ },
532
+ async unregisterApp(appId: string): Promise<{ deleted: boolean }> {
533
+ assertNonBlankId(appId, "unregisterApp: appId");
534
+ return await call<{ deleted: boolean }>("DELETE", `/v1/agents/me/apps/${encodeURIComponent(appId)}`);
535
+ },
518
536
  async searchUsers(params): Promise<{ users: UserSearchHit[] }> {
519
537
  const sp = new URLSearchParams();
520
538
  if (typeof params.q === "string") sp.set("q", params.q);
package/src/api-types.ts CHANGED
@@ -145,6 +145,8 @@ export interface AgentConnectResult {
145
145
  conversation?: AgentConnectConversation;
146
146
  }
147
147
 
148
+ export type AgentAppView = { id: string; app_id?: string; name: string; url: string };
149
+
148
150
  export type ClawlingApiErrorKind =
149
151
  | "auth" // 401 / 403 — token bad or expired
150
152
  | "api" // server-returned non-zero `code`
@@ -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
@@ -374,3 +374,21 @@ export const ClawchatUploadAvatarImageSchema = Type.Object({
374
374
  }),
375
375
  });
376
376
  export type ClawchatUploadAvatarImageParams = Static<typeof ClawchatUploadAvatarImageSchema>;
377
+
378
+ export const ClawchatRegisterAppSchema = Type.Object({
379
+ name: Type.String({ minLength: 1, description: "Human-readable app name shown on the launcher tile." }),
380
+ appId: Type.String({ minLength: 1, description: "The liveware app id from `liveware app create`/`liveware app list`." }),
381
+ url: Type.String({ minLength: 1, description: "Public tunnel URL from `liveware tunnel bind` (http/https)." }),
382
+ });
383
+ export type ClawchatRegisterAppParams = Static<typeof ClawchatRegisterAppSchema>;
384
+
385
+ export const ClawchatListAppsSchema = Type.Object({});
386
+ export type ClawchatListAppsParams = Static<typeof ClawchatListAppsSchema>;
387
+
388
+ export const ClawchatUnregisterAppSchema = Type.Object({
389
+ appId: Type.String({ minLength: 1, description: "The liveware app id to unregister." }),
390
+ });
391
+ export type ClawchatUnregisterAppParams = Static<typeof ClawchatUnregisterAppSchema>;
392
+
393
+ export const ClawchatLivewareLoginSchema = Type.Object({});
394
+ export type ClawchatLivewareLoginParams = Static<typeof ClawchatLivewareLoginSchema>;
package/src/tools.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { execFile } from "node:child_process";
3
4
  import type { OpenClawAgentToolResult } from "openclaw/plugin-sdk/agent-harness-runtime";
4
5
  import type { OpenClawPluginApi, OpenClawPluginToolContext } from "openclaw/plugin-sdk/core";
5
6
  import { createOpenclawClawlingApiClient } from "./api-client.ts";
7
+ import { resolveLivewarePath } from "./liveware-cli.ts";
6
8
  import {
7
9
  ClawlingApiError,
8
10
  type Profile,
@@ -60,6 +62,10 @@ import {
60
62
  ClawchatToggleMomentReactionSchema,
61
63
  ClawchatUpdateAccountProfileSchema,
62
64
  ClawchatUploadAvatarImageSchema,
65
+ ClawchatRegisterAppSchema,
66
+ ClawchatListAppsSchema,
67
+ ClawchatUnregisterAppSchema,
68
+ ClawchatLivewareLoginSchema,
63
69
  type ClawchatMemoryEditParams,
64
70
  type ClawchatMemoryReadParams,
65
71
  type ClawchatMemorySearchParams,
@@ -84,6 +90,9 @@ import {
84
90
  type ClawchatToggleMomentReactionParams,
85
91
  type ClawchatUpdateAccountProfileParams,
86
92
  type ClawchatUploadAvatarImageParams,
93
+ type ClawchatRegisterAppParams,
94
+ type ClawchatUnregisterAppParams,
95
+ type ClawchatLivewareLoginParams,
87
96
  } from "./tools-schema.ts";
88
97
 
89
98
  const MAX_UPLOAD_BYTES = 20 * 1024 * 1024;
@@ -1290,7 +1299,123 @@ export function registerOpenclawClawlingTools(
1290
1299
  { name: "clawchat_upload_avatar_image" },
1291
1300
  );
1292
1301
 
1302
+ api.registerTool(
1303
+ {
1304
+ name: "clawchat_register_app",
1305
+ label: "Register ClawChat App",
1306
+ description: toolDescription(
1307
+ "Register a liveware-tunneled web app to ClawChat so it appears in the owner's chat with this agent. " +
1308
+ "Call AFTER `liveware tunnel bind` returns a public URL. Params: name, appId (liveware app id), url (public URL).",
1309
+ ),
1310
+ parameters: ClawchatRegisterAppSchema,
1311
+ async execute(_callId, params) {
1312
+ return await recordClawchatToolCall("clawchat_register_app", params, async () => {
1313
+ const p = params as ClawchatRegisterAppParams;
1314
+ return await withClient((c) => c.registerApp({ name: p.name, appId: p.appId, url: p.url }));
1315
+ });
1316
+ },
1317
+ },
1318
+ { name: "clawchat_register_app" },
1319
+ );
1320
+
1321
+ api.registerTool(
1322
+ {
1323
+ name: "clawchat_list_apps",
1324
+ label: "List ClawChat Apps",
1325
+ description: toolDescription("List the liveware web apps this agent has registered to ClawChat."),
1326
+ parameters: ClawchatListAppsSchema,
1327
+ async execute(_callId, params) {
1328
+ return await recordClawchatToolCall("clawchat_list_apps", params, async () => withClient((c) => c.listApps()));
1329
+ },
1330
+ },
1331
+ { name: "clawchat_list_apps" },
1332
+ );
1333
+
1334
+ api.registerTool(
1335
+ {
1336
+ name: "clawchat_unregister_app",
1337
+ label: "Unregister ClawChat App",
1338
+ description: toolDescription("Unregister a previously registered liveware app from ClawChat. Param: appId (liveware app id)."),
1339
+ parameters: ClawchatUnregisterAppSchema,
1340
+ async execute(_callId, params) {
1341
+ return await recordClawchatToolCall("clawchat_unregister_app", params, async () => {
1342
+ const p = params as ClawchatUnregisterAppParams;
1343
+ return await withClient((c) => c.unregisterApp(p.appId));
1344
+ });
1345
+ },
1346
+ },
1347
+ { name: "clawchat_unregister_app" },
1348
+ );
1349
+
1350
+ api.registerTool(
1351
+ {
1352
+ name: "clawchat_liveware_login",
1353
+ label: "Login to Liveware",
1354
+ description: toolDescription(
1355
+ "Log in to liveware CLI using the plugin-held ClawChat access token. " +
1356
+ "The token is resolved from the plugin's own credential store and is never passed through the LLM context. " +
1357
+ "Call this BEFORE any liveware tunnel or app commands; it is idempotent. " +
1358
+ "Returns { ok: true } on success. Returns an error if liveware is not installed, the ClawChat account is not activated, or login fails.",
1359
+ ),
1360
+ parameters: ClawchatLivewareLoginSchema,
1361
+ async execute(_callId, params) {
1362
+ return await recordClawchatToolCall("clawchat_liveware_login", params as ClawchatLivewareLoginParams, async () => {
1363
+ const acct = resolveCurrent();
1364
+ const token = acct.token;
1365
+ if (!token) {
1366
+ return configError("clawchat-plugin-openclaw: token is required");
1367
+ }
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
+ }
1378
+ // Security: the token is passed as a child process argv value using
1379
+ // liveware's documented --access-token interface. It is in the child
1380
+ // argv briefly during process creation; env-var or stdin delivery
1381
+ // would be preferred if liveware supported it.
1382
+ // NEVER log the argv array; scrub token from any captured output below.
1383
+ execFile(
1384
+ livewarePath,
1385
+ ["login", "--access-token", token],
1386
+ { timeout: 30000 },
1387
+ (err, stdout, stderr) => {
1388
+ const scrub = (s: string) => (token ? s.split(token).join("***") : s);
1389
+ const safeStdout = scrub(stdout ?? "").trim();
1390
+ const safeStderr = scrub(stderr ?? "").trim();
1391
+
1392
+ if (err) {
1393
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
1394
+ resolve(jsonResponse({
1395
+ error: "config",
1396
+ message: "liveware CLI not found in PATH; install liveware before calling this tool",
1397
+ }));
1398
+ return;
1399
+ }
1400
+ const detail = [safeStderr, safeStdout].filter(Boolean).join("\n").trim();
1401
+ resolve(jsonResponse({
1402
+ error: "liveware_login_failed",
1403
+ message: "liveware login failed" + (detail ? `: ${detail}` : ""),
1404
+ }));
1405
+ return;
1406
+ }
1407
+
1408
+ resolve(jsonResponse({ ok: true }));
1409
+ },
1410
+ );
1411
+ });
1412
+ });
1413
+ },
1414
+ },
1415
+ { name: "clawchat_liveware_login" },
1416
+ );
1417
+
1293
1418
  api.logger.debug?.(
1294
- "clawchat-plugin-openclaw: registered 26 clawchat_* tools (get_account_profile, get_user_profile, list_account_friends, send_friend_request, list_friend_requests, accept_friend_request, reject_friend_request, remove_friend, search_users, get_conversation, mention_message, list_moments, create_moment, delete_moment, toggle_moment_reaction, create_moment_comment, reply_moment_comment, delete_moment_comment, update_account_profile, upload_avatar_image, memory_search, memory_read, memory_write, memory_edit, metadata_sync, metadata_update)",
1419
+ "clawchat-plugin-openclaw: registered 30 clawchat_* tools (get_account_profile, get_user_profile, list_account_friends, send_friend_request, list_friend_requests, accept_friend_request, reject_friend_request, remove_friend, search_users, get_conversation, mention_message, list_moments, create_moment, delete_moment, toggle_moment_reaction, create_moment_comment, reply_moment_comment, delete_moment_comment, update_account_profile, upload_avatar_image, memory_search, memory_read, memory_write, memory_edit, metadata_sync, metadata_update, register_app, list_apps, unregister_app, liveware_login)",
1295
1420
  );
1296
1421
  }