linear-grab-bridge 0.11.0 → 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.10.0';
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');
@@ -274,6 +277,8 @@ function ingest(task, event) {
274
277
  return;
275
278
  }
276
279
  if (event.type === 'assistant') {
280
+ // The stream tells us the ACTUAL model — surface it even for defaults.
281
+ if (event.message?.model) task.model = event.message.model;
277
282
  const usage = event.message?.usage;
278
283
  if (usage) {
279
284
  const context =
@@ -544,6 +549,49 @@ createServer(async (req, res) => {
544
549
  const task = await startTask(body);
545
550
  return json(res, 201, summary(task));
546
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
+ }
547
595
  // Upload proxy: browsers can't PUT to Linear's storage (no CORS there) —
548
596
  // this local process can. SSRF-guarded to Linear storage hosts.
549
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.11.0",
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": {