linear-grab-bridge 0.10.0 → 0.12.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 +59 -13
- package/package.json +1 -1
package/linear-grab-bridge.mjs
CHANGED
|
@@ -28,16 +28,37 @@ 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.12.1';
|
|
32
32
|
|
|
33
33
|
/** Best-effort command runner (git/gh introspection). Never throws. */
|
|
34
|
-
function run(cmd, args) {
|
|
34
|
+
function run(cmd, args, cwd = DIR) {
|
|
35
35
|
return new Promise((resolve) => {
|
|
36
|
-
execFile(cmd, args, { cwd
|
|
36
|
+
execFile(cmd, args, { cwd, timeout: 15_000, maxBuffer: 2_000_000 }, (err, stdout) =>
|
|
37
37
|
resolve(err ? null : stdout.toString()),
|
|
38
38
|
);
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
|
+
|
|
42
|
+
/** Opt-in isolation: give a task its own git worktree + branch so parallel
|
|
43
|
+
local agents never trample each other's working tree. */
|
|
44
|
+
async function setupWorktree(task) {
|
|
45
|
+
const base = join(
|
|
46
|
+
HISTORY_DIR,
|
|
47
|
+
'worktrees',
|
|
48
|
+
createHash('sha1').update(DIR).digest('hex').slice(0, 8),
|
|
49
|
+
);
|
|
50
|
+
mkdirSync(base, { recursive: true });
|
|
51
|
+
const path = join(base, task.id);
|
|
52
|
+
const slug = task.title
|
|
53
|
+
.toLowerCase()
|
|
54
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
55
|
+
.replace(/^-+|-+$/g, '')
|
|
56
|
+
.slice(0, 32);
|
|
57
|
+
const branch = `lg/${slug || 'task'}-${task.id}`;
|
|
58
|
+
const out = await run('git', ['worktree', 'add', path, '-b', branch]);
|
|
59
|
+
if (out === null) return null;
|
|
60
|
+
return { path, branch, removed: false };
|
|
61
|
+
}
|
|
41
62
|
const MAX_TAIL = 300;
|
|
42
63
|
const IDLE_KILL_MS = 30 * 60_000; // free an idle interactive session after 30min (resumable)
|
|
43
64
|
|
|
@@ -87,6 +108,8 @@ function saveHistory() {
|
|
|
87
108
|
usage: t.usage ?? null,
|
|
88
109
|
startCommit: t.startCommit ?? null,
|
|
89
110
|
subagents: t.subagents ?? 0,
|
|
111
|
+
worktree: t.worktree ?? null,
|
|
112
|
+
cwd: t.cwd ?? null,
|
|
90
113
|
tail: (t.tail ?? []).slice(-60),
|
|
91
114
|
}));
|
|
92
115
|
writeFileSync(HISTORY_FILE, JSON.stringify(items));
|
|
@@ -145,6 +168,7 @@ function summary(t) {
|
|
|
145
168
|
usage: t.usage ?? null,
|
|
146
169
|
subagents: t.subagents ?? 0,
|
|
147
170
|
permissionMode: t.permissionMode ?? 'acceptEdits',
|
|
171
|
+
worktree: t.worktree ?? null,
|
|
148
172
|
};
|
|
149
173
|
}
|
|
150
174
|
|
|
@@ -180,7 +204,7 @@ function startProcess(task, { resume, initialText }) {
|
|
|
180
204
|
if (resume) args.push('--resume', resume);
|
|
181
205
|
|
|
182
206
|
const child = spawn(CLAUDE_BIN, args, {
|
|
183
|
-
cwd: DIR,
|
|
207
|
+
cwd: task.cwd ?? DIR,
|
|
184
208
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
185
209
|
env: { ...process.env, ...(task.env ?? {}) },
|
|
186
210
|
});
|
|
@@ -250,6 +274,8 @@ function ingest(task, event) {
|
|
|
250
274
|
return;
|
|
251
275
|
}
|
|
252
276
|
if (event.type === 'assistant') {
|
|
277
|
+
// The stream tells us the ACTUAL model — surface it even for defaults.
|
|
278
|
+
if (event.message?.model) task.model = event.message.model;
|
|
253
279
|
const usage = event.message?.usage;
|
|
254
280
|
if (usage) {
|
|
255
281
|
const context =
|
|
@@ -299,7 +325,7 @@ function ingest(task, event) {
|
|
|
299
325
|
}
|
|
300
326
|
}
|
|
301
327
|
|
|
302
|
-
function startTask({ title, prompt, model, env, permissionMode }) {
|
|
328
|
+
async function startTask({ title, prompt, model, env, permissionMode, worktree }) {
|
|
303
329
|
const id = randomUUID().slice(0, 8);
|
|
304
330
|
const task = {
|
|
305
331
|
id,
|
|
@@ -325,19 +351,28 @@ function startTask({ title, prompt, model, env, permissionMode }) {
|
|
|
325
351
|
};
|
|
326
352
|
tasks.set(id, task);
|
|
327
353
|
pushTail(task, 'user', String(prompt ?? '').slice(0, 2000));
|
|
354
|
+
if (worktree) {
|
|
355
|
+
const wt = await setupWorktree(task);
|
|
356
|
+
if (wt) {
|
|
357
|
+
task.worktree = wt;
|
|
358
|
+
task.cwd = wt.path;
|
|
359
|
+
pushTail(task, 'tool', `⎇ isolated worktree: ${wt.branch} @ ${wt.path}`);
|
|
360
|
+
} else {
|
|
361
|
+
pushTail(task, 'stderr', 'Worktree setup failed — running in the main working tree.');
|
|
362
|
+
}
|
|
363
|
+
}
|
|
328
364
|
// Snapshot HEAD so the Changes view can attribute work to this task.
|
|
329
|
-
|
|
330
|
-
task.startCommit = out?.trim() || null;
|
|
331
|
-
});
|
|
365
|
+
task.startCommit = ((await run('git', ['rev-parse', 'HEAD'], task.cwd ?? DIR)) ?? '').trim() || null;
|
|
332
366
|
startProcess(task, { initialText: String(prompt ?? '') });
|
|
333
367
|
return task;
|
|
334
368
|
}
|
|
335
369
|
|
|
336
370
|
/** What this task changed: files (+/−), branch, untracked, matching PRs. */
|
|
337
371
|
async function computeDiff(task) {
|
|
338
|
-
const
|
|
372
|
+
const cwd = task.cwd ?? DIR;
|
|
373
|
+
const branch = (await run('git', ['branch', '--show-current'], cwd))?.trim() ?? '';
|
|
339
374
|
const base = task.startCommit;
|
|
340
|
-
const numstat = (await run('git', ['diff', '--numstat', ...(base ? [base] : [])])) ?? '';
|
|
375
|
+
const numstat = (await run('git', ['diff', '--numstat', ...(base ? [base] : [])], cwd)) ?? '';
|
|
341
376
|
const files = numstat
|
|
342
377
|
.split('\n')
|
|
343
378
|
.filter(Boolean)
|
|
@@ -351,7 +386,7 @@ async function computeDiff(task) {
|
|
|
351
386
|
};
|
|
352
387
|
})
|
|
353
388
|
.filter((f) => f.path);
|
|
354
|
-
const untracked = ((await run('git', ['status', '--porcelain'])) ?? '')
|
|
389
|
+
const untracked = ((await run('git', ['status', '--porcelain'], cwd)) ?? '')
|
|
355
390
|
.split('\n')
|
|
356
391
|
.filter((l) => l.startsWith('??'))
|
|
357
392
|
.map((l) => l.slice(3).trim())
|
|
@@ -367,7 +402,7 @@ async function computeDiff(task) {
|
|
|
367
402
|
]) {
|
|
368
403
|
if (!args) continue;
|
|
369
404
|
try {
|
|
370
|
-
const out = await run('gh', args);
|
|
405
|
+
const out = await run('gh', args, cwd);
|
|
371
406
|
for (const pr of JSON.parse(out ?? '[]')) prs.set(pr.url, pr);
|
|
372
407
|
} catch {
|
|
373
408
|
/* gh missing or not a repo with remote */
|
|
@@ -466,6 +501,17 @@ createServer(async (req, res) => {
|
|
|
466
501
|
}
|
|
467
502
|
return json(res, 200, { ok: true });
|
|
468
503
|
}
|
|
504
|
+
const wtRemove = url.pathname.match(/^\/tasks\/([\w-]+)\/worktree\/remove$/);
|
|
505
|
+
if (req.method === 'POST' && wtRemove) {
|
|
506
|
+
const t = tasks.get(wtRemove[1]);
|
|
507
|
+
if (!t?.worktree || t.worktree.removed) return json(res, 404, { error: 'no worktree' });
|
|
508
|
+
if (t.status === 'running') return json(res, 409, { error: 'task still running' });
|
|
509
|
+
await run('git', ['worktree', 'remove', '--force', t.worktree.path]);
|
|
510
|
+
t.worktree.removed = true;
|
|
511
|
+
t.cwd = null;
|
|
512
|
+
saveHistory();
|
|
513
|
+
return json(res, 200, summary(t));
|
|
514
|
+
}
|
|
469
515
|
const message = url.pathname.match(/^\/tasks\/([\w-]+)\/message$/);
|
|
470
516
|
if (req.method === 'POST' && message) {
|
|
471
517
|
const t = tasks.get(message[1]);
|
|
@@ -497,7 +543,7 @@ createServer(async (req, res) => {
|
|
|
497
543
|
if (req.method === 'POST' && url.pathname === '/tasks') {
|
|
498
544
|
const body = await readBody(req);
|
|
499
545
|
if (!body.prompt) return json(res, 400, { error: 'prompt required' });
|
|
500
|
-
const task = startTask(body);
|
|
546
|
+
const task = await startTask(body);
|
|
501
547
|
return json(res, 201, summary(task));
|
|
502
548
|
}
|
|
503
549
|
// Upload proxy: browsers can't PUT to Linear's storage (no CORS there) —
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "linear-grab-bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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": {
|