@feynmanzhang/open-party 0.1.1 → 0.1.3-beta.0

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
@@ -201,7 +201,7 @@ function joinTailnet(authKey, timeout = 3e4) {
201
201
  const binary = getTailscaleBinary();
202
202
  try {
203
203
  const output = execWithSudoFallback(
204
- [binary, "up", "--authkey", authKey, "--accept-routes"],
204
+ [binary, "up", "--authkey", authKey],
205
205
  timeout
206
206
  );
207
207
  return { success: true, output: output.trim() };
@@ -3171,7 +3171,7 @@ var init_dist2 = __esm({
3171
3171
  });
3172
3172
  if (!chunk) {
3173
3173
  if (i === 1) {
3174
- await new Promise((resolve2) => setTimeout(resolve2));
3174
+ await new Promise((resolve3) => setTimeout(resolve3));
3175
3175
  maxReadCount = 3;
3176
3176
  continue;
3177
3177
  }
@@ -3410,8 +3410,8 @@ function classifyFetchError(error) {
3410
3410
  if (error instanceof DOMException && error.name === "AbortError") return null;
3411
3411
  return null;
3412
3412
  }
3413
- function sleep(ms) {
3414
- return new Promise((resolve2) => setTimeout(resolve2, ms));
3413
+ function sleep2(ms) {
3414
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
3415
3415
  }
3416
3416
  var UNKNOWN, PARTY_SERVER, DEGRADED, SUSPECT, DOWN, NOT_SERVER, MAYBE, MAYBE_MAX_RETRIES, BACKOFF_BASE, BACKOFF_CAP, FAILURE_SUSPECT, FAILURE_DOWN, PeerDiscovery;
3417
3417
  var init_peer_discovery = __esm({
@@ -3487,7 +3487,7 @@ var init_peer_discovery = __esm({
3487
3487
  } catch (e) {
3488
3488
  console.error("[Discovery] Cycle failed:", e);
3489
3489
  }
3490
- await sleep(DISCOVERY_INTERVAL * 1e3);
3490
+ await sleep2(DISCOVERY_INTERVAL * 1e3);
3491
3491
  }
3492
3492
  }
3493
3493
  async discoveryCycle() {
@@ -4639,6 +4639,19 @@ body::after{
4639
4639
  fastTimer = setInterval(fastRefresh, 3000);
4640
4640
  fullTimer = setInterval(fullRefresh, 5000);
4641
4641
 
4642
+ // Clipboard click delegation for .cmd elements
4643
+ document.addEventListener('click', function(e) {
4644
+ var cmd = e.target.closest('.cmd');
4645
+ if (!cmd) return;
4646
+ var text = cmd.getAttribute('data-clipboard');
4647
+ if (text) {
4648
+ navigator.clipboard.writeText(text).then(function() {
4649
+ var hint = cmd.querySelector('.copy-hint');
4650
+ if (hint) hint.textContent = 'Copied!';
4651
+ });
4652
+ }
4653
+ });
4654
+
4642
4655
  // Update uptime display every second
4643
4656
  setInterval(function() {
4644
4657
  if (overview && overview.server) {
@@ -4766,7 +4779,7 @@ body::after{
4766
4779
  html += '<div style="color:var(--muted);margin-bottom:6px">Install for ' + info.os + ':</div>';
4767
4780
  info.commands.forEach(function(cmd) {
4768
4781
  const display = info.needs_sudo ? 'sudo ' + cmd : cmd;
4769
- html += '<div class="cmd" onclick="navigator.clipboard.writeText('' + display.replace(/'/g, "\\'") + '').then(function(){this.querySelector('.copy-hint').textContent='Copied!'}.bind(this))">';
4782
+ html += '<div class="cmd" data-clipboard="' + display.replace(/"/g, '&quot;') + '">';
4770
4783
  html += '<code>' + display + '</code><span class="copy-hint">Click to copy</span></div>';
4771
4784
  });
4772
4785
  if (info.download_url) {
@@ -4927,9 +4940,20 @@ var init_dashboard = __esm({
4927
4940
 
4928
4941
  // src/server/index.ts
4929
4942
  var server_exports = {};
4943
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2 } from "fs";
4944
+ import { join as join5, dirname as dirname3 } from "path";
4945
+ import { homedir as homedir4 } from "os";
4930
4946
  async function periodicCleanup() {
4931
4947
  }
4948
+ function pidFilePath2() {
4949
+ const pluginData = process.env.CLAUDE_PLUGIN_DATA || "";
4950
+ if (pluginData) return join5(pluginData, "server.pid");
4951
+ return join5(homedir4(), ".open-party", "server.pid");
4952
+ }
4932
4953
  async function main() {
4954
+ const pidPath = pidFilePath2();
4955
+ mkdirSync3(dirname3(pidPath), { recursive: true });
4956
+ writeFileSync3(pidPath, String(process.pid));
4933
4957
  console.log(`Starting Party Server on port ${PARTY_PORT} (Tailscale IP: ${getSelfIp()})`);
4934
4958
  process.on("SIGHUP", () => {
4935
4959
  });
@@ -4938,6 +4962,10 @@ async function main() {
4938
4962
  const cleanupPromise = periodicCleanup();
4939
4963
  const shutdown = () => {
4940
4964
  console.log("\nShutting down Party Server...");
4965
+ try {
4966
+ unlinkSync2(pidPath);
4967
+ } catch {
4968
+ }
4941
4969
  server.close();
4942
4970
  process.exit(0);
4943
4971
  };
@@ -4998,7 +5026,7 @@ async function installTailscale(platform) {
4998
5026
  const args2 = entry.needsSudo ? [entry.cmd, ...entry.args] : entry.args;
4999
5027
  console.log(`Running: ${cmd} ${args2.join(" ")}
5000
5028
  `);
5001
- return new Promise((resolve2) => {
5029
+ return new Promise((resolve3) => {
5002
5030
  const child = spawn(cmd, args2, {
5003
5031
  stdio: "inherit",
5004
5032
  windowsHide: true
@@ -5008,15 +5036,15 @@ async function installTailscale(platform) {
5008
5036
  if (exited) return;
5009
5037
  exited = true;
5010
5038
  if (code === 0) {
5011
- resolve2({ success: true, output: "Installation completed." });
5039
+ resolve3({ success: true, output: "Installation completed." });
5012
5040
  } else {
5013
- resolve2({ success: false, output: `Installation exited with code ${code}` });
5041
+ resolve3({ success: false, output: `Installation exited with code ${code}` });
5014
5042
  }
5015
5043
  });
5016
5044
  child.on("error", (err) => {
5017
5045
  if (exited) return;
5018
5046
  exited = true;
5019
- resolve2({ success: false, output: err.message });
5047
+ resolve3({ success: false, output: err.message });
5020
5048
  });
5021
5049
  });
5022
5050
  }
@@ -5068,93 +5096,229 @@ function detectAgents() {
5068
5096
  }
5069
5097
 
5070
5098
  // src/cli/agent-installer.ts
5071
- import { existsSync as existsSync3, readFileSync, writeFileSync, mkdirSync } from "fs";
5099
+ import { existsSync as existsSync3, readFileSync, writeFileSync, mkdirSync, cpSync, rmSync, readdirSync } from "fs";
5072
5100
  import { join as join3, dirname, resolve } from "path";
5073
5101
  import { homedir as homedir2 } from "os";
5074
- function findPluginDist() {
5102
+ function ensureDir(filePath) {
5103
+ const dir = dirname(filePath);
5104
+ if (!existsSync3(dir)) {
5105
+ mkdirSync(dir, { recursive: true });
5106
+ }
5107
+ }
5108
+ function readJsonFile(filePath, fallback) {
5109
+ if (!existsSync3(filePath)) return fallback;
5110
+ try {
5111
+ return JSON.parse(readFileSync(filePath, "utf-8"));
5112
+ } catch {
5113
+ return fallback;
5114
+ }
5115
+ }
5116
+ function writeJsonFile(filePath, data) {
5117
+ ensureDir(filePath);
5118
+ writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n");
5119
+ }
5120
+ function findPluginDistDir() {
5121
+ const distDir = resolve(import.meta.dirname ?? ".", "..", "claude-code");
5122
+ if (!existsSync3(distDir)) return null;
5123
+ try {
5124
+ const entries = readdirSync(distDir);
5125
+ const dirs = entries.filter((e) => e.startsWith("open-party-"));
5126
+ if (dirs.length === 0) return null;
5127
+ const pluginDir = join3(distDir, dirs[dirs.length - 1]);
5128
+ if (existsSync3(join3(pluginDir, ".claude-plugin", "plugin.json"))) {
5129
+ return pluginDir;
5130
+ }
5131
+ } catch {
5132
+ }
5133
+ return null;
5134
+ }
5135
+ function findDistJsDir() {
5075
5136
  const possiblePaths = [
5076
- // When installed globally: <pkg-root>/dist/cli/index.js → <pkg-root>/dist/
5077
- resolve(import.meta.dirname ?? ".", "..", "party-server.js"),
5078
- // When running from source: <project>/dist/cli/index.js → <project>/dist/
5079
- resolve(import.meta.dirname ?? ".", "..", "mcp-server.js")
5137
+ // When installed globally via npm: <pkg-root>/dist/cli/index.js → <pkg-root>/dist/
5138
+ resolve(import.meta.dirname ?? ".", "..")
5080
5139
  ];
5081
5140
  for (const p of possiblePaths) {
5082
- if (existsSync3(p)) return dirname(p);
5141
+ if (existsSync3(join3(p, "mcp-server.js")) && existsSync3(join3(p, "hook-handler.js"))) {
5142
+ return p;
5143
+ }
5083
5144
  }
5084
5145
  return null;
5085
5146
  }
5086
- function ensureConfigFile(configPath) {
5087
- const dir = dirname(configPath);
5088
- if (!existsSync3(dir)) {
5089
- mkdirSync(dir, { recursive: true });
5090
- }
5091
- if (existsSync3(configPath)) {
5092
- try {
5093
- const content = readFileSync(configPath, "utf-8");
5094
- return JSON.parse(content);
5095
- } catch {
5096
- }
5147
+ function getPluginVersion() {
5148
+ const pluginDir = findPluginDistDir();
5149
+ if (pluginDir) {
5150
+ const manifest2 = readJsonFile(
5151
+ join3(pluginDir, ".claude-plugin", "plugin.json"),
5152
+ {}
5153
+ );
5154
+ if (manifest2.version) return manifest2.version;
5097
5155
  }
5098
- return { mcpServers: {} };
5156
+ const sourceManifest = resolve(
5157
+ import.meta.dirname ?? ".",
5158
+ "..",
5159
+ "..",
5160
+ "src",
5161
+ "client",
5162
+ "claude-code",
5163
+ ".claude-plugin",
5164
+ "plugin.json"
5165
+ );
5166
+ const manifest = readJsonFile(sourceManifest, {});
5167
+ if (manifest.version) return manifest.version;
5168
+ return "0.1.0";
5099
5169
  }
5100
- function writeConfigFile(configPath, config) {
5101
- writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
5170
+ function getMarketplaceDir() {
5171
+ return join3(homedir2(), ".claude", "plugins", "marketplaces", "open-party");
5102
5172
  }
5103
- function getPluginCommand() {
5104
- const distDir = findPluginDist();
5105
- if (distDir) {
5106
- const serverPath = join3(distDir, "mcp-server.js");
5107
- if (existsSync3(serverPath)) {
5108
- return { command: "node", args: [serverPath] };
5109
- }
5173
+ function registerMarketplace(version, pluginDir) {
5174
+ const marketplaceDir = getMarketplaceDir();
5175
+ const pluginSourceDir = join3(marketplaceDir, "plugin");
5176
+ const marketplacePluginDir = join3(marketplaceDir, ".claude-plugin");
5177
+ if (!existsSync3(marketplacePluginDir)) {
5178
+ mkdirSync(marketplacePluginDir, { recursive: true });
5110
5179
  }
5111
- return { command: "npx", args: ["@feynmanzhang/open-party", "mcp"] };
5112
- }
5113
- function getMcpServerConfig() {
5114
- const cmd = getPluginCommand();
5115
- if (!cmd) return null;
5116
- return {
5117
- type: "stdio",
5118
- command: cmd.command,
5119
- args: cmd.args
5180
+ const marketplaceManifest = {
5181
+ name: "open-party",
5182
+ owner: { name: "Feynman Zhang" },
5183
+ metadata: {
5184
+ description: "Decentralized Agent communication network for Claude Code",
5185
+ homepage: "https://github.com/FeynmanZhang/open-party"
5186
+ },
5187
+ plugins: [
5188
+ {
5189
+ name: "open-party",
5190
+ version,
5191
+ source: "./plugin",
5192
+ description: "Decentralized Agent communication network for Claude Code"
5193
+ }
5194
+ ]
5195
+ };
5196
+ writeJsonFile(join3(marketplacePluginDir, "marketplace.json"), marketplaceManifest);
5197
+ if (existsSync3(pluginSourceDir)) {
5198
+ rmSync(pluginSourceDir, { recursive: true });
5199
+ }
5200
+ mkdirSync(pluginSourceDir, { recursive: true });
5201
+ cpSync(pluginDir, pluginSourceDir, { recursive: true });
5202
+ const knownMarketplacesPath = join3(homedir2(), ".claude", "plugins", "known_marketplaces.json");
5203
+ const knownMarketplaces = readJsonFile(knownMarketplacesPath, {});
5204
+ knownMarketplaces["open-party"] = {
5205
+ source: {
5206
+ source: "github",
5207
+ repo: "FeynmanZhang/open-party"
5208
+ },
5209
+ installLocation: marketplaceDir,
5210
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
5120
5211
  };
5212
+ writeJsonFile(knownMarketplacesPath, knownMarketplaces);
5121
5213
  }
5122
5214
  function installClaudeCode() {
5123
- const configPath = join3(homedir2(), ".claude", "settings.json");
5124
- const serverConfig = getMcpServerConfig();
5125
- if (!serverConfig) {
5126
- return { success: false, error: "Could not locate Open Party plugin files" };
5215
+ const pluginDir = findPluginDistDir();
5216
+ if (!pluginDir) {
5217
+ return {
5218
+ success: false,
5219
+ error: 'Plugin package not found. Run "npm run build:plugin" first, or use "claude --plugin-dir" to install manually.'
5220
+ };
5127
5221
  }
5128
- const config = ensureConfigFile(configPath);
5129
- if (!config.mcpServers) config.mcpServers = {};
5130
- config.mcpServers["open-party"] = serverConfig;
5131
- writeConfigFile(configPath, config);
5132
- return { success: true, configPath };
5222
+ const version = getPluginVersion();
5223
+ const installDir = join3(homedir2(), ".claude", "plugins", "cache", "open-party", "open-party", version);
5224
+ if (existsSync3(installDir)) {
5225
+ rmSync(installDir, { recursive: true });
5226
+ }
5227
+ mkdirSync(installDir, { recursive: true });
5228
+ cpSync(pluginDir, installDir, { recursive: true });
5229
+ const mcpServerPath = join3(installDir, "dist", "mcp-server.js");
5230
+ if (!existsSync3(mcpServerPath)) {
5231
+ const distJsDir = findDistJsDir();
5232
+ if (distJsDir) {
5233
+ const targetDist = join3(installDir, "dist");
5234
+ if (!existsSync3(targetDist)) mkdirSync(targetDist, { recursive: true });
5235
+ for (const file of ["mcp-server.js", "hook-handler.js", "party-server.js"]) {
5236
+ const src = join3(distJsDir, file);
5237
+ if (existsSync3(src)) {
5238
+ cpSync(src, join3(targetDist, file));
5239
+ }
5240
+ }
5241
+ for (const file of ["mcp-server.js.map", "hook-handler.js.map", "party-server.js.map"]) {
5242
+ const src = join3(distJsDir, file);
5243
+ if (existsSync3(src)) {
5244
+ cpSync(src, join3(targetDist, file));
5245
+ }
5246
+ }
5247
+ }
5248
+ }
5249
+ const orphanedPath = join3(installDir, "dist", ".orphaned_at");
5250
+ if (existsSync3(orphanedPath)) {
5251
+ rmSync(orphanedPath);
5252
+ }
5253
+ registerMarketplace(version, pluginDir);
5254
+ const pluginsJsonPath = join3(homedir2(), ".claude", "plugins", "installed_plugins.json");
5255
+ const pluginsData = readJsonFile(
5256
+ pluginsJsonPath,
5257
+ { version: 2, plugins: {} }
5258
+ );
5259
+ if (!pluginsData.plugins) pluginsData.plugins = {};
5260
+ if (pluginsData.plugins["open-party@local"]) {
5261
+ delete pluginsData.plugins["open-party@local"];
5262
+ }
5263
+ pluginsData.plugins["open-party@open-party"] = [
5264
+ {
5265
+ scope: "user",
5266
+ installPath: installDir,
5267
+ version,
5268
+ installedAt: (/* @__PURE__ */ new Date()).toISOString(),
5269
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
5270
+ }
5271
+ ];
5272
+ writeJsonFile(pluginsJsonPath, pluginsData);
5273
+ const settingsPath = join3(homedir2(), ".claude", "settings.json");
5274
+ const settings = readJsonFile(settingsPath, {});
5275
+ if (settings.mcpServers?.["open-party"]) {
5276
+ delete settings.mcpServers["open-party"];
5277
+ if (Object.keys(settings.mcpServers).length === 0) {
5278
+ delete settings.mcpServers;
5279
+ }
5280
+ }
5281
+ if (!settings.enabledPlugins) {
5282
+ settings.enabledPlugins = {};
5283
+ }
5284
+ if (settings.enabledPlugins["open-party@local"] !== void 0) {
5285
+ delete settings.enabledPlugins["open-party@local"];
5286
+ }
5287
+ settings.enabledPlugins["open-party@open-party"] = true;
5288
+ writeJsonFile(settingsPath, settings);
5289
+ return {
5290
+ success: true,
5291
+ configPath: settingsPath
5292
+ };
5133
5293
  }
5134
5294
  function installCursor() {
5135
5295
  const configPath = join3(homedir2(), ".cursor", "mcp.json");
5136
- const serverConfig = getMcpServerConfig();
5137
- if (!serverConfig) {
5138
- return { success: false, error: "Could not locate Open Party plugin files" };
5139
- }
5140
- const config = ensureConfigFile(configPath);
5296
+ const { command: command2, args: args2 } = getPluginCommand();
5297
+ const config = readJsonFile(configPath, {});
5141
5298
  if (!config.mcpServers) config.mcpServers = {};
5142
- config.mcpServers["open-party"] = serverConfig;
5143
- writeConfigFile(configPath, config);
5299
+ config.mcpServers["open-party"] = { type: "stdio", command: command2, args: args2 };
5300
+ writeJsonFile(configPath, config);
5144
5301
  return { success: true, configPath };
5145
5302
  }
5146
5303
  function installGeminiCli() {
5147
5304
  const configPath = join3(homedir2(), ".gemini", "settings.json");
5148
- const serverConfig = getMcpServerConfig();
5149
- if (!serverConfig) {
5150
- return { success: false, error: "Could not locate Open Party plugin files" };
5151
- }
5152
- const config = ensureConfigFile(configPath);
5305
+ const { command: command2, args: args2 } = getPluginCommand();
5306
+ const config = readJsonFile(configPath, {});
5153
5307
  if (!config.mcpServers) config.mcpServers = {};
5154
- config.mcpServers["open-party"] = serverConfig;
5155
- writeConfigFile(configPath, config);
5308
+ config.mcpServers["open-party"] = { type: "stdio", command: command2, args: args2 };
5309
+ writeJsonFile(configPath, config);
5156
5310
  return { success: true, configPath };
5157
5311
  }
5312
+ function getPluginCommand() {
5313
+ const distDir = findDistJsDir();
5314
+ if (distDir) {
5315
+ const serverPath = join3(distDir, "mcp-server.js");
5316
+ if (existsSync3(serverPath)) {
5317
+ return { command: "node", args: [serverPath] };
5318
+ }
5319
+ }
5320
+ return { command: "npx", args: ["@feynmanzhang/open-party", "mcp"] };
5321
+ }
5158
5322
  async function installPluginToAgent(agentType) {
5159
5323
  switch (agentType) {
5160
5324
  case "claude-code":
@@ -5171,7 +5335,7 @@ async function installPluginToAgent(agentType) {
5171
5335
  // src/cli/setup.ts
5172
5336
  var rl = createInterface({ input: process.stdin, output: process.stdout });
5173
5337
  function prompt(question) {
5174
- return new Promise((resolve2) => rl.question(question, (answer) => resolve2(answer.trim())));
5338
+ return new Promise((resolve3) => rl.question(question, (answer) => resolve3(answer.trim())));
5175
5339
  }
5176
5340
  function cyan(text) {
5177
5341
  return `\x1B[36m${text}\x1B[0m`;
@@ -5274,10 +5438,10 @@ async function handleInteractiveLogin(binary) {
5274
5438
  console.log(`
5275
5439
  ${cyan("Running interactive login...")}`);
5276
5440
  console.log("A browser window should open. Authenticate in the browser, then return here.\n");
5277
- const { spawn: spawn2 } = await import("child_process");
5278
- const child = spawn2(binary, ["login"], { stdio: "inherit" });
5279
- const exitCode = await new Promise((resolve2) => {
5280
- child.on("close", resolve2);
5441
+ const { spawn: spawn3 } = await import("child_process");
5442
+ const child = spawn3(binary, ["login"], { stdio: "inherit" });
5443
+ const exitCode = await new Promise((resolve3) => {
5444
+ child.on("close", resolve3);
5281
5445
  });
5282
5446
  resetTailscaleBinaryCache();
5283
5447
  const status = getTailscaleInstallationStatus();
@@ -5293,7 +5457,7 @@ ${yellow("\u26A0\uFE0F Login may not have completed. Status: " + status.state)}
5293
5457
  }
5294
5458
  async function handleAuthKeyLogin(binary) {
5295
5459
  console.log("");
5296
- console.log("You can generate an Auth Key at:");
5460
+ console.log("Ask the network creator to generate an Auth Key at:");
5297
5461
  console.log(`${cyan(" https://login.tailscale.com/admin/settings/keys")}
5298
5462
  `);
5299
5463
  const authKey = await prompt("Enter Auth Key: ");
@@ -5352,6 +5516,12 @@ Installing Open Party plugin for ${agent.name}...`);
5352
5516
  const result = await installPluginToAgent(agent.type);
5353
5517
  if (result.success) {
5354
5518
  console.log(`${green("\u2705")} Plugin installed for ${agent.name}${result.configPath ? ` (${result.configPath})` : ""}`);
5519
+ if (result.warning) {
5520
+ console.log(` ${yellow("\u26A0\uFE0F")} ${result.warning}`);
5521
+ }
5522
+ if (agent.type === "claude-code") {
5523
+ console.log(` ${bold("Please restart Claude Code")} for changes to take effect.`);
5524
+ }
5355
5525
  } else {
5356
5526
  console.log(`${red("\u274C")} Failed to install for ${agent.name}: ${result.error}`);
5357
5527
  }
@@ -5377,12 +5547,12 @@ async function setupCommand() {
5377
5547
  await stepAgentPlugin();
5378
5548
  console.log(`
5379
5549
  ${bold(cyan("\u{1F680} Starting Party Server..."))}`);
5380
- const { spawn: spawn2 } = await import("child_process");
5381
- const { resolve: resolve2, dirname: dirname2 } = await import("path");
5382
- const { fileURLToPath } = await import("url");
5383
- const __dirname = dirname2(fileURLToPath(import.meta.url));
5384
- const serverScript = resolve2(__dirname, "..", "party-server.js");
5385
- const serverProc = spawn2(process.execPath, [serverScript], {
5550
+ const { spawn: spawn3 } = await import("child_process");
5551
+ const { resolve: resolve3, dirname: dirname4 } = await import("path");
5552
+ const { fileURLToPath: fileURLToPath2 } = await import("url");
5553
+ const __dirname2 = dirname4(fileURLToPath2(import.meta.url));
5554
+ const serverScript = resolve3(__dirname2, "..", "party-server.js");
5555
+ const serverProc = spawn3(process.execPath, [serverScript], {
5386
5556
  detached: true,
5387
5557
  stdio: "ignore",
5388
5558
  windowsHide: true
@@ -5396,28 +5566,334 @@ ${bold(green("\u{1F389} Setup complete!"))}`);
5396
5566
  rl.close();
5397
5567
  }
5398
5568
 
5569
+ // src/cli/server-utils.ts
5570
+ import { spawn as spawn2, execSync as execSync3 } from "child_process";
5571
+ import { existsSync as existsSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync, mkdirSync as mkdirSync2, openSync } from "fs";
5572
+ import { join as join4, dirname as dirname2, resolve as resolve2 } from "path";
5573
+ import { homedir as homedir3 } from "os";
5574
+ import { fileURLToPath } from "url";
5575
+ var __dirname = dirname2(fileURLToPath(import.meta.url));
5576
+ function pidFilePath() {
5577
+ const pluginData = process.env.CLAUDE_PLUGIN_DATA || "";
5578
+ if (pluginData) return join4(pluginData, "server.pid");
5579
+ return join4(homedir3(), ".open-party", "server.pid");
5580
+ }
5581
+ function logFilePath() {
5582
+ const pluginData = process.env.CLAUDE_PLUGIN_DATA || "";
5583
+ if (pluginData) return join4(pluginData, "server.log");
5584
+ return join4(homedir3(), ".open-party", "server.log");
5585
+ }
5586
+ function serverScriptPath() {
5587
+ return resolve2(__dirname, "..", "party-server.js");
5588
+ }
5589
+ function readPid() {
5590
+ const path = pidFilePath();
5591
+ if (!existsSync4(path)) return null;
5592
+ try {
5593
+ return parseInt(readFileSync2(path, "utf-8").trim(), 10);
5594
+ } catch {
5595
+ return null;
5596
+ }
5597
+ }
5598
+ function writePid(pid) {
5599
+ const path = pidFilePath();
5600
+ const dir = dirname2(path);
5601
+ if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
5602
+ writeFileSync2(path, String(pid));
5603
+ }
5604
+ function removePidFile() {
5605
+ try {
5606
+ unlinkSync(pidFilePath());
5607
+ } catch {
5608
+ }
5609
+ }
5610
+ function isProcessRunning(pid) {
5611
+ if (process.platform === "win32") {
5612
+ try {
5613
+ const output = execSync3(`tasklist /FI "PID eq ${pid}" /NH`, {
5614
+ encoding: "utf-8",
5615
+ windowsHide: true,
5616
+ stdio: ["pipe", "pipe", "pipe"]
5617
+ });
5618
+ return output.includes(String(pid));
5619
+ } catch {
5620
+ return false;
5621
+ }
5622
+ }
5623
+ try {
5624
+ process.kill(pid, 0);
5625
+ return true;
5626
+ } catch {
5627
+ return false;
5628
+ }
5629
+ }
5630
+ function resolvePort() {
5631
+ return parseInt(process.env.PARTY_PORT || "8000", 10);
5632
+ }
5633
+ async function fetchJson(url, timeoutMs = 2e3) {
5634
+ try {
5635
+ const controller = new AbortController();
5636
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
5637
+ const resp = await fetch(url, { signal: controller.signal });
5638
+ clearTimeout(timer);
5639
+ if (!resp.ok) return null;
5640
+ return await resp.json();
5641
+ } catch {
5642
+ return null;
5643
+ }
5644
+ }
5645
+ async function isServerHealthy(port) {
5646
+ const p = port ?? resolvePort();
5647
+ const data = await fetchJson(`http://127.0.0.1:${p}/proxy/health`);
5648
+ return data !== null && data.status === "ok";
5649
+ }
5650
+ async function getServerHealth(port) {
5651
+ const p = port ?? resolvePort();
5652
+ return fetchJson(`http://127.0.0.1:${p}/proxy/health`);
5653
+ }
5654
+ async function getServerOverview(port) {
5655
+ const p = port ?? resolvePort();
5656
+ return fetchJson(`http://127.0.0.1:${p}/dashboard/api/overview`, 3e3);
5657
+ }
5658
+ async function spawnServerInBackground(port) {
5659
+ const script = serverScriptPath();
5660
+ if (!existsSync4(script)) {
5661
+ console.error(`Server script not found: ${script}`);
5662
+ return { pid: 0, ok: false };
5663
+ }
5664
+ const logPath = logFilePath();
5665
+ mkdirSync2(dirname2(logPath), { recursive: true });
5666
+ const logFd = openSync(logPath, "a");
5667
+ const env = { ...process.env, PARTY_PORT: String(port) };
5668
+ const proc = spawn2(process.execPath, [script], {
5669
+ stdio: ["ignore", logFd, logFd],
5670
+ detached: true,
5671
+ windowsHide: true,
5672
+ env
5673
+ });
5674
+ proc.unref();
5675
+ const pid = proc.pid;
5676
+ writePid(pid);
5677
+ proc.on("error", (err) => {
5678
+ console.error(`Failed to start server: ${err.message}`);
5679
+ });
5680
+ return { pid, ok: true };
5681
+ }
5682
+ async function waitForServerReady(port, timeoutMs = 1e4) {
5683
+ const deadline = Date.now() + timeoutMs;
5684
+ while (Date.now() < deadline) {
5685
+ if (await isServerHealthy(port)) return true;
5686
+ const pid = readPid();
5687
+ if (pid !== null && !isProcessRunning(pid)) {
5688
+ return false;
5689
+ }
5690
+ await sleep(500);
5691
+ }
5692
+ return false;
5693
+ }
5694
+ function killServer(pid) {
5695
+ try {
5696
+ if (process.platform === "win32") {
5697
+ execSync3(`taskkill /F /T /PID ${pid}`, { stdio: "ignore", windowsHide: true });
5698
+ } else {
5699
+ process.kill(pid, "SIGTERM");
5700
+ }
5701
+ } catch {
5702
+ }
5703
+ }
5704
+ function parseStartArgs(args2) {
5705
+ let daemon = false;
5706
+ let port = null;
5707
+ for (let i = 0; i < args2.length; i++) {
5708
+ if (args2[i] === "-d" || args2[i] === "--daemon") {
5709
+ daemon = true;
5710
+ } else if (args2[i] === "-p" || args2[i] === "--port") {
5711
+ const val = args2[++i];
5712
+ if (val) port = parseInt(val, 10);
5713
+ } else if (args2[i].startsWith("--port=")) {
5714
+ port = parseInt(args2[i].split("=")[1], 10);
5715
+ }
5716
+ }
5717
+ return { daemon, port };
5718
+ }
5719
+ function sleep(ms) {
5720
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
5721
+ }
5722
+
5399
5723
  // src/cli/start-server.ts
5400
- async function startServer() {
5724
+ async function startServer(args2 = []) {
5725
+ const opts = parseStartArgs(args2);
5726
+ const port = opts.port ?? resolvePort();
5727
+ if (opts.daemon) {
5728
+ await startDaemon(port);
5729
+ } else {
5730
+ await startForeground();
5731
+ }
5732
+ }
5733
+ async function startForeground() {
5734
+ writePid(process.pid);
5735
+ process.on("exit", () => {
5736
+ removePidFile();
5737
+ });
5401
5738
  await Promise.resolve().then(() => (init_server(), server_exports));
5402
5739
  }
5740
+ async function startDaemon(port) {
5741
+ if (await isServerHealthy(port)) {
5742
+ const pid2 = readPid();
5743
+ console.log(`Party Server is already running (PID ${pid2 ?? "unknown"}, port ${port}).`);
5744
+ process.exit(0);
5745
+ }
5746
+ const existingPid = readPid();
5747
+ if (existingPid !== null && !isProcessRunning(existingPid)) {
5748
+ removePidFile();
5749
+ }
5750
+ const { pid, ok } = await spawnServerInBackground(port);
5751
+ if (!ok) {
5752
+ process.exit(1);
5753
+ }
5754
+ console.log(`Starting Party Server in background (PID ${pid})...`);
5755
+ const ready = await waitForServerReady(port);
5756
+ if (ready) {
5757
+ console.log(`Party Server is running on port ${port}.`);
5758
+ console.log(` Dashboard: http://127.0.0.1:${port}/dashboard`);
5759
+ console.log(` Logs: ${logFilePath()}`);
5760
+ console.log(` Use 'open-party stop' to stop the server.`);
5761
+ } else {
5762
+ console.error("Party Server failed to start within timeout.");
5763
+ console.error(`Check logs: ${logFilePath()}`);
5764
+ process.exit(1);
5765
+ }
5766
+ }
5767
+
5768
+ // src/cli/stop-server.ts
5769
+ async function stopServer() {
5770
+ const pid = readPid();
5771
+ if (pid === null) {
5772
+ const port2 = resolvePort();
5773
+ const healthy = await isServerHealthy(port2);
5774
+ if (healthy) {
5775
+ console.log(`No PID file found, but a server is responding on port ${port2}.`);
5776
+ console.log("It may have been started manually. Kill it by port or process name.");
5777
+ } else {
5778
+ console.log("Party Server is not running (no PID file found).");
5779
+ }
5780
+ return;
5781
+ }
5782
+ if (!isProcessRunning(pid)) {
5783
+ console.log(`Stale PID file found (PID ${pid} is not running). Cleaning up.`);
5784
+ removePidFile();
5785
+ return;
5786
+ }
5787
+ console.log(`Stopping Party Server (PID ${pid})...`);
5788
+ killServer(pid);
5789
+ removePidFile();
5790
+ const port = resolvePort();
5791
+ const stillUp = await isServerHealthy(port);
5792
+ if (stillUp) {
5793
+ console.warn(`Process ${pid} was killed, but port ${port} is still responding.`);
5794
+ console.warn("Another process may be using this port.");
5795
+ } else {
5796
+ console.log("Party Server stopped.");
5797
+ }
5798
+ }
5799
+
5800
+ // src/cli/status.ts
5801
+ async function statusCommand() {
5802
+ const port = resolvePort();
5803
+ const pid = readPid();
5804
+ let processAlive = false;
5805
+ if (pid !== null) {
5806
+ processAlive = isProcessRunning(pid);
5807
+ }
5808
+ const healthy = await isServerHealthy(port);
5809
+ if (healthy) {
5810
+ const health = await getServerHealth(port);
5811
+ const overview = await getServerOverview(port);
5812
+ console.log("Party Server is running.");
5813
+ console.log(` PID: ${pid ?? "unknown (no PID file)"}`);
5814
+ console.log(` Port: ${port}`);
5815
+ console.log(` Tailscale IP: ${health?.tailscale_ip ?? "N/A"}`);
5816
+ console.log(` Hostname: ${health?.hostname ?? "N/A"}`);
5817
+ if (overview) {
5818
+ const server = overview.server;
5819
+ const agents = overview.agents;
5820
+ if (server?.uptime_seconds != null) {
5821
+ const uptime = server.uptime_seconds;
5822
+ const mins = Math.floor(uptime / 60);
5823
+ const secs = Math.floor(uptime % 60);
5824
+ console.log(` Uptime: ${mins}m ${secs}s`);
5825
+ }
5826
+ console.log(` Local agents: ${agents?.local_count ?? "N/A"}`);
5827
+ console.log(` Remote agents: ${agents?.remote_count ?? "N/A"}`);
5828
+ } else {
5829
+ console.log(` Local agents: ${health?.agent_count ?? "N/A"}`);
5830
+ }
5831
+ console.log(` Dashboard: http://127.0.0.1:${port}/dashboard`);
5832
+ } else if (processAlive && pid !== null) {
5833
+ console.log("Party Server process exists but is not responding on health endpoint.");
5834
+ console.log(` PID: ${pid}`);
5835
+ console.log(" The server may be starting up or has crashed.");
5836
+ console.log(` Logs: ~/.open-party/server.log`);
5837
+ } else if (pid !== null) {
5838
+ console.log("Party Server is NOT running (stale PID file).");
5839
+ console.log(` PID file references PID ${pid}, which is not a live process.`);
5840
+ console.log(" Use: open-party start to start the server.");
5841
+ } else {
5842
+ console.log("Party Server is NOT running.");
5843
+ console.log(" No PID file found.");
5844
+ console.log(" Use: open-party start to start the server.");
5845
+ }
5846
+ }
5403
5847
 
5404
5848
  // src/cli/index.ts
5849
+ function showHelp() {
5850
+ console.log(`Usage: open-party <command> [options]
5851
+
5852
+ Commands:
5853
+ start Start the Party Server (default when no command given)
5854
+ stop Stop the Party Server
5855
+ status Show server status
5856
+ setup Interactive setup wizard (Tailscale + agent plugins)
5857
+ help Show this help message
5858
+
5859
+ Options for 'start':
5860
+ -d, --daemon Run in background (daemon mode)
5861
+ -p, --port <port> Override port (default: 8000, env: PARTY_PORT)
5862
+
5863
+ Examples:
5864
+ open-party Start server in foreground
5865
+ open-party start Start server in foreground
5866
+ open-party start -d Start server in background
5867
+ open-party start -d -p 9000 Start server in background on port 9000
5868
+ open-party stop Stop the server
5869
+ open-party status Check if the server is running`);
5870
+ }
5405
5871
  var args = process.argv.slice(2);
5406
5872
  var command = args[0] ?? "start";
5873
+ var commandArgs = args.slice(1);
5407
5874
  async function main2() {
5408
5875
  switch (command) {
5409
5876
  case "setup":
5410
5877
  await setupCommand();
5411
5878
  break;
5412
5879
  case "start":
5413
- await startServer();
5880
+ await startServer(commandArgs);
5881
+ break;
5882
+ case "stop":
5883
+ await stopServer();
5884
+ break;
5885
+ case "status":
5886
+ await statusCommand();
5887
+ break;
5888
+ case "help":
5889
+ case "--help":
5890
+ case "-h":
5891
+ showHelp();
5414
5892
  break;
5415
5893
  default:
5416
- console.log(`Usage: npx open-party [setup|start]`);
5417
- console.log("");
5418
- console.log("Commands:");
5419
- console.log(" setup Interactive setup wizard (Tailscale + agent plugins)");
5420
- console.log(" start Start the Party Server (default)");
5894
+ console.log(`Unknown command: ${command}
5895
+ `);
5896
+ showHelp();
5421
5897
  process.exit(1);
5422
5898
  }
5423
5899
  }