linear-grab-bridge 0.13.0 → 0.18.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.
@@ -28,7 +28,7 @@ const flag = (name, fallback) => {
28
28
  const PORT = Number(flag('--port', '4577'));
29
29
  const DIR = flag('--dir', process.cwd());
30
30
  const CLAUDE_BIN = flag('--claude', 'claude');
31
- const VERSION = '0.13.0';
31
+ const VERSION = '0.18.0';
32
32
 
33
33
  /** Best-effort command runner (git/gh introspection). Never throws. */
34
34
  function run(cmd, args, cwd = DIR) {
@@ -68,6 +68,9 @@ const tasks = new Map();
68
68
  /** Linear Authorization header supplied by the panel (for the media proxy). */
69
69
  let linearAuth = null;
70
70
 
71
+ /** PR state cache (gh pr view) — 60s TTL. */
72
+ const prStatusCache = new Map();
73
+
71
74
  // ---- persistence -----------------------------------------------------------
72
75
 
73
76
  const HISTORY_DIR = join(homedir(), '.linear-grab');
@@ -579,6 +582,44 @@ createServer(async (req, res) => {
579
582
  });
580
583
  return res.end(Buffer.from(await upstream.arrayBuffer()));
581
584
  }
585
+ // Real PR states (OPEN/MERGED/CLOSED) via gh — the panel can't know
586
+ // merge status from Linear attachments alone.
587
+ if (req.method === 'POST' && url.pathname === '/pr/status') {
588
+ const body = await readBody(req);
589
+ const urls = (Array.isArray(body.urls) ? body.urls : []).slice(0, 20);
590
+ const statuses = {};
591
+ const previews = {};
592
+ await Promise.all(
593
+ urls.map(async (u) => {
594
+ const prUrl = String(u);
595
+ if (!/^https:\/\/github\.com\/[\w.-]+\/[\w.-]+\/pull\/\d+$/.test(prUrl)) return;
596
+ const cached = prStatusCache.get(prUrl);
597
+ if (cached && Date.now() - cached.at < 60_000) {
598
+ statuses[prUrl] = cached.state;
599
+ if (cached.preview) previews[prUrl] = cached.preview;
600
+ return;
601
+ }
602
+ // comments carry the Vercel bot's preview link; the PR body often
603
+ // has it too (agents embed it per the completion gate).
604
+ const out = await run('gh', ['pr', 'view', prUrl, '--json', 'state,comments,body']);
605
+ try {
606
+ const parsed = JSON.parse(out ?? '');
607
+ const state = parsed.state;
608
+ const haystack =
609
+ (parsed.comments ?? []).map((c) => c.body ?? '').join('\n') + '\n' + (parsed.body ?? '');
610
+ const preview = (haystack.match(/https:\/\/[a-z0-9-]+\.vercel\.app[^\s)"\]]*/i) ?? [])[0] ?? null;
611
+ if (state) {
612
+ prStatusCache.set(prUrl, { state, preview, at: Date.now() });
613
+ statuses[prUrl] = state;
614
+ if (preview) previews[prUrl] = preview;
615
+ }
616
+ } catch {
617
+ /* gh missing / not accessible */
618
+ }
619
+ }),
620
+ );
621
+ return json(res, 200, { statuses, previews });
622
+ }
582
623
  // Fast-merge: after reviewing the demo, one click merges the PR via gh.
583
624
  if (req.method === 'POST' && url.pathname === '/pr/merge') {
584
625
  const body = await readBody(req);
@@ -587,6 +628,7 @@ createServer(async (req, res) => {
587
628
  return json(res, 400, { error: 'invalid PR url' });
588
629
  }
589
630
  const out = await run('gh', ['pr', 'merge', prUrl, '--squash']);
631
+ prStatusCache.delete(prUrl);
590
632
  if (out === null) {
591
633
  return json(res, 502, { error: 'gh pr merge failed — check gh auth / merge conflicts / required checks' });
592
634
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linear-grab-bridge",
3
- "version": "0.13.0",
3
+ "version": "0.18.0",
4
4
  "description": "Local bridge for Linear Grab — delegate issues from the browser panel to headless Claude Code sessions running in your repo, with live status and an upload relay.",
5
5
  "type": "module",
6
6
  "bin": {