@vanillagreen/pi-claude-bridge 1.0.6 → 1.1.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/bundle/index.js +185 -17
- package/package.json +1 -1
- package/src/convert.ts +43 -5
- package/src/index.ts +107 -5
- package/src/typebox-to-zod.ts +19 -5
package/bundle/index.js
CHANGED
|
@@ -18744,9 +18744,22 @@ function lA($, X) {
|
|
|
18744
18744
|
import { randomUUID } from "crypto";
|
|
18745
18745
|
import { mkdirSync as mkdirSync2, writeFileSync, appendFileSync as appendFileSync2, existsSync as existsSync2, rmSync as rmSync2 } from "fs";
|
|
18746
18746
|
import { dirname } from "path";
|
|
18747
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
18747
18748
|
import { realpathSync as realpathSync2 } from "fs";
|
|
18748
18749
|
import { homedir } from "os";
|
|
18749
18750
|
import { join } from "path";
|
|
18751
|
+
function parseJsonl(content) {
|
|
18752
|
+
return content.split("\n").filter((line) => line.trim()).map(parseRecord);
|
|
18753
|
+
}
|
|
18754
|
+
function parseJsonlFile(path) {
|
|
18755
|
+
return parseJsonl(readFileSync2(path, "utf-8"));
|
|
18756
|
+
}
|
|
18757
|
+
function parseRecord(line) {
|
|
18758
|
+
const raw = JSON.parse(line);
|
|
18759
|
+
if (raw.type === "user") return raw;
|
|
18760
|
+
if (raw.type === "assistant") return raw;
|
|
18761
|
+
return raw;
|
|
18762
|
+
}
|
|
18750
18763
|
function serializeRecord(record2) {
|
|
18751
18764
|
return JSON.stringify(record2);
|
|
18752
18765
|
}
|
|
@@ -19147,8 +19160,31 @@ function createSession(opts) {
|
|
|
19147
19160
|
model: opts.model
|
|
19148
19161
|
});
|
|
19149
19162
|
}
|
|
19163
|
+
function openSession(opts) {
|
|
19164
|
+
const projectPath = normalizeProjectPath(opts.projectPath);
|
|
19165
|
+
const jsonlPath = getSessionPath(opts.sessionId, projectPath, opts.claudeDir);
|
|
19166
|
+
return readSession(jsonlPath, projectPath);
|
|
19167
|
+
}
|
|
19168
|
+
function readSession(jsonlPath, projectPath) {
|
|
19169
|
+
const records = parseJsonlFile(jsonlPath);
|
|
19170
|
+
const firstMsg = records.find((r6) => r6.type === "user" || r6.type === "assistant");
|
|
19171
|
+
const sessionId = firstMsg?.sessionId ?? "";
|
|
19172
|
+
const resolvedProjectPath = projectPath ?? firstMsg?.cwd ?? "";
|
|
19173
|
+
return new Session({
|
|
19174
|
+
sessionId,
|
|
19175
|
+
projectPath: resolvedProjectPath,
|
|
19176
|
+
jsonlPath,
|
|
19177
|
+
slug: firstMsg?.slug,
|
|
19178
|
+
cwd: firstMsg?.cwd,
|
|
19179
|
+
version: firstMsg?.version,
|
|
19180
|
+
gitBranch: firstMsg?.gitBranch,
|
|
19181
|
+
records,
|
|
19182
|
+
fileExists: true
|
|
19183
|
+
});
|
|
19184
|
+
}
|
|
19150
19185
|
|
|
19151
19186
|
// src/index.ts
|
|
19187
|
+
import { createHash } from "crypto";
|
|
19152
19188
|
import { accessSync, appendFileSync as appendFileSync3, constants as fsConstants, mkdirSync as mkdirSync3, realpathSync as realpathSync3, statSync as statSync3 } from "fs";
|
|
19153
19189
|
import { homedir as homedir5 } from "os";
|
|
19154
19190
|
import { delimiter, dirname as dirname5, join as join5 } from "path";
|
|
@@ -19275,6 +19311,38 @@ function messageContentToText(content) {
|
|
|
19275
19311
|
}
|
|
19276
19312
|
return hasText ? parts.join("\n") : "";
|
|
19277
19313
|
}
|
|
19314
|
+
function imageBlockToAnthropic(block) {
|
|
19315
|
+
if (!block.data || !block.mimeType) return void 0;
|
|
19316
|
+
return { type: "image", source: { type: "base64", media_type: block.mimeType, data: block.data } };
|
|
19317
|
+
}
|
|
19318
|
+
function toolResultContentToAnthropic(content) {
|
|
19319
|
+
if (typeof content === "string") return content;
|
|
19320
|
+
if (!Array.isArray(content)) return "";
|
|
19321
|
+
const blocks = [];
|
|
19322
|
+
for (const block of content) {
|
|
19323
|
+
if (block.type === "text" && block.text) {
|
|
19324
|
+
blocks.push({ type: "text", text: block.text });
|
|
19325
|
+
} else if (block.type === "image") {
|
|
19326
|
+
const image = imageBlockToAnthropic(block);
|
|
19327
|
+
if (image) blocks.push(image);
|
|
19328
|
+
} else if (block.type) {
|
|
19329
|
+
blocks.push({ type: "text", text: `[${block.type}]` });
|
|
19330
|
+
}
|
|
19331
|
+
}
|
|
19332
|
+
if (blocks.length === 0) return "";
|
|
19333
|
+
if (blocks.every((block) => block.type === "text")) return blocks.map((block) => block.text).join("\n");
|
|
19334
|
+
return blocks;
|
|
19335
|
+
}
|
|
19336
|
+
function assistantProvenancePrefix(msg) {
|
|
19337
|
+
if (msg.role !== "assistant") return void 0;
|
|
19338
|
+
const provider = typeof msg.provider === "string" ? msg.provider : void 0;
|
|
19339
|
+
const model = typeof msg.model === "string" ? msg.model : void 0;
|
|
19340
|
+
const api = typeof msg.api === "string" ? msg.api : void 0;
|
|
19341
|
+
if (!provider && !model && !api) return void 0;
|
|
19342
|
+
if (provider === PROVIDER_ID || api === "anthropic") return void 0;
|
|
19343
|
+
return `[Prior Pi assistant response from ${provider ?? api ?? "unknown-provider"}${model ? `/${model}` : ""}]
|
|
19344
|
+
`;
|
|
19345
|
+
}
|
|
19278
19346
|
function convertPiMessages(messages, customToolNameToSdk) {
|
|
19279
19347
|
const anthropicMessages = [];
|
|
19280
19348
|
const sanitizedIds = /* @__PURE__ */ new Map();
|
|
@@ -19287,16 +19355,18 @@ function convertPiMessages(messages, customToolNameToSdk) {
|
|
|
19287
19355
|
for (const block of msg.content) {
|
|
19288
19356
|
if (block.type === "text" && block.text) parts.push({ type: "text", text: block.text });
|
|
19289
19357
|
else if (block.type === "image" && block.data && block.mimeType) {
|
|
19290
|
-
parts.push(
|
|
19358
|
+
parts.push(imageBlockToAnthropic(block));
|
|
19291
19359
|
}
|
|
19292
19360
|
}
|
|
19293
|
-
anthropicMessages.push({ role: "user", content: parts.length ? parts : "[image]" });
|
|
19361
|
+
anthropicMessages.push({ role: "user", content: parts.filter(Boolean).length ? parts.filter(Boolean) : "[image]" });
|
|
19294
19362
|
} else {
|
|
19295
19363
|
anthropicMessages.push({ role: "user", content: "[empty]" });
|
|
19296
19364
|
}
|
|
19297
19365
|
} else if (msg.role === "assistant") {
|
|
19298
19366
|
const content = Array.isArray(msg.content) ? msg.content : [];
|
|
19299
19367
|
const blocks = [];
|
|
19368
|
+
const provenance = assistantProvenancePrefix(msg);
|
|
19369
|
+
if (provenance) blocks.push({ type: "text", text: provenance });
|
|
19300
19370
|
for (const block of content) {
|
|
19301
19371
|
if (block.type === "text" && block.text) {
|
|
19302
19372
|
blocks.push({ type: "text", text: block.text });
|
|
@@ -19314,10 +19384,10 @@ function convertPiMessages(messages, customToolNameToSdk) {
|
|
|
19314
19384
|
if (!blocks.length) blocks.push({ type: "text", text: "[incompatible content omitted]" });
|
|
19315
19385
|
anthropicMessages.push({ role: "assistant", content: blocks });
|
|
19316
19386
|
} else if (msg.role === "toolResult") {
|
|
19317
|
-
const
|
|
19387
|
+
const content = toolResultContentToAnthropic(msg.content);
|
|
19318
19388
|
anthropicMessages.push({
|
|
19319
19389
|
role: "user",
|
|
19320
|
-
content: [{ type: "tool_result", tool_use_id: sanitizeToolId(msg.toolCallId, sanitizedIds), content:
|
|
19390
|
+
content: [{ type: "tool_result", tool_use_id: sanitizeToolId(msg.toolCallId, sanitizedIds), content: content || "", is_error: msg.isError }]
|
|
19321
19391
|
});
|
|
19322
19392
|
}
|
|
19323
19393
|
}
|
|
@@ -19360,7 +19430,7 @@ function rewriteSkillsBlock(skillsBlock) {
|
|
|
19360
19430
|
}
|
|
19361
19431
|
|
|
19362
19432
|
// src/session-verify.ts
|
|
19363
|
-
import { statSync as statSync2, readFileSync as
|
|
19433
|
+
import { statSync as statSync2, readFileSync as readFileSync3 } from "fs";
|
|
19364
19434
|
function verifyWrittenSession(jsonlPath, expectedSessionId, expectedRecordCount) {
|
|
19365
19435
|
const warnings = [];
|
|
19366
19436
|
let st;
|
|
@@ -19372,7 +19442,7 @@ function verifyWrittenSession(jsonlPath, expectedSessionId, expectedRecordCount)
|
|
|
19372
19442
|
}
|
|
19373
19443
|
let content;
|
|
19374
19444
|
try {
|
|
19375
|
-
content =
|
|
19445
|
+
content = readFileSync3(jsonlPath, "utf8");
|
|
19376
19446
|
} catch (e2) {
|
|
19377
19447
|
warnings.push(`file unreadable \u2014 path=${jsonlPath} size=${st.size} err=${e2.message}`);
|
|
19378
19448
|
return warnings;
|
|
@@ -19484,7 +19554,7 @@ function popContext() {
|
|
|
19484
19554
|
}
|
|
19485
19555
|
|
|
19486
19556
|
// src/config.ts
|
|
19487
|
-
import { existsSync as existsSync3, readFileSync as
|
|
19557
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
|
|
19488
19558
|
import { homedir as homedir2 } from "os";
|
|
19489
19559
|
import { dirname as dirname2, join as join2, resolve } from "path";
|
|
19490
19560
|
var PACKAGE_ID = "@vanillagreen/pi-claude-bridge";
|
|
@@ -19525,7 +19595,7 @@ function settingsPaths(cwd) {
|
|
|
19525
19595
|
function tryParseJson(path) {
|
|
19526
19596
|
if (!existsSync3(path)) return {};
|
|
19527
19597
|
try {
|
|
19528
|
-
return JSON.parse(
|
|
19598
|
+
return JSON.parse(readFileSync4(path, "utf-8"));
|
|
19529
19599
|
} catch (e2) {
|
|
19530
19600
|
console.error(`claude-bridge: failed to parse ${path}: ${e2}`);
|
|
19531
19601
|
return {};
|
|
@@ -19536,7 +19606,7 @@ function readManagerConfig(cwd) {
|
|
|
19536
19606
|
for (const path of settingsPaths(cwd)) {
|
|
19537
19607
|
if (!existsSync3(path)) continue;
|
|
19538
19608
|
try {
|
|
19539
|
-
const parsed = JSON.parse(
|
|
19609
|
+
const parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
19540
19610
|
const configRoot = asRecord(asRecord(asRecord(parsed?.vstack)?.extensionManager)?.config);
|
|
19541
19611
|
const config2 = asRecord(configRoot?.[PACKAGE_ID]);
|
|
19542
19612
|
if (config2) mergeDeep(merged, config2);
|
|
@@ -19587,7 +19657,7 @@ function loadConfig(cwd) {
|
|
|
19587
19657
|
}
|
|
19588
19658
|
|
|
19589
19659
|
// src/agents-md.ts
|
|
19590
|
-
import { existsSync as existsSync4, readFileSync as
|
|
19660
|
+
import { existsSync as existsSync4, readFileSync as readFileSync5 } from "fs";
|
|
19591
19661
|
import { homedir as homedir3 } from "os";
|
|
19592
19662
|
import { dirname as dirname3, join as join3, resolve as resolve2 } from "path";
|
|
19593
19663
|
var GLOBAL_AGENTS_PATH = join3(homedir3(), ".pi", "agent", "AGENTS.md");
|
|
@@ -19612,7 +19682,7 @@ function extractAgentsAppend() {
|
|
|
19612
19682
|
const agentsPath = resolveAgentsMdPath();
|
|
19613
19683
|
if (!agentsPath) return void 0;
|
|
19614
19684
|
try {
|
|
19615
|
-
const content =
|
|
19685
|
+
const content = readFileSync5(agentsPath, "utf-8").trim();
|
|
19616
19686
|
if (!content) return void 0;
|
|
19617
19687
|
const sanitized = sanitizeAgentsContent(content);
|
|
19618
19688
|
return sanitized.length > 0 ? `# CLAUDE.md
|
|
@@ -19632,7 +19702,7 @@ function sanitizeAgentsContent(content) {
|
|
|
19632
19702
|
}
|
|
19633
19703
|
|
|
19634
19704
|
// src/prompt-context.ts
|
|
19635
|
-
import { existsSync as existsSync5, readFileSync as
|
|
19705
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6 } from "fs";
|
|
19636
19706
|
import { homedir as homedir4 } from "os";
|
|
19637
19707
|
import { dirname as dirname4, join as join4, resolve as resolve3 } from "path";
|
|
19638
19708
|
function piUserDir2() {
|
|
@@ -19643,7 +19713,7 @@ function piUserDir2() {
|
|
|
19643
19713
|
function readTrimmed(path) {
|
|
19644
19714
|
try {
|
|
19645
19715
|
if (!existsSync5(path)) return void 0;
|
|
19646
|
-
const content =
|
|
19716
|
+
const content = readFileSync6(path, "utf8").trim();
|
|
19647
19717
|
return content.length > 0 ? content : void 0;
|
|
19648
19718
|
} catch {
|
|
19649
19719
|
return void 0;
|
|
@@ -34267,7 +34337,7 @@ config(en_default());
|
|
|
34267
34337
|
// src/typebox-to-zod.ts
|
|
34268
34338
|
function jsonSchemaPropertyToZod(prop) {
|
|
34269
34339
|
let base;
|
|
34270
|
-
if (Array.isArray(prop.enum)) base = external_exports.enum(prop.enum);
|
|
34340
|
+
if (Array.isArray(prop.enum) && prop.enum.length > 0) base = external_exports.enum(prop.enum);
|
|
34271
34341
|
else switch (prop.type) {
|
|
34272
34342
|
case "string":
|
|
34273
34343
|
base = external_exports.string();
|
|
@@ -34279,12 +34349,22 @@ function jsonSchemaPropertyToZod(prop) {
|
|
|
34279
34349
|
case "boolean":
|
|
34280
34350
|
base = external_exports.boolean();
|
|
34281
34351
|
break;
|
|
34282
|
-
case "array":
|
|
34352
|
+
case "array": {
|
|
34283
34353
|
base = prop.items ? external_exports.array(jsonSchemaPropertyToZod(prop.items)) : external_exports.array(external_exports.unknown());
|
|
34354
|
+
const minItems = typeof prop.minItems === "number" ? prop.minItems : void 0;
|
|
34355
|
+
if (minItems !== void 0) base = base.min(minItems);
|
|
34284
34356
|
break;
|
|
34285
|
-
|
|
34286
|
-
|
|
34357
|
+
}
|
|
34358
|
+
case "object": {
|
|
34359
|
+
if (prop.properties && typeof prop.properties === "object" && !Array.isArray(prop.properties)) {
|
|
34360
|
+
base = external_exports.object(jsonSchemaToZodShape(prop));
|
|
34361
|
+
if (prop.additionalProperties === false) base = base.strict();
|
|
34362
|
+
else if (prop.additionalProperties === true) base = base.passthrough();
|
|
34363
|
+
} else {
|
|
34364
|
+
base = external_exports.record(external_exports.string(), external_exports.unknown());
|
|
34365
|
+
}
|
|
34287
34366
|
break;
|
|
34367
|
+
}
|
|
34288
34368
|
default:
|
|
34289
34369
|
base = external_exports.unknown();
|
|
34290
34370
|
}
|
|
@@ -34423,6 +34503,88 @@ var CLAUDE_BRIDGE_TOOL_ISOLATION = {
|
|
|
34423
34503
|
allowedTools: [`mcp__${MCP_SERVER_NAME}__*`]
|
|
34424
34504
|
};
|
|
34425
34505
|
var sharedSession = null;
|
|
34506
|
+
var extensionApi;
|
|
34507
|
+
var BRIDGE_SESSION_CUSTOM_TYPE = "claude-bridge-session";
|
|
34508
|
+
function fingerprintMessages(messages) {
|
|
34509
|
+
const normalized = messages.map((message) => {
|
|
34510
|
+
if (message.role === "assistant") {
|
|
34511
|
+
return {
|
|
34512
|
+
role: message.role,
|
|
34513
|
+
provider: message.provider,
|
|
34514
|
+
model: message.model,
|
|
34515
|
+
content: message.content
|
|
34516
|
+
};
|
|
34517
|
+
}
|
|
34518
|
+
return message;
|
|
34519
|
+
});
|
|
34520
|
+
return createHash("sha256").update(JSON.stringify(normalized)).digest("hex");
|
|
34521
|
+
}
|
|
34522
|
+
function readBuiltSessionContext(sessionManager) {
|
|
34523
|
+
const built = typeof sessionManager?.buildSessionContext === "function" ? sessionManager.buildSessionContext() : void 0;
|
|
34524
|
+
return Array.isArray(built?.messages) ? built : void 0;
|
|
34525
|
+
}
|
|
34526
|
+
function latestPersistedBridgeSession(sessionManager) {
|
|
34527
|
+
const entries = typeof sessionManager?.getEntries === "function" ? sessionManager.getEntries() : [];
|
|
34528
|
+
if (!Array.isArray(entries)) return void 0;
|
|
34529
|
+
for (let i2 = entries.length - 1; i2 >= 0; i2--) {
|
|
34530
|
+
const entry = entries[i2];
|
|
34531
|
+
if (entry?.type !== "custom" || entry.customType !== BRIDGE_SESSION_CUSTOM_TYPE) continue;
|
|
34532
|
+
const data = entry.data;
|
|
34533
|
+
if (!data || typeof data.sessionId !== "string" || typeof data.cursor !== "number" || typeof data.cwd !== "string" || typeof data.fingerprint !== "string") continue;
|
|
34534
|
+
return data;
|
|
34535
|
+
}
|
|
34536
|
+
return void 0;
|
|
34537
|
+
}
|
|
34538
|
+
function claudeSessionExists(sessionId, cwd) {
|
|
34539
|
+
try {
|
|
34540
|
+
const session = openSession({ sessionId, projectPath: cwd, claudeDir: process.env.CLAUDE_CONFIG_DIR });
|
|
34541
|
+
statSync3(session.jsonlPath);
|
|
34542
|
+
return true;
|
|
34543
|
+
} catch {
|
|
34544
|
+
return false;
|
|
34545
|
+
}
|
|
34546
|
+
}
|
|
34547
|
+
function restoreSharedSessionFromPi(ctx2) {
|
|
34548
|
+
const persisted = latestPersistedBridgeSession(ctx2.sessionManager);
|
|
34549
|
+
if (!persisted) return;
|
|
34550
|
+
const built = readBuiltSessionContext(ctx2.sessionManager);
|
|
34551
|
+
if (!built) return;
|
|
34552
|
+
const cursor = Math.max(0, Math.min(persisted.cursor, built.messages.length));
|
|
34553
|
+
const fingerprint = fingerprintMessages(built.messages.slice(0, cursor));
|
|
34554
|
+
if (fingerprint !== persisted.fingerprint) {
|
|
34555
|
+
debug(`restoreSharedSession: fingerprint mismatch for ${persisted.sessionId.slice(0, 8)}`);
|
|
34556
|
+
return;
|
|
34557
|
+
}
|
|
34558
|
+
if (!claudeSessionExists(persisted.sessionId, persisted.cwd)) {
|
|
34559
|
+
debug(`restoreSharedSession: Claude session missing for ${persisted.sessionId.slice(0, 8)}`);
|
|
34560
|
+
return;
|
|
34561
|
+
}
|
|
34562
|
+
sharedSession = { sessionId: persisted.sessionId, cursor, cwd: persisted.cwd };
|
|
34563
|
+
debug(`restoreSharedSession: restored ${persisted.sessionId.slice(0, 8)}, cursor=${cursor}`);
|
|
34564
|
+
}
|
|
34565
|
+
function schedulePersistSharedSession(ctxLike) {
|
|
34566
|
+
if (!extensionApi || !sharedSession || !ctxLike?.sessionManager) return;
|
|
34567
|
+
const snapshot = { ...sharedSession };
|
|
34568
|
+
const timer = setTimeout(() => {
|
|
34569
|
+
try {
|
|
34570
|
+
const built = readBuiltSessionContext(ctxLike.sessionManager);
|
|
34571
|
+
if (!built) return;
|
|
34572
|
+
const cursor = Math.max(0, Math.min(snapshot.cursor, built.messages.length));
|
|
34573
|
+
const data = {
|
|
34574
|
+
...snapshot,
|
|
34575
|
+
cursor,
|
|
34576
|
+
fingerprint: fingerprintMessages(built.messages.slice(0, cursor)),
|
|
34577
|
+
piSessionId: typeof ctxLike.sessionManager?.getSessionId === "function" ? ctxLike.sessionManager.getSessionId() : void 0,
|
|
34578
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
34579
|
+
};
|
|
34580
|
+
extensionApi?.appendEntry(BRIDGE_SESSION_CUSTOM_TYPE, data);
|
|
34581
|
+
debug(`persistSharedSession: saved ${data.sessionId.slice(0, 8)}, cursor=${data.cursor}`);
|
|
34582
|
+
} catch (error51) {
|
|
34583
|
+
debug("persistSharedSession failed:", error51);
|
|
34584
|
+
}
|
|
34585
|
+
}, 0);
|
|
34586
|
+
timer.unref?.();
|
|
34587
|
+
}
|
|
34426
34588
|
function convertAndImportMessages(session, messages, customToolNameToSdk) {
|
|
34427
34589
|
const { anthropicMessages, sanitizedIds } = convertPiMessages(messages, customToolNameToSdk);
|
|
34428
34590
|
debug(`convertAndImportMessages: ${messages.length} pi msgs \u2192 ${anthropicMessages.length} anthropic msgs`);
|
|
@@ -35138,6 +35300,7 @@ function streamClaudeAgentSdk(model, context, options) {
|
|
|
35138
35300
|
return stream;
|
|
35139
35301
|
}
|
|
35140
35302
|
function index_default(pi) {
|
|
35303
|
+
extensionApi = pi;
|
|
35141
35304
|
process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = "1";
|
|
35142
35305
|
const config2 = loadConfig(process.cwd());
|
|
35143
35306
|
debug("loadConfig:", JSON.stringify(config2));
|
|
@@ -35159,8 +35322,13 @@ function index_default(pi) {
|
|
|
35159
35322
|
if (event.reason === "new" || event.reason === "resume" || event.reason === "fork") {
|
|
35160
35323
|
clearSession(`session_start:${event.reason}`);
|
|
35161
35324
|
}
|
|
35325
|
+
if (event.reason === "startup" || event.reason === "resume" || event.reason === "fork") restoreSharedSessionFromPi(ctx2);
|
|
35162
35326
|
});
|
|
35163
35327
|
pi.on("session_shutdown", () => clearSession("session_shutdown"));
|
|
35328
|
+
pi.on("message_end", (event, ctx2) => {
|
|
35329
|
+
const message = event.message;
|
|
35330
|
+
if (message?.role === "assistant" && message.provider === PROVIDER_ID) schedulePersistSharedSession(ctx2);
|
|
35331
|
+
});
|
|
35164
35332
|
const markRebuild = (event) => {
|
|
35165
35333
|
if (sharedSession) {
|
|
35166
35334
|
debug(`${event}: marking needsRebuild on session ${sharedSession.sessionId.slice(0, 8)}`);
|
package/package.json
CHANGED
package/src/convert.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Extracted so they can be tested without pulling in the full extension runtime.
|
|
3
3
|
|
|
4
4
|
import type { Message as PiMessage } from "@earendil-works/pi-ai";
|
|
5
|
-
import type { Message as SessionMessage } from "cc-session-io";
|
|
5
|
+
import type { ContentBlock, Message as SessionMessage } from "cc-session-io";
|
|
6
6
|
import { pascalCase } from "change-case";
|
|
7
7
|
|
|
8
8
|
export const PROVIDER_ID = "claude-bridge";
|
|
@@ -44,6 +44,42 @@ export function messageContentToText(
|
|
|
44
44
|
return hasText ? parts.join("\n") : "";
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
function imageBlockToAnthropic(block: { data?: string; mimeType?: string }): ContentBlock | undefined {
|
|
48
|
+
if (!block.data || !block.mimeType) return undefined;
|
|
49
|
+
return { type: "image", source: { type: "base64", media_type: block.mimeType, data: block.data } } as ContentBlock;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function toolResultContentToAnthropic(
|
|
53
|
+
content: string | Array<{ type: string; text?: string; data?: string; mimeType?: string }>,
|
|
54
|
+
): string | ContentBlock[] {
|
|
55
|
+
if (typeof content === "string") return content;
|
|
56
|
+
if (!Array.isArray(content)) return "";
|
|
57
|
+
const blocks: ContentBlock[] = [];
|
|
58
|
+
for (const block of content) {
|
|
59
|
+
if (block.type === "text" && block.text) {
|
|
60
|
+
blocks.push({ type: "text", text: block.text });
|
|
61
|
+
} else if (block.type === "image") {
|
|
62
|
+
const image = imageBlockToAnthropic(block);
|
|
63
|
+
if (image) blocks.push(image);
|
|
64
|
+
} else if (block.type) {
|
|
65
|
+
blocks.push({ type: "text", text: `[${block.type}]` });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (blocks.length === 0) return "";
|
|
69
|
+
if (blocks.every((block) => block.type === "text")) return blocks.map((block) => (block as { text: string }).text).join("\n");
|
|
70
|
+
return blocks;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function assistantProvenancePrefix(msg: PiMessage): string | undefined {
|
|
74
|
+
if (msg.role !== "assistant") return undefined;
|
|
75
|
+
const provider = typeof (msg as any).provider === "string" ? (msg as any).provider : undefined;
|
|
76
|
+
const model = typeof (msg as any).model === "string" ? (msg as any).model : undefined;
|
|
77
|
+
const api = typeof (msg as any).api === "string" ? (msg as any).api : undefined;
|
|
78
|
+
if (!provider && !model && !api) return undefined;
|
|
79
|
+
if (provider === PROVIDER_ID || api === "anthropic") return undefined;
|
|
80
|
+
return `[Prior Pi assistant response from ${provider ?? api ?? "unknown-provider"}${model ? `/${model}` : ""}]\n`;
|
|
81
|
+
}
|
|
82
|
+
|
|
47
83
|
/** Convert pi message array to Anthropic API format. */
|
|
48
84
|
export function convertPiMessages(
|
|
49
85
|
messages: PiMessage[],
|
|
@@ -61,16 +97,18 @@ export function convertPiMessages(
|
|
|
61
97
|
for (const block of msg.content) {
|
|
62
98
|
if (block.type === "text" && block.text) parts.push({ type: "text", text: block.text });
|
|
63
99
|
else if (block.type === "image" && block.data && block.mimeType) {
|
|
64
|
-
parts.push(
|
|
100
|
+
parts.push(imageBlockToAnthropic(block));
|
|
65
101
|
}
|
|
66
102
|
}
|
|
67
|
-
anthropicMessages.push({ role: "user", content: parts.length ? parts : "[image]" });
|
|
103
|
+
anthropicMessages.push({ role: "user", content: parts.filter(Boolean).length ? parts.filter(Boolean) as ContentBlock[] : "[image]" });
|
|
68
104
|
} else {
|
|
69
105
|
anthropicMessages.push({ role: "user", content: "[empty]" });
|
|
70
106
|
}
|
|
71
107
|
} else if (msg.role === "assistant") {
|
|
72
108
|
const content = Array.isArray(msg.content) ? msg.content : [];
|
|
73
109
|
const blocks = [];
|
|
110
|
+
const provenance = assistantProvenancePrefix(msg);
|
|
111
|
+
if (provenance) blocks.push({ type: "text", text: provenance });
|
|
74
112
|
for (const block of content) {
|
|
75
113
|
if (block.type === "text" && block.text) {
|
|
76
114
|
blocks.push({ type: "text", text: block.text });
|
|
@@ -88,10 +126,10 @@ export function convertPiMessages(
|
|
|
88
126
|
if (!blocks.length) blocks.push({ type: "text", text: "[incompatible content omitted]" });
|
|
89
127
|
anthropicMessages.push({ role: "assistant", content: blocks });
|
|
90
128
|
} else if (msg.role === "toolResult") {
|
|
91
|
-
const
|
|
129
|
+
const content = toolResultContentToAnthropic(msg.content);
|
|
92
130
|
anthropicMessages.push({
|
|
93
131
|
role: "user",
|
|
94
|
-
content: [{ type: "tool_result", tool_use_id: sanitizeToolId(msg.toolCallId, sanitizedIds), content:
|
|
132
|
+
content: [{ type: "tool_result", tool_use_id: sanitizeToolId(msg.toolCallId, sanitizedIds), content: content || "", is_error: msg.isError }],
|
|
95
133
|
});
|
|
96
134
|
}
|
|
97
135
|
}
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,8 @@ import * as piAi from "@earendil-works/pi-ai";
|
|
|
3
3
|
import { type ExtensionAPI, type ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { createSdkMcpServer, query, type EffortLevel, type SDKMessage, type SDKUserMessage, type SettingSource } from "@anthropic-ai/claude-agent-sdk";
|
|
5
5
|
import type { Base64ImageSource, ContentBlockParam, MessageParam } from "@anthropic-ai/sdk/resources";
|
|
6
|
-
import { createSession, deleteSession, repairToolPairing } from "cc-session-io";
|
|
6
|
+
import { createSession, deleteSession, openSession, repairToolPairing } from "cc-session-io";
|
|
7
|
+
import { createHash } from "crypto";
|
|
7
8
|
import { accessSync, appendFileSync, constants as fsConstants, mkdirSync, realpathSync, statSync } from "fs";
|
|
8
9
|
import { homedir } from "os";
|
|
9
10
|
import { delimiter, dirname, join } from "path";
|
|
@@ -184,12 +185,107 @@ interface SessionState {
|
|
|
184
185
|
}
|
|
185
186
|
|
|
186
187
|
let sharedSession: SessionState | null = null;
|
|
188
|
+
let extensionApi: ExtensionAPI | undefined;
|
|
189
|
+
|
|
190
|
+
const BRIDGE_SESSION_CUSTOM_TYPE = "claude-bridge-session";
|
|
191
|
+
|
|
192
|
+
interface PersistedBridgeSessionState extends SessionState {
|
|
193
|
+
fingerprint: string;
|
|
194
|
+
piSessionId?: string;
|
|
195
|
+
updatedAt: string;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function fingerprintMessages(messages: Context["messages"]): string {
|
|
199
|
+
const normalized = messages.map((message) => {
|
|
200
|
+
if (message.role === "assistant") {
|
|
201
|
+
return {
|
|
202
|
+
role: message.role,
|
|
203
|
+
provider: (message as AssistantMessage).provider,
|
|
204
|
+
model: (message as AssistantMessage).model,
|
|
205
|
+
content: (message as AssistantMessage).content,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
return message;
|
|
209
|
+
});
|
|
210
|
+
return createHash("sha256").update(JSON.stringify(normalized)).digest("hex");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function readBuiltSessionContext(sessionManager: unknown): { messages: Context["messages"] } | undefined {
|
|
214
|
+
const built = typeof (sessionManager as any)?.buildSessionContext === "function" ? (sessionManager as any).buildSessionContext() : undefined;
|
|
215
|
+
return Array.isArray(built?.messages) ? built as { messages: Context["messages"] } : undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function latestPersistedBridgeSession(sessionManager: unknown): PersistedBridgeSessionState | undefined {
|
|
219
|
+
const entries = typeof (sessionManager as any)?.getEntries === "function" ? (sessionManager as any).getEntries() : [];
|
|
220
|
+
if (!Array.isArray(entries)) return undefined;
|
|
221
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
222
|
+
const entry = entries[i];
|
|
223
|
+
if (entry?.type !== "custom" || entry.customType !== BRIDGE_SESSION_CUSTOM_TYPE) continue;
|
|
224
|
+
const data = entry.data as Partial<PersistedBridgeSessionState> | undefined;
|
|
225
|
+
if (!data || typeof data.sessionId !== "string" || typeof data.cursor !== "number" || typeof data.cwd !== "string" || typeof data.fingerprint !== "string") continue;
|
|
226
|
+
return data as PersistedBridgeSessionState;
|
|
227
|
+
}
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function claudeSessionExists(sessionId: string, cwd: string): boolean {
|
|
232
|
+
try {
|
|
233
|
+
const session = openSession({ sessionId, projectPath: cwd, claudeDir: process.env.CLAUDE_CONFIG_DIR });
|
|
234
|
+
statSync(session.jsonlPath);
|
|
235
|
+
return true;
|
|
236
|
+
} catch {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function restoreSharedSessionFromPi(ctx: { sessionManager?: unknown }): void {
|
|
242
|
+
const persisted = latestPersistedBridgeSession(ctx.sessionManager);
|
|
243
|
+
if (!persisted) return;
|
|
244
|
+
const built = readBuiltSessionContext(ctx.sessionManager);
|
|
245
|
+
if (!built) return;
|
|
246
|
+
const cursor = Math.max(0, Math.min(persisted.cursor, built.messages.length));
|
|
247
|
+
const fingerprint = fingerprintMessages(built.messages.slice(0, cursor));
|
|
248
|
+
if (fingerprint !== persisted.fingerprint) {
|
|
249
|
+
debug(`restoreSharedSession: fingerprint mismatch for ${persisted.sessionId.slice(0, 8)}`);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (!claudeSessionExists(persisted.sessionId, persisted.cwd)) {
|
|
253
|
+
debug(`restoreSharedSession: Claude session missing for ${persisted.sessionId.slice(0, 8)}`);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
sharedSession = { sessionId: persisted.sessionId, cursor, cwd: persisted.cwd };
|
|
257
|
+
debug(`restoreSharedSession: restored ${persisted.sessionId.slice(0, 8)}, cursor=${cursor}`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function schedulePersistSharedSession(ctxLike?: { sessionManager?: unknown }): void {
|
|
261
|
+
if (!extensionApi || !sharedSession || !ctxLike?.sessionManager) return;
|
|
262
|
+
const snapshot = { ...sharedSession };
|
|
263
|
+
const timer = setTimeout(() => {
|
|
264
|
+
try {
|
|
265
|
+
const built = readBuiltSessionContext(ctxLike.sessionManager);
|
|
266
|
+
if (!built) return;
|
|
267
|
+
const cursor = Math.max(0, Math.min(snapshot.cursor, built.messages.length));
|
|
268
|
+
const data: PersistedBridgeSessionState = {
|
|
269
|
+
...snapshot,
|
|
270
|
+
cursor,
|
|
271
|
+
fingerprint: fingerprintMessages(built.messages.slice(0, cursor)),
|
|
272
|
+
piSessionId: typeof (ctxLike.sessionManager as any)?.getSessionId === "function" ? (ctxLike.sessionManager as any).getSessionId() : undefined,
|
|
273
|
+
updatedAt: new Date().toISOString(),
|
|
274
|
+
};
|
|
275
|
+
extensionApi?.appendEntry(BRIDGE_SESSION_CUSTOM_TYPE, data);
|
|
276
|
+
debug(`persistSharedSession: saved ${data.sessionId.slice(0, 8)}, cursor=${data.cursor}`);
|
|
277
|
+
} catch (error) {
|
|
278
|
+
debug("persistSharedSession failed:", error);
|
|
279
|
+
}
|
|
280
|
+
}, 0);
|
|
281
|
+
timer.unref?.();
|
|
282
|
+
}
|
|
187
283
|
|
|
188
284
|
// Convert pi messages to Anthropic API format for session import.
|
|
189
|
-
// Lossy: non-Anthropic thinking blocks are dropped (no valid signature)
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
285
|
+
// Lossy: non-Anthropic thinking blocks are dropped (no valid signature). User and
|
|
286
|
+
// tool-result image blocks are preserved when possible. If assistant blocks are
|
|
287
|
+
// otherwise incompatible, convertPiMessages emits a text placeholder so the record
|
|
288
|
+
// sequence stays valid before repairToolPairing runs.
|
|
193
289
|
function convertAndImportMessages(
|
|
194
290
|
session: ReturnType<typeof createSession>,
|
|
195
291
|
messages: Context["messages"],
|
|
@@ -1166,6 +1262,7 @@ function streamClaudeAgentSdk(model: Model<any>, context: Context, options?: Sim
|
|
|
1166
1262
|
// --- Extension registration ---
|
|
1167
1263
|
|
|
1168
1264
|
export default function (pi: ExtensionAPI) {
|
|
1265
|
+
extensionApi = pi;
|
|
1169
1266
|
// Disable non-essential Claude Code traffic (update checks, MCP registry, telemetry)
|
|
1170
1267
|
process.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = "1";
|
|
1171
1268
|
|
|
@@ -1195,8 +1292,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
1195
1292
|
if (event.reason === "new" || event.reason === "resume" || event.reason === "fork") {
|
|
1196
1293
|
clearSession(`session_start:${event.reason}`);
|
|
1197
1294
|
}
|
|
1295
|
+
if (event.reason === "startup" || event.reason === "resume" || event.reason === "fork") restoreSharedSessionFromPi(ctx);
|
|
1198
1296
|
});
|
|
1199
1297
|
pi.on("session_shutdown", () => clearSession("session_shutdown"));
|
|
1298
|
+
pi.on("message_end", (event, ctx) => {
|
|
1299
|
+
const message = (event as { message?: AssistantMessage }).message;
|
|
1300
|
+
if (message?.role === "assistant" && message.provider === PROVIDER_ID) schedulePersistSharedSession(ctx);
|
|
1301
|
+
});
|
|
1200
1302
|
|
|
1201
1303
|
// pi /compact and session-tree navigation (rewind / fork-at-point /
|
|
1202
1304
|
// branch switch) both mutate pi's messages array out from under the
|
package/src/typebox-to-zod.ts
CHANGED
|
@@ -13,15 +13,29 @@ import { z } from "zod";
|
|
|
13
13
|
|
|
14
14
|
export function jsonSchemaPropertyToZod(prop: Record<string, unknown>): z.ZodTypeAny {
|
|
15
15
|
let base: z.ZodTypeAny;
|
|
16
|
-
if (Array.isArray(prop.enum)) base = z.enum(prop.enum as [string, ...string[]]);
|
|
16
|
+
if (Array.isArray(prop.enum) && prop.enum.length > 0) base = z.enum(prop.enum as [string, ...string[]]);
|
|
17
17
|
else switch (prop.type) {
|
|
18
18
|
case "string": base = z.string(); break;
|
|
19
19
|
case "number": case "integer": base = z.number(); break;
|
|
20
20
|
case "boolean": base = z.boolean(); break;
|
|
21
|
-
case "array":
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
case "array": {
|
|
22
|
+
base = prop.items
|
|
23
|
+
? z.array(jsonSchemaPropertyToZod(prop.items as Record<string, unknown>))
|
|
24
|
+
: z.array(z.unknown());
|
|
25
|
+
const minItems = typeof prop.minItems === "number" ? prop.minItems : undefined;
|
|
26
|
+
if (minItems !== undefined) base = (base as z.ZodArray<z.ZodTypeAny>).min(minItems);
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
case "object": {
|
|
30
|
+
if (prop.properties && typeof prop.properties === "object" && !Array.isArray(prop.properties)) {
|
|
31
|
+
base = z.object(jsonSchemaToZodShape(prop));
|
|
32
|
+
if (prop.additionalProperties === false) base = (base as z.ZodObject<Record<string, z.ZodTypeAny>>).strict();
|
|
33
|
+
else if (prop.additionalProperties === true) base = (base as z.ZodObject<Record<string, z.ZodTypeAny>>).passthrough();
|
|
34
|
+
} else {
|
|
35
|
+
base = z.record(z.string(), z.unknown());
|
|
36
|
+
}
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
25
39
|
default: base = z.unknown();
|
|
26
40
|
}
|
|
27
41
|
if (typeof prop.description === "string") base = base.describe(prop.description);
|