linear-grab-bridge 0.21.0 → 0.23.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.
- package/linear-grab-bridge.mjs +110 -2
- package/package.json +1 -1
package/linear-grab-bridge.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import { createServer } from 'node:http';
|
|
|
16
16
|
import { spawn, execFile } from 'node:child_process';
|
|
17
17
|
import { randomUUID } from 'node:crypto';
|
|
18
18
|
import { createHash } from 'node:crypto';
|
|
19
|
-
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
19
|
+
import { appendFileSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
20
20
|
import { homedir } from 'node:os';
|
|
21
21
|
import { join } from 'node:path';
|
|
22
22
|
|
|
@@ -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.23.0';
|
|
32
32
|
|
|
33
33
|
/** Best-effort command runner (git/gh introspection). Never throws. */
|
|
34
34
|
function run(cmd, args, cwd = DIR) {
|
|
@@ -465,11 +465,19 @@ createServer(async (req, res) => {
|
|
|
465
465
|
const url = new URL(req.url ?? '/', 'http://localhost');
|
|
466
466
|
|
|
467
467
|
if (req.method === 'GET' && url.pathname === '/health') {
|
|
468
|
+
// Committed project config — read fresh each poll so edits apply live.
|
|
469
|
+
let projectConfig = null;
|
|
470
|
+
try {
|
|
471
|
+
projectConfig = JSON.parse(readFileSync(join(DIR, '.lineargrab.json'), 'utf8'));
|
|
472
|
+
} catch {
|
|
473
|
+
/* missing or invalid — fine */
|
|
474
|
+
}
|
|
468
475
|
return json(res, 200, {
|
|
469
476
|
ok: true,
|
|
470
477
|
version: VERSION,
|
|
471
478
|
cwd: DIR,
|
|
472
479
|
active: [...tasks.values()].filter((t) => t.status === 'running').length,
|
|
480
|
+
projectConfig,
|
|
473
481
|
});
|
|
474
482
|
}
|
|
475
483
|
if (req.method === 'GET' && url.pathname === '/tasks') {
|
|
@@ -621,6 +629,106 @@ createServer(async (req, res) => {
|
|
|
621
629
|
);
|
|
622
630
|
return json(res, 200, { statuses, previews });
|
|
623
631
|
}
|
|
632
|
+
// ---- react-scan telemetry -------------------------------------------
|
|
633
|
+
// Browser pushes render/interaction events; they land in an append-only
|
|
634
|
+
// NDJSON file agents read directly (.lineargrab/scan.ndjson at the repo).
|
|
635
|
+
if (req.method === 'POST' && url.pathname === '/scan/events') {
|
|
636
|
+
const body = await readBody(req);
|
|
637
|
+
const events = Array.isArray(body.events) ? body.events : [body];
|
|
638
|
+
const dir = join(DIR, '.lineargrab');
|
|
639
|
+
try {
|
|
640
|
+
mkdirSync(dir, { recursive: true });
|
|
641
|
+
// Self-ignoring: telemetry must never pollute commits.
|
|
642
|
+
writeFileSync(join(dir, '.gitignore'), '*\n', { flag: 'wx' });
|
|
643
|
+
} catch {
|
|
644
|
+
/* exists */
|
|
645
|
+
}
|
|
646
|
+
const file = join(dir, 'scan.ndjson');
|
|
647
|
+
const lines =
|
|
648
|
+
events
|
|
649
|
+
.slice(0, 200)
|
|
650
|
+
.map((e) => JSON.stringify({ at: Date.now(), ...e }))
|
|
651
|
+
.join('\n') + '\n';
|
|
652
|
+
try {
|
|
653
|
+
appendFileSync(file, lines);
|
|
654
|
+
// Rotation: cap ~2MB by keeping the newest half.
|
|
655
|
+
const size = statSync(file).size;
|
|
656
|
+
if (size > 2_000_000) {
|
|
657
|
+
const keep = readFileSync(file, 'utf8');
|
|
658
|
+
writeFileSync(file, keep.slice(Math.floor(keep.length / 2)).replace(/^[^\n]*\n/, ''));
|
|
659
|
+
}
|
|
660
|
+
} catch {
|
|
661
|
+
/* disk issues — telemetry must never error the page */
|
|
662
|
+
}
|
|
663
|
+
return json(res, 200, { ok: true });
|
|
664
|
+
}
|
|
665
|
+
// Aggregated "what's slow right now" — a convenience view over the file.
|
|
666
|
+
if (req.method === 'GET' && url.pathname === '/scan/report') {
|
|
667
|
+
const windowMs = Number(url.searchParams.get('window') ?? 120_000);
|
|
668
|
+
let raw = '';
|
|
669
|
+
try {
|
|
670
|
+
raw = readFileSync(join(DIR, '.lineargrab', 'scan.ndjson'), 'utf8');
|
|
671
|
+
} catch {
|
|
672
|
+
return json(res, 200, { report: 'No scan telemetry yet — is react-scan running with the bridge enabled?', components: [] });
|
|
673
|
+
}
|
|
674
|
+
const cutoff = Date.now() - windowMs;
|
|
675
|
+
const byComponent = new Map();
|
|
676
|
+
const interactions = [];
|
|
677
|
+
for (const line of raw.split('\n')) {
|
|
678
|
+
if (!line) continue;
|
|
679
|
+
let e;
|
|
680
|
+
try {
|
|
681
|
+
e = JSON.parse(line);
|
|
682
|
+
} catch {
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
if ((e.at ?? 0) < cutoff) continue;
|
|
686
|
+
if (e.kind === 'interaction') interactions.push(e);
|
|
687
|
+
for (const c of e.components ?? []) {
|
|
688
|
+
const cur = byComponent.get(c.name) ?? {
|
|
689
|
+
name: c.name,
|
|
690
|
+
renders: 0,
|
|
691
|
+
selfTime: 0,
|
|
692
|
+
source: c.source ?? null,
|
|
693
|
+
unnecessary: 0,
|
|
694
|
+
changes: {},
|
|
695
|
+
};
|
|
696
|
+
cur.renders += c.renders ?? 1;
|
|
697
|
+
cur.selfTime += c.selfTime ?? 0;
|
|
698
|
+
if (c.source) cur.source = c.source;
|
|
699
|
+
if (c.unnecessary) cur.unnecessary += c.renders ?? 1;
|
|
700
|
+
for (const ch of c.changes ?? []) cur.changes[ch] = (cur.changes[ch] ?? 0) + 1;
|
|
701
|
+
byComponent.set(c.name, cur);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
const components = [...byComponent.values()].sort((a, b) => b.selfTime - a.selfTime).slice(0, 25);
|
|
705
|
+
const md = [
|
|
706
|
+
`# react-scan report (last ${Math.round(windowMs / 1000)}s)`,
|
|
707
|
+
'',
|
|
708
|
+
...components.map(
|
|
709
|
+
(c) =>
|
|
710
|
+
`- **${c.name}** — ${c.renders} renders · ${c.selfTime.toFixed(1)}ms self` +
|
|
711
|
+
(c.unnecessary ? ` · ${c.unnecessary} unnecessary` : '') +
|
|
712
|
+
(c.source ? ` · \`${c.source}\`` : '') +
|
|
713
|
+
(Object.keys(c.changes).length
|
|
714
|
+
? ` · causes: ${Object.entries(c.changes)
|
|
715
|
+
.sort((a, b) => b[1] - a[1])
|
|
716
|
+
.slice(0, 3)
|
|
717
|
+
.map(([k, v]) => `${k}×${v}`)
|
|
718
|
+
.join(', ')}`
|
|
719
|
+
: ''),
|
|
720
|
+
),
|
|
721
|
+
'',
|
|
722
|
+
...interactions
|
|
723
|
+
.slice(-10)
|
|
724
|
+
.map(
|
|
725
|
+
(i) =>
|
|
726
|
+
`- interaction ${i.type ?? '?'} on ${i.target ?? '?'} — ${Math.round(i.duration ?? 0)}ms` +
|
|
727
|
+
(i.slow ? ' ⚠ SLOW' : ''),
|
|
728
|
+
),
|
|
729
|
+
].join('\n');
|
|
730
|
+
return json(res, 200, { report: md, components, interactions: interactions.slice(-20) });
|
|
731
|
+
}
|
|
624
732
|
// Reset the staging branch: delete + recreate from the default branch.
|
|
625
733
|
if (req.method === 'POST' && url.pathname === '/branch/reset') {
|
|
626
734
|
const body = await readBody(req);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "linear-grab-bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.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": {
|