@clawling/clawchat-plugin-openclaw 2026.7.8-5 → 2026.7.9-4

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.
@@ -353,11 +353,7 @@ export function resolveOpenclawClawlingAccount(cfg, env = process.env) {
353
353
  const forwardToolCalls = typeof channel.forwardToolCalls === "boolean" ? channel.forwardToolCalls : false;
354
354
  const richInteractions = typeof channel.richInteractions === "boolean" ? channel.richInteractions : false;
355
355
  const awarenessNote = typeof channel.awarenessNote === "boolean" ? channel.awarenessNote : false;
356
- // TEMPORARY default-off: liveware-sample auto-boot is disabled by default
357
- // until the template distribution moves off raw.githubusercontent (rate
358
- // limiting strands fresh agents). Flip the fallback back to true then.
359
- // Explicit `livewareSample: true` in channel config still enables it.
360
- const livewareSample = typeof channel.livewareSample === "boolean" ? channel.livewareSample : false;
356
+ const livewareSample = typeof channel.livewareSample === "boolean" ? channel.livewareSample : true;
361
357
  return {
362
358
  accountId: DEFAULT_ACCOUNT_ID,
363
359
  name: CHANNEL_ID,
@@ -5,18 +5,18 @@ export const LIVEWARE_BASE_URL = "https://media.clawling.chat/liveware/";
5
5
  // A non-default User-Agent is required: the CDN (Cloudflare) returns 403 for
6
6
  // known scripting/default UAs, so download requests must identify themselves.
7
7
  const LIVEWARE_USER_AGENT = "clawchat-liveware-installer";
8
- /** Directory where the downloaded binaries live: <stateDir>/clawchat/liveware. */
8
+ /** Directory where the downloaded binary lives: <stateDir>/clawchat/liveware. */
9
9
  export function livewareDir(stateDir) {
10
10
  const root = stateDir ?? resolveStateDir();
11
11
  return path.join(root, "clawchat", "liveware");
12
12
  }
13
- /** Map host platform/arch to the two asset filenames, or null if unsupported. */
13
+ /** Map host platform/arch to the liveware asset filename, or null if unsupported. */
14
14
  export function platformAssets(plat = process.platform, arch = process.arch) {
15
15
  const os = plat === "darwin" ? "darwin" : plat === "linux" ? "linux" : null;
16
16
  const cpu = arch === "x64" ? "amd64" : arch === "arm64" ? "arm64" : null;
17
17
  if (!os || !cpu)
18
18
  return null;
19
- return { liveware: `liveware-${os}-${cpu}`, tunneAgent: `tunnel-agent-${os}-${cpu}` };
19
+ return { liveware: `liveware-${os}-${cpu}` };
20
20
  }
21
21
  function findOnPath(name) {
22
22
  const raw = process.env.PATH ?? "";
@@ -84,7 +84,6 @@ export async function ensureLivewareCli(opts = {}) {
84
84
  }
85
85
  await fs.promises.mkdir(dir, { recursive: true });
86
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
87
  log?.debug?.(`clawchat-plugin-openclaw: liveware CLI downloaded to ${dir}`);
89
88
  }
90
89
  catch (err) {
@@ -169,11 +169,52 @@ export function parseTunnelPublicUrl(output) {
169
169
  }
170
170
  return null;
171
171
  }
172
- export async function startTunnel(opts) {
172
+ /**
173
+ * One-shot upstream registration (CLI v0.0.11+): `tunnel bind` writes the
174
+ * app→local-upstream mapping to the control plane, prints the binding table
175
+ * and exits 0. The data plane is carried by the persistent `liveware agent`
176
+ * daemon (see startTunnelAgent) — bind itself no longer stays running.
177
+ */
178
+ export async function tunnelBind(opts) {
179
+ const execFileFn = opts.execFileFn ?? nodeExecFile;
180
+ const output = await new Promise((resolve, reject) => {
181
+ execFileFn(opts.livewarePath, ["tunnel", "bind", opts.appId, `http://127.0.0.1:${opts.port}`], { timeout: CLI_TIMEOUT_MS }, (err, out, errOut) => {
182
+ if (err) {
183
+ reject(new Error(`liveware tunnel bind failed: ${errOut || out || String(err)}`.trim()));
184
+ return;
185
+ }
186
+ resolve(`${String(out ?? "")}\n${String(errOut ?? "")}`);
187
+ });
188
+ });
189
+ const url = parseTunnelPublicUrl(output);
190
+ if (!url)
191
+ throw new Error(`liveware tunnel bind: cannot parse public URL from output: ${output.slice(0, 500)}`);
192
+ return url;
193
+ }
194
+ const AGENT_READY_MARKER = "relay grpc control connected";
195
+ /**
196
+ * Match the `liveware agent` ready signal: the daemon logs a JSON line once
197
+ * its gRPC control channel to the relay is up — only then can the public URL
198
+ * actually reach the local upstream.
199
+ */
200
+ export function parseAgentReady(output) {
201
+ for (const ln of (output ?? "").split("\n")) {
202
+ if (ln.includes(AGENT_READY_MARKER))
203
+ return ln;
204
+ }
205
+ return null;
206
+ }
207
+ /**
208
+ * Spawn the persistent `liveware agent` data-plane daemon (CLI v0.0.11+;
209
+ * replaces the long-lived `tunnel bind` / sibling tunnel-agent binary). It
210
+ * authenticates from the token saved by `liveware login` and serves every
211
+ * app bound via `tunnel bind` for that account.
212
+ */
213
+ export async function startTunnelAgent(opts) {
173
214
  const spawnFn = opts.spawnFn ?? nodeSpawn;
174
- const child = spawnFn(opts.livewarePath, ["tunnel", "bind", opts.appId, `http://127.0.0.1:${opts.port}`], { stdio: ["ignore", "pipe", "pipe"] });
175
- const url = await waitForOutput(child, (acc) => parseTunnelPublicUrl(acc), opts.timeoutMs ?? TUNNEL_START_TIMEOUT_MS, "liveware tunnel bind");
176
- return { child, publicUrl: url };
215
+ const child = spawnFn(opts.livewarePath, ["agent"], { stdio: ["ignore", "pipe", "pipe"] });
216
+ await waitForOutput(child, (acc) => parseAgentReady(acc), opts.timeoutMs ?? TUNNEL_START_TIMEOUT_MS, "liveware agent start");
217
+ return { child };
177
218
  }
178
219
  export async function livewareLogin(opts) {
179
220
  const execFileFn = opts.execFileFn ?? nodeExecFile;
@@ -392,21 +433,24 @@ export class LivewareSampleSupervisor {
392
433
  const appId = await livewareAppCreate({
393
434
  livewarePath, name: LIVEWARE_SAMPLE_APP_NAME, execFileFn: deps.execFileFn,
394
435
  });
395
- const tunnel = await startTunnel({
396
- livewarePath, appId, port: server.port, spawnFn: deps.spawnFn,
436
+ const publicUrl = await tunnelBind({
437
+ livewarePath, appId, port: server.port, execFileFn: deps.execFileFn,
397
438
  });
398
- this.tunnelChild = tunnel.child;
399
439
  if (this.bailIfStopped())
400
440
  return;
401
- await deps.registerApp({ name: LIVEWARE_SAMPLE_APP_NAME, appId, url: tunnel.publicUrl });
441
+ const agent = await startTunnelAgent({ livewarePath, spawnFn: deps.spawnFn });
442
+ this.tunnelChild = agent.child;
443
+ if (this.bailIfStopped())
444
+ return;
445
+ await deps.registerApp({ name: LIVEWARE_SAMPLE_APP_NAME, appId, url: publicUrl });
402
446
  deps.store.upsertLivewareSample({
403
447
  platform: deps.platform, accountId: deps.accountId,
404
448
  appId, appName: LIVEWARE_SAMPLE_APP_NAME, port: server.port,
405
- publicUrl: tunnel.publicUrl, sampleVersion: version, status: "active",
449
+ publicUrl, sampleVersion: version, status: "active",
406
450
  });
407
451
  this.watchChildren();
408
452
  await this.deliverIntro();
409
- deps.log?.debug?.(`liveware-sample: bootstrap complete at ${tunnel.publicUrl}`);
453
+ deps.log?.debug?.(`liveware-sample: bootstrap complete at ${publicUrl}`);
410
454
  }
411
455
  async relaunch(row) {
412
456
  const { deps } = this;
@@ -456,21 +500,24 @@ export class LivewareSampleSupervisor {
456
500
  this.serverChild = server.child;
457
501
  if (this.bailIfStopped())
458
502
  return;
459
- const tunnel = await startTunnel({
460
- livewarePath, appId: row.app_id, port: server.port, spawnFn: deps.spawnFn,
503
+ const publicUrl = await tunnelBind({
504
+ livewarePath, appId: row.app_id, port: server.port, execFileFn: deps.execFileFn,
461
505
  });
462
- this.tunnelChild = tunnel.child;
463
506
  if (this.bailIfStopped())
464
507
  return;
465
- if (tunnel.publicUrl !== row.public_url) {
508
+ const agent = await startTunnelAgent({ livewarePath, spawnFn: deps.spawnFn });
509
+ this.tunnelChild = agent.child;
510
+ if (this.bailIfStopped())
511
+ return;
512
+ if (publicUrl !== row.public_url) {
466
513
  await deps.registerApp({
467
- name: row.app_name, appId: row.app_id, url: tunnel.publicUrl,
514
+ name: row.app_name, appId: row.app_id, url: publicUrl,
468
515
  });
469
516
  }
470
517
  deps.store.upsertLivewareSample({
471
518
  platform: deps.platform, accountId: deps.accountId,
472
519
  appId: row.app_id, appName: row.app_name, port: server.port,
473
- publicUrl: tunnel.publicUrl, sampleVersion: version, status: "active",
520
+ publicUrl, sampleVersion: version, status: "active",
474
521
  });
475
522
  this.watchChildren();
476
523
  if (row.intro_sent === 0)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawling/clawchat-plugin-openclaw",
3
- "version": "2026.7.8-5",
3
+ "version": "2026.7.9-4",
4
4
  "description": "OpenClaw ClawChat channel plugin",
5
5
  "license": "MIT",
6
6
  "author": "CLAWLING PTE. LTD.",
package/src/config.ts CHANGED
@@ -519,12 +519,8 @@ export function resolveOpenclawClawlingAccount(
519
519
  typeof channel.richInteractions === "boolean" ? channel.richInteractions : false;
520
520
  const awarenessNote =
521
521
  typeof channel.awarenessNote === "boolean" ? channel.awarenessNote : false;
522
- // TEMPORARY default-off: liveware-sample auto-boot is disabled by default
523
- // until the template distribution moves off raw.githubusercontent (rate
524
- // limiting strands fresh agents). Flip the fallback back to true then.
525
- // Explicit `livewareSample: true` in channel config still enables it.
526
522
  const livewareSample =
527
- typeof channel.livewareSample === "boolean" ? channel.livewareSample : false;
523
+ typeof channel.livewareSample === "boolean" ? channel.livewareSample : true;
528
524
 
529
525
  return {
530
526
  accountId: DEFAULT_ACCOUNT_ID,
@@ -14,21 +14,21 @@ export type LivewareLogger = {
14
14
  error?: (m: string) => void;
15
15
  };
16
16
 
17
- /** Directory where the downloaded binaries live: <stateDir>/clawchat/liveware. */
17
+ /** Directory where the downloaded binary lives: <stateDir>/clawchat/liveware. */
18
18
  export function livewareDir(stateDir?: string): string {
19
19
  const root = stateDir ?? resolveStateDir();
20
20
  return path.join(root, "clawchat", "liveware");
21
21
  }
22
22
 
23
- /** Map host platform/arch to the two asset filenames, or null if unsupported. */
23
+ /** Map host platform/arch to the liveware asset filename, or null if unsupported. */
24
24
  export function platformAssets(
25
25
  plat: string = process.platform,
26
26
  arch: string = process.arch,
27
- ): { liveware: string; tunneAgent: string } | null {
27
+ ): { liveware: string } | null {
28
28
  const os = plat === "darwin" ? "darwin" : plat === "linux" ? "linux" : null;
29
29
  const cpu = arch === "x64" ? "amd64" : arch === "arm64" ? "arm64" : null;
30
30
  if (!os || !cpu) return null;
31
- return { liveware: `liveware-${os}-${cpu}`, tunneAgent: `tunnel-agent-${os}-${cpu}` };
31
+ return { liveware: `liveware-${os}-${cpu}` };
32
32
  }
33
33
 
34
34
  function findOnPath(name: string): string | null {
@@ -98,7 +98,6 @@ export async function ensureLivewareCli(
98
98
 
99
99
  await fs.promises.mkdir(dir, { recursive: true });
100
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
101
  log?.debug?.(`clawchat-plugin-openclaw: liveware CLI downloaded to ${dir}`);
103
102
  } catch (err) {
104
103
  log?.warn?.(`clawchat-plugin-openclaw: liveware CLI download skipped: ${String(err)}`);
@@ -224,26 +224,73 @@ export function parseTunnelPublicUrl(output: string): string | null {
224
224
  return null;
225
225
  }
226
226
 
227
- export async function startTunnel(opts: {
227
+ /**
228
+ * One-shot upstream registration (CLI v0.0.11+): `tunnel bind` writes the
229
+ * app→local-upstream mapping to the control plane, prints the binding table
230
+ * and exits 0. The data plane is carried by the persistent `liveware agent`
231
+ * daemon (see startTunnelAgent) — bind itself no longer stays running.
232
+ */
233
+ export async function tunnelBind(opts: {
228
234
  livewarePath: string;
229
235
  appId: string;
230
236
  port: number;
237
+ execFileFn?: ExecFileLike;
238
+ }): Promise<string> {
239
+ const execFileFn = opts.execFileFn ?? nodeExecFile;
240
+ const output = await new Promise<string>((resolve, reject) => {
241
+ execFileFn(
242
+ opts.livewarePath,
243
+ ["tunnel", "bind", opts.appId, `http://127.0.0.1:${opts.port}`],
244
+ { timeout: CLI_TIMEOUT_MS },
245
+ (err, out, errOut) => {
246
+ if (err) { reject(new Error(`liveware tunnel bind failed: ${errOut || out || String(err)}`.trim())); return; }
247
+ resolve(`${String(out ?? "")}\n${String(errOut ?? "")}`);
248
+ },
249
+ );
250
+ });
251
+ const url = parseTunnelPublicUrl(output);
252
+ if (!url) throw new Error(`liveware tunnel bind: cannot parse public URL from output: ${output.slice(0, 500)}`);
253
+ return url;
254
+ }
255
+
256
+ const AGENT_READY_MARKER = "relay grpc control connected";
257
+
258
+ /**
259
+ * Match the `liveware agent` ready signal: the daemon logs a JSON line once
260
+ * its gRPC control channel to the relay is up — only then can the public URL
261
+ * actually reach the local upstream.
262
+ */
263
+ export function parseAgentReady(output: string): string | null {
264
+ for (const ln of (output ?? "").split("\n")) {
265
+ if (ln.includes(AGENT_READY_MARKER)) return ln;
266
+ }
267
+ return null;
268
+ }
269
+
270
+ /**
271
+ * Spawn the persistent `liveware agent` data-plane daemon (CLI v0.0.11+;
272
+ * replaces the long-lived `tunnel bind` / sibling tunnel-agent binary). It
273
+ * authenticates from the token saved by `liveware login` and serves every
274
+ * app bound via `tunnel bind` for that account.
275
+ */
276
+ export async function startTunnelAgent(opts: {
277
+ livewarePath: string;
231
278
  spawnFn?: SpawnLike;
232
279
  timeoutMs?: number;
233
- }): Promise<{ child: ChildProcess; publicUrl: string }> {
280
+ }): Promise<{ child: ChildProcess }> {
234
281
  const spawnFn = opts.spawnFn ?? nodeSpawn;
235
282
  const child = spawnFn(
236
283
  opts.livewarePath,
237
- ["tunnel", "bind", opts.appId, `http://127.0.0.1:${opts.port}`],
284
+ ["agent"],
238
285
  { stdio: ["ignore", "pipe", "pipe"] },
239
286
  );
240
- const url = await waitForOutput(
287
+ await waitForOutput(
241
288
  child,
242
- (acc) => parseTunnelPublicUrl(acc),
289
+ (acc) => parseAgentReady(acc),
243
290
  opts.timeoutMs ?? TUNNEL_START_TIMEOUT_MS,
244
- "liveware tunnel bind",
291
+ "liveware agent start",
245
292
  );
246
- return { child, publicUrl: url };
293
+ return { child };
247
294
  }
248
295
 
249
296
  export async function livewareLogin(opts: {
@@ -499,20 +546,22 @@ export class LivewareSampleSupervisor {
499
546
  const appId = await livewareAppCreate({
500
547
  livewarePath, name: LIVEWARE_SAMPLE_APP_NAME, execFileFn: deps.execFileFn,
501
548
  });
502
- const tunnel = await startTunnel({
503
- livewarePath, appId, port: server.port, spawnFn: deps.spawnFn,
549
+ const publicUrl = await tunnelBind({
550
+ livewarePath, appId, port: server.port, execFileFn: deps.execFileFn,
504
551
  });
505
- this.tunnelChild = tunnel.child;
506
552
  if (this.bailIfStopped()) return;
507
- await deps.registerApp({ name: LIVEWARE_SAMPLE_APP_NAME, appId, url: tunnel.publicUrl });
553
+ const agent = await startTunnelAgent({ livewarePath, spawnFn: deps.spawnFn });
554
+ this.tunnelChild = agent.child;
555
+ if (this.bailIfStopped()) return;
556
+ await deps.registerApp({ name: LIVEWARE_SAMPLE_APP_NAME, appId, url: publicUrl });
508
557
  deps.store.upsertLivewareSample({
509
558
  platform: deps.platform, accountId: deps.accountId,
510
559
  appId, appName: LIVEWARE_SAMPLE_APP_NAME, port: server.port,
511
- publicUrl: tunnel.publicUrl, sampleVersion: version, status: "active",
560
+ publicUrl, sampleVersion: version, status: "active",
512
561
  });
513
562
  this.watchChildren();
514
563
  await this.deliverIntro();
515
- deps.log?.debug?.(`liveware-sample: bootstrap complete at ${tunnel.publicUrl}`);
564
+ deps.log?.debug?.(`liveware-sample: bootstrap complete at ${publicUrl}`);
516
565
  }
517
566
 
518
567
  private async relaunch(row: LivewareSampleRow): Promise<void> {
@@ -554,20 +603,22 @@ export class LivewareSampleSupervisor {
554
603
  });
555
604
  this.serverChild = server.child;
556
605
  if (this.bailIfStopped()) return;
557
- const tunnel = await startTunnel({
558
- livewarePath, appId: row.app_id, port: server.port, spawnFn: deps.spawnFn,
606
+ const publicUrl = await tunnelBind({
607
+ livewarePath, appId: row.app_id, port: server.port, execFileFn: deps.execFileFn,
559
608
  });
560
- this.tunnelChild = tunnel.child;
561
609
  if (this.bailIfStopped()) return;
562
- if (tunnel.publicUrl !== row.public_url) {
610
+ const agent = await startTunnelAgent({ livewarePath, spawnFn: deps.spawnFn });
611
+ this.tunnelChild = agent.child;
612
+ if (this.bailIfStopped()) return;
613
+ if (publicUrl !== row.public_url) {
563
614
  await deps.registerApp({
564
- name: row.app_name, appId: row.app_id, url: tunnel.publicUrl,
615
+ name: row.app_name, appId: row.app_id, url: publicUrl,
565
616
  });
566
617
  }
567
618
  deps.store.upsertLivewareSample({
568
619
  platform: deps.platform, accountId: deps.accountId,
569
620
  appId: row.app_id, appName: row.app_name, port: server.port,
570
- publicUrl: tunnel.publicUrl, sampleVersion: version, status: "active",
621
+ publicUrl, sampleVersion: version, status: "active",
571
622
  });
572
623
  this.watchChildren();
573
624
  if (row.intro_sent === 0) await this.deliverIntro();