@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.
Files changed (31) hide show
  1. package/README.md +138 -0
  2. package/dist/claude-code/{open-party-0.1.6 → open-party-0.1.7}/.claude-plugin/plugin.json +1 -1
  3. package/dist/claude-code/open-party-0.1.7/BUILD_INFO.json +6 -0
  4. package/dist/claude-code/open-party-0.1.7/dist/dispatcher.js +187 -0
  5. package/dist/claude-code/{open-party-0.1.6 → open-party-0.1.7}/dist/hook-handler.js +58 -73
  6. package/dist/claude-code/{open-party-0.1.6 → open-party-0.1.7}/dist/mcp-server.js +552 -364
  7. package/dist/claude-code/{open-party-0.1.6 → open-party-0.1.7}/dist/party-server.js +426 -1657
  8. package/dist/claude-code/{open-party-0.1.6 → open-party-0.1.7}/hooks/hooks.json +39 -50
  9. package/dist/claude-code/{open-party-0.1.6 → open-party-0.1.7}/package.json +1 -1
  10. package/dist/claude-code/{open-party-0.1.6 → open-party-0.1.7}/skills/open-party/SKILL.md +39 -21
  11. package/dist/cli/index.js +1528 -2647
  12. package/dist/cli/index.js.map +1 -1
  13. package/dist/party-server.js +426 -1657
  14. package/dist/party-server.js.map +1 -1
  15. package/package.json +10 -13
  16. package/dist/claude-code/open-party-0.1.6/BUILD_INFO.json +0 -6
  17. package/dist/openclaw/open-party-0.1.5/BUILD_INFO.json +0 -6
  18. package/dist/openclaw/open-party-0.1.5/SKILL.md +0 -127
  19. package/dist/openclaw/open-party-0.1.5/dist/index.js +0 -550
  20. package/dist/openclaw/open-party-0.1.5/dist/party-server.js +0 -5502
  21. package/dist/openclaw/open-party-0.1.5/openclaw.plugin.json +0 -28
  22. package/dist/openclaw/open-party-0.1.5/package.json +0 -12
  23. package/dist/openclaw/open-party-0.1.5/skills/open-party/SKILL.md +0 -90
  24. package/dist/openclaw/open-party-0.1.6/BUILD_INFO.json +0 -6
  25. package/dist/openclaw/open-party-0.1.6/SKILL.md +0 -127
  26. package/dist/openclaw/open-party-0.1.6/dist/index.js +0 -550
  27. package/dist/openclaw/open-party-0.1.6/dist/party-server.js +0 -5502
  28. package/dist/openclaw/open-party-0.1.6/openclaw.plugin.json +0 -28
  29. package/dist/openclaw/open-party-0.1.6/package.json +0 -12
  30. package/dist/openclaw/open-party-0.1.6/skills/open-party/SKILL.md +0 -90
  31. /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 || !existsSync(path)) return false;
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
- join(process.env.ProgramFiles || "C:\\Program Files", "Tailscale", "tailscale.exe"),
54
- join(process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)", "Tailscale", "tailscale.exe"),
55
- join(process.env.LOCALAPPDATA || "", "Tailscale", "tailscale.exe")
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 = execSync(
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("/") && !existsSync(candidate)) continue;
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 existsSync5, mkdirSync as mkdirSync3, appendFileSync, readdirSync as readdirSync2, unlinkSync as unlinkSync2, statSync as statSync2 } from "fs";
3356
- import { join as join5 } from "path";
3357
- import { homedir as homedir4 } from "os";
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 (!existsSync5(LOG_DIR)) {
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(join5(LOG_DIR, f));
3247
+ const stat = statSync2(join4(LOG_DIR, f));
3376
3248
  if (stat.mtimeMs < cutoff) {
3377
- unlinkSync2(join5(LOG_DIR, f));
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 yy = String(d.getFullYear()).slice(2);
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 join5(LOG_DIR, `${yy}-${mm}-${dd}-open-party.log`);
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 yy = String(now.getFullYear()).slice(2);
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 = `${yy}-${mm}-${dd} ${hh}:${min}:${ss}`;
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 = join5(homedir4(), ".open-party", "logs");
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
- output(console.warn, "warn", tag, data ? `${message} ${JSON.stringify(data)}` : message);
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 existsSync6, readFileSync as readFileSync3, writeFileSync as writeFileSync3, unlinkSync as unlinkSync3, renameSync, mkdirSync as mkdirSync4 } from "fs";
3458
- import { join as join6 } from "path";
3459
- import { homedir as homedir5 } from "os";
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 join6(pluginData, "data");
3463
- return join6(homedir5(), ".open-party", "data");
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, DEBOUNCE_MS, MIGRATORS, SnapshotManager;
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 = 1;
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 = join6(this._dir, SNAPSHOT_FILE);
3519
- this._markerPath = join6(this._dir, SHUTDOWN_MARKER_FILE);
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 (existsSync6(tmpPath)) {
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 message queue history. */
3534
- writeSnapshot(agents, history) {
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
- history
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 (!existsSync6(this._snapshotPath)) {
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
- return runMigrations(raw2);
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 message history into queue. */
3590
- hydrateHistory(queue) {
3445
+ /** Restore ring buffer state into message queue. */
3446
+ hydrateBuffers(queue) {
3591
3447
  const snapshot = this.loadSnapshot();
3592
- if (!snapshot || Object.keys(snapshot.history).length === 0) return 0;
3448
+ if (!snapshot || Object.keys(snapshot.buffers).length === 0) return 0;
3449
+ queue.restoreBufferSnapshots(snapshot.buffers);
3593
3450
  let totalEntries = 0;
3594
- for (const [agentId, entries] of Object.entries(snapshot.history)) {
3595
- for (const entry of entries) {
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 (existsSync6(this._markerPath)) {
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 existsSync6(this._markerPath);
3483
+ return existsSync5(this._markerPath);
3636
3484
  }
3637
3485
  // ------------------------------------------------------------------
3638
- // Snapshot loop & debounce
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, getHistory, intervalMs = DEFAULT_SNAPSHOT_INTERVAL_MS) {
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(), getHistory());
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
- * Schedule a debounced snapshot write.
3662
- * Multiple calls within DEBOUNCE_MS window are coalesced into one write.
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
- scheduleSnapshot(getAgents, getHistory) {
3665
- if (this._debounceTimer !== null) {
3666
- clearTimeout(this._debounceTimer);
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
- this._debounceTimer = setTimeout(() => {
3669
- this._debounceTimer = null;
3670
- try {
3671
- this.writeSnapshot(getAgents(), getHistory());
3672
- } catch (error) {
3673
- logger.warn("Persistence", "Debounced snapshot failed", error);
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
- }, DEBOUNCE_MS);
3667
+ }
3668
+ result.sort((a, b) => a.seq - b.seq);
3669
+ return result;
3676
3670
  }
3677
- /** Cancel pending debounced snapshot (called during shutdown). */
3678
- cancelDebounce() {
3679
- if (this._debounceTimer !== null) {
3680
- clearTimeout(this._debounceTimer);
3681
- this._debounceTimer = null;
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
- var HISTORY_CAP, MessageQueue;
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
- HISTORY_CAP = 200;
3717
+ init_ring_buffer();
3718
+ DEFAULT_CAPACITY2 = 200;
3694
3719
  MessageQueue = class {
3695
- _queues = /* @__PURE__ */ new Map();
3696
- _history = /* @__PURE__ */ new Map();
3697
- /** Enqueue a message for agentId. Returns the queue length after enqueue. */
3698
- enqueue(agentId, envelope) {
3699
- let q = this._queues.get(agentId);
3700
- if (!q) {
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
- q.push(envelope);
3705
- return q.length;
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 q = this._queues.get(agentId);
3710
- if (!q) return [];
3711
- return q.splice(0, Math.min(maxCount, q.length));
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._queues.get(agentId)?.length ?? 0;
3743
+ return this._buffers.get(agentId)?.unreadReceivedCount() ?? 0;
3716
3744
  }
3717
- /** Clean up queue when agent is removed. */
3745
+ /** Clean up buffer when agent is removed. */
3718
3746
  removeAgent(agentId) {
3719
- this._queues.delete(agentId);
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
- let h = this._history.get(agentId);
3724
- if (!h) {
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 h = this._history.get(agentId);
3743
- if (!h) return [];
3744
- return h.slice(-limit);
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._history.delete(agentId);
3762
+ this._buffers.delete(agentId);
3749
3763
  }
3750
- /** Return a shallow copy of the full history map (for persistence snapshots). */
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, entries] of this._history) {
3754
- copy[agentId] = [...entries];
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, PARTY_SERVER, DEGRADED, SUSPECT, DOWN, NOT_SERVER, MAYBE, MAYBE_MAX_RETRIES, BACKOFF_BASE, BACKOFF_CAP, FAILURE_SUSPECT, FAILURE_DOWN, PeerDiscovery;
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
- PARTY_SERVER = "PARTY_SERVER";
3782
- DEGRADED = "DEGRADED";
3783
- SUSPECT = "SUSPECT";
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 in a serving state. */
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 && (ps.status === PARTY_SERVER || ps.status === DEGRADED || ps.status === SUSPECT);
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 for dashboard consumption. */
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, maybeRetries: 0 });
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 === NOT_SERVER) {
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.evictDownAgents();
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 === null) {
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, false);
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 !== PARTY_SERVER) {
3938
- this.transition(ps, PARTY_SERVER);
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, timeout) {
3943
- if (ps.status === UNKNOWN || ps.status === MAYBE) {
3944
- if (timeout) {
3945
- ps.maybeRetries++;
3946
- if (ps.maybeRetries >= MAYBE_MAX_RETRIES) {
3947
- this.transition(ps, NOT_SERVER);
3948
- } else {
3949
- this.transition(ps, MAYBE);
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
- evictDownAgents() {
4027
- const downPeers = /* @__PURE__ */ new Set();
4016
+ evictDeadAgents() {
4017
+ const deadPeers = /* @__PURE__ */ new Set();
4028
4018
  for (const [ip, ps] of this._peers) {
4029
- if (ps.status === DOWN) downPeers.add(ip);
4019
+ if (ps.status === DEAD) deadPeers.add(ip);
4030
4020
  }
4031
- if (!downPeers.size) return;
4021
+ if (!deadPeers.size) return;
4032
4022
  for (const [aid, entry] of this._remoteAgents) {
4033
- if (downPeers.has(entry.sourcePeerIp)) {
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 stale = [];
4094
+ const toRemove = [];
4095
4095
  for (const [aid, info] of this._agents) {
4096
4096
  if (now - info.last_heartbeat > timeout) {
4097
- stale.push(aid);
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 stale) {
4106
+ for (const aid of toRemove) {
4101
4107
  this._agents.delete(aid);
4108
+ this._staleCounts.delete(aid);
4102
4109
  }
4103
- return stale;
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, scheduleSnapshot: scheduleSnapshot3 } = await Promise.resolve().then(() => (init_state(), state_exports));
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
- scheduleSnapshot3();
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, scheduleSnapshot: scheduleSnapshot3 } = await Promise.resolve().then(() => (init_state(), state_exports));
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) scheduleSnapshot3();
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
- const info = registry2.heartbeat(req.agent_id);
4232
- logger.info("Agent", `Heartbeat from ${req.agent_id}`);
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, scheduleSnapshot: scheduleSnapshot3 } = await Promise.resolve().then(() => (init_state(), state_exports));
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
- if (registry2.get(recipient)) {
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
- messageQueue2.logToHistory(recipient, "received", stamped);
4254
- scheduleSnapshot3();
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/dashboard-html.ts
4362
- var DASHBOARD_HTML;
4363
- var init_dashboard_html = __esm({
4364
- "src/server/dashboard-html.ts"() {
4365
- "use strict";
4366
- DASHBOARD_HTML = `<!DOCTYPE html>
4367
- <html lang="zh-CN">
4368
- <head>
4369
- <meta charset="UTF-8">
4370
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
4371
- <title>OPEN PARTY // Dashboard</title>
4372
- <style>
4373
- *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
4374
- :root{
4375
- --bg:#0a0a0f;--card:#12121a;--border:#1e1e2e;--border-bright:#2a2a3e;
4376
- --text:#e0e0e8;--muted:#6a6a8a;
4377
- --cyan:#00fff0;--magenta:#ff00ff;--green:#00ff88;--red:#ff3366;--yellow:#ffaa00;--orange:#ff8800;
4378
- --font-mono:'JetBrains Mono','Fira Code','Cascadia Code','Consolas','Courier New',monospace;
4379
- --font-sans:'Inter','Segoe UI',system-ui,-apple-system,sans-serif;
4380
- }
4381
- html{font-size:14px}
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
- .peer-table td{padding:8px 10px;border-bottom:1px solid var(--border);font-family:var(--font-mono);font-size:0.8rem}
4492
- .peer-table tr:last-child td{border-bottom:none}
4493
- .peer-table tr:hover td{background:rgba(255,255,255,0.02)}
4494
- .badge{
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
- .badge-party{background:rgba(0,255,136,0.15);color:var(--green)}
4499
- .badge-degraded{background:rgba(255,170,0,0.15);color:var(--yellow)}
4500
- .badge-suspect{background:rgba(255,136,0,0.15);color:var(--orange)}
4501
- .badge-down{background:rgba(255,51,102,0.15);color:var(--red)}
4502
- .badge-unknown{background:rgba(106,106,138,0.15);color:var(--muted)}
4503
- .badge-not_server{background:rgba(106,106,138,0.1);color:var(--muted)}
4504
- .badge-maybe{background:rgba(0,255,240,0.1);color:var(--cyan)}
4505
- .empty-state{text-align:center;color:var(--muted);padding:40px 20px;font-family:var(--font-mono);font-size:0.85rem}
4506
-
4507
- /* ===== Agent & Message Grid ===== */
4508
- .bottom-grid{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-bottom:24px}
4509
- @media(max-width:1024px){.bottom-grid{grid-template-columns:1fr}}
4510
-
4511
- /* Agent cards */
4512
- .agent-list{display:flex;flex-direction:column;gap:8px;max-height:400px;overflow-y:auto}
4513
- .agent-card{
4514
- display:flex;align-items:center;gap:12px;padding:10px 14px;
4515
- border:1px solid var(--border);border-radius:6px;transition:border-color 0.3s;
4516
- }
4517
- .agent-card:hover{border-color:var(--border-bright)}
4518
- .agent-card.local{border-left:3px solid var(--cyan)}
4519
- .agent-card.remote{border-left:3px solid var(--green)}
4520
- .agent-icon{
4521
- width:36px;height:36px;border-radius:6px;display:flex;align-items:center;
4522
- justify-content:center;font-family:var(--font-mono);font-size:0.9rem;font-weight:700;
4523
- flex-shrink:0;
4524
- }
4525
- .agent-card.local .agent-icon{background:rgba(0,255,240,0.1);color:var(--cyan)}
4526
- .agent-card.remote .agent-icon{background:rgba(0,255,136,0.1);color:var(--green)}
4527
- .agent-info{flex:1;min-width:0}
4528
- .agent-name{font-weight:600;font-size:0.9rem;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
4529
- .agent-id{font-family:var(--font-mono);font-size:0.7rem;color:var(--muted);margin-top:2px}
4530
- .agent-meta{display:flex;gap:6px;flex-shrink:0;align-items:center}
4531
- .agent-tag{
4532
- font-family:var(--font-mono);font-size:0.65rem;padding:2px 6px;
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' ? '&#8592;' : '&#8594;';
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, '&quot;') + '">';
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.hydrateHistory(messageQueue);
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.getHistorySnapshot()
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
- init_tailscale();
5750
- init_tailscale_installer();
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 createRl() {
6123
- return createInterface({ input: process.stdin, output: process.stdout });
6124
- }
6125
- function closeRl(rl) {
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 multiSelect(options, opts) {
6197
- if (options.length === 0) return [];
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) + 1;
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
- process.stdout.write(`${prefix}${check} ${label}
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
- } else {
6469
- console.log(`${red("\u274C")} Failed to install for ${agent.name}: ${result.error}`);
6470
- }
6471
- }
6472
- }
6473
- async function setupCommand() {
6474
- console.log(bold(cyan("\n\u{1F680} Open Party Setup Wizard\n")));
6475
- const rl = createRl();
6476
- await stepTailscale();
6477
- await stepAgentPlugin();
6478
- console.log(`
6479
- ${bold(cyan("\u{1F680} Starting Party Server..."))}`);
6480
- const { spawn: spawn4 } = await import("child_process");
6481
- const { resolve: resolve4, dirname: dirname6 } = await import("path");
6482
- const { fileURLToPath: fileURLToPath3 } = await import("url");
6483
- const __dirname2 = dirname6(fileURLToPath3(import.meta.url));
6484
- const serverScript = resolve4(__dirname2, "..", "party-server.js");
6485
- const serverProc = spawn4(process.execPath, [serverScript], {
6486
- detached: true,
6487
- stdio: "ignore",
6488
- windowsHide: true
6489
- });
6490
- serverProc.unref();
6491
- await new Promise((r) => setTimeout(r, 2e3));
6492
- console.log(`
6493
- ${bold(green("\u{1F389} Setup complete!"))}`);
6494
- console.log(` Dashboard: http://127.0.0.1:8000/dashboard`);
6495
- console.log(" Other agents can join with: npx @feynmanzhang/open-party setup\n");
6496
- closeRl(rl);
6497
- }
6498
-
6499
- // src/cli/login.ts
6500
- init_tailscale();
6501
- async function loginCommand() {
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 as spawn3, execSync as execSync3 } from "child_process";
6560
- import { existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync2, openSync } from "fs";
6561
- import { join as join4, dirname as dirname2, resolve as resolve2 } from "path";
6562
- import { homedir as homedir3 } from "os";
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 = dirname2(fileURLToPath(import.meta.url));
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 join4(pluginData, "server.pid");
6568
- return join4(homedir3(), ".open-party", "server.pid");
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 join4(pluginData, "server.log");
6573
- return join4(homedir3(), ".open-party", "server.log");
4702
+ if (pluginData) return join(pluginData, "server.log");
4703
+ return join(homedir(), ".open-party", "server.log");
6574
4704
  }
6575
4705
  function serverScriptPath() {
6576
- return resolve2(__dirname, "..", "party-server.js");
4706
+ return resolve(__dirname, "..", "party-server.js");
6577
4707
  }
6578
4708
  function readPid() {
6579
4709
  const path = pidFilePath();
6580
- if (!existsSync4(path)) return null;
4710
+ if (!existsSync(path)) return null;
6581
4711
  try {
6582
- return parseInt(readFileSync2(path, "utf-8").trim(), 10);
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 = dirname2(path);
6590
- if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
6591
- writeFileSync2(path, String(pid));
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 = execSync3(`tasklist /FI "PID eq ${pid}" /NH`, {
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 (!existsSync4(script)) {
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
- mkdirSync2(dirname2(logPath), { recursive: true });
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 = spawn3(process.execPath, [script], {
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
- function killServer(pid) {
6684
- try {
6685
- if (process.platform === "win32") {
6686
- execSync3(`taskkill /F /T /PID ${pid}`, { stdio: "ignore", windowsHide: true });
6687
- } else {
6688
- process.kill(pid, "SIGTERM");
6689
- }
6690
- } catch (error) {
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
- function findPidByPort(port) {
6695
- try {
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
- function parseStartArgs(args2) {
6715
- let daemon = false;
6716
- let port = null;
6717
- for (let i = 0; i < args2.length; i++) {
6718
- if (args2[i] === "-d" || args2[i] === "--daemon") {
6719
- daemon = true;
6720
- } else if (args2[i] === "-p" || args2[i] === "--port") {
6721
- const val = args2[++i];
6722
- if (val) port = parseInt(val, 10);
6723
- } else if (args2[i].startsWith("--port=")) {
6724
- port = parseInt(args2[i].split("=")[1], 10);
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/cli/agents.ts
6912
- async function agentsCommand() {
6913
- const port = resolvePort();
6914
- if (!await isServerHealthy(port)) {
6915
- console.log("Party Server is not running.");
6916
- console.log(" Use 'open-party start' to start it.");
6917
- return;
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
- const overview = await getServerOverview(port);
6920
- if (!overview) {
6921
- console.log("Failed to get server overview.");
6922
- return;
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
- const agents = overview.agents;
6925
- if (!agents) {
6926
- console.log("No agent data available.");
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
- const localAgents = agents.local_agents ?? [];
6930
- const remoteAgents = agents.remote_agents ?? [];
6931
- const localCount = agents.local_count ?? localAgents.length;
6932
- const remoteCount = agents.remote_count ?? remoteAgents.length;
6933
- if (localCount === 0) {
6934
- console.log("Local agents: (none)");
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
- if (remoteCount > 0) {
6945
- console.log(`
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
- for (const agent of remoteAgents) {
6948
- const id = agent.agent_id ?? "?";
6949
- const name = agent.display_name ?? id;
6950
- const via = agent.source_peer_ip ?? "?";
6951
- const ago = formatTimeAgo(agent.last_heartbeat);
6952
- console.log(` ${id.padEnd(20)} ${name.padEnd(16)} (via ${via}) ${ago}`);
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
- if (localCount === 0 && remoteCount === 0) {
6956
- console.log("\nNo agents connected yet.");
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 port = resolvePort();
6971
- if (!await isServerHealthy(port)) {
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
- const overview = await getServerOverview(port);
6977
- if (!overview) {
6978
- console.log("Failed to get server overview.");
6979
- return;
6980
- }
6981
- const peers = overview.peers;
6982
- if (!peers) {
6983
- console.log("No peer data available.");
6984
- return;
6985
- }
6986
- const details = peers.details ?? [];
6987
- const remoteAgents = overview.agents?.remote_agents ?? [];
6988
- const peerAgentCounts = /* @__PURE__ */ new Map();
6989
- for (const agent of remoteAgents) {
6990
- const ip = agent.source_peer_ip;
6991
- peerAgentCounts.set(ip, (peerAgentCounts.get(ip) ?? 0) + 1);
6992
- }
6993
- const total = peers.total ?? details.length;
6994
- if (details.length === 0) {
6995
- console.log("No peers discovered yet.");
6996
- return;
6997
- }
6998
- console.log(`Peers (${total}):
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
- for (const peer of details) {
7001
- const agentCount = peerAgentCounts.get(peer.ip);
7002
- const agentStr = agentCount != null ? String(agentCount) : "\u2014";
7003
- const statusStr = formatStatus(peer.status);
7004
- console.log(` ${peer.ip.padEnd(18)} ${statusStr.padEnd(16)} ${agentStr} agents`);
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
- PARTY_SERVER: "Online",
7010
- DEGRADED: "Degraded",
7011
- SUSPECT: "Suspect",
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/install.ts
7021
- init_tailscale();
7022
- init_tailscale_installer();
7023
- async function installCommand() {
7024
- const status = getTailscaleInstallationStatus();
7025
- if (status.state === "connected" || status.state === "not_connected") {
7026
- console.log(green("\u2705 Tailscale is already installed!"), `Binary: ${status.binary}`);
7027
- if (status.state === "connected") {
7028
- console.log(` IP: ${status.tailscale_ip} Hostname: ${status.hostname}`);
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 platform = process.platform;
7033
- const info = getInstallInstructions(platform);
7034
- console.log(bold(cyan("Installing Tailscale...\n")));
7035
- console.log(` Platform: ${info.os}`);
7036
- const result = await installTailscale(platform);
7037
- if (result.success) {
7038
- console.log(green("\n\u2705 Tailscale installed successfully!"));
7039
- console.log(" Run: open-party login");
7040
- } else {
7041
- console.log(red("\n\u274C Installation failed:"), result.output);
7042
- console.log(" Install manually: https://tailscale.com/download");
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/index.ts
7047
- function showHelp() {
7048
- console.log(`Usage: open-party <command> [options]
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
- Commands:
7051
- start Start the Party Server (default when no command given)
7052
- stop Stop the Party Server
7053
- status Show server status
7054
- setup Interactive setup wizard (Tailscale + agent plugins)
7055
- login Login to Tailscale network
7056
- logout Log out of Tailscale network
7057
- install Install Tailscale
7058
- agents List connected agents
7059
- peers List discovered peer nodes
7060
- help Show this help message
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
- Options for 'start':
7063
- -d, --daemon Run in background (daemon mode)
7064
- -p, --port <port> Override port (default: 8000, env: PARTY_PORT)
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
- Global options:
7067
- -v, --version Show version number
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
- Examples:
7070
- open-party Start server in foreground
7071
- open-party start Start server in foreground
7072
- open-party start -d Start server in background
7073
- open-party start -d -p 9000 Start server in background on port 9000
7074
- open-party stop Stop the server
7075
- open-party status Check if the server is running
7076
- open-party login Login to Tailscale
7077
- open-party logout Log out of Tailscale
7078
- open-party install Install Tailscale
7079
- open-party agents List connected agents
7080
- open-party peers List discovered peer nodes`);
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
- await agentsCommand();
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}