@feynmanzhang/open-party 0.1.6 → 0.1.7
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/README.md +138 -0
- package/dist/claude-code/{open-party-0.1.6 → open-party-0.1.7}/.claude-plugin/plugin.json +1 -1
- package/dist/claude-code/open-party-0.1.7/BUILD_INFO.json +6 -0
- package/dist/claude-code/open-party-0.1.7/dist/dispatcher.js +187 -0
- package/dist/claude-code/{open-party-0.1.6 → open-party-0.1.7}/dist/hook-handler.js +58 -73
- package/dist/claude-code/{open-party-0.1.6 → open-party-0.1.7}/dist/mcp-server.js +552 -364
- package/dist/claude-code/{open-party-0.1.6 → open-party-0.1.7}/dist/party-server.js +426 -1657
- package/dist/claude-code/{open-party-0.1.6 → open-party-0.1.7}/hooks/hooks.json +39 -50
- package/dist/claude-code/{open-party-0.1.6 → open-party-0.1.7}/package.json +1 -1
- package/dist/claude-code/{open-party-0.1.6 → open-party-0.1.7}/skills/open-party/SKILL.md +39 -21
- package/dist/cli/index.js +1528 -2647
- package/dist/cli/index.js.map +1 -1
- package/dist/party-server.js +426 -1657
- package/dist/party-server.js.map +1 -1
- package/package.json +10 -13
- package/dist/claude-code/open-party-0.1.6/BUILD_INFO.json +0 -6
- package/dist/openclaw/open-party-0.1.5/BUILD_INFO.json +0 -6
- package/dist/openclaw/open-party-0.1.5/SKILL.md +0 -127
- package/dist/openclaw/open-party-0.1.5/dist/index.js +0 -550
- package/dist/openclaw/open-party-0.1.5/dist/party-server.js +0 -5502
- package/dist/openclaw/open-party-0.1.5/openclaw.plugin.json +0 -28
- package/dist/openclaw/open-party-0.1.5/package.json +0 -12
- package/dist/openclaw/open-party-0.1.5/skills/open-party/SKILL.md +0 -90
- package/dist/openclaw/open-party-0.1.6/BUILD_INFO.json +0 -6
- package/dist/openclaw/open-party-0.1.6/SKILL.md +0 -127
- package/dist/openclaw/open-party-0.1.6/dist/index.js +0 -550
- package/dist/openclaw/open-party-0.1.6/dist/party-server.js +0 -5502
- package/dist/openclaw/open-party-0.1.6/openclaw.plugin.json +0 -28
- package/dist/openclaw/open-party-0.1.6/package.json +0 -12
- package/dist/openclaw/open-party-0.1.6/skills/open-party/SKILL.md +0 -90
- /package/dist/claude-code/{open-party-0.1.6 → open-party-0.1.7}/.mcp.json +0 -0
package/dist/cli/index.js
CHANGED
|
@@ -11,9 +11,9 @@ var __export = (target, all) => {
|
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
// src/infra/tailscale.ts
|
|
14
|
-
import { execFileSync, execSync } from "child_process";
|
|
15
|
-
import { existsSync } from "fs";
|
|
16
|
-
import { join } from "path";
|
|
14
|
+
import { execFileSync, execSync as execSync3 } from "child_process";
|
|
15
|
+
import { existsSync as existsSync3 } from "fs";
|
|
16
|
+
import { join as join3 } from "path";
|
|
17
17
|
import { spawn as nodeSpawn } from "child_process";
|
|
18
18
|
function parsePossiblyNoisyJson(raw2) {
|
|
19
19
|
const trimmed = raw2.trim();
|
|
@@ -33,7 +33,7 @@ function runExec(cmd, timeout = 5e3) {
|
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
35
|
function checkBinary(path, timeout = 3e3) {
|
|
36
|
-
if (!path || !
|
|
36
|
+
if (!path || !existsSync3(path)) return false;
|
|
37
37
|
try {
|
|
38
38
|
execFileSync(path, ["--version"], { timeout, encoding: "utf-8", stdio: "pipe", windowsHide: true });
|
|
39
39
|
return true;
|
|
@@ -50,9 +50,9 @@ function findTailscaleBinary() {
|
|
|
50
50
|
} catch {
|
|
51
51
|
}
|
|
52
52
|
const knownPaths = process.platform === "win32" ? [
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
join3(process.env.ProgramFiles || "C:\\Program Files", "Tailscale", "tailscale.exe"),
|
|
54
|
+
join3(process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)", "Tailscale", "tailscale.exe"),
|
|
55
|
+
join3(process.env.LOCALAPPDATA || "", "Tailscale", "tailscale.exe")
|
|
56
56
|
] : [
|
|
57
57
|
"/Applications/Tailscale.app/Contents/MacOS/Tailscale",
|
|
58
58
|
"/usr/bin/tailscale",
|
|
@@ -63,7 +63,7 @@ function findTailscaleBinary() {
|
|
|
63
63
|
}
|
|
64
64
|
if (process.platform !== "win32") {
|
|
65
65
|
try {
|
|
66
|
-
const result =
|
|
66
|
+
const result = execSync3(
|
|
67
67
|
'find /Applications -maxdepth 3 -name Tailscale -path "*/Tailscale.app/Contents/MacOS/Tailscale"',
|
|
68
68
|
{ timeout: 5e3, encoding: "utf-8" }
|
|
69
69
|
);
|
|
@@ -96,7 +96,7 @@ function getTailnetHostname(binary) {
|
|
|
96
96
|
];
|
|
97
97
|
let lastErr = null;
|
|
98
98
|
for (const candidate of candidates) {
|
|
99
|
-
if (candidate.startsWith("/") && !
|
|
99
|
+
if (candidate.startsWith("/") && !existsSync3(candidate)) continue;
|
|
100
100
|
try {
|
|
101
101
|
const stdout = runExec([candidate, "status", "--json"], 5e3);
|
|
102
102
|
const parsed = stdout.trim() ? parsePossiblyNoisyJson(stdout) : {};
|
|
@@ -191,83 +191,6 @@ function logoutTailscale(timeout = 15e3) {
|
|
|
191
191
|
return { success: false, output: (err.stderr ?? err.message ?? "").trim() };
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
|
-
function startInteractiveLogin() {
|
|
195
|
-
const binary = getTailscaleBinary();
|
|
196
|
-
const child = nodeSpawn(binary, ["login"], {
|
|
197
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
198
|
-
windowsHide: true
|
|
199
|
-
});
|
|
200
|
-
const urlRegex = /https:\/\/login\.tailscale\.com\/a\/[^\s]+/;
|
|
201
|
-
const promise = new Promise((resolve4) => {
|
|
202
|
-
let stdout = "";
|
|
203
|
-
let resolved = false;
|
|
204
|
-
const done = (result) => {
|
|
205
|
-
if (resolved) return;
|
|
206
|
-
resolved = true;
|
|
207
|
-
resolve4(result);
|
|
208
|
-
};
|
|
209
|
-
child.stdout?.on("data", (data) => {
|
|
210
|
-
stdout += data.toString();
|
|
211
|
-
const match2 = stdout.match(urlRegex);
|
|
212
|
-
if (match2) {
|
|
213
|
-
done({ success: true, url: match2[0], output: stdout.trim() });
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
child.stderr?.on("data", (data) => {
|
|
217
|
-
stdout += data.toString();
|
|
218
|
-
});
|
|
219
|
-
child.on("close", (code) => {
|
|
220
|
-
if (code === 0) {
|
|
221
|
-
done({ success: true, output: stdout.trim() });
|
|
222
|
-
} else {
|
|
223
|
-
done({ success: false, output: stdout.trim() || `Exited with code ${code}` });
|
|
224
|
-
}
|
|
225
|
-
});
|
|
226
|
-
child.on("error", (err) => {
|
|
227
|
-
done({ success: false, output: err.message });
|
|
228
|
-
});
|
|
229
|
-
setTimeout(() => {
|
|
230
|
-
done({ success: false, output: "Timeout waiting for login URL" });
|
|
231
|
-
try {
|
|
232
|
-
child.kill();
|
|
233
|
-
} catch {
|
|
234
|
-
}
|
|
235
|
-
}, 3e4);
|
|
236
|
-
});
|
|
237
|
-
return { promise, process: child };
|
|
238
|
-
}
|
|
239
|
-
function getInstallInstructions(platform) {
|
|
240
|
-
switch (platform) {
|
|
241
|
-
case "linux":
|
|
242
|
-
return {
|
|
243
|
-
os: "linux",
|
|
244
|
-
download_url: "https://tailscale.com/download/linux",
|
|
245
|
-
commands: ["curl -fsSL https://tailscale.com/install.sh | sh"],
|
|
246
|
-
needs_sudo: true
|
|
247
|
-
};
|
|
248
|
-
case "darwin":
|
|
249
|
-
return {
|
|
250
|
-
os: "macOS",
|
|
251
|
-
download_url: "https://tailscale.com/download/mac",
|
|
252
|
-
commands: ["brew install tailscale"],
|
|
253
|
-
needs_sudo: false
|
|
254
|
-
};
|
|
255
|
-
case "win32":
|
|
256
|
-
return {
|
|
257
|
-
os: "windows",
|
|
258
|
-
download_url: "https://tailscale.com/download/windows",
|
|
259
|
-
commands: ["winget install Tailscale.Tailscale"],
|
|
260
|
-
needs_sudo: false
|
|
261
|
-
};
|
|
262
|
-
default:
|
|
263
|
-
return {
|
|
264
|
-
os: platform,
|
|
265
|
-
download_url: "https://tailscale.com/download/",
|
|
266
|
-
commands: [],
|
|
267
|
-
needs_sudo: false
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
194
|
var cachedBinary, PERMISSION_KEYWORDS;
|
|
272
195
|
var init_tailscale = __esm({
|
|
273
196
|
"src/infra/tailscale.ts"() {
|
|
@@ -287,58 +210,6 @@ var init_tailscale = __esm({
|
|
|
287
210
|
}
|
|
288
211
|
});
|
|
289
212
|
|
|
290
|
-
// src/cli/tailscale-installer.ts
|
|
291
|
-
var tailscale_installer_exports = {};
|
|
292
|
-
__export(tailscale_installer_exports, {
|
|
293
|
-
installTailscale: () => installTailscale
|
|
294
|
-
});
|
|
295
|
-
import { spawn } from "child_process";
|
|
296
|
-
async function installTailscale(platform) {
|
|
297
|
-
const entry = INSTALL_COMMANDS[platform];
|
|
298
|
-
if (!entry) {
|
|
299
|
-
return {
|
|
300
|
-
success: false,
|
|
301
|
-
output: `Unsupported platform: ${platform}. Please install manually from https://tailscale.com/download`
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
const cmd = entry.needsSudo ? "sudo" : entry.cmd;
|
|
305
|
-
const args2 = entry.needsSudo ? [entry.cmd, ...entry.args] : entry.args;
|
|
306
|
-
console.log(`Running: ${cmd} ${args2.join(" ")}
|
|
307
|
-
`);
|
|
308
|
-
return new Promise((resolve4) => {
|
|
309
|
-
const child = spawn(cmd, args2, {
|
|
310
|
-
stdio: "inherit",
|
|
311
|
-
windowsHide: true
|
|
312
|
-
});
|
|
313
|
-
let exited = false;
|
|
314
|
-
child.on("close", (code) => {
|
|
315
|
-
if (exited) return;
|
|
316
|
-
exited = true;
|
|
317
|
-
if (code === 0) {
|
|
318
|
-
resolve4({ success: true, output: "Installation completed." });
|
|
319
|
-
} else {
|
|
320
|
-
resolve4({ success: false, output: `Installation exited with code ${code}` });
|
|
321
|
-
}
|
|
322
|
-
});
|
|
323
|
-
child.on("error", (err) => {
|
|
324
|
-
if (exited) return;
|
|
325
|
-
exited = true;
|
|
326
|
-
resolve4({ success: false, output: err.message });
|
|
327
|
-
});
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
|
-
var INSTALL_COMMANDS;
|
|
331
|
-
var init_tailscale_installer = __esm({
|
|
332
|
-
"src/cli/tailscale-installer.ts"() {
|
|
333
|
-
"use strict";
|
|
334
|
-
INSTALL_COMMANDS = {
|
|
335
|
-
linux: { cmd: "bash", args: ["-c", "curl -fsSL https://tailscale.com/install.sh | sh"], needsSudo: true },
|
|
336
|
-
darwin: { cmd: "brew", args: ["install", "tailscale"], needsSudo: false },
|
|
337
|
-
win32: { cmd: "winget", args: ["install", "Tailscale.Tailscale", "--accept-source-agreements"], needsSudo: false }
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
});
|
|
341
|
-
|
|
342
213
|
// node_modules/hono/dist/compose.js
|
|
343
214
|
var compose;
|
|
344
215
|
var init_compose = __esm({
|
|
@@ -3338,11 +3209,12 @@ var init_dist2 = __esm({
|
|
|
3338
3209
|
});
|
|
3339
3210
|
|
|
3340
3211
|
// src/server/config.ts
|
|
3341
|
-
var PARTY_PORT, HEARTBEAT_TIMEOUT, CLEANUP_INTERVAL, DISCOVERY_INTERVAL, REMOTE_STALE_FACTOR, PROBE_TIMEOUT;
|
|
3212
|
+
var PARTY_PORT, STALE_THRESHOLD, HEARTBEAT_TIMEOUT, CLEANUP_INTERVAL, DISCOVERY_INTERVAL, REMOTE_STALE_FACTOR, PROBE_TIMEOUT;
|
|
3342
3213
|
var init_config = __esm({
|
|
3343
3214
|
"src/server/config.ts"() {
|
|
3344
3215
|
"use strict";
|
|
3345
3216
|
PARTY_PORT = parseInt(process.env.PARTY_PORT || "8000", 10);
|
|
3217
|
+
STALE_THRESHOLD = parseInt(process.env.STALE_THRESHOLD || "3", 10);
|
|
3346
3218
|
HEARTBEAT_TIMEOUT = parseFloat(process.env.HEARTBEAT_TIMEOUT || "60");
|
|
3347
3219
|
CLEANUP_INTERVAL = parseFloat(process.env.CLEANUP_INTERVAL || "60");
|
|
3348
3220
|
DISCOVERY_INTERVAL = parseFloat(process.env.DISCOVERY_INTERVAL || "20");
|
|
@@ -3352,16 +3224,16 @@ var init_config = __esm({
|
|
|
3352
3224
|
});
|
|
3353
3225
|
|
|
3354
3226
|
// src/server/logger.ts
|
|
3355
|
-
import { existsSync as
|
|
3356
|
-
import { join as
|
|
3357
|
-
import { homedir as
|
|
3227
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, appendFileSync, readdirSync as readdirSync2, unlinkSync as unlinkSync2, statSync as statSync2 } from "fs";
|
|
3228
|
+
import { join as join4 } from "path";
|
|
3229
|
+
import { homedir as homedir3 } from "os";
|
|
3358
3230
|
function getEffectiveLevel() {
|
|
3359
3231
|
const env = (process.env.LOG_LEVEL || "info").toLowerCase().trim();
|
|
3360
3232
|
if (env in LEVEL_ORDER) return env;
|
|
3361
3233
|
return "info";
|
|
3362
3234
|
}
|
|
3363
3235
|
function initLogFile() {
|
|
3364
|
-
if (!
|
|
3236
|
+
if (!existsSync4(LOG_DIR)) {
|
|
3365
3237
|
mkdirSync3(LOG_DIR, { recursive: true });
|
|
3366
3238
|
return;
|
|
3367
3239
|
}
|
|
@@ -3372,9 +3244,9 @@ function initLogFile() {
|
|
|
3372
3244
|
for (const f of files) {
|
|
3373
3245
|
if (!f.endsWith("-open-party.log")) continue;
|
|
3374
3246
|
try {
|
|
3375
|
-
const stat = statSync2(
|
|
3247
|
+
const stat = statSync2(join4(LOG_DIR, f));
|
|
3376
3248
|
if (stat.mtimeMs < cutoff) {
|
|
3377
|
-
unlinkSync2(
|
|
3249
|
+
unlinkSync2(join4(LOG_DIR, f));
|
|
3378
3250
|
}
|
|
3379
3251
|
} catch {
|
|
3380
3252
|
}
|
|
@@ -3384,10 +3256,10 @@ function initLogFile() {
|
|
|
3384
3256
|
}
|
|
3385
3257
|
function getLogFilePath() {
|
|
3386
3258
|
const d = /* @__PURE__ */ new Date();
|
|
3387
|
-
const
|
|
3259
|
+
const yyyy = String(d.getFullYear());
|
|
3388
3260
|
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
3389
3261
|
const dd = String(d.getDate()).padStart(2, "0");
|
|
3390
|
-
return
|
|
3262
|
+
return join4(LOG_DIR, `${yyyy}-${mm}-${dd}-open-party.log`);
|
|
3391
3263
|
}
|
|
3392
3264
|
function shouldLog(level) {
|
|
3393
3265
|
return LEVEL_ORDER[level] >= LEVEL_ORDER[effectiveLevel];
|
|
@@ -3403,13 +3275,13 @@ ${err.stack}` : "";
|
|
|
3403
3275
|
function format(level, tag, message) {
|
|
3404
3276
|
const now = /* @__PURE__ */ new Date();
|
|
3405
3277
|
const levelStr = level.toUpperCase().padEnd(5);
|
|
3406
|
-
const
|
|
3278
|
+
const yyyy = String(now.getFullYear());
|
|
3407
3279
|
const mm = String(now.getMonth() + 1).padStart(2, "0");
|
|
3408
3280
|
const dd = String(now.getDate()).padStart(2, "0");
|
|
3409
3281
|
const hh = String(now.getHours()).padStart(2, "0");
|
|
3410
3282
|
const min = String(now.getMinutes()).padStart(2, "0");
|
|
3411
3283
|
const ss = String(now.getSeconds()).padStart(2, "0");
|
|
3412
|
-
const ts = `${
|
|
3284
|
+
const ts = `${yyyy}-${mm}-${dd} ${hh}:${min}:${ss}`;
|
|
3413
3285
|
return `${ts} [${levelStr}] [${tag}] ${message}`;
|
|
3414
3286
|
}
|
|
3415
3287
|
function output(consoleFn, level, tag, message) {
|
|
@@ -3432,7 +3304,7 @@ var init_logger = __esm({
|
|
|
3432
3304
|
error: 3
|
|
3433
3305
|
};
|
|
3434
3306
|
effectiveLevel = getEffectiveLevel();
|
|
3435
|
-
LOG_DIR =
|
|
3307
|
+
LOG_DIR = join4(homedir3(), ".open-party", "logs");
|
|
3436
3308
|
LOG_RETENTION_DAYS = 7;
|
|
3437
3309
|
initLogFile();
|
|
3438
3310
|
logger = {
|
|
@@ -3440,7 +3312,8 @@ var init_logger = __esm({
|
|
|
3440
3312
|
output(console.log, "info", tag, data ? `${message} ${JSON.stringify(data)}` : message);
|
|
3441
3313
|
},
|
|
3442
3314
|
warn(tag, message, data) {
|
|
3443
|
-
|
|
3315
|
+
const detail = data instanceof Error ? `: ${extractError(data)}` : data ? ` ${JSON.stringify(data)}` : "";
|
|
3316
|
+
output(console.warn, "warn", tag, message + detail);
|
|
3444
3317
|
},
|
|
3445
3318
|
error(tag, message, err) {
|
|
3446
3319
|
const detail = err ? `: ${extractError(err)}` : "";
|
|
@@ -3454,33 +3327,13 @@ var init_logger = __esm({
|
|
|
3454
3327
|
});
|
|
3455
3328
|
|
|
3456
3329
|
// src/server/persistence.ts
|
|
3457
|
-
import { existsSync as
|
|
3458
|
-
import { join as
|
|
3459
|
-
import { homedir as
|
|
3330
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3, renameSync, mkdirSync as mkdirSync4 } from "fs";
|
|
3331
|
+
import { join as join5 } from "path";
|
|
3332
|
+
import { homedir as homedir4 } from "os";
|
|
3460
3333
|
function dataDirPath() {
|
|
3461
3334
|
const pluginData = process.env.CLAUDE_PLUGIN_DATA || "";
|
|
3462
|
-
if (pluginData) return
|
|
3463
|
-
return
|
|
3464
|
-
}
|
|
3465
|
-
function runMigrations(raw2) {
|
|
3466
|
-
const data = raw2;
|
|
3467
|
-
if (!data || typeof data !== "object") {
|
|
3468
|
-
throw new Error("Snapshot is not a valid object");
|
|
3469
|
-
}
|
|
3470
|
-
const version = typeof data.version === "number" ? data.version : 0;
|
|
3471
|
-
let snapshot = {
|
|
3472
|
-
version,
|
|
3473
|
-
saved_at: typeof data.saved_at === "number" ? data.saved_at : 0,
|
|
3474
|
-
agents: Array.isArray(data.agents) ? data.agents : [],
|
|
3475
|
-
history: typeof data.history === "object" && data.history !== null && !Array.isArray(data.history) ? data.history : {}
|
|
3476
|
-
};
|
|
3477
|
-
for (const m of MIGRATORS) {
|
|
3478
|
-
if (m.version > snapshot.version) {
|
|
3479
|
-
snapshot = m.migrate(snapshot);
|
|
3480
|
-
snapshot.version = m.version;
|
|
3481
|
-
}
|
|
3482
|
-
}
|
|
3483
|
-
return snapshot;
|
|
3335
|
+
if (pluginData) return join5(pluginData, "data");
|
|
3336
|
+
return join5(homedir4(), ".open-party", "data");
|
|
3484
3337
|
}
|
|
3485
3338
|
function abortableSleep(ms, signal) {
|
|
3486
3339
|
return new Promise((resolve4, reject) => {
|
|
@@ -3495,32 +3348,27 @@ function abortableSleep(ms, signal) {
|
|
|
3495
3348
|
);
|
|
3496
3349
|
});
|
|
3497
3350
|
}
|
|
3498
|
-
var CURRENT_SCHEMA_VERSION, SNAPSHOT_FILE, SHUTDOWN_MARKER_FILE, DEFAULT_SNAPSHOT_INTERVAL_MS,
|
|
3351
|
+
var CURRENT_SCHEMA_VERSION, SNAPSHOT_FILE, SHUTDOWN_MARKER_FILE, DEFAULT_SNAPSHOT_INTERVAL_MS, SnapshotManager;
|
|
3499
3352
|
var init_persistence = __esm({
|
|
3500
3353
|
"src/server/persistence.ts"() {
|
|
3501
3354
|
"use strict";
|
|
3502
3355
|
init_logger();
|
|
3503
|
-
CURRENT_SCHEMA_VERSION =
|
|
3356
|
+
CURRENT_SCHEMA_VERSION = 2;
|
|
3504
3357
|
SNAPSHOT_FILE = "snapshot.json";
|
|
3505
3358
|
SHUTDOWN_MARKER_FILE = "shutdown-marker.json";
|
|
3506
3359
|
DEFAULT_SNAPSHOT_INTERVAL_MS = 6e4;
|
|
3507
|
-
DEBOUNCE_MS = 5e3;
|
|
3508
|
-
MIGRATORS = [
|
|
3509
|
-
// Future: { version: 2, migrate(snapshot) { ... } },
|
|
3510
|
-
];
|
|
3511
3360
|
SnapshotManager = class {
|
|
3512
3361
|
_dir;
|
|
3513
3362
|
_snapshotPath;
|
|
3514
3363
|
_markerPath;
|
|
3515
|
-
_debounceTimer = null;
|
|
3516
3364
|
constructor(dataDir) {
|
|
3517
3365
|
this._dir = dataDir ?? dataDirPath();
|
|
3518
|
-
this._snapshotPath =
|
|
3519
|
-
this._markerPath =
|
|
3366
|
+
this._snapshotPath = join5(this._dir, SNAPSHOT_FILE);
|
|
3367
|
+
this._markerPath = join5(this._dir, SHUTDOWN_MARKER_FILE);
|
|
3520
3368
|
mkdirSync4(this._dir, { recursive: true });
|
|
3521
3369
|
const tmpPath = this._snapshotPath + ".tmp";
|
|
3522
3370
|
try {
|
|
3523
|
-
if (
|
|
3371
|
+
if (existsSync5(tmpPath)) {
|
|
3524
3372
|
unlinkSync3(tmpPath);
|
|
3525
3373
|
}
|
|
3526
3374
|
} catch (error) {
|
|
@@ -3530,13 +3378,13 @@ var init_persistence = __esm({
|
|
|
3530
3378
|
// ------------------------------------------------------------------
|
|
3531
3379
|
// Write / Load
|
|
3532
3380
|
// ------------------------------------------------------------------
|
|
3533
|
-
/** Atomically write a snapshot of registry agents and
|
|
3534
|
-
writeSnapshot(agents,
|
|
3381
|
+
/** Atomically write a snapshot of registry agents and ring buffer state. */
|
|
3382
|
+
writeSnapshot(agents, buffers) {
|
|
3535
3383
|
const snapshot = {
|
|
3536
3384
|
version: CURRENT_SCHEMA_VERSION,
|
|
3537
3385
|
saved_at: Date.now(),
|
|
3538
3386
|
agents,
|
|
3539
|
-
|
|
3387
|
+
buffers
|
|
3540
3388
|
};
|
|
3541
3389
|
const serialized = JSON.stringify(snapshot, null, 2);
|
|
3542
3390
|
const tmpPath = this._snapshotPath + ".tmp";
|
|
@@ -3554,12 +3402,20 @@ var init_persistence = __esm({
|
|
|
3554
3402
|
}
|
|
3555
3403
|
/** Load and validate snapshot. Returns null if file missing or corrupt. */
|
|
3556
3404
|
loadSnapshot() {
|
|
3557
|
-
if (!
|
|
3405
|
+
if (!existsSync5(this._snapshotPath)) {
|
|
3558
3406
|
return null;
|
|
3559
3407
|
}
|
|
3560
3408
|
try {
|
|
3561
3409
|
const raw2 = JSON.parse(readFileSync3(this._snapshotPath, "utf-8"));
|
|
3562
|
-
|
|
3410
|
+
if (!raw2 || typeof raw2 !== "object") {
|
|
3411
|
+
throw new Error("Snapshot is not a valid object");
|
|
3412
|
+
}
|
|
3413
|
+
return {
|
|
3414
|
+
version: typeof raw2.version === "number" ? raw2.version : CURRENT_SCHEMA_VERSION,
|
|
3415
|
+
saved_at: typeof raw2.saved_at === "number" ? raw2.saved_at : 0,
|
|
3416
|
+
agents: Array.isArray(raw2.agents) ? raw2.agents : [],
|
|
3417
|
+
buffers: raw2.buffers && typeof raw2.buffers === "object" && !Array.isArray(raw2.buffers) ? raw2.buffers : {}
|
|
3418
|
+
};
|
|
3563
3419
|
} catch (error) {
|
|
3564
3420
|
logger.warn("Persistence", "Failed to load snapshot (starting fresh)", error);
|
|
3565
3421
|
return null;
|
|
@@ -3586,22 +3442,14 @@ var init_persistence = __esm({
|
|
|
3586
3442
|
}
|
|
3587
3443
|
return count;
|
|
3588
3444
|
}
|
|
3589
|
-
/** Restore
|
|
3590
|
-
|
|
3445
|
+
/** Restore ring buffer state into message queue. */
|
|
3446
|
+
hydrateBuffers(queue) {
|
|
3591
3447
|
const snapshot = this.loadSnapshot();
|
|
3592
|
-
if (!snapshot || Object.keys(snapshot.
|
|
3448
|
+
if (!snapshot || Object.keys(snapshot.buffers).length === 0) return 0;
|
|
3449
|
+
queue.restoreBufferSnapshots(snapshot.buffers);
|
|
3593
3450
|
let totalEntries = 0;
|
|
3594
|
-
for (const
|
|
3595
|
-
|
|
3596
|
-
queue.logToHistory(agentId, entry.direction, {
|
|
3597
|
-
sender_id: entry.sender_id,
|
|
3598
|
-
recipient_id: entry.recipient_id,
|
|
3599
|
-
summary: entry.summary,
|
|
3600
|
-
content: entry.content,
|
|
3601
|
-
timestamp: entry.timestamp
|
|
3602
|
-
});
|
|
3603
|
-
totalEntries++;
|
|
3604
|
-
}
|
|
3451
|
+
for (const snap of Object.values(snapshot.buffers)) {
|
|
3452
|
+
totalEntries += snap.entries.length;
|
|
3605
3453
|
}
|
|
3606
3454
|
return totalEntries;
|
|
3607
3455
|
}
|
|
@@ -3623,7 +3471,7 @@ var init_persistence = __esm({
|
|
|
3623
3471
|
/** Remove shutdown marker — called after successful shutdown. */
|
|
3624
3472
|
removeShutdownMarker() {
|
|
3625
3473
|
try {
|
|
3626
|
-
if (
|
|
3474
|
+
if (existsSync5(this._markerPath)) {
|
|
3627
3475
|
unlinkSync3(this._markerPath);
|
|
3628
3476
|
}
|
|
3629
3477
|
} catch (error) {
|
|
@@ -3632,16 +3480,16 @@ var init_persistence = __esm({
|
|
|
3632
3480
|
}
|
|
3633
3481
|
/** Check if a shutdown marker exists (indicates previous shutdown was interrupted). */
|
|
3634
3482
|
hasShutdownMarker() {
|
|
3635
|
-
return
|
|
3483
|
+
return existsSync5(this._markerPath);
|
|
3636
3484
|
}
|
|
3637
3485
|
// ------------------------------------------------------------------
|
|
3638
|
-
// Snapshot loop
|
|
3486
|
+
// Snapshot loop
|
|
3639
3487
|
// ------------------------------------------------------------------
|
|
3640
3488
|
/**
|
|
3641
3489
|
* Start periodic snapshot background loop.
|
|
3642
3490
|
* Writes snapshot every `intervalMs` milliseconds until signal is aborted.
|
|
3643
3491
|
*/
|
|
3644
|
-
async startSnapshotLoop(signal, getAgents,
|
|
3492
|
+
async startSnapshotLoop(signal, getAgents, getBuffers, intervalMs = DEFAULT_SNAPSHOT_INTERVAL_MS) {
|
|
3645
3493
|
while (!signal.aborted) {
|
|
3646
3494
|
try {
|
|
3647
3495
|
await abortableSleep(intervalMs, signal);
|
|
@@ -3651,110 +3499,295 @@ var init_persistence = __esm({
|
|
|
3651
3499
|
}
|
|
3652
3500
|
if (signal.aborted) break;
|
|
3653
3501
|
try {
|
|
3654
|
-
this.writeSnapshot(getAgents(),
|
|
3502
|
+
this.writeSnapshot(getAgents(), getBuffers());
|
|
3655
3503
|
} catch (error) {
|
|
3656
3504
|
logger.warn("Persistence", "Periodic snapshot failed", error);
|
|
3657
3505
|
}
|
|
3658
3506
|
}
|
|
3659
3507
|
}
|
|
3508
|
+
};
|
|
3509
|
+
}
|
|
3510
|
+
});
|
|
3511
|
+
|
|
3512
|
+
// src/server/ring-buffer.ts
|
|
3513
|
+
var DEFAULT_CAPACITY, AgentRingBuffer;
|
|
3514
|
+
var init_ring_buffer = __esm({
|
|
3515
|
+
"src/server/ring-buffer.ts"() {
|
|
3516
|
+
"use strict";
|
|
3517
|
+
DEFAULT_CAPACITY = 200;
|
|
3518
|
+
AgentRingBuffer = class {
|
|
3519
|
+
_buffer;
|
|
3520
|
+
_capacity;
|
|
3521
|
+
_head = 0;
|
|
3522
|
+
// next write position (0 ~ capacity-1)
|
|
3523
|
+
_nextSeq = 1;
|
|
3524
|
+
// next sequence number to assign
|
|
3525
|
+
_count = 0;
|
|
3526
|
+
// valid entries currently in buffer
|
|
3527
|
+
_cursor = 0;
|
|
3528
|
+
// server-side read cursor (last consumed seq)
|
|
3529
|
+
constructor(capacity = DEFAULT_CAPACITY) {
|
|
3530
|
+
this._capacity = Math.max(1, capacity);
|
|
3531
|
+
this._buffer = new Array(this._capacity);
|
|
3532
|
+
}
|
|
3533
|
+
// ------------------------------------------------------------------
|
|
3534
|
+
// Write
|
|
3535
|
+
// ------------------------------------------------------------------
|
|
3536
|
+
/** Write an entry to the buffer. Returns the assigned sequence number. */
|
|
3537
|
+
write(direction, envelope) {
|
|
3538
|
+
const seq = this._nextSeq++;
|
|
3539
|
+
const entry = {
|
|
3540
|
+
seq,
|
|
3541
|
+
direction,
|
|
3542
|
+
sender_id: envelope.sender_id,
|
|
3543
|
+
recipient_id: envelope.recipient_id,
|
|
3544
|
+
summary: envelope.summary,
|
|
3545
|
+
content: envelope.content,
|
|
3546
|
+
timestamp: envelope.timestamp ?? Date.now() / 1e3
|
|
3547
|
+
};
|
|
3548
|
+
this._buffer[this._head] = entry;
|
|
3549
|
+
this._head = (this._head + 1) % this._capacity;
|
|
3550
|
+
if (this._count < this._capacity) {
|
|
3551
|
+
this._count++;
|
|
3552
|
+
}
|
|
3553
|
+
return seq;
|
|
3554
|
+
}
|
|
3555
|
+
// ------------------------------------------------------------------
|
|
3556
|
+
// Read (cursor-based — dequeue semantics)
|
|
3557
|
+
// ------------------------------------------------------------------
|
|
3558
|
+
/**
|
|
3559
|
+
* Read up to maxCount entries after the cursor, then advance the cursor.
|
|
3560
|
+
* Non-destructive: entries remain in the buffer for history queries.
|
|
3561
|
+
*/
|
|
3562
|
+
dequeue(maxCount = 50) {
|
|
3563
|
+
const unread = this._readSince(this._cursor);
|
|
3564
|
+
if (unread.length === 0) return [];
|
|
3565
|
+
const taken = unread.slice(0, maxCount);
|
|
3566
|
+
this._cursor = taken[taken.length - 1].seq;
|
|
3567
|
+
return taken;
|
|
3568
|
+
}
|
|
3660
3569
|
/**
|
|
3661
|
-
*
|
|
3662
|
-
*
|
|
3570
|
+
* Like dequeue(), but only returns 'received' entries.
|
|
3571
|
+
* Cursor advances to the last returned 'received' entry's seq.
|
|
3572
|
+
* Used for inbox semantics — agents only see incoming messages.
|
|
3663
3573
|
*/
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3574
|
+
dequeueReceived(maxCount = 50) {
|
|
3575
|
+
const all = this._readSince(this._cursor);
|
|
3576
|
+
if (all.length === 0) return [];
|
|
3577
|
+
const received = all.filter((e) => e.direction === "received").slice(0, maxCount);
|
|
3578
|
+
if (received.length === 0) return [];
|
|
3579
|
+
this._cursor = received[received.length - 1].seq;
|
|
3580
|
+
return received;
|
|
3581
|
+
}
|
|
3582
|
+
/** Number of unread entries (next_seq - cursor). */
|
|
3583
|
+
unreadCount() {
|
|
3584
|
+
const diff = this._nextSeq - 1 - this._cursor;
|
|
3585
|
+
return Math.max(0, diff);
|
|
3586
|
+
}
|
|
3587
|
+
/** Number of unread 'received' entries after cursor. */
|
|
3588
|
+
unreadReceivedCount() {
|
|
3589
|
+
const all = this._readSince(this._cursor);
|
|
3590
|
+
let count = 0;
|
|
3591
|
+
for (const e of all) {
|
|
3592
|
+
if (e.direction === "received") count++;
|
|
3667
3593
|
}
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3594
|
+
return count;
|
|
3595
|
+
}
|
|
3596
|
+
// ------------------------------------------------------------------
|
|
3597
|
+
// Read (non-destructive — no cursor advance)
|
|
3598
|
+
// ------------------------------------------------------------------
|
|
3599
|
+
/** Read all entries with seq > sinceSeq. Does NOT advance cursor. */
|
|
3600
|
+
readSince(sinceSeq) {
|
|
3601
|
+
return this._readSince(sinceSeq);
|
|
3602
|
+
}
|
|
3603
|
+
/** Get the most recent N entries regardless of cursor. */
|
|
3604
|
+
getRecent(limit = 20) {
|
|
3605
|
+
const all = this._allSorted();
|
|
3606
|
+
return all.slice(-limit);
|
|
3607
|
+
}
|
|
3608
|
+
/** Get total number of valid entries in the buffer. */
|
|
3609
|
+
get count() {
|
|
3610
|
+
return this._count;
|
|
3611
|
+
}
|
|
3612
|
+
// ------------------------------------------------------------------
|
|
3613
|
+
// Lifecycle
|
|
3614
|
+
// ------------------------------------------------------------------
|
|
3615
|
+
/** Clear the buffer and reset cursor. */
|
|
3616
|
+
clear() {
|
|
3617
|
+
this._buffer.fill(void 0);
|
|
3618
|
+
this._head = 0;
|
|
3619
|
+
this._nextSeq = 1;
|
|
3620
|
+
this._count = 0;
|
|
3621
|
+
this._cursor = 0;
|
|
3622
|
+
}
|
|
3623
|
+
// ------------------------------------------------------------------
|
|
3624
|
+
// Snapshot / Restore
|
|
3625
|
+
// ------------------------------------------------------------------
|
|
3626
|
+
/** Export buffer state for persistence. */
|
|
3627
|
+
getSnapshot() {
|
|
3628
|
+
return {
|
|
3629
|
+
entries: this._allSorted(),
|
|
3630
|
+
next_seq: this._nextSeq,
|
|
3631
|
+
cursor: this._cursor
|
|
3632
|
+
};
|
|
3633
|
+
}
|
|
3634
|
+
/** Restore buffer from a snapshot. */
|
|
3635
|
+
restoreFromSnapshot(snap) {
|
|
3636
|
+
this.clear();
|
|
3637
|
+
for (const entry of snap.entries) {
|
|
3638
|
+
this._buffer[this._head] = entry;
|
|
3639
|
+
this._head = (this._head + 1) % this._capacity;
|
|
3640
|
+
this._count++;
|
|
3641
|
+
}
|
|
3642
|
+
this._nextSeq = snap.next_seq;
|
|
3643
|
+
this._cursor = snap.cursor;
|
|
3644
|
+
}
|
|
3645
|
+
/** Get the next sequence number that will be assigned. */
|
|
3646
|
+
get nextSeq() {
|
|
3647
|
+
return this._nextSeq;
|
|
3648
|
+
}
|
|
3649
|
+
/** Get the current cursor position. */
|
|
3650
|
+
get cursor() {
|
|
3651
|
+
return this._cursor;
|
|
3652
|
+
}
|
|
3653
|
+
// ------------------------------------------------------------------
|
|
3654
|
+
// Private helpers
|
|
3655
|
+
// ------------------------------------------------------------------
|
|
3656
|
+
/**
|
|
3657
|
+
* Collect all valid entries sorted by seq.
|
|
3658
|
+
* O(count) — scans the ring buffer once.
|
|
3659
|
+
*/
|
|
3660
|
+
_allSorted() {
|
|
3661
|
+
const result = [];
|
|
3662
|
+
for (let i = 0; i < this._capacity; i++) {
|
|
3663
|
+
const entry = this._buffer[i];
|
|
3664
|
+
if (entry !== void 0) {
|
|
3665
|
+
result.push(entry);
|
|
3674
3666
|
}
|
|
3675
|
-
}
|
|
3667
|
+
}
|
|
3668
|
+
result.sort((a, b) => a.seq - b.seq);
|
|
3669
|
+
return result;
|
|
3676
3670
|
}
|
|
3677
|
-
/**
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3671
|
+
/**
|
|
3672
|
+
* Read entries with seq > sinceSeq.
|
|
3673
|
+
* Uses binary search on sorted entries for efficiency.
|
|
3674
|
+
*/
|
|
3675
|
+
_readSince(sinceSeq) {
|
|
3676
|
+
const all = this._allSorted();
|
|
3677
|
+
let lo = 0;
|
|
3678
|
+
let hi = all.length;
|
|
3679
|
+
while (lo < hi) {
|
|
3680
|
+
const mid = lo + hi >>> 1;
|
|
3681
|
+
if (all[mid].seq <= sinceSeq) {
|
|
3682
|
+
lo = mid + 1;
|
|
3683
|
+
} else {
|
|
3684
|
+
hi = mid;
|
|
3685
|
+
}
|
|
3682
3686
|
}
|
|
3687
|
+
return all.slice(lo);
|
|
3683
3688
|
}
|
|
3684
3689
|
};
|
|
3685
3690
|
}
|
|
3686
3691
|
});
|
|
3687
3692
|
|
|
3688
3693
|
// src/server/message-queue.ts
|
|
3689
|
-
|
|
3694
|
+
function toEnvelope(e) {
|
|
3695
|
+
return {
|
|
3696
|
+
sender_id: e.sender_id,
|
|
3697
|
+
recipient_id: e.recipient_id,
|
|
3698
|
+
summary: e.summary,
|
|
3699
|
+
content: e.content,
|
|
3700
|
+
timestamp: e.timestamp
|
|
3701
|
+
};
|
|
3702
|
+
}
|
|
3703
|
+
function toHistoryEntry(e) {
|
|
3704
|
+
return {
|
|
3705
|
+
direction: e.direction,
|
|
3706
|
+
sender_id: e.sender_id,
|
|
3707
|
+
recipient_id: e.recipient_id,
|
|
3708
|
+
summary: e.summary,
|
|
3709
|
+
content: e.content,
|
|
3710
|
+
timestamp: e.timestamp
|
|
3711
|
+
};
|
|
3712
|
+
}
|
|
3713
|
+
var DEFAULT_CAPACITY2, MessageQueue;
|
|
3690
3714
|
var init_message_queue = __esm({
|
|
3691
3715
|
"src/server/message-queue.ts"() {
|
|
3692
3716
|
"use strict";
|
|
3693
|
-
|
|
3717
|
+
init_ring_buffer();
|
|
3718
|
+
DEFAULT_CAPACITY2 = 200;
|
|
3694
3719
|
MessageQueue = class {
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
q = [];
|
|
3702
|
-
this._queues.set(agentId, q);
|
|
3720
|
+
_buffers = /* @__PURE__ */ new Map();
|
|
3721
|
+
_getOrCreate(agentId) {
|
|
3722
|
+
let buf = this._buffers.get(agentId);
|
|
3723
|
+
if (!buf) {
|
|
3724
|
+
buf = new AgentRingBuffer(DEFAULT_CAPACITY2);
|
|
3725
|
+
this._buffers.set(agentId, buf);
|
|
3703
3726
|
}
|
|
3704
|
-
|
|
3705
|
-
|
|
3727
|
+
return buf;
|
|
3728
|
+
}
|
|
3729
|
+
/** Enqueue a message for agentId. Returns the unread count after enqueue. */
|
|
3730
|
+
enqueue(agentId, envelope) {
|
|
3731
|
+
const buf = this._getOrCreate(agentId);
|
|
3732
|
+
buf.write("received", envelope);
|
|
3733
|
+
return buf.unreadReceivedCount();
|
|
3706
3734
|
}
|
|
3707
|
-
/** Pop up to maxCount messages for agentId. */
|
|
3735
|
+
/** Pop up to maxCount received messages for agentId (non-destructive, cursor-based). */
|
|
3708
3736
|
dequeue(agentId, maxCount = 50) {
|
|
3709
|
-
const
|
|
3710
|
-
if (!
|
|
3711
|
-
return
|
|
3737
|
+
const buf = this._buffers.get(agentId);
|
|
3738
|
+
if (!buf) return [];
|
|
3739
|
+
return buf.dequeueReceived(maxCount).map(toEnvelope);
|
|
3712
3740
|
}
|
|
3713
|
-
/** Return the number of pending messages for agentId. */
|
|
3741
|
+
/** Return the number of pending received messages for agentId. */
|
|
3714
3742
|
pendingCount(agentId) {
|
|
3715
|
-
return this.
|
|
3743
|
+
return this._buffers.get(agentId)?.unreadReceivedCount() ?? 0;
|
|
3716
3744
|
}
|
|
3717
|
-
/** Clean up
|
|
3745
|
+
/** Clean up buffer when agent is removed. */
|
|
3718
3746
|
removeAgent(agentId) {
|
|
3719
|
-
this.
|
|
3747
|
+
this._buffers.delete(agentId);
|
|
3720
3748
|
}
|
|
3721
|
-
/** Record a message in an agent's history. */
|
|
3749
|
+
/** Record a message in an agent's history (writes to the same ring buffer). */
|
|
3722
3750
|
logToHistory(agentId, direction, envelope) {
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
h = [];
|
|
3726
|
-
this._history.set(agentId, h);
|
|
3727
|
-
}
|
|
3728
|
-
h.push({
|
|
3729
|
-
direction,
|
|
3730
|
-
sender_id: envelope.sender_id,
|
|
3731
|
-
recipient_id: envelope.recipient_id,
|
|
3732
|
-
summary: envelope.summary,
|
|
3733
|
-
content: envelope.content,
|
|
3734
|
-
timestamp: envelope.timestamp ?? Date.now() / 1e3
|
|
3735
|
-
});
|
|
3736
|
-
if (h.length > HISTORY_CAP) {
|
|
3737
|
-
this._history.set(agentId, h.slice(-HISTORY_CAP));
|
|
3738
|
-
}
|
|
3751
|
+
const buf = this._getOrCreate(agentId);
|
|
3752
|
+
buf.write(direction, envelope);
|
|
3739
3753
|
}
|
|
3740
3754
|
/** Get recent N history entries for an agent. */
|
|
3741
3755
|
getHistory(agentId, limit = 20) {
|
|
3742
|
-
const
|
|
3743
|
-
if (!
|
|
3744
|
-
return
|
|
3756
|
+
const buf = this._buffers.get(agentId);
|
|
3757
|
+
if (!buf) return [];
|
|
3758
|
+
return buf.getRecent(limit).map(toHistoryEntry);
|
|
3745
3759
|
}
|
|
3746
|
-
/** Clean up history when agent is removed. */
|
|
3760
|
+
/** Clean up history when agent is removed (same as removeAgent — unified storage). */
|
|
3747
3761
|
removeAgentHistory(agentId) {
|
|
3748
|
-
this.
|
|
3762
|
+
this._buffers.delete(agentId);
|
|
3749
3763
|
}
|
|
3750
|
-
/**
|
|
3764
|
+
/**
|
|
3765
|
+
* Return a shallow copy of the full history map (for persistence snapshots).
|
|
3766
|
+
* Kept for backward compatibility with persistence v1 consumers.
|
|
3767
|
+
*/
|
|
3751
3768
|
getHistorySnapshot() {
|
|
3752
3769
|
const copy = {};
|
|
3753
|
-
for (const [agentId,
|
|
3754
|
-
copy[agentId] =
|
|
3770
|
+
for (const [agentId, buf] of this._buffers) {
|
|
3771
|
+
copy[agentId] = buf.getRecent().map(toHistoryEntry);
|
|
3755
3772
|
}
|
|
3756
3773
|
return copy;
|
|
3757
3774
|
}
|
|
3775
|
+
/** Return ring buffer snapshots for persistence v2. */
|
|
3776
|
+
getBufferSnapshots() {
|
|
3777
|
+
const result = {};
|
|
3778
|
+
for (const [agentId, buf] of this._buffers) {
|
|
3779
|
+
result[agentId] = buf.getSnapshot();
|
|
3780
|
+
}
|
|
3781
|
+
return result;
|
|
3782
|
+
}
|
|
3783
|
+
/** Restore buffers from v2 snapshots. */
|
|
3784
|
+
restoreBufferSnapshots(snapshots) {
|
|
3785
|
+
for (const [agentId, snap] of Object.entries(snapshots)) {
|
|
3786
|
+
const buf = new AgentRingBuffer(DEFAULT_CAPACITY2);
|
|
3787
|
+
buf.restoreFromSnapshot(snap);
|
|
3788
|
+
this._buffers.set(agentId, buf);
|
|
3789
|
+
}
|
|
3790
|
+
}
|
|
3758
3791
|
};
|
|
3759
3792
|
}
|
|
3760
3793
|
});
|
|
@@ -3769,7 +3802,7 @@ function classifyFetchError(error) {
|
|
|
3769
3802
|
if (error instanceof DOMException && error.name === "AbortError") return null;
|
|
3770
3803
|
return null;
|
|
3771
3804
|
}
|
|
3772
|
-
var UNKNOWN,
|
|
3805
|
+
var UNKNOWN, ALIVE, DEAD, MAX_FAILURES, BACKOFF_BASE, BACKOFF_CAP, PeerDiscovery;
|
|
3773
3806
|
var init_peer_discovery = __esm({
|
|
3774
3807
|
"src/server/peer-discovery.ts"() {
|
|
3775
3808
|
"use strict";
|
|
@@ -3778,17 +3811,11 @@ var init_peer_discovery = __esm({
|
|
|
3778
3811
|
init_persistence();
|
|
3779
3812
|
init_logger();
|
|
3780
3813
|
UNKNOWN = "UNKNOWN";
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
DOWN = "DOWN";
|
|
3785
|
-
NOT_SERVER = "NOT_SERVER";
|
|
3786
|
-
MAYBE = "MAYBE";
|
|
3787
|
-
MAYBE_MAX_RETRIES = 3;
|
|
3814
|
+
ALIVE = "ALIVE";
|
|
3815
|
+
DEAD = "DEAD";
|
|
3816
|
+
MAX_FAILURES = 3;
|
|
3788
3817
|
BACKOFF_BASE = 60;
|
|
3789
3818
|
BACKOFF_CAP = 900;
|
|
3790
|
-
FAILURE_SUSPECT = 2;
|
|
3791
|
-
FAILURE_DOWN = 3;
|
|
3792
3819
|
PeerDiscovery = class {
|
|
3793
3820
|
_selfIp;
|
|
3794
3821
|
_peers = /* @__PURE__ */ new Map();
|
|
@@ -3803,10 +3830,10 @@ var init_peer_discovery = __esm({
|
|
|
3803
3830
|
getPeerForAgent(agentId) {
|
|
3804
3831
|
return this._remoteAgents.get(agentId)?.sourcePeerIp;
|
|
3805
3832
|
}
|
|
3806
|
-
/** Return true if the peer is
|
|
3833
|
+
/** Return true if the peer is alive. */
|
|
3807
3834
|
isPeerReachable(peerIp) {
|
|
3808
3835
|
const ps = this._peers.get(peerIp);
|
|
3809
|
-
return ps !== void 0 &&
|
|
3836
|
+
return ps !== void 0 && ps.status === ALIVE;
|
|
3810
3837
|
}
|
|
3811
3838
|
/** Return all remote agents (including unreachable ones). */
|
|
3812
3839
|
getAllRemoteAgents() {
|
|
@@ -3816,7 +3843,7 @@ var init_peer_discovery = __esm({
|
|
|
3816
3843
|
getReachableRemoteAgents() {
|
|
3817
3844
|
return Array.from(this._remoteAgents.values()).filter((e) => e.reachable).map((e) => e.agentInfo);
|
|
3818
3845
|
}
|
|
3819
|
-
/** Return all known peer states
|
|
3846
|
+
/** Return all known peer states. */
|
|
3820
3847
|
getPeerStates() {
|
|
3821
3848
|
return Array.from(this._peers.values()).map((ps) => ({
|
|
3822
3849
|
ip: ps.ip,
|
|
@@ -3852,23 +3879,19 @@ var init_peer_discovery = __esm({
|
|
|
3852
3879
|
const peerIps = this.getTailscalePeers();
|
|
3853
3880
|
for (const ip of peerIps) {
|
|
3854
3881
|
if (!this._peers.has(ip)) {
|
|
3855
|
-
this._peers.set(ip, { ip, status: UNKNOWN, consecutiveFailures: 0, lastProbeAt: 0, backoffUntil: null
|
|
3882
|
+
this._peers.set(ip, { ip, status: UNKNOWN, consecutiveFailures: 0, lastProbeAt: 0, backoffUntil: null });
|
|
3856
3883
|
}
|
|
3857
3884
|
}
|
|
3858
3885
|
const now = performance.now() / 1e3;
|
|
3859
3886
|
for (const ip of peerIps) {
|
|
3860
3887
|
const ps = this._peers.get(ip);
|
|
3861
|
-
if (ps.status ===
|
|
3888
|
+
if (ps.status === DEAD) {
|
|
3862
3889
|
if (ps.backoffUntil !== null && now < ps.backoffUntil) continue;
|
|
3863
3890
|
ps.status = UNKNOWN;
|
|
3864
3891
|
}
|
|
3865
|
-
if (ps.status === MAYBE && ps.maybeRetries >= MAYBE_MAX_RETRIES) {
|
|
3866
|
-
this.transition(ps, NOT_SERVER);
|
|
3867
|
-
continue;
|
|
3868
|
-
}
|
|
3869
3892
|
await this.probePeer(ps);
|
|
3870
3893
|
}
|
|
3871
|
-
this.
|
|
3894
|
+
this.evictDeadAgents();
|
|
3872
3895
|
this.evictStaleAgents();
|
|
3873
3896
|
}
|
|
3874
3897
|
// ------------------------------------------------------------------
|
|
@@ -3906,12 +3929,10 @@ var init_peer_discovery = __esm({
|
|
|
3906
3929
|
async probePeer(ps) {
|
|
3907
3930
|
ps.lastProbeAt = Date.now() / 1e3;
|
|
3908
3931
|
const healthy = await this.checkHealth(ps.ip);
|
|
3909
|
-
if (healthy ===
|
|
3910
|
-
this.handleProbeFailure(ps, true);
|
|
3911
|
-
} else if (healthy) {
|
|
3932
|
+
if (healthy === true) {
|
|
3912
3933
|
await this.handleProbeSuccess(ps);
|
|
3913
|
-
} else {
|
|
3914
|
-
this.handleProbeFailure(ps
|
|
3934
|
+
} else if (healthy === false) {
|
|
3935
|
+
this.handleProbeFailure(ps);
|
|
3915
3936
|
}
|
|
3916
3937
|
}
|
|
3917
3938
|
async checkHealth(ip) {
|
|
@@ -3932,55 +3953,24 @@ var init_peer_discovery = __esm({
|
|
|
3932
3953
|
// ------------------------------------------------------------------
|
|
3933
3954
|
async handleProbeSuccess(ps) {
|
|
3934
3955
|
ps.consecutiveFailures = 0;
|
|
3935
|
-
ps.maybeRetries = 0;
|
|
3936
3956
|
ps.backoffUntil = null;
|
|
3937
|
-
if (ps.status !==
|
|
3938
|
-
|
|
3957
|
+
if (ps.status !== ALIVE) {
|
|
3958
|
+
const old = ps.status;
|
|
3959
|
+
ps.status = ALIVE;
|
|
3960
|
+
logger.info("Discovery", `Peer ${ps.ip}: ${old} -> ALIVE`);
|
|
3939
3961
|
}
|
|
3940
3962
|
await this.syncAgents(ps.ip);
|
|
3941
3963
|
}
|
|
3942
|
-
handleProbeFailure(ps
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
}
|
|
3951
|
-
} else {
|
|
3952
|
-
this.transition(ps, NOT_SERVER);
|
|
3953
|
-
}
|
|
3954
|
-
} else if (ps.status === PARTY_SERVER) {
|
|
3955
|
-
ps.consecutiveFailures++;
|
|
3956
|
-
if (ps.consecutiveFailures >= FAILURE_DOWN) {
|
|
3957
|
-
this.transition(ps, DOWN);
|
|
3958
|
-
} else if (ps.consecutiveFailures >= FAILURE_SUSPECT) {
|
|
3959
|
-
this.transition(ps, SUSPECT);
|
|
3960
|
-
} else {
|
|
3961
|
-
this.transition(ps, DEGRADED);
|
|
3964
|
+
handleProbeFailure(ps) {
|
|
3965
|
+
ps.consecutiveFailures++;
|
|
3966
|
+
if (ps.consecutiveFailures >= MAX_FAILURES) {
|
|
3967
|
+
const old = ps.status;
|
|
3968
|
+
ps.status = DEAD;
|
|
3969
|
+
if (old !== DEAD) {
|
|
3970
|
+
const delay = Math.min(BACKOFF_BASE * Math.pow(2, ps.consecutiveFailures - 1), BACKOFF_CAP);
|
|
3971
|
+
ps.backoffUntil = performance.now() / 1e3 + delay;
|
|
3972
|
+
logger.info("Discovery", `Peer ${ps.ip}: ${old} -> DEAD (backoff ${delay}s)`);
|
|
3962
3973
|
}
|
|
3963
|
-
} else if (ps.status === DEGRADED || ps.status === SUSPECT) {
|
|
3964
|
-
ps.consecutiveFailures++;
|
|
3965
|
-
if (ps.consecutiveFailures >= FAILURE_DOWN) {
|
|
3966
|
-
this.transition(ps, DOWN);
|
|
3967
|
-
} else if (ps.status === DEGRADED && ps.consecutiveFailures >= FAILURE_SUSPECT) {
|
|
3968
|
-
this.transition(ps, SUSPECT);
|
|
3969
|
-
}
|
|
3970
|
-
}
|
|
3971
|
-
}
|
|
3972
|
-
transition(ps, newStatus) {
|
|
3973
|
-
const old = ps.status;
|
|
3974
|
-
ps.status = newStatus;
|
|
3975
|
-
if (old !== newStatus) {
|
|
3976
|
-
logger.info("Discovery", `Peer ${ps.ip}: ${old} -> ${newStatus}`);
|
|
3977
|
-
}
|
|
3978
|
-
if (newStatus === NOT_SERVER) {
|
|
3979
|
-
const retries = ps.maybeRetries > 0 ? ps.maybeRetries : 1;
|
|
3980
|
-
const delay = Math.min(BACKOFF_BASE * Math.pow(2, retries - 1), BACKOFF_CAP);
|
|
3981
|
-
ps.backoffUntil = performance.now() / 1e3 + delay;
|
|
3982
|
-
}
|
|
3983
|
-
if (newStatus === DOWN) {
|
|
3984
3974
|
for (const entry of this._remoteAgents.values()) {
|
|
3985
3975
|
if (entry.sourcePeerIp === ps.ip) {
|
|
3986
3976
|
entry.reachable = false;
|
|
@@ -4023,14 +4013,14 @@ var init_peer_discovery = __esm({
|
|
|
4023
4013
|
// ------------------------------------------------------------------
|
|
4024
4014
|
// Cleanup
|
|
4025
4015
|
// ------------------------------------------------------------------
|
|
4026
|
-
|
|
4027
|
-
const
|
|
4016
|
+
evictDeadAgents() {
|
|
4017
|
+
const deadPeers = /* @__PURE__ */ new Set();
|
|
4028
4018
|
for (const [ip, ps] of this._peers) {
|
|
4029
|
-
if (ps.status ===
|
|
4019
|
+
if (ps.status === DEAD) deadPeers.add(ip);
|
|
4030
4020
|
}
|
|
4031
|
-
if (!
|
|
4021
|
+
if (!deadPeers.size) return;
|
|
4032
4022
|
for (const [aid, entry] of this._remoteAgents) {
|
|
4033
|
-
if (
|
|
4023
|
+
if (deadPeers.has(entry.sourcePeerIp)) {
|
|
4034
4024
|
this._remoteAgents.delete(aid);
|
|
4035
4025
|
}
|
|
4036
4026
|
}
|
|
@@ -4053,8 +4043,10 @@ var AgentRegistry;
|
|
|
4053
4043
|
var init_registry = __esm({
|
|
4054
4044
|
"src/server/registry.ts"() {
|
|
4055
4045
|
"use strict";
|
|
4046
|
+
init_config();
|
|
4056
4047
|
AgentRegistry = class {
|
|
4057
4048
|
_agents = /* @__PURE__ */ new Map();
|
|
4049
|
+
_staleCounts = /* @__PURE__ */ new Map();
|
|
4058
4050
|
_selfIp;
|
|
4059
4051
|
constructor(selfIp) {
|
|
4060
4052
|
this._selfIp = selfIp;
|
|
@@ -4067,19 +4059,23 @@ var init_registry = __esm({
|
|
|
4067
4059
|
host_ip: this._selfIp,
|
|
4068
4060
|
registered_at: now,
|
|
4069
4061
|
last_heartbeat: now,
|
|
4070
|
-
metadata: req.metadata ?? {}
|
|
4062
|
+
metadata: req.metadata ?? {},
|
|
4063
|
+
callback_url: req.callback_url
|
|
4071
4064
|
};
|
|
4072
4065
|
this._agents.set(req.agent_id, info);
|
|
4066
|
+
this._staleCounts.set(req.agent_id, 0);
|
|
4073
4067
|
return info;
|
|
4074
4068
|
}
|
|
4075
4069
|
remove(agentId) {
|
|
4076
4070
|
const existed = this._agents.delete(agentId);
|
|
4071
|
+
this._staleCounts.delete(agentId);
|
|
4077
4072
|
return existed;
|
|
4078
4073
|
}
|
|
4079
4074
|
heartbeat(agentId) {
|
|
4080
4075
|
const info = this._agents.get(agentId);
|
|
4081
4076
|
if (!info) throw new Error(`Agent '${agentId}' not registered`);
|
|
4082
4077
|
info.last_heartbeat = Date.now() / 1e3;
|
|
4078
|
+
this._staleCounts.set(agentId, 0);
|
|
4083
4079
|
return info;
|
|
4084
4080
|
}
|
|
4085
4081
|
get(agentId) {
|
|
@@ -4088,19 +4084,30 @@ var init_registry = __esm({
|
|
|
4088
4084
|
listAll() {
|
|
4089
4085
|
return Array.from(this._agents.values());
|
|
4090
4086
|
}
|
|
4091
|
-
/** Remove agents whose last heartbeat is older than timeout seconds.
|
|
4087
|
+
/** Remove agents whose last heartbeat is older than timeout seconds.
|
|
4088
|
+
* Uses a stale counter: an agent must exceed the timeout STALE_THRESHOLD
|
|
4089
|
+
* consecutive times before being actually removed. Any heartbeat resets
|
|
4090
|
+
* the counter to zero, giving transient network issues room to recover.
|
|
4091
|
+
*/
|
|
4092
4092
|
cleanupStale(timeout) {
|
|
4093
4093
|
const now = Date.now() / 1e3;
|
|
4094
|
-
const
|
|
4094
|
+
const toRemove = [];
|
|
4095
4095
|
for (const [aid, info] of this._agents) {
|
|
4096
4096
|
if (now - info.last_heartbeat > timeout) {
|
|
4097
|
-
|
|
4097
|
+
const count = (this._staleCounts.get(aid) ?? 0) + 1;
|
|
4098
|
+
this._staleCounts.set(aid, count);
|
|
4099
|
+
if (count >= STALE_THRESHOLD) {
|
|
4100
|
+
toRemove.push(aid);
|
|
4101
|
+
}
|
|
4102
|
+
} else {
|
|
4103
|
+
this._staleCounts.set(aid, 0);
|
|
4098
4104
|
}
|
|
4099
4105
|
}
|
|
4100
|
-
for (const aid of
|
|
4106
|
+
for (const aid of toRemove) {
|
|
4101
4107
|
this._agents.delete(aid);
|
|
4108
|
+
this._staleCounts.delete(aid);
|
|
4102
4109
|
}
|
|
4103
|
-
return
|
|
4110
|
+
return toRemove;
|
|
4104
4111
|
}
|
|
4105
4112
|
};
|
|
4106
4113
|
}
|
|
@@ -4118,7 +4125,6 @@ __export(state_exports, {
|
|
|
4118
4125
|
messageQueue: () => messageQueue,
|
|
4119
4126
|
refreshSelfIp: () => refreshSelfIp,
|
|
4120
4127
|
registry: () => registry,
|
|
4121
|
-
scheduleSnapshot: () => scheduleSnapshot,
|
|
4122
4128
|
snapshotManager: () => snapshotManager
|
|
4123
4129
|
});
|
|
4124
4130
|
function resolveSelfIp() {
|
|
@@ -4139,12 +4145,6 @@ function refreshSelfIp() {
|
|
|
4139
4145
|
_selfIp = resolveSelfIp();
|
|
4140
4146
|
return _selfIp;
|
|
4141
4147
|
}
|
|
4142
|
-
function scheduleSnapshot() {
|
|
4143
|
-
snapshotManager?.scheduleSnapshot(
|
|
4144
|
-
() => registry.listAll(),
|
|
4145
|
-
() => messageQueue.getHistorySnapshot()
|
|
4146
|
-
);
|
|
4147
|
-
}
|
|
4148
4148
|
function initSnapshotManager(mgr) {
|
|
4149
4149
|
snapshotManager = mgr;
|
|
4150
4150
|
}
|
|
@@ -4183,12 +4183,38 @@ var init_models = __esm({
|
|
|
4183
4183
|
}
|
|
4184
4184
|
});
|
|
4185
4185
|
|
|
4186
|
+
// src/server/callback.ts
|
|
4187
|
+
function postCallback(callbackUrl, payload) {
|
|
4188
|
+
fetch(callbackUrl, {
|
|
4189
|
+
method: "POST",
|
|
4190
|
+
headers: { "Content-Type": "application/json" },
|
|
4191
|
+
body: JSON.stringify(payload),
|
|
4192
|
+
signal: AbortSignal.timeout(CALLBACK_TIMEOUT_MS)
|
|
4193
|
+
}).then((resp) => {
|
|
4194
|
+
if (!resp.ok) {
|
|
4195
|
+
logger.warn("Webhook", `Callback HTTP error: url=${callbackUrl}, status=${resp.status}`);
|
|
4196
|
+
}
|
|
4197
|
+
}).catch((err) => {
|
|
4198
|
+
logger.warn("Webhook", `Callback failed: url=${callbackUrl}, error=${err.message}`);
|
|
4199
|
+
});
|
|
4200
|
+
}
|
|
4201
|
+
var CALLBACK_TIMEOUT_MS;
|
|
4202
|
+
var init_callback = __esm({
|
|
4203
|
+
"src/server/callback.ts"() {
|
|
4204
|
+
"use strict";
|
|
4205
|
+
init_logger();
|
|
4206
|
+
CALLBACK_TIMEOUT_MS = 5e3;
|
|
4207
|
+
}
|
|
4208
|
+
});
|
|
4209
|
+
|
|
4186
4210
|
// src/server/routes/agent.ts
|
|
4187
4211
|
async function forwardToPeer(peerIp, envelope) {
|
|
4188
4212
|
const url = `http://${peerIp}:${PARTY_PORT}/proxy/receive`;
|
|
4189
4213
|
const payload = { sender_id: envelope.sender_id, content: envelope.content };
|
|
4190
4214
|
if (envelope.recipient_id) payload.recipient_id = envelope.recipient_id;
|
|
4191
4215
|
if (envelope.group_id) payload.group_id = envelope.group_id;
|
|
4216
|
+
if (envelope.summary) payload.summary = envelope.summary;
|
|
4217
|
+
if (envelope.timestamp) payload.timestamp = envelope.timestamp;
|
|
4192
4218
|
try {
|
|
4193
4219
|
await fetch(url, {
|
|
4194
4220
|
method: "POST",
|
|
@@ -4208,28 +4234,49 @@ var init_agent = __esm({
|
|
|
4208
4234
|
init_models();
|
|
4209
4235
|
init_config();
|
|
4210
4236
|
init_logger();
|
|
4237
|
+
init_callback();
|
|
4211
4238
|
agentRoutes = new Hono2();
|
|
4212
4239
|
agentRoutes.post("/register", async (c) => {
|
|
4213
|
-
const { registry: registry2
|
|
4240
|
+
const { registry: registry2 } = await Promise.resolve().then(() => (init_state(), state_exports));
|
|
4214
4241
|
const req = await c.req.json();
|
|
4215
4242
|
const info = registry2.register(req);
|
|
4216
|
-
|
|
4217
|
-
logger.info("Agent", `Registered: ${info.agent_id} (display: "${info.display_name ?? "N/A"}")`);
|
|
4243
|
+
logger.info("Agent", `Registered: ${info.agent_id} (display: "${info.display_name ?? "N/A"}", callback=${info.callback_url ?? "none"})`);
|
|
4218
4244
|
return c.json(sanitizeAgentInfo(info));
|
|
4219
4245
|
});
|
|
4220
4246
|
agentRoutes.post("/remove", async (c) => {
|
|
4221
|
-
const { registry: registry2,
|
|
4247
|
+
const { registry: registry2, messageQueue: messageQueue2 } = await Promise.resolve().then(() => (init_state(), state_exports));
|
|
4222
4248
|
const req = await c.req.json();
|
|
4223
4249
|
const removed = registry2.remove(req.agent_id);
|
|
4224
|
-
if (removed)
|
|
4250
|
+
if (removed) {
|
|
4251
|
+
messageQueue2.removeAgent(req.agent_id);
|
|
4252
|
+
messageQueue2.removeAgentHistory(req.agent_id);
|
|
4253
|
+
}
|
|
4225
4254
|
logger.info("Agent", removed ? `Removed: ${req.agent_id}` : `Remove failed: ${req.agent_id} (not found)`);
|
|
4226
4255
|
return c.json({ status: removed ? "removed" : "not_found" });
|
|
4227
4256
|
});
|
|
4228
4257
|
agentRoutes.post("/heartbeat", async (c) => {
|
|
4229
4258
|
const { registry: registry2 } = await Promise.resolve().then(() => (init_state(), state_exports));
|
|
4230
4259
|
const req = await c.req.json();
|
|
4231
|
-
|
|
4232
|
-
|
|
4260
|
+
let info;
|
|
4261
|
+
try {
|
|
4262
|
+
info = registry2.heartbeat(req.agent_id);
|
|
4263
|
+
if (req.callback_url) {
|
|
4264
|
+
info.callback_url = req.callback_url;
|
|
4265
|
+
}
|
|
4266
|
+
} catch (err) {
|
|
4267
|
+
if (err instanceof Error && err.message.includes("not registered")) {
|
|
4268
|
+
info = registry2.register({
|
|
4269
|
+
agent_id: req.agent_id,
|
|
4270
|
+
display_name: req.display_name,
|
|
4271
|
+
metadata: req.metadata,
|
|
4272
|
+
callback_url: req.callback_url
|
|
4273
|
+
});
|
|
4274
|
+
logger.info("Agent", `Auto-re-registered: ${req.agent_id} (was cleaned up)`);
|
|
4275
|
+
} else {
|
|
4276
|
+
throw err;
|
|
4277
|
+
}
|
|
4278
|
+
}
|
|
4279
|
+
logger.info("Agent", `Heartbeat from ${req.agent_id}${req.callback_url ? `, callback=${req.callback_url}` : ""}`);
|
|
4233
4280
|
return c.json({ status: "ok", last_heartbeat: info.last_heartbeat });
|
|
4234
4281
|
});
|
|
4235
4282
|
agentRoutes.get("/list", async (c) => {
|
|
@@ -4240,18 +4287,31 @@ var init_agent = __esm({
|
|
|
4240
4287
|
return c.json({ agents: sanitizeAgentList(allAgents), count: allAgents.length });
|
|
4241
4288
|
});
|
|
4242
4289
|
agentRoutes.post("/send", async (c) => {
|
|
4243
|
-
const { registry: registry2, messageQueue: messageQueue2, discovery: discovery2
|
|
4290
|
+
const { registry: registry2, messageQueue: messageQueue2, discovery: discovery2 } = await Promise.resolve().then(() => (init_state(), state_exports));
|
|
4244
4291
|
const envelope = await c.req.json();
|
|
4245
4292
|
const recipient = envelope.recipient_id;
|
|
4246
4293
|
if (!recipient) {
|
|
4247
4294
|
return c.json({ status: "error", error: "recipient_id is required" });
|
|
4248
4295
|
}
|
|
4249
|
-
|
|
4296
|
+
const recipientInfo = registry2.get(recipient);
|
|
4297
|
+
if (recipientInfo) {
|
|
4250
4298
|
const stamped = { ...envelope, timestamp: envelope.timestamp ?? Date.now() / 1e3 };
|
|
4251
4299
|
const count = messageQueue2.enqueue(recipient, stamped);
|
|
4252
4300
|
messageQueue2.logToHistory(envelope.sender_id, "sent", stamped);
|
|
4253
|
-
|
|
4254
|
-
|
|
4301
|
+
if (recipientInfo.callback_url) {
|
|
4302
|
+
const callbackPayload = {
|
|
4303
|
+
type: "message_received",
|
|
4304
|
+
recipient_id: recipient,
|
|
4305
|
+
sender_id: envelope.sender_id,
|
|
4306
|
+
summary: envelope.summary,
|
|
4307
|
+
timestamp: stamped.timestamp,
|
|
4308
|
+
pending_count: count
|
|
4309
|
+
};
|
|
4310
|
+
logger.info("Webhook", `Callback: agent=${recipient}, url=${recipientInfo.callback_url}, sender=${envelope.sender_id}`);
|
|
4311
|
+
postCallback(recipientInfo.callback_url, callbackPayload);
|
|
4312
|
+
} else {
|
|
4313
|
+
logger.debug("Webhook", `No callback_url for agent=${recipient}, skipping webhook`);
|
|
4314
|
+
}
|
|
4255
4315
|
logger.info("Agent", `Send ${envelope.sender_id} -> ${recipient}: delivered_locally`);
|
|
4256
4316
|
return c.json({ status: "delivered_locally", target: recipient });
|
|
4257
4317
|
}
|
|
@@ -4261,7 +4321,6 @@ var init_agent = __esm({
|
|
|
4261
4321
|
const result = await forwardToPeer(peerIp, envelope);
|
|
4262
4322
|
if (result.status === "forwarded") {
|
|
4263
4323
|
messageQueue2.logToHistory(envelope.sender_id, "sent", { ...envelope, timestamp: envelope.timestamp ?? Date.now() / 1e3 });
|
|
4264
|
-
scheduleSnapshot3();
|
|
4265
4324
|
logger.info("Agent", `Send ${envelope.sender_id} -> ${recipient}: forwarded (peer ${peerIp})`);
|
|
4266
4325
|
}
|
|
4267
4326
|
return c.json(result);
|
|
@@ -4300,6 +4359,7 @@ var init_proxy = __esm({
|
|
|
4300
4359
|
init_tailscale();
|
|
4301
4360
|
init_state();
|
|
4302
4361
|
init_logger();
|
|
4362
|
+
init_callback();
|
|
4303
4363
|
proxyRoutes = new Hono2();
|
|
4304
4364
|
proxyRoutes.get("/health", async (c) => {
|
|
4305
4365
|
let hostname = "127.0.0.1";
|
|
@@ -4322,9 +4382,22 @@ var init_proxy = __esm({
|
|
|
4322
4382
|
const envelope = await c.req.json();
|
|
4323
4383
|
const stamped = { ...envelope, timestamp: envelope.timestamp ?? Date.now() / 1e3 };
|
|
4324
4384
|
if (envelope.recipient_id) {
|
|
4325
|
-
messageQueue.enqueue(envelope.recipient_id, stamped);
|
|
4385
|
+
const count = messageQueue.enqueue(envelope.recipient_id, stamped);
|
|
4326
4386
|
messageQueue.logToHistory(envelope.recipient_id, "received", stamped);
|
|
4327
4387
|
logger.info("Proxy", `Received msg ${envelope.sender_id} -> ${envelope.recipient_id}`);
|
|
4388
|
+
const recipientInfo = registry.get(envelope.recipient_id);
|
|
4389
|
+
if (recipientInfo?.callback_url) {
|
|
4390
|
+
const callbackPayload = {
|
|
4391
|
+
type: "message_received",
|
|
4392
|
+
recipient_id: envelope.recipient_id,
|
|
4393
|
+
sender_id: envelope.sender_id,
|
|
4394
|
+
summary: envelope.summary,
|
|
4395
|
+
timestamp: stamped.timestamp,
|
|
4396
|
+
pending_count: count
|
|
4397
|
+
};
|
|
4398
|
+
logger.info("Webhook", `Proxy callback: agent=${envelope.recipient_id}, url=${recipientInfo.callback_url}, sender=${envelope.sender_id}`);
|
|
4399
|
+
postCallback(recipientInfo.callback_url, callbackPayload);
|
|
4400
|
+
}
|
|
4328
4401
|
} else {
|
|
4329
4402
|
for (const agent of registry.listAll()) {
|
|
4330
4403
|
messageQueue.enqueue(agent.agent_id, stamped);
|
|
@@ -4358,1298 +4431,68 @@ var init_proxy = __esm({
|
|
|
4358
4431
|
}
|
|
4359
4432
|
});
|
|
4360
4433
|
|
|
4361
|
-
// src/server/
|
|
4362
|
-
var
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
body{
|
|
4383
|
-
background:var(--bg);color:var(--text);font-family:var(--font-sans);
|
|
4384
|
-
min-height:100vh;overflow-x:hidden;position:relative;
|
|
4385
|
-
}
|
|
4386
|
-
/* Grid background */
|
|
4387
|
-
body::before{
|
|
4388
|
-
content:'';position:fixed;inset:0;z-index:0;pointer-events:none;
|
|
4389
|
-
background-image:
|
|
4390
|
-
linear-gradient(rgba(0,255,240,0.03) 1px,transparent 1px),
|
|
4391
|
-
linear-gradient(90deg,rgba(0,255,240,0.03) 1px,transparent 1px);
|
|
4392
|
-
background-size:40px 40px;
|
|
4393
|
-
}
|
|
4394
|
-
/* Scanline overlay */
|
|
4395
|
-
body::after{
|
|
4396
|
-
content:'';position:fixed;inset:0;z-index:9999;pointer-events:none;
|
|
4397
|
-
background:repeating-linear-gradient(
|
|
4398
|
-
0deg,transparent,transparent 2px,rgba(0,0,0,0.08) 2px,rgba(0,0,0,0.08) 4px
|
|
4399
|
-
);
|
|
4400
|
-
}
|
|
4401
|
-
#app{position:relative;z-index:1;max-width:1400px;margin:0 auto;padding:0 20px 40px}
|
|
4402
|
-
|
|
4403
|
-
/* ===== Header ===== */
|
|
4404
|
-
.header{
|
|
4405
|
-
display:flex;align-items:center;justify-content:space-between;
|
|
4406
|
-
padding:16px 0;border-bottom:1px solid var(--border);
|
|
4407
|
-
margin-bottom:20px;flex-wrap:wrap;gap:12px;
|
|
4408
|
-
}
|
|
4409
|
-
.header-title{
|
|
4410
|
-
font-family:var(--font-mono);font-size:1.4rem;font-weight:700;
|
|
4411
|
-
color:var(--cyan);letter-spacing:3px;
|
|
4412
|
-
text-shadow:0 0 10px rgba(0,255,240,0.5),0 0 30px rgba(0,255,240,0.2);
|
|
4413
|
-
}
|
|
4414
|
-
.header-title span{color:var(--muted);font-weight:400;font-size:0.9rem;margin-left:8px;letter-spacing:1px}
|
|
4415
|
-
.header-center{display:flex;align-items:center;gap:16px;flex-wrap:wrap}
|
|
4416
|
-
.header-status{display:flex;align-items:center;gap:6px;font-family:var(--font-mono);font-size:0.85rem}
|
|
4417
|
-
.status-dot{
|
|
4418
|
-
width:8px;height:8px;border-radius:50%;background:var(--green);
|
|
4419
|
-
box-shadow:0 0 6px var(--green);animation:pulse 2s ease-in-out infinite;
|
|
4420
|
-
}
|
|
4421
|
-
.status-dot.offline{background:var(--red);box-shadow:0 0 6px var(--red)}
|
|
4422
|
-
.status-dot.not-installed{background:var(--red);box-shadow:0 0 6px var(--red)}
|
|
4423
|
-
.status-dot.not-connected{background:var(--yellow);box-shadow:0 0 6px var(--yellow)}
|
|
4424
|
-
@keyframes pulse{0%,100%{opacity:1;transform:scale(1)}50%{opacity:0.5;transform:scale(0.8)}}
|
|
4425
|
-
.header-meta{color:var(--muted);font-family:var(--font-mono);font-size:0.8rem}
|
|
4426
|
-
.header-right{font-family:var(--font-mono);font-size:0.85rem;color:var(--muted)}
|
|
4427
|
-
.header-right .value{color:var(--cyan)}
|
|
4428
|
-
|
|
4429
|
-
/* ===== Stats Cards ===== */
|
|
4430
|
-
.stats-row{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:24px}
|
|
4431
|
-
@media(max-width:768px){.stats-row{grid-template-columns:repeat(2,1fr)}}
|
|
4432
|
-
.stat-card{
|
|
4433
|
-
background:var(--card);border:1px solid var(--border);border-radius:8px;
|
|
4434
|
-
padding:16px 20px;position:relative;overflow:hidden;
|
|
4435
|
-
transition:border-color 0.3s;
|
|
4436
|
-
}
|
|
4437
|
-
.stat-card:hover{border-color:var(--border-bright)}
|
|
4438
|
-
.stat-card::before{
|
|
4439
|
-
content:'';position:absolute;top:0;left:0;right:0;height:2px;
|
|
4440
|
-
}
|
|
4441
|
-
.stat-card.cyan::before{background:var(--cyan);box-shadow:0 0 10px var(--cyan)}
|
|
4442
|
-
.stat-card.green::before{background:var(--green);box-shadow:0 0 10px var(--green)}
|
|
4443
|
-
.stat-card.magenta::before{background:var(--magenta);box-shadow:0 0 10px var(--magenta)}
|
|
4444
|
-
.stat-card.yellow::before{background:var(--yellow);box-shadow:0 0 10px var(--yellow)}
|
|
4445
|
-
.stat-value{
|
|
4446
|
-
font-family:var(--font-mono);font-size:2rem;font-weight:700;
|
|
4447
|
-
transition:color 0.3s;
|
|
4448
|
-
}
|
|
4449
|
-
.stat-card.cyan .stat-value{color:var(--cyan)}
|
|
4450
|
-
.stat-card.green .stat-value{color:var(--green)}
|
|
4451
|
-
.stat-card.magenta .stat-value{color:var(--magenta)}
|
|
4452
|
-
.stat-card.yellow .stat-value{color:var(--yellow)}
|
|
4453
|
-
.stat-label{color:var(--muted);font-size:0.8rem;margin-top:4px;text-transform:uppercase;letter-spacing:1px}
|
|
4454
|
-
.stat-value.flash{animation:flash 0.3s ease}
|
|
4455
|
-
@keyframes flash{0%{opacity:1}50%{opacity:0.3}100%{opacity:1}}
|
|
4456
|
-
|
|
4457
|
-
/* ===== Main Grid ===== */
|
|
4458
|
-
.main-grid{display:grid;grid-template-columns:2fr 1fr;gap:20px;margin-bottom:24px}
|
|
4459
|
-
@media(max-width:1024px){.main-grid{grid-template-columns:1fr}}
|
|
4460
|
-
.section{
|
|
4461
|
-
background:var(--card);border:1px solid var(--border);border-radius:8px;
|
|
4462
|
-
overflow:hidden;
|
|
4463
|
-
}
|
|
4464
|
-
.section-header{
|
|
4465
|
-
padding:12px 16px;border-bottom:1px solid var(--border);
|
|
4466
|
-
font-family:var(--font-mono);font-size:0.8rem;font-weight:600;
|
|
4467
|
-
color:var(--muted);letter-spacing:2px;text-transform:uppercase;
|
|
4468
|
-
display:flex;align-items:center;gap:8px;
|
|
4469
|
-
}
|
|
4470
|
-
.section-header .dot{width:6px;height:6px;border-radius:50%;background:var(--cyan)}
|
|
4471
|
-
.section-body{padding:16px}
|
|
4472
|
-
|
|
4473
|
-
/* ===== Topology ===== */
|
|
4474
|
-
.topology-container{display:flex;justify-content:center;align-items:center;min-height:300px}
|
|
4475
|
-
.topology-container svg{width:100%;max-width:500px;height:300px}
|
|
4476
|
-
.topo-node{cursor:pointer;transition:filter 0.3s}
|
|
4477
|
-
.topo-node:hover{filter:brightness(1.3)}
|
|
4478
|
-
.topo-label{font-family:var(--font-mono);font-size:10px;fill:var(--muted)}
|
|
4479
|
-
.topo-badge{
|
|
4480
|
-
font-family:var(--font-mono);font-size:9px;fill:var(--bg);
|
|
4481
|
-
font-weight:700;
|
|
4482
|
-
}
|
|
4483
|
-
|
|
4484
|
-
/* ===== Peer Table ===== */
|
|
4485
|
-
.peer-table{width:100%;border-collapse:collapse;font-size:0.85rem}
|
|
4486
|
-
.peer-table th{
|
|
4487
|
-
text-align:left;padding:8px 10px;color:var(--muted);font-weight:600;
|
|
4488
|
-
font-family:var(--font-mono);font-size:0.75rem;text-transform:uppercase;
|
|
4489
|
-
letter-spacing:1px;border-bottom:1px solid var(--border);
|
|
4434
|
+
// src/server/index.ts
|
|
4435
|
+
var server_exports = {};
|
|
4436
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, unlinkSync as unlinkSync4 } from "fs";
|
|
4437
|
+
import { join as join6, dirname as dirname4 } from "path";
|
|
4438
|
+
import { homedir as homedir5 } from "os";
|
|
4439
|
+
async function periodicCleanup() {
|
|
4440
|
+
while (!lifecycleController.signal.aborted) {
|
|
4441
|
+
try {
|
|
4442
|
+
const removed = registry.cleanupStale(HEARTBEAT_TIMEOUT);
|
|
4443
|
+
if (removed.length > 0) {
|
|
4444
|
+
logger.info("Cleanup", `Removed ${removed.length} stale agent(s): ${removed.join(", ")}`);
|
|
4445
|
+
for (const aid of removed) {
|
|
4446
|
+
messageQueue.removeAgent(aid);
|
|
4447
|
+
messageQueue.removeAgentHistory(aid);
|
|
4448
|
+
}
|
|
4449
|
+
}
|
|
4450
|
+
} catch (e) {
|
|
4451
|
+
logger.error("Cleanup", "Error during cleanup", e);
|
|
4452
|
+
}
|
|
4453
|
+
await abortableSleep(CLEANUP_INTERVAL * 1e3, lifecycleController.signal);
|
|
4454
|
+
}
|
|
4490
4455
|
}
|
|
4491
|
-
|
|
4492
|
-
.
|
|
4493
|
-
|
|
4494
|
-
.
|
|
4495
|
-
display:inline-block;padding:2px 8px;border-radius:4px;font-size:0.7rem;
|
|
4496
|
-
font-weight:600;text-transform:uppercase;letter-spacing:0.5px;
|
|
4456
|
+
function pidFilePath2() {
|
|
4457
|
+
const pluginData = process.env.CLAUDE_PLUGIN_DATA || "";
|
|
4458
|
+
if (pluginData) return join6(pluginData, "server.pid");
|
|
4459
|
+
return join6(homedir5(), ".open-party", "server.pid");
|
|
4497
4460
|
}
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
.
|
|
4502
|
-
|
|
4503
|
-
.
|
|
4504
|
-
|
|
4505
|
-
.
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
.
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
.
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
.
|
|
4518
|
-
|
|
4519
|
-
.
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
.
|
|
4528
|
-
|
|
4529
|
-
.
|
|
4530
|
-
|
|
4531
|
-
.
|
|
4532
|
-
|
|
4533
|
-
border-radius:3px;background:rgba(255,255,255,0.05);color:var(--muted);
|
|
4534
|
-
}
|
|
4535
|
-
.agent-tag.unreachable{background:rgba(255,51,102,0.1);color:var(--red)}
|
|
4536
|
-
|
|
4537
|
-
/* Message feed */
|
|
4538
|
-
.msg-feed{display:flex;flex-direction:column;gap:6px;max-height:400px;overflow-y:auto}
|
|
4539
|
-
.msg-item{
|
|
4540
|
-
padding:10px 12px;border:1px solid var(--border);border-radius:6px;
|
|
4541
|
-
border-left:3px solid var(--cyan);font-size:0.8rem;
|
|
4542
|
-
}
|
|
4543
|
-
.msg-item.received{border-left-color:var(--magenta)}
|
|
4544
|
-
.msg-top{display:flex;justify-content:space-between;align-items:center;margin-bottom:4px}
|
|
4545
|
-
.msg-flow{font-family:var(--font-mono);font-size:0.75rem;color:var(--cyan)}
|
|
4546
|
-
.msg-flow .arrow{color:var(--muted);margin:0 4px}
|
|
4547
|
-
.msg-time{font-family:var(--font-mono);font-size:0.65rem;color:var(--muted)}
|
|
4548
|
-
.msg-content{color:var(--text);font-size:0.8rem;line-height:1.4;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
4549
|
-
|
|
4550
|
-
/* Scrollbar */
|
|
4551
|
-
::-webkit-scrollbar{width:4px}
|
|
4552
|
-
::-webkit-scrollbar-track{background:var(--bg)}
|
|
4553
|
-
::-webkit-scrollbar-thumb{background:var(--border);border-radius:2px}
|
|
4554
|
-
::-webkit-scrollbar-thumb:hover{background:var(--border-bright)}
|
|
4555
|
-
|
|
4556
|
-
/* Footer */
|
|
4557
|
-
.footer{
|
|
4558
|
-
text-align:center;padding:20px 0;border-top:1px solid var(--border);
|
|
4559
|
-
color:var(--muted);font-family:var(--font-mono);font-size:0.75rem;
|
|
4560
|
-
letter-spacing:1px;
|
|
4561
|
-
}
|
|
4562
|
-
|
|
4563
|
-
/* Join Network Button */
|
|
4564
|
-
.btn-join{
|
|
4565
|
-
font-family:var(--font-mono);font-size:0.75rem;letter-spacing:1px;
|
|
4566
|
-
padding:6px 14px;border:1px solid var(--cyan);border-radius:4px;
|
|
4567
|
-
background:rgba(0,255,240,0.08);color:var(--cyan);cursor:pointer;
|
|
4568
|
-
transition:all 0.2s;text-transform:uppercase;margin-left:12px;
|
|
4569
|
-
}
|
|
4570
|
-
.btn-join:hover{background:rgba(0,255,240,0.18);box-shadow:0 0 10px rgba(0,255,240,0.2)}
|
|
4571
|
-
.btn-join:active{transform:scale(0.97)}
|
|
4572
|
-
.btn-logout{border-color:var(--red);color:var(--red);background:rgba(255,51,102,0.08)}
|
|
4573
|
-
.btn-logout:hover{background:rgba(255,51,102,0.18);box-shadow:0 0 10px rgba(255,51,102,0.2)}
|
|
4574
|
-
.btn-install{border-color:var(--yellow);color:var(--yellow);background:rgba(255,170,0,0.08)}
|
|
4575
|
-
.btn-install:hover{background:rgba(255,170,0,0.18);box-shadow:0 0 10px rgba(255,170,0,0.2)}
|
|
4576
|
-
|
|
4577
|
-
/* Tab bar inside modal */
|
|
4578
|
-
.tab-bar{display:flex;gap:0;margin-bottom:18px;border-bottom:1px solid var(--border)}
|
|
4579
|
-
.tab-bar .tab{
|
|
4580
|
-
font-family:var(--font-mono);font-size:0.8rem;padding:8px 16px;cursor:pointer;
|
|
4581
|
-
color:var(--muted);border-bottom:2px solid transparent;transition:all 0.2s;
|
|
4582
|
-
}
|
|
4583
|
-
.tab-bar .tab:hover{color:var(--text)}
|
|
4584
|
-
.tab-bar .tab.active{color:var(--cyan);border-bottom-color:var(--cyan)}
|
|
4585
|
-
.tab-content{display:none}
|
|
4586
|
-
.tab-content.active{display:block}
|
|
4587
|
-
|
|
4588
|
-
/* Modal */
|
|
4589
|
-
.modal-overlay{
|
|
4590
|
-
position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:10000;
|
|
4591
|
-
display:flex;align-items:center;justify-content:center;
|
|
4592
|
-
opacity:0;pointer-events:none;transition:opacity 0.25s;
|
|
4593
|
-
}
|
|
4594
|
-
.modal-overlay.open{opacity:1;pointer-events:all}
|
|
4595
|
-
.modal{
|
|
4596
|
-
background:var(--card);border:1px solid var(--border-bright);border-radius:10px;
|
|
4597
|
-
padding:28px 32px;width:90%;max-width:440px;
|
|
4598
|
-
box-shadow:0 0 40px rgba(0,255,240,0.08),0 8px 32px rgba(0,0,0,0.5);
|
|
4599
|
-
}
|
|
4600
|
-
.modal-title{
|
|
4601
|
-
font-family:var(--font-mono);font-size:1rem;font-weight:700;color:var(--cyan);
|
|
4602
|
-
letter-spacing:2px;margin-bottom:6px;
|
|
4603
|
-
text-shadow:0 0 8px rgba(0,255,240,0.3);
|
|
4604
|
-
}
|
|
4605
|
-
.modal-desc{color:var(--muted);font-size:0.8rem;margin-bottom:20px;line-height:1.5}
|
|
4606
|
-
.modal-input{
|
|
4607
|
-
width:100%;padding:10px 14px;background:var(--bg);border:1px solid var(--border);
|
|
4608
|
-
border-radius:6px;color:var(--text);font-family:var(--font-mono);font-size:0.85rem;
|
|
4609
|
-
outline:none;transition:border-color 0.2s;
|
|
4610
|
-
}
|
|
4611
|
-
.modal-input:focus{border-color:var(--cyan);box-shadow:0 0 8px rgba(0,255,240,0.15)}
|
|
4612
|
-
.modal-input::placeholder{color:var(--muted)}
|
|
4613
|
-
.modal-actions{display:flex;gap:10px;margin-top:18px;justify-content:flex-end}
|
|
4614
|
-
.modal-btn{
|
|
4615
|
-
font-family:var(--font-mono);font-size:0.8rem;padding:8px 18px;
|
|
4616
|
-
border-radius:4px;cursor:pointer;transition:all 0.2s;letter-spacing:0.5px;
|
|
4617
|
-
}
|
|
4618
|
-
.modal-btn-cancel{
|
|
4619
|
-
border:1px solid var(--border);background:transparent;color:var(--muted);
|
|
4620
|
-
}
|
|
4621
|
-
.modal-btn-cancel:hover{border-color:var(--muted);color:var(--text)}
|
|
4622
|
-
.modal-btn-submit{
|
|
4623
|
-
border:1px solid var(--cyan);background:rgba(0,255,240,0.12);color:var(--cyan);
|
|
4624
|
-
}
|
|
4625
|
-
.modal-btn-submit:hover{background:rgba(0,255,240,0.22);box-shadow:0 0 10px rgba(0,255,240,0.2)}
|
|
4626
|
-
.modal-btn-submit:disabled{opacity:0.4;cursor:not-allowed}
|
|
4627
|
-
.modal-status{
|
|
4628
|
-
margin-top:12px;padding:8px 12px;border-radius:4px;font-size:0.78rem;
|
|
4629
|
-
font-family:var(--font-mono);display:none;
|
|
4630
|
-
}
|
|
4631
|
-
.modal-status.success{display:block;background:rgba(0,255,136,0.1);border:1px solid rgba(0,255,136,0.3);color:var(--green)}
|
|
4632
|
-
.modal-status.error{display:block;background:rgba(255,51,102,0.1);border:1px solid rgba(255,51,102,0.3);color:var(--red)}
|
|
4633
|
-
.spinner{
|
|
4634
|
-
display:inline-block;width:12px;height:12px;border:2px solid transparent;
|
|
4635
|
-
border-top-color:var(--cyan);border-radius:50%;animation:spin 0.6s linear infinite;
|
|
4636
|
-
vertical-align:middle;margin-right:6px;
|
|
4637
|
-
}
|
|
4638
|
-
@keyframes spin{to{transform:rotate(360deg)}}
|
|
4639
|
-
|
|
4640
|
-
/* ===== Tailscale Status Panel ===== */
|
|
4641
|
-
.ts-panel{
|
|
4642
|
-
margin-top:12px;padding:16px 20px;background:var(--card);border:1px solid var(--border);
|
|
4643
|
-
border-radius:8px;font-family:var(--font-mono);font-size:0.8rem;
|
|
4644
|
-
}
|
|
4645
|
-
.ts-panel-title{
|
|
4646
|
-
font-weight:700;letter-spacing:2px;text-transform:uppercase;margin-bottom:10px;
|
|
4647
|
-
display:flex;align-items:center;gap:8px;
|
|
4648
|
-
}
|
|
4649
|
-
.ts-panel-title.connected{color:var(--green)}
|
|
4650
|
-
.ts-panel-title.not-installed{color:var(--red)}
|
|
4651
|
-
.ts-panel-title.not-connected{color:var(--yellow)}
|
|
4652
|
-
.ts-info-row{display:flex;gap:8px;margin:4px 0;color:var(--muted)}
|
|
4653
|
-
.ts-info-row .label{min-width:100px;color:var(--muted)}
|
|
4654
|
-
.ts-info-row .value{color:var(--text)}
|
|
4655
|
-
.ts-install-guide{
|
|
4656
|
-
margin-top:12px;padding:12px 16px;background:rgba(0,0,0,0.3);border:1px solid var(--border);
|
|
4657
|
-
border-radius:6px;
|
|
4658
|
-
}
|
|
4659
|
-
.ts-install-guide .cmd{
|
|
4660
|
-
display:flex;align-items:center;justify-content:space-between;
|
|
4661
|
-
padding:6px 10px;background:rgba(255,255,255,0.04);border-radius:4px;
|
|
4662
|
-
margin:6px 0;font-size:0.8rem;cursor:pointer;transition:background 0.2s;
|
|
4663
|
-
}
|
|
4664
|
-
.ts-install-guide .cmd:hover{background:rgba(255,255,255,0.08)}
|
|
4665
|
-
.ts-install-guide .cmd code{color:var(--cyan)}
|
|
4666
|
-
.ts-install-guide .copy-hint{color:var(--muted);font-size:0.7rem}
|
|
4667
|
-
.ts-install-guide a{color:var(--cyan);text-decoration:none}
|
|
4668
|
-
.ts-install-guide a:hover{text-decoration:underline}
|
|
4669
|
-
.ts-setup-hint{
|
|
4670
|
-
margin-top:10px;padding:8px 12px;background:rgba(0,255,240,0.05);border:1px solid rgba(0,255,240,0.15);
|
|
4671
|
-
border-radius:4px;color:var(--cyan);font-size:0.78rem;
|
|
4672
|
-
}
|
|
4673
|
-
.btn-redetect{
|
|
4674
|
-
font-family:var(--font-mono);font-size:0.7rem;padding:4px 12px;
|
|
4675
|
-
border:1px solid var(--border-bright);border-radius:4px;background:transparent;
|
|
4676
|
-
color:var(--muted);cursor:pointer;transition:all 0.2s;margin-top:8px;
|
|
4677
|
-
}
|
|
4678
|
-
.btn-redetect:hover{border-color:var(--cyan);color:var(--cyan)}
|
|
4679
|
-
</style>
|
|
4680
|
-
</head>
|
|
4681
|
-
<body>
|
|
4682
|
-
<div id="app">
|
|
4683
|
-
<header class="header">
|
|
4684
|
-
<div class="header-title">OPEN PARTY<span>// Dashboard</span></div>
|
|
4685
|
-
<div class="header-center">
|
|
4686
|
-
<div class="header-status">
|
|
4687
|
-
<div class="status-dot" id="statusDot"></div>
|
|
4688
|
-
<span id="statusText">CONNECTING</span>
|
|
4689
|
-
</div>
|
|
4690
|
-
<div class="header-meta" id="serverInfo">--</div>
|
|
4691
|
-
</div>
|
|
4692
|
-
<div class="header-right">
|
|
4693
|
-
UPTIME <span class="value" id="uptime">--:--:--</span>
|
|
4694
|
-
<button class="btn-join" id="btnJoinNetwork" title="Join Tailscale Network">Join Network</button>
|
|
4695
|
-
</div>
|
|
4696
|
-
</header>
|
|
4697
|
-
|
|
4698
|
-
<!-- Tailscale status panel (shown when not connected) -->
|
|
4699
|
-
<div id="tsPanel" class="ts-panel" style="display:none"></div>
|
|
4700
|
-
|
|
4701
|
-
<div class="stats-row" id="statsRow">
|
|
4702
|
-
<div class="stat-card cyan">
|
|
4703
|
-
<div class="stat-value" id="localAgentCount">-</div>
|
|
4704
|
-
<div class="stat-label">Local Agents</div>
|
|
4705
|
-
</div>
|
|
4706
|
-
<div class="stat-card green">
|
|
4707
|
-
<div class="stat-value" id="remoteAgentCount">-</div>
|
|
4708
|
-
<div class="stat-label">Remote Agents</div>
|
|
4709
|
-
</div>
|
|
4710
|
-
<div class="stat-card magenta">
|
|
4711
|
-
<div class="stat-value" id="peerCount">-</div>
|
|
4712
|
-
<div class="stat-label">Known Peers</div>
|
|
4713
|
-
</div>
|
|
4714
|
-
<div class="stat-card yellow">
|
|
4715
|
-
<div class="stat-value" id="partyServerCount">-</div>
|
|
4716
|
-
<div class="stat-label">Party Servers</div>
|
|
4717
|
-
</div>
|
|
4718
|
-
</div>
|
|
4719
|
-
|
|
4720
|
-
<div class="main-grid">
|
|
4721
|
-
<div class="section">
|
|
4722
|
-
<div class="section-header"><div class="dot"></div>NETWORK TOPOLOGY</div>
|
|
4723
|
-
<div class="section-body">
|
|
4724
|
-
<div class="topology-container" id="topologyContainer">
|
|
4725
|
-
<svg id="topologySvg" viewBox="0 0 500 300"></svg>
|
|
4726
|
-
</div>
|
|
4727
|
-
</div>
|
|
4728
|
-
</div>
|
|
4729
|
-
<div class="section">
|
|
4730
|
-
<div class="section-header"><div class="dot" style="background:var(--green)"></div>PEER HEALTH</div>
|
|
4731
|
-
<div class="section-body" style="padding:0">
|
|
4732
|
-
<div id="peerTableContainer">
|
|
4733
|
-
<table class="peer-table">
|
|
4734
|
-
<thead><tr><th>IP</th><th>Status</th><th>Fails</th><th>Last Probe</th></tr></thead>
|
|
4735
|
-
<tbody id="peerTableBody"></tbody>
|
|
4736
|
-
</table>
|
|
4737
|
-
</div>
|
|
4738
|
-
</div>
|
|
4739
|
-
</div>
|
|
4740
|
-
</div>
|
|
4741
|
-
|
|
4742
|
-
<div class="bottom-grid">
|
|
4743
|
-
<div class="section">
|
|
4744
|
-
<div class="section-header"><div class="dot" style="background:var(--cyan)"></div>AGENT DIRECTORY</div>
|
|
4745
|
-
<div class="section-body">
|
|
4746
|
-
<div class="agent-list" id="agentList"></div>
|
|
4747
|
-
</div>
|
|
4748
|
-
</div>
|
|
4749
|
-
<div class="section">
|
|
4750
|
-
<div class="section-header"><div class="dot" style="background:var(--magenta)"></div>MESSAGE FLOW</div>
|
|
4751
|
-
<div class="section-body">
|
|
4752
|
-
<div class="msg-feed" id="msgFeed"></div>
|
|
4753
|
-
</div>
|
|
4754
|
-
</div>
|
|
4755
|
-
</div>
|
|
4756
|
-
|
|
4757
|
-
<div class="footer">OPEN PARTY v0.1 // DECENTRALIZED AGENT NETWORK</div>
|
|
4758
|
-
|
|
4759
|
-
<!-- Join Network / Login Modal (two tabs: Interactive + Auth Key) -->
|
|
4760
|
-
<div class="modal-overlay" id="joinModal">
|
|
4761
|
-
<div class="modal">
|
|
4762
|
-
<div class="modal-title">CONNECT TO TAILNET</div>
|
|
4763
|
-
<div class="tab-bar" id="joinTabs">
|
|
4764
|
-
<div class="tab active" data-tab="interactive">Interactive</div>
|
|
4765
|
-
<div class="tab" data-tab="authkey">Auth Key</div>
|
|
4766
|
-
</div>
|
|
4767
|
-
|
|
4768
|
-
<!-- Interactive tab -->
|
|
4769
|
-
<div class="tab-content active" id="tabInteractive">
|
|
4770
|
-
<div class="modal-desc">Click the button below to open a browser authentication page.<br>Your Tailscale connection will be established once you authenticate.</div>
|
|
4771
|
-
<div class="modal-status" id="interactiveStatus"></div>
|
|
4772
|
-
<div class="modal-actions">
|
|
4773
|
-
<button class="modal-btn modal-btn-cancel" id="btnCancelJoin">Cancel</button>
|
|
4774
|
-
<button class="modal-btn modal-btn-submit" id="btnInteractiveLogin">Open Browser Login</button>
|
|
4775
|
-
</div>
|
|
4776
|
-
</div>
|
|
4777
|
-
|
|
4778
|
-
<!-- Auth Key tab -->
|
|
4779
|
-
<div class="tab-content" id="tabAuthkey">
|
|
4780
|
-
<div class="modal-desc">Enter your Tailscale auth key to join the network.<br>You can generate one from the Tailscale admin console.</div>
|
|
4781
|
-
<input type="password" class="modal-input" id="authKeyInput" placeholder="tskey-auth-xxxxx..." autocomplete="off" spellcheck="false" />
|
|
4782
|
-
<div class="modal-status" id="joinStatus"></div>
|
|
4783
|
-
<div class="modal-actions">
|
|
4784
|
-
<button class="modal-btn modal-btn-cancel" id="btnCancelAuthkey">Cancel</button>
|
|
4785
|
-
<button class="modal-btn modal-btn-submit" id="btnSubmitJoin">Connect</button>
|
|
4786
|
-
</div>
|
|
4787
|
-
</div>
|
|
4788
|
-
</div>
|
|
4789
|
-
</div>
|
|
4790
|
-
|
|
4791
|
-
<!-- Logout Confirmation Modal -->
|
|
4792
|
-
<div class="modal-overlay" id="logoutModal">
|
|
4793
|
-
<div class="modal">
|
|
4794
|
-
<div class="modal-title" style="color:var(--red)">LOG OUT OF TAILNET</div>
|
|
4795
|
-
<div class="modal-desc">This will disconnect from Tailscale and remove your credentials.<br>You will need to re-authenticate to reconnect.</div>
|
|
4796
|
-
<div class="modal-status" id="logoutStatus"></div>
|
|
4797
|
-
<div class="modal-actions">
|
|
4798
|
-
<button class="modal-btn modal-btn-cancel" id="btnCancelLogout">Cancel</button>
|
|
4799
|
-
<button class="modal-btn modal-btn-submit" style="border-color:var(--red);color:var(--red);background:rgba(255,51,102,0.12)" id="btnConfirmLogout">Log Out</button>
|
|
4800
|
-
</div>
|
|
4801
|
-
</div>
|
|
4802
|
-
</div>
|
|
4803
|
-
</div>
|
|
4804
|
-
|
|
4805
|
-
<script>
|
|
4806
|
-
(function() {
|
|
4807
|
-
'use strict';
|
|
4808
|
-
|
|
4809
|
-
// ---- Helpers ----
|
|
4810
|
-
const $ = (s) => document.querySelector(s);
|
|
4811
|
-
const $$ = (s) => document.querySelectorAll(s);
|
|
4812
|
-
|
|
4813
|
-
function formatUptime(seconds) {
|
|
4814
|
-
const h = Math.floor(seconds / 3600);
|
|
4815
|
-
const m = Math.floor((seconds % 3600) / 60);
|
|
4816
|
-
const s = seconds % 60;
|
|
4817
|
-
return String(h).padStart(2,'0') + ':' + String(m).padStart(2,'0') + ':' + String(s).padStart(2,'0');
|
|
4818
|
-
}
|
|
4819
|
-
|
|
4820
|
-
function timeAgo(ts) {
|
|
4821
|
-
if (!ts) return '--';
|
|
4822
|
-
const diff = Math.floor(Date.now() / 1000 - ts);
|
|
4823
|
-
if (diff < 0) return 'now';
|
|
4824
|
-
if (diff < 60) return diff + 's ago';
|
|
4825
|
-
if (diff < 3600) return Math.floor(diff / 60) + 'm ago';
|
|
4826
|
-
return Math.floor(diff / 3600) + 'h ago';
|
|
4827
|
-
}
|
|
4828
|
-
|
|
4829
|
-
function flashEl(el) {
|
|
4830
|
-
el.classList.remove('flash');
|
|
4831
|
-
void el.offsetWidth;
|
|
4832
|
-
el.classList.add('flash');
|
|
4833
|
-
}
|
|
4834
|
-
|
|
4835
|
-
const statusColors = {
|
|
4836
|
-
PARTY_SERVER: '#00ff88',
|
|
4837
|
-
DEGRADED: '#ffaa00',
|
|
4838
|
-
SUSPECT: '#ff8800',
|
|
4839
|
-
DOWN: '#ff3366',
|
|
4840
|
-
UNKNOWN: '#6a6a8a',
|
|
4841
|
-
NOT_SERVER: '#6a6a8a',
|
|
4842
|
-
MAYBE: '#00fff0',
|
|
4843
|
-
};
|
|
4844
|
-
|
|
4845
|
-
const statusBadgeClass = {
|
|
4846
|
-
PARTY_SERVER: 'badge-party',
|
|
4847
|
-
DEGRADED: 'badge-degraded',
|
|
4848
|
-
SUSPECT: 'badge-suspect',
|
|
4849
|
-
DOWN: 'badge-down',
|
|
4850
|
-
UNKNOWN: 'badge-unknown',
|
|
4851
|
-
NOT_SERVER: 'badge-not_server',
|
|
4852
|
-
MAYBE: 'badge-maybe',
|
|
4853
|
-
};
|
|
4854
|
-
|
|
4855
|
-
// ---- State ----
|
|
4856
|
-
let overview = null;
|
|
4857
|
-
let prevStats = null;
|
|
4858
|
-
|
|
4859
|
-
// ---- Fetch helpers ----
|
|
4860
|
-
async function fetchStats() {
|
|
4861
|
-
try {
|
|
4862
|
-
const r = await fetch('/dashboard/api/stats');
|
|
4863
|
-
return await r.json();
|
|
4864
|
-
} catch { return null; }
|
|
4865
|
-
}
|
|
4866
|
-
|
|
4867
|
-
async function fetchOverview() {
|
|
4868
|
-
try {
|
|
4869
|
-
const r = await fetch('/dashboard/api/overview');
|
|
4870
|
-
return await r.json();
|
|
4871
|
-
} catch { return null; }
|
|
4872
|
-
}
|
|
4873
|
-
|
|
4874
|
-
// ---- Render functions ----
|
|
4875
|
-
function renderHeader(data) {
|
|
4876
|
-
const s = data.server;
|
|
4877
|
-
if (tsState && tsState.state === 'connected') {
|
|
4878
|
-
$('#statusDot').className = 'status-dot';
|
|
4879
|
-
$('#statusText').textContent = 'ONLINE';
|
|
4880
|
-
$('#serverInfo').textContent = s.tailscale_ip + ' // ' + s.hostname;
|
|
4881
|
-
} else if (tsState && tsState.state === 'not_connected') {
|
|
4882
|
-
$('#statusDot').className = 'status-dot not-connected';
|
|
4883
|
-
$('#statusText').textContent = 'NOT CONNECTED';
|
|
4884
|
-
$('#serverInfo').textContent = 'Tailscale installed but not authenticated';
|
|
4885
|
-
} else if (tsState && tsState.state === 'not_installed') {
|
|
4886
|
-
$('#statusDot').className = 'status-dot not-installed';
|
|
4887
|
-
$('#statusText').textContent = 'NO TAILSCALE';
|
|
4888
|
-
$('#serverInfo').textContent = 'Tailscale not installed - local mode';
|
|
4889
|
-
} else {
|
|
4890
|
-
$('#statusDot').className = 'status-dot';
|
|
4891
|
-
$('#statusText').textContent = 'ONLINE';
|
|
4892
|
-
$('#serverInfo').textContent = s.tailscale_ip + ' // ' + s.hostname;
|
|
4893
|
-
}
|
|
4894
|
-
$('#uptime').textContent = formatUptime(s.uptime_seconds);
|
|
4895
|
-
}
|
|
4896
|
-
|
|
4897
|
-
function renderStats(data) {
|
|
4898
|
-
const prev = {
|
|
4899
|
-
local: parseInt($('#localAgentCount').textContent) || 0,
|
|
4900
|
-
remote: parseInt($('#remoteAgentCount').textContent) || 0,
|
|
4901
|
-
peer: parseInt($('#peerCount').textContent) || 0,
|
|
4902
|
-
party: parseInt($('#partyServerCount').textContent) || 0,
|
|
4903
|
-
};
|
|
4904
|
-
const vals = {
|
|
4905
|
-
local: data.agents.local_count,
|
|
4906
|
-
remote: data.agents.remote_count,
|
|
4907
|
-
peer: data.peers.total,
|
|
4908
|
-
party: data.peers.party_servers,
|
|
4909
|
-
};
|
|
4910
|
-
if (vals.local !== prev.local) { $('#localAgentCount').textContent = vals.local; flashEl($('#localAgentCount')); }
|
|
4911
|
-
if (vals.remote !== prev.remote) { $('#remoteAgentCount').textContent = vals.remote; flashEl($('#remoteAgentCount')); }
|
|
4912
|
-
if (vals.peer !== prev.peer) { $('#peerCount').textContent = vals.peer; flashEl($('#peerCount')); }
|
|
4913
|
-
if (vals.party !== prev.party) { $('#partyServerCount').textContent = vals.party; flashEl($('#partyServerCount')); }
|
|
4914
|
-
}
|
|
4915
|
-
|
|
4916
|
-
function renderTopology(data) {
|
|
4917
|
-
const svg = $('#topologySvg');
|
|
4918
|
-
const peers = data.peers.details || [];
|
|
4919
|
-
const cx = 250, cy = 150, radius = 100;
|
|
4920
|
-
|
|
4921
|
-
let html = '';
|
|
4922
|
-
|
|
4923
|
-
// Center node
|
|
4924
|
-
html += '<circle cx="' + cx + '" cy="' + cy + '" r="24" fill="rgba(0,255,240,0.15)" stroke="#00fff0" stroke-width="2">';
|
|
4925
|
-
html += '<animate attributeName="r" values="24;26;24" dur="3s" repeatCount="indefinite"/>';
|
|
4926
|
-
html += '</circle>';
|
|
4927
|
-
html += '<text x="' + cx + '" y="' + cy + '" text-anchor="middle" dominant-baseline="central" fill="#00fff0" font-family="var(--font-mono)" font-size="10" font-weight="700">SELF</text>';
|
|
4928
|
-
html += '<text x="' + cx + '" y="' + (cy + 38) + '" text-anchor="middle" class="topo-label">' + data.server.tailscale_ip + '</text>';
|
|
4929
|
-
|
|
4930
|
-
if (peers.length === 0) {
|
|
4931
|
-
html += '<text x="' + cx + '" y="' + (cy + 60) + '" text-anchor="middle" fill="#6a6a8a" font-family="var(--font-mono)" font-size="11">No peers discovered</text>';
|
|
4932
|
-
}
|
|
4933
|
-
|
|
4934
|
-
peers.forEach(function(p, i) {
|
|
4935
|
-
const angle = (2 * Math.PI * i / Math.max(peers.length, 1)) - Math.PI / 2;
|
|
4936
|
-
const px = cx + radius * Math.cos(angle);
|
|
4937
|
-
const py = cy + radius * Math.sin(angle);
|
|
4938
|
-
const color = statusColors[p.status] || '#6a6a8a';
|
|
4939
|
-
const opacity = (p.status === 'PARTY_SERVER' || p.status === 'DEGRADED' || p.status === 'SUSPECT') ? 1 : 0.4;
|
|
4940
|
-
|
|
4941
|
-
// Connection line
|
|
4942
|
-
html += '<line x1="' + cx + '" y1="' + cy + '" x2="' + px + '" y2="' + py + '" stroke="' + color + '" stroke-width="1" opacity="' + (opacity * 0.3) + '"/>';
|
|
4943
|
-
|
|
4944
|
-
// Peer node
|
|
4945
|
-
html += '<g class="topo-node"><circle cx="' + px + '" cy="' + py + '" r="16" fill="rgba(255,255,255,0.03)" stroke="' + color + '" stroke-width="1.5" opacity="' + opacity + '"/>';
|
|
4946
|
-
html += '<text x="' + px + '" y="' + py + '" text-anchor="middle" dominant-baseline="central" fill="' + color + '" font-family="var(--font-mono)" font-size="8" opacity="' + opacity + '">' + p.ip.split('.').slice(-1)[0] + '</text></g>';
|
|
4947
|
-
|
|
4948
|
-
// IP label
|
|
4949
|
-
html += '<text x="' + px + '" y="' + (py + 26) + '" text-anchor="middle" class="topo-label">' + p.ip + '</text>';
|
|
4950
|
-
});
|
|
4951
|
-
|
|
4952
|
-
svg.innerHTML = html;
|
|
4953
|
-
}
|
|
4954
|
-
|
|
4955
|
-
function renderPeerTable(data) {
|
|
4956
|
-
const tbody = $('#peerTableBody');
|
|
4957
|
-
const peers = data.peers.details || [];
|
|
4958
|
-
|
|
4959
|
-
if (peers.length === 0) {
|
|
4960
|
-
tbody.innerHTML = '<tr><td colspan="4" class="empty-state">No peers discovered</td></tr>';
|
|
4961
|
-
return;
|
|
4962
|
-
}
|
|
4963
|
-
|
|
4964
|
-
// Sort: PARTY_SERVER first, then by severity
|
|
4965
|
-
const order = { PARTY_SERVER: 0, DEGRADED: 1, SUSPECT: 2, MAYBE: 3, UNKNOWN: 4, NOT_SERVER: 5, DOWN: 6 };
|
|
4966
|
-
const sorted = [...peers].sort(function(a, b) { return (order[a.status] || 99) - (order[b.status] || 99); });
|
|
4967
|
-
|
|
4968
|
-
tbody.innerHTML = sorted.map(function(p) {
|
|
4969
|
-
const badge = statusBadgeClass[p.status] || 'badge-unknown';
|
|
4970
|
-
const label = p.status === 'PARTY_SERVER' ? 'SERVER' : p.status === 'NOT_SERVER' ? 'NOT_SVR' : p.status;
|
|
4971
|
-
return '<tr>'
|
|
4972
|
-
+ '<td>' + p.ip + '</td>'
|
|
4973
|
-
+ '<td><span class="badge ' + badge + '">' + label + '</span></td>'
|
|
4974
|
-
+ '<td>' + p.consecutiveFailures + '</td>'
|
|
4975
|
-
+ '<td>' + timeAgo(p.lastProbeAt) + '</td>'
|
|
4976
|
-
+ '</tr>';
|
|
4977
|
-
}).join('');
|
|
4978
|
-
}
|
|
4979
|
-
|
|
4980
|
-
function renderAgents(data) {
|
|
4981
|
-
const container = $('#agentList');
|
|
4982
|
-
const local = data.agents.local_agents || [];
|
|
4983
|
-
const remote = data.agents.remote_agents || [];
|
|
4984
|
-
const all = [
|
|
4985
|
-
...local.map(function(a) { return { ...a, type: 'local' }; }),
|
|
4986
|
-
...remote.map(function(a) { return { ...a, type: 'remote' }; }),
|
|
4987
|
-
];
|
|
4988
|
-
|
|
4989
|
-
if (all.length === 0) {
|
|
4990
|
-
container.innerHTML = '<div class="empty-state">No agents registered</div>';
|
|
4991
|
-
return;
|
|
4992
|
-
}
|
|
4993
|
-
|
|
4994
|
-
container.innerHTML = all.map(function(a) {
|
|
4995
|
-
const initials = (a.display_name || a.agent_id).substring(0, 2).toUpperCase();
|
|
4996
|
-
const tags = [];
|
|
4997
|
-
if (a.type === 'remote') {
|
|
4998
|
-
tags.push('<span class="agent-tag">' + a.source_peer_ip + '</span>');
|
|
4999
|
-
if (!a.reachable) tags.push('<span class="agent-tag unreachable">offline</span>');
|
|
5000
|
-
} else {
|
|
5001
|
-
tags.push('<span class="agent-tag">local</span>');
|
|
5002
|
-
}
|
|
5003
|
-
return '<div class="agent-card ' + a.type + '">'
|
|
5004
|
-
+ '<div class="agent-icon">' + initials + '</div>'
|
|
5005
|
-
+ '<div class="agent-info">'
|
|
5006
|
-
+ '<div class="agent-name">' + (a.display_name || a.agent_id) + '</div>'
|
|
5007
|
-
+ '<div class="agent-id">' + a.agent_id + '</div>'
|
|
5008
|
-
+ '</div>'
|
|
5009
|
-
+ '<div class="agent-meta">' + tags.join('') + '</div>'
|
|
5010
|
-
+ '</div>';
|
|
5011
|
-
}).join('');
|
|
5012
|
-
}
|
|
5013
|
-
|
|
5014
|
-
// Build agent_id \u2192 display_name lookup from local + remote agents
|
|
5015
|
-
function buildNameMap(data) {
|
|
5016
|
-
const map = {};
|
|
5017
|
-
const all = [...(data.agents.local_agents || []), ...(data.agents.remote_agents || [])];
|
|
5018
|
-
for (const a of all) { map[a.agent_id] = a.display_name || a.agent_id; }
|
|
5019
|
-
return map;
|
|
5020
|
-
}
|
|
5021
|
-
|
|
5022
|
-
function resolveName(map, id) { return map[id] || id; }
|
|
5023
|
-
|
|
5024
|
-
function renderMessages(data) {
|
|
5025
|
-
const container = $('#msgFeed');
|
|
5026
|
-
const msgs = data.messages.recent || [];
|
|
5027
|
-
|
|
5028
|
-
if (msgs.length === 0) {
|
|
5029
|
-
container.innerHTML = '<div class="empty-state">No recent messages</div>';
|
|
5030
|
-
return;
|
|
5031
|
-
}
|
|
5032
|
-
|
|
5033
|
-
const names = buildNameMap(data);
|
|
5034
|
-
|
|
5035
|
-
container.innerHTML = msgs.map(function(m) {
|
|
5036
|
-
const dir = m.direction === 'received' ? 'received' : '';
|
|
5037
|
-
const arrow = m.direction === 'received' ? '←' : '→';
|
|
5038
|
-
const flow = m.direction === 'received'
|
|
5039
|
-
? resolveName(names, m.sender_id) + ' <span class="arrow">' + arrow + '</span> ' + resolveName(names, m.agent_id)
|
|
5040
|
-
: resolveName(names, m.agent_id) + ' <span class="arrow">' + arrow + '</span> ' + resolveName(names, m.recipient_id) || 'broadcast';
|
|
5041
|
-
return '<div class="msg-item ' + dir + '">'
|
|
5042
|
-
+ '<div class="msg-top">'
|
|
5043
|
-
+ '<div class="msg-flow">' + flow + '</div>'
|
|
5044
|
-
+ '<div class="msg-time">' + timeAgo(m.timestamp) + '</div>'
|
|
5045
|
-
+ '</div>'
|
|
5046
|
-
+ '<div class="msg-content">' + (m.summary || m.content) + '</div>'
|
|
5047
|
-
+ '</div>';
|
|
5048
|
-
}).join('');
|
|
5049
|
-
}
|
|
5050
|
-
|
|
5051
|
-
function renderAll(data) {
|
|
5052
|
-
renderHeader(data);
|
|
5053
|
-
renderStats(data);
|
|
5054
|
-
renderTopology(data);
|
|
5055
|
-
renderPeerTable(data);
|
|
5056
|
-
renderAgents(data);
|
|
5057
|
-
renderMessages(data);
|
|
5058
|
-
}
|
|
5059
|
-
|
|
5060
|
-
// ---- Polling ----
|
|
5061
|
-
let fullTimer = null;
|
|
5062
|
-
let fastTimer = null;
|
|
5063
|
-
|
|
5064
|
-
async function fullRefresh() {
|
|
5065
|
-
const data = await fetchOverview();
|
|
5066
|
-
if (!data) {
|
|
5067
|
-
$('#statusDot').className = 'status-dot offline';
|
|
5068
|
-
$('#statusText').textContent = 'OFFLINE';
|
|
5069
|
-
return;
|
|
5070
|
-
}
|
|
5071
|
-
overview = data;
|
|
5072
|
-
renderAll(data);
|
|
5073
|
-
}
|
|
5074
|
-
|
|
5075
|
-
async function fastRefresh() {
|
|
5076
|
-
const stats = await fetchStats();
|
|
5077
|
-
if (!stats) return;
|
|
5078
|
-
const changed = JSON.stringify(stats) !== JSON.stringify(prevStats);
|
|
5079
|
-
prevStats = stats;
|
|
5080
|
-
if (changed) {
|
|
5081
|
-
fullRefresh();
|
|
5082
|
-
}
|
|
5083
|
-
}
|
|
5084
|
-
|
|
5085
|
-
// ---- Init ----
|
|
5086
|
-
fullRefresh();
|
|
5087
|
-
fastTimer = setInterval(fastRefresh, 3000);
|
|
5088
|
-
fullTimer = setInterval(fullRefresh, 5000);
|
|
5089
|
-
|
|
5090
|
-
// Clipboard click delegation for .cmd elements
|
|
5091
|
-
document.addEventListener('click', function(e) {
|
|
5092
|
-
var cmd = e.target.closest('.cmd');
|
|
5093
|
-
if (!cmd) return;
|
|
5094
|
-
var text = cmd.getAttribute('data-clipboard');
|
|
5095
|
-
if (text) {
|
|
5096
|
-
navigator.clipboard.writeText(text).then(function() {
|
|
5097
|
-
var hint = cmd.querySelector('.copy-hint');
|
|
5098
|
-
if (hint) hint.textContent = 'Copied!';
|
|
5099
|
-
});
|
|
5100
|
-
}
|
|
5101
|
-
});
|
|
5102
|
-
|
|
5103
|
-
// Update uptime display every second
|
|
5104
|
-
setInterval(function() {
|
|
5105
|
-
if (overview && overview.server) {
|
|
5106
|
-
overview.server.uptime_seconds++;
|
|
5107
|
-
$('#uptime').textContent = formatUptime(overview.server.uptime_seconds);
|
|
5108
|
-
}
|
|
5109
|
-
}, 1000);
|
|
5110
|
-
|
|
5111
|
-
// ---- Join Modal Tabs ----
|
|
5112
|
-
const joinTabs = $$('#joinTabs .tab');
|
|
5113
|
-
joinTabs.forEach(function(tab) {
|
|
5114
|
-
tab.addEventListener('click', function() {
|
|
5115
|
-
joinTabs.forEach(function(t) { t.classList.remove('active'); });
|
|
5116
|
-
tab.classList.add('active');
|
|
5117
|
-
// Toggle tab contents
|
|
5118
|
-
const target = tab.getAttribute('data-tab');
|
|
5119
|
-
$$('.tab-content').forEach(function(tc) { tc.classList.remove('active'); });
|
|
5120
|
-
if (target === 'interactive') {
|
|
5121
|
-
$('#tabInteractive').classList.add('active');
|
|
5122
|
-
} else {
|
|
5123
|
-
$('#tabAuthkey').classList.add('active');
|
|
5124
|
-
}
|
|
5125
|
-
});
|
|
5126
|
-
});
|
|
5127
|
-
|
|
5128
|
-
// ---- Join Network Modal (open/close) ----
|
|
5129
|
-
const joinModal = $('#joinModal');
|
|
5130
|
-
const btnJoin = $('#btnJoinNetwork');
|
|
5131
|
-
const btnCancel = $('#btnCancelJoin');
|
|
5132
|
-
const btnCancelAuthkey = $('#btnCancelAuthkey');
|
|
5133
|
-
const btnSubmit = $('#btnSubmitJoin');
|
|
5134
|
-
const authKeyInput = $('#authKeyInput');
|
|
5135
|
-
const joinStatus = $('#joinStatus');
|
|
5136
|
-
|
|
5137
|
-
function openJoinModal() {
|
|
5138
|
-
// Reset both tabs
|
|
5139
|
-
joinStatus.className = 'modal-status';
|
|
5140
|
-
joinStatus.textContent = '';
|
|
5141
|
-
authKeyInput.value = '';
|
|
5142
|
-
$('#interactiveStatus').className = 'modal-status';
|
|
5143
|
-
$('#interactiveStatus').textContent = '';
|
|
5144
|
-
// Default to Interactive tab
|
|
5145
|
-
$$('#joinTabs .tab').forEach(function(t) { t.classList.remove('active'); });
|
|
5146
|
-
$$('#joinTabs .tab')[0].classList.add('active');
|
|
5147
|
-
$$('.tab-content').forEach(function(tc) { tc.classList.remove('active'); });
|
|
5148
|
-
$('#tabInteractive').classList.add('active');
|
|
5149
|
-
joinModal.classList.add('open');
|
|
5150
|
-
}
|
|
5151
|
-
|
|
5152
|
-
function closeJoinModal() {
|
|
5153
|
-
joinModal.classList.remove('open');
|
|
5154
|
-
}
|
|
5155
|
-
|
|
5156
|
-
btnJoin.addEventListener('click', function() {
|
|
5157
|
-
// Decide action based on Tailscale state
|
|
5158
|
-
if (tsState && tsState.state === 'connected') {
|
|
5159
|
-
openLogoutModal();
|
|
5160
|
-
} else if (tsState && tsState.state === 'not_installed') {
|
|
5161
|
-
doInstallTailscale();
|
|
5162
|
-
} else {
|
|
5163
|
-
openJoinModal();
|
|
5164
|
-
}
|
|
5165
|
-
});
|
|
5166
|
-
btnCancel.addEventListener('click', closeJoinModal);
|
|
5167
|
-
btnCancelAuthkey.addEventListener('click', closeJoinModal);
|
|
5168
|
-
joinModal.addEventListener('click', function(e) {
|
|
5169
|
-
if (e.target === joinModal) closeJoinModal();
|
|
5170
|
-
});
|
|
5171
|
-
authKeyInput.addEventListener('keydown', function(e) {
|
|
5172
|
-
if (e.key === 'Enter') btnSubmit.click();
|
|
5173
|
-
if (e.key === 'Escape') closeJoinModal();
|
|
5174
|
-
});
|
|
5175
|
-
|
|
5176
|
-
// ---- Auth Key submit ----
|
|
5177
|
-
btnSubmit.addEventListener('click', async function() {
|
|
5178
|
-
const key = authKeyInput.value.trim();
|
|
5179
|
-
if (!key) {
|
|
5180
|
-
authKeyInput.focus();
|
|
5181
|
-
return;
|
|
5182
|
-
}
|
|
5183
|
-
btnSubmit.disabled = true;
|
|
5184
|
-
btnSubmit.innerHTML = '<span class="spinner"></span>Connecting...';
|
|
5185
|
-
joinStatus.className = 'modal-status';
|
|
5186
|
-
joinStatus.textContent = '';
|
|
5187
|
-
|
|
5188
|
-
try {
|
|
5189
|
-
const r = await fetch('/dashboard/api/join-network', {
|
|
5190
|
-
method: 'POST',
|
|
5191
|
-
headers: { 'Content-Type': 'application/json' },
|
|
5192
|
-
body: JSON.stringify({ auth_key: key }),
|
|
5193
|
-
});
|
|
5194
|
-
const data = await r.json();
|
|
5195
|
-
if (data.success) {
|
|
5196
|
-
joinStatus.className = 'modal-status success';
|
|
5197
|
-
joinStatus.textContent = 'Successfully joined network!';
|
|
5198
|
-
btnJoin.textContent = 'Logout';
|
|
5199
|
-
btnJoin.className = 'btn-join btn-logout';
|
|
5200
|
-
setTimeout(function() { closeJoinModal(); checkTailscaleStatus(); fullRefresh(); }, 1500);
|
|
5201
|
-
} else {
|
|
5202
|
-
joinStatus.className = 'modal-status error';
|
|
5203
|
-
joinStatus.textContent = data.output || 'Failed to join network';
|
|
5204
|
-
}
|
|
5205
|
-
} catch (e) {
|
|
5206
|
-
joinStatus.className = 'modal-status error';
|
|
5207
|
-
joinStatus.textContent = 'Network error: ' + (e.message || 'unknown');
|
|
5208
|
-
}
|
|
5209
|
-
btnSubmit.disabled = false;
|
|
5210
|
-
btnSubmit.textContent = 'Connect';
|
|
5211
|
-
});
|
|
5212
|
-
|
|
5213
|
-
// ---- Interactive Login ----
|
|
5214
|
-
const btnInteractiveLogin = $('#btnInteractiveLogin');
|
|
5215
|
-
btnInteractiveLogin.addEventListener('click', async function() {
|
|
5216
|
-
const statusEl = $('#interactiveStatus');
|
|
5217
|
-
statusEl.className = 'modal-status';
|
|
5218
|
-
statusEl.textContent = '';
|
|
5219
|
-
btnInteractiveLogin.disabled = true;
|
|
5220
|
-
btnInteractiveLogin.innerHTML = '<span class="spinner"></span>Opening browser...';
|
|
5221
|
-
|
|
5222
|
-
try {
|
|
5223
|
-
const r = await fetch('/dashboard/api/tailscale-login', { method: 'POST' });
|
|
5224
|
-
const data = await r.json();
|
|
5225
|
-
|
|
5226
|
-
if (data.success && data.url) {
|
|
5227
|
-
// Open the auth URL in a new tab
|
|
5228
|
-
window.open(data.url, '_blank');
|
|
5229
|
-
statusEl.className = 'modal-status success';
|
|
5230
|
-
statusEl.textContent = 'Authentication page opened in your browser. Waiting for connection...';
|
|
5231
|
-
|
|
5232
|
-
// Poll for connection
|
|
5233
|
-
var pollCount = 0;
|
|
5234
|
-
var pollInterval = setInterval(async function() {
|
|
5235
|
-
pollCount++;
|
|
5236
|
-
if (pollCount > 40) { // 2 minutes timeout
|
|
5237
|
-
clearInterval(pollInterval);
|
|
5238
|
-
statusEl.className = 'modal-status error';
|
|
5239
|
-
statusEl.textContent = 'Timed out waiting for authentication. Please try again.';
|
|
5240
|
-
btnInteractiveLogin.disabled = false;
|
|
5241
|
-
btnInteractiveLogin.textContent = 'Open Browser Login';
|
|
5242
|
-
return;
|
|
5243
|
-
}
|
|
5244
|
-
try {
|
|
5245
|
-
var sr = await fetch('/dashboard/api/tailscale-status');
|
|
5246
|
-
var sd = await sr.json();
|
|
5247
|
-
if (sd.state === 'connected') {
|
|
5248
|
-
clearInterval(pollInterval);
|
|
5249
|
-
btnJoin.textContent = 'Logout';
|
|
5250
|
-
btnJoin.className = 'btn-join btn-logout';
|
|
5251
|
-
closeJoinModal();
|
|
5252
|
-
checkTailscaleStatus();
|
|
5253
|
-
fullRefresh();
|
|
5254
|
-
return;
|
|
5255
|
-
}
|
|
5256
|
-
} catch { /* poll error, continue */ }
|
|
5257
|
-
}, 3000);
|
|
5258
|
-
} else {
|
|
5259
|
-
statusEl.className = 'modal-status error';
|
|
5260
|
-
statusEl.textContent = data.output || 'Failed to start interactive login';
|
|
5261
|
-
btnInteractiveLogin.disabled = false;
|
|
5262
|
-
btnInteractiveLogin.textContent = 'Open Browser Login';
|
|
5263
|
-
}
|
|
5264
|
-
} catch (e) {
|
|
5265
|
-
statusEl.className = 'modal-status error';
|
|
5266
|
-
statusEl.textContent = 'Network error: ' + (e.message || 'unknown');
|
|
5267
|
-
btnInteractiveLogin.disabled = false;
|
|
5268
|
-
btnInteractiveLogin.textContent = 'Open Browser Login';
|
|
5269
|
-
}
|
|
5270
|
-
});
|
|
5271
|
-
|
|
5272
|
-
// ---- Logout Modal ----
|
|
5273
|
-
const logoutModal = $('#logoutModal');
|
|
5274
|
-
const btnConfirmLogout = $('#btnConfirmLogout');
|
|
5275
|
-
const btnCancelLogout = $('#btnCancelLogout');
|
|
5276
|
-
const logoutStatus = $('#logoutStatus');
|
|
5277
|
-
|
|
5278
|
-
function openLogoutModal() {
|
|
5279
|
-
logoutStatus.className = 'modal-status';
|
|
5280
|
-
logoutStatus.textContent = '';
|
|
5281
|
-
logoutModal.classList.add('open');
|
|
5282
|
-
}
|
|
5283
|
-
|
|
5284
|
-
btnCancelLogout.addEventListener('click', function() { logoutModal.classList.remove('open'); });
|
|
5285
|
-
logoutModal.addEventListener('click', function(e) { if (e.target === logoutModal) logoutModal.classList.remove('open'); });
|
|
5286
|
-
|
|
5287
|
-
btnConfirmLogout.addEventListener('click', async function() {
|
|
5288
|
-
btnConfirmLogout.disabled = true;
|
|
5289
|
-
btnConfirmLogout.innerHTML = '<span class="spinner"></span>Logging out...';
|
|
5290
|
-
logoutStatus.className = 'modal-status';
|
|
5291
|
-
logoutStatus.textContent = '';
|
|
5292
|
-
|
|
5293
|
-
try {
|
|
5294
|
-
const r = await fetch('/dashboard/api/logout', { method: 'POST' });
|
|
5295
|
-
const data = await r.json();
|
|
5296
|
-
logoutModal.classList.remove('open');
|
|
5297
|
-
if (data.success) {
|
|
5298
|
-
checkTailscaleStatus();
|
|
5299
|
-
fullRefresh();
|
|
5300
|
-
} else {
|
|
5301
|
-
alert('Logout failed: ' + (data.output || 'unknown error'));
|
|
5302
|
-
}
|
|
5303
|
-
} catch (e) {
|
|
5304
|
-
logoutModal.classList.remove('open');
|
|
5305
|
-
alert('Network error: ' + (e.message || 'unknown'));
|
|
5306
|
-
}
|
|
5307
|
-
btnConfirmLogout.disabled = false;
|
|
5308
|
-
btnConfirmLogout.textContent = 'Log Out';
|
|
5309
|
-
});
|
|
5310
|
-
|
|
5311
|
-
// ---- Install Tailscale ----
|
|
5312
|
-
async function doInstallTailscale() {
|
|
5313
|
-
if (!confirm('Install Tailscale on this machine?')) return;
|
|
5314
|
-
|
|
5315
|
-
btnJoin.disabled = true;
|
|
5316
|
-
btnJoin.innerHTML = '<span class="spinner"></span>Installing...';
|
|
5317
|
-
|
|
5318
|
-
try {
|
|
5319
|
-
const r = await fetch('/dashboard/api/install-tailscale', { method: 'POST' });
|
|
5320
|
-
const data = await r.json();
|
|
5321
|
-
if (data.success) {
|
|
5322
|
-
btnJoin.textContent = 'Installed';
|
|
5323
|
-
btnJoin.disabled = false;
|
|
5324
|
-
checkTailscaleStatus();
|
|
5325
|
-
fullRefresh();
|
|
5326
|
-
} else {
|
|
5327
|
-
alert('Installation failed: ' + (data.output || 'unknown error'));
|
|
5328
|
-
btnJoin.textContent = 'Install Tailscale';
|
|
5329
|
-
btnJoin.className = 'btn-join btn-install';
|
|
5330
|
-
btnJoin.disabled = false;
|
|
5331
|
-
}
|
|
5332
|
-
} catch (e) {
|
|
5333
|
-
alert('Network error: ' + (e.message || 'unknown'));
|
|
5334
|
-
btnJoin.textContent = 'Install Tailscale';
|
|
5335
|
-
btnJoin.className = 'btn-join btn-install';
|
|
5336
|
-
btnJoin.disabled = false;
|
|
5337
|
-
}
|
|
5338
|
-
}
|
|
5339
|
-
|
|
5340
|
-
// Check initial Tailscale status (tri-state)
|
|
5341
|
-
let tsState = null;
|
|
5342
|
-
let tsInstallInfo = null;
|
|
5343
|
-
|
|
5344
|
-
async function checkTailscaleStatus() {
|
|
5345
|
-
try {
|
|
5346
|
-
const r = await fetch('/dashboard/api/tailscale-status');
|
|
5347
|
-
tsState = await r.json();
|
|
5348
|
-
} catch { tsState = { state: 'not_installed', platform: 'unknown' }; }
|
|
5349
|
-
|
|
5350
|
-
const dot = $('#statusDot');
|
|
5351
|
-
const text = $('#statusText');
|
|
5352
|
-
const btnJoin = $('#btnJoinNetwork');
|
|
5353
|
-
const panel = $('#tsPanel');
|
|
5354
|
-
|
|
5355
|
-
if (tsState.state === 'connected') {
|
|
5356
|
-
dot.className = 'status-dot';
|
|
5357
|
-
text.textContent = 'ONLINE';
|
|
5358
|
-
btnJoin.textContent = 'Logout';
|
|
5359
|
-
btnJoin.className = 'btn-join btn-logout';
|
|
5360
|
-
btnJoin.style.display = '';
|
|
5361
|
-
panel.style.display = 'none';
|
|
5362
|
-
} else if (tsState.state === 'not_installed') {
|
|
5363
|
-
dot.className = 'status-dot not-installed';
|
|
5364
|
-
text.textContent = 'NOT INSTALLED';
|
|
5365
|
-
btnJoin.textContent = 'Install Tailscale';
|
|
5366
|
-
btnJoin.className = 'btn-join btn-install';
|
|
5367
|
-
btnJoin.style.display = '';
|
|
5368
|
-
await renderNotInstalledPanel();
|
|
5369
|
-
} else {
|
|
5370
|
-
dot.className = 'status-dot not-connected';
|
|
5371
|
-
text.textContent = 'NOT CONNECTED';
|
|
5372
|
-
btnJoin.textContent = 'Join Network';
|
|
5373
|
-
btnJoin.className = 'btn-join';
|
|
5374
|
-
btnJoin.style.display = '';
|
|
5375
|
-
await renderNotConnectedPanel();
|
|
5376
|
-
}
|
|
5377
|
-
}
|
|
5378
|
-
|
|
5379
|
-
async function fetchInstallInfo() {
|
|
5380
|
-
if (tsInstallInfo) return tsInstallInfo;
|
|
5381
|
-
try {
|
|
5382
|
-
const r = await fetch('/dashboard/api/tailscale-install-info');
|
|
5383
|
-
tsInstallInfo = await r.json();
|
|
5384
|
-
} catch { tsInstallInfo = null; }
|
|
5385
|
-
return tsInstallInfo;
|
|
5386
|
-
}
|
|
5387
|
-
|
|
5388
|
-
async function renderNotInstalledPanel() {
|
|
5389
|
-
const info = await fetchInstallInfo();
|
|
5390
|
-
const panel = $('#tsPanel');
|
|
5391
|
-
let html = '<div class="ts-panel-title not-installed">Tailscale Not Installed</div>';
|
|
5392
|
-
html += '<div class="ts-info-row"><span class="label">Status:</span><span class="value" style="color:var(--red)">Tailscale is not detected on this system</span></div>';
|
|
5393
|
-
|
|
5394
|
-
if (info && info.commands && info.commands.length > 0) {
|
|
5395
|
-
html += '<div class="ts-install-guide">';
|
|
5396
|
-
html += '<div style="color:var(--muted);margin-bottom:6px">Install for ' + info.os + ':</div>';
|
|
5397
|
-
info.commands.forEach(function(cmd) {
|
|
5398
|
-
const display = info.needs_sudo ? 'sudo ' + cmd : cmd;
|
|
5399
|
-
html += '<div class="cmd" data-clipboard="' + display.replace(/"/g, '"') + '">';
|
|
5400
|
-
html += '<code>' + display + '</code><span class="copy-hint">Click to copy</span></div>';
|
|
5401
|
-
});
|
|
5402
|
-
if (info.download_url) {
|
|
5403
|
-
html += '<div style="margin-top:8px">Download: <a href="' + info.download_url + '" target="_blank">' + info.download_url + '</a></div>';
|
|
5404
|
-
}
|
|
5405
|
-
html += '</div>';
|
|
5406
|
-
}
|
|
5407
|
-
|
|
5408
|
-
html += '<div class="ts-setup-hint">Or click the <strong>Install Tailscale</strong> button above, or run <code style="color:var(--cyan)">npx open-party setup</code></div>';
|
|
5409
|
-
html += '<button class="btn-redetect" onclick="window.__redetectTailscale()">Re-detect</button>';
|
|
5410
|
-
panel.innerHTML = html;
|
|
5411
|
-
panel.style.display = 'block';
|
|
5412
|
-
}
|
|
5413
|
-
|
|
5414
|
-
async function renderNotConnectedPanel() {
|
|
5415
|
-
const panel = $('#tsPanel');
|
|
5416
|
-
|
|
5417
|
-
let html = '<div class="ts-panel-title not-connected">Tailscale Not Connected</div>';
|
|
5418
|
-
html += '<div class="ts-info-row"><span class="label">Status:</span><span class="value" style="color:var(--yellow)">Installed but not authenticated</span></div>';
|
|
5419
|
-
html += '<div class="ts-setup-hint">Use the <strong>Join Network</strong> button above to log in</div>';
|
|
5420
|
-
html += '<button class="btn-redetect" onclick="window.__redetectTailscale()">Re-detect</button>';
|
|
5421
|
-
panel.innerHTML = html;
|
|
5422
|
-
panel.style.display = 'block';
|
|
5423
|
-
}
|
|
5424
|
-
|
|
5425
|
-
window.__redetectTailscale = async function() {
|
|
5426
|
-
const panel = $('#tsPanel');
|
|
5427
|
-
panel.innerHTML = '<div style="color:var(--muted);padding:12px"><span class="spinner"></span> Re-detecting Tailscale...</div>';
|
|
5428
|
-
try {
|
|
5429
|
-
await fetch('/dashboard/api/tailscale-detect', { method: 'POST' });
|
|
5430
|
-
} catch { /* ignore */ }
|
|
5431
|
-
await checkTailscaleStatus();
|
|
5432
|
-
fullRefresh();
|
|
5433
|
-
};
|
|
5434
|
-
|
|
5435
|
-
checkTailscaleStatus();
|
|
5436
|
-
|
|
5437
|
-
})();
|
|
5438
|
-
</script>
|
|
5439
|
-
</body>
|
|
5440
|
-
</html>`;
|
|
5441
|
-
}
|
|
5442
|
-
});
|
|
5443
|
-
|
|
5444
|
-
// src/server/routes/dashboard.ts
|
|
5445
|
-
var dashboardRoutes, activeLogin;
|
|
5446
|
-
var init_dashboard = __esm({
|
|
5447
|
-
"src/server/routes/dashboard.ts"() {
|
|
5448
|
-
"use strict";
|
|
5449
|
-
init_dist();
|
|
5450
|
-
init_models();
|
|
5451
|
-
init_dashboard_html();
|
|
5452
|
-
init_tailscale();
|
|
5453
|
-
init_state();
|
|
5454
|
-
init_logger();
|
|
5455
|
-
dashboardRoutes = new Hono2();
|
|
5456
|
-
dashboardRoutes.get("/", (c) => {
|
|
5457
|
-
return c.html(DASHBOARD_HTML);
|
|
5458
|
-
});
|
|
5459
|
-
dashboardRoutes.get("/api/stats", async (c) => {
|
|
5460
|
-
const localAgents = registry.listAll();
|
|
5461
|
-
const remoteAgents = discovery.getReachableRemoteAgents();
|
|
5462
|
-
const peerStates = discovery.getPeerStates();
|
|
5463
|
-
const partyServers = peerStates.filter((p) => p.status === "PARTY_SERVER" || p.status === "DEGRADED" || p.status === "SUSPECT");
|
|
5464
|
-
return c.json({
|
|
5465
|
-
local_agent_count: localAgents.length,
|
|
5466
|
-
remote_agent_count: remoteAgents.length,
|
|
5467
|
-
peer_count: peerStates.length,
|
|
5468
|
-
party_server_count: partyServers.length
|
|
5469
|
-
});
|
|
5470
|
-
});
|
|
5471
|
-
dashboardRoutes.get("/api/overview", async (c) => {
|
|
5472
|
-
let hostname = "127.0.0.1";
|
|
5473
|
-
try {
|
|
5474
|
-
hostname = getTailnetHostname();
|
|
5475
|
-
} catch {
|
|
5476
|
-
}
|
|
5477
|
-
const localAgents = sanitizeAgentList(registry.listAll());
|
|
5478
|
-
const remoteEntries = discovery.getRemoteAgentEntries();
|
|
5479
|
-
const peerStates = discovery.getPeerStates();
|
|
5480
|
-
const seen = /* @__PURE__ */ new Set();
|
|
5481
|
-
const recentMessages = [];
|
|
5482
|
-
for (const agent of localAgents) {
|
|
5483
|
-
const history = messageQueue.getHistory(agent.agent_id, 5);
|
|
5484
|
-
for (const entry of history) {
|
|
5485
|
-
const key = `${entry.sender_id}:${entry.recipient_id ?? ""}:${Math.floor(entry.timestamp)}`;
|
|
5486
|
-
if (seen.has(key)) continue;
|
|
5487
|
-
seen.add(key);
|
|
5488
|
-
recentMessages.push({ agent_id: agent.agent_id, ...entry });
|
|
5489
|
-
}
|
|
5490
|
-
}
|
|
5491
|
-
recentMessages.sort((a, b) => b.timestamp - a.timestamp);
|
|
5492
|
-
if (recentMessages.length > 20) recentMessages.length = 20;
|
|
5493
|
-
const partyServers = peerStates.filter(
|
|
5494
|
-
(p) => p.status === "PARTY_SERVER" || p.status === "DEGRADED" || p.status === "SUSPECT"
|
|
5495
|
-
);
|
|
5496
|
-
return c.json({
|
|
5497
|
-
server: {
|
|
5498
|
-
status: "ok",
|
|
5499
|
-
tailscale_ip: getSelfIp(),
|
|
5500
|
-
hostname,
|
|
5501
|
-
uptime_seconds: Math.floor((Date.now() - STARTED_AT) / 1e3)
|
|
5502
|
-
},
|
|
5503
|
-
agents: {
|
|
5504
|
-
local_count: localAgents.length,
|
|
5505
|
-
remote_count: remoteEntries.length,
|
|
5506
|
-
local_agents: localAgents,
|
|
5507
|
-
remote_agents: remoteEntries.map((e) => ({
|
|
5508
|
-
...sanitizeAgentList([e.agentInfo])[0],
|
|
5509
|
-
source_peer_ip: e.sourcePeerIp,
|
|
5510
|
-
reachable: e.reachable
|
|
5511
|
-
}))
|
|
5512
|
-
},
|
|
5513
|
-
peers: {
|
|
5514
|
-
total: peerStates.length,
|
|
5515
|
-
party_servers: partyServers.length,
|
|
5516
|
-
down: peerStates.filter((p) => p.status === "DOWN").length,
|
|
5517
|
-
unknown: peerStates.filter((p) => p.status === "UNKNOWN" || p.status === "MAYBE").length,
|
|
5518
|
-
details: peerStates
|
|
5519
|
-
},
|
|
5520
|
-
messages: {
|
|
5521
|
-
recent: recentMessages
|
|
5522
|
-
}
|
|
5523
|
-
});
|
|
5524
|
-
});
|
|
5525
|
-
dashboardRoutes.get("/api/tailscale-status", async (c) => {
|
|
5526
|
-
try {
|
|
5527
|
-
return c.json(getTailscaleInstallationStatus());
|
|
5528
|
-
} catch (e) {
|
|
5529
|
-
return c.json({ state: "not_installed", platform: process.platform, error: e.message });
|
|
5530
|
-
}
|
|
5531
|
-
});
|
|
5532
|
-
dashboardRoutes.post("/api/tailscale-detect", async (c) => {
|
|
5533
|
-
resetTailscaleBinaryCache();
|
|
5534
|
-
const state = getTailscaleInstallationStatus();
|
|
5535
|
-
if (state.state === "connected") {
|
|
5536
|
-
refreshSelfIp();
|
|
5537
|
-
}
|
|
5538
|
-
return c.json(state);
|
|
5539
|
-
});
|
|
5540
|
-
dashboardRoutes.get("/api/tailscale-install-info", async (c) => {
|
|
5541
|
-
return c.json(getInstallInstructions(process.platform));
|
|
5542
|
-
});
|
|
5543
|
-
dashboardRoutes.post("/api/join-network", async (c) => {
|
|
5544
|
-
try {
|
|
5545
|
-
const body = await c.req.json();
|
|
5546
|
-
const authKey = (body.auth_key ?? "").trim();
|
|
5547
|
-
if (!authKey) {
|
|
5548
|
-
return c.json({ success: false, output: "auth_key is required" }, 400);
|
|
5549
|
-
}
|
|
5550
|
-
const result = joinTailnet(authKey);
|
|
5551
|
-
logger.info("Dashboard", `Join network: ${result.success ? "success" : "failed"}`);
|
|
5552
|
-
return c.json(result, result.success ? 200 : 500);
|
|
5553
|
-
} catch (e) {
|
|
5554
|
-
return c.json({ success: false, output: e.message }, 500);
|
|
5555
|
-
}
|
|
5556
|
-
});
|
|
5557
|
-
activeLogin = null;
|
|
5558
|
-
dashboardRoutes.post("/api/logout", async (c) => {
|
|
5559
|
-
const result = logoutTailscale();
|
|
5560
|
-
logger.info("Dashboard", `Logout: ${result.success ? "success" : "failed"}`);
|
|
5561
|
-
if (result.success) {
|
|
5562
|
-
resetTailscaleBinaryCache();
|
|
5563
|
-
refreshSelfIp();
|
|
5564
|
-
}
|
|
5565
|
-
return c.json(result, result.success ? 200 : 500);
|
|
5566
|
-
});
|
|
5567
|
-
dashboardRoutes.post("/api/tailscale-login", async (c) => {
|
|
5568
|
-
if (activeLogin?.url) {
|
|
5569
|
-
return c.json({ success: true, url: activeLogin.url });
|
|
5570
|
-
}
|
|
5571
|
-
const { promise, process: process2 } = startInteractiveLogin();
|
|
5572
|
-
activeLogin = { process: process2 };
|
|
5573
|
-
logger.info("Dashboard", "Tailscale login initiated");
|
|
5574
|
-
const result = await promise;
|
|
5575
|
-
if (result.success && result.url) {
|
|
5576
|
-
activeLogin.url = result.url;
|
|
5577
|
-
return c.json({ success: true, url: result.url });
|
|
5578
|
-
}
|
|
5579
|
-
activeLogin = null;
|
|
5580
|
-
return c.json({ success: false, output: result.output }, 500);
|
|
5581
|
-
});
|
|
5582
|
-
dashboardRoutes.post("/api/install-tailscale", async (c) => {
|
|
5583
|
-
const { installTailscale: installTailscale2 } = await Promise.resolve().then(() => (init_tailscale_installer(), tailscale_installer_exports));
|
|
5584
|
-
const result = await installTailscale2(process.platform);
|
|
5585
|
-
logger.info("Dashboard", `Install Tailscale: ${result.success ? "success" : "failed"}`);
|
|
5586
|
-
if (result.success) {
|
|
5587
|
-
resetTailscaleBinaryCache();
|
|
5588
|
-
}
|
|
5589
|
-
return c.json(result, result.success ? 200 : 500);
|
|
5590
|
-
});
|
|
5591
|
-
}
|
|
5592
|
-
});
|
|
5593
|
-
|
|
5594
|
-
// src/server/index.ts
|
|
5595
|
-
var server_exports = {};
|
|
5596
|
-
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, unlinkSync as unlinkSync4 } from "fs";
|
|
5597
|
-
import { join as join7, dirname as dirname4 } from "path";
|
|
5598
|
-
import { homedir as homedir6 } from "os";
|
|
5599
|
-
async function periodicCleanup() {
|
|
5600
|
-
while (!lifecycleController.signal.aborted) {
|
|
5601
|
-
try {
|
|
5602
|
-
const removed = registry.cleanupStale(HEARTBEAT_TIMEOUT);
|
|
5603
|
-
if (removed.length > 0) {
|
|
5604
|
-
logger.info("Cleanup", `Removed ${removed.length} stale agent(s): ${removed.join(", ")}`);
|
|
5605
|
-
}
|
|
5606
|
-
} catch (e) {
|
|
5607
|
-
logger.error("Cleanup", "Error during cleanup", e);
|
|
5608
|
-
}
|
|
5609
|
-
await abortableSleep(CLEANUP_INTERVAL * 1e3, lifecycleController.signal);
|
|
5610
|
-
}
|
|
5611
|
-
}
|
|
5612
|
-
function pidFilePath2() {
|
|
5613
|
-
const pluginData = process.env.CLAUDE_PLUGIN_DATA || "";
|
|
5614
|
-
if (pluginData) return join7(pluginData, "server.pid");
|
|
5615
|
-
return join7(homedir6(), ".open-party", "server.pid");
|
|
5616
|
-
}
|
|
5617
|
-
async function performShutdown(server, pidPath) {
|
|
5618
|
-
if (shutdownInitiated) return;
|
|
5619
|
-
shutdownInitiated = true;
|
|
5620
|
-
logger.info("Shutdown", "Shutting down Party Server...");
|
|
5621
|
-
try {
|
|
5622
|
-
lifecycleController.abort();
|
|
5623
|
-
getSnapshotManager()?.cancelDebounce();
|
|
5624
|
-
try {
|
|
5625
|
-
getSnapshotManager()?.writeSnapshot(registry.listAll(), messageQueue.getHistorySnapshot());
|
|
5626
|
-
logger.info("Shutdown", "Final snapshot written.");
|
|
5627
|
-
} catch (error) {
|
|
5628
|
-
logger.error("Shutdown", "Failed to write final snapshot", error);
|
|
5629
|
-
}
|
|
5630
|
-
if (server.closeAllConnections) {
|
|
5631
|
-
server.closeAllConnections();
|
|
5632
|
-
}
|
|
5633
|
-
if (process.platform === "win32") {
|
|
5634
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
5635
|
-
}
|
|
5636
|
-
await new Promise((resolve4, reject) => {
|
|
5637
|
-
server.close((err) => err ? reject(err) : resolve4());
|
|
5638
|
-
});
|
|
5639
|
-
if (process.platform === "win32") {
|
|
5640
|
-
await new Promise((r) => setTimeout(r, 500));
|
|
5641
|
-
}
|
|
5642
|
-
getSnapshotManager()?.removeShutdownMarker();
|
|
5643
|
-
try {
|
|
5644
|
-
unlinkSync4(pidPath);
|
|
5645
|
-
} catch {
|
|
5646
|
-
}
|
|
5647
|
-
logger.info("Shutdown", "Party Server shut down cleanly.");
|
|
5648
|
-
} catch (error) {
|
|
5649
|
-
logger.error("Shutdown", "Error during shutdown sequence", error);
|
|
5650
|
-
} finally {
|
|
5651
|
-
process.exit(0);
|
|
5652
|
-
}
|
|
4461
|
+
async function performShutdown(server, pidPath) {
|
|
4462
|
+
if (shutdownInitiated) return;
|
|
4463
|
+
shutdownInitiated = true;
|
|
4464
|
+
logger.info("Shutdown", "Shutting down Party Server...");
|
|
4465
|
+
try {
|
|
4466
|
+
lifecycleController.abort();
|
|
4467
|
+
try {
|
|
4468
|
+
getSnapshotManager()?.writeSnapshot(registry.listAll(), messageQueue.getHistorySnapshot());
|
|
4469
|
+
logger.info("Shutdown", "Final snapshot written.");
|
|
4470
|
+
} catch (error) {
|
|
4471
|
+
logger.error("Shutdown", "Failed to write final snapshot", error);
|
|
4472
|
+
}
|
|
4473
|
+
if (server.closeAllConnections) {
|
|
4474
|
+
server.closeAllConnections();
|
|
4475
|
+
}
|
|
4476
|
+
if (process.platform === "win32") {
|
|
4477
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
4478
|
+
}
|
|
4479
|
+
await new Promise((resolve4, reject) => {
|
|
4480
|
+
server.close((err) => err ? reject(err) : resolve4());
|
|
4481
|
+
});
|
|
4482
|
+
if (process.platform === "win32") {
|
|
4483
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
4484
|
+
}
|
|
4485
|
+
getSnapshotManager()?.removeShutdownMarker();
|
|
4486
|
+
try {
|
|
4487
|
+
unlinkSync4(pidPath);
|
|
4488
|
+
} catch {
|
|
4489
|
+
}
|
|
4490
|
+
logger.info("Shutdown", "Party Server shut down cleanly.");
|
|
4491
|
+
} catch (error) {
|
|
4492
|
+
logger.error("Shutdown", "Error during shutdown sequence", error);
|
|
4493
|
+
} finally {
|
|
4494
|
+
process.exit(0);
|
|
4495
|
+
}
|
|
5653
4496
|
}
|
|
5654
4497
|
async function main() {
|
|
5655
4498
|
const pidPath = pidFilePath2();
|
|
@@ -5665,7 +4508,7 @@ async function main() {
|
|
|
5665
4508
|
const savedSnapshot = sm.loadSnapshot();
|
|
5666
4509
|
if (savedSnapshot) {
|
|
5667
4510
|
recoveredAgents = sm.hydrateAgents(registry, getSelfIp());
|
|
5668
|
-
recoveredHistoryEntries = sm.
|
|
4511
|
+
recoveredHistoryEntries = sm.hydrateBuffers(messageQueue);
|
|
5669
4512
|
if (recoveredAgents > 0 || recoveredHistoryEntries > 0) {
|
|
5670
4513
|
logger.info(
|
|
5671
4514
|
"Recovery",
|
|
@@ -5682,7 +4525,7 @@ async function main() {
|
|
|
5682
4525
|
const snapshotLoopPromise = sm.startSnapshotLoop(
|
|
5683
4526
|
lifecycleController.signal,
|
|
5684
4527
|
() => registry.listAll(),
|
|
5685
|
-
() => messageQueue.
|
|
4528
|
+
() => messageQueue.getBufferSnapshots()
|
|
5686
4529
|
);
|
|
5687
4530
|
const shutdownHandler = () => void performShutdown(server, pidPath);
|
|
5688
4531
|
process.on("SIGINT", shutdownHandler);
|
|
@@ -5706,7 +4549,6 @@ var init_server = __esm({
|
|
|
5706
4549
|
init_state();
|
|
5707
4550
|
init_agent();
|
|
5708
4551
|
init_proxy();
|
|
5709
|
-
init_dashboard();
|
|
5710
4552
|
app = new Hono2();
|
|
5711
4553
|
app.use("*", cors());
|
|
5712
4554
|
app.use("*", async (c, next) => {
|
|
@@ -5730,7 +4572,6 @@ var init_server = __esm({
|
|
|
5730
4572
|
});
|
|
5731
4573
|
app.route("/agent", agentRoutes);
|
|
5732
4574
|
app.route("/proxy", proxyRoutes);
|
|
5733
|
-
app.route("/dashboard", dashboardRoutes);
|
|
5734
4575
|
shutdownInitiated = false;
|
|
5735
4576
|
main().catch((e) => {
|
|
5736
4577
|
logger.error("Server", "Fatal error", e);
|
|
@@ -5746,849 +4587,138 @@ var init_server = __esm({
|
|
|
5746
4587
|
});
|
|
5747
4588
|
|
|
5748
4589
|
// src/cli/setup.ts
|
|
5749
|
-
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
// src/cli/agent-detector.ts
|
|
5753
|
-
import { existsSync as existsSync2 } from "fs";
|
|
5754
|
-
import { join as join2 } from "path";
|
|
5755
|
-
import { homedir } from "os";
|
|
4590
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, readdirSync, statSync } from "fs";
|
|
4591
|
+
import { join as join2, dirname as dirname2, resolve as resolve2 } from "path";
|
|
4592
|
+
import { homedir as homedir2, platform } from "os";
|
|
5756
4593
|
import { execSync as execSync2 } from "child_process";
|
|
5757
|
-
function isExecutableInPath(name) {
|
|
5758
|
-
try {
|
|
5759
|
-
const which = process.platform === "win32" ? "where" : "which";
|
|
5760
|
-
execSync2(`${which} ${name}`, { timeout: 3e3, stdio: "pipe", windowsHide: true });
|
|
5761
|
-
return true;
|
|
5762
|
-
} catch {
|
|
5763
|
-
return false;
|
|
5764
|
-
}
|
|
5765
|
-
}
|
|
5766
|
-
function detectClaudeCode() {
|
|
5767
|
-
const configDir = join2(homedir(), ".claude");
|
|
5768
|
-
const settingsPath = join2(configDir, "settings.json");
|
|
5769
|
-
return {
|
|
5770
|
-
type: "claude-code",
|
|
5771
|
-
name: "Claude Code",
|
|
5772
|
-
detected: existsSync2(settingsPath) || isExecutableInPath("claude"),
|
|
5773
|
-
configPath: settingsPath
|
|
5774
|
-
};
|
|
5775
|
-
}
|
|
5776
|
-
function detectOpenClaw() {
|
|
5777
|
-
const configPath = join2(homedir(), ".openclaw", "openclaw.json");
|
|
5778
|
-
if (isExecutableInPath("openclaw") || isExecutableInPath("openclaw.mjs")) {
|
|
5779
|
-
return { type: "openclaw", name: "OpenClaw", detected: true, configPath };
|
|
5780
|
-
}
|
|
5781
|
-
const candidatePaths = [
|
|
5782
|
-
join2(homedir(), ".openclaw", "openclaw.mjs"),
|
|
5783
|
-
join2(homedir(), ".openclaw", "openclaw")
|
|
5784
|
-
];
|
|
5785
|
-
for (const p of candidatePaths) {
|
|
5786
|
-
if (existsSync2(p)) {
|
|
5787
|
-
return { type: "openclaw", name: "OpenClaw", detected: true, configPath };
|
|
5788
|
-
}
|
|
5789
|
-
}
|
|
5790
|
-
return { type: "openclaw", name: "OpenClaw", detected: false, configPath };
|
|
5791
|
-
}
|
|
5792
|
-
function detectAgents() {
|
|
5793
|
-
return [detectClaudeCode(), detectOpenClaw()];
|
|
5794
|
-
}
|
|
5795
|
-
|
|
5796
|
-
// src/cli/agent-installer.ts
|
|
5797
|
-
import { existsSync as existsSync3, readFileSync, writeFileSync, mkdirSync, cpSync, rmSync, readdirSync, statSync } from "fs";
|
|
5798
|
-
import { createHash } from "crypto";
|
|
5799
|
-
import { join as join3, dirname, resolve } from "path";
|
|
5800
|
-
import { homedir as homedir2 } from "os";
|
|
5801
|
-
function ensureDir(filePath) {
|
|
5802
|
-
const dir = dirname(filePath);
|
|
5803
|
-
if (!existsSync3(dir)) {
|
|
5804
|
-
mkdirSync(dir, { recursive: true });
|
|
5805
|
-
}
|
|
5806
|
-
}
|
|
5807
|
-
function readJsonFile(filePath, fallback) {
|
|
5808
|
-
if (!existsSync3(filePath)) return fallback;
|
|
5809
|
-
try {
|
|
5810
|
-
return JSON.parse(readFileSync(filePath, "utf-8"));
|
|
5811
|
-
} catch {
|
|
5812
|
-
return fallback;
|
|
5813
|
-
}
|
|
5814
|
-
}
|
|
5815
|
-
function writeJsonFile(filePath, data) {
|
|
5816
|
-
ensureDir(filePath);
|
|
5817
|
-
writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
5818
|
-
}
|
|
5819
|
-
function findPluginDistDir() {
|
|
5820
|
-
const distDir = resolve(import.meta.dirname ?? ".", "..", "claude-code");
|
|
5821
|
-
if (!existsSync3(distDir)) return null;
|
|
5822
|
-
try {
|
|
5823
|
-
const entries = readdirSync(distDir);
|
|
5824
|
-
const dirs = entries.filter((e) => e.startsWith("open-party-"));
|
|
5825
|
-
if (dirs.length === 0) return null;
|
|
5826
|
-
const pluginDir = join3(distDir, dirs[dirs.length - 1]);
|
|
5827
|
-
if (existsSync3(join3(pluginDir, ".claude-plugin", "plugin.json"))) {
|
|
5828
|
-
return pluginDir;
|
|
5829
|
-
}
|
|
5830
|
-
} catch (error) {
|
|
5831
|
-
console.error("[Agent Installer] Failed to list plugin dist directory:", error instanceof Error ? error.message : String(error));
|
|
5832
|
-
}
|
|
5833
|
-
return null;
|
|
5834
|
-
}
|
|
5835
|
-
function findDistJsDir() {
|
|
5836
|
-
const possiblePaths = [
|
|
5837
|
-
// When installed globally via npm: <pkg-root>/dist/cli/index.js → <pkg-root>/dist/
|
|
5838
|
-
resolve(import.meta.dirname ?? ".", "..")
|
|
5839
|
-
];
|
|
5840
|
-
for (const p of possiblePaths) {
|
|
5841
|
-
if (existsSync3(join3(p, "mcp-server.js")) && existsSync3(join3(p, "hook-handler.js"))) {
|
|
5842
|
-
return p;
|
|
5843
|
-
}
|
|
5844
|
-
}
|
|
5845
|
-
return null;
|
|
5846
|
-
}
|
|
5847
|
-
function getPluginVersion() {
|
|
5848
|
-
const pluginDir = findPluginDistDir();
|
|
5849
|
-
if (pluginDir) {
|
|
5850
|
-
const manifest = readJsonFile(
|
|
5851
|
-
join3(pluginDir, ".claude-plugin", "plugin.json"),
|
|
5852
|
-
{}
|
|
5853
|
-
);
|
|
5854
|
-
if (manifest.version) return manifest.version;
|
|
5855
|
-
}
|
|
5856
|
-
const distJsDir = findDistJsDir();
|
|
5857
|
-
if (distJsDir) {
|
|
5858
|
-
const buildInfo = readJsonFile(
|
|
5859
|
-
join3(distJsDir, "..", "BUILD_INFO.json"),
|
|
5860
|
-
{}
|
|
5861
|
-
);
|
|
5862
|
-
if (buildInfo.version) return buildInfo.version;
|
|
5863
|
-
}
|
|
5864
|
-
try {
|
|
5865
|
-
const pkg = JSON.parse(readFileSync(join3(import.meta.dirname ?? ".", "..", "..", "package.json"), "utf-8"));
|
|
5866
|
-
if (pkg.version) return pkg.version;
|
|
5867
|
-
} catch {
|
|
5868
|
-
}
|
|
5869
|
-
return "0.0.0";
|
|
5870
|
-
}
|
|
5871
|
-
function getMarketplaceDir() {
|
|
5872
|
-
return join3(homedir2(), ".claude", "plugins", "marketplaces", "open-party");
|
|
5873
|
-
}
|
|
5874
|
-
function registerMarketplace(version, pluginDir) {
|
|
5875
|
-
const marketplaceDir = getMarketplaceDir();
|
|
5876
|
-
const pluginSourceDir = join3(marketplaceDir, "plugin");
|
|
5877
|
-
const marketplacePluginDir = join3(marketplaceDir, ".claude-plugin");
|
|
5878
|
-
if (!existsSync3(marketplacePluginDir)) {
|
|
5879
|
-
mkdirSync(marketplacePluginDir, { recursive: true });
|
|
5880
|
-
}
|
|
5881
|
-
const marketplaceManifest = {
|
|
5882
|
-
name: "open-party",
|
|
5883
|
-
owner: { name: "Feynman Zhang" },
|
|
5884
|
-
metadata: {
|
|
5885
|
-
description: "Decentralized Agent communication network for Claude Code",
|
|
5886
|
-
homepage: "https://github.com/FeynmanZhang/open-party"
|
|
5887
|
-
},
|
|
5888
|
-
plugins: [
|
|
5889
|
-
{
|
|
5890
|
-
name: "open-party",
|
|
5891
|
-
version,
|
|
5892
|
-
source: "./plugin",
|
|
5893
|
-
description: "Decentralized Agent communication network for Claude Code"
|
|
5894
|
-
}
|
|
5895
|
-
]
|
|
5896
|
-
};
|
|
5897
|
-
writeJsonFile(join3(marketplacePluginDir, "marketplace.json"), marketplaceManifest);
|
|
5898
|
-
if (existsSync3(pluginSourceDir)) {
|
|
5899
|
-
rmSync(pluginSourceDir, { recursive: true });
|
|
5900
|
-
}
|
|
5901
|
-
mkdirSync(pluginSourceDir, { recursive: true });
|
|
5902
|
-
cpSync(pluginDir, pluginSourceDir, { recursive: true });
|
|
5903
|
-
const knownMarketplacesPath = join3(homedir2(), ".claude", "plugins", "known_marketplaces.json");
|
|
5904
|
-
const knownMarketplaces = readJsonFile(knownMarketplacesPath, {});
|
|
5905
|
-
knownMarketplaces["open-party"] = {
|
|
5906
|
-
source: {
|
|
5907
|
-
source: "github",
|
|
5908
|
-
repo: "FeynmanZhang/open-party"
|
|
5909
|
-
},
|
|
5910
|
-
installLocation: marketplaceDir,
|
|
5911
|
-
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
5912
|
-
};
|
|
5913
|
-
writeJsonFile(knownMarketplacesPath, knownMarketplaces);
|
|
5914
|
-
}
|
|
5915
|
-
function installClaudeCode() {
|
|
5916
|
-
const pluginDir = findPluginDistDir();
|
|
5917
|
-
if (!pluginDir) {
|
|
5918
|
-
return {
|
|
5919
|
-
success: false,
|
|
5920
|
-
error: 'Plugin package not found. Run "npm run build:plugin" first, or use "claude --plugin-dir" to install manually.'
|
|
5921
|
-
};
|
|
5922
|
-
}
|
|
5923
|
-
const version = getPluginVersion();
|
|
5924
|
-
const installDir = join3(homedir2(), ".claude", "plugins", "cache", "open-party", "open-party", version);
|
|
5925
|
-
if (existsSync3(installDir)) {
|
|
5926
|
-
rmSync(installDir, { recursive: true });
|
|
5927
|
-
}
|
|
5928
|
-
mkdirSync(installDir, { recursive: true });
|
|
5929
|
-
cpSync(pluginDir, installDir, { recursive: true });
|
|
5930
|
-
const mcpServerPath = join3(installDir, "dist", "mcp-server.js");
|
|
5931
|
-
if (!existsSync3(mcpServerPath)) {
|
|
5932
|
-
const distJsDir = findDistJsDir();
|
|
5933
|
-
if (distJsDir) {
|
|
5934
|
-
const targetDist = join3(installDir, "dist");
|
|
5935
|
-
if (!existsSync3(targetDist)) mkdirSync(targetDist, { recursive: true });
|
|
5936
|
-
for (const file of ["mcp-server.js", "hook-handler.js", "party-server.js"]) {
|
|
5937
|
-
const src = join3(distJsDir, file);
|
|
5938
|
-
if (existsSync3(src)) {
|
|
5939
|
-
cpSync(src, join3(targetDist, file));
|
|
5940
|
-
}
|
|
5941
|
-
}
|
|
5942
|
-
for (const file of ["mcp-server.js.map", "hook-handler.js.map", "party-server.js.map"]) {
|
|
5943
|
-
const src = join3(distJsDir, file);
|
|
5944
|
-
if (existsSync3(src)) {
|
|
5945
|
-
cpSync(src, join3(targetDist, file));
|
|
5946
|
-
}
|
|
5947
|
-
}
|
|
5948
|
-
}
|
|
5949
|
-
}
|
|
5950
|
-
const orphanedPath = join3(installDir, "dist", ".orphaned_at");
|
|
5951
|
-
if (existsSync3(orphanedPath)) {
|
|
5952
|
-
rmSync(orphanedPath);
|
|
5953
|
-
}
|
|
5954
|
-
registerMarketplace(version, pluginDir);
|
|
5955
|
-
const pluginsJsonPath = join3(homedir2(), ".claude", "plugins", "installed_plugins.json");
|
|
5956
|
-
const pluginsData = readJsonFile(
|
|
5957
|
-
pluginsJsonPath,
|
|
5958
|
-
{ version: 2, plugins: {} }
|
|
5959
|
-
);
|
|
5960
|
-
if (!pluginsData.plugins) pluginsData.plugins = {};
|
|
5961
|
-
if (pluginsData.plugins["open-party@local"]) {
|
|
5962
|
-
delete pluginsData.plugins["open-party@local"];
|
|
5963
|
-
}
|
|
5964
|
-
pluginsData.plugins["open-party@open-party"] = [
|
|
5965
|
-
{
|
|
5966
|
-
scope: "user",
|
|
5967
|
-
installPath: installDir,
|
|
5968
|
-
version,
|
|
5969
|
-
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5970
|
-
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
5971
|
-
}
|
|
5972
|
-
];
|
|
5973
|
-
writeJsonFile(pluginsJsonPath, pluginsData);
|
|
5974
|
-
const settingsPath = join3(homedir2(), ".claude", "settings.json");
|
|
5975
|
-
const settings = readJsonFile(settingsPath, {});
|
|
5976
|
-
if (settings.mcpServers?.["open-party"]) {
|
|
5977
|
-
delete settings.mcpServers["open-party"];
|
|
5978
|
-
if (Object.keys(settings.mcpServers).length === 0) {
|
|
5979
|
-
delete settings.mcpServers;
|
|
5980
|
-
}
|
|
5981
|
-
}
|
|
5982
|
-
if (!settings.enabledPlugins) {
|
|
5983
|
-
settings.enabledPlugins = {};
|
|
5984
|
-
}
|
|
5985
|
-
if (settings.enabledPlugins["open-party@local"] !== void 0) {
|
|
5986
|
-
delete settings.enabledPlugins["open-party@local"];
|
|
5987
|
-
}
|
|
5988
|
-
settings.enabledPlugins["open-party@open-party"] = true;
|
|
5989
|
-
writeJsonFile(settingsPath, settings);
|
|
5990
|
-
return {
|
|
5991
|
-
success: true,
|
|
5992
|
-
configPath: settingsPath
|
|
5993
|
-
};
|
|
5994
|
-
}
|
|
5995
|
-
function findOpenclawDistDir() {
|
|
5996
|
-
const distDir = resolve(import.meta.dirname ?? ".", "..", "openclaw");
|
|
5997
|
-
if (!existsSync3(distDir)) return null;
|
|
5998
|
-
try {
|
|
5999
|
-
const entries = readdirSync(distDir);
|
|
6000
|
-
const dirs = entries.filter((e) => e.startsWith("open-party-"));
|
|
6001
|
-
if (dirs.length === 0) return null;
|
|
6002
|
-
return join3(distDir, dirs[dirs.length - 1]);
|
|
6003
|
-
} catch (error) {
|
|
6004
|
-
console.error("[Agent Installer] Failed to list openclaw dist directory:", error instanceof Error ? error.message : String(error));
|
|
6005
|
-
}
|
|
6006
|
-
return null;
|
|
6007
|
-
}
|
|
6008
|
-
function installOpenClaw() {
|
|
6009
|
-
const pluginDir = findOpenclawDistDir();
|
|
6010
|
-
if (!pluginDir) {
|
|
6011
|
-
return {
|
|
6012
|
-
success: false,
|
|
6013
|
-
error: 'OpenClaw plugin package not found. Run "npm run build:openclaw" first.'
|
|
6014
|
-
};
|
|
6015
|
-
}
|
|
6016
|
-
const configPath = join3(homedir2(), ".openclaw", "openclaw.json");
|
|
6017
|
-
const extensionDir = join3(homedir2(), ".openclaw", "extensions", "open-party");
|
|
6018
|
-
if (existsSync3(extensionDir)) {
|
|
6019
|
-
rmSync(extensionDir, { recursive: true });
|
|
6020
|
-
}
|
|
6021
|
-
mkdirSync(extensionDir, { recursive: true });
|
|
6022
|
-
cpSync(pluginDir, extensionDir, { recursive: true });
|
|
6023
|
-
const config = readJsonFile(configPath, {});
|
|
6024
|
-
if (!config.plugins) config.plugins = {};
|
|
6025
|
-
if (!config.plugins.entries) {
|
|
6026
|
-
config.plugins.entries = {};
|
|
6027
|
-
}
|
|
6028
|
-
const entries = config.plugins.entries;
|
|
6029
|
-
entries["open-party"] = {
|
|
6030
|
-
enabled: true,
|
|
6031
|
-
config: {
|
|
6032
|
-
partyServerUrl: "http://127.0.0.1:8000",
|
|
6033
|
-
heartbeatInterval: 3e4
|
|
6034
|
-
}
|
|
6035
|
-
};
|
|
6036
|
-
const pluginsConfig = config.plugins;
|
|
6037
|
-
if (!Array.isArray(pluginsConfig.allow)) {
|
|
6038
|
-
pluginsConfig.allow = [];
|
|
6039
|
-
}
|
|
6040
|
-
const allowList = pluginsConfig.allow;
|
|
6041
|
-
if (!allowList.includes("open-party")) {
|
|
6042
|
-
allowList.push("open-party");
|
|
6043
|
-
}
|
|
6044
|
-
writeJsonFile(configPath, config);
|
|
6045
|
-
const installsPath = join3(homedir2(), ".openclaw", "plugins", "installs.json");
|
|
6046
|
-
const manifestPath = join3(extensionDir, "openclaw.plugin.json");
|
|
6047
|
-
const entrySource = join3(extensionDir, "dist", "index.js");
|
|
6048
|
-
let manifestHash = "";
|
|
6049
|
-
try {
|
|
6050
|
-
const manifestContent = readFileSync(manifestPath, "utf-8");
|
|
6051
|
-
manifestHash = createHash("sha256").update(manifestContent).digest("hex");
|
|
6052
|
-
} catch {
|
|
6053
|
-
}
|
|
6054
|
-
const stat = (filePath) => {
|
|
6055
|
-
try {
|
|
6056
|
-
return statSync(filePath);
|
|
6057
|
-
} catch {
|
|
6058
|
-
return void 0;
|
|
6059
|
-
}
|
|
6060
|
-
};
|
|
6061
|
-
const mStat = stat(manifestPath);
|
|
6062
|
-
const installs = readJsonFile(installsPath, { plugins: {} });
|
|
6063
|
-
if (!installs.plugins) installs.plugins = {};
|
|
6064
|
-
installs.plugins["open-party"] = [{
|
|
6065
|
-
pluginId: "open-party",
|
|
6066
|
-
manifestPath,
|
|
6067
|
-
manifestHash,
|
|
6068
|
-
manifestFile: mStat ? { size: mStat.size, mtimeMs: mStat.mtimeMs, ctimeMs: mStat.ctimeMs } : void 0,
|
|
6069
|
-
source: entrySource,
|
|
6070
|
-
rootDir: extensionDir,
|
|
6071
|
-
origin: "local",
|
|
6072
|
-
enabled: true,
|
|
6073
|
-
startup: {
|
|
6074
|
-
sidecar: false,
|
|
6075
|
-
memory: false,
|
|
6076
|
-
deferConfiguredChannelFullLoadUntilAfterListen: false,
|
|
6077
|
-
agentHarnesses: []
|
|
6078
|
-
},
|
|
6079
|
-
compat: [],
|
|
6080
|
-
enabledByDefault: true,
|
|
6081
|
-
packageName: "@feynmanzhang/open-party-openclaw",
|
|
6082
|
-
packageVersion: getPluginVersion()
|
|
6083
|
-
}];
|
|
6084
|
-
writeJsonFile(installsPath, installs);
|
|
6085
|
-
return { success: true, configPath };
|
|
6086
|
-
}
|
|
6087
|
-
async function installPluginToAgent(agentType) {
|
|
6088
|
-
switch (agentType) {
|
|
6089
|
-
case "claude-code":
|
|
6090
|
-
return installClaudeCode();
|
|
6091
|
-
case "openclaw":
|
|
6092
|
-
return installOpenClaw();
|
|
6093
|
-
default:
|
|
6094
|
-
return { success: false, error: `Unknown agent type: ${agentType}` };
|
|
6095
|
-
}
|
|
6096
|
-
}
|
|
6097
|
-
|
|
6098
|
-
// src/cli/tailscale-login.ts
|
|
6099
|
-
init_tailscale();
|
|
6100
|
-
import { spawn as spawn2 } from "child_process";
|
|
6101
4594
|
|
|
6102
4595
|
// src/cli/tty-utils.ts
|
|
6103
4596
|
import { createInterface } from "readline";
|
|
6104
4597
|
function cyan(text) {
|
|
6105
4598
|
return `\x1B[36m${text}\x1B[0m`;
|
|
6106
|
-
}
|
|
6107
|
-
function green(text) {
|
|
6108
|
-
return `\x1B[32m${text}\x1B[0m`;
|
|
6109
|
-
}
|
|
6110
|
-
function yellow(text) {
|
|
6111
|
-
return `\x1B[33m${text}\x1B[0m`;
|
|
6112
|
-
}
|
|
6113
|
-
function red(text) {
|
|
6114
|
-
return `\x1B[31m${text}\x1B[0m`;
|
|
6115
|
-
}
|
|
6116
|
-
function bold(text) {
|
|
6117
|
-
return `\x1B[1m${text}\x1B[0m`;
|
|
6118
|
-
}
|
|
6119
|
-
function dim(text) {
|
|
6120
|
-
return `\x1B[2m${text}\x1B[0m`;
|
|
6121
|
-
}
|
|
6122
|
-
function
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
|
|
6126
|
-
rl.close();
|
|
6127
|
-
}
|
|
6128
|
-
async function prompt(question) {
|
|
6129
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
6130
|
-
return new Promise((resolve4) => {
|
|
6131
|
-
rl.question(question, (answer) => {
|
|
6132
|
-
rl.close();
|
|
6133
|
-
resolve4(answer.trim());
|
|
6134
|
-
});
|
|
6135
|
-
});
|
|
6136
|
-
}
|
|
6137
|
-
async function select(options, opts) {
|
|
6138
|
-
if (options.length === 0) throw new Error("select() requires at least one option");
|
|
6139
|
-
if (options.length === 1) return options[0].value;
|
|
6140
|
-
const message = opts?.message ?? "";
|
|
6141
|
-
const wasRaw = process.stdin.isRaw;
|
|
6142
|
-
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
6143
|
-
try {
|
|
6144
|
-
let cursor = 0;
|
|
6145
|
-
const render = () => {
|
|
6146
|
-
const lines = options.length + (message ? 1 : 0);
|
|
6147
|
-
process.stdout.write(`\x1B[${lines}A\x1B[0J`);
|
|
6148
|
-
if (message) {
|
|
6149
|
-
process.stdout.write(`${message}
|
|
6150
|
-
`);
|
|
6151
|
-
}
|
|
6152
|
-
for (let i = 0; i < options.length; i++) {
|
|
6153
|
-
const opt = options[i];
|
|
6154
|
-
const prefix = i === cursor ? `${cyan("\u276F")} ` : " ";
|
|
6155
|
-
const label = i === cursor ? bold(opt.label) : opt.label;
|
|
6156
|
-
const hintStr = opt.hint ? ` ${dim(opt.hint)}` : "";
|
|
6157
|
-
process.stdout.write(`${prefix}${label}${hintStr}
|
|
6158
|
-
`);
|
|
6159
|
-
}
|
|
6160
|
-
};
|
|
6161
|
-
if (message) {
|
|
6162
|
-
process.stdout.write(`${message}
|
|
6163
|
-
`);
|
|
6164
|
-
}
|
|
6165
|
-
for (let i = 0; i < options.length; i++) {
|
|
6166
|
-
const opt = options[i];
|
|
6167
|
-
const prefix = i === cursor ? `${cyan("\u276F")} ` : " ";
|
|
6168
|
-
const label = i === cursor ? bold(opt.label) : opt.label;
|
|
6169
|
-
const hintStr = opt.hint ? ` ${dim(opt.hint)}` : "";
|
|
6170
|
-
process.stdout.write(`${prefix}${label}${hintStr}
|
|
6171
|
-
`);
|
|
6172
|
-
}
|
|
6173
|
-
return new Promise((resolve4) => {
|
|
6174
|
-
const onKey = (ch, key) => {
|
|
6175
|
-
if (key.name === "up" || key.sequence === "\x1B[A") {
|
|
6176
|
-
cursor = (cursor - 1 + options.length) % options.length;
|
|
6177
|
-
render();
|
|
6178
|
-
} else if (key.name === "down" || key.sequence === "\x1B[B") {
|
|
6179
|
-
cursor = (cursor + 1) % options.length;
|
|
6180
|
-
render();
|
|
6181
|
-
} else if (key.name === "return" || key.sequence === "\r") {
|
|
6182
|
-
process.stdin.removeListener("keypress", onKey);
|
|
6183
|
-
process.stdout.write("\n");
|
|
6184
|
-
resolve4(options[cursor].value);
|
|
6185
|
-
}
|
|
6186
|
-
};
|
|
6187
|
-
process.stdin.on("keypress", onKey);
|
|
6188
|
-
if (process.stdin.isTTY) {
|
|
6189
|
-
process.stdin.resume();
|
|
6190
|
-
}
|
|
6191
|
-
});
|
|
6192
|
-
} finally {
|
|
6193
|
-
if (process.stdin.isTTY) process.stdin.setRawMode(wasRaw ?? false);
|
|
4599
|
+
}
|
|
4600
|
+
function green(text) {
|
|
4601
|
+
return `\x1B[32m${text}\x1B[0m`;
|
|
4602
|
+
}
|
|
4603
|
+
function yellow(text) {
|
|
4604
|
+
return `\x1B[33m${text}\x1B[0m`;
|
|
4605
|
+
}
|
|
4606
|
+
function red(text) {
|
|
4607
|
+
return `\x1B[31m${text}\x1B[0m`;
|
|
4608
|
+
}
|
|
4609
|
+
function bold(text) {
|
|
4610
|
+
return `\x1B[1m${text}\x1B[0m`;
|
|
4611
|
+
}
|
|
4612
|
+
function dim(text) {
|
|
4613
|
+
return `\x1B[2m${text}\x1B[0m`;
|
|
4614
|
+
}
|
|
4615
|
+
async function prompt(question) {
|
|
4616
|
+
if (process.stdin.isTTY) {
|
|
4617
|
+
process.stdin.removeAllListeners("keypress");
|
|
4618
|
+
process.stdin.pause();
|
|
6194
4619
|
}
|
|
4620
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
4621
|
+
return new Promise((resolve4) => {
|
|
4622
|
+
rl.question(question, (answer) => {
|
|
4623
|
+
rl.close();
|
|
4624
|
+
resolve4(answer.trim());
|
|
4625
|
+
});
|
|
4626
|
+
});
|
|
6195
4627
|
}
|
|
6196
|
-
async function
|
|
6197
|
-
if (options.length === 0)
|
|
4628
|
+
async function select(options, opts) {
|
|
4629
|
+
if (options.length === 0) throw new Error("select() requires at least one option");
|
|
4630
|
+
if (options.length === 1) return options[0].value;
|
|
6198
4631
|
const message = opts?.message ?? "";
|
|
6199
4632
|
const wasRaw = process.stdin.isRaw;
|
|
6200
4633
|
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
6201
4634
|
try {
|
|
6202
4635
|
let cursor = 0;
|
|
6203
|
-
const selected = /* @__PURE__ */ new Set();
|
|
6204
4636
|
const render = () => {
|
|
6205
|
-
const lines = options.length + (message ? 1 : 0)
|
|
4637
|
+
const lines = options.length + (message ? 1 : 0);
|
|
6206
4638
|
process.stdout.write(`\x1B[${lines}A\x1B[0J`);
|
|
6207
4639
|
if (message) {
|
|
6208
4640
|
process.stdout.write(`${message}
|
|
6209
4641
|
`);
|
|
6210
4642
|
}
|
|
6211
|
-
process.stdout.write(`${dim(" \u2191/\u2193 navigate, space to select, enter to confirm")}
|
|
6212
|
-
`);
|
|
6213
4643
|
for (let i = 0; i < options.length; i++) {
|
|
6214
4644
|
const opt = options[i];
|
|
6215
4645
|
const prefix = i === cursor ? `${cyan("\u276F")} ` : " ";
|
|
6216
|
-
const check = selected.has(i) ? green("\u25C9") : "\u25CB";
|
|
6217
4646
|
const label = i === cursor ? bold(opt.label) : opt.label;
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
}
|
|
6221
|
-
};
|
|
6222
|
-
if (message) {
|
|
6223
|
-
process.stdout.write(`${message}
|
|
6224
|
-
`);
|
|
6225
|
-
}
|
|
6226
|
-
process.stdout.write(`${dim(" \u2191/\u2193 navigate, space to select, enter to confirm")}
|
|
6227
|
-
`);
|
|
6228
|
-
for (let i = 0; i < options.length; i++) {
|
|
6229
|
-
const opt = options[i];
|
|
6230
|
-
const prefix = i === cursor ? `${cyan("\u276F")} ` : " ";
|
|
6231
|
-
const check = selected.has(i) ? green("\u25C9") : "\u25CB";
|
|
6232
|
-
const label = i === cursor ? bold(opt.label) : opt.label;
|
|
6233
|
-
process.stdout.write(`${prefix}${check} ${label}
|
|
6234
|
-
`);
|
|
6235
|
-
}
|
|
6236
|
-
return new Promise((resolve4) => {
|
|
6237
|
-
const onKey = (_ch, key) => {
|
|
6238
|
-
if (key.name === "up" || key.sequence === "\x1B[A") {
|
|
6239
|
-
cursor = (cursor - 1 + options.length) % options.length;
|
|
6240
|
-
render();
|
|
6241
|
-
} else if (key.name === "down" || key.sequence === "\x1B[B") {
|
|
6242
|
-
cursor = (cursor + 1) % options.length;
|
|
6243
|
-
render();
|
|
6244
|
-
} else if (key.name === "space") {
|
|
6245
|
-
if (selected.has(cursor)) {
|
|
6246
|
-
selected.delete(cursor);
|
|
6247
|
-
} else {
|
|
6248
|
-
selected.add(cursor);
|
|
6249
|
-
}
|
|
6250
|
-
render();
|
|
6251
|
-
} else if (key.name === "return" || key.sequence === "\r") {
|
|
6252
|
-
process.stdin.removeListener("keypress", onKey);
|
|
6253
|
-
process.stdout.write("\n");
|
|
6254
|
-
const result = Array.from(selected).sort((a, b) => a - b).map((i) => options[i].value);
|
|
6255
|
-
resolve4(result);
|
|
6256
|
-
}
|
|
6257
|
-
};
|
|
6258
|
-
process.stdin.on("keypress", onKey);
|
|
6259
|
-
if (process.stdin.isTTY) {
|
|
6260
|
-
process.stdin.resume();
|
|
6261
|
-
}
|
|
6262
|
-
});
|
|
6263
|
-
} finally {
|
|
6264
|
-
if (process.stdin.isTTY) process.stdin.setRawMode(wasRaw ?? false);
|
|
6265
|
-
}
|
|
6266
|
-
}
|
|
6267
|
-
|
|
6268
|
-
// src/cli/tailscale-login.ts
|
|
6269
|
-
async function interactiveLogin(binary) {
|
|
6270
|
-
console.log(`
|
|
6271
|
-
${cyan("Running interactive login...")}`);
|
|
6272
|
-
console.log("A browser window should open. Authenticate in the browser, then return here.\n");
|
|
6273
|
-
const child = spawn2(binary, ["login"], { stdio: "inherit" });
|
|
6274
|
-
const exitCode = await new Promise((resolve4) => {
|
|
6275
|
-
child.on("close", resolve4);
|
|
6276
|
-
});
|
|
6277
|
-
resetTailscaleBinaryCache();
|
|
6278
|
-
const status = getTailscaleInstallationStatus();
|
|
6279
|
-
if (exitCode === 0 && status.state === "connected") {
|
|
6280
|
-
console.log(`
|
|
6281
|
-
${green("\u2705 Login successful!")} IP: ${status.tailscale_ip}`);
|
|
6282
|
-
showAuthKeyTip();
|
|
6283
|
-
return true;
|
|
6284
|
-
}
|
|
6285
|
-
console.log(`
|
|
6286
|
-
${yellow("\u26A0\uFE0F Login may not have completed. Status: " + status.state)}`);
|
|
6287
|
-
console.log(" Try running: open-party login");
|
|
6288
|
-
return false;
|
|
6289
|
-
}
|
|
6290
|
-
async function authKeyLogin(binary) {
|
|
6291
|
-
console.log("");
|
|
6292
|
-
console.log("Ask the network creator to generate an Auth Key at:");
|
|
6293
|
-
console.log(`${cyan(" https://login.tailscale.com/admin/settings/keys")}
|
|
6294
|
-
`);
|
|
6295
|
-
const authKey = await prompt("Enter Auth Key: ");
|
|
6296
|
-
if (!authKey) {
|
|
6297
|
-
console.log(yellow("No auth key provided, skipping login."));
|
|
6298
|
-
return false;
|
|
6299
|
-
}
|
|
6300
|
-
const result = joinTailnet(authKey);
|
|
6301
|
-
if (result.success) {
|
|
6302
|
-
resetTailscaleBinaryCache();
|
|
6303
|
-
const status = getTailscaleInstallationStatus();
|
|
6304
|
-
console.log(`
|
|
6305
|
-
${green("\u2705 Login successful!")} IP: ${status.state === "connected" ? status.tailscale_ip : "unknown"}`);
|
|
6306
|
-
showAuthKeyTip();
|
|
6307
|
-
return true;
|
|
6308
|
-
}
|
|
6309
|
-
console.log(`
|
|
6310
|
-
${red("\u274C Login failed:")}
|
|
6311
|
-
${result.output}`);
|
|
6312
|
-
console.log(" Check your auth key and try again.");
|
|
6313
|
-
return false;
|
|
6314
|
-
}
|
|
6315
|
-
function showAuthKeyTip() {
|
|
6316
|
-
console.log("");
|
|
6317
|
-
console.log(`${bold("\u{1F4A1} To share network access with teammates:")}`);
|
|
6318
|
-
console.log(" 1. Go to https://login.tailscale.com/admin/settings/keys");
|
|
6319
|
-
console.log(" 2. Generate an Auth Key");
|
|
6320
|
-
console.log(" 3. Share it with teammates \u2014 they can run: open-party login");
|
|
6321
|
-
}
|
|
6322
|
-
|
|
6323
|
-
// src/cli/setup.ts
|
|
6324
|
-
async function stepTailscale() {
|
|
6325
|
-
console.log(`
|
|
6326
|
-
${bold(cyan("\u{1F50D} Step 1: Tailscale Network"))}
|
|
6327
|
-
`);
|
|
6328
|
-
console.log(
|
|
6329
|
-
"Tailscale enables agents across different machines to discover and\ncommunicate with each other over a secure network.\n"
|
|
6330
|
-
);
|
|
6331
|
-
console.log(
|
|
6332
|
-
`${dim("Without Tailscale, Open Party runs in local mode \u2014 connecting only\nto agents on this machine.")}
|
|
6333
|
-
`
|
|
6334
|
-
);
|
|
6335
|
-
const status = getTailscaleInstallationStatus();
|
|
6336
|
-
if (status.state === "connected") {
|
|
6337
|
-
console.log(`${green("\u2705 Tailscale is connected!")}`);
|
|
6338
|
-
console.log(` IP: ${status.tailscale_ip} Hostname: ${status.hostname}`);
|
|
6339
|
-
return;
|
|
6340
|
-
}
|
|
6341
|
-
if (status.state === "not_installed") {
|
|
6342
|
-
await handleNotInstalled(status.platform);
|
|
6343
|
-
const newStatus = getTailscaleInstallationStatus();
|
|
6344
|
-
if (newStatus.state === "not_installed") {
|
|
6345
|
-
showLocalModeNotice();
|
|
6346
|
-
return;
|
|
6347
|
-
}
|
|
6348
|
-
if (newStatus.state === "connected") {
|
|
6349
|
-
console.log(`
|
|
6350
|
-
${green("\u2705 Tailscale is connected!")} IP: ${newStatus.tailscale_ip}`);
|
|
6351
|
-
return;
|
|
6352
|
-
}
|
|
6353
|
-
await handleNotConnected(newStatus.binary);
|
|
6354
|
-
return;
|
|
6355
|
-
}
|
|
6356
|
-
await handleNotConnected(status.binary);
|
|
6357
|
-
}
|
|
6358
|
-
async function handleNotInstalled(platform) {
|
|
6359
|
-
const info = getInstallInstructions(platform);
|
|
6360
|
-
console.log(`${red("\u274C Tailscale is not installed.")}`);
|
|
6361
|
-
console.log(`
|
|
6362
|
-
Install Tailscale for ${bold(info.os)}:
|
|
6363
|
-
`);
|
|
6364
|
-
if (info.commands.length > 0) {
|
|
6365
|
-
for (const cmd of info.commands) {
|
|
6366
|
-
const prefix = info.needs_sudo ? "sudo " : "";
|
|
6367
|
-
console.log(` ${cyan(prefix + cmd)}`);
|
|
6368
|
-
}
|
|
6369
|
-
}
|
|
6370
|
-
console.log(`
|
|
6371
|
-
Download: ${info.download_url}
|
|
6372
|
-
`);
|
|
6373
|
-
const options = [];
|
|
6374
|
-
if (info.commands.length > 0 && platform !== "win32") {
|
|
6375
|
-
options.push({ label: "Install Tailscale automatically", value: "auto", hint: "recommended" });
|
|
6376
|
-
}
|
|
6377
|
-
options.push({ label: "I've installed Tailscale, re-detect", value: "redetect" });
|
|
6378
|
-
options.push({ label: "Skip \u2014 use local mode only", value: "skip", hint: "agents on this machine only" });
|
|
6379
|
-
const choice = await select(options, { message: "Choose:" });
|
|
6380
|
-
if (choice === "skip") {
|
|
6381
|
-
return;
|
|
6382
|
-
}
|
|
6383
|
-
if (choice === "auto") {
|
|
6384
|
-
console.log("");
|
|
6385
|
-
const result = await installTailscale(platform);
|
|
6386
|
-
if (result.success) {
|
|
6387
|
-
console.log(`${green("\u2705 Tailscale installed successfully!")}`);
|
|
6388
|
-
} else {
|
|
6389
|
-
console.log(`${red("\u274C Installation failed:")}
|
|
6390
|
-
${result.output}`);
|
|
6391
|
-
console.log(`
|
|
6392
|
-
Please install manually and re-run: ${cyan("open-party setup")}`);
|
|
6393
|
-
}
|
|
6394
|
-
return;
|
|
6395
|
-
}
|
|
6396
|
-
if (choice === "redetect") {
|
|
6397
|
-
resetTailscaleBinaryCache();
|
|
6398
|
-
return;
|
|
6399
|
-
}
|
|
6400
|
-
}
|
|
6401
|
-
async function handleNotConnected(binary) {
|
|
6402
|
-
console.log(`${yellow("\u{1F512} Tailscale is installed but not connected.")}
|
|
6403
|
-
`);
|
|
6404
|
-
const options = [
|
|
6405
|
-
{ label: "Interactive login", value: "interactive", hint: "opens browser to authenticate" },
|
|
6406
|
-
{ label: "Auth key", value: "authkey", hint: "from network creator" },
|
|
6407
|
-
{ label: "Skip", value: "skip", hint: "login later with: open-party login" }
|
|
6408
|
-
];
|
|
6409
|
-
const choice = await select(options, { message: "Choose a login method:" });
|
|
6410
|
-
if (choice === "interactive") {
|
|
6411
|
-
await interactiveLogin(binary);
|
|
6412
|
-
} else if (choice === "authkey") {
|
|
6413
|
-
await authKeyLogin(binary);
|
|
6414
|
-
} else {
|
|
6415
|
-
console.log(`
|
|
6416
|
-
${yellow("\u26A0\uFE0F Tailscale not connected. Running in local mode.")}`);
|
|
6417
|
-
console.log(` To connect later, run: ${cyan("open-party login")}`);
|
|
6418
|
-
}
|
|
6419
|
-
}
|
|
6420
|
-
function showLocalModeNotice() {
|
|
6421
|
-
console.log(`
|
|
6422
|
-
${yellow("\u26A0\uFE0F Running in local mode \u2014 connecting to agents on this machine only.")}`);
|
|
6423
|
-
console.log(" To enable cross-machine communication later:");
|
|
6424
|
-
console.log(` 1. Install Tailscale: ${cyan("https://tailscale.com/download")}`);
|
|
6425
|
-
console.log(` 2. Run: ${cyan("open-party login")}`);
|
|
6426
|
-
}
|
|
6427
|
-
async function stepAgentPlugin() {
|
|
6428
|
-
console.log(`
|
|
6429
|
-
${bold(cyan("\u{1F50D} Step 2: Detecting AI agents in your environment..."))}
|
|
4647
|
+
const hintStr = opt.hint ? ` ${dim(opt.hint)}` : "";
|
|
4648
|
+
process.stdout.write(`${prefix}${label}${hintStr}
|
|
6430
4649
|
`);
|
|
6431
|
-
const agents = detectAgents();
|
|
6432
|
-
const detected = agents.filter((a) => a.detected);
|
|
6433
|
-
if (detected.length === 0) {
|
|
6434
|
-
console.log(yellow("No supported AI agents detected in this environment."));
|
|
6435
|
-
console.log(" Supported agents: Claude Code, OpenClaw");
|
|
6436
|
-
console.log("");
|
|
6437
|
-
console.log(" Install one and re-run: open-party setup");
|
|
6438
|
-
return;
|
|
6439
|
-
}
|
|
6440
|
-
console.log("Detected agents:\n");
|
|
6441
|
-
for (const agent of detected) {
|
|
6442
|
-
console.log(` ${green("\u2713")} ${agent.name}`);
|
|
6443
|
-
}
|
|
6444
|
-
console.log("");
|
|
6445
|
-
const options = detected.map((a) => ({ label: a.name, value: a }));
|
|
6446
|
-
const selected = await multiSelect(options, {
|
|
6447
|
-
message: "Select agents to install Open Party plugin:"
|
|
6448
|
-
});
|
|
6449
|
-
if (selected.length === 0) {
|
|
6450
|
-
console.log(yellow("No agents selected, skipping plugin installation."));
|
|
6451
|
-
return;
|
|
6452
|
-
}
|
|
6453
|
-
for (const agent of selected) {
|
|
6454
|
-
console.log(`
|
|
6455
|
-
Installing Open Party plugin for ${agent.name}...`);
|
|
6456
|
-
const result = await installPluginToAgent(agent.type);
|
|
6457
|
-
if (result.success) {
|
|
6458
|
-
console.log(`${green("\u2705")} Plugin installed for ${agent.name}${result.configPath ? ` (${result.configPath})` : ""}`);
|
|
6459
|
-
if (result.warning) {
|
|
6460
|
-
console.log(` ${yellow("\u26A0\uFE0F")} ${result.warning}`);
|
|
6461
|
-
}
|
|
6462
|
-
if (agent.type === "claude-code") {
|
|
6463
|
-
console.log(` ${bold("Please restart Claude Code")} for changes to take effect.`);
|
|
6464
|
-
}
|
|
6465
|
-
if (agent.type === "openclaw") {
|
|
6466
|
-
console.log(` ${bold("Please restart OpenClaw gateway")} for changes to take effect.`);
|
|
6467
4650
|
}
|
|
6468
|
-
}
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
}
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
6484
|
-
|
|
6485
|
-
|
|
6486
|
-
|
|
6487
|
-
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
|
|
6501
|
-
|
|
6502
|
-
const status = getTailscaleInstallationStatus();
|
|
6503
|
-
if (status.state === "connected") {
|
|
6504
|
-
console.log(`${green("\u2705 Tailscale is already connected!")}`);
|
|
6505
|
-
console.log(` IP: ${status.tailscale_ip} Hostname: ${status.hostname}`);
|
|
6506
|
-
return;
|
|
6507
|
-
}
|
|
6508
|
-
if (status.state === "not_installed") {
|
|
6509
|
-
console.log(`${red("\u274C Tailscale is not installed.")}`);
|
|
6510
|
-
console.log(" Install it first: https://tailscale.com/download");
|
|
6511
|
-
console.log(` Then run: ${cyan("open-party login")}`);
|
|
6512
|
-
return;
|
|
6513
|
-
}
|
|
6514
|
-
console.log(`${yellow("\u{1F512} Tailscale is installed but not connected.")}
|
|
6515
|
-
`);
|
|
6516
|
-
const options = [
|
|
6517
|
-
{ label: "Interactive login", value: "interactive", hint: "opens browser to authenticate" },
|
|
6518
|
-
{ label: "Auth key", value: "authkey", hint: "from network creator" }
|
|
6519
|
-
];
|
|
6520
|
-
const choice = await select(options, { message: "Choose a login method:" });
|
|
6521
|
-
if (choice === "interactive") {
|
|
6522
|
-
await interactiveLogin(status.binary);
|
|
6523
|
-
} else {
|
|
6524
|
-
await authKeyLogin(status.binary);
|
|
6525
|
-
}
|
|
6526
|
-
}
|
|
6527
|
-
|
|
6528
|
-
// src/cli/logout.ts
|
|
6529
|
-
init_tailscale();
|
|
6530
|
-
async function logoutCommand() {
|
|
6531
|
-
const status = getTailscaleInstallationStatus();
|
|
6532
|
-
if (status.state === "not_installed") {
|
|
6533
|
-
console.log(red("\u274C Tailscale is not installed."));
|
|
6534
|
-
return;
|
|
6535
|
-
}
|
|
6536
|
-
if (status.state === "not_connected") {
|
|
6537
|
-
console.log(yellow("\u26A0\uFE0F Tailscale is not connected \u2014 nothing to log out from."));
|
|
6538
|
-
return;
|
|
6539
|
-
}
|
|
6540
|
-
const choice = await select(
|
|
6541
|
-
[
|
|
6542
|
-
{ label: "Log out (remove credentials)", value: "logout", hint: "need to re-authenticate next time" },
|
|
6543
|
-
{ label: "Cancel", value: "cancel" }
|
|
6544
|
-
],
|
|
6545
|
-
{ message: "Are you sure you want to log out?" }
|
|
6546
|
-
);
|
|
6547
|
-
if (choice === "cancel") return;
|
|
6548
|
-
console.log("Logging out of Tailscale...");
|
|
6549
|
-
const result = logoutTailscale();
|
|
6550
|
-
if (result.success) {
|
|
6551
|
-
console.log(green("\u2705 Logged out successfully."));
|
|
6552
|
-
console.log(" To reconnect, run: open-party login");
|
|
6553
|
-
} else {
|
|
6554
|
-
console.log(red("\u274C Logout failed:"), result.output);
|
|
4651
|
+
};
|
|
4652
|
+
if (message) {
|
|
4653
|
+
process.stdout.write(`${message}
|
|
4654
|
+
`);
|
|
4655
|
+
}
|
|
4656
|
+
for (let i = 0; i < options.length; i++) {
|
|
4657
|
+
const opt = options[i];
|
|
4658
|
+
const prefix = i === cursor ? `${cyan("\u276F")} ` : " ";
|
|
4659
|
+
const label = i === cursor ? bold(opt.label) : opt.label;
|
|
4660
|
+
const hintStr = opt.hint ? ` ${dim(opt.hint)}` : "";
|
|
4661
|
+
process.stdout.write(`${prefix}${label}${hintStr}
|
|
4662
|
+
`);
|
|
4663
|
+
}
|
|
4664
|
+
return new Promise((resolve4) => {
|
|
4665
|
+
const onKey = (ch, key) => {
|
|
4666
|
+
if (key.name === "up" || key.sequence === "\x1B[A") {
|
|
4667
|
+
cursor = (cursor - 1 + options.length) % options.length;
|
|
4668
|
+
render();
|
|
4669
|
+
} else if (key.name === "down" || key.sequence === "\x1B[B") {
|
|
4670
|
+
cursor = (cursor + 1) % options.length;
|
|
4671
|
+
render();
|
|
4672
|
+
} else if (key.name === "return" || key.sequence === "\r") {
|
|
4673
|
+
process.stdin.removeListener("keypress", onKey);
|
|
4674
|
+
process.stdout.write("\n");
|
|
4675
|
+
resolve4(options[cursor].value);
|
|
4676
|
+
}
|
|
4677
|
+
};
|
|
4678
|
+
process.stdin.on("keypress", onKey);
|
|
4679
|
+
if (process.stdin.isTTY) {
|
|
4680
|
+
process.stdin.resume();
|
|
4681
|
+
}
|
|
4682
|
+
});
|
|
4683
|
+
} finally {
|
|
4684
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(wasRaw ?? false);
|
|
6555
4685
|
}
|
|
6556
4686
|
}
|
|
6557
4687
|
|
|
6558
4688
|
// src/cli/server-utils.ts
|
|
6559
|
-
import { spawn
|
|
6560
|
-
import { existsSync
|
|
6561
|
-
import { join
|
|
6562
|
-
import { homedir
|
|
4689
|
+
import { spawn, execSync } from "child_process";
|
|
4690
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdirSync, openSync } from "fs";
|
|
4691
|
+
import { join, dirname, resolve } from "path";
|
|
4692
|
+
import { homedir } from "os";
|
|
6563
4693
|
import { fileURLToPath } from "url";
|
|
6564
|
-
var __dirname =
|
|
4694
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6565
4695
|
function pidFilePath() {
|
|
6566
4696
|
const pluginData = process.env.CLAUDE_PLUGIN_DATA || "";
|
|
6567
|
-
if (pluginData) return
|
|
6568
|
-
return
|
|
4697
|
+
if (pluginData) return join(pluginData, "server.pid");
|
|
4698
|
+
return join(homedir(), ".open-party", "server.pid");
|
|
6569
4699
|
}
|
|
6570
4700
|
function logFilePath() {
|
|
6571
4701
|
const pluginData = process.env.CLAUDE_PLUGIN_DATA || "";
|
|
6572
|
-
if (pluginData) return
|
|
6573
|
-
return
|
|
4702
|
+
if (pluginData) return join(pluginData, "server.log");
|
|
4703
|
+
return join(homedir(), ".open-party", "server.log");
|
|
6574
4704
|
}
|
|
6575
4705
|
function serverScriptPath() {
|
|
6576
|
-
return
|
|
4706
|
+
return resolve(__dirname, "..", "party-server.js");
|
|
6577
4707
|
}
|
|
6578
4708
|
function readPid() {
|
|
6579
4709
|
const path = pidFilePath();
|
|
6580
|
-
if (!
|
|
4710
|
+
if (!existsSync(path)) return null;
|
|
6581
4711
|
try {
|
|
6582
|
-
return parseInt(
|
|
4712
|
+
return parseInt(readFileSync(path, "utf-8").trim(), 10);
|
|
6583
4713
|
} catch {
|
|
6584
4714
|
return null;
|
|
6585
4715
|
}
|
|
6586
4716
|
}
|
|
6587
4717
|
function writePid(pid) {
|
|
6588
4718
|
const path = pidFilePath();
|
|
6589
|
-
const dir =
|
|
6590
|
-
if (!
|
|
6591
|
-
|
|
4719
|
+
const dir = dirname(path);
|
|
4720
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
4721
|
+
writeFileSync(path, String(pid));
|
|
6592
4722
|
}
|
|
6593
4723
|
function removePidFile() {
|
|
6594
4724
|
try {
|
|
@@ -6599,7 +4729,7 @@ function removePidFile() {
|
|
|
6599
4729
|
function isProcessRunning(pid) {
|
|
6600
4730
|
if (process.platform === "win32") {
|
|
6601
4731
|
try {
|
|
6602
|
-
const output2 =
|
|
4732
|
+
const output2 = execSync(`tasklist /FI "PID eq ${pid}" /NH`, {
|
|
6603
4733
|
encoding: "utf-8",
|
|
6604
4734
|
windowsHide: true,
|
|
6605
4735
|
stdio: ["pipe", "pipe", "pipe"]
|
|
@@ -6646,15 +4776,15 @@ async function getServerOverview(port) {
|
|
|
6646
4776
|
}
|
|
6647
4777
|
async function spawnServerInBackground(port) {
|
|
6648
4778
|
const script = serverScriptPath();
|
|
6649
|
-
if (!
|
|
4779
|
+
if (!existsSync(script)) {
|
|
6650
4780
|
console.error(`Server script not found: ${script}`);
|
|
6651
4781
|
return { pid: 0, ok: false };
|
|
6652
4782
|
}
|
|
6653
4783
|
const logPath = logFilePath();
|
|
6654
|
-
|
|
4784
|
+
mkdirSync(dirname(logPath), { recursive: true });
|
|
6655
4785
|
const logFd = openSync(logPath, "a");
|
|
6656
4786
|
const env = { ...process.env, PARTY_PORT: String(port) };
|
|
6657
|
-
const proc =
|
|
4787
|
+
const proc = spawn(process.execPath, [script], {
|
|
6658
4788
|
stdio: ["ignore", logFd, logFd],
|
|
6659
4789
|
detached: true,
|
|
6660
4790
|
windowsHide: true,
|
|
@@ -6678,56 +4808,356 @@ async function waitForServerReady(port, timeoutMs = 1e4) {
|
|
|
6678
4808
|
}
|
|
6679
4809
|
await sleep(500);
|
|
6680
4810
|
}
|
|
6681
|
-
return false;
|
|
4811
|
+
return false;
|
|
4812
|
+
}
|
|
4813
|
+
function killServer(pid) {
|
|
4814
|
+
try {
|
|
4815
|
+
if (process.platform === "win32") {
|
|
4816
|
+
execSync(`taskkill /F /T /PID ${pid}`, { stdio: "ignore", windowsHide: true });
|
|
4817
|
+
} else {
|
|
4818
|
+
process.kill(pid, "SIGTERM");
|
|
4819
|
+
}
|
|
4820
|
+
} catch (error) {
|
|
4821
|
+
void error;
|
|
4822
|
+
}
|
|
4823
|
+
}
|
|
4824
|
+
function findPidByPort(port) {
|
|
4825
|
+
try {
|
|
4826
|
+
if (process.platform === "win32") {
|
|
4827
|
+
const output3 = execSync(
|
|
4828
|
+
`netstat -ano -p tcp | findstr "LISTENING" | findstr ":${port} "`,
|
|
4829
|
+
{ encoding: "utf-8", windowsHide: true, stdio: ["pipe", "pipe", "pipe"] }
|
|
4830
|
+
);
|
|
4831
|
+
const match2 = output3.trim().match(/\s(\d+)\s*$/);
|
|
4832
|
+
return match2 ? parseInt(match2[1], 10) : null;
|
|
4833
|
+
}
|
|
4834
|
+
const output2 = execSync(`lsof -t -i :${port} -sTCP:LISTEN`, {
|
|
4835
|
+
encoding: "utf-8",
|
|
4836
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
4837
|
+
});
|
|
4838
|
+
const pid = parseInt(output2.trim().split("\n")[0], 10);
|
|
4839
|
+
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
4840
|
+
} catch {
|
|
4841
|
+
return null;
|
|
4842
|
+
}
|
|
4843
|
+
}
|
|
4844
|
+
function extractFlagValue(args2, flag) {
|
|
4845
|
+
for (let i = 0; i < args2.length; i++) {
|
|
4846
|
+
if (args2[i] === flag) return args2[i + 1];
|
|
4847
|
+
}
|
|
4848
|
+
return void 0;
|
|
4849
|
+
}
|
|
4850
|
+
function parseStartArgs(args2) {
|
|
4851
|
+
let daemon = false;
|
|
4852
|
+
let port = null;
|
|
4853
|
+
for (let i = 0; i < args2.length; i++) {
|
|
4854
|
+
if (args2[i] === "-d" || args2[i] === "--daemon") {
|
|
4855
|
+
daemon = true;
|
|
4856
|
+
} else if (args2[i] === "-p" || args2[i] === "--port") {
|
|
4857
|
+
const val = args2[++i];
|
|
4858
|
+
if (val) port = parseInt(val, 10);
|
|
4859
|
+
} else if (args2[i].startsWith("--port=")) {
|
|
4860
|
+
port = parseInt(args2[i].split("=")[1], 10);
|
|
4861
|
+
}
|
|
4862
|
+
}
|
|
4863
|
+
return { daemon, port };
|
|
4864
|
+
}
|
|
4865
|
+
function sleep(ms) {
|
|
4866
|
+
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
4867
|
+
}
|
|
4868
|
+
|
|
4869
|
+
// src/cli/setup.ts
|
|
4870
|
+
var IS_WIN = platform() === "win32";
|
|
4871
|
+
var ROOT = resolve2(import.meta.dirname ?? ".", "..", "..");
|
|
4872
|
+
function sh(cmd) {
|
|
4873
|
+
execSync2(cmd, { stdio: "inherit", shell: IS_WIN });
|
|
4874
|
+
}
|
|
4875
|
+
function safeRm(target) {
|
|
4876
|
+
if (!existsSync2(target)) return;
|
|
4877
|
+
if (IS_WIN) {
|
|
4878
|
+
const isDir = statSync(target).isDirectory();
|
|
4879
|
+
sh(isDir ? `rd /s /q "${target}"` : `del /f /q "${target}"`);
|
|
4880
|
+
} else {
|
|
4881
|
+
sh(`rm -rf "${target}"`);
|
|
4882
|
+
}
|
|
4883
|
+
}
|
|
4884
|
+
function safeCp(src, dst) {
|
|
4885
|
+
ensureDir(dst);
|
|
4886
|
+
if (IS_WIN) {
|
|
4887
|
+
const isDir = statSync(src).isDirectory();
|
|
4888
|
+
sh(isDir ? `xcopy "${src}" "${dst}\\" /E /I /Y /Q >nul 2>&1` : `copy /y "${src}" "${dst}" >nul 2>&1`);
|
|
4889
|
+
} else {
|
|
4890
|
+
sh(`cp -r "${src}" "${dst}"`);
|
|
4891
|
+
}
|
|
4892
|
+
}
|
|
4893
|
+
function ensureDir(filePath) {
|
|
4894
|
+
const dir = dirname2(filePath);
|
|
4895
|
+
if (!existsSync2(dir)) mkdirSync2(dir, { recursive: true });
|
|
4896
|
+
}
|
|
4897
|
+
function readJsonFile(filePath, fallback) {
|
|
4898
|
+
if (!existsSync2(filePath)) return fallback;
|
|
4899
|
+
try {
|
|
4900
|
+
return JSON.parse(readFileSync2(filePath, "utf-8"));
|
|
4901
|
+
} catch {
|
|
4902
|
+
return fallback;
|
|
4903
|
+
}
|
|
4904
|
+
}
|
|
4905
|
+
function writeJsonFile(filePath, data) {
|
|
4906
|
+
ensureDir(filePath);
|
|
4907
|
+
writeFileSync2(filePath, JSON.stringify(data, null, 2) + "\n");
|
|
4908
|
+
}
|
|
4909
|
+
function findPluginDistDir() {
|
|
4910
|
+
const distDir = join2(ROOT, "dist", "claude-code");
|
|
4911
|
+
if (!existsSync2(distDir)) return null;
|
|
4912
|
+
try {
|
|
4913
|
+
const entries = readdirSync(distDir);
|
|
4914
|
+
const dirs = entries.filter((e) => e.startsWith("open-party-"));
|
|
4915
|
+
if (dirs.length === 0) return null;
|
|
4916
|
+
const pluginDir = join2(distDir, dirs[dirs.length - 1]);
|
|
4917
|
+
if (existsSync2(join2(pluginDir, ".claude-plugin", "plugin.json"))) return pluginDir;
|
|
4918
|
+
} catch {
|
|
4919
|
+
}
|
|
4920
|
+
return null;
|
|
4921
|
+
}
|
|
4922
|
+
function getPluginVersion(pluginDir) {
|
|
4923
|
+
const manifest = readJsonFile(join2(pluginDir, ".claude-plugin", "plugin.json"), {});
|
|
4924
|
+
if (manifest.version) return manifest.version;
|
|
4925
|
+
try {
|
|
4926
|
+
const pkg = JSON.parse(readFileSync2(join2(ROOT, "package.json"), "utf-8"));
|
|
4927
|
+
if (pkg.version) return pkg.version;
|
|
4928
|
+
} catch {
|
|
4929
|
+
}
|
|
4930
|
+
return "0.0.0";
|
|
4931
|
+
}
|
|
4932
|
+
function installToClaudeCode() {
|
|
4933
|
+
const pluginDir = findPluginDistDir();
|
|
4934
|
+
if (!pluginDir) {
|
|
4935
|
+
console.log(`${red('Plugin package not found. Run "npm run build:plugin" first.')}`);
|
|
4936
|
+
return false;
|
|
4937
|
+
}
|
|
4938
|
+
const version = getPluginVersion(pluginDir);
|
|
4939
|
+
console.log(`Installing Open Party v${version} into Claude Code...
|
|
4940
|
+
`);
|
|
4941
|
+
const installDir = join2(homedir2(), ".claude", "plugins", "cache", "open-party", "open-party", version);
|
|
4942
|
+
if (existsSync2(installDir)) safeRm(installDir);
|
|
4943
|
+
mkdirSync2(installDir, { recursive: true });
|
|
4944
|
+
safeCp(pluginDir, installDir);
|
|
4945
|
+
registerMarketplace(version, pluginDir);
|
|
4946
|
+
const pluginsJsonPath = join2(homedir2(), ".claude", "plugins", "installed_plugins.json");
|
|
4947
|
+
const pluginsData = readJsonFile(
|
|
4948
|
+
pluginsJsonPath,
|
|
4949
|
+
{ version: 2, plugins: {} }
|
|
4950
|
+
);
|
|
4951
|
+
if (!pluginsData.plugins) pluginsData.plugins = {};
|
|
4952
|
+
delete pluginsData.plugins["open-party@local"];
|
|
4953
|
+
pluginsData.plugins["open-party@open-party"] = [{
|
|
4954
|
+
scope: "user",
|
|
4955
|
+
installPath: installDir,
|
|
4956
|
+
version,
|
|
4957
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4958
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
4959
|
+
}];
|
|
4960
|
+
writeJsonFile(pluginsJsonPath, pluginsData);
|
|
4961
|
+
const settingsPath = join2(homedir2(), ".claude", "settings.json");
|
|
4962
|
+
const settings = readJsonFile(settingsPath, {});
|
|
4963
|
+
if (settings.mcpServers?.["open-party"]) {
|
|
4964
|
+
delete settings.mcpServers["open-party"];
|
|
4965
|
+
}
|
|
4966
|
+
if (!settings.enabledPlugins) settings.enabledPlugins = {};
|
|
4967
|
+
delete settings.enabledPlugins["open-party@local"];
|
|
4968
|
+
settings.enabledPlugins["open-party@open-party"] = true;
|
|
4969
|
+
writeJsonFile(settingsPath, settings);
|
|
4970
|
+
console.log(`${green("Plugin installed successfully.")}`);
|
|
4971
|
+
console.log(` Path: ${installDir}
|
|
4972
|
+
`);
|
|
4973
|
+
return true;
|
|
4974
|
+
}
|
|
4975
|
+
function registerMarketplace(version, pluginDir) {
|
|
4976
|
+
const marketplaceDir = join2(homedir2(), ".claude", "plugins", "marketplaces", "open-party");
|
|
4977
|
+
const marketplacePluginDir = join2(marketplaceDir, ".claude-plugin");
|
|
4978
|
+
if (!existsSync2(marketplacePluginDir)) mkdirSync2(marketplacePluginDir, { recursive: true });
|
|
4979
|
+
writeJsonFile(join2(marketplacePluginDir, "marketplace.json"), {
|
|
4980
|
+
name: "open-party",
|
|
4981
|
+
owner: { name: "Feynman Zhang" },
|
|
4982
|
+
metadata: {
|
|
4983
|
+
description: "Decentralized Agent communication network for Claude Code",
|
|
4984
|
+
homepage: "https://github.com/FeynmanZhang/open-party"
|
|
4985
|
+
},
|
|
4986
|
+
plugins: [{
|
|
4987
|
+
name: "open-party",
|
|
4988
|
+
version,
|
|
4989
|
+
source: "./plugin",
|
|
4990
|
+
description: "Decentralized Agent communication network for Claude Code"
|
|
4991
|
+
}]
|
|
4992
|
+
});
|
|
4993
|
+
const pluginSourceDir = join2(marketplaceDir, "plugin");
|
|
4994
|
+
if (existsSync2(pluginSourceDir)) safeRm(pluginSourceDir);
|
|
4995
|
+
mkdirSync2(pluginSourceDir, { recursive: true });
|
|
4996
|
+
safeCp(pluginDir, pluginSourceDir);
|
|
4997
|
+
const knownPath = join2(homedir2(), ".claude", "plugins", "known_marketplaces.json");
|
|
4998
|
+
const known = readJsonFile(knownPath, {});
|
|
4999
|
+
known["open-party"] = {
|
|
5000
|
+
source: { source: "github", repo: "FeynmanZhang/open-party" },
|
|
5001
|
+
installLocation: marketplaceDir,
|
|
5002
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
5003
|
+
};
|
|
5004
|
+
writeJsonFile(knownPath, known);
|
|
5005
|
+
}
|
|
5006
|
+
function buildPlugin() {
|
|
5007
|
+
console.log(`${bold(cyan("Building plugin..."))}
|
|
5008
|
+
`);
|
|
5009
|
+
try {
|
|
5010
|
+
execSync2("node scripts/build-plugin.mjs", { cwd: ROOT, stdio: "inherit" });
|
|
5011
|
+
return true;
|
|
5012
|
+
} catch {
|
|
5013
|
+
console.log(`${red("Build failed.")}`);
|
|
5014
|
+
return false;
|
|
5015
|
+
}
|
|
5016
|
+
}
|
|
5017
|
+
async function setupCommand() {
|
|
5018
|
+
console.log(`
|
|
5019
|
+
${bold(cyan("Open Party Setup"))}
|
|
5020
|
+
`);
|
|
5021
|
+
if (!buildPlugin()) {
|
|
5022
|
+
process.exit(1);
|
|
5023
|
+
}
|
|
5024
|
+
if (!installToClaudeCode()) {
|
|
5025
|
+
process.exit(1);
|
|
5026
|
+
}
|
|
5027
|
+
console.log(`${bold(cyan("Starting Party Server..."))}
|
|
5028
|
+
`);
|
|
5029
|
+
const port = resolvePort([]);
|
|
5030
|
+
try {
|
|
5031
|
+
spawnServerInBackground(port);
|
|
5032
|
+
await waitForServerReady(port, 15e3);
|
|
5033
|
+
console.log(`${green("Party Server started on port " + port)}.
|
|
5034
|
+
`);
|
|
5035
|
+
} catch (error) {
|
|
5036
|
+
console.log(`${red("Failed to start Party Server:")} ${error instanceof Error ? error.message : String(error)}`);
|
|
5037
|
+
console.log(" You can start it manually with: open-party start");
|
|
5038
|
+
}
|
|
5039
|
+
console.log(`${green("Setup complete!")}`);
|
|
5040
|
+
console.log(" Restart Claude Code to load the plugin.");
|
|
5041
|
+
console.log(` Use ${cyan("open-party agents")} to see who's online.
|
|
5042
|
+
`);
|
|
5043
|
+
}
|
|
5044
|
+
|
|
5045
|
+
// src/cli/login.ts
|
|
5046
|
+
init_tailscale();
|
|
5047
|
+
|
|
5048
|
+
// src/cli/tailscale-login.ts
|
|
5049
|
+
init_tailscale();
|
|
5050
|
+
import { spawn as spawn2 } from "child_process";
|
|
5051
|
+
async function interactiveLogin(binary) {
|
|
5052
|
+
console.log(`
|
|
5053
|
+
${cyan("Running interactive login...")}`);
|
|
5054
|
+
console.log("A browser window should open. Authenticate in the browser, then return here.\n");
|
|
5055
|
+
const child = spawn2(binary, ["login"], { stdio: "inherit" });
|
|
5056
|
+
const exitCode = await new Promise((resolve4) => {
|
|
5057
|
+
child.on("close", resolve4);
|
|
5058
|
+
});
|
|
5059
|
+
resetTailscaleBinaryCache();
|
|
5060
|
+
const status = getTailscaleInstallationStatus();
|
|
5061
|
+
if (exitCode === 0 && status.state === "connected") {
|
|
5062
|
+
console.log(`
|
|
5063
|
+
${green("\u2705 Login successful!")} IP: ${status.tailscale_ip}`);
|
|
5064
|
+
showAuthKeyTip();
|
|
5065
|
+
return true;
|
|
5066
|
+
}
|
|
5067
|
+
console.log(`
|
|
5068
|
+
${yellow("\u26A0\uFE0F Login may not have completed. Status: " + status.state)}`);
|
|
5069
|
+
console.log(" Try running: open-party login");
|
|
5070
|
+
return false;
|
|
5071
|
+
}
|
|
5072
|
+
async function authKeyLogin(binary) {
|
|
5073
|
+
console.log("");
|
|
5074
|
+
console.log("Ask the network creator to generate an Auth Key at:");
|
|
5075
|
+
console.log(`${cyan(" https://login.tailscale.com/admin/settings/keys")}
|
|
5076
|
+
`);
|
|
5077
|
+
const authKey = await prompt("Enter Auth Key: ");
|
|
5078
|
+
if (!authKey) {
|
|
5079
|
+
console.log(yellow("No auth key provided, skipping login."));
|
|
5080
|
+
return false;
|
|
5081
|
+
}
|
|
5082
|
+
const result = joinTailnet(authKey);
|
|
5083
|
+
if (result.success) {
|
|
5084
|
+
resetTailscaleBinaryCache();
|
|
5085
|
+
const status = getTailscaleInstallationStatus();
|
|
5086
|
+
console.log(`
|
|
5087
|
+
${green("\u2705 Login successful!")} IP: ${status.state === "connected" ? status.tailscale_ip : "unknown"}`);
|
|
5088
|
+
showAuthKeyTip();
|
|
5089
|
+
return true;
|
|
5090
|
+
}
|
|
5091
|
+
console.log(`
|
|
5092
|
+
${red("\u274C Login failed:")}
|
|
5093
|
+
${result.output}`);
|
|
5094
|
+
console.log(" Check your auth key and try again.");
|
|
5095
|
+
return false;
|
|
5096
|
+
}
|
|
5097
|
+
function showAuthKeyTip() {
|
|
5098
|
+
console.log("");
|
|
5099
|
+
console.log(`${bold("\u{1F4A1} To share network access with teammates:")}`);
|
|
5100
|
+
console.log(" 1. Go to https://login.tailscale.com/admin/settings/keys");
|
|
5101
|
+
console.log(" 2. Generate an Auth Key");
|
|
5102
|
+
console.log(" 3. Share it with teammates \u2014 they can run: open-party login");
|
|
5103
|
+
}
|
|
5104
|
+
|
|
5105
|
+
// src/cli/login.ts
|
|
5106
|
+
async function loginCommand() {
|
|
5107
|
+
const status = getTailscaleInstallationStatus();
|
|
5108
|
+
if (status.state === "connected") {
|
|
5109
|
+
console.log(`${green("\u2705 Tailscale is already connected!")}`);
|
|
5110
|
+
console.log(` IP: ${status.tailscale_ip} Hostname: ${status.hostname}`);
|
|
5111
|
+
return;
|
|
5112
|
+
}
|
|
5113
|
+
if (status.state === "not_installed") {
|
|
5114
|
+
console.log(`${red("\u274C Tailscale is not installed.")}`);
|
|
5115
|
+
console.log(" Install it first: https://tailscale.com/download");
|
|
5116
|
+
console.log(` Then run: ${cyan("open-party login")}`);
|
|
5117
|
+
return;
|
|
5118
|
+
}
|
|
5119
|
+
console.log(`${yellow("\u{1F512} Tailscale is installed but not connected.")}
|
|
5120
|
+
`);
|
|
5121
|
+
const options = [
|
|
5122
|
+
{ label: "Interactive login", value: "interactive", hint: "opens browser to authenticate" },
|
|
5123
|
+
{ label: "Auth key", value: "authkey", hint: "from network creator" }
|
|
5124
|
+
];
|
|
5125
|
+
const choice = await select(options, { message: "Choose a login method:" });
|
|
5126
|
+
if (choice === "interactive") {
|
|
5127
|
+
await interactiveLogin(status.binary);
|
|
5128
|
+
} else {
|
|
5129
|
+
await authKeyLogin(status.binary);
|
|
5130
|
+
}
|
|
6682
5131
|
}
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
void error;
|
|
5132
|
+
|
|
5133
|
+
// src/cli/logout.ts
|
|
5134
|
+
init_tailscale();
|
|
5135
|
+
async function logoutCommand() {
|
|
5136
|
+
const status = getTailscaleInstallationStatus();
|
|
5137
|
+
if (status.state === "not_installed") {
|
|
5138
|
+
console.log(red("\u274C Tailscale is not installed."));
|
|
5139
|
+
return;
|
|
6692
5140
|
}
|
|
6693
|
-
|
|
6694
|
-
|
|
6695
|
-
|
|
6696
|
-
if (process.platform === "win32") {
|
|
6697
|
-
const output3 = execSync3(
|
|
6698
|
-
`netstat -ano -p tcp | findstr "LISTENING" | findstr ":${port} "`,
|
|
6699
|
-
{ encoding: "utf-8", windowsHide: true, stdio: ["pipe", "pipe", "pipe"] }
|
|
6700
|
-
);
|
|
6701
|
-
const match2 = output3.trim().match(/\s(\d+)\s*$/);
|
|
6702
|
-
return match2 ? parseInt(match2[1], 10) : null;
|
|
6703
|
-
}
|
|
6704
|
-
const output2 = execSync3(`lsof -t -i :${port} -sTCP:LISTEN`, {
|
|
6705
|
-
encoding: "utf-8",
|
|
6706
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
6707
|
-
});
|
|
6708
|
-
const pid = parseInt(output2.trim().split("\n")[0], 10);
|
|
6709
|
-
return Number.isFinite(pid) && pid > 0 ? pid : null;
|
|
6710
|
-
} catch {
|
|
6711
|
-
return null;
|
|
5141
|
+
if (status.state === "not_connected") {
|
|
5142
|
+
console.log(yellow("\u26A0\uFE0F Tailscale is not connected \u2014 nothing to log out from."));
|
|
5143
|
+
return;
|
|
6712
5144
|
}
|
|
6713
|
-
|
|
6714
|
-
|
|
6715
|
-
|
|
6716
|
-
|
|
6717
|
-
|
|
6718
|
-
|
|
6719
|
-
|
|
6720
|
-
|
|
6721
|
-
|
|
6722
|
-
|
|
6723
|
-
|
|
6724
|
-
|
|
6725
|
-
|
|
5145
|
+
const choice = await select(
|
|
5146
|
+
[
|
|
5147
|
+
{ label: "Log out (remove credentials)", value: "logout", hint: "need to re-authenticate next time" },
|
|
5148
|
+
{ label: "Cancel", value: "cancel" }
|
|
5149
|
+
],
|
|
5150
|
+
{ message: "Are you sure you want to log out?" }
|
|
5151
|
+
);
|
|
5152
|
+
if (choice === "cancel") return;
|
|
5153
|
+
console.log("Logging out of Tailscale...");
|
|
5154
|
+
const result = logoutTailscale();
|
|
5155
|
+
if (result.success) {
|
|
5156
|
+
console.log(green("\u2705 Logged out successfully."));
|
|
5157
|
+
console.log(" To reconnect, run: open-party login");
|
|
5158
|
+
} else {
|
|
5159
|
+
console.log(red("\u274C Logout failed:"), result.output);
|
|
6726
5160
|
}
|
|
6727
|
-
return { daemon, port };
|
|
6728
|
-
}
|
|
6729
|
-
function sleep(ms) {
|
|
6730
|
-
return new Promise((resolve4) => setTimeout(resolve4, ms));
|
|
6731
5161
|
}
|
|
6732
5162
|
|
|
6733
5163
|
// src/cli/start-server.ts
|
|
@@ -6908,52 +5338,142 @@ function showVersion() {
|
|
|
6908
5338
|
console.log(`open-party v${pkg.version}`);
|
|
6909
5339
|
}
|
|
6910
5340
|
|
|
6911
|
-
// src/
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
|
|
5341
|
+
// src/client/shared/client.ts
|
|
5342
|
+
var PartyHttpClient = class {
|
|
5343
|
+
baseUrl;
|
|
5344
|
+
timeout;
|
|
5345
|
+
constructor(baseUrl = "http://127.0.0.1:8000", timeout = 5e3) {
|
|
5346
|
+
this.baseUrl = baseUrl.replace(/\/$/, "");
|
|
5347
|
+
this.timeout = timeout;
|
|
6918
5348
|
}
|
|
6919
|
-
|
|
6920
|
-
|
|
6921
|
-
|
|
6922
|
-
|
|
5349
|
+
// -- Dashboard --
|
|
5350
|
+
async getOverview() {
|
|
5351
|
+
return this.request("/dashboard/api/overview");
|
|
5352
|
+
}
|
|
5353
|
+
async request(path, options = {}) {
|
|
5354
|
+
const controller = new AbortController();
|
|
5355
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
5356
|
+
try {
|
|
5357
|
+
const resp = await fetch(`${this.baseUrl}${path}`, {
|
|
5358
|
+
...options,
|
|
5359
|
+
signal: controller.signal,
|
|
5360
|
+
headers: {
|
|
5361
|
+
"Content-Type": "application/json",
|
|
5362
|
+
...options.headers
|
|
5363
|
+
}
|
|
5364
|
+
});
|
|
5365
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}: ${resp.statusText}`);
|
|
5366
|
+
return resp.json();
|
|
5367
|
+
} finally {
|
|
5368
|
+
clearTimeout(timer);
|
|
5369
|
+
}
|
|
5370
|
+
}
|
|
5371
|
+
// -- Agent lifecycle --
|
|
5372
|
+
async register(agentId, displayName, metadata, callbackUrl) {
|
|
5373
|
+
return this.request("/agent/register", {
|
|
5374
|
+
method: "POST",
|
|
5375
|
+
body: JSON.stringify({ agent_id: agentId, display_name: displayName, metadata: metadata ?? {}, callback_url: callbackUrl })
|
|
5376
|
+
});
|
|
5377
|
+
}
|
|
5378
|
+
async remove(agentId) {
|
|
5379
|
+
const result = await this.request("/agent/remove", {
|
|
5380
|
+
method: "POST",
|
|
5381
|
+
body: JSON.stringify({ agent_id: agentId })
|
|
5382
|
+
});
|
|
5383
|
+
return result.status === "removed";
|
|
6923
5384
|
}
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
5385
|
+
async heartbeat(agentId, displayName, metadata, callbackUrl) {
|
|
5386
|
+
return this.request("/agent/heartbeat", {
|
|
5387
|
+
method: "POST",
|
|
5388
|
+
body: JSON.stringify({ agent_id: agentId, display_name: displayName, metadata, callback_url: callbackUrl })
|
|
5389
|
+
});
|
|
5390
|
+
}
|
|
5391
|
+
async listAgents() {
|
|
5392
|
+
const result = await this.request("/agent/list");
|
|
5393
|
+
return result.agents ?? [];
|
|
5394
|
+
}
|
|
5395
|
+
// -- Messaging --
|
|
5396
|
+
async sendMessage(senderId, recipientId, content, summary) {
|
|
5397
|
+
return this.request("/agent/send", {
|
|
5398
|
+
method: "POST",
|
|
5399
|
+
body: JSON.stringify({ sender_id: senderId, recipient_id: recipientId, content, summary })
|
|
5400
|
+
});
|
|
5401
|
+
}
|
|
5402
|
+
async checkMessages(agentId) {
|
|
5403
|
+
const result = await this.request(`/agent/messages/${agentId}`);
|
|
5404
|
+
return result.messages ?? [];
|
|
5405
|
+
}
|
|
5406
|
+
/** Get message history for an agent. */
|
|
5407
|
+
async getMessageHistory(agentId, limit = 20) {
|
|
5408
|
+
const result = await this.request(`/agent/history/${agentId}?limit=${limit}`);
|
|
5409
|
+
return result.history ?? [];
|
|
5410
|
+
}
|
|
5411
|
+
// -- Proxy --
|
|
5412
|
+
async health() {
|
|
5413
|
+
return this.request("/proxy/health");
|
|
5414
|
+
}
|
|
5415
|
+
};
|
|
5416
|
+
|
|
5417
|
+
// src/cli/list-agents.ts
|
|
5418
|
+
async function listAgentsCommand(args2) {
|
|
5419
|
+
const jsonMode = args2.includes("--json");
|
|
5420
|
+
const client = new PartyHttpClient();
|
|
5421
|
+
try {
|
|
5422
|
+
await client.health();
|
|
5423
|
+
} catch {
|
|
5424
|
+
console.log("Party Server is not running.");
|
|
5425
|
+
console.log(" Use 'open-party start' to start it.");
|
|
6927
5426
|
return;
|
|
6928
5427
|
}
|
|
6929
|
-
|
|
6930
|
-
|
|
6931
|
-
|
|
6932
|
-
|
|
6933
|
-
|
|
6934
|
-
|
|
6935
|
-
} else {
|
|
6936
|
-
console.log(`Local agents (${localCount}):`);
|
|
6937
|
-
for (const agent of localAgents) {
|
|
6938
|
-
const id = agent.agent_id ?? "?";
|
|
6939
|
-
const name = agent.display_name ?? id;
|
|
6940
|
-
const ago = formatTimeAgo(agent.last_heartbeat);
|
|
6941
|
-
console.log(` ${id.padEnd(20)} ${name.padEnd(16)} ${ago}`);
|
|
5428
|
+
try {
|
|
5429
|
+
const overview = await client.getOverview();
|
|
5430
|
+
const agentsData = overview.agents;
|
|
5431
|
+
if (!agentsData) {
|
|
5432
|
+
console.log("No agent data available.");
|
|
5433
|
+
return;
|
|
6942
5434
|
}
|
|
6943
|
-
|
|
6944
|
-
|
|
6945
|
-
|
|
5435
|
+
const localAgents = agentsData.local_agents ?? [];
|
|
5436
|
+
const remoteAgents = agentsData.remote_agents ?? [];
|
|
5437
|
+
const localCount = agentsData.local_count ?? localAgents.length;
|
|
5438
|
+
const remoteCount = agentsData.remote_count ?? remoteAgents.length;
|
|
5439
|
+
const allAgents = [...localAgents, ...remoteAgents];
|
|
5440
|
+
const totalCount = localCount + remoteCount;
|
|
5441
|
+
if (jsonMode) {
|
|
5442
|
+
console.log(JSON.stringify({ agents: allAgents, count: totalCount, local_count: localCount, remote_count: remoteCount }, null, 2));
|
|
5443
|
+
return;
|
|
5444
|
+
}
|
|
5445
|
+
if (totalCount === 0) {
|
|
5446
|
+
console.log("No agents currently online.");
|
|
5447
|
+
return;
|
|
5448
|
+
}
|
|
5449
|
+
if (localCount === 0) {
|
|
5450
|
+
console.log("Local agents: (none)");
|
|
5451
|
+
} else {
|
|
5452
|
+
console.log(`Local agents (${localCount}):`);
|
|
5453
|
+
for (const agent of localAgents) {
|
|
5454
|
+
const id = agent.agent_id ?? "?";
|
|
5455
|
+
const name = agent.display_name ?? id;
|
|
5456
|
+
const ago = formatTimeAgo(agent.last_heartbeat);
|
|
5457
|
+
console.log(` ${id.padEnd(20)} ${name.padEnd(16)} ${ago}`);
|
|
5458
|
+
}
|
|
5459
|
+
}
|
|
5460
|
+
if (remoteCount > 0) {
|
|
5461
|
+
console.log(`
|
|
6946
5462
|
Remote agents (${remoteCount}):`);
|
|
6947
|
-
|
|
6948
|
-
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
6952
|
-
|
|
5463
|
+
for (const agent of remoteAgents) {
|
|
5464
|
+
const id = agent.agent_id ?? "?";
|
|
5465
|
+
const name = agent.display_name ?? id;
|
|
5466
|
+
const via = agent.source_peer_ip ?? "?";
|
|
5467
|
+
const ago = formatTimeAgo(agent.last_heartbeat);
|
|
5468
|
+
console.log(` ${id.padEnd(20)} ${name.padEnd(16)} (via ${via}) ${ago}`);
|
|
5469
|
+
}
|
|
6953
5470
|
}
|
|
6954
|
-
|
|
6955
|
-
|
|
6956
|
-
|
|
5471
|
+
console.log("");
|
|
5472
|
+
console.log("Tip: Use `open-party send-message --recipient <id>` to reach any agent above.");
|
|
5473
|
+
} catch (err) {
|
|
5474
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5475
|
+
console.error(`Error: ${msg}`);
|
|
5476
|
+
process.exit(1);
|
|
6957
5477
|
}
|
|
6958
5478
|
}
|
|
6959
5479
|
function formatTimeAgo(timestamp) {
|
|
@@ -6966,118 +5486,465 @@ function formatTimeAgo(timestamp) {
|
|
|
6966
5486
|
}
|
|
6967
5487
|
|
|
6968
5488
|
// src/cli/peers.ts
|
|
6969
|
-
async function peersCommand() {
|
|
6970
|
-
const
|
|
6971
|
-
|
|
5489
|
+
async function peersCommand(args2 = []) {
|
|
5490
|
+
const jsonMode = args2.includes("--json");
|
|
5491
|
+
const client = new PartyHttpClient();
|
|
5492
|
+
try {
|
|
5493
|
+
await client.health();
|
|
5494
|
+
} catch {
|
|
6972
5495
|
console.log("Party Server is not running.");
|
|
6973
5496
|
console.log(" Use 'open-party start' to start it.");
|
|
6974
5497
|
return;
|
|
6975
5498
|
}
|
|
6976
|
-
|
|
6977
|
-
|
|
6978
|
-
|
|
6979
|
-
|
|
6980
|
-
|
|
6981
|
-
|
|
6982
|
-
|
|
6983
|
-
|
|
6984
|
-
|
|
6985
|
-
|
|
6986
|
-
|
|
6987
|
-
|
|
6988
|
-
|
|
6989
|
-
|
|
6990
|
-
const
|
|
6991
|
-
|
|
6992
|
-
|
|
6993
|
-
|
|
6994
|
-
|
|
6995
|
-
|
|
6996
|
-
|
|
6997
|
-
|
|
6998
|
-
|
|
5499
|
+
try {
|
|
5500
|
+
const overview = await client.getOverview();
|
|
5501
|
+
const peers = overview.peers;
|
|
5502
|
+
if (!peers) {
|
|
5503
|
+
console.log("No peer data available.");
|
|
5504
|
+
return;
|
|
5505
|
+
}
|
|
5506
|
+
const details = peers.details ?? [];
|
|
5507
|
+
const remoteAgents = overview.agents?.remote_agents ?? [];
|
|
5508
|
+
const peerAgentCounts = /* @__PURE__ */ new Map();
|
|
5509
|
+
for (const agent of remoteAgents) {
|
|
5510
|
+
const ip = agent.source_peer_ip;
|
|
5511
|
+
peerAgentCounts.set(ip, (peerAgentCounts.get(ip) ?? 0) + 1);
|
|
5512
|
+
}
|
|
5513
|
+
const total = peers.total ?? details.length;
|
|
5514
|
+
if (jsonMode) {
|
|
5515
|
+
console.log(JSON.stringify({
|
|
5516
|
+
peers: details.map((p) => ({
|
|
5517
|
+
...p,
|
|
5518
|
+
agent_count: peerAgentCounts.get(p.ip) ?? 0
|
|
5519
|
+
})),
|
|
5520
|
+
total
|
|
5521
|
+
}, null, 2));
|
|
5522
|
+
return;
|
|
5523
|
+
}
|
|
5524
|
+
if (details.length === 0) {
|
|
5525
|
+
console.log("No peers discovered yet.");
|
|
5526
|
+
return;
|
|
5527
|
+
}
|
|
5528
|
+
console.log(`Peers (${total}):
|
|
6999
5529
|
`);
|
|
7000
|
-
|
|
7001
|
-
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
|
|
5530
|
+
for (const peer of details) {
|
|
5531
|
+
const agentCount = peerAgentCounts.get(peer.ip);
|
|
5532
|
+
const agentStr = agentCount != null ? String(agentCount) : "\u2014";
|
|
5533
|
+
const statusStr = formatStatus(peer.status);
|
|
5534
|
+
console.log(` ${peer.ip.padEnd(18)} ${statusStr.padEnd(16)} ${agentStr} agents`);
|
|
5535
|
+
}
|
|
5536
|
+
} catch (err) {
|
|
5537
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5538
|
+
console.error(`Error: ${msg}`);
|
|
5539
|
+
process.exit(1);
|
|
7005
5540
|
}
|
|
7006
5541
|
}
|
|
7007
5542
|
function formatStatus(status) {
|
|
7008
5543
|
const map = {
|
|
7009
|
-
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
DOWN: "Down",
|
|
7013
|
-
UNKNOWN: "Unknown",
|
|
7014
|
-
MAYBE: "Probing",
|
|
7015
|
-
NOT_SERVER: "Not a server"
|
|
5544
|
+
ALIVE: "Online",
|
|
5545
|
+
DEAD: "Down",
|
|
5546
|
+
UNKNOWN: "Unknown"
|
|
7016
5547
|
};
|
|
7017
5548
|
return map[status] ?? status;
|
|
7018
5549
|
}
|
|
7019
5550
|
|
|
7020
|
-
// src/cli/
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
-
|
|
5551
|
+
// src/cli/identity.ts
|
|
5552
|
+
import { existsSync as existsSync8, readdirSync as readdirSync4, readFileSync as readFileSync6, statSync as statSync3 } from "fs";
|
|
5553
|
+
import { join as join8 } from "path";
|
|
5554
|
+
|
|
5555
|
+
// src/client/shared/session-store.ts
|
|
5556
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync5, readdirSync as readdirSync3, unlinkSync as unlinkSync5, writeFileSync as writeFileSync5 } from "fs";
|
|
5557
|
+
import { join as join7 } from "path";
|
|
5558
|
+
import { homedir as homedir6 } from "os";
|
|
5559
|
+
var SESSION_DIR = join7(homedir6(), ".open-party");
|
|
5560
|
+
var SESSIONS_DIR = join7(SESSION_DIR, "sessions");
|
|
5561
|
+
var AGENTS_DIR = join7(SESSION_DIR, "agents");
|
|
5562
|
+
function updateOnlineStatus(agentId, online) {
|
|
5563
|
+
const session = readSessionByAgent(agentId);
|
|
5564
|
+
if (!session) return;
|
|
5565
|
+
const sessionId = session.session_id;
|
|
5566
|
+
const path = join7(SESSIONS_DIR, `${sessionId}.json`);
|
|
5567
|
+
if (!existsSync7(path)) return;
|
|
5568
|
+
const data = JSON.parse(readFileSync5(path, "utf-8"));
|
|
5569
|
+
data.online = online;
|
|
5570
|
+
writeFileSync5(path, JSON.stringify(data));
|
|
5571
|
+
}
|
|
5572
|
+
function readSession(sessionId) {
|
|
5573
|
+
const path = join7(SESSIONS_DIR, `${sessionId}.json`);
|
|
5574
|
+
if (!existsSync7(path)) return void 0;
|
|
5575
|
+
return JSON.parse(readFileSync5(path, "utf-8"));
|
|
5576
|
+
}
|
|
5577
|
+
function readSessionByAgent(agentId) {
|
|
5578
|
+
const mappingPath = join7(AGENTS_DIR, `${agentId}.json`);
|
|
5579
|
+
if (!existsSync7(mappingPath)) return void 0;
|
|
5580
|
+
const mapping = JSON.parse(readFileSync5(mappingPath, "utf-8"));
|
|
5581
|
+
return readSession(mapping.session_id);
|
|
5582
|
+
}
|
|
5583
|
+
|
|
5584
|
+
// src/cli/identity.ts
|
|
5585
|
+
function resolveIdentity(explicitAgentId) {
|
|
5586
|
+
if (explicitAgentId) {
|
|
5587
|
+
const session = readSessionByAgent(explicitAgentId);
|
|
5588
|
+
if (!session) {
|
|
5589
|
+
console.error(`Error: Agent "${explicitAgentId}" not found in session store.`);
|
|
5590
|
+
console.error('Run "open-party setup" to register, or use --agent-id with a known ID.');
|
|
5591
|
+
process.exit(1);
|
|
7029
5592
|
}
|
|
7030
|
-
return
|
|
5593
|
+
return {
|
|
5594
|
+
agent_id: explicitAgentId,
|
|
5595
|
+
display_name: session.display_name || explicitAgentId,
|
|
5596
|
+
server_url: session.server_url || "http://127.0.0.1:8000"
|
|
5597
|
+
};
|
|
7031
5598
|
}
|
|
7032
|
-
const
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
|
|
5599
|
+
const envId = process.env.OPEN_PARTY_AGENT_ID;
|
|
5600
|
+
if (envId) {
|
|
5601
|
+
const session = readSessionByAgent(envId);
|
|
5602
|
+
if (!session) {
|
|
5603
|
+
console.error(`Error: Agent "${envId}" (from OPEN_PARTY_AGENT_ID) not found in session store.`);
|
|
5604
|
+
process.exit(1);
|
|
5605
|
+
}
|
|
5606
|
+
return {
|
|
5607
|
+
agent_id: envId,
|
|
5608
|
+
display_name: session.display_name || envId,
|
|
5609
|
+
server_url: session.server_url || "http://127.0.0.1:8000"
|
|
5610
|
+
};
|
|
5611
|
+
}
|
|
5612
|
+
if (!existsSync8(SESSIONS_DIR)) {
|
|
5613
|
+
console.error("Error: No Open Party session found.");
|
|
5614
|
+
console.error('Run "open-party setup" to get started, or provide --agent-id.');
|
|
5615
|
+
process.exit(1);
|
|
7043
5616
|
}
|
|
5617
|
+
const files = readdirSync4(SESSIONS_DIR).filter((f) => f.endsWith(".json"));
|
|
5618
|
+
if (files.length === 0) {
|
|
5619
|
+
console.error("Error: No active Open Party sessions.");
|
|
5620
|
+
console.error('Run "open-party setup" to register, or provide --agent-id.');
|
|
5621
|
+
process.exit(1);
|
|
5622
|
+
}
|
|
5623
|
+
const sorted = files.sort(
|
|
5624
|
+
(a, b) => statSync3(join8(SESSIONS_DIR, b)).mtimeMs - statSync3(join8(SESSIONS_DIR, a)).mtimeMs
|
|
5625
|
+
);
|
|
5626
|
+
const latestFile = sorted[0];
|
|
5627
|
+
const raw2 = JSON.parse(readFileSync6(join8(SESSIONS_DIR, latestFile), "utf-8"));
|
|
5628
|
+
return {
|
|
5629
|
+
agent_id: raw2.agent_id,
|
|
5630
|
+
display_name: raw2.display_name || raw2.agent_id,
|
|
5631
|
+
server_url: raw2.server_url || "http://127.0.0.1:8000"
|
|
5632
|
+
};
|
|
7044
5633
|
}
|
|
7045
5634
|
|
|
7046
|
-
// src/cli/
|
|
7047
|
-
function
|
|
7048
|
-
|
|
5635
|
+
// src/cli/send-message.ts
|
|
5636
|
+
async function sendMessageCommand(args2) {
|
|
5637
|
+
const jsonMode = args2.includes("--json");
|
|
5638
|
+
const recipient = extractFlagValue(args2, "--recipient");
|
|
5639
|
+
const content = extractFlagValue(args2, "--content");
|
|
5640
|
+
const summary = extractFlagValue(args2, "--summary");
|
|
5641
|
+
const agentId = extractFlagValue(args2, "--agent-id");
|
|
5642
|
+
if (!recipient || !content) {
|
|
5643
|
+
console.error("Usage: open-party send-message --recipient <agent-id> --content <message> [--summary <text>] [--agent-id <id>] [--json]");
|
|
5644
|
+
process.exit(1);
|
|
5645
|
+
}
|
|
5646
|
+
const identity = resolveIdentity(agentId ?? null);
|
|
5647
|
+
const client = new PartyHttpClient(identity.server_url);
|
|
5648
|
+
try {
|
|
5649
|
+
const result = await client.sendMessage(identity.agent_id, recipient, content, summary);
|
|
5650
|
+
if (jsonMode) {
|
|
5651
|
+
console.log(JSON.stringify(result, null, 2));
|
|
5652
|
+
return;
|
|
5653
|
+
}
|
|
5654
|
+
const status = result.status;
|
|
5655
|
+
if (status === "delivered_locally" || status === "forwarded") {
|
|
5656
|
+
console.log(`[Sent] to ${recipient}`);
|
|
5657
|
+
} else if (status === "agent_not_found") {
|
|
5658
|
+
console.error(`[Warning] ${recipient} not found. Use \`open-party list-agents\` to see who's online.`);
|
|
5659
|
+
process.exit(1);
|
|
5660
|
+
} else {
|
|
5661
|
+
console.log(`[${status}] to ${recipient}`);
|
|
5662
|
+
}
|
|
5663
|
+
} catch (err) {
|
|
5664
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5665
|
+
console.error(`Error: ${msg}`);
|
|
5666
|
+
process.exit(1);
|
|
5667
|
+
}
|
|
5668
|
+
}
|
|
5669
|
+
|
|
5670
|
+
// src/cli/check-messages.ts
|
|
5671
|
+
async function checkMessagesCommand(args2) {
|
|
5672
|
+
const jsonMode = args2.includes("--json");
|
|
5673
|
+
const showHistory = args2.includes("--history");
|
|
5674
|
+
const agentIdFlag = extractFlagValue(args2, "--agent-id");
|
|
5675
|
+
const limitStr = extractFlagValue(args2, "--limit");
|
|
5676
|
+
const limit = limitStr ? Math.min(Math.max(parseInt(limitStr, 10), 1), 50) : 10;
|
|
5677
|
+
const identity = resolveIdentity(agentIdFlag);
|
|
5678
|
+
const client = new PartyHttpClient(identity.server_url);
|
|
5679
|
+
try {
|
|
5680
|
+
const messages = await client.checkMessages(identity.agent_id);
|
|
5681
|
+
if (jsonMode) {
|
|
5682
|
+
const result = { messages, count: messages.length };
|
|
5683
|
+
if (showHistory) {
|
|
5684
|
+
const history = await client.getMessageHistory(identity.agent_id, limit);
|
|
5685
|
+
result.history = history;
|
|
5686
|
+
result.history_count = history.length;
|
|
5687
|
+
}
|
|
5688
|
+
console.log(JSON.stringify(result, null, 2));
|
|
5689
|
+
return;
|
|
5690
|
+
}
|
|
5691
|
+
if (messages.length > 0) {
|
|
5692
|
+
console.log(`New Messages (${messages.length})`);
|
|
5693
|
+
for (const msg of messages) {
|
|
5694
|
+
console.log("");
|
|
5695
|
+
console.log("---");
|
|
5696
|
+
console.log(`From: \`${msg.sender_id}\``);
|
|
5697
|
+
if (msg.summary) console.log(`Summary: ${msg.summary}`);
|
|
5698
|
+
console.log("");
|
|
5699
|
+
console.log(`> ${msg.content}`);
|
|
5700
|
+
}
|
|
5701
|
+
console.log("");
|
|
5702
|
+
console.log('Reply with `open-party send-message --recipient <id> --content "reply"` if needed.');
|
|
5703
|
+
} else {
|
|
5704
|
+
console.log("No new messages.");
|
|
5705
|
+
}
|
|
5706
|
+
if (showHistory) {
|
|
5707
|
+
const history = await client.getMessageHistory(identity.agent_id, limit);
|
|
5708
|
+
if (history.length === 0) {
|
|
5709
|
+
console.log("\nNo message history yet.");
|
|
5710
|
+
return;
|
|
5711
|
+
}
|
|
5712
|
+
console.log(`
|
|
5713
|
+
Message History (last ${history.length})`);
|
|
5714
|
+
for (const entry of history) {
|
|
5715
|
+
console.log("");
|
|
5716
|
+
console.log("---");
|
|
5717
|
+
const isSent = entry.sender_id === identity.agent_id;
|
|
5718
|
+
const arrow = isSent ? "->" : "<-";
|
|
5719
|
+
const peer = isSent ? entry.recipient_id : entry.sender_id;
|
|
5720
|
+
console.log(`${arrow} To: \`${peer}\``);
|
|
5721
|
+
console.log(`> ${entry.content}`);
|
|
5722
|
+
}
|
|
5723
|
+
}
|
|
5724
|
+
} catch (err) {
|
|
5725
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5726
|
+
console.error(`Error: ${msg}`);
|
|
5727
|
+
process.exit(1);
|
|
5728
|
+
}
|
|
5729
|
+
}
|
|
7049
5730
|
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
|
|
7053
|
-
|
|
7054
|
-
|
|
7055
|
-
|
|
7056
|
-
|
|
7057
|
-
|
|
7058
|
-
|
|
7059
|
-
|
|
7060
|
-
|
|
5731
|
+
// src/cli/online.ts
|
|
5732
|
+
function parseArgs(args2) {
|
|
5733
|
+
const result = {};
|
|
5734
|
+
for (let i = 0; i < args2.length; i++) {
|
|
5735
|
+
if (args2[i] === "--name" && args2[i + 1]) {
|
|
5736
|
+
result.name = args2[++i];
|
|
5737
|
+
}
|
|
5738
|
+
}
|
|
5739
|
+
return result;
|
|
5740
|
+
}
|
|
5741
|
+
async function onlineCommand(args2 = []) {
|
|
5742
|
+
const { name } = parseArgs(args2);
|
|
5743
|
+
const identity = resolveIdentity();
|
|
5744
|
+
const displayName = name || identity.display_name;
|
|
5745
|
+
const client = new PartyHttpClient(identity.server_url);
|
|
5746
|
+
try {
|
|
5747
|
+
await client.health();
|
|
5748
|
+
} catch {
|
|
5749
|
+
console.log(`${red("Party Server is not running.")}`);
|
|
5750
|
+
console.log(" Use 'open-party start' to start it.");
|
|
5751
|
+
process.exit(1);
|
|
5752
|
+
}
|
|
5753
|
+
try {
|
|
5754
|
+
await client.register(identity.agent_id, displayName, { type: "claude-code" });
|
|
5755
|
+
updateOnlineStatus(identity.agent_id, true);
|
|
5756
|
+
console.log(`${green(bold("Online"))} ${identity.agent_id} (${displayName})`);
|
|
5757
|
+
console.log(` Use ${cyan("open-party agents")} to see who else is online.`);
|
|
5758
|
+
} catch (err) {
|
|
5759
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5760
|
+
console.error(`${red("Failed to go online:")} ${msg}`);
|
|
5761
|
+
process.exit(1);
|
|
5762
|
+
}
|
|
5763
|
+
}
|
|
7061
5764
|
|
|
7062
|
-
|
|
7063
|
-
|
|
7064
|
-
|
|
5765
|
+
// src/cli/offline.ts
|
|
5766
|
+
async function offlineCommand() {
|
|
5767
|
+
const identity = resolveIdentity();
|
|
5768
|
+
const client = new PartyHttpClient(identity.server_url);
|
|
5769
|
+
try {
|
|
5770
|
+
await client.health();
|
|
5771
|
+
} catch {
|
|
5772
|
+
updateOnlineStatus(identity.agent_id, false);
|
|
5773
|
+
console.log(`${yellow("Party Server is not running.")} Marked as offline.`);
|
|
5774
|
+
return;
|
|
5775
|
+
}
|
|
5776
|
+
try {
|
|
5777
|
+
await client.remove(identity.agent_id);
|
|
5778
|
+
updateOnlineStatus(identity.agent_id, false);
|
|
5779
|
+
console.log(`${green(bold("Offline"))} ${identity.agent_id} (${identity.display_name})`);
|
|
5780
|
+
} catch (err) {
|
|
5781
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5782
|
+
console.error(`${red("Failed to go offline:")} ${msg}`);
|
|
5783
|
+
process.exit(1);
|
|
5784
|
+
}
|
|
5785
|
+
}
|
|
7065
5786
|
|
|
7066
|
-
|
|
7067
|
-
|
|
5787
|
+
// src/cli/registry.ts
|
|
5788
|
+
var COMMANDS = [
|
|
5789
|
+
{
|
|
5790
|
+
name: "setup",
|
|
5791
|
+
description: "Build & install plugin into Claude Code, then start server",
|
|
5792
|
+
usage: "open-party setup"
|
|
5793
|
+
},
|
|
5794
|
+
{
|
|
5795
|
+
name: "start",
|
|
5796
|
+
description: "Start the Party Server (default when no command given)",
|
|
5797
|
+
usage: "open-party start [-d] [-p <port>]",
|
|
5798
|
+
options: [
|
|
5799
|
+
{ flags: "-d, --daemon", description: "Run in background (daemon mode)" },
|
|
5800
|
+
{ flags: "-p, --port <port>", description: "Override port (default: 8000, env: PARTY_PORT)" }
|
|
5801
|
+
],
|
|
5802
|
+
examples: [
|
|
5803
|
+
"open-party start",
|
|
5804
|
+
"open-party start -d",
|
|
5805
|
+
"open-party start -d -p 9000"
|
|
5806
|
+
]
|
|
5807
|
+
},
|
|
5808
|
+
{
|
|
5809
|
+
name: "stop",
|
|
5810
|
+
description: "Stop the Party Server",
|
|
5811
|
+
usage: "open-party stop"
|
|
5812
|
+
},
|
|
5813
|
+
{
|
|
5814
|
+
name: "status",
|
|
5815
|
+
description: "Show server status",
|
|
5816
|
+
usage: "open-party status"
|
|
5817
|
+
},
|
|
5818
|
+
{
|
|
5819
|
+
name: "login",
|
|
5820
|
+
description: "Login to Tailscale network",
|
|
5821
|
+
usage: "open-party login"
|
|
5822
|
+
},
|
|
5823
|
+
{
|
|
5824
|
+
name: "logout",
|
|
5825
|
+
description: "Log out of Tailscale network",
|
|
5826
|
+
usage: "open-party logout"
|
|
5827
|
+
},
|
|
5828
|
+
{
|
|
5829
|
+
name: "agents",
|
|
5830
|
+
alias: "list-agents",
|
|
5831
|
+
description: "List online agents (local + remote, with status)",
|
|
5832
|
+
usage: "open-party agents [--json]",
|
|
5833
|
+
options: [
|
|
5834
|
+
{ flags: "--json", description: "Output raw JSON response" }
|
|
5835
|
+
],
|
|
5836
|
+
examples: [
|
|
5837
|
+
"open-party agents",
|
|
5838
|
+
"open-party agents --json"
|
|
5839
|
+
]
|
|
5840
|
+
},
|
|
5841
|
+
{
|
|
5842
|
+
name: "peers",
|
|
5843
|
+
description: "List discovered peer nodes",
|
|
5844
|
+
usage: "open-party peers [--json]",
|
|
5845
|
+
options: [
|
|
5846
|
+
{ flags: "--json", description: "Output raw JSON response" }
|
|
5847
|
+
]
|
|
5848
|
+
},
|
|
5849
|
+
{
|
|
5850
|
+
name: "send-message",
|
|
5851
|
+
description: "Send a message to an agent",
|
|
5852
|
+
usage: "open-party send-message --recipient <id> --content <text> [options]",
|
|
5853
|
+
options: [
|
|
5854
|
+
{ flags: "--recipient <id>", description: "Target agent ID (required)" },
|
|
5855
|
+
{ flags: "--content <text>", description: "Message content (required)" },
|
|
5856
|
+
{ flags: "--summary <text>", description: "Optional one-line summary" },
|
|
5857
|
+
{ flags: "--agent-id <id>", description: "Override sender identity" },
|
|
5858
|
+
{ flags: "--json", description: "Output raw JSON response" }
|
|
5859
|
+
],
|
|
5860
|
+
examples: [
|
|
5861
|
+
'open-party send-message --recipient <id> --content "hello"',
|
|
5862
|
+
'open-party send-message --recipient <id> --content "hi" --summary "Greeting"'
|
|
5863
|
+
]
|
|
5864
|
+
},
|
|
5865
|
+
{
|
|
5866
|
+
name: "online",
|
|
5867
|
+
description: "Go online \u2014 register this agent with the Party Server",
|
|
5868
|
+
usage: "open-party online [--name <name>]",
|
|
5869
|
+
options: [
|
|
5870
|
+
{ flags: "--name <name>", description: "Override display name" }
|
|
5871
|
+
],
|
|
5872
|
+
examples: [
|
|
5873
|
+
"open-party online",
|
|
5874
|
+
'open-party online --name "my-agent"'
|
|
5875
|
+
]
|
|
5876
|
+
},
|
|
5877
|
+
{
|
|
5878
|
+
name: "offline",
|
|
5879
|
+
description: "Go offline \u2014 unregister from the Party Server",
|
|
5880
|
+
usage: "open-party offline"
|
|
5881
|
+
},
|
|
5882
|
+
{
|
|
5883
|
+
name: "check-messages",
|
|
5884
|
+
description: "Check for new messages (add --history to include history)",
|
|
5885
|
+
usage: "open-party check-messages [--agent-id <id>] [--history] [--limit N] [--json]",
|
|
5886
|
+
options: [
|
|
5887
|
+
{ flags: "--agent-id <id>", description: "Override agent identity" },
|
|
5888
|
+
{ flags: "--history", description: "Also show message history" },
|
|
5889
|
+
{ flags: "--limit N", description: "Number of history messages (default: 10, max: 50)" },
|
|
5890
|
+
{ flags: "--json", description: "Output raw JSON response" }
|
|
5891
|
+
],
|
|
5892
|
+
examples: [
|
|
5893
|
+
"open-party check-messages",
|
|
5894
|
+
"open-party check-messages --history",
|
|
5895
|
+
"open-party check-messages --history --limit 20"
|
|
5896
|
+
]
|
|
5897
|
+
}
|
|
5898
|
+
];
|
|
5899
|
+
function findCommand(name) {
|
|
5900
|
+
return COMMANDS.find(
|
|
5901
|
+
(c) => c.name === name || c.alias === name
|
|
5902
|
+
);
|
|
5903
|
+
}
|
|
7068
5904
|
|
|
7069
|
-
|
|
7070
|
-
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
|
|
7078
|
-
|
|
7079
|
-
|
|
7080
|
-
|
|
5905
|
+
// src/cli/index.ts
|
|
5906
|
+
function showHelp(commandName) {
|
|
5907
|
+
if (commandName) {
|
|
5908
|
+
const cmd = findCommand(commandName);
|
|
5909
|
+
if (!cmd) {
|
|
5910
|
+
console.log(`Unknown command: ${commandName}
|
|
5911
|
+
`);
|
|
5912
|
+
showHelp();
|
|
5913
|
+
return;
|
|
5914
|
+
}
|
|
5915
|
+
console.log(`Usage: ${cmd.usage}`);
|
|
5916
|
+
console.log("");
|
|
5917
|
+
console.log(cmd.description);
|
|
5918
|
+
if (cmd.alias) {
|
|
5919
|
+
console.log(`Alias: ${cmd.alias}`);
|
|
5920
|
+
}
|
|
5921
|
+
if (cmd.options && cmd.options.length > 0) {
|
|
5922
|
+
console.log("");
|
|
5923
|
+
console.log("Options:");
|
|
5924
|
+
for (const opt of cmd.options) {
|
|
5925
|
+
console.log(` ${opt.flags.padEnd(24)} ${opt.description}`);
|
|
5926
|
+
}
|
|
5927
|
+
}
|
|
5928
|
+
if (cmd.examples && cmd.examples.length > 0) {
|
|
5929
|
+
console.log("");
|
|
5930
|
+
console.log("Examples:");
|
|
5931
|
+
for (const ex of cmd.examples) {
|
|
5932
|
+
console.log(` ${ex}`);
|
|
5933
|
+
}
|
|
5934
|
+
}
|
|
5935
|
+
return;
|
|
5936
|
+
}
|
|
5937
|
+
const lines = ["Usage: open-party <command> [options]", "", "Commands:"];
|
|
5938
|
+
for (const cmd of COMMANDS) {
|
|
5939
|
+
const label = cmd.alias ? `${cmd.name}, ${cmd.alias}` : cmd.name;
|
|
5940
|
+
lines.push(` ${label.padEnd(22)} ${cmd.description}`);
|
|
5941
|
+
}
|
|
5942
|
+
lines.push("");
|
|
5943
|
+
lines.push("Global options:");
|
|
5944
|
+
lines.push(" -v, --version Show version number");
|
|
5945
|
+
lines.push("");
|
|
5946
|
+
lines.push('Run "open-party help <command>" for details on a specific command.');
|
|
5947
|
+
console.log(lines.join("\n"));
|
|
7081
5948
|
}
|
|
7082
5949
|
var args = process.argv.slice(2);
|
|
7083
5950
|
var command = args[0] ?? "start";
|
|
@@ -7087,6 +5954,10 @@ async function main2() {
|
|
|
7087
5954
|
showVersion();
|
|
7088
5955
|
process.exit(0);
|
|
7089
5956
|
}
|
|
5957
|
+
if (commandArgs.includes("--help") || commandArgs.includes("-h")) {
|
|
5958
|
+
showHelp(command);
|
|
5959
|
+
process.exit(0);
|
|
5960
|
+
}
|
|
7090
5961
|
switch (command) {
|
|
7091
5962
|
case "setup":
|
|
7092
5963
|
await setupCommand();
|
|
@@ -7097,9 +5968,6 @@ async function main2() {
|
|
|
7097
5968
|
case "logout":
|
|
7098
5969
|
await logoutCommand();
|
|
7099
5970
|
break;
|
|
7100
|
-
case "install":
|
|
7101
|
-
await installCommand();
|
|
7102
|
-
break;
|
|
7103
5971
|
case "start":
|
|
7104
5972
|
await startServer(commandArgs);
|
|
7105
5973
|
break;
|
|
@@ -7110,15 +5978,28 @@ async function main2() {
|
|
|
7110
5978
|
await statusCommand();
|
|
7111
5979
|
break;
|
|
7112
5980
|
case "agents":
|
|
7113
|
-
|
|
5981
|
+
case "list-agents":
|
|
5982
|
+
await listAgentsCommand(commandArgs);
|
|
7114
5983
|
break;
|
|
7115
5984
|
case "peers":
|
|
7116
|
-
await peersCommand();
|
|
5985
|
+
await peersCommand(commandArgs);
|
|
5986
|
+
break;
|
|
5987
|
+
case "send-message":
|
|
5988
|
+
await sendMessageCommand(commandArgs);
|
|
5989
|
+
break;
|
|
5990
|
+
case "check-messages":
|
|
5991
|
+
await checkMessagesCommand(commandArgs);
|
|
5992
|
+
break;
|
|
5993
|
+
case "online":
|
|
5994
|
+
await onlineCommand(commandArgs);
|
|
5995
|
+
break;
|
|
5996
|
+
case "offline":
|
|
5997
|
+
await offlineCommand();
|
|
7117
5998
|
break;
|
|
7118
5999
|
case "help":
|
|
7119
6000
|
case "--help":
|
|
7120
6001
|
case "-h":
|
|
7121
|
-
showHelp();
|
|
6002
|
+
showHelp(args[1]);
|
|
7122
6003
|
break;
|
|
7123
6004
|
default:
|
|
7124
6005
|
console.log(`Unknown command: ${command}
|