linear-grab-bridge 0.12.1 → 0.15.1
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.
- package/linear-grab-bridge.mjs +80 -1
- package/package.json +1 -1
package/linear-grab-bridge.mjs
CHANGED
|
@@ -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.
|
|
31
|
+
const VERSION = '0.15.1';
|
|
32
32
|
|
|
33
33
|
/** Best-effort command runner (git/gh introspection). Never throws. */
|
|
34
34
|
function run(cmd, args, cwd = DIR) {
|
|
@@ -65,6 +65,12 @@ 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
|
+
|
|
71
|
+
/** PR state cache (gh pr view) — 60s TTL. */
|
|
72
|
+
const prStatusCache = new Map();
|
|
73
|
+
|
|
68
74
|
// ---- persistence -----------------------------------------------------------
|
|
69
75
|
|
|
70
76
|
const HISTORY_DIR = join(homedir(), '.linear-grab');
|
|
@@ -546,6 +552,79 @@ createServer(async (req, res) => {
|
|
|
546
552
|
const task = await startTask(body);
|
|
547
553
|
return json(res, 201, summary(task));
|
|
548
554
|
}
|
|
555
|
+
// Panel hands us its Linear auth so /fetch can display Linear-hosted media.
|
|
556
|
+
if (req.method === 'POST' && url.pathname === '/config') {
|
|
557
|
+
const body = await readBody(req);
|
|
558
|
+
if (typeof body.linearAuth === 'string') linearAuth = body.linearAuth;
|
|
559
|
+
return json(res, 200, { ok: true });
|
|
560
|
+
}
|
|
561
|
+
// Media proxy: uploads.linear.app requires Linear auth on every GET — an
|
|
562
|
+
// <img>/<video> tag can't send headers, this local process can.
|
|
563
|
+
if (req.method === 'GET' && url.pathname === '/fetch') {
|
|
564
|
+
const target = url.searchParams.get('url') ?? '';
|
|
565
|
+
let host = '';
|
|
566
|
+
try {
|
|
567
|
+
host = new URL(target).hostname;
|
|
568
|
+
} catch {
|
|
569
|
+
/* invalid */
|
|
570
|
+
}
|
|
571
|
+
if (!/(^|\.)uploads\.linear\.app$/.test(host)) {
|
|
572
|
+
return json(res, 400, { error: 'host not allowed' });
|
|
573
|
+
}
|
|
574
|
+
const upstream = await fetch(target, {
|
|
575
|
+
headers: linearAuth ? { Authorization: linearAuth } : {},
|
|
576
|
+
});
|
|
577
|
+
if (!upstream.ok) return json(res, upstream.status, { error: `upstream ${upstream.status}` });
|
|
578
|
+
res.writeHead(200, {
|
|
579
|
+
'Content-Type': upstream.headers.get('content-type') ?? 'application/octet-stream',
|
|
580
|
+
'Cache-Control': 'private, max-age=300',
|
|
581
|
+
...CORS,
|
|
582
|
+
});
|
|
583
|
+
return res.end(Buffer.from(await upstream.arrayBuffer()));
|
|
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
|
+
await Promise.all(
|
|
592
|
+
urls.map(async (u) => {
|
|
593
|
+
const prUrl = String(u);
|
|
594
|
+
if (!/^https:\/\/github\.com\/[\w.-]+\/[\w.-]+\/pull\/\d+$/.test(prUrl)) return;
|
|
595
|
+
const cached = prStatusCache.get(prUrl);
|
|
596
|
+
if (cached && Date.now() - cached.at < 60_000) {
|
|
597
|
+
statuses[prUrl] = cached.state;
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
const out = await run('gh', ['pr', 'view', prUrl, '--json', 'state']);
|
|
601
|
+
try {
|
|
602
|
+
const state = JSON.parse(out ?? '').state;
|
|
603
|
+
if (state) {
|
|
604
|
+
prStatusCache.set(prUrl, { state, at: Date.now() });
|
|
605
|
+
statuses[prUrl] = state;
|
|
606
|
+
}
|
|
607
|
+
} catch {
|
|
608
|
+
/* gh missing / not accessible */
|
|
609
|
+
}
|
|
610
|
+
}),
|
|
611
|
+
);
|
|
612
|
+
return json(res, 200, { statuses });
|
|
613
|
+
}
|
|
614
|
+
// Fast-merge: after reviewing the demo, one click merges the PR via gh.
|
|
615
|
+
if (req.method === 'POST' && url.pathname === '/pr/merge') {
|
|
616
|
+
const body = await readBody(req);
|
|
617
|
+
const prUrl = String(body.url ?? '');
|
|
618
|
+
if (!/^https:\/\/github\.com\/[\w.-]+\/[\w.-]+\/pull\/\d+$/.test(prUrl)) {
|
|
619
|
+
return json(res, 400, { error: 'invalid PR url' });
|
|
620
|
+
}
|
|
621
|
+
const out = await run('gh', ['pr', 'merge', prUrl, '--squash']);
|
|
622
|
+
prStatusCache.delete(prUrl);
|
|
623
|
+
if (out === null) {
|
|
624
|
+
return json(res, 502, { error: 'gh pr merge failed — check gh auth / merge conflicts / required checks' });
|
|
625
|
+
}
|
|
626
|
+
return json(res, 200, { ok: true, output: out.slice(0, 500) });
|
|
627
|
+
}
|
|
549
628
|
// Upload proxy: browsers can't PUT to Linear's storage (no CORS there) —
|
|
550
629
|
// this local process can. SSRF-guarded to Linear storage hosts.
|
|
551
630
|
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.
|
|
3
|
+
"version": "0.15.1",
|
|
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": {
|