patchcord 0.3.56 → 0.3.58

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 (2) hide show
  1. package/bin/patchcord.mjs +175 -251
  2. package/package.json +1 -1
package/bin/patchcord.mjs CHANGED
@@ -77,8 +77,8 @@ if (cmd === "plugin-path") {
77
77
  }
78
78
 
79
79
  // ── main flow: global setup + project setup (or just install/agent for back-compat) ──
80
- if (!cmd || cmd === "install" || cmd === "agent") {
81
- const flags = process.argv.slice(3);
80
+ if (!cmd || cmd === "install" || cmd === "agent" || cmd === "--token" || cmd === "--no-browser") {
81
+ const flags = cmd?.startsWith("--") ? process.argv.slice(2) : process.argv.slice(3);
82
82
  const fullStatusline = flags.includes("--full");
83
83
  const { readFileSync, writeFileSync } = await import("fs");
84
84
 
@@ -252,254 +252,46 @@ if (!cmd || cmd === "install" || cmd === "agent") {
252
252
  const rl = createInterface({ input: process.stdin, output: process.stdout });
253
253
  const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
254
254
 
255
- console.log(`\n${bold}Which tool are you setting up?${r}\n`);
256
- console.log(` ${cyan}1.${r} Claude Code`);
257
- console.log(` ${cyan}2.${r} Codex CLI`);
258
- console.log(` ${cyan}3.${r} Cursor`);
259
- console.log(` ${cyan}4.${r} Windsurf`);
260
- console.log(` ${cyan}5.${r} Gemini CLI`);
261
- console.log(` ${cyan}6.${r} VS Code (Copilot)`);
262
- console.log(` ${cyan}7.${r} Zed`);
263
- console.log(` ${cyan}8.${r} OpenCode\n`);
264
-
265
- const choice = (await ask(`${dim}Choose (1-8):${r} `)).trim();
266
- const isCodex = choice === "2";
267
- const isCursor = choice === "3";
268
- const isWindsurf = choice === "4";
269
- const isGemini = choice === "5";
270
- const isVSCode = choice === "6";
271
- const isZed = choice === "7";
272
- const isOpenCode = choice === "8";
273
-
274
- if (!["1", "2", "3", "4", "5", "6", "7", "8"].includes(choice)) {
275
- console.error("Invalid choice.");
276
- rl.close();
277
- process.exit(1);
278
- }
255
+ // Tool picker only shown for --token bypass. Browser flow gets tool from web.
256
+ let choice = "";
279
257
 
280
- if (isWindsurf || isGemini || isZed) {
281
- const toolLabel = isZed ? "Zed" : isWindsurf ? "Windsurf" : "Gemini CLI";
282
- console.log(`\n ${yellow}Note: ${toolLabel} uses global config applies to all projects.${r}`);
283
- } else {
284
- const folderType = detectFolder(cwd);
285
- const folderName = cwd.split("/").pop() || cwd.split("\\").pop() || cwd;
286
-
287
- if (folderType === "HOME") {
288
- console.log(`\n ${red}✗ You're in your home folder.${r}`);
289
- console.log(` ${yellow}Patchcord must be installed inside a project folder —${r}`);
290
- console.log(` ${yellow}the folder where your agent actually runs.${r}`);
291
- console.log(` ${dim}cd into your project first, then run this again.${r}`);
292
- rl.close();
293
- process.exit(0);
294
- } else if (folderType === "CONTAINER") {
295
- console.log(`\n ${yellow}⚠ This looks like a projects container, not a project.${r}`);
296
- console.log(` ${dim}${cwd}${r}`);
297
- console.log(` ${dim}Patchcord should be installed inside a project, not the folder above it.${r}`);
298
- const proceed = (await ask(` ${dim}Set up here anyway? (y/N):${r} `)).trim().toLowerCase();
299
- if (proceed !== "y" && proceed !== "yes") {
300
- console.log(` ${dim}cd into your project and run this again.${r}`);
301
- rl.close();
302
- process.exit(0);
303
- }
304
- }
258
+ const CLIENT_TYPE_MAP = {
259
+ "claude_code": "1", "codex": "2", "cursor": "3", "windsurf": "4",
260
+ "gemini": "5", "vscode": "6", "zed": "7", "opencode": "8",
261
+ };
305
262
 
306
- console.log(`\n ${dim}Agent identity:${r} ${bold}${folderName}${r}`);
307
- console.log(` ${dim}Folder:${r} ${cwd}`);
308
- }
309
263
 
310
264
 
311
- // Check if already configured
312
- if (choice === "1") {
313
- const mcpPath = join(cwd, ".mcp.json");
314
- if (existsSync(mcpPath)) {
315
- try {
316
- const existing = JSON.parse(readFileSync(mcpPath, "utf-8"));
317
- if (existing.mcpServers?.patchcord) {
318
- console.log(`\n ${yellow}⚠ Claude Code already configured in this project${r}`);
319
- console.log(` ${dim}${mcpPath}${r}`);
320
- const replace = (await ask(` ${dim}Replace? (y/N):${r} `)).trim().toLowerCase();
321
- if (replace !== "y" && replace !== "yes") {
322
- console.log("Keeping existing config.");
323
- rl.close();
324
- process.exit(0);
325
- }
326
- }
327
- } catch {}
328
- }
329
- } else if (isCursor) {
330
- const cursorPath = join(cwd, ".cursor", "mcp.json");
331
- if (existsSync(cursorPath)) {
332
- try {
333
- const existing = JSON.parse(readFileSync(cursorPath, "utf-8"));
334
- if (existing.mcpServers?.patchcord) {
335
- console.log(`\n ${yellow}⚠ Cursor already configured in this project${r}`);
336
- console.log(` ${dim}${cursorPath}${r}`);
337
- const replace = (await ask(` ${dim}Replace? (y/N):${r} `)).trim().toLowerCase();
338
- if (replace !== "y" && replace !== "yes") {
339
- console.log("Keeping existing config.");
340
- rl.close();
341
- process.exit(0);
342
- }
343
- }
344
- } catch {}
345
- }
346
- // Warn about global config conflict
347
- const globalCursor = join(HOME, ".cursor", "mcp.json");
348
- if (existsSync(globalCursor)) {
349
- try {
350
- const global = JSON.parse(readFileSync(globalCursor, "utf-8"));
351
- if (global.mcpServers?.patchcord) {
352
- console.log(`\n ${yellow}⚠ Patchcord is also configured globally in Cursor${r}`);
353
- console.log(` ${dim}${globalCursor}${r}`);
354
- console.log(` ${yellow}Having both global AND per-project will cause duplicate tool calls.${r}`);
355
- console.log(` ${dim}Remove patchcord from global config: Cursor Settings → MCP → remove patchcord${r}`);
356
- }
357
- } catch {}
358
- }
359
- } else if (isWindsurf) {
360
- const wsPath = join(HOME, ".codeium", "windsurf", "mcp_config.json");
361
- if (existsSync(wsPath)) {
362
- try {
363
- const content = readFileSync(wsPath, "utf-8").trim();
364
- const existing = content ? JSON.parse(content) : {};
365
- if (existing.mcpServers?.patchcord) {
366
- console.log(`\n ${yellow}⚠ Windsurf already configured${r}`);
367
- console.log(` ${dim}${wsPath}${r}`);
368
- const replace = (await ask(` ${dim}Replace? (y/N):${r} `)).trim().toLowerCase();
369
- if (replace !== "y" && replace !== "yes") {
370
- console.log("Keeping existing config.");
371
- rl.close();
372
- process.exit(0);
373
- }
374
- }
375
- } catch {}
376
- }
377
- } else if (isGemini) {
378
- const geminiPath = join(HOME, ".gemini", "settings.json");
379
- if (existsSync(geminiPath)) {
380
- try {
381
- const existing = safeReadJson(geminiPath) || {};
382
- if (existing.mcpServers?.patchcord) {
383
- console.log(`\n ${yellow}⚠ Gemini CLI already configured${r}`);
384
- console.log(` ${dim}${geminiPath}${r}`);
385
- const replace = (await ask(` ${dim}Replace? (y/N):${r} `)).trim().toLowerCase();
386
- if (replace !== "y" && replace !== "yes") {
387
- console.log("Keeping existing config.");
388
- rl.close();
389
- process.exit(0);
390
- }
391
- }
392
- } catch {}
393
- }
394
- } else if (isVSCode) {
395
- const vscodePath = join(cwd, ".vscode", "mcp.json");
396
- if (existsSync(vscodePath)) {
397
- try {
398
- const existing = JSON.parse(readFileSync(vscodePath, "utf-8"));
399
- if (existing.servers?.patchcord) {
400
- console.log(`\n ${yellow}⚠ VS Code already configured in this project${r}`);
401
- console.log(` ${dim}${vscodePath}${r}`);
402
- const replace = (await ask(` ${dim}Replace? (y/N):${r} `)).trim().toLowerCase();
403
- if (replace !== "y" && replace !== "yes") {
404
- console.log("Keeping existing config.");
405
- rl.close();
406
- process.exit(0);
407
- }
408
- }
409
- } catch {}
410
- }
411
- } else if (isZed) {
412
- const zedPath = process.platform === "darwin"
413
- ? join(HOME, "Library", "Application Support", "Zed", "settings.json")
414
- : join(HOME, ".config", "zed", "settings.json");
415
- if (existsSync(zedPath)) {
416
- try {
417
- const existing = safeReadJson(zedPath) || {};
418
- if (existing.context_servers?.patchcord) {
419
- console.log(`\n ${yellow}⚠ Zed already configured${r}`);
420
- console.log(` ${dim}${zedPath}${r}`);
421
- const replace = (await ask(` ${dim}Replace? (y/N):${r} `)).trim().toLowerCase();
422
- if (replace !== "y" && replace !== "yes") {
423
- console.log("Keeping existing config.");
424
- rl.close();
425
- process.exit(0);
426
- }
427
- }
428
- } catch {}
429
- }
430
- } else if (isOpenCode) {
431
- const ocPath = join(cwd, "opencode.json");
432
- if (existsSync(ocPath)) {
433
- try {
434
- const existing = JSON.parse(readFileSync(ocPath, "utf-8"));
435
- if (existing.mcp?.patchcord) {
436
- console.log(`\n ${yellow}⚠ OpenCode already configured in this project${r}`);
437
- console.log(` ${dim}${ocPath}${r}`);
438
- const replace = (await ask(` ${dim}Replace? (y/N):${r} `)).trim().toLowerCase();
439
- if (replace !== "y" && replace !== "yes") {
440
- console.log("Keeping existing config.");
441
- rl.close();
442
- process.exit(0);
443
- }
444
- }
445
- } catch {}
446
- }
447
- } else if (isCodex) {
448
- // Check global config for stale patchcord MCP — user may have run installer in ~ by mistake
449
- const globalCodexConfig = join(HOME, ".codex", "config.toml");
450
- if (existsSync(globalCodexConfig)) {
451
- const globalContent = readFileSync(globalCodexConfig, "utf-8");
452
- if (globalContent.includes("[mcp_servers.patchcord-codex]")) {
453
- console.log(`\n ${red}⚠ Patchcord is in your GLOBAL Codex config!${r}`);
454
- console.log(` ${dim}${globalCodexConfig}${r}`);
455
- console.log(` ${yellow}This overrides per-project config and causes conflicts.${r}`);
456
- const cleanGlobal = (await ask(` ${dim}Remove patchcord from global config? (Y/n):${r} `)).trim().toLowerCase();
457
- if (cleanGlobal !== "n" && cleanGlobal !== "no") {
458
- const cleaned = globalContent.replace(/\[mcp_servers\.patchcord\]\n(?:(?!\[)[^\n]*\n?)*/g, "").replace(/\n{3,}/g, "\n\n").trim();
459
- writeFileSync(globalCodexConfig, cleaned + "\n");
460
- console.log(` ${green}✓${r} Removed from global config`);
461
- }
462
- }
463
- }
464
- const configPath = join(cwd, ".codex", "config.toml");
465
- if (existsSync(configPath)) {
466
- const content = readFileSync(configPath, "utf-8");
467
- if (content.includes("[mcp_servers.patchcord-codex]")) {
468
- console.log(`\n ${yellow}⚠ Codex CLI already configured in this project${r}`);
469
- console.log(` ${dim}${configPath}${r}`);
470
- const replace = (await ask(` ${dim}Replace? (y/N):${r} `)).trim().toLowerCase();
471
- if (replace !== "y" && replace !== "yes") {
472
- console.log("Keeping existing config.");
473
- rl.close();
474
- process.exit(0);
475
- }
476
- }
477
- }
478
- }
479
-
480
265
  let token = "";
481
266
  let identity = "";
482
267
  let serverUrl = "https://mcp.patchcord.dev";
483
-
484
- console.log(`\n${dim}Get your token at:${r} ${cyan}https://patchcord.dev/console${r}`);
485
- console.log(`${dim}Create a project → Add agent → Copy token${r}`);
486
-
487
- while (!identity) {
488
- token = (await ask(`\n${bold}Paste your agent token:${r} `)).trim();
489
-
490
- if (!token) {
491
- console.error("Token is required. Get one from your patchcord dashboard.");
268
+ let apiUrl = "https://api.patchcord.dev";
269
+ let clientType = "";
270
+
271
+ // --token bypass for power users / CI
272
+ const tokenFlag = flags.find(f => f.startsWith("--token="))?.split("=")[1]
273
+ || (flags.includes("--token") ? flags[flags.indexOf("--token") + 1] : "");
274
+
275
+ if (tokenFlag) {
276
+ // --token bypass: need tool picker in terminal
277
+ console.log(`\n${bold}Which tool are you setting up?${r}\n`);
278
+ console.log(` ${cyan}1.${r} Claude Code ${cyan}5.${r} Gemini CLI`);
279
+ console.log(` ${cyan}2.${r} Codex CLI ${cyan}6.${r} VS Code`);
280
+ console.log(` ${cyan}3.${r} Cursor ${cyan}7.${r} Zed`);
281
+ console.log(` ${cyan}4.${r} Windsurf ${cyan}8.${r} OpenCode\n`);
282
+ choice = (await ask(`${dim}Choose (1-8):${r} `)).trim();
283
+ if (!["1","2","3","4","5","6","7","8"].includes(choice)) {
284
+ console.error("Invalid choice.");
492
285
  rl.close();
493
286
  process.exit(1);
494
287
  }
495
-
288
+ token = tokenFlag.trim();
496
289
  if (!isSafeToken(token)) {
497
- console.log(` ${red}✗${r} Invalid token format`);
290
+ console.error("Invalid token format.");
498
291
  rl.close();
499
292
  process.exit(1);
500
293
  }
501
-
502
- console.log("Validating...");
294
+ console.log("Validating token...");
503
295
  const validateResp = run(`curl -sf --max-time 5 -H "Authorization: Bearer ${token}" "${serverUrl}/api/inbox?limit=0"`);
504
296
  if (validateResp) {
505
297
  try {
@@ -509,29 +301,161 @@ if (!cmd || cmd === "install" || cmd === "agent") {
509
301
  } catch {}
510
302
  }
511
303
  if (!identity) {
512
- console.log(` ${red}✗${r} Token not recognized`);
513
- const retry = (await ask(`${dim}Try again? (Y/n):${r} `)).trim().toLowerCase();
514
- if (retry === "n" || retry === "no") {
515
- rl.close();
304
+ console.error("Token not recognized.");
305
+ rl.close();
306
+ process.exit(1);
307
+ }
308
+ rl.close();
309
+ } else {
310
+ // Browser connect flow
311
+ rl.close();
312
+
313
+ function canOpenBrowser() {
314
+ if (process.env.SSH_CLIENT || process.env.SSH_TTY) return false;
315
+ if (!process.env.DISPLAY && process.platform === "linux") return false;
316
+ if (flags.includes("--no-browser")) return false;
317
+ return true;
318
+ }
319
+
320
+ function openBrowser(url) {
321
+ try {
322
+ if (process.platform === "darwin") execSync(`open "${url}"`, { stdio: "ignore" });
323
+ else if (process.platform === "win32") execSync(`start "" "${url}"`, { stdio: "ignore" });
324
+ else execSync(`xdg-open "${url}"`, { stdio: "ignore" });
325
+ return true;
326
+ } catch { return false; }
327
+ }
328
+
329
+ // Create session
330
+ let sessionId = "";
331
+ try {
332
+ const resp = run(`curl -sf --max-time 10 -X POST "${apiUrl}/api/connect/session" -H "Content-Type: application/json" -d '{"tool":"${choice}"}'`);
333
+ if (resp) {
334
+ const data = JSON.parse(resp);
335
+ sessionId = data.session_id || "";
336
+ }
337
+ } catch {}
338
+
339
+ if (!sessionId) {
340
+ // Fallback to manual token paste if connect API unavailable
341
+ console.log(`\n${dim}Browser connect unavailable. Paste token manually.${r}`);
342
+ console.log(`${dim}Get your token at:${r} ${cyan}https://patchcord.dev/console${r}`);
343
+ const { createInterface: createRL2 } = await import("readline");
344
+ const rl2 = createRL2({ input: process.stdin, output: process.stdout });
345
+ const ask2 = (q) => new Promise((resolve) => rl2.question(q, resolve));
346
+ token = (await ask2(`\n${bold}Paste your agent token:${r} `)).trim();
347
+ rl2.close();
348
+ if (!token || !isSafeToken(token)) {
349
+ console.error("Invalid token.");
516
350
  process.exit(1);
517
351
  }
518
- }
519
- }
352
+ const validateResp = run(`curl -sf --max-time 5 -H "Authorization: Bearer ${token}" "${serverUrl}/api/inbox?limit=0"`);
353
+ if (validateResp) {
354
+ try {
355
+ const data = JSON.parse(validateResp);
356
+ identity = `${data.agent_id}@${data.namespace_id}`;
357
+ console.log(` ${green}✓${r} ${bold}${identity}${r}`);
358
+ } catch {}
359
+ }
360
+ if (!identity) {
361
+ console.error("Token not recognized.");
362
+ process.exit(1);
363
+ }
364
+ } else {
365
+ // Open browser or show URL
366
+ const connectUrl = `https://patchcord.dev/connect?session=${sessionId}`;
367
+
368
+ if (canOpenBrowser()) {
369
+ const opened = openBrowser(connectUrl);
370
+ if (opened) {
371
+ console.log(`\n ${green}✓${r} Browser opened.`);
372
+ } else {
373
+ console.log(`\n ${dim}Could not open browser. Open this URL manually:${r}`);
374
+ console.log(`\n ${cyan}${connectUrl}${r}\n`);
375
+ }
376
+ } else {
377
+ console.log(`\n ${dim}Can't open a browser on this machine.${r}`);
378
+ console.log(` ${dim}Open this URL on any device:${r}`);
379
+ console.log(`\n ${cyan}${connectUrl}${r}\n`);
380
+ }
381
+
382
+ console.log(` ${dim}⏳ Waiting for you to complete setup in the browser...${r}`);
383
+ console.log(` ${dim} (press Ctrl+C to cancel)${r}\n`);
384
+
385
+ // SSE listener — wait for session completion
386
+ const http = await import("https");
387
+ const sseResult = await new Promise((resolve, reject) => {
388
+ const timeout = setTimeout(() => {
389
+ reject(new Error("Session expired. Run npx patchcord@latest again."));
390
+ }, 5 * 60 * 1000);
391
+
392
+ function connect() {
393
+ const req = http.get(`${apiUrl}/api/connect/session/${sessionId}/wait`, {
394
+ headers: { "Accept": "text/event-stream" },
395
+ }, (res) => {
396
+ if (res.statusCode !== 200) {
397
+ clearTimeout(timeout);
398
+ reject(new Error(`Server returned ${res.statusCode}`));
399
+ return;
400
+ }
401
+ let buffer = "";
402
+ res.on("data", (chunk) => {
403
+ buffer += chunk.toString();
404
+ const lines = buffer.split("\n");
405
+ buffer = lines.pop();
406
+ for (const line of lines) {
407
+ if (line.startsWith("data: ")) {
408
+ try {
409
+ const payload = JSON.parse(line.slice(6));
410
+ if (payload.error) {
411
+ clearTimeout(timeout);
412
+ reject(new Error(payload.error));
413
+ return;
414
+ }
415
+ if (payload.token) {
416
+ clearTimeout(timeout);
417
+ resolve(payload);
418
+ return;
419
+ }
420
+ } catch {}
421
+ }
422
+ }
423
+ });
424
+ res.on("end", () => {
425
+ // Connection dropped — retry
426
+ setTimeout(connect, 2000);
427
+ });
428
+ res.on("error", () => {
429
+ setTimeout(connect, 2000);
430
+ });
431
+ });
432
+ req.on("error", () => {
433
+ setTimeout(connect, 2000);
434
+ });
435
+ }
436
+
437
+ connect();
438
+ });
520
439
 
521
- const customUrl = (await ask(`\n${dim}Custom server URL? (y/N):${r} `)).trim().toLowerCase();
522
- if (customUrl === "y" || customUrl === "yes") {
523
- const url = (await ask("Server URL: ")).trim();
524
- if (url) {
525
- if (!isSafeUrl(url)) {
526
- console.error("Invalid URL. Must start with https:// or http://");
527
- rl.close();
440
+ token = sseResult.token;
441
+ identity = `${sseResult.agent_id}@${sseResult.namespace_id}`;
442
+ clientType = sseResult.client_type || "";
443
+ choice = CLIENT_TYPE_MAP[clientType] || "";
444
+ if (!choice) {
445
+ console.error(`Unknown tool type: ${clientType}`);
528
446
  process.exit(1);
529
447
  }
530
- serverUrl = url;
448
+ console.log(` ${green}✓${r} ${bold}${identity}${r} connected.`);
531
449
  }
532
450
  }
533
451
 
534
- rl.close();
452
+ const isCodex = choice === "2";
453
+ const isCursor = choice === "3";
454
+ const isWindsurf = choice === "4";
455
+ const isGemini = choice === "5";
456
+ const isVSCode = choice === "6";
457
+ const isZed = choice === "7";
458
+ const isOpenCode = choice === "8";
535
459
 
536
460
  const hostname = run("hostname -s") || run("hostname") || "unknown";
537
461
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "version": "0.3.56",
3
+ "version": "0.3.58",
4
4
  "description": "Cross-machine agent messaging for Claude Code and Codex",
5
5
  "author": "ppravdin",
6
6
  "license": "MIT",