pubblue 0.6.4 → 0.6.9

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
@@ -2,102 +2,37 @@
2
2
  import {
3
3
  CHANNELS,
4
4
  CONTROL_CHANNEL,
5
+ DEFAULT_BASE_URL,
5
6
  PubApiClient,
6
- TEXT_FILE_EXTENSIONS,
7
- buildBridgeProcessEnv,
8
- buildDaemonForkStdio,
9
- ensureNodeDatachannelAvailable,
7
+ PubApiError,
10
8
  errorMessage,
11
9
  failCli,
12
- formatApiError,
13
10
  generateMessageId,
14
- getAgentSocketPath,
15
- getFollowReadDelayMs,
16
- getMimeType,
17
- ipcCall,
18
- isDaemonRunning,
11
+ getConfig,
12
+ getConfigDir,
13
+ getTelegramMiniAppUrl,
14
+ isClaudeCodeAvailableInEnv,
15
+ isOpenClawAvailable,
16
+ liveInfoDir,
19
17
  liveInfoPath,
20
18
  liveLogPath,
21
- messageContainsPong,
22
- parsePositiveIntegerOption,
19
+ readConfig,
23
20
  readLogTail,
24
- resolveActiveSlug,
25
- resolveBridgeMode,
26
- stopOtherDaemons,
21
+ resolveOpenClawHome,
22
+ resolveOpenClawWorkspaceDir,
23
+ runClaudeCodeBridgeStartupProbe,
24
+ runOpenClawBridgeStartupProbe,
25
+ saveConfig,
27
26
  toCliFailure,
28
- waitForDaemonReady,
29
27
  writeLatestCliVersion
30
- } from "./chunk-JXEXE632.js";
28
+ } from "./chunk-JSX5KHV3.js";
31
29
 
32
30
  // src/program.ts
33
31
  import { Command } from "commander";
34
32
 
35
- // src/commands/configure.ts
36
- import { createInterface } from "readline/promises";
37
-
38
- // src/lib/config.ts
33
+ // src/commands/shared.ts
39
34
  import * as fs from "fs";
40
- import * as os from "os";
41
35
  import * as path from "path";
42
- var DEFAULT_BASE_URL = "https://silent-guanaco-514.convex.site";
43
- function getConfigDir(homeDir) {
44
- const home = homeDir || os.homedir();
45
- return path.join(home, ".config", "pubblue");
46
- }
47
- function getConfigPath(homeDir) {
48
- const dir = getConfigDir(homeDir);
49
- fs.mkdirSync(dir, { recursive: true, mode: 448 });
50
- try {
51
- fs.chmodSync(dir, 448);
52
- } catch {
53
- }
54
- return path.join(dir, "config.json");
55
- }
56
- function readConfig(homeDir) {
57
- const configPath = getConfigPath(homeDir);
58
- if (!fs.existsSync(configPath)) return null;
59
- const raw = fs.readFileSync(configPath, "utf-8");
60
- return JSON.parse(raw);
61
- }
62
- function saveConfig(config, homeDir) {
63
- const configPath = getConfigPath(homeDir);
64
- fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
65
- `, {
66
- mode: 384
67
- });
68
- try {
69
- fs.chmodSync(configPath, 384);
70
- } catch {
71
- }
72
- }
73
- function getConfig(homeDir) {
74
- const envKey = process.env.PUBBLUE_API_KEY;
75
- const envUrl = process.env.PUBBLUE_URL;
76
- const baseUrl = envUrl || DEFAULT_BASE_URL;
77
- const saved = readConfig(homeDir);
78
- if (envKey) {
79
- return { apiKey: envKey, baseUrl, bridge: saved?.bridge };
80
- }
81
- if (!saved) {
82
- throw new Error(
83
- "Not configured. Run `pubblue configure` or set PUBBLUE_API_KEY environment variable."
84
- );
85
- }
86
- return {
87
- apiKey: saved.apiKey,
88
- baseUrl,
89
- bridge: saved.bridge
90
- };
91
- }
92
- function getTelegramMiniAppUrl(slug) {
93
- const saved = readConfig();
94
- if (!saved?.telegram?.botUsername) return null;
95
- return `https://t.me/${saved.telegram.botUsername}?startapp=${slug}`;
96
- }
97
-
98
- // src/commands/shared.ts
99
- import * as fs2 from "fs";
100
- import * as path2 from "path";
101
36
  function createClient(configOverride) {
102
37
  const config = configOverride || getConfig();
103
38
  return new PubApiClient(config.baseUrl, config.apiKey);
@@ -121,49 +56,48 @@ function resolveVisibilityFlags(opts) {
121
56
  return void 0;
122
57
  }
123
58
  function readFile(filePath) {
124
- const resolved = path2.resolve(filePath);
125
- if (!fs2.existsSync(resolved)) {
59
+ const resolved = path.resolve(filePath);
60
+ if (!fs.existsSync(resolved)) {
126
61
  failCli(`File not found: ${resolved}`);
127
62
  }
128
63
  return {
129
- content: fs2.readFileSync(resolved, "utf-8"),
130
- basename: path2.basename(resolved)
64
+ content: fs.readFileSync(resolved, "utf-8"),
65
+ basename: path.basename(resolved)
131
66
  };
132
67
  }
133
68
 
134
- // src/commands/configure.ts
135
- function readApiKeyFromPrompt() {
136
- const rl = createInterface({
137
- input: process.stdin,
138
- output: process.stdout
139
- });
140
- return rl.question("Enter API key: ").then((answer) => answer.trim()).finally(() => {
141
- rl.close();
142
- });
143
- }
144
- async function resolveConfigureApiKey(opts) {
69
+ // src/commands/configure/io.ts
70
+ function resolveConfigureApiKey(opts) {
145
71
  if (opts.apiKey && opts.apiKeyStdin) {
146
72
  throw new Error("Use only one of --api-key or --api-key-stdin.");
147
73
  }
148
74
  if (opts.apiKey) {
149
- return opts.apiKey.trim();
75
+ return Promise.resolve(opts.apiKey.trim());
150
76
  }
151
77
  if (opts.apiKeyStdin) {
152
78
  return readFromStdin();
153
79
  }
154
80
  const envKey = process.env.PUBBLUE_API_KEY?.trim();
155
- if (envKey) return envKey;
156
- if (!process.stdin.isTTY || !process.stdout.isTTY) {
157
- throw new Error(
158
- "No TTY available. Provide --api-key, --api-key-stdin, or PUBBLUE_API_KEY for configure."
159
- );
160
- }
161
- return readApiKeyFromPrompt();
81
+ if (envKey) return Promise.resolve(envKey);
82
+ throw new Error(
83
+ "No API key provided. Use --api-key <KEY>, --api-key-stdin, or set PUBBLUE_API_KEY."
84
+ );
162
85
  }
163
86
  function collectValues(value, previous) {
164
87
  previous.push(value);
165
88
  return previous;
166
89
  }
90
+
91
+ // src/lib/number.ts
92
+ function parsePositiveInteger(raw, label) {
93
+ const parsed = Number.parseInt(raw, 10);
94
+ if (!Number.isFinite(parsed) || parsed <= 0) {
95
+ throw new Error(`${label} must be a positive integer. Received: ${raw}`);
96
+ }
97
+ return parsed;
98
+ }
99
+
100
+ // src/commands/configure/schema.ts
167
101
  function parseSetInput(raw) {
168
102
  const sepIndex = raw.indexOf("=");
169
103
  if (sepIndex <= 0 || sepIndex === raw.length - 1) {
@@ -182,119 +116,168 @@ function parseBooleanValue(raw, key) {
182
116
  return false;
183
117
  throw new Error(`Invalid boolean value for ${key}: ${raw}`);
184
118
  }
185
- function parsePositiveInteger(raw, key) {
186
- const parsed = Number.parseInt(raw, 10);
187
- if (!Number.isFinite(parsed) || parsed <= 0) {
188
- throw new Error(`${key} must be a positive integer. Received: ${raw}`);
119
+ var CONFIG_KEY_REGISTRY = {
120
+ "openclaw.path": { target: "bridge", field: "openclawPath", type: "string" },
121
+ "openclaw.stateDir": { target: "bridge", field: "openclawStateDir", type: "string" },
122
+ "openclaw.workspace": { target: "bridge", field: "openclawWorkspace", type: "string" },
123
+ "openclaw.sessionId": { target: "bridge", field: "sessionId", type: "string" },
124
+ "openclaw.threadId": { target: "bridge", field: "threadId", type: "string" },
125
+ "openclaw.canvasReminderEvery": {
126
+ target: "bridge",
127
+ field: "canvasReminderEvery",
128
+ type: "integer"
129
+ },
130
+ "openclaw.deliver": { target: "bridge", field: "deliver", type: "boolean" },
131
+ "openclaw.deliverChannel": { target: "bridge", field: "deliverChannel", type: "string" },
132
+ "openclaw.replyTo": { target: "bridge", field: "replyTo", type: "string" },
133
+ "openclaw.deliverTimeoutMs": { target: "bridge", field: "deliverTimeoutMs", type: "integer" },
134
+ "openclaw.attachmentDir": { target: "bridge", field: "attachmentDir", type: "string" },
135
+ "openclaw.attachmentMaxBytes": { target: "bridge", field: "attachmentMaxBytes", type: "integer" },
136
+ "claude-code.path": { target: "bridge", field: "claudeCodePath", type: "string" },
137
+ "claude-code.model": { target: "bridge", field: "claudeCodeModel", type: "string" },
138
+ "claude-code.allowedTools": { target: "bridge", field: "claudeCodeAllowedTools", type: "string" },
139
+ "claude-code.appendSystemPrompt": {
140
+ target: "bridge",
141
+ field: "claudeCodeAppendSystemPrompt",
142
+ type: "string",
143
+ displayAs: "set-only"
144
+ },
145
+ "claude-code.maxTurns": { target: "bridge", field: "claudeCodeMaxTurns", type: "integer" },
146
+ "claude-code.cwd": { target: "bridge", field: "claudeCodeCwd", type: "string" },
147
+ "command.defaultTimeoutMs": {
148
+ target: "bridge",
149
+ field: "commandDefaultTimeoutMs",
150
+ type: "integer"
151
+ },
152
+ "command.maxOutputBytes": {
153
+ target: "bridge",
154
+ field: "commandMaxOutputBytes",
155
+ type: "integer"
156
+ },
157
+ "command.maxConcurrent": {
158
+ target: "bridge",
159
+ field: "commandMaxConcurrent",
160
+ type: "integer"
161
+ },
162
+ "telegram.botToken": {
163
+ target: "telegram",
164
+ field: "botToken",
165
+ type: "string",
166
+ cascadeUnset: ["botUsername", "hasMainWebApp"]
189
167
  }
190
- return parsed;
168
+ };
169
+ var SUPPORTED_KEYS = Object.keys(CONFIG_KEY_REGISTRY);
170
+ function coerceValue(raw, type, key) {
171
+ if (type === "integer") return parsePositiveInteger(raw, key);
172
+ if (type === "boolean") return parseBooleanValue(raw, key);
173
+ return raw;
191
174
  }
192
- var SUPPORTED_KEYS = [
193
- "openclaw.path",
194
- "openclaw.sessionId",
195
- "openclaw.threadId",
196
- "openclaw.canvasReminderEvery",
197
- "openclaw.deliver",
198
- "openclaw.deliverChannel",
199
- "openclaw.replyTo",
200
- "openclaw.deliverTimeoutMs",
201
- "openclaw.attachmentDir",
202
- "openclaw.attachmentMaxBytes",
203
- "telegram.botToken"
204
- ];
205
175
  function applyConfigSet(bridge, telegram, key, value) {
206
- switch (key) {
207
- case "openclaw.path":
208
- bridge.openclawPath = value;
209
- return;
210
- case "openclaw.sessionId":
211
- bridge.sessionId = value;
212
- return;
213
- case "openclaw.threadId":
214
- bridge.threadId = value;
215
- return;
216
- case "openclaw.canvasReminderEvery":
217
- bridge.canvasReminderEvery = parsePositiveInteger(value, key);
218
- return;
219
- case "openclaw.deliver":
220
- bridge.deliver = parseBooleanValue(value, key);
221
- return;
222
- case "openclaw.deliverChannel":
223
- bridge.deliverChannel = value;
224
- return;
225
- case "openclaw.replyTo":
226
- bridge.replyTo = value;
227
- return;
228
- case "openclaw.deliverTimeoutMs":
229
- bridge.deliverTimeoutMs = parsePositiveInteger(value, key);
230
- return;
231
- case "openclaw.attachmentDir":
232
- bridge.attachmentDir = value;
233
- return;
234
- case "openclaw.attachmentMaxBytes":
235
- bridge.attachmentMaxBytes = parsePositiveInteger(value, key);
236
- return;
237
- case "telegram.botToken":
238
- telegram.botToken = value;
239
- return;
240
- default:
241
- throw new Error(
242
- [
243
- `Unknown config key: ${key}`,
244
- "Supported keys:",
245
- ...SUPPORTED_KEYS.map((k) => ` ${k}`)
246
- ].join("\n")
247
- );
176
+ const def = CONFIG_KEY_REGISTRY[key];
177
+ if (!def) {
178
+ throw new Error(
179
+ [
180
+ `Unknown config key: ${key}`,
181
+ "Supported keys:",
182
+ ...SUPPORTED_KEYS.map((k) => ` ${k}`)
183
+ ].join("\n")
184
+ );
185
+ }
186
+ const coerced = coerceValue(value, def.type, key);
187
+ if (def.target === "bridge") {
188
+ Object.assign(bridge, { [def.field]: coerced });
189
+ } else {
190
+ Object.assign(telegram, { [def.field]: coerced });
248
191
  }
249
192
  }
250
193
  function applyConfigUnset(bridge, telegram, key) {
251
- switch (key) {
252
- case "openclaw.path":
253
- delete bridge.openclawPath;
254
- return;
255
- case "openclaw.sessionId":
256
- delete bridge.sessionId;
257
- return;
258
- case "openclaw.threadId":
259
- delete bridge.threadId;
260
- return;
261
- case "openclaw.canvasReminderEvery":
262
- delete bridge.canvasReminderEvery;
263
- return;
264
- case "openclaw.deliver":
265
- delete bridge.deliver;
266
- return;
267
- case "openclaw.deliverChannel":
268
- delete bridge.deliverChannel;
269
- return;
270
- case "openclaw.replyTo":
271
- delete bridge.replyTo;
272
- return;
273
- case "openclaw.deliverTimeoutMs":
274
- delete bridge.deliverTimeoutMs;
275
- return;
276
- case "openclaw.attachmentDir":
277
- delete bridge.attachmentDir;
278
- return;
279
- case "openclaw.attachmentMaxBytes":
280
- delete bridge.attachmentMaxBytes;
281
- return;
282
- case "telegram.botToken":
283
- delete telegram.botToken;
284
- delete telegram.botUsername;
285
- delete telegram.hasMainWebApp;
286
- return;
287
- default:
288
- throw new Error(`Unknown config key for --unset: ${key}`);
194
+ const def = CONFIG_KEY_REGISTRY[key];
195
+ if (!def) {
196
+ throw new Error(`Unknown config key for --unset: ${key}`);
197
+ }
198
+ if (def.target === "bridge") {
199
+ delete bridge[def.field];
200
+ } else {
201
+ delete telegram[def.field];
202
+ if (def.cascadeUnset) {
203
+ for (const cascadeField of def.cascadeUnset) {
204
+ delete telegram[cascadeField];
205
+ }
206
+ }
289
207
  }
290
208
  }
291
209
  function hasValues(obj) {
292
210
  return Object.values(obj).some((value) => value !== void 0);
293
211
  }
212
+
213
+ // src/commands/configure/render.ts
294
214
  function maskSecret(value) {
295
215
  if (value.length <= 8) return "********";
296
216
  return `${value.slice(0, 4)}...${value.slice(-4)}`;
297
217
  }
218
+ function formatFieldValue(value, def) {
219
+ if (def.displayAs === "set-only") return "(set)";
220
+ if (def.type === "boolean") return value ? "true" : "false";
221
+ return String(value);
222
+ }
223
+ function printBridgeStatus(bridge) {
224
+ if (!hasValues(bridge)) return;
225
+ console.log("");
226
+ console.log("Bridge:");
227
+ for (const [key, def] of Object.entries(CONFIG_KEY_REGISTRY)) {
228
+ if (def.target !== "bridge") continue;
229
+ const value = bridge[def.field];
230
+ if (value === void 0) continue;
231
+ console.log(` ${key}: ${formatFieldValue(value, def)}`);
232
+ }
233
+ }
234
+ function printTelegramStatus(telegram) {
235
+ if (telegram?.botToken && telegram.botUsername) {
236
+ console.log(` Telegram: @${telegram.botUsername}`);
237
+ if (!telegram.hasMainWebApp) {
238
+ console.log(
239
+ " Mini App not registered \u2014 deep links open in browser, not inside Telegram."
240
+ );
241
+ console.log(
242
+ ` Fix: @BotFather \u2192 /mybots \u2192 @${telegram.botUsername} \u2192 Bot Settings \u2192 Configure Mini App`
243
+ );
244
+ console.log(" Set URL to: https://pub.blue");
245
+ }
246
+ } else {
247
+ console.log(" Telegram: not configured");
248
+ }
249
+ }
250
+ function printSetupInstructions(saved) {
251
+ const needsApiKey = !saved?.apiKey;
252
+ const needsTelegram = !saved?.telegram?.botUsername;
253
+ if (!needsApiKey && !needsTelegram) return;
254
+ console.log("");
255
+ if (needsApiKey) {
256
+ console.log(" pubblue configure --api-key <KEY>");
257
+ console.log(" Get your key at https://pub.blue/dashboard");
258
+ if (needsTelegram) console.log("");
259
+ }
260
+ if (needsTelegram) {
261
+ console.log(" pubblue configure --set telegram.botToken=<TOKEN> (optional)");
262
+ console.log(" Prints a t.me/<bot> deep link when you create or update a pub.");
263
+ console.log(" Requires a Telegram bot with Mini App URL set to https://pub.blue");
264
+ console.log(" (@BotFather \u2192 /newbot \u2192 Bot Settings \u2192 Configure Mini App)");
265
+ }
266
+ }
267
+ function printConfigStatus(saved) {
268
+ console.log(`Config directory: ${getConfigDir()}`);
269
+ console.log(" API key: %s", saved?.apiKey ? maskSecret(saved.apiKey) : "not set");
270
+ printTelegramStatus(saved?.telegram);
271
+ printBridgeStatus(saved?.bridge ?? {});
272
+ printSetupInstructions(saved);
273
+ }
274
+ function printMutationSummary(config) {
275
+ console.log(" API key: %s", maskSecret(config.apiKey));
276
+ printTelegramStatus(config.telegram);
277
+ printBridgeStatus(config.bridge ?? {});
278
+ }
279
+
280
+ // src/commands/configure/telegram.ts
298
281
  async function telegramGetMe(token) {
299
282
  const resp = await fetch(`https://api.telegram.org/bot${token}/getMe`);
300
283
  const data = await resp.json();
@@ -317,142 +300,767 @@ async function telegramSetMenuButton(token, button) {
317
300
  throw new Error(data.description ?? "setChatMenuButton failed");
318
301
  }
319
302
  }
320
- function printConfigSummary(saved) {
321
- if (!saved) {
322
- console.log("Saved config: none");
323
- return;
324
- }
325
- console.log("Saved config:");
326
- console.log(` apiKey: ${maskSecret(saved.apiKey)}`);
327
- if (saved.bridge && hasValues(saved.bridge)) {
328
- if (saved.bridge.openclawPath) console.log(` openclaw.path: ${saved.bridge.openclawPath}`);
329
- if (saved.bridge.sessionId) console.log(` openclaw.sessionId: ${saved.bridge.sessionId}`);
330
- if (saved.bridge.threadId) console.log(` openclaw.threadId: ${saved.bridge.threadId}`);
331
- if (saved.bridge.canvasReminderEvery !== void 0)
332
- console.log(` openclaw.canvasReminderEvery: ${saved.bridge.canvasReminderEvery}`);
333
- if (saved.bridge.deliver !== void 0)
334
- console.log(` openclaw.deliver: ${saved.bridge.deliver ? "true" : "false"}`);
335
- if (saved.bridge.deliverChannel)
336
- console.log(` openclaw.deliverChannel: ${saved.bridge.deliverChannel}`);
337
- if (saved.bridge.replyTo) console.log(` openclaw.replyTo: ${saved.bridge.replyTo}`);
338
- if (saved.bridge.deliverTimeoutMs !== void 0)
339
- console.log(` openclaw.deliverTimeoutMs: ${saved.bridge.deliverTimeoutMs}`);
340
- if (saved.bridge.attachmentDir)
341
- console.log(` openclaw.attachmentDir: ${saved.bridge.attachmentDir}`);
342
- if (saved.bridge.attachmentMaxBytes !== void 0)
343
- console.log(` openclaw.attachmentMaxBytes: ${saved.bridge.attachmentMaxBytes}`);
344
- } else {
345
- console.log(" bridge: none");
346
- }
347
- if (saved.telegram?.botToken && saved.telegram.botUsername) {
348
- console.log(` telegram.botToken: ${maskSecret(saved.telegram.botToken)}`);
349
- console.log(` telegram.botUsername: @${saved.telegram.botUsername}`);
350
- if (!saved.telegram.hasMainWebApp) {
351
- console.log(" INFO: Register Mini App in @BotFather for deep links to open in Telegram");
352
- }
353
- } else if (saved.telegram?.botToken) {
354
- console.log(` telegram.botToken: ${maskSecret(saved.telegram.botToken)}`);
355
- console.log(" telegram.botUsername: (not resolved)");
356
- } else {
357
- console.log(" telegram: not configured");
358
- console.log(" INFO: Set telegram.botToken to enable Telegram Mini App links");
359
- console.log(" Example: pubblue configure --set telegram.botToken=<BOT_TOKEN>");
360
- }
361
- }
303
+
304
+ // src/commands/configure/command.ts
362
305
  function registerConfigureCommand(program2) {
363
- program2.command("configure").description("Configure the CLI with your API key").option("--api-key <key>", "Your API key (less secure: appears in shell history)").option("--api-key-stdin", "Read API key from stdin").option(
306
+ program2.command("configure").description("Show configuration status, or update settings with --api-key / --set / --unset").option("--api-key <key>", "Set API key (appears in shell history; prefer --api-key-stdin)").option("--api-key-stdin", "Read API key from stdin").option(
364
307
  "--set <key=value>",
365
308
  "Set config key (repeatable). Example: --set telegram.botToken=<token>",
366
309
  collectValues,
367
310
  []
368
- ).option("--unset <key>", "Unset config key (repeatable)", collectValues, []).option("--show", "Show saved configuration").action(
369
- async (opts) => {
370
- const saved = readConfig();
371
- const hasApiUpdate = Boolean(opts.apiKey || opts.apiKeyStdin);
372
- const hasSet = opts.set.length > 0;
373
- const hasUnset = opts.unset.length > 0;
374
- const hasMutation = hasApiUpdate || hasSet || hasUnset;
375
- if (!hasMutation && opts.show) {
376
- printConfigSummary(saved);
377
- return;
378
- }
379
- let apiKey = saved?.apiKey;
380
- if (hasApiUpdate || !hasMutation) {
381
- apiKey = await resolveConfigureApiKey(opts);
382
- }
383
- if (!apiKey) {
384
- const envKey = process.env.PUBBLUE_API_KEY?.trim();
385
- if (envKey) {
386
- apiKey = envKey;
387
- } else {
388
- throw new Error(
389
- "No API key available. Provide --api-key/--api-key-stdin (or run plain `pubblue configure` first)."
390
- );
391
- }
311
+ ).option("--unset <key>", "Unset config key (repeatable)", collectValues, []).action(async (opts) => {
312
+ const saved = readConfig();
313
+ const hasApiUpdate = Boolean(opts.apiKey || opts.apiKeyStdin);
314
+ const hasSet = opts.set.length > 0;
315
+ const hasUnset = opts.unset.length > 0;
316
+ const hasMutation = hasApiUpdate || hasSet || hasUnset;
317
+ if (!hasMutation) {
318
+ printConfigStatus(saved);
319
+ return;
320
+ }
321
+ let apiKey = saved?.apiKey;
322
+ if (hasApiUpdate) {
323
+ apiKey = await resolveConfigureApiKey(opts);
324
+ }
325
+ if (!apiKey) {
326
+ const envKey = process.env.PUBBLUE_API_KEY?.trim();
327
+ if (envKey) {
328
+ apiKey = envKey;
329
+ } else {
330
+ throw new Error("No API key configured. Set it first: pubblue configure --api-key <KEY>");
392
331
  }
393
- const nextBridge = { ...saved?.bridge ?? {} };
394
- const nextTelegram = { ...saved?.telegram ?? {} };
395
- let telegramTokenChanged = false;
396
- for (const entry of opts.set) {
397
- const { key, value } = parseSetInput(entry);
398
- applyConfigSet(nextBridge, nextTelegram, key, value);
399
- if (key === "telegram.botToken") telegramTokenChanged = true;
400
- }
401
- for (const key of opts.unset) {
402
- if (key.trim() === "telegram.botToken" && nextTelegram.botToken) {
403
- try {
404
- await telegramSetMenuButton(nextTelegram.botToken, { type: "default" });
405
- console.log("Telegram menu button reset to default.");
406
- } catch (error) {
407
- console.error(
408
- `Warning: failed to reset Telegram menu button: ${errorMessage(error)}`
409
- );
410
- }
332
+ }
333
+ const nextBridge = { ...saved?.bridge ?? {} };
334
+ const nextTelegram = { ...saved?.telegram ?? {} };
335
+ let telegramTokenChanged = false;
336
+ for (const entry of opts.set) {
337
+ const { key, value } = parseSetInput(entry);
338
+ applyConfigSet(nextBridge, nextTelegram, key, value);
339
+ if (key === "telegram.botToken") telegramTokenChanged = true;
340
+ }
341
+ for (const key of opts.unset) {
342
+ if (key.trim() === "telegram.botToken" && nextTelegram.botToken) {
343
+ try {
344
+ await telegramSetMenuButton(nextTelegram.botToken, { type: "default" });
345
+ console.log("Telegram menu button reset to default.");
346
+ } catch (error) {
347
+ console.error(`Warning: failed to reset Telegram menu button: ${errorMessage(error)}`);
411
348
  }
412
- applyConfigUnset(nextBridge, nextTelegram, key.trim());
413
- }
414
- if (telegramTokenChanged && nextTelegram.botToken) {
415
- console.log("Verifying Telegram bot token...");
416
- const bot = await telegramGetMe(nextTelegram.botToken);
417
- nextTelegram.botUsername = bot.username;
418
- nextTelegram.hasMainWebApp = bot.hasMainWebApp;
419
- console.log(` Bot: @${bot.username}`);
420
- await telegramSetMenuButton(nextTelegram.botToken, {
421
- type: "web_app",
422
- text: "Open",
423
- web_app: { url: "https://pub.blue" }
424
- });
425
- console.log(" Menu button set to https://pub.blue");
426
- if (!bot.hasMainWebApp) {
427
- console.log("");
428
- console.log(" INFO: For deep links to open inside Telegram, register the Mini App:");
429
- console.log(" @BotFather \u2192 /mybots \u2192 your bot \u2192 Bot Settings \u2192 Configure Mini App");
430
- console.log(" Set Web App URL to: https://pub.blue");
349
+ try {
350
+ const api = new PubApiClient(process.env.PUBBLUE_URL || DEFAULT_BASE_URL, apiKey);
351
+ await api.deleteBotToken();
352
+ console.log("Bot token removed from server.");
353
+ } catch (error) {
354
+ console.error(
355
+ `Warning: failed to remove bot token from server: ${errorMessage(error)}`
356
+ );
431
357
  }
432
358
  }
433
- const nextConfig = {
434
- apiKey,
435
- bridge: hasValues(nextBridge) ? nextBridge : void 0,
436
- telegram: hasValues(nextTelegram) ? nextTelegram : void 0
437
- };
438
- saveConfig(nextConfig);
439
- console.log("Configuration saved.");
440
- if (opts.show || hasSet || hasUnset) {
441
- printConfigSummary(nextConfig);
359
+ applyConfigUnset(nextBridge, nextTelegram, key.trim());
360
+ }
361
+ if (telegramTokenChanged && nextTelegram.botToken) {
362
+ console.log("Verifying Telegram bot token...");
363
+ const bot = await telegramGetMe(nextTelegram.botToken);
364
+ nextTelegram.botUsername = bot.username;
365
+ nextTelegram.hasMainWebApp = bot.hasMainWebApp;
366
+ console.log(` Bot: @${bot.username}`);
367
+ await telegramSetMenuButton(nextTelegram.botToken, {
368
+ type: "web_app",
369
+ text: "Open",
370
+ web_app: { url: "https://pub.blue" }
371
+ });
372
+ console.log(" Menu button set to https://pub.blue");
373
+ if (!bot.hasMainWebApp) {
374
+ console.log("");
375
+ console.log(" Mini App not registered \u2014 deep links will open in browser, not Telegram.");
376
+ console.log(" @BotFather \u2192 /mybots \u2192 your bot \u2192 Bot Settings \u2192 Configure Mini App");
377
+ console.log(" Set Web App URL to: https://pub.blue");
442
378
  }
379
+ const api = new PubApiClient(process.env.PUBBLUE_URL || DEFAULT_BASE_URL, apiKey);
380
+ await api.uploadBotToken({
381
+ botToken: nextTelegram.botToken,
382
+ botUsername: bot.username
383
+ });
384
+ console.log(" Bot token synced to server.");
443
385
  }
444
- );
386
+ const nextConfig = {
387
+ apiKey,
388
+ bridge: hasValues(nextBridge) ? nextBridge : void 0,
389
+ telegram: hasValues(nextTelegram) ? nextTelegram : void 0
390
+ };
391
+ saveConfig(nextConfig);
392
+ console.log("Configuration saved.");
393
+ printMutationSummary(nextConfig);
394
+ });
445
395
  }
446
396
 
447
397
  // src/commands/live.ts
448
398
  import * as fs3 from "fs";
449
399
  import * as path3 from "path";
450
400
 
401
+ // src/lib/live-ipc.ts
402
+ import * as net from "net";
403
+ function getAgentSocketPath() {
404
+ const override = process.env.PUBBLUE_AGENT_SOCKET?.trim();
405
+ if (override && override.length > 0) return override;
406
+ return "/tmp/pubblue-agent.sock";
407
+ }
408
+ async function ipcCall(socketPath, request) {
409
+ return new Promise((resolve3, reject) => {
410
+ let settled = false;
411
+ let timeoutId = null;
412
+ const finish = (fn) => {
413
+ if (settled) return;
414
+ settled = true;
415
+ if (timeoutId) clearTimeout(timeoutId);
416
+ fn();
417
+ };
418
+ const client = net.createConnection(socketPath, () => {
419
+ client.write(`${JSON.stringify(request)}
420
+ `);
421
+ });
422
+ let data = "";
423
+ client.on("data", (chunk) => {
424
+ data += chunk.toString();
425
+ const newlineIdx = data.indexOf("\n");
426
+ if (newlineIdx !== -1) {
427
+ const line = data.slice(0, newlineIdx);
428
+ client.end();
429
+ try {
430
+ finish(() => resolve3(JSON.parse(line)));
431
+ } catch {
432
+ finish(() => reject(new Error("Invalid response from daemon")));
433
+ }
434
+ }
435
+ });
436
+ client.on("error", (err) => {
437
+ if (err.code === "ECONNREFUSED" || err.code === "ENOENT") {
438
+ finish(() => reject(new Error("Daemon not running.")));
439
+ } else {
440
+ finish(() => reject(err));
441
+ }
442
+ });
443
+ client.on("end", () => {
444
+ if (!data.includes("\n")) {
445
+ finish(() => reject(new Error("Daemon closed connection unexpectedly")));
446
+ }
447
+ });
448
+ timeoutId = setTimeout(() => {
449
+ client.destroy();
450
+ finish(() => reject(new Error("Daemon request timed out")));
451
+ }, 1e4);
452
+ });
453
+ }
454
+
455
+ // src/lib/live-runtime/bridge-runtime.ts
456
+ function buildBridgeProcessEnv(bridgeConfig) {
457
+ const env = { ...process.env };
458
+ const setIfMissing = (key, value) => {
459
+ if (value === void 0) return;
460
+ const current = env[key];
461
+ if (typeof current === "string" && current.length > 0) return;
462
+ env[key] = String(value);
463
+ };
464
+ setIfMissing("OPENCLAW_HOME", resolveOpenClawHome(env));
465
+ if (bridgeConfig) {
466
+ setIfMissing("OPENCLAW_PATH", bridgeConfig.openclawPath);
467
+ setIfMissing("OPENCLAW_STATE_DIR", bridgeConfig.openclawStateDir);
468
+ setIfMissing("OPENCLAW_WORKSPACE", bridgeConfig.openclawWorkspace);
469
+ setIfMissing("OPENCLAW_SESSION_ID", bridgeConfig.sessionId);
470
+ setIfMissing("OPENCLAW_THREAD_ID", bridgeConfig.threadId);
471
+ setIfMissing("OPENCLAW_CANVAS_REMINDER_EVERY", bridgeConfig.canvasReminderEvery);
472
+ setIfMissing(
473
+ "OPENCLAW_DELIVER",
474
+ bridgeConfig.deliver === void 0 ? void 0 : bridgeConfig.deliver ? "1" : "0"
475
+ );
476
+ setIfMissing("OPENCLAW_DELIVER_CHANNEL", bridgeConfig.deliverChannel);
477
+ setIfMissing("OPENCLAW_REPLY_TO", bridgeConfig.replyTo);
478
+ setIfMissing("OPENCLAW_DELIVER_TIMEOUT_MS", bridgeConfig.deliverTimeoutMs);
479
+ setIfMissing("OPENCLAW_ATTACHMENT_DIR", bridgeConfig.attachmentDir);
480
+ setIfMissing("OPENCLAW_ATTACHMENT_MAX_BYTES", bridgeConfig.attachmentMaxBytes);
481
+ setIfMissing("CLAUDE_CODE_PATH", bridgeConfig.claudeCodePath);
482
+ setIfMissing("CLAUDE_CODE_MODEL", bridgeConfig.claudeCodeModel);
483
+ setIfMissing("CLAUDE_CODE_ALLOWED_TOOLS", bridgeConfig.claudeCodeAllowedTools);
484
+ setIfMissing("CLAUDE_CODE_APPEND_SYSTEM_PROMPT", bridgeConfig.claudeCodeAppendSystemPrompt);
485
+ setIfMissing("CLAUDE_CODE_MAX_TURNS", bridgeConfig.claudeCodeMaxTurns);
486
+ setIfMissing("CLAUDE_CODE_CWD", bridgeConfig.claudeCodeCwd);
487
+ setIfMissing("PUBBLUE_COMMAND_DEFAULT_TIMEOUT_MS", bridgeConfig.commandDefaultTimeoutMs);
488
+ setIfMissing("PUBBLUE_COMMAND_MAX_OUTPUT_BYTES", bridgeConfig.commandMaxOutputBytes);
489
+ setIfMissing("PUBBLUE_COMMAND_MAX_CONCURRENT", bridgeConfig.commandMaxConcurrent);
490
+ }
491
+ setIfMissing("OPENCLAW_WORKSPACE", resolveOpenClawWorkspaceDir(env));
492
+ setIfMissing("PUBBLUE_PROJECT_ROOT", process.cwd());
493
+ return env;
494
+ }
495
+ async function ensureNodeDatachannelAvailable() {
496
+ try {
497
+ await import("node-datachannel");
498
+ } catch (error) {
499
+ throw new Error(
500
+ [
501
+ "node-datachannel native module is not available.",
502
+ "Run `pnpm rebuild node-datachannel` in the cli package and retry.",
503
+ `Details: ${error instanceof Error ? error.message : String(error)}`
504
+ ].join("\n")
505
+ );
506
+ }
507
+ }
508
+ function parseBridgeMode(raw) {
509
+ const normalized = raw.trim().toLowerCase();
510
+ if (normalized === "openclaw" || normalized === "claude-code") {
511
+ return normalized;
512
+ }
513
+ throw new Error(`--bridge must be one of: openclaw, claude-code. Received: ${raw}`);
514
+ }
515
+ function describeConfiguredPath(key, env) {
516
+ const configured = env[key]?.trim();
517
+ return configured ? `${key}=${configured}` : `${key} not set`;
518
+ }
519
+ var BRIDGE_PROVIDERS = [
520
+ {
521
+ mode: "openclaw",
522
+ priority: 100,
523
+ detect(env) {
524
+ const available = isOpenClawAvailable(env);
525
+ if (available) {
526
+ return {
527
+ available: true,
528
+ detail: `OpenClaw runtime detected (${describeConfiguredPath("OPENCLAW_PATH", env)})`
529
+ };
530
+ }
531
+ return {
532
+ available: false,
533
+ detail: `OpenClaw runtime not detected (${describeConfiguredPath("OPENCLAW_PATH", env)})`
534
+ };
535
+ },
536
+ async startupProbe(env) {
537
+ const runtime = await runOpenClawBridgeStartupProbe(env);
538
+ return [
539
+ `OpenClaw executable: ${runtime.openclawPath}`,
540
+ `OpenClaw session: ${runtime.sessionId} (${runtime.sessionSource ?? "unknown"})`,
541
+ 'OpenClaw communication via `pubblue write "pong"`: OK'
542
+ ];
543
+ }
544
+ },
545
+ {
546
+ mode: "claude-code",
547
+ priority: 50,
548
+ detect(env) {
549
+ const available = isClaudeCodeAvailableInEnv(env);
550
+ if (available) {
551
+ return {
552
+ available: true,
553
+ detail: `Claude Code runtime detected (${describeConfiguredPath("CLAUDE_CODE_PATH", env)})`
554
+ };
555
+ }
556
+ return {
557
+ available: false,
558
+ detail: `Claude Code runtime not detected (${describeConfiguredPath("CLAUDE_CODE_PATH", env)})`
559
+ };
560
+ },
561
+ async startupProbe(env) {
562
+ const runtime = await runClaudeCodeBridgeStartupProbe(env);
563
+ const cwd = runtime.cwd || env.PUBBLUE_PROJECT_ROOT || process.cwd();
564
+ return [
565
+ `Claude executable: ${runtime.claudePath}`,
566
+ `Claude cwd: ${cwd}`,
567
+ 'Claude communication via `pubblue write "pong"`: OK'
568
+ ];
569
+ }
570
+ }
571
+ ].sort((a, b) => b.priority - a.priority);
572
+ function getBridgeProvider(mode) {
573
+ const provider = BRIDGE_PROVIDERS.find((entry) => entry.mode === mode);
574
+ if (!provider) {
575
+ throw new Error(`Unsupported bridge provider: ${mode}`);
576
+ }
577
+ return provider;
578
+ }
579
+ function detectBridgeAvailability(env = process.env) {
580
+ return BRIDGE_PROVIDERS.map((provider) => {
581
+ const detection = provider.detect(env);
582
+ return {
583
+ mode: provider.mode,
584
+ available: detection.available,
585
+ detail: detection.detail
586
+ };
587
+ });
588
+ }
589
+ function resolveBridgeSelection(opts, env = process.env) {
590
+ if (opts.bridge) {
591
+ const mode = parseBridgeMode(opts.bridge);
592
+ const provider = getBridgeProvider(mode);
593
+ const detection = provider.detect(env);
594
+ if (!detection.available) {
595
+ throw new Error(`Requested bridge "${mode}" is unavailable: ${detection.detail}`);
596
+ }
597
+ return {
598
+ mode,
599
+ source: "explicit",
600
+ detail: detection.detail
601
+ };
602
+ }
603
+ const detections = BRIDGE_PROVIDERS.map((provider) => ({
604
+ provider,
605
+ detection: provider.detect(env)
606
+ }));
607
+ const selected = detections.find((entry) => entry.detection.available);
608
+ if (!selected) {
609
+ const details = detections.map(
610
+ (entry) => `- ${entry.provider.mode}: ${entry.detection.detail}`
611
+ );
612
+ throw new Error(
613
+ [
614
+ "No bridge detected.",
615
+ "Install/configure OpenClaw or Claude Code and retry.",
616
+ ...details
617
+ ].join("\n")
618
+ );
619
+ }
620
+ return {
621
+ mode: selected.provider.mode,
622
+ source: "auto",
623
+ detail: selected.detection.detail
624
+ };
625
+ }
626
+ async function runBridgeStartupPreflight(selection, env = process.env) {
627
+ const provider = getBridgeProvider(selection.mode);
628
+ return provider.startupProbe(env);
629
+ }
630
+
631
+ // src/lib/live-runtime/command-utils.ts
632
+ function getFollowReadDelayMs(disconnected, consecutiveFailures) {
633
+ if (!disconnected) return 1e3;
634
+ return Math.min(5e3, 1e3 * 2 ** Math.min(consecutiveFailures, 3));
635
+ }
636
+ function messageContainsPong(payload) {
637
+ if (!payload || typeof payload !== "object") return false;
638
+ const message = payload.msg;
639
+ if (!message || typeof message !== "object") return false;
640
+ const type = message.type;
641
+ const data = message.data;
642
+ return type === "text" && typeof data === "string" && data.trim().toLowerCase() === "pong";
643
+ }
644
+ function formatApiError(error) {
645
+ if (error instanceof PubApiError) {
646
+ if (error.status === 429 && error.retryAfterSeconds !== void 0) {
647
+ return `Rate limit exceeded. Retry after ${error.retryAfterSeconds}s.`;
648
+ }
649
+ return `${error.message} (HTTP ${error.status})`;
650
+ }
651
+ return errorMessage(error);
652
+ }
653
+
654
+ // src/lib/live-runtime/daemon-process.ts
655
+ import * as fs2 from "fs";
656
+ function hasErrnoCode(error, code) {
657
+ return typeof error === "object" && error !== null && "code" in error && error.code === code;
658
+ }
659
+ function removeStaleDaemonInfo(infoPath) {
660
+ try {
661
+ fs2.unlinkSync(infoPath);
662
+ } catch (error) {
663
+ if (hasErrnoCode(error, "ENOENT")) return;
664
+ throw new Error(`Failed to clean up stale daemon info: ${errorMessage(error)}`);
665
+ }
666
+ }
667
+ function isProcessAlive(pid) {
668
+ try {
669
+ process.kill(pid, 0);
670
+ return true;
671
+ } catch (error) {
672
+ if (hasErrnoCode(error, "ESRCH")) return false;
673
+ if (hasErrnoCode(error, "EPERM")) return true;
674
+ throw new Error(`Failed to check process ${pid}: ${errorMessage(error)}`);
675
+ }
676
+ }
677
+ function readDaemonProcessInfo(slug) {
678
+ const infoPath = liveInfoPath(slug);
679
+ let raw;
680
+ try {
681
+ raw = fs2.readFileSync(infoPath, "utf-8");
682
+ } catch (error) {
683
+ if (hasErrnoCode(error, "ENOENT")) return null;
684
+ throw new Error(`Failed to read daemon info at ${infoPath}: ${errorMessage(error)}`);
685
+ }
686
+ let info;
687
+ try {
688
+ info = JSON.parse(raw);
689
+ } catch (error) {
690
+ removeStaleDaemonInfo(infoPath);
691
+ throw new Error(`Invalid daemon info JSON at ${infoPath}: ${errorMessage(error)}`);
692
+ }
693
+ if (!Number.isFinite(info.pid)) {
694
+ removeStaleDaemonInfo(infoPath);
695
+ return null;
696
+ }
697
+ try {
698
+ if (!isProcessAlive(info.pid)) {
699
+ removeStaleDaemonInfo(infoPath);
700
+ return null;
701
+ }
702
+ return info;
703
+ } catch (error) {
704
+ throw new Error(`Failed to inspect daemon process ${info.pid}: ${errorMessage(error)}`);
705
+ }
706
+ }
707
+ async function waitForProcessExit(pid, timeoutMs) {
708
+ const startedAt = Date.now();
709
+ while (Date.now() - startedAt < timeoutMs) {
710
+ if (!isProcessAlive(pid)) return true;
711
+ await new Promise((resolve3) => setTimeout(resolve3, 120));
712
+ }
713
+ return !isProcessAlive(pid);
714
+ }
715
+ async function stopDaemonForLive(info) {
716
+ const pid = info.pid;
717
+ if (!isProcessAlive(pid)) return null;
718
+ const socketPath = info.socketPath;
719
+ if (socketPath) {
720
+ try {
721
+ await ipcCall(socketPath, { method: "close", params: {} });
722
+ } catch (error) {
723
+ try {
724
+ process.kill(pid, "SIGTERM");
725
+ } catch (killError) {
726
+ return `daemon ${pid}: IPC close failed (${errorMessage(error)}); SIGTERM failed (${errorMessage(killError)})`;
727
+ }
728
+ }
729
+ } else {
730
+ try {
731
+ process.kill(pid, "SIGTERM");
732
+ } catch (error) {
733
+ return `daemon ${pid}: no socketPath and SIGTERM failed (${errorMessage(error)})`;
734
+ }
735
+ }
736
+ const stopped = await waitForProcessExit(pid, 8e3);
737
+ if (!stopped) return `daemon ${pid}: did not exit after stop request`;
738
+ return null;
739
+ }
740
+ function isDaemonRunning(slug) {
741
+ return readDaemonProcessInfo(slug) !== null;
742
+ }
743
+ async function stopOtherDaemons() {
744
+ const dir = liveInfoDir();
745
+ const entries = fs2.readdirSync(dir).filter((name) => name.endsWith(".json"));
746
+ const failures = [];
747
+ for (const entry of entries) {
748
+ const slug = entry.replace(/\.json$/, "");
749
+ const info = readDaemonProcessInfo(slug);
750
+ if (!info) continue;
751
+ const daemonError = await stopDaemonForLive(info);
752
+ if (daemonError) failures.push(`[${slug}] ${daemonError}`);
753
+ }
754
+ if (failures.length > 0) {
755
+ throw new Error(
756
+ [
757
+ "Critical: failed to stop previous live daemon processes.",
758
+ "Starting a new daemon now would leak resources and increase bandwidth usage.",
759
+ ...failures
760
+ ].join("\n")
761
+ );
762
+ }
763
+ }
764
+ function buildDaemonForkStdio(logFd) {
765
+ return ["ignore", logFd, logFd, "ipc"];
766
+ }
767
+ function waitForDaemonReady({
768
+ child,
769
+ infoPath,
770
+ socketPath,
771
+ timeoutMs
772
+ }) {
773
+ return new Promise((resolve3) => {
774
+ let settled = false;
775
+ let pollInFlight = false;
776
+ let lastIpcError = null;
777
+ const done = (result) => {
778
+ if (settled) return;
779
+ settled = true;
780
+ clearInterval(poll);
781
+ clearTimeout(timeout);
782
+ child.off("exit", onExit);
783
+ resolve3(result);
784
+ };
785
+ const onExit = (code, signal) => {
786
+ const suffix = signal ? ` (signal ${signal})` : "";
787
+ done({ ok: false, reason: `daemon exited with code ${code ?? 0}${suffix}` });
788
+ };
789
+ child.on("exit", onExit);
790
+ const poll = setInterval(() => {
791
+ if (pollInFlight || !fs2.existsSync(infoPath)) return;
792
+ pollInFlight = true;
793
+ void ipcCall(socketPath, { method: "status", params: {} }).then((status) => {
794
+ if (status.ok) done({ ok: true });
795
+ }).catch((error) => {
796
+ lastIpcError = errorMessage(error);
797
+ }).finally(() => {
798
+ pollInFlight = false;
799
+ });
800
+ }, 120);
801
+ const timeout = setTimeout(() => {
802
+ 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`;
803
+ done({ ok: false, reason });
804
+ }, timeoutMs);
805
+ });
806
+ }
807
+ async function resolveActiveSlug() {
808
+ const socketPath = getAgentSocketPath();
809
+ const response = await ipcCall(socketPath, { method: "active-slug", params: {} });
810
+ if (response.ok && typeof response.slug === "string" && response.slug.length > 0) {
811
+ return response.slug;
812
+ }
813
+ throw new Error("Daemon is running but no live is active. Wait for browser to initiate live.");
814
+ }
815
+
816
+ // src/lib/live-runtime/file-payload.ts
817
+ import * as path2 from "path";
818
+ var TEXT_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
819
+ ".txt",
820
+ ".md",
821
+ ".markdown",
822
+ ".json",
823
+ ".csv",
824
+ ".xml",
825
+ ".yaml",
826
+ ".yml",
827
+ ".js",
828
+ ".mjs",
829
+ ".cjs",
830
+ ".ts",
831
+ ".tsx",
832
+ ".jsx",
833
+ ".css",
834
+ ".scss",
835
+ ".sass",
836
+ ".less",
837
+ ".log"
838
+ ]);
839
+ var MIME_BY_EXT = {
840
+ ".html": "text/html; charset=utf-8",
841
+ ".htm": "text/html; charset=utf-8",
842
+ ".txt": "text/plain; charset=utf-8",
843
+ ".md": "text/markdown; charset=utf-8",
844
+ ".markdown": "text/markdown; charset=utf-8",
845
+ ".json": "application/json",
846
+ ".csv": "text/csv; charset=utf-8",
847
+ ".xml": "application/xml",
848
+ ".yaml": "application/x-yaml",
849
+ ".yml": "application/x-yaml",
850
+ ".js": "text/javascript; charset=utf-8",
851
+ ".mjs": "text/javascript; charset=utf-8",
852
+ ".cjs": "text/javascript; charset=utf-8",
853
+ ".ts": "text/typescript; charset=utf-8",
854
+ ".tsx": "text/typescript; charset=utf-8",
855
+ ".jsx": "text/javascript; charset=utf-8",
856
+ ".css": "text/css; charset=utf-8",
857
+ ".scss": "text/x-scss; charset=utf-8",
858
+ ".sass": "text/x-sass; charset=utf-8",
859
+ ".less": "text/x-less; charset=utf-8",
860
+ ".log": "text/plain; charset=utf-8",
861
+ ".png": "image/png",
862
+ ".jpg": "image/jpeg",
863
+ ".jpeg": "image/jpeg",
864
+ ".gif": "image/gif",
865
+ ".webp": "image/webp",
866
+ ".svg": "image/svg+xml",
867
+ ".pdf": "application/pdf",
868
+ ".zip": "application/zip",
869
+ ".mp3": "audio/mpeg",
870
+ ".wav": "audio/wav",
871
+ ".mp4": "video/mp4"
872
+ };
873
+ function getMimeType(filePath) {
874
+ const ext = path2.extname(filePath).toLowerCase();
875
+ return MIME_BY_EXT[ext] || "application/octet-stream";
876
+ }
877
+
878
+ // src/lib/live-runtime/start-preflight.ts
879
+ var BRIDGE_CONFIG_FIELDS = [
880
+ { field: "openclawPath", label: "openclaw.path" },
881
+ { field: "openclawStateDir", label: "openclaw.stateDir" },
882
+ { field: "openclawWorkspace", label: "openclaw.workspace" },
883
+ { field: "sessionId", label: "openclaw.sessionId" },
884
+ { field: "threadId", label: "openclaw.threadId" },
885
+ { field: "canvasReminderEvery", label: "openclaw.canvasReminderEvery" },
886
+ { field: "deliver", label: "openclaw.deliver" },
887
+ { field: "deliverChannel", label: "openclaw.deliverChannel" },
888
+ { field: "replyTo", label: "openclaw.replyTo" },
889
+ { field: "deliverTimeoutMs", label: "openclaw.deliverTimeoutMs" },
890
+ { field: "attachmentDir", label: "openclaw.attachmentDir" },
891
+ { field: "attachmentMaxBytes", label: "openclaw.attachmentMaxBytes" },
892
+ { field: "claudeCodePath", label: "claude-code.path" },
893
+ { field: "claudeCodeModel", label: "claude-code.model" },
894
+ { field: "claudeCodeAllowedTools", label: "claude-code.allowedTools" },
895
+ { field: "claudeCodeAppendSystemPrompt", label: "claude-code.appendSystemPrompt" },
896
+ { field: "claudeCodeMaxTurns", label: "claude-code.maxTurns" },
897
+ { field: "claudeCodeCwd", label: "claude-code.cwd" },
898
+ { field: "commandDefaultTimeoutMs", label: "command.defaultTimeoutMs" },
899
+ { field: "commandMaxOutputBytes", label: "command.maxOutputBytes" },
900
+ { field: "commandMaxConcurrent", label: "command.maxConcurrent" }
901
+ ];
902
+ var BRIDGE_ENV_OVERRIDE_KEYS = [
903
+ "OPENCLAW_PATH",
904
+ "OPENCLAW_STATE_DIR",
905
+ "OPENCLAW_WORKSPACE",
906
+ "OPENCLAW_SESSION_ID",
907
+ "OPENCLAW_THREAD_ID",
908
+ "OPENCLAW_CANVAS_REMINDER_EVERY",
909
+ "OPENCLAW_DELIVER",
910
+ "OPENCLAW_DELIVER_CHANNEL",
911
+ "OPENCLAW_REPLY_TO",
912
+ "OPENCLAW_DELIVER_TIMEOUT_MS",
913
+ "OPENCLAW_ATTACHMENT_DIR",
914
+ "OPENCLAW_ATTACHMENT_MAX_BYTES",
915
+ "CLAUDE_CODE_PATH",
916
+ "CLAUDE_CODE_MODEL",
917
+ "CLAUDE_CODE_ALLOWED_TOOLS",
918
+ "CLAUDE_CODE_APPEND_SYSTEM_PROMPT",
919
+ "CLAUDE_CODE_MAX_TURNS",
920
+ "CLAUDE_CODE_CWD",
921
+ "PUBBLUE_COMMAND_DEFAULT_TIMEOUT_MS",
922
+ "PUBBLUE_COMMAND_MAX_OUTPUT_BYTES",
923
+ "PUBBLUE_COMMAND_MAX_CONCURRENT"
924
+ ];
925
+ function listSavedBridgeConfigKeys(bridgeConfig) {
926
+ if (!bridgeConfig) return [];
927
+ return BRIDGE_CONFIG_FIELDS.filter(({ field }) => bridgeConfig[field] !== void 0).map(
928
+ ({ label }) => label
929
+ );
930
+ }
931
+ function listBridgeEnvOverrides(env = process.env) {
932
+ return BRIDGE_ENV_OVERRIDE_KEYS.filter((key) => {
933
+ const value = env[key];
934
+ return typeof value === "string" && value.trim().length > 0;
935
+ });
936
+ }
937
+ function formatBridgeAvailability(entries) {
938
+ return entries.map(
939
+ (entry) => `${entry.mode}=${entry.available ? "available" : "unavailable"} (${entry.detail})`
940
+ ).join(" | ");
941
+ }
942
+ function formatPreflightError(params) {
943
+ const { failures, passed, skipped } = params;
944
+ const lines = [
945
+ "Start preflight failed. Critical checks did not pass:",
946
+ ...failures.map((entry) => `- [${entry.label}] ${entry.detail}`)
947
+ ];
948
+ if (passed.length > 0) {
949
+ lines.push("", "Passed checks:");
950
+ lines.push(...passed.map((entry) => `- [${entry.label}] ${entry.detail}`));
951
+ }
952
+ if (skipped.length > 0) {
953
+ lines.push("", "Skipped checks:");
954
+ lines.push(...skipped.map((entry) => `- [${entry.label}] ${entry.detail}`));
955
+ }
956
+ lines.push("", "Debug tips:");
957
+ lines.push("- Run `pubblue configure` to inspect saved CLI configuration.");
958
+ lines.push("- Use `pubblue start --bridge openclaw|claude-code` to force a bridge mode.");
959
+ return lines.join("\n");
960
+ }
961
+ async function runStartPreflight(opts) {
962
+ const passed = [];
963
+ const failures = [];
964
+ const skipped = [];
965
+ let runtimeConfig = null;
966
+ let bridgeSelection = null;
967
+ let bridgeProcessEnv = buildBridgeProcessEnv();
968
+ try {
969
+ await ensureNodeDatachannelAvailable();
970
+ passed.push({ label: "node-datachannel", detail: "native module loaded" });
971
+ } catch (error) {
972
+ failures.push({ label: "node-datachannel", detail: errorMessage(error) });
973
+ }
974
+ try {
975
+ runtimeConfig = getConfig();
976
+ const source = process.env.PUBBLUE_API_KEY?.trim() ? "PUBBLUE_API_KEY env" : "saved config";
977
+ passed.push({ label: "config", detail: `API key configured (${source})` });
978
+ bridgeProcessEnv = buildBridgeProcessEnv(runtimeConfig.bridge);
979
+ const savedBridgeConfig = listSavedBridgeConfigKeys(runtimeConfig.bridge);
980
+ const envOverrides = listBridgeEnvOverrides();
981
+ passed.push({
982
+ label: "bridge.config",
983
+ detail: `saved: ${savedBridgeConfig.join(", ") || "(none)"} | env overrides: ${envOverrides.join(", ") || "(none)"}`
984
+ });
985
+ } catch (error) {
986
+ failures.push({ label: "config", detail: errorMessage(error) });
987
+ bridgeProcessEnv = buildBridgeProcessEnv();
988
+ }
989
+ const bridgeAvailability = detectBridgeAvailability(bridgeProcessEnv);
990
+ passed.push({
991
+ label: "bridge.available",
992
+ detail: formatBridgeAvailability(bridgeAvailability)
993
+ });
994
+ try {
995
+ bridgeSelection = resolveBridgeSelection(opts, bridgeProcessEnv);
996
+ passed.push({
997
+ label: "bridge.resolve",
998
+ detail: `${bridgeSelection.mode} (${bridgeSelection.source}, ${bridgeSelection.detail})`
999
+ });
1000
+ } catch (error) {
1001
+ failures.push({ label: "bridge.resolve", detail: errorMessage(error) });
1002
+ }
1003
+ if (bridgeSelection) {
1004
+ try {
1005
+ const details = await runBridgeStartupPreflight(bridgeSelection, bridgeProcessEnv);
1006
+ passed.push({ label: "bridge.preflight", detail: details.join(" | ") });
1007
+ } catch (error) {
1008
+ failures.push({ label: `bridge.${bridgeSelection.mode}`, detail: errorMessage(error) });
1009
+ }
1010
+ } else {
1011
+ skipped.push({
1012
+ label: "bridge.preflight",
1013
+ detail: "skipped because bridge mode could not be resolved"
1014
+ });
1015
+ }
1016
+ if (runtimeConfig) {
1017
+ const client = new PubApiClient(runtimeConfig.baseUrl, runtimeConfig.apiKey);
1018
+ try {
1019
+ await client.getPendingLive();
1020
+ passed.push({
1021
+ label: "api",
1022
+ detail: `authenticated and reachable at ${runtimeConfig.baseUrl}`
1023
+ });
1024
+ } catch (error) {
1025
+ failures.push({ label: "api", detail: formatApiError(error) });
1026
+ }
1027
+ } else {
1028
+ skipped.push({
1029
+ label: "api",
1030
+ detail: "skipped because configuration is unavailable"
1031
+ });
1032
+ }
1033
+ try {
1034
+ await stopOtherDaemons();
1035
+ passed.push({
1036
+ label: "daemon.cleanup",
1037
+ detail: "no stale daemon conflicts detected"
1038
+ });
1039
+ } catch (error) {
1040
+ failures.push({ label: "daemon.cleanup", detail: errorMessage(error) });
1041
+ }
1042
+ if (failures.length > 0) {
1043
+ throw new Error(formatPreflightError({ failures, passed, skipped }));
1044
+ }
1045
+ if (!runtimeConfig || !bridgeSelection) {
1046
+ throw new Error(
1047
+ "Start preflight failed: internal error while resolving runtime configuration."
1048
+ );
1049
+ }
1050
+ return {
1051
+ runtimeConfig,
1052
+ bridgeMode: bridgeSelection.mode,
1053
+ bridgeProcessEnv,
1054
+ bridgeSelection,
1055
+ passedChecks: passed.map((entry) => `[${entry.label}] ${entry.detail}`)
1056
+ };
1057
+ }
1058
+
451
1059
  // package.json
452
1060
  var package_default = {
453
1061
  name: "pubblue",
454
- version: "0.6.4",
455
- description: "CLI tool for publishing content and running interactive sessions via pub.blue",
1062
+ version: "0.6.9",
1063
+ description: "CLI for publishing and visualizing AI-agent output via pub.blue",
456
1064
  type: "module",
457
1065
  bin: {
458
1066
  pubblue: "./dist/index.js"
@@ -466,6 +1074,7 @@ var package_default = {
466
1074
  },
467
1075
  dependencies: {
468
1076
  commander: "^13.0.0",
1077
+ convex: "^1.19.0",
469
1078
  "node-datachannel": "^0.32.0"
470
1079
  },
471
1080
  devDependencies: {
@@ -510,36 +1119,34 @@ function registerLiveCommands(program2) {
510
1119
  registerChannelsCommand(program2);
511
1120
  registerDoctorCommand(program2);
512
1121
  }
1122
+ function printLocalRuntimeSummary() {
1123
+ const saved = readConfig();
1124
+ const hasEnvApiKey = typeof process.env.PUBBLUE_API_KEY === "string" && process.env.PUBBLUE_API_KEY.trim().length > 0;
1125
+ const apiSource = hasEnvApiKey ? "PUBBLUE_API_KEY env" : saved?.apiKey ? "saved config" : "not configured";
1126
+ const baseUrl = process.env.PUBBLUE_URL?.trim() || DEFAULT_BASE_URL;
1127
+ console.log("Local runtime configuration:");
1128
+ console.log(` API key source: ${apiSource}`);
1129
+ console.log(` Base URL: ${baseUrl}`);
1130
+ const availability = detectBridgeAvailability();
1131
+ console.log("Bridge runtime availability:");
1132
+ for (const bridge of availability) {
1133
+ console.log(
1134
+ ` ${bridge.mode}: ${bridge.available ? "available" : "unavailable"} (${bridge.detail})`
1135
+ );
1136
+ }
1137
+ }
513
1138
  function registerStartCommand(program2) {
514
- 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) => {
515
- await ensureNodeDatachannelAvailable();
1139
+ 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|claude-code").action(async (opts) => {
516
1140
  writeLatestCliVersion(CLI_VERSION);
517
- const runtimeConfig = getConfig();
518
- const apiClient = createClient(runtimeConfig);
519
- const bridgeMode = resolveBridgeMode(opts);
520
- const bridgeProcessEnv = buildBridgeProcessEnv(runtimeConfig.bridge);
1141
+ const preflight = await runStartPreflight({ bridge: opts.bridge });
1142
+ const { runtimeConfig, bridgeMode, bridgeProcessEnv } = preflight;
1143
+ console.log("Preflight checks passed:");
1144
+ for (const line of preflight.passedChecks) {
1145
+ console.log(` ${line}`);
1146
+ }
521
1147
  const socketPath = getAgentSocketPath();
522
1148
  const infoPath = liveInfoPath("agent");
523
1149
  const logPath = liveLogPath("agent");
524
- await stopOtherDaemons();
525
- if (opts.foreground) {
526
- const { startDaemon } = await import("./live-daemon-EEIBVVBU.js");
527
- console.log("Agent daemon starting in foreground...");
528
- console.log("Press Ctrl+C to stop.");
529
- try {
530
- await startDaemon({
531
- cliVersion: CLI_VERSION,
532
- apiClient,
533
- socketPath,
534
- infoPath,
535
- bridgeMode,
536
- agentName: opts.agentName
537
- });
538
- } catch (error) {
539
- failCli(`Daemon failed: ${errorMessage(error)}`);
540
- }
541
- return;
542
- }
543
1150
  const { fork } = await import("child_process");
544
1151
  const daemonScript = path3.join(import.meta.dirname, "live-daemon-entry.js");
545
1152
  const daemonLogFd = fs3.openSync(logPath, "a");
@@ -574,7 +1181,12 @@ function registerStartCommand(program2) {
574
1181
  `Daemon failed to start: ${ready.reason ?? "unknown reason"}`,
575
1182
  `Daemon log: ${logPath}`
576
1183
  ];
577
- const tail = readLogTail(logPath);
1184
+ let tail = null;
1185
+ try {
1186
+ tail = readLogTail(logPath);
1187
+ } catch (error) {
1188
+ lines.push(`Failed to read daemon log tail: ${errorMessage(error)}`);
1189
+ }
578
1190
  if (tail) {
579
1191
  lines.push("---- daemon log tail ----");
580
1192
  lines.push(tail.trimEnd());
@@ -603,17 +1215,25 @@ function registerStatusCommand(program2) {
603
1215
  let response;
604
1216
  try {
605
1217
  response = await ipcCall(socketPath, { method: "status", params: {} });
606
- } catch {
1218
+ } catch (error) {
1219
+ if (errorMessage(error) !== "Daemon not running.") {
1220
+ failCli(`Failed to fetch daemon status: ${errorMessage(error)}`);
1221
+ }
607
1222
  console.log("Agent daemon is not running.");
1223
+ printLocalRuntimeSummary();
608
1224
  return;
609
1225
  }
610
- const activeSlug = response.activeSlug;
1226
+ if (!response.ok) {
1227
+ failCli(`Failed to fetch daemon status: ${response.error || "unknown error"}`);
1228
+ }
611
1229
  console.log(` Daemon: running`);
612
- console.log(` Active slug: ${activeSlug || "(none)"}`);
1230
+ console.log(` Active slug: ${response.activeSlug || "(none)"}`);
613
1231
  console.log(` Status: ${response.connected ? "connected" : "waiting"}`);
1232
+ if (typeof response.signalingConnected === "boolean") {
1233
+ console.log(` Signaling: ${response.signalingConnected ? "connected" : "reconnecting"}`);
1234
+ }
614
1235
  console.log(` Uptime: ${response.uptime}s`);
615
- const chNames = Array.isArray(response.channels) ? response.channels.map((c) => typeof c === "string" ? c : String(c)) : [];
616
- console.log(` Channels: ${chNames.join(", ") || "(none)"}`);
1236
+ console.log(` Channels: ${response.channels.join(", ") || "(none)"}`);
617
1237
  console.log(` Buffered: ${response.bufferedMessages ?? 0} messages`);
618
1238
  if (typeof response.lastError === "string" && response.lastError.length > 0) {
619
1239
  console.log(` Last error: ${response.lastError}`);
@@ -624,7 +1244,8 @@ function registerStatusCommand(program2) {
624
1244
  }
625
1245
  const bridge = response.bridge;
626
1246
  if (bridge) {
627
- console.log(` Bridge: openclaw (${bridge.running ? "running" : "stopped"})`);
1247
+ const bridgeLabel = response.bridgeMode ?? "unknown";
1248
+ console.log(` Bridge: ${bridgeLabel} (${bridge.running ? "running" : "stopped"})`);
628
1249
  if (bridge.sessionId) {
629
1250
  console.log(` Bridge session: ${bridge.sessionId}`);
630
1251
  }
@@ -764,10 +1385,12 @@ function registerChannelsCommand(program2) {
764
1385
  function registerDoctorCommand(program2) {
765
1386
  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(
766
1387
  async (opts) => {
767
- const timeoutSeconds = parsePositiveIntegerOption(opts.timeout, "--timeout");
1388
+ const timeoutSeconds = parsePositiveInteger(opts.timeout, "--timeout");
768
1389
  const timeoutMs = timeoutSeconds * 1e3;
769
1390
  const socketPath = getAgentSocketPath();
770
- const slug = await resolveActiveSlug();
1391
+ const slug = await resolveActiveSlug().catch(
1392
+ (error) => failCli(`No active daemon. Run \`pubblue start\` first. (${errorMessage(error)})`)
1393
+ );
771
1394
  const apiClient = createClient();
772
1395
  const fail = (message) => failCli(`Doctor failed: ${message}`);
773
1396
  console.log(`Doctor: ${slug}`);
@@ -782,7 +1405,13 @@ function registerDoctorCommand(program2) {
782
1405
  fail("daemon is running but browser is not connected.");
783
1406
  }
784
1407
  const channelNames = Array.isArray(statusResponse.channels) ? statusResponse.channels.map((entry) => String(entry)) : [];
785
- for (const required of [CONTROL_CHANNEL, CHANNELS.CHAT, CHANNELS.CANVAS]) {
1408
+ for (const required of [
1409
+ CONTROL_CHANNEL,
1410
+ CHANNELS.CHAT,
1411
+ CHANNELS.CANVAS,
1412
+ CHANNELS.RENDER_ERROR,
1413
+ CHANNELS.COMMAND
1414
+ ]) {
786
1415
  if (!channelNames.includes(required)) {
787
1416
  fail(`required channel is missing: ${required}`);
788
1417
  }