@vanillagreen/pi-claude-bridge 1.1.3 → 1.1.4
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/README.md +3 -1
- package/bundle/index.js +238 -23
- package/package.json +13 -5
- package/src/index.ts +241 -3
- package/src/prompt-context.ts +45 -11
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ Restart Pi after installation.
|
|
|
37
37
|
|
|
38
38
|
Default behavior matches upstream: append `AGENTS.md` plus Pi's skills block to Claude Code's `claude_code` preset prompt.
|
|
39
39
|
|
|
40
|
-
Extra Pi context is off by default. Enable per item in the extension manager when you want Claude Code to see prompt blocks that other Pi extensions add to your session.
|
|
40
|
+
Extra Pi context is off by default. Enable per item in the extension manager when you want Claude Code to see prompt blocks that other Pi extensions add to your session. Forwarded blocks are wrapped in explicit XML tags so Pi 0.75+ project-context boundaries do not bleed into adjacent sections.
|
|
41
41
|
|
|
42
42
|
## Settings
|
|
43
43
|
|
|
@@ -79,3 +79,5 @@ Open `/extensions:settings`; settings appear under the **Claude Bridge** tab.
|
|
|
79
79
|
## Debugging
|
|
80
80
|
|
|
81
81
|
Set `CLAUDE_BRIDGE_DEBUG=1` to write bridge logs to `~/.pi/agent/claude-bridge.log` and per-query Claude Code CLI logs under `~/.pi/agent/cc-cli-logs/`.
|
|
82
|
+
|
|
83
|
+
Before starting Claude Code, the bridge preflights the resolved executable and working directory. Failures include the underlying `code`, `errno`, `syscall`, `path`, `cwd`, and detected executable file type so spawn issues point at the real failing path instead of the Claude Agent SDK's generic native-binary message. If Node still emits a spawn error after preflight, the bridge wraps that error with the same context before handing it back to the SDK.
|
package/bundle/index.js
CHANGED
|
@@ -19184,8 +19184,9 @@ function readSession(jsonlPath, projectPath) {
|
|
|
19184
19184
|
}
|
|
19185
19185
|
|
|
19186
19186
|
// src/index.ts
|
|
19187
|
+
import { spawn as spawnProcess } from "child_process";
|
|
19187
19188
|
import { createHash } from "crypto";
|
|
19188
|
-
import { accessSync, appendFileSync as appendFileSync3, constants as fsConstants, mkdirSync as mkdirSync3, realpathSync as realpathSync3, statSync as statSync3 } from "fs";
|
|
19189
|
+
import { accessSync, appendFileSync as appendFileSync3, constants as fsConstants, mkdirSync as mkdirSync3, readFileSync as readFileSync7, realpathSync as realpathSync3, statSync as statSync3 } from "fs";
|
|
19189
19190
|
import { resolve as pathResolve } from "path";
|
|
19190
19191
|
import { homedir as homedir5 } from "os";
|
|
19191
19192
|
import { delimiter, dirname as dirname5, join as join5 } from "path";
|
|
@@ -19758,8 +19759,13 @@ function extractHeadingSection(systemPrompt, headings) {
|
|
|
19758
19759
|
}
|
|
19759
19760
|
if (start < 0) return void 0;
|
|
19760
19761
|
const rest = systemPrompt.slice(start).trim();
|
|
19761
|
-
const
|
|
19762
|
-
|
|
19762
|
+
const endCandidates = [
|
|
19763
|
+
rest.slice(1).search(/\n##\s+/),
|
|
19764
|
+
rest.search(/\n<\/project_instructions>/),
|
|
19765
|
+
rest.search(/\n<\/project_context>/)
|
|
19766
|
+
].map((index, offset) => index >= 0 && offset === 0 ? index + 1 : index).filter((index) => index >= 0);
|
|
19767
|
+
const end = endCandidates.length > 0 ? Math.min(...endCandidates) : -1;
|
|
19768
|
+
return (end >= 0 ? rest.slice(0, end) : rest).trim();
|
|
19763
19769
|
}
|
|
19764
19770
|
function extractBlockByMarkers(systemPrompt, markers) {
|
|
19765
19771
|
for (const block of splitPromptBlocks(systemPrompt)) {
|
|
@@ -19772,48 +19778,57 @@ function buildPromptContextAppend(systemPrompt, cwd, settings) {
|
|
|
19772
19778
|
const labels = [];
|
|
19773
19779
|
if (settings.includeAppendSystemPromptMd) {
|
|
19774
19780
|
for (const file2 of readAppendSystemPromptFiles(cwd)) {
|
|
19775
|
-
parts.push(
|
|
19776
|
-
|
|
19777
|
-
${file2.content}`);
|
|
19781
|
+
parts.push(xmlBlock("append_system_prompt", { label: file2.label }, file2.content));
|
|
19778
19782
|
labels.push(file2.label);
|
|
19779
19783
|
}
|
|
19780
19784
|
}
|
|
19781
19785
|
if (settings.includeProjectAgentsHook) {
|
|
19782
19786
|
const projectAgents = extractHeadingSection(systemPrompt, ["## Project Agents", "## Project Subagents"]);
|
|
19783
19787
|
if (projectAgents) {
|
|
19784
|
-
parts.push(
|
|
19785
|
-
|
|
19786
|
-
${projectAgents}`);
|
|
19788
|
+
parts.push(xmlBlock("before_agent_start", { source: "project-agents" }, projectAgents));
|
|
19787
19789
|
labels.push("project agents hook");
|
|
19788
19790
|
}
|
|
19789
19791
|
}
|
|
19790
19792
|
if (settings.includeTaskPanelHook) {
|
|
19791
19793
|
const taskReminder = extractBlockByMarkers(systemPrompt, [/^Task workflow reminder:/]);
|
|
19792
19794
|
if (taskReminder) {
|
|
19793
|
-
parts.push(
|
|
19794
|
-
|
|
19795
|
-
${taskReminder}`);
|
|
19795
|
+
parts.push(xmlBlock("before_agent_start", { source: "task-panel" }, taskReminder));
|
|
19796
19796
|
labels.push("task panel hook");
|
|
19797
19797
|
}
|
|
19798
19798
|
}
|
|
19799
19799
|
if (settings.includeCavemanHook) {
|
|
19800
19800
|
const caveman = extractBlockByMarkers(systemPrompt, [/^You MUST respond in caveman /m]);
|
|
19801
19801
|
if (caveman) {
|
|
19802
|
-
parts.push(
|
|
19803
|
-
|
|
19804
|
-
${caveman}`);
|
|
19802
|
+
parts.push(xmlBlock("before_agent_start", { source: "caveman" }, caveman));
|
|
19805
19803
|
labels.push("caveman hook");
|
|
19806
19804
|
}
|
|
19807
19805
|
}
|
|
19808
19806
|
if (parts.length === 0) return { labels };
|
|
19809
19807
|
return {
|
|
19810
19808
|
labels,
|
|
19811
|
-
text:
|
|
19812
|
-
"
|
|
19813
|
-
|
|
19814
|
-
|
|
19815
|
-
|
|
19816
|
-
|
|
19809
|
+
text: xmlBlock(
|
|
19810
|
+
"forwarded_pi_context",
|
|
19811
|
+
{},
|
|
19812
|
+
[
|
|
19813
|
+
"The following content was explicitly enabled in pi-claude-bridge settings and comes from Pi prompt files or before_agent_start prompt hooks.",
|
|
19814
|
+
...parts
|
|
19815
|
+
].join("\n\n"),
|
|
19816
|
+
false
|
|
19817
|
+
)
|
|
19818
|
+
};
|
|
19819
|
+
}
|
|
19820
|
+
function escapeXmlAttr(value) {
|
|
19821
|
+
return value.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<").replace(/>/g, ">");
|
|
19822
|
+
}
|
|
19823
|
+
function escapeXmlText(value) {
|
|
19824
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
19825
|
+
}
|
|
19826
|
+
function xmlBlock(tag, attrs, content, escapeContent = true) {
|
|
19827
|
+
const attrText = Object.entries(attrs).map(([key, value]) => ` ${key}="${escapeXmlAttr(value)}"`).join("");
|
|
19828
|
+
const body = escapeContent ? escapeXmlText(content.trim()) : content.trim();
|
|
19829
|
+
return `<${tag}${attrText}>
|
|
19830
|
+
${body}
|
|
19831
|
+
</${tag}>`;
|
|
19817
19832
|
}
|
|
19818
19833
|
|
|
19819
19834
|
// node_modules/zod/v4/classic/external.js
|
|
@@ -34423,6 +34438,199 @@ function resolveClaudeExecutable(configured) {
|
|
|
34423
34438
|
if (trimmed) return trimmed;
|
|
34424
34439
|
return executableFromPath("claude") ?? executableFromPath("claude-code");
|
|
34425
34440
|
}
|
|
34441
|
+
function errnoValue(err) {
|
|
34442
|
+
return typeof err?.errno === "number" ? err.errno : void 0;
|
|
34443
|
+
}
|
|
34444
|
+
function syscallValue(err) {
|
|
34445
|
+
return typeof err?.syscall === "string" ? err.syscall : void 0;
|
|
34446
|
+
}
|
|
34447
|
+
function pathValue(err) {
|
|
34448
|
+
const value = err?.path;
|
|
34449
|
+
return typeof value === "string" ? value : void 0;
|
|
34450
|
+
}
|
|
34451
|
+
function codeValue(err, fallback) {
|
|
34452
|
+
const value = err?.code;
|
|
34453
|
+
return typeof value === "string" ? value : fallback;
|
|
34454
|
+
}
|
|
34455
|
+
function displayValue(value) {
|
|
34456
|
+
return value === void 0 || value === null || value === "" ? "<none>" : String(value);
|
|
34457
|
+
}
|
|
34458
|
+
function makeClaudePreflightError(summary, details) {
|
|
34459
|
+
const detail = [
|
|
34460
|
+
`code=${details.code}`,
|
|
34461
|
+
`errno=${displayValue(details.errno)}`,
|
|
34462
|
+
`syscall=${displayValue(details.syscall)}`,
|
|
34463
|
+
`path=${details.path}`,
|
|
34464
|
+
`cwd=${details.cwd}`,
|
|
34465
|
+
...details.fileType ? [`fileType=${details.fileType}`] : [],
|
|
34466
|
+
...details.realPath ? [`realPath=${details.realPath}`] : []
|
|
34467
|
+
].join(" ");
|
|
34468
|
+
const error51 = new Error(`${summary} (${detail})`);
|
|
34469
|
+
error51.name = "ClaudeExecutablePreflightError";
|
|
34470
|
+
error51.code = details.code;
|
|
34471
|
+
if (details.errno !== void 0) error51.errno = typeof details.errno === "number" ? details.errno : Number(details.errno);
|
|
34472
|
+
if (details.syscall) error51.syscall = details.syscall;
|
|
34473
|
+
error51.path = details.path;
|
|
34474
|
+
error51.cwd = details.cwd;
|
|
34475
|
+
if (details.fileType) error51.fileType = details.fileType;
|
|
34476
|
+
if (details.realPath) error51.realPath = details.realPath;
|
|
34477
|
+
if (details.cause !== void 0) error51.cause = details.cause;
|
|
34478
|
+
return error51;
|
|
34479
|
+
}
|
|
34480
|
+
function classifyClaudeExecutableBytes(bytes) {
|
|
34481
|
+
if (bytes.length === 0) return "empty";
|
|
34482
|
+
if (bytes.length >= 2 && bytes[0] === 35 && bytes[1] === 33) return "shebang-script";
|
|
34483
|
+
if (bytes.length >= 4 && bytes[0] === 127 && bytes[1] === 69 && bytes[2] === 76 && bytes[3] === 70) return "elf";
|
|
34484
|
+
if (bytes.length >= 2 && bytes[0] === 77 && bytes[1] === 90) return "pe";
|
|
34485
|
+
if (bytes.length >= 4) {
|
|
34486
|
+
const magic = bytes[0] * 16777216 + bytes[1] * 65536 + bytes[2] * 256 + bytes[3];
|
|
34487
|
+
if (magic === 4277009102 || magic === 4277009103 || magic === 3472551422 || magic === 3489328638 || magic === 3405691582 || magic === 3199925962) return "mach-o";
|
|
34488
|
+
}
|
|
34489
|
+
return "unknown";
|
|
34490
|
+
}
|
|
34491
|
+
function preflightClaudeExecutable(path, cwd) {
|
|
34492
|
+
let realCwd = cwd;
|
|
34493
|
+
try {
|
|
34494
|
+
const cwdStat = statSync3(cwd);
|
|
34495
|
+
if (!cwdStat.isDirectory()) {
|
|
34496
|
+
throw makeClaudePreflightError("Claude Code spawn cwd preflight failed: cwd is not a directory.", {
|
|
34497
|
+
code: "ENOTDIR",
|
|
34498
|
+
syscall: "chdir",
|
|
34499
|
+
path: cwd,
|
|
34500
|
+
cwd
|
|
34501
|
+
});
|
|
34502
|
+
}
|
|
34503
|
+
accessSync(cwd, fsConstants.X_OK);
|
|
34504
|
+
realCwd = realpathSync3(cwd);
|
|
34505
|
+
} catch (err) {
|
|
34506
|
+
if (err.name === "ClaudeExecutablePreflightError") throw err;
|
|
34507
|
+
throw makeClaudePreflightError("Claude Code spawn cwd preflight failed: cwd is not reachable before spawning Claude Code.", {
|
|
34508
|
+
code: codeValue(err, "EACCES"),
|
|
34509
|
+
errno: errnoValue(err),
|
|
34510
|
+
syscall: syscallValue(err),
|
|
34511
|
+
path: pathValue(err) ?? cwd,
|
|
34512
|
+
cwd,
|
|
34513
|
+
cause: err
|
|
34514
|
+
});
|
|
34515
|
+
}
|
|
34516
|
+
let realPath = path;
|
|
34517
|
+
try {
|
|
34518
|
+
const stat = statSync3(path);
|
|
34519
|
+
if (!stat.isFile()) {
|
|
34520
|
+
throw makeClaudePreflightError("Claude Code executable preflight failed: resolved path is not a file.", {
|
|
34521
|
+
code: "EACCES",
|
|
34522
|
+
syscall: "exec",
|
|
34523
|
+
path,
|
|
34524
|
+
cwd
|
|
34525
|
+
});
|
|
34526
|
+
}
|
|
34527
|
+
accessSync(path, fsConstants.X_OK);
|
|
34528
|
+
realPath = realpathSync3(path);
|
|
34529
|
+
} catch (err) {
|
|
34530
|
+
if (err.name === "ClaudeExecutablePreflightError") throw err;
|
|
34531
|
+
throw makeClaudePreflightError("Claude Code executable preflight failed: cannot access resolved executable before spawning Claude Code.", {
|
|
34532
|
+
code: codeValue(err, "ENOENT"),
|
|
34533
|
+
errno: errnoValue(err),
|
|
34534
|
+
syscall: syscallValue(err),
|
|
34535
|
+
path: pathValue(err) ?? path,
|
|
34536
|
+
cwd,
|
|
34537
|
+
cause: err
|
|
34538
|
+
});
|
|
34539
|
+
}
|
|
34540
|
+
let fileType;
|
|
34541
|
+
try {
|
|
34542
|
+
fileType = classifyClaudeExecutableBytes(readFileSync7(realPath).subarray(0, 16));
|
|
34543
|
+
} catch (err) {
|
|
34544
|
+
throw makeClaudePreflightError("Claude Code executable preflight failed: cannot read executable header before spawning Claude Code.", {
|
|
34545
|
+
code: codeValue(err, "EACCES"),
|
|
34546
|
+
errno: errnoValue(err),
|
|
34547
|
+
syscall: syscallValue(err),
|
|
34548
|
+
path: pathValue(err) ?? realPath,
|
|
34549
|
+
cwd,
|
|
34550
|
+
realPath,
|
|
34551
|
+
cause: err
|
|
34552
|
+
});
|
|
34553
|
+
}
|
|
34554
|
+
if (!["elf", "mach-o", "pe", "shebang-script"].includes(fileType)) {
|
|
34555
|
+
throw makeClaudePreflightError("Claude Code executable preflight failed: executable header is not an ELF, Mach-O, PE, or shebang script.", {
|
|
34556
|
+
code: "ENOEXEC",
|
|
34557
|
+
syscall: "exec",
|
|
34558
|
+
path,
|
|
34559
|
+
cwd,
|
|
34560
|
+
fileType,
|
|
34561
|
+
realPath
|
|
34562
|
+
});
|
|
34563
|
+
}
|
|
34564
|
+
return { path, realPath, cwd, realCwd, fileType };
|
|
34565
|
+
}
|
|
34566
|
+
function envFlagEnabled(value) {
|
|
34567
|
+
return value === "1" || value?.toLowerCase() === "true";
|
|
34568
|
+
}
|
|
34569
|
+
function wrapClaudeSpawnErrorForSdk(err, options) {
|
|
34570
|
+
const originalCode = codeValue(err, "SPAWN_ERROR");
|
|
34571
|
+
const originalMessage = err.message;
|
|
34572
|
+
const spawnPath = pathValue(err) ?? options.command;
|
|
34573
|
+
const cwd = options.cwd ?? process.cwd();
|
|
34574
|
+
const detail = [
|
|
34575
|
+
`code=${originalCode}`,
|
|
34576
|
+
`errno=${displayValue(errnoValue(err))}`,
|
|
34577
|
+
`syscall=${displayValue(syscallValue(err))}`,
|
|
34578
|
+
`path=${spawnPath}`,
|
|
34579
|
+
`cwd=${cwd}`,
|
|
34580
|
+
`command=${options.command}`
|
|
34581
|
+
].join(" ");
|
|
34582
|
+
const wrapped = new Error(`Claude Code spawn failed: ${originalMessage} (${detail})`);
|
|
34583
|
+
wrapped.name = "ClaudeSpawnDiagnosticError";
|
|
34584
|
+
wrapped.code = originalCode === "ENOENT" ? "CLAUDE_BRIDGE_SPAWN_FAILED" : originalCode;
|
|
34585
|
+
wrapped.originalCode = originalCode;
|
|
34586
|
+
wrapped.originalMessage = originalMessage;
|
|
34587
|
+
const errno = errnoValue(err);
|
|
34588
|
+
if (errno !== void 0) wrapped.errno = typeof errno === "number" ? errno : Number(errno);
|
|
34589
|
+
const syscall = syscallValue(err);
|
|
34590
|
+
if (syscall) wrapped.syscall = syscall;
|
|
34591
|
+
wrapped.path = spawnPath;
|
|
34592
|
+
wrapped.cwd = cwd;
|
|
34593
|
+
return wrapped;
|
|
34594
|
+
}
|
|
34595
|
+
function spawnClaudeCodeWithDiagnostics(options) {
|
|
34596
|
+
const pipeStderr = DEBUG || envFlagEnabled(options.env.DEBUG_CLAUDE_AGENT_SDK);
|
|
34597
|
+
const child = spawnProcess(options.command, options.args, {
|
|
34598
|
+
cwd: options.cwd,
|
|
34599
|
+
env: options.env,
|
|
34600
|
+
signal: options.signal,
|
|
34601
|
+
stdio: ["pipe", "pipe", pipeStderr ? "pipe" : "ignore"],
|
|
34602
|
+
windowsHide: true
|
|
34603
|
+
});
|
|
34604
|
+
if (pipeStderr) {
|
|
34605
|
+
child.stderr?.on("data", (data) => {
|
|
34606
|
+
for (const line of data.toString().split(/\r?\n/)) {
|
|
34607
|
+
if (line) debug(`[cli-stderr spawn] ${line}`);
|
|
34608
|
+
}
|
|
34609
|
+
});
|
|
34610
|
+
}
|
|
34611
|
+
child.prependListener("error", (err) => {
|
|
34612
|
+
const originalStack = err.stack;
|
|
34613
|
+
const wrapped = wrapClaudeSpawnErrorForSdk(err, options);
|
|
34614
|
+
Object.assign(err, wrapped);
|
|
34615
|
+
err.name = wrapped.name;
|
|
34616
|
+
err.message = wrapped.message;
|
|
34617
|
+
if (originalStack) err.stack = originalStack;
|
|
34618
|
+
});
|
|
34619
|
+
return {
|
|
34620
|
+
stdin: child.stdin,
|
|
34621
|
+
stdout: child.stdout,
|
|
34622
|
+
get killed() {
|
|
34623
|
+
return child.killed;
|
|
34624
|
+
},
|
|
34625
|
+
get exitCode() {
|
|
34626
|
+
return child.exitCode;
|
|
34627
|
+
},
|
|
34628
|
+
kill: child.kill.bind(child),
|
|
34629
|
+
on: child.on.bind(child),
|
|
34630
|
+
once: child.once.bind(child),
|
|
34631
|
+
off: child.off.bind(child)
|
|
34632
|
+
};
|
|
34633
|
+
}
|
|
34426
34634
|
var nextCliDebugSeq = 1;
|
|
34427
34635
|
function makeCliDebugOptions(tag) {
|
|
34428
34636
|
if (!DEBUG) return {};
|
|
@@ -35150,7 +35358,6 @@ function streamClaudeAgentSdk(model, context, options) {
|
|
|
35150
35358
|
ctx().latestCursor = 0;
|
|
35151
35359
|
const { mcpTools, customToolNameToSdk, customToolNameToPi } = resolveMcpTools(context);
|
|
35152
35360
|
const cwd = options?.cwd ?? process.cwd();
|
|
35153
|
-
const { sessionId: resumeSessionId } = syncSharedSession(context.messages, cwd, customToolNameToSdk, model.id);
|
|
35154
35361
|
const promptBlocks = extractUserPromptBlocks(context.messages);
|
|
35155
35362
|
let promptText = extractUserPrompt(context.messages) ?? "";
|
|
35156
35363
|
if (!promptText && !promptBlocks) {
|
|
@@ -35178,6 +35385,8 @@ function streamClaudeAgentSdk(model, context, options) {
|
|
|
35178
35385
|
const settingSources = appendSystemPrompt ? void 0 : providerSettings.settingSources ?? ["user", "project"];
|
|
35179
35386
|
const strictMcpConfigEnabled = !appendSystemPrompt && providerSettings.strictMcpConfig !== false;
|
|
35180
35387
|
const claudeExecutable = resolveClaudeExecutable(providerSettings.pathToClaudeCodeExecutable);
|
|
35388
|
+
const claudeExecutablePreflight = claudeExecutable ? preflightClaudeExecutable(claudeExecutable, cwd) : void 0;
|
|
35389
|
+
const { sessionId: resumeSessionId } = syncSharedSession(context.messages, cwd, customToolNameToSdk, model.id);
|
|
35181
35390
|
const effort = options?.reasoning ? model.thinkingLevelMap?.[options.reasoning] ?? REASONING_TO_EFFORT[options.reasoning] : void 0;
|
|
35182
35391
|
const extraArgs = { model: model.id };
|
|
35183
35392
|
if (strictMcpConfigEnabled) extraArgs["strict-mcp-config"] = null;
|
|
@@ -35200,6 +35409,7 @@ function streamClaudeAgentSdk(model, context, options) {
|
|
|
35200
35409
|
...mcpServers ? { mcpServers } : {},
|
|
35201
35410
|
...resumeSessionId ? { resume: resumeSessionId } : {},
|
|
35202
35411
|
...claudeExecutable ? { pathToClaudeCodeExecutable: claudeExecutable } : {},
|
|
35412
|
+
spawnClaudeCodeProcess: spawnClaudeCodeWithDiagnostics,
|
|
35203
35413
|
...makeCliDebugOptions("provider")
|
|
35204
35414
|
};
|
|
35205
35415
|
debug(
|
|
@@ -35207,6 +35417,7 @@ function streamClaudeAgentSdk(model, context, options) {
|
|
|
35207
35417
|
`model=${model.id} msgs=${context.messages.length} tools=${mcpTools.length}`,
|
|
35208
35418
|
`resume=${resumeSessionId?.slice(0, 8) ?? "none"} effort=${effort ?? "default"}`,
|
|
35209
35419
|
`appendSys=${appendSystemPrompt} promptCtx=${promptContextAppend.labels.join(",") || "none"} strictMcp=${strictMcpConfigEnabled}`,
|
|
35420
|
+
`claudeExec=${claudeExecutablePreflight ? `${claudeExecutablePreflight.fileType}:${claudeExecutablePreflight.path}` : "sdk-default"}`,
|
|
35210
35421
|
`prompt=${promptText.slice(0, 60)}${promptBlocks ? " [+images]" : ""}`
|
|
35211
35422
|
);
|
|
35212
35423
|
let wasAborted = false;
|
|
@@ -35376,8 +35587,12 @@ function index_default(pi) {
|
|
|
35376
35587
|
export {
|
|
35377
35588
|
CLAUDE_BRIDGE_TOOL_ISOLATION,
|
|
35378
35589
|
DISALLOWED_BUILTIN_TOOLS,
|
|
35590
|
+
classifyClaudeExecutableBytes,
|
|
35379
35591
|
index_default as default,
|
|
35380
35592
|
mapToolName,
|
|
35593
|
+
preflightClaudeExecutable,
|
|
35381
35594
|
restoreSharedSessionFromPi,
|
|
35382
|
-
shouldRestorePersistedBridgeEntry
|
|
35595
|
+
shouldRestorePersistedBridgeEntry,
|
|
35596
|
+
spawnClaudeCodeWithDiagnostics,
|
|
35597
|
+
wrapClaudeSpawnErrorForSdk
|
|
35383
35598
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vanillagreen/pi-claude-bridge",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"description": "Pi provider bridge that runs Claude Code through the Claude Agent SDK, with opt-in forwarding for Pi prompt context.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -108,15 +108,15 @@
|
|
|
108
108
|
"@earendil-works/pi-coding-agent": "*"
|
|
109
109
|
},
|
|
110
110
|
"devDependencies": {
|
|
111
|
-
"@earendil-works/pi-ai": "^0.
|
|
112
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
111
|
+
"@earendil-works/pi-ai": "^0.75.0",
|
|
112
|
+
"@earendil-works/pi-coding-agent": "^0.75.0",
|
|
113
113
|
"@types/node": "^24.3.0",
|
|
114
114
|
"esbuild": "^0.28.0",
|
|
115
115
|
"tsx": "^4.21.0",
|
|
116
116
|
"typescript": "^6.0.3"
|
|
117
117
|
},
|
|
118
118
|
"scripts": {
|
|
119
|
-
"build": "esbuild src/index.ts --bundle --platform=node --format=esm --target=
|
|
119
|
+
"build": "esbuild src/index.ts --bundle --platform=node --format=esm --target=node22 --outfile=bundle/index.js --external:@earendil-works/pi-ai --external:@earendil-works/pi-coding-agent",
|
|
120
120
|
"prepack": "npm run build",
|
|
121
121
|
"test:unit": "node --import tsx --test tests/unit-*.mjs",
|
|
122
122
|
"test": "set -a && [ -f .env.test ] && . ./.env.test; set +a && npm run test:unit && tests/int-smoke.sh && tests/int-multi-turn.sh && tests/int-cache.sh && node --import tsx --test tests/int-*.mjs",
|
|
@@ -145,6 +145,14 @@
|
|
|
145
145
|
"access": "public"
|
|
146
146
|
},
|
|
147
147
|
"engines": {
|
|
148
|
-
"node": ">=
|
|
148
|
+
"node": ">=22.19.0"
|
|
149
|
+
},
|
|
150
|
+
"peerDependenciesMeta": {
|
|
151
|
+
"@earendil-works/pi-ai": {
|
|
152
|
+
"optional": true
|
|
153
|
+
},
|
|
154
|
+
"@earendil-works/pi-coding-agent": {
|
|
155
|
+
"optional": true
|
|
156
|
+
}
|
|
149
157
|
}
|
|
150
158
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { calculateCost, getModels, type AssistantMessage, type AssistantMessageEventStream, type Context, type Model, type SimpleStreamOptions, type Tool } from "@earendil-works/pi-ai";
|
|
2
2
|
import * as piAi from "@earendil-works/pi-ai";
|
|
3
3
|
import { type ExtensionAPI, type ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { createSdkMcpServer, query, type EffortLevel, type SDKMessage, type SDKUserMessage, type SettingSource } from "@anthropic-ai/claude-agent-sdk";
|
|
4
|
+
import { createSdkMcpServer, query, type EffortLevel, type SDKMessage, type SDKUserMessage, type SettingSource, type SpawnOptions, type SpawnedProcess } from "@anthropic-ai/claude-agent-sdk";
|
|
5
5
|
import type { Base64ImageSource, ContentBlockParam, MessageParam } from "@anthropic-ai/sdk/resources";
|
|
6
6
|
import { createSession, deleteSession, openSession, repairToolPairing } from "cc-session-io";
|
|
7
|
+
import { spawn as spawnProcess } from "child_process";
|
|
7
8
|
import { createHash } from "crypto";
|
|
8
|
-
import { accessSync, appendFileSync, constants as fsConstants, mkdirSync, realpathSync, statSync } from "fs";
|
|
9
|
+
import { accessSync, appendFileSync, constants as fsConstants, mkdirSync, readFileSync, realpathSync, statSync } from "fs";
|
|
9
10
|
import { resolve as pathResolve } from "path";
|
|
10
11
|
import { homedir } from "os";
|
|
11
12
|
import { delimiter, dirname, join } from "path";
|
|
@@ -79,6 +80,240 @@ function resolveClaudeExecutable(configured?: string): string | undefined {
|
|
|
79
80
|
return executableFromPath("claude") ?? executableFromPath("claude-code");
|
|
80
81
|
}
|
|
81
82
|
|
|
83
|
+
export type ClaudeExecutableFileType = "elf" | "mach-o" | "pe" | "shebang-script" | "empty" | "unknown";
|
|
84
|
+
|
|
85
|
+
export interface ClaudeExecutablePreflightResult {
|
|
86
|
+
path: string;
|
|
87
|
+
realPath: string;
|
|
88
|
+
cwd: string;
|
|
89
|
+
realCwd: string;
|
|
90
|
+
fileType: ClaudeExecutableFileType;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function errnoValue(err: unknown): string | number | undefined {
|
|
94
|
+
return typeof (err as NodeJS.ErrnoException)?.errno === "number" ? (err as NodeJS.ErrnoException).errno : undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function syscallValue(err: unknown): string | undefined {
|
|
98
|
+
return typeof (err as NodeJS.ErrnoException)?.syscall === "string" ? (err as NodeJS.ErrnoException).syscall : undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function pathValue(err: unknown): string | undefined {
|
|
102
|
+
const value = (err as NodeJS.ErrnoException)?.path;
|
|
103
|
+
return typeof value === "string" ? value : undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function codeValue(err: unknown, fallback: string): string {
|
|
107
|
+
const value = (err as NodeJS.ErrnoException)?.code;
|
|
108
|
+
return typeof value === "string" ? value : fallback;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function displayValue(value: unknown): string {
|
|
112
|
+
return value === undefined || value === null || value === "" ? "<none>" : String(value);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function makeClaudePreflightError(
|
|
116
|
+
summary: string,
|
|
117
|
+
details: { code: string; errno?: string | number; syscall?: string; path: string; cwd: string; fileType?: ClaudeExecutableFileType; realPath?: string; cause?: unknown },
|
|
118
|
+
): Error & NodeJS.ErrnoException & { cwd: string; fileType?: ClaudeExecutableFileType; realPath?: string } {
|
|
119
|
+
const detail = [
|
|
120
|
+
`code=${details.code}`,
|
|
121
|
+
`errno=${displayValue(details.errno)}`,
|
|
122
|
+
`syscall=${displayValue(details.syscall)}`,
|
|
123
|
+
`path=${details.path}`,
|
|
124
|
+
`cwd=${details.cwd}`,
|
|
125
|
+
...(details.fileType ? [`fileType=${details.fileType}`] : []),
|
|
126
|
+
...(details.realPath ? [`realPath=${details.realPath}`] : []),
|
|
127
|
+
].join(" ");
|
|
128
|
+
const error = new Error(`${summary} (${detail})`) as Error & NodeJS.ErrnoException & { cwd: string; fileType?: ClaudeExecutableFileType; realPath?: string };
|
|
129
|
+
error.name = "ClaudeExecutablePreflightError";
|
|
130
|
+
error.code = details.code;
|
|
131
|
+
if (details.errno !== undefined) error.errno = typeof details.errno === "number" ? details.errno : Number(details.errno);
|
|
132
|
+
if (details.syscall) error.syscall = details.syscall;
|
|
133
|
+
error.path = details.path;
|
|
134
|
+
error.cwd = details.cwd;
|
|
135
|
+
if (details.fileType) error.fileType = details.fileType;
|
|
136
|
+
if (details.realPath) error.realPath = details.realPath;
|
|
137
|
+
if (details.cause !== undefined) (error as Error & { cause?: unknown }).cause = details.cause;
|
|
138
|
+
return error;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function classifyClaudeExecutableBytes(bytes: Uint8Array): ClaudeExecutableFileType {
|
|
142
|
+
if (bytes.length === 0) return "empty";
|
|
143
|
+
if (bytes.length >= 2 && bytes[0] === 0x23 && bytes[1] === 0x21) return "shebang-script";
|
|
144
|
+
if (bytes.length >= 4 && bytes[0] === 0x7f && bytes[1] === 0x45 && bytes[2] === 0x4c && bytes[3] === 0x46) return "elf";
|
|
145
|
+
if (bytes.length >= 2 && bytes[0] === 0x4d && bytes[1] === 0x5a) return "pe";
|
|
146
|
+
if (bytes.length >= 4) {
|
|
147
|
+
const magic = bytes[0] * 0x1000000 + bytes[1] * 0x10000 + bytes[2] * 0x100 + bytes[3];
|
|
148
|
+
if (
|
|
149
|
+
magic === 0xfeedface ||
|
|
150
|
+
magic === 0xfeedfacf ||
|
|
151
|
+
magic === 0xcefaedfe ||
|
|
152
|
+
magic === 0xcffaedfe ||
|
|
153
|
+
magic === 0xcafebabe ||
|
|
154
|
+
magic === 0xbebafeca
|
|
155
|
+
) return "mach-o";
|
|
156
|
+
}
|
|
157
|
+
return "unknown";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function preflightClaudeExecutable(path: string, cwd: string): ClaudeExecutablePreflightResult {
|
|
161
|
+
let realCwd = cwd;
|
|
162
|
+
try {
|
|
163
|
+
const cwdStat = statSync(cwd);
|
|
164
|
+
if (!cwdStat.isDirectory()) {
|
|
165
|
+
throw makeClaudePreflightError("Claude Code spawn cwd preflight failed: cwd is not a directory.", {
|
|
166
|
+
code: "ENOTDIR",
|
|
167
|
+
syscall: "chdir",
|
|
168
|
+
path: cwd,
|
|
169
|
+
cwd,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
accessSync(cwd, fsConstants.X_OK);
|
|
173
|
+
realCwd = realpathSync(cwd);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
if ((err as Error).name === "ClaudeExecutablePreflightError") throw err;
|
|
176
|
+
throw makeClaudePreflightError("Claude Code spawn cwd preflight failed: cwd is not reachable before spawning Claude Code.", {
|
|
177
|
+
code: codeValue(err, "EACCES"),
|
|
178
|
+
errno: errnoValue(err),
|
|
179
|
+
syscall: syscallValue(err),
|
|
180
|
+
path: pathValue(err) ?? cwd,
|
|
181
|
+
cwd,
|
|
182
|
+
cause: err,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let realPath = path;
|
|
187
|
+
try {
|
|
188
|
+
const stat = statSync(path);
|
|
189
|
+
if (!stat.isFile()) {
|
|
190
|
+
throw makeClaudePreflightError("Claude Code executable preflight failed: resolved path is not a file.", {
|
|
191
|
+
code: "EACCES",
|
|
192
|
+
syscall: "exec",
|
|
193
|
+
path,
|
|
194
|
+
cwd,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
accessSync(path, fsConstants.X_OK);
|
|
198
|
+
realPath = realpathSync(path);
|
|
199
|
+
} catch (err) {
|
|
200
|
+
if ((err as Error).name === "ClaudeExecutablePreflightError") throw err;
|
|
201
|
+
throw makeClaudePreflightError("Claude Code executable preflight failed: cannot access resolved executable before spawning Claude Code.", {
|
|
202
|
+
code: codeValue(err, "ENOENT"),
|
|
203
|
+
errno: errnoValue(err),
|
|
204
|
+
syscall: syscallValue(err),
|
|
205
|
+
path: pathValue(err) ?? path,
|
|
206
|
+
cwd,
|
|
207
|
+
cause: err,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
let fileType: ClaudeExecutableFileType;
|
|
212
|
+
try {
|
|
213
|
+
fileType = classifyClaudeExecutableBytes(readFileSync(realPath).subarray(0, 16));
|
|
214
|
+
} catch (err) {
|
|
215
|
+
throw makeClaudePreflightError("Claude Code executable preflight failed: cannot read executable header before spawning Claude Code.", {
|
|
216
|
+
code: codeValue(err, "EACCES"),
|
|
217
|
+
errno: errnoValue(err),
|
|
218
|
+
syscall: syscallValue(err),
|
|
219
|
+
path: pathValue(err) ?? realPath,
|
|
220
|
+
cwd,
|
|
221
|
+
realPath,
|
|
222
|
+
cause: err,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!["elf", "mach-o", "pe", "shebang-script"].includes(fileType)) {
|
|
227
|
+
throw makeClaudePreflightError("Claude Code executable preflight failed: executable header is not an ELF, Mach-O, PE, or shebang script.", {
|
|
228
|
+
code: "ENOEXEC",
|
|
229
|
+
syscall: "exec",
|
|
230
|
+
path,
|
|
231
|
+
cwd,
|
|
232
|
+
fileType,
|
|
233
|
+
realPath,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return { path, realPath, cwd, realCwd, fileType };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function envFlagEnabled(value: string | undefined): boolean {
|
|
241
|
+
return value === "1" || value?.toLowerCase() === "true";
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function wrapClaudeSpawnErrorForSdk(err: Error, options: SpawnOptions): Error & NodeJS.ErrnoException & { cwd: string; originalCode?: string; originalMessage?: string } {
|
|
245
|
+
const originalCode = codeValue(err, "SPAWN_ERROR");
|
|
246
|
+
const originalMessage = err.message;
|
|
247
|
+
const spawnPath = pathValue(err) ?? options.command;
|
|
248
|
+
const cwd = options.cwd ?? process.cwd();
|
|
249
|
+
const detail = [
|
|
250
|
+
`code=${originalCode}`,
|
|
251
|
+
`errno=${displayValue(errnoValue(err))}`,
|
|
252
|
+
`syscall=${displayValue(syscallValue(err))}`,
|
|
253
|
+
`path=${spawnPath}`,
|
|
254
|
+
`cwd=${cwd}`,
|
|
255
|
+
`command=${options.command}`,
|
|
256
|
+
].join(" ");
|
|
257
|
+
const wrapped = new Error(`Claude Code spawn failed: ${originalMessage} (${detail})`) as Error & NodeJS.ErrnoException & { cwd: string; originalCode?: string; originalMessage?: string };
|
|
258
|
+
wrapped.name = "ClaudeSpawnDiagnosticError";
|
|
259
|
+
// The SDK special-cases code === ENOENT and replaces the message with its
|
|
260
|
+
// generic "native binary not found" text. Preserve the original code in the
|
|
261
|
+
// message/originalCode while using a bridge code so the SDK surfaces context.
|
|
262
|
+
wrapped.code = originalCode === "ENOENT" ? "CLAUDE_BRIDGE_SPAWN_FAILED" : originalCode;
|
|
263
|
+
wrapped.originalCode = originalCode;
|
|
264
|
+
wrapped.originalMessage = originalMessage;
|
|
265
|
+
const errno = errnoValue(err);
|
|
266
|
+
if (errno !== undefined) wrapped.errno = typeof errno === "number" ? errno : Number(errno);
|
|
267
|
+
const syscall = syscallValue(err);
|
|
268
|
+
if (syscall) wrapped.syscall = syscall;
|
|
269
|
+
wrapped.path = spawnPath;
|
|
270
|
+
wrapped.cwd = cwd;
|
|
271
|
+
// Do not set `cause` here: the listener copies these structured fields back
|
|
272
|
+
// onto the original Error. A cause reference to that same object would become
|
|
273
|
+
// `err.cause === err`, making JSON.stringify throw on a circular structure.
|
|
274
|
+
// originalMessage plus code/errno/syscall/path/cwd preserve the useful data.
|
|
275
|
+
return wrapped;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function spawnClaudeCodeWithDiagnostics(options: SpawnOptions): SpawnedProcess {
|
|
279
|
+
const pipeStderr = DEBUG || envFlagEnabled(options.env.DEBUG_CLAUDE_AGENT_SDK);
|
|
280
|
+
const child = spawnProcess(options.command, options.args, {
|
|
281
|
+
cwd: options.cwd,
|
|
282
|
+
env: options.env,
|
|
283
|
+
signal: options.signal,
|
|
284
|
+
stdio: ["pipe", "pipe", pipeStderr ? "pipe" : "ignore"],
|
|
285
|
+
windowsHide: true,
|
|
286
|
+
});
|
|
287
|
+
if (pipeStderr) {
|
|
288
|
+
child.stderr?.on("data", (data) => {
|
|
289
|
+
for (const line of data.toString().split(/\r?\n/)) {
|
|
290
|
+
if (line) debug(`[cli-stderr spawn] ${line}`);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
child.prependListener("error", (err) => {
|
|
295
|
+
const originalStack = err.stack;
|
|
296
|
+
const wrapped = wrapClaudeSpawnErrorForSdk(err, options);
|
|
297
|
+
Object.assign(err, wrapped);
|
|
298
|
+
err.name = wrapped.name;
|
|
299
|
+
err.message = wrapped.message;
|
|
300
|
+
// Keep V8's stack from the actual Node spawn failure, not the wrapper
|
|
301
|
+
// construction site. Diagnostic fields above remain enumerable and
|
|
302
|
+
// JSON-serializable; stack stays the spawn-time breadcrumb for operators.
|
|
303
|
+
if (originalStack) err.stack = originalStack;
|
|
304
|
+
});
|
|
305
|
+
return {
|
|
306
|
+
stdin: child.stdin,
|
|
307
|
+
stdout: child.stdout,
|
|
308
|
+
get killed() { return child.killed; },
|
|
309
|
+
get exitCode() { return child.exitCode; },
|
|
310
|
+
kill: child.kill.bind(child),
|
|
311
|
+
on: child.on.bind(child),
|
|
312
|
+
once: child.once.bind(child),
|
|
313
|
+
off: child.off.bind(child),
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
82
317
|
// Per-query CLI debug capture. When CLAUDE_BRIDGE_DEBUG=1, ask the Claude Code
|
|
83
318
|
// CLI subprocess to write its own debug log to a file we choose, and also
|
|
84
319
|
// forward its stderr into our debug stream. Drops straight into the real SDK's
|
|
@@ -1072,7 +1307,6 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1072
1307
|
|
|
1073
1308
|
const { mcpTools, customToolNameToSdk, customToolNameToPi } = resolveMcpTools(context);
|
|
1074
1309
|
const cwd = (options as { cwd?: string } | undefined)?.cwd ?? process.cwd();
|
|
1075
|
-
const { sessionId: resumeSessionId } = syncSharedSession(context.messages, cwd, customToolNameToSdk, model.id);
|
|
1076
1310
|
const promptBlocks = extractUserPromptBlocks(context.messages);
|
|
1077
1311
|
let promptText = extractUserPrompt(context.messages) ?? "";
|
|
1078
1312
|
|
|
@@ -1114,6 +1348,8 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1114
1348
|
: providerSettings.settingSources ?? ["user", "project"];
|
|
1115
1349
|
const strictMcpConfigEnabled = !appendSystemPrompt && providerSettings.strictMcpConfig !== false;
|
|
1116
1350
|
const claudeExecutable = resolveClaudeExecutable(providerSettings.pathToClaudeCodeExecutable);
|
|
1351
|
+
const claudeExecutablePreflight = claudeExecutable ? preflightClaudeExecutable(claudeExecutable, cwd) : undefined;
|
|
1352
|
+
const { sessionId: resumeSessionId } = syncSharedSession(context.messages, cwd, customToolNameToSdk, model.id);
|
|
1117
1353
|
|
|
1118
1354
|
// Prefer the model's own thinkingLevelMap when present (pi-ai 0.72+ ships
|
|
1119
1355
|
// per-model overrides — e.g. opus-4-7 wants xhigh→xhigh, not xhigh→max).
|
|
@@ -1156,6 +1392,7 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1156
1392
|
...(mcpServers ? { mcpServers } : {}),
|
|
1157
1393
|
...(resumeSessionId ? { resume: resumeSessionId } : {}),
|
|
1158
1394
|
...(claudeExecutable ? { pathToClaudeCodeExecutable: claudeExecutable } : {}),
|
|
1395
|
+
spawnClaudeCodeProcess: spawnClaudeCodeWithDiagnostics,
|
|
1159
1396
|
...makeCliDebugOptions("provider"),
|
|
1160
1397
|
};
|
|
1161
1398
|
|
|
@@ -1163,6 +1400,7 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1163
1400
|
`model=${model.id} msgs=${context.messages.length} tools=${mcpTools.length}`,
|
|
1164
1401
|
`resume=${resumeSessionId?.slice(0, 8) ?? "none"} effort=${effort ?? "default"}`,
|
|
1165
1402
|
`appendSys=${appendSystemPrompt} promptCtx=${promptContextAppend.labels.join(",") || "none"} strictMcp=${strictMcpConfigEnabled}`,
|
|
1403
|
+
`claudeExec=${claudeExecutablePreflight ? `${claudeExecutablePreflight.fileType}:${claudeExecutablePreflight.path}` : "sdk-default"}`,
|
|
1166
1404
|
`prompt=${promptText.slice(0, 60)}${promptBlocks ? " [+images]" : ""}`);
|
|
1167
1405
|
|
|
1168
1406
|
// 3. Start SDK query and claim it for this context
|
package/src/prompt-context.ts
CHANGED
|
@@ -76,8 +76,15 @@ function extractHeadingSection(systemPrompt: string | undefined, headings: strin
|
|
|
76
76
|
}
|
|
77
77
|
if (start < 0) return undefined;
|
|
78
78
|
const rest = systemPrompt.slice(start).trim();
|
|
79
|
-
const
|
|
80
|
-
|
|
79
|
+
const endCandidates = [
|
|
80
|
+
rest.slice(1).search(/\n##\s+/),
|
|
81
|
+
rest.search(/\n<\/project_instructions>/),
|
|
82
|
+
rest.search(/\n<\/project_context>/),
|
|
83
|
+
]
|
|
84
|
+
.map((index, offset) => (index >= 0 && offset === 0 ? index + 1 : index))
|
|
85
|
+
.filter((index) => index >= 0);
|
|
86
|
+
const end = endCandidates.length > 0 ? Math.min(...endCandidates) : -1;
|
|
87
|
+
return (end >= 0 ? rest.slice(0, end) : rest).trim();
|
|
81
88
|
}
|
|
82
89
|
|
|
83
90
|
function extractBlockByMarkers(systemPrompt: string | undefined, markers: RegExp[]): string | undefined {
|
|
@@ -93,7 +100,7 @@ export function buildPromptContextAppend(systemPrompt: string | undefined, cwd:
|
|
|
93
100
|
|
|
94
101
|
if (settings.includeAppendSystemPromptMd) {
|
|
95
102
|
for (const file of readAppendSystemPromptFiles(cwd)) {
|
|
96
|
-
parts.push(
|
|
103
|
+
parts.push(xmlBlock("append_system_prompt", { label: file.label }, file.content));
|
|
97
104
|
labels.push(file.label);
|
|
98
105
|
}
|
|
99
106
|
}
|
|
@@ -101,7 +108,7 @@ export function buildPromptContextAppend(systemPrompt: string | undefined, cwd:
|
|
|
101
108
|
if (settings.includeProjectAgentsHook) {
|
|
102
109
|
const projectAgents = extractHeadingSection(systemPrompt, ["## Project Agents", "## Project Subagents"]);
|
|
103
110
|
if (projectAgents) {
|
|
104
|
-
parts.push(
|
|
111
|
+
parts.push(xmlBlock("before_agent_start", { source: "project-agents" }, projectAgents));
|
|
105
112
|
labels.push("project agents hook");
|
|
106
113
|
}
|
|
107
114
|
}
|
|
@@ -109,7 +116,7 @@ export function buildPromptContextAppend(systemPrompt: string | undefined, cwd:
|
|
|
109
116
|
if (settings.includeTaskPanelHook) {
|
|
110
117
|
const taskReminder = extractBlockByMarkers(systemPrompt, [/^Task workflow reminder:/]);
|
|
111
118
|
if (taskReminder) {
|
|
112
|
-
parts.push(
|
|
119
|
+
parts.push(xmlBlock("before_agent_start", { source: "task-panel" }, taskReminder));
|
|
113
120
|
labels.push("task panel hook");
|
|
114
121
|
}
|
|
115
122
|
}
|
|
@@ -117,7 +124,7 @@ export function buildPromptContextAppend(systemPrompt: string | undefined, cwd:
|
|
|
117
124
|
if (settings.includeCavemanHook) {
|
|
118
125
|
const caveman = extractBlockByMarkers(systemPrompt, [/^You MUST respond in caveman /m]);
|
|
119
126
|
if (caveman) {
|
|
120
|
-
parts.push(
|
|
127
|
+
parts.push(xmlBlock("before_agent_start", { source: "caveman" }, caveman));
|
|
121
128
|
labels.push("caveman hook");
|
|
122
129
|
}
|
|
123
130
|
}
|
|
@@ -125,10 +132,37 @@ export function buildPromptContextAppend(systemPrompt: string | undefined, cwd:
|
|
|
125
132
|
if (parts.length === 0) return { labels };
|
|
126
133
|
return {
|
|
127
134
|
labels,
|
|
128
|
-
text:
|
|
129
|
-
"
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
135
|
+
text: xmlBlock(
|
|
136
|
+
"forwarded_pi_context",
|
|
137
|
+
{},
|
|
138
|
+
[
|
|
139
|
+
"The following content was explicitly enabled in pi-claude-bridge settings and comes from Pi prompt files or before_agent_start prompt hooks.",
|
|
140
|
+
...parts,
|
|
141
|
+
].join("\n\n"),
|
|
142
|
+
false,
|
|
143
|
+
),
|
|
133
144
|
};
|
|
134
145
|
}
|
|
146
|
+
|
|
147
|
+
function escapeXmlAttr(value: string): string {
|
|
148
|
+
return value
|
|
149
|
+
.replace(/&/g, "&")
|
|
150
|
+
.replace(/"/g, """)
|
|
151
|
+
.replace(/</g, "<")
|
|
152
|
+
.replace(/>/g, ">");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function escapeXmlText(value: string): string {
|
|
156
|
+
return value
|
|
157
|
+
.replace(/&/g, "&")
|
|
158
|
+
.replace(/</g, "<")
|
|
159
|
+
.replace(/>/g, ">");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function xmlBlock(tag: string, attrs: Record<string, string>, content: string, escapeContent = true): string {
|
|
163
|
+
const attrText = Object.entries(attrs)
|
|
164
|
+
.map(([key, value]) => ` ${key}="${escapeXmlAttr(value)}"`)
|
|
165
|
+
.join("");
|
|
166
|
+
const body = escapeContent ? escapeXmlText(content.trim()) : content.trim();
|
|
167
|
+
return `<${tag}${attrText}>\n${body}\n</${tag}>`;
|
|
168
|
+
}
|