@feynmanzhang/open-party 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -138,11 +138,11 @@ function execWithSudoFallback(cmd, timeout = 15e3) {
138
138
  function joinTailnet(authKey, timeout = 3e4) {
139
139
  const binary = getTailscaleBinary();
140
140
  try {
141
- const output = execWithSudoFallback(
141
+ const output2 = execWithSudoFallback(
142
142
  [binary, "up", "--authkey", authKey],
143
143
  timeout
144
144
  );
145
- return { success: true, output: output.trim() };
145
+ return { success: true, output: output2.trim() };
146
146
  } catch (e) {
147
147
  const err = e;
148
148
  return { success: false, output: (err.stderr ?? err.message ?? "").trim() };
@@ -168,11 +168,12 @@ function getTailscaleInstallationStatus() {
168
168
  };
169
169
  }
170
170
  return { state: "not_connected", binary };
171
- } catch (e) {
171
+ } catch (error) {
172
+ console.error("[Tailscale] Failed to get installation status:", error);
172
173
  return {
173
174
  state: "not_connected",
174
175
  binary,
175
- error: e.message
176
+ error: error instanceof Error ? error.message : String(error)
176
177
  };
177
178
  }
178
179
  }
@@ -182,9 +183,9 @@ function resetTailscaleBinaryCache() {
182
183
  function logoutTailscale(timeout = 15e3) {
183
184
  const binary = getTailscaleBinary();
184
185
  try {
185
- const output = runExec([binary, "logout"], timeout);
186
+ const output2 = runExec([binary, "logout"], timeout);
186
187
  resetTailscaleBinaryCache();
187
- return { success: true, output: output.trim() };
188
+ return { success: true, output: output2.trim() };
188
189
  } catch (e) {
189
190
  const err = e;
190
191
  return { success: false, output: (err.stderr ?? err.message ?? "").trim() };
@@ -3342,14 +3343,348 @@ var init_config = __esm({
3342
3343
  "src/server/config.ts"() {
3343
3344
  "use strict";
3344
3345
  PARTY_PORT = parseInt(process.env.PARTY_PORT || "8000", 10);
3345
- HEARTBEAT_TIMEOUT = parseFloat(process.env.HEARTBEAT_TIMEOUT || "30");
3346
- CLEANUP_INTERVAL = parseFloat(process.env.CLEANUP_INTERVAL || "10");
3346
+ HEARTBEAT_TIMEOUT = parseFloat(process.env.HEARTBEAT_TIMEOUT || "60");
3347
+ CLEANUP_INTERVAL = parseFloat(process.env.CLEANUP_INTERVAL || "60");
3347
3348
  DISCOVERY_INTERVAL = parseFloat(process.env.DISCOVERY_INTERVAL || "20");
3348
3349
  REMOTE_STALE_FACTOR = parseInt(process.env.REMOTE_STALE_FACTOR || "3", 10);
3349
3350
  PROBE_TIMEOUT = parseFloat(process.env.PROBE_TIMEOUT || "5");
3350
3351
  }
3351
3352
  });
3352
3353
 
3354
+ // src/server/logger.ts
3355
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, appendFileSync, readdirSync as readdirSync2, unlinkSync as unlinkSync2, statSync } from "fs";
3356
+ import { join as join5 } from "path";
3357
+ import { homedir as homedir4 } from "os";
3358
+ function getEffectiveLevel() {
3359
+ const env = (process.env.LOG_LEVEL || "info").toLowerCase().trim();
3360
+ if (env in LEVEL_ORDER) return env;
3361
+ return "info";
3362
+ }
3363
+ function initLogFile() {
3364
+ if (!existsSync5(LOG_DIR)) {
3365
+ mkdirSync3(LOG_DIR, { recursive: true });
3366
+ return;
3367
+ }
3368
+ try {
3369
+ const now = Date.now();
3370
+ const cutoff = now - LOG_RETENTION_DAYS * 24 * 60 * 60 * 1e3;
3371
+ const files = readdirSync2(LOG_DIR);
3372
+ for (const f of files) {
3373
+ if (!f.endsWith("-open-party.log")) continue;
3374
+ try {
3375
+ const stat = statSync(join5(LOG_DIR, f));
3376
+ if (stat.mtimeMs < cutoff) {
3377
+ unlinkSync2(join5(LOG_DIR, f));
3378
+ }
3379
+ } catch {
3380
+ }
3381
+ }
3382
+ } catch {
3383
+ }
3384
+ }
3385
+ function getLogFilePath() {
3386
+ const d = /* @__PURE__ */ new Date();
3387
+ const yy = String(d.getFullYear()).slice(2);
3388
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
3389
+ const dd = String(d.getDate()).padStart(2, "0");
3390
+ return join5(LOG_DIR, `${yy}-${mm}-${dd}-open-party.log`);
3391
+ }
3392
+ function shouldLog(level) {
3393
+ return LEVEL_ORDER[level] >= LEVEL_ORDER[effectiveLevel];
3394
+ }
3395
+ function extractError(err) {
3396
+ if (err instanceof Error) {
3397
+ const stack = err.stack ? `
3398
+ ${err.stack}` : "";
3399
+ return `${err.message}${stack}`;
3400
+ }
3401
+ return String(err);
3402
+ }
3403
+ function format(level, tag, message) {
3404
+ const now = /* @__PURE__ */ new Date();
3405
+ const levelStr = level.toUpperCase().padEnd(5);
3406
+ const yy = String(now.getFullYear()).slice(2);
3407
+ const mm = String(now.getMonth() + 1).padStart(2, "0");
3408
+ const dd = String(now.getDate()).padStart(2, "0");
3409
+ const hh = String(now.getHours()).padStart(2, "0");
3410
+ const min = String(now.getMinutes()).padStart(2, "0");
3411
+ const ss = String(now.getSeconds()).padStart(2, "0");
3412
+ const ts = `${yy}-${mm}-${dd} ${hh}:${min}:${ss}`;
3413
+ return `${ts} [${levelStr}] [${tag}] ${message}`;
3414
+ }
3415
+ function output(consoleFn, level, tag, message) {
3416
+ if (!shouldLog(level)) return;
3417
+ const line = format(level, tag, message);
3418
+ consoleFn(line);
3419
+ try {
3420
+ appendFileSync(getLogFilePath(), line + "\n", "utf-8");
3421
+ } catch {
3422
+ }
3423
+ }
3424
+ var LEVEL_ORDER, effectiveLevel, LOG_DIR, LOG_RETENTION_DAYS, logger;
3425
+ var init_logger = __esm({
3426
+ "src/server/logger.ts"() {
3427
+ "use strict";
3428
+ LEVEL_ORDER = {
3429
+ debug: 0,
3430
+ info: 1,
3431
+ warn: 2,
3432
+ error: 3
3433
+ };
3434
+ effectiveLevel = getEffectiveLevel();
3435
+ LOG_DIR = join5(homedir4(), ".open-party", "logs");
3436
+ LOG_RETENTION_DAYS = 7;
3437
+ initLogFile();
3438
+ logger = {
3439
+ info(tag, message, data) {
3440
+ output(console.log, "info", tag, data ? `${message} ${JSON.stringify(data)}` : message);
3441
+ },
3442
+ warn(tag, message, data) {
3443
+ output(console.warn, "warn", tag, data ? `${message} ${JSON.stringify(data)}` : message);
3444
+ },
3445
+ error(tag, message, err) {
3446
+ const detail = err ? `: ${extractError(err)}` : "";
3447
+ output(console.error, "error", tag, message + detail);
3448
+ },
3449
+ debug(tag, message, data) {
3450
+ output(console.debug, "debug", tag, data ? `${message} ${JSON.stringify(data)}` : message);
3451
+ }
3452
+ };
3453
+ }
3454
+ });
3455
+
3456
+ // 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";
3460
+ function dataDirPath() {
3461
+ 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;
3484
+ }
3485
+ function abortableSleep(ms, signal) {
3486
+ return new Promise((resolve4, reject) => {
3487
+ const timer = setTimeout(resolve4, ms);
3488
+ signal?.addEventListener(
3489
+ "abort",
3490
+ () => {
3491
+ clearTimeout(timer);
3492
+ reject(new DOMException("Aborted", "AbortError"));
3493
+ },
3494
+ { once: true }
3495
+ );
3496
+ });
3497
+ }
3498
+ var CURRENT_SCHEMA_VERSION, SNAPSHOT_FILE, SHUTDOWN_MARKER_FILE, DEFAULT_SNAPSHOT_INTERVAL_MS, DEBOUNCE_MS, MIGRATORS, SnapshotManager;
3499
+ var init_persistence = __esm({
3500
+ "src/server/persistence.ts"() {
3501
+ "use strict";
3502
+ init_logger();
3503
+ CURRENT_SCHEMA_VERSION = 1;
3504
+ SNAPSHOT_FILE = "snapshot.json";
3505
+ SHUTDOWN_MARKER_FILE = "shutdown-marker.json";
3506
+ DEFAULT_SNAPSHOT_INTERVAL_MS = 6e4;
3507
+ DEBOUNCE_MS = 5e3;
3508
+ MIGRATORS = [
3509
+ // Future: { version: 2, migrate(snapshot) { ... } },
3510
+ ];
3511
+ SnapshotManager = class {
3512
+ _dir;
3513
+ _snapshotPath;
3514
+ _markerPath;
3515
+ _debounceTimer = null;
3516
+ constructor(dataDir) {
3517
+ this._dir = dataDir ?? dataDirPath();
3518
+ this._snapshotPath = join6(this._dir, SNAPSHOT_FILE);
3519
+ this._markerPath = join6(this._dir, SHUTDOWN_MARKER_FILE);
3520
+ mkdirSync4(this._dir, { recursive: true });
3521
+ const tmpPath = this._snapshotPath + ".tmp";
3522
+ try {
3523
+ if (existsSync6(tmpPath)) {
3524
+ unlinkSync3(tmpPath);
3525
+ }
3526
+ } catch (error) {
3527
+ logger.warn("Persistence", "Failed to clean up stale tmp file", error);
3528
+ }
3529
+ }
3530
+ // ------------------------------------------------------------------
3531
+ // Write / Load
3532
+ // ------------------------------------------------------------------
3533
+ /** Atomically write a snapshot of registry agents and message queue history. */
3534
+ writeSnapshot(agents, history) {
3535
+ const snapshot = {
3536
+ version: CURRENT_SCHEMA_VERSION,
3537
+ saved_at: Date.now(),
3538
+ agents,
3539
+ history
3540
+ };
3541
+ const serialized = JSON.stringify(snapshot, null, 2);
3542
+ const tmpPath = this._snapshotPath + ".tmp";
3543
+ try {
3544
+ writeFileSync3(tmpPath, serialized, "utf-8");
3545
+ renameSync(tmpPath, this._snapshotPath);
3546
+ } catch (error) {
3547
+ logger.error("Persistence", "Failed to write snapshot", error);
3548
+ try {
3549
+ unlinkSync3(tmpPath);
3550
+ } catch {
3551
+ }
3552
+ throw error;
3553
+ }
3554
+ }
3555
+ /** Load and validate snapshot. Returns null if file missing or corrupt. */
3556
+ loadSnapshot() {
3557
+ if (!existsSync6(this._snapshotPath)) {
3558
+ return null;
3559
+ }
3560
+ try {
3561
+ const raw2 = JSON.parse(readFileSync3(this._snapshotPath, "utf-8"));
3562
+ return runMigrations(raw2);
3563
+ } catch (error) {
3564
+ logger.warn("Persistence", "Failed to load snapshot (starting fresh)", error);
3565
+ return null;
3566
+ }
3567
+ }
3568
+ // ------------------------------------------------------------------
3569
+ // Hydration (restore in-memory state from snapshot)
3570
+ // ------------------------------------------------------------------
3571
+ /** Restore agents into registry. Overwrites host_ip with current selfIp. */
3572
+ hydrateAgents(registry2, selfIp) {
3573
+ const snapshot = this.loadSnapshot();
3574
+ if (!snapshot || snapshot.agents.length === 0) return 0;
3575
+ const now = Date.now() / 1e3;
3576
+ let count = 0;
3577
+ for (const agent of snapshot.agents) {
3578
+ agent.last_heartbeat = now;
3579
+ const info = registry2.register({
3580
+ agent_id: agent.agent_id,
3581
+ display_name: agent.display_name,
3582
+ metadata: agent.metadata ?? {}
3583
+ });
3584
+ info.host_ip = selfIp;
3585
+ count++;
3586
+ }
3587
+ return count;
3588
+ }
3589
+ /** Restore message history into queue. */
3590
+ hydrateHistory(queue) {
3591
+ const snapshot = this.loadSnapshot();
3592
+ if (!snapshot || Object.keys(snapshot.history).length === 0) return 0;
3593
+ 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
+ }
3605
+ }
3606
+ return totalEntries;
3607
+ }
3608
+ // ------------------------------------------------------------------
3609
+ // Shutdown marker
3610
+ // ------------------------------------------------------------------
3611
+ /** Write shutdown marker to detect interrupted shutdown on next start. */
3612
+ writeShutdownMarker() {
3613
+ try {
3614
+ writeFileSync3(
3615
+ this._markerPath,
3616
+ JSON.stringify({ started_at: Date.now() }),
3617
+ "utf-8"
3618
+ );
3619
+ } catch (error) {
3620
+ logger.warn("Persistence", "Failed to write shutdown marker", error);
3621
+ }
3622
+ }
3623
+ /** Remove shutdown marker — called after successful shutdown. */
3624
+ removeShutdownMarker() {
3625
+ try {
3626
+ if (existsSync6(this._markerPath)) {
3627
+ unlinkSync3(this._markerPath);
3628
+ }
3629
+ } catch (error) {
3630
+ logger.warn("Persistence", "Failed to remove shutdown marker", error);
3631
+ }
3632
+ }
3633
+ /** Check if a shutdown marker exists (indicates previous shutdown was interrupted). */
3634
+ hasShutdownMarker() {
3635
+ return existsSync6(this._markerPath);
3636
+ }
3637
+ // ------------------------------------------------------------------
3638
+ // Snapshot loop & debounce
3639
+ // ------------------------------------------------------------------
3640
+ /**
3641
+ * Start periodic snapshot background loop.
3642
+ * Writes snapshot every `intervalMs` milliseconds until signal is aborted.
3643
+ */
3644
+ async startSnapshotLoop(signal, getAgents, getHistory, intervalMs = DEFAULT_SNAPSHOT_INTERVAL_MS) {
3645
+ while (!signal.aborted) {
3646
+ try {
3647
+ await abortableSleep(intervalMs, signal);
3648
+ } catch (e) {
3649
+ if (signal.aborted) break;
3650
+ throw e;
3651
+ }
3652
+ if (signal.aborted) break;
3653
+ try {
3654
+ this.writeSnapshot(getAgents(), getHistory());
3655
+ } catch (error) {
3656
+ logger.warn("Persistence", "Periodic snapshot failed", error);
3657
+ }
3658
+ }
3659
+ }
3660
+ /**
3661
+ * Schedule a debounced snapshot write.
3662
+ * Multiple calls within DEBOUNCE_MS window are coalesced into one write.
3663
+ */
3664
+ scheduleSnapshot(getAgents, getHistory) {
3665
+ if (this._debounceTimer !== null) {
3666
+ clearTimeout(this._debounceTimer);
3667
+ }
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);
3674
+ }
3675
+ }, DEBOUNCE_MS);
3676
+ }
3677
+ /** Cancel pending debounced snapshot (called during shutdown). */
3678
+ cancelDebounce() {
3679
+ if (this._debounceTimer !== null) {
3680
+ clearTimeout(this._debounceTimer);
3681
+ this._debounceTimer = null;
3682
+ }
3683
+ }
3684
+ };
3685
+ }
3686
+ });
3687
+
3353
3688
  // src/server/message-queue.ts
3354
3689
  var HISTORY_CAP, MessageQueue;
3355
3690
  var init_message_queue = __esm({
@@ -3412,6 +3747,14 @@ var init_message_queue = __esm({
3412
3747
  removeAgentHistory(agentId) {
3413
3748
  this._history.delete(agentId);
3414
3749
  }
3750
+ /** Return a shallow copy of the full history map (for persistence snapshots). */
3751
+ getHistorySnapshot() {
3752
+ const copy = {};
3753
+ for (const [agentId, entries] of this._history) {
3754
+ copy[agentId] = [...entries];
3755
+ }
3756
+ return copy;
3757
+ }
3415
3758
  };
3416
3759
  }
3417
3760
  });
@@ -3426,15 +3769,14 @@ function classifyFetchError(error) {
3426
3769
  if (error instanceof DOMException && error.name === "AbortError") return null;
3427
3770
  return null;
3428
3771
  }
3429
- function sleep2(ms) {
3430
- return new Promise((resolve4) => setTimeout(resolve4, ms));
3431
- }
3432
3772
  var UNKNOWN, PARTY_SERVER, DEGRADED, SUSPECT, DOWN, NOT_SERVER, MAYBE, MAYBE_MAX_RETRIES, BACKOFF_BASE, BACKOFF_CAP, FAILURE_SUSPECT, FAILURE_DOWN, PeerDiscovery;
3433
3773
  var init_peer_discovery = __esm({
3434
3774
  "src/server/peer-discovery.ts"() {
3435
3775
  "use strict";
3436
3776
  init_config();
3437
3777
  init_tailscale();
3778
+ init_persistence();
3779
+ init_logger();
3438
3780
  UNKNOWN = "UNKNOWN";
3439
3781
  PARTY_SERVER = "PARTY_SERVER";
3440
3782
  DEGRADED = "DEGRADED";
@@ -3496,14 +3838,14 @@ var init_peer_discovery = __esm({
3496
3838
  // ------------------------------------------------------------------
3497
3839
  // Main discovery loop
3498
3840
  // ------------------------------------------------------------------
3499
- async runLoop() {
3500
- while (true) {
3841
+ async runLoop(signal) {
3842
+ while (!signal?.aborted) {
3501
3843
  try {
3502
3844
  await this.discoveryCycle();
3503
3845
  } catch (e) {
3504
- console.error("[Discovery] Cycle failed:", e);
3846
+ logger.error("Discovery", "Cycle failed", e);
3505
3847
  }
3506
- await sleep2(DISCOVERY_INTERVAL * 1e3);
3848
+ await abortableSleep(DISCOVERY_INTERVAL * 1e3, signal);
3507
3849
  }
3508
3850
  }
3509
3851
  async discoveryCycle() {
@@ -3536,8 +3878,8 @@ var init_peer_discovery = __esm({
3536
3878
  let status;
3537
3879
  try {
3538
3880
  status = readTailscaleStatus();
3539
- } catch {
3540
- console.warn("[Discovery] Failed to read Tailscale status");
3881
+ } catch (error) {
3882
+ logger.warn("Discovery", "Failed to read Tailscale status", error);
3541
3883
  return [];
3542
3884
  }
3543
3885
  const peersRaw = status.Peer;
@@ -3562,7 +3904,7 @@ var init_peer_discovery = __esm({
3562
3904
  // Peer probing
3563
3905
  // ------------------------------------------------------------------
3564
3906
  async probePeer(ps) {
3565
- ps.lastProbeAt = performance.now() / 1e3;
3907
+ ps.lastProbeAt = Date.now() / 1e3;
3566
3908
  const healthy = await this.checkHealth(ps.ip);
3567
3909
  if (healthy === null) {
3568
3910
  this.handleProbeFailure(ps, true);
@@ -3631,7 +3973,7 @@ var init_peer_discovery = __esm({
3631
3973
  const old = ps.status;
3632
3974
  ps.status = newStatus;
3633
3975
  if (old !== newStatus) {
3634
- console.log(`[Discovery] Peer ${ps.ip}: ${old} -> ${newStatus}`);
3976
+ logger.info("Discovery", `Peer ${ps.ip}: ${old} -> ${newStatus}`);
3635
3977
  }
3636
3978
  if (newStatus === NOT_SERVER) {
3637
3979
  const retries = ps.maybeRetries > 0 ? ps.maybeRetries : 1;
@@ -3674,8 +4016,8 @@ var init_peer_discovery = __esm({
3674
4016
  this._remoteAgents.delete(aid);
3675
4017
  }
3676
4018
  }
3677
- } catch {
3678
- console.warn(`[Discovery] Failed to sync agents from ${peerIp}`);
4019
+ } catch (error) {
4020
+ logger.warn("Discovery", `Failed to sync agents from ${peerIp}`, error);
3679
4021
  }
3680
4022
  }
3681
4023
  // ------------------------------------------------------------------
@@ -3731,7 +4073,8 @@ var init_registry = __esm({
3731
4073
  return info;
3732
4074
  }
3733
4075
  remove(agentId) {
3734
- return this._agents.delete(agentId);
4076
+ const existed = this._agents.delete(agentId);
4077
+ return existed;
3735
4078
  }
3736
4079
  heartbeat(agentId) {
3737
4080
  const info = this._agents.get(agentId);
@@ -3769,9 +4112,14 @@ __export(state_exports, {
3769
4112
  STARTED_AT: () => STARTED_AT,
3770
4113
  discovery: () => discovery,
3771
4114
  getSelfIp: () => getSelfIp,
4115
+ getSnapshotManager: () => getSnapshotManager,
4116
+ initSnapshotManager: () => initSnapshotManager,
4117
+ lifecycleController: () => lifecycleController,
3772
4118
  messageQueue: () => messageQueue,
3773
4119
  refreshSelfIp: () => refreshSelfIp,
3774
- registry: () => registry
4120
+ registry: () => registry,
4121
+ scheduleSnapshot: () => scheduleSnapshot,
4122
+ snapshotManager: () => snapshotManager
3775
4123
  });
3776
4124
  function resolveSelfIp() {
3777
4125
  try {
@@ -3791,7 +4139,19 @@ function refreshSelfIp() {
3791
4139
  _selfIp = resolveSelfIp();
3792
4140
  return _selfIp;
3793
4141
  }
3794
- var _selfIp, STARTED_AT, registry, messageQueue, discovery;
4142
+ function scheduleSnapshot() {
4143
+ snapshotManager?.scheduleSnapshot(
4144
+ () => registry.listAll(),
4145
+ () => messageQueue.getHistorySnapshot()
4146
+ );
4147
+ }
4148
+ function initSnapshotManager(mgr) {
4149
+ snapshotManager = mgr;
4150
+ }
4151
+ function getSnapshotManager() {
4152
+ return snapshotManager;
4153
+ }
4154
+ var _selfIp, STARTED_AT, registry, messageQueue, discovery, snapshotManager, lifecycleController;
3795
4155
  var init_state = __esm({
3796
4156
  "src/server/state.ts"() {
3797
4157
  "use strict";
@@ -3804,6 +4164,8 @@ var init_state = __esm({
3804
4164
  registry = new AgentRegistry(getSelfIp());
3805
4165
  messageQueue = new MessageQueue();
3806
4166
  discovery = new PeerDiscovery(getSelfIp());
4167
+ snapshotManager = null;
4168
+ lifecycleController = new AbortController();
3807
4169
  }
3808
4170
  });
3809
4171
 
@@ -3845,23 +4207,29 @@ var init_agent = __esm({
3845
4207
  init_dist();
3846
4208
  init_models();
3847
4209
  init_config();
4210
+ init_logger();
3848
4211
  agentRoutes = new Hono2();
3849
4212
  agentRoutes.post("/register", async (c) => {
3850
- const { registry: registry2 } = await Promise.resolve().then(() => (init_state(), state_exports));
4213
+ const { registry: registry2, scheduleSnapshot: scheduleSnapshot3 } = await Promise.resolve().then(() => (init_state(), state_exports));
3851
4214
  const req = await c.req.json();
3852
4215
  const info = registry2.register(req);
4216
+ scheduleSnapshot3();
4217
+ logger.info("Agent", `Registered: ${info.agent_id} (display: "${info.display_name ?? "N/A"}")`);
3853
4218
  return c.json(sanitizeAgentInfo(info));
3854
4219
  });
3855
4220
  agentRoutes.post("/remove", async (c) => {
3856
- const { registry: registry2 } = await Promise.resolve().then(() => (init_state(), state_exports));
4221
+ const { registry: registry2, scheduleSnapshot: scheduleSnapshot3 } = await Promise.resolve().then(() => (init_state(), state_exports));
3857
4222
  const req = await c.req.json();
3858
4223
  const removed = registry2.remove(req.agent_id);
4224
+ if (removed) scheduleSnapshot3();
4225
+ logger.info("Agent", removed ? `Removed: ${req.agent_id}` : `Remove failed: ${req.agent_id} (not found)`);
3859
4226
  return c.json({ status: removed ? "removed" : "not_found" });
3860
4227
  });
3861
4228
  agentRoutes.post("/heartbeat", async (c) => {
3862
4229
  const { registry: registry2 } = await Promise.resolve().then(() => (init_state(), state_exports));
3863
4230
  const req = await c.req.json();
3864
4231
  const info = registry2.heartbeat(req.agent_id);
4232
+ logger.info("Agent", `Heartbeat from ${req.agent_id}`);
3865
4233
  return c.json({ status: "ok", last_heartbeat: info.last_heartbeat });
3866
4234
  });
3867
4235
  agentRoutes.get("/list", async (c) => {
@@ -3872,7 +4240,7 @@ var init_agent = __esm({
3872
4240
  return c.json({ agents: sanitizeAgentList(allAgents), count: allAgents.length });
3873
4241
  });
3874
4242
  agentRoutes.post("/send", async (c) => {
3875
- const { registry: registry2, messageQueue: messageQueue2, discovery: discovery2 } = await Promise.resolve().then(() => (init_state(), state_exports));
4243
+ const { registry: registry2, messageQueue: messageQueue2, discovery: discovery2, scheduleSnapshot: scheduleSnapshot3 } = await Promise.resolve().then(() => (init_state(), state_exports));
3876
4244
  const envelope = await c.req.json();
3877
4245
  const recipient = envelope.recipient_id;
3878
4246
  if (!recipient) {
@@ -3883,6 +4251,8 @@ var init_agent = __esm({
3883
4251
  const count = messageQueue2.enqueue(recipient, stamped);
3884
4252
  messageQueue2.logToHistory(envelope.sender_id, "sent", stamped);
3885
4253
  messageQueue2.logToHistory(recipient, "received", stamped);
4254
+ scheduleSnapshot3();
4255
+ logger.info("Agent", `Send ${envelope.sender_id} -> ${recipient}: delivered_locally`);
3886
4256
  return c.json({ status: "delivered_locally", target: recipient });
3887
4257
  }
3888
4258
  const peerIp = discovery2.getPeerForAgent(recipient);
@@ -3891,12 +4261,16 @@ var init_agent = __esm({
3891
4261
  const result = await forwardToPeer(peerIp, envelope);
3892
4262
  if (result.status === "forwarded") {
3893
4263
  messageQueue2.logToHistory(envelope.sender_id, "sent", { ...envelope, timestamp: envelope.timestamp ?? Date.now() / 1e3 });
4264
+ scheduleSnapshot3();
4265
+ logger.info("Agent", `Send ${envelope.sender_id} -> ${recipient}: forwarded (peer ${peerIp})`);
3894
4266
  }
3895
4267
  return c.json(result);
3896
4268
  } else {
4269
+ logger.info("Agent", `Send ${envelope.sender_id} -> ${recipient}: peer_unreachable (peer ${peerIp})`);
3897
4270
  return c.json({ status: "peer_unreachable", target: peerIp });
3898
4271
  }
3899
4272
  }
4273
+ logger.info("Agent", `Send ${envelope.sender_id} -> ${recipient}: agent_not_found`);
3900
4274
  return c.json({ status: "agent_not_found", target: recipient });
3901
4275
  });
3902
4276
  agentRoutes.get("/messages/:agent_id", async (c) => {
@@ -3925,6 +4299,7 @@ var init_proxy = __esm({
3925
4299
  init_config();
3926
4300
  init_tailscale();
3927
4301
  init_state();
4302
+ init_logger();
3928
4303
  proxyRoutes = new Hono2();
3929
4304
  proxyRoutes.get("/health", async (c) => {
3930
4305
  let hostname = "127.0.0.1";
@@ -3949,13 +4324,13 @@ var init_proxy = __esm({
3949
4324
  if (envelope.recipient_id) {
3950
4325
  messageQueue.enqueue(envelope.recipient_id, stamped);
3951
4326
  messageQueue.logToHistory(envelope.recipient_id, "received", stamped);
3952
- console.log(`[\u6536\u5230\u6D88\u606F] ${envelope.sender_id} -> ${envelope.recipient_id}: ${envelope.content}`);
4327
+ logger.info("Proxy", `Received msg ${envelope.sender_id} -> ${envelope.recipient_id}`);
3953
4328
  } else {
3954
4329
  for (const agent of registry.listAll()) {
3955
4330
  messageQueue.enqueue(agent.agent_id, stamped);
3956
4331
  messageQueue.logToHistory(agent.agent_id, "received", stamped);
3957
4332
  }
3958
- console.log(`[\u5E7F\u64AD\u6D88\u606F] ${envelope.sender_id} -> all: ${envelope.content}`);
4333
+ logger.info("Proxy", `Broadcast from ${envelope.sender_id} to all agents`);
3959
4334
  }
3960
4335
  return c.json({ status: "received" });
3961
4336
  });
@@ -3973,10 +4348,10 @@ var init_proxy = __esm({
3973
4348
  headers: { "Content-Type": "application/json" },
3974
4349
  body: JSON.stringify(payload)
3975
4350
  });
3976
- console.log(`[\u53D1\u9001\u6210\u529F] \u76EE\u6807 ${targetIp}`);
4351
+ logger.info("Proxy", `Forwarded to ${targetIp}`);
3977
4352
  return c.json({ status: "forwarded", target: targetIp });
3978
4353
  } catch (e) {
3979
- console.log(`[\u53D1\u9001\u5931\u8D25] \u76EE\u6807 ${targetIp}, \u9519\u8BEF: ${e.message}`);
4354
+ logger.warn("Proxy", `Forward to ${targetIp} failed: ${e.message}`);
3980
4355
  return c.json({ status: "error", target: targetIp, error: e.message });
3981
4356
  }
3982
4357
  });
@@ -4636,6 +5011,16 @@ body::after{
4636
5011
  }).join('');
4637
5012
  }
4638
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
+
4639
5024
  function renderMessages(data) {
4640
5025
  const container = $('#msgFeed');
4641
5026
  const msgs = data.messages.recent || [];
@@ -4645,12 +5030,14 @@ body::after{
4645
5030
  return;
4646
5031
  }
4647
5032
 
5033
+ const names = buildNameMap(data);
5034
+
4648
5035
  container.innerHTML = msgs.map(function(m) {
4649
5036
  const dir = m.direction === 'received' ? 'received' : '';
4650
5037
  const arrow = m.direction === 'received' ? '&#8592;' : '&#8594;';
4651
5038
  const flow = m.direction === 'received'
4652
- ? m.sender_id + ' <span class="arrow">' + arrow + '</span> ' + (m.agent_id || '?')
4653
- : (m.agent_id || '?') + ' <span class="arrow">' + arrow + '</span> ' + (m.recipient_id || 'broadcast');
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';
4654
5041
  return '<div class="msg-item ' + dir + '">'
4655
5042
  + '<div class="msg-top">'
4656
5043
  + '<div class="msg-flow">' + flow + '</div>'
@@ -5064,6 +5451,7 @@ var init_dashboard = __esm({
5064
5451
  init_dashboard_html();
5065
5452
  init_tailscale();
5066
5453
  init_state();
5454
+ init_logger();
5067
5455
  dashboardRoutes = new Hono2();
5068
5456
  dashboardRoutes.get("/", (c) => {
5069
5457
  return c.html(DASHBOARD_HTML);
@@ -5089,10 +5477,14 @@ var init_dashboard = __esm({
5089
5477
  const localAgents = sanitizeAgentList(registry.listAll());
5090
5478
  const remoteEntries = discovery.getRemoteAgentEntries();
5091
5479
  const peerStates = discovery.getPeerStates();
5480
+ const seen = /* @__PURE__ */ new Set();
5092
5481
  const recentMessages = [];
5093
5482
  for (const agent of localAgents) {
5094
5483
  const history = messageQueue.getHistory(agent.agent_id, 5);
5095
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);
5096
5488
  recentMessages.push({ agent_id: agent.agent_id, ...entry });
5097
5489
  }
5098
5490
  }
@@ -5156,6 +5548,7 @@ var init_dashboard = __esm({
5156
5548
  return c.json({ success: false, output: "auth_key is required" }, 400);
5157
5549
  }
5158
5550
  const result = joinTailnet(authKey);
5551
+ logger.info("Dashboard", `Join network: ${result.success ? "success" : "failed"}`);
5159
5552
  return c.json(result, result.success ? 200 : 500);
5160
5553
  } catch (e) {
5161
5554
  return c.json({ success: false, output: e.message }, 500);
@@ -5164,6 +5557,7 @@ var init_dashboard = __esm({
5164
5557
  activeLogin = null;
5165
5558
  dashboardRoutes.post("/api/logout", async (c) => {
5166
5559
  const result = logoutTailscale();
5560
+ logger.info("Dashboard", `Logout: ${result.success ? "success" : "failed"}`);
5167
5561
  if (result.success) {
5168
5562
  resetTailscaleBinaryCache();
5169
5563
  refreshSelfIp();
@@ -5176,6 +5570,7 @@ var init_dashboard = __esm({
5176
5570
  }
5177
5571
  const { promise, process: process2 } = startInteractiveLogin();
5178
5572
  activeLogin = { process: process2 };
5573
+ logger.info("Dashboard", "Tailscale login initiated");
5179
5574
  const result = await promise;
5180
5575
  if (result.success && result.url) {
5181
5576
  activeLogin.url = result.url;
@@ -5187,6 +5582,7 @@ var init_dashboard = __esm({
5187
5582
  dashboardRoutes.post("/api/install-tailscale", async (c) => {
5188
5583
  const { installTailscale: installTailscale2 } = await Promise.resolve().then(() => (init_tailscale_installer(), tailscale_installer_exports));
5189
5584
  const result = await installTailscale2(process.platform);
5585
+ logger.info("Dashboard", `Install Tailscale: ${result.success ? "success" : "failed"}`);
5190
5586
  if (result.success) {
5191
5587
  resetTailscaleBinaryCache();
5192
5588
  }
@@ -5197,40 +5593,107 @@ var init_dashboard = __esm({
5197
5593
 
5198
5594
  // src/server/index.ts
5199
5595
  var server_exports = {};
5200
- import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2 } from "fs";
5201
- import { join as join5, dirname as dirname3 } from "path";
5202
- import { homedir as homedir4 } from "os";
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";
5203
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
+ }
5204
5611
  }
5205
5612
  function pidFilePath2() {
5206
5613
  const pluginData = process.env.CLAUDE_PLUGIN_DATA || "";
5207
- if (pluginData) return join5(pluginData, "server.pid");
5208
- return join5(homedir4(), ".open-party", "server.pid");
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
+ }
5209
5653
  }
5210
5654
  async function main() {
5211
5655
  const pidPath = pidFilePath2();
5212
- mkdirSync3(dirname3(pidPath), { recursive: true });
5213
- writeFileSync3(pidPath, String(process.pid));
5214
- console.log(`Starting Party Server on port ${PARTY_PORT} (Tailscale IP: ${getSelfIp()})`);
5656
+ mkdirSync5(dirname4(pidPath), { recursive: true });
5657
+ writeFileSync4(pidPath, String(process.pid));
5658
+ initSnapshotManager(new SnapshotManager());
5659
+ const sm = getSnapshotManager();
5660
+ let recoveredAgents = 0;
5661
+ let recoveredHistoryEntries = 0;
5662
+ if (sm.hasShutdownMarker()) {
5663
+ logger.info("Recovery", "Previous shutdown was interrupted (shutdown marker found).");
5664
+ }
5665
+ const savedSnapshot = sm.loadSnapshot();
5666
+ if (savedSnapshot) {
5667
+ recoveredAgents = sm.hydrateAgents(registry, getSelfIp());
5668
+ recoveredHistoryEntries = sm.hydrateHistory(messageQueue);
5669
+ if (recoveredAgents > 0 || recoveredHistoryEntries > 0) {
5670
+ logger.info(
5671
+ "Recovery",
5672
+ `Recovered ${recoveredAgents} agent(s), ${recoveredHistoryEntries} history entry/entries.`
5673
+ );
5674
+ }
5675
+ }
5676
+ logger.info("Server", `Starting Party Server on port ${PARTY_PORT} (Tailscale IP: ${getSelfIp()})`);
5215
5677
  process.on("SIGHUP", () => {
5216
5678
  });
5217
5679
  const server = serve({ fetch: app.fetch, port: PARTY_PORT });
5218
- const discoveryPromise = discovery.runLoop();
5680
+ const discoveryPromise = discovery.runLoop(lifecycleController.signal);
5219
5681
  const cleanupPromise = periodicCleanup();
5220
- const shutdown = () => {
5221
- console.log("\nShutting down Party Server...");
5222
- try {
5223
- unlinkSync2(pidPath);
5224
- } catch {
5225
- }
5226
- server.close();
5227
- process.exit(0);
5228
- };
5229
- process.on("SIGINT", shutdown);
5230
- process.on("SIGTERM", shutdown);
5231
- await Promise.race([discoveryPromise, cleanupPromise]);
5682
+ const snapshotLoopPromise = sm.startSnapshotLoop(
5683
+ lifecycleController.signal,
5684
+ () => registry.listAll(),
5685
+ () => messageQueue.getHistorySnapshot()
5686
+ );
5687
+ const shutdownHandler = () => void performShutdown(server, pidPath);
5688
+ process.on("SIGINT", shutdownHandler);
5689
+ process.on("SIGTERM", shutdownHandler);
5690
+ app.post("/api/shutdown", (c) => {
5691
+ void performShutdown(server, pidPath);
5692
+ return c.json({ status: "shutting_down" });
5693
+ });
5694
+ await Promise.race([discoveryPromise, cleanupPromise, snapshotLoopPromise]);
5232
5695
  }
5233
- var app;
5696
+ var app, shutdownInitiated;
5234
5697
  var init_server = __esm({
5235
5698
  "src/server/index.ts"() {
5236
5699
  "use strict";
@@ -5238,24 +5701,46 @@ var init_server = __esm({
5238
5701
  init_cors();
5239
5702
  init_dist2();
5240
5703
  init_config();
5704
+ init_persistence();
5705
+ init_logger();
5241
5706
  init_state();
5242
5707
  init_agent();
5243
5708
  init_proxy();
5244
5709
  init_dashboard();
5245
5710
  app = new Hono2();
5246
5711
  app.use("*", cors());
5712
+ app.use("*", async (c, next) => {
5713
+ const start = Date.now();
5714
+ await next();
5715
+ const ms = Date.now() - start;
5716
+ const ip = c.req.header("x-forwarded-for")?.split(",")[0]?.trim() ?? c.info?.remote?.address ?? "unknown";
5717
+ const path = c.req.path;
5718
+ if (path === "/proxy/health") {
5719
+ logger.debug("HTTP", `${c.req.method} ${path} ${c.res.status} ${ms}ms ${ip}`);
5720
+ } else {
5721
+ logger.info("HTTP", `${c.req.method} ${path} ${c.res.status} ${ms}ms ${ip}`);
5722
+ }
5723
+ });
5724
+ app.onError((err, c) => {
5725
+ logger.error("HTTP", `${c.req.method} ${c.req.path} failed (${err.status ?? 500}): ${err.message}`, err);
5726
+ return c.json({
5727
+ status: "error",
5728
+ error: (err.status ?? 500) >= 500 ? "Internal server error" : err.message
5729
+ }, err.status ?? 500);
5730
+ });
5247
5731
  app.route("/agent", agentRoutes);
5248
5732
  app.route("/proxy", proxyRoutes);
5249
5733
  app.route("/dashboard", dashboardRoutes);
5734
+ shutdownInitiated = false;
5250
5735
  main().catch((e) => {
5251
- console.error("Fatal error:", e);
5736
+ logger.error("Server", "Fatal error", e);
5252
5737
  process.exit(1);
5253
5738
  });
5254
5739
  process.on("uncaughtException", (e) => {
5255
- console.error("Uncaught exception:", e);
5740
+ logger.error("Server", "Uncaught exception", e);
5256
5741
  });
5257
5742
  process.on("unhandledRejection", (e) => {
5258
- console.error("Unhandled rejection:", e);
5743
+ logger.error("Server", "Unhandled rejection", e);
5259
5744
  });
5260
5745
  }
5261
5746
  });
@@ -5288,26 +5773,24 @@ function detectClaudeCode() {
5288
5773
  configPath: settingsPath
5289
5774
  };
5290
5775
  }
5291
- function detectCursor() {
5292
- const configPath = process.platform === "win32" ? join2(homedir(), "AppData", "Roaming", "Cursor", "User", "globalStorage", "settings.json") : join2(homedir(), ".cursor", "mcp.json");
5293
- const mcpPath = join2(homedir(), ".cursor", "mcp.json");
5294
- return {
5295
- type: "cursor",
5296
- name: "Cursor",
5297
- detected: existsSync2(mcpPath) || isExecutableInPath("cursor"),
5298
- configPath: mcpPath
5299
- };
5300
- }
5301
- function detectGeminiCli() {
5302
- return {
5303
- type: "gemini-cli",
5304
- name: "Gemini CLI",
5305
- detected: isExecutableInPath("gemini"),
5306
- configPath: join2(homedir(), ".gemini", "settings.json")
5307
- };
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 };
5308
5791
  }
5309
5792
  function detectAgents() {
5310
- return [detectClaudeCode(), detectCursor(), detectGeminiCli()];
5793
+ return [detectClaudeCode(), detectOpenClaw()];
5311
5794
  }
5312
5795
 
5313
5796
  // src/cli/agent-installer.ts
@@ -5343,7 +5826,8 @@ function findPluginDistDir() {
5343
5826
  if (existsSync3(join3(pluginDir, ".claude-plugin", "plugin.json"))) {
5344
5827
  return pluginDir;
5345
5828
  }
5346
- } catch {
5829
+ } catch (error) {
5830
+ console.error("[Agent Installer] Failed to list plugin dist directory:", error instanceof Error ? error.message : String(error));
5347
5831
  }
5348
5832
  return null;
5349
5833
  }
@@ -5362,25 +5846,26 @@ function findDistJsDir() {
5362
5846
  function getPluginVersion() {
5363
5847
  const pluginDir = findPluginDistDir();
5364
5848
  if (pluginDir) {
5365
- const manifest2 = readJsonFile(
5849
+ const manifest = readJsonFile(
5366
5850
  join3(pluginDir, ".claude-plugin", "plugin.json"),
5367
5851
  {}
5368
5852
  );
5369
- if (manifest2.version) return manifest2.version;
5370
- }
5371
- const sourceManifest = resolve(
5372
- import.meta.dirname ?? ".",
5373
- "..",
5374
- "..",
5375
- "src",
5376
- "client",
5377
- "claude-code",
5378
- ".claude-plugin",
5379
- "plugin.json"
5380
- );
5381
- const manifest = readJsonFile(sourceManifest, {});
5382
- if (manifest.version) return manifest.version;
5383
- return "0.1.0";
5853
+ if (manifest.version) return manifest.version;
5854
+ }
5855
+ const distJsDir = findDistJsDir();
5856
+ if (distJsDir) {
5857
+ const buildInfo = readJsonFile(
5858
+ join3(distJsDir, "..", "BUILD_INFO.json"),
5859
+ {}
5860
+ );
5861
+ if (buildInfo.version) return buildInfo.version;
5862
+ }
5863
+ try {
5864
+ const pkg = JSON.parse(readFileSync(join3(import.meta.dirname ?? ".", "..", "..", "package.json"), "utf-8"));
5865
+ if (pkg.version) return pkg.version;
5866
+ } catch {
5867
+ }
5868
+ return "0.0.0";
5384
5869
  }
5385
5870
  function getMarketplaceDir() {
5386
5871
  return join3(homedir2(), ".claude", "plugins", "marketplaces", "open-party");
@@ -5506,42 +5991,56 @@ function installClaudeCode() {
5506
5991
  configPath: settingsPath
5507
5992
  };
5508
5993
  }
5509
- function installCursor() {
5510
- const configPath = join3(homedir2(), ".cursor", "mcp.json");
5511
- const { command: command2, args: args2 } = getPluginCommand();
5512
- const config = readJsonFile(configPath, {});
5513
- if (!config.mcpServers) config.mcpServers = {};
5514
- config.mcpServers["open-party"] = { type: "stdio", command: command2, args: args2 };
5515
- writeJsonFile(configPath, config);
5516
- return { success: true, configPath };
5994
+ function findOpenclawDistDir() {
5995
+ const distDir = resolve(import.meta.dirname ?? ".", "..", "openclaw");
5996
+ if (!existsSync3(distDir)) return null;
5997
+ try {
5998
+ const entries = readdirSync(distDir);
5999
+ const dirs = entries.filter((e) => e.startsWith("open-party-"));
6000
+ if (dirs.length === 0) return null;
6001
+ return join3(distDir, dirs[dirs.length - 1]);
6002
+ } catch (error) {
6003
+ console.error("[Agent Installer] Failed to list openclaw dist directory:", error instanceof Error ? error.message : String(error));
6004
+ }
6005
+ return null;
5517
6006
  }
5518
- function installGeminiCli() {
5519
- const configPath = join3(homedir2(), ".gemini", "settings.json");
5520
- const { command: command2, args: args2 } = getPluginCommand();
6007
+ function installOpenClaw() {
6008
+ const pluginDir = findOpenclawDistDir();
6009
+ if (!pluginDir) {
6010
+ return {
6011
+ success: false,
6012
+ error: 'OpenClaw plugin package not found. Run "npm run build:openclaw" first.'
6013
+ };
6014
+ }
6015
+ const configPath = join3(homedir2(), ".openclaw", "openclaw.json");
6016
+ const extensionDir = join3(homedir2(), ".openclaw", "extensions", "open-party");
6017
+ if (existsSync3(extensionDir)) {
6018
+ rmSync(extensionDir, { recursive: true });
6019
+ }
6020
+ mkdirSync(extensionDir, { recursive: true });
6021
+ cpSync(pluginDir, extensionDir, { recursive: true });
5521
6022
  const config = readJsonFile(configPath, {});
5522
- if (!config.mcpServers) config.mcpServers = {};
5523
- config.mcpServers["open-party"] = { type: "stdio", command: command2, args: args2 };
6023
+ if (!config.plugins) config.plugins = {};
6024
+ if (!config.plugins.entries) {
6025
+ config.plugins.entries = {};
6026
+ }
6027
+ const entries = config.plugins.entries;
6028
+ entries["open-party"] = {
6029
+ enabled: true,
6030
+ config: {
6031
+ partyServerUrl: "http://127.0.0.1:8000",
6032
+ heartbeatInterval: 3e4
6033
+ }
6034
+ };
5524
6035
  writeJsonFile(configPath, config);
5525
6036
  return { success: true, configPath };
5526
6037
  }
5527
- function getPluginCommand() {
5528
- const distDir = findDistJsDir();
5529
- if (distDir) {
5530
- const serverPath = join3(distDir, "mcp-server.js");
5531
- if (existsSync3(serverPath)) {
5532
- return { command: "node", args: [serverPath] };
5533
- }
5534
- }
5535
- return { command: "npx", args: ["@feynmanzhang/open-party", "mcp"] };
5536
- }
5537
6038
  async function installPluginToAgent(agentType) {
5538
6039
  switch (agentType) {
5539
6040
  case "claude-code":
5540
6041
  return installClaudeCode();
5541
- case "cursor":
5542
- return installCursor();
5543
- case "gemini-cli":
5544
- return installGeminiCli();
6042
+ case "openclaw":
6043
+ return installOpenClaw();
5545
6044
  default:
5546
6045
  return { success: false, error: `Unknown agent type: ${agentType}` };
5547
6046
  }
@@ -5884,7 +6383,7 @@ ${bold(cyan("\u{1F50D} Step 2: Detecting AI agents in your environment..."))}
5884
6383
  const detected = agents.filter((a) => a.detected);
5885
6384
  if (detected.length === 0) {
5886
6385
  console.log(yellow("No supported AI agents detected in this environment."));
5887
- console.log(" Supported agents: Claude Code, Cursor, Gemini CLI");
6386
+ console.log(" Supported agents: Claude Code, OpenClaw");
5888
6387
  console.log("");
5889
6388
  console.log(" Install one and re-run: open-party setup");
5890
6389
  return;
@@ -5914,6 +6413,9 @@ Installing Open Party plugin for ${agent.name}...`);
5914
6413
  if (agent.type === "claude-code") {
5915
6414
  console.log(` ${bold("Please restart Claude Code")} for changes to take effect.`);
5916
6415
  }
6416
+ if (agent.type === "openclaw") {
6417
+ console.log(` ${bold("Please restart OpenClaw gateway")} for changes to take effect.`);
6418
+ }
5917
6419
  } else {
5918
6420
  console.log(`${red("\u274C")} Failed to install for ${agent.name}: ${result.error}`);
5919
6421
  }
@@ -5927,9 +6429,9 @@ async function setupCommand() {
5927
6429
  console.log(`
5928
6430
  ${bold(cyan("\u{1F680} Starting Party Server..."))}`);
5929
6431
  const { spawn: spawn4 } = await import("child_process");
5930
- const { resolve: resolve4, dirname: dirname5 } = await import("path");
6432
+ const { resolve: resolve4, dirname: dirname6 } = await import("path");
5931
6433
  const { fileURLToPath: fileURLToPath3 } = await import("url");
5932
- const __dirname2 = dirname5(fileURLToPath3(import.meta.url));
6434
+ const __dirname2 = dirname6(fileURLToPath3(import.meta.url));
5933
6435
  const serverScript = resolve4(__dirname2, "..", "party-server.js");
5934
6436
  const serverProc = spawn4(process.execPath, [serverScript], {
5935
6437
  detached: true,
@@ -6048,12 +6550,12 @@ function removePidFile() {
6048
6550
  function isProcessRunning(pid) {
6049
6551
  if (process.platform === "win32") {
6050
6552
  try {
6051
- const output = execSync3(`tasklist /FI "PID eq ${pid}" /NH`, {
6553
+ const output2 = execSync3(`tasklist /FI "PID eq ${pid}" /NH`, {
6052
6554
  encoding: "utf-8",
6053
6555
  windowsHide: true,
6054
6556
  stdio: ["pipe", "pipe", "pipe"]
6055
6557
  });
6056
- return output.includes(String(pid));
6558
+ return output2.includes(String(pid));
6057
6559
  } catch {
6058
6560
  return false;
6059
6561
  }
@@ -6136,7 +6638,28 @@ function killServer(pid) {
6136
6638
  } else {
6137
6639
  process.kill(pid, "SIGTERM");
6138
6640
  }
6641
+ } catch (error) {
6642
+ void error;
6643
+ }
6644
+ }
6645
+ function findPidByPort(port) {
6646
+ try {
6647
+ if (process.platform === "win32") {
6648
+ const output3 = execSync3(
6649
+ `netstat -ano -p tcp | findstr "LISTENING" | findstr ":${port} "`,
6650
+ { encoding: "utf-8", windowsHide: true, stdio: ["pipe", "pipe", "pipe"] }
6651
+ );
6652
+ const match2 = output3.trim().match(/\s(\d+)\s*$/);
6653
+ return match2 ? parseInt(match2[1], 10) : null;
6654
+ }
6655
+ const output2 = execSync3(`lsof -t -i :${port} -sTCP:LISTEN`, {
6656
+ encoding: "utf-8",
6657
+ stdio: ["pipe", "pipe", "pipe"]
6658
+ });
6659
+ const pid = parseInt(output2.trim().split("\n")[0], 10);
6660
+ return Number.isFinite(pid) && pid > 0 ? pid : null;
6139
6661
  } catch {
6662
+ return null;
6140
6663
  }
6141
6664
  }
6142
6665
  function parseStartArgs(args2) {
@@ -6204,14 +6727,48 @@ async function startDaemon(port) {
6204
6727
  }
6205
6728
 
6206
6729
  // src/cli/stop-server.ts
6730
+ async function stopServerGracefully(pid, port) {
6731
+ try {
6732
+ const controller = new AbortController();
6733
+ const timer = setTimeout(() => controller.abort(), 8e3);
6734
+ const resp = await fetch(`http://127.0.0.1:${port}/api/shutdown`, {
6735
+ method: "POST",
6736
+ signal: controller.signal
6737
+ });
6738
+ clearTimeout(timer);
6739
+ return resp.ok;
6740
+ } catch {
6741
+ return false;
6742
+ }
6743
+ }
6744
+ async function waitForProcessExit(pid, timeoutMs = 1e4) {
6745
+ const deadline = Date.now() + timeoutMs;
6746
+ while (Date.now() < deadline) {
6747
+ if (!isProcessRunning(pid)) return true;
6748
+ await new Promise((r) => setTimeout(r, 200));
6749
+ }
6750
+ return false;
6751
+ }
6207
6752
  async function stopServer() {
6208
6753
  const pid = readPid();
6754
+ const port = resolvePort();
6209
6755
  if (pid === null) {
6210
- const port2 = resolvePort();
6211
- const healthy = await isServerHealthy(port2);
6756
+ const healthy = await isServerHealthy(port);
6212
6757
  if (healthy) {
6213
- console.log(`No PID file found, but a server is responding on port ${port2}.`);
6214
- console.log("It may have been started manually. Kill it by port or process name.");
6758
+ const foundPid = findPidByPort(port);
6759
+ if (foundPid !== null && isProcessRunning(foundPid)) {
6760
+ console.log(`No PID file found, but a server is responding on port ${port} (PID ${foundPid}).`);
6761
+ console.log(`Stopping by port lookup...`);
6762
+ killServer(foundPid);
6763
+ const stillUp2 = await isServerHealthy(port);
6764
+ if (!stillUp2) {
6765
+ console.log("Party Server stopped.");
6766
+ return;
6767
+ }
6768
+ console.warn(`Process ${foundPid} was killed, but port ${port} is still responding.`);
6769
+ } else {
6770
+ console.log(`No PID file found, and no process found on port ${port}.`);
6771
+ }
6215
6772
  } else {
6216
6773
  console.log("Party Server is not running (no PID file found).");
6217
6774
  }
@@ -6223,9 +6780,18 @@ async function stopServer() {
6223
6780
  return;
6224
6781
  }
6225
6782
  console.log(`Stopping Party Server (PID ${pid})...`);
6783
+ const gracefulOk = await stopServerGracefully(pid, port);
6784
+ if (gracefulOk) {
6785
+ const exited = await waitForProcessExit(pid);
6786
+ if (exited) {
6787
+ removePidFile();
6788
+ console.log("Party Server stopped gracefully.");
6789
+ return;
6790
+ }
6791
+ console.warn("Graceful shutdown timed out, falling back to force kill...");
6792
+ }
6226
6793
  killServer(pid);
6227
6794
  removePidFile();
6228
- const port = resolvePort();
6229
6795
  const stillUp = await isServerHealthy(port);
6230
6796
  if (stillUp) {
6231
6797
  console.warn(`Process ${pid} was killed, but port ${port} is still responding.`);
@@ -6284,12 +6850,12 @@ async function statusCommand() {
6284
6850
  }
6285
6851
 
6286
6852
  // src/cli/version.ts
6287
- import { readFileSync as readFileSync3 } from "fs";
6288
- import { resolve as resolve3, dirname as dirname4 } from "path";
6853
+ import { readFileSync as readFileSync4 } from "fs";
6854
+ import { resolve as resolve3, dirname as dirname5 } from "path";
6289
6855
  import { fileURLToPath as fileURLToPath2 } from "url";
6290
6856
  function showVersion() {
6291
- const pkgPath = resolve3(dirname4(fileURLToPath2(import.meta.url)), "..", "..", "package.json");
6292
- const pkg = JSON.parse(readFileSync3(pkgPath, "utf-8"));
6857
+ const pkgPath = resolve3(dirname5(fileURLToPath2(import.meta.url)), "..", "..", "package.json");
6858
+ const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
6293
6859
  console.log(`open-party v${pkg.version}`);
6294
6860
  }
6295
6861