linear-grab-bridge 0.12.1 → 0.13.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.12.1';
31
+ const VERSION = '0.13.0';
32
32
 
33
33
  /** Best-effort command runner (git/gh introspection). Never throws. */
34
34
  function run(cmd, args, cwd = DIR) {
@@ -65,6 +65,9 @@ const IDLE_KILL_MS = 30 * 60_000; // free an idle interactive session after 30mi
65
65
  /** @type {Map<string, any>} */
66
66
  const tasks = new Map();
67
67
 
68
+ /** Linear Authorization header supplied by the panel (for the media proxy). */
69
+ let linearAuth = null;
70
+
68
71
  // ---- persistence -----------------------------------------------------------
69
72
 
70
73
  const HISTORY_DIR = join(homedir(), '.linear-grab');
@@ -546,6 +549,49 @@ createServer(async (req, res) => {
546
549
  const task = await startTask(body);
547
550
  return json(res, 201, summary(task));
548
551
  }
552
+ // Panel hands us its Linear auth so /fetch can display Linear-hosted media.
553
+ if (req.method === 'POST' && url.pathname === '/config') {
554
+ const body = await readBody(req);
555
+ if (typeof body.linearAuth === 'string') linearAuth = body.linearAuth;
556
+ return json(res, 200, { ok: true });
557
+ }
558
+ // Media proxy: uploads.linear.app requires Linear auth on every GET — an
559
+ // <img>/<video> tag can't send headers, this local process can.
560
+ if (req.method === 'GET' && url.pathname === '/fetch') {
561
+ const target = url.searchParams.get('url') ?? '';
562
+ let host = '';
563
+ try {
564
+ host = new URL(target).hostname;
565
+ } catch {
566
+ /* invalid */
567
+ }
568
+ if (!/(^|\.)uploads\.linear\.app$/.test(host)) {
569
+ return json(res, 400, { error: 'host not allowed' });
570
+ }
571
+ const upstream = await fetch(target, {
572
+ headers: linearAuth ? { Authorization: linearAuth } : {},
573
+ });
574
+ if (!upstream.ok) return json(res, upstream.status, { error: `upstream ${upstream.status}` });
575
+ res.writeHead(200, {
576
+ 'Content-Type': upstream.headers.get('content-type') ?? 'application/octet-stream',
577
+ 'Cache-Control': 'private, max-age=300',
578
+ ...CORS,
579
+ });
580
+ return res.end(Buffer.from(await upstream.arrayBuffer()));
581
+ }
582
+ // Fast-merge: after reviewing the demo, one click merges the PR via gh.
583
+ if (req.method === 'POST' && url.pathname === '/pr/merge') {
584
+ const body = await readBody(req);
585
+ const prUrl = String(body.url ?? '');
586
+ if (!/^https:\/\/github\.com\/[\w.-]+\/[\w.-]+\/pull\/\d+$/.test(prUrl)) {
587
+ return json(res, 400, { error: 'invalid PR url' });
588
+ }
589
+ const out = await run('gh', ['pr', 'merge', prUrl, '--squash']);
590
+ if (out === null) {
591
+ return json(res, 502, { error: 'gh pr merge failed — check gh auth / merge conflicts / required checks' });
592
+ }
593
+ return json(res, 200, { ok: true, output: out.slice(0, 500) });
594
+ }
549
595
  // Upload proxy: browsers can't PUT to Linear's storage (no CORS there) —
550
596
  // this local process can. SSRF-guarded to Linear storage hosts.
551
597
  if (req.method === 'POST' && url.pathname === '/put') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linear-grab-bridge",
3
- "version": "0.12.1",
3
+ "version": "0.13.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": {