claude-bridge-cli 2.0.8 → 2.0.12
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/lib/bridge.js +54 -9
- package/package.json +1 -1
package/lib/bridge.js
CHANGED
|
@@ -245,7 +245,7 @@ function looksLikeInternal(text) {
|
|
|
245
245
|
}
|
|
246
246
|
|
|
247
247
|
function parseSessionFile(filePath) {
|
|
248
|
-
let preview = "", aiTitle = "", customTitle = "", msgCount = 0, lastPrompt = "";
|
|
248
|
+
let preview = "", aiTitle = "", customTitle = "", msgCount = 0, lastPrompt = "", cwd = "";
|
|
249
249
|
try {
|
|
250
250
|
// For listing, read just the first 64KB and the last 64KB.
|
|
251
251
|
// First chunk: get preview + ai_title from early events.
|
|
@@ -262,6 +262,11 @@ function parseSessionFile(filePath) {
|
|
|
262
262
|
if (!line) continue;
|
|
263
263
|
try {
|
|
264
264
|
const obj = JSON.parse(line);
|
|
265
|
+
// The real working directory is recorded on the session's events;
|
|
266
|
+
// capture the first one. Far more accurate than decoding the project
|
|
267
|
+
// dir name (which is lossy — it can't recover ":" or distinguish a
|
|
268
|
+
// path separator from a literal "-", e.g. Windows "C:\GIT\…").
|
|
269
|
+
if (!cwd && typeof obj.cwd === "string" && obj.cwd) cwd = obj.cwd;
|
|
265
270
|
if (obj.type === "summary" && obj.summary) preview = preview || obj.summary.slice(0, 200);
|
|
266
271
|
if (obj.type === "user" && obj.message?.content) {
|
|
267
272
|
const text = typeof obj.message.content === "string" ? obj.message.content : JSON.stringify(obj.message.content);
|
|
@@ -304,7 +309,7 @@ function parseSessionFile(filePath) {
|
|
|
304
309
|
fs.closeSync(fd);
|
|
305
310
|
}
|
|
306
311
|
} catch {}
|
|
307
|
-
return { preview, ai_title: customTitle || aiTitle, message_count: msgCount, last_prompt: lastPrompt };
|
|
312
|
+
return { preview, ai_title: customTitle || aiTitle, message_count: msgCount, last_prompt: lastPrompt, cwd };
|
|
308
313
|
}
|
|
309
314
|
|
|
310
315
|
function projectDirToCwd(name) {
|
|
@@ -327,7 +332,7 @@ function listSessions(opts = {}) {
|
|
|
327
332
|
const title = titleOverrides[id] || parsed.ai_title;
|
|
328
333
|
sessions.push({
|
|
329
334
|
id, project,
|
|
330
|
-
cwd: projectDirToCwd(project),
|
|
335
|
+
cwd: parsed.cwd || projectDirToCwd(project),
|
|
331
336
|
mtime: stat.mtimeMs / 1000,
|
|
332
337
|
mtime_iso: stat.mtime.toISOString(),
|
|
333
338
|
preview: parsed.preview,
|
|
@@ -376,13 +381,14 @@ function searchSessions(query, limit = 30) {
|
|
|
376
381
|
const snippetEnd = Math.min(content.length, idx + q.length + 80);
|
|
377
382
|
const snippet = content.slice(snippetStart, snippetEnd).replace(/\n/g, " ").trim();
|
|
378
383
|
|
|
379
|
-
let aiTitle = "", msgCount = 0;
|
|
384
|
+
let aiTitle = "", msgCount = 0, realCwd = "";
|
|
380
385
|
try {
|
|
381
386
|
const lines = content.split("\n").filter(Boolean);
|
|
382
387
|
msgCount = lines.length;
|
|
383
388
|
for (const line of lines) {
|
|
384
389
|
try {
|
|
385
390
|
const obj = JSON.parse(line);
|
|
391
|
+
if (!realCwd && typeof obj.cwd === "string" && obj.cwd) realCwd = obj.cwd;
|
|
386
392
|
if (obj.type === "result" && obj.result?.metadata?.title?.value) {
|
|
387
393
|
aiTitle = obj.result.metadata.title.value;
|
|
388
394
|
}
|
|
@@ -394,7 +400,7 @@ function searchSessions(query, limit = 30) {
|
|
|
394
400
|
|
|
395
401
|
results.push({
|
|
396
402
|
id, project,
|
|
397
|
-
cwd: projectDirToCwd(project),
|
|
403
|
+
cwd: realCwd || projectDirToCwd(project),
|
|
398
404
|
mtime: stat.mtimeMs / 1000,
|
|
399
405
|
mtime_iso: stat.mtime.toISOString(),
|
|
400
406
|
ai_title: titleOverrides[id] || aiTitle,
|
|
@@ -620,7 +626,22 @@ function renameSession(sessionId, title) {
|
|
|
620
626
|
|
|
621
627
|
// ── Run claude -p ──
|
|
622
628
|
|
|
623
|
-
|
|
629
|
+
// Injected into every turn (--append-system-prompt) so Claude never ends a turn
|
|
630
|
+
// promising async follow-up it can't keep: a bridge turn is one-shot and atomic —
|
|
631
|
+
// nothing re-invokes Claude after it stops, so "I'll report once the build
|
|
632
|
+
// completes" is a promise that never resolves (it leaves the UI dead-ended).
|
|
633
|
+
const ATOMIC_TURN_PROMPT =
|
|
634
|
+
"You are running inside a one-shot, non-interactive bridge: THIS TURN IS ATOMIC. " +
|
|
635
|
+
"It ends the moment you stop producing output, and nothing re-invokes you afterward. " +
|
|
636
|
+
"You cannot do work in the background, report back later, be re-triggered when an " +
|
|
637
|
+
"external job (build/CI/deploy/long command) finishes, or continue on your own. " +
|
|
638
|
+
"Therefore NEVER end a turn by promising future follow-up such as 'I'll report once " +
|
|
639
|
+
"it completes', 'I'll continue when CI is done', or 'waiting on X to finish'. Instead, " +
|
|
640
|
+
"run any work to completion within this turn and report the actual result now. If a " +
|
|
641
|
+
"task genuinely cannot finish in this turn, say so plainly and tell the user the exact " +
|
|
642
|
+
"command(s) to run or the next message to send to continue — do not imply you will resume.";
|
|
643
|
+
|
|
644
|
+
function askClaude(config, { prompt, session_id, images, cwd, plan_mode, allow_tools, model, fork }) {
|
|
624
645
|
return new Promise((resolve) => {
|
|
625
646
|
let finalPrompt = prompt;
|
|
626
647
|
if (images && images.length) {
|
|
@@ -654,22 +675,36 @@ function askClaude(config, { prompt, session_id, images, cwd, plan_mode, allow_t
|
|
|
654
675
|
fs.writeFileSync(tmpFile, finalPrompt, "utf8");
|
|
655
676
|
}
|
|
656
677
|
|
|
678
|
+
// Allow list — the caller (extension) can override via `allow_tools`, e.g.
|
|
679
|
+
// after the user clicks "Allow <tool>" on a permission denial, so an MCP
|
|
680
|
+
// tool (mcp__server__tool) or any other non-default tool can be granted for
|
|
681
|
+
// the session. Falls back to the default working set. Mirrors the Python
|
|
682
|
+
// bridge so both behave identically.
|
|
683
|
+
const defaultAllow = ["Read", "Write", "Edit", "Bash", "Grep", "Glob", "WebFetch", "WebSearch",
|
|
684
|
+
"Task", "TaskCreate", "TaskList", "TaskGet", "TaskUpdate", "NotebookEdit"];
|
|
685
|
+
const allow = (Array.isArray(allow_tools) && allow_tools.length &&
|
|
686
|
+
allow_tools.every(x => typeof x === "string")) ? allow_tools : defaultAllow;
|
|
657
687
|
const settings = JSON.stringify({
|
|
658
688
|
permissions: {
|
|
659
689
|
defaultMode: "acceptEdits",
|
|
660
|
-
allow
|
|
661
|
-
"Task", "TaskCreate", "TaskList", "TaskGet", "TaskUpdate", "NotebookEdit"],
|
|
690
|
+
allow,
|
|
662
691
|
deny: ["AskUserQuestion"],
|
|
663
692
|
}
|
|
664
693
|
});
|
|
665
694
|
const promptArg = useTempFile ? `@${tmpFile}` : finalPrompt;
|
|
666
695
|
const args = ["-p", promptArg, "--output-format", "json",
|
|
667
|
-
"--settings", settings, "--permission-mode", "acceptEdits"
|
|
696
|
+
"--settings", settings, "--permission-mode", "acceptEdits",
|
|
697
|
+
"--append-system-prompt", ATOMIC_TURN_PROMPT];
|
|
668
698
|
let resumeCwd = null;
|
|
669
699
|
if (session_id) {
|
|
670
700
|
const existing = scanSessionFiles().find(s => s.id === session_id);
|
|
671
701
|
if (existing) {
|
|
672
702
|
args.push("--resume", session_id);
|
|
703
|
+
// Fork: replay this session's history into a BRAND-NEW session id instead
|
|
704
|
+
// of appending to the original. claude returns the new id in the result,
|
|
705
|
+
// which the client adopts (leaving the original untouched). --fork-session
|
|
706
|
+
// only works alongside --resume.
|
|
707
|
+
if (fork) args.push("--fork-session");
|
|
673
708
|
// Scan JSONL for the first event with a cwd field
|
|
674
709
|
try {
|
|
675
710
|
const lines = fs.readFileSync(existing.filePath, "utf8").split("\n");
|
|
@@ -724,12 +759,22 @@ function askClaude(config, { prompt, session_id, images, cwd, plan_mode, allow_t
|
|
|
724
759
|
const newDir = path.join(dataDir(), "images", sid);
|
|
725
760
|
try { fs.renameSync(oldDir, newDir); } catch {}
|
|
726
761
|
}
|
|
762
|
+
// Surface tool-permission denials so the extension can show an "Allow
|
|
763
|
+
// <tool>" button (then resend with allow_tools including it). Without
|
|
764
|
+
// this, a tool that needs approval — notably any MCP tool, which isn't
|
|
765
|
+
// in the default allow list — silently fails with no way to grant it in
|
|
766
|
+
// headless -p mode. Mirrors the Python bridge's shape.
|
|
767
|
+
const rawDenials = Array.isArray(result.permission_denials) ? result.permission_denials : [];
|
|
768
|
+
const permission_denials = rawDenials
|
|
769
|
+
.filter(x => x && typeof x === "object")
|
|
770
|
+
.map(x => ({ tool_name: x.tool_name || "?", tool_input: x.tool_input || {} }));
|
|
727
771
|
resolve({
|
|
728
772
|
response: result.result || result.assistantMessage || stdout.slice(0, 5000),
|
|
729
773
|
session_id: sid,
|
|
730
774
|
cost_usd: result.cost_usd || null,
|
|
731
775
|
duration_ms: result.duration_ms || null,
|
|
732
776
|
context: result.context || null,
|
|
777
|
+
permission_denials,
|
|
733
778
|
});
|
|
734
779
|
} catch {
|
|
735
780
|
if (stdout.trim()) {
|
package/package.json
CHANGED