pubblue 0.4.12 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-BBJOOZHS.js +676 -0
- package/dist/chunk-WXNNDR4T.js +1313 -0
- package/dist/index.js +457 -1265
- package/dist/tunnel-daemon-BR5XKNEA.js +7 -0
- package/dist/tunnel-daemon-entry.js +14 -12
- package/package.json +4 -4
- package/dist/chunk-4YTJ2WKF.js +0 -60
- package/dist/chunk-7NFHPJ76.js +0 -79
- package/dist/chunk-HJ5LTUHS.js +0 -56
- package/dist/chunk-UW7JILRJ.js +0 -677
- package/dist/tunnel-bridge-entry.d.ts +0 -2
- package/dist/tunnel-bridge-entry.js +0 -703
- package/dist/tunnel-daemon-RKWEA5BV.js +0 -14
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
// src/lib/cli-error.ts
|
|
2
|
+
import { CommanderError } from "commander";
|
|
3
|
+
var CliError = class extends Error {
|
|
4
|
+
exitCode;
|
|
5
|
+
constructor(message, exitCode = 1) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "CliError";
|
|
8
|
+
this.exitCode = exitCode;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
function failCli(message, exitCode = 1) {
|
|
12
|
+
throw new CliError(message, exitCode);
|
|
13
|
+
}
|
|
14
|
+
function toCliFailure(error) {
|
|
15
|
+
if (error instanceof CommanderError) {
|
|
16
|
+
return {
|
|
17
|
+
exitCode: error.exitCode,
|
|
18
|
+
message: ""
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
if (error instanceof CliError) {
|
|
22
|
+
return {
|
|
23
|
+
exitCode: error.exitCode,
|
|
24
|
+
message: error.message
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (error instanceof Error) {
|
|
28
|
+
return {
|
|
29
|
+
exitCode: 1,
|
|
30
|
+
message: error.message
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
exitCode: 1,
|
|
35
|
+
message: String(error)
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// src/lib/api.ts
|
|
40
|
+
var PubApiError = class extends Error {
|
|
41
|
+
constructor(message, status, retryAfterSeconds) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.status = status;
|
|
44
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
45
|
+
this.name = "PubApiError";
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
var PubApiClient = class {
|
|
49
|
+
constructor(baseUrl, apiKey) {
|
|
50
|
+
this.baseUrl = baseUrl;
|
|
51
|
+
this.apiKey = apiKey;
|
|
52
|
+
}
|
|
53
|
+
async request(path3, options = {}) {
|
|
54
|
+
const url = new URL(path3, this.baseUrl);
|
|
55
|
+
const res = await fetch(url, {
|
|
56
|
+
...options,
|
|
57
|
+
headers: {
|
|
58
|
+
"Content-Type": "application/json",
|
|
59
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
60
|
+
...options.headers
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
const retryAfterHeader = res.headers.get("Retry-After");
|
|
64
|
+
const parsedRetryAfterSeconds = typeof retryAfterHeader === "string" ? Number.parseInt(retryAfterHeader, 10) : void 0;
|
|
65
|
+
const retryAfterSeconds = parsedRetryAfterSeconds !== void 0 && Number.isFinite(parsedRetryAfterSeconds) ? parsedRetryAfterSeconds : void 0;
|
|
66
|
+
let data;
|
|
67
|
+
try {
|
|
68
|
+
data = await res.json();
|
|
69
|
+
} catch {
|
|
70
|
+
data = {};
|
|
71
|
+
}
|
|
72
|
+
if (!res.ok) {
|
|
73
|
+
if (res.status === 429) {
|
|
74
|
+
const retrySuffix = retryAfterSeconds !== void 0 ? ` Retry after ${retryAfterSeconds}s.` : "";
|
|
75
|
+
throw new PubApiError(`Rate limit exceeded.${retrySuffix}`, res.status, retryAfterSeconds);
|
|
76
|
+
}
|
|
77
|
+
throw new PubApiError(data.error || `Request failed with status ${res.status}`, res.status);
|
|
78
|
+
}
|
|
79
|
+
return data;
|
|
80
|
+
}
|
|
81
|
+
// -- Pub CRUD -------------------------------------------------------------
|
|
82
|
+
async create(opts) {
|
|
83
|
+
return this.request("/api/v1/pubs", {
|
|
84
|
+
method: "POST",
|
|
85
|
+
body: JSON.stringify(opts)
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async get(slug) {
|
|
89
|
+
const data = await this.request(`/api/v1/pubs/${encodeURIComponent(slug)}`);
|
|
90
|
+
return data.pub;
|
|
91
|
+
}
|
|
92
|
+
async listPage(cursor, limit) {
|
|
93
|
+
const params = new URLSearchParams();
|
|
94
|
+
if (cursor) params.set("cursor", cursor);
|
|
95
|
+
if (limit) params.set("limit", String(limit));
|
|
96
|
+
const qs = params.toString();
|
|
97
|
+
return this.request(`/api/v1/pubs${qs ? `?${qs}` : ""}`);
|
|
98
|
+
}
|
|
99
|
+
async list() {
|
|
100
|
+
const all = [];
|
|
101
|
+
let cursor;
|
|
102
|
+
do {
|
|
103
|
+
const result = await this.listPage(cursor, 100);
|
|
104
|
+
all.push(...result.pubs);
|
|
105
|
+
cursor = result.hasMore ? result.cursor : void 0;
|
|
106
|
+
} while (cursor);
|
|
107
|
+
return all;
|
|
108
|
+
}
|
|
109
|
+
async update(opts) {
|
|
110
|
+
const { slug, newSlug, ...rest } = opts;
|
|
111
|
+
const body = { ...rest };
|
|
112
|
+
if (newSlug) body.slug = newSlug;
|
|
113
|
+
return this.request(`/api/v1/pubs/${encodeURIComponent(slug)}`, {
|
|
114
|
+
method: "PATCH",
|
|
115
|
+
body: JSON.stringify(body)
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
async remove(slug) {
|
|
119
|
+
await this.request(`/api/v1/pubs/${encodeURIComponent(slug)}`, {
|
|
120
|
+
method: "DELETE"
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
// -- Agent presence -------------------------------------------------------
|
|
124
|
+
async goOnline() {
|
|
125
|
+
await this.request("/api/v1/agent/online", { method: "POST" });
|
|
126
|
+
}
|
|
127
|
+
async heartbeat() {
|
|
128
|
+
await this.request("/api/v1/agent/heartbeat", { method: "POST" });
|
|
129
|
+
}
|
|
130
|
+
async goOffline() {
|
|
131
|
+
await this.request("/api/v1/agent/offline", { method: "POST" });
|
|
132
|
+
}
|
|
133
|
+
// -- Agent live management ------------------------------------------------
|
|
134
|
+
async getPendingLive() {
|
|
135
|
+
const data = await this.request("/api/v1/agent/live");
|
|
136
|
+
return data.live;
|
|
137
|
+
}
|
|
138
|
+
async signalAnswer(opts) {
|
|
139
|
+
await this.request("/api/v1/agent/live/signal", {
|
|
140
|
+
method: "PATCH",
|
|
141
|
+
body: JSON.stringify(opts)
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
async closeActiveLive() {
|
|
145
|
+
await this.request("/api/v1/agent/live", { method: "DELETE" });
|
|
146
|
+
}
|
|
147
|
+
// -- Per-slug live info ---------------------------------------------------
|
|
148
|
+
async getLive(slug) {
|
|
149
|
+
const data = await this.request(
|
|
150
|
+
`/api/v1/pubs/${encodeURIComponent(slug)}/live`
|
|
151
|
+
);
|
|
152
|
+
return data.live;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// ../shared/bridge-protocol-core.ts
|
|
157
|
+
var CONTROL_CHANNEL = "_control";
|
|
158
|
+
var CHANNELS = {
|
|
159
|
+
CHAT: "chat",
|
|
160
|
+
CANVAS: "canvas",
|
|
161
|
+
AUDIO: "audio",
|
|
162
|
+
MEDIA: "media",
|
|
163
|
+
FILE: "file"
|
|
164
|
+
};
|
|
165
|
+
var idCounter = 0;
|
|
166
|
+
function generateMessageId() {
|
|
167
|
+
const ts = Date.now().toString(36);
|
|
168
|
+
const seq = (idCounter++).toString(36);
|
|
169
|
+
const rand = Math.random().toString(36).slice(2, 6);
|
|
170
|
+
return `${ts}-${seq}-${rand}`;
|
|
171
|
+
}
|
|
172
|
+
function encodeMessage(msg) {
|
|
173
|
+
return JSON.stringify(msg);
|
|
174
|
+
}
|
|
175
|
+
function decodeMessage(raw) {
|
|
176
|
+
try {
|
|
177
|
+
const parsed = JSON.parse(raw);
|
|
178
|
+
if (parsed && typeof parsed.id === "string" && typeof parsed.type === "string") {
|
|
179
|
+
return parsed;
|
|
180
|
+
}
|
|
181
|
+
return null;
|
|
182
|
+
} catch {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function makeEventMessage(event, meta) {
|
|
187
|
+
return { id: generateMessageId(), type: "event", data: event, meta };
|
|
188
|
+
}
|
|
189
|
+
function makeAckMessage(messageId, channel) {
|
|
190
|
+
return makeEventMessage("ack", { messageId, channel, receivedAt: Date.now() });
|
|
191
|
+
}
|
|
192
|
+
function parseAckMessage(msg) {
|
|
193
|
+
if (msg.type !== "event" || msg.data !== "ack" || !msg.meta) return null;
|
|
194
|
+
const messageId = typeof msg.meta.messageId === "string" ? msg.meta.messageId : null;
|
|
195
|
+
const channel = typeof msg.meta.channel === "string" ? msg.meta.channel : null;
|
|
196
|
+
if (!messageId || !channel) return null;
|
|
197
|
+
const receivedAt = typeof msg.meta.receivedAt === "number" ? msg.meta.receivedAt : void 0;
|
|
198
|
+
return { messageId, channel, receivedAt };
|
|
199
|
+
}
|
|
200
|
+
function shouldAcknowledgeMessage(channel, msg) {
|
|
201
|
+
return channel !== CONTROL_CHANNEL && parseAckMessage(msg) === null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// src/commands/tunnel-helpers.ts
|
|
205
|
+
import * as fs2 from "fs";
|
|
206
|
+
import { homedir as homedir2 } from "os";
|
|
207
|
+
import * as path2 from "path";
|
|
208
|
+
|
|
209
|
+
// src/lib/config.ts
|
|
210
|
+
import * as fs from "fs";
|
|
211
|
+
import * as os from "os";
|
|
212
|
+
import * as path from "path";
|
|
213
|
+
var DEFAULT_BASE_URL = "https://silent-guanaco-514.convex.site";
|
|
214
|
+
function getConfigDir(homeDir) {
|
|
215
|
+
const home = homeDir || os.homedir();
|
|
216
|
+
return path.join(home, ".config", "pubblue");
|
|
217
|
+
}
|
|
218
|
+
function getConfigPath(homeDir) {
|
|
219
|
+
const dir = getConfigDir(homeDir);
|
|
220
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
221
|
+
try {
|
|
222
|
+
fs.chmodSync(dir, 448);
|
|
223
|
+
} catch {
|
|
224
|
+
}
|
|
225
|
+
return path.join(dir, "config.json");
|
|
226
|
+
}
|
|
227
|
+
function loadConfig(homeDir) {
|
|
228
|
+
const configPath = getConfigPath(homeDir);
|
|
229
|
+
if (!fs.existsSync(configPath)) return null;
|
|
230
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
231
|
+
return JSON.parse(raw);
|
|
232
|
+
}
|
|
233
|
+
function saveConfig(config, homeDir) {
|
|
234
|
+
const configPath = getConfigPath(homeDir);
|
|
235
|
+
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}
|
|
236
|
+
`, {
|
|
237
|
+
mode: 384
|
|
238
|
+
});
|
|
239
|
+
try {
|
|
240
|
+
fs.chmodSync(configPath, 384);
|
|
241
|
+
} catch {
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
function getConfig(homeDir) {
|
|
245
|
+
const envKey = process.env.PUBBLUE_API_KEY;
|
|
246
|
+
const envUrl = process.env.PUBBLUE_URL;
|
|
247
|
+
const baseUrl = envUrl || DEFAULT_BASE_URL;
|
|
248
|
+
const saved = loadConfig(homeDir);
|
|
249
|
+
if (envKey) {
|
|
250
|
+
return { apiKey: envKey, baseUrl, bridge: saved?.bridge };
|
|
251
|
+
}
|
|
252
|
+
if (!saved) {
|
|
253
|
+
throw new Error(
|
|
254
|
+
"Not configured. Run `pubblue configure` or set PUBBLUE_API_KEY environment variable."
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
apiKey: saved.apiKey,
|
|
259
|
+
baseUrl,
|
|
260
|
+
bridge: saved.bridge
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function getTelegramMiniAppUrl(slug) {
|
|
264
|
+
const saved = loadConfig();
|
|
265
|
+
if (!saved?.telegram?.botUsername) return null;
|
|
266
|
+
return `https://t.me/${saved.telegram.botUsername}?startapp=${slug}`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// src/lib/tunnel-ipc.ts
|
|
270
|
+
import * as net from "net";
|
|
271
|
+
function getAgentSocketPath() {
|
|
272
|
+
return "/tmp/pubblue-agent.sock";
|
|
273
|
+
}
|
|
274
|
+
async function ipcCall(socketPath, request) {
|
|
275
|
+
return new Promise((resolve, reject) => {
|
|
276
|
+
let settled = false;
|
|
277
|
+
let timeoutId = null;
|
|
278
|
+
const finish = (fn) => {
|
|
279
|
+
if (settled) return;
|
|
280
|
+
settled = true;
|
|
281
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
282
|
+
fn();
|
|
283
|
+
};
|
|
284
|
+
const client = net.createConnection(socketPath, () => {
|
|
285
|
+
client.write(`${JSON.stringify(request)}
|
|
286
|
+
`);
|
|
287
|
+
});
|
|
288
|
+
let data = "";
|
|
289
|
+
client.on("data", (chunk) => {
|
|
290
|
+
data += chunk.toString();
|
|
291
|
+
const newlineIdx = data.indexOf("\n");
|
|
292
|
+
if (newlineIdx !== -1) {
|
|
293
|
+
const line = data.slice(0, newlineIdx);
|
|
294
|
+
client.end();
|
|
295
|
+
try {
|
|
296
|
+
finish(() => resolve(JSON.parse(line)));
|
|
297
|
+
} catch {
|
|
298
|
+
finish(() => reject(new Error("Invalid response from daemon")));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
client.on("error", (err) => {
|
|
303
|
+
if (err.code === "ECONNREFUSED" || err.code === "ENOENT") {
|
|
304
|
+
finish(() => reject(new Error("Daemon not running.")));
|
|
305
|
+
} else {
|
|
306
|
+
finish(() => reject(err));
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
client.on("end", () => {
|
|
310
|
+
if (!data.includes("\n")) {
|
|
311
|
+
finish(() => reject(new Error("Daemon closed connection unexpectedly")));
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
timeoutId = setTimeout(() => {
|
|
315
|
+
client.destroy();
|
|
316
|
+
finish(() => reject(new Error("Daemon request timed out")));
|
|
317
|
+
}, 1e4);
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/commands/tunnel-helpers.ts
|
|
322
|
+
var TEXT_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
323
|
+
".txt",
|
|
324
|
+
".md",
|
|
325
|
+
".markdown",
|
|
326
|
+
".json",
|
|
327
|
+
".csv",
|
|
328
|
+
".xml",
|
|
329
|
+
".yaml",
|
|
330
|
+
".yml",
|
|
331
|
+
".js",
|
|
332
|
+
".mjs",
|
|
333
|
+
".cjs",
|
|
334
|
+
".ts",
|
|
335
|
+
".tsx",
|
|
336
|
+
".jsx",
|
|
337
|
+
".css",
|
|
338
|
+
".scss",
|
|
339
|
+
".sass",
|
|
340
|
+
".less",
|
|
341
|
+
".log"
|
|
342
|
+
]);
|
|
343
|
+
function getMimeType(filePath) {
|
|
344
|
+
const ext = path2.extname(filePath).toLowerCase();
|
|
345
|
+
const mimeByExt = {
|
|
346
|
+
".html": "text/html; charset=utf-8",
|
|
347
|
+
".htm": "text/html; charset=utf-8",
|
|
348
|
+
".txt": "text/plain; charset=utf-8",
|
|
349
|
+
".md": "text/markdown; charset=utf-8",
|
|
350
|
+
".markdown": "text/markdown; charset=utf-8",
|
|
351
|
+
".json": "application/json",
|
|
352
|
+
".csv": "text/csv; charset=utf-8",
|
|
353
|
+
".xml": "application/xml",
|
|
354
|
+
".yaml": "application/x-yaml",
|
|
355
|
+
".yml": "application/x-yaml",
|
|
356
|
+
".png": "image/png",
|
|
357
|
+
".jpg": "image/jpeg",
|
|
358
|
+
".jpeg": "image/jpeg",
|
|
359
|
+
".gif": "image/gif",
|
|
360
|
+
".webp": "image/webp",
|
|
361
|
+
".svg": "image/svg+xml",
|
|
362
|
+
".pdf": "application/pdf",
|
|
363
|
+
".zip": "application/zip",
|
|
364
|
+
".mp3": "audio/mpeg",
|
|
365
|
+
".wav": "audio/wav",
|
|
366
|
+
".mp4": "video/mp4"
|
|
367
|
+
};
|
|
368
|
+
return mimeByExt[ext] || "application/octet-stream";
|
|
369
|
+
}
|
|
370
|
+
function liveInfoDir() {
|
|
371
|
+
const dir = path2.join(
|
|
372
|
+
process.env.HOME || process.env.USERPROFILE || "/tmp",
|
|
373
|
+
".config",
|
|
374
|
+
"pubblue",
|
|
375
|
+
"lives"
|
|
376
|
+
);
|
|
377
|
+
if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
|
|
378
|
+
return dir;
|
|
379
|
+
}
|
|
380
|
+
function liveInfoPath(slug) {
|
|
381
|
+
return path2.join(liveInfoDir(), `${slug}.json`);
|
|
382
|
+
}
|
|
383
|
+
function liveLogPath(slug) {
|
|
384
|
+
return path2.join(liveInfoDir(), `${slug}.log`);
|
|
385
|
+
}
|
|
386
|
+
function createApiClient(configOverride) {
|
|
387
|
+
const config = configOverride || getConfig();
|
|
388
|
+
return new PubApiClient(config.baseUrl, config.apiKey);
|
|
389
|
+
}
|
|
390
|
+
function buildBridgeProcessEnv(bridgeConfig) {
|
|
391
|
+
const env = { ...process.env };
|
|
392
|
+
const setIfMissing = (key, value) => {
|
|
393
|
+
if (value === void 0) return;
|
|
394
|
+
const current = env[key];
|
|
395
|
+
if (typeof current === "string" && current.length > 0) return;
|
|
396
|
+
env[key] = String(value);
|
|
397
|
+
};
|
|
398
|
+
setIfMissing("PUBBLUE_PROJECT_ROOT", process.cwd());
|
|
399
|
+
setIfMissing("OPENCLAW_HOME", homedir2());
|
|
400
|
+
if (!bridgeConfig) return env;
|
|
401
|
+
setIfMissing("OPENCLAW_PATH", bridgeConfig.openclawPath);
|
|
402
|
+
setIfMissing("OPENCLAW_SESSION_ID", bridgeConfig.sessionId);
|
|
403
|
+
setIfMissing("OPENCLAW_THREAD_ID", bridgeConfig.threadId);
|
|
404
|
+
setIfMissing("OPENCLAW_CANVAS_REMINDER_EVERY", bridgeConfig.canvasReminderEvery);
|
|
405
|
+
setIfMissing(
|
|
406
|
+
"OPENCLAW_DELIVER",
|
|
407
|
+
bridgeConfig.deliver === void 0 ? void 0 : bridgeConfig.deliver ? "1" : "0"
|
|
408
|
+
);
|
|
409
|
+
setIfMissing("OPENCLAW_DELIVER_CHANNEL", bridgeConfig.deliverChannel);
|
|
410
|
+
setIfMissing("OPENCLAW_REPLY_TO", bridgeConfig.replyTo);
|
|
411
|
+
setIfMissing("OPENCLAW_DELIVER_TIMEOUT_MS", bridgeConfig.deliverTimeoutMs);
|
|
412
|
+
setIfMissing("OPENCLAW_ATTACHMENT_DIR", bridgeConfig.attachmentDir);
|
|
413
|
+
setIfMissing("OPENCLAW_ATTACHMENT_MAX_BYTES", bridgeConfig.attachmentMaxBytes);
|
|
414
|
+
return env;
|
|
415
|
+
}
|
|
416
|
+
async function ensureNodeDatachannelAvailable() {
|
|
417
|
+
try {
|
|
418
|
+
await import("node-datachannel");
|
|
419
|
+
} catch (error) {
|
|
420
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
421
|
+
failCli(
|
|
422
|
+
[
|
|
423
|
+
"node-datachannel native module is not available.",
|
|
424
|
+
"Run `pnpm rebuild node-datachannel` in the cli package and retry.",
|
|
425
|
+
`Details: ${message}`
|
|
426
|
+
].join("\n")
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
function isDaemonRunning(slug) {
|
|
431
|
+
return readDaemonProcessInfo(slug) !== null;
|
|
432
|
+
}
|
|
433
|
+
function readDaemonProcessInfo(slug) {
|
|
434
|
+
const infoPath = liveInfoPath(slug);
|
|
435
|
+
if (!fs2.existsSync(infoPath)) return null;
|
|
436
|
+
try {
|
|
437
|
+
const info = JSON.parse(fs2.readFileSync(infoPath, "utf-8"));
|
|
438
|
+
if (!Number.isFinite(info.pid)) throw new Error("invalid daemon pid");
|
|
439
|
+
if (!isProcessAlive(info.pid)) throw new Error("process not alive");
|
|
440
|
+
return info;
|
|
441
|
+
} catch {
|
|
442
|
+
try {
|
|
443
|
+
fs2.unlinkSync(infoPath);
|
|
444
|
+
} catch {
|
|
445
|
+
}
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function latestCliVersionPath() {
|
|
450
|
+
return path2.join(liveInfoDir(), "cli-version.txt");
|
|
451
|
+
}
|
|
452
|
+
function readLatestCliVersion(versionPath) {
|
|
453
|
+
const resolved = versionPath || latestCliVersionPath();
|
|
454
|
+
if (!fs2.existsSync(resolved)) return null;
|
|
455
|
+
try {
|
|
456
|
+
const value = fs2.readFileSync(resolved, "utf-8").trim();
|
|
457
|
+
return value.length === 0 ? null : value;
|
|
458
|
+
} catch {
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
function writeLatestCliVersion(version, versionPath) {
|
|
463
|
+
if (!version || version.trim().length === 0) return;
|
|
464
|
+
const resolved = versionPath || latestCliVersionPath();
|
|
465
|
+
const dir = path2.dirname(resolved);
|
|
466
|
+
if (!fs2.existsSync(dir)) fs2.mkdirSync(dir, { recursive: true });
|
|
467
|
+
fs2.writeFileSync(resolved, version.trim(), "utf-8");
|
|
468
|
+
}
|
|
469
|
+
function isProcessAlive(pid) {
|
|
470
|
+
try {
|
|
471
|
+
process.kill(pid, 0);
|
|
472
|
+
return true;
|
|
473
|
+
} catch {
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
async function waitForProcessExit(pid, timeoutMs) {
|
|
478
|
+
const startedAt = Date.now();
|
|
479
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
480
|
+
if (!isProcessAlive(pid)) return true;
|
|
481
|
+
await new Promise((resolve) => setTimeout(resolve, 120));
|
|
482
|
+
}
|
|
483
|
+
return !isProcessAlive(pid);
|
|
484
|
+
}
|
|
485
|
+
async function stopDaemonForLive(info) {
|
|
486
|
+
const pid = info.pid;
|
|
487
|
+
if (!Number.isFinite(pid) || !isProcessAlive(pid)) return null;
|
|
488
|
+
const socketPath = info.socketPath;
|
|
489
|
+
if (socketPath) {
|
|
490
|
+
try {
|
|
491
|
+
await ipcCall(socketPath, { method: "close", params: {} });
|
|
492
|
+
} catch (error) {
|
|
493
|
+
try {
|
|
494
|
+
process.kill(pid, "SIGTERM");
|
|
495
|
+
} catch (killError) {
|
|
496
|
+
return `daemon ${pid}: IPC close failed (${error instanceof Error ? error.message : String(error)}); SIGTERM failed (${killError instanceof Error ? killError.message : String(killError)})`;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
} else {
|
|
500
|
+
try {
|
|
501
|
+
process.kill(pid, "SIGTERM");
|
|
502
|
+
} catch (error) {
|
|
503
|
+
return `daemon ${pid}: no socketPath and SIGTERM failed (${error instanceof Error ? error.message : String(error)})`;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
const stopped = await waitForProcessExit(pid, 8e3);
|
|
507
|
+
if (!stopped) return `daemon ${pid}: did not exit after stop request`;
|
|
508
|
+
return null;
|
|
509
|
+
}
|
|
510
|
+
async function stopOtherDaemons() {
|
|
511
|
+
const dir = liveInfoDir();
|
|
512
|
+
const entries = fs2.readdirSync(dir).filter((name) => name.endsWith(".json"));
|
|
513
|
+
const failures = [];
|
|
514
|
+
for (const entry of entries) {
|
|
515
|
+
const slug = entry.replace(/\.json$/, "");
|
|
516
|
+
const info = readDaemonProcessInfo(slug);
|
|
517
|
+
if (!info) continue;
|
|
518
|
+
const daemonError = await stopDaemonForLive(info);
|
|
519
|
+
if (daemonError) failures.push(`[${slug}] ${daemonError}`);
|
|
520
|
+
}
|
|
521
|
+
if (failures.length > 0) {
|
|
522
|
+
throw new Error(
|
|
523
|
+
[
|
|
524
|
+
"Critical: failed to stop previous live daemon processes.",
|
|
525
|
+
"Starting a new daemon now would leak resources and increase bandwidth usage.",
|
|
526
|
+
...failures
|
|
527
|
+
].join("\n")
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
function getFollowReadDelayMs(disconnected, consecutiveFailures) {
|
|
532
|
+
if (!disconnected) return 1e3;
|
|
533
|
+
return Math.min(5e3, 1e3 * 2 ** Math.min(consecutiveFailures, 3));
|
|
534
|
+
}
|
|
535
|
+
function buildDaemonForkStdio(logFd) {
|
|
536
|
+
return ["ignore", logFd, logFd, "ipc"];
|
|
537
|
+
}
|
|
538
|
+
function parsePositiveIntegerOption(raw, optionName) {
|
|
539
|
+
const parsed = Number.parseInt(raw, 10);
|
|
540
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
541
|
+
throw new Error(`${optionName} must be a positive integer. Received: ${raw}`);
|
|
542
|
+
}
|
|
543
|
+
return parsed;
|
|
544
|
+
}
|
|
545
|
+
function parseBridgeMode(raw) {
|
|
546
|
+
const normalized = raw.trim().toLowerCase();
|
|
547
|
+
if (normalized === "openclaw" || normalized === "none") {
|
|
548
|
+
return normalized;
|
|
549
|
+
}
|
|
550
|
+
throw new Error(`--bridge must be one of: openclaw, none. Received: ${raw}`);
|
|
551
|
+
}
|
|
552
|
+
function resolveBridgeMode(opts) {
|
|
553
|
+
return parseBridgeMode(opts.bridge || (opts.foreground ? "none" : "openclaw"));
|
|
554
|
+
}
|
|
555
|
+
function messageContainsPong(payload) {
|
|
556
|
+
if (!payload || typeof payload !== "object") return false;
|
|
557
|
+
const message = payload.msg;
|
|
558
|
+
if (!message || typeof message !== "object") return false;
|
|
559
|
+
const type = message.type;
|
|
560
|
+
const data = message.data;
|
|
561
|
+
return type === "text" && typeof data === "string" && data.trim().toLowerCase() === "pong";
|
|
562
|
+
}
|
|
563
|
+
function readLogTail(logPath, maxChars = 4e3) {
|
|
564
|
+
if (!fs2.existsSync(logPath)) return null;
|
|
565
|
+
try {
|
|
566
|
+
const content = fs2.readFileSync(logPath, "utf-8");
|
|
567
|
+
if (content.length <= maxChars) return content;
|
|
568
|
+
return content.slice(-maxChars);
|
|
569
|
+
} catch {
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
function formatApiError(error) {
|
|
574
|
+
if (error instanceof PubApiError) {
|
|
575
|
+
if (error.status === 429 && error.retryAfterSeconds !== void 0) {
|
|
576
|
+
return `Rate limit exceeded. Retry after ${error.retryAfterSeconds}s.`;
|
|
577
|
+
}
|
|
578
|
+
return `${error.message} (HTTP ${error.status})`;
|
|
579
|
+
}
|
|
580
|
+
return error instanceof Error ? error.message : String(error);
|
|
581
|
+
}
|
|
582
|
+
async function resolveActiveSlug() {
|
|
583
|
+
const socketPath = getAgentSocketPath();
|
|
584
|
+
let response;
|
|
585
|
+
try {
|
|
586
|
+
response = await ipcCall(socketPath, { method: "active-slug", params: {} });
|
|
587
|
+
} catch {
|
|
588
|
+
failCli("No active daemon. Run `pubblue start` first.");
|
|
589
|
+
}
|
|
590
|
+
if (response.ok && typeof response.slug === "string" && response.slug.length > 0) {
|
|
591
|
+
return response.slug;
|
|
592
|
+
}
|
|
593
|
+
failCli("Daemon is running but no live is active. Wait for browser to initiate live.");
|
|
594
|
+
}
|
|
595
|
+
function waitForDaemonReady({
|
|
596
|
+
child,
|
|
597
|
+
infoPath,
|
|
598
|
+
socketPath,
|
|
599
|
+
timeoutMs
|
|
600
|
+
}) {
|
|
601
|
+
return new Promise((resolve) => {
|
|
602
|
+
let settled = false;
|
|
603
|
+
let pollInFlight = false;
|
|
604
|
+
let lastIpcError = null;
|
|
605
|
+
const done = (result) => {
|
|
606
|
+
if (settled) return;
|
|
607
|
+
settled = true;
|
|
608
|
+
clearInterval(poll);
|
|
609
|
+
clearTimeout(timeout);
|
|
610
|
+
child.off("exit", onExit);
|
|
611
|
+
resolve(result);
|
|
612
|
+
};
|
|
613
|
+
const onExit = (code, signal) => {
|
|
614
|
+
const suffix = signal ? ` (signal ${signal})` : "";
|
|
615
|
+
done({ ok: false, reason: `daemon exited with code ${code ?? 0}${suffix}` });
|
|
616
|
+
};
|
|
617
|
+
child.on("exit", onExit);
|
|
618
|
+
const poll = setInterval(() => {
|
|
619
|
+
if (pollInFlight || !fs2.existsSync(infoPath)) return;
|
|
620
|
+
pollInFlight = true;
|
|
621
|
+
void ipcCall(socketPath, { method: "status", params: {} }).then((status) => {
|
|
622
|
+
if (status.ok) done({ ok: true });
|
|
623
|
+
}).catch((error) => {
|
|
624
|
+
lastIpcError = error instanceof Error ? error.message : String(error);
|
|
625
|
+
}).finally(() => {
|
|
626
|
+
pollInFlight = false;
|
|
627
|
+
});
|
|
628
|
+
}, 120);
|
|
629
|
+
const timeout = setTimeout(() => {
|
|
630
|
+
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`;
|
|
631
|
+
done({ ok: false, reason });
|
|
632
|
+
}, timeoutMs);
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
export {
|
|
637
|
+
failCli,
|
|
638
|
+
toCliFailure,
|
|
639
|
+
loadConfig,
|
|
640
|
+
saveConfig,
|
|
641
|
+
getConfig,
|
|
642
|
+
getTelegramMiniAppUrl,
|
|
643
|
+
PubApiError,
|
|
644
|
+
PubApiClient,
|
|
645
|
+
CONTROL_CHANNEL,
|
|
646
|
+
CHANNELS,
|
|
647
|
+
generateMessageId,
|
|
648
|
+
encodeMessage,
|
|
649
|
+
decodeMessage,
|
|
650
|
+
makeAckMessage,
|
|
651
|
+
parseAckMessage,
|
|
652
|
+
shouldAcknowledgeMessage,
|
|
653
|
+
getAgentSocketPath,
|
|
654
|
+
ipcCall,
|
|
655
|
+
TEXT_FILE_EXTENSIONS,
|
|
656
|
+
getMimeType,
|
|
657
|
+
liveInfoPath,
|
|
658
|
+
liveLogPath,
|
|
659
|
+
createApiClient,
|
|
660
|
+
buildBridgeProcessEnv,
|
|
661
|
+
ensureNodeDatachannelAvailable,
|
|
662
|
+
isDaemonRunning,
|
|
663
|
+
latestCliVersionPath,
|
|
664
|
+
readLatestCliVersion,
|
|
665
|
+
writeLatestCliVersion,
|
|
666
|
+
stopOtherDaemons,
|
|
667
|
+
getFollowReadDelayMs,
|
|
668
|
+
buildDaemonForkStdio,
|
|
669
|
+
parsePositiveIntegerOption,
|
|
670
|
+
resolveBridgeMode,
|
|
671
|
+
messageContainsPong,
|
|
672
|
+
readLogTail,
|
|
673
|
+
formatApiError,
|
|
674
|
+
resolveActiveSlug,
|
|
675
|
+
waitForDaemonReady
|
|
676
|
+
};
|