pubblue 0.4.12 → 0.6.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/index.js CHANGED
@@ -1,55 +1,37 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- getSocketPath,
4
- ipcCall
5
- } from "./chunk-HJ5LTUHS.js";
6
- import {
7
- TunnelApiClient,
8
- TunnelApiError
9
- } from "./chunk-7NFHPJ76.js";
10
2
  import {
11
3
  CHANNELS,
12
4
  CONTROL_CHANNEL,
13
- generateMessageId
14
- } from "./chunk-4YTJ2WKF.js";
15
-
16
- // src/lib/cli-error.ts
17
- import { CommanderError } from "commander";
18
- var CliError = class extends Error {
19
- exitCode;
20
- constructor(message, exitCode = 1) {
21
- super(message);
22
- this.name = "CliError";
23
- this.exitCode = exitCode;
24
- }
25
- };
26
- function failCli(message, exitCode = 1) {
27
- throw new CliError(message, exitCode);
28
- }
29
- function toCliFailure(error) {
30
- if (error instanceof CommanderError) {
31
- return {
32
- exitCode: error.exitCode,
33
- message: ""
34
- };
35
- }
36
- if (error instanceof CliError) {
37
- return {
38
- exitCode: error.exitCode,
39
- message: error.message
40
- };
41
- }
42
- if (error instanceof Error) {
43
- return {
44
- exitCode: 1,
45
- message: error.message
46
- };
47
- }
48
- return {
49
- exitCode: 1,
50
- message: String(error)
51
- };
52
- }
5
+ PubApiClient,
6
+ TEXT_FILE_EXTENSIONS,
7
+ buildBridgeProcessEnv,
8
+ buildDaemonForkStdio,
9
+ createApiClient,
10
+ ensureNodeDatachannelAvailable,
11
+ failCli,
12
+ formatApiError,
13
+ generateMessageId,
14
+ getAgentSocketPath,
15
+ getConfig,
16
+ getFollowReadDelayMs,
17
+ getMimeType,
18
+ getTelegramMiniAppUrl,
19
+ ipcCall,
20
+ isDaemonRunning,
21
+ liveInfoPath,
22
+ liveLogPath,
23
+ loadConfig,
24
+ messageContainsPong,
25
+ parsePositiveIntegerOption,
26
+ readLogTail,
27
+ resolveActiveSlug,
28
+ resolveBridgeMode,
29
+ saveConfig,
30
+ stopOtherDaemons,
31
+ toCliFailure,
32
+ waitForDaemonReady,
33
+ writeLatestCliVersion
34
+ } from "./chunk-BBJOOZHS.js";
53
35
 
54
36
  // src/program.ts
55
37
  import { Command } from "commander";
@@ -57,136 +39,9 @@ import { Command } from "commander";
57
39
  // src/commands/configure.ts
58
40
  import { createInterface } from "readline/promises";
59
41
 
60
- // src/lib/config.ts
42
+ // src/commands/shared.ts
61
43
  import * as fs from "fs";
62
- import * as os from "os";
63
44
  import * as path from "path";
64
- var DEFAULT_BASE_URL = "https://silent-guanaco-514.convex.site";
65
- function getConfigDir(homeDir) {
66
- const home = homeDir || os.homedir();
67
- return path.join(home, ".config", "pubblue");
68
- }
69
- function getConfigPath(homeDir) {
70
- const dir = getConfigDir(homeDir);
71
- fs.mkdirSync(dir, { recursive: true, mode: 448 });
72
- try {
73
- fs.chmodSync(dir, 448);
74
- } catch {
75
- }
76
- return path.join(dir, "config.json");
77
- }
78
- function loadConfig(homeDir) {
79
- const configPath = getConfigPath(homeDir);
80
- if (!fs.existsSync(configPath)) return null;
81
- const raw = fs.readFileSync(configPath, "utf-8");
82
- return JSON.parse(raw);
83
- }
84
- function saveConfig(config, homeDir) {
85
- const configPath = getConfigPath(homeDir);
86
- fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
87
- `, {
88
- mode: 384
89
- });
90
- try {
91
- fs.chmodSync(configPath, 384);
92
- } catch {
93
- }
94
- }
95
- function getConfig(homeDir) {
96
- const envKey = process.env.PUBBLUE_API_KEY;
97
- const envUrl = process.env.PUBBLUE_URL;
98
- const baseUrl = envUrl || DEFAULT_BASE_URL;
99
- const saved = loadConfig(homeDir);
100
- if (envKey) {
101
- return { apiKey: envKey, baseUrl, bridge: saved?.bridge };
102
- }
103
- if (!saved) {
104
- throw new Error(
105
- "Not configured. Run `pubblue configure` or set PUBBLUE_API_KEY environment variable."
106
- );
107
- }
108
- return {
109
- apiKey: saved.apiKey,
110
- baseUrl,
111
- bridge: saved.bridge
112
- };
113
- }
114
- function getTelegramMiniAppUrl(type, id) {
115
- const saved = loadConfig();
116
- if (!saved?.telegram?.botUsername) return null;
117
- return `https://t.me/${saved.telegram.botUsername}?startapp=${type === "pub" ? "p" : "t"}_${id}`;
118
- }
119
-
120
- // src/commands/shared.ts
121
- import * as fs2 from "fs";
122
- import * as path2 from "path";
123
-
124
- // src/lib/api.ts
125
- var PubApiClient = class {
126
- constructor(baseUrl, apiKey) {
127
- this.baseUrl = baseUrl;
128
- this.apiKey = apiKey;
129
- }
130
- async request(path6, options = {}) {
131
- const url = new URL(path6, this.baseUrl);
132
- const res = await fetch(url, {
133
- ...options,
134
- headers: {
135
- "Content-Type": "application/json",
136
- Authorization: `Bearer ${this.apiKey}`,
137
- ...options.headers
138
- }
139
- });
140
- const data = await res.json();
141
- if (!res.ok) {
142
- throw new Error(data.error || `Request failed with status ${res.status}`);
143
- }
144
- return data;
145
- }
146
- async create(opts) {
147
- return this.request("/api/v1/publications", {
148
- method: "POST",
149
- body: JSON.stringify(opts)
150
- });
151
- }
152
- async get(slug) {
153
- const data = await this.request(`/api/v1/publications/${encodeURIComponent(slug)}`);
154
- return data.publication;
155
- }
156
- async listPage(cursor, limit) {
157
- const params = new URLSearchParams();
158
- if (cursor) params.set("cursor", cursor);
159
- if (limit) params.set("limit", String(limit));
160
- const qs = params.toString();
161
- return this.request(`/api/v1/publications${qs ? `?${qs}` : ""}`);
162
- }
163
- async list() {
164
- const all = [];
165
- let cursor;
166
- do {
167
- const result = await this.listPage(cursor, 100);
168
- all.push(...result.publications);
169
- cursor = result.hasMore ? result.cursor : void 0;
170
- } while (cursor);
171
- return all;
172
- }
173
- async update(opts) {
174
- const { slug, newSlug, ...rest } = opts;
175
- const body = { ...rest };
176
- if (newSlug) body.slug = newSlug;
177
- return this.request(`/api/v1/publications/${encodeURIComponent(slug)}`, {
178
- method: "PATCH",
179
- body: JSON.stringify(body)
180
- });
181
- }
182
- async remove(slug) {
183
- await this.request(`/api/v1/publications/${encodeURIComponent(slug)}`, {
184
- method: "DELETE"
185
- });
186
- }
187
- };
188
-
189
- // src/commands/shared.ts
190
45
  function createClient() {
191
46
  const config = getConfig();
192
47
  return new PubApiClient(config.baseUrl, config.apiKey);
@@ -210,13 +65,13 @@ function resolveVisibilityFlags(opts) {
210
65
  return void 0;
211
66
  }
212
67
  function readFile(filePath) {
213
- const resolved = path2.resolve(filePath);
214
- if (!fs2.existsSync(resolved)) {
68
+ const resolved = path.resolve(filePath);
69
+ if (!fs.existsSync(resolved)) {
215
70
  failCli(`File not found: ${resolved}`);
216
71
  }
217
72
  return {
218
- content: fs2.readFileSync(resolved, "utf-8"),
219
- basename: path2.basename(resolved)
73
+ content: fs.readFileSync(resolved, "utf-8"),
74
+ basename: path.basename(resolved)
220
75
  };
221
76
  }
222
77
 
@@ -271,11 +126,6 @@ function parseBooleanValue(raw, key) {
271
126
  return false;
272
127
  throw new Error(`Invalid boolean value for ${key}: ${raw}`);
273
128
  }
274
- function parseBridgeModeValue(raw) {
275
- const normalized = raw.trim().toLowerCase();
276
- if (normalized === "openclaw" || normalized === "none") return normalized;
277
- throw new Error(`Invalid bridge mode: ${raw}. Use openclaw or none.`);
278
- }
279
129
  function parsePositiveInteger(raw, key) {
280
130
  const parsed = Number.parseInt(raw, 10);
281
131
  if (!Number.isFinite(parsed) || parsed <= 0) {
@@ -284,7 +134,6 @@ function parsePositiveInteger(raw, key) {
284
134
  return parsed;
285
135
  }
286
136
  var SUPPORTED_KEYS = [
287
- "bridge.mode",
288
137
  "openclaw.path",
289
138
  "openclaw.sessionId",
290
139
  "openclaw.threadId",
@@ -299,9 +148,6 @@ var SUPPORTED_KEYS = [
299
148
  ];
300
149
  function applyConfigSet(bridge, telegram, key, value) {
301
150
  switch (key) {
302
- case "bridge.mode":
303
- bridge.mode = parseBridgeModeValue(value);
304
- return;
305
151
  case "openclaw.path":
306
152
  bridge.openclawPath = value;
307
153
  return;
@@ -347,9 +193,6 @@ function applyConfigSet(bridge, telegram, key, value) {
347
193
  }
348
194
  function applyConfigUnset(bridge, telegram, key) {
349
195
  switch (key) {
350
- case "bridge.mode":
351
- delete bridge.mode;
352
- return;
353
196
  case "openclaw.path":
354
197
  delete bridge.openclawPath;
355
198
  return;
@@ -407,13 +250,11 @@ async function telegramGetMe(token) {
407
250
  hasMainWebApp: data.result.has_main_web_app === true
408
251
  };
409
252
  }
410
- async function telegramSetMenuButton(token, url) {
253
+ async function telegramSetMenuButton(token, button) {
411
254
  const resp = await fetch(`https://api.telegram.org/bot${token}/setChatMenuButton`, {
412
255
  method: "POST",
413
256
  headers: { "Content-Type": "application/json" },
414
- body: JSON.stringify({
415
- menu_button: { type: "web_app", text: "Open", web_app: { url } }
416
- })
257
+ body: JSON.stringify({ menu_button: button })
417
258
  });
418
259
  const data = await resp.json();
419
260
  if (!data.ok) {
@@ -428,7 +269,6 @@ function printConfigSummary(saved) {
428
269
  console.log("Saved config:");
429
270
  console.log(` apiKey: ${maskSecret(saved.apiKey)}`);
430
271
  if (saved.bridge && hasValues(saved.bridge)) {
431
- console.log(` bridge.mode: ${saved.bridge.mode ?? "(unset)"}`);
432
272
  if (saved.bridge.openclawPath) console.log(` openclaw.path: ${saved.bridge.openclawPath}`);
433
273
  if (saved.bridge.sessionId) console.log(` openclaw.sessionId: ${saved.bridge.sessionId}`);
434
274
  if (saved.bridge.threadId) console.log(` openclaw.threadId: ${saved.bridge.threadId}`);
@@ -503,6 +343,16 @@ function registerConfigureCommand(program2) {
503
343
  if (key === "telegram.botToken") telegramTokenChanged = true;
504
344
  }
505
345
  for (const key of opts.unset) {
346
+ if (key.trim() === "telegram.botToken" && nextTelegram.botToken) {
347
+ try {
348
+ await telegramSetMenuButton(nextTelegram.botToken, { type: "default" });
349
+ console.log("Telegram menu button reset to default.");
350
+ } catch (error) {
351
+ console.error(
352
+ `Warning: failed to reset Telegram menu button: ${error instanceof Error ? error.message : String(error)}`
353
+ );
354
+ }
355
+ }
506
356
  applyConfigUnset(nextBridge, nextTelegram, key.trim());
507
357
  }
508
358
  if (telegramTokenChanged && nextTelegram.botToken) {
@@ -511,7 +361,11 @@ function registerConfigureCommand(program2) {
511
361
  nextTelegram.botUsername = bot.username;
512
362
  nextTelegram.hasMainWebApp = bot.hasMainWebApp;
513
363
  console.log(` Bot: @${bot.username}`);
514
- await telegramSetMenuButton(nextTelegram.botToken, "https://pub.blue");
364
+ await telegramSetMenuButton(nextTelegram.botToken, {
365
+ type: "web_app",
366
+ text: "Open",
367
+ web_app: { url: "https://pub.blue" }
368
+ });
515
369
  console.log(" Menu button set to https://pub.blue");
516
370
  if (!bot.hasMainWebApp) {
517
371
  console.log("");
@@ -534,525 +388,316 @@ function registerConfigureCommand(program2) {
534
388
  );
535
389
  }
536
390
 
537
- // src/commands/publications.ts
538
- function registerPublicationCommands(program2) {
539
- program2.command("create").description("Create a new publication").argument("[file]", "Path to the file (reads stdin if omitted)").option("--slug <slug>", "Custom slug for the URL").option("--title <title>", "Title for the publication").option("--public", "Make the publication public").option("--private", "Make the publication private (default)").option("--expires <duration>", "Auto-delete after duration (e.g. 1h, 24h, 7d)").action(
540
- async (fileArg, opts) => {
541
- const client = createClient();
542
- let content;
543
- let filename;
544
- if (fileArg) {
545
- const file = readFile(fileArg);
546
- content = file.content;
547
- filename = file.basename;
548
- } else {
549
- content = await readFromStdin();
550
- }
551
- const resolvedVisibility = resolveVisibilityFlags({
552
- public: opts.public,
553
- private: opts.private,
554
- commandName: "create"
555
- });
556
- const result = await client.create({
557
- content,
558
- filename,
559
- title: opts.title,
560
- slug: opts.slug,
561
- isPublic: resolvedVisibility ?? false,
562
- expiresIn: opts.expires
563
- });
564
- console.log(`Created: ${result.url}`);
565
- const tmaUrl = getTelegramMiniAppUrl("pub", result.slug);
566
- if (tmaUrl) console.log(`Telegram: ${tmaUrl}`);
567
- if (result.expiresAt) {
568
- console.log(` Expires: ${new Date(result.expiresAt).toISOString()}`);
391
+ // src/commands/live.ts
392
+ import * as fs2 from "fs";
393
+ import * as path2 from "path";
394
+
395
+ // package.json
396
+ var package_default = {
397
+ name: "pubblue",
398
+ version: "0.6.1",
399
+ description: "CLI tool for publishing content and running interactive sessions via pub.blue",
400
+ type: "module",
401
+ bin: {
402
+ pubblue: "./dist/index.js"
403
+ },
404
+ scripts: {
405
+ build: "tsup src/index.ts src/tunnel-daemon-entry.ts --format esm --dts --clean",
406
+ dev: "tsup src/index.ts src/tunnel-daemon-entry.ts --format esm --watch",
407
+ test: "vitest run",
408
+ "test:watch": "vitest",
409
+ lint: "tsc --noEmit"
410
+ },
411
+ dependencies: {
412
+ commander: "^13.0.0",
413
+ "node-datachannel": "^0.32.0"
414
+ },
415
+ devDependencies: {
416
+ "@types/node": "22.10.2",
417
+ tsup: "^8.3.6",
418
+ typescript: "^5.7.2",
419
+ vitest: "^3.0.0"
420
+ },
421
+ files: [
422
+ "dist"
423
+ ],
424
+ repository: {
425
+ type: "git",
426
+ url: "git+https://github.com/xmanatee/pub.git",
427
+ directory: "cli"
428
+ },
429
+ publishConfig: {
430
+ access: "public"
431
+ },
432
+ pnpm: {
433
+ onlyBuiltDependencies: [
434
+ "esbuild",
435
+ "node-datachannel"
436
+ ]
437
+ }
438
+ };
439
+
440
+ // src/lib/version.ts
441
+ var version = package_default.version;
442
+ if (typeof version !== "string" || version.length === 0) {
443
+ throw new Error("Invalid CLI version in package.json");
444
+ }
445
+ var CLI_VERSION = version;
446
+
447
+ // src/commands/live.ts
448
+ function registerLiveCommands(program2) {
449
+ registerStartCommand(program2);
450
+ registerStopCommand(program2);
451
+ registerStatusCommand(program2);
452
+ registerWriteCommand(program2);
453
+ registerReadCommand(program2);
454
+ registerChannelsCommand(program2);
455
+ registerDoctorCommand(program2);
456
+ }
457
+ function registerStartCommand(program2) {
458
+ program2.command("start").description("Start the agent daemon (registers presence, awaits live requests)").requiredOption("--agent-name <name>", "Agent display name shown to the browser user").option("--bridge <mode>", "Bridge mode: openclaw|none").option("--foreground", "Run in foreground (don't fork)").action(async (opts) => {
459
+ await ensureNodeDatachannelAvailable();
460
+ writeLatestCliVersion(CLI_VERSION);
461
+ const runtimeConfig = getConfig();
462
+ const apiClient = createApiClient(runtimeConfig);
463
+ const bridgeMode = resolveBridgeMode(opts);
464
+ const bridgeProcessEnv = buildBridgeProcessEnv(runtimeConfig.bridge);
465
+ const socketPath = getAgentSocketPath();
466
+ const infoPath = liveInfoPath("agent");
467
+ const logPath = liveLogPath("agent");
468
+ await stopOtherDaemons();
469
+ if (opts.foreground) {
470
+ const { startDaemon } = await import("./tunnel-daemon-BR5XKNEA.js");
471
+ console.log("Agent daemon starting in foreground...");
472
+ console.log("Press Ctrl+C to stop.");
473
+ try {
474
+ await startDaemon({
475
+ cliVersion: CLI_VERSION,
476
+ apiClient,
477
+ socketPath,
478
+ infoPath,
479
+ bridgeMode,
480
+ agentName: opts.agentName
481
+ });
482
+ } catch (error) {
483
+ const message = error instanceof Error ? error.message : String(error);
484
+ failCli(`Daemon failed: ${message}`);
569
485
  }
570
- }
571
- );
572
- program2.command("get").description("Get details of a publication").argument("<slug>", "Slug of the publication").option("--content", "Output raw content to stdout (no metadata, pipeable)").action(async (slug, opts) => {
573
- const client = createClient();
574
- const pub = await client.get(slug);
575
- if (opts.content) {
576
- process.stdout.write(pub.content);
577
486
  return;
578
487
  }
579
- console.log(` Slug: ${pub.slug}`);
580
- console.log(` Type: ${pub.contentType}`);
581
- if (pub.title) console.log(` Title: ${pub.title}`);
582
- console.log(` Status: ${formatVisibility(pub.isPublic)}`);
583
- if (pub.expiresAt) console.log(` Expires: ${new Date(pub.expiresAt).toISOString()}`);
584
- console.log(` Created: ${new Date(pub.createdAt).toLocaleDateString()}`);
585
- console.log(` Updated: ${new Date(pub.updatedAt).toLocaleDateString()}`);
586
- console.log(` Size: ${pub.content.length} bytes`);
587
- });
588
- program2.command("update").description("Update a publication's content and/or metadata").argument("<slug>", "Slug of the publication to update").option("--file <file>", "New content from file").option("--title <title>", "New title").option("--public", "Make the publication public").option("--private", "Make the publication private").option("--slug <newSlug>", "Rename the slug").action(
589
- async (slug, opts) => {
590
- const client = createClient();
591
- let content;
592
- let filename;
593
- if (opts.file) {
594
- const file = readFile(opts.file);
595
- content = file.content;
596
- filename = file.basename;
488
+ const { fork } = await import("child_process");
489
+ const daemonScript = path2.join(import.meta.dirname, "tunnel-daemon-entry.js");
490
+ const daemonLogFd = fs2.openSync(logPath, "a");
491
+ const child = fork(daemonScript, [], {
492
+ detached: true,
493
+ stdio: buildDaemonForkStdio(daemonLogFd),
494
+ env: {
495
+ ...bridgeProcessEnv,
496
+ PUBBLUE_DAEMON_BASE_URL: runtimeConfig.baseUrl,
497
+ PUBBLUE_DAEMON_API_KEY: runtimeConfig.apiKey,
498
+ PUBBLUE_DAEMON_SOCKET: socketPath,
499
+ PUBBLUE_DAEMON_INFO: infoPath,
500
+ PUBBLUE_DAEMON_AGENT_NAME: opts.agentName,
501
+ PUBBLUE_CLI_VERSION: CLI_VERSION,
502
+ PUBBLUE_DAEMON_BRIDGE_MODE: bridgeMode
597
503
  }
598
- const isPublic = resolveVisibilityFlags({
599
- public: opts.public,
600
- private: opts.private,
601
- commandName: "update"
602
- });
603
- const result = await client.update({
604
- slug,
605
- content,
606
- filename,
607
- title: opts.title,
608
- isPublic,
609
- newSlug: opts.slug
610
- });
611
- console.log(`Updated: ${result.slug}`);
612
- if (result.title) console.log(` Title: ${result.title}`);
613
- console.log(` Status: ${formatVisibility(result.isPublic)}`);
614
- }
615
- );
616
- program2.command("list").description("List your publications").action(async () => {
617
- const client = createClient();
618
- const pubs = await client.list();
619
- if (pubs.length === 0) {
620
- console.log("No publications.");
621
- return;
504
+ });
505
+ fs2.closeSync(daemonLogFd);
506
+ if (child.connected) {
507
+ child.disconnect();
622
508
  }
623
- for (const pub of pubs) {
624
- const date = new Date(pub.createdAt).toLocaleDateString();
625
- const expires = pub.expiresAt ? ` expires:${new Date(pub.expiresAt).toISOString()}` : "";
626
- console.log(
627
- ` ${pub.slug} [${pub.contentType}] ${formatVisibility(pub.isPublic)} ${date}${expires}`
628
- );
509
+ child.unref();
510
+ console.log("Starting agent daemon...");
511
+ const ready = await waitForDaemonReady({
512
+ child,
513
+ infoPath,
514
+ socketPath,
515
+ timeoutMs: 8e3
516
+ });
517
+ if (!ready.ok) {
518
+ const lines = [
519
+ `Daemon failed to start: ${ready.reason ?? "unknown reason"}`,
520
+ `Daemon log: ${logPath}`
521
+ ];
522
+ const tail = readLogTail(logPath);
523
+ if (tail) {
524
+ lines.push("---- daemon log tail ----");
525
+ lines.push(tail.trimEnd());
526
+ lines.push("---- end daemon log tail ----");
527
+ }
528
+ failCli(lines.join("\n"));
629
529
  }
530
+ console.log("Agent daemon started. Waiting for browser to initiate live.");
531
+ console.log(`Daemon log: ${logPath}`);
532
+ console.log(`Bridge mode: ${bridgeMode}`);
630
533
  });
631
- program2.command("delete").description("Delete a publication").argument("<slug>", "Slug of the publication to delete").action(async (slug) => {
632
- const client = createClient();
633
- await client.remove(slug);
634
- console.log(`Deleted: ${slug}`);
635
- });
636
- }
637
-
638
- // src/commands/tunnel/management-commands.ts
639
- import * as fs4 from "fs";
640
-
641
- // src/commands/tunnel-helpers.ts
642
- import { fork } from "child_process";
643
- import * as fs3 from "fs";
644
- import * as path3 from "path";
645
- var TEXT_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
646
- ".txt",
647
- ".md",
648
- ".markdown",
649
- ".json",
650
- ".csv",
651
- ".xml",
652
- ".yaml",
653
- ".yml",
654
- ".js",
655
- ".mjs",
656
- ".cjs",
657
- ".ts",
658
- ".tsx",
659
- ".jsx",
660
- ".css",
661
- ".scss",
662
- ".sass",
663
- ".less",
664
- ".log"
665
- ]);
666
- function getMimeType(filePath) {
667
- const ext = path3.extname(filePath).toLowerCase();
668
- const mimeByExt = {
669
- ".html": "text/html; charset=utf-8",
670
- ".htm": "text/html; charset=utf-8",
671
- ".txt": "text/plain; charset=utf-8",
672
- ".md": "text/markdown; charset=utf-8",
673
- ".markdown": "text/markdown; charset=utf-8",
674
- ".json": "application/json",
675
- ".csv": "text/csv; charset=utf-8",
676
- ".xml": "application/xml",
677
- ".yaml": "application/x-yaml",
678
- ".yml": "application/x-yaml",
679
- ".png": "image/png",
680
- ".jpg": "image/jpeg",
681
- ".jpeg": "image/jpeg",
682
- ".gif": "image/gif",
683
- ".webp": "image/webp",
684
- ".svg": "image/svg+xml",
685
- ".pdf": "application/pdf",
686
- ".zip": "application/zip",
687
- ".mp3": "audio/mpeg",
688
- ".wav": "audio/wav",
689
- ".mp4": "video/mp4"
690
- };
691
- return mimeByExt[ext] || "application/octet-stream";
692
- }
693
- function tunnelInfoDir() {
694
- const dir = path3.join(
695
- process.env.HOME || process.env.USERPROFILE || "/tmp",
696
- ".config",
697
- "pubblue",
698
- "tunnels"
699
- );
700
- if (!fs3.existsSync(dir)) fs3.mkdirSync(dir, { recursive: true });
701
- return dir;
702
- }
703
- function tunnelInfoPath(tunnelId) {
704
- return path3.join(tunnelInfoDir(), `${tunnelId}.json`);
705
- }
706
- function tunnelLogPath(tunnelId) {
707
- return path3.join(tunnelInfoDir(), `${tunnelId}.log`);
708
- }
709
- function bridgeInfoPath(tunnelId) {
710
- return path3.join(tunnelInfoDir(), `${tunnelId}.bridge.json`);
711
534
  }
712
- function bridgeLogPath(tunnelId) {
713
- return path3.join(tunnelInfoDir(), `${tunnelId}.bridge.log`);
714
- }
715
- function createApiClient(configOverride) {
716
- const config = configOverride || getConfig();
717
- return new TunnelApiClient(config.baseUrl, config.apiKey);
718
- }
719
- function buildBridgeProcessEnv(bridgeConfig) {
720
- const env = { ...process.env };
721
- if (!bridgeConfig) return env;
722
- const setIfMissing = (key, value) => {
723
- if (value === void 0 || value === null) return;
724
- const current = env[key];
725
- if (typeof current === "string" && current.length > 0) return;
726
- env[key] = String(value);
727
- };
728
- setIfMissing("OPENCLAW_PATH", bridgeConfig.openclawPath);
729
- setIfMissing("OPENCLAW_SESSION_ID", bridgeConfig.sessionId);
730
- setIfMissing("OPENCLAW_THREAD_ID", bridgeConfig.threadId);
731
- if (bridgeConfig.canvasReminderEvery !== void 0) {
732
- setIfMissing("OPENCLAW_CANVAS_REMINDER_EVERY", bridgeConfig.canvasReminderEvery);
733
- }
734
- if (bridgeConfig.deliver !== void 0) {
735
- setIfMissing("OPENCLAW_DELIVER", bridgeConfig.deliver ? "1" : "0");
736
- }
737
- setIfMissing("OPENCLAW_DELIVER_CHANNEL", bridgeConfig.deliverChannel);
738
- setIfMissing("OPENCLAW_REPLY_TO", bridgeConfig.replyTo);
739
- if (bridgeConfig.deliverTimeoutMs !== void 0) {
740
- setIfMissing("OPENCLAW_DELIVER_TIMEOUT_MS", bridgeConfig.deliverTimeoutMs);
741
- }
742
- setIfMissing("OPENCLAW_ATTACHMENT_DIR", bridgeConfig.attachmentDir);
743
- if (bridgeConfig.attachmentMaxBytes !== void 0) {
744
- setIfMissing("OPENCLAW_ATTACHMENT_MAX_BYTES", bridgeConfig.attachmentMaxBytes);
745
- }
746
- return env;
747
- }
748
- async function ensureNodeDatachannelAvailable() {
749
- try {
750
- await import("node-datachannel");
751
- } catch (error) {
752
- const message = error instanceof Error ? error.message : String(error);
753
- failCli(
754
- [
755
- "node-datachannel native module is not available.",
756
- "Run `pnpm rebuild node-datachannel` in the cli package and retry.",
757
- `Details: ${message}`
758
- ].join("\n")
759
- );
760
- }
761
- }
762
- function isDaemonRunning(tunnelId) {
763
- return readDaemonProcessInfo(tunnelId) !== null;
764
- }
765
- function readDaemonProcessInfo(tunnelId) {
766
- const infoPath = tunnelInfoPath(tunnelId);
767
- if (!fs3.existsSync(infoPath)) return null;
768
- try {
769
- const info = JSON.parse(fs3.readFileSync(infoPath, "utf-8"));
770
- if (!Number.isFinite(info.pid)) throw new Error("invalid daemon pid");
771
- process.kill(info.pid, 0);
772
- return info;
773
- } catch {
774
- try {
775
- fs3.unlinkSync(infoPath);
776
- } catch {
535
+ function registerStopCommand(program2) {
536
+ program2.command("stop").description("Stop the agent daemon (deregisters presence, closes active live)").action(async () => {
537
+ if (!isDaemonRunning("agent")) {
538
+ console.log("Agent daemon is not running.");
539
+ return;
777
540
  }
778
- return null;
779
- }
780
- }
781
- function readBridgeProcessInfo(tunnelId) {
782
- const infoPath = bridgeInfoPath(tunnelId);
783
- if (!fs3.existsSync(infoPath)) return null;
784
- try {
785
- return JSON.parse(fs3.readFileSync(infoPath, "utf-8"));
786
- } catch {
787
- return null;
788
- }
541
+ await stopOtherDaemons();
542
+ console.log("Agent daemon stopped.");
543
+ });
789
544
  }
790
- function isBridgeRunning(tunnelId) {
791
- const infoPath = bridgeInfoPath(tunnelId);
792
- if (!fs3.existsSync(infoPath)) return false;
793
- try {
794
- const info = JSON.parse(fs3.readFileSync(infoPath, "utf-8"));
795
- process.kill(info.pid, 0);
796
- return true;
797
- } catch {
545
+ function registerStatusCommand(program2) {
546
+ program2.command("status").description("Check agent daemon and live connection status").action(async () => {
547
+ const socketPath = getAgentSocketPath();
548
+ let response;
798
549
  try {
799
- fs3.unlinkSync(infoPath);
550
+ response = await ipcCall(socketPath, { method: "status", params: {} });
800
551
  } catch {
552
+ console.log("Agent daemon is not running.");
553
+ return;
801
554
  }
802
- return false;
803
- }
804
- }
805
- function stopBridgeProcess(tunnelId) {
806
- const info = readBridgeProcessInfo(tunnelId);
807
- if (!info || !Number.isFinite(info.pid)) return;
808
- try {
809
- process.kill(info.pid, "SIGTERM");
810
- } catch {
811
- }
812
- }
813
- function buildBridgeForkStdio(logFd) {
814
- return ["ignore", logFd, logFd, "ipc"];
815
- }
816
- function getFollowReadDelayMs(disconnected, consecutiveFailures) {
817
- if (!disconnected) return 1e3;
818
- return Math.min(5e3, 1e3 * 2 ** Math.min(consecutiveFailures, 3));
819
- }
820
- function resolveTunnelIdSelection(tunnelIdArg, tunnelOpt) {
821
- return tunnelOpt || tunnelIdArg;
822
- }
823
- function buildDaemonForkStdio(logFd) {
824
- return ["ignore", logFd, logFd, "ipc"];
555
+ const activeSlug = response.activeSlug;
556
+ console.log(` Daemon: running`);
557
+ console.log(` Active slug: ${activeSlug || "(none)"}`);
558
+ console.log(` Status: ${response.connected ? "connected" : "waiting"}`);
559
+ console.log(` Uptime: ${response.uptime}s`);
560
+ const chNames = Array.isArray(response.channels) ? response.channels.map((c) => typeof c === "string" ? c : String(c)) : [];
561
+ console.log(` Channels: ${chNames.join(", ") || "(none)"}`);
562
+ console.log(` Buffered: ${response.bufferedMessages ?? 0} messages`);
563
+ if (typeof response.lastError === "string" && response.lastError.length > 0) {
564
+ console.log(` Last error: ${response.lastError}`);
565
+ }
566
+ const logPath = liveLogPath("agent");
567
+ if (fs2.existsSync(logPath)) {
568
+ console.log(` Log: ${logPath}`);
569
+ }
570
+ const bridge = response.bridge;
571
+ if (bridge) {
572
+ console.log(` Bridge: openclaw (${bridge.running ? "running" : "stopped"})`);
573
+ if (bridge.sessionId) {
574
+ console.log(` Bridge session: ${bridge.sessionId}`);
575
+ }
576
+ if (bridge.sessionSource) {
577
+ console.log(` Bridge session source: ${bridge.sessionSource}`);
578
+ }
579
+ if (bridge.sessionKey) {
580
+ console.log(` Bridge session key: ${bridge.sessionKey}`);
581
+ }
582
+ if (bridge.forwardedMessages !== void 0) {
583
+ console.log(` Bridge forwarded: ${bridge.forwardedMessages} messages`);
584
+ }
585
+ if (bridge.lastError) {
586
+ console.log(` Bridge last error: ${bridge.lastError}`);
587
+ }
588
+ }
589
+ });
825
590
  }
826
- function parsePositiveIntegerOption(raw, optionName) {
827
- const parsed = Number.parseInt(raw, 10);
828
- if (!Number.isFinite(parsed) || parsed <= 0) {
829
- throw new Error(`${optionName} must be a positive integer. Received: ${raw}`);
830
- }
831
- return parsed;
832
- }
833
- function parseBridgeMode(raw) {
834
- const normalized = raw.trim().toLowerCase();
835
- if (normalized === "openclaw" || normalized === "none") {
836
- return normalized;
837
- }
838
- throw new Error(`--bridge must be one of: openclaw, none. Received: ${raw}`);
839
- }
840
- function shouldRestartDaemonForCliUpgrade(daemonCliVersion, currentCliVersion) {
841
- if (!daemonCliVersion || daemonCliVersion.trim().length === 0) return true;
842
- return daemonCliVersion.trim() !== currentCliVersion;
843
- }
844
- function messageContainsPong(payload) {
845
- if (!payload || typeof payload !== "object") return false;
846
- const message = payload.msg;
847
- if (!message || typeof message !== "object") return false;
848
- const type = message.type;
849
- const data = message.data;
850
- return type === "text" && typeof data === "string" && data.trim().toLowerCase() === "pong";
851
- }
852
- function getPublicTunnelUrl(tunnelId) {
853
- const base = process.env.PUBBLUE_PUBLIC_URL || "https://pub.blue";
854
- return `${base.replace(/\/$/, "")}/t/${tunnelId}`;
855
- }
856
- function pickReusableTunnel(tunnels, nowMs = Date.now()) {
857
- const active = tunnels.filter((t) => t.status === "active" && t.expiresAt > nowMs).sort((a, b) => b.createdAt - a.createdAt);
858
- return active[0] ?? null;
859
- }
860
- function readLogTail(logPath, maxChars = 4e3) {
861
- if (!fs3.existsSync(logPath)) return null;
862
- try {
863
- const content = fs3.readFileSync(logPath, "utf-8");
864
- if (content.length <= maxChars) return content;
865
- return content.slice(-maxChars);
866
- } catch {
867
- return null;
868
- }
869
- }
870
- function formatApiError(error) {
871
- if (error instanceof TunnelApiError) {
872
- if (error.status === 429 && error.retryAfterSeconds !== void 0) {
873
- return `Rate limit exceeded. Retry after ${error.retryAfterSeconds}s.`;
874
- }
875
- return `${error.message} (HTTP ${error.status})`;
876
- }
877
- return error instanceof Error ? error.message : String(error);
878
- }
879
- async function cleanupCreatedTunnelOnStartFailure(apiClient, target) {
880
- if (!target.createdNew) return;
881
- try {
882
- await apiClient.close(target.tunnelId);
883
- } catch (closeError) {
884
- console.error(
885
- `Failed to clean up newly created tunnel ${target.tunnelId}: ${formatApiError(closeError)}`
886
- );
887
- }
888
- }
889
- async function resolveActiveTunnel() {
890
- const dir = tunnelInfoDir();
891
- const files = fs3.readdirSync(dir).filter((f) => f.endsWith(".json") && !f.endsWith(".bridge.json"));
892
- const active = [];
893
- for (const f of files) {
894
- const tunnelId = f.replace(".json", "");
895
- if (isDaemonRunning(tunnelId)) active.push(tunnelId);
896
- }
897
- if (active.length === 0) {
898
- failCli("No active tunnels. Run `pubblue tunnel start` first.");
899
- }
900
- if (active.length === 1) return active[0];
901
- failCli(`Multiple active tunnels: ${active.join(", ")}. Specify one.`);
902
- }
903
- function waitForDaemonReady({
904
- child,
905
- infoPath,
906
- socketPath,
907
- timeoutMs
908
- }) {
909
- return new Promise((resolve3) => {
910
- let settled = false;
911
- let pollInFlight = false;
912
- let lastIpcError = null;
913
- const done = (result) => {
914
- if (settled) return;
915
- settled = true;
916
- clearInterval(poll);
917
- clearTimeout(timeout);
918
- child.off("exit", onExit);
919
- resolve3(result);
920
- };
921
- const onExit = (code, signal) => {
922
- const suffix = signal ? ` (signal ${signal})` : "";
923
- done({ ok: false, reason: `daemon exited with code ${code ?? 0}${suffix}` });
924
- };
925
- child.on("exit", onExit);
926
- const poll = setInterval(() => {
927
- if (pollInFlight || !fs3.existsSync(infoPath)) return;
928
- pollInFlight = true;
929
- void ipcCall(socketPath, { method: "status", params: {} }).then((status) => {
930
- if (status.ok) done({ ok: true });
931
- }).catch((error) => {
932
- lastIpcError = error instanceof Error ? error.message : String(error);
933
- }).finally(() => {
934
- pollInFlight = false;
935
- });
936
- }, 120);
937
- const timeout = setTimeout(() => {
938
- const reason = lastIpcError ? `timed out after ${timeoutMs}ms waiting for daemon readiness (last IPC error: ${lastIpcError})` : `timed out after ${timeoutMs}ms waiting for daemon readiness`;
939
- done({ ok: false, reason });
940
- }, timeoutMs);
941
- });
942
- }
943
- async function waitForAgentOffer(params) {
944
- const startedAt = Date.now();
945
- let lastError = null;
946
- while (Date.now() - startedAt < params.timeoutMs) {
947
- try {
948
- const tunnel = await params.apiClient.get(params.tunnelId);
949
- if (typeof tunnel.agentOffer === "string" && tunnel.agentOffer.length > 0) {
950
- return { ok: true };
591
+ function registerWriteCommand(program2) {
592
+ program2.command("write").description("Write data to a live channel").argument("[message]", "Text message (or use --file)").option("-c, --channel <channel>", "Channel name", "chat").option("-f, --file <file>", "Read content from file").action(async (messageArg, opts) => {
593
+ let msg;
594
+ let binaryBase64;
595
+ if (opts.file) {
596
+ const filePath = path2.resolve(opts.file);
597
+ const ext = path2.extname(filePath).toLowerCase();
598
+ const bytes = fs2.readFileSync(filePath);
599
+ const filename = path2.basename(filePath);
600
+ if (ext === ".html" || ext === ".htm") {
601
+ msg = {
602
+ id: generateMessageId(),
603
+ type: "html",
604
+ data: bytes.toString("utf-8"),
605
+ meta: { title: filename, filename, mime: getMimeType(filePath), size: bytes.length }
606
+ };
607
+ } else if (TEXT_FILE_EXTENSIONS.has(ext)) {
608
+ msg = {
609
+ id: generateMessageId(),
610
+ type: "text",
611
+ data: bytes.toString("utf-8"),
612
+ meta: { filename, mime: getMimeType(filePath), size: bytes.length }
613
+ };
614
+ } else {
615
+ msg = {
616
+ id: generateMessageId(),
617
+ type: "binary",
618
+ meta: { filename, mime: getMimeType(filePath), size: bytes.length }
619
+ };
620
+ binaryBase64 = bytes.toString("base64");
951
621
  }
952
- } catch (error) {
953
- lastError = formatApiError(error);
622
+ } else if (messageArg) {
623
+ msg = {
624
+ id: generateMessageId(),
625
+ type: "text",
626
+ data: messageArg
627
+ };
628
+ } else {
629
+ const chunks = [];
630
+ for await (const chunk of process.stdin) chunks.push(chunk);
631
+ msg = {
632
+ id: generateMessageId(),
633
+ type: "text",
634
+ data: Buffer.concat(chunks).toString("utf-8").trim()
635
+ };
954
636
  }
955
- await new Promise((resolve3) => setTimeout(resolve3, 150));
956
- }
957
- return {
958
- ok: false,
959
- reason: lastError ? `agent offer was not published in time (last API error: ${lastError})` : "agent offer was not published in time"
960
- };
961
- }
962
- async function ensureBridgeReady(params) {
963
- if (params.bridgeMode === "none") {
964
- return { ok: true };
965
- }
966
- const infoPath = bridgeInfoPath(params.tunnelId);
967
- if (isBridgeRunning(params.tunnelId)) {
968
- return waitForBridgeReady({
969
- infoPath,
970
- tunnelId: params.tunnelId,
971
- timeoutMs: params.timeoutMs
637
+ const socketPath = getAgentSocketPath();
638
+ const response = await ipcCall(socketPath, {
639
+ method: "write",
640
+ params: { channel: opts.channel, msg, binaryBase64 }
972
641
  });
973
- }
974
- const bridgeScript = path3.join(import.meta.dirname, "tunnel-bridge-entry.js");
975
- const logPath = bridgeLogPath(params.tunnelId);
976
- const logFd = fs3.openSync(logPath, "a");
977
- const child = fork(bridgeScript, [], {
978
- detached: true,
979
- stdio: buildBridgeForkStdio(logFd),
980
- env: {
981
- ...params.bridgeProcessEnv,
982
- PUBBLUE_BRIDGE_MODE: params.bridgeMode,
983
- PUBBLUE_BRIDGE_TUNNEL_ID: params.tunnelId,
984
- PUBBLUE_BRIDGE_SOCKET: params.socketPath,
985
- PUBBLUE_BRIDGE_INFO: infoPath
642
+ if (!response.ok) {
643
+ failCli(`Failed: ${response.error}`);
986
644
  }
987
645
  });
988
- fs3.closeSync(logFd);
989
- if (child.connected) {
990
- child.disconnect();
991
- }
992
- child.unref();
993
- return waitForBridgeReady({
994
- child,
995
- infoPath,
996
- tunnelId: params.tunnelId,
997
- timeoutMs: params.timeoutMs
998
- });
999
646
  }
1000
- function waitForBridgeReady({
1001
- child,
1002
- infoPath,
1003
- tunnelId,
1004
- timeoutMs
1005
- }) {
1006
- return new Promise((resolve3) => {
1007
- let settled = false;
1008
- let lastState;
1009
- let lastError;
1010
- const done = (result) => {
1011
- if (settled) return;
1012
- settled = true;
1013
- clearInterval(poll);
1014
- clearTimeout(timeout);
1015
- if (child) {
1016
- child.off("exit", onExit);
647
+ function registerReadCommand(program2) {
648
+ program2.command("read").description("Read buffered messages from live channels").option("-c, --channel <channel>", "Filter by channel").option("--follow", "Stream messages continuously").option("--all", "With --follow, include all channels instead of chat-only default").action(async (opts) => {
649
+ const socketPath = getAgentSocketPath();
650
+ const readChannel = opts.channel || (opts.follow && !opts.all ? CHANNELS.CHAT : void 0);
651
+ if (opts.follow) {
652
+ if (!opts.channel && !opts.all) {
653
+ console.error(
654
+ "Following chat channel by default. Use `--all` to include binary/file channels."
655
+ );
1017
656
  }
1018
- resolve3(result);
1019
- };
1020
- const onExit = (code, signal) => {
1021
- const suffix = signal ? ` (signal ${signal})` : "";
1022
- done({ ok: false, reason: `bridge exited with code ${code ?? 0}${suffix}` });
1023
- };
1024
- if (child) {
1025
- child.on("exit", onExit);
1026
- }
1027
- const poll = setInterval(() => {
1028
- if (!fs3.existsSync(infoPath)) return;
1029
- const info = readBridgeProcessInfo(tunnelId);
1030
- if (!info) return;
1031
- lastState = info.status;
1032
- lastError = info.lastError;
1033
- if (info.status === "ready" && isBridgeRunning(tunnelId)) {
1034
- done({ ok: true });
1035
- return;
657
+ let consecutiveFailures = 0;
658
+ let warnedDisconnected = false;
659
+ while (true) {
660
+ try {
661
+ const response = await ipcCall(socketPath, {
662
+ method: "read",
663
+ params: { channel: readChannel }
664
+ });
665
+ if (warnedDisconnected) {
666
+ console.error("Daemon reconnected.");
667
+ warnedDisconnected = false;
668
+ }
669
+ consecutiveFailures = 0;
670
+ if (response.messages && response.messages.length > 0) {
671
+ for (const m of response.messages) {
672
+ console.log(JSON.stringify(m));
673
+ }
674
+ }
675
+ } catch (error) {
676
+ consecutiveFailures += 1;
677
+ if (!warnedDisconnected) {
678
+ const detail = error instanceof Error ? ` ${error.message}` : "";
679
+ console.error(`Daemon disconnected. Waiting for recovery...${detail}`);
680
+ warnedDisconnected = true;
681
+ }
682
+ }
683
+ const delayMs = getFollowReadDelayMs(warnedDisconnected, consecutiveFailures);
684
+ await new Promise((resolve3) => setTimeout(resolve3, delayMs));
1036
685
  }
1037
- if (info.status === "error") {
1038
- done({
1039
- ok: false,
1040
- reason: info.lastError ? `bridge reported startup error: ${info.lastError}` : "bridge reported startup error"
1041
- });
686
+ } else {
687
+ const response = await ipcCall(socketPath, {
688
+ method: "read",
689
+ params: { channel: readChannel }
690
+ });
691
+ if (!response.ok) {
692
+ failCli(`Failed: ${response.error}`);
1042
693
  }
1043
- }, 120);
1044
- const timeout = setTimeout(() => {
1045
- const reason = lastError && lastError.length > 0 ? `timed out after ${timeoutMs}ms waiting for bridge readiness (last error: ${lastError})` : `timed out after ${timeoutMs}ms waiting for bridge readiness (state: ${lastState || "unknown"})`;
1046
- done({ ok: false, reason });
1047
- }, timeoutMs);
694
+ console.log(JSON.stringify(response.messages || [], null, 2));
695
+ }
1048
696
  });
1049
697
  }
1050
-
1051
- // src/commands/tunnel/management-commands.ts
1052
- function registerTunnelManagementCommands(tunnel) {
1053
- tunnel.command("channels").description("List active channels").argument("[tunnelId]", "Tunnel ID").option("-t, --tunnel <tunnelId>", "Tunnel ID (alternative to positional arg)").action(async (tunnelIdArg, opts) => {
1054
- const tunnelId = resolveTunnelIdSelection(tunnelIdArg, opts.tunnel) || await resolveActiveTunnel();
1055
- const socketPath = getSocketPath(tunnelId);
698
+ function registerChannelsCommand(program2) {
699
+ program2.command("channels").description("List active live channels").action(async () => {
700
+ const socketPath = getAgentSocketPath();
1056
701
  const response = await ipcCall(socketPath, { method: "channels", params: {} });
1057
702
  if (response.channels) {
1058
703
  for (const ch of response.channels) {
@@ -1060,54 +705,17 @@ function registerTunnelManagementCommands(tunnel) {
1060
705
  }
1061
706
  }
1062
707
  });
1063
- tunnel.command("status").description("Check tunnel connection status").argument("[tunnelId]", "Tunnel ID").option("-t, --tunnel <tunnelId>", "Tunnel ID (alternative to positional arg)").action(async (tunnelIdArg, opts) => {
1064
- const tunnelId = resolveTunnelIdSelection(tunnelIdArg, opts.tunnel) || await resolveActiveTunnel();
1065
- const socketPath = getSocketPath(tunnelId);
1066
- const response = await ipcCall(socketPath, { method: "status", params: {} });
1067
- console.log(` Status: ${response.connected ? "connected" : "waiting"}`);
1068
- console.log(` Uptime: ${response.uptime}s`);
1069
- const chNames = Array.isArray(response.channels) ? response.channels.map((c) => typeof c === "string" ? c : String(c)) : [];
1070
- console.log(` Channels: ${chNames.join(", ")}`);
1071
- console.log(` Buffered: ${response.bufferedMessages ?? 0} messages`);
1072
- if (typeof response.lastError === "string" && response.lastError.length > 0) {
1073
- console.log(` Last error: ${response.lastError}`);
1074
- }
1075
- const logPath = tunnelLogPath(tunnelId);
1076
- if (fs4.existsSync(logPath)) {
1077
- console.log(` Log: ${logPath}`);
1078
- }
1079
- const bridgeInfo = readBridgeProcessInfo(tunnelId);
1080
- if (bridgeInfo) {
1081
- const bridgeRunning = isBridgeRunning(tunnelId);
1082
- const bridgeState = bridgeInfo.status || (bridgeRunning ? "running" : "stopped");
1083
- console.log(` Bridge: ${bridgeInfo.mode} (${bridgeState})`);
1084
- if (bridgeInfo.sessionId) {
1085
- console.log(` Bridge session: ${bridgeInfo.sessionId}`);
1086
- }
1087
- if (bridgeInfo.sessionSource) {
1088
- console.log(` Bridge session source: ${bridgeInfo.sessionSource}`);
1089
- }
1090
- if (bridgeInfo.sessionKey) {
1091
- console.log(` Bridge session key: ${bridgeInfo.sessionKey}`);
1092
- }
1093
- if (bridgeInfo.lastError) {
1094
- console.log(` Bridge last error: ${bridgeInfo.lastError}`);
1095
- }
1096
- }
1097
- const bridgeLog = bridgeLogPath(tunnelId);
1098
- if (fs4.existsSync(bridgeLog)) {
1099
- console.log(` Bridge log: ${bridgeLog}`);
1100
- }
1101
- });
1102
- tunnel.command("doctor").description("Run strict end-to-end tunnel checks (daemon, channels, chat/canvas ping)").option("-t, --tunnel <tunnelId>", "Tunnel ID (auto-detected if one active)").option("--timeout <seconds>", "Timeout for pong wait and repeated reads", "30").option("--wait-pong", "Wait for user to reply with exact text 'pong' on chat channel").option("--skip-chat", "Skip chat ping check").option("--skip-canvas", "Skip canvas ping check").action(
708
+ }
709
+ function registerDoctorCommand(program2) {
710
+ program2.command("doctor").description("Run end-to-end live checks (daemon, channels, chat/canvas ping)").option("--timeout <seconds>", "Timeout for pong wait and repeated reads", "30").option("--wait-pong", "Wait for user to reply with exact text 'pong' on chat channel").option("--skip-chat", "Skip chat ping check").option("--skip-canvas", "Skip canvas ping check").action(
1103
711
  async (opts) => {
1104
712
  const timeoutSeconds = parsePositiveIntegerOption(opts.timeout, "--timeout");
1105
713
  const timeoutMs = timeoutSeconds * 1e3;
1106
- const tunnelId = opts.tunnel || await resolveActiveTunnel();
1107
- const socketPath = getSocketPath(tunnelId);
714
+ const socketPath = getAgentSocketPath();
715
+ const slug = await resolveActiveSlug();
1108
716
  const apiClient = createApiClient();
1109
717
  const fail = (message) => failCli(`Doctor failed: ${message}`);
1110
- console.log(`Doctor tunnel: ${tunnelId}`);
718
+ console.log(`Doctor: ${slug}`);
1111
719
  let statusResponse = null;
1112
720
  try {
1113
721
  statusResponse = await ipcCall(socketPath, {
@@ -1136,23 +744,25 @@ function registerTunnelManagementCommands(tunnel) {
1136
744
  }
1137
745
  }
1138
746
  console.log("Daemon/channel check: OK");
1139
- let apiTunnel;
1140
- try {
1141
- apiTunnel = await apiClient.get(tunnelId);
1142
- } catch (error) {
1143
- fail(`failed to fetch tunnel info from API: ${formatApiError(error)}`);
1144
- }
1145
- if (apiTunnel.status !== "active") {
1146
- fail(`API reports tunnel is not active (status: ${apiTunnel.status})`);
747
+ const live = await (async () => {
748
+ try {
749
+ return await apiClient.getLive(slug);
750
+ } catch (error) {
751
+ fail(`failed to fetch live info from API: ${formatApiError(error)}`);
752
+ }
753
+ throw new Error("unreachable");
754
+ })();
755
+ if (live.status !== "active") {
756
+ fail(`API reports live is not active (status: ${live.status})`);
1147
757
  }
1148
- if (apiTunnel.expiresAt <= Date.now()) {
1149
- fail("API reports tunnel is expired.");
758
+ if (live.expiresAt <= Date.now()) {
759
+ fail("API reports live is expired.");
1150
760
  }
1151
- if (!apiTunnel.hasConnection) {
1152
- fail("API reports no browser connection.");
761
+ if (typeof live.browserOffer !== "string" || live.browserOffer.length === 0) {
762
+ fail("browser offer was not published.");
1153
763
  }
1154
- if (typeof apiTunnel.agentOffer !== "string" || apiTunnel.agentOffer.length === 0) {
1155
- fail("agent offer was not published.");
764
+ if (typeof live.agentAnswer !== "string" || live.agentAnswer.length === 0) {
765
+ fail("agent answer was not published.");
1156
766
  }
1157
767
  console.log("API/signaling check: OK");
1158
768
  if (!opts.skipChat) {
@@ -1214,545 +824,127 @@ function registerTunnelManagementCommands(tunnel) {
1214
824
  }
1215
825
  console.log("Canvas ping write ACK: OK");
1216
826
  }
1217
- console.log("Tunnel doctor: PASS");
827
+ console.log("Doctor: PASS");
1218
828
  }
1219
829
  );
1220
- tunnel.command("list").description("List active tunnels").action(async () => {
1221
- const apiClient = createApiClient();
1222
- const tunnels = await apiClient.list();
1223
- if (tunnels.length === 0) {
1224
- console.log("No active tunnels.");
1225
- return;
1226
- }
1227
- for (const t of tunnels) {
1228
- const age = Math.floor((Date.now() - t.createdAt) / 6e4);
1229
- const running = isDaemonRunning(t.tunnelId) ? "running" : "no daemon";
1230
- const bridgeInfo = readBridgeProcessInfo(t.tunnelId);
1231
- const bridge = bridgeInfo ? isBridgeRunning(t.tunnelId) ? `${bridgeInfo.mode}:running` : `${bridgeInfo.mode}:stopped` : "none";
1232
- const conn = t.hasConnection ? "connected" : "waiting";
1233
- console.log(` ${t.tunnelId} ${conn} ${running} bridge=${bridge} ${age}m ago`);
1234
- }
1235
- });
1236
- tunnel.command("close").description("Close a tunnel and stop its daemon").argument("<tunnelId>", "Tunnel ID").action(async (tunnelId) => {
1237
- stopBridgeProcess(tunnelId);
1238
- try {
1239
- fs4.unlinkSync(bridgeInfoPath(tunnelId));
1240
- } catch {
1241
- }
1242
- const socketPath = getSocketPath(tunnelId);
1243
- try {
1244
- await ipcCall(socketPath, { method: "close", params: {} });
1245
- } catch {
1246
- }
1247
- const apiClient = createApiClient();
1248
- try {
1249
- await apiClient.close(tunnelId);
1250
- } catch (error) {
1251
- const message = formatApiError(error);
1252
- if (!/Tunnel not found/i.test(message)) {
1253
- failCli(`Failed to close tunnel ${tunnelId}: ${message}`);
1254
- }
1255
- }
1256
- console.log(`Closed: ${tunnelId}`);
1257
- });
1258
830
  }
1259
831
 
1260
- // src/commands/tunnel/message-commands.ts
1261
- import * as fs5 from "fs";
1262
- import * as path4 from "path";
1263
- function registerTunnelMessageCommands(tunnel) {
1264
- tunnel.command("write").description("Write data to a channel").argument("[message]", "Text message (or use --file)").option("-t, --tunnel <tunnelId>", "Tunnel ID (auto-detected if one active)").option("-c, --channel <channel>", "Channel name", "chat").option("-f, --file <file>", "Read content from file").action(
1265
- async (messageArg, opts) => {
1266
- let msg;
1267
- let binaryBase64;
1268
- if (opts.file) {
1269
- const filePath = path4.resolve(opts.file);
1270
- const ext = path4.extname(filePath).toLowerCase();
1271
- const bytes = fs5.readFileSync(filePath);
1272
- const filename = path4.basename(filePath);
1273
- if (ext === ".html" || ext === ".htm") {
1274
- msg = {
1275
- id: generateMessageId(),
1276
- type: "html",
1277
- data: bytes.toString("utf-8"),
1278
- meta: { title: filename, filename, mime: getMimeType(filePath), size: bytes.length }
1279
- };
1280
- } else if (TEXT_FILE_EXTENSIONS.has(ext)) {
1281
- msg = {
1282
- id: generateMessageId(),
1283
- type: "text",
1284
- data: bytes.toString("utf-8"),
1285
- meta: { filename, mime: getMimeType(filePath), size: bytes.length }
1286
- };
1287
- } else {
1288
- msg = {
1289
- id: generateMessageId(),
1290
- type: "binary",
1291
- meta: { filename, mime: getMimeType(filePath), size: bytes.length }
1292
- };
1293
- binaryBase64 = bytes.toString("base64");
1294
- }
1295
- } else if (messageArg) {
1296
- msg = {
1297
- id: generateMessageId(),
1298
- type: "text",
1299
- data: messageArg
1300
- };
832
+ // src/commands/pubs.ts
833
+ function registerPubCommands(program2) {
834
+ program2.command("create").description("Create a new pub").argument("[file]", "Path to the file (reads stdin if omitted)").option("--slug <slug>", "Custom slug for the URL").option("--title <title>", "Title for the pub").option("--public", "Make the pub public").option("--private", "Make the pub private (default)").option("--expires <duration>", "Auto-delete after duration (e.g. 1h, 24h, 7d)").action(
835
+ async (fileArg, opts) => {
836
+ const client = createClient();
837
+ let content;
838
+ let filename;
839
+ if (fileArg) {
840
+ const file = readFile(fileArg);
841
+ content = file.content;
842
+ filename = file.basename;
1301
843
  } else {
1302
- const chunks = [];
1303
- for await (const chunk of process.stdin) chunks.push(chunk);
1304
- msg = {
1305
- id: generateMessageId(),
1306
- type: "text",
1307
- data: Buffer.concat(chunks).toString("utf-8").trim()
1308
- };
844
+ content = await readFromStdin();
1309
845
  }
1310
- const tunnelId = opts.tunnel || await resolveActiveTunnel();
1311
- const socketPath = getSocketPath(tunnelId);
1312
- const response = await ipcCall(socketPath, {
1313
- method: "write",
1314
- params: { channel: opts.channel, msg, binaryBase64 }
846
+ const resolvedVisibility = resolveVisibilityFlags({
847
+ public: opts.public,
848
+ private: opts.private,
849
+ commandName: "create"
1315
850
  });
1316
- if (!response.ok) {
1317
- failCli(`Failed: ${response.error}`);
851
+ const result = await client.create({
852
+ content,
853
+ filename,
854
+ title: opts.title,
855
+ slug: opts.slug,
856
+ isPublic: resolvedVisibility ?? false,
857
+ expiresIn: opts.expires
858
+ });
859
+ console.log(`Created: ${result.url}`);
860
+ const tmaUrl = getTelegramMiniAppUrl(result.slug);
861
+ if (tmaUrl) console.log(`Telegram: ${tmaUrl}`);
862
+ if (result.expiresAt) {
863
+ console.log(` Expires: ${new Date(result.expiresAt).toISOString()}`);
1318
864
  }
1319
865
  }
1320
866
  );
1321
- tunnel.command("read").description("Read buffered messages from channels").argument("[tunnelId]", "Tunnel ID (auto-detected if one active)").option("-t, --tunnel <tunnelId>", "Tunnel ID (alternative to positional arg)").option("-c, --channel <channel>", "Filter by channel").option("--follow", "Stream messages continuously").option("--all", "With --follow, include all channels instead of chat-only default").action(
1322
- async (tunnelIdArg, opts) => {
1323
- const tunnelId = resolveTunnelIdSelection(tunnelIdArg, opts.tunnel) || await resolveActiveTunnel();
1324
- const socketPath = getSocketPath(tunnelId);
1325
- const readChannel = opts.channel || (opts.follow && !opts.all ? CHANNELS.CHAT : void 0);
1326
- if (opts.follow) {
1327
- if (!opts.channel && !opts.all) {
1328
- console.error(
1329
- "Following chat channel by default. Use `--all` to include binary/file channels."
1330
- );
1331
- }
1332
- let consecutiveFailures = 0;
1333
- let warnedDisconnected = false;
1334
- while (true) {
1335
- try {
1336
- const response = await ipcCall(socketPath, {
1337
- method: "read",
1338
- params: { channel: readChannel }
1339
- });
1340
- if (warnedDisconnected) {
1341
- console.error("Daemon reconnected.");
1342
- warnedDisconnected = false;
1343
- }
1344
- consecutiveFailures = 0;
1345
- if (response.messages && response.messages.length > 0) {
1346
- for (const m of response.messages) {
1347
- console.log(JSON.stringify(m));
1348
- }
1349
- }
1350
- } catch (error) {
1351
- consecutiveFailures += 1;
1352
- if (!warnedDisconnected) {
1353
- const detail = error instanceof Error ? ` ${error.message}` : "";
1354
- console.error(`Daemon disconnected. Waiting for recovery...${detail}`);
1355
- warnedDisconnected = true;
1356
- }
1357
- }
1358
- const delayMs = getFollowReadDelayMs(warnedDisconnected, consecutiveFailures);
1359
- await new Promise((resolve3) => setTimeout(resolve3, delayMs));
1360
- }
1361
- } else {
1362
- const response = await ipcCall(socketPath, {
1363
- method: "read",
1364
- params: { channel: readChannel }
1365
- });
1366
- if (!response.ok) {
1367
- failCli(`Failed: ${response.error}`);
1368
- }
1369
- console.log(JSON.stringify(response.messages || [], null, 2));
1370
- }
867
+ program2.command("get").description("Get details of a pub").argument("<slug>", "Slug of the pub").option("--content", "Output raw content to stdout (no metadata, pipeable)").action(async (slug, opts) => {
868
+ const client = createClient();
869
+ const pub = await client.get(slug);
870
+ if (opts.content) {
871
+ process.stdout.write(pub.content ?? "");
872
+ return;
1371
873
  }
1372
- );
1373
- }
1374
-
1375
- // src/commands/tunnel/start-command.ts
1376
- import { fork as fork2 } from "child_process";
1377
- import * as fs6 from "fs";
1378
- import * as path5 from "path";
1379
-
1380
- // package.json
1381
- var package_default = {
1382
- name: "pubblue",
1383
- version: "0.4.12",
1384
- description: "CLI tool for publishing static content via pub.blue",
1385
- type: "module",
1386
- bin: {
1387
- pubblue: "./dist/index.js"
1388
- },
1389
- scripts: {
1390
- build: "tsup src/index.ts src/tunnel-daemon-entry.ts src/tunnel-bridge-entry.ts --format esm --dts --clean",
1391
- dev: "tsup src/index.ts src/tunnel-daemon-entry.ts src/tunnel-bridge-entry.ts --format esm --watch",
1392
- test: "vitest run",
1393
- "test:watch": "vitest",
1394
- lint: "tsc --noEmit"
1395
- },
1396
- dependencies: {
1397
- commander: "^13.0.0",
1398
- "node-datachannel": "^0.32.0"
1399
- },
1400
- devDependencies: {
1401
- "@types/node": "22.10.2",
1402
- tsup: "^8.3.6",
1403
- typescript: "^5.7.2",
1404
- vitest: "^3.0.0"
1405
- },
1406
- files: [
1407
- "dist"
1408
- ],
1409
- repository: {
1410
- type: "git",
1411
- url: "git+https://github.com/xmanatee/pub.git",
1412
- directory: "cli"
1413
- },
1414
- publishConfig: {
1415
- access: "public"
1416
- },
1417
- pnpm: {
1418
- onlyBuiltDependencies: [
1419
- "esbuild",
1420
- "node-datachannel"
1421
- ]
1422
- }
1423
- };
1424
-
1425
- // src/lib/version.ts
1426
- var version = package_default.version;
1427
- if (typeof version !== "string" || version.length === 0) {
1428
- throw new Error("Invalid CLI version in package.json");
1429
- }
1430
- var CLI_VERSION = version;
1431
-
1432
- // src/commands/tunnel/start-command.ts
1433
- async function waitForStopped(isRunning, timeoutMs, pollMs = 120) {
1434
- const started = Date.now();
1435
- while (Date.now() - started < timeoutMs) {
1436
- if (!isRunning()) return true;
1437
- await new Promise((resolve3) => setTimeout(resolve3, pollMs));
1438
- }
1439
- return !isRunning();
1440
- }
1441
- function registerTunnelStartCommand(tunnel) {
1442
- tunnel.command("start").description("Start a tunnel daemon (reuses existing tunnel when possible)").option("--expires <duration>", "Auto-close after duration (e.g. 4h, 1d)", "24h").option("-t, --tunnel <tunnelId>", "Attach/start daemon for an existing tunnel").option("--new", "Always create a new tunnel (skip single-tunnel reuse)").option("--bridge <mode>", "Bridge mode: openclaw|none").option("--foreground", "Run in foreground (don't fork, no managed bridge)").action(
1443
- async (opts) => {
1444
- await ensureNodeDatachannelAvailable();
1445
- const runtimeConfig = getConfig();
1446
- const apiClient = createApiClient(runtimeConfig);
1447
- let target = null;
1448
- const bridgeMode = parseBridgeMode(opts.bridge || runtimeConfig.bridge?.mode || "openclaw");
1449
- const bridgeProcessEnv = buildBridgeProcessEnv(runtimeConfig.bridge);
1450
- if (opts.tunnel) {
1451
- try {
1452
- const existing = await apiClient.get(opts.tunnel);
1453
- if (existing.status === "closed" || existing.expiresAt <= Date.now()) {
1454
- failCli(`Tunnel ${opts.tunnel} is closed or expired.`);
1455
- }
1456
- target = {
1457
- createdNew: false,
1458
- expiresAt: existing.expiresAt,
1459
- mode: "existing",
1460
- tunnelId: existing.tunnelId,
1461
- url: getPublicTunnelUrl(existing.tunnelId)
1462
- };
1463
- } catch (error) {
1464
- failCli(`Failed to use tunnel ${opts.tunnel}: ${formatApiError(error)}`);
1465
- }
1466
- } else if (!opts.new) {
1467
- try {
1468
- const listed = await apiClient.list();
1469
- const active = listed.filter((t) => t.status === "active" && t.expiresAt > Date.now()).sort((a, b) => b.createdAt - a.createdAt);
1470
- const reusable = pickReusableTunnel(listed);
1471
- if (reusable) {
1472
- target = {
1473
- createdNew: false,
1474
- expiresAt: reusable.expiresAt,
1475
- mode: "existing",
1476
- tunnelId: reusable.tunnelId,
1477
- url: getPublicTunnelUrl(reusable.tunnelId)
1478
- };
1479
- if (active.length > 1) {
1480
- console.error(
1481
- [
1482
- `Multiple active tunnels found: ${active.map((t) => t.tunnelId).join(", ")}`,
1483
- `Reusing most recent active tunnel ${reusable.tunnelId}.`,
1484
- "Use --tunnel <id> to choose explicitly or --new to force creation."
1485
- ].join("\n")
1486
- );
1487
- } else {
1488
- console.error(
1489
- `Reusing existing active tunnel ${reusable.tunnelId}. Use --new to force creation.`
1490
- );
1491
- }
1492
- }
1493
- } catch (error) {
1494
- failCli(`Failed to list tunnels for reuse check: ${formatApiError(error)}`);
1495
- }
1496
- }
1497
- if (!target) {
1498
- try {
1499
- const created = await apiClient.create({
1500
- expiresIn: opts.expires
1501
- });
1502
- target = {
1503
- createdNew: true,
1504
- expiresAt: created.expiresAt,
1505
- mode: "created",
1506
- tunnelId: created.tunnelId,
1507
- url: created.url
1508
- };
1509
- } catch (error) {
1510
- failCli(`Failed to create tunnel: ${formatApiError(error)}`);
1511
- }
1512
- }
1513
- if (!target) {
1514
- failCli("Failed to resolve tunnel target.");
1515
- }
1516
- const socketPath = getSocketPath(target.tunnelId);
1517
- const infoPath = tunnelInfoPath(target.tunnelId);
1518
- const logPath = tunnelLogPath(target.tunnelId);
1519
- if (opts.foreground) {
1520
- if (bridgeMode !== "none") {
1521
- throw new Error(
1522
- "Foreground mode disables managed bridge process. Use background mode for --bridge openclaw."
1523
- );
1524
- }
1525
- const { startDaemon } = await import("./tunnel-daemon-RKWEA5BV.js");
1526
- console.log(`Tunnel started: ${target.url}`);
1527
- const fgTma = getTelegramMiniAppUrl("tunnel", target.tunnelId);
1528
- if (fgTma) console.log(`Telegram: ${fgTma}`);
1529
- console.log(`Tunnel ID: ${target.tunnelId}`);
1530
- console.log(`Expires: ${new Date(target.expiresAt).toISOString()}`);
1531
- if (target.mode === "existing") console.log("Mode: attached existing tunnel");
1532
- console.log("Running in foreground. Press Ctrl+C to stop.");
1533
- try {
1534
- await startDaemon({
1535
- cliVersion: CLI_VERSION,
1536
- tunnelId: target.tunnelId,
1537
- apiClient,
1538
- socketPath,
1539
- infoPath
1540
- });
1541
- } catch (error) {
1542
- const message = error instanceof Error ? error.message : String(error);
1543
- failCli(`Daemon failed: ${message}`);
1544
- }
1545
- return;
1546
- }
1547
- const runningDaemonInfo = readDaemonProcessInfo(target.tunnelId);
1548
- if (runningDaemonInfo) {
1549
- const daemonVersion = runningDaemonInfo.cliVersion;
1550
- const shouldRestartForUpgrade = shouldRestartDaemonForCliUpgrade(
1551
- daemonVersion,
1552
- CLI_VERSION
1553
- );
1554
- if (shouldRestartForUpgrade) {
1555
- console.error(
1556
- `Restarting daemon for CLI version ${CLI_VERSION} (running: ${daemonVersion || "unknown"}).`
1557
- );
1558
- if (isBridgeRunning(target.tunnelId)) {
1559
- stopBridgeProcess(target.tunnelId);
1560
- const bridgeStopped = await waitForStopped(
1561
- () => isBridgeRunning(target.tunnelId),
1562
- 5e3
1563
- );
1564
- if (!bridgeStopped) {
1565
- failCli("Bridge process did not stop during daemon upgrade restart.");
1566
- }
1567
- }
1568
- try {
1569
- await ipcCall(socketPath, { method: "close", params: {} });
1570
- } catch (error) {
1571
- failCli(
1572
- [
1573
- `Failed to stop running daemon for upgrade: ${error instanceof Error ? error.message : String(error)}`,
1574
- "Run `pubblue tunnel close <id>` and retry."
1575
- ].join("\n")
1576
- );
1577
- }
1578
- const daemonStopped = await waitForStopped(
1579
- () => isDaemonRunning(target.tunnelId),
1580
- 6e3
1581
- );
1582
- if (!daemonStopped) {
1583
- failCli("Daemon did not stop in time during upgrade restart.");
1584
- }
1585
- } else {
1586
- try {
1587
- const status = await ipcCall(socketPath, { method: "status", params: {} });
1588
- if (!status.ok) throw new Error(String(status.error || "status check failed"));
1589
- } catch (error) {
1590
- failCli(
1591
- [
1592
- `Daemon process exists but is not responding: ${error instanceof Error ? error.message : String(error)}`,
1593
- "Run `pubblue tunnel close <id>` and start again."
1594
- ].join("\n")
1595
- );
1596
- }
1597
- if (bridgeMode !== "none") {
1598
- const bridgeReady = await ensureBridgeReady({
1599
- bridgeMode,
1600
- tunnelId: target.tunnelId,
1601
- socketPath,
1602
- bridgeProcessEnv,
1603
- timeoutMs: 8e3
1604
- });
1605
- if (!bridgeReady.ok) {
1606
- const lines = [
1607
- `Bridge failed to start for running tunnel: ${bridgeReady.reason ?? "unknown reason"}`
1608
- ];
1609
- const existingBridgeLog = bridgeLogPath(target.tunnelId);
1610
- if (fs6.existsSync(existingBridgeLog)) {
1611
- lines.push(`Bridge log: ${existingBridgeLog}`);
1612
- const bridgeTail = readLogTail(existingBridgeLog);
1613
- if (bridgeTail) {
1614
- lines.push("---- bridge log tail ----");
1615
- lines.push(bridgeTail.trimEnd());
1616
- lines.push("---- end bridge log tail ----");
1617
- }
1618
- }
1619
- failCli(lines.join("\n"));
1620
- }
1621
- }
1622
- console.log(`Tunnel started: ${target.url}`);
1623
- const runTma = getTelegramMiniAppUrl("tunnel", target.tunnelId);
1624
- if (runTma) console.log(`Telegram: ${runTma}`);
1625
- console.log(`Tunnel ID: ${target.tunnelId}`);
1626
- console.log(`Expires: ${new Date(target.expiresAt).toISOString()}`);
1627
- console.log("Daemon already running for this tunnel.");
1628
- console.log(`Daemon log: ${logPath}`);
1629
- if (bridgeMode !== "none") {
1630
- console.log("Bridge mode: openclaw");
1631
- console.log(`Bridge log: ${bridgeLogPath(target.tunnelId)}`);
1632
- }
1633
- return;
1634
- }
1635
- }
1636
- const daemonScript = path5.join(import.meta.dirname, "tunnel-daemon-entry.js");
1637
- const daemonLogFd = fs6.openSync(logPath, "a");
1638
- const child = fork2(daemonScript, [], {
1639
- detached: true,
1640
- stdio: buildDaemonForkStdio(daemonLogFd),
1641
- env: {
1642
- ...process.env,
1643
- PUBBLUE_DAEMON_TUNNEL_ID: target.tunnelId,
1644
- PUBBLUE_DAEMON_BASE_URL: runtimeConfig.baseUrl,
1645
- PUBBLUE_DAEMON_API_KEY: runtimeConfig.apiKey,
1646
- PUBBLUE_DAEMON_SOCKET: socketPath,
1647
- PUBBLUE_DAEMON_INFO: infoPath,
1648
- PUBBLUE_CLI_VERSION: CLI_VERSION
1649
- }
1650
- });
1651
- fs6.closeSync(daemonLogFd);
1652
- if (child.connected) {
1653
- child.disconnect();
874
+ console.log(` Slug: ${pub.slug}`);
875
+ if (pub.contentType) console.log(` Type: ${pub.contentType}`);
876
+ if (pub.title) console.log(` Title: ${pub.title}`);
877
+ console.log(` Status: ${formatVisibility(pub.isPublic)}`);
878
+ if (pub.expiresAt) console.log(` Expires: ${new Date(pub.expiresAt).toISOString()}`);
879
+ console.log(` Created: ${new Date(pub.createdAt).toLocaleDateString()}`);
880
+ console.log(` Updated: ${new Date(pub.updatedAt).toLocaleDateString()}`);
881
+ if (pub.content) console.log(` Size: ${pub.content.length} bytes`);
882
+ if (pub.live) {
883
+ console.log(` Live: ${pub.live.status}`);
884
+ console.log(` Connected: ${pub.live.hasConnection ? "yes" : "no"}`);
885
+ console.log(` Expires: ${new Date(pub.live.expiresAt).toISOString()}`);
886
+ }
887
+ });
888
+ program2.command("update").description("Update a pub's content and/or metadata").argument("<slug>", "Slug of the pub to update").option("--file <file>", "New content from file").option("--title <title>", "New title").option("--public", "Make the pub public").option("--private", "Make the pub private").option("--slug <newSlug>", "Rename the slug").action(
889
+ async (slug, opts) => {
890
+ const client = createClient();
891
+ let content;
892
+ let filename;
893
+ if (opts.file) {
894
+ const file = readFile(opts.file);
895
+ content = file.content;
896
+ filename = file.basename;
1654
897
  }
1655
- child.unref();
1656
- console.log(`Starting daemon for tunnel ${target.tunnelId}...`);
1657
- const ready = await waitForDaemonReady({
1658
- child,
1659
- infoPath,
1660
- socketPath,
1661
- timeoutMs: 8e3
898
+ const isPublic = resolveVisibilityFlags({
899
+ public: opts.public,
900
+ private: opts.private,
901
+ commandName: "update"
1662
902
  });
1663
- if (!ready.ok) {
1664
- const lines = [
1665
- `Daemon failed to start: ${ready.reason ?? "unknown reason"}`,
1666
- `Daemon log: ${logPath}`
1667
- ];
1668
- const tail = readLogTail(logPath);
1669
- if (tail) {
1670
- lines.push("---- daemon log tail ----");
1671
- lines.push(tail.trimEnd());
1672
- lines.push("---- end daemon log tail ----");
1673
- }
1674
- await cleanupCreatedTunnelOnStartFailure(apiClient, target);
1675
- failCli(lines.join("\n"));
1676
- }
1677
- const offerReady = await waitForAgentOffer({
1678
- apiClient,
1679
- tunnelId: target.tunnelId,
1680
- timeoutMs: 5e3
903
+ const result = await client.update({
904
+ slug,
905
+ content,
906
+ filename,
907
+ title: opts.title,
908
+ isPublic,
909
+ newSlug: opts.slug
1681
910
  });
1682
- if (!offerReady.ok) {
1683
- const lines = [
1684
- `Daemon started but signaling is not ready: ${offerReady.reason}`,
1685
- `Daemon log: ${logPath}`
1686
- ];
1687
- const tail = readLogTail(logPath);
1688
- if (tail) {
1689
- lines.push("---- daemon log tail ----");
1690
- lines.push(tail.trimEnd());
1691
- lines.push("---- end daemon log tail ----");
1692
- }
1693
- await cleanupCreatedTunnelOnStartFailure(apiClient, target);
1694
- failCli(lines.join("\n"));
1695
- }
1696
- if (bridgeMode !== "none") {
1697
- const bridgeReady = await ensureBridgeReady({
1698
- bridgeMode,
1699
- tunnelId: target.tunnelId,
1700
- socketPath,
1701
- bridgeProcessEnv,
1702
- timeoutMs: 8e3
1703
- });
1704
- if (!bridgeReady.ok) {
1705
- const lines = [`Bridge failed to start: ${bridgeReady.reason ?? "unknown reason"}`];
1706
- const bridgeLog = bridgeLogPath(target.tunnelId);
1707
- if (fs6.existsSync(bridgeLog)) {
1708
- lines.push(`Bridge log: ${bridgeLog}`);
1709
- const bridgeTail = readLogTail(bridgeLog);
1710
- if (bridgeTail) {
1711
- lines.push("---- bridge log tail ----");
1712
- lines.push(bridgeTail.trimEnd());
1713
- lines.push("---- end bridge log tail ----");
1714
- }
1715
- }
1716
- try {
1717
- await ipcCall(socketPath, { method: "close", params: {} });
1718
- } catch {
1719
- }
1720
- await cleanupCreatedTunnelOnStartFailure(apiClient, target);
1721
- failCli(lines.join("\n"));
1722
- }
1723
- }
1724
- console.log(`Tunnel started: ${target.url}`);
1725
- const tma = getTelegramMiniAppUrl("tunnel", target.tunnelId);
1726
- if (tma) console.log(`Telegram: ${tma}`);
1727
- console.log(`Tunnel ID: ${target.tunnelId}`);
1728
- console.log(`Expires: ${new Date(target.expiresAt).toISOString()}`);
1729
- if (target.mode === "existing") console.log("Mode: attached existing tunnel");
1730
- console.log("Daemon health: OK");
1731
- console.log(`Daemon log: ${logPath}`);
1732
- if (bridgeMode !== "none") {
1733
- console.log("Bridge mode: openclaw");
1734
- console.log(`Bridge log: ${bridgeLogPath(target.tunnelId)}`);
1735
- }
911
+ console.log(`Updated: ${result.slug}`);
912
+ if (result.title) console.log(` Title: ${result.title}`);
913
+ console.log(` Status: ${formatVisibility(result.isPublic)}`);
1736
914
  }
1737
915
  );
1738
- }
1739
-
1740
- // src/commands/tunnel.ts
1741
- function registerTunnelCommands(program2) {
1742
- const tunnel = program2.command("tunnel").description("P2P encrypted tunnel to browser");
1743
- registerTunnelStartCommand(tunnel);
1744
- registerTunnelMessageCommands(tunnel);
1745
- registerTunnelManagementCommands(tunnel);
916
+ program2.command("list").description("List your pubs").action(async () => {
917
+ const client = createClient();
918
+ const pubs = await client.list();
919
+ if (pubs.length === 0) {
920
+ console.log("No pubs.");
921
+ return;
922
+ }
923
+ for (const pub of pubs) {
924
+ const date = new Date(pub.createdAt).toLocaleDateString();
925
+ const expires = pub.expiresAt ? ` expires:${new Date(pub.expiresAt).toISOString()}` : "";
926
+ const contentLabel = pub.contentType ? `[${pub.contentType}]` : "[no content]";
927
+ const sessionLabel = pub.live?.status === "active" ? " [live]" : "";
928
+ console.log(
929
+ ` ${pub.slug} ${contentLabel} ${formatVisibility(pub.isPublic)} ${date}${expires}${sessionLabel}`
930
+ );
931
+ }
932
+ });
933
+ program2.command("delete").description("Delete a pub").argument("<slug>", "Slug of the pub to delete").action(async (slug) => {
934
+ const client = createClient();
935
+ await client.remove(slug);
936
+ console.log(`Deleted: ${slug}`);
937
+ });
1746
938
  }
1747
939
 
1748
940
  // src/program.ts
1749
941
  function buildProgram() {
1750
942
  const program2 = new Command();
1751
943
  program2.exitOverride();
1752
- program2.name("pubblue").description("Publish static content and get shareable URLs").version(CLI_VERSION);
944
+ program2.name("pubblue").description("Publish content and go live").version(CLI_VERSION);
1753
945
  registerConfigureCommand(program2);
1754
- registerPublicationCommands(program2);
1755
- registerTunnelCommands(program2);
946
+ registerPubCommands(program2);
947
+ registerLiveCommands(program2);
1756
948
  return program2;
1757
949
  }
1758
950