linear-grab-bridge 0.9.0 → 0.10.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 +75 -2
- package/package.json +1 -1
package/linear-grab-bridge.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Binds 127.0.0.1 only — never exposed to the network.
|
|
14
14
|
*/
|
|
15
15
|
import { createServer } from 'node:http';
|
|
16
|
-
import { spawn } from 'node:child_process';
|
|
16
|
+
import { spawn, execFile } from 'node:child_process';
|
|
17
17
|
import { randomUUID } from 'node:crypto';
|
|
18
18
|
import { createHash } from 'node:crypto';
|
|
19
19
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
@@ -28,7 +28,16 @@ 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.10.0';
|
|
32
|
+
|
|
33
|
+
/** Best-effort command runner (git/gh introspection). Never throws. */
|
|
34
|
+
function run(cmd, args) {
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
execFile(cmd, args, { cwd: DIR, timeout: 8_000, maxBuffer: 2_000_000 }, (err, stdout) =>
|
|
37
|
+
resolve(err ? null : stdout.toString()),
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
32
41
|
const MAX_TAIL = 300;
|
|
33
42
|
const IDLE_KILL_MS = 30 * 60_000; // free an idle interactive session after 30min (resumable)
|
|
34
43
|
|
|
@@ -76,6 +85,8 @@ function saveHistory() {
|
|
|
76
85
|
sessionId: t.sessionId ?? null,
|
|
77
86
|
model: t.model ?? null,
|
|
78
87
|
usage: t.usage ?? null,
|
|
88
|
+
startCommit: t.startCommit ?? null,
|
|
89
|
+
subagents: t.subagents ?? 0,
|
|
79
90
|
tail: (t.tail ?? []).slice(-60),
|
|
80
91
|
}));
|
|
81
92
|
writeFileSync(HISTORY_FILE, JSON.stringify(items));
|
|
@@ -314,10 +325,66 @@ function startTask({ title, prompt, model, env, permissionMode }) {
|
|
|
314
325
|
};
|
|
315
326
|
tasks.set(id, task);
|
|
316
327
|
pushTail(task, 'user', String(prompt ?? '').slice(0, 2000));
|
|
328
|
+
// Snapshot HEAD so the Changes view can attribute work to this task.
|
|
329
|
+
void run('git', ['rev-parse', 'HEAD']).then((out) => {
|
|
330
|
+
task.startCommit = out?.trim() || null;
|
|
331
|
+
});
|
|
317
332
|
startProcess(task, { initialText: String(prompt ?? '') });
|
|
318
333
|
return task;
|
|
319
334
|
}
|
|
320
335
|
|
|
336
|
+
/** What this task changed: files (+/−), branch, untracked, matching PRs. */
|
|
337
|
+
async function computeDiff(task) {
|
|
338
|
+
const branch = (await run('git', ['branch', '--show-current']))?.trim() ?? '';
|
|
339
|
+
const base = task.startCommit;
|
|
340
|
+
const numstat = (await run('git', ['diff', '--numstat', ...(base ? [base] : [])])) ?? '';
|
|
341
|
+
const files = numstat
|
|
342
|
+
.split('\n')
|
|
343
|
+
.filter(Boolean)
|
|
344
|
+
.map((line) => {
|
|
345
|
+
const [a, d, ...path] = line.split('\t');
|
|
346
|
+
return {
|
|
347
|
+
path: path.join('\t'),
|
|
348
|
+
added: a === '-' ? 0 : Number(a),
|
|
349
|
+
deleted: d === '-' ? 0 : Number(d),
|
|
350
|
+
binary: a === '-',
|
|
351
|
+
};
|
|
352
|
+
})
|
|
353
|
+
.filter((f) => f.path);
|
|
354
|
+
const untracked = ((await run('git', ['status', '--porcelain'])) ?? '')
|
|
355
|
+
.split('\n')
|
|
356
|
+
.filter((l) => l.startsWith('??'))
|
|
357
|
+
.map((l) => l.slice(3).trim())
|
|
358
|
+
.filter(Boolean)
|
|
359
|
+
.slice(0, 40);
|
|
360
|
+
|
|
361
|
+
// PRs: by current head branch AND by the issue identifier in the title.
|
|
362
|
+
const prs = new Map();
|
|
363
|
+
const ident = task.title.match(/^([A-Z][A-Z0-9]*-\d+)/)?.[1];
|
|
364
|
+
for (const args of [
|
|
365
|
+
branch ? ['pr', 'list', '--head', branch, '--state', 'all', '--json', 'url,title,state', '--limit', '3'] : null,
|
|
366
|
+
ident ? ['pr', 'list', '--search', ident, '--state', 'all', '--json', 'url,title,state', '--limit', '3'] : null,
|
|
367
|
+
]) {
|
|
368
|
+
if (!args) continue;
|
|
369
|
+
try {
|
|
370
|
+
const out = await run('gh', args);
|
|
371
|
+
for (const pr of JSON.parse(out ?? '[]')) prs.set(pr.url, pr);
|
|
372
|
+
} catch {
|
|
373
|
+
/* gh missing or not a repo with remote */
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return {
|
|
378
|
+
branch,
|
|
379
|
+
baseCommit: base ?? null,
|
|
380
|
+
files: files.slice(0, 60),
|
|
381
|
+
untracked,
|
|
382
|
+
totalAdded: files.reduce((n, f) => n + f.added, 0),
|
|
383
|
+
totalDeleted: files.reduce((n, f) => n + f.deleted, 0),
|
|
384
|
+
prs: [...prs.values()],
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
321
388
|
/** Send a follow-up. Respawns via --resume when the process is gone or a model
|
|
322
389
|
change is pending — that's also how model switches take effect. */
|
|
323
390
|
function sendMessage(task, text) {
|
|
@@ -374,6 +441,12 @@ createServer(async (req, res) => {
|
|
|
374
441
|
? json(res, 200, { ...summary(t), tail: (t.tail ?? []).slice(-120) })
|
|
375
442
|
: json(res, 404, { error: 'not found' });
|
|
376
443
|
}
|
|
444
|
+
const diff = url.pathname.match(/^\/tasks\/([\w-]+)\/diff$/);
|
|
445
|
+
if (req.method === 'GET' && diff) {
|
|
446
|
+
const t = tasks.get(diff[1]);
|
|
447
|
+
if (!t) return json(res, 404, { error: 'not found' });
|
|
448
|
+
return json(res, 200, await computeDiff(t));
|
|
449
|
+
}
|
|
377
450
|
const stop = url.pathname.match(/^\/tasks\/([\w-]+)\/stop$/);
|
|
378
451
|
if (req.method === 'POST' && stop) {
|
|
379
452
|
const t = tasks.get(stop[1]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "linear-grab-bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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": {
|