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/chunk-JSX5KHV3.js +1659 -0
- package/dist/index.js +982 -353
- package/dist/live-daemon-entry.js +2023 -6
- package/package.json +3 -2
- package/dist/chunk-JXEXE632.js +0 -608
- package/dist/chunk-QFJDLFK5.js +0 -1366
- package/dist/live-daemon-EEIBVVBU.js +0 -7
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
|
-
|
|
7
|
-
buildBridgeProcessEnv,
|
|
8
|
-
buildDaemonForkStdio,
|
|
9
|
-
ensureNodeDatachannelAvailable,
|
|
7
|
+
PubApiError,
|
|
10
8
|
errorMessage,
|
|
11
9
|
failCli,
|
|
12
|
-
formatApiError,
|
|
13
10
|
generateMessageId,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
getConfig,
|
|
12
|
+
getConfigDir,
|
|
13
|
+
getTelegramMiniAppUrl,
|
|
14
|
+
isClaudeCodeAvailableInEnv,
|
|
15
|
+
isOpenClawAvailable,
|
|
16
|
+
liveInfoDir,
|
|
19
17
|
liveInfoPath,
|
|
20
18
|
liveLogPath,
|
|
21
|
-
|
|
22
|
-
parsePositiveIntegerOption,
|
|
19
|
+
readConfig,
|
|
23
20
|
readLogTail,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
resolveOpenClawHome,
|
|
22
|
+
resolveOpenClawWorkspaceDir,
|
|
23
|
+
runClaudeCodeBridgeStartupProbe,
|
|
24
|
+
runOpenClawBridgeStartupProbe,
|
|
25
|
+
saveConfig,
|
|
27
26
|
toCliFailure,
|
|
28
|
-
waitForDaemonReady,
|
|
29
27
|
writeLatestCliVersion
|
|
30
|
-
} from "./chunk-
|
|
28
|
+
} from "./chunk-JSX5KHV3.js";
|
|
31
29
|
|
|
32
30
|
// src/program.ts
|
|
33
31
|
import { Command } from "commander";
|
|
34
32
|
|
|
35
|
-
// src/commands/
|
|
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 =
|
|
125
|
-
if (!
|
|
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:
|
|
130
|
-
basename:
|
|
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
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
321
|
-
|
|
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("
|
|
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, []).
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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.
|
|
455
|
-
description: "CLI
|
|
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|
|
|
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
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 [
|
|
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
|
}
|