@clawling/clawchat-plugin-openclaw 2026.7.9-1 → 2026.7.10-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.
@@ -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,
@@ -144,7 +144,10 @@ function waitForOutput(child, match, timeoutMs, label) {
144
144
  }
145
145
  export async function startSampleServer(opts) {
146
146
  const spawnFn = opts.spawnFn ?? nodeSpawn;
147
- const child = spawnFn(process.execPath, [path.join(opts.appDir, "server.mjs"), "--dir", opts.appDir, "--port", String(opts.port)], { stdio: ["ignore", "pipe", "pipe"] });
147
+ const args = [path.join(opts.appDir, "server.mjs"), "--dir", opts.appDir, "--port", String(opts.port)];
148
+ if (opts.agentUserId)
149
+ args.push("--agent-id", opts.agentUserId);
150
+ const child = spawnFn(process.execPath, args, { stdio: ["ignore", "pipe", "pipe"] });
148
151
  const line = await waitForOutput(child, (acc) => acc.split("\n").find((l) => l.trim().startsWith('{"port"')) ?? null, opts.timeoutMs ?? SERVER_START_TIMEOUT_MS, "liveware-sample server start");
149
152
  return { child, port: JSON.parse(line).port };
150
153
  }
@@ -169,11 +172,52 @@ export function parseTunnelPublicUrl(output) {
169
172
  }
170
173
  return null;
171
174
  }
172
- export async function startTunnel(opts) {
175
+ /**
176
+ * One-shot upstream registration (CLI v0.0.11+): `tunnel bind` writes the
177
+ * app→local-upstream mapping to the control plane, prints the binding table
178
+ * and exits 0. The data plane is carried by the persistent `liveware agent`
179
+ * daemon (see startTunnelAgent) — bind itself no longer stays running.
180
+ */
181
+ export async function tunnelBind(opts) {
182
+ const execFileFn = opts.execFileFn ?? nodeExecFile;
183
+ const output = await new Promise((resolve, reject) => {
184
+ execFileFn(opts.livewarePath, ["tunnel", "bind", opts.appId, `http://127.0.0.1:${opts.port}`], { timeout: CLI_TIMEOUT_MS }, (err, out, errOut) => {
185
+ if (err) {
186
+ reject(new Error(`liveware tunnel bind failed: ${errOut || out || String(err)}`.trim()));
187
+ return;
188
+ }
189
+ resolve(`${String(out ?? "")}\n${String(errOut ?? "")}`);
190
+ });
191
+ });
192
+ const url = parseTunnelPublicUrl(output);
193
+ if (!url)
194
+ throw new Error(`liveware tunnel bind: cannot parse public URL from output: ${output.slice(0, 500)}`);
195
+ return url;
196
+ }
197
+ const AGENT_READY_MARKER = "relay grpc control connected";
198
+ /**
199
+ * Match the `liveware agent` ready signal: the daemon logs a JSON line once
200
+ * its gRPC control channel to the relay is up — only then can the public URL
201
+ * actually reach the local upstream.
202
+ */
203
+ export function parseAgentReady(output) {
204
+ for (const ln of (output ?? "").split("\n")) {
205
+ if (ln.includes(AGENT_READY_MARKER))
206
+ return ln;
207
+ }
208
+ return null;
209
+ }
210
+ /**
211
+ * Spawn the persistent `liveware agent` data-plane daemon (CLI v0.0.11+;
212
+ * replaces the long-lived `tunnel bind` / sibling tunnel-agent binary). It
213
+ * authenticates from the token saved by `liveware login` and serves every
214
+ * app bound via `tunnel bind` for that account.
215
+ */
216
+ export async function startTunnelAgent(opts) {
173
217
  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 };
218
+ const child = spawnFn(opts.livewarePath, ["agent"], { stdio: ["ignore", "pipe", "pipe"] });
219
+ await waitForOutput(child, (acc) => parseAgentReady(acc), opts.timeoutMs ?? TUNNEL_START_TIMEOUT_MS, "liveware agent start");
220
+ return { child };
177
221
  }
178
222
  export async function livewareLogin(opts) {
179
223
  const execFileFn = opts.execFileFn ?? nodeExecFile;
@@ -250,8 +294,9 @@ export async function livewareAppCreate(opts) {
250
294
  throw new Error(`liveware app create: cannot parse app id from output: ${stdout.slice(0, 500)}`);
251
295
  return appId;
252
296
  }
253
- export const LIVEWARE_SAMPLE_INTRO_TEXT = "我给你安装了一个 liveware 演示应用「Liveware Sample」,它已经出现在我们的聊天里。" +
254
- "点开它看看,然后试试对我说:把标题改成 Hello Liveware。" +
297
+ export const LIVEWARE_SAMPLE_INTRO_TEXT = "我给你安装了一个 liveware 演示应用「Liveware Sample」。" +
298
+ "入口:在我们的对话页面,点右上角的「应用」按钮(✦),在打开的面板里选名为「Liveware Sample」的应用。" +
299
+ "页面上有完整的使用引导,试试对我说:把标题改成 Hello Liveware。" +
255
300
  "你在页面上点的按钮、提交的留言我也能看到,随时问我。";
256
301
  const DEFAULT_SAMPLE_PORT = 43110;
257
302
  const RESTART_WINDOW_MS = 30 * 60 * 1000;
@@ -384,6 +429,7 @@ export class LivewareSampleSupervisor {
384
429
  return;
385
430
  const server = await startSampleServer({
386
431
  appDir, port: DEFAULT_SAMPLE_PORT, spawnFn: deps.spawnFn,
432
+ agentUserId: deps.resolveAgentUserId?.() ?? null,
387
433
  });
388
434
  this.serverChild = server.child;
389
435
  if (this.bailIfStopped())
@@ -392,21 +438,24 @@ export class LivewareSampleSupervisor {
392
438
  const appId = await livewareAppCreate({
393
439
  livewarePath, name: LIVEWARE_SAMPLE_APP_NAME, execFileFn: deps.execFileFn,
394
440
  });
395
- const tunnel = await startTunnel({
396
- livewarePath, appId, port: server.port, spawnFn: deps.spawnFn,
441
+ const publicUrl = await tunnelBind({
442
+ livewarePath, appId, port: server.port, execFileFn: deps.execFileFn,
397
443
  });
398
- this.tunnelChild = tunnel.child;
399
444
  if (this.bailIfStopped())
400
445
  return;
401
- await deps.registerApp({ name: LIVEWARE_SAMPLE_APP_NAME, appId, url: tunnel.publicUrl });
446
+ const agent = await startTunnelAgent({ livewarePath, spawnFn: deps.spawnFn });
447
+ this.tunnelChild = agent.child;
448
+ if (this.bailIfStopped())
449
+ return;
450
+ await deps.registerApp({ name: LIVEWARE_SAMPLE_APP_NAME, appId, url: publicUrl });
402
451
  deps.store.upsertLivewareSample({
403
452
  platform: deps.platform, accountId: deps.accountId,
404
453
  appId, appName: LIVEWARE_SAMPLE_APP_NAME, port: server.port,
405
- publicUrl: tunnel.publicUrl, sampleVersion: version, status: "active",
454
+ publicUrl, sampleVersion: version, status: "active",
406
455
  });
407
456
  this.watchChildren();
408
457
  await this.deliverIntro();
409
- deps.log?.debug?.(`liveware-sample: bootstrap complete at ${tunnel.publicUrl}`);
458
+ deps.log?.debug?.(`liveware-sample: bootstrap complete at ${publicUrl}`);
410
459
  }
411
460
  async relaunch(row) {
412
461
  const { deps } = this;
@@ -452,25 +501,29 @@ export class LivewareSampleSupervisor {
452
501
  return;
453
502
  const server = await startSampleServer({
454
503
  appDir, port: row.port || DEFAULT_SAMPLE_PORT, spawnFn: deps.spawnFn,
504
+ agentUserId: deps.resolveAgentUserId?.() ?? null,
455
505
  });
456
506
  this.serverChild = server.child;
457
507
  if (this.bailIfStopped())
458
508
  return;
459
- const tunnel = await startTunnel({
460
- livewarePath, appId: row.app_id, port: server.port, spawnFn: deps.spawnFn,
509
+ const publicUrl = await tunnelBind({
510
+ livewarePath, appId: row.app_id, port: server.port, execFileFn: deps.execFileFn,
461
511
  });
462
- this.tunnelChild = tunnel.child;
463
512
  if (this.bailIfStopped())
464
513
  return;
465
- if (tunnel.publicUrl !== row.public_url) {
514
+ const agent = await startTunnelAgent({ livewarePath, spawnFn: deps.spawnFn });
515
+ this.tunnelChild = agent.child;
516
+ if (this.bailIfStopped())
517
+ return;
518
+ if (publicUrl !== row.public_url) {
466
519
  await deps.registerApp({
467
- name: row.app_name, appId: row.app_id, url: tunnel.publicUrl,
520
+ name: row.app_name, appId: row.app_id, url: publicUrl,
468
521
  });
469
522
  }
470
523
  deps.store.upsertLivewareSample({
471
524
  platform: deps.platform, accountId: deps.accountId,
472
525
  appId: row.app_id, appName: row.app_name, port: server.port,
473
- publicUrl: tunnel.publicUrl, sampleVersion: version, status: "active",
526
+ publicUrl, sampleVersion: version, status: "active",
474
527
  });
475
528
  this.watchChildren();
476
529
  if (row.intro_sent === 0)
@@ -801,6 +801,7 @@ export async function startOpenclawClawlingGateway(params) {
801
801
  store: store,
802
802
  sampleRoot: path.join(stateDirForSample ?? "", "clawchat", "liveware-sample"),
803
803
  resolveToken: () => account.token ?? "",
804
+ resolveAgentUserId: () => account.userId ?? null,
804
805
  resolveLivewarePath: () => resolveLivewarePath(stateDirForSample),
805
806
  cliReady: livewareCliReady,
806
807
  listApps: async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawling/clawchat-plugin-openclaw",
3
- "version": "2026.7.9-1",
3
+ "version": "2026.7.10-1",
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,
@@ -183,15 +183,14 @@ function waitForOutput(
183
183
  export async function startSampleServer(opts: {
184
184
  appDir: string;
185
185
  port: number;
186
+ agentUserId?: string | null;
186
187
  spawnFn?: SpawnLike;
187
188
  timeoutMs?: number;
188
189
  }): Promise<{ child: ChildProcess; port: number }> {
189
190
  const spawnFn = opts.spawnFn ?? nodeSpawn;
190
- const child = spawnFn(
191
- process.execPath,
192
- [path.join(opts.appDir, "server.mjs"), "--dir", opts.appDir, "--port", String(opts.port)],
193
- { stdio: ["ignore", "pipe", "pipe"] },
194
- );
191
+ const args = [path.join(opts.appDir, "server.mjs"), "--dir", opts.appDir, "--port", String(opts.port)];
192
+ if (opts.agentUserId) args.push("--agent-id", opts.agentUserId);
193
+ const child = spawnFn(process.execPath, args, { stdio: ["ignore", "pipe", "pipe"] });
195
194
  const line = await waitForOutput(
196
195
  child,
197
196
  (acc) => acc.split("\n").find((l) => l.trim().startsWith('{"port"')) ?? null,
@@ -224,26 +223,73 @@ export function parseTunnelPublicUrl(output: string): string | null {
224
223
  return null;
225
224
  }
226
225
 
227
- export async function startTunnel(opts: {
226
+ /**
227
+ * One-shot upstream registration (CLI v0.0.11+): `tunnel bind` writes the
228
+ * app→local-upstream mapping to the control plane, prints the binding table
229
+ * and exits 0. The data plane is carried by the persistent `liveware agent`
230
+ * daemon (see startTunnelAgent) — bind itself no longer stays running.
231
+ */
232
+ export async function tunnelBind(opts: {
228
233
  livewarePath: string;
229
234
  appId: string;
230
235
  port: number;
236
+ execFileFn?: ExecFileLike;
237
+ }): Promise<string> {
238
+ const execFileFn = opts.execFileFn ?? nodeExecFile;
239
+ const output = await new Promise<string>((resolve, reject) => {
240
+ execFileFn(
241
+ opts.livewarePath,
242
+ ["tunnel", "bind", opts.appId, `http://127.0.0.1:${opts.port}`],
243
+ { timeout: CLI_TIMEOUT_MS },
244
+ (err, out, errOut) => {
245
+ if (err) { reject(new Error(`liveware tunnel bind failed: ${errOut || out || String(err)}`.trim())); return; }
246
+ resolve(`${String(out ?? "")}\n${String(errOut ?? "")}`);
247
+ },
248
+ );
249
+ });
250
+ const url = parseTunnelPublicUrl(output);
251
+ if (!url) throw new Error(`liveware tunnel bind: cannot parse public URL from output: ${output.slice(0, 500)}`);
252
+ return url;
253
+ }
254
+
255
+ const AGENT_READY_MARKER = "relay grpc control connected";
256
+
257
+ /**
258
+ * Match the `liveware agent` ready signal: the daemon logs a JSON line once
259
+ * its gRPC control channel to the relay is up — only then can the public URL
260
+ * actually reach the local upstream.
261
+ */
262
+ export function parseAgentReady(output: string): string | null {
263
+ for (const ln of (output ?? "").split("\n")) {
264
+ if (ln.includes(AGENT_READY_MARKER)) return ln;
265
+ }
266
+ return null;
267
+ }
268
+
269
+ /**
270
+ * Spawn the persistent `liveware agent` data-plane daemon (CLI v0.0.11+;
271
+ * replaces the long-lived `tunnel bind` / sibling tunnel-agent binary). It
272
+ * authenticates from the token saved by `liveware login` and serves every
273
+ * app bound via `tunnel bind` for that account.
274
+ */
275
+ export async function startTunnelAgent(opts: {
276
+ livewarePath: string;
231
277
  spawnFn?: SpawnLike;
232
278
  timeoutMs?: number;
233
- }): Promise<{ child: ChildProcess; publicUrl: string }> {
279
+ }): Promise<{ child: ChildProcess }> {
234
280
  const spawnFn = opts.spawnFn ?? nodeSpawn;
235
281
  const child = spawnFn(
236
282
  opts.livewarePath,
237
- ["tunnel", "bind", opts.appId, `http://127.0.0.1:${opts.port}`],
283
+ ["agent"],
238
284
  { stdio: ["ignore", "pipe", "pipe"] },
239
285
  );
240
- const url = await waitForOutput(
286
+ await waitForOutput(
241
287
  child,
242
- (acc) => parseTunnelPublicUrl(acc),
288
+ (acc) => parseAgentReady(acc),
243
289
  opts.timeoutMs ?? TUNNEL_START_TIMEOUT_MS,
244
- "liveware tunnel bind",
290
+ "liveware agent start",
245
291
  );
246
- return { child, publicUrl: url };
292
+ return { child };
247
293
  }
248
294
 
249
295
  export async function livewareLogin(opts: {
@@ -349,6 +395,10 @@ export type LivewareSampleSupervisorDeps = {
349
395
  sampleRoot: string;
350
396
  resolveToken: () => string;
351
397
  resolveLivewarePath: () => string | null;
398
+ /** The agent's own ClawChat user id (`usr_…`) — minted into the sample
399
+ * page's back-to-chat deep link. Read at spawn time; null/absent → the
400
+ * page renders its text fallback instead of the link. */
401
+ resolveAgentUserId?: () => string | null;
352
402
  /** Settles when the liveware CLI self-download has finished (either way).
353
403
  * start() awaits it before the CLI gate so a first boot doesn't lose the
354
404
  * race against ensureLivewareCli and silently skip forever. */
@@ -365,8 +415,9 @@ export type LivewareSampleSupervisorDeps = {
365
415
  };
366
416
 
367
417
  export const LIVEWARE_SAMPLE_INTRO_TEXT =
368
- "我给你安装了一个 liveware 演示应用「Liveware Sample」,它已经出现在我们的聊天里。" +
369
- "点开它看看,然后试试对我说:把标题改成 Hello Liveware。" +
418
+ "我给你安装了一个 liveware 演示应用「Liveware Sample」。" +
419
+ "入口:在我们的对话页面,点右上角的「应用」按钮(✦),在打开的面板里选名为「Liveware Sample」的应用。" +
420
+ "页面上有完整的使用引导,试试对我说:把标题改成 Hello Liveware。" +
370
421
  "你在页面上点的按钮、提交的留言我也能看到,随时问我。";
371
422
 
372
423
  const DEFAULT_SAMPLE_PORT = 43110;
@@ -492,6 +543,7 @@ export class LivewareSampleSupervisor {
492
543
  if (this.bailIfStopped()) return;
493
544
  const server = await startSampleServer({
494
545
  appDir, port: DEFAULT_SAMPLE_PORT, spawnFn: deps.spawnFn,
546
+ agentUserId: deps.resolveAgentUserId?.() ?? null,
495
547
  });
496
548
  this.serverChild = server.child;
497
549
  if (this.bailIfStopped()) return;
@@ -499,20 +551,22 @@ export class LivewareSampleSupervisor {
499
551
  const appId = await livewareAppCreate({
500
552
  livewarePath, name: LIVEWARE_SAMPLE_APP_NAME, execFileFn: deps.execFileFn,
501
553
  });
502
- const tunnel = await startTunnel({
503
- livewarePath, appId, port: server.port, spawnFn: deps.spawnFn,
554
+ const publicUrl = await tunnelBind({
555
+ livewarePath, appId, port: server.port, execFileFn: deps.execFileFn,
504
556
  });
505
- this.tunnelChild = tunnel.child;
506
557
  if (this.bailIfStopped()) return;
507
- await deps.registerApp({ name: LIVEWARE_SAMPLE_APP_NAME, appId, url: tunnel.publicUrl });
558
+ const agent = await startTunnelAgent({ livewarePath, spawnFn: deps.spawnFn });
559
+ this.tunnelChild = agent.child;
560
+ if (this.bailIfStopped()) return;
561
+ await deps.registerApp({ name: LIVEWARE_SAMPLE_APP_NAME, appId, url: publicUrl });
508
562
  deps.store.upsertLivewareSample({
509
563
  platform: deps.platform, accountId: deps.accountId,
510
564
  appId, appName: LIVEWARE_SAMPLE_APP_NAME, port: server.port,
511
- publicUrl: tunnel.publicUrl, sampleVersion: version, status: "active",
565
+ publicUrl, sampleVersion: version, status: "active",
512
566
  });
513
567
  this.watchChildren();
514
568
  await this.deliverIntro();
515
- deps.log?.debug?.(`liveware-sample: bootstrap complete at ${tunnel.publicUrl}`);
569
+ deps.log?.debug?.(`liveware-sample: bootstrap complete at ${publicUrl}`);
516
570
  }
517
571
 
518
572
  private async relaunch(row: LivewareSampleRow): Promise<void> {
@@ -551,23 +605,26 @@ export class LivewareSampleSupervisor {
551
605
  if (this.bailIfStopped()) return;
552
606
  const server = await startSampleServer({
553
607
  appDir, port: row.port || DEFAULT_SAMPLE_PORT, spawnFn: deps.spawnFn,
608
+ agentUserId: deps.resolveAgentUserId?.() ?? null,
554
609
  });
555
610
  this.serverChild = server.child;
556
611
  if (this.bailIfStopped()) return;
557
- const tunnel = await startTunnel({
558
- livewarePath, appId: row.app_id, port: server.port, spawnFn: deps.spawnFn,
612
+ const publicUrl = await tunnelBind({
613
+ livewarePath, appId: row.app_id, port: server.port, execFileFn: deps.execFileFn,
559
614
  });
560
- this.tunnelChild = tunnel.child;
561
615
  if (this.bailIfStopped()) return;
562
- if (tunnel.publicUrl !== row.public_url) {
616
+ const agent = await startTunnelAgent({ livewarePath, spawnFn: deps.spawnFn });
617
+ this.tunnelChild = agent.child;
618
+ if (this.bailIfStopped()) return;
619
+ if (publicUrl !== row.public_url) {
563
620
  await deps.registerApp({
564
- name: row.app_name, appId: row.app_id, url: tunnel.publicUrl,
621
+ name: row.app_name, appId: row.app_id, url: publicUrl,
565
622
  });
566
623
  }
567
624
  deps.store.upsertLivewareSample({
568
625
  platform: deps.platform, accountId: deps.accountId,
569
626
  appId: row.app_id, appName: row.app_name, port: server.port,
570
- publicUrl: tunnel.publicUrl, sampleVersion: version, status: "active",
627
+ publicUrl, sampleVersion: version, status: "active",
571
628
  });
572
629
  this.watchChildren();
573
630
  if (row.intro_sent === 0) await this.deliverIntro();
package/src/runtime.ts CHANGED
@@ -1119,6 +1119,7 @@ export async function startOpenclawClawlingGateway(params: StartGatewayParams):
1119
1119
  store: store as unknown as LivewareSampleStoreLike,
1120
1120
  sampleRoot: path.join(stateDirForSample ?? "", "clawchat", "liveware-sample"),
1121
1121
  resolveToken: () => account.token ?? "",
1122
+ resolveAgentUserId: () => account.userId ?? null,
1122
1123
  resolveLivewarePath: () => resolveLivewarePath(stateDirForSample),
1123
1124
  cliReady: livewareCliReady,
1124
1125
  listApps: async () => {