patchcord 0.3.56 → 0.3.57

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 +145 -31
  2. package/package.json +1 -1
package/bin/patchcord.mjs CHANGED
@@ -480,26 +480,21 @@ if (!cmd || cmd === "install" || cmd === "agent") {
480
480
  let token = "";
481
481
  let identity = "";
482
482
  let serverUrl = "https://mcp.patchcord.dev";
483
+ let apiUrl = "https://api.patchcord.dev";
484
+ let clientType = "";
483
485
 
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.");
492
- rl.close();
493
- process.exit(1);
494
- }
486
+ // --token bypass for power users / CI
487
+ const tokenFlag = flags.find(f => f.startsWith("--token="))?.split("=")[1]
488
+ || (flags.includes("--token") ? flags[flags.indexOf("--token") + 1] : "");
495
489
 
490
+ if (tokenFlag) {
491
+ token = tokenFlag.trim();
496
492
  if (!isSafeToken(token)) {
497
- console.log(` ${red}✗${r} Invalid token format`);
493
+ console.error("Invalid token format.");
498
494
  rl.close();
499
495
  process.exit(1);
500
496
  }
501
-
502
- console.log("Validating...");
497
+ console.log("Validating token...");
503
498
  const validateResp = run(`curl -sf --max-time 5 -H "Authorization: Bearer ${token}" "${serverUrl}/api/inbox?limit=0"`);
504
499
  if (validateResp) {
505
500
  try {
@@ -509,30 +504,149 @@ if (!cmd || cmd === "install" || cmd === "agent") {
509
504
  } catch {}
510
505
  }
511
506
  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();
516
- process.exit(1);
517
- }
507
+ console.error("Token not recognized.");
508
+ rl.close();
509
+ process.exit(1);
518
510
  }
519
- }
511
+ rl.close();
512
+ } else {
513
+ // Browser connect flow
514
+ rl.close();
520
515
 
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();
516
+ function canOpenBrowser() {
517
+ if (process.env.SSH_CLIENT || process.env.SSH_TTY) return false;
518
+ if (!process.env.DISPLAY && process.platform === "linux") return false;
519
+ if (flags.includes("--no-browser")) return false;
520
+ return true;
521
+ }
522
+
523
+ function openBrowser(url) {
524
+ try {
525
+ if (process.platform === "darwin") execSync(`open "${url}"`, { stdio: "ignore" });
526
+ else if (process.platform === "win32") execSync(`start "" "${url}"`, { stdio: "ignore" });
527
+ else execSync(`xdg-open "${url}"`, { stdio: "ignore" });
528
+ return true;
529
+ } catch { return false; }
530
+ }
531
+
532
+ // Create session
533
+ let sessionId = "";
534
+ try {
535
+ const resp = run(`curl -sf --max-time 10 -X POST "${apiUrl}/api/connect/session" -H "Content-Type: application/json" -d '{"tool":"${choice}"}'`);
536
+ if (resp) {
537
+ const data = JSON.parse(resp);
538
+ sessionId = data.session_id || "";
539
+ }
540
+ } catch {}
541
+
542
+ if (!sessionId) {
543
+ // Fallback to manual token paste if connect API unavailable
544
+ console.log(`\n${dim}Browser connect unavailable. Paste token manually.${r}`);
545
+ console.log(`${dim}Get your token at:${r} ${cyan}https://patchcord.dev/console${r}`);
546
+ const { createInterface: createRL2 } = await import("readline");
547
+ const rl2 = createRL2({ input: process.stdin, output: process.stdout });
548
+ const ask2 = (q) => new Promise((resolve) => rl2.question(q, resolve));
549
+ token = (await ask2(`\n${bold}Paste your agent token:${r} `)).trim();
550
+ rl2.close();
551
+ if (!token || !isSafeToken(token)) {
552
+ console.error("Invalid token.");
528
553
  process.exit(1);
529
554
  }
530
- serverUrl = url;
555
+ const validateResp = run(`curl -sf --max-time 5 -H "Authorization: Bearer ${token}" "${serverUrl}/api/inbox?limit=0"`);
556
+ if (validateResp) {
557
+ try {
558
+ const data = JSON.parse(validateResp);
559
+ identity = `${data.agent_id}@${data.namespace_id}`;
560
+ console.log(` ${green}✓${r} ${bold}${identity}${r}`);
561
+ } catch {}
562
+ }
563
+ if (!identity) {
564
+ console.error("Token not recognized.");
565
+ process.exit(1);
566
+ }
567
+ } else {
568
+ // Open browser or show URL
569
+ const connectUrl = `https://patchcord.dev/connect?session=${sessionId}`;
570
+
571
+ if (canOpenBrowser()) {
572
+ const opened = openBrowser(connectUrl);
573
+ if (opened) {
574
+ console.log(`\n ${green}✓${r} Browser opened.`);
575
+ } else {
576
+ console.log(`\n ${dim}Could not open browser. Open this URL manually:${r}`);
577
+ console.log(`\n ${cyan}${connectUrl}${r}\n`);
578
+ }
579
+ } else {
580
+ console.log(`\n ${dim}Can't open a browser on this machine.${r}`);
581
+ console.log(` ${dim}Open this URL on any device:${r}`);
582
+ console.log(`\n ${cyan}${connectUrl}${r}\n`);
583
+ }
584
+
585
+ console.log(` ${dim}⏳ Waiting for you to complete setup in the browser...${r}`);
586
+ console.log(` ${dim} (press Ctrl+C to cancel)${r}\n`);
587
+
588
+ // SSE listener — wait for session completion
589
+ const http = await import("https");
590
+ const sseResult = await new Promise((resolve, reject) => {
591
+ const timeout = setTimeout(() => {
592
+ reject(new Error("Session expired. Run npx patchcord@latest again."));
593
+ }, 5 * 60 * 1000);
594
+
595
+ function connect() {
596
+ const req = http.get(`${apiUrl}/api/connect/session/${sessionId}/wait`, {
597
+ headers: { "Accept": "text/event-stream" },
598
+ }, (res) => {
599
+ if (res.statusCode !== 200) {
600
+ clearTimeout(timeout);
601
+ reject(new Error(`Server returned ${res.statusCode}`));
602
+ return;
603
+ }
604
+ let buffer = "";
605
+ res.on("data", (chunk) => {
606
+ buffer += chunk.toString();
607
+ const lines = buffer.split("\n");
608
+ buffer = lines.pop();
609
+ for (const line of lines) {
610
+ if (line.startsWith("data: ")) {
611
+ try {
612
+ const payload = JSON.parse(line.slice(6));
613
+ if (payload.error) {
614
+ clearTimeout(timeout);
615
+ reject(new Error(payload.error));
616
+ return;
617
+ }
618
+ if (payload.token) {
619
+ clearTimeout(timeout);
620
+ resolve(payload);
621
+ return;
622
+ }
623
+ } catch {}
624
+ }
625
+ }
626
+ });
627
+ res.on("end", () => {
628
+ // Connection dropped — retry
629
+ setTimeout(connect, 2000);
630
+ });
631
+ res.on("error", () => {
632
+ setTimeout(connect, 2000);
633
+ });
634
+ });
635
+ req.on("error", () => {
636
+ setTimeout(connect, 2000);
637
+ });
638
+ }
639
+
640
+ connect();
641
+ });
642
+
643
+ token = sseResult.token;
644
+ identity = `${sseResult.agent_id}@${sseResult.namespace_id}`;
645
+ clientType = sseResult.client_type || "";
646
+ console.log(` ${green}✓${r} ${bold}${identity}${r} connected.`);
531
647
  }
532
648
  }
533
649
 
534
- rl.close();
535
-
536
650
  const hostname = run("hostname -s") || run("hostname") || "unknown";
537
651
 
538
652
  if (isCursor) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "patchcord",
3
- "version": "0.3.56",
3
+ "version": "0.3.57",
4
4
  "description": "Cross-machine agent messaging for Claude Code and Codex",
5
5
  "author": "ppravdin",
6
6
  "license": "MIT",